@xh/hoist 73.0.0-SNAPSHOT.1746647745931 → 73.0.0-SNAPSHOT.1746740884592

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 (38) hide show
  1. package/CHANGELOG.md +4 -1
  2. package/admin/AppModel.ts +6 -10
  3. package/admin/columns/Tracking.ts +11 -11
  4. package/admin/tabs/activity/tracking/ActivityTracking.scss +18 -0
  5. package/admin/tabs/activity/tracking/ActivityTrackingModel.ts +15 -6
  6. package/admin/tabs/activity/tracking/detail/ActivityDetailModel.ts +3 -0
  7. package/admin/tabs/activity/tracking/detail/ActivityDetailView.ts +1 -0
  8. package/admin/tabs/{client/clients → clients}/ClientsPanel.ts +2 -2
  9. package/admin/tabs/{client/clients → clients}/activity/ClientDetailModel.ts +2 -2
  10. package/admin/tabs/{client/clients → clients}/activity/ClientDetailPanel.ts +1 -1
  11. package/admin/tabs/cluster/ClusterTab.ts +1 -1
  12. package/admin/tabs/cluster/instances/InstancesTabModel.ts +1 -1
  13. package/admin/tabs/general/GeneralTab.ts +0 -2
  14. package/appcontainer/FeedbackDialogModel.ts +12 -13
  15. package/build/types/admin/columns/Tracking.d.ts +1 -3
  16. package/build/types/admin/tabs/{client/clients → clients}/activity/ClientDetailModel.d.ts +2 -2
  17. package/build/types/admin/tabs/{client/clients → clients}/activity/ClientDetailPanel.d.ts +1 -1
  18. package/build/types/appcontainer/FeedbackDialogModel.d.ts +1 -4
  19. package/core/exception/ExceptionHandler.ts +14 -15
  20. package/package.json +1 -1
  21. package/styles/XH.scss +1 -0
  22. package/styles/vars.scss +1 -0
  23. package/tsconfig.tsbuildinfo +1 -1
  24. package/admin/tabs/client/ClientTab.ts +0 -24
  25. package/admin/tabs/client/errors/ClientErrorDetail.ts +0 -129
  26. package/admin/tabs/client/errors/ClientErrors.scss +0 -52
  27. package/admin/tabs/client/errors/ClientErrorsModel.ts +0 -227
  28. package/admin/tabs/client/errors/ClientErrorsPanel.ts +0 -101
  29. package/admin/tabs/general/feedback/FeedbackPanel.ts +0 -61
  30. package/build/types/admin/tabs/client/ClientTab.d.ts +0 -1
  31. package/build/types/admin/tabs/client/errors/ClientErrorDetail.d.ts +0 -2
  32. package/build/types/admin/tabs/client/errors/ClientErrorsModel.d.ts +0 -30
  33. package/build/types/admin/tabs/client/errors/ClientErrorsPanel.d.ts +0 -3
  34. package/build/types/admin/tabs/general/feedback/FeedbackPanel.d.ts +0 -1
  35. /package/admin/tabs/{client/clients → clients}/ClientsModel.ts +0 -0
  36. /package/admin/tabs/{client/clients → clients}/activity/ClientDetail.scss +0 -0
  37. /package/build/types/admin/tabs/{client/clients → clients}/ClientsModel.d.ts +0 -0
  38. /package/build/types/admin/tabs/{client/clients → clients}/ClientsPanel.d.ts +0 -0
package/CHANGELOG.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  ### 💥 Breaking Changes (upgrade difficulty: 🟢 TRIVIAL - minor upgrade to Hoist Core)
6
6
 
7
- * Requires `hoist-core >= 30.0` with new APIs to support the consolidated Admin Console "Clients"
7
+ * Requires `hoist-core >= 30.1.0` with new APIs to support the consolidated Admin Console "Clients"
8
8
  tab and new properties on `TrackLog`.
9
9
  * Apps with a custom `AppModel` for their admin app that extends `@xh/hoist/admin/AppModel` must
10
10
  ensure they call `super.initAsync()` within their override of that lifecycle method, if
@@ -25,6 +25,9 @@
25
25
  the same tab.
26
26
  * Improved charting, with a column chart used for both timeseries and category data and fixes to
27
27
  the "skip weekends" option.
