diff --git a/frontend/src/js/authorization/LoginPage.tsx b/frontend/src/js/authorization/LoginPage.tsx index 65f7d8eab9..69b2b4f488 100644 --- a/frontend/src/js/authorization/LoginPage.tsx +++ b/frontend/src/js/authorization/LoginPage.tsx @@ -5,8 +5,8 @@ import { useNavigate } from "react-router"; import { tv } from "tailwind-variants"; import { usePostLogin } from "../api/api"; import { useAppTheme } from "../app-theme-context"; -import PrimaryButton from "../button/PrimaryButton"; import ErrorMessage from "../error-message/ErrorMessage"; +import { Button } from "../ui-components/Button"; import { Icon } from "../ui-components/Icon"; import InputPlain from "../ui-components/InputPlain/InputPlain"; @@ -115,18 +115,16 @@ const LoginPage = () => { disabled: loading, }} /> - - + {t("login.submit")} - + diff --git a/frontend/src/js/button/BadgeToggleButton.tsx b/frontend/src/js/button/BadgeToggleButton.tsx index ae8ad3f3ce..82bf3c0e94 100644 --- a/frontend/src/js/button/BadgeToggleButton.tsx +++ b/frontend/src/js/button/BadgeToggleButton.tsx @@ -3,10 +3,10 @@ import { useHotkeys } from "react-hotkeys-hook"; import { tv } from "tailwind-variants"; -import BasicButton from "./BasicButton"; +import { Button } from "../ui-components/Button"; const badgeToggleButton = tv({ - base: ["rounded", "px-1 py-px", "text-sm", "font-bold", "whitespace-nowrap"], + base: ["h-auto px-1 py-px", "text-sm", "font-bold"], variants: { active: { true: [ @@ -51,13 +51,16 @@ export const BadgeToggleButton = ({ useHotkeys(hotkey || "", onClick, { enabled: !!hotkey }, [hotkey, onClick]); return ( - {!active && "+ "} {children} {hotkey && {hotkey}} - + ); }; diff --git a/frontend/src/js/button/DestroyButton.tsx b/frontend/src/js/button/DestroyButton.tsx deleted file mode 100644 index d9328c4f18..0000000000 --- a/frontend/src/js/button/DestroyButton.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import type { ComponentProps } from "react"; - -import { tv } from "tailwind-variants"; - -import { TransparentButton } from "./TransparentButton"; - -const destroyButton = tv({ - base: [ - "text-red hover:text-white active:text-white focus:text-white", - "border-2 border-red", - "hover:bg-red active:bg-red focus:bg-red", - ], -}); - -export const DestroyButton = ({ - className, - ...props -}: ComponentProps) => ( - -); diff --git a/frontend/src/js/button/PrimaryButton.tsx b/frontend/src/js/button/PrimaryButton.tsx deleted file mode 100644 index 34ece7cc0e..0000000000 --- a/frontend/src/js/button/PrimaryButton.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import type { Ref } from "react"; - -import { tv } from "tailwind-variants"; - -import BasicButton, { type BasicButtonProps } from "./BasicButton"; - -const primaryButton = tv({ - base: [ - "text-white", - "bg-primary-500 bg-clip-padding", - "border border-primary-500", - "hover:opacity-90", - ], -}); - -const PrimaryButton = ({ - className, - ...props -}: BasicButtonProps & { ref?: Ref }) => ( - -); - -export default PrimaryButton; diff --git a/frontend/src/js/button/SelectFileButton.tsx b/frontend/src/js/button/SelectFileButton.tsx deleted file mode 100644 index 15eee1b7fc..0000000000 --- a/frontend/src/js/button/SelectFileButton.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import type { Ref } from "react"; - -import { tv } from "tailwind-variants"; - -import BasicButton, { type BasicButtonProps } from "./BasicButton"; - -const selectFileButton = tv({ - base: [ - "flex items-center", - "gap-[5px]", - "bg-transparent", - "border-0", - "text-[11px]", - "text-gray-500", - "font-light", - "hover:underline", - ], -}); - -export const SelectFileButton = ({ - className, - ...props -}: BasicButtonProps & { ref?: Ref }) => ( - -); diff --git a/frontend/src/js/button/TransparentButton.tsx b/frontend/src/js/button/TransparentButton.tsx deleted file mode 100644 index 27519c7fb6..0000000000 --- a/frontend/src/js/button/TransparentButton.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import type { Ref } from "react"; - -import { tv } from "tailwind-variants"; - -import BasicButton, { type BasicButtonProps } from "./BasicButton"; - -const transparentButton = tv({ - base: [ - "rounded", - "bg-transparent hover:bg-gray-50 focus:bg-gray-50", - "border border-gray-500 focus:border-green", - "text-gray-800", - ], - variants: { - light: { true: "border-gray-100 text-gray-500" }, - }, -}); - -export const TransparentButton = ({ - className, - light, - ...props -}: BasicButtonProps & { light?: boolean; ref?: Ref }) => ( - -); diff --git a/frontend/src/js/concept-trees/ConceptTreeSearchBox.tsx b/frontend/src/js/concept-trees/ConceptTreeSearchBox.tsx index 629f322a41..097e1245a4 100644 --- a/frontend/src/js/concept-trees/ConceptTreeSearchBox.tsx +++ b/frontend/src/js/concept-trees/ConceptTreeSearchBox.tsx @@ -2,12 +2,11 @@ import { useCallback, useRef } 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 { TransparentButton } from "../button/TransparentButton"; import AnimatedDots from "../common/components/AnimatedDots"; import ConceptTreesOpenButtons from "../concept-trees-open/ConceptTreesOpenButtons"; import SearchBar from "../search-bar/SearchBar"; +import { Button } from "../ui-components/Button"; import { clearSearchQuery, @@ -90,15 +89,16 @@ const ConceptTreeSearchBox = ({ className }: { className?: string }) => { ? t("conceptTreeList.showingMismatches") : t("conceptTreeList.showingMatchesOnly")} - {showMismatches ? t("conceptTreeList.showMatchesOnly") : t("conceptTreeList.showMismatches")} - + ) diff --git a/frontend/src/js/entity-history/SearchEntities.tsx b/frontend/src/js/entity-history/SearchEntities.tsx index 011d2ca2a4..31679ac5d8 100644 --- a/frontend/src/js/entity-history/SearchEntities.tsx +++ b/frontend/src/js/entity-history/SearchEntities.tsx @@ -3,7 +3,6 @@ import { useCallback, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { useSelector } from "react-redux"; import { tv } from "tailwind-variants"; - import { usePostPrefixForSuggestions, usePostResolveEntities, @@ -11,7 +10,6 @@ import { import { transformFilterValueToApi } from "../api/apiHelper"; import type { TableT } from "../api/types"; import type { StateT } from "../app/reducers"; -import PrimaryButton from "../button/PrimaryButton"; import { getConceptById } from "../concept-trees/globalTreeStoreHelper"; import { useDatasetId } from "../dataset/selectors"; import { isMultiSelectFilter, resetFilters } from "../model/filter"; @@ -22,6 +20,7 @@ import type { BigMultiSelectFilterWithValueType, MultiSelectFilterWithValueType, } from "../standard-query-editor/types"; +import { Button } from "../ui-components/Button"; import { Icon } from "../ui-components/Icon"; import type { LoadingPayload } from "./LoadHistoryDropzone"; @@ -246,14 +245,15 @@ export const SearchEntitiesComponent = ({ onSwitchFilterMode={noop} onLoadFilterSuggestions={loadFilterSuggestions} /> - {loading && } {t("history.searchEntitiesButton")} - + ); }; diff --git a/frontend/src/js/error-fallback/ErrorFallback.tsx b/frontend/src/js/error-fallback/ErrorFallback.tsx index 004b61caaf..c08c19f8a5 100644 --- a/frontend/src/js/error-fallback/ErrorFallback.tsx +++ b/frontend/src/js/error-fallback/ErrorFallback.tsx @@ -1,7 +1,6 @@ import { useTranslation } from "react-i18next"; import { tv } from "tailwind-variants"; - -import { TransparentButton } from "../button/TransparentButton"; +import { Button } from "../ui-components/Button"; const root = tv({ base: [ @@ -36,20 +35,25 @@ const ErrorFallback = ({ {allowFullRefresh && ( <>

{t("error.reloadDescription")}

- window.location.reload()} + onPress={() => window.location.reload()} > {t("error.reload")} - + )} {onReset && ( <>

{t("error.resetDescription")}

- + )} diff --git a/frontend/src/js/external-forms/form-concept-group/FormConceptCopyModal.tsx b/frontend/src/js/external-forms/form-concept-group/FormConceptCopyModal.tsx index e7b51b17f3..a6f872aa42 100644 --- a/frontend/src/js/external-forms/form-concept-group/FormConceptCopyModal.tsx +++ b/frontend/src/js/external-forms/form-concept-group/FormConceptCopyModal.tsx @@ -2,13 +2,11 @@ import { useEffect, useState } from "react"; import { useFormContext } from "react-hook-form"; import { useTranslation } from "react-i18next"; import { tv } from "tailwind-variants"; - import type { SelectOptionT } from "../../api/types"; -import PrimaryButton from "../../button/PrimaryButton"; -import { TransparentButton } from "../../button/TransparentButton"; import { exists } from "../../common/helpers/exists"; import { useActiveLang } from "../../localization/useActiveLang"; import Modal from "../../modal/Modal"; +import { Button } from "../../ui-components/Button"; import InputCheckbox from "../../ui-components/InputCheckbox"; import InputSelect from "../../ui-components/InputSelect/InputSelect"; import { useVisibleConceptListFields } from "../stateSelectors"; @@ -163,12 +161,16 @@ const FormConceptCopyModal = ({ )}
- + +
); 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 e656ec79e2..8c5e054af1 100644 --- a/frontend/src/js/external-forms/form-concept-group/FormConceptGroup.tsx +++ b/frontend/src/js/external-forms/form-concept-group/FormConceptGroup.tsx @@ -1,10 +1,8 @@ import { type ReactNode, useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { tv } from "tailwind-variants"; - import { usePostPrefixForSuggestions } from "../../api/api"; import type { SelectorResultType } from "../../api/types"; -import { TransparentButton } from "../../button/TransparentButton"; import { DNDType } from "../../common/constants/dndTypes"; import { exists } from "../../common/helpers/exists"; import { @@ -16,6 +14,7 @@ import { nodeHasNonDefaultSettings, } from "../../model/node"; import type { DragItemConceptTreeNode } from "../../standard-query-editor/types"; +import { Button } from "../../ui-components/Button"; import { isMovedObject, type PossibleDroppableObject, @@ -309,13 +308,14 @@ const FormConceptGroup = (props: Props) => { <> {props.label} {allowExtendedCopying && ( - setIsCopyModalOpen(true)} + size="sm" + onPress={() => setIsCopyModalOpen(true)} > {t("externalForms.common.concept.copyFrom")} - + )} } diff --git a/frontend/src/js/external-forms/form/fields/DisclosureListField.tsx b/frontend/src/js/external-forms/form/fields/DisclosureListField.tsx index 9c783ede1f..561a56bebc 100644 --- a/frontend/src/js/external-forms/form/fields/DisclosureListField.tsx +++ b/frontend/src/js/external-forms/form/fields/DisclosureListField.tsx @@ -8,9 +8,9 @@ import { type ComponentProps, useEffect, useState } from "react"; import { useFieldArray } from "react-hook-form"; import { tv } from "tailwind-variants"; import IconButton from "../../../button/IconButton"; -import { TransparentButton } from "../../../button/TransparentButton"; import { exists } from "../../../common/helpers/exists"; import { usePrevious } from "../../../common/helpers/usePrevious"; +import { Button } from "../../../ui-components/Button"; import { Icon } from "../../../ui-components/Icon"; import InfoTooltip from "../../../ui-components/InfoTooltip"; import type { DisclosureListField as DisclosureListFieldT } from "../../config-types"; @@ -222,10 +222,11 @@ export const DisclosureListField = ({ /> ))} {field.creatable && ( - { + )} ); diff --git a/frontend/src/js/modal/DeleteModal.tsx b/frontend/src/js/modal/DeleteModal.tsx index ca15f36a8d..14a01725f2 100644 --- a/frontend/src/js/modal/DeleteModal.tsx +++ b/frontend/src/js/modal/DeleteModal.tsx @@ -1,9 +1,7 @@ import type { ReactNode } from "react"; import { useTranslation } from "react-i18next"; import { tv } from "tailwind-variants"; - -import { DestroyButton } from "../button/DestroyButton"; -import { TransparentButton } from "../button/TransparentButton"; +import { Button } from "../ui-components/Button"; import Modal from "./Modal"; @@ -31,10 +29,12 @@ const DeleteModal = ({
{description &&

{description}

}
- + +
diff --git a/frontend/src/js/modal/Modal.tsx b/frontend/src/js/modal/Modal.tsx index d99e245313..f53f897b31 100644 --- a/frontend/src/js/modal/Modal.tsx +++ b/frontend/src/js/modal/Modal.tsx @@ -2,10 +2,9 @@ import { type ReactNode, useRef } from "react"; import { useHotkeys } from "react-hotkeys-hook"; import { useTranslation } from "react-i18next"; import { tv } from "tailwind-variants"; - -import { TransparentButton } from "../button/TransparentButton"; import { useClickOutside } from "../common/helpers/useClickOutside"; import { Heading3 } from "../headings/Headings"; +import { Button } from "../ui-components/Button"; import { Tooltip, TooltipTrigger } from "../ui-components/Tooltip"; const root = tv({ @@ -95,9 +94,9 @@ const Modal = ({ {headline} {doneButton && ( - + {t("common.closeEsc")} )} diff --git a/frontend/src/js/preview/Preview.tsx b/frontend/src/js/preview/Preview.tsx index 0df5912053..fd44479c63 100644 --- a/frontend/src/js/preview/Preview.tsx +++ b/frontend/src/js/preview/Preview.tsx @@ -4,10 +4,9 @@ import { useHotkeys } from "react-hotkeys-hook"; import { useTranslation } from "react-i18next"; import { useDispatch, useSelector } from "react-redux"; import { tv } from "tailwind-variants"; - import type { PreviewStatistics, SecondaryId } from "../api/types"; import type { StateT } from "../app/reducers"; -import { TransparentButton } from "../button/TransparentButton"; +import { Button } from "../ui-components/Button"; import { Icon } from "../ui-components/Icon"; import { closePreview } from "./actions"; import Charts from "./Charts"; @@ -94,9 +93,9 @@ export default function Preview() {
- + Ergebnisvorschau { inputProps={{ autoFocus: true }} />
- +
diff --git a/frontend/src/js/previous-queries/upload/CSVColumnPicker.tsx b/frontend/src/js/previous-queries/upload/CSVColumnPicker.tsx index 4a338f9ba3..2034ce85a6 100644 --- a/frontend/src/js/previous-queries/upload/CSVColumnPicker.tsx +++ b/frontend/src/js/previous-queries/upload/CSVColumnPicker.tsx @@ -14,11 +14,10 @@ import { useTranslation } from "react-i18next"; import { tv } from "tailwind-variants"; import type { QueryUploadConfigT, UploadQueryResponseT } from "../../api/types"; import IconButton from "../../button/IconButton"; -import PrimaryButton from "../../button/PrimaryButton"; -import { TransparentButton } from "../../button/TransparentButton"; import { parseCSV, toCSV } from "../../file/csv"; import { useActiveLang } from "../../localization/useActiveLang"; import ScrollableList from "../../scrollable-list/ScrollableList"; +import { Button } from "../../ui-components/Button"; import { Icon } from "../../ui-components/Icon"; import InputSelect from "../../ui-components/InputSelect/InputSelect"; import { Tooltip, TooltipTrigger } from "../../ui-components/Tooltip"; @@ -350,20 +349,25 @@ const CSVColumnPicker = ({ {uploadResult && (uploadResult.unreadableDate.length > 0 || uploadResult.unresolvedId.length > 0) && ( - - {" "} + )} {uploadResult && ( - {loading ? ( @@ -371,21 +375,23 @@ const CSVColumnPicker = ({ )}{" "} {t("uploadQueryResultsModal.uploadAgain")} - + )} {uploadResult ? ( - {t("common.done")} - + ) : ( - {loading ? ( @@ -393,7 +399,7 @@ const CSVColumnPicker = ({ )}{" "} {t("uploadQueryResultsModal.upload")} - + )}
diff --git a/frontend/src/js/query-node-editor/ResetAndClose.tsx b/frontend/src/js/query-node-editor/ResetAndClose.tsx index ac71f7442e..7d25570311 100644 --- a/frontend/src/js/query-node-editor/ResetAndClose.tsx +++ b/frontend/src/js/query-node-editor/ResetAndClose.tsx @@ -1,8 +1,7 @@ import { memo } from "react"; import { useTranslation } from "react-i18next"; - -import { TransparentButton } from "../button/TransparentButton"; import type { NodeResetConfig } from "../model/node"; +import { Button } from "../ui-components/Button"; import { Tooltip, TooltipTrigger } from "../ui-components/Tooltip"; import ResetAllSettingsButton from "./ResetAllSettingsButton"; @@ -31,9 +30,9 @@ const ResetAndClose = ({ /> )} - + {t("common.saveAndCloseEsc")} diff --git a/frontend/src/js/query-node-editor/UploadFilterListModal.tsx b/frontend/src/js/query-node-editor/UploadFilterListModal.tsx index 5bc9e4f9a0..123560c065 100644 --- a/frontend/src/js/query-node-editor/UploadFilterListModal.tsx +++ b/frontend/src/js/query-node-editor/UploadFilterListModal.tsx @@ -7,9 +7,9 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; import { tv } from "tailwind-variants"; import type { PostFilterResolveResponseT } from "../api/types"; -import PrimaryButton from "../button/PrimaryButton"; import Modal from "../modal/Modal"; import ScrollableList from "../scrollable-list/ScrollableList"; +import { Button } from "../ui-components/Button"; import { Icon } from "../ui-components/Icon"; import InputCheckbox from "../ui-components/InputCheckbox"; @@ -132,15 +132,16 @@ const UploadFilterListModal = ({ /> )} - { + ); diff --git a/frontend/src/js/query-runner/QueryRunnerButton.tsx b/frontend/src/js/query-runner/QueryRunnerButton.tsx index 0b8befaa49..67d38f574f 100644 --- a/frontend/src/js/query-runner/QueryRunnerButton.tsx +++ b/frontend/src/js/query-runner/QueryRunnerButton.tsx @@ -3,11 +3,16 @@ import type { Ref } from "react"; import { useTranslation } from "react-i18next"; import { tv } from "tailwind-variants"; -import BasicButton from "../button/BasicButton"; +import { Button } from "../ui-components/Button"; import { Icon } from "../ui-components/Icon"; const left = tv({ - base: ["px-[15px]", "transition-[color,background-color] duration-100"], + base: [ + "self-stretch", + "flex items-center", + "px-[15px]", + "transition-[color,background-color] duration-100", + ], variants: { running: { true: ["bg-white", "border-r border-primary-500"], @@ -21,24 +26,15 @@ const runnerLabel = tv({ "px-[15px]", "bg-white group-hover/runner:bg-gray-50", "text-gray-800", - "leading-[2.5]", + "self-stretch", + "flex items-center", "whitespace-nowrap", "transition-[background-color] duration-100", ], }); const button = tv({ - base: [ - "group/runner", - "inline-flex flex-row items-center", - "m-0 p-0", - "overflow-hidden", - "outline-none", - "rounded", - "border border-primary-500", - "text-sm", - "leading-[2.5]", - ], + base: ["group/runner", "p-0", "overflow-hidden", "border-primary-500"], }); function getIcon(loading: boolean, running: boolean) { @@ -67,11 +63,11 @@ const QueryRunnerButton = ({ return (
- @@ -81,7 +77,7 @@ const QueryRunnerButton = ({ /> {label} - +
); }; diff --git a/frontend/src/js/standard-query-editor/ExpandPreviousQueryModal.tsx b/frontend/src/js/standard-query-editor/ExpandPreviousQueryModal.tsx index 8f7d782c80..14dac56e8b 100644 --- a/frontend/src/js/standard-query-editor/ExpandPreviousQueryModal.tsx +++ b/frontend/src/js/standard-query-editor/ExpandPreviousQueryModal.tsx @@ -1,9 +1,7 @@ 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"; +import { Button } from "../ui-components/Button"; const description = tv({ base: ["max-w-[400px]", "mb-5"], @@ -36,12 +34,12 @@ const ExpandPreviousQueryModal = ({ {t("expandPreviousQueryModal.description")}

- + +
diff --git a/frontend/src/js/ui-components/Button.stories.tsx b/frontend/src/js/ui-components/Button.stories.tsx new file mode 100644 index 0000000000..f699054816 --- /dev/null +++ b/frontend/src/js/ui-components/Button.stories.tsx @@ -0,0 +1,84 @@ +import { faCheck, 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", + component: Button, + parameters: { layout: "centered" }, +} as Meta; + +type Story = StoryObj; + +const intents = ["primary", "secondary", "tertiary", "danger", "link"] as const; +const sizes = ["sm", "md", "lg"] as const; + +export const Intents: Story = { + render: () => ( +
+ {intents.map((intent) => ( + + ))} + +
+ ), +}; + +export const Sizes: Story = { + render: () => ( +
+ {sizes.map((size) => ( +
+ + + + +
+ ))} +
+ ), +}; + +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 new file mode 100644 index 0000000000..2a9aa9d35d --- /dev/null +++ b/frontend/src/js/ui-components/Button.tsx @@ -0,0 +1,121 @@ +import { + Children, + type CSSProperties, + isValidElement, + type ReactNode, + type Ref, +} from "react"; +import { + Button as RacButton, + type ButtonProps as RacButtonProps, +} from "react-aria-components"; +import { tv } from "tailwind-variants"; + +import { Icon } from "./Icon"; + +const button = tv({ + base: [ + "inline-flex items-center justify-center", + "shrink-0", + "rounded", + "border", + "leading-none font-normal 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: { + primary: [ + "bg-primary-500 text-white border-primary-500", + "hover:opacity-90", + ], + secondary: [ + "bg-transparent text-gray-800 border-gray-500", + "hover:bg-gray-50", + ], + tertiary: [ + "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 + link: [ + "bg-transparent border-transparent px-0", + "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", + }, + // an icon-only button is a square + iconOnly: { true: "px-0" }, + }, + compoundVariants: [ + { iconOnly: true, size: "sm", class: "w-6" }, + { iconOnly: true, size: "md", class: "w-[30px]" }, + { iconOnly: true, size: "lg", class: "w-9" }, + ], + defaultVariants: { intent: "secondary", size: "md" }, +}); + +export interface ButtonProps + extends Omit { + /** what the button does in its context; the look follows */ + intent?: "primary" | "secondary" | "tertiary" | "danger" | "link"; + size?: "sm" | "md" | "lg"; + className?: string; + style?: CSSProperties; + children?: ReactNode; + ref?: Ref; +} + +const isIconOnly = (children: ReactNode) => { + const items = Children.toArray(children); + return ( + items.length > 0 && + items.every((child) => isValidElement(child) && child.type === Icon) + ); +}; + +/** + * The button. react-aria's Button underneath, so it is the trigger that + * TooltipTrigger, MenuTrigger and friends expect, handles press via + * `onPress`, and exposes `data-hovered`, `data-pressed`, `data-focus-visible`. + * + * An icon goes in as a child and takes the button's text color: + * + * + * + * + * 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. + */ +export const Button = ({ + intent, + size, + className, + children, + ...props +}: ButtonProps) => ( + + {children} + +); diff --git a/frontend/src/js/ui-components/DropzoneWithFileInput.tsx b/frontend/src/js/ui-components/DropzoneWithFileInput.tsx index 9d22086079..0533c6b33c 100644 --- a/frontend/src/js/ui-components/DropzoneWithFileInput.tsx +++ b/frontend/src/js/ui-components/DropzoneWithFileInput.tsx @@ -1,16 +1,14 @@ -import { faFileImport } from "@fortawesome/free-solid-svg-icons"; import { type ReactNode, type Ref, useRef, useState } from "react"; import type { DropTargetMonitor } from "react-dnd"; import { NativeTypes } from "react-dnd-html5-backend"; import { useTranslation } from "react-i18next"; import { tv } from "tailwind-variants"; +import { Button } from "./Button"; -import { SelectFileButton } from "../button/SelectFileButton"; import Dropzone, { type ChildArgs, type PossibleDroppableObject, } from "./Dropzone"; -import { Icon } from "./Icon"; import { ImportModal } from "./ImportModal"; export interface DragItemFile { @@ -31,20 +29,17 @@ const dropzone = tv({ }, }); -const selectFileButton = tv({ - base: "absolute", +// a small text link at the dropzone's top right corner, or above it +const importButton = tv({ + base: "absolute text-xs", variants: { outside: { - true: "-top-[26px] -right-[12px]", - false: "top-[3px] right-0", + true: "-top-[30px] right-0", + false: "top-[3px] right-2", }, }, }); -const importIcon = tv({ - base: ["h-[10px]", "pr-[3px]"], -}); - interface PropsT { children: (args: ChildArgs) => ReactNode; onSelectFile?: (file: File) => void; @@ -144,16 +139,13 @@ const DropzoneWithFileInput = < /> )} {showImportButton && onImportLines && ( - setImportModalOpen(true)} + )} {onSelectFile && ( danger text - + red icon button diff --git a/frontend/src/js/ui-components/ImportModal.tsx b/frontend/src/js/ui-components/ImportModal.tsx index d41a79aeb8..88ee86c281 100644 --- a/frontend/src/js/ui-components/ImportModal.tsx +++ b/frontend/src/js/ui-components/ImportModal.tsx @@ -1,20 +1,13 @@ import { faFile, faPaste } from "@fortawesome/free-solid-svg-icons"; -import { - type ChangeEvent, - type MouseEvent, - useEffect, - useRef, - useState, -} from "react"; +import { type ChangeEvent, useEffect, useRef, useState } from "react"; import { NativeTypes } from "react-dnd-html5-backend"; import { createPortal } from "react-dom"; import { useTranslation } from "react-i18next"; import { tv } from "tailwind-variants"; - import IconButton from "../button/IconButton"; -import PrimaryButton from "../button/PrimaryButton"; import { getUniqueFileRows } from "../common/helpers/fileHelper"; import Modal from "../modal/Modal"; +import { Button } from "./Button"; import DropzoneWithFileInput, { type DragItemFile, @@ -75,9 +68,7 @@ export const ImportModal = ({ const fileInputRef = useRef(null); - const onSubmitClick = (e: MouseEvent) => { - e.stopPropagation(); - + const onSubmitClick = () => { const lines = textInput .split("\n") .map((line) => line.trim()) @@ -181,12 +172,13 @@ export const ImportModal = ({ {t("importModal.paste")} )} - {t("importModal.submit")} - + {options.map((option) => ( - onSelect(option.value as number)} + onPress={() => onSelect(option.value as number)} > {option.label} - + ))} diff --git a/frontend/src/js/ui-components/InputMultiSelect/MenuActionBar.tsx b/frontend/src/js/ui-components/InputMultiSelect/MenuActionBar.tsx index fd0316ce66..54d934195b 100644 --- a/frontend/src/js/ui-components/InputMultiSelect/MenuActionBar.tsx +++ b/frontend/src/js/ui-components/InputMultiSelect/MenuActionBar.tsx @@ -1,8 +1,7 @@ import { useTranslation } from "react-i18next"; import { tv } from "tailwind-variants"; - -import { TransparentButton } from "../../button/TransparentButton"; import { exists } from "../../common/helpers/exists"; +import { Button } from "../Button"; const row = tv({ base: [ @@ -33,13 +32,14 @@ const MenuActionBar = ({ optionsCount, total, onInsertAllClick }: Props) => { total !== optionsCount && t("inputMultiSelect.ofTotal", { count: total })}

- {t("inputMultiSelect.insertAll")} - + ); }; diff --git a/frontend/src/js/ui-components/Menu.stories.tsx b/frontend/src/js/ui-components/Menu.stories.tsx index b1ee3670da..5fc274d13b 100644 --- a/frontend/src/js/ui-components/Menu.stories.tsx +++ b/frontend/src/js/ui-components/Menu.stories.tsx @@ -6,9 +6,8 @@ import { } from "@fortawesome/free-solid-svg-icons"; import type { Meta, StoryObj } from "@storybook/react"; import { MenuTrigger } from "react-aria-components"; - import IconButton from "../button/IconButton"; -import { TransparentButton } from "../button/TransparentButton"; +import { Button } from "./Button"; import { ConfirmMenu } from "./ConfirmMenu"; import { Icon } from "./Icon"; import { Menu, MenuItem } from "./Menu"; @@ -49,7 +48,7 @@ export const Confirm: Story = { render: () => (
{}}> - Clear + Delete - Cancel + Discards your changes - Save + Saves and closes the editor - Disabled + Disabled buttons show no tooltip
@@ -49,7 +49,9 @@ export const Placements: Story = {
{(["top", "bottom", "left", "right"] as const).map((placement) => ( - {placement} + Placed at {placement} ))} @@ -67,7 +69,7 @@ export const Timing: Story = { - Further info + Explains a larger surface: long warm-up @@ -83,7 +85,7 @@ export const Timing: Story = { export const RichContent: Story = { render: () => ( - Rich content +

Headline

diff --git a/frontend/src/js/upload-concept-list-modal/UploadConceptListModal.tsx b/frontend/src/js/upload-concept-list-modal/UploadConceptListModal.tsx index b9da121590..65fa4fc6a4 100644 --- a/frontend/src/js/upload-concept-list-modal/UploadConceptListModal.tsx +++ b/frontend/src/js/upload-concept-list-modal/UploadConceptListModal.tsx @@ -14,7 +14,6 @@ import { import { useTranslation } from "react-i18next"; import { useSelector } from "react-redux"; import { tv } from "tailwind-variants"; - import { usePostConceptsListToResolve, usePostFilterValuesResolve, @@ -29,10 +28,10 @@ import type { SelectOptionT, } from "../api/types"; import type { StateT } from "../app/reducers"; -import PrimaryButton from "../button/PrimaryButton"; import Modal from "../modal/Modal"; import { nodeIsElement } from "../model/node"; import ScrollableList from "../scrollable-list/ScrollableList"; +import { Button } from "../ui-components/Button"; import { Icon } from "../ui-components/Icon"; import InputCheckbox from "../ui-components/InputCheckbox"; import InputPlain from "../ui-components/InputPlain/InputPlain"; @@ -571,7 +570,8 @@ const UploadConceptListModal = ({ label={t("uploadConceptListModal.includeUnresolved")} /> )} - + )}