fix(web): dark mode text color fixes

fixes #544
This commit is contained in:
Paul Armstrong 2021-01-19 08:44:18 -08:00 committed by Blake Blackshear
parent 11016b8486
commit 2132352639
11 changed files with 273 additions and 179 deletions

View File

@ -24,7 +24,7 @@ export default function App() {
<div /> <div />
) : ( ) : (
<Config.Provider value={config}> <Config.Provider value={config}>
<div className="flex md:min-h-screen w-full bg-gray-100 dark:bg-gray-800"> <div className="md:flex flex-col md:flex-row md:min-h-screen w-full bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-white">
<Sidebar /> <Sidebar />
<div className="p-4 min-w-0"> <div className="p-4 min-w-0">
<Router> <Router>

View File

@ -1,4 +1,5 @@
import { h } from 'preact'; import { h } from 'preact';
import Box from './components/Box';
import Heading from './components/Heading'; import Heading from './components/Heading';
import Link from './components/Link'; import Link from './components/Link';
import Switch from './components/Switch'; import Switch from './components/Switch';
@ -32,45 +33,39 @@ export default function Camera({ camera, url }) {
} }
return ( return (
<div> <div className="space-y-4">
<Heading size="2xl">{camera}</Heading> <Heading size="2xl">{camera}</Heading>
<img <Box>
width={cameraConfig.width} <img
height={cameraConfig.height} width={cameraConfig.width}
key={searchParamsString} height={cameraConfig.height}
src={`${apiHost}/api/${camera}?${searchParamsString}`} key={searchParamsString}
/> src={`${apiHost}/api/${camera}?${searchParamsString}`}
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 p-4"> />
</Box>
<Box className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 p-4">
<Switch checked={getBoolean('bbox')} id="bbox" label="Bounding box" onChange={handleSetOption} /> <Switch checked={getBoolean('bbox')} id="bbox" label="Bounding box" onChange={handleSetOption} />
<Switch checked={getBoolean('timestamp')} id="timestamp" label="Timestamp" onChange={handleSetOption} /> <Switch checked={getBoolean('timestamp')} id="timestamp" label="Timestamp" onChange={handleSetOption} />
<Switch checked={getBoolean('zones')} id="zones" label="Zones" onChange={handleSetOption} /> <Switch checked={getBoolean('zones')} id="zones" label="Zones" onChange={handleSetOption} />
<Switch checked={getBoolean('mask')} id="mask" label="Masks" onChange={handleSetOption} /> <Switch checked={getBoolean('mask')} id="mask" label="Masks" onChange={handleSetOption} />
<Switch checked={getBoolean('motion')} id="motion" label="Motion boxes" onChange={handleSetOption} /> <Switch checked={getBoolean('motion')} id="motion" label="Motion boxes" onChange={handleSetOption} />
<Switch checked={getBoolean('regions')} id="regions" label="Regions" onChange={handleSetOption} /> <Switch checked={getBoolean('regions')} id="regions" label="Regions" onChange={handleSetOption} />
</div> <Link href={`/cameras/${camera}/editor`}>Mask & Zone creator</Link>
<div> </Box>
<div className="space-y-4">
<Heading size="sm">Tracked objects</Heading> <Heading size="sm">Tracked objects</Heading>
<ul className="flex flex-row flex-wrap space-x-4"> <div className="grid grid-cols-3 md:grid-cols-4 gap-4">
{cameraConfig.objects.track.map((objectType) => { {cameraConfig.objects.track.map((objectType) => {
return ( return (
<li key={objectType}> <Box key={objectType} hover href={`/events?camera=${camera}&label=${objectType}`}>
<Link href={`/events?camera=${camera}&label=${objectType}`}> <Heading size="sm">{objectType}</Heading>
<span className="capitalize">{objectType}</span> <img src={`${apiHost}/api/${camera}/${objectType}/best.jpg?crop=1&h=150`} />
<img src={`${apiHost}/api/${camera}/${objectType}/best.jpg?crop=1&h=150`} /> </Box>
</Link>
</li>
); );
})} })}
</ul> </div>
</div>
<div>
<Heading size="sm">Options</Heading>
<ul>
<li>
<Link href={`/cameras/${camera}/editor`}>Mask & Zone creator</Link>
</li>
</ul>
</div> </div>
</div> </div>
); );

