@designcrowd/library.brand-page 5.18.2 → 5.18.3-actionable-buttons-2

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 (78) hide show
  1. package/lib/src/{CreatedOnBrandCrowd-de-de-DB2oJ5vl.js → CreatedOnBrandCrowd-de-de-HPO35Atc.js} +1 -1
  2. package/lib/src/{CreatedOnBrandCrowd-en-us-fbk0Gf2G.js → CreatedOnBrandCrowd-en-us-Cl4aaalq.js} +1 -1
  3. package/lib/src/{CreatedOnBrandCrowd-es-es-Ch4Q9__I.js → CreatedOnBrandCrowd-es-es-B_2laZxX.js} +1 -1
  4. package/lib/src/{CreatedOnBrandCrowd-fr-fr-DaHJ6OMH.js → CreatedOnBrandCrowd-fr-ca-BTauzkB5.js} +1 -1
  5. package/lib/src/{CreatedOnBrandCrowd-fr-ca-DaHJ6OMH.js → CreatedOnBrandCrowd-fr-fr-BTauzkB5.js} +1 -1
  6. package/lib/src/{CreatedOnBrandCrowd-pt-pt-BgF8UCqy.js → CreatedOnBrandCrowd-pt-br-B0V4UZv3.js} +1 -1
  7. package/lib/src/{CreatedOnBrandCrowd-pt-br-BgF8UCqy.js → CreatedOnBrandCrowd-pt-pt-B0V4UZv3.js} +1 -1
  8. package/lib/src/{CreatedOnDcom-de-de-j3rcGaGp.js → CreatedOnDcom-de-de-DvX7MlWL.js} +1 -1
  9. package/lib/src/{CreatedOnDcom-en-us-Cx4eQPRU.js → CreatedOnDcom-en-us-BnXBQ4jL.js} +1 -1
  10. package/lib/src/{CreatedOnDcom-es-es-D-1APCL9.js → CreatedOnDcom-es-es-lKzARICU.js} +1 -1
  11. package/lib/src/{CreatedOnDcom-fr-ca-DAHdBw0D.js → CreatedOnDcom-fr-ca-BXBuLMp3.js} +1 -1
  12. package/lib/src/{CreatedOnDcom-fr-fr-BYAw2kXr.js → CreatedOnDcom-fr-fr-wUZo-ppX.js} +1 -1
  13. package/lib/src/{CreatedOnDcom-pt-br-CdSknjZg.js → CreatedOnDcom-pt-br-Dcpf3PTV.js} +1 -1
  14. package/lib/src/{CreatedOnDcom-pt-pt-DzyZ2j80.js → CreatedOnDcom-pt-pt-BUN3ynDi.js} +1 -1
  15. package/lib/src/brand-page-maker/composables/ai-chat/types.d.ts +3 -0
  16. package/lib/src/brand-page-maker/composables/ai-chat/ui-actions.d.ts +29 -0
  17. package/lib/src/brand-page-maker/composables/ai-chat/ui-actions.js +32 -0
  18. package/lib/src/brand-page-maker/composables/ai-chat/useAiWebsiteBuilderChat.js +201 -172
  19. package/lib/src/brand-page-maker/composables/useEventTracker.d.ts +1 -0
  20. package/lib/src/brand-page-maker/composables/useEventTracker.js +1 -0
  21. package/lib/src/{bundled-translations-CnareLWC.js → bundled-translations-B5Fkt3qd.js} +1 -1
  22. package/lib/src/{bundled-translations.de-DE-2Ys7epLO.js → bundled-translations.de-DE-DpdVePM9.js} +1 -1
  23. package/lib/src/bundled-translations.es-ES-BsIcrzZF.js +175 -0
  24. package/lib/src/bundled-translations.fr-CA-G1XCp2Qh.js +175 -0
  25. package/lib/src/bundled-translations.fr-FR-BdwLXOnB.js +175 -0
  26. package/lib/src/bundled-translations.pt-BR-Bvn5ePe7.js +175 -0
  27. package/lib/src/bundled-translations.pt-PT-CDtkb2k1.js +175 -0
  28. package/lib/src/bundles/bundled-translations.de-DE.json +2 -0
  29. package/lib/src/bundles/bundled-translations.es-ES.json +2 -0
  30. package/lib/src/bundles/bundled-translations.fr-CA.json +2 -0
  31. package/lib/src/bundles/bundled-translations.fr-FR.json +2 -0
  32. package/lib/src/bundles/bundled-translations.json +2 -0
  33. package/lib/src/bundles/bundled-translations.pt-BR.json +2 -0
  34. package/lib/src/bundles/bundled-translations.pt-PT.json +2 -0
  35. package/lib/src/css/library.brand-page-brandCrowd.min.css +102 -19
  36. package/lib/src/css/library.brand-page-brandPage.min.css +102 -19
  37. package/lib/src/css/library.brand-page-designCom.min.css +102 -19
  38. package/lib/src/css/library.brand-page-preview.min.css +102 -19
  39. package/lib/src/index.mjs +1 -1
  40. package/lib/src/{lib-mcvVokSM.js → lib-BP3rUcsW.js} +17328 -17189
  41. package/lib/src/plugins/google-maps-plugin/google-maps-plugin.d.ts +14 -2
  42. package/lib/src/plugins/google-maps-plugin/google-maps-plugin.js +43 -13
  43. package/package.json +2 -2
  44. package/src/brand-page-maker/components/ai-text-edit/AiActionButtons.vue +34 -7
  45. package/src/brand-page-maker/components/ai-text-edit/AiChatbox.vue +3 -0
  46. package/src/brand-page-maker/components/ai-text-edit/TextBubble.vue +7 -1
  47. package/src/brand-page-maker/components/ai-website-builder/AiWebsiteBuilder.vue +21 -1
  48. package/src/brand-page-maker/components/brandpage-settings/BrandPageSettings.vue +8 -0
  49. package/src/brand-page-maker/composables/ai-chat/types.ts +6 -0
  50. package/src/brand-page-maker/composables/ai-chat/ui-actions.test.ts +80 -0
  51. package/src/brand-page-maker/composables/ai-chat/ui-actions.ts +58 -0
  52. package/src/brand-page-maker/composables/ai-chat/useAiWebsiteBuilderChat.ts +305 -256
  53. package/src/brand-page-maker/composables/useEventTracker.ts +1 -0
  54. package/src/brand-page-maker/constants/i18n/constants.de-DE.json +2 -0
  55. package/src/brand-page-maker/constants/i18n/constants.es-ES.json +2 -0
  56. package/src/brand-page-maker/constants/i18n/constants.fr-CA.json +2 -0
  57. package/src/brand-page-maker/constants/i18n/constants.fr-FR.json +2 -0
  58. package/src/brand-page-maker/constants/i18n/constants.json +2 -0
  59. package/src/brand-page-maker/constants/i18n/constants.pt-BR.json +2 -0
  60. package/src/brand-page-maker/constants/i18n/constants.pt-PT.json +2 -0
  61. package/src/bundles/bundled-translations.de-DE.json +2 -0
  62. package/src/bundles/bundled-translations.es-ES.json +2 -0
  63. package/src/bundles/bundled-translations.fr-CA.json +2 -0
  64. package/src/bundles/bundled-translations.fr-FR.json +2 -0
  65. package/src/bundles/bundled-translations.json +2 -0
  66. package/src/bundles/bundled-translations.pt-BR.json +2 -0
  67. package/src/bundles/bundled-translations.pt-PT.json +2 -0
  68. package/src/plugins/google-maps-plugin/google-maps-plugin.ts +58 -18
  69. package/src/shared/content-settings/components/google-address-field/GoogleAddressField.vue +148 -15
  70. package/src/shared/font-family-selector/FontPicker.vue +91 -4
  71. package/lib/src/bundled-translations.es-ES-Cjp-G8rT.js +0 -175
  72. package/lib/src/bundled-translations.fr-CA-CsaW4Oae.js +0 -175
  73. package/lib/src/bundled-translations.fr-FR-DIT2dd8J.js +0 -175
  74. package/lib/src/bundled-translations.pt-BR-BamoBpDW.js +0 -175
  75. package/lib/src/bundled-translations.pt-PT-C9A4hY_e.js +0 -175
  76. package/lib/src/shared/content-settings/components/google-address-field/GoogleAddressField.mixin.d.ts +0 -54
  77. package/lib/src/shared/content-settings/components/google-address-field/GoogleAddressField.mixin.js +0 -67
  78. package/src/shared/content-settings/components/google-address-field/GoogleAddressField.mixin.ts +0 -84
