diff --git a/frontend/src/js/entity-history/EntityCard.tsx b/frontend/src/js/entity-history/EntityCard.tsx index 447508a776..b1d9ed7204 100644 --- a/frontend/src/js/entity-history/EntityCard.tsx +++ b/frontend/src/js/entity-history/EntityCard.tsx @@ -1,19 +1,13 @@ import { tv } from "tailwind-variants"; import type { EntityInfo, TimeStratifiedInfo } from "../api/types"; +import { Card } from "../ui-components/Card"; import EntityInfos from "./EntityInfos"; import { TabbableTimeStratifiedInfos } from "./TabbableTimeStratifiedInfos"; const container = tv({ - base: [ - "grid grid-cols-2 items-center", - "gap-[10px]", - "px-6 py-5", - "bg-bg-50", - "rounded", - "border border-gray-100", - ], + base: ["grid grid-cols-2 items-center", "gap-[10px]"], }); export const EntityCard = ({ @@ -28,13 +22,17 @@ export const EntityCard = ({ timeStratifiedInfos: TimeStratifiedInfo[]; }) => { return ( -
-
- -
- {timeStratifiedInfos.length > 0 && ( - - )} +
+ +
+
+ +
+ {timeStratifiedInfos.length > 0 && ( + + )} +
+
); }; diff --git a/frontend/src/js/entity-history/Navigation.tsx b/frontend/src/js/entity-history/Navigation.tsx index f06d2d6992..ac1776d078 100644 --- a/frontend/src/js/entity-history/Navigation.tsx +++ b/frontend/src/js/entity-history/Navigation.tsx @@ -170,7 +170,7 @@ export const Navigation = memo( )}
{!empty && ( -
+
+ + + {t("history.settings.headline")} + +
+
+ {idsCount} + + {t("common.entitiesFound", { count: idsCount })} + + {markedCount} + + {t("history.marked", { count: markedCount })} + +
+
- - - - - - {t("history.settings.headline")} - -
-
- {idsCount} - {t("common.entitiesFound", { count: idsCount })} - {markedCount} - {t("history.marked", { count: markedCount })} -
- +
); }, diff --git a/frontend/src/js/entity-history/timeline/EventCard.tsx b/frontend/src/js/entity-history/timeline/EventCard.tsx index dcd3a866d7..6f0d963fb6 100644 --- a/frontend/src/js/entity-history/timeline/EventCard.tsx +++ b/frontend/src/js/entity-history/timeline/EventCard.tsx @@ -8,6 +8,7 @@ import type { CurrencyConfigT, } from "../../api/types"; import { exists } from "../../common/helpers/exists"; +import { Card } from "../../ui-components/Card"; import { Highlighter } from "../../ui-components/Highlighter"; import { Tooltip, @@ -29,16 +30,7 @@ const card = tv({ base: ["relative", "grid grid-cols-[auto_45px_1fr]", "gap-[3px]", "py-[5px]"], }); -const eventItemContent = tv({ - base: [ - "mt-[5px]", - "rounded", - "shadow-[0_0_1px_1px_var(--color-gray-100)]", - "bg-white", - "overflow-hidden", - "[&>div:first-of-type]:pt-[14px]", - ], -}); +const eventItemContent = tv({ base: ["flex flex-col", "gap-5"] }); const colBucket = tv({ base: [ @@ -49,9 +41,7 @@ const colBucket = tv({ ], }); -const flex = tv({ - base: ["flex items-start", "gap-[5px]", "pt-3 pr-[15px] pb-[10px] pl-[6px]"], -}); +const flex = tv({ base: ["flex items-start", "gap-[5px]"] }); const rawDataBadge = tv({ base: ["absolute top-[4px] left-[55px]", "z-1"], @@ -124,112 +114,116 @@ const EventCard = ({ event={row} sourceColumn={sourceColumn} /> -
- {contentFilter.money && applicableMoney.length > 0 && ( -
- - - - - {moneyTooltip} - -
- {applicableMoney.map((column) => ( -
- {column.defaultLabel} - - - thousandSeparator={currencyConfig.thousandSeparator} - decimalSeparator={currencyConfig.decimalSeparator} - decimalScale={currencyConfig.decimalScale} - suffix={` ${currencyConfig.unit}`} - displayType="text" - value={parseFloat(row[column.label] as string)} - /> - +
+ +
+ {contentFilter.money && applicableMoney.length > 0 && ( +
+ + + + + {moneyTooltip} + +
+ {applicableMoney.map((column) => ( +
+ {column.defaultLabel} + + + thousandSeparator={currencyConfig.thousandSeparator} + decimalSeparator={currencyConfig.decimalSeparator} + decimalScale={currencyConfig.decimalScale} + suffix={` ${currencyConfig.unit}`} + displayType="text" + value={parseFloat(row[column.label] as string)} + /> + +
+ ))}
- ))} -
-
- )} - {groupedRowsKeysWithDifferentValues && groupedRows && ( - - )} - {contentFilter.rest && applicableRest.length > 0 && ( -
- - - - - {restTooltip} - -
- {applicableRest.map((column) => ( -
- {column.defaultLabel} - - {searchTerm && searchTerm.length > 0 ? ( - - ) : ( - (row[column.label] as string) - )} - +
+ )} + {groupedRowsKeysWithDifferentValues && groupedRows && ( + + )} + {contentFilter.rest && applicableRest.length > 0 && ( +
+ + + + + {restTooltip} + +
+ {applicableRest.map((column) => ( +
+ {column.defaultLabel} + + {searchTerm && searchTerm.length > 0 ? ( + + ) : ( + (row[column.label] as string) + )} + +
+ ))}
- ))} -
-
- )} - {contentFilter.groupId && applicableGroupableIds.length > 0 && ( -
- - - - - {groupableIdsTooltip} - -
- {applicableGroupableIds.map((column) => ( -
- {column.defaultLabel} - - {searchTerm && searchTerm.length > 0 ? ( - - ) : ( - (row[column.label] as string) - )} - +
+ )} + {contentFilter.groupId && applicableGroupableIds.length > 0 && ( +
+ + + + + {groupableIdsTooltip} + +
+ {applicableGroupableIds.map((column) => ( +
+ {column.defaultLabel} + + {searchTerm && searchTerm.length > 0 ? ( + + ) : ( + (row[column.label] as string) + )} + +
+ ))}
- ))} -
+
+ )}
- )} +
); diff --git a/frontend/src/js/entity-history/timeline/GroupedContent.tsx b/frontend/src/js/entity-history/timeline/GroupedContent.tsx index 6b617ddee5..304920a77d 100644 --- a/frontend/src/js/entity-history/timeline/GroupedContent.tsx +++ b/frontend/src/js/entity-history/timeline/GroupedContent.tsx @@ -29,12 +29,9 @@ const grid = tv({ base: ["inline-grid", "gap-x-[10px] gap-y-[5px]", "whitespace-nowrap"], }); +// indented past the bucket icons above and below const extraArea = tv({ - base: [ - "pt-2 pr-[15px] pb-3 pl-[49px]", - "overflow-x-auto", - "[-webkit-overflow-scrolling:touch]", - ], + base: ["pl-[43px]", "overflow-x-auto", "[-webkit-overflow-scrolling:touch]"], }); const getColumnDescriptionContentType = ( diff --git a/frontend/src/js/info-pane/InfoPane.tsx b/frontend/src/js/info-pane/InfoPane.tsx index 340bfc875f..6760229ad1 100644 --- a/frontend/src/js/info-pane/InfoPane.tsx +++ b/frontend/src/js/info-pane/InfoPane.tsx @@ -70,10 +70,6 @@ const pieceOfInfo = tv({ ], }); -const matchingStats = tv({ - base: ["grid grid-cols-[auto_1fr]", "gap-3", "items-center"], -}); - const HighlightedText = ({ text, words = [], @@ -163,7 +159,6 @@ const InfoPane = () => {
{ +interface Props { matchingEntries?: number | null; matchingEntities?: number | null; dateRange?: DateRangeT; @@ -64,8 +71,7 @@ interface Props extends HTMLAttributes { const MatchingStats = (props: Props) => { const { t } = useTranslation(); - const { matchingEntries, matchingEntities, dateRange, idLabel, ...rest } = - props; + const { matchingEntries, matchingEntities, dateRange, idLabel } = props; const isZero = props.matchingEntries === 0; const isZeroEntities = props.matchingEntities === 0; @@ -88,18 +94,7 @@ const MatchingStats = (props: Props) => { : "- - - - - - -"; return ( -
- {idLabel && ( - <> - -
-

{idLabel}

-

- {t("queryEditor.secondaryId")} -

-
- - )} +

@@ -151,6 +146,19 @@ const MatchingStats = (props: Props) => { {`${t("infoPane.date.to")}`}

+ {idLabel && ( + <> + +
+

+ {idLabel} +

+

+ {t("queryEditor.secondaryId")} +

+
+ + )}
); }; diff --git a/frontend/src/js/preview/Charts.tsx b/frontend/src/js/preview/Charts.tsx index e655f31fcd..4fcf58f544 100644 --- a/frontend/src/js/preview/Charts.tsx +++ b/frontend/src/js/preview/Charts.tsx @@ -1,100 +1,43 @@ -import { t } from "i18next"; -import { ArrowLeftIcon, ArrowRightIcon } from "lucide-react"; -import { useHotkeys } from "react-hotkeys-hook"; import { tv } from "tailwind-variants"; import type { PreviewStatistics } from "../api/types"; -import { Button } from "../ui-components/Button"; import Diagram from "./Diagram"; -const diagram = tv({ - base: ["h-[27vh]", "mr-[15px]", "p-[5px]"], +const root = tv({ + base: ["grid grid-cols-2", "gap-5", "overflow-x-hidden"], }); -// the old styles also declared `font-size: 24` on the buttons — a unitless -// value, invalid css, never applied -const directionSelector = tv({ - base: [ - "col-start-1 col-end-3 row-start-3", - "flex flex-row items-center justify-center", - "mb-[5px]", - "px-[100px]", - ], -}); - -const diagramContainer = tv({ - base: ["grid grid-cols-2", "gap-[5px]", "overflow-x-hidden"], -}); +const diagram = tv({ base: "h-[27vh]" }); -type ChartProps = { - statistics: PreviewStatistics[]; - className?: string; - showPopup: (statistic: PreviewStatistics) => void; - page: number; - setPage: (page: number) => void; -}; +export const DIAGRAMS_PER_PAGE = 4; -const DIAGRAMS_PER_PAGE = 4; +// the axis keeps its height for the chart when category labels run long +const MAX_LABEL_LENGTH = 24; export default function Charts({ statistics, - className, showPopup, page, - setPage, -}: ChartProps) { +}: { + statistics: PreviewStatistics[]; + showPopup: (statistic: PreviewStatistics) => void; + page: number; +}) { const diagramsOnPage = statistics.slice( page * DIAGRAMS_PER_PAGE, (page + 1) * DIAGRAMS_PER_PAGE, ); - const maxPage = Math.ceil(statistics.length / DIAGRAMS_PER_PAGE); - - const updatePage = (change: number) => { - const newValue = page + change; - if (newValue >= 0 && newValue < maxPage) { - setPage(newValue); - } - }; - - useHotkeys("left", () => updatePage(-1), [page]); - useHotkeys("right", () => updatePage(1), [page]); return ( -
-
- {diagramsOnPage.map((statistic) => { - return ( -
- showPopup(statistic)} - /> -
- ); - })} -
-
- - - {t("preview.page")} {page + 1}/ - {Math.ceil(statistics.length / DIAGRAMS_PER_PAGE)} - - -
+
+ {diagramsOnPage.map((statistic) => ( + showPopup(statistic)} + /> + ))}
); } diff --git a/frontend/src/js/preview/Diagram.tsx b/frontend/src/js/preview/Diagram.tsx index 3c6a50b097..21c486ee4f 100644 --- a/frontend/src/js/preview/Diagram.tsx +++ b/frontend/src/js/preview/Diagram.tsx @@ -3,6 +3,7 @@ import { addMonths, format } from "date-fns"; import { useMemo } from "react"; import { Bar, Line } from "react-chartjs-2"; import { useTranslation } from "react-i18next"; +import { tv } from "tailwind-variants"; import type { BarStatistics, DateStatistics, @@ -18,13 +19,23 @@ import { useDateTickHandler, } from "./util"; +const root = tv({ + variants: { clickable: { true: "cursor-pointer" } }, +}); + type DiagramProps = { stat: PreviewStatistics; className?: string; onClick?: () => void; height?: string | number; width?: string | number; + showTitle?: boolean; + /** cuts category labels on the axis, the tooltip keeps the full label */ + maxLabelLength?: number; }; + +const cut = (label: string, maxLength: number) => + label.length > maxLength ? `${label.slice(0, maxLength - 1)}…` : label; function transformBarStatsToData( stats: BarStatistics, color: string, @@ -95,6 +106,8 @@ export default function Diagram({ onClick, height, width, + showTitle = true, + maxLabelLength, }: DiagramProps) { const data = useMemo(() => { const color = `rgba(${hexToRgbA(getCssVarColor("--color-primary-500"))}, 1)`; @@ -121,7 +134,7 @@ export default function Diagram({ }, plugins: { title: { - display: true, + display: showTitle, font: { weight: "normal", size: 14, @@ -167,6 +180,14 @@ export default function Diagram({ title: yScaleTitle, beginAtZero: true, }, + x: { + ticks: { + callback: (valueIndex: number) => { + const label = String(data?.labels?.[valueIndex] ?? ""); + return maxLabelLength ? cut(label, maxLabelLength) : label; + }, + }, + }, }, } as ChartOptions<"bar">; } @@ -210,10 +231,10 @@ export default function Diagram({ } throw new Error("Unknown stats type"); - }, [data?.labels, stat, t, shouldTickRender]); + }, [data?.labels, stat, t, shouldTickRender, showTitle, maxLabelLength]); return ( -
+
{previewStatsIsBarStats(stat) ? ( } diff --git a/frontend/src/js/preview/DiagramModal.tsx b/frontend/src/js/preview/DiagramModal.tsx index 9a286c7557..3d70bd8ee1 100644 --- a/frontend/src/js/preview/DiagramModal.tsx +++ b/frontend/src/js/preview/DiagramModal.tsx @@ -1,8 +1,8 @@ -import { t } from "i18next"; import RcTable from "rc-table"; +import { useTranslation } from "react-i18next"; import { tv } from "tailwind-variants"; import type { PreviewStatistics } from "../api/types"; -import { Modal, ModalBody } from "../ui-components/Modal"; +import { Modal, ModalBody, ModalHeader } from "../ui-components/Modal"; import Diagram from "./Diagram"; import { StyledTable } from "./Table"; import { previewStatsIsBarStats } from "./util"; @@ -12,34 +12,36 @@ interface DiagramModalProps { onClose: () => void; } -const diagram = tv({ - base: ["h-[70vh] w-[70vw]", "mr-[15px]"], -}); +const body = tv({ base: ["flex items-start", "gap-5"] }); + +const diagram = tv({ base: ["h-[70vh]", "min-w-0 grow"] }); + +const components = { table: StyledTable }; export default function DiagramModal({ statistic, onClose, }: DiagramModalProps) { - const components = { - table: StyledTable, - }; + const { t } = useTranslation(); return ( { if (!isOpen) onClose(); }} > + + {statistic.label} + -
- +
+ {previewStatsIsBarStats(statistic) && Object.keys(statistic.extras).length > 0 && ( - -
- ); -} diff --git a/frontend/src/js/preview/Pagination.tsx b/frontend/src/js/preview/Pagination.tsx new file mode 100644 index 0000000000..78cda39818 --- /dev/null +++ b/frontend/src/js/preview/Pagination.tsx @@ -0,0 +1,54 @@ +import { ArrowLeftIcon, ArrowRightIcon } from "lucide-react"; +import { useHotkeys } from "react-hotkeys-hook"; +import { useTranslation } from "react-i18next"; +import { tv } from "tailwind-variants"; +import { Button } from "../ui-components/Button"; +import { C2 } from "../ui-components/Typography"; + +const root = tv({ base: ["flex items-center justify-end", "gap-2"] }); + +export default function Pagination({ + page, + pageCount, + setPage, +}: { + page: number; + pageCount: number; + setPage: (page: number) => void; +}) { + const { t } = useTranslation(); + + const updatePage = (change: number) => { + const newValue = page + change; + if (newValue >= 0 && newValue < pageCount) { + setPage(newValue); + } + }; + + useHotkeys("left", () => updatePage(-1), [page]); + useHotkeys("right", () => updatePage(1), [page]); + + return ( +
+ + + {t("preview.page")} {page + 1}/{pageCount} + + +
+ ); +} diff --git a/frontend/src/js/preview/Preview.tsx b/frontend/src/js/preview/Preview.tsx index 9e7c5bc67c..e39618ef60 100644 --- a/frontend/src/js/preview/Preview.tsx +++ b/frontend/src/js/preview/Preview.tsx @@ -1,65 +1,72 @@ -import { LoaderCircleIcon } from "lucide-react"; -import { useMemo, useState } from "react"; +import { + ChevronDownIcon, + ChevronLeftIcon, + LoaderCircleIcon, +} from "lucide-react"; +import { type ReactNode, useMemo, useState } from "react"; +import { MenuTrigger } from "react-aria-components"; import { useHotkeys } from "react-hotkeys-hook"; import { useTranslation } from "react-i18next"; import { useDispatch, useSelector } from "react-redux"; import { tv } from "tailwind-variants"; import type { PreviewStatistics, SecondaryId } from "../api/types"; import type { StateT } from "../app/reducers"; +import MatchingStats from "../info-pane/MatchingStats"; import { Button } from "../ui-components/Button"; +import { Card } from "../ui-components/Card"; +import { Menu, MenuItem } from "../ui-components/Menu"; +import { C3, H3 } from "../ui-components/Typography"; import { closePreview } from "./actions"; -import Charts from "./Charts"; +import Charts, { DIAGRAMS_PER_PAGE } from "./Charts"; import DiagramModal from "./DiagramModal"; -import HeadlineStats from "./HeadlineStats"; +import Pagination from "./Pagination"; import type { PreviewStateT } from "./reducer"; import ScrollBox from "./ScrollBox"; -import SelectBox from "./SelectBox"; import Table from "./Table"; const fullScreen = tv({ - base: [ - "fixed top-0 left-0", - "z-2", - "flex flex-col", - "gap-[15px]", - "h-full w-full", - "bg-bg-100", - ], + base: ["fixed top-0 left-0", "z-2", "h-full w-full", "bg-bg-100"], }); -const headline = tv({ - base: ["flex flex-row items-center", "gap-[30px]"], +const content = tv({ + base: ["flex flex-col", "gap-5", "px-5 pt-[55px] pb-5"], }); -const scrollBox = tv({ - base: ["flex flex-col", "gap-5", "pt-[60px] px-5 pb-5"], -}); +const topRow = tv({ base: ["flex items-center", "gap-[30px]"] }); -const charts = tv({ - base: [ - "w-full", - "bg-white", - "p-[10px]", - "shadow-[0_0_5px_0_rgba(0,0,0,0.2)]", - ], -}); +const section = tv({ base: ["flex flex-col", "gap-[10px]"] }); -const chartLoadingBlocker = tv({ - base: [ - "flex items-center justify-center", - "h-[65vh] w-full", - "bg-white", - "p-[10px]", - "shadow-[0_0_5px_0_rgba(0,0,0,0.2)]", - ], +const sectionHeader = tv({ + base: ["flex items-start justify-between", "gap-5"], }); -const spinnerIcon = tv({ base: "size-[30px]" }); +// the stats stand beside the charts, like the info pane beside the panes +const statistics = tv({ + base: ["grid grid-cols-[auto_minmax(0,1fr)]", "gap-5", "items-start"], +}); -const selectBox = tv({ - base: ["rounded", "bg-white", "shadow-[0_0_5px_0_rgba(0,0,0,0.2)]"], +const loading = tv({ + base: ["flex items-center justify-center", "h-[65vh]"], }); +const SectionHeader = ({ + title, + subtitle, + children, +}: { + title: string; + subtitle: string; + children?: ReactNode; +}) => ( +
+
+

{title}

+ {subtitle} +
+ {children} +
+); + export default function Preview() { const preview = useSelector((state) => state.preview); const loadedSecondaryIds = useSelector( @@ -67,11 +74,10 @@ export default function Preview() { ); const dispatch = useDispatch(); const { t } = useTranslation(); - const [selectBoxOpen, setSelectBoxOpen] = useState(false); const [page, setPage] = useState(0); const [popOver, setPopOver] = useState(null); const onClose = () => dispatch(closePreview()); - const statistics = preview.statisticsData; + const stats = preview.statisticsData; const idLabel = useMemo(() => { const primaryIdLabel = t("common.entitiesFound", { count: 2 }); if (preview.queryData?.secondaryId) { @@ -85,58 +91,114 @@ export default function Preview() { }, [preview.queryData, loadedSecondaryIds, t]); useHotkeys("esc", () => { - if (!selectBoxOpen && !popOver) onClose(); + if (!popOver) onClose(); }); return (
- -
- - Ergebnisvorschau - { - const stat = statistics?.statistics.find( - (stat) => stat.label === res.label, - ); - setPopOver(stat ?? null); - }} - isOpen={selectBoxOpen} - setIsOpen={setSelectBoxOpen} - /> - -
- {statistics ? ( - { - setPopOver(statistic); - }} - page={page} - setPage={setPage} - /> - ) : ( -
- + +
+
+ +
+

{preview.queryData?.label}

+ {t("preview.headline")} +
- )} - {popOver && ( - setPopOver(null)} /> - )} - {preview.arrowReader && - preview.initialTableData && - preview.queryData && ( - + + {stats && ( + + + { + const stat = stats.statistics.find( + (stat) => stat.label === key, + ); + setPopOver(stat ?? null); + }} + > + {stats.statistics.map((stat) => ( + + {stat.label} + + ))} + + + )} + +
+
+ +
+ + {stats ? ( + { + setPopOver(statistic); + }} + page={page} + /> + ) : ( +
+ +
+ )} +
+
+ {stats && ( + + )} + + {popOver && ( + setPopOver(null)} /> )} + {preview.arrowReader && + preview.initialTableData?.value && + preview.queryData && ( +
+ + +
+ + + )} + ); diff --git a/frontend/src/js/preview/ScrollBox.tsx b/frontend/src/js/preview/ScrollBox.tsx index 95419db05d..6f7560c2d2 100644 --- a/frontend/src/js/preview/ScrollBox.tsx +++ b/frontend/src/js/preview/ScrollBox.tsx @@ -6,40 +6,30 @@ import { useRef, useState, } from "react"; -import { Button as RacButton } from "react-aria-components"; +import { useTranslation } from "react-i18next"; import { tv } from "tailwind-variants"; +import { Button } from "../ui-components/Button"; const root = tv({ base: "overflow-auto" }); +// a white backing, because the secondary button is transparent over the content const scrollTopButton = tv({ - base: [ - "absolute right-[30px] bottom-[30px]", - "z-3", - "flex justify-center", - "h-[50px] w-[50px]", - "items-center", - "rounded-full", - "border border-gray-500", - "bg-white hover:bg-gray-50", - "text-gray-800", - "cursor-pointer", - "shadow-[0_0_5px_0_rgba(0,0,0,0.2)]", - ], + base: ["absolute right-5 bottom-5", "z-3", "rounded", "bg-white"], }); export default function ScrollBox({ - threshold = 0, children, className, ...props -}: PropsWithChildren<{ threshold?: number }> & HTMLAttributes) { +}: PropsWithChildren>) { + const { t } = useTranslation(); const scrollBoxRef = useRef(null); const [showButton, setShowButton] = useState(false); useEffect(() => { const scrollHandler = (e: Event) => { const target = e.target as HTMLDivElement; - setShowButton(target.scrollTop > threshold); + setShowButton(target.scrollTop > 0); }; const scrollBox = scrollBoxRef.current; @@ -47,19 +37,22 @@ export default function ScrollBox({ passive: true, }); return () => scrollBox?.removeEventListener("scroll", scrollHandler); - }, [threshold]); + }, []); return (
{showButton && ( - - scrollBoxRef.current?.scrollTo({ top: 0, behavior: "smooth" }) - } - > - - +
+ +
)} {children}
diff --git a/frontend/src/js/preview/SelectBox.tsx b/frontend/src/js/preview/SelectBox.tsx deleted file mode 100644 index d9c1beb6c8..0000000000 --- a/frontend/src/js/preview/SelectBox.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import { ChevronDownIcon, ChevronUpIcon } from "lucide-react"; -import { type SetStateAction, useMemo, useRef, useState } from "react"; -import { tv } from "tailwind-variants"; -import { useClickOutside } from "../common/helpers/useClickOutside"; -import { textStyle } from "../ui-components/Typography"; - -export interface SelectItem { - label: string; -} - -interface SelectBoxProps { - items: T[]; - onChange: (item: T) => void; - className?: string; - isOpen: boolean; - setIsOpen: (open: boolean) => void; -} - -const input = tv({ - base: [ - "mt-[5px] w-[190px] h-5", - "border-0 outline-none", - textStyle({ size: 2 }), - "disabled:cursor-not-allowed disabled:pointer-events-none", - ], -}); - -const root = tv({ - base: ["flex flex-col", "min-h-[30px]", "w-[20vw]"], -}); - -const list = tv({ - base: [ - "absolute", - "z-1", - "mt-[35px]", - "flex flex-col", - "gap-[5px]", - "max-h-[40vh] w-[20vw]", - "overflow-y-auto", - "rounded", - "bg-white", - "shadow-[0_0_5px_rgba(0,0,0,0.2)]", - "[clip-path:inset(0px_-8px_-8px_-8px)]", - ], -}); - -const listItem = tv({ - base: ["px-[5px]", "cursor-pointer", "hover:bg-gray-50"], -}); - -const arrow = tv({ - base: ["mt-[5px]", "size-[17px]", "text-gray-600", "cursor-pointer"], -}); - -export default function SelectBox({ - items, - onChange, - className, - isOpen, - setIsOpen, -}: SelectBoxProps) { - const [searchTerm, setSearchTerm] = useState(""); - const clickOutsideRef = useRef(null); - useClickOutside(clickOutsideRef, () => setIsOpen(false)); - - const displayedItems = useMemo(() => { - return items.filter((item) => { - if (searchTerm === "") { - return true; - } - if (item.label === null) { - return false; - } - return item.label.toLowerCase().includes(searchTerm.toLowerCase()); - }); - }, [items, searchTerm]); - - return ( - // biome-ignore lint/a11y/noStaticElementInteractions: TODO make this a real combobox - // biome-ignore lint/a11y/useKeyWithClickEvents: see above -
setIsOpen(!isOpen)}> -
- } }) => - setSearchTerm(e.target.value) - } - onKeyDown={(e) => { - if (e.key === "Enter") { - onChange(displayedItems[0]); - } - }} - spellCheck={false} - /> -
- {isOpen ? ( - - ) : ( - - )} -
-
-
- {isOpen && - displayedItems.map((item) => { - return ( - // biome-ignore lint/a11y/noStaticElementInteractions: TODO make this a real listbox option - // biome-ignore lint/a11y/useKeyWithClickEvents: see above -
onChange(item)} - > - {item.label} -
- ); - })} -
-
- ); -} diff --git a/frontend/src/js/preview/Table.tsx b/frontend/src/js/preview/Table.tsx index f3521fc82e..34fdcda39f 100644 --- a/frontend/src/js/preview/Table.tsx +++ b/frontend/src/js/preview/Table.tsx @@ -20,8 +20,6 @@ interface Props { // flipped so the horizontal scrollbar sits on top; the inner table flips back const root = tv({ base: [ - "grow", - "shadow-[0_0_10px_0_rgba(0,0,0,0.2)]", "[transform:rotateX(180deg)]", "[&_table]:[transform:rotateX(-180deg)]", ], @@ -31,12 +29,13 @@ const table = tv({ base: [ "w-full", "border-spacing-0", - "[&_th]:bg-gray-50 [&_th]:text-left [&_th]:font-normal", + "[&_th]:bg-bg-50 [&_th]:text-left", "[&_td]:max-w-[25ch] [&_td]:overflow-hidden [&_td]:text-ellipsis [&_td]:whitespace-nowrap", - "[&_th]:p-[10px] [&_td]:p-[10px]", - "[&_th]:border-r [&_th]:border-b [&_th]:border-gray-400", - "[&_td]:border-r [&_td]:border-b [&_td]:border-gray-400", + "[&_th]:px-2 [&_th]:py-[5px] [&_td]:px-2 [&_td]:py-[5px]", + "[&_th]:border-r [&_th]:border-b [&_th]:border-gray-100", + "[&_td]:border-r [&_td]:border-b [&_td]:border-gray-100", "[&_th:last-of-type]:border-r-0 [&_td:last-of-type]:border-r-0", + "[&_tbody_tr:last-of-type_td]:border-b-0", "[&_.rc-table-measure-cell]:py-0 [&_.rc-table-measure-cell]:border-y-0", "[&_.rc-table-measure-cell-content]:invisible [&_.rc-table-measure-cell-content]:pointer-events-none", "[&_.rc-table-measure-cell-content]:h-0 [&_.rc-table-measure-cell-content]:overflow-hidden", @@ -65,8 +64,14 @@ export default memo(function Table({ arrowReader.schema?.fields.map((field) => { const renderer = getRenderFunctionByFieldName(field.name); + const title = field.name.charAt(0).toUpperCase() + field.name.slice(1); + return { - title: field.name.charAt(0).toUpperCase() + field.name.slice(1), + title: ( + + {title} + + ), dataIndex: field.name, key: field.name, render: (value: string | Vector) => { diff --git a/frontend/src/js/ui-components/Card.stories.tsx b/frontend/src/js/ui-components/Card.stories.tsx new file mode 100644 index 0000000000..a8512e30b0 --- /dev/null +++ b/frontend/src/js/ui-components/Card.stories.tsx @@ -0,0 +1,35 @@ +import type { Meta, StoryObj } from "@storybook/react"; + +import { Card } from "./Card"; +import { C2, C3, H3 } from "./Typography"; + +export default { + title: "UiComponents/Card", + component: Card, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +} as Meta; + +type Story = StoryObj; + +export const Default: Story = { + render: () => ( +
+ +

Regions

+ 12 entries +
+ North, South, East and West, with their population per year. +
+
+ + Products + +
+ ), +}; diff --git a/frontend/src/js/ui-components/Card.tsx b/frontend/src/js/ui-components/Card.tsx new file mode 100644 index 0000000000..ba3bf3f654 --- /dev/null +++ b/frontend/src/js/ui-components/Card.tsx @@ -0,0 +1,14 @@ +import type { ReactNode } from "react"; +import { tv } from "tailwind-variants"; + +const card = tv({ + base: ["rounded", "border border-gray-100", "bg-white", "p-3"], +}); + +/** + * A white surface that groups content on a page background. Layout inside + * and around it belongs to the content and the parent. + */ +export const Card = ({ children }: { children: ReactNode }) => ( +
{children}
+); diff --git a/frontend/src/js/ui-components/Menu.tsx b/frontend/src/js/ui-components/Menu.tsx index 838b7b0e88..8ef295dd78 100644 --- a/frontend/src/js/ui-components/Menu.tsx +++ b/frontend/src/js/ui-components/Menu.tsx @@ -27,7 +27,8 @@ const menuItem = tv({ "flex items-center", "gap-[10px]", "rounded", - "h-[30px] px-3", + // the menu scrolls at its max height instead of squeezing the items + "h-[30px] shrink-0 px-3", textStyle({ size: 2, tone: "default" }), "whitespace-nowrap", "cursor-pointer", diff --git a/frontend/src/localization/de.json b/frontend/src/localization/de.json index 403532d67d..f9b884f041 100644 --- a/frontend/src/localization/de.json +++ b/frontend/src/localization/de.json @@ -409,21 +409,16 @@ "preview": "Vorschau", "previousPage": "Vorherige Seite", "nextPage": "Nächste Seite", + "scrollToTop": "Nach oben", "page": "Seite", "headline": "Ergebnisvorschau", "previewHeadline": "Ergebnis-CSV", "previewSubline": "Auszug der ersten {{count}} Zeilen", "statisticsHeadline": "Spaltenstatistik", "statisticsSubline": "Basierend auf den vollständigen Ergebnissen. Fehlende Zahlenwerte werden als 0 interpretiert.", - "total": "Zeilen", - "min": "Min Datum", - "max": "Max Datum", - "span": "Datumsbereich", "loadingError": "Da ist etwas schiefgelaufen, bitte versuche es nochmal.", - "dateError": "Datum unbekannt", "name": "Name", "value": "Wert", - "densityPlot": "Dichtediagramm", "chartYLabel": "Anzahl", "idLabel": "{{primaryIdLabel}} und {{secondaryIdLabel}}" }, diff --git a/frontend/src/localization/en.json b/frontend/src/localization/en.json index c679b6449a..9e81d2d737 100644 --- a/frontend/src/localization/en.json +++ b/frontend/src/localization/en.json @@ -409,21 +409,16 @@ "preview": "Preview", "previousPage": "Previous page", "nextPage": "Next page", + "scrollToTop": "Scroll to top", "page": "Page", "headline": "Results preview", "previewHeadline": "Result CSV", "previewSubline": "Preview of the first {{count}} lines", "statisticsHeadline": "Statistics per column", "statisticsSubline": "Based on the full results. Missing number values interpreted as 0.", - "total": "Total rows", - "min": "Min date", - "max": "Max date", - "span": "Date span", "loadingError": "An error occurred while loading the preview.", - "dateError": "Date unknown", "name": "Name", "value": "Value", - "densityPlot": "Density Plot", "chartYLabel": "Count", "idLabel": "{{primaryIdLabel}} and {{secondaryIdLabel}}" },