@designcrowd/library.brand-page 6.0.26-json-prereq-2 → 6.0.27-websitev2-dev-1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/src/{CreatedOnBrandCrowd-de-de-CKBqjDH3.js → CreatedOnBrandCrowd-de-de-BpFKOKIo.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-en-us-CRXiimvT.js → CreatedOnBrandCrowd-en-us-USGAnC_n.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-es-es-DCYdY6sj.js → CreatedOnBrandCrowd-es-es-CvVx3h4H.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-fr-fr-Dk7LOhxE.js → CreatedOnBrandCrowd-fr-ca-BCwamW5a.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-fr-ca-Dk7LOhxE.js → CreatedOnBrandCrowd-fr-fr-BCwamW5a.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-pt-pt-Cp4ufHJP.js → CreatedOnBrandCrowd-pt-br-Bt72hMrQ.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-pt-br-Cp4ufHJP.js → CreatedOnBrandCrowd-pt-pt-Bt72hMrQ.js} +1 -1
- package/lib/src/{CreatedOnDcom-de-de-ZSLkWMwM.js → CreatedOnDcom-de-de-BxUDrgG9.js} +1 -1
- package/lib/src/{CreatedOnDcom-en-us-BgdIGfT3.js → CreatedOnDcom-en-us-wjVp5eOs.js} +1 -1
- package/lib/src/{CreatedOnDcom-es-es-D0KemkCZ.js → CreatedOnDcom-es-es-CwFL2q5h.js} +1 -1
- package/lib/src/{CreatedOnDcom-fr-ca-BuwNjNbC.js → CreatedOnDcom-fr-ca-BPZ2_PNq.js} +1 -1
- package/lib/src/{CreatedOnDcom-fr-fr-DCQcsFPf.js → CreatedOnDcom-fr-fr-D0vLOikZ.js} +1 -1
- package/lib/src/{CreatedOnDcom-pt-br-DD-oLAS1.js → CreatedOnDcom-pt-br-CTwbxm9H.js} +1 -1
- package/lib/src/{CreatedOnDcom-pt-pt-D8qLq2wW.js → CreatedOnDcom-pt-pt-Cfqf8LaM.js} +1 -1
- package/lib/src/brand-page/components/contact-form/ContactForm.mixin.js +2 -5
- package/lib/src/brand-page/models/index.d.ts +26 -34
- package/lib/src/brand-page/utils/migration.util.js +0 -11
- package/lib/src/brand-page/utils/text.util.d.ts +1 -1
- package/lib/src/brand-page/utils/text.util.js +0 -32
- package/lib/src/brand-page/utils/vcard.util.js +4 -4
- package/lib/src/brand-page/utils/view-model.util.d.ts +0 -1
- package/lib/src/brand-page/utils/view-model.util.js +20 -68
- package/lib/src/brand-page/view-models.d.ts +3 -3
- package/lib/src/brand-page-maker/composables/ai-chat/useAiMetadataBuilder.d.ts +1 -0
- package/lib/src/brand-page-maker/composables/ai-chat/useAiWebsiteBuilderChat.js +7 -3
- package/lib/src/brand-page-maker/composables/rich-text-editor/bubbleMenu.d.ts +9 -2
- package/lib/src/brand-page-maker/composables/rich-text-editor/bubbleMenu.js +10 -6
- package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/FontSize.d.ts +20 -0
- package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/FontSize.js +44 -0
- package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/LineHeight.d.ts +27 -0
- package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/LineHeight.js +44 -0
- package/lib/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.d.ts +1 -1
- package/lib/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.js +103 -24
- package/lib/src/brand-page-maker/utils/index.d.ts +0 -2
- package/lib/src/brand-page-maker/utils/index.js +0 -2
- package/lib/src/brand-page-maker/utils/rich-text-editor.util.d.ts +5 -28
- package/lib/src/brand-page-maker/utils/rich-text-editor.util.js +5 -48
- package/lib/src/brand-page-maker/validators/field.validator.js +2 -10
- package/lib/src/clients/ai-assistant-client.d.ts +1 -0
- package/lib/src/clients/ai-assistant-client.js +1 -0
- package/lib/src/css/library.brand-page-brandCrowd.min.css +39 -39
- package/lib/src/css/library.brand-page-brandPage.min.css +39 -39
- package/lib/src/css/library.brand-page-designCom.min.css +39 -39
- package/lib/src/css/library.brand-page-preview.min.css +39 -39
- package/lib/src/index.d.ts +0 -1
- package/lib/src/index.mjs +138 -143
- package/lib/src/{lib-Db6j9fQZ.js → lib-Cg4BExY4.js} +72135 -75458
- package/lib/src/lib.js +0 -2
- package/lib/src/metadata-builder/ai-metadata-executor.d.ts +1 -0
- package/lib/src/metadata-builder/ai-metadata-executor.js +19 -0
- package/lib/src/metadata-builder/builders/global-font-propagation.d.ts +28 -0
- package/lib/src/metadata-builder/builders/global-font-propagation.js +125 -0
- package/lib/src/metadata-builder/builders/global-styles-builder.js +16 -0
- package/lib/src/metadata-builder/builders/section-builders/about-section-builder.js +3 -2
- package/lib/src/metadata-builder/builders/section-builders/banner-section-builder.js +3 -2
- package/lib/src/metadata-builder/builders/section-builders/booking-section-builder.js +3 -2
- package/lib/src/metadata-builder/builders/section-builders/contact-form-builder.js +4 -3
- package/lib/src/metadata-builder/builders/section-builders/images-section-builder.js +3 -2
- package/lib/src/metadata-builder/builders/section-builders/index.d.ts +1 -0
- package/lib/src/metadata-builder/builders/section-builders/index.js +1 -0
- package/lib/src/metadata-builder/builders/section-builders/mailing-list-builder.js +2 -1
- package/lib/src/metadata-builder/builders/section-builders/mailing-list-section-builder.js +2 -1
- package/lib/src/metadata-builder/builders/section-builders/profile-section-builder.js +4 -3
- package/lib/src/metadata-builder/builders/section-builders/shapes-section-builder.js +3 -2
- package/lib/src/metadata-builder/builders/section-builders/support-me-builder.d.ts +8 -0
- package/lib/src/metadata-builder/builders/section-builders/support-me-builder.js +25 -0
- package/lib/src/metadata-builder/builders/section-builders/testimonial-section-builder.js +2 -1
- package/lib/src/metadata-builder/builders/section-builders/text-section-builder.js +16 -17
- package/lib/src/metadata-builder/builders/section-field-maps.js +15 -0
- package/lib/src/metadata-builder/builders/section-field-utils.d.ts +6 -1
- package/lib/src/metadata-builder/builders/section-field-utils.js +48 -12
- package/lib/src/metadata-builder/builders/section-rules/support-me.rules.d.ts +2 -0
- package/lib/src/metadata-builder/builders/section-rules/support-me.rules.js +17 -0
- package/lib/src/metadata-builder/builders/update-content-text-builder.d.ts +17 -0
- package/lib/src/metadata-builder/builders/update-content-text-builder.js +103 -0
- package/lib/src/metadata-builder/builders/update-content-text-style-builder.d.ts +16 -0
- package/lib/src/metadata-builder/builders/update-content-text-style-builder.js +216 -0
- package/lib/src/metadata-builder/helpers/rte.helpers.d.ts +13 -11
- package/lib/src/metadata-builder/helpers/rte.helpers.js +73 -14
- package/lib/src/metadata-builder/metadata-executor.js +7 -1
- package/lib/src/metadata-builder/models.d.ts +4 -0
- package/lib/src/metadata-builder/models.js +4 -0
- package/lib/src/metadata-builder/parsers/ai-toolcall-parser.d.ts +20 -0
- package/lib/src/metadata-builder/parsers/ai-toolcall-parser.js +8 -0
- package/lib/src/metadata-builder/parsers/section-tool-arguments.d.ts +14 -2
- package/lib/src/metadata-builder/sanitizers/rte-schema-check.d.ts +8 -7
- package/lib/src/metadata-builder/sanitizers/rte-schema-check.js +52 -21
- package/lib/src/shared/content-settings/components/GenericTextInput.mixin.js +1 -14
- package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.js +3 -12
- package/lib/src/shared/content-settings/components/testimonial-items-field/TestimonialItemsField.mixin.d.ts +2 -3
- package/lib/src/shared/content-settings/components/testimonial-items-field/TestimonialItemsField.mixin.js +3 -10
- package/lib/src/shared/content-settings/components/text-items-field/TextItemsField.mixin.d.ts +1 -2
- package/lib/src/shared/content-settings/components/text-items-field/TextItemsField.mixin.js +1 -4
- package/lib/src/utils/minify-brand-page-metadata.js +8 -4
- package/package.json +15 -17
- package/src/brand-page/components/contact-form/ContactForm.mixin.ts +2 -5
- package/src/brand-page/components/text-wrapper/TextWrapper.vue +6 -17
- package/src/brand-page/models/index.ts +26 -35
- package/src/brand-page/utils/__snapshots__/migration.util.test.ts.snap +3 -3
- package/src/brand-page/utils/migration.util.ts +0 -12
- package/src/brand-page/utils/style.util.test.ts +0 -51
- package/src/brand-page/utils/text.util.ts +4 -43
- package/src/brand-page/utils/vcard.util.ts +4 -6
- package/src/brand-page/utils/view-model.util.ts +26 -94
- package/src/brand-page/view-models.ts +2 -3
- package/src/brand-page-maker/components/ai-website-builder/AiWebsiteBuilder.vue +1 -1
- package/src/brand-page-maker/components/rich-text-editor/RichTextEditor.vue +3 -4
- package/src/brand-page-maker/composables/ai-chat/useAiMetadataBuilder.ts +1 -0
- package/src/brand-page-maker/composables/ai-chat/useAiWebsiteBuilderChat.ts +7 -3
- package/src/brand-page-maker/composables/rich-text-editor/bubbleMenu.ts +10 -6
- package/src/brand-page-maker/composables/rich-text-editor/extensions/FontSize.ts +73 -0
- package/src/brand-page-maker/composables/rich-text-editor/extensions/LineHeight.ts +85 -0
- package/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.ts +111 -27
- package/src/brand-page-maker/utils/index.ts +0 -2
- package/src/brand-page-maker/utils/rich-text-editor.util.test.ts +3 -37
- package/src/brand-page-maker/utils/rich-text-editor.util.ts +11 -66
- package/src/brand-page-maker/validators/field.validator.ts +2 -10
- package/src/clients/ai-assistant-client.ts +1 -0
- package/src/lib.ts +0 -9
- package/src/metadata-builder/ai-metadata-executor.ts +26 -0
- package/src/metadata-builder/builders/global-font-propagation.test.ts +269 -0
- package/src/metadata-builder/builders/global-font-propagation.ts +205 -0
- package/src/metadata-builder/builders/global-styles-builder.ts +21 -0
- package/src/metadata-builder/builders/section-builders/about-section-builder.ts +3 -2
- package/src/metadata-builder/builders/section-builders/banner-section-builder.ts +3 -2
- package/src/metadata-builder/builders/section-builders/booking-section-builder.ts +5 -2
- package/src/metadata-builder/builders/section-builders/contact-form-builder.ts +4 -3
- package/src/metadata-builder/builders/section-builders/images-section-builder.ts +3 -2
- package/src/metadata-builder/builders/section-builders/index.ts +1 -0
- package/src/metadata-builder/builders/section-builders/mailing-list-builder.ts +2 -1
- package/src/metadata-builder/builders/section-builders/mailing-list-section-builder.ts +2 -1
- package/src/metadata-builder/builders/section-builders/profile-section-builder.ts +6 -3
- package/src/metadata-builder/builders/section-builders/shapes-section-builder.ts +3 -2
- package/src/metadata-builder/builders/section-builders/support-me-builder.test.ts +177 -0
- package/src/metadata-builder/builders/section-builders/support-me-builder.ts +43 -0
- package/src/metadata-builder/builders/section-builders/testimonial-section-builder.ts +2 -1
- package/src/metadata-builder/builders/section-builders/text-section-builder.ts +18 -21
- package/src/metadata-builder/builders/section-field-maps.ts +18 -0
- package/src/metadata-builder/builders/section-field-utils.test.ts +83 -0
- package/src/metadata-builder/builders/section-field-utils.ts +69 -18
- package/src/metadata-builder/builders/section-rules/support-me.rules.ts +39 -0
- package/src/metadata-builder/builders/update-content-text-builder.ts +148 -0
- package/src/metadata-builder/builders/update-content-text-style-builder.ts +352 -0
- package/src/metadata-builder/helpers/rte.helpers.ts +101 -15
- package/src/metadata-builder/metadata-executor.ts +8 -0
- package/src/metadata-builder/models.ts +5 -0
- package/src/metadata-builder/parsers/ai-toolcall-parser.ts +31 -0
- package/src/metadata-builder/parsers/section-tool-arguments.ts +14 -1
- package/src/metadata-builder/sanitizers/rte-schema-check.ts +53 -24
- package/src/shared/content-settings/components/GenericTextInput.mixin.ts +1 -17
- package/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.ts +3 -16
- package/src/shared/content-settings/components/testimonial-items-field/TestimonialItemsField.mixin.ts +4 -13
- package/src/shared/content-settings/components/testimonial-items-field/TestimonialItemsField.vue +4 -6
- package/src/shared/content-settings/components/text-image-field/TextImageField.vue +3 -6
- package/src/shared/content-settings/components/text-items-field/TextItemsField.mixin.ts +1 -6
- package/src/shared/content-settings/components/text-items-field/TextItemsField.vue +1 -1
- package/src/utils/minify-brand-page-metadata.test.ts +61 -0
- package/src/utils/minify-brand-page-metadata.ts +9 -4
- package/lib/src/brand-page/utils/migration/migration-20.d.ts +0 -2
- package/lib/src/brand-page/utils/migration/migration-20.js +0 -66
- package/lib/src/brand-page-maker/__fixtures__/complex-rte.fixture.d.ts +0 -22
- package/lib/src/brand-page-maker/__fixtures__/complex-rte.fixture.js +0 -22
- package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.d.ts +0 -14
- package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.js +0 -39
- package/lib/src/brand-page-maker/composables/rich-text-editor/getRteExtensions.d.ts +0 -28
- package/lib/src/brand-page-maker/composables/rich-text-editor/getRteExtensions.js +0 -90
- package/lib/src/brand-page-maker/utils/rte-render.util.d.ts +0 -63
- package/lib/src/brand-page-maker/utils/rte-render.util.js +0 -95
- package/lib/src/brand-page-maker/utils/rte-sanitize.util.d.ts +0 -6
- package/lib/src/brand-page-maker/utils/rte-sanitize.util.js +0 -158
- package/src/brand-page/utils/migration/migration-20.server.test.ts +0 -125
- package/src/brand-page/utils/migration/migration-20.test.ts +0 -72
- package/src/brand-page/utils/migration/migration-20.ts +0 -87
- package/src/brand-page/utils/text.util.test.ts +0 -47
- package/src/brand-page/utils/view-model.pageid-rte.test.ts +0 -104
- package/src/brand-page-maker/__fixtures__/complex-rte.fixture.ts +0 -23
- package/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.test.ts +0 -52
- package/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.ts +0 -42
- package/src/brand-page-maker/composables/rich-text-editor/getRteExtensions.ts +0 -137
- package/src/brand-page-maker/utils/__snapshots__/rte-render-parity.test.ts.snap +0 -202
- package/src/brand-page-maker/utils/rte-features.test.ts +0 -351
- package/src/brand-page-maker/utils/rte-render-parity.test.ts +0 -157
- package/src/brand-page-maker/utils/rte-render.util.ts +0 -165
- package/src/brand-page-maker/utils/rte-sanitize-json.test.ts +0 -158
- package/src/brand-page-maker/utils/rte-sanitize.util.ts +0 -195
- package/src/brand-page-maker/validators/field.validator.test.ts +0 -55
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
BrandPageModel,
|
|
4
4
|
TextStyleDeclaration,
|
|
5
5
|
} from '../../../brand-page/models';
|
|
6
|
+
import { tiptapJsonToHtml } from '../../helpers/rte.helpers';
|
|
6
7
|
import type { BuilderResult } from '../../models';
|
|
7
8
|
import {
|
|
8
9
|
isTiptapDocument,
|
|
@@ -47,7 +48,7 @@ export class MailingListSectionBuilder extends BaseSectionBuilder<MailingListCon
|
|
|
47
48
|
|
|
48
49
|
if (mailingListArgs.title !== undefined) {
|
|
49
50
|
if (isTiptapDocument(mailingListArgs.title)) {
|
|
50
|
-
resolved.content.title = mailingListArgs.title;
|
|
51
|
+
resolved.content.title = tiptapJsonToHtml(mailingListArgs.title);
|
|
51
52
|
resolved.content.titleStyle = resolveGlobalPresetIntoStyle(
|
|
52
53
|
resolved.content.titleStyle,
|
|
53
54
|
textStyleOptions,
|
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
BrandPageModel,
|
|
4
4
|
TextStyleDeclaration,
|
|
5
5
|
} from '../../../brand-page/models';
|
|
6
|
+
import { tiptapJsonToHtml } from '../../helpers/rte.helpers';
|
|
6
7
|
import type { BuilderResult } from '../../models';
|
|
7
8
|
import {
|
|
8
9
|
isTiptapDocument,
|
|
@@ -40,7 +41,7 @@ export class ProfileSectionBuilder extends BaseSectionBuilder<ProfileContent> {
|
|
|
40
41
|
|
|
41
42
|
if (profileArgs.name !== undefined) {
|
|
42
43
|
if (isTiptapDocument(profileArgs.name)) {
|
|
43
|
-
resolved.content.name = profileArgs.name;
|
|
44
|
+
resolved.content.name = tiptapJsonToHtml(profileArgs.name);
|
|
44
45
|
resolved.content.nameStyle = resolveGlobalPresetIntoStyle(
|
|
45
46
|
resolved.content.nameStyle,
|
|
46
47
|
textStyleOptions,
|
|
@@ -52,7 +53,7 @@ export class ProfileSectionBuilder extends BaseSectionBuilder<ProfileContent> {
|
|
|
52
53
|
|
|
53
54
|
if (profileArgs.title !== undefined) {
|
|
54
55
|
if (isTiptapDocument(profileArgs.title)) {
|
|
55
|
-
resolved.content.title = profileArgs.title;
|
|
56
|
+
resolved.content.title = tiptapJsonToHtml(profileArgs.title);
|
|
56
57
|
resolved.content.titleStyle = resolveGlobalPresetIntoStyle(
|
|
57
58
|
resolved.content.titleStyle,
|
|
58
59
|
textStyleOptions,
|
|
@@ -64,7 +65,9 @@ export class ProfileSectionBuilder extends BaseSectionBuilder<ProfileContent> {
|
|
|
64
65
|
|
|
65
66
|
if (profileArgs.description !== undefined) {
|
|
66
67
|
if (isTiptapDocument(profileArgs.description)) {
|
|
67
|
-
resolved.content.description =
|
|
68
|
+
resolved.content.description = tiptapJsonToHtml(
|
|
69
|
+
profileArgs.description,
|
|
70
|
+
);
|
|
68
71
|
resolved.content.descriptionStyle = resolveGlobalPresetIntoStyle(
|
|
69
72
|
resolved.content.descriptionStyle,
|
|
70
73
|
textStyleOptions,
|
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
TextShapeItem,
|
|
6
6
|
TextStyleDeclaration,
|
|
7
7
|
} from '../../../brand-page/models';
|
|
8
|
+
import { tiptapJsonToHtml } from '../../helpers/rte.helpers';
|
|
8
9
|
import type { BuilderResult } from '../../models';
|
|
9
10
|
import {
|
|
10
11
|
isTiptapDocument,
|
|
@@ -194,7 +195,7 @@ export class ShapesSectionBuilder extends BaseSectionBuilder<ShapesSectionConten
|
|
|
194
195
|
|
|
195
196
|
if (args.title !== undefined) {
|
|
196
197
|
if (isTiptapDocument(args.title)) {
|
|
197
|
-
content.title = args.title;
|
|
198
|
+
content.title = tiptapJsonToHtml(args.title);
|
|
198
199
|
content.titleStyle = resolveGlobalPresetIntoStyle(
|
|
199
200
|
content.titleStyle,
|
|
200
201
|
textStyleOptions,
|
|
@@ -206,7 +207,7 @@ export class ShapesSectionBuilder extends BaseSectionBuilder<ShapesSectionConten
|
|
|
206
207
|
|
|
207
208
|
if (args.description !== undefined) {
|
|
208
209
|
if (isTiptapDocument(args.description)) {
|
|
209
|
-
content.description = args.description;
|
|
210
|
+
content.description = tiptapJsonToHtml(args.description);
|
|
210
211
|
content.descriptionStyle = resolveGlobalPresetIntoStyle(
|
|
211
212
|
content.descriptionStyle,
|
|
212
213
|
textStyleOptions,
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import type {
|
|
3
|
+
BrandPageModel,
|
|
4
|
+
SupportMeContent,
|
|
5
|
+
} from '../../../brand-page/models';
|
|
6
|
+
import { BUILDER_TYPES } from '../../models';
|
|
7
|
+
import type { SupportMeArguments } from '../../parsers/section-tool-arguments';
|
|
8
|
+
import { SupportMeBuilder } from './support-me-builder';
|
|
9
|
+
|
|
10
|
+
const makeModel = (contents: SupportMeContent[] = []): BrandPageModel =>
|
|
11
|
+
({
|
|
12
|
+
brandPageType: 'brandSite',
|
|
13
|
+
styles: { textStyles: { textStyleOptions: {} } },
|
|
14
|
+
pages: [{ id: 'page-1', contents }],
|
|
15
|
+
}) as unknown as BrandPageModel;
|
|
16
|
+
|
|
17
|
+
const seedContent = (): SupportMeContent =>
|
|
18
|
+
({
|
|
19
|
+
id: 'sm-1',
|
|
20
|
+
type: 'support-me',
|
|
21
|
+
title: 'Existing title',
|
|
22
|
+
description: 'Existing description',
|
|
23
|
+
successMessage: 'Existing success',
|
|
24
|
+
displayStyle: 'preview',
|
|
25
|
+
supportMeConfig: {
|
|
26
|
+
paymentOptions: [{ amount: '0', id: 'po-1', name: 'Custom' }],
|
|
27
|
+
isCustomerMessageEnabled: true,
|
|
28
|
+
},
|
|
29
|
+
titleStyle: {},
|
|
30
|
+
headingStyle: {},
|
|
31
|
+
descriptionStyle: {},
|
|
32
|
+
buttonTextStyle: {},
|
|
33
|
+
itemTextStyle: {},
|
|
34
|
+
}) as unknown as SupportMeContent;
|
|
35
|
+
|
|
36
|
+
const create = (args: SupportMeArguments, model: BrandPageModel) =>
|
|
37
|
+
new SupportMeBuilder({
|
|
38
|
+
id: 't-create',
|
|
39
|
+
type: BUILDER_TYPES.CREATE_SUPPORT_ME,
|
|
40
|
+
payload: { sectionType: 'support-me', arguments: args, pageId: 'page-1' },
|
|
41
|
+
}).buildAsync(model);
|
|
42
|
+
|
|
43
|
+
const update = (
|
|
44
|
+
args: SupportMeArguments,
|
|
45
|
+
model: BrandPageModel,
|
|
46
|
+
contentId = 'sm-1',
|
|
47
|
+
) =>
|
|
48
|
+
new SupportMeBuilder({
|
|
49
|
+
id: 't-update',
|
|
50
|
+
type: BUILDER_TYPES.UPDATE_SUPPORT_ME,
|
|
51
|
+
payload: { sectionType: 'support-me', arguments: args, contentId },
|
|
52
|
+
}).buildAsync(model);
|
|
53
|
+
|
|
54
|
+
describe('SupportMeBuilder — create', () => {
|
|
55
|
+
it('creates a support-me section with factory defaults', async () => {
|
|
56
|
+
const model = makeModel();
|
|
57
|
+
|
|
58
|
+
const { model: out, errors } = await create({}, model);
|
|
59
|
+
|
|
60
|
+
expect(errors ?? []).toEqual([]);
|
|
61
|
+
|
|
62
|
+
const created = out.pages[0].contents[0] as SupportMeContent;
|
|
63
|
+
|
|
64
|
+
expect(created.type).toBe('support-me');
|
|
65
|
+
// display style default comes from the factory
|
|
66
|
+
expect(created.displayStyle).toBe('preview');
|
|
67
|
+
// supportMeConfig (payment options) comes free from the factory, never the AI
|
|
68
|
+
expect(created.supportMeConfig.paymentOptions.length).toBeGreaterThan(0);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('applies AI-provided scalar fields over the defaults', async () => {
|
|
72
|
+
const model = makeModel();
|
|
73
|
+
|
|
74
|
+
const { model: out, errors } = await create(
|
|
75
|
+
{
|
|
76
|
+
title: 'Buy me a coffee',
|
|
77
|
+
description: 'Support my work',
|
|
78
|
+
success_message: 'Thanks!',
|
|
79
|
+
display_style: 'collapsed',
|
|
80
|
+
},
|
|
81
|
+
model,
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
expect(errors ?? []).toEqual([]);
|
|
85
|
+
|
|
86
|
+
const created = out.pages[0].contents[0] as SupportMeContent;
|
|
87
|
+
|
|
88
|
+
expect(created.title).toBe('Buy me a coffee');
|
|
89
|
+
expect(created.description).toBe('Support my work');
|
|
90
|
+
expect(created.successMessage).toBe('Thanks!');
|
|
91
|
+
expect(created.displayStyle).toBe('collapsed');
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('SupportMeBuilder — update', () => {
|
|
96
|
+
it('updates scalar fields on an existing section', async () => {
|
|
97
|
+
const model = makeModel([seedContent()]);
|
|
98
|
+
|
|
99
|
+
const { model: out, errors } = await update(
|
|
100
|
+
{
|
|
101
|
+
title: 'New title',
|
|
102
|
+
success_message: 'New success',
|
|
103
|
+
display_style: 'collapsed',
|
|
104
|
+
},
|
|
105
|
+
model,
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
expect(errors ?? []).toEqual([]);
|
|
109
|
+
|
|
110
|
+
const updated = out.pages[0].contents[0] as SupportMeContent;
|
|
111
|
+
|
|
112
|
+
expect(updated.title).toBe('New title');
|
|
113
|
+
expect(updated.successMessage).toBe('New success');
|
|
114
|
+
expect(updated.displayStyle).toBe('collapsed');
|
|
115
|
+
// untouched fields survive
|
|
116
|
+
expect(updated.description).toBe('Existing description');
|
|
117
|
+
// supportMeConfig is never AI-editable
|
|
118
|
+
expect(updated.supportMeConfig.paymentOptions.length).toBe(1);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('applies the five style fields to their model keys', async () => {
|
|
122
|
+
const model = makeModel([seedContent()]);
|
|
123
|
+
|
|
124
|
+
const { model: out, errors } = await update(
|
|
125
|
+
{
|
|
126
|
+
title_style: { textAlign: 'left' },
|
|
127
|
+
heading_style: { textAlign: 'right' },
|
|
128
|
+
description_style: { textAlign: 'center' },
|
|
129
|
+
item_text_style: { textAlign: 'justify' },
|
|
130
|
+
button_text_style: { color: '#123456' },
|
|
131
|
+
},
|
|
132
|
+
model,
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
expect(errors ?? []).toEqual([]);
|
|
136
|
+
|
|
137
|
+
const updated = out.pages[0].contents[0] as SupportMeContent;
|
|
138
|
+
|
|
139
|
+
expect(updated.titleStyle).toMatchObject({ textAlign: 'left' });
|
|
140
|
+
expect(updated.headingStyle).toMatchObject({ textAlign: 'right' });
|
|
141
|
+
expect(updated.descriptionStyle).toMatchObject({ textAlign: 'center' });
|
|
142
|
+
expect(updated.itemTextStyle).toMatchObject({ textAlign: 'justify' });
|
|
143
|
+
expect(updated.buttonTextStyle).toMatchObject({ color: '#123456' });
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe('SupportMeBuilder — validation', () => {
|
|
148
|
+
it('rejects an invalid display_style enum value', async () => {
|
|
149
|
+
const model = makeModel([seedContent()]);
|
|
150
|
+
|
|
151
|
+
const { errors } = await update({ display_style: 'fancy' as never }, model);
|
|
152
|
+
|
|
153
|
+
expect(errors?.length).toBeGreaterThan(0);
|
|
154
|
+
expect(errors!.join(' ')).toContain('display_style');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('rejects an over-length title', async () => {
|
|
158
|
+
const model = makeModel([seedContent()]);
|
|
159
|
+
|
|
160
|
+
const { errors } = await update({ title: 'x'.repeat(201) }, model);
|
|
161
|
+
|
|
162
|
+
expect(errors?.length).toBeGreaterThan(0);
|
|
163
|
+
expect(errors!.join(' ')).toContain('title');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('rejects an over-length success_message', async () => {
|
|
167
|
+
const model = makeModel([seedContent()]);
|
|
168
|
+
|
|
169
|
+
const { errors } = await update(
|
|
170
|
+
{ success_message: 'x'.repeat(1001) },
|
|
171
|
+
model,
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
expect(errors?.length).toBeGreaterThan(0);
|
|
175
|
+
expect(errors!.join(' ')).toContain('success_message');
|
|
176
|
+
});
|
|
177
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BrandPageModel,
|
|
3
|
+
SupportMeContent,
|
|
4
|
+
} from '../../../brand-page/models';
|
|
5
|
+
import type { BuilderResult } from '../../models';
|
|
6
|
+
import { applyScalarFields, applyStyleFields } from '../section-field-utils';
|
|
7
|
+
import { BaseSectionBuilder } from './base-section-builder';
|
|
8
|
+
import { validateSupportMe } from '../section-rules/support-me.rules';
|
|
9
|
+
|
|
10
|
+
export class SupportMeBuilder extends BaseSectionBuilder<SupportMeContent> {
|
|
11
|
+
protected get sectionValidate() {
|
|
12
|
+
return validateSupportMe;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async buildAsync(model: BrandPageModel): Promise<BuilderResult> {
|
|
16
|
+
this.log('Build called');
|
|
17
|
+
|
|
18
|
+
const validation = await this.validateAsync();
|
|
19
|
+
|
|
20
|
+
if (validation.errors.length) {
|
|
21
|
+
return { request: this.request, model, errors: validation.errors };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const resolved = this.resolveAndRepositionContent(model);
|
|
25
|
+
|
|
26
|
+
if ('error' in resolved) return resolved.error;
|
|
27
|
+
|
|
28
|
+
const content = resolved.content;
|
|
29
|
+
|
|
30
|
+
// title/description/success_message are plain-text fields (no RTE support),
|
|
31
|
+
// so scalar application covers all content fields. supportMeConfig (payment
|
|
32
|
+
// options) is intentionally not AI-editable — it comes from the factory.
|
|
33
|
+
applyScalarFields(content, this.args, this.sectionType);
|
|
34
|
+
applyStyleFields(
|
|
35
|
+
content,
|
|
36
|
+
this.args,
|
|
37
|
+
this.sectionType,
|
|
38
|
+
this.getGlobalTextStyles(model),
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
return { request: this.request, model };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
BrandPageModel,
|
|
5
5
|
TextStyleDeclaration,
|
|
6
6
|
} from '../../../brand-page/models';
|
|
7
|
+
import { tiptapJsonToHtml } from '../../helpers/rte.helpers';
|
|
7
8
|
import {
|
|
8
9
|
isTiptapDocument,
|
|
9
10
|
type TestimonialItem,
|
|
@@ -172,7 +173,7 @@ export class TestimonialSectionBuilder extends BaseSectionBuilder<TestimonialSec
|
|
|
172
173
|
|
|
173
174
|
if (args.title !== undefined) {
|
|
174
175
|
if (isTiptapDocument(args.title)) {
|
|
175
|
-
content.title = args.title;
|
|
176
|
+
content.title = tiptapJsonToHtml(args.title);
|
|
176
177
|
content.titleStyle = resolveGlobalPresetIntoStyle(
|
|
177
178
|
content.titleStyle,
|
|
178
179
|
textStyleOptions,
|
|
@@ -4,9 +4,9 @@ import type {
|
|
|
4
4
|
TextItem,
|
|
5
5
|
TextStyleDeclaration,
|
|
6
6
|
BrandPageModel,
|
|
7
|
-
RichTextValue,
|
|
8
7
|
} from '../../../brand-page/models';
|
|
9
8
|
import type { TextStyleOptions } from '../../../brand-page/models/styles.model';
|
|
9
|
+
import { tiptapJsonToHtml } from '../../helpers/rte.helpers';
|
|
10
10
|
import {
|
|
11
11
|
isTiptapDocument,
|
|
12
12
|
type TextSectionArguments,
|
|
@@ -26,18 +26,12 @@ import {
|
|
|
26
26
|
repositionItemByAnchor,
|
|
27
27
|
} from '../item-id-utils';
|
|
28
28
|
|
|
29
|
-
/**
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
* text-style preset resolution for rich content.
|
|
34
|
-
*/
|
|
35
|
-
const resolveText = (
|
|
36
|
-
value: unknown,
|
|
37
|
-
): { value: RichTextValue; wasTiptap: boolean } => {
|
|
38
|
-
if (isTiptapDocument(value)) return { value, wasTiptap: true };
|
|
29
|
+
/** Resolve a RichTextField (string or TiptapDocument) to a plain HTML string. */
|
|
30
|
+
const resolveText = (value: unknown): { html: string; wasTiptap: boolean } => {
|
|
31
|
+
if (isTiptapDocument(value))
|
|
32
|
+
return { html: tiptapJsonToHtml(value), wasTiptap: true };
|
|
39
33
|
|
|
40
|
-
return {
|
|
34
|
+
return { html: typeof value === 'string' ? value : '', wasTiptap: false };
|
|
41
35
|
};
|
|
42
36
|
|
|
43
37
|
const clearTextStyleTypeIfTiptap = (
|
|
@@ -61,11 +55,11 @@ const createTextItem = (
|
|
|
61
55
|
item.text_style ? { ...baseStyle, ...item.text_style } : { ...baseStyle }
|
|
62
56
|
) as TextStyleDeclaration;
|
|
63
57
|
|
|
64
|
-
const {
|
|
58
|
+
const { html, wasTiptap } = resolveText(item.text);
|
|
65
59
|
|
|
66
60
|
return {
|
|
67
61
|
id: item.item_id || generateItemId(),
|
|
68
|
-
text:
|
|
62
|
+
text: html,
|
|
69
63
|
textStyle: clearTextStyleTypeIfTiptap(
|
|
70
64
|
textStyle,
|
|
71
65
|
wasTiptap,
|
|
@@ -117,9 +111,9 @@ const mergeTextItems = (
|
|
|
117
111
|
if (!existing) continue;
|
|
118
112
|
|
|
119
113
|
if (item.text !== undefined) {
|
|
120
|
-
const {
|
|
114
|
+
const { html, wasTiptap } = resolveText(item.text);
|
|
121
115
|
|
|
122
|
-
existing.text =
|
|
116
|
+
existing.text = html;
|
|
123
117
|
existing.textStyle = clearTextStyleTypeIfTiptap(
|
|
124
118
|
existing.textStyle || {},
|
|
125
119
|
wasTiptap,
|
|
@@ -166,12 +160,12 @@ const applySingleTextField = (
|
|
|
166
160
|
textStyleOptions: TextStyleOptions | undefined,
|
|
167
161
|
): void => {
|
|
168
162
|
if (args.text !== undefined) {
|
|
169
|
-
const {
|
|
163
|
+
const { html, wasTiptap } = resolveText(args.text);
|
|
170
164
|
|
|
171
165
|
if (Array.isArray(content.textItems) && content.textItems.length > 0) {
|
|
172
166
|
content.textItems[0] = {
|
|
173
167
|
...content.textItems[0],
|
|
174
|
-
text:
|
|
168
|
+
text: html,
|
|
175
169
|
textStyle: clearTextStyleTypeIfTiptap(
|
|
176
170
|
content.textItems[0].textStyle || {},
|
|
177
171
|
wasTiptap,
|
|
@@ -182,7 +176,7 @@ const applySingleTextField = (
|
|
|
182
176
|
content.textItems = [
|
|
183
177
|
{
|
|
184
178
|
id: generateItemId(),
|
|
185
|
-
text:
|
|
179
|
+
text: html,
|
|
186
180
|
textStyle: clearTextStyleTypeIfTiptap(
|
|
187
181
|
{ ...(content.textStyle || {}) },
|
|
188
182
|
wasTiptap,
|
|
@@ -246,13 +240,16 @@ export class TextSectionBuilder extends BaseSectionBuilder<TextContent> {
|
|
|
246
240
|
// Full replacement: discard existing items and rebuild from the payload.
|
|
247
241
|
applyTextItems(content, args.items, textStyleOptions);
|
|
248
242
|
} else if (this.mode === 'create') {
|
|
249
|
-
|
|
243
|
+
// An EMPTY items array must not shadow the single text field — the AI emits
|
|
244
|
+
// `items: []` alongside `text` and the section rendered blank (QA 2026-07-05,
|
|
245
|
+
// T06: items=[] set textItems=[] and `text` was silently discarded).
|
|
246
|
+
if (args.items && Array.isArray(args.items) && args.items.length > 0) {
|
|
250
247
|
applyTextItems(content, args.items, textStyleOptions);
|
|
251
248
|
} else {
|
|
252
249
|
applySingleTextField(content, args, textStyleOptions);
|
|
253
250
|
}
|
|
254
251
|
} else if (
|
|
255
|
-
(args.items && Array.isArray(args.items)) ||
|
|
252
|
+
(args.items && Array.isArray(args.items) && args.items.length > 0) ||
|
|
256
253
|
args._remove_item_ids?.length
|
|
257
254
|
) {
|
|
258
255
|
// Update: id-based add / edit / move / remove in a single pass.
|
|
@@ -23,6 +23,7 @@ import type {
|
|
|
23
23
|
ImageContent,
|
|
24
24
|
ButtonContent,
|
|
25
25
|
ImageGalleryContent,
|
|
26
|
+
SupportMeContent,
|
|
26
27
|
} from '../../brand-page/models';
|
|
27
28
|
|
|
28
29
|
export type SectionFieldMap = Record<string, string>;
|
|
@@ -175,9 +176,17 @@ const socialButtonFields = defineFieldMap<ButtonContent>({
|
|
|
175
176
|
url: 'url',
|
|
176
177
|
});
|
|
177
178
|
|
|
179
|
+
const supportMeFields = defineFieldMap<SupportMeContent>({
|
|
180
|
+
title: 'title',
|
|
181
|
+
description: 'description',
|
|
182
|
+
success_message: 'successMessage',
|
|
183
|
+
display_style: 'displayStyle',
|
|
184
|
+
});
|
|
185
|
+
|
|
178
186
|
export const SECTION_FIELD_MAPS: Record<string, SectionFieldMap> = {
|
|
179
187
|
'banner-section': bannerFields,
|
|
180
188
|
'booking-section': bookingFields,
|
|
189
|
+
'support-me': supportMeFields,
|
|
181
190
|
'about-section': aboutFields,
|
|
182
191
|
'testimonial-section': testimonialFields,
|
|
183
192
|
'images-section': imagesSectionFields,
|
|
@@ -265,6 +274,14 @@ const mailingListStyleFields = defineFieldMap<MailingListContent>({
|
|
|
265
274
|
button_text_style: 'buttonTextStyle',
|
|
266
275
|
});
|
|
267
276
|
|
|
277
|
+
const supportMeStyleFields = defineFieldMap<SupportMeContent>({
|
|
278
|
+
title_style: 'titleStyle',
|
|
279
|
+
heading_style: 'headingStyle',
|
|
280
|
+
description_style: 'descriptionStyle',
|
|
281
|
+
item_text_style: 'itemTextStyle',
|
|
282
|
+
button_text_style: 'buttonTextStyle',
|
|
283
|
+
});
|
|
284
|
+
|
|
268
285
|
const socialLinksStyleFields = defineFieldMap<SocialLinksContent>({
|
|
269
286
|
icon_fill_style: 'iconFillStyles',
|
|
270
287
|
icon_background_style: 'iconBackgroundStyles',
|
|
@@ -273,6 +290,7 @@ const socialLinksStyleFields = defineFieldMap<SocialLinksContent>({
|
|
|
273
290
|
export const STYLE_FIELD_MAPS: Record<string, SectionFieldMap> = {
|
|
274
291
|
'banner-section': bannerStyleFields,
|
|
275
292
|
'booking-section': bookingStyleFields,
|
|
293
|
+
'support-me': supportMeStyleFields,
|
|
276
294
|
'about-section': aboutStyleFields,
|
|
277
295
|
text: textStyleFields,
|
|
278
296
|
'testimonial-section': testimonialStyleFields,
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import type { Content } from '../../brand-page/models';
|
|
3
|
+
import { applyStyleFields } from './section-field-utils';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Regression tests for the AI QA 2026-07-05 rerun P0: a colour-only *_style
|
|
7
|
+
* patch left the field's existing inline textStyle marks (old colour) in
|
|
8
|
+
* place — inline marks override the style layer at render, so recoloured
|
|
9
|
+
* bands showed invisible old-colour text. applyStyleFields now propagates a
|
|
10
|
+
* new style colour into the stored RTE HTML when the patch carries no new
|
|
11
|
+
* text for that field.
|
|
12
|
+
*/
|
|
13
|
+
describe('applyStyleFields inline-colour propagation', () => {
|
|
14
|
+
const makeContent = (): Content =>
|
|
15
|
+
({
|
|
16
|
+
type: 'about-section',
|
|
17
|
+
title: '<h3><span style="color: #111111">Fine Grooming</span></h3>',
|
|
18
|
+
titleStyle: { color: '#111111', fontSize: '2.188em' },
|
|
19
|
+
paragraph: '<p><span style="color: #111111">Body copy</span></p>',
|
|
20
|
+
paragraphStyle: { color: '#111111' },
|
|
21
|
+
}) as unknown as Content;
|
|
22
|
+
|
|
23
|
+
const recolour = (html: string, color: string): string =>
|
|
24
|
+
html.replace(/color: #[0-9a-fA-F]{6}/g, `color: ${color}`);
|
|
25
|
+
|
|
26
|
+
it('colour-only patch recolours the existing inline marks', () => {
|
|
27
|
+
const content = makeContent();
|
|
28
|
+
|
|
29
|
+
applyStyleFields(
|
|
30
|
+
content,
|
|
31
|
+
{ title_style: { color: '#f8f2e9' } },
|
|
32
|
+
'about-section',
|
|
33
|
+
undefined,
|
|
34
|
+
recolour,
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const record = content as unknown as Record<string, unknown>;
|
|
38
|
+
|
|
39
|
+
expect(record.title).toContain('color: #f8f2e9');
|
|
40
|
+
expect((record.titleStyle as Record<string, unknown>).color).toBe(
|
|
41
|
+
'#f8f2e9',
|
|
42
|
+
);
|
|
43
|
+
// Untouched fields keep their marks.
|
|
44
|
+
expect(record.paragraph).toContain('color: #111111');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('does NOT rewrite marks when the patch also carries new text for the field', () => {
|
|
48
|
+
const content = makeContent();
|
|
49
|
+
|
|
50
|
+
applyStyleFields(
|
|
51
|
+
content,
|
|
52
|
+
{
|
|
53
|
+
title: { type: 'doc', content: [] },
|
|
54
|
+
title_style: { color: '#f8f2e9' },
|
|
55
|
+
},
|
|
56
|
+
'about-section',
|
|
57
|
+
undefined,
|
|
58
|
+
recolour,
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
// The builder's own text path owns the rewrite — HTML left alone here.
|
|
62
|
+
expect((content as unknown as Record<string, unknown>).title).toContain(
|
|
63
|
+
'color: #111111',
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('merged style keeps non-colour properties from the current style', () => {
|
|
68
|
+
const content = makeContent();
|
|
69
|
+
|
|
70
|
+
applyStyleFields(
|
|
71
|
+
content,
|
|
72
|
+
{ title_style: { color: '#f8f2e9' } },
|
|
73
|
+
'about-section',
|
|
74
|
+
undefined,
|
|
75
|
+
recolour,
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const style = (content as unknown as Record<string, unknown>)
|
|
79
|
+
.titleStyle as Record<string, unknown>;
|
|
80
|
+
|
|
81
|
+
expect(style.fontSize).toBe('2.188em');
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -9,6 +9,10 @@ import {
|
|
|
9
9
|
isTiptapDocument,
|
|
10
10
|
type SectionToolArguments,
|
|
11
11
|
} from '../parsers/section-tool-arguments';
|
|
12
|
+
import { hasRTEContent } from '../../brand-page-maker/utils';
|
|
13
|
+
import { useExternalRichTextEditor } from '../../brand-page-maker/composables/rich-text-editor/useRichTextEditor';
|
|
14
|
+
import { applyStyleToRTEText } from '../helpers/rte.helpers';
|
|
15
|
+
import type { TextStyleDeclaration } from '../../brand-page/models';
|
|
12
16
|
import { getSectionFieldMap, getStyleFieldMap } from './section-field-maps';
|
|
13
17
|
|
|
14
18
|
/**
|
|
@@ -186,36 +190,83 @@ export const applyDetachedStyleOverride = <T extends object>(
|
|
|
186
190
|
...updates,
|
|
187
191
|
}) as T;
|
|
188
192
|
|
|
193
|
+
/**
|
|
194
|
+
* Rewrites the stored RTE HTML's inline colour marks. Injectable for tests;
|
|
195
|
+
* the default uses the external Tiptap editor (font-propagation precedent).
|
|
196
|
+
*/
|
|
197
|
+
export type ApplyColorToRTE = (html: string, color: string) => string;
|
|
198
|
+
|
|
189
199
|
export const applyStyleFields = (
|
|
190
200
|
content: Content,
|
|
191
201
|
args: SectionToolArguments,
|
|
192
202
|
sectionType: string,
|
|
193
203
|
textStyleOptions?: TextStyleOptions,
|
|
204
|
+
applyColor?: ApplyColorToRTE,
|
|
194
205
|
): void => {
|
|
195
206
|
const styleMap = getStyleFieldMap(sectionType);
|
|
196
207
|
|
|
197
208
|
if (!styleMap) return;
|
|
198
209
|
|
|
199
210
|
const record = content as IndexableContent;
|
|
211
|
+
let editor:
|
|
212
|
+
| ReturnType<typeof useExternalRichTextEditor>['externalEditor']
|
|
213
|
+
| undefined;
|
|
214
|
+
const applyColorToHtml: ApplyColorToRTE =
|
|
215
|
+
applyColor ??
|
|
216
|
+
((html, color) => {
|
|
217
|
+
editor = editor ?? useExternalRichTextEditor().externalEditor;
|
|
218
|
+
|
|
219
|
+
return applyStyleToRTEText(editor, html, {
|
|
220
|
+
color,
|
|
221
|
+
} as TextStyleDeclaration);
|
|
222
|
+
});
|
|
200
223
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
224
|
+
try {
|
|
225
|
+
for (const [aiKey, modelKey] of Object.entries(styleMap)) {
|
|
226
|
+
if (!(aiKey in args) || !args[aiKey] || typeof args[aiKey] !== 'object')
|
|
227
|
+
continue;
|
|
228
|
+
|
|
229
|
+
const aiStyle = args[aiKey] as Record<string, unknown>;
|
|
230
|
+
const filtered: Record<string, unknown> = {};
|
|
231
|
+
|
|
232
|
+
for (const [k, v] of Object.entries(aiStyle)) {
|
|
233
|
+
const normalized = normalizeStyleValue(k, v);
|
|
234
|
+
|
|
235
|
+
if (normalized !== undefined) filtered[k] = normalized;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const resolved = resolveGlobalPresetIntoStyle(
|
|
239
|
+
content[modelKey] as object,
|
|
240
|
+
textStyleOptions,
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
record[modelKey] = { ...resolved, ...filtered, textStyleType: undefined };
|
|
244
|
+
|
|
245
|
+
// A colour patch without new text leaves the field's EXISTING inline
|
|
246
|
+
// textStyle marks in place — and inline marks override every style layer
|
|
247
|
+
// at render, so the old colour survives on a recoloured band (invisible
|
|
248
|
+
// text, AI QA 2026-07-05 rerun). Propagate the new colour into the
|
|
249
|
+
// stored HTML, mirroring global-font-propagation for font families.
|
|
250
|
+
const textKey = modelKey.endsWith('Style')
|
|
251
|
+
? modelKey.slice(0, -'Style'.length)
|
|
252
|
+
: undefined;
|
|
253
|
+
const textAiKey = aiKey.endsWith('_style')
|
|
254
|
+
? aiKey.slice(0, -'_style'.length)
|
|
255
|
+
: undefined;
|
|
256
|
+
const newColor = filtered.color;
|
|
257
|
+
|
|
258
|
+
if (
|
|
259
|
+
textKey &&
|
|
260
|
+
typeof newColor === 'string' &&
|
|
261
|
+
newColor &&
|
|
262
|
+
!(textAiKey && textAiKey in args) &&
|
|
263
|
+
typeof record[textKey] === 'string' &&
|
|
264
|
+
hasRTEContent(record[textKey] as string)
|
|
265
|
+
) {
|
|
266
|
+
record[textKey] = applyColorToHtml(record[textKey] as string, newColor);
|
|
267
|
+
}
|
|
212
268
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
content[modelKey] as object,
|
|
216
|
-
textStyleOptions,
|
|
217
|
-
);
|
|
218
|
-
|
|
219
|
-
record[modelKey] = { ...resolved, ...filtered, textStyleType: undefined };
|
|
269
|
+
} finally {
|
|
270
|
+
editor?.destroy();
|
|
220
271
|
}
|
|
221
272
|
};
|