@xh/hoist 86.0.1 → 86.1.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 (59) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/admin/tabs/userData/roles/RoleModel.ts +1 -19
  3. package/appcontainer/RouterModel.ts +2 -0
  4. package/appcontainer/ThemeModel.ts +41 -0
  5. package/appcontainer/login/LoginPanelModel.ts +6 -2
  6. package/build/types/appcontainer/RouterModel.d.ts +1 -0
  7. package/build/types/appcontainer/ThemeModel.d.ts +9 -0
  8. package/build/types/cmp/input/SegmentedControlOption.d.ts +44 -0
  9. package/build/types/cmp/input/index.d.ts +1 -0
  10. package/build/types/core/AppSpec.d.ts +8 -2
  11. package/build/types/core/HoistAuthModel.d.ts +6 -3
  12. package/build/types/data/filter/BaseFilterFieldSpec.d.ts +7 -1
  13. package/build/types/data/filter/FieldFilter.d.ts +2 -0
  14. package/build/types/desktop/cmp/filechooser/FileChooserModel.d.ts +1 -1
  15. package/build/types/desktop/cmp/input/SegmentedControl.d.ts +1 -41
  16. package/build/types/icon/Icon.d.ts +1 -0
  17. package/build/types/mobile/cmp/appOption/SizingModeAppOption.d.ts +3 -3
  18. package/build/types/mobile/cmp/appOption/ThemeAppOption.d.ts +3 -3
  19. package/build/types/mobile/cmp/input/SegmentedControl.d.ts +47 -0
  20. package/build/types/mobile/cmp/input/SwitchInput.d.ts +1 -1
  21. package/build/types/mobile/cmp/input/TextInput.d.ts +3 -0
  22. package/build/types/mobile/cmp/input/index.d.ts +1 -0
  23. package/build/types/svc/JsonBlobService.d.ts +7 -7
  24. package/cmp/error/ErrorMessage.scss +2 -0
  25. package/cmp/grid/filter/GridFilterFieldSpec.ts +5 -2
  26. package/cmp/grid/impl/GridHScrollbar.ts +10 -3
  27. package/cmp/input/SegmentedControlOption.ts +61 -0
  28. package/cmp/input/index.ts +1 -0
  29. package/core/AppSpec.ts +9 -1
  30. package/core/HoistAuthModel.ts +13 -6
  31. package/data/Field.ts +3 -1
  32. package/data/filter/BaseFilterFieldSpec.ts +16 -2
  33. package/data/filter/FieldFilter.ts +90 -21
  34. package/desktop/appcontainer/LoginPanel.ts +5 -3
  35. package/desktop/cmp/filechooser/FileChooserModel.ts +1 -1
  36. package/desktop/cmp/grid/impl/filter/headerfilter/HeaderFilterModel.ts +9 -4
  37. package/desktop/cmp/grid/impl/filter/headerfilter/custom/CustomRowModel.ts +2 -2
  38. package/desktop/cmp/input/SegmentedControl.ts +8 -51
  39. package/desktop/cmp/tab/Tabs.scss +6 -2
  40. package/desktop/cmp/tab/dynamic/DynamicTabSwitcher.ts +7 -3
  41. package/docs/error-handling.md +9 -0
  42. package/icon/Icon.ts +3 -0
  43. package/icon/index.ts +8 -0
  44. package/mobile/appcontainer/LoginPanel.scss +53 -11
  45. package/mobile/appcontainer/LoginPanel.ts +69 -44
  46. package/mobile/cmp/appOption/SizingModeAppOption.ts +5 -16
  47. package/mobile/cmp/appOption/ThemeAppOption.ts +8 -25
  48. package/mobile/cmp/error/impl/ErrorMessage.ts +4 -8
  49. package/mobile/cmp/input/SegmentedControl.scss +126 -0
  50. package/mobile/cmp/input/SegmentedControl.ts +210 -0
  51. package/mobile/cmp/input/SwitchInput.ts +1 -1
  52. package/mobile/cmp/input/TextInput.scss +7 -0
  53. package/mobile/cmp/input/TextInput.ts +11 -1
  54. package/mobile/cmp/input/index.ts +1 -0
  55. package/mobile/cmp/tab/impl/Tabs.scss +8 -0
  56. package/mobile/cmp/toolbar/Toolbar.scss +8 -0
  57. package/package.json +2 -2
  58. package/styles/vars.scss +6 -1
  59. package/svc/JsonBlobService.ts +21 -20
