diff --git a/frontend/src/js/authorization/LoginPage.tsx b/frontend/src/js/authorization/LoginPage.tsx index b819d57c3e..c908448716 100644 --- a/frontend/src/js/authorization/LoginPage.tsx +++ b/frontend/src/js/authorization/LoginPage.tsx @@ -42,9 +42,7 @@ const form = tv({ ], }); -const submitButton = tv({ - base: ["flex items-center justify-center", "mt-[35px]", "w-[255px]"], -}); +const submitButton = tv({ base: ["grid", "mt-[35px]", "w-[255px]"] }); const LoginPage = () => { const [user, setUser] = useState(""); @@ -115,16 +113,20 @@ const LoginPage = () => { disabled: loading, }} /> - +
+ +
diff --git a/frontend/src/js/button/BadgeToggleButton.tsx b/frontend/src/js/button/BadgeToggleButton.tsx index 82bf3c0e94..ca6139eef1 100644 --- a/frontend/src/js/button/BadgeToggleButton.tsx +++ b/frontend/src/js/button/BadgeToggleButton.tsx @@ -1,26 +1,22 @@ import type { ReactNode } from "react"; +import { ToggleButton } from "react-aria-components"; import { useHotkeys } from "react-hotkeys-hook"; - import { tv } from "tailwind-variants"; -import { Button } from "../ui-components/Button"; - +// a chip that toggles: dotted while off, solid in the primary color while on const badgeToggleButton = tv({ - base: ["h-auto px-1 py-px", "text-sm", "font-bold"], - 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", - ], - }, - }, + base: [ + "inline-flex items-center", + "rounded", + "px-1 py-px", + "border-2 border-dotted border-gray-100", + "text-sm font-bold text-gray-500 whitespace-nowrap", + "cursor-pointer", + "hover:bg-bg-50", + "data-selected:border-solid data-selected:border-primary-500", + "data-selected:bg-white data-selected:hover:bg-gray-50", + "data-selected:text-primary-500", + ], }); const superScript = tv({ @@ -51,16 +47,14 @@ export const BadgeToggleButton = ({ useHotkeys(hotkey || "", onClick, { enabled: !!hotkey }, [hotkey, onClick]); return ( - + ); }; diff --git a/frontend/src/js/button/DownloadButton.tsx b/frontend/src/js/button/DownloadButton.tsx index 4dbd6587cd..270e06af08 100644 --- a/frontend/src/js/button/DownloadButton.tsx +++ b/frontend/src/js/button/DownloadButton.tsx @@ -13,7 +13,7 @@ import { tv } from "tailwind-variants"; import type { ResultUrlWithLabel } from "../api/types"; import { AuthTokenContext } from "../authorization/AuthTokenProvider"; import { getEnding } from "../query-runner/DownloadResultsDropdownButton"; -import { Button, type ButtonProps } from "../ui-components/Button"; +import { Button } from "../ui-components/Button"; import { Icon } from "../ui-components/Icon"; const link = tv({ base: "leading-none" }); @@ -43,7 +43,7 @@ export function getFileIcon(url: string): FileIcon { return { icon: faFileDownload }; } -interface Props extends Omit { +interface Props { resultUrl: ResultUrlWithLabel; className?: string; children?: ReactNode; @@ -58,7 +58,6 @@ const DownloadButton = ({ className, children, showColoredIcon, - ...restProps }: Props & { ref?: Ref }) => { const { authToken } = useContext(AuthTokenContext); @@ -68,7 +67,7 @@ const DownloadButton = ({ return ( - diff --git a/frontend/src/js/concept-trees/ConceptTreeSearchBox.tsx b/frontend/src/js/concept-trees/ConceptTreeSearchBox.tsx index 097e1245a4..d3e00e3942 100644 --- a/frontend/src/js/concept-trees/ConceptTreeSearchBox.tsx +++ b/frontend/src/js/concept-trees/ConceptTreeSearchBox.tsx @@ -83,7 +83,7 @@ const ConceptTreeSearchBox = ({ className }: { className?: string }) => { duration: (search.duration / 1000.0).toFixed(2), })}

