@xh/hoist 86.0.0 → 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.
- package/CHANGELOG.md +73 -0
- package/admin/tabs/userData/roles/RoleModel.ts +1 -19
- package/appcontainer/RouterModel.ts +2 -0
- package/appcontainer/ThemeModel.ts +41 -0
- package/appcontainer/login/LoginPanelModel.ts +6 -2
- package/build/types/appcontainer/RouterModel.d.ts +1 -0
- package/build/types/appcontainer/ThemeModel.d.ts +9 -0
- package/build/types/cmp/input/SegmentedControlOption.d.ts +44 -0
- package/build/types/cmp/input/index.d.ts +1 -0
- package/build/types/core/AppSpec.d.ts +8 -2
- package/build/types/core/HoistAuthModel.d.ts +6 -3
- package/build/types/data/filter/BaseFilterFieldSpec.d.ts +7 -1
- package/build/types/data/filter/FieldFilter.d.ts +2 -0
- package/build/types/desktop/cmp/filechooser/FileChooserModel.d.ts +1 -1
- package/build/types/desktop/cmp/input/SegmentedControl.d.ts +1 -41
- package/build/types/icon/Icon.d.ts +1 -0
- package/build/types/mobile/cmp/appOption/SizingModeAppOption.d.ts +3 -3
- package/build/types/mobile/cmp/appOption/ThemeAppOption.d.ts +3 -3
- package/build/types/mobile/cmp/input/SegmentedControl.d.ts +47 -0
- package/build/types/mobile/cmp/input/SwitchInput.d.ts +1 -1
- package/build/types/mobile/cmp/input/TextInput.d.ts +3 -0
- package/build/types/mobile/cmp/input/index.d.ts +1 -0
- package/build/types/svc/JsonBlobService.d.ts +7 -7
- package/build/types/svc/PrefService.d.ts +4 -3
- package/build/types/svc/TraceService.d.ts +8 -6
- package/build/types/svc/TrackService.d.ts +5 -3
- package/build/types/svc/impl/Fetch.d.ts +13 -0
- package/build/types/utils/js/TestUtils.d.ts +1 -1
- package/build/types/utils/react/LayoutPropUtils.d.ts +1 -1
- package/build/types/utils/react/ReactUtils.d.ts +1 -1
- package/cmp/error/ErrorMessage.scss +2 -0
- package/cmp/grid/filter/GridFilterFieldSpec.ts +5 -2
- package/cmp/grid/impl/GridHScrollbar.ts +10 -3
- package/cmp/input/SegmentedControlOption.ts +61 -0
- package/cmp/input/index.ts +1 -0
- package/core/AppSpec.ts +9 -1
- package/core/HoistAuthModel.ts +13 -6
- package/data/Field.ts +3 -1
- package/data/filter/BaseFilterFieldSpec.ts +16 -2
- package/data/filter/FieldFilter.ts +90 -21
- package/desktop/appcontainer/LoginPanel.ts +5 -3
- package/desktop/cmp/filechooser/FileChooserModel.ts +1 -1
- package/desktop/cmp/filter/FilterChooser.scss +0 -5
- package/desktop/cmp/grid/impl/filter/headerfilter/HeaderFilterModel.ts +9 -4
- package/desktop/cmp/grid/impl/filter/headerfilter/custom/CustomRowModel.ts +2 -2
- package/desktop/cmp/input/SegmentedControl.ts +8 -51
- package/desktop/cmp/tab/Tabs.scss +6 -2
- package/desktop/cmp/tab/dynamic/DynamicTabSwitcher.ts +7 -3
- package/docs/error-handling.md +9 -0
- package/icon/Icon.ts +3 -0
- package/icon/index.ts +8 -0
- package/mobile/appcontainer/LoginPanel.scss +53 -11
- package/mobile/appcontainer/LoginPanel.ts +69 -44
- package/mobile/cmp/appOption/SizingModeAppOption.ts +5 -16
- package/mobile/cmp/appOption/ThemeAppOption.ts +8 -25
- package/mobile/cmp/error/impl/ErrorMessage.ts +4 -8
- package/mobile/cmp/input/SegmentedControl.scss +126 -0
- package/mobile/cmp/input/SegmentedControl.ts +210 -0
- package/mobile/cmp/input/SwitchInput.ts +1 -1
- package/mobile/cmp/input/TextInput.scss +7 -0
- package/mobile/cmp/input/TextInput.ts +11 -1
- package/mobile/cmp/input/index.ts +1 -0
- package/mobile/cmp/tab/impl/Tabs.scss +8 -0
- package/mobile/cmp/toolbar/Toolbar.scss +8 -0
- package/package.json +4 -4
- package/styles/vars.scss +6 -1
- package/svc/JsonBlobService.ts +21 -20
- package/svc/PrefService.ts +20 -10
- package/svc/TraceService.ts +53 -41
- package/svc/TrackService.ts +21 -10
- package/svc/impl/Fetch.ts +33 -0
- package/utils/js/ClipboardUtils.ts +2 -2
- package/utils/js/TestUtils.ts +1 -1
- package/utils/react/LayoutPropUtils.ts +1 -1
- package/utils/react/ReactUtils.ts +1 -1
|
@@ -4,22 +4,25 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Copyright © 2026 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
HoistInputModel,
|
|
9
|
+
HoistInputProps,
|
|
10
|
+
OptionPrimitive,
|
|
11
|
+
SegmentedControlNullOption,
|
|
12
|
+
SegmentedControlOption,
|
|
13
|
+
useHoistInputModel
|
|
14
|
+
} from '@xh/hoist/cmp/input';
|
|
8
15
|
import {div} from '@xh/hoist/cmp/layout';
|
|
9
16
|
import {hoistCmp, HoistProps, Intent} from '@xh/hoist/core';
|
|
10
17
|
import '@xh/hoist/desktop/register';
|
|
11
18
|
import {bpSegmentedControl} from '@xh/hoist/kit/blueprint';
|
|
12
19
|
import {computed, makeObservable} from '@xh/hoist/mobx';
|
|
13
|
-
import {LocalDate} from '@xh/hoist/utils/datetime';
|
|
14
20
|
import {getLayoutProps, getNonLayoutProps} from '@xh/hoist/utils/react';
|
|
15
21
|
import {TEST_ID} from '@xh/hoist/utils/js';
|
|
16
22
|
import classNames from 'classnames';
|
|
17
23
|
import {filter, isObject} from 'lodash';
|
|
18
|
-
import {ReactElement} from 'react';
|
|
19
24
|
import './SegmentedControl.scss';
|
|
20
25
|
|
|
21
|
-
type OptionPrimitive = string | number | boolean | LocalDate;
|
|
22
|
-
|
|
23
26
|
export interface SegmentedControlProps extends HoistProps, HoistInputProps {
|
|
24
27
|
/** True to render in a compact mode with reduced sizing for space-constrained contexts. */
|
|
25
28
|
compact?: boolean;
|
|
@@ -51,52 +54,6 @@ export interface SegmentedControlProps extends HoistProps, HoistInputProps {
|
|
|
51
54
|
outlined?: boolean;
|
|
52
55
|
}
|
|
53
56
|
|
|
54
|
-
export interface SegmentedControlOption {
|
|
55
|
-
/** Value for this option. */
|
|
56
|
-
value: OptionPrimitive;
|
|
57
|
-
|
|
58
|
-
/** Display label. Defaults to `value.toString()` if omitted. */
|
|
59
|
-
label?: string;
|
|
60
|
-
|
|
61
|
-
/** Icon element, displayed before the label. */
|
|
62
|
-
icon?: ReactElement;
|
|
63
|
-
|
|
64
|
-
/** True to disable this individual option. */
|
|
65
|
-
disabled?: boolean;
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Visual intent for this option - rendered as a solid fill when selected and as a subtle
|
|
69
|
-
* text-color hint when not (e.g. to flag a destructive choice). Overrides any control-level
|
|
70
|
-
* `intent` default. Defaults to the control's `intent`.
|
|
71
|
-
*/
|
|
72
|
-
intent?: Intent;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Variant of SegmentedControlOption for representing a null/"no value" selection.
|
|
77
|
-
* Label is required to force use case to override default js 'null' toString rendering.
|
|
78
|
-
*/
|
|
79
|
-
export interface SegmentedControlNullOption {
|
|
80
|
-
/** Null value for this option. */
|
|
81
|
-
value: null;
|
|
82
|
-
|
|
83
|
-
/** Display label - required for null options. */
|
|
84
|
-
label: string;
|
|
85
|
-
|
|
86
|
-
/** Icon element, displayed before the label. */
|
|
87
|
-
icon?: ReactElement;
|
|
88
|
-
|
|
89
|
-
/** True to disable this individual option. */
|
|
90
|
-
disabled?: boolean;
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Visual intent for this option - rendered as a solid fill when selected and as a subtle
|
|
94
|
-
* text-color hint when not (e.g. to flag a destructive choice). Overrides any control-level
|
|
95
|
-
* `intent` default. Defaults to the control's `intent`.
|
|
96
|
-
*/
|
|
97
|
-
intent?: Intent;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
57
|
/**
|
|
101
58
|
* An input for selecting a single value from a small set of mutually exclusive options,
|
|
102
59
|
* rendered as a group of toggle buttons with clear visual indication of the active
|
|
@@ -10,13 +10,17 @@
|
|
|
10
10
|
max-width: 100%;
|
|
11
11
|
max-height: 100%;
|
|
12
12
|
flex-direction: column;
|
|
13
|
-
|
|
13
|
+
//noinspection CssRedundantUnit
|
|
14
|
+
flex: 1 1 0%;
|
|
15
|
+
// % unit used to avoid browser fallback to px,
|
|
16
|
+
// which prevents container expansion
|
|
14
17
|
|
|
15
18
|
.xh-tab-wrapper {
|
|
16
19
|
display: flex;
|
|
17
20
|
min-width: 0;
|
|
18
21
|
min-height: 0;
|
|
19
|
-
|
|
22
|
+
//noinspection CssRedundantUnit
|
|
23
|
+
flex: 1 1 0%;
|
|
20
24
|
}
|
|
21
25
|
}
|
|
22
26
|
|
|
@@ -24,7 +24,7 @@ import {Icon} from '@xh/hoist/icon';
|
|
|
24
24
|
import {tooltip as bpTooltip, showContextMenu} from '@xh/hoist/kit/blueprint';
|
|
25
25
|
import {dragDropContext, draggable, droppable} from '@xh/hoist/kit/react-beautiful-dnd';
|
|
26
26
|
import {wait} from '@xh/hoist/promise';
|
|
27
|
-
import {consumeEvent} from '@xh/hoist/utils/js';
|
|
27
|
+
import {consumeEvent, getTestId} from '@xh/hoist/utils/js';
|
|
28
28
|
import {composeRefs, getLayoutProps} from '@xh/hoist/utils/react';
|
|
29
29
|
import classNames from 'classnames';
|
|
30
30
|
import {first, isFinite, last} from 'lodash';
|
|
@@ -41,13 +41,14 @@ export const [DynamicTabSwitcher, dynamicTabSwitcher] = hoistCmp.withFactory<Tab
|
|
|
41
41
|
className: 'xh-dynamic-tab-switcher',
|
|
42
42
|
displayName: 'DynamicTabSwitcher',
|
|
43
43
|
model: uses(TabContainerModel),
|
|
44
|
-
render({className, orientation, ...props}) {
|
|
44
|
+
render({className, orientation, testId, ...props}) {
|
|
45
45
|
const impl = useLocalModel(DynamicTabSwitcherLocalModel);
|
|
46
46
|
return scroller({
|
|
47
47
|
className: classNames(className, impl.isVertical && `${className}--vertical`),
|
|
48
48
|
content: tabs,
|
|
49
49
|
contentProps: {localModel: impl},
|
|
50
50
|
orientation: ['left', 'right'].includes(orientation) ? 'vertical' : 'horizontal',
|
|
51
|
+
testId,
|
|
51
52
|
...getLayoutProps(props)
|
|
52
53
|
});
|
|
53
54
|
}
|
|
@@ -127,7 +128,8 @@ const tabCmp = hoistCmp.factory<TabProps>(({tab, index, localModel, model}) => {
|
|
|
127
128
|
{disabled, icon, tooltip} = tab,
|
|
128
129
|
isFavorite = model.isTabFavorite(tab.id),
|
|
129
130
|
{isVertical, props} = localModel,
|
|
130
|
-
{tabWidth, tabMinWidth, tabMaxWidth} = props
|
|
131
|
+
{tabWidth, tabMinWidth, tabMaxWidth} = props,
|
|
132
|
+
tabTestId = getTestId(props, tab.id);
|
|
131
133
|
|
|
132
134
|
// Handle tab sizing props
|
|
133
135
|
const tabStyle: CSSProperties = {};
|
|
@@ -172,6 +174,7 @@ const tabCmp = hoistCmp.factory<TabProps>(({tab, index, localModel, model}) => {
|
|
|
172
174
|
);
|
|
173
175
|
},
|
|
174
176
|
ref: composeRefs(provided.innerRef, tabRef),
|
|
177
|
+
testId: tabTestId,
|
|
175
178
|
...provided.draggableProps,
|
|
176
179
|
...provided.dragHandleProps,
|
|
177
180
|
style: getStyles(isVertical, provided.draggableProps.style),
|
|
@@ -199,6 +202,7 @@ const tabCmp = hoistCmp.factory<TabProps>(({tab, index, localModel, model}) => {
|
|
|
199
202
|
button({
|
|
200
203
|
className:
|
|
201
204
|
'xh-dynamic-tab-switcher__tabs__tab__close-button',
|
|
205
|
+
testId: getTestId(tabTestId, 'remove-btn'),
|
|
202
206
|
icon: Icon.x({size: 'sm'}),
|
|
203
207
|
title: 'Remove Tab',
|
|
204
208
|
minimal: true,
|
package/docs/error-handling.md
CHANGED
|
@@ -490,6 +490,15 @@ The logged payload includes:
|
|
|
490
490
|
- Whether the user was shown an alert
|
|
491
491
|
- An optional user-provided message (via the "Report" dialog)
|
|
492
492
|
|
|
493
|
+
> **Routing client errors to a chat system:** Because reported errors land server-side as
|
|
494
|
+
> `'Client Error'` track entries, a Grails service can forward them to a realtime chat system
|
|
495
|
+
> (Slack, Teams, etc.) by subscribing to the `xhTrackReceived` cluster topic - no client changes
|
|
496
|
+
> required. XH's [Toolbox](https://github.com/xh/toolbox) demo app
|
|
497
|
+
> [includes a `SlackAlertService` that does exactly this](https://github.com/xh/toolbox/blob/develop/grails-app/services/io/xh/toolbox/SlackAlertService.groovy),
|
|
498
|
+
> posting client errors (alongside monitor alerts and user feedback) to Slack. See the hoist-core
|
|
499
|
+
> [activity tracking](https://github.com/xh/hoist-core/blob/develop/docs/activity-tracking.md) docs
|
|
500
|
+
> for the server-side topic mechanism.
|
|
501
|
+
|
|
493
502
|
### Sensitive Data Redaction
|
|
494
503
|
|
|
495
504
|
The handler automatically redacts values at paths listed in
|
package/icon/Icon.ts
CHANGED
|
@@ -651,6 +651,9 @@ export const Icon = {
|
|
|
651
651
|
shieldCheck(p?: IconProps) {
|
|
652
652
|
return Icon.icon({...p, iconName: 'shield-check'});
|
|
653
653
|
},
|
|
654
|
+
shieldHalved(p?: IconProps) {
|
|
655
|
+
return Icon.icon({...p, iconName: 'shield-halved'});
|
|
656
|
+
},
|
|
654
657
|
sigma(p?: IconProps) {
|
|
655
658
|
return Icon.icon({...p, iconName: 'sigma'});
|
|
656
659
|
},
|
package/icon/index.ts
CHANGED
|
@@ -176,6 +176,7 @@ import {
|
|
|
176
176
|
faServer as faServerLight,
|
|
177
177
|
faShieldAlt as faShieldAltLight,
|
|
178
178
|
faShieldCheck as faShieldCheckLight,
|
|
179
|
+
faShieldHalved as faShieldHalvedLight,
|
|
179
180
|
faSigma as faSigmaLight,
|
|
180
181
|
faSignIn as faSignInLight,
|
|
181
182
|
faSignOut as faSignOutLight,
|
|
@@ -392,6 +393,7 @@ import {
|
|
|
392
393
|
faServer,
|
|
393
394
|
faShieldAlt,
|
|
394
395
|
faShieldCheck,
|
|
396
|
+
faShieldHalved,
|
|
395
397
|
faSigma,
|
|
396
398
|
faSignIn,
|
|
397
399
|
faSignOut,
|
|
@@ -608,6 +610,7 @@ import {
|
|
|
608
610
|
faServer as faServerSolid,
|
|
609
611
|
faShieldAlt as faShieldAltSolid,
|
|
610
612
|
faShieldCheck as faShieldCheckSolid,
|
|
613
|
+
faShieldHalved as faShieldHalvedSolid,
|
|
611
614
|
faSigma as faSigmaSolid,
|
|
612
615
|
faSignIn as faSignInSolid,
|
|
613
616
|
faSignOut as faSignOutSolid,
|
|
@@ -825,6 +828,7 @@ import {
|
|
|
825
828
|
faServer as faServerThin,
|
|
826
829
|
faShieldAlt as faShieldAltThin,
|
|
827
830
|
faShieldCheck as faShieldCheckThin,
|
|
831
|
+
faShieldHalved as faShieldHalvedThin,
|
|
828
832
|
faSigma as faSigmaThin,
|
|
829
833
|
faSignIn as faSignInThin,
|
|
830
834
|
faSignOut as faSignOutThin,
|
|
@@ -1540,6 +1544,10 @@ library.add(
|
|
|
1540
1544
|
faShieldCheckLight,
|
|
1541
1545
|
faShieldCheckSolid,
|
|
1542
1546
|
faShieldCheckThin,
|
|
1547
|
+
faShieldHalved,
|
|
1548
|
+
faShieldHalvedLight,
|
|
1549
|
+
faShieldHalvedSolid,
|
|
1550
|
+
faShieldHalvedThin,
|
|
1543
1551
|
faSigma,
|
|
1544
1552
|
faSigmaLight,
|
|
1545
1553
|
faSigmaSolid,
|
|
@@ -1,18 +1,59 @@
|
|
|
1
1
|
.xh-login {
|
|
2
|
-
.xh-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
color: var(--xh-appbar-title-color);
|
|
2
|
+
.xh-panel__content {
|
|
3
|
+
align-items: center;
|
|
4
|
+
justify-content: center;
|
|
6
5
|
}
|
|
7
6
|
|
|
8
|
-
&
|
|
9
|
-
|
|
7
|
+
&__content {
|
|
8
|
+
display: flex;
|
|
9
|
+
flex-direction: column;
|
|
10
|
+
width: 90vw;
|
|
11
|
+
max-width: 400px;
|
|
12
|
+
padding: 0 40px;
|
|
10
13
|
}
|
|
11
14
|
|
|
12
|
-
|
|
13
|
-
|
|
15
|
+
&__icon {
|
|
16
|
+
align-self: center;
|
|
17
|
+
display: flex;
|
|
18
|
+
align-items: center;
|
|
19
|
+
justify-content: center;
|
|
20
|
+
width: 64px;
|
|
21
|
+
height: 64px;
|
|
22
|
+
margin-bottom: var(--xh-pad-double-px);
|
|
23
|
+
font-size: 26px;
|
|
24
|
+
color: var(--xh-intent-primary);
|
|
25
|
+
background-color: var(--xh-intent-primary-trans1);
|
|
26
|
+
border-radius: 10px;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
&__title {
|
|
30
|
+
color: var(--xh-text-color);
|
|
31
|
+
font-size: var(--xh-font-size-large-px);
|
|
32
|
+
font-weight: 500;
|
|
33
|
+
text-align: center;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
&__subtitle {
|
|
37
|
+
color: var(--xh-text-color-muted);
|
|
38
|
+
font-size: var(--xh-font-size-small-px);
|
|
39
|
+
margin: 3px 0 var(--xh-pad-double-px);
|
|
40
|
+
text-align: center;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
&__field {
|
|
14
44
|
margin-bottom: var(--xh-pad-px);
|
|
15
|
-
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
&__field-label {
|
|
48
|
+
margin-bottom: var(--xh-pad-half-px);
|
|
49
|
+
font-size: var(--xh-font-size-small-px);
|
|
50
|
+
color: var(--xh-text-color-muted);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.xh-text-input {
|
|
54
|
+
width: 100%;
|
|
55
|
+
border: var(--xh-border-solid);
|
|
56
|
+
border-radius: var(--xh-border-radius-px);
|
|
16
57
|
}
|
|
17
58
|
|
|
18
59
|
&__warning,
|
|
@@ -32,7 +73,8 @@
|
|
|
32
73
|
background-color: var(--xh-bg-alt);
|
|
33
74
|
}
|
|
34
75
|
|
|
35
|
-
|
|
36
|
-
|
|
76
|
+
&__submit {
|
|
77
|
+
margin-top: var(--xh-pad-px);
|
|
78
|
+
padding: var(--xh-pad-double-px) 0;
|
|
37
79
|
}
|
|
38
80
|
}
|
|
@@ -5,13 +5,12 @@
|
|
|
5
5
|
* Copyright © 2026 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
7
|
import {LoginPanelModel} from '@xh/hoist/appcontainer/login/LoginPanelModel';
|
|
8
|
-
import {div,
|
|
8
|
+
import {box, div, form, viewport} from '@xh/hoist/cmp/layout';
|
|
9
9
|
import {creates, hoistCmp, XH} from '@xh/hoist/core';
|
|
10
10
|
import {Icon} from '@xh/hoist/icon';
|
|
11
11
|
import {button} from '@xh/hoist/mobile/cmp/button';
|
|
12
12
|
import {textInput} from '@xh/hoist/mobile/cmp/input';
|
|
13
13
|
import {panel} from '@xh/hoist/mobile/cmp/panel';
|
|
14
|
-
import {toolbar} from '@xh/hoist/mobile/cmp/toolbar';
|
|
15
14
|
import './LoginPanel.scss';
|
|
16
15
|
|
|
17
16
|
/**
|
|
@@ -25,59 +24,85 @@ export const loginPanel = hoistCmp.factory({
|
|
|
25
24
|
model: creates(LoginPanelModel),
|
|
26
25
|
|
|
27
26
|
render({model}) {
|
|
28
|
-
const {loginMessage} = XH.appSpec,
|
|
29
|
-
{
|
|
27
|
+
const {loginMessage, loginPanelIcon, clientAppName} = XH.appSpec,
|
|
28
|
+
{warning, loginInProgress} = model;
|
|
30
29
|
|
|
31
|
-
return
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
className: 'xh-login__body',
|
|
38
|
-
mask: loadObserver,
|
|
30
|
+
return viewport(
|
|
31
|
+
panel({
|
|
32
|
+
className: 'xh-login',
|
|
33
|
+
testId: 'xh-login',
|
|
34
|
+
item: div({
|
|
35
|
+
className: 'xh-login__content',
|
|
39
36
|
items: [
|
|
37
|
+
box({
|
|
38
|
+
className: 'xh-login__icon',
|
|
39
|
+
item: loginPanelIcon ?? Icon.shieldHalved({prefix: 'fas'})
|
|
40
|
+
}),
|
|
41
|
+
div({className: 'xh-login__title', item: `Login to ${clientAppName}`}),
|
|
42
|
+
div({
|
|
43
|
+
className: 'xh-login__subtitle',
|
|
44
|
+
item: 'Enter your credentials to continue.'
|
|
45
|
+
}),
|
|
40
46
|
form({
|
|
41
47
|
className: 'xh-login__fields',
|
|
42
48
|
items: [
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
49
|
+
field({
|
|
50
|
+
label: 'Username',
|
|
51
|
+
input: textInput({
|
|
52
|
+
bind: 'username',
|
|
53
|
+
leftIcon: Icon.user(),
|
|
54
|
+
autoComplete: 'username',
|
|
55
|
+
autoCapitalize: 'none',
|
|
56
|
+
commitOnChange: true,
|
|
57
|
+
testId: 'xh-login-username'
|
|
58
|
+
})
|
|
59
|
+
}),
|
|
60
|
+
field({
|
|
61
|
+
label: 'Password',
|
|
62
|
+
input: textInput({
|
|
63
|
+
bind: 'password',
|
|
64
|
+
leftIcon: Icon.lock(),
|
|
65
|
+
type: 'password',
|
|
66
|
+
autoComplete: 'current-password',
|
|
67
|
+
commitOnChange: true,
|
|
68
|
+
testId: 'xh-login-password'
|
|
69
|
+
})
|
|
50
70
|
}),
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
71
|
+
div({
|
|
72
|
+
className: 'xh-login__message',
|
|
73
|
+
omit: !loginMessage,
|
|
74
|
+
item: loginMessage
|
|
75
|
+
}),
|
|
76
|
+
div({
|
|
77
|
+
className: 'xh-login__warning',
|
|
78
|
+
omit: !warning,
|
|
79
|
+
item: warning
|
|
80
|
+
}),
|
|
81
|
+
button({
|
|
82
|
+
className: 'xh-login__submit',
|
|
83
|
+
icon: loginInProgress
|
|
84
|
+
? Icon.spinner({spin: true})
|
|
85
|
+
: Icon.login(),
|
|
86
|
+
text: loginInProgress ? 'Logging in...' : 'Login',
|
|
87
|
+
intent: 'primary',
|
|
88
|
+
onClick: () => model.submitAsync(),
|
|
89
|
+
testId: 'xh-login-btn'
|
|
58
90
|
})
|
|
59
91
|
]
|
|
60
|
-
}),
|
|
61
|
-
div({
|
|
62
|
-
className: 'xh-login__message',
|
|
63
|
-
omit: !loginMessage,
|
|
64
|
-
item: loginMessage
|
|
65
|
-
}),
|
|
66
|
-
div({
|
|
67
|
-
className: 'xh-login__warning',
|
|
68
|
-
omit: !warning,
|
|
69
|
-
item: warning
|
|
70
|
-
}),
|
|
71
|
-
button({
|
|
72
|
-
icon: Icon.login(),
|
|
73
|
-
text: loginInProgress ? 'Please wait...' : 'Login',
|
|
74
|
-
disabled: !isValid || loginInProgress,
|
|
75
|
-
onClick: () => model.submitAsync(),
|
|
76
|
-
testId: 'xh-login-btn'
|
|
77
92
|
})
|
|
78
93
|
]
|
|
79
94
|
})
|
|
80
|
-
|
|
95
|
+
})
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const field = hoistCmp.factory({
|
|
101
|
+
model: false,
|
|
102
|
+
render({label, input}) {
|
|
103
|
+
return div({
|
|
104
|
+
className: 'xh-login__field',
|
|
105
|
+
items: [div({className: 'xh-login__field-label', item: label}), input]
|
|
81
106
|
});
|
|
82
107
|
}
|
|
83
108
|
});
|
|
@@ -5,8 +5,7 @@
|
|
|
5
5
|
* Copyright © 2026 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
7
|
import {AppOptionSpec, SizingMode, XH} from '@xh/hoist/core';
|
|
8
|
-
import {
|
|
9
|
-
import {buttonGroupInput, ButtonGroupInputProps} from '@xh/hoist/mobile/cmp/input';
|
|
8
|
+
import {segmentedControl, SegmentedControlProps} from '@xh/hoist/mobile/cmp/input';
|
|
10
9
|
import {startCase, values} from 'lodash';
|
|
11
10
|
import {FormFieldProps} from '../form/FormField';
|
|
12
11
|
import '@xh/hoist/mobile/register';
|
|
@@ -16,8 +15,8 @@ interface SizingModeAppOptionSpec {
|
|
|
16
15
|
modes?: SizingMode[];
|
|
17
16
|
/** Props for nested FormField. */
|
|
18
17
|
formFieldProps?: Partial<FormFieldProps>;
|
|
19
|
-
/** Props for nested
|
|
20
|
-
inputProps?: Partial<
|
|
18
|
+
/** Props for nested SegmentedControl. */
|
|
19
|
+
inputProps?: Partial<SegmentedControlProps>;
|
|
21
20
|
}
|
|
22
21
|
|
|
23
22
|
/**
|
|
@@ -33,18 +32,8 @@ export const sizingModeAppOption = ({
|
|
|
33
32
|
name: 'sizingMode',
|
|
34
33
|
formField: {
|
|
35
34
|
label: 'Grid sizing',
|
|
36
|
-
item:
|
|
37
|
-
|
|
38
|
-
button({
|
|
39
|
-
value: mode,
|
|
40
|
-
text: startCase(mode),
|
|
41
|
-
flex: 1,
|
|
42
|
-
style: {
|
|
43
|
-
fontSize: `var(--xh-grid-${mode}-font-size-px)`
|
|
44
|
-
}
|
|
45
|
-
})
|
|
46
|
-
),
|
|
47
|
-
width: '100%',
|
|
35
|
+
item: segmentedControl({
|
|
36
|
+
options: modes.map(mode => ({value: mode, label: startCase(mode)})),
|
|
48
37
|
...inputProps
|
|
49
38
|
}),
|
|
50
39
|
...formFieldProps
|
|
@@ -6,16 +6,15 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import {AppOptionSpec, XH} from '@xh/hoist/core';
|
|
8
8
|
import {Icon} from '@xh/hoist/icon/Icon';
|
|
9
|
-
import {button} from '@xh/hoist/mobile/cmp/button';
|
|
10
9
|
import {FormFieldProps} from '@xh/hoist/mobile/cmp/form';
|
|
11
|
-
import {
|
|
10
|
+
import {segmentedControl, SegmentedControlProps} from '@xh/hoist/mobile/cmp/input';
|
|
12
11
|
import '@xh/hoist/mobile/register';
|
|
13
12
|
|
|
14
13
|
interface ThemeAppOptionSpec {
|
|
15
14
|
/** Props for nested FormField */
|
|
16
15
|
formFieldProps?: Partial<FormFieldProps>;
|
|
17
|
-
/** Props for nested
|
|
18
|
-
inputProps?: Partial<
|
|
16
|
+
/** Props for nested SegmentedControl */
|
|
17
|
+
inputProps?: Partial<SegmentedControlProps>;
|
|
19
18
|
}
|
|
20
19
|
|
|
21
20
|
/**
|
|
@@ -29,28 +28,12 @@ export const themeAppOption = ({
|
|
|
29
28
|
name: 'theme',
|
|
30
29
|
formField: {
|
|
31
30
|
label: 'Theme',
|
|
32
|
-
item:
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
icon: Icon.sun(),
|
|
38
|
-
width: '33.33%'
|
|
39
|
-
}),
|
|
40
|
-
button({
|
|
41
|
-
value: 'dark',
|
|
42
|
-
text: 'Dark',
|
|
43
|
-
icon: Icon.moon(),
|
|
44
|
-
width: '33.33%'
|
|
45
|
-
}),
|
|
46
|
-
button({
|
|
47
|
-
value: 'system',
|
|
48
|
-
text: 'System',
|
|
49
|
-
icon: Icon.sync(),
|
|
50
|
-
width: '33.33%'
|
|
51
|
-
})
|
|
31
|
+
item: segmentedControl({
|
|
32
|
+
options: [
|
|
33
|
+
{value: 'light', label: 'Light', icon: Icon.sun()},
|
|
34
|
+
{value: 'dark', label: 'Dark', icon: Icon.moon()},
|
|
35
|
+
{value: 'system', label: 'System', icon: Icon.sync()}
|
|
52
36
|
],
|
|
53
|
-
width: '100%',
|
|
54
37
|
...inputProps
|
|
55
38
|
}),
|
|
56
39
|
...formFieldProps
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Copyright © 2026 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
7
|
import {hoistCmp} from '@xh/hoist/core';
|
|
8
|
-
import {div,
|
|
8
|
+
import {div, p, vbox} from '@xh/hoist/cmp/layout';
|
|
9
9
|
import {ErrorMessageProps} from '@xh/hoist/cmp/error';
|
|
10
10
|
import {button, ButtonProps} from '@xh/hoist/mobile/cmp/button';
|
|
11
11
|
import {Icon} from '@xh/hoist/icon';
|
|
@@ -21,15 +21,11 @@ export const errorMessageImpl = hoistCmp.factory<
|
|
|
21
21
|
Omit<ErrorMessageProps, 'error' | 'actionFn' | 'detailsFn'>
|
|
22
22
|
>({
|
|
23
23
|
render({message, title, actionButtonProps, detailsButtonProps}) {
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
// Stack action buttons vertically and full-width - idiomatic for mobile.
|
|
25
|
+
const buttons = [];
|
|
26
26
|
if (detailsButtonProps) buttons.push(detailsButton(detailsButtonProps as ButtonProps));
|
|
27
27
|
if (actionButtonProps) buttons.push(actionButton(actionButtonProps as ButtonProps));
|
|
28
|
-
|
|
29
|
-
buttonBar = buttons[0];
|
|
30
|
-
} else if (buttons.length == 2) {
|
|
31
|
-
buttonBar = hbox(buttons[0], filler(), buttons[1]);
|
|
32
|
-
}
|
|
28
|
+
const buttonBar = buttons.length ? vbox({gap: true, items: buttons}) : null;
|
|
33
29
|
|
|
34
30
|
return div({
|
|
35
31
|
className: 'xh-error-message__inner',
|