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}
}
-
+
- {t("common.delete")}
+
+
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")}
/>
)}
-
+
>
)}