@buoy-gg/shared-ui 7.0.21 → 7.0.23

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 (178) 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 +64 -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/commonjs/utils/devServer.js +62 -0
  29. package/lib/commonjs/utils/index.js +13 -0
  30. package/lib/commonjs/utils/renderCaptureRegistry.js +32 -3
  31. package/lib/commonjs/utils/safeExpoRouter.js +27 -2
  32. package/lib/module/JsModal.js +39 -16
  33. package/lib/module/dataViewer/JsonStringNotice.js +54 -0
  34. package/lib/module/dataViewer/ThemedSplitView.js +20 -1
  35. package/lib/module/dataViewer/TimestampDeltaBar.js +91 -0
  36. package/lib/module/dataViewer/index.js +4 -0
  37. package/lib/module/dataViewer/jsonStringValue.js +89 -0
  38. package/lib/module/dataViewer/timestampDiff.js +139 -0
  39. package/lib/module/hooks/safe-area-impl.js +1 -1
  40. package/lib/module/index.js +5 -3
  41. package/lib/module/license/FeatureGate.js +43 -10
  42. package/lib/module/license/FreeAccountPrompt.js +176 -0
  43. package/lib/module/license/index.js +5 -40
  44. package/lib/module/license/licenseModule.js +92 -0
  45. package/lib/module/license/tierLimits.js +158 -0
  46. package/lib/module/ui/backgrounds/BackgroundSwitcher.js +176 -0
  47. package/lib/module/ui/backgrounds/README.md +115 -0
  48. package/lib/module/ui/backgrounds/ToolBackground.js +70 -0
  49. package/lib/module/ui/backgrounds/backgroundStore.js +111 -0
  50. package/lib/module/ui/backgrounds/index.js +9 -0
  51. package/lib/module/ui/backgrounds/primitives.js +328 -0
  52. package/lib/module/ui/backgrounds/registry.js +105 -0
  53. package/lib/module/ui/backgrounds/rng.js +65 -0
  54. package/lib/module/ui/backgrounds/types.js +1 -0
  55. package/lib/module/ui/backgrounds/variants/deepField.js +347 -0
  56. package/lib/module/ui/backgrounds/variants/lattice.js +575 -0
  57. package/lib/module/ui/backgrounds/variants/starfield.js +206 -0
  58. package/lib/module/ui/index.js +3 -0
  59. package/lib/module/utils/devServer.js +58 -0
  60. package/lib/module/utils/index.js +2 -1
  61. package/lib/module/utils/renderCaptureRegistry.js +32 -3
  62. package/lib/module/utils/safeExpoRouter.js +26 -2
  63. package/lib/typescript/commonjs/JsModal.d.ts.map +1 -1
  64. package/lib/typescript/commonjs/dataViewer/JsonStringNotice.d.ts +6 -0
  65. package/lib/typescript/commonjs/dataViewer/JsonStringNotice.d.ts.map +1 -0
  66. package/lib/typescript/commonjs/dataViewer/ThemedSplitView.d.ts.map +1 -1
  67. package/lib/typescript/commonjs/dataViewer/TimestampDeltaBar.d.ts +7 -0
  68. package/lib/typescript/commonjs/dataViewer/TimestampDeltaBar.d.ts.map +1 -0
  69. package/lib/typescript/commonjs/dataViewer/index.d.ts +6 -0
  70. package/lib/typescript/commonjs/dataViewer/index.d.ts.map +1 -1
  71. package/lib/typescript/commonjs/dataViewer/jsonStringValue.d.ts +25 -0
  72. package/lib/typescript/commonjs/dataViewer/jsonStringValue.d.ts.map +1 -0
  73. package/lib/typescript/commonjs/dataViewer/timestampDiff.d.ts +55 -0
  74. package/lib/typescript/commonjs/dataViewer/timestampDiff.d.ts.map +1 -0
  75. package/lib/typescript/commonjs/hooks/safe-area-impl.d.ts +1 -1
  76. package/lib/typescript/commonjs/index.d.ts +4 -3
  77. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  78. package/lib/typescript/commonjs/license/FeatureGate.d.ts +19 -3
  79. package/lib/typescript/commonjs/license/FeatureGate.d.ts.map +1 -1
  80. package/lib/typescript/commonjs/license/FreeAccountPrompt.d.ts +23 -0
  81. package/lib/typescript/commonjs/license/FreeAccountPrompt.d.ts.map +1 -0
  82. package/lib/typescript/commonjs/license/index.d.ts +6 -15
  83. package/lib/typescript/commonjs/license/index.d.ts.map +1 -1
  84. package/lib/typescript/commonjs/license/licenseModule.d.ts +55 -0
  85. package/lib/typescript/commonjs/license/licenseModule.d.ts.map +1 -0
  86. package/lib/typescript/commonjs/license/tierLimits.d.ts +143 -0
  87. package/lib/typescript/commonjs/license/tierLimits.d.ts.map +1 -0
  88. package/lib/typescript/commonjs/ui/backgrounds/BackgroundSwitcher.d.ts +13 -0
  89. package/lib/typescript/commonjs/ui/backgrounds/BackgroundSwitcher.d.ts.map +1 -0
  90. package/lib/typescript/commonjs/ui/backgrounds/ToolBackground.d.ts +30 -0
  91. package/lib/typescript/commonjs/ui/backgrounds/ToolBackground.d.ts.map +1 -0
  92. package/lib/typescript/commonjs/ui/backgrounds/backgroundStore.d.ts +43 -0
  93. package/lib/typescript/commonjs/ui/backgrounds/backgroundStore.d.ts.map +1 -0
  94. package/lib/typescript/commonjs/ui/backgrounds/index.d.ts +7 -0
  95. package/lib/typescript/commonjs/ui/backgrounds/index.d.ts.map +1 -0
  96. package/lib/typescript/commonjs/ui/backgrounds/primitives.d.ts +106 -0
  97. package/lib/typescript/commonjs/ui/backgrounds/primitives.d.ts.map +1 -0
  98. package/lib/typescript/commonjs/ui/backgrounds/registry.d.ts +28 -0
  99. package/lib/typescript/commonjs/ui/backgrounds/registry.d.ts.map +1 -0
  100. package/lib/typescript/commonjs/ui/backgrounds/rng.d.ts +27 -0
  101. package/lib/typescript/commonjs/ui/backgrounds/rng.d.ts.map +1 -0
  102. package/lib/typescript/commonjs/ui/backgrounds/types.d.ts +14 -0
  103. package/lib/typescript/commonjs/ui/backgrounds/types.d.ts.map +1 -0
  104. package/lib/typescript/commonjs/ui/backgrounds/variants/deepField.d.ts +65 -0
  105. package/lib/typescript/commonjs/ui/backgrounds/variants/deepField.d.ts.map +1 -0
  106. package/lib/typescript/commonjs/ui/backgrounds/variants/lattice.d.ts +33 -0
  107. package/lib/typescript/commonjs/ui/backgrounds/variants/lattice.d.ts.map +1 -0
  108. package/lib/typescript/commonjs/ui/backgrounds/variants/starfield.d.ts +12 -0
  109. package/lib/typescript/commonjs/ui/backgrounds/variants/starfield.d.ts.map +1 -0
  110. package/lib/typescript/commonjs/ui/index.d.ts +1 -0
  111. package/lib/typescript/commonjs/ui/index.d.ts.map +1 -1
  112. package/lib/typescript/commonjs/utils/devServer.d.ts +24 -0
  113. package/lib/typescript/commonjs/utils/devServer.d.ts.map +1 -0
  114. package/lib/typescript/commonjs/utils/index.d.ts +3 -2
  115. package/lib/typescript/commonjs/utils/index.d.ts.map +1 -1
  116. package/lib/typescript/commonjs/utils/renderCaptureRegistry.d.ts +52 -2
  117. package/lib/typescript/commonjs/utils/renderCaptureRegistry.d.ts.map +1 -1
  118. package/lib/typescript/commonjs/utils/safeExpoRouter.d.ts +23 -0
  119. package/lib/typescript/commonjs/utils/safeExpoRouter.d.ts.map +1 -1
  120. package/lib/typescript/module/JsModal.d.ts.map +1 -1
  121. package/lib/typescript/module/dataViewer/JsonStringNotice.d.ts +6 -0
  122. package/lib/typescript/module/dataViewer/JsonStringNotice.d.ts.map +1 -0
  123. package/lib/typescript/module/dataViewer/ThemedSplitView.d.ts.map +1 -1
  124. package/lib/typescript/module/dataViewer/TimestampDeltaBar.d.ts +7 -0
  125. package/lib/typescript/module/dataViewer/TimestampDeltaBar.d.ts.map +1 -0
  126. package/lib/typescript/module/dataViewer/index.d.ts +6 -0
  127. package/lib/typescript/module/dataViewer/index.d.ts.map +1 -1
  128. package/lib/typescript/module/dataViewer/jsonStringValue.d.ts +25 -0
  129. package/lib/typescript/module/dataViewer/jsonStringValue.d.ts.map +1 -0
  130. package/lib/typescript/module/dataViewer/timestampDiff.d.ts +55 -0
  131. package/lib/typescript/module/dataViewer/timestampDiff.d.ts.map +1 -0
  132. package/lib/typescript/module/hooks/safe-area-impl.d.ts +1 -1
  133. package/lib/typescript/module/index.d.ts +4 -3
  134. package/lib/typescript/module/index.d.ts.map +1 -1
  135. package/lib/typescript/module/license/FeatureGate.d.ts +19 -3
  136. package/lib/typescript/module/license/FeatureGate.d.ts.map +1 -1
  137. package/lib/typescript/module/license/FreeAccountPrompt.d.ts +23 -0
  138. package/lib/typescript/module/license/FreeAccountPrompt.d.ts.map +1 -0
  139. package/lib/typescript/module/license/index.d.ts +6 -15
  140. package/lib/typescript/module/license/index.d.ts.map +1 -1
  141. package/lib/typescript/module/license/licenseModule.d.ts +55 -0
  142. package/lib/typescript/module/license/licenseModule.d.ts.map +1 -0
  143. package/lib/typescript/module/license/tierLimits.d.ts +143 -0
  144. package/lib/typescript/module/license/tierLimits.d.ts.map +1 -0
  145. package/lib/typescript/module/ui/backgrounds/BackgroundSwitcher.d.ts +13 -0
  146. package/lib/typescript/module/ui/backgrounds/BackgroundSwitcher.d.ts.map +1 -0
  147. package/lib/typescript/module/ui/backgrounds/ToolBackground.d.ts +30 -0
  148. package/lib/typescript/module/ui/backgrounds/ToolBackground.d.ts.map +1 -0
  149. package/lib/typescript/module/ui/backgrounds/backgroundStore.d.ts +43 -0
  150. package/lib/typescript/module/ui/backgrounds/backgroundStore.d.ts.map +1 -0
  151. package/lib/typescript/module/ui/backgrounds/index.d.ts +7 -0
  152. package/lib/typescript/module/ui/backgrounds/index.d.ts.map +1 -0
  153. package/lib/typescript/module/ui/backgrounds/primitives.d.ts +106 -0
  154. package/lib/typescript/module/ui/backgrounds/primitives.d.ts.map +1 -0
  155. package/lib/typescript/module/ui/backgrounds/registry.d.ts +28 -0
  156. package/lib/typescript/module/ui/backgrounds/registry.d.ts.map +1 -0
  157. package/lib/typescript/module/ui/backgrounds/rng.d.ts +27 -0
  158. package/lib/typescript/module/ui/backgrounds/rng.d.ts.map +1 -0
  159. package/lib/typescript/module/ui/backgrounds/types.d.ts +14 -0
  160. package/lib/typescript/module/ui/backgrounds/types.d.ts.map +1 -0
  161. package/lib/typescript/module/ui/backgrounds/variants/deepField.d.ts +65 -0
  162. package/lib/typescript/module/ui/backgrounds/variants/deepField.d.ts.map +1 -0
  163. package/lib/typescript/module/ui/backgrounds/variants/lattice.d.ts +33 -0
  164. package/lib/typescript/module/ui/backgrounds/variants/lattice.d.ts.map +1 -0
  165. package/lib/typescript/module/ui/backgrounds/variants/starfield.d.ts +12 -0
  166. package/lib/typescript/module/ui/backgrounds/variants/starfield.d.ts.map +1 -0
  167. package/lib/typescript/module/ui/index.d.ts +1 -0
  168. package/lib/typescript/module/ui/index.d.ts.map +1 -1
  169. package/lib/typescript/module/utils/devServer.d.ts +24 -0
  170. package/lib/typescript/module/utils/devServer.d.ts.map +1 -0
  171. package/lib/typescript/module/utils/index.d.ts +3 -2
  172. package/lib/typescript/module/utils/index.d.ts.map +1 -1
  173. package/lib/typescript/module/utils/renderCaptureRegistry.d.ts +52 -2
  174. package/lib/typescript/module/utils/renderCaptureRegistry.d.ts.map +1 -1
  175. package/lib/typescript/module/utils/safeExpoRouter.d.ts +23 -0
  176. package/lib/typescript/module/utils/safeExpoRouter.d.ts.map +1 -1
  177. package/package.json +4 -4
  178. package/scripts/detect-safe-area.js +1 -0
