@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
@@ -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,3 @@
1
+ export var DATE_FIELD_RULES = [
2
+ { selector: 'input[type="date"], input[type="datetime-local"], input.xeplr-datefield-input', label: 'Date input element' }
3
+ ];
@@ -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';