@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/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-toolbar {
3
- font-size: var(--xh-appbar-title-font-size-px);
4
- background-color: var(--xh-appbar-bg);
5
- color: var(--xh-appbar-title-color);
2
+ .xh-panel__content {
3
+ align-items: center;
4
+ justify-content: center;
6
5
  }
7
6
 
8
- &__body {
9
- margin: 40px;
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
- .xh-input {
13
- width: 100%;
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
- padding: 4px 8px;
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
- .xh-button {
36
- padding: 1.5em 0;
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, filler, form} from '@xh/hoist/cmp/layout';
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
- {isValid, loadObserver, warning, loginInProgress} = model;
27
+ const {loginMessage, loginPanelIcon, clientAppName} = XH.appSpec,
28
+ {warning, loginInProgress} = model;
30
29
 
31
- return panel({
32
- className: 'xh-login',
33
- testId: 'xh-login',
34
- items: [
35
- toolbar(filler(), XH.clientAppName, filler()),
36
- panel({
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
- textInput({
44
- bind: 'username',
45
- placeholder: 'Username',
46
- autoComplete: 'username',
47
- autoCapitalize: 'none',
48
- commitOnChange: true,
49
- testId: 'xh-login-username'
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
- textInput({
52
- bind: 'password',
53
- placeholder: 'Password',
54
- autoComplete: 'current-password',
55
- type: 'password',
56
- commitOnChange: true,
57
- testId: 'xh-login-password'
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 {button} from '@xh/hoist/mobile/cmp/button';
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 ButtonGroupInput. */
20
- inputProps?: Partial<ButtonGroupInputProps>;
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: buttonGroupInput({
37
- items: modes.map(mode =>
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 {buttonGroupInput, ButtonGroupInputProps} from '@xh/hoist/mobile/cmp/input';
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 ButtonGroupInput */
18
- inputProps?: Partial<ButtonGroupInputProps>;
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: buttonGroupInput({
33
- items: [
34
- button({
35
- value: 'light',
36
- text: 'Light',
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, filler, hbox, p} from '@xh/hoist/cmp/layout';
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
- let buttons = [],
25
- buttonBar = null;
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
- if (buttons.length == 1) {
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
+ }