Skip to content

Commit

Permalink
support theme=light/dark in url for embedding on other sites #307 #229
Browse files Browse the repository at this point in the history
  • Loading branch information
Aprillion committed Sep 2, 2023
1 parent dd69523 commit e424db0
Show file tree
Hide file tree
Showing 6 changed files with 161 additions and 47 deletions.
2 changes: 1 addition & 1 deletion app/assets/icons/tag.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
22 changes: 12 additions & 10 deletions app/components/icons-generated/Tag.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

54 changes: 54 additions & 0 deletions app/hooks/theme.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import {useCallback, useEffect, useState} from 'react'
import {useSearchParams} from '@remix-run/react'

const THEME = 'theme'
const supported = new Set(['dark', 'light', undefined])
const isSupported = (theme: string | null | undefined): theme is string | undefined =>
theme !== null && supported.has(theme)

export const useTheme = () => {
const [searchParams] = useSearchParams() // url parameter for iframe usage: ...?embedded&theme=dark
const [savedTheme, setSavedTheme] = useState(() => {
const fromParams = searchParams.get(THEME)
if (isSupported(fromParams)) return fromParams

if (typeof window === 'undefined') return
const fromStorage = window.localStorage.getItem(THEME)
if (isSupported(fromStorage)) return fromStorage
})

// remember UI theme toggle state in localStorage
const setStorageTheme = useCallback((theme: string | undefined) => {
if (isSupported(theme)) {
if (!theme) {
window.localStorage.removeItem(THEME)
} else {
window.localStorage.setItem(THEME, theme)
}
setSavedTheme(theme)
}
}, [])

// if no preference is saved, use the system preferences
useEffect(() => {
if (savedTheme) return

const media = window.matchMedia('(prefers-color-scheme: dark)')
const updateHtmlClass = () => {
const classList = document.documentElement.classList
if (media.matches) {
classList.remove('light')
classList.add('dark')
} else {
classList.remove('dark')
classList.add('light')
}
}
media.addEventListener('change', updateHtmlClass)
updateHtmlClass()

return () => media.removeEventListener('change', updateHtmlClass)
}, [savedTheme])

return {savedTheme, setStorageTheme}
}
121 changes: 87 additions & 34 deletions app/root.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
box-sizing: border-box;
}

