@xh/hoist 86.2.0 → 86.4.0

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 (75) hide show
  1. package/.gitattributes +4 -0
  2. package/CHANGELOG.md +75 -0
  3. package/admin/AppModel.ts +38 -11
  4. package/admin/tabs/activity/tracking/ActivityTrackingModel.ts +56 -6
  5. package/admin/tabs/activity/tracking/ActivityTrackingPanel.ts +9 -26
  6. package/admin/tabs/clients/ClientsModel.ts +4 -1
  7. package/admin/tabs/clients/activity/ClientDetailModel.ts +5 -1
  8. package/admin/tabs/userData/roles/RoleModel.ts +11 -18
  9. package/admin/tabs/userData/roles/RolePanel.ts +2 -9
  10. package/appcontainer/RouterModel.ts +6 -6
  11. package/build/types/admin/AppModel.d.ts +7 -4
  12. package/build/types/admin/tabs/activity/tracking/ActivityTrackingModel.d.ts +14 -3
  13. package/build/types/admin/tabs/clients/ClientsModel.d.ts +3 -0
  14. package/build/types/admin/tabs/userData/roles/RoleModel.d.ts +2 -2
  15. package/build/types/appcontainer/RouterModel.d.ts +5 -5
  16. package/build/types/cmp/grid/Grid.d.ts +2 -1
  17. package/build/types/cmp/grid/GridModel.d.ts +2 -2
  18. package/build/types/cmp/grid/Types.d.ts +8 -1
  19. package/build/types/cmp/input/SegmentedControlOption.d.ts +12 -0
  20. package/build/types/core/HoistAppModel.d.ts +2 -3
  21. package/build/types/core/types/Types.d.ts +12 -0
  22. package/build/types/data/Store.d.ts +3 -2
  23. package/build/types/desktop/cmp/filter/FilterChooser.d.ts +5 -0
  24. package/build/types/desktop/cmp/filter/PopoverFilterChooser.d.ts +3 -1
  25. package/build/types/desktop/cmp/grid/impl/colchooser/ColChooserModel.d.ts +2 -2
  26. package/build/types/desktop/cmp/grid/impl/filter/headerfilter/custom/CustomRowModel.d.ts +9 -1
  27. package/build/types/desktop/cmp/grid/impl/filter/headerfilter/custom/CustomTabModel.d.ts +8 -2
  28. package/build/types/desktop/cmp/input/Select.d.ts +8 -0
  29. package/build/types/kit/react-dropzone/index.d.ts +1 -0
  30. package/build/types/kit/swiper/index.d.ts +4 -3
  31. package/build/types/mobile/cmp/grid/impl/ColChooserModel.d.ts +2 -2
  32. package/build/types/mobile/cmp/input/Select.d.ts +8 -0
  33. package/build/types/svc/PrefService.d.ts +12 -1
  34. package/build/types/utils/datetime/LocalDate.d.ts +23 -1
  35. package/cmp/grid/Grid.ts +10 -2
  36. package/cmp/grid/GridModel.ts +4 -3
  37. package/cmp/grid/Types.ts +9 -1
  38. package/cmp/grid/impl/MenuSupport.ts +1 -1
  39. package/cmp/input/SegmentedControlOption.ts +14 -0
  40. package/cmp/store/impl/StoreFilterFieldImplModel.ts +5 -6
  41. package/core/HoistAppModel.ts +2 -3
  42. package/core/types/Types.ts +14 -5
  43. package/data/Store.ts +3 -2
  44. package/desktop/cmp/filechooser/FileChooser.ts +6 -3
  45. package/desktop/cmp/filechooser/FileChooserModel.ts +12 -5
  46. package/desktop/cmp/filter/FilterChooser.scss +42 -0
  47. package/desktop/cmp/filter/FilterChooser.ts +147 -7
  48. package/desktop/cmp/filter/PopoverFilterChooser.ts +7 -84
  49. package/desktop/cmp/grid/editors/impl/InlineEditorModel.ts +6 -1
  50. package/desktop/cmp/grid/find/impl/GridFindFieldImplModel.ts +3 -5
  51. package/desktop/cmp/grid/impl/colchooser/ColChooserModel.ts +2 -2
  52. package/desktop/cmp/grid/impl/filter/headerfilter/custom/CustomRow.ts +12 -12
  53. package/desktop/cmp/grid/impl/filter/headerfilter/custom/CustomRowModel.ts +18 -0
  54. package/desktop/cmp/grid/impl/filter/headerfilter/custom/CustomTab.scss +5 -0
  55. package/desktop/cmp/grid/impl/filter/headerfilter/custom/CustomTab.ts +13 -2
  56. package/desktop/cmp/grid/impl/filter/headerfilter/custom/CustomTabModel.ts +97 -14
  57. package/desktop/cmp/input/SegmentedControl.ts +7 -3
  58. package/desktop/cmp/input/Select.ts +24 -16
  59. package/docs/routing.md +4 -2
  60. package/docs/upgrade-notes/v86-upgrade-notes.md +73 -8
  61. package/kit/react-dropzone/index.ts +1 -0
  62. package/mcp/README.md +12 -0
  63. package/mcp/data/ts-registry.spec.ts +156 -0
  64. package/mcp/data/ts-registry.ts +13 -6
  65. package/mcp/formatters/typescript.ts +7 -3
  66. package/mcp/util/paths.ts +21 -0
  67. package/mobile/cmp/grid/impl/ColChooserModel.ts +2 -2
  68. package/mobile/cmp/input/SegmentedControl.ts +7 -3
  69. package/mobile/cmp/input/Select.ts +21 -15
  70. package/mobx/README.md +2 -1
  71. package/package.json +5 -5
  72. package/svc/PrefService.ts +80 -20
  73. package/svc/README.md +6 -0
  74. package/utils/datetime/LocalDate.ts +24 -1
  75. package/desktop/cmp/filter/PopoverFilterChooser.scss +0 -42
