diff --git a/frontend/.storybook/preview.tsx b/frontend/.storybook/preview.tsx index 673e4c3839..ff3d740139 100644 --- a/frontend/.storybook/preview.tsx +++ b/frontend/.storybook/preview.tsx @@ -3,7 +3,6 @@ import type { Decorator, Preview } from "@storybook/react"; import { theme } from "../src/app-theme"; import DndProvider from "../src/js/app/DndProvider"; -import GlobalStyles from "../src/js/GlobalStyles"; import i18next from "../src/js/localization/i18next"; import translationsDe from "../src/localization/de.json"; @@ -13,7 +12,6 @@ i18next.changeLanguage("de"); const withProviders: Decorator = (Story) => ( - diff --git a/frontend/package.json b/frontend/package.json index 3852b1f317..1c16efbb65 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -72,7 +72,7 @@ "remark-flexible-markers": "^1.3.6", "remark-gfm": "^4.0.1", "resize-observer-polyfill": "^1.5.1", - "tailwind-styled-components": "^2.2.0", + "tailwind-variants": "^3.3.1", "tippy.js": "^6.3.7", "typesafe-actions": "^5.1.0" }, diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index b13deeed3a..4521eed863 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -158,9 +158,9 @@ importers: resize-observer-polyfill: specifier: ^1.5.1 version: 1.5.1 - tailwind-styled-components: - specifier: ^2.2.0 - version: 2.2.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + tailwind-variants: + specifier: ^3.3.1 + version: 3.3.1(tailwind-merge@3.6.0)(tailwindcss@4.3.3) tippy.js: specifier: ^6.3.7 version: 6.3.7 @@ -3256,14 +3256,20 @@ packages: tabbable@6.5.0: resolution: {integrity: sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA==} - tailwind-merge@1.14.0: - resolution: {integrity: sha512-3mFKyCo/MBcgyOTlrY8T7odzZFx+w+qKSMAmdFzRvqBfLlSigU6TZnlFHK0lkMwj9Bj8OYU+9yW9lmGuS0QEnQ==} + tailwind-merge@3.6.0: + resolution: {integrity: sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==} - tailwind-styled-components@2.2.0: - resolution: {integrity: sha512-Ogemwk0p69aU8WE/ooJZHjqstdJgT5R6HGU6TFz2uSnveSEtvW+C6aWOjGCvCr5H/bREv0IbbQ4yODknRrLBRQ==} + tailwind-variants@3.3.1: + resolution: {integrity: sha512-4pAvwUtM4HKBiRZftncAbpn6V9Hhwoa5Fl7O2u5zbp7Z5Cvu+/o/6+176WY3WCEES209543quG8zFIcXCsc5Jw==} + engines: {node: '>=16.9.x', pnpm: '>=7.x'} peerDependencies: - react: '>= 16.8.0' - react-dom: '>= 16.8.0' + tailwind-merge: '>=3.0.0' + tailwindcss: '*' + peerDependenciesMeta: + tailwind-merge: + optional: true + tailwindcss: + optional: true tailwindcss@4.3.3: resolution: {integrity: sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==} @@ -6562,13 +6568,13 @@ snapshots: tabbable@6.5.0: {} - tailwind-merge@1.14.0: {} + tailwind-merge@3.6.0: + optional: true - tailwind-styled-components@2.2.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8): - dependencies: - react: 19.2.8 - react-dom: 19.2.8(react@19.2.8) - tailwind-merge: 1.14.0 + tailwind-variants@3.3.1(tailwind-merge@3.6.0)(tailwindcss@4.3.3): + optionalDependencies: + tailwind-merge: 3.6.0 + tailwindcss: 4.3.3 tailwindcss@4.3.3: {} diff --git a/frontend/src/index.css b/frontend/src/index.css index fd23df48cd..2697573042 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -14,6 +14,109 @@ --color-bg-50: #fafafa; --color-bg-100: #f4f6f5; + + --color-red: #b22125; + --color-green: #36971c; + --color-orange: #e9711c; + + --color-filetype-csv: #007bff; + --color-filetype-pdf: #d73a49; + --color-filetype-zip: #6f42c1; + --color-filetype-xlsx: #28a745; + --color-filetype-json: #1f5f30; + + /* `text-tiny`; the other sizes map onto tailwind defaults: + xs 12px, sm 14px, base 16px, xl 20px, 2xl 24px */ + --text-tiny: 11px; + + /* bare `rounded` */ + --radius: 3px; +} + +@layer base { + body { + font-family: "Roboto", "Arial", sans-serif; + font-weight: 300; + background: var(--color-bg-50); + color: var(--color-gray-800); + min-height: 100vh; + height: 100%; + overflow: hidden; + } + + p { + line-height: 1.5; + } + + button, + select, + input, + textarea { + font-family: "Roboto", "Arial", sans-serif; + } + + a, + a:visited, + a:active { + color: var(--color-gray-800); + text-decoration: none; + } + + button { + outline: none; + cursor: pointer; + } + button::-moz-focus-inner { + padding: 0; + border: 0; + } + + input::-ms-clear { + display: none; + } + + select { + outline: 0; + border-radius: var(--radius); + padding: 8px 30px 8px 8px; + border: 1px solid var(--color-gray-500); + appearance: none; + font-size: var(--text-sm); + cursor: pointer; + &:disabled { + cursor: not-allowed; + } + &:hover { + background-color: var(--color-gray-50); + } + } + + h3 { + font-size: var(--text-sm); + margin: 0 0 10px 0; + color: var(--color-primary-500); + line-height: 25px; + } +} + +/* @tippyjs/react sets inline styles, hence the specificity */ +div[data-tippy-root] { + max-width: 700px; + box-shadow: 0 0 8px rgba(0, 0, 0, 0.18); + border-radius: var(--radius); + + > div { + box-shadow: none; + /* biome-ignore lint/complexity/noImportantStyles: beats the inline style tippy sets */ + max-width: inherit !important; + width: 100%; + padding: 0; + } + + .tippy-content { + padding: 0px; + box-shadow: none; + } } /* diff --git a/frontend/src/js/GlobalStyles.tsx b/frontend/src/js/GlobalStyles.tsx deleted file mode 100644 index 1d566709a5..0000000000 --- a/frontend/src/js/GlobalStyles.tsx +++ /dev/null @@ -1,151 +0,0 @@ -import type { Theme } from "@emotion/react"; -import { css, Global, useTheme } from "@emotion/react"; - -import { tippyjsReactOverrides } from "./tooltip/WithTooltip"; - -const globalStyles = (theme: Theme) => css` - * { - box-sizing: border-box; - } - - p { - line-height: 1.5; - } - - body { - font-family: "Roboto", "Arial", sans-serif; - font-weight: 300; - background: ${theme.col.bg}; - margin: 0; - color: ${theme.col.black}; - min-height: 100vh; - height: 100%; - overflow: hidden; - } - - button, - select, - input, - textarea { - font-family: "Roboto", "Arial", sans-serif; - } - - a, - a:visited, - a:active { - color: ${theme.col.black}; - text-decoration: none; - } - - button { - outline: none; - cursor: pointer; - } - button::-moz-focus-inner { - padding: 0; - border: 0; - } - - input::-ms-clear { - display: none; - } - - select { - outline: 0; - border-radius: 3px; - padding: 8px 30px 8px 8px; - border: 1px solid ${theme.col.gray}; - appearance: none; - font-size: ${theme.font.sm}; - cursor: pointer; - &:disabled { - cursor: not-allowed; - } - &:hover { - background-color: ${theme.col.grayVeryLight}; - } - } - - h3 { - font-size: ${theme.font.sm}; - margin: 0 0 10px 0; - color: ${theme.col.blueGrayDark}; - line-height: 25px; - } -`; - -const splitPaneStyles = (theme: Theme) => css` - .SplitPane { - .Pane1 { - overflow: hidden; - } - - .Pane2 { - overflow: hidden; - } - } - .SplitPane--tooltip-fixed { - > .Pane1 { - width: 30px !important; // Because SplitPane sets an element style after the first drag - } - } - - .Resizer { - background: ${theme.col.grayMediumLight}; - opacity: 0.8; - z-index: 1; - transition: all 0.5s ease-in-out; - box-sizing: border-box; - -moz-background-clip: padding; - -webkit-background-clip: padding; - background-clip: padding-box; - - &.vertical { - width: 11px; - margin: 0 -5px; - border-left: 5px solid rgba(0, 0, 0, 0); - border-right: 5px solid rgba(0, 0, 0, 0); - cursor: col-resize; - - &:hover { - &:not(.disabled) { - border-left: 5px solid rgba(0, 0, 0, 0.1); - border-right: 5px solid rgba(0, 0, 0, 0.1); - } - } - } - - &.horizontal { - height: 11px; - margin: -5px 0; - border-top: 5px solid rgba(0, 0, 0, 0); - border-bottom: 5px solid rgba(0, 0, 0, 0); - cursor: row-resize; - - &:hover { - &:not(.disabled) { - border-top: 5px solid rgba(0, 0, 0, 0.1); - border-bottom: 5px solid rgba(0, 0, 0, 0.1); - } - } - } - - &.disabled { - cursor: not-allowed; - } - } -`; - -const GlobalStyles = () => { - const theme = useTheme(); - - return ( - <> - - - - - ); -}; - -export default GlobalStyles; diff --git a/frontend/src/js/concept-trees/ConceptTreeList.tsx b/frontend/src/js/concept-trees/ConceptTreeList.tsx index c5581f37a0..0388815bea 100644 --- a/frontend/src/js/concept-trees/ConceptTreeList.tsx +++ b/frontend/src/js/concept-trees/ConceptTreeList.tsx @@ -1,6 +1,6 @@ import { useMemo } from "react"; import { useSelector } from "react-redux"; -import tw from "tailwind-styled-components"; +import { tv } from "tailwind-variants"; import type { DatasetT } from "../api/types"; import type { StateT } from "../app/reducers"; import { useLoadTree } from "./actions"; @@ -12,21 +12,22 @@ import type { SearchT, TreesT } from "./reducer"; import { useAreTreesAvailable } from "./selectors"; import { useRootConceptIds } from "./useRootConceptIds"; -/** - @param show For historic reasons, it was necessary to only hide / show the concept tree list, - instead of mounting / unmounting it. Maybe we can remove this in the future. -*/ -const Root = tw("div")<{ $show?: boolean }>` - grow - shrink-0 - basis-0 - px-[10px] - overflow-y-auto - whitespace-nowrap - mb-[10px] - - ${({ $show }) => ($show ? "block" : "hidden")} -`; +// For historic reasons, the concept tree list is only hidden / shown instead of +// mounted / unmounted. Maybe we can remove this in the future. +const root = tv({ + base: [ + "grow shrink-0 basis-0", + "mb-[10px]", + "px-[10px]", + "overflow-y-auto", + "whitespace-nowrap", + ], + variants: { + show: { + false: "hidden", + }, + }, +}); const ConceptTreeList = ({ datasetId, @@ -68,7 +69,7 @@ const ConceptTreeList = ({ if (search.loading) return null; return ( - +
{loading && } {!loading && !areTreesAvailable && !areDatasetsPristineOrLoading && ( @@ -84,7 +85,7 @@ const ConceptTreeList = ({ conceptId={conceptId} /> ))} - +
); }; diff --git a/frontend/src/js/external-forms/form/fields/DisclosureListField.tsx b/frontend/src/js/external-forms/form/fields/DisclosureListField.tsx index a7eeaa4b10..5d3e4f19a1 100644 --- a/frontend/src/js/external-forms/form/fields/DisclosureListField.tsx +++ b/frontend/src/js/external-forms/form/fields/DisclosureListField.tsx @@ -7,7 +7,7 @@ import { import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { type ComponentProps, useEffect, useState } from "react"; import { useFieldArray } from "react-hook-form"; -import tw from "tailwind-styled-components"; +import { tv } from "tailwind-variants"; import IconButton from "../../../button/IconButton"; import { TransparentButton } from "../../../button/TransparentButton"; import { exists } from "../../../common/helpers/exists"; @@ -22,20 +22,18 @@ import { } from "../../helper"; import Field from "../Field"; -const Summary = tw("summary")` - relative - cursor-pointer - flex - items-center - justify-between - gap-3 - py-3 - pl-3 - pr-10 - bg-white - text-sm - font-normal -`; +const summary = tv({ + base: [ + "relative", + "flex items-center justify-between", + "gap-3", + "cursor-pointer", + "bg-white", + "py-3 pr-10 pl-3", + "text-sm", + "font-normal", + ], +}); const DisclosureField = ({ field, @@ -74,7 +72,7 @@ const DisclosureField = ({ toggleOpen(); }} > - +
@@ -91,7 +89,7 @@ const DisclosureField = ({ onClick={() => remove(index)} /> )} -
+
{field.fields.map((f, i) => { const key = getFieldKey(formType, f, i); diff --git a/frontend/src/js/headings/Headings.tsx b/frontend/src/js/headings/Headings.tsx index 834f63e5a6..018e5d315d 100644 --- a/frontend/src/js/headings/Headings.tsx +++ b/frontend/src/js/headings/Headings.tsx @@ -1,5 +1,13 @@ -import tw from "tailwind-styled-components"; +import type { ComponentProps } from "react"; +import { tv } from "tailwind-variants"; -export const Heading3 = tw("h3")`text-lg font-normal text-gray-800`; +const heading3 = tv({ base: "text-lg font-normal text-gray-800" }); +const heading4 = tv({ base: "text-sm font-normal text-gray-500 uppercase" }); -export const Heading4 = tw("h4")`text-sm font-normal text-gray-500 uppercase`; +export const Heading3 = ({ className, ...props }: ComponentProps<"h3">) => ( +

+); + +export const Heading4 = ({ className, ...props }: ComponentProps<"h4">) => ( +

+); diff --git a/frontend/src/js/index.tsx b/frontend/src/js/index.tsx index 35091c3764..170336c327 100644 --- a/frontend/src/js/index.tsx +++ b/frontend/src/js/index.tsx @@ -7,7 +7,6 @@ import "../index.css"; import AppRoot from "./AppRoot"; import type { StateT } from "./app/reducers"; -import GlobalStyles from "./GlobalStyles"; import { makeStore } from "./store"; // TODO: OG image required? @@ -23,7 +22,6 @@ const renderRoot = (theme: Theme) => { return root.render( - , ); diff --git a/frontend/src/js/pane/TabNavigation.tsx b/frontend/src/js/pane/TabNavigation.tsx index d0b153cd51..c2ac05ac21 100644 --- a/frontend/src/js/pane/TabNavigation.tsx +++ b/frontend/src/js/pane/TabNavigation.tsx @@ -1,7 +1,7 @@ import styled from "@emotion/styled"; import { faSpinner } from "@fortawesome/free-solid-svg-icons"; -import tw from "tailwind-styled-components"; +import { tv } from "tailwind-variants"; import FaIcon from "../icon/FaIcon"; import { HoverNavigatable } from "../small-tab-navigation/HoverNavigatable"; import WithTooltip from "../tooltip/WithTooltip"; @@ -14,29 +14,28 @@ const Root = styled("div")` align-items: flex-start; `; -const Headline = tw("h2")<{ $active: boolean }>` - text-sm - mb-0 - mt-[6px] - mr-[5px] - px-3 - font-bold - leading-[30px] - uppercase - shrink-0 - transition-colors - cursor-pointer - tracking-wider - - border-b-[3px] - ${({ $active }) => - $active ? "text-primary-500" : "text-gray-500 hover:text-black"}; - ${({ $active }) => - $active - ? "border-primary-500" - : "border-transparent hover:border-primary-200"}; - -`; +const headline = tv({ + base: [ + "mt-[6px] mr-[5px] mb-0", + "px-3", + "shrink-0", + "cursor-pointer", + "border-b-[3px]", + "transition-colors", + "text-sm", + "leading-[30px]", + "font-bold", + "uppercase", + "tracking-wider", + ], + variants: { + active: { + true: "border-primary-500 text-primary-500", + false: + "border-transparent text-gray-500 hover:border-primary-200 hover:text-black", + }, + }, +}); const SxWithTooltip = styled(WithTooltip)` flex-shrink: 0; @@ -78,13 +77,14 @@ const TabNavigation = ({ return ( - {label} {loading && } - + ); diff --git a/frontend/src/js/previous-queries/upload/CSVColumnPicker.tsx b/frontend/src/js/previous-queries/upload/CSVColumnPicker.tsx index c56ff41e63..50550167e6 100644 --- a/frontend/src/js/previous-queries/upload/CSVColumnPicker.tsx +++ b/frontend/src/js/previous-queries/upload/CSVColumnPicker.tsx @@ -12,8 +12,8 @@ import { saveAs } from "file-saver"; import type { TFunction } from "i18next"; import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; +import { tv } from "tailwind-variants"; -import tw from "tailwind-styled-components"; import type { QueryUploadConfigT, UploadQueryResponseT } from "../../api/types"; import IconButton from "../../button/IconButton"; import PrimaryButton from "../../button/PrimaryButton"; @@ -32,13 +32,9 @@ const Row = styled("div")` margin-bottom: 15px; `; -const Td = tw("td")` - text-xs - text-ellipsis - overflow-hidden - whitespace-nowrap - min-w-[150px] -`; +const td = tv({ + base: "min-w-[150px] overflow-hidden text-ellipsis whitespace-nowrap text-xs", +}); const Th = styled("th")` font-size: ${({ theme }) => theme.font.xs}; vertical-align: top; @@ -230,18 +226,18 @@ const CSVPreviewTable = ({ csv.slice(1, 6).map((row, j) => ( {row.map((cell, i) => ( - + {cell} - + ))} ))} {csv.length > 6 && ( {new Array(csv[0].length).fill(null).map((_, j) => ( - + ... - + ))} )} diff --git a/frontend/src/js/snack-message/SnackMessage.tsx b/frontend/src/js/snack-message/SnackMessage.tsx index a7adbac842..9a1d1293d4 100644 --- a/frontend/src/js/snack-message/SnackMessage.tsx +++ b/frontend/src/js/snack-message/SnackMessage.tsx @@ -1,31 +1,33 @@ import { faTimes } from "@fortawesome/free-solid-svg-icons"; import { memo, useRef } from "react"; import { useDispatch, useSelector } from "react-redux"; -import tw from "tailwind-styled-components"; +import { tv } from "tailwind-variants"; import type { StateT } from "../app/reducers"; import { useClickOutside } from "../common/helpers/useClickOutside"; import FaIcon from "../icon/FaIcon"; import { resetMessage as resetMessageAction } from "./actions"; import type { SnackMessageStateT } from "./reducer"; -const Root = tw("div")<{ $success?: boolean }>` - fixed - z-10 - bottom-5 - right-5 - text-white - flex items-start - max-w-[500px] - rounded-lg - ${({ $success }) => ($success ? "bg-primary-500/90" : "bg-black/75")} -`; +const root = tv({ + base: [ + "fixed right-5 bottom-5", + "z-10", + "flex items-start", + "max-w-[500px]", + "rounded-lg", + "text-white", + ], + variants: { + success: { + true: "bg-primary-500/90", + false: "bg-black/75", + }, + }, +}); -const ClearZone = tw("div")` - absolute top-3 right-4 - z-[11] - cursor-pointer - opacity-80 hover:opacity-100 -`; +const clearZone = tv({ + base: ["absolute top-3 right-4", "z-11", "opacity-80 hover:opacity-100"], +}); export const SnackMessage = memo(function SnackMessageComponent() { const ref = useRef(null); @@ -44,15 +46,19 @@ export const SnackMessage = memo(function SnackMessageComponent() { return (
{message && ( - +
{/* biome-ignore lint/security/noDangerouslySetInnerHtml: messages are our own i18n text */}
- +
- +
)}
); diff --git a/frontend/src/js/tooltip/TooltipHeader.tsx b/frontend/src/js/tooltip/TooltipHeader.tsx index a6a9efefd5..9b0f680ba5 100644 --- a/frontend/src/js/tooltip/TooltipHeader.tsx +++ b/frontend/src/js/tooltip/TooltipHeader.tsx @@ -3,24 +3,25 @@ import { faAngleLeft } from "@fortawesome/free-solid-svg-icons"; import { memo } from "react"; import { useTranslation } from "react-i18next"; import { useDispatch } from "react-redux"; -import tw from "tailwind-styled-components"; +import { tv } from "tailwind-variants"; import IconButton from "../button/IconButton"; import { toggleDisplayTooltip } from "./actions"; -const Header = tw("h2")` - bg-white - h-[40px] - shrink-0 - flex items-center - px-5 - pt-1 - text-sm - tracking-[1px] - uppercase - text-primary-500 - border-b border-gray-100 - font-bold -`; +const header = tv({ + base: [ + "flex items-center", + "h-[40px]", + "shrink-0", + "border-b border-gray-100", + "bg-white", + "px-5 pt-1", + "text-sm", + "font-bold", + "uppercase", + "tracking-[1px]", + "text-primary-500", + ], +}); const StyledIconButton = styled(IconButton)` position: absolute; @@ -43,7 +44,7 @@ export const TooltipHeader = memo(() => { onClick={onToggleDisplayTooltip} icon={faAngleLeft} /> -
{t("tooltip.headline")}
+

{t("tooltip.headline")}

); }); diff --git a/frontend/src/js/tooltip/WithTooltip.tsx b/frontend/src/js/tooltip/WithTooltip.tsx index 010462290b..741aacd9ea 100644 --- a/frontend/src/js/tooltip/WithTooltip.tsx +++ b/frontend/src/js/tooltip/WithTooltip.tsx @@ -1,31 +1,10 @@ -import { css, useTheme } from "@emotion/react"; +import { useTheme } from "@emotion/react"; import styled from "@emotion/styled"; import Tippy, { type TippyProps } from "@tippyjs/react"; import { memo, type ReactElement, type Ref, useMemo } from "react"; import "tippy.js/dist/tippy.css"; import "tippy.js/themes/light.css"; -/* !important: to override inline styles by tippyjs/react */ -export const tippyjsReactOverrides = css` - div[data-tippy-root] { - max-width: 700px; - box-shadow: 0 0 8px rgba(0, 0, 0, 0.18); - border-radius: 3px; - - > div { - box-shadow: none; - max-width: inherit !important; - width: 100%; - padding: 0; - } - - .tippy-content { - padding: 0px; - box-shadow: none; - } - } -`; - const Text = styled("div")<{ wide?: boolean }>` max-width: ${({ wide }) => (wide ? "700px" : "400px")}; text-align: left;