@buoy-gg/time-machine 7.0.17 → 7.0.19

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 (69) hide show
  1. package/lib/commonjs/components/HeaderReloadButton.js +61 -0
  2. package/lib/commonjs/components/HeaderReloadButton.js.map +1 -0
  3. package/lib/commonjs/components/TimeMachineBody.js +343 -127
  4. package/lib/commonjs/components/TimeMachineBody.js.map +1 -1
  5. package/lib/commonjs/components/TimeMachineModal.js +1 -0
  6. package/lib/commonjs/components/TimeMachineModal.js.map +1 -1
  7. package/lib/commonjs/components/preview/ItemDetailScreen.js +189 -0
  8. package/lib/commonjs/components/preview/ItemDetailScreen.js.map +1 -0
  9. package/lib/commonjs/components/preview/RestorePreviewScreen.js +1056 -0
  10. package/lib/commonjs/components/preview/RestorePreviewScreen.js.map +1 -0
  11. package/lib/commonjs/components/preview/verdictMeta.js +58 -0
  12. package/lib/commonjs/components/preview/verdictMeta.js.map +1 -0
  13. package/lib/commonjs/preset.js +11 -2
  14. package/lib/commonjs/preset.js.map +1 -1
  15. package/lib/commonjs/store/snapshotVault.js +19 -0
  16. package/lib/commonjs/store/snapshotVault.js.map +1 -1
  17. package/lib/commonjs/store/timeMachineStore.js +298 -7
  18. package/lib/commonjs/store/timeMachineStore.js.map +1 -1
  19. package/lib/commonjs/sync/timeMachineSyncAdapter.js +62 -2
  20. package/lib/commonjs/sync/timeMachineSyncAdapter.js.map +1 -1
  21. package/lib/module/components/HeaderReloadButton.js +56 -0
  22. package/lib/module/components/HeaderReloadButton.js.map +1 -0
  23. package/lib/module/components/TimeMachineBody.js +345 -129
  24. package/lib/module/components/TimeMachineBody.js.map +1 -1
  25. package/lib/module/components/TimeMachineModal.js +1 -0
  26. package/lib/module/components/TimeMachineModal.js.map +1 -1
  27. package/lib/module/components/preview/ItemDetailScreen.js +184 -0
  28. package/lib/module/components/preview/ItemDetailScreen.js.map +1 -0
  29. package/lib/module/components/preview/RestorePreviewScreen.js +1051 -0
  30. package/lib/module/components/preview/RestorePreviewScreen.js.map +1 -0
  31. package/lib/module/components/preview/verdictMeta.js +53 -0
  32. package/lib/module/components/preview/verdictMeta.js.map +1 -0
  33. package/lib/module/preset.js +11 -2
  34. package/lib/module/preset.js.map +1 -1
  35. package/lib/module/store/snapshotVault.js +18 -0
  36. package/lib/module/store/snapshotVault.js.map +1 -1
  37. package/lib/module/store/timeMachineStore.js +299 -8
  38. package/lib/module/store/timeMachineStore.js.map +1 -1
  39. package/lib/module/sync/timeMachineSyncAdapter.js +62 -2
  40. package/lib/module/sync/timeMachineSyncAdapter.js.map +1 -1
  41. package/lib/typescript/components/HeaderReloadButton.d.ts +2 -0
  42. package/lib/typescript/components/HeaderReloadButton.d.ts.map +1 -0
  43. package/lib/typescript/components/TimeMachineBody.d.ts.map +1 -1
  44. package/lib/typescript/components/TimeMachineModal.d.ts.map +1 -1
  45. package/lib/typescript/components/preview/ItemDetailScreen.d.ts +11 -0
  46. package/lib/typescript/components/preview/ItemDetailScreen.d.ts.map +1 -0
  47. package/lib/typescript/components/preview/RestorePreviewScreen.d.ts +15 -0
  48. package/lib/typescript/components/preview/RestorePreviewScreen.d.ts.map +1 -0
  49. package/lib/typescript/components/preview/verdictMeta.d.ts +10 -0
  50. package/lib/typescript/components/preview/verdictMeta.d.ts.map +1 -0
  51. package/lib/typescript/preset.d.ts +15 -3
  52. package/lib/typescript/preset.d.ts.map +1 -1
  53. package/lib/typescript/store/snapshotVault.d.ts +7 -0
  54. package/lib/typescript/store/snapshotVault.d.ts.map +1 -1
  55. package/lib/typescript/store/timeMachineStore.d.ts +49 -2
  56. package/lib/typescript/store/timeMachineStore.d.ts.map +1 -1
  57. package/lib/typescript/sync/timeMachineSyncAdapter.d.ts +36 -0
  58. package/lib/typescript/sync/timeMachineSyncAdapter.d.ts.map +1 -1
  59. package/package.json +19 -7
  60. package/src/components/HeaderReloadButton.tsx +45 -0
  61. package/src/components/TimeMachineBody.tsx +416 -137
  62. package/src/components/TimeMachineModal.tsx +1 -0
  63. package/src/components/preview/ItemDetailScreen.tsx +172 -0
  64. package/src/components/preview/RestorePreviewScreen.tsx +1089 -0
  65. package/src/components/preview/verdictMeta.ts +40 -0
  66. package/src/preset.tsx +11 -2
  67. package/src/store/snapshotVault.ts +28 -0
  68. package/src/store/timeMachineStore.ts +330 -7
  69. package/src/sync/timeMachineSyncAdapter.ts +63 -3
