@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.
- package/CHANGELOG.md +58 -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/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/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 +2 -2
- package/styles/vars.scss +6 -1
- package/svc/JsonBlobService.ts +21 -20
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',
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file belongs to Hoist, an application development toolkit
|
|
3
|
+
* developed by Extremely Heavy Industries (www.xh.io | info@xh.io)
|
|
4
|
+
*
|
|
5
|
+
* Copyright © 2026 Extremely Heavy Industries Inc.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Scoped under .xh-app to win specificity over the .xh-app-scoped mobile Button styles.
|
|
9
|
+
.xh-app {
|
|
10
|
+
.xh-segmented-control {
|
|
11
|
+
// The tray - a recessed container the selected option rises out of.
|
|
12
|
+
display: flex;
|
|
13
|
+
align-items: stretch;
|
|
14
|
+
// Size the whole control (tray padding included) to one standard control height, so it matches
|
|
15
|
+
// a button / input rather than standing taller. Options stretch to fill the tray (below) rather
|
|
16
|
+
// than each taking the full button height, which - plus the tray padding - would overshoot.
|
|
17
|
+
box-sizing: border-box;
|
|
18
|
+
height: var(--xh-button-height);
|
|
19
|
+
background-color: var(--xh-segmented-control-bg);
|
|
20
|
+
border-radius: var(--xh-segmented-control-border-radius-px);
|
|
21
|
+
padding: var(--xh-segmented-control-pad-px);
|
|
22
|
+
gap: var(--xh-segmented-control-pad-px);
|
|
23
|
+
|
|
24
|
+
// Options - buttons receding into the tray by default. Override the minimal Button base so
|
|
25
|
+
// the tray background shows through and our segmented text color applies.
|
|
26
|
+
.xh-button.xh-segmented-control-option {
|
|
27
|
+
flex: 0 0 auto;
|
|
28
|
+
// Fill the tray height (via the tray's align-items: stretch) instead of the fixed button
|
|
29
|
+
// height, so tray padding does not add on top of a full-height button.
|
|
30
|
+
height: auto;
|
|
31
|
+
// Allow the button to shrink below its content width so the label can ellipsis-truncate
|
|
32
|
+
// (flex items default to min-width: auto, which forces overflow / hard clipping instead).
|
|
33
|
+
min-width: 0;
|
|
34
|
+
border-radius: var(--xh-segmented-control-border-radius-px);
|
|
35
|
+
background-color: transparent;
|
|
36
|
+
color: var(--xh-segmented-control-text-color);
|
|
37
|
+
|
|
38
|
+
// Keep icons at full size - only the label gives way when space is tight.
|
|
39
|
+
> svg {
|
|
40
|
+
flex: 0 0 auto;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Label - truncate with an ellipsis when its segment is too narrow to fit the full text.
|
|
45
|
+
.xh-segmented-control-option__label {
|
|
46
|
+
overflow: hidden;
|
|
47
|
+
min-width: 0;
|
|
48
|
+
text-overflow: ellipsis;
|
|
49
|
+
white-space: nowrap;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Selected option - a solid tile standing out from the tray.
|
|
53
|
+
.xh-button.xh-segmented-control-option--selected {
|
|
54
|
+
background-color: var(--xh-segmented-control-selected-bg);
|
|
55
|
+
color: var(--xh-segmented-control-selected-text-color);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Per-option intents - keyed off the className applied to each button. Renders the selected
|
|
59
|
+
// option as a solid fill in the intent color, and tints unselected options' text so an intent
|
|
60
|
+
// (e.g. danger) reads as such even before it is chosen.
|
|
61
|
+
@each $intent in (primary, success, warning, danger) {
|
|
62
|
+
.xh-button.xh-segmented-control-option--#{$intent} {
|
|
63
|
+
color: var(--xh-intent-#{$intent}-text-color);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.xh-button.xh-segmented-control-option--#{$intent}.xh-segmented-control-option--selected {
|
|
67
|
+
background-color: var(--xh-intent-#{$intent});
|
|
68
|
+
color: var(--xh-white);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Fill mode (default) - options grow to fill the available width. The basis set here is `auto`
|
|
73
|
+
// (content), so without `equalSegmentWidths` each option keeps its content width and only the
|
|
74
|
+
// leftover space is shared out - a wider label yields a wider segment. The equal-widths rule
|
|
75
|
+
// below (the default) overrides this basis. Options still shrink + ellipsis (see above) when
|
|
76
|
+
// the set genuinely cannot fit.
|
|
77
|
+
&.xh-segmented-control--fill .xh-button.xh-segmented-control-option {
|
|
78
|
+
flex: 1 1 auto;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Equal-width segments (default, via the `equalSegmentWidths` prop) - a 0 flex-basis divides
|
|
82
|
+
// the row into equal parts regardless of label length, the conventional segmented-control
|
|
83
|
+
// appearance. Higher specificity than the content-basis rule above, so it wins when applied.
|
|
84
|
+
&.xh-segmented-control--fill.xh-segmented-control--equal-widths
|
|
85
|
+
.xh-button.xh-segmented-control-option {
|
|
86
|
+
flex-basis: 0;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Outlined mode - border around the tray with no inner background fill.
|
|
90
|
+
&.xh-segmented-control--outlined {
|
|
91
|
+
background-color: transparent;
|
|
92
|
+
border: 1px solid var(--xh-border-color);
|
|
93
|
+
|
|
94
|
+
// Reserve border space on every option so selecting one does not shift the row. The
|
|
95
|
+
// transparent tray offers no recessed backdrop to set a filled tile against, so indicate
|
|
96
|
+
// selection with a ring instead - currentColor keeps it in step with the option's
|
|
97
|
+
// text / intent color.
|
|
98
|
+
.xh-button.xh-segmented-control-option {
|
|
99
|
+
border: 1px solid transparent;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.xh-button.xh-segmented-control-option--selected {
|
|
103
|
+
border-color: currentColor;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
@each $intent in (primary, success, warning, danger) {
|
|
108
|
+
&.xh-segmented-control--outlined.xh-segmented-control--#{$intent} {
|
|
109
|
+
border-color: var(--xh-intent-#{$intent});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Validation borders
|
|
114
|
+
&.xh-input--invalid {
|
|
115
|
+
border: var(--xh-form-field-invalid-border);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
&.xh-input--warning {
|
|
119
|
+
border: var(--xh-form-field-warning-border);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
&.xh-input--info {
|
|
123
|
+
border: var(--xh-form-field-info-border);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|