Add light theme
This commit is contained in:
@@ -1,12 +1,13 @@
|
|||||||
#root {
|
#root {
|
||||||
background-color: var(--color-neutral1);
|
background-color: var(--color-background1);
|
||||||
|
color: var(--color-text);
|
||||||
display: grid;
|
display: grid;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
|
||||||
> div {
|
> div {
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
background-color: var(--color-neutral0);
|
background-color: var(--color-background0);
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: max-content;
|
height: max-content;
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export const CartesianGrid = () => (
|
|||||||
key={index}
|
key={index}
|
||||||
y1={Math.max(1, Math.min(height - 1, (height * index) / 4))}
|
y1={Math.max(1, Math.min(height - 1, (height * index) / 4))}
|
||||||
y2={Math.max(1, Math.min(height - 1, (height * index) / 4))}
|
y2={Math.max(1, Math.min(height - 1, (height * index) / 4))}
|
||||||
stroke='var(--color-neutral1)'
|
stroke='var(--color-background1)'
|
||||||
strokeWidth={1}
|
strokeWidth={1}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
.chart {
|
.chart {
|
||||||
color: var(--color-neutral2);
|
color: var(--color-text-subdued);
|
||||||
display: grid;
|
display: grid;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useAnimationFrame } from '@/hooks/use-animation-frame';
|
import { useAnimationFrame } from '@/hooks/use-animation-frame';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import { Switch } from '../switch';
|
||||||
|
|
||||||
const formatUptime = (value: number) => {
|
const formatUptime = (value: number) => {
|
||||||
const seconds = String(Math.floor(value % 60)).padStart(2, '0');
|
const seconds = String(Math.floor(value % 60)).padStart(2, '0');
|
||||||
@@ -37,6 +38,12 @@ const Uptime = ({ boot_time }: Pick<StaticData, 'boot_time'>) => {
|
|||||||
|
|
||||||
export const Static = () => {
|
export const Static = () => {
|
||||||
const { data: staticData } = useQuery<StaticData>({ queryKey: ['static'] });
|
const { data: staticData } = useQuery<StaticData>({ queryKey: ['static'] });
|
||||||
|
const root = useRef(document.getElementById('root')!);
|
||||||
|
const [dark, setDark] = useState(window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
root.current.setAttribute('data-theme', dark ? 'dark' : 'light');
|
||||||
|
}, [dark]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
staticData && (
|
staticData && (
|
||||||
@@ -51,6 +58,12 @@ export const Static = () => {
|
|||||||
<small>Kernel</small>
|
<small>Kernel</small>
|
||||||
<h3>{staticData.kernel_version}</h3>
|
<h3>{staticData.kernel_version}</h3>
|
||||||
{staticData.boot_time && <Uptime boot_time={staticData.boot_time} />}
|
{staticData.boot_time && <Uptime boot_time={staticData.boot_time} />}
|
||||||
|
|
||||||
|
<Switch
|
||||||
|
checked={dark}
|
||||||
|
label={`${dark ? 'Dark' : 'Light'} theme`}
|
||||||
|
onChange={({ target }) => setDark(target.checked)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|||||||
46
src/client/components/switch/index.css
Normal file
46
src/client/components/switch/index.css
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
.switch-wrapper {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, max-content);
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch {
|
||||||
|
position: relative;
|
||||||
|
width: 64px;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch input {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.slider {
|
||||||
|
position: absolute;
|
||||||
|
cursor: pointer;
|
||||||
|
inset: 0;
|
||||||
|
background-color: var(--color-background2);
|
||||||
|
transition: 200ms background-color;
|
||||||
|
border-radius: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider:before {
|
||||||
|
position: absolute;
|
||||||
|
content: "";
|
||||||
|
aspect-ratio: 1;
|
||||||
|
left: 4px;
|
||||||
|
top: 4px;
|
||||||
|
bottom: 4px;
|
||||||
|
background-color: var(--color-background0);
|
||||||
|
transition: 200ms transform;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked + .slider {
|
||||||
|
background-color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked + .slider:before {
|
||||||
|
transform: translateX(32px);
|
||||||
|
}
|
||||||
18
src/client/components/switch/index.tsx
Normal file
18
src/client/components/switch/index.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import type { ComponentPropsWithoutRef } from 'react';
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
type Props = ComponentPropsWithoutRef<'input'> & {
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Switch = ({ label, ...rest }: Props) => {
|
||||||
|
return (
|
||||||
|
<label className='switch-wrapper'>
|
||||||
|
<span className={'switch'}>
|
||||||
|
<input {...rest} type='checkbox' />
|
||||||
|
<span className='slider' />
|
||||||
|
</span>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -7,19 +7,34 @@
|
|||||||
--color-neutral5: #93a1a1;
|
--color-neutral5: #93a1a1;
|
||||||
--color-neutral6: #eee8d5;
|
--color-neutral6: #eee8d5;
|
||||||
--color-neutral7: #fdf6e3;
|
--color-neutral7: #fdf6e3;
|
||||||
|
--color-primary: #2aa198;
|
||||||
|
|
||||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
|
||||||
color: var(--color-neutral6);
|
|
||||||
|
|
||||||
font-synthesis: none;
|
font-synthesis: none;
|
||||||
text-rendering: optimizeLegibility;
|
text-rendering: optimizeLegibility;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] {
|
||||||
|
--color-background0: var(--color-neutral7);
|
||||||
|
--color-background1: var(--color-neutral6);
|
||||||
|
--color-background2: var(--color-neutral5);
|
||||||
|
--color-text: var(--color-neutral1);
|
||||||
|
--color-text-subdued: var(--color-neutral5);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] {
|
||||||
|
--color-background0: var(--color-neutral0);
|
||||||
|
--color-background1: var(--color-neutral1);
|
||||||
|
--color-background2: var(--color-neutral2);
|
||||||
|
--color-text: var(--color-neutral6);
|
||||||
|
--color-text-subdued: var(--color-neutral2);
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user