@studiocms/ui 0.1.0 → 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.
@@ -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 class="sui-toggle-switch" />
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-container:has(.sui-checkbox:checked) .sui-toggle-switch {
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">
@@ -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,
@@ -24,24 +24,24 @@
24
24
  --shadow: 0 0% 0%;
25
25
 
26
26
  --default-base: 0 0% 14%;
27
- --default-hover: 0 0% 15%;
28
- --default-active: 0 0% 19%;
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% 71%;
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% 52%;
36
- --success-active: 142 87% 59%;
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% 61%;
40
- --warning-active: 48 100% 71%;
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% 45%;
44
+ --danger-active: 337 88% 32%;
45
45
 
46
46
  /* Non-specific variables */
47
47
  --text-light: 0 0% 100%;
@@ -19,7 +19,6 @@ html[data-theme="light"] {
19
19
  }
20
20
 
21
21
  body {
22
- font-family: "Onest Variable", sans-serif;
23
22
  -webkit-font-smoothing: antialiased;
24
23
  color: hsl(var(--text-normal));
25
24
  background-color: hsl(var(--background-base));
@@ -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';
@@ -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 { dirname, resolve } from 'node:path'; // Swapped out pathe for node:path to cut down on dependencies
2
+ import { fileURLToPath } from 'node:url';
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
+ };