From 347719843affea498d4cedbfbd966f56a85caaa9 Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Wed, 2 Sep 2026 14:27:29 +0200 Subject: [PATCH] refactor(frontend): standard query editor, previous queries, editor v2 on tailwind-variants sixth styled() slice --- frontend/src/js/editor-v2/EditorLayout.ts | 24 -- frontend/src/js/editor-v2/EditorLayout.tsx | 33 +++ frontend/src/js/editor-v2/EditorV2.tsx | 85 +++---- .../js/editor-v2/KeyboardShortcutTooltip.tsx | 25 +- frontend/src/js/editor-v2/TreeNode.tsx | 199 +++++++-------- frontend/src/js/editor-v2/TreeNodeConcept.tsx | 65 +++-- .../editor-v2/date-restriction/DateModal.tsx | 50 ++-- .../editor-v2/date-restriction/DateRange.tsx | 37 ++- .../time-connection/TimeConnection.tsx | 78 +++--- .../time-connection/TimeConnectionModal.tsx | 93 ++++--- .../previous-queries/list/AddFolderModal.tsx | 33 +-- .../list/EditProjectItemFoldersModal.tsx | 9 +- .../src/js/previous-queries/list/Folder.tsx | 90 ++++--- .../src/js/previous-queries/list/Folders.tsx | 129 +++++----- .../list/FoldersToggleButton.tsx | 14 +- .../js/previous-queries/list/ProjectItem.tsx | 228 ++++++++---------- .../list/ProjectItemLabel.tsx | 25 +- .../js/previous-queries/list/ProjectItems.tsx | 16 +- .../previous-queries/list/ProjectItemsTab.tsx | 112 +++------ .../list/ShareProjectItemModal.tsx | 19 +- .../upload/CSVColumnPicker.tsx | 170 ++++++------- .../upload/UploadQueryResults.tsx | 8 +- .../upload/UploadQueryResultsModal.tsx | 48 ++-- .../EmptyQueryEditorDropzone.tsx | 128 +++++----- .../ExpandPreviousQueryModal.tsx | 23 +- .../src/js/standard-query-editor/Query.tsx | 41 +--- .../QueryAndDropzone.tsx | 17 +- .../js/standard-query-editor/QueryEditor.tsx | 15 +- .../QueryEditorDropzone.tsx | 43 +--- .../js/standard-query-editor/QueryGroup.tsx | 75 +++--- .../QueryGroupActions.tsx | 78 +++--- .../js/standard-query-editor/QueryHeader.tsx | 12 +- .../js/standard-query-editor/QueryNode.tsx | 60 +++-- .../QueryNodeActions.tsx | 67 +++-- .../QueryNodeContent.tsx | 90 ++++--- .../SecondaryIdSelector.tsx | 45 ++-- 36 files changed, 982 insertions(+), 1302 deletions(-) delete mode 100644 frontend/src/js/editor-v2/EditorLayout.ts create mode 100644 frontend/src/js/editor-v2/EditorLayout.tsx diff --git a/frontend/src/js/editor-v2/EditorLayout.ts b/frontend/src/js/editor-v2/EditorLayout.ts deleted file mode 100644 index 3cb8d5902e..0000000000 --- a/frontend/src/js/editor-v2/EditorLayout.ts +++ /dev/null @@ -1,24 +0,0 @@ -import styled from "@emotion/styled"; - -export const Grid = styled("div")` - flex-grow: 1; - display: grid; - gap: 3px; - height: 100%; - width: 100%; - place-items: center; - overflow: auto; -`; - -export const Connector = styled("span")` - text-transform: uppercase; - font-size: ${({ theme }) => theme.font.sm}; - color: black; - user-select: none; - - border-radius: ${({ theme }) => theme.borderRadius}; - padding: 0px 5px; - display: flex; - justify-content: center; - align-items: center; -`; diff --git a/frontend/src/js/editor-v2/EditorLayout.tsx b/frontend/src/js/editor-v2/EditorLayout.tsx new file mode 100644 index 0000000000..0d0c3e518b --- /dev/null +++ b/frontend/src/js/editor-v2/EditorLayout.tsx @@ -0,0 +1,33 @@ +import type { ComponentProps } from "react"; +import { tv } from "tailwind-variants"; + +const grid = tv({ + base: [ + "grow", + "grid", + "gap-[3px]", + "h-full w-full", + "place-items-center", + "overflow-auto", + ], +}); + +const connector = tv({ + base: [ + "flex items-center justify-center", + "px-[5px]", + "rounded", + "uppercase", + "text-sm", + "text-black", + "select-none", + ], +}); + +export const Grid = ({ className, ...props }: ComponentProps<"div">) => ( +
+); + +export const Connector = ({ className, ...props }: ComponentProps<"span">) => ( + +); diff --git a/frontend/src/js/editor-v2/EditorV2.tsx b/frontend/src/js/editor-v2/EditorV2.tsx index 62614f4d57..72ff2c2bcd 100644 --- a/frontend/src/js/editor-v2/EditorV2.tsx +++ b/frontend/src/js/editor-v2/EditorV2.tsx @@ -1,4 +1,3 @@ -import styled from "@emotion/styled"; import { faCalendar, faTrashCan } from "@fortawesome/free-regular-svg-icons"; import { faBan, @@ -13,6 +12,7 @@ import { createId } from "@paralleldrive/cuid2"; import { useCallback, useEffect, useMemo, useState } from "react"; import { useHotkeys } from "react-hotkeys-hook"; import { useTranslation } from "react-i18next"; +import { tv } from "tailwind-variants"; import IconButton from "../button/IconButton"; import { useDatasetId } from "../dataset/selectors"; @@ -42,43 +42,19 @@ import { useTimeConnectionEditing } from "./time-connection/useTimeConnectionEdi import type { Tree, TreeChildrenTime } from "./types"; import { findNodeById, useGetTranslatedConnection } from "./util"; -const Root = styled("div")` - flex-grow: 1; - height: 100%; - display: flex; - flex-direction: column; -`; +const main = tv({ + base: [ + "grow", + "h-full", + "pt-2 pb-[10px] px-[10px]", + "flex flex-col", + "gap-[10px]", + ], +}); -const Main = styled("div")` - flex-grow: 1; - height: 100%; - padding: 8px 10px 10px 10px; - display: flex; - flex-direction: column; - gap: 10px; -`; - -const SxDropzone = styled(Dropzone)` - width: 100%; - height: 100%; -`; - -const Actions = styled("div")` - display: flex; - align-items: center; - justify-content: space-between; -`; - -const Flex = styled("div")` - display: flex; - align-items: center; -`; - -const SxIconButton = styled(IconButton)` - display: flex; - align-items: center; - gap: 5px; -`; +const actionIconButton = tv({ + base: ["flex items-center", "gap-[5px]"], +}); const useEditorState = () => { const [tree, setTree] = useState(undefined); @@ -270,8 +246,8 @@ export function EditorV2({ ); return ( - -
+
+
{showQueryNodeEditor && selectedNode?.data && nodeIsConceptQueryNode(selectedNode.data) && ( @@ -319,8 +295,8 @@ export function EditorV2({ /> )} {tree && ( - - +
+
{featureQueryNodeEdit && selectedNode && ( - {connection} - + )} {selectedNode?.children?.connection === "time" && ( - { @@ -403,7 +381,7 @@ export function EditorV2({ }} > {t("editorV2.timeConnection")} - + )} {canExpand && ( @@ -420,8 +398,8 @@ export function EditorV2({ )} - - +
+
{selectedNode && ( - - +
+
)} { @@ -486,7 +464,8 @@ export function EditorV2({ onRotateConnector={onRotateConnector} /> ) : ( - { const id = createId(); setTree({ @@ -498,12 +477,12 @@ export function EditorV2({ acceptedDropTypes={EDITOR_DROP_TYPES} > {() => } - + )} -
+
- + ); } diff --git a/frontend/src/js/editor-v2/KeyboardShortcutTooltip.tsx b/frontend/src/js/editor-v2/KeyboardShortcutTooltip.tsx index 81f1f0b407..9b564b1e59 100644 --- a/frontend/src/js/editor-v2/KeyboardShortcutTooltip.tsx +++ b/frontend/src/js/editor-v2/KeyboardShortcutTooltip.tsx @@ -1,22 +1,13 @@ -import styled from "@emotion/styled"; import { Fragment, type ReactElement } from "react"; import { useTranslation } from "react-i18next"; +import { tv } from "tailwind-variants"; import { KeyboardKey } from "../common/components/KeyboardKey"; import WithTooltip from "../tooltip/WithTooltip"; -const KeyTooltip = styled("div")` - padding: 8px 15px; - display: flex; - align-items: center; - gap: 5px; -`; - -const Keys = styled("div")` - display: flex; - align-items: center; - gap: 2px; -`; +const keyTooltip = tv({ + base: ["flex items-center", "gap-[5px]", "px-[15px] py-2"], +}); export const KeyboardShortcutTooltip = ({ keyname, @@ -31,17 +22,17 @@ export const KeyboardShortcutTooltip = ({ return ( +
{t("common.shortcut")}:{" "} - +
{keynames.map((keyPart, i) => ( {keyPart} {i < keynames.length - 1 && "+"} ))} - - +
+
} > {children} diff --git a/frontend/src/js/editor-v2/TreeNode.tsx b/frontend/src/js/editor-v2/TreeNode.tsx index cde8a0d0fe..6e3ecc1101 100644 --- a/frontend/src/js/editor-v2/TreeNode.tsx +++ b/frontend/src/js/editor-v2/TreeNode.tsx @@ -1,8 +1,8 @@ -import styled from "@emotion/styled"; import { faCalendarMinus } from "@fortawesome/free-regular-svg-icons"; import { createId } from "@paralleldrive/cuid2"; import { type DOMAttributes, memo } from "react"; import { useTranslation } from "react-i18next"; +import { tv } from "tailwind-variants"; import { DNDType } from "../common/constants/dndTypes"; import FaIcon from "../icon/FaIcon"; @@ -13,7 +13,10 @@ import type { DragItemQuery, } from "../standard-query-editor/types"; import WithTooltip from "../tooltip/WithTooltip"; -import Dropzone, { type DropzoneProps } from "../ui-components/Dropzone"; +import Dropzone, { + type DropzoneProps, + type PossibleDroppableObject, +} from "../ui-components/Dropzone"; import { EDITOR_DROP_TYPES } from "./config"; import { DateRange } from "./date-restriction/DateRange"; import { Connector, Grid } from "./EditorLayout"; @@ -22,40 +25,35 @@ import { TimeConnection } from "./time-connection/TimeConnection"; import type { ConnectionKind, Tree } from "./types"; import { useGetTranslatedConnection } from "./util"; -const NodeContainer = styled("div")` - display: grid; - gap: 5px; -`; +const nodeContainer = tv({ + base: ["grid", "gap-[5px]"], +}); -const Node = styled("div")<{ - selected?: boolean; - active?: boolean; - negated?: boolean; - leaf?: boolean; - isDragging?: boolean; -}>` - padding: ${({ leaf, isDragging }) => - leaf ? "8px 10px" : isDragging ? "5px" : "24px"}; - border: 2px solid - ${({ negated, theme, selected, active }) => - negated - ? theme.col.red - : active - ? theme.col.blueGrayDark - : selected - ? theme.col.gray - : theme.col.grayMediumLight}; - box-shadow: ${({ selected, theme }) => - selected ? `inset 0px 0px 0px 4px ${theme.col.blueGrayVeryLight}` : "none"}; - - border-radius: ${({ theme }) => theme.borderRadius}; - width: ${({ leaf }) => (leaf ? "230px" : "inherit")}; - background-color: ${({ leaf, theme }) => (leaf ? "white" : theme.col.bg)}; - cursor: pointer; - display: flex; - flex-direction: column; - gap: 10px; -`; +const node = tv({ + base: [ + "flex flex-col", + "gap-[10px]", + "p-6", + "w-[inherit]", + "rounded", + "border-2 border-gray-400", + "bg-bg-50", + "cursor-pointer", + ], + variants: { + // later wins when several are set + isDragging: { true: "p-[5px]" }, + leaf: { true: ["px-[10px] py-2", "w-[230px]", "bg-white"] }, + selected: { + true: [ + "border-gray-500", + "shadow-[inset_0px_0px_0px_4px_var(--color-primary-50)]", + ], + }, + active: { true: "border-primary-500" }, + negated: { true: "border-red" }, + }, +}); function getGridStyles(tree: Tree) { if (!tree.children) { @@ -73,11 +71,22 @@ function getGridStyles(tree: Tree) { } } -const InvisibleDropzoneContainer = styled(Dropzone)<{ bare?: boolean }>` - width: 100%; - height: 100%; - padding: ${({ bare }) => (bare ? "6px" : "20px")}; -`; +const invisibleDropzone = tv({ + base: ["h-full w-full", "p-5"], + variants: { + bare: { true: "p-[6px]" }, + }, +}); + +const InvisibleDropzoneContainer = ({ + className, + ...props +}: DropzoneProps & { className?: string }) => ( + +); const InvisibleDropzone = ( props: Omit, "acceptedDropTypes">, @@ -92,43 +101,30 @@ const InvisibleDropzone = ( ); }; -const Name = styled("div")` - font-size: ${({ theme }) => theme.font.sm}; - font-weight: 400; - color: ${({ theme }) => theme.col.black}; -`; +const previousQueryLabel = tv({ + base: [ + "leading-[1.2]", + "text-xs", + "uppercase", + "font-bold", + "text-primary-500", + ], +}); -const PreviousQueryLabel = styled("p")` - margin: 0; - line-height: 1.2; - font-size: ${({ theme }) => theme.font.xs}; - text-transform: uppercase; - font-weight: 700; - color: ${({ theme }) => theme.col.blueGrayDark}; -`; +const rootNode = tv({ + base: [ + "leading-none", + "text-xs", + "uppercase", + "font-bold", + "text-primary-500", + "[word-break:break-word]", + ], +}); -const ContentContainer = styled("div")` - display: flex; - flex-direction: column; - gap: 4px; -`; - -const RootNode = styled("p")` - margin: 0; - line-height: 1; - text-transform: uppercase; - font-weight: 700; - font-size: ${({ theme }) => theme.font.xs}; - color: ${({ theme }) => theme.col.blueGrayDark}; - word-break: break-word; -`; - -const Dates = styled("div")` - text-align: right; - font-size: ${({ theme }) => theme.font.xs}; - text-transform: uppercase; - font-weight: 400; -`; +const dates = tv({ + base: ["text-right", "text-xs", "uppercase", "font-normal"], +}); export function TreeNode({ tree, @@ -231,7 +227,7 @@ export function TreeNode({ }; return ( - +
{droppable.v && ( @@ -239,7 +235,8 @@ export function TreeNode({ } /> )} - {({ canDrop }) => ( - )} - +
)} @@ -403,7 +410,7 @@ export function TreeNode({ } /> )} - + {droppable.v && ( @@ -411,7 +418,7 @@ export function TreeNode({ } /> )} - + ); } diff --git a/frontend/src/js/editor-v2/TreeNodeConcept.tsx b/frontend/src/js/editor-v2/TreeNodeConcept.tsx index c0b5496efe..4c52f0f3f7 100644 --- a/frontend/src/js/editor-v2/TreeNodeConcept.tsx +++ b/frontend/src/js/editor-v2/TreeNodeConcept.tsx @@ -1,37 +1,22 @@ -import styled from "@emotion/styled"; import { Fragment } from "react"; import { useTranslation } from "react-i18next"; +import { tv } from "tailwind-variants"; import { exists } from "../common/helpers/exists"; import type { DragItemConceptTreeNode } from "../standard-query-editor/types"; -const Bold = styled("span")` - font-weight: 400; -`; +const sectionHeading = tv({ + base: ["font-bold", "text-primary-500", "uppercase", "text-xs"], +}); -const SectionHeading = styled("h4")` - font-weight: 700; - color: ${(props) => props.theme.col.blueGrayDark}; - margin: 0; - text-transform: uppercase; - font-size: ${({ theme }) => theme.font.xs}; -`; - -const Appendix = styled("div")` - display: flex; - flex-direction: column; - gap: 6px; - margin-top: 8px; -`; - -const Description = styled("div")` - font-size: ${({ theme }) => theme.font.xs}; - color: ${({ theme }) => theme.col.black}; - display: flex; - align-items: center; - gap: 0px 5px; - flex-wrap: wrap; -`; +const description = tv({ + base: [ + "flex items-center flex-wrap", + "gap-x-[5px] gap-y-0", + "text-xs", + "text-gray-800", + ], +}); export const TreeNodeConcept = ({ node, @@ -58,29 +43,35 @@ export const TreeNodeConcept = ({ return ( <> - {node.description && {node.description}} + {node.description && ( +
{node.description}
+ )} {showAppendix && ( - +
{selectedSelects.length > 0 && (
- {t("editorV2.outputSection")} - +

+ {t("editorV2.outputSection")} +

+
- +
)} {filtersWithValues.length > 0 && (
- {t("editorV2.filtersSection")} +

+ {t("editorV2.filtersSection")} +

{filtersWithValues.map((f) => ( - - {f.label}: +
+ {f.label}: - +
))}
)} - +
)} ); diff --git a/frontend/src/js/editor-v2/date-restriction/DateModal.tsx b/frontend/src/js/editor-v2/date-restriction/DateModal.tsx index eb20c96827..65b24a7a3e 100644 --- a/frontend/src/js/editor-v2/date-restriction/DateModal.tsx +++ b/frontend/src/js/editor-v2/date-restriction/DateModal.tsx @@ -1,9 +1,9 @@ -import styled from "@emotion/styled"; import { faCalendarMinus } from "@fortawesome/free-regular-svg-icons"; import { faUndo } from "@fortawesome/free-solid-svg-icons"; import { useCallback, useMemo } from "react"; import { useHotkeys } from "react-hotkeys-hook"; import { useTranslation } from "react-i18next"; +import { tv } from "tailwind-variants"; import type { DateRangeT } from "../../api/types"; import IconButton from "../../button/IconButton"; @@ -13,26 +13,19 @@ import Modal from "../../modal/Modal"; import InputCheckbox from "../../ui-components/InputCheckbox"; import InputDateRange from "../../ui-components/InputDateRange"; -const Col = styled("div")` - display: flex; - flex-direction: column; - gap: 32px; -`; +const sectionHeadline = tv({ + base: [ + "flex items-center", + "gap-[10px]", + "mb-[10px]", + "text-base", + "font-normal", + ], +}); -const SectionHeadline = styled("p")` - display: flex; - align-items: center; - gap: 10px; - margin: 0 0 10px; - font-size: ${({ theme }) => theme.font.md}; - font-weight: 400; -`; - -const ResetAll = styled(IconButton)` - color: ${({ theme }) => theme.col.blueGrayDark}; - font-weight: 700; - margin-left: 20px; -`; +const resetAll = tv({ + base: ["ml-5", "text-primary-500", "font-bold"], +}); export const DateModal = ({ onClose, @@ -61,9 +54,14 @@ export const DateModal = ({ const labelSuffix = useMemo(() => { return hasActiveDate ? ( - + {t("queryNodeEditor.reset")} - + ) : null; }, [t, hasActiveDate, onResetDates]); @@ -85,7 +83,7 @@ export const DateModal = ({ doneButton headline={t("queryGroupModal.explanation")} > - +
{headline}
- +

{t("queryNodeEditor.excludeTimestamps")} - +

- +
); }; diff --git a/frontend/src/js/editor-v2/date-restriction/DateRange.tsx b/frontend/src/js/editor-v2/date-restriction/DateRange.tsx index 7c85595664..028ab8d48b 100644 --- a/frontend/src/js/editor-v2/date-restriction/DateRange.tsx +++ b/frontend/src/js/editor-v2/date-restriction/DateRange.tsx @@ -1,23 +1,22 @@ -import styled from "@emotion/styled"; import { useTranslation } from "react-i18next"; +import { tv } from "tailwind-variants"; import type { DateRangeT } from "../../api/types"; import { formatDate } from "../../common/helpers/dateHelper"; -const Root = styled("div")` - font-size: ${({ theme }) => theme.font.xs}; - font-family: monospace; - display: inline-grid; - gap: 0 5px; - grid-template-columns: auto 1fr; -`; - -const Label = styled("div")` - text-transform: uppercase; - color: ${({ theme }) => theme.col.blueGrayDark}; - font-weight: 700; - justify-self: flex-end; -`; +const root = tv({ + base: [ + "inline-grid", + "grid-cols-[auto_1fr]", + "gap-x-[5px] gap-y-0", + "text-xs", + "font-mono", + ], +}); + +const dateLabel = tv({ + base: ["justify-self-end", "uppercase", "text-primary-500", "font-bold"], +}); const getFormattedDate = (date: string | undefined, dateFormat: string) => { if (!date) return null; @@ -37,19 +36,19 @@ export const DateRange = ({ dateRange }: { dateRange: DateRangeT }) => { const dateMax = getFormattedDate(dateRange.max, dateFormat); return ( - +
{dateMin && ( <> - +
{t("inputDateRange.from")}
{dateMin} )} {dateMax && dateMax !== dateMin && ( <> - +
{t("inputDateRange.to")}
{dateMax} )} - +
); }; diff --git a/frontend/src/js/editor-v2/time-connection/TimeConnection.tsx b/frontend/src/js/editor-v2/time-connection/TimeConnection.tsx index 3b3c7ee8fc..39710d68fa 100644 --- a/frontend/src/js/editor-v2/time-connection/TimeConnection.tsx +++ b/frontend/src/js/editor-v2/time-connection/TimeConnection.tsx @@ -1,6 +1,7 @@ -import styled from "@emotion/styled"; import { type DOMAttributes, memo } from "react"; import { useTranslation } from "react-i18next"; +import { tv } from "tailwind-variants"; +import { useAppTheme } from "../../app-theme-context"; import type { TreeChildrenTime } from "../types"; import { @@ -10,36 +11,13 @@ import { useTranslatedOperator, } from "../util"; -const Container = styled("div")` - margin: 0 auto; - display: inline-flex; - flex-direction: column; - user-select: none; -`; +const container = tv({ + base: ["mx-auto", "inline-flex flex-col", "select-none"], +}); -const Row = styled("div")` - display: flex; - align-items: center; - gap: 5px; - font-size: ${({ theme }) => theme.font.sm}; -`; - -const ConceptName = styled("span")` - font-weight: bold; - color: ${({ theme }) => theme.col.blueGrayDark}; -`; -const Timestamp = styled("span")` - font-weight: bold; - color: ${({ theme }) => theme.col.palette[0]}; -`; -const Interval = styled("span")` - font-weight: bold; - color: ${({ theme }) => theme.col.palette[1]}; -`; -const Operator = styled("span")` - font-weight: bold; - color: ${({ theme }) => theme.col.palette.at(-2)}; -`; +const row = tv({ + base: ["flex items-center", "gap-[5px]", "text-sm"], +}); export const TimeConnection = memo( ({ @@ -50,6 +28,7 @@ export const TimeConnection = memo( onDoubleClick: DOMAttributes["onDoubleClick"]; }) => { const { t } = useTranslation(); + const { palette } = useAppTheme(); const getNodeLabel = useGetNodeLabel(); const getTranslatedTimestamp = useGetTranslatedTimestamp(); @@ -61,22 +40,33 @@ export const TimeConnection = memo( const interval = useTranslatedInterval(conditions.interval); return ( - - - {aTimestamp} + // biome-ignore lint/a11y/noStaticElementInteractions: TODO double-click opens the time modal, emotion had hidden this +
+
+ + {aTimestamp} + {t("editorV2.dateRangeFrom")} - {a} - - - {conditions.operator !== "WHILE" && {interval}} - {operator} - - - {bTimestamp} + {a} +
+
+ {conditions.operator !== "WHILE" && ( + + {interval} + + )} + + {operator} + +
+
+ + {bTimestamp} + {t("editorV2.dateRangeFrom")} - {b} - - + {b} +
+
); }, ); diff --git a/frontend/src/js/editor-v2/time-connection/TimeConnectionModal.tsx b/frontend/src/js/editor-v2/time-connection/TimeConnectionModal.tsx index dc2ebafcf7..86900f22da 100644 --- a/frontend/src/js/editor-v2/time-connection/TimeConnectionModal.tsx +++ b/frontend/src/js/editor-v2/time-connection/TimeConnectionModal.tsx @@ -1,6 +1,6 @@ -import styled from "@emotion/styled"; import { memo, useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; +import { tv } from "tailwind-variants"; import { exists } from "../../common/helpers/exists"; import Modal from "../../modal/Modal"; @@ -9,39 +9,24 @@ import InputSelect from "../../ui-components/InputSelect/InputSelect"; import type { TimeOperator, TimeTimestamp, TreeChildrenTime } from "../types"; import { useGetNodeLabel } from "../util"; -const Content = styled("div")` - display: flex; - flex-direction: column; - gap: 15px; - min-width: 350px; -`; +const content = tv({ + base: ["flex flex-col", "gap-[15px]", "min-w-[350px]"], +}); -const Row = styled("div")` - display: flex; - align-items: center; - gap: 15px; -`; +const row = tv({ + base: ["flex items-center", "gap-[15px]"], +}); -const SxBaseInput = styled(BaseInput)` - width: 100px; -`; - -const SxInputSelect = styled(InputSelect)<{ disabled?: boolean }>` - min-width: 150px; - flex-basis: 0; - opacity: ${({ disabled }) => (disabled ? 0.5 : 1)}; -`; - -const DateRangeFrom = styled("span")` - white-space: nowrap; -`; +const inputSelect = tv({ + base: ["min-w-[150px]", "basis-0"], + variants: { + disabled: { true: "opacity-50" }, + }, +}); -const ConceptName = styled("span")` - white-space: nowrap; - font-weight: bold; - color: ${({ theme }) => theme.col.blueGrayDark}; - flex-grow: 1; -`; +const conceptName = tv({ + base: ["grow", "whitespace-nowrap", "font-bold", "text-primary-500"], +}); export const TimeConnectionModal = memo( ({ @@ -118,9 +103,10 @@ export const TimeConnectionModal = memo( return ( - - - +
+ o.value === aTimestamp)!} onChange={(opt) => { @@ -129,11 +115,14 @@ export const TimeConnectionModal = memo( } }} /> - {t("editorV2.dateRangeFrom")} - {a} - - - + {t("editorV2.dateRangeFrom")} + + {a} +
+
+ - - - o.value === operator)!} onChange={(opt) => { @@ -189,9 +181,10 @@ export const TimeConnectionModal = memo( } }} /> - - - +
+ o.value === bTimestamp)!} onChange={(opt) => { @@ -200,10 +193,12 @@ export const TimeConnectionModal = memo( } }} /> - {t("editorV2.dateRangeFrom")} - {b} - - + + {t("editorV2.dateRangeFrom")} + + {b} +
+
); }, diff --git a/frontend/src/js/previous-queries/list/AddFolderModal.tsx b/frontend/src/js/previous-queries/list/AddFolderModal.tsx index 57d8c269b0..e9b8cf43b9 100644 --- a/frontend/src/js/previous-queries/list/AddFolderModal.tsx +++ b/frontend/src/js/previous-queries/list/AddFolderModal.tsx @@ -1,4 +1,3 @@ -import styled from "@emotion/styled"; import { useState } from "react"; import { useTranslation } from "react-i18next"; @@ -7,22 +6,6 @@ import { TransparentButton } from "../../button/TransparentButton"; import Modal from "../../modal/Modal"; import InputPlain from "../../ui-components/InputPlain/InputPlain"; -const Buttons = styled("div")` - width: 100%; - display: flex; - align-items: center; - justify-content: space-between; - margin-top: 20px; -`; - -const SxPrimaryButton = styled(PrimaryButton)` - margin-left: 20px; -`; - -const Content = styled("div")` - max-width: 500px; -`; - interface Props { onClose: () => void; onSubmit: (folderName: string) => void; @@ -35,7 +18,7 @@ const AddFolderModal = ({ onClose, onSubmit, isValidName }: Props) => { return ( - +

{t("addFolderModal.description")}

{ @@ -50,16 +33,20 @@ const AddFolderModal = ({ onClose, onSubmit, isValidName }: Props) => { onChange={(value) => setFolderName((value as string | null) || "")} inputProps={{ autoFocus: true }} /> - +
{t("common.cancel")} - + {t("common.create")} - - + +
- +
); }; diff --git a/frontend/src/js/previous-queries/list/EditProjectItemFoldersModal.tsx b/frontend/src/js/previous-queries/list/EditProjectItemFoldersModal.tsx index 4582e255c0..50494f99bd 100644 --- a/frontend/src/js/previous-queries/list/EditProjectItemFoldersModal.tsx +++ b/frontend/src/js/previous-queries/list/EditProjectItemFoldersModal.tsx @@ -1,4 +1,3 @@ -import styled from "@emotion/styled"; import { useTranslation } from "react-i18next"; import Modal from "../../modal/Modal"; @@ -8,11 +7,6 @@ import { isFormConfig } from "./helpers"; import type { ProjectItemT } from "./ProjectItem"; import { useFolders } from "./selector"; -const SxEditableTagsForm = styled(EditableTagsForm)` - min-width: 300px; - max-width: 500px; -`; - const EditProjectItemFoldersModal = ({ item, onClose, @@ -43,7 +37,8 @@ const EditProjectItemFoldersModal = ({ headline={t("editPreviousQueryFoldersModal.headline")} subtitle={item.label} > - ` - display: inline-flex; - align-items: center; - padding: 2px 7px; - border-radius: ${({ theme }) => theme.borderRadius}; - font-size: ${({ theme }) => theme.font.sm}; - cursor: pointer; - font-style: ${({ special }) => (special ? "italic" : "inherit")}; - - background-color: ${({ theme, active }) => - active ? theme.col.grayLight : "transparent"}; - &:hover { - background-color: ${({ theme }) => theme.col.blueGrayVeryLight}; - } -`; - -const Text = styled("div")` - flex-shrink: 0; - color: ${({ theme }) => theme.col.black}; -`; - -const ResultCount = styled("span")` - flex-shrink: 0; - display: inline-flex; - align-items: center; - justify-content: center; - line-height: 1; - padding: 2px 0; - margin-right: 5px; - font-size: ${({ theme }) => theme.font.xs}; - border-radius: ${({ theme }) => theme.borderRadius}; - color: ${({ theme }) => theme.col.blueGrayDark}; - font-weight: 700; -`; +const resultCount = tv({ + base: [ + "shrink-0", + "inline-flex items-center justify-center", + "leading-none", + "py-[2px]", + "mr-[5px]", + "text-xs", + "rounded", + "text-primary-500", + "font-bold", + ], +}); const Folder = ({ className, - resultCount, + resultCount: count, resultWords, folder, active, @@ -65,24 +55,28 @@ const Folder = ({ onClick: () => void; }) => { return ( - - - {exists(resultCount) && {resultCount}} - + + {exists(count) && {count}} +
{!empty && resultWords.length > 0 ? ( ) : ( folder )} - - +
+ ); }; export default Folder; diff --git a/frontend/src/js/previous-queries/list/Folders.tsx b/frontend/src/js/previous-queries/list/Folders.tsx index fff8ab7c22..3f14f4a5a0 100644 --- a/frontend/src/js/previous-queries/list/Folders.tsx +++ b/frontend/src/js/previous-queries/list/Folders.tsx @@ -1,8 +1,8 @@ -import styled from "@emotion/styled"; import { faPlus, faTimes } from "@fortawesome/free-solid-svg-icons"; import { useCallback, useEffect, useRef, 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 IconButton from "../../button/IconButton"; @@ -35,69 +35,41 @@ const DROP_TYPES = [ DNDType.PREVIOUS_SECONDARY_ID_QUERY, ]; -const Root = styled("div")` - flex-shrink: 0; - height: 100%; - overflow: hidden; - border-right: none; - display: flex; - align-items: flex-start; - flex-direction: column; -`; +const root = tv({ + base: ["flex flex-col items-start", "shrink-0", "h-full", "overflow-hidden"], +}); -const SxIconButton = styled(IconButton)` - background-color: ${({ theme }) => theme.col.bg}; - padding: 2px 8px; - opacity: 1; - border-radius: 0; +// hidden until the surrounding folder dropzone (group/folder) is hovered +const deleteButton = tv({ + base: [ + "absolute top-0 right-0", + "hidden group-hover/folder:block", + "bg-bg-50", + "px-2 py-[2px]", + "opacity-100", + "rounded-none", + ], +}); - position: absolute; - right: 0px; - top: 0px; - display: none; -`; +const folderDropzone = tv({ + base: [ + "group/folder", + "relative", + "justify-start", + "mb-[2px]", + "cursor-pointer", + "hover:bg-gray-50", + ], +}); -const AddFolderIconButton = styled(IconButton)` - text-align: left; - padding: 4px 6px; -`; - -const Row = styled("div")` - display: flex; - align-items: flex-start; - margin-bottom: 12px; - min-width: 100px; - width: 100%; -`; - -const SxDropzone = styled(Dropzone)` - justify-content: flex-start; - margin-bottom: 2px; - position: relative; - cursor: pointer; - - &:hover { - background-color: ${({ theme }) => theme.col.grayVeryLight}; - .folder-delete-button { - display: block; - } - } -`; - -const SxPreviousQueriesFolder = styled(Folder)` - margin-bottom: 5px; -`; - -const ScrollContainer = styled("div")` - overflow-y: auto; - overflow-x: hidden; - flex-grow: 1; - width: 100%; - - display: flex; - align-items: flex-start; - flex-direction: column; -`; +const scrollContainer = tv({ + base: [ + "flex flex-col items-start", + "grow", + "w-full", + "overflow-y-auto overflow-x-hidden", + ], +}); const NARROW_WIDTH = 120; const useIsParentNarrow = () => { @@ -203,7 +175,7 @@ const Folders = ({ className }: { className?: string }) => { ); return ( - +
{folderToDelete && ( { }} /> )} - - + setShowAddFolderModal(true)} > {isNarrow ? t("folders.addShort") : t("folders.add")} - - + +
{showAddFolderModal && ( setShowAddFolderModal(false)} @@ -236,7 +212,8 @@ const Folders = ({ className }: { className?: string }) => { }} /> )} - { resultCount={searchResult ? searchResult.__all__ : null} resultWords={[]} /> - { resultCount={searchResult ? searchResult.__without_folder__ : null} resultWords={[]} /> - +
{folders.map((folder, i) => { return ( - { resultWords={searchResultWords} /> - { setFolderToDelete(folder); @@ -297,11 +276,11 @@ const Folders = ({ className }: { className?: string }) => { )} - + ); })} - - +
+ ); }; export default Folders; diff --git a/frontend/src/js/previous-queries/list/FoldersToggleButton.tsx b/frontend/src/js/previous-queries/list/FoldersToggleButton.tsx index 77222f4fb3..f466885cd0 100644 --- a/frontend/src/js/previous-queries/list/FoldersToggleButton.tsx +++ b/frontend/src/js/previous-queries/list/FoldersToggleButton.tsx @@ -1,15 +1,9 @@ -import styled from "@emotion/styled"; import { faFolder } from "@fortawesome/free-solid-svg-icons"; import { useTranslation } from "react-i18next"; import IconButton from "../../button/IconButton"; import WithTooltip from "../../tooltip/WithTooltip"; -const FoldersButton = styled(IconButton)` - padding: 9px 6px; - margin-right: 5px; -`; - const FoldersToggleButton = ({ className, active, @@ -26,7 +20,13 @@ const FoldersToggleButton = ({ text={t("previousQueriesFolderButton.tooltip")} className={className} > - + ); }; diff --git a/frontend/src/js/previous-queries/list/ProjectItem.tsx b/frontend/src/js/previous-queries/list/ProjectItem.tsx index da25e64864..ed9f941b51 100644 --- a/frontend/src/js/previous-queries/list/ProjectItem.tsx +++ b/frontend/src/js/previous-queries/list/ProjectItem.tsx @@ -1,4 +1,3 @@ -import styled from "@emotion/styled"; import { faCalendar, faFolder as faFolderRegular, @@ -14,6 +13,7 @@ import type { TFunction } from "i18next"; import { type Ref, useState } from "react"; import { useTranslation } from "react-i18next"; import { useSelector } from "react-redux"; +import { tv } from "tailwind-variants"; import type { ResultUrlWithLabel, SecondaryId } from "../../api/types"; import type { StateT } from "../../app/reducers"; import DownloadButton from "../../button/DownloadButton"; @@ -34,112 +34,62 @@ import type { FormConfigT, PreviousQueryT } from "./reducer"; export type ProjectItemT = PreviousQueryT | FormConfigT; -const Root = styled("div")<{ own?: boolean; system?: boolean }>` - margin: 0; - cursor: pointer; - border-radius: ${({ theme }) => theme.borderRadius}; - border: 1px solid ${({ theme }) => theme.col.grayLight}; - background-color: ${({ theme }) => theme.col.bg}; - box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.2); - overflow: hidden; - display: flex; - align-items: center; - - &:hover { - border-color: ${({ theme }) => theme.col.blueGray}; - } -`; - -const Gray = styled("div")` - color: ${({ theme }) => theme.col.gray}; - font-size: ${({ theme }) => theme.font.xs}; -`; - -const TopInfos = styled(Gray)` - line-height: 20px; - display: flex; - align-items: center; - justify-content: space-between; -`; - -const OwnerName = styled(Gray)` - flex-shrink: 0; - padding-left: 5px; -`; - -const SxQuerySymbol = styled(QuerySymbol)` - flex-shrink: 0; -`; - -const SxFormSymbol = styled(FormSymbol)` - flex-shrink: 0; -`; - -const TopRight = styled("div")` - display: flex; - align-items: center; - gap: 10px; - flex-shrink: 0; - margin-left: 5px; -`; -const TopLeft = styled("div")` - display: flex; - align-items: center; -`; - -const TooltipText = styled("div")` - font-weight: 400; - font-size: ${({ theme }) => theme.font.md}; - display: flex; - flex-direction: column; - align-items: flex-start; - padding: 8px 14px; -`; - -const ActiveFolders = styled("ul")` - margin: 6px 0 0; - text-align: left; - padding-left: 18px; -`; - -const NonBreakingText = styled("span")` - white-space: nowrap; -`; -const LabelRow = styled("div")` - display: flex; - width: 100%; - justify-content: space-between; - line-height: 24px; - margin: 2px 0; -`; - -const Content = styled("div")<{ own?: boolean; system?: boolean }>` - flex-grow: 1; - flex-shrink: 1; - padding: 4px 10px; - overflow: hidden; - border-left: ${({ theme, own, system }) => - own - ? `5px solid ${theme.col.blueGrayDark}` - : system - ? `5px solid ${theme.col.grayLight}` - : `1px solid ${theme.col.grayLight}`}; -`; - -const SxDownloadButton = styled(DownloadButton)` - white-space: nowrap; - button { - font-size: ${({ theme }) => theme.font.xs}; - } -`; - -const SxFaIcon = styled(FaIcon)` - opacity: 0.7; -`; - -const FoldersButton = styled(IconButton)` - margin-right: 10px; -`; +const root = tv({ + base: [ + "flex items-center", + "m-0", + "cursor-pointer", + "rounded", + "border border-gray-100 hover:border-primary-200", + "bg-bg-50", + "shadow-[0_1px_2px_0_rgba(0,0,0,0.2)]", + "overflow-hidden", + ], +}); + +const topInfos = tv({ + base: [ + "flex items-center justify-between", + "leading-5", + "text-gray-500", + "text-xs", + ], +}); + +const ownerName = tv({ + base: ["shrink-0", "pl-[5px]", "text-gray-500", "text-xs"], +}); + +const tooltipText = tv({ + base: [ + "flex flex-col items-start", + "px-[14px] py-2", + "font-normal", + "text-base", + ], +}); + +const labelRow = tv({ + base: ["flex justify-between", "w-full", "leading-6", "my-[2px]"], +}); + +const content = tv({ + base: [ + "grow shrink", + "px-[10px] py-1", + "overflow-hidden", + "border-l border-gray-100", + ], + variants: { + // later wins when both are set + system: { true: "border-l-[5px]" }, + own: { true: "border-l-[5px] border-primary-500" }, + }, +}); + +const downloadButton = tv({ + base: ["whitespace-nowrap", "[&_button]:text-xs"], +}); const getTopLeftLabel = ( item: ProjectItemT, @@ -169,16 +119,16 @@ const HighlightedText = ({ const FoldersTooltip = ({ folders }: { folders: string[] }) => { const { t } = useTranslation(); return ( - +
{t("previousQuery.editFolders")} {folders.length > 0 && ( - +
    {folders.map((f) => (
  • {f}
  • ))} - +
)} - +
); }; @@ -193,9 +143,9 @@ const ShareButton = ({ return ( +
{isShared ? t("common.shared") : t("common.share")} - +
} > { const { t } = useTranslation(); - if (!resultUrl) return {label}; + if (!resultUrl) return {label}; return ( - + {label} - + ); }; @@ -300,13 +257,18 @@ const ProjectItem = ({ const onRenameLabel = useRenameProjectItem(item); return ( - - {isForm ? : } - - - +
+ {isForm ? ( + + ) : ( + + )} +
+
+
}> - {hasNoDates && ( - + )}
- - +
+
{executedAt} {secondaryId && ( )} {item.own && } - - - +
+
+
- +
- - - - +
+
+
+ ); }; diff --git a/frontend/src/js/previous-queries/list/ProjectItemLabel.tsx b/frontend/src/js/previous-queries/list/ProjectItemLabel.tsx index eed6b54b23..0442242214 100644 --- a/frontend/src/js/previous-queries/list/ProjectItemLabel.tsx +++ b/frontend/src/js/previous-queries/list/ProjectItemLabel.tsx @@ -1,22 +1,12 @@ -import styled from "@emotion/styled"; import { useTranslation } from "react-i18next"; +import { tv } from "tailwind-variants"; import { Highlighter } from "../../common/components/Highlighter"; import EditableText from "../../ui-components/EditableText"; -const Text = styled("div")` - display: block; - font-weight: 400; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -`; -const SxEditableText = styled(EditableText)` - font-weight: 400; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -`; +const labelText = tv({ + base: ["font-normal", "whitespace-nowrap", "overflow-hidden text-ellipsis"], +}); const ProjectItemLabel = ({ mayEdit, @@ -40,7 +30,8 @@ const ProjectItemLabel = ({ const { t } = useTranslation(); return mayEdit ? ( - ) : ( - +
{highlightedWords.length > 0 ? ( ) : ( label )} - +
); }; diff --git a/frontend/src/js/previous-queries/list/ProjectItems.tsx b/frontend/src/js/previous-queries/list/ProjectItems.tsx index 8b4821e178..662c8367df 100644 --- a/frontend/src/js/previous-queries/list/ProjectItems.tsx +++ b/frontend/src/js/previous-queries/list/ProjectItems.tsx @@ -1,4 +1,3 @@ -import styled from "@emotion/styled"; import { useCallback, useEffect, @@ -7,6 +6,7 @@ import { useState, } from "react"; import { List, type RowComponentProps } from "react-window"; +import { tv } from "tailwind-variants"; import type { DatasetT } from "../../api/types"; import { useResizeObserver } from "../../common/helpers/useResizeObserver"; @@ -40,13 +40,12 @@ const ProjectItemRow = ({ ); +// must match root's py-1 const ROOT_PADDING_Y = 4; -const Root = styled("div")` - flex-grow: 1; - font-size: ${({ theme }) => theme.font.sm}; - padding: ${ROOT_PADDING_Y}px 0; -`; +const root = tv({ + base: ["grow", "py-1", "text-sm"], +}); export const ProjectItems = ({ datasetId, @@ -101,7 +100,8 @@ export const ProjectItems = ({ }, []); return ( - { if (!instance) { @@ -131,6 +131,6 @@ export const ProjectItems = ({ rowProps={{ items, setItemToShare, setItemToEditFolders }} /> )} - + ); }; diff --git a/frontend/src/js/previous-queries/list/ProjectItemsTab.tsx b/frontend/src/js/previous-queries/list/ProjectItemsTab.tsx index 4361b0831a..a5063b0e29 100644 --- a/frontend/src/js/previous-queries/list/ProjectItemsTab.tsx +++ b/frontend/src/js/previous-queries/list/ProjectItemsTab.tsx @@ -1,9 +1,8 @@ -import { css } from "@emotion/react"; -import styled from "@emotion/styled"; import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { useDispatch, useSelector } from "react-redux"; import { Group, Panel } from "react-resizable-panels"; +import { tv } from "tailwind-variants"; import type { DatasetT } from "../../api/types"; import type { StateT } from "../../app/reducers"; import { usePrevious } from "../../common/helpers/usePrevious"; @@ -27,65 +26,26 @@ import { ProjectItems } from "./ProjectItems"; import type { FormConfigT, PreviousQueryT } from "./reducer"; import { selectPreviousQueries } from "./selector"; -const ScrollContainer = styled("div")` - overflow-y: auto; - -webkit-overflow-scrolling: touch; - font-size: ${({ theme }) => theme.font.sm}; -`; - -const Row = styled("div")` - display: flex; - align-items: flex-start; - margin: 8px 10px 0; -`; - -const FoldersAndQueries = styled(Row)` - flex-grow: 1; - margin: 8px 8px 0 10px; - overflow: hidden; - position: relative; -`; -const SxProjectItemsSearchBox = styled(ProjectItemsSearchBox)` - flex-grow: 1; -`; - -const Filters = styled("div")` - display: flex; - align-items: flex-start; - margin: 8px 0; -`; -const SxProjectItemsFilter = styled(ProjectItemsFilter)` - display: flex; - align-items: flex-start; -`; - -const SxProjectItemsTypeFilter = styled(ProjectItemsTypeFilter)` - display: flex; - align-items: flex-start; - margin-right: 20px; - padding-right: 10px; -`; - -const SxUploadQueryResults = styled(UploadQueryResults)` - margin-left: 5px; -`; - -const SxFolders = styled(Folders)` - padding: 8px 8px 8px 0; -`; - -const Expand = styled("div")<{ areFoldersOpen?: boolean }>` - flex-grow: 1; - display: flex; - flex-direction: column; - height: 100%; - padding-right: 2px; - ${({ areFoldersOpen }) => - areFoldersOpen && - css` - padding-left: 8px; - `} -`; +const foldersAndQueries = tv({ + base: [ + "flex items-start", + "grow", + "mt-2 mr-2 mb-0 ml-[10px]", + "overflow-hidden", + "relative", + ], +}); + +const typeFilter = tv({ + base: ["flex items-start", "mr-5", "pr-[10px]"], +}); + +const expand = tv({ + base: ["flex flex-col", "grow", "h-full", "pr-[2px]"], + variants: { + areFoldersOpen: { true: "pl-2" }, + }, +}); interface PropsT { datasetId: DatasetT["id"] | null; @@ -110,17 +70,17 @@ const ProjectItemsTab = ({ datasetId }: PropsT) => { return ( <> - +
- + {hasPermissionToUpload && ( - + )} - - +
+
{ minSize="10" defaultSize={areFoldersOpen ? "25" : 0} > - + - - - - - - +
+
+ + +
+
{items.length === 0 && !loading && ( )} - +
- +
- +
); }; diff --git a/frontend/src/js/previous-queries/list/ShareProjectItemModal.tsx b/frontend/src/js/previous-queries/list/ShareProjectItemModal.tsx index 0b8f53c95d..20ee3d4870 100644 --- a/frontend/src/js/previous-queries/list/ShareProjectItemModal.tsx +++ b/frontend/src/js/previous-queries/list/ShareProjectItemModal.tsx @@ -1,4 +1,3 @@ -import styled from "@emotion/styled"; import { faCheck, faSpinner } from "@fortawesome/free-solid-svg-icons"; import { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; @@ -19,17 +18,6 @@ import { import { isFormConfig } from "./helpers"; import type { ProjectItemT } from "./ProjectItem"; -const Row = styled("div")` - width: 100%; - display: flex; - align-items: flex-end; -`; - -const SxIconButton = styled(IconButton)` - padding: 7px 10px; - margin-left: 3px; -`; - const getInitialUserGroupsValue = ( userGroups: UserGroupT[], projectItem?: ProjectItemT, @@ -150,7 +138,7 @@ const ShareProjectItemModal = ({ item, onClose }: PropsT) => { onShareClicked(); }} > - +
{ options={userGroupOptions} /> - - +
); diff --git a/frontend/src/js/previous-queries/upload/CSVColumnPicker.tsx b/frontend/src/js/previous-queries/upload/CSVColumnPicker.tsx index 8c8d4af716..a55315d922 100644 --- a/frontend/src/js/previous-queries/upload/CSVColumnPicker.tsx +++ b/frontend/src/js/previous-queries/upload/CSVColumnPicker.tsx @@ -1,4 +1,3 @@ -import styled from "@emotion/styled"; import { faCheckCircle, faDownload, @@ -24,80 +23,31 @@ import ScrollableList from "../../scrollable-list/ScrollableList"; import WithTooltip from "../../tooltip/WithTooltip"; import InputSelect from "../../ui-components/InputSelect/InputSelect"; -const Row = styled("div")` - display: flex; - align-items: flex-end; - justify-content: space-between; - margin-bottom: 15px; -`; - 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; - width: 150px; -`; - -const Padded = styled("span")` - padding: 0 6px; -`; -const SxPadded = styled(Padded)` - display: inline-block; - margin-top: 10px; -`; -const SxInputSelect = styled(InputSelect)` - width: 150px; - text-align: left; - display: inline-block; - margin-left: 15px; -`; - -const Msg = styled("p")` - margin: 10px 0 8px; - &:first-of-type { - margin-top: 0; - } - font-size: ${({ theme }) => theme.font.sm}; - display: flex; - align-items: center; -`; - -const PartialUploadResults = styled("div")` - box-shadow: 0 0 5px 0 rgb(0, 0, 0, 0.1); - padding: 15px; - margin-top: 15px; -`; +const th = tv({ + base: ["w-[150px]", "align-top", "text-xs"], +}); -const BigIcon = styled(FaIcon)` - font-size: ${({ theme }) => theme.font.lg}; - margin-right: 7px; -`; -const ErrorIcon = styled(BigIcon)` - color: ${({ theme }) => theme.col.red}; -`; -const SuccessIcon = styled(BigIcon)` - color: ${({ theme }) => theme.col.green}; -`; -const Buttons = styled("div")` - display: flex; - align-items: flex-end; - justify-content: flex-end; - margin-top: 12px; -`; +const msg = tv({ + base: ["flex items-center", "mt-[10px] mb-2 first-of-type:mt-0", "text-sm"], +}); -const SxPrimaryButton = styled(PrimaryButton)` - margin-left: 10px; -`; -const SxTransparentButton = styled(TransparentButton)` - margin-left: 10px; -`; +const partialUploadResults = tv({ + base: ["mt-[15px]", "p-[15px]", "shadow-[0_0_5px_0_rgba(0,0,0,0.1)]"], +}); -const DownloadUnresolvedButton = styled(TransparentButton)` - margin-right: auto; -`; +const bigIcon = tv({ + base: ["mr-[7px]", "text-xl"], + variants: { + kind: { + error: "text-red", + success: "text-green", + }, + }, +}); export interface QueryToUploadT { format: string[]; @@ -196,7 +146,7 @@ const CSVPreviewTable = ({ csv.slice(0, 1).map((row, j) => ( {row.map((cell, i) => ( - + - {cell} - + + {cell} + + ))} ))} @@ -226,7 +178,7 @@ const CSVPreviewTable = ({ {row.map((cell, i) => ( - {cell} + {cell} ))} @@ -235,7 +187,7 @@ const CSVPreviewTable = ({ {new Array(csv[0].length).fill(null).map((_, j) => ( - ... + ... ))} @@ -301,7 +253,7 @@ const CSVColumnPicker = ({ return (
- +
{file.name} @@ -312,7 +264,8 @@ const CSVColumnPicker = ({
{csv.length > 0 && ( - { if (val) setDelimiter(val.value as string); @@ -324,7 +277,7 @@ const CSVColumnPicker = ({ options={DELIMITER_OPTIONS} /> )} - +
{uploadResult && ( - - - {uploadResult.resolved > 0 && } +
+

+ {uploadResult.resolved > 0 && ( + + )} {t("csvColumnPicker.resolved", { count: uploadResult.resolved })} - +

{uploadResult.unreadableDate.length > 0 && ( <> - - +

+ {t("csvColumnPicker.unreadableDate", { count: uploadResult.unreadableDate.length, })} - +

0 && ( <> - - +

+ {t("csvColumnPicker.unresolvedId", { count: uploadResult.unresolvedId.length, })} - +

)} - +
)} - +
{uploadResult && (uploadResult.unreadableDate.length > 0 || uploadResult.unresolvedId.length > 0) && ( - + {" "} {t("uploadQueryResultsModal.downloadUnresolved", { count: uploadResult.unreadableDate.length + uploadResult.unresolvedId.length, })} - + )} {uploadResult && ( - + {loading ? ( ) : ( )}{" "} {t("uploadQueryResultsModal.uploadAgain")} - + )} {uploadResult ? ( - + {t("common.done")} - + ) : ( - + {loading ? ( ) : ( )}{" "} {t("uploadQueryResultsModal.upload")} - + )} - +
); }; diff --git a/frontend/src/js/previous-queries/upload/UploadQueryResults.tsx b/frontend/src/js/previous-queries/upload/UploadQueryResults.tsx index 4345cc03e2..c76577ac23 100644 --- a/frontend/src/js/previous-queries/upload/UploadQueryResults.tsx +++ b/frontend/src/js/previous-queries/upload/UploadQueryResults.tsx @@ -1,4 +1,3 @@ -import styled from "@emotion/styled"; import { faUpload } from "@fortawesome/free-solid-svg-icons"; import { useState } from "react"; import { useTranslation } from "react-i18next"; @@ -19,10 +18,6 @@ import { useLoadQueries } from "../list/actions"; import type { QueryToUploadT } from "./CSVColumnPicker"; import UploadQueryResultsModal from "./UploadQueryResultsModal"; -const SxIconButton = styled(IconButton)` - padding: 9px 6px; -`; - const UploadQueryResults = ({ className, datasetId, @@ -82,7 +77,8 @@ const UploadQueryResults = ({ return (
- setIsModalOpen(true)} diff --git a/frontend/src/js/previous-queries/upload/UploadQueryResultsModal.tsx b/frontend/src/js/previous-queries/upload/UploadQueryResultsModal.tsx index 77f447fe3b..cc9ddbab9f 100644 --- a/frontend/src/js/previous-queries/upload/UploadQueryResultsModal.tsx +++ b/frontend/src/js/previous-queries/upload/UploadQueryResultsModal.tsx @@ -1,7 +1,7 @@ -import styled from "@emotion/styled"; import { faCheckCircle } from "@fortawesome/free-solid-svg-icons"; import { useState } from "react"; import { useTranslation } from "react-i18next"; +import { tv } from "tailwind-variants"; import type { QueryUploadConfigT, UploadQueryResponseT } from "../../api/types"; import FaIcon from "../../icon/FaIcon"; @@ -11,28 +11,13 @@ import DropzoneWithFileInput from "../../ui-components/DropzoneWithFileInput"; import CSVColumnPicker, { type QueryToUploadT } from "./CSVColumnPicker"; -const Root = styled("div")``; +const successIcon = tv({ + base: ["block", "mx-auto mb-[10px]", "text-[40px]", "text-green"], +}); -const Success = styled("div")` - margin: 25px 0; -`; - -const StyledFaIcon = styled(FaIcon)` - font-size: 40px; - display: block; - margin: 0 auto 10px; - color: ${({ theme }) => theme.col.green}; -`; - -const SuccessMsg = styled("p")` - margin: 0; -`; - -const SxDropzoneWithFileInput = styled(DropzoneWithFileInput)` - padding: 180px 250px; - width: 100%; - cursor: pointer; -`; +const dropzone = tv({ + base: ["w-full", "cursor-pointer", "px-[250px] py-[180px]"], +}); const UploadQueryResultsModal = ({ loading, @@ -72,16 +57,16 @@ const UploadQueryResultsModal = ({ } > - +
{fullUploadSuccess ? ( - - - +
+ +

{t("uploadQueryResultsModal.uploadSucceeded", { count: uploadResult?.resolved || 0, })} - - +

+
) : (
{file && ( @@ -99,7 +84,8 @@ const UploadQueryResultsModal = ({ /> )} {!file && ( - { if (item.type === "__NATIVE_FILE__") { setFile(item.files[0]); @@ -109,11 +95,11 @@ const UploadQueryResultsModal = ({ accept="text/csv" > {() => t("uploadQueryResultsModal.dropzone")} - + )}
)} - +
); }; diff --git a/frontend/src/js/standard-query-editor/EmptyQueryEditorDropzone.tsx b/frontend/src/js/standard-query-editor/EmptyQueryEditorDropzone.tsx index 834f99e958..a525642d00 100644 --- a/frontend/src/js/standard-query-editor/EmptyQueryEditorDropzone.tsx +++ b/frontend/src/js/standard-query-editor/EmptyQueryEditorDropzone.tsx @@ -1,4 +1,3 @@ -import styled from "@emotion/styled"; import { faFile } from "@fortawesome/free-regular-svg-icons"; import { faArrowRight, @@ -8,91 +7,80 @@ import { } from "@fortawesome/free-solid-svg-icons"; import { memo } from "react"; import { useTranslation } from "react-i18next"; +import { tv } from "tailwind-variants"; import FaIcon from "../icon/FaIcon"; -const TextInitial = styled("div")` - width: 100%; - font-size: ${({ theme }) => theme.font.lg}; - padding: 30px; - font-weight: 400; - display: grid; - grid-template-areas: "free headline" "arrow description"; - gap: 12px 20px; +const textInitial = tv({ + base: [ + "grid", + "[grid-template-areas:'free_headline'_'arrow_description']", + "gap-y-3 gap-x-5", + "w-full", + "p-[30px]", + "text-xl", + "font-normal", + ], +}); + +const arrowRight = tv({ + base: ["[grid-area:arrow]", "text-[140px]", "text-gray-100"], +}); + +const headline = tv({ + base: ["[grid-area:headline]", "text-2xl", "leading-tight", "font-bold"], +}); - p { - margin: 0; - } -`; -const ArrowRight = styled(FaIcon)` - font-size: 140px; - color: ${({ theme }) => theme.col.grayLight}; - grid-area: arrow; -`; -const Headline = styled("h2")` - grid-area: headline; -`; -const Grid = styled("div")` - display: grid; - gap: 5px; - align-items: center; - grid-template-columns: auto 1fr; - margin-top: 10px; -`; -const Row = styled("div")` - display: flex; - gap: 6px; - align-items: center; - justify-content: flex-end; -`; -const Description = styled("div")` - grid-area: description; -`; -const IconInABox = styled("div")` - border: 1px solid ${({ theme }) => theme.col.grayLight}; - width: 30px; - height: 30px; - display: flex; - align-items: center; - justify-content: center; - border-radius: ${({ theme }) => theme.borderRadius}; -`; +const grid = tv({ + base: ["grid items-center", "grid-cols-[auto_1fr]", "gap-[5px]", "mt-[10px]"], +}); + +const row = tv({ + base: ["flex items-center justify-end", "gap-[6px]"], +}); + +const iconInABox = tv({ + base: [ + "flex items-center justify-center", + "h-[30px] w-[30px]", + "border border-gray-100", + "rounded", + ], +}); export const EmptyQueryEditorDropzone = memo(() => { const { t } = useTranslation(); return ( - - - {t("dropzone.explanation")} - - - +
+

{t("dropzone.explanation")}

+ +

{t("dropzone.dropIntoThisArea")}

- - - +
+
+
- - +
+
- - +
+
{t("dropzone.aConcept")} - - +
+
- - +
+
{t("dropzone.aQuery")} - - +
+
- - +
+
{t("dropzone.aConceptList")} - - - +
+
+
); }); diff --git a/frontend/src/js/standard-query-editor/ExpandPreviousQueryModal.tsx b/frontend/src/js/standard-query-editor/ExpandPreviousQueryModal.tsx index 2f34fc136d..8f7d782c80 100644 --- a/frontend/src/js/standard-query-editor/ExpandPreviousQueryModal.tsx +++ b/frontend/src/js/standard-query-editor/ExpandPreviousQueryModal.tsx @@ -1,20 +1,13 @@ -import styled from "@emotion/styled"; import { useTranslation } from "react-i18next"; +import { tv } from "tailwind-variants"; import PrimaryButton from "../button/PrimaryButton"; import { TransparentButton } from "../button/TransparentButton"; import Modal from "../modal/Modal"; -const Description = styled.p` - max-width: 400px; - margin: 0 0 20px; -`; - -const Buttons = styled.div` - display: flex; - align-items: center; - justify-content: space-between; -`; +const description = tv({ + base: ["max-w-[400px]", "mb-5"], +}); const ExpandPreviousQueryModal = ({ onClose, @@ -39,15 +32,17 @@ const ExpandPreviousQueryModal = ({ onAccept(); }} > - {t("expandPreviousQueryModal.description")} - +

+ {t("expandPreviousQueryModal.description")} +

+
{t("common.cancel")} {t("expandPreviousQueryModal.submit")} - +
); diff --git a/frontend/src/js/standard-query-editor/Query.tsx b/frontend/src/js/standard-query-editor/Query.tsx index 0c5869b4e3..f52f344194 100644 --- a/frontend/src/js/standard-query-editor/Query.tsx +++ b/frontend/src/js/standard-query-editor/Query.tsx @@ -1,7 +1,7 @@ -import styled from "@emotion/styled"; import { Fragment, useCallback, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { useDispatch, useSelector } from "react-redux"; +import { tv } from "tailwind-variants"; import type { QueryT } from "../api/types"; import type { StateT } from "../app/reducers"; @@ -83,28 +83,13 @@ const useImport = () => { }; }; -const Container = styled("div")` - height: 100%; - display: flex; - flex-direction: column; -`; +const groups = tv({ + base: ["flex flex-row items-start", "grow", "mb-[10px]", "overflow-auto"], +}); -const Groups = styled("div")` - display: flex; - flex-direction: row; - align-items: flex-start; - margin: 0 0 10px; - flex-grow: 1; - overflow: auto; -`; - -const QueryGroupConnector = styled("p")` - padding: 110px 6px 0; - margin: 0; - font-size: ${({ theme }) => theme.font.sm}; - color: ${({ theme }) => theme.col.gray}; - text-align: center; -`; +const queryGroupConnector = tv({ + base: ["pt-[110px] px-[6px]", "text-sm", "text-gray-500", "text-center"], +}); const Query = ({ setEditedNode, @@ -205,7 +190,7 @@ const Query = ({ } return ( - +
{dropFileModalOpen && ( - +
{query.map((group, andIdx) => ( - +

{t("common.and")} - +

))} - +
)} - +
); }; diff --git a/frontend/src/js/standard-query-editor/QueryAndDropzone.tsx b/frontend/src/js/standard-query-editor/QueryAndDropzone.tsx index 98e0771f34..dbcdf1e2e9 100644 --- a/frontend/src/js/standard-query-editor/QueryAndDropzone.tsx +++ b/frontend/src/js/standard-query-editor/QueryAndDropzone.tsx @@ -1,4 +1,3 @@ -import styled from "@emotion/styled"; import { memo } from "react"; import { useTranslation } from "react-i18next"; @@ -8,14 +7,6 @@ import WithTooltip from "../tooltip/WithTooltip"; import QueryEditorDropzone from "./QueryEditorDropzone"; import type { DragItemConceptTreeNode, DragItemQuery } from "./types"; -const PaddedTop = styled("div")` - padding-top: 70px; -`; - -const SxWithTooltip = styled(WithTooltip)` - display: block !important; -`; - interface Props { onDropFile: (file: File, andIdx?: number) => Promise; onDropAndNode: (node: DragItemQuery | DragItemConceptTreeNode) => void; @@ -32,8 +23,8 @@ const QueryAndDropzone = ({ const { t } = useTranslation(); return ( - - +
+ - - + +
); }; diff --git a/frontend/src/js/standard-query-editor/QueryEditor.tsx b/frontend/src/js/standard-query-editor/QueryEditor.tsx index 4672fbdf7c..9e418e92ac 100644 --- a/frontend/src/js/standard-query-editor/QueryEditor.tsx +++ b/frontend/src/js/standard-query-editor/QueryEditor.tsx @@ -1,17 +1,14 @@ -import styled from "@emotion/styled"; import { useCallback, useEffect, useState } from "react"; +import { tv } from "tailwind-variants"; import { useDatasetId } from "../dataset/selectors"; import Query from "./Query"; import StandardQueryNodeEditor from "./StandardQueryNodeEditor"; -const Root = styled("div")` - flex-grow: 1; - height: 100%; - padding: 8px 10px 10px 10px; - overflow: hidden; -`; +const root = tv({ + base: ["grow", "h-full", "pt-2 px-[10px] pb-[10px]", "overflow-hidden"], +}); export const QueryEditor = () => { const [editedNode, setEditedNode] = useState<{ @@ -28,11 +25,11 @@ export const QueryEditor = () => { }, [datasetId, onClose]); return ( - +
{editedNode && ( )} - +
); }; diff --git a/frontend/src/js/standard-query-editor/QueryEditorDropzone.tsx b/frontend/src/js/standard-query-editor/QueryEditorDropzone.tsx index 2fcc3aae73..3c10472a4a 100644 --- a/frontend/src/js/standard-query-editor/QueryEditorDropzone.tsx +++ b/frontend/src/js/standard-query-editor/QueryEditorDropzone.tsx @@ -1,7 +1,6 @@ -import { css } from "@emotion/react"; -import styled from "@emotion/styled"; import { memo, type Ref, useCallback } from "react"; import { useTranslation } from "react-i18next"; +import { tv } from "tailwind-variants"; import type { QueryIdT } from "../api/types"; import { DNDType } from "../common/constants/dndTypes"; @@ -19,29 +18,12 @@ const DROP_TYPES = [ DNDType.PREVIOUS_SECONDARY_ID_QUERY, ]; -const SxDropzoneWithFileInput = styled(DropzoneWithFileInput)<{ - isInitial?: boolean; - isAnd?: boolean; -}>` - ${({ isInitial }) => - isInitial && - css` - height: 100%; - `}; - - ${({ isAnd }) => - isAnd && - css` - height: 100px; - white-space: nowrap; - width: initial; - `}; -`; - -const Text = styled("p")` - margin: 0; - font-size: ${({ theme }) => theme.font.sm}; -`; +const dropzone = tv({ + variants: { + isInitial: { true: "h-full" }, + isAnd: { true: ["h-[100px]", "whitespace-nowrap", "w-[initial]"] }, + }, +}); interface Props { className?: string; @@ -78,10 +60,9 @@ const QueryEditorDropzone = ({ ); return ( - onDrop(item as StandardQueryNodeT | DragItemFile)} @@ -93,10 +74,12 @@ const QueryEditorDropzone = ({ {() => ( <> {isInitial && } - {!isInitial && {t("dropzone.dragElementPlease")}} + {!isInitial && ( +

{t("dropzone.dragElementPlease")}

+ )} )} -
+ ); }; diff --git a/frontend/src/js/standard-query-editor/QueryGroup.tsx b/frontend/src/js/standard-query-editor/QueryGroup.tsx index 9677dd8dd8..ac7ce05386 100644 --- a/frontend/src/js/standard-query-editor/QueryGroup.tsx +++ b/frontend/src/js/standard-query-editor/QueryGroup.tsx @@ -1,6 +1,6 @@ -import styled from "@emotion/styled"; import { useCallback, useMemo } from "react"; import { useTranslation } from "react-i18next"; +import { tv } from "tailwind-variants"; import type { DateRangeT, QueryT } from "../api/types"; import type { PreviousQueryT } from "../previous-queries/list/reducer"; @@ -11,38 +11,27 @@ import QueryGroupActions from "./QueryGroupActions"; import QueryNode from "./QueryNode"; import type { QueryGroupType, StandardQueryNodeT } from "./types"; -const Root = styled("div")` - font-size: ${({ theme }) => theme.font.sm}; - max-width: 250px; -`; - -const Group = styled("div")<{ excluded?: boolean }>` - position: relative; - padding: 6px 8px 8px; - background-color: ${({ theme }) => theme.col.bg}; - border: ${({ theme, excluded }) => - excluded - ? `2px solid ${theme.col.red}` - : `1px solid ${theme.col.grayLight}`}; - box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.12); - text-align: center; - border-radius: ${({ theme }) => theme.borderRadius}; - width: 220px; -`; - -const QueryOrConnector = styled("p")` - margin: 0; - font-size: ${({ theme }) => theme.font.sm}; - color: ${({ theme }) => theme.col.gray}; - text-align: center; -`; +const groupBox = tv({ + base: [ + "relative", + "w-[220px]", + "pt-[6px] px-2 pb-2", + "bg-bg-50", + "shadow-[0_0_10px_0_rgba(0,0,0,0.12)]", + "rounded", + "text-center", + ], + variants: { + excluded: { + true: "border-2 border-red", + false: "border border-gray-100", + }, + }, +}); -// To override tippy here. -// Maybe also possible in another way by adjusting -// QueryEditorDropzone styles -const SxWithTooltip = styled(WithTooltip)` - display: block !important; -`; +const queryOrConnector = tv({ + base: ["text-sm", "text-gray-500", "text-center"], +}); const isDateActive = (dateRange?: DateRangeT) => { return !!dateRange && (!!dateRange.min || !!dateRange.max); @@ -112,8 +101,9 @@ const QueryGroup = ({ ); return ( - - +
+ {/* block! overrides tippy's inline-block wrapper */} + - - {t("common.or")} - + +

{t("common.or")}

+
{orIdx !== group.elements.length - 1 && ( - +

{t("common.or")} - +

)}
)), @@ -163,8 +156,8 @@ const QueryGroup = ({ onExpandClick, ], )} - - +
+
); }; diff --git a/frontend/src/js/standard-query-editor/QueryGroupActions.tsx b/frontend/src/js/standard-query-editor/QueryGroupActions.tsx index ef9f487ce2..96b1828bdd 100644 --- a/frontend/src/js/standard-query-editor/QueryGroupActions.tsx +++ b/frontend/src/js/standard-query-editor/QueryGroupActions.tsx @@ -1,49 +1,37 @@ -import styled from "@emotion/styled"; import { faCalendar } from "@fortawesome/free-regular-svg-icons"; import { faBan, faTimes } from "@fortawesome/free-solid-svg-icons"; import { memo } from "react"; import { useTranslation } from "react-i18next"; +import { tv } from "tailwind-variants"; import IconButton from "../button/IconButton"; import WithTooltip from "../tooltip/WithTooltip"; -const Actions = styled("div")` - margin: 0 0 6px; - text-align: left; - height: 18px; // To provide enough space when only --right is available -`; +// h-[18px]: to provide enough space when only the right side is rendered +const actions = tv({ + base: ["mb-[6px]", "h-[18px]", "text-left"], +}); -const Right = styled("div")` - position: absolute; - right: 7px; - top: 5px; -`; +const dateButton = tv({ + base: ["mr-[5px]", "px-[3px] py-0"], + variants: { + active: { + true: "underline", + false: "no-underline", + }, + }, +}); -const StyledIconButton = styled(IconButton)` - margin-right: 5px; - padding: 0 3px; - - text-decoration: ${({ active }) => (active ? "underline" : "initial")}; -`; - -const RedIconButton = styled(IconButton)` - margin-right: 5px; - padding: 0 3px; - - color: ${({ active, theme }) => (active ? theme.col.red : theme.col.black)}; - svg { - color: ${({ active, theme }) => (active ? theme.col.red : theme.col.black)}; - } - - &:hover { - opacity: 0.7; - color: ${({ active, theme }) => (active ? theme.col.red : theme.col.black)}; - svg { - color: ${({ active, theme }) => - active ? theme.col.red : theme.col.black}; - } - } -`; +const excludeButton = tv({ + base: ["mr-[5px]", "px-[3px] py-0", "hover:opacity-70"], + variants: { + active: { + // beats IconButton's own colors (incl. its red flag) via merge + true: ["text-red hover:text-red", "[&_svg]:text-red"], + false: ["text-gray-800 hover:text-gray-800", "[&_svg]:text-gray-800"], + }, + }, +}); interface PropsT { excludeActive: boolean; @@ -63,10 +51,11 @@ const QueryGroupActions = ({ const { t } = useTranslation(); return ( - +
- {t("queryEditor.exclude")} - + - {t("queryEditor.date")} - +
- +
- - +
+
); }; diff --git a/frontend/src/js/standard-query-editor/QueryHeader.tsx b/frontend/src/js/standard-query-editor/QueryHeader.tsx index 479ade802a..f36bac11ce 100644 --- a/frontend/src/js/standard-query-editor/QueryHeader.tsx +++ b/frontend/src/js/standard-query-editor/QueryHeader.tsx @@ -1,20 +1,12 @@ -import styled from "@emotion/styled"; import { memo } from "react"; import QueryClearButton from "./QueryClearButton"; -const Container = styled("div")` - display: flex; - align-items: center; - justify-content: flex-end; - margin-bottom: 5px; -`; - const QueryHeader = () => { return ( - +
- +
); }; diff --git a/frontend/src/js/standard-query-editor/QueryNode.tsx b/frontend/src/js/standard-query-editor/QueryNode.tsx index 7c3b0b026d..902f781c72 100644 --- a/frontend/src/js/standard-query-editor/QueryNode.tsx +++ b/frontend/src/js/standard-query-editor/QueryNode.tsx @@ -1,7 +1,7 @@ -import styled from "@emotion/styled"; import { memo, useCallback, useRef } from "react"; import { useDrag } from "react-dnd"; import { useSelector } from "react-redux"; +import { tv } from "tailwind-variants"; import type { QueryT } from "../api/types"; import { getWidthAndHeight } from "../app/DndProvider"; @@ -20,31 +20,26 @@ import QueryNodeActions from "./QueryNodeActions"; import QueryNodeContent from "./QueryNodeContent"; import type { StandardQueryNodeT } from "./types"; -const FlexHoverNavigatable = styled(HoverNavigatable)` - display: flex; - width: 100%; -`; - -const Root = styled("div")<{ - active?: boolean; -}>` - position: relative; - width: 100%; - margin: 0 auto; - display: grid; - grid-template-columns: 1fr auto; - - padding: 7px; - font-size: ${({ theme }) => theme.font.sm}; - cursor: pointer; - text-align: left; - border-radius: ${({ theme }) => theme.borderRadius}; - transition: background-color ${({ theme }) => theme.transitionTime}; - border: ${({ theme, active }) => - active - ? `2px solid ${theme.col.blueGrayDark}` - : `1px solid ${theme.col.grayMediumLight}`}; -`; +const root = tv({ + base: [ + "relative", + "grid grid-cols-[1fr_auto]", + "w-full", + "mx-auto", + "p-[7px]", + "rounded", + "text-sm", + "text-left", + "cursor-pointer", + "transition-[background-color] duration-100", + ], + variants: { + active: { + true: "border-2 border-primary-500", + false: "border border-gray-400", + }, + }, +}); interface PropsT { node: StandardQueryNodeT; @@ -174,17 +169,20 @@ const QueryNode = ({ : undefined; const QueryNodeRoot = ( - canNodeBeDropped(node, item)} highlightDroppable > - { ref.current = instance; drag(instance); }} - active={active} onClick={node.error ? undefined : () => onEditClick(andIdx, orIdx)} > - - +
+ ); if (nodeIsConceptQueryNode(node)) { diff --git a/frontend/src/js/standard-query-editor/QueryNodeActions.tsx b/frontend/src/js/standard-query-editor/QueryNodeActions.tsx index 39e1007a97..14e700e50a 100644 --- a/frontend/src/js/standard-query-editor/QueryNodeActions.tsx +++ b/frontend/src/js/standard-query-editor/QueryNodeActions.tsx @@ -1,4 +1,3 @@ -import styled from "@emotion/styled"; import { faCalendar } from "@fortawesome/free-regular-svg-icons"; import { faExpandArrowsAlt, @@ -8,40 +7,26 @@ import { } from "@fortawesome/free-solid-svg-icons"; import { memo } from "react"; import { useTranslation } from "react-i18next"; +import { tv } from "tailwind-variants"; import IconButton from "../button/IconButton"; import FaIcon from "../icon/FaIcon"; import WithTooltip from "../tooltip/WithTooltip"; -const Actions = styled("div")` - display: flex; - flex-direction: column; - align-items: center; - justify-content: flex-start; -`; +const actionButton = tv({ + base: "px-[6px] py-1", +}); -const StyledFaIcon = styled(FaIcon)` - margin: 7px 6px 4px; -`; - -const StyledIconButton = styled(IconButton)` - padding: 4px 6px 4px; -`; - -const RelativeContainer = styled.div` - position: relative; -`; -const CrossedOut = styled.div` - position: absolute; - top: 40%; - left: 10%; - width: 22px; - height: 3px; - transform: rotate(135deg); - background-color: ${({ theme }) => theme.col.red}; - opacity: 0.5; - pointer-events: none; -`; +const crossedOut = tv({ + base: [ + "absolute top-[40%] left-[10%]", + "h-[3px] w-[22px]", + "rotate-[135deg]", + "bg-red", + "opacity-50", + "pointer-events-none", + ], +}); interface Props { andIdx: number; @@ -63,9 +48,10 @@ const QueryNodeActions = (props: Props) => { const { t } = useTranslation(); return ( - +
- { e.stopPropagation(); @@ -75,7 +61,8 @@ const QueryNodeActions = (props: Props) => { {props.excludeTimestamps && ( - { @@ -87,12 +74,13 @@ const QueryNodeActions = (props: Props) => { )} {!props.error && !!props.previousQueryLoading && ( - + )} {!props.error && props.isExpandable && !props.previousQueryLoading && ( - { e.stopPropagation(); @@ -109,8 +97,9 @@ const QueryNodeActions = (props: Props) => { : t("queryEditor.hasSecondaryId") } > - - + { @@ -118,11 +107,11 @@ const QueryNodeActions = (props: Props) => { props.onToggleSecondaryIdExclude(props.andIdx, props.orIdx); }} /> - {props.excludeFromSecondaryId && } - + {props.excludeFromSecondaryId &&
} +
)} - +
); }; diff --git a/frontend/src/js/standard-query-editor/QueryNodeContent.tsx b/frontend/src/js/standard-query-editor/QueryNodeContent.tsx index e6b7fd043b..0128cc1617 100644 --- a/frontend/src/js/standard-query-editor/QueryNodeContent.tsx +++ b/frontend/src/js/standard-query-editor/QueryNodeContent.tsx @@ -1,51 +1,47 @@ -import styled from "@emotion/styled"; import { memo } from "react"; import { useTranslation } from "react-i18next"; +import { tv } from "tailwind-variants"; import ErrorMessage from "../error-message/ErrorMessage"; import WithTooltip from "../tooltip/WithTooltip"; -const Label = styled("p")` - margin: 0; - word-break: break-word; - line-height: 1.2; - font-size: ${({ theme }) => theme.font.md}; -`; -const Description = styled("p")` - margin: 3px 0 0; - word-break: break-word; - line-height: 1.2; - text-transform: uppercase; - font-size: ${({ theme }) => theme.font.xs}; -`; +// tv consts named *Text to not shadow the label/description props +const labelText = tv({ + base: ["[word-break:break-word]", "leading-[1.2]", "text-base"], +}); -const PreviousQueryLabel = styled("p")` - margin: 0 0 3px; - line-height: 1.2; - font-size: ${({ theme }) => theme.font.xs}; - text-transform: uppercase; - font-weight: 700; - color: ${({ theme }) => theme.col.blueGrayDark}; -`; +const descriptionText = tv({ + base: [ + "mt-[3px]", + "[word-break:break-word]", + "leading-[1.2]", + "uppercase", + "text-xs", + ], +}); -const StyledErrorMessage = styled(ErrorMessage)` - margin: 0; -`; +const previousQueryLabel = tv({ + base: [ + "mb-[3px]", + "leading-[1.2]", + "text-xs", + "uppercase", + "font-bold", + "text-primary-500", + ], +}); -const RootNode = styled("p")` - margin: 0 0 4px; - line-height: 1; - text-transform: uppercase; - font-weight: 700; - font-size: ${({ theme }) => theme.font.xs}; - color: ${({ theme }) => theme.col.blueGrayDark}; - word-break: break-word; -`; - -const Node = styled("div")` - flex-grow: 1; - padding-top: 2px; -`; +const rootNode = tv({ + base: [ + "mb-1", + "leading-none", + "uppercase", + "font-bold", + "text-xs", + "text-primary-500", + "[word-break:break-word]", + ], +}); interface Props { tooltipText?: string; @@ -68,22 +64,22 @@ const QueryNodeContent = ({ return ( - +
{!isConceptQueryNode && ( - +

{t("queryEditor.previousQuery")} - +

)} {error ? ( - + ) : ( <> - {rootNodeLabel && {rootNodeLabel}} - - {description && {description}} + {rootNodeLabel &&

{rootNodeLabel}

} +

{label}

+ {description &&

{description}

} )} - +
); }; diff --git a/frontend/src/js/standard-query-editor/SecondaryIdSelector.tsx b/frontend/src/js/standard-query-editor/SecondaryIdSelector.tsx index f313df436b..1235428034 100644 --- a/frontend/src/js/standard-query-editor/SecondaryIdSelector.tsx +++ b/frontend/src/js/standard-query-editor/SecondaryIdSelector.tsx @@ -1,8 +1,8 @@ -import styled from "@emotion/styled"; import { faMicroscope } from "@fortawesome/free-solid-svg-icons"; import { memo, useCallback, useEffect, useMemo, useRef } from "react"; import { useTranslation } from "react-i18next"; import { useDispatch, useSelector } from "react-redux"; +import { tv } from "tailwind-variants"; import type { SecondaryId } from "../api/types"; import type { StateT } from "../app/reducers"; @@ -16,20 +16,25 @@ import { setSelectedSecondaryId } from "./actions"; import type { StandardQueryStateT } from "./queryReducer"; import type { SelectedSecondaryIdStateT } from "./selectedSecondaryIdReducer"; -const Headline = styled.h3<{ active?: boolean }>` - font-size: ${({ theme }) => theme.font.sm}; - margin: 0; - text-transform: uppercase; - transition: color ${({ theme }) => theme.transitionTime}; - color: ${({ theme, active }) => - active ? theme.col.blueGrayDark : theme.col.gray}; -`; - -const SxFaIcon = styled(FaIcon)<{ active?: boolean }>` - transition: color ${({ theme }) => theme.transitionTime}; - color: ${({ theme, active }) => - active ? theme.col.blueGrayDark : theme.col.gray}; -`; +const headline = tv({ + base: ["m-0", "text-sm", "uppercase", "transition-[color] duration-100"], + variants: { + active: { + true: "text-primary-500", + false: "text-gray-500", + }, + }, +}); + +const headlineIcon = tv({ + base: "transition-[color] duration-100", + variants: { + active: { + true: "text-primary-500", + false: "text-gray-500", + }, + }, +}); const SecondaryIdSelector = () => { const { t } = useTranslation(); @@ -154,11 +159,15 @@ const SecondaryIdSelectorUI = memo( return (
- - +

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