@@ -11,7 +11,7 @@
11
11
  */
12
12
  import {z} from 'zod';
13
13
  import type {MemberInfo, MemberIndexEntry, SymbolEntry, SymbolDetail} from '../data/ts-registry.js';
14
- import {resolveRepoRoot} from '../util/paths.js';
14
+ import {resolveRepoRootPosix, toPosixPath} from '../util/paths.js';
15
15
 
16
16
  /** Remove blank lines from a JSDoc string to produce more compact output. */
17
17
  function collapseJsDoc(jsDoc: string): string {
@@ -31,8 +31,12 @@ export function truncateType(typeStr: string): string {
31
31
 
32
32
  /** Convert an absolute file path to a repo-relative path. */
33
33
  export function toRelativePath(filePath: string): string {
34
- const root = resolveRepoRoot();
35
- return filePath.startsWith(root) ? filePath.slice(root.length + 1) : filePath;
34
+ // Symbol filePaths originate from ts-morph (forward slashes on all platforms);
35
+ // compare in POSIX form so the repo-root prefix strips correctly on Windows,
36
+ // where `resolveRepoRoot()` would otherwise yield a backslash path.
37
+ const root = resolveRepoRootPosix();
38
+ const posix = toPosixPath(filePath);
39
+ return posix.startsWith(root) ? posix.slice(root.length + 1) : posix;
36
40
  }
37
41
 
38
42
  /**
package/mcp/util/paths.ts CHANGED
@@ -39,6 +39,27 @@ export function resolveRepoRoot(): string {
39
39
  return repoRoot;
40
40
  }
41
41
 
42
+ /** Convert a filesystem path to POSIX (forward-slash) form. */
43
+ export function toPosixPath(p: string): string {
44
+ return p.replace(/\\/g, '/');
45
+ }
46
+
47
+ /**
48
+ * Repo root in POSIX (forward-slash) form, matching ts-morph's `getFilePath()`
49
+ * convention, which always reports forward slashes on every platform.
50
+ *
51
+ * On Windows, {@link resolveRepoRoot} returns a backslash path (e.g.
52
+ * `D:\hoist-react`) while ts-morph reports `D:/hoist-react/...`; comparing or
53
+ * slicing the two directly fails, silently filtering out every source file and
54
+ * yielding an empty symbol index. Use this whenever comparing against or
55
+ * slicing a ts-morph source-file path (see `data/ts-registry.ts` and
56
+ * `formatters/typescript.ts`). Filesystem access that stays within Node's
57
+ * `path` APIs should keep using {@link resolveRepoRoot}.
58
+ */
59
+ export function resolveRepoRootPosix(): string {
60
+ return toPosixPath(resolveRepoRoot());
61
+ }
62
+
42
63
  /**
43
64
  * Resolve the `@xh/hoist` library version from the repo root `package.json`.
44
65
  *
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * Copyright © 2026 Extremely Heavy Industries Inc.
6
6
  */
7
- import {GridModel} from '@xh/hoist/cmp/grid';
7
+ import {GridModel, IColChooserModel} from '@xh/hoist/cmp/grid';
8
8
  import {HoistModel, HSide, XH} from '@xh/hoist/core';
9
9
  import '@xh/hoist/mobile/register';
10
10
  import {bindable, action, makeObservable, observable} from '@xh/hoist/mobx';
@@ -17,7 +17,7 @@ import {clone, find, sortBy} from 'lodash';
17
17
  * It is not necessary to manually create instances of this class within an application.
18
18
  * @internal
19
19
  */
20
- export class ColChooserModel extends HoistModel {
20
+ export class ColChooserModel extends HoistModel implements IColChooserModel {
21
21
  override xhImpl = true;
22
22
 
23
23
  gridModel: GridModel;
@@ -85,6 +85,7 @@ export const [SegmentedControl, segmentedControl] = hoistCmp.withFactory<Segment
85
85
  interface NormalizedOption extends SegmentedControlOption {
86
86
  label: string;
87
87
  intent?: Intent;
88
+ testId?: string;
88
89
  _key: string;
89
90
  }
90
91
 
@@ -97,13 +98,14 @@ class SegmentedControlModel extends HoistInputModel {
97
98
  return options.map((o: any, idx: number) => {
98
99
  const key = String(idx);
99
100
  if (isObject(o)) {
100
- const {label, value, icon, disabled, intent} = o as SegmentedControlOption;
101
+ const {label, value, icon, disabled, intent, testId} = o as SegmentedControlOption;
101
102
  return {
102
103
  value: this.toInternal(value),
103
104
  label: label ?? (icon ? '' : String(value)),
104
105
  icon,
105
106
  disabled,
106
107
  intent,
108
+ testId,
107
109
  _key: key
108
110
  };
109
111
  } else {
@@ -168,7 +170,8 @@ const cmp = hoistCmp.factory<SegmentedControlModel>(({model, className, ...props
168
170
 
169
171
  const buttons = model.normalizedOptions.map(opt => {
170
172
  const optIntent = opt.intent ?? defaultIntent,
171
- selected = opt._key === selectedKey;
173
+ selected = opt._key === selectedKey,
174
+ optTestId = opt.testId ?? (testId ? `${testId}-${String(opt.value)}` : null);
172
175
  // Wrap the label so it can truncate with an ellipsis when the segment is too narrow,
173
176
  // rather than hard-clipping mid-character. Pass null for icon-only options so the Button
174
177
  // renders the icon alone (an empty span would suppress that).
@@ -187,7 +190,8 @@ const cmp = hoistCmp.factory<SegmentedControlModel>(({model, className, ...props
187
190
  selected && 'xh-segmented-control-option--selected',
188
191
  optIntent && `xh-segmented-control-option--${optIntent}`
189
192
  ),
190
- onClick: () => model.onValueChange(opt._key)
193
+ onClick: () => model.onValueChange(opt._key),
194
+ testId: optTestId
191
195
  });
192
196
  });
193
197
 
@@ -21,7 +21,7 @@ import {action, bindable, makeObservable, observable, override} from '@xh/hoist/
21
21
  import {debouncePromise, wait} from '@xh/hoist/promise';
22
22
  import {throwIf, withDefault, mergeDeep} from '@xh/hoist/utils/js';
23
23
  import {createObservableRef, getLayoutProps} from '@xh/hoist/utils/react';
24
- import {escapeRegExp, isEqual, isNil, isPlainObject, keyBy} from 'lodash';
24
+ import {escapeRegExp, isEqual, isNil, isPlainObject, unionWith} from 'lodash';
25
25
  import {Children, ReactNode, ReactPortal} from 'react';
26
26
  import ReactDom from 'react-dom';
27
27
  import './Select.scss';
@@ -157,6 +157,15 @@ export interface SelectProps extends HoistProps, HoistInputProps, LayoutProps {
157
157
 
158
158
  /** Field on provided options for sourcing each option's value (default `value`). */
159
159
  valueField?: string;
160
+
161
+ /**
162
+ * Function to generate a `SelectOption` for a (non-null) selected value not present in the
163
+ * current options list. Return null to fall back to the default value-as-label behavior.
164
+ *
165
+ * Useful with queryFn-based selects, readonly forms, or any case where options may not be
166
+ * loaded when a value is set, ensuring the value renders with its proper label.
167
+ */
168
+ generateOptionFn?: (value: any) => SelectOption;
160
169
  }
161
170
 
162
171
  /**
@@ -377,7 +386,8 @@ class SelectInputModel extends HoistInputModel {
377
386
 
378
387
  // Convert external value into option object(s). Options created if missing - this takes the
379
388
  // external value from the model, and we will respect that even if we don't know about it.
380
- // (Exception for a null value, which we will only accept if explicitly present in options.)
389
+ // (Exception for a null value, which is never synthesized - accepted only if provided via
390
+ // options.)
381
391
  override toInternal(external) {
382
392
  return this.findOption(external, !isNil(external));
383
393
  }
@@ -393,7 +403,10 @@ class SelectInputModel extends HoistInputModel {
393
403
  }
394
404
  }
395
405
 
396
- return createIfNotFound ? this.valueToOption(value) : null;
406
+ if (!createIfNotFound) return null;
407
+
408
+ // Value not among options - let the app generate an option for it, else synthesize one.
409
+ return this.componentProps.generateOptionFn?.(value) ?? this.valueToOption(value);
397
410
  }
398
411
 
399
412
  override toExternal(internal) {
@@ -453,15 +466,9 @@ class SelectInputModel extends HoistInputModel {
453
466
 
454
467
  // Carry forward and add to any existing internalOpts to allow our value
455
468
  // converters to continue all selected values in multiMode.
456
- const matchesByVal = keyBy(matchOpts, 'value'),
457
- newOpts = [...matchOpts];
458
-
459
- this.internalOptions.forEach(currOpt => {
460
- const matchOpt = matchesByVal[currOpt.value];
461
- if (!matchOpt) newOpts.push(currOpt); // avoiding dupes
462
- });
463
-
464
- this.internalOptions = newOpts;
469
+ this.internalOptions = unionWith(matchOpts, this.internalOptions, (a, b) =>
470
+ isEqual(a.value, b.value)
471
+ );
465
472
 
466
473
  // But only return the matching options back to the combo.
467
474
  return matchOpts;
@@ -484,8 +491,7 @@ class SelectInputModel extends HoistInputModel {
484
491
  // Option Rendering
485
492
  //----------------------
486
493
  formatOptionLabel = (opt, params) => {
487
- // Always display the standard label string in the value container (context == 'value').
488
- // If we need to expose customization here, we could consider a dedicated prop.
494
+ // Display the standard label string in the value container (context == 'value').
489
495
  if (params.context !== 'menu') {
490
496
  return opt.label;
491
497
  }
@@ -501,7 +507,7 @@ class SelectInputModel extends HoistInputModel {
501
507
  return div(opt.label);
502
508
  }
503
509
 
504
- return this.externalValue === opt.value
510
+ return isEqual(this.externalValue, opt.value)
505
511
  ? hbox({
506
512
  items: [
507
513
  div({
package/mobx/README.md CHANGED
@@ -45,7 +45,8 @@ The following are re-exported from MobX and mobx-react-lite:
45
45
 
46
46
  The package configures MobX with `enforceActions: 'observed'`, meaning any modification to
47
47
  an observable property that is currently being observed must occur inside an `@action` method,
48
- `runInAction()` block, or `@bindable` setter. MobX logs a warning if this rule is violated.
48
+ `runInAction()` block, `@bindable` setter, or a `reaction`/`when` run callback (MobX
49
+ auto-wraps these in an action). MobX logs a warning if this rule is violated.
49
50
 
50
51
  This enforcement prevents accidental state mutations and makes data flow predictable.
51
52
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xh/hoist",
3
- "version": "86.2.0",
3
+ "version": "86.4.0",
4
4
  "description": "Hoist add-on for building and deploying React Applications.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -38,8 +38,8 @@
38
38
  ]
39
39
  },
40
40
  "dependencies": {
41
- "@auth0/auth0-spa-js": "~2.21.1",
42
- "@azure/msal-browser": "~5.14.0",
41
+ "@auth0/auth0-spa-js": "~2.23.0",
42
+ "@azure/msal-browser": "~5.17.0",
43
43
  "@blueprintjs/core": "^6.3.2",
44
44
  "@blueprintjs/datetime": "^6.0.6",
45
45
  "@codemirror/commands": "^6.10.3",
@@ -90,7 +90,7 @@
90
90
  "semver": "~7.8.0",
91
91
  "short-unique-id": "~5.3.2",
92
92
  "store2": "~2.14.3",
93
- "swiper": "^12.1.2",
93
+ "swiper": "^14.0.0",
94
94
  "ts-morph": "^27.0.2",
95
95
  "tsx": "^4.21.0",
96
96
  "ua-parser-js": "~2.0.4",
@@ -118,7 +118,7 @@
118
118
  "react-dom": "~18.2.0",
119
119
  "stylelint": "17.x",
120
120
  "stylelint-config-standard-scss": "17.x",
121
- "type-fest": "4.x",
121
+ "type-fest": "5.x",
122
122
  "typescript": "~5.9.3"
123
123
  },
124
124
  "resolutions": {
@@ -30,9 +30,8 @@ export class PrefService extends HoistService {
30
30
  override telemetryPrefix = 'xh.client.prefs';
31
31
 
32
32
  static instance: PrefService;
33
-
34
- private _data = {};
35
- private _updates = {};
33
+ private _data: Record<string, PrefEntry> = {};
34
+ private _updates: Record<string, any> = {}; // undefined indicates unset
36
35
 
37
36
  override async initAsync(ctx: InitContext) {
38
37
  // Flush on page teardown while the page is still alive.
@@ -52,6 +51,17 @@ export class PrefService extends HoistService {
52
51
  return this._data.hasOwnProperty(key);
53
52
  }
54
53
 
54
+ /**
55
+ * Check whether the current user has an explicit value on file for the given preference, vs.
56
+ * receiving the preference's server-side default value.
57
+ *
58
+ * @param key - unique key used to identify the pref.
59
+ */
60
+ isSet(key: string): boolean {
61
+ this.ensureKeyExists(key);
62
+ return !!this._data[key].isSet;
63
+ }
64
+
55
65
  /**
56
66
  * Get the value for a given key, either the user-specific value (if set) or the default.
57
67
  * Typically accessed via the convenience alias {@link XH.getPref}.
@@ -91,7 +101,9 @@ export class PrefService extends HoistService {
91
101
 
92
102
  // Change local value to sanitized copy and fire.
93
103
  value = deepFreeze(cloneDeep(value));
94
- this._data[key].value = value;
104
+ const pref = this._data[key];
105
+ pref.value = value;
106
+ pref.isSet = true;
95
107
 
96
108
  // Schedule serialization to storage
97
109
  this._updates[key] = value;
@@ -99,11 +111,22 @@ export class PrefService extends HoistService {
99
111
  }
100
112
 
101
113
  /**
102
- * Restore a preference to its default value.
114
+ * Restore a preference to its default value, clearing the user's explicit value on the server.
115
+ *
116
+ * Unlike `set()`, this clears the user's explicit value rather than persisting the default as
117
+ * one - so {@link isSet} will report `false` afterwards. Saved asynchronously (see `set()`).
103
118
  */
104
119
  unset(key: string) {
105
- // TODO: round-trip this to the server as a proper unset?
106
- this.set(key, this._data[key]?.defaultValue);
120
+ this.ensureKeyExists(key);
121
+ const pref = this._data[key];
122
+ if (!pref.isSet && isEqual(pref.value, pref.defaultValue)) return;
123
+
124
+ pref.value = pref.defaultValue;
125
+ pref.isSet = false;
126
+
127
+ // Schedule serialization to storage
128
+ this._updates[key] = undefined;
129
+ this.pushPendingBuffered();
107
130
  }
108
131
 
109
132
  /**
@@ -131,18 +154,44 @@ export class PrefService extends HoistService {
131
154
  // Clear synchronously with the capture, so overlapping flushes cannot post twice.
132
155
  this._updates = {};
133
156
 
157
+ // Partition into value updates and unsets.
158
+ // On a core that predates unset support, fall back to persisting default
159
+ const setPrefs = {},
160
+ unsetKeys = [];
161
+ forEach(updates, (value, key) => {
162
+ const pref = this._data[key];
163
+ if (value !== undefined) {
164
+ setPrefs[key] = value;
165
+ } else if (pref.hasOwnProperty('isSet')) {
166
+ unsetKeys.push(key);
167
+ } else {
168
+ setPrefs[key] = pref.defaultValue;
169
+ }
170
+ });
171
+
134
172
  await this.runner()
135
- .span('set')
136
- .run(ctx =>
137
- terminationSafePostJson(
138
- {
139
- url: 'xh/setPrefs',
140
- body: updates,
141
- params: {clientUsername: XH.getUsername()}
142
- },
143
- ctx
144
- )
145
- );
173
+ .span('update')
174
+ .run(async ctx => {
175
+ const clientUsername = XH.getUsername(),
176
+ tasks = [];
177
+ if (!isEmpty(setPrefs)) {
178
+ tasks.push(
179
+ terminationSafePostJson(
180
+ {url: 'xh/setPrefs', body: setPrefs, params: {clientUsername}},
181
+ ctx
182
+ )
183
+ );
184
+ }
185
+ if (!isEmpty(unsetKeys)) {
186
+ tasks.push(
187
+ terminationSafePostJson(
188
+ {url: 'xh/unsetPrefs', body: unsetKeys, params: {clientUsername}},
189
+ ctx
190
+ )
191
+ );
192
+ }
193
+ await Promise.all(tasks);
194
+ });
146
195
  }
147
196
 
148
197
  //-------------------
@@ -172,9 +221,13 @@ export class PrefService extends HoistService {
172
221
  });
173
222
  }
174
223
 
175
- private validateBeforeSet(key, value) {
224
+ private ensureKeyExists(key: string) {
225
+ throwIf(!this.hasKey(key), `Preference key not found: '${key}'`);
226
+ }
227
+
228
+ private validateBeforeSet(key: string, value: any) {
229
+ this.ensureKeyExists(key);
176
230
  const pref = this._data[key];
177
- throwIf(!pref, `Cannot set preference ${key}: not found`);
178
231
  throwIf(value === undefined, `Cannot set preference ${key}: value not defined`);
179
232
  throwIf(
180
233
  !this.valueIsOfType(value, pref.type),
@@ -201,3 +254,10 @@ export class PrefService extends HoistService {
201
254
  }
202
255
  }
203
256
  }
257
+
258
+ interface PrefEntry {
259
+ type: string;
260
+ value: any;
261
+ defaultValue: any;
262
+ isSet: boolean;
263
+ }
package/svc/README.md CHANGED
@@ -186,6 +186,12 @@ XH.setPref('gridPageSize', 100);
186
186
 
187
187
  // Immediate save - no alias, access service directly
188
188
  await XH.prefService.pushAsync('criticalPref', value);
189
+
190
+ // Distinguish an explicit user value from the server-side default
191
+ if (XH.prefService.isSet('gridPageSize')) { /* user has customized this */ }
192
+
193
+ // Clear the user's value, reverting to the default (real server-side unset)
194
+ XH.prefService.unset('gridPageSize');
189
195
  ```
190
196
 
191
197
  Preferences are type-validated against server-defined types: `string`, `int`, `long`, `double`,
@@ -18,6 +18,15 @@ import moment, {Moment, MomentInput} from 'moment';
18
18
  * For efficiency and to enable strict equality checks, instances of this class are memoized:
19
19
  * only a single version of the object will be created and returned for each calendar day,
20
20
  * as long as the caller uses one of the *public factory methods*, which they always should!
21
+ *
22
+ * Instances serialize directly to their ISO date string (e.g. '2024-01-15') via built-in
23
+ * `toString()`, `valueOf()`, and `toJSON()` overrides. This means a LocalDate can be passed
24
+ * as-is within the params or body of a `FetchService` request (or any `JSON.stringify()` call)
25
+ * and will serialize as expected - prefer this over calling `toString()` or `format()` yourself.
26
+ *
27
+ * Instances also support natural comparison: because they are memoized, `===` tests whether two
28
+ * references are the same calendar day, while `valueOf()` returning the (lexically sortable) ISO
29
+ * string means the relational operators `<`, `>`, `<=`, `>=` order instances chronologically.
21
30
  */
22
31
  export class LocalDate {
23
32
  static readonly VALID_UNITS: Set<LocalDateUnit> = new Set([
@@ -77,7 +86,11 @@ export class LocalDate {
77
86
  return this.get(m.format('YYYY-MM-DD'));
78
87
  }
79
88
 
80
- /** LocalDate representing the current day. */
89
+ /**
90
+ * LocalDate representing the current day in the browser's local time zone.
91
+ * See `currentAppDay()` / `currentServerDay()` to resolve "today" in the app or server zone,
92
+ * which can differ from the browser for users in another region.
93
+ */
81
94
  static today(): LocalDate {
82
95
  return this.from(moment());
83
96
  }
@@ -120,18 +133,26 @@ export class LocalDate {
120
133
  return this._isoString;
121
134
  }
122
135
 
136
+ /** JS `Date` for this day at midnight in the browser's local time zone. Fresh instance per call. */
123
137
  get date(): Date {
124
138
  return new Date(this.timestamp);
125
139
  }
126
140
 
141
+ /** A mutable moment.js clone - safe to modify without affecting this (immutable) instance. */
127
142
  get moment(): Moment {
128
143
  return this._moment.clone();
129
144
  }
130
145
 
146
+ /** Epoch millis for this day at midnight in the browser's local time zone. */
131
147
  get timestamp(): number {
132
148
  return this._date.getTime();
133
149
  }
134
150
 
151
+ /**
152
+ * Format this date using moment.js format tokens, primarily for display.
153
+ * Note: to send a LocalDate to the server, pass the instance directly rather than a formatted
154
+ * string - it serializes to an ISO date on its own (see class-level docs).
155
+ */
135
156
  format(...args): string {
136
157
  return this._moment.format(...args);
137
158
  }
@@ -186,6 +207,7 @@ export class LocalDate {
186
207
  return this._isoString;
187
208
  }
188
209
 
210
+ // Returns the ISO string (not a number) so the relational operators sort instances by date.
189
211
  valueOf(): string {
190
212
  return this._isoString;
191
213
  }
@@ -319,6 +341,7 @@ export class LocalDate {
319
341
  return this.isWeekday ? this : this.previousWeekday();
320
342
  }
321
343
 
344
+ /** Difference between this date and `other` in the given unit; positive when this is later. */
322
345
  diff(other: LocalDate, unit: LocalDateUnit = 'days'): number {
323
346
  this.ensureUnitValid(unit);
324
347
  return this._moment.diff(other._moment, unit);
@@ -1,42 +0,0 @@
1
- /*
2
- * This file belongs to Hoist, an application development toolkit
3
- * developed by Extremely Heavy Industries (www.xh.io | info@xh.io)
4
- *
5
- * Copyright © 2026 Extremely Heavy Industries Inc.
6
- */
7
- .xh-popover-filter-chooser {
8
- & > .bp6-popover-target {
9
- display: flex;
10
- flex: 1;
11
- }
12
-
13
- // Extra class names required to override the default styles of the popover
14
- &__popover.bp6-popover.bp6-minimal {
15
- margin-top: -15px !important;
16
- box-shadow: none;
17
-
18
- .bp6-popover-content {
19
- background: transparent;
20
- }
21
-
22
- .xh-select__value-container--is-multi {
23
- height: unset;
24
- line-height: unset;
25
- }
26
- }
27
-
28
- &__filter-chooser {
29
- .xh-select__value-container--is-multi {
30
- overflow-y: hidden !important;
31
- }
32
-
33
- .xh-select {
34
- .xh-select__control--is-disabled {
35
- background: var(--xh-input-bg);
36
- }
37
- .xh-select__multi-value--is-disabled .xh-select__multi-value__label {
38
- color: unset;
39
- }
40
- }
41
- }
42
- }