@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
|
@@ -2,13 +2,37 @@
|
|
|
2
2
|
import type { StudioCMSColorway } from '../utils/colors';
|
|
3
3
|
import { generateID } from '../utils/generateID';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* The props for the toggle component
|
|
7
|
+
*/
|
|
5
8
|
interface Props {
|
|
9
|
+
/**
|
|
10
|
+
* The label of the toggle.
|
|
11
|
+
*/
|
|
6
12
|
label: string;
|
|
13
|
+
/**
|
|
14
|
+
* The size of the toggle. Defaults to `md`.
|
|
15
|
+
*/
|
|
7
16
|
size?: 'sm' | 'md' | 'lg';
|
|
17
|
+
/**
|
|
18
|
+
* The color of the toggle. Defaults to `default`.
|
|
19
|
+
*/
|
|
8
20
|
color?: StudioCMSColorway;
|
|
21
|
+
/**
|
|
22
|
+
* Whether the toggle is checked by default. Defaults to `false`.
|
|
23
|
+
*/
|
|
9
24
|
defaultChecked?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Whether the toggle is disabled. Defaults to `false`.
|
|
27
|
+
*/
|
|
10
28
|
disabled?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* The name of the toggle.
|
|
31
|
+
*/
|
|
11
32
|
name?: string;
|
|
33
|
+
/**
|
|
34
|
+
* Whether the toggle is required. Defaults to `false`.
|
|
35
|
+
*/
|
|
12
36
|
isRequired?: boolean;
|
|
13
37
|
}
|
|
14
38
|
|
|
@@ -32,7 +56,13 @@ const {
|
|
|
32
56
|
]}
|
|
33
57
|
>
|
|
34
58
|
<div class="sui-toggle-container">
|
|
35
|
-
<div
|
|
59
|
+
<div
|
|
60
|
+
class="sui-toggle-switch"
|
|
61
|
+
tabindex="0"
|
|
62
|
+
role="checkbox"
|
|
63
|
+
aria-checked={defaultChecked}
|
|
64
|
+
aria-label={label}
|
|
65
|
+
/>
|
|
36
66
|
<input
|
|
37
67
|
type="checkbox"
|
|
38
68
|
name={name}
|
|
@@ -40,13 +70,46 @@ const {
|
|
|
40
70
|
checked={defaultChecked}
|
|
41
71
|
disabled={disabled}
|
|
42
72
|
required={isRequired}
|
|
43
|
-
class="sui-checkbox"
|
|
73
|
+
class="sui-toggle-checkbox"
|
|
74
|
+
hidden
|
|
44
75
|
/>
|
|
45
76
|
</div>
|
|
46
|
-
<span>
|
|
77
|
+
<span id={`label-${name}`}>
|
|
47
78
|
{label} <span class="req-star">{isRequired && "*"}</span>
|
|
48
79
|
</span>
|
|
49
80
|
</label>
|
|
81
|
+
<script>
|
|
82
|
+
const elements = document.querySelectorAll<HTMLDivElement>('.sui-toggle-container');
|
|
83
|
+
const toggles = document.querySelectorAll<HTMLInputElement>('.sui-toggle-checkbox');
|
|
84
|
+
|
|
85
|
+
for (const element of elements) {
|
|
86
|
+
if (element.dataset.initialized) continue;
|
|
87
|
+
|
|
88
|
+
element.dataset.initialized = 'true';
|
|
89
|
+
|
|
90
|
+
element.addEventListener('keydown', (e) => {
|
|
91
|
+
if (e.key !== 'Enter' && e.key !== ' ') return;
|
|
92
|
+
|
|
93
|
+
e.preventDefault();
|
|
94
|
+
|
|
95
|
+
const checkbox = element.querySelector<HTMLInputElement>('.sui-toggle-checkbox');
|
|
96
|
+
|
|
97
|
+
if (!checkbox) return;
|
|
98
|
+
|
|
99
|
+
checkbox.click();
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
for (const box of toggles) {
|
|
104
|
+
if (box.dataset.initialized) continue;
|
|
105
|
+
|
|
106
|
+
box.dataset.initialized = 'true';
|
|
107
|
+
|
|
108
|
+
box.addEventListener('change', (e) => {
|
|
109
|
+
(box.previousSibling as HTMLDivElement).ariaChecked = (e.target as HTMLInputElement).checked ? 'true' : 'false';
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
</script>
|
|
50
113
|
<style>
|
|
51
114
|
.sui-toggle-label {
|
|
52
115
|
display: flex;
|
|
@@ -92,7 +155,12 @@ const {
|
|
|
92
155
|
will-change: transform;
|
|
93
156
|
}
|
|
94
157
|
|
|
95
|
-
.sui-toggle-
|
|
158
|
+
.sui-toggle-switch:focus-visible {
|
|
159
|
+
outline: 2px solid hsl(var(--text-normal));
|
|
160
|
+
outline-offset: 2px;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.sui-toggle-container:has(.sui-toggle-checkbox:checked) .sui-toggle-switch {
|
|
96
164
|
left: calc(100% - var(--switch));
|
|
97
165
|
background-color: hsl(var(--text-normal));
|
|
98
166
|
}
|
|
@@ -115,19 +183,19 @@ const {
|
|
|
115
183
|
--switch: calc(var(--toggle-height) * 1.65);
|
|
116
184
|
}
|
|
117
185
|
|
|
118
|
-
.sui-toggle-label.primary .sui-toggle-container:has(.sui-checkbox:checked) {
|
|
186
|
+
.sui-toggle-label.primary .sui-toggle-container:has(.sui-toggle-checkbox:checked) {
|
|
119
187
|
background-color: hsl(var(--primary-base));
|
|
120
188
|
}
|
|
121
189
|
|
|
122
|
-
.sui-toggle-label.success .sui-toggle-container:has(.sui-checkbox:checked) {
|
|
190
|
+
.sui-toggle-label.success .sui-toggle-container:has(.sui-toggle-checkbox:checked) {
|
|
123
191
|
background-color: hsl(var(--success-base));
|
|
124
192
|
}
|
|
125
193
|
|
|
126
|
-
.sui-toggle-label.warning .sui-toggle-container:has(.sui-checkbox:checked) {
|
|
194
|
+
.sui-toggle-label.warning .sui-toggle-container:has(.sui-toggle-checkbox:checked) {
|
|
127
195
|
background-color: hsl(var(--warning-base));
|
|
128
196
|
}
|
|
129
197
|
|
|
130
|
-
.sui-toggle-label.danger .sui-toggle-container:has(.sui-checkbox:checked) {
|
|
198
|
+
.sui-toggle-label.danger .sui-toggle-container:has(.sui-toggle-checkbox:checked) {
|
|
131
199
|
background-color: hsl(var(--danger-base));
|
|
132
200
|
}
|
|
133
201
|
|
|
@@ -136,7 +204,7 @@ const {
|
|
|
136
204
|
font-weight: 700;
|
|
137
205
|
}
|
|
138
206
|
|
|
139
|
-
.sui-checkbox {
|
|
207
|
+
.sui-toggle-checkbox {
|
|
140
208
|
width: 0;
|
|
141
209
|
height: 0;
|
|
142
210
|
visibility: hidden;
|
|
@@ -2,22 +2,40 @@
|
|
|
2
2
|
import { Image } from 'astro:assets';
|
|
3
3
|
import Icon from '../utils/Icon.astro';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* The props for the User component.
|
|
7
|
+
*/
|
|
5
8
|
interface Props {
|
|
9
|
+
/**
|
|
10
|
+
* The name of the user.
|
|
11
|
+
*/
|
|
6
12
|
name: string;
|
|
13
|
+
/**
|
|
14
|
+
* The description of the user. Could be a role, a handle, etc.
|
|
15
|
+
*/
|
|
7
16
|
description: string;
|
|
17
|
+
/**
|
|
18
|
+
* The avatar of the user. Either a URL to an image or an imported image.
|
|
19
|
+
*/
|
|
8
20
|
avatar?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Additional classes to apply to the user container.
|
|
23
|
+
*/
|
|
9
24
|
class?: string;
|
|
25
|
+
/**
|
|
26
|
+
* The loading strategy for the image. Defaults to `lazy`.
|
|
27
|
+
*/
|
|
10
28
|
loading?: 'eager' | 'lazy';
|
|
11
29
|
}
|
|
12
30
|
|
|
13
|
-
const { name, description, avatar, class: className, loading } = Astro.props;
|
|
31
|
+
const { name, description, avatar, class: className, loading = 'lazy' } = Astro.props;
|
|
14
32
|
---
|
|
15
33
|
<div class="sui-user-container" class:list={[ className ]}>
|
|
16
34
|
<div class="sui-avatar-container">
|
|
17
35
|
{avatar ? (
|
|
18
36
|
<Image src={avatar} inferSize loading={loading} alt={name} class="sui-avatar-img" />
|
|
19
37
|
) : (
|
|
20
|
-
<Icon name='user' width={24} height={24} />
|
|
38
|
+
<Icon name='user' width={24} height={24} role='img' aria-label={`Placeholder avatar for ${name}`} />
|
|
21
39
|
)}
|
|
22
40
|
</div>
|
|
23
41
|
<div class="sui-text-content">
|
package/src/components/index.ts
CHANGED
|
@@ -16,6 +16,7 @@ export { default as Dropdown } from './Dropdown/Dropdown.astro';
|
|
|
16
16
|
export { default as User } from './User.astro';
|
|
17
17
|
export { default as ThemeToggle } from './ThemeToggle.astro';
|
|
18
18
|
export { default as Footer } from './Footer.astro';
|
|
19
|
+
export { Tabs, TabItem } from './Tabs/index';
|
|
19
20
|
|
|
20
21
|
export { default as Sidebar } from './Sidebar/Single.astro';
|
|
21
22
|
export { default as DoubleSidebar } from './Sidebar/Double.astro';
|
package/src/components.ts
CHANGED
|
@@ -16,6 +16,7 @@ export { Dropdown, DropdownHelper } from './components/index';
|
|
|
16
16
|
export { User } from './components/index';
|
|
17
17
|
export { ThemeToggle } from './components/index';
|
|
18
18
|
export { Footer } from './components/index';
|
|
19
|
+
export { Tabs, TabItem } from './components/index';
|
|
19
20
|
|
|
20
21
|
export {
|
|
21
22
|
Sidebar,
|
package/src/css/colors.css
CHANGED
|
@@ -24,24 +24,24 @@
|
|
|
24
24
|
--shadow: 0 0% 0%;
|
|
25
25
|
|
|
26
26
|
--default-base: 0 0% 14%;
|
|
27
|
-
--default-hover: 0 0%
|
|
28
|
-
--default-active: 0 0%
|
|
27
|
+
--default-hover: 0 0% 21%;
|
|
28
|
+
--default-active: 0 0% 15%;
|
|
29
29
|
|
|
30
30
|
--primary-base: 259 83% 73%;
|
|
31
|
-
--primary-hover: 259 77%
|
|
31
|
+
--primary-hover: 259 77% 78%;
|
|
32
32
|
--primary-active: 259 73% 67%;
|
|
33
33
|
|
|
34
34
|
--success-base: 142 71% 46%;
|
|
35
|
-
--success-hover: 142 72%
|
|
36
|
-
--success-active: 142 87%
|
|
35
|
+
--success-hover: 142 72% 61%;
|
|
36
|
+
--success-active: 142 87% 52%;
|
|
37
37
|
|
|
38
38
|
--warning-base: 48 96% 53%;
|
|
39
|
-
--warning-hover: 48 97%
|
|
40
|
-
--warning-active: 48 100%
|
|
39
|
+
--warning-hover: 48 97% 70%;
|
|
40
|
+
--warning-active: 48 100% 61%;
|
|
41
41
|
|
|
42
42
|
--danger-base: 339 97% 31%;
|
|
43
43
|
--danger-hover: 337 98% 37%;
|
|
44
|
-
--danger-active: 337 88%
|
|
44
|
+
--danger-active: 337 88% 32%;
|
|
45
45
|
|
|
46
46
|
/* Non-specific variables */
|
|
47
47
|
--text-light: 0 0% 100%;
|
package/src/css/resets.css
CHANGED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { AstroIntegration } from 'astro';
|
|
2
|
+
import { createResolver } from './utils/create-resolver';
|
|
3
|
+
import { viteVirtualModulePluginBuilder } from './utils/virtual-module-plugin-builder';
|
|
4
|
+
|
|
5
|
+
// biome-ignore lint/complexity/noBannedTypes: Will be implemented in v0.3.0
|
|
6
|
+
type Options = {};
|
|
7
|
+
|
|
8
|
+
export default function integration(options: Options = {}): AstroIntegration {
|
|
9
|
+
const { resolve } = createResolver(import.meta.url);
|
|
10
|
+
|
|
11
|
+
const globalCss = viteVirtualModulePluginBuilder(
|
|
12
|
+
'studiocms:ui/global-css',
|
|
13
|
+
'sui-global-css',
|
|
14
|
+
`import '${resolve('./css/global.css')}'`
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
name: '@studiocms/ui',
|
|
19
|
+
hooks: {
|
|
20
|
+
'astro:config:setup': ({ injectScript, updateConfig }) => {
|
|
21
|
+
updateConfig({
|
|
22
|
+
vite: {
|
|
23
|
+
plugins: [globalCss()],
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
injectScript('page-ssr', `import 'studiocms:ui/global-css';`);
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
package/src/utils/ThemeHelper.ts
CHANGED
|
@@ -15,6 +15,7 @@ class ThemeHelper {
|
|
|
15
15
|
*/
|
|
16
16
|
constructor(themeProvider?: HTMLElement) {
|
|
17
17
|
this.themeManagerElement = themeProvider || document.documentElement;
|
|
18
|
+
this.themeManagerElement.dataset.theme = this.getTheme(true);
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
/**
|
|
@@ -32,7 +33,7 @@ class ThemeHelper {
|
|
|
32
33
|
return theme as T extends true ? 'dark' | 'light' : Theme;
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
if (this.themeManagerElement.dataset.theme !== 'system') {
|
|
36
|
+
if ((this.themeManagerElement.dataset.theme ?? 'system') !== 'system') {
|
|
36
37
|
return this.themeManagerElement.dataset.theme as 'dark' | 'light';
|
|
37
38
|
}
|
|
38
39
|
|
|
@@ -56,7 +57,13 @@ class ThemeHelper {
|
|
|
56
57
|
* @param theme The new theme. One of `dark`, `light` or `system`.
|
|
57
58
|
*/
|
|
58
59
|
public setTheme = (theme: Theme): void => {
|
|
60
|
+
// Assign the new theme to the dataset
|
|
59
61
|
this.themeManagerElement.dataset.theme = theme;
|
|
62
|
+
|
|
63
|
+
// If starlight is used, we also want to set the theme in local storage.
|
|
64
|
+
if (typeof localStorage.getItem('starlight-theme') === 'string') {
|
|
65
|
+
localStorage.setItem('starlight-theme', theme === 'system' ? '' : theme);
|
|
66
|
+
}
|
|
60
67
|
};
|
|
61
68
|
|
|
62
69
|
/**
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { fileURLToPath } from 'node:url';
|
|
2
|
+
import { dirname, resolve } from 'pathe';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* From the Astro Integration Kit (https://astro-integration-kit.netlify.app/).
|
|
6
|
+
*
|
|
7
|
+
* Allows resolving paths relatively to the integration folder easily. Call it like this:
|
|
8
|
+
*
|
|
9
|
+
* @param {string} _base - The location you want to create relative references from. `import.meta.url` is usually what you'll want.
|
|
10
|
+
*
|
|
11
|
+
* @see https://astro-integration-kit.netlify.app/core/create-resolver/
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* const { resolve } = createResolver(import.meta.url);
|
|
16
|
+
* const pluginPath = resolve("./plugin.ts");
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* This way, you do not have to add your plugin to your package.json `exports`.
|
|
20
|
+
*/
|
|
21
|
+
export const createResolver = (_base: string) => {
|
|
22
|
+
let base = _base;
|
|
23
|
+
if (base.startsWith('file://')) {
|
|
24
|
+
base = dirname(fileURLToPath(base));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
resolve: (...path: Array<string>) => resolve(base, ...path),
|
|
29
|
+
};
|
|
30
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { AstroConfig } from 'astro';
|
|
2
|
+
|
|
3
|
+
type VitePlugin = Required<AstroConfig['vite']>['plugins'][number];
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Builds a Vite plugin that creates a virtual module.
|
|
7
|
+
*
|
|
8
|
+
* @param moduleId - The ID of the virtual module.
|
|
9
|
+
* @param name - The name of the plugin.
|
|
10
|
+
* @param moduleContent - The content of the virtual module.
|
|
11
|
+
* @returns A Vite plugin that resolves and loads the virtual module.
|
|
12
|
+
*/
|
|
13
|
+
export function viteVirtualModulePluginBuilder(
|
|
14
|
+
moduleId: string,
|
|
15
|
+
name: string,
|
|
16
|
+
moduleContent: string
|
|
17
|
+
) {
|
|
18
|
+
return function modulePlugin(): VitePlugin {
|
|
19
|
+
const resolvedVirtualModuleId = `\0${moduleId}`; // Prefix with \0 to avoid conflicts
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
name,
|
|
23
|
+
resolveId(id) {
|
|
24
|
+
if (id === moduleId) {
|
|
25
|
+
return resolvedVirtualModuleId;
|
|
26
|
+
}
|
|
27
|
+
return;
|
|
28
|
+
},
|
|
29
|
+
load(id) {
|
|
30
|
+
if (id === resolvedVirtualModuleId) {
|
|
31
|
+
return moduleContent;
|
|
32
|
+
}
|
|
33
|
+
return;
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
}
|