@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.
Files changed (41) hide show
  1. package/lib/src/{CreatedOnBrandCrowd-de-de-DUgkmx89.js → CreatedOnBrandCrowd-de-de-D-2WhY-F.js} +1 -1
  2. package/lib/src/{CreatedOnBrandCrowd-en-us-C5dQGLuA.js → CreatedOnBrandCrowd-en-us-B7ldKpJK.js} +1 -1
  3. package/lib/src/{CreatedOnBrandCrowd-es-es-CjPcnNlq.js → CreatedOnBrandCrowd-es-es-BatQY8Tt.js} +1 -1
  4. package/lib/src/{CreatedOnBrandCrowd-fr-fr-D2wnZOOE.js → CreatedOnBrandCrowd-fr-ca-CFivOJkN.js} +1 -1
  5. package/lib/src/{CreatedOnBrandCrowd-fr-ca-D2wnZOOE.js → CreatedOnBrandCrowd-fr-fr-CFivOJkN.js} +1 -1
  6. package/lib/src/{CreatedOnBrandCrowd-pt-pt-OQj-IBh_.js → CreatedOnBrandCrowd-pt-br-wHkXfYLP.js} +1 -1
  7. package/lib/src/{CreatedOnBrandCrowd-pt-br-OQj-IBh_.js → CreatedOnBrandCrowd-pt-pt-wHkXfYLP.js} +1 -1
  8. package/lib/src/{CreatedOnDcom-de-de-DLijuIaL.js → CreatedOnDcom-de-de--HwniV-Y.js} +1 -1
  9. package/lib/src/{CreatedOnDcom-en-us-D9M_heGE.js → CreatedOnDcom-en-us-Bqm5NnRX.js} +1 -1
  10. package/lib/src/{CreatedOnDcom-es-es-PgWwbBLA.js → CreatedOnDcom-es-es-COGQIuEn.js} +1 -1
  11. package/lib/src/{CreatedOnDcom-fr-ca-BXiB-abj.js → CreatedOnDcom-fr-ca-eVDHO7MX.js} +1 -1
  12. package/lib/src/{CreatedOnDcom-fr-fr-BXhTmu7F.js → CreatedOnDcom-fr-fr-BhGKbdoG.js} +1 -1
  13. package/lib/src/{CreatedOnDcom-pt-br-3e6y567w.js → CreatedOnDcom-pt-br-5kDZKPZc.js} +1 -1
  14. package/lib/src/{CreatedOnDcom-pt-pt-GojOtHmO.js → CreatedOnDcom-pt-pt-CnTcVxct.js} +1 -1
  15. package/lib/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.js +2 -11
  16. package/lib/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.js +15 -47
  17. package/lib/src/brand-page-maker/stores/history/history-store.d.ts +7 -47
  18. package/lib/src/brand-page-maker/stores/history/history-store.js +98 -283
  19. package/lib/src/index.mjs +1 -1
  20. package/lib/src/{lib-DVs3jUit.js → lib-B5jbN-ro.js} +31 -44
  21. package/lib/src/shared/content-settings/components/GenericTextInput.mixin.d.ts +1 -1
  22. package/lib/src/shared/content-settings/components/content-settings/ContentSettings.mixin.js +1 -8
  23. package/lib/src/shared/content-settings/components/email-field/EmailField.mixin.d.ts +1 -1
  24. package/lib/src/shared/content-settings/components/payment-config-field/PaymentConfigField.mixin.js +2 -8
  25. package/lib/src/shared/content-settings/components/radio-field/RadioField.mixin.d.ts +1 -1
  26. package/lib/src/shared/content-settings/components/tel-field/TelField.mixin.d.ts +1 -1
  27. package/lib/src/shared/content-settings/components/text-field/TextField.mixin.d.ts +1 -1
  28. package/lib/src/shared/content-settings/components/textarea-field/TextareaField.mixin.d.ts +1 -1
  29. package/lib/src/shared/content-settings/components/url-field/UrlField.mixin.d.ts +1 -1
  30. package/lib/src/shared/content-settings/components/youtube-video-field/YoutubeVideoField.mixin.d.ts +1 -1
  31. package/lib/src/shared/logo-settings/store/editable-logo-store.d.ts +0 -18
  32. package/lib/src/shared/logo-settings/store/editable-logo-store.js +6 -24
  33. package/package.json +1 -1
  34. package/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.ts +2 -11
  35. package/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.ts +15 -47
  36. package/src/brand-page-maker/stores/history/history-store.test.ts +32 -0
  37. package/src/brand-page-maker/stores/history/history-store.ts +107 -340
  38. package/src/shared/content-settings/components/content-settings/ContentSettings.mixin.ts +1 -8
  39. package/src/shared/content-settings/components/page-dropdown-field/PageDropdownField.vue +1 -4
  40. package/src/shared/content-settings/components/payment-config-field/PaymentConfigField.mixin.ts +2 -8
  41. 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