@@ -12,6 +12,8 @@
12
12
  "modernDisplayName": "modern",
13
13
  "uniqueDisplayName": "unique",
14
14
  "allFontStylesDisplayName": "All font styles",
15
+ "searchFontsPlaceholder": "Search fonts",
16
+ "noFontsFoundMessage": "No fonts found",
15
17
  "settingsSaveSuccessMessage": "Settings saved successfully"
16
18
  }
17
19
  }
@@ -12,6 +12,8 @@
12
12
  "modernDisplayName": "moderno",
13
13
  "uniqueDisplayName": "único",
14
14
  "allFontStylesDisplayName": "Todos os estilos de fonte",
15
+ "searchFontsPlaceholder": "Pesquisar fontes",
16
+ "noFontsFoundMessage": "Não foram encontradas fontes",
15
17
  "settingsSaveSuccessMessage": "Definições salvas com sucesso"
16
18
  }
17
19
  }
@@ -12,6 +12,8 @@
12
12
  "modernDisplayName": "moderno",
13
13
  "uniqueDisplayName": "único",
14
14
  "allFontStylesDisplayName": "Todos os estilos de letra",
15
+ "searchFontsPlaceholder": "Procurar tipos de letra",
16
+ "noFontsFoundMessage": "Nenhum tipo de letra encontrado",
15
17
  "settingsSaveSuccessMessage": "Definições guardadas com sucesso"