@@ -0,0 +1,91 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * TimestampDeltaBar — reads out the timestamp pairs in a diff.
5
+ *
6
+ * Sits under the diff summary and translates `1787085897165 → 1787012175875`
7
+ * into "20h 29m earlier", with both instants spelled out. The raw values stay
8
+ * on the rows below: this annotates the diff, it never replaces it.
9
+ *
10
+ * Renders nothing when no side of the diff is confidently a timestamp — see
11
+ * timestampDiff.ts for how narrow "confidently" is.
12
+ */
13
+ import { memo } from "react";
14
+ import { View, Text, StyleSheet } from "react-native";
15
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
16
+ export const TimestampDeltaBar = /*#__PURE__*/memo(function TimestampDeltaBar({
17
+ deltas,
18
+ theme
19
+ }) {
20
+ if (deltas.length === 0) return null;
21
+ return /*#__PURE__*/_jsx(View, {
22
+ style: [styles.container, {
23
+ backgroundColor: theme.summaryBackground,
24
+ borderTopColor: theme.borderColor
25
+ }],
26
+ children: deltas.map(delta => /*#__PURE__*/_jsxs(View, {
27
+ style: styles.row,
28
+ children: [/*#__PURE__*/_jsx(Text, {
29
+ style: [styles.glyph, {
30
+ color: theme.summaryModifiedText
31
+ }],
32
+ children: "\u25F7"
33
+ }), /*#__PURE__*/_jsxs(View, {
34
+ style: styles.text,
35
+ children: [/*#__PURE__*/_jsxs(Text, {
36
+ style: styles.headline,
37
+ children: [delta.path ? /*#__PURE__*/_jsxs(Text, {
38
+ style: [styles.path, {
39
+ color: theme.lineNumberText
40
+ }],
41
+ children: [delta.path, " "]
42
+ }) : null, /*#__PURE__*/_jsx(Text, {
43
+ style: {
44
+ color: theme.summaryModifiedText
45
+ },
46
+ children: delta.deltaLabel
47
+ })]
48
+ }), /*#__PURE__*/_jsxs(Text, {
49
+ style: [styles.instants, {
50
+ color: theme.lineNumberText
51
+ }],
52
+ children: [delta.fromLabel, " \u2192 ", delta.toLabel]
53
+ })]
54
+ })]
55
+ }, delta.path ?? "__value__"))
56
+ });
57
+ });
58
+ const styles = StyleSheet.create({
59
+ container: {
60
+ borderTopWidth: StyleSheet.hairlineWidth,
61
+ paddingHorizontal: 10,
62
+ paddingVertical: 7,
63
+ gap: 6
64
+ },
65
+ row: {
66
+ flexDirection: "row",
67
+ alignItems: "flex-start",
68
+ gap: 7
69
+ },
70
+ glyph: {
71
+ fontSize: 12,
72
+ lineHeight: 15
73
+ },
74
+ text: {
75
+ flex: 1,
76
+ minWidth: 0
77
+ },
78
+ headline: {
79
+ fontSize: 12,
80
+ fontWeight: "600",
81
+ lineHeight: 15
82
+ },
83
+ path: {
84
+ fontWeight: "400"
85
+ },
86
+ instants: {
87
+ fontSize: 11,
88
+ marginTop: 1,
89
+ fontVariant: ["tabular-nums"]
90
+ }
91
+ });
@@ -33,6 +33,10 @@ export { DataTreeActionDock } from "./DataTreeActionDock.js";
33
33
  // Diff viewer components
