@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.
- package/README.md +28 -544
- package/package.json +11 -6
- package/src/components/Button.astro +303 -269
- package/src/components/Card.astro +37 -13
- package/src/components/Center.astro +2 -2
- package/src/components/Checkbox.astro +99 -29
- package/src/components/Divider.astro +15 -8
- package/src/components/Dropdown/Dropdown.astro +102 -41
- package/src/components/Dropdown/dropdown.ts +111 -23
- package/src/components/Footer.astro +137 -0
- package/src/components/Input.astro +42 -14
- package/src/components/Modal/Modal.astro +84 -30
- package/src/components/Modal/modal.ts +43 -9
- package/src/components/RadioGroup.astro +153 -29
- package/src/components/Row.astro +16 -7
- package/src/components/SearchSelect.astro +278 -222
- package/src/components/Select.astro +260 -127
- package/src/components/Sidebar/Double.astro +12 -12
- package/src/components/Sidebar/Single.astro +6 -6
- package/src/components/Sidebar/helpers.ts +53 -7
- 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 +56 -15
- package/src/components/ThemeToggle.astro +14 -8
- package/src/components/Toast/Toaster.astro +171 -31
- package/src/components/Toggle.astro +89 -21
- package/src/components/User.astro +34 -15
- package/src/components/index.ts +24 -22
- package/src/components.ts +2 -0
- package/src/css/colors.css +65 -65
- package/src/css/resets.css +0 -1
- package/src/integration.ts +18 -0
- package/src/layouts/RootLayout.astro +1 -2
- package/src/types/index.ts +1 -1
- package/src/utils/ThemeHelper.ts +135 -117
- 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>
|
|
@@ -1,33 +1,68 @@
|
|
|
1
1
|
---
|
|
2
|
+
import type { HTMLAttributes } from 'astro/types';
|
|
2
3
|
import { generateID } from '../utils/generateID';
|
|
3
4
|
|
|
4
|
-
|
|
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
|
|
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));
|