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