Selaa lähdekoodia

fix(web): dark mode text color fixes

fixes #544
Paul Armstrong 4 vuotta sitten
vanhempi
commit
2132352639

+ 1 - 1
web/src/App.jsx

@@ -24,7 +24,7 @@ export default function App() {
     <div />
   ) : (
     <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 />
         <div className="p-4 min-w-0">
           <Router>

+ 22 - 27
web/src/Camera.jsx

@@ -1,4 +1,5 @@
 import { h } from 'preact';
+import Box from './components/Box';
 import Heading from './components/Heading';
 import Link from './components/Link';
 import Switch from './components/Switch';
@@ -32,45 +33,39 @@ export default function Camera({ camera, url }) {
   }
 
   return (
-    <div>
+    <div className="space-y-4">
       <Heading size="2xl">{camera}</Heading>
-      <img
-        width={cameraConfig.width}
-        height={cameraConfig.height}
-        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>
+        <img
+          width={cameraConfig.width}
+          height={cameraConfig.height}
+          key={searchParamsString}
+          src={`${apiHost}/api/${camera}?${searchParamsString}`}
+        />
+      </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('timestamp')} id="timestamp" label="Timestamp" 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('motion')} id="motion" label="Motion boxes" onChange={handleSetOption} />
         <Switch checked={getBoolean('regions')} id="regions" label="Regions" onChange={handleSetOption} />
-      </div>
-      <div>
+        <Link href={`/cameras/${camera}/editor`}>Mask & Zone creator</Link>
+      </Box>
+
+      <div className="space-y-4">
         <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) => {
             return (
-              <li key={objectType}>
-                <Link href={`/events?camera=${camera}&label=${objectType}`}>
-                  <span className="capitalize">{objectType}</span>
-                  <img src={`${apiHost}/api/${camera}/${objectType}/best.jpg?crop=1&h=150`} />
-                </Link>
-              </li>
+              <Box key={objectType} hover href={`/events?camera=${camera}&label=${objectType}`}>
+                <Heading size="sm">{objectType}</Heading>
+                <img src={`${apiHost}/api/${camera}/${objectType}/best.jpg?crop=1&h=150`} />
+              </Box>
             );
           })}
-        </ul>
-      </div>
-
-      <div>
-        <Heading size="sm">Options</Heading>
-        <ul>
-          <li>
-            <Link href={`/cameras/${camera}/editor`}>Mask & Zone creator</Link>
-          </li>
-        </ul>
+        </div>
       </div>
     </div>
   );

+ 77 - 53
web/src/CameraMap.jsx

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

+ 8 - 6
web/src/Cameras.jsx

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

+ 60 - 15
web/src/Event.jsx

@@ -1,6 +1,9 @@
-import { h } from 'preact';
+import { h, Fragment } from 'preact';
 import { ApiHost } from './context';
+import Box from './components/Box';
 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';
 
 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 (
-    <div>
+    <div className="space-y-4">
       <Heading>
-        {data.camera} {data.label} <span className="text-sm">{datetime.toLocaleString()}</span>
+        {data.camera} {data.label} <span className="text-sm">{startime.toLocaleString()}</span>
       </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>
   );
 }

+ 75 - 63
web/src/Events.jsx

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

+ 16 - 0
web/src/components/Box.jsx

@@ -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>
+  );
+}

+ 1 - 5
web/src/components/Heading.jsx

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

+ 1 - 1
web/src/components/Link.jsx

@@ -2,7 +2,7 @@ import { h } from 'preact';
 
 export default function Link({ className, children, href, ...props }) {
   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}
     </a>
   );

+ 5 - 1
web/src/components/Switch.jsx

@@ -14,7 +14,11 @@ export default function Switch({ checked, label, id, onChange }) {
     <label for={id} className="flex items-center cursor-pointer">
       <div className="relative">
         <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
           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%);'}

+ 8 - 8
web/src/components/Table.jsx

@@ -1,31 +1,31 @@
 import { h } from 'preact';
 
-export function Table({ children, className }) {
+export function Table({ children, className = '' }) {
   return (
     <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>;
 }
 
-export function Tbody({ children, className }) {
+export function Tbody({ children, className = '' }) {
   return <tbody className={`${className}`}>{children}</tbody>;
 }
 
-export function Tfoot({ children, className }) {
+export function Tfoot({ children, className = '' }) {
   return <tfoot className={`${className}`}>{children}</tfoot>;
 }
 
-export function Tr({ children, className, index }) {
-  return <tr className={`${index % 2 ? 'bg-gray-200 ' : ''} ${className}`}>{children}</tr>;
+export function Tr({ children, className = '', index }) {
+  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>;
 }
 
-export function Td({ children, className }) {
+export function Td({ children, className = '' }) {
   return <td className={`p-4 ${className}`}>{children}</td>;
 }