@buoy-gg/time-machine 7.0.22 → 7.0.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/lib/commonjs/components/TimeMachineBody.js +147 -677
  2. package/lib/commonjs/components/TimeMachineBody.js.map +1 -1
  3. package/lib/commonjs/components/TimeMachineModal.js +10 -11
  4. package/lib/commonjs/components/TimeMachineModal.js.map +1 -1
  5. package/lib/commonjs/components/confirm.js +39 -0
  6. package/lib/commonjs/components/confirm.js.map +1 -0
  7. package/lib/commonjs/components/format.js +24 -0
  8. package/lib/commonjs/components/format.js.map +1 -0
  9. package/lib/commonjs/components/list/CaptureBar.js +95 -0
  10. package/lib/commonjs/components/list/CaptureBar.js.map +1 -0
  11. package/lib/commonjs/components/list/RestoreOutcomeCard.js +162 -0
  12. package/lib/commonjs/components/list/RestoreOutcomeCard.js.map +1 -0
  13. package/lib/commonjs/components/list/SnapshotRow.js +141 -0
  14. package/lib/commonjs/components/list/SnapshotRow.js.map +1 -0
  15. package/lib/commonjs/components/list/SourcesStrip.js +126 -0
  16. package/lib/commonjs/components/list/SourcesStrip.js.map +1 -0
  17. package/lib/commonjs/components/list/UndoDeleteBar.js +81 -0
  18. package/lib/commonjs/components/list/UndoDeleteBar.js.map +1 -0
  19. package/lib/commonjs/components/list/WipeAllRow.js +46 -0
  20. package/lib/commonjs/components/list/WipeAllRow.js.map +1 -0
  21. package/lib/commonjs/components/preview/ItemDiffPanel.js +77 -0
  22. package/lib/commonjs/components/preview/ItemDiffPanel.js.map +1 -0
  23. package/lib/commonjs/components/preview/PreviewFlow.js +51 -0
  24. package/lib/commonjs/components/preview/PreviewFlow.js.map +1 -0
  25. package/lib/commonjs/components/preview/RestorePointNameRow.js +83 -0
  26. package/lib/commonjs/components/preview/RestorePointNameRow.js.map +1 -0
  27. package/lib/commonjs/components/preview/RestorePreviewScreen.js +549 -635
  28. package/lib/commonjs/components/preview/RestorePreviewScreen.js.map +1 -1
  29. package/lib/commonjs/components/preview/verdictMeta.js +8 -8
  30. package/lib/commonjs/components/preview/verdictMeta.js.map +1 -1
  31. package/lib/commonjs/components/sources.js +72 -0
  32. package/lib/commonjs/components/sources.js.map +1 -0
  33. package/lib/commonjs/store/snapshotVault.js +93 -9
  34. package/lib/commonjs/store/snapshotVault.js.map +1 -1
  35. package/lib/commonjs/store/timeMachineStore.js +125 -6
  36. package/lib/commonjs/store/timeMachineStore.js.map +1 -1
  37. package/lib/commonjs/sync/timeMachineSyncAdapter.js +41 -0
  38. package/lib/commonjs/sync/timeMachineSyncAdapter.js.map +1 -1
  39. package/lib/commonjs/ui/SwipeActionsRow.js +167 -0
  40. package/lib/commonjs/ui/SwipeActionsRow.js.map +1 -0
  41. package/lib/commonjs/ui/primitives.js +164 -0
  42. package/lib/commonjs/ui/primitives.js.map +1 -0
  43. package/lib/commonjs/ui/tmTheme.js +103 -0
  44. package/lib/commonjs/ui/tmTheme.js.map +1 -0
  45. package/lib/module/components/TimeMachineBody.js +150 -680
  46. package/lib/module/components/TimeMachineBody.js.map +1 -1
  47. package/lib/module/components/TimeMachineModal.js +11 -12
  48. package/lib/module/components/TimeMachineModal.js.map +1 -1
  49. package/lib/module/components/confirm.js +34 -0
  50. package/lib/module/components/confirm.js.map +1 -0
  51. package/lib/module/components/format.js +19 -0
  52. package/lib/module/components/format.js.map +1 -0
  53. package/lib/module/components/list/CaptureBar.js +90 -0
  54. package/lib/module/components/list/CaptureBar.js.map +1 -0
  55. package/lib/module/components/list/RestoreOutcomeCard.js +156 -0
  56. package/lib/module/components/list/RestoreOutcomeCard.js.map +1 -0
  57. package/lib/module/components/list/SnapshotRow.js +138 -0
  58. package/lib/module/components/list/SnapshotRow.js.map +1 -0
  59. package/lib/module/components/list/SourcesStrip.js +121 -0
  60. package/lib/module/components/list/SourcesStrip.js.map +1 -0
  61. package/lib/module/components/list/UndoDeleteBar.js +78 -0
  62. package/lib/module/components/list/UndoDeleteBar.js.map +1 -0
  63. package/lib/module/components/list/WipeAllRow.js +41 -0
  64. package/lib/module/components/list/WipeAllRow.js.map +1 -0
  65. package/lib/module/components/preview/ItemDiffPanel.js +72 -0
  66. package/lib/module/components/preview/ItemDiffPanel.js.map +1 -0
  67. package/lib/module/components/preview/PreviewFlow.js +46 -0
  68. package/lib/module/components/preview/PreviewFlow.js.map +1 -0
  69. package/lib/module/components/preview/RestorePointNameRow.js +78 -0
  70. package/lib/module/components/preview/RestorePointNameRow.js.map +1 -0
  71. package/lib/module/components/preview/RestorePreviewScreen.js +552 -638
  72. package/lib/module/components/preview/RestorePreviewScreen.js.map +1 -1
  73. package/lib/module/components/preview/verdictMeta.js +9 -8
  74. package/lib/module/components/preview/verdictMeta.js.map +1 -1
  75. package/lib/module/components/sources.js +66 -0
  76. package/lib/module/components/sources.js.map +1 -0
  77. package/lib/module/store/snapshotVault.js +87 -9
  78. package/lib/module/store/snapshotVault.js.map +1 -1
  79. package/lib/module/store/timeMachineStore.js +125 -7
  80. package/lib/module/store/timeMachineStore.js.map +1 -1
  81. package/lib/module/sync/timeMachineSyncAdapter.js +41 -0
  82. package/lib/module/sync/timeMachineSyncAdapter.js.map +1 -1
  83. package/lib/module/ui/SwipeActionsRow.js +162 -0
  84. package/lib/module/ui/SwipeActionsRow.js.map +1 -0
  85. package/lib/module/ui/primitives.js +156 -0
  86. package/lib/module/ui/primitives.js.map +1 -0
  87. package/lib/module/ui/tmTheme.js +99 -0
  88. package/lib/module/ui/tmTheme.js.map +1 -0
  89. package/lib/typescript/components/TimeMachineBody.d.ts.map +1 -1
  90. package/lib/typescript/components/TimeMachineModal.d.ts.map +1 -1
  91. package/lib/typescript/components/confirm.d.ts +2 -0
  92. package/lib/typescript/components/confirm.d.ts.map +1 -0
  93. package/lib/typescript/components/format.d.ts +4 -0
  94. package/lib/typescript/components/format.d.ts.map +1 -0
  95. package/lib/typescript/components/list/CaptureBar.d.ts +6 -0
  96. package/lib/typescript/components/list/CaptureBar.d.ts.map +1 -0
  97. package/lib/typescript/components/list/RestoreOutcomeCard.d.ts +11 -0
  98. package/lib/typescript/components/list/RestoreOutcomeCard.d.ts.map +1 -0
  99. package/lib/typescript/components/list/SnapshotRow.d.ts +12 -0
  100. package/lib/typescript/components/list/SnapshotRow.d.ts.map +1 -0
  101. package/lib/typescript/components/list/SourcesStrip.d.ts +5 -0
  102. package/lib/typescript/components/list/SourcesStrip.d.ts.map +1 -0
  103. package/lib/typescript/components/list/UndoDeleteBar.d.ts +8 -0
  104. package/lib/typescript/components/list/UndoDeleteBar.d.ts.map +1 -0
  105. package/lib/typescript/components/list/WipeAllRow.d.ts +5 -0
  106. package/lib/typescript/components/list/WipeAllRow.d.ts.map +1 -0
  107. package/lib/typescript/components/preview/ItemDiffPanel.d.ts +5 -0
  108. package/lib/typescript/components/preview/ItemDiffPanel.d.ts.map +1 -0
  109. package/lib/typescript/components/preview/PreviewFlow.d.ts +9 -0
  110. package/lib/typescript/components/preview/PreviewFlow.d.ts.map +1 -0
  111. package/lib/typescript/components/preview/RestorePointNameRow.d.ts +5 -0
  112. package/lib/typescript/components/preview/RestorePointNameRow.d.ts.map +1 -0
  113. package/lib/typescript/components/preview/RestorePreviewScreen.d.ts +1 -5
  114. package/lib/typescript/components/preview/RestorePreviewScreen.d.ts.map +1 -1
  115. package/lib/typescript/components/preview/verdictMeta.d.ts +5 -0
  116. package/lib/typescript/components/preview/verdictMeta.d.ts.map +1 -1
  117. package/lib/typescript/components/sources.d.ts +36 -0
  118. package/lib/typescript/components/sources.d.ts.map +1 -0
  119. package/lib/typescript/store/snapshotVault.d.ts +48 -0
  120. package/lib/typescript/store/snapshotVault.d.ts.map +1 -1
  121. package/lib/typescript/store/timeMachineStore.d.ts +35 -1
  122. package/lib/typescript/store/timeMachineStore.d.ts.map +1 -1
  123. package/lib/typescript/sync/timeMachineSyncAdapter.d.ts +17 -0
  124. package/lib/typescript/sync/timeMachineSyncAdapter.d.ts.map +1 -1
  125. package/lib/typescript/ui/SwipeActionsRow.d.ts +32 -0
  126. package/lib/typescript/ui/SwipeActionsRow.d.ts.map +1 -0
  127. package/lib/typescript/ui/primitives.d.ts +57 -0
  128. package/lib/typescript/ui/primitives.d.ts.map +1 -0
  129. package/lib/typescript/ui/tmTheme.d.ts +67 -0
  130. package/lib/typescript/ui/tmTheme.d.ts.map +1 -0
  131. package/package.json +7 -7
  132. package/src/components/TimeMachineBody.tsx +183 -817
  133. package/src/components/TimeMachineModal.tsx +10 -11
  134. package/src/components/confirm.ts +35 -0
  135. package/src/components/format.ts +17 -0
  136. package/src/components/list/CaptureBar.tsx +100 -0
  137. package/src/components/list/RestoreOutcomeCard.tsx +159 -0
  138. package/src/components/list/SnapshotRow.tsx +155 -0
  139. package/src/components/list/SourcesStrip.tsx +124 -0
  140. package/src/components/list/UndoDeleteBar.tsx +76 -0
  141. package/src/components/list/WipeAllRow.tsx +42 -0
  142. package/src/components/preview/ItemDiffPanel.tsx +77 -0
  143. package/src/components/preview/PreviewFlow.tsx +70 -0
  144. package/src/components/preview/RestorePointNameRow.tsx +80 -0
  145. package/src/components/preview/RestorePreviewScreen.tsx +535 -535
  146. package/src/components/preview/verdictMeta.ts +8 -8
  147. package/src/components/sources.ts +92 -0
  148. package/src/store/snapshotVault.ts +131 -9
  149. package/src/store/timeMachineStore.ts +136 -5
  150. package/src/sync/timeMachineSyncAdapter.ts +30 -0
  151. package/src/ui/SwipeActionsRow.tsx +199 -0
  152. package/src/ui/primitives.tsx +157 -0
  153. package/src/ui/tmTheme.ts +107 -0
  154. package/lib/commonjs/components/preview/ItemDetailScreen.js +0 -189
  155. package/lib/commonjs/components/preview/ItemDetailScreen.js.map +0 -1
  156. package/lib/module/components/preview/ItemDetailScreen.js +0 -184
  157. package/lib/module/components/preview/ItemDetailScreen.js.map +0 -1
  158. package/lib/typescript/components/preview/ItemDetailScreen.d.ts +0 -11
  159. package/lib/typescript/components/preview/ItemDetailScreen.d.ts.map +0 -1
  160. package/src/components/preview/ItemDetailScreen.tsx +0 -172