package/CHANGELOG.md CHANGED
@@ -1,5 +1,59 @@
1
1
  # Changelog
2
2
 
3
+ ## 86.1.0 - 2026-06-22
4
+
5
+ ### 🎁 New Features
6
+
7
+ * Added a mobile `SegmentedControl` input - the mobile counterpart to the desktop component, with a
8
+ matching `options`-driven API for selecting a single value from a set of mutually exclusive
9
+ choices. Built on Hoist's mobile `Button` (no Blueprint dependency).
10
+ * The shared `SegmentedControlOption` option types now export from `@xh/hoist/cmp/input` rather
11
+ than the desktop `SegmentedControl` module; update any direct type imports.
12
+ * Added a `leftIcon` prop to mobile `TextInput`.
13
+ * `FilterChooser` and grid column filters on a timestamp (`date`) field now filter by calendar day,
14
+ comparing against full-day bounds for range and equality operators rather than midnight - e.g.
15
+ `> 2023-05-31` excludes the 31st and `= 2023-05-31` matches any time that day. Filter specs now
16
+ default a `date` source field to `fieldType: 'localDate'`; set `fieldType: 'date'` for exact
17
+ timestamps. Applications using workarounds to provide similar behavior may be able to unwind that
18
+ behavior and rely on Hoist default behavior.
19
+
20
+ ### 🐞 Bug Fixes
21
+
22
+ * Fixed the "Is blank" / "Is not blank" grid column filters for `tags`-typed fields - empty tag
23
+ arrays now correctly match "Is blank", and such filters are edited on the Custom tab rather than
24
+ producing a phantom blank entry in the Values list.
25
+ * Fixed an issue where `Grid` column headers could fall out of sync with body content during
26
+ horizontal scrolling when both `enableFullWidthScroll` and `useVirtualColumns` were enabled.
27
+ * Updated `DynamicTabSwitcher` to properly apply `testId` passed down by `TabContainer`.
28
+ * Ensure publication of `router5-plugin-browser` TS module augmentation.
29
+ * Set an explicit `%` unit on the `flex-basis: 0` of `TabContainer`'s flex shorthand to ensure
30
+ that the `0` is not interpreted as a `0px` basis and that the container sizes as expected.
31
+ * ⚠️Apps that upgrade to `hoist-dev-utils v13.x` and use `flex: 1 1 0` or `flex-basis: 0` should
32
+ verify that their flex layouts continue to work as expected and add an explicit unit if
33
+ not (e.g. `flex: 1 1 0%` or `flex-basis: 0%`).
34
+
35
+ ### ⚙️ Technical
36
+
37
+ * `JsonBlobService` and `HoistAuthModel` now accept an optional `CallContextLike` argument on all of
38
+ their public methods, allowing callers to nest their fetches within an existing trace.
39
+
40
+ ### ✨ Styles
41
+
42
+ * Mobile `Button` now defaults to a more touch-friendly height (40px, up from 28px) and rounder
43
+ corners for buttons in body content. Toolbar buttons derive their height from the toolbar size so
44
+ a single token drives both, landing slightly taller than before (34px, up from 28px).
45
+ * `SegmentedControl` adopts the same standard height for a consistent control row.
46
+ * ⚠️ Mobile buttons outside toolbars are now taller - verify body / form / panel layouts that
47
+ pair tightly with button dimensions.
48
+ * Mobile tab content (`TabContainer`) now takes the themed app background, so content reads
49
+ correctly in dark mode rather than showing through to Onsen's light default page background.
50
+ * Applied the active theme's `color-scheme` onto `html` and added a `theme-color` meta tag matching
51
+ the active theme's app-bar color, so browser chrome and overscroll / safe-area regions might
52
+ better match the theme.
53
+
54
+ ### 📚 Libraries
55
+
56
+ * @azure/msal-browser `5.13 → 5.14`
3
57
 
4
58
  ## 86.0.1 - 2026-06-16
5
59
 
@@ -14,6 +68,10 @@
14
68
  the page is hidden or unloaded, reacting to `XH.pageState` and issuing the flush via
15
69
  `fetch({keepalive: true})` (replacing the less reliable `beforeunload` + normal-fetch approach).
16
70
 
71
+ ### 📚 Libraries
72
+
73
+ * @azure/msal-browser `5.11 → 5.13`
74
+ * mobx `6.15 → 6.16`
17
75
 
18
76
  ## 86.0.0 - 2026-06-12
19
77
 
