From 5995e490245fc479d52dfa319839687b7354ef44 Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Thu, 8 Oct 2026 15:16:35 +0200 Subject: [PATCH 1/9] preview page on the shared components, history look --- frontend/src/js/info-pane/InfoPane.tsx | 5 - frontend/src/js/info-pane/MatchingStats.tsx | 12 +- frontend/src/js/preview/Charts.tsx | 57 +++--- frontend/src/js/preview/Diagram.tsx | 6 +- frontend/src/js/preview/DiagramModal.tsx | 26 +-- frontend/src/js/preview/HeadlineStats.tsx | 32 --- frontend/src/js/preview/Preview.tsx | 210 ++++++++++++-------- frontend/src/js/preview/ScrollBox.tsx | 45 ++--- frontend/src/js/preview/SelectBox.tsx | 125 ------------ frontend/src/js/preview/Table.tsx | 22 +- frontend/src/localization/de.json | 7 +- frontend/src/localization/en.json | 7 +- 12 files changed, 219 insertions(+), 335 deletions(-) delete mode 100644 frontend/src/js/preview/HeadlineStats.tsx delete mode 100644 frontend/src/js/preview/SelectBox.tsx 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 +67,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,7 +90,7 @@ const MatchingStats = (props: Props) => { : "- - - - - - -"; return ( -
+
{idLabel && ( <> diff --git a/frontend/src/js/preview/Charts.tsx b/frontend/src/js/preview/Charts.tsx index e655f31fcd..84274649fc 100644 --- a/frontend/src/js/preview/Charts.tsx +++ b/frontend/src/js/preview/Charts.tsx @@ -1,33 +1,26 @@ -import { t } from "i18next"; import { ArrowLeftIcon, ArrowRightIcon } from "lucide-react"; import { useHotkeys } from "react-hotkeys-hook"; +import { useTranslation } from "react-i18next"; import { tv } from "tailwind-variants"; import type { PreviewStatistics } from "../api/types"; import { Button } from "../ui-components/Button"; +import { C2 } from "../ui-components/Typography"; import Diagram from "./Diagram"; -const diagram = tv({ - base: ["h-[27vh]", "mr-[15px]", "p-[5px]"], -}); +const root = tv({ base: ["flex flex-col", "gap-4"] }); -// 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-5", "overflow-x-hidden"], }); -const diagramContainer = tv({ - base: ["grid grid-cols-2", "gap-[5px]", "overflow-x-hidden"], +const diagram = tv({ base: "h-[27vh]" }); + +const pagination = tv({ + base: ["flex items-center justify-center", "gap-2"], }); type ChartProps = { statistics: PreviewStatistics[]; - className?: string; showPopup: (statistic: PreviewStatistics) => void; page: number; setPage: (page: number) => void; @@ -37,11 +30,11 @@ const DIAGRAMS_PER_PAGE = 4; export default function Charts({ statistics, - className, showPopup, page, setPage, }: ChartProps) { + const { t } = useTranslation(); const diagramsOnPage = statistics.slice( page * DIAGRAMS_PER_PAGE, (page + 1) * DIAGRAMS_PER_PAGE, @@ -59,21 +52,18 @@ export default function Charts({ useHotkeys("right", () => updatePage(1), [page]); return ( -
+
- {diagramsOnPage.map((statistic) => { - return ( -
- showPopup(statistic)} - /> -
- ); - })} + {diagramsOnPage.map((statistic) => ( + showPopup(statistic)} + /> + ))}
-
+
- - {t("preview.page")} {page + 1}/ - {Math.ceil(statistics.length / DIAGRAMS_PER_PAGE)} - + + {t("preview.page")} {page + 1}/{maxPage} + - 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 && ( - + + {statistics && ( + + + { + const stat = statistics.statistics.find( + (stat) => stat.label === key, + ); + setPopOver(stat ?? null); + }} + > + {statistics.statistics.map((stat) => ( + + {stat.label} + + ))} + + + )} + + {statistics ? ( +
+ + { + setPopOver(statistic); + }} + page={page} + setPage={setPage} + /> +
+ ) : ( +
+ +
+ )} + + {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..a71715b523 100644 --- a/frontend/src/js/preview/ScrollBox.tsx +++ b/frontend/src/js/preview/ScrollBox.tsx @@ -6,40 +6,36 @@ 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]", + "absolute right-5 bottom-5", "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)]", + "rounded", + "bg-white", + "shadow-[1px_1px_5px_0px_rgba(0,0,0,0.2)]", ], }); 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 +43,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..258e2340df 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,15 @@ 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", + "[&_th]:max-w-[25ch] [&_th]:overflow-hidden [&_th]:text-ellipsis [&_th]:whitespace-nowrap", "[&_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", + "[&_tbody_tr:hover_td]:bg-gray-50", "[&_.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 +66,15 @@ 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} + + ), + onHeaderCell: () => ({ title }), dataIndex: field.name, key: field.name, render: (value: string | Vector) => { 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}}" }, From 234629e40241eb4b342af092021f3e37795376fb Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Thu, 8 Oct 2026 15:37:15 +0200 Subject: [PATCH 2/9] Card component for history and preview, stats beside the cards --- frontend/src/js/entity-history/EntityCard.tsx | 28 ++- frontend/src/js/entity-history/Navigation.tsx | 4 +- .../js/entity-history/NavigationHeader.tsx | 80 +++---- .../js/entity-history/timeline/EventCard.tsx | 218 +++++++++--------- frontend/src/js/preview/Diagram.tsx | 7 +- frontend/src/js/preview/Preview.tsx | 175 +++++++------- frontend/src/js/preview/ScrollBox.tsx | 8 +- frontend/src/js/preview/Table.tsx | 3 - .../src/js/ui-components/Card.stories.tsx | 46 ++++ frontend/src/js/ui-components/Card.tsx | 26 +++ 10 files changed, 330 insertions(+), 265 deletions(-) create mode 100644 frontend/src/js/ui-components/Card.stories.tsx create mode 100644 frontend/src/js/ui-components/Card.tsx 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..99f5b74664 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, @@ -30,14 +31,7 @@ const card = tv({ }); 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]", - ], + base: ["rounded", "overflow-hidden", "[&>div:first-of-type]:pt-[14px]"], }); const colBucket = tv({ @@ -124,112 +118,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/preview/Diagram.tsx b/frontend/src/js/preview/Diagram.tsx index 688d34ab79..9cf5aef9d9 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,6 +19,10 @@ import { useDateTickHandler, } from "./util"; +const root = tv({ + variants: { clickable: { true: "cursor-pointer" } }, +}); + type DiagramProps = { stat: PreviewStatistics; className?: string; @@ -215,7 +220,7 @@ export default function Diagram({ }, [data?.labels, stat, t, shouldTickRender, showTitle]); return ( -
+
{previewStatsIsBarStats(stat) ? ( } diff --git a/frontend/src/js/preview/Preview.tsx b/frontend/src/js/preview/Preview.tsx index c658e311a4..d2215f02cc 100644 --- a/frontend/src/js/preview/Preview.tsx +++ b/frontend/src/js/preview/Preview.tsx @@ -13,6 +13,7 @@ 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"; @@ -32,25 +33,19 @@ const content = tv({ const topRow = tv({ base: ["flex items-center", "gap-[30px]"] }); -const card = tv({ - base: [ - "flex flex-col", - "gap-4", - "rounded", - "bg-white", - "p-[14px]", - "shadow-[1px_1px_5px_0px_rgba(0,0,0,0.2)]", - ], +// the stats stand beside the cards, like the info pane beside the panes +const main = tv({ + base: ["grid grid-cols-[auto_minmax(0,1fr)]", "gap-5", "items-start"], }); +const cards = tv({ base: ["flex flex-col", "gap-[10px]"] }); + +const section = tv({ base: ["flex flex-col", "gap-4"] }); + const cardHeader = tv({ base: ["flex items-start justify-between", "gap-5"], }); -const statisticsBody = tv({ - base: ["grid grid-cols-[auto_minmax(0,1fr)]", "gap-5", "items-start"], -}); - const loading = tv({ base: ["flex items-center justify-center", "h-[65vh]"], }); @@ -114,82 +109,88 @@ export default function Preview() { {t("preview.headline")}
-
- - {statistics && ( - - - { - const stat = statistics.statistics.find( - (stat) => stat.label === key, - ); - setPopOver(stat ?? null); - }} - > - {statistics.statistics.map((stat) => ( - - {stat.label} - - ))} - - - )} - - {statistics ? ( -
- - { - setPopOver(statistic); - }} - page={page} - setPage={setPage} - /> -
- ) : ( -
- -
- )} -
- {popOver && ( - setPopOver(null)} +
+ - )} - {preview.arrowReader && - preview.initialTableData?.value && - preview.queryData && ( -
- -
+ +
+ + {statistics && ( + + + { + const stat = statistics.statistics.find( + (stat) => stat.label === key, + ); + setPopOver(stat ?? null); + }} + > + {statistics.statistics.map((stat) => ( + + {stat.label} + + ))} + + + )} + + {statistics ? ( + { + setPopOver(statistic); + }} + page={page} + setPage={setPage} + /> + ) : ( +
+ +
+ )} +
+
+ {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 a71715b523..6f7560c2d2 100644 --- a/frontend/src/js/preview/ScrollBox.tsx +++ b/frontend/src/js/preview/ScrollBox.tsx @@ -14,13 +14,7 @@ const root = tv({ base: "overflow-auto" }); // a white backing, because the secondary button is transparent over the content const scrollTopButton = tv({ - base: [ - "absolute right-5 bottom-5", - "z-3", - "rounded", - "bg-white", - "shadow-[1px_1px_5px_0px_rgba(0,0,0,0.2)]", - ], + base: ["absolute right-5 bottom-5", "z-3", "rounded", "bg-white"], }); export default function ScrollBox({ diff --git a/frontend/src/js/preview/Table.tsx b/frontend/src/js/preview/Table.tsx index 258e2340df..34fdcda39f 100644 --- a/frontend/src/js/preview/Table.tsx +++ b/frontend/src/js/preview/Table.tsx @@ -30,14 +30,12 @@ const table = tv({ "w-full", "border-spacing-0", "[&_th]:bg-bg-50 [&_th]:text-left", - "[&_th]:max-w-[25ch] [&_th]:overflow-hidden [&_th]:text-ellipsis [&_th]:whitespace-nowrap", "[&_td]:max-w-[25ch] [&_td]:overflow-hidden [&_td]:text-ellipsis [&_td]:whitespace-nowrap", "[&_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", - "[&_tbody_tr:hover_td]:bg-gray-50", "[&_.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", @@ -74,7 +72,6 @@ export default memo(function Table({ {title} ), - onHeaderCell: () => ({ 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..3fea2e0f73 --- /dev/null +++ b/frontend/src/js/ui-components/Card.stories.tsx @@ -0,0 +1,46 @@ +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 WithHeading: Story = { + render: () => ( + +

Regions

+ 12 entries +
+ North, South, East and West, with their population per year. +
+
+ ), +}; + +export const WithoutPadding: Story = { + render: () => ( + +
+ Year +
+
+ 2024 +
+
+ 2025 +
+
+ ), +}; diff --git a/frontend/src/js/ui-components/Card.tsx b/frontend/src/js/ui-components/Card.tsx new file mode 100644 index 0000000000..bd95e26e2b --- /dev/null +++ b/frontend/src/js/ui-components/Card.tsx @@ -0,0 +1,26 @@ +import type { ReactNode } from "react"; +import { tv } from "tailwind-variants"; + +const card = tv({ + base: ["rounded", "border border-gray-100", "bg-white"], + variants: { + // `none` for content that brings its own padding, like a table or buckets + padding: { + md: "p-[14px]", + none: "", + }, + }, + defaultVariants: { padding: "md" }, +}); + +/** + * 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 = ({ + padding, + children, +}: { + padding?: "md" | "none"; + children: ReactNode; +}) =>
{children}
; From 916bc2d8ce2699297227836591c97f839f9236b7 Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Thu, 8 Oct 2026 15:45:48 +0200 Subject: [PATCH 3/9] card without a padding variant, event card keeps its column width --- .../js/entity-history/timeline/EventCard.tsx | 12 ++++------- .../timeline/GroupedContent.tsx | 7 ++----- .../src/js/ui-components/Card.stories.tsx | 21 ++++++++----------- frontend/src/js/ui-components/Card.tsx | 20 ++++-------------- 4 files changed, 19 insertions(+), 41 deletions(-) diff --git a/frontend/src/js/entity-history/timeline/EventCard.tsx b/frontend/src/js/entity-history/timeline/EventCard.tsx index 99f5b74664..6f0d963fb6 100644 --- a/frontend/src/js/entity-history/timeline/EventCard.tsx +++ b/frontend/src/js/entity-history/timeline/EventCard.tsx @@ -30,9 +30,7 @@ const card = tv({ base: ["relative", "grid grid-cols-[auto_45px_1fr]", "gap-[3px]", "py-[5px]"], }); -const eventItemContent = tv({ - base: ["rounded", "overflow-hidden", "[&>div:first-of-type]:pt-[14px]"], -}); +const eventItemContent = tv({ base: ["flex flex-col", "gap-5"] }); const colBucket = tv({ base: [ @@ -43,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"], @@ -118,8 +114,8 @@ const EventCard = ({ event={row} sourceColumn={sourceColumn} /> -
- +
+
{contentFilter.money && applicableMoney.length > 0 && (
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/ui-components/Card.stories.tsx b/frontend/src/js/ui-components/Card.stories.tsx index 3fea2e0f73..dce478b3ea 100644 --- a/frontend/src/js/ui-components/Card.stories.tsx +++ b/frontend/src/js/ui-components/Card.stories.tsx @@ -29,18 +29,15 @@ export const WithHeading: Story = { ), }; -export const WithoutPadding: Story = { +export const Stacked: Story = { render: () => ( - -
- Year -
-
- 2024 -
-
- 2025 -
-
+
+ + Population + + + Products + +
), }; diff --git a/frontend/src/js/ui-components/Card.tsx b/frontend/src/js/ui-components/Card.tsx index bd95e26e2b..e5267d9598 100644 --- a/frontend/src/js/ui-components/Card.tsx +++ b/frontend/src/js/ui-components/Card.tsx @@ -2,25 +2,13 @@ import type { ReactNode } from "react"; import { tv } from "tailwind-variants"; const card = tv({ - base: ["rounded", "border border-gray-100", "bg-white"], - variants: { - // `none` for content that brings its own padding, like a table or buckets - padding: { - md: "p-[14px]", - none: "", - }, - }, - defaultVariants: { padding: "md" }, + base: ["rounded", "border border-gray-100", "bg-white", "p-[14px]"], }); /** * 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 = ({ - padding, - children, -}: { - padding?: "md" | "none"; - children: ReactNode; -}) =>
{children}
; +export const Card = ({ children }: { children: ReactNode }) => ( +
{children}
+); From 4071112aecb1fcd13326b9c2d5e1e322f025239b Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Thu, 8 Oct 2026 15:48:43 +0200 Subject: [PATCH 4/9] section headlines on the page, stats beside the charts only, card padding 12 --- frontend/src/js/info-pane/MatchingStats.tsx | 28 +-- frontend/src/js/preview/Preview.tsx | 180 ++++++++++---------- frontend/src/js/ui-components/Card.tsx | 2 +- 3 files changed, 107 insertions(+), 103 deletions(-) diff --git a/frontend/src/js/info-pane/MatchingStats.tsx b/frontend/src/js/info-pane/MatchingStats.tsx index 44b7f55419..d622b8cf9d 100644 --- a/frontend/src/js/info-pane/MatchingStats.tsx +++ b/frontend/src/js/info-pane/MatchingStats.tsx @@ -25,6 +25,10 @@ const dateText = tv({ ], }); +const idText = tv({ + base: ["pr-2", textStyle({ size: 2, strong: true, truncate: true })], +}); + const text = tv({ base: textStyle({ size: 3 }), variants: { @@ -91,17 +95,6 @@ const MatchingStats = (props: Props) => { return (
- {idLabel && ( - <> - -
-

{idLabel}

-

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

-
- - )}

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

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

+ {idLabel} +

+

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

+
+ + )}
); }; diff --git a/frontend/src/js/preview/Preview.tsx b/frontend/src/js/preview/Preview.tsx index d2215f02cc..29a79e3c97 100644 --- a/frontend/src/js/preview/Preview.tsx +++ b/frontend/src/js/preview/Preview.tsx @@ -28,29 +28,27 @@ const fullScreen = tv({ }); const content = tv({ - base: ["flex flex-col", "gap-[10px]", "px-5 pt-[55px] pb-5"], + base: ["flex flex-col", "gap-5", "px-5 pt-[55px] pb-5"], }); const topRow = tv({ base: ["flex items-center", "gap-[30px]"] }); -// the stats stand beside the cards, like the info pane beside the panes -const main = tv({ - base: ["grid grid-cols-[auto_minmax(0,1fr)]", "gap-5", "items-start"], -}); - -const cards = tv({ base: ["flex flex-col", "gap-[10px]"] }); +const section = tv({ base: ["flex flex-col", "gap-[10px]"] }); -const section = tv({ base: ["flex flex-col", "gap-4"] }); - -const cardHeader = tv({ +const sectionHeader = tv({ base: ["flex items-start justify-between", "gap-5"], }); +// 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 loading = tv({ base: ["flex items-center justify-center", "h-[65vh]"], }); -const CardHeader = ({ +const SectionHeader = ({ title, subtitle, children, @@ -59,7 +57,7 @@ const CardHeader = ({ subtitle: string; children?: ReactNode; }) => ( -
+

{title}

{subtitle} @@ -78,7 +76,7 @@ export default function Preview() { 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) { @@ -109,88 +107,88 @@ export default function Preview() { {t("preview.headline")}
-
- -
- -
- + + {stats && ( + + + { + const stat = stats.statistics.find( + (stat) => stat.label === key, + ); + setPopOver(stat ?? null); + }} > - {statistics && ( - - - { - const stat = statistics.statistics.find( - (stat) => stat.label === key, - ); - setPopOver(stat ?? null); - }} - > - {statistics.statistics.map((stat) => ( - - {stat.label} - - ))} - - - )} - - {statistics ? ( - { - setPopOver(statistic); - }} - page={page} - setPage={setPage} - /> - ) : ( -
- -
- )} -
-
- {popOver && ( - setPopOver(null)} - /> + {stats.statistics.map((stat) => ( + + {stat.label} + + ))} + + )} - {preview.arrowReader && - preview.initialTableData?.value && - preview.queryData && ( - -
- -
- - + +
+
+ +
+ + {stats ? ( + { + setPopOver(statistic); + }} + page={page} + setPage={setPage} + /> + ) : ( +
+ +
)} +
- + + {popOver && ( + setPopOver(null)} + /> + )} + {preview.arrowReader && + preview.initialTableData?.value && + preview.queryData && ( +
+ + +
+ + + )} diff --git a/frontend/src/js/ui-components/Card.tsx b/frontend/src/js/ui-components/Card.tsx index e5267d9598..ba3bf3f654 100644 --- a/frontend/src/js/ui-components/Card.tsx +++ b/frontend/src/js/ui-components/Card.tsx @@ -2,7 +2,7 @@ import type { ReactNode } from "react"; import { tv } from "tailwind-variants"; const card = tv({ - base: ["rounded", "border border-gray-100", "bg-white", "p-[14px]"], + base: ["rounded", "border border-gray-100", "bg-white", "p-3"], }); /** From 262d8f8f63aa86239a4901e2fb6b2f59d31b5a91 Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Thu, 8 Oct 2026 15:49:44 +0200 Subject: [PATCH 5/9] stats column at most 200 px --- frontend/src/js/preview/Preview.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/js/preview/Preview.tsx b/frontend/src/js/preview/Preview.tsx index 29a79e3c97..f313922ffe 100644 --- a/frontend/src/js/preview/Preview.tsx +++ b/frontend/src/js/preview/Preview.tsx @@ -138,7 +138,7 @@ export default function Preview() { )}
-
+
Date: Thu, 8 Oct 2026 15:52:24 +0200 Subject: [PATCH 6/9] menu items keep their height when the menu scrolls --- frontend/src/js/ui-components/Menu.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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", From 50d055f69e268c166c2839f0a2821412568b712d Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Thu, 8 Oct 2026 15:55:15 +0200 Subject: [PATCH 7/9] pagination below the chart card, long axis labels cut --- frontend/src/js/preview/Charts.tsx | 82 ++++++-------------------- frontend/src/js/preview/Diagram.tsx | 16 ++++- frontend/src/js/preview/Pagination.tsx | 54 +++++++++++++++++ frontend/src/js/preview/Preview.tsx | 13 +++- 4 files changed, 98 insertions(+), 67 deletions(-) create mode 100644 frontend/src/js/preview/Pagination.tsx diff --git a/frontend/src/js/preview/Charts.tsx b/frontend/src/js/preview/Charts.tsx index 84274649fc..4fcf58f544 100644 --- a/frontend/src/js/preview/Charts.tsx +++ b/frontend/src/js/preview/Charts.tsx @@ -1,89 +1,43 @@ -import { ArrowLeftIcon, ArrowRightIcon } from "lucide-react"; -import { useHotkeys } from "react-hotkeys-hook"; -import { useTranslation } from "react-i18next"; import { tv } from "tailwind-variants"; import type { PreviewStatistics } from "../api/types"; -import { Button } from "../ui-components/Button"; -import { C2 } from "../ui-components/Typography"; import Diagram from "./Diagram"; -const root = tv({ base: ["flex flex-col", "gap-4"] }); - -const diagramContainer = tv({ +const root = tv({ base: ["grid grid-cols-2", "gap-5", "overflow-x-hidden"], }); const diagram = tv({ base: "h-[27vh]" }); -const pagination = tv({ - base: ["flex items-center justify-center", "gap-2"], -}); - -type ChartProps = { - statistics: PreviewStatistics[]; - 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, showPopup, page, - setPage, -}: ChartProps) { - const { t } = useTranslation(); +}: { + 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) => ( - showPopup(statistic)} - /> - ))} -
-
- - - {t("preview.page")} {page + 1}/{maxPage} - - -
+ {diagramsOnPage.map((statistic) => ( + showPopup(statistic)} + /> + ))}
); } diff --git a/frontend/src/js/preview/Diagram.tsx b/frontend/src/js/preview/Diagram.tsx index 9cf5aef9d9..21c486ee4f 100644 --- a/frontend/src/js/preview/Diagram.tsx +++ b/frontend/src/js/preview/Diagram.tsx @@ -30,7 +30,12 @@ type DiagramProps = { 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, @@ -102,6 +107,7 @@ export default function Diagram({ height, width, showTitle = true, + maxLabelLength, }: DiagramProps) { const data = useMemo(() => { const color = `rgba(${hexToRgbA(getCssVarColor("--color-primary-500"))}, 1)`; @@ -174,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">; } @@ -217,7 +231,7 @@ export default function Diagram({ } throw new Error("Unknown stats type"); - }, [data?.labels, stat, t, shouldTickRender, showTitle]); + }, [data?.labels, stat, t, shouldTickRender, showTitle, maxLabelLength]); return (
diff --git a/frontend/src/js/preview/Pagination.tsx b/frontend/src/js/preview/Pagination.tsx new file mode 100644 index 0000000000..27a0ce8028 --- /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-center", "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 f313922ffe..e39618ef60 100644 --- a/frontend/src/js/preview/Preview.tsx +++ b/frontend/src/js/preview/Preview.tsx @@ -17,8 +17,9 @@ 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 Pagination from "./Pagination"; import type { PreviewStateT } from "./reducer"; import ScrollBox from "./ScrollBox"; import Table from "./Table"; @@ -154,7 +155,6 @@ export default function Preview() { setPopOver(statistic); }} page={page} - setPage={setPage} /> ) : (
@@ -163,6 +163,15 @@ export default function Preview() { )}
+ {stats && ( + + )} {popOver && ( Date: Thu, 8 Oct 2026 15:55:46 +0200 Subject: [PATCH 8/9] pagination right-aligned --- frontend/src/js/preview/Pagination.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/js/preview/Pagination.tsx b/frontend/src/js/preview/Pagination.tsx index 27a0ce8028..78cda39818 100644 --- a/frontend/src/js/preview/Pagination.tsx +++ b/frontend/src/js/preview/Pagination.tsx @@ -5,7 +5,7 @@ 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-center", "gap-2"] }); +const root = tv({ base: ["flex items-center justify-end", "gap-2"] }); export default function Pagination({ page, From b02ace51a8c27a58236cda7c7545b53084b95419 Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Thu, 8 Oct 2026 16:03:51 +0200 Subject: [PATCH 9/9] one card story --- .../src/js/ui-components/Card.stories.tsx | 20 ++++++------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/frontend/src/js/ui-components/Card.stories.tsx b/frontend/src/js/ui-components/Card.stories.tsx index dce478b3ea..a8512e30b0 100644 --- a/frontend/src/js/ui-components/Card.stories.tsx +++ b/frontend/src/js/ui-components/Card.stories.tsx @@ -17,23 +17,15 @@ export default { type Story = StoryObj; -export const WithHeading: Story = { - render: () => ( - -

Regions

- 12 entries -
- North, South, East and West, with their population per year. -
-
- ), -}; - -export const Stacked: Story = { +export const Default: Story = { render: () => (
- Population +

Regions

+ 12 entries +
+ North, South, East and West, with their population per year. +
Products