@buoy-gg/shared-ui 7.0.34 → 7.0.36

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 (207) hide show
  1. package/lib/commonjs/dataViewer/CyberpunkInput.js +3 -2
  2. package/lib/commonjs/dataViewer/DataTreeActionBar.js +3 -2
  3. package/lib/commonjs/dataViewer/DataTreeActionDock.js +2 -1
  4. package/lib/commonjs/dataViewer/DiffSummary.js +4 -3
  5. package/lib/commonjs/dataViewer/LiveExplorer.js +11 -10
  6. package/lib/commonjs/dataViewer/SplitDiffViewer.js +10 -9
  7. package/lib/commonjs/dataViewer/ThemedSplitView.js +12 -11
  8. package/lib/commonjs/dataViewer/ValueEditModal.js +6 -5
  9. package/lib/commonjs/dataViewer/VirtualizedDataExplorer.js +4 -3
  10. package/lib/commonjs/dataViewer/tree/TreeDiffViewer.js +15 -14
  11. package/lib/commonjs/hooks/safe-area-impl.js +1 -1
  12. package/lib/commonjs/icons/index.js +540 -1
  13. package/lib/commonjs/index.js +48 -0
  14. package/lib/commonjs/license/FreeAccountPrompt.js +2 -1
  15. package/lib/commonjs/license/LicenseEntryModal.js +2 -1
  16. package/lib/commonjs/sync/keyedArrays.js +209 -0
  17. package/lib/commonjs/sync/toolAttention.test.js +51 -0
  18. package/lib/commonjs/sync/toolUiBridge.js +58 -0
  19. package/lib/commonjs/sync/typedEdit.js +228 -0
  20. package/lib/commonjs/sync/valuePath.js +337 -0
  21. package/lib/commonjs/sync/valueShape.js +161 -0
  22. package/lib/commonjs/ui/components/ChangeRow.js +2 -1
  23. package/lib/commonjs/ui/components/ClipboardHintBanner.js +2 -1
  24. package/lib/commonjs/ui/components/CompactRow.js +3 -2
  25. package/lib/commonjs/ui/components/DataInspector.js +3 -2
  26. package/lib/commonjs/ui/components/DetailView.js +2 -1
  27. package/lib/commonjs/ui/components/DynamicFilterView.js +8 -7
  28. package/lib/commonjs/ui/components/EntityFilterView.js +9 -8
  29. package/lib/commonjs/ui/components/EventHistoryViewer/CompareBar.js +5 -4
  30. package/lib/commonjs/ui/components/EventHistoryViewer/DiffModeTabs.js +2 -1
  31. package/lib/commonjs/ui/components/EventHistoryViewer/EventPickerModal.js +5 -4
  32. package/lib/commonjs/ui/components/EventStepperFooter.js +4 -3
  33. package/lib/commonjs/ui/components/ExpandablePopover.js +48 -8
  34. package/lib/commonjs/ui/components/ExpandedInfoRow.js +3 -2
  35. package/lib/commonjs/ui/components/FilterComponents.js +2 -1
  36. package/lib/commonjs/ui/components/InfoRow.js +2 -1
  37. package/lib/commonjs/ui/components/Pill.js +2 -1
  38. package/lib/commonjs/ui/components/ToolFooter.js +3 -2
  39. package/lib/commonjs/ui/console/CyberpunkSectionButton.js +3 -2
  40. package/lib/commonjs/ui/fonts.js +12 -0
  41. package/lib/commonjs/ui/gameUI/components/GameUICollapsibleSection.js +4 -3
  42. package/lib/commonjs/ui/gameUI/components/GameUICompactStats.js +12 -11
  43. package/lib/commonjs/ui/gameUI/components/GameUIIssuesList.js +9 -8
  44. package/lib/commonjs/ui/gameUI/components/GameUIStatusHeader.js +4 -3
  45. package/lib/commonjs/ui/index.js +8 -0
  46. package/lib/commonjs/ui/toolTheme.js +4 -3
  47. package/lib/module/dataViewer/CyberpunkInput.js +3 -2
  48. package/lib/module/dataViewer/DataTreeActionBar.js +3 -2
  49. package/lib/module/dataViewer/DataTreeActionDock.js +2 -1
  50. package/lib/module/dataViewer/DiffSummary.js +4 -3
  51. package/lib/module/dataViewer/LiveExplorer.js +11 -11
  52. package/lib/module/dataViewer/SplitDiffViewer.js +10 -9
  53. package/lib/module/dataViewer/ThemedSplitView.js +12 -11
  54. package/lib/module/dataViewer/ValueEditModal.js +6 -5
  55. package/lib/module/dataViewer/VirtualizedDataExplorer.js +5 -4
  56. package/lib/module/dataViewer/tree/TreeDiffViewer.js +15 -15
  57. package/lib/module/hooks/safe-area-impl.js +1 -1
  58. package/lib/module/icons/index.js +29 -2
  59. package/lib/module/index.js +4 -0
  60. package/lib/module/license/FreeAccountPrompt.js +2 -2
  61. package/lib/module/license/LicenseEntryModal.js +3 -3
  62. package/lib/module/sync/keyedArrays.js +204 -0
  63. package/lib/module/sync/toolAttention.test.js +50 -0
  64. package/lib/module/sync/toolUiBridge.js +55 -0
  65. package/lib/module/sync/typedEdit.js +220 -0
  66. package/lib/module/sync/valuePath.js +330 -0
  67. package/lib/module/sync/valueShape.js +153 -0
  68. package/lib/module/ui/components/ChangeRow.js +2 -1
  69. package/lib/module/ui/components/ClipboardHintBanner.js +2 -1
  70. package/lib/module/ui/components/CompactRow.js +3 -2
  71. package/lib/module/ui/components/DataInspector.js +3 -2
  72. package/lib/module/ui/components/DetailView.js +2 -1
  73. package/lib/module/ui/components/DynamicFilterView.js +8 -7
  74. package/lib/module/ui/components/EntityFilterView.js +9 -8
  75. package/lib/module/ui/components/EventHistoryViewer/CompareBar.js +6 -5
  76. package/lib/module/ui/components/EventHistoryViewer/DiffModeTabs.js +3 -2
  77. package/lib/module/ui/components/EventHistoryViewer/EventPickerModal.js +6 -5
  78. package/lib/module/ui/components/EventStepperFooter.js +4 -3
  79. package/lib/module/ui/components/ExpandablePopover.js +48 -8
  80. package/lib/module/ui/components/ExpandedInfoRow.js +3 -2
  81. package/lib/module/ui/components/FilterComponents.js +2 -2
  82. package/lib/module/ui/components/InfoRow.js +2 -1
  83. package/lib/module/ui/components/Pill.js +2 -1
  84. package/lib/module/ui/components/ToolFooter.js +3 -2
  85. package/lib/module/ui/console/CyberpunkSectionButton.js +3 -2
  86. package/lib/module/ui/fonts.js +8 -0
  87. package/lib/module/ui/gameUI/components/GameUICollapsibleSection.js +4 -3
  88. package/lib/module/ui/gameUI/components/GameUICompactStats.js +12 -11
  89. package/lib/module/ui/gameUI/components/GameUIIssuesList.js +9 -8
  90. package/lib/module/ui/gameUI/components/GameUIStatusHeader.js +4 -3
  91. package/lib/module/ui/index.js +3 -0
  92. package/lib/module/ui/toolTheme.js +4 -4
  93. package/lib/typescript/commonjs/dataViewer/CyberpunkInput.d.ts.map +1 -1
  94. package/lib/typescript/commonjs/dataViewer/DataTreeActionBar.d.ts.map +1 -1
  95. package/lib/typescript/commonjs/dataViewer/DataTreeActionDock.d.ts.map +1 -1
  96. package/lib/typescript/commonjs/dataViewer/DiffSummary.d.ts.map +1 -1
  97. package/lib/typescript/commonjs/dataViewer/LiveExplorer.d.ts.map +1 -1
  98. package/lib/typescript/commonjs/dataViewer/SplitDiffViewer.d.ts.map +1 -1
  99. package/lib/typescript/commonjs/dataViewer/ThemedSplitView.d.ts.map +1 -1
  100. package/lib/typescript/commonjs/dataViewer/ValueEditModal.d.ts.map +1 -1
  101. package/lib/typescript/commonjs/dataViewer/VirtualizedDataExplorer.d.ts.map +1 -1
  102. package/lib/typescript/commonjs/dataViewer/tree/TreeDiffViewer.d.ts.map +1 -1
  103. package/lib/typescript/commonjs/hooks/safe-area-impl.d.ts +1 -1
  104. package/lib/typescript/commonjs/icons/index.d.ts +4 -1
  105. package/lib/typescript/commonjs/icons/index.d.ts.map +1 -1
  106. package/lib/typescript/commonjs/index.d.ts +4 -0
  107. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  108. package/lib/typescript/commonjs/license/FreeAccountPrompt.d.ts.map +1 -1
  109. package/lib/typescript/commonjs/license/LicenseEntryModal.d.ts.map +1 -1
  110. package/lib/typescript/commonjs/sync/keyedArrays.d.ts +35 -0
  111. package/lib/typescript/commonjs/sync/keyedArrays.d.ts.map +1 -0
  112. package/lib/typescript/commonjs/sync/toolAttention.test.d.ts +2 -0
  113. package/lib/typescript/commonjs/sync/toolAttention.test.d.ts.map +1 -0
  114. package/lib/typescript/commonjs/sync/toolUiBridge.d.ts +25 -0
  115. package/lib/typescript/commonjs/sync/toolUiBridge.d.ts.map +1 -1
  116. package/lib/typescript/commonjs/sync/typedEdit.d.ts +70 -0
  117. package/lib/typescript/commonjs/sync/typedEdit.d.ts.map +1 -0
  118. package/lib/typescript/commonjs/sync/valuePath.d.ts +52 -0
  119. package/lib/typescript/commonjs/sync/valuePath.d.ts.map +1 -0
  120. package/lib/typescript/commonjs/sync/valueShape.d.ts +77 -0
  121. package/lib/typescript/commonjs/sync/valueShape.d.ts.map +1 -0
  122. package/lib/typescript/commonjs/ui/components/ChangeRow.d.ts.map +1 -1
  123. package/lib/typescript/commonjs/ui/components/ClipboardHintBanner.d.ts.map +1 -1
  124. package/lib/typescript/commonjs/ui/components/CompactRow.d.ts.map +1 -1
  125. package/lib/typescript/commonjs/ui/components/DataInspector.d.ts.map +1 -1
  126. package/lib/typescript/commonjs/ui/components/DetailView.d.ts.map +1 -1
  127. package/lib/typescript/commonjs/ui/components/DynamicFilterView.d.ts.map +1 -1
  128. package/lib/typescript/commonjs/ui/components/EntityFilterView.d.ts.map +1 -1
  129. package/lib/typescript/commonjs/ui/components/EventHistoryViewer/CompareBar.d.ts.map +1 -1
  130. package/lib/typescript/commonjs/ui/components/EventHistoryViewer/DiffModeTabs.d.ts.map +1 -1
  131. package/lib/typescript/commonjs/ui/components/EventHistoryViewer/EventPickerModal.d.ts.map +1 -1
  132. package/lib/typescript/commonjs/ui/components/EventStepperFooter.d.ts.map +1 -1
  133. package/lib/typescript/commonjs/ui/components/ExpandablePopover.d.ts +18 -1
  134. package/lib/typescript/commonjs/ui/components/ExpandablePopover.d.ts.map +1 -1
  135. package/lib/typescript/commonjs/ui/components/ExpandedInfoRow.d.ts.map +1 -1
  136. package/lib/typescript/commonjs/ui/components/FilterComponents.d.ts.map +1 -1
  137. package/lib/typescript/commonjs/ui/components/InfoRow.d.ts.map +1 -1
  138. package/lib/typescript/commonjs/ui/components/Pill.d.ts.map +1 -1
  139. package/lib/typescript/commonjs/ui/components/ToolFooter.d.ts.map +1 -1
  140. package/lib/typescript/commonjs/ui/console/CyberpunkSectionButton.d.ts.map +1 -1
  141. package/lib/typescript/commonjs/ui/fonts.d.ts +7 -0
  142. package/lib/typescript/commonjs/ui/fonts.d.ts.map +1 -0
  143. package/lib/typescript/commonjs/ui/gameUI/components/GameUICollapsibleSection.d.ts.map +1 -1
  144. package/lib/typescript/commonjs/ui/gameUI/components/GameUICompactStats.d.ts.map +1 -1
  145. package/lib/typescript/commonjs/ui/gameUI/components/GameUIIssuesList.d.ts.map +1 -1
  146. package/lib/typescript/commonjs/ui/gameUI/components/GameUIStatusHeader.d.ts.map +1 -1
  147. package/lib/typescript/commonjs/ui/index.d.ts +1 -0
  148. package/lib/typescript/commonjs/ui/index.d.ts.map +1 -1
  149. package/lib/typescript/commonjs/ui/toolTheme.d.ts.map +1 -1
  150. package/lib/typescript/module/dataViewer/CyberpunkInput.d.ts.map +1 -1
  151. package/lib/typescript/module/dataViewer/DataTreeActionBar.d.ts.map +1 -1
  152. package/lib/typescript/module/dataViewer/DataTreeActionDock.d.ts.map +1 -1
  153. package/lib/typescript/module/dataViewer/DiffSummary.d.ts.map +1 -1
  154. package/lib/typescript/module/dataViewer/LiveExplorer.d.ts.map +1 -1
  155. package/lib/typescript/module/dataViewer/SplitDiffViewer.d.ts.map +1 -1
  156. package/lib/typescript/module/dataViewer/ThemedSplitView.d.ts.map +1 -1
  157. package/lib/typescript/module/dataViewer/ValueEditModal.d.ts.map +1 -1
  158. package/lib/typescript/module/dataViewer/VirtualizedDataExplorer.d.ts.map +1 -1
  159. package/lib/typescript/module/dataViewer/tree/TreeDiffViewer.d.ts.map +1 -1
  160. package/lib/typescript/module/hooks/safe-area-impl.d.ts +1 -1
  161. package/lib/typescript/module/icons/index.d.ts +4 -1
  162. package/lib/typescript/module/icons/index.d.ts.map +1 -1
  163. package/lib/typescript/module/index.d.ts +4 -0
  164. package/lib/typescript/module/index.d.ts.map +1 -1
  165. package/lib/typescript/module/license/FreeAccountPrompt.d.ts.map +1 -1
  166. package/lib/typescript/module/license/LicenseEntryModal.d.ts.map +1 -1
  167. package/lib/typescript/module/sync/keyedArrays.d.ts +35 -0
  168. package/lib/typescript/module/sync/keyedArrays.d.ts.map +1 -0
  169. package/lib/typescript/module/sync/toolAttention.test.d.ts +2 -0
  170. package/lib/typescript/module/sync/toolAttention.test.d.ts.map +1 -0
  171. package/lib/typescript/module/sync/toolUiBridge.d.ts +25 -0
  172. package/lib/typescript/module/sync/toolUiBridge.d.ts.map +1 -1
  173. package/lib/typescript/module/sync/typedEdit.d.ts +70 -0
  174. package/lib/typescript/module/sync/typedEdit.d.ts.map +1 -0
  175. package/lib/typescript/module/sync/valuePath.d.ts +52 -0
  176. package/lib/typescript/module/sync/valuePath.d.ts.map +1 -0
  177. package/lib/typescript/module/sync/valueShape.d.ts +77 -0
  178. package/lib/typescript/module/sync/valueShape.d.ts.map +1 -0
  179. package/lib/typescript/module/ui/components/ChangeRow.d.ts.map +1 -1
  180. package/lib/typescript/module/ui/components/ClipboardHintBanner.d.ts.map +1 -1
  181. package/lib/typescript/module/ui/components/CompactRow.d.ts.map +1 -1
  182. package/lib/typescript/module/ui/components/DataInspector.d.ts.map +1 -1
  183. package/lib/typescript/module/ui/components/DetailView.d.ts.map +1 -1
  184. package/lib/typescript/module/ui/components/DynamicFilterView.d.ts.map +1 -1
  185. package/lib/typescript/module/ui/components/EntityFilterView.d.ts.map +1 -1
  186. package/lib/typescript/module/ui/components/EventHistoryViewer/CompareBar.d.ts.map +1 -1
  187. package/lib/typescript/module/ui/components/EventHistoryViewer/DiffModeTabs.d.ts.map +1 -1
  188. package/lib/typescript/module/ui/components/EventHistoryViewer/EventPickerModal.d.ts.map +1 -1
  189. package/lib/typescript/module/ui/components/EventStepperFooter.d.ts.map +1 -1
  190. package/lib/typescript/module/ui/components/ExpandablePopover.d.ts +18 -1
  191. package/lib/typescript/module/ui/components/ExpandablePopover.d.ts.map +1 -1
  192. package/lib/typescript/module/ui/components/ExpandedInfoRow.d.ts.map +1 -1
  193. package/lib/typescript/module/ui/components/FilterComponents.d.ts.map +1 -1
  194. package/lib/typescript/module/ui/components/InfoRow.d.ts.map +1 -1
  195. package/lib/typescript/module/ui/components/Pill.d.ts.map +1 -1
  196. package/lib/typescript/module/ui/components/ToolFooter.d.ts.map +1 -1
  197. package/lib/typescript/module/ui/console/CyberpunkSectionButton.d.ts.map +1 -1
  198. package/lib/typescript/module/ui/fonts.d.ts +7 -0
  199. package/lib/typescript/module/ui/fonts.d.ts.map +1 -0
  200. package/lib/typescript/module/ui/gameUI/components/GameUICollapsibleSection.d.ts.map +1 -1
  201. package/lib/typescript/module/ui/gameUI/components/GameUICompactStats.d.ts.map +1 -1
  202. package/lib/typescript/module/ui/gameUI/components/GameUIIssuesList.d.ts.map +1 -1
  203. package/lib/typescript/module/ui/gameUI/components/GameUIStatusHeader.d.ts.map +1 -1
  204. package/lib/typescript/module/ui/index.d.ts +1 -0
  205. package/lib/typescript/module/ui/index.d.ts.map +1 -1
  206. package/lib/typescript/module/ui/toolTheme.d.ts.map +1 -1
  207. package/package.json +4 -4
