@designcrowd/library.brand-page 6.2.2 → 6.2.3-ast-faq

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 (147) hide show
  1. package/lib/src/{CreatedOnBrandCrowd-de-de-7P2gNr-R.js → CreatedOnBrandCrowd-de-de-BRisMzFY.js} +1 -1
  2. package/lib/src/{CreatedOnBrandCrowd-en-us-Dl7BymW6.js → CreatedOnBrandCrowd-en-us-CDDsL9GX.js} +1 -1
  3. package/lib/src/{CreatedOnBrandCrowd-es-es-CO4FvWVw.js → CreatedOnBrandCrowd-es-es-BkZrEo4j.js} +1 -1
  4. package/lib/src/{CreatedOnBrandCrowd-fr-fr-DHNqakQu.js → CreatedOnBrandCrowd-fr-ca-sx_ury7R.js} +1 -1
  5. package/lib/src/{CreatedOnBrandCrowd-fr-ca-DHNqakQu.js → CreatedOnBrandCrowd-fr-fr-sx_ury7R.js} +1 -1
  6. package/lib/src/{CreatedOnBrandCrowd-pt-pt-CGUC-ZbP.js → CreatedOnBrandCrowd-pt-br-DCWiAfkU.js} +1 -1
  7. package/lib/src/{CreatedOnBrandCrowd-pt-br-CGUC-ZbP.js → CreatedOnBrandCrowd-pt-pt-DCWiAfkU.js} +1 -1
  8. package/lib/src/{CreatedOnDcom-de-de-Dvds-Vmy.js → CreatedOnDcom-de-de-C1S7f22L.js} +1 -1
  9. package/lib/src/{CreatedOnDcom-en-us-fd3C-8Tp.js → CreatedOnDcom-en-us-DgTlWaXE.js} +1 -1
  10. package/lib/src/{CreatedOnDcom-es-es-DTXZW35-.js → CreatedOnDcom-es-es-CxSQJe_V.js} +1 -1
  11. package/lib/src/{CreatedOnDcom-fr-ca-DRCYOKPs.js → CreatedOnDcom-fr-ca-Bbam2tt6.js} +1 -1
  12. package/lib/src/{CreatedOnDcom-fr-fr-DpIB8x2x.js → CreatedOnDcom-fr-fr-Cb0XDRPL.js} +1 -1
  13. package/lib/src/{CreatedOnDcom-pt-br-Esnxy8wX.js → CreatedOnDcom-pt-br-BjdX3-E3.js} +1 -1
  14. package/lib/src/{CreatedOnDcom-pt-pt-CaiVHFXR.js → CreatedOnDcom-pt-pt-BXpbfopB.js} +1 -1
  15. package/lib/src/brand-page/constants/misc.constants.d.ts +8 -0
  16. package/lib/src/brand-page/constants/misc.constants.js +7 -0
  17. package/lib/src/brand-page/constants/styles.constants.d.ts +2 -0
  18. package/lib/src/brand-page/constants/styles.constants.js +2 -0
  19. package/lib/src/brand-page/models/brand-page-context.model.d.ts +0 -2
  20. package/lib/src/brand-page/models/brand-page-context.model.js +0 -12
  21. package/lib/src/brand-page/models/content.model.d.ts +1 -0
  22. package/lib/src/brand-page/models/content.model.js +1 -0
  23. package/lib/src/brand-page/models/index.d.ts +19 -0
  24. package/lib/src/brand-page/utils/content-callback-util.d.ts +2 -1
  25. package/lib/src/brand-page/utils/content-callback-util.js +3 -0
  26. package/lib/src/brand-page/utils/model.util.d.ts +2 -1
  27. package/lib/src/brand-page/utils/model.util.js +3 -0
  28. package/lib/src/brand-page/utils/view-model.util.d.ts +2 -0
  29. package/lib/src/brand-page/utils/view-model.util.js +24 -0
  30. package/lib/src/brand-page/view-models.d.ts +9 -2
  31. package/lib/src/brand-page-maker/components/action-bar/ActionBar.mixin.d.ts +0 -1
  32. package/lib/src/brand-page-maker/components/action-bar/ActionBar.mixin.js +0 -9
  33. package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.d.ts +10 -42
  34. package/lib/src/brand-page-maker/components/brandpage-maker-page/BrandPageMakerPage.mixin.d.ts +10 -42
  35. package/lib/src/brand-page-maker/components/preview/Preview.mixin.d.ts +0 -14
  36. package/lib/src/brand-page-maker/components/quick-link-bar/QuickLinkBar.mixin.d.ts +0 -42
  37. package/lib/src/brand-page-maker/components/seo-settings/SeoPageSettings.mixin.d.ts +0 -42
  38. package/lib/src/brand-page-maker/components/seo-settings/UrlSettings.mixin.d.ts +0 -42
  39. package/lib/src/brand-page-maker/components/seo-settings/UseExistingDomainModal.mixin.d.ts +0 -42
  40. package/lib/src/brand-page-maker/components/template-preview/TemplatePreview.mixin.d.ts +0 -42
  41. package/lib/src/brand-page-maker/components/upgrade-to-premium-button/UpgradeToPremiumButton.mixin.d.ts +0 -42
  42. package/lib/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.d.ts +13 -0
  43. package/lib/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.js +64 -30
  44. package/lib/src/brand-page-maker/constants/config-settings-constant.d.ts +2 -1
  45. package/lib/src/brand-page-maker/constants/config-settings-constant.js +101 -2
  46. package/lib/src/brand-page-maker/convertors/faq-display-style.convertor.d.ts +3 -0
  47. package/lib/src/brand-page-maker/convertors/faq-display-style.convertor.js +47 -0
  48. package/lib/src/brand-page-maker/models/content-settings-configs.d.ts +22 -1
  49. package/lib/src/brand-page-maker/models/content-settings-configs.js +1 -0
  50. package/lib/src/brand-page-maker/models/content-settings.d.ts +4 -1
  51. package/lib/src/brand-page-maker/models/content-settings.js +45 -1
  52. package/lib/src/brand-page-maker/stores/brandpage-container/brand-page-container-store.d.ts +0 -42
  53. package/lib/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.js +1 -0
  54. package/lib/src/brand-page-maker/utils/apply-color-strategy.js +47 -17
  55. package/lib/src/brand-page-maker/utils/rich-text-editor.util.d.ts +6 -0
  56. package/lib/src/brand-page-maker/utils/rich-text-editor.util.js +24 -1
  57. package/lib/src/brand-page-maker/validators/field.validator.js +2 -0
  58. package/lib/src/bundled-translations-DY9YvuL-.js +179 -0
  59. package/lib/src/bundles/bundled-translations.json +20 -2
  60. package/lib/src/composables/useTranslate.d.ts +1 -1
  61. package/lib/src/composables/useTranslate.js +1 -1
  62. package/lib/src/css/library.brand-page-brandCrowd.min.css +143 -36
  63. package/lib/src/css/library.brand-page-brandPage.min.css +143 -36
  64. package/lib/src/css/library.brand-page-designCom.min.css +143 -36
  65. package/lib/src/css/library.brand-page-preview.min.css +143 -36
  66. package/lib/src/index.mjs +159 -158
  67. package/lib/src/{lib-V7aqfuiB.js → lib-DwpSAbLZ.js} +26889 -26178
  68. package/lib/src/metadata-builder/parsers/section-tool-arguments.d.ts +1 -1
  69. package/lib/src/shared/color-strategies/color-strategies.js +4 -0
  70. package/lib/src/shared/color-strategies/rte-field-pairs.d.ts +12 -0
  71. package/lib/src/shared/color-strategies/rte-field-pairs.js +10 -0
  72. package/lib/src/shared/content-settings/components/content-settings/ContentSettings.mixin.d.ts +10 -0
  73. package/lib/src/shared/content-settings/components/content-settings/ReplaceImageSettingsForContent.mixin.d.ts +11 -1
  74. package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItem.mixin.d.ts +10 -0
  75. package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.d.ts +10 -0
  76. package/lib/src/shared/content-settings/components/focused-content-item-settings/FocusedContentItemSettings.mixin.d.ts +10 -0
  77. package/lib/src/shared/content-settings/components/items-field/ItemsField.mixin.d.ts +14 -1
  78. package/lib/src/shared/content-settings/components/items-field/ItemsField.mixin.js +11 -1
  79. package/lib/src/shared/content-settings/components/payment-config-field/PaymentConfigField.mixin.d.ts +0 -42
  80. package/lib/src/shared/content-settings/components/textarea-field/TextareaField.mixin.d.ts +12 -0
  81. package/lib/src/shared/content-settings/components/textarea-field/TextareaField.mixin.js +5 -0
  82. package/lib/src/shared/content-settings/models.d.ts +6 -0
  83. package/lib/src/shared/content-settings/store/content-settings-store.d.ts +14 -1
  84. package/lib/src/shared/content-settings/store/content-settings-store.js +24 -2
  85. package/lib/src/shared/logo-settings/components/LogoSettings.mixin.d.ts +0 -42
  86. package/lib/src/shared/page-settings/components/page-settings/PageSettings.mixin.d.ts +10 -42
  87. package/lib/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.d.ts +10 -42
  88. package/lib/src/shared/publish-brand-page-with-state/PublishBrandPageModalWithState.mixin.d.ts +0 -56
  89. package/lib/src/shared/side-bar/components/SideBar.mixin.d.ts +0 -42
  90. package/lib/src/shared/style-settings/components/StyleSettings.mixin.d.ts +0 -42
  91. package/package.json +2 -2
  92. package/src/brand-page/components/faq-section/FaqSection.test.ts +292 -0
  93. package/src/brand-page/components/faq-section/FaqSection.vue +299 -0
  94. package/src/brand-page/components/page/PageComponent.vue +2 -0
  95. package/src/brand-page/components/section-heading/SectionHeading.vue +52 -0
  96. package/src/brand-page/components/text-wrapper/TextWrapper.test.ts +33 -0
  97. package/src/brand-page/constants/misc.constants.ts +12 -0
  98. package/src/brand-page/constants/styles.constants.ts +2 -0
  99. package/src/brand-page/models/brand-page-context.model.ts +0 -23
  100. package/src/brand-page/models/content.model.ts +1 -0
  101. package/src/brand-page/models/index.ts +21 -0
  102. package/src/brand-page/utils/content-callback-util.ts +5 -0
  103. package/src/brand-page/utils/model.util.ts +7 -0
  104. package/src/brand-page/utils/view-model.util.test.ts +96 -0
  105. package/src/brand-page/utils/view-model.util.ts +49 -0
  106. package/src/brand-page/view-models.ts +12 -1
  107. package/src/brand-page-maker/components/action-bar/ActionBar.mixin.ts +0 -10
  108. package/src/brand-page-maker/components/action-bar/ActionBarNavigation.test.ts +8 -49
  109. package/src/brand-page-maker/components/action-bar/ActionBarNavigation.vue +1 -1
  110. package/src/brand-page-maker/components/rich-text-editor/RichTextEditor.test.ts +165 -0
  111. package/src/brand-page-maker/components/rich-text-editor/RichTextEditor.vue +37 -49
  112. package/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.ts +87 -30
  113. package/src/brand-page-maker/constants/config-settings-constant.ts +108 -0
  114. package/src/brand-page-maker/constants/faq-section-config.test.ts +151 -0
  115. package/src/brand-page-maker/convertors/faq-display-style.convertor.ts +59 -0
  116. package/src/brand-page-maker/convertors/i18n/converter.json +4 -1
  117. package/src/brand-page-maker/models/content-settings-configs.ts +24 -0
  118. package/src/brand-page-maker/models/content-settings.ts +57 -0
  119. package/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.ts +2 -0
  120. package/src/brand-page-maker/utils/__snapshots__/apply-color-strategy.golden.test.ts.snap +4063 -895
  121. package/src/brand-page-maker/utils/apply-color-strategy.test.ts +104 -2
  122. package/src/brand-page-maker/utils/apply-color-strategy.ts +77 -19
  123. package/src/brand-page-maker/utils/rich-text-editor.util.test.ts +110 -0
  124. package/src/brand-page-maker/utils/rich-text-editor.util.ts +66 -0
  125. package/src/brand-page-maker/validators/field.validator.ts +2 -0
  126. package/src/bundles/bundled-translations.json +20 -2
  127. package/src/composables/useTranslate.ts +2 -1
  128. package/src/metadata-builder/builders/item-id-utils.test.ts +24 -0
  129. package/src/metadata-builder/parsers/section-tool-arguments.ts +2 -1
  130. package/src/metadata-builder/parsers/section-tool-map.test.ts +12 -0
  131. package/src/shared/color-strategies/color-strategies.ts +4 -0
  132. package/src/shared/color-strategies/rte-field-pairs.ts +23 -0
  133. package/src/shared/content-settings/components/content-settings/i18n/content-settings.json +17 -1
  134. package/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.vue +3 -0
  135. package/src/shared/content-settings/components/items-field/ItemsField.mixin.ts +11 -1
  136. package/src/shared/content-settings/components/items-field/ItemsField.vue +8 -2
  137. package/src/shared/content-settings/components/list-items-field/ListItemsField.test.ts +429 -0
  138. package/src/shared/content-settings/components/list-items-field/ListItemsField.vue +262 -0
  139. package/src/shared/content-settings/components/textarea-field/TextareaField.mixin.ts +6 -0
  140. package/src/shared/content-settings/components/textarea-field/TextareaField.vue +1 -0
  141. package/src/shared/content-settings/models.ts +7 -0
  142. package/src/shared/content-settings/store/content-settings-store.test.ts +70 -0
  143. package/src/shared/content-settings/store/content-settings-store.ts +37 -3
  144. package/src/shared/ecommerce-settings/util/refreshDuplicatedMetadata.test.ts +19 -0
  145. package/src/shared/page-settings/store/page-settings-store.test.ts +0 -71
  146. package/src/utils/minify-brand-page-metadata.test.ts +35 -0
  147. package/lib/src/bundled-translations-BYUfWBT3.js +0 -179
