@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
@@ -1,11 +1,16 @@
1
1
  class ModalHelper {
2
- public element: HTMLDialogElement;
3
- public cancelButton: HTMLButtonElement | undefined;
4
- public confirmButton: HTMLButtonElement | undefined;
2
+ private element: HTMLDialogElement;
3
+ private cancelButton: HTMLButtonElement | undefined;
4
+ private confirmButton: HTMLButtonElement | undefined;
5
5
 
6
6
  private isForm = false;
7
7
  private modalForm: HTMLFormElement;
8
8
 
9
+ /**
10
+ * A helper to manage modals.
11
+ * @param id The ID of the modal.
12
+ * @param triggerID The ID of the element that should trigger the modal.
13
+ */
9
14
  constructor(id: string, triggerID?: string) {
10
15
  const element = document.getElementById(id) as HTMLDialogElement;
11
16
 
@@ -32,27 +37,36 @@ class ModalHelper {
32
37
  }
33
38
  }
34
39
 
40
+ /**
41
+ * A helper function which adds event listeners to the modal buttons to close the modal when clicked.
42
+ * @param id The ID of the modal.
43
+ * @param dismissable Whether the modal is dismissable.
44
+ */
35
45
  private addButtonListeners = (id: string, dismissable: boolean) => {
36
- if (dismissable || !this.element.dataset.buttons) {
46
+ if (
47
+ dismissable ||
48
+ (!this.element.dataset.hasCancelButton && !this.element.dataset.hasActionButton)
49
+ ) {
37
50
  const xMarkButton = document.getElementById(`${id}-btn-x`) as HTMLButtonElement;
38
51
  xMarkButton.addEventListener('click', this.hide);
39
52
  }
40
53
 
41
- if (!this.element.dataset.buttons) return;
54
+ if (!!this.element.dataset.hasCancelButton && !this.element.dataset.hasActionButton) return;
42
55
 
43
- const usedButtons = this.element.dataset.buttons.split(';');
44
-
45
- if (usedButtons.includes('cancel')) {
56
+ if (this.element.dataset.hasCancelButton) {
46
57
  this.cancelButton = document.getElementById(`${id}-btn-cancel`) as HTMLButtonElement;
47
58
  this.cancelButton.addEventListener('click', this.hide);
48
59
  }
49
60
 
50
- if (usedButtons.includes('confirm')) {
61
+ if (this.element.dataset.hasActionButton) {
51
62
  this.confirmButton = document.getElementById(`${id}-btn-confirm`) as HTMLButtonElement;
52
63
  this.confirmButton.addEventListener('click', this.hide);
53
64
  }
54
65
  };
55
66
 
67
+ /**
68
+ * A helper function to close the modal when the user clicks outside of it.
69
+ */
56
70
  private addDismissiveClickListener = () => {
57
71
  this.element.addEventListener('click', (e: MouseEvent) => {
58
72
  if (!e.target) return;
@@ -68,14 +82,25 @@ class ModalHelper {
68
82
  });
69
83
  };
70
84
 
85
+ /**
86
+ * A function to show the modal.
87
+ */
71
88
  public show = () => {
72
89
  this.element.showModal();
90
+ this.element.focus();
73
91
  };
74
92
 
93
+ /**
94
+ * A function to hide the modal.
95
+ */
75
96
  public hide = () => {
76
97
  this.element.close();
77
98
  };
78
99
 
100
+ /**
101
+ * A function to add another trigger to show the modal with.
102
+ * @param elementID The ID of the element that should trigger the modal when clicked.
103
+ */
79
104
  public bindTrigger = (elementID: string) => {
80
105
  const element = document.getElementById(elementID);
81
106
 
@@ -86,6 +111,10 @@ class ModalHelper {
86
111
  element.addEventListener('click', this.show);
87
112
  };
88
113
 
114
+ /**
115
+ * Registers a callback for the cancel button.
116
+ * @param func The callback function.
117
+ */
89
118
  public registerCancelCallback = (func: () => void) => {
90
119
  if (!this.cancelButton) {
91
120
  throw new Error('Unable to register cancel callback without a cancel button.');
@@ -99,6 +128,11 @@ class ModalHelper {
99
128
  });
100
129
  };
101
130
 
131
+ /**
132
+ * Registers a callback for the confirm button.
133
+ * @param func The callback function. If the modal is a form, the function will be called with
134
+ * the form data as the first argument.
135
+ */
102
136
  public registerConfirmCallback = (func: (data?: FormData | undefined) => void) => {
103
137
  if (!this.confirmButton) {
104
138
  throw new Error('Unable to register cancel callback without a confirmation button.');
@@ -2,39 +2,81 @@
2
2
  import type { StudioCMSColorway } from '../utils/colors';
3
3
  import { generateID } from '../utils/generateID';
4
4
 
5
+ /**
6
+ * The props for the RadioGroup 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.
19
+ */
8
20
  disabled?: boolean;
9
- };
21
+ }
10
22
 
23
+ /**
24
+ * The props for the RadioGroup component.
25
+ */
11
26
  interface Props {
27
+ /**
28
+ * The label of the radio group.
29
+ */
12
30
  label: string;
31
+ /**
32
+ * The color of the radio group. Defaults to `default`.
33
+ */
13
34
  color?: StudioCMSColorway;
35
+ /**
36
+ * The default value of the radio group. Needs to be one of the values in the options.
37
+ */
14
38
  defaultValue?: string;
39
+ /**
40
+ * The options to display in the radio group.
41
+ */
15
42
  options: Option[];
43
+ /**
44
+ * Whether the radio group is disabled. Defaults to `false`.
45
+ */
16
46
  disabled?: boolean;
47
+ /**
48
+ * The name of the radio group.
49
+ */
17
50
  name?: string;
51
+ /**
52
+ * Whether the radio group is required. Defaults to `false`.
53
+ */
18
54
  isRequired?: boolean;
55
+ /**
56
+ * Whether the radio group is horizontal. Defaults to `false`.
57
+ */
19
58
  horizontal?: boolean;
59
+ /**
60
+ * Additional classes to apply to the radio group.
61
+ */
20
62
  class?: string;
21
- };
63
+ }
22
64
 
23
65
  const {
24
66
  label,
25
- color,
67
+ color = 'default',
26
68
  defaultValue,
27
69
  options,
28
- disabled,
70
+ disabled = false,
71
+ isRequired = false,
72
+ horizontal = false,
29
73
  name = generateID('radio'),
30
- isRequired,
31
- horizontal,
32
74
  class: className,
33
75
  } = Astro.props;
34
76
  ---
35
77
 
36
78
  <div
37
- class="radio-container"
79
+ class="sui-radio-container"
38
80
  class:list={[
39
81
  disabled && "disabled",
40
82
  horizontal && "horizontal",
@@ -45,18 +87,24 @@ const {
45
87
  <span>
46
88
  {label} <span class="req-star">{isRequired && "*"}</span>
47
89
  </span>
48
- <div class="radio-inputs">
49
- {options.map(({ label, value, disabled: individuallyDisabled }) => (
90
+ <div class="sui-radio-inputs" role="radiogroup">
91
+ {options.map(({ label, value, disabled: individuallyDisabled }, i) => (
50
92
  <label
51
93
  for={value}
52
- class="radio-label"
94
+ class="sui-radio-label"
53
95
  class:list={[ individuallyDisabled && "disabled" ]}
54
96
  >
55
- <div class="radio-box-container">
56
- <div class="radio-box" />
97
+ <div class="sui-radio-box-container">
98
+ <div
99
+ class="sui-radio-box"
100
+ role="radio"
101
+ tabindex={i === 0 ? 0 : -1}
102
+ aria-checked={value === defaultValue}
103
+ aria-label={label}
104
+ />
57
105
  </div>
58
106
  <input
59
- class="radio-toggle"
107
+ class="sui-radio-toggle"
60
108
  type="radio"
61
109
  value={value}
62
110
  id={value}
@@ -70,19 +118,91 @@ const {
70
118
  ))}
71
119
  </div>
72
120
  </div>
121
+ <script>
122
+ const elements = document.querySelectorAll<HTMLDivElement>('.sui-radio-container');
123
+
124
+ for (const element of elements) {
125
+ if (element.dataset.initialized) continue;
126
+
127
+ element.dataset.initialized = 'true';
128
+
129
+ const radioBoxes = element.querySelectorAll<HTMLDivElement>('.sui-radio-box');
130
+
131
+ let i = 0;
132
+
133
+ for (const radioBox of radioBoxes) {
134
+ radioBox.addEventListener('keydown', (e) => {
135
+ if (e.key === 'Enter' || e.key === " ") {
136
+ e.preventDefault();
137
+
138
+ const input = (e.target as HTMLDivElement).parentElement!.parentElement!.querySelector<HTMLInputElement>('.sui-radio-toggle')!;
139
+
140
+ if (input.disabled) return;
141
+
142
+ input.checked = true;
143
+ }
144
+
145
+ if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
146
+ e.preventDefault();
147
+
148
+ let nextRadioBox: HTMLDivElement | undefined;
149
+
150
+ radioBoxes.forEach((box, index) => {
151
+ if (box === radioBox) nextRadioBox = radioBoxes[index + 1];
152
+ });
153
+
154
+ if (!nextRadioBox) return;
155
+
156
+ radioBox.tabIndex = -1;
157
+ nextRadioBox.tabIndex = 0;
158
+ nextRadioBox.focus();
159
+ nextRadioBox.click();
160
+ }
161
+
162
+ if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
163
+ e.preventDefault();
164
+
165
+ let previousRadioBox: HTMLDivElement | undefined;
166
+
167
+ radioBoxes.forEach((box, index) => {
168
+ if (box === radioBox) previousRadioBox = radioBoxes[index - 1];
169
+ });
170
+
171
+ if (!previousRadioBox) return;
172
+
173
+ radioBox.tabIndex = -1;
174
+ previousRadioBox.tabIndex = 0;
175
+ previousRadioBox.focus();
176
+ previousRadioBox.click();
177
+ }
178
+ });
179
+
180
+ i++;
181
+ }
182
+ element.addEventListener('keydown', (e) => {
183
+ if (e.key !== 'Enter') return;
184
+
185
+ const checkbox = element.querySelector<HTMLInputElement>('.sui-checkbox');
186
+
187
+ if (!checkbox) return;
188
+
189
+ checkbox.click();
190
+ });
191
+ }
192
+ </script>
73
193
  <style>
74
- .radio-container {
194
+ .sui-radio-container {
75
195
  display: flex;
76
196
  flex-direction: column;
77
197
  gap: .5rem;
78
198
  }
79
199
 
80
- .radio-container.disabled {
200
+ .sui-radio-container.disabled {
81
201
  opacity: 0.5;
82
202
  color: hsl(var(--text-muted));
83
203
  }
84
204
 
85
- .radio-label.disabled {
205
+ .sui-radio-label.disabled {
86
206
  opacity: 0.5;
87
207
  color: hsl(var(--text-muted));
88
208
  pointer-events: none;
@@ -93,17 +213,17 @@ const {
93
213
  font-weight: 700;
94
214
  }
95
215
 
96
- .radio-inputs {
216
+ .sui-radio-inputs {
97
217
  display: flex;
98
218
  flex-direction: column;
99
219
  gap: .75rem;
100
220
  }
101
221
 
102
- .radio-container.horizontal .radio-inputs {
222
+ .sui-radio-container.horizontal .sui-radio-inputs {
103
223
  flex-direction: row;
104
224
  }
105
225
 
106
- .radio-label {
226
+ .sui-radio-label {
107
227
  display: flex;
108
228
  flex-direction: row;
109
229
  gap: .5rem;
@@ -111,41 +231,41 @@ const {
111
231
  align-items: center;
112
232
  }
113
233
 
114
- .radio-label:hover .radio-box {
234
+ .sui-radio-label:hover .sui-radio-box {
115
235
  outline-color: hsl(var(--default-hover));
116
236
  }
117
237
 
118
- .radio-container:not(.disabled) .radio-label:active .radio-box {
238
+ .sui-radio-container:not(.disabled) .sui-radio-label:active .sui-radio-box {
119
239
  outline-color: hsl(var(--default-active));
120
240
  scale: 0.9;
121
241
  }
122
242
 
123
- .radio-label:has(.radio-toggle:checked) .radio-box {
243
+ .sui-radio-label:has(.sui-radio-toggle:checked) .sui-radio-box {
124
244
  background-color: hsl(var(--text-normal));
125
245
  outline-color: hsl(var(--text-normal));
126
246
  }
127
247
 
128
- .radio-container.primary .radio-label:has(.radio-toggle:checked) .radio-box {
248
+ .sui-radio-container.primary .sui-radio-label:has(.sui-radio-toggle:checked) .sui-radio-box {
129
249
  background-color: hsl(var(--primary-base));
130
250
  outline-color: hsl(var(--primary-base));
131
251
  }
132
252
 
133
- .radio-container.success .radio-label:has(.radio-toggle:checked) .radio-box {
253
+ .sui-radio-container.success .sui-radio-label:has(.sui-radio-toggle:checked) .sui-radio-box {
134
254
  background-color: hsl(var(--success-base));
135
255
  outline-color: hsl(var(--success-base));
136
256
  }
137
257
 
138
- .radio-container.warning .radio-label:has(.radio-toggle:checked) .radio-box {
258
+ .sui-radio-container.warning .sui-radio-label:has(.sui-radio-toggle:checked) .sui-radio-box {
139
259
  background-color: hsl(var(--warning-base));
140
260
  outline-color: hsl(var(--warning-base));
141
261
  }
142
262
 
143
- .radio-container.danger .radio-label:has(.radio-toggle:checked) .radio-box {
263
+ .sui-radio-container.danger .sui-radio-label:has(.sui-radio-toggle:checked) .sui-radio-box {
144
264
  background-color: hsl(var(--danger-base));
145
265
  outline-color: hsl(var(--danger-base));
146
266
  }
147
267
 
148
- .radio-box-container {
268
+ .sui-radio-box-container {
149
269
  width: 20px;
150
270
  height: 20px;
151
271
  display: flex;
@@ -155,7 +275,7 @@ const {
155
275
  cursor: pointer;
156
276
  }
157
277
 
158
- .radio-box {
278
+ .sui-radio-box {
159
279
  width: 12px;
160
280
  height: 12px;
161
281
  border-radius: 20px;
@@ -164,7 +284,11 @@ const {
164
284
  transition: all .15s ease;
165
285
  }
166
286
 
167
- .radio-toggle {
287
+ .sui-radio-box:focus-visible {
288
+ outline-color: hsl(var(--text-normal)) !important;
289
+ }
290
+
291
+ .sui-radio-toggle {
168
292
  width: 0;
169
293
  height: 0;
170
294
  visibility: hidden;
@@ -1,38 +1,47 @@
1
1
  ---
2
2
  import type { HTMLAttributes } from 'astro/types';
3
3
 
4
+ /**
5
+ * The props for the row component.
6
+ */
4
7
  interface Props extends HTMLAttributes<'div'> {
8
+ /**
9
+ * Whether the row should be aligned to the center. Defaults to `false`.
10
+ */
5
11
  alignCenter?: boolean;
12
+ /**
13
+ * The size of the gap between the children. Defaults to `md`.
14
+ */
6
15
  gapSize?: 'sm' | 'md' | 'lg';
7
- };
16
+ }
8
17
 
9
18
  const { alignCenter, gapSize = 'md', ...props } = Astro.props;
10
19
  ---
11
20
 
12
- <div class="row" class:list={[alignCenter && "align", gapSize]} {...props}>
21
+ <div class="sui-row" class:list={[alignCenter && "align", gapSize]} {...props}>
13
22
  <slot />
14
23
  </div>
15
24
  <style>
16
- .row {
25
+ .sui-row {
17
26
  display: flex;
18
27
  flex-direction: row;
19
28
  position: relative;
20
29
  flex-wrap: wrap;
21
30
  }
22
31
 
23
- .row.align {
32
+ .sui-row.align {
24
33
  align-items: center;
25
34
  }
26
35
 
27
- .row.sm {
36
+ .sui-row.sm {
28
37
  gap: .5rem;
29
38
  }
30
39
 
31
- .row.md {
40
+ .sui-row.md {
32
41
  gap: 1rem;
33
42
  }
34
43
 
35
- .row.lg {
44
+ .sui-row.lg {
36
45
  gap: 2rem;
37
46
  }
38
47
  </style>