@buoy-gg/time-machine 7.0.35 → 7.0.38

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 (145) hide show
  1. package/README.md +40 -35
  2. package/lib/commonjs/components/HeaderActions.js +91 -0
  3. package/lib/commonjs/components/HeaderReloadButton.js +22 -25
  4. package/lib/commonjs/components/TimeMachineBody.js +68 -13
  5. package/lib/commonjs/components/floating/NamePromptModal.js +193 -0
  6. package/lib/commonjs/components/floating/TimeMachineFloatingTool.js +715 -0
  7. package/lib/commonjs/components/floating/armedPoint.js +92 -0
  8. package/lib/commonjs/components/floating/barSettings.js +66 -0
  9. package/lib/commonjs/components/list/CaptureBar.js +2 -0
  10. package/lib/commonjs/components/list/RestoreOutcomeCard.js +2 -0
  11. package/lib/commonjs/components/list/SnapshotRow.js +37 -13
  12. package/lib/commonjs/components/list/SourcesStrip.js +127 -83
  13. package/lib/commonjs/components/list/UndoDeleteBar.js +3 -1
  14. package/lib/commonjs/components/preview/RestorePreviewScreen.js +7 -2
  15. package/lib/commonjs/components/sources.js +19 -8
  16. package/lib/commonjs/index.js +42 -0
  17. package/lib/commonjs/preset.js +6 -4
  18. package/lib/commonjs/store/sourceIds.js +9 -0
  19. package/lib/commonjs/store/sourceSettings.js +79 -0
  20. package/lib/commonjs/store/timeMachineStore.js +75 -10
  21. package/lib/commonjs/sync/timeMachineSyncAdapter.js +5 -1
  22. package/lib/commonjs/ui/SwipeActionsRow.js +14 -3
  23. package/lib/commonjs/ui/primitives.js +2 -0
  24. package/lib/commonjs/ui/tmTheme.js +18 -5
  25. package/lib/module/components/HeaderActions.js +85 -0
  26. package/lib/module/components/HeaderReloadButton.js +24 -27
  27. package/lib/module/components/TimeMachineBody.js +71 -16
  28. package/lib/module/components/floating/NamePromptModal.js +188 -0
  29. package/lib/module/components/floating/TimeMachineFloatingTool.js +708 -0
  30. package/lib/module/components/floating/armedPoint.js +82 -0
  31. package/lib/module/components/floating/barSettings.js +57 -0
  32. package/lib/module/components/list/CaptureBar.js +2 -0
  33. package/lib/module/components/list/RestoreOutcomeCard.js +2 -0
  34. package/lib/module/components/list/SnapshotRow.js +37 -13
  35. package/lib/module/components/list/SourcesStrip.js +130 -84
  36. package/lib/module/components/list/UndoDeleteBar.js +3 -1
  37. package/lib/module/components/preview/RestorePreviewScreen.js +7 -2
  38. package/lib/module/components/sources.js +13 -6
  39. package/lib/module/index.js +7 -1
  40. package/lib/module/preset.js +6 -4
  41. package/lib/module/store/sourceIds.js +5 -0
  42. package/lib/module/store/sourceSettings.js +69 -0
  43. package/lib/module/store/timeMachineStore.js +74 -9
  44. package/lib/module/sync/timeMachineSyncAdapter.js +5 -1
  45. package/lib/module/ui/SwipeActionsRow.js +14 -3
  46. package/lib/module/ui/primitives.js +2 -0
  47. package/lib/module/ui/tmTheme.js +18 -5
  48. package/lib/typescript/components/HeaderActions.d.ts +7 -0
  49. package/lib/typescript/components/floating/NamePromptModal.d.ts +9 -0
  50. package/lib/typescript/components/floating/TimeMachineFloatingTool.d.ts +6 -0
  51. package/lib/typescript/components/floating/armedPoint.d.ts +14 -0
  52. package/lib/typescript/components/floating/barSettings.d.ts +7 -0
  53. package/lib/typescript/components/list/SnapshotRow.d.ts +5 -1
  54. package/lib/typescript/components/list/SourcesStrip.d.ts +3 -1
  55. package/lib/typescript/components/sources.d.ts +7 -2
  56. package/lib/typescript/index.d.ts +3 -1
  57. package/lib/typescript/preset.d.ts +4 -2
  58. package/lib/typescript/store/snapshotVault.d.ts +6 -0
  59. package/lib/typescript/store/sourceIds.d.ts +3 -0
  60. package/lib/typescript/store/sourceSettings.d.ts +9 -0
  61. package/lib/typescript/store/timeMachineStore.d.ts +16 -1
  62. package/lib/typescript/sync/timeMachineSyncAdapter.d.ts +4 -0
  63. package/lib/typescript/ui/tmTheme.d.ts +17 -4
  64. package/package.json +11 -8
  65. package/lib/commonjs/components/HeaderReloadButton.js.map +0 -1
  66. package/lib/commonjs/components/TimeMachineBody.js.map +0 -1
  67. package/lib/commonjs/components/TimeMachineModal.js.map +0 -1
  68. package/lib/commonjs/components/confirm.js.map +0 -1
  69. package/lib/commonjs/components/format.js.map +0 -1
  70. package/lib/commonjs/components/list/CaptureBar.js.map +0 -1
  71. package/lib/commonjs/components/list/RestoreOutcomeCard.js.map +0 -1
  72. package/lib/commonjs/components/list/SnapshotRow.js.map +0 -1
  73. package/lib/commonjs/components/list/SourcesStrip.js.map +0 -1
  74. package/lib/commonjs/components/list/UndoDeleteBar.js.map +0 -1
  75. package/lib/commonjs/components/list/WipeAllRow.js.map +0 -1
  76. package/lib/commonjs/components/preview/ItemDiffPanel.js.map +0 -1
  77. package/lib/commonjs/components/preview/PreviewFlow.js.map +0 -1
  78. package/lib/commonjs/components/preview/RestorePointNameRow.js.map +0 -1
  79. package/lib/commonjs/components/preview/RestorePreviewScreen.js.map +0 -1
  80. package/lib/commonjs/components/preview/verdictMeta.js.map +0 -1
  81. package/lib/commonjs/components/sources.js.map +0 -1
  82. package/lib/commonjs/hooks/useTimeMachine.js.map +0 -1
  83. package/lib/commonjs/index.js.map +0 -1
  84. package/lib/commonjs/preset.js.map +0 -1
  85. package/lib/commonjs/store/routeRestore.js.map +0 -1
  86. package/lib/commonjs/store/snapshotVault.js.map +0 -1
  87. package/lib/commonjs/store/timeMachineStore.js.map +0 -1
  88. package/lib/commonjs/sync/timeMachineSyncAdapter.js.map +0 -1
  89. package/lib/commonjs/ui/SwipeActionsRow.js.map +0 -1
  90. package/lib/commonjs/ui/primitives.js.map +0 -1
  91. package/lib/commonjs/ui/tmTheme.js.map +0 -1
  92. package/lib/module/components/HeaderReloadButton.js.map +0 -1
  93. package/lib/module/components/TimeMachineBody.js.map +0 -1
  94. package/lib/module/components/TimeMachineModal.js.map +0 -1
  95. package/lib/module/components/confirm.js.map +0 -1
  96. package/lib/module/components/format.js.map +0 -1
  97. package/lib/module/components/list/CaptureBar.js.map +0 -1
  98. package/lib/module/components/list/RestoreOutcomeCard.js.map +0 -1
  99. package/lib/module/components/list/SnapshotRow.js.map +0 -1
  100. package/lib/module/components/list/SourcesStrip.js.map +0 -1
  101. package/lib/module/components/list/UndoDeleteBar.js.map +0 -1
  102. package/lib/module/components/list/WipeAllRow.js.map +0 -1
  103. package/lib/module/components/preview/ItemDiffPanel.js.map +0 -1
  104. package/lib/module/components/preview/PreviewFlow.js.map +0 -1
  105. package/lib/module/components/preview/RestorePointNameRow.js.map +0 -1
  106. package/lib/module/components/preview/RestorePreviewScreen.js.map +0 -1
  107. package/lib/module/components/preview/verdictMeta.js.map +0 -1
  108. package/lib/module/components/sources.js.map +0 -1
  109. package/lib/module/hooks/useTimeMachine.js.map +0 -1
  110. package/lib/module/index.js.map +0 -1
  111. package/lib/module/preset.js.map +0 -1
  112. package/lib/module/store/routeRestore.js.map +0 -1
  113. package/lib/module/store/snapshotVault.js.map +0 -1
  114. package/lib/module/store/timeMachineStore.js.map +0 -1
  115. package/lib/module/sync/timeMachineSyncAdapter.js.map +0 -1
  116. package/lib/module/ui/SwipeActionsRow.js.map +0 -1
  117. package/lib/module/ui/primitives.js.map +0 -1
  118. package/lib/module/ui/tmTheme.js.map +0 -1
  119. package/lib/typescript/components/HeaderReloadButton.d.ts.map +0 -1
  120. package/lib/typescript/components/TimeMachineBody.d.ts.map +0 -1
  121. package/lib/typescript/components/TimeMachineModal.d.ts.map +0 -1
  122. package/lib/typescript/components/confirm.d.ts.map +0 -1
  123. package/lib/typescript/components/format.d.ts.map +0 -1
  124. package/lib/typescript/components/list/CaptureBar.d.ts.map +0 -1
  125. package/lib/typescript/components/list/RestoreOutcomeCard.d.ts.map +0 -1
  126. package/lib/typescript/components/list/SnapshotRow.d.ts.map +0 -1
  127. package/lib/typescript/components/list/SourcesStrip.d.ts.map +0 -1
  128. package/lib/typescript/components/list/UndoDeleteBar.d.ts.map +0 -1
  129. package/lib/typescript/components/list/WipeAllRow.d.ts.map +0 -1
  130. package/lib/typescript/components/preview/ItemDiffPanel.d.ts.map +0 -1
  131. package/lib/typescript/components/preview/PreviewFlow.d.ts.map +0 -1
  132. package/lib/typescript/components/preview/RestorePointNameRow.d.ts.map +0 -1
  133. package/lib/typescript/components/preview/RestorePreviewScreen.d.ts.map +0 -1
  134. package/lib/typescript/components/preview/verdictMeta.d.ts.map +0 -1
  135. package/lib/typescript/components/sources.d.ts.map +0 -1
  136. package/lib/typescript/hooks/useTimeMachine.d.ts.map +0 -1
  137. package/lib/typescript/index.d.ts.map +0 -1
  138. package/lib/typescript/preset.d.ts.map +0 -1
  139. package/lib/typescript/store/routeRestore.d.ts.map +0 -1
  140. package/lib/typescript/store/snapshotVault.d.ts.map +0 -1
  141. package/lib/typescript/store/timeMachineStore.d.ts.map +0 -1
  142. package/lib/typescript/sync/timeMachineSyncAdapter.d.ts.map +0 -1
  143. package/lib/typescript/ui/SwipeActionsRow.d.ts.map +0 -1
  144. package/lib/typescript/ui/primitives.d.ts.map +0 -1
  145. package/lib/typescript/ui/tmTheme.d.ts.map +0 -1
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Which restore point the floating restore bar restores on tap.
5
+ *
6
+ * The bar needs an answer before the user picks anything, or its first tap is
7
+ * a picker instead of a restore. So the armed point is resolved, not just
8
+ * stored: an explicit pick wins, then the point restored most recently, then
9
+ * the newest capture. Baselines never resolve; restoring one wipes storage
10
+ * and reloads behind a confirm dialog, which is not a one-tap action. The
11
+ * safety slot never resolves either: Undo is its way in.
12
+ *
13
+ * The explicit pick is persisted so it survives the reloads the loop itself
14
+ * causes (RELOAD APP, reload-mode restores).
15
+ */
16
+ import { devToolsStorageKeys, persistentStorage } from "@buoy-gg/shared-ui";
17
+ const KEY = devToolsStorageKeys.timeMachine.armedPoint();
18
+ let pickedId = null;
19
+ let loaded = false;
20
+ let loadPromise = null;
21
+ const listeners = new Set();
22
+ const notify = () => listeners.forEach(listener => listener());
23
+ export function getPickedPointId() {
24
+ return pickedId;
25
+ }
26
+ export function subscribePickedPoint(listener) {
27
+ listeners.add(listener);
28
+ return () => {
29
+ listeners.delete(listener);
30
+ };
31
+ }
32
+
33
+ /** Read the saved pick once. Later calls return the same promise. */
34
+ export function ensurePickedPointLoaded() {
35
+ if (loaded) return Promise.resolve();
36
+ if (!loadPromise) {
37
+ loadPromise = persistentStorage.getItem(KEY).then(saved => {
38
+ // A pick made while the read was in flight wins over the disk copy.
39
+ if (pickedId === null && saved) {
40
+ pickedId = saved;
41
+ notify();
42
+ }
43
+ }).catch(() => {}).finally(() => {
44
+ loaded = true;
45
+ });
46
+ }
47
+ return loadPromise;
48
+ }
49
+ export function setPickedPointId(id) {
50
+ if (pickedId === id) return;
51
+ pickedId = id;
52
+ notify();
53
+ void (id === null ? persistentStorage.removeItem(KEY) : persistentStorage.setItem(KEY, id)).catch(() => {});
54
+ }
55
+
56
+ /** Test seam: forget the pick and the load, without touching storage. */
57
+ export function resetPickedPointForTests() {
58
+ pickedId = null;
59
+ loaded = false;
60
+ loadPromise = null;
61
+ }
62
+
63
+ /**
64
+ * The point the bar restores: the saved pick if it still exists, else the
65
+ * most recently restored point, else the newest capture. Never a baseline.
66
+ */
67
+ export function resolveArmedPoint(snapshots, pickedId) {
68
+ const candidates = snapshots.filter(meta => !meta.baseline && !meta.safety);
69
+ if (candidates.length === 0) return null;
70
+ if (pickedId) {
71
+ const picked = candidates.find(meta => meta.id === pickedId);
72
+ if (picked) return picked;
73
+ }
74
+ let best = null;
75
+ for (const meta of candidates) {
76
+ if (meta.lastRestoredAt === undefined) continue;
77
+ if (!best || meta.lastRestoredAt > (best.lastRestoredAt ?? 0)) best = meta;
78
+ }
79
+ if (best) return best;
80
+ return candidates.reduce((newest, meta) => meta.createdAt > newest.createdAt ? meta : newest);
81
+ }
82
+ //# sourceMappingURL=armedPoint.js.map
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * The action bar's one setting: restore live, or restore and reload.
5
+ *
6
+ * Live restore replaces stores in place and is right for state that lives in
7
+ * Redux, Zustand, Jotai or the query cache. It cannot reach a React context
8
+ * or `useState` that hydrates itself from storage once at mount: the
9
+ * storage key comes back, the screen keeps showing what it already had, and
10
+ * "restore did nothing" is the report. A restore that reloads the app makes
11
+ * those screens rebuild from the restored storage. Persisted, because the
12
+ * reload itself is what brings the bar back.
13
+ */
14
+ import { devToolsStorageKeys, persistentStorage } from "@buoy-gg/shared-ui";
15
+ const KEY = devToolsStorageKeys.timeMachine.barReloadMode();
16
+ let reloadMode = false;
17
+ let loaded = false;
18
+ let loadPromise = null;
19
+ const listeners = new Set();
20
+ const notify = () => listeners.forEach(listener => listener());
21
+ export function getBarReloadMode() {
22
+ return reloadMode;
23
+ }
24
+ export function subscribeBarSettings(listener) {
25
+ listeners.add(listener);
26
+ return () => {
27
+ listeners.delete(listener);
28
+ };
29
+ }
30
+ export function ensureBarSettingsLoaded() {
31
+ if (loaded) return Promise.resolve();
32
+ if (!loadPromise) {
33
+ loadPromise = persistentStorage.getItem(KEY).then(saved => {
34
+ if (!reloadMode && saved === "1") {
35
+ reloadMode = true;
36
+ notify();
37
+ }
38
+ }).catch(() => {}).finally(() => {
39
+ loaded = true;
40
+ });
41
+ }
42
+ return loadPromise;
43
+ }
44
+ export function setBarReloadMode(on) {
45
+ if (on === reloadMode) return;
46
+ reloadMode = on;
47
+ notify();
48
+ void persistentStorage.setItem(KEY, on ? "1" : "0").catch(() => {});
49
+ }
50
+
51
+ /** Test seam. */
52
+ export function resetBarSettingsForTests() {
53
+ reloadMode = false;
54
+ loaded = false;
55
+ loadPromise = null;
56
+ }
57
+ //# sourceMappingURL=barSettings.js.map
@@ -56,6 +56,8 @@ const styles = StyleSheet.create({
56
56
  marginHorizontal: tm.gutter,
57
57
  backgroundColor: tm.color.group,
58
58
  borderRadius: tm.radius.group,
59
+ borderWidth: 1,
60
+ borderColor: tm.color.groupBorder,
59
61
  paddingLeft: tm.textInset,
60
62
  padding: 6
61
63
  },
@@ -130,6 +130,8 @@ const styles = StyleSheet.create({
130
130
  marginTop: 12,
131
131
  backgroundColor: tm.color.group,
132
132
  borderRadius: tm.radius.group,
133
+ borderWidth: 1,
134
+ borderColor: tm.color.groupBorder,
133
135
  padding: 14,
134
136
  gap: 4
135
137
  },
@@ -29,20 +29,29 @@ function describe(meta) {
29
29
  // A saved selection is worth stating on the row — this restore point no
30
30
  // longer does the obvious thing, and one tap on RESTORE honors it.
31
31
  if (meta.excludedKeys?.length) parts.push(`skips ${meta.excludedKeys.length}`);else if (meta.scope?.length) parts.push(`scoped (${meta.scope.length})`);
32
+ // The bar's own undo slot: kept for one restore, not a point you chose.
33
+ if (meta.safety) parts.unshift("safety copy");
32
34
  return parts.join(" · ");
33
35
  }
34
36
  export function SnapshotRow({
35
37
  meta,
36
38
  busy,
39
+ armed = false,
37
40
  onOpen,
38
41
  onRestore,
39
42
  onDelete,
40
- onDuplicate
43
+ onDuplicate,
44
+ onArm
41
45
  }) {
42
46
  const testID = `time-machine-snap-${meta.id}`;
43
47
  return /*#__PURE__*/_jsx(SwipeActionsRow, {
44
48
  testID: testID,
45
49
  actions: [
50
+ // "Bar": put this point on the floating action bar.
51
+ {
52
+ label: "Bar",
53
+ onPress: onArm
54
+ },
46
55
  // "Dup" over "Copy": on a row full of text, "Copy" reads as
47
56
  // copy-to-clipboard.
48
57
  {
@@ -54,21 +63,26 @@ export function SnapshotRow({
54
63
  background: tm.color.danger,
55
64
  color: tm.color.onDanger
56
65
  }],
57
- children: /*#__PURE__*/_jsxs(TouchableOpacity, {
66
+ children: /*#__PURE__*/_jsxs(View, {
58
67
  style: styles.row,
59
- onPress: onOpen,
60
- activeOpacity: 0.7,
61
- accessibilityRole: "button",
62
- accessibilityLabel: `${meta.name}. ${describe(meta)}. Open restore preview`,
63
- testID: testID,
64
- children: [/*#__PURE__*/_jsxs(View, {
68
+ children: [/*#__PURE__*/_jsxs(TouchableOpacity, {
65
69
  style: styles.text,
70
+ onPress: onOpen,
71
+ activeOpacity: 0.7,
72
+ accessibilityRole: "button",
73
+ accessibilityLabel: `${meta.name}. ${describe(meta)}. Open restore preview`,
74
+ testID: testID,
66
75
  children: [/*#__PURE__*/_jsx(Text, {
67
76
  style: styles.name,
68
77
  numberOfLines: 1,
69
78
  testID: `${testID}-name`,
70
79
  children: meta.name
71
- }), /*#__PURE__*/_jsx(Text, {
80
+ }), armed ? /*#__PURE__*/_jsx(Text, {
81
+ style: styles.armed,
82
+ numberOfLines: 1,
83
+ testID: `${testID}-armed`,
84
+ children: "On the action bar"
85
+ }) : null, /*#__PURE__*/_jsx(Text, {
72
86
  style: styles.meta,
73
87
  numberOfLines: 1,
74
88
  testID: `${testID}-meta`,
@@ -87,7 +101,7 @@ export function SnapshotRow({
87
101
  }), /*#__PURE__*/_jsx(TouchableOpacity, {
88
102
  onPress: onRestore,
89
103
  disabled: busy,
90
- style: busy ? styles.actionDisabled : null,
104
+ style: [styles.restore, busy ? styles.actionDisabled : null],
91
105
  accessibilityRole: "button",
92
106
  accessibilityLabel: meta.baseline ? "Wipe and reload" : "Restore",
93
107
  testID: `${testID}-restore`,
@@ -106,12 +120,17 @@ const styles = StyleSheet.create({
106
120
  flexDirection: "row",
107
121
  alignItems: "center",
108
122
  gap: 10,
109
- paddingHorizontal: tm.textInset,
110
- paddingVertical: 11
123
+ paddingHorizontal: tm.textInset
111
124
  },
125
+ // The text block is the tallest child, so its padding sets the row height
126
+ // and the open target spans the full row.
112
127
  text: {
113
128
  flex: 1,
114
- minWidth: 0
129
+ minWidth: 0,
130
+ paddingVertical: 11
131
+ },
132
+ restore: {
133
+ justifyContent: "center"
115
134
  },
116
135
  name: {
117
136
  color: tm.color.label,
@@ -130,6 +149,11 @@ const styles = StyleSheet.create({
130
149
  fontSize: tm.font.footnote,
131
150
  marginTop: 2
132
151
  },
152
+ armed: {
153
+ color: tm.color.accent,
154
+ fontSize: tm.font.footnote,
155
+ marginTop: 2
156
+ },
133
157
  route: {
134
158
  color: tm.color.labelTertiary,
135
159
  fontFamily: TM_MONO,
@@ -1,123 +1,169 @@
1
1
  "use strict";
2
2
 
3
3
  /**
4
- * Sources — one horizontally scrollable line of what a capture will pick up.
4
+ * Sources — one settings row per source, each with a switch.
5
5
  *
6
- * Each source is its own tile (icon over name) rather than a count behind a
7
- * disclosure: "is Redux wired?" is a glance question, and the answer should
8
- * not cost a screen. A source that can't fully participate dims and carries a
9
- * status dot, with the reason spelled out underneath the strip.
6
+ * The iOS Settings shape the rest of the tool uses, because that is what a
7
+ * list of things you can turn on and off looks like: icon, name, a line of
8
+ * status, and a switch at the trailing edge. Off means every capture and
9
+ * restore leaves that source alone, for the store whose full replace the
10
+ * app cannot survive.
11
+ *
12
+ * Only sources the app has wired are listed; a library the app does not use
13
+ * is not a choice to make. Icons keep full brightness in every state. Under
14
+ * each name is what the source holds, in plain words; a short state line
15
+ * appears only when something is off or capture-only.
10
16
  */
11
- import { ScrollView, StyleSheet, Text, View } from "react-native";
17
+ import { StyleSheet, Text, View } from "react-native";
18
+ import { NightSwitch } from "@buoy-gg/shared-ui";
19
+ import { Group } from "../../ui/primitives";
12
20
  import { tm } from "../../ui/tmTheme";
13
21
  import { sourceIcon } from "../sources";
22
+
23
+ /**
24
+ * What each source holds, in words for someone who has never heard of the
25
+ * library. The library name is the row title; this line is the meaning.
26
+ */
14
27
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
15
- function statusDotColor(source) {
16
- if (!source.canCapture) return tm.color.labelTertiary; // not detected
17
- if (!source.canRestore) return tm.color.warning; // capture only
18
- return null; // ready — nothing to flag
28
+ const DESCRIPTIONS = {
29
+ storage: "Settings, sign-in and other things saved on the device",
30
+ redux: "Data the whole app shares between screens",
31
+ zustand: "Data the whole app shares between screens",
32
+ jotai: "Small pieces of data screens share",
33
+ query: "Data the app loaded from the server",
34
+ route: "Which screen you were on"
35
+ };
36
+ function describe(source) {
37
+ return DESCRIPTIONS[source.id] ?? "App data";
38
+ }
39
+
40
+ /**
41
+ * The short state under the description, when there is one worth saying.
42
+ * "Working" is the default and needs no line.
43
+ */
44
+ function stateLine(source) {
45
+ if (!source.enabled) return {
46
+ text: "Off · left as it is",
47
+ tone: "off"
48
+ };
49
+ if (!source.canRestore) return {
50
+ text: "Can be saved but not put back",
51
+ tone: "warn"
52
+ };
53
+ return null;
19
54
  }
20
- function SourceTile({
21
- source
55
+ function SourceRow({
56
+ source,
57
+ onToggle
22
58
  }) {
23
59
  const Icon = sourceIcon(source.id);
24
- const dot = statusDotColor(source);
60
+ const description = describe(source);
61
+ const state = stateLine(source);
25
62
  return /*#__PURE__*/_jsxs(View, {
26
- style: styles.tile,
63
+ style: styles.row,
27
64
  testID: `time-machine-source-${source.id}`,
28
- accessibilityLabel: `${source.label}: ${source.canCapture ? source.canRestore ? "capture and restore" : "capture only" : "not detected"}`,
29
- children: [/*#__PURE__*/_jsxs(View, {
30
- style: [styles.disc, !source.canCapture ? styles.discOff : null],
31
- children: [/*#__PURE__*/_jsx(Icon, {
32
- size: 26,
33
- bgColor: tm.color.fill
34
- }), dot ? /*#__PURE__*/_jsx(View, {
35
- style: [styles.dot, {
36
- backgroundColor: dot
37
- }]
65
+ accessibilityLabel: `${source.label}: ${description}${state ? `. ${state.text}` : ""}`,
66
+ children: [/*#__PURE__*/_jsx(View, {
67
+ style: styles.disc,
68
+ children: /*#__PURE__*/_jsx(Icon, {
69
+ size: 22,
70
+ bgColor: tm.color.disc
71
+ })
72
+ }), /*#__PURE__*/_jsxs(View, {
73
+ style: styles.text,
74
+ children: [/*#__PURE__*/_jsx(Text, {
75
+ style: styles.label,
76
+ numberOfLines: 1,
77
+ testID: `time-machine-source-${source.id}-label`,
78
+ children: source.label
79
+ }), /*#__PURE__*/_jsx(Text, {
80
+ style: styles.description,
81
+ numberOfLines: 2,
82
+ testID: `time-machine-source-${source.id}-description`,
83
+ children: description
84
+ }), state ? /*#__PURE__*/_jsx(Text, {
85
+ style: [styles.state, styles[`state_${state.tone}`]],
86
+ numberOfLines: 1,
87
+ testID: state.tone === "off" ? `time-machine-source-${source.id}-off` : `time-machine-source-${source.id}-detail`,
88
+ children: state.text
38
89
  }) : null]
39
- }), /*#__PURE__*/_jsx(Text, {
40
- style: [styles.label, !source.canCapture ? styles.labelOff : null],
41
- numberOfLines: 1,
42
- testID: `time-machine-source-${source.id}-label`,
43
- children: source.label
90
+ }), /*#__PURE__*/_jsx(View, {
91
+ testID: `time-machine-source-${source.id}-switch`,
92
+ children: /*#__PURE__*/_jsx(NightSwitch, {
93
+ value: source.enabled
94
+ // The desktop mirror shows the device's setting and cannot flip it.
95
+ ,
96
+ disabled: !onToggle,
97
+ onValueChange: next => onToggle?.(source.id, next)
98
+ })
44
99
  })]
45
100
  });
46
101
  }
47
102
  export function SourcesStrip({
48
- sources
103
+ sources,
104
+ onToggle
49
105
  }) {
50
- return /*#__PURE__*/_jsx(View, {
51
- style: styles.card,
52
- children: /*#__PURE__*/_jsxs(ScrollView, {
53
- horizontal: true,
54
- showsHorizontalScrollIndicator: false,
55
- contentContainerStyle: styles.strip,
56
- testID: "time-machine-sources",
57
- children: [sources.map(source => /*#__PURE__*/_jsx(SourceTile, {
58
- source: source
59
- }, source.id)), sources.length === 0 ? /*#__PURE__*/_jsx(Text, {
60
- style: styles.empty,
61
- testID: "time-machine-sources-empty",
62
- children: "No state tools installed."
63
- }) : null]
64
- })
106
+ // A source the app has not wired (no Redux Provider, no Zustand stores)
107
+ // is not listed: there is nothing to capture and nothing to switch off.
108
+ const wired = sources.filter(source => source.canCapture);
109
+ return /*#__PURE__*/_jsxs(Group, {
110
+ children: [wired.map(source => /*#__PURE__*/_jsx(SourceRow, {
111
+ source: source,
112
+ onToggle: onToggle
113
+ }, source.id)), wired.length === 0 ? /*#__PURE__*/_jsx(View, {
114
+ style: styles.row,
115
+ testID: "time-machine-sources-empty",
116
+ children: /*#__PURE__*/_jsx(Text, {
117
+ style: styles.description,
118
+ children: "No state tools are set up in this app."
119
+ })
120
+ }) : null]
65
121
  });
66
122
  }
67
123
  const styles = StyleSheet.create({
68
- card: {
69
- marginHorizontal: tm.gutter,
70
- backgroundColor: tm.color.group,
71
- borderRadius: tm.radius.group,
72
- paddingVertical: 12
73
- },
74
- strip: {
124
+ row: {
75
125
  flexDirection: "row",
76
- alignItems: "flex-start",
77
- paddingHorizontal: tm.textInset,
78
- gap: 18
79
- },
80
- tile: {
81
126
  alignItems: "center",
82
- gap: 7,
83
- // Fixed width so the row reads as a grid, and wide enough for the
84
- // longest source name on ONE line: a second line would be blank for
85
- // every other tile while still setting the card's height.
86
- width: 78
127
+ gap: 12,
128
+ minHeight: tm.rowMinHeight,
129
+ paddingHorizontal: tm.textInset,
130
+ paddingVertical: 10
87
131
  },
88
132
  disc: {
89
- width: 50,
90
- height: 50,
133
+ width: 36,
134
+ height: 36,
91
135
  borderRadius: tm.radius.pill,
92
- backgroundColor: tm.color.fill,
136
+ backgroundColor: tm.color.disc,
93
137
  alignItems: "center",
94
138
  justifyContent: "center"
95
139
  },
96
- discOff: {
97
- opacity: 0.35
98
- },
99
- dot: {
100
- position: "absolute",
101
- top: 2,
102
- right: 2,
103
- width: 10,
104
- height: 10,
105
- borderRadius: 5,
106
- // Ring it in the card color so it reads as a badge, not a smudge.
107
- borderWidth: 2,
108
- borderColor: tm.color.group
140
+ text: {
141
+ flex: 1,
142
+ minWidth: 0,
143
+ gap: 2
109
144
  },
110
145
  label: {
111
- color: tm.color.labelSecondary,
146
+ color: tm.color.label,
147
+ fontSize: tm.font.row,
148
+ fontWeight: "500",
149
+ letterSpacing: tm.tracking.row
150
+ },
151
+ description: {
152
+ fontSize: tm.font.footnote,
153
+ lineHeight: tm.font.footnote * 1.3,
154
+ color: tm.color.labelSecondary
155
+ },
156
+ state: {
112
157
  fontSize: tm.font.mono,
113
- textAlign: "center"
158
+ lineHeight: tm.font.mono * 1.3,
159
+ fontWeight: "600",
160
+ marginTop: 1
114
161
  },
115
- labelOff: {
162
+ state_off: {
116
163
  color: tm.color.labelTertiary
117
164
  },
118
- empty: {
119
- color: tm.color.labelSecondary,
120
- fontSize: tm.font.footnote
165
+ state_warn: {
166
+ color: tm.color.warning
121
167
  }
122
168
  });
123
169
  //# sourceMappingURL=SourcesStrip.js.map
@@ -62,7 +62,9 @@ const styles = StyleSheet.create({
62
62
  minHeight: 46,
63
63
  paddingHorizontal: tm.textInset,
64
64
  backgroundColor: tm.color.group,
65
- borderRadius: tm.radius.group
65
+ borderRadius: tm.radius.group,
66
+ borderWidth: 1,
67
+ borderColor: tm.color.groupBorder
66
68
  },
67
69
  text: {
68
70
  flex: 1,
@@ -20,6 +20,7 @@ import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
20
20
  import { ActivityIndicator, FlatList, StyleSheet, Text, TouchableOpacity, View } from "react-native";
21
21
  import { ChevronUp, AlertTriangle, RefreshCw, useSafeAreaInsets } from "@buoy-gg/shared-ui";
22
22
  import { useToolHeader, useToolHost } from "@buoy-gg/core";
23
+ import { TimeMachineHeaderActions } from "../HeaderActions";
23
24
  import { DisclosureChevron, Group, SectionLabel } from "../../ui/primitives";
24
25
  import { TM_MONO, tm } from "../../ui/tmTheme";
25
26
  import { timeMachineStore, willRestoreRoute } from "../../store/timeMachineStore";
@@ -379,10 +380,14 @@ export function RestorePreviewScreen({
379
380
  useToolHeader(hosted ? {
380
381
  title: "Restore Preview",
381
382
  subtitle: rightMeta?.name ?? "preview",
382
- onBack
383
+ onBack,
384
+ // BAR from here arms the point being previewed.
385
+ actions: /*#__PURE__*/_jsx(TimeMachineHeaderActions, {
386
+ armId: rightMeta?.id
387
+ })
383
388
  } : null,
384
389
  // eslint-disable-next-line react-hooks/exhaustive-deps
385
- [hosted, rightMeta?.name]);
390
+ [hosted, rightMeta?.name, rightMeta?.id]);
386
391
  const {
387
392
  rows,
388
393
  blockedSources
@@ -11,8 +11,12 @@
11
11
  */
12
12
 
13
13
  import { JotaiIcon, QueryIcon, ReduxIcon, RoutesIcon, StorageIcon, TimeMachineIcon, ZustandIcon } from "@buoy-gg/floating-tools-core";
14
+ import { useSyncExternalStore } from "react";
14
15
  import { useSnapshotProviders, useTimeMachineState } from "../hooks/useTimeMachine";
15
16
  import { canNavigate } from "../store/routeRestore";
17
+ import { ROUTE_SOURCE_ID } from "../store/sourceIds";
18
+ import { getDisabledSources, subscribeSourceSettings } from "../store/sourceSettings";
19
+ export { ROUTE_SOURCE_ID };
16
20
 
17
21
  /**
18
22
  * Buoy icons paint their knock-out parts with `fill: "bg"`, defaulting to a
@@ -32,19 +36,21 @@ const ICONS = {
32
36
  export function sourceIcon(id) {
33
37
  return ICONS[id] ?? TimeMachineIcon;
34
38
  }
35
-
36
- /** Id of the route tile — a device capability, not a snapshot provider. */
37
- export const ROUTE_SOURCE_ID = "route";
38
39
  export function useSourceStatuses() {
39
40
  const {
40
41
  remoteProviders,
41
42
  remoteRoute
42
43
  } = useTimeMachineState();
43
44
  const localProviders = useSnapshotProviders();
44
- const providers = remoteProviders ?? localProviders.map(p => ({
45
+ const disabled = useSyncExternalStore(subscribeSourceSettings, getDisabledSources, getDisabledSources);
46
+ const providers = remoteProviders ? remoteProviders.map(p => ({
47
+ ...p,
48
+ enabled: p.enabled !== false
49
+ })) : localProviders.map(p => ({
45
50
  id: p.id,
46
51
  label: p.label,
47
- ...p.status()
52
+ ...p.status(),
53
+ enabled: !disabled.has(p.id)
48
54
  }));
49
55
  const routeAvailable = remoteProviders ? remoteRoute?.available === true : canNavigate();
50
56
  return [...providers, {
@@ -52,6 +58,7 @@ export function useSourceStatuses() {
52
58
  label: "Route",
53
59
  canCapture: routeAvailable,
54
60
  canRestore: routeAvailable,
61
+ enabled: remoteProviders ? remoteRoute?.enabled !== false : !disabled.has(ROUTE_SOURCE_ID),
55
62
  // No live pathname here: these screens only re-render on store changes,
56
63
  // so a route printed as status goes stale the moment you navigate with
57
64
  // the tool still open. Each restore point shows the route it holds.
@@ -61,6 +68,6 @@ export function useSourceStatuses() {
61
68
 
62
69
  /** A source is "attached" when a snapshot would actually pick it up. */
63
70
  export function isAttached(source) {
64
- return source.canCapture;
71
+ return source.canCapture && source.enabled;
65
72
  }
66
73
  //# sourceMappingURL=sources.js.map
@@ -20,15 +20,21 @@ export { timeMachineToolPreset, createTimeMachineTool } from "./preset";
20
20
  // Components
21
21
  export { TimeMachineModal, TimeMachineToolView } from "./components/TimeMachineModal";
22
22
 
23
+ // The floating restore bar (one armed point, one tap to restore)
24
+ export { openTimeMachineFloatingTool, TIME_MACHINE_FLOATING_TOOL_ID } from "./components/floating/TimeMachineFloatingTool";
25
+
23
26
  // External-sync adapter (auto-registered by @buoy-gg/core)
24
27
  export { timeMachineSyncAdapter } from "./sync/timeMachineSyncAdapter";
25
28
 
26
29
  // Programmatic snapshot control (also used by tests / power users)
27
- export { timeMachineStore, RESTORE_ORDER } from "./store/timeMachineStore";
30
+ export { timeMachineStore, RESTORE_ORDER, SAFETY_SNAPSHOT_NAME } from "./store/timeMachineStore";
28
31
 
29
32
  // Route capture / restore ("also take me back to the screen I was on")
30
33
  export { armPendingRouteRestore, captureCurrentRoute, navigateToRoute, canNavigate as canRestoreRoute } from "./store/routeRestore";
31
34
 
35
+ // Per-source on/off switches (the Sources strip); off = never captured or restored
36
+ export { isSourceEnabled, setSourceEnabled, subscribeSourceSettings } from "./store/sourceSettings";
37
+
32
38
  // Buoy Desktop remote-mirror seam
33
39
  export { setRemoteTimeMachineHandler, replaceTimeMachineFromSnapshot } from "./store/timeMachineStore";
34
40
  // A reload-mode restore that also restores the route can't navigate itself —
@@ -11,9 +11,10 @@
11
11
  * ```
12
12
  */
13
13
 
14
+ import { Platform } from "react-native";
14
15
  import { TimeMachineIcon, TIME_MACHINE_ICON_COLOR } from "@buoy-gg/shared-ui";
15
16
  import { TimeMachineBody } from "./components/TimeMachineBody";
16
- import { HeaderReloadButton } from "./components/HeaderReloadButton";
17
+ import { TimeMachineHeaderActions } from "./components/HeaderActions";
17
18
  import { jsx as _jsx } from "react/jsx-runtime";
18
19
  export const timeMachineToolPreset = {
19
20
  id: "time-machine",
@@ -22,9 +23,10 @@ export const timeMachineToolPreset = {
22
23
  slot: "both",
23
24
  icon: ({
24
25
  size,
25
- color
26
+ color,
27
+ slot
26
28
  }) => /*#__PURE__*/_jsx(TimeMachineIcon, {
27
- size: size,
29
+ size: slot === "dial" && Platform.OS !== "web" ? size * 1.5 : size,
28
30
  color: color || TIME_MACHINE_ICON_COLOR
29
31
  }),
30
32
  component: TimeMachineBody,
@@ -34,7 +36,7 @@ export const timeMachineToolPreset = {
34
36
  subtitle: "snapshot & restore app state",
35
37
  searchable: true,
36
38
  searchPlaceholder: "Filter…",
37
- headerActions: /*#__PURE__*/_jsx(HeaderReloadButton, {})
39
+ headerActions: /*#__PURE__*/_jsx(TimeMachineHeaderActions, {})
38
40
  }
39
41
  };
40
42