34
34
  export { SplitDiffViewer } from "./SplitDiffViewer.js";
35
35
  export { DiffSummary } from "./DiffSummary.js";
36
+ export { TimestampDeltaBar } from "./TimestampDeltaBar.js";
37
+ export { JsonStringNotice } from "./JsonStringNotice.js";
38
+ export { decodeJsonStringPair, parseJsonContainer } from "./jsonStringValue.js";
39
+ export { detectTimestamp, describeTimestampDeltas, formatDuration, formatInstant } from "./timestampDiff.js";
36
40
  // Diff utilities and themes
37
41
  export { computeLineDiff, DiffType, ABSENT_VALUE, isAbsent } from "./lineDiff.js";
38
42
  export { diffThemes, gitClassicTheme, devToolsDefaultTheme } from "./diffThemes.js";
@@ -0,0 +1,89 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * JSON-encoded string values in diffs.
5
+ *
6
+ * Every device storage backend stores STRINGS, so apps write
7
+ * `JSON.stringify(["pikachu"])` and the stored value is the six-character
8
+ * string `["pikachu"]`. Rendering that through the diff's own
9
+ * `JSON.stringify` escapes it a second time and you get `"[\"pikachu\"]"` on
10
+ * one unreadable line — and a real change (one entry added to a list of
11
+ * twenty) becomes a single modified line you can't read.
12
+ *
13
+ * So: when a side is a JSON-encoded OBJECT OR ARRAY, diff the decoded value
14
+ * and say so on screen. The label is the whole contract — the store really
15
+ * does hold a string, and a viewer that quietly showed an array without
16
+ * mentioning it would be lying about the shape of the data.
17
+ *
18
+ * Scalars are deliberately NOT decoded. Turning the stored string `"5"` into
19
+ * the number 5, or `"true"` into a boolean, changes the apparent type while
20
+ * adding no readability — the exact misrepresentation this is trying to
21
+ * avoid.
22
+ */
23
+ import { isAbsent } from "./lineDiff.js";
24
+
25
+ /**
26
+ * Parse a string that encodes a JSON object or array. Returns a wrapper so a
27
+ * successfully decoded `null`-ish payload is still distinguishable from "not
28
+ * decodable" (the prefix check makes that unreachable today, but callers
29
+ * shouldn't have to know that).
30
+ */
31
+ export function parseJsonContainer(value) {
32
+ if (typeof value !== "string") return null;
33
+ const trimmed = value.trim();
34
+ // Cheap gate before JSON.parse: only containers qualify, and this keeps a
35
+ // try/catch off the path for every ordinary string in a diff.
36
+ if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return null;
37
+ try {
38
+ const parsed = JSON.parse(trimmed);
39
+ if (typeof parsed !== "object" || parsed === null) return null;
40
+ return {
41
+ value: parsed
42
+ };
43
+ } catch {
44
+ return null;
45
+ }
46
+ }
47
+ /** A side that carries no value at all — an add or a delete, not a change. */
48
+ function isMissing(value) {
49
+ return isAbsent(value) || value === undefined || value === null;
50
+ }
51
+
52
+ /**
53
+ * Decode both sides of a diff when they are JSON-encoded containers.
54
+ *
55
+ * One side alone is decoded ONLY when the other side is missing entirely
56
+ * (a key being added or removed). A JSON string on one side and an ordinary
57
+ * value on the other is a change of shape, and decoding just one half would
58
+ * present it as something it isn't.
59
+ */
60
+ export function decodeJsonStringPair(left, right) {
61
+ const decodedLeft = parseJsonContainer(left);
62
+ const decodedRight = parseJsonContainer(right);
63
+ if (decodedLeft && decodedRight) {
64
+ return {
65
+ left: decodedLeft.value,
66
+ right: decodedRight.value,
67
+ decoded: true
68
+ };
69
+ }
70
+ if (decodedLeft && isMissing(right)) {
71
+ return {
72
+ left: decodedLeft.value,
73
+ right,
74
+ decoded: true
75
+ };
76
+ }
77
+ if (decodedRight && isMissing(left)) {
78
+ return {
79
+ left,
80
+ right: decodedRight.value,
81
+ decoded: true
82
+ };
83
+ }
84
+ return {
85
+ left,
86
+ right,
87
+ decoded: false
88
+ };
89
+ }
@@ -0,0 +1,139 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Timestamp detection for diff views.
5
+ *
6
+ * `1787085897165 → 1787012175875` is unreadable. It is also the single most
7
+ * common diff in a devtool: `lastSyncTimestamp`, `expiresAt`, `updatedAt`,
8
+ * every cache stamp. This turns that pair into "20h 29m earlier" plus both
9
+ * instants spelled out, WITHOUT hiding the raw values — a devtool that
10
+ * silently reinterprets what is stored is worse than one that says nothing.
11
+ *
12
+ * DETECTION IS DELIBERATELY NARROW, because the failure mode is confidently
13
+ * mislabelling an id or a counter as a date:
14
+ * - Epoch milliseconds only in [2001-09-09, 2100), i.e. 13 digits.
15
+ * - Epoch seconds only in the same window, i.e. 10 digits.
16
+ * The two ranges cannot overlap, so a value is never ambiguous.
17
+ * - ISO 8601 strings that both match the shape AND parse.
18
+ * - BOTH sides must be the same kind. An ISO string against an epoch number
19
+ * is a representation change, not a time delta, and reporting a gap there
20
+ * would bury the more interesting fact.
21
+ */
22
+
23
+ /** Units we recognize. A pair must agree on one of them. */
24
+
25
+ /** 2001-09-09 — below this, a 13-digit-ish number is not a plausible stamp. */
26
+ const MS_MIN = 1_000_000_000_000;
27
+ /** 2100-01-01. */
28
+ const MS_MAX = 4_102_444_800_000;
29
+ const S_MIN = 1_000_000_000;
30
+ const S_MAX = 4_102_444_800;
31
+
32
+ /** Date, or date-time with optional seconds/fraction/zone. */
33
+ const ISO_RE = /^\d{4}-\d{2}-\d{2}(?:[T ]\d{2}:\d{2}(?::\d{2}(?:\.\d{1,9})?)?(?:Z|[+-]\d{2}:?\d{2})?)?$/;
34
+ function fromNumber(n) {
35
+ if (!Number.isInteger(n)) return null;
36
+ if (n >= MS_MIN && n < MS_MAX) return {
37
+ kind: "epoch-ms",
38
+ epochMs: n
39
+ };
40
+ if (n >= S_MIN && n < S_MAX) return {
41
+ kind: "epoch-s",
42
+ epochMs: n * 1000
43
+ };
44
+ return null;
45
+ }
46
+
47
+ /**
48
+ * Read one value as an instant, or null when it isn't confidently a time.
49
+ * Numeric strings count: storage backends hand back everything as strings.
50
+ */
51
+ export function detectTimestamp(value) {
52
+ if (value instanceof Date) {
53
+ const ms = value.getTime();
54
+ return Number.isFinite(ms) ? {
55
+ kind: "iso",
56
+ epochMs: ms
57
+ } : null;
58
+ }
59
+ if (typeof value === "number") return fromNumber(value);
60
+ if (typeof value !== "string") return null;
61
+ const trimmed = value.trim();
62
+ if (trimmed === "") return null;
63
+ if (/^\d+$/.test(trimmed)) return fromNumber(Number(trimmed));
64
+ if (!ISO_RE.test(trimmed)) return null;
65
+ const parsed = Date.parse(trimmed);
66
+ return Number.isFinite(parsed) ? {
67
+ kind: "iso",
68
+ epochMs: parsed
69
+ } : null;
70
+ }
71
+ function two(n) {
72
+ return n < 10 ? `0${n}` : String(n);
73
+ }
74
+
75
+ /** Local wall-clock, fixed format — a devtool wants it sortable, not chatty. */
76
+ export function formatInstant(epochMs) {
77
+ const d = new Date(epochMs);
78
+ return `${d.getFullYear()}-${two(d.getMonth() + 1)}-${two(d.getDate())} ` + `${two(d.getHours())}:${two(d.getMinutes())}:${two(d.getSeconds())}`;
79
+ }
80
+
81
+ /** Coarsest two units that still carry the magnitude: "3d 4h", "5m 30s". */
82
+ export function formatDuration(ms) {
83
+ const abs = Math.abs(ms);
84
+ if (abs < 1000) return `${abs}ms`;
85
+ const seconds = Math.floor(abs / 1000);
86
+ if (seconds < 60) return `${seconds}s`;
87
+ const minutes = Math.floor(seconds / 60);
88
+ const restSeconds = seconds % 60;
89
+ if (minutes < 60) return restSeconds ? `${minutes}m ${restSeconds}s` : `${minutes}m`;
90
+ const hours = Math.floor(minutes / 60);
91
+ const restMinutes = minutes % 60;
92
+ if (hours < 24) return restMinutes ? `${hours}h ${restMinutes}m` : `${hours}h`;
93
+ const days = Math.floor(hours / 24);
94
+ const restHours = hours % 24;
95
+ return restHours ? `${days}d ${restHours}h` : `${days}d`;
96
+ }
97
+ function pair(oldValue, newValue, path) {
98
+ const before = detectTimestamp(oldValue);
99
+ const after = detectTimestamp(newValue);
100
+ if (!before || !after || before.kind !== after.kind) return null;
101
+ const deltaMs = after.epochMs - before.epochMs;
102
+ return {
103
+ path,
104
+ kind: before.kind,
105
+ deltaMs,
106
+ deltaLabel: deltaMs === 0 ? "same instant" : `${formatDuration(deltaMs)} ${deltaMs < 0 ? "earlier" : "later"}`,
107
+ fromLabel: formatInstant(before.epochMs),
108
+ toLabel: formatInstant(after.epochMs)
109
+ };
110
+ }
111
+ function isPlainObject(value) {
112
+ return typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Date);
113
+ }
114
+
115
+ /** Beyond a handful the bar stops being a summary and becomes the diff. */
116
+ const MAX_DELTAS = 6;
117
+
118
+ /**
119
+ * Every timestamp pair worth explaining in a diff of these two values.
120
+ *
121
+ * Scalars give at most one entry. Plain objects are inspected ONE level deep
122
+ * — enough for the `{ updatedAt, expiresAt }` shape that dominates, without
123
+ * walking a whole document to hunt for dates.
124
+ */
125
+ export function describeTimestampDeltas(oldValue, newValue) {
126
+ const scalar = pair(oldValue, newValue);
127
+ if (scalar) return [scalar];
128
+ if (!isPlainObject(oldValue) || !isPlainObject(newValue)) return [];
129
+ const out = [];
130
+ for (const key of Object.keys(oldValue)) {
131
+ if (out.length >= MAX_DELTAS) break;
132
+ if (!(key in newValue)) continue;
133
+ // Unchanged stamps are noise: the point is explaining what MOVED.
134
+ if (Object.is(oldValue[key], newValue[key])) continue;
135
+ const entry = pair(oldValue[key], newValue[key], key);
136
+ if (entry) out.push(entry);
137
+ }
138
+ return out;
139
+ }
@@ -3,7 +3,7 @@
3
3
  /**
4
4
  * Auto-generated safe area implementation
5
5
  * Detected: none
6
- * Generated at: 2026-08-15T04:40:03.239Z
6
+ * Generated at: 2026-08-19T20:01:42.950Z
7
7
  *
8
8
  * DO NOT EDIT - This file is generated by scripts/detect-safe-area.js
9
9
  *
@@ -33,13 +33,15 @@ registerStateSnapshotProvider, getStateSnapshotProviders, getStateSnapshotProvid
33
33
  superJsonSerialize, superJsonDeserialize,
34
34
  // JS-bundle reload (core's `app` adapter + perf-monitor's batch runner)
35
35
  reloadApp, scheduleReloadApp, resolveReloadStrategy, REMOTE_RELOAD_DELAY_MS,
36
+ // Dev-server origin — how a remote driver recovers an app that crashed too
37
+ // hard to reload itself
38
+ getDevServerOrigin,
36
39
  // Subscribable base class for self-managing listeners
37
40
  Subscribable,
38
41
  // Subscriber count notifier for cross-package notifications
39
42
  subscriberCountNotifier, subscribeToSubscriberCountChanges, notifySubscriberCountChange,
40
43
  // Safe expo-router wrappers (falls back to no-ops on RN CLI)
41
- useSafeRouter, useSafePathname, useSafeSegments, useSafeGlobalSearchParams, getSafeRouter, getSafeCurrentPathname, isExpoRouterAvailable } from "./utils/index.js";
42
-
44
+ useSafeRouter, useSafePathname, useSafeSegments, useSafeGlobalSearchParams, getSafeRouter, getSafeCurrentPathname, getSafeCurrentRoute, isExpoRouterAvailable } from "./utils/index.js";
43
45
  // Also export formatting utils
44
46
  export * from "./utils/formatting/index.js";
45
47
  export * from "./utils/time/index.js";
@@ -80,4 +82,4 @@ export { HintsProvider, useHintsDisabled, useHintsContext } from "./context/inde
80
82
  export * from "./history/index.js";
81
83
 
82
84
  // License/Feature gating exports
83
- export { FeatureGate, UpgradePrompt, useFeatureGate, ProFeatureBanner, UpgradeModal, LicenseEntryModal, ProBadge, ProUpgradeModal, getUseIsPro, getUseLicense } from "./license/index.js";
85
+ export { FeatureGate, UpgradePrompt, useFeatureGate, ProFeatureBanner, UpgradeModal, LicenseEntryModal, ProBadge, ProUpgradeModal, FreeAccountPrompt, LOGIN_COMMAND, getUseIsPro, getUseLicense, getUseTier, TIER_LIMITS, useLimit, resolveEffectiveTier, freeKeyRaises } from "./license/index.js";
@@ -12,6 +12,9 @@ import { gameUIColors, buoyColors } from "../ui/gameUI/constants/gameUIColors.js
12
12
  import { LockIcon, X, Zap, Shield, Clock, Check } from "../icons/lucide-icons.js";
13
13
  import { LicenseEntryModal } from "./LicenseEntryModal.js";
14
14
  import { openPricing, isWeb } from "./openPricing.js";
15
+ import { FreeAccountPrompt } from "./FreeAccountPrompt.js";
16
+ import { getUseTier } from "./licenseModule.js";
17
+ import { freeKeyRaises } from "./tierLimits.js";
15
18
  import { useIsPro, useLicense, useProAccess } from "@buoy-gg/license";
16
19
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
17
20
  // Weekend Pass branding — a distinct violet so it never reads as the gold PRO.
@@ -49,14 +52,19 @@ export const ProBadge = ({
49
52
  export const ProUpgradeModal = ({
50
53
  visible,
51
54
  onClose,
52
- featureName = "This feature"
55
+ featureName = "This feature",
56
+ limitKey
53
57
  }) => {
54
58
  // Simple wrapper around UpgradeModal with minimal description and no benefits
55
59
  return /*#__PURE__*/_jsx(UpgradeModal, {
56
60
  visible: visible,
57
61
  onClose: onClose,
58
- featureName: featureName,
59
- description: `${featureName} requires a Pro subscription.`
62
+ featureName: featureName
63
+ // Only meaningful when this upsell is about a LIMIT; without it the
64
+ // description below (which promises Pro) is the accurate message.
65
+ ,
66
+ limitKey: limitKey,
67
+ description: limitKey ? undefined : `${featureName} requires a Pro subscription.`
60
68
  });
