@xh/hoist 86.0.1 → 86.2.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 (99) hide show
  1. package/CHANGELOG.md +104 -0
  2. package/admin/AdminJsonDisplay.ts +31 -0
  3. package/admin/App.scss +3 -11
  4. package/admin/jsonsearch/impl/JsonSearchImplModel.ts +4 -4
  5. package/admin/tabs/cluster/instances/connpool/ConnPoolMonitorPanel.ts +3 -11
  6. package/admin/tabs/cluster/instances/services/DetailsPanel.ts +2 -13
  7. package/admin/tabs/cluster/objects/DetailPanel.ts +3 -12
  8. package/admin/tabs/userData/roles/RoleModel.ts +1 -19
  9. package/appcontainer/RouterModel.ts +2 -0
  10. package/appcontainer/ThemeModel.ts +41 -0
  11. package/appcontainer/login/LoginPanelModel.ts +6 -2
  12. package/build/types/admin/AdminJsonDisplay.d.ts +7 -0
  13. package/build/types/admin/jsonsearch/impl/JsonSearchImplModel.d.ts +2 -2
  14. package/build/types/appcontainer/RouterModel.d.ts +1 -0
  15. package/build/types/appcontainer/ThemeModel.d.ts +9 -0
  16. package/build/types/cmp/grid/Grid.d.ts +6 -3
  17. package/build/types/cmp/grid/GridModel.d.ts +11 -0
  18. package/build/types/cmp/grid/filter/GridFilterFieldSpec.d.ts +24 -6
  19. package/build/types/cmp/input/SegmentedControlOption.d.ts +44 -0
  20. package/build/types/cmp/input/index.d.ts +1 -0
  21. package/build/types/core/AppSpec.d.ts +8 -2
  22. package/build/types/core/HoistAuthModel.d.ts +6 -3
  23. package/build/types/data/filter/BaseFilterFieldSpec.d.ts +7 -1
  24. package/build/types/data/filter/FieldFilter.d.ts +2 -0
  25. package/build/types/desktop/cmp/button/grid/ExpandToLevelButton.d.ts +2 -1
  26. package/build/types/desktop/cmp/filechooser/FileChooserModel.d.ts +1 -1
  27. package/build/types/desktop/cmp/input/CodeInput.d.ts +7 -0
  28. package/build/types/desktop/cmp/input/NumberInput.d.ts +1 -1
  29. package/build/types/desktop/cmp/input/SegmentedControl.d.ts +1 -41
  30. package/build/types/desktop/cmp/input/impl/CalcWindowedMenuWidth.d.ts +20 -0
  31. package/build/types/desktop/cmp/rest/impl/RestFormModel.d.ts +1 -1
  32. package/build/types/format/FormatNumber.d.ts +11 -3
  33. package/build/types/icon/Icon.d.ts +5 -3
  34. package/build/types/mobile/cmp/appOption/SizingModeAppOption.d.ts +3 -3
  35. package/build/types/mobile/cmp/appOption/ThemeAppOption.d.ts +3 -3
  36. package/build/types/mobile/cmp/button/grid/ExpandToLevelButton.d.ts +2 -1
  37. package/build/types/mobile/cmp/input/NumberInput.d.ts +1 -1
  38. package/build/types/mobile/cmp/input/SegmentedControl.d.ts +47 -0
  39. package/build/types/mobile/cmp/input/SwitchInput.d.ts +1 -1
  40. package/build/types/mobile/cmp/input/TextInput.d.ts +3 -0
  41. package/build/types/mobile/cmp/input/index.d.ts +1 -0
  42. package/build/types/svc/JsonBlobService.d.ts +7 -7
  43. package/cmp/error/ErrorMessage.scss +2 -0
  44. package/cmp/grid/Grid.ts +17 -0
  45. package/cmp/grid/GridModel.ts +17 -5
  46. package/cmp/grid/filter/GridFilterFieldSpec.ts +33 -7
  47. package/cmp/grid/impl/GridHScrollbar.ts +10 -3
  48. package/cmp/grid/impl/MenuSupport.ts +2 -4
  49. package/cmp/input/SegmentedControlOption.ts +61 -0
  50. package/cmp/input/index.ts +1 -0
  51. package/core/AppSpec.ts +9 -1
  52. package/core/HoistAuthModel.ts +13 -6
  53. package/data/Field.ts +3 -1
  54. package/data/filter/BaseFilterFieldSpec.ts +16 -2
  55. package/data/filter/FieldFilter.ts +90 -21
  56. package/desktop/appcontainer/LoginPanel.ts +5 -3
  57. package/desktop/cmp/button/grid/ExpandToLevelButton.ts +4 -5
  58. package/desktop/cmp/filechooser/FileChooserModel.ts +1 -1
  59. package/desktop/cmp/grid/editors/NumberEditor.ts +16 -17
  60. package/desktop/cmp/grid/editors/SelectEditor.ts +15 -10
  61. package/desktop/cmp/grid/impl/filter/GridFilterDialog.ts +2 -2
  62. package/desktop/cmp/grid/impl/filter/headerfilter/HeaderFilterModel.ts +9 -4
  63. package/desktop/cmp/grid/impl/filter/headerfilter/custom/CustomRowModel.ts +2 -2
  64. package/desktop/cmp/grid/impl/filter/headerfilter/values/ValuesTabModel.ts +46 -7
  65. package/desktop/cmp/input/CheckboxButton.ts +21 -1
  66. package/desktop/cmp/input/CodeInput.ts +20 -3
  67. package/desktop/cmp/input/NumberInput.ts +1 -1
  68. package/desktop/cmp/input/SegmentedControl.ts +8 -51
  69. package/desktop/cmp/input/Select.ts +13 -2
  70. package/desktop/cmp/input/impl/CalcWindowedMenuWidth.ts +106 -0
  71. package/desktop/cmp/rest/impl/RestFormField.ts +1 -1
  72. package/desktop/cmp/tab/Tabs.scss +78 -27
  73. package/desktop/cmp/tab/dynamic/DynamicTabSwitcher.ts +7 -3
  74. package/docs/error-handling.md +9 -0
  75. package/format/FormatNumber.ts +69 -32
  76. package/icon/Icon.scss +13 -0
  77. package/icon/Icon.ts +7 -3
  78. package/icon/impl/IconHtml.ts +1 -1
  79. package/icon/index.ts +8 -0
  80. package/mcp/data/ts-registry.ts +1 -1
  81. package/mobile/appcontainer/LoginPanel.scss +53 -11
  82. package/mobile/appcontainer/LoginPanel.ts +69 -44
  83. package/mobile/cmp/appOption/SizingModeAppOption.ts +5 -16
  84. package/mobile/cmp/appOption/ThemeAppOption.ts +8 -25
  85. package/mobile/cmp/button/grid/ExpandToLevelButton.ts +4 -5
  86. package/mobile/cmp/error/impl/ErrorMessage.ts +4 -8
  87. package/mobile/cmp/input/CheckboxButton.ts +18 -1
  88. package/mobile/cmp/input/NumberInput.ts +1 -1
  89. package/mobile/cmp/input/SegmentedControl.scss +126 -0
  90. package/mobile/cmp/input/SegmentedControl.ts +210 -0
  91. package/mobile/cmp/input/SwitchInput.ts +1 -1
  92. package/mobile/cmp/input/TextInput.scss +7 -0
  93. package/mobile/cmp/input/TextInput.ts +11 -1
  94. package/mobile/cmp/input/index.ts +1 -0
  95. package/mobile/cmp/tab/impl/Tabs.scss +8 -0
  96. package/mobile/cmp/toolbar/Toolbar.scss +8 -0
  97. package/package.json +7 -7
  98. package/styles/vars.scss +21 -1
  99. package/svc/JsonBlobService.ts +21 -20
