@buoy-gg/shared-ui 7.0.33 → 7.0.34
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/JsModal.js +57 -22
- package/lib/commonjs/hooks/index.js +26 -0
- package/lib/commonjs/hooks/safe-area-impl.js +1 -1
- package/lib/commonjs/hooks/useChangeLog.js +55 -0
- package/lib/commonjs/hooks/useEventStoreEvents.js +65 -0
- package/lib/commonjs/hooks/usePersistedState.js +45 -10
- package/lib/commonjs/index.js +143 -0
- package/lib/commonjs/storage/definePersistedSetting.js +67 -0
- package/lib/commonjs/storage/index.js +8 -1
- package/lib/commonjs/stores/ChangeLog.js +197 -0
- package/lib/commonjs/stores/createToolStore.js +175 -0
- package/lib/commonjs/stores/index.js +15 -1
- package/lib/commonjs/sync/toolSyncAdapter.js +63 -0
- package/lib/commonjs/sync/wireForm.js +172 -0
- package/lib/commonjs/tools/defineTool.js +79 -0
- package/lib/commonjs/ui/components/ChangeRow.js +110 -0
- package/lib/commonjs/ui/components/ChipStrip.js +94 -0
- package/lib/commonjs/ui/components/DetailViewToggle.js +130 -0
- package/lib/commonjs/ui/components/DevToolsCard.js +3 -1
- package/lib/commonjs/ui/components/EntityFilterView.js +354 -0
- package/lib/commonjs/ui/components/ExportPreview.js +174 -0
- package/lib/commonjs/ui/components/InfoRow.js +90 -0
- package/lib/commonjs/ui/components/ModalHeader.js +40 -7
- package/lib/commonjs/ui/components/Pill.js +114 -0
- package/lib/commonjs/ui/components/ToggleRow.js +116 -0
- package/lib/commonjs/ui/components/ToolFooter.js +204 -0
- package/lib/commonjs/ui/components/index.js +100 -1
- package/lib/commonjs/ui/index.js +24 -2
- package/lib/commonjs/ui/listPresets.js +43 -0
- package/lib/commonjs/ui/night/primitives.js +3 -1
- package/lib/commonjs/utils/snapshotProviderKit.js +167 -0
- package/lib/commonjs/utils/stateTools.js +160 -0
- package/lib/module/JsModal.js +57 -22
- package/lib/module/hooks/index.js +3 -1
- package/lib/module/hooks/safe-area-impl.js +1 -1
- package/lib/module/hooks/useChangeLog.js +50 -0
- package/lib/module/hooks/useEventStoreEvents.js +61 -0
- package/lib/module/hooks/usePersistedState.js +44 -10
- package/lib/module/index.js +11 -1
- package/lib/module/storage/definePersistedSetting.js +63 -0
- package/lib/module/storage/index.js +2 -1
- package/lib/module/stores/ChangeLog.js +192 -0
- package/lib/module/stores/createToolStore.js +171 -0
- package/lib/module/stores/index.js +3 -1
- package/lib/module/sync/toolSyncAdapter.js +58 -0
- package/lib/module/sync/wireForm.js +159 -0
- package/lib/module/tools/defineTool.js +74 -0
- package/lib/module/ui/components/ChangeRow.js +105 -0
- package/lib/module/ui/components/ChipStrip.js +91 -0
- package/lib/module/ui/components/DetailViewToggle.js +125 -0
- package/lib/module/ui/components/DevToolsCard.js +3 -1
- package/lib/module/ui/components/EntityFilterView.js +350 -0
- package/lib/module/ui/components/ExportPreview.js +169 -0
- package/lib/module/ui/components/InfoRow.js +86 -0
- package/lib/module/ui/components/ModalHeader.js +40 -7
- package/lib/module/ui/components/Pill.js +110 -0
- package/lib/module/ui/components/ToggleRow.js +112 -0
- package/lib/module/ui/components/ToolFooter.js +196 -0
- package/lib/module/ui/components/index.js +10 -1
- package/lib/module/ui/index.js +4 -1
- package/lib/module/ui/listPresets.js +39 -0
- package/lib/module/ui/night/primitives.js +3 -1
- package/lib/module/utils/snapshotProviderKit.js +156 -0
- package/lib/module/utils/stateTools.js +154 -0
- package/lib/typescript/commonjs/JsModal.d.ts.map +1 -1
- package/lib/typescript/commonjs/hooks/index.d.ts +3 -1
- package/lib/typescript/commonjs/hooks/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/hooks/safe-area-impl.d.ts +1 -1
- package/lib/typescript/commonjs/hooks/useChangeLog.d.ts +38 -0
- package/lib/typescript/commonjs/hooks/useChangeLog.d.ts.map +1 -0
- package/lib/typescript/commonjs/hooks/useEventStoreEvents.d.ts +25 -0
- package/lib/typescript/commonjs/hooks/useEventStoreEvents.d.ts.map +1 -0
- package/lib/typescript/commonjs/hooks/usePersistedState.d.ts +34 -5
- package/lib/typescript/commonjs/hooks/usePersistedState.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +6 -1
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/storage/definePersistedSetting.d.ts +39 -0
- package/lib/typescript/commonjs/storage/definePersistedSetting.d.ts.map +1 -0
- package/lib/typescript/commonjs/storage/index.d.ts +1 -0
- package/lib/typescript/commonjs/storage/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/stores/ChangeLog.d.ts +100 -0
- package/lib/typescript/commonjs/stores/ChangeLog.d.ts.map +1 -0
- package/lib/typescript/commonjs/stores/createToolStore.d.ts +92 -0
- package/lib/typescript/commonjs/stores/createToolStore.d.ts.map +1 -0
- package/lib/typescript/commonjs/stores/index.d.ts +2 -0
- package/lib/typescript/commonjs/stores/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/sync/toolSyncAdapter.d.ts +66 -0
- package/lib/typescript/commonjs/sync/toolSyncAdapter.d.ts.map +1 -0
- package/lib/typescript/commonjs/sync/wireForm.d.ts +100 -0
- package/lib/typescript/commonjs/sync/wireForm.d.ts.map +1 -0
- package/lib/typescript/commonjs/tools/defineTool.d.ts +100 -0
- package/lib/typescript/commonjs/tools/defineTool.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/ChangeRow.d.ts +52 -0
- package/lib/typescript/commonjs/ui/components/ChangeRow.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/ChipStrip.d.ts +41 -0
- package/lib/typescript/commonjs/ui/components/ChipStrip.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/DetailViewToggle.d.ts +36 -0
- package/lib/typescript/commonjs/ui/components/DetailViewToggle.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/DevToolsCard.d.ts +8 -1
- package/lib/typescript/commonjs/ui/components/DevToolsCard.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/EntityFilterView.d.ts +51 -0
- package/lib/typescript/commonjs/ui/components/EntityFilterView.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/ExportPreview.d.ts +48 -0
- package/lib/typescript/commonjs/ui/components/ExportPreview.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/InfoRow.d.ts +33 -0
- package/lib/typescript/commonjs/ui/components/InfoRow.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/ModalHeader.d.ts +31 -1
- package/lib/typescript/commonjs/ui/components/ModalHeader.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/Pill.d.ts +38 -0
- package/lib/typescript/commonjs/ui/components/Pill.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/ToggleRow.d.ts +33 -0
- package/lib/typescript/commonjs/ui/components/ToggleRow.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/ToolFooter.d.ts +69 -0
- package/lib/typescript/commonjs/ui/components/ToolFooter.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/index.d.ts +9 -0
- package/lib/typescript/commonjs/ui/components/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/index.d.ts +1 -0
- package/lib/typescript/commonjs/ui/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/listPresets.d.ts +28 -0
- package/lib/typescript/commonjs/ui/listPresets.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/night/primitives.d.ts +3 -1
- package/lib/typescript/commonjs/ui/night/primitives.d.ts.map +1 -1
- package/lib/typescript/commonjs/utils/snapshotProviderKit.d.ts +81 -0
- package/lib/typescript/commonjs/utils/snapshotProviderKit.d.ts.map +1 -0
- package/lib/typescript/commonjs/utils/stateTools.d.ts +44 -0
- package/lib/typescript/commonjs/utils/stateTools.d.ts.map +1 -0
- package/lib/typescript/module/JsModal.d.ts.map +1 -1
- package/lib/typescript/module/hooks/index.d.ts +3 -1
- package/lib/typescript/module/hooks/index.d.ts.map +1 -1
- package/lib/typescript/module/hooks/safe-area-impl.d.ts +1 -1
- package/lib/typescript/module/hooks/useChangeLog.d.ts +38 -0
- package/lib/typescript/module/hooks/useChangeLog.d.ts.map +1 -0
- package/lib/typescript/module/hooks/useEventStoreEvents.d.ts +25 -0
- package/lib/typescript/module/hooks/useEventStoreEvents.d.ts.map +1 -0
- package/lib/typescript/module/hooks/usePersistedState.d.ts +34 -5
- package/lib/typescript/module/hooks/usePersistedState.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +6 -1
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/storage/definePersistedSetting.d.ts +39 -0
- package/lib/typescript/module/storage/definePersistedSetting.d.ts.map +1 -0
- package/lib/typescript/module/storage/index.d.ts +1 -0
- package/lib/typescript/module/storage/index.d.ts.map +1 -1
- package/lib/typescript/module/stores/ChangeLog.d.ts +100 -0
- package/lib/typescript/module/stores/ChangeLog.d.ts.map +1 -0
- package/lib/typescript/module/stores/createToolStore.d.ts +92 -0
- package/lib/typescript/module/stores/createToolStore.d.ts.map +1 -0
- package/lib/typescript/module/stores/index.d.ts +2 -0
- package/lib/typescript/module/stores/index.d.ts.map +1 -1
- package/lib/typescript/module/sync/toolSyncAdapter.d.ts +66 -0
- package/lib/typescript/module/sync/toolSyncAdapter.d.ts.map +1 -0
- package/lib/typescript/module/sync/wireForm.d.ts +100 -0
- package/lib/typescript/module/sync/wireForm.d.ts.map +1 -0
- package/lib/typescript/module/tools/defineTool.d.ts +100 -0
- package/lib/typescript/module/tools/defineTool.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/ChangeRow.d.ts +52 -0
- package/lib/typescript/module/ui/components/ChangeRow.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/ChipStrip.d.ts +41 -0
- package/lib/typescript/module/ui/components/ChipStrip.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/DetailViewToggle.d.ts +36 -0
- package/lib/typescript/module/ui/components/DetailViewToggle.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/DevToolsCard.d.ts +8 -1
- package/lib/typescript/module/ui/components/DevToolsCard.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/EntityFilterView.d.ts +51 -0
- package/lib/typescript/module/ui/components/EntityFilterView.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/ExportPreview.d.ts +48 -0
- package/lib/typescript/module/ui/components/ExportPreview.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/InfoRow.d.ts +33 -0
- package/lib/typescript/module/ui/components/InfoRow.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/ModalHeader.d.ts +31 -1
- package/lib/typescript/module/ui/components/ModalHeader.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/Pill.d.ts +38 -0
- package/lib/typescript/module/ui/components/Pill.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/ToggleRow.d.ts +33 -0
- package/lib/typescript/module/ui/components/ToggleRow.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/ToolFooter.d.ts +69 -0
- package/lib/typescript/module/ui/components/ToolFooter.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/index.d.ts +9 -0
- package/lib/typescript/module/ui/components/index.d.ts.map +1 -1
- package/lib/typescript/module/ui/index.d.ts +1 -0
- package/lib/typescript/module/ui/index.d.ts.map +1 -1
- package/lib/typescript/module/ui/listPresets.d.ts +28 -0
- package/lib/typescript/module/ui/listPresets.d.ts.map +1 -0
- package/lib/typescript/module/ui/night/primitives.d.ts +3 -1
- package/lib/typescript/module/ui/night/primitives.d.ts.map +1 -1
- package/lib/typescript/module/utils/snapshotProviderKit.d.ts +81 -0
- package/lib/typescript/module/utils/snapshotProviderKit.d.ts.map +1 -0
- package/lib/typescript/module/utils/stateTools.d.ts +44 -0
- package/lib/typescript/module/utils/stateTools.d.ts.map +1 -0
- package/package.json +4 -4
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.PREVIEW_ITEM_RENDER_BYTE_LIMIT = void 0;
|
|
7
|
+
exports.classifyPreviewItem = classifyPreviewItem;
|
|
8
|
+
exports.createRestoreScope = createRestoreScope;
|
|
9
|
+
exports.emptyRestoreResult = emptyRestoreResult;
|
|
10
|
+
exports.failedRestore = failedRestore;
|
|
11
|
+
exports.skippedRestore = skippedRestore;
|
|
12
|
+
exports.unionKeys = unionKeys;
|
|
13
|
+
exports.wontApply = wontApply;
|
|
14
|
+
var _objectDiff = require("../dataViewer/objectDiff.js");
|
|
15
|
+
/**
|
|
16
|
+
* snapshotProviderKit — the parts of a Time Machine `StateSnapshotProvider`
|
|
17
|
+
* that are the same for every source.
|
|
18
|
+
*
|
|
19
|
+
* storage, redux, zustand, jotai and react-query each register a provider
|
|
20
|
+
* (see stateSnapshotRegistry.ts). What differs is how they read and write
|
|
21
|
+
* their source. What was copied five times is:
|
|
22
|
+
*
|
|
23
|
+
* - the restore SCOPE gate: `excludeKeys` (touch all but these) and
|
|
24
|
+
* `includeKeys` (touch only these), the "N item(s) excluded by you"
|
|
25
|
+
* warning, and the empty `StateSnapshotRestoreResult`;
|
|
26
|
+
* - the preview VERDICT ladder: removed → added → unchanged → too-large →
|
|
27
|
+
* changed, with the 256 KB per-item render cap;
|
|
28
|
+
* - the "won't apply" override: a difference restore cannot apply must say
|
|
29
|
+
* so instead of rendering as a green change.
|
|
30
|
+
*
|
|
31
|
+
* The parts that stay per-tool are the ones that matter: redux verifies the
|
|
32
|
+
* jump by reference identity, zustand grafts action functions back, jotai
|
|
33
|
+
* leaves derived atoms to recompute, storage gates MMKV buffers and
|
|
34
|
+
* biometric keys, react-query diffs a comparable projection.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/** Beyond this a preview item is "too-large": restore still applies it, the diff isn't drawn. */
|
|
38
|
+
const PREVIEW_ITEM_RENDER_BYTE_LIMIT = exports.PREVIEW_ITEM_RENDER_BYTE_LIMIT = 256 * 1024;
|
|
39
|
+
/**
|
|
40
|
+
* Both option lists use preview item keys, and both mean "leave everything
|
|
41
|
+
* else untouched in BOTH directions". `includeKeys` wins when present;
|
|
42
|
+
* `excludeKeys` still applies on top.
|
|
43
|
+
*/
|
|
44
|
+
function createRestoreScope(options) {
|
|
45
|
+
const excludedSet = new Set(options?.excludeKeys ?? []);
|
|
46
|
+
const includedSet = options?.includeKeys ? new Set(options.includeKeys) : null;
|
|
47
|
+
let excludedHits = 0;
|
|
48
|
+
const touch = key => !excludedSet.has(key) && (!includedSet || includedSet.has(key));
|
|
49
|
+
return {
|
|
50
|
+
touch,
|
|
51
|
+
allows(key) {
|
|
52
|
+
if (touch(key)) return true;
|
|
53
|
+
excludedHits += 1;
|
|
54
|
+
return false;
|
|
55
|
+
},
|
|
56
|
+
countExcluded(n) {
|
|
57
|
+
excludedHits += n;
|
|
58
|
+
},
|
|
59
|
+
get excludedHits() {
|
|
60
|
+
return excludedHits;
|
|
61
|
+
},
|
|
62
|
+
excludedWarning(noun) {
|
|
63
|
+
return excludedHits > 0 ? `${excludedHits} ${noun} excluded by you` : null;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** A fresh, successful, empty result to accumulate into. */
|
|
69
|
+
function emptyRestoreResult() {
|
|
70
|
+
return {
|
|
71
|
+
ok: true,
|
|
72
|
+
applied: 0,
|
|
73
|
+
skipped: [],
|
|
74
|
+
warnings: []
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** A failed result with one warning (an empty or unusable snapshot). */
|
|
79
|
+
function failedRestore(warning) {
|
|
80
|
+
return {
|
|
81
|
+
ok: false,
|
|
82
|
+
applied: 0,
|
|
83
|
+
skipped: [],
|
|
84
|
+
warnings: [warning]
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** A failed result with one skipped item and its reason. */
|
|
89
|
+
function skippedRestore(id, reason) {
|
|
90
|
+
return {
|
|
91
|
+
ok: false,
|
|
92
|
+
applied: 0,
|
|
93
|
+
skipped: [{
|
|
94
|
+
id,
|
|
95
|
+
reason
|
|
96
|
+
}],
|
|
97
|
+
warnings: []
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* The verdict ladder: removed → added → unchanged → too-large → changed.
|
|
102
|
+
* Callers layer source-specific verdicts (`wont-apply`, `recomputes`) on top
|
|
103
|
+
* with `wontApply()`.
|
|
104
|
+
*/
|
|
105
|
+
function classifyPreviewItem(input, options = {}) {
|
|
106
|
+
const {
|
|
107
|
+
key,
|
|
108
|
+
label,
|
|
109
|
+
oldValue,
|
|
110
|
+
newValue,
|
|
111
|
+
inOld,
|
|
112
|
+
inNew
|
|
113
|
+
} = input;
|
|
114
|
+
if (!inNew) return {
|
|
115
|
+
key,
|
|
116
|
+
label,
|
|
117
|
+
verdict: "removed",
|
|
118
|
+
oldValue
|
|
119
|
+
};
|
|
120
|
+
if (!inOld) return {
|
|
121
|
+
key,
|
|
122
|
+
label,
|
|
123
|
+
verdict: "added",
|
|
124
|
+
newValue
|
|
125
|
+
};
|
|
126
|
+
const equal = options.equal ?? _objectDiff.boundedDeepEqual;
|
|
127
|
+
if (equal(oldValue, newValue)) return {
|
|
128
|
+
key,
|
|
129
|
+
label,
|
|
130
|
+
verdict: "unchanged"
|
|
131
|
+
};
|
|
132
|
+
const tooLarge = options.tooLarge ?? (value => (0, _objectDiff.jsonLikelyLargerThan)(value, PREVIEW_ITEM_RENDER_BYTE_LIMIT));
|
|
133
|
+
if (tooLarge(oldValue) || tooLarge(newValue)) {
|
|
134
|
+
return {
|
|
135
|
+
key,
|
|
136
|
+
label,
|
|
137
|
+
verdict: "too-large",
|
|
138
|
+
reason: options.tooLargeReason ?? "value too large to render a diff — restore still applies it"
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
key,
|
|
143
|
+
label,
|
|
144
|
+
verdict: "changed",
|
|
145
|
+
oldValue,
|
|
146
|
+
newValue
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Mark an item as something restore cannot apply. An unchanged item is left
|
|
152
|
+
* alone — there is nothing to apply, so nothing is being refused.
|
|
153
|
+
*/
|
|
154
|
+
function wontApply(item, reason) {
|
|
155
|
+
if (item.verdict === "unchanged") return item;
|
|
156
|
+
return {
|
|
157
|
+
...item,
|
|
158
|
+
verdict: "wont-apply",
|
|
159
|
+
reason
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Every key present in either record, once. */
|
|
164
|
+
function unionKeys(a, b) {
|
|
165
|
+
const keysOf = r => r === undefined ? [] : r instanceof Map ? Array.from(r.keys()) : Object.keys(r);
|
|
166
|
+
return new Set([...keysOf(a), ...keysOf(b)]);
|
|
167
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.entityColor = entityColor;
|
|
7
|
+
exports.formatValuePreview = formatValuePreview;
|
|
8
|
+
exports.shallowDiffSummary = shallowDiffSummary;
|
|
9
|
+
/**
|
|
10
|
+
* stateTools — helpers the state tools (redux / zustand / jotai) each carried
|
|
11
|
+
* a private copy of. Pure functions; no React, no RN.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// ============================================
|
|
15
|
+
// Entity color
|
|
16
|
+
// ============================================
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Consistent color for a named entity (a zustand store, a jotai atom, a redux
|
|
20
|
+
* slice). Exact match on `known` wins, then a substring match, then a stable
|
|
21
|
+
* HSL hash of the name rendered as `#rrggbb` so the "color + alpha hex"
|
|
22
|
+
* pattern works everywhere.
|
|
23
|
+
*/
|
|
24
|
+
function entityColor(name, known = {}) {
|
|
25
|
+
const lower = name.toLowerCase();
|
|
26
|
+
if (known[lower]) return known[lower];
|
|
27
|
+
for (const [key, color] of Object.entries(known)) {
|
|
28
|
+
if (lower.includes(key)) return color;
|
|
29
|
+
}
|
|
30
|
+
const hash = name.split("").reduce((acc, char) => acc + char.charCodeAt(0), 0);
|
|
31
|
+
const hue = hash * 137 % 360;
|
|
32
|
+
const s = 0.7;
|
|
33
|
+
const l = 0.6;
|
|
34
|
+
const c = (1 - Math.abs(2 * l - 1)) * s;
|
|
35
|
+
const x = c * (1 - Math.abs(hue / 60 % 2 - 1));
|
|
36
|
+
const m = l - c / 2;
|
|
37
|
+
let r = 0;
|
|
38
|
+
let g = 0;
|
|
39
|
+
let b = 0;
|
|
40
|
+
if (hue < 60) {
|
|
41
|
+
r = c;
|
|
42
|
+
g = x;
|
|
43
|
+
} else if (hue < 120) {
|
|
44
|
+
r = x;
|
|
45
|
+
g = c;
|
|
46
|
+
} else if (hue < 180) {
|
|
47
|
+
g = c;
|
|
48
|
+
b = x;
|
|
49
|
+
} else if (hue < 240) {
|
|
50
|
+
g = x;
|
|
51
|
+
b = c;
|
|
52
|
+
} else if (hue < 300) {
|
|
53
|
+
r = x;
|
|
54
|
+
b = c;
|
|
55
|
+
} else {
|
|
56
|
+
r = c;
|
|
57
|
+
b = x;
|
|
58
|
+
}
|
|
59
|
+
const toHex = v => Math.round((v + m) * 255).toString(16).padStart(2, "0");
|
|
60
|
+
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ============================================
|
|
64
|
+
// Value preview
|
|
65
|
+
// ============================================
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* One-line preview of an arbitrary value for a list row: strings quoted and
|
|
69
|
+
* truncated, arrays and objects stringified when short, otherwise collapsed
|
|
70
|
+
* to `[N items]` / `{ N keys }`. Never throws.
|
|
71
|
+
*/
|
|
72
|
+
function formatValuePreview(value, options = {}) {
|
|
73
|
+
const maxLength = options.maxLength ?? 40;
|
|
74
|
+
if (value === undefined) return options.undefinedLabel ?? "undefined";
|
|
75
|
+
if (value === null) return "null";
|
|
76
|
+
try {
|
|
77
|
+
if (typeof value === "function") return options.functionLabel ?? "(fn)";
|
|
78
|
+
if (typeof value === "string") {
|
|
79
|
+
return value.length > maxLength ? `"${value.slice(0, maxLength - 3)}..."` : `"${value}"`;
|
|
80
|
+
}
|
|
81
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
82
|
+
return String(value);
|
|
83
|
+
}
|
|
84
|
+
if (Array.isArray(value)) {
|
|
85
|
+
if (value.length === 0) return "[]";
|
|
86
|
+
const preview = JSON.stringify(value);
|
|
87
|
+
return preview.length > maxLength ? `[${value.length} items]` : preview;
|
|
88
|
+
}
|
|
89
|
+
if (typeof value === "object") {
|
|
90
|
+
const keys = Object.keys(value);
|
|
91
|
+
if (keys.length === 0) return "{}";
|
|
92
|
+
const preview = JSON.stringify(value);
|
|
93
|
+
if (preview.length <= maxLength) return preview;
|
|
94
|
+
return `{ ${keys.length} keys }`;
|
|
95
|
+
}
|
|
96
|
+
return String(value).slice(0, maxLength);
|
|
97
|
+
} catch {
|
|
98
|
+
return "[complex]";
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ============================================
|
|
103
|
+
// Shallow diff summary
|
|
104
|
+
// ============================================
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Top-level, reference-equality diff between two states. Enough for a list
|
|
108
|
+
* row ("+1 ~2 keys"); the detail view runs the real tree diff.
|
|
109
|
+
*
|
|
110
|
+
* A state object that was REPLACED without any top-level value moving —
|
|
111
|
+
* exactly what zustand's persist middleware does when it rehydrates — is
|
|
112
|
+
* reported as `"nested change"` with no keys, never as one changed key with
|
|
113
|
+
* none named.
|
|
114
|
+
*/
|
|
115
|
+
function shallowDiffSummary(prev, next) {
|
|
116
|
+
if (prev === next) {
|
|
117
|
+
return {
|
|
118
|
+
summary: "no change",
|
|
119
|
+
changedKeys: [],
|
|
120
|
+
changedCount: 0
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
if (typeof prev !== "object" || typeof next !== "object" || prev === null || next === null) {
|
|
124
|
+
return {
|
|
125
|
+
summary: "changed",
|
|
126
|
+
changedKeys: [],
|
|
127
|
+
changedCount: 0
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
const prevRecord = prev;
|
|
131
|
+
const nextRecord = next;
|
|
132
|
+
const prevKeys = Object.keys(prevRecord);
|
|
133
|
+
const nextKeys = Object.keys(nextRecord);
|
|
134
|
+
const added = nextKeys.filter(k => !prevKeys.includes(k));
|
|
135
|
+
const removed = prevKeys.filter(k => !nextKeys.includes(k));
|
|
136
|
+
const changed = [];
|
|
137
|
+
for (const key of prevKeys) {
|
|
138
|
+
if (nextKeys.includes(key) && prevRecord[key] !== nextRecord[key]) {
|
|
139
|
+
changed.push(key);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const changedKeys = [...added, ...removed, ...changed];
|
|
143
|
+
const parts = [];
|
|
144
|
+
if (added.length > 0) parts.push(`+${added.length}`);
|
|
145
|
+
if (removed.length > 0) parts.push(`-${removed.length}`);
|
|
146
|
+
if (changed.length > 0) parts.push(`~${changed.length}`);
|
|
147
|
+
if (parts.length === 0) {
|
|
148
|
+
return {
|
|
149
|
+
summary: "nested change",
|
|
150
|
+
changedKeys: [],
|
|
151
|
+
changedCount: 0
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
const total = changedKeys.length;
|
|
155
|
+
return {
|
|
156
|
+
summary: `${parts.join(" ")} ${total === 1 ? "key" : "keys"}`,
|
|
157
|
+
changedKeys,
|
|
158
|
+
changedCount: total
|
|
159
|
+
};
|
|
160
|
+
}
|
package/lib/module/JsModal.js
CHANGED
|
@@ -602,7 +602,6 @@ const JsModalComponent = ({
|
|
|
602
602
|
// ============================================================================
|
|
603
603
|
// REFS for values we need to track
|
|
604
604
|
// ============================================================================
|
|
605
|
-
const isExternallyControlled = !!externalAnimatedHeight;
|
|
606
605
|
const effectiveMaxHeight = maxHeight || SCREEN.height - insets.top;
|
|
607
606
|
|
|
608
607
|
// Mode toggle handler
|
|
@@ -724,29 +723,57 @@ const JsModalComponent = ({
|
|
|
724
723
|
// ============================================================================
|
|
725
724
|
// EFFECT: Visibility Animations - All using native driver!
|
|
726
725
|
// ============================================================================
|
|
726
|
+
const snapVisibility = shown => {
|
|
727
|
+
if (shown) {
|
|
728
|
+
if (mode === "bottomSheet") {
|
|
729
|
+
bottomSheetTranslateY.setValue(0);
|
|
730
|
+
} else {
|
|
731
|
+
floatingScale.setValue(1);
|
|
732
|
+
floatingPosition.setValue({
|
|
733
|
+
x: currentDimensionsRef.current.left,
|
|
734
|
+
y: currentDimensionsRef.current.top
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
visibilityProgress.setValue(1);
|
|
738
|
+
} else {
|
|
739
|
+
if (mode === "bottomSheet") {
|
|
740
|
+
bottomSheetTranslateY.setValue(SCREEN.height);
|
|
741
|
+
}
|
|
742
|
+
visibilityProgress.setValue(0);
|
|
743
|
+
}
|
|
744
|
+
};
|
|
745
|
+
|
|
746
|
+
/**
|
|
747
|
+
* Snap DURING RENDER when `visible` flips, not only in the effect below.
|
|
748
|
+
*
|
|
749
|
+
* These values drive the sheet's opacity/translate through the native
|
|
750
|
+
* driver. The React commit for a `visible` change carries whatever the
|
|
751
|
+
* JS-side values are at render time — the OLD, hidden ones when the snap
|
|
752
|
+
* only happens in the effect — and on Fabric that commit's mount can land
|
|
753
|
+
* on the main thread AFTER the effect's native `setValue`, overwriting it.
|
|
754
|
+
* The Animated node then says "shown" while the UIView sits at opacity 0,
|
|
755
|
+
* until some unrelated re-render commits the JS-side values again. That
|
|
756
|
+
* is how "restore tool A: nothing; restore tool B: A appears; hide B: now
|
|
757
|
+
* B appears" happened — every restore was one commit behind.
|
|
758
|
+
*
|
|
759
|
+
* Setting the values before the render's props are computed makes the
|
|
760
|
+
* commit and the native node agree, whichever lands last. `setValue` is
|
|
761
|
+
* idempotent, so a re-run render is harmless.
|
|
762
|
+
*/
|
|
763
|
+
const snappedVisibleRef = useRef(null);
|
|
764
|
+
if (INSTANT_MODAL_ENTRANCE && snappedVisibleRef.current !== visible) {
|
|
765
|
+
snappedVisibleRef.current = visible;
|
|
766
|
+
snapVisibility(visible);
|
|
767
|
+
}
|
|
727
768
|
useEffect(() => {
|
|
728
769
|
// Visibility effect
|
|
729
770
|
let openAnimation = null;
|
|
730
771
|
let closeAnimation = null;
|
|
731
772
|
if (INSTANT_MODAL_ENTRANCE) {
|
|
732
|
-
// Snap to the final state — no animation in either direction.
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
} else {
|
|
737
|
-
floatingScale.setValue(1);
|
|
738
|
-
floatingPosition.setValue({
|
|
739
|
-
x: currentDimensionsRef.current.left,
|
|
740
|
-
y: currentDimensionsRef.current.top
|
|
741
|
-
});
|
|
742
|
-
}
|
|
743
|
-
visibilityProgress.setValue(1);
|
|
744
|
-
} else {
|
|
745
|
-
if (mode === "bottomSheet") {
|
|
746
|
-
bottomSheetTranslateY.setValue(SCREEN.height);
|
|
747
|
-
}
|
|
748
|
-
visibilityProgress.setValue(0);
|
|
749
|
-
}
|
|
773
|
+
// Snap to the final state — no animation in either direction. The
|
|
774
|
+
// render-phase snap above already did this for a `visible` flip; this
|
|
775
|
+
// re-applies it for a `mode` change while shown.
|
|
776
|
+
snapVisibility(visible);
|
|
750
777
|
return;
|
|
751
778
|
}
|
|
752
779
|
if (visible) {
|
|
@@ -911,8 +938,15 @@ const JsModalComponent = ({
|
|
|
911
938
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
912
939
|
}, [benchMotion, visible]);
|
|
913
940
|
const bottomSheetPanResponder = useMemo(() => PanResponder.create({
|
|
914
|
-
|
|
915
|
-
|
|
941
|
+
// An external `animatedHeight` used to disable this responder
|
|
942
|
+
// outright ("externally controlled"), which left Ask Buoy's sheet —
|
|
943
|
+
// the one host that passes one, for its peek animation — with a
|
|
944
|
+
// header that could not be dragged in bottom-sheet mode. The move
|
|
945
|
+
// and release handlers below already write every height into the
|
|
946
|
+
// external value, so the host and the finger share one number; the
|
|
947
|
+
// grab just has to stop whatever the host is animating first.
|
|
948
|
+
onStartShouldSetPanResponder: () => mode === "bottomSheet",
|
|
949
|
+
onMoveShouldSetPanResponder: (evt, gestureState) => mode === "bottomSheet" && Math.abs(gestureState.dy) > 3,
|
|
916
950
|
onPanResponderTerminationRequest: () => false,
|
|
917
951
|
onPanResponderGrant: evt => {
|
|
918
952
|
setIsResizing(true);
|
|
@@ -924,6 +958,7 @@ const JsModalComponent = ({
|
|
|
924
958
|
animatedBottomPosition.stopAnimation(val => {
|
|
925
959
|
currentHeightRef.current = val;
|
|
926
960
|
});
|
|
961
|
+
externalAnimatedHeight?.stopAnimation();
|
|
927
962
|
bottomSheetTranslateY.stopAnimation();
|
|
928
963
|
},
|
|
929
964
|
onPanResponderMove: evt => {
|
|
@@ -970,7 +1005,7 @@ const JsModalComponent = ({
|
|
|
970
1005
|
setIsResizing(false);
|
|
971
1006
|
// snap back to the last stable height if you want; otherwise no-op
|
|
972
1007
|
}
|
|
973
|
-
}), [mode,
|
|
1008
|
+
}), [mode, minHeight, effectiveMaxHeight, animatedBottomPosition, externalAnimatedHeight, bottomSheetTranslateY, visibilityProgress, onClose]);
|
|
974
1009
|
|
|
975
1010
|
// ============================================================================
|
|
976
1011
|
// CREATE RESIZE HANDLER: For 4-corner resize in floating mode (fixed geometry)
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
export { useFilterManager } from "./useFilterManager.js";
|
|
4
|
-
export { usePersistedState } from "./usePersistedState.js";
|
|
4
|
+
export { usePersistedState, resetPersistedStateCache } from "./usePersistedState.js";
|
|
5
|
+
export { useChangeLog, useSubscribedValue } from "./useChangeLog.js";
|
|
6
|
+
export { useEventStoreEvents } from "./useEventStoreEvents.js";
|
|
5
7
|
export { useSafeAreaInsets } from "./useSafeAreaInsets.js";
|
|
6
8
|
export { getSafeAreaInsets } from "./useSafeAreaInsets.js";
|
|
7
9
|
export { useDeviceInfo, getDeviceInfo, logDeviceInfo } from "./useDeviceInfo.js";
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* React bindings for `ChangeLog` — what useReduxActions, useZustandStateChanges
|
|
5
|
+
* and useJotaiAtomChanges each re-derived: seed state from the log, subscribe
|
|
6
|
+
* for the lifetime of the component, hold a filter, memoize the filtered
|
|
7
|
+
* list, and expose clear / pause / lookup.
|
|
8
|
+
*
|
|
9
|
+
* Tool-specific stats and registries (stores, atoms) compose on top — see
|
|
10
|
+
* `useSubscribedValue` for the registry half.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
14
|
+
export function useChangeLog(log, options = {}) {
|
|
15
|
+
const [items, setItems] = useState(() => log.getAll());
|
|
16
|
+
const [filter, setFilter] = useState(() => options.initialFilter ?? {});
|
|
17
|
+
const [isEnabled, setIsEnabled] = useState(() => log.getEnabled());
|
|
18
|
+
useEffect(() => log.subscribe(setItems), [log]);
|
|
19
|
+
const filterItems = options.filterItems;
|
|
20
|
+
const filteredItems = useMemo(() => filterItems ? filterItems(items, filter) : items, [items, filter, filterItems]);
|
|
21
|
+
const clear = useCallback(() => {
|
|
22
|
+
log.clear();
|
|
23
|
+
}, [log]);
|
|
24
|
+
const toggleCapture = useCallback(() => {
|
|
25
|
+
const next = !isEnabled;
|
|
26
|
+
log.setEnabled(next);
|
|
27
|
+
setIsEnabled(next);
|
|
28
|
+
}, [log, isEnabled]);
|
|
29
|
+
const getById = useCallback(id => log.getById(id), [log]);
|
|
30
|
+
return {
|
|
31
|
+
items,
|
|
32
|
+
filteredItems,
|
|
33
|
+
filter,
|
|
34
|
+
setFilter,
|
|
35
|
+
isEnabled,
|
|
36
|
+
toggleCapture,
|
|
37
|
+
clear,
|
|
38
|
+
getById
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Hold a value that a store pushes through a `(value) => void` listener —
|
|
44
|
+
* a registry of stores, atoms, sources. Seeded synchronously from `read`.
|
|
45
|
+
*/
|
|
46
|
+
export function useSubscribedValue(read, subscribe) {
|
|
47
|
+
const [value, setValue] = useState(read);
|
|
48
|
+
useEffect(() => subscribe(setValue), [subscribe]);
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* React bindings for a `BaseEventStore` — what useRouteEvents and
|
|
5
|
+
* useStorageEvents each hand-rolled: subscribe while enabled (which
|
|
6
|
+
* auto-starts the store's capture), coalesce bursts onto one animation
|
|
7
|
+
* frame so a storm of writes is one commit instead of one per emission,
|
|
8
|
+
* select/limit the list, and report whether capture is live.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { useCallback, useEffect, useState } from "react";
|
|
12
|
+
export function useEventStoreEvents(store, options = {}) {
|
|
13
|
+
const {
|
|
14
|
+
enabled = true,
|
|
15
|
+
maxEvents = 500,
|
|
16
|
+
select
|
|
17
|
+
} = options;
|
|
18
|
+
const [events, setEvents] = useState([]);
|
|
19
|
+
// Tracked locally so it updates reactively with the subscription.
|
|
20
|
+
const [isActive, setIsActive] = useState(enabled);
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (!enabled) {
|
|
23
|
+
setIsActive(false);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Frame-coalesced: bursts land as ONE commit per frame instead of one
|
|
28
|
+
// per emission — the dominant live-churn cost measured on the network
|
|
29
|
+
// tool (tool-bench 2026-07-06).
|
|
30
|
+
let rafId = null;
|
|
31
|
+
let pending = null;
|
|
32
|
+
const unsubscribe = store.subscribeToEvents(all => {
|
|
33
|
+
pending = all;
|
|
34
|
+
if (rafId != null) return;
|
|
35
|
+
rafId = requestAnimationFrame(() => {
|
|
36
|
+
rafId = null;
|
|
37
|
+
if (!pending) return;
|
|
38
|
+
let next = pending;
|
|
39
|
+
pending = null;
|
|
40
|
+
if (select) next = select(next);
|
|
41
|
+
if (next.length > maxEvents) next = next.slice(0, maxEvents);
|
|
42
|
+
setEvents(next);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
setIsActive(true);
|
|
46
|
+
return () => {
|
|
47
|
+
unsubscribe();
|
|
48
|
+
if (rafId != null) cancelAnimationFrame(rafId);
|
|
49
|
+
// Not setting isActive=false here: a dependency change re-subscribes
|
|
50
|
+
// immediately, and that is not "stopped".
|
|
51
|
+
};
|
|
52
|
+
}, [store, enabled, maxEvents, select]);
|
|
53
|
+
const clearEvents = useCallback(() => {
|
|
54
|
+
store.clearEvents();
|
|
55
|
+
}, [store]);
|
|
56
|
+
return {
|
|
57
|
+
events,
|
|
58
|
+
clearEvents,
|
|
59
|
+
isActive
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -10,25 +10,43 @@ import { persistentStorage } from "../utils/persistentStorage.js";
|
|
|
10
10
|
*/
|
|
11
11
|
const hydratedCache = new Map();
|
|
12
12
|
|
|
13
|
+
/** The two calls a backend needs. `persistentStorage` and AsyncStorage both fit. */
|
|
14
|
+
|
|
15
|
+
function defaultParse(raw) {
|
|
16
|
+
try {
|
|
17
|
+
return JSON.parse(raw);
|
|
18
|
+
} catch {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
13
23
|
/**
|
|
14
24
|
* `useState` persisted through `persistentStorage` (FileSystem → AsyncStorage
|
|
15
25
|
* → memory). Replaces the load-then-`hasLoaded`-guard `useEffect` pattern
|
|
16
26
|
* every tool used to hand-roll.
|
|
17
27
|
*
|
|
18
|
-
* Values are JSON-serialized; the default is returned until
|
|
19
|
-
* completes (subsequent mounts of the same key hydrate synchronously
|
|
20
|
-
* in-memory cache). The third tuple element reports hydration so
|
|
21
|
-
* defer writes that must not clobber a not-yet-loaded value.
|
|
28
|
+
* Values are JSON-serialized by default; the default is returned until
|
|
29
|
+
* hydration completes (subsequent mounts of the same key hydrate synchronously
|
|
30
|
+
* from an in-memory cache). The third tuple element reports hydration so
|
|
31
|
+
* callers can defer writes that must not clobber a not-yet-loaded value.
|
|
22
32
|
*
|
|
23
33
|
* ```tsx
|
|
24
34
|
* const [tab, setTab, tabHydrated] = usePersistedState("@react_buoy_custom_flags_tab", "flags");
|
|
35
|
+
*
|
|
36
|
+
* // A Set, stored as an array:
|
|
37
|
+
* const [pins, setPins] = usePersistedState(key, new Set<string>(), {
|
|
38
|
+
* parse: (raw) => new Set(JSON.parse(raw) as string[]),
|
|
39
|
+
* serialize: (set) => JSON.stringify([...set]),
|
|
40
|
+
* });
|
|
25
41
|
* ```
|
|
26
42
|
*/
|
|
27
|
-
export function usePersistedState(key, defaultValue) {
|
|
43
|
+
export function usePersistedState(key, defaultValue, options = {}) {
|
|
28
44
|
const [value, setValue] = useState(() => hydratedCache.has(key) ? hydratedCache.get(key) : defaultValue);
|
|
29
45
|
const [hydrated, setHydrated] = useState(() => hydratedCache.has(key));
|
|
30
46
|
const keyRef = useRef(key);
|
|
31
47
|
keyRef.current = key;
|
|
48
|
+
const optionsRef = useRef(options);
|
|
49
|
+
optionsRef.current = options;
|
|
32
50
|
useEffect(() => {
|
|
33
51
|
if (hydratedCache.has(key)) {
|
|
34
52
|
setValue(hydratedCache.get(key));
|
|
@@ -36,17 +54,22 @@ export function usePersistedState(key, defaultValue) {
|
|
|
36
54
|
return;
|
|
37
55
|
}
|
|
38
56
|
let cancelled = false;
|
|
39
|
-
|
|
57
|
+
const storage = optionsRef.current.storage ?? persistentStorage;
|
|
58
|
+
const parse = optionsRef.current.parse ?? defaultParse;
|
|
59
|
+
storage.getItem(key).then(raw => {
|
|
40
60
|
if (cancelled) return;
|
|
41
61
|
if (raw != null) {
|
|
62
|
+
let parsed;
|
|
42
63
|
try {
|
|
43
|
-
|
|
44
|
-
hydratedCache.set(key, parsed);
|
|
45
|
-
setValue(parsed);
|
|
64
|
+
parsed = parse(raw);
|
|
46
65
|
} catch {
|
|
47
66
|
// Corrupt payload — fall back to the default and let the next
|
|
48
67
|
// write repair it.
|
|
49
68
|
}
|
|
69
|
+
if (parsed !== undefined) {
|
|
70
|
+
hydratedCache.set(key, parsed);
|
|
71
|
+
setValue(parsed);
|
|
72
|
+
}
|
|
50
73
|
}
|
|
51
74
|
setHydrated(true);
|
|
52
75
|
}).catch(() => {
|
|
@@ -60,9 +83,20 @@ export function usePersistedState(key, defaultValue) {
|
|
|
60
83
|
setValue(prev => {
|
|
61
84
|
const resolved = typeof next === "function" ? next(prev) : next;
|
|
62
85
|
hydratedCache.set(keyRef.current, resolved);
|
|
63
|
-
|
|
86
|
+
const storage = optionsRef.current.storage ?? persistentStorage;
|
|
87
|
+
const serialize = optionsRef.current.serialize ?? JSON.stringify;
|
|
88
|
+
try {
|
|
89
|
+
storage.setItem(keyRef.current, serialize(resolved)).catch(() => {});
|
|
90
|
+
} catch {
|
|
91
|
+
// Unserializable value — keep it in memory, skip the write.
|
|
92
|
+
}
|
|
64
93
|
return resolved;
|
|
65
94
|
});
|
|
66
95
|
}, []);
|
|
67
96
|
return [value, set, hydrated];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Test seam: forget every hydrated value. */
|
|
100
|
+
export function resetPersistedStateCache() {
|
|
101
|
+
hydratedCache.clear();
|
|
68
102
|
}
|