diff --git a/airflow/ui/src/components/DataTable/CardList.tsx b/airflow/ui/src/components/DataTable/CardList.tsx new file mode 100644 index 0000000000000..ddebff81b2495 --- /dev/null +++ b/airflow/ui/src/components/DataTable/CardList.tsx @@ -0,0 +1,70 @@ +/*! + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { Box, SimpleGrid, Skeleton } from "@chakra-ui/react"; +import { + type CoreRow, + flexRender, + type Table as TanStackTable, +} from "@tanstack/react-table"; +import type { SyntheticEvent } from "react"; + +import type { CardDef } from "./types"; + +type DataTableProps = { + readonly cardDef: CardDef; + readonly isLoading?: boolean; + readonly onRowClick?: (e: SyntheticEvent, row: CoreRow) => void; + readonly table: TanStackTable; +}; + +export const CardList = ({ + cardDef, + isLoading, + onRowClick, + table, +}: DataTableProps) => { + const defaultGridProps = { column: { base: 1 }, spacing: 2 }; + + return ( + + + {table.getRowModel().rows.map((row) => ( + onRowClick(event, row) : undefined} + title={onRowClick ? "View details" : undefined} + > + {Boolean(isLoading) && + (cardDef.meta?.customSkeleton ?? ( + + ))} + {!Boolean(isLoading) && + flexRender(cardDef.card, { row: row.original })} + + ))} + + + ); +}; diff --git a/airflow/ui/src/components/DataTable/DataTable.test.tsx b/airflow/ui/src/components/DataTable/DataTable.test.tsx index c83f15f8f3822..028ba27ce2a94 100644 --- a/airflow/ui/src/components/DataTable/DataTable.test.tsx +++ b/airflow/ui/src/components/DataTable/DataTable.test.tsx @@ -16,12 +16,14 @@ * specific language governing permissions and limitations * under the License. */ +import { Text } from "@chakra-ui/react"; import type { ColumnDef, PaginationState } from "@tanstack/react-table"; import "@testing-library/jest-dom"; import { render, screen } from "@testing-library/react"; import { describe, expect, it, vi } from "vitest"; import { DataTable } from "./DataTable.tsx"; +import type { CardDef } from "./types.ts"; const columns: Array> = [ { @@ -36,6 +38,10 @@ const data = [{ name: "John Doe" }, { name: "Jane Doe" }]; const pagination: PaginationState = { pageIndex: 0, pageSize: 1 }; const onStateChange = vi.fn(); +const cardDef: CardDef<{ name: string }> = { + card: ({ row }) => My name is {row.name}., +}; + describe("DataTable", () => { it("renders table with data", () => { render( @@ -84,4 +90,44 @@ describe("DataTable", () => { expect(screen.getByText(">>")).toBeDisabled(); expect(screen.getByText(">")).toBeDisabled(); }); + + it("when isLoading renders skeleton columns", () => { + render(); + + expect(screen.getAllByTestId("skeleton")).toHaveLength(10); + }); + + it("still displays table if mode is card but there is no cardDef", () => { + render(); + + expect(screen.getByText("Name")).toBeInTheDocument(); + }); + + it("displays cards if mode is card and there is cardDef", () => { + render( + , + ); + + expect(screen.getByText("My name is John Doe.")).toBeInTheDocument(); + }); + + it("displays skeleton for loading card list", () => { + render( + , + ); + + expect(screen.getAllByTestId("skeleton")).toHaveLength(5); + }); }); diff --git a/airflow/ui/src/components/DataTable/DataTable.tsx b/airflow/ui/src/components/DataTable/DataTable.tsx index 48b934660283f..7f4c3cf083966 100644 --- a/airflow/ui/src/components/DataTable/DataTable.tsx +++ b/airflow/ui/src/components/DataTable/DataTable.tsx @@ -16,60 +16,60 @@ * specific language governing permissions and limitations * under the License. */ +import { Progress, Text } from "@chakra-ui/react"; import { - Table as ChakraTable, - TableContainer, - Tbody, - Td, - Th, - Thead, - Tr, - useColorModeValue, -} from "@chakra-ui/react"; -import { - flexRender, getCoreRowModel, getExpandedRowModel, getPaginationRowModel, useReactTable, - type ColumnDef, type OnChangeFn, type TableState as ReactTableState, type Row, type Table as TanStackTable, type Updater, } from "@tanstack/react-table"; -import React, { Fragment, useCallback, useRef } from "react"; -import { - TiArrowSortedDown, - TiArrowSortedUp, - TiArrowUnsorted, -} from "react-icons/ti"; +import React, { type ReactNode, useCallback, useRef } from "react"; +import { CardList } from "./CardList"; +import { TableList } from "./TableList"; import { TablePaginator } from "./TablePaginator"; -import type { TableState } from "./types"; +import { createSkeletonMock } from "./skeleton"; +import type { CardDef, MetaColumn, TableState } from "./types"; type DataTableProps = { - readonly columns: Array>; + readonly cardDef?: CardDef; + readonly columns: Array>; readonly data: Array; + readonly displayMode?: "card" | "table"; readonly getRowCanExpand?: (row: Row) => boolean; readonly initialState?: TableState; + readonly isFetching?: boolean; + readonly isLoading?: boolean; + readonly modelName?: string; + readonly noRowsMessage?: ReactNode; readonly onStateChange?: (state: TableState) => void; readonly renderSubComponent?: (props: { row: Row; }) => React.ReactElement; + readonly skeletonCount?: number; readonly total?: number; }; const defaultGetRowCanExpand = () => false; export const DataTable = ({ + cardDef, columns, data, + displayMode = "table", getRowCanExpand = defaultGetRowCanExpand, initialState, + isFetching, + isLoading, + modelName, + noRowsMessage, onStateChange, - renderSubComponent, + skeletonCount = 10, total = 0, }: DataTableProps) => { const ref = useRef<{ tableRef: TanStackTable | undefined }>({ @@ -93,6 +93,10 @@ export const DataTable = ({ [onStateChange], ); + const rest = Boolean(isLoading) + ? createSkeletonMock(displayMode, skeletonCount, columns) + : {}; + const table = useReactTable({ columns, data, @@ -105,87 +109,34 @@ export const DataTable = ({ onStateChange: handleStateChange, rowCount: total, state: initialState, + ...rest, }); ref.current.tableRef = table; - const theadBg = useColorModeValue("white", "gray.800"); + const { rows } = table.getRowModel(); - return ( - - - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map( - ({ colSpan, column, getContext, id, isPlaceholder }) => { - const sort = column.getIsSorted(); - const canSort = column.getCanSort(); + const display = displayMode === "card" && Boolean(cardDef) ? "card" : "table"; - return ( - - {isPlaceholder ? undefined : ( - <>{flexRender(column.columnDef.header, getContext())} - )} - {canSort && sort === false ? ( - - ) : undefined} - {canSort && sort !== false ? ( - sort === "desc" ? ( - - ) : ( - - ) - ) : undefined} - - ); - }, - )} - - ))} - - - {table.getRowModel().rows.map((row) => ( - - - {/* first row is a normal row */} - {row.getVisibleCells().map((cell) => ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ))} - - {row.getIsExpanded() && ( - - {/* 2nd row is a custom 1 cell row */} - - {renderSubComponent?.({ row })} - - - )} - - ))} - - + return ( + <> + + {!Boolean(isLoading) && !rows.length && ( + + {noRowsMessage ?? `No ${modelName}s found.`} + + )} + {display === "table" && } + {display === "card" && cardDef !== undefined && ( + + )} - + ); }; diff --git a/airflow/ui/src/components/DataTable/TableList.tsx b/airflow/ui/src/components/DataTable/TableList.tsx new file mode 100644 index 0000000000000..97b7fd6aed080 --- /dev/null +++ b/airflow/ui/src/components/DataTable/TableList.tsx @@ -0,0 +1,125 @@ +/*! + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { + Table as ChakraTable, + TableContainer, + Tbody, + Td, + Th, + Thead, + Tr, +} from "@chakra-ui/react"; +import { + flexRender, + type Row, + type Table as TanStackTable, +} from "@tanstack/react-table"; +import React, { Fragment } from "react"; +import { + TiArrowSortedDown, + TiArrowSortedUp, + TiArrowUnsorted, +} from "react-icons/ti"; + +type DataTableProps = { + readonly renderSubComponent?: (props: { + row: Row; + }) => React.ReactElement; + readonly table: TanStackTable; +}; + +export const TableList = ({ + renderSubComponent, + table, +}: DataTableProps) => ( + + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map( + ({ colSpan, column, getContext, id, isPlaceholder }) => { + const sort = column.getIsSorted(); + const canSort = column.getCanSort(); + + return ( + + {isPlaceholder ? undefined : ( + <>{flexRender(column.columnDef.header, getContext())} + )} + {canSort && sort === false ? ( + + ) : undefined} + {canSort && sort !== false ? ( + sort === "desc" ? ( + + ) : ( + + ) + ) : undefined} + + ); + }, + )} + + ))} + + + {table.getRowModel().rows.map((row) => ( + + + {/* first row is a normal row */} + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + {row.getIsExpanded() && ( + + {/* 2nd row is a custom 1 cell row */} + + {renderSubComponent?.({ row })} + + + )} + + ))} + + + +); diff --git a/airflow/ui/src/components/DataTable/ToggleTableDisplay.tsx b/airflow/ui/src/components/DataTable/ToggleTableDisplay.tsx new file mode 100644 index 0000000000000..489fbdc1ce3b6 --- /dev/null +++ b/airflow/ui/src/components/DataTable/ToggleTableDisplay.tsx @@ -0,0 +1,54 @@ +/*! + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { HStack, IconButton } from "@chakra-ui/react"; +import { FiAlignJustify, FiGrid } from "react-icons/fi"; + +type Display = "card" | "table"; + +type Props = { + readonly display: Display; + readonly setDisplay: (display: Display) => void; +}; + +export const ToggleTableDisplay = ({ display, setDisplay }: Props) => ( + + } + isActive={display === "card"} + minWidth={8} + onClick={() => setDisplay("card")} + variant="outline" + width={8} + /> + } + isActive={display === "table"} + minWidth={8} + onClick={() => setDisplay("table")} + variant="outline" + width={8} + /> + +); diff --git a/airflow/ui/src/components/DataTable/skeleton.tsx b/airflow/ui/src/components/DataTable/skeleton.tsx new file mode 100644 index 0000000000000..e237e4ad1030b --- /dev/null +++ b/airflow/ui/src/components/DataTable/skeleton.tsx @@ -0,0 +1,51 @@ +/*! + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { Skeleton } from "@chakra-ui/react"; + +import type { MetaColumn } from "./types"; + +export const createSkeletonMock = ( + mode: "card" | "table", + skeletonCount: number, + columnDefs: Array>, +) => { + const colDefs = columnDefs.map((colDef) => ({ + ...colDef, + cell: () => { + if (mode === "table") { + return ( + colDef.meta?.customSkeleton ?? ( + + ) + ); + } + + return undefined; + }, + })); + + const data = [...Array(skeletonCount)].map(() => ({})); + + return { columns: colDefs, data }; +}; diff --git a/airflow/ui/src/components/DataTable/types.ts b/airflow/ui/src/components/DataTable/types.ts index febf9acdb0403..4741b61ff6433 100644 --- a/airflow/ui/src/components/DataTable/types.ts +++ b/airflow/ui/src/components/DataTable/types.ts @@ -16,9 +16,30 @@ * specific language governing permissions and limitations * under the License. */ -import type { PaginationState, SortingState } from "@tanstack/react-table"; +import type { SimpleGridProps } from "@chakra-ui/react"; +import type { + ColumnDef, + PaginationState, + SortingState, +} from "@tanstack/react-table"; +import type { ReactNode } from "react"; export type TableState = { pagination: PaginationState; sorting: SortingState; }; + +export type CardDef = { + card: (props: { row: TData }) => ReactNode; + gridProps?: SimpleGridProps; + meta?: { + customSkeleton?: JSX.Element; + }; +}; + +export type MetaColumn = { + meta?: { + customSkeleton?: ReactNode; + skeletonWidth?: number; + } & ColumnDef["meta"]; +} & ColumnDef; diff --git a/airflow/ui/src/pages/DagsList/DagCard.test.tsx b/airflow/ui/src/pages/DagsList/DagCard.test.tsx new file mode 100644 index 0000000000000..3ae6e4fceeac9 --- /dev/null +++ b/airflow/ui/src/pages/DagsList/DagCard.test.tsx @@ -0,0 +1,87 @@ +/* eslint-disable unicorn/no-null */ + +/*! + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { render, screen } from "@testing-library/react"; +import type { + DAGResponse, + DagTagPydantic, +} from "openapi-gen/requests/types.gen"; +import { afterEach, describe, it, vi, expect } from "vitest"; + +import { Wrapper } from "src/utils/Wrapper"; + +import { DagCard } from "./DagCard"; + +const mockDag = { + dag_display_name: "nested_groups", + dag_id: "nested_groups", + default_view: "grid", + description: null, + file_token: + "Ii9maWxlcy9kYWdzL25lc3RlZF90YXNrX2dyb3Vwcy5weSI.G3EkdxmDUDQsVb7AIZww1TSGlFE", + fileloc: "/files/dags/nested_task_groups.py", + has_import_errors: false, + has_task_concurrency_limits: false, + is_active: true, + is_paused: false, + last_expired: null, + last_parsed_time: "2024-08-22T13:50:10.372238+00:00", + last_pickled: null, + max_active_runs: 16, + max_active_tasks: 16, + max_consecutive_failed_dag_runs: 0, + next_dagrun: "2024-08-22T00:00:00+00:00", + next_dagrun_create_after: "2024-08-23T00:00:00+00:00", + next_dagrun_data_interval_end: "2024-08-23T00:00:00+00:00", + next_dagrun_data_interval_start: "2024-08-22T00:00:00+00:00", + owners: ["airflow"], + pickle_id: null, + scheduler_lock: null, + tags: [], + timetable_description: "", + timetable_summary: "", +} satisfies DAGResponse; + +afterEach(() => { + vi.restoreAllMocks(); +}); + +describe("DagCard", () => { + it("DagCard should render without tags", () => { + render(, { wrapper: Wrapper }); + expect(screen.getByText(mockDag.dag_display_name)).toBeInTheDocument(); + expect(screen.queryByTestId("dag-tag")).toBeNull(); + }); + + it("DagCard should show +X more text if there are more than 3 tags", () => { + const tags = [ + { dag_id: "id", name: "tag1" }, + { dag_id: "id", name: "tag2" }, + { dag_id: "id", name: "tag3" }, + { dag_id: "id", name: "tag4" }, + ] satisfies Array; + + const expandedMockDag = { ...mockDag, tags } satisfies DAGResponse; + + render(, { wrapper: Wrapper }); + expect(screen.getByTestId("dag-tag")).toBeInTheDocument(); + expect(screen.getByText("+1 more")).toBeInTheDocument(); + }); +}); diff --git a/airflow/ui/src/pages/DagsList/DagCard.tsx b/airflow/ui/src/pages/DagsList/DagCard.tsx new file mode 100644 index 0000000000000..d555abbc0ce1b --- /dev/null +++ b/airflow/ui/src/pages/DagsList/DagCard.tsx @@ -0,0 +1,118 @@ +/*! + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { + Badge, + Box, + Flex, + HStack, + Heading, + SimpleGrid, + Text, + Tooltip, + useColorModeValue, + VStack, +} from "@chakra-ui/react"; +import { FiCalendar, FiTag } from "react-icons/fi"; + +import type { DAGResponse } from "openapi/requests/types.gen"; +import { TogglePause } from "src/components/TogglePause"; + +type Props = { + readonly dag: DAGResponse; +}; + +const MAX_TAGS = 3; + +export const DagCard = ({ dag }: Props) => { + const cardBorder = useColorModeValue("gray.100", "gray.700"); + const tooltipBg = useColorModeValue("gray.200", "gray.700"); + + return ( + + + + + + {dag.dag_display_name} + + + {dag.tags.length ? ( + + + {dag.tags.slice(0, MAX_TAGS).map((tag) => ( + {tag.name} + ))} + {dag.tags.length > MAX_TAGS && ( + + {dag.tags.slice(MAX_TAGS).map((tag) => ( + {tag.name} + ))} + + } + > + +{dag.tags.length - MAX_TAGS} more + + )} + + ) : undefined} + + + + + + +
+ + + Next Run + + {Boolean(dag.next_dagrun) ? ( + {dag.next_dagrun} + ) : undefined} + {Boolean(dag.timetable_summary) ? ( + + + {" "} + {" "} + {dag.timetable_summary} + + + ) : undefined} + +
+
+ + + ); +}; diff --git a/airflow/ui/src/pages/DagsList/DagsList.tsx b/airflow/ui/src/pages/DagsList/DagsList.tsx index 90981a0747643..7b87e8d253fb2 100644 --- a/airflow/ui/src/pages/DagsList/DagsList.tsx +++ b/airflow/ui/src/pages/DagsList/DagsList.tsx @@ -21,21 +21,24 @@ import { Heading, HStack, Select, - Spinner, + Skeleton, VStack, } from "@chakra-ui/react"; import type { ColumnDef } from "@tanstack/react-table"; -import { type ChangeEventHandler, useCallback } from "react"; +import { type ChangeEventHandler, useCallback, useState } from "react"; import { useSearchParams } from "react-router-dom"; import { useDagServiceGetDags } from "openapi/queries"; import type { DAGResponse } from "openapi/requests/types.gen"; import { DataTable } from "src/components/DataTable"; +import { ToggleTableDisplay } from "src/components/DataTable/ToggleTableDisplay"; +import type { CardDef } from "src/components/DataTable/types"; import { useTableURLState } from "src/components/DataTable/useTableUrlState"; import { SearchBar } from "src/components/SearchBar"; import { TogglePause } from "src/components/TogglePause"; import { pluralize } from "src/utils/pluralize"; +import { DagCard } from "./DagCard"; import { DagsFilters } from "./DagsFilters"; const columns: Array> = [ @@ -49,6 +52,9 @@ const columns: Array> = [ ), enableSorting: false, header: "", + meta: { + skeletonWidth: 10, + }, }, { accessorKey: "dag_id", @@ -83,10 +89,18 @@ const columns: Array> = [ }, ]; +const cardDef: CardDef = { + card: ({ row }) => , + meta: { + customSkeleton: , + }, +}; + const PAUSED_PARAM = "paused"; -export const DagsList = ({ cardView = false }) => { +export const DagsList = () => { const [searchParams] = useSearchParams(); + const [display, setDisplay] = useState<"card" | "table">("card"); const showPaused = searchParams.get(PAUSED_PARAM); @@ -97,7 +111,7 @@ export const DagsList = ({ cardView = false }) => { const [sort] = sorting; const orderBy = sort ? `${sort.desc ? "-" : ""}${sort.id}` : undefined; - const { data, isLoading } = useDagServiceGetDags({ + const { data, isFetching, isLoading } = useDagServiceGetDags({ limit: pagination.pageSize, offset: pagination.pageIndex * pagination.pageSize, onlyActive: true, @@ -119,44 +133,47 @@ export const DagsList = ({ cardView = false }) => { return ( <> - {isLoading ? : undefined} - {!isLoading && Boolean(data?.dags) && ( - <> - - - - - - {pluralize("DAG", data?.total_entries)} - - {cardView ? ( - - ) : ( - false - )} - - - - - )} + + + + + + {pluralize("DAG", data?.total_entries)} + + {display === "card" ? ( + + ) : ( + false + )} + + + + ); }; diff --git a/airflow/ui/src/theme.ts b/airflow/ui/src/theme.ts index e172bf76508d1..06a3b10cc7fcf 100644 --- a/airflow/ui/src/theme.ts +++ b/airflow/ui/src/theme.ts @@ -24,39 +24,33 @@ import { createMultiStyleConfigHelpers, extendTheme } from "@chakra-ui/react"; const { defineMultiStyleConfig, definePartsStyle } = createMultiStyleConfigHelpers(tableAnatomy.keys); -const baseStyle = definePartsStyle((props) => { - const { colorMode, colorScheme } = props; - - return { - tbody: { - tr: { - "&:nth-of-type(even)": { - "th, td": { - borderBottomWidth: "0px", - }, +const baseStyle = definePartsStyle(() => ({ + tbody: { + tr: { + "&:nth-of-type(even)": { + "th, td": { + borderBottomWidth: "0px", + }, + }, + "&:nth-of-type(odd)": { + td: { + background: "subtle-bg", }, - "&:nth-of-type(odd)": { - td: { - background: - colorMode === "light" ? `${colorScheme}.50` : `gray.900`, - }, - "th, td": { - borderBottomWidth: "0px", - borderColor: - colorMode === "light" ? `${colorScheme}.50` : `gray.900`, - }, + "th, td": { + borderBottomWidth: "0px", + borderColor: "subtle-bg", }, }, }, - thead: { - tr: { - th: { - borderBottomWidth: 0, - }, + }, + thead: { + tr: { + th: { + borderBottomWidth: 0, }, }, - }; -}); + }, +})); export const tableTheme = defineMultiStyleConfig({ baseStyle }); @@ -72,6 +66,12 @@ const theme = extendTheme({ config: { useSystemColorMode: true, }, + semanticTokens: { + colors: { + "subtle-bg": { _dark: "gray.900", _light: "blue.50" }, + "subtle-text": { _dark: "blue.500", _light: "blue.600" }, + }, + }, styles: { global: { "*, *::before, &::after": {