@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.
- package/lib/commonjs/components/TimeMachineBody.js +147 -677
- package/lib/commonjs/components/TimeMachineBody.js.map +1 -1
- package/lib/commonjs/components/TimeMachineModal.js +10 -11
- package/lib/commonjs/components/TimeMachineModal.js.map +1 -1
- package/lib/commonjs/components/confirm.js +39 -0
- package/lib/commonjs/components/confirm.js.map +1 -0
- package/lib/commonjs/components/format.js +24 -0
- package/lib/commonjs/components/format.js.map +1 -0
- package/lib/commonjs/components/list/CaptureBar.js +95 -0
- package/lib/commonjs/components/list/CaptureBar.js.map +1 -0
- package/lib/commonjs/components/list/RestoreOutcomeCard.js +162 -0
- package/lib/commonjs/components/list/RestoreOutcomeCard.js.map +1 -0
- package/lib/commonjs/components/list/SnapshotRow.js +141 -0
- package/lib/commonjs/components/list/SnapshotRow.js.map +1 -0
- package/lib/commonjs/components/list/SourcesStrip.js +126 -0
- package/lib/commonjs/components/list/SourcesStrip.js.map +1 -0
- package/lib/commonjs/components/list/UndoDeleteBar.js +81 -0
- package/lib/commonjs/components/list/UndoDeleteBar.js.map +1 -0
- package/lib/commonjs/components/list/WipeAllRow.js +46 -0
- package/lib/commonjs/components/list/WipeAllRow.js.map +1 -0
- package/lib/commonjs/components/preview/ItemDiffPanel.js +77 -0
- package/lib/commonjs/components/preview/ItemDiffPanel.js.map +1 -0
- package/lib/commonjs/components/preview/PreviewFlow.js +51 -0
- package/lib/commonjs/components/preview/PreviewFlow.js.map +1 -0
- package/lib/commonjs/components/preview/RestorePointNameRow.js +83 -0
- package/lib/commonjs/components/preview/RestorePointNameRow.js.map +1 -0
- package/lib/commonjs/components/preview/RestorePreviewScreen.js +549 -635
- package/lib/commonjs/components/preview/RestorePreviewScreen.js.map +1 -1
- package/lib/commonjs/components/preview/verdictMeta.js +8 -8
- package/lib/commonjs/components/preview/verdictMeta.js.map +1 -1
- package/lib/commonjs/components/sources.js +72 -0
- package/lib/commonjs/components/sources.js.map +1 -0
- package/lib/commonjs/store/snapshotVault.js +93 -9
- package/lib/commonjs/store/snapshotVault.js.map +1 -1
- package/lib/commonjs/store/timeMachineStore.js +125 -6
- package/lib/commonjs/store/timeMachineStore.js.map +1 -1
- package/lib/commonjs/sync/timeMachineSyncAdapter.js +41 -0
- package/lib/commonjs/sync/timeMachineSyncAdapter.js.map +1 -1
- package/lib/commonjs/ui/SwipeActionsRow.js +167 -0
- package/lib/commonjs/ui/SwipeActionsRow.js.map +1 -0
- package/lib/commonjs/ui/primitives.js +164 -0
- package/lib/commonjs/ui/primitives.js.map +1 -0
- package/lib/commonjs/ui/tmTheme.js +103 -0
- package/lib/commonjs/ui/tmTheme.js.map +1 -0
- package/lib/module/components/TimeMachineBody.js +150 -680
- package/lib/module/components/TimeMachineBody.js.map +1 -1
- package/lib/module/components/TimeMachineModal.js +11 -12
- package/lib/module/components/TimeMachineModal.js.map +1 -1
- package/lib/module/components/confirm.js +34 -0
- package/lib/module/components/confirm.js.map +1 -0
- package/lib/module/components/format.js +19 -0
- package/lib/module/components/format.js.map +1 -0
- package/lib/module/components/list/CaptureBar.js +90 -0
- package/lib/module/components/list/CaptureBar.js.map +1 -0
- package/lib/module/components/list/RestoreOutcomeCard.js +156 -0
- package/lib/module/components/list/RestoreOutcomeCard.js.map +1 -0
- package/lib/module/components/list/SnapshotRow.js +138 -0
- package/lib/module/components/list/SnapshotRow.js.map +1 -0
- package/lib/module/components/list/SourcesStrip.js +121 -0
- package/lib/module/components/list/SourcesStrip.js.map +1 -0
- package/lib/module/components/list/UndoDeleteBar.js +78 -0
- package/lib/module/components/list/UndoDeleteBar.js.map +1 -0
- package/lib/module/components/list/WipeAllRow.js +41 -0
- package/lib/module/components/list/WipeAllRow.js.map +1 -0
- package/lib/module/components/preview/ItemDiffPanel.js +72 -0
- package/lib/module/components/preview/ItemDiffPanel.js.map +1 -0
- package/lib/module/components/preview/PreviewFlow.js +46 -0
- package/lib/module/components/preview/PreviewFlow.js.map +1 -0
- package/lib/module/components/preview/RestorePointNameRow.js +78 -0
- package/lib/module/components/preview/RestorePointNameRow.js.map +1 -0
- package/lib/module/components/preview/RestorePreviewScreen.js +552 -638
- package/lib/module/components/preview/RestorePreviewScreen.js.map +1 -1
- package/lib/module/components/preview/verdictMeta.js +9 -8
- package/lib/module/components/preview/verdictMeta.js.map +1 -1
- package/lib/module/components/sources.js +66 -0
- package/lib/module/components/sources.js.map +1 -0
- package/lib/module/store/snapshotVault.js +87 -9
- package/lib/module/store/snapshotVault.js.map +1 -1
- package/lib/module/store/timeMachineStore.js +125 -7
- package/lib/module/store/timeMachineStore.js.map +1 -1
- package/lib/module/sync/timeMachineSyncAdapter.js +41 -0
- package/lib/module/sync/timeMachineSyncAdapter.js.map +1 -1
- package/lib/module/ui/SwipeActionsRow.js +162 -0
- package/lib/module/ui/SwipeActionsRow.js.map +1 -0
- package/lib/module/ui/primitives.js +156 -0
- package/lib/module/ui/primitives.js.map +1 -0
- package/lib/module/ui/tmTheme.js +99 -0
- package/lib/module/ui/tmTheme.js.map +1 -0
- package/lib/typescript/components/TimeMachineBody.d.ts.map +1 -1
- package/lib/typescript/components/TimeMachineModal.d.ts.map +1 -1
- package/lib/typescript/components/confirm.d.ts +2 -0
- package/lib/typescript/components/confirm.d.ts.map +1 -0
- package/lib/typescript/components/format.d.ts +4 -0
- package/lib/typescript/components/format.d.ts.map +1 -0
- package/lib/typescript/components/list/CaptureBar.d.ts +6 -0
- package/lib/typescript/components/list/CaptureBar.d.ts.map +1 -0
- package/lib/typescript/components/list/RestoreOutcomeCard.d.ts +11 -0
- package/lib/typescript/components/list/RestoreOutcomeCard.d.ts.map +1 -0
- package/lib/typescript/components/list/SnapshotRow.d.ts +12 -0
- package/lib/typescript/components/list/SnapshotRow.d.ts.map +1 -0
- package/lib/typescript/components/list/SourcesStrip.d.ts +5 -0
- package/lib/typescript/components/list/SourcesStrip.d.ts.map +1 -0
- package/lib/typescript/components/list/UndoDeleteBar.d.ts +8 -0
- package/lib/typescript/components/list/UndoDeleteBar.d.ts.map +1 -0
- package/lib/typescript/components/list/WipeAllRow.d.ts +5 -0
- package/lib/typescript/components/list/WipeAllRow.d.ts.map +1 -0
- package/lib/typescript/components/preview/ItemDiffPanel.d.ts +5 -0
- package/lib/typescript/components/preview/ItemDiffPanel.d.ts.map +1 -0
- package/lib/typescript/components/preview/PreviewFlow.d.ts +9 -0
- package/lib/typescript/components/preview/PreviewFlow.d.ts.map +1 -0
- package/lib/typescript/components/preview/RestorePointNameRow.d.ts +5 -0
- package/lib/typescript/components/preview/RestorePointNameRow.d.ts.map +1 -0
- package/lib/typescript/components/preview/RestorePreviewScreen.d.ts +1 -5
- package/lib/typescript/components/preview/RestorePreviewScreen.d.ts.map +1 -1
- package/lib/typescript/components/preview/verdictMeta.d.ts +5 -0
- package/lib/typescript/components/preview/verdictMeta.d.ts.map +1 -1
- package/lib/typescript/components/sources.d.ts +36 -0
- package/lib/typescript/components/sources.d.ts.map +1 -0
- package/lib/typescript/store/snapshotVault.d.ts +48 -0
- package/lib/typescript/store/snapshotVault.d.ts.map +1 -1
- package/lib/typescript/store/timeMachineStore.d.ts +35 -1
- package/lib/typescript/store/timeMachineStore.d.ts.map +1 -1
- package/lib/typescript/sync/timeMachineSyncAdapter.d.ts +17 -0
- package/lib/typescript/sync/timeMachineSyncAdapter.d.ts.map +1 -1
- package/lib/typescript/ui/SwipeActionsRow.d.ts +32 -0
- package/lib/typescript/ui/SwipeActionsRow.d.ts.map +1 -0
- package/lib/typescript/ui/primitives.d.ts +57 -0
- package/lib/typescript/ui/primitives.d.ts.map +1 -0
- package/lib/typescript/ui/tmTheme.d.ts +67 -0
- package/lib/typescript/ui/tmTheme.d.ts.map +1 -0
- package/package.json +7 -7
- package/src/components/TimeMachineBody.tsx +183 -817
- package/src/components/TimeMachineModal.tsx +10 -11
- package/src/components/confirm.ts +35 -0
- package/src/components/format.ts +17 -0
- package/src/components/list/CaptureBar.tsx +100 -0
- package/src/components/list/RestoreOutcomeCard.tsx +159 -0
- package/src/components/list/SnapshotRow.tsx +155 -0
- package/src/components/list/SourcesStrip.tsx +124 -0
- package/src/components/list/UndoDeleteBar.tsx +76 -0
- package/src/components/list/WipeAllRow.tsx +42 -0
- package/src/components/preview/ItemDiffPanel.tsx +77 -0
- package/src/components/preview/PreviewFlow.tsx +70 -0
- package/src/components/preview/RestorePointNameRow.tsx +80 -0
- package/src/components/preview/RestorePreviewScreen.tsx +535 -535
- package/src/components/preview/verdictMeta.ts +8 -8
- package/src/components/sources.ts +92 -0
- package/src/store/snapshotVault.ts +131 -9
- package/src/store/timeMachineStore.ts +136 -5
- package/src/sync/timeMachineSyncAdapter.ts +30 -0
- package/src/ui/SwipeActionsRow.tsx +199 -0
- package/src/ui/primitives.tsx +157 -0
- package/src/ui/tmTheme.ts +107 -0
- package/lib/commonjs/components/preview/ItemDetailScreen.js +0 -189
- package/lib/commonjs/components/preview/ItemDetailScreen.js.map +0 -1
- package/lib/module/components/preview/ItemDetailScreen.js +0 -184
- package/lib/module/components/preview/ItemDetailScreen.js.map +0 -1
- package/lib/typescript/components/preview/ItemDetailScreen.d.ts +0 -11
- package/lib/typescript/components/preview/ItemDetailScreen.d.ts.map +0 -1
- package/src/components/preview/ItemDetailScreen.tsx +0 -172
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Restore Preview — "what will restoring this snapshot actually change?"
|
|
3
3
|
*
|
|
4
|
-
* ONE virtualized FlatList is the scrollable area (
|
|
5
|
-
* source
|
|
6
|
-
* footer — no nested scroll regions, thousands of
|
|
4
|
+
* ONE virtualized FlatList is the scrollable area (the section header, the
|
|
5
|
+
* source rows, their item rows and the inline diffs are all list rows) with
|
|
6
|
+
* a sticky Restore CTA footer — no nested scroll regions, so thousands of
|
|
7
|
+
* storage keys stay cheap.
|
|
7
8
|
*
|
|
8
9
|
* Layout follows the Time Machine mockup: source sections are card
|
|
9
10
|
* dropdowns with tool-icon tiles, a "N keys · M changed" detection subtitle,
|
|
@@ -13,56 +14,45 @@
|
|
|
13
14
|
* INCLUDE checkbox (source headers toggle their whole section) — unchecked
|
|
14
15
|
* items are passed to restore as exclusions and left untouched.
|
|
15
16
|
*/
|
|
16
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
memo,
|
|
19
|
+
useCallback,
|
|
20
|
+
useEffect,
|
|
21
|
+
useMemo,
|
|
22
|
+
useRef,
|
|
23
|
+
useState,
|
|
24
|
+
type ReactNode,
|
|
25
|
+
} from "react";
|
|
17
26
|
import {
|
|
18
27
|
ActivityIndicator,
|
|
19
28
|
FlatList,
|
|
20
29
|
StyleSheet,
|
|
21
30
|
Text,
|
|
22
|
-
TextInput,
|
|
23
31
|
TouchableOpacity,
|
|
24
32
|
View,
|
|
25
33
|
type ListRenderItem,
|
|
26
34
|
} from "react-native";
|
|
27
35
|
import {
|
|
28
|
-
ChevronDown,
|
|
29
36
|
ChevronUp,
|
|
30
|
-
|
|
37
|
+
AlertTriangle,
|
|
31
38
|
RefreshCw,
|
|
32
|
-
|
|
33
|
-
buoyColors,
|
|
34
|
-
macOSColors,
|
|
39
|
+
useSafeAreaInsets,
|
|
35
40
|
} from "@buoy-gg/shared-ui";
|
|
36
|
-
import {
|
|
37
|
-
JotaiIcon,
|
|
38
|
-
QueryIcon,
|
|
39
|
-
ReduxIcon,
|
|
40
|
-
StorageIcon,
|
|
41
|
-
TimeMachineIcon,
|
|
42
|
-
ZustandIcon,
|
|
43
|
-
} from "@buoy-gg/floating-tools-core";
|
|
44
41
|
import type { StateSnapshotPreviewItem } from "@buoy-gg/shared-ui";
|
|
45
42
|
import { useToolHeader, useToolHost } from "@buoy-gg/core";
|
|
43
|
+
import { DisclosureChevron, Group, SectionLabel } from "../../ui/primitives";
|
|
44
|
+
import { TM_MONO, tm } from "../../ui/tmTheme";
|
|
46
45
|
import type {
|
|
47
46
|
PreviewSideRef,
|
|
48
47
|
PreviewSource,
|
|
49
48
|
PreviewState,
|
|
50
49
|
} from "../../store/timeMachineStore";
|
|
51
|
-
import { timeMachineStore } from "../../store/timeMachineStore";
|
|
50
|
+
import { timeMachineStore, willRestoreRoute } from "../../store/timeMachineStore";
|
|
51
|
+
import { ItemDiffPanel } from "./ItemDiffPanel";
|
|
52
|
+
import { RestorePointNameRow } from "./RestorePointNameRow";
|
|
52
53
|
import type { SnapshotMeta } from "../../store/snapshotVault";
|
|
53
54
|
import { VERDICT_META } from "./verdictMeta";
|
|
54
55
|
|
|
55
|
-
const SOURCE_ICONS: Record<
|
|
56
|
-
string,
|
|
57
|
-
React.ComponentType<{ size?: number; color?: string }>
|
|
58
|
-
> = {
|
|
59
|
-
storage: StorageIcon,
|
|
60
|
-
redux: ReduxIcon,
|
|
61
|
-
zustand: ZustandIcon,
|
|
62
|
-
jotai: JotaiIcon,
|
|
63
|
-
query: QueryIcon,
|
|
64
|
-
};
|
|
65
|
-
|
|
66
56
|
const SOURCE_NOUNS: Record<string, string> = {
|
|
67
57
|
storage: "keys",
|
|
68
58
|
redux: "slices",
|
|
@@ -71,7 +61,18 @@ const SOURCE_NOUNS: Record<string, string> = {
|
|
|
71
61
|
query: "queries",
|
|
72
62
|
};
|
|
73
63
|
|
|
74
|
-
|
|
64
|
+
/**
|
|
65
|
+
* Where a row sits in the one card that holds the whole list. The list has
|
|
66
|
+
* to stay flat to stay virtualized, so each row paints its own share of the
|
|
67
|
+
* card: the first row rounds its top, the last rounds its bottom, and every
|
|
68
|
+
* other row draws the hairline above it.
|
|
69
|
+
*/
|
|
70
|
+
interface CardPosition {
|
|
71
|
+
first: boolean;
|
|
72
|
+
last: boolean;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface RowSourceHeader extends CardPosition {
|
|
75
76
|
kind: "source";
|
|
76
77
|
source: PreviewSource;
|
|
77
78
|
collapsed: boolean;
|
|
@@ -83,18 +84,43 @@ interface RowSourceHeader {
|
|
|
83
84
|
/** "60 keys · 7 changed" — detection proof without expanding. */
|
|
84
85
|
summary: string;
|
|
85
86
|
}
|
|
86
|
-
interface RowItem {
|
|
87
|
+
interface RowItem extends CardPosition {
|
|
87
88
|
kind: "item";
|
|
88
89
|
sourceId: string;
|
|
89
90
|
item: StateSnapshotPreviewItem;
|
|
90
91
|
excluded: boolean;
|
|
91
92
|
}
|
|
92
|
-
|
|
93
|
+
/**
|
|
94
|
+
* The route leg, as a row in the same card as the sources. It is not a
|
|
95
|
+
* source — nothing diffs it — but "will this also take me back to
|
|
96
|
+
* /checkout?" is a decision about THIS restore, so it belongs beside the
|
|
97
|
+
* other things the restore is about to do rather than on the list screen.
|
|
98
|
+
*/
|
|
99
|
+
interface RowRoute extends CardPosition {
|
|
100
|
+
kind: "route";
|
|
101
|
+
href: string;
|
|
102
|
+
on: boolean;
|
|
103
|
+
}
|
|
104
|
+
interface RowDiff extends CardPosition {
|
|
105
|
+
kind: "diff";
|
|
106
|
+
sourceId: string;
|
|
107
|
+
item: StateSnapshotPreviewItem;
|
|
108
|
+
}
|
|
109
|
+
interface RowNote extends CardPosition {
|
|
93
110
|
kind: "note";
|
|
94
111
|
id: string;
|
|
95
112
|
text: string;
|
|
96
113
|
}
|
|
97
|
-
type Row = RowSourceHeader | RowItem | RowNote;
|
|
114
|
+
type Row = RowSourceHeader | RowItem | RowNote | RowDiff | RowRoute;
|
|
115
|
+
|
|
116
|
+
/** Shared empty selection — a stable identity for "nothing is excluded". */
|
|
117
|
+
const EMPTY_SELECTION: ReadonlySet<string> = new Set<string>();
|
|
118
|
+
|
|
119
|
+
function sameSelection(a: ReadonlySet<string>, b: ReadonlySet<string>): boolean {
|
|
120
|
+
if (a.size !== b.size) return false;
|
|
121
|
+
for (const key of a) if (!b.has(key)) return false;
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
98
124
|
|
|
99
125
|
/** Compound exclusion key — what timeMachineStore.restore expects. */
|
|
100
126
|
function cKey(sourceId: string, itemKey: string): string {
|
|
@@ -112,39 +138,72 @@ function shortPreview(value: unknown): string {
|
|
|
112
138
|
}
|
|
113
139
|
}
|
|
114
140
|
|
|
115
|
-
|
|
116
|
-
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* The grouped-card shell one row paints. Rounds the ends, insets the card
|
|
145
|
+
* from the sheet, and draws the hairline that separates it from the row
|
|
146
|
+
* above — the same shape the home list's <Group> makes, rebuilt per row so
|
|
147
|
+
* the list stays virtualized.
|
|
148
|
+
*/
|
|
149
|
+
function CardRow({
|
|
150
|
+
position,
|
|
151
|
+
children,
|
|
152
|
+
}: {
|
|
153
|
+
position: CardPosition;
|
|
154
|
+
children: ReactNode;
|
|
155
|
+
}) {
|
|
117
156
|
return (
|
|
118
|
-
|
|
157
|
+
<View
|
|
158
|
+
style={[
|
|
159
|
+
styles.cardRow,
|
|
160
|
+
position.first ? styles.cardRowFirst : null,
|
|
161
|
+
position.last ? styles.cardRowLast : null,
|
|
162
|
+
]}
|
|
163
|
+
>
|
|
164
|
+
{position.first ? null : <View style={styles.cardSeparator} />}
|
|
165
|
+
{children}
|
|
166
|
+
</View>
|
|
119
167
|
);
|
|
120
168
|
}
|
|
121
169
|
|
|
122
|
-
function
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
170
|
+
function CountPill({
|
|
171
|
+
glyph,
|
|
172
|
+
count,
|
|
173
|
+
color,
|
|
174
|
+
}: {
|
|
175
|
+
glyph: string;
|
|
176
|
+
count: number;
|
|
177
|
+
color: string;
|
|
178
|
+
}) {
|
|
179
|
+
return (
|
|
180
|
+
<View style={[styles.countPill, { backgroundColor: `${color}26` }]}>
|
|
181
|
+
<Text style={[styles.countPillText, { color }]}>
|
|
182
|
+
{glyph}
|
|
183
|
+
{count}
|
|
184
|
+
</Text>
|
|
185
|
+
</View>
|
|
186
|
+
);
|
|
129
187
|
}
|
|
130
188
|
|
|
131
189
|
function Checkbox({ state }: { state: "all" | "some" | "none" }) {
|
|
132
190
|
return (
|
|
133
|
-
<View
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
{state === "some" && <Text style={styles.checkboxGlyph}>−</Text>}
|
|
191
|
+
<View style={[styles.checkbox, state !== "none" && styles.checkboxChecked]}>
|
|
192
|
+
{state === "none" ? null : (
|
|
193
|
+
<Text style={styles.checkboxGlyph}>{state === "all" ? "✓" : "−"}</Text>
|
|
194
|
+
)}
|
|
138
195
|
</View>
|
|
139
196
|
);
|
|
140
197
|
}
|
|
141
198
|
|
|
142
199
|
const ItemRow = memo(function ItemRow({
|
|
143
200
|
row,
|
|
201
|
+
expanded,
|
|
144
202
|
onOpen,
|
|
145
203
|
onToggle,
|
|
146
204
|
}: {
|
|
147
205
|
row: RowItem;
|
|
206
|
+
expanded: boolean;
|
|
148
207
|
/** Zero-arg by design — press plumbing may invoke handlers with a
|
|
149
208
|
* synthetic event; the row is bound at the call site. */
|
|
150
209
|
onOpen: () => void;
|
|
@@ -157,16 +216,16 @@ const ItemRow = memo(function ItemRow({
|
|
|
157
216
|
row.item.verdict === "added" ||
|
|
158
217
|
row.item.verdict === "removed";
|
|
159
218
|
return (
|
|
160
|
-
<
|
|
161
|
-
{
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
219
|
+
<CardRow position={row}>
|
|
220
|
+
<View style={[styles.itemRow, dimmed && styles.dimmed]}>
|
|
221
|
+
<TouchableOpacity
|
|
222
|
+
onPress={onToggle}
|
|
223
|
+
disabled={!meta.applies}
|
|
224
|
+
hitSlop={{ top: 10, bottom: 10, left: 8, right: 8 }}
|
|
225
|
+
testID={`tm-preview-toggle-${row.sourceId}-${row.item.key}`}
|
|
226
|
+
>
|
|
227
|
+
<Checkbox state={!meta.applies || row.excluded ? "none" : "all"} />
|
|
228
|
+
</TouchableOpacity>
|
|
170
229
|
<TouchableOpacity
|
|
171
230
|
style={styles.itemBodyTouch}
|
|
172
231
|
onPress={() => openable && onOpen()}
|
|
@@ -211,9 +270,16 @@ const ItemRow = memo(function ItemRow({
|
|
|
211
270
|
)
|
|
212
271
|
)}
|
|
213
272
|
</View>
|
|
214
|
-
{openable
|
|
273
|
+
{openable ? (
|
|
274
|
+
expanded ? (
|
|
275
|
+
<ChevronUp size={16} color={tm.color.labelTertiary} />
|
|
276
|
+
) : (
|
|
277
|
+
<DisclosureChevron />
|
|
278
|
+
)
|
|
279
|
+
) : null}
|
|
215
280
|
</TouchableOpacity>
|
|
216
|
-
|
|
281
|
+
</View>
|
|
282
|
+
</CardRow>
|
|
217
283
|
);
|
|
218
284
|
});
|
|
219
285
|
|
|
@@ -222,10 +288,7 @@ export interface RestorePreviewScreenProps {
|
|
|
222
288
|
snapshots: SnapshotMeta[];
|
|
223
289
|
busy: boolean;
|
|
224
290
|
onBack: () => void;
|
|
225
|
-
onOpenItem: (sourceId: string, item: StateSnapshotPreviewItem) => void;
|
|
226
291
|
onRestore: (mode: "live" | "reload", excludeKeys: string[]) => void;
|
|
227
|
-
onPickSide: (position: "left" | "right") => void;
|
|
228
|
-
onRefresh: () => void;
|
|
229
292
|
}
|
|
230
293
|
|
|
231
294
|
export function RestorePreviewScreen({
|
|
@@ -233,18 +296,32 @@ export function RestorePreviewScreen({
|
|
|
233
296
|
snapshots,
|
|
234
297
|
busy,
|
|
235
298
|
onBack,
|
|
236
|
-
onOpenItem,
|
|
237
299
|
onRestore,
|
|
238
|
-
onPickSide,
|
|
239
|
-
onRefresh,
|
|
240
300
|
}: RestorePreviewScreenProps) {
|
|
241
301
|
const host = useToolHost();
|
|
242
302
|
const hosted = host.instanceId !== undefined;
|
|
243
|
-
|
|
303
|
+
// `minBottom` is a floor, not a preference: the dimension map can't know
|
|
304
|
+
// about a device that ships next year, and an unmapped one falls through to
|
|
305
|
+
// `bottom: 0`. This keeps the CTA off the home indicator either way.
|
|
306
|
+
const insets = useSafeAreaInsets({ minBottom: 16 });
|
|
307
|
+
|
|
244
308
|
const [collapsed, setCollapsed] = useState<Record<string, boolean>>({});
|
|
245
|
-
const [allCollapsed, setAllCollapsed] = useState(false);
|
|
246
309
|
const [excluded, setExcluded] = useState<ReadonlySet<string>>(new Set());
|
|
247
|
-
|
|
310
|
+
/** Set by every checkbox interaction — see the auto-save effect below. */
|
|
311
|
+
const userEditedRef = useRef(false);
|
|
312
|
+
/** Item rows whose diff is expanded inline, by compound key. */
|
|
313
|
+
const [openDiffs, setOpenDiffs] = useState<ReadonlySet<string>>(EMPTY_SELECTION);
|
|
314
|
+
const toggleDiff = useCallback((key: string) => {
|
|
315
|
+
setOpenDiffs((prev) => {
|
|
316
|
+
const next = new Set(prev);
|
|
317
|
+
if (next.has(key)) next.delete(key);
|
|
318
|
+
else next.add(key);
|
|
319
|
+
return next;
|
|
320
|
+
});
|
|
321
|
+
}, []);
|
|
322
|
+
// Filtering lives in the shared modal header (the magnifier), same as the
|
|
323
|
+
// restore-point list and every other Buoy tool — one place to look for it.
|
|
324
|
+
const search = host.search;
|
|
248
325
|
|
|
249
326
|
const rightMeta =
|
|
250
327
|
preview.right === "current"
|
|
@@ -252,30 +329,42 @@ export function RestorePreviewScreen({
|
|
|
252
329
|
: snapshots.find(
|
|
253
330
|
(s) => s.id === (preview.right as { snapshotId: string }).snapshotId
|
|
254
331
|
);
|
|
332
|
+
// Latest-ref: `renderRow` is memoized on its handlers, so this one has to
|
|
333
|
+
// keep a stable identity while still reading the current snapshot.
|
|
334
|
+
const rightMetaRef = useRef(rightMeta);
|
|
335
|
+
rightMetaRef.current = rightMeta;
|
|
336
|
+
|
|
337
|
+
/** Flip whether this restore also navigates back to the captured route. */
|
|
338
|
+
const toggleRoute = useCallback(() => {
|
|
339
|
+
if (!rightMetaRef.current) return;
|
|
340
|
+
const meta = rightMetaRef.current;
|
|
341
|
+
void timeMachineStore.setRestoreRoute(meta.id, !willRestoreRoute(meta));
|
|
342
|
+
}, []);
|
|
343
|
+
|
|
255
344
|
const isBaseline = rightMeta?.baseline === true;
|
|
256
345
|
const liveLeft = preview.left === "current";
|
|
257
346
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
setExcluded(next);
|
|
269
|
-
}, [preview.sources]);
|
|
347
|
+
/**
|
|
348
|
+
* The selection this restore point already remembers, as exclusions. Null
|
|
349
|
+
* until there is a snapshot on the right-hand side to have one.
|
|
350
|
+
*/
|
|
351
|
+
const savedExcluded = useMemo<ReadonlySet<string> | null>(() => {
|
|
352
|
+
if (!rightMeta) return null;
|
|
353
|
+
return rightMeta.excludedKeys?.length
|
|
354
|
+
? new Set(rightMeta.excludedKeys)
|
|
355
|
+
: EMPTY_SELECTION;
|
|
356
|
+
}, [rightMeta]);
|
|
270
357
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
358
|
+
/**
|
|
359
|
+
* A snapshot still carrying the older `scope` include-list ("only ever
|
|
360
|
+
* touch these", set via MCP). It seeds the checkboxes and is converted to
|
|
361
|
+
* exclusions on read — a restore of it would otherwise still apply include
|
|
362
|
+
* semantics, and the two forms would disagree about what one tap does.
|
|
363
|
+
*/
|
|
364
|
+
const legacyScoped =
|
|
365
|
+
!!rightMeta?.scope?.length && !rightMeta?.excludedKeys?.length;
|
|
366
|
+
const legacySeed = useMemo<ReadonlySet<string>>(() => {
|
|
367
|
+
if (!legacyScoped || !rightMeta?.scope) return EMPTY_SELECTION;
|
|
279
368
|
const scopeSet = new Set(rightMeta.scope);
|
|
280
369
|
const next = new Set<string>();
|
|
281
370
|
for (const src of preview.sources) {
|
|
@@ -286,34 +375,49 @@ export function RestorePreviewScreen({
|
|
|
286
375
|
}
|
|
287
376
|
}
|
|
288
377
|
}
|
|
289
|
-
|
|
290
|
-
}, [
|
|
378
|
+
return next;
|
|
379
|
+
}, [legacyScoped, rightMeta, preview.sources]);
|
|
291
380
|
|
|
292
|
-
//
|
|
293
|
-
//
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
if (!rightMeta) return;
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
381
|
+
// Open with the saved selection already applied — once per snapshot, and
|
|
382
|
+
// only once there is a diff to apply it to.
|
|
383
|
+
const seededFor = useRef<string | null>(null);
|
|
384
|
+
useEffect(() => {
|
|
385
|
+
if (!rightMeta || preview.building || !savedExcluded) return;
|
|
386
|
+
if (seededFor.current === rightMeta.id) return;
|
|
387
|
+
seededFor.current = rightMeta.id;
|
|
388
|
+
userEditedRef.current = false;
|
|
389
|
+
const seed = savedExcluded.size > 0 ? savedExcluded : legacySeed;
|
|
390
|
+
if (seed.size > 0) setExcluded(new Set(seed));
|
|
391
|
+
// Convert an older include-list scope the moment it's read, so the two
|
|
392
|
+
// forms never disagree about what a restore will do.
|
|
393
|
+
if (legacyScoped && seed.size > 0) {
|
|
394
|
+
void timeMachineStore.setExclusions(rightMeta.id, Array.from(seed));
|
|
305
395
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
396
|
+
}, [rightMeta, preview.building, savedExcluded, legacySeed, legacyScoped]);
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Auto-save. The checkboxes ARE the restore point's setting, so there is
|
|
400
|
+
* nothing to confirm — every edit persists as EXCLUSIONS (the items to
|
|
401
|
+
* leave alone), and every later restore of this point honors them, one-tap
|
|
402
|
+
* RESTORE and MCP included.
|
|
403
|
+
*
|
|
404
|
+
* Never an include list: unchecking one of ten keys must keep restoring the
|
|
405
|
+
* other nine AND anything that only starts differing tomorrow, which a list
|
|
406
|
+
* built from today's diff cannot do.
|
|
407
|
+
*
|
|
408
|
+
* Gated on a ref rather than on comparing to what's stored, because on the
|
|
409
|
+
* frames before the seed lands `excluded` is legitimately empty — writing
|
|
410
|
+
* that would erase the saved selection we are about to load.
|
|
411
|
+
*/
|
|
412
|
+
useEffect(() => {
|
|
413
|
+
if (!rightMeta || !savedExcluded || !userEditedRef.current) return;
|
|
414
|
+
if (sameSelection(excluded, savedExcluded)) return;
|
|
415
|
+
const keys = Array.from(excluded);
|
|
416
|
+
void timeMachineStore.setExclusions(rightMeta.id, keys.length ? keys : null);
|
|
417
|
+
}, [excluded, rightMeta, savedExcluded]);
|
|
315
418
|
|
|
316
419
|
const toggleItem = useCallback((key: string) => {
|
|
420
|
+
userEditedRef.current = true;
|
|
317
421
|
setExcluded((prev) => {
|
|
318
422
|
const next = new Set(prev);
|
|
319
423
|
if (next.has(key)) next.delete(key);
|
|
@@ -324,6 +428,7 @@ export function RestorePreviewScreen({
|
|
|
324
428
|
|
|
325
429
|
const toggleSource = useCallback(
|
|
326
430
|
(source: PreviewSource, includeState: "all" | "some" | "none") => {
|
|
431
|
+
userEditedRef.current = true;
|
|
327
432
|
setExcluded((prev) => {
|
|
328
433
|
const next = new Set(prev);
|
|
329
434
|
for (const item of source.items) {
|
|
@@ -345,16 +450,6 @@ export function RestorePreviewScreen({
|
|
|
345
450
|
setCollapsed((c) => ({ ...c, [id]: !currentlyCollapsed }));
|
|
346
451
|
}, []);
|
|
347
452
|
|
|
348
|
-
const toggleAllCollapsed = useCallback(() => {
|
|
349
|
-
setAllCollapsed((prev) => {
|
|
350
|
-
const next = !prev;
|
|
351
|
-
setCollapsed(
|
|
352
|
-
Object.fromEntries(preview.sources.map((s) => [s.id, next]))
|
|
353
|
-
);
|
|
354
|
-
return next;
|
|
355
|
-
});
|
|
356
|
-
}, [preview.sources]);
|
|
357
|
-
|
|
358
453
|
const totals = useMemo(() => {
|
|
359
454
|
let applies = 0;
|
|
360
455
|
let excludedCount = 0;
|
|
@@ -383,23 +478,27 @@ export function RestorePreviewScreen({
|
|
|
383
478
|
hosted
|
|
384
479
|
? {
|
|
385
480
|
title: "Restore Preview",
|
|
386
|
-
subtitle: rightMeta?.name ?? "
|
|
481
|
+
subtitle: rightMeta?.name ?? "preview",
|
|
387
482
|
onBack,
|
|
388
|
-
searchable: false, // the in-body "Filter keys…" field covers it
|
|
389
483
|
}
|
|
390
484
|
: null,
|
|
391
485
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
392
486
|
[hosted, rightMeta?.name]
|
|
393
487
|
);
|
|
394
488
|
|
|
395
|
-
const rows = useMemo
|
|
489
|
+
const { rows, blockedSources } = useMemo(() => {
|
|
396
490
|
const q = search.trim().toLowerCase();
|
|
397
491
|
const out: Row[] = [];
|
|
492
|
+
// Sources the restore would not touch at all get no card: "no changes"
|
|
493
|
+
// rows are noise on a screen about what WILL change.
|
|
494
|
+
/** Sources holding a won't-apply item, so the note can name where to look. */
|
|
495
|
+
const blocked: string[] = [];
|
|
398
496
|
for (const src of preview.sources) {
|
|
399
497
|
// Every detected source gets a card whose subtitle carries the
|
|
400
498
|
// detection proof ("60 keys · 7 changed" / "2 slices · no changes"),
|
|
401
499
|
// so clean sources need no expansion. Unchanged ITEMS never render.
|
|
402
500
|
const relevant = src.items.some((i) => i.verdict !== "unchanged");
|
|
501
|
+
if (!relevant && !src.error) continue;
|
|
403
502
|
const expandable = relevant || src.notes.length > 0 || !!src.error;
|
|
404
503
|
|
|
405
504
|
const applicable = src.items.filter(
|
|
@@ -408,6 +507,7 @@ export function RestorePreviewScreen({
|
|
|
408
507
|
const wontApplyCount = src.items.filter(
|
|
409
508
|
(i) => i.verdict === "wont-apply"
|
|
410
509
|
).length;
|
|
510
|
+
if (wontApplyCount > 0) blocked.push(src.label);
|
|
411
511
|
const noun =
|
|
412
512
|
src.items.length === 1
|
|
413
513
|
? (SOURCE_NOUNS[src.id] ?? "items").replace(/ies$/, "y").replace(/s$/, "")
|
|
@@ -444,9 +544,10 @@ export function RestorePreviewScreen({
|
|
|
444
544
|
if (q && !item.label.toLowerCase().includes(q)) return false;
|
|
445
545
|
return true;
|
|
446
546
|
});
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
547
|
+
// Everything starts collapsed. The screen's job on open is "which
|
|
548
|
+
// sources would this touch, and how much" — expanding a source with
|
|
549
|
+
// 58 keys before you asked buries that under its own contents.
|
|
550
|
+
const isCollapsed = expandable ? (collapsed[src.id] ?? true) : true;
|
|
450
551
|
out.push({
|
|
451
552
|
kind: "source",
|
|
452
553
|
source: src,
|
|
@@ -456,55 +557,97 @@ export function RestorePreviewScreen({
|
|
|
456
557
|
counts,
|
|
457
558
|
expandable,
|
|
458
559
|
summary,
|
|
560
|
+
first: false,
|
|
561
|
+
last: false,
|
|
459
562
|
});
|
|
460
563
|
if (!isCollapsed) {
|
|
461
564
|
for (const note of src.notes) {
|
|
462
|
-
out.push({
|
|
565
|
+
out.push({
|
|
566
|
+
kind: "note",
|
|
567
|
+
id: `${src.id}:${note}`,
|
|
568
|
+
text: note,
|
|
569
|
+
first: false,
|
|
570
|
+
last: false,
|
|
571
|
+
});
|
|
463
572
|
}
|
|
464
573
|
if (src.error) {
|
|
465
|
-
out.push({
|
|
574
|
+
out.push({
|
|
575
|
+
kind: "note",
|
|
576
|
+
id: `${src.id}:err`,
|
|
577
|
+
text: `⚠ ${src.error}`,
|
|
578
|
+
first: false,
|
|
579
|
+
last: false,
|
|
580
|
+
});
|
|
466
581
|
}
|
|
467
582
|
for (const item of visible) {
|
|
583
|
+
const key = cKey(src.id, item.key);
|
|
468
584
|
out.push({
|
|
469
585
|
kind: "item",
|
|
470
586
|
sourceId: src.id,
|
|
471
587
|
item,
|
|
472
|
-
excluded: excluded.has(
|
|
588
|
+
excluded: excluded.has(key),
|
|
589
|
+
first: false,
|
|
590
|
+
last: false,
|
|
473
591
|
});
|
|
592
|
+
if (openDiffs.has(key)) {
|
|
593
|
+
out.push({
|
|
594
|
+
kind: "diff",
|
|
595
|
+
sourceId: src.id,
|
|
596
|
+
item,
|
|
597
|
+
first: false,
|
|
598
|
+
last: false,
|
|
599
|
+
});
|
|
600
|
+
}
|
|
474
601
|
}
|
|
475
602
|
}
|
|
476
603
|
}
|
|
477
|
-
|
|
478
|
-
|
|
604
|
+
if (rightMeta?.route) {
|
|
605
|
+
out.push({
|
|
606
|
+
kind: "route",
|
|
607
|
+
href: rightMeta.route.href,
|
|
608
|
+
on: willRestoreRoute(rightMeta),
|
|
609
|
+
first: false,
|
|
610
|
+
last: false,
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
// ONE card holds every source — the ends of the LIST round, and every
|
|
614
|
+
// other boundary is just a hairline. Per-source cards would read as
|
|
615
|
+
// separate groups, which they aren't: this is one list of what a restore
|
|
616
|
+
// would touch.
|
|
617
|
+
if (out.length > 0) {
|
|
618
|
+
out[0].first = true;
|
|
619
|
+
out[out.length - 1].last = true;
|
|
620
|
+
}
|
|
621
|
+
return { rows: out, blockedSources: blocked };
|
|
622
|
+
}, [preview.sources, search, collapsed, excluded, openDiffs, rightMeta]);
|
|
479
623
|
|
|
480
624
|
const renderRow: ListRenderItem<Row> = useCallback(
|
|
481
625
|
({ item: row }) => {
|
|
482
626
|
if (row.kind === "source") {
|
|
483
627
|
const src = row.source;
|
|
484
628
|
const { counts } = row;
|
|
485
|
-
const clean =
|
|
486
|
-
counts.added + counts.removed + counts.changed === 0 && !src.error;
|
|
487
|
-
const IconComp = SOURCE_ICONS[src.id] ?? TimeMachineIcon;
|
|
488
629
|
return (
|
|
630
|
+
<CardRow position={row}>
|
|
489
631
|
<View style={styles.sourceCard}>
|
|
490
|
-
{
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
632
|
+
{/* Always rendered, even when there is nothing to select: it is
|
|
633
|
+
what every row lines up against now that the icons are gone,
|
|
634
|
+
and an empty ring reads as "nothing selected here". */}
|
|
635
|
+
<TouchableOpacity
|
|
636
|
+
onPress={() => toggleSource(src, row.includeState)}
|
|
637
|
+
disabled={row.applicableCount === 0}
|
|
638
|
+
hitSlop={{ top: 10, bottom: 10, left: 8, right: 8 }}
|
|
639
|
+
testID={`tm-preview-source-toggle-${src.id}`}
|
|
640
|
+
>
|
|
641
|
+
<Checkbox
|
|
642
|
+
state={row.applicableCount === 0 ? "none" : row.includeState}
|
|
643
|
+
/>
|
|
644
|
+
</TouchableOpacity>
|
|
499
645
|
<TouchableOpacity
|
|
500
646
|
style={styles.sourceCardTouch}
|
|
501
647
|
onPress={() => row.expandable && toggleCollapse(src.id, row.collapsed)}
|
|
502
648
|
disabled={!row.expandable}
|
|
503
649
|
testID={`tm-preview-source-${src.id}`}
|
|
504
650
|
>
|
|
505
|
-
<View style={styles.sourceIconTile}>
|
|
506
|
-
<IconComp size={18} />
|
|
507
|
-
</View>
|
|
508
651
|
<View style={styles.sourceNameBlock}>
|
|
509
652
|
<Text style={styles.sourceName} numberOfLines={1}>
|
|
510
653
|
{src.label}
|
|
@@ -515,53 +658,82 @@ export function RestorePreviewScreen({
|
|
|
515
658
|
: row.summary}
|
|
516
659
|
</Text>
|
|
517
660
|
</View>
|
|
518
|
-
{
|
|
519
|
-
|
|
520
|
-
{counts.removed
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
)}
|
|
530
|
-
{counts.changed > 0 && (
|
|
531
|
-
<Text style={[styles.sourceBadge, { color: buoyColors.info }]}>
|
|
532
|
-
≈{counts.changed}
|
|
533
|
-
</Text>
|
|
534
|
-
)}
|
|
535
|
-
</View>
|
|
536
|
-
)}
|
|
661
|
+
<View style={styles.sourceBadges}>
|
|
662
|
+
{counts.removed > 0 ? (
|
|
663
|
+
<CountPill glyph="−" count={counts.removed} color={tm.color.danger} />
|
|
664
|
+
) : null}
|
|
665
|
+
{counts.added > 0 ? (
|
|
666
|
+
<CountPill glyph="+" count={counts.added} color={tm.color.accent} />
|
|
667
|
+
) : null}
|
|
668
|
+
{counts.changed > 0 ? (
|
|
669
|
+
<CountPill glyph="≈" count={counts.changed} color={tm.color.info} />
|
|
670
|
+
) : null}
|
|
671
|
+
</View>
|
|
537
672
|
{row.expandable ? (
|
|
538
673
|
row.collapsed ? (
|
|
539
|
-
<
|
|
674
|
+
<DisclosureChevron />
|
|
540
675
|
) : (
|
|
541
|
-
<ChevronUp size={16} color={
|
|
676
|
+
<ChevronUp size={16} color={tm.color.labelTertiary} />
|
|
542
677
|
)
|
|
543
678
|
) : null}
|
|
544
679
|
</TouchableOpacity>
|
|
545
680
|
</View>
|
|
681
|
+
</CardRow>
|
|
546
682
|
);
|
|
547
683
|
}
|
|
548
684
|
if (row.kind === "note") {
|
|
549
|
-
return
|
|
685
|
+
return (
|
|
686
|
+
<CardRow position={row}>
|
|
687
|
+
<Text style={styles.noteRow}>{row.text}</Text>
|
|
688
|
+
</CardRow>
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
if (row.kind === "route") {
|
|
692
|
+
return (
|
|
693
|
+
<CardRow position={row}>
|
|
694
|
+
<TouchableOpacity
|
|
695
|
+
style={styles.routeRow}
|
|
696
|
+
onPress={toggleRoute}
|
|
697
|
+
accessibilityRole="switch"
|
|
698
|
+
accessibilityState={{ checked: row.on }}
|
|
699
|
+
accessibilityLabel={`Also return to ${row.href}`}
|
|
700
|
+
testID="tm-preview-route"
|
|
701
|
+
>
|
|
702
|
+
<Checkbox state={row.on ? "all" : "none"} />
|
|
703
|
+
<View style={styles.routeText}>
|
|
704
|
+
<Text style={styles.routeTitle}>Return to this screen</Text>
|
|
705
|
+
<Text style={styles.routeHref} numberOfLines={1}>
|
|
706
|
+
{row.href}
|
|
707
|
+
</Text>
|
|
708
|
+
</View>
|
|
709
|
+
</TouchableOpacity>
|
|
710
|
+
</CardRow>
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
if (row.kind === "diff") {
|
|
714
|
+
return (
|
|
715
|
+
<CardRow position={row}>
|
|
716
|
+
<ItemDiffPanel item={row.item} />
|
|
717
|
+
</CardRow>
|
|
718
|
+
);
|
|
550
719
|
}
|
|
551
720
|
return (
|
|
552
721
|
<ItemRow
|
|
553
722
|
row={row}
|
|
554
|
-
|
|
723
|
+
expanded={openDiffs.has(cKey(row.sourceId, row.item.key))}
|
|
724
|
+
onOpen={() => toggleDiff(cKey(row.sourceId, row.item.key))}
|
|
555
725
|
onToggle={() => toggleItem(cKey(row.sourceId, row.item.key))}
|
|
556
726
|
/>
|
|
557
727
|
);
|
|
558
728
|
},
|
|
559
|
-
[
|
|
729
|
+
[openDiffs, toggleDiff, toggleCollapse, toggleItem, toggleSource, toggleRoute]
|
|
560
730
|
);
|
|
561
731
|
|
|
562
732
|
const keyExtractor = useCallback((row: Row) => {
|
|
563
733
|
if (row.kind === "source") return `s:${row.source.id}`;
|
|
564
734
|
if (row.kind === "note") return `n:${row.id}`;
|
|
735
|
+
if (row.kind === "route") return "route";
|
|
736
|
+
if (row.kind === "diff") return `d:${row.sourceId}:${row.item.key}`;
|
|
565
737
|
return `i:${row.sourceId}:${row.item.key}`;
|
|
566
738
|
}, []);
|
|
567
739
|
|
|
@@ -578,182 +750,21 @@ export function RestorePreviewScreen({
|
|
|
578
750
|
</View>
|
|
579
751
|
)}
|
|
580
752
|
|
|
581
|
-
{
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
<Text style={styles.compareTag}>
|
|
591
|
-
{liveLeft ? "CURRENT STATE" : "SNAPSHOT"}
|
|
592
|
-
</Text>
|
|
593
|
-
</View>
|
|
594
|
-
<Text style={styles.compareName} numberOfLines={1}>
|
|
595
|
-
{sideName(preview.left, snapshots)}
|
|
596
|
-
</Text>
|
|
597
|
-
{liveLeft ? (
|
|
598
|
-
<View style={styles.compareMetaRow}>
|
|
599
|
-
<Text style={styles.compareMeta}>
|
|
600
|
-
built {timeAgo(preview.builtAt)} ago
|
|
601
|
-
</Text>
|
|
602
|
-
<TouchableOpacity
|
|
603
|
-
onPress={onRefresh}
|
|
604
|
-
disabled={preview.building}
|
|
605
|
-
hitSlop={{ top: 8, bottom: 8, left: 6, right: 8 }}
|
|
606
|
-
accessibilityLabel="Rebuild preview"
|
|
607
|
-
testID="tm-preview-refresh"
|
|
608
|
-
>
|
|
609
|
-
<RefreshCw
|
|
610
|
-
size={11}
|
|
611
|
-
color={
|
|
612
|
-
preview.building
|
|
613
|
-
? macOSColors.text.disabled
|
|
614
|
-
: buoyColors.textSecondary
|
|
615
|
-
}
|
|
616
|
-
/>
|
|
617
|
-
</TouchableOpacity>
|
|
618
|
-
</View>
|
|
619
|
-
) : (
|
|
620
|
-
<Text style={styles.compareMeta}>snapshot</Text>
|
|
621
|
-
)}
|
|
622
|
-
</TouchableOpacity>
|
|
623
|
-
|
|
624
|
-
<View style={styles.swapBadge}>
|
|
625
|
-
<Text style={styles.swapGlyph}>⇄</Text>
|
|
626
|
-
</View>
|
|
627
|
-
|
|
628
|
-
<TouchableOpacity
|
|
629
|
-
style={[styles.compareSide, styles.compareSideRight]}
|
|
630
|
-
onPress={() => onPickSide("right")}
|
|
631
|
-
testID="tm-preview-pick-right"
|
|
632
|
-
>
|
|
633
|
-
<View style={styles.compareTagRow}>
|
|
634
|
-
<Text style={styles.compareTag}>
|
|
635
|
-
{isBaseline ? "BASELINE" : "SNAPSHOT"}
|
|
636
|
-
</Text>
|
|
637
|
-
<View
|
|
638
|
-
style={[styles.stateDot, { backgroundColor: buoyColors.success }]}
|
|
753
|
+
{rightMeta ? (
|
|
754
|
+
<>
|
|
755
|
+
<SectionLabel>Restore Point</SectionLabel>
|
|
756
|
+
<Group>
|
|
757
|
+
<RestorePointNameRow
|
|
758
|
+
name={rightMeta.name}
|
|
759
|
+
onRename={(next) =>
|
|
760
|
+
void timeMachineStore.rename(rightMeta.id, next)
|
|
761
|
+
}
|
|
639
762
|
/>
|
|
640
|
-
</
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
</Text>
|
|
644
|
-
<Text style={[styles.compareMeta, styles.compareMetaRight]}>
|
|
645
|
-
{rightMeta ? `${timeAgo(rightMeta.createdAt)} ago` : ""}
|
|
646
|
-
</Text>
|
|
647
|
-
</TouchableOpacity>
|
|
648
|
-
</View>
|
|
763
|
+
</Group>
|
|
764
|
+
</>
|
|
765
|
+
) : null}
|
|
649
766
|
|
|
650
|
-
|
|
651
|
-
<View style={styles.changesRow}>
|
|
652
|
-
<View style={styles.changesTitleRow}>
|
|
653
|
-
<Text style={styles.changesTitle}>Changes to restore</Text>
|
|
654
|
-
{totals.applies + totals.excludedCount > 1 && (
|
|
655
|
-
<View style={styles.selectLinks}>
|
|
656
|
-
<TouchableOpacity
|
|
657
|
-
onPress={selectAll}
|
|
658
|
-
hitSlop={{ top: 8, bottom: 8, left: 4, right: 4 }}
|
|
659
|
-
testID="tm-preview-select-all"
|
|
660
|
-
>
|
|
661
|
-
<Text
|
|
662
|
-
style={[
|
|
663
|
-
styles.selectLink,
|
|
664
|
-
totals.excludedCount === 0 && styles.selectLinkActive,
|
|
665
|
-
]}
|
|
666
|
-
>
|
|
667
|
-
all
|
|
668
|
-
</Text>
|
|
669
|
-
</TouchableOpacity>
|
|
670
|
-
<Text style={styles.selectLinkSep}>·</Text>
|
|
671
|
-
<TouchableOpacity
|
|
672
|
-
onPress={selectNone}
|
|
673
|
-
hitSlop={{ top: 8, bottom: 8, left: 4, right: 4 }}
|
|
674
|
-
testID="tm-preview-select-none"
|
|
675
|
-
>
|
|
676
|
-
<Text
|
|
677
|
-
style={[
|
|
678
|
-
styles.selectLink,
|
|
679
|
-
totals.applies === 0 &&
|
|
680
|
-
totals.excludedCount > 0 &&
|
|
681
|
-
styles.selectLinkActive,
|
|
682
|
-
]}
|
|
683
|
-
>
|
|
684
|
-
none
|
|
685
|
-
</Text>
|
|
686
|
-
</TouchableOpacity>
|
|
687
|
-
{liveLeft && rightMeta && (
|
|
688
|
-
<>
|
|
689
|
-
<Text style={styles.selectLinkSep}>·</Text>
|
|
690
|
-
<TouchableOpacity
|
|
691
|
-
onPress={saveScope}
|
|
692
|
-
hitSlop={{ top: 8, bottom: 8, left: 4, right: 4 }}
|
|
693
|
-
testID="tm-preview-save-scope"
|
|
694
|
-
>
|
|
695
|
-
<Text
|
|
696
|
-
style={[
|
|
697
|
-
styles.selectLink,
|
|
698
|
-
!!rightMeta.scope?.length && styles.selectLinkActive,
|
|
699
|
-
]}
|
|
700
|
-
>
|
|
701
|
-
save scope
|
|
702
|
-
</Text>
|
|
703
|
-
</TouchableOpacity>
|
|
704
|
-
</>
|
|
705
|
-
)}
|
|
706
|
-
</View>
|
|
707
|
-
)}
|
|
708
|
-
</View>
|
|
709
|
-
{preview.building ? (
|
|
710
|
-
<ActivityIndicator size="small" color={buoyColors.primary} />
|
|
711
|
-
) : (
|
|
712
|
-
<View style={styles.changesChip}>
|
|
713
|
-
<Text style={styles.changesChipText} numberOfLines={1}>
|
|
714
|
-
{totals.applies === 0
|
|
715
|
-
? totals.excludedCount > 0
|
|
716
|
-
? "everything excluded"
|
|
717
|
-
: "no changes"
|
|
718
|
-
: totals.perSource.join(" · ")}
|
|
719
|
-
{totals.excludedCount > 0 && totals.applies > 0
|
|
720
|
-
? ` · ${totals.excludedCount} excluded`
|
|
721
|
-
: ""}
|
|
722
|
-
</Text>
|
|
723
|
-
</View>
|
|
724
|
-
)}
|
|
725
|
-
</View>
|
|
726
|
-
{totals.wontApply > 0 && (
|
|
727
|
-
<Text style={styles.wontApplyLine}>
|
|
728
|
-
⊘ {totals.wontApply} item{totals.wontApply === 1 ? "" : "s"} won't
|
|
729
|
-
apply — shown inline with the reason
|
|
730
|
-
</Text>
|
|
731
|
-
)}
|
|
732
|
-
|
|
733
|
-
{/* Search + expand/collapse all */}
|
|
734
|
-
<View style={styles.searchRow}>
|
|
735
|
-
<View style={styles.searchField}>
|
|
736
|
-
<Search size={14} color={buoyColors.textMuted} />
|
|
737
|
-
<TextInput
|
|
738
|
-
style={styles.searchInput}
|
|
739
|
-
value={localSearch}
|
|
740
|
-
onChangeText={setLocalSearch}
|
|
741
|
-
placeholder="Filter keys…"
|
|
742
|
-
placeholderTextColor={buoyColors.textMuted}
|
|
743
|
-
autoCorrect={false}
|
|
744
|
-
autoCapitalize="none"
|
|
745
|
-
testID="tm-preview-search"
|
|
746
|
-
/>
|
|
747
|
-
</View>
|
|
748
|
-
<TouchableOpacity
|
|
749
|
-
style={styles.collapseAllButton}
|
|
750
|
-
onPress={toggleAllCollapsed}
|
|
751
|
-
accessibilityLabel={allCollapsed ? "Expand all" : "Collapse all"}
|
|
752
|
-
testID="tm-preview-collapse-all"
|
|
753
|
-
>
|
|
754
|
-
<Filter size={15} color={buoyColors.textSecondary} />
|
|
755
|
-
</TouchableOpacity>
|
|
756
|
-
</View>
|
|
767
|
+
<SectionLabel>Changes to Restore</SectionLabel>
|
|
757
768
|
</View>
|
|
758
769
|
);
|
|
759
770
|
|
|
@@ -767,7 +778,13 @@ export function RestorePreviewScreen({
|
|
|
767
778
|
keyExtractor={keyExtractor}
|
|
768
779
|
ListHeaderComponent={header}
|
|
769
780
|
ListEmptyComponent={
|
|
770
|
-
preview.building ?
|
|
781
|
+
preview.building ? (
|
|
782
|
+
<ActivityIndicator
|
|
783
|
+
style={styles.buildingSpinner}
|
|
784
|
+
size="small"
|
|
785
|
+
color={tm.color.accent}
|
|
786
|
+
/>
|
|
787
|
+
) : (
|
|
771
788
|
<Text style={styles.emptyText}>
|
|
772
789
|
{search
|
|
773
790
|
? "Nothing matches this filter."
|
|
@@ -775,6 +792,23 @@ export function RestorePreviewScreen({
|
|
|
775
792
|
</Text>
|
|
776
793
|
)
|
|
777
794
|
}
|
|
795
|
+
ListFooterComponent={
|
|
796
|
+
<View>
|
|
797
|
+
{totals.wontApply > 0 ? (
|
|
798
|
+
<View style={styles.warnNote}>
|
|
799
|
+
<AlertTriangle size={12} color={tm.color.warning} />
|
|
800
|
+
<Text style={styles.warnNoteText}>
|
|
801
|
+
{totals.wontApply} item{totals.wontApply === 1 ? "" : "s"}{" "}
|
|
802
|
+
won't apply — expand{" "}
|
|
803
|
+
{blockedSources.length > 0
|
|
804
|
+
? blockedSources.join(" and ")
|
|
805
|
+
: "the source"}{" "}
|
|
806
|
+
to see the reason.
|
|
807
|
+
</Text>
|
|
808
|
+
</View>
|
|
809
|
+
) : null}
|
|
810
|
+
</View>
|
|
811
|
+
}
|
|
778
812
|
removeClippedSubviews
|
|
779
813
|
initialNumToRender={20}
|
|
780
814
|
windowSize={7}
|
|
@@ -783,7 +817,7 @@ export function RestorePreviewScreen({
|
|
|
783
817
|
|
|
784
818
|
{/* Sticky Restore CTA */}
|
|
785
819
|
{liveLeft && rightMeta && (
|
|
786
|
-
<View style={styles.footer}>
|
|
820
|
+
<View style={[styles.footer, { paddingBottom: insets.bottom }]}>
|
|
787
821
|
<TouchableOpacity
|
|
788
822
|
style={[
|
|
789
823
|
styles.cta,
|
|
@@ -796,8 +830,10 @@ export function RestorePreviewScreen({
|
|
|
796
830
|
disabled={isBaseline ? busy || preview.building : ctaDisabled}
|
|
797
831
|
testID="tm-preview-restore"
|
|
798
832
|
>
|
|
799
|
-
<RefreshCw size={16} color=
|
|
800
|
-
<Text
|
|
833
|
+
<RefreshCw size={16} color={isBaseline ? tm.color.onDanger : tm.color.onAccent} />
|
|
834
|
+
<Text
|
|
835
|
+
style={[styles.ctaText, isBaseline ? styles.ctaTextDanger : null]}
|
|
836
|
+
>
|
|
801
837
|
{isBaseline ? "Wipe & Reload" : "Restore State"}
|
|
802
838
|
</Text>
|
|
803
839
|
</TouchableOpacity>
|
|
@@ -817,161 +853,125 @@ export function RestorePreviewScreen({
|
|
|
817
853
|
const HIT = { top: 10, bottom: 10, left: 10, right: 10 };
|
|
818
854
|
|
|
819
855
|
const styles = StyleSheet.create({
|
|
820
|
-
container: { flex: 1 },
|
|
856
|
+
container: { flex: 1, backgroundColor: tm.color.sheet },
|
|
821
857
|
fallbackHeader: {
|
|
822
858
|
flexDirection: "row",
|
|
823
859
|
alignItems: "center",
|
|
824
|
-
|
|
860
|
+
paddingHorizontal: tm.gutter,
|
|
861
|
+
paddingVertical: 10,
|
|
825
862
|
gap: 10,
|
|
826
863
|
},
|
|
827
|
-
backText: { color:
|
|
864
|
+
backText: { color: tm.color.accent, fontSize: tm.font.control },
|
|
828
865
|
screenTitle: {
|
|
829
|
-
color:
|
|
830
|
-
fontSize:
|
|
866
|
+
color: tm.color.label,
|
|
867
|
+
fontSize: 20,
|
|
831
868
|
fontWeight: "700",
|
|
869
|
+
letterSpacing: tm.tracking.row,
|
|
832
870
|
flex: 1,
|
|
833
871
|
},
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
listContent: { paddingHorizontal: 12, paddingTop: 8, paddingBottom: 12 },
|
|
872
|
+
dimmed: { opacity: 0.5 },
|
|
873
|
+
listContent: { paddingTop: 14, paddingBottom: 12 },
|
|
837
874
|
|
|
838
|
-
//
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
backgroundColor: buoyColors.card,
|
|
843
|
-
borderColor: buoyColors.border,
|
|
844
|
-
borderWidth: 1,
|
|
845
|
-
borderRadius: 14,
|
|
846
|
-
padding: 14,
|
|
847
|
-
gap: 10,
|
|
848
|
-
},
|
|
849
|
-
compareSide: { flex: 1, minWidth: 0, gap: 3 },
|
|
850
|
-
compareSideRight: { alignItems: "flex-end" },
|
|
851
|
-
compareTagRow: { flexDirection: "row", alignItems: "center", gap: 5 },
|
|
852
|
-
stateDot: { width: 6, height: 6, borderRadius: 3 },
|
|
853
|
-
compareTag: {
|
|
854
|
-
color: buoyColors.textSecondary,
|
|
855
|
-
fontSize: 10,
|
|
856
|
-
fontWeight: "700",
|
|
857
|
-
letterSpacing: 0.6,
|
|
858
|
-
},
|
|
859
|
-
compareName: { color: buoyColors.text, fontSize: 15, fontWeight: "700" },
|
|
860
|
-
compareNameRight: { textAlign: "right" },
|
|
861
|
-
compareMeta: { color: buoyColors.textMuted, fontSize: 10 },
|
|
862
|
-
compareMetaRight: { textAlign: "right" },
|
|
863
|
-
compareMetaRow: { flexDirection: "row", alignItems: "center", gap: 6 },
|
|
864
|
-
swapBadge: {
|
|
865
|
-
width: 34,
|
|
866
|
-
height: 34,
|
|
867
|
-
borderRadius: 17,
|
|
868
|
-
backgroundColor: buoyColors.hover,
|
|
869
|
-
borderColor: buoyColors.border,
|
|
870
|
-
borderWidth: 1,
|
|
871
|
-
alignItems: "center",
|
|
872
|
-
justifyContent: "center",
|
|
875
|
+
// ── The grouped card each list row paints its own share of ──────────────
|
|
876
|
+
cardRow: {
|
|
877
|
+
marginHorizontal: tm.gutter,
|
|
878
|
+
backgroundColor: tm.color.group,
|
|
873
879
|
},
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
changesRow: {
|
|
878
|
-
flexDirection: "row",
|
|
879
|
-
alignItems: "center",
|
|
880
|
-
justifyContent: "space-between",
|
|
881
|
-
marginTop: 14,
|
|
882
|
-
gap: 10,
|
|
880
|
+
cardRowFirst: {
|
|
881
|
+
borderTopLeftRadius: tm.radius.group,
|
|
882
|
+
borderTopRightRadius: tm.radius.group,
|
|
883
883
|
},
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
gap: 8,
|
|
884
|
+
cardRowLast: {
|
|
885
|
+
borderBottomLeftRadius: tm.radius.group,
|
|
886
|
+
borderBottomRightRadius: tm.radius.group,
|
|
887
|
+
marginBottom: 10,
|
|
889
888
|
},
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
changesChip: {
|
|
895
|
-
backgroundColor: buoyColors.card,
|
|
896
|
-
borderColor: buoyColors.border,
|
|
897
|
-
borderWidth: 1,
|
|
898
|
-
borderRadius: 8,
|
|
899
|
-
paddingHorizontal: 10,
|
|
900
|
-
paddingVertical: 5,
|
|
901
|
-
maxWidth: "60%",
|
|
889
|
+
cardSeparator: {
|
|
890
|
+
height: StyleSheet.hairlineWidth,
|
|
891
|
+
marginLeft: 51,
|
|
892
|
+
backgroundColor: tm.color.separator,
|
|
902
893
|
},
|
|
903
|
-
changesChipText: { color: buoyColors.textSecondary, fontSize: 11 },
|
|
904
|
-
wontApplyLine: { color: buoyColors.warning, fontSize: 11, marginTop: 6 },
|
|
905
894
|
|
|
906
|
-
//
|
|
907
|
-
|
|
895
|
+
// ── Selection card ──────────────────────────────────────────────────────
|
|
896
|
+
|
|
897
|
+
// ── The "won't apply" heads-up under the card ───────────────────────────
|
|
898
|
+
warnNote: {
|
|
908
899
|
flexDirection: "row",
|
|
909
|
-
alignItems: "
|
|
910
|
-
gap:
|
|
911
|
-
|
|
912
|
-
|
|
900
|
+
alignItems: "flex-start",
|
|
901
|
+
gap: 6,
|
|
902
|
+
paddingHorizontal: tm.gutter + tm.textInset,
|
|
903
|
+
paddingTop: 8,
|
|
913
904
|
},
|
|
914
|
-
|
|
905
|
+
warnNoteText: {
|
|
915
906
|
flex: 1,
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
backgroundColor: buoyColors.card,
|
|
920
|
-
borderColor: buoyColors.border,
|
|
921
|
-
borderWidth: 1,
|
|
922
|
-
borderRadius: 10,
|
|
923
|
-
paddingHorizontal: 10,
|
|
907
|
+
color: tm.color.labelSecondary,
|
|
908
|
+
fontSize: tm.font.footnote,
|
|
909
|
+
lineHeight: tm.font.footnote * 1.35,
|
|
924
910
|
},
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
911
|
+
|
|
912
|
+
// ── Count pills on a source row ─────────────────────────────────────────
|
|
913
|
+
countPill: {
|
|
914
|
+
borderRadius: tm.radius.pill,
|
|
915
|
+
paddingHorizontal: 9,
|
|
916
|
+
paddingVertical: 3,
|
|
930
917
|
},
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
backgroundColor: buoyColors.card,
|
|
936
|
-
borderColor: buoyColors.border,
|
|
937
|
-
borderWidth: 1,
|
|
938
|
-
alignItems: "center",
|
|
939
|
-
justifyContent: "center",
|
|
918
|
+
countPillText: {
|
|
919
|
+
fontFamily: TM_MONO,
|
|
920
|
+
fontSize: tm.font.footnote,
|
|
921
|
+
fontWeight: "600",
|
|
940
922
|
},
|
|
941
923
|
|
|
942
|
-
//
|
|
924
|
+
// ── Include checkbox ────────────────────────────────────────────────────
|
|
943
925
|
checkbox: {
|
|
944
|
-
width:
|
|
945
|
-
height:
|
|
946
|
-
borderRadius:
|
|
947
|
-
borderWidth: 1.
|
|
948
|
-
borderColor:
|
|
926
|
+
width: 23,
|
|
927
|
+
height: 23,
|
|
928
|
+
borderRadius: tm.radius.pill,
|
|
929
|
+
borderWidth: 1.7,
|
|
930
|
+
borderColor: tm.color.labelTertiary,
|
|
949
931
|
alignItems: "center",
|
|
950
932
|
justifyContent: "center",
|
|
951
933
|
},
|
|
952
934
|
checkboxChecked: {
|
|
953
|
-
borderColor:
|
|
954
|
-
backgroundColor:
|
|
935
|
+
borderColor: tm.color.accent,
|
|
936
|
+
backgroundColor: tm.color.accent,
|
|
955
937
|
},
|
|
956
938
|
checkboxGlyph: {
|
|
957
|
-
color:
|
|
958
|
-
fontSize:
|
|
939
|
+
color: tm.color.onAccent,
|
|
940
|
+
fontSize: 13,
|
|
959
941
|
fontWeight: "700",
|
|
960
|
-
lineHeight:
|
|
942
|
+
lineHeight: 15,
|
|
943
|
+
},
|
|
944
|
+
|
|
945
|
+
// ── Route row ───────────────────────────────────────────────────────────
|
|
946
|
+
routeRow: {
|
|
947
|
+
flexDirection: "row",
|
|
948
|
+
alignItems: "center",
|
|
949
|
+
minHeight: 58,
|
|
950
|
+
paddingHorizontal: tm.textInset,
|
|
951
|
+
paddingVertical: 9,
|
|
952
|
+
gap: 12,
|
|
953
|
+
},
|
|
954
|
+
routeText: { flex: 1, minWidth: 0, gap: 2 },
|
|
955
|
+
routeTitle: {
|
|
956
|
+
color: tm.color.label,
|
|
957
|
+
fontSize: tm.font.row,
|
|
958
|
+
fontWeight: "600",
|
|
959
|
+
letterSpacing: tm.tracking.row,
|
|
960
|
+
},
|
|
961
|
+
routeHref: {
|
|
962
|
+
color: tm.color.labelSecondary,
|
|
963
|
+
fontFamily: TM_MONO,
|
|
964
|
+
fontSize: tm.font.mono,
|
|
961
965
|
},
|
|
962
966
|
|
|
963
|
-
// Source
|
|
967
|
+
// ── Source header row (first row of its card) ───────────────────────────
|
|
964
968
|
sourceCard: {
|
|
965
969
|
flexDirection: "row",
|
|
966
970
|
alignItems: "center",
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
paddingHorizontal: 12,
|
|
972
|
-
paddingVertical: 10,
|
|
973
|
-
marginTop: 8,
|
|
974
|
-
gap: 10,
|
|
971
|
+
minHeight: 58,
|
|
972
|
+
paddingHorizontal: tm.textInset,
|
|
973
|
+
paddingVertical: 9,
|
|
974
|
+
gap: 12,
|
|
975
975
|
},
|
|
976
976
|
sourceCardTouch: {
|
|
977
977
|
flex: 1,
|
|
@@ -980,110 +980,110 @@ const styles = StyleSheet.create({
|
|
|
980
980
|
gap: 12,
|
|
981
981
|
minWidth: 0,
|
|
982
982
|
},
|
|
983
|
-
sourceIconTile: {
|
|
984
|
-
width: 36,
|
|
985
|
-
height: 36,
|
|
986
|
-
borderRadius: 10,
|
|
987
|
-
backgroundColor: buoyColors.hover,
|
|
988
|
-
alignItems: "center",
|
|
989
|
-
justifyContent: "center",
|
|
990
|
-
},
|
|
991
983
|
sourceNameBlock: { flex: 1, minWidth: 0, gap: 2 },
|
|
992
984
|
sourceName: {
|
|
993
|
-
color:
|
|
994
|
-
fontSize:
|
|
995
|
-
fontWeight: "
|
|
985
|
+
color: tm.color.label,
|
|
986
|
+
fontSize: tm.font.row,
|
|
987
|
+
fontWeight: "600",
|
|
988
|
+
letterSpacing: tm.tracking.row,
|
|
996
989
|
},
|
|
997
990
|
sourceSummary: {
|
|
998
|
-
color:
|
|
999
|
-
fontSize:
|
|
1000
|
-
fontVariant: ["tabular-nums"],
|
|
1001
|
-
},
|
|
1002
|
-
sourceBadges: { flexDirection: "row", gap: 8 },
|
|
1003
|
-
sourceBadge: {
|
|
1004
|
-
fontSize: 13,
|
|
1005
|
-
fontWeight: "700",
|
|
991
|
+
color: tm.color.labelSecondary,
|
|
992
|
+
fontSize: tm.font.footnote,
|
|
1006
993
|
fontVariant: ["tabular-nums"],
|
|
1007
994
|
},
|
|
995
|
+
sourceBadges: { flexDirection: "row", gap: 6 },
|
|
1008
996
|
|
|
1009
997
|
noteRow: {
|
|
1010
|
-
color:
|
|
1011
|
-
fontSize:
|
|
1012
|
-
|
|
1013
|
-
|
|
998
|
+
color: tm.color.labelTertiary,
|
|
999
|
+
fontSize: tm.font.footnote,
|
|
1000
|
+
lineHeight: tm.font.footnote * 1.35,
|
|
1001
|
+
paddingHorizontal: tm.textInset,
|
|
1002
|
+
paddingVertical: 9,
|
|
1014
1003
|
},
|
|
1015
1004
|
|
|
1016
|
-
// Item rows (
|
|
1005
|
+
// ── Item rows (further rows of the same card) ───────────────────────────
|
|
1017
1006
|
itemRow: {
|
|
1018
1007
|
flexDirection: "row",
|
|
1019
1008
|
alignItems: "center",
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
paddingHorizontal: 10,
|
|
1025
|
-
paddingVertical: 9,
|
|
1026
|
-
marginTop: 6,
|
|
1027
|
-
marginLeft: 14,
|
|
1028
|
-
gap: 10,
|
|
1009
|
+
paddingLeft: tm.textInset,
|
|
1010
|
+
paddingRight: tm.textInset,
|
|
1011
|
+
paddingVertical: 10,
|
|
1012
|
+
gap: 12,
|
|
1029
1013
|
},
|
|
1030
1014
|
itemBodyTouch: {
|
|
1031
1015
|
flex: 1,
|
|
1032
1016
|
flexDirection: "row",
|
|
1033
1017
|
alignItems: "center",
|
|
1034
|
-
gap:
|
|
1018
|
+
gap: 10,
|
|
1035
1019
|
minWidth: 0,
|
|
1036
1020
|
},
|
|
1037
|
-
itemGlyph: { fontSize:
|
|
1021
|
+
itemGlyph: { fontSize: 14, fontWeight: "700", width: 14, textAlign: "center" },
|
|
1038
1022
|
itemBody: { flex: 1, minWidth: 0 },
|
|
1039
|
-
itemLabel: {
|
|
1023
|
+
itemLabel: {
|
|
1024
|
+
color: tm.color.label,
|
|
1025
|
+
fontSize: tm.font.control,
|
|
1026
|
+
letterSpacing: tm.tracking.control,
|
|
1027
|
+
},
|
|
1040
1028
|
itemPreview: {
|
|
1041
|
-
color:
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1029
|
+
color: tm.color.labelSecondary,
|
|
1030
|
+
fontFamily: TM_MONO,
|
|
1031
|
+
fontSize: tm.font.mono,
|
|
1032
|
+
marginTop: 3,
|
|
1045
1033
|
},
|
|
1046
1034
|
itemPreviewRemoved: { textDecorationLine: "line-through" },
|
|
1047
|
-
itemReason: {
|
|
1035
|
+
itemReason: {
|
|
1036
|
+
color: tm.color.warning,
|
|
1037
|
+
fontSize: tm.font.mono,
|
|
1038
|
+
marginTop: 3,
|
|
1039
|
+
},
|
|
1048
1040
|
itemExcluded: {
|
|
1049
|
-
color:
|
|
1050
|
-
fontSize:
|
|
1051
|
-
marginTop:
|
|
1041
|
+
color: tm.color.labelTertiary,
|
|
1042
|
+
fontSize: tm.font.mono,
|
|
1043
|
+
marginTop: 3,
|
|
1052
1044
|
fontStyle: "italic",
|
|
1053
1045
|
},
|
|
1054
|
-
|
|
1046
|
+
buildingSpinner: {
|
|
1047
|
+
marginTop: 28,
|
|
1048
|
+
},
|
|
1055
1049
|
emptyText: {
|
|
1056
|
-
color:
|
|
1057
|
-
fontSize:
|
|
1050
|
+
color: tm.color.labelSecondary,
|
|
1051
|
+
fontSize: tm.font.footnote,
|
|
1058
1052
|
textAlign: "center",
|
|
1059
|
-
marginTop:
|
|
1053
|
+
marginTop: 28,
|
|
1060
1054
|
},
|
|
1061
1055
|
|
|
1062
|
-
//
|
|
1056
|
+
// ── Sticky restore CTA ──────────────────────────────────────────────────
|
|
1063
1057
|
footer: {
|
|
1064
|
-
paddingHorizontal:
|
|
1065
|
-
paddingTop:
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
backgroundColor: buoyColors.base,
|
|
1058
|
+
paddingHorizontal: tm.gutter,
|
|
1059
|
+
paddingTop: 10,
|
|
1060
|
+
gap: 8,
|
|
1061
|
+
borderTopColor: tm.color.separator,
|
|
1062
|
+
borderTopWidth: StyleSheet.hairlineWidth,
|
|
1063
|
+
backgroundColor: tm.color.sheet,
|
|
1071
1064
|
},
|
|
1072
1065
|
cta: {
|
|
1073
1066
|
flexDirection: "row",
|
|
1074
1067
|
alignItems: "center",
|
|
1075
1068
|
justifyContent: "center",
|
|
1076
1069
|
gap: 8,
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1070
|
+
height: 50,
|
|
1071
|
+
backgroundColor: tm.color.accent,
|
|
1072
|
+
borderRadius: tm.radius.group,
|
|
1080
1073
|
},
|
|
1081
|
-
ctaDanger: { backgroundColor:
|
|
1074
|
+
ctaDanger: { backgroundColor: tm.color.danger },
|
|
1075
|
+
ctaTextDanger: { color: tm.color.onDanger },
|
|
1082
1076
|
ctaDisabled: { opacity: 0.4 },
|
|
1083
|
-
ctaText: {
|
|
1077
|
+
ctaText: {
|
|
1078
|
+
color: tm.color.onAccent,
|
|
1079
|
+
fontSize: tm.font.row,
|
|
1080
|
+
fontWeight: "600",
|
|
1081
|
+
letterSpacing: tm.tracking.row,
|
|
1082
|
+
},
|
|
1084
1083
|
ctaCaption: {
|
|
1085
|
-
color:
|
|
1086
|
-
fontSize:
|
|
1084
|
+
color: tm.color.labelSecondary,
|
|
1085
|
+
fontSize: tm.font.footnote,
|
|
1086
|
+
lineHeight: tm.font.footnote * 1.35,
|
|
1087
1087
|
textAlign: "center",
|
|
1088
1088
|
},
|
|
1089
1089
|
});
|