-
-
Notifications
You must be signed in to change notification settings - Fork 21
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'issue-0304' into add-sentry
- Loading branch information
Showing
12 changed files
with
362 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"@studiocms/ui": patch | ||
--- | ||
|
||
Added a theme helper and theme toggle component |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
--- | ||
import type { ComponentProps } from "astro/types"; | ||
import Button from "./Button.astro"; | ||
interface Props extends ComponentProps<typeof Button> {}; | ||
const props = Astro.props; | ||
--- | ||
|
||
<Button id='sui__theme-toggle' {...props}> | ||
<div id="dark-content"> | ||
<slot name="dark" /> | ||
</div> | ||
<div id="light-content"> | ||
<slot name="light" /> | ||
</div> | ||
</Button> | ||
|
||
<script> | ||
import { ThemeHelper } from '../utils/ThemeHelper'; | ||
|
||
const themeToggle = document.getElementById('sui__theme-toggle'); | ||
const themeHelper = new ThemeHelper(); | ||
|
||
themeHelper.registerToggle(themeToggle); | ||
</script> | ||
|
||
<style is:global> | ||
#sui__theme-toggle #dark-content, #sui__theme-toggle #light-content { | ||
display: none; | ||
} | ||
|
||
[data-theme="dark"] #sui__theme-toggle #dark-content { | ||
display: block; | ||
} | ||
|
||
[data-theme="light"] #sui__theme-toggle #light-content { | ||
display: block; | ||
} | ||
</style> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
type Theme = 'dark' | 'light' | 'system'; | ||
type ThemeChangeCallback = (newTheme: Theme, oldTheme: Theme) => void; | ||
|
||
/** | ||
* A helper to toggle, set and get the current StudioCMS UI theme. | ||
*/ | ||
class ThemeHelper { | ||
private themeManagerElement: HTMLElement; | ||
private observer: MutationObserver | undefined; | ||
private themeChangeCallbacks: ThemeChangeCallback[] = []; | ||
|
||
/** | ||
* A helper to toggle, set and get the current StudioCMS UI theme. | ||
* @param themeProvider The element that should carry the data-theme attribute (replaces the document root) | ||
*/ | ||
constructor(themeProvider?: HTMLElement) { | ||
this.themeManagerElement = themeProvider || document.documentElement; | ||
} | ||
|
||
/** | ||
* Get the current theme. | ||
* @param {boolean} resolveSystemTheme Whether to resolve the `system` theme to the actual theme (`dark` or `light`) | ||
* @returns {Theme} The current theme. | ||
*/ | ||
public getTheme = <T extends boolean>(resolveSystemTheme?: T): T extends true ? 'dark' | 'light' : Theme => { | ||
const theme = this.themeManagerElement.dataset.theme as Theme || 'system'; | ||
|
||
if (!resolveSystemTheme) { | ||
// Side note: Don't ask me why this type wizardry is needed but it gives proper return types so I don't care | ||
return theme as T extends true ? 'dark' | 'light' : Theme; | ||
} | ||
|
||
if (this.themeManagerElement.dataset.theme !== 'system') { | ||
return this.themeManagerElement.dataset.theme as 'dark' | 'light'; | ||
} | ||
|
||
if (window.matchMedia('(prefers-color-scheme: dark)').matches) { | ||
return 'dark'; | ||
} | ||
|
||
if (window.matchMedia('(prefers-color-scheme: light)').matches) { | ||
return 'light'; | ||
} | ||
|
||
// This should (in theory) never happen since, at time of writing, window.matchMedia is supported | ||
// by 96.83% of all browsers in use. (https://caniuse.com/mdn-api_window_matchmedia) | ||
throw new Error('Unable to resolve theme. (Most likely cause: window.matchMedia is not supported by the browser)'); | ||
}; | ||
|
||
/** | ||
* Sets the current theme. | ||
* @param theme The new theme. One of `dark`, `light` or `system`. | ||
*/ | ||
public setTheme = (theme: Theme): void => { | ||
this.themeManagerElement.dataset.theme = theme; | ||
}; | ||
|
||
/** | ||
* Toggles the current theme. | ||
* | ||
* If the theme is set to `system` (or no theme is set via the root element), | ||
* the theme is set depending on the user's color scheme preference (set in the browser). | ||
*/ | ||
public toggleTheme = (): void => { | ||
const theme = this.getTheme(); | ||
|
||
if (theme === 'dark') { | ||
this.setTheme('light'); | ||
return; | ||
} | ||
|
||
if (theme === 'light') { | ||
this.setTheme('dark'); | ||
return; | ||
} | ||
|
||
if (window.matchMedia('(prefers-color-scheme: dark)').matches) { | ||
this.setTheme('light'); | ||
return; | ||
} | ||
|
||
if (window.matchMedia('(prefers-color-scheme: light)').matches) { | ||
this.setTheme('dark'); | ||
return; | ||
} | ||
}; | ||
|
||
/** | ||
* Register an element to act as a toggle! When clicked, it will toggle the theme. | ||
* @param toggle The HTML element that should act as the toggle | ||
*/ | ||
public registerToggle = (toggle: HTMLElement | null): void => { | ||
if (!toggle) { | ||
console.error('Element passed to toggle registration does not exist.'); | ||
return; | ||
}; | ||
|
||
toggle.addEventListener('click', this.toggleTheme); | ||
}; | ||
|
||
/** | ||
* Allows for adding a callback that gets called whenever the theme changes, | ||
* @param callback The callback to be executed | ||
*/ | ||
public onThemeChange = (callback: ThemeChangeCallback): void => { | ||
if (!this.observer) { | ||
this.observer = new MutationObserver(this.themeManagerMutationHandler); | ||
this.observer.observe(this.themeManagerElement, { attributes: true, attributeOldValue: true, attributeFilter: ['data-theme'] }); | ||
} | ||
|
||
this.themeChangeCallbacks.push(callback); | ||
}; | ||
|
||
/** | ||
* Simply gets the first mutation and calls all registered callbacks. | ||
* @param mutations The mutations array from the observer. Due to the specified options, this will always be a 1-length array, | ||
*/ | ||
private themeManagerMutationHandler = (mutations: MutationRecord[]): void => { | ||
if (!mutations[0]) return; | ||
|
||
for (const callback of this.themeChangeCallbacks) { | ||
callback(this.getTheme(), mutations[0].oldValue as Theme || 'system'); | ||
} | ||
}; | ||
} | ||
|
||
export { ThemeHelper, type Theme }; |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
<script> | ||
import { ThemeHelper } from '@studiocms/ui/utils/ThemeHelper.ts'; | ||
|
||
const themeHelper = new ThemeHelper(); | ||
|
||
const outputSpan = document.querySelector<HTMLSpanElement>('#theme-listener-output')!; | ||
|
||
themeHelper.onThemeChange((newTheme, oldTheme) => { | ||
outputSpan.textContent = `Theme is now: ${newTheme}! (Before: ${oldTheme})`; | ||
}); | ||
</script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
113 changes: 113 additions & 0 deletions
113
www/docs/src/content/docs/customizing/studiocms-ui/components/theme-helper.mdx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
--- | ||
title: Theme Helper | ||
--- | ||
import { Tabs, TabItem } from '@astrojs/starlight/components'; | ||
import PreviewCard from '~/components/PreviewCard.astro'; | ||
import ThemeHelperScript from '~/components/ThemeHelperScript.astro'; | ||
|
||
To make managing the active theme easier, we provide a helper class to get, set and toggle the theme. Additionally, | ||
you can provide callbacks for when the theme gets changed! | ||
|
||
## Usage | ||
|
||
:::caution | ||
The `ThemeHelper` can only be used client-side (in `<script>` tags). | ||
::: | ||
```ts title="script.ts" twoslash | ||
import { ThemeHelper } from '@studiocms/ui/utils/ThemeHelper.ts'; | ||
|
||
// Instanciate a new helper | ||
const themeHelper = new ThemeHelper(); | ||
|
||
// Get the current theme. (One of `dark`, `light` or `system`) | ||
const theme = themeHelper.getTheme(); | ||
|
||
// Get the current theme but resolve the actual theme if `system` is selected | ||
// @noErrors | ||
const resolvedTheme = themeHelper.getTheme(true); | ||
|
||
// Set the theme to light | ||
themeHelper.setTheme('light'); | ||
|
||
// Toggle the theme | ||
themeHelper.toggleTheme(); | ||
|
||
// Register an element that should act as a toggle | ||
const toggleButton = document.querySelector<HTMLButtonElement>('#toggle-button'); | ||
themeHelper.registerToggle(toggleButton); | ||
``` | ||
|
||
### Listening for theme changes | ||
|
||
Using the `ThemeHelper` class, you can listen to theme changes! This is useful when you have logic that needs to run | ||
whenever the color scheme changes, for example in a `three.js` canvas where you need to change an image | ||
(*\*cough cough\* our login page \*cough\**). | ||
|
||
```ts title="script.ts" twoslash | ||
import { ThemeHelper } from '@studiocms/ui/utils/ThemeHelper.ts'; | ||
|
||
// Instanciate a new helper | ||
const themeHelper = new ThemeHelper(); | ||
|
||
// Add a callback that gets called when the theme changes | ||
themeHelper.onThemeChange((newTheme) => { | ||
// Your logic here! | ||
}); | ||
``` | ||
|
||
Here's a live example: Change the theme via the option in the top-right corner of your screen. If you're on mobile, | ||
open the navbar and scroll all the way down. After you've changed the theme, check the text below: | ||
|
||
<Tabs> | ||
<TabItem label="Preview"> | ||
<PreviewCard> | ||
<span id="theme-listener-output"> | ||
Theme hasn't changed yet. | ||
</span> | ||
</PreviewCard> | ||
</TabItem> | ||
<TabItem label="Code"> | ||
```html | ||
<span id="theme-listener-output"> | ||
Theme hasn't changed yet. | ||
</span> | ||
``` | ||
```ts twoslash | ||
import { ThemeHelper, type Theme } from '@studiocms/ui/utils/ThemeHelper.ts'; | ||
|
||
// Instanciate a new helper | ||
const themeHelper = new ThemeHelper(); | ||
|
||
const outputSpan = document.querySelector<HTMLSpanElement>('#theme-listener-output')!; | ||
|
||
// Add a callback that gets called when the theme changes | ||
// @noErrors | ||
themeHelper.onThemeChange((newTheme: Theme, oldTheme: Theme) => { | ||
// Your logic here! | ||
outputSpan.textContent = `Theme is now: ${newTheme}! (Before: ${oldTheme})`; | ||
}); | ||
``` | ||
</TabItem> | ||
</Tabs> | ||
|
||
Since `@studiocms/ui` is compatible with Starlight's theme system, this even picks up on those changes. | ||
|
||
### Remembering a users theme selection | ||
|
||
One of the few things the `ThemeHelper` does not do is saving the users theme preference. This is by design, since we don't want to force websites operating in the EU (and other GDPR-enforcing countries) to have to add a cookie notice just for a UI library. Instead, implementation of this functionality is up to the developers themselves. | ||
|
||
As a starting point, here's a barebones example of how to implement this: | ||
|
||
```ts twoslash | ||
import { ThemeHelper, type Theme } from '@studiocms/ui/utils/ThemeHelper.ts'; | ||
|
||
const themeHelper = new ThemeHelper(); | ||
|
||
themeHelper.onThemeChange((newTheme: Theme) => { | ||
localStorage.setItem('theme-selection', newTheme); | ||
}) | ||
``` | ||
|
||
If you want to go even further, you can store this information in a cookie to retrieve it server-side. | ||
|
||
<ThemeHelperScript /> |
Oops, something went wrong.