@studiocms/ui 0.0.1 → 0.3.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.
Files changed (37) hide show
  1. package/README.md +28 -544
  2. package/package.json +11 -6
  3. package/src/components/Button.astro +303 -269
  4. package/src/components/Card.astro +37 -13
  5. package/src/components/Center.astro +2 -2
  6. package/src/components/Checkbox.astro +99 -29
  7. package/src/components/Divider.astro +15 -8
  8. package/src/components/Dropdown/Dropdown.astro +102 -41
  9. package/src/components/Dropdown/dropdown.ts +111 -23
  10. package/src/components/Footer.astro +137 -0
  11. package/src/components/Input.astro +42 -14
  12. package/src/components/Modal/Modal.astro +84 -30
  13. package/src/components/Modal/modal.ts +43 -9
  14. package/src/components/RadioGroup.astro +153 -29
  15. package/src/components/Row.astro +16 -7
  16. package/src/components/SearchSelect.astro +278 -222
  17. package/src/components/Select.astro +260 -127
  18. package/src/components/Sidebar/Double.astro +12 -12
  19. package/src/components/Sidebar/Single.astro +6 -6
  20. package/src/components/Sidebar/helpers.ts +53 -7
  21. package/src/components/Tabs/TabItem.astro +47 -0
  22. package/src/components/Tabs/Tabs.astro +376 -0
  23. package/src/components/Tabs/index.ts +2 -0
  24. package/src/components/Textarea.astro +56 -15
  25. package/src/components/ThemeToggle.astro +14 -8
  26. package/src/components/Toast/Toaster.astro +171 -31
  27. package/src/components/Toggle.astro +89 -21
  28. package/src/components/User.astro +34 -15
  29. package/src/components/index.ts +24 -22
  30. package/src/components.ts +2 -0
  31. package/src/css/colors.css +65 -65
  32. package/src/css/resets.css +0 -1
  33. package/src/integration.ts +18 -0
  34. package/src/layouts/RootLayout.astro +1 -2
  35. package/src/types/index.ts +1 -1
  36. package/src/utils/ThemeHelper.ts +135 -117
  37. package/src/utils/create-resolver.ts +30 -0
@@ -2,22 +2,65 @@
2
2
  import Icon from '../utils/Icon.astro';
3
3
  import { generateID } from '../utils/generateID';
4
4
 
5
+ /**
6
+ * The props for the select component.
7
+ */
5
8
  interface Option {
9
+ /**
10
+ * The label of the option.
11
+ */
6
12
  label: string;
13
+ /**
14
+ * The value of the option.
15
+ */
7
16
  value: string;
17
+ /**
18
+ * Whether the option is disabled. Defaults to `false`.
19
+ */
8
20
  disabled?: boolean;
9
- };
21
+ }
10
22
 
23
+ /**
24
+ * The props for the select component.
25
+ */
11
26
  interface Props {
27
+ /**
28
+ * The label of the select.
29
+ */
12
30
  label?: string;
31
+ /**
32
+ * The default value of the select.
33
+ */
13
34
  defaultValue?: string;
35
+ /**
36
+ * Additional classes to apply to the select.
37
+ */
14
38
  class?: string;
39
+ /**
40
+ * The name of the select. Required because of the helper.
41
+ */
15
42
  name?: string;
43
+ /**
44
+ * Whether the select is required. Defaults to `false`.
45
+ */
16
46
  isRequired?: boolean;
47
+ /**
48
+ * The options to display in the select.
49
+ */
17
50
  options: Option[];
51
+ /**
52
+ * Whether the select is disabled. Defaults to `false`.
53
+ */
18
54
  disabled?: boolean;
55
+ /**
56
+ * Whether the select is full width. Defaults to `false`.
57
+ */
19
58
  fullWidth?: boolean;
20
- };
59
+ /**
60
+ * The placeholder of the select. Defaults to `Select`.
61
+ */
62
+ placeholder?: string;
63
+ }
21
64
 
