@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.
Files changed (68) hide show
  1. package/package.json +25 -6
  2. package/src/confirm/confirm.js +175 -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 +68 -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/dateRange/dateRange.js +273 -0
  20. package/src/dateRange/designs/DateRangeSample.jsx +138 -0
  21. package/src/dateRange/designs/dateRange.css +69 -0
  22. package/src/dateRange/designs/index.js +1 -0
  23. package/src/dateRange/pages.jsx +10 -0
  24. package/src/dateRange/useDateRangeController.js +118 -0
  25. package/src/dateRange/validateDesign.js +20 -0
  26. package/src/dropdown/designs/DropdownSample.jsx +151 -0
  27. package/src/dropdown/designs/dropdown.css +226 -0
  28. package/src/dropdown/designs/index.js +1 -0
  29. package/src/dropdown/dropdown.js +75 -0
  30. package/src/dropdown/pages.jsx +10 -0
  31. package/src/dropdown/useDropdownController.js +254 -0
  32. package/src/dropdown/validateDesign.js +4 -0
  33. package/src/fileUpload/validateDesign.js +27 -8
  34. package/src/gridDisplayer/designs/GridDisplayerSample.jsx +58 -0
  35. package/src/gridDisplayer/designs/gridDisplayer.css +89 -0
  36. package/src/gridDisplayer/designs/index.js +1 -0
  37. package/src/gridDisplayer/gridDisplayer.js +44 -0
  38. package/src/gridDisplayer/pages.jsx +10 -0
  39. package/src/gridDisplayer/useGridDisplayerController.js +59 -0
  40. package/src/gridDisplayer/validateDesign.js +4 -0
  41. package/src/index.js +177 -1
  42. package/src/numberField/designs/NumberFieldSample.jsx +52 -0
  43. package/src/numberField/designs/index.js +1 -0
  44. package/src/numberField/designs/numberField.css +66 -0
  45. package/src/numberField/numberField.js +105 -0
  46. package/src/numberField/pages.jsx +10 -0
  47. package/src/numberField/useNumberFieldController.js +116 -0
  48. package/src/numberField/validateDesign.js +3 -0
  49. package/src/progress/ProgressNotifier.jsx +118 -0
  50. package/src/progress/designs/progressNotifier.css +73 -0
  51. package/src/progress/progressSource.js +168 -0
  52. package/src/rangeField/designs/RangeFieldSample.jsx +25 -0
  53. package/src/rangeField/designs/index.js +1 -0
  54. package/src/rangeField/designs/rangeField.css +26 -0
  55. package/src/rangeField/pages.jsx +10 -0
  56. package/src/rangeField/rangeField.js +29 -0
  57. package/src/rangeField/useRangeFieldController.js +52 -0
  58. package/src/rangeField/validateDesign.js +3 -0
  59. package/src/snackbar/snackbar.js +6 -3
  60. package/src/textField/designs/TextFieldSample.jsx +75 -0
  61. package/src/textField/designs/index.js +1 -0
  62. package/src/textField/designs/textField.css +92 -0
  63. package/src/textField/pages.jsx +10 -0
  64. package/src/textField/textField.js +65 -0
  65. package/src/textField/useTextFieldController.js +101 -0
  66. package/src/textField/validateDesign.js +3 -0
  67. package/src/theme/default.theme.json +934 -0
  68. 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
+ }
@@ -0,0 +1,4 @@
1
+ export var GRID_DISPLAYER_RULES = [
2
+ { selector: '.xeplr-grid-search', label: 'Search input' },
3
+ { selector: '.xeplr-grid', label: 'Tile grid' }
4
+ ];
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 (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
+ // 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
+ }