@@ -0,0 +1,1051 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Restore Preview — "what will restoring this snapshot actually change?"
5
+ *
6
+ * ONE virtualized FlatList is the scrollable area (compare card, filters,
7
+ * source cards and item rows are all list rows) with a sticky Restore CTA
8
+ * footer — no nested scroll regions, thousands of storage keys stay cheap.
9
+ *
10
+ * Layout follows the Time Machine mockup: source sections are card
11
+ * dropdowns with tool-icon tiles, a "N keys · M changed" detection subtitle,
12
+ * ±≈ badges, a search field with an expand/collapse-all button, and a bottom
13
+ * "Restore State" pill. Only rows a restore would touch (or can't touch)
14
+ * render — unchanged items never do. Every applicable item keeps its
15
+ * INCLUDE checkbox (source headers toggle their whole section) — unchecked
16
+ * items are passed to restore as exclusions and left untouched.
17
+ */
18
+ import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
19
+ import { ActivityIndicator, FlatList, StyleSheet, Text, TextInput, TouchableOpacity, View } from "react-native";
20
+ import { ChevronDown, ChevronUp, Filter, RefreshCw, Search, buoyColors, macOSColors } from "@buoy-gg/shared-ui";
21
+ import { JotaiIcon, QueryIcon, ReduxIcon, StorageIcon, TimeMachineIcon, ZustandIcon } from "@buoy-gg/floating-tools-core";
22
+ import { useToolHeader, useToolHost } from "@buoy-gg/core";
23
+ import { timeMachineStore } from "../../store/timeMachineStore";
24
+ import { VERDICT_META } from "./verdictMeta";
25
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
26
+ const SOURCE_ICONS = {
27
+ storage: StorageIcon,
28
+ redux: ReduxIcon,
29
+ zustand: ZustandIcon,
30
+ jotai: JotaiIcon,
31
+ query: QueryIcon
32
+ };
33
+ const SOURCE_NOUNS = {
34
+ storage: "keys",
35
+ redux: "slices",
36
+ zustand: "stores",
37
+ jotai: "atoms",
38
+ query: "queries"
39
+ };
40
+ /** Compound exclusion key — what timeMachineStore.restore expects. */
41
+ function cKey(sourceId, itemKey) {
42
+ return `${sourceId}::${itemKey}`;
43
+ }
44
+ function shortPreview(value) {
45
+ if (value === undefined) return "∅";
46
+ try {
47
+ const s = typeof value === "string" ? `"${value}"` : JSON.stringify(value);
48
+ if (!s) return String(value);
49
+ return s.length > 46 ? `${s.slice(0, 43)}…` : s;
50
+ } catch {
51
+ return "[complex]";
52
+ }
53
+ }
54
+ function sideName(ref, snapshots) {
55
+ if (ref === "current") return "Current state";
56
+ return snapshots.find(s => s.id === ref.snapshotId)?.name ?? ref.snapshotId;
57
+ }
58
+ function timeAgo(ts) {
59
+ const s = Math.max(0, Math.round((Date.now() - ts) / 1000));
60
+ if (s < 60) return `${s}s`;
61
+ const m = Math.round(s / 60);
62
+ if (m < 60) return `${m}m`;
63
+ const h = Math.round(m / 60);
64
+ return h < 24 ? `${h}h` : `${Math.round(h / 24)}d`;
65
+ }
66
+ function Checkbox({
67
+ state
68
+ }) {
69
+ return /*#__PURE__*/_jsxs(View, {
70
+ style: [styles.checkbox, state !== "none" && styles.checkboxChecked],
71
+ children: [state === "all" && /*#__PURE__*/_jsx(Text, {
72
+ style: styles.checkboxGlyph,
73
+ children: "\u2713"
74
+ }), state === "some" && /*#__PURE__*/_jsx(Text, {
75
+ style: styles.checkboxGlyph,
76
+ children: "\u2212"
77
+ })]
78
+ });
79
+ }
80
+ const ItemRow = /*#__PURE__*/memo(function ItemRow({
81
+ row,
82
+ onOpen,
83
+ onToggle
84
+ }) {
85
+ const meta = VERDICT_META[row.item.verdict];
86
+ const dimmed = !meta.applies || row.excluded;
87
+ const openable = row.item.verdict === "changed" || row.item.verdict === "added" || row.item.verdict === "removed";
88
+ return /*#__PURE__*/_jsxs(View, {
89
+ style: [styles.itemRow, dimmed && styles.dimmed],
90
+ children: [meta.applies && /*#__PURE__*/_jsx(TouchableOpacity, {
91
+ onPress: onToggle,
92
+ hitSlop: {
93
+ top: 10,
94
+ bottom: 10,
95
+ left: 8,
96
+ right: 8
97
+ },
98
+ testID: `tm-preview-toggle-${row.sourceId}-${row.item.key}`,
99
+ children: /*#__PURE__*/_jsx(Checkbox, {
100
+ state: row.excluded ? "none" : "all"
101
+ })
102
+ }), /*#__PURE__*/_jsxs(TouchableOpacity, {
103
+ style: styles.itemBodyTouch,
104
+ onPress: () => openable && onOpen(),
105
+ disabled: !openable,
106
+ testID: `tm-preview-item-${row.sourceId}-${row.item.key}`,
107
+ children: [/*#__PURE__*/_jsx(Text, {
108
+ style: [styles.itemGlyph, {
109
+ color: meta.color
110
+ }],
111
+ children: meta.glyph
112
+ }), /*#__PURE__*/_jsxs(View, {
113
+ style: styles.itemBody,
114
+ children: [/*#__PURE__*/_jsx(Text, {
115
+ style: styles.itemLabel,
116
+ numberOfLines: 1,
117
+ children: row.item.label
118
+ }), row.item.verdict === "changed" && /*#__PURE__*/_jsxs(Text, {
119
+ style: styles.itemPreview,
120
+ numberOfLines: 1,
121
+ children: [shortPreview(row.item.oldValue), " \u2192", " ", shortPreview(row.item.newValue)]
122
+ }), row.item.verdict === "added" && /*#__PURE__*/_jsx(Text, {
123
+ style: styles.itemPreview,
124
+ numberOfLines: 1,
125
+ children: shortPreview(row.item.newValue)
126
+ }), row.item.verdict === "removed" && /*#__PURE__*/_jsx(Text, {
127
+ style: [styles.itemPreview, styles.itemPreviewRemoved],
128
+ numberOfLines: 1,
129
+ children: shortPreview(row.item.oldValue)
130
+ }), row.excluded ? /*#__PURE__*/_jsx(Text, {
131
+ style: styles.itemExcluded,
132
+ children: "excluded \u2014 restore leaves this untouched"
133
+ }) : row.item.reason && /*#__PURE__*/_jsx(Text, {
134
+ style: styles.itemReason,
135
+ numberOfLines: 2,
136
+ children: row.item.reason
137
+ })]
138
+ }), openable && /*#__PURE__*/_jsx(Text, {
139
+ style: styles.itemChevron,
140
+ children: "\u203A"
141
+ })]
142
+ })]
143
+ });
144
+ });
145
+ export function RestorePreviewScreen({
146
+ preview,
147
+ snapshots,
148
+ busy,
149
+ onBack,
150
+ onOpenItem,
151
+ onRestore,
152
+ onPickSide,
153
+ onRefresh
154
+ }) {
155
+ const host = useToolHost();
156
+ const hosted = host.instanceId !== undefined;
157
+ const [localSearch, setLocalSearch] = useState("");
158
+ const [collapsed, setCollapsed] = useState({});
159
+ const [allCollapsed, setAllCollapsed] = useState(false);
160
+ const [excluded, setExcluded] = useState(new Set());
161
+ const search = localSearch;
162
+ const rightMeta = preview.right === "current" ? undefined : snapshots.find(s => s.id === preview.right.snapshotId);
163
+ const isBaseline = rightMeta?.baseline === true;
164
+ const liveLeft = preview.left === "current";
165
+
166
+ // Master selection: "None → tick just what you want" is the fast path for
167
+ // targeted restores ("only my cart"), the inverse of the default all-in.
168
+ const selectAll = useCallback(() => setExcluded(new Set()), []);
169
+ const selectNone = useCallback(() => {
170
+ const next = new Set();
171
+ for (const src of preview.sources) {
172
+ for (const item of src.items) {
173
+ if (VERDICT_META[item.verdict].applies) next.add(cKey(src.id, item.key));
174
+ }
175
+ }
176
+ setExcluded(next);
177
+ }, [preview.sources]);
178
+
179
+ // A saved scope pre-selects the checkboxes: everything applicable that is
180
+ // OUTSIDE the scope starts excluded, so the preview shows exactly what a
181
+ // scoped restore will do (and stays adjustable).
182
+ const seededScopeFor = useRef(null);
183
+ useEffect(() => {
184
+ if (!rightMeta?.scope?.length || preview.building) return;
185
+ if (seededScopeFor.current === rightMeta.id) return;
186
+ seededScopeFor.current = rightMeta.id;
187
+ const scopeSet = new Set(rightMeta.scope);
188
+ const next = new Set();
189
+ for (const src of preview.sources) {
190
+ for (const item of src.items) {
191
+ const key = cKey(src.id, item.key);
192
+ if (VERDICT_META[item.verdict].applies && !scopeSet.has(key)) {
193
+ next.add(key);
194
+ }
195
+ }
196
+ }
197
+ setExcluded(next);
198
+ }, [rightMeta, preview.building, preview.sources]);
199
+
200
+ // Persist the current selection as this restore point's scope — from then
201
+ // on, RESTORE (home row, MCP, this screen) touches only these items.
202
+ // Selecting everything clears the scope back to a full restore.
203
+ const saveScope = useCallback(() => {
204
+ if (!rightMeta) return;
205
+ const selected = [];
206
+ for (const src of preview.sources) {
207
+ for (const item of src.items) {
208
+ const key = cKey(src.id, item.key);
209
+ if (VERDICT_META[item.verdict].applies && !excluded.has(key)) {
210
+ selected.push(key);
211
+ }
212
+ }
213
+ }
214
+ const clearing = excluded.size === 0;
215
+ void timeMachineStore.setScope(rightMeta.id, clearing ? null : selected);
216
+ host.showToast(clearing ? "Scope cleared — full restore" : `Scope saved — this restore point now restores only ${selected.length} item${selected.length === 1 ? "" : "s"}`, "success");
217
+ }, [rightMeta, preview.sources, excluded, host]);
218
+ const toggleItem = useCallback(key => {
219
+ setExcluded(prev => {
220
+ const next = new Set(prev);
221
+ if (next.has(key)) next.delete(key);else next.add(key);
222
+ return next;
223
+ });
224
+ }, []);
225
+ const toggleSource = useCallback((source, includeState) => {
226
+ setExcluded(prev => {
227
+ const next = new Set(prev);
228
+ for (const item of source.items) {
229
+ if (!VERDICT_META[item.verdict].applies) continue;
230
+ const key = cKey(source.id, item.key);
231
+ if (includeState === "all") next.add(key);else next.delete(key);
232
+ }
233
+ return next;
234
+ });
235
+ }, []);
236
+
237
+ // Takes the EFFECTIVE collapsed state — the map is sparse (defaults come
238
+ // from the ?? fallback), so `!map[id]` would no-op the first tap on a
239
+ // default-collapsed card.
240
+ const toggleCollapse = useCallback((id, currentlyCollapsed) => {
241
+ setCollapsed(c => ({
242
+ ...c,
243
+ [id]: !currentlyCollapsed
244
+ }));
245
+ }, []);
246
+ const toggleAllCollapsed = useCallback(() => {
247
+ setAllCollapsed(prev => {
248
+ const next = !prev;
249
+ setCollapsed(Object.fromEntries(preview.sources.map(s => [s.id, next])));
250
+ return next;
251
+ });
252
+ }, [preview.sources]);
253
+ const totals = useMemo(() => {
254
+ let applies = 0;
255
+ let excludedCount = 0;
256
+ let wontApply = 0;
257
+ const perSource = [];
258
+ for (const src of preview.sources) {
259
+ let n = 0;
260
+ for (const item of src.items) {
261
+ const m = VERDICT_META[item.verdict];
262
+ if (m.applies) {
263
+ if (excluded.has(cKey(src.id, item.key))) excludedCount += 1;else {
264
+ applies += 1;
265
+ n += 1;
266
+ }
267
+ } else if (item.verdict === "wont-apply") wontApply += 1;
268
+ }
269
+ if (n > 0) perSource.push(`${n} ${src.label.toLowerCase()}`);
270
+ }
271
+ return {
272
+ applies,
273
+ excludedCount,
274
+ wontApply,
275
+ perSource
276
+ };
277
+ }, [preview.sources, excluded]);
278
+
279
+ // Hosted: back + identity in the shared modal header. Restore lives in the
280
+ // bottom CTA; the preset's labeled RELOAD APP action stays available.
281
+ useToolHeader(hosted ? {
282
+ title: "Restore Preview",
283
+ subtitle: rightMeta?.name ?? "compare",
284
+ onBack,
285
+ searchable: false // the in-body "Filter keys…" field covers it
286
+ } : null,
287
+ // eslint-disable-next-line react-hooks/exhaustive-deps
288
+ [hosted, rightMeta?.name]);
289
+ const rows = useMemo(() => {
290
+ const q = search.trim().toLowerCase();
291
+ const out = [];
292
+ for (const src of preview.sources) {
293
+ // Every detected source gets a card whose subtitle carries the
294
+ // detection proof ("60 keys · 7 changed" / "2 slices · no changes"),
295
+ // so clean sources need no expansion. Unchanged ITEMS never render.
296
+ const relevant = src.items.some(i => i.verdict !== "unchanged");
297
+ const expandable = relevant || src.notes.length > 0 || !!src.error;
298
+ const applicable = src.items.filter(i => VERDICT_META[i.verdict].applies);
299
+ const wontApplyCount = src.items.filter(i => i.verdict === "wont-apply").length;
300
+ const noun = src.items.length === 1 ? (SOURCE_NOUNS[src.id] ?? "items").replace(/ies$/, "y").replace(/s$/, "") : SOURCE_NOUNS[src.id] ?? "items";
301
+ const summary = `${src.items.length} ${noun} · ` + (applicable.length > 0 ? `${applicable.length} changed` : wontApplyCount > 0 ? `${wontApplyCount} won't apply` : "no changes");
302
+ const excludedInSource = applicable.filter(i => excluded.has(cKey(src.id, i.key))).length;
303
+ const includeState = applicable.length === 0 || excludedInSource === 0 ? "all" : excludedInSource === applicable.length ? "none" : "some";
304
+ const counts = {
305
+ added: 0,
306
+ removed: 0,
307
+ changed: 0
308
+ };
309
+ for (const i of src.items) {
310
+ if (excluded.has(cKey(src.id, i.key))) continue;
311
+ if (i.verdict === "added") counts.added += 1;else if (i.verdict === "removed") counts.removed += 1;else if (i.verdict === "changed" || i.verdict === "too-large") counts.changed += 1;
312
+ }
313
+ const visible = src.items.filter(item => {
314
+ // Unchanged items never render — the dropdowns show only what a
315
+ // restore would touch (changed) or can't touch (won't-apply).
316
+ if (item.verdict === "unchanged") return false;
317
+ if (q && !item.label.toLowerCase().includes(q)) return false;
318
+ return true;
319
+ });
320
+ const isCollapsed = !expandable ? true : collapsed[src.id] ?? (applicable.length === 0 && !src.error);
321
+ out.push({
322
+ kind: "source",
323
+ source: src,
324
+ collapsed: isCollapsed,
325
+ includeState,
326
+ applicableCount: applicable.length,
327
+ counts,
328
+ expandable,
329
+ summary
330
+ });
331
+ if (!isCollapsed) {
332
+ for (const note of src.notes) {
333
+ out.push({
334
+ kind: "note",
335
+ id: `${src.id}:${note}`,
336
+ text: note
337
+ });
338
+ }
339
+ if (src.error) {
340
+ out.push({
341
+ kind: "note",
342
+ id: `${src.id}:err`,
343
+ text: `⚠ ${src.error}`
344
+ });
345
+ }
346
+ for (const item of visible) {
347
+ out.push({
348
+ kind: "item",
349
+ sourceId: src.id,
350
+ item,
351
+ excluded: excluded.has(cKey(src.id, item.key))
352
+ });
353
+ }
354
+ }
355
+ }
356
+ return out;
357
+ }, [preview.sources, search, collapsed, excluded]);
358
+ const renderRow = useCallback(({
359
+ item: row
360
+ }) => {
361
+ if (row.kind === "source") {
362
+ const src = row.source;
363
+ const {
364
+ counts
365
+ } = row;
366
+ const clean = counts.added + counts.removed + counts.changed === 0 && !src.error;
367
+ const IconComp = SOURCE_ICONS[src.id] ?? TimeMachineIcon;
368
+ return /*#__PURE__*/_jsxs(View, {
369
+ style: styles.sourceCard,
370
+ children: [row.applicableCount > 0 && /*#__PURE__*/_jsx(TouchableOpacity, {
371
+ onPress: () => toggleSource(src, row.includeState),
372
+ hitSlop: {
373
+ top: 10,
374
+ bottom: 10,
375
+ left: 8,
376
+ right: 8
377
+ },
378
+ testID: `tm-preview-source-toggle-${src.id}`,
379
+ children: /*#__PURE__*/_jsx(Checkbox, {
380
+ state: row.includeState
381
+ })
382
+ }), /*#__PURE__*/_jsxs(TouchableOpacity, {
383
+ style: styles.sourceCardTouch,
384
+ onPress: () => row.expandable && toggleCollapse(src.id, row.collapsed),
385
+ disabled: !row.expandable,
386
+ testID: `tm-preview-source-${src.id}`,
387
+ children: [/*#__PURE__*/_jsx(View, {
388
+ style: styles.sourceIconTile,
389
+ children: /*#__PURE__*/_jsx(IconComp, {
390
+ size: 18
391
+ })
392
+ }), /*#__PURE__*/_jsxs(View, {
393
+ style: styles.sourceNameBlock,
394
+ children: [/*#__PURE__*/_jsx(Text, {
395
+ style: styles.sourceName,
396
+ numberOfLines: 1,
397
+ children: src.label
398
+ }), /*#__PURE__*/_jsx(Text, {
399
+ style: styles.sourceSummary,
400
+ numberOfLines: 1,
401
+ children: row.includeState === "none" && row.applicableCount > 0 ? `${row.summary} · excluded` : row.summary
402
+ })]
403
+ }), !clean && /*#__PURE__*/_jsxs(View, {
404
+ style: styles.sourceBadges,
405
+ children: [counts.removed > 0 && /*#__PURE__*/_jsxs(Text, {
406
+ style: [styles.sourceBadge, {
407
+ color: buoyColors.error
408
+ }],
409
+ children: ["\u2212", counts.removed]
410
+ }), counts.added > 0 && /*#__PURE__*/_jsxs(Text, {
411
+ style: [styles.sourceBadge, {
412
+ color: buoyColors.success
413
+ }],
414
+ children: ["+", counts.added]
415
+ }), counts.changed > 0 && /*#__PURE__*/_jsxs(Text, {
416
+ style: [styles.sourceBadge, {
417
+ color: buoyColors.info
418
+ }],
419
+ children: ["\u2248", counts.changed]
420
+ })]
421
+ }), row.expandable ? row.collapsed ? /*#__PURE__*/_jsx(ChevronDown, {
422
+ size: 16,
423
+ color: buoyColors.textMuted
424
+ }) : /*#__PURE__*/_jsx(ChevronUp, {
425
+ size: 16,
426
+ color: buoyColors.textMuted
427
+ }) : null]
428
+ })]
429
+ });
430
+ }
431
+ if (row.kind === "note") {
432
+ return /*#__PURE__*/_jsx(Text, {
433
+ style: styles.noteRow,
434
+ children: row.text
435
+ });
436
+ }
437
+ return /*#__PURE__*/_jsx(ItemRow, {
438
+ row: row,
439
+ onOpen: () => onOpenItem(row.sourceId, row.item),
440
+ onToggle: () => toggleItem(cKey(row.sourceId, row.item.key))
441
+ });
442
+ }, [onOpenItem, toggleCollapse, toggleItem, toggleSource]);
443
+ const keyExtractor = useCallback(row => {
444
+ if (row.kind === "source") return `s:${row.source.id}`;
445
+ if (row.kind === "note") return `n:${row.id}`;
446
+ return `i:${row.sourceId}:${row.item.key}`;
447
+ }, []);
448
+ const header = /*#__PURE__*/_jsxs(View, {
449
+ children: [!hosted && /*#__PURE__*/_jsxs(View, {
450
+ style: styles.fallbackHeader,
451
+ children: [/*#__PURE__*/_jsx(TouchableOpacity, {
452
+ onPress: onBack,
453
+ hitSlop: HIT,
454
+ testID: "tm-preview-back",
455
+ children: /*#__PURE__*/_jsx(Text, {
456
+ style: styles.backText,
457
+ children: "\u2039 Back"
458
+ })
459
+ }), /*#__PURE__*/_jsx(Text, {
460
+ style: styles.screenTitle,
461
+ numberOfLines: 1,
462
+ children: "Restore Preview"
463
+ })]
464
+ }), /*#__PURE__*/_jsxs(View, {
465
+ style: styles.compareCard,
466
+ children: [/*#__PURE__*/_jsxs(TouchableOpacity, {
467
+ style: styles.compareSide,
468
+ onPress: () => onPickSide("left"),
469
+ testID: "tm-preview-pick-left",
470
+ children: [/*#__PURE__*/_jsxs(View, {
471
+ style: styles.compareTagRow,
472
+ children: [/*#__PURE__*/_jsx(View, {
473
+ style: [styles.stateDot, {
474
+ backgroundColor: buoyColors.info
475
+ }]
476
+ }), /*#__PURE__*/_jsx(Text, {
477
+ style: styles.compareTag,
478
+ children: liveLeft ? "CURRENT STATE" : "SNAPSHOT"
479
+ })]
480
+ }), /*#__PURE__*/_jsx(Text, {
481
+ style: styles.compareName,
482
+ numberOfLines: 1,
483
+ children: sideName(preview.left, snapshots)
484
+ }), liveLeft ? /*#__PURE__*/_jsxs(View, {
485
+ style: styles.compareMetaRow,
486
+ children: [/*#__PURE__*/_jsxs(Text, {
487
+ style: styles.compareMeta,
488
+ children: ["built ", timeAgo(preview.builtAt), " ago"]
489
+ }), /*#__PURE__*/_jsx(TouchableOpacity, {
490
+ onPress: onRefresh,
491
+ disabled: preview.building,
492
+ hitSlop: {
493
+ top: 8,
494
+ bottom: 8,
495
+ left: 6,
496
+ right: 8
497
+ },
498
+ accessibilityLabel: "Rebuild preview",
499
+ testID: "tm-preview-refresh",
500
+ children: /*#__PURE__*/_jsx(RefreshCw, {
501
+ size: 11,
502
+ color: preview.building ? macOSColors.text.disabled : buoyColors.textSecondary
503
+ })
504
+ })]
505
+ }) : /*#__PURE__*/_jsx(Text, {
506
+ style: styles.compareMeta,
507
+ children: "snapshot"
508
+ })]
509
+ }), /*#__PURE__*/_jsx(View, {
510
+ style: styles.swapBadge,
511
+ children: /*#__PURE__*/_jsx(Text, {
512
+ style: styles.swapGlyph,
513
+ children: "\u21C4"
514
+ })
515
+ }), /*#__PURE__*/_jsxs(TouchableOpacity, {
516
+ style: [styles.compareSide, styles.compareSideRight],
517
+ onPress: () => onPickSide("right"),
518
+ testID: "tm-preview-pick-right",
519
+ children: [/*#__PURE__*/_jsxs(View, {
520
+ style: styles.compareTagRow,
521
+ children: [/*#__PURE__*/_jsx(Text, {
522
+ style: styles.compareTag,
523
+ children: isBaseline ? "BASELINE" : "SNAPSHOT"
524
+ }), /*#__PURE__*/_jsx(View, {
525
+ style: [styles.stateDot, {
526
+ backgroundColor: buoyColors.success
527
+ }]
528
+ })]
529
+ }), /*#__PURE__*/_jsx(Text, {
530
+ style: [styles.compareName, styles.compareNameRight],
531
+ numberOfLines: 1,
532
+ children: sideName(preview.right, snapshots)
533
+ }), /*#__PURE__*/_jsx(Text, {
534
+ style: [styles.compareMeta, styles.compareMetaRight],
535
+ children: rightMeta ? `${timeAgo(rightMeta.createdAt)} ago` : ""
536
+ })]
537
+ })]
538
+ }), /*#__PURE__*/_jsxs(View, {
539
+ style: styles.changesRow,
540
+ children: [/*#__PURE__*/_jsxs(View, {
541
+ style: styles.changesTitleRow,
542
+ children: [/*#__PURE__*/_jsx(Text, {
543
+ style: styles.changesTitle,
544
+ children: "Changes to restore"
545
+ }), totals.applies + totals.excludedCount > 1 && /*#__PURE__*/_jsxs(View, {
546
+ style: styles.selectLinks,
547
+ children: [/*#__PURE__*/_jsx(TouchableOpacity, {
548
+ onPress: selectAll,
549
+ hitSlop: {
550
+ top: 8,
551
+ bottom: 8,
552
+ left: 4,
553
+ right: 4
554
+ },
555
+ testID: "tm-preview-select-all",
556
+ children: /*#__PURE__*/_jsx(Text, {
557
+ style: [styles.selectLink, totals.excludedCount === 0 && styles.selectLinkActive],
558
+ children: "all"
559
+ })
560
+ }), /*#__PURE__*/_jsx(Text, {
561
+ style: styles.selectLinkSep,
562
+ children: "\xB7"
563
+ }), /*#__PURE__*/_jsx(TouchableOpacity, {
564
+ onPress: selectNone,
565
+ hitSlop: {
566
+ top: 8,
567
+ bottom: 8,
568
+ left: 4,
569
+ right: 4
570
+ },
571
+ testID: "tm-preview-select-none",
572
+ children: /*#__PURE__*/_jsx(Text, {
573
+ style: [styles.selectLink, totals.applies === 0 && totals.excludedCount > 0 && styles.selectLinkActive],
574
+ children: "none"
575
+ })
576
+ }), liveLeft && rightMeta && /*#__PURE__*/_jsxs(_Fragment, {
577
+ children: [/*#__PURE__*/_jsx(Text, {
578
+ style: styles.selectLinkSep,
579
+ children: "\xB7"
580
+ }), /*#__PURE__*/_jsx(TouchableOpacity, {
581
+ onPress: saveScope,
582
+ hitSlop: {
583
+ top: 8,
584
+ bottom: 8,
585
+ left: 4,
586
+ right: 4
587
+ },
588
+ testID: "tm-preview-save-scope",
589
+ children: /*#__PURE__*/_jsx(Text, {
590
+ style: [styles.selectLink, !!rightMeta.scope?.length && styles.selectLinkActive],
591
+ children: "save scope"
592
+ })
593
+ })]
594
+ })]
595
+ })]
596
+ }), preview.building ? /*#__PURE__*/_jsx(ActivityIndicator, {
597
+ size: "small",
598
+ color: buoyColors.primary
599
+ }) : /*#__PURE__*/_jsx(View, {
600
+ style: styles.changesChip,
601
+ children: /*#__PURE__*/_jsxs(Text, {
602
+ style: styles.changesChipText,
603
+ numberOfLines: 1,
604
+ children: [totals.applies === 0 ? totals.excludedCount > 0 ? "everything excluded" : "no changes" : totals.perSource.join(" · "), totals.excludedCount > 0 && totals.applies > 0 ? ` · ${totals.excludedCount} excluded` : ""]
605
+ })
606
+ })]
607
+ }), totals.wontApply > 0 && /*#__PURE__*/_jsxs(Text, {
608
+ style: styles.wontApplyLine,
609
+ children: ["\u2298 ", totals.wontApply, " item", totals.wontApply === 1 ? "" : "s", " won't apply \u2014 shown inline with the reason"]
610
+ }), /*#__PURE__*/_jsxs(View, {
611
+ style: styles.searchRow,
612
+ children: [/*#__PURE__*/_jsxs(View, {
613
+ style: styles.searchField,
614
+ children: [/*#__PURE__*/_jsx(Search, {
615
+ size: 14,
616
+ color: buoyColors.textMuted
617
+ }), /*#__PURE__*/_jsx(TextInput, {
618
+ style: styles.searchInput,
619
+ value: localSearch,
620
+ onChangeText: setLocalSearch,
621
+ placeholder: "Filter keys\u2026",
622
+ placeholderTextColor: buoyColors.textMuted,
623
+ autoCorrect: false,
624
+ autoCapitalize: "none",
625
+ testID: "tm-preview-search"
626
+ })]
627
+ }), /*#__PURE__*/_jsx(TouchableOpacity, {
628
+ style: styles.collapseAllButton,
629
+ onPress: toggleAllCollapsed,
630
+ accessibilityLabel: allCollapsed ? "Expand all" : "Collapse all",
631
+ testID: "tm-preview-collapse-all",
632
+ children: /*#__PURE__*/_jsx(Filter, {
633
+ size: 15,
634
+ color: buoyColors.textSecondary
635
+ })
636
+ })]
637
+ })]
638
+ });
639
+ const ctaDisabled = busy || preview.building || totals.applies === 0;
640
+ return /*#__PURE__*/_jsxs(View, {
641
+ style: styles.container,
642
+ children: [/*#__PURE__*/_jsx(FlatList, {
643
+ data: rows,
644
+ renderItem: renderRow,
645
+ keyExtractor: keyExtractor,
646
+ ListHeaderComponent: header,
647
+ ListEmptyComponent: preview.building ? null : /*#__PURE__*/_jsx(Text, {
648
+ style: styles.emptyText,
649
+ children: search ? "Nothing matches this filter." : "No changes — restoring would change nothing."
650
+ }),
651
+ removeClippedSubviews: true,
652
+ initialNumToRender: 20,
653
+ windowSize: 7,
654
+ contentContainerStyle: styles.listContent
655
+ }), liveLeft && rightMeta && /*#__PURE__*/_jsxs(View, {
656
+ style: styles.footer,
657
+ children: [/*#__PURE__*/_jsxs(TouchableOpacity, {
658
+ style: [styles.cta, isBaseline && styles.ctaDanger, ctaDisabled && styles.ctaDisabled],
659
+ onPress: () => onRestore(isBaseline ? "reload" : "live", Array.from(excluded)),
660
+ disabled: isBaseline ? busy || preview.building : ctaDisabled,
661
+ testID: "tm-preview-restore",
662
+ children: [/*#__PURE__*/_jsx(RefreshCw, {
663
+ size: 16,
664
+ color: "#0A0A0A"
665
+ }), /*#__PURE__*/_jsx(Text, {
666
+ style: styles.ctaText,
667
+ children: isBaseline ? "Wipe & Reload" : "Restore State"
668
+ })]
669
+ }), /*#__PURE__*/_jsx(Text, {
670
+ style: styles.ctaCaption,
671
+ children: isBaseline ? "This wipes all app storage and reloads the app." : totals.excludedCount > 0 ? `Restores ${totals.applies} of ${totals.applies + totals.excludedCount} change${totals.applies + totals.excludedCount === 1 ? "" : "s"} — everything else stays as it is.` : "This will replace the current state with the snapshot."
672
+ })]
673
+ })]
674
+ });
675
+ }
676
+ const HIT = {
677
+ top: 10,
678
+ bottom: 10,
679
+ left: 10,
680
+ right: 10
681
+ };
682
+ const styles = StyleSheet.create({
683
+ container: {
684
+ flex: 1
685
+ },
686
+ fallbackHeader: {
687
+ flexDirection: "row",
688
+ alignItems: "center",
689
+ paddingVertical: 8,
690
+ gap: 10
691
+ },
692
+ backText: {
693
+ color: buoyColors.primary,
694
+ fontSize: 14
695
+ },
696
+ screenTitle: {
697
+ color: buoyColors.text,
698
+ fontSize: 13,
699
+ fontWeight: "700",
700
+ flex: 1
701
+ },
702
+ toolbarButton: {
703
+ padding: 4,
704
+ justifyContent: "center",
705
+ alignItems: "center"
706
+ },
707
+ dimmed: {
708
+ opacity: 0.55
709
+ },
710
+ listContent: {
711
+ paddingHorizontal: 12,
712
+ paddingTop: 8,
713
+ paddingBottom: 12
714
+ },
715
+ // Compare card
716
+ compareCard: {
717
+ flexDirection: "row",
718
+ alignItems: "center",
719
+ backgroundColor: buoyColors.card,
720
+ borderColor: buoyColors.border,
721
+ borderWidth: 1,
722
+ borderRadius: 14,
723
+ padding: 14,
724
+ gap: 10
725
+ },
726
+ compareSide: {
727
+ flex: 1,
728
+ minWidth: 0,
729
+ gap: 3
730
+ },
731
+ compareSideRight: {
732
+ alignItems: "flex-end"
733
+ },
734
+ compareTagRow: {
735
+ flexDirection: "row",
736
+ alignItems: "center",
737
+ gap: 5
738
+ },
739
+ stateDot: {
740
+ width: 6,
741
+ height: 6,
742
+ borderRadius: 3
743
+ },
744
+ compareTag: {
745
+ color: buoyColors.textSecondary,
746
+ fontSize: 10,
747
+ fontWeight: "700",
748
+ letterSpacing: 0.6
749
+ },
750
+ compareName: {
751
+ color: buoyColors.text,
752
+ fontSize: 15,
753
+ fontWeight: "700"
754
+ },
755
+ compareNameRight: {
756
+ textAlign: "right"
757
+ },
758
+ compareMeta: {
759
+ color: buoyColors.textMuted,
760
+ fontSize: 10
761
+ },
762
+ compareMetaRight: {
763
+ textAlign: "right"
764
+ },
765
+ compareMetaRow: {
766
+ flexDirection: "row",
767
+ alignItems: "center",
768
+ gap: 6
769
+ },
770
+ swapBadge: {
771
+ width: 34,
772
+ height: 34,
773
+ borderRadius: 17,
774
+ backgroundColor: buoyColors.hover,
775
+ borderColor: buoyColors.border,
776
+ borderWidth: 1,
777
+ alignItems: "center",
778
+ justifyContent: "center"
779
+ },
780
+ swapGlyph: {
781
+ color: buoyColors.textSecondary,
782
+ fontSize: 14
783
+ },
784
+ // Changes row
785
+ changesRow: {
786
+ flexDirection: "row",
787
+ alignItems: "center",
788
+ justifyContent: "space-between",
789
+ marginTop: 14,
790
+ gap: 10
791
+ },
792
+ changesTitle: {
793
+ color: buoyColors.text,
794
+ fontSize: 14,
795
+ fontWeight: "700"
796
+ },
797
+ changesTitleRow: {
798
+ flexDirection: "row",
799
+ alignItems: "baseline",
800
+ gap: 8
801
+ },
802
+ selectLinks: {
803
+ flexDirection: "row",
804
+ alignItems: "baseline",
805
+ gap: 4
806
+ },
807
+ selectLink: {
808
+ color: buoyColors.textMuted,
809
+ fontSize: 12
810
+ },
811
+ selectLinkActive: {
812
+ color: buoyColors.primary,
813
+ fontWeight: "600"
814
+ },
815
+ selectLinkSep: {
816
+ color: buoyColors.textMuted,
817
+ fontSize: 12
818
+ },
819
+ changesChip: {
820
+ backgroundColor: buoyColors.card,
821
+ borderColor: buoyColors.border,
822
+ borderWidth: 1,
823
+ borderRadius: 8,
824
+ paddingHorizontal: 10,
825
+ paddingVertical: 5,
826
+ maxWidth: "60%"
827
+ },
828
+ changesChipText: {
829
+ color: buoyColors.textSecondary,
830
+ fontSize: 11
831
+ },
832
+ wontApplyLine: {
833
+ color: buoyColors.warning,
834
+ fontSize: 11,
835
+ marginTop: 6
836
+ },
837
+ // Search row
838
+ searchRow: {
839
+ flexDirection: "row",
840
+ alignItems: "center",
841
+ gap: 8,
842
+ marginTop: 10,
843
+ marginBottom: 6
844
+ },
845
+ searchField: {
846
+ flex: 1,
847
+ flexDirection: "row",
848
+ alignItems: "center",
849
+ gap: 8,
850
+ backgroundColor: buoyColors.card,
851
+ borderColor: buoyColors.border,
852
+ borderWidth: 1,
853
+ borderRadius: 10,
854
+ paddingHorizontal: 10
855
+ },
856
+ searchInput: {
857
+ flex: 1,
858
+ color: buoyColors.text,
859
+ fontSize: 12,
860
+ paddingVertical: 8
861
+ },
862
+ collapseAllButton: {
863
+ width: 36,
864
+ height: 36,
865
+ borderRadius: 10,
866
+ backgroundColor: buoyColors.card,
867
+ borderColor: buoyColors.border,
868
+ borderWidth: 1,
869
+ alignItems: "center",
870
+ justifyContent: "center"
871
+ },
872
+ // Checkbox
873
+ checkbox: {
874
+ width: 18,
875
+ height: 18,
876
+ borderRadius: 4,
877
+ borderWidth: 1.5,
878
+ borderColor: buoyColors.textMuted,
879
+ alignItems: "center",
880
+ justifyContent: "center"
881
+ },
882
+ checkboxChecked: {
883
+ borderColor: buoyColors.primary,
884
+ backgroundColor: `${buoyColors.primary}26`
885
+ },
886
+ checkboxGlyph: {
887
+ color: buoyColors.primary,
888
+ fontSize: 11,
889
+ fontWeight: "700",
890
+ lineHeight: 13
891
+ },
892
+ // Source cards
893
+ sourceCard: {
894
+ flexDirection: "row",
895
+ alignItems: "center",
896
+ backgroundColor: buoyColors.card,
897
+ borderColor: buoyColors.border,
898
+ borderWidth: 1,
899
+ borderRadius: 12,
900
+ paddingHorizontal: 12,
901
+ paddingVertical: 10,
902
+ marginTop: 8,
903
+ gap: 10
904
+ },
905
+ sourceCardTouch: {
906
+ flex: 1,
907
+ flexDirection: "row",
908
+ alignItems: "center",
909
+ gap: 12,
910
+ minWidth: 0
911
+ },
912
+ sourceIconTile: {
913
+ width: 36,
914
+ height: 36,
915
+ borderRadius: 10,
916
+ backgroundColor: buoyColors.hover,
917
+ alignItems: "center",
918
+ justifyContent: "center"
919
+ },
920
+ sourceNameBlock: {
921
+ flex: 1,
922
+ minWidth: 0,
923
+ gap: 2
924
+ },
925
+ sourceName: {
926
+ color: buoyColors.text,
927
+ fontSize: 15,
928
+ fontWeight: "700"
929
+ },
930
+ sourceSummary: {
931
+ color: buoyColors.textMuted,
932
+ fontSize: 11,
933
+ fontVariant: ["tabular-nums"]
934
+ },
935
+ sourceBadges: {
936
+ flexDirection: "row",
937
+ gap: 8
938
+ },
939
+ sourceBadge: {
940
+ fontSize: 13,
941
+ fontWeight: "700",
942
+ fontVariant: ["tabular-nums"]
943
+ },
944
+ noteRow: {
945
+ color: buoyColors.textMuted,
946
+ fontSize: 10,
947
+ paddingLeft: 24,
948
+ paddingTop: 6
949
+ },
950
+ // Item rows (nested under a source card)
951
+ itemRow: {
952
+ flexDirection: "row",
953
+ alignItems: "center",
954
+ backgroundColor: buoyColors.hover,
955
+ borderColor: buoyColors.border,
956
+ borderWidth: 1,
957
+ borderRadius: 10,
958
+ paddingHorizontal: 10,
959
+ paddingVertical: 9,
960
+ marginTop: 6,
961
+ marginLeft: 14,
962
+ gap: 10
963
+ },
964
+ itemBodyTouch: {
965
+ flex: 1,
966
+ flexDirection: "row",
967
+ alignItems: "center",
968
+ gap: 8,
969
+ minWidth: 0
970
+ },
971
+ itemGlyph: {
972
+ fontSize: 13,
973
+ fontWeight: "700",
974
+ width: 14,
975
+ textAlign: "center"
976
+ },
977
+ itemBody: {
978
+ flex: 1,
979
+ minWidth: 0
980
+ },
981
+ itemLabel: {
982
+ color: buoyColors.text,
983
+ fontSize: 12
984
+ },
985
+ itemPreview: {
986
+ color: buoyColors.textMuted,
987
+ fontSize: 10,
988
+ marginTop: 2,
989
+ fontVariant: ["tabular-nums"]
990
+ },
991
+ itemPreviewRemoved: {
992
+ textDecorationLine: "line-through"
993
+ },
994
+ itemReason: {
995
+ color: buoyColors.warning,
996
+ fontSize: 10,
997
+ marginTop: 2
998
+ },
999
+ itemExcluded: {
1000
+ color: buoyColors.textMuted,
1001
+ fontSize: 10,
1002
+ marginTop: 2,
1003
+ fontStyle: "italic"
1004
+ },
1005
+ itemChevron: {
1006
+ color: buoyColors.textMuted,
1007
+ fontSize: 16
1008
+ },
1009
+ emptyText: {
1010
+ color: buoyColors.textMuted,
1011
+ fontSize: 12,
1012
+ textAlign: "center",
1013
+ marginTop: 24
1014
+ },
1015
+ // Footer CTA
1016
+ footer: {
1017
+ paddingHorizontal: 12,
1018
+ paddingTop: 8,
1019
+ paddingBottom: 12,
1020
+ gap: 6,
1021
+ borderTopColor: buoyColors.border,
1022
+ borderTopWidth: 1,
1023
+ backgroundColor: buoyColors.base
1024
+ },
1025
+ cta: {
1026
+ flexDirection: "row",
1027
+ alignItems: "center",
1028
+ justifyContent: "center",
1029
+ gap: 8,
1030
+ backgroundColor: buoyColors.primary,
1031
+ borderRadius: 24,
1032
+ paddingVertical: 13
1033
+ },
1034
+ ctaDanger: {
1035
+ backgroundColor: buoyColors.error
1036
+ },
1037
+ ctaDisabled: {
1038
+ opacity: 0.4
1039
+ },
1040
+ ctaText: {
1041
+ color: "#0A0A0A",
1042
+ fontSize: 14,
1043
+ fontWeight: "700"
1044
+ },
1045
+ ctaCaption: {
1046
+ color: buoyColors.textMuted,
1047
+ fontSize: 10,
1048
+ textAlign: "center"
1049
+ }
1050
+ });
1051
+ //# sourceMappingURL=RestorePreviewScreen.js.map