committed by
GitHub
13 changed files with 921 additions and 1799 deletions
@ -5,13 +5,13 @@ |
|||
"description": "Meshtastic web client", |
|||
"license": "GPL-3.0-only", |
|||
"scripts": { |
|||
"build": "rsbuild build", |
|||
"build:analyze": "BUNDLE_ANALYZE=true rsbuild build", |
|||
"build": "vite build", |
|||
"build:analyze": "BUNDLE_ANALYZE=true vite build", |
|||
"check": "biome check src/", |
|||
"check:fix": "pnpm check --write src/", |
|||
"format": "biome format --write src/", |
|||
"dev": "rsbuild dev --open", |
|||
"preview": "rsbuild preview", |
|||
"dev": "vite dev --open", |
|||
"preview": "vite preview", |
|||
"package": "gzipper c -i html,js,css,png,ico,svg,webmanifest,txt dist dist/output && tar -cvf dist/build.tar -C ./dist/output/ $(ls ./dist/output/)", |
|||
"postinstall": "npx simple-git-hooks" |
|||
}, |
|||
@ -24,6 +24,10 @@ |
|||
"npm run format" |
|||
] |
|||
}, |
|||
"engines": { |
|||
"node": ">=20.0.0", |
|||
"pnpm": ">=10.0.0" |
|||
}, |
|||
"repository": { |
|||
"type": "git", |
|||
"url": "git+https://github.com/meshtastic/web.git" |
|||
@ -73,8 +77,6 @@ |
|||
}, |
|||
"devDependencies": { |
|||
"@biomejs/biome": "^1.9.4", |
|||
"@rsbuild/core": "^1.2.8", |
|||
"@rsbuild/plugin-react": "^1.1.0", |
|||
"@tailwindcss/postcss": "^4.0.7", |
|||
"@types/chrome": "^0.0.304", |
|||
"@types/js-cookie": "^3.0.6", |
|||
@ -83,6 +85,8 @@ |
|||
"@types/react-dom": "^19.0.4", |
|||
"@types/w3c-web-serial": "^1.0.7", |
|||
"@types/web-bluetooth": "^0.0.20", |
|||
"@vitejs/plugin-react": "^4.3.4", |
|||
"autoprefixer": "^10.4.20", |
|||
"gzipper": "^8.2.0", |
|||
"postcss": "^8.5.1", |
|||
"simple-git-hooks": "^2.11.1", |
|||
@ -90,7 +94,7 @@ |
|||
"tailwindcss": "^4.0.7", |
|||
"tailwindcss-animate": "^1.0.7", |
|||
"tar": "^7.4.3", |
|||
"typescript": "^5.7.3" |
|||
}, |
|||
"packageManager": "[email protected]" |
|||
"typescript": "^5.7.3", |
|||
"vite": "^6.1.1" |
|||
} |
|||
} |
|||
|
|||
File diff suppressed because it is too large
@ -1,30 +0,0 @@ |
|||
import { execSync } from "node:child_process"; |
|||
import { defineConfig } from "@rsbuild/core"; |
|||
import { pluginReact } from "@rsbuild/plugin-react"; |
|||
|
|||
let hash = ""; |
|||
|
|||
try { |
|||
hash = execSync("git rev-parse --short HEAD").toString().trim(); |
|||
} catch (error) { |
|||
hash = "DEV"; |
|||
} |
|||
|
|||
export default defineConfig({ |
|||
plugins: [pluginReact()], |
|||
source: { |
|||
define: { |
|||
"process.env.COMMIT_HASH": JSON.stringify(hash), |
|||
}, |
|||
alias: { |
|||
"@app": "./src", |
|||
"@pages": "./src/pages", |
|||
"@components": "./src/components", |
|||
"@core": "./src/core", |
|||
"@layouts": "./src/layouts", |
|||
}, |
|||
}, |
|||
html: { |
|||
title: "Meshtastic Web", |
|||
}, |
|||
}); |
|||
@ -1,39 +1,44 @@ |
|||
import { useTheme } from "@app/core/hooks/useTheme"; |
|||
import { Moon, Sun } from "lucide-react"; |
|||
import React from "react"; |
|||
import { cn } from "@app/core/utils/cn"; |
|||
import { Monitor, Moon, Sun } from "lucide-react"; |
|||
|
|||
type Theme = "light" | "dark"; |
|||
type ThemePreference = "light" | "dark" | "system"; |
|||
|
|||
export default function ThemeSwitcher({ |
|||
className = "", |
|||
}: { className?: string }) { |
|||
const currentTheme = useTheme(); // Get current theme from DOM
|
|||
const [theme, setTheme] = React.useState<Theme>(currentTheme); |
|||
|
|||
React.useEffect(() => { |
|||
document.documentElement.setAttribute("data-theme", theme); |
|||
localStorage.setItem("theme", theme); |
|||
}, [theme]); |
|||
}: { |
|||
className?: string; |
|||
}) { |
|||
const { theme, preference, setPreference } = useTheme(); |
|||
|
|||
const themeIcons = { |
|||
light: ( |
|||
<Sun className="size-5 transition-transform duration-300 scale-100" /> |
|||
), |
|||
dark: ( |
|||
<Moon className="size-5 transition-transform duration-300 scale-100" /> |
|||
), |
|||
light: <Sun className="size-5" />, |
|||
dark: <Moon className="size-5" />, |
|||
system: <Monitor className="size-5" />, |
|||
}; |
|||
|
|||
const toggleTheme = () => setTheme(theme === "light" ? "dark" : "light"); |
|||
const toggleTheme = () => { |
|||
const preferences: ThemePreference[] = ["light", "dark", "system"]; |
|||
const currentIndex = preferences.indexOf(preference); |
|||
const nextPreference = preferences[(currentIndex + 1) % preferences.length]; |
|||
setPreference(nextPreference); |
|||
}; |
|||
|
|||
return ( |
|||
<button |
|||
type="button" |
|||
className={`transition-all duration-300 hover:text-accent ${className}`} |
|||
className={cn( |
|||
"transition-all duration-300 scale-100 cursor-pointer m-6 p-2", |
|||
className, |
|||
)} |
|||
onClick={toggleTheme} |
|||
aria-label={`Current theme: ${theme}. Click to change theme.`} |
|||
aria-label={ |
|||
preference === "system" |
|||
? `System theme (currently ${theme}). Click to change theme.` |
|||
: `Current theme: ${theme}. Click to change theme.` |
|||
} |
|||
> |
|||
{themeIcons[theme]} |
|||
{themeIcons[preference]} |
|||
</button> |
|||
); |
|||
} |
|||
|
|||
@ -1,37 +1,42 @@ |
|||
import { useEffect, useState } from "react"; |
|||
import { useCallback, useEffect, useState } from "react"; |
|||
|
|||
type Theme = "light" | "dark"; |
|||
type Theme = "light" | "dark" | "system"; |
|||
|
|||
export function useTheme() { |
|||
const [theme, setTheme] = useState<Theme>(() => { |
|||
if (typeof window === "undefined") return "light"; |
|||
return ( |
|||
(document.documentElement.getAttribute("data-theme") as Theme) || "light" |
|||
); |
|||
}); |
|||
const getSystemTheme = () => |
|||
window.matchMedia("(prefers-color-scheme: dark)").matches |
|||
? "dark" |
|||
: "light"; |
|||
|
|||
const getStoredPreference = useCallback( |
|||
(): Theme => (localStorage.getItem("theme") as Theme) || "system", |
|||
[], |
|||
); |
|||
|
|||
const [preference, setPreference] = useState<Theme>(() => |
|||
typeof window !== "undefined" ? getStoredPreference() : "light", |
|||
); |
|||
|
|||
const theme = preference === "system" ? getSystemTheme() : preference; |
|||
|
|||
useEffect(() => { |
|||
document.documentElement.setAttribute("data-theme", theme); |
|||
}, [theme]); |
|||
|
|||
useEffect(() => { |
|||
const observer = new MutationObserver((mutations) => { |
|||
for (const mutation of mutations) { |
|||
if ( |
|||
mutation.type === "attributes" && |
|||
mutation.attributeName === "data-theme" |
|||
) { |
|||
const newTheme = document.documentElement.getAttribute( |
|||
"data-theme", |
|||
) as Theme; |
|||
setTheme(newTheme); |
|||
} |
|||
} |
|||
}); |
|||
|
|||
observer.observe(document.documentElement, { |
|||
attributes: true, |
|||
attributeFilter: ["data-theme"], |
|||
}); |
|||
|
|||
return () => observer.disconnect(); |
|||
}, []); |
|||
|
|||
return theme; |
|||
if (preference !== "system") return; |
|||
|
|||
const media = window.matchMedia("(prefers-color-scheme: dark)"); |
|||
const updateTheme = () => setPreference(getStoredPreference()); |
|||
|
|||
media.addEventListener("change", updateTheme); |
|||
return () => media.removeEventListener("change", updateTheme); |
|||
}, [preference, getStoredPreference]); |
|||
|
|||
const setPreferenceValue = (newPreference: Theme) => { |
|||
localStorage.setItem("theme", newPreference); |
|||
setPreference(newPreference); |
|||
}; |
|||
|
|||
return { theme, preference, setPreference: setPreferenceValue }; |
|||
} |
|||
|
|||
@ -1,45 +1,30 @@ |
|||
import { execSync } from "node:child_process"; |
|||
import { resolve } from "node:path"; |
|||
import react from "@vitejs/plugin-react"; |
|||
import { visualizer } from "rollup-plugin-visualizer"; |
|||
import { defineConfig } from "vite"; |
|||
import EnvironmentPlugin from "vite-plugin-environment"; |
|||
|
|||
let hash = ""; |
|||
import { defineConfig } from 'vite'; |
|||
import react from '@vitejs/plugin-react'; |
|||
import { execSync } from 'node:child_process'; |
|||
import path from 'path'; |
|||
|
|||
let hash = ''; |
|||
try { |
|||
hash = execSync("git rev-parse --short HEAD").toString().trim(); |
|||
hash = execSync('git rev-parse --short HEAD').toString().trim(); |
|||
} catch (error) { |
|||
hash = "DEVELOPMENT"; |
|||
hash = 'DEV'; |
|||
} |
|||
|
|||
export default defineConfig({ |
|||
plugins: [ |
|||
react(), |
|||
EnvironmentPlugin({ |
|||
COMMIT_HASH: hash, |
|||
}), |
|||
// VitePWA({
|
|||
// registerType: "autoUpdate",
|
|||
// devOptions: {
|
|||
// enabled: true
|
|||
// }
|
|||
// })
|
|||
], |
|||
build: { |
|||
target: "esnext", |
|||
assetsDir: "", |
|||
rollupOptions: { |
|||
plugins: [visualizer()], |
|||
}, |
|||
plugins: [react()], |
|||
define: { |
|||
'process.env.COMMIT_HASH': JSON.stringify(hash), |
|||
}, |
|||
resolve: { |
|||
alias: { |
|||
"@app": resolve(__dirname, "./src"), |
|||
"@pages": resolve(__dirname, "./src/pages"), |
|||
"@components": resolve(__dirname, "./src/components"), |
|||
"@core": resolve(__dirname, "./src/core"), |
|||
"@layouts": resolve(__dirname, "./src/layouts"), |
|||
'@app': path.resolve(__dirname, './src'), |
|||
'@pages': path.resolve(__dirname, './src/pages'), |
|||
'@components': path.resolve(__dirname, './src/components'), |
|||
'@core': path.resolve(__dirname, './src/core'), |
|||
'@layouts': path.resolve(__dirname, './src/layouts'), |
|||
}, |
|||
}, |
|||
}); |
|||
server: { |
|||
port: 3000 |
|||
} |
|||
}); |
|||
Loading…
Reference in new issue