@designcrowd/library.brand-page 6.0.6-undo-9 → 6.0.6-undo-11

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 (38) hide show
  1. package/lib/src/{CreatedOnBrandCrowd-de-de-leTOAQPM.js → CreatedOnBrandCrowd-de-de-DGLT4mjc.js} +1 -1
  2. package/lib/src/{CreatedOnBrandCrowd-en-us-_9b-3HPl.js → CreatedOnBrandCrowd-en-us-dyPp4NQB.js} +1 -1
  3. package/lib/src/{CreatedOnBrandCrowd-es-es-BPeAHmQH.js → CreatedOnBrandCrowd-es-es-pxoT8xpT.js} +1 -1
  4. package/lib/src/{CreatedOnBrandCrowd-fr-fr-B7hbXmje.js → CreatedOnBrandCrowd-fr-ca-Bpvv3YSe.js} +1 -1
  5. package/lib/src/{CreatedOnBrandCrowd-fr-ca-B7hbXmje.js → CreatedOnBrandCrowd-fr-fr-Bpvv3YSe.js} +1 -1
  6. package/lib/src/{CreatedOnBrandCrowd-pt-pt-C0U-9HO7.js → CreatedOnBrandCrowd-pt-br-DqKwinmI.js} +1 -1
  7. package/lib/src/{CreatedOnBrandCrowd-pt-br-C0U-9HO7.js → CreatedOnBrandCrowd-pt-pt-DqKwinmI.js} +1 -1
  8. package/lib/src/{CreatedOnDcom-de-de-Bdn37x7V.js → CreatedOnDcom-de-de-DhhAudIO.js} +1 -1
  9. package/lib/src/{CreatedOnDcom-en-us-usSC3HOQ.js → CreatedOnDcom-en-us-DZhMl0Ro.js} +1 -1
  10. package/lib/src/{CreatedOnDcom-es-es-BE0kziRp.js → CreatedOnDcom-es-es-brPE0-Lo.js} +1 -1
  11. package/lib/src/{CreatedOnDcom-fr-ca-CcDaV0KR.js → CreatedOnDcom-fr-ca-CzKZrAVq.js} +1 -1
  12. package/lib/src/{CreatedOnDcom-fr-fr-C1bX4cuW.js → CreatedOnDcom-fr-fr-JLjx-E9X.js} +1 -1
  13. package/lib/src/{CreatedOnDcom-pt-br-DYju5xhk.js → CreatedOnDcom-pt-br-B_4Ot1uS.js} +1 -1
  14. package/lib/src/{CreatedOnDcom-pt-pt-CKYX6Rd0.js → CreatedOnDcom-pt-pt-By5SiPq_.js} +1 -1
  15. package/lib/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.js +1 -1
  16. package/lib/src/css/library.brand-page-brandCrowd.min.css +8 -2
  17. package/lib/src/css/library.brand-page-brandPage.min.css +8 -2
  18. package/lib/src/css/library.brand-page-designCom.min.css +8 -2
  19. package/lib/src/css/library.brand-page-preview.min.css +8 -2
  20. package/lib/src/index.mjs +1 -1
  21. package/lib/src/{lib-FHU3Jvoq.js → lib-C0lwMScz.js} +16056 -16044
  22. package/lib/src/shared/content-settings/components/GenericTextInput.mixin.d.ts +1 -1
  23. package/lib/src/shared/content-settings/components/GenericTextInput.mixin.js +1 -1
  24. package/lib/src/shared/content-settings/components/email-field/EmailField.mixin.d.ts +1 -1
  25. package/lib/src/shared/content-settings/components/tel-field/TelField.mixin.d.ts +1 -1
  26. package/lib/src/shared/content-settings/components/text-field/TextField.mixin.d.ts +1 -1
  27. package/lib/src/shared/content-settings/components/textarea-field/TextareaField.mixin.d.ts +1 -1
  28. package/lib/src/shared/content-settings/components/url-field/UrlField.mixin.d.ts +1 -1
  29. package/lib/src/shared/content-settings/components/youtube-video-field/YoutubeVideoField.mixin.d.ts +1 -1
  30. package/lib/src/shared/content-settings/store/content-settings-store.js +27 -33
  31. package/package.json +1 -1
  32. package/src/brand-page-maker/components/quick-link-bar/RedoButton.vue +15 -1
  33. package/src/brand-page-maker/components/quick-link-bar/UndoButton.vue +15 -1
  34. package/src/brand-page-maker/components/quick-link-bar/undo-redo-buttons.test.ts +7 -0
  35. package/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.ts +1 -1
  36. package/src/shared/content-settings/components/GenericTextInput.mixin.test.ts +9 -9
  37. package/src/shared/content-settings/components/GenericTextInput.mixin.ts +5 -2
  38. package/src/shared/content-settings/store/content-settings-store.ts +28 -33