:root {
:root, :root.light {
/* light theme: */
--bgColorPage: #eee;
--bgColorQuestionTitle: #bdf;
--bgColorQuestionAnswer: #eff;
--bgColorCopied: #fed;
Expand All @@ -14,6 +15,9 @@
--colorTitleHighlight: #d86;
--colorQuestionTitle: #333;
--borderColor: #ddd;
--borderColorButton: #999;
--bgColorInput: #fff;
--bgColorButton: #ddd;
--bgColorHighlight: #c0d3e4;
--bgColorTableRows: #e8f7fe;
--colorLink: #79f;
Expand All @@ -28,17 +32,6 @@
/* dark theme at the end of file to overwrite all previous rules */
}

.loader {
flex: none;
border: 4px solid #f3f3f3; /* Light grey */
border-top: 4px solid #3498db; /* Blue */
border-radius: 50%;
width: 30px;
height: 30px;
animation: spin 2s linear infinite;
margin: 10px auto;
}

@keyframes spin {
0% {
transform: rotate(0deg);
Expand Down Expand Up @@ -67,6 +60,12 @@
html {
height: 100%;
overflow-y: scroll;
background: var(--bgColorPage);
}

html.embed {
background: transparent;
padding: var(--paddingSides) 0;
}

body {
Expand Down Expand Up @@ -104,6 +103,33 @@ a:hover,
cursor: pointer;
}

/* reset default styles if user prefers light/dark mode, but chooses dark/light mode in the toggle */
input, textarea, select {
background: var(--bgColorInput);
color: var(--colorText);
border: 1px solid var(--borderColorButton);
border-radius: 4px;
}

button {
background: var(--bgColorButton);
color: var(--colorText);
border: 1px solid var(--borderColorButton);
border-radius: 4px;
cursor: pointer;
}

.loader {
flex: none;
border: 4px solid #f3f3f3; /* Light grey */
border-top: 4px solid #3498db; /* Blue */
border-radius: 50%;
width: 30px;
height: 30px;
animation: spin 2s linear infinite;
margin: 10px auto;
}

header {
display: flex;
align-items: flex-start;
Expand Down Expand Up @@ -635,6 +661,8 @@ footer > *:not(:last-child) {

.dialog {
width: 30em;
background: var(--bgColorPage);
color: var(--colorText);
}
/* Make sure older browsers work correctly */
div.dialog {
Expand Down Expand Up @@ -791,31 +819,56 @@ a[target='_blank']:not(.icon-link):after {
/* dark theme */
@media (prefers-color-scheme: dark) {
:root {
--bgColorQuestionTitle: #30386e;
--bgColorQuestionAnswer: #171f29;
--bgColorCopied: #fed;
--colorText: #f7fff9;
--colorTitle: #f7fff9;
--colorTitleHighlight: #d86;
--colorQuestionTitle: #ccc;
--borderColor: #444;
--bgColorHighlight: #354652;
--bgColorTableRows: #1d2436;
--colorLink: #89e3ff;
--colorLinkVisited: #79f;
--colorTooltip: #2e2e2e;
/* duplicate to avoid flash of white background while JS loads, consider some CSS postprocessing solution if this needs to be maintainable */
--bgColorPage: #333;
--bgColorQuestionTitle: #30386e;
--bgColorQuestionAnswer: #171f29;
--bgColorCopied: #fed;
--colorText: #f7fff9;
--colorTitle: #f7fff9;
--colorTitleHighlight: #d86;
--colorQuestionTitle: #ccc;
--borderColor: #444;
--borderColorButton: #666;
--bgColorInput: #2b2b2b;
--bgColorButton: #222;
--bgColorHighlight: #354652;
--bgColorTableRows: #1d2436;
--colorLink: #89e3ff;
--colorLinkVisited: #79f;
--colorTooltip: #2e2e2e;
}
}
:root.dark {
--bgColorPage: #333;
--bgColorQuestionTitle: #30386e;
--bgColorQuestionAnswer: #171f29;
--bgColorCopied: #fed;
--colorText: #f7fff9;
--colorTitle: #f7fff9;
--colorTitleHighlight: #d86;
--colorQuestionTitle: #ccc;
--borderColor: #444;
--borderColorButton: #666;
--bgColorInput: #2b2b2b;
--bgColorButton: #222;
--bgColorHighlight: #354652;
--bgColorTableRows: #1d2436;
--colorLink: #89e3ff;
--colorLinkVisited: #79f;
--colorTooltip: #2e2e2e;
}

svg *[class$='svg__gray'] {
fill: #ccc;
}
:root.dark svg *[class$='svg__gray'] {
fill: #ccc;
}

article > h2::after {
filter: invert(1);
}
:root.dark article > h2::after {
filter: invert(1);
}

.icon-link:active {
filter: drop-shadow(0 4px 8px rgba(255, 255, 255, 0.2));
}
:root.dark .icon-link:active {
filter: drop-shadow(0 4px 8px rgba(255, 255, 255, 0.2));
}

/* do not add more rules below, add them above dark theme */
8 changes: 6 additions & 2 deletions app/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import styles from '~/root.css'
import {useLoaderData} from '@remix-run/react'
import {questionsOnPage} from '~/hooks/stateModifiers'
import {loadQuestionDetail} from '~/server-utils/stampy'
import {useTheme} from './hooks/theme'

/*
* Transform the given text into a meta header format.
Expand Down Expand Up @@ -106,7 +107,9 @@ function Head({minLogo}: {minLogo?: boolean}) {
)
}

export function ErrorBoundary() {
export function ErrorBoundary({error}: {error: Error}) {
console.error(error)

return (
<html>
<Head />
Expand All @@ -123,9 +126,10 @@ export function ErrorBoundary() {

export default function App() {
const {minLogo} = useLoaderData<ReturnType<typeof loader>>()
const {savedTheme} = useTheme()

return (
<html lang="en">
<html lang="en" className={savedTheme}>
<Head minLogo={minLogo} />
<body>
<Outlet context={minLogo} />
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
"dev": "npm start",
"prod": "cross-env NODE_ENV=production miniflare ./build/index.js",
"tsc": "tsc",
"wrangler": "wrangler",
"deploy": "npm run build && wrangler publish",
"test": "jest"
},
Expand Down

0 comments on commit e424db0

Please sign in to comment.