61
69
  };
62
70
  /**
@@ -66,9 +74,13 @@ export const ProUpgradeModal = ({
66
74
  export const UpgradePrompt = ({
67
75
  featureName,
68
76
  onUpgradePress,
69
- compact = false
77
+ compact = false,
78
+ limitKey
70
79
  }) => {
71
80
  const [showLicenseModal, setShowLicenseModal] = useState(false);
81
+ // Only offer the free key when it would actually raise this limit — see
82
+ // UpgradeModalProps.limitKey.
83
+ const isAnonymous = getUseTier()() === "anonymous" && limitKey != null && freeKeyRaises(limitKey);
72
84
  const handlePress = useCallback(() => {
73
85
  if (onUpgradePress) {
74
86
  onUpgradePress();
@@ -96,7 +108,11 @@ export const UpgradePrompt = ({
96
108
  style: styles.compactText,
97
109
  children: "PRO"
98
110
  })]
99
- }), /*#__PURE__*/_jsx(LicenseEntryModal, {
111
+ }), isAnonymous ? /*#__PURE__*/_jsx(FreeAccountPrompt, {
112
+ visible: showLicenseModal,
113
+ onClose: handleCloseModal,
114
+ featureName: featureName
115
+ }) : /*#__PURE__*/_jsx(LicenseEntryModal, {
100
116
  visible: showLicenseModal,
101
117
  onClose: handleCloseModal
102
118
  })]
