Add synced themes, color modes and custom CSS snippets
This commit is contained in:
1 parent
1562519f08
commit
7f65162115
17 files changed
+766
-36
No files matched your search
@@ -0,0 +1,75 @@
|
||||
export interface CssSnippet {
|
||||
id: string
|
||||
name: string
|
||||
css: string
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
export interface Appearance {
|
||||
colorMode: 'system' | 'light' | 'dark'
|
||||
theme: string
|
||||
cssSnippets: CssSnippet[]
|
||||
}
|
||||
|
||||
export const appearancePaused = new URLSearchParams(location.search).has('reset-appearance')
|
||||
|
||||
export function sanitizeAppearance(raw: Record<string, unknown>): Appearance {
|
||||
return {
|
||||
colorMode: raw.colorMode === 'light' || raw.colorMode === 'dark' ? raw.colorMode : 'system',
|
||||
theme: typeof raw.theme === 'string' && /^[a-z0-9][a-z0-9_-]{0,63}$/.test(raw.theme) ? raw.theme : 'default',
|
||||
cssSnippets: Array.isArray(raw.cssSnippets) ? raw.cssSnippets.filter((s): s is CssSnippet =>
|
||||
s !== null && typeof s === 'object' && typeof s.id === 'string' && typeof s.name === 'string' &&
|
||||
typeof s.css === 'string' && typeof s.enabled === 'boolean',
|
||||
).map((s) => ({ id: s.id, name: s.name, css: s.css, enabled: s.enabled })) : [],
|
||||
}
|
||||
}
|
||||
|
||||
let current: Appearance = { colorMode: 'system', theme: 'default', cssSnippets: [] }
|
||||
let themeCSS = ''
|
||||
const darkMode = matchMedia('(prefers-color-scheme: dark)')
|
||||
let rendered = ''
|
||||
|
||||
function style(id: string, css: string) {
|
||||
let el = document.getElementById(id) as HTMLStyleElement | null
|
||||
if (!el) {
|
||||
el = document.createElement('style')
|
||||
el.id = id
|
||||
document.head.append(el)
|
||||
}
|
||||
if (el.textContent !== css) el.textContent = css
|
||||
}
|
||||
|
||||
function render() {
|
||||
const mode = appearancePaused ? 'system' : current.colorMode
|
||||
const dark = mode === 'system' ? darkMode.matches : mode === 'dark'
|
||||
const signature = JSON.stringify([dark, appearancePaused ? '' : [current.theme, themeCSS, current.cssSnippets]])
|
||||
if (signature === rendered) return
|
||||
rendered = signature
|
||||
const root = document.documentElement
|
||||
root.dataset.rmColorMode = dark ? 'dark' : 'light'
|
||||
root.dataset.rmTheme = appearancePaused ? 'default' : current.theme
|
||||
style('rm-theme', appearancePaused ? '' : themeCSS)
|
||||
// 独立样式元素避免某个片段的未闭合语法吞掉其他片段。
|
||||
document.querySelectorAll('style[data-rm-snippet]').forEach((el) => el.remove())
|
||||
if (!appearancePaused) for (const snippet of current.cssSnippets) {
|
||||
if (!snippet.enabled) continue
|
||||
const el = document.createElement('style')
|
||||
el.dataset.rmSnippet = snippet.id
|
||||
el.textContent = snippet.css
|
||||
document.head.append(el)
|
||||
}
|
||||
const bg = getComputedStyle(root).getPropertyValue('--rm-bg').trim()
|
||||
for (const meta of document.querySelectorAll<HTMLMetaElement>('meta[name="theme-color"]')) {
|
||||
meta.removeAttribute('media')
|
||||
meta.content = bg
|
||||
}
|
||||
window.dispatchEvent(new Event('rawmark-appearance'))
|
||||
}
|
||||
|
||||
darkMode.addEventListener('change', render)
|
||||
|
||||
export function applyAppearance(next: Appearance, css: string) {
|
||||
current = next
|
||||
themeCSS = css
|
||||
render()
|
||||
}
|
||||
+40
-1
@@ -3,6 +3,16 @@
|
||||
* 代码块与行内代码配色取自 vuejs.org(@vue/theme + Shiki github-dark)。
|
||||
*/
|
||||
:root {
|
||||
color-scheme: light;
|
||||
--rm-sidebar-bg: #f9f9f9;
|
||||
--rm-sidebar-width: 260px;
|
||||
--rm-hover: rgba(0, 0, 0, 0.045);
|
||||
--rm-active: rgba(66, 184, 131, 0.14);
|
||||
--rm-danger: #e5484d;
|
||||
--rm-warn-bg: #fff8e6;
|
||||
--rm-warn-text: #8a5a00;
|
||||
--rm-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||
|
||||
--rm-font-text: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'PingFang SC', 'Hiragino Sans GB',
|
||||
'Microsoft YaHei', 'Noto Sans CJK SC', 'Helvetica Neue', sans-serif;
|
||||
--rm-font-mono: ui-monospace, 'SF Mono', 'SFMono-Regular', Menlo, Monaco, 'Cascadia Mono', Consolas,
|
||||
@@ -27,8 +37,30 @@
|
||||
--rm-highlight-bg: rgba(255, 213, 0, 0.35);
|
||||
}
|
||||
|
||||
:root:where([data-rm-color-mode="dark"]) {
|
||||
color-scheme: dark;
|
||||
--rm-bg: #1a1a1a;
|
||||
--rm-text: rgba(255, 255, 255, 0.87);
|
||||
--rm-text-muted: rgba(235, 235, 235, 0.6);
|
||||
--rm-divider: rgba(84, 84, 84, 0.48);
|
||||
--rm-accent: #42d392;
|
||||
--rm-accent-strong: #42b883;
|
||||
--rm-inline-code-bg: #2f2f2f;
|
||||
--rm-inline-code-text: #aac8e4;
|
||||
--rm-code-bg: #242424;
|
||||
--rm-highlight-bg: rgba(255, 213, 0, 0.28);
|
||||
|
||||
--rm-sidebar-bg: #161618;
|
||||
--rm-hover: rgba(255, 255, 255, 0.06);
|
||||
--rm-active: rgba(66, 211, 146, 0.16);
|
||||
--rm-warn-bg: #2f2716;
|
||||
--rm-warn-text: #f0c060;
|
||||
--rm-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
:root:where(:not([data-rm-color-mode])) {
|
||||
color-scheme: dark;
|
||||
--rm-bg: #1a1a1a;
|
||||
--rm-text: rgba(255, 255, 255, 0.87);
|
||||
--rm-text-muted: rgba(235, 235, 235, 0.6);
|
||||
@@ -39,6 +71,13 @@
|
||||
--rm-inline-code-text: #aac8e4;
|
||||
--rm-code-bg: #242424;
|
||||
--rm-highlight-bg: rgba(255, 213, 0, 0.28);
|
||||
|
||||
--rm-sidebar-bg: #161618;
|
||||
--rm-hover: rgba(255, 255, 255, 0.06);
|
||||
--rm-active: rgba(66, 211, 146, 0.16);
|
||||
--rm-warn-bg: #2f2716;
|
||||
--rm-warn-text: #f0c060;
|
||||
--rm-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user