@@ -394,7 +394,11 @@ import {
394
394
  } from '@designcrowd/fe-shared-lib';
395
395
  import { EditorContent } from '@tiptap/vue-3';
396
396
  import { BubbleMenu } from '@tiptap/vue-3/menus';
397
- import { useRichTextEditor } from '../../composables/rich-text-editor/useRichTextEditor';
397
+ import {
398
+ useRichTextEditor,
399
+ writeListItemStyles,
400
+ } from '../../composables/rich-text-editor/useRichTextEditor';
401
+ import type { RteItemStyleTarget } from '@shared/content-settings/models';
398
402
  import FontPicker from '@shared/font-family-selector/FontPicker.vue';
399
403
  import ColorField from '@shared/content-settings/components/color-field/ColorField.vue';
400
404
  import UrlField from '@shared/content-settings/components/url-field/UrlField.vue';
@@ -460,6 +464,8 @@ const props = withDefaults(
460
464
  textItemIndex: number;
461
465
  item: TextItem;
462
466
  contentType: string;
467
+ /** Edit a list item's text: toolbar styles go to that item's own style field. */
468
+ itemStyleTarget?: RteItemStyleTarget;
463
469
  }>(),
464
470
  {
465
471
  id: '',
@@ -472,6 +478,7 @@ const props = withDefaults(
472
478
  textStyleName: '',
473
479
  styles: () => ({}),
474
480
  rawId: '',
481
+ itemStyleTarget: undefined,
475
482
  },
476
483
  );
