@buoy-gg/time-machine 7.0.36 → 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 +10 -7
  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,5 @@
1
+ "use strict";
2
+
3
+ /** Id of the route "source": a device capability, not a snapshot provider. */
4
+ export const ROUTE_SOURCE_ID = "route";
5
+ //# sourceMappingURL=sourceIds.js.map
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Which sources Time Machine is allowed to touch.
5
+ *
6
+ * A source switched off here is left out of every capture and every restore,
7
+ * from the tool, the bar, the desktop and MCP alike: "off" has to mean off
8
+ * everywhere, or the one path that forgets it is the one that breaks the app.
9
+ * The case this exists for is a store whose full replace the app cannot
10
+ * survive (a Zustand store holding live handles, say): leave it off and the
11
+ * rest of the state still round-trips.
12
+ *
13
+ * Persisted as the set of ids that are OFF, so a source that registers for
14
+ * the first time tomorrow starts on, and the default is "everything".
15
+ */
16
+ import { definePersistedSetting, devToolsStorageKeys } from "@buoy-gg/shared-ui";
17
+ const setting = definePersistedSetting(devToolsStorageKeys.timeMachine.disabledSources(), [], {
18
+ sanitize: value => Array.isArray(value) ? value.filter(v => typeof v === "string") : []
19
+ });
20
+ let disabled = new Set();
21
+ let loaded = false;
22
+ let loadPromise = null;
23
+ const listeners = new Set();
24
+ const notify = () => listeners.forEach(listener => listener());
25
+ export function ensureSourceSettingsLoaded() {
26
+ if (loaded) return Promise.resolve();
27
+ if (!loadPromise) {
28
+ loadPromise = setting.load().then(ids => {
29
+ // A toggle made while the read was in flight wins over the disk copy.
30
+ if (disabled.size === 0) {
31
+ disabled = new Set(ids);
32
+ notify();
33
+ }
34
+ }).finally(() => {
35
+ loaded = true;
36
+ });
37
+ }
38
+ return loadPromise;
39
+ }
40
+ export function subscribeSourceSettings(listener) {
41
+ listeners.add(listener);
42
+ return () => {
43
+ listeners.delete(listener);
44
+ };
45
+ }
46
+
47
+ /** Stable across renders until something changes, for useSyncExternalStore. */
48
+ export function getDisabledSources() {
49
+ return disabled;
50
+ }
51
+ export function isSourceEnabled(id) {
52
+ return !disabled.has(id);
53
+ }
54
+ export function setSourceEnabled(id, enabled) {
55
+ if (enabled === !disabled.has(id)) return;
56
+ const next = new Set(disabled);
57
+ if (enabled) next.delete(id);else next.add(id);
58
+ disabled = next;
59
+ notify();
60
+ void setting.save([...next]);
61
+ }
62
+
63
+ /** Test seam: forget everything without touching storage. */
64
+ export function resetSourceSettingsForTests() {
65
+ disabled = new Set();
66
+ loaded = false;
67
+ loadPromise = null;
68
+ }
69
+ //# sourceMappingURL=sourceSettings.js.map
@@ -20,6 +20,8 @@ import { getStateSnapshotProvider, getStateSnapshotProviders, scheduleReloadApp,
20
20
  import { objectDiff } from "@buoy-gg/shared-ui/dataViewer";
21
21
  import { deleteSnapshot, duplicateSnapshot, loadIndex, loadSnapshot, markSnapshotRestored, readSnapshot, renameSnapshot, restoreDeletedSnapshot, saveSnapshot, setSnapshotRestoreRoute, setSnapshotScope, setSnapshotExclusions } from "./snapshotVault";
22
22
  import { captureCurrentRoute, navigateToRoute, savePendingRoute } from "./routeRestore";
23
+ import { ensureSourceSettingsLoaded, isSourceEnabled } from "./sourceSettings";
24
+ import { ROUTE_SOURCE_ID } from "./sourceIds";
23
25
  export const RESTORE_ORDER = ["storage", "redux", "zustand", "jotai", "query"];
24
26
  /**
25
27
  * What to call a capture nobody named. The screen you were on plus the time
@@ -30,7 +32,22 @@ export const RESTORE_ORDER = ["storage", "redux", "zustand", "jotai", "query"];
30
32
  function defaultSnapshotName(route) {
31
33
  const now = new Date();
32
34
  const clock = `${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}`;
33
- return route ? `${route.pathname} · ${clock}` : `Snapshot · ${clock}`;
35
+ const screen = route ? route.pathname === "/" ? "Home" : route.pathname : null;
36
+ return screen ? `${screen} · ${clock}` : `Snapshot · ${clock}`;
37
+ }
38
+
39
+ /**
40
+ * Two captures in the same minute on the same screen would get the same
41
+ * default name and be told apart by nothing. Number the repeats.
42
+ */
43
+ function dedupeName(name, existing) {
44
+ const taken = new Set(existing.map(m => m.name));
45
+ if (!taken.has(name)) return name;
46
+ for (let n = 2; n < 1000; n++) {
47
+ const candidate = `${name} (${n})`;
48
+ if (!taken.has(candidate)) return candidate;
49
+ }
50
+ return name;
34
51
  }
35
52
 
36
53
  /**
@@ -44,6 +61,8 @@ export function willRestoreRoute(meta) {
44
61
 
45
62
  /** Extra knobs a restore accepts beyond mode + exclusions. */
46
63
 
64
+ export const SAFETY_SNAPSHOT_NAME = "Before last restore";
65
+
47
66
  /** Provider capability row as mirrored to a remote dashboard. */
48
67
 
49
68
  /** One side of a preview comparison. */
@@ -135,7 +154,7 @@ class TimeMachineStore {
135
154
  }
136
155
 
137
156
  /** Capture all capturable sources (or a subset) into a named snapshot. */
138
- async capture(name, sourceIds) {
157
+ async capture(name, sourceIds, options) {
139
158
  if (remoteHandler) {
140
159
  const result = await remoteHandler("capture", {
141
160
  name,
@@ -144,11 +163,12 @@ class TimeMachineStore {
144
163
  return result.snapshot;
145
164
  }
146
165
  await this.ensureLoaded();
166
+ await ensureSourceSettingsLoaded();
147
167
  this.setState({
148
168
  busy: "capture"
149
169
  });
150
170
  try {
151
- const providers = getStateSnapshotProviders().filter(p => p.status().canCapture && (!sourceIds || sourceIds.includes(p.id)));
171
+ const providers = getStateSnapshotProviders().filter(p => p.status().canCapture && isSourceEnabled(p.id) && (!sourceIds || sourceIds.includes(p.id)));
152
172
  const data = {};
153
173
  const sources = {};
154
174
  for (const provider of providers) {
@@ -165,21 +185,31 @@ class TimeMachineStore {
165
185
  }
166
186
  }
167
187
  // Where the app was when this state was true — restoring can return here.
168
- const route = captureCurrentRoute();
188
+ const route = isSourceEnabled(ROUTE_SOURCE_ID) ? captureCurrentRoute() : null;
189
+ // The safety slot is rolling: the previous one goes before this one lands.
190
+ let current = this.state.snapshots;
191
+ if (options?.safety) {
192
+ for (const stale of current.filter(m => m.safety)) {
193
+ current = await deleteSnapshot(stale.id, current);
194
+ }
195
+ }
169
196
  const snapshot = {
170
197
  meta: {
171
198
  id: nextId(),
172
- name: name.trim() || defaultSnapshotName(route),
199
+ name: options?.safety ? SAFETY_SNAPSHOT_NAME : dedupeName(name.trim() || defaultSnapshotName(route), current),
173
200
  createdAt: Date.now(),
174
201
  sizeBytes: 0,
175
202
  ...(route ? {
176
203
  route
177
204
  } : {}),
205
+ ...(options?.safety ? {
206
+ safety: true
207
+ } : {}),
178
208
  sources
179
209
  },
180
210
  data
181
211
  };
182
- const snapshots = await saveSnapshot(snapshot, this.state.snapshots);
212
+ const snapshots = await saveSnapshot(snapshot, current);
183
213
  this.setState({
184
214
  snapshots
185
215
  });
@@ -202,6 +232,7 @@ class TimeMachineStore {
202
232
  return result.snapshot;
203
233
  }
204
234
  await this.ensureLoaded();
235
+ await ensureSourceSettingsLoaded();
205
236
  const snapshot = {
206
237
  meta: {
207
238
  id: nextId(),
@@ -239,6 +270,16 @@ class TimeMachineStore {
239
270
  return outcome;
240
271
  }
241
272
  await this.ensureLoaded();
273
+ await ensureSourceSettingsLoaded();
274
+ // The way back from a mis-tap: capture what is about to be replaced.
275
+ // Restoring the safety slot itself never rolls it, or Undo would eat
276
+ // its own target.
277
+ let safetySnapshotId;
278
+ if (options?.safetyCapture && !this.state.snapshots.find(m => m.id === id)?.safety) {
279
+ safetySnapshotId = (await this.capture(SAFETY_SNAPSHOT_NAME, undefined, {
280
+ safety: true
281
+ })).id;
282
+ }
242
283
  this.setState({
243
284
  busy: "restore"
244
285
  });
@@ -251,7 +292,10 @@ class TimeMachineStore {
251
292
  mode,
252
293
  at: Date.now(),
253
294
  willReload: false,
254
- results: {}
295
+ results: {},
296
+ ...(safetySnapshotId ? {
297
+ safetySnapshotId
298
+ } : {})
255
299
  };
256
300
  if (stored.meta.baseline) {
257
301
  // Fresh install: wipe every clearable source, then reload so all
@@ -299,10 +343,10 @@ class TimeMachineStore {
299
343
  // No prompt any more: the point carries its own answer, and having a
300
344
  // route means wanting it back unless you said otherwise. An explicit
301
345
  // option still wins, so MCP and the desktop can force either way.
302
- const wantsRoute = options?.restoreRoute ?? willRestoreRoute(stored.meta);
346
+ const wantsRoute = isSourceEnabled(ROUTE_SOURCE_ID) && (options?.restoreRoute ?? willRestoreRoute(stored.meta));
303
347
  if (mode === "reload") {
304
348
  // Storage only; everything in-memory rebuilds after the reload.
305
- if (stored.data.storage && (!includeBySource || includeBySource.has("storage"))) {
349
+ if (stored.data.storage && isSourceEnabled("storage") && (!includeBySource || includeBySource.has("storage"))) {
306
350
  outcome.results.storage = await this.restoreSource("storage", stored.data.storage, excludeBySource.get("storage"), includeBySource?.get("storage"));
307
351
  }
308
352
  // Hand the route to the NEXT boot — this realm is about to die, so the
@@ -334,6 +378,17 @@ class TimeMachineStore {
334
378
  for (const sid of sourceIds) {
335
379
  // Sources entirely outside the scope are untouched and unreported.
336
380
  if (includeBySource && !includeBySource.has(sid)) continue;
381
+ // A source switched off in Sources is left exactly as it is. Reported
382
+ // as a warning, not a failure: this is what you asked for.
383
+ if (!isSourceEnabled(sid)) {
384
+ outcome.results[sid] = {
385
+ ok: true,
386
+ applied: 0,
387
+ skipped: [],
388
+ warnings: ["Switched off in Sources; left as it is"]
389
+ };
390
+ continue;
391
+ }
337
392
  outcome.results[sid] = await this.restoreSource(sid, stored.data[sid], excludeBySource.get(sid), includeBySource?.get(sid));
338
393
  }
339
394
  // Navigate LAST — the screen we land on should read the restored state.
@@ -400,6 +455,7 @@ class TimeMachineStore {
400
455
  return;
401
456
  }
402
457
  await this.ensureLoaded();
458
+ await ensureSourceSettingsLoaded();
403
459
  const stored = await readSnapshot(id);
404
460
  const snapshots = await deleteSnapshot(id, this.state.snapshots);
405
461
  this.setState({
@@ -439,6 +495,7 @@ class TimeMachineStore {
439
495
  return;
440
496
  }
441
497
  await this.ensureLoaded();
498
+ await ensureSourceSettingsLoaded();
442
499
  const snapshots = await setSnapshotRestoreRoute(id, restoreRoute, this.state.snapshots);
443
500
  this.setState({
444
501
  snapshots
@@ -454,6 +511,7 @@ class TimeMachineStore {
454
511
  return result?.snapshot ?? null;
455
512
  }
456
513
  await this.ensureLoaded();
514
+ await ensureSourceSettingsLoaded();
457
515
  const source = this.state.snapshots.find(s => s.id === id);
458
516
  if (!source) return null;
459
517
  const result = await duplicateSnapshot(id, `${source.name} copy`, this.state.snapshots);
@@ -472,6 +530,7 @@ class TimeMachineStore {
472
530
  return;
473
531
  }
474
532
  await this.ensureLoaded();
533
+ await ensureSourceSettingsLoaded();
475
534
  const snapshots = await renameSnapshot(id, name, this.state.snapshots);
476
535
  this.setState({
477
536
  snapshots
@@ -492,6 +551,7 @@ class TimeMachineStore {
492
551
  return;
493
552
  }
494
553
  await this.ensureLoaded();
554
+ await ensureSourceSettingsLoaded();
495
555
  const snapshots = await setSnapshotScope(id, scope, this.state.snapshots);
496
556
  this.setState({
497
557
  snapshots
@@ -512,6 +572,7 @@ class TimeMachineStore {
512
572
  return;
513
573
  }
514
574
  await this.ensureLoaded();
575
+ await ensureSourceSettingsLoaded();
515
576
  const snapshots = await setSnapshotExclusions(id, excludedKeys, this.state.snapshots);
516
577
  this.setState({
517
578
  snapshots
@@ -629,6 +690,7 @@ class TimeMachineStore {
629
690
  return;
630
691
  }
631
692
  await this.ensureLoaded();
693
+ await ensureSourceSettingsLoaded();
632
694
  const runId = ++this.previewRunId;
633
695
  this.setState({
634
696
  preview: {
@@ -660,6 +722,9 @@ class TimeMachineStore {
660
722
  const sourceIds = isBaseline ? getStateSnapshotProviders().filter(p => p.clear).map(p => p.id) : [...RESTORE_ORDER.filter(sid => sid in rightData), ...Object.keys(rightData).filter(sid => !RESTORE_ORDER.includes(sid))];
661
723
  const sources = [];
662
724
  for (const sid of sourceIds) {
725
+ // Off in Sources: a restore will not touch it, so there is nothing
726
+ // to preview.
727
+ if (!isSourceEnabled(sid)) continue;
663
728
  // Yield a frame between sources — verdict passes are bounded but a
664
729
  // five-source build shouldn't run as one synchronous block.
665
730
  await new Promise(r => setTimeout(r, 0));
@@ -18,6 +18,8 @@ import { jsonLikelyLargerThan } from "@buoy-gg/shared-ui/dataViewer";
18
18
  import { timeMachineStore } from "../store/timeMachineStore";
19
19
  import { loadSnapshot } from "../store/snapshotVault";
20
20
  import { canNavigate, currentRoute } from "../store/routeRestore";
21
+ import { isSourceEnabled } from "../store/sourceSettings";
22
+ import { ROUTE_SOURCE_ID } from "../store/sourceIds";
21
23
  function providerStatuses() {
22
24
  return getStateSnapshotProviders().map(p => {
23
25
  const s = p.status();
@@ -26,7 +28,8 @@ function providerStatuses() {
26
28
  label: p.label,
27
29
  canCapture: s.canCapture,
28
30
  canRestore: s.canRestore,
29
- detail: s.detail
31
+ detail: s.detail,
32
+ enabled: isSourceEnabled(p.id)
30
33
  };
31
34
  });
32
35
  }
@@ -46,6 +49,7 @@ function metadata() {
46
49
  // a dashboard has no router of its own to probe, so it's mirrored here.
47
50
  route: {
48
51
  available: canNavigate(),
52
+ enabled: isSourceEnabled(ROUTE_SOURCE_ID),
49
53
  ...(current ? {
50
54
  current: current.href
51
55
  } : {})
@@ -91,11 +91,20 @@ export function SwipeActionsRow({
91
91
  useEffect(() => () => {
92
92
  if (closeOpenRow === close) closeOpenRow = null;
93
93
  }, [close]);
94
+
95
+ // The row is glass now, so the actions can't hide under it: they fade in
96
+ // with the first points of the drag instead of sitting there at rest.
97
+ const actionOpacity = translateX.interpolate({
98
+ inputRange: [-24, 0],
99
+ outputRange: [1, 0],
100
+ extrapolate: "clamp"
101
+ });
94
102
  return /*#__PURE__*/_jsxs(View, {
95
103
  style: styles.clip,
96
- children: [/*#__PURE__*/_jsx(View, {
104
+ children: [/*#__PURE__*/_jsx(Animated.View, {
97
105
  style: [styles.actionLayer, {
98
- width: openWidth
106
+ width: openWidth,
107
+ opacity: actionOpacity
99
108
  }],
100
109
  children: actions.map(action => /*#__PURE__*/_jsx(TouchableOpacity, {
101
110
  style: [styles.action, action.background ? {
@@ -154,7 +163,9 @@ const styles = StyleSheet.create({
154
163
  fontWeight: "600",
155
164
  letterSpacing: tm.tracking.control
156
165
  },
157
- // Opaque so the red action never bleeds through the row it sits under.
166
+ // Glass, like the card it sits in. The actions underneath are faded out
167
+ // at rest (see actionOpacity), which is what keeps them from bleeding
168
+ // through a translucent row.
158
169
  content: {
159
170
  backgroundColor: tm.color.group
160
171
  }
@@ -139,6 +139,8 @@ const styles = StyleSheet.create({
139
139
  marginHorizontal: tm.gutter,
140
140
  backgroundColor: tm.color.group,
141
141
  borderRadius: tm.radius.group,
142
+ borderWidth: 1,
143
+ borderColor: tm.color.groupBorder,
142
144
  overflow: "hidden"
143
145
  },
144
146
  separator: {
@@ -4,7 +4,7 @@
4
4
  * Time Machine design tokens — the "modern" surface language.
5
5
  *
6
6
  * A grouped-inset list system (iOS Settings shape): one dark sheet, rounded
7
- * grouped cards holding hairline-separated rows, uppercase section labels
7
+ * glass cards holding hairline-separated rows, uppercase section labels
8
8
  * outside the cards, and explanatory footnotes under them.
9
9
  *
10
10
  * COLOR COMES FROM the shared `night` palette (ui/night/nightTheme.ts) — not
@@ -35,15 +35,28 @@ export const tm = {
35
35
  * without the list reading through the CTA.
36
36
  */
37
37
  sheet: "rgba(5, 5, 5, 0.92)",
38
- /** A grouped card. */
39
- group: night.color.surface,
38
+ /**
39
+ * A grouped card: night glass, so the star field behind the sheet shows
40
+ * through every card the way it does through the header chips. Cards
41
+ * take `groupBorder` as a hairline, or the glass reads as a smudge.
42
+ */
43
+ group: night.color.glass,
44
+ groupBorder: night.color.glassBorder,
40
45
  /** Row press / hover wash on top of a group. */
41
- rowPressed: night.color.surfaceElevated,
46
+ rowPressed: night.color.glassElevated,
42
47
  /** Hairline between rows, inset from the left like iOS. */
43
48
  separator: night.color.separator,
44
49
  /** Neutral control fill (icon discs, chips, round buttons). */
45
- fill: night.color.surfaceElevated,
50
+ fill: night.color.glassElevated,
46
51
  fillStrong: night.color.fillTertiary,
52
+ /** Opaque ring around a badge on a glass card (the sheet's own color). */
53
+ ring: night.color.bg,
54
+ /**
55
+ * Opaque disc behind a Buoy icon. The icons paint their knock-outs in
56
+ * the color they are handed, so a translucent fill turns a floppy disk
57
+ * into a yellow square. Stays solid on purpose.
58
+ */
59
+ disc: night.color.surfaceElevated,
47
60
  label: night.color.text,
48
61
  labelSecondary: night.color.textSecondary,
49
62
  labelTertiary: night.color.textTertiary,
@@ -0,0 +1,7 @@
1
+ export declare function HeaderBarButton({ armId }: {
2
+ armId?: string;
3
+ }): import("react").JSX.Element;
4
+ export declare function TimeMachineHeaderActions({ armId }: {
5
+ armId?: string;
6
+ }): import("react").JSX.Element;
7
+ //# sourceMappingURL=HeaderActions.d.ts.map
@@ -0,0 +1,9 @@
1
+ export declare function NamePromptModal({ visible, summary, onCapture, onCancel, }: {
2
+ visible: boolean;
3
+ /** What the capture will take, e.g. "5 sources · /shop/bag". */
4
+ summary?: string;
5
+ /** Called with the trimmed name; empty means "use the default". */
6
+ onCapture: (name: string) => void;
7
+ onCancel: () => void;
8
+ }): import("react").JSX.Element;
9
+ //# sourceMappingURL=NamePromptModal.d.ts.map
@@ -0,0 +1,6 @@
1
+ import { type FloatingToolRenderContext } from "@buoy-gg/shared-ui";
2
+ export declare const TIME_MACHINE_FLOATING_TOOL_ID = "time-machine-restore";
3
+ /** Open (or reveal) the restore bar, optionally arming a specific point. */
4
+ export declare function openTimeMachineFloatingTool(armId?: string): void;
5
+ export declare function TimeMachineFloatingTool({ id, revealToken, dismiss, }: FloatingToolRenderContext): import("react").JSX.Element;
6
+ //# sourceMappingURL=TimeMachineFloatingTool.d.ts.map
@@ -0,0 +1,14 @@
1
+ import type { SnapshotMeta } from "../../store/snapshotVault";
2
+ export declare function getPickedPointId(): string | null;
3
+ export declare function subscribePickedPoint(listener: () => void): () => void;
4
+ /** Read the saved pick once. Later calls return the same promise. */
5
+ export declare function ensurePickedPointLoaded(): Promise<void>;
6
+ export declare function setPickedPointId(id: string | null): void;
7
+ /** Test seam: forget the pick and the load, without touching storage. */
8
+ export declare function resetPickedPointForTests(): void;
9
+ /**
10
+ * The point the bar restores: the saved pick if it still exists, else the
11
+ * most recently restored point, else the newest capture. Never a baseline.
12
+ */
13
+ export declare function resolveArmedPoint(snapshots: readonly SnapshotMeta[], pickedId: string | null): SnapshotMeta | null;
14
+ //# sourceMappingURL=armedPoint.d.ts.map
@@ -0,0 +1,7 @@
1
+ export declare function getBarReloadMode(): boolean;
2
+ export declare function subscribeBarSettings(listener: () => void): () => void;
3
+ export declare function ensureBarSettingsLoaded(): Promise<void>;
4
+ export declare function setBarReloadMode(on: boolean): void;
5
+ /** Test seam. */
6
+ export declare function resetBarSettingsForTests(): void;
7
+ //# sourceMappingURL=barSettings.d.ts.map
@@ -1,12 +1,16 @@
1
1
  import type { SnapshotMeta } from "../../store/snapshotVault";
2
- export declare function SnapshotRow({ meta, busy, onOpen, onRestore, onDelete, onDuplicate, }: {
2
+ export declare function SnapshotRow({ meta, busy, armed, onOpen, onRestore, onDelete, onDuplicate, onArm, }: {
3
3
  meta: SnapshotMeta;
4
4
  busy: boolean;
5
+ /** This point is the one the floating restore bar restores right now. */
6
+ armed?: boolean;
5
7
  /** Zero-arg: a data-taking `onPress` prop is invoked with the event by
6
8
  * Buoy MCP's tap_element, which crashes the row. */
7
9
  onOpen: () => void;
8
10
  onRestore: () => void;
9
11
  onDelete: () => void;
10
12
  onDuplicate: () => void;
13
+ /** Put this point on the floating restore bar. */
14
+ onArm: () => void;
11
15
  }): import("react").JSX.Element;
12
16
  //# sourceMappingURL=SnapshotRow.d.ts.map
@@ -1,5 +1,7 @@
1
1
  import { type SourceStatus } from "../sources";
2
- export declare function SourcesStrip({ sources }: {
2
+ export declare function SourcesStrip({ sources, onToggle, }: {
3
3
  sources: SourceStatus[];
4
+ /** Flip a source on or off. Absent on the desktop mirror. */
5
+ onToggle?: (id: string, enabled: boolean) => void;
4
6
  }): import("react").JSX.Element;
5
7
  //# sourceMappingURL=SourcesStrip.d.ts.map
@@ -8,11 +8,18 @@
8
8
  * same question as "will my Redux store come back".
9
9
  */
10
10
  import type { ComponentType } from "react";
11
+ import { ROUTE_SOURCE_ID } from "../store/sourceIds";
12
+ export { ROUTE_SOURCE_ID };
11
13
  export interface SourceStatus {
12
14
  id: string;
13
15
  label: string;
14
16
  canCapture: boolean;
15
17
  canRestore: boolean;
18
+ /**
19
+ * Switched on in the Sources strip. Off means every capture and restore
20
+ * leaves this source alone, whatever else it could do.
21
+ */
22
+ enabled: boolean;
16
23
  /** Why it is in this state — shown on the Sources screen. */
17
24
  detail?: string;
18
25
  }
@@ -28,8 +35,6 @@ export type SourceIcon = ComponentType<{
28
35
  bgColor?: string;
29
36
  }>;
30
37
  export declare function sourceIcon(id: string): SourceIcon;
31
- /** Id of the route tile — a device capability, not a snapshot provider. */
32
- export declare const ROUTE_SOURCE_ID = "route";
33
38
  export declare function useSourceStatuses(): SourceStatus[];
34
39
  /** A source is "attached" when a snapshot would actually pick it up. */
35
40
  export declare function isAttached(source: SourceStatus): boolean;
@@ -11,9 +11,11 @@
11
11
  */
12
12
  export { timeMachineToolPreset, createTimeMachineTool } from "./preset";
13
13
  export { TimeMachineModal, TimeMachineToolView, type TimeMachineModalProps, } from "./components/TimeMachineModal";
14
+ export { openTimeMachineFloatingTool, TIME_MACHINE_FLOATING_TOOL_ID, } from "./components/floating/TimeMachineFloatingTool";
14
15
  export { timeMachineSyncAdapter } from "./sync/timeMachineSyncAdapter";
15
- export { timeMachineStore, RESTORE_ORDER, type RestoreMode, type RestoreOptions, type RestoreOutcome, type TimeMachineState, type RemoteProviderStatus, type PreviewSource, } from "./store/timeMachineStore";
16
+ export { timeMachineStore, RESTORE_ORDER, SAFETY_SNAPSHOT_NAME, type RestoreMode, type RestoreOptions, type RestoreOutcome, type TimeMachineState, type RemoteProviderStatus, type PreviewSource, } from "./store/timeMachineStore";
16
17
  export { armPendingRouteRestore, captureCurrentRoute, navigateToRoute, canNavigate as canRestoreRoute, type SnapshotRoute, type RouteRestoreResult, type RouteRestoreStatus, } from "./store/routeRestore";
18
+ export { isSourceEnabled, setSourceEnabled, subscribeSourceSettings, } from "./store/sourceSettings";
17
19
  export { setRemoteTimeMachineHandler, replaceTimeMachineFromSnapshot, } from "./store/timeMachineStore";
18
20
  export type { SnapshotMeta, SnapshotSourceMeta, StoredSnapshot, } from "./store/snapshotVault";
19
21
  //# sourceMappingURL=index.d.ts.map
@@ -14,9 +14,10 @@ export declare const timeMachineToolPreset: {
14
14
  name: string;
15
15
  description: string;
16
16
  slot: "both";
17
- icon: ({ size, color }: {
17
+ icon: ({ size, color, slot }: {
18
18
  size: number;
19
19
  color?: string;
20
+ slot?: string;
20
21
  }) => import("react").JSX.Element;
21
22
  component: typeof TimeMachineBody;
22
23
  modal: {
@@ -35,9 +36,10 @@ export declare function createTimeMachineTool(options?: {
35
36
  description: string;
36
37
  id: string;
37
38
  slot: "both";
38
- icon: ({ size, color }: {
39
+ icon: ({ size, color, slot }: {
39
40
  size: number;
40
41
  color?: string;
42
+ slot?: string;
41
43
  }) => import("react").JSX.Element;
42
44
  component: typeof TimeMachineBody;
43
45
  modal: {
@@ -65,6 +65,12 @@ export interface SnapshotMeta {
65
65
  restoreRoute?: boolean;
66
66
  /** When this point was last restored, for the "restored 4m ago" marker. */
67
67
  lastRestoredAt?: number;
68
+ /**
69
+ * The rolling "Before last restore" slot the restore bar writes before it
70
+ * restores, so a mis-tap has a way back. One exists at a time, it never
71
+ * counts against the tier cap, and the bar never arms it.
72
+ */
73
+ safety?: boolean;
68
74
  /** Source ids captured, with their capture warnings. */
69
75
  sources: Record<string, SnapshotSourceMeta>;
70
76
  }
@@ -0,0 +1,3 @@
1
+ /** Id of the route "source": a device capability, not a snapshot provider. */
2
+ export declare const ROUTE_SOURCE_ID = "route";
3
+ //# sourceMappingURL=sourceIds.d.ts.map
@@ -0,0 +1,9 @@
1
+ export declare function ensureSourceSettingsLoaded(): Promise<void>;
2
+ export declare function subscribeSourceSettings(listener: () => void): () => void;
3
+ /** Stable across renders until something changes, for useSyncExternalStore. */
4
+ export declare function getDisabledSources(): ReadonlySet<string>;
5
+ export declare function isSourceEnabled(id: string): boolean;
6
+ export declare function setSourceEnabled(id: string, enabled: boolean): void;
7
+ /** Test seam: forget everything without touching storage. */
8
+ export declare function resetSourceSettingsForTests(): void;
9
+ //# sourceMappingURL=sourceSettings.d.ts.map
@@ -32,6 +32,8 @@ export interface RestoreOutcome {
32
32
  * "deferred" means the navigation is queued for the post-reload boot.
33
33
  */
34
34
  route?: RouteRestoreResult;
35
+ /** Set when the restore wrote a safety snapshot first (see RestoreOptions). */
36
+ safetySnapshotId?: string;
35
37
  }
36
38
  /**
37
39
  * Will restoring this point also navigate back to its route? Having a route
@@ -47,13 +49,22 @@ export interface RestoreOptions {
47
49
  * app's own start route).
48
50
  */
49
51
  restoreRoute?: boolean;
52
+ /**
53
+ * Capture the app's current state into the rolling "Before last restore"
54
+ * slot first, so this restore can be undone. The outcome carries its id as
55
+ * `safetySnapshotId`. Local only: the desktop mirror has no bar.
56
+ */
57
+ safetyCapture?: boolean;
50
58
  }
59
+ export declare const SAFETY_SNAPSHOT_NAME = "Before last restore";
51
60
  /** Provider capability row as mirrored to a remote dashboard. */
52
61
  export interface RemoteProviderStatus {
53
62
  id: string;
54
63
  label: string;
55
64
  canCapture: boolean;
56
65
  canRestore: boolean;
66
+ /** False when the device has this source switched off in Sources. */
67
+ enabled?: boolean;
57
68
  detail?: string;
58
69
  }
59
70
  /** One side of a preview comparison. */
@@ -104,6 +115,8 @@ export interface TimeMachineState {
104
115
  /** Whether this device can capture and return to routes, and where it is now. */
105
116
  export interface RouteToolStatus {
106
117
  available: boolean;
118
+ /** False when the device has the route source switched off in Sources. */
119
+ enabled?: boolean;
107
120
  /** The route the app is on right now, when one can be read. */
108
121
  current?: string;
109
122
  }
@@ -128,7 +141,9 @@ declare class TimeMachineStore {
128
141
  route?: RouteToolStatus;
129
142
  }): void;
130
143
  /** Capture all capturable sources (or a subset) into a named snapshot. */
131
- capture(name: string, sourceIds?: string[]): Promise<SnapshotMeta>;
144
+ capture(name: string, sourceIds?: string[], options?: {
145
+ safety?: boolean;
146
+ }): Promise<SnapshotMeta>;
132
147
  /**
133
148
  * Capture the fresh-install baseline: an EMPTY snapshot whose restore
134
149
  * wipes app state (clear + reload) — regardless of what state the app is