@xh/hoist 86.0.1 → 86.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +104 -0
- package/admin/AdminJsonDisplay.ts +31 -0
- package/admin/App.scss +3 -11
- package/admin/jsonsearch/impl/JsonSearchImplModel.ts +4 -4
- package/admin/tabs/cluster/instances/connpool/ConnPoolMonitorPanel.ts +3 -11
- package/admin/tabs/cluster/instances/services/DetailsPanel.ts +2 -13
- package/admin/tabs/cluster/objects/DetailPanel.ts +3 -12
- 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/admin/AdminJsonDisplay.d.ts +7 -0
- package/build/types/admin/jsonsearch/impl/JsonSearchImplModel.d.ts +2 -2
- package/build/types/appcontainer/RouterModel.d.ts +1 -0
- package/build/types/appcontainer/ThemeModel.d.ts +9 -0
- package/build/types/cmp/grid/Grid.d.ts +6 -3
- package/build/types/cmp/grid/GridModel.d.ts +11 -0
- package/build/types/cmp/grid/filter/GridFilterFieldSpec.d.ts +24 -6
- 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/button/grid/ExpandToLevelButton.d.ts +2 -1
- package/build/types/desktop/cmp/filechooser/FileChooserModel.d.ts +1 -1
- package/build/types/desktop/cmp/input/CodeInput.d.ts +7 -0
- package/build/types/desktop/cmp/input/NumberInput.d.ts +1 -1
- package/build/types/desktop/cmp/input/SegmentedControl.d.ts +1 -41
- package/build/types/desktop/cmp/input/impl/CalcWindowedMenuWidth.d.ts +20 -0
- package/build/types/desktop/cmp/rest/impl/RestFormModel.d.ts +1 -1
- package/build/types/format/FormatNumber.d.ts +11 -3
- package/build/types/icon/Icon.d.ts +5 -3
- 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/button/grid/ExpandToLevelButton.d.ts +2 -1
- package/build/types/mobile/cmp/input/NumberInput.d.ts +1 -1
- 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/Grid.ts +17 -0
- package/cmp/grid/GridModel.ts +17 -5
- package/cmp/grid/filter/GridFilterFieldSpec.ts +33 -7
- package/cmp/grid/impl/GridHScrollbar.ts +10 -3
- package/cmp/grid/impl/MenuSupport.ts +2 -4
- 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/button/grid/ExpandToLevelButton.ts +4 -5
- package/desktop/cmp/filechooser/FileChooserModel.ts +1 -1
- package/desktop/cmp/grid/editors/NumberEditor.ts +16 -17
- package/desktop/cmp/grid/editors/SelectEditor.ts +15 -10
- package/desktop/cmp/grid/impl/filter/GridFilterDialog.ts +2 -2
- 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/grid/impl/filter/headerfilter/values/ValuesTabModel.ts +46 -7
- package/desktop/cmp/input/CheckboxButton.ts +21 -1
- package/desktop/cmp/input/CodeInput.ts +20 -3
- package/desktop/cmp/input/NumberInput.ts +1 -1
- package/desktop/cmp/input/SegmentedControl.ts +8 -51
- package/desktop/cmp/input/Select.ts +13 -2
- package/desktop/cmp/input/impl/CalcWindowedMenuWidth.ts +106 -0
- package/desktop/cmp/rest/impl/RestFormField.ts +1 -1
- package/desktop/cmp/tab/Tabs.scss +78 -27
- package/desktop/cmp/tab/dynamic/DynamicTabSwitcher.ts +7 -3
- package/docs/error-handling.md +9 -0
- package/format/FormatNumber.ts +69 -32
- package/icon/Icon.scss +13 -0
- package/icon/Icon.ts +7 -3
- package/icon/impl/IconHtml.ts +1 -1
- package/icon/index.ts +8 -0
- package/mcp/data/ts-registry.ts +1 -1
- 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/button/grid/ExpandToLevelButton.ts +4 -5
- package/mobile/cmp/error/impl/ErrorMessage.ts +4 -8
- package/mobile/cmp/input/CheckboxButton.ts +18 -1
- package/mobile/cmp/input/NumberInput.ts +1 -1
- 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 +7 -7
- package/styles/vars.scss +21 -1
- package/svc/JsonBlobService.ts +21 -20
|
@@ -44,7 +44,24 @@ class CheckboxButtonInputModel extends HoistInputModel {
|
|
|
44
44
|
// Implementation
|
|
45
45
|
//----------------------------------
|
|
46
46
|
const cmp = hoistCmp.factory<CheckboxButtonInputModel>(
|
|
47
|
-
(
|
|
47
|
+
(
|
|
48
|
+
{
|
|
49
|
+
// HoistInput props - exclude from passthrough to Onsen
|
|
50
|
+
bind,
|
|
51
|
+
value,
|
|
52
|
+
commitOnChange,
|
|
53
|
+
onChange,
|
|
54
|
+
onCommit,
|
|
55
|
+
// Consumed by this component
|
|
56
|
+
model,
|
|
57
|
+
text,
|
|
58
|
+
checkedIcon,
|
|
59
|
+
uncheckedIcon,
|
|
60
|
+
// Remainder passed to hoist button & Onsen button
|
|
61
|
+
...props
|
|
62
|
+
},
|
|
63
|
+
ref
|
|
64
|
+
) => {
|
|
48
65
|
const checked = !!model.renderValue;
|
|
49
66
|
return button({
|
|
50
67
|
text: withDefault(text, model.getField()?.displayName),
|
|
@@ -46,7 +46,7 @@ export interface NumberInputProps extends HoistProps, HoistInputProps, StyleProp
|
|
|
46
46
|
/** Text to display when control is empty. */
|
|
47
47
|
placeholder?: string;
|
|
48
48
|
|
|
49
|
-
/** Max decimal precision of the value, defaults to 4. */
|
|
49
|
+
/** Max decimal precision of the value, defaults to 4. Set to null for full, unrestricted precision. */
|
|
50
50
|
precision?: NumericPrecision;
|
|
51
51
|
|
|
52
52
|
/**
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
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
|
+
import {
|
|
8
|
+
HoistInputModel,
|
|
9
|
+
HoistInputProps,
|
|
10
|
+
OptionPrimitive,
|
|
11
|
+
SegmentedControlNullOption,
|
|
12
|
+
SegmentedControlOption,
|
|
13
|
+
useHoistInputModel
|
|
14
|
+
} from '@xh/hoist/cmp/input';
|
|
15
|
+
import {hbox, span} from '@xh/hoist/cmp/layout';
|
|
16
|
+
import {hoistCmp, HoistProps, Intent} from '@xh/hoist/core';
|
|
17
|
+
import {button} from '@xh/hoist/mobile/cmp/button';
|
|
18
|
+
import '@xh/hoist/mobile/register';
|
|
19
|
+
import {computed, makeObservable} from '@xh/hoist/mobx';
|
|
20
|
+
import {TEST_ID} from '@xh/hoist/utils/js';
|
|
21
|
+
import {getLayoutProps, getNonLayoutProps} from '@xh/hoist/utils/react';
|
|
22
|
+
import classNames from 'classnames';
|
|
23
|
+
import {filter, isObject} from 'lodash';
|
|
24
|
+
import './SegmentedControl.scss';
|
|
25
|
+
|
|
26
|
+
export interface SegmentedControlProps extends HoistProps, HoistInputProps {
|
|
27
|
+
/**
|
|
28
|
+
* True (default) to render all segments at an equal width when {@link fill} is enabled,
|
|
29
|
+
* dividing the available width into equal parts regardless of label length - the conventional
|
|
30
|
+
* segmented-control appearance. Set false to instead size each segment to its content and
|
|
31
|
+
* share only the leftover space, so a longer label yields a wider segment. No effect when
|
|
32
|
+
* `fill` is false. Either way, a label too wide for its segment truncates with an ellipsis.
|
|
33
|
+
*/
|
|
34
|
+
equalSegmentWidths?: boolean;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* True (default) to stretch the control to fill available width,
|
|
38
|
+
* distributing space equally among options.
|
|
39
|
+
*/
|
|
40
|
+
fill?: boolean;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Default visual intent applied to the selected option, and as a subtle text-color hint
|
|
44
|
+
* to options when not selected. Serves as the default for any option that does not specify
|
|
45
|
+
* its own `intent`. Defaults to `'none'`.
|
|
46
|
+
*/
|
|
47
|
+
intent?: 'none' | Intent;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Array of available options. Each entry may be a SegmentedControlOption object
|
|
51
|
+
* with value/label/icon/disabled properties, or a primitive value used as both
|
|
52
|
+
* the value and the display label.
|
|
53
|
+
*/
|
|
54
|
+
options: Array<SegmentedControlOption | SegmentedControlNullOption | OptionPrimitive>;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* True to render with an outlined style - a border around the control tray
|
|
58
|
+
* with no inner background fill. Border color follows the current intent.
|
|
59
|
+
*/
|
|
60
|
+
outlined?: boolean;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* An input for selecting a single value from a small set of mutually exclusive options,
|
|
65
|
+
* rendered as a group of toggle buttons with clear visual indication of the active
|
|
66
|
+
* selection.
|
|
67
|
+
*
|
|
68
|
+
* Similar to ButtonGroupInput but driven by an `options` prop (like Select) rather than Button
|
|
69
|
+
* children, and with stronger visual differentiation between selected and unselected states.
|
|
70
|
+
* The mobile counterpart to the desktop SegmentedControl, built on Hoist's mobile Button
|
|
71
|
+
* (no Blueprint dependency).
|
|
72
|
+
*/
|
|
73
|
+
export const [SegmentedControl, segmentedControl] = hoistCmp.withFactory<SegmentedControlProps>({
|
|
74
|
+
displayName: 'SegmentedControl',
|
|
75
|
+
className: 'xh-segmented-control',
|
|
76
|
+
render(props, ref) {
|
|
77
|
+
return useHoistInputModel(cmp, props, ref, SegmentedControlModel);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
(SegmentedControl as any).hasLayoutSupport = true;
|
|
81
|
+
|
|
82
|
+
//-----------------------
|
|
83
|
+
// Implementation
|
|
84
|
+
//-----------------------
|
|
85
|
+
interface NormalizedOption extends SegmentedControlOption {
|
|
86
|
+
label: string;
|
|
87
|
+
intent?: Intent;
|
|
88
|
+
_key: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
class SegmentedControlModel extends HoistInputModel {
|
|
92
|
+
override xhImpl = true;
|
|
93
|
+
|
|
94
|
+
@computed
|
|
95
|
+
get normalizedOptions(): NormalizedOption[] {
|
|
96
|
+
const options = this.componentProps.options ?? [];
|
|
97
|
+
return options.map((o: any, idx: number) => {
|
|
98
|
+
const key = String(idx);
|
|
99
|
+
if (isObject(o)) {
|
|
100
|
+
const {label, value, icon, disabled, intent} = o as SegmentedControlOption;
|
|
101
|
+
return {
|
|
102
|
+
value: this.toInternal(value),
|
|
103
|
+
label: label ?? (icon ? '' : String(value)),
|
|
104
|
+
icon,
|
|
105
|
+
disabled,
|
|
106
|
+
intent,
|
|
107
|
+
_key: key
|
|
108
|
+
};
|
|
109
|
+
} else {
|
|
110
|
+
return {value: this.toInternal(o), label: String(o), _key: key};
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Map the current render value to the string key used to identify the selected option. */
|
|
116
|
+
@computed
|
|
117
|
+
get selectedKey(): string {
|
|
118
|
+
const {renderValue, normalizedOptions} = this;
|
|
119
|
+
return normalizedOptions.find(o => o.value === renderValue)?._key;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
get enabledButtons(): HTMLButtonElement[] {
|
|
123
|
+
const btns = this.domEl?.querySelectorAll('button') ?? [];
|
|
124
|
+
return filter(btns, (b: HTMLButtonElement) => !b.disabled) as HTMLButtonElement[];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
constructor() {
|
|
128
|
+
super();
|
|
129
|
+
makeObservable(this);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
onValueChange = (key: string) => {
|
|
133
|
+
const match = this.normalizedOptions.find(o => o._key === key);
|
|
134
|
+
if (match) this.noteValueChange(match.value);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
override blur() {
|
|
138
|
+
this.enabledButtons.forEach(it => it.blur());
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
override focus() {
|
|
142
|
+
this.enabledButtons[0]?.focus();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const cmp = hoistCmp.factory<SegmentedControlModel>(({model, className, ...props}, ref) => {
|
|
147
|
+
const {
|
|
148
|
+
// HoistInput props - consumed here or by the model, not passed to the tray
|
|
149
|
+
bind,
|
|
150
|
+
disabled,
|
|
151
|
+
onChange,
|
|
152
|
+
onCommit,
|
|
153
|
+
tabIndex,
|
|
154
|
+
value,
|
|
155
|
+
commitOnChange,
|
|
156
|
+
options,
|
|
157
|
+
// Consumed by this component
|
|
158
|
+
equalSegmentWidths = true,
|
|
159
|
+
fill = true,
|
|
160
|
+
intent,
|
|
161
|
+
outlined,
|
|
162
|
+
testId,
|
|
163
|
+
...rest
|
|
164
|
+
} = getNonLayoutProps(props);
|
|
165
|
+
|
|
166
|
+
const {selectedKey} = model,
|
|
167
|
+
defaultIntent = intent && intent !== 'none' ? intent : null;
|
|
168
|
+
|
|
169
|
+
const buttons = model.normalizedOptions.map(opt => {
|
|
170
|
+
const optIntent = opt.intent ?? defaultIntent,
|
|
171
|
+
selected = opt._key === selectedKey;
|
|
172
|
+
// Wrap the label so it can truncate with an ellipsis when the segment is too narrow,
|
|
173
|
+
// rather than hard-clipping mid-character. Pass null for icon-only options so the Button
|
|
174
|
+
// renders the icon alone (an empty span would suppress that).
|
|
175
|
+
const label = opt.label
|
|
176
|
+
? span({className: 'xh-segmented-control-option__label', item: opt.label})
|
|
177
|
+
: null;
|
|
178
|
+
|
|
179
|
+
return button({
|
|
180
|
+
key: opt._key,
|
|
181
|
+
text: label,
|
|
182
|
+
icon: opt.icon,
|
|
183
|
+
disabled: disabled || opt.disabled,
|
|
184
|
+
minimal: true,
|
|
185
|
+
className: classNames(
|
|
186
|
+
'xh-segmented-control-option',
|
|
187
|
+
selected && 'xh-segmented-control-option--selected',
|
|
188
|
+
optIntent && `xh-segmented-control-option--${optIntent}`
|
|
189
|
+
),
|
|
190
|
+
onClick: () => model.onValueChange(opt._key)
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
return hbox({
|
|
195
|
+
className: classNames(
|
|
196
|
+
className,
|
|
197
|
+
defaultIntent && `xh-segmented-control--${defaultIntent}`,
|
|
198
|
+
outlined && 'xh-segmented-control--outlined',
|
|
199
|
+
fill && 'xh-segmented-control--fill',
|
|
200
|
+
fill && equalSegmentWidths && 'xh-segmented-control--equal-widths'
|
|
201
|
+
),
|
|
202
|
+
ref,
|
|
203
|
+
onFocus: model.onFocus,
|
|
204
|
+
onBlur: model.onBlur,
|
|
205
|
+
...getLayoutProps(props),
|
|
206
|
+
[TEST_ID]: testId,
|
|
207
|
+
items: buttons,
|
|
208
|
+
...rest
|
|
209
|
+
});
|
|
210
|
+
});
|
|
@@ -12,7 +12,7 @@ import {TEST_ID} from '@xh/hoist/utils/js';
|
|
|
12
12
|
import './SwitchInput.scss';
|
|
13
13
|
|
|
14
14
|
export interface SwitchInputProps extends HoistProps, HoistInputProps, StyleProps {
|
|
15
|
-
value?:
|
|
15
|
+
value?: boolean;
|
|
16
16
|
|
|
17
17
|
/** Onsen modifier string */
|
|
18
18
|
modifier?: string;
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Copyright © 2026 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
7
|
import {HoistInputModel, HoistInputProps, useHoistInputModel} from '@xh/hoist/cmp/input';
|
|
8
|
-
import {hbox} from '@xh/hoist/cmp/layout';
|
|
8
|
+
import {box, hbox} from '@xh/hoist/cmp/layout';
|
|
9
9
|
import {hoistCmp, HoistProps, LayoutProps, StyleProps} from '@xh/hoist/core';
|
|
10
10
|
import {Icon} from '@xh/hoist/icon';
|
|
11
11
|
import {input} from '@xh/hoist/kit/onsen';
|
|
@@ -15,6 +15,7 @@ import {getTestId, TEST_ID, withDefault} from '@xh/hoist/utils/js';
|
|
|
15
15
|
import {getLayoutProps} from '@xh/hoist/utils/react';
|
|
16
16
|
import type {Property} from 'csstype';
|
|
17
17
|
import {isEmpty} from 'lodash';
|
|
18
|
+
import {ReactElement} from 'react';
|
|
18
19
|
import './TextInput.scss';
|
|
19
20
|
|
|
20
21
|
export interface TextInputProps extends HoistProps, HoistInputProps, StyleProps, LayoutProps {
|
|
@@ -46,6 +47,9 @@ export interface TextInputProps extends HoistProps, HoistInputProps, StyleProps,
|
|
|
46
47
|
/** True to show a "clear" button aligned to the right of the control. Defaults to false. */
|
|
47
48
|
enableClear?: boolean;
|
|
48
49
|
|
|
50
|
+
/** Icon to display inline on the left side of the input. */
|
|
51
|
+
leftIcon?: ReactElement;
|
|
52
|
+
|
|
49
53
|
/** Onsen modifier string */
|
|
50
54
|
modifier?: string;
|
|
51
55
|
|
|
@@ -123,6 +127,7 @@ const cmp = hoistCmp.factory<TextInputModel>(({model, className, ...props}, ref)
|
|
|
123
127
|
width: withDefault(width, null)
|
|
124
128
|
},
|
|
125
129
|
items: [
|
|
130
|
+
leftIcon(),
|
|
126
131
|
input({
|
|
127
132
|
value: model.renderValue || '',
|
|
128
133
|
|
|
@@ -151,6 +156,11 @@ const cmp = hoistCmp.factory<TextInputModel>(({model, className, ...props}, ref)
|
|
|
151
156
|
});
|
|
152
157
|
});
|
|
153
158
|
|
|
159
|
+
const leftIcon = hoistCmp.factory<TextInputModel>(({model}) => {
|
|
160
|
+
const {leftIcon} = model.componentProps;
|
|
161
|
+
return leftIcon ? box({className: 'xh-text-input__left-icon', item: leftIcon}) : null;
|
|
162
|
+
});
|
|
163
|
+
|
|
154
164
|
const clearButton = hoistCmp.factory<TextInputModel>(({model}) =>
|
|
155
165
|
button({
|
|
156
166
|
className: 'xh-text-input__clear-button',
|
|
@@ -11,6 +11,7 @@ export * from './CheckboxButton';
|
|
|
11
11
|
export * from './DateInput';
|
|
12
12
|
export * from './NumberInput';
|
|
13
13
|
export * from './SearchInput';
|
|
14
|
+
export * from './SegmentedControl';
|
|
14
15
|
export * from './Select';
|
|
15
16
|
export * from './SwitchInput';
|
|
16
17
|
export * from './TextInput';
|
|
@@ -25,6 +25,14 @@
|
|
|
25
25
|
transform: translateX(0);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
// Theme the page backdrop. Onsen renders an opaque, light-by-default `.page__background` behind
|
|
29
|
+
// each page's content, and the content itself is forced transparent (so the app background shows
|
|
30
|
+
// through). That leaves the light backdrop visible for tab content that is not itself a panel,
|
|
31
|
+
// rendering themed text unreadable in dark mode - so paint the backdrop with the app background.
|
|
32
|
+
.xh-tab-page .page__background {
|
|
33
|
+
background-color: var(--xh-bg);
|
|
34
|
+
}
|
|
35
|
+
|
|
28
36
|
.xh-tab-container {
|
|
29
37
|
// Override Onsen's tabbar height CSS var
|
|
30
38
|
--tabbar-height: var(--xh-tbar-min-size-px);
|
|
@@ -5,6 +5,14 @@
|
|
|
5
5
|
* Copyright © 2026 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
7
|
.xh-toolbar {
|
|
8
|
+
// Toolbars are the compact-button special case: keep buttons short and tight-cornered so they
|
|
9
|
+
// fit dense bars, overriding the taller / rounder defaults used for buttons in body content
|
|
10
|
+
// (see vars.scss). Derive the button height from the bar size so a single token
|
|
11
|
+
// (--xh-tbar-min-size) drives both - subtracting the bar's vertical padding so buttons fit a
|
|
12
|
+
// horizontal bar. The same height is reused for vertical bars to keep buttons consistent.
|
|
13
|
+
--xh-button-height: calc(var(--xh-tbar-min-size-px) - 2 * var(--xh-pad-half-px));
|
|
14
|
+
--xh-button-border-radius-px: var(--xh-border-radius-px);
|
|
15
|
+
|
|
8
16
|
padding: var(--xh-pad-half-px) var(--xh-pad-px);
|
|
9
17
|
color: var(--xh-tbar-text-color);
|
|
10
18
|
background-color: var(--xh-tbar-bg);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xh/hoist",
|
|
3
|
-
"version": "86.0
|
|
3
|
+
"version": "86.2.0",
|
|
4
4
|
"description": "Hoist add-on for building and deploying React Applications.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -39,14 +39,14 @@
|
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@auth0/auth0-spa-js": "~2.21.1",
|
|
42
|
-
"@azure/msal-browser": "~5.
|
|
42
|
+
"@azure/msal-browser": "~5.14.0",
|
|
43
43
|
"@blueprintjs/core": "^6.3.2",
|
|
44
44
|
"@blueprintjs/datetime": "^6.0.6",
|
|
45
|
-
"@codemirror/commands": "
|
|
46
|
-
"@codemirror/language": "
|
|
47
|
-
"@codemirror/language-data": "
|
|
48
|
-
"@codemirror/lint": "
|
|
49
|
-
"@codemirror/state": "
|
|
45
|
+
"@codemirror/commands": "^6.10.3",
|
|
46
|
+
"@codemirror/language": "^6.12.3",
|
|
47
|
+
"@codemirror/language-data": "^6.5.2",
|
|
48
|
+
"@codemirror/lint": "^6.9.6",
|
|
49
|
+
"@codemirror/state": "^6.7.0",
|
|
50
50
|
"@codemirror/view": "^6.43.0",
|
|
51
51
|
"@fortawesome/fontawesome-pro": "^7.2.0",
|
|
52
52
|
"@fortawesome/fontawesome-svg-core": "^7.2.0",
|
package/styles/vars.scss
CHANGED
|
@@ -344,7 +344,12 @@ body {
|
|
|
344
344
|
--xh-button-active-text-color: var(--button-active-text-color, white);
|
|
345
345
|
--xh-button-bg: var(--button-bg, #{mc('blue-grey', '600')});
|
|
346
346
|
--xh-button-text-color: var(--button-text-color, white);
|
|
347
|
-
|
|
347
|
+
// Default to a comfortable touch height for buttons in body content - a bit more compact than
|
|
348
|
+
// the 44px toolbar/touch-target max, which reads as oversized for XH's denser style. Toolbars
|
|
349
|
+
// derive their own slightly smaller button height from the bar size (see Toolbar.scss).
|
|
350
|
+
--xh-button-height: var(--button-height, 40px);
|
|
351
|
+
// Slightly rounder corners to suit the taller mobile button (the desktop default is tighter).
|
|
352
|
+
--xh-button-border-radius-px: var(--button-border-radius-px, 8px);
|
|
348
353
|
--xh-toolbar-button-bg: var(--toolbar-button-bg, var(--xh-button-bg));
|
|
349
354
|
|
|
350
355
|
// Light theme pressed bg for 'minimal' / 'outlined'
|
|
@@ -839,6 +844,21 @@ body {
|
|
|
839
844
|
--xh-tab-font-size: var(--tab-font-size, var(--xh-font-size));
|
|
840
845
|
--xh-tab-font-size-px: calc(var(--xh-tab-font-size) * 1px);
|
|
841
846
|
|
|
847
|
+
// Vertical (left/right) TabSwitcher - the styled "rail" of rounded, hoverable items with a
|
|
848
|
+
// filled active "pill". All overridable via the unprefixed hooks.
|
|
849
|
+
--xh-tab-switcher-vertical-min-width: var(--tab-switcher-vertical-min-width, auto);
|
|
850
|
+
--xh-tab-switcher-vertical-padding: var(--tab-switcher-vertical-padding, var(--xh-pad-half-px));
|
|
851
|
+
--xh-tab-switcher-vertical-border: var(--tab-switcher-vertical-border, var(--xh-border-solid));
|
|
852
|
+
--xh-tab-switcher-vertical-item-gap: var(--tab-switcher-vertical-item-gap, 2px);
|
|
853
|
+
--xh-tab-switcher-vertical-item-padding: var(--tab-switcher-vertical-item-padding, var(--xh-pad-half-px) var(--xh-pad-px));
|
|
854
|
+
--xh-tab-switcher-vertical-item-border-radius: var(--tab-switcher-vertical-item-border-radius, var(--xh-border-radius-px));
|
|
855
|
+
--xh-tab-switcher-vertical-item-text-color: var(--tab-switcher-vertical-item-text-color, var(--xh-text-color-muted));
|
|
856
|
+
--xh-tab-switcher-vertical-item-active-text-color: var(--tab-switcher-vertical-item-active-text-color, var(--xh-tab-active-text-color));
|
|
857
|
+
--xh-tab-switcher-vertical-item-hover-bg: var(--tab-switcher-vertical-item-hover-bg, var(--xh-bg-alt));
|
|
858
|
+
--xh-tab-switcher-vertical-item-active-bg: var(--tab-switcher-vertical-item-active-bg, var(--xh-bg-highlight));
|
|
859
|
+
--xh-tab-switcher-vertical-item-font-weight: var(--tab-switcher-vertical-item-font-weight, 500);
|
|
860
|
+
--xh-tab-switcher-vertical-item-active-font-weight: var(--tab-switcher-vertical-item-active-font-weight, 600);
|
|
861
|
+
|
|
842
862
|
&.xh-dark {
|
|
843
863
|
--xh-tab-active-text-color: var(--tab-active-text-color, var(--xh-intent-primary-lighter));
|
|
844
864
|
}
|
package/svc/JsonBlobService.ts
CHANGED
|
@@ -52,8 +52,8 @@ export class JsonBlobService extends HoistService {
|
|
|
52
52
|
static instance: JsonBlobService;
|
|
53
53
|
|
|
54
54
|
/** Retrieve a single JSONBlob by its unique token. */
|
|
55
|
-
async getAsync(token: string): Promise<JsonBlob> {
|
|
56
|
-
return this.runner().span('get').fetchJson({
|
|
55
|
+
async getAsync(token: string, ctx?: CallContextLike): Promise<JsonBlob> {
|
|
56
|
+
return this.runner(ctx).span('get').fetchJson({
|
|
57
57
|
url: 'xh/getJsonBlob',
|
|
58
58
|
params: {token}
|
|
59
59
|
});
|
|
@@ -63,7 +63,7 @@ export class JsonBlobService extends HoistService {
|
|
|
63
63
|
async listAsync(spec: {
|
|
64
64
|
type: string;
|
|
65
65
|
includeValue?: boolean;
|
|
66
|
-
ctx
|
|
66
|
+
ctx?: CallContextLike;
|
|
67
67
|
}): Promise<JsonBlob[]> {
|
|
68
68
|
const {type, includeValue, ctx} = spec;
|
|
69
69
|
return this.runner(ctx)
|
|
@@ -72,15 +72,11 @@ export class JsonBlobService extends HoistService {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
/** Persist a new JSONBlob back to the server. */
|
|
75
|
-
async createAsync(
|
|
76
|
-
acl,
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
name,
|
|
81
|
-
value
|
|
82
|
-
}: Partial<JsonBlob>): Promise<JsonBlob> {
|
|
83
|
-
return this.runner()
|
|
75
|
+
async createAsync(
|
|
76
|
+
{acl, description, type, meta, name, value}: Partial<JsonBlob>,
|
|
77
|
+
ctx?: CallContextLike
|
|
78
|
+
): Promise<JsonBlob> {
|
|
79
|
+
return this.runner(ctx)
|
|
84
80
|
.span('create')
|
|
85
81
|
.fetchJson({
|
|
86
82
|
url: 'xh/createJsonBlob',
|
|
@@ -91,8 +87,12 @@ export class JsonBlobService extends HoistService {
|
|
|
91
87
|
}
|
|
92
88
|
|
|
93
89
|
/** Modify mutable properties of an existing JSONBlob, as identified by its unique token. */
|
|
94
|
-
async updateAsync(
|
|
95
|
-
|
|
90
|
+
async updateAsync(
|
|
91
|
+
token: string,
|
|
92
|
+
update: Partial<JsonBlob>,
|
|
93
|
+
ctx?: CallContextLike
|
|
94
|
+
): Promise<JsonBlob> {
|
|
95
|
+
return this.runner(ctx)
|
|
96
96
|
.span('update')
|
|
97
97
|
.run(async ctx => {
|
|
98
98
|
update = pick(update, ['acl', 'description', 'meta', 'name', 'owner', 'value']);
|
|
@@ -110,9 +110,10 @@ export class JsonBlobService extends HoistService {
|
|
|
110
110
|
async createOrUpdateAsync(
|
|
111
111
|
type: string,
|
|
112
112
|
name: string,
|
|
113
|
-
data: Partial<JsonBlob
|
|
113
|
+
data: Partial<JsonBlob>,
|
|
114
|
+
ctx?: CallContextLike
|
|
114
115
|
): Promise<JsonBlob> {
|
|
115
|
-
return this.runner()
|
|
116
|
+
return this.runner(ctx)
|
|
116
117
|
.span('createOrUpdate')
|
|
117
118
|
.run(async ctx => {
|
|
118
119
|
const update = pick(data, ['acl', 'description', 'meta', 'value']);
|
|
@@ -127,16 +128,16 @@ export class JsonBlobService extends HoistService {
|
|
|
127
128
|
}
|
|
128
129
|
|
|
129
130
|
/** Find a blob owned by this user with a specific type and name. If none exists, return null. */
|
|
130
|
-
async findAsync(type: string, name: string): Promise<JsonBlob> {
|
|
131
|
-
return this.runner().span('find').fetchJson({
|
|
131
|
+
async findAsync(type: string, name: string, ctx?: CallContextLike): Promise<JsonBlob> {
|
|
132
|
+
return this.runner(ctx).span('find').fetchJson({
|
|
132
133
|
url: 'xh/findJsonBlob',
|
|
133
134
|
params: {type, name}
|
|
134
135
|
});
|
|
135
136
|
}
|
|
136
137
|
|
|
137
138
|
/** Archive (soft-delete) an existing JSONBlob, as identified by its unique token. */
|
|
138
|
-
async archiveAsync(token: string): Promise<JsonBlob> {
|
|
139
|
-
return this.runner().span('archive').fetchJson({
|
|
139
|
+
async archiveAsync(token: string, ctx?: CallContextLike): Promise<JsonBlob> {
|
|
140
|
+
return this.runner(ctx).span('archive').fetchJson({
|
|
140
141
|
url: 'xh/archiveJsonBlob',
|
|
141
142
|
params: {token}
|
|
142
143
|
});
|