@refrakt-md/svelte 0.12.0 → 0.14.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/package.json +4 -4
- package/src/ThemeToggle.svelte +147 -0
- package/src/index.ts +1 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@refrakt-md/svelte",
|
|
3
3
|
"description": "Svelte renderer for refrakt.md content",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.14.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -27,9 +27,9 @@
|
|
|
27
27
|
],
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@markdoc/markdoc": "0.4.0",
|
|
30
|
-
"@refrakt-md/behaviors": "0.
|
|
31
|
-
"@refrakt-md/transform": "0.
|
|
32
|
-
"@refrakt-md/types": "0.
|
|
30
|
+
"@refrakt-md/behaviors": "0.14.0",
|
|
31
|
+
"@refrakt-md/transform": "0.14.0",
|
|
32
|
+
"@refrakt-md/types": "0.14.0"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
35
|
"svelte": "^5.0.0"
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount } from 'svelte';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Theme toggle — three-state button cycling auto → light → dark → auto.
|
|
6
|
+
*
|
|
7
|
+
* Reads and writes the `rf-theme` localStorage key in lockstep with the
|
|
8
|
+
* canonical pre-paint script (see `prePaintScript()` in
|
|
9
|
+
* `@refrakt-md/content`). On click, applies the new value as
|
|
10
|
+
* `document.documentElement.dataset.theme` so the change takes effect
|
|
11
|
+
* immediately without a page reload.
|
|
12
|
+
*
|
|
13
|
+
* **Locked pages** (`<html data-tint-lock="true">`, set by the cascade
|
|
14
|
+
* SSR per SPEC-052) hide the toggle entirely — the saved preference is
|
|
15
|
+
* preserved in localStorage but ignored while the lock is active, and
|
|
16
|
+
* surfacing a toggle that does nothing would be confusing.
|
|
17
|
+
*
|
|
18
|
+
* Used by any refrakt site rendering through `@refrakt-md/svelte`. The
|
|
19
|
+
* default rendering is a small inline button labelled with an icon and
|
|
20
|
+
* a tooltip; override via `class` or a `children` snippet for custom
|
|
21
|
+
* presentation while keeping the behaviour.
|
|
22
|
+
*/
|
|
23
|
+
type ThemePref = 'auto' | 'light' | 'dark';
|
|
24
|
+
|
|
25
|
+
let {
|
|
26
|
+
class: className = '',
|
|
27
|
+
children,
|
|
28
|
+
}: { class?: string; children?: import('svelte').Snippet<[{ pref: ThemePref }]> } = $props();
|
|
29
|
+
|
|
30
|
+
let pref = $state<ThemePref>('auto');
|
|
31
|
+
let locked = $state(false);
|
|
32
|
+
let mounted = $state(false);
|
|
33
|
+
|
|
34
|
+
onMount(() => {
|
|
35
|
+
mounted = true;
|
|
36
|
+
locked = document.documentElement.dataset.tintLock === 'true';
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const saved = localStorage.getItem('rf-theme') as ThemePref | null;
|
|
40
|
+
if (saved === 'light' || saved === 'dark' || saved === 'auto') {
|
|
41
|
+
pref = saved;
|
|
42
|
+
}
|
|
43
|
+
} catch (_) {
|
|
44
|
+
// localStorage may be unavailable (private mode, file://, etc.).
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Watch for changes to data-tint-lock during client-side navigation —
|
|
48
|
+
// SvelteKit may rewrite this attribute when navigating between pages
|
|
49
|
+
// with different cascade states.
|
|
50
|
+
const observer = new MutationObserver(() => {
|
|
51
|
+
locked = document.documentElement.dataset.tintLock === 'true';
|
|
52
|
+
});
|
|
53
|
+
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['data-tint-lock'] });
|
|
54
|
+
return () => observer.disconnect();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
function cycle() {
|
|
58
|
+
const next: ThemePref = pref === 'auto' ? 'light' : pref === 'light' ? 'dark' : 'auto';
|
|
59
|
+
pref = next;
|
|
60
|
+
try {
|
|
61
|
+
localStorage.setItem('rf-theme', next);
|
|
62
|
+
} catch (_) {
|
|
63
|
+
// Persist failures are silent — UI still works in-tab.
|
|
64
|
+
}
|
|
65
|
+
applyPref(next);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function applyPref(p: ThemePref) {
|
|
69
|
+
const d = document.documentElement;
|
|
70
|
+
if (p === 'auto') {
|
|
71
|
+
// Remove the explicit attribute; the @media (prefers-color-scheme)
|
|
72
|
+
// rule takes over.
|
|
73
|
+
delete d.dataset.theme;
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
d.dataset.theme = p;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const label = $derived(
|
|
80
|
+
pref === 'auto' ? 'Theme: auto (system)' : pref === 'light' ? 'Theme: light' : 'Theme: dark',
|
|
81
|
+
);
|
|
82
|
+
</script>
|
|
83
|
+
|
|
84
|
+
{#if mounted && !locked}
|
|
85
|
+
<button
|
|
86
|
+
type="button"
|
|
87
|
+
class="rf-theme-toggle {className}"
|
|
88
|
+
data-theme-pref={pref}
|
|
89
|
+
aria-label={label}
|
|
90
|
+
title={label}
|
|
91
|
+
onclick={cycle}
|
|
92
|
+
>
|
|
93
|
+
{#if children}
|
|
94
|
+
{@render children({ pref })}
|
|
95
|
+
{:else}
|
|
96
|
+
<span aria-hidden="true" class="rf-theme-toggle__icon rf-theme-toggle__icon--{pref}"></span>
|
|
97
|
+
{/if}
|
|
98
|
+
</button>
|
|
99
|
+
{/if}
|
|
100
|
+
|
|
101
|
+
<style>
|
|
102
|
+
.rf-theme-toggle {
|
|
103
|
+
display: inline-flex;
|
|
104
|
+
align-items: center;
|
|
105
|
+
justify-content: center;
|
|
106
|
+
width: 2rem;
|
|
107
|
+
height: 2rem;
|
|
108
|
+
padding: 0;
|
|
109
|
+
border: 1px solid var(--rf-color-border, transparent);
|
|
110
|
+
border-radius: var(--rf-radius-md, 8px);
|
|
111
|
+
background: transparent;
|
|
112
|
+
color: var(--rf-color-text);
|
|
113
|
+
cursor: pointer;
|
|
114
|
+
transition: background-color 120ms ease, border-color 120ms ease;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.rf-theme-toggle:hover {
|
|
118
|
+
background: var(--rf-color-surface-hover, rgba(0, 0, 0, 0.04));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.rf-theme-toggle:focus-visible {
|
|
122
|
+
outline: 2px solid var(--rf-color-primary);
|
|
123
|
+
outline-offset: 2px;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.rf-theme-toggle__icon {
|
|
127
|
+
display: inline-block;
|
|
128
|
+
width: 1rem;
|
|
129
|
+
height: 1rem;
|
|
130
|
+
background: currentColor;
|
|
131
|
+
mask-size: contain;
|
|
132
|
+
mask-repeat: no-repeat;
|
|
133
|
+
mask-position: center;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.rf-theme-toggle__icon--auto {
|
|
137
|
+
mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><circle cx='12' cy='12' r='10'/><path d='M12 2a10 10 0 0 0 0 20Z' fill='currentColor'/></svg>");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.rf-theme-toggle__icon--light {
|
|
141
|
+
mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><circle cx='12' cy='12' r='4'/><path d='M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41'/></svg>");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.rf-theme-toggle__icon--dark {
|
|
145
|
+
mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z'/></svg>");
|
|
146
|
+
}
|
|
147
|
+
</style>
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { default as Renderer } from './Renderer.svelte';
|
|
2
2
|
export { default as ThemeShell } from './ThemeShell.svelte';
|
|
3
|
+
export { default as ThemeToggle } from './ThemeToggle.svelte';
|
|
3
4
|
export type { SerializedTag, RendererNode } from './types.js';
|
|
4
5
|
export { serialize, serializeTree } from './serialize.js';
|
|
5
6
|
export { setRegistry, getComponent, setElementOverrides, getElementOverrides } from './context.js';
|