@xeplr/ui-utils 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/package.json +22 -6
  2. package/src/confirm/confirm.js +106 -0
  3. package/src/dataImport/dataImport.js +32 -0
  4. package/src/dataImport/designs/DataImportSample.jsx +71 -0
  5. package/src/dataImport/designs/ImportHistorySample.jsx +130 -0
  6. package/src/dataImport/designs/dataImport.css +98 -0
  7. package/src/dataImport/designs/index.js +2 -0
  8. package/src/dataImport/pages.jsx +17 -0
  9. package/src/dataImport/useDataImportController.js +94 -0
  10. package/src/dataImport/useImportHistoryController.js +67 -0
  11. package/src/dataImport/validateDesign.js +49 -0
  12. package/src/dateField/dateField.js +71 -0
  13. package/src/dateField/designs/DateFieldSample.jsx +30 -0
  14. package/src/dateField/designs/dateField.css +60 -0
  15. package/src/dateField/designs/index.js +1 -0
  16. package/src/dateField/pages.jsx +10 -0
  17. package/src/dateField/useDateFieldController.js +77 -0
  18. package/src/dateField/validateDesign.js +3 -0
  19. package/src/dropdown/designs/DropdownSample.jsx +151 -0
  20. package/src/dropdown/designs/dropdown.css +226 -0
  21. package/src/dropdown/designs/index.js +1 -0
  22. package/src/dropdown/dropdown.js +75 -0
  23. package/src/dropdown/pages.jsx +10 -0
  24. package/src/dropdown/useDropdownController.js +254 -0
  25. package/src/dropdown/validateDesign.js +4 -0
  26. package/src/gridDisplayer/designs/GridDisplayerSample.jsx +58 -0
  27. package/src/gridDisplayer/designs/gridDisplayer.css +89 -0
  28. package/src/gridDisplayer/designs/index.js +1 -0
  29. package/src/gridDisplayer/gridDisplayer.js +44 -0
  30. package/src/gridDisplayer/pages.jsx +10 -0
  31. package/src/gridDisplayer/useGridDisplayerController.js +59 -0
  32. package/src/gridDisplayer/validateDesign.js +4 -0
  33. package/src/index.js +115 -1
  34. package/src/numberField/designs/NumberFieldSample.jsx +52 -0
  35. package/src/numberField/designs/index.js +1 -0
  36. package/src/numberField/designs/numberField.css +66 -0
  37. package/src/numberField/numberField.js +105 -0
  38. package/src/numberField/pages.jsx +10 -0
  39. package/src/numberField/useNumberFieldController.js +116 -0
  40. package/src/numberField/validateDesign.js +3 -0
  41. package/src/snackbar/snackbar.js +6 -3
  42. package/src/theme/default.theme.json +178 -0
  43. package/src/theme/index.js +75 -0
package/src/index.js CHANGED
@@ -10,12 +10,126 @@ 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 (controller + sample design wired)
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
+ // Dropdown — model
88
+ export {
89
+ DROPDOWN_MODES,
90
+ normalizeKeys,
91
+ sortByKey,
92
+ filterByQuery,
93
+ pickSelected
94
+ } from './dropdown/dropdown.js';
95
+
96
+ // Dropdown — controller
97
+ export { useDropdownController } from './dropdown/useDropdownController.js';
98
+
99
+ // Dropdown — design validation
100
+ export { DROPDOWN_RULES } from './dropdown/validateDesign.js';
101
+
102
+ // Dropdown — sample design
103
+ export { DropdownSample } from './dropdown/designs/index.js';
104
+
105
+ // Dropdown — ready-made page
106
+ export { DropdownPage } from './dropdown/pages.jsx';
107
+
108
+ // GridDisplayer — model
109
+ export { filterRows as filterGridRows, sortRows as sortGridRows } from './gridDisplayer/gridDisplayer.js';
110
+
111
+ // GridDisplayer — controller
112
+ export { useGridDisplayerController } from './gridDisplayer/useGridDisplayerController.js';
113
+
114
+ // GridDisplayer — design validation
115
+ export { GRID_DISPLAYER_RULES } from './gridDisplayer/validateDesign.js';
116
+
117
+ // GridDisplayer — sample design
118
+ export { GridDisplayerSample } from './gridDisplayer/designs/index.js';
119
+
120
+ // GridDisplayer — ready-made page
121
+ export { GridDisplayerPage } from './gridDisplayer/pages.jsx';
122
+
16
123
  // Snackbar
17
124
  export { raiseSnackbar } from './snackbar/snackbar.js';
18
125
 
126
+ // Confirm
127
+ export { raiseConfirm } from './confirm/confirm.js';
128
+
129
+ // Theme — brand-neutral DEFAULT design tokens (chart + table), override-friendly.
130
+ // Consumers pass overrides to resolveTheme(); overrides win, rest falls through.
131
+ export { default as DEFAULT_THEME, resolveTheme, getThemeVariant, getChartOptions, deepMerge } from './theme/index.js';
132
+
19
133
  // Static data
20
134
  export { default as COUNTRIES } from './data/countries.json';
21
135
  import _statesData from './data/states.json';