@@ -0,0 +1,44 @@
1
+ import { Intent } from '@xh/hoist/core';
2
+ import { LocalDate } from '@xh/hoist/utils/datetime';
3
+ import { ReactElement } from 'react';
4
+ /** Primitive value types supported as a SegmentedControl option value/label. */
5
+ export type OptionPrimitive = string | number | boolean | LocalDate;
6
+ /**
7
+ * Option for a SegmentedControl, shared by the desktop and mobile implementations.
8
+ */
9
+ export interface SegmentedControlOption {
10
+ /** Value for this option. */
11
+ value: OptionPrimitive;
12
+ /** Display label. Defaults to `value.toString()` if omitted. */
13
+ label?: string;
14
+ /** Icon element, displayed before the label. */
15
+ icon?: ReactElement;
16
+ /** True to disable this individual option. */
17
+ disabled?: boolean;
18
+ /**
19
+ * Visual intent for this option - rendered as a solid fill when selected and as a subtle
20
+ * text-color hint when not (e.g. to flag a destructive choice). Overrides any control-level
21
+ * `intent` default. Defaults to the control's `intent`.
22
+ */
23
+ intent?: Intent;
24
+ }
25
+ /**
26
+ * Variant of SegmentedControlOption for representing a null/"no value" selection.
27
+ * Label is required to force use case to override default js 'null' toString rendering.
28
+ */
29
+ export interface SegmentedControlNullOption {
30
+ /** Null value for this option. */
31
+ value: null;
32
+ /** Display label - required for null options. */
33
+ label: string;
34
+ /** Icon element, displayed before the label. */
35
+ icon?: ReactElement;
36
+ /** True to disable this individual option. */
37
+ disabled?: boolean;
38
+ /**
39
+ * Visual intent for this option - rendered as a solid fill when selected and as a subtle
40
+ * text-color hint when not (e.g. to flag a destructive choice). Overrides any control-level
41
+ * `intent` default. Defaults to the control's `intent`.
42
+ */
43
+ intent?: Intent;
44
+ }
@@ -1,2 +1,3 @@
1
1
  export * from './HoistInputModel';
2
2
  export * from './HoistInputProps';
3
+ export * from './SegmentedControlOption';
@@ -1,5 +1,5 @@
1
1
  import { ElementFactory, HoistAppModel, HoistAuthModel, HoistProps } from '@xh/hoist/core';
2
- import { Component, ComponentClass, FunctionComponent } from 'react';
2
+ import { Component, ComponentClass, FunctionComponent, ReactElement } from 'react';
3
3
  /**
4
4
  * Spec for a client-side Hoist application. A config matching this class's shape is provided
5
5
  * to {@link XHApi.renderApp} to kick-off app rendering and is available thereafter as `XH.appSpec`.
@@ -98,6 +98,11 @@ export declare class AppSpec<T extends HoistAppModel = HoistAppModel> {
98
98
  lockoutPanel?: ElementFactory | FunctionComponent | Component;
99
99
  /** Optional message to show users when denied access to app. */
