@designcrowd/library.brand-page 6.0.26-json-prereq-2 → 6.0.26

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 (155) hide show
  1. package/lib/src/{CreatedOnBrandCrowd-de-de-CKBqjDH3.js → CreatedOnBrandCrowd-de-de-DU6YEVcW.js} +1 -1
  2. package/lib/src/{CreatedOnBrandCrowd-en-us-CRXiimvT.js → CreatedOnBrandCrowd-en-us-DNWWK6Tj.js} +1 -1
  3. package/lib/src/{CreatedOnBrandCrowd-es-es-DCYdY6sj.js → CreatedOnBrandCrowd-es-es-DVFvV9Qp.js} +1 -1
  4. package/lib/src/{CreatedOnBrandCrowd-fr-fr-Dk7LOhxE.js → CreatedOnBrandCrowd-fr-ca-B4ZkjNdw.js} +1 -1
  5. package/lib/src/{CreatedOnBrandCrowd-fr-ca-Dk7LOhxE.js → CreatedOnBrandCrowd-fr-fr-B4ZkjNdw.js} +1 -1
  6. package/lib/src/{CreatedOnBrandCrowd-pt-pt-Cp4ufHJP.js → CreatedOnBrandCrowd-pt-br-DP7AQGgW.js} +1 -1
  7. package/lib/src/{CreatedOnBrandCrowd-pt-br-Cp4ufHJP.js → CreatedOnBrandCrowd-pt-pt-DP7AQGgW.js} +1 -1
  8. package/lib/src/{CreatedOnDcom-de-de-ZSLkWMwM.js → CreatedOnDcom-de-de-CaC3GtG9.js} +1 -1
  9. package/lib/src/{CreatedOnDcom-en-us-BgdIGfT3.js → CreatedOnDcom-en-us-D16yT0-P.js} +1 -1
  10. package/lib/src/{CreatedOnDcom-es-es-D0KemkCZ.js → CreatedOnDcom-es-es-DA5sHgD_.js} +1 -1
  11. package/lib/src/{CreatedOnDcom-fr-ca-BuwNjNbC.js → CreatedOnDcom-fr-ca-CDv2SG1-.js} +1 -1
  12. package/lib/src/{CreatedOnDcom-fr-fr-DCQcsFPf.js → CreatedOnDcom-fr-fr-Cygc7c5d.js} +1 -1
  13. package/lib/src/{CreatedOnDcom-pt-br-DD-oLAS1.js → CreatedOnDcom-pt-br-B_3RIzgF.js} +1 -1
  14. package/lib/src/{CreatedOnDcom-pt-pt-D8qLq2wW.js → CreatedOnDcom-pt-pt-Bz5w-rrD.js} +1 -1
  15. package/lib/src/brand-page/components/contact-form/ContactForm.mixin.js +2 -5
  16. package/lib/src/brand-page/models/index.d.ts +26 -34
  17. package/lib/src/brand-page/utils/migration.util.js +0 -11
  18. package/lib/src/brand-page/utils/text.util.d.ts +1 -1
  19. package/lib/src/brand-page/utils/text.util.js +0 -32
  20. package/lib/src/brand-page/utils/vcard.util.js +4 -4
  21. package/lib/src/brand-page/utils/view-model.util.d.ts +0 -1
  22. package/lib/src/brand-page/utils/view-model.util.js +20 -68
  23. package/lib/src/brand-page/view-models.d.ts +3 -3
  24. package/lib/src/brand-page-maker/composables/ai-chat/useAiMetadataBuilder.d.ts +1 -0
  25. package/lib/src/brand-page-maker/composables/rich-text-editor/bubbleMenu.d.ts +9 -2
  26. package/lib/src/brand-page-maker/composables/rich-text-editor/bubbleMenu.js +10 -6
  27. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/FontSize.d.ts +20 -0
  28. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/FontSize.js +44 -0
  29. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/LineHeight.d.ts +27 -0
  30. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/LineHeight.js +44 -0
  31. package/lib/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.d.ts +1 -1
  32. package/lib/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.js +103 -24
  33. package/lib/src/brand-page-maker/utils/index.d.ts +0 -2
  34. package/lib/src/brand-page-maker/utils/index.js +0 -2
  35. package/lib/src/brand-page-maker/utils/rich-text-editor.util.d.ts +5 -28
  36. package/lib/src/brand-page-maker/utils/rich-text-editor.util.js +5 -48
  37. package/lib/src/brand-page-maker/validators/field.validator.js +2 -10
  38. package/lib/src/css/library.brand-page-brandCrowd.min.css +39 -39
  39. package/lib/src/css/library.brand-page-brandPage.min.css +39 -39
  40. package/lib/src/css/library.brand-page-designCom.min.css +39 -39
  41. package/lib/src/css/library.brand-page-preview.min.css +39 -39
  42. package/lib/src/index.d.ts +0 -1
  43. package/lib/src/index.mjs +138 -143
  44. package/lib/src/{lib-Db6j9fQZ.js → lib-6_P6kXPu.js} +51709 -55194
  45. package/lib/src/lib.js +0 -2
  46. package/lib/src/metadata-builder/ai-metadata-executor.d.ts +1 -0
  47. package/lib/src/metadata-builder/ai-metadata-executor.js +19 -0
  48. package/lib/src/metadata-builder/builders/section-builders/about-section-builder.js +3 -2
  49. package/lib/src/metadata-builder/builders/section-builders/banner-section-builder.js +3 -2
  50. package/lib/src/metadata-builder/builders/section-builders/booking-section-builder.js +3 -2
  51. package/lib/src/metadata-builder/builders/section-builders/contact-form-builder.js +4 -3
  52. package/lib/src/metadata-builder/builders/section-builders/images-section-builder.js +3 -2
  53. package/lib/src/metadata-builder/builders/section-builders/mailing-list-builder.js +2 -1
  54. package/lib/src/metadata-builder/builders/section-builders/mailing-list-section-builder.js +2 -1
  55. package/lib/src/metadata-builder/builders/section-builders/profile-section-builder.js +4 -3
  56. package/lib/src/metadata-builder/builders/section-builders/shapes-section-builder.js +3 -2
  57. package/lib/src/metadata-builder/builders/section-builders/testimonial-section-builder.js +2 -1
  58. package/lib/src/metadata-builder/builders/section-builders/text-section-builder.js +11 -15
  59. package/lib/src/metadata-builder/builders/update-content-text-builder.d.ts +17 -0
  60. package/lib/src/metadata-builder/builders/update-content-text-builder.js +103 -0
  61. package/lib/src/metadata-builder/builders/update-content-text-style-builder.d.ts +16 -0
  62. package/lib/src/metadata-builder/builders/update-content-text-style-builder.js +216 -0
  63. package/lib/src/metadata-builder/helpers/rte.helpers.d.ts +13 -11
  64. package/lib/src/metadata-builder/helpers/rte.helpers.js +73 -14
  65. package/lib/src/metadata-builder/metadata-executor.js +4 -0
  66. package/lib/src/metadata-builder/models.d.ts +2 -0
  67. package/lib/src/metadata-builder/models.js +2 -0
  68. package/lib/src/metadata-builder/parsers/ai-toolcall-parser.d.ts +19 -0
  69. package/lib/src/metadata-builder/parsers/ai-toolcall-parser.js +4 -0
  70. package/lib/src/metadata-builder/sanitizers/rte-schema-check.d.ts +8 -7
  71. package/lib/src/metadata-builder/sanitizers/rte-schema-check.js +52 -21
  72. package/lib/src/shared/content-settings/components/GenericTextInput.mixin.js +1 -14
  73. package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.js +3 -12
  74. package/lib/src/shared/content-settings/components/testimonial-items-field/TestimonialItemsField.mixin.d.ts +2 -3
  75. package/lib/src/shared/content-settings/components/testimonial-items-field/TestimonialItemsField.mixin.js +3 -10
  76. package/lib/src/shared/content-settings/components/text-items-field/TextItemsField.mixin.d.ts +1 -2
  77. package/lib/src/shared/content-settings/components/text-items-field/TextItemsField.mixin.js +1 -4
  78. package/lib/src/utils/minify-brand-page-metadata.js +1 -4
  79. package/package.json +15 -17
  80. package/src/brand-page/components/contact-form/ContactForm.mixin.ts +2 -5
  81. package/src/brand-page/components/text-wrapper/TextWrapper.vue +6 -17
  82. package/src/brand-page/models/index.ts +26 -35
  83. package/src/brand-page/utils/__snapshots__/migration.util.test.ts.snap +3 -3
  84. package/src/brand-page/utils/migration.util.ts +0 -12
  85. package/src/brand-page/utils/style.util.test.ts +0 -51
  86. package/src/brand-page/utils/text.util.ts +4 -43
  87. package/src/brand-page/utils/vcard.util.ts +4 -6
  88. package/src/brand-page/utils/view-model.util.ts +26 -94
  89. package/src/brand-page/view-models.ts +2 -3
  90. package/src/brand-page-maker/components/rich-text-editor/RichTextEditor.vue +3 -4
  91. package/src/brand-page-maker/composables/ai-chat/useAiMetadataBuilder.ts +1 -0
  92. package/src/brand-page-maker/composables/rich-text-editor/bubbleMenu.ts +10 -6
  93. package/src/brand-page-maker/composables/rich-text-editor/extensions/FontSize.ts +73 -0
  94. package/src/brand-page-maker/composables/rich-text-editor/extensions/LineHeight.ts +85 -0
  95. package/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.ts +111 -27
  96. package/src/brand-page-maker/utils/index.ts +0 -2
  97. package/src/brand-page-maker/utils/rich-text-editor.util.test.ts +3 -37
  98. package/src/brand-page-maker/utils/rich-text-editor.util.ts +11 -66
  99. package/src/brand-page-maker/validators/field.validator.ts +2 -10
  100. package/src/lib.ts +0 -9
  101. package/src/metadata-builder/ai-metadata-executor.ts +26 -0
  102. package/src/metadata-builder/builders/section-builders/about-section-builder.ts +3 -2
  103. package/src/metadata-builder/builders/section-builders/banner-section-builder.ts +3 -2
  104. package/src/metadata-builder/builders/section-builders/booking-section-builder.ts +5 -2
  105. package/src/metadata-builder/builders/section-builders/contact-form-builder.ts +4 -3
  106. package/src/metadata-builder/builders/section-builders/images-section-builder.ts +3 -2
  107. package/src/metadata-builder/builders/section-builders/mailing-list-builder.ts +2 -1
  108. package/src/metadata-builder/builders/section-builders/mailing-list-section-builder.ts +2 -1
  109. package/src/metadata-builder/builders/section-builders/profile-section-builder.ts +6 -3
  110. package/src/metadata-builder/builders/section-builders/shapes-section-builder.ts +3 -2
  111. package/src/metadata-builder/builders/section-builders/testimonial-section-builder.ts +2 -1
  112. package/src/metadata-builder/builders/section-builders/text-section-builder.ts +13 -19
  113. package/src/metadata-builder/builders/update-content-text-builder.ts +148 -0
  114. package/src/metadata-builder/builders/update-content-text-style-builder.ts +352 -0
  115. package/src/metadata-builder/helpers/rte.helpers.ts +101 -15
  116. package/src/metadata-builder/metadata-executor.ts +5 -0
  117. package/src/metadata-builder/models.ts +3 -0
  118. package/src/metadata-builder/parsers/ai-toolcall-parser.ts +27 -0
  119. package/src/metadata-builder/sanitizers/rte-schema-check.ts +53 -24
  120. package/src/shared/content-settings/components/GenericTextInput.mixin.ts +1 -17
  121. package/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.ts +3 -16
  122. package/src/shared/content-settings/components/testimonial-items-field/TestimonialItemsField.mixin.ts +4 -13
  123. package/src/shared/content-settings/components/testimonial-items-field/TestimonialItemsField.vue +4 -6
  124. package/src/shared/content-settings/components/text-image-field/TextImageField.vue +3 -6
  125. package/src/shared/content-settings/components/text-items-field/TextItemsField.mixin.ts +1 -6
  126. package/src/shared/content-settings/components/text-items-field/TextItemsField.vue +1 -1
  127. package/src/utils/minify-brand-page-metadata.ts +1 -4
  128. package/lib/src/brand-page/utils/migration/migration-20.d.ts +0 -2
  129. package/lib/src/brand-page/utils/migration/migration-20.js +0 -66
  130. package/lib/src/brand-page-maker/__fixtures__/complex-rte.fixture.d.ts +0 -22
  131. package/lib/src/brand-page-maker/__fixtures__/complex-rte.fixture.js +0 -22
  132. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.d.ts +0 -14
  133. package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.js +0 -39
  134. package/lib/src/brand-page-maker/composables/rich-text-editor/getRteExtensions.d.ts +0 -28
  135. package/lib/src/brand-page-maker/composables/rich-text-editor/getRteExtensions.js +0 -90
  136. package/lib/src/brand-page-maker/utils/rte-render.util.d.ts +0 -63
  137. package/lib/src/brand-page-maker/utils/rte-render.util.js +0 -95
  138. package/lib/src/brand-page-maker/utils/rte-sanitize.util.d.ts +0 -6
  139. package/lib/src/brand-page-maker/utils/rte-sanitize.util.js +0 -158
  140. package/src/brand-page/utils/migration/migration-20.server.test.ts +0 -125
  141. package/src/brand-page/utils/migration/migration-20.test.ts +0 -72
  142. package/src/brand-page/utils/migration/migration-20.ts +0 -87
  143. package/src/brand-page/utils/text.util.test.ts +0 -47
  144. package/src/brand-page/utils/view-model.pageid-rte.test.ts +0 -104
  145. package/src/brand-page-maker/__fixtures__/complex-rte.fixture.ts +0 -23
  146. package/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.test.ts +0 -52
  147. package/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.ts +0 -42
  148. package/src/brand-page-maker/composables/rich-text-editor/getRteExtensions.ts +0 -137
  149. package/src/brand-page-maker/utils/__snapshots__/rte-render-parity.test.ts.snap +0 -202
  150. package/src/brand-page-maker/utils/rte-features.test.ts +0 -351
  151. package/src/brand-page-maker/utils/rte-render-parity.test.ts +0 -157
  152. package/src/brand-page-maker/utils/rte-render.util.ts +0 -165
  153. package/src/brand-page-maker/utils/rte-sanitize-json.test.ts +0 -158
  154. package/src/brand-page-maker/utils/rte-sanitize.util.ts +0 -195
  155. package/src/brand-page-maker/validators/field.validator.test.ts +0 -55
