@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,138 @@
1
+ import './dateRange.css';
2
+
3
+ var TABS = [
4
+ ['quick', 'Quick date'],
5
+ ['range', 'Range'],
6
+ ['single', 'Single']
7
+ ];
8
+
9
+ /**
10
+ * DateRange — design. Presentation only: everything arrives as props from
11
+ * useDateRangeController, so an app can swap this out without touching the
12
+ * behaviour.
13
+ *
14
+ * Three ways in, because they are genuinely different questions:
15
+ * Quick — "the current month", which stays current as time passes
16
+ * Range — two explicit dates
17
+ * Single — one date, month or year, typed
18
+ */
19
+ export default function DateRangeSample(props) {
20
+ return (
21
+ <div className="xeplr-daterange">
22
+ <div className="xeplr-daterange-tabs">
23
+ {TABS.map(function(t) {
24
+ return (
25
+ <button
26
+ key={t[0]}
27
+ type="button"
28
+ data-xeplr-daterange-tab={t[0]}
29
+ className={props.tab === t[0] ? 'active' : ''}
30
+ onClick={function() { props.setTab(t[0]); }}
31
+ >
32
+ {t[1]}
33
+ </button>
34
+ );
35
+ })}
36
+ </div>
37
+
38
+ {props.tab === 'quick' && (
39
+ <div className="xeplr-daterange-list">
40
+ {props.presets.map(function(p) {
41
+ var on = props.value && props.value.kind === 'quick' && props.value.preset === p.key;
42
+ return (
43
+ <button
44
+ key={p.key}
45
+ type="button"
46
+ data-xeplr-daterange-preset={p.key}
47
+ className={'xeplr-daterange-item' + (on ? ' on' : '')}
48
+ onClick={function() { props.selectPreset(p.key); }}
49
+ >
50
+ {p.label}
51
+ </button>
52
+ );
53
+ })}
54
+ </div>
55
+ )}
56
+
57
+ {props.tab === 'range' && (
58
+ <div className="xeplr-daterange-body">
59
+ <label className="xeplr-daterange-field">
60
+ <span>From</span>
61
+ <input type="date" value={props.range.from || ''} onChange={function(e) { props.setRangeFrom(e.target.value); }} />
62
+ </label>
63
+ <label className="xeplr-daterange-field">
64
+ <span>To</span>
65
+ <input type="date" value={props.range.to || ''} onChange={function(e) { props.setRangeTo(e.target.value); }} />
66
+ </label>
67
+ {/* One-sided is legitimate — "everything since April" is a real
68
+ answer — so it is stated rather than blocked. */}
69
+ <div className="xeplr-daterange-note">Leave either side empty for an open-ended range.</div>
70
+ </div>
71
+ )}
72
+
73
+ {props.tab === 'single' && (
74
+ <div className="xeplr-daterange-body">
75
+ <input
76
+ className="xeplr-daterange-input"
77
+ value={props.single}
78
+ placeholder="1/1/2022, May 2022, or 2022"
79
+ onChange={function(e) { props.setSingle(e.target.value); }}
80
+ onBlur={function() { props.commitSingle(false); }}
81
+ onKeyDown={function(e) {
82
+ if (e.key !== 'Enter') return;
83
+ e.preventDefault();
84
+ props.commitSingle(true);
85
+ }}
86
+ />
87
+ {/* WHAT YOU PROBABLY MEAN, one click away. "j" is three months and
88
+ every month is several years — the list is shorter than typing
89
+ the rest, and it is ordered so the year already on screen comes
90
+ first. Months and years only; a day is quicker to finish typing
91
+ than to find among thirty. */}
92
+ {props.suggestions && props.suggestions.length > 0 && (
93
+ <ul className="xeplr-daterange-suggest">
94
+ {props.suggestions.map(function(s) {
95
+ return (
96
+ <li key={s.value}>
97
+ <button
98
+ type="button"
99
+ // MOUSEDOWN, not click: the input's onBlur commits what
100
+ // is typed and would close this list before a click ever
101
+ // landed on it.
102
+ onMouseDown={function(e) { e.preventDefault(); props.applySuggestion(s.value); }}
103
+ >
104
+ <span>{s.label}</span>
105
+ {s.note && <span className="xeplr-daterange-suggest-note">{s.note}</span>}
106
+ </button>
107
+ </li>
108
+ );
109
+ })}
110
+ </ul>
111
+ )}
112
+
113
+ {/* A month or a year IS a range, and showing the resolved span is the
114
+ quickest way to confirm "May 2022" was read as intended. */}
115
+ <div className="xeplr-daterange-note">
116
+ {props.single
117
+ ? (props.singleParsed ? props.singleParsed.from + ' → ' + props.singleParsed.to : 'Not a date yet')
118
+ : 'A day, a whole month, or a whole year.'}
119
+ </div>
120
+ </div>
121
+ )}
122
+
123
+ <div className="xeplr-daterange-foot">
124
+ {/* Absent, not disabled, where the host cannot accept an empty
125
+ selection — a control you can press that does nothing is worse
126
+ than one that was never offered. */}
127
+ {props.allowClear && (
128
+ <button type="button" className="xeplr-daterange-clear" onClick={props.clear}>Clear</button>
129
+ )}
130
+ {props.label && <span className="xeplr-daterange-current">{props.label}</span>}
131
+ </div>
132
+
133
+ {props.inheritedLabel && !props.value && (
134
+ <div className="xeplr-daterange-inherited">Using {props.inheritedLabel}</div>
135
+ )}
136
+ </div>
137
+ );
138
+ }
@@ -0,0 +1,69 @@
1
+ /* Namespaced xeplr-daterange-* so a host's own styles can't collide with it. */
2
+ .xeplr-daterange {
3
+ display: flex;
4
+ flex-direction: column;
5
+ width: 260px;
6
+ background: var(--xeplr-surface, #fff);
7
+ border: 1px solid var(--xeplr-border-strong, #c9cdd6);
8
+ border-radius: 9px;
9
+ overflow: hidden;
10
+ color: inherit;
11
+ }
12
+ .xeplr-daterange-tabs { display: flex; border-bottom: 1px solid var(--xeplr-border, #e2e5eb); }
13
+ .xeplr-daterange-tabs button {
14
+ flex: 1; padding: 7px 4px; border: none; background: none; cursor: pointer;
15
+ font-size: 11.5px; font-weight: 600; color: var(--xeplr-muted, #6b7280);
16
+ border-bottom: 2px solid transparent;
17
+ }
18
+ .xeplr-daterange-tabs button.active {
19
+ color: var(--xeplr-accent, #4f5fd6);
20
+ border-bottom-color: var(--xeplr-accent, #4f5fd6);
21
+ }
22
+ .xeplr-daterange-list { max-height: 240px; overflow-y: auto; padding: 4px; }
23
+ .xeplr-daterange-item {
24
+ display: block; width: 100%; text-align: left;
25
+ padding: 5px 7px; border: none; border-radius: 5px; background: none;
26
+ font-size: 12.5px; color: inherit; cursor: pointer;
27
+ }
28
+ .xeplr-daterange-item:hover { background: var(--xeplr-hover, #f4f5f8); }
29
+ .xeplr-daterange-item.on { color: var(--xeplr-accent, #4f5fd6); font-weight: 600; }
30
+ .xeplr-daterange-body { padding: 10px; }
31
+ .xeplr-daterange-field { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
32
+ .xeplr-daterange-field span { width: 42px; flex-shrink: 0; font-size: 11.5px; color: var(--xeplr-muted, #6b7280); }
33
+ .xeplr-daterange-field input,
34
+ .xeplr-daterange-input {
35
+ flex: 1; min-width: 0; width: 100%; padding: 5px 8px;
36
+ border: 1px solid var(--xeplr-border, #e2e5eb); border-radius: 6px;
37
+ background: var(--xeplr-surface-2, #fafbfc); color: inherit; font-size: 12.5px;
38
+ }
39
+ .xeplr-daterange-note { font-size: 11px; color: var(--xeplr-muted-2, #9aa1ad); margin-top: 6px; line-height: 1.4; }
40
+ .xeplr-daterange-foot {
41
+ display: flex; align-items: center; justify-content: space-between;
42
+ padding: 6px 10px; border-top: 1px solid var(--xeplr-border, #e2e5eb);
43
+ }
44
+ .xeplr-daterange-clear {
45
+ border: none; background: none; cursor: pointer;
46
+ font-size: 11.5px; font-weight: 600; color: var(--xeplr-accent, #4f5fd6);
47
+ }
48
+ .xeplr-daterange-current { font-size: 11px; color: var(--xeplr-muted-2, #9aa1ad); }
49
+ .xeplr-daterange-inherited {
50
+ padding: 6px 10px; font-size: 10.5px; line-height: 1.4;
51
+ color: var(--xeplr-muted-2, #9aa1ad);
52
+ border-top: 1px solid var(--xeplr-border, #e2e5eb);
53
+ }
54
+
55
+ /* ── Suggestions, under the Single box ──────────────────────────────────── */
56
+ .xeplr-daterange-suggest {
57
+ list-style: none; margin: 6px 0 0; padding: 0;
58
+ max-height: 190px; overflow: auto;
59
+ border: 1px solid var(--xeplr-border, #e2e5eb); border-radius: 8px;
60
+ }
61
+ .xeplr-daterange-suggest li + li { border-top: 1px solid var(--xeplr-border-secondary, #eef0f4); }
62
+ .xeplr-daterange-suggest button {
63
+ display: flex; justify-content: space-between; align-items: baseline; gap: 10px;
64
+ width: 100%; padding: 7px 10px; border: 0; background: none;
65
+ font: inherit; font-size: 13px; color: var(--xeplr-text, #1c2029);
66
+ text-align: left; cursor: pointer;
67
+ }
68
+ .xeplr-daterange-suggest button:hover { background: var(--xeplr-hover, #f4f5f8); }
69
+ .xeplr-daterange-suggest-note { font-size: 11px; color: var(--xeplr-muted, #6b7280); }
@@ -0,0 +1 @@
1
+ export { default as DateRangeSample } from './DateRangeSample.jsx';
@@ -0,0 +1,10 @@
1
+ import { useDateRangeController } from './useDateRangeController.js';
2
+ import DateRangeSample from './designs/DateRangeSample.jsx';
3
+ import { useDesignValidator } from '../fileUpload/validateDesign.js';
4
+ import { DATE_RANGE_RULES } from './validateDesign.js';
5
+
6
+ export function DateRangePage(props) {
7
+ var controller = useDateRangeController(props);
8
+ var ref = useDesignValidator('DateRangePage', DATE_RANGE_RULES);
9
+ return <div ref={ref}><DateRangeSample {...controller} /></div>;
10
+ }
@@ -0,0 +1,118 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { QUICK_PRESETS, parseSingleDate, dateValueLabel, singleDateSuggestions } from './dateRange.js';
3
+
4
+ /**
5
+ * DateRange — controller. Owns which tab is open and the half-typed text; the
6
+ * design draws it. No JSX here.
7
+ *
8
+ * @param {object} props
9
+ * @param {object|null} props.value - the current selection, or null
10
+ * @param {Function} props.onChange - (selection|null) => void
11
+ * @param {Function} [props.onClose] - called after a choice that ends the interaction
12
+ * @param {Array} [props.presets] - override the quick list
13
+ * @param {boolean} [props.allowClear] - default true. Set false where an
14
+ * empty selection is not a state the host can accept — a report bounded by
15
+ * a date, say, for which "no date" means scanning all of history.
16
+ */
17
+ export function useDateRangeController(props) {
18
+ var value = props.value || null;
19
+ var onChange = props.onChange || function() {};
20
+ var onClose = props.onClose;
21
+ var presets = props.presets || QUICK_PRESETS;
22
+ // Opt-OUT, so every existing caller keeps the button it already had.
23
+ var allowClear = props.allowClear !== false;
24
+
25
+ var initial = value && value.kind ? value.kind : 'quick';
26
+ var tabState = useState(initial);
27
+ var tab = tabState[0];
28
+ var setTab = tabState[1];
29
+
30
+ var singleState = useState(value && value.kind === 'single' ? value.value : '');
31
+ var single = singleState[0];
32
+ var setSingle = singleState[1];
33
+
34
+ // A selection made elsewhere (a reset, a different record) has to show up
35
+ // here, or the box keeps displaying what the user last typed into it.
36
+ useEffect(function() {
37
+ if (value && value.kind === 'single') setSingle(value.value || '');
38
+ }, [value]);
39
+
40
+ var range = value && value.kind === 'range' ? value : { from: '', to: '' };
41
+ // Parsed as you type, so "May 202" reads as not-a-date-yet rather than
42
+ // silently saving as nothing.
43
+ var singleParsed = single ? parseSingleDate(single) : null;
44
+
45
+ // WHAT YEAR TO OFFER FIRST — the one already on screen.
46
+ //
47
+ // Somebody typing "j" after looking at Feb 2025 is almost certainly after a
48
+ // month of 2025. Read from what is COMMITTED rather than from what is being
49
+ // typed, or the anchor would move as soon as a suggestion resolved to a
50
+ // different year and the list would reorder under the cursor.
51
+ var committed = value && value.kind === 'single' ? parseSingleDate(value.value) : null;
52
+ var anchorYear = committed && committed.from
53
+ ? Number(String(committed.from).slice(0, 4))
54
+ : new Date().getFullYear();
55
+
56
+ var suggestions = singleDateSuggestions(single, anchorYear);
57
+
58
+ function applySuggestion(text) {
59
+ setSingle(text);
60
+ // COMMITTED IMMEDIATELY. Picking from a list is a decision — leaving it in
61
+ // the box for the user to then press Enter on would be asking twice.
62
+ if (parseSingleDate(text)) {
63
+ onChange({ kind: 'single', value: text });
64
+ if (onClose) onClose();
65
+ }
66
+ }
67
+
68
+ function selectPreset(key) {
69
+ onChange({ kind: 'quick', preset: key });
70
+ if (onClose) onClose();
71
+ }
72
+
73
+ function setRangeFrom(from) {
74
+ onChange({ kind: 'range', from: from, to: range.to || '' });
75
+ }
76
+
77
+ function setRangeTo(to) {
78
+ onChange({ kind: 'range', from: range.from || '', to: to });
79
+ }
80
+
81
+ // Only committed once it parses — a partial entry must not overwrite a good
82
+ // selection with nothing.
83
+ function commitSingle(close) {
84
+ if (!singleParsed) return;
85
+ onChange({ kind: 'single', value: single.trim() });
86
+ if (close && onClose) onClose();
87
+ }
88
+
89
+ function clear() {
90
+ // Enforced here, not only by hiding the button: a custom design that
91
+ // renders its own clear must not be able to produce a state the host
92
+ // said it cannot accept.
93
+ if (!allowClear) return;
94
+ onChange(null);
95
+ if (onClose) onClose();
96
+ }
97
+
98
+ return {
99
+ tab: tab,
100
+ setTab: setTab,
101
+ presets: presets,
102
+ value: value,
103
+ label: dateValueLabel(value),
104
+ range: range,
105
+ single: single,
106
+ setSingle: setSingle,
107
+ singleParsed: singleParsed,
108
+ suggestions: suggestions,
109
+ applySuggestion: applySuggestion,
110
+ selectPreset: selectPreset,
111
+ setRangeFrom: setRangeFrom,
112
+ setRangeTo: setRangeTo,
113
+ commitSingle: commitSingle,
114
+ clear: clear,
115
+ allowClear: allowClear,
116
+ inheritedLabel: props.inheritedLabel || null
117
+ };
118
+ }
@@ -0,0 +1,20 @@
1
+ // A design that omits any of these can't collect a date, so the page reports
2
+ // loudly at mount rather than rendering a control nobody can use.
3
+ //
4
+ // The tabs are the one thing always on screen. Everything else belongs to a
5
+ // tab — the quick list to "Quick", the date inputs to "Range"/"Single" — so
6
+ // requiring a preset list AND a date input at the same time is a rule no
7
+ // tabbed design can satisfy on any tab. It failed every time it was mounted.
8
+ // `anyOf` asks what is actually meant: is there some way to enter a date?
9
+ export var DATE_RANGE_RULES = [
10
+ { selector: '.xeplr-daterange-tabs button, [data-xeplr-daterange-tab]', label: 'Tab buttons (quick / range / single)' },
11
+ {
12
+ anyOf: [
13
+ '.xeplr-daterange-list button',
14
+ '[data-xeplr-daterange-preset]',
15
+ 'input[type="date"]',
16
+ '.xeplr-daterange-input'
17
+ ],
18
+ label: 'A way to pick a date — a quick preset list, or a date input'
19
+ }
20
+ ];
@@ -0,0 +1,151 @@
1
+ import './dropdown.css';
2
+
3
+ function initials(name) {
4
+ if (!name) return '·';
5
+ var parts = String(name).trim().split(/\s+/).slice(0, 2);
6
+ return parts.map(function(p) { return p.charAt(0).toUpperCase(); }).join('');
7
+ }
8
+
9
+ function joinExtras(item, extras) {
10
+ var bits = [];
11
+ for (var i = 0; i < extras.length; i++) {
12
+ var v = item[extras[i]];
13
+ if (v != null && v !== '') bits.push(String(v));
14
+ }
15
+ return bits.join(' · ');
16
+ }
17
+
18
+ function summary(selectedItems, mode, keys, placeholder) {
19
+ if (selectedItems.length === 0) return <span className="xeplr-dropdown-placeholder">{placeholder}</span>;
20
+ if (mode === 'single') {
21
+ return <span className="xeplr-dropdown-selected-single">{selectedItems[0][keys.name]}</span>;
22
+ }
23
+ var first = selectedItems[0][keys.name];
24
+ var more = selectedItems.length - 1;
25
+ return (
26
+ <span className="xeplr-dropdown-selected-multi">
27
+ {first}{more > 0 ? <span className="xeplr-dropdown-selected-more"> +{more}</span> : null}
28
+ </span>
29
+ );
30
+ }
31
+
32
+ export default function DropdownSample({
33
+ mode, keys, searchable, placeholder, disabled,
34
+ filteredItems, selectedItems, loading, error, query, isOpen, rootRef,
35
+ isSelected, toggle, selectItem, removeSelection, clearSelection, handleSearch,
36
+ canCreate, createLabel, creating, createError, createItem
37
+ }) {
38
+ var hasSelection = selectedItems.length > 0;
39
+ var trimmedQuery = (query || '').trim();
40
+ var exactMatch = filteredItems.some(function(item) {
41
+ return String(item[keys.name]).toLowerCase() === trimmedQuery.toLowerCase();
42
+ });
43
+ var showCreate = canCreate && trimmedQuery && !exactMatch;
44
+
45
+ return (
46
+ <div
47
+ ref={rootRef}
48
+ className={'xeplr-dropdown xeplr-dropdown-' + mode + (isOpen ? ' xeplr-dropdown-open' : '') + (disabled ? ' xeplr-dropdown-disabled' : '')}
49
+ >
50
+ <div
51
+ className="xeplr-dropdown-trigger"
52
+ role="combobox"
53
+ aria-expanded={isOpen}
54
+ onClick={toggle}
55
+ >
56
+ {mode === 'multi' && hasSelection ? (
57
+ <div className="xeplr-dropdown-chips">
58
+ {selectedItems.map(function(item) {
59
+ return (
60
+ <span key={item[keys.id]} className="xeplr-dropdown-chip">
61
+ {item[keys.name]}
62
+ <span
63
+ className="xeplr-dropdown-chip-x"
64
+ onClick={function(e) { e.stopPropagation(); removeSelection(item[keys.id]); }}
65
+ >×</span>
66
+ </span>
67
+ );
68
+ })}
69
+ </div>
70
+ ) : summary(selectedItems, mode, keys, placeholder)}
71
+ <span className="xeplr-dropdown-caret">⌄</span>
72
+ </div>
73
+
74
+ <div
75
+ className={'xeplr-dropdown-panel' + (isOpen ? ' xeplr-dropdown-panel-open' : '')}
76
+ role="listbox"
77
+ >
78
+ {searchable && (
79
+ <div className="xeplr-dropdown-search">
80
+ <input
81
+ autoFocus
82
+ className="xeplr-dropdown-search-input"
83
+ placeholder="Search…"
84
+ value={query}
85
+ onChange={handleSearch}
86
+ />
87
+ </div>
88
+ )}
89
+
90
+ {loading && <div className="xeplr-dropdown-state">Loading…</div>}
91
+ {error && <div className="xeplr-dropdown-state xeplr-dropdown-state-error">{error}</div>}
92
+
93
+ {!loading && !error && filteredItems.length === 0 && !showCreate && (
94
+ <div className="xeplr-dropdown-state">
95
+ {canCreate && !trimmedQuery ? 'Type a name to add' : 'No matches'}
96
+ </div>
97
+ )}
98
+
99
+ {!loading && !error && filteredItems.map(function(item) {
100
+ var selected = isSelected(item[keys.id]);
101
+ var meta = joinExtras(item, keys.extras);
102
+ return (
103
+ <div
104
+ key={item[keys.id]}
105
+ role="option"
106
+ aria-selected={selected}
107
+ className={'xeplr-dropdown-row' + (selected ? ' xeplr-dropdown-row-selected' : '')}
108
+ onClick={function() { selectItem(item[keys.id]); }}
109
+ >
110
+ {mode === 'multi' && (
111
+ <span className={'xeplr-dropdown-check' + (selected ? ' xeplr-dropdown-check-on' : '')}>
112
+ {selected ? '✓' : ''}
113
+ </span>
114
+ )}
115
+ <span className="xeplr-dropdown-avatar">{initials(item[keys.name])}</span>
116
+ <span className="xeplr-dropdown-info">
117
+ <span className="xeplr-dropdown-name">{item[keys.name]}</span>
118
+ {meta && <span className="xeplr-dropdown-meta">{meta}</span>}
119
+ </span>
120
+ </div>
121
+ );
122
+ })}
123
+
124
+ {showCreate && (
125
+ <div
126
+ role="option"
127
+ className="xeplr-dropdown-row xeplr-dropdown-create"
128
+ onClick={function() { if (!creating) createItem(); }}
129
+ >
130
+ <span className="xeplr-dropdown-avatar xeplr-dropdown-create-plus">+</span>
131
+ <span className="xeplr-dropdown-info">
132
+ <span className="xeplr-dropdown-name">
133
+ {creating ? 'Adding…' : (createLabel + ' “' + trimmedQuery + '”')}
134
+ </span>
135
+ </span>
136
+ </div>
137
+ )}
138
+
139
+ {createError && (
140
+ <div className="xeplr-dropdown-state xeplr-dropdown-state-error">{createError}</div>
141
+ )}
142
+
143
+ {hasSelection && (
144
+ <div className="xeplr-dropdown-footer">
145
+ <span className="xeplr-dropdown-clear" onClick={clearSelection}>Clear selection</span>
146
+ </div>
147
+ )}
148
+ </div>
149
+ </div>
150
+ );
151
+ }