477
484
 
@@ -602,6 +609,29 @@ const isAddLinkButtonDisabled = computed(() => {
602
609
  );
603
610
  });
604
611
 
612
+ // Where this editor's role/colour writes land: the list item it edits, a Text block
613
+ // item, or the section's own `${name}Style`.
614
+ const writeStyles = (value: TextStyleDeclaration): void => {
615
+ if (writeListItemStyles(contentSettingStore, props, value)) {
616
+ return;
617
+ }
618
+
619
+ if (props.name === 'textItems') {
620
+ contentSettingStore.updateTextItemTextStyleTypeWithStyles({
621
+ id: props.rawId,
622
+ name: props.name,
623
+ value,
624
+ index: props.textItemIndex,
625
+ });
626
+ } else {
627
+ contentSettingStore.updateTextStyles({
628
+ id: props.rawId,
629
+ name: props.name,
630
+ value,
631
+ });
632
+ }
633
+ };
634
+
605
635
  // Keeps a text ITEM's own style in step with the global text style it links to.
606
636
  watch(
607
637
  () =>
@@ -641,20 +671,7 @@ watch(
641
671
  backgroundColor: undefined,
642
672
  };
643
673
 
644
- if (props.name === 'textItems') {
645
- contentSettingStore.updateTextItemTextStyleTypeWithStyles({
646
- id: props.rawId,
647
- name: props.name,
648
- value,
649
- index: props.textItemIndex,
650
- });
651
- } else {
652
- contentSettingStore.updateTextStyles({
653
- id: props.rawId,
654
- name: props.name,
655
- value,
656
- });
657
- }
674
+ writeStyles(value);
658
675
  }
