@studiocms/ui 0.1.0 → 0.3.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 +6 -4
- package/src/components/Button.astro +302 -269
- package/src/components/Card.astro +27 -3
- package/src/components/Checkbox.astro +72 -3
- package/src/components/Divider.astro +8 -1
- package/src/components/Dropdown/Dropdown.astro +55 -2
- package/src/components/Dropdown/dropdown.ts +104 -17
- package/src/components/Footer.astro +21 -4
- package/src/components/Input.astro +27 -0
- package/src/components/Modal/Modal.astro +31 -1
- package/src/components/Modal/modal.ts +33 -0
- package/src/components/RadioGroup.astro +132 -8
- package/src/components/Row.astro +9 -0
- package/src/components/SearchSelect.astro +249 -197
- package/src/components/Select.astro +229 -105
- package/src/components/Sidebar/helpers.ts +46 -0
- package/src/components/Tabs/TabItem.astro +47 -0
- package/src/components/Tabs/Tabs.astro +376 -0
- package/src/components/Tabs/index.ts +2 -0
- package/src/components/Textarea.astro +30 -0
- package/src/components/ThemeToggle.astro +6 -3
- package/src/components/Toast/Toaster.astro +140 -1
- package/src/components/Toggle.astro +77 -9
- package/src/components/User.astro +20 -2
- package/src/components/index.ts +1 -0
- package/src/components.ts +1 -0
- package/src/css/colors.css +8 -8
- package/src/css/resets.css +0 -1
- package/src/integration.ts +31 -0
- package/src/layouts/RootLayout.astro +0 -1
- package/src/utils/ThemeHelper.ts +8 -1
- package/src/utils/create-resolver.ts +30 -0
- package/src/utils/virtual-module-plugin-builder.ts +37 -0
|
@@ -3,21 +3,63 @@ import Icon from '../utils/Icon.astro';
|
|
|
3
3
|
import { generateID } from '../utils/generateID';
|
|
4
4
|
import Input from './Input.astro';
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* An option in the select dropdown
|
|
8
|
+
*/
|
|
6
9
|
interface Option {
|
|
10
|
+
/**
|
|
11
|
+
* The label of the option.
|
|
12
|
+
*/
|
|
7
13
|
label: string;
|
|
14
|
+
/**
|
|
15
|
+
* The value of the option.
|
|
16
|
+
*/
|
|
8
17
|
value: string;
|
|
18
|
+
/**
|
|
19
|
+
* Whether the option is disabled.
|
|
20
|
+
*/
|
|
9
21
|
disabled?: boolean;
|
|
10
22
|
}
|
|
11
23
|
|
|
24
|
+
/**
|
|
25
|
+
* The props for the search select component.
|
|
26
|
+
*/
|
|
12
27
|
interface Props {
|
|
28
|
+
/**
|
|
29
|
+
* The label of the search select.
|
|
30
|
+
*/
|
|
13
31
|
label?: string;
|
|
32
|
+
/**
|
|
33
|
+
* The default value of the search select. Needs to be one of the values in the options.
|
|
34
|
+
*/
|
|
14
35
|
defaultValue?: string;
|
|
36
|
+
/**
|
|
37
|
+
* Additional classes to apply to the search select.
|
|
38
|
+
*/
|
|
15
39
|
class?: string;
|
|
40
|
+
/**
|
|
41
|
+
* The name of the search select.
|
|
42
|
+
*/
|
|
16
43
|
name?: string;
|
|
44
|
+
/**
|
|
45
|
+
* Whether the search select is required. Defaults to `false`.
|
|
46
|
+
*/
|
|
17
47
|
isRequired?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* The options to display in the search select.
|
|
50
|
+
*/
|
|
18
51
|
options: Option[];
|
|
52
|
+
/**
|
|
53
|
+
* Whether the search select is disabled. Defaults to `false`.
|
|
54
|
+
*/
|
|
19
55
|
disabled?: boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Whether the search select should take up the full width of its container.
|
|
58
|
+
*/
|
|
20
59
|
fullWidth?: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* The placeholder of the search select.
|
|
62
|
+
*/
|
|
21
63
|
placeholder?: string;
|
|
22
64
|
}
|
|
23
65
|
|
|
@@ -38,6 +80,8 @@ const {
|
|
|
38
80
|
id={`${name}-container`}
|
|
39
81
|
class="sui-search-select-label"
|
|
40
82
|
class:list={[disabled && "disabled", className, fullWidth && "full"]}
|
|
83
|
+
data-options={JSON.stringify(options)}
|
|
84
|
+
data-id={name}
|
|
41
85
|
>
|
|
42
86
|
<div class="sui-search-input-wrapper" id={`${name}-search-input-wrapper`}>
|
|
43
87
|
<Input
|
|
@@ -70,7 +114,7 @@ const {
|
|
|
70
114
|
))
|
|
71
115
|
}
|
|
72
116
|
</ul>
|
|
73
|
-
<select class="sui-hidden-select" id={name} name={name} required={isRequired}>
|
|
117
|
+
<select class="sui-hidden-select" id={name} name={name} required={isRequired} hidden tabindex="-1">
|
|
74
118
|
<option value={""}> Select </option>
|
|
75
119
|
{
|
|
76
120
|
options.map((x) => (
|
|
@@ -85,245 +129,249 @@ const {
|
|
|
85
129
|
}
|
|
86
130
|
</select>
|
|
87
131
|
</div>
|
|
88
|
-
<script
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
132
|
+
<script>
|
|
133
|
+
interface Option {
|
|
134
|
+
label: string;
|
|
135
|
+
value: string;
|
|
136
|
+
disabled?: boolean;
|
|
137
|
+
}
|
|
138
|
+
// id: name, options, defaultValue
|
|
139
|
+
const searchSelects = document.querySelectorAll<HTMLDivElement>('.sui-search-select-label');
|
|
140
|
+
|
|
141
|
+
for (const container of searchSelects) {
|
|
142
|
+
const hiddenSelect = container.querySelector<HTMLSelectElement>('select')!;
|
|
143
|
+
const searchWrapper = container.querySelector<HTMLDivElement>(`.sui-search-input-wrapper`)!;
|
|
144
|
+
const searchInput = searchWrapper.querySelector('input')!;
|
|
145
|
+
const dropdown = container.querySelector(`.sui-search-select-dropdown`)!;
|
|
146
|
+
let optionElements = container.querySelectorAll("li");
|
|
147
|
+
|
|
148
|
+
let active = false;
|
|
149
|
+
|
|
150
|
+
const options = JSON.parse(container.dataset.options!) as Option[];
|
|
151
|
+
const id = container.dataset.id!;
|
|
152
|
+
let filteredOptions = options;
|
|
153
|
+
|
|
154
|
+
searchWrapper.addEventListener("click", () => {
|
|
155
|
+
const { bottom, left, right, width, x, y, height } = searchWrapper.getBoundingClientRect();
|
|
156
|
+
|
|
157
|
+
const optionHeight = 36;
|
|
158
|
+
const totalBorderSize = 2;
|
|
159
|
+
const margin = 4;
|
|
160
|
+
|
|
161
|
+
const dropdownHeight = options.length * optionHeight + totalBorderSize + margin;
|
|
162
|
+
|
|
163
|
+
const CustomRect = {
|
|
164
|
+
top: bottom + margin,
|
|
165
|
+
left,
|
|
166
|
+
right,
|
|
167
|
+
bottom: bottom + margin + dropdownHeight,
|
|
168
|
+
width,
|
|
169
|
+
height: dropdownHeight,
|
|
170
|
+
x,
|
|
171
|
+
y: y + height + margin,
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
if (active) {
|
|
175
|
+
searchInput.ariaExpanded = 'false';
|
|
176
|
+
dropdown.classList.remove("active", "above");
|
|
177
|
+
active = false;
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
127
180
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if (
|
|
132
|
-
CustomRect.top >= 0 &&
|
|
133
|
-
CustomRect.left >= 0 &&
|
|
134
|
-
CustomRect.bottom <=
|
|
135
|
-
(window.innerHeight || document.documentElement.clientHeight) &&
|
|
136
|
-
CustomRect.right <=
|
|
137
|
-
(window.innerWidth || document.documentElement.clientWidth)
|
|
138
|
-
) {
|
|
139
|
-
dropdown.classList.add("active");
|
|
140
|
-
} else {
|
|
141
|
-
dropdown.classList.add("active", "above");
|
|
142
|
-
}
|
|
143
|
-
});
|
|
181
|
+
active = true;
|
|
182
|
+
searchInput.ariaExpanded = 'true';
|
|
144
183
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
184
|
+
if (
|
|
185
|
+
CustomRect.top >= 0 &&
|
|
186
|
+
CustomRect.left >= 0 &&
|
|
187
|
+
CustomRect.bottom <=
|
|
188
|
+
(window.innerHeight || document.documentElement.clientHeight) &&
|
|
189
|
+
CustomRect.right <=
|
|
190
|
+
(window.innerWidth || document.documentElement.clientWidth)
|
|
191
|
+
) {
|
|
192
|
+
dropdown.classList.add("active");
|
|
193
|
+
} else {
|
|
194
|
+
dropdown.classList.add("active", "above");
|
|
195
|
+
}
|
|
196
|
+
});
|
|
149
197
|
|
|
150
|
-
const
|
|
198
|
+
const handleSelection = (e: MouseEvent, option: HTMLLIElement) => {
|
|
199
|
+
e.stopImmediatePropagation();
|
|
200
|
+
|
|
201
|
+
if (option.id === `${id}-selected` || !id) return;
|
|
151
202
|
|
|
152
|
-
|
|
153
|
-
currentlySelected.classList.remove("selected");
|
|
154
|
-
currentlySelected.id = "";
|
|
155
|
-
}
|
|
203
|
+
const currentlySelected = document.getElementById(`${id}-selected`);
|
|
156
204
|
|
|
157
|
-
|
|
158
|
-
|
|
205
|
+
if (currentlySelected) {
|
|
206
|
+
currentlySelected.classList.remove("selected");
|
|
207
|
+
currentlySelected.id = "";
|
|
208
|
+
}
|
|
159
209
|
|
|
160
|
-
|
|
161
|
-
|
|
210
|
+
option.id = `${id}-selected`;
|
|
211
|
+
option.classList.add("selected");
|
|
162
212
|
|
|
163
|
-
|
|
164
|
-
|
|
213
|
+
const index = options.findIndex((x) => x.value === option.dataset.value);
|
|
214
|
+
focusIndex = index;
|
|
165
215
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
searchInput.blur();
|
|
216
|
+
const opt = options[index]!;
|
|
217
|
+
hiddenSelect.value = opt.value;
|
|
169
218
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
219
|
+
searchInput.placeholder = opt.label;
|
|
220
|
+
dropdown.classList.remove("active", "above");
|
|
221
|
+
// searchInput.blur();
|
|
173
222
|
|
|
174
|
-
|
|
175
|
-
|
|
223
|
+
searchInput.value = "";
|
|
224
|
+
filteredOptions = options;
|
|
225
|
+
constructOptionsBasedOnOptions(options);
|
|
176
226
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
});
|
|
227
|
+
active = false;
|
|
228
|
+
}
|
|
180
229
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
});
|
|
230
|
+
optionElements.forEach((option) => {
|
|
231
|
+
option.addEventListener("click", (e) => handleSelection(e, option));
|
|
232
|
+
});
|
|
185
233
|
|
|
186
|
-
|
|
234
|
+
window.addEventListener("scroll", () => {
|
|
235
|
+
dropdown.classList.remove("active", "above");
|
|
236
|
+
active = false;
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
hideOnClickOutside(container);
|
|
240
|
+
|
|
241
|
+
function hideOnClickOutside(element: HTMLElement) {
|
|
242
|
+
const outsideClickListener = (event: MouseEvent) => {
|
|
243
|
+
if (
|
|
244
|
+
!element.contains(event.target! as Element) &&
|
|
245
|
+
isVisible(element) &&
|
|
246
|
+
active === true
|
|
247
|
+
) {
|
|
248
|
+
// or use: event.target.closest(selector) === null
|
|
249
|
+
dropdown.classList.remove("active", "above");
|
|
250
|
+
active = false;
|
|
251
|
+
}
|
|
252
|
+
};
|
|
187
253
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
if (
|
|
191
|
-
!element.contains(event.target) &&
|
|
192
|
-
isVisible(element) &&
|
|
193
|
-
active === true
|
|
194
|
-
) {
|
|
195
|
-
// or use: event.target.closest(selector) === null
|
|
196
|
-
dropdown.classList.remove("active", "above");
|
|
197
|
-
active = false;
|
|
198
|
-
}
|
|
199
|
-
};
|
|
254
|
+
document.addEventListener("click", outsideClickListener);
|
|
255
|
+
}
|
|
200
256
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
257
|
+
// source (2018-03-11): https://github.com/jquery/jquery/blob/master/src/css/hiddenVisibleSelectors.js
|
|
258
|
+
const isVisible = (elem: HTMLElement) =>
|
|
259
|
+
!!elem &&
|
|
260
|
+
!!(
|
|
261
|
+
elem.offsetWidth ||
|
|
262
|
+
elem.offsetHeight ||
|
|
263
|
+
elem.getClientRects().length
|
|
264
|
+
);
|
|
204
265
|
|
|
205
|
-
|
|
206
|
-
}
|
|
266
|
+
let focusIndex = 0;
|
|
207
267
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
);
|
|
216
|
-
|
|
217
|
-
let focusIndex = 0;
|
|
218
|
-
|
|
219
|
-
const recomputeOptions = () => {
|
|
220
|
-
for (const entry of optionElements) {
|
|
221
|
-
if (entry.dataset.optionIndex == focusIndex) {
|
|
222
|
-
entry.classList.add('focused');
|
|
223
|
-
} else {
|
|
224
|
-
entry.classList.remove('focused');
|
|
268
|
+
const recomputeOptions = () => {
|
|
269
|
+
for (const entry of optionElements) {
|
|
270
|
+
if (Number.parseInt(entry.dataset.optionIndex!) == focusIndex) {
|
|
271
|
+
entry.classList.add('focused');
|
|
272
|
+
} else {
|
|
273
|
+
entry.classList.remove('focused');
|
|
274
|
+
}
|
|
225
275
|
}
|
|
226
276
|
}
|
|
227
|
-
}
|
|
228
277
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
active = false;
|
|
235
|
-
dropdown.classList.remove("active", "above");
|
|
236
|
-
searchInput.blur();
|
|
237
|
-
|
|
238
|
-
return;
|
|
239
|
-
}
|
|
278
|
+
searchInput.addEventListener('keydown', (e) => {
|
|
279
|
+
if (e.key === "Escape") {
|
|
280
|
+
e.preventDefault();
|
|
281
|
+
e.stopImmediatePropagation();
|
|
240
282
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
283
|
+
active = false;
|
|
284
|
+
dropdown.classList.remove("active", "above");
|
|
285
|
+
searchInput.blur();
|
|
286
|
+
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
244
289
|
|
|
245
|
-
focusIndex
|
|
246
|
-
|
|
290
|
+
if (e.key === "ArrowUp" && focusIndex > 0) {
|
|
291
|
+
e.preventDefault();
|
|
292
|
+
e.stopImmediatePropagation();
|
|
247
293
|
|
|
248
|
-
|
|
249
|
-
|
|
294
|
+
focusIndex--;
|
|
295
|
+
recomputeOptions();
|
|
250
296
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
e.stopImmediatePropagation();
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
254
299
|
|
|
255
|
-
focusIndex
|
|
256
|
-
|
|
300
|
+
if (e.key === "ArrowDown" && focusIndex + 1 < filteredOptions.filter(x => !x.disabled).length) {
|
|
301
|
+
e.preventDefault();
|
|
302
|
+
e.stopImmediatePropagation();
|
|
257
303
|
|
|
258
|
-
|
|
259
|
-
|
|
304
|
+
focusIndex++;
|
|
305
|
+
recomputeOptions();
|
|
260
306
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
e.stopImmediatePropagation();
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
264
309
|
|
|
265
|
-
|
|
310
|
+
if (e.key === "Enter") {
|
|
311
|
+
e.preventDefault();
|
|
312
|
+
e.stopImmediatePropagation();
|
|
266
313
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
314
|
+
for (const entry of optionElements) {
|
|
315
|
+
if (Number.parseInt(entry.dataset.optionIndex!) === focusIndex) {
|
|
316
|
+
entry.click();
|
|
317
|
+
}
|
|
270
318
|
}
|
|
319
|
+
|
|
320
|
+
return;
|
|
271
321
|
}
|
|
322
|
+
});
|
|
272
323
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
});
|
|
324
|
+
searchInput.addEventListener('keyup', (e) => {
|
|
325
|
+
if (["Enter", "ArrowUp", "ArrowDown"].includes(e.key)) return;
|
|
276
326
|
|
|
277
|
-
|
|
278
|
-
|
|
327
|
+
if (searchInput.value.trim().length === 0) {
|
|
328
|
+
constructOptionsBasedOnOptions(options);
|
|
329
|
+
filteredOptions = options;
|
|
330
|
+
return;
|
|
331
|
+
};
|
|
279
332
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
filteredOptions = options;
|
|
283
|
-
return;
|
|
284
|
-
};
|
|
333
|
+
filteredOptions = options.filter(x => x.label.includes(searchInput.value));
|
|
334
|
+
focusIndex = 0;
|
|
285
335
|
|
|
286
|
-
|
|
287
|
-
|
|
336
|
+
constructOptionsBasedOnOptions(filteredOptions);
|
|
337
|
+
});
|
|
288
338
|
|
|
289
|
-
constructOptionsBasedOnOptions(
|
|
290
|
-
|
|
339
|
+
function constructOptionsBasedOnOptions(options: Option[]) {
|
|
340
|
+
dropdown.innerHTML = '';
|
|
291
341
|
|
|
292
|
-
|
|
293
|
-
|
|
342
|
+
if (options.length === 0) {
|
|
343
|
+
const element = document.createElement('li');
|
|
344
|
+
element.classList.add('empty-search-results');
|
|
345
|
+
element.textContent = "No results found.";
|
|
294
346
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
element.classList.add('empty-search-results');
|
|
298
|
-
element.textContent = "No results found.";
|
|
347
|
+
dropdown.appendChild(element);
|
|
348
|
+
}
|
|
299
349
|
|
|
300
|
-
|
|
301
|
-
}
|
|
350
|
+
let i = 0;
|
|
302
351
|
|
|
303
|
-
|
|
352
|
+
for (const option of options) {
|
|
353
|
+
const element = document.createElement('li');
|
|
354
|
+
element.classList.add(...[
|
|
355
|
+
'sui-search-select-option',
|
|
356
|
+
option.disabled && "disabled",
|
|
357
|
+
focusIndex === i && 'focused',
|
|
358
|
+
].filter((x) => typeof x === 'string'));
|
|
359
|
+
element.role = "option";
|
|
360
|
+
element.value = Number.parseInt(option.value);
|
|
361
|
+
element.id = "";
|
|
362
|
+
element.dataset.optionIndex = i.toString();
|
|
363
|
+
element.dataset.value = option.value;
|
|
364
|
+
element.textContent = option.label;
|
|
304
365
|
|
|
305
|
-
|
|
306
|
-
const element = document.createElement('li');
|
|
307
|
-
element.classList.add(...[
|
|
308
|
-
'sui-search-select-option',
|
|
309
|
-
option.disabled && "disabled",
|
|
310
|
-
focusIndex === i && 'focused',
|
|
311
|
-
].filter(Boolean));
|
|
312
|
-
element.role = "option";
|
|
313
|
-
element.value = option.value;
|
|
314
|
-
element.id = "";
|
|
315
|
-
element.dataset.optionIndex = i;
|
|
316
|
-
element.dataset.value = option.value;
|
|
317
|
-
element.textContent = option.label;
|
|
366
|
+
element.addEventListener("click", (e) => handleSelection(e, element));
|
|
318
367
|
|
|
319
|
-
|
|
368
|
+
dropdown.appendChild(element);
|
|
320
369
|
|
|
321
|
-
|
|
370
|
+
i++;
|
|
371
|
+
}
|
|
322
372
|
|
|
323
|
-
|
|
373
|
+
optionElements = container.querySelectorAll("li");
|
|
324
374
|
}
|
|
325
|
-
|
|
326
|
-
optionElements = container.querySelectorAll("li");
|
|
327
375
|
}
|
|
328
376
|
</script>
|
|
329
377
|
<style is:global>
|
|
@@ -418,10 +466,14 @@ const {
|
|
|
418
466
|
cursor: pointer;
|
|
419
467
|
}
|
|
420
468
|
|
|
469
|
+
.sui-search-input-wrapper input {
|
|
470
|
+
padding-right: 2.5rem;
|
|
471
|
+
}
|
|
472
|
+
|
|
421
473
|
.sui-search-select-indicator {
|
|
422
474
|
position: absolute;
|
|
423
|
-
bottom: .
|
|
424
|
-
right: .
|
|
475
|
+
bottom: .675rem;
|
|
476
|
+
right: .675rem;
|
|
425
477
|
}
|
|
426
478
|
|
|
427
479
|
.sui-search-input-wrapper:has(input:focus) + .sui-search-select-dropdown {
|