@designcrowd/library.brand-page 6.2.3-ast-faq → 6.2.3
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-BRisMzFY.js → CreatedOnBrandCrowd-de-de-Cn0M-eZ1.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-en-us-CDDsL9GX.js → CreatedOnBrandCrowd-en-us-DRLMSDZr.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-es-es-BkZrEo4j.js → CreatedOnBrandCrowd-es-es-BglTu73U.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-fr-fr-sx_ury7R.js → CreatedOnBrandCrowd-fr-ca-Bj6YZ_el.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-fr-ca-sx_ury7R.js → CreatedOnBrandCrowd-fr-fr-Bj6YZ_el.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-pt-pt-DCWiAfkU.js → CreatedOnBrandCrowd-pt-br-DEoOmQ1F.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-pt-br-DCWiAfkU.js → CreatedOnBrandCrowd-pt-pt-DEoOmQ1F.js} +1 -1
- package/lib/src/{CreatedOnDcom-de-de-C1S7f22L.js → CreatedOnDcom-de-de-BZ7gTt4Z.js} +1 -1
- package/lib/src/{CreatedOnDcom-en-us-DgTlWaXE.js → CreatedOnDcom-en-us-CD6Km5sH.js} +1 -1
- package/lib/src/{CreatedOnDcom-es-es-CxSQJe_V.js → CreatedOnDcom-es-es-CNpZHvEl.js} +1 -1
- package/lib/src/{CreatedOnDcom-fr-ca-Bbam2tt6.js → CreatedOnDcom-fr-ca-URS-ydJ1.js} +1 -1
- package/lib/src/{CreatedOnDcom-fr-fr-Cb0XDRPL.js → CreatedOnDcom-fr-fr-D2AVeA3H.js} +1 -1
- package/lib/src/{CreatedOnDcom-pt-br-BjdX3-E3.js → CreatedOnDcom-pt-br-C32frnFs.js} +1 -1
- package/lib/src/{CreatedOnDcom-pt-pt-BXpbfopB.js → CreatedOnDcom-pt-pt-TDYGjRXS.js} +1 -1
- package/lib/src/brand-page/constants/misc.constants.d.ts +0 -8
- package/lib/src/brand-page/constants/misc.constants.js +0 -7
- package/lib/src/brand-page/constants/styles.constants.d.ts +0 -2
- package/lib/src/brand-page/constants/styles.constants.js +0 -2
- package/lib/src/brand-page/models/content.model.d.ts +0 -1
- package/lib/src/brand-page/models/content.model.js +0 -1
- package/lib/src/brand-page/models/index.d.ts +0 -19
- package/lib/src/brand-page/utils/content-callback-util.d.ts +1 -2
- package/lib/src/brand-page/utils/content-callback-util.js +0 -3
- package/lib/src/brand-page/utils/model.util.d.ts +1 -2
- package/lib/src/brand-page/utils/model.util.js +0 -3
- package/lib/src/brand-page/utils/view-model.util.d.ts +0 -2
- package/lib/src/brand-page/utils/view-model.util.js +0 -24
- package/lib/src/brand-page/view-models.d.ts +2 -9
- package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.d.ts +0 -10
- package/lib/src/brand-page-maker/components/brandpage-maker-page/BrandPageMakerPage.mixin.d.ts +0 -10
- package/lib/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.d.ts +0 -13
- package/lib/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.js +30 -64
- package/lib/src/brand-page-maker/constants/config-settings-constant.d.ts +1 -2
- package/lib/src/brand-page-maker/constants/config-settings-constant.js +2 -101
- package/lib/src/brand-page-maker/models/content-settings-configs.d.ts +1 -22
- package/lib/src/brand-page-maker/models/content-settings-configs.js +0 -1
- package/lib/src/brand-page-maker/models/content-settings.d.ts +1 -4
- package/lib/src/brand-page-maker/models/content-settings.js +1 -45
- package/lib/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.js +0 -1
- package/lib/src/brand-page-maker/utils/apply-color-strategy.js +17 -47
- package/lib/src/brand-page-maker/utils/rich-text-editor.util.d.ts +0 -6
- package/lib/src/brand-page-maker/utils/rich-text-editor.util.js +1 -24
- package/lib/src/brand-page-maker/validators/field.validator.js +0 -2
- package/lib/src/bundled-translations-BYUfWBT3.js +179 -0
- package/lib/src/bundles/bundled-translations.json +2 -20
- package/lib/src/composables/useTranslate.d.ts +1 -1
- package/lib/src/composables/useTranslate.js +1 -1
- package/lib/src/css/library.brand-page-brandCrowd.min.css +36 -143
- package/lib/src/css/library.brand-page-brandPage.min.css +36 -143
- package/lib/src/css/library.brand-page-designCom.min.css +36 -143
- package/lib/src/css/library.brand-page-preview.min.css +36 -143
- package/lib/src/index.mjs +158 -159
- package/lib/src/{lib-DwpSAbLZ.js → lib-Dm9AuOJL.js} +26180 -26906
- package/lib/src/metadata-builder/parsers/section-tool-arguments.d.ts +1 -1
- package/lib/src/shared/color-strategies/color-strategies.js +0 -4
- package/lib/src/shared/color-strategies/rte-field-pairs.d.ts +0 -12
- package/lib/src/shared/color-strategies/rte-field-pairs.js +0 -10
- package/lib/src/shared/content-settings/components/content-settings/ContentSettings.mixin.d.ts +0 -10
- package/lib/src/shared/content-settings/components/content-settings/ReplaceImageSettingsForContent.mixin.d.ts +1 -11
- package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItem.mixin.d.ts +0 -10
- package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.d.ts +0 -10
- package/lib/src/shared/content-settings/components/focused-content-item-settings/FocusedContentItemSettings.mixin.d.ts +0 -10
- package/lib/src/shared/content-settings/components/items-field/ItemsField.mixin.d.ts +1 -14
- package/lib/src/shared/content-settings/components/items-field/ItemsField.mixin.js +1 -11
- package/lib/src/shared/content-settings/components/textarea-field/TextareaField.mixin.d.ts +0 -12
- package/lib/src/shared/content-settings/components/textarea-field/TextareaField.mixin.js +0 -5
- package/lib/src/shared/content-settings/models.d.ts +0 -6
- package/lib/src/shared/content-settings/store/content-settings-store.d.ts +1 -14
- package/lib/src/shared/content-settings/store/content-settings-store.js +2 -24
- package/lib/src/shared/page-settings/components/page-settings/PageSettings.mixin.d.ts +0 -10
- package/lib/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.d.ts +0 -10
- package/package.json +2 -2
- package/src/brand-page/components/page/PageComponent.vue +0 -2
- package/src/brand-page/constants/misc.constants.ts +0 -12
- package/src/brand-page/constants/styles.constants.ts +0 -2
- package/src/brand-page/models/content.model.ts +0 -1
- package/src/brand-page/models/index.ts +0 -21
- package/src/brand-page/utils/content-callback-util.ts +0 -5
- package/src/brand-page/utils/model.util.ts +0 -7
- package/src/brand-page/utils/view-model.util.test.ts +0 -96
- package/src/brand-page/utils/view-model.util.ts +0 -49
- package/src/brand-page/view-models.ts +1 -12
- package/src/brand-page-maker/components/rich-text-editor/RichTextEditor.vue +49 -37
- package/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.ts +30 -87
- package/src/brand-page-maker/constants/config-settings-constant.ts +0 -108
- package/src/brand-page-maker/convertors/i18n/converter.json +1 -4
- package/src/brand-page-maker/models/content-settings-configs.ts +0 -24
- package/src/brand-page-maker/models/content-settings.ts +0 -57
- package/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.ts +0 -2
- package/src/brand-page-maker/utils/__snapshots__/apply-color-strategy.golden.test.ts.snap +389 -3557
- package/src/brand-page-maker/utils/apply-color-strategy.test.ts +2 -104
- package/src/brand-page-maker/utils/apply-color-strategy.ts +19 -77
- package/src/brand-page-maker/utils/rich-text-editor.util.test.ts +0 -110
- package/src/brand-page-maker/utils/rich-text-editor.util.ts +0 -66
- package/src/brand-page-maker/validators/field.validator.ts +0 -2
- package/src/bundles/bundled-translations.json +2 -20
- package/src/composables/useTranslate.ts +1 -2
- package/src/metadata-builder/builders/item-id-utils.test.ts +0 -24
- package/src/metadata-builder/parsers/section-tool-arguments.ts +1 -2
- package/src/shared/color-strategies/color-strategies.ts +0 -4
- package/src/shared/color-strategies/rte-field-pairs.ts +0 -23
- package/src/shared/content-settings/components/content-settings/i18n/content-settings.json +1 -17
- package/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.vue +0 -3
- package/src/shared/content-settings/components/items-field/ItemsField.mixin.ts +1 -11
- package/src/shared/content-settings/components/items-field/ItemsField.vue +2 -8
- package/src/shared/content-settings/components/textarea-field/TextareaField.mixin.ts +0 -6
- package/src/shared/content-settings/components/textarea-field/TextareaField.vue +0 -1
- package/src/shared/content-settings/models.ts +0 -7
- package/src/shared/content-settings/store/content-settings-store.test.ts +0 -70
- package/src/shared/content-settings/store/content-settings-store.ts +3 -37
- package/src/shared/ecommerce-settings/util/refreshDuplicatedMetadata.test.ts +0 -19
- package/src/utils/minify-brand-page-metadata.test.ts +0 -35
- package/lib/src/brand-page-maker/convertors/faq-display-style.convertor.d.ts +0 -3
- package/lib/src/brand-page-maker/convertors/faq-display-style.convertor.js +0 -47
- package/lib/src/bundled-translations-DY9YvuL-.js +0 -179
- package/src/brand-page/components/faq-section/FaqSection.test.ts +0 -292
- package/src/brand-page/components/faq-section/FaqSection.vue +0 -299
- package/src/brand-page/components/section-heading/SectionHeading.vue +0 -52
- package/src/brand-page/components/text-wrapper/TextWrapper.test.ts +0 -33
- package/src/brand-page-maker/components/rich-text-editor/RichTextEditor.test.ts +0 -165
- package/src/brand-page-maker/constants/faq-section-config.test.ts +0 -151
- package/src/brand-page-maker/convertors/faq-display-style.convertor.ts +0 -59
- package/src/metadata-builder/parsers/section-tool-map.test.ts +0 -12
- package/src/shared/content-settings/components/list-items-field/ListItemsField.test.ts +0 -429
- package/src/shared/content-settings/components/list-items-field/ListItemsField.vue +0 -262
|
@@ -1,299 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<section data-component="faq-section">
|
|
3
|
-
<div
|
|
4
|
-
class="tw-relative tw-px-6"
|
|
5
|
-
:style="[viewModel.backgroundStyle, contentPaddingStyle]"
|
|
6
|
-
style="container-type: inline-size"
|
|
7
|
-
>
|
|
8
|
-
<div
|
|
9
|
-
class="c_faq"
|
|
10
|
-
:class="[`c_faq--${layout}`, { 'c_faq--stacked': viewModel.isStacked }]"
|
|
11
|
-
>
|
|
12
|
-
<SectionHeading
|
|
13
|
-
class="c_faq__heading"
|
|
14
|
-
:title="viewModel.title"
|
|
15
|
-
:title-style="viewModel.titleStyle"
|
|
16
|
-
:description="viewModel.description"
|
|
17
|
-
:description-style="viewModel.descriptionStyle"
|
|
18
|
-
:slug="context.brandPageSlug"
|
|
19
|
-
/>
|
|
20
|
-
|
|
21
|
-
<!-- No items (D1): no list at all, so the heading gap doesn't linger. -->
|
|
22
|
-
<div v-if="hasItems && isAccordion" class="c_faq__items">
|
|
23
|
-
<div
|
|
24
|
-
v-for="item in viewModel.items"
|
|
25
|
-
:key="item.id"
|
|
26
|
-
class="c_faq__item"
|
|
27
|
-
:style="itemStyle(item)"
|
|
28
|
-
:data-test-faq-item="item.id"
|
|
29
|
-
>
|
|
30
|
-
<h3 class="tw-m-0">
|
|
31
|
-
<button
|
|
32
|
-
type="button"
|
|
33
|
-
class="c_faq__question"
|
|
34
|
-
:aria-expanded="isOpen(item.id)"
|
|
35
|
-
:aria-controls="answerId(item.id)"
|
|
36
|
-
data-test-faq-question
|
|
37
|
-
@click="onQuestionClick($event, item.id)"
|
|
38
|
-
>
|
|
39
|
-
<TextWrapper
|
|
40
|
-
:text-style="item.questionStyle"
|
|
41
|
-
:text="renderText(item.question)"
|
|
42
|
-
:slug="context.brandPageSlug"
|
|
43
|
-
element="span"
|
|
44
|
-
/>
|
|
45
|
-
<Icon
|
|
46
|
-
class="c_faq__toggle"
|
|
47
|
-
:name="isOpen(item.id) ? 'minus' : 'plus-light'"
|
|
48
|
-
size="sm"
|
|
49
|
-
/>
|
|
50
|
-
</button>
|
|
51
|
-
</h3>
|
|
52
|
-
<div
|
|
53
|
-
v-show="isOpen(item.id)"
|
|
54
|
-
:id="answerId(item.id)"
|
|
55
|
-
class="c_faq__answer"
|
|
56
|
-
data-test-faq-answer
|
|
57
|
-
>
|
|
58
|
-
<TextWrapper
|
|
59
|
-
:text-style="item.answerStyle"
|
|
60
|
-
:text="renderText(item.answer)"
|
|
61
|
-
:slug="context.brandPageSlug"
|
|
62
|
-
element="div"
|
|
63
|
-
/>
|
|
64
|
-
</div>
|
|
65
|
-
</div>
|
|
66
|
-
</div>
|
|
67
|
-
|
|
68
|
-
<div v-else-if="hasItems" class="c_faq__grid">
|
|
69
|
-
<div
|
|
70
|
-
v-for="item in viewModel.items"
|
|
71
|
-
:key="item.id"
|
|
72
|
-
:data-test-faq-item="item.id"
|
|
73
|
-
>
|
|
74
|
-
<TextWrapper
|
|
75
|
-
:text-style="item.questionStyle"
|
|
76
|
-
:text="renderText(item.question)"
|
|
77
|
-
:slug="context.brandPageSlug"
|
|
78
|
-
element="h3"
|
|
79
|
-
/>
|
|
80
|
-
<TextWrapper
|
|
81
|
-
class="tw-mt-2"
|
|
82
|
-
:text-style="item.answerStyle"
|
|
83
|
-
:text="renderText(item.answer)"
|
|
84
|
-
:slug="context.brandPageSlug"
|
|
85
|
-
element="div"
|
|
86
|
-
data-test-faq-answer
|
|
87
|
-
/>
|
|
88
|
-
</div>
|
|
89
|
-
</div>
|
|
90
|
-
</div>
|
|
91
|
-
</div>
|
|
92
|
-
</section>
|
|
93
|
-
</template>
|
|
94
|
-
<script setup lang="ts">
|
|
95
|
-
import type { PropType } from 'vue';
|
|
96
|
-
import { computed, ref, watch } from 'vue';
|
|
97
|
-
import { Icon } from '@designcrowd/fe-shared-lib';
|
|
98
|
-
import TextWrapper from '../text-wrapper/TextWrapper.vue';
|
|
99
|
-
import SectionHeading from '../section-heading/SectionHeading.vue';
|
|
100
|
-
import type { FaqSectionViewModel } from '@brand-page/view-models';
|
|
101
|
-
import type {
|
|
102
|
-
BrandPageContext,
|
|
103
|
-
FaqItem,
|
|
104
|
-
RichTextValue,
|
|
105
|
-
} from '@brand-page/models';
|
|
106
|
-
import { FAQ_DISPLAY_STYLES } from '@brand-page/constants';
|
|
107
|
-
import {
|
|
108
|
-
getRenderText,
|
|
109
|
-
contentPaddingStyle as toContentPaddingStyle,
|
|
110
|
-
} from '@brand-page/utils';
|
|
111
|
-
|
|
112
|
-
const props = defineProps({
|
|
113
|
-
viewModel: {
|
|
114
|
-
type: Object as PropType<FaqSectionViewModel>,
|
|
115
|
-
required: true,
|
|
116
|
-
},
|
|
117
|
-
context: {
|
|
118
|
-
type: Object as PropType<BrandPageContext>,
|
|
119
|
-
required: true,
|
|
120
|
-
},
|
|
121
|
-
containerClientWidth: {
|
|
122
|
-
type: Number as PropType<number | undefined>,
|
|
123
|
-
required: false,
|
|
124
|
-
default: undefined,
|
|
125
|
-
},
|
|
126
|
-
containerClientHeight: {
|
|
127
|
-
type: Number as PropType<number | undefined>,
|
|
128
|
-
required: false,
|
|
129
|
-
default: undefined,
|
|
130
|
-
},
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
const contentPaddingStyle = computed(() =>
|
|
134
|
-
toContentPaddingStyle(props.viewModel),
|
|
135
|
-
);
|
|
136
|
-
|
|
137
|
-
const layout = computed((): string => {
|
|
138
|
-
switch (props.viewModel.displayStyle) {
|
|
139
|
-
case FAQ_DISPLAY_STYLES.split:
|
|
140
|
-
return 'split';
|
|
141
|
-
case FAQ_DISPLAY_STYLES.grid:
|
|
142
|
-
return 'grid';
|
|
143
|
-
default:
|
|
144
|
-
return 'centered';
|
|
145
|
-
}
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
const isAccordion = computed((): boolean => layout.value !== 'grid');
|
|
149
|
-
|
|
150
|
-
const hasItems = computed((): boolean => props.viewModel.items.length > 0);
|
|
151
|
-
|
|
152
|
-
// The row divider and +/- icon follow the item's question colour.
|
|
153
|
-
const itemStyle = (item: FaqItem): { color?: string } => ({
|
|
154
|
-
color: item.questionStyle?.color,
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
// `null` = the visitor closed every item. Seeded from data (not `mounted`) so
|
|
158
|
-
// SSR and hydration render the same open item.
|
|
159
|
-
const openItemId = ref<string | null>(
|
|
160
|
-
props.viewModel.activeItemId ?? props.viewModel.items[0]?.id ?? null,
|
|
161
|
-
);
|
|
162
|
-
|
|
163
|
-
const isOpen = (itemId: string): boolean => openItemId.value === itemId;
|
|
164
|
-
|
|
165
|
-
const toggle = (itemId: string): void => {
|
|
166
|
-
openItemId.value = isOpen(itemId) ? null : itemId;
|
|
167
|
-
};
|
|
168
|
-
|
|
169
|
-
// A link inside a rich-text question follows the link; only the rest of the row toggles.
|
|
170
|
-
const onQuestionClick = (event: MouseEvent, itemId: string): void => {
|
|
171
|
-
if ((event.target as Element | null)?.closest?.('a')) {
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
toggle(itemId);
|
|
176
|
-
};
|
|
177
|
-
|
|
178
|
-
const answerId = (itemId: string): string =>
|
|
179
|
-
`faq-answer-${props.viewModel.id}-${itemId}`;
|
|
180
|
-
|
|
181
|
-
// Plain-text items keep their escaped newlines; a rich-text document renders as-is.
|
|
182
|
-
const renderText = (text: RichTextValue): RichTextValue =>
|
|
183
|
-
typeof text === 'string' || text == null ? getRenderText(text ?? '') : text;
|
|
184
|
-
|
|
185
|
-
// Maker only: follow the item the settings panel opened.
|
|
186
|
-
watch(
|
|
187
|
-
() => props.viewModel.activeItemId,
|
|
188
|
-
(activeItemId) => {
|
|
189
|
-
if (activeItemId) {
|
|
190
|
-
openItemId.value = activeItemId;
|
|
191
|
-
}
|
|
192
|
-
},
|
|
193
|
-
);
|
|
194
|
-
|
|
195
|
-
// The open item was deleted → open the first remaining one.
|
|
196
|
-
watch(
|
|
197
|
-
() => props.viewModel.items.map((item) => item.id),
|
|
198
|
-
(itemIds) => {
|
|
199
|
-
if (openItemId.value !== null && !itemIds.includes(openItemId.value)) {
|
|
200
|
-
openItemId.value = itemIds[0] ?? null;
|
|
201
|
-
}
|
|
202
|
-
},
|
|
203
|
-
);
|
|
204
|
-
</script>
|
|
205
|
-
|
|
206
|
-
<style scoped>
|
|
207
|
-
/* Spacing follows the Figma redlines ("Website — Features & Widgets", FAQ): 32 heading→items,
|
|
208
|
-
16 above and below each question, dividers only between items. */
|
|
209
|
-
.c_faq {
|
|
210
|
-
margin: 0 auto;
|
|
211
|
-
max-width: var(--brandsite-max-width);
|
|
212
|
-
display: flex;
|
|
213
|
-
flex-direction: column;
|
|
214
|
-
gap: 32px;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
.c_faq--centered .c_faq__items {
|
|
218
|
-
width: 100%;
|
|
219
|
-
max-width: 48rem;
|
|
220
|
-
margin: 0 auto;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
.c_faq__item {
|
|
224
|
-
padding: 16px 0;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
.c_faq__item + .c_faq__item {
|
|
228
|
-
/* ADR 0009: color-mix without a fallback is accepted. */
|
|
229
|
-
border-top: 1px solid color-mix(in srgb, currentColor 15%, transparent);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
.c_faq__question {
|
|
233
|
-
display: flex;
|
|
234
|
-
align-items: center;
|
|
235
|
-
justify-content: space-between;
|
|
236
|
-
gap: 16px;
|
|
237
|
-
width: 100%;
|
|
238
|
-
padding: 0;
|
|
239
|
-
border: none;
|
|
240
|
-
background: none;
|
|
241
|
-
/* Buttons don't inherit font by default; the question's em sizes need the h3's. */
|
|
242
|
-
font: inherit;
|
|
243
|
-
color: inherit;
|
|
244
|
-
text-align: left;
|
|
245
|
-
cursor: pointer;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
.c_faq__toggle {
|
|
249
|
-
flex: 0 0 auto;
|
|
250
|
-
/* Stays on the right when the question is empty and renders nothing (D9). */
|
|
251
|
-
margin-left: auto;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
/* A long unbroken word wraps instead of pushing the +/- out of the row. */
|
|
255
|
-
.c_faq__question > [data-component='text-wrapper'] {
|
|
256
|
-
flex: 1 1 auto;
|
|
257
|
-
min-width: 0;
|
|
258
|
-
overflow-wrap: anywhere;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
.c_faq__answer {
|
|
262
|
-
margin-top: 8px;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
.c_faq__grid {
|
|
266
|
-
display: grid;
|
|
267
|
-
grid-template-columns: 1fr 1fr;
|
|
268
|
-
gap: 32px;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
@container (min-width: 640px) {
|
|
272
|
-
.c_faq--split:not(.c_faq--stacked) {
|
|
273
|
-
flex-direction: row;
|
|
274
|
-
align-items: flex-start;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
/* 1/3 heading · 2/3 items, either side of the 32px gap. */
|
|
278
|
-
.c_faq--split:not(.c_faq--stacked) .c_faq__heading {
|
|
279
|
-
flex: 1 1 0;
|
|
280
|
-
min-width: 0;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
.c_faq--split:not(.c_faq--stacked) .c_faq__items {
|
|
284
|
-
flex: 2 1 0;
|
|
285
|
-
min-width: 0;
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
@container (max-width: 640px) {
|
|
290
|
-
.c_faq__grid {
|
|
291
|
-
grid-template-columns: 1fr;
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
/* brand.page / brand.contact always stack split and grid. */
|
|
296
|
-
.c_faq--stacked .c_faq__grid {
|
|
297
|
-
grid-template-columns: 1fr;
|
|
298
|
-
}
|
|
299
|
-
</style>
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div v-if="hasTitle || hasDescription" data-component="section-heading">
|
|
3
|
-
<TextWrapper
|
|
4
|
-
v-if="hasTitle"
|
|
5
|
-
:text-style="titleStyle"
|
|
6
|
-
:text="title"
|
|
7
|
-
:slug="slug"
|
|
8
|
-
element="h2"
|
|
9
|
-
/>
|
|
10
|
-
<TextWrapper
|
|
11
|
-
v-if="hasDescription"
|
|
12
|
-
class="tw-mt-4"
|
|
13
|
-
:text-style="descriptionStyle"
|
|
14
|
-
:text="renderDescription"
|
|
15
|
-
:slug="slug"
|
|
16
|
-
element="div"
|
|
17
|
-
/>
|
|
18
|
-
</div>
|
|
19
|
-
</template>
|
|
20
|
-
<script setup lang="ts">
|
|
21
|
-
import { computed } from 'vue';
|
|
22
|
-
import TextWrapper from '../text-wrapper/TextWrapper.vue';
|
|
23
|
-
import type { RichTextValue, TextStyleDeclaration } from '@brand-page/models';
|
|
24
|
-
import { escapeContentText, getRenderText } from '@brand-page/utils';
|
|
25
|
-
|
|
26
|
-
// Section title (the h2 — ADR 0004) + optional description, shared by the
|
|
27
|
-
// item-list sections (see content-sections.md, near-term requirement).
|
|
28
|
-
const props = withDefaults(
|
|
29
|
-
defineProps<{
|
|
30
|
-
title?: RichTextValue;
|
|
31
|
-
titleStyle?: TextStyleDeclaration;
|
|
32
|
-
description?: RichTextValue;
|
|
33
|
-
descriptionStyle?: TextStyleDeclaration;
|
|
34
|
-
slug?: string;
|
|
35
|
-
}>(),
|
|
36
|
-
{
|
|
37
|
-
title: undefined,
|
|
38
|
-
titleStyle: undefined,
|
|
39
|
-
description: undefined,
|
|
40
|
-
descriptionStyle: undefined,
|
|
41
|
-
slug: undefined,
|
|
42
|
-
},
|
|
43
|
-
);
|
|
44
|
-
|
|
45
|
-
const hasTitle = computed((): boolean => !!escapeContentText(props.title));
|
|
46
|
-
const hasDescription = computed(
|
|
47
|
-
(): boolean => !!escapeContentText(props.description),
|
|
48
|
-
);
|
|
49
|
-
const renderDescription = computed(
|
|
50
|
-
(): RichTextValue => getRenderText(props.description as string),
|
|
51
|
-
);
|
|
52
|
-
</script>
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { mount } from '@vue/test-utils';
|
|
2
|
-
import TextWrapper from './TextWrapper.vue';
|
|
3
|
-
|
|
4
|
-
const boldDoc = {
|
|
5
|
-
type: 'doc',
|
|
6
|
-
content: [
|
|
7
|
-
{
|
|
8
|
-
type: 'paragraph',
|
|
9
|
-
content: [{ type: 'text', text: 'Hello', marks: [{ type: 'bold' }] }],
|
|
10
|
-
},
|
|
11
|
-
],
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
const textStyle = { fontSize: '1.5em', color: 'rgb(255, 0, 0)' };
|
|
15
|
-
|
|
16
|
-
const container = (wrapper: ReturnType<typeof mount>) =>
|
|
17
|
-
wrapper.find('.empty-para').element as HTMLElement;
|
|
18
|
-
|
|
19
|
-
describe('TextWrapper', () => {
|
|
20
|
-
test('rich text ignores the text style by default — its marks own the look', () => {
|
|
21
|
-
const wrapper = mount(TextWrapper, { props: { text: boldDoc, textStyle } });
|
|
22
|
-
|
|
23
|
-
expect(wrapper.find('strong').text()).toBe('Hello');
|
|
24
|
-
expect(container(wrapper).style.fontSize).toBe('');
|
|
25
|
-
expect(container(wrapper).style.color).toBe('');
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
test('plain text is styled by the text style', () => {
|
|
29
|
-
const wrapper = mount(TextWrapper, { props: { text: 'Plain', textStyle } });
|
|
30
|
-
|
|
31
|
-
expect(container(wrapper).style.fontSize).toBe('1.5em');
|
|
32
|
-
});
|
|
33
|
-
});
|
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
import { vi } from 'vitest';
|
|
2
|
-
import { flushPromises, mount } from '@vue/test-utils';
|
|
3
|
-
import { createPinia, setActivePinia } from 'pinia';
|
|
4
|
-
import RichTextEditor from './RichTextEditor.vue';
|
|
5
|
-
import { useUiContentSettingsStore } from '@shared/content-settings/store';
|
|
6
|
-
|
|
7
|
-
// The toolbar capitalises translated role names at setup; tests carry no bundles.
|
|
8
|
-
vi.mock('@src/composables/useTranslate', async (importOriginal) => ({
|
|
9
|
-
...(await importOriginal<Record<string, unknown>>()),
|
|
10
|
-
styleSettingsTr: (key: string) => key,
|
|
11
|
-
commonTr: (key: string) => key,
|
|
12
|
-
}));
|
|
13
|
-
|
|
14
|
-
const doc = {
|
|
15
|
-
type: 'doc',
|
|
16
|
-
content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Hi' }] }],
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
const itemStyleTarget = {
|
|
20
|
-
itemsField: 'items',
|
|
21
|
-
index: 1,
|
|
22
|
-
styleField: 'questionStyle',
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
const mountEditor = (props: Record<string, unknown> = {}) =>
|
|
26
|
-
mount(RichTextEditor, {
|
|
27
|
-
props: {
|
|
28
|
-
id: 'faq-1-question',
|
|
29
|
-
name: 'question',
|
|
30
|
-
rawId: 'faq-1',
|
|
31
|
-
modelValue: doc,
|
|
32
|
-
...props,
|
|
33
|
-
} as any,
|
|
34
|
-
global: {
|
|
35
|
-
directives: { clickOutside: {} },
|
|
36
|
-
stubs: { FontPicker: true },
|
|
37
|
-
},
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
const editorOf = (wrapper: ReturnType<typeof mountEditor>) =>
|
|
41
|
-
(wrapper.find('.ProseMirror').element as any).editor;
|
|
42
|
-
|
|
43
|
-
describe('RichTextEditor', () => {
|
|
44
|
-
beforeEach(() => {
|
|
45
|
-
setActivePinia(createPinia());
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
test('an item editor has the full section-title toolbar', () => {
|
|
49
|
-
const tags = mountEditor({ itemStyleTarget })
|
|
50
|
-
.find('.style-editor')
|
|
51
|
-
.findAll('[data-test-tag]')
|
|
52
|
-
.map((el) => el.attributes('data-test-tag'));
|
|
53
|
-
|
|
54
|
-
expect(tags).toEqual(
|
|
55
|
-
expect.arrayContaining([
|
|
56
|
-
'text-style-dropdown',
|
|
57
|
-
'font-style-dropdown',
|
|
58
|
-
'font-size-stepper',
|
|
59
|
-
'text-color-picker',
|
|
60
|
-
'bold-toolbar',
|
|
61
|
-
'link-toolbar',
|
|
62
|
-
]),
|
|
63
|
-
);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
test.each([
|
|
67
|
-
['an item editor', { itemStyleTarget }, 'updateListItemTextStyles'],
|
|
68
|
-
['a section field', {}, 'updateTextStyles'],
|
|
69
|
-
])(
|
|
70
|
-
'%s writes its toolbar styles through %s',
|
|
71
|
-
async (_label, props, expectedAction) => {
|
|
72
|
-
const store = useUiContentSettingsStore();
|
|
73
|
-
const spies = {
|
|
74
|
-
updateListItemTextStyles: vi.spyOn(store, 'updateListItemTextStyles'),
|
|
75
|
-
updateTextStyles: vi.spyOn(store, 'updateTextStyles'),
|
|
76
|
-
};
|
|
77
|
-
const wrapper = mountEditor(props);
|
|
78
|
-
|
|
79
|
-
await flushPromises();
|
|
80
|
-
Object.values(spies).forEach((spy) => spy.mockClear());
|
|
81
|
-
await wrapper.find('[data-test-tag="bold-toolbar"]').trigger('click');
|
|
82
|
-
|
|
83
|
-
Object.entries(spies).forEach(([action, spy]) =>
|
|
84
|
-
expect(spy).toHaveBeenCalledTimes(action === expectedAction ? 1 : 0),
|
|
85
|
-
);
|
|
86
|
-
|
|
87
|
-
if (expectedAction === 'updateListItemTextStyles') {
|
|
88
|
-
expect(spies.updateListItemTextStyles).toHaveBeenCalledWith(
|
|
89
|
-
expect.objectContaining({
|
|
90
|
-
id: 'faq-1',
|
|
91
|
-
target: itemStyleTarget,
|
|
92
|
-
value: expect.objectContaining({ fontWeight: 'bold' }),
|
|
93
|
-
}),
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
},
|
|
97
|
-
);
|
|
98
|
-
|
|
99
|
-
test('picking a text style on an item editor links that item, not the section', async () => {
|
|
100
|
-
const store = useUiContentSettingsStore();
|
|
101
|
-
const updateListItemTextStyles = vi.spyOn(
|
|
102
|
-
store,
|
|
103
|
-
'updateListItemTextStyles',
|
|
104
|
-
);
|
|
105
|
-
const updateTextStyles = vi.spyOn(store, 'updateTextStyles');
|
|
106
|
-
const wrapper = mountEditor({ itemStyleTarget });
|
|
107
|
-
|
|
108
|
-
await flushPromises();
|
|
109
|
-
updateTextStyles.mockClear();
|
|
110
|
-
|
|
111
|
-
const headings = wrapper
|
|
112
|
-
.findAll('.drop-down-item')
|
|
113
|
-
.find((el) => el.text().toLowerCase().includes('headings'));
|
|
114
|
-
|
|
115
|
-
await headings!.trigger('click');
|
|
116
|
-
|
|
117
|
-
expect(updateListItemTextStyles).toHaveBeenCalledWith(
|
|
118
|
-
expect.objectContaining({
|
|
119
|
-
target: itemStyleTarget,
|
|
120
|
-
value: expect.objectContaining({ textStyleType: 'headings' }),
|
|
121
|
-
}),
|
|
122
|
-
);
|
|
123
|
-
expect(updateTextStyles).not.toHaveBeenCalled();
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
test("a linked item editor's mount-time style sync lands on the item, as a hydration write", async () => {
|
|
127
|
-
const store = useUiContentSettingsStore();
|
|
128
|
-
const updateListItemTextStyles = vi.spyOn(
|
|
129
|
-
store,
|
|
130
|
-
'updateListItemTextStyles',
|
|
131
|
-
);
|
|
132
|
-
const updateTextStyles = vi.spyOn(store, 'updateTextStyles');
|
|
133
|
-
|
|
134
|
-
mountEditor({ itemStyleTarget, styles: { textStyleType: 'headings' } });
|
|
135
|
-
await flushPromises();
|
|
136
|
-
|
|
137
|
-
expect(updateListItemTextStyles).toHaveBeenCalledWith(
|
|
138
|
-
expect.objectContaining({
|
|
139
|
-
target: itemStyleTarget,
|
|
140
|
-
isHydrationWrite: true,
|
|
141
|
-
}),
|
|
142
|
-
);
|
|
143
|
-
expect(updateTextStyles).not.toHaveBeenCalled();
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
// The toolbar has taken focus by the time its command runs; tagging that
|
|
147
|
-
// write as hydration would fold it into the previous undo entry.
|
|
148
|
-
test('a toolbar command on a selection emits a user write, not a hydration write', async () => {
|
|
149
|
-
const wrapper = mountEditor();
|
|
150
|
-
|
|
151
|
-
await flushPromises();
|
|
152
|
-
|
|
153
|
-
const editor = editorOf(wrapper);
|
|
154
|
-
|
|
155
|
-
editor.commands.selectAll();
|
|
156
|
-
editor.commands.blur();
|
|
157
|
-
await wrapper.find('[data-test-tag="bold-toolbar"]').trigger('click');
|
|
158
|
-
|
|
159
|
-
const emits = wrapper.emitted('update:modelValue') ?? [];
|
|
160
|
-
const [value, meta] = emits[emits.length - 1] as [any, any];
|
|
161
|
-
|
|
162
|
-
expect(JSON.stringify(value)).toContain('"bold"');
|
|
163
|
-
expect(meta.isHydrationWrite).toBe(false);
|
|
164
|
-
});
|
|
165
|
-
});
|
|
@@ -1,151 +0,0 @@
|
|
|
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
|
-
});
|