@@ -99,7 +99,7 @@ declare const _default: import("vue").DefineComponent<import("vue").ExtractPropT
99
99
  onFocus(): void;
100
100
  onBlur(): void;
101
101
  onRteModelValue(newVal: string, meta?: {
102
- isHydration?: boolean;
102
+ isHydrationWrite?: boolean;
103
103
  }): void;
104
104
  emitChange(): void;
105
105
  }, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, ("update:modelValue" | "on-toggle-state-change")[], "update:modelValue" | "on-toggle-state-change", import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
@@ -209,7 +209,7 @@ export default defineComponent({
209
209
  // would drop the second argument), records it for the `text` setter,
210
210
  // then applies the write.
211
211
  onRteModelValue(newVal, meta) {
212
- this.pendingWriteIsHydration = meta?.isHydration === true;
212
+ this.pendingWriteIsHydration = meta?.isHydrationWrite === true;
213
213
  this.text = newVal;
214
214
  },
215
215
  emitChange() {
@@ -96,7 +96,7 @@ declare const _default: import("vue").DefineComponent<{}, {}, {}, {}, {}, import
96
96
  onFocus(): void;
97
97
  onBlur(): void;
98
98
  onRteModelValue(newVal: string, meta?: {
99
- isHydration?: boolean;
99
+ isHydrationWrite?: boolean;
100
100
  }): void;
101
101
  emitChange(): void;
102
102
  }, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, ("update:modelValue" | "on-toggle-state-change")[], "update:modelValue" | "on-toggle-state-change", import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
@@ -96,7 +96,7 @@ declare const _default: import("vue").DefineComponent<{}, {}, {}, {}, {}, import
96
96
  onFocus(): void;
97
97
  onBlur(): void;
98
98
  onRteModelValue(newVal: string, meta?: {
99
- isHydration?: boolean;
99
+ isHydrationWrite?: boolean;
100
100
  }): void;
101
101
  emitChange(): void;
102
102
  }, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, ("update:modelValue" | "on-toggle-state-change")[], "update:modelValue" | "on-toggle-state-change", import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
@@ -96,7 +96,7 @@ declare const _default: import("vue").DefineComponent<{}, {}, {}, {}, {}, import
96
96
  onFocus(): void;
97
97
  onBlur(): void;
98
98
  onRteModelValue(newVal: string, meta?: {
99
- isHydration?: boolean;
99
+ isHydrationWrite?: boolean;
100
100
  }): void;
101
101
  emitChange(): void;
102
102
  }, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, ("update:modelValue" | "on-toggle-state-change")[], "update:modelValue" | "on-toggle-state-change", import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
@@ -110,7 +110,7 @@ declare const _default: import("vue").DefineComponent<import("vue").ExtractPropT
110
110
  onFocus(): void;
111
111
  onBlur(): void;
112
112
  onRteModelValue(newVal: string, meta?: {
113
- isHydration?: boolean;
113
+ isHydrationWrite?: boolean;
114
114
  }): void;
115
115
  emitChange(): void;
116
116
  }, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, ("update:modelValue" | "on-toggle-state-change")[], "update:modelValue" | "on-toggle-state-change", import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
@@ -103,7 +103,7 @@ declare const _default: import("vue").DefineComponent<{}, {}, Record<string, any
103
103
  onFocus(): void;
104
104
  onBlur(): void;
105
105
  onRteModelValue(newVal: string, meta?: {
106
- isHydration?: boolean;
106
+ isHydrationWrite?: boolean;
107
107
  }): void;
108
108
  emitChange(): void;
109
109
  }, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, ("update:modelValue" | "on-toggle-state-change")[], "update:modelValue" | "on-toggle-state-change", import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
@@ -99,7 +99,7 @@ declare const _default: import("vue").DefineComponent<{}, {}, Record<string, any
99
99
  onFocus(): void;
100
100
  onBlur(): void;
101
101
  onRteModelValue(newVal: string, meta?: {
102
- isHydration?: boolean;
102
+ isHydrationWrite?: boolean;
103
103
  }): void;