@@ -4,8 +4,9 @@
4
4
  * for Buoy Desktop.
5
5
  */
6
6
  import { StyleSheet, Text, View } from "react-native";
7
- import { JsModal, buoyColors } from "@buoy-gg/shared-ui";
7
+ import { JsModal } from "@buoy-gg/shared-ui";
8
8
  import type { ModalState } from "@buoy-gg/shared-ui";
9
+ import { tm } from "../ui/tmTheme";
9
10
  import { TimeMachineBody } from "./TimeMachineBody";
10
11
 
11
12
  export interface TimeMachineModalProps {
@@ -19,8 +20,8 @@ export interface TimeMachineModalProps {
19
20
  function Header() {
20
21
  return (
21
22
  <View style={styles.header}>
22
- <Text style={styles.headerTitle}>TIME MACHINE</Text>
23
- <Text style={styles.headerSubtitle}>snapshot & restore app state</Text>
23
+ <Text style={styles.headerTitle}>Time Machine</Text>
24
+ <Text style={styles.headerSubtitle}>Snapshot & restore app state</Text>
24
25
  </View>
25
26
  );
26
27
  }
@@ -58,19 +59,17 @@ export function TimeMachineToolView() {
58
59
 
59
60
  const styles = StyleSheet.create({
60
61
  header: {
61
- flexDirection: "row",
62
- alignItems: "baseline",
63
- gap: 8,
64
62
  paddingHorizontal: 4,
63
+ gap: 2,
65
64
  },
66
65
  headerTitle: {
67
- color: buoyColors.text,
68
- fontSize: 13,
66
+ color: tm.color.label,
67
+ fontSize: 22,
69
68
  fontWeight: "700",
70
- letterSpacing: 1,
69
+ letterSpacing: 0.3,
71
70
  },
72
71
  headerSubtitle: {
73
- color: buoyColors.textMuted,
74
- fontSize: 10,
72
+ color: tm.color.labelSecondary,
73
+ fontSize: tm.font.footnote,
75
74
  },
76
75
  });
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Confirmation dialogs for the destructive / branching Time Machine actions.
3
+ *
4
+ * RN-web's Alert renders no buttons at all, so the web path (Buoy Desktop,
5
+ * the web example) uses `window.confirm`.
6
+ *
7
+ * There is deliberately NO "restore the route too?" prompt: that question is
8
+ * answered once per restore point by the route toggle on its row, because
9
+ * asking it on every restore meant answering it thirty times a day.
10
+ */
11
+ import { Alert, Platform } from "react-native";
12
+
13
+ function webConfirm(message: string): boolean {
14
+ const confirmFn = (globalThis as { confirm?: (msg: string) => boolean })
15
+ .confirm;
16
+ // No dialog available at all (headless web): don't silently swallow the
17
+ // user's intent — treat it as confirmed, same as the previous behavior.
18
+ return confirmFn ? confirmFn(message) : true;
19
+ }
20
+
21
+ export function confirmDestructive(
22
+ title: string,
23
+ message: string,
24
+ confirmLabel: string,
25
+ onConfirm: () => void
26
+ ) {
27
+ if (Platform.OS === "web") {
28
+ if (webConfirm(`${title}\n\n${message}`)) onConfirm();
29
+ return;
30
+ }
31
+ Alert.alert(title, message, [
32
+ { text: "Cancel", style: "cancel" },
33
+ { text: confirmLabel, style: "destructive", onPress: onConfirm },
34
+ ]);
35
+ }
@@ -0,0 +1,17 @@
1
+ /** Display formatting shared by the Time Machine screens. */
2
+
3
+ export function timeAgo(ts: number): string {
4
+ const s = Math.max(0, Math.round((Date.now() - ts) / 1000));
5
+ if (s < 60) return `${s}s ago`;
6
+ const m = Math.round(s / 60);
7
+ if (m < 60) return `${m}m ago`;
8
+ const h = Math.round(m / 60);
9
+ if (h < 24) return `${h}h ago`;
10
+ return `${Math.round(h / 24)}d ago`;
11
+ }
12
+
13
+ export function formatBytes(bytes: number): string {
14
+ if (bytes < 1024) return `${bytes} B`;
15
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
16
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
17
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Capture bar — the single field-plus-pill control at the top of the list.
3
+ *
4
+ * Owns the draft name itself so keystrokes re-render this component and not
5
+ * the snapshot list underneath it. The parent only hears about a finished
6
+ * capture.
7
+ */
8
+ import { useState } from "react";
9
+ import {
10
+ StyleSheet,
11
+ Text,
12
+ TextInput,
13
+ TouchableOpacity,
14
+ View,
15
+ } from "react-native";
16
+ import { tm } from "../../ui/tmTheme";
17
+
18
+ export function CaptureBar({
19
+ disabled,
20
+ busy,
21
+ onCapture,
22
+ }: {
23
+ disabled: boolean;
24
+ busy: boolean;
25
+ onCapture: (name: string) => void;
26
+ }) {
27
+ const [name, setName] = useState("");
28
+
29
+ const capture = () => {
30
+ if (disabled) return;
31
+ onCapture(name);
32
+ setName("");
33
+ };
34
+
35
+ return (
36
+ <View style={styles.bar}>
37
+ <TextInput
38
+ style={styles.input}
39
+ value={name}
40
+ onChangeText={setName}
41
+ placeholder="Name this restore point…"
42
+ placeholderTextColor={tm.color.labelTertiary}
43
+ editable={!disabled}
44
+ onSubmitEditing={capture}
45
+ returnKeyType="done"
46
+ testID="time-machine-name-input"
47
+ />
48
+ <TouchableOpacity
49
+ style={[styles.button, disabled ? styles.buttonDisabled : null]}
50
+ onPress={capture}
51
+ disabled={disabled}
52
+ accessibilityRole="button"
53
+ accessibilityLabel="Capture restore point"
54
+ testID="time-machine-capture"
55
+ >
56
+ <Text style={styles.buttonText}>{busy ? "Capturing…" : "Capture"}</Text>
57
+ </TouchableOpacity>
58
+ </View>
59
+ );
60
+ }
61
+
62
+ const styles = StyleSheet.create({
63
+ bar: {
64
+ flexDirection: "row",
65
+ alignItems: "center",
66
+ gap: 8,
67
+ marginHorizontal: tm.gutter,
68
+ backgroundColor: tm.color.group,
69
+ borderRadius: tm.radius.group,
70
+ paddingLeft: tm.textInset,
71
+ padding: 6,
72
+ },
73
+ input: {
74
+ flex: 1,
75
+ minWidth: 0,
76
+ color: tm.color.label,
77
+ fontSize: tm.font.row,
78
+ letterSpacing: tm.tracking.row,
79
+ // Zero out the platform chrome so the field reads as part of the card.
80
+ padding: 0,
81
+ backgroundColor: "transparent",
82
+ },
83
+ button: {
84
+ height: 34,
85
+ paddingHorizontal: 16,
86
+ borderRadius: tm.radius.pill,
87
+ backgroundColor: tm.color.accent,
88
+ alignItems: "center",
89
+ justifyContent: "center",
90
+ },
91
+ buttonDisabled: {
92
+ opacity: 0.4,
93
+ },
94
+ buttonText: {
95
+ color: tm.color.onAccent,
96
+ fontSize: tm.font.control,
97
+ fontWeight: "600",
98
+ letterSpacing: tm.tracking.control,
99
+ },
100
+ });
@@ -0,0 +1,159 @@
1
+ /**
2
+ * What the last restore did, shown ONLY when something went wrong.
3
+ *
4
+ * A clean restore says so with a toast and then gets out of the way — the
5
+ * restored app is the proof, and a wall of green ticks on top of the list
6
+ * every single time was noise. A restore that skipped or failed a source is
7
+ * the opposite: nothing else on screen would ever tell you, so it stays until
8
+ * dismissed.
9
+ */
10
+ import { StyleSheet, Text, TouchableOpacity, View } from "react-native";
11
+ import { X } from "@buoy-gg/shared-ui";
12
+ import { tm } from "../../ui/tmTheme";
13
+ import { timeMachineStore } from "../../store/timeMachineStore";
14
+ import type { RestoreOutcome } from "../../store/timeMachineStore";
15
+
16
+ const HIT = { top: 10, bottom: 10, left: 10, right: 10 };
17
+
18
+ /**
19
+ * Did this restore do anything the app itself won't show you? Warnings alone
20
+ * don't count — "1 derived atom recomputes" is true of every restore and
21
+ * would put the card back on screen permanently.
22
+ */
23
+ export function restoreNeedsAttention(outcome: RestoreOutcome): boolean {
24
+ if (Object.values(outcome.results).some((r) => !r.ok || r.skipped.length > 0))
25
+ return true;
26
+ const route = outcome.route;
27
+ return route?.status === "failed" || route?.status === "unavailable";
28
+ }
29
+
30
+ /** ✓ / ✕ / ↻ line — the glyph carries the meaning, the color reinforces it. */
31
+ function ResultLine({
32
+ glyph,
33
+ text,
34
+ ok,
35
+ }: {
36
+ glyph: string;
37
+ text: string;
38
+ ok: boolean;
39
+ }) {
40
+ return (
41
+ <Text
42
+ style={[styles.line, { color: ok ? tm.color.accent : tm.color.warning }]}
43
+ numberOfLines={2}
44
+ >
45
+ <Text style={styles.glyph}>{glyph} </Text>
46
+ {text}
47
+ </Text>
48
+ );
49
+ }
50
+
51
+ function RouteResult({ route }: { route: NonNullable<RestoreOutcome["route"]> }) {
52
+ if (route.status === "navigated")
53
+ return <ResultLine glyph="✓" ok text={`route — ${route.href}`} />;
54
+ if (route.status === "deferred")
55
+ return (
56
+ <ResultLine glyph="↻" ok text={`route — ${route.href} after reload`} />
57
+ );
58
+ return (
59
+ <ResultLine
60
+ glyph="✕"
61
+ ok={false}
62
+ text={`route — ${route.error ?? route.status}`}
63
+ />
64
+ );
65
+ }
66
+
67
+ export function RestoreOutcomeCard({ outcome }: { outcome: RestoreOutcome }) {
68
+ const entries = Object.entries(outcome.results);
69
+ const reloading = outcome.mode === "reload" || outcome.willReload;
70
+
71
+ return (
72
+ <View style={styles.card}>
73
+ <View style={styles.headerRow}>
74
+ <Text style={styles.title} numberOfLines={1}>
75
+ Restored “{outcome.snapshotName}”{reloading ? " · reloading" : ""}
76
+ </Text>
77
+ <TouchableOpacity
78
+ onPress={() => timeMachineStore.clearLastOutcome()}
79
+ hitSlop={HIT}
80
+ accessibilityRole="button"
81
+ accessibilityLabel="Dismiss restore result"
82
+ >
83
+ <X size={13} color={tm.color.labelSecondary} />
84
+ </TouchableOpacity>
85
+ </View>
86
+
87
+ {entries.length === 0 && !outcome.route ? (
88
+ <Text style={styles.empty}>No sources restored.</Text>
89
+ ) : null}
90
+
91
+ {outcome.route ? <RouteResult route={outcome.route} /> : null}
92
+
93
+ {entries.map(([sourceId, result]) => (
94
+ <View key={sourceId}>
95
+ <ResultLine
96
+ glyph={result.ok ? "✓" : "✕"}
97
+ ok={result.ok}
98
+ text={`${sourceId} — applied ${result.applied}${
99
+ result.skipped.length > 0
100
+ ? `, skipped ${result.skipped.length}`
101
+ : ""
102
+ }`}
103
+ />
104
+ {result.skipped.slice(0, 3).map((skip) => (
105
+ <Text key={skip.id} style={styles.detail} numberOfLines={2}>
106
+ {skip.id}: {skip.reason}
107
+ </Text>
108
+ ))}
109
+ {result.warnings.slice(0, 3).map((warning) => (
110
+ <Text key={warning} style={styles.detail} numberOfLines={2}>
111
+ ⚠ {warning}
112
+ </Text>
113
+ ))}
114
+ </View>
115
+ ))}
116
+ </View>
117
+ );
118
+ }
119
+
120
+ const styles = StyleSheet.create({
121
+ card: {
122
+ marginHorizontal: tm.gutter,
123
+ marginTop: 12,
124
+ backgroundColor: tm.color.group,
125
+ borderRadius: tm.radius.group,
126
+ padding: 14,
127
+ gap: 4,
128
+ },
129
+ headerRow: {
130
+ flexDirection: "row",
131
+ alignItems: "center",
132
+ gap: 10,
133
+ marginBottom: 2,
134
+ },
135
+ title: {
136
+ flex: 1,
137
+ color: tm.color.label,
138
+ fontSize: 15,
139
+ fontWeight: "600",
140
+ letterSpacing: tm.tracking.control,
141
+ },
142
+ line: {
143
+ fontSize: tm.font.footnote,
144
+ lineHeight: tm.font.footnote * 1.4,
145
+ },
146
+ glyph: {
147
+ fontWeight: "700",
148
+ },
149
+ detail: {
150
+ color: tm.color.labelTertiary,
151
+ fontSize: tm.font.mono,
152
+ lineHeight: tm.font.mono * 1.4,
153
+ paddingLeft: 14,
154
+ },
155
+ empty: {
156
+ color: tm.color.labelSecondary,
157
+ fontSize: tm.font.footnote,
158
+ },
159
+ });
@@ -0,0 +1,155 @@
1
+ /**
2
+ * One restore point in the list: what it is, when it was taken, how big it
3
+ * is, where it was captured — and the single action worth having at arm's
4
+ * reach. Tapping the row opens the restore preview; the pill restores now.
5
+ *
6
+ * The route is shown as context — where this state was true. Whether a
7
+ * restore also navigates there is decided in the restore preview, beside the
8
+ * other changes it is about to make.
9
+ *
10
+ * Duplicate and delete live behind a swipe. A destructive target beside
11
+ * "Restore" is a mis-tap that costs a snapshot, and the two-step gesture is
12
+ * deliberate enough — recovery is the undo the list offers after, not a
13
+ * dialog before. Renaming is NOT here: it belongs with the restore point's
14
+ * other settings, on the preview screen.
15
+ */
16
+ import { StyleSheet, Text, TouchableOpacity, View } from "react-native";
17
+ import { PillLabel } from "../../ui/primitives";
18
+ import { SwipeActionsRow } from "../../ui/SwipeActionsRow";
19
+ import { TM_MONO, tm } from "../../ui/tmTheme";
20
+ import type { SnapshotMeta } from "../../store/snapshotVault";
21
+ import { formatBytes, timeAgo } from "../format";
22
+
23
+ /** "15h ago · 9.5 KB · 5 sources · skips 2" */
24
+ function describe(meta: SnapshotMeta): string {
25
+ if (meta.baseline) return "Wipes to fresh install";
26
+ const parts = [
27
+ timeAgo(meta.createdAt),
28
+ formatBytes(meta.sizeBytes),
29
+ `${Object.keys(meta.sources).length} sources`,
30
+ ];
31
+ // A saved selection is worth stating on the row — this restore point no
32
+ // longer does the obvious thing, and one tap on RESTORE honors it.
33
+ if (meta.excludedKeys?.length) parts.push(`skips ${meta.excludedKeys.length}`);
34
+ else if (meta.scope?.length) parts.push(`scoped (${meta.scope.length})`);
35
+ return parts.join(" · ");
36
+ }
37
+
38
+ export function SnapshotRow({
39
+ meta,
40
+ busy,
41
+ onOpen,
42
+ onRestore,
43
+ onDelete,
44
+ onDuplicate,
45
+ }: {
46
+ meta: SnapshotMeta;
47
+ busy: boolean;
48
+ /** Zero-arg: a data-taking `onPress` prop is invoked with the event by
49
+ * Buoy MCP's tap_element, which crashes the row. */
50
+ onOpen: () => void;
51
+ onRestore: () => void;
52
+ onDelete: () => void;
53
+ onDuplicate: () => void;
54
+ }) {
55
+ const testID = `time-machine-snap-${meta.id}`;
56
+
57
+ return (
58
+ <SwipeActionsRow
59
+ testID={testID}
60
+ actions={[
61
+ // "Dup" over "Copy": on a row full of text, "Copy" reads as
62
+ // copy-to-clipboard.
63
+ { label: "Dup", onPress: onDuplicate },
64
+ {
65
+ label: "Delete",
66
+ onPress: onDelete,
67
+ background: tm.color.danger,
68
+ color: tm.color.onDanger,
69
+ },
70
+ ]}
71
+ >
72
+ <TouchableOpacity
73
+ style={styles.row}
74
+ onPress={onOpen}
75
+ activeOpacity={0.7}
76
+ accessibilityRole="button"
77
+ accessibilityLabel={`${meta.name}. ${describe(meta)}. Open restore preview`}
78
+ testID={testID}
79
+ >
80
+ <View style={styles.text}>
81
+ <Text style={styles.name} numberOfLines={1}>
82
+ {meta.name}
83
+ </Text>
84
+ <Text style={styles.meta} numberOfLines={1}>
85
+ {describe(meta)}
86
+ </Text>
87
+ {meta.lastRestoredAt ? (
88
+ <Text style={styles.restored} numberOfLines={1}>
89
+ Restored {timeAgo(meta.lastRestoredAt)}
90
+ </Text>
91
+ ) : null}
92
+ {meta.route ? (
93
+ <Text style={styles.route} numberOfLines={1}>
94
+ {meta.route.href}
95
+ </Text>
96
+ ) : null}
97
+ </View>
98
+ <TouchableOpacity
99
+ onPress={onRestore}
100
+ disabled={busy}
101
+ style={busy ? styles.actionDisabled : null}
102
+ accessibilityRole="button"
103
+ accessibilityLabel={meta.baseline ? "Wipe and reload" : "Restore"}
104
+ testID={`${testID}-restore`}
105
+ >
106
+ <PillLabel
107
+ label={meta.baseline ? "Wipe" : "Restore"}
108
+ color={meta.baseline ? tm.color.danger : tm.color.accent}
109
+ background={meta.baseline ? tm.color.dangerSoft : tm.color.accentSoft}
110
+ />
111
+ </TouchableOpacity>
112
+ </TouchableOpacity>
113
+ </SwipeActionsRow>
114
+ );
115
+ }
116
+
117
+ const styles = StyleSheet.create({
118
+ row: {
119
+ flexDirection: "row",
120
+ alignItems: "center",
121
+ gap: 10,
122
+ paddingHorizontal: tm.textInset,
123
+ paddingVertical: 11,
124
+ },
125
+ text: {
126
+ flex: 1,
127
+ minWidth: 0,
128
+ },
129
+ name: {
130
+ color: tm.color.label,
131
+ fontSize: tm.font.row,
132
+ fontWeight: "600",
133
+ letterSpacing: tm.tracking.row,
134
+ },
135
+ meta: {
136
+ color: tm.color.labelSecondary,
137
+ fontSize: tm.font.footnote,
138
+ marginTop: 2,
139
+ fontVariant: ["tabular-nums"],
140
+ },
141
+ restored: {
142
+ color: tm.color.accent,
143
+ fontSize: tm.font.footnote,
144
+ marginTop: 2,
145
+ },
146
+ route: {
147
+ color: tm.color.labelTertiary,
148
+ fontFamily: TM_MONO,
149
+ fontSize: tm.font.mono,
150
+ marginTop: 4,
151
+ },
152
+ actionDisabled: {
153
+ opacity: 0.4,
154
+ },
155
+ });
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Sources — one horizontally scrollable line of what a capture will pick up.
3
+ *
4
+ * Each source is its own tile (icon over name) rather than a count behind a
5
+ * disclosure: "is Redux wired?" is a glance question, and the answer should
6
+ * not cost a screen. A source that can't fully participate dims and carries a
7
+ * status dot, with the reason spelled out underneath the strip.
8
+ */
9
+ import { ScrollView, StyleSheet, Text, View } from "react-native";
10
+ import { tm } from "../../ui/tmTheme";
11
+ import { sourceIcon, type SourceStatus } from "../sources";
12
+
13
+ function statusDotColor(source: SourceStatus): string | null {
14
+ if (!source.canCapture) return tm.color.labelTertiary; // not detected
15
+ if (!source.canRestore) return tm.color.warning; // capture only
16
+ return null; // ready — nothing to flag
17
+ }
18
+
19
+ function SourceTile({ source }: { source: SourceStatus }) {
20
+ const Icon = sourceIcon(source.id);
21
+ const dot = statusDotColor(source);
22
+ return (
23
+ <View
24
+ style={styles.tile}
25
+ testID={`time-machine-source-${source.id}`}
26
+ accessibilityLabel={`${source.label}: ${
27
+ source.canCapture
28
+ ? source.canRestore
29
+ ? "capture and restore"
30
+ : "capture only"
31
+ : "not detected"
32
+ }`}
33
+ >
34
+ <View style={[styles.disc, !source.canCapture ? styles.discOff : null]}>
35
+ {/* Disc color in, so the icon's cut-outs blend with it. */}
36
+ <Icon size={26} bgColor={tm.color.fill} />
37
+ {dot ? <View style={[styles.dot, { backgroundColor: dot }]} /> : null}
38
+ </View>
39
+ <Text
40
+ style={[styles.label, !source.canCapture ? styles.labelOff : null]}
41
+ numberOfLines={1}
42
+ >
43
+ {source.label}
44
+ </Text>
45
+ </View>
46
+ );
47
+ }
48
+
49
+ export function SourcesStrip({ sources }: { sources: SourceStatus[] }) {
50
+ return (
51
+ <View style={styles.card}>
52
+ <ScrollView
53
+ horizontal
54
+ showsHorizontalScrollIndicator={false}
55
+ contentContainerStyle={styles.strip}
56
+ testID="time-machine-sources"
57
+ >
58
+ {sources.map((source) => (
59
+ <SourceTile key={source.id} source={source} />
60
+ ))}
61
+ {sources.length === 0 ? (
62
+ <Text style={styles.empty}>No state tools installed.</Text>
63
+ ) : null}
64
+ </ScrollView>
65
+ </View>
66
+ );
67
+ }
68
+
69
+ const styles = StyleSheet.create({
70
+ card: {
71
+ marginHorizontal: tm.gutter,
72
+ backgroundColor: tm.color.group,
73
+ borderRadius: tm.radius.group,
74
+ paddingVertical: 12,
75
+ },
76
+ strip: {
77
+ flexDirection: "row",
78
+ alignItems: "flex-start",
79
+ paddingHorizontal: tm.textInset,
80
+ gap: 18,
81
+ },
82
+ tile: {
83
+ alignItems: "center",
84
+ gap: 7,
85
+ // Fixed width so the row reads as a grid, and wide enough for the
86
+ // longest source name on ONE line: a second line would be blank for
87
+ // every other tile while still setting the card's height.
88
+ width: 78,
89
+ },
90
+ disc: {
91
+ width: 50,
92
+ height: 50,
93
+ borderRadius: tm.radius.pill,
94
+ backgroundColor: tm.color.fill,
95
+ alignItems: "center",
96
+ justifyContent: "center",
97
+ },
98
+ discOff: {
99
+ opacity: 0.35,
100
+ },
101
+ dot: {
102
+ position: "absolute",
103
+ top: 2,
104
+ right: 2,
105
+ width: 10,
106
+ height: 10,
107
+ borderRadius: 5,
108
+ // Ring it in the card color so it reads as a badge, not a smudge.
109
+ borderWidth: 2,
110
+ borderColor: tm.color.group,
111
+ },
112
+ label: {
113
+ color: tm.color.labelSecondary,
114
+ fontSize: tm.font.mono,
115
+ textAlign: "center",
116
+ },
117
+ labelOff: {
118
+ color: tm.color.labelTertiary,
119
+ },
120
+ empty: {
121
+ color: tm.color.labelSecondary,
122
+ fontSize: tm.font.footnote,
123
+ },
124
+ });