22
65
  const {
23
66
  label,
@@ -28,38 +71,47 @@ const {
28
71
  options = [],
29
72
  disabled,
30
73
  fullWidth,
74
+ placeholder = 'Select',
31
75
  } = Astro.props;
32
76
  ---
33
77
 
34
78
  <div
35
79
  id={`${name}-container`}
36
- class="select-label"
80
+ class="sui-select-label"
37
81
  class:list={[disabled && "disabled", className, fullWidth && "full"]}
82
+ data-options={JSON.stringify(options)}
83
+ data-id={name}
38
84
  >
39
- <label class="label" for={`${name}-select-btn`}>
40
- {label}
41
- <span class="req-star">{isRequired && "*"}</span>
42
- </label>
85
+ {label && (
86
+ <label class="label" for={`${name}-select-btn`}>
87
+ {label}
88
+ <span class="req-star">{isRequired && "*"}</span>
89
+ </label>
90
+ )}
43
91
  <button
44
- class="select-button"
92
+ class="sui-select-button"
45
93
  role="combobox"
94
+ aria-controls={`${name}-dropdown`}
95
+ aria-expanded="false"
46
96
  id={`${name}-select-btn`}
47
97
  type="button"
98
+ aria-label={placeholder}
99
+ title={placeholder}
48
100
  >
49
- <span id={`${name}-value-span`}>
101
+ <span class="sui-select-value-span" id={`${name}-value-span`}>
50
102
  {
51
103
  defaultValue
52
104
  ? options.find((x) => x.value === defaultValue)?.label
53
- : "Select"
105
+ : placeholder
54
106
  }
55
107
  </span>
56
108
  <Icon name="chevron-up-down" width={24} height={24} />
57
109
  </button>
58
- <ul class="select-dropdown" role="listbox" id={`${name}-dropdown`}>
110
+ <ul class="sui-select-dropdown" role="listbox" id={`${name}-dropdown`}>
59
111
  {
60
112
  options.map((x, i) => (
61
113
  <li
62
- class="select-option"
114
+ class="sui-select-option"
63
115
  role="option"
64
116
  value={x.value}
65
117
  class:list={[
@@ -68,14 +120,13 @@ const {
68
120
  ]}
69
121
  id={defaultValue === x.value ? `${name}-selected` : ""}
70
122
  data-option-index={i}
71
- tabindex={x.disabled ? -1 : 0}
72
123
  >
73
124
  {x.label}
74
125
  </li>
75
126
  ))
76
127
  }
77
128
  </ul>
78
- <select class="hidden-select" id={name} name={name} required={isRequired}>
129
+ <select class="sui-hidden-select" id={name} name={name} required={isRequired} hidden tabindex="-1">
79
130
  <option value={""}> Select </option>
80
131
  {
81
132
  options.map((x) => (
@@ -90,143 +141,221 @@ const {
90
141
  }
91
142
  </select>
92
143
  </div>
93
- <script is:inline define:vars={{ id: name, options }}>
94
- const container = document.getElementById(`${id}-container`);
95
- const hiddenSelect = document.getElementById(id);
96
- const button = document.getElementById(`${id}-select-btn`);
97
- const valueSpan = document.getElementById(`${id}-value-span`);
98
- const dropdown = document.getElementById(`${id}-dropdown`);
99
- const optionElements = container.querySelectorAll("li");
100
-
101
- let active = false;
102
-
103
- button.addEventListener("click", () => {
104
- const { bottom, left, right, width, x, y, height } =
105
- button.getBoundingClientRect();
106
-
107
- const optionHeight = 36;
108
- const totalBorderSize = 2;
109
- const margin = 4;
110
-
111
- const dropdownHeight =
112
- options.length * optionHeight + totalBorderSize + margin;
113
-
114
- const CustomRect = {
115
- top: bottom + margin,
116
- left,
117
- right,
118
- bottom: bottom + margin + dropdownHeight,
119
- width,
120
- height: dropdownHeight,
121
- x,
122
- y: y + height + margin,
123
- };
124
-
125
- if (active) {
144
+ <script>
145
+ const allSelects = document.querySelectorAll<HTMLDivElement>(".sui-select-label");
146
+ // id, options
147
+
148
+ for (const container of allSelects) {
149
+ const hiddenSelect = container.querySelector('select')!;
150
+ const button = container.querySelector('button')!;
151
+ const valueSpan = container.querySelector('.sui-select-value-span')!;
152
+ const dropdown = container.querySelector('.sui-select-dropdown')!;
153
+ const optionElements = container.querySelectorAll<HTMLLIElement>('.sui-select-option');
154
+
155
+ const options = JSON.parse(container.dataset.options!);
156
+ const id = container.dataset.id!;
157
+ let active = false;
158
+
159
+ const closeDropdown = () => {
126
160
  dropdown.classList.remove("active", "above");
127
161
  active = false;
128
- return;
162
+ button.ariaExpanded = 'false';
163
+ focusIndex = -1;
164
+
165
+ for (const entry of optionElements) {
166
+ entry.classList.remove('focused');
167
+ }
168
+ };
169
+
170
+ const openDropdown = (toggle: boolean) => {
171
+ const { bottom, left, right, width, x, y, height } =
172
+ button.getBoundingClientRect();
173
+
174
+ const optionHeight = 36;
175
+ const totalBorderSize = 2;
176
+ const margin = 4;
177
+
178
+ const dropdownHeight =
179
+ options.length * optionHeight + totalBorderSize + margin;
180
+
181
+ const CustomRect = {
182
+ top: bottom + margin,
183
+ left,
184
+ right,
185
+ bottom: bottom + margin + dropdownHeight,
186
+ width,
187
+ height: dropdownHeight,
188
+ x,
189
+ y: y + height + margin,
190
+ };
191
+
192
+ if (active && toggle) {
193
+ closeDropdown();
194
+ return;
195
+ }
196
+
197
+ active = true;
198
+ button.ariaExpanded = 'true';
199
+
200
+ // Set focusIndex to currently selected option
201
+ focusIndex = Array.from(optionElements).findIndex((x) =>
202
+ x.classList.contains("selected")
203
+ );
204
+
205
+ if (
206
+ CustomRect.top >= 0 &&
207
+ CustomRect.left >= 0 &&
208
+ CustomRect.bottom <=
209
+ (window.innerHeight || document.documentElement.clientHeight) &&
210
+ CustomRect.right <=
211
+ (window.innerWidth || document.documentElement.clientWidth)
212
+ ) {
213
+ dropdown.classList.add("active");
214
+ } else {
215
+ dropdown.classList.add("active", "above");
216
+ }
129
217
  }
130
218
 
131
- active = true;
132
-
133
- if (
134
- CustomRect.top >= 0 &&
135
- CustomRect.left >= 0 &&
136
- CustomRect.bottom <=
137
- (window.innerHeight || document.documentElement.clientHeight) &&
138
- CustomRect.right <=
139
- (window.innerWidth || document.documentElement.clientWidth)
140
- ) {
141
- dropdown.classList.add("active");
142
- } else {
143
- dropdown.classList.add("active", "above");
219
+ button.addEventListener("click", () => openDropdown(true));
220
+
221
+ let focusIndex = -1;
222
+
223
+ const recomputeOptions = () => {
224
+ for (const entry of optionElements) {
225
+ if (Number.parseInt(entry.dataset.optionIndex!) == focusIndex) {
226
+ entry.classList.add('focused');
227
+ } else {
228
+ entry.classList.remove('focused');
229
+ }
230
+ }
144
231
  }
145
- });
146
232
 
147
- optionElements.forEach((option) => {
148
- const handleSelection = (e) => {
233
+ button.addEventListener('keydown', (e) => {
234
+ if (e.key === 'Tab' || e.key === 'Escape') {
235
+ closeDropdown();
236
+ return;
237
+ }
238
+
239
+ if (e.key === ' ' && !active) openDropdown(false);
240
+
241
+ if (e.key === 'Enter') {
242
+ let currentlyFocused = container.querySelector<HTMLElement>('.focused');
243
+ if (currentlyFocused) {
244
+ currentlyFocused.classList.remove('focused');
245
+ currentlyFocused.click();
246
+
247
+ // Stop dropdown from immediately reopening
248
+ e.preventDefault();
249
+ e.stopImmediatePropagation();
250
+ }
251
+
252
+ return;
253
+ }
254
+
255
+ e.preventDefault();
149
256
  e.stopImmediatePropagation();
150
- if (option.id === `${id}-selected` || !id) return;
151
257
 
152
- const currentlySelected = document.getElementById(`${id}-selected`);
258
+ const neighbor = (offset: number) => {
259
+ return optionElements.item((Array.from(optionElements).findIndex((x) => x.classList.contains("selected")) ?? -1) + offset)
260
+ }
261
+
262
+ if (e.key === "ArrowUp" && (focusIndex > 0 || !active)) {
263
+ if (!active) return neighbor(-1)?.click();
264
+ focusIndex--;
265
+ recomputeOptions();
266
+ }
153
267
 
268
+ if (e.key === "ArrowDown" && focusIndex + 1 < optionElements.length) {
269
+ if (!active) return neighbor(1)?.click();
270
+ focusIndex++;
271
+ recomputeOptions();
272
+ }
273
+
274
+ if (e.key === 'PageUp') {
275
+ focusIndex = 0;
276
+ if (!active) return optionElements.item(focusIndex)?.click();
277
+ recomputeOptions();
278
+ }
279
+ if (e.key === 'PageDown') {
280
+ focusIndex = optionElements.length - 1;
281
+ if (!active) return optionElements.item(focusIndex)?.click();
282
+ recomputeOptions();
283
+ }
284
+ });
285
+
286
+ const handleSelection = (e: MouseEvent, option: HTMLElement) => {
287
+ e.stopImmediatePropagation();
288
+ if (option.id === `${id}-selected` || !id) return;
289
+ const currentlySelected = document.getElementById(`${id}-selected`);
154
290
  if (currentlySelected) {
155
291
  currentlySelected.classList.remove("selected");
156
292
  currentlySelected.id = "";
157
293
  }
158
-
159
294
  option.id = `${id}-selected`;
160
295
  option.classList.add("selected");
161
-
162
- const opt = options[parseInt(option.dataset.optionIndex)];
296
+ const opt = options[parseInt(option.dataset.optionIndex!)];
163
297
  hiddenSelect.value = opt.value;
164
-
165
298
  valueSpan.textContent = opt.label;
166
- dropdown.classList.remove("active", "above");
167
-
168
- active = false;
299
+ closeDropdown();
169
300
  }
170
301
 
171
- option.addEventListener("click", handleSelection);
172
- option.addEventListener("keydown", (e) => {
173
- if (e.key === 'Enter') {
174
- handleSelection(e);
175
- }
176
- });
177
- });
302
+ optionElements.forEach((option) => {
303
+ const handleSelectionForOption = (e: MouseEvent) => handleSelection(e, option)
178
304
 
179
- window.addEventListener("scroll", () => {
180
- dropdown.classList.remove("active", "above");
181
- active = false;
182
- });
305
+ option.addEventListener("click", handleSelectionForOption);
306
+ });
183
307
 
184
- hideOnClickOutside(container);
308
+ window.addEventListener("scroll", closeDropdown);
185
309
 
186
- function hideOnClickOutside(element) {
187
- const outsideClickListener = (event) => {
188
- if (
189
- !element.contains(event.target) &&
190
- isVisible(element) &&
191
- active === true
192
- ) {
193
- // or use: event.target.closest(selector) === null
194
- dropdown.classList.remove("active", "above");
195
- active = false;
310
+ document.addEventListener("keydown", (e) => {
311
+ if (e.key === "Escape" && dropdown.classList.contains("active")) {
312
+ closeDropdown();
196
313
  }
197
- };
314
+ });
198
315
 
199
- const removeClickListener = () => {
200
- document.removeEventListener("click", outsideClickListener);
201
- };
316
+ hideOnClickOutside(container);
317
+
318
+ function hideOnClickOutside(element: HTMLElement) {
319
+ const outsideClickListener = (event: MouseEvent) => {
320
+ if (
321
+ !element.contains(event.target as HTMLElement) &&
322
+ isVisible(element) &&
323
+ active === true
324
+ ) {
325
+ // or use: event.target.closest(selector) === null
326
+ closeDropdown();
327
+ }
328
+ };
329
+
330
+ document.addEventListener("click", outsideClickListener);
331
+ }
202
332
 
203
- document.addEventListener("click", outsideClickListener);
333
+ // source (2018-03-11): https://github.com/jquery/jquery/blob/master/src/css/hiddenVisibleSelectors.js
334
+ const isVisible = (elem: HTMLElement) =>
335
+ !!elem &&
336
+ !!(
337
+ elem.offsetWidth ||
338
+ elem.offsetHeight ||
339
+ elem.getClientRects().length
340
+ );
204
341
  }
205
-
206
- // source (2018-03-11): https://github.com/jquery/jquery/blob/master/src/css/hiddenVisibleSelectors.js
207
- const isVisible = (elem) =>
208
- !!elem &&
209
- !!(
210
- elem.offsetWidth ||
211
- elem.offsetHeight ||
212
- elem.getClientRects().length
213
- );
214
342
  </script>
215
343
  <style>
216
- .select-label {
344
+ .sui-select-label {
217
345
  width: fit-content;
218
346
  display: flex;
219
347
  flex-direction: column;
220
348
  gap: 0.25rem;
221
349
  min-width: 200px;
222
350
  position: relative;
351
+ height: fit-content;
223
352
  }
224
353
 
225
- .select-label.full {
354
+ .sui-select-label.full, .sui-select-label.full .sui-select-button {
226
355
  width: 100%;
227
356
  }
228
357
 
229
- .select-label.disabled {
358
+ .sui-select-label.disabled {
230
359
  opacity: 0.5;
231
360
  pointer-events: none;
232
361
  color: hsl(var(--text-muted));
@@ -241,13 +370,13 @@ const {
241
370
  font-weight: 700;
242
371
  }
243
372
 
244
- .select-button {
373
+ .sui-select-button {
245
374
  padding: 0.5rem 0.75rem 0.5rem 1rem;
246
375
  border-radius: 8px;
247
376
  border: 1px solid hsl(var(--border));
248
377
  background: hsl(var(--background-step-2));
249
378
  color: hsl(var(--text-normal));
250
- transition: all 0.15s ease;
379
+ transition: background border 0.15s ease;
251
380
  display: flex;
252
381
  flex-direction: row;
253
382
  align-items: center;
@@ -256,18 +385,22 @@ const {
256
385
  gap: 1rem;
257
386
  }
258
387
 
259
- .select-button:hover {
388
+ .sui-select-button:focus {
389
+ border: 1px solid hsl(var(--primary-base));
390
+ }
391
+
392
+ .sui-select-button:hover, .sui-select-button:focus {
260
393
  background: hsl(var(--background-step-3));
261
394
  }
262
395
 
263
- .select-button.active,
264
- .select-button:active,
265
- .select-button:has(+ .select-dropdown.active) {
396
+ .sui-select-button.active,
397
+ .sui-select-button:active,
398
+ .sui-select-button:has(+ .sui-select-dropdown.active) {
266
399
  border: 1px solid hsl(var(--primary-base));
267
400
  background: hsl(var(--background-step-2));
268
401
  }
269
402
 
270
- .select-dropdown {
403
+ .sui-select-dropdown {
271
404
  position: absolute;
272
405
  width: 100%;
273
406
  border: 1px solid hsl(var(--border));
@@ -285,43 +418,43 @@ const {
285
418
  box-shadow: 0px 4px 8px hsl(var(--shadow), 0.5);
286
419
  }
287
420
 
288
- .select-dropdown.active {
421
+ .sui-select-dropdown.active {
289
422
  display: flex;
290
423
  }
291
424
 
292
- .select-dropdown.above {
425
+ .sui-select-dropdown.above {
293
426
  top: auto;
294
427
  bottom: calc(100% - 18px + 0.25rem);
295
428
  }
296
429
 
297
- .select-option {
430
+ .sui-select-option {
298
431
  padding: 0.5rem;
299
432
  cursor: pointer;
300
433
  font-size: 0.975em;
301
434
  transition: all 0.15s ease;
302
435
  }
303
436
 
304
- .select-option.disabled {
437
+ .sui-select-option.disabled {
305
438
  pointer-events: none;
306
439
  color: hsl(var(--text-muted));
307
440
  }
308
441
 
309
- .select-option:hover, .select-option:focus {
442
+ .sui-select-option:hover, .sui-select-option:focus, .sui-select-option.focused {
310
443
  background-color: hsl(var(--background-step-3));
311
444
  }
312
445
 
313
- .select-option:focus {
446
+ .sui-select-option:focus {
314
447
  outline: none;
315
448
  border: none;
316
449
  }
317
450
 
318
- .select-option.selected {
451
+ .sui-select-option.selected {
319
452
  background-color: hsl(var(--primary-base));
320
453
  color: hsl(var(--text-inverted));
321
454
  cursor: default;
322
455
  }
323
456
 
324
- .hidden-select {
457
+ .sui-hidden-select {
325
458
  height: 0;
326
459
  width: 0;
327
460
  border: none;
@@ -1,13 +1,13 @@
1
- <div id="sidebars" class="active inner">
2
- <div id="sidebar-outer">
1
+ <div id="sui-sidebars" class="active inner">
2
+ <div id="sui-sidebar-outer">
3
3
  <slot name="outer" />
4
4
  </div>
5
- <div id="sidebar-inner">
5
+ <div id="sui-sidebar-inner">
6
6
  <slot name="inner" />
7
7
  </div>
8
8
  </div>
9
9
  <style>
10
- #sidebars {
10
+ #sui-sidebars {
11
11
  --sidebars-container-width: calc((280px + 1px) * 2);
12
12
  display: flex;
13
13
  align-items: center;
@@ -19,11 +19,11 @@
19
19
  height: 100%;
20
20
  }
21
21
 
22
- #sidebars.active {
22
+ #sui-sidebars.active {
23
23
  transform: translateX(0%);
24
24
  }
25
25
 
26
- #sidebar-outer {
26
+ #sui-sidebar-outer {
27
27
  height: 100%;
28
28
  min-width: 280px;
29
29
  width: 280px;
@@ -39,7 +39,7 @@
39
39
  padding: 1.5rem;
40
40
  }
41
41
 
42
- #sidebar-inner {
42
+ #sui-sidebar-inner {
43
43
  min-width: 280px;
44
44
  width: 280px;
45
45
  height: 100%;
@@ -56,7 +56,7 @@
56
56
  }
57
57
 
58
58
  @media screen and (max-width: 1200px) {
59
- #sidebars {
59
+ #sui-sidebars {
60
60
  --sidebars-container-width: calc(280px + 1px);
61
61
  }
62
62
 
@@ -64,7 +64,7 @@
64
64
  display: block;
65
65
  }
66
66
 
67
- #sidebars.inner {
67
+ #sui-sidebars.inner {
68
68
  #sidebar-outer,
69
69
  #sidebar-inner {
70
70
  transform: translateX(-100%);
@@ -73,7 +73,7 @@
73
73
  }
74
74
 
75
75
  @media screen and (max-width: 840px) {
76
- #sidebars {
76
+ #sui-sidebars {
77
77
  transform: translateX(-100%);
78
78
  position: absolute;
79
79
  top: 0;
@@ -82,8 +82,8 @@
82
82
  width: 100%;
83
83
  }
84
84
 
85
- #sidebar-outer,
86
- #sidebar-inner {
85
+ #sui-sidebar-outer,
86
+ #sui-sidebar-inner {
87
87
  width: 100%;
88
88
  flex: 0 0 100%;
89
89
  }
@@ -1,15 +1,15 @@
1
1
  ---
2
- import type { HTMLAttributes } from 'astro/types'
2
+ import type { HTMLAttributes } from 'astro/types';
3
3
 
4
- interface Props extends Exclude<HTMLAttributes<'aside'>, 'id'> {};
4
+ interface Props extends Exclude<HTMLAttributes<'aside'>, 'id'> {}
5
5
 
6
6
  const props = Astro.props;
7
7
  ---
8
- <aside id="sidebar" {...props}>
8
+ <aside id="sui-sidebar" {...props}>
9
9
  <slot />
10
10
  </aside>
11
11
  <style>
12
- #sidebar {
12
+ #sui-sidebar {
13
13
  height: 100%;
14
14
  min-width: 280px;
15
15
  width: 280px;
@@ -24,12 +24,12 @@ const props = Astro.props;
24
24
  transition: all .3s ease;
25
25
  }
26
26
 
27
- #sidebar.active {
27
+ #sui-sidebar.active {
28
28
  transform: translateX(0%);
29
29
  }
30
30
 
31
31
  @media screen and (max-width: 840px) {
32
- #sidebar {
32
+ #sui-sidebar {
33
33
  transform: translateX(-100%);
34
34
  position: absolute;
35
35
  top: 0;