- * Single-stack history model with a pointer instead of two stacks.
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: 40-entry cap. FR-13: 100 MB memory budget. FR-13c: per-entry
36
- // oversize threshold — entries above this are skipped + emit telemetry.
37
- // 25 MB accommodates a high-res BYO logo upload, which renders ~2.5 MB
38
- // of base64 PNG into each of four viewport sections (~10 MB) plus per-page
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-capture during which any auto-captured snapshot
47
- // REPLACES the baseline rather than pushing a new entry. Covers post-init
48
- // reactive cascades (template loadPageNo, router afterEach, layout settle)
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
- * `shallowRef` keeps the array ref reactive (so dependent computeds recompute
56
- * when we swap the array) but prevents Pinia / Vue from proxying every entry
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
- * Cached reference to the maker store. Set once when the watcher installs so
72
- * the capture hot path is synchronous.
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(cachedMakerStore.brandPageData)));
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
- * One-shot "fold next mutation back into the baseline (or current entry)
165
- * rather than pushing a new entry" flag.
166
- *
167
- * Hydration write sites (TipTap mount, lazy paymentConfig fill,
168
- * stale-page-ref clear) all run from a lifecycle hook on first sidebar
169
- * open / section click. Each site mutates `brandPageData` exactly once
170
- * with a value the user can't see as a change — but the deep watcher
171
- * fires anyway, and without this gate the next idle flush would land a
172
- * phantom undo entry.
173
- *
174
- * Each call site sets the flag immediately before the imperative write
175
- * it knows is a hydration. The watcher's flush consumes the flag (clears
176
- * it) and merges the resulting snapshot into `entries[currentIndex]`
177
- * instead of pushing. No TTL — the flag stays armed until either:
178
- * - the watcher fires and consumes it, or
179
- * - a snapshot apply (undo/redo) clears it (we don't want a hydration
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 the snapshot bytes AND the logoEdits sidecar.
216
- // Pre-purchase logo-tab edits (text/color/font) live in the
217
- // `editableLogoStore.logoEdits` sidecar, NOT in `brandPageData`.
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
- * Install a deep watcher on `brandPageData` that auto-captures mutations.
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
- // During the post-init settle window, fold any captured mutation back
322
- // into the baseline (entry 0). Async cascades like template
323
- // `loadPageNo`, the router's `afterEach`, and layout settling all
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 branch — a hydration write site (TipTap mount, lazy
333
- // paymentConfig fill, stale-page-ref clear) flagged this flush as
334
- // "fold into the head, don't push". Replace `entries[currentIndex]`
335
- // in place so the user-visible undo stack stays exactly as it was
336
- // before the hydration burst arrived. Consume the flag whether or
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 burst arming. `isApplyingSnapshot` is gated here so an
378
- // undo/redo doesn't leave a stale timer ticking that later flushes
379
- // mid-keystroke when the user starts editing inside the debounce
380
- // window. `baselineCaptured` is not gated here — `installWatcher` is
381
- // only called from `captureBaseline`, so by the time this fires it is
382
- // always true, and `flush` re-checks it.
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(() => cachedMakerStore.brandPageData, scheduleAutoCapture, {
266
+ watch(() => useBrandPageMakerStore().brandPageData, scheduleAutoCapture, {
392
267
  deep: true,
393
268
  });
394
269
  watcherInstalled.value = true;
395
270
  }
396
271
  // --- Actions ---
397
- /**
398
- * Captures the baseline entry. Called once per session, typically by
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
- * Force-commit a `template_switch` entry. Cancels any pending auto-capture
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
- * Parse a stringified snapshot and push it through the maker store's
467
- * dedicated `applyHistorySnapshot` action, which rehydrates every Pinia
468
- * source store + own-store scalar fields that `brandPageData` reads from.
469
- *
470
- * Holds `isApplyingSnapshot` for the duration of the apply so the
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 against the source-of-transition. The first page in
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. Used when no page-structural diff
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
- // Drop any merge flag a hydration site might have armed but the
543
- // watcher hasn't yet consumed — otherwise the post-apply watcher
544
- // tick would silently fold the freshly-restored snapshot into the
545
- // entry at the new pointer position and erase the user's redo.
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
- // Capture the actor of the entry we're leaving — that's the change being undone.
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
- * Clear the stack and re-baseline against the just-saved state.
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
- mergeNextFlush.value = false;
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
- * Arm the next watcher flush to fold its snapshot into the current head
628
- * entry instead of pushing a new one. Hydration write sites (TipTap mount,
629
- * lazy paymentConfig fill, stale-page-ref clear) call this immediately
630
- * before the imperative `update:modelValue` / `$emit` they know is a
631
- * hydration mutation. The next flush consumes (clears) the flag.
632
- *
633
- * Idempotent — multiple call sites in the same burst all set the same
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
- mergeNextFlush.value = true;
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-DVs3jUit.js";
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,