mirror of
https://github.com/sergeyyarkov/yarkov.tech.git
synced 2026-10-11 02:19:08 +03:00
feat: toggle theme for code highlighting in article content
This commit is contained in:
parent
2ea0c75258
commit
c3ac7c6c14
@ -5,14 +5,13 @@
|
|||||||
* and body markup to prevent FOUC (Flash of unstyled content).
|
* and body markup to prevent FOUC (Flash of unstyled content).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const getTheme = () => {
|
window.theme = (() => {
|
||||||
const local = localStorage.getItem('theme');
|
const local = localStorage.getItem('theme');
|
||||||
if (local && (local === 'dark' || local === 'light')) return local;
|
if (local && (local === 'dark' || local === 'light')) return local;
|
||||||
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
return isDark ? 'dark' : 'light';
|
return isDark ? 'dark' : 'light';
|
||||||
};
|
})();
|
||||||
|
|
||||||
window.theme = getTheme();
|
|
||||||
document.documentElement.setAttribute('data-theme', window.theme);
|
document.documentElement.setAttribute('data-theme', window.theme);
|
||||||
|
|
||||||
/* Sync with OS theme */
|
/* Sync with OS theme */
|
||||||
|
|||||||
48
public/assets/js/load-code-highlight.js
Normal file
48
public/assets/js/load-code-highlight.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
/**
|
||||||
|
* This script will load the properly code highlithing theme based on user theme
|
||||||
|
* It is important to insert it after detecting a user theme
|
||||||
|
* and assigning the `window.theme` property
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (window.theme) {
|
||||||
|
/* the {theme} in the path will be dynamically changed */
|
||||||
|
const PATH = `assets/css/prism-one-{theme}.min.css`;
|
||||||
|
const $head = document.getElementsByTagName('head')[0];
|
||||||
|
const getPath = (theme) => PATH.replace(`{theme}`, theme);
|
||||||
|
const createLink = (theme, media) => {
|
||||||
|
const $link = document.createElement('link');
|
||||||
|
$link.rel = 'stylesheet';
|
||||||
|
$link.type = 'text/css';
|
||||||
|
$link.href = getPath(theme);
|
||||||
|
$link.media = media;
|
||||||
|
return $link;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* init links to load them first */
|
||||||
|
const $light = createLink('light', 'none');
|
||||||
|
const $dark = createLink('dark', 'none');
|
||||||
|
|
||||||
|
const toggleMedia = (theme) => {
|
||||||
|
switch (theme) {
|
||||||
|
case 'light':
|
||||||
|
$light.media = 'all';
|
||||||
|
$dark.media = 'none';
|
||||||
|
break;
|
||||||
|
case 'dark':
|
||||||
|
$light.media = 'none';
|
||||||
|
$dark.media = 'all';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
toggleMedia(window.theme);
|
||||||
|
$head.appendChild($light);
|
||||||
|
$head.appendChild($dark);
|
||||||
|
|
||||||
|
window.addEventListener('onthemetoggled', ({ detail: newTheme }) => toggleMedia(newTheme));
|
||||||
|
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', ({ matches: isDark }) => {
|
||||||
|
return isDark ? toggleMedia('dark') : toggleMedia('light');
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -1,12 +1,12 @@
|
|||||||
import type { Component } from 'solid-js';
|
import { Component } from 'solid-js';
|
||||||
|
|
||||||
import { createSignal, createEffect, For } from 'solid-js';
|
import { createSignal, createEffect, For } from 'solid-js';
|
||||||
import { search, selectedTags } from '@stores/searchStore';
|
import { search, selectedTags } from '@stores/searchStore';
|
||||||
|
import { findArticlesBySearch, findArticlesByTags, sortArtcilesByDate } from '@utils/articles';
|
||||||
import { ArticleItem } from '@components/ArticleItem';
|
import { ArticleItem } from '@components/ArticleItem';
|
||||||
import * as utils from '@utils/articles';
|
import SearchInput from '@components/SearchInput';
|
||||||
|
import TagsList from '@components/TagsList';
|
||||||
import './ArticlesList.scss';
|
import './ArticlesList.scss';
|
||||||
import SearchInput from '../SearchInput';
|
|
||||||
import TagsList from '../TagsList';
|
|
||||||
|
|
||||||
type ArticlesListProps = {
|
type ArticlesListProps = {
|
||||||
data: {
|
data: {
|
||||||
@ -39,7 +39,7 @@ const ArticlesList: Component<ArticlesListProps> = (props) => {
|
|||||||
* Find articles by title
|
* Find articles by title
|
||||||
*/
|
*/
|
||||||
if (search()) {
|
if (search()) {
|
||||||
filtered = utils.findArticlesBySearch(search(), filtered);
|
filtered = findArticlesBySearch(search(), filtered);
|
||||||
setArticles(filtered);
|
setArticles(filtered);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,7 +47,7 @@ const ArticlesList: Component<ArticlesListProps> = (props) => {
|
|||||||
* Find articles by selected tags
|
* Find articles by selected tags
|
||||||
*/
|
*/
|
||||||
if (selectedTags().length !== 0) {
|
if (selectedTags().length !== 0) {
|
||||||
filtered = utils.findArticlesByTags(selectedTags(), filtered);
|
filtered = findArticlesByTags(selectedTags(), filtered);
|
||||||
setArticles(filtered);
|
setArticles(filtered);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
@ -78,7 +78,7 @@ const ArticlesList: Component<ArticlesListProps> = (props) => {
|
|||||||
return (
|
return (
|
||||||
<div class="articles-list__wrapper">
|
<div class="articles-list__wrapper">
|
||||||
<h2>{year}</h2>
|
<h2>{year}</h2>
|
||||||
<For each={utils.sortArtcilesByDate(articles()[year])} children={(article) => <ArticleItem data={article} />} />
|
<For each={sortArtcilesByDate(articles()[year])} children={(article) => <ArticleItem data={article} />} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -1,17 +1,18 @@
|
|||||||
---
|
---
|
||||||
import { SITE_METADATA } from '@root/config';
|
import { SITE_METADATA } from '@root/config';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
title?: string | undefined;
|
title?: string | undefined;
|
||||||
description?: string | undefined;
|
description?: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { title = SITE_METADATA.title, description } = Astro.props as Props;
|
const { title = SITE_METADATA.title, description } = Astro.props as Props;
|
||||||
---
|
---
|
||||||
|
|
||||||
<link rel='preconnect' href='https://fonts.googleapis.com' />
|
<link rel='preconnect' href='https://fonts.googleapis.com' />
|
||||||
<link rel='preconnect' href='https://fonts.gstatic.com' crossorigin />
|
<link rel='preconnect' href='https://fonts.gstatic.com' crossorigin />
|
||||||
<link href='https://fonts.googleapis.com/css2?family=Rubik:ital,wght@0,400;0,500;0,600;0,700;1,400&display=swap' rel='stylesheet' />
|
<link href='https://fonts.googleapis.com/css2?family=Rubik:ital,wght@0,400;0,500;0,600;0,700;1,400&display=swap'
|
||||||
|
rel='stylesheet' />
|
||||||
|
|
||||||
<meta charset='utf-8' />
|
<meta charset='utf-8' />
|
||||||
<meta name='viewport' content='width=device-width, initial-scale=1' />
|
<meta name='viewport' content='width=device-width, initial-scale=1' />
|
||||||
@ -21,4 +22,4 @@ const { title = SITE_METADATA.title, description } = Astro.props as Props;
|
|||||||
<meta name='title' content={title} />
|
<meta name='title' content={title} />
|
||||||
<meta name='description' content={description || SITE_METADATA.description} />
|
<meta name='description' content={description || SITE_METADATA.description} />
|
||||||
<script is:inline src='assets/js/detect-theme.js' />
|
<script is:inline src='assets/js/detect-theme.js' />
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
@ -1,13 +1,13 @@
|
|||||||
---
|
---
|
||||||
import './BaseLayout.scss'
|
import './BaseLayout.scss';
|
||||||
import Header from '@components/Header'
|
import Header from '@components/Header';
|
||||||
import Footer from '@components/Footer'
|
import Footer from '@components/Footer';
|
||||||
---
|
---
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<Header />
|
<Header />
|
||||||
<main class='site-content'>
|
<main class='site-content'>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</main>
|
</main>
|
||||||
<Footer />
|
<Footer />
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@ -25,7 +25,7 @@ const SearchInput: Component<SearchInputProps> = (props) => {
|
|||||||
* Update state and set url param
|
* Update state and set url param
|
||||||
*/
|
*/
|
||||||
const onSearch: JSX.EventHandler<HTMLInputElement, InputEvent> = (e) => {
|
const onSearch: JSX.EventHandler<HTMLInputElement, InputEvent> = (e) => {
|
||||||
const value = e.currentTarget.value;
|
const value = e.currentTarget.value.trim();
|
||||||
|
|
||||||
if (value.length === 0) {
|
if (value.length === 0) {
|
||||||
resetSearch();
|
resetSearch();
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import type { Component, JSX } from 'solid-js';
|
import type { Component, JSX } from 'solid-js';
|
||||||
|
|
||||||
import { SITE_SETTINGS } from '@root/config';
|
|
||||||
import { createSignal, onMount, Switch, Match } from 'solid-js';
|
import { createSignal, onMount, Switch, Match } from 'solid-js';
|
||||||
import './ThemeSwitcher.scss';
|
import './ThemeSwitcher.scss';
|
||||||
|
|
||||||
@ -23,14 +22,17 @@ const SunIcon: Component = () => (
|
|||||||
);
|
);
|
||||||
|
|
||||||
const ThemeSwitcher: Component = () => {
|
const ThemeSwitcher: Component = () => {
|
||||||
const [theme, setTheme] = createSignal<ThemeModeType>(SITE_SETTINGS.defaultTheme);
|
const [theme, setTheme] = createSignal<ThemeModeType>('light');
|
||||||
const [isReady, setIsReady] = createSignal<boolean>(false);
|
const [isReady, setIsReady] = createSignal<boolean>(false);
|
||||||
|
|
||||||
const toggleTheme: JSX.EventHandler<HTMLButtonElement, MouseEvent> = () => {
|
const toggleTheme: JSX.EventHandler<HTMLButtonElement, MouseEvent> = () => {
|
||||||
if (!window.theme) return;
|
if (!window.theme) return;
|
||||||
|
|
||||||
const newTheme = theme() === 'light' ? 'dark' : 'light';
|
const newTheme = theme() === 'light' ? 'dark' : 'light';
|
||||||
|
const toggleEvent = new CustomEvent('onthemetoggled', { detail: newTheme });
|
||||||
|
|
||||||
setTheme(newTheme);
|
setTheme(newTheme);
|
||||||
|
window.dispatchEvent(toggleEvent);
|
||||||
localStorage.setItem('theme', newTheme);
|
localStorage.setItem('theme', newTheme);
|
||||||
document.documentElement.setAttribute('data-theme', newTheme);
|
document.documentElement.setAttribute('data-theme', newTheme);
|
||||||
};
|
};
|
||||||
@ -39,7 +41,7 @@ const ThemeSwitcher: Component = () => {
|
|||||||
setIsReady(true);
|
setIsReady(true);
|
||||||
if (window.theme) {
|
if (window.theme) {
|
||||||
setTheme(window.theme);
|
setTheme(window.theme);
|
||||||
/* Update theme state on changing OS the,e */
|
/* Update theme state on changing OS theme */
|
||||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', ({ matches: isDark }) => {
|
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', ({ matches: isDark }) => {
|
||||||
const theme = isDark ? 'dark' : 'light';
|
const theme = isDark ? 'dark' : 'light';
|
||||||
setTheme(theme);
|
setTheme(theme);
|
||||||
|
|||||||
@ -1,35 +1,36 @@
|
|||||||
---
|
---
|
||||||
import type { Props as BaseHeadProps } from '@root/components/BaseHead.astro';
|
import type { Props as BaseHeadProps } from '@root/components/BaseHead.astro';
|
||||||
|
|
||||||
import BaseHead from '@components/BaseHead.astro';
|
import BaseHead from '@components/BaseHead.astro';
|
||||||
import BaseLayout from '@components/BaseLayout';
|
import BaseLayout from '@components/BaseLayout';
|
||||||
import ArticleContent from '@components/ArticleContent';
|
import ArticleContent from '@components/ArticleContent';
|
||||||
import ArticlePost from '@components/ArticlePost';
|
import ArticlePost from '@components/ArticlePost';
|
||||||
import '@styles/global.scss';
|
import '@styles/global.scss';
|
||||||
|
|
||||||
export interface Props extends BaseHeadProps {
|
export interface Props extends BaseHeadProps {
|
||||||
lang?: string | undefined;
|
lang?: string | undefined;
|
||||||
content: ArticleType;
|
content: ArticleType;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { lang = 'ru', content } = Astro.props as Props;
|
const { lang = 'ru', content } = Astro.props as Props;
|
||||||
---
|
---
|
||||||
|
|
||||||
<html lang={lang}>
|
<html lang={lang}>
|
||||||
<head>
|
|
||||||
<BaseHead title={content.title}>
|
|
||||||
<link rel='stylesheet' href='assets/css/prism-one-dark.min.css' media='(prefers-color-scheme: dark)' />
|
|
||||||
<link rel='stylesheet' href='assets/css/prism-one-light.min.css' media='(prefers-color-scheme: light)' />
|
|
||||||
</BaseHead>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<BaseLayout>
|
<head>
|
||||||
<div class='container'>
|
<BaseHead title={content.title}>
|
||||||
<ArticlePost data={content}>
|
<script is:inline defer src='assets/js/load-code-highlight.js' />
|
||||||
<ArticleContent>
|
</BaseHead>
|
||||||
<slot></slot>
|
</head>
|
||||||
</ArticleContent>
|
|
||||||
</ArticlePost>
|
<BaseLayout>
|
||||||
</div>
|
<div class='container'>
|
||||||
</BaseLayout>
|
<ArticlePost data={content}>
|
||||||
</html>
|
<ArticleContent>
|
||||||
|
<slot></slot>
|
||||||
|
</ArticleContent>
|
||||||
|
</ArticlePost>
|
||||||
|
</div>
|
||||||
|
</BaseLayout>
|
||||||
|
|
||||||
|
</html>
|
||||||
@ -116,7 +116,7 @@
|
|||||||
--font-color-grayer: var(--color-gray-200);
|
--font-color-grayer: var(--color-gray-200);
|
||||||
|
|
||||||
/* Border */
|
/* Border */
|
||||||
--border-color: var(--color-gray-700);
|
--border-color: var(--color-gray-600);
|
||||||
--border-color-hover: var(--color-gray-600);
|
--border-color-hover: var(--color-gray-600);
|
||||||
|
|
||||||
/* Elements */
|
/* Elements */
|
||||||
|
|||||||
5
src/types/main.d.ts
vendored
5
src/types/main.d.ts
vendored
@ -3,6 +3,11 @@ export {};
|
|||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
theme?: ThemeModeType | undefined;
|
theme?: ThemeModeType | undefined;
|
||||||
|
addEventListener<K extends keyof CustomEventMap>(type: K, listener: (this: Window, ev: CustomEventMap[K]) => void): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CustomEventMap {
|
||||||
|
onthemetoggled: CustomEvent<ThemeModeType>;
|
||||||
}
|
}
|
||||||
|
|
||||||
type ArticleType = {
|
type ArticleType = {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user