Merge pull request #201 from tmshv/issue-200

Accessibility Panel
This commit is contained in:
tmshv authored and GitHub committed 2025-08-25 22:04:05 +03:00
commit c28e3f74ae
21 files changed
+684 -111

No files matched your search

-2
View File
@@ -1,2 +0,0 @@
[tools]
node = "20"
+2
View File
@@ -0,0 +1,2 @@
[tools]
node = "22"
+179 -24
View File
@@ -24,9 +24,11 @@
"sharp": "^0.32.6",
"sitemap": "^1.5.0",
"typograf": "^6.11.1",
"use-media": "^1.4.0"
"use-media": "^1.4.0",
"valtio": "^2.1.7"
},
"devDependencies": {
"@eslint/eslintrc": "^3.3.1",
"@types/classnames": "^2.2.10",
"@types/markdown-it": "^12.2.3",
"@types/node": "^18.13.0",
@@ -90,16 +92,16 @@
}
},
"node_modules/@eslint/eslintrc": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz",
"integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==",
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz",
"integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ajv": "^6.12.4",
"debug": "^4.3.2",
"espree": "^9.6.0",
"globals": "^13.19.0",
"espree": "^10.0.1",
"globals": "^14.0.0",
"ignore": "^5.2.0",
"import-fresh": "^3.2.1",
"js-yaml": "^4.1.0",
@@ -107,12 +109,56 @@
"strip-json-comments": "^3.1.1"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/@eslint/eslintrc/node_modules/eslint-visitor-keys": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
"integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/@eslint/eslintrc/node_modules/espree": {
"version": "10.4.0",
"resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
"integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"acorn": "^8.15.0",
"acorn-jsx": "^5.3.2",
"eslint-visitor-keys": "^4.2.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/@eslint/eslintrc/node_modules/globals": {
"version": "14.0.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
"integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@eslint/js": {
"version": "8.57.1",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz",
@@ -1223,13 +1269,13 @@
"version": "15.7.5",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
"integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==",
"dev": true
"devOptional": true
},
"node_modules/@types/react": {
"version": "18.0.18",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.18.tgz",
"integrity": "sha512-6hI08umYs6NaiHFEEGioXnxJ+oEhY3eRz8VCUaudZmGdtvPviCJB8mgaMxaDWAdPSYd4eFavrPk2QIolwbLYrg==",
"dev": true,
"devOptional": true,
"dependencies": {
"@types/prop-types": "*",
"@types/scheduler": "*",
@@ -1240,7 +1286,7 @@
"version": "0.16.2",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
"integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==",
"dev": true
"devOptional": true
},
"node_modules/@types/sharp": {
"version": "0.29.2",
@@ -1501,9 +1547,9 @@
"integrity": "sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ=="
},
"node_modules/acorn": {
"version": "8.14.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
"integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
"version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"bin": {
@@ -2832,6 +2878,30 @@
"url": "https://opencollective.com/eslint"
}
},
"node_modules/eslint/node_modules/@eslint/eslintrc": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz",
"integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ajv": "^6.12.4",
"debug": "^4.3.2",
"espree": "^9.6.0",
"globals": "^13.19.0",
"ignore": "^5.2.0",
"import-fresh": "^3.2.1",
"js-yaml": "^4.1.0",
"minimatch": "^3.1.2",
"strip-json-comments": "^3.1.1"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/eslint/node_modules/ansi-styles": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz",
@@ -4666,6 +4736,12 @@
"react-is": "^16.13.1"
}
},
"node_modules/proxy-compare": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-3.0.1.tgz",
"integrity": "sha512-V9plBAt3qjMlS1+nC8771KNf6oJ12gExvaxnNzN/9yVRLdTv/lc+oJlnSzrdYDAvBfTStPCoiaCOTmTs0adv7Q==",
"license": "MIT"
},
"node_modules/pump": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
@@ -5842,6 +5918,30 @@
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
},
"node_modules/valtio": {
"version": "2.1.7",
"resolved": "https://registry.npmjs.org/valtio/-/valtio-2.1.7.tgz",
"integrity": "sha512-DwJhCDpujuQuKdJ2H84VbTjEJJteaSmqsuUltsfbfdbotVfNeTE4K/qc/Wi57I9x8/2ed4JNdjEna7O6PfavRg==",
"license": "MIT",
"dependencies": {
"proxy-compare": "^3.0.1"
},
"engines": {
"node": ">=12.20.0"
},
"peerDependencies": {
"@types/react": ">=18.0.0",
"react": ">=18.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"react": {
"optional": true
}
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -6015,20 +6115,45 @@
"dev": true
},
"@eslint/eslintrc": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz",
"integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==",
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz",
"integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==",
"dev": true,
"requires": {
"ajv": "^6.12.4",
"debug": "^4.3.2",
"espree": "^9.6.0",
"globals": "^13.19.0",
"espree": "^10.0.1",
"globals": "^14.0.0",
"ignore": "^5.2.0",
"import-fresh": "^3.2.1",
"js-yaml": "^4.1.0",
"minimatch": "^3.1.2",
"strip-json-comments": "^3.1.1"
},
"dependencies": {
"eslint-visitor-keys": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
"integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
"dev": true
},
"espree": {
"version": "10.4.0",
"resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
"integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
"dev": true,
"requires": {
"acorn": "^8.15.0",
"acorn-jsx": "^5.3.2",
"eslint-visitor-keys": "^4.2.1"
}
},
"globals": {
"version": "14.0.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
"integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
"dev": true
}
}
},
"@eslint/js": {
@@ -6684,13 +6809,13 @@
"version": "15.7.5",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
"integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==",
"dev": true
"devOptional": true
},
"@types/react": {
"version": "18.0.18",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.18.tgz",
"integrity": "sha512-6hI08umYs6NaiHFEEGioXnxJ+oEhY3eRz8VCUaudZmGdtvPviCJB8mgaMxaDWAdPSYd4eFavrPk2QIolwbLYrg==",
"dev": true,
"devOptional": true,
"requires": {
"@types/prop-types": "*",
"@types/scheduler": "*",
@@ -6701,7 +6826,7 @@
"version": "0.16.2",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
"integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==",
"dev": true
"devOptional": true
},
"@types/sharp": {
"version": "0.29.2",
@@ -6848,9 +6973,9 @@
"integrity": "sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ=="
},
"acorn": {
"version": "8.14.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
"integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
"version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true
},
"acorn-jsx": {
@@ -7537,6 +7662,23 @@
"text-table": "^0.2.0"
},
"dependencies": {
"@eslint/eslintrc": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz",
"integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==",
"dev": true,
"requires": {
"ajv": "^6.12.4",
"debug": "^4.3.2",
"espree": "^9.6.0",
"globals": "^13.19.0",
"ignore": "^5.2.0",
"import-fresh": "^3.2.1",
"js-yaml": "^4.1.0",
"minimatch": "^3.1.2",
"strip-json-comments": "^3.1.1"
}
},
"ansi-styles": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz",
@@ -9020,6 +9162,11 @@
"react-is": "^16.13.1"
}
},
"proxy-compare": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-3.0.1.tgz",
"integrity": "sha512-V9plBAt3qjMlS1+nC8771KNf6oJ12gExvaxnNzN/9yVRLdTv/lc+oJlnSzrdYDAvBfTStPCoiaCOTmTs0adv7Q=="
},
"pump": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
@@ -9840,6 +9987,14 @@
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
},
"valtio": {
"version": "2.1.7",
"resolved": "https://registry.npmjs.org/valtio/-/valtio-2.1.7.tgz",
"integrity": "sha512-DwJhCDpujuQuKdJ2H84VbTjEJJteaSmqsuUltsfbfdbotVfNeTE4K/qc/Wi57I9x8/2ed4JNdjEna7O6PfavRg==",
"requires": {
"proxy-compare": "^3.0.1"
}
},
"which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+5 -3
View File
@@ -7,8 +7,8 @@
"start": "next start",
"dev": "next dev --turbo",
"build": "next build",
"lint": "next lint",
"format": "next lint --fix",
"lint": "eslint .",
"format": "eslint --fix .",
"sitemap": "node scripts/sitemap.js",
"feed": "node scripts/feed.js"
},
@@ -34,9 +34,11 @@
"sharp": "^0.32.6",
"sitemap": "^1.5.0",
"typograf": "^6.11.1",
"use-media": "^1.4.0"
"use-media": "^1.4.0",
"valtio": "^2.1.7"
},
"devDependencies": {
"@eslint/eslintrc": "^3.3.1",
"@types/classnames": "^2.2.10",
"@types/markdown-it": "^12.2.3",
"@types/node": "^18.13.0",
-3
View File
@@ -8,7 +8,6 @@ import App from "next/app"
import Head from "next/head"
import Script from "next/script"
import { YMetrika } from "src/components/YMetrika"
import { Theme } from "@/ui/theme/Theme"
import { ThemeColor } from "@/ui/ThemeColor"
export default class MyApp extends App {
@@ -51,9 +50,7 @@ export default class MyApp extends App {
</Head>
<Script src="https://culturaltracking.ru/static/js/spxl.js?pixelId=27154" data-pixel-id="27154"></Script>
<Theme>
<Component {...pageProps} />
</Theme>
</>
)
}
+5 -6
View File
@@ -1,19 +1,18 @@
import { theme, toggleTheme } from "@/store/theme"
import { Button } from "@/ui/Button"
import { HiOutlineEye, HiOutlineEyeOff } from "react-icons/hi"
import { useTheme } from "@/ui/theme/useTheme"
import { useSnapshot } from "valtio"
export function AccessibilityButton() {
const { theme, setTheme } = useTheme()
const t = useSnapshot(theme)
return (
<Button
theme="icon"
size="default"
onClick={() => {
setTheme(theme === "default" ? "contrast" : "default")
}}
onClick={toggleTheme}
>
{theme === "default" ? (
{t.theme === "default" ? (
<HiOutlineEyeOff size={24} />
) : (
<HiOutlineEye size={24} />
@@ -0,0 +1,79 @@
import play from "@/play"
import { state as playState, setVoice } from "@/store/play"
import { decreaseFontSize, hideImages, increaseFontSize, reset, setBlackOnWhite, setBlueOnBlue, setBrownOnYellow, setDefaultTheme, setGreenOnBrown, setLetterSpacingOne, setLetterSpacingTwo, setLetterSpacingFour, setLineHeightOneAndHalf, setLineHeightTwo, setLineHeightTwoAndHalf, setSerifFont, setSansSerifFont, setWhiteOnBlack, showImages } from "@/store/theme"
import { HiCog, HiOutlineEyeOff, HiPlay } from "react-icons/hi"
import { useSnapshot } from "valtio"
import s from "./panel.module.css"
import VoiceSelect from "./voice"
function AccessibilityPanel() {
const p = useSnapshot(playState)
return ( <div className={s.viewsettings}>
<div className={s.row}>
<div className={s.block}>
<p>Размер шрифта:</p>
<a onClick={decreaseFontSize} title="уменьшить размер шрифта">A-</a>
<a onClick={increaseFontSize} title="увеличить размер шрифта"><strong>A+</strong></a>
</div>
<div className={s.block}>
<p>Изображения:</p>
<a onClick={showImages}>Включить</a>
<a onClick={hideImages}>Выключить</a>
</div>
<div className={s.block}>
<p>Цвет сайта:</p>
<a onClick={setBlackOnWhite} title="черные буквы на белом фоне" style={{color: "black", backgroundColor: "white"}}>Ц</a>
<a onClick={setWhiteOnBlack} title="белые буквы на черном фоне" style={{color: "white", backgroundColor: "black"}}>Ц</a>
<a onClick={setBlueOnBlue} title="синии буквы на голубом фоне" style={{color: "oklch(32% 24% 253deg)", backgroundColor: "oklch(84% 21% 245deg)"}}>Ц</a>
<a onClick={setBrownOnYellow} title="коричневые буквы на желтом фоне" style={{color: "oklch(41% 3% 92deg)", backgroundColor: "oklch(96% 9% 101deg)"}}>Ц</a>
<a onClick={setGreenOnBrown} title="зеленые буквы на коричневом фоне" style={{color: "oklch(85% 47% 128deg)", backgroundColor: "oklch(29% 10% 61deg)"}}>Ц</a>
</div>
<div className={s.block}>
<p>Шрифт:</p>
<a onClick={setSerifFont}>С засечками</a>
<a onClick={setSansSerifFont}>Без засечек</a>
{/*<a id="btn-on-braille" href="javascript:void(0)" onClick="smSetting('fontBraille', 'brailleOn')" title="только для печати">Брайля</a>*/}
{/*<a id="btn-off-braille" href="javascript:void(0)" onClick="smSetting('fontBraille', 'brailleOff')">Обычный</a>*/}
</div>
<div className={s.block}>
<p>Высота строк:</p>
<a onClick={setLineHeightOneAndHalf}>1.5</a>
<a onClick={setLineHeightTwo}>2.0</a>
<a onClick={setLineHeightTwoAndHalf}>2.5</a>
</div>
<div className={s.block}>
<p>Межбуквенный интервал:</p>
<a onClick={setLetterSpacingOne}>1</a>
<a onClick={setLetterSpacingTwo}>2</a>
<a onClick={setLetterSpacingFour}>4</a>
</div>
</div>
<div className={s.row}>
<button onClick={reset}>
<HiCog />
Сброс настроек
</button>
<button onClick={setDefaultTheme}>
<HiOutlineEyeOff />
Обычная версия
</button>
<button onClick={play}>
<HiPlay />
Прослушать всё
</button>
<VoiceSelect value={p.voice} onChange={(event) => {
setVoice(event.target.value)
}} />
</div>
</div>
)
}
export default AccessibilityPanel
@@ -0,0 +1,91 @@
/*body {
margin: 0 auto;
padding: 0;
width: 96%;
padding-top: 90px;
}*/
.viewsettings {
display: flex;
flex-direction: column;
gap: 10px;
color: black;
background: white;
z-index: 10;
font-size: 20px !important;
line-height: 30px;
font-family: sans-serif;
letter-spacing: normal !important;
/*position: fixed;
top: 0;*/
margin: 10px;
padding: 10px;
text-align: center;
border: 2px solid;
}
.viewsettings .row {
display: flex;
flex-wrap: wrap;
gap: 8px;
justify-content: center;
}
.viewsettings p {
margin: 0;
}
.viewsettings a {
border: 1px solid;
text-decoration: none;
padding: 4px 10px;
cursor: pointer;
color: black;
}
.viewsettings a:hover {
background-color: #cfcfcf !important;
color: #000 !important;
}
.viewsettings a:focus {
outline: 1px solid !important;
}
.viewsettings .block {
display: inline-block;
vertical-align: top;
line-height: 30px;
border: 2px solid;
padding: 6px 10px;
margin: 2px;
}
.viewsettings .block a {
margin-right: 8px;
}
.viewsettings button {
display: flex;
align-items: center;
gap: 4px;
border: 1px solid;
font-size: 20px !important;
padding: 4px 10px;
cursor: pointer;
color: black;
background-color: white;
}
.viewsettings button:hover {
background-color: #cfcfcf !important;
color: #000 !important;
}
.viewsettings button:focus {
outline: 1px solid !important;
}
@@ -0,0 +1,27 @@
function useVoices() {
return window.speechSynthesis
.getVoices()
.map(v => {
return {
name: `${v.name} (${v.lang})`,
value: v.name,
}
})
}
export type VoiceSelectProps = React.SelectHTMLAttributes<HTMLSelectElement>
function VoiceSelect(props: VoiceSelectProps) {
const voices = useVoices()
return (
<select {...props}>
{voices.map(voice => (
<option key={voice.value} value={voice.value}>
{voice.name}
</option>
))}
</select>
)
}
export default VoiceSelect
+7 -2
View File
@@ -10,7 +10,6 @@ import { useMobile } from "src/hooks/useMobile"
import { Navigation } from "@/components/Navigation"
import { Box } from "@/ui/Box"
import { AccessibilityButton } from "../AccessibilityButton"
import { useTheme } from "@/ui/theme/useTheme"
import { Copyright } from "../Copyright"
import { Spacer } from "../Spacer"
import { Author } from "../Author"
@@ -22,6 +21,8 @@ import dynamic from "next/dynamic"
import qr0 from "src/assets/qr00.png"
import qr1 from "src/assets/qr01.png"
import Image from "next/image"
import AccessibilityPanel from "../AccessibilityPanel"
import useAccessibility from "@/hooks/useAccessibility"
const Gosuslugi = dynamic(import("../Gosuslugi").then(module => module.Gosuslugi), {
ssr: false,
@@ -37,11 +38,11 @@ export type AppProps = {
export const App: React.FC<AppProps> = ({ showAuthor = false, menu, ...props }) => {
const hideBreadcrumbs = useMobile()
const accessibility = useAccessibility()
const router = useRouter()
const blockStyle = {
justifyContent: "center",
}
const { theme, setTheme } = useTheme()
return (
<ConfigContext.Provider value={{
@@ -51,6 +52,10 @@ export const App: React.FC<AppProps> = ({ showAuthor = false, menu, ...props })
<Wrapper
header={(
<header>
{!accessibility ? null : (
<AccessibilityPanel/>
)}
<Navigation style={{
padding: "var(--size-s)",
}} />
+4 -3
View File
@@ -1,7 +1,8 @@
import s from "./wrapper.module.css"
import cx from "classnames"
import { useTheme } from "@/ui/theme/useTheme"
import { useSnapshot } from "valtio"
import { theme } from "@/store/theme"
export type WrapperProps = {
header: React.ReactNode
@@ -12,10 +13,10 @@ export type WrapperProps = {
}
export const Wrapper: React.FC<WrapperProps> = props => {
const { theme } = useTheme()
const t = useSnapshot(theme)
return (
<div className={cx(s.container, `theme-${theme}`)} style={props.style}>
<div className={cx(s.container, `theme-${t.theme}`, `font-${t.fontType}`, t.colorScheme ?? "black-on-white", !t.showImages ? "no-images" : null)} style={props.style}>
{props.header}
<main className={s.main} style={props.mainStyle}>
@@ -1,6 +1,8 @@
.container {
display: flex;
flex-direction: column;
color: var(--color-text);
background-color: var(--color-back-main);
font-family: var(--text-font);
+10
View File
@@ -0,0 +1,10 @@
import { theme } from "@/store/theme"
import { useSnapshot } from "valtio"
function useAccessibility(): boolean {
const t = useSnapshot(theme)
return t.theme === "contrast"
}
export default useAccessibility
+20
View File
@@ -0,0 +1,20 @@
import { state } from "./store/play"
function play() {
const synth = window.speechSynthesis
const a = document.querySelector("article")
if (!a || !a.textContent) {
return
}
const u = new SpeechSynthesisUtterance(a.textContent)
const voice = synth.getVoices().find((v) => v.name === state.voice)
if (voice) {
u.voice = voice
}
synth.speak(u)
}
export default play
+13
View File
@@ -0,0 +1,13 @@
import { proxy } from "valtio"
type State = {
voice: string
}
export const state = proxy<State>({
voice: "Milena",
})
export function setVoice(value: string) {
state.voice = value
}
+146
View File
@@ -0,0 +1,146 @@
import { proxy, subscribe } from "valtio"
export type ThemeName = "default" | "contrast"
export type ColorSchemeVariant = "black-on-white" | "white-on-black" | "blue-on-blue" | "brown-on-yellow" | "green-on-brown"
export type FontTypeVariant = "serif" | "sans-serif"
const MIN_FONT_SIZE = 8
const MAX_FONT_SIZE = 48
const CHANGE_FONT_SIZE_DELTA = 1
function clamp(val: number, min: number, max: number): number {
if (val < min) {
return min
}
if (val > max) {
return max
}
return val
}
export type ThemeOptions = {
theme: ThemeName
fontType: FontTypeVariant
fontSize: number
lineHeight: number
letterSpacing: number
showImages: boolean
colorScheme: ColorSchemeVariant
}
export const theme = proxy<ThemeOptions>({
theme: "default",
fontSize: 14,
lineHeight: 1.5,
letterSpacing: 1,
fontType: "sans-serif",
showImages: true,
colorScheme: "black-on-white",
})
export function reset() {
theme.fontSize = 14
theme.lineHeight = 1.5
theme.letterSpacing = 1
theme.fontType = "sans-serif"
theme.showImages = true
theme.colorScheme = "black-on-white"
}
export function setDefaultTheme() {
theme.theme = "default"
reset()
}
export function toggleTheme() {
theme.theme = theme.theme === "default" ? "contrast" : "default"
}
export function increaseFontSize() {
theme.fontSize = clamp(theme.fontSize + CHANGE_FONT_SIZE_DELTA, MIN_FONT_SIZE, MAX_FONT_SIZE)
}
export function decreaseFontSize() {
theme.fontSize = clamp(theme.fontSize - CHANGE_FONT_SIZE_DELTA, MIN_FONT_SIZE, MAX_FONT_SIZE)
}
export function setSansSerifFont() {
theme.fontType = "sans-serif"
}
export function setSerifFont() {
theme.fontType = "serif"
}
export function showImages() {
theme.showImages = true
}
export function hideImages() {
theme.showImages = false
}
export function setBlackOnWhite() {
theme.colorScheme = "black-on-white"
}
export function setWhiteOnBlack() {
theme.colorScheme = "white-on-black"
}
export function setBlueOnBlue() {
theme.colorScheme = "blue-on-blue"
}
export function setBrownOnYellow() {
theme.colorScheme = "brown-on-yellow"
}
export function setGreenOnBrown() {
theme.colorScheme = "green-on-brown"
}
export function setLineHeightOneAndHalf() {
theme.lineHeight = 1.5
}
export function setLineHeightTwo() {
theme.lineHeight = 2
}
export function setLineHeightTwoAndHalf() {
theme.lineHeight = 2.5
}
export function setLetterSpacingOne() {
theme.letterSpacing = 1
}
export function setLetterSpacingTwo() {
theme.letterSpacing = 2
}
export function setLetterSpacingFour() {
theme.letterSpacing = 4
}
const unsubscribe = subscribe(theme, () => {
document.documentElement.style.setProperty("--font-size-default", `${theme.fontSize}pt`)
document.documentElement.style.setProperty("--font-size-second", `${theme.fontSize}pt`)
document.documentElement.style.setProperty("--font-size-accent", `${theme.fontSize}pt`)
document.documentElement.style.setProperty("--article-line-height", `${theme.lineHeight}em`)
if (theme.letterSpacing === 1) {
document.documentElement.style.setProperty("--letter-spacing", "normal")
} else {
document.documentElement.style.setProperty("--letter-spacing", `${theme.letterSpacing}px`)
}
// TODO remove this from here and move this logic directly to body tag after moving from pages to app
document.body.classList.forEach(name => {
document.body.classList.remove(name)
})
if (theme.theme === "contrast") {
document.body.classList.add("theme-contrast")
document.body.classList.add(theme.colorScheme)
}
})
+94 -30
View File
@@ -21,6 +21,8 @@
--font-size-h2: 3rem;
--font-size-h3: 1.5rem;
--letter-spacing: normal;
/* COLOR */
--color-text: hsl(0, 0%, 15%);
--color-text-second: hsl(240, 10%, 60%);
@@ -89,6 +91,7 @@ body {
background-color: var(--color-back-main);
font-family: var(--text-font);
letter-spacing: var(--letter-spacing);
}
/* TITLE */
@@ -167,35 +170,16 @@ h3 {
/* CONTRAST THEME */
.theme-contrast {
/* FONTS */
--title-font: 'Roboto', Helvetica, sans-serif;
--text-font: 'Roboto', Helvetica, sans-serif;
/* TEXT COLOR */
--color-text: black;
--color-text-second: black;
--color-text-opposite: white;
--color-back-main: white;
--color-back: white;
--color-back-active: hsl(240, 10%, 20%);
/* --color-back-opposite: hsl(240, 10%, 80%); */
--color-back-opposite: black;
--color-link: hsl(240, 10%, 40%);
--color-link-opposite: hsl(240, 10%, 60%);
--color-hover: white;
--color-hover-back: black;
/*margin-top: 235px;*/
/* EFFECTS */
/* --radius: 0px; */
--shadow: none;
--shadow-active: none;
/* FONTS */
--font-size-default: 16pt;
--font-size-accent: 24pt;
--font-size-second: 14pt;
/*--font-size-default: 16pt;*/
/*--font-size-accent: 24pt;*/
/*--font-size-second: 14pt;*/
--font-size-h1: 5rem;
--font-size-h2: 3rem;
@@ -207,23 +191,103 @@ h3 {
--border: 5px solid var(--color-back-opposite);
}
/* CONTRAST + DARK THEME */
@media (prefers-color-scheme: dark) {
.theme-contrast {
/* TEXT COLOR */
.theme-contrast.font-sans-serif {
--title-font: "Roboto", Helvetica, sans-serif;
--text-font: "Roboto", Helvetica, sans-serif;
}
.theme-contrast.font-serif {
--title-font: "Times New Roman", serif;
--text-font: "Times New Roman", serif;
}
.theme-contrast.black-on-white {
--color-text: black;
--color-text-second: black;
--color-text-opposite: white;
--color-back-main: white;
--color-back: white;
--color-back-opposite: black;
--color-hover: white;
--color-hover-back: black;
--color-back-active: black;
--color-link: black;
--color-link-opposite: white;
}
.theme-contrast.white-on-black {
--color-text: white;
--color-text-second: white;
--color-text-opposite: black;
--color-back-main: black;
--color-back: black;
--color-back-opposite: white;
--color-hover: black;
--color-hover-back: white;
/*--color-back-active: hsl(240, 10%, 20%);*/
/*--color-link: hsl(240, 10%, 40%);*/
/*--color-link-opposite: hsl(240, 10%, 60%);*/
}
--color-hover: white;
--color-hover-back: white;
.theme-contrast.blue-on-blue {
--color-text: oklch(32% 24% 253deg);
--color-text-second: oklch(32% 24% 253deg);
--color-text-opposite: oklch(84% 21% 245deg);
--color-back-main: oklch(84% 21% 245deg);
--color-back: oklch(84% 21% 245deg);
--color-back-opposite: oklch(32% 24% 253deg);
--color-hover: oklch(84% 21% 245deg);
--color-hover-back: oklch(32% 24% 253deg);
/*--color-back-active: hsl(240, 10%, 20%);*/
/*--color-link: hsl(240, 10%, 40%);*/
/*--color-link-opposite: hsl(240, 10%, 60%);*/
}
.theme-contrast.brown-on-yellow {
--color-text: oklch(41% 3% 92deg);
--color-text-second: oklch(41% 3% 92deg);
--color-text-opposite: oklch(96% 9% 101deg);
--color-back-main: oklch(96% 9% 101deg);
--color-back: oklch(96% 9% 101deg);
--color-back-opposite: oklch(41% 3% 92deg);
--color-hover: oklch(96% 9% 101deg);
--color-hover-back: oklch(41% 3% 92deg);
/*--color-back-active: hsl(240, 10%, 20%);*/
/*--color-link: hsl(240, 10%, 40%);*/
/*--color-link-opposite: hsl(240, 10%, 60%);*/
}
.theme-contrast.green-on-brown {
--color-text: oklch(85% 47% 128deg);
--color-text-second: oklch(85% 47% 128deg);
--color-text-opposite: oklch(29% 10% 61deg);
--color-back-main: oklch(29% 10% 61deg);
--color-back: oklch(29% 10% 61deg);
--color-back-opposite: oklch(85% 47% 128deg);
--color-hover: oklch(29% 10% 61deg);
--color-hover-back: oklch(85% 47% 128deg);
/*--color-back-active: hsl(240, 10%, 20%);*/
/*--color-link: hsl(240, 10%, 40%);*/
/*--color-link-opposite: hsl(240, 10%, 60%);*/
}
.theme-contrast.no-images img {
display: none;
}
/*.theme-contrast.no-images img:after {
position: absolute;
width: 100%;
height: 100%;
border: 1px solid black;
}*/
-17
View File
@@ -1,17 +0,0 @@
import { useLocalStorage } from "react-use"
import { ThemeContext } from "./ThemeContext"
import { ThemeType } from "./types"
export type ThemeProps = {
children?: React.ReactNode
}
export const Theme: React.FC<ThemeProps> = ({ children }) => {
const [theme, setTheme] = useLocalStorage<ThemeType>("hudozka-theme", "default")
return (
<ThemeContext.Provider value={[theme ?? "default", setTheme]}>
{children}
</ThemeContext.Provider>
)
}
-4
View File
@@ -1,4 +0,0 @@
import { createContext } from "react"
import { SetTheme, ThemeType } from "./types"
export const ThemeContext = createContext<[ThemeType, SetTheme]>(["default", () => {}])
-2
View File
@@ -1,2 +0,0 @@
export type ThemeType = "default" | "contrast"
export type SetTheme = (theme: ThemeType) => void
-15
View File
@@ -1,15 +0,0 @@
import { useContext } from "react"
import { ThemeContext } from "./ThemeContext"
import { SetTheme, ThemeType } from "./types"
export type UseThemeResult = {
theme: ThemeType
setTheme: SetTheme
// toggleTheme:
}
export function useTheme(): UseThemeResult {
const [theme, setTheme] = useContext(ThemeContext)
return { theme, setTheme }
}