@@ -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
+ }
@@ -0,0 +1,116 @@
1
+ import { useState, useEffect, useMemo } from 'react';
2
+ import {
3
+ sanitizeNumberInput,
4
+ toNumeric,
5
+ formatDisplay,
6
+ validateRange,
7
+ resolveAffixState,
8
+ NUMBER_TYPES
9
+ } from './numberField.js';
10
+
11
+ /**
12
+ * @param {object} props
13
+ * @param {'float'|'currency'|'units'} [props.type='float']
14
+ * @param {number|null} [props.value] — controlled numeric value
15
+ * @param {number|null} [props.defaultValue] — uncontrolled initial value
16
+ * @param {(value, meta) => void} [props.onChange] — called with (numericValue, { affix })
17
+ * @param {Array} [props.currencies] — for currency type: [{ code, symbol }]
18
+ * @param {string} [props.defaultCurrency]
19
+ * @param {'before'|'after'} [props.currencyPosition='before']
20
+ * @param {Array} [props.units] — for units type: [{ value, label }]
21
+ * @param {string} [props.defaultUnit]
22
+ * @param {'before'|'after'} [props.unitPosition='after']
23
+ * @param {number} [props.min]
24
+ * @param {number} [props.max]
25
+ * @param {number} [props.decimals] — display decimals on blur
26
+ * @param {string} [props.placeholder]
27
+ * @param {boolean} [props.disabled]
28
+ */
29
+ export function useNumberFieldController(props) {
30
+ props = props || {};
31
+ var type = props.type || 'float';
32
+ if (NUMBER_TYPES.indexOf(type) === -1) {
33
+ throw new Error('[xeplr-ui-utils:NumberField] Unknown type: ' + type + '. Expected one of ' + NUMBER_TYPES.join(', '));
34
+ }
35
+
36
+ var affixInit = useMemo(function() {
37
+ return resolveAffixState(type, props);
38
+ }, [
39
+ type,
40
+ props.currencies, props.defaultCurrency, props.currencyPosition,
41
+ props.units, props.defaultUnit, props.unitPosition
42
+ ]);
43
+
44
+ var isControlled = props.value !== undefined;
45
+ var initial = isControlled ? props.value : (props.defaultValue !== undefined ? props.defaultValue : null);
46
+
47
+ var [raw, setRaw] = useState(initial === null || initial === undefined ? '' : String(initial));
48
+ var [affix, setAffix] = useState(affixInit ? affixInit.selected : null);
49
+ var [error, setError] = useState('');
50
+
51
+ useEffect(function() {
52
+ if (isControlled) {
53
+ var next = props.value === null || props.value === undefined ? '' : String(props.value);
54
+ setRaw(next);
55
+ }
56
+ }, [props.value, isControlled]);
57
+
58
+ useEffect(function() {
59
+ if (affixInit) setAffix(affixInit.selected);
60
+ }, [affixInit]);
61
+
62
+ function emit(nextRaw, nextAffix) {
63
+ var numeric = toNumeric(nextRaw);
64
+ var rangeCheck = validateRange(numeric, props.min, props.max);
65
+ setError(rangeCheck.valid ? '' : rangeCheck.error);
66
+ if (props.onChange) {
67
+ props.onChange(numeric, { affix: nextAffix });
68
+ }
69
+ }
70
+
71
+ function handleNumberChange(e) {
72
+ var sanitized = sanitizeNumberInput(e.target.value);
73
+ if (!isControlled) setRaw(sanitized);
74
+ emit(sanitized, affix);
75
+ }
76
+
77
+ function handleAffixChange(e) {
78
+ var nextValue = e.target.value;
79
+ var next = (affixInit.list).find(function(item) {
80
+ return item[affixInit.valueKey] === nextValue;
81
+ });
82
+ if (!next) return;
83
+ setAffix(next);
84
+ emit(raw, next);
85
+ }
86
+
87
+ function handleBlur() {
88
+ var numeric = toNumeric(raw);
89
+ if (numeric === null) return;
90
+ if (typeof props.decimals === 'number') {
91
+ var fixed = numeric.toFixed(props.decimals);
92
+ if (!isControlled) setRaw(fixed);
93
+ }
94
+ }
95
+
96
+ return {
97
+ type,
98
+ raw,
99
+ numeric: toNumeric(raw),
100
+ error,
101
+ placeholder: props.placeholder,
102
+ disabled: !!props.disabled,
103
+ decimals: props.decimals,
104
+ formatDisplay: formatDisplay,
105
+ affix: affixInit ? {
106
+ list: affixInit.list,
107
+ selected: affix,
108
+ position: affixInit.position,
109
+ labelKey: affixInit.labelKey,
110
+ valueKey: affixInit.valueKey
111
+ } : null,
112
+ handleNumberChange,
113
+ handleAffixChange,
114
+ handleBlur
115
+ };
116
+ }
@@ -0,0 +1,3 @@
1
+ export var NUMBER_FIELD_RULES = [
2
+ { selector: 'input[inputmode="decimal"], input.xeplr-numberfield-input', label: 'Numeric input element' }
3
+ ];
@@ -2,10 +2,13 @@ var _container = null;
2
2
  var _queue = [];
3
3
  var _timer = null;
4
4
 
5
+ // var(--xeplr-*, fallback) — picks up @xeplr/ui-account's theme tokens
6
+ // (light/dark) when that stylesheet is loaded, falls back to these original
7
+ // colors otherwise.
5
8
  var DESIGNS = {
6
- success: { bg: '#059669', color: '#fff' },
7
- error: { bg: '#dc2626', color: '#fff' },
8
- default: { bg: '#2563eb', color: '#fff' }
9
+ success: { bg: 'var(--xeplr-success, #059669)', color: 'var(--xeplr-accent-text, #fff)' },
10
+ error: { bg: 'var(--xeplr-danger, #dc2626)', color: 'var(--xeplr-accent-text, #fff)' },
11
+ default: { bg: 'var(--xeplr-accent, #2563eb)', color: 'var(--xeplr-accent-text, #fff)' }
9
12
  };
10
13
 
11
14
  var DEFAULT_DURATION = 3000;