+ className="even:bg-slate-200 even:dark:bg-slate-800">
{label} |
{formatBytes(size)}
diff --git a/src/components/RoomTabs.js b/src/components/RoomTabs.js
index 7b61234..9ca3f5b 100644
--- a/src/components/RoomTabs.js
+++ b/src/components/RoomTabs.js
@@ -36,8 +36,8 @@ const RoomTabs = ({ currentTab, setCurrentTab }) => {
onClick={() => setCurrentTab(tab.name)}
className={clsx(
tab.current
- ? 'bg-slate-200 text-slate-700'
- : 'text-slate-500 hover:text-slate-700',
+ ? 'bg-slate-200 text-slate-700 dark:bg-slate-800 dark:text-slate-300'
+ : 'text-slate-500 hover:text-slate-700 hover:dark:text-slate-300',
'rounded px-3 py-2 text-sm font-medium',
)}
aria-current={tab.current ? 'page' : undefined}>
diff --git a/src/components/ThemeSwitcher.js b/src/components/ThemeSwitcher.js
new file mode 100644
index 0000000..4c610a2
--- /dev/null
+++ b/src/components/ThemeSwitcher.js
@@ -0,0 +1,39 @@
+import { RadioGroup } from '@headlessui/react';
+import { clsx } from 'clsx';
+
+const ThemeSwitcher = ({ theme, setTheme, themeOptions }) => {
+ return (
+ <>
+
+ Colour theme
+
+
+
+ Choose a theme
+
+ {themeOptions.map((option) => (
+
+ clsx(
+ 'cursor-pointer focus:outline-none',
+ active && 'ring-2 ring-primary-600 ring-offset-2',
+ checked
+ ? 'bg-primary-600 text-slate-100 hover:bg-primary-500'
+ : 'bg-slate-200 text-slate-900 ring-1 ring-inset ring-slate-300 hover:bg-slate-100 dark:bg-slate-800 dark:text-slate-100 dark:ring-slate-700 hover:dark:bg-slate-900',
+ 'flex w-48 max-w-48 flex-initial items-center justify-center rounded-md px-3 py-3 text-sm font-semibold sm:flex-1',
+ )
+ }>
+ {option.name}
+
+ ))}
+
+
+ >
+ );
+};
+
+export default ThemeSwitcher;
diff --git a/src/containers/DropZoneContainer.js b/src/containers/DropZoneContainer.js
index ea6dccb..be8577f 100644
--- a/src/containers/DropZoneContainer.js
+++ b/src/containers/DropZoneContainer.js
@@ -89,7 +89,7 @@ const DropZoneContainer = ({ onFile }) => {
return (
{
if (!resources) {
@@ -55,6 +56,10 @@ const ResourceExplorer = ({ rom, res, resources }) => {
/>
}
/>
+ }
+ />
);
};
diff --git a/src/containers/RomMapCanvasContainer.js b/src/containers/RomMapCanvasContainer.js
index c9994ef..76902a9 100644
--- a/src/containers/RomMapCanvasContainer.js
+++ b/src/containers/RomMapCanvasContainer.js
@@ -1,5 +1,5 @@
import { useRef, useState, useEffect } from 'react';
-import clsx from 'clsx';
+import { clsx } from 'clsx';
import { getResourceColour } from '../lib/resourceUtils';
const WIDTH = 512;
@@ -26,7 +26,7 @@ const RomMapCanvasContainer = ({ rom, res, resourceList }) => {
width={WIDTH}
height={HEIGHT}
className={clsx(
- 'aspect-[4/3] w-full rounded border border-slate-700',
+ 'aspect-[4/3] w-full rounded border border-slate-600 dark:border-slate-400',
isComputing ? 'opacity-0' : 'opacity-100 transition-opacity',
)}
style={{ maxWidth: WIDTH }}
diff --git a/src/containers/RoomCanvasContainer.js b/src/containers/RoomCanvasContainer.js
index 591cd39..1cbf067 100644
--- a/src/containers/RoomCanvasContainer.js
+++ b/src/containers/RoomCanvasContainer.js
@@ -1,5 +1,5 @@
import { useRef, useState, useEffect } from 'react';
-import clsx from 'clsx';
+import { clsx } from 'clsx';
import { nesNTSCPalette as nesPalette } from '../lib/palettes';
const RoomCanvasContainer = ({
diff --git a/src/containers/SettingsContainer.js b/src/containers/SettingsContainer.js
new file mode 100644
index 0000000..5c5bac7
--- /dev/null
+++ b/src/containers/SettingsContainer.js
@@ -0,0 +1,48 @@
+import { useState } from 'react';
+import Main from '../components/Main';
+import MainHeader from '../components/MainHeader';
+import ThemeSwitcher from '../components/ThemeSwitcher';
+import { setColourTheme } from '../lib/colourThemeUtils';
+
+const themeOptions = [
+ { name: 'Dark', value: 'dark' },
+ { name: 'System', value: 'system', defaultTheme: true },
+ { name: 'Light', value: 'light' },
+];
+
+const SettingsContainer = () => {
+ // Find the selected theme, or the default one.
+ const selectedThemeValue = localStorage.getItem('theme');
+ const selectedTheme =
+ themeOptions.find(({ value }) => value === selectedThemeValue) ||
+ themeOptions.find(({ defaultTheme }) => defaultTheme);
+
+ const [theme, setTheme] = useState(selectedTheme);
+
+ // Keep the local storage and the DOM in sync with the state.
+ const setThemeWrapper = (theme) => {
+ setTheme(theme);
+
+ if (theme.defaultTheme) {
+ localStorage.removeItem('theme');
+ setColourTheme();
+ return;
+ }
+
+ localStorage.setItem('theme', theme.value);
+ setColourTheme(theme.value);
+ };
+
+ return (
+
+
+
+
+ );
+};
+
+export default SettingsContainer;
diff --git a/src/css/base.css b/src/css/base.css
index bb63565..ff89e22 100644
--- a/src/css/base.css
+++ b/src/css/base.css
@@ -2,10 +2,32 @@
@tailwind components;
@tailwind utilities;
+/* With color-scheme the UA will style some elements appropriately. */
+:root {
+ color-scheme: light dark;
+}
+@media (prefers-color-scheme: dark) {
+ :root {
+ color-scheme: dark;
+ }
+}
+.light {
+ color-scheme: light;
+}
+.dark {
+ color-scheme: dark;
+}
+/* Transition color scheme change. */
+html {
+ transition-duration: 150ms;
+ transition-property: color, background-color;
+}
+
html {
@apply font-neogrote;
@apply font-normal;
- @apply bg-white;
+ @apply bg-slate-50;
+ @apply dark:bg-slate-900;
}
canvas {
diff --git a/src/index.js b/src/index.js
index e152e9c..8b82cd0 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,6 +1,7 @@
import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
+import { setColourTheme } from './lib/colourThemeUtils';
const basename =
process.env.NODE_ENV === 'development' ? undefined : '/scumm-nes';
@@ -12,6 +13,8 @@ root.render(
,
);
+setColourTheme(localStorage.getItem('theme'));
+
if (process.env.NODE_ENV === 'production' && insights) {
// Load the analytics.
insights.init('OTy7QFoUv4bUuKzo');
diff --git a/src/lib/colourThemeUtils.js b/src/lib/colourThemeUtils.js
new file mode 100644
index 0000000..e8e8814
--- /dev/null
+++ b/src/lib/colourThemeUtils.js
@@ -0,0 +1,25 @@
+const mediaQuery = window.matchMedia('(prefers-color-scheme:dark)');
+const mediaQueryEventListener = (event) => {
+ addColourThemeClass(event.matches ? 'dark' : 'light');
+};
+
+// Set the `dark` or `light` class name on the tag.
+const addColourThemeClass = (theme) => {
+ document.documentElement.classList.remove('dark', 'light');
+ document.documentElement.classList.add(theme);
+};
+
+// Sets the colour theme, and update the DOM accordingly.
+// If no theme is set, listens to changes in the system theme.
+const setColourTheme = (theme = null) => {
+ mediaQuery.removeEventListener('change', mediaQueryEventListener);
+
+ if (!theme) {
+ mediaQuery.addEventListener('change', mediaQueryEventListener);
+ theme = mediaQuery.matches ? 'dark' : 'light';
+ }
+
+ addColourThemeClass(theme);
+};
+
+export { setColourTheme };
diff --git a/tailwind.config.js b/tailwind.config.js
index 1269d3c..f845233 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -2,6 +2,7 @@ import tailwindcssForms from '@tailwindcss/forms';
import colors from 'tailwindcss/colors';
const config = {
+ darkMode: 'selector',
content: ['./src/**/*.{html,js}'],
theme: {
extend: {
|