@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.
- package/package.json +22 -6
- package/src/confirm/confirm.js +106 -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 +49 -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/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/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 +115 -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/snackbar/snackbar.js +6 -3
- package/src/theme/default.theme.json +178 -0
- package/src/theme/index.js +75 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Validates that required elements exist in the rendered design.
|
|
5
|
+
* Throws a visible error if any required element is missing.
|
|
6
|
+
*
|
|
7
|
+
* @param {string} componentName - Name of the page (for error messages)
|
|
8
|
+
* @param {Array<{id?: string, role?: string, selector?: string, label: string}>} requiredElements
|
|
9
|
+
*/
|
|
10
|
+
export function useDesignValidator(componentName, requiredElements) {
|
|
11
|
+
var containerRef = useRef(null);
|
|
12
|
+
|
|
13
|
+
useEffect(function() {
|
|
14
|
+
if (!containerRef.current) return;
|
|
15
|
+
|
|
16
|
+
var missing = [];
|
|
17
|
+
for (var i = 0; i < requiredElements.length; i++) {
|
|
18
|
+
var rule = requiredElements[i];
|
|
19
|
+
var found = false;
|
|
20
|
+
|
|
21
|
+
if (rule.id) {
|
|
22
|
+
found = !!containerRef.current.querySelector('#' + rule.id);
|
|
23
|
+
} else if (rule.role) {
|
|
24
|
+
found = !!containerRef.current.querySelector('[role="' + rule.role + '"]');
|
|
25
|
+
} else if (rule.selector) {
|
|
26
|
+
found = !!containerRef.current.querySelector(rule.selector);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!found) {
|
|
30
|
+
missing.push(rule.label + (rule.id ? ' (id="' + rule.id + '")' : '') + (rule.selector ? ' (' + rule.selector + ')' : ''));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (missing.length > 0) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
'[xeplr-ui-utils] ' + componentName + ' design is missing required elements:\n' +
|
|
37
|
+
missing.map(function(m) { return ' - ' + m; }).join('\n')
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
}, []);
|
|
41
|
+
|
|
42
|
+
return containerRef;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export var DATA_IMPORT_RULES = [
|
|
46
|
+
{ selector: 'input[type="file"]', label: 'File input' }
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
export var IMPORT_HISTORY_RULES = [];
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DateField — pure model. No React.
|
|
3
|
+
*
|
|
4
|
+
* Three flavours:
|
|
5
|
+
* - 'date' → ISO date string (YYYY-MM-DD)
|
|
6
|
+
* - 'datetime' → ISO datetime string (YYYY-MM-DDTHH:mm)
|
|
7
|
+
* - 'age' → input is a DOB (date), output value is the DOB but the
|
|
8
|
+
* age in years is computed and returned alongside.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export var DATE_TYPES = ['date', 'datetime', 'age'];
|
|
12
|
+
|
|
13
|
+
export function isValidISODate(s) {
|
|
14
|
+
if (typeof s !== 'string' || s.length === 0) return false;
|
|
15
|
+
var d = new Date(s);
|
|
16
|
+
return !isNaN(d.getTime());
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Calculate completed years between dob and `now`. Returns null if dob is invalid.
|
|
21
|
+
*/
|
|
22
|
+
export function calculateAge(dob, now) {
|
|
23
|
+
if (!dob) return null;
|
|
24
|
+
var birth = dob instanceof Date ? dob : new Date(dob);
|
|
25
|
+
if (isNaN(birth.getTime())) return null;
|
|
26
|
+
var ref = now || new Date();
|
|
27
|
+
var years = ref.getFullYear() - birth.getFullYear();
|
|
28
|
+
var monthDiff = ref.getMonth() - birth.getMonth();
|
|
29
|
+
if (monthDiff < 0 || (monthDiff === 0 && ref.getDate() < birth.getDate())) {
|
|
30
|
+
years--;
|
|
31
|
+
}
|
|
32
|
+
return years < 0 ? null : years;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Map our public type to the underlying <input> type attribute.
|
|
37
|
+
*/
|
|
38
|
+
export function inputTypeFor(type) {
|
|
39
|
+
if (type === 'datetime') return 'datetime-local';
|
|
40
|
+
return 'date';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Normalise a Date or string into the format the <input> element expects.
|
|
45
|
+
*/
|
|
46
|
+
export function toInputValue(value, type) {
|
|
47
|
+
if (!value) return '';
|
|
48
|
+
var d = value instanceof Date ? value : new Date(value);
|
|
49
|
+
if (isNaN(d.getTime())) return '';
|
|
50
|
+
var pad = function(n) { return n < 10 ? '0' + n : '' + n; };
|
|
51
|
+
var ymd = d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate());
|
|
52
|
+
if (type === 'datetime') {
|
|
53
|
+
return ymd + 'T' + pad(d.getHours()) + ':' + pad(d.getMinutes());
|
|
54
|
+
}
|
|
55
|
+
return ymd;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function validateBounds(value, min, max) {
|
|
59
|
+
if (!value) return { valid: true };
|
|
60
|
+
var d = value instanceof Date ? value : new Date(value);
|
|
61
|
+
if (isNaN(d.getTime())) return { valid: false, error: 'Invalid date' };
|
|
62
|
+
if (min) {
|
|
63
|
+
var dMin = min instanceof Date ? min : new Date(min);
|
|
64
|
+
if (d < dMin) return { valid: false, error: 'Date is before minimum' };
|
|
65
|
+
}
|
|
66
|
+
if (max) {
|
|
67
|
+
var dMax = max instanceof Date ? max : new Date(max);
|
|
68
|
+
if (d > dMax) return { valid: false, error: 'Date is after maximum' };
|
|
69
|
+
}
|
|
70
|
+
return { valid: true };
|
|
71
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import './dateField.css';
|
|
2
|
+
|
|
3
|
+
export default function DateFieldSample({
|
|
4
|
+
type, inputType, raw, age, error, placeholder, disabled, min, max,
|
|
5
|
+
handleChange
|
|
6
|
+
}) {
|
|
7
|
+
return (
|
|
8
|
+
<div className={'xeplr-datefield xeplr-datefield-' + type + (error ? ' xeplr-datefield-error' : '')}>
|
|
9
|
+
<div className="xeplr-datefield-row">
|
|
10
|
+
<input
|
|
11
|
+
type={inputType}
|
|
12
|
+
className="xeplr-datefield-input"
|
|
13
|
+
value={raw}
|
|
14
|
+
placeholder={placeholder}
|
|
15
|
+
disabled={disabled}
|
|
16
|
+
min={min}
|
|
17
|
+
max={max}
|
|
18
|
+
onChange={handleChange}
|
|
19
|
+
/>
|
|
20
|
+
{type === 'age' && (
|
|
21
|
+
<div className="xeplr-datefield-age">
|
|
22
|
+
<span className="xeplr-datefield-age-label">Age</span>
|
|
23
|
+
<span className="xeplr-datefield-age-value">{age === null ? '—' : age + ' yrs'}</span>
|
|
24
|
+
</div>
|
|
25
|
+
)}
|
|
26
|
+
</div>
|
|
27
|
+
{error && <div className="xeplr-datefield-error-text">{error}</div>}
|
|
28
|
+
</div>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
.xeplr-datefield {
|
|
2
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
3
|
+
color: #e0e0e0;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
.xeplr-datefield-row {
|
|
7
|
+
display: flex;
|
|
8
|
+
align-items: stretch;
|
|
9
|
+
gap: 10px;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.xeplr-datefield-input {
|
|
13
|
+
flex: 1;
|
|
14
|
+
background: #1a1a1a;
|
|
15
|
+
border: 1px solid #444;
|
|
16
|
+
border-radius: 6px;
|
|
17
|
+
padding: 9px 12px;
|
|
18
|
+
color: inherit;
|
|
19
|
+
font-size: 14px;
|
|
20
|
+
outline: none;
|
|
21
|
+
color-scheme: dark;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.xeplr-datefield-input:focus {
|
|
25
|
+
border-color: #646cff;
|
|
26
|
+
box-shadow: 0 0 0 2px rgba(100, 108, 255, 0.18);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.xeplr-datefield-error .xeplr-datefield-input {
|
|
30
|
+
border-color: #e25555;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.xeplr-datefield-age {
|
|
34
|
+
display: flex;
|
|
35
|
+
flex-direction: column;
|
|
36
|
+
justify-content: center;
|
|
37
|
+
background: #232540;
|
|
38
|
+
border-radius: 6px;
|
|
39
|
+
padding: 4px 14px;
|
|
40
|
+
min-width: 90px;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.xeplr-datefield-age-label {
|
|
44
|
+
font-size: 10px;
|
|
45
|
+
text-transform: uppercase;
|
|
46
|
+
letter-spacing: 0.6px;
|
|
47
|
+
color: #888bbf;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.xeplr-datefield-age-value {
|
|
51
|
+
font-size: 14px;
|
|
52
|
+
font-weight: 600;
|
|
53
|
+
color: #e0e0e0;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.xeplr-datefield-error-text {
|
|
57
|
+
margin-top: 4px;
|
|
58
|
+
font-size: 12px;
|
|
59
|
+
color: #e25555;
|
|
60
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as DateFieldSample } from './DateFieldSample.jsx';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { useDateFieldController } from './useDateFieldController.js';
|
|
2
|
+
import DateFieldSample from './designs/DateFieldSample.jsx';
|
|
3
|
+
import { useDesignValidator } from '../fileUpload/validateDesign.js';
|
|
4
|
+
import { DATE_FIELD_RULES } from './validateDesign.js';
|
|
5
|
+
|
|
6
|
+
export function DateFieldPage(props) {
|
|
7
|
+
var controller = useDateFieldController(props);
|
|
8
|
+
var ref = useDesignValidator('DateFieldPage', DATE_FIELD_RULES);
|
|
9
|
+
return <div ref={ref}><DateFieldSample {...controller} /></div>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { useState, useEffect, useMemo } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
DATE_TYPES,
|
|
4
|
+
calculateAge,
|
|
5
|
+
inputTypeFor,
|
|
6
|
+
toInputValue,
|
|
7
|
+
validateBounds
|
|
8
|
+
} from './dateField.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param {object} props
|
|
12
|
+
* @param {'date'|'datetime'|'age'} [props.type='date']
|
|
13
|
+
* @param {Date|string|null} [props.value] — controlled value
|
|
14
|
+
* @param {Date|string|null} [props.defaultValue] — uncontrolled initial value
|
|
15
|
+
* @param {(value, meta) => void} [props.onChange] — meta.age included for age type
|
|
16
|
+
* @param {Date|string} [props.min]
|
|
17
|
+
* @param {Date|string} [props.max]
|
|
18
|
+
* @param {string} [props.placeholder]
|
|
19
|
+
* @param {boolean} [props.disabled]
|
|
20
|
+
*/
|
|
21
|
+
export function useDateFieldController(props) {
|
|
22
|
+
props = props || {};
|
|
23
|
+
var type = props.type || 'date';
|
|
24
|
+
if (DATE_TYPES.indexOf(type) === -1) {
|
|
25
|
+
throw new Error('[xeplr-ui-utils:DateField] Unknown type: ' + type + '. Expected one of ' + DATE_TYPES.join(', '));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
var isControlled = props.value !== undefined;
|
|
29
|
+
var initial = isControlled ? props.value : (props.defaultValue !== undefined ? props.defaultValue : null);
|
|
30
|
+
|
|
31
|
+
var [raw, setRaw] = useState(toInputValue(initial, type === 'age' ? 'date' : type));
|
|
32
|
+
var [error, setError] = useState('');
|
|
33
|
+
|
|
34
|
+
useEffect(function() {
|
|
35
|
+
if (isControlled) {
|
|
36
|
+
setRaw(toInputValue(props.value, type === 'age' ? 'date' : type));
|
|
37
|
+
}
|
|
38
|
+
}, [props.value, isControlled, type]);
|
|
39
|
+
|
|
40
|
+
var inputType = useMemo(function() {
|
|
41
|
+
return inputTypeFor(type === 'age' ? 'date' : type);
|
|
42
|
+
}, [type]);
|
|
43
|
+
|
|
44
|
+
var age = useMemo(function() {
|
|
45
|
+
if (type !== 'age') return null;
|
|
46
|
+
return calculateAge(raw);
|
|
47
|
+
}, [type, raw]);
|
|
48
|
+
|
|
49
|
+
function emit(nextRaw) {
|
|
50
|
+
var bounds = validateBounds(nextRaw, props.min, props.max);
|
|
51
|
+
setError(bounds.valid ? '' : bounds.error);
|
|
52
|
+
if (props.onChange) {
|
|
53
|
+
var meta = {};
|
|
54
|
+
if (type === 'age') meta.age = calculateAge(nextRaw);
|
|
55
|
+
props.onChange(nextRaw || null, meta);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function handleChange(e) {
|
|
60
|
+
var v = e.target.value;
|
|
61
|
+
if (!isControlled) setRaw(v);
|
|
62
|
+
emit(v);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
type,
|
|
67
|
+
inputType,
|
|
68
|
+
raw,
|
|
69
|
+
age,
|
|
70
|
+
error,
|
|
71
|
+
placeholder: props.placeholder,
|
|
72
|
+
disabled: !!props.disabled,
|
|
73
|
+
min: props.min ? toInputValue(props.min, type === 'age' ? 'date' : type) : undefined,
|
|
74
|
+
max: props.max ? toInputValue(props.max, type === 'age' ? 'date' : type) : undefined,
|
|
75
|
+
handleChange
|
|
76
|
+
};
|
|
77
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/* Uses @xeplr/ui-account's --xeplr-* theme tokens when available (see
|
|
2
|
+
xeplr-ui-account/src/designs/theme.css — apply .xeplr-theme-light/-dark on
|
|
3
|
+
an ancestor to pick a theme); falls back to the original dark hex values
|
|
4
|
+
when that stylesheet isn't loaded, so this still renders sensibly standalone. */
|
|
5
|
+
|
|
6
|
+
.xeplr-dropdown {
|
|
7
|
+
position: relative;
|
|
8
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
9
|
+
color: var(--xeplr-text-primary, #e0e0e0);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.xeplr-dropdown-trigger {
|
|
13
|
+
display: flex;
|
|
14
|
+
align-items: center;
|
|
15
|
+
gap: 8px;
|
|
16
|
+
min-height: 40px;
|
|
17
|
+
background: var(--xeplr-input-bg, #1a1a1a);
|
|
18
|
+
border: 1px solid var(--xeplr-input-border, #444);
|
|
19
|
+
border-radius: 6px;
|
|
20
|
+
padding: 6px 10px 6px 12px;
|
|
21
|
+
cursor: pointer;
|
|
22
|
+
font-size: 14px;
|
|
23
|
+
color: var(--xeplr-input-text, #e0e0e0);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.xeplr-dropdown-open .xeplr-dropdown-trigger {
|
|
27
|
+
border-color: var(--xeplr-accent, #646cff);
|
|
28
|
+
box-shadow: 0 0 0 2px rgba(100, 108, 255, 0.18);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.xeplr-dropdown-disabled .xeplr-dropdown-trigger {
|
|
32
|
+
opacity: 0.55;
|
|
33
|
+
cursor: not-allowed;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.xeplr-dropdown-placeholder { color: var(--xeplr-input-placeholder, #888); flex: 1; }
|
|
37
|
+
.xeplr-dropdown-selected-single { flex: 1; }
|
|
38
|
+
.xeplr-dropdown-selected-multi { flex: 1; }
|
|
39
|
+
.xeplr-dropdown-selected-more { color: var(--xeplr-text-secondary, #888bbf); margin-left: 6px; font-size: 12px; }
|
|
40
|
+
|
|
41
|
+
.xeplr-dropdown-caret {
|
|
42
|
+
margin-left: auto;
|
|
43
|
+
color: var(--xeplr-text-muted, #888);
|
|
44
|
+
font-size: 14px;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.xeplr-dropdown-chips {
|
|
48
|
+
display: flex;
|
|
49
|
+
flex-wrap: wrap;
|
|
50
|
+
gap: 4px;
|
|
51
|
+
flex: 1;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.xeplr-dropdown-chip {
|
|
55
|
+
display: inline-flex;
|
|
56
|
+
align-items: center;
|
|
57
|
+
gap: 6px;
|
|
58
|
+
background: var(--xeplr-tag-bg, #232540);
|
|
59
|
+
border-radius: 999px;
|
|
60
|
+
padding: 3px 4px 3px 10px;
|
|
61
|
+
font-size: 12px;
|
|
62
|
+
color: var(--xeplr-tag-text, #c8cbe8);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.xeplr-dropdown-chip-x {
|
|
66
|
+
width: 18px;
|
|
67
|
+
height: 18px;
|
|
68
|
+
border-radius: 50%;
|
|
69
|
+
background: var(--xeplr-bg-primary, #1a1a1a);
|
|
70
|
+
display: grid;
|
|
71
|
+
place-items: center;
|
|
72
|
+
cursor: pointer;
|
|
73
|
+
font-size: 12px;
|
|
74
|
+
line-height: 1;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.xeplr-dropdown-chip-x:hover { background: var(--xeplr-danger, #e25555); color: var(--xeplr-accent-text, #fff); }
|
|
78
|
+
|
|
79
|
+
.xeplr-dropdown-panel {
|
|
80
|
+
display: none;
|
|
81
|
+
position: absolute;
|
|
82
|
+
top: calc(100% + 6px);
|
|
83
|
+
left: 0;
|
|
84
|
+
right: 0;
|
|
85
|
+
background: var(--xeplr-bg-tertiary, #141625);
|
|
86
|
+
border: 1px solid var(--xeplr-border-primary, #444);
|
|
87
|
+
border-radius: 6px;
|
|
88
|
+
box-shadow: var(--xeplr-shadow-lg, 0 10px 30px rgba(0, 0, 0, 0.55));
|
|
89
|
+
z-index: 50;
|
|
90
|
+
overflow: hidden;
|
|
91
|
+
max-height: 360px;
|
|
92
|
+
overflow-y: auto;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.xeplr-dropdown-panel-open { display: block; }
|
|
96
|
+
|
|
97
|
+
.xeplr-dropdown-search {
|
|
98
|
+
padding: 10px 12px;
|
|
99
|
+
border-bottom: 1px solid var(--xeplr-border-secondary, #2a2d44);
|
|
100
|
+
background: var(--xeplr-bg-secondary, #1a1c30);
|
|
101
|
+
position: sticky;
|
|
102
|
+
top: 0;
|
|
103
|
+
z-index: 1;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.xeplr-dropdown-search-input {
|
|
107
|
+
width: 100%;
|
|
108
|
+
background: var(--xeplr-input-bg, #0e0f1c);
|
|
109
|
+
border: 1px solid var(--xeplr-input-border, #2a2d44);
|
|
110
|
+
border-radius: 4px;
|
|
111
|
+
color: var(--xeplr-input-text, #e0e0e0);
|
|
112
|
+
padding: 7px 10px;
|
|
113
|
+
font-size: 13px;
|
|
114
|
+
outline: none;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.xeplr-dropdown-search-input:focus {
|
|
118
|
+
border-color: var(--xeplr-accent, #646cff);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.xeplr-dropdown-state {
|
|
122
|
+
padding: 14px 16px;
|
|
123
|
+
color: var(--xeplr-text-secondary, #888bbf);
|
|
124
|
+
font-size: 13px;
|
|
125
|
+
text-align: center;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.xeplr-dropdown-state-error { color: var(--xeplr-danger, #e25555); }
|
|
129
|
+
|
|
130
|
+
.xeplr-dropdown-row {
|
|
131
|
+
display: grid;
|
|
132
|
+
grid-template-columns: auto 32px 1fr;
|
|
133
|
+
gap: 10px;
|
|
134
|
+
align-items: center;
|
|
135
|
+
padding: 9px 12px;
|
|
136
|
+
cursor: pointer;
|
|
137
|
+
border-bottom: 1px solid var(--xeplr-border-secondary, #1f2236);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.xeplr-dropdown-single .xeplr-dropdown-row {
|
|
141
|
+
grid-template-columns: 32px 1fr;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.xeplr-dropdown-row:hover { background: var(--xeplr-bg-hover, #1c1f33); }
|
|
145
|
+
.xeplr-dropdown-row-selected { background: var(--xeplr-bg-active, rgba(100, 108, 255, 0.13)); }
|
|
146
|
+
|
|
147
|
+
.xeplr-dropdown-check {
|
|
148
|
+
width: 16px;
|
|
149
|
+
height: 16px;
|
|
150
|
+
border-radius: 4px;
|
|
151
|
+
border: 1.5px solid var(--xeplr-input-border, #444);
|
|
152
|
+
background: var(--xeplr-input-bg, #0e0f1c);
|
|
153
|
+
display: grid;
|
|
154
|
+
place-items: center;
|
|
155
|
+
font-size: 11px;
|
|
156
|
+
color: transparent;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.xeplr-dropdown-check-on {
|
|
160
|
+
background: var(--xeplr-accent, #646cff);
|
|
161
|
+
border-color: var(--xeplr-accent, #646cff);
|
|
162
|
+
color: var(--xeplr-accent-text, #fff);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.xeplr-dropdown-avatar {
|
|
166
|
+
width: 30px;
|
|
167
|
+
height: 30px;
|
|
168
|
+
border-radius: 50%;
|
|
169
|
+
background: var(--xeplr-tag-bg, #232540);
|
|
170
|
+
color: var(--xeplr-tag-text, #b8bbe0);
|
|
171
|
+
display: grid;
|
|
172
|
+
place-items: center;
|
|
173
|
+
font-size: 11px;
|
|
174
|
+
font-weight: 700;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.xeplr-dropdown-info {
|
|
178
|
+
display: flex;
|
|
179
|
+
flex-direction: column;
|
|
180
|
+
min-width: 0;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.xeplr-dropdown-name {
|
|
184
|
+
font-size: 13px;
|
|
185
|
+
color: var(--xeplr-text-primary, #e6e8f5);
|
|
186
|
+
font-weight: 600;
|
|
187
|
+
overflow: hidden;
|
|
188
|
+
text-overflow: ellipsis;
|
|
189
|
+
white-space: nowrap;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.xeplr-dropdown-meta {
|
|
193
|
+
font-size: 11px;
|
|
194
|
+
color: var(--xeplr-text-secondary, #888bbf);
|
|
195
|
+
margin-top: 2px;
|
|
196
|
+
overflow: hidden;
|
|
197
|
+
text-overflow: ellipsis;
|
|
198
|
+
white-space: nowrap;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.xeplr-dropdown-footer {
|
|
202
|
+
padding: 8px 12px;
|
|
203
|
+
border-top: 1px solid var(--xeplr-border-secondary, #2a2d44);
|
|
204
|
+
background: var(--xeplr-bg-secondary, #1a1c30);
|
|
205
|
+
text-align: right;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.xeplr-dropdown-clear {
|
|
209
|
+
font-size: 12px;
|
|
210
|
+
color: var(--xeplr-text-secondary, #888bbf);
|
|
211
|
+
cursor: pointer;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.xeplr-dropdown-clear:hover { color: var(--xeplr-danger, #e25555); }
|
|
215
|
+
|
|
216
|
+
.xeplr-dropdown-create {
|
|
217
|
+
border-top: 1px solid var(--xeplr-border-secondary, #2a2d44);
|
|
218
|
+
color: var(--xeplr-accent, #9fb2ff);
|
|
219
|
+
}
|
|
220
|
+
.xeplr-dropdown-create:hover { background: var(--xeplr-bg-hover, #232744); }
|
|
221
|
+
.xeplr-dropdown-create .xeplr-dropdown-name { color: var(--xeplr-accent, #9fb2ff); font-weight: 600; }
|
|
222
|
+
.xeplr-dropdown-create-plus {
|
|
223
|
+
background: var(--xeplr-tag-bg, #2a2f52);
|
|
224
|
+
color: var(--xeplr-accent, #9fb2ff);
|
|
225
|
+
font-weight: 700;
|
|
226
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as DropdownSample } from './DropdownSample.jsx';
|