@@ -113,23 +129,27 @@ export const UpgradePrompt = ({
113
129
  })
114
130
  }), /*#__PURE__*/_jsx(Text, {
115
131
  style: styles.title,
116
- children: "Pro Feature"
132
+ children: isAnonymous ? "Get a free key" : "Pro Feature"
117
133
  }), featureName && /*#__PURE__*/_jsx(Text, {
118
134
  style: styles.featureName,
119
135
  children: featureName
120
136
  }), /*#__PURE__*/_jsx(Text, {
121
137
  style: styles.description,
122
- children: "Upgrade to React Buoy Pro to unlock this feature and more."
138
+ children: isAnonymous ? "Buoy works without an account, with a small slice of each tool. A free key opens it up." : "Upgrade to React Buoy Pro to unlock this feature and more."
123
139
  }), /*#__PURE__*/_jsx(TouchableOpacity, {
124
140
  style: styles.upgradeButton,
125
141
  onPress: handlePress,
126
142
  activeOpacity: 0.8,
127
143
  children: /*#__PURE__*/_jsx(Text, {
128
144
  style: styles.upgradeButtonText,
129
- children: "Upgrade to Pro"
145
+ children: isAnonymous ? "Get a free key" : "Upgrade to Pro"
130
146
  })
131
147
  })]