16
18
  }
17
19
  }
@@ -1230,6 +1230,8 @@
1230
1230
  "modernDisplayName": "modern",
1231
1231
  "uniqueDisplayName": "einzigartig",
1232
1232
  "allFontStylesDisplayName": "Alle Schriftstile",
1233
+ "searchFontsPlaceholder": "Schriftarten durchsuchen",
1234
+ "noFontsFoundMessage": "Keine Schriftarten gefunden",
1233
1235
  "settingsSaveSuccessMessage": "Einstellungen wurden erfolgreich gespeichert"
1234
1236
  },
1235
1237
  "converter": {
@@ -1230,6 +1230,8 @@
1230
1230
  "modernDisplayName": "moderna",
1231
1231
  "uniqueDisplayName": "única",
1232
1232
  "allFontStylesDisplayName": "Todos los estilos de fuente",
1233
+ "searchFontsPlaceholder": "Busca fuentes",
1234
+ "noFontsFoundMessage": "No se encontraron fuentes",
1233
1235
  "settingsSaveSuccessMessage": "La configuración se ha guardado correctamente"
1234
1236
  },
1235
1237
  "converter": {
@@ -1230,6 +1230,8 @@
1230
1230
  "modernDisplayName": "Moderne",
1231
1231
  "uniqueDisplayName": "Unique",
1232
1232
  "allFontStylesDisplayName": "Tous les styles de police",
1233
+ "searchFontsPlaceholder": "Rechercher des polices",
1234
+ "noFontsFoundMessage": "Aucune police trouvée",
1233
1235
  "settingsSaveSuccessMessage": "Paramètres enregistrés avec succès"
1234
1236
  },
1235
1237
  "converter": {
@@ -1230,6 +1230,8 @@
1230
1230
  "modernDisplayName": "Moderne",
1231
1231
  "uniqueDisplayName": "Unique",
1232
1232
  "allFontStylesDisplayName": "Tous les styles de police",
1233
+ "searchFontsPlaceholder": "Rechercher des polices",
1234
+ "noFontsFoundMessage": "Aucune police trouvée",
1233
1235
  "settingsSaveSuccessMessage": "Paramètres enregistrés avec succès"
1234
1236
  },
1235
1237
  "converter": {
@@ -1230,6 +1230,8 @@
1230
1230
  "modernDisplayName": "modern",
1231
1231
  "uniqueDisplayName": "unique",
1232
1232
  "allFontStylesDisplayName": "All font styles",
1233
+ "searchFontsPlaceholder": "Search fonts",
1234
+ "noFontsFoundMessage": "No fonts found",
1233
1235
  "settingsSaveSuccessMessage": "Settings saved successfully"
1234
1236
  },
