@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,13 +1,17 @@
1
1
  class SingleSidebarHelper {
2
- sidebar: HTMLElement;
3
- sidebarToggle?: HTMLElement | undefined;
2
+ private sidebar: HTMLElement;
3
+ private sidebarToggle?: HTMLElement | undefined;
4
4
 
5
+ /**
6
+ * A helper to manage the sidebar with.
7
+ * @param toggleID The ID of the element that should toggle the sidebar.
8
+ */
5
9
  constructor(toggleID?: string) {
6
- const sidebarContainer = document.getElementById('sidebar');
10
+ const sidebarContainer = document.getElementById('sui-sidebar');
7
11
 
8
12
  if (!sidebarContainer) {
9
13
  throw new Error(
10
- `No item with ID 'sidebar' found. Please add the <Sidebar> component to this page.`
14
+ `No item with ID 'sui-sidebar' found. Please add the <Sidebar> component to this page.`
11
15
  );
12
16
  }
13
17
 
@@ -28,6 +32,10 @@ class SingleSidebarHelper {
28
32
  }
29
33
  }
30
34
 
35
+ /**
36
+ * A helper function register an element which should toggle the sidebar.
37
+ * @param elementID The ID of the element that should toggle the sidebar.
38
+ */
31
39
  public toggleSidebarOnClick = (elementID: string) => {
32
40
  const navToggle = document.getElementById(elementID);
33
41
 
@@ -40,8 +48,12 @@ class SingleSidebarHelper {
40
48
  this.sidebarToggle.addEventListener('click', () => {
41
49
  this.sidebar.classList.toggle('active');
42
50
  });
43
- }
51
+ };
44
52
 
53
+ /**
54
+ * A helper function to hide the sidebar when an element is clicked.
55
+ * @param elementID The ID of the element that should hide the sidebar.
56
+ */
45
57
  public hideSidebarOnClick = (elementID: string) => {
46
58
  const element = document.getElementById(elementID);
47
59
 
@@ -52,6 +64,10 @@ class SingleSidebarHelper {
52
64
  element.addEventListener('click', this.hideSidebar);
53
65
  };
54
66
 
67
+ /**
68
+ * A helper function to show the sidebar when an element is clicked.
69
+ * @param elementID The ID of the element that should show the sidebar.
70
+ */
55
71
  public showSidebarOnClick = (elementID: string) => {
56
72
  const element = document.getElementById(elementID);
57
73
 
@@ -62,20 +78,29 @@ class SingleSidebarHelper {
62
78
  element.addEventListener('click', this.showSidebar);
63
79
  };
64
80
 
81
+ /**
82
+ * A function to hide the sidebar.
83
+ */
65
84
  public hideSidebar = () => {
66
85
  this.sidebar.classList.remove('active');
67
86
  };
68
87
 
88
+ /**
89
+ * A function to show the sidebar.
90
+ */
69
91
  public showSidebar = () => {
70
92
  this.sidebar.classList.add('active');
71
93
  };
72
94
  }
73
95
 
74
96
  class DoubleSidebarHelper {
75
- sidebarsContainer: HTMLElement;
97
+ private sidebarsContainer: HTMLElement;
76
98
 
99
+ /**
100
+ * A helper to manage the double sidebar with.
101
+ */
77
102
  constructor() {
78
- const sidebarsContainer = document.getElementById('sidebars');
103
+ const sidebarsContainer = document.getElementById('sui-sidebars');
79
104
 
80
105
  if (!sidebarsContainer) {
81
106
  throw new Error(
@@ -86,6 +111,10 @@ class DoubleSidebarHelper {
86
111
  this.sidebarsContainer = sidebarsContainer;
87
112
  }
88
113
 
114
+ /**
115
+ * A helper function to hide the sidebar when an element is clicked.
116
+ * @param elementID The ID of the element that should hide the sidebar.
117
+ */
89
118
  public hideSidebarOnClick = (elementID: string) => {
90
119
  const element = document.getElementById(elementID);
91
120
 
@@ -96,6 +125,10 @@ class DoubleSidebarHelper {
96
125
  element.addEventListener('click', this.hideSidebar);
97
126
  };
98
127
 
128
+ /**
129
+ * A helper function to show the outer sidebar when an element is clicked.
130
+ * @param elementID The ID of the element that should show the outer sidebar.
131
+ */
99
132
  public showOuterOnClick = (elementID: string) => {
100
133
  const element = document.getElementById(elementID);
101
134
 
@@ -106,6 +139,10 @@ class DoubleSidebarHelper {
106
139
  element.addEventListener('click', this.showOuterSidebar);
107
140
  };
108
141
 
142
+ /**
143
+ * A helper function to show the inner sidebar when an element is clicked.
144
+ * @param elementID The ID of the element that should show the inner sidebar.
145
+ */
109
146
  public showInnerOnClick = (elementID: string) => {
110
147
  const element = document.getElementById(elementID);
111
148
 
@@ -116,15 +153,24 @@ class DoubleSidebarHelper {
116
153
  element.addEventListener('click', this.showInnerSidebar);
117
154
  };
118
155
 
156
+ /**
157
+ * A function to show the inner sidebar.
158
+ */
119
159
  public showInnerSidebar = () => {
120
160
  this.sidebarsContainer.classList.add('inner', 'active');
121
161
  };
122
162
 
163
+ /**
164
+ * A function to show the outer sidebar.
165
+ */
123
166
  public showOuterSidebar = () => {
124
167
  this.sidebarsContainer.classList.add('active');
125
168
  this.sidebarsContainer.classList.remove('inner');
126
169
  };
127
170
 
171
+ /**
172
+ * A function to hide the sidebar altogether.
173
+ */
128
174
  public hideSidebar = () => {
129
175
  this.sidebarsContainer.classList.remove('inner', 'active');
130
176
  };
@@ -0,0 +1,47 @@
1
+ ---
2
+ import type { StudioCMSColorway } from '../../utils/colors';
3
+ import { generateID } from '../../utils/generateID';
4
+ import type { HeroIconName } from '../../utils/iconType';
5
+
6
+ /**
7
+ * The props for the TabItem component.
8
+ */
9
+ interface Props {
10
+ /**
11
+ * The icon to display next to the tab.
12
+ */
13
+ icon?: HeroIconName;
14
+ /**
15
+ * The label of the tab.
16
+ */
17
+ label: string;
18
+ /**
19
+ * The color of the tab. Defaults to `primary`.
20
+ */
21
+ color?: Exclude<StudioCMSColorway, 'default'>;
22
+ }
23
+
24
+ const id = generateID('tab');
25
+
26
+ const { icon, label, color = 'primary' } = Astro.props;
27
+ ---
28
+ <sui-tab-item
29
+ data-icon={icon}
30
+ data-label={label}
31
+ data-color={color}
32
+ data-tab-id={id}
33
+ class=""
34
+ >
35
+ <slot />
36
+ </sui-tab-item>
37
+ <style>
38
+ sui-tab-item {
39
+ display: none;
40
+ width: 100%;
41
+ height: auto;
42
+ }
43
+
44
+ sui-tab-item.active {
45
+ display: block;
46
+ }
47
+ </style>
@@ -0,0 +1,376 @@
1
+ ---
2
+ import { Icon } from '../../utils';
3
+ import type { StudioCMSColorway } from '../../utils/colors';
4
+ import { generateID } from '../../utils/generateID';
5
+ import type { HeroIconName } from '../../utils/iconType';
6
+
7
+ interface Tab {
8
+ icon?: HeroIconName;
9
+ label: string;
10
+ color: Exclude<StudioCMSColorway, 'default'>;
11
+ tabId: string;
12
+ }
13
+
14
+ /**
15
+ * The props for the Tabs component.
16
+ */
17
+ interface Props {
18
+ /**
19
+ * The sync key for the tabs. If provided, the active tab will be synced across all instances of the tabs with the same sync key.
20
+ * Additionally, the active tab will be stored session- or local storage depending on the `storage` prop.
21
+ */
22
+ syncKey?: string;
23
+ /**
24
+ * The storage type for the tabs. Defaults to `session`.
25
+ */
26
+ storage?: 'session' | 'persistent';
27
+ /**
28
+ * The variant of the tabs. Defaults to `default`.
29
+ */
30
+ variant?: 'default' | 'starlight';
31
+ /**
32
+ * The alignment of the tabs. Defaults to `left`.
33
+ */
34
+ align?: 'left' | 'center' | 'right';
35
+ }
36
+
37
+ const extractTabInfoWithRegex = (input: string) => {
38
+ const tabItemRegex = /<sui-tab-item([^>]*)>/g;
39
+
40
+ const attributeRegex = /data-([\w-]+)="([^"]*)"/g;
41
+
42
+ const tabs: Tab[] = [];
43
+ let tabMatch: RegExpExecArray | null;
44
+
45
+ // biome-ignore lint/suspicious/noAssignInExpressions: Nop
46
+ while ((tabMatch = tabItemRegex.exec(input)) !== null) {
47
+ let attributes: { [key: string]: string } = {};
48
+
49
+ let attributeMatch: RegExpExecArray | null;
50
+
51
+ if (!tabMatch[1]) continue;
52
+
53
+ // biome-ignore lint/suspicious/noAssignInExpressions: Nop
54
+ while ((attributeMatch = attributeRegex.exec(tabMatch[1])) !== null) {
55
+ if (!attributeMatch[1] || !attributeMatch[2]) continue;
56
+
57
+ if (
58
+ attributeMatch[1] === 'icon' ||
59
+ attributeMatch[1] === 'label' ||
60
+ attributeMatch[1] === 'color'
61
+ ) {
62
+ attributes[attributeMatch[1]] = attributeMatch[2];
63
+ }
64
+
65
+ if (attributeMatch[1] === 'tab-id') {
66
+ attributes.tabId = attributeMatch[2];
67
+ }
68
+ }
69
+
70
+ tabs.push(attributes as unknown as Tab);
71
+ }
72
+
73
+ return tabs;
74
+ };
75
+
76
+ const markTabAsActive = (tabId: string, html: string): string => {
77
+ if (!tabId) return html;
78
+
79
+ const updatedHtml = html.replace(
80
+ new RegExp(`(<sui-tab-item[^>]*data-tab-id="${tabId}"[^>]*class=")([^"]*)(")`, 'g'),
81
+ '$1$2 active$3'
82
+ );
83
+
84
+ return updatedHtml;
85
+ };
86
+
87
+ const uniqueId = generateID('sui-tabs-container');
88
+
89
+ const {
90
+ syncKey: originalSyncKey,
91
+ storage = 'session',
92
+ variant = 'default',
93
+ align = 'left',
94
+ } = Astro.props;
95
+
96
+ const syncKey = originalSyncKey ? `sui-tabs-${originalSyncKey}` : undefined;
97
+
98
+ const tabContents = await Astro.slots.render('default');
99
+ const tabs = extractTabInfoWithRegex(tabContents);
100
+ const finalizedTabContents = markTabAsActive(tabs[0]?.tabId || '', tabContents);
101
+ const containerId = generateID('sui-tabs-container');
102
+ ---
103
+
104
+ <div
105
+ class="sui-tabs-container"
106
+ id={containerId}
107
+ data-sync-key={syncKey}
108
+ data-unique-id={uniqueId}
109
+ data-storage-strategy={storage}
110
+ class:list={[variant, align]}
111
+ >
112
+ <div class="sui-tabs-list" role="tablist">
113
+ {tabs.map((tab, i) => (
114
+ <button
115
+ role="tab"
116
+ class="sui-tab-header"
117
+ id={syncKey ? `${syncKey}-${i}` : undefined}
118
+ tabindex={i === 0 ? 0 : -1}
119
+ data-tab-child={tab.tabId}
120
+ class:list={[i === 0 && "active", tab.color, syncKey && `${syncKey}:${i}`]}
121
+ >
122
+ {tab.icon && (
123
+ <Icon name={tab.icon} width={24} height={24} />
124
+ )}
125
+ <span>{tab.label}</span>
126
+ </button>
127
+ ))}
128
+ </div>
129
+ <div class="sui-tabs-content">
130
+ <Fragment set:html={finalizedTabContents} />
131
+ </div>
132
+ </div>
133
+ <script>
134
+ const tabContainers = document.querySelectorAll<HTMLDivElement>('.sui-tabs-container');
135
+
136
+ for (const tabContainer of tabContainers) {
137
+ const storage = tabContainer.dataset.storageStrategy!;
138
+ const syncKey = tabContainer.dataset.syncKey!;
139
+
140
+ let storageLayer = storage === 'session' ? sessionStorage : localStorage;
141
+
142
+ const constructCustomEvent = (tabIndex: number, uniqueId: string) => {
143
+ return new CustomEvent(`sui-tab-switch:${syncKey}`, {
144
+ detail: {
145
+ tabIndex,
146
+ uniqueId
147
+ }
148
+ });
149
+ }
150
+
151
+ const switchTab = (target: HTMLElement, container: HTMLElement, originatedFromSync = false) => {
152
+ const activeChildren = container.querySelectorAll<HTMLElement>('.active');
153
+
154
+ for (const child of activeChildren) {
155
+ child.tabIndex = -1;
156
+ child.classList.remove('active');
157
+ }
158
+
159
+ const newActiveTab = target as HTMLElement;
160
+ newActiveTab.classList.add('active');
161
+ newActiveTab.tabIndex = 0;
162
+
163
+ const newActiveTabContentId = newActiveTab.dataset.tabChild;
164
+ const newActiveTabContent = container.querySelector<HTMLElement>(`sui-tab-item[data-tab-id="${newActiveTabContentId}"]`)!;
165
+
166
+ newActiveTabContent.classList.add('active');
167
+
168
+ if (syncKey && !originatedFromSync) {
169
+ const tabIndex = Array.prototype.indexOf.call(newActiveTab.parentElement!.children, newActiveTab);
170
+ storageLayer.setItem(syncKey, tabIndex.toString());
171
+
172
+ document.dispatchEvent(constructCustomEvent(tabIndex, container.dataset.uniqueId!));
173
+ }
174
+
175
+ }
176
+
177
+ const tabHeaders = tabContainer.querySelectorAll<HTMLElement>('.sui-tab-header');
178
+
179
+ for (const tab of tabHeaders) {
180
+ tab.addEventListener('click', (e) => switchTab(e.target as HTMLElement, tabContainer));
181
+
182
+ tab.addEventListener('keydown', (e) => {
183
+ if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') {
184
+ const activeTabIndex = Array.prototype.indexOf.call(tab.parentElement!.children, tab);
185
+ const nextTabIndex = e.key === 'ArrowLeft' ? activeTabIndex - 1 : activeTabIndex + 1;
186
+
187
+
188
+ if (nextTabIndex >= 0 && nextTabIndex < tab.parentElement!.children.length) {
189
+ tab.tabIndex = -1;
190
+
191
+ const nextTab = tab.parentElement!.children[nextTabIndex]! as HTMLElement;
192
+
193
+ nextTab.tabIndex = 0;
194
+ nextTab.click();
195
+ nextTab.focus();
196
+ } else if (nextTabIndex < 0) {
197
+ tab.tabIndex = -1;
198
+
199
+ const lastTab = tab.parentElement!.children[tab.parentElement!.children.length - 1] as HTMLElement;
200
+
201
+ lastTab.tabIndex = 0;
202
+ lastTab.click();
203
+ lastTab.focus();
204
+ } else {
205
+ tab.tabIndex = -1;
206
+
207
+ const firstTab = tab.parentElement!.children[0] as HTMLElement;
208
+
209
+ firstTab.tabIndex = 0;
210
+ firstTab.click();
211
+ firstTab.focus();
212
+ }
213
+ }
214
+ });
215
+ }
216
+
217
+ if (syncKey) {
218
+ // Retrieve the sync key value from localstorage, set the tab.
219
+ const activeTabIndex = storageLayer.getItem(syncKey);
220
+
221
+ if (activeTabIndex) {
222
+ const activeTab = tabContainer.querySelector<HTMLElement>(`#${syncKey}-${activeTabIndex}`);
223
+
224
+ if (activeTab) {
225
+ activeTab.click();
226
+ }
227
+ }
228
+
229
+ document.addEventListener(`sui-tab-switch:${syncKey}`, (e) => {
230
+ const event = e as CustomEvent<{ tabIndex: number, uniqueId: string }>;
231
+ const { tabIndex, uniqueId } = event.detail;
232
+
233
+ if (uniqueId === tabContainer.dataset.uniqueId) return;
234
+
235
+ const newTab = tabContainer.querySelector<HTMLElement>(`#${syncKey}-${tabIndex}`)!;
236
+
237
+ switchTab(newTab, tabContainer, true);
238
+ });
239
+ }
240
+ }
241
+ </script>
242
+ <style>
243
+ .sui-tabs-container {
244
+ width: 100%;
245
+ }
246
+
247
+ .sui-tabs-list {
248
+ display: flex;
249
+ flex-direction: row;
250
+ gap: 1rem;
251
+ align-items: center;
252
+ width: 100%;
253
+ overflow-x: auto;
254
+ overflow-y: visible;
255
+ position: relative;
256
+ }
257
+
258
+ .default .sui-tabs-list {
259
+ margin-top: -4px;
260
+ margin-bottom: calc(2rem - 4px);
261
+ padding: 4px 4px;
262
+ margin-left: -4px;
263
+ }
264
+
265
+ .center .sui-tabs-list {
266
+ justify-content: center;
267
+ }
268
+
269
+ .right .sui-tabs-list {
270
+ justify-content: flex-end;
271
+ }
272
+
273
+ .sui-tab-header {
274
+ margin-top: 0 !important;
275
+ display: flex;
276
+ flex-direction: row;
277
+ gap: .5rem;
278
+ cursor: pointer;
279
+ position: relative;
280
+ min-width: fit-content;
281
+ }
282
+
283
+ .default .sui-tab-header {
284
+ border-radius: 0.5rem;
285
+ height: 40px;
286
+ padding: 0.5rem 0.75rem;
287
+ transition: all .15s ease;
288
+ font-size: 0.875em;
289
+ outline: 2px solid transparent;
290
+ outline-offset: 2px;
291
+ }
292
+
293
+ .sui-tab-header * {
294
+ pointer-events: none;
295
+ }
296
+
297
+ .default .sui-tab-header:focus-visible {
298
+ outline: 2px solid hsl(var(--text-normal));
299
+ outline-offset: 2px;
300
+ }
301
+
302
+ .default .sui-tab-header:hover {
303
+ background-color: hsla(var(--default-flat-active)) !important;
304
+ }
305
+
306
+ .default .sui-tab-header.active {
307
+ background-color: hsla(var(--primary-flat-active)) !important;
308
+ }
309
+
310
+ .default .sui-tab-header.success.active {
311
+ background-color: hsla(var(--success-flat-active)) !important;
312
+ }
313
+
314
+ .default .sui-tab-header.warning.active {
315
+ background-color: hsla(var(--warning-flat-active)) !important;
316
+ }
317
+
318
+ .default .sui-tab-header.danger.active {
319
+ background-color: hsla(var(--danger-flat-active)) !important;
320
+ }
321
+
322
+ .starlight .sui-tabs-list {
323
+ margin-bottom: 1rem;
324
+ gap: 0;
325
+ }
326
+
327
+ .starlight .sui-tabs-list::before {
328
+ content: "";
329
+ position: absolute;
330
+ bottom: 0;
331
+ left: 0;
332
+ width: 100%;
333
+ height: 2px;
334
+ background-color: hsl(var(--border));
335
+ }
336
+
337
+ .starlight .sui-tab-header {
338
+ padding: 0.25rem 1.25rem;
339
+ color: hsl(var(--text-muted));
340
+ }
341
+
342
+ .starlight .sui-tab-header.active {
343
+ font-weight: 600;
344
+ color: hsl(var(--text-normal));
345
+ }
346
+
347
+ .starlight .sui-tab-header.active::after {
348
+ content: "";
349
+ width: 100%;
350
+ height: 2px;
351
+ background-color: hsl(var(--primary-base));
352
+ position: absolute;
353
+ bottom: 0;
354
+ left: 0;
355
+ z-index: 15;
356
+ }
357
+
358
+ .starlight .sui-tab-header:focus-visible::after {
359
+ height: calc(100% - 2px);
360
+ width: calc(100% - 2px);
361
+ bottom: 1px;
362
+ left: 1px;
363
+ border: 2px solid hsl(var(--primary-base));
364
+ background-color: transparent;
365
+ outline: 1px solid hsl(var(--text-normal));
366
+ }
367
+
368
+ .default .sui-tab-header.active {
369
+ background-color: hsla(var(--primary-flat-active)) !important;
370
+ }
371
+
372
+ .sui-tabs-content {
373
+ width: 100%;
374
+ margin: 0 !important;
375
+ }
376
+ </style>
@@ -0,0 +1,2 @@
1
+ export { default as TabItem } from './TabItem.astro';
2
+ export { default as Tabs } from './Tabs.astro';
@@ -1,33 +1,68 @@
1
1
  ---
2
+ import type { HTMLAttributes } from 'astro/types';
2
3
  import { generateID } from '../utils/generateID';
3
4
 
4
- interface Props {
5
+ /**
6
+ * Props for the textarea component
7
+ */
8
+ interface Props extends HTMLAttributes<'textarea'> {
9
+ /**
10
+ * The label of the textarea.
11
+ */
5
12
  label?: string;
13
+ /**
14
+ * The placeholder of the textarea.
15
+ */
6
16
  placeholder?: string;
17
+ /**
18
+ * Whether the textarea is required. Defaults to `false`.
19
+ */
7
20
  isRequired?: boolean;
21
+ /**
22
+ * Whether the textarea should take up the full width of its container. Defaults to `false`.
23
+ */
8
24
  fullWidth?: boolean;
25
+ /**
26
+ * Whether the textarea should take up the full height of its container. Defaults to `false`.
27
+ */
28
+ fullHeight?: boolean;
29
+ /**
30
+ * Whether the textarea should be resizable. Defaults to `false`.
31
+ */
9
32
  resize?: boolean;
33
+ /**
34
+ * The name attribute for the textarea. Useful for form submission.
35
+ */
10
36
  name?: string;
37
+ /**
38
+ * Whether the textarea is disabled. Defaults to `false`.
39
+ */
11
40
  disabled?: boolean;
41
+ /**
42
+ * The default value of the textarea.
43
+ */
12
44
  defaultValue?: string;
13
- };
45
+ }
14
46
 
15
47
  const {
16
48
  label,
17
49
  placeholder,
18
50
  isRequired,
19
51
  fullWidth,
52
+ fullHeight,
20
53
  resize,
21
54
  name = generateID('textarea'),
22
55
  disabled,
23
56
  defaultValue,
57
+ ...props
24
58
  } = Astro.props;
25
59
  ---
26
60
  <label
27
61
  for={name}
28
- class="textarea-label"
62
+ class="sui-textarea-label"
29
63
  class:list={[
30
- fullWidth && "full",
64
+ fullWidth && "full-width",
65
+ fullHeight && "full-height",
31
66
  resize && "resize",
32
67
  disabled && "disabled",
33
68
  ]}
@@ -39,38 +74,42 @@ const {
39
74
  placeholder={placeholder}
40
75
  name={name}
41
76
  id={name}
42
- class="textarea"
77
+ class="sui-textarea"
43
78
  required={isRequired}
44
79
  disabled={disabled}
80
+ {...props}
45
81
 
46
82
  >{defaultValue}</textarea>
47
83
  </label>
48
84
  <style>
49
- .textarea-label {
85
+ .sui-textarea-label {
50
86
  display: flex;
51
87
  flex-direction: column;
52
88
  gap: .25rem;
53
- margin-top: .5rem;
54
89
  max-width: 80ch;
55
90
  }
56
91
 
57
- .textarea-label.disabled {
92
+ .sui-textarea-label.disabled {
58
93
  opacity: 0.5;
59
94
  pointer-events: none;
60
95
  color: hsl(var(--text-muted));
61
96
  }
62
97
 
63
- .textarea-label.full {
98
+ .sui-textarea-label.full-width {
64
99
  width: 100%;
65
100
  max-width: none;
66
101
  }
67
102
 
103
+ .sui-textarea-label.full-height {
104
+ height: 100%;
105
+ }
106
+
68
107
  .label {
69
108
  font-size: 14px;
70
109
  }
71
110
 
72
- .textarea {
73
- padding: .65rem 1rem;
111
+ .sui-textarea {
112
+ padding: .65rem;
74
113
  border-radius: 8px;
75
114
  border: 1px solid hsl(var(--border));
76
115
  background: hsl(var(--background-step-2));
@@ -78,18 +117,20 @@ const {
78
117
  transition: all .15s ease;
79
118
  resize: none;
80
119
  min-height: 12ch;
120
+ width: 100%;
121
+ height: 100%;
81
122
  }
82
123
 
83
- .textarea:hover {
124
+ .sui-textarea:hover {
84
125
  background: hsl(var(--background-step-3));
85
126
  }
86
127
 
87
- .resize .textarea {
128
+ .resize .sui-textarea {
88
129
  resize: both;
89
130
  }
90
131
 
91
- .textarea:active,
92
- .textarea:focus {
132
+ .sui-textarea:active,
133
+ .sui-textarea:focus {
93
134
  border: 1px solid hsl(var(--primary-base));
94
135
  outline: none;
95
136
  background: hsl(var(--background-step-2));