659
676
  } else {
660
677
  // make sure text style options are updated and selected Global style is set to none
@@ -685,26 +702,10 @@ const onSelectTextStyleType = (option: ButtonGroupOption) => {
685
702
  if (optionSelected) {
686
703
  selectedGlobalTextStyles.value = item.value;
687
704
  if (option.value === undefined) {
688
- if (props.name === 'textItems') {
689
- contentSettingStore.updateTextItemTextStyleTypeWithStyles({
690
- id: props.rawId,
691
- name: props.name,
692
- value: {
693
- textStyleType: undefined,
694
- fontSize: fontSettings.selectedFontSize,
695
- },
696
- index: props.textItemIndex,
697
- });
698
- } else {
699
- contentSettingStore.updateTextStyles({
700
- id: props.rawId,
701
- name: props.name,
702
- value: {
703
- textStyleType: undefined,
704
- fontSize: fontSettings.selectedFontSize,
705
- },
706
- });
707
- }
705
+ writeStyles({
706
+ textStyleType: undefined,
707
+ fontSize: fontSettings.selectedFontSize,
708
+ });
708
709
  } else {
709
710
  const selectedStyle: BrandPageCssStyle =
710
711
  styleSettingsStore.textStyles.textStyleOptions[
@@ -723,20 +724,7 @@ const onSelectTextStyleType = (option: ButtonGroupOption) => {
723
724
  backgroundColor: undefined,
724
725
  };
725
726
 
726
- if (props.name === 'textItems') {
727
- contentSettingStore.updateTextItemTextStyleTypeWithStyles({
728
- id: props.rawId,
729
- name: props.name,
730
- value,
731
- index: props.textItemIndex,
732
- });
733
- } else {
734
- contentSettingStore.updateTextStyles({
735
- id: props.rawId,
736
- name: props.name,
737
- value,
738
- });
739
- }
727
+ writeStyles(value);
740
728
  }
741
729
  item.isSelected = true;
742
730
  } else {
@@ -32,6 +32,7 @@ import {
32
32
  import type { ButtonGroupOption } from '@brand-page-maker/models/button-group.model';
33
33
  import { letterSpacingValueConvertor } from '@brand-page-maker/convertors';
34
34
  import { useUiContentSettingsStore } from '@shared/content-settings/store';
35
+ import type { RteItemStyleTarget } from '@shared/content-settings/models';
35
36
  import { useHistoryStore } from '@brand-page-maker/stores/history/history-store';
36
37
  import { AllSelection } from 'prosemirror-state';
37
38
  import { useHyperlink } from './hyperlink';
@@ -113,6 +114,32 @@ export const useExternalRichTextEditor = (): {
113
114
  };
114
115
  };
115
116
 
117
+ /**
118
+ * An editor bound to a list item (`itemStyleTarget`) writes its toolbar styles to that
119
+ * item's own style field instead of the section-level `${name}Style`. Returns false when
120
+ * the editor has no item target, so the caller falls through to its section-level write.
121
+ */
122
+ export const writeListItemStyles = (
123
+ store: ReturnType<typeof useUiContentSettingsStore>,
124
+ props: { rawId: string; name: string; itemStyleTarget?: RteItemStyleTarget },
125
+ value: TextStyleDeclaration,
126
+ isHydrationWrite?: boolean,
127
+ ): boolean => {
128
+ if (!props.itemStyleTarget) {
129
+ return false;
130
+ }
131
+
132
+ store.updateListItemTextStyles({
133
+ id: props.rawId,
134
+ name: props.name,
135
+ value,
136
+ target: props.itemStyleTarget,
137
+ isHydrationWrite,
138
+ });
139
+
140
+ return true;
141
+ };
142
+
116
143
  export const useRichTextEditor = (
117
144
  model: ModelRef<any>,
118
145
  props: {
@@ -126,6 +153,7 @@ export const useRichTextEditor = (
126
153
  textItemIndex: number;
127
154
  item: TextItem;
128
155
  contentType: string;
156
+ itemStyleTarget?: RteItemStyleTarget;
129
157
  },
130
158
  emit: (event: 'focus' | 'blur' | 'update:modelValue', ...args: any[]) => void,
131
159
  fontSettings: {
@@ -426,6 +454,23 @@ export const useRichTextEditor = (
426
454
  }
427
455
  });
428
456
 
457
+ // Set while a toolbar command runs. The toolbar has taken focus by then, so
458
+ // `isFocused` alone would tag the command's write as hydration and fold it into
459
+ // the previous undo entry — hidden only when a style write rides along and opens
460
+ // an entry of its own. Synchronous commands only: the spacing sliders dispatch
461
+ // after a debounce and are not covered.
462
+ let isRunningToolbarCommand = false;
463
+
464
+ const runAsToolbarCommand = <T>(command: () => T): T => {
465
+ isRunningToolbarCommand = true;
466
+
467
+ try {
468
+ return command();
469
+ } finally {
470
+ isRunningToolbarCommand = false;
471
+ }
472
+ };
473
+
429
474
  // Update model when editor content changes. RTE content is stored as Tiptap
430
475
  // JSON (BP-5787). getJSON() is already in hand (the editor holds the doc) and
431
476
  // is sanitised natively — no getHTML(), no DOMPurify, no extra editor — so
@@ -439,15 +484,9 @@ export const useRichTextEditor = (
439
484
  // field → store chain and arms the history merge at the moment of the
440
485
  // store write — arming here at emit time would lapse before the
441
486
  // debounce fires. Genuine edits happen while focused and commit
442
- // normally.
443
- //
444
- // Known limitation: a programmatic toolbar command run on an editor the
445
- // user hasn't focused yet would also merge (losing that undo step). Today
446
- // the RTE only mounts inside an already-open settings drawer and the user
447
- // focuses it before formatting, so this doesn't occur — revisit if a
448
- // consumer ever drives toolbar actions on an unfocused editor.
487
+ // normally, and so do synchronous toolbar commands (isRunningToolbarCommand).
449
488
  emit('update:modelValue', sanitizeRteJson(editor.getJSON()), {
450
- isHydrationWrite: !editor.isFocused,
489
+ isHydrationWrite: !editor.isFocused && !isRunningToolbarCommand,
451
490
  });
452
491
  });
453
492
 
@@ -551,26 +590,38 @@ export const useRichTextEditor = (
551
590
  // write lands ~100ms BEFORE the editor's canonical text write-back, and
552
591
  // an unflagged write here would open a plain capture burst that
553
592
  // swallows the flagged text write into a phantom entry (BP-5810).
554
- contentSettingStore.updateTextStyles({
555
- id: props.rawId,
556
- name: props.name,
557
- value: {
558
- fontSize: globalStyles.fontSize,
559
- fontFamily: globalStyles.fontFamily,
560
- fontWeight: globalStyles.fontWeight,
561
- fontStyle: globalStyles.fontStyle,
562
- textDecoration: globalStyles.textDecoration,
563
- // Keep an element colour override (a creation-time contrast bake, or a
564
- // colour picked while linked) — only fall back to the global style's colour
565
- // when the element has none, mirroring `resolveTextColor`'s element-first
566
- // cascade. Blindly taking `globalStyles.color` here re-derived the linked
567
- // colour on every RTE mount and clobbered the override.
568
- color: props.styles?.color || globalStyles.color,
569
- lineHeight: globalStyles.lineHeight,
570
- letterSpacing: globalStyles.letterSpacing,
571
- },
572
- isHydrationWrite: !editor.isFocused,
573
- });
593
+ const syncedStyles = {
594
+ fontSize: globalStyles.fontSize,
595
+ fontFamily: globalStyles.fontFamily,
596
+ fontWeight: globalStyles.fontWeight,
597
+ fontStyle: globalStyles.fontStyle,
598
+ textDecoration: globalStyles.textDecoration,
599
+ // Keep an element colour override (a creation-time contrast bake, or a
600
+ // colour picked while linked) — only fall back to the global style's colour
601
+ // when the element has none, mirroring `resolveTextColor`'s element-first
602
+ // cascade. Blindly taking `globalStyles.color` here re-derived the linked
603
+ // colour on every RTE mount and clobbered the override.
604
+ color: props.styles?.color || globalStyles.color,
605
+ lineHeight: globalStyles.lineHeight,
606
+ letterSpacing: globalStyles.letterSpacing,
607
+ };
608
+ const isHydrationWrite = !editor.isFocused;
609
+
610
+ if (
611
+ !writeListItemStyles(
612
+ contentSettingStore,
613
+ props,
614
+ syncedStyles,
615
+ isHydrationWrite,
616
+ )
617
+ ) {
618
+ contentSettingStore.updateTextStyles({
619
+ id: props.rawId,
620
+ name: props.name,
621
+ value: syncedStyles,
622
+ isHydrationWrite,
623
+ });
624
+ }
574
625
 
575
626
  // sync toolbar editor styles
576
627
  fontSettings.selectedFont = globalStyles?.fontFamily ?? 'Lato';
@@ -762,6 +813,10 @@ export const useRichTextEditor = (
762
813
  };
763
814
 
764
815
  const syncContentSettings = (fontStyleChanges: TextStyleDeclaration) => {
816
+ if (writeListItemStyles(contentSettingStore, props, fontStyleChanges)) {
817
+ return;
818
+ }
819
+
765
820
  if (props.name === 'textItems') {
766
821
  contentSettingStore.updateTextItemTextStyleTypeWithStyles({
767
822
  id: props.rawId,
@@ -897,7 +952,8 @@ export const useRichTextEditor = (
897
952
  editor,
898
953
  onFocus,
899
954
  onBlur,
900
- triggerEditorStyleChange,
955
+ triggerEditorStyleChange: (editorOption: EditorOptionType) =>
956
+ runAsToolbarCommand(() => triggerEditorStyleChange(editorOption)),
901
957
  toggleBold,
902
958
  toggleItalic,
903
959
  toggleUnderline,
@@ -914,7 +970,8 @@ export const useRichTextEditor = (
914
970
  setInitialStyleSettings,
915
971
  copyGlobalStyleSettings,
916
972
  unsetAllMarks,
917
- handleHyperlink,
973
+ handleHyperlink: (...args: Parameters<typeof handleHyperlink>) =>
974
+ runAsToolbarCommand(() => handleHyperlink(...args)),
918
975
  hyperlinkValue,
919
976
  setHyperlinkUrlValue,
920
977
  shouldShow,
@@ -35,8 +35,11 @@ import type {
35
35
  Content,
36
36
  ShopContent,
37
37
  BookingSectionContent,
38
+ FaqSectionContent,
38
39
  } from '@brand-page';
39
40
  import {
41
+ FAQ_DISPLAY_STYLES,
42
+ FAQ_MAX_ITEMS,
40
43
  SOCIAL_LINKS_ICON_DISPLAY_STYLES,
41
44
  BRAND_PAGE_TYPES,
42
45
  DIVIDER_STYLES,
@@ -84,6 +87,9 @@ import {
84
87
  VARIANT_DISPLAY_MODES,
85
88
  createInitialShopContent,
86
89
  createInitialBookingSectionContent,
90
+ createInitialFaqSectionContent,
91
+ createInitialFaqQuestionStyle,
92
+ createInitialFaqAnswerStyle,
87
93
  } from '../models';
88
94
  import * as uuid from 'uuid';
89
95
  import type {
@@ -149,6 +155,7 @@ import {
149
155
  import { socialLinksIconDisplayStyleConvertor } from '../convertors/social-links-icon-display-style.convertor';
150
156
  import { getTextMediaOptionsConverterByMediaType } from '../convertors/text-image-display-style.convertor';
151
157
  import { testimonialDisplayStyleConvertor } from '../convertors/testimonial-display-style-convertor';
158
+ import { faqDisplayStyleConvertor } from '../convertors/faq-display-style.convertor';
152
159
  import { pageValidator, urlValidator } from '../validators';
153
160
  import { imagesSectionAspectRatioVisibilityAssessor } from '../assessors/images-section-aspect-ratio-visibility.assessor';
154
161
  import {
@@ -3295,6 +3302,106 @@ export const BOOKING_SECTION_SETTINGS_CONFIG =
3295
3302
  isLocked: false,
3296
3303
  });
3297
3304
 
3305
+ const FAQ_FIELD_GROUP_IDS = {
3306
+ settings: 'settings',
3307
+ items: 'items',
3308
+ style: 'style',
3309
+ } as const;
3310
+
3311
+ export const FAQ_SECTION_SETTINGS_CONFIG =
3312
+ (): ContentSettingsConfig<FaqSectionContent> => ({
3313
+ type: 'faq-section',
3314
+ category: 'general',
3315
+ createInitialContent: createInitialFaqSectionContent,
3316
+ displayMode: CONTENT_SETTINGS_DISPLAY_MODES.focused,
3317
+ defaultIconName: 'question-circle-filled',
3318
+ displayName: contentSettingsTr(`faqSectionDisplayName`),
3319
+ fieldGroups: [
3320
+ {
3321
+ id: FAQ_FIELD_GROUP_IDS.settings,
3322
+ displayName: contentSettingsTr(`settingsDisplayName`),
3323
+ isCollapsed: false,
3324
+ },
3325
+ {
3326
+ id: FAQ_FIELD_GROUP_IDS.items,
3327
+ displayName: contentSettingsTr(`faqItemsDisplayName`),
3328
+ isCollapsed: false,
3329
+ },
3330
+ {
3331
+ id: FAQ_FIELD_GROUP_IDS.style,
3332
+ displayName: contentSettingsTr(`styleDisplayName`),
3333
+ isCollapsed: true,
3334
+ },
3335
+ ],
3336
+ fields: {
3337
+ titleStyle: DEFAULT_TITLE_STYLE_SETTINGS_FILED_WITH_TITLE(),
3338
+ descriptionStyle: DEFAULT_DESCRIPTION_STYLE_FILED(),
3339
+ title: {
3340
+ type: 'text',
3341
+ placeholder: contentSettingsTr(`titlePlaceholder`),
3342
+ title: contentSettingsTr(`titleTitle`),
3343
+ groupId: FAQ_FIELD_GROUP_IDS.settings,
3344
+ maxLength: 200,
3345
+ },
3346
+ description: {
3347
+ type: 'textarea',
3348
+ placeholder: contentSettingsTr(`descriptionPlaceholder`),
3349
+ title: contentSettingsTr(`descriptionTitle`),
3350
+ groupId: FAQ_FIELD_GROUP_IDS.settings,
3351
+ maxLength: 1000,
3352
+ },
3353
+ items: {
3354
+ type: 'list-items',
3355
+ defaultValue: [],
3356
+ groupId: FAQ_FIELD_GROUP_IDS.items,
3357
+ maxLength: FAQ_MAX_ITEMS,
3358
+ itemFields: [
3359
+ {
3360
+ key: 'question',
3361
+ kind: 'text',
3362
+ styleKey: 'questionStyle',
3363
+ defaultStyle: () => createInitialFaqQuestionStyle(),
3364
+ maxLength: 200,
3365
+ title: contentSettingsTr(`faqQuestionTitle`),
3366
+ placeholder: contentSettingsTr(`faqQuestionPlaceholder`),
3367
+ defaultValue: () => contentSettingsTr(`faqNewQuestion`),
3368
+ },
3369
+ {
3370
+ key: 'answer',
3371
+ kind: 'textarea',
3372
+ styleKey: 'answerStyle',
3373
+ defaultStyle: () => createInitialFaqAnswerStyle(),
3374
+ maxLength: 1000,
3375
+ title: contentSettingsTr(`faqAnswerTitle`),
3376
+ placeholder: contentSettingsTr(`faqAnswerPlaceholder`),
3377
+ defaultValue: () => contentSettingsTr(`faqNewAnswer`),
3378
+ },
3379
+ ],
3380
+ },
3381
+ displayStyle: {
3382
+ type: 'radio',
3383
+ role: 'multi',
3384
+ defaultValue: FAQ_DISPLAY_STYLES.centered,
3385
+ title: contentSettingsTr(`displayStyleTitle`),
3386
+ optionsConvertor: faqDisplayStyleConvertor,
3387
+ groupId: FAQ_FIELD_GROUP_IDS.style,
3388
+ variant: 'icon',
3389
+ },
3390
+ ...PADDING_CONTROL_FIELDS(FAQ_FIELD_GROUP_IDS.style),
3391
+ backgroundStyle: {
3392
+ type: 'color',
3393
+ groupId: FAQ_FIELD_GROUP_IDS.style,
3394
+ defaultValue: {},
3395
+ isInline: true,
3396
+ showToggle: false,
3397
+ title: contentSettingsTr(`backgroundColorTitle`),
3398
+ valueConvertor: backgroundColorConvertor,
3399
+ },
3400
+ },
3401
+ isEnabled: true,
3402
+ isLocked: false,
3403
+ });
3404
+
3298
3405
  export const CONTENT_SETTINGS_CONFIGS = (): Record<
3299
3406
  ContentType,
3300
3407
  ContentSettingsConfig<any>
@@ -3331,4 +3438,5 @@ export const CONTENT_SETTINGS_CONFIGS = (): Record<
3331
3438
  'shapes-section': SHAPES_SECTION_SETTINGS_CONFIG(),
3332
3439
  'shop-section': SHOP_SECTION_SETTINGS_CONFIG(),
3333
3440
  'booking-section': BOOKING_SECTION_SETTINGS_CONFIG(),
3441
+ 'faq-section': FAQ_SECTION_SETTINGS_CONFIG(),
3334
3442
  });
@@ -0,0 +1,151 @@
1
+ import { vi } from 'vitest';
2
+ import { createPinia, setActivePinia } from 'pinia';
3
+ import {
4
+ BRAND_PAGE_TYPES,
5
+ FAQ_DISPLAY_STYLES,
6
+ FAQ_MAX_ITEMS,
7
+ } from '@brand-page';
8
+ import { FAQ_SECTION_SETTINGS_CONFIG } from './config-settings-constant';
9
+ import { createInitialFaqSectionContent } from '../models/content-settings';
10
+ import { faqDisplayStyleConvertor } from '../convertors/faq-display-style.convertor';
11
+ import { getEnabledContentSettingsConfigs } from '../stores/brandpage-maker/brandpage-maker-store-utils';
12
+ import { useBrandPageMakerStore } from '../stores/brandpage-maker/brandpage-maker-store';
13
+ import { RTE_ITEM_FIELD_PAIRS } from '@shared/color-strategies/rte-field-pairs';
14
+
15
+ // Tests load no translation bundles; return the key so defaults are assertable.
16
+ vi.mock('@src/composables/useTranslate', async (importOriginal) => ({
17
+ ...(await importOriginal<Record<string, unknown>>()),
18
+ contentSettingsTr: (key: string) => key,
19
+ }));
20
+
21
+ describe('FAQ section config', () => {
22
+ test('is a General block with no website-type restriction', () => {
23
+ const config = FAQ_SECTION_SETTINGS_CONFIG();
24
+
25
+ expect(config.category).toBe('general');
26
+ expect(config.brandPageType).toBeUndefined();
27
+ expect(config.defaultIconName).toBe('question-circle-filled');
28
+ });
29
+
30
+ test.each([
31
+ BRAND_PAGE_TYPES.default,
32
+ BRAND_PAGE_TYPES.brandSite,
33
+ BRAND_PAGE_TYPES.brandContact,
34
+ ])('is offered in Add Content on %s', (brandPageType) => {
35
+ setActivePinia(createPinia());
36
+ useBrandPageMakerStore().$patch({ brandPageType });
37
+
38
+ const types = getEnabledContentSettingsConfigs().map((c) => c.type);
39
+
40
+ expect(types).toContain('faq-section');
41
+ });
42
+
43
+ test('caps items at 6 and labels rows by question', () => {
44
+ const items = FAQ_SECTION_SETTINGS_CONFIG().fields.items as any;
45
+
46
+ expect(items.type).toBe('list-items');
47
+ expect(items.maxLength).toBe(FAQ_MAX_ITEMS);
48
+ expect(FAQ_MAX_ITEMS).toBe(6);
49
+ expect(items.itemFields.map((f: any) => [f.key, f.kind])).toEqual([
50
+ ['question', 'text'],
51
+ ['answer', 'textarea'],
52
+ ]);
53
+ });
54
+
55
+ test('question and answer are rich text with their own per-item style, capped like title and description', () => {
56
+ const items = FAQ_SECTION_SETTINGS_CONFIG().fields.items as any;
57
+
58
+ expect(
59
+ items.itemFields.map((f: any) => [f.key, f.styleKey, f.maxLength]),
60
+ ).toEqual([
61
+ ['question', 'questionStyle', 200],
62
+ ['answer', 'answerStyle', 1000],
63
+ ]);
64
+ expect(
65
+ items.itemFields.map((f: any) => f.defaultStyle().textStyleType),
66
+ ).toEqual(['headings', 'paragraphs']);
67
+ });
68
+
69
+ test('has no section-level Question/Answer style — each item carries its own', () => {
70
+ const { fields } = FAQ_SECTION_SETTINGS_CONFIG() as any;
71
+
72
+ expect(fields.questionStyle).toBeUndefined();
73
+ expect(fields.answerStyle).toBeUndefined();
74
+ expect(RTE_ITEM_FIELD_PAIRS['faq-section']).toEqual([
75
+ {
76
+ itemsField: 'items',
77
+ textField: 'question',
78
+ styleField: 'questionStyle',
79
+ },
80
+ { itemsField: 'items', textField: 'answer', styleField: 'answerStyle' },
81
+ ]);
82
+ });
83
+ });
84
+
85
+ describe('createInitialFaqSectionContent', () => {
86
+ test('starts with one sample item and the demo defaults', () => {
87
+ const content = createInitialFaqSectionContent({
88
+ brandPageType: BRAND_PAGE_TYPES.brandSite,
89
+ } as any);
90
+
91
+ expect(content.type).toBe('faq-section');
92
+ expect(content.displayStyle).toBe(FAQ_DISPLAY_STYLES.centered);
93
+ expect(content.topPadding).toBe(32);
94
+ expect(content.bottomPadding).toBe(32);
95
+ expect(content.backgroundStyle).toEqual({});
96
+ expect(content.items).toHaveLength(1);
97
+ expect(content.items[0]).toMatchObject({
98
+ id: expect.any(String),
99
+ question: 'defaultFaqQuestion',
100
+ answer: 'defaultFaqAnswer',
101
+ });
102
+ expect(content.titleStyle.textStyleType).toBe('titles');
103
+ expect(content.descriptionStyle.textStyleType).toBe('paragraphs');
104
+ content.items.forEach((item) => {
105
+ expect(item.questionStyle).toEqual({
106
+ applyAutoScale: true,
107
+ textStyleType: 'headings',
108
+ textAlign: 'left',
109
+ });
110
+ expect(item.answerStyle).toEqual({
111
+ applyAutoScale: true,
112
+ textStyleType: 'paragraphs',
113
+ textAlign: 'left',
114
+ });
115
+ });
116
+ });
117
+ });
118
+
119
+ describe('faqDisplayStyleConvertor', () => {
120
+ test('offers the three styles with their icons', () => {
121
+ const { options } = faqDisplayStyleConvertor.convert(
122
+ FAQ_DISPLAY_STYLES.split,
123
+ );
124
+
125
+ expect(
126
+ options.map((o: any) => [o.value, o.iconName, o.isSelected]),
127
+ ).toEqual([
128
+ [FAQ_DISPLAY_STYLES.centered, 'display-style-faq-1', false],
129
+ [FAQ_DISPLAY_STYLES.split, 'display-style-faq-2', true],
130
+ [FAQ_DISPLAY_STYLES.grid, 'display-style-faq-3', false],
131
+ ]);
132
+ });
133
+
134
+ test.each([
135
+ [FAQ_DISPLAY_STYLES.centered, 'center'],
136
+ [FAQ_DISPLAY_STYLES.split, 'left'],
137
+ [FAQ_DISPLAY_STYLES.grid, 'center'],
138
+ ])(
139
+ 'switching to %s only moves the display style and heading alignment',
140
+ (value, textAlign) => {
141
+ const config = faqDisplayStyleConvertor.convert(value);
142
+ const result = faqDisplayStyleConvertor.convertBack(config);
143
+
144
+ expect(result).toEqual({
145
+ displayStyle: value,
146
+ titleStyle: { textAlign },
147
+ descriptionStyle: { textAlign },
148
+ });
149
+ },
150
+ );
151
+ });
@@ -0,0 +1,59 @@
1
+ import type { ButtonGroupConfig, ValueConvertor } from '../models';
2
+ import { FAQ_DISPLAY_STYLES } from '@brand-page/constants';
3
+ import type { FaqSectionContent } from '@brand-page/models';
4
+ import { converterTr } from '@src/composables/useTranslate';
5
+
6
+ const ICON_VIEW_BOX = '0 0 96 56';
7
+
8
+ export const faqDisplayStyleConvertor: ValueConvertor<
9
+ string | null,
10
+ ButtonGroupConfig,
11
+ Partial<FaqSectionContent>
12
+ > = {
13
+ convertBackToMultipleValues: true,
14
+ convert(value: string | null): ButtonGroupConfig {
15
+ return {
16
+ options: [
17
+ {
18
+ title: converterTr(`faqCenteredAccordion`),
19
+ value: FAQ_DISPLAY_STYLES.centered,
20
+ isSelected: value === FAQ_DISPLAY_STYLES.centered,
21
+ iconName: 'display-style-faq-1',
22
+ iconViewBox: ICON_VIEW_BOX,
23
+ dataAttribute: 'data-test-btn-faq-centered',
24
+ },
25
+ {
26
+ title: converterTr(`faqSplitAccordion`),
27
+ value: FAQ_DISPLAY_STYLES.split,
28
+ isSelected: value === FAQ_DISPLAY_STYLES.split,
29
+ iconName: 'display-style-faq-2',
30
+ iconViewBox: ICON_VIEW_BOX,
31
+ dataAttribute: 'data-test-btn-faq-split',
32
+ },
33
+ {
34
+ title: converterTr(`faqGrid`),
35
+ value: FAQ_DISPLAY_STYLES.grid,
36
+ isSelected: value === FAQ_DISPLAY_STYLES.grid,
37
+ iconName: 'display-style-faq-3',
38
+ iconViewBox: ICON_VIEW_BOX,
39
+ dataAttribute: 'data-test-btn-faq-grid',
40
+ },
41
+ ],
42
+ };
43
+ },
44
+
45
+ // The heading sits left beside the accordion in split, centred above it otherwise.
46
+ convertBack(config: ButtonGroupConfig): Partial<FaqSectionContent> {
47
+ const displayStyle =
48
+ config.options.find((v) => v.isSelected)?.value ??
49
+ FAQ_DISPLAY_STYLES.centered;
50
+ const textAlign =
51
+ displayStyle === FAQ_DISPLAY_STYLES.split ? 'left' : 'center';
52
+
53
+ return {
54
+ displayStyle,
55
+ titleStyle: { textAlign },
56
+ descriptionStyle: { textAlign },
57
+ };
58
+ },
59
+ };
@@ -21,6 +21,9 @@
21
21
  "medium": "Medium",
22
22
  "large": "Large",
23
23
  "bigImage": "Big Image",
24
- "columns": "Columns"
24
+ "columns": "Columns",
25
+ "faqCenteredAccordion": "Centered accordion",
26
+ "faqSplitAccordion": "Split accordion",
27
+ "faqGrid": "Grid"
25
28
  }
26
29
  }