@@ -19,7 +19,7 @@ import React, { useMemo, useState, useEffect, useRef } from "react";
19
19
  import { View, Text, ScrollView, StyleSheet, TouchableOpacity } from "react-native";
20
20
  import { gameUIColors, night } from "@buoy-gg/shared-ui";
21
21
  import { isAbsent } from "../lineDiff.js";
22
-
22
+ import { monoFont } from "../../ui/fonts.js";
23
23
  // ============================================
24
24
  // TYPES & INTERFACES
25
25
  // ============================================
@@ -785,16 +785,16 @@ const styles = StyleSheet.create({
785
785
  summaryIcon: {
786
786
  fontSize: 14,
787
787
  fontWeight: "700",
788
- fontFamily: "monospace"
788
+ fontFamily: monoFont
789
789
  },
790
790
  summaryCount: {
791
791
  fontSize: 13,
792
792
  fontWeight: "600",
793
- fontFamily: "monospace"
793
+ fontFamily: monoFont
794
794
  },
795
795
  summaryLabel: {
796
796
  fontSize: 11,
797
- fontFamily: "monospace",
797
+ fontFamily: monoFont,
798
798
  opacity: 0.9
799
799
  },
800
800
  scrollView: {
@@ -818,7 +818,7 @@ const styles = StyleSheet.create({
818
818
  },
819
819
  lineNumberText: {
820
820
  fontSize: 11,
821
- fontFamily: "monospace",
821
+ fontFamily: monoFont,
822
822
  textAlign: "right"
823
823
  },
824
824
  marker: {
@@ -830,7 +830,7 @@ const styles = StyleSheet.create({
830
830
  },
831
831
  markerText: {
832
832
  fontSize: 12,
833
- fontFamily: "monospace",
833
+ fontFamily: monoFont,
834
834
  fontWeight: "600"
835
835
  },
836
836
  content: {
@@ -851,33 +851,33 @@ const styles = StyleSheet.create({
851
851
  },
852
852
  expandIcon: {
853
853
  fontSize: 11,
854
- fontFamily: "monospace",
854
+ fontFamily: monoFont,
855
855
  fontWeight: "600"
856
856
  },
857
857
  colon: {
858
858
  fontSize: 12,
859
- fontFamily: "monospace",
859
+ fontFamily: monoFont,
860
860
  marginHorizontal: 4
861
861
  },
862
862
  key: {
863
863
  fontSize: 11,
864
- fontFamily: "monospace",
864
+ fontFamily: monoFont,
865
865
  fontWeight: "500",
866
866
  opacity: 0.9
867
867
  },
868
868
  value: {
869
869
  fontSize: 11,
870
- fontFamily: "monospace",
870
+ fontFamily: monoFont,
871
871
  maxWidth: "80%"
872
872
  },
873
873
  arrow: {
874
874
  fontSize: 12,
875
- fontFamily: "monospace",
875
+ fontFamily: monoFont,
876
876
  fontWeight: "600"
877
877
  },
878
878
  badge: {
879
879
  fontSize: 11,
880
- fontFamily: "monospace",
880
+ fontFamily: monoFont,
881
881
  fontWeight: "600",
882
882
  marginLeft: 8
883
883
  },
@@ -889,19 +889,19 @@ const styles = StyleSheet.create({
889
889
  },
890
890
  emptyIcon: {
891
891
  fontSize: 48,
892
- fontFamily: "monospace",
892
+ fontFamily: monoFont,
893
893
  opacity: 0.2,
894
894
  marginBottom: 12
895
895
  },
896
896
  emptyTitle: {
897
897
  fontSize: 14,
898
- fontFamily: "monospace",
898
+ fontFamily: monoFont,
899
899
  fontWeight: "600",
900
900
  marginBottom: 4
901
901
  },
902
902
  emptySubtitle: {
903
903
  fontSize: 12,
904
- fontFamily: "monospace",
904
+ fontFamily: monoFont,
905
905
  opacity: 0.6
906
906
  }
907
907
  });
@@ -3,7 +3,7 @@
3
3
  /**
4
4
  * Auto-generated safe area implementation
5
5
  * Detected: none
6
- * Generated at: 2026-09-04T01:21:28.486Z
6
+ * Generated at: 2026-09-10T21:40:50.832Z
7
7
  *
8
8
  * DO NOT EDIT - This file is generated by scripts/detect-safe-area.js
9
9
  *
@@ -28,8 +28,35 @@ export { WifiCircuitIcon } from "./WifiCircuitIcon.js";
28
28
  export { RouteMapIcon, RouteIcon, NavigationIcon } from "./RouteMapIcon.js";
29
29
  export { StackPulseIcon, RenderPulseIcon, HighlightUpdatesIcon } from "./StackPulseIcon.js";
30
30
  export { RenderCountIcon } from "./RenderCountIcon.js";
31
- export { BenchmarkIcon } from "./BenchmarkIcon.js";
31
+ // The hand-drawn stopwatch is legacy; `BenchmarkIcon` is the BIF artwork (same on every port).
32
+ export { BenchmarkIcon as BenchmarkLegacyIcon } from "./BenchmarkIcon.js";
32
33
  export { IconBackground } from "./IconBackground.js";
33
34
 
34
35
  // Export lucide icons
35
- export * from "./lucide-icons.js";
36
+ export * from "./lucide-icons.js";
37
+
38
+ // The lucide glyph tier, drawn from shared/icons/glyphs/*.json — the SAME data
39
+ // the Flutter and Swift renderers read. React Native used to carry a second,
40
+ // hand-drawn copy of these icons (`./lucide-icons`), so `Globe` was one drawing
41
+ // on RN and another on every port; the cross-framework parity sheet measured
42
+ // `icons/globe-24` 10.8 % apart on both. These explicit exports shadow the
43
+ // `export * from "./lucide-icons"` above (the same trick `WifiBifIcon` uses),
44
+ // so one edit to a JSON now moves all three frameworks. The hand-drawn
45
+ // originals stay reachable as `<Name>Icon` from `./lucide-icons`.
46
+ //
47
+ // `Pin` is deliberately NOT here: RN's pin carries an extra `active` prop that
48
+ // stands it upright, which the glyph data has no way to express (the ports draw
49
+ // the resting pose only). `WifiIcon` stays lucide for the same kind of reason —
50
+ // see the `WifiBifIcon` note below.
51
+ export { ActivityGlyph as Activity, AlertCircleGlyph as AlertCircle, AlertTriangleGlyph as AlertTriangle, BookmarkGlyph as Bookmark, BoxGlyph as Box, BugGlyph as Bug, CheckCircleGlyph as CheckCircle, CheckGlyph as Check, ChevronDownGlyph as ChevronDown, ChevronLeftGlyph as ChevronLeft, ChevronRightGlyph as ChevronRight, ChevronUpGlyph as ChevronUp, ClockGlyph as Clock, CopyGlyph as Copy, DatabaseGlyph as Database, DockBottomGlyph as DockBottom, DownloadGlyph as Download, Edit3Glyph as Edit3, EyeOffGlyph as EyeOff, EyeGlyph as Eye, FileJsonGlyph as FileJson, FileTextGlyph as FileText, FilmGlyph as Film, FilterGlyph as Filter, FlaskConicalGlyph as FlaskConical, FloatWindowGlyph as FloatWindow, GitBranchGlyph as GitBranch, GlobeGlyph as Globe, HashGlyph as Hash, HomeGlyph as Home, ImageGlyph as Image, InfoGlyph as Info, KeyGlyph as Key, LayersGlyph as Layers, LinkGlyph as Link, LockGlyph as Lock, MinusGlyph as Minus, MusicGlyph as Music, NavigationGlyph as Navigation, PaletteGlyph as Palette, PauseGlyph as Pause, PlayGlyph as Play, PlusGlyph as Plus, PowerGlyph as Power, RefreshCwGlyph as RefreshCw, SearchGlyph as Search, ServerGlyph as Server, SettingsGlyph as Settings,
52
+ // Solid gear (brand tier, so a caller's strokeWidth can't blow the hole open).
53
+ SettingsSolidIcon as SettingsSolid, ShieldGlyph as Shield, Trash2Glyph as Trash2,
54
+ // RN aliases `Trash` onto `Trash2Icon`; the glyph has to shadow both.
55
+ Trash2Glyph as Trash, UnlockGlyph as Unlock, UploadGlyph as Upload, UserGlyph as User, WifiGlyph as Wifi, XCircleGlyph as XCircle, XGlyph as X, ZapGlyph as Zap } from "@buoy-gg/floating-tools-core";
56
+
57
+ // Every BIF brand icon the dial can show, re-exported so the parity sheet (and tools) render
58
+ // the same artwork by name — see shared/parity/specimens.json `icons/bif-*`.
59
+ export { SentryIcon, EventsIcon, RoutesIcon, RoutePinIcon, RoutePathIcon, RouteDotIcon, RouteTwoPinsIcon, CameraIcon, HighlightIcon, ReduxIcon, StorageIcon, AskBuoyIcon, HighlighterIcon, QueryIcon, ScenariosIcon } from "@buoy-gg/floating-tools-core";
60
+ // `WifiIcon` here stays the lucide glyph (react-query's online toggle); the BIF wifi ships as
61
+ // `WifiBifIcon`. `BenchmarkIcon` IS the BIF stopwatch since 2026-09-04 (perf-monitor's dial icon).
62
+ export { WifiIcon as WifiBifIcon, BenchmarkIcon, BENCHMARK_ICON_COLOR } from "@buoy-gg/floating-tools-core";
@@ -5,6 +5,10 @@ export * from "./ui/index.js";
5
5
 
6
6
  // Store base classes
7
7
  export * from "./stores/index.js";
8
+ export * from "./sync/keyedArrays.js";
9
+ export * from "./sync/valueShape.js";
10
+ export * from "./sync/valuePath.js";
11
+ export * from "./sync/typedEdit.js";
8
12
  export * from "./sync/wireBudget.js";
9
13
  export * from "./sync/wireForm.js";
10
14
  export * from "./sync/toolSyncAdapter.js";
@@ -19,7 +19,7 @@ import { buoyColors } from "../ui/gameUI/constants/gameUIColors.js";
19
19
  import { Check, Copy, X } from "../icons/lucide-icons.js";
20
20
  import { copyToClipboard } from "../clipboard/index.js";
21
21
  import { openPricing } from "./openPricing.js";
22
-
22
+ import { monoFont } from "../ui/fonts.js";
23
23
  /** The one thing a reader has to be able to act on. */
24
24
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
25
25
  export const LOGIN_COMMAND = "npx buoy login";
@@ -155,7 +155,7 @@ const styles = StyleSheet.create({
155
155
  fontSize: 14,
156
156
  fontWeight: "600",
157
157
  color: buoyColors.text,
158
- fontFamily: "monospace"
158
+ fontFamily: monoFont
159
159
  },
160
160
  reassure: {
161
161
  fontSize: 12,
@@ -9,12 +9,12 @@
9
9
  */
10
10
 
11
11
  import React, { useCallback } from "react";
12
- import { View, Text, TouchableOpacity, StyleSheet, Modal, Platform } from "react-native";
12
+ import { View, Text, TouchableOpacity, StyleSheet, Modal } from "react-native";
13
13
  import { absoluteFill } from "../utils/absoluteFill.js";
14
14
  import { macOSColors } from "../ui/gameUI/constants/macOSDesignSystemColors.js";
15
15
  import { openPricing, isWeb } from "./openPricing.js";
16
16
  import { X, Key, Link, FileCode, Copy } from "../icons/lucide-icons.js";
17
-
17
+ import { monoFont } from "../ui/fonts.js";
18
18
  // For clipboard functionality
19
19
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
20
20
  let Clipboard = null;
@@ -339,7 +339,7 @@ const styles = StyleSheet.create({
339
339
  },
340
340
  codeText: {
341
341
  fontSize: 11,
342
- fontFamily: Platform.OS === "ios" ? "Menlo" : "monospace",
342
+ fontFamily: monoFont,
343
343
  lineHeight: 18,
344
344
  color: macOSColors.text.primary
345
345
  },
@@ -0,0 +1,204 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Id-addressed list edits — one implementation, shared by every adapter that
5
+ * takes a merge patch.
6
+ *
7
+ * WHY. Resending a whole array is correct only when the caller has SEEN the
8
+ * whole array, and reads are capped. Measured with a 20-item list truncated to
9
+ * 5 (`pnpm ai:array --truncate=5`): the whole-array form scored 0/9 across three
10
+ * models — two refused outright, and one silently sent back only the 5 items it
11
+ * had been shown, destroying the other 15. Addressed edits scored 9/9.
12
+ *
13
+ * It also makes mass deletion impossible BY OMISSION: an addressed edit can only
14
+ * remove ids it names. Sending a real array still replaces the list, which is
15
+ * how a caller deliberately empties one.
16
+ *
17
+ * This lives here rather than in one adapter because react-query, zustand and
18
+ * network all take merge patches, and a second copy of a data-destroying safety
19
+ * rule is a copy that drifts.
20
+ */
21
+ const isPlainObject = v => typeof v === "object" && v !== null && !Array.isArray(v);
22
+
23
+ /** Fields we will treat as an item's stable identity, best first. */
24
+ const ID_FIELDS = ["id", "lineId", "key", "uuid", "_id", "slug", "name"];
25
+
26
+ /**
27
+ * The field that identifies items in this list, or undefined if there isn't one.
28
+ *
29
+ * Every item must carry it, and every value must be a distinct string/number —
30
+ * a duplicated id would make an addressed edit ambiguous, and silently picking
31
+ * the first match is exactly the kind of "helpful" guess that loses data.
32
+ */
33
+ export function identityField(items) {
34
+ if (!items.length || !items.every(isPlainObject)) return undefined;
35
+ const first = items[0];
36
+ for (const f of ID_FIELDS) {
37
+ if (!(f in first)) continue;
38
+ const values = items.map(it => it[f]);
39
+ const usable = values.every(v => typeof v === "string" || typeof v === "number");
40
+ if (!usable) continue;
41
+ if (new Set(values.map(String)).size !== values.length) continue;
42
+ return f;
43
+ }
44
+ return undefined;
45
+ }
46
+
47
+ /**
48
+ * The identity an ADDED item should carry.
49
+ *
50
+ * `Object.entries` hands back STRING keys, so `{"9901": {id: 9901, …}}` was
51
+ * written as `{id: "9901"}` — the caller's own numeric id replaced by text, and
52
+ * then refused by the typed-edit guard as `[2].id: number → string`. The caller
53
+ * recovers by resending the WHOLE array, which is exactly the truncation-unsafe
54
+ * move this module exists to prevent. Measured on a live QA run: one added
55
+ * offer cost a refusal, a full-list retransmission and 35 seconds.
56
+ *
57
+ * So the id the caller WROTE wins. With no id in the body, the key is coerced
58
+ * to the type the existing items use, and only when the text round-trips
59
+ * exactly: "007" is not 7, and inventing that identity is worse than a refusal
60
+ * the caller can read.
61
+ */
62
+ function identityForAdd(current, idField, key, edit) {
63
+ const supplied = edit[idField];
64
+ if (typeof supplied === "string" || typeof supplied === "number") {
65
+ // An item whose id disagrees with the address it was sent under is
66
+ // ambiguous — either could be the one the caller meant. Say so rather than
67
+ // silently picking one and writing an item nobody asked for.
68
+ if (String(supplied) !== key) {
69
+ return {
70
+ ok: false,
71
+ error: `The new item is addressed as \`${key}\` but carries \`${idField}\`: ${JSON.stringify(supplied)}. Use the same value for both — address it by the id it carries.`
72
+ };
73
+ }
74
+ return {
75
+ ok: true,
76
+ value: supplied
77
+ };
78
+ }
79
+ const numericList = current.every(it => typeof it[idField] === "number");
80
+ if (numericList && key.trim() !== "" && String(Number(key)) === key) {
81
+ return {
82
+ ok: true,
83
+ value: Number(key)
84
+ };
85
+ }
86
+ return {
87
+ ok: true,
88
+ value: key
89
+ };
90
+ }
91
+
92
+ /**
93
+ * Let a caller address list items by their own id instead of resending the list.
94
+ *
95
+ * { lines: { "seed-1": { qty: 5 } } } change one field of one item
96
+ * { lines: { "seed-2": null } } remove that item
97
+ * { lines: { "new-1": { …all fields… } } } append (id not already present)
98
+ *
99
+ * WHY THIS EXISTS. Resending the whole array is correct only when the caller has
100
+ * SEEN the whole array, and reads are capped — so on a long list the honest
101
+ * agents refuse and the careless ones send back only the items they were shown,
102
+ * silently destroying the rest. Measured: with a 20-item bag truncated to 5, the
103
+ * whole-array form scored 0/9 across three models and one of them deleted 15
104
+ * lines without a word. Addressed edits scored 9/9. (`pnpm ai:array`.)
105
+ *
106
+ * It also makes mass deletion impossible BY OMISSION: `lines: []` empties a bag,
107
+ * but a keyed edit can only remove ids it names.
108
+ *
109
+ * Returns the patch with keyed objects expanded into real arrays, so everything
110
+ * downstream — the typed-edit guard, the receipt, the store write — is unchanged
111
+ * and keeps all of its protections.
112
+ */
113
+ export function expandKeyedArrays(current, patch, path = "", notes = []) {
114
+ if (Array.isArray(current) && isPlainObject(patch)) {
115
+ const idField = identityField(current);
116
+ if (!idField) {
117
+ // No stable identity: leave it alone and let the normal guard report the
118
+ // object-vs-array type change, which says something truer than we could.
119
+ return {
120
+ patch,
121
+ notes
122
+ };
123
+ }
124
+ const byId = new Map(current.map(it => [String(it[idField]), it]));
125
+ const next = current.slice();
126
+ let removed = 0;
127
+ let changed = 0;
128
+ let added = 0;
129
+ for (const [id, edit] of Object.entries(patch)) {
130
+ const i = next.findIndex(it => String(it[idField]) === id);
131
+ if (edit === null) {
132
+ if (i === -1) return {
133
+ patch,
134
+ notes
135
+ };
136
+ next.splice(i, 1);
137
+ removed++;
138
+ } else if (i === -1) {
139
+ if (!isPlainObject(edit)) return {
140
+ patch,
141
+ notes
142
+ };
143
+ const identity = identityForAdd(current, idField, id, edit);
144
+ if (!identity.ok) {
145
+ return {
146
+ patch,
147
+ notes,
148
+ error: identity.error
149
+ };
150
+ }
151
+ next.push({
152
+ ...edit,
153
+ [idField]: identity.value
154
+ });
155
+ added++;
156
+ } else {
157
+ const merged = expandKeyedArrays(byId.get(id), edit, `${path}[${id}]`, notes);
158
+ if (merged.error) return {
159
+ patch,
160
+ notes,
161
+ error: merged.error
162
+ };
163
+ const base = next[i];
164
+ next[i] = isPlainObject(base) && isPlainObject(merged.patch) ? {
165
+ ...base,
166
+ ...merged.patch
167
+ } : merged.patch;
168
+ changed++;
169
+ }
170
+ }
171
+ notes.push(`${path || "(root)"}: addressed by \`${idField}\` — ${changed} changed, ${added} added, ${removed} removed, ${next.length} item(s) remain`);
172
+ return {
173
+ patch: next,
174
+ notes
175
+ };
176
+ }
177
+ if (isPlainObject(current) && isPlainObject(patch)) {
178
+ const outPatch = {};
179
+ for (const [k, v] of Object.entries(patch)) {
180
+ const p = path ? `${path}.${k}` : k;
181
+ if (!(k in current)) {
182
+ outPatch[k] = v;
183
+ continue;
184
+ }
185
+ const inner = expandKeyedArrays(current[k], v, p, notes);
186
+ // A refusal deep in the tree is the whole patch's refusal: writing the
187
+ // rest of it would apply half of what the caller asked for.
188
+ if (inner.error) return {
189
+ patch,
190
+ notes,
191
+ error: inner.error
192
+ };
193
+ outPatch[k] = inner.patch;
194
+ }
195
+ return {
196
+ patch: outPatch,
197
+ notes
198
+ };
199
+ }
200
+ return {
201
+ patch,
202
+ notes
203
+ };
204
+ }
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * The attention badge seam: a tool says "I need you" and the rail hears it.
5
+ */
6
+ import { describe, expect, it, vi } from "vitest";
7
+ import { getToolAttention, setToolAttention, subscribeToolAttention } from "./toolUiBridge.js";
8
+ describe("toolAttention", () => {
9
+ it("stores, replaces and clears a badge, notifying once per change", () => {
10
+ const fn = vi.fn();
11
+ const off = subscribeToolAttention(fn);
12
+ setToolAttention("ask-buoy", {
13
+ kind: "needsInput",
14
+ label: "Needs your approval"
15
+ });
16
+ expect(getToolAttention("ask-buoy")).toEqual({
17
+ kind: "needsInput",
18
+ label: "Needs your approval"
19
+ });
20
+ expect(fn).toHaveBeenCalledTimes(1);
21
+ // Same value again is not a change.
22
+ setToolAttention("ask-buoy", {
23
+ kind: "needsInput",
24
+ label: "Needs your approval"
25
+ });
26
+ expect(fn).toHaveBeenCalledTimes(1);
27
+ setToolAttention("ask-buoy", {
28
+ kind: "done",
29
+ label: "Finished"
30
+ });
31
+ expect(fn).toHaveBeenCalledTimes(2);
32
+ setToolAttention("ask-buoy", null);
33
+ expect(getToolAttention("ask-buoy")).toBeNull();
34
+ expect(fn).toHaveBeenCalledTimes(3);
35
+ // Clearing what is already clear is silent.
36
+ setToolAttention("ask-buoy", null);
37
+ expect(fn).toHaveBeenCalledTimes(3);
38
+ off();
39
+ });
40
+ it("keeps tools apart", () => {
41
+ setToolAttention("a", {
42
+ kind: "failed",
43
+ label: "x"
44
+ });
45
+ setToolAttention("b", null);
46
+ expect(getToolAttention("a")?.kind).toBe("failed");
47
+ expect(getToolAttention("b")).toBeNull();
48
+ setToolAttention("a", null);
49
+ });
50
+ });
@@ -65,4 +65,59 @@ export function openBuoyTool(toolId) {
65
65
  /** True when some host is listening — for deciding whether to offer the button. */
66
66
  export function canOpenBuoyTools() {
67
67
  return openers().size > 0;
68
+ }
69
+
70
+ // ─── Attention ───────────────────────────────────────────────────────────────
71
+
72
+ /**
73
+ * "This tool needs you" — shown as a badge on its chip in the minimized rail.
74
+ *
75
+ * The case: Ask Buoy is minimized so a tester can watch the app, and the
76
+ * agent hits a step that needs a tap. Nobody can see the card. The sheet used
77
+ * to fail safe by DECLINING the change and ending the turn — which made
78
+ * minimize mean "stop", the opposite of what it is for. Now the approval
79
+ * holds, the chip gets a badge, and tapping the chip (the existing restore
80
+ * path) brings the card back. Same seam shape as the opener above: the rail
81
+ * lives inside the host, the tool cannot import the host, so it publishes
82
+ * through a global.
83
+ *
84
+ * `done` and `failed` cover the other thing a hidden tool cannot say — that
85
+ * the work you minimized it to wait for has finished.
86
+ */
87
+
88
+ const ATTENTION_KEY = "__buoyToolAttention";
89
+ function attention() {
90
+ const g = globalThis;
91
+ if (!g[ATTENTION_KEY]) g[ATTENTION_KEY] = {
92
+ byTool: new Map(),
93
+ listeners: new Set()
94
+ };
95
+ return g[ATTENTION_KEY];
96
+ }
97
+
98
+ /** Set, replace or (with null) clear a tool's badge. No-op when nothing changed. */
99
+ export function setToolAttention(toolId, next) {
100
+ const store = attention();
101
+ const prev = store.byTool.get(toolId) ?? null;
102
+ if (prev === next || prev && next && prev.kind === next.kind && prev.label === next.label) return;
103
+ if (next) store.byTool.set(toolId, next);else store.byTool.delete(toolId);
104
+ for (const fn of store.listeners) {
105
+ try {
106
+ fn();
107
+ } catch {
108
+ // A throwing subscriber must not stop the others from hearing.
109
+ }
110
+ }
111
+ }
112
+ export function getToolAttention(toolId) {
113
+ return attention().byTool.get(toolId) ?? null;
114
+ }
115
+
116
+ /** For `useSyncExternalStore` in the rail. */
117
+ export function subscribeToolAttention(fn) {
118
+ const store = attention();
119
+ store.listeners.add(fn);
120
+ return () => {
121
+ store.listeners.delete(fn);
122
+ };
68
123
  }