@referralgps/selectra 1.0.0
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/LICENSE +203 -0
- package/README.md +346 -0
- package/dist/selectra.css +1 -0
- package/dist/selectra.es.js +1318 -0
- package/dist/selectra.es.js.map +1 -0
- package/dist/selectra.iife.js +32 -0
- package/dist/selectra.iife.js.map +1 -0
- package/dist/selectra.umd.js +32 -0
- package/dist/selectra.umd.js.map +1 -0
- package/package.json +64 -0
- package/src-alpine/README.md +346 -0
- package/src-alpine/index.d.ts +217 -0
- package/src-alpine/index.js +70 -0
- package/src-alpine/plugins/index.js +219 -0
- package/src-alpine/selectize.js +1022 -0
- package/src-alpine/sifter.js +270 -0
- package/src-alpine/styles/selectra.css +277 -0
- package/src-alpine/utils.js +181 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sifter - A library for textually searching arrays and hashes of objects.
|
|
3
|
+
* Designed specifically for autocomplete. Supports diacritics, smart scoring,
|
|
4
|
+
* multi-field sorting, and nested properties.
|
|
5
|
+
*
|
|
6
|
+
* Originally by Brian Reavis, modernized as ES module.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const DIACRITICS = {
|
|
10
|
+
a: '[aḀḁĂăÂâǍǎȺⱥȦȧẠạÄäÀàÁáĀāÃãÅåąĄÃąĄ]',
|
|
11
|
+
b: '[b␢βΒB฿𐌁ᛒ]',
|
|
12
|
+
c: '[cĆćĈĉČčĊċC̄c̄ÇçḈḉȻȼƇƈɕᴄCc]',
|
|
13
|
+
d: '[dĎďḊḋḐḑḌḍḒḓḎḏĐđD̦d̦ƉɖƊɗƋƌᵭᶁᶑȡᴅDdð]',
|
|
14
|
+
e: '[eÉéÈèÊêḘḙĚěĔĕẼẽḚḛẺẻĖėËëĒēȨȩĘęᶒɆɇȄȅẾếỀềỄễỂểḜḝḖḗḔḕȆȇẸẹỆệⱸᴇEeɘǝƏƐε]',
|
|
15
|
+
f: '[fƑƒḞḟ]',
|
|
16
|
+
g: '[gɢ₲ǤǥĜĝĞğĢģƓɠĠġ]',
|
|
17
|
+
h: '[hĤĥĦħḨḩẖẖḤḥḢḣɦʰǶƕ]',
|
|
18
|
+
i: '[iÍíÌìĬĭÎîǏǐÏïḮḯĨĩĮįĪīỈỉȈȉȊȋỊịḬḭƗɨɨ̆ᵻᶖİiIıɪIi]',
|
|
19
|
+
j: '[jȷĴĵɈɉʝɟʲ]',
|
|
20
|
+
k: '[kƘƙꝀꝁḰḱǨǩḲḳḴḵκϰ₭]',
|
|
21
|
+
l: '[lŁłĽľĻļĹĺḶḷḸḹḼḽḺḻĿŀȽƚⱠⱡⱢɫɬᶅɭȴʟLl]',
|
|
22
|
+
n: '[nŃńǸǹŇňÑñṄṅŅņṆṇṊṋṈṉN̈n̈ƝɲȠƞᵰᶇɳȵɴNnŊŋ]',
|
|
23
|
+
o: '[oØøÖöÓóÒòÔôǑǒŐőŎŏȮȯỌọƟɵƠơỎỏŌōÕõǪǫȌȍՕօ]',
|
|
24
|
+
p: '[pṔṕṖṗⱣᵽƤƥᵱ]',
|
|
25
|
+
q: '[qꝖꝗʠɊɋꝘꝙq̃]',
|
|
26
|
+
r: '[rŔŕɌɍŘřŖŗṘṙȐȑȒȓṚṛⱤɽ]',
|
|
27
|
+
s: '[sŚśṠṡṢṣꞨꞩŜŝŠšŞşȘșS̈s̈]',
|
|
28
|
+
t: '[tŤťṪṫŢţṬṭƮʈȚțṰṱṮṯƬƭ]',
|
|
29
|
+
u: '[uŬŭɄʉỤụÜüÚúÙùÛûǓǔŰűŬŭƯưỦủŪūŨũŲųȔȕ∪]',
|
|
30
|
+
v: '[vṼṽṾṿƲʋꝞꝟⱱʋ]',
|
|
31
|
+
w: '[wẂẃẀẁŴŵẄẅẆẇẈẉ]',
|
|
32
|
+
x: '[xẌẍẊẋχ]',
|
|
33
|
+
y: '[yÝýỲỳŶŷŸÿỸỹẎẏỴỵɎɏƳƴ]',
|
|
34
|
+
z: '[zŹźẐẑŽžŻżẒẓẔẕƵƶ]',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const asciifold = (() => {
|
|
38
|
+
const lookup = {};
|
|
39
|
+
let i18nChars = '';
|
|
40
|
+
for (const k in DIACRITICS) {
|
|
41
|
+
const chunk = DIACRITICS[k].substring(2, DIACRITICS[k].length - 1);
|
|
42
|
+
i18nChars += chunk;
|
|
43
|
+
for (let i = 0; i < chunk.length; i++) {
|
|
44
|
+
lookup[chunk.charAt(i)] = k;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const regexp = new RegExp('[' + i18nChars + ']', 'g');
|
|
48
|
+
return (str) =>
|
|
49
|
+
str
|
|
50
|
+
.replace(regexp, (ch) => lookup[ch] || '')
|
|
51
|
+
.toLowerCase();
|
|
52
|
+
})();
|
|
53
|
+
|
|
54
|
+
function escapeRegex(str) {
|
|
55
|
+
return (str + '').replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function getAttr(obj, name, nesting) {
|
|
59
|
+
if (!obj || !name) return undefined;
|
|
60
|
+
if (!nesting) return obj[name];
|
|
61
|
+
const names = name.split('.');
|
|
62
|
+
let current = obj;
|
|
63
|
+
while (names.length && current) {
|
|
64
|
+
current = current[names.shift()];
|
|
65
|
+
}
|
|
66
|
+
return current;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function cmp(a, b) {
|
|
70
|
+
if (typeof a === 'number' && typeof b === 'number') {
|
|
71
|
+
return a > b ? 1 : a < b ? -1 : 0;
|
|
72
|
+
}
|
|
73
|
+
a = asciifold(String(a || ''));
|
|
74
|
+
b = asciifold(String(b || ''));
|
|
75
|
+
if (a > b) return 1;
|
|
76
|
+
if (b > a) return -1;
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export default class Sifter {
|
|
81
|
+
constructor(items, settings = {}) {
|
|
82
|
+
this.items = items;
|
|
83
|
+
this.settings = { diacritics: true, ...settings };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
tokenize(query, respectWordBoundaries) {
|
|
87
|
+
query = String(query || '').toLowerCase().trim();
|
|
88
|
+
if (!query.length) return [];
|
|
89
|
+
|
|
90
|
+
const tokens = [];
|
|
91
|
+
const words = query.split(/\s+/);
|
|
92
|
+
|
|
93
|
+
for (const word of words) {
|
|
94
|
+
let regex = escapeRegex(word);
|
|
95
|
+
if (this.settings.diacritics) {
|
|
96
|
+
for (const letter in DIACRITICS) {
|
|
97
|
+
regex = regex.replace(new RegExp(letter, 'g'), DIACRITICS[letter]);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (respectWordBoundaries) regex = '\\b' + regex;
|
|
101
|
+
tokens.push({ string: word, regex: new RegExp(regex, 'i') });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return tokens;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
getScoreFunction(search, options) {
|
|
108
|
+
search = this.prepareSearch(search, options);
|
|
109
|
+
const { tokens } = search;
|
|
110
|
+
const { fields } = search.options;
|
|
111
|
+
const tokenCount = tokens.length;
|
|
112
|
+
const { nesting } = search.options;
|
|
113
|
+
|
|
114
|
+
const scoreValue = (value, token) => {
|
|
115
|
+
if (!value) return 0;
|
|
116
|
+
value = String(value || '');
|
|
117
|
+
const pos = value.search(token.regex);
|
|
118
|
+
if (pos === -1) return 0;
|
|
119
|
+
let score = token.string.length / value.length;
|
|
120
|
+
if (pos === 0) score += 0.5;
|
|
121
|
+
return score;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const fieldCount = fields.length;
|
|
125
|
+
const scoreObject =
|
|
126
|
+
fieldCount === 0
|
|
127
|
+
? () => 0
|
|
128
|
+
: fieldCount === 1
|
|
129
|
+
? (token, data) => scoreValue(getAttr(data, fields[0], nesting), token)
|
|
130
|
+
: (token, data) => {
|
|
131
|
+
let sum = 0;
|
|
132
|
+
for (let i = 0; i < fieldCount; i++) {
|
|
133
|
+
sum += scoreValue(getAttr(data, fields[i], nesting), token);
|
|
134
|
+
}
|
|
135
|
+
return sum / fieldCount;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
if (!tokenCount) return () => 0;
|
|
139
|
+
if (tokenCount === 1) return (data) => scoreObject(tokens[0], data);
|
|
140
|
+
|
|
141
|
+
if (search.options.conjunction === 'and') {
|
|
142
|
+
return (data) => {
|
|
143
|
+
let sum = 0;
|
|
144
|
+
for (let i = 0; i < tokenCount; i++) {
|
|
145
|
+
const score = scoreObject(tokens[i], data);
|
|
146
|
+
if (score <= 0) return 0;
|
|
147
|
+
sum += score;
|
|
148
|
+
}
|
|
149
|
+
return sum / tokenCount;
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return (data) => {
|
|
154
|
+
let sum = 0;
|
|
155
|
+
for (let i = 0; i < tokenCount; i++) {
|
|
156
|
+
sum += scoreObject(tokens[i], data);
|
|
157
|
+
}
|
|
158
|
+
return sum / tokenCount;
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
getSortFunction(search, options) {
|
|
163
|
+
search = this.prepareSearch(search, options);
|
|
164
|
+
const sort = (!search.query && options.sort_empty) || options.sort;
|
|
165
|
+
|
|
166
|
+
const getField = (name, result) => {
|
|
167
|
+
if (name === '$score') return result.score;
|
|
168
|
+
return getAttr(this.items[result.id], name, options.nesting);
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const fields = [];
|
|
172
|
+
if (sort) {
|
|
173
|
+
for (const s of sort) {
|
|
174
|
+
if (search.query || s.field !== '$score') {
|
|
175
|
+
fields.push(s);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (search.query) {
|
|
181
|
+
let implicitScore = true;
|
|
182
|
+
for (const f of fields) {
|
|
183
|
+
if (f.field === '$score') {
|
|
184
|
+
implicitScore = false;
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (implicitScore) fields.unshift({ field: '$score', direction: 'desc' });
|
|
189
|
+
} else {
|
|
190
|
+
const idx = fields.findIndex((f) => f.field === '$score');
|
|
191
|
+
if (idx !== -1) fields.splice(idx, 1);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const multipliers = fields.map((f) => (f.direction === 'desc' ? -1 : 1));
|
|
195
|
+
const fieldCount = fields.length;
|
|
196
|
+
|
|
197
|
+
if (!fieldCount) return null;
|
|
198
|
+
if (fieldCount === 1) {
|
|
199
|
+
const field = fields[0].field;
|
|
200
|
+
const mult = multipliers[0];
|
|
201
|
+
return (a, b) => mult * cmp(getField(field, a), getField(field, b));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return (a, b) => {
|
|
205
|
+
for (let i = 0; i < fieldCount; i++) {
|
|
206
|
+
const result = multipliers[i] * cmp(getField(fields[i].field, a), getField(fields[i].field, b));
|
|
207
|
+
if (result) return result;
|
|
208
|
+
}
|
|
209
|
+
return 0;
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
prepareSearch(query, options) {
|
|
214
|
+
if (typeof query === 'object') return query;
|
|
215
|
+
options = { ...options };
|
|
216
|
+
|
|
217
|
+
if (options.fields && !Array.isArray(options.fields)) options.fields = [options.fields];
|
|
218
|
+
if (options.sort && !Array.isArray(options.sort)) options.sort = [options.sort];
|
|
219
|
+
if (options.sort_empty && !Array.isArray(options.sort_empty))
|
|
220
|
+
options.sort_empty = [options.sort_empty];
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
options,
|
|
224
|
+
query: String(query || '').toLowerCase(),
|
|
225
|
+
tokens: this.tokenize(query, options.respect_word_boundaries),
|
|
226
|
+
total: 0,
|
|
227
|
+
items: [],
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
search(query, options) {
|
|
232
|
+
const search = this.prepareSearch(query, options);
|
|
233
|
+
const opts = search.options;
|
|
234
|
+
const q = search.query;
|
|
235
|
+
|
|
236
|
+
const fnScore = opts.score || this.getScoreFunction(search);
|
|
237
|
+
|
|
238
|
+
const items = this.items;
|
|
239
|
+
if (q.length) {
|
|
240
|
+
const iterate = Array.isArray(items)
|
|
241
|
+
? (cb) => items.forEach((item, i) => cb(item, i))
|
|
242
|
+
: (cb) => Object.keys(items).forEach((key) => cb(items[key], key));
|
|
243
|
+
|
|
244
|
+
iterate((item, id) => {
|
|
245
|
+
const score = fnScore(item);
|
|
246
|
+
if (opts.filter === false || score > 0) {
|
|
247
|
+
search.items.push({ score, id });
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
} else {
|
|
251
|
+
const iterate = Array.isArray(items)
|
|
252
|
+
? (cb) => items.forEach((item, i) => cb(item, i))
|
|
253
|
+
: (cb) => Object.keys(items).forEach((key) => cb(items[key], key));
|
|
254
|
+
|
|
255
|
+
iterate((item, id) => {
|
|
256
|
+
search.items.push({ score: 1, id });
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const fnSort = this.getSortFunction(search, opts);
|
|
261
|
+
if (fnSort) search.items.sort(fnSort);
|
|
262
|
+
|
|
263
|
+
search.total = search.items.length;
|
|
264
|
+
if (typeof opts.limit === 'number') {
|
|
265
|
+
search.items = search.items.slice(0, opts.limit);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return search;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
@tailwind base;
|
|
2
|
+
@tailwind components;
|
|
3
|
+
@tailwind utilities;
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
* Selectra - Tailwind CSS Component Styles
|
|
7
|
+
*
|
|
8
|
+
* These styles use @apply to create component classes that work
|
|
9
|
+
* with dynamically generated markup. Override with Tailwind utilities.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
@layer components {
|
|
13
|
+
/* ── Wrapper ─────────────────────────────────────────── */
|
|
14
|
+
.selectra-control {
|
|
15
|
+
@apply relative w-full text-sm;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.selectra-control.is-disabled {
|
|
19
|
+
@apply opacity-50 pointer-events-none;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/* ── Input Wrapper ───────────────────────────────────── */
|
|
23
|
+
.selectra-input {
|
|
24
|
+
@apply relative flex flex-wrap items-center gap-1
|
|
25
|
+
w-full min-h-[38px] px-3 py-1.5
|
|
26
|
+
bg-white border border-gray-300 rounded-lg
|
|
27
|
+
cursor-text transition-all duration-150
|
|
28
|
+
text-gray-900;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.selectra-input:hover {
|
|
32
|
+
@apply border-gray-400;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.selectra-input.is-focused {
|
|
36
|
+
@apply border-blue-500 ring-2 ring-blue-500/20 outline-none;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.selectra-input.is-invalid {
|
|
40
|
+
@apply border-red-500 ring-2 ring-red-500/20;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.selectra-input.is-locked {
|
|
44
|
+
@apply bg-gray-50 cursor-default;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.selectra-input.is-single {
|
|
48
|
+
@apply cursor-pointer pr-8;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.selectra-input.is-single.has-items .selectra-search {
|
|
52
|
+
@apply absolute inset-0 px-3 py-1.5;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* ── Search Input ────────────────────────────────────── */
|
|
56
|
+
.selectra-search {
|
|
57
|
+
@apply flex-1 min-w-[60px] bg-transparent
|
|
58
|
+
outline-none border-none p-0 m-0
|
|
59
|
+
text-gray-900 placeholder-gray-400;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.selectra-search:focus {
|
|
63
|
+
@apply outline-none ring-0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.selectra-input.is-single .selectra-search {
|
|
67
|
+
@apply min-w-0 w-full;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/* ── Selected Items (Tags) ───────────────────────────── */
|
|
71
|
+
.selectra-item {
|
|
72
|
+
@apply inline-flex items-center gap-1
|
|
73
|
+
px-2 py-0.5 rounded-md
|
|
74
|
+
bg-blue-50 text-blue-700 border border-blue-200
|
|
75
|
+
text-sm leading-snug
|
|
76
|
+
animate-fade-in;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.selectra-item .selectra-item-remove {
|
|
80
|
+
@apply inline-flex items-center justify-center
|
|
81
|
+
w-4 h-4 rounded-full
|
|
82
|
+
text-blue-400 hover:text-blue-600
|
|
83
|
+
hover:bg-blue-100
|
|
84
|
+
cursor-pointer transition-colors duration-100
|
|
85
|
+
-mr-0.5 ml-0.5;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/* Single select display */
|
|
89
|
+
.selectra-single-value {
|
|
90
|
+
@apply truncate text-gray-900;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/* ── Dropdown Arrow ──────────────────────────────────── */
|
|
94
|
+
.selectra-arrow {
|
|
95
|
+
@apply absolute right-2.5 top-1/2 -translate-y-1/2
|
|
96
|
+
w-4 h-4 text-gray-400
|
|
97
|
+
pointer-events-none transition-transform duration-200;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.selectra-arrow.is-open {
|
|
101
|
+
@apply rotate-180;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/* ── Dropdown ────────────────────────────────────────── */
|
|
105
|
+
.selectra-dropdown {
|
|
106
|
+
@apply absolute z-50 w-full mt-1
|
|
107
|
+
bg-white border border-gray-200 rounded-lg
|
|
108
|
+
shadow-lg overflow-hidden
|
|
109
|
+
animate-dropdown-in;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.selectra-dropdown.is-top {
|
|
113
|
+
@apply bottom-full top-auto mb-1 mt-0;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.selectra-dropdown-content {
|
|
117
|
+
@apply max-h-60 overflow-y-auto overscroll-contain
|
|
118
|
+
py-1;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/* ── Dropdown Option ─────────────────────────────────── */
|
|
122
|
+
.selectra-option {
|
|
123
|
+
@apply px-3 py-2 cursor-pointer
|
|
124
|
+
text-gray-700 transition-colors duration-75
|
|
125
|
+
leading-snug;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.selectra-option:hover,
|
|
129
|
+
.selectra-option.is-active {
|
|
130
|
+
@apply bg-blue-500 text-white;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.selectra-option.is-active .highlight {
|
|
134
|
+
@apply text-white;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.selectra-option.is-disabled {
|
|
138
|
+
@apply opacity-50 cursor-not-allowed;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.selectra-option.is-selected {
|
|
142
|
+
@apply bg-blue-50 text-blue-700;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.selectra-option.is-selected.is-active {
|
|
146
|
+
@apply bg-blue-500 text-white;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/* ── Option Create ───────────────────────────────────── */
|
|
150
|
+
.selectra-option-create {
|
|
151
|
+
@apply px-3 py-2 cursor-pointer
|
|
152
|
+
text-gray-500 transition-colors duration-75
|
|
153
|
+
border-t border-gray-100;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.selectra-option-create:hover,
|
|
157
|
+
.selectra-option-create.is-active {
|
|
158
|
+
@apply bg-blue-500 text-white;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/* ── No Results ──────────────────────────────────────── */
|
|
162
|
+
.selectra-no-results {
|
|
163
|
+
@apply px-3 py-3 text-center text-gray-400 text-sm;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/* ── Loading ─────────────────────────────────────────── */
|
|
167
|
+
.selectra-loading {
|
|
168
|
+
@apply px-3 py-3 text-center text-gray-400 text-sm
|
|
169
|
+
flex items-center justify-center gap-2;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.selectra-spinner {
|
|
173
|
+
@apply w-4 h-4 border-2 border-gray-300
|
|
174
|
+
border-t-blue-500 rounded-full animate-spin;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/* ── Optgroup ────────────────────────────────────────── */
|
|
178
|
+
.selectra-optgroup-header {
|
|
179
|
+
@apply px-3 py-1.5 text-xs font-semibold
|
|
180
|
+
text-gray-500 uppercase tracking-wider
|
|
181
|
+
bg-gray-50 border-b border-gray-100;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.selectra-optgroup + .selectra-optgroup {
|
|
185
|
+
@apply border-t border-gray-100;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/* ── Dropdown Header (plugin) ────────────────────────── */
|
|
189
|
+
.selectra-dropdown-header {
|
|
190
|
+
@apply flex items-center justify-between
|
|
191
|
+
px-3 py-2 bg-gray-50
|
|
192
|
+
border-b border-gray-200
|
|
193
|
+
text-sm font-medium text-gray-700;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.selectra-dropdown-header .close-btn {
|
|
197
|
+
@apply w-5 h-5 flex items-center justify-center
|
|
198
|
+
rounded text-gray-400 hover:text-gray-600
|
|
199
|
+
hover:bg-gray-200 cursor-pointer transition-colors;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/* ── Clear Button (plugin) ───────────────────────────── */
|
|
203
|
+
.selectra-clear-button {
|
|
204
|
+
@apply absolute right-7 top-1/2 -translate-y-1/2
|
|
205
|
+
w-5 h-5 flex items-center justify-center
|
|
206
|
+
rounded-full text-gray-400 hover:text-gray-600
|
|
207
|
+
hover:bg-gray-100 cursor-pointer transition-colors
|
|
208
|
+
z-10;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.selectra-input.is-single .selectra-clear-button {
|
|
212
|
+
@apply right-8;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/* ── Tag Limit Badge (plugin) ────────────────────────── */
|
|
216
|
+
.selectra-tag-limit-badge {
|
|
217
|
+
@apply inline-flex items-center justify-center
|
|
218
|
+
px-2 py-0.5 rounded-full
|
|
219
|
+
bg-gray-100 text-gray-500
|
|
220
|
+
text-xs font-medium;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/* ── Highlight ───────────────────────────────────────── */
|
|
224
|
+
.highlight {
|
|
225
|
+
@apply font-semibold text-inherit;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/* ── Animations ──────────────────────────────────────────── */
|
|
230
|
+
@layer utilities {
|
|
231
|
+
@keyframes fade-in {
|
|
232
|
+
from {
|
|
233
|
+
opacity: 0;
|
|
234
|
+
transform: scale(0.95);
|
|
235
|
+
}
|
|
236
|
+
to {
|
|
237
|
+
opacity: 1;
|
|
238
|
+
transform: scale(1);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
@keyframes dropdown-in {
|
|
243
|
+
from {
|
|
244
|
+
opacity: 0;
|
|
245
|
+
transform: translateY(-4px);
|
|
246
|
+
}
|
|
247
|
+
to {
|
|
248
|
+
opacity: 1;
|
|
249
|
+
transform: translateY(0);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.animate-fade-in {
|
|
254
|
+
animation: fade-in 0.15s ease-out;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.animate-dropdown-in {
|
|
258
|
+
animation: dropdown-in 0.15s ease-out;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/* ── Scrollbar Styling ───────────────────────────────────── */
|
|
263
|
+
.selectra-dropdown-content::-webkit-scrollbar {
|
|
264
|
+
width: 6px;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.selectra-dropdown-content::-webkit-scrollbar-track {
|
|
268
|
+
background: transparent;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
.selectra-dropdown-content::-webkit-scrollbar-thumb {
|
|
272
|
+
@apply bg-gray-200 rounded-full;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.selectra-dropdown-content::-webkit-scrollbar-thumb:hover {
|
|
276
|
+
@apply bg-gray-300;
|
|
277
|
+
}
|