104
104
  emitChange(): void;
105
105
  }, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, ("update:modelValue" | "on-toggle-state-change")[], "update:modelValue" | "on-toggle-state-change", import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
@@ -22,6 +22,23 @@ const DEFAULT_TEXT_COLOR_DEPENDENT_TYPES = [
22
22
  // result[i].x = v`), which would leave the payload byte-identical to
23
23
  // the store while state actually changed.
24
24
  const isSameValue = (a, b) => a === b;
25
+ /**
26
+ * Arms the undo history to fold the next mutation into the current head
27
+ * instead of pushing an entry — the hydration-write contract (BP-5810).
28
+ * Framework-originated writes (RTE canonical HTML on mount, style
29
+ * re-derivation, lazy config hydration) reach this store through debounced
30
+ * field chains, so the flag travels WITH the payload and the arm happens
31
+ * here, in the same tick as the write — arming at emit time would lapse
32
+ * before the write lands. Call only AFTER an action's drop guards (missing
33
+ * item, value-identical): a dropped write must never arm. See
34
+ * UpdateItemValuePayload.isHydrationWrite (models.ts) for the payload
35
+ * contract.
36
+ */
37
+ const armHistoryMergeForHydrationWrite = (isHydrationWrite) => {
38
+ if (!isHydrationWrite)
39
+ return;
40
+ useHistoryStore().mergeNextMutation();
41
+ };
25
42
  export const CONTENT_SETTINGS_MODES = {
26
43
  edit: 'edit',
27
44
  add: 'add',
@@ -222,15 +239,7 @@ export const useUiContentSettingsStore = defineStore('uiContentSettings', {
222
239
  if (isSameValue(item.settings.content[payload.name], payload.value)) {
223
240
  return; // no-op write: don't validate, don't flip the modified flag
224
241
  }
225
- // Hydration write-backs (RTE canonical HTML on mount, lazy config
226
- // hydration, stale-id clears) fold into the current undo head instead
227
- // of pushing an entry. Arming here — same tick as the write — is what
228
- // makes the merge reliable: the flag travelled with the payload
229
- // through the debounced field chain, so emit-time arming would lapse
230
- // before the write lands (BP-5810).
231
- if (payload.isHydrationWrite) {
232
- useHistoryStore().mergeNextMutation();
233
- }
242
+ armHistoryMergeForHydrationWrite(payload.isHydrationWrite);
234
243
  item.settings.content[payload.name] = payload.value;
235
244
  this.validateItem({
236
245
  id: payload.id,
@@ -246,10 +255,7 @@ export const useUiContentSettingsStore = defineStore('uiContentSettings', {
246
255
  if (Object.keys(value ?? {}).every((k) => isSameValue(item.settings.content[k], value[k]))) {
247
256
  return; // every field a no-op — see isSameValue
248
257
  }
249
- // Same hydration-write contract as updateItemValue.
250
- if (isHydrationWrite) {
251
- useHistoryStore().mergeNextMutation();
252
- }
258
+ armHistoryMergeForHydrationWrite(isHydrationWrite);
253
259
  item.settings.content = vueDeepMerge(item.settings.content, value);
254
260
  this.validateItem({ id, name });
255
261
  this.onContentChanged();
@@ -259,10 +265,7 @@ export const useUiContentSettingsStore = defineStore('uiContentSettings', {
259
265
  if (!item) {
260
266
  return;
261
267
  }
262
- // Same hydration-write contract as updateItemValue.
263
- if (isHydrationWrite) {
264
- useHistoryStore().mergeNextMutation();
265
- }
268
+ armHistoryMergeForHydrationWrite(isHydrationWrite);
266
269
  item.settings.content[`${name}Style`].textStyleType = vueDeepMerge(item.settings.content, value);
267
270
  this.validateItem({ id, name });
268
271
  this.onContentChanged();
@@ -272,14 +275,11 @@ export const useUiContentSettingsStore = defineStore('uiContentSettings', {
272
275
  if (!item) {
273
276
  return;
274
277
  }
275
- // Same hydration-write contract as updateItemValue. The RTE's
276
- // mount-time style sync (setInitialStyleSettings) lands here ~100ms
277
- // BEFORE its canonical text write-back — if this write opens a plain
278
- // capture burst, the flagged text write joins it and both commit as a
279
- // phantom entry (BP-5810's second act).
280
- if (isHydrationWrite) {
281
- useHistoryStore().mergeNextMutation();
282
- }
278
+ // The RTE's mount-time style sync (setInitialStyleSettings) lands here
279
+ // ~100ms BEFORE its canonical text write-back — unflagged, it would
280
+ // open a plain capture burst that swallows the flagged text write
281
+ // (BP-5810's second act).
282
+ armHistoryMergeForHydrationWrite(isHydrationWrite);
283
283
  item.settings.content[`${name}Style`] = vueDeepMerge(item.settings.content[`${name}Style`], value);
284
284
  this.validateItem({ id, name });
285
285
  this.onContentChanged();
@@ -296,10 +296,7 @@ export const useUiContentSettingsStore = defineStore('uiContentSettings', {
296
296
  if (!item) {
297
297
  return;
298
298
  }
299
- // Same hydration-write contract as updateItemValue.
300
- if (isHydrationWrite) {
301
- useHistoryStore().mergeNextMutation();
302
- }
299
+ armHistoryMergeForHydrationWrite(isHydrationWrite);
303
300
  item.settings.content.textItems[index].textStyle.textStyleType =
304
301
  vueDeepMerge(item.settings.content.textItems[index].textStyle.textStyleType, value);
305
302
  this.validateItem({ id, name });
@@ -310,10 +307,7 @@ export const useUiContentSettingsStore = defineStore('uiContentSettings', {
310
307
  if (!item) {
311
308
  return;
312
309
  }
313
- // Same hydration-write contract as updateItemValue.
314
- if (isHydrationWrite) {
315
- useHistoryStore().mergeNextMutation();
316
- }
310
+ armHistoryMergeForHydrationWrite(isHydrationWrite);
317
311
  item.settings.content.textItems[index].textStyle = vueDeepMerge(item.settings.content.textItems[index].textStyle, value);
318
312
  this.validateItem({ id, name });
319
313
  this.onContentChanged();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@designcrowd/library.brand-page",
3
3
  "description": "A collection of Maker/Designer applications",
4
- "version": "6.0.6-undo-9",
4
+ "version": "6.0.6-undo-11",
5
5
  "author": "Design.com Eng",
6
6
  "license": "UNLICENSED",
7
7
  "type": "module",
@@ -3,7 +3,10 @@
3
3
  direction="top"
4
4
  alignment="center"
5
5
  :should-show-icon="false"
6
- class="c-redo-button-tooltip"
6
+ :class="[
7
+ 'c-redo-button-tooltip',
8
+ { 'c-tooltip-hidden': isMobileScreenSize },
9
+ ]"
7
10
  >
8
11
  <template #open>
9
12
  <Icon
@@ -39,13 +42,20 @@ import { Icon, Tooltip } from '@designcrowd/fe-shared-lib';
39
42
  import { useHistoryStore } from '@brand-page-maker/stores/history/history-store';
40
43
  import { useAiWebsiteBuilderThreadStore } from '@brand-page-maker/stores/ai-thread/ai-website-builder-thread-store';
41
44
  import { useBrandPageContainerStore } from '@brand-page-maker/stores/brandpage-container';
45
+ import { useUiStore } from '@brand-page-maker/stores/ui/ui-store';
42
46
  import { useUiTemplateSettingsStore } from '@shared/template-settings/store';
47
+ import { UI_SCREENSIZE_MODES } from '@shared/models';
43
48
  import { quickLinkBarTr } from '@src/composables/useTranslate';
44
49
 
45
50
  const historyStore = useHistoryStore();
46
51
  const aiThreadStore = useAiWebsiteBuilderThreadStore();
47
52
  const containerStore = useBrandPageContainerStore();
48
53
  const templateSettingsStore = useUiTemplateSettingsStore();
54
+ const uiStore = useUiStore();
55
+
56
+ const isMobileScreenSize = computed(
57
+ () => uiStore.screenSizeMode === UI_SCREENSIZE_MODES.mobile,
58
+ );
49
59
 
50
60
  const isDisabled = computed(
51
61
  () =>
@@ -71,4 +81,8 @@ const onClick = () => {
71
81
  width: auto;
72
82
  padding: 4px 8px;
73
83
  }
84
+
85
+ .c-tooltip-hidden :deep(.shared-tooltip__content) {
86
+ display: none !important;
87
+ }
74
88
  </style>
@@ -3,7 +3,10 @@
3
3
  direction="top"
4
4
  alignment="center"
5
5
  :should-show-icon="false"
6
- class="c-undo-button-tooltip"
6
+ :class="[
7
+ 'c-undo-button-tooltip',
8
+ { 'c-tooltip-hidden': isMobileScreenSize },
9
+ ]"
7
10
  >
8
11
  <template #open>
9
12
  <Icon
@@ -39,13 +42,20 @@ import { Icon, Tooltip } from '@designcrowd/fe-shared-lib';
39
42
  import { useHistoryStore } from '@brand-page-maker/stores/history/history-store';
40
43
  import { useAiWebsiteBuilderThreadStore } from '@brand-page-maker/stores/ai-thread/ai-website-builder-thread-store';
41
44
  import { useBrandPageContainerStore } from '@brand-page-maker/stores/brandpage-container';
45
+ import { useUiStore } from '@brand-page-maker/stores/ui/ui-store';
42
46
  import { useUiTemplateSettingsStore } from '@shared/template-settings/store';
47
+ import { UI_SCREENSIZE_MODES } from '@shared/models';
43
48
  import { quickLinkBarTr } from '@src/composables/useTranslate';
44
49
 
45
50
  const historyStore = useHistoryStore();
46
51
  const aiThreadStore = useAiWebsiteBuilderThreadStore();
47
52
  const containerStore = useBrandPageContainerStore();
48
53
  const templateSettingsStore = useUiTemplateSettingsStore();
54
+ const uiStore = useUiStore();
55
+
56
+ const isMobileScreenSize = computed(
57
+ () => uiStore.screenSizeMode === UI_SCREENSIZE_MODES.mobile,
58
+ );
49
59
 
50
60
  const isDisabled = computed(
51
61
  () =>
@@ -71,4 +81,8 @@ const onClick = () => {
71
81
  width: auto;
72
82
  padding: 4px 8px;
73
83
  }
84
+
85
+ .c-tooltip-hidden :deep(.shared-tooltip__content) {
86
+ display: none !important;
87
+ }
74
88
  </style>
@@ -66,6 +66,12 @@ vi.mock('@shared/template-settings/store', () => ({
66
66
  useUiTemplateSettingsStore: vi.fn(() => templateSettingsStoreMock),
67
67
  }));
68
68
 
69
+ const uiStoreMock = { screenSizeMode: 'desktop' as 'desktop' | 'mobile' };
70
+
71
+ vi.mock('@brand-page-maker/stores/ui/ui-store', () => ({
72
+ useUiStore: vi.fn(() => uiStoreMock),
73
+ }));
74
+
69
75
  beforeEach(() => {
70
76
  setActivePinia(createPinia());
71
77
  historyStoreMock.canUndo = true;
@@ -76,6 +82,7 @@ beforeEach(() => {
76
82
  aiStoreMock.isProcessing = false;
77
83
  containerStoreMock.isSaving = false;
78
84
  templateSettingsStoreMock.isBusy = false;
85
+ uiStoreMock.screenSizeMode = 'desktop';
79
86
  });
80
87
 
81
88
  describe('UndoButton', () => {
@@ -523,7 +523,7 @@ export const useRichTextEditor = (
523
523
  // focuses it before formatting, so this doesn't occur — revisit if a
524
524
  // consumer ever drives toolbar actions on an unfocused editor.
525
525
  emit('update:modelValue', sanitizedData.sanitizedHtml, {
526
- isHydration: !editor.isFocused,
526
+ isHydrationWrite: !editor.isFocused,
527
527
  });
528
528
  });
529
529
 
@@ -2,10 +2,10 @@ import { defineComponent } from 'vue';
2
2
  import { mount } from '@vue/test-utils';
3
3
  import GenericTextInputMixin from './GenericTextInput.mixin';
4
4
 
5
- // BP-5810: the RTE attaches `{ isHydration: true }` meta to its canonical
6
- // write-back emits. GenericTextInput must carry that through its 100ms
7
- // debounce onto the outgoing payload as `isHydrationWrite` — and downgrade
8
- // the burst as soon as a single non-hydration write lands in it.
5
+ // BP-5810: the RTE attaches `{ isHydrationWrite: true }` meta to its
6
+ // canonical write-back emits. GenericTextInput must carry that through its
7
+ // 100ms debounce onto the outgoing payload under the same name — and
8
+ // downgrade the burst as soon as a single non-hydration write lands in it.
9
9
 
10
10
  const DEBOUNCE_WAIT_MS = 100;
11
11
 
@@ -50,7 +50,7 @@ describe('GenericTextInput hydration burst flag', () => {
50
50
  const wrapper = mountHost();
51
51
 
52
52
  wrapper.vm.onRteModelValue('<p style="text-align: center;"></p>', {
53
- isHydration: true,
53
+ isHydrationWrite: true,
54
54
  });
55
55
 
56
56
  await vi.advanceTimersByTimeAsync(DEBOUNCE_WAIT_MS + 10);
@@ -69,8 +69,8 @@ describe('GenericTextInput hydration burst flag', () => {
69
69
  test('coalesced hydration writes in one burst stay flagged', async () => {
70
70
  const wrapper = mountHost();
71
71
 
72
- wrapper.vm.onRteModelValue('<p>a</p>', { isHydration: true });
73
- wrapper.vm.onRteModelValue('<p>b</p>', { isHydration: true });
72
+ wrapper.vm.onRteModelValue('<p>a</p>', { isHydrationWrite: true });
73
+ wrapper.vm.onRteModelValue('<p>b</p>', { isHydrationWrite: true });
74
74
 
75
75
  await vi.advanceTimersByTimeAsync(DEBOUNCE_WAIT_MS + 10);
76
76
 
@@ -82,7 +82,7 @@ describe('GenericTextInput hydration burst flag', () => {
82
82
  test('a user write in the same burst downgrades it to a normal edit', async () => {
83
83
  const wrapper = mountHost();
84
84
 
85
- wrapper.vm.onRteModelValue('<p>canonical</p>', { isHydration: true });
85
+ wrapper.vm.onRteModelValue('<p>canonical</p>', { isHydrationWrite: true });
86
86
  // Focused typing emits without meta.
87
87
  wrapper.vm.onRteModelValue('<p>typed</p>');
88
88
 
@@ -106,7 +106,7 @@ describe('GenericTextInput hydration burst flag', () => {
106
106
  test('the flag resets between bursts', async () => {
107
107
  const wrapper = mountHost();
108
108
 
109
- wrapper.vm.onRteModelValue('<p>canonical</p>', { isHydration: true });
109
+ wrapper.vm.onRteModelValue('<p>canonical</p>', { isHydrationWrite: true });
110
110
  await vi.advanceTimersByTimeAsync(DEBOUNCE_WAIT_MS + 10);
111
111
 
112
112
  wrapper.vm.text = '<p>typed later</p>';
@@ -232,8 +232,11 @@ export default defineComponent({
232
232
  // attaches to its update:modelValue emits (a plain v-model binding
233
233
  // would drop the second argument), records it for the `text` setter,
234
234
  // then applies the write.
235
- onRteModelValue(newVal: string, meta?: { isHydration?: boolean }): void {
236
- this.pendingWriteIsHydration = meta?.isHydration === true;
235
+ onRteModelValue(
236
+ newVal: string,
237
+ meta?: { isHydrationWrite?: boolean },
238
+ ): void {
239
+ this.pendingWriteIsHydration = meta?.isHydrationWrite === true;
237
240
  this.text = newVal;
238
241
  },
239
242
 
@@ -69,6 +69,24 @@ const DEFAULT_TEXT_COLOR_DEPENDENT_TYPES: readonly ContentType[] = [
69
69
  // the store while state actually changed.
70
70
  const isSameValue = (a: unknown, b: unknown): boolean => a === b;
71
71
 
72
+ /**
73
+ * Arms the undo history to fold the next mutation into the current head
74
+ * instead of pushing an entry — the hydration-write contract (BP-5810).
75
+ * Framework-originated writes (RTE canonical HTML on mount, style
76
+ * re-derivation, lazy config hydration) reach this store through debounced
77
+ * field chains, so the flag travels WITH the payload and the arm happens
78
+ * here, in the same tick as the write — arming at emit time would lapse
79
+ * before the write lands. Call only AFTER an action's drop guards (missing
80
+ * item, value-identical): a dropped write must never arm. See
81
+ * UpdateItemValuePayload.isHydrationWrite (models.ts) for the payload
82
+ * contract.
83
+ */
84
+ const armHistoryMergeForHydrationWrite = (isHydrationWrite?: boolean): void => {
85
+ if (!isHydrationWrite) return;
86
+
87
+ useHistoryStore().mergeNextMutation();
88
+ };
89
+
72
90
  export const CONTENT_SETTINGS_MODES = {
73
91
  edit: 'edit',
74
92
  add: 'add',
@@ -403,15 +421,7 @@ export const useUiContentSettingsStore = defineStore('uiContentSettings', {
403
421
  return; // no-op write: don't validate, don't flip the modified flag
404
422
  }
405
423
 
406
- // Hydration write-backs (RTE canonical HTML on mount, lazy config
407
- // hydration, stale-id clears) fold into the current undo head instead
408
- // of pushing an entry. Arming here — same tick as the write — is what
409
- // makes the merge reliable: the flag travelled with the payload
410
- // through the debounced field chain, so emit-time arming would lapse
411
- // before the write lands (BP-5810).
412
- if (payload.isHydrationWrite) {
413
- useHistoryStore().mergeNextMutation();
414
- }
424
+ armHistoryMergeForHydrationWrite(payload.isHydrationWrite);
415
425
 
416
426
  item.settings.content[payload.name] = payload.value;
417
427
 
@@ -445,10 +455,7 @@ export const useUiContentSettingsStore = defineStore('uiContentSettings', {
445
455
  return; // every field a no-op — see isSameValue
446
456
  }
447
457
 
448
- // Same hydration-write contract as updateItemValue.
449
- if (isHydrationWrite) {
450
- useHistoryStore().mergeNextMutation();
451
- }
458
+ armHistoryMergeForHydrationWrite(isHydrationWrite);
452
459
 
453
460
  item.settings.content = vueDeepMerge(item.settings.content, value);
454
461
  this.validateItem({ id, name });
@@ -466,10 +473,7 @@ export const useUiContentSettingsStore = defineStore('uiContentSettings', {
466
473
  return;
467
474
  }
468
475
 
469
- // Same hydration-write contract as updateItemValue.
470
- if (isHydrationWrite) {
471
- useHistoryStore().mergeNextMutation();
472
- }
476
+ armHistoryMergeForHydrationWrite(isHydrationWrite);
473
477
 
474
478
  item.settings.content[`${name}Style`].textStyleType = vueDeepMerge(
475
479
  item.settings.content,
@@ -490,14 +494,11 @@ export const useUiContentSettingsStore = defineStore('uiContentSettings', {
490
494
  return;
491
495
  }
492
496
 
493
- // Same hydration-write contract as updateItemValue. The RTE's
494
- // mount-time style sync (setInitialStyleSettings) lands here ~100ms
495
- // BEFORE its canonical text write-back — if this write opens a plain
496
- // capture burst, the flagged text write joins it and both commit as a
497
- // phantom entry (BP-5810's second act).
498
- if (isHydrationWrite) {
499
- useHistoryStore().mergeNextMutation();
500
- }
497
+ // The RTE's mount-time style sync (setInitialStyleSettings) lands here
498
+ // ~100ms BEFORE its canonical text write-back — unflagged, it would
499
+ // open a plain capture burst that swallows the flagged text write
500
+ // (BP-5810's second act).
501
+ armHistoryMergeForHydrationWrite(isHydrationWrite);
501
502
 
502
503
  item.settings.content[`${name}Style`] = vueDeepMerge(
503
504
  item.settings.content[`${name}Style`],
@@ -528,10 +529,7 @@ export const useUiContentSettingsStore = defineStore('uiContentSettings', {
528
529
  return;
529
530
  }
530
531
 
531
- // Same hydration-write contract as updateItemValue.
532
- if (isHydrationWrite) {
533
- useHistoryStore().mergeNextMutation();
534
- }
532
+ armHistoryMergeForHydrationWrite(isHydrationWrite);
535
533
 
536
534
  item.settings.content.textItems[index!].textStyle.textStyleType =
537
535
  vueDeepMerge(
@@ -554,10 +552,7 @@ export const useUiContentSettingsStore = defineStore('uiContentSettings', {
554
552
  return;
555
553
  }
556
554
 
557
- // Same hydration-write contract as updateItemValue.
558
- if (isHydrationWrite) {
559
- useHistoryStore().mergeNextMutation();
560
- }
555
+ armHistoryMergeForHydrationWrite(isHydrationWrite);
561
556
 
562
557
  item.settings.content.textItems[index!].textStyle = vueDeepMerge(
563
558
  item.settings.content.textItems[index!].textStyle,