From e123bcf50bc61fd112c1b25e2f49b7244df46f7e Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Thu, 27 Aug 2026 16:54:11 +0200 Subject: [PATCH 1/3] refactor(frontend): button and icon components on tailwind-variants first styled() slice; shared tv in src/js/tv.ts so the class merger knows text-tiny is a font size, not a colour --- frontend/src/index.css | 2 + frontend/src/js/button/BadgeToggleButton.tsx | 79 ++++---- frontend/src/js/button/BasicButton.tsx | 73 +++++--- frontend/src/js/button/DestroyButton.tsx | 28 +-- frontend/src/js/button/DownloadButton.tsx | 46 ++--- frontend/src/js/button/IconButton.tsx | 174 +++++++----------- frontend/src/js/button/PreviewButton.tsx | 20 +- frontend/src/js/button/PrimaryButton.tsx | 31 ++-- .../js/button/QueryResultHistoryButton.tsx | 11 +- frontend/src/js/button/SelectFileButton.tsx | 37 ++-- frontend/src/js/button/TransparentButton.tsx | 37 ++-- .../src/js/concept-trees/ConceptTreeList.tsx | 2 +- frontend/src/js/editor-v2/TreeNode.tsx | 4 +- .../editor-v2/date-restriction/DateModal.tsx | 4 +- .../form/fields/DisclosureListField.tsx | 2 +- frontend/src/js/headings/Headings.tsx | 2 +- frontend/src/js/icon/FaIcon.tsx | 111 ++++++----- frontend/src/js/pane/TabNavigation.tsx | 3 +- .../upload/CSVColumnPicker.tsx | 3 +- .../src/js/snack-message/SnackMessage.tsx | 2 +- frontend/src/js/tooltip/TooltipHeader.tsx | 2 +- frontend/src/js/tv.ts | 16 ++ .../ui-components/DropzoneWithFileInput.tsx | 4 +- .../src/js/ui-components/InputDateRange.tsx | 4 +- .../DropdownOption.tsx | 4 +- 25 files changed, 356 insertions(+), 345 deletions(-) create mode 100644 frontend/src/js/tv.ts diff --git a/frontend/src/index.css b/frontend/src/index.css index 2697573042..d89331cd9e 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -31,6 +31,8 @@ /* bare `rounded` */ --radius: 3px; + + --animate-spin-fast: spin 0.5s linear infinite; } @layer base { diff --git a/frontend/src/js/button/BadgeToggleButton.tsx b/frontend/src/js/button/BadgeToggleButton.tsx index c8823e480e..ac70272235 100644 --- a/frontend/src/js/button/BadgeToggleButton.tsx +++ b/frontend/src/js/button/BadgeToggleButton.tsx @@ -1,47 +1,37 @@ -import { css } from "@emotion/react"; -import styled from "@emotion/styled"; import type { ReactNode } from "react"; import { useHotkeys } from "react-hotkeys-hook"; -import BasicButton from "./BasicButton"; - -const common = css` - font-weight: 700; - padding: 1px 4px; - white-space: nowrap; -`; - -const ActiveButton = styled(BasicButton)` - border-radius: ${({ theme }) => theme.borderRadius}; - border: 2px solid ${({ theme }) => theme.col.blueGrayDark}; - background-color: white; - font-size: ${({ theme }) => theme.font.sm}; - color: ${({ theme }) => theme.col.blueGrayDark}; - ${common}; +import { tv } from "../tv"; - &:hover { - background-color: ${({ theme }) => theme.col.grayVeryLight}; - } -`; - -const InactiveButton = styled(BasicButton)` - border-radius: ${({ theme }) => theme.borderRadius}; - border: 2px dotted ${({ theme }) => theme.col.grayLight}; - font-size: ${({ theme }) => theme.font.sm}; - color: ${({ theme }) => theme.col.gray}; - ${common}; - &:hover { - background-color: ${({ theme }) => theme.col.bg}; - } -`; +import BasicButton from "./BasicButton"; -const SuperScript = styled("span")` - padding-left: 3px; - font-size: ${({ theme }) => theme.font.tiny}; - transform: translate(1px, -2px); - display: inline-block; - color: ${({ theme }) => theme.col.gray}; -`; +const badgeToggleButton = tv({ + base: ["rounded", "px-1 py-px", "text-sm", "font-bold", "whitespace-nowrap"], + variants: { + active: { + true: [ + "border-2 border-primary-500", + "bg-white hover:bg-gray-50", + "text-primary-500", + ], + false: [ + "border-2 border-dotted border-gray-100", + "hover:bg-bg-50", + "text-gray-500", + ], + }, + }, +}); + +const superScript = tv({ + base: [ + "inline-block", + "pl-[3px]", + "translate-x-px -translate-y-[2px]", + "text-tiny", + "text-gray-500", + ], +}); interface Props { className?: string; @@ -58,15 +48,16 @@ export const BadgeToggleButton = ({ children, hotkey, }: Props) => { - const Component = active ? ActiveButton : InactiveButton; - useHotkeys(hotkey || "", onClick, { enabled: !!hotkey }, [hotkey, onClick]); return ( - + {!active && "+ "} {children} - {hotkey && {hotkey}} - + {hotkey && {hotkey}} + ); }; diff --git a/frontend/src/js/button/BasicButton.tsx b/frontend/src/js/button/BasicButton.tsx index 2a6ae5558c..b8d5d77aaa 100644 --- a/frontend/src/js/button/BasicButton.tsx +++ b/frontend/src/js/button/BasicButton.tsx @@ -1,6 +1,7 @@ -import styled from "@emotion/styled"; import type { ButtonHTMLAttributes, Ref } from "react"; +import { tv } from "../tv"; + export interface BasicButtonProps extends ButtonHTMLAttributes { bare?: boolean; @@ -9,42 +10,54 @@ export interface BasicButtonProps large?: boolean; active?: boolean; secondary?: boolean; - autoFocus?: boolean; // Should actually be within the extends, not sure why I had to declare this. } -const Button = styled("button")` - cursor: pointer; - font-weight: ${({ active, secondary }) => - active || secondary ? "700" : "400"}; - padding: ${({ small, tiny, bare, large }) => - bare - ? "0" - : tiny - ? "4px 6px" - : small - ? "6px 8px" - : large - ? "12px 18px" - : "8px 15px"}; - font-size: ${({ theme, small, tiny, large }) => - tiny || small ? theme.font.xs : large ? theme.font.lg : theme.font.sm}; - transition: all 0.2s; - border-radius: ${({ theme }) => theme.borderRadius}; - - &:disabled { - cursor: not-allowed; - opacity: 0.4; - } -`; +const button = tv({ + base: [ + "cursor-pointer", + "rounded", + "px-[15px] py-2", + "text-sm", + "font-normal", + "transition-all duration-200", + "disabled:cursor-not-allowed disabled:opacity-40", + ], + variants: { + active: { true: "font-bold" }, + secondary: { true: "font-bold" }, + // later wins when several are set + large: { true: "px-[18px] py-3 text-xl" }, + small: { true: "px-2 py-[6px] text-xs" }, + tiny: { true: "px-[6px] py-1 text-xs" }, + bare: { true: "p-0" }, + }, +}); const BasicButton = ({ ref, - children, + className, + bare, + tiny, + small, + large, + active, + secondary, ...props }: BasicButtonProps & { ref?: Ref }) => ( - + + ); }; diff --git a/frontend/src/js/button/PrimaryButton.tsx b/frontend/src/js/button/PrimaryButton.tsx index 2e4780c39e..97042799d1 100644 --- a/frontend/src/js/button/PrimaryButton.tsx +++ b/frontend/src/js/button/PrimaryButton.tsx @@ -1,14 +1,23 @@ -import styled from "@emotion/styled"; +import type { Ref } from "react"; -import BasicButton from "./BasicButton"; +import { tv } from "../tv"; -export default styled(BasicButton)` - color: white; - background-color: ${({ theme }) => theme.col.blueGrayDark}; - background-clip: padding-box; - border: 1px solid ${({ theme }) => theme.col.blueGrayDark}; +import BasicButton, { type BasicButtonProps } from "./BasicButton"; - &:hover { - opacity: 0.9; - } -`; +const primaryButton = tv({ + base: [ + "text-white", + "bg-primary-500 bg-clip-padding", + "border border-primary-500", + "hover:opacity-90", + ], +}); + +const PrimaryButton = ({ + className, + ...props +}: BasicButtonProps & { ref?: Ref }) => ( + +); + +export default PrimaryButton; diff --git a/frontend/src/js/button/QueryResultHistoryButton.tsx b/frontend/src/js/button/QueryResultHistoryButton.tsx index d3145734f4..3491c2300f 100644 --- a/frontend/src/js/button/QueryResultHistoryButton.tsx +++ b/frontend/src/js/button/QueryResultHistoryButton.tsx @@ -1,4 +1,3 @@ -import styled from "@emotion/styled"; import { faListUl, faSpinner } from "@fortawesome/free-solid-svg-icons"; import { useTranslation } from "react-i18next"; import { useDispatch, useSelector } from "react-redux"; @@ -8,11 +7,6 @@ import { useGetAuthorizedUrl } from "../authorization/useAuthorizedUrl"; import { openHistory, useNewHistorySession } from "../entity-history/actions"; import IconButton from "./IconButton"; -const SxIconButton = styled(IconButton)` - white-space: nowrap; - height: 35px; -`; - export const QueryResultHistoryButton = ({ url, label, @@ -32,7 +26,8 @@ export const QueryResultHistoryButton = ({ const newHistorySession = useNewHistorySession(); return ( - { @@ -41,6 +36,6 @@ export const QueryResultHistoryButton = ({ }} > {t("history.history")} - + ); }; diff --git a/frontend/src/js/button/SelectFileButton.tsx b/frontend/src/js/button/SelectFileButton.tsx index d31269783e..5c99eb786d 100644 --- a/frontend/src/js/button/SelectFileButton.tsx +++ b/frontend/src/js/button/SelectFileButton.tsx @@ -1,18 +1,25 @@ -import styled from "@emotion/styled"; +import type { Ref } from "react"; -import BasicButton from "./BasicButton"; +import { tv } from "../tv"; -export const SelectFileButton = styled(BasicButton)` - color: ${({ theme }) => theme.col.gray}; - background-color: transparent; - font-weight: 300; - border: none; - font-size: ${({ theme }) => theme.font.tiny}; - display: flex; - align-items: center; - gap: 5px; +import BasicButton, { type BasicButtonProps } from "./BasicButton"; - &:hover { - text-decoration: underline; - } -`; +const selectFileButton = tv({ + base: [ + "flex items-center", + "gap-[5px]", + "bg-transparent", + "border-0", + "text-tiny", + "text-gray-500", + "font-light", + "hover:underline", + ], +}); + +export const SelectFileButton = ({ + className, + ...props +}: BasicButtonProps & { ref?: Ref }) => ( + +); diff --git a/frontend/src/js/button/TransparentButton.tsx b/frontend/src/js/button/TransparentButton.tsx index 3c5632274d..bc48314d93 100644 --- a/frontend/src/js/button/TransparentButton.tsx +++ b/frontend/src/js/button/TransparentButton.tsx @@ -1,20 +1,25 @@ -import styled from "@emotion/styled"; +import type { Ref } from "react"; -import BasicButton from "./BasicButton"; +import { tv } from "../tv"; -export const TransparentButton = styled(BasicButton)<{ light?: boolean }>` - color: ${({ theme, light }) => (light ? theme.col.gray : theme.col.black)}; - background-color: transparent; - border-radius: ${({ theme }) => theme.borderRadius}; - border: 1px solid - ${({ theme, light }) => (light ? theme.col.grayLight : theme.col.gray)}; +import BasicButton, { type BasicButtonProps } from "./BasicButton"; - &:hover { - background-color: ${({ theme }) => theme.col.grayVeryLight}; - } +const transparentButton = tv({ + base: [ + "rounded", + "bg-transparent hover:bg-gray-50 focus:bg-gray-50", + "border border-gray-500 focus:border-green", + "text-gray-800", + ], + variants: { + light: { true: "border-gray-100 text-gray-500" }, + }, +}); - &:focus { - border: 1px solid ${({ theme }) => theme.col.green}; - background-color: ${({ theme }) => theme.col.grayVeryLight}; - } -`; +export const TransparentButton = ({ + className, + light, + ...props +}: BasicButtonProps & { light?: boolean; ref?: Ref }) => ( + +); diff --git a/frontend/src/js/concept-trees/ConceptTreeList.tsx b/frontend/src/js/concept-trees/ConceptTreeList.tsx index 0388815bea..903e646f38 100644 --- a/frontend/src/js/concept-trees/ConceptTreeList.tsx +++ b/frontend/src/js/concept-trees/ConceptTreeList.tsx @@ -1,8 +1,8 @@ import { useMemo } from "react"; import { useSelector } from "react-redux"; -import { tv } from "tailwind-variants"; import type { DatasetT } from "../api/types"; import type { StateT } from "../app/reducers"; +import { tv } from "../tv"; import { useLoadTree } from "./actions"; import ConceptsProgressBar from "./ConceptsProgressBar"; import ConceptTreeListItem from "./ConceptTreeListItem"; diff --git a/frontend/src/js/editor-v2/TreeNode.tsx b/frontend/src/js/editor-v2/TreeNode.tsx index 985f5504ef..cde8a0d0fe 100644 --- a/frontend/src/js/editor-v2/TreeNode.tsx +++ b/frontend/src/js/editor-v2/TreeNode.tsx @@ -5,7 +5,7 @@ import { type DOMAttributes, memo } from "react"; import { useTranslation } from "react-i18next"; import { DNDType } from "../common/constants/dndTypes"; -import { Icon } from "../icon/FaIcon"; +import FaIcon from "../icon/FaIcon"; import { nodeIsConceptQueryNode, useActiveState } from "../model/node"; import { getRootNodeLabel } from "../standard-query-editor/helper"; import type { @@ -296,7 +296,7 @@ export function TreeNode({ )} {tree.dates?.excluded && ( - + {t("editorV2.datesExcluded")} )} diff --git a/frontend/src/js/editor-v2/date-restriction/DateModal.tsx b/frontend/src/js/editor-v2/date-restriction/DateModal.tsx index 6ae1ad9f65..eb20c96827 100644 --- a/frontend/src/js/editor-v2/date-restriction/DateModal.tsx +++ b/frontend/src/js/editor-v2/date-restriction/DateModal.tsx @@ -8,7 +8,7 @@ import { useTranslation } from "react-i18next"; import type { DateRangeT } from "../../api/types"; import IconButton from "../../button/IconButton"; import type { DateStringMinMax } from "../../common/helpers/dateHelper"; -import { Icon } from "../../icon/FaIcon"; +import FaIcon from "../../icon/FaIcon"; import Modal from "../../modal/Modal"; import InputCheckbox from "../../ui-components/InputCheckbox"; import InputDateRange from "../../ui-components/InputDateRange"; @@ -101,7 +101,7 @@ export const DateModal = ({ />
- + {t("queryNodeEditor.excludeTimestamps")} ["style"]; } export interface FaIconPropsT extends IconStyleProps { @@ -28,61 +26,72 @@ export interface FaIconPropsT extends IconStyleProps { className?: string; } -const spin = keyframes` - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(360deg); - } -`; - -const shouldForwardProp = (prop: keyof FaIconPropsT) => - isPropValid(prop) || prop === "icon" || prop === "className"; +const icon = tv({ + base: [ + "w-[initial]!", + "text-sm", + "text-gray-800", + "[&.fa-spinner]:animate-spin-fast", + ], + variants: { + left: { true: "pr-[10px]" }, + right: { true: "pl-[10px]" }, + center: { true: "text-center" }, + // later wins when both are set + tiny: { true: "text-tiny" }, + large: { true: "text-base" }, + disabled: { true: "cursor-not-allowed" }, + }, +}); // First matching flag wins, in this order -const iconColor = (theme: Theme, props: IconStyleProps) => { - if (props.disabled) return theme.col.grayMediumLight; - if (props.red) return theme.col.red; - if (props.gray) return theme.col.gray; - if (props.active) return theme.col.blueGrayDark; - if (props.white) return "#fff"; - if (props.light) return theme.col.blueGrayLight; - if (props.main) return theme.col.blueGray; - return theme.col.black; +const colorClass = (p: IconStyleProps) => { + if (p.disabled) return "text-gray-400"; + if (p.red) return "text-red"; + if (p.gray) return "text-gray-500"; + if (p.active) return "text-primary-500"; + if (p.white) return "text-white"; + if (p.light) return "text-primary-100"; + if (p.main) return "text-primary-200"; + return undefined; }; -// @ts-ignore TODO: Figure out how to avoid a type error with styled here -export const Icon = styled(FontAwesomeIcon, { - shouldForwardProp, -})` - padding-right: ${({ left }) => (left ? "10px" : "0")}; - padding-left: ${({ right }) => (right ? "10px" : "0")}; - text-align: ${({ center }) => (center ? "center" : "left")}; - font-size: ${({ theme, large, tiny }) => - large ? theme.font.md : tiny ? theme.font.tiny : theme.font.sm}; - color: ${({ theme, ...props }) => iconColor(theme, props)}; - cursor: ${({ disabled }) => (disabled ? "not-allowed" : "inherit")}; - width: initial !important; - - &.fa-spinner { - animation: ${spin} 0.5s linear 0s infinite; - } -`; - const FaIcon = ({ ref, - icon, + icon: iconProp, className, - ...restProps + left, + center, + right, + white, + red, + light, + gray, + main, + active, + disabled, + tiny, + large, + small: _small, // only meaningful to IconButton + style, }: FaIconPropsT & { ref?: Ref }) => { return ( - ); }; diff --git a/frontend/src/js/pane/TabNavigation.tsx b/frontend/src/js/pane/TabNavigation.tsx index c2ac05ac21..d451d2a104 100644 --- a/frontend/src/js/pane/TabNavigation.tsx +++ b/frontend/src/js/pane/TabNavigation.tsx @@ -1,10 +1,9 @@ import styled from "@emotion/styled"; import { faSpinner } from "@fortawesome/free-solid-svg-icons"; - -import { tv } from "tailwind-variants"; import FaIcon from "../icon/FaIcon"; import { HoverNavigatable } from "../small-tab-navigation/HoverNavigatable"; import WithTooltip from "../tooltip/WithTooltip"; +import { tv } from "../tv"; const Root = styled("div")` border-bottom: 1px solid ${({ theme }) => theme.col.grayLight}; diff --git a/frontend/src/js/previous-queries/upload/CSVColumnPicker.tsx b/frontend/src/js/previous-queries/upload/CSVColumnPicker.tsx index 50550167e6..5bfa0c1e37 100644 --- a/frontend/src/js/previous-queries/upload/CSVColumnPicker.tsx +++ b/frontend/src/js/previous-queries/upload/CSVColumnPicker.tsx @@ -12,8 +12,6 @@ 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 type { QueryUploadConfigT, UploadQueryResponseT } from "../../api/types"; import IconButton from "../../button/IconButton"; import PrimaryButton from "../../button/PrimaryButton"; @@ -23,6 +21,7 @@ import FaIcon from "../../icon/FaIcon"; import { useActiveLang } from "../../localization/useActiveLang"; import ScrollableList from "../../scrollable-list/ScrollableList"; import WithTooltip from "../../tooltip/WithTooltip"; +import { tv } from "../../tv"; import InputSelect from "../../ui-components/InputSelect/InputSelect"; const Row = styled("div")` diff --git a/frontend/src/js/snack-message/SnackMessage.tsx b/frontend/src/js/snack-message/SnackMessage.tsx index 9a1d1293d4..905612faf8 100644 --- a/frontend/src/js/snack-message/SnackMessage.tsx +++ b/frontend/src/js/snack-message/SnackMessage.tsx @@ -1,10 +1,10 @@ import { faTimes } from "@fortawesome/free-solid-svg-icons"; import { memo, useRef } from "react"; import { useDispatch, useSelector } from "react-redux"; -import { tv } from "tailwind-variants"; import type { StateT } from "../app/reducers"; import { useClickOutside } from "../common/helpers/useClickOutside"; import FaIcon from "../icon/FaIcon"; +import { tv } from "../tv"; import { resetMessage as resetMessageAction } from "./actions"; import type { SnackMessageStateT } from "./reducer"; diff --git a/frontend/src/js/tooltip/TooltipHeader.tsx b/frontend/src/js/tooltip/TooltipHeader.tsx index 9b0f680ba5..722de7a2d6 100644 --- a/frontend/src/js/tooltip/TooltipHeader.tsx +++ b/frontend/src/js/tooltip/TooltipHeader.tsx @@ -3,8 +3,8 @@ import { faAngleLeft } from "@fortawesome/free-solid-svg-icons"; import { memo } from "react"; import { useTranslation } from "react-i18next"; import { useDispatch } from "react-redux"; -import { tv } from "tailwind-variants"; import IconButton from "../button/IconButton"; +import { tv } from "../tv"; import { toggleDisplayTooltip } from "./actions"; const header = tv({ diff --git a/frontend/src/js/tv.ts b/frontend/src/js/tv.ts new file mode 100644 index 0000000000..bf73791fab --- /dev/null +++ b/frontend/src/js/tv.ts @@ -0,0 +1,16 @@ +import { createTV } from "tailwind-variants"; + +/** + * Every class list goes through this `tv`, never through the bare import: + * the class merger classifies unknown `text-*` classes as colours, so a + * custom font size would be dropped when a colour class is merged in. + */ +export const tv = createTV({ + twMergeConfig: { + extend: { + classGroups: { + "font-size": ["text-tiny"], + }, + }, + }, +}); diff --git a/frontend/src/js/ui-components/DropzoneWithFileInput.tsx b/frontend/src/js/ui-components/DropzoneWithFileInput.tsx index 883e89fd85..46d7622a86 100644 --- a/frontend/src/js/ui-components/DropzoneWithFileInput.tsx +++ b/frontend/src/js/ui-components/DropzoneWithFileInput.tsx @@ -39,7 +39,9 @@ const SxDropzone = styled(Dropzone)<{ isInitial?: boolean; tight?: boolean }>` } `; -const SxSelectFileButton = styled(SelectFileButton)<{ outside?: boolean }>` +const SxSelectFileButton = styled(SelectFileButton, { + shouldForwardProp: (prop) => prop !== "outside", +})<{ outside?: boolean }>` position: absolute; top: ${({ outside }) => (outside ? "-26px" : "3px")}; right: ${({ outside }) => (outside ? "-12px" : "0")}; diff --git a/frontend/src/js/ui-components/InputDateRange.tsx b/frontend/src/js/ui-components/InputDateRange.tsx index fd2266f306..a5a4bba947 100644 --- a/frontend/src/js/ui-components/InputDateRange.tsx +++ b/frontend/src/js/ui-components/InputDateRange.tsx @@ -14,7 +14,7 @@ import { parseDateToState, } from "../common/helpers/dateHelper"; import { exists } from "../common/helpers/exists"; -import { Icon } from "../icon/FaIcon"; +import FaIcon from "../icon/FaIcon"; import InfoTooltip from "../tooltip/InfoTooltip"; import InputDate from "./InputDate/InputDate"; @@ -172,7 +172,7 @@ const InputDateRange = ({ return ( - + {exists(indexPrefix) && # {indexPrefix}} {label} # {filterIdx} ) : ( - Date: Thu, 27 Aug 2026 17:15:53 +0200 Subject: [PATCH 2/3] fix(frontend): IconButton small never reached BasicButton, text utilities font-size only matches the previous behaviour: small only shrank the icon, and the emotion theme set font sizes without a line-height --- frontend/src/index.css | 9 ++++++++- frontend/src/js/button/IconButton.tsx | 1 - 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/frontend/src/index.css b/frontend/src/index.css index d89331cd9e..1fda34b873 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -26,8 +26,15 @@ --color-filetype-json: #1f5f30; /* `text-tiny`; the other sizes map onto tailwind defaults: - xs 12px, sm 14px, base 16px, xl 20px, 2xl 24px */ + xs 12px, sm 14px, base 16px, xl 20px, 2xl 24px. + Font size only, line-height stays inherited (as the old theme did it) */ --text-tiny: 11px; + --text-xs--line-height: initial; + --text-sm--line-height: initial; + --text-base--line-height: initial; + --text-lg--line-height: initial; + --text-xl--line-height: initial; + --text-2xl--line-height: initial; /* bare `rounded` */ --radius: 3px; diff --git a/frontend/src/js/button/IconButton.tsx b/frontend/src/js/button/IconButton.tsx index 126f07127e..0b0591d73c 100644 --- a/frontend/src/js/button/IconButton.tsx +++ b/frontend/src/js/button/IconButton.tsx @@ -129,7 +129,6 @@ const IconButton = ({ active={active} secondary={secondary} large={large} - small={small} {...restProps} className={iconButton({ secondary, From 7283ce0840a31f16c2604d8262e9d362e2cfa424 Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Thu, 27 Aug 2026 17:39:30 +0200 Subject: [PATCH 3/3] chore(frontend): only tailwind's own text sizes, literals for the rest drops the text-tiny token and the createTV merge config it needed --- frontend/src/index.css | 6 ++---- frontend/src/js/button/BadgeToggleButton.tsx | 4 ++-- frontend/src/js/button/BasicButton.tsx | 2 +- frontend/src/js/button/DestroyButton.tsx | 2 +- frontend/src/js/button/DownloadButton.tsx | 4 +--- frontend/src/js/button/IconButton.tsx | 3 +-- frontend/src/js/button/PreviewButton.tsx | 2 +- frontend/src/js/button/PrimaryButton.tsx | 2 +- frontend/src/js/button/SelectFileButton.tsx | 4 ++-- frontend/src/js/button/TransparentButton.tsx | 2 +- .../src/js/concept-trees/ConceptTreeList.tsx | 2 +- .../form/fields/DisclosureListField.tsx | 2 +- frontend/src/js/headings/Headings.tsx | 2 +- frontend/src/js/icon/FaIcon.tsx | 4 ++-- frontend/src/js/pane/TabNavigation.tsx | 2 +- .../previous-queries/upload/CSVColumnPicker.tsx | 2 +- frontend/src/js/snack-message/SnackMessage.tsx | 2 +- frontend/src/js/tooltip/TooltipHeader.tsx | 2 +- frontend/src/js/tv.ts | 16 ---------------- 19 files changed, 22 insertions(+), 43 deletions(-) delete mode 100644 frontend/src/js/tv.ts diff --git a/frontend/src/index.css b/frontend/src/index.css index 1fda34b873..a2056fbc15 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -25,10 +25,8 @@ --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. - Font size only, line-height stays inherited (as the old theme did it) */ - --text-tiny: 11px; + /* Font size only, line-height stays inherited (as the old theme did it). + Only tailwind's own sizes, anything else is a literal like `text-[11px]` */ --text-xs--line-height: initial; --text-sm--line-height: initial; --text-base--line-height: initial; diff --git a/frontend/src/js/button/BadgeToggleButton.tsx b/frontend/src/js/button/BadgeToggleButton.tsx index ac70272235..ae8ad3f3ce 100644 --- a/frontend/src/js/button/BadgeToggleButton.tsx +++ b/frontend/src/js/button/BadgeToggleButton.tsx @@ -1,7 +1,7 @@ import type { ReactNode } from "react"; import { useHotkeys } from "react-hotkeys-hook"; -import { tv } from "../tv"; +import { tv } from "tailwind-variants"; import BasicButton from "./BasicButton"; @@ -28,7 +28,7 @@ const superScript = tv({ "inline-block", "pl-[3px]", "translate-x-px -translate-y-[2px]", - "text-tiny", + "text-[11px]", "text-gray-500", ], }); diff --git a/frontend/src/js/button/BasicButton.tsx b/frontend/src/js/button/BasicButton.tsx index b8d5d77aaa..11a999fb65 100644 --- a/frontend/src/js/button/BasicButton.tsx +++ b/frontend/src/js/button/BasicButton.tsx @@ -1,6 +1,6 @@ import type { ButtonHTMLAttributes, Ref } from "react"; -import { tv } from "../tv"; +import { tv } from "tailwind-variants"; export interface BasicButtonProps extends ButtonHTMLAttributes { diff --git a/frontend/src/js/button/DestroyButton.tsx b/frontend/src/js/button/DestroyButton.tsx index cb18d79be6..d9328c4f18 100644 --- a/frontend/src/js/button/DestroyButton.tsx +++ b/frontend/src/js/button/DestroyButton.tsx @@ -1,6 +1,6 @@ import type { ComponentProps } from "react"; -import { tv } from "../tv"; +import { tv } from "tailwind-variants"; import { TransparentButton } from "./TransparentButton"; diff --git a/frontend/src/js/button/DownloadButton.tsx b/frontend/src/js/button/DownloadButton.tsx index e48cf76f29..a5e731f654 100644 --- a/frontend/src/js/button/DownloadButton.tsx +++ b/frontend/src/js/button/DownloadButton.tsx @@ -9,13 +9,11 @@ import { faFilePdf, } from "@fortawesome/free-solid-svg-icons"; import { type ReactNode, type Ref, useContext } from "react"; - +import { tv } from "tailwind-variants"; import type { ResultUrlWithLabel } from "../api/types"; import { AuthTokenContext } from "../authorization/AuthTokenProvider"; import { getEnding } from "../query-runner/DownloadResultsDropdownButton"; -import { tv } from "../tv"; - import IconButton, { type IconButtonPropsT } from "./IconButton"; const link = tv({ base: "leading-none" }); diff --git a/frontend/src/js/button/IconButton.tsx b/frontend/src/js/button/IconButton.tsx index 0b0591d73c..ae77c50e1b 100644 --- a/frontend/src/js/button/IconButton.tsx +++ b/frontend/src/js/button/IconButton.tsx @@ -1,8 +1,7 @@ import type { IconProp } from "@fortawesome/fontawesome-svg-core"; import { memo, type Ref, useMemo } from "react"; - +import { tv } from "tailwind-variants"; import FaIcon, { type IconStyleProps } from "../icon/FaIcon"; -import { tv } from "../tv"; import BasicButton, { type BasicButtonProps } from "./BasicButton"; diff --git a/frontend/src/js/button/PreviewButton.tsx b/frontend/src/js/button/PreviewButton.tsx index 189866ab29..c1e792fefd 100644 --- a/frontend/src/js/button/PreviewButton.tsx +++ b/frontend/src/js/button/PreviewButton.tsx @@ -5,9 +5,9 @@ import { import { useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { useDispatch, useSelector } from "react-redux"; +import { tv } from "tailwind-variants"; import type { StateT } from "../app/reducers"; import { openPreview, useLoadPreviewData } from "../preview/actions"; -import { tv } from "../tv"; import IconButton, { type IconButtonPropsT } from "./IconButton"; const previewButton = tv({ diff --git a/frontend/src/js/button/PrimaryButton.tsx b/frontend/src/js/button/PrimaryButton.tsx index 97042799d1..34ece7cc0e 100644 --- a/frontend/src/js/button/PrimaryButton.tsx +++ b/frontend/src/js/button/PrimaryButton.tsx @@ -1,6 +1,6 @@ import type { Ref } from "react"; -import { tv } from "../tv"; +import { tv } from "tailwind-variants"; import BasicButton, { type BasicButtonProps } from "./BasicButton"; diff --git a/frontend/src/js/button/SelectFileButton.tsx b/frontend/src/js/button/SelectFileButton.tsx index 5c99eb786d..15eee1b7fc 100644 --- a/frontend/src/js/button/SelectFileButton.tsx +++ b/frontend/src/js/button/SelectFileButton.tsx @@ -1,6 +1,6 @@ import type { Ref } from "react"; -import { tv } from "../tv"; +import { tv } from "tailwind-variants"; import BasicButton, { type BasicButtonProps } from "./BasicButton"; @@ -10,7 +10,7 @@ const selectFileButton = tv({ "gap-[5px]", "bg-transparent", "border-0", - "text-tiny", + "text-[11px]", "text-gray-500", "font-light", "hover:underline", diff --git a/frontend/src/js/button/TransparentButton.tsx b/frontend/src/js/button/TransparentButton.tsx index bc48314d93..27519c7fb6 100644 --- a/frontend/src/js/button/TransparentButton.tsx +++ b/frontend/src/js/button/TransparentButton.tsx @@ -1,6 +1,6 @@ import type { Ref } from "react"; -import { tv } from "../tv"; +import { tv } from "tailwind-variants"; import BasicButton, { type BasicButtonProps } from "./BasicButton"; diff --git a/frontend/src/js/concept-trees/ConceptTreeList.tsx b/frontend/src/js/concept-trees/ConceptTreeList.tsx index 903e646f38..0388815bea 100644 --- a/frontend/src/js/concept-trees/ConceptTreeList.tsx +++ b/frontend/src/js/concept-trees/ConceptTreeList.tsx @@ -1,8 +1,8 @@ import { useMemo } from "react"; import { useSelector } from "react-redux"; +import { tv } from "tailwind-variants"; import type { DatasetT } from "../api/types"; import type { StateT } from "../app/reducers"; -import { tv } from "../tv"; import { useLoadTree } from "./actions"; import ConceptsProgressBar from "./ConceptsProgressBar"; import ConceptTreeListItem from "./ConceptTreeListItem"; diff --git a/frontend/src/js/external-forms/form/fields/DisclosureListField.tsx b/frontend/src/js/external-forms/form/fields/DisclosureListField.tsx index fa499a7914..5d3e4f19a1 100644 --- a/frontend/src/js/external-forms/form/fields/DisclosureListField.tsx +++ b/frontend/src/js/external-forms/form/fields/DisclosureListField.tsx @@ -7,13 +7,13 @@ import { import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { type ComponentProps, useEffect, useState } from "react"; import { useFieldArray } from "react-hook-form"; +import { tv } from "tailwind-variants"; import IconButton from "../../../button/IconButton"; import { TransparentButton } from "../../../button/TransparentButton"; import { exists } from "../../../common/helpers/exists"; import { usePrevious } from "../../../common/helpers/usePrevious"; import FaIcon from "../../../icon/FaIcon"; import InfoTooltip from "../../../tooltip/InfoTooltip"; -import { tv } from "../../../tv"; import type { DisclosureListField as DisclosureListFieldT } from "../../config-types"; import { getFieldKey, diff --git a/frontend/src/js/headings/Headings.tsx b/frontend/src/js/headings/Headings.tsx index 3661805383..018e5d315d 100644 --- a/frontend/src/js/headings/Headings.tsx +++ b/frontend/src/js/headings/Headings.tsx @@ -1,5 +1,5 @@ import type { ComponentProps } from "react"; -import { tv } from "../tv"; +import { tv } from "tailwind-variants"; const heading3 = tv({ base: "text-lg font-normal text-gray-800" }); const heading4 = tv({ base: "text-sm font-normal text-gray-500 uppercase" }); diff --git a/frontend/src/js/icon/FaIcon.tsx b/frontend/src/js/icon/FaIcon.tsx index 4a286670c8..e50b2b6d11 100644 --- a/frontend/src/js/icon/FaIcon.tsx +++ b/frontend/src/js/icon/FaIcon.tsx @@ -2,7 +2,7 @@ import type { IconProp } from "@fortawesome/fontawesome-svg-core"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import type { ComponentProps, Ref } from "react"; -import { tv } from "../tv"; +import { tv } from "tailwind-variants"; export interface IconStyleProps { left?: boolean; @@ -38,7 +38,7 @@ const icon = tv({ right: { true: "pl-[10px]" }, center: { true: "text-center" }, // later wins when both are set - tiny: { true: "text-tiny" }, + tiny: { true: "text-[11px]" }, large: { true: "text-base" }, disabled: { true: "cursor-not-allowed" }, }, diff --git a/frontend/src/js/pane/TabNavigation.tsx b/frontend/src/js/pane/TabNavigation.tsx index d451d2a104..a576b0a35d 100644 --- a/frontend/src/js/pane/TabNavigation.tsx +++ b/frontend/src/js/pane/TabNavigation.tsx @@ -1,9 +1,9 @@ import styled from "@emotion/styled"; import { faSpinner } from "@fortawesome/free-solid-svg-icons"; +import { tv } from "tailwind-variants"; import FaIcon from "../icon/FaIcon"; import { HoverNavigatable } from "../small-tab-navigation/HoverNavigatable"; import WithTooltip from "../tooltip/WithTooltip"; -import { tv } from "../tv"; const Root = styled("div")` border-bottom: 1px solid ${({ theme }) => theme.col.grayLight}; diff --git a/frontend/src/js/previous-queries/upload/CSVColumnPicker.tsx b/frontend/src/js/previous-queries/upload/CSVColumnPicker.tsx index 5bfa0c1e37..8c8d4af716 100644 --- a/frontend/src/js/previous-queries/upload/CSVColumnPicker.tsx +++ b/frontend/src/js/previous-queries/upload/CSVColumnPicker.tsx @@ -12,6 +12,7 @@ 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 type { QueryUploadConfigT, UploadQueryResponseT } from "../../api/types"; import IconButton from "../../button/IconButton"; import PrimaryButton from "../../button/PrimaryButton"; @@ -21,7 +22,6 @@ import FaIcon from "../../icon/FaIcon"; import { useActiveLang } from "../../localization/useActiveLang"; import ScrollableList from "../../scrollable-list/ScrollableList"; import WithTooltip from "../../tooltip/WithTooltip"; -import { tv } from "../../tv"; import InputSelect from "../../ui-components/InputSelect/InputSelect"; const Row = styled("div")` diff --git a/frontend/src/js/snack-message/SnackMessage.tsx b/frontend/src/js/snack-message/SnackMessage.tsx index 905612faf8..9a1d1293d4 100644 --- a/frontend/src/js/snack-message/SnackMessage.tsx +++ b/frontend/src/js/snack-message/SnackMessage.tsx @@ -1,10 +1,10 @@ import { faTimes } from "@fortawesome/free-solid-svg-icons"; import { memo, useRef } from "react"; import { useDispatch, useSelector } from "react-redux"; +import { tv } from "tailwind-variants"; import type { StateT } from "../app/reducers"; import { useClickOutside } from "../common/helpers/useClickOutside"; import FaIcon from "../icon/FaIcon"; -import { tv } from "../tv"; import { resetMessage as resetMessageAction } from "./actions"; import type { SnackMessageStateT } from "./reducer"; diff --git a/frontend/src/js/tooltip/TooltipHeader.tsx b/frontend/src/js/tooltip/TooltipHeader.tsx index 722de7a2d6..9b0f680ba5 100644 --- a/frontend/src/js/tooltip/TooltipHeader.tsx +++ b/frontend/src/js/tooltip/TooltipHeader.tsx @@ -3,8 +3,8 @@ import { faAngleLeft } from "@fortawesome/free-solid-svg-icons"; import { memo } from "react"; import { useTranslation } from "react-i18next"; import { useDispatch } from "react-redux"; +import { tv } from "tailwind-variants"; import IconButton from "../button/IconButton"; -import { tv } from "../tv"; import { toggleDisplayTooltip } from "./actions"; const header = tv({ diff --git a/frontend/src/js/tv.ts b/frontend/src/js/tv.ts deleted file mode 100644 index bf73791fab..0000000000 --- a/frontend/src/js/tv.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { createTV } from "tailwind-variants"; - -/** - * Every class list goes through this `tv`, never through the bare import: - * the class merger classifies unknown `text-*` classes as colours, so a - * custom font size would be dropped when a colour class is merged in. - */ -export const tv = createTV({ - twMergeConfig: { - extend: { - classGroups: { - "font-size": ["text-tiny"], - }, - }, - }, -});