1235
1237
  "converter": {
@@ -1230,6 +1230,8 @@
1230
1230
  "modernDisplayName": "moderno",
1231
1231
  "uniqueDisplayName": "único",
1232
1232
  "allFontStylesDisplayName": "Todos os estilos de fonte",
1233
+ "searchFontsPlaceholder": "Pesquisar fontes",
1234
+ "noFontsFoundMessage": "Não foram encontradas fontes",
1233
1235
  "settingsSaveSuccessMessage": "Definições salvas com sucesso"
1234
1236
  },
1235
1237
  "converter": {
@@ -1230,6 +1230,8 @@
1230
1230
  "modernDisplayName": "moderno",
1231
1231
  "uniqueDisplayName": "único",
1232
1232
  "allFontStylesDisplayName": "Todos os estilos de letra",
1233
+ "searchFontsPlaceholder": "Procurar tipos de letra",
1234
+ "noFontsFoundMessage": "Nenhum tipo de letra encontrado",
1233
1235
  "settingsSaveSuccessMessage": "Definições guardadas com sucesso"
1234
1236
  },
1235
1237
  "converter": {
@@ -10,7 +10,6 @@ const DELAY_MS = 100;
10
10
  const MAP_MODE_PLACE = 'place';
11
11
  const SCRIPT_TAG_NAME = 'SCRIPT';
12
12
 
13
- export const PLACE_CHANGED = 'place_changed';
14
13
  export const PLACE_ID_QUERY_PREFIX = 'place_id:';
15
14
 
16
15
  export interface GoogleMapsPluginOptions {
@@ -24,6 +23,11 @@ export interface GetEmbedGoogleMapsOptions {
24
23
  readonly locale?: string;
25
24
  }
26
25
 
26
+ export interface AddressDetails {
27
+ readonly formattedAddress: string;
28
+ readonly placeId: string;
29
+ }
30
+
27
31
  export class GoogleMapsPlugin {
28
32
  private initialised = false;
29
33
  private initialising = false;
@@ -51,23 +55,30 @@ export class GoogleMapsPlugin {
51
55
 
52
56
  const url = this.getGoogleScriptUrl();
53
57
 
54
- let googleMapsScript: HTMLScriptElement | undefined;
55
-
56
- googleMapsScript = Array.from(document.head.children).find(
58
+ const existingScript = Array.from(document.head.children).find(
57
59
  (c): c is HTMLScriptElement =>
58
60
  c.tagName.toUpperCase() === SCRIPT_TAG_NAME &&
59
61
  c.hasAttribute('src') &&
60
62
  c.getAttribute('src') === url,
61
63
  );
62
64
 
63
- if (googleMapsScript) {
64
- return;
65
+ if (!existingScript) {
66
+ document.head.appendChild(this.createGoogleMapsScriptElement(url));
65
67
  }
66
68
 
67
- googleMapsScript = this.createGoogleMapsScriptElement(url);
68
- document.head.appendChild(googleMapsScript);
69
-
70
- while (typeof google !== 'object') {
69
+ // Wait for the specific class we use. The `places` library loads eagerly
70
+ // via the `libraries=places` script param, so the new Places classes are
71
+ // on the `google.maps.places` namespace once ready. We avoid
72
+ // `importLibrary` (absent in some hosts that already loaded Maps).
73
+ //
74
+ // `typeof google === 'undefined'` must be checked first and on its own:
75
+ // `typeof` only suppresses ReferenceError for a bare identifier, so
76
+ // touching `google.maps` while `google` is still undeclared (before the
77
+ // script executes) throws "google is not defined".
78
+ while (
79
+ typeof google === 'undefined' ||
80
+ typeof google.maps?.places?.PlaceAutocompleteElement !== 'function'
81
+ ) {
71
82
  await delayInMS(DELAY_MS);
72
83
  }
73
84
 
@@ -77,23 +88,52 @@ export class GoogleMapsPlugin {
77
88
  }
78
89
  }
79
90
 
80
- async createAutoCompleteWidget(
81
- input: HTMLInputElement,
82
- options?: google.maps.places.AutocompleteOptions,
83
- ): Promise<google.maps.places.Autocomplete> {
91
+ private async getPlacesLibrary(): Promise<google.maps.PlacesLibrary> {
84
92
  await this.initialise();
85
93
 
86
94
  while (!this.initialised) {
87
95
  await delayInMS(DELAY_MS);
88
96
  }
89
97
 
90
- const autoComplete = new google.maps.places.Autocomplete(input, options);
98
+ return google.maps.places as unknown as google.maps.PlacesLibrary;
99
+ }
100
+
101
+ /**
102
+ * Creates Google's official Places Autocomplete widget (new Places API).
103
+ * It renders its own input + dropdown with built-in icons, formatting,
104
+ * keyboard/a11y support and the required "Powered by Google" attribution,
105
+ * and manages session tokens internally. Listen for `gmp-select` to read the
106
+ * chosen prediction, then pass it to {@link fetchAddressDetails}.
107
+ */
108
+ async createPlaceAutocompleteElement(
109
+ options?: google.maps.places.PlaceAutocompleteElementOptions,
110
+ ): Promise<google.maps.places.PlaceAutocompleteElement> {
111
+ const { PlaceAutocompleteElement } = await this.getPlacesLibrary();
112
+
113
+ return new PlaceAutocompleteElement({
114
+ requestedLanguage: this._options.locale,
115
+ ...options,
116
+ });
117
+ }
118
+
119
+ async fetchAddressDetails(
120
+ prediction: google.maps.places.PlacePrediction,
121
+ ): Promise<AddressDetails | undefined> {
122
+ const place = prediction.toPlace();
91
123
 
92
124
  // NOTE: additional fields may incur a charge, check before adding a new field
93
125
  // See https://developers.google.com/maps/billing-and-pricing/pricing#basic-data for more information
94
- autoComplete.setFields(['formatted_address', 'place_id']);
126
+ // The widget's session token is included automatically on this first fetch.
127
+ await place.fetchFields({ fields: ['formattedAddress', 'id'] });
128
+
129
+ if (!place.formattedAddress || !place.id) {
130
+ return undefined;
131
+ }
95
132
 
96
- return autoComplete;
133
+ return {
134
+ formattedAddress: place.formattedAddress,
135
+ placeId: place.id,
136
+ };
97
137
  }
98
138
 
99
139
  private createGoogleMapsScriptElement(url: string): HTMLScriptElement {
@@ -109,7 +149,7 @@ export class GoogleMapsPlugin {
109
149
  }
110
150
 
111
151
  private getGoogleScriptUrl(): string {
112
- const baseUrl = `https://maps.googleapis.com/maps/api/js?key=${this._options.apiKey}&libraries=places`;
152
+ const baseUrl = `https://maps.googleapis.com/maps/api/js?key=${this._options.apiKey}&libraries=places&loading=async`;
113
153
 
114
154
  return this._options.locale
115
155
  ? `${baseUrl}&language=${this._options.locale}`
@@ -1,24 +1,157 @@
1
1
  <template>
2
2
  <div data-component="google-address-field" class="tw-w-full">
3
- <input
4
- ref="pacinput"
5
- :placeholder="googleAddressFieldTr('addressPlaceholder')"
6
- class="tw-bg-white tw-text-grayscale-600 tw-box-border tw-w-full tw-text-base tw-px-4 tw-border tw-border-solid focus:tw-outline-none tw-transition-colors tw-duration-300 tw-ease-out tw-shadow-inner tw-border-grayscale-500 focus:tw-border-grayscale-600 tw-rounded tw-py-2"
7
- />
8
- <p v-if="isErrorVisible" class="tw-text-error-500 tw-text-sm tw-mt-2">
3
+ <div ref="containerEl" data-test-address-autocomplete />
4
+ <p
5
+ v-if="errorMessage && !isValid"
6
+ class="tw-text-error-500 tw-text-sm tw-mt-2"
7
+ >
9
8
  {{ errorMessage }}
10
9
  </p>
11
10
  </div>
12
11
  </template>
13
- <script>
14
- import AddressFieldMixin from './GoogleAddressField.mixin.ts';
15
- import { googleAddressFieldTr } from '@src/composables/useTranslate';
16
-
17
- export default {
18
- components: {},
19
- mixins: [AddressFieldMixin],
20
- setup() {
21
- return { googleAddressFieldTr };
12
+ <script setup lang="ts">
13
+ import {
14
+ computed,
15
+ getCurrentInstance,
16
+ onBeforeUnmount,
17
+ onMounted,
18
+ ref,
19
+ } from 'vue';
20
+ import type { PropType } from 'vue';
21
+ import type { Address } from '@brand-page';
22
+ import type { ContentItemFieldSettings } from '../../models';
23
+ import {
24
+ getCurrentLocale,
25
+ googleAddressFieldTr,
26
+ } from '@src/composables/useTranslate';
27
+
28
+ const props = defineProps({
29
+ id: {
30
+ type: String,
31
+ required: true,
32
+ },
33
+ name: {
34
+ type: String,
35
+ required: true,
36
+ },
37
+ value: {
38
+ type: Object as PropType<Address>,
39
+ required: false,
40
+ default: undefined,
22
41
  },
42
+ settings: {
43
+ type: Object as PropType<ContentItemFieldSettings>,
44
+ required: true,
45
+ },
46
+ });
47
+
48
+ const emit = defineEmits<{
49
+ 'update:modelValue': [
50
+ payload: { id: string; name: string; type: 'map'; value: Address },
51
+ ];
52
+ }>();
53
+
54
+ const gmap = getCurrentInstance()!.appContext.config.globalProperties.$gmap;
55
+
56
+ const containerEl = ref<HTMLElement>();
57
+ const errorMessage = computed(() => props.settings?.errorMessage);
58
+ const isValid = computed(() => props.settings?.isValid);
59
+
60
+ let element: google.maps.places.PlaceAutocompleteElement | undefined;
61
+
62
+ const handleSelect = async (
63
+ prediction: google.maps.places.PlacePrediction,
64
+ ): Promise<void> => {
65
+ const details = await gmap.fetchAddressDetails(prediction);
66
+
67
+ if (!details) {
68
+ return;
69
+ }
70
+
71
+ emit('update:modelValue', {
72
+ id: props.id,
73
+ name: props.name,
74
+ type: 'map',
75
+ value: details,
76
+ });
23
77
  };
78
+
79
+ // Typed as Event so the same reference is removable; the runtime payload is a
80
+ // PlacePredictionSelectEvent.
81
+ const onSelect = (event: Event): void => {
82
+ const { placePrediction } =
83
+ event as google.maps.places.PlacePredictionSelectEvent;
84
+
85
+ void handleSelect(placePrediction);
86
+ };
87
+
88
+ // Same classes as the shared content-settings text input, applied to the
89
+ // widget's host element so it matches our other fields across all brand themes
90
+ // (the grayscale tokens resolve per theme — no hardcoded colours). The widget's
91
+ // own `:host` border is lower priority than these document-level utilities.
92
+ const HOST_INPUT_CLASSES = [
93
+ 'tw-block',
94
+ 'tw-w-full',
95
+ 'tw-box-border',
96
+ 'tw-bg-white',
97
+ 'tw-text-base',
98
+ 'tw-text-grayscale-600',
99
+ 'tw-border',
100
+ 'tw-border-solid',
101
+ 'tw-border-grayscale-500',
102
+ 'focus-within:tw-border-grayscale-600',
103
+ 'tw-rounded',
104
+ 'tw-px-4',
105
+ 'tw-py-2',
106
+ 'tw-shadow-inner',
107
+ 'tw-transition-colors',
108
+ 'tw-duration-300',
109
+ 'tw-ease-out',
110
+ ];
111
+
112
+ onMounted(async () => {
113
+ element = await gmap.createPlaceAutocompleteElement({
114
+ value: props.value?.formattedAddress ?? '',
115
+ placeholder: googleAddressFieldTr('addressPlaceholder'),
116
+ requestedLanguage: getCurrentLocale(),
117
+ // Drop the widget's built-in search icon and clear button for a plain
118
+ // input that matches our design system.
119
+ noInputIcon: true,
120
+ noClearButton: true,
121
+ });
122
+
123
+ element.classList.add(...HOST_INPUT_CLASSES);
124
+ element.addEventListener('gmp-select', onSelect);
125
+ containerEl.value?.appendChild(element);
126
+ });
127
+
128
+ onBeforeUnmount(() => {
129
+ element?.removeEventListener('gmp-select', onSelect);
130
+ });
24
131
  </script>
132
+ <style>
133
+ /* The widget renders its own input inside a closed shadow DOM. We style the box
134
+ on the host via Tailwind classes (see HOST_INPUT_CLASSES) and normalise the
135
+ exposed parts here:
136
+ - force the light theme (the widget otherwise follows the OS color-scheme and
137
+ renders dark inside our always-light panel);
138
+ - make the inner input inherit the host's font/colour and drop its own
139
+ padding (the host provides px-4/py-2);
140
+ - hide the widget's focus-ring so focus is shown by the host border darkening,
141
+ matching our other inputs. */
142
+ [data-component='google-address-field'] gmp-place-autocomplete {
143
+ color-scheme: light;
144
+ }
145
+
146
+ [data-component='google-address-field'] gmp-place-autocomplete::part(input) {
147
+ padding: 0;
148
+ background: transparent;
149
+ color: inherit;
150
+ font: inherit;
151
+ }
152
+
153
+ [data-component='google-address-field']
154
+ gmp-place-autocomplete::part(focus-ring) {
155
+ display: none;
156
+ }
157
+ </style>
@@ -27,6 +27,33 @@
27
27
  }"
28
28
  >
29
29
  <div :class="{ 'tw-px-4 tw-py-3': !panelOnly }">
30
+ <div
31
+ class="tw-mb-3 tw-flex tw-h-10 tw-items-center tw-gap-2 tw-rounded-lg tw-border tw-border-solid tw-border-grayscale-500 tw-bg-white tw-px-3"
32
+ >
33
+ <Icon
34
+ name="search"
35
+ size="sm"
36
+ class="tw-shrink-0 tw-text-grayscale-600"
37
+ />
38
+ <input
39
+ v-model="searchQuery"
40
+ type="text"
41
+ :placeholder="SEARCH_FONTS_PLACEHOLDER"
42
+ :aria-label="SEARCH_FONTS_PLACEHOLDER"
43
+ data-test-tag="font-search-input"
44
+ class="tw-min-w-0 tw-flex-auto tw-border-0 tw-bg-transparent tw-p-0 tw-text-base tw-text-secondary-500 tw-outline-none placeholder:tw-text-grayscale-600 placeholder:tw-opacity-50"
45
+ @focus="closeCategoryMenu"
46
+ @keydown.enter.prevent.stop="onSearchEnter"
47
+ @keydown.esc="onSearchEscape"
48
+ />
49
+ <Icon
50
+ v-show="searchQuery"
51
+ name="close"
52
+ size="sm"
53
+ class="tw-shrink-0 tw-cursor-pointer tw-text-grayscale-600"
54
+ @click="searchQuery = ''"
55
+ />
56
+ </div>
30
57
  <Dropdown
31
58
  ref="categoryDropdownRef"
32
59
  class="tw-w-full tw-pb-3"
@@ -50,9 +77,16 @@
50
77
  ref="fontListRef"
51
78
  class="font-list tw-max-h-[340px] tw-overflow-y-auto"
52
79
  >
80
+ <div
81
+ v-if="!filteredFontOptions.length"
82
+ class="tw-py-3 tw-text-center tw-text-sm tw-text-secondary-200"
83
+ >
84
+ {{ NO_FONTS_FOUND_MESSAGE }}
85
+ </div>
53
86
  <DropdownItem
54
87
  v-for="option in filteredFontOptions"
55
88
  :key="option.key"
89
+ class="font-list-item"
56
90
  :data-test-font="option.key"
57
91
  @click="onFontClick(option)"
58
92
  >
@@ -116,8 +150,11 @@ const emit = defineEmits<{
116
150
  const categoryDropdownRef = ref<InstanceType<typeof Dropdown> | null>(null);
117
151
  const fontListRef = ref<HTMLElement | null>(null);
118
152
  const isPanelOpen = ref(false);
153
+ const searchQuery = ref('');
119
154
 
120
155
  const ALL_FONT_STYLES_LABEL = constantsTr('allFontStylesDisplayName');
156
+ const SEARCH_FONTS_PLACEHOLDER = constantsTr('searchFontsPlaceholder');
157
+ const NO_FONTS_FOUND_MESSAGE = constantsTr('noFontsFoundMessage');
121
158
 
122
159
  const fontCategories = computed(() => Object.values(FONT_FAMILY_CATEGORIES()));
123
160
 
@@ -147,11 +184,26 @@ const sortedFontOptions = computed(() =>
147
184
  ),
148
185
  );
149
186
 
187
+ // Normalize each font name once (query-independent) so the per-keystroke
188
+ // filter only does a cheap substring check.
189
+ const searchableFontOptions = computed(() =>
190
+ sortedFontOptions.value.map((option) => ({
191
+ option,
192
+ searchName: stripQuotes(FONT_FAMILY_CONFIGS[option.key].name).toLowerCase(),
193
+ })),
194
+ );
195
+
150
196
  const filteredFontOptions = computed(() => {
151
- if (!selectedCategory.value) return sortedFontOptions.value;
152
- return sortedFontOptions.value.filter((option) =>
153
- option.categories.includes(selectedCategory.value!),
154
- );
197
+ const query = searchQuery.value.trim().toLowerCase();
198
+ return searchableFontOptions.value
199
+ .filter(({ option, searchName }) => {
200
+ const matchesCategory =
201
+ !selectedCategory.value ||
202
+ option.categories.includes(selectedCategory.value);
203
+ const matchesQuery = !query || searchName.includes(query);
204
+ return matchesCategory && matchesQuery;
205
+ })
206
+ .map(({ option }) => option);
155
207
  });
156
208
 
157
209
  function scrollToSelectedFont(): void {
@@ -174,6 +226,7 @@ function scrollToSelectedFont(): void {
174
226
  watch(isPanelOpen, (open) => {
175
227
  if (open) {
176
228
  selectedCategory.value = null;
229
+ searchQuery.value = '';
177
230
  scrollToSelectedFont();
178
231
  }
179
232
  });
@@ -183,6 +236,7 @@ watch(fontListRef, (list, _old, onCleanup) => {
183
236
  const observer = new IntersectionObserver((entries) => {
184
237
  if (entries[0].isIntersecting) {
185
238
  selectedCategory.value = null;
239
+ searchQuery.value = '';
186
240
  scrollToSelectedFont();
187
241
  }
188
242
  });
@@ -204,11 +258,32 @@ function onSelectCategory(category: FontFamilyCategory | null): void {
204
258
  scrollToSelectedFont();
205
259
  }
206
260
 
261
+ // Close the category list when the user engages the search field (BP-5732).
262
+ function closeCategoryMenu(): void {
263
+ categoryDropdownRef.value?.hideMenu();
264
+ }
265
+
207
266
  function onFontClick(option: StyleTextFontFamilyOption): void {
208
267
  closePanel();
209
268
  emit('on-select', option);
210
269
  }
211
270
 
271
+ // Enter selects the first match (same path as clicking it).
272
+ function onSearchEnter(): void {
273
+ const [firstOption] = filteredFontOptions.value;
274
+ if (firstOption) onFontClick(firstOption);
275
+ }
276
+
277
+ // Escape clears the query first; a second Escape closes the panel
278
+ // (sidebar mode — the RTE dropdown handles its own dismissal).
279
+ function onSearchEscape(): void {
280
+ if (searchQuery.value) {
281
+ searchQuery.value = '';
282
+ } else {
283
+ closePanel();
284
+ }
285
+ }
286
+
212
287
  defineExpose({ hideMenu: closePanel });
213
288
  </script>
214
289
  <style scoped>
@@ -220,4 +295,16 @@ defineExpose({ hideMenu: closePanel });
220
295
  .font-list::-webkit-scrollbar {
221
296
  display: none; /* Chrome, Safari, Opera */
222
297
  }
298
+
299
+ /*
300
+ * Skip layout + paint for off-screen rows. Each row renders an inline-SVG
301
+ * preview; with ~130 rows this defers the bulk of the open-time cost to
302
+ * only the visible window. Rows are uniform height, so the intrinsic-size
303
+ * estimate keeps the scrollbar and scrollToSelectedFont accurate. Nodes
304
+ * stay in the DOM, so find-in-page / a11y / selectors are unaffected.
305
+ */
306
+ .font-list-item {
307
+ content-visibility: auto;
308
+ contain-intrinsic-size: auto 36px;
309
+ }
223
310
  </style>