@buoy-gg/time-machine 7.0.21 → 7.0.23

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