132
- }), /*#__PURE__*/_jsx(LicenseEntryModal, {
148
+ }), isAnonymous ? /*#__PURE__*/_jsx(FreeAccountPrompt, {
149
+ visible: showLicenseModal,
150
+ onClose: handleCloseModal,
151
+ featureName: featureName
152
+ }) : /*#__PURE__*/_jsx(LicenseEntryModal, {
133
153
  visible: showLicenseModal,
134
154
  onClose: handleCloseModal
135
155
  })]
@@ -308,8 +328,14 @@ export const UpgradeModal = ({
308
328
  onClose,
309
329
  featureName = "This Feature",
310
330
  description,
311
- benefits
331
+ benefits,
332
+ limitKey
312
333
  }) => {
334
+ // Anonymous users get a different ask — but ONLY when a free key would
335
+ // actually fix what they just hit. Everything else here is Pro-exclusive,
336
+ // where a free key changes nothing and offering one would be dishonest.
337
+ const tier = getUseTier()();
338
+ const offerFreeKey = tier === "anonymous" && limitKey != null && freeKeyRaises(limitKey);
313
339
  const hasBenefits = benefits && benefits.length > 0;
314
340
  const [showLicenseModal, setShowLicenseModal] = useState(false);
315
341
  const license = useLicense();
@@ -332,6 +358,13 @@ export const UpgradeModal = ({
332
358
  onClose();
333
359
  }, [onClose]);
334
360
  if (!visible) return null;
361
+ if (offerFreeKey) {
362
+ return /*#__PURE__*/_jsx(FreeAccountPrompt, {
363
+ visible: visible,
364
+ onClose: onClose,
365
+ featureName: featureName === "This Feature" ? undefined : featureName
366
+ });
367
+ }
335
368
  return /*#__PURE__*/_jsxs(_Fragment, {
336
369
  children: [/*#__PURE__*/_jsx(Modal, {
337
370
  visible: visible && !showLicenseModal,
@@ -0,0 +1,176 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * The upsell an ANONYMOUS user sees — someone with no key at all.
5
+ *
6
+ * Deliberately not the Pro modal. Their next step is free and takes about
7
+ * thirty seconds, so asking them for money would be asking the wrong question
8
+ * at the wrong moment, and would suppress exactly the signup this exists to
9
+ * produce. Pro is still offered, as a secondary action.
10
+ *
11
+ * The command is shown inline and copyable rather than linked, because there is
12
+ * no grace period: someone who just watched their limits shrink needs the fix
13
+ * in front of them, not a link to a page that has a link to a command.
14
+ */
15
+
16
+ import React, { useCallback, useEffect, useRef, useState } from "react";
17
+ import { Modal, StyleSheet, Text, TouchableOpacity } from "react-native";
18
+ import { buoyColors } from "../ui/gameUI/constants/gameUIColors.js";
19
+ import { Check, Copy, X } from "../icons/lucide-icons.js";
20
+ import { copyToClipboard } from "../clipboard/index.js";
21
+ import { openPricing } from "./openPricing.js";
22
+
23
+ /** The one thing a reader has to be able to act on. */
24
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
25
+ export const LOGIN_COMMAND = "npx buoy login";
26
+ export const FreeAccountPrompt = ({
27
+ visible,
28
+ onClose,
29
+ featureName
30
+ }) => {
31
+ const [copied, setCopied] = useState(false);
32
+ const resetTimer = useRef(null);
33
+ useEffect(() => () => {
34
+ if (resetTimer.current) clearTimeout(resetTimer.current);
35
+ }, []);
36
+ const handleCopy = useCallback(() => {
37
+ void copyToClipboard(LOGIN_COMMAND).then(ok => {
38
+ if (!ok) return;
39
+ setCopied(true);
40
+ if (resetTimer.current) clearTimeout(resetTimer.current);
41
+ resetTimer.current = setTimeout(() => setCopied(false), 1500);
42
+ });
43
+ }, []);
44
+ if (!visible) return null;
45
+ return /*#__PURE__*/_jsx(Modal, {
46
+ visible: visible,
47
+ transparent: true,
48
+ animationType: "fade",
49
+ onRequestClose: onClose,
50
+ children: /*#__PURE__*/_jsx(TouchableOpacity, {
51
+ style: styles.overlay,
52
+ onPress: onClose,
53
+ activeOpacity: 1,
54
+ children: /*#__PURE__*/_jsxs(TouchableOpacity, {
55
+ style: styles.card,
56
+ activeOpacity: 1,
57
+ onPress: () => {},
58
+ children: [/*#__PURE__*/_jsx(TouchableOpacity, {
59
+ style: styles.close,
60
+ onPress: onClose,
61
+ activeOpacity: 0.7,
62
+ children: /*#__PURE__*/_jsx(X, {
63
+ size: 18,
64
+ color: buoyColors.textMuted
65
+ })
66
+ }), /*#__PURE__*/_jsx(Text, {
67
+ style: styles.title,
68
+ children: featureName ? `More ${featureName}` : "See more"
69
+ }), /*#__PURE__*/_jsx(Text, {
70
+ style: styles.subtitle,
71
+ children: "Buoy runs without an account \u2014 you just get a small slice of each tool. A free key opens it up."
72
+ }), /*#__PURE__*/_jsxs(TouchableOpacity, {
73
+ style: styles.command,
74
+ onPress: handleCopy,
75
+ activeOpacity: 0.7,
76
+ accessibilityLabel: `Copy ${LOGIN_COMMAND}`,
77
+ children: [/*#__PURE__*/_jsx(Text, {
78
+ style: styles.commandText,
79
+ children: LOGIN_COMMAND
80
+ }), copied ? /*#__PURE__*/_jsx(Check, {
81
+ size: 14,
82
+ color: buoyColors.success
83
+ }) : /*#__PURE__*/_jsx(Copy, {
84
+ size: 14,
85
+ color: buoyColors.textMuted
86
+ })]
87
+ }), /*#__PURE__*/_jsx(Text, {
88
+ style: styles.reassure,
89
+ children: "Free forever. No card. About thirty seconds."
90
+ }), /*#__PURE__*/_jsx(TouchableOpacity, {
91
+ style: styles.secondary,
92
+ onPress: () => openPricing(),
93
+ activeOpacity: 0.8,
94
+ children: /*#__PURE__*/_jsx(Text, {
95
+ style: styles.secondaryText,
96
+ children: "Or see Pro"
97
+ })
98
+ })]
99
+ })
100
+ })
101
+ });
102
+ };
103
+ const styles = StyleSheet.create({
104
+ overlay: {
105
+ flex: 1,
106
+ backgroundColor: "rgba(0, 0, 0, 0.75)",
107
+ alignItems: "center",
108
+ justifyContent: "center",
109
+ padding: 20
110
+ },
111
+ card: {
112
+ backgroundColor: buoyColors.card,
113
+ borderRadius: 16,
114
+ borderWidth: 1,
115
+ borderColor: buoyColors.primary + "60",
116
+ padding: 24,
117
+ maxWidth: 340,
118
+ width: "100%",
119
+ position: "relative"
120
+ },
121
+ close: {
122
+ position: "absolute",
123
+ top: 12,
124
+ right: 12,
125
+ padding: 6,
126
+ zIndex: 1
127
+ },
128
+ title: {
129
+ fontSize: 20,
130
+ fontWeight: "700",
131
+ color: buoyColors.text,
132
+ textAlign: "center",
133
+ marginBottom: 8
134
+ },
135
+ subtitle: {
136
+ fontSize: 13,
137
+ lineHeight: 19,
138
+ color: buoyColors.textMuted,
139
+ textAlign: "center",
140
+ marginBottom: 20
141
+ },
142
+ command: {
143
+ flexDirection: "row",
144
+ alignItems: "center",
145
+ justifyContent: "space-between",
146
+ backgroundColor: buoyColors.base,
147
+ borderRadius: 8,
148
+ borderWidth: 1,
149
+ borderColor: buoyColors.border,
150
+ paddingHorizontal: 14,
151
+ paddingVertical: 12,
152
+ gap: 10
153
+ },
154
+ commandText: {
155
+ fontSize: 14,
156
+ fontWeight: "600",
157
+ color: buoyColors.text,
158
+ fontFamily: "monospace"
159
+ },
160
+ reassure: {
161
+ fontSize: 12,
162
+ color: buoyColors.textMuted,
163
+ textAlign: "center",
164
+ marginTop: 10
165
+ },
166
+ secondary: {
167
+ marginTop: 18,
168
+ paddingVertical: 10,
169
+ alignItems: "center"
170
+ },
171
+ secondaryText: {
172
+ fontSize: 13,
173
+ fontWeight: "600",
174
+ color: buoyColors.textMuted
175
+ }
176
+ });