diff --git a/.mise.toml b/.mise.toml deleted file mode 100644 index 126f6807..00000000 --- a/.mise.toml +++ /dev/null @@ -1,2 +0,0 @@ -[tools] -node = "20" diff --git a/mise.toml b/mise.toml new file mode 100644 index 00000000..6a0493c6 --- /dev/null +++ b/mise.toml @@ -0,0 +1,2 @@ +[tools] +node = "22" diff --git a/package-lock.json b/package-lock.json index b5149c9a..6cb8865f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 1c0f51eb..ae127516 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pages/_app.tsx b/pages/_app.tsx index a0b03fc4..0c71cc1d 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -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 { - - - + ) } diff --git a/src/components/AccessibilityButton/index.tsx b/src/components/AccessibilityButton/index.tsx index 1499d7b5..36d60a43 100644 --- a/src/components/AccessibilityButton/index.tsx +++ b/src/components/AccessibilityButton/index.tsx @@ -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 ( + + + { + setVoice(event.target.value) + }} /> + + + ) +} + +export default AccessibilityPanel diff --git a/src/components/AccessibilityPanel/panel.module.css b/src/components/AccessibilityPanel/panel.module.css new file mode 100644 index 00000000..9587a847 --- /dev/null +++ b/src/components/AccessibilityPanel/panel.module.css @@ -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; +} diff --git a/src/components/AccessibilityPanel/voice.tsx b/src/components/AccessibilityPanel/voice.tsx new file mode 100644 index 00000000..58eb636d --- /dev/null +++ b/src/components/AccessibilityPanel/voice.tsx @@ -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 + +function VoiceSelect(props: VoiceSelectProps) { + const voices = useVoices() + return ( + + ) +} + +export default VoiceSelect diff --git a/src/components/App/index.tsx b/src/components/App/index.tsx index 32e9d30c..3e7bafb6 100644 --- a/src/components/App/index.tsx +++ b/src/components/App/index.tsx @@ -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 = ({ showAuthor = false, menu, ...props }) => { const hideBreadcrumbs = useMobile() + const accessibility = useAccessibility() const router = useRouter() const blockStyle = { justifyContent: "center", } - const { theme, setTheme } = useTheme() return ( = ({ showAuthor = false, menu, ...props }) + {!accessibility ? null : ( + + )} + diff --git a/src/components/Wrapper/index.tsx b/src/components/Wrapper/index.tsx index 753ac961..bae1a282 100644 --- a/src/components/Wrapper/index.tsx +++ b/src/components/Wrapper/index.tsx @@ -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 = props => { - const { theme } = useTheme() + const t = useSnapshot(theme) return ( -
+
{props.header}
diff --git a/src/components/Wrapper/wrapper.module.css b/src/components/Wrapper/wrapper.module.css index 6b8d26b9..bb3a26a1 100644 --- a/src/components/Wrapper/wrapper.module.css +++ b/src/components/Wrapper/wrapper.module.css @@ -1,13 +1,15 @@ .container { - display: flex; - flex-direction: column; - background-color: var(--color-back-main); + display: flex; + flex-direction: column; - font-family: var(--text-font); - font-size: var(--font-size-default); + color: var(--color-text); + background-color: var(--color-back-main); + + font-family: var(--text-font); + font-size: var(--font-size-default); } .main { - min-height: 100vh; - flex: 1; + min-height: 100vh; + flex: 1; } diff --git a/src/hooks/useAccessibility.ts b/src/hooks/useAccessibility.ts new file mode 100644 index 00000000..c328c99f --- /dev/null +++ b/src/hooks/useAccessibility.ts @@ -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 diff --git a/src/play.ts b/src/play.ts new file mode 100644 index 00000000..d5c1b3f2 --- /dev/null +++ b/src/play.ts @@ -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 diff --git a/src/store/play.ts b/src/store/play.ts new file mode 100644 index 00000000..6cb2d192 --- /dev/null +++ b/src/store/play.ts @@ -0,0 +1,13 @@ +import { proxy } from "valtio" + +type State = { + voice: string +} + +export const state = proxy({ + voice: "Milena", +}) + +export function setVoice(value: string) { + state.voice = value +} diff --git a/src/store/theme.ts b/src/store/theme.ts new file mode 100644 index 00000000..69b5384f --- /dev/null +++ b/src/store/theme.ts @@ -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({ + 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) + } +}) diff --git a/src/style/style.css b/src/style/style.css index d112d8da..45c397a9 100644 --- a/src/style/style.css +++ b/src/style/style.css @@ -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 */ - --color-text: white; - --color-text-second: white; - --color-text-opposite: black; - - --color-back-main: black; - --color-back: black; - --color-back-opposite: 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.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%);*/ +} + +.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; +}*/ diff --git a/src/ui/theme/Theme.tsx b/src/ui/theme/Theme.tsx deleted file mode 100644 index 5cf5bd02..00000000 --- a/src/ui/theme/Theme.tsx +++ /dev/null @@ -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 = ({ children }) => { - const [theme, setTheme] = useLocalStorage("hudozka-theme", "default") - - return ( - - {children} - - ) -} diff --git a/src/ui/theme/ThemeContext.ts b/src/ui/theme/ThemeContext.ts deleted file mode 100644 index 2dcb4d68..00000000 --- a/src/ui/theme/ThemeContext.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { createContext } from "react" -import { SetTheme, ThemeType } from "./types" - -export const ThemeContext = createContext<[ThemeType, SetTheme]>(["default", () => {}]) diff --git a/src/ui/theme/types.ts b/src/ui/theme/types.ts deleted file mode 100644 index 8baed57c..00000000 --- a/src/ui/theme/types.ts +++ /dev/null @@ -1,2 +0,0 @@ -export type ThemeType = "default" | "contrast" -export type SetTheme = (theme: ThemeType) => void diff --git a/src/ui/theme/useTheme.ts b/src/ui/theme/useTheme.ts deleted file mode 100644 index b3fb9232..00000000 --- a/src/ui/theme/useTheme.ts +++ /dev/null @@ -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 } -}