diff --git a/frontend/src/js/entity-history/ConceptBubble.ts b/frontend/src/js/entity-history/ConceptBubble.ts deleted file mode 100644 index 9c1ff3539a..0000000000 --- a/frontend/src/js/entity-history/ConceptBubble.ts +++ /dev/null @@ -1,10 +0,0 @@ -import styled from "@emotion/styled"; - -export const ConceptBubble = styled("span")` - padding: 0 3px; - border-radius: ${({ theme }) => theme.borderRadius}; - color: ${({ theme }) => theme.col.black}; - border: 1px solid ${({ theme }) => theme.col.gray}; - background-color: white; - font-size: ${({ theme }) => theme.font.sm}; -`; diff --git a/frontend/src/js/entity-history/ConceptBubble.tsx b/frontend/src/js/entity-history/ConceptBubble.tsx new file mode 100644 index 0000000000..6bdf5b9c39 --- /dev/null +++ b/frontend/src/js/entity-history/ConceptBubble.tsx @@ -0,0 +1,20 @@ +import type { ComponentProps } from "react"; +import { tv } from "tailwind-variants"; + +const conceptBubble = tv({ + base: [ + "px-[3px]", + "rounded", + "border border-gray-500", + "bg-white", + "text-sm", + "text-gray-800", + ], +}); + +export const ConceptBubble = ({ + className, + ...props +}: ComponentProps<"span">) => ( + +); diff --git a/frontend/src/js/entity-history/ContentControl.tsx b/frontend/src/js/entity-history/ContentControl.tsx index 9fab0b7810..b22d131db0 100644 --- a/frontend/src/js/entity-history/ContentControl.tsx +++ b/frontend/src/js/entity-history/ContentControl.tsx @@ -1,4 +1,3 @@ -import styled from "@emotion/styled"; import { faEuroSign, faFingerprint, @@ -11,12 +10,6 @@ import { useTranslation } from "react-i18next"; import IconButton from "../button/IconButton"; import WithTooltip from "../tooltip/WithTooltip"; -const Root = styled("div")` - display: flex; - flex-direction: column; - align-items: center; -`; - export type ContentType = | "groupId" | "secondaryId" @@ -61,7 +54,7 @@ const ContentControl = ({ value, onChange }: Props) => { ); return ( - +
{options.map((option) => { const active = value[option.key]; return ( @@ -77,7 +70,7 @@ const ContentControl = ({ value, onChange }: Props) => { ); })} - +
); }; diff --git a/frontend/src/js/entity-history/DetailControl.tsx b/frontend/src/js/entity-history/DetailControl.tsx index d8313283e1..55d13e443d 100644 --- a/frontend/src/js/entity-history/DetailControl.tsx +++ b/frontend/src/js/entity-history/DetailControl.tsx @@ -1,4 +1,3 @@ -import styled from "@emotion/styled"; import type { IconProp } from "@fortawesome/fontawesome-svg-core"; import { faBullseye, @@ -8,15 +7,12 @@ import { import { type Dispatch, memo, type SetStateAction, useMemo } from "react"; import { useTranslation } from "react-i18next"; +import { tv } from "tailwind-variants"; + import IconButton from "../button/IconButton"; import WithTooltip from "../tooltip/WithTooltip"; -const Root = styled("div")` - display: flex; - flex-direction: column; - align-items: center; -`; - +const root = tv({ base: "flex flex-col items-center" }); export type DetailLevel = "summary" | "detail" | "full"; interface Props { @@ -57,7 +53,7 @@ export const DetailControl = memo( ({ className, detailLevel, setDetailLevel }: Props) => { const navOptions = useButtonConfig(); return ( - +
{navOptions.map(({ value, icon, tooltip }) => { const selected = value === detailLevel; @@ -72,7 +68,7 @@ export const DetailControl = memo( ); })} - +
); }, ); diff --git a/frontend/src/js/entity-history/EntityCard.tsx b/frontend/src/js/entity-history/EntityCard.tsx index a8ffe44b81..447508a776 100644 --- a/frontend/src/js/entity-history/EntityCard.tsx +++ b/frontend/src/js/entity-history/EntityCard.tsx @@ -1,27 +1,20 @@ -import styled from "@emotion/styled"; +import { tv } from "tailwind-variants"; import type { EntityInfo, TimeStratifiedInfo } from "../api/types"; import EntityInfos from "./EntityInfos"; import { TabbableTimeStratifiedInfos } from "./TabbableTimeStratifiedInfos"; -const Container = styled("div")` - display: grid; - grid-template-columns: 1fr 1fr; - gap: 10px; - padding: 20px 24px; - background-color: ${({ theme }) => theme.col.bg}; - border-radius: ${({ theme }) => theme.borderRadius}; - border: 1px solid ${({ theme }) => theme.col.grayLight}; - align-items: center; -`; - -const Centered = styled("div")` - display: flex; - align-items: flex-start; - flex-direction: column; - gap: 10px; -`; +const container = tv({ + base: [ + "grid grid-cols-2 items-center", + "gap-[10px]", + "px-6 py-5", + "bg-bg-50", + "rounded", + "border border-gray-100", + ], +}); export const EntityCard = ({ blurred, @@ -35,13 +28,13 @@ export const EntityCard = ({ timeStratifiedInfos: TimeStratifiedInfo[]; }) => { return ( - - +
+
- +
{timeStratifiedInfos.length > 0 && ( )} - +
); }; diff --git a/frontend/src/js/entity-history/EntityHeader.tsx b/frontend/src/js/entity-history/EntityHeader.tsx index 1ef2f5efeb..2e11074c91 100644 --- a/frontend/src/js/entity-history/EntityHeader.tsx +++ b/frontend/src/js/entity-history/EntityHeader.tsx @@ -1,6 +1,6 @@ -import styled from "@emotion/styled"; import { useTranslation } from "react-i18next"; import { useSelector } from "react-redux"; +import { tv } from "tailwind-variants"; import type { SelectOptionT } from "../api/types"; import type { StateT } from "../app/reducers"; @@ -9,44 +9,29 @@ import { Heading3 } from "../headings/Headings"; import type { EntityId } from "./reducer"; -const Root = styled("div")` - display: flex; - align-items: center; - gap: 30px; - padding-left: 10px; - justify-content: space-between; - width: 100%; -`; -const Flex = styled("div")` - display: flex; - align-items: center; - gap: 30px; -`; -const Buttons = styled("div")` - display: grid; - grid-template-rows: 1fr 1fr; - grid-auto-flow: column; - gap: 5px; -`; +const root = tv({ + base: [ + "flex items-center justify-between", + "gap-[30px]", + "w-full", + "pl-[10px]", + ], +}); -const SxHeading3 = styled(Heading3)<{ blurred?: boolean }>` - flex-shrink: 0; - margin: 0; - ${({ blurred }) => blurred && "filter: blur(6px);"} -`; -const Subtitle = styled("div")` - font-size: ${({ theme }) => theme.font.xs}; - color: ${({ theme }) => theme.col.gray}; - margin-top: 5px; -`; -const EntityBadge = styled("div")` - display: flex; - gap: 5px; -`; -const Avatar = styled(SxHeading3)` - color: ${({ theme }) => theme.col.gray}; - font-weight: 300; -`; +const buttons = tv({ + base: ["grid grid-rows-[1fr_1fr] grid-flow-col", "gap-[5px]"], +}); + +const entityId = tv({ + base: ["shrink-0", "m-0"], + variants: { + blurred: { true: "blur-[6px]" }, + }, +}); + +const avatar = tv({ + base: ["shrink-0", "m-0", "text-gray-500", "font-light"], +}); export const EntityHeader = ({ blurred, @@ -83,19 +68,21 @@ export const EntityHeader = ({ }; return ( - - +
+
- - #{currentEntityIndex + 1} - {currentEntityId.id} - - +
+ #{currentEntityIndex + 1} + + {currentEntityId.id} + +
+
{totalEvents} {t("history.events", { count: totalEvents })} - +
- - +
+
{entityStatusOptions.map((option, i) => ( ))} - - +
+
); }; diff --git a/frontend/src/js/entity-history/EntityIdsList.tsx b/frontend/src/js/entity-history/EntityIdsList.tsx index 0de7cccb5a..149b1ca59c 100644 --- a/frontend/src/js/entity-history/EntityIdsList.tsx +++ b/frontend/src/js/entity-history/EntityIdsList.tsx @@ -1,6 +1,6 @@ -import styled from "@emotion/styled"; import { faSpinner } from "@fortawesome/free-solid-svg-icons"; import { useMemo } from "react"; +import { tv } from "tailwind-variants"; import { IncrementalList } from "../common/components/IncrementalList"; import FaIcon from "../icon/FaIcon"; @@ -8,63 +8,40 @@ import type { useUpdateHistorySession } from "./actions"; import type { EntityIdsStatus } from "./History"; import type { EntityId } from "./reducer"; -const Row = styled("div")<{ active?: boolean }>` - padding: 1px 3px; - font-size: ${({ theme }) => theme.font.xs}; - display: flex; - align-items: center; - background-color: ${({ active, theme }) => - active ? theme.col.blueGrayVeryLight : "white"}; - height: 24px; - cursor: pointer; - gap: 3px; +const row = tv({ + base: [ + "flex items-center", + "gap-[3px]", + "h-6", + "px-[3px] py-px", + "cursor-pointer", + "text-xs", + ], + variants: { + active: { + true: "bg-primary-50", + false: "bg-white hover:bg-gray-50", + }, + }, +}); - &:hover { - background-color: ${({ active, theme }) => - active ? theme.col.blueGrayVeryLight : theme.col.grayVeryLight}; - } -`; +const entityStatus = tv({ + base: [ + "rounded", + "border-2 border-primary-500", + "bg-white", + "px-1", + "text-xs", + "text-primary-500", + "font-bold", + ], +}); -const Statuses = styled("div")` - display: flex; - align-items: center; - gap: 2px; - margin-left: auto; -`; - -const EntityStatus = styled("div")` - border-radius: ${({ theme }) => theme.borderRadius}; - border: 2px solid ${({ theme }) => theme.col.blueGrayDark}; - background-color: white; - padding: 0px 4px; - font-size: ${({ theme }) => theme.font.xs}; - color: ${({ theme }) => theme.col.blueGrayDark}; - font-weight: 700; -`; - -const TheEntityId = styled("div")<{ active?: boolean }>` - font-weight: 700; - flex-shrink: 0; -`; - -const NumberText = styled("div")` - font-size: ${({ theme }) => theme.font.xs}; - color: ${({ theme }) => theme.col.gray}; - flex-shrink: 0; -`; - -const Gray = styled("span")` - font-weight: 300; - color: ${({ theme }) => theme.col.gray}; -`; - -const SxFaIcon = styled(FaIcon)` - margin: 3px 6px; -`; - -const Blurred = styled("span")<{ blurred?: boolean }>` - ${({ blurred }) => blurred && "filter: blur(6px);"} -`; +const blurrable = tv({ + variants: { + blurred: { true: "blur-[6px]" }, + }, +}); export const EntityIdsList = ({ blurred, @@ -93,25 +70,38 @@ export const EntityIdsList = ({ const entityId = entityIds[index]; return ( - updateHistorySession({ entityId, years: [] })} > - #{index + 1} - - {entityId.id}{" "} - ({entityId.kind}) - - {loadingId === entityId.id && } - +
+ #{index + 1} +
+
+ {entityId.id}{" "} + ({entityId.kind}) +
+ {loadingId === entityId.id && ( + + )} +
{entityIdsStatus[entityId.id] && entityIdsStatus[entityId.id].map((val) => ( - {val.label} +
+ {val.label} +
))} - - +
+ ); }; diff --git a/frontend/src/js/entity-history/EntityInfos.tsx b/frontend/src/js/entity-history/EntityInfos.tsx index ce4b052e80..5da80969c2 100644 --- a/frontend/src/js/entity-history/EntityInfos.tsx +++ b/frontend/src/js/entity-history/EntityInfos.tsx @@ -1,22 +1,22 @@ -import styled from "@emotion/styled"; import { Fragment, memo } from "react"; +import { tv } from "tailwind-variants"; import type { EntityInfo } from "../api/types"; -const Grid = styled("div")` - display: inline-grid; - grid-template-columns: 1fr auto; - gap: 0 20px; - place-items: center start; -`; -const Label = styled("div")` - font-size: ${({ theme }) => theme.font.sm}; -`; -const Value = styled("div")<{ blurred?: boolean }>` - font-size: ${({ theme }) => theme.font.sm}; - font-weight: 400; - ${({ blurred }) => blurred && "filter: blur(6px);"} -`; +const grid = tv({ + base: [ + "inline-grid grid-cols-[1fr_auto]", + "gap-x-5 gap-y-0", + "[place-items:center_start]", + ], +}); + +const value = tv({ + base: ["text-sm", "font-normal"], + variants: { + blurred: { true: "blur-[6px]" }, + }, +}); const EntityInfos = ({ infos, @@ -26,14 +26,14 @@ const EntityInfos = ({ blurred?: boolean; }) => { return ( - +
{infos.map((info) => ( - - {info.value} +
{info.label}
+
{info.value}
))} - +
); }; diff --git a/frontend/src/js/entity-history/History.tsx b/frontend/src/js/entity-history/History.tsx index 6ae2e375f8..4f698d4eb0 100644 --- a/frontend/src/js/entity-history/History.tsx +++ b/frontend/src/js/entity-history/History.tsx @@ -1,10 +1,10 @@ -import styled from "@emotion/styled"; import { useCallback, useMemo, useState } from "react"; import { ErrorBoundary } from "react-error-boundary"; import { useHotkeys } from "react-hotkeys-hook"; import { useTranslation } from "react-i18next"; import { useSelector } from "react-redux"; import { Group, Panel } from "react-resizable-panels"; +import { tv } from "tailwind-variants"; import type { EntityInfo, ResultUrlWithLabel, @@ -32,70 +32,39 @@ import { useOpenCloseInteraction } from "./useOpenCloseInteraction"; import { useSourcesControl } from "./useSourcesControl"; import VisibilityControl from "./VisibilityControl"; -const FullScreen = styled("div")` - position: fixed; - top: 0; - left: 0; - height: 100%; - width: 100%; - z-index: 2; - background-color: ${({ theme }) => theme.col.bgAlt}; -`; +const fullScreen = tv({ + base: ["fixed top-0 left-0", "z-2", "h-full w-full", "bg-bg-100"], +}); -const SxNavigation = styled(Navigation)` - height: 100%; - padding: 55px 0 10px; -`; +const controls = tv({ + base: ["flex items-center", "gap-[18px]", "mr-5"], +}); -const Controls = styled("div")` - display: flex; - align-items: center; - gap: 18px; - margin-right: 20px; -`; +const sidebar = tv({ + base: ["flex flex-col", "gap-5", "pt-[10px]", "border-r border-gray-100"], +}); -const Sidebar = styled("div")` - padding: 10px 0 0; - border-right: 1px solid ${({ theme }) => theme.col.grayLight}; - display: flex; - flex-direction: column; - gap: 20px; -`; -const SidebarBottom = styled("div")` - flex-grow: 1; - display: flex; - flex-direction: column; - gap: 20px; - justify-content: flex-end; -`; +const sidebarBottom = tv({ + base: ["flex flex-col justify-end", "grow", "gap-5"], +}); -const Header = styled("div")` - display: flex; - flex-direction: row-reverse; - gap: 15px; - justify-content: space-between; -`; +const header = tv({ + base: ["flex flex-row-reverse justify-between", "gap-[15px]"], +}); -const Main = styled("div")` - overflow: hidden; - height: 100%; - display: grid; - grid-template-rows: auto 1fr; - padding: 55px 0 10px; - gap: 10px; -`; +const main = tv({ + base: [ + "grid grid-rows-[auto_1fr]", + "gap-[10px]", + "h-full", + "overflow-hidden", + "pt-[55px] pb-[10px]", + ], +}); -const Flex = styled("div")` - display: flex; - height: 100%; - overflow: hidden; - border-top: 1px solid ${({ theme }) => theme.col.grayLight}; -`; - -const SxSourcesControl = styled(SourcesControl)` - flex-shrink: 0; - width: 450px; -`; +const flex = tv({ + base: ["flex", "h-full", "overflow-hidden", "border-t border-gray-100"], +}); export interface EntityIdsStatus { [entityId: string]: SelectOptionT[]; @@ -178,10 +147,11 @@ export const History = () => { return ( - +
- { }> -
-
- - +
+
+ - +
{currentEntityId && ( { entityStatusOptions={entityStatusOptions} /> )} -
- - +
+
+
{ value={contentFilter} onChange={setContentFilter} /> - +
{resultUrls.length > 0 && ( { tooltip={t("history.downloadEntityData")} /> )} - - +
+
{ toggleOpenYear={toggleOpenYear} toggleOpenQuarter={toggleOpenQuarter} /> - - +
+ -
+
); }; diff --git a/frontend/src/js/entity-history/InteractionControl.tsx b/frontend/src/js/entity-history/InteractionControl.tsx index d94e2b9f9c..abe0404c2b 100644 --- a/frontend/src/js/entity-history/InteractionControl.tsx +++ b/frontend/src/js/entity-history/InteractionControl.tsx @@ -1,4 +1,3 @@ -import styled from "@emotion/styled"; import { faChevronRight, faHome } from "@fortawesome/free-solid-svg-icons"; import { memo } from "react"; import { useTranslation } from "react-i18next"; @@ -6,12 +5,6 @@ import { useTranslation } from "react-i18next"; import IconButton from "../button/IconButton"; import WithTooltip from "../tooltip/WithTooltip"; -const Root = styled("div")` - display: flex; - flex-direction: column; - align-items: center; -`; - const InteractionControl = ({ onCloseAll, onOpenAll, @@ -22,14 +15,14 @@ const InteractionControl = ({ const { t } = useTranslation(); return ( - +
- +
); }; diff --git a/frontend/src/js/entity-history/Navigation.tsx b/frontend/src/js/entity-history/Navigation.tsx index 59fbb902b3..8c7c3b6248 100644 --- a/frontend/src/js/entity-history/Navigation.tsx +++ b/frontend/src/js/entity-history/Navigation.tsx @@ -1,4 +1,3 @@ -import styled from "@emotion/styled"; import { faArrowDown, faArrowUp, @@ -16,6 +15,7 @@ import { import { useHotkeys } from "react-hotkeys-hook"; import { useTranslation } from "react-i18next"; import { useDispatch, useSelector } from "react-redux"; +import { tv } from "tailwind-variants"; import type { SelectOptionT } from "../api/types"; import type { StateT } from "../app/reducers"; @@ -34,60 +34,40 @@ import type { EntityId } from "./reducer"; import { SearchEntites } from "./SearchEntities"; import { saveHistory } from "./saveAndLoad"; -const Root = styled("div")` - display: grid; - gap: 10px; - overflow: hidden; - background-color: ${({ theme }) => theme.col.bg}; -`; - -const Row = styled("div")` - margin: 0 10px 0 20px; - display: flex; - gap: 10px; -`; - -const EntityIdNav = styled("div")` - display: flex; - flex-direction: column; - overflow: hidden; - padding: 0 10px 0 20px; -`; -const TopActions = styled("div")` - display: flex; -`; - -const SxNavigationHeader = styled(NavigationHeader)` - margin: 0 10px 0 20px; -`; - -const SxLoadHistoryDropzone = styled(LoadHistoryDropzone)` - height: 100%; - flex-grow: 1; - overflow-y: auto; - padding: 2px; - display: block; - color: inherit; -`; -const BottomActions = styled("div")` - display: flex; -`; - -const ContainedIconButton = styled(IconButton)` - flex-grow: 1; - justify-content: center; -`; - -const SxIconButton = styled(IconButton)` - width: 100%; - justify-content: center; -`; - -const ButtonWithTooltip = styled(WithTooltip)` - color: black; - flex-shrink: 0; - width: 100%; -`; +const root = tv({ + base: ["grid", "gap-[10px]", "overflow-hidden", "bg-bg-50"], +}); + +const row = tv({ + base: ["flex", "gap-[10px]", "mr-[10px] ml-5"], +}); + +const entityIdNav = tv({ + base: ["flex flex-col", "overflow-hidden", "pr-[10px] pl-5"], +}); + +const loadHistoryDropzone = tv({ + base: [ + "block", + "h-full", + "grow", + "overflow-y-auto", + "p-[2px]", + "text-inherit", + ], +}); + +const containedIconButton = tv({ + base: ["grow", "justify-center"], +}); + +const fullWidthIconButton = tv({ + base: ["w-full", "justify-center"], +}); + +const buttonWithTooltip = tv({ + base: ["w-full", "shrink-0", "text-black"], +}); export const Navigation = memo( ({ @@ -158,21 +138,22 @@ export const Navigation = memo( const empty = ids.length === 0; return ( - - +
- {t("common.back")} - + {!empty && ( - + {t("history.settings.reset")} - + )} - +
{!empty && ( - )} - +
{!empty && ( - - + - - - + + +
)} - + {entityIds.length === 0 && ( )} @@ -217,33 +211,42 @@ export const Navigation = memo( entityIdsStatus={entityIdsStatus} loadingId={loadingId} /> - + {!empty && ( <> - - + - - - - - - + + +
+ + CSV - - - + + +
)} -
-
+ + ); }, ); diff --git a/frontend/src/js/entity-history/NavigationHeader.tsx b/frontend/src/js/entity-history/NavigationHeader.tsx index 8b857ab0f5..c5ba7b0fe7 100644 --- a/frontend/src/js/entity-history/NavigationHeader.tsx +++ b/frontend/src/js/entity-history/NavigationHeader.tsx @@ -1,8 +1,8 @@ -import styled from "@emotion/styled"; import { faSliders } from "@fortawesome/free-solid-svg-icons"; import { type Dispatch, memo, type SetStateAction, useState } from "react"; import { useTranslation } from "react-i18next"; import { useSelector } from "react-redux"; +import { tv } from "tailwind-variants"; import type { SelectOptionT } from "../api/types"; import type { StateT } from "../app/reducers"; @@ -13,47 +13,41 @@ import WithTooltip from "../tooltip/WithTooltip"; import { SettingsModal } from "./SettingsModal"; -const Root = styled("div")` - display: grid; - gap: 8px; - background-color: white; - box-shadow: 1px 1px 5px 0px rgba(0, 0, 0, 0.2); - padding: 14px; - border-radius: ${({ theme }) => theme.borderRadius}; -`; +const root = tv({ + base: [ + "grid", + "gap-2", + "bg-white", + "shadow-[1px_1px_5px_0px_rgba(0,0,0,0.2)]", + "p-[14px]", + "rounded", + ], +}); -const BaseInfo = styled("div")` - display: flex; - gap: 15px; - justify-content: space-between; - overflow: hidden; -`; +const baseInfo = tv({ + base: ["flex justify-between", "gap-[15px]", "overflow-hidden"], +}); -const SxHeading3 = styled(Heading3)` - flex-shrink: 0; - margin: 0; - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; -`; - -const Count = styled(SxHeading3)` - justify-self: end; -`; +const heading = tv({ + base: [ + "shrink-0", + "m-0", + "whitespace-nowrap", + "text-ellipsis", + "overflow-hidden", + ], + variants: { + end: { true: "justify-self-end" }, + }, +}); -const Text = styled("span")` - font-size: ${({ theme }) => theme.font.md}; - color: ${({ theme }) => theme.col.gray}; - font-weight: 400; -`; +const infoText = tv({ + base: ["text-base", "text-gray-500", "font-normal"], +}); -const SpecialText = styled("p")<{ zero?: boolean }>` - margin: 0; - font-size: ${({ theme }) => theme.font.xs}; - color: ${({ theme, zero }) => (zero ? theme.col.red : theme.col.gray)}; - text-transform: uppercase; - font-weight: 400; -`; +const specialText = tv({ + base: ["m-0", "text-xs", "uppercase", "font-normal", "text-gray-500"], +}); interface Props { className?: string; @@ -78,7 +72,7 @@ export const NavigationHeader = memo( const [settingsModalOpen, setSettingsModalOpen] = useState(false); return ( - +
{settingsModalOpen && ( setSettingsModalOpen(false)} @@ -86,10 +80,12 @@ export const NavigationHeader = memo( entityStatusOptions={entityStatusOptions} /> )} - +
- {label} - {t("history.history")} + + {label} + +

{t("history.history")}

setSettingsModalOpen(true)} /> - +
- {idsCount} - {t("tooltip.entitiesFound", { count: idsCount })} - {markedCount} - {t("history.marked", { count: markedCount })} + {idsCount} + + {t("tooltip.entitiesFound", { count: idsCount })} + + {markedCount} + + {t("history.marked", { count: markedCount })} +
- +
); }, ); diff --git a/frontend/src/js/entity-history/RowDates.tsx b/frontend/src/js/entity-history/RowDates.tsx index 9f23e26aeb..2976ff8f0e 100644 --- a/frontend/src/js/entity-history/RowDates.tsx +++ b/frontend/src/js/entity-history/RowDates.tsx @@ -1,17 +1,3 @@ -import styled from "@emotion/styled"; - -const DateText = styled("div")` - font-size: ${({ theme }) => theme.font.xs}; -`; - -const Line = styled("div")` - display: block; - margin: 1px 0 1px 14px; - width: 1px; - height: 7px; - background-color: ${({ theme }) => theme.col.gray}; -`; - export const formatHistoryDayRange = (dateStr: string) => { const [, month, day] = dateStr.split("-"); return `${day}.${month}.`; @@ -28,16 +14,12 @@ export const RowDates = ({ dates }: Props) => { const sameDate = dates.from === dates.to; return sameDate ? ( - - {formatHistoryDayRange(dates.from)} - +
{formatHistoryDayRange(dates.from)}
) : ( - +
{formatHistoryDayRange(dates.from)} - +
{formatHistoryDayRange(dates.to)} - +
); }; diff --git a/frontend/src/js/entity-history/SearchEntities.tsx b/frontend/src/js/entity-history/SearchEntities.tsx index a2f6ab8c9f..6dcb8f0662 100644 --- a/frontend/src/js/entity-history/SearchEntities.tsx +++ b/frontend/src/js/entity-history/SearchEntities.tsx @@ -1,8 +1,8 @@ -import styled from "@emotion/styled"; import { faSpinner } from "@fortawesome/free-solid-svg-icons"; import { useCallback, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { useSelector } from "react-redux"; +import { tv } from "tailwind-variants"; import { usePostPrefixForSuggestions, @@ -204,25 +204,18 @@ const useSubmitSearch = ({ }; }; -const Root = styled("div")` - display: flex; - flex-direction: column; - align-items: center; - gap: 14px; - padding: 10px 12px; - - > div { - width: 100%; - } -`; - -const SxPrimaryButton = styled(PrimaryButton)` - width: 100%; - display: flex; - align-items: center; - justify-content: center; - gap: 14px; -`; +const root = tv({ + base: [ + "flex flex-col items-center", + "gap-[14px]", + "px-3 py-[10px]", + "[&>div]:w-full", + ], +}); + +const submitButton = tv({ + base: ["flex items-center justify-center", "w-full", "gap-[14px]"], +}); const noop = () => {}; @@ -245,7 +238,7 @@ export const SearchEntitiesComponent = ({ if (searchFilters.length === 0) return null; return ( - +
- {loading && } {t("history.searchEntitiesButton")} - - + +
); }; diff --git a/frontend/src/js/entity-history/SettingsModal.tsx b/frontend/src/js/entity-history/SettingsModal.tsx index 0e3b4486da..3b61557d01 100644 --- a/frontend/src/js/entity-history/SettingsModal.tsx +++ b/frontend/src/js/entity-history/SettingsModal.tsx @@ -1,4 +1,3 @@ -import styled from "@emotion/styled"; import type { Dispatch, SetStateAction } from "react"; import { useTranslation } from "react-i18next"; @@ -6,10 +5,6 @@ import type { SelectOptionT } from "../api/types"; import Modal from "../modal/Modal"; import InputMultiSelect from "../ui-components/InputMultiSelect/InputMultiSelect"; -const Content = styled("div")` - width: 300px; -`; - interface Props { onClose: () => void; entityStatusOptions: SelectOptionT[]; @@ -24,7 +19,7 @@ export const SettingsModal = ({ const { t } = useTranslation(); return ( - +
- +
); }; diff --git a/frontend/src/js/entity-history/TabbableTimeStratifiedInfos.tsx b/frontend/src/js/entity-history/TabbableTimeStratifiedInfos.tsx index 28d8659017..2ba233cd19 100644 --- a/frontend/src/js/entity-history/TabbableTimeStratifiedInfos.tsx +++ b/frontend/src/js/entity-history/TabbableTimeStratifiedInfos.tsx @@ -1,5 +1,5 @@ -import styled from "@emotion/styled"; import { useMemo, useState } from "react"; +import { tv } from "tailwind-variants"; import type { TimeStratifiedInfo } from "../api/types"; import SmallTabNavigation from "../small-tab-navigation/SmallTabNavigation"; @@ -8,13 +8,9 @@ import { TimeStratifiedChart } from "./TimeStratifiedChart"; import { TimeStratifiedConceptChart } from "./TimeStratifiedConceptChart"; import { isConceptColumn, isMoneyColumn } from "./timeline/util/util"; -const Container = styled("div")` - align-self: flex-start; - display: flex; - flex-direction: column; - align-items: flex-end; - overflow-x: hidden; -`; +const container = tv({ + base: ["self-start", "flex flex-col items-end", "overflow-x-hidden"], +}); export const TabbableTimeStratifiedInfos = ({ infos, @@ -54,7 +50,7 @@ export const TabbableTimeStratifiedInfos = ({ }, [infos, activeTab]); return ( - +
)} - +
); }; diff --git a/frontend/src/js/entity-history/TimeStratifiedChart.tsx b/frontend/src/js/entity-history/TimeStratifiedChart.tsx index a0c1a023d4..992cd48c30 100644 --- a/frontend/src/js/entity-history/TimeStratifiedChart.tsx +++ b/frontend/src/js/entity-history/TimeStratifiedChart.tsx @@ -1,5 +1,3 @@ -import { useTheme } from "@emotion/react"; -import styled from "@emotion/styled"; import { BarElement, CategoryScale, @@ -13,9 +11,11 @@ import { } from "chart.js"; import { useMemo } from "react"; import { Bar } from "react-chartjs-2"; +import { tv } from "tailwind-variants"; import type { TimeStratifiedInfo } from "../api/types"; import { exists } from "../common/helpers/exists"; +import { getCssVarColor } from "../preview/getThemeColor"; import { formatCurrency } from "./timeline/util/util"; @@ -33,12 +33,9 @@ ChartJS.register( Tooltip, ); -const ChartContainer = styled("div")` - height: 190px; - width: 100%; - display: flex; - justify-content: flex-end; -`; +const chartContainer = tv({ + base: ["flex justify-end", "h-[190px]", "w-full"], +}); export function hexToRgbA(hex: string) { let c: string | string[]; @@ -63,24 +60,22 @@ export const TimeStratifiedChart = ({ }: { timeStratifiedInfo: TimeStratifiedInfo; }) => { - const theme = useTheme(); const labels = timeStratifiedInfo.columns.map((col) => col.label); const datasets = useMemo(() => { const sortedYears = [...timeStratifiedInfo.years].sort( (a, b) => b.year - a.year, ); + const color = hexToRgbA(getCssVarColor("--color-primary-500")); return sortedYears.map((year, i) => { return { label: year.year.toString(), data: labels.map((label) => year.values[label]), - backgroundColor: `rgba(${hexToRgbA( - theme.col.blueGrayDark, - )}, ${interpolateDecreasingOpacity(i)})`, + backgroundColor: `rgba(${color}, ${interpolateDecreasingOpacity(i)})`, }; }); - }, [theme, timeStratifiedInfo, labels]); + }, [timeStratifiedInfo, labels]); const data = { labels, @@ -156,8 +151,8 @@ export const TimeStratifiedChart = ({ }, [timeStratifiedInfo, labels]); return ( - +
- +
); }; diff --git a/frontend/src/js/entity-history/TimeStratifiedConceptChart.tsx b/frontend/src/js/entity-history/TimeStratifiedConceptChart.tsx index 2fc2d532e7..c5bbc0d506 100644 --- a/frontend/src/js/entity-history/TimeStratifiedConceptChart.tsx +++ b/frontend/src/js/entity-history/TimeStratifiedConceptChart.tsx @@ -1,7 +1,7 @@ -import styled from "@emotion/styled"; import { faBan } from "@fortawesome/free-solid-svg-icons"; import { Fragment } from "react"; import { useTranslation } from "react-i18next"; +import { tv } from "tailwind-variants"; import type { ColumnDescriptionSemanticConceptColumn, @@ -13,38 +13,35 @@ import WithTooltip from "../tooltip/WithTooltip"; import { ConceptBubble } from "./ConceptBubble"; -const Container = styled("div")` - display: grid; - place-items: center; - max-width: 100%; - overflow-x: auto; - gap: 0 3px; - padding: 10px; -`; +const container = tv({ + base: [ + "grid place-items-center", + "gap-x-[3px] gap-y-0", + "max-w-full", + "overflow-x-auto", + "p-[10px]", + ], +}); -const EmptyMsg = styled("p")` - font-size: ${({ theme }) => theme.font.md}; - color: ${({ theme }) => theme.col.gray}; - margin: 40px 0; - display: flex; - align-items: center; - gap: 10px; -`; +const emptyMsg = tv({ + base: [ + "flex items-center", + "gap-[10px]", + "my-10", + "text-base", + "text-gray-500", + ], +}); -const BubbleYes = styled("div")` - width: 14px; - height: 14px; - background-color: ${({ theme }) => theme.col.blueGray}; -`; -const BubbleNo = styled("div")` - width: 14px; - height: 14px; - background-color: ${({ theme }) => theme.col.grayLight}; -`; - -const Year = styled("div")` - font-size: ${({ theme }) => theme.font.sm}; -`; +const bubble = tv({ + base: ["h-[14px] w-[14px]"], + variants: { + filled: { + true: "bg-primary-200", + false: "bg-gray-100", + }, + }, +}); export const TimeStratifiedConceptChart = ({ timeStratifiedInfo, @@ -87,16 +84,17 @@ export const TimeStratifiedConceptChart = ({ if (allValues.length === 0) return ( - - +
+

{t("history.noData")} - - +

+
); return ( - ( - {year} - {allValues.map((val) => - valuesPerYear[i].includes(val) ? ( - - ) : ( - - ), - )} +
{year}
+ {allValues.map((val) => ( +
+ ))} ))} - +
); }; diff --git a/frontend/src/js/entity-history/Timeline.tsx b/frontend/src/js/entity-history/Timeline.tsx index ce015332b3..d87ef886b1 100644 --- a/frontend/src/js/entity-history/Timeline.tsx +++ b/frontend/src/js/entity-history/Timeline.tsx @@ -1,6 +1,6 @@ -import styled from "@emotion/styled"; import { Fragment, memo } from "react"; import { useSelector } from "react-redux"; +import { tv } from "tailwind-variants"; import type { CurrencyConfigT, @@ -20,29 +20,28 @@ import Year from "./timeline/Year"; import { TimelineSearch } from "./timeline-search/TimelineSearch"; import { useTimelineSearch } from "./timeline-search/timelineSearchState"; -const Root = styled("div")<{ isEmpty?: boolean }>` - overflow-y: auto; - -webkit-overflow-scrolling: touch; - padding: 0 20px 20px 10px; - display: inline-grid; - grid-template-columns: 280px auto; - grid-auto-rows: ${({ isEmpty }) => - isEmpty ? "1fr" : "minmax(min-content, max-content) 1fr"}; - gap: 20px 4px; - width: 100%; - height: 100%; -`; - -const Divider = styled("div")` - grid-column: 1 / span 2; - height: 1px; - background: ${({ theme }) => theme.col.grayLight}; -`; +const root = tv({ + base: [ + "overflow-y-auto", + "[-webkit-overflow-scrolling:touch]", + "pt-0 pr-5 pb-5 pl-[10px]", + "inline-grid", + "grid-cols-[280px_auto]", + "gap-x-[4px] gap-y-5", + "w-full", + "h-full", + ], + variants: { + isEmpty: { + true: "auto-rows-fr", + false: "auto-rows-[minmax(min-content,max-content)_1fr]", + }, + }, +}); -const SxTimelineEmptyPlaceholder = styled(TimelineEmptyPlaceholder)` - grid-column: span 2; - height: 100%; -`; +const divider = tv({ + base: ["col-start-1 col-span-2", "h-px", "bg-gray-100"], +}); export const Timeline = memo( ({ @@ -103,7 +102,7 @@ export const Timeline = memo( return (
- +
{!isEmpty && !searchTerm && ( )} - {isEmpty && } + {isEmpty && ( + + )} {dateColumn && sourceColumn && eventsByQuarterWithGroups.map(({ year, quarterwiseData }, i) => ( @@ -133,10 +137,12 @@ export const Timeline = memo( dateColumn={dateColumn} sourceColumn={sourceColumn} /> - {i < eventsByQuarterWithGroups.length - 1 && } + {i < eventsByQuarterWithGroups.length - 1 && ( +
+ )} ))} - +
); }, diff --git a/frontend/src/js/entity-history/VisibilityControl.tsx b/frontend/src/js/entity-history/VisibilityControl.tsx index f1bfed7ce6..5080515c68 100644 --- a/frontend/src/js/entity-history/VisibilityControl.tsx +++ b/frontend/src/js/entity-history/VisibilityControl.tsx @@ -1,4 +1,3 @@ -import styled from "@emotion/styled"; import { faEye, faEyeSlash } from "@fortawesome/free-regular-svg-icons"; import { memo } from "react"; import { useTranslation } from "react-i18next"; @@ -6,16 +5,6 @@ import { useTranslation } from "react-i18next"; import IconButton from "../button/IconButton"; import WithTooltip from "../tooltip/WithTooltip"; -const Root = styled("div")` - display: flex; - flex-direction: column; - align-items: center; -`; - -const SxIconButton = styled(IconButton)` - padding: 8px 10px; -`; - const VisibilityControl = ({ blurred, toggleBlurred, @@ -26,14 +15,15 @@ const VisibilityControl = ({ const { t } = useTranslation(); return ( - +
- - +
); }; diff --git a/frontend/src/js/entity-history/timeline/ConceptName.tsx b/frontend/src/js/entity-history/timeline/ConceptName.tsx index ace0b6a4de..f1bb630f9d 100644 --- a/frontend/src/js/entity-history/timeline/ConceptName.tsx +++ b/frontend/src/js/entity-history/timeline/ConceptName.tsx @@ -1,20 +1,15 @@ -import styled from "@emotion/styled"; import { faFolder } from "@fortawesome/free-solid-svg-icons"; import { memo } from "react"; +import { tv } from "tailwind-variants"; import type { ConceptIdT, ConceptT } from "../../api/types"; import { Highlighter } from "../../common/components/Highlighter"; import { getConceptById } from "../../concept-trees/globalTreeStoreHelper"; import FaIcon from "../../icon/FaIcon"; import { useTimelineSearch } from "../timeline-search/timelineSearchState"; -const Root = styled("div")` - display: flex; - align-items: center; - gap: 10px; -`; -const Named = styled("span")` - font-weight: 400; -`; +const conceptRoot = tv({ + base: ["flex items-center", "gap-[10px]"], +}); interface Props { className?: string; @@ -39,7 +34,7 @@ const ConceptLabel = ({ : conceptId; return ( - + {searchTerm && searchTerm.length > 0 ? ( + ); }; @@ -96,7 +91,7 @@ const ConceptName = ({ className, title, rootConceptId, conceptId }: Props) => { const rootConcept = getConceptById(rootConceptId, rootConceptId); return ( - +
{rootConcept && ( @@ -108,7 +103,7 @@ const ConceptName = ({ className, title, rootConceptId, conceptId }: Props) => { searchTerm={searchTerm} /> - +
); }; diff --git a/frontend/src/js/entity-history/timeline/EventCard.tsx b/frontend/src/js/entity-history/timeline/EventCard.tsx index 4597ec9b83..c467a2bdd8 100644 --- a/frontend/src/js/entity-history/timeline/EventCard.tsx +++ b/frontend/src/js/entity-history/timeline/EventCard.tsx @@ -1,4 +1,3 @@ -import styled from "@emotion/styled"; import { faEuroSign, faFingerprint, @@ -6,6 +5,7 @@ import { } from "@fortawesome/free-solid-svg-icons"; import { useTranslation } from "react-i18next"; import { type InputAttributes, NumericFormat } from "react-number-format"; +import { tv } from "tailwind-variants"; import type { ColumnDescription, ConceptIdT, @@ -25,73 +25,64 @@ import { TinyLabel } from "./TinyLabel"; import type { ColumnBuckets } from "./util/useColumnInformation"; import { isDateColumn, isSourceColumn } from "./util/util"; -const Card = styled("div")` - display: grid; - grid-template-columns: auto 45px 1fr; - gap: 3px; - font-size: ${({ theme }) => theme.font.xs}; - padding: 5px 0; - position: relative; -`; +const card = tv({ + base: [ + "relative", + "grid grid-cols-[auto_45px_1fr]", + "gap-[3px]", + "py-[5px]", + "text-xs", + ], +}); -const EventItemContent = styled("div")` - border-radius: ${({ theme }) => theme.borderRadius}; - box-shadow: 0 0 1px 1px ${({ theme }) => theme.col.grayLight}; - margin-top: 5px; - background-color: white; - overflow: hidden; - > div { - &:first-of-type { - padding-top: 14px; - } - } -`; +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 ColBucket = styled("div")` - color: black; - padding: 1px 4px; - display: grid; - width: 100%; - grid-template-columns: 1fr 1fr 1fr; - @media (min-width: 1800px) { - grid-template-columns: 1fr 1fr 1fr 1fr; - } - @media (min-width: 2500px) { - grid-template-columns: 1fr 1fr 1fr 1fr 1fr; - } - gap: 3px 10px; -`; +const colBucket = tv({ + base: [ + "grid grid-cols-3 min-[1800px]:grid-cols-4 min-[2500px]:grid-cols-5", + "gap-x-[10px] gap-y-[3px]", + "w-full", + "px-1 py-px", + "text-black", + ], +}); -const Flex = styled("div")` - display: flex; - align-items: flex-start; - gap: 5px; - padding: 12px 15px 10px 6px; - font-size: ${({ theme }) => theme.font.sm}; -`; +const flex = tv({ + base: [ + "flex items-start", + "gap-[5px]", + "pt-3 pr-[15px] pb-[10px] pl-[6px]", + "text-sm", + ], +}); -const SxRawDataBadge = styled(RawDataBadge)` - position: absolute; - z-index: 1; - top: 4px; - left: 55px; -`; +const rawDataBadge = tv({ + base: ["absolute top-[4px] left-[55px]", "z-1"], +}); -const SxFaIcon = styled(FaIcon)` - width: 24px !important; - text-align: center; - margin: 8px 5px; - font-size: ${({ theme }) => theme.font.md}; -`; +const bucketIcon = tv({ + // w-6! beats FaIcon's own w-[initial]! via merge, like the old !important did + base: ["w-6!", "text-center", "mx-[5px] my-2", "text-base"], +}); -const Bullet = styled("div")` - width: 10px; - height: 10px; - margin: 2px 0; - background-color: ${({ theme }) => theme.col.blueGrayDark}; - border-radius: 50%; - flex-shrink: 0; -`; +const bullet = tv({ + base: [ + "h-[10px] w-[10px]", + "my-[2px]", + "bg-primary-500", + "rounded-full", + "shrink-0", + ], +}); const EventCard = ({ row, @@ -138,19 +129,28 @@ const EventCard = ({ const restTooltip = t("history.content.rest"); return ( - - +
+
- - + +
{contentFilter.money && applicableMoney.length > 0 && ( - +
- + - +
{applicableMoney.map((column) => (
{column.defaultLabel} @@ -166,8 +166,8 @@ const EventCard = ({
))} - - +
+
)} {groupedRowsKeysWithDifferentValues && groupedRows && ( )} {contentFilter.rest && applicableRest.length > 0 && ( - +
- + - +
{applicableRest.map((column) => (
{column.defaultLabel} @@ -204,17 +204,22 @@ const EventCard = ({
))} - - +
+
)} {contentFilter.groupId && applicableGroupableIds.length > 0 && ( - +
- + - +
{applicableGroupableIds.map((column) => (
{column.defaultLabel} @@ -230,11 +235,11 @@ const EventCard = ({
))} - - +
+
)} - - +
+
); }; diff --git a/frontend/src/js/entity-history/timeline/GroupedContent.tsx b/frontend/src/js/entity-history/timeline/GroupedContent.tsx index d65477996d..910c9ebe07 100644 --- a/frontend/src/js/entity-history/timeline/GroupedContent.tsx +++ b/frontend/src/js/entity-history/timeline/GroupedContent.tsx @@ -1,7 +1,10 @@ -import { css, type Theme } from "@emotion/react"; -import styled from "@emotion/styled"; import { memo, type ReactNode, useMemo } from "react"; -import { NumericFormat } from "react-number-format"; +import { + type InputAttributes, + NumericFormat, + type NumericFormatProps, +} from "react-number-format"; +import { tv } from "tailwind-variants"; import type { ColumnDescription, @@ -22,16 +25,17 @@ import { isVisibleColumn, } from "./util/util"; -const Grid = styled("div")` - display: inline-grid; - gap: 5px 10px; -`; +const grid = tv({ + base: ["inline-grid", "gap-x-[10px] gap-y-[5px]"], +}); -const ExtraArea = styled("div")` - padding: 8px 15px 12px 49px; - overflow-x: auto; - -webkit-overflow-scrolling: touch; -`; +const extraArea = tv({ + base: [ + "pt-2 pr-[15px] pb-3 pl-[49px]", + "overflow-x-auto", + "[-webkit-overflow-scrolling:touch]", + ], +}); const getColumnDescriptionContentType = ( columnDescription?: ColumnDescription, @@ -95,8 +99,9 @@ const GroupedContent = ({ } return ( - - +
)), )} - - +
+
); }; -const cellStyles = (theme: Theme) => css` - white-space: nowrap; - font-size: ${theme.font.sm}; -`; -const CellWrap = styled("span")` - ${({ theme }) => cellStyles(theme)}; -`; -const SxConceptName = styled(ConceptName)` - ${({ theme }) => cellStyles(theme)}; -`; -const SxNumericFormat = styled(NumericFormat)` - ${({ theme }) => cellStyles(theme)}; -`; +// named cellText, not cell: the Cell component's `cell` prop would shadow it +const cellText = tv({ + base: ["whitespace-nowrap", "text-sm"], +}); const Cell = memo( ({ @@ -148,18 +144,21 @@ const Cell = memo( }) => { if (isDateColumn(columnDescription)) { return (cell as DateRow).from === (cell as DateRow).to ? ( - {formatHistoryDayRange((cell as DateRow).from)} + + {formatHistoryDayRange((cell as DateRow).from)} + ) : ( - + {formatHistoryDayRange((cell as DateRow).from)} -{" "} {formatHistoryDayRange((cell as DateRow).to)} - + ); } if (isConceptColumn(columnDescription)) { return ( - - ); + // typed up front with an explicit base type: the polymorphic NumericFormat + // props are otherwise too complex a union for tsc + const numericFormatProps: NumericFormatProps = { + ...currencyConfig, + className: cellText(), + displayType: "text", + value: parseFloat(cell as string), + }; + return {...numericFormatProps} />; } - return {cell as ReactNode}; + return {cell as ReactNode}; }, ); diff --git a/frontend/src/js/entity-history/timeline/Quarter.tsx b/frontend/src/js/entity-history/timeline/Quarter.tsx index 6f4364ac94..fc0373e0ff 100644 --- a/frontend/src/js/entity-history/timeline/Quarter.tsx +++ b/frontend/src/js/entity-history/timeline/Quarter.tsx @@ -1,7 +1,7 @@ -import styled from "@emotion/styled"; import { faCaretDown, faCaretRight } from "@fortawesome/free-solid-svg-icons"; import { memo } from "react"; import { useTranslation } from "react-i18next"; +import { tv } from "tailwind-variants"; import type { ColumnDescription, @@ -16,63 +16,50 @@ import EventCard from "./EventCard"; import { SmallHeading } from "./SmallHeading"; import type { ColumnBuckets } from "./util/useColumnInformation"; -const EventTimeline = styled("div")` - display: grid; - grid-template-columns: auto 1fr; -`; -const EventItemList = styled("div")` - width: calc(100% + 10px); - margin-left: -10px; -`; +const eventTimeline = tv({ + base: ["grid grid-cols-[auto_1fr]"], +}); -const VerticalLine = styled("div")` - height: calc(100% - 20px); - width: 2px; - background-color: ${({ theme }) => theme.col.blueGrayVeryLight}; - margin: 10px 4px; -`; +const eventItemList = tv({ + base: ["w-[calc(100%+10px)]", "-ml-[10px]"], +}); -const QuarterGroup = styled("div")``; -const QuarterHead = styled("div")<{ empty?: boolean }>` - font-size: ${({ theme }) => theme.font.xs}; - color: ${({ theme, empty }) => - empty ? theme.col.grayLight : theme.col.gray}; - position: sticky; - top: 0; - z-index: 2; - background-color: ${({ theme }) => theme.col.bgAlt}; - margin-left: -6px; - line-height: 1; - width: calc(100% + 8px); -`; +const verticalLine = tv({ + base: [ + "h-[calc(100%-20px)]", + "w-[2px]", + "bg-primary-50", + "mx-[4px] my-[10px]", + ], +}); -const InlineGrid = styled("div")` - display: inline-grid; - grid-template-columns: 20px 20px 110px 1fr; - align-items: center; - cursor: pointer; - border: 1px solid transparent; - border-radius: ${({ theme }) => theme.borderRadius}; - padding: 6px 10px; - &:hover { - border: 1px solid ${({ theme }) => theme.col.blueGray}; - } -`; - -const Boxes = styled("div")` - display: flex; - align-items: center; -`; -const Box = styled("div")` - width: 2px; - height: 16px; - margin-left: 1px; - background-color: ${({ theme }) => theme.col.blueGrayDark}; -`; +const quarterHead = tv({ + base: [ + "sticky top-0", + "z-2", + "bg-bg-100", + "-ml-[6px]", + "w-[calc(100%+8px)]", + "text-xs", + "leading-none", + ], + variants: { + empty: { + true: "text-gray-100", + false: "text-gray-500", + }, + }, +}); -const SxSmallHeading = styled(SmallHeading)` - line-height: 1; -`; +const inlineGrid = tv({ + base: [ + "inline-grid grid-cols-[20px_20px_110px_1fr] items-center", + "cursor-pointer", + "border border-transparent hover:border-primary-200", + "rounded", + "px-[10px] py-[6px]", + ], +}); export const Quarter = memo( ({ @@ -114,11 +101,16 @@ export const Quarter = memo( (isOpen || detailLevel !== "summary") && totalEventsPerQuarter > 0; return ( - - - toggleOpenQuarter(year, quarter)}> +
+
+ {/* biome-ignore lint/a11y/noStaticElementInteractions: TODO make this a button */} + {/* biome-ignore lint/a11y/useKeyWithClickEvents: TODO make this a button */} +
toggleOpenQuarter(year, quarter)} + > - Q{quarter} + Q{quarter} – {totalEventsPerQuarter}{" "} {t("history.events", { @@ -128,12 +120,12 @@ export const Quarter = memo( {detailLevel === "summary" && ( )} - - +
+
{areEventsShown && ( - - - +
+
+
{groupedEvents.map((group, index) => { if (group.length === 0) return null; @@ -188,10 +180,10 @@ export const Quarter = memo( ); } })} - - +
+
)} - +
); }, ); @@ -199,11 +191,11 @@ export const Quarter = memo( const MemoizedBoxes = memo( ({ totalEventsPerQuarter }: { totalEventsPerQuarter: number }) => { return ( - +
{new Array(totalEventsPerQuarter).fill(0).map((_, i) => ( - +
))} - +
); }, ); diff --git a/frontend/src/js/entity-history/timeline/RawDataBadge.tsx b/frontend/src/js/entity-history/timeline/RawDataBadge.tsx index db0468d1fc..05a81a22db 100644 --- a/frontend/src/js/entity-history/timeline/RawDataBadge.tsx +++ b/frontend/src/js/entity-history/timeline/RawDataBadge.tsx @@ -1,16 +1,18 @@ -import styled from "@emotion/styled"; +import { tv } from "tailwind-variants"; import type { ColumnDescription } from "../../api/types"; import type { EntityEvent } from "../reducer"; -const Badge = styled("div")` - border-radius: ${({ theme }) => theme.borderRadius}; - background-color: ${({ theme }) => theme.col.blueGrayDark}; - padding: 1px 4px; - font-size: ${({ theme }) => theme.font.xs}; - color: white; - font-weight: 700; -`; +const badge = tv({ + base: [ + "rounded", + "bg-primary-500", + "px-1 py-px", + "text-xs", + "text-white", + "font-bold", + ], +}); interface Props { event: EntityEvent; @@ -20,8 +22,10 @@ interface Props { export const RawDataBadge = ({ className, event, sourceColumn }: Props) => { return ( - { if (navigator.clipboard) { navigator.clipboard.writeText(JSON.stringify(event, null, 2)); @@ -29,6 +33,6 @@ export const RawDataBadge = ({ className, event, sourceColumn }: Props) => { }} > {event[sourceColumn.label] as string} - +
); }; diff --git a/frontend/src/js/entity-history/timeline/SmallHeading.tsx b/frontend/src/js/entity-history/timeline/SmallHeading.tsx index 51d5a8a06f..2a1998195a 100644 --- a/frontend/src/js/entity-history/timeline/SmallHeading.tsx +++ b/frontend/src/js/entity-history/timeline/SmallHeading.tsx @@ -1,10 +1,15 @@ -import styled from "@emotion/styled"; +import type { ComponentProps } from "react"; +import { tv } from "tailwind-variants"; import { Heading4 } from "../../headings/Headings"; -export const SmallHeading = styled(Heading4)` - flex-shrink: 0; - margin: 0; - color: ${({ theme }) => theme.col.black}; - font-size: ${({ theme }) => theme.font.md}; -`; +const smallHeading = tv({ + base: ["shrink-0", "text-gray-800", "text-base"], +}); + +export const SmallHeading = ({ + className, + ...props +}: ComponentProps) => ( + +); diff --git a/frontend/src/js/entity-history/timeline/TimelineEmptyPlaceholder.tsx b/frontend/src/js/entity-history/timeline/TimelineEmptyPlaceholder.tsx index 133803f8e9..35c5a61409 100644 --- a/frontend/src/js/entity-history/timeline/TimelineEmptyPlaceholder.tsx +++ b/frontend/src/js/entity-history/timeline/TimelineEmptyPlaceholder.tsx @@ -1,51 +1,29 @@ -import styled from "@emotion/styled"; import { faListUl, faMagnifyingGlass } from "@fortawesome/free-solid-svg-icons"; import { useMemo } from "react"; import { useTranslation } from "react-i18next"; import { useSelector } from "react-redux"; +import { tv } from "tailwind-variants"; import type { StateT } from "../../app/reducers"; import FaIcon from "../../icon/FaIcon"; import type { EntityHistoryStateT } from "../reducer"; -const Root = styled("div")` - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: 20px; - width: 100%; - font-weight: 400; - color: ${({ theme }) => theme.col.gray}; -`; +const root = tv({ + base: [ + "flex flex-col items-center justify-center", + "p-5", + "w-full", + "font-normal", + "text-gray-500", + ], +}); -const Message = styled("p")` - color: ${({ theme }) => theme.col.black}; - font-size: ${({ theme }) => theme.font.lg}; - margin: 10px 0 0; - font-weight: 400; -`; +const message = tv({ + base: ["mt-[10px]", "text-xl", "font-normal", "text-gray-800"], +}); -const BigIcon = styled(FaIcon)` - font-size: 120px; - color: ${({ theme }) => theme.col.grayLight}; -`; - -const Headline = styled("h2")` - margin: 0; - font-size: ${({ theme }) => theme.font.huge}; - line-height: 1.3; -`; - -const Row = styled("div")` - display: flex; - gap: 30px; - align-items: center; -`; - -const Description = styled("p")` - font-size: ${({ theme }) => theme.font.lg}; - margin: 0; -`; +const bigIcon = tv({ + base: ["text-[120px]", "text-gray-100"], +}); export const TimelineEmptyPlaceholder = ({ className, @@ -63,7 +41,8 @@ export const TimelineEmptyPlaceholder = ({ (state) => state.entityHistory.currentEntityId, ); - const message = useMemo(() => { + // named messageHtml: the tv const `message` would be shadowed otherwise + const messageHtml = useMemo(() => { if (searchTerm) { return t("history.emptyTimeline.descriptionWithSearchTerm", { searchTerm, @@ -78,16 +57,24 @@ export const TimelineEmptyPlaceholder = ({ }, [ids, id, t, searchTerm]); return ( - - - +
+
+
- {t("history.emptyTimeline.headline")} - {t("history.emptyTimeline.description")} - {/* biome-ignore lint/security/noDangerouslySetInnerHtml: message is our own i18n text */} - +

+ {t("history.emptyTimeline.headline")} +

+

{t("history.emptyTimeline.description")}

+

- - +
+
); }; diff --git a/frontend/src/js/entity-history/timeline/TinyLabel.ts b/frontend/src/js/entity-history/timeline/TinyLabel.ts deleted file mode 100644 index b538a69687..0000000000 --- a/frontend/src/js/entity-history/timeline/TinyLabel.ts +++ /dev/null @@ -1,11 +0,0 @@ -import styled from "@emotion/styled"; - -export const TinyLabel = styled("p")` - margin: 5px 0 0; - font-size: ${({ theme }) => theme.font.tiny}; - font-weight: 400; - color: ${({ theme }) => theme.col.gray}; - text-transform: uppercase; - white-space: nowrap; - line-height: 1; -`; diff --git a/frontend/src/js/entity-history/timeline/TinyLabel.tsx b/frontend/src/js/entity-history/timeline/TinyLabel.tsx new file mode 100644 index 0000000000..a337d6ec4f --- /dev/null +++ b/frontend/src/js/entity-history/timeline/TinyLabel.tsx @@ -0,0 +1,18 @@ +import type { ComponentProps } from "react"; +import { tv } from "tailwind-variants"; + +const tinyLabel = tv({ + base: [ + "mt-[5px]", + "text-[11px]", + "font-normal", + "text-gray-500", + "uppercase", + "whitespace-nowrap", + "leading-none", + ], +}); + +export const TinyLabel = ({ className, ...props }: ComponentProps<"p">) => ( +

+); diff --git a/frontend/src/js/entity-history/timeline/Year.tsx b/frontend/src/js/entity-history/timeline/Year.tsx index d107175400..b2b132afe7 100644 --- a/frontend/src/js/entity-history/timeline/Year.tsx +++ b/frontend/src/js/entity-history/timeline/Year.tsx @@ -1,4 +1,3 @@ -import styled from "@emotion/styled"; import { memo } from "react"; import type { @@ -15,12 +14,6 @@ import type { EventsByQuarterWithGroups } from "./util/findEventGroups"; import type { ColumnBuckets } from "./util/useColumnInformation"; import YearHead from "./YearHead"; -const YearGroup = styled("div")` - display: flex; - flex-direction: column; - border-radius: ${({ theme }) => theme.borderRadius}; -`; - const Year = ({ year, getIsOpen, @@ -70,7 +63,7 @@ const Year = ({ onClick={() => toggleOpenYear(year)} timeStratifiedInfos={timeStratifiedInfos} /> - +

{quarterwiseData.map(({ quarter, groupedEvents, differences }) => { const totalEventsPerQuarter = groupedEvents.reduce( (s, evts) => s + evts.length, @@ -99,7 +92,7 @@ const Year = ({ /> ); })} - +
); }; diff --git a/frontend/src/js/entity-history/timeline/YearHead.tsx b/frontend/src/js/entity-history/timeline/YearHead.tsx index 7b0817d89f..2e940cabef 100644 --- a/frontend/src/js/entity-history/timeline/YearHead.tsx +++ b/frontend/src/js/entity-history/timeline/YearHead.tsx @@ -1,7 +1,7 @@ -import styled from "@emotion/styled"; import { faCaretDown, faCaretRight } from "@fortawesome/free-solid-svg-icons"; import { Fragment, memo } from "react"; import { useTranslation } from "react-i18next"; +import { tv } from "tailwind-variants"; import type { ColumnDescriptionSemanticConceptColumn, @@ -16,61 +16,43 @@ import { ConceptBubble } from "../ConceptBubble"; import { SmallHeading } from "./SmallHeading"; import { formatCurrency, isConceptColumn, isMoneyColumn } from "./util/util"; -const Root = styled("div")` - font-size: ${({ theme }) => theme.font.xs}; - padding: 0 10px 0 0; -`; -const StickyWrap = styled("div")` - position: sticky; - top: 0; - left: 0; - padding: 6px 10px; - cursor: pointer; - display: grid; - grid-template-columns: 16px 1fr; - gap: 8px 0; - border-radius: ${({ theme }) => theme.borderRadius}; - border: 1px solid transparent; - &:hover { - border: 1px solid ${({ theme }) => theme.col.blueGray}; - } -`; - -const Col = styled("div")` - display: flex; - flex-direction: column; - gap: 6px; -`; - -const Grid = styled("div")` - display: grid; - grid-template-columns: auto minmax(min-content, 25px); - gap: 0px 10px; -`; - -const ConceptRow = styled("div")` - grid-column: span 2; - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 4px; -`; - -const Value = styled("div")` - font-size: ${({ theme }) => theme.font.sm}; - font-weight: 400; - justify-self: end; - width: 100%; - text-align: right; -`; - -const Label = styled("div")` - font-size: ${({ theme }) => theme.font.sm}; - max-width: 100%; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -`; +const stickyWrap = tv({ + base: [ + "sticky top-0 left-0", + "px-[10px] py-[6px]", + "cursor-pointer", + "grid grid-cols-[16px_1fr]", + "gap-x-0 gap-y-2", + "rounded", + "border border-transparent hover:border-primary-200", + ], +}); + +const infoGrid = tv({ + base: [ + "grid grid-cols-[auto_minmax(min-content,25px)]", + "gap-x-[10px] gap-y-0", + ], +}); + +const conceptRow = tv({ + base: ["col-span-2", "flex flex-wrap items-center", "gap-1"], +}); + +// named valueCell: `value` is shadowed by destructured data entries below +const valueCell = tv({ + base: ["text-sm", "font-normal", "justify-self-end", "w-full", "text-right"], +}); + +const labelText = tv({ + base: [ + "text-sm", + "max-w-full", + "whitespace-nowrap", + "overflow-hidden", + "text-ellipsis", + ], +}); type YearValue = TimeStratifiedInfo["years"][number]["values"][string]; type Column = TimeStratifiedInfo["columns"][number]; @@ -117,14 +99,16 @@ const ConceptValues = ({ return ( <> - - +
+ {label} +
+
{concepts.map((concept) => ( {concept.label} ))} - +
); }; @@ -153,10 +137,10 @@ const TimeStratifiedInfos = ({ ); return ( - +
{infos.map(({ info, yearInfo }) => { return ( - +
{Object.entries(yearInfo.values) .sort( ([l1], [l2]) => @@ -186,17 +170,17 @@ const TimeStratifiedInfos = ({ return ( - - +
{label}
+
{valueFormatted} - +
); })} - +
); })} - +
); }; @@ -216,8 +200,10 @@ const YearHead = ({ const { t } = useTranslation(); return ( - - +
+ {/* biome-ignore lint/a11y/noStaticElementInteractions: TODO make this a button */} + {/* biome-ignore lint/a11y/useKeyWithClickEvents: TODO make this a button */} +
{year} @@ -230,8 +216,8 @@ const YearHead = ({ year={year} timeStratifiedInfos={timeStratifiedInfos} /> - - +
+
); };