@@ -119,7 +119,7 @@ import {
119
119
  getRouteNameByPageIndex,
120
120
  getRouteParamsByPage,
121
121
  } from './route.util';
122
- import { hasRTEContent, isTiptapDocument } from '../../brand-page-maker/utils';
122
+ import { hasRTEContent } from '../../brand-page-maker/utils';
123
123
  import {
124
124
  getPageIdFromHref,
125
125
  isHrefPageId,
@@ -1519,86 +1519,6 @@ const mapPageIdReferencesToRelativePaths = (
1519
1519
  });
1520
1520
  };
1521
1521
 
1522
- // Resolve a `pageid:<id>` internal-link href to the published relative path.
1523
- // Returns the resolved href, or null when `href` is not an internal pageid link
1524
- // (external/absolute links are left untouched). Falls back to the site root when
1525
- // the pageid can't be matched to a nav route.
1526
- const resolvePageIdHref = (
1527
- href: string,
1528
- nav: NavViewModel,
1529
- options: ViewModelMappingOptions,
1530
- ): string | null => {
1531
- // Only process internal links (no protocol, doesn't start with /)
1532
- if (!href || href.match(/^(https?:\/\/|\/)/) || !isHrefPageId(href)) {
1533
- return null;
1534
- }
1535
-
1536
- const pageId = getPageIdFromHref(href);
1537
- const route = findNavItemByPageId(nav.navItems, pageId);
1538
- const brandPageSlugRoute = options.brandPageSlug
1539
- ? `/${options.brandPageSlug}`
1540
- : '/';
1541
-
1542
- if (route) {
1543
- if (route.isRoot) {
1544
- return brandPageSlugRoute;
1545
- }
1546
-
1547
- return `${brandPageSlugRoute}${brandPageSlugRoute === '/' ? '' : '/'}${route.relativePath}`;
1548
- }
1549
-
1550
- // fallback to root if pageid cannot be resolved.
1551
- return brandPageSlugRoute;
1552
- };
1553
-
1554
- // BP-5847: walk a Tiptap JSON doc and rewrite every link mark whose href is a
1555
- // `pageid:` reference into its published relative path. Returns a NEW doc when
1556
- // something changed (so the shared model doc is never mutated), else the same
1557
- // reference.
1558
- export const resolvePageIdsInRteJson = (
1559
- doc: unknown,
1560
- nav: NavViewModel,
1561
- options: ViewModelMappingOptions,
1562
- ): unknown => {
1563
- let changed = false;
1564
-
1565
- const walk = (node: any): any => {
1566
- if (!node || typeof node !== 'object') {
1567
- return node;
1568
- }
1569
-
1570
- let next = node;
1571
-
1572
- if (Array.isArray(node.marks)) {
1573
- const marks = node.marks.map((mark: any) => {
1574
- if (mark?.type === 'link' && typeof mark.attrs?.href === 'string') {
1575
- const resolved = resolvePageIdHref(mark.attrs.href, nav, options);
1576
-
1577
- if (resolved !== null) {
1578
- changed = true;
1579
-
1580
- return { ...mark, attrs: { ...mark.attrs, href: resolved } };
1581
- }
1582
- }
1583
-
1584
- return mark;
1585
- });
1586
-
1587
- next = { ...next, marks };
1588
- }
1589
-
1590
- if (Array.isArray(node.content)) {
1591
- next = { ...next, content: node.content.map(walk) };
1592
- }
1593
-
1594
- return next;
1595
- };
1596
-
1597
- const result = walk(doc);
1598
-
1599
- return changed ? result : doc;
1600
- };
1601
-
1602
1522
  const dynamicallyMapPageIdInHyperlinksToRelativePaths = (
1603
1523
  content: any,
1604
1524
  nav: NavViewModel,
@@ -1611,15 +1531,7 @@ const dynamicallyMapPageIdInHyperlinksToRelativePaths = (
1611
1531
  return;
1612
1532
  }
1613
1533
 
1614
- if (isTiptapDocument(value)) {
1615
- // BP-5847: RTE fields may now be Tiptap JSON. Resolve pageid link marks
1616
- // here too, or internal links render as raw `pageid:` and don't navigate.
1617
- (content as unknown as Record<string, unknown>)[key] =
1618
- resolvePageIdsInRteJson(value, nav, options);
1619
- } else if (
1620
- typeof value === 'object' &&
1621
- (key === 'items' || key === 'textItems')
1622
- ) {
1534
+ if (typeof value === 'object' && (key === 'items' || key === 'textItems')) {
1623
1535
  if (Array.isArray(value)) {
1624
1536
  value.forEach((item) => {
1625
1537
  dynamicallyMapPageIdInHyperlinksToRelativePaths(item, nav, options);
@@ -1631,11 +1543,31 @@ const dynamicallyMapPageIdInHyperlinksToRelativePaths = (
1631
1543
  (content as unknown as Record<string, unknown>)[key] = value.replace(
1632
1544
  /<a\s+([^>]*?)href="([^"]*?)"([^>]*?)>/gi,
1633
1545
  (match, existingAttrsStart, href, existingAttrsEnd) => {
1634
- const resolved = resolvePageIdHref(href, nav, options);
1546
+ // Only process internal links (no protocol, doesn't start with /)
1547
+ if (href && !href.match(/^(https?:\/\/|\/)/) && isHrefPageId(href)) {
1548
+ const pageId = getPageIdFromHref(href);
1549
+
1550
+ const route = findNavItemByPageId(nav.navItems, pageId);
1551
+
1552
+ const brandPageSlugRoute = options.brandPageSlug
1553
+ ? `/${options.brandPageSlug}`
1554
+ : '/';
1555
+
1556
+ if (route) {
1557
+ let href = `${brandPageSlugRoute}`;
1558
+
1559
+ if (!route.isRoot) {
1560
+ href = `${brandPageSlugRoute}${brandPageSlugRoute === '/' ? '' : '/'}${route.relativePath}`;
1561
+ }
1562
+
1563
+ return `<a ${existingAttrsStart}href="${href}"${existingAttrsEnd}>`;
1564
+ }
1565
+
1566
+ // fallback to root if pageid cannot be resolved.
1567
+ return `<a ${existingAttrsStart}href="${brandPageSlugRoute}"${existingAttrsEnd}>`;
1568
+ }
1635
1569
 
1636
- return resolved === null
1637
- ? match
1638
- : `<a ${existingAttrsStart}href="${resolved}"${existingAttrsEnd}>`;
1570
+ return match;
1639
1571
  },
1640
1572
  );
1641
1573
  }
@@ -41,7 +41,6 @@ import type {
41
41
  BackgroundShape,
42
42
  PersonalisationRole,
43
43
  SaveContactOptions,
44
- RichTextValue,
45
44
  } from './models';
46
45
  import type { ImagesSectionContentDisplayStyle } from './constants';
47
46
 
@@ -197,8 +196,8 @@ export interface TextImageSectionViewModel {
197
196
  id: string;
198
197
  type: 'images-section';
199
198
  role: ContentType;
200
- title: RichTextValue;
201
- description: RichTextValue;
199
+ title: string;
200
+ description: string;
202
201
  displayStyle: ImagesSectionContentDisplayStyle;
203
202
  aspectRatio: AspectRatio;
204
203
  areImagesFullWidth: boolean;
@@ -384,7 +384,7 @@
384
384
  <div class="tw-relative" :class="{ 'editor-compact': isCompact }">
385
385
  <BubbleMenu
386
386
  :editor="editor"
387
- :options="bubbleMenuOptions"
387
+ :tippy-options="tippyOptions"
388
388
  :update-delay="0"
389
389
  :should-show="shouldShow"
390
390
  class="tw-w-fit"
@@ -453,8 +453,7 @@ import {
453
453
  ButtonGroup,
454
454
  TextInput,
455
455
  } from '@designcrowd/fe-shared-lib';
456
- import { EditorContent } from '@tiptap/vue-3';
457
- import { BubbleMenu } from '@tiptap/vue-3/menus';
456
+ import { EditorContent, BubbleMenu } from '@tiptap/vue-3';
458
457
  import { useRichTextEditor } from '../../composables/rich-text-editor/useRichTextEditor';
459
458
  import FontPicker from '@shared/font-family-selector/FontPicker.vue';
460
459
  import ColorField from '@shared/content-settings/components/color-field/ColorField.vue';
@@ -952,7 +951,7 @@ const {
952
951
  hyperlinkValue,
953
952
  setHyperlinkUrlValue,
954
953
  shouldShow,
955
- bubbleMenuOptions,
954
+ tippyOptions,
956
955
  handleAdminColorChange,
957
956
  } = useRichTextEditor(
958
957
  model,
@@ -33,6 +33,7 @@ export function useAiMetadataBuilder(logger: Logger) {
33
33
  model: BrandPageModel,
34
34
  aiAssistantToolCall: AiAssistantToolCall[],
35
35
  options: {
36
+ maxCharacters?: number;
36
37
  /**
37
38
  * Invoked by the UndoBuilder to resolve the pre-prompt snapshot.
38
39
  * Required when the response includes an `undo` tool call.
@@ -23,16 +23,20 @@ export const useBubbleMenu = (
23
23
  return false;
24
24
  };
25
25
 
26
- // v3 BubbleMenu uses Floating UI (was tippy.js in v2). Only placement + offset
27
- // carry over; the rest of the old tippy config (trigger/interactive/hideOnClick/
28
- // zIndex) is either the v3 default or handled in CSS.
29
- const bubbleMenuOptions = {
26
+ const tippyOptions = {
27
+ duration: 100,
30
28
  placement: 'bottom' as const,
31
- offset: 10,
29
+ interactive: true,
30
+ appendTo: 'parent' as const,
31
+ hideOnClick: false,
32
+ arrow: false,
33
+ offset: [0, 10] as [number, number],
34
+ trigger: 'manual' as const,
35
+ zIndex: 10,
32
36
  };
33
37
 
34
38
  return {
35
39
  shouldShow,
36
- bubbleMenuOptions,
40
+ tippyOptions,
37
41
  };
38
42
  };
@@ -0,0 +1,73 @@
1
+ import { Extension } from '@tiptap/core';
2
+ import '@tiptap/extension-text-style';
3
+
4
+ export type FontSizeOptions = {
5
+ types: string[];
6
+ };
7
+
8
+ declare module '@tiptap/core' {
9
+ interface Commands<ReturnType> {
10
+ fontSize: {
11
+ /**
12
+ * Set the font size
13
+ */
14
+ setFontSize: (fontSize: string) => ReturnType;
15
+ /**
16
+ * Unset the font size
17
+ */
18
+ unsetFontSize: () => ReturnType;
19
+ };
20
+ }
21
+ }
22
+
23
+ export const FontSize = Extension.create<FontSizeOptions>({
24
+ name: 'fontSize',
25
+
26
+ addOptions() {
27
+ return {
28
+ types: ['textStyle'],
29
+ };
30
+ },
31
+
32
+ addGlobalAttributes() {
33
+ return [
34
+ {
35
+ types: this.options.types,
36
+ attributes: {
37
+ fontSize: {
38
+ default: null,
39
+ parseHTML: (element) =>
40
+ element.style.fontSize.replace(/['"]+/g, ''),
41
+ renderHTML: (attributes) => {
42
+ if (!attributes.fontSize) {
43
+ return {};
44
+ }
45
+
46
+ return {
47
+ style: `font-size: ${attributes.fontSize}`,
48
+ };
49
+ },
50
+ },
51
+ },
52
+ },
53
+ ];
54
+ },
55
+
56
+ addCommands() {
57
+ return {
58
+ setFontSize:
59
+ (fontSize) =>
60
+ ({ chain }) => {
61
+ return chain().setMark('textStyle', { fontSize }).run();
62
+ },
63
+ unsetFontSize:
64
+ () =>
65
+ ({ chain }) => {
66
+ return chain()
67
+ .setMark('textStyle', { fontSize: null })
68
+ .removeEmptyTextStyle()
69
+ .run();
70
+ },
71
+ };
72
+ },
73
+ });
@@ -0,0 +1,85 @@
1
+ import { Extension } from '@tiptap/core';
2
+
3
+ export type LineHeightOptions = {
4
+ /**
5
+ * Node / mark types that should carry the `lineHeight` attribute.
6
+ * Defaults to `['textStyle']` to piggy‑back on TipTap’s built‑in mark.
7
+ */
8
+ types: string[];
9
+ };
10
+
11
+ declare module '@tiptap/core' {
12
+ interface Commands<ReturnType> {
13
+ lineHeight: {
14
+ /**
15
+ * Apply a line‑height to the current selection (or stored marks).
16
+ */
17
+ setLineHeight: (heightValue: string) => ReturnType;
18
+ /**
19
+ * Remove any line‑height attribute.
20
+ */
21
+ unsetLineHeight: () => ReturnType;
22
+ /**
23
+ * Read the current line‑height from the selection / stored marks.
24
+ */
25
+ getLineHeight: () => ReturnType;
26
+ };
27
+ }
28
+ }
29
+
30
+ export const LineHeight = Extension.create<LineHeightOptions>({
31
+ name: 'lineHeight',
32
+
33
+ addOptions() {
34
+ return {
35
+ types: ['textStyle'],
36
+ };
37
+ },
38
+
39
+ addGlobalAttributes() {
40
+ return [
41
+ {
42
+ types: this.options.types,
43
+ attributes: {
44
+ lineHeight: {
45
+ default: null,
46
+ parseHTML: (element) =>
47
+ (element.style.lineHeight || '').replace(/['"]+/g, ''),
48
+ renderHTML: (attributes) => {
49
+ if (!attributes.lineHeight) {
50
+ return {};
51
+ }
52
+
53
+ return { style: `line-height: ${attributes.lineHeight}` };
54
+ },
55
+ },
56
+ },
57
+ },
58
+ ];
59
+ },
60
+
61
+ addCommands() {
62
+ return {
63
+ setLineHeight:
64
+ (lineHeight) =>
65
+ ({ chain }) => {
66
+ return chain().setMark('textStyle', { lineHeight }).run();
67
+ },
68
+
69
+ unsetLineHeight:
70
+ () =>
71
+ ({ chain }) => {
72
+ return chain()
73
+ .setMark('textStyle', { lineHeight: null })
74
+ .removeEmptyTextStyle()
75
+ .run();
76
+ },
77
+
78
+ getLineHeight:
79
+ () =>
80
+ ({ editor }) => {
81
+ return editor.getAttributes('textStyle').lineHeight;
82
+ },
83
+ };
84
+ },
85
+ });
@@ -8,7 +8,18 @@ import {
8
8
  watch,
9
9
  } from 'vue';
10
10
  import { Editor } from '@tiptap/vue-3';
11
- import { getRteExtensions } from './getRteExtensions';
11
+ import StarterKit from '@tiptap/starter-kit';
12
+ import Underline from '@tiptap/extension-underline';
13
+ import TextStyle from '@tiptap/extension-text-style';
14
+ import TextAlign from '@tiptap/extension-text-align';
15
+ import FontFamily from '@tiptap/extension-font-family';
16
+ import ColorExtension from '@tiptap/extension-color';
17
+ import { FontSize } from './extensions/FontSize';
18
+ import { LetterSpacing } from './extensions/LetterSpacing';
19
+ import { LineHeight } from './extensions/LineHeight';
20
+ import Highlight from '@tiptap/extension-highlight';
21
+ import Placeholder from '@tiptap/extension-placeholder';
22
+ import Link from '@tiptap/extension-link';
12
23
  import type { StyleTextFontFamilyOption } from '@shared/style-settings/models';
13
24
  import { createFormattingActions } from './formatting';
14
25
  import { useTextAlign } from './textAlign';
@@ -20,7 +31,7 @@ import {
20
31
  emToPx,
21
32
  isStyleChanged,
22
33
  pxToEm,
23
- sanitizeRteJson,
34
+ sanitiseClientSideRTE,
24
35
  skipStyleDetection,
25
36
  } from '@brand-page-maker/utils';
26
37
  import type { HyperlinkType, TextStyles } from '@brand-page';
@@ -38,7 +49,9 @@ import {
38
49
  import { useUiContentSettingsStore } from '@shared/content-settings/store';
39
50
  import { useHistoryStore } from '@brand-page-maker/stores/history/history-store';
40
51
  import { AllSelection } from 'prosemirror-state';
41
- import { useHyperlink } from './hyperlink';
52
+ import { MaxLength } from './extensions/MaxLength';
53
+ import { AutoLink } from './extensions/AutoLink';
54
+ import { isHrefPageId, useHyperlink } from './hyperlink';
42
55
  import { useBubbleMenu } from './bubbleMenu';
43
56
  import {
44
57
  hasLogoColorVar,
@@ -79,7 +92,7 @@ interface UseRichTextEditorReturn {
79
92
  hyperlinkValue: ComputedRef<{ id: string; name: string; value: string }>;
80
93
  setHyperlinkUrlValue: (url: string) => void;
81
94
  shouldShow: ({ editor, state }: { editor: any; state: any }) => boolean;
82
- bubbleMenuOptions: Record<string, any>;
95
+ tippyOptions: Record<string, any>;
83
96
  handleAdminColorChange: (
84
97
  color: {
85
98
  id: string;
@@ -116,7 +129,44 @@ export const useExternalRichTextEditor = (): {
116
129
  } => {
117
130
  const externalEditor = new Editor({
118
131
  editable: false,
119
- extensions: getRteExtensions({ includeLinkRel: true }),
132
+ extensions: [
133
+ StarterKit,
134
+ Underline,
135
+ TextStyle.configure({ mergeNestedSpanStyles: true }),
136
+ TextAlign.configure({
137
+ types: ['heading', 'paragraph'],
138
+ }),
139
+ FontFamily.configure({
140
+ types: ['textStyle'],
141
+ }),
142
+ FontSize,
143
+ LetterSpacing,
144
+ LineHeight,
145
+ ColorExtension.configure({
146
+ types: ['textStyle'],
147
+ }),
148
+ Highlight.configure({
149
+ multicolor: true,
150
+ }),
151
+ Link.configure({
152
+ openOnClick: false,
153
+ defaultProtocol: 'https',
154
+ autolink: true,
155
+ HTMLAttributes: (attrs) => {
156
+ const href = attrs.href;
157
+ const isExternal =
158
+ href && (href.startsWith('http://') || href.startsWith('https://'));
159
+
160
+ return {
161
+ rel: 'noopener noreferrer',
162
+ target: isExternal ? '_blank' : null,
163
+ };
164
+ },
165
+ isAllowedUri: (url, ctx) =>
166
+ ctx.defaultValidate(url) || isHrefPageId(url),
167
+ }),
168
+ AutoLink,
169
+ ],
120
170
  });
121
171
 
122
172
  return {
@@ -164,13 +214,54 @@ export const useRichTextEditor = (
164
214
  const editor = new Editor({
165
215
  content: model.value ?? '',
166
216
  editable: !props.readOnly,
167
- extensions: getRteExtensions({
168
- restrictStarterKit: true,
169
- editing: {
217
+ extensions: [
218
+ StarterKit.configure({
219
+ bulletList: false,
220
+ orderedList: false,
221
+ listItem: false,
222
+ horizontalRule: false,
223
+ codeBlock: false,
224
+ code: false,
225
+ }),
226
+ Underline,
227
+ TextStyle.configure({ mergeNestedSpanStyles: true }),
228
+ TextAlign.configure({
229
+ types: ['heading', 'paragraph'],
230
+ }),
231
+ FontFamily.configure({
232
+ types: ['textStyle'],
233
+ }),
234
+ FontSize,
235
+ LetterSpacing,
236
+ LineHeight,
237
+ ColorExtension.configure({
238
+ types: ['textStyle'],
239
+ }),
240
+ Highlight.configure({
241
+ multicolor: true,
242
+ }),
243
+ Placeholder.configure({
170
244
  placeholder: props.placeholder,
171
- maxLength: props.maxLength,
172
- },
173
- }),
245
+ }),
246
+ MaxLength.configure({ maxLength: props.maxLength }),
247
+ Link.configure({
248
+ openOnClick: false,
249
+ defaultProtocol: 'https',
250
+ autolink: true,
251
+ HTMLAttributes: (attrs) => {
252
+ const href = attrs.href;
253
+ const isExternal =
254
+ href && (href.startsWith('http://') || href.startsWith('https://'));
255
+
256
+ return {
257
+ target: isExternal ? '_blank' : null,
258
+ };
259
+ },
260
+ isAllowedUri: (url, ctx) =>
261
+ ctx.defaultValidate(url) || isHrefPageId(url),
262
+ }),
263
+ AutoLink,
264
+ ],
174
265
  editorProps: {
175
266
  handleDOMEvents: {
176
267
  drop(view, event: DragEvent) {
@@ -349,7 +440,7 @@ export const useRichTextEditor = (
349
440
  applyHyperlinkToEditor,
350
441
  } = useHyperlink(editor);
351
442
 
352
- const { shouldShow, bubbleMenuOptions } = useBubbleMenu(
443
+ const { shouldShow, tippyOptions } = useBubbleMenu(
353
444
  editor,
354
445
  isHyperlinkPopupOpen,
355
446
  );
@@ -381,14 +472,8 @@ export const useRichTextEditor = (
381
472
 
382
473
  const isExternalMutation = updateRichTextEditor.value || appliedSinceLast;
383
474
 
384
- // Compare in the stored (JSON) format so a JSON model value isn't always
385
- // seen as "changed" against the editor's HTML (which would re-run
386
- // setContent and reset the user's just-applied styles).
387
- const hasChanged =
388
- JSON.stringify(editor.getJSON()) !== JSON.stringify(newValue);
389
-
390
- if (isExternalMutation && hasChanged) {
391
- editor.commands.setContent(newValue, { emitUpdate: false });
475
+ if (isExternalMutation && editor.getHTML() !== newValue) {
476
+ editor.commands.setContent(newValue, false);
392
477
  updateRichTextEditor.value = false;
393
478
  //text style type is not switching when reordering
394
479
  textStyleTypeOptions.forEach((item) => {
@@ -418,13 +503,12 @@ export const useRichTextEditor = (
418
503
  }
419
504
  });
420
505
 
421
- // Update model when editor content changes. RTE content is stored as Tiptap
422
- // JSON (BP-5787). getJSON() is already in hand (the editor holds the doc) and
423
- // is sanitised natively — no getHTML(), no DOMPurify, no extra editor — so
424
- // the per-keystroke path allocates nothing heavy.
506
+ // Update model when editor content changes
425
507
  editor.on('update', () => {
508
+ const sanitizedData = sanitiseClientSideRTE(editor.getHTML());
509
+
426
510
  // An update fired while the editor is unfocused is TipTap rewriting the
427
- // model back into its canonical form during mount rehydration or a
511
+ // model back into its canonical HTML during mount rehydration or a
428
512
  // programmatic setContent — not a user edit. Tag the emit so the write
429
513
  // folds into the current undo head instead of pushing a phantom entry
430
514
  // (BP-5810). The flag travels WITH the value through the debounced
@@ -438,7 +522,7 @@ export const useRichTextEditor = (
438
522
  // the RTE only mounts inside an already-open settings drawer and the user
439
523
  // focuses it before formatting, so this doesn't occur — revisit if a
440
524
  // consumer ever drives toolbar actions on an unfocused editor.
441
- emit('update:modelValue', sanitizeRteJson(editor.getJSON()), {
525
+ emit('update:modelValue', sanitizedData.sanitizedHtml, {
442
526
  isHydrationWrite: !editor.isFocused,
443
527
  });
444
528
  });
@@ -965,7 +1049,7 @@ export const useRichTextEditor = (
965
1049
  hyperlinkValue,
966
1050
  setHyperlinkUrlValue,
967
1051
  shouldShow,
968
- bubbleMenuOptions,
1052
+ tippyOptions,
969
1053
  handleAdminColorChange,
970
1054
  };
971
1055
  };
@@ -7,8 +7,6 @@ export * from './url.util';
7
7
  export * from './slug.util';
8
8
  export * from './text.util';
9
9
  export * from './rich-text-editor.util';
10
- export * from './rte-render.util';
11
- export * from './rte-sanitize.util';
12
10
 
13
11
  export const isColorMatched = (color: string | null | undefined): boolean => {
14
12
  if (!color) return false;
@@ -8,7 +8,6 @@ import {
8
8
  getUpdatedHTMLFromEditor,
9
9
  sanitiseClientSideRTE,
10
10
  getRTEColor,
11
- getRteTextLength,
12
11
  } from './rich-text-editor.util';
13
12
  import type { TextStyleDeclaration } from '@brand-page';
14
13
 
@@ -76,40 +75,8 @@ describe('hasHTML', () => {
76
75
  });
77
76
  });
78
77
 
79
- describe('getRteTextLength', () => {
80
- it('returns the raw length for a plain string (unchanged legacy behaviour)', () => {
81
- expect(getRteTextLength('hello')).toBe(5);
82
- expect(getRteTextLength('<p>hello</p>')).toBe('<p>hello</p>'.length);
83
- expect(getRteTextLength('')).toBe(0);
84
- });
85
-
86
- it('returns visible-text length for a Tiptap JSON doc (not undefined/throw)', () => {
87
- const doc = {
88
- type: 'doc',
89
- content: [
90
- {
91
- type: 'paragraph',
92
- content: [
93
- { type: 'text', text: 'hello ' },
94
- { type: 'text', marks: [{ type: 'bold' }], text: 'world' },
95
- ],
96
- },
97
- ],
98
- };
99
-
100
- // "hello world" — measured from the text nodes, not the JSON wrapper.
101
- expect(getRteTextLength(doc)).toBe(11);
102
- });
103
-
104
- it('returns 0 for null/undefined and an empty doc', () => {
105
- expect(getRteTextLength(null)).toBe(0);
106
- expect(getRteTextLength(undefined)).toBe(0);
107
- expect(getRteTextLength({ type: 'doc', content: [] })).toBe(0);
108
- });
109
- });
110
-
111
78
  describe('getUpdatedHTMLFromEditor', () => {
112
- it('applies style commands and returns updated JSON', () => {
79
+ it('applies style commands and returns updated HTML', () => {
113
80
  // Mock editor
114
81
  const runMock = vi.fn();
115
82
  const chain = {
@@ -152,7 +119,7 @@ describe('getUpdatedHTMLFromEditor', () => {
152
119
  },
153
120
  },
154
121
  chain: () => chain as any,
155
- getJSON: () => ({ type: 'doc', content: [{ type: 'paragraph' }] }),
122
+ getHTML: () => '<div>styled</div>',
156
123
  } as unknown as Editor;
157
124
 
158
125
  const newStyle = {
@@ -170,8 +137,7 @@ describe('getUpdatedHTMLFromEditor', () => {
170
137
 
171
138
  expect(externalEditor.commands.setContent).toHaveBeenCalledWith('foo');
172
139
  expect(runMock).toHaveBeenCalled();
173
- // BP-5787: returns the JSON form (consistent with JSON storage).
174
- expect(result).toEqual({ type: 'doc', content: [{ type: 'paragraph' }] });
140
+ expect(result).toBe('<div>styled</div>');
175
141
  });
176
142
  });
177
143