100
100
  lockoutMessage?: string;
101
+ /**
102
+ * Icon to display on the form-based login page, if active via `enableLoginForm: true`.
103
+ * Defaults to `Icon.shieldHalved()`.
104
+ */
105
+ loginPanelIcon?: ReactElement;
101
106
  /** Optional message to show on login form, if `showLoginForm: true`. */
102
107
  loginMessage?: string;
103
108
  /**
@@ -116,7 +121,7 @@ export declare class AppSpec<T extends HoistAppModel = HoistAppModel> {
116
121
  * initialized, including a breakdown of elapsed time throughout the init process.
117
122
  */
118
123
  trackAppLoad?: boolean;
119
- constructor({ authModelClass, checkAccess, clientAppCode, clientAppName, componentClass, containerClass, disableWebSockets, enableXssProtection, enableLoginForm, enableLogout, idlePanel, isMobileApp, lockoutMessage, lockoutPanel, loginMessage, modelClass, showBrowserContextMenu, trackAppLoad }: {
124
+ constructor({ authModelClass, checkAccess, clientAppCode, clientAppName, componentClass, containerClass, disableWebSockets, enableXssProtection, enableLoginForm, enableLogout, idlePanel, isMobileApp, lockoutMessage, lockoutPanel, loginMessage, loginPanelIcon, modelClass, showBrowserContextMenu, trackAppLoad }: {
120
125
  authModelClass?: typeof HoistAuthModel;
121
126
  checkAccess: any;
122
127
  clientAppCode?: string;
@@ -132,6 +137,7 @@ export declare class AppSpec<T extends HoistAppModel = HoistAppModel> {
132
137
  lockoutMessage?: any;
133
138
  lockoutPanel?: any;
134
139
  loginMessage?: any;
140
+ loginPanelIcon?: any;
135
141
  modelClass: any;
136
142
  showBrowserContextMenu?: boolean;
137
143
  trackAppLoad?: boolean;
@@ -43,21 +43,24 @@ export declare class HoistAuthModel extends HoistModel {
43
43
  * Process a manual login, submitted by user via form.
44
44
  * @returns identity of the user authenticated with the server; null if not yet authenticated.
45
45
  */
46
- loginWithCredentialsAsync(username: string, password: string): Promise<IdentityInfo>;
46
+ loginWithCredentialsAsync(username: string, password: string, ctx?: CallContextLike): Promise<IdentityInfo>;
47
47
  /**
48
48
  * Logout this user with the backend server.
49
49
  *
50
50
  * The default implementation will call the 'logout' endpoint on the Grails server, clearing
51
51
  * any server-side session state there. Override to manage any client-side or third-party state.
52
52
  */
53
- logoutAsync(): Promise<void>;
53
+ logoutAsync(ctx?: CallContextLike): Promise<void>;
54
54
  /**
55
55
  * Load auth-related config from the server via an /xh/ endpoint that is specifically
56
56
  * whitelisted by Hoist to allow access prior to user authentication. For use in bootstrapping
57
57
  * client-side auth solutions that require configs such as OAuth endpoint URLs and client IDs.
58
58
  * See `BaseAuthenticationService.getClientConfig()` in hoist-core.
59
+ *
60
+ * @param ctx - tracing/load context. When called from within a `completeAuthAsync` override,
61
+ * forward the supplied context so this fetch nests under the bootstrap trace.
59
62
  */
60
- loadConfigAsync(): Promise<PlainObject>;
63
+ loadConfigAsync(ctx?: CallContextLike): Promise<PlainObject>;
61
64
  /**
62
65
  * Create a client-side HoistUser.
63
66
  *
@@ -13,7 +13,12 @@ import { FieldFilterOperator } from './Types';
13
13
  export interface BaseFilterFieldSpecConfig {
14
14
  /** Identifying field name to filter on. */
15
15
  field: string;
16
- /** Type of field, will default from related field on source if provided, or 'auto'. */
16
+ /**
17
+ * Type of field, will default from related field on source if provided, or 'auto'. A `date`
18
+ * (timestamp) source defaults to 'localDate' so filtering compares by calendar day (range and
19
+ * equality operators use full-day bounds). Set explicitly to 'date' to filter by exact
20
+ * timestamp instead.
21
+ */
17
22
  fieldType?: FieldType;
18
23
  /** DisplayName, will default from related field on source if provided */
19
24
  displayName?: string;
@@ -78,5 +83,6 @@ export declare abstract class BaseFilterFieldSpec extends HoistBase {
78
83
  abstract loadValuesFromSource(): any;
79
84
  private parseOperators;
80
85
  private getDefaultOperators;
86
+ private get isLocalDateFilteringTimestamp();
81
87
  private get isEnumerableByDefault();
82
88
  }
@@ -26,6 +26,8 @@ export declare class FieldFilter extends Filter {
26
26
  */
27
27
  constructor({ field, op, value, valueType }: FieldFilterSpec);
28
28
  getTestFn(store?: Store): FilterTestFn;
29
+ private getOpFn;
30
+ private getDayBoundedOpFn;
29
31
  equals(other: Filter): boolean;
30
32
  toJSON(): FieldFilterSpec;
31
33
  removeFieldFilters(field?: string): Filter;
@@ -13,6 +13,7 @@ export interface ExpandToLevelButtonProps extends Omit<ButtonProps, 'title'> {
13
13
  }
14
14
  /**
15
15
  * A menu button to expand a multi-level grouped or tree grid out to a desired level.
16
- * Requires {@link GridConfig.levelLabels} to be configured on the bound GridModel.
16
+ * Requires {@link GridConfig.levelLabels} to be configured on the bound GridModel - the menu offers
17
+ * one entry per labelled level, so a partial array will limit the available expand-to targets.
17
18
  */
18
19
  export declare const ExpandToLevelButton: import("react").FC<ExpandToLevelButtonProps>, expandToLevelButton: import("@xh/hoist/core").ElementFactory<ExpandToLevelButtonProps>;
@@ -65,7 +65,7 @@ export declare class FileChooserModel extends HoistModel {
65
65
  private readonly onFileRejected;
66
66
  private readonly rejectToastMessage;
67
67
  private readonly rejectToastSpec;
68
- constructor(config: FileChooserConfig);
68
+ constructor(config?: FileChooserConfig);
69
69
  /** Open the file browser programmatically. Typically used in a button's onClick callback.*/
70
70
  openFileBrowser(): void;
71
71
  /**
@@ -4,6 +4,13 @@ import './CodeInput.scss';
4
4
  export interface CodeInputProps extends HoistProps, HoistInputProps, LayoutProps {
5
5
  /** True to focus the control on render. */
6
6
  autoFocus?: boolean;
7
+ /**
8
+ * True to automatically format content for display using the configured `formatter`.
9
+ * Defaults to true for `readonly` inputs - set false to opt out. May also be enabled on
10
+ * editable inputs, in which case content is formatted on blur (never mid-edit, so user
11
+ * edits and cursor position are preserved while typing).
12
+ */
13
+ autoFormat?: boolean;
7
14
  /** False to not commit on every change/keystroke, default true. */
8
15
  commitOnChange?: boolean;
9
16
  /**
@@ -36,7 +36,7 @@ export interface NumberInputProps extends HoistProps, LayoutProps, StyleProps, H
36
36
  onKeyDown?: KeyboardEventHandler<HTMLInputElement>;
37
37
  /** Text to display when control is empty. */
38
38
  placeholder?: string;
39
- /** Max decimal precision of the value, defaults to 4. */
39
+ /** Max decimal precision of the value, defaults to 4. Set to null for full, unrestricted precision. */
40
40
  precision?: NumericPrecision;
41
41
  /** Element to display inline on the right side of the input. */
42
42
  rightElement?: ReactNode;
@@ -1,10 +1,7 @@
1
- import { HoistInputProps } from '@xh/hoist/cmp/input';
1
+ import { HoistInputProps, OptionPrimitive, SegmentedControlNullOption, SegmentedControlOption } from '@xh/hoist/cmp/input';
2
2
  import { HoistProps, Intent } from '@xh/hoist/core';
3
3
  import '@xh/hoist/desktop/register';
4
- import { LocalDate } from '@xh/hoist/utils/datetime';
5
- import { ReactElement } from 'react';
6
4
  import './SegmentedControl.scss';
7
- type OptionPrimitive = string | number | boolean | LocalDate;
8
5
  export interface SegmentedControlProps extends HoistProps, HoistInputProps {
9
6
  /** True to render in a compact mode with reduced sizing for space-constrained contexts. */
10
7
  compact?: boolean;
@@ -31,42 +28,6 @@ export interface SegmentedControlProps extends HoistProps, HoistInputProps {
31
28
  */
32
29
  outlined?: boolean;
33
30
  }
34
- export interface SegmentedControlOption {
35
- /** Value for this option. */
36
- value: OptionPrimitive;
37
- /** Display label. Defaults to `value.toString()` if omitted. */
38
- label?: string;
39
- /** Icon element, displayed before the label. */
40
- icon?: ReactElement;
41
- /** True to disable this individual option. */
42
- disabled?: boolean;
43
- /**
44
- * Visual intent for this option - rendered as a solid fill when selected and as a subtle
45
- * text-color hint when not (e.g. to flag a destructive choice). Overrides any control-level
46
- * `intent` default. Defaults to the control's `intent`.
47
- */
48
- intent?: Intent;
49
- }
50
- /**
51
- * Variant of SegmentedControlOption for representing a null/"no value" selection.
52
- * Label is required to force use case to override default js 'null' toString rendering.
53
- */
54
- export interface SegmentedControlNullOption {
55
- /** Null value for this option. */
56
- value: null;
57
- /** Display label - required for null options. */
58
- label: string;
59
- /** Icon element, displayed before the label. */
60
- icon?: ReactElement;
61
- /** True to disable this individual option. */
62
- disabled?: boolean;
63
- /**
64
- * Visual intent for this option - rendered as a solid fill when selected and as a subtle
65
- * text-color hint when not (e.g. to flag a destructive choice). Overrides any control-level
66
- * `intent` default. Defaults to the control's `intent`.
67
- */
68
- intent?: Intent;
69
- }
70
31
  /**
71
32
  * An input for selecting a single value from a small set of mutually exclusive options,
72
33
  * rendered as a group of toggle buttons with clear visual indication of the active
@@ -79,4 +40,3 @@ export interface SegmentedControlNullOption {
79
40
  * Built on Blueprint's SegmentedControl component.
80
41
  */
81
42
  export declare const SegmentedControl: import("react").FC<SegmentedControlProps>, segmentedControl: import("@xh/hoist/core").ElementFactory<SegmentedControlProps>;
82
- export {};
@@ -0,0 +1,20 @@
1
+ import { ReactNode } from 'react';
2
+ /**
3
+ * Compute an explicit pixel width for a windowed (virtualized) `Select` menu.
4
+ *
5
+ * Windowed menus can't auto-size via CSS - react-window absolutely-positions rows at width:100%,
6
+ * so option content never widens the menu and it collapses to the control width. This restores
7
+ * content-based sizing using Canvas + off-screen DOM, the same render-and-measure approach as the
8
+ * grid's `ColumnWidthCalculator`: render each option's actual menu markup (via the configured
9
+ * `optionRenderer`, if any), canvas-estimate its width to pick the widest candidates, then measure
10
+ * only those candidates' displayed width in a hidden probe - so custom `optionRenderer`s (icons,
11
+ * multi-element content, etc.) size correctly. Unlike the grid (which may autosize many columns
12
+ * over thousands of records), this renders one menu's options once per options change.
13
+ *
14
+ * @param options - normalized (possibly grouped) Select options.
15
+ * @param renderOption - renders an option to its menu `ReactNode` (e.g. model.formatOptionLabel).
16
+ * @param portal - shared menu portal div, used to host probes so menu CSS (font, padding) applies.
17
+ * @returns explicit menu width in px, or null to fall back to the control width.
18
+ * @internal
19
+ */
20
+ export declare function calcWindowedMenuWidth(options: any[], renderOption: (opt: any) => ReactNode, portal: HTMLElement): number;
@@ -20,7 +20,7 @@ export declare class RestFormModel extends HoistModel {
20
20
  del?: import("react").ReactNode | ((recs: import("@xh/hoist/data").StoreRecord[]) => import("react").ReactNode);
21
21
  edit?: import("react").ReactNode | ((recs: import("@xh/hoist/data").StoreRecord[]) => import("react").ReactNode);
22
22
  };
23
- get actions(): (import("@xh/hoist/data").RecordActionSpec | import("@xh/hoist/data").RecordAction)[];
23
+ get actions(): (import("@xh/hoist/data").RecordAction | import("@xh/hoist/data").RecordActionSpec)[];
24
24
  get editors(): RestGridEditor[];
25
25
  get gridModel(): import("../../../../cmp/grid").GridModel;
26
26
  get store(): import("@xh/hoist/desktop/cmp/rest").RestStore;
@@ -33,8 +33,9 @@ export interface NumberFormatOptions extends Omit<FormatOptions<number>, 'toolti
33
33
  */
34
34
  omitFourDigitComma?: boolean;
35
35
  /**
36
- * Desired number of decimal places, or 'auto' (default) to adjust the displayed precision
37
- * automatically based on the scale of the value.
36
+ * Desired number of decimal places, 'auto' (default) to adjust the displayed precision
37
+ * automatically based on the scale of the value, or null for full, unrestricted precision
38
+ * (capped at the max supported precision, with trailing zeros trimmed).
38
39
  */
39
40
  precision?: Precision;
40
41
  /** Prefix to prepend to value (between the number and its sign). */
@@ -65,13 +66,20 @@ export interface NumberFormatOptions extends Omit<FormatOptions<number>, 'toolti
65
66
  *
66
67
  * e.g. `{precision:4, zeroPad:2}` will format `1.2` → "1.20" and `1.234` → "1.234"
67
68
  *
68
- * Default is true if a fixed precision is set, false if precision is 'auto'.
69
+ * Default is true if a fixed precision is set, false if precision is 'auto' or null (full).
69
70
  */
70
71
  zeroPad?: ZeroPad;
71
72
  }
72
73
  export interface QuantityFormatOptions extends NumberFormatOptions {
74
+ /** True to compact values \>= 1 million into units of millions (m). Default true. */
73
75
  useMillions?: boolean;
76
+ /** True to compact values \>= 1 billion into units of billions (b). Default true. */
74
77
  useBillions?: boolean;
78
+ /**
79
+ * True to compact to m/b units only when no precision is lost, else render the full value.
80
+ * Default false.
81
+ */
82
+ lossless?: boolean;
75
83
  }
76
84
  /** Config for pos/neg/neutral color classes. */
77
85
  export interface ColorSpec {
@@ -3,6 +3,7 @@ import { FontAwesomeIconProps } from '@fortawesome/react-fontawesome';
3
3
  import { HoistProps, Intent, Thunkable } from '@xh/hoist/core';
4
4
  import { ReactElement } from 'react';
5
5
  import { SetRequired } from 'type-fest';
6
+ import './Icon.scss';
6
7
  export interface IconProps extends HoistProps, Partial<Omit<FontAwesomeIconProps, 'ref'>> {
7
8
  /** Name of the icon in FontAwesome. */
8
9
  iconName?: IconName;
@@ -232,6 +233,7 @@ export declare const Icon: {
232
233
  settings(p?: IconProps): any;
233
234
  shield(p?: IconProps): any;
234
235
  shieldCheck(p?: IconProps): any;
236
+ shieldHalved(p?: IconProps): any;
235
237
  sigma(p?: IconProps): any;
236
238
  skull(p?: IconProps): any;
237
239
  slashedCircle(p?: IconProps): any;
@@ -332,9 +334,9 @@ export declare const Icon: {
332
334
  filename: string;
333
335
  }): any;
334
336
  /**
335
- * Returns an empty div with FA sizing classes applied. Can be used to take up room in a layout
336
- * where an icon might otherwise go - e.g. to align a series of menu items, where some items do
337
- * not have an icon but others do.
337
+ * Returns an empty div sized to occupy the width of a standard icon. Can be used to take up
338
+ * room in a layout where an icon might otherwise go - e.g. to align a series of menu items,
339
+ * where some items do not have an icon but others do.
338
340
  */
339
341
  placeholder(opts?: IconProps): any;
340
342
  };
@@ -1,5 +1,5 @@
1
1
  import { AppOptionSpec, SizingMode } from '@xh/hoist/core';
2
- import { ButtonGroupInputProps } from '@xh/hoist/mobile/cmp/input';
2
+ import { SegmentedControlProps } from '@xh/hoist/mobile/cmp/input';
3
3
  import { FormFieldProps } from '../form/FormField';
4
4
  import '@xh/hoist/mobile/register';
5
5
  interface SizingModeAppOptionSpec {
@@ -7,8 +7,8 @@ interface SizingModeAppOptionSpec {
7
7
  modes?: SizingMode[];
8
8
  /** Props for nested FormField. */
9
9
  formFieldProps?: Partial<FormFieldProps>;
10
- /** Props for nested ButtonGroupInput. */
11
- inputProps?: Partial<ButtonGroupInputProps>;
10
+ /** Props for nested SegmentedControl. */
11
+ inputProps?: Partial<SegmentedControlProps>;
12
12
  }
13
13
  /**
14
14
  * Convenience configuration for the `sizingMode` AppOption.
@@ -1,12 +1,12 @@
1
1
  import { AppOptionSpec } from '@xh/hoist/core';
2
2
  import { FormFieldProps } from '@xh/hoist/mobile/cmp/form';
3
- import { ButtonGroupInputProps } from '@xh/hoist/mobile/cmp/input';
3
+ import { SegmentedControlProps } from '@xh/hoist/mobile/cmp/input';
4
4
  import '@xh/hoist/mobile/register';
5
5
  interface ThemeAppOptionSpec {
6
6
  /** Props for nested FormField */
7
7
  formFieldProps?: Partial<FormFieldProps>;
8
- /** Props for nested ButtonGroupInput */
9
- inputProps?: Partial<ButtonGroupInputProps>;
8
+ /** Props for nested SegmentedControl */
9
+ inputProps?: Partial<SegmentedControlProps>;
10
10
  }
11
11
  /**
12
12
  * Convenience configuration for the `theme` AppOption.
@@ -10,6 +10,7 @@ export interface ExpandToLevelButtonProps extends MenuButtonProps {
10
10
  }
11
11
  /**
12
12
  * A menu button to expand a multi-level grouped or tree grid out to a desired level.
13
- * Requires {@link GridConfig.levelLabels} to be configured on the bound GridModel.
13
+ * Requires {@link GridConfig.levelLabels} to be configured on the bound GridModel - the menu offers
14
+ * one entry per labelled level, so a partial array will limit the available expand-to targets.
14
15
  */
15
16
  export declare const ExpandToLevelButton: import("react").FC<ExpandToLevelButtonProps>, expandToLevelButton: import("@xh/hoist/core").ElementFactory<ExpandToLevelButtonProps>;
@@ -26,7 +26,7 @@ export interface NumberInputProps extends HoistProps, HoistInputProps, StyleProp
26
26
  modifier?: string;
27
27
  /** Text to display when control is empty. */
28
28
  placeholder?: string;
29
- /** Max decimal precision of the value, defaults to 4. */
29
+ /** Max decimal precision of the value, defaults to 4. Set to null for full, unrestricted precision. */
30
30
  precision?: NumericPrecision;
31
31
  /**
32
32
  * Scale factor to apply when converting between the internal and external value. Useful for
@@ -0,0 +1,47 @@
1
+ import { HoistInputProps, OptionPrimitive, SegmentedControlNullOption, SegmentedControlOption } from '@xh/hoist/cmp/input';
2
+ import { HoistProps, Intent } from '@xh/hoist/core';
3
+ import '@xh/hoist/mobile/register';
4
+ import './SegmentedControl.scss';
5
+ export interface SegmentedControlProps extends HoistProps, HoistInputProps {
6
+ /**
7
+ * True (default) to render all segments at an equal width when {@link fill} is enabled,
8
+ * dividing the available width into equal parts regardless of label length - the conventional
9
+ * segmented-control appearance. Set false to instead size each segment to its content and
10
+ * share only the leftover space, so a longer label yields a wider segment. No effect when
11
+ * `fill` is false. Either way, a label too wide for its segment truncates with an ellipsis.
12
+ */
13
+ equalSegmentWidths?: boolean;
14
+ /**
15
+ * True (default) to stretch the control to fill available width,
16
+ * distributing space equally among options.
17
+ */
18
+ fill?: boolean;
19
+ /**
20
+ * Default visual intent applied to the selected option, and as a subtle text-color hint
21
+ * to options when not selected. Serves as the default for any option that does not specify
22
+ * its own `intent`. Defaults to `'none'`.
23
+ */
24
+ intent?: 'none' | Intent;
25
+ /**
26
+ * Array of available options. Each entry may be a SegmentedControlOption object
27
+ * with value/label/icon/disabled properties, or a primitive value used as both
28
+ * the value and the display label.
29
+ */
30
+ options: Array<SegmentedControlOption | SegmentedControlNullOption | OptionPrimitive>;
31
+ /**
32
+ * True to render with an outlined style - a border around the control tray
33
+ * with no inner background fill. Border color follows the current intent.
34
+ */
35
+ outlined?: boolean;
36
+ }
37
+ /**
38
+ * An input for selecting a single value from a small set of mutually exclusive options,
39
+ * rendered as a group of toggle buttons with clear visual indication of the active
40
+ * selection.
41
+ *
42
+ * Similar to ButtonGroupInput but driven by an `options` prop (like Select) rather than Button
43
+ * children, and with stronger visual differentiation between selected and unselected states.
44
+ * The mobile counterpart to the desktop SegmentedControl, built on Hoist's mobile Button
45
+ * (no Blueprint dependency).
46
+ */
47
+ export declare const SegmentedControl: import("react").FC<SegmentedControlProps>, segmentedControl: import("@xh/hoist/core").ElementFactory<SegmentedControlProps>;
@@ -3,7 +3,7 @@ import { HoistProps, StyleProps } from '@xh/hoist/core';
3
3
  import '@xh/hoist/mobile/register';
4
4
  import './SwitchInput.scss';
5
5
  export interface SwitchInputProps extends HoistProps, HoistInputProps, StyleProps {
6
- value?: string;
6
+ value?: boolean;
7
7
  /** Onsen modifier string */
8
8
  modifier?: string;
9
9
  }
@@ -2,6 +2,7 @@ import { HoistInputProps } from '@xh/hoist/cmp/input';
2
2
  import { HoistProps, LayoutProps, StyleProps } from '@xh/hoist/core';
3
3
  import '@xh/hoist/mobile/register';
4
4
  import type { Property } from 'csstype';
5
+ import { ReactElement } from 'react';
5
6
  import './TextInput.scss';
6
7
  export interface TextInputProps extends HoistProps, HoistInputProps, StyleProps, LayoutProps {
7
8
  value?: string;
@@ -27,6 +28,8 @@ export interface TextInputProps extends HoistProps, HoistInputProps, StyleProps,
27
28
  commitOnChange?: boolean;
28
29
  /** True to show a "clear" button aligned to the right of the control. Defaults to false. */
29
30
  enableClear?: boolean;
31
+ /** Icon to display inline on the left side of the input. */
32
+ leftIcon?: ReactElement;
30
33
  /** Onsen modifier string */
31
34
  modifier?: string;
32
35
  /** Text to display when control is empty */
@@ -5,6 +5,7 @@ export * from './CheckboxButton';
5
5
  export * from './DateInput';
6
6
  export * from './NumberInput';
7
7
  export * from './SearchInput';
8
+ export * from './SegmentedControl';
8
9
  export * from './Select';
9
10
  export * from './SwitchInput';
10
11
  export * from './TextInput';
@@ -41,21 +41,21 @@ export declare class JsonBlobService extends HoistService {
41
41
  telemetryPrefix: string;
42
42
  static instance: JsonBlobService;
43
43
  /** Retrieve a single JSONBlob by its unique token. */
44
- getAsync(token: string): Promise<JsonBlob>;
44
+ getAsync(token: string, ctx?: CallContextLike): Promise<JsonBlob>;
45
45
  /** Retrieve all blobs of a particular type that are visible to the current user. */
46
46
  listAsync(spec: {
47
47
  type: string;
48
48
  includeValue?: boolean;
49
- ctx: CallContextLike;
49
+ ctx?: CallContextLike;
50
50
  }): Promise<JsonBlob[]>;
51
51
  /** Persist a new JSONBlob back to the server. */
52
- createAsync({ acl, description, type, meta, name, value }: Partial<JsonBlob>): Promise<JsonBlob>;
52
+ createAsync({ acl, description, type, meta, name, value }: Partial<JsonBlob>, ctx?: CallContextLike): Promise<JsonBlob>;
53
53
  /** Modify mutable properties of an existing JSONBlob, as identified by its unique token. */
54
- updateAsync(token: string, update: Partial<JsonBlob>): Promise<JsonBlob>;
54
+ updateAsync(token: string, update: Partial<JsonBlob>, ctx?: CallContextLike): Promise<JsonBlob>;
55
55
  /** Create or update a blob for a user with the existing type and name. */
56
- createOrUpdateAsync(type: string, name: string, data: Partial<JsonBlob>): Promise<JsonBlob>;
56
+ createOrUpdateAsync(type: string, name: string, data: Partial<JsonBlob>, ctx?: CallContextLike): Promise<JsonBlob>;
57
57
  /** Find a blob owned by this user with a specific type and name. If none exists, return null. */
58
- findAsync(type: string, name: string): Promise<JsonBlob>;
58
+ findAsync(type: string, name: string, ctx?: CallContextLike): Promise<JsonBlob>;
59
59
  /** Archive (soft-delete) an existing JSONBlob, as identified by its unique token. */
60
- archiveAsync(token: string): Promise<JsonBlob>;
60
+ archiveAsync(token: string, ctx?: CallContextLike): Promise<JsonBlob>;
61
61
  }
@@ -36,5 +36,7 @@
36
36
  }
37
37
 
38
38
  .xh-mobile .xh-error-message__inner {
39
+ flex: 1;
40
+ max-width: none;
39
41
  margin: 0 var(--xh-pad-px);
40
42
  }
package/cmp/grid/Grid.ts CHANGED
@@ -48,6 +48,7 @@ import {consumeEvent, isDisplayed, logWithDebug} from '@xh/hoist/utils/js';
48
48
  import {composeRefs, createObservableRef, getLayoutProps} from '@xh/hoist/utils/react';
49
49
  import classNames from 'classnames';
50
50
  import {compact, debounce, isBoolean, isEmpty, isEqual, isNil, max, maxBy, merge} from 'lodash';
51
+ import {type MouseEvent} from 'react';
51
52
  import './Grid.scss';
52
53
  import {GridModel} from './GridModel';
53
54
  import {columnGroupHeader} from './impl/ColumnGroupHeader';
@@ -135,6 +136,7 @@ export const [Grid, grid] = hoistCmp.withFactory<GridProps>({
135
136
  ],
136
137
  testId,
137
138
  onKeyDown: impl.onKeyDown,
139
+ onMouseDown: impl.onViewMouseDown,
138
140
  ref: composeRefs(impl.viewRef, model.viewRef, ref)
139
141
  }),
140
142
  colChooserModel ? platformColChooser({model: colChooserModel}) : null,
@@ -151,6 +153,10 @@ export const [Grid, grid] = hoistCmp.withFactory<GridProps>({
151
153
  export class GridLocalModel extends HoistModel {
152
154
  override xhImpl = true;
153
155
 
156
+ // Structural "empty" grid space.
157
+ private static EMPTY_SPACE_SELECTOR =
158
+ '.ag-body-viewport, .ag-center-cols-viewport, .ag-center-cols-container, .ag-row';
159
+
154
160
  @lookup(GridModel)
155
161
  private model: GridModel;
156
162
  agOptions: GridOptions;
@@ -844,6 +850,17 @@ export class GridLocalModel extends HoistModel {
844
850
  return this.rowKeyNavSupport?.navigateToNextCell(agParams);
845
851
  };
846
852
 
853
+ // `stopEditingWhenCellsLoseFocus` doesn't fire on clicks in empty grid space (focus stays in
854
+ // the grid), so commit the active edit on those clicks ourselves. Require exact match on empty
855
+ // space to avoid interfering with cell editors.
856
+ onViewMouseDown = (evt: MouseEvent) => {
857
+ const {model} = this,
858
+ target = evt.target as HTMLElement;
859
+ if (model.isEditing && target.matches(GridLocalModel.EMPTY_SPACE_SELECTOR)) {
860
+ model.agApi?.stopEditing();
861
+ }
862
+ };
863
+
847
864
  onCellMouseDown = evt => {
848
865
  const {model} = this;
849
866
  if (model.highlightRowOnClick) {
@@ -305,6 +305,10 @@ export interface GridConfig {
305
305
  * expand/collapse options in the default context menu will be enhanced to allow users to
306
306
  * expand/collapse to a specific level. See {@link GroupingChooserModel.valueDisplayNames}
307
307
  * for a convenient getter that will satisfy this API when a GroupingChooser is in play.
308
+ *
309
+ * Labels are matched to levels top-down and need not cover the full depth of the grid - provide
310
+ * a partial array to label only the top levels (e.g. when deeper levels should not be
311
+ * expand-to targets). Deeper, unlabelled levels are omitted from the menu.
308
312
  */
309
313
  levelLabels?: Thunkable<string[]>;
310
314
 
@@ -1174,15 +1178,23 @@ export class GridModel extends HoistModel {
1174
1178
 
1175
1179
  /**
1176
1180
  * Get the resolved level labels for the current state of the grid.
1181
+ * An over-long array is truncated to the current `maxDepth`.
1177
1182
  */
1178
1183
  get resolvedLevelLabels(): string[] {
1179
1184
  const {maxDepth, levelLabels} = this,
1180
1185
  ret = executeIfFunction(levelLabels);
1181
- if (ret && ret.length < maxDepth + 1) {
1182
- this.logDebug('Value produced by `GridModel.levelLabels` has insufficient length.');
1183
- return null;
1184
- }
1185
- return ret ? take(ret, maxDepth + 1) : null;
1186
+ return !isEmpty(ret) ? take(ret, maxDepth + 1) : null;
1187
+ }
1188
+
1189
+ /**
1190
+ * True if the given `resolvedLevelLabels` index is the grid's current expand level - used to
1191
+ * mark the active item in the "Expand to..." menu. The deepest labelled level counts as current
1192
+ * whenever the grid is expanded to or beyond it.
1193
+ */
1194
+ isCurrentExpandLevel(idx: number): boolean {
1195
+ const {expandLevel, resolvedLevelLabels} = this,
1196
+ lastIdx = resolvedLevelLabels?.length - 1;
1197
+ return expandLevel === idx || (idx === lastIdx && expandLevel > lastIdx);
1186
1198
  }
1187
1199
 
1188
1200
  /**