28
+ * Client Error reports and user feedback have also been consolidated into the new tracking
29
+ system for more integrated and powerful reporting.
30
+
28
31
  * Updated `FormModel` to support `persistWith` for storing and recalling its values, including
29
32
  developer options to persist all or a provided subset of fields.
30
33
 
package/admin/AppModel.ts CHANGED
@@ -13,7 +13,7 @@ import {Icon} from '@xh/hoist/icon';
13
13
  import {without} from 'lodash';
14
14
  import {Route} from 'router5';
15
15
  import {activityTrackingPanel} from './tabs/activity/tracking/ActivityTrackingPanel';
16
- import {clientTab} from './tabs/client/ClientTab';
16
+ import {clientsPanel} from './tabs/clients/ClientsPanel';
17
17
  import {generalTab} from './tabs/general/GeneralTab';
18
18
  import {monitorTab} from './tabs/monitor/MonitorTab';
19
19
  import {userDataTab} from './tabs/userData/UserDataTab';
@@ -75,8 +75,8 @@ export class AppModel extends HoistAppModel {
75
75
  ]
76
76
  },
77
77
  {
78
- name: 'cluster',
79
- path: '/cluster',
78
+ name: 'servers',
79
+ path: '/servers',
80
80
  children: [
81
81
  {
82
82
  name: 'instances',
@@ -94,11 +94,7 @@ export class AppModel extends HoistAppModel {
94
94
  },
95
95
  {
96
96
  name: 'clients',
97
- path: '/clients',
98
- children: [
99
- {name: 'connections', path: '/connections'},
100
- {name: 'errors', path: '/errors'}
101
- ]
97
+ path: '/clients'
102
98
  },
103
99
  {
104
100
  name: 'monitors',
@@ -129,14 +125,14 @@ export class AppModel extends HoistAppModel {
129
125
  content: generalTab
130
126
  },
131
127
  {
132
- id: 'cluster',
128
+ id: 'servers',
133
129
  icon: Icon.server(),
134
130
  content: clusterTab
135
131
  },
136
132
  {
137
133
  id: 'clients',
138
134
  icon: Icon.desktop(),
139
- content: clientTab
135
+ content: clientsPanel
140
136
  },
141
137
  {
142
138
  id: 'monitors',
@@ -141,7 +141,7 @@ export const deviceIcon: ColumnSpec = {
141
141
  case 'IPAD':
142
142
  case 'IPHONE':
143
143
  case 'IPOD':
144
- return Icon.phone();
144
+ return Icon.mobile();
145
145
  case 'LINUX':
146
146
  case 'MAC':
147
147
  case 'WINDOWS':
@@ -227,7 +227,8 @@ export const errorMessage: ColumnSpec = {
227
227
  export const errorName: ColumnSpec = {
228
228
  field: {
229
229
  name: 'errorName',
230
- type: 'string'
230
+ type: 'string',
231
+ isDimension: true
231
232
  },
232
233
  chooserGroup: 'Errors',
233
234
  width: 150,
@@ -237,6 +238,7 @@ export const errorName: ColumnSpec = {
237
238
  export const instance: ColumnSpec = {
238
239
  field: {
239
240
  name: 'instance',
241
+ displayName: 'Server',
240
242
  type: 'string',
241
243
  isDimension: true,
242
244
  aggregator: 'UNIQUE'
@@ -291,21 +293,22 @@ export const severityIcon: ColumnSpec = {
291
293
  resizable: false,
292
294
  align: 'center',
293
295
  width: 50,
296
+ cellClass: v => (v ? `xh-admin-activity-cell--${v.toLowerCase()}` : ''),
294
297
  renderer: v => getSeverityIcon(v)
295
298
  };
296
299
 
297
- export function getSeverityIcon(severity: TrackSeverity): ReactElement {
300
+ function getSeverityIcon(severity: TrackSeverity): ReactElement {
298
301
  if (!severity) return null;
299
302
 
300
303
  switch (severity) {
301
304
  case 'DEBUG':
302
305
  return Icon.code();
303
306
  case 'INFO':
304
- return Icon.infoCircle({className: 'xh-text-color-muted'});
307
+ return Icon.infoCircle();
305
308
  case 'WARN':
306
- return Icon.warning({intent: 'warning'});
309
+ return Icon.warning();
307
310
  case 'ERROR':
308
- return Icon.error({intent: 'danger'});
311
+ return Icon.error();
309
312
  default:
310
313
  return Icon.questionCircle();
311
314
  }
@@ -376,7 +379,7 @@ export const userAlertedFlag: ColumnSpec = {
376
379
  };
377
380
 
378
381
  export const userMessageFlag: ColumnSpec = {
379
- field: {name: 'userMessageFlag', type: 'bool'},
382
+ field: {name: 'userMessage', type: 'string'},
380
383
  headerName: Icon.comment(),
381
384
  headerTooltip:
382
385
  'Indicates if the user provided a message along with the automated error report.',
@@ -385,10 +388,7 @@ export const userMessageFlag: ColumnSpec = {
385
388
  resizable: false,
386
389
  align: 'center',
387
390
  width: 50,
388
- renderer: (v, {record}) => {
389
- const {msg} = record.data;
390
- return msg ? Icon.comment() : null;
391
- }
391
+ renderer: v => (v ? Icon.comment() : null)
392
392
  };
393
393
 
394
394
  //-----------------------
@@ -97,3 +97,21 @@
97
97
  }
98
98
  }
99
99
  }
100
+
101
+ .xh-admin-activity-cell {
102
+ &--debug {
103
+ color: var(--xh-intent-primary) !important;
104
+ background-color: var(--xh-intent-primary-trans2);
105
+ }
106
+ &--error {
107
+ color: var(--xh-intent-danger) !important;
108
+ background-color: var(--xh-intent-danger-trans2);
109
+ }
110
+ &--info {
111
+ color: var(--xh-text-color-muted);
112
+ }
113
+ &--warn {
114
+ color: var(--xh-intent-warning) !important;
115
+ background-color: var(--xh-intent-warning-trans2);
116
+ }
117
+ }
@@ -330,6 +330,7 @@ export class ActivityTrackingModel extends HoistModel implements ActivityDetailP
330
330
  Col.day.field,
331
331
  Col.dayRange.field,
332
332
  Col.device.field,
333
+ Col.errorName.field,
333
334
  Col.elapsed.field,
334
335
  Col.elapsedMax.field,
335
336
  Col.entryCount.field,
@@ -343,6 +344,8 @@ export class ActivityTrackingModel extends HoistModel implements ActivityDetailP
343
344
  Col.userAgent.field,
344
345
  Col.username.field,
345
346
  Col.url.field,
347
+ Col.userAlertedFlag.field,
348
+ Col.userMessageFlag.field,
346
349
  ...this.dataFields
347
350
  ] as CubeFieldSpec[];
348
351
 
@@ -453,6 +456,8 @@ export class ActivityTrackingModel extends HoistModel implements ActivityDetailP
453
456
  {...Col.tabId, hidden},
454
457
  {...Col.url, hidden},
455
458
  {...Col.instance, hidden},
459
+ {...Col.errorName, hidden},
460
+ {...Col.userAlertedFlag, hidden},
456
461
  ...this.dataFieldCols.map(it => ({...it, hidden: !it.appData.showInAggGrid}))
457
462
  ]
458
463
  });
@@ -471,12 +476,16 @@ export class ActivityTrackingModel extends HoistModel implements ActivityDetailP
471
476
  raw.elapsedMax = raw.elapsed;
472
477
 
473
478
  const data = JSON.parse(raw.data);
474
- if (isEmpty(data)) return;
475
-
476
- this.dataFields.forEach(df => {
477
- const path = df.path;
478
- raw[df.name] = get(data, path);
479
- });
479
+ if (!isEmpty(data)) {
480
+ raw.userMessage = get(data, 'userMessage');
481
+ raw.userAlerted = get(data, 'userAlerted');
482
+ raw.errorName = get(data, 'error.name');
483
+
484
+ this.dataFields.forEach(df => {
485
+ const path = df.path;
486
+ raw[df.name] = get(data, path);
487
+ });
488
+ }
480
489
  } catch (e) {
481
490
  this.logError(`Error processing raw track log`, e);
482
491
  }
@@ -170,10 +170,13 @@ export class ActivityDetailModel extends HoistModel {
170
170
  {...Col.entryId, hidden, pinned},
171
171
  {...Col.severityIcon, pinned},
172
172
  {...Col.impersonatingFlag, pinned},
173
+ {...Col.userAlertedFlag, hidden, pinned},
174
+ {...Col.userMessageFlag, hidden, pinned},
173
175
  {...Col.username, pinned},
174
176
  {...Col.impersonating, hidden},
175
177
  {...Col.category},
176
178
  {...Col.msg},
179
+ {...Col.errorName, hidden},
177
180
  {...Col.elapsed},
178
181
  {...Col.deviceIcon},
179
182
  {...Col.browser, hidden},
@@ -128,6 +128,7 @@ const detailRecForm = hoistCmp.factory<ActivityDetailModel>(({model}) => {
128
128
  }),
129
129
  formField({
130
130
  field: 'instance',
131
+ label: 'Server',
131
132
  readonlyRenderer: badgeRenderer,
132
133
  omit: !formModel.values.instance
133
134
  }),
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * Copyright © 2025 Extremely Heavy Industries Inc.
6
6
  */
7
- import {clientDetailPanel} from '@xh/hoist/admin/tabs/client/clients/activity/ClientDetailPanel';
7
+ import {clientDetailPanel} from './activity/ClientDetailPanel';
8
8
  import {errorMessage} from '@xh/hoist/cmp/error';
9
9
  import {grid, gridCountLabel} from '@xh/hoist/cmp/grid';
10
10
  import {filler, fragment, hframe, p} from '@xh/hoist/cmp/layout';
@@ -30,7 +30,7 @@ export const clientsPanel = hoistCmp.factory<ClientsModel>({
30
30
  placeholder: 'Ungrouped',
31
31
  options: [
32
32
  {value: 'user', label: 'By User'},
33
- {value: 'instance', label: 'By Instance'}
33
+ {value: 'instance', label: 'By Server'}
34
34
  ],
35
35
  enableClear: true,
36
36
  enableFilter: false
@@ -1,10 +1,10 @@
1
- import {ClientsModel} from '@xh/hoist/admin/tabs/client/clients/ClientsModel';
1
+ import {ClientsModel} from '../ClientsModel';
2
2
  import {ColumnSpec} from '@xh/hoist/cmp/grid';
3
3
  import {HoistModel, LoadSpec, lookup, PlainObject, XH} from '@xh/hoist/core';
4
4
  import {StoreRecord} from '@xh/hoist/data';
5
5
  import {bindable, computed, makeObservable} from '@xh/hoist/mobx';
6
6
  import {ReactNode} from 'react';
7
- import {ActivityDetailProvider} from '../../../activity/tracking/detail/ActivityDetailModel';
7
+ import {ActivityDetailProvider} from '../../activity/tracking/detail/ActivityDetailModel';
8
8
 
9
9
  export class ClientDetailModel extends HoistModel implements ActivityDetailProvider {
10
10
  @lookup(ClientsModel) clientsModel: ClientsModel;
@@ -1,6 +1,6 @@
1
1
  import {isOpen} from '@xh/hoist/admin/columns';
2
2
  import {activityDetailView} from '@xh/hoist/admin/tabs/activity/tracking/detail/ActivityDetailView';
3
- import {ClientDetailModel} from '@xh/hoist/admin/tabs/client/clients/activity/ClientDetailModel';
3
+ import {ClientDetailModel} from './ClientDetailModel';
4
4
  import {h2, hbox, placeholder, vbox} from '@xh/hoist/cmp/layout';
5
5
  import {mask} from '@xh/hoist/cmp/mask';
6
6
  import {relativeTimestamp} from '@xh/hoist/cmp/relativetimestamp';
@@ -13,7 +13,7 @@ import {Icon} from '@xh/hoist/icon';
13
13
  export const clusterTab = hoistCmp.factory(() =>
14
14
  tabContainer({
15
15
  modelConfig: {
16
- route: 'default.cluster',
16
+ route: 'default.servers',
17
17
  switcher: {orientation: 'left', testId: 'cluster-tab-switcher'},
18
18
  tabs: [
19
19
  {id: 'instances', icon: Icon.server(), content: instancesTab},
@@ -192,7 +192,7 @@ export class InstancesTabModel extends HoistModel {
192
192
 
193
193
  private createTabContainerModel() {
194
194
  return new TabContainerModel({
195
- route: 'default.cluster.instances',
195
+ route: 'default.servers.instances',
196
196
  switcher: false,
197
197
  tabs: [
198
198
  {id: 'logs', icon: Icon.fileText(), content: logViewer},
@@ -5,7 +5,6 @@
5
5
  * Copyright © 2025 Extremely Heavy Industries Inc.
6
6
  */
7
7
  import {configPanel} from '@xh/hoist/admin/tabs/general/config/ConfigPanel';
8
- import {feedbackPanel} from '@xh/hoist/admin/tabs/general/feedback/FeedbackPanel';
9
8
  import {tabContainer} from '@xh/hoist/cmp/tab';
10
9
  import {hoistCmp} from '@xh/hoist/core';
11
10
  import {Icon} from '@xh/hoist/icon';
@@ -20,7 +19,6 @@ export const generalTab = hoistCmp.factory(() =>
20
19
  tabs: [
21
20
  {id: 'about', icon: Icon.info(), content: aboutPanel},
22
21
  {id: 'config', icon: Icon.settings(), content: configPanel},
23
- {id: 'feedback', icon: Icon.comment(), content: feedbackPanel},
24
22
  {id: 'alertBanner', icon: Icon.bullhorn(), content: alertBannerPanel}
25
23
  ]
26
24
  }
@@ -48,24 +48,23 @@ export class FeedbackDialogModel extends HoistModel {
48
48
  }
49
49
 
50
50
  /**
51
- * Submit the feedback entry. Username, browser info, environment info, and datetime will be
52
- * recorded automatically by the server. Feedback entries are viewable via the Admin console,
53
- * and feedback submissions can trigger server-side notifications.
54
- * See FeedbackService.groovy within hoist-core for details.
51
+ * Submit the feedback entry to the activity tracking system.
55
52
  */
56
53
  async submitAsync() {
57
- if (!this.message) this.hide();
54
+ const {message} = this,
55
+ {trackService} = XH;
56
+
57
+ if (!message) this.hide();
58
58
 
59
59
  try {
60
- await XH.fetchJson({
61
- url: 'xh/submitFeedback',
62
- params: {
63
- msg: this.message,
64
- appVersion: XH.getEnv('appVersion'),
65
- clientUsername: XH.getUsername()
60
+ trackService.track({
61
+ category: 'Feedback',
62
+ message: 'User submitted feedback',
63
+ data: {
64
+ userMessage: this.message
66
65
  }
67
- }).linkTo(XH.appLoadModel);
68
-
66
+ });
67
+ trackService.pushPendingAsync().linkTo(XH.appLoadModel);
69
68
  XH.successToast('Thank you - your feedback has been sent.');
70
69
  this.hide();
71
70
  } catch (e) {
@@ -1,6 +1,5 @@
1
+ /// <reference types="react" />
1
2
  import { ColumnSpec } from '@xh/hoist/cmp/grid/columns';
2
- import { TrackSeverity } from '@xh/hoist/core';
3
- import { ReactElement } from 'react';
4
3
  export declare const appBuild: ColumnSpec;
5
4
  export declare const appEnvironment: ColumnSpec;
6
5
  export declare const appVersion: ColumnSpec;
@@ -25,7 +24,6 @@ export declare const loadId: ColumnSpec;
25
24
  export declare const msg: ColumnSpec;
26
25
  export declare const severity: ColumnSpec;
27
26
  export declare const severityIcon: ColumnSpec;
28
- export declare function getSeverityIcon(severity: TrackSeverity): ReactElement;
29
27
  export declare const tabId: ColumnSpec;
30
28
  export declare const url: ColumnSpec;
31
29
  export declare const urlPathOnly: ColumnSpec;
@@ -1,9 +1,9 @@
1
- import { ClientsModel } from '@xh/hoist/admin/tabs/client/clients/ClientsModel';
1
+ import { ClientsModel } from '../ClientsModel';
2
2
  import { ColumnSpec } from '@xh/hoist/cmp/grid';
3
3
  import { HoistModel, LoadSpec, PlainObject } from '@xh/hoist/core';
4
4
  import { StoreRecord } from '@xh/hoist/data';
5
5
  import { ReactNode } from 'react';
6
- import { ActivityDetailProvider } from '../../../activity/tracking/detail/ActivityDetailModel';
6
+ import { ActivityDetailProvider } from '../../activity/tracking/detail/ActivityDetailModel';
7
7
  export declare class ClientDetailModel extends HoistModel implements ActivityDetailProvider {
8
8
  clientsModel: ClientsModel;
9
9
  readonly isActivityDetailProvider = true;
@@ -1,3 +1,3 @@
1
- import { ClientDetailModel } from '@xh/hoist/admin/tabs/client/clients/activity/ClientDetailModel';
1
+ import { ClientDetailModel } from './ClientDetailModel';
2
2
  import './ClientDetail.scss';
3
3
  export declare const clientDetailPanel: import("@xh/hoist/core").ElementFactory<import("@xh/hoist/core").DefaultHoistProps<ClientDetailModel>>;
@@ -15,10 +15,7 @@ export declare class FeedbackDialogModel extends HoistModel {
15
15
  hide(): void;
16
16
  setMessage(message: string): void;
17
17
  /**
18
- * Submit the feedback entry. Username, browser info, environment info, and datetime will be
19
- * recorded automatically by the server. Feedback entries are viewable via the Admin console,
20
- * and feedback submissions can trigger server-side notifications.
21
- * See FeedbackService.groovy within hoist-core for details.
18
+ * Submit the feedback entry to the activity tracking system.
22
19
  */
23
20
  submitAsync(): Promise<void>;
24
21
  }
@@ -193,22 +193,21 @@ export class ExceptionHandler {
193
193
  return false;
194
194
  }
195
195
 
196
- await XH.fetchService.postJson({
197
- url: 'xh/submitError',
198
- body: {
199
- error,
200
- msg: userMessage ? stripTags(userMessage) : '',
201
- appVersion: XH.getEnv('clientVersion'),
202
- url: window.location.href,
203
- userAlerted,
204
- clientUsername: username,
205
- correlationId: exception.correlationId
206
- },
207
- // Post clientUsername as a parameter to ensure client username matches session.
208
- params: {
209
- clientUsername: username
210
- }
196
+ const data: PlainObject = {
197
+ error: JSON.parse(error),
198
+ userAlerted
199
+ };
200
+ if (userMessage) data.userMessage = stripTags(userMessage);
201
+
202
+ XH.track({
203
+ category: 'Client Error',
204
+ severity: exception.isRoutine ? 'INFO' : 'ERROR',
205
+ message: exception.message ?? 'Client Error',
206
+ correlationId: exception.correlationId,
207
+ data,
208
+ logData: ['userAlerted']
211
209
  });
210
+ await XH.trackService.pushPendingAsync();
212
211
  return true;
213
212
  } catch (e) {
214
213
  logError(['Exception while submitting error report to UI server', e], this);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xh/hoist",
3
- "version": "73.0.0-SNAPSHOT.1746647745931",
3
+ "version": "73.0.0-SNAPSHOT.1746740884592",
4
4
  "description": "Hoist add-on for building and deploying React Applications.",
5
5
  "repository": "github:xh/hoist-react",
6
6
  "homepage": "https://xh.io",
package/styles/XH.scss CHANGED
@@ -96,6 +96,7 @@ body.xh-app {
96
96
  .xh-text-color-accent { color: var(--xh-text-color-accent); }
97
97
  .xh-text-color-muted { color: var(--xh-text-color-muted); }
98
98
  .xh-transparent { color: transparent; }
99
+ .xh-white { color: var(--xh-white); }
99
100
  .xh-yellow { color: var(--xh-yellow); }
100
101
  .xh-yellow-light { color: var(--xh-yellow-light); }
101
102
 
package/styles/vars.scss CHANGED
@@ -47,6 +47,7 @@ body {
47
47
  --xh-purple: var(--purple, #{mc('purple', '800')});
48
48
  --xh-red: var(--red, #{mc('red', '900')});
49
49
  --xh-red-muted: var(--red-muted, #{mc-muted('red', '900', 40%, 20%)});
50
+ --xh-white: var(--white, hsl(0, 0%, 100%));
50
51
  --xh-yellow: var(--yellow, #{mc('yellow', '800')});
51
52
  --xh-yellow-light: var(--yellow-light, #{mc('yellow', '50')});
52
53