-
+
{showMismatches ? t("conceptTreeList.showingMismatches") @@ -91,7 +91,6 @@ const ConceptTreeSearchBox = ({ className }: { className?: string }) => { + )} {featureDates && selectedNode && ( - + )} {featureNegate && selectedNode && ( - + )} {featureConnectorRotate && selectedNode && connection && ( @@ -351,7 +347,6 @@ export function EditorV2({ > )} - - - - - {t("editorV2.clear")} - + + + {t("editorV2.clear")} + +
)} diff --git a/frontend/src/js/editor-v2/date-restriction/DateModal.tsx b/frontend/src/js/editor-v2/date-restriction/DateModal.tsx index ab69219fb6..321bbb61fb 100644 --- a/frontend/src/js/editor-v2/date-restriction/DateModal.tsx +++ b/frontend/src/js/editor-v2/date-restriction/DateModal.tsx @@ -22,10 +22,6 @@ const sectionHeadline = tv({ ], }); -const resetAll = tv({ - base: ["ml-5", "text-primary-500", "font-bold"], -}); - export const DateModal = ({ onClose, dateRange = {}, @@ -53,15 +49,12 @@ export const DateModal = ({ const labelSuffix = useMemo(() => { return hasActiveDate ? ( - + + + ) : null; }, [t, hasActiveDate, onResetDates]); diff --git a/frontend/src/js/entity-history/ContentControl.tsx b/frontend/src/js/entity-history/ContentControl.tsx index d4fa8635f4..f36349178a 100644 --- a/frontend/src/js/entity-history/ContentControl.tsx +++ b/frontend/src/js/entity-history/ContentControl.tsx @@ -5,10 +5,10 @@ import { faInfo, } from "@fortawesome/free-solid-svg-icons"; import { memo, useMemo, useState } from "react"; +import { ToggleButtonGroup } from "react-aria-components"; import { useTranslation } from "react-i18next"; -import { Button } from "../ui-components/Button"; import { Icon } from "../ui-components/Icon"; - +import { ToggleButton } from "../ui-components/ToggleButton"; import { Tooltip, TooltipTrigger } from "../ui-components/Tooltip"; export type ContentType = @@ -55,29 +55,27 @@ const ContentControl = ({ value, onChange }: Props) => { ); return ( -
- {options.map((option) => { - const active = value[option.key]; - return ( - - - {option.tooltip} - - ); - })} -
+ value[o.key]).map((o) => o.key)} + onSelectionChange={(keys) => + onChange({ + ...value, + ...Object.fromEntries(options.map((o) => [o.key, keys.has(o.key)])), + }) + } + > + {options.map((option) => ( + + + + + {option.tooltip} + + ))} + ); }; diff --git a/frontend/src/js/entity-history/DetailControl.tsx b/frontend/src/js/entity-history/DetailControl.tsx index 9c1140255d..9b860615a7 100644 --- a/frontend/src/js/entity-history/DetailControl.tsx +++ b/frontend/src/js/entity-history/DetailControl.tsx @@ -5,14 +5,18 @@ import { faCircleDot, } from "@fortawesome/free-solid-svg-icons"; import { type Dispatch, memo, type SetStateAction, useMemo } from "react"; +import { type Key, ToggleButtonGroup } from "react-aria-components"; import { useTranslation } from "react-i18next"; import { tv } from "tailwind-variants"; -import { Button } from "../ui-components/Button"; import { Icon } from "../ui-components/Icon"; - +import { ToggleButton } from "../ui-components/ToggleButton"; import { Tooltip, TooltipTrigger } from "../ui-components/Tooltip"; const root = tv({ base: "flex flex-col items-center" }); + +const detailLevels: DetailLevel[] = ["summary", "detail", "full"]; +const isDetailLevel = (key: Key): key is DetailLevel => + detailLevels.some((level) => level === key); export type DetailLevel = "summary" | "detail" | "full"; interface Props { @@ -53,26 +57,26 @@ export const DetailControl = memo( ({ className, detailLevel, setDetailLevel }: Props) => { const navOptions = useButtonConfig(); return ( -
- {navOptions.map(({ value, icon, tooltip }) => { - const selected = value === detailLevel; - - return ( - - - {tooltip} - - ); - })} -
+ { + const [key] = keys; + if (key !== undefined && isDetailLevel(key)) setDetailLevel(key); + }} + > + {navOptions.map(({ value, icon, tooltip }) => ( + + + + + {tooltip} + + ))} + ); }, ); diff --git a/frontend/src/js/entity-history/History.tsx b/frontend/src/js/entity-history/History.tsx index 4f698d4eb0..9608d72de9 100644 --- a/frontend/src/js/entity-history/History.tsx +++ b/frontend/src/js/entity-history/History.tsx @@ -1,4 +1,5 @@ import { useCallback, useMemo, useState } from "react"; +import { Toolbar } from "react-aria-components"; import { ErrorBoundary } from "react-error-boundary"; import { useHotkeys } from "react-hotkeys-hook"; import { useTranslation } from "react-i18next"; @@ -188,7 +189,11 @@ export const History = () => { )}
-
+ { /> )}
-
+ - {backButtonWarning} - - {!empty && ( - - - + {backButtonWarning} + + + {!empty && ( +
+ + + +
)} {!empty && ( @@ -179,13 +171,12 @@ export const Navigation = memo( )}
{!empty && ( -
+
@@ -211,26 +202,21 @@ export const Navigation = memo( {!empty && ( <> -
+
{`${t("history.nextButtonLabel")} (shift + ⬇)`}
-
+
- diff --git a/frontend/src/js/entity-history/SearchEntities.tsx b/frontend/src/js/entity-history/SearchEntities.tsx index 31679ac5d8..554258367c 100644 --- a/frontend/src/js/entity-history/SearchEntities.tsx +++ b/frontend/src/js/entity-history/SearchEntities.tsx @@ -212,10 +212,6 @@ const root = tv({ ], }); -const submitButton = tv({ - base: ["flex items-center justify-center", "w-full", "gap-[14px]"], -}); - const noop = () => {}; export const SearchEntitiesComponent = ({ @@ -245,15 +241,16 @@ export const SearchEntitiesComponent = ({ onSwitchFilterMode={noop} onLoadFilterSuggestions={loadFilterSuggestions} /> - +
+ +
); }; diff --git a/frontend/src/js/entity-history/VisibilityControl.tsx b/frontend/src/js/entity-history/VisibilityControl.tsx index 1f605a8366..c87ea0efd1 100644 --- a/frontend/src/js/entity-history/VisibilityControl.tsx +++ b/frontend/src/js/entity-history/VisibilityControl.tsx @@ -1,9 +1,8 @@ import { faEye, faEyeSlash } from "@fortawesome/free-regular-svg-icons"; import { memo } from "react"; import { useTranslation } from "react-i18next"; -import { Button } from "../ui-components/Button"; import { Icon } from "../ui-components/Icon"; - +import { ToggleButton } from "../ui-components/ToggleButton"; import { Tooltip, TooltipTrigger } from "../ui-components/Tooltip"; const VisibilityControl = ({ @@ -18,13 +17,13 @@ const VisibilityControl = ({ return (
- + {t("history.blurred")}
diff --git a/frontend/src/js/entity-history/timeline-search/SearchControl.tsx b/frontend/src/js/entity-history/timeline-search/SearchControl.tsx index f5fc79ce12..746a128cf4 100644 --- a/frontend/src/js/entity-history/timeline-search/SearchControl.tsx +++ b/frontend/src/js/entity-history/timeline-search/SearchControl.tsx @@ -1,8 +1,8 @@ import { faSearch } from "@fortawesome/free-solid-svg-icons"; import { memo } from "react"; import { useTranslation } from "react-i18next"; -import { Button } from "../../ui-components/Button"; import { Icon } from "../../ui-components/Icon"; +import { ToggleButton } from "../../ui-components/ToggleButton"; import { Tooltip, TooltipTrigger } from "../../ui-components/Tooltip"; import { useTimelineSearch } from "./timelineSearchState"; @@ -10,19 +10,17 @@ const SearchControl = () => { const { t } = useTranslation(); const { searchVisible, setSearchVisible } = useTimelineSearch(); - const toggleSearchVisible = () => setSearchVisible(!searchVisible); return (
- + {t("history.search")}
diff --git a/frontend/src/js/entity-history/timeline/TimelineEmptyPlaceholder.tsx b/frontend/src/js/entity-history/timeline/TimelineEmptyPlaceholder.tsx index 4fc6796688..8792941096 100644 --- a/frontend/src/js/entity-history/timeline/TimelineEmptyPlaceholder.tsx +++ b/frontend/src/js/entity-history/timeline/TimelineEmptyPlaceholder.tsx @@ -22,7 +22,7 @@ const message = tv({ }); const bigIcon = tv({ - base: ["text-[120px]", "text-gray-100"], + base: ["size-[120px]", "text-gray-100"], }); export const TimelineEmptyPlaceholder = ({ diff --git a/frontend/src/js/error-fallback/ErrorFallback.tsx b/frontend/src/js/error-fallback/ErrorFallback.tsx index c08c19f8a5..e27162136b 100644 --- a/frontend/src/js/error-fallback/ErrorFallback.tsx +++ b/frontend/src/js/error-fallback/ErrorFallback.tsx @@ -35,25 +35,21 @@ const ErrorFallback = ({ {allowFullRefresh && ( <>

{t("error.reloadDescription")}

- +
+ +
)} {onReset && ( <>

{t("error.resetDescription")}

- +
+ +
)}
diff --git a/frontend/src/js/external-forms/FormHeader.tsx b/frontend/src/js/external-forms/FormHeader.tsx index 3465c33428..35831d1189 100644 --- a/frontend/src/js/external-forms/FormHeader.tsx +++ b/frontend/src/js/external-forms/FormHeader.tsx @@ -28,8 +28,8 @@ const FormHeader = ({
); diff --git a/frontend/src/js/external-forms/form-components/DropzoneList.tsx b/frontend/src/js/external-forms/form-components/DropzoneList.tsx index 54e6f5f4fd..3f9d367882 100644 --- a/frontend/src/js/external-forms/form-components/DropzoneList.tsx +++ b/frontend/src/js/external-forms/form-components/DropzoneList.tsx @@ -93,14 +93,15 @@ const DropzoneList = ({ /> )}
- +
+ +
{item}
diff --git a/frontend/src/js/external-forms/form-components/DynamicInputGroup.tsx b/frontend/src/js/external-forms/form-components/DynamicInputGroup.tsx index ac4a296147..e86321dabb 100644 --- a/frontend/src/js/external-forms/form-components/DynamicInputGroup.tsx +++ b/frontend/src/js/external-forms/form-components/DynamicInputGroup.tsx @@ -18,7 +18,7 @@ const container = tv({ }); const removeButton = tv({ - base: ["absolute -top-[7px] -right-[7px]", "z-1", "bg-white"], + base: ["absolute -top-[7px] -right-[7px]", "z-1", "rounded bg-white"], }); const groupItem = tv({ @@ -51,14 +51,15 @@ const DynamicInputGroup = ({ you can also just delete the following constraint: */} {limit !== 1 && ( - +
+ +
)}
))} diff --git a/frontend/src/js/external-forms/form-concept-group/FormConceptGroup.tsx b/frontend/src/js/external-forms/form-concept-group/FormConceptGroup.tsx index 8c5e054af1..bebf938b3a 100644 --- a/frontend/src/js/external-forms/form-concept-group/FormConceptGroup.tsx +++ b/frontend/src/js/external-forms/form-concept-group/FormConceptGroup.tsx @@ -22,7 +22,7 @@ import { import DropzoneWithFileInput, { type DragItemFile, } from "../../ui-components/DropzoneWithFileInput"; -import ToggleButton from "../../ui-components/ToggleButton"; +import ToggleButtonGroup from "../../ui-components/ToggleButtonGroup"; import UploadConceptListModal from "../../upload-concept-list-modal/UploadConceptListModal"; import type { ConceptListDefaults as ConceptListDefaultsType } from "../config-types"; import { Description } from "../form-components/Description"; @@ -308,14 +308,15 @@ const FormConceptGroup = (props: Props) => { <> {props.label} {allowExtendedCopying && ( - + + + )} } @@ -361,7 +362,7 @@ const FormConceptGroup = (props: Props) => { {t("externalForms.common.connectedWith")}: - { props.onChange( diff --git a/frontend/src/js/external-forms/form/fields/ConceptListField.tsx b/frontend/src/js/external-forms/form/fields/ConceptListField.tsx index 1807960f52..b1ecdb3fa3 100644 --- a/frontend/src/js/external-forms/form/fields/ConceptListField.tsx +++ b/frontend/src/js/external-forms/form/fields/ConceptListField.tsx @@ -2,7 +2,7 @@ import type { ComponentProps } from "react"; import { useTranslation } from "react-i18next"; import { exists } from "../../../common/helpers/exists"; import { nodeIsInvalid } from "../../../model/node"; -import ToggleButton from "../../../ui-components/ToggleButton"; +import ToggleButtonGroup from "../../../ui-components/ToggleButtonGroup"; import type { ConceptListField as ConceptListFieldT } from "../../config-types"; import FormConceptGroup from "../../form-concept-group/FormConceptGroup"; import type { FormConceptGroupT } from "../../form-concept-group/formConceptGroupState"; @@ -77,7 +77,7 @@ export const ConceptListField = ({ renderRowPrefix={ exists(field.rowPrefixField) ? ({ value: fieldValue, onChange, row, i }) => ( - ({ label: option.label[locale] || "", diff --git a/frontend/src/js/external-forms/form/fields/DisclosureListField.tsx b/frontend/src/js/external-forms/form/fields/DisclosureListField.tsx index 99bd4990b7..6b66130557 100644 --- a/frontend/src/js/external-forms/form/fields/DisclosureListField.tsx +++ b/frontend/src/js/external-forms/form/fields/DisclosureListField.tsx @@ -81,14 +81,11 @@ const DisclosureField = ({ )} {field.creatable && canRemove && ( - +
+ +
)}
@@ -224,28 +221,29 @@ export const DisclosureListField = ({ /> ))} {field.creatable && ( - +
+ +
)}
); diff --git a/frontend/src/js/info-pane/InfoPane.tsx b/frontend/src/js/info-pane/InfoPane.tsx index 98b1dfcdb8..295c0bc8c9 100644 --- a/frontend/src/js/info-pane/InfoPane.tsx +++ b/frontend/src/js/info-pane/InfoPane.tsx @@ -11,8 +11,8 @@ import remarkGfm from "remark-gfm"; import { tv } from "tailwind-variants"; import type { StateT } from "../app/reducers"; import { Highlighter } from "../common/components/Highlighter"; -import { Button } from "../ui-components/Button"; import { Icon } from "../ui-components/Icon"; +import { ToggleButton } from "../ui-components/ToggleButton"; import { toggleAdditionalInfos as toggleInfos } from "./actions"; import InfoPaneCollapsed from "./InfoPaneCollapsed"; import { InfoPaneHeader } from "./InfoPaneHeader"; @@ -50,10 +50,14 @@ const head = tv({ const typeIcon = tv({ base: ["mt-px", "mr-[6px]", "text-primary-500"] }); -const tackButton = tv({ base: "ml-[5px]" }); - const pinnedLabel = tv({ - base: ["flex flex-row items-start", "m-0", "leading-[1.2]", "text-sm"], + base: [ + "flex flex-row items-start", + "gap-[5px]", + "m-0", + "leading-[1.2]", + "text-sm", + ], }); const descriptionText = tv({ @@ -184,15 +188,14 @@ const InfoPane = () => { conceptIcon={mainIcon} tackIcon={ toggleAdditionalInfos && ( - +
) } /> diff --git a/frontend/src/js/info-pane/InfoPaneCollapsed.tsx b/frontend/src/js/info-pane/InfoPaneCollapsed.tsx index 56395adc4e..320e5ac661 100644 --- a/frontend/src/js/info-pane/InfoPaneCollapsed.tsx +++ b/frontend/src/js/info-pane/InfoPaneCollapsed.tsx @@ -1,18 +1,21 @@ import { faAngleRight } from "@fortawesome/free-solid-svg-icons"; +import { Button as RacButton } from "react-aria-components"; import { useDispatch } from "react-redux"; import { tv } from "tailwind-variants"; -import { Button } from "../ui-components/Button"; import { Icon } from "../ui-components/Icon"; import { toggleInfoPane } from "./actions"; +// a strip that fills the collapsed pane; the chevron sits at its top const button = tv({ base: [ "absolute top-[40px] right-0 bottom-0", - "w-full h-auto", + "w-full", + "flex justify-center items-start", "pt-3", - "rounded-none", - "items-start", + "text-gray-800", + "cursor-pointer", + "hover:bg-gray-50", ], }); @@ -22,9 +25,9 @@ const InfoPaneCollapsed = () => { return (
- +
); }; diff --git a/frontend/src/js/info-pane/InfoPaneHeader.tsx b/frontend/src/js/info-pane/InfoPaneHeader.tsx index 8149c68d08..fc711366b1 100644 --- a/frontend/src/js/info-pane/InfoPaneHeader.tsx +++ b/frontend/src/js/info-pane/InfoPaneHeader.tsx @@ -1,9 +1,9 @@ import { faAngleLeft } from "@fortawesome/free-solid-svg-icons"; import { memo } from "react"; +import { Button as RacButton } from "react-aria-components"; import { useTranslation } from "react-i18next"; import { useDispatch } from "react-redux"; import { tv } from "tailwind-variants"; -import { Button } from "../ui-components/Button"; import { Icon } from "../ui-components/Icon"; import { toggleInfoPane } from "./actions"; @@ -23,8 +23,16 @@ const header = tv({ ], }); +// as tall as the header it sits in const toggleButton = tv({ - base: ["absolute top-[40px] right-0", "h-[39px]", "rounded-none"], + base: [ + "absolute top-[40px] right-0", + "h-[39px] w-[30px]", + "flex items-center justify-center", + "text-gray-800", + "cursor-pointer", + "hover:bg-gray-50", + ], }); export const InfoPaneHeader = memo(() => { @@ -35,13 +43,9 @@ export const InfoPaneHeader = memo(() => { return ( <> - +

{t("infoPane.headline")}

); diff --git a/frontend/src/js/modal/DeleteModal.tsx b/frontend/src/js/modal/DeleteModal.tsx index 14a01725f2..012a3a2428 100644 --- a/frontend/src/js/modal/DeleteModal.tsx +++ b/frontend/src/js/modal/DeleteModal.tsx @@ -29,10 +29,12 @@ const DeleteModal = ({
{description &&

{description}

}
- - +
+
diff --git a/frontend/src/js/preview/ScrollBox.tsx b/frontend/src/js/preview/ScrollBox.tsx index 12b08b0370..e0d74bd8ba 100644 --- a/frontend/src/js/preview/ScrollBox.tsx +++ b/frontend/src/js/preview/ScrollBox.tsx @@ -6,8 +6,8 @@ import { useRef, useState, } from "react"; +import { Button as RacButton } from "react-aria-components"; import { tv } from "tailwind-variants"; -import { Button } from "../ui-components/Button"; import { Icon } from "../ui-components/Icon"; const root = tv({ base: "overflow-auto" }); @@ -18,9 +18,12 @@ const scrollTopButton = tv({ "z-3", "flex justify-center", "h-[50px] w-[50px]", + "items-center", "rounded-full", "border border-gray-500", - "bg-white", + "bg-white hover:bg-gray-50", + "text-gray-800", + "cursor-pointer", "shadow-[0_0_5px_0_rgba(0,0,0,0.2)]", ], }); @@ -50,15 +53,14 @@ export default function ScrollBox({ return (
{showButton && ( - + )} {children}
diff --git a/frontend/src/js/preview/SelectBox.tsx b/frontend/src/js/preview/SelectBox.tsx index a25f102bcf..37b8fa0ca9 100644 --- a/frontend/src/js/preview/SelectBox.tsx +++ b/frontend/src/js/preview/SelectBox.tsx @@ -42,7 +42,7 @@ const listItem = tv({ }); const arrow = tv({ - base: ["mt-[5px]", "text-[17px]", "text-gray-500", "cursor-pointer"], + base: ["mt-[5px]", "size-[17px]", "text-gray-500", "cursor-pointer"], }); export default function SelectBox({ diff --git a/frontend/src/js/previous-queries/list/AddFolderModal.tsx b/frontend/src/js/previous-queries/list/AddFolderModal.tsx index 23b9c1fcd6..167e841cec 100644 --- a/frontend/src/js/previous-queries/list/AddFolderModal.tsx +++ b/frontend/src/js/previous-queries/list/AddFolderModal.tsx @@ -31,13 +31,12 @@ const AddFolderModal = ({ onClose, onSubmit, isValidName }: Props) => { onChange={(value) => setFolderName((value as string | null) || "")} inputProps={{ autoFocus: true }} /> -
+
+
+ +
{t("common.delete")} diff --git a/frontend/src/js/previous-queries/list/FoldersToggleButton.tsx b/frontend/src/js/previous-queries/list/FoldersToggleButton.tsx index d9973b3ca3..da1e472745 100644 --- a/frontend/src/js/previous-queries/list/FoldersToggleButton.tsx +++ b/frontend/src/js/previous-queries/list/FoldersToggleButton.tsx @@ -1,7 +1,7 @@ import { faFolder } from "@fortawesome/free-solid-svg-icons"; import { useTranslation } from "react-i18next"; -import { Button } from "../../ui-components/Button"; import { Icon } from "../../ui-components/Icon"; +import { ToggleButton } from "../../ui-components/ToggleButton"; import { Tooltip, TooltipTrigger } from "../../ui-components/Tooltip"; @@ -16,15 +16,14 @@ const FoldersToggleButton = ({ return ( - + {t("previousQueriesFolderButton.tooltip")} ); diff --git a/frontend/src/js/previous-queries/list/ProjectItem.tsx b/frontend/src/js/previous-queries/list/ProjectItem.tsx index bd552596f9..d0f77ea10b 100644 --- a/frontend/src/js/previous-queries/list/ProjectItem.tsx +++ b/frontend/src/js/previous-queries/list/ProjectItem.tsx @@ -67,7 +67,7 @@ const ownerName = tv({ const tooltipText = tv({ base: ["flex flex-col items-start", "font-normal"] }); const labelRow = tv({ - base: ["flex justify-between", "w-full", "leading-6", "my-[2px]"], + base: ["flex justify-between", "w-full", "leading-6"], }); const content = tv({ @@ -263,14 +263,13 @@ const ProjectItem = ({ )}
-
+
- {shareLabel} - +
+ + + {shareLabel} + +
diff --git a/frontend/src/js/previous-queries/upload/CSVColumnPicker.tsx b/frontend/src/js/previous-queries/upload/CSVColumnPicker.tsx index 08f082ca73..6bcc37559a 100644 --- a/frontend/src/js/previous-queries/upload/CSVColumnPicker.tsx +++ b/frontend/src/js/previous-queries/upload/CSVColumnPicker.tsx @@ -350,27 +350,24 @@ const CSVColumnPicker = ({ )}
)} -
+
{uploadResult && (uploadResult.unreadableDate.length > 0 || uploadResult.unresolvedId.length > 0) && ( - +
+ +
)} {uploadResult && ( )} {uploadResult ? ( - ) : ( + + + ) : null; }, [t, hasActiveDate, onResetAllDates]); diff --git a/frontend/src/js/query-node-editor/ConceptEntry.tsx b/frontend/src/js/query-node-editor/ConceptEntry.tsx index e1052c3ce8..d79b397bc3 100644 --- a/frontend/src/js/query-node-editor/ConceptEntry.tsx +++ b/frontend/src/js/query-node-editor/ConceptEntry.tsx @@ -62,7 +62,6 @@ const ConceptEntry = ({ intent="tertiary" size="sm" onPress={() => onRemoveConcept(conceptId)} - className="shrink-0" > diff --git a/frontend/src/js/query-node-editor/MenuColumnItem.tsx b/frontend/src/js/query-node-editor/MenuColumnItem.tsx index 81f6df2af4..bdf78e2e3d 100644 --- a/frontend/src/js/query-node-editor/MenuColumnItem.tsx +++ b/frontend/src/js/query-node-editor/MenuColumnItem.tsx @@ -7,6 +7,7 @@ import { tableHasFilterValues, tableIsDisabled } from "../model/table"; import type { TableWithFilterValueT } from "../standard-query-editor/types"; import { Button } from "../ui-components/Button"; import { Icon } from "../ui-components/Icon"; +import { ToggleButton } from "../ui-components/ToggleButton"; import { Tooltip, TooltipTrigger } from "../ui-components/Tooltip"; const container = tv({ @@ -90,11 +91,11 @@ const MenuColumnItem = ({
{isFilterActive && ( - + {t("queryNodeEditor.clearSettings")} )} diff --git a/frontend/src/js/query-node-editor/ResetAllSettingsButton.tsx b/frontend/src/js/query-node-editor/ResetAllSettingsButton.tsx index 987b5e3ae5..ee6021a26d 100644 --- a/frontend/src/js/query-node-editor/ResetAllSettingsButton.tsx +++ b/frontend/src/js/query-node-editor/ResetAllSettingsButton.tsx @@ -17,7 +17,7 @@ const ResetAllSettingsButton = ({ const confirmationText = t("queryNodeEditor.clearAllSettingsConfirm"); const trigger = ( - diff --git a/frontend/src/js/query-runner/DownloadResultsDropdownButton.tsx b/frontend/src/js/query-runner/DownloadResultsDropdownButton.tsx index e39b197d74..2882612aa8 100644 --- a/frontend/src/js/query-runner/DownloadResultsDropdownButton.tsx +++ b/frontend/src/js/query-runner/DownloadResultsDropdownButton.tsx @@ -1,34 +1,39 @@ import { faCaretDown, faDownload } from "@fortawesome/free-solid-svg-icons"; import { memo, useContext, useEffect, useMemo, useState } from "react"; -import { MenuTrigger } from "react-aria-components"; +import { MenuTrigger, Button as RacButton } from "react-aria-components"; import { useTranslation } from "react-i18next"; import { tv } from "tailwind-variants"; import type { ResultUrlWithLabel } from "../api/types"; import { AuthTokenContext } from "../authorization/AuthTokenProvider"; -import DownloadButton, { getFileIcon } from "../button/DownloadButton"; -import { Button } from "../ui-components/Button"; +import { getFileIcon } from "../button/DownloadButton"; import { Icon } from "../ui-components/Icon"; import { Menu, MenuItem } from "../ui-components/Menu"; import { Tooltip, TooltipTrigger } from "../ui-components/Tooltip"; import { getUserSettings, storeUserSettings } from "../user/userSettings"; +// a split button: the chosen format downloads, the caret opens the list const frame = tv({ - base: [ - "flex items-center justify-center", - "rounded", - "border border-gray-500", - "transition-opacity duration-100", - ], + base: ["inline-flex items-stretch", "h-[30px]", "rounded", "overflow-hidden"], variants: { - noborder: { true: "border-none" }, + bordered: { true: "border border-gray-500" }, }, }); -const downloadButton = tv({ - base: ["[&_button]:w-full", "[&_button]:px-[14px] [&_button]:py-2"], +const part = tv({ + base: [ + "inline-flex items-center", + "gap-[10px]", + "h-full", + "text-sm font-medium text-gray-800 whitespace-nowrap", + "cursor-pointer", + "hover:bg-gray-50", + ], + variants: { + caret: { true: "px-2", false: "px-[14px]" }, + }, }); -const separator = tv({ base: ["h-[33px] w-px", "bg-gray-500"] }); +const separator = tv({ base: ["w-px self-stretch", "bg-gray-500"] }); interface FileChoice { label: string; @@ -91,24 +96,28 @@ const DownloadResultsDropdownButton = ({ }, [fileChoice]); return ( -
+
{!tiny && ( <> - - {truncChosenLabel} - + + + {truncChosenLabel} + +
)} - + { diff --git a/frontend/src/js/query-runner/QueryRunnerButton.tsx b/frontend/src/js/query-runner/QueryRunnerButton.tsx index 67d38f574f..4834ae9a7c 100644 --- a/frontend/src/js/query-runner/QueryRunnerButton.tsx +++ b/frontend/src/js/query-runner/QueryRunnerButton.tsx @@ -1,9 +1,8 @@ import { faPlay, faSpinner, faStop } from "@fortawesome/free-solid-svg-icons"; import type { Ref } from "react"; +import { Button as RacButton } from "react-aria-components"; import { useTranslation } from "react-i18next"; import { tv } from "tailwind-variants"; - -import { Button } from "../ui-components/Button"; import { Icon } from "../ui-components/Icon"; const left = tv({ @@ -25,7 +24,7 @@ const runnerLabel = tv({ base: [ "px-[15px]", "bg-white group-hover/runner:bg-gray-50", - "text-gray-800", + "text-gray-800 font-medium", "self-stretch", "flex items-center", "whitespace-nowrap", @@ -33,8 +32,19 @@ const runnerLabel = tv({ ], }); +// two-tone: the icon part is filled, the label part stays white const button = tv({ - base: ["group/runner", "p-0", "overflow-hidden", "border-primary-500"], + base: [ + "group/runner", + "inline-flex items-center", + "h-[30px]", + "overflow-hidden", + "rounded", + "border border-primary-500", + "text-sm", + "cursor-pointer", + "disabled:cursor-not-allowed disabled:opacity-40", + ], }); function getIcon(loading: boolean, running: boolean) { @@ -63,8 +73,7 @@ const QueryRunnerButton = ({ return (
- +
); }; diff --git a/frontend/src/js/search-bar/SearchBar.tsx b/frontend/src/js/search-bar/SearchBar.tsx index 251de7bc78..da164e94bb 100644 --- a/frontend/src/js/search-bar/SearchBar.tsx +++ b/frontend/src/js/search-bar/SearchBar.tsx @@ -1,9 +1,9 @@ import { faSearch } from "@fortawesome/free-solid-svg-icons"; import { memo, useEffect, useState } from "react"; +import { Button as RacButton } from "react-aria-components"; import { tv } from "tailwind-variants"; import { exists } from "../common/helpers/exists"; import BaseInput from "../ui-components/BaseInput"; -import { Button } from "../ui-components/Button"; import { Icon } from "../ui-components/Icon"; const inputContainer = tv({ base: ["relative", "grow"] }); @@ -26,7 +26,14 @@ const right = tv({ ], }); -const searchButton = tv({ base: "text-gray-500" }); +const searchButton = tv({ + base: [ + "inline-flex items-center justify-center", + "size-[30px]", + "text-gray-500 hover:text-gray-800", + "cursor-pointer", + ], +}); interface Props { className?: string; @@ -71,14 +78,13 @@ const SearchBar = ({ /> {exists(localSearchTerm) && (
- +
)}
diff --git a/frontend/src/js/snack-message/SnackMessage.tsx b/frontend/src/js/snack-message/SnackMessage.tsx index b18d69c4a0..ae1ceb72d7 100644 --- a/frontend/src/js/snack-message/SnackMessage.tsx +++ b/frontend/src/js/snack-message/SnackMessage.tsx @@ -1,11 +1,11 @@ import { faTimes } from "@fortawesome/free-solid-svg-icons"; import { memo, useRef } from "react"; +import { Button as RacButton } from "react-aria-components"; import { useTranslation } from "react-i18next"; import { useDispatch, useSelector } from "react-redux"; import { tv } from "tailwind-variants"; import type { StateT } from "../app/reducers"; import { useClickOutside } from "../common/helpers/useClickOutside"; -import { Button } from "../ui-components/Button"; import { Icon } from "../ui-components/Icon"; import { resetMessage as resetMessageAction } from "./actions"; import type { SnackMessageStateT } from "./reducer"; @@ -28,7 +28,12 @@ const root = tv({ }); const clearZone = tv({ - base: ["absolute top-3 right-4", "z-11", "opacity-80 hover:opacity-100"], + base: [ + "absolute top-3 right-4", + "z-11", + "opacity-80 hover:opacity-100", + "cursor-pointer", + ], }); export const SnackMessage = memo(function SnackMessageComponent() { @@ -53,14 +58,13 @@ export const SnackMessage = memo(function SnackMessageComponent() {
{/* biome-ignore lint/security/noDangerouslySetInnerHtml: messages are our own i18n text */}
- +
)} diff --git a/frontend/src/js/standard-query-editor/EmptyQueryEditorDropzone.tsx b/frontend/src/js/standard-query-editor/EmptyQueryEditorDropzone.tsx index 764ab8a173..7d1fd55072 100644 --- a/frontend/src/js/standard-query-editor/EmptyQueryEditorDropzone.tsx +++ b/frontend/src/js/standard-query-editor/EmptyQueryEditorDropzone.tsx @@ -24,7 +24,7 @@ const textInitial = tv({ }); const arrowRight = tv({ - base: ["[grid-area:arrow]", "text-[140px]", "text-gray-100"], + base: ["[grid-area:arrow]", "size-[140px]", "text-gray-100"], }); const headline = tv({ diff --git a/frontend/src/js/standard-query-editor/QueryGroupActions.tsx b/frontend/src/js/standard-query-editor/QueryGroupActions.tsx index ae18517cd5..1bdeac572b 100644 --- a/frontend/src/js/standard-query-editor/QueryGroupActions.tsx +++ b/frontend/src/js/standard-query-editor/QueryGroupActions.tsx @@ -5,6 +5,7 @@ import { useTranslation } from "react-i18next"; import { tv } from "tailwind-variants"; import { Button } from "../ui-components/Button"; import { Icon } from "../ui-components/Icon"; +import { ToggleButton } from "../ui-components/ToggleButton"; import { Tooltip, @@ -17,19 +18,6 @@ const actions = tv({ base: ["mb-[6px]", "h-[18px]", "text-left"], }); -const dateButton = tv({ - base: "mr-[5px]", - variants: { - active: { - true: "underline", - false: "no-underline", - }, - }, -}); - -// excluding is a warning state: pressed shows red, not the usual primary -const excludeButton = tv({ base: ["mr-[5px]", "aria-pressed:text-red"] }); - interface PropsT { excludeActive: boolean; dateActive: boolean; @@ -49,31 +37,30 @@ const QueryGroupActions = ({ return (
-
+
- + {t("help.queryEditorExclude")} - + {t("help.queryEditorDate")}
diff --git a/frontend/src/js/standard-query-editor/QueryNodeActions.tsx b/frontend/src/js/standard-query-editor/QueryNodeActions.tsx index fc36aa6b16..6e358dbeec 100644 --- a/frontend/src/js/standard-query-editor/QueryNodeActions.tsx +++ b/frontend/src/js/standard-query-editor/QueryNodeActions.tsx @@ -67,10 +67,11 @@ const QueryNodeActions = (props: Props) => { diff --git a/frontend/src/js/standard-query-editor/SecondaryIdSelector.tsx b/frontend/src/js/standard-query-editor/SecondaryIdSelector.tsx index f66079dcf9..e936e390de 100644 --- a/frontend/src/js/standard-query-editor/SecondaryIdSelector.tsx +++ b/frontend/src/js/standard-query-editor/SecondaryIdSelector.tsx @@ -3,14 +3,13 @@ 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"; import { exists } from "../common/helpers/exists"; import { nodeIsConceptQueryNode } from "../model/node"; import { Icon } from "../ui-components/Icon"; import InfoTooltip from "../ui-components/InfoTooltip"; -import ToggleButton from "../ui-components/ToggleButton"; +import ToggleButtonGroup from "../ui-components/ToggleButtonGroup"; import { setSelectedSecondaryId } from "./actions"; import type { StandardQueryStateT } from "./queryReducer"; @@ -167,7 +166,7 @@ const SecondaryIdSelectorUI = memo( {t("queryEditor.secondaryId")} - {invalidText} )} - + )}
diff --git a/frontend/src/js/ui-components/Button.stories.tsx b/frontend/src/js/ui-components/Button.stories.tsx index f699054816..e25abe47f1 100644 --- a/frontend/src/js/ui-components/Button.stories.tsx +++ b/frontend/src/js/ui-components/Button.stories.tsx @@ -1,10 +1,8 @@ -import { faCheck, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons"; +import { faPlus, faTrash } from "@fortawesome/free-solid-svg-icons"; import type { Meta, StoryObj } from "@storybook/react"; -import { useState } from "react"; import { Button } from "./Button"; import { Icon } from "./Icon"; -import { Tooltip, TooltipTrigger } from "./Tooltip"; export default { title: "UiComponents/Button", @@ -14,7 +12,7 @@ export default { type Story = StoryObj; -const intents = ["primary", "secondary", "tertiary", "danger", "link"] as const; +const intents = ["primary", "secondary", "tertiary", "link"] as const; const sizes = ["sm", "md", "lg"] as const; export const Intents: Story = { @@ -32,6 +30,35 @@ export const Intents: Story = { ), }; +export const LinkInText: Story = { + render: () => ( +

+ A link button sits in flowing text, takes the text's size and line height, + and wraps with it: or{" "} + + . +

+ ), +}; + +export const Danger: Story = { + render: () => ( +
+ + +
+ ), +}; + export const Sizes: Story = { render: () => (
@@ -53,32 +80,3 @@ export const Sizes: Story = {
), }; - -const Pressed = () => { - const [pressed, setPressed] = useState(true); - return ( -
- - - - A pressed icon button - -
- ); -}; - -export const PressedState: Story = { render: () => }; diff --git a/frontend/src/js/ui-components/Button.tsx b/frontend/src/js/ui-components/Button.tsx index 360bcc074b..04f63f27cc 100644 --- a/frontend/src/js/ui-components/Button.tsx +++ b/frontend/src/js/ui-components/Button.tsx @@ -9,21 +9,20 @@ import { Button as RacButton, type ButtonProps as RacButtonProps, } from "react-aria-components"; -import { type ClassValue, tv } from "tailwind-variants"; +import { tv } from "tailwind-variants"; import { Icon } from "./Icon"; -const button = tv({ +export const buttonStyle = tv({ base: [ "inline-flex items-center justify-center", "shrink-0", "rounded", "border", - "leading-none font-normal whitespace-nowrap", + "leading-none font-medium whitespace-nowrap", "cursor-pointer", "transition-[color,background-color,border-color,opacity] duration-100", "disabled:cursor-not-allowed disabled:opacity-40", - "aria-pressed:text-primary-500", ], variants: { intent: { @@ -39,27 +38,38 @@ const button = tv({ "bg-transparent text-gray-800 border-transparent", "hover:bg-gray-50", ], - danger: [ - "bg-transparent text-red border-red", - "hover:bg-red hover:text-white", - ], - // reads as a text link, aligns with text: no padding, no box + // reads as a text link, see the compound variant for how it flows with text link: [ - "bg-transparent border-transparent px-0", + "bg-transparent border-transparent", "text-gray-500", "hover:text-gray-800 hover:underline", ], }, // every size is a fixed height, so text and icon-only buttons line up size: { - sm: "h-6 px-2 gap-[5px] text-xs", - md: "h-[30px] px-[15px] gap-[10px] text-sm", - lg: "h-9 px-[18px] gap-[10px] text-base", + sm: "h-6 px-2 gap-2 text-xs", + md: "h-[30px] px-[15px] gap-2 text-sm", + lg: "h-9 px-[18px] gap-3 text-base", }, // an icon-only button is a square iconOnly: { true: "px-0" }, + // a destructive or warning action, in the look of its intent + danger: { true: "" }, }, compoundVariants: [ + // a link sits in flowing text: no box, no padding, no fixed height, + // the surrounding text's size and line-height (after the size variant) + { + intent: "link", + class: + "h-auto px-0 gap-1 leading-[inherit] text-[length:inherit] align-baseline", + }, + { + danger: true, + intent: "secondary", + class: "text-red border-red hover:bg-red hover:text-white", + }, + { danger: true, intent: "tertiary", class: "text-red hover:text-red" }, { iconOnly: true, size: "sm", class: "w-6" }, { iconOnly: true, size: "md", class: "w-[30px]" }, { iconOnly: true, size: "lg", class: "w-9" }, @@ -67,17 +77,25 @@ const button = tv({ defaultVariants: { intent: "secondary", size: "md" }, }); -export interface ButtonProps +interface CommonProps extends Omit { - /** what the button does in its context; the look follows */ - intent?: "primary" | "secondary" | "tertiary" | "danger" | "link"; size?: "sm" | "md" | "lg"; - className?: ClassValue; style?: CSSProperties; children?: ReactNode; ref?: Ref; } +/** what the button does in its context; the look follows */ +export type ButtonProps = CommonProps & + ( + | { + intent?: "secondary" | "tertiary"; + /** a destructive or warning action: red, in the look of the intent */ + danger?: boolean; + } + | { intent: "primary" | "link"; danger?: never } + ); + const isIconOnly = (children: ReactNode) => { const items = Children.toArray(children); return ( @@ -97,22 +115,27 @@ const isIconOnly = (children: ReactNode) => { * * * A button whose only children are icons is square; give it an `aria-label`. - * A pressed state (toggles) is `aria-pressed`, which colors the button. - * `link` is for a button that reads as a text link. + * `link` is for a button that reads as a text link and sits in flowing text. + * `danger` turns a secondary or tertiary button red for destructive or + * warning actions. Something that is on or off is a ToggleButton. + * + * There is no className: layout belongs to the parent (a `grid` wrapper + * stretches a button to full width), and anything that needs another look is + * not this button but a react-aria Button styled where it lives. */ export const Button = ({ intent, size, - className, + danger, children, ...props }: ButtonProps) => ( diff --git a/frontend/src/js/ui-components/DropzoneWithFileInput.tsx b/frontend/src/js/ui-components/DropzoneWithFileInput.tsx index 0533c6b33c..c3c65aa754 100644 --- a/frontend/src/js/ui-components/DropzoneWithFileInput.tsx +++ b/frontend/src/js/ui-components/DropzoneWithFileInput.tsx @@ -139,13 +139,11 @@ const DropzoneWithFileInput = < /> )} {showImportButton && onImportLines && ( - +
+ +
)} {onSelectFile && ( - - - {t("common.save")} - +
+ + + {t("common.save")} + +
); }; diff --git a/frontend/src/js/ui-components/EditableText.tsx b/frontend/src/js/ui-components/EditableText.tsx index a709544499..cc36a4acba 100644 --- a/frontend/src/js/ui-components/EditableText.tsx +++ b/frontend/src/js/ui-components/EditableText.tsx @@ -7,7 +7,9 @@ import EditableTextForm from "./EditableTextForm"; import { Icon } from "./Icon"; import { Tooltip, TooltipTrigger } from "./Tooltip"; +// a flex wrapper, so the button does not sit on a text baseline and grow the row const editButton = tv({ + base: "flex shrink-0", variants: { large: { true: "mr-[10px]", @@ -63,18 +65,19 @@ const EditableText = ({ /> ) : (
- - - {tooltip} - + + + + {tooltip} + + {highlightedWords && highlightedWords.length > 0 ? ( {!saveOnClickoutside && ( - - - {t("common.save")} - +
+ + + {t("common.save")} + +
)} ); diff --git a/frontend/src/js/ui-components/Icon.stories.tsx b/frontend/src/js/ui-components/Icon.stories.tsx index e059345055..380383a58f 100644 --- a/frontend/src/js/ui-components/Icon.stories.tsx +++ b/frontend/src/js/ui-components/Icon.stories.tsx @@ -80,7 +80,7 @@ export const InheritsColor: Story = { - diff --git a/frontend/src/js/ui-components/InputDate/CustomHeader.tsx b/frontend/src/js/ui-components/InputDate/CustomHeader.tsx index 5934039156..158296f9a3 100644 --- a/frontend/src/js/ui-components/InputDate/CustomHeader.tsx +++ b/frontend/src/js/ui-components/InputDate/CustomHeader.tsx @@ -10,6 +10,7 @@ import { useMonthName, useMonthNames } from "../../common/helpers/dateHelper"; import { Button } from "../Button"; import { Icon } from "../Icon"; import { List, Menu } from "../InputSelect/InputSelectComponents"; +import { ToggleButton } from "../ToggleButton"; const root = tv({ base: "flex items-center justify-between", @@ -53,18 +54,18 @@ const SelectMenu = ({ {options.map((option) => ( - + ))} diff --git a/frontend/src/js/ui-components/InputDate/InputDate.tsx b/frontend/src/js/ui-components/InputDate/InputDate.tsx index fdaf242cf6..2c4bd89287 100644 --- a/frontend/src/js/ui-components/InputDate/InputDate.tsx +++ b/frontend/src/js/ui-components/InputDate/InputDate.tsx @@ -1,7 +1,7 @@ import { faCalendar } from "@fortawesome/free-regular-svg-icons"; import { type Ref, useRef } from "react"; +import { Button as RacButton } from "react-aria-components"; import ReactDatePicker from "react-datepicker"; -import { Button } from "../Button"; import { Icon } from "../Icon"; import "react-datepicker/dist/react-datepicker.css"; import { mergeRefs } from "react-merge-refs"; @@ -19,7 +19,13 @@ const root = tv({ }); const calendarButton = tv({ - base: "absolute top-0 left-0", + base: [ + "absolute top-0 left-0", + "h-[30px] px-[10px]", + "flex items-center", + "text-gray-800", + "cursor-pointer", + ], }); const baseInput = tv({ @@ -69,13 +75,12 @@ const InputDate = ({ }, }} /> - + - onSwitchMode(mode as ModeT)} options={[ diff --git a/frontend/src/js/ui-components/InputSelect/InputSelectComponents.tsx b/frontend/src/js/ui-components/InputSelect/InputSelectComponents.tsx index ffa784269f..77563f36c8 100644 --- a/frontend/src/js/ui-components/InputSelect/InputSelectComponents.tsx +++ b/frontend/src/js/ui-components/InputSelect/InputSelectComponents.tsx @@ -1,6 +1,6 @@ import type { ComponentProps } from "react"; +import { Button as RacButton } from "react-aria-components"; import { tv } from "tailwind-variants"; -import { Button } from "../Button"; import SelectListOption from "./SelectListOption"; @@ -114,19 +114,32 @@ export const Input = ({ className, ...props }: ComponentProps<"input">) => ( ); +// the select box's own small square buttons +const adornment = tv({ + base: [ + "inline-flex items-center justify-center", + "size-6 shrink-0", + "rounded", + "text-gray-800", + "cursor-pointer", + "hover:bg-gray-50", + "disabled:cursor-not-allowed disabled:opacity-40", + ], +}); + export const DropdownToggleButton = ({ className, ...props -}: ComponentProps) => ( - + )}
diff --git a/frontend/src/js/ui-components/ToggleButton.stories.tsx b/frontend/src/js/ui-components/ToggleButton.stories.tsx new file mode 100644 index 0000000000..7506513bc3 --- /dev/null +++ b/frontend/src/js/ui-components/ToggleButton.stories.tsx @@ -0,0 +1,153 @@ +import { + faBan, + faCalendar, + faEdit, + faFolder, + faThumbtack, +} from "@fortawesome/free-solid-svg-icons"; +import type { Meta, StoryObj } from "@storybook/react"; +import { useState } from "react"; + +import { Icon } from "./Icon"; +import { ToggleButton, type ToggleButtonProps } from "./ToggleButton"; +import { Tooltip, TooltipTrigger } from "./Tooltip"; + +export default { + title: "UiComponents/ToggleButton", + component: ToggleButton, + parameters: { layout: "centered" }, +} as Meta; + +type Story = StoryObj; + +// each example keeps its own state so it can be switched in the story +const Toggle = ({ + children, + defaultSelected = false, + ...props +}: Omit & { + defaultSelected?: boolean; +}) => { + const [selected, setSelected] = useState(defaultSelected); + return ( + + {children} + + ); +}; + +const Row = ({ + label, + children, +}: { + label: string; + children: React.ReactNode; +}) => ( +
+ {label} + {children} +
+); + +export const Intents: Story = { + render: () => ( +
+ + + + folders + + + + folders + + + + + + folders + + + + folders + + +
+ ), +}; + +export const Highlights: Story = { + render: () => ( +
+ + + + date + + + + + + exclude + + +
+ ), +}; + +export const Sizes: Story = { + render: () => ( +
+ {(["sm", "md", "lg"] as const).map((size) => ( + + + + edit + + + + edit + + + + + + ))} +
+ ), +}; + +export const IconOnlyWithTooltip: Story = { + render: () => ( + + + + + + Pin + + + ), +}; + +const OpensAnEditor = () => { + const [restriction, setRestriction] = useState(null); + return ( + + + setRestriction((current) => + current === null ? window.prompt("Date restriction") : null, + ) + } + > + + {restriction ?? "date"} + + + ); +}; + +/** The selected look reflects a state the user set; pressing edits it. */ +export const StateWithEditor: Story = { render: () => }; diff --git a/frontend/src/js/ui-components/ToggleButton.tsx b/frontend/src/js/ui-components/ToggleButton.tsx index dccd194477..3ced06a0e0 100644 --- a/frontend/src/js/ui-components/ToggleButton.tsx +++ b/frontend/src/js/ui-components/ToggleButton.tsx @@ -1,71 +1,73 @@ -import { Focusable } from "react-aria-components"; +import { Children, isValidElement, type ReactNode, type Ref } from "react"; +import { + ToggleButton as RacToggleButton, + type ToggleButtonProps as RacToggleButtonProps, +} from "react-aria-components"; import { tv } from "tailwind-variants"; -import { Tooltip, TooltipTrigger } from "./Tooltip"; +import { buttonStyle } from "./Button"; +import { Icon } from "./Icon"; -const root = tv({ base: ["m-0", "flex flex-wrap items-center"] }); - -const option = tv({ - base: [ - "inline-block", - "px-2 py-1", - "-ml-px mb-[2px]", - "cursor-pointer", - "border border-gray-500", - "text-xs", - "transition-[color,background-color] duration-100", - ], +// while selected, the button is bold and shows its highlight color +const toggleStyle = tv({ + extend: buttonStyle, + base: "data-selected:font-bold", variants: { - active: { - true: ["text-gray-800", "bg-white hover:bg-white"], - false: ["text-gray-500", "bg-gray-50 hover:bg-bg-50"], + highlight: { + primary: "data-selected:text-primary-500", + danger: "data-selected:text-red", }, - isFirst: { true: ["ml-0", "rounded-l-[2px]"] }, - isLast: { true: "rounded-r-[2px]" }, }, }); -interface OptionsT { - label: string; - value: string; - description?: string; +interface CommonProps + extends Omit { + size?: "sm" | "md" | "lg"; + children?: ReactNode; + ref?: Ref; +} + +export interface ToggleButtonProps extends CommonProps { + /** Button's intents that can be switched on and off */ + intent?: "secondary" | "tertiary"; + /** how it shows while selected: the primary color, or red for a warning state */ + highlight?: "primary" | "danger"; } -const ToggleButton = ({ - options, - value: inputValue, - onChange, - className, -}: { - className?: string; - options: OptionsT[]; - value: string; - onChange: (value: string) => void; -}) => { +const isIconOnly = (children: ReactNode) => { + const items = Children.toArray(children); return ( -
- {options.map(({ value, label, description }, i) => ( - - - - - {description} - - ))} -
+ items.length > 0 && + items.every((child) => isValidElement(child) && child.type === Icon) ); }; -export default ToggleButton; +/** + * A button whose look reflects a state that is on or off, in Button's look. + * react-aria's ToggleButton underneath: `isSelected` / `onChange`, and it + * works inside a ToggleButtonGroup and as a tooltip trigger. Pressing may + * flip the state or open an editor for it. + * + * + * + * + */ +export const ToggleButton = ({ + intent = "tertiary", + size, + highlight = "primary", + children, + ...props +}: ToggleButtonProps) => ( + + {children} + +); diff --git a/frontend/src/js/ui-components/ToggleButtonGroup.tsx b/frontend/src/js/ui-components/ToggleButtonGroup.tsx new file mode 100644 index 0000000000..8203dbc038 --- /dev/null +++ b/frontend/src/js/ui-components/ToggleButtonGroup.tsx @@ -0,0 +1,71 @@ +import { Focusable } from "react-aria-components"; +import { tv } from "tailwind-variants"; + +import { Tooltip, TooltipTrigger } from "./Tooltip"; + +const root = tv({ base: ["m-0", "flex flex-wrap items-center"] }); + +const option = tv({ + base: [ + "inline-block", + "px-2 py-1", + "-ml-px mb-[2px]", + "cursor-pointer", + "border border-gray-500", + "text-xs", + "transition-[color,background-color] duration-100", + ], + variants: { + active: { + true: ["text-gray-800", "bg-white hover:bg-white"], + false: ["text-gray-500", "bg-gray-50 hover:bg-bg-50"], + }, + isFirst: { true: ["ml-0", "rounded-l-[2px]"] }, + isLast: { true: "rounded-r-[2px]" }, + }, +}); + +interface OptionsT { + label: string; + value: string; + description?: string; +} + +const ToggleButtonGroup = ({ + options, + value: inputValue, + onChange, + className, +}: { + className?: string; + options: OptionsT[]; + value: string; + onChange: (value: string) => void; +}) => { + return ( +
+ {options.map(({ value, label, description }, i) => ( + + + + + {description} + + ))} +
+ ); +}; + +export default ToggleButtonGroup; diff --git a/frontend/src/js/upload-concept-list-modal/UploadConceptListModal.tsx b/frontend/src/js/upload-concept-list-modal/UploadConceptListModal.tsx index 65fa4fc6a4..71998e2a03 100644 --- a/frontend/src/js/upload-concept-list-modal/UploadConceptListModal.tsx +++ b/frontend/src/js/upload-concept-list-modal/UploadConceptListModal.tsx @@ -570,12 +570,7 @@ const UploadConceptListModal = ({ label={t("uploadConceptListModal.includeUnresolved")} /> )} -