@designcrowd/library.brand-page 5.18.1-undo-32 → 5.18.1-undo-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/src/{CreatedOnBrandCrowd-de-de-DUgkmx89.js → CreatedOnBrandCrowd-de-de-D-2WhY-F.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-en-us-C5dQGLuA.js → CreatedOnBrandCrowd-en-us-B7ldKpJK.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-es-es-CjPcnNlq.js → CreatedOnBrandCrowd-es-es-BatQY8Tt.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-fr-fr-D2wnZOOE.js → CreatedOnBrandCrowd-fr-ca-CFivOJkN.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-fr-ca-D2wnZOOE.js → CreatedOnBrandCrowd-fr-fr-CFivOJkN.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-pt-pt-OQj-IBh_.js → CreatedOnBrandCrowd-pt-br-wHkXfYLP.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-pt-br-OQj-IBh_.js → CreatedOnBrandCrowd-pt-pt-wHkXfYLP.js} +1 -1
- package/lib/src/{CreatedOnDcom-de-de-DLijuIaL.js → CreatedOnDcom-de-de--HwniV-Y.js} +1 -1
- package/lib/src/{CreatedOnDcom-en-us-D9M_heGE.js → CreatedOnDcom-en-us-Bqm5NnRX.js} +1 -1
- package/lib/src/{CreatedOnDcom-es-es-PgWwbBLA.js → CreatedOnDcom-es-es-COGQIuEn.js} +1 -1
- package/lib/src/{CreatedOnDcom-fr-ca-BXiB-abj.js → CreatedOnDcom-fr-ca-eVDHO7MX.js} +1 -1
- package/lib/src/{CreatedOnDcom-fr-fr-BXhTmu7F.js → CreatedOnDcom-fr-fr-BhGKbdoG.js} +1 -1
- package/lib/src/{CreatedOnDcom-pt-br-3e6y567w.js → CreatedOnDcom-pt-br-5kDZKPZc.js} +1 -1
- package/lib/src/{CreatedOnDcom-pt-pt-GojOtHmO.js → CreatedOnDcom-pt-pt-CnTcVxct.js} +1 -1
- package/lib/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.js +2 -11
- package/lib/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.js +15 -47
- package/lib/src/brand-page-maker/stores/history/history-store.d.ts +7 -47
- package/lib/src/brand-page-maker/stores/history/history-store.js +98 -283
- package/lib/src/index.mjs +1 -1
- package/lib/src/{lib-DVs3jUit.js → lib-B5jbN-ro.js} +31 -44
- package/lib/src/shared/content-settings/components/GenericTextInput.mixin.d.ts +1 -1
- package/lib/src/shared/content-settings/components/content-settings/ContentSettings.mixin.js +1 -8
- package/lib/src/shared/content-settings/components/email-field/EmailField.mixin.d.ts +1 -1
- package/lib/src/shared/content-settings/components/payment-config-field/PaymentConfigField.mixin.js +2 -8
- package/lib/src/shared/content-settings/components/radio-field/RadioField.mixin.d.ts +1 -1
- package/lib/src/shared/content-settings/components/tel-field/TelField.mixin.d.ts +1 -1
- package/lib/src/shared/content-settings/components/text-field/TextField.mixin.d.ts +1 -1
- package/lib/src/shared/content-settings/components/textarea-field/TextareaField.mixin.d.ts +1 -1
- package/lib/src/shared/content-settings/components/url-field/UrlField.mixin.d.ts +1 -1
- package/lib/src/shared/content-settings/components/youtube-video-field/YoutubeVideoField.mixin.d.ts +1 -1
- package/lib/src/shared/logo-settings/store/editable-logo-store.d.ts +0 -18
- package/lib/src/shared/logo-settings/store/editable-logo-store.js +6 -24
- package/package.json +1 -1
- package/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.ts +2 -11
- package/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.ts +15 -47
- package/src/brand-page-maker/stores/history/history-store.test.ts +32 -0
- package/src/brand-page-maker/stores/history/history-store.ts +107 -340
- package/src/shared/content-settings/components/content-settings/ContentSettings.mixin.ts +1 -8
- package/src/shared/content-settings/components/page-dropdown-field/PageDropdownField.vue +1 -4
- package/src/shared/content-settings/components/payment-config-field/PaymentConfigField.mixin.ts +2 -8
- package/src/shared/logo-settings/store/editable-logo-store.ts +9 -29
|
@@ -6,57 +6,27 @@ import { useAiWebsiteBuilderThreadStore } from '../ai-thread/ai-website-builder-
|
|
|
6
6
|
import { useEventTracker } from '../../composables/useEventTracker';
|
|
7
7
|
import { usePageNavigationStore } from '@shared/page-navigation/store/page-navigation-store';
|
|
8
8
|
import { useUiEditableLogoStore } from '@shared/logo-settings/store/editable-logo-store';
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
*
|
|
12
|
-
* entries: [e0, e1, e2, e3, e4]
|
|
13
|
-
* ^
|
|
14
|
-
* currentIndex
|
|
15
|
-
*
|
|
16
|
-
* canUndo = currentIndex > 0
|
|
17
|
-
* canRedo = currentIndex < entries.length - 1
|
|
18
|
-
*
|
|
19
|
-
* capture → entries = entries.slice(0, currentIndex + 1); push; currentIndex = end
|
|
20
|
-
* undo → currentIndex--; applySnapshot(entries[currentIndex])
|
|
21
|
-
* redo → currentIndex++; applySnapshot(entries[currentIndex])
|
|
22
|
-
*
|
|
23
|
-
* This model:
|
|
24
|
-
* - eliminates the two-stack-desync risk class (mutating two refs in sequence)
|
|
25
|
-
* - makes FR-11 "clear redo on new change" a single array truncation
|
|
26
|
-
* - makes it trivial to build a V2 timeline UI (iterate entries, highlight
|
|
27
|
-
* currentIndex)
|
|
28
|
-
*/
|
|
29
|
-
// Collapses multi-step mutations into a single capture. Monotonic: first
|
|
30
|
-
// mutation starts the timer, further mutations inside the window do not
|
|
31
|
-
// reset it. A max-wait ceiling forces a commit during continuous activity
|
|
32
|
-
// so a user holding down a key for 15 s still gets periodic snapshots.
|
|
9
|
+
// Monotonic burst — first mutation starts the timer, subsequent mutations
|
|
10
|
+
// don't reset it; max-wait ceiling forces a commit during continuous typing.
|
|
33
11
|
const CAPTURE_DEBOUNCE_MS = 1700;
|
|
34
12
|
const CAPTURE_MAX_WAIT_MS = 10000;
|
|
35
|
-
// FR-12
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
// logo content blocks. The variations grid (~10 MB) does NOT live in the
|
|
40
|
-
// snapshot — it sits on the editable-logo store keyed by draft token, and
|
|
41
|
-
// is restored on undo from there. The 100 MB total budget still evicts
|
|
42
|
-
// oldest non-baseline entries to keep memory bounded.
|
|
13
|
+
// FR-12 / FR-13 / FR-13c. 25 MB per-entry accommodates a high-res BYO logo
|
|
14
|
+
// rendered into 4 viewport sections + per-page logo blocks. Oversize
|
|
15
|
+
// entries are skipped with telemetry; 100 MB total evicts oldest
|
|
16
|
+
// non-baseline entries.
|
|
43
17
|
const MAX_ENTRIES = 40;
|
|
44
18
|
const MAX_MEMORY_BYTES = 100 * 1024 * 1024;
|
|
45
19
|
const MAX_ENTRY_BYTES = 25 * 1024 * 1024;
|
|
46
|
-
// Window after baseline
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
// that would otherwise make undo light up before the user has done anything.
|
|
20
|
+
// Window after baseline during which captured mutations REPLACE the
|
|
21
|
+
// baseline instead of pushing — covers async post-init cascades
|
|
22
|
+
// (template load, router afterEach, layout settle).
|
|
50
23
|
const BASELINE_SETTLE_MS = 2000;
|
|
51
24
|
export const useHistoryStore = defineStore('history', () => {
|
|
52
25
|
const eventTracker = useEventTracker();
|
|
53
26
|
// --- State ---
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
* object inside. Combined with string snapshots, entries are fully opaque to
|
|
58
|
-
* reactivity traversal — which is what we want for a history log.
|
|
59
|
-
*/
|
|
27
|
+
// shallowRef keeps the array reactive but prevents Pinia from proxying
|
|
28
|
+
// every entry — combined with stringified snapshots, entries are
|
|
29
|
+
// fully opaque to reactivity traversal.
|
|
60
30
|
const entries = shallowRef([]);
|
|
61
31
|
const currentIndex = ref(-1); // -1 means "no baseline yet"
|
|
62
32
|
const isApplyingSnapshot = ref(false);
|
|
@@ -67,24 +37,10 @@ export const useHistoryStore = defineStore('history', () => {
|
|
|
67
37
|
const canUndo = computed(() => currentIndex.value > 0);
|
|
68
38
|
const canRedo = computed(() => currentIndex.value >= 0 && currentIndex.value < entries.value.length - 1);
|
|
69
39
|
// --- Internals ---
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
let cachedMakerStore = null;
|
|
75
|
-
/**
|
|
76
|
-
* Walk `brandPageData` and null `logoImageBase64Url` on every logo
|
|
77
|
-
* placement (header/footer × desktop/mobile + per-page logo content).
|
|
78
|
-
* Pre-purchase only — the same gate the save path uses
|
|
79
|
-
* (see `brand-page-container-store.ts:805`).
|
|
80
|
-
*
|
|
81
|
-
* Each rendered base64 is ~2-4 MB and the brand-site templates put the
|
|
82
|
-
* same logo in 6-8 places, so unstripped pre-purchase snapshots run
|
|
83
|
-
* 18-24 MB and bump the 25 MB per-entry cap, which silently drops the
|
|
84
|
-
* entry. The base64 is repopulated on undo by `_syncLogoSettingsFromSnapshot`
|
|
85
|
-
* (designed: re-issue fabric-render; BYO: stamp from per-token cache),
|
|
86
|
-
* so the snapshot doesn't need to carry it.
|
|
87
|
-
*/
|
|
40
|
+
// Pre-purchase only (matches the save-path strip in
|
|
41
|
+
// `brand-page-container-store.ts:805`). Each rendered base64 is ~2-4 MB
|
|
42
|
+
// across 6-8 placements, so unstripped snapshots bump the 25 MB cap.
|
|
43
|
+
// `_syncLogoSettingsFromSnapshot` repopulates on undo.
|
|
88
44
|
function stripLogoBase64IfPrePurchase(data) {
|
|
89
45
|
try {
|
|
90
46
|
if (!useBrandPageContainerStore().isPrePurchase)
|
|
@@ -119,16 +75,9 @@ export const useHistoryStore = defineStore('history', () => {
|
|
|
119
75
|
}
|
|
120
76
|
}
|
|
121
77
|
}
|
|
122
|
-
/**
|
|
123
|
-
* Serialize the current `brandPageData` into a detached JSON string.
|
|
124
|
-
* Synchronous — safe to call inside a microtask without racing a follow-up
|
|
125
|
-
* mutation.
|
|
126
|
-
*/
|
|
127
78
|
function serializeCurrentSnapshot() {
|
|
128
|
-
if (!cachedMakerStore)
|
|
129
|
-
return null;
|
|
130
79
|
try {
|
|
131
|
-
const cloned = JSON.parse(JSON.stringify(toRaw(
|
|
80
|
+
const cloned = JSON.parse(JSON.stringify(toRaw(useBrandPageMakerStore().brandPageData)));
|
|
132
81
|
stripLogoBase64IfPrePurchase(cloned);
|
|
133
82
|
return JSON.stringify(cloned);
|
|
134
83
|
}
|
|
@@ -137,19 +86,9 @@ export const useHistoryStore = defineStore('history', () => {
|
|
|
137
86
|
return null;
|
|
138
87
|
}
|
|
139
88
|
}
|
|
140
|
-
/**
|
|
141
|
-
* Read the currently active pageId from `pageNavigationStore`. Sidecar to
|
|
142
|
-
* the snapshot string — see `HistoryEntry.activePageId`. Empty string when
|
|
143
|
-
* no page is active yet.
|
|
144
|
-
*/
|
|
145
89
|
function getCurrentActivePageId() {
|
|
146
90
|
return usePageNavigationStore().activePageId ?? '';
|
|
147
91
|
}
|
|
148
|
-
/**
|
|
149
|
-
* Serialize the current `editableLogoStore.logoEdits` as a sidecar to the
|
|
150
|
-
* snapshot — see `HistoryEntry.logoEdits`. Returns undefined when the
|
|
151
|
-
* editable-logo store hasn't initialized yet (no logo tab interaction).
|
|
152
|
-
*/
|
|
153
92
|
function serializeCurrentLogoEdits() {
|
|
154
93
|
try {
|
|
155
94
|
const editableLogoStore = useUiEditableLogoStore();
|
|
@@ -160,65 +99,28 @@ export const useHistoryStore = defineStore('history', () => {
|
|
|
160
99
|
return undefined;
|
|
161
100
|
}
|
|
162
101
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
* flag set immediately before an apply to swallow the post-apply
|
|
181
|
-
* watcher tick), or
|
|
182
|
-
* - `reset()` clears it on save.
|
|
183
|
-
*
|
|
184
|
-
* Multiple call sites in the same burst all set the same flag — they
|
|
185
|
-
* collapse together into a single merge into the current head, which is
|
|
186
|
-
* the semantics we want.
|
|
187
|
-
*/
|
|
188
|
-
const mergeNextFlush = ref(false);
|
|
189
|
-
/**
|
|
190
|
-
* Commit a new entry into the stack at the current pointer position.
|
|
191
|
-
*
|
|
192
|
-
* Returns 'committed' when the entry lands in the stack, 'oversized' when
|
|
193
|
-
* the entry exceeds `MAX_ENTRY_BYTES` and is skipped (FR-13c), or 'noop'
|
|
194
|
-
* when the snapshot is byte-identical to the entry at the current pointer.
|
|
195
|
-
* Callers can use the return to emit telemetry — analytics wiring lives
|
|
196
|
-
* in U5.
|
|
197
|
-
*
|
|
198
|
-
* Enforces these invariants in order:
|
|
199
|
-
* 1. Drop no-op duplicates — if the new snapshot is identical to the
|
|
200
|
-
* entry at the current pointer, skip it. Color/text settle paths,
|
|
201
|
-
* template-switch confirms with no real diff, and AI applies that
|
|
202
|
-
* land identical state would otherwise create undo/redo steps that
|
|
203
|
-
* do nothing visible.
|
|
204
|
-
* 2. FR-13c — if the new entry exceeds 5 MB, skip it entirely.
|
|
205
|
-
* 3. FR-11 — truncate any redo tail.
|
|
206
|
-
* 4. FR-12 — cap at 40 entries; drop oldest non-baseline on overflow.
|
|
207
|
-
* 5. FR-13 — keep total memory under 100 MB; drop oldest non-baseline
|
|
208
|
-
* repeatedly until we're under budget.
|
|
209
|
-
*
|
|
210
|
-
* Baseline (entry 0) is never evicted so "undo everything" always returns
|
|
211
|
-
* to session start.
|
|
212
|
-
*/
|
|
102
|
+
// Two-phase merge flag. Hydration write sites call `mergeNextMutation()`
|
|
103
|
+
// immediately before their imperative write, which sets `mergeArmed`.
|
|
104
|
+
// When the watcher's `scheduleAutoCapture` arms a new idle timer, it
|
|
105
|
+
// transfers `mergeArmed` into `pendingTimerIsMerge`. The flush then
|
|
106
|
+
// consumes `pendingTimerIsMerge` and merges into the current head
|
|
107
|
+
// instead of pushing.
|
|
108
|
+
//
|
|
109
|
+
// The microtask cleanup on `mergeArmed` prevents stale flags surviving
|
|
110
|
+
// when the user navigates away (e.g. sidebar tab switch) before the
|
|
111
|
+
// flagged write actually happens — a previously-set arm is dropped
|
|
112
|
+
// synchronously after the current tick if no watcher fired.
|
|
113
|
+
const mergeArmed = ref(false);
|
|
114
|
+
let pendingTimerIsMerge = false;
|
|
115
|
+
// Returns 'noop' when no-change vs current head, 'oversized' when over
|
|
116
|
+
// MAX_ENTRY_BYTES, else 'committed'. Truncates the redo tail, caps at
|
|
117
|
+
// MAX_ENTRIES, evicts oldest non-baseline to stay under MAX_MEMORY_BYTES.
|
|
118
|
+
// Baseline (index 0) is never evicted.
|
|
213
119
|
function commitEntry(args) {
|
|
214
120
|
const current = entries.value[currentIndex.value];
|
|
215
|
-
// Compare both
|
|
216
|
-
//
|
|
217
|
-
//
|
|
218
|
-
// The accompanying base64 writeback is also stripped from the
|
|
219
|
-
// snapshot (size cap), so a pure logo edit would produce a
|
|
220
|
-
// byte-identical snapshot to the previous entry — without the
|
|
221
|
-
// sidecar comparison every logo edit would dedup as a no-op.
|
|
121
|
+
// Compare both snapshot AND logoEdits sidecar — pre-purchase logo
|
|
122
|
+
// edits don't change brandPageData (base64 is stripped) so the
|
|
123
|
+
// sidecar is the only signal that distinguishes them.
|
|
222
124
|
if (current &&
|
|
223
125
|
current.snapshot === args.snapshot &&
|
|
224
126
|
current.logoEdits === args.logoEdits) {
|
|
@@ -271,26 +173,8 @@ export const useHistoryStore = defineStore('history', () => {
|
|
|
271
173
|
}
|
|
272
174
|
return 'committed';
|
|
273
175
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
*
|
|
277
|
-
* Debounce strategy (FR-16 / ADR-005):
|
|
278
|
-
* - Monotonic — the first mutation starts the idle timer; subsequent
|
|
279
|
-
* mutations inside the window DO NOT reset it. This is what collapses
|
|
280
|
-
* e.g. an RTE italic click's sync style write + ~100 ms-later HTML
|
|
281
|
-
* update into a single capture.
|
|
282
|
-
* - Idle flush — the idle timer fires CAPTURE_DEBOUNCE_MS after the
|
|
283
|
-
* first mutation in a burst (2 s by default).
|
|
284
|
-
* - Max-wait ceiling — if mutations keep arriving and the idle timer
|
|
285
|
-
* keeps being "one more edit away", a second timer forces a commit at
|
|
286
|
-
* CAPTURE_MAX_WAIT_MS after the burst started (10 s by default). This
|
|
287
|
-
* is what keeps a user holding down a key from losing undo resolution.
|
|
288
|
-
*
|
|
289
|
-
* Both timers clear after a commit fires, so the next mutation starts a
|
|
290
|
-
* fresh burst.
|
|
291
|
-
*/
|
|
292
|
-
// Hoisted so force-commit paths (template switch, AI apply) can cancel any
|
|
293
|
-
// pending auto-capture that would otherwise duplicate their entry.
|
|
176
|
+
// Hoisted so force-commit paths (template switch, AI apply) can cancel
|
|
177
|
+
// any pending auto-capture that would otherwise duplicate their entry.
|
|
294
178
|
let idleTimerId = null;
|
|
295
179
|
let maxWaitTimerId = null;
|
|
296
180
|
function cancelPendingAutoCapture() {
|
|
@@ -306,7 +190,6 @@ export const useHistoryStore = defineStore('history', () => {
|
|
|
306
190
|
function installWatcher() {
|
|
307
191
|
if (watcherInstalled.value)
|
|
308
192
|
return;
|
|
309
|
-
cachedMakerStore = useBrandPageMakerStore();
|
|
310
193
|
const flush = () => {
|
|
311
194
|
cancelPendingAutoCapture();
|
|
312
195
|
if (isApplyingSnapshot.value)
|
|
@@ -318,28 +201,16 @@ export const useHistoryStore = defineStore('history', () => {
|
|
|
318
201
|
return;
|
|
319
202
|
const activePageId = getCurrentActivePageId();
|
|
320
203
|
const logoEdits = serializeCurrentLogoEdits();
|
|
321
|
-
//
|
|
322
|
-
//
|
|
323
|
-
//
|
|
324
|
-
// mutate `brandPageData` after baseline is taken — without this they
|
|
325
|
-
// would each look like an undoable user edit on first paint. The
|
|
326
|
-
// baseline's `activePageId` is also overwritten so it tracks where
|
|
327
|
-
// the user actually settled (the router's `afterEach` runs inside
|
|
328
|
-
// this window, so the post-init `activePageId` is more accurate
|
|
329
|
-
// than the pre-router value captured in `captureBaseline`).
|
|
204
|
+
// Settle window: fold post-init reactive cascades back into the
|
|
205
|
+
// baseline so undo doesn't light up before the user has done
|
|
206
|
+
// anything.
|
|
330
207
|
const inSettleWindow = currentIndex.value === 0 &&
|
|
331
208
|
Date.now() - baselineCapturedAt < BASELINE_SETTLE_MS;
|
|
332
|
-
// Merge
|
|
333
|
-
//
|
|
334
|
-
//
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
// not we're in the settle window — `inSettleWindow` already merges
|
|
338
|
-
// into the baseline, but past that window the same merge has to
|
|
339
|
-
// happen against the current head for hydrations that fire when the
|
|
340
|
-
// user clicks into a section after the settle elapsed.
|
|
341
|
-
const shouldMerge = mergeNextFlush.value;
|
|
342
|
-
mergeNextFlush.value = false;
|
|
209
|
+
// Merge: the timer that fired this flush was armed during a
|
|
210
|
+
// hydration burst — replace the current head in place instead of
|
|
211
|
+
// pushing. Consumed once.
|
|
212
|
+
const shouldMerge = pendingTimerIsMerge;
|
|
213
|
+
pendingTimerIsMerge = false;
|
|
343
214
|
if (inSettleWindow) {
|
|
344
215
|
entries.value = [
|
|
345
216
|
{
|
|
@@ -374,31 +245,32 @@ export const useHistoryStore = defineStore('history', () => {
|
|
|
374
245
|
logoEdits,
|
|
375
246
|
});
|
|
376
247
|
};
|
|
377
|
-
// Monotonic
|
|
378
|
-
//
|
|
379
|
-
//
|
|
380
|
-
//
|
|
381
|
-
//
|
|
382
|
-
//
|
|
248
|
+
// Monotonic — first mutation arms; subsequent ones inside the window
|
|
249
|
+
// don't reset. Gated on isApplyingSnapshot so undo/redo don't leave
|
|
250
|
+
// a stale timer ticking. If a hydration site armed `mergeArmed`
|
|
251
|
+
// immediately before the mutation that triggered this run, transfer
|
|
252
|
+
// the intent into `pendingTimerIsMerge` so the resulting flush
|
|
253
|
+
// merges instead of commits.
|
|
383
254
|
const scheduleAutoCapture = () => {
|
|
384
255
|
if (isApplyingSnapshot.value)
|
|
385
256
|
return;
|
|
386
257
|
if (idleTimerId !== null)
|
|
387
258
|
return;
|
|
259
|
+
if (mergeArmed.value) {
|
|
260
|
+
pendingTimerIsMerge = true;
|
|
261
|
+
mergeArmed.value = false;
|
|
262
|
+
}
|
|
388
263
|
idleTimerId = setTimeout(flush, CAPTURE_DEBOUNCE_MS);
|
|
389
264
|
maxWaitTimerId = setTimeout(flush, CAPTURE_MAX_WAIT_MS);
|
|
390
265
|
};
|
|
391
|
-
watch(() =>
|
|
266
|
+
watch(() => useBrandPageMakerStore().brandPageData, scheduleAutoCapture, {
|
|
392
267
|
deep: true,
|
|
393
268
|
});
|
|
394
269
|
watcherInstalled.value = true;
|
|
395
270
|
}
|
|
396
271
|
// --- Actions ---
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
* `BrandPageMaker.mixin.ts` immediately after `setIsBrandPageLoaded(true)`.
|
|
400
|
-
* Installs the reactive watcher so subsequent mutations auto-capture.
|
|
401
|
-
*/
|
|
272
|
+
// Called once per session by BrandPageMaker.mixin.ts after
|
|
273
|
+
// setIsBrandPageLoaded(true). Installs the watcher.
|
|
402
274
|
function captureBaseline() {
|
|
403
275
|
if (baselineCaptured.value)
|
|
404
276
|
return;
|
|
@@ -406,7 +278,6 @@ export const useHistoryStore = defineStore('history', () => {
|
|
|
406
278
|
const snapshot = serializeCurrentSnapshot();
|
|
407
279
|
if (!snapshot)
|
|
408
280
|
return;
|
|
409
|
-
// Baseline starts a fresh stack of exactly 1 entry with pointer at 0.
|
|
410
281
|
entries.value = [
|
|
411
282
|
{
|
|
412
283
|
snapshot,
|
|
@@ -423,13 +294,7 @@ export const useHistoryStore = defineStore('history', () => {
|
|
|
423
294
|
baselineCaptured.value = true;
|
|
424
295
|
baselineCapturedAt = Date.now();
|
|
425
296
|
}
|
|
426
|
-
|
|
427
|
-
* Force-commit capture used by non-watcher call sites (template switch,
|
|
428
|
-
* AI apply). The watcher path commits atomically on its own.
|
|
429
|
-
*
|
|
430
|
-
* Returns the `commitEntry` result so force-commit callers can log
|
|
431
|
-
* oversized telemetry — analytics wiring lives in U5.
|
|
432
|
-
*/
|
|
297
|
+
// Force-commit for non-watcher call sites (template switch, AI apply).
|
|
433
298
|
function capture(args) {
|
|
434
299
|
if (isApplyingSnapshot.value)
|
|
435
300
|
return 'skipped';
|
|
@@ -445,47 +310,22 @@ export const useHistoryStore = defineStore('history', () => {
|
|
|
445
310
|
...args,
|
|
446
311
|
});
|
|
447
312
|
}
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
* so the template-switch change lands as exactly one entry (not a duplicate
|
|
451
|
-
* with the watcher's eventual flush).
|
|
452
|
-
*/
|
|
313
|
+
// Cancel pending auto-capture so the forced entry doesn't duplicate
|
|
314
|
+
// with the watcher's eventual flush.
|
|
453
315
|
function captureTemplateSwitch(label) {
|
|
454
316
|
cancelPendingAutoCapture();
|
|
455
317
|
return capture({ source: 'template_switch', actor: 'user', label });
|
|
456
318
|
}
|
|
457
|
-
/**
|
|
458
|
-
* Force-commit an `ai_changeset` entry. Cancels any pending auto-capture
|
|
459
|
-
* so the AI apply lands as exactly one entry per prompt (US-E1).
|
|
460
|
-
*/
|
|
461
319
|
function captureAiChangeset(label) {
|
|
462
320
|
cancelPendingAutoCapture();
|
|
463
321
|
return capture({ source: 'ai_changeset', actor: 'ai_builder', label });
|
|
464
322
|
}
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
* watcher guard suppresses the cascade of mutations from being
|
|
472
|
-
* re-captured as a spurious duplicate entry.
|
|
473
|
-
*
|
|
474
|
-
* Active-page resolution priority:
|
|
475
|
-
* 1. Diff against `cameFromIndex` — any page present in the target
|
|
476
|
-
* snapshot but absent from the source snapshot is being "restored"
|
|
477
|
-
* by this transition. Navigate there. Covers undo-of-delete (lands
|
|
478
|
-
* on the deleted page) and redo-of-add (lands on the newly-added
|
|
479
|
-
* page) — both of which the user expects to "follow" the change.
|
|
480
|
-
* Without this, undo of a page-delete would resolve via walk-back
|
|
481
|
-
* to whatever page the user was on at the previous baseline, not
|
|
482
|
-
* the page they just un-deleted.
|
|
483
|
-
* 2. Walk back from `targetIndex` to find the closest entry whose
|
|
484
|
-
* captured `activePageId` still exists in the restored snapshot.
|
|
485
|
-
* Covers undo-of-add (falls back to the page the user was on
|
|
486
|
-
* before they added the new one) and any non-page-structural
|
|
487
|
-
* edits.
|
|
488
|
-
*/
|
|
323
|
+
// Active-page resolution priority:
|
|
324
|
+
// 1. Diff `cameFromIndex` against target — pages present in target
|
|
325
|
+
// but absent from source are being restored/added; navigate there
|
|
326
|
+
// (undo-of-delete, redo-of-add).
|
|
327
|
+
// 2. Walk back from targetIndex for the closest entry whose
|
|
328
|
+
// activePageId still exists (undo-of-add and non-page edits).
|
|
489
329
|
async function applySnapshotString(snapshotStr, targetIndex, cameFromIndex) {
|
|
490
330
|
const brandPageMakerStore = useBrandPageMakerStore();
|
|
491
331
|
let snapshot;
|
|
@@ -502,9 +342,7 @@ export const useHistoryStore = defineStore('history', () => {
|
|
|
502
342
|
restoredPageIds.add(p.id);
|
|
503
343
|
});
|
|
504
344
|
let resolvedActivePageId;
|
|
505
|
-
// Step 1: diff
|
|
506
|
-
// the target snapshot whose id is absent from the source is the page
|
|
507
|
-
// being restored/added by this apply.
|
|
345
|
+
// Step 1: diff cameFromIndex → first page absent from source wins.
|
|
508
346
|
const sourceEntry = entries.value[cameFromIndex];
|
|
509
347
|
if (sourceEntry) {
|
|
510
348
|
const sourcePageIds = new Set();
|
|
@@ -525,9 +363,7 @@ export const useHistoryStore = defineStore('history', () => {
|
|
|
525
363
|
// Source snapshot unparseable — fall through to walk-back.
|
|
526
364
|
}
|
|
527
365
|
}
|
|
528
|
-
// Step 2: walk-back fallback
|
|
529
|
-
// applies (style/content edits) or when the diff produced nothing
|
|
530
|
-
// viable.
|
|
366
|
+
// Step 2: walk-back fallback for non-page-structural edits.
|
|
531
367
|
if (!resolvedActivePageId) {
|
|
532
368
|
for (let i = targetIndex; i >= 0; i--) {
|
|
533
369
|
const candidate = entries.value[i]?.activePageId;
|
|
@@ -539,11 +375,10 @@ export const useHistoryStore = defineStore('history', () => {
|
|
|
539
375
|
}
|
|
540
376
|
const targetEntry = entries.value[targetIndex];
|
|
541
377
|
isApplyingSnapshot.value = true;
|
|
542
|
-
//
|
|
543
|
-
//
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
mergeNextFlush.value = false;
|
|
378
|
+
// Clear any unconsumed merge intent — a stale flag would fold the
|
|
379
|
+
// post-apply tick into the restored entry and erase the redo.
|
|
380
|
+
mergeArmed.value = false;
|
|
381
|
+
pendingTimerIsMerge = false;
|
|
547
382
|
cancelPendingAutoCapture();
|
|
548
383
|
try {
|
|
549
384
|
await brandPageMakerStore.applyHistorySnapshot(snapshot, resolvedActivePageId, targetEntry?.logoEdits);
|
|
@@ -552,14 +387,13 @@ export const useHistoryStore = defineStore('history', () => {
|
|
|
552
387
|
isApplyingSnapshot.value = false;
|
|
553
388
|
}
|
|
554
389
|
}
|
|
555
|
-
/** Move the pointer back one step and apply the entry at the new position. */
|
|
556
390
|
async function undo() {
|
|
557
391
|
if (!canUndo.value)
|
|
558
392
|
return;
|
|
559
393
|
const aiThreadStore = useAiWebsiteBuilderThreadStore();
|
|
560
394
|
if (aiThreadStore.isProcessing)
|
|
561
395
|
return;
|
|
562
|
-
//
|
|
396
|
+
// Actor of the entry being undone (the one we're leaving).
|
|
563
397
|
const undoneActor = entries.value[currentIndex.value]?.actor;
|
|
564
398
|
const cameFromIndex = currentIndex.value;
|
|
565
399
|
currentIndex.value -= 1;
|
|
@@ -572,7 +406,6 @@ export const useHistoryStore = defineStore('history', () => {
|
|
|
572
406
|
actor: undoneActor,
|
|
573
407
|
});
|
|
574
408
|
}
|
|
575
|
-
/** Move the pointer forward one step and apply the entry at the new position. */
|
|
576
409
|
async function redo() {
|
|
577
410
|
if (!canRedo.value)
|
|
578
411
|
return;
|
|
@@ -585,67 +418,49 @@ export const useHistoryStore = defineStore('history', () => {
|
|
|
585
418
|
if (target) {
|
|
586
419
|
await applySnapshotString(target.snapshot, currentIndex.value, cameFromIndex);
|
|
587
420
|
}
|
|
588
|
-
// Actor of the entry we're moving onto — that's the change being re-applied.
|
|
589
421
|
eventTracker.sendRedoEvent({
|
|
590
422
|
entries_count: entries.value.length,
|
|
591
423
|
actor: target?.actor,
|
|
592
424
|
});
|
|
593
425
|
}
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
* PRD §3 Non-Goals: undo history resets on save/publish — the saved state
|
|
598
|
-
* becomes the new clean baseline so a user can't accidentally undo back
|
|
599
|
-
* past a save. Called by `brand-page-container-store.saveBrandPage` on a
|
|
600
|
-
* successful save (covers both Save Progress and Publish, since publish
|
|
601
|
-
* routes through `saveBrandPage`).
|
|
602
|
-
*
|
|
603
|
-
* No-ops when no baseline has ever been captured — the undo/redo feature
|
|
604
|
-
* is gated by `userStore.features.canUseUndoRedo` (see
|
|
605
|
-
* BrandPageMaker.mixin.ts), and only flagged sessions install the watcher
|
|
606
|
-
* + capture a baseline. Without this guard, calling reset() in an
|
|
607
|
-
* unflagged session would install the watcher and start eating memory
|
|
608
|
-
* for snapshots the user can never see.
|
|
609
|
-
*
|
|
610
|
-
* Re-runs `captureBaseline` synchronously so the next mutation lands as
|
|
611
|
-
* an actual undoable edit rather than disappearing into a fresh settle
|
|
612
|
-
* window. The watcher stays installed; `installWatcher` early-returns
|
|
613
|
-
* if `watcherInstalled` is already true.
|
|
614
|
-
*/
|
|
426
|
+
// PRD §3: undo history resets on save/publish. Called by
|
|
427
|
+
// brand-page-container-store.saveBrandPage. No-ops when no baseline
|
|
428
|
+
// exists — unflagged sessions never installed the watcher.
|
|
615
429
|
function reset() {
|
|
616
430
|
if (!baselineCaptured.value)
|
|
617
431
|
return;
|
|
618
432
|
cancelPendingAutoCapture();
|
|
619
|
-
|
|
433
|
+
mergeArmed.value = false;
|
|
434
|
+
pendingTimerIsMerge = false;
|
|
620
435
|
entries.value = [];
|
|
621
436
|
currentIndex.value = -1;
|
|
622
437
|
baselineCaptured.value = false;
|
|
623
438
|
baselineCapturedAt = 0;
|
|
624
439
|
captureBaseline();
|
|
625
440
|
}
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
* flag and collapse together into a single merge into the current head.
|
|
635
|
-
*
|
|
636
|
-
* No-op when the watcher hasn't yet baselined: hydration writes inside
|
|
637
|
-
* the settle window already merge into the baseline; outside a flagged
|
|
638
|
-
* session the flag has no observable effect.
|
|
639
|
-
*/
|
|
441
|
+
// Hydration write sites call this immediately before their imperative
|
|
442
|
+
// mutation. The arm is auto-cleared on the next Vue tick — `nextTick`
|
|
443
|
+
// runs AFTER all queued reactive effects (including the watcher), so
|
|
444
|
+
// if the hydration write triggered `scheduleAutoCapture`, the arm has
|
|
445
|
+
// already been transferred into `pendingTimerIsMerge` before this
|
|
446
|
+
// cleanup fires. If no mutation followed (e.g. the user navigated
|
|
447
|
+
// away before the flagged write), the cleanup drops the stale arm so
|
|
448
|
+
// it can't be consumed by an unrelated future mutation.
|
|
640
449
|
function mergeNextMutation() {
|
|
641
|
-
|
|
450
|
+
mergeArmed.value = true;
|
|
451
|
+
// setTimeout(0) is a macrotask — runs AFTER the synchronous write
|
|
452
|
+
// and AFTER Vue's microtask flush of the watcher's reactive effect.
|
|
453
|
+
// If `scheduleAutoCapture` fired in between (the imperative write
|
|
454
|
+
// triggered the watcher), the arm has already been transferred into
|
|
455
|
+
// `pendingTimerIsMerge` before this cleanup runs.
|
|
456
|
+
setTimeout(() => {
|
|
457
|
+
mergeArmed.value = false;
|
|
458
|
+
}, 0);
|
|
642
459
|
}
|
|
643
460
|
// --- Instrumentation (NFR-04a) ---
|
|
644
|
-
/** Sum of all entries' serialized sizes in bytes. */
|
|
645
461
|
function getCurrentSizeBytes() {
|
|
646
462
|
return entries.value.reduce((sum, e) => sum + e.sizeBytes, 0);
|
|
647
463
|
}
|
|
648
|
-
/** Total number of entries currently in the stack. */
|
|
649
464
|
function getCurrentEntryCount() {
|
|
650
465
|
return entries.value.length;
|
|
651
466
|
}
|
package/lib/src/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as E, a as T, b as s, c as I, d as S, e as N, f as e, B as A, g as O, h as C, i as G, j as D, k as P, l as t, m as R, n as L, o as M, p as o, q as r, r as b, s as n, t as F, u as i, v as l, w as g, x as B, y as c, z as Y, C as U, D as d, E as u, F as H, G as p, H as y, I as K, J as k, K as m, L as V, M as X, N as h, O as v, P as W, Q as f, R as x, S as z, T as w, U as Z, V as j, W as Q, X as q, Y as J, Z as $, $ as aa, a0 as _a, a1 as Ea, a2 as Ta, a3 as sa, a4 as Ia, a5 as Sa, a6 as Na, a7 as ea, a8 as Aa, a9 as Oa, aa as Ca, ab as Ga, ac as Da, ad as Pa, ae as ta, af as Ra, ag as La, ah as Ma, ai as oa, aj as ra, ak as ba, al as na, am as Fa, an as ia, ao as la, ap as ga, aq as Ba, ar as ca, as as Ya, at as Ua, au as da, av as ua, aw as Ha, ax as pa, ay as ya, az as Ka, aA as ka, aB as ma, aC as Va, aD as Xa, aE as ha, aF as va, aG as Wa, aH as fa, aI as xa, aJ as za, aK as wa, aL as Za, aM as ja, aN as Qa, aO as qa, aP as Ja, aQ as $a, aR as a_, aS as __, aT as E_, aU as T_, aV as s_, aW as I_, aX as S_, aY as N_, aZ as e_, a_ as A_, a$ as O_, b0 as C_, b1 as G_, b2 as D_, b3 as P_, b4 as t_, b5 as R_, b6 as L_, b7 as M_, b8 as o_, b9 as r_, ba as b_, bb as n_, bc as F_, bd as i_, be as l_, bf as g_, bg as B_, bh as c_, bi as Y_, bj as U_, bk as d_, bl as u_, bm as H_, bn as p_, bo as y_, bp as K_, bq as k_, br as m_, bs as V_, bt as X_, bu as h_, bv as v_, bw as W_, bx as f_, by as x_, bz as z_, bA as w_, bB as Z_, bC as j_, bD as Q_, bE as q_, bF as J_, bG as $_, bH as aE, bI as _E, bJ as EE, bK as TE, bL as sE, bM as IE, bN as SE, bO as NE, bP as eE, bQ as AE, bR as OE, bS as CE, bT as GE, bU as DE, bV as PE, bW as tE, bX as RE, bY as LE, bZ as ME, b_ as oE, b$ as rE, c0 as bE, c1 as nE, c2 as FE, c3 as iE, c4 as lE, c5 as gE, c6 as BE, c7 as cE, c8 as YE, c9 as UE, ca as dE, cb as uE, cc as HE, cd as pE, ce as yE, cf as KE, cg as kE, ch as mE, ci as VE, cj as XE, ck as hE, cl as vE, cm as WE, cn as fE, co as xE } from "./lib-
|
|
1
|
+
import { A as E, a as T, b as s, c as I, d as S, e as N, f as e, B as A, g as O, h as C, i as G, j as D, k as P, l as t, m as R, n as L, o as M, p as o, q as r, r as b, s as n, t as F, u as i, v as l, w as g, x as B, y as c, z as Y, C as U, D as d, E as u, F as H, G as p, H as y, I as K, J as k, K as m, L as V, M as X, N as h, O as v, P as W, Q as f, R as x, S as z, T as w, U as Z, V as j, W as Q, X as q, Y as J, Z as $, $ as aa, a0 as _a, a1 as Ea, a2 as Ta, a3 as sa, a4 as Ia, a5 as Sa, a6 as Na, a7 as ea, a8 as Aa, a9 as Oa, aa as Ca, ab as Ga, ac as Da, ad as Pa, ae as ta, af as Ra, ag as La, ah as Ma, ai as oa, aj as ra, ak as ba, al as na, am as Fa, an as ia, ao as la, ap as ga, aq as Ba, ar as ca, as as Ya, at as Ua, au as da, av as ua, aw as Ha, ax as pa, ay as ya, az as Ka, aA as ka, aB as ma, aC as Va, aD as Xa, aE as ha, aF as va, aG as Wa, aH as fa, aI as xa, aJ as za, aK as wa, aL as Za, aM as ja, aN as Qa, aO as qa, aP as Ja, aQ as $a, aR as a_, aS as __, aT as E_, aU as T_, aV as s_, aW as I_, aX as S_, aY as N_, aZ as e_, a_ as A_, a$ as O_, b0 as C_, b1 as G_, b2 as D_, b3 as P_, b4 as t_, b5 as R_, b6 as L_, b7 as M_, b8 as o_, b9 as r_, ba as b_, bb as n_, bc as F_, bd as i_, be as l_, bf as g_, bg as B_, bh as c_, bi as Y_, bj as U_, bk as d_, bl as u_, bm as H_, bn as p_, bo as y_, bp as K_, bq as k_, br as m_, bs as V_, bt as X_, bu as h_, bv as v_, bw as W_, bx as f_, by as x_, bz as z_, bA as w_, bB as Z_, bC as j_, bD as Q_, bE as q_, bF as J_, bG as $_, bH as aE, bI as _E, bJ as EE, bK as TE, bL as sE, bM as IE, bN as SE, bO as NE, bP as eE, bQ as AE, bR as OE, bS as CE, bT as GE, bU as DE, bV as PE, bW as tE, bX as RE, bY as LE, bZ as ME, b_ as oE, b$ as rE, c0 as bE, c1 as nE, c2 as FE, c3 as iE, c4 as lE, c5 as gE, c6 as BE, c7 as cE, c8 as YE, c9 as UE, ca as dE, cb as uE, cc as HE, cd as pE, ce as yE, cf as KE, cg as kE, ch as mE, ci as VE, cj as XE, ck as hE, cl as vE, cm as WE, cn as fE, co as xE } from "./lib-B5jbN-ro.js";
|
|
2
2
|
import { createPinia as wE, setActivePinia as ZE } from "pinia";
|
|
3
3
|
export {
|
|
4
4
|
E as ABOUT_SECTION_SETTINGS_CONFIG,
|