diff --git a/.eslintrc.js b/.eslintrc.js index 67a1e06e..36b8a8bf 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -184,6 +184,27 @@ module.exports = { ], "no-sparse-arrays": "error", "no-template-curly-in-string": "error", + // Web interface guidelines — mechanical design rules enforced inside + // styled-components template literals. + // See docs/design/web-interface-guidelines.md + "no-restricted-syntax": [ + "error", + { + selector: "TemplateElement[value.raw=/transition:\\s*all/]", + message: + "Avoid `transition: all` — list only the properties you animate (web interface guidelines).", + }, + { + selector: "TemplateElement[value.raw=/[^-\\w]:focus(?![-\\w])/]", + message: + "Prefer `:focus-visible` over `:focus` for focus styling (web interface guidelines).", + }, + { + selector: "TemplateElement[value.raw=/outline:\\s*(none|0)\\b/]", + message: + "Don't remove `outline` without a visible focus replacement (web interface guidelines).", + }, + ], "no-throw-literal": "error", "no-trailing-spaces": "off", "no-undef-init": "error", diff --git a/src/ThemeProvider.tsx b/src/ThemeProvider.tsx index 8747f852..f6574570 100644 --- a/src/ThemeProvider.tsx +++ b/src/ThemeProvider.tsx @@ -1,4 +1,5 @@ import React, { createContext, FC, useMemo } from "react" +import { Helmet } from "react-helmet" import { ThemeProvider as BaseThemeProvider } from "styled-components" import { darkTheme, lightTheme } from "./design/themes" @@ -62,6 +63,11 @@ const ThemeProvider: FC = ({ children }) => { return ( + {isBrowser ? children : undefined} diff --git a/src/components/Footer/styles.tsx b/src/components/Footer/styles.tsx index 76b11fbd..a1d2bd11 100644 --- a/src/components/Footer/styles.tsx +++ b/src/components/Footer/styles.tsx @@ -22,7 +22,7 @@ export const FooterWrapper = styled.footer` } &:hover, - &:focus { + &:focus-visible { background: linear-gradient(92.97deg, #feaf6d 0%, #ff70a5 100%); background-clip: text; -webkit-background-clip: text; diff --git a/src/components/FourZeroFourHint/styles.tsx b/src/components/FourZeroFourHint/styles.tsx index 4dbcb2f7..354ff9c0 100644 --- a/src/components/FourZeroFourHint/styles.tsx +++ b/src/components/FourZeroFourHint/styles.tsx @@ -9,7 +9,7 @@ export const StyledLink = styled(Link)` transition: color 0.3s; &:hover, - &:focus { + &:focus-visible { color: #5dbbea; transition: none; } diff --git a/src/components/Home/styles.tsx b/src/components/Home/styles.tsx index b6d48aa0..6235a6ee 100644 --- a/src/components/Home/styles.tsx +++ b/src/components/Home/styles.tsx @@ -111,7 +111,8 @@ export const MenuItemLine = styled.div` margin-top: -3px; bottom: 0; background: ${(props) => props.theme.main.foreground}; - transition: all 0.3s; + transition: padding 0.3s, height 0.3s, background 0.3s, box-shadow 0.3s, + margin 0.3s, border-radius 0.3s; left: 0; ` diff --git a/src/components/Link/styles.tsx b/src/components/Link/styles.tsx index 23ecbf5c..ff7ed7af 100644 --- a/src/components/Link/styles.tsx +++ b/src/components/Link/styles.tsx @@ -23,7 +23,7 @@ const linkStyle = css` } &:hover, - &:focus { + &:focus-visible { background: linear-gradient(92.97deg, #feaf6d 0%, #ff70a5 100%); background-clip: text; -webkit-background-clip: text; diff --git a/src/components/Markdown/styles.tsx b/src/components/Markdown/styles.tsx index c0ccfbba..ea0f1493 100644 --- a/src/components/Markdown/styles.tsx +++ b/src/components/Markdown/styles.tsx @@ -42,6 +42,7 @@ export const MarkdownWrapper = styled.div` overflow-wrap: break-word; margin-top: ${BASE_LINE_HEIGHT * 2}px; margin-bottom: ${BASE_LINE_HEIGHT}px; + scroll-margin-top: 80px; } h1 { @@ -178,7 +179,7 @@ export const MarkdownWrapper = styled.div` background-repeat: no-repeat; background-size: 100% 3px; background-position: 0 100%; - transition: all 0.125s ease-in; + transition: color 0.125s ease-in, background-size 0.125s ease-in; font-weight: 700; word-break: break-all; &:hover { diff --git a/src/components/PageNavigation/styles.tsx b/src/components/PageNavigation/styles.tsx index fa615358..8833cb6f 100644 --- a/src/components/PageNavigation/styles.tsx +++ b/src/components/PageNavigation/styles.tsx @@ -22,7 +22,7 @@ export const NavLink = styled(Link)` text-decoration: none; &:hover, - &:focus { + &:focus-visible { color: ${(props) => props.theme.main.foreground}; text-decoration: underline; } diff --git a/src/components/PageSidebarLink/styles.tsx b/src/components/PageSidebarLink/styles.tsx index 13b816e0..d103da70 100644 --- a/src/components/PageSidebarLink/styles.tsx +++ b/src/components/PageSidebarLink/styles.tsx @@ -7,7 +7,7 @@ const extraLink = css` word-break: break-word; &:hover, - &:focus { + &:focus-visible { text-decoration: underline; } ` diff --git a/src/components/ThemeToggler/index.tsx b/src/components/ThemeToggler/index.tsx index 84de93e0..4e6d6458 100644 --- a/src/components/ThemeToggler/index.tsx +++ b/src/components/ThemeToggler/index.tsx @@ -6,7 +6,7 @@ export const ThemeToggler: FC = () => { const { theme, toggleTheme } = useTheme() return ( - + Switch to {theme === "dark" ? "light" : "dark"} mode ) diff --git a/src/components/ThemeToggler/styles.tsx b/src/components/ThemeToggler/styles.tsx index f6feac96..d9275707 100644 --- a/src/components/ThemeToggler/styles.tsx +++ b/src/components/ThemeToggler/styles.tsx @@ -1,7 +1,17 @@ import { transparentize } from "polished" import styled from "styled-components" -export const ThemeTogglerWrapper = styled.div`` +export const ThemeTogglerWrapper = styled.button` + display: block; + width: 100%; + padding: 0; + border: none; + background: none; + font: inherit; + color: inherit; + text-align: left; + cursor: pointer; +` export const Link = styled.span` display: inline-block; diff --git a/src/globalStyles.tsx b/src/globalStyles.tsx index a295a9c4..d19dec5a 100644 --- a/src/globalStyles.tsx +++ b/src/globalStyles.tsx @@ -8,6 +8,7 @@ import { export const GlobalStyles = createGlobalStyle` html { font-family: ${fontFamily.body}; + color-scheme: ${(props) => props.theme.name}; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%; } @@ -19,4 +20,23 @@ export const GlobalStyles = createGlobalStyle` -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } + + a, + button, + label, + summary, + [role="button"] { + touch-action: manipulation; + } + + @media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } + } ` diff --git a/src/layouts/ColumnLayout/index.tsx b/src/layouts/ColumnLayout/index.tsx index 492aff53..e18231c5 100644 --- a/src/layouts/ColumnLayout/index.tsx +++ b/src/layouts/ColumnLayout/index.tsx @@ -35,10 +35,11 @@ const InnerColumnLayout: FC = ({ return ( + Skip to content {title} {sidebar({ className: openOnMobile ? "is-open" : "" })} - {content} + {content} props.theme.main.background}; + color: ${(props) => props.theme.main.foreground}; + font-weight: 700; + text-decoration: none; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25); + transition: top 0.15s ease-in; + + &:focus-visible { + top: 8px; + } +` + export const Main = styled.div` display: flex; width: 100%; diff --git a/src/layouts/HomeLayout/styles.tsx b/src/layouts/HomeLayout/styles.tsx index 038a16fd..7591af08 100644 --- a/src/layouts/HomeLayout/styles.tsx +++ b/src/layouts/HomeLayout/styles.tsx @@ -29,7 +29,7 @@ export const MainContent = styled.main` transition: color 0.3s; &:hover, - &:focus { + &:focus-visible { color: #5dbbea; transition: none; }