@salesforcedevs/dx-components 1.28.7-alpha.9 → 1.29.0-ssgalpha2

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.
@@ -48,6 +48,10 @@
48
48
  <dx-sidebar-search
49
49
  onchange={onSearchChange}
50
50
  onloading={onSearchLoading}
51
+ coveo-organization-id={coveoOrganizationId}
52
+ coveo-public-access-token={coveoPublicAccessToken}
53
+ coveo-search-hub={coveoSearchHub}
54
+ coveo-advanced-query-config={coveoAdvancedQueryConfig}
51
55
  ></dx-sidebar-search>
52
56
  </div>
53
57
  <slot name="version-picker"></slot>
@@ -1,18 +1,18 @@
1
1
  import cx from "classnames";
2
2
  import { api, track } from "lwc";
3
3
  import { TreeNode, SidebarSearchResult } from "typings/custom";
4
+ import { getSidebarSearchParams } from "dxUtils/coveo";
4
5
  import { toJson } from "dxUtils/normalizers";
5
6
  import SidebarSearch from "dx/sidebarSearch";
6
7
  import { SidebarBase } from "dxBaseElements/sidebarBase";
7
8
 
8
9
  const MOBILE_SIZE_MATCH = "768px";
9
10
 
10
- const getSearchQueryParam = (): string | null =>
11
- typeof window !== "undefined"
12
- ? new URLSearchParams(window.location.search).get("q")
13
- : null;
14
-
15
11
  export default class Sidebar extends SidebarBase {
12
+ @api coveoOrganizationId!: string;
13
+ @api coveoPublicAccessToken!: string;
14
+ @api coveoSearchHub!: string;
15
+ @api coveoAdvancedQueryConfig!: string;
16
16
  @api header: string = "";
17
17
 
18
18
  @api
@@ -130,7 +130,7 @@ export default class Sidebar extends SidebarBase {
130
130
  constructor() {
131
131
  super();
132
132
 
133
- if (getSearchQueryParam()) {
133
+ if (getSidebarSearchParams()) {
134
134
  this.isSearchLoading = true;
135
135
  this.scrollToSelectedSearchResult = true;
136
136
  }
@@ -175,7 +175,7 @@ export default class Sidebar extends SidebarBase {
175
175
  this.isSearchLoading = e.detail;
176
176
  }
177
177
 
178
- private onSidebarSearchContentScroll(scrollEvent: Event) {
178
+ private onSidebarSearchContentScroll(scrollEvent) {
179
179
  if (this.isSearchLoading) {
180
180
  return;
181
181
  }
@@ -1,27 +1,75 @@
1
1
  import { LightningElement, api } from "lwc";
2
2
  import debounce from "debounce";
3
3
  import {
4
- type DataCloudSearchResultItem,
5
- fetchSearch
6
- } from "dxUtils/dataCloudSearch";
4
+ Result,
5
+ ResultList,
6
+ Unsubscribe,
7
+ ResultsPerPage,
8
+ SearchBox,
9
+ buildInteractiveResult,
10
+ buildResultList,
11
+ buildResultsPerPage,
12
+ buildSearchBox,
13
+ loadAdvancedSearchQueryActions,
14
+ SearchEngine,
15
+ SearchAppState,
16
+ loadQueryActions
17
+ } from "@coveo/headless";
18
+ import { buildSearchEngine, getSidebarSearchParams } from "dxUtils/coveo";
7
19
  import { RecentSearches } from "dxUtils/recentSearches";
20
+ import { toJson } from "dxUtils/normalizers";
8
21
  import {
9
22
  Option,
10
23
  PopoverRequestCloseType,
11
24
  SidebarSearchResult
12
25
  } from "typings/custom";
13
26
 
27
+ const RESULT_CLICK_LOCAL_STORAGE_KEY = "dx-sidebar-search-result-click";
28
+ export const SESSION_KEY = "dx-sidebar-search-state";
29
+
30
+ const DEFAULT_RESULT_COUNT = 20;
14
31
  const SEARCH_DEBOUNCE_DELAY = 1200;
15
32
 
16
33
  const UserRecentSearches = new RecentSearches();
17
34
 
18
- const getSearchQueryParam = (): string =>
19
- new URLSearchParams(window.location.search).get("q") ?? "";
35
+ const normalizeCoveoAdvancedQueryValue = (version: string): string =>
36
+ version?.split(".").join("\\.");
20
37
 
21
38
  export default class SidebarSearch extends LightningElement {
39
+ @api coveoOrganizationId!: string;
40
+ @api coveoPublicAccessToken!: string;
41
+ @api coveoSearchHub!: string;
42
+ @api
43
+ get coveoAdvancedQueryConfig(): { [key: string]: any } {
44
+ return this._coveoAdvancedQueryConfig;
45
+ }
46
+ set coveoAdvancedQueryConfig(value) {
47
+ const jsonValue = toJson(value);
48
+ const hasChanged =
49
+ this._coveoAdvancedQueryConfig &&
50
+ Object.entries(jsonValue).some(
51
+ ([key, val]: [string, any]) =>
52
+ this._coveoAdvancedQueryConfig[key] !== val
53
+ );
54
+ this._coveoAdvancedQueryConfig = jsonValue;
55
+
56
+ if (hasChanged && this.engine && this.value !== "") {
57
+ this.dispatchOnLoading(true);
58
+ }
59
+
60
+ if (hasChanged && this.engine) {
61
+ // set advanced filters needs access to the latest coveoAdvancedQueryConfig
62
+ this.setAdvancedFilters(this.engine);
63
+ this.submitSearch(true);
64
+ }
65
+ }
66
+
22
67
  @api
23
68
  public fetchMoreResults() {
24
- // Data Cloud Search API does not expose pagination in the same way; no-op
69
+ if (this.resultList?.state?.moreResultsAvailable) {
70
+ this.fetchingMoreResults = true;
71
+ this.resultList.fetchMoreResults();
72
+ }
25
73
  }
26
74
 
27
75
  @api
@@ -37,87 +85,313 @@ export default class SidebarSearch extends LightningElement {
37
85
  this.dropdownRequestedOpen = value;
38
86
  }
39
87
 
88
+ private _coveoAdvancedQueryConfig!: { [key: string]: any };
40
89
  private dropdownRequestedOpen: boolean = false;
41
90
  private recentSearches: Option[] = [];
42
- private value: string = "";
91
+ private unsubscribeFromResultList: Unsubscribe = () => {};
92
+ private resultList: ResultList | null = null;
93
+ private resultsPerPage: ResultsPerPage | null = null;
94
+ private preloadedState?: SearchAppState;
95
+ private searchBox: SearchBox | null = null;
96
+ private value?: string = "";
97
+ private engine: SearchEngine | null = null;
98
+ private fetchingMoreResults: boolean = false;
43
99
  private didRender = false;
44
- private dataCloudSearchInitialized: boolean = false;
100
+
101
+ private get hasCorrectCoveoConfig(): boolean {
102
+ const coveoConfigurations = [
103
+ this.coveoOrganizationId,
104
+ this.coveoPublicAccessToken,
105
+ this.coveoSearchHub,
106
+ this.coveoAdvancedQueryConfig
107
+ ];
108
+
109
+ return coveoConfigurations.every(
110
+ (val) =>
111
+ !!val && (typeof val === "string" || typeof val === "object")
112
+ );
113
+ }
45
114
 
46
115
  private get isDropdownOpen() {
47
116
  return (
48
117
  this.dropdownRequestedOpen &&
49
- this.recentSearches?.length > 0 &&
118
+ this.recentSearches &&
119
+ this.recentSearches.length > 0 &&
50
120
  !this.value
51
121
  );
52
122
  }
53
123
 
54
124
  constructor() {
55
125
  super();
126
+
56
127
  this.updateRecentSearches();
57
- this.value = getSearchQueryParam();
128
+ this.value = getSidebarSearchParams() || "";
129
+ }
130
+
131
+ disconnectedCallback() {
132
+ this.unsubscribeFromResultList();
58
133
  }
59
134
 
60
135
  renderedCallback() {
61
- if (!this.dataCloudSearchInitialized) {
62
- this.dataCloudSearchInitialized = true;
63
- if (this.value) {
64
- // Page load with ?q= in URL: run search so results and loading state resolve
65
- if (!this.didRender) {
66
- this.dispatchHighlightedTermChange();
136
+ // in prod some of the critical coveo configuration attributes seem to arrive later
137
+ // so I needed to put this coveo startup flow here where we can wait for them to arrive
138
+
139
+ if (this.hasCorrectCoveoConfig && !this.engine) {
140
+ this.initializeUserSession();
141
+ this.initializeCoveo();
142
+ } else if (!this.hasCorrectCoveoConfig) {
143
+ console.error(
144
+ "Incorrect coveo search configuration provided. All coveo configuration attributes must be defined.",
145
+ {
146
+ coveoOrganizationId: this.coveoOrganizationId,
147
+ coveoPublicAccessToken: this.coveoPublicAccessToken,
148
+ coveoSearchHub: this.coveoSearchHub,
149
+ coveoAdvancedQueryConfig: this.coveoAdvancedQueryConfig
67
150
  }
68
- this.dispatchOnLoading(true);
69
- this.fetchDataCloudSearch();
70
- } else {
71
- this.dispatchOnLoading(false);
72
- }
151
+ );
73
152
  }
153
+
154
+ // If this is the first render and there is a search value, trigger
155
+ // term highlighting
74
156
  if (!this.didRender && this.value) {
75
157
  this.dispatchHighlightedTermChange();
76
158
  }
159
+
77
160
  this.didRender = true;
78
161
  }
79
162
 
80
- private normalizeDataCloudResult = (
81
- item: DataCloudSearchResultItem,
82
- index: number
83
- ): SidebarSearchResult => {
84
- const href = item.url ?? "";
85
- const resultPath = href.startsWith("http")
86
- ? new URL(href).pathname
87
- : href;
88
- const isSelected =
89
- !!resultPath && resultPath === window.location.pathname;
163
+ private initializeUserSession() {
164
+ const stringified = window.sessionStorage.getItem(SESSION_KEY);
165
+
166
+ if (!stringified) {
167
+ return;
168
+ }
169
+
170
+ try {
171
+ const json = JSON.parse(stringified);
172
+
173
+ const hasSameConfig = Object.entries(
174
+ json.coveoAdvancedQueryConfig
175
+ ).every(
176
+ ([key, value]: [string, any]) =>
177
+ this.coveoAdvancedQueryConfig[key] === value
178
+ );
179
+ const hasSameQuery = json?.state?.query?.q === this.value;
180
+
181
+ if (hasSameConfig && hasSameQuery) {
182
+ this.preloadedState = json?.state;
183
+ } else {
184
+ window.sessionStorage.removeItem(SESSION_KEY);
185
+ }
186
+ } catch (e) {
187
+ console.error(e);
188
+ window.sessionStorage.removeItem(SESSION_KEY);
189
+ }
190
+ }
191
+
192
+ private initializeCoveo = () => {
193
+ try {
194
+ this.engine = buildSearchEngine({
195
+ organizationId: this.coveoOrganizationId,
196
+ publicAccessToken: this.coveoPublicAccessToken,
197
+ searchHub: this.coveoSearchHub,
198
+ preloadedState: this.preloadedState
199
+ });
200
+ } catch (ex) {
201
+ console.error(`Coveo engine failed to initialize (${ex})`);
202
+ }
203
+
204
+ if (!this.engine) {
205
+ console.error("Coveo engine failed to initialize!");
206
+ return;
207
+ }
208
+
209
+ if (this.preloadedState) {
210
+ const actions = loadQueryActions(this.engine);
211
+ this.engine.dispatch(actions.updateQuery({ q: this.value || "" }));
212
+ }
213
+
214
+ this.resultList = buildResultList(this.engine, {
215
+ options: { fieldsToInclude: ["sfhtml_url__c"] }
216
+ });
217
+
218
+ this.resultsPerPage = buildResultsPerPage(this.engine, {
219
+ initialState: {
220
+ numberOfResults: DEFAULT_RESULT_COUNT
221
+ }
222
+ });
223
+
224
+ this.searchBox = buildSearchBox(this.engine, {
225
+ options: { numberOfSuggestions: 3 }
226
+ });
227
+
228
+ this.setAdvancedFilters(this.engine);
229
+
230
+ this.unsubscribeFromResultList = this.resultList?.subscribe(
231
+ this.onResultListChange
232
+ );
233
+
234
+ if (this.value) {
235
+ this.dispatchHighlightedTermChange();
236
+ }
237
+
238
+ if (!this.preloadedState) {
239
+ if (this.value) {
240
+ this.submitSearch();
241
+ } else {
242
+ this.dispatchOnLoading(false);
243
+ }
244
+ }
245
+
246
+ this.syncAnalytics();
247
+ };
248
+
249
+ private syncAnalytics = () => {
250
+ const storedResult = window.localStorage.getItem(
251
+ RESULT_CLICK_LOCAL_STORAGE_KEY
252
+ );
253
+ if (storedResult) {
254
+ const interactiveResult = buildInteractiveResult(this.engine!, {
255
+ options: { result: JSON.parse(storedResult) }
256
+ });
257
+ interactiveResult.cancelPendingSelect();
258
+ interactiveResult.select();
259
+ window.localStorage.removeItem(RESULT_CLICK_LOCAL_STORAGE_KEY);
260
+ }
261
+ };
262
+
263
+ private onResultListChange = () => {
264
+ if (!this.resultList) {
265
+ return;
266
+ }
267
+
268
+ const { isLoading, firstSearchExecuted, results } =
269
+ this.resultList.state;
270
+
271
+ if ((!firstSearchExecuted && !isLoading) || !this.value) {
272
+ // coveo search is firing off some unwanted payloads on startup
273
+ return;
274
+ }
275
+
276
+ if (!isLoading) {
277
+ this.dispatchOnChangeEvent(results);
278
+ }
279
+
280
+ if (!this.fetchingMoreResults) {
281
+ this.dispatchOnLoading(isLoading);
282
+ }
283
+
284
+ if (!isLoading && this.fetchingMoreResults) {
285
+ this.fetchingMoreResults = false;
286
+ }
287
+ };
288
+
289
+ private setAdvancedFilters(engine: SearchEngine) {
290
+ const aq = Object.entries(this.coveoAdvancedQueryConfig).reduce(
291
+ (result, [key, value]) =>
292
+ `${result}(@${key}=="${normalizeCoveoAdvancedQueryValue(
293
+ value
294
+ )}")`,
295
+ ""
296
+ );
297
+
298
+ const registerSearchQueryAction = loadAdvancedSearchQueryActions(
299
+ engine
300
+ ).registerAdvancedSearchQueries({
301
+ aq: aq
302
+ });
303
+
304
+ engine.dispatch(registerSearchQueryAction);
305
+ }
306
+
307
+ private normalizeCoveoResult = (result: Result): SidebarSearchResult => {
308
+ const {
309
+ clickUri,
310
+ excerpt,
311
+ excerptHighlights,
312
+ raw: { sfhtml_url__c },
313
+ title,
314
+ titleHighlights,
315
+ uniqueId
316
+ } = result;
317
+
318
+ // discussion about this normalization here: https://salesforce-internal.slack.com/archives/C020S6784JX/p1639506238376600
319
+
320
+ let pathname, queryParam;
321
+ if (sfhtml_url__c) {
322
+ pathname = `/docs/${sfhtml_url__c}`;
323
+ } else {
324
+ const isNestedGuide = clickUri.includes("/guide/");
325
+ const url = new URL(clickUri);
326
+ const extension =
327
+ isNestedGuide && !url.pathname?.endsWith(".html")
328
+ ? ".html"
329
+ : "";
330
+ pathname = `${url.pathname}${extension}`;
331
+ queryParam = url.search;
332
+ }
333
+
334
+ let href;
335
+ let isSelected = false;
336
+ const isReferenceUrl = clickUri.includes("/references/");
337
+ if (isReferenceUrl) {
338
+ if (queryParam) {
339
+ href = `${pathname}${queryParam}&q=${this.value}`;
340
+ } else {
341
+ href = `${pathname}?q=${this.value}`;
342
+ }
343
+
344
+ //NOTE: This is specific to references related comparison
345
+ const resultHrefWithMetaQuery = `${pathname}${queryParam}`;
346
+
347
+ const urlParams = new URLSearchParams(window.location.search);
348
+ const metaQueryParam = urlParams.get("meta");
349
+
350
+ let currentUrlPathWithQuery = window.location.pathname;
351
+ if (metaQueryParam) {
352
+ currentUrlPathWithQuery = `${currentUrlPathWithQuery}?meta=${metaQueryParam}`;
353
+ }
354
+
355
+ isSelected = resultHrefWithMetaQuery === currentUrlPathWithQuery;
356
+ } else {
357
+ href = `${pathname}?q=${this.value}`;
358
+ isSelected = pathname === window.location.pathname;
359
+ }
360
+
90
361
  return {
91
- title: item.title ?? "",
92
- titleHighlights: [],
93
- excerpt: item.matchedText ?? "",
94
- excerptHighlights: [],
95
- uniqueId: href || `result-${index}`,
362
+ title,
363
+ titleHighlights,
364
+ excerpt,
365
+ excerptHighlights,
366
+ uniqueId,
96
367
  href,
97
368
  selected: isSelected,
98
- select: () => {}
369
+ select: () =>
370
+ window.localStorage.setItem(
371
+ RESULT_CLICK_LOCAL_STORAGE_KEY,
372
+ JSON.stringify(result)
373
+ )
99
374
  };
100
375
  };
101
376
 
102
- private async fetchDataCloudSearch(): Promise<void> {
103
- try {
104
- const rawResults = await fetchSearch(this.value.trim());
105
- const results: SidebarSearchResult[] = rawResults.map(
106
- this.normalizeDataCloudResult
107
- );
108
- this.dispatchChange(results);
109
- } catch (err) {
110
- console.error("Data Cloud Search request failed", err);
111
- this.dispatchChange([]);
112
- } finally {
113
- this.dispatchOnLoading(false);
114
- }
377
+ private updateSessionStorage() {
378
+ window.sessionStorage.setItem(
379
+ SESSION_KEY,
380
+ JSON.stringify({
381
+ coveoAdvancedQueryConfig: this.coveoAdvancedQueryConfig,
382
+ state: this.engine?.state
383
+ })
384
+ );
115
385
  }
116
386
 
117
- private dispatchChange(results: SidebarSearchResult[]) {
387
+ private dispatchOnChangeEvent(results: Result[]) {
388
+ this.updateSessionStorage();
118
389
  this.dispatchEvent(
119
390
  new CustomEvent("change", {
120
- detail: { results, value: this.value }
391
+ detail: {
392
+ results: results.map(this.normalizeCoveoResult),
393
+ value: this.value
394
+ }
121
395
  })
122
396
  );
123
397
  }
@@ -153,12 +427,19 @@ export default class SidebarSearch extends LightningElement {
153
427
  }
154
428
 
155
429
  private submitSearch = debounce((isSyncingSearchValue = false) => {
156
- UserRecentSearches.add(this.value);
157
- this.updateRecentSearches();
158
- if (!isSyncingSearchValue) {
159
- this.dispatchSidebarSearchChange(this.value);
430
+ if (this.searchBox) {
431
+ UserRecentSearches.add(this.value);
432
+ this.updateRecentSearches();
433
+
434
+ // When `isSyncingSearchValue` is true, we are submitting in a case
435
+ // where the search box is already synced correctly.
436
+ if (!isSyncingSearchValue) {
437
+ this.dispatchSidebarSearchChange(this.value);
438
+ }
439
+
440
+ this.searchBox.updateText(this.value || "");
441
+ this.searchBox.submit();
160
442
  }
161
- this.fetchDataCloudSearch();
162
443
  }, SEARCH_DEBOUNCE_DELAY);
163
444
 
164
445
  private onClickRecentSearch(e: CustomEvent) {
@@ -180,6 +461,7 @@ export default class SidebarSearch extends LightningElement {
180
461
 
181
462
  private onInputChange(e: CustomEvent) {
182
463
  this.value = e.detail;
464
+
183
465
  this.handleValueChange(false);
184
466
  this.dispatchHighlightedTermChange();
185
467
  }
@@ -195,10 +477,16 @@ export default class SidebarSearch extends LightningElement {
195
477
  private handleValueChange(isSyncingSearchValue = false) {
196
478
  if (this.value) {
197
479
  this.dispatchOnLoading(true);
198
- this.submitSearch(isSyncingSearchValue);
480
+ if (this.searchBox) {
481
+ this.submitSearch(isSyncingSearchValue);
482
+ }
199
483
  } else {
484
+ // coveo's reaction to an empty value triggers a return of results
485
+ // bootlegging our own ux here`
200
486
  this.dispatchOnLoading(false);
201
- this.dispatchChange([]);
487
+ this.dispatchOnChangeEvent([]);
488
+ // Empty search values are not submitted for search, so we need to manually
489
+ // trigger a search change on our own here
202
490
  this.dispatchSidebarSearchChange(this.value);
203
491
  }
204
492
  }
@@ -1,8 +1,8 @@
1
1
  import { LightningElement, api } from "lwc";
2
2
  import cx from "classnames";
3
- import { HighlightedSections } from "typings/custom";
3
+ import { CoveoHighlights } from "typings/custom";
4
4
 
5
- const toChunks = (value: string, highlights: HighlightedSections) => {
5
+ const toChunks = (value: string, highlights: CoveoHighlights) => {
6
6
  if (!highlights || highlights.length < 1) {
7
7
  return [
8
8
  {
@@ -51,11 +51,11 @@ const toChunks = (value: string, highlights: HighlightedSections) => {
51
51
 
52
52
  export default class SidebarSearchResult extends LightningElement {
53
53
  @api description!: string;
54
- @api descriptionHighlights!: HighlightedSections;
54
+ @api descriptionHighlights!: CoveoHighlights;
55
55
  @api href!: string;
56
56
  @api selected!: boolean;
57
57
  @api header!: string;
58
- @api titleHighlights!: HighlightedSections;
58
+ @api titleHighlights!: CoveoHighlights;
59
59
  @api select!: Function;
60
60
 
61
61
  private get titleChunks() {
@@ -1,17 +1,14 @@
1
1
  import type { BundledLanguage, BundledTheme, HighlighterCore } from "shiki";
2
+ import * as shiki from "shiki";
3
+ import { transformerColorizedBrackets } from "@shikijs/colorized-brackets";
2
4
  import { getCustomLanguageGrammars } from "dxUtils/shikiGrammars";
3
5
 
4
- let shikiModulePromise: Promise<typeof import("shiki")> | null = null;
5
- let bracketsModulePromise: Promise<
6
- typeof import("@shikijs/colorized-brackets")
7
- > | null = null;
8
-
9
6
  async function getShiki() {
10
- return (shikiModulePromise ??= import("shiki"));
7
+ return shiki;
11
8
  }
12
9
 
13
10
  async function getBrackets() {
14
- return (bracketsModulePromise ??= import("@shikijs/colorized-brackets"));
11
+ return { transformerColorizedBrackets };
15
12
  }
16
13
 
17
14
  interface ShikiSingleton {
@@ -101,7 +98,7 @@ const OPTIONAL_LANGUAGES: Record<string, any> = {
101
98
  agentscript: getCustomLanguageGrammars().agentscript
102
99
  };
103
100
 
104
- // Initialize Shiki highlighter (lazy-load module and keep initial set minimal)
101
+ // Initialize Shiki highlighter
105
102
  async function initializeShiki(): Promise<HighlighterCore> {
106
103
  if (shikiInstance.highlighter) {
107
104
  return shikiInstance.highlighter;
@@ -113,8 +110,8 @@ async function initializeShiki(): Promise<HighlighterCore> {
113
110
 
114
111
  // Assign promise IMMEDIATELY before any async work to prevent race conditions
115
112
  shikiInstance.initPromise = (async () => {
116
- const shiki = await getShiki();
117
- const highlighter = await shiki.createHighlighter({
113
+ const shikiModule = await getShiki();
114
+ const highlighter = await shikiModule.createHighlighter({
118
115
  themes: ["light-plus", "material-theme-darker"],
119
116
  langs: CORE_LANGUAGES
120
117
  });
@@ -182,12 +179,12 @@ export async function highlightCode(
182
179
  mappedLanguage = "text" as BundledLanguage;
183
180
  }
184
181
 
185
- const { transformerColorizedBrackets } = await getBrackets();
182
+ const { transformerColorizedBrackets: transformer } = await getBrackets();
186
183
 
187
184
  return highlighter.codeToHtml(code, {
188
185
  lang: mappedLanguage,
189
186
  theme: THEME_MAP[theme],
190
- transformers: [transformerColorizedBrackets()],
187
+ transformers: [transformer()],
191
188
  colorReplacements:
192
189
  THEME_MAP_COLOR_REPLACEMENTS[theme].colorReplacements
193
190
  });