@@ -12,12 +12,10 @@ import {fragment, p} from '@xh/hoist/cmp/layout';
12
12
  import {CallContext, HoistModel, LoadSpec, managed, XH} from '@xh/hoist/core';
13
13
  import {RecordActionSpec} from '@xh/hoist/data';
14
14
  import {actionCol, calcActionColWidth} from '@xh/hoist/desktop/cmp/grid';
15
- import {fmtDate} from '@xh/hoist/format';
16
15
  import {Icon} from '@xh/hoist/icon';
17
16
  import {action, bindable, makeObservable, observable, runInAction} from '@xh/hoist/mobx';
18
17
  import {wait} from '@xh/hoist/promise';
19
18
  import {compact, groupBy, mapValues} from 'lodash';
20
- import moment from 'moment/moment';
21
19
  import {RoleEditorModel} from './editor/RoleEditorModel';
22
20
  import {HoistRole, RoleModuleConfig} from './Types';
23
21
 
@@ -397,23 +395,7 @@ export class RoleModel extends HoistModel {
397
395
  config.directoryGroupsSupported && 'effectiveDirectoryGroupNames',
398
396
  'effectiveRoleNames',
399
397
  'lastUpdatedBy',
400
- {
401
- field: 'lastUpdated',
402
- example: 'YYYY-MM-DD',
403
- valueParser: (v, op) => {
404
- let ret = moment(v, ['YYYY-MM-DD', 'YYYYMMDD'], true);
405
- if (!ret.isValid()) return null;
406
-
407
- // Note special handling for '>' & '<=' queries.
408
- if (['>', '<='].includes(op)) {
409
- ret = moment(ret).endOf('day');
410
- }
411
-
412
- return ret.toDate();
413
- },
414
- valueRenderer: v => fmtDate(v),
415
- ops: ['>', '>=', '<', '<=']
416
- }
398
+ 'lastUpdated'
417
399
  ]),
418
400
  persistWith: {...RoleModel.PERSIST_WITH, path: 'mainFilterChooser'}
419
401
  });
@@ -10,6 +10,8 @@ import {mergeDeep} from '@xh/hoist/utils/js';
10
10
  import {isOmitted} from '@xh/hoist/utils/impl';
11
11
  import {createRouter, Router, State} from 'router5';
12
12
  import browserPlugin from 'router5-plugin-browser';
13
+ // Required so downstream consumers pick up Router TS augmentation (buildUrl, etc.).
14
+ import 'router5-plugin-browser';
13
15
 
14
16
  /**
15
17
  * Top-level model for managing application routing in Hoist.
@@ -31,7 +31,14 @@ export class ThemeModel extends HoistModel {
31
31
  const classList = document.body.classList;
32
32
  classList.toggle('xh-dark', value);
33
33
  classList.toggle('bp6-dark', value);
34
+
35
+ // Set color-scheme on the document root (<html>) so browser chrome and overscroll / safe-area
36
+ // regions match the theme - the theme class only reaches <body>, leaving the root light.
37
+ // See https://developer.mozilla.org/en-US/docs/Web/CSS/color-scheme
38
+ document.documentElement.style.colorScheme = value ? 'dark' : 'light';
39
+
34
40
  this.darkTheme = value;
41
+ this.syncThemeColorMeta();
35
42
  }
36
43
 
37
44
  @action
@@ -64,4 +71,38 @@ export class ThemeModel extends HoistModel {
64
71
  }
65
72
  });
66
73
  }
74
+
75
+ /**
76
+ * Sync the `<meta name="theme-color">` tag to the active theme's app-bar color, creating it if
77
+ * needed. This tints browser chrome to match the app - e.g. Android Chrome's status bar and task
78
+ * switcher, and desktop installed PWAs. Hoist's theme is independent of the OS color scheme, so
79
+ * the tag's content must be updated on each theme change rather than relying on the static
80
+ * `media="(prefers-color-scheme)"` form. Note Safari 26+ ignores `theme-color`, instead deriving
81
+ * its chrome color from the page background (see the companion `color-scheme` handling above).
82
+ */
83
+ private syncThemeColorMeta() {
84
+ // Resolve --xh-appbar-bg (a chain of var() fallbacks) to a concrete color via a throwaway
85
+ // probe - reading computed `color` fully substitutes the var chain.
86
+ const probe = document.createElement('div');
87
+ probe.style.cssText = 'display: none; color: var(--xh-appbar-bg)';
88
+ document.body.appendChild(probe);
89
+ const color = window.getComputedStyle(probe).color;
90
+ probe.remove();
91
+
92
+ // Bail if the color failed to resolve (e.g. styles not yet applied) rather than write a
93
+ // bad value - the next theme change will sync it.
94
+ if (!color) return;
95
+
96
+ // Maintain our own tag, marked so we can find it again on subsequent theme changes. If an
97
+ // app has hand-authored any theme-color tag, defer to it entirely.
98
+ let meta = document.querySelector('meta[name=theme-color][data-xh-managed]');
99
+ if (!meta) {
100
+ if (document.querySelector('meta[name=theme-color]')) return;
101
+ meta = document.createElement('meta');
102
+ meta.setAttribute('name', 'theme-color');
103
+ meta.setAttribute('data-xh-managed', '');
104
+ document.head.appendChild(meta);
105
+ }
106
+ meta.setAttribute('content', color);
107
+ }
67
108
  }
@@ -34,8 +34,12 @@ export class LoginPanelModel extends HoistModel {
34
34
  // Debounce to defend against double-click fast enough to get through masking + button disable.
35
35
  @debounced(300)
36
36
  async submitAsync() {
37
- const {username, password, loginTask, isValid} = this;
38
- if (!isValid) return;
37
+ const {username, password, loginTask, isValid, loginInProgress} = this;
38
+ if (loginInProgress) return;
39
+ if (!isValid) {
40
+ this.warning = 'Please enter a username and password.';
41
+ return;
42
+ }
39
43
 
40
44
  try {
41
45
  this.loginInProgress = true;
@@ -1,5 +1,6 @@
1
1
  import { HoistModel } from '../core';
2
2
  import { Router, State } from 'router5';
3
+ import 'router5-plugin-browser';
3
4
  /**
4
5
  * Top-level model for managing application routing in Hoist.
5
6
  *
@@ -10,4 +10,13 @@ export declare class ThemeModel extends HoistModel {
10
10
  setDarkTheme(value: boolean): void;
11
11
  setTheme(value: Theme, persist?: boolean): void;
12
12
  init(): void;
13
+ /**
14
+ * Sync the `<meta name="theme-color">` tag to the active theme's app-bar color, creating it if
15
+ * needed. This tints browser chrome to match the app - e.g. Android Chrome's status bar and task
16
+ * switcher, and desktop installed PWAs. Hoist's theme is independent of the OS color scheme, so
17
+ * the tag's content must be updated on each theme change rather than relying on the static
18
+ * `media="(prefers-color-scheme)"` form. Note Safari 26+ ignores `theme-color`, instead deriving
19
+ * its chrome color from the page background (see the companion `color-scheme` handling above).
20
+ */
21
+ private syncThemeColorMeta;
13
22
  }
@@ -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;
@@ -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
  /**
@@ -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 {};
@@ -232,6 +232,7 @@ export declare const Icon: {
232
232
  settings(p?: IconProps): any;
233
233
  shield(p?: IconProps): any;
234
234
  shieldCheck(p?: IconProps): any;
235
+ shieldHalved(p?: IconProps): any;
235
236
  sigma(p?: IconProps): any;
236
237
  skull(p?: IconProps): any;
237
238
  slashedCircle(p?: IconProps): any;
@@ -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.
@@ -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
  }
@@ -87,10 +87,13 @@ export class GridFilterFieldSpec extends BaseFilterFieldSpec {
87
87
  // Values from current column filter. `flatMap` here unwraps the array `parseVal` returns
88
88
  // for `tags`-typed fields, lining those values up with the scalar values produced by
89
89
  // `Store.getValuesForFieldFilter` above. Without this, a filter value like `'foo'` would
90
- // survive as `['foo']` and dedupe incorrectly.
90
+ // survive as `['foo']` and dedupe incorrectly. The `filter` drops the non-selectable null
91
+ // value carried by a blank `tags` filter.
91
92
  const colFilterVals = flatMap(columnFilters, filter => {
92
93
  return castArray(filter.value).flatMap(val => sourceField.parseVal(val));
93
- }).map(it => this.toDisplayValue(it));
94
+ })
95
+ .filter(it => sourceField.type !== 'tags' || it != null)
96
+ .map(it => this.toDisplayValue(it));
94
97
 
95
98
  // Combine + unique - these are all values that *could* be shown in the filter UI.
96
99
  const allValues = uniqBy([...allSrcVals, ...colFilterVals], this.getUniqueValue);