@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
|
@@ -2,37 +2,56 @@
|
|
|
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;
|
|
10
|
-
|
|
25
|
+
/**
|
|
26
|
+
* The loading strategy for the image. Defaults to `lazy`.
|
|
27
|
+
*/
|
|
28
|
+
loading?: 'eager' | 'lazy';
|
|
29
|
+
}
|
|
11
30
|
|
|
12
|
-
const { name, description, avatar, class: className } = Astro.props;
|
|
31
|
+
const { name, description, avatar, class: className, loading = 'lazy' } = Astro.props;
|
|
13
32
|
---
|
|
14
|
-
<div class="user-container" class:list={[ className ]}>
|
|
15
|
-
<div class="avatar-container">
|
|
33
|
+
<div class="sui-user-container" class:list={[ className ]}>
|
|
34
|
+
<div class="sui-avatar-container">
|
|
16
35
|
{avatar ? (
|
|
17
|
-
<Image src={avatar} inferSize alt={name} class="avatar-img" />
|
|
36
|
+
<Image src={avatar} inferSize loading={loading} alt={name} class="sui-avatar-img" />
|
|
18
37
|
) : (
|
|
19
|
-
<Icon name='user' width={24} height={24} />
|
|
38
|
+
<Icon name='user' width={24} height={24} role='img' aria-label={`Placeholder avatar for ${name}`} />
|
|
20
39
|
)}
|
|
21
40
|
</div>
|
|
22
|
-
<div class="text-content">
|
|
23
|
-
<span class="name">{name}</span>
|
|
24
|
-
<span class="description">{description}</span>
|
|
41
|
+
<div class="sui-text-content">
|
|
42
|
+
<span class="sui-name">{name}</span>
|
|
43
|
+
<span class="sui-description">{description}</span>
|
|
25
44
|
</div>
|
|
26
45
|
</div>
|
|
27
46
|
<style>
|
|
28
|
-
.user-container {
|
|
47
|
+
.sui-user-container {
|
|
29
48
|
display: flex;
|
|
30
49
|
flex-direction: row;
|
|
31
50
|
align-items: center;
|
|
32
51
|
gap: 1rem;
|
|
33
52
|
}
|
|
34
53
|
|
|
35
|
-
.avatar-container {
|
|
54
|
+
.sui-avatar-container {
|
|
36
55
|
width: 2.5rem;
|
|
37
56
|
height: 2.5rem;
|
|
38
57
|
background-color: hsl(var(--default-base));
|
|
@@ -44,23 +63,23 @@ const { name, description, avatar, class: className } = Astro.props;
|
|
|
44
63
|
border: 1px solid hsl(var(--border));
|
|
45
64
|
}
|
|
46
65
|
|
|
47
|
-
.avatar-img {
|
|
66
|
+
.sui-avatar-img {
|
|
48
67
|
width: 100%;
|
|
49
68
|
height: auto;
|
|
50
69
|
}
|
|
51
70
|
|
|
52
|
-
.text-content {
|
|
71
|
+
.sui-text-content {
|
|
53
72
|
display: flex;
|
|
54
73
|
flex-direction: column;
|
|
55
74
|
gap: .125rem;
|
|
56
75
|
}
|
|
57
76
|
|
|
58
|
-
.name {
|
|
77
|
+
.sui-name {
|
|
59
78
|
font-size: 1em;
|
|
60
79
|
font-weight: 600;
|
|
61
80
|
}
|
|
62
81
|
|
|
63
|
-
.description {
|
|
82
|
+
.sui-description {
|
|
64
83
|
font-size: .875em;
|
|
65
84
|
font-weight: 400;
|
|
66
85
|
color: hsl(var(--text-muted));
|
package/src/components/index.ts
CHANGED
|
@@ -1,25 +1,27 @@
|
|
|
1
|
-
export { default as Button } from
|
|
2
|
-
export { default as Divider } from
|
|
3
|
-
export { default as Input } from
|
|
4
|
-
export { default as Row } from
|
|
5
|
-
export { default as Center } from
|
|
6
|
-
export { default as Textarea } from
|
|
7
|
-
export { default as Checkbox } from
|
|
8
|
-
export { default as Toggle } from
|
|
9
|
-
export { default as RadioGroup } from
|
|
10
|
-
export { default as Toaster } from
|
|
11
|
-
export { default as Card } from
|
|
12
|
-
export { default as Modal } from
|
|
13
|
-
export { default as Select } from
|
|
14
|
-
export { default as SearchSelect } from
|
|
15
|
-
export { default as Dropdown } from
|
|
16
|
-
export { default as User } from
|
|
1
|
+
export { default as Button } from './Button.astro';
|
|
2
|
+
export { default as Divider } from './Divider.astro';
|
|
3
|
+
export { default as Input } from './Input.astro';
|
|
4
|
+
export { default as Row } from './Row.astro';
|
|
5
|
+
export { default as Center } from './Center.astro';
|
|
6
|
+
export { default as Textarea } from './Textarea.astro';
|
|
7
|
+
export { default as Checkbox } from './Checkbox.astro';
|
|
8
|
+
export { default as Toggle } from './Toggle.astro';
|
|
9
|
+
export { default as RadioGroup } from './RadioGroup.astro';
|
|
10
|
+
export { default as Toaster } from './Toast/Toaster.astro';
|
|
11
|
+
export { default as Card } from './Card.astro';
|
|
12
|
+
export { default as Modal } from './Modal/Modal.astro';
|
|
13
|
+
export { default as Select } from './Select.astro';
|
|
14
|
+
export { default as SearchSelect } from './SearchSelect.astro';
|
|
15
|
+
export { default as Dropdown } from './Dropdown/Dropdown.astro';
|
|
16
|
+
export { default as User } from './User.astro';
|
|
17
17
|
export { default as ThemeToggle } from './ThemeToggle.astro';
|
|
18
|
+
export { default as Footer } from './Footer.astro';
|
|
19
|
+
export { Tabs, TabItem } from './Tabs/index';
|
|
18
20
|
|
|
19
|
-
export { default as Sidebar } from
|
|
20
|
-
export { default as DoubleSidebar } from
|
|
21
|
-
export { SingleSidebarHelper, DoubleSidebarHelper } from
|
|
21
|
+
export { default as Sidebar } from './Sidebar/Single.astro';
|
|
22
|
+
export { default as DoubleSidebar } from './Sidebar/Double.astro';
|
|
23
|
+
export { SingleSidebarHelper, DoubleSidebarHelper } from './Sidebar/helpers';
|
|
22
24
|
|
|
23
|
-
export { toast } from
|
|
24
|
-
export { ModalHelper } from
|
|
25
|
-
export { DropdownHelper } from
|
|
25
|
+
export { toast } from './Toast/toast';
|
|
26
|
+
export { ModalHelper } from './Modal/modal';
|
|
27
|
+
export { DropdownHelper } from './Dropdown/dropdown';
|
package/src/components.ts
CHANGED
|
@@ -15,6 +15,8 @@ export { SearchSelect } from './components/index';
|
|
|
15
15
|
export { Dropdown, DropdownHelper } from './components/index';
|
|
16
16
|
export { User } from './components/index';
|
|
17
17
|
export { ThemeToggle } from './components/index';
|
|
18
|
+
export { Footer } from './components/index';
|
|
19
|
+
export { Tabs, TabItem } from './components/index';
|
|
18
20
|
|
|
19
21
|
export {
|
|
20
22
|
Sidebar,
|
package/src/css/colors.css
CHANGED
|
@@ -10,97 +10,97 @@
|
|
|
10
10
|
|
|
11
11
|
:root {
|
|
12
12
|
/* Default Styles (Dark Mode) */
|
|
13
|
-
--background-base: 0
|
|
14
|
-
--background-step-1: 0
|
|
15
|
-
--background-step-2: 0
|
|
16
|
-
--background-step-3: 0
|
|
13
|
+
--background-base: 0 0% 6%;
|
|
14
|
+
--background-step-1: 0 0% 8%;
|
|
15
|
+
--background-step-2: 0 0% 10%;
|
|
16
|
+
--background-step-3: 0 0% 14%;
|
|
17
17
|
|
|
18
|
-
--text-normal: 0
|
|
19
|
-
--text-muted: 0
|
|
20
|
-
--text-inverted: 0
|
|
18
|
+
--text-normal: 0 0% 100%;
|
|
19
|
+
--text-muted: 0 0% 54%;
|
|
20
|
+
--text-inverted: 0 0% 0%;
|
|
21
21
|
|
|
22
|
-
--border: 240
|
|
22
|
+
--border: 240 5% 17%;
|
|
23
23
|
|
|
24
|
-
--shadow: 0
|
|
24
|
+
--shadow: 0 0% 0%;
|
|
25
25
|
|
|
26
|
-
--default-base: 0
|
|
27
|
-
--default-hover: 0
|
|
28
|
-
--default-active: 0
|
|
26
|
+
--default-base: 0 0% 14%;
|
|
27
|
+
--default-hover: 0 0% 21%;
|
|
28
|
+
--default-active: 0 0% 15%;
|
|
29
29
|
|
|
30
|
-
--primary-base: 259
|
|
31
|
-
--primary-hover: 259
|
|
32
|
-
--primary-active: 259
|
|
30
|
+
--primary-base: 259 83% 73%;
|
|
31
|
+
--primary-hover: 259 77% 78%;
|
|
32
|
+
--primary-active: 259 73% 67%;
|
|
33
33
|
|
|
34
|
-
--success-base: 142
|
|
35
|
-
--success-hover: 142
|
|
36
|
-
--success-active: 142
|
|
34
|
+
--success-base: 142 71% 46%;
|
|
35
|
+
--success-hover: 142 72% 61%;
|
|
36
|
+
--success-active: 142 87% 52%;
|
|
37
37
|
|
|
38
|
-
--warning-base: 48
|
|
39
|
-
--warning-hover: 48
|
|
40
|
-
--warning-active: 48
|
|
38
|
+
--warning-base: 48 96% 53%;
|
|
39
|
+
--warning-hover: 48 97% 70%;
|
|
40
|
+
--warning-active: 48 100% 61%;
|
|
41
41
|
|
|
42
|
-
--danger-base: 339
|
|
43
|
-
--danger-hover: 337
|
|
44
|
-
--danger-active: 337
|
|
42
|
+
--danger-base: 339 97% 31%;
|
|
43
|
+
--danger-hover: 337 98% 37%;
|
|
44
|
+
--danger-active: 337 88% 32%;
|
|
45
45
|
|
|
46
46
|
/* Non-specific variables */
|
|
47
|
-
--text-light: 0
|
|
48
|
-
--text-dark: 0
|
|
47
|
+
--text-light: 0 0% 100%;
|
|
48
|
+
--text-dark: 0 0% 0%;
|
|
49
49
|
|
|
50
50
|
/* Flat colorways since they use variables */
|
|
51
|
-
--default-flat: var(--default-base)
|
|
52
|
-
--default-flat-hover: var(--default-base)
|
|
53
|
-
--default-flat-active: var(--default-base)
|
|
51
|
+
--default-flat: var(--default-base) / 0.5;
|
|
52
|
+
--default-flat-hover: var(--default-base) / 0.75;
|
|
53
|
+
--default-flat-active: var(--default-base) / 0.85;
|
|
54
54
|
|
|
55
|
-
--primary-flat: var(--primary-base)
|
|
56
|
-
--primary-flat-hover: var(--primary-base)
|
|
57
|
-
--primary-flat-active: var(--primary-base)
|
|
55
|
+
--primary-flat: var(--primary-base) / 0.1;
|
|
56
|
+
--primary-flat-hover: var(--primary-base) / 0.25;
|
|
57
|
+
--primary-flat-active: var(--primary-base) / 0.35;
|
|
58
58
|
|
|
59
|
-
--success-flat: var(--success-base)
|
|
60
|
-
--success-flat-hover: var(--success-base)
|
|
61
|
-
--success-flat-active: var(--success-base)
|
|
59
|
+
--success-flat: var(--success-base) / 0.1;
|
|
60
|
+
--success-flat-hover: var(--success-base) / 0.25;
|
|
61
|
+
--success-flat-active: var(--success-base) / 0.35;
|
|
62
62
|
|
|
63
|
-
--warning-flat: var(--warning-base)
|
|
64
|
-
--warning-flat-hover: var(--warning-base)
|
|
65
|
-
--warning-flat-active: var(--warning-base)
|
|
63
|
+
--warning-flat: var(--warning-base) / 0.1;
|
|
64
|
+
--warning-flat-hover: var(--warning-base) / 0.25;
|
|
65
|
+
--warning-flat-active: var(--warning-base) / 0.35;
|
|
66
66
|
|
|
67
|
-
--danger-flat: var(--danger-base)
|
|
68
|
-
--danger-flat-hover: var(--danger-base)
|
|
69
|
-
--danger-flat-active: var(--danger-base)
|
|
67
|
+
--danger-flat: var(--danger-base) / 0.1;
|
|
68
|
+
--danger-flat-hover: var(--danger-base) / 0.25;
|
|
69
|
+
--danger-flat-active: var(--danger-base) / 0.35;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
[data-theme="light"] {
|
|
73
73
|
/* Light Mode Styles */
|
|
74
|
-
--background-base: 0
|
|
75
|
-
--background-step-1: 0
|
|
76
|
-
--background-step-2: 0
|
|
77
|
-
--background-step-3: 0
|
|
74
|
+
--background-base: 0 0% 97%;
|
|
75
|
+
--background-step-1: 0 0% 90%;
|
|
76
|
+
--background-step-2: 0 0% 85%;
|
|
77
|
+
--background-step-3: 0 0% 80%;
|
|
78
78
|
|
|
79
|
-
--text-normal: 0
|
|
80
|
-
--text-muted: 0
|
|
81
|
-
--text-inverted: 0
|
|
79
|
+
--text-normal: 0 0% 0%;
|
|
80
|
+
--text-muted: 0 0% 24%;
|
|
81
|
+
--text-inverted: 0 0% 100%;
|
|
82
82
|
|
|
83
|
-
--border: 263
|
|
83
|
+
--border: 263 5% 68%;
|
|
84
84
|
|
|
85
|
-
--shadow: 0
|
|
85
|
+
--shadow: 0 0% 65%;
|
|
86
86
|
|
|
87
|
-
--default-base: 0
|
|
88
|
-
--default-hover: 0
|
|
89
|
-
--default-active: 0
|
|
87
|
+
--default-base: 0 0% 74%;
|
|
88
|
+
--default-hover: 0 0% 81%;
|
|
89
|
+
--default-active: 0 0% 91%;
|
|
90
90
|
|
|
91
|
-
--primary-base: 259
|
|
92
|
-
--primary-hover: 259
|
|
93
|
-
--primary-active: 259
|
|
91
|
+
--primary-base: 259 85% 61%;
|
|
92
|
+
--primary-hover: 259 78% 56%;
|
|
93
|
+
--primary-active: 259 71% 50%;
|
|
94
94
|
|
|
95
|
-
--success-base: 142
|
|
96
|
-
--success-hover: 142
|
|
97
|
-
--success-active: 142
|
|
95
|
+
--success-base: 142 59% 47%;
|
|
96
|
+
--success-hover: 142 62% 56%;
|
|
97
|
+
--success-active: 142 87% 59%;
|
|
98
98
|
|
|
99
|
-
--warning-base: 48
|
|
100
|
-
--warning-hover: 48
|
|
101
|
-
--warning-active: 48
|
|
99
|
+
--warning-base: 48 92% 46%;
|
|
100
|
+
--warning-hover: 48 88% 43%;
|
|
101
|
+
--warning-active: 48 85% 40%;
|
|
102
102
|
|
|
103
|
-
--danger-base: 339
|
|
104
|
-
--danger-hover: 337
|
|
105
|
-
--danger-active: 337
|
|
103
|
+
--danger-base: 339 97% 31%;
|
|
104
|
+
--danger-hover: 337 98% 37%;
|
|
105
|
+
--danger-active: 337 88% 45%;
|
|
106
106
|
}
|
package/src/css/resets.css
CHANGED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { AstroIntegration } from 'astro';
|
|
2
|
+
import { createResolver } from './utils/create-resolver';
|
|
3
|
+
|
|
4
|
+
// biome-ignore lint/complexity/noBannedTypes: Will be implemented in v0.3.0
|
|
5
|
+
type Options = {};
|
|
6
|
+
|
|
7
|
+
export default function integration(options: Options = {}): AstroIntegration {
|
|
8
|
+
const { resolve } = createResolver(import.meta.url);
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
name: '@studiocms/ui',
|
|
12
|
+
hooks: {
|
|
13
|
+
'astro:config:setup': ({ injectScript }) => {
|
|
14
|
+
injectScript('page-ssr', `import '${resolve('./css/global.css')}'`);
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
---
|
|
2
|
-
import '@fontsource-variable/onest/index.css';
|
|
3
2
|
import '../css/global.css';
|
|
4
3
|
import BaseHead from '../components/BaseHead.astro';
|
|
5
4
|
import type { HeadConfig } from '../utils/headers';
|
|
@@ -11,7 +10,7 @@ export interface RootLayoutProps {
|
|
|
11
10
|
headers?: HeadConfig | undefined;
|
|
12
11
|
lang?: string;
|
|
13
12
|
defaultTheme?: 'dark' | 'light';
|
|
14
|
-
}
|
|
13
|
+
}
|
|
15
14
|
|
|
16
15
|
type Props = RootLayoutProps;
|
|
17
16
|
|
package/src/types/index.ts
CHANGED
package/src/utils/ThemeHelper.ts
CHANGED
|
@@ -5,123 +5,141 @@ type ThemeChangeCallback = (newTheme: Theme, oldTheme: Theme) => void;
|
|
|
5
5
|
* A helper to toggle, set and get the current StudioCMS UI theme.
|
|
6
6
|
*/
|
|
7
7
|
class ThemeHelper {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
8
|
+
private themeManagerElement: HTMLElement;
|
|
9
|
+
private observer: MutationObserver | undefined;
|
|
10
|
+
private themeChangeCallbacks: ThemeChangeCallback[] = [];
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A helper to toggle, set and get the current StudioCMS UI theme.
|
|
14
|
+
* @param themeProvider The element that should carry the data-theme attribute (replaces the document root)
|
|
15
|
+
*/
|
|
16
|
+
constructor(themeProvider?: HTMLElement) {
|
|
17
|
+
this.themeManagerElement = themeProvider || document.documentElement;
|
|
18
|
+
this.themeManagerElement.dataset.theme = this.getTheme(true);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Get the current theme.
|
|
23
|
+
* @param {boolean} resolveSystemTheme Whether to resolve the `system` theme to the actual theme (`dark` or `light`)
|
|
24
|
+
* @returns {Theme} The current theme.
|
|
25
|
+
*/
|
|
26
|
+
public getTheme = <T extends boolean>(
|
|
27
|
+
resolveSystemTheme?: T
|
|
28
|
+
): T extends true ? 'dark' | 'light' : Theme => {
|
|
29
|
+
const theme = (this.themeManagerElement.dataset.theme as Theme) || 'system';
|
|
30
|
+
|
|
31
|
+
if (!resolveSystemTheme) {
|
|
32
|
+
// Side note: Don't ask me why this type wizardry is needed but it gives proper return types so I don't care
|
|
33
|
+
return theme as T extends true ? 'dark' | 'light' : Theme;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if ((this.themeManagerElement.dataset.theme ?? 'system') !== 'system') {
|
|
37
|
+
return this.themeManagerElement.dataset.theme as 'dark' | 'light';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
|
41
|
+
return 'dark';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (window.matchMedia('(prefers-color-scheme: light)').matches) {
|
|
45
|
+
return 'light';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// This should (in theory) never happen since, at time of writing, window.matchMedia is supported
|
|
49
|
+
// by 96.83% of all browsers in use. (https://caniuse.com/mdn-api_window_matchmedia)
|
|
50
|
+
throw new Error(
|
|
51
|
+
'Unable to resolve theme. (Most likely cause: window.matchMedia is not supported by the browser)'
|
|
52
|
+
);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Sets the current theme.
|
|
57
|
+
* @param theme The new theme. One of `dark`, `light` or `system`.
|
|
58
|
+
*/
|
|
59
|
+
public setTheme = (theme: Theme): void => {
|
|
60
|
+
// Assign the new theme to the dataset
|
|
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
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Toggles the current theme.
|
|
71
|
+
*
|
|
72
|
+
* If the theme is set to `system` (or no theme is set via the root element),
|
|
73
|
+
* the theme is set depending on the user's color scheme preference (set in the browser).
|
|
74
|
+
*/
|
|
75
|
+
public toggleTheme = (): void => {
|
|
76
|
+
const theme = this.getTheme();
|
|
77
|
+
|
|
78
|
+
if (theme && theme === 'dark') {
|
|
79
|
+
this.setTheme('light');
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (theme && theme === 'light') {
|
|
84
|
+
this.setTheme('dark');
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
|
89
|
+
this.setTheme('light');
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (window.matchMedia('(prefers-color-scheme: light)').matches) {
|
|
94
|
+
this.setTheme('dark');
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Register an element to act as a toggle! When clicked, it will toggle the theme.
|
|
101
|
+
* @param toggle The HTML element that should act as the toggle
|
|
102
|
+
*/
|
|
103
|
+
public registerToggle = (toggle: HTMLElement | null): void => {
|
|
104
|
+
if (!toggle) {
|
|
105
|
+
console.error('Element passed to toggle registration does not exist.');
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!toggle.dataset.suiThemeToggle) {
|
|
110
|
+
toggle.addEventListener('click', this.toggleTheme);
|
|
111
|
+
toggle.dataset.suiThemeToggle = 'true';
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Allows for adding a callback that gets called whenever the theme changes.
|
|
117
|
+
* @param callback The callback to be executed
|
|
118
|
+
*/
|
|
119
|
+
public onThemeChange = (callback: ThemeChangeCallback): void => {
|
|
120
|
+
if (!this.observer) {
|
|
121
|
+
this.observer = new MutationObserver(this.themeManagerMutationHandler);
|
|
122
|
+
this.observer.observe(this.themeManagerElement, {
|
|
123
|
+
attributes: true,
|
|
124
|
+
attributeOldValue: true,
|
|
125
|
+
attributeFilter: ['data-theme'],
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
this.themeChangeCallbacks.push(callback);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Simply gets the first mutation and calls all registered callbacks.
|
|
134
|
+
* @param mutations The mutations array from the observer. Due to the specified options, this will always be a 1-length array,
|
|
135
|
+
*/
|
|
136
|
+
private themeManagerMutationHandler = (mutations: MutationRecord[]): void => {
|
|
137
|
+
if (!mutations[0]) return;
|
|
138
|
+
|
|
139
|
+
for (const callback of this.themeChangeCallbacks) {
|
|
140
|
+
callback(this.getTheme(), (mutations[0].oldValue as Theme) || 'system');
|
|
141
|
+
}
|
|
142
|
+
};
|
|
125
143
|
}
|
|
126
144
|
|
|
127
145
|
export { ThemeHelper, type Theme };
|