View File

@ -1,4 +1,5 @@
import { h } from 'preact'; import { h } from 'preact';
import Box from './components/Box';
import Button from './components/Button'; import Button from './components/Button';
import Heading from './components/Heading'; import Heading from './components/Heading';
import Switch from './components/Switch'; import Switch from './components/Switch';
@ -11,6 +12,7 @@ export default function CameraMasks({ camera, url }) {
const apiHost = useContext(ApiHost); const apiHost = useContext(ApiHost);
const imageRef = useRef(null); const imageRef = useRef(null);
const [imageScale, setImageScale] = useState(1); const [imageScale, setImageScale] = useState(1);
const [snap, setSnap] = useState(true);
if (!(camera in config.cameras)) { if (!(camera in config.cameras)) {
return <div>{`No camera named ${camera}`}</div>; return <div>{`No camera named ${camera}`}</div>;
@ -203,23 +205,39 @@ ${Object.keys(objectMaskPoints)
.join('\n')}`); .join('\n')}`);
}, [objectMaskPoints]); }, [objectMaskPoints]);
const handleChangeSnap = useCallback(
(id, value) => {
setSnap(value);
},
[setSnap]
);
return ( return (
<div class="flex-col space-y-4" style={`max-width: ${width}px`}> <div class="flex-col space-y-4" style={`max-width: ${width}px`}>
<Heading size="2xl">{camera} mask & zone creator</Heading> <Heading size="2xl">{camera} mask & zone creator</Heading>
<p>
This tool can help you create masks & zones for your {camera} camera. When done, copy each mask configuration <Box>
into your <code className="font-mono">config.yml</code> file restart your Frigate instance to save your changes. <p>
</p> This tool can help you create masks & zones for your {camera} camera. When done, copy each mask configuration
<div className="relative"> into your <code className="font-mono">config.yml</code> file restart your Frigate instance to save your
<img ref={imageRef} width={width} height={height} src={`${apiHost}/api/${camera}/latest.jpg`} /> changes.
<EditableMask </p>
onChange={handleUpdateEditable} </Box>
points={editing.subkey ? editing.set[editing.key][editing.subkey] : editing.set[editing.key]}
scale={imageScale} <Box className="space-y-4">
width={width} <div className="relative">
height={height} <img ref={imageRef} width={width} height={height} src={`${apiHost}/api/${camera}/latest.jpg`} />
/> <EditableMask
</div> onChange={handleUpdateEditable}
points={editing.subkey ? editing.set[editing.key][editing.subkey] : editing.set[editing.key]}
scale={imageScale}
snap={snap}
width={width}
height={height}
/>
</div>
<Switch checked={snap} label="Snap to edges" onChange={handleChangeSnap} />
</Box>
<div class="flex-col space-y-4"> <div class="flex-col space-y-4">
<MaskValues <MaskValues
@ -276,14 +294,25 @@ function objectYamlKeyPrefix(points, key, subkey) {
return ` - `; return ` - `;
} }
function EditableMask({ onChange, points, scale, width, height }) { const MaskInset = 20;
function EditableMask({ onChange, points, scale, snap, width, height }) {
if (!points) { if (!points) {
return null; return null;
} }
const boundingRef = useRef(null); const boundingRef = useRef(null);
function boundedSize(value, maxValue) { function boundedSize(value, maxValue) {
return Math.min(Math.max(0, Math.round(value)), maxValue); const newValue = Math.min(Math.max(0, Math.round(value)), maxValue);
if (snap) {
if (newValue <= MaskInset) {
return 0;
} else if (maxValue - newValue <= MaskInset) {
return maxValue;
}
}
return newValue;
} }
const handleMovePoint = useCallback( const handleMovePoint = useCallback(
@ -291,35 +320,40 @@ function EditableMask({ onChange, points, scale, width, height }) {
if (newX < 0 && newY < 0) { if (newX < 0 && newY < 0) {
return; return;
} }
const x = boundedSize(newX / scale, width); let x = boundedSize(newX / scale, width, snap);
const y = boundedSize(newY / scale, height); let y = boundedSize(newY / scale, height, snap);
const newPoints = [...points]; const newPoints = [...points];
newPoints[index] = [x, y]; newPoints[index] = [x, y];
onChange(newPoints); onChange(newPoints);
}, },
[scale, points] [scale, points, snap]
); );
// Add a new point between the closest two other points // Add a new point between the closest two other points
const handleAddPoint = useCallback( const handleAddPoint = useCallback(
(event) => { (event) => {
const { offsetX, offsetY } = event; const { offsetX, offsetY } = event;
const scaledX = boundedSize(offsetX / scale, width); const scaledX = boundedSize((offsetX - MaskInset) / scale, width, snap);
const scaledY = boundedSize(offsetY / scale, height); const scaledY = boundedSize((offsetY - MaskInset) / scale, height, snap);
const newPoint = [scaledX, scaledY]; const newPoint = [scaledX, scaledY];
const closest = points.reduce((a, b, i) => {
if (!a) { let closest;
return b; const { index } = points.reduce(
} (result, point, i) => {
return distance(a, newPoint) < distance(b, newPoint) ? a : b; const nextPoint = points.length === i + 1 ? points[0] : points[i + 1];
}, null); const distance0 = Math.sqrt(Math.pow(point[0] - newPoint[0], 2) + Math.pow(point[1] - newPoint[1], 2));
const index = points.indexOf(closest); const distance1 = Math.sqrt(Math.pow(point[0] - nextPoint[0], 2) + Math.pow(point[1] - nextPoint[1], 2));
const distance = distance0 + distance1;
return distance < result.distance ? { distance, index: i } : result;
},
{ distance: Infinity, index: -1 }
);
const newPoints = [...points]; const newPoints = [...points];
newPoints.splice(index, 0, newPoint); newPoints.splice(index, 0, newPoint);
console.log(points, newPoints);
onChange(newPoints); onChange(newPoints);
}, },
[scale, points, onChange] [scale, points, onChange, snap]
); );
const handleRemovePoint = useCallback( const handleRemovePoint = useCallback(
@ -334,7 +368,7 @@ function EditableMask({ onChange, points, scale, width, height }) {
const scaledPoints = useMemo(() => scalePolylinePoints(points, scale), [points, scale]); const scaledPoints = useMemo(() => scalePolylinePoints(points, scale), [points, scale]);
return ( return (
<div onclick={handleAddPoint}> <div className="absolute" style={`inset: -${MaskInset}px`}>
{!scaledPoints {!scaledPoints
? null ? null
: scaledPoints.map(([x, y], i) => ( : scaledPoints.map(([x, y], i) => (
@ -343,17 +377,12 @@ function EditableMask({ onChange, points, scale, width, height }) {
index={i} index={i}
onMove={handleMovePoint} onMove={handleMovePoint}
onRemove={handleRemovePoint} onRemove={handleRemovePoint}
x={x} x={x + MaskInset}
y={y} y={y + MaskInset}
/> />
))} ))}
<svg <div className="absolute inset-0 right-0 bottom-0" onclick={handleAddPoint} ref={boundingRef} />
ref={boundingRef} <svg width="100%" height="100%" className="absolute pointer-events-none" style={`inset: ${MaskInset}px`}>
width="100%"
height="100%"
className="absolute"
style="top: 0; left: 0; right: 0; bottom: 0;"
>
{!scaledPoints ? null : ( {!scaledPoints ? null : (
<g> <g>
<polyline points={polylinePointsToPolyline(scaledPoints)} fill="rgba(244,0,0,0.5)" /> <polyline points={polylinePointsToPolyline(scaledPoints)} fill="rgba(244,0,0,0.5)" />
@ -410,11 +439,7 @@ function MaskValues({
); );
return ( return (
<div <Box className="overflow-hidden" onmouseover={handleMousein} onmouseout={handleMouseout}>
className="overflow-hidden rounded border-gray-500 border-solid border p-2"
onmouseover={handleMousein}
onmouseout={handleMouseout}
>
<div class="flex space-x-4"> <div class="flex space-x-4">
<Heading className="flex-grow self-center" size="base"> <Heading className="flex-grow self-center" size="base">
{title} {title}
@ -422,7 +447,7 @@ function MaskValues({
<Button onClick={onCopy}>Copy</Button> <Button onClick={onCopy}>Copy</Button>
<Button onClick={onCreate}>Add</Button> <Button onClick={onCreate}>Add</Button>
</div> </div>
<pre class="overflow-hidden font-mono text-gray-900 dark:text-gray-100"> <pre class="relative overflow-auto font-mono text-gray-900 dark:text-gray-100 rounded bg-gray-100 dark:bg-gray-800 p-2">
{yamlPrefix} {yamlPrefix}
{Object.keys(points).map((mainkey) => { {Object.keys(points).map((mainkey) => {
if (isMulti) { if (isMulti) {
@ -458,7 +483,7 @@ function MaskValues({
} }
})} })}
</pre> </pre>
</div> </Box>
); );
} }
@ -489,10 +514,6 @@ function Item({ mainkey, subkey, editing, handleEdit, points, showButtons, handl
); );
} }
function distance([x0, y0], [x1, y1]) {
return Math.sqrt(Math.pow(x0 - x1, 2) + Math.pow(y0 - y1, 2));
}
function getPolylinePoints(polyline) { function getPolylinePoints(polyline) {
if (!polyline) { if (!polyline) {
return; return;
@ -529,10 +550,13 @@ function PolyPoint({ boundingRef, index, x, y, onMove, onRemove }) {
const handleDragOver = useCallback( const handleDragOver = useCallback(
(event) => { (event) => {
if (event.target !== boundingRef.current && !boundingRef.current.contains(event.target)) { if (
!boundingRef.current ||
(event.target !== boundingRef.current && !boundingRef.current.contains(event.target))
) {
return; return;
} }
onMove(index, event.layerX, event.layerY - PolyPointRadius); onMove(index, event.layerX - PolyPointRadius * 2, event.layerY - PolyPointRadius * 2);
}, },
[onMove, index, boundingRef.current] [onMove, index, boundingRef.current]
); );

View File

@ -1,4 +1,5 @@
import { h } from 'preact'; import { h } from 'preact';
import Box from './components/Box';
import Events from './Events'; import Events from './Events';
import Heading from './components/Heading'; import Heading from './components/Heading';
import { route } from 'preact-router'; import { route } from 'preact-router';
@ -26,11 +27,12 @@ function Camera({ name }) {
const href = `/cameras/${name}`; const href = `/cameras/${name}`;
return ( return (
<div className="bg-white dark:bg-gray-700 shadow-lg rounded-lg p-4 hover:bg-gray-300 hover:dark:bg-gray-500 dark:hover:text-gray-900"> <Box
<a className="dark:hover:text-gray-900" href={href}> className="bg-white dark:bg-gray-700 shadow-lg rounded-lg p-4 hover:bg-gray-300 hover:dark:bg-gray-500 dark:hover:text-gray-900 dark:hover:text-gray-900"
<Heading size="base">{name}</Heading> href={href}
<img className="w-full" src={`${apiHost}/api/${name}/latest.jpg`} /> >
</a> <Heading size="base">{name}</Heading>
</div> <img className="w-full" src={`${apiHost}/api/${name}/latest.jpg`} />
</Box>
); );
} }

View File

@ -1,6 +1,9 @@
import { h } from 'preact'; import { h, Fragment } from 'preact';
import { ApiHost } from './context'; import { ApiHost } from './context';
import Box from './components/Box';
import Heading from './components/Heading'; import Heading from './components/Heading';
import Link from './components/Link';
import { Table, Thead, Tbody, Tfoot, Th, Tr, Td } from './components/Table';
import { useContext, useEffect, useState } from 'preact/hooks'; import { useContext, useEffect, useState } from 'preact/hooks';
export default function Event({ eventId }) { export default function Event({ eventId }) {
@ -22,24 +25,66 @@ export default function Event({ eventId }) {
); );
} }
const datetime = new Date(data.start_time * 1000); const startime = new Date(data.start_time * 1000);
const endtime = new Date(data.end_time * 1000);
return ( return (
<div> <div className="space-y-4">
<Heading> <Heading>
{data.camera} {data.label} <span className="text-sm">{datetime.toLocaleString()}</span> {data.camera} {data.label} <span className="text-sm">{startime.toLocaleString()}</span>
</Heading> </Heading>
<img
src={`${apiHost}/clips/${data.camera}-${eventId}.jpg`}
alt={`${data.label} at ${(data.top_score * 100).toFixed(1)}% confidence`}
/>
{data.has_clip ? (
<video className="w-96" src={`${apiHost}/clips/${data.camera}-${eventId}.mp4`} controls />
) : (
<p>No clip available</p>
)}
<pre>{JSON.stringify(data, null, 2)}</pre> <Box>
{data.has_clip ? (
<Fragment>
<Heading size="sm">Clip</Heading>
<video className="w-100" src={`${apiHost}/clips/${data.camera}-${eventId}.mp4`} controls />
</Fragment>
) : (
<p>No clip available</p>
)}
</Box>
<Box>
<Heading size="sm">{data.has_snapshot ? 'Best image' : 'Thumbnail'}</Heading>
<img
src={
data.has_snapshot
? `${apiHost}/clips/${data.camera}-${eventId}.jpg`
: `data:image/jpeg;base64,${data.thumbnail}`
}
alt={`${data.label} at ${(data.top_score * 100).toFixed(1)}% confidence`}
/>
</Box>
<Table>
<Thead>
<Th>Key</Th>
<Th>Value</Th>
</Thead>
<Tbody>
<Tr>
<Td>Camera</Td>
<Td>
<Link href={`/cameras/${data.camera}`}>{data.camera}</Link>
</Td>
</Tr>
<Tr index={1}>
<Td>Timeframe</Td>
<Td>
{startime.toLocaleString()} {endtime.toLocaleString()}
</Td>
</Tr>
<Tr>
<Td>Score</Td>
<Td>{(data.top_score * 100).toFixed(2)}%</Td>
</Tr>
<Tr index={1}>
<Td>Zones</Td>
<Td>{data.zones.join(', ')}</Td>
</Tr>
</Tbody>
</Table>
</div> </div>
); );
} }

View File

@ -1,5 +1,6 @@
import { h } from 'preact'; import { h } from 'preact';
import { ApiHost } from './context'; import { ApiHost } from './context';
import Box from './components/Box';
import Heading from './components/Heading'; import Heading from './components/Heading';
import Link from './components/Link'; import Link from './components/Link';
import { route } from 'preact-router'; import { route } from 'preact-router';
@ -19,71 +20,82 @@ export default function Events({ url } = {}) {
setEvents(data); setEvents(data);
}, [searchParamsString]); }, [searchParamsString]);
const searchKeys = Array.from(searchParams.keys());
return ( return (
<div> <div className="space-y-4">
<Heading>Events</Heading> <Heading>Events</Heading>
<div className="flex flex-wrap space-x-2">
{Array.from(searchParams.keys()).map((filterKey) => ( {searchKeys.length ? (
<UnFilterable <Box>
paramName={filterKey} <Heading size="sm">Filters</Heading>
searchParams={searchParamsString} <div className="flex flex-wrap space-x-2">
name={`${filterKey}: ${searchParams.get(filterKey)}`} {searchKeys.map((filterKey) => (
/> <UnFilterable
))} paramName={filterKey}
</div> searchParams={searchParamsString}
<Table> name={`${filterKey}: ${searchParams.get(filterKey)}`}
<Thead> />
<Tr> ))}
<Th></Th> </div>
<Th>Camera</Th> </Box>
<Th>Label</Th> ) : null}
<Th>Score</Th>
<Th>Zones</Th> <Box className="min-w-0 overflow-auto">
<Th>Date</Th> <Table>
<Th>Start</Th> <Thead>
<Th>End</Th> <Tr>
</Tr> <Th></Th>
</Thead> <Th>Camera</Th>
<Tbody> <Th>Label</Th>
{events.map( <Th>Score</Th>
( <Th>Zones</Th>
{ camera, id, label, start_time: startTime, end_time: endTime, thumbnail, top_score: score, zones }, <Th>Date</Th>
i <Th>Start</Th>
) => { <Th>End</Th>
const start = new Date(parseInt(startTime * 1000, 10)); </Tr>
const end = new Date(parseInt(endTime * 1000, 10)); </Thead>
return ( <Tbody>
<Tr key={id} index={i}> {events.map(
<Td> (
<a href={`/events/${id}`}> { camera, id, label, start_time: startTime, end_time: endTime, thumbnail, top_score: score, zones },
<img className="w-32" src={`data:image/jpeg;base64,${thumbnail}`} /> i
</a> ) => {
</Td> const start = new Date(parseInt(startTime * 1000, 10));
<Td> const end = new Date(parseInt(endTime * 1000, 10));
<Filterable searchParams={searchParamsString} paramName="camera" name={camera} /> return (
</Td> <Tr key={id} index={i}>
<Td> <Td>
<Filterable searchParams={searchParamsString} paramName="label" name={label} /> <a href={`/events/${id}`}>
</Td> <img className="w-32 max-w-none" src={`data:image/jpeg;base64,${thumbnail}`} />
<Td>{(score * 100).toFixed(2)}%</Td> </a>
<Td> </Td>
<ul> <Td>
{zones.map((zone) => ( <Filterable searchParams={searchParamsString} paramName="camera" name={camera} />
<li> </Td>
<Filterable searchParams={searchParamsString} paramName="zone" name={zone} /> <Td>
</li> <Filterable searchParams={searchParamsString} paramName="label" name={label} />
))} </Td>
</ul> <Td>{(score * 100).toFixed(2)}%</Td>
</Td> <Td>
<Td>{start.toLocaleDateString()}</Td> <ul>
<Td>{start.toLocaleTimeString()}</Td> {zones.map((zone) => (
<Td>{end.toLocaleTimeString()}</Td> <li>
</Tr> <Filterable searchParams={searchParamsString} paramName="zone" name={zone} />
); </li>
} ))}
)} </ul>
</Tbody> </Td>
</Table> <Td>{start.toLocaleDateString()}</Td>
<Td>{start.toLocaleTimeString()}</Td>
<Td>{end.toLocaleTimeString()}</Td>
</Tr>
);
}
)}
</Tbody>
</Table>
</Box>
</div> </div>
); );
} }

View File

@ -0,0 +1,16 @@
import { h } from 'preact';
export default function Box({ children, className = '', hover = false, href, ...props }) {
const Element = href ? 'a' : 'div';
return (
<Element
className={`bg-white dark:bg-gray-700 shadow-lg rounded-lg p-4 ${
hover ? 'hover:bg-gray-300 hover:dark:bg-gray-500 dark:hover:text-gray-900 dark:hover:text-gray-900' : ''
} ${className}`}
href={href}
{...props}
>
{children}
</Element>
);
}

View File

@ -1,9 +1,5 @@
import { h } from 'preact'; import { h } from 'preact';
export default function Heading({ children, className = '', size = '2xl' }) { export default function Heading({ children, className = '', size = '2xl' }) {
return ( return <h1 className={`font-semibold tracking-widest uppercase text-${size} ${className}`}>{children}</h1>;
<h1 className={`font-semibold tracking-widest text-gray-900 uppercase dark:text-white text-${size} ${className}`}>
{children}
</h1>
);
} }

View File

@ -2,7 +2,7 @@ import { h } from 'preact';
export default function Link({ className, children, href, ...props }) { export default function Link({ className, children, href, ...props }) {
return ( return (
<a className={`text-blue-500 hover:underline ${className}`} href={href} {...props}> <a className={`text-blue-500 dark:text-blue-400 hover:underline ${className}`} href={href} {...props}>
{children} {children}
</a> </a>
); );

View File

@ -14,7 +14,11 @@ export default function Switch({ checked, label, id, onChange }) {
<label for={id} className="flex items-center cursor-pointer"> <label for={id} className="flex items-center cursor-pointer">
<div className="relative"> <div className="relative">
<input id={id} type="checkbox" className="hidden" onChange={handleChange} checked={checked} /> <input id={id} type="checkbox" className="hidden" onChange={handleChange} checked={checked} />
<div className="toggle__line w-12 h-6 bg-gray-400 rounded-full shadow-inner" /> <div
className={`transition-colors toggle__line w-12 h-6 ${
!checked ? 'bg-gray-400' : 'bg-blue-400'
} rounded-full shadow-inner`}
/>
<div <div
className="transition-transform absolute w-6 h-6 bg-white rounded-full shadow-md inset-y-0 left-0" className="transition-transform absolute w-6 h-6 bg-white rounded-full shadow-md inset-y-0 left-0"
style={checked ? 'transform: translateX(100%);' : 'transform: translateX(0%);'} style={checked ? 'transform: translateX(100%);' : 'transform: translateX(0%);'}

View File

@ -1,31 +1,31 @@
import { h } from 'preact'; import { h } from 'preact';
export function Table({ children, className }) { export function Table({ children, className = '' }) {
return ( return (
<table className={`table-auto border-collapse text-gray-900 dark:text-gray-200 ${className}`}>{children}</table> <table className={`table-auto border-collapse text-gray-900 dark:text-gray-200 ${className}`}>{children}</table>
); );
} }
export function Thead({ children, className }) { export function Thead({ children, className = '' }) {
return <thead className={`${className}`}>{children}</thead>; return <thead className={`${className}`}>{children}</thead>;
} }
export function Tbody({ children, className }) { export function Tbody({ children, className = '' }) {
return <tbody className={`${className}`}>{children}</tbody>; return <tbody className={`${className}`}>{children}</tbody>;
} }
export function Tfoot({ children, className }) { export function Tfoot({ children, className = '' }) {
return <tfoot className={`${className}`}>{children}</tfoot>; return <tfoot className={`${className}`}>{children}</tfoot>;
} }
export function Tr({ children, className, index }) { export function Tr({ children, className = '', index }) {
return <tr className={`${index % 2 ? 'bg-gray-200 ' : ''} ${className}`}>{children}</tr>; return <tr className={`${index % 2 ? 'bg-gray-200 dark:bg-gray-700' : ''} ${className}`}>{children}</tr>;
} }
export function Th({ children, className }) { export function Th({ children, className = '' }) {
return <th className={`border-b-2 border-gray-400 p-4 text-left ${className}`}>{children}</th>; return <th className={`border-b-2 border-gray-400 p-4 text-left ${className}`}>{children}</th>;
} }
export function Td({ children, className }) { export function Td({ children, className = '' }) {
return <td className={`p-4 ${className}`}>{children}</td>; return <td className={`p-4 ${className}`}>{children}</td>;
} }