@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
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/format/FormatNumber.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
isNil,
|
|
15
15
|
isNumber,
|
|
16
16
|
isString,
|
|
17
|
+
isUndefined,
|
|
17
18
|
round
|
|
18
19
|
} from 'lodash';
|
|
19
20
|
import Numbro from 'numbro';
|
|
@@ -78,8 +79,9 @@ export interface NumberFormatOptions extends Omit<FormatOptions<number>, 'toolti
|
|
|
78
79
|
omitFourDigitComma?: boolean;
|
|
79
80
|
|
|
80
81
|
/**
|
|
81
|
-
* Desired number of decimal places,
|
|
82
|
-
* automatically based on the scale of the value
|
|
82
|
+
* Desired number of decimal places, 'auto' (default) to adjust the displayed precision
|
|
83
|
+
* automatically based on the scale of the value, or null for full, unrestricted precision
|
|
84
|
+
* (capped at the max supported precision, with trailing zeros trimmed).
|
|
83
85
|
*/
|
|
84
86
|
precision?: Precision;
|
|
85
87
|
|
|
@@ -118,15 +120,23 @@ export interface NumberFormatOptions extends Omit<FormatOptions<number>, 'toolti
|
|
|
118
120
|
*
|
|
119
121
|
* e.g. `{precision:4, zeroPad:2}` will format `1.2` → "1.20" and `1.234` → "1.234"
|
|
120
122
|
*
|
|
121
|
-
* Default is true if a fixed precision is set, false if precision is 'auto'.
|
|
123
|
+
* Default is true if a fixed precision is set, false if precision is 'auto' or null (full).
|
|
122
124
|
*/
|
|
123
125
|
zeroPad?: ZeroPad;
|
|
124
126
|
}
|
|
125
127
|
|
|
126
128
|
export interface QuantityFormatOptions extends NumberFormatOptions {
|
|
129
|
+
/** True to compact values \>= 1 million into units of millions (m). Default true. */
|
|
127
130
|
useMillions?: boolean;
|
|
128
131
|
|
|
132
|
+
/** True to compact values \>= 1 billion into units of billions (b). Default true. */
|
|
129
133
|
useBillions?: boolean;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* True to compact to m/b units only when no precision is lost, else render the full value.
|
|
137
|
+
* Default false.
|
|
138
|
+
*/
|
|
139
|
+
lossless?: boolean;
|
|
130
140
|
}
|
|
131
141
|
|
|
132
142
|
/** Config for pos/neg/neutral color classes. */
|
|
@@ -175,9 +185,16 @@ export function fmtNumber(v: number, opts?: NumberFormatOptions): ReactNode {
|
|
|
175
185
|
} = opts ?? ({} as NumberFormatOptions);
|
|
176
186
|
if (isInvalidInput(v)) return nullDisplay;
|
|
177
187
|
|
|
178
|
-
//
|
|
179
|
-
|
|
180
|
-
if (
|
|
188
|
+
// Resolve precision: null means full precision, other non-integers (e.g. undefined) mean 'auto'.
|
|
189
|
+
const fullPrecision = precision === null;
|
|
190
|
+
if (fullPrecision) {
|
|
191
|
+
precision = MAX_NUMERIC_PRECISION;
|
|
192
|
+
} else if (!isInteger(precision)) {
|
|
193
|
+
precision = 'auto';
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Default zeroPad to pad only for a fixed precision - 'auto' and full precision trim zeros.
|
|
197
|
+
if (isNil(zeroPad)) zeroPad = precision != 'auto' && !fullPrecision;
|
|
181
198
|
|
|
182
199
|
formatConfig =
|
|
183
200
|
formatConfig || buildFormatConfig(v, precision, zeroPad, withCommas, omitFourDigitComma);
|
|
@@ -271,20 +288,40 @@ export function fmtQuantity(v: number, opts?: QuantityFormatOptions) {
|
|
|
271
288
|
saveOriginal(v, opts);
|
|
272
289
|
if (isInvalidInput(v)) return fmtNumber(v, opts);
|
|
273
290
|
|
|
274
|
-
const lessThanM = Math.abs(v) < MILLION,
|
|
275
|
-
lessThanB = Math.abs(v) < BILLION;
|
|
276
|
-
|
|
277
291
|
defaults(opts, {
|
|
278
292
|
ledger: true,
|
|
279
293
|
label: true,
|
|
280
|
-
precision: lessThanM ? 0 : 2,
|
|
281
294
|
useMillions: true,
|
|
282
|
-
useBillions: true
|
|
295
|
+
useBillions: true,
|
|
296
|
+
lossless: false
|
|
283
297
|
});
|
|
284
298
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
299
|
+
const absV = Math.abs(v),
|
|
300
|
+
lessM = absV < MILLION,
|
|
301
|
+
lessB = absV < BILLION,
|
|
302
|
+
targetPrecision = opts.precision ?? (lessM ? 0 : 2);
|
|
303
|
+
|
|
304
|
+
// Compute scaling, if any (Lossless flag may preclude).
|
|
305
|
+
let scale = !lessB && opts.useBillions ? BILLION : !lessM && opts.useMillions ? MILLION : null;
|
|
306
|
+
if (scale && opts.lossless) {
|
|
307
|
+
const precision = parsePrecision(absV / scale, targetPrecision),
|
|
308
|
+
lossy = v % (scale / 10 ** precision) !== 0;
|
|
309
|
+
if (lossy) scale = null;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Resolve render precision (unless the caller set one).
|
|
313
|
+
if (isUndefined(opts.precision)) {
|
|
314
|
+
opts.precision = opts.lossless ? null : targetPrecision;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
switch (scale) {
|
|
318
|
+
case BILLION:
|
|
319
|
+
return fmtBillions(v, opts);
|
|
320
|
+
case MILLION:
|
|
321
|
+
return fmtMillions(v, opts);
|
|
322
|
+
default:
|
|
323
|
+
return fmtNumber(v, opts);
|
|
324
|
+
}
|
|
288
325
|
}
|
|
289
326
|
|
|
290
327
|
/**
|
|
@@ -462,6 +499,23 @@ function calcStyleFromColorSpec(v: number, colorSpec: ColorSpec | boolean): CSSP
|
|
|
462
499
|
return !isString(possibleStyles) ? possibleStyles : {};
|
|
463
500
|
}
|
|
464
501
|
|
|
502
|
+
/**
|
|
503
|
+
* Resolve a precisionSpec to a concrete number of decimal places. A fixed precision is used as-is
|
|
504
|
+
* (capped at max), while 'auto' is derived from the scale of the value.
|
|
505
|
+
*/
|
|
506
|
+
function parsePrecision(v: number, precisionSpec: Precision): number {
|
|
507
|
+
// Fixed precision - use requested, capped at max.
|
|
508
|
+
if (precisionSpec !== 'auto') return Math.min(precisionSpec, MAX_NUMERIC_PRECISION);
|
|
509
|
+
|
|
510
|
+
// 'auto' - derive from the scale of the value.
|
|
511
|
+
const absVal = Math.abs(v);
|
|
512
|
+
if (absVal === 0) return 2;
|
|
513
|
+
if (absVal < 0.01) return 6;
|
|
514
|
+
if (absVal < 100) return 4;
|
|
515
|
+
if (absVal < 10000) return 2;
|
|
516
|
+
return 0;
|
|
517
|
+
}
|
|
518
|
+
|
|
465
519
|
function buildFormatConfig(
|
|
466
520
|
v: number,
|
|
467
521
|
precisionSpec: Precision,
|
|
@@ -472,24 +526,7 @@ function buildFormatConfig(
|
|
|
472
526
|
const absVal = Math.abs(v),
|
|
473
527
|
config: Numbro.Format = {};
|
|
474
528
|
|
|
475
|
-
|
|
476
|
-
if (precisionSpec === 'auto') {
|
|
477
|
-
// Auto-precision - base on scale of number
|
|
478
|
-
if (absVal === 0) {
|
|
479
|
-
precision = 2;
|
|
480
|
-
} else if (absVal < 0.01) {
|
|
481
|
-
precision = 6;
|
|
482
|
-
} else if (absVal < 100) {
|
|
483
|
-
precision = 4;
|
|
484
|
-
} else if (absVal < 10000) {
|
|
485
|
-
precision = 2;
|
|
486
|
-
} else {
|
|
487
|
-
precision = 0;
|
|
488
|
-
}
|
|
489
|
-
} else {
|
|
490
|
-
// Fixed precision - use requested, capped at max.
|
|
491
|
-
precision = precisionSpec < MAX_NUMERIC_PRECISION ? precisionSpec : MAX_NUMERIC_PRECISION;
|
|
492
|
-
}
|
|
529
|
+
const precision = parsePrecision(v, precisionSpec);
|
|
493
530
|
|
|
494
531
|
// If zeroPad gte precision, treat as `true` to pad out to (but not beyond) full precision.
|
|
495
532
|
// We don't support applying some precision (rounding) then padding out zeroes after that.
|
package/icon/Icon.scss
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
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
|
+
// FA v7 sizes icons via the `--fa-width` custom property (default 1.25em) rather than the old
|
|
9
|
+
// `fa-fw` class. The placeholder is a plain div, so it reads the same property to stay aligned
|
|
10
|
+
// with sibling icons under any width override, falling back to FA's default when unset.
|
|
11
|
+
.xh-icon--placeholder {
|
|
12
|
+
width: var(--fa-width, 1.25em);
|
|
13
|
+
}
|
package/icon/Icon.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {ReactElement} from 'react';
|
|
|
15
15
|
import {iconCmp} from './impl/IconCmp';
|
|
16
16
|
import {enhanceFaClasses, iconHtml} from './impl/IconHtml';
|
|
17
17
|
import {SetRequired} from 'type-fest';
|
|
18
|
+
import './Icon.scss';
|
|
18
19
|
|
|
19
20
|
export interface IconProps extends HoistProps, Partial<Omit<FontAwesomeIconProps, 'ref'>> {
|
|
20
21
|
/** Name of the icon in FontAwesome. */
|
|
@@ -651,6 +652,9 @@ export const Icon = {
|
|
|
651
652
|
shieldCheck(p?: IconProps) {
|
|
652
653
|
return Icon.icon({...p, iconName: 'shield-check'});
|
|
653
654
|
},
|
|
655
|
+
shieldHalved(p?: IconProps) {
|
|
656
|
+
return Icon.icon({...p, iconName: 'shield-halved'});
|
|
657
|
+
},
|
|
654
658
|
sigma(p?: IconProps) {
|
|
655
659
|
return Icon.icon({...p, iconName: 'sigma'});
|
|
656
660
|
},
|
|
@@ -929,9 +933,9 @@ export const Icon = {
|
|
|
929
933
|
},
|
|
930
934
|
|
|
931
935
|
/**
|
|
932
|
-
* Returns an empty div
|
|
933
|
-
* where an icon might otherwise go - e.g. to align a series of menu items,
|
|
934
|
-
* not have an icon but others do.
|
|
936
|
+
* Returns an empty div sized to occupy the width of a standard icon. Can be used to take up
|
|
937
|
+
* room in a layout where an icon might otherwise go - e.g. to align a series of menu items,
|
|
938
|
+
* where some items do not have an icon but others do.
|
|
935
939
|
*/
|
|
936
940
|
placeholder(opts?: IconProps): any {
|
|
937
941
|
const {size, asHtml = false} = opts ?? {},
|
package/icon/impl/IconHtml.ts
CHANGED
|
@@ -32,5 +32,5 @@ export function iconHtml({
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
export function enhanceFaClasses(className: string, size: string) {
|
|
35
|
-
return classNames(className, '
|
|
35
|
+
return classNames(className, 'xh-icon', isString(size) ? `fa-${size}` : null);
|
|
36
36
|
}
|
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,
|
package/mcp/data/ts-registry.ts
CHANGED
|
@@ -1432,7 +1432,7 @@ function extractSymbolDetail(entry: SymbolEntry): SymbolDetail | null {
|
|
|
1432
1432
|
*
|
|
1433
1433
|
* `JSDoc.getDescription()` returns the text before the first JSDoc block tag,
|
|
1434
1434
|
* but the TS parser treats ANY line whose first non-whitespace char is `@` as a
|
|
1435
|
-
* tag boundary - even inside a fenced
|
|
1435
|
+
* tag boundary - even inside a fenced code block. So a canonical-usage
|
|
1436
1436
|
* example containing e.g. `@observable.ref` silently truncates the description
|
|
1437
1437
|
* mid-example, dropping everything after it (further examples, "SEE ALSO"
|
|
1438
1438
|
* lists, trailing prose). See #4352.
|
|
@@ -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
|
|
@@ -21,7 +21,8 @@ export interface ExpandToLevelButtonProps extends MenuButtonProps {
|
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* A menu button to expand a multi-level grouped or tree grid out to a desired level.
|
|
24
|
-
* Requires {@link GridConfig.levelLabels} to be configured on the bound GridModel
|
|
24
|
+
* Requires {@link GridConfig.levelLabels} to be configured on the bound GridModel - the menu offers
|
|
25
|
+
* one entry per labelled level, so a partial array will limit the available expand-to targets.
|
|
25
26
|
*/
|
|
26
27
|
export const [ExpandToLevelButton, expandToLevelButton] =
|
|
27
28
|
hoistCmp.withFactory<ExpandToLevelButtonProps>({
|
|
@@ -51,13 +52,11 @@ export const [ExpandToLevelButton, expandToLevelButton] =
|
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
// Render a disabled button if requested or if we have a flat grid, or no level labels
|
|
54
|
-
const {maxDepth,
|
|
55
|
+
const {maxDepth, resolvedLevelLabels} = gridModel;
|
|
55
56
|
if (disabled || !maxDepth || !resolvedLevelLabels) return disabledButton();
|
|
56
57
|
|
|
57
58
|
const menuItems: MenuItemLike[] = resolvedLevelLabels.map((label, idx) => {
|
|
58
|
-
const isCurrLevel =
|
|
59
|
-
expandLevel === idx ||
|
|
60
|
-
(expandLevel > maxDepth && idx === resolvedLevelLabels.length - 1);
|
|
59
|
+
const isCurrLevel = gridModel.isCurrentExpandLevel(idx);
|
|
61
60
|
|
|
62
61
|
return {
|
|
63
62
|
icon: isCurrLevel ? Icon.check() : Icon.placeholder(),
|
|
@@ -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',
|