@xeplr/ui-utils 1.0.0 → 1.0.2
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/package.json +25 -6
- package/src/confirm/confirm.js +175 -0
- package/src/dataImport/dataImport.js +32 -0
- package/src/dataImport/designs/DataImportSample.jsx +71 -0
- package/src/dataImport/designs/ImportHistorySample.jsx +130 -0
- package/src/dataImport/designs/dataImport.css +98 -0
- package/src/dataImport/designs/index.js +2 -0
- package/src/dataImport/pages.jsx +17 -0
- package/src/dataImport/useDataImportController.js +94 -0
- package/src/dataImport/useImportHistoryController.js +67 -0
- package/src/dataImport/validateDesign.js +68 -0
- package/src/dateField/dateField.js +71 -0
- package/src/dateField/designs/DateFieldSample.jsx +30 -0
- package/src/dateField/designs/dateField.css +60 -0
- package/src/dateField/designs/index.js +1 -0
- package/src/dateField/pages.jsx +10 -0
- package/src/dateField/useDateFieldController.js +77 -0
- package/src/dateField/validateDesign.js +3 -0
- package/src/dateRange/dateRange.js +273 -0
- package/src/dateRange/designs/DateRangeSample.jsx +138 -0
- package/src/dateRange/designs/dateRange.css +69 -0
- package/src/dateRange/designs/index.js +1 -0
- package/src/dateRange/pages.jsx +10 -0
- package/src/dateRange/useDateRangeController.js +118 -0
- package/src/dateRange/validateDesign.js +20 -0
- package/src/dropdown/designs/DropdownSample.jsx +151 -0
- package/src/dropdown/designs/dropdown.css +226 -0
- package/src/dropdown/designs/index.js +1 -0
- package/src/dropdown/dropdown.js +75 -0
- package/src/dropdown/pages.jsx +10 -0
- package/src/dropdown/useDropdownController.js +254 -0
- package/src/dropdown/validateDesign.js +4 -0
- package/src/fileUpload/validateDesign.js +27 -8
- package/src/gridDisplayer/designs/GridDisplayerSample.jsx +58 -0
- package/src/gridDisplayer/designs/gridDisplayer.css +89 -0
- package/src/gridDisplayer/designs/index.js +1 -0
- package/src/gridDisplayer/gridDisplayer.js +44 -0
- package/src/gridDisplayer/pages.jsx +10 -0
- package/src/gridDisplayer/useGridDisplayerController.js +59 -0
- package/src/gridDisplayer/validateDesign.js +4 -0
- package/src/index.js +177 -1
- package/src/numberField/designs/NumberFieldSample.jsx +52 -0
- package/src/numberField/designs/index.js +1 -0
- package/src/numberField/designs/numberField.css +66 -0
- package/src/numberField/numberField.js +105 -0
- package/src/numberField/pages.jsx +10 -0
- package/src/numberField/useNumberFieldController.js +116 -0
- package/src/numberField/validateDesign.js +3 -0
- package/src/progress/ProgressNotifier.jsx +118 -0
- package/src/progress/designs/progressNotifier.css +73 -0
- package/src/progress/progressSource.js +168 -0
- package/src/rangeField/designs/RangeFieldSample.jsx +25 -0
- package/src/rangeField/designs/index.js +1 -0
- package/src/rangeField/designs/rangeField.css +26 -0
- package/src/rangeField/pages.jsx +10 -0
- package/src/rangeField/rangeField.js +29 -0
- package/src/rangeField/useRangeFieldController.js +52 -0
- package/src/rangeField/validateDesign.js +3 -0
- package/src/snackbar/snackbar.js +6 -3
- package/src/textField/designs/TextFieldSample.jsx +75 -0
- package/src/textField/designs/index.js +1 -0
- package/src/textField/designs/textField.css +92 -0
- package/src/textField/pages.jsx +10 -0
- package/src/textField/textField.js +65 -0
- package/src/textField/useTextFieldController.js +101 -0
- package/src/textField/validateDesign.js +3 -0
- package/src/theme/default.theme.json +934 -0
- package/src/theme/index.js +155 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/* Uses @xeplr/ui-account's --xeplr-* theme tokens when available (apply
|
|
2
|
+
.xeplr-theme-light/-dark on an ancestor); falls back to the original dark
|
|
3
|
+
hex values when that stylesheet isn't loaded. */
|
|
4
|
+
|
|
5
|
+
.xeplr-grid-displayer {
|
|
6
|
+
display: flex;
|
|
7
|
+
flex-direction: column;
|
|
8
|
+
gap: 14px;
|
|
9
|
+
color: var(--xeplr-text-primary, #e0e0e0);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/* ─── Toolbar ─── */
|
|
13
|
+
|
|
14
|
+
.xeplr-grid-toolbar {
|
|
15
|
+
display: flex;
|
|
16
|
+
align-items: center;
|
|
17
|
+
justify-content: space-between;
|
|
18
|
+
gap: 10px;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.xeplr-grid-search {
|
|
22
|
+
flex: 1;
|
|
23
|
+
max-width: 320px;
|
|
24
|
+
padding: 8px 12px;
|
|
25
|
+
border: 1px solid var(--xeplr-input-border, #3a3a3a);
|
|
26
|
+
border-radius: 8px;
|
|
27
|
+
background: var(--xeplr-input-bg, #141414);
|
|
28
|
+
color: var(--xeplr-input-text, #e0e0e0);
|
|
29
|
+
font-size: 14px;
|
|
30
|
+
}
|
|
31
|
+
.xeplr-grid-search:focus { outline: none; border-color: var(--xeplr-accent, #646cff); }
|
|
32
|
+
|
|
33
|
+
/* One dropdown carries both field + direction (e.g. "Name (A-Z)") — no
|
|
34
|
+
separate asc/desc icon button to keep the toolbar clean. */
|
|
35
|
+
.xeplr-grid-sort-select {
|
|
36
|
+
padding: 7px 10px;
|
|
37
|
+
border: 1px solid var(--xeplr-input-border, #3a3a3a);
|
|
38
|
+
border-radius: 8px;
|
|
39
|
+
background: var(--xeplr-input-bg, #141414);
|
|
40
|
+
color: var(--xeplr-input-text, #e0e0e0);
|
|
41
|
+
font-size: 13px;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/* ─── Grid + tiles ───
|
|
45
|
+
Netflix-style: a hovered tile scales up and lifts above its neighbors —
|
|
46
|
+
z-index only bumps on hover so it doesn't otherwise disturb grid stacking. */
|
|
47
|
+
|
|
48
|
+
.xeplr-grid {
|
|
49
|
+
display: grid;
|
|
50
|
+
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
51
|
+
gap: 16px;
|
|
52
|
+
/* room for hovered tiles to scale up without clipping against the container edge */
|
|
53
|
+
padding: 6px;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.xeplr-grid-tile {
|
|
57
|
+
position: relative;
|
|
58
|
+
border-radius: 14px;
|
|
59
|
+
background: var(--xeplr-bg-tertiary, #1a1a1a);
|
|
60
|
+
border: 1px solid var(--xeplr-border-secondary, #2a2a2a);
|
|
61
|
+
overflow: hidden;
|
|
62
|
+
cursor: pointer;
|
|
63
|
+
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
|
|
64
|
+
transform-origin: center;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.xeplr-grid-tile:hover {
|
|
68
|
+
transform: scale(1.08);
|
|
69
|
+
z-index: 5;
|
|
70
|
+
border-color: var(--xeplr-accent, #646cff);
|
|
71
|
+
box-shadow: var(--xeplr-shadow-lg, 0 12px 28px rgba(0, 0, 0, 0.45));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.xeplr-grid-tile-add {
|
|
75
|
+
border-style: dashed;
|
|
76
|
+
border-color: var(--xeplr-border-primary, #444);
|
|
77
|
+
display: grid;
|
|
78
|
+
place-items: center;
|
|
79
|
+
min-height: 120px;
|
|
80
|
+
color: var(--xeplr-text-muted, #888);
|
|
81
|
+
}
|
|
82
|
+
.xeplr-grid-tile-add:hover { border-color: var(--xeplr-accent, #646cff); color: var(--xeplr-text-primary, #e0e0e0); }
|
|
83
|
+
|
|
84
|
+
.xeplr-grid-empty {
|
|
85
|
+
grid-column: 1 / -1;
|
|
86
|
+
padding: 32px;
|
|
87
|
+
text-align: center;
|
|
88
|
+
color: var(--xeplr-text-muted, #777);
|
|
89
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as GridDisplayerSample } from './GridDisplayerSample.jsx';
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GridDisplayer — pure model. No React.
|
|
3
|
+
* Client-side search + sort over a plain `rows` array, same philosophy as
|
|
4
|
+
* CrudDisplayer/Dropdown — no fetch function, no server round-trip.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Substring match (case-insensitive) across `searchFields`. A field whose
|
|
9
|
+
* value is an array (e.g. tags) matches if ANY entry contains the query.
|
|
10
|
+
*/
|
|
11
|
+
export function filterRows(rows, query, searchFields) {
|
|
12
|
+
if (!query) return rows || [];
|
|
13
|
+
var q = String(query).toLowerCase();
|
|
14
|
+
return (rows || []).filter(function(row) {
|
|
15
|
+
for (var i = 0; i < searchFields.length; i++) {
|
|
16
|
+
var value = row[searchFields[i]];
|
|
17
|
+
if (value == null) continue;
|
|
18
|
+
if (Array.isArray(value)) {
|
|
19
|
+
if (value.some(function(v) { return String(v).toLowerCase().indexOf(q) !== -1; })) return true;
|
|
20
|
+
} else if (String(value).toLowerCase().indexOf(q) !== -1) {
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return false;
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function compareValues(a, b) {
|
|
29
|
+
if (a == null && b == null) return 0;
|
|
30
|
+
if (a == null) return -1;
|
|
31
|
+
if (b == null) return 1;
|
|
32
|
+
if (typeof a === 'number' && typeof b === 'number') return a - b;
|
|
33
|
+
return String(a).toLowerCase().localeCompare(String(b).toLowerCase());
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function sortRows(rows, field, direction) {
|
|
37
|
+
if (!field) return rows || [];
|
|
38
|
+
var copy = (rows || []).slice();
|
|
39
|
+
copy.sort(function(a, b) {
|
|
40
|
+
var result = compareValues(a[field], b[field]);
|
|
41
|
+
return direction === 'desc' ? -result : result;
|
|
42
|
+
});
|
|
43
|
+
return copy;
|
|
44
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { useGridDisplayerController } from './useGridDisplayerController.js';
|
|
2
|
+
import GridDisplayerSample from './designs/GridDisplayerSample.jsx';
|
|
3
|
+
import { useDesignValidator } from '../fileUpload/validateDesign.js';
|
|
4
|
+
import { GRID_DISPLAYER_RULES } from './validateDesign.js';
|
|
5
|
+
|
|
6
|
+
export function GridDisplayerPage(props) {
|
|
7
|
+
var controller = useGridDisplayerController(props);
|
|
8
|
+
var ref = useDesignValidator('GridDisplayerPage', GRID_DISPLAYER_RULES);
|
|
9
|
+
return <div ref={ref}><GridDisplayerSample {...controller} /></div>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { useState, useMemo } from 'react';
|
|
2
|
+
import { filterRows, sortRows } from './gridDisplayer.js';
|
|
3
|
+
|
|
4
|
+
// Module-level (not recreated per render) — used as fallbacks below so a
|
|
5
|
+
// caller that omits rows/searchFields/sortFields doesn't hand displayRows'
|
|
6
|
+
// useMemo a fresh array identity every render, which would defeat the memo.
|
|
7
|
+
var DEFAULT_ROWS = [];
|
|
8
|
+
var DEFAULT_SEARCH_FIELDS = ['name'];
|
|
9
|
+
// Each sortFields entry is a fully-resolved sort choice — field + direction
|
|
10
|
+
// + label combined into one dropdown option (e.g. "Name (A-Z)") rather than
|
|
11
|
+
// a bare field name paired with a separate asc/desc toggle button. One
|
|
12
|
+
// dropdown, no extra icon button to keep clean.
|
|
13
|
+
var DEFAULT_SORT_FIELDS = [{ value: 'name_asc', label: 'Name (A-Z)', field: 'name', direction: 'asc' }];
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {object} props
|
|
17
|
+
* @param {Array<object>} props.rows
|
|
18
|
+
* @param {*} props.tileFormatComponent - component rendering a tile's CONTENT only
|
|
19
|
+
* (the design owns the clickable/hover-zoom wrapper around it) — receives
|
|
20
|
+
* { row } for a normal tile, or { isAddTile: true } for the leading "add new"
|
|
21
|
+
* tile (only rendered when props.onAddNew is supplied).
|
|
22
|
+
* @param {Array<string>} [props.searchFields=['name']] - row fields the search box matches
|
|
23
|
+
* @param {Array<{value:string, label:string, field:string, direction:'asc'|'desc'}>} [props.sortFields]
|
|
24
|
+
* - default: [{value:'name_asc', label:'Name (A-Z)', field:'name', direction:'asc'}]
|
|
25
|
+
* @param {(row:object)=>void} [props.onSelect] - tile click (non-add tile)
|
|
26
|
+
* @param {()=>void} [props.onAddNew] - if provided, a leading "add new" tile renders
|
|
27
|
+
*/
|
|
28
|
+
export function useGridDisplayerController(props) {
|
|
29
|
+
props = props || {};
|
|
30
|
+
var rows = props.rows || DEFAULT_ROWS;
|
|
31
|
+
var searchFields = props.searchFields || DEFAULT_SEARCH_FIELDS;
|
|
32
|
+
var sortFields = props.sortFields || DEFAULT_SORT_FIELDS;
|
|
33
|
+
|
|
34
|
+
var [query, setQuery] = useState('');
|
|
35
|
+
var [sortValue, setSortValue] = useState(sortFields[0] ? sortFields[0].value : null);
|
|
36
|
+
|
|
37
|
+
var activeSort = null;
|
|
38
|
+
for (var i = 0; i < sortFields.length; i++) {
|
|
39
|
+
if (sortFields[i].value === sortValue) { activeSort = sortFields[i]; break; }
|
|
40
|
+
}
|
|
41
|
+
if (!activeSort) activeSort = sortFields[0] || null;
|
|
42
|
+
|
|
43
|
+
var displayRows = useMemo(function() {
|
|
44
|
+
var filtered = filterRows(rows, query, searchFields);
|
|
45
|
+
return sortRows(filtered, activeSort ? activeSort.field : null, activeSort ? activeSort.direction : 'asc');
|
|
46
|
+
}, [rows, query, searchFields, activeSort]);
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
query: query,
|
|
50
|
+
setQuery: setQuery,
|
|
51
|
+
sortFields: sortFields,
|
|
52
|
+
sortValue: activeSort ? activeSort.value : null,
|
|
53
|
+
setSortValue: setSortValue,
|
|
54
|
+
displayRows: displayRows,
|
|
55
|
+
tileFormatComponent: props.tileFormatComponent,
|
|
56
|
+
onSelect: props.onSelect,
|
|
57
|
+
onAddNew: props.onAddNew
|
|
58
|
+
};
|
|
59
|
+
}
|
package/src/index.js
CHANGED
|
@@ -10,14 +10,190 @@ export { useDesignValidator, FILE_UPLOAD_RULES } from './fileUpload/validateDesi
|
|
|
10
10
|
// File Upload — sample design
|
|
11
11
|
export { FileUploadSample } from './fileUpload/designs/index.js';
|
|
12
12
|
|
|
13
|
-
// File Upload — ready-made page
|
|
13
|
+
// File Upload — ready-made page
|
|
14
14
|
export { FileUploadPage } from './fileUpload/pages.jsx';
|
|
15
15
|
|
|
16
|
+
// DataImport — model. Upload a file, map its parts (e.g. Excel sheets — or
|
|
17
|
+
// the whole file, for CSV/JSON) to app-defined named targets, commit. The
|
|
18
|
+
// app supplies onInspect/onCommit (its own upload + load endpoints); this
|
|
19
|
+
// module only owns the file-pick -> mapping -> commit flow and UI.
|
|
20
|
+
export { sanitizeTargetName, buildInitialMappings } from './dataImport/dataImport.js';
|
|
21
|
+
|
|
22
|
+
// DataImport — controller
|
|
23
|
+
export { useDataImportController } from './dataImport/useDataImportController.js';
|
|
24
|
+
|
|
25
|
+
// DataImport — design validation
|
|
26
|
+
export { DATA_IMPORT_RULES } from './dataImport/validateDesign.js';
|
|
27
|
+
|
|
28
|
+
// DataImport — sample design
|
|
29
|
+
export { DataImportSample } from './dataImport/designs/index.js';
|
|
30
|
+
|
|
31
|
+
// DataImport — ready-made page
|
|
32
|
+
export { DataImportPage } from './dataImport/pages.jsx';
|
|
33
|
+
|
|
34
|
+
// ImportHistory — lists past import jobs (app-supplied onList) with a
|
|
35
|
+
// Rollback action per row (app-supplied onRollback) — the read/undo side of
|
|
36
|
+
// DataImportPage's write side, same file/model. Product-agnostic: the app
|
|
37
|
+
// owns what "list" and "rollback" actually call.
|
|
38
|
+
export { useImportHistoryController } from './dataImport/useImportHistoryController.js';
|
|
39
|
+
export { IMPORT_HISTORY_RULES } from './dataImport/validateDesign.js';
|
|
40
|
+
export { ImportHistorySample } from './dataImport/designs/index.js';
|
|
41
|
+
export { ImportHistoryPage } from './dataImport/pages.jsx';
|
|
42
|
+
|
|
43
|
+
// NumberField — model
|
|
44
|
+
export {
|
|
45
|
+
NUMBER_TYPES,
|
|
46
|
+
sanitizeNumberInput,
|
|
47
|
+
toNumeric,
|
|
48
|
+
formatDisplay,
|
|
49
|
+
validateRange,
|
|
50
|
+
resolveAffixState
|
|
51
|
+
} from './numberField/numberField.js';
|
|
52
|
+
|
|
53
|
+
// NumberField — controller
|
|
54
|
+
export { useNumberFieldController } from './numberField/useNumberFieldController.js';
|
|
55
|
+
|
|
56
|
+
// NumberField — design validation
|
|
57
|
+
export { NUMBER_FIELD_RULES } from './numberField/validateDesign.js';
|
|
58
|
+
|
|
59
|
+
// NumberField — sample design
|
|
60
|
+
export { NumberFieldSample } from './numberField/designs/index.js';
|
|
61
|
+
|
|
62
|
+
// NumberField — ready-made page
|
|
63
|
+
export { NumberFieldPage } from './numberField/pages.jsx';
|
|
64
|
+
|
|
65
|
+
// DateField — model
|
|
66
|
+
export {
|
|
67
|
+
DATE_TYPES,
|
|
68
|
+
isValidISODate,
|
|
69
|
+
calculateAge,
|
|
70
|
+
inputTypeFor,
|
|
71
|
+
toInputValue,
|
|
72
|
+
validateBounds
|
|
73
|
+
} from './dateField/dateField.js';
|
|
74
|
+
|
|
75
|
+
// DateField — controller
|
|
76
|
+
export { useDateFieldController } from './dateField/useDateFieldController.js';
|
|
77
|
+
|
|
78
|
+
// DateField — design validation
|
|
79
|
+
export { DATE_FIELD_RULES } from './dateField/validateDesign.js';
|
|
80
|
+
|
|
81
|
+
// DateField — sample design
|
|
82
|
+
export { DateFieldSample } from './dateField/designs/index.js';
|
|
83
|
+
|
|
84
|
+
// DateField — ready-made page
|
|
85
|
+
export { DateFieldPage } from './dateField/pages.jsx';
|
|
86
|
+
|
|
87
|
+
// DateRange — model. One selection covering a quick preset, an explicit
|
|
88
|
+
// range, or a single day/month/year; everything resolves to {from, to}.
|
|
89
|
+
export {
|
|
90
|
+
QUICK_PRESETS,
|
|
91
|
+
parseSingleDate,
|
|
92
|
+
resolveQuickPreset,
|
|
93
|
+
resolveDateValue,
|
|
94
|
+
dateValueLabel,
|
|
95
|
+
dateValueIsSet
|
|
96
|
+
} from './dateRange/dateRange.js';
|
|
97
|
+
|
|
98
|
+
// DateRange — controller
|
|
99
|
+
export { useDateRangeController } from './dateRange/useDateRangeController.js';
|
|
100
|
+
|
|
101
|
+
// DateRange — design validation
|
|
102
|
+
export { DATE_RANGE_RULES } from './dateRange/validateDesign.js';
|
|
103
|
+
|
|
104
|
+
// DateRange — sample design
|
|
105
|
+
export { DateRangeSample } from './dateRange/designs/index.js';
|
|
106
|
+
|
|
107
|
+
// DateRange — ready-made page
|
|
108
|
+
export { DateRangePage } from './dateRange/pages.jsx';
|
|
109
|
+
|
|
110
|
+
// RangeField — model. Not DateRange (below) — that's a full quick/range/
|
|
111
|
+
// single POPOVER picker; this is just a From+To pair rendered as one field,
|
|
112
|
+
// for a caller that already knows it wants an explicit range and has
|
|
113
|
+
// nowhere to put a popover.
|
|
114
|
+
export { RANGE_TYPES, emptyRange, normalizeRange, rangeIsSet } from './rangeField/rangeField.js';
|
|
115
|
+
|
|
116
|
+
// RangeField — controller
|
|
117
|
+
export { useRangeFieldController } from './rangeField/useRangeFieldController.js';
|
|
118
|
+
|
|
119
|
+
// RangeField — design validation
|
|
120
|
+
export { RANGE_FIELD_RULES } from './rangeField/validateDesign.js';
|
|
121
|
+
|
|
122
|
+
// RangeField — sample design
|
|
123
|
+
export { RangeFieldSample } from './rangeField/designs/index.js';
|
|
124
|
+
|
|
125
|
+
// RangeField — ready-made page
|
|
126
|
+
export { RangeFieldPage } from './rangeField/pages.jsx';
|
|
127
|
+
|
|
128
|
+
// Dropdown — model
|
|
129
|
+
export {
|
|
130
|
+
DROPDOWN_MODES,
|
|
131
|
+
normalizeKeys,
|
|
132
|
+
sortByKey,
|
|
133
|
+
filterByQuery,
|
|
134
|
+
pickSelected
|
|
135
|
+
} from './dropdown/dropdown.js';
|
|
136
|
+
|
|
137
|
+
// Dropdown — controller
|
|
138
|
+
export { useDropdownController } from './dropdown/useDropdownController.js';
|
|
139
|
+
|
|
140
|
+
// Dropdown — design validation
|
|
141
|
+
export { DROPDOWN_RULES } from './dropdown/validateDesign.js';
|
|
142
|
+
|
|
143
|
+
// Dropdown — sample design
|
|
144
|
+
export { DropdownSample } from './dropdown/designs/index.js';
|
|
145
|
+
|
|
146
|
+
// Dropdown — ready-made page
|
|
147
|
+
export { DropdownPage } from './dropdown/pages.jsx';
|
|
148
|
+
|
|
149
|
+
// GridDisplayer — model
|
|
150
|
+
export { filterRows as filterGridRows, sortRows as sortGridRows } from './gridDisplayer/gridDisplayer.js';
|
|
151
|
+
|
|
152
|
+
// GridDisplayer — controller
|
|
153
|
+
export { useGridDisplayerController } from './gridDisplayer/useGridDisplayerController.js';
|
|
154
|
+
|
|
155
|
+
// GridDisplayer — design validation
|
|
156
|
+
export { GRID_DISPLAYER_RULES } from './gridDisplayer/validateDesign.js';
|
|
157
|
+
|
|
158
|
+
// GridDisplayer — sample design
|
|
159
|
+
export { GridDisplayerSample } from './gridDisplayer/designs/index.js';
|
|
160
|
+
|
|
161
|
+
// GridDisplayer — ready-made page
|
|
162
|
+
export { GridDisplayerPage } from './gridDisplayer/pages.jsx';
|
|
163
|
+
|
|
16
164
|
// Snackbar
|
|
17
165
|
export { raiseSnackbar } from './snackbar/snackbar.js';
|
|
18
166
|
|
|
167
|
+
// Confirm
|
|
168
|
+
export { raiseConfirm, raiseChoice } from './confirm/confirm.js';
|
|
169
|
+
|
|
170
|
+
// Theme — brand-neutral DEFAULT design tokens (chart + table), override-friendly.
|
|
171
|
+
// Consumers pass overrides to resolveTheme(); overrides win, rest falls through.
|
|
172
|
+
export { default as DEFAULT_THEME, resolveTheme, getThemeVariant, getChartOptions, inputStyleVars, deepMerge } from './theme/index.js';
|
|
173
|
+
|
|
19
174
|
// Static data
|
|
20
175
|
export { default as COUNTRIES } from './data/countries.json';
|
|
21
176
|
import _statesData from './data/states.json';
|
|
22
177
|
export var STATES = _statesData.STATES;
|
|
23
178
|
export var STATES_IN = _statesData.STATES_IN;
|
|
179
|
+
|
|
180
|
+
// TextField — the plain text input, its multiline and search shapes.
|
|
181
|
+
export { TEXT_TYPES, toValue, normalise, validate, remaining } from './textField/textField.js';
|
|
182
|
+
export { useTextFieldController } from './textField/useTextFieldController.js';
|
|
183
|
+
export { TEXT_FIELD_RULES } from './textField/validateDesign.js';
|
|
184
|
+
export { TextFieldSample } from './textField/designs/index.js';
|
|
185
|
+
export { TextFieldPage } from './textField/pages.jsx';
|
|
186
|
+
|
|
187
|
+
// ── Progress notifier ────────────────────────────────────────────────────
|
|
188
|
+
//
|
|
189
|
+
// A small moving bar and a line of text, driven by an event KEY the back end
|
|
190
|
+
// chooses. Hidden until an event for that key arrives, so it is safe to mount
|
|
191
|
+
// on anything; visible while the work says it is continuing.
|
|
192
|
+
//
|
|
193
|
+
// An app attaches its own stream ONCE with attachProgressSource, adapting
|
|
194
|
+
// whatever it publishes into { key, continue, status?, data }. This package
|
|
195
|
+
// knows nothing about SSE — see progressSource.js for why the sentence is
|
|
196
|
+
// built on the client rather than sent as prose.
|
|
197
|
+
export { attachProgressSource, progressSourceAttached, subscribeToProgress,
|
|
198
|
+
describeProgress, describeComplete } from './progress/progressSource.js';
|
|
199
|
+
export { default as ProgressNotifier } from './progress/ProgressNotifier.jsx';
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import './numberField.css';
|
|
2
|
+
|
|
3
|
+
export default function NumberFieldSample({
|
|
4
|
+
type, raw, error, placeholder, disabled, affix,
|
|
5
|
+
handleNumberChange, handleAffixChange, handleBlur
|
|
6
|
+
}) {
|
|
7
|
+
function renderAffix() {
|
|
8
|
+
if (!affix) return null;
|
|
9
|
+
if (affix.list.length === 1) {
|
|
10
|
+
return <span className="xeplr-numberfield-affix-static">{affix.selected[affix.labelKey]}</span>;
|
|
11
|
+
}
|
|
12
|
+
return (
|
|
13
|
+
<select
|
|
14
|
+
className="xeplr-numberfield-affix-select"
|
|
15
|
+
value={affix.selected[affix.valueKey]}
|
|
16
|
+
onChange={handleAffixChange}
|
|
17
|
+
disabled={disabled}
|
|
18
|
+
>
|
|
19
|
+
{affix.list.map(function(item) {
|
|
20
|
+
return (
|
|
21
|
+
<option key={item[affix.valueKey]} value={item[affix.valueKey]}>
|
|
22
|
+
{item[affix.labelKey]}
|
|
23
|
+
</option>
|
|
24
|
+
);
|
|
25
|
+
})}
|
|
26
|
+
</select>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
var affixBefore = affix && affix.position === 'before';
|
|
31
|
+
var affixAfter = affix && affix.position === 'after';
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div className={'xeplr-numberfield xeplr-numberfield-' + type + (error ? ' xeplr-numberfield-error' : '')}>
|
|
35
|
+
<div className="xeplr-numberfield-row">
|
|
36
|
+
{affixBefore && renderAffix()}
|
|
37
|
+
<input
|
|
38
|
+
type="text"
|
|
39
|
+
inputMode="decimal"
|
|
40
|
+
className="xeplr-numberfield-input"
|
|
41
|
+
value={raw}
|
|
42
|
+
placeholder={placeholder}
|
|
43
|
+
disabled={disabled}
|
|
44
|
+
onChange={handleNumberChange}
|
|
45
|
+
onBlur={handleBlur}
|
|
46
|
+
/>
|
|
47
|
+
{affixAfter && renderAffix()}
|
|
48
|
+
</div>
|
|
49
|
+
{error && <div className="xeplr-numberfield-error-text">{error}</div>}
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as NumberFieldSample } from './NumberFieldSample.jsx';
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
.xeplr-numberfield {
|
|
2
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
3
|
+
color: #e0e0e0;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
.xeplr-numberfield-row {
|
|
7
|
+
display: flex;
|
|
8
|
+
align-items: stretch;
|
|
9
|
+
border: 1px solid #444;
|
|
10
|
+
border-radius: 6px;
|
|
11
|
+
background: #1a1a1a;
|
|
12
|
+
overflow: hidden;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.xeplr-numberfield-row:focus-within {
|
|
16
|
+
border-color: #646cff;
|
|
17
|
+
box-shadow: 0 0 0 2px rgba(100, 108, 255, 0.18);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.xeplr-numberfield-error .xeplr-numberfield-row {
|
|
21
|
+
border-color: #e25555;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.xeplr-numberfield-input {
|
|
25
|
+
flex: 1;
|
|
26
|
+
background: transparent;
|
|
27
|
+
border: none;
|
|
28
|
+
outline: none;
|
|
29
|
+
color: inherit;
|
|
30
|
+
padding: 9px 12px;
|
|
31
|
+
font-size: 14px;
|
|
32
|
+
min-width: 0;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.xeplr-numberfield-affix-select,
|
|
36
|
+
.xeplr-numberfield-affix-static {
|
|
37
|
+
background: #232540;
|
|
38
|
+
border: none;
|
|
39
|
+
color: #b8bbe0;
|
|
40
|
+
padding: 0 12px;
|
|
41
|
+
font-size: 13px;
|
|
42
|
+
display: inline-flex;
|
|
43
|
+
align-items: center;
|
|
44
|
+
cursor: pointer;
|
|
45
|
+
outline: none;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.xeplr-numberfield-affix-static {
|
|
49
|
+
cursor: default;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.xeplr-numberfield-row > .xeplr-numberfield-affix-select:first-child,
|
|
53
|
+
.xeplr-numberfield-row > .xeplr-numberfield-affix-static:first-child {
|
|
54
|
+
border-right: 1px solid #444;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.xeplr-numberfield-row > .xeplr-numberfield-affix-select:last-child,
|
|
58
|
+
.xeplr-numberfield-row > .xeplr-numberfield-affix-static:last-child {
|
|
59
|
+
border-left: 1px solid #444;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.xeplr-numberfield-error-text {
|
|
63
|
+
margin-top: 4px;
|
|
64
|
+
font-size: 12px;
|
|
65
|
+
color: #e25555;
|
|
66
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NumberField — pure model. No React.
|
|
3
|
+
*
|
|
4
|
+
* Handles the parsing, formatting, and validation of numeric input.
|
|
5
|
+
* Three flavours of value: float, currency, units. Currency and units
|
|
6
|
+
* are numerically the same — they only differ by the affix shown next
|
|
7
|
+
* to the number (a dropdown of currencies or a dropdown of units).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export var NUMBER_TYPES = ['float', 'currency', 'units'];
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Strip everything that isn't a digit, decimal point, or leading minus.
|
|
14
|
+
* Collapses multiple decimal points to the first one.
|
|
15
|
+
*/
|
|
16
|
+
export function sanitizeNumberInput(raw) {
|
|
17
|
+
if (raw === null || raw === undefined) return '';
|
|
18
|
+
var str = String(raw);
|
|
19
|
+
var negative = str.charAt(0) === '-';
|
|
20
|
+
str = str.replace(/[^0-9.]/g, '');
|
|
21
|
+
var parts = str.split('.');
|
|
22
|
+
if (parts.length > 2) {
|
|
23
|
+
str = parts[0] + '.' + parts.slice(1).join('');
|
|
24
|
+
}
|
|
25
|
+
return (negative ? '-' : '') + str;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Convert a raw string to a Number. Returns null if not a finite number.
|
|
30
|
+
*/
|
|
31
|
+
export function toNumeric(raw) {
|
|
32
|
+
if (raw === null || raw === undefined || raw === '' || raw === '-') return null;
|
|
33
|
+
var n = Number(raw);
|
|
34
|
+
return isFinite(n) ? n : null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Format a numeric value for display. Locale-aware grouping.
|
|
39
|
+
*/
|
|
40
|
+
export function formatDisplay(value, decimals) {
|
|
41
|
+
if (value === null || value === undefined || value === '') return '';
|
|
42
|
+
var n = Number(value);
|
|
43
|
+
if (!isFinite(n)) return '';
|
|
44
|
+
if (typeof decimals === 'number') {
|
|
45
|
+
return n.toLocaleString(undefined, {
|
|
46
|
+
minimumFractionDigits: decimals,
|
|
47
|
+
maximumFractionDigits: decimals
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return n.toLocaleString();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Validate a numeric value against optional min/max bounds.
|
|
55
|
+
* Returns { valid: true } or { valid: false, error: string }.
|
|
56
|
+
*/
|
|
57
|
+
export function validateRange(value, min, max) {
|
|
58
|
+
if (value === null) return { valid: true };
|
|
59
|
+
if (typeof min === 'number' && value < min) {
|
|
60
|
+
return { valid: false, error: 'Value must be at least ' + min };
|
|
61
|
+
}
|
|
62
|
+
if (typeof max === 'number' && value > max) {
|
|
63
|
+
return { valid: false, error: 'Value must be at most ' + max };
|
|
64
|
+
}
|
|
65
|
+
return { valid: true };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Resolve the affix list and currently-selected affix for a given type.
|
|
70
|
+
* Falls back to a sensible default when the user supplied only one item.
|
|
71
|
+
*/
|
|
72
|
+
export function resolveAffixState(type, options) {
|
|
73
|
+
options = options || {};
|
|
74
|
+
if (type === 'currency') {
|
|
75
|
+
var currencies = options.currencies || [];
|
|
76
|
+
if (currencies.length === 0) {
|
|
77
|
+
throw new Error('[xeplr-ui-utils:NumberField] type="currency" requires a `currencies` prop with at least one entry like { code: "INR", symbol: "₹" }');
|
|
78
|
+
}
|
|
79
|
+
var defaultCode = options.defaultCurrency || currencies[0].code;
|
|
80
|
+
var selected = currencies.find(function(c) { return c.code === defaultCode; }) || currencies[0];
|
|
81
|
+
return {
|
|
82
|
+
list: currencies,
|
|
83
|
+
selected: selected,
|
|
84
|
+
position: options.currencyPosition || 'before',
|
|
85
|
+
labelKey: 'symbol',
|
|
86
|
+
valueKey: 'code'
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
if (type === 'units') {
|
|
90
|
+
var units = options.units || [];
|
|
91
|
+
if (units.length === 0) {
|
|
92
|
+
throw new Error('[xeplr-ui-utils:NumberField] type="units" requires a `units` prop with at least one entry like { value: "kg", label: "kg" }');
|
|
93
|
+
}
|
|
94
|
+
var defaultValue = options.defaultUnit || units[0].value;
|
|
95
|
+
var selectedUnit = units.find(function(u) { return u.value === defaultValue; }) || units[0];
|
|
96
|
+
return {
|
|
97
|
+
list: units,
|
|
98
|
+
selected: selectedUnit,
|
|
99
|
+
position: options.unitPosition || 'after',
|
|
100
|
+
labelKey: 'label',
|
|
101
|
+
valueKey: 'value'
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { useNumberFieldController } from './useNumberFieldController.js';
|
|
2
|
+
import NumberFieldSample from './designs/NumberFieldSample.jsx';
|
|
3
|
+
import { useDesignValidator } from '../fileUpload/validateDesign.js';
|
|
4
|
+
import { NUMBER_FIELD_RULES } from './validateDesign.js';
|
|
5
|
+
|
|
6
|
+
export function NumberFieldPage(props) {
|
|
7
|
+
var controller = useNumberFieldController(props);
|
|
8
|
+
var ref = useDesignValidator('NumberFieldPage', NUMBER_FIELD_RULES);
|
|
9
|
+
return <div ref={ref}><NumberFieldSample {...controller} /></div>;
|
|
10
|
+
}
|