@designcrowd/library.brand-page 6.0.12 → 6.0.13

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 (109) hide show
  1. package/lib/src/{CreatedOnBrandCrowd-de-de-BfQcnhof.js → CreatedOnBrandCrowd-de-de-CBjFFeBP.js} +1 -1
  2. package/lib/src/{CreatedOnBrandCrowd-en-us-B86JfNzV.js → CreatedOnBrandCrowd-en-us-NuNXF81K.js} +1 -1
  3. package/lib/src/{CreatedOnBrandCrowd-es-es-Dy70BV7X.js → CreatedOnBrandCrowd-es-es-BJG8nfug.js} +1 -1
  4. package/lib/src/{CreatedOnBrandCrowd-fr-fr-CQ8UhGRG.js → CreatedOnBrandCrowd-fr-ca-uX-nL53e.js} +1 -1
  5. package/lib/src/{CreatedOnBrandCrowd-fr-ca-CQ8UhGRG.js → CreatedOnBrandCrowd-fr-fr-uX-nL53e.js} +1 -1
  6. package/lib/src/{CreatedOnBrandCrowd-pt-pt-c257u4q-.js → CreatedOnBrandCrowd-pt-br-DTrgFGQV.js} +1 -1
  7. package/lib/src/{CreatedOnBrandCrowd-pt-br-c257u4q-.js → CreatedOnBrandCrowd-pt-pt-DTrgFGQV.js} +1 -1
  8. package/lib/src/{CreatedOnDcom-de-de-cRZ_Lx3_.js → CreatedOnDcom-de-de-DSfpxioT.js} +1 -1
  9. package/lib/src/{CreatedOnDcom-en-us-CrfHKR-1.js → CreatedOnDcom-en-us-C7G4gKem.js} +1 -1
  10. package/lib/src/{CreatedOnDcom-es-es-BFjhMPTR.js → CreatedOnDcom-es-es-DVckCaji.js} +1 -1
  11. package/lib/src/{CreatedOnDcom-fr-ca-CZimjPcn.js → CreatedOnDcom-fr-ca-C5GDrm_C.js} +1 -1
  12. package/lib/src/{CreatedOnDcom-fr-fr-V1z3-WzE.js → CreatedOnDcom-fr-fr-v5eMm-j2.js} +1 -1
  13. package/lib/src/{CreatedOnDcom-pt-br-NrmWKrvi.js → CreatedOnDcom-pt-br-Dq3-fPUc.js} +1 -1
  14. package/lib/src/{CreatedOnDcom-pt-pt-CKLASXM9.js → CreatedOnDcom-pt-pt-DFsncYax.js} +1 -1
  15. package/lib/src/brand-page/components/image-gallery/ImageGallery.stories.js +3 -0
  16. package/lib/src/brand-page/components/image-gallery/masonry/MasonryGallery.mixin.d.ts +1 -1
  17. package/lib/src/brand-page/components/images-section/ImagesSection.stories.js +2 -1
  18. package/lib/src/brand-page/components/social-links/SocialLinks.stories.js +3 -0
  19. package/lib/src/brand-page/components/testimonial-section/__fixtures__/data.js +3 -0
  20. package/lib/src/brand-page/components/text/Text.stories.js +2 -0
  21. package/lib/src/brand-page/models/index.d.ts +13 -0
  22. package/lib/src/brand-page/models/index.js +4 -0
  23. package/lib/src/brand-page/utils/migration/migration-01.js +2 -0
  24. package/lib/src/brand-page/utils/migration/migration-18.d.ts +14 -0
  25. package/lib/src/brand-page/utils/migration/migration-18.js +17 -0
  26. package/lib/src/brand-page/utils/migration.util.js +6 -0
  27. package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.stories.js +1 -0
  28. package/lib/src/brand-page-maker/components/brand-page-maker/__fixtures__/brand-page-examples.js +1 -0
  29. package/lib/src/brand-page-maker/composables/ai-chat/useAiWebsiteBuilderChat.js +5 -0
  30. package/lib/src/brand-page-maker/constants/config-settings-constant.js +4 -0
  31. package/lib/src/brand-page-maker/models/content-settings.js +4 -0
  32. package/lib/src/brand-page-maker/stores/ai-checkpoint/ai-checkpoint-store.d.ts +18 -0
  33. package/lib/src/index.mjs +1 -1
  34. package/lib/src/{lib-CHfUKgt-.js → lib-BG-pyXi9.js} +19091 -18974
  35. package/lib/src/metadata-builder/ai-metadata-executor.js +24 -1
  36. package/lib/src/metadata-builder/builders/footer-builder.js +32 -41
  37. package/lib/src/metadata-builder/builders/item-id-utils.d.ts +54 -0
  38. package/lib/src/metadata-builder/builders/item-id-utils.js +96 -0
  39. package/lib/src/metadata-builder/builders/move-page-builder.d.ts +32 -0
  40. package/lib/src/metadata-builder/builders/move-page-builder.js +137 -0
  41. package/lib/src/metadata-builder/builders/nav-utils.d.ts +26 -0
  42. package/lib/src/metadata-builder/builders/nav-utils.js +95 -0
  43. package/lib/src/metadata-builder/builders/section-builders/image-gallery-builder.js +57 -68
  44. package/lib/src/metadata-builder/builders/section-builders/images-section-builder.js +58 -93
  45. package/lib/src/metadata-builder/builders/section-builders/move-section-builder.d.ts +13 -1
  46. package/lib/src/metadata-builder/builders/section-builders/move-section-builder.js +40 -30
  47. package/lib/src/metadata-builder/builders/section-builders/shapes-section-builder.js +66 -99
  48. package/lib/src/metadata-builder/builders/section-builders/social-links-builder.js +57 -17
  49. package/lib/src/metadata-builder/builders/section-builders/testimonial-section-builder.js +51 -85
  50. package/lib/src/metadata-builder/builders/section-builders/text-section-builder.js +50 -74
  51. package/lib/src/metadata-builder/builders/section-field-maps.js +17 -0
  52. package/lib/src/metadata-builder/builders/set-page-visibility-builder.d.ts +23 -0
  53. package/lib/src/metadata-builder/builders/set-page-visibility-builder.js +83 -0
  54. package/lib/src/metadata-builder/metadata-executor.js +4 -0
  55. package/lib/src/metadata-builder/models.d.ts +2 -0
  56. package/lib/src/metadata-builder/models.js +2 -0
  57. package/lib/src/metadata-builder/parsers/ai-toolcall-parser.d.ts +31 -7
  58. package/lib/src/metadata-builder/parsers/ai-toolcall-parser.js +2 -0
  59. package/lib/src/metadata-builder/parsers/section-tool-arguments.d.ts +84 -48
  60. package/lib/src/shared/content-settings/components/links-field/LinksField.mixin.js +2 -0
  61. package/lib/src/shared/page-settings/components/page-settings/PageSettings.mixin.d.ts +2 -0
  62. package/lib/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.d.ts +2 -0
  63. package/lib/src/utils/minify-brand-page-metadata.d.ts +2 -0
  64. package/lib/src/utils/minify-brand-page-metadata.js +8 -1
  65. package/package.json +1 -1
  66. package/src/brand-page/components/image-gallery/ImageGallery.stories.ts +3 -0
  67. package/src/brand-page/components/image-gallery/ImageGallery.test.ts +1 -0
  68. package/src/brand-page/components/image-gallery/masonry/MasonryGallery.mixin.ts +2 -1
  69. package/src/brand-page/components/images-section/ImagesSection.stories.ts +2 -1
  70. package/src/brand-page/components/social-links/SocialLinks.stories.ts +3 -0
  71. package/src/brand-page/components/social-links/SocialLinks.vue.test.ts +5 -0
  72. package/src/brand-page/components/testimonial-section/__fixtures__/data.ts +3 -0
  73. package/src/brand-page/components/text/Text.stories.ts +2 -0
  74. package/src/brand-page/models/index.ts +17 -0
  75. package/src/brand-page/utils/__snapshots__/migration.util.test.ts.snap +23 -3
  76. package/src/brand-page/utils/__snapshots__/migration2.util.test.ts.snap +6 -0
  77. package/src/brand-page/utils/__snapshots__/model.util.test.ts.snap +12 -0
  78. package/src/brand-page/utils/migration/migration-01.ts +2 -0
  79. package/src/brand-page/utils/migration/migration-18.test.ts +131 -0
  80. package/src/brand-page/utils/migration/migration-18.ts +22 -0
  81. package/src/brand-page/utils/migration.util.ts +7 -0
  82. package/src/brand-page/utils/personalisation.util.test.ts +4 -0
  83. package/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.stories.ts +1 -0
  84. package/src/brand-page-maker/components/brand-page-maker/__fixtures__/brand-page-examples.ts +1 -0
  85. package/src/brand-page-maker/composables/ai-chat/useAiWebsiteBuilderChat.ts +6 -0
  86. package/src/brand-page-maker/constants/config-settings-constant.ts +4 -0
  87. package/src/brand-page-maker/models/content-settings.ts +4 -0
  88. package/src/metadata-builder/ai-metadata-executor.ts +31 -1
  89. package/src/metadata-builder/builders/footer-builder.test.ts +335 -28
  90. package/src/metadata-builder/builders/footer-builder.ts +40 -57
  91. package/src/metadata-builder/builders/item-id-utils.test.ts +184 -0
  92. package/src/metadata-builder/builders/item-id-utils.ts +117 -0
  93. package/src/metadata-builder/builders/move-page-builder.ts +227 -0
  94. package/src/metadata-builder/builders/nav-utils.ts +141 -0
  95. package/src/metadata-builder/builders/section-builders/image-gallery-builder.ts +62 -88
  96. package/src/metadata-builder/builders/section-builders/images-section-builder.ts +66 -129
  97. package/src/metadata-builder/builders/section-builders/move-section-builder.ts +58 -35
  98. package/src/metadata-builder/builders/section-builders/shapes-section-builder.ts +105 -160
  99. package/src/metadata-builder/builders/section-builders/social-links-builder.ts +65 -20
  100. package/src/metadata-builder/builders/section-builders/testimonial-section-builder.ts +83 -146
  101. package/src/metadata-builder/builders/section-builders/text-section-builder.ts +71 -122
  102. package/src/metadata-builder/builders/section-field-maps.ts +18 -0
  103. package/src/metadata-builder/builders/set-page-visibility-builder.ts +122 -0
  104. package/src/metadata-builder/metadata-executor.ts +4 -0
  105. package/src/metadata-builder/models.ts +2 -0
  106. package/src/metadata-builder/parsers/ai-toolcall-parser.ts +32 -7
  107. package/src/metadata-builder/parsers/section-tool-arguments.ts +84 -48
  108. package/src/shared/content-settings/components/links-field/LinksField.mixin.ts +2 -0
  109. package/src/utils/minify-brand-page-metadata.ts +14 -2
@@ -3,6 +3,7 @@ import { isTiptapDocument, } from '../../parsers/section-tool-arguments';
3
3
  import { applyScalarFields, applyStyleFields, resolveGlobalPresetIntoStyle, } from '../section-field-utils';
4
4
  import { BaseSectionBuilder } from './base-section-builder';
5
5
  import { validateTextSection } from '../section-rules/text-section.rules';
6
+ import { ensureItemIds, generateItemId, removeItemsByIds, repositionItemByAnchor, } from '../item-id-utils';
6
7
  /** Resolve a RichTextField (string or TiptapDocument) to a plain HTML string. */
7
8
  const resolveText = (value) => {
8
9
  if (isTiptapDocument(value))
@@ -16,55 +17,58 @@ const createTextItem = (item, baseStyle, textStyleOptions) => {
16
17
  const textStyle = (item.text_style ? { ...baseStyle, ...item.text_style } : { ...baseStyle });
17
18
  const { html, wasTiptap } = resolveText(item.text);
18
19
  return {
20
+ id: item.item_id || generateItemId(),
19
21
  text: html,
20
22
  textStyle: clearTextStyleTypeIfTiptap(textStyle, wasTiptap, textStyleOptions),
21
23
  };
22
24
  };
25
+ /**
26
+ * Full replacement: discard existing items and rebuild from the payload,
27
+ * assigning a stable id to each new item.
28
+ */
23
29
  const applyTextItems = (content, items, textStyleOptions) => {
24
30
  content.textItems = items.map((item) => createTextItem(item, content.textStyle || {}, textStyleOptions));
25
31
  };
26
- const mergeTextItems = (content, items, replaceAll, textStyleOptions) => {
27
- const hasIndexedItems = items.some((item) => item.item_index !== undefined);
28
- if (hasIndexedItems) {
29
- for (const item of items) {
30
- const targetIndex = item.item_index;
31
- if (targetIndex === undefined ||
32
- targetIndex < 0 ||
33
- !content.textItems?.[targetIndex])
32
+ /**
33
+ * Id-based merge (BP-5603): mirrors the section-level id contract one level
34
+ * deeper.
35
+ * - item with `item_id` matching an existing item → update in place and
36
+ * reposition if `move_before_item_id` is set; unknown id is skipped.
37
+ * - item without `item_id` → add a new item,
38
+ * positioned by `move_before_item_id`.
39
+ * - `removeIds` → delete by id.
40
+ *
41
+ * Existing items are id-backfilled first so legacy sections without ids
42
+ * become referenceable the moment this builder processes them.
43
+ */
44
+ const mergeTextItems = (content, items, removeIds, textStyleOptions) => {
45
+ ensureItemIds(content.textItems);
46
+ content.textItems = removeItemsByIds(content.textItems, removeIds);
47
+ for (const item of items) {
48
+ if (item.item_id) {
49
+ const existing = content.textItems?.find((i) => i.id === item.item_id);
50
+ // Unknown id — skip rather than guess at a position.
51
+ if (!existing)
34
52
  continue;
35
- const existing = content.textItems[targetIndex];
36
53
  if (item.text !== undefined) {
37
54
  const { html, wasTiptap } = resolveText(item.text);
38
55
  existing.text = html;
39
56
  existing.textStyle = clearTextStyleTypeIfTiptap(existing.textStyle || {}, wasTiptap, textStyleOptions);
40
57
  }
41
58
  if (item.text_style && typeof item.text_style === 'object') {
42
- const mergedStyle = (item.text_style
43
- ? { ...(existing.textStyle || {}), ...item.text_style }
44
- : existing.textStyle || {});
45
- existing.textStyle = mergedStyle;
59
+ existing.textStyle = {
60
+ ...(existing.textStyle || {}),
61
+ ...item.text_style,
62
+ };
46
63
  }
64
+ repositionItemByAnchor(content.textItems, existing, item.move_before_item_id);
65
+ }
66
+ else {
67
+ const newItem = createTextItem(item, content.textStyle || {}, textStyleOptions);
68
+ content.textItems = content.textItems ?? [];
69
+ repositionItemByAnchor(content.textItems, newItem, item.move_before_item_id);
47
70
  }
48
- return;
49
- }
50
- const itemsToMerge = items.filter((item) => item.insert_at === undefined);
51
- const updatedItems = itemsToMerge.map((item, index) => {
52
- const existing = content.textItems?.[index];
53
- const baseStyle = existing?.textStyle || content.textStyle || {};
54
- const mergedStyle = (item.text_style ? { ...baseStyle, ...item.text_style } : baseStyle);
55
- const resolved = item.text !== undefined ? resolveText(item.text) : null;
56
- const text = resolved ? resolved.html : (existing?.text ?? '');
57
- return {
58
- text,
59
- textStyle: clearTextStyleTypeIfTiptap(mergedStyle, resolved?.wasTiptap ?? false, textStyleOptions),
60
- };
61
- });
62
- if (!replaceAll &&
63
- content.textItems &&
64
- content.textItems.length > itemsToMerge.length) {
65
- updatedItems.push(...content.textItems.slice(itemsToMerge.length));
66
71
  }
67
- content.textItems = updatedItems;
68
72
  };
69
73
  /**
70
74
  * Handles the single `text` + `text_style` fields (BrandPage/BrandContact
@@ -83,6 +87,7 @@ const applySingleTextField = (content, args, textStyleOptions) => {
83
87
  else {
84
88
  content.textItems = [
85
89
  {
90
+ id: generateItemId(),
86
91
  text: html,
87
92
  textStyle: clearTextStyleTypeIfTiptap({ ...(content.textStyle || {}) }, wasTiptap, textStyleOptions),
88
93
  },
@@ -121,56 +126,27 @@ export class TextSectionBuilder extends BaseSectionBuilder {
121
126
  applyScalarFields(content, this.args, this.sectionType);
122
127
  applyStyleFields(content, this.args, this.sectionType, this.getGlobalTextStyles(model));
123
128
  const args = this.args;
124
- if (args.items && Array.isArray(args.items)) {
125
- if (this.mode === 'create') {
129
+ if (args._replace_items && args.items && Array.isArray(args.items)) {
130
+ // Full replacement: discard existing items and rebuild from the payload.
131
+ applyTextItems(content, args.items, textStyleOptions);
132
+ }
133
+ else if (this.mode === 'create') {
134
+ if (args.items && Array.isArray(args.items)) {
126
135
  applyTextItems(content, args.items, textStyleOptions);
127
136
  }
128
137
  else {
129
- mergeTextItems(content, args.items, !!args._replace_items, textStyleOptions);
138
+ applySingleTextField(content, args, textStyleOptions);
130
139
  }
131
140
  }
141
+ else if ((args.items && Array.isArray(args.items)) ||
142
+ args._remove_item_ids?.length) {
143
+ // Update: id-based add / edit / move / remove in a single pass.
144
+ mergeTextItems(content, args.items ?? [], args._remove_item_ids, textStyleOptions);
145
+ }
132
146
  else {
147
+ // No items array: apply the single text/text_style shorthand fields.
133
148
  applySingleTextField(content, args, textStyleOptions);
134
149
  }
135
- // Phase 2: Item operations (remove, insert, reorder)
136
- if (args._remove_item_indices?.length && content.textItems) {
137
- const sorted = [...args._remove_item_indices].sort((a, b) => b - a);
138
- for (const idx of sorted) {
139
- if (idx >= 0 && idx < content.textItems.length) {
140
- content.textItems.splice(idx, 1);
141
- }
142
- }
143
- }
144
- if (args.items && Array.isArray(args.items)) {
145
- const insertItems = args.items.filter((item) => item.insert_at !== undefined && item.item_index === undefined);
146
- const sortedInserts = [...insertItems].sort((a, b) => (b.insert_at ?? 0) - (a.insert_at ?? 0));
147
- for (const item of sortedInserts) {
148
- const pos = Math.max(0, Math.min(item.insert_at ?? 0, content.textItems?.length ?? 0));
149
- const baseStyle = content.textStyle || {};
150
- const textStyle = (item.text_style
151
- ? { ...baseStyle, ...item.text_style }
152
- : { ...baseStyle });
153
- const { html, wasTiptap } = resolveText(item.text);
154
- const newItem = {
155
- text: html,
156
- textStyle: clearTextStyleTypeIfTiptap(textStyle, wasTiptap, textStyleOptions),
157
- };
158
- content.textItems = content.textItems ?? [];
159
- content.textItems.splice(pos, 0, newItem);
160
- }
161
- }
162
- if (args._reorder_items?.length && content.textItems) {
163
- const newOrder = args._reorder_items;
164
- const unique = new Set(newOrder);
165
- if (unique.size === newOrder.length) {
166
- const reordered = newOrder
167
- .filter((idx) => idx >= 0 && idx < content.textItems.length)
168
- .map((idx) => content.textItems[idx]);
169
- if (reordered.length === content.textItems.length) {
170
- content.textItems = reordered;
171
- }
172
- }
173
- }
174
150
  if (args.background_color !== undefined) {
175
151
  content.backgroundColor = {
176
152
  ...content.backgroundColor,
@@ -12,6 +12,12 @@
12
12
  function defineFieldMap(map) {
13
13
  return map;
14
14
  }
15
+ // BP-5687: vertical padding fields shared by all sections that render
16
+ // contentPaddingStyle (see src/brand-page/utils/content-padding.util.ts).
17
+ const PADDING_FIELDS = {
18
+ padding_top: 'topPadding',
19
+ padding_bottom: 'bottomPadding',
20
+ };
15
21
  const bannerFields = defineFieldMap({
16
22
  title: 'title',
17
23
  description: 'description',
@@ -20,12 +26,14 @@ const bannerFields = defineFieldMap({
20
26
  background_image_url: 'imageUrl',
21
27
  background_shape_url: 'imageUrl',
22
28
  display_style: 'displayStyle',
29
+ ...PADDING_FIELDS,
23
30
  });
24
31
  const bookingFields = defineFieldMap({
25
32
  title: 'title',
26
33
  description: 'description',
27
34
  button_text: 'buttonText',
28
35
  button_url: 'url',
36
+ ...PADDING_FIELDS,
29
37
  });
30
38
  const aboutFields = defineFieldMap({
31
39
  title: 'title',
@@ -33,11 +41,13 @@ const aboutFields = defineFieldMap({
33
41
  about_image_url: 'imageUrl',
34
42
  about_shape_url: 'imageUrl',
35
43
  display_style: 'displayStyle',
44
+ ...PADDING_FIELDS,
36
45
  });
37
46
  const testimonialFields = defineFieldMap({
38
47
  title: 'title',
39
48
  display_style: 'displayStyle',
40
49
  // items handled separately
50
+ ...PADDING_FIELDS,
41
51
  });
42
52
  const imagesSectionFields = defineFieldMap({
43
53
  title: 'title',
@@ -46,6 +56,7 @@ const imagesSectionFields = defineFieldMap({
46
56
  aspect_ratio: 'aspectRatio',
47
57
  are_images_full_width: 'areImagesFullWidth',
48
58
  // items handled separately
59
+ ...PADDING_FIELDS,
49
60
  });
50
61
  const shapesSectionFields = defineFieldMap({
51
62
  title: 'title',
@@ -53,10 +64,12 @@ const shapesSectionFields = defineFieldMap({
53
64
  display_style: 'displayStyle',
54
65
  size: 'size',
55
66
  // items handled separately
67
+ ...PADDING_FIELDS,
56
68
  });
57
69
  const textFields = defineFieldMap({
58
70
  text: 'text',
59
71
  column_style: 'columnStyle',
72
+ ...PADDING_FIELDS,
60
73
  });
61
74
  const profileFields = defineFieldMap({
62
75
  name: 'name',
@@ -73,15 +86,18 @@ const contactFormFields = defineFieldMap({
73
86
  header_image_url: 'imageUrl',
74
87
  display_style: 'displayStyle',
75
88
  layout_style: 'layoutStyle',
89
+ ...PADDING_FIELDS,
76
90
  });
77
91
  const mailingListFields = defineFieldMap({
78
92
  button_text: 'buttonText',
79
93
  accepted_fields: 'acceptedFields',
94
+ ...PADDING_FIELDS,
80
95
  });
81
96
  const socialLinksFields = defineFieldMap({
82
97
  title: 'title',
83
98
  icon_style: 'iconDisplayStyle',
84
99
  // links handled separately
100
+ ...PADDING_FIELDS,
85
101
  });
86
102
  const dividerFields = defineFieldMap({
87
103
  divider_style: 'dividerStyle',
@@ -94,6 +110,7 @@ const imageFields = defineFieldMap({
94
110
  const imageGalleryFields = defineFieldMap({
95
111
  is_full_width: 'isFullWidth',
96
112
  // images handled separately
113
+ ...PADDING_FIELDS,
97
114
  });
98
115
  const buttonWithIconFields = defineFieldMap({
99
116
  title: 'title',
@@ -0,0 +1,23 @@
1
+ import type { BrandPageModel } from '../../brand-page/models';
2
+ import { BaseMetadataBuilder, type BuilderResult, type MetadataBuilderRequest } from '../models';
3
+ import type { Logger } from '../logger';
4
+ export interface SetPageVisibilityPayload {
5
+ /** The page whose visibility is toggled (resolved from page_id). */
6
+ pageId: string;
7
+ /** true = hide the page from the navigation; false = show it. */
8
+ isHidden: boolean;
9
+ }
10
+ /**
11
+ * Toggle a page's navigation visibility (the AI-chat equivalent of the page-settings
12
+ * "Hide page / Show page" menu). Sets `isHidden` on BOTH the matching `NavRoute`
13
+ * (in `model.nav.routes`) and the `PageModel` (in `model.pages`) — the two places the
14
+ * editor keeps in sync. No slug/order recompute (the nav order is unchanged); the
15
+ * published-site nav already filters hidden pages via `isHidden`.
16
+ */
17
+ export declare class SetPageVisibilityBuilder extends BaseMetadataBuilder<SetPageVisibilityPayload> {
18
+ constructor(request: MetadataBuilderRequest<SetPageVisibilityPayload>, logger?: Logger);
19
+ validateAsync(): Promise<{
20
+ errors: string[];
21
+ }>;
22
+ buildAsync(model: BrandPageModel): Promise<BuilderResult>;
23
+ }
@@ -0,0 +1,83 @@
1
+ import { BaseMetadataBuilder, } from '../models';
2
+ import { syncPagesWithNavOrders } from './nav-utils';
3
+ /**
4
+ * Toggle a page's navigation visibility (the AI-chat equivalent of the page-settings
5
+ * "Hide page / Show page" menu). Sets `isHidden` on BOTH the matching `NavRoute`
6
+ * (in `model.nav.routes`) and the `PageModel` (in `model.pages`) — the two places the
7
+ * editor keeps in sync. No slug/order recompute (the nav order is unchanged); the
8
+ * published-site nav already filters hidden pages via `isHidden`.
9
+ */
10
+ export class SetPageVisibilityBuilder extends BaseMetadataBuilder {
11
+ constructor(request, logger) {
12
+ super(request, logger);
13
+ }
14
+ async validateAsync() {
15
+ const errors = [];
16
+ if (!this.request.payload?.pageId) {
17
+ errors.push('pageId is required for set_page_visibility');
18
+ }
19
+ return { errors };
20
+ }
21
+ async buildAsync(model) {
22
+ this.log('Build called');
23
+ const validation = await this.validateAsync();
24
+ if (validation.errors.length) {
25
+ return { request: this.request, model, errors: validation.errors };
26
+ }
27
+ const { pageId, isHidden } = this.request.payload;
28
+ const targetRoute = model.nav.routes.find((r) => r.pageId === pageId);
29
+ if (!targetRoute) {
30
+ return {
31
+ request: this.request,
32
+ model,
33
+ errors: [`Page with ID "${pageId}" not found`],
34
+ };
35
+ }
36
+ // Guards apply only when HIDING; showing a page is always safe.
37
+ if (isHidden) {
38
+ // --- GUARD: the home page (orders[0]) cannot be hidden ---
39
+ const homeRouteId = model.nav.orders[0]?.routeId;
40
+ if (homeRouteId === targetRoute.id) {
41
+ return {
42
+ request: this.request,
43
+ model,
44
+ errors: [
45
+ `Home page "${pageId}" cannot be hidden from the navigation`,
46
+ ],
47
+ };
48
+ }
49
+ // --- GUARD: cannot hide the last remaining visible page ---
50
+ const visibleCount = model.nav.routes.filter((r) => !r.isHidden).length;
51
+ if (!targetRoute.isHidden && visibleCount <= 1) {
52
+ return {
53
+ request: this.request,
54
+ model,
55
+ errors: [
56
+ `Cannot hide "${pageId}" — at least one page must stay visible`,
57
+ ],
58
+ };
59
+ }
60
+ }
61
+ // Set on the NavRoute (drives the nav) and the PageModel (drives header page lists).
62
+ targetRoute.isHidden = isHidden;
63
+ const targetPage = model.pages.find((p) => p.id === pageId);
64
+ if (targetPage) {
65
+ targetPage.isHidden = isHidden;
66
+ }
67
+ // Match the editor's page-navigation store (reorderVisibleAndHiddenRoutes): after any
68
+ // visibility change, float visible pages to the top and sink hidden pages to the bottom
69
+ // of the top-level nav order, stable within each group. Top-level only — mirrors the
70
+ // editor, which does not re-partition nested subRoutes. `model.pages` is then re-synced
71
+ // to the new DFS order (same as MovePageBuilder).
72
+ const routeById = new Map(model.nav.routes.map((r) => [r.id, r]));
73
+ const isOrderHidden = (o) => Boolean(routeById.get(o.routeId)?.isHidden);
74
+ const visibleOrders = model.nav.orders.filter((o) => !isOrderHidden(o));
75
+ const hiddenOrders = model.nav.orders.filter((o) => isOrderHidden(o));
76
+ model.nav.orders = [...visibleOrders, ...hiddenOrders];
77
+ syncPagesWithNavOrders(model);
78
+ this.log({
79
+ message: `Set page "${pageId}" visibility — isHidden: ${isHidden}`,
80
+ });
81
+ return { request: this.request, model };
82
+ }
83
+ }
@@ -7,6 +7,8 @@ import { BannerSectionBuilder, BookingSectionBuilder, AboutSectionBuilder, Conta
7
7
  import { AddPageBuilder } from './builders/add-page-builder';
8
8
  import { RemovePageBuilder } from './builders/remove-page-builder';
9
9
  import { RenamePageBuilder } from './builders/rename-page-builder';
10
+ import { MovePageBuilder } from './builders/move-page-builder';
11
+ import { SetPageVisibilityBuilder } from './builders/set-page-visibility-builder';
10
12
  import { FooterBuilder } from './builders/footer-builder';
11
13
  import { GlobalStylesBuilder } from './builders/global-styles-builder';
12
14
  import { HeaderBuilder } from './builders/header-builder';
@@ -74,6 +76,8 @@ const getBuilderRegistry = () => ({
74
76
  [BUILDER_TYPES.ADD_PAGE]: AddPageBuilder,
75
77
  [BUILDER_TYPES.REMOVE_PAGE]: RemovePageBuilder,
76
78
  [BUILDER_TYPES.RENAME_PAGE]: RenamePageBuilder,
79
+ [BUILDER_TYPES.MOVE_PAGE]: MovePageBuilder,
80
+ [BUILDER_TYPES.SET_PAGE_VISIBILITY]: SetPageVisibilityBuilder,
77
81
  // Footer operations
78
82
  [BUILDER_TYPES.UPDATE_FOOTER]: FooterBuilder,
79
83
  // Global styles operations
@@ -59,6 +59,8 @@ export declare const BUILDER_TYPES: {
59
59
  readonly ADD_PAGE: "add-page";
60
60
  readonly REMOVE_PAGE: "remove-page";
61
61
  readonly RENAME_PAGE: "rename-page";
62
+ readonly MOVE_PAGE: "move-page";
63
+ readonly SET_PAGE_VISIBILITY: "set-page-visibility";
62
64
  readonly UPDATE_FOOTER: "update-footer";
63
65
  readonly UPDATE_GLOBAL_STYLES: "update-global-styles";
64
66
  readonly UPDATE_HEADER: "update-header";
@@ -61,6 +61,8 @@ export const BUILDER_TYPES = {
61
61
  ADD_PAGE: 'add-page',
62
62
  REMOVE_PAGE: 'remove-page',
63
63
  RENAME_PAGE: 'rename-page',
64
+ MOVE_PAGE: 'move-page',
65
+ SET_PAGE_VISIBILITY: 'set-page-visibility',
64
66
  // Footer operations
65
67
  UPDATE_FOOTER: 'update-footer',
66
68
  // Global styles operations
@@ -35,6 +35,8 @@ export declare const AI_ASSISTANT_TOOL_NAMES: {
35
35
  readonly ADD_PAGE: "add_page";
36
36
  readonly REMOVE_PAGE: "remove_page";
37
37
  readonly RENAME_PAGE: "rename_page";
38
+ readonly MOVE_PAGE: "move_page";
39
+ readonly SET_PAGE_VISIBILITY: "set_page_visibility";
38
40
  readonly UPDATE_FOOTER: "update_footer";
39
41
  readonly UPDATE_GLOBAL_STYLES: "tool_update_global_styles";
40
42
  readonly UPDATE_HEADER: "update_header";
@@ -72,7 +74,11 @@ export interface DeleteSectionArguments {
72
74
  }
73
75
  export interface MoveSectionArguments {
74
76
  content_id: string;
75
- position: number;
77
+ /**
78
+ * Relative anchor on the destination page: the content_id to insert this section
79
+ * BEFORE (a page's first content_id moves it to the top). Omit to append to the end.
80
+ */
81
+ move_before_content_id?: string;
76
82
  target_page_id?: string;
77
83
  }
78
84
  export interface AddPageArguments {
@@ -86,8 +92,17 @@ export interface RenamePageArguments {
86
92
  page_id: string;
87
93
  page_name: string;
88
94
  }
89
- export interface ReorderPagesArguments {
90
- page_order: string[];
95
+ export interface MovePageArguments {
96
+ page_id: string;
97
+ /** Destination parent page_id, or null/omitted for top-level. */
98
+ parent_page_id?: string | null;
99
+ /** Sibling anchor page_id to insert before, or "__end__"/omitted to append. */
100
+ move_before_page_id?: string | null;
101
+ }
102
+ export interface SetPageVisibilityArguments {
103
+ page_id: string;
104
+ /** true = hide the page from the navigation; false = show it. */
105
+ is_hidden: boolean;
91
106
  }
92
107
  /**
93
108
  * Arguments for the `undo` tool call.
@@ -99,8 +114,17 @@ export interface ReorderPagesArguments {
99
114
  export interface UndoArguments {
100
115
  }
101
116
  export interface FooterSocialLinkItem {
102
- item_index?: number;
103
- insert_at?: number;
117
+ /**
118
+ * Stable id of the existing link to update or move. Omit to ADD a new link.
119
+ * Mirrors the section-level item_id convention (BP-5603).
120
+ */
121
+ item_id?: string;
122
+ /**
123
+ * Id of an existing link to position this link before (insert for new links,
124
+ * move for existing ones). Use "__end__" for the end of the list. Omit to
125
+ * leave an existing link in place / append a new link.
126
+ */
127
+ move_before_item_id?: string;
104
128
  social_link_icon?: string;
105
129
  url?: string;
106
130
  }
@@ -122,8 +146,8 @@ export interface UpdateFooterArguments {
122
146
  copyright_text_style?: TextStyleArguments;
123
147
  social_links?: FooterSocialLinkItem[];
124
148
  _replace_social_links?: boolean;
125
- _remove_social_link_indices?: number[];
126
- _reorder_social_links?: number[];
149
+ /** Stable ids of social links to remove. Order-independent (replaces index-based _remove_social_link_indices). */
150
+ _remove_social_link_ids?: string[];
127
151
  /** Desktop-only styling (display_style, background_color, icon_color, is_logo_visible, logo sizes). */
128
152
  desktop?: ViewportFooterOverrides;
129
153
  /** Mobile-only styling (display_style, background_color, icon_color, is_logo_visible, logo sizes). */
@@ -41,6 +41,8 @@ export const AI_ASSISTANT_TOOL_NAMES = {
41
41
  ADD_PAGE: 'add_page',
42
42
  REMOVE_PAGE: 'remove_page',
43
43
  RENAME_PAGE: 'rename_page',
44
+ MOVE_PAGE: 'move_page',
45
+ SET_PAGE_VISIBILITY: 'set_page_visibility',
44
46
  // Footer operation tools
45
47
  UPDATE_FOOTER: 'update_footer',
46
48
  // Global styles operation tools