From c758dd9cbbb6f401d2512216eab974fde15146b2 Mon Sep 17 00:00:00 2001
From: Kai Rollmann
Date: Thu, 3 Sep 2026 15:09:08 +0200
Subject: [PATCH 01/16] refactor(frontend): Button without className; toggles,
adornments and special shapes on react-aria
---
frontend/src/js/authorization/LoginPage.tsx | 28 +++++----
frontend/src/js/button/BadgeToggleButton.tsx | 44 ++++++-------
frontend/src/js/button/DownloadButton.tsx | 2 +-
.../js/button/QueryResultHistoryButton.tsx | 1 -
frontend/src/js/concept-trees/ConceptTree.tsx | 6 +-
.../js/concept-trees/ConceptTreeSearchBox.tsx | 3 +-
frontend/src/js/editor-v2/EditorV2.tsx | 54 +++++++++-------
.../editor-v2/date-restriction/DateModal.tsx | 19 ++----
.../src/js/entity-history/ContentControl.tsx | 51 +++++++--------
.../src/js/entity-history/DetailControl.tsx | 48 +++++++-------
frontend/src/js/entity-history/History.tsx | 9 ++-
.../js/entity-history/InteractionControl.tsx | 19 ++----
frontend/src/js/entity-history/Navigation.tsx | 62 +++++++------------
.../src/js/entity-history/SearchEntities.tsx | 23 +++----
.../src/js/entity-history/SidebarControl.tsx | 31 ++++++++++
.../js/entity-history/VisibilityControl.tsx | 11 ++--
.../timeline-search/SearchControl.tsx | 12 ++--
.../src/js/error-fallback/ErrorFallback.tsx | 24 +++----
frontend/src/js/external-forms/FormHeader.tsx | 4 +-
.../src/js/external-forms/FormsNavigation.tsx | 29 ++++-----
.../form-components/DropzoneList.tsx | 17 ++---
.../form-components/DynamicInputGroup.tsx | 19 +++---
.../form-concept-group/FormConceptGroup.tsx | 17 ++---
.../form/fields/DisclosureListField.tsx | 58 +++++++++--------
frontend/src/js/info-pane/InfoPane.tsx | 11 ++--
.../src/js/info-pane/InfoPaneCollapsed.tsx | 15 +++--
frontend/src/js/info-pane/InfoPaneHeader.tsx | 20 +++---
frontend/src/js/modal/DeleteModal.tsx | 8 ++-
frontend/src/js/preview/ScrollBox.tsx | 14 +++--
.../previous-queries/list/AddFolderModal.tsx | 3 +-
.../src/js/previous-queries/list/Folders.tsx | 25 ++++----
.../list/FoldersToggleButton.tsx | 1 -
.../js/previous-queries/list/ProjectItem.tsx | 3 +-
.../previous-queries/list/ProjectItemsTab.tsx | 2 +-
.../list/ShareProjectItemModal.tsx | 25 ++++----
.../upload/CSVColumnPicker.tsx | 33 ++++------
.../js/query-group-modal/QueryGroupModal.tsx | 20 ++----
.../src/js/query-node-editor/ConceptEntry.tsx | 1 -
.../src/js/query-runner/QueryRunnerButton.tsx | 21 +++++--
frontend/src/js/search-bar/SearchBar.tsx | 16 +++--
.../src/js/snack-message/SnackMessage.tsx | 14 +++--
.../QueryGroupActions.tsx | 38 ++++++------
.../QueryNodeActions.tsx | 4 +-
frontend/src/js/ui-components/BaseInput.tsx | 17 +++--
frontend/src/js/ui-components/Button.tsx | 22 +++----
.../ui-components/DropzoneWithFileInput.tsx | 12 ++--
.../src/js/ui-components/EditableTagsForm.tsx | 29 ++++-----
.../src/js/ui-components/EditableText.tsx | 25 ++++----
.../src/js/ui-components/EditableTextForm.tsx | 29 ++++-----
.../src/js/ui-components/Icon.stories.tsx | 2 +-
.../js/ui-components/InputDate/InputDate.tsx | 17 +++--
.../InputSelect/InputSelectComponents.tsx | 27 +++++---
.../InputTextarea/InputTextarea.tsx | 15 +++--
.../UploadConceptListModal.tsx | 7 +--
frontend/src/localization/de.json | 3 +-
frontend/src/localization/en.json | 3 +-
56 files changed, 541 insertions(+), 532 deletions(-)
create mode 100644 frontend/src/js/entity-history/SidebarControl.tsx
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,
}}
/>
-
-
- {t("login.submit")}
-
+
+
+
+ {t("login.submit")}
+
+
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 (
-
{!active && "+ "}
{children}
{hotkey && {hotkey} }
-
+
);
};
diff --git a/frontend/src/js/button/DownloadButton.tsx b/frontend/src/js/button/DownloadButton.tsx
index 4dbd6587cd..6cd6dd3db0 100644
--- a/frontend/src/js/button/DownloadButton.tsx
+++ b/frontend/src/js/button/DownloadButton.tsx
@@ -68,7 +68,7 @@ const DownloadButton = ({
return (
-
+
{t("history.history")}
diff --git a/frontend/src/js/concept-trees/ConceptTree.tsx b/frontend/src/js/concept-trees/ConceptTree.tsx
index ac9c70ad43..79a07140bd 100644
--- a/frontend/src/js/concept-trees/ConceptTree.tsx
+++ b/frontend/src/js/concept-trees/ConceptTree.tsx
@@ -57,11 +57,7 @@ const ConceptTree = ({
className={message({ error: true })}
style={{ paddingLeft: 12 + depth * 15 }}
>
- onLoadTree(conceptId)}
- className="text-red"
- >
+ onLoadTree(conceptId)}>
{t("conceptTreeList.error", { tree: label })}
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 }) => {
diff --git a/frontend/src/js/editor-v2/EditorV2.tsx b/frontend/src/js/editor-v2/EditorV2.tsx
index 2f0d09cc91..8a82590204 100644
--- a/frontend/src/js/editor-v2/EditorV2.tsx
+++ b/frontend/src/js/editor-v2/EditorV2.tsx
@@ -10,6 +10,7 @@ import {
} from "@fortawesome/free-solid-svg-icons";
import { createId } from "@paralleldrive/cuid2";
import { useCallback, useEffect, useMemo, useState } from "react";
+import { ToggleButton } from "react-aria-components";
import { useHotkeys } from "react-hotkeys-hook";
import { useTranslation } from "react-i18next";
import { tv } from "tailwind-variants";
@@ -42,6 +43,19 @@ import { useTimeConnectionEditing } from "./time-connection/useTimeConnectionEdi
import type { Tree, TreeChildrenTime } from "./types";
import { findNodeById, useGetTranslatedConnection } from "./util";
+// negating is a warning state: selected shows red, not the usual primary
+const negateToggle = tv({
+ base: [
+ "inline-flex items-center gap-[5px]",
+ "h-6 px-2",
+ "rounded border border-transparent",
+ "text-xs text-gray-800",
+ "cursor-pointer",
+ "hover:bg-gray-50",
+ "data-selected:text-red",
+ ],
+});
+
const main = tv({
base: [
"grow",
@@ -331,18 +345,14 @@ export function EditorV2({
)}
{featureNegate && selectedNode && (
- {
- onNegateClick();
- }}
- className="aria-pressed:text-red"
+
{t("editorV2.negate")}
-
+
)}
{featureConnectorRotate && selectedNode && connection && (
@@ -425,21 +435,19 @@ 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 ? (
-
-
- {t("queryNodeEditor.reset")}
-
+
+
+
+ {t("queryNodeEditor.reset")}
+
+
) : null;
}, [t, hasActiveDate, onResetDates]);
diff --git a/frontend/src/js/entity-history/ContentControl.tsx b/frontend/src/js/entity-history/ContentControl.tsx
index d4fa8635f4..100ccc6fed 100644
--- a/frontend/src/js/entity-history/ContentControl.tsx
+++ b/frontend/src/js/entity-history/ContentControl.tsx
@@ -5,11 +5,11 @@ 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 { Tooltip, TooltipTrigger } from "../ui-components/Tooltip";
+import { SidebarToggle } from "./SidebarControl";
export type ContentType =
| "groupId"
@@ -55,29 +55,30 @@ const ContentControl = ({ value, onChange }: Props) => {
);
return (
-
- {options.map((option) => {
- const active = value[option.key];
- return (
-
- {
- onChange({ ...value, [option.key]: !value[option.key] });
- }}
- >
-
-
- {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..79baeea773 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 { Tooltip, TooltipTrigger } from "../ui-components/Tooltip";
+import { SidebarToggle } from "./SidebarControl";
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 (
-
- setDetailLevel(value as DetailLevel)}
- aria-pressed={selected}
- >
-
-
- {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 = () => {
)}
+
-
+
-
+
{t("history.closeAll")}
-
+
-
+
{t("history.openAll")}
diff --git a/frontend/src/js/entity-history/Navigation.tsx b/frontend/src/js/entity-history/Navigation.tsx
index 9babc6aba5..34cf574946 100644
--- a/frontend/src/js/entity-history/Navigation.tsx
+++ b/frontend/src/js/entity-history/Navigation.tsx
@@ -61,14 +61,6 @@ const loadHistoryDropzone = tv({
],
});
-const containedButton = tv({
- base: ["grow", "justify-center"],
-});
-
-const fullWidthButton = tv({
- base: ["w-full", "justify-center"],
-});
-
export const Navigation = memo(
({
blurred,
@@ -145,27 +137,27 @@ export const Navigation = memo(
}}
>
-
-
-
- {t("common.back")}
-
- {backButtonWarning}
-
- {!empty && (
-
-
-
- {t("history.settings.reset")}
+
+
+
+
+ {t("common.back")}
-
+ {backButtonWarning}
+
+
+ {!empty && (
+
+
+
+
+ {t("history.settings.reset")}
+
+
+
)}
{!empty && (
@@ -179,13 +171,12 @@ export const Navigation = memo(
)}
{!empty && (
-
+
@@ -211,26 +202,21 @@ export const Navigation = memo(
{!empty && (
<>
-
+
{`${t("history.nextButtonLabel")} (shift + ⬇)`}
-
+
-
+
CSV
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}
/>
-
- {loading && }
- {t("history.searchEntitiesButton")}
-
+
+
+ {loading && }
+ {t("history.searchEntitiesButton")}
+
+
);
};
diff --git a/frontend/src/js/entity-history/SidebarControl.tsx b/frontend/src/js/entity-history/SidebarControl.tsx
new file mode 100644
index 0000000000..1fa2f93d12
--- /dev/null
+++ b/frontend/src/js/entity-history/SidebarControl.tsx
@@ -0,0 +1,31 @@
+import {
+ Button as RacButton,
+ type ButtonProps as RacButtonProps,
+ ToggleButton as RacToggleButton,
+ type ToggleButtonProps as RacToggleButtonProps,
+} from "react-aria-components";
+import { tv } from "tailwind-variants";
+
+// The history sidebar's vertical toolbar: square controls, a selected one
+// shows in the primary color. Local to the history, not a design-system part.
+const control = tv({
+ base: [
+ "inline-flex items-center justify-center",
+ "size-[30px]",
+ "rounded",
+ "border border-transparent",
+ "text-gray-800",
+ "cursor-pointer",
+ "hover:bg-gray-50",
+ "data-selected:bg-gray-100 data-selected:text-primary-500",
+ "transition-colors duration-100",
+ ],
+});
+
+export const SidebarToggle = (props: RacToggleButtonProps) => (
+
+);
+
+export const SidebarAction = (props: RacButtonProps) => (
+
+);
diff --git a/frontend/src/js/entity-history/VisibilityControl.tsx b/frontend/src/js/entity-history/VisibilityControl.tsx
index 1f605a8366..275154c5eb 100644
--- a/frontend/src/js/entity-history/VisibilityControl.tsx
+++ b/frontend/src/js/entity-history/VisibilityControl.tsx
@@ -1,10 +1,9 @@
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 { Tooltip, TooltipTrigger } from "../ui-components/Tooltip";
+import { SidebarToggle } from "./SidebarControl";
const VisibilityControl = ({
blurred,
@@ -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..c29d648c44 100644
--- a/frontend/src/js/entity-history/timeline-search/SearchControl.tsx
+++ b/frontend/src/js/entity-history/timeline-search/SearchControl.tsx
@@ -1,28 +1,26 @@
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 { Tooltip, TooltipTrigger } from "../../ui-components/Tooltip";
+import { SidebarToggle } from "../SidebarControl";
import { useTimelineSearch } from "./timelineSearchState";
const SearchControl = () => {
const { t } = useTranslation();
const { searchVisible, setSearchVisible } = useTimelineSearch();
- const toggleSearchVisible = () => setSearchVisible(!searchVisible);
return (
-
-
+
{t("history.search")}
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")}
-
window.location.reload()}
- >
- {t("error.reload")}
-
+
+ window.location.reload()}>
+ {t("error.reload")}
+
+
>
)}
{onReset && (
<>
{t("error.resetDescription")}
-
- {t("error.reset")}
-
+
+
+ {t("error.reset")}
+
+
>
)}
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 =
({
/>
)}
-
onDelete(i)}
- className="absolute top-0 right-0"
- >
-
-
+
+ onDelete(i)}
+ >
+
+
+
{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 && (
-
onRemoveClick(idx)}
- className={removeButton()}
- >
-
-
+
+ onRemoveClick(idx)}
+ >
+
+
+
)}
))}
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..1ec00cbc26 100644
--- a/frontend/src/js/external-forms/form-concept-group/FormConceptGroup.tsx
+++ b/frontend/src/js/external-forms/form-concept-group/FormConceptGroup.tsx
@@ -308,14 +308,15 @@ const FormConceptGroup = (props: Props) => {
<>
{props.label}
{allowExtendedCopying && (
- setIsCopyModalOpen(true)}
- >
- {t("externalForms.common.concept.copyFrom")}
-
+
+ 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 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 && (
- remove(index)}
- className="absolute right-0 top-1/2 -translate-y-1/2"
- >
-
-
+
+ remove(index)}>
+
+
+
)}
@@ -224,28 +221,29 @@ export const DisclosureListField = ({
/>
))}
{field.creatable && (
-
{
- append(
- Object.fromEntries(
- field.fields.filter(isFormFieldWithValue).map((f) => [
- f.name,
- getInitialValue(f, {
- activeLang: locale,
- availableDatasets: commonProps.availableDatasets,
- datasetId,
- }),
- ]),
- ),
- );
- }}
- >
-
- {field.createNewLabel ? field.createNewLabel[locale] : undefined}
-
+
+ {
+ append(
+ Object.fromEntries(
+ field.fields.filter(isFormFieldWithValue).map((f) => [
+ f.name,
+ getInitialValue(f, {
+ activeLang: locale,
+ availableDatasets: commonProps.availableDatasets,
+ datasetId,
+ }),
+ ]),
+ ),
+ );
+ }}
+ >
+
+ {field.createNewLabel ? field.createNewLabel[locale] : undefined}
+
+
)}
);
diff --git a/frontend/src/js/info-pane/InfoPane.tsx b/frontend/src/js/info-pane/InfoPane.tsx
index 98b1dfcdb8..64f1e02067 100644
--- a/frontend/src/js/info-pane/InfoPane.tsx
+++ b/frontend/src/js/info-pane/InfoPane.tsx
@@ -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({
@@ -189,7 +193,6 @@ const InfoPane = () => {
size="sm"
aria-pressed
onPress={onToggleAdditionalInfos}
- className={tackButton()}
>
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..51532daf52 100644
--- a/frontend/src/js/modal/DeleteModal.tsx
+++ b/frontend/src/js/modal/DeleteModal.tsx
@@ -29,9 +29,11 @@ const DeleteModal = ({
{description &&
{description}
}
-
- {t("common.cancel")}
-
+
+
+ {t("common.cancel")}
+
+
{t("common.delete")}
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 && (
-
scrollBoxRef.current?.scrollTo({ top: 0, behavior: "smooth" })
}
- className={scrollTopButton()}
>
-
+
)}
{children}
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.cancel")}
diff --git a/frontend/src/js/previous-queries/list/Folders.tsx b/frontend/src/js/previous-queries/list/Folders.tsx
index e75729a7f1..3c9bc00dcc 100644
--- a/frontend/src/js/previous-queries/list/Folders.tsx
+++ b/frontend/src/js/previous-queries/list/Folders.tsx
@@ -46,7 +46,6 @@ const deleteButton = tv({
"absolute top-0 right-0",
"invisible group-hover/folder:visible",
"bg-bg-50",
- "rounded-none",
],
});
@@ -193,7 +192,6 @@ const Folders = ({ className }: { className?: string }) => {
intent="tertiary"
size="sm"
onPress={() => setShowAddFolderModal(true)}
- className="text-left"
>
{isNarrow ? t("folders.addShort") : t("folders.add")}
@@ -264,17 +262,18 @@ const Folders = ({ className }: { className?: string }) => {
resultWords={searchResultWords}
/>
- {
- setFolderToDelete(folder);
- }}
- className={deleteButton()}
- >
-
-
+
+ {
+ setFolderToDelete(folder);
+ }}
+ >
+
+
+
{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..402b22c30f 100644
--- a/frontend/src/js/previous-queries/list/FoldersToggleButton.tsx
+++ b/frontend/src/js/previous-queries/list/FoldersToggleButton.tsx
@@ -21,7 +21,6 @@ const FoldersToggleButton = ({
intent="secondary"
onPress={onClick}
aria-pressed={active}
- className="mr-[5px]"
>
diff --git a/frontend/src/js/previous-queries/list/ProjectItem.tsx b/frontend/src/js/previous-queries/list/ProjectItem.tsx
index bd552596f9..c5c63305ce 100644
--- a/frontend/src/js/previous-queries/list/ProjectItem.tsx
+++ b/frontend/src/js/previous-queries/list/ProjectItem.tsx
@@ -263,14 +263,13 @@ const ProjectItem = ({
)}
-
+
{
return (
<>
-
+
{
label={groupsLabel}
options={userGroupOptions}
/>
-
-
-
-
- {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) && (
-
-
- {t("uploadQueryResultsModal.downloadUnresolved", {
- count:
- uploadResult.unreadableDate.length +
- uploadResult.unresolvedId.length,
- })}
-
+
+
+
+ {t("uploadQueryResultsModal.downloadUnresolved", {
+ count:
+ uploadResult.unreadableDate.length +
+ uploadResult.unresolvedId.length,
+ })}
+
+
)}
{uploadResult && (
@@ -383,18 +380,12 @@ const CSVColumnPicker = ({
)}
{uploadResult ? (
-
+
{t("common.done")}
) : (
diff --git a/frontend/src/js/query-group-modal/QueryGroupModal.tsx b/frontend/src/js/query-group-modal/QueryGroupModal.tsx
index 7f00c18742..c929714323 100644
--- a/frontend/src/js/query-group-modal/QueryGroupModal.tsx
+++ b/frontend/src/js/query-group-modal/QueryGroupModal.tsx
@@ -2,7 +2,6 @@ import { faUndo } from "@fortawesome/free-solid-svg-icons";
import { Fragment, useCallback, useMemo } 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 type { DateStringMinMax } from "../common/helpers/dateHelper";
import Modal from "../modal/Modal";
@@ -18,10 +17,6 @@ import {
queryGroupModalSetDate,
} from "./actions";
-const resetAll = tv({
- base: ["text-primary-500", "font-bold", "ml-5"],
-});
-
function findGroup(query: StandardQueryStateT, andIdx: number) {
if (!query[andIdx]) return null;
@@ -82,15 +77,12 @@ const QueryGroupModal = ({
const labelSuffix = useMemo(() => {
return hasActiveDate ? (
-
-
- {t("queryNodeEditor.reset")}
-
+
+
+
+ {t("queryNodeEditor.reset")}
+
+
) : 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-runner/QueryRunnerButton.tsx b/frontend/src/js/query-runner/QueryRunnerButton.tsx
index 67d38f574f..8e9b74c607 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({
@@ -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 (
-
{label}
-
+
);
};
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) && (
- onSearch(localSearchTerm)}
className={searchButton()}
>
-
+
)}
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/QueryGroupActions.tsx b/frontend/src/js/standard-query-editor/QueryGroupActions.tsx
index ae18517cd5..1a0debd919 100644
--- a/frontend/src/js/standard-query-editor/QueryGroupActions.tsx
+++ b/frontend/src/js/standard-query-editor/QueryGroupActions.tsx
@@ -1,6 +1,7 @@
import { faCalendar } from "@fortawesome/free-regular-svg-icons";
import { faBan, faTimes } from "@fortawesome/free-solid-svg-icons";
import { memo } from "react";
+import { ToggleButton } from "react-aria-components";
import { useTranslation } from "react-i18next";
import { tv } from "tailwind-variants";
import { Button } from "../ui-components/Button";
@@ -17,19 +18,19 @@ 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: selected shows red, not the usual primary
+const excludeToggle = tv({
+ base: [
+ "inline-flex items-center gap-[5px]",
+ "h-6 px-2",
+ "rounded border border-transparent",
+ "text-xs text-gray-800",
+ "cursor-pointer",
+ "hover:bg-gray-50",
+ "data-selected:text-red",
+ ],
});
-// 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,18 +50,16 @@ const QueryGroupActions = ({
return (
-
+
-
{t("queryEditor.exclude")}
-
+
{t("help.queryEditorExclude")}
@@ -69,7 +68,6 @@ const QueryGroupActions = ({
size="sm"
aria-pressed={dateActive}
onPress={onDateClick}
- className={dateButton({ active: dateActive })}
>
{t("queryEditor.date")}
diff --git a/frontend/src/js/standard-query-editor/QueryNodeActions.tsx b/frontend/src/js/standard-query-editor/QueryNodeActions.tsx
index fc36aa6b16..efef2cc068 100644
--- a/frontend/src/js/standard-query-editor/QueryNodeActions.tsx
+++ b/frontend/src/js/standard-query-editor/QueryNodeActions.tsx
@@ -66,11 +66,11 @@ const QueryNodeActions = (props: Props) => {
{
props.onToggleTimestamps(props.andIdx, props.orIdx);
}}
- className="text-red"
>
diff --git a/frontend/src/js/ui-components/BaseInput.tsx b/frontend/src/js/ui-components/BaseInput.tsx
index 34d4d368f7..a2d4ffd996 100644
--- a/frontend/src/js/ui-components/BaseInput.tsx
+++ b/frontend/src/js/ui-components/BaseInput.tsx
@@ -9,12 +9,12 @@ import {
type Ref,
useCallback,
} from "react";
+import { Button as RacButton } from "react-aria-components";
import { useTranslation } from "react-i18next";
import { tv } from "tailwind-variants";
import type { CurrencyConfigT } from "../api/types";
import { isEmpty } from "../common/helpers/commonHelper";
import { exists } from "../common/helpers/exists";
-import { Button } from "./Button";
import CurrencyInput from "./CurrencyInput";
import { Icon } from "./Icon";
import {
@@ -50,13 +50,14 @@ const redIcon = tv({ base: ["opacity-80", "text-red"] });
const absoluteWrap = tv({ base: "absolute top-[5px] right-[35px]" });
-const clearZoneIconButton = tv({
+const clearButton = tv({
base: [
"absolute top-0 right-[10px]",
- "h-full",
+ "h-full px-1",
"flex items-center",
+ "text-gray-800 hover:text-red",
"cursor-pointer",
- "hover:text-red",
+ "disabled:cursor-not-allowed disabled:opacity-40",
],
});
@@ -204,17 +205,15 @@ const BaseInput = ({
{invalidText}
)}
- onChange(null)}
- className={clearZoneIconButton()}
>
-
+
>
)}
diff --git a/frontend/src/js/ui-components/Button.tsx b/frontend/src/js/ui-components/Button.tsx
index 360bcc074b..33dec43026 100644
--- a/frontend/src/js/ui-components/Button.tsx
+++ b/frontend/src/js/ui-components/Button.tsx
@@ -9,7 +9,7 @@ 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";
@@ -72,7 +72,6 @@ export interface ButtonProps
/** 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
;
@@ -99,21 +98,14 @@ 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.
+ *
+ * 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,
- children,
- ...props
-}: ButtonProps) => (
+export const Button = ({ intent, size, children, ...props }: ButtonProps) => (
{children}
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 && (
- setImportModalOpen(true)}
- >
- {t("common.import")}
-
+
+ setImportModalOpen(true)}>
+ {t("common.import")}
+
+
)}
{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..d98a0e13dc 100644
--- a/frontend/src/js/ui-components/EditableText.tsx
+++ b/frontend/src/js/ui-components/EditableText.tsx
@@ -63,18 +63,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..ecc7886ed4 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 = {
inside a button
-
+
red icon button
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 = ({
},
}}
/>
- datePickerRef.current?.setOpen(true)}
+ datePickerRef.current?.setOpen(true)}
>
-
+
) => (
);
+// 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) => (
-
-);
+}: Omit, "className"> & {
+ className?: string;
+}) => ;
export const ResetButton = ({
className,
...props
-}: ComponentProps) => (
-
-);
+}: Omit, "className"> & {
+ className?: string;
+}) => ;
const verticalSeparator = tv({
base: ["self-stretch", "shrink-0", "w-px", "my-[3px]", "bg-gray-100"],
diff --git a/frontend/src/js/ui-components/InputTextarea/InputTextarea.tsx b/frontend/src/js/ui-components/InputTextarea/InputTextarea.tsx
index 84b3b0deda..ac40ec5111 100644
--- a/frontend/src/js/ui-components/InputTextarea/InputTextarea.tsx
+++ b/frontend/src/js/ui-components/InputTextarea/InputTextarea.tsx
@@ -1,8 +1,8 @@
import { faTimes } from "@fortawesome/free-solid-svg-icons";
import type { DetailedHTMLProps, Ref, TextareaHTMLAttributes } from "react";
+import { Button as RacButton } from "react-aria-components";
import { useTranslation } from "react-i18next";
import { tv } from "tailwind-variants";
-import { Button } from "../Button";
import { Icon } from "../Icon";
import Labeled from "../Labeled";
@@ -18,11 +18,12 @@ const textarea = tv({
],
});
-const clearZoneIconButton = tv({
+const clearButton = tv({
base: [
"absolute top-0 right-[10px]",
- "h-[30px]",
+ "h-[30px] px-1",
"flex items-center",
+ "text-gray-800 hover:text-red",
"cursor-pointer",
],
});
@@ -71,16 +72,14 @@ export const InputTextarea = ({
value={props.value || ""}
/>
{props.value && (
- onChange(null)}
- className={clearZoneIconButton()}
>
-
+
)}
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")}
/>
)}
-
+
{mustIncludeUnresolved
? t("uploadConceptListModal.insertRegardless")
: t("uploadConceptListModal.insertNode")}
diff --git a/frontend/src/localization/de.json b/frontend/src/localization/de.json
index 7d56153b37..369019975c 100644
--- a/frontend/src/localization/de.json
+++ b/frontend/src/localization/de.json
@@ -529,7 +529,8 @@
"importPlaceholder": "{{idkind}};A0123456788;auffällig;\n{{idkind}};A0123456789;;unauffällig",
"error": "Konnte Historie für diese Datei nicht laden."
},
- "searchResultLabel": "Gefundene IDs"
+ "searchResultLabel": "Gefundene IDs",
+ "toolbar": "Werkzeugleiste"
},
"importModal": {
"headline": "Zeilenweise Importieren",
diff --git a/frontend/src/localization/en.json b/frontend/src/localization/en.json
index bc890d23b1..f5bef007c8 100644
--- a/frontend/src/localization/en.json
+++ b/frontend/src/localization/en.json
@@ -529,7 +529,8 @@
"importPlaceholder": "{{idkind}};A0123456788;interesting;\n{{idkind}};A0123456789;;not interesting",
"error": "Couldn't load history for this file."
},
- "searchResultLabel": "Found IDs"
+ "searchResultLabel": "Found IDs",
+ "toolbar": "Toolbar"
},
"importModal": {
"headline": "Import codes",
From d5a1bdd8a673dbaf7dc79c2761c38cef48660393 Mon Sep 17 00:00:00 2001
From: Kai Rollmann
Date: Thu, 3 Sep 2026 17:22:32 +0200
Subject: [PATCH 02/16] danger as its own axis, medium weight on buttons,
picture icons keep their size, 30 px name input and download split button
---
frontend/src/js/concept-trees/ConceptTree.tsx | 7 ++-
frontend/src/js/editor-v2/EditorV2.tsx | 26 +++------
.../timeline/TimelineEmptyPlaceholder.tsx | 2 +-
frontend/src/js/modal/DeleteModal.tsx | 2 +-
frontend/src/js/preview/SelectBox.tsx | 2 +-
.../upload/UploadQueryResultsModal.tsx | 2 +-
.../DownloadResultsDropdownButton.tsx | 53 +++++++++++--------
.../src/js/query-runner/QueryRunnerButton.tsx | 2 +-
.../EmptyQueryEditorDropzone.tsx | 2 +-
.../QueryGroupActions.tsx | 26 +++------
.../QueryNodeActions.tsx | 3 +-
.../src/js/ui-components/Button.stories.tsx | 19 ++++++-
frontend/src/js/ui-components/Button.tsx | 38 +++++++++----
.../src/js/ui-components/EditableTextForm.tsx | 2 +-
.../src/js/ui-components/Icon.stories.tsx | 2 +-
15 files changed, 108 insertions(+), 80 deletions(-)
diff --git a/frontend/src/js/concept-trees/ConceptTree.tsx b/frontend/src/js/concept-trees/ConceptTree.tsx
index 79a07140bd..4547879fb4 100644
--- a/frontend/src/js/concept-trees/ConceptTree.tsx
+++ b/frontend/src/js/concept-trees/ConceptTree.tsx
@@ -57,7 +57,12 @@ const ConceptTree = ({
className={message({ error: true })}
style={{ paddingLeft: 12 + depth * 15 }}
>
- onLoadTree(conceptId)}>
+ onLoadTree(conceptId)}
+ >
{t("conceptTreeList.error", { tree: label })}
diff --git a/frontend/src/js/editor-v2/EditorV2.tsx b/frontend/src/js/editor-v2/EditorV2.tsx
index 8a82590204..beb9a1099b 100644
--- a/frontend/src/js/editor-v2/EditorV2.tsx
+++ b/frontend/src/js/editor-v2/EditorV2.tsx
@@ -10,7 +10,6 @@ import {
} from "@fortawesome/free-solid-svg-icons";
import { createId } from "@paralleldrive/cuid2";
import { useCallback, useEffect, useMemo, useState } from "react";
-import { ToggleButton } from "react-aria-components";
import { useHotkeys } from "react-hotkeys-hook";
import { useTranslation } from "react-i18next";
import { tv } from "tailwind-variants";
@@ -43,19 +42,6 @@ import { useTimeConnectionEditing } from "./time-connection/useTimeConnectionEdi
import type { Tree, TreeChildrenTime } from "./types";
import { findNodeById, useGetTranslatedConnection } from "./util";
-// negating is a warning state: selected shows red, not the usual primary
-const negateToggle = tv({
- base: [
- "inline-flex items-center gap-[5px]",
- "h-6 px-2",
- "rounded border border-transparent",
- "text-xs text-gray-800",
- "cursor-pointer",
- "hover:bg-gray-50",
- "data-selected:text-red",
- ],
-});
-
const main = tv({
base: [
"grow",
@@ -345,14 +331,16 @@ export function EditorV2({
)}
{featureNegate && selectedNode && (
-
{t("editorV2.negate")}
-
+
)}
{featureConnectorRotate && selectedNode && connection && (
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/modal/DeleteModal.tsx b/frontend/src/js/modal/DeleteModal.tsx
index 51532daf52..012a3a2428 100644
--- a/frontend/src/js/modal/DeleteModal.tsx
+++ b/frontend/src/js/modal/DeleteModal.tsx
@@ -34,7 +34,7 @@ const DeleteModal = ({
{t("common.cancel")}
-
+
{t("common.delete")}
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/upload/UploadQueryResultsModal.tsx b/frontend/src/js/previous-queries/upload/UploadQueryResultsModal.tsx
index d7a7f2efb5..08ab0f6eda 100644
--- a/frontend/src/js/previous-queries/upload/UploadQueryResultsModal.tsx
+++ b/frontend/src/js/previous-queries/upload/UploadQueryResultsModal.tsx
@@ -12,7 +12,7 @@ import InfoTooltip from "../../ui-components/InfoTooltip";
import CSVColumnPicker, { type QueryToUploadT } from "./CSVColumnPicker";
const successIcon = tv({
- base: ["block", "mx-auto mb-[10px]", "text-[40px]", "text-green"],
+ base: ["block", "mx-auto mb-[10px]", "size-10", "text-green"],
});
const dropzone = tv({
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 8e9b74c607..4834ae9a7c 100644
--- a/frontend/src/js/query-runner/QueryRunnerButton.tsx
+++ b/frontend/src/js/query-runner/QueryRunnerButton.tsx
@@ -24,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",
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 1a0debd919..63720a1ae8 100644
--- a/frontend/src/js/standard-query-editor/QueryGroupActions.tsx
+++ b/frontend/src/js/standard-query-editor/QueryGroupActions.tsx
@@ -1,7 +1,6 @@
import { faCalendar } from "@fortawesome/free-regular-svg-icons";
import { faBan, faTimes } from "@fortawesome/free-solid-svg-icons";
import { memo } from "react";
-import { ToggleButton } from "react-aria-components";
import { useTranslation } from "react-i18next";
import { tv } from "tailwind-variants";
import { Button } from "../ui-components/Button";
@@ -18,19 +17,6 @@ const actions = tv({
base: ["mb-[6px]", "h-[18px]", "text-left"],
});
-// excluding is a warning state: selected shows red, not the usual primary
-const excludeToggle = tv({
- base: [
- "inline-flex items-center gap-[5px]",
- "h-6 px-2",
- "rounded border border-transparent",
- "text-xs text-gray-800",
- "cursor-pointer",
- "hover:bg-gray-50",
- "data-selected:text-red",
- ],
-});
-
interface PropsT {
excludeActive: boolean;
dateActive: boolean;
@@ -52,14 +38,16 @@ const QueryGroupActions = ({
-
{t("queryEditor.exclude")}
-
+
{t("help.queryEditorExclude")}
diff --git a/frontend/src/js/standard-query-editor/QueryNodeActions.tsx b/frontend/src/js/standard-query-editor/QueryNodeActions.tsx
index efef2cc068..6e358dbeec 100644
--- a/frontend/src/js/standard-query-editor/QueryNodeActions.tsx
+++ b/frontend/src/js/standard-query-editor/QueryNodeActions.tsx
@@ -66,7 +66,8 @@ const QueryNodeActions = (props: Props) => {
{
props.onToggleTimestamps(props.andIdx, props.orIdx);
diff --git a/frontend/src/js/ui-components/Button.stories.tsx b/frontend/src/js/ui-components/Button.stories.tsx
index f699054816..721811b4d1 100644
--- a/frontend/src/js/ui-components/Button.stories.tsx
+++ b/frontend/src/js/ui-components/Button.stories.tsx
@@ -14,7 +14,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 +32,23 @@ export const Intents: Story = {
),
};
+export const Danger: Story = {
+ render: () => (
+
+ {intents.map((intent) => (
+
+
+ {intent}
+
+ ))}
+
+
+ pressed
+
+
+ ),
+};
+
export const Sizes: Story = {
render: () => (
diff --git a/frontend/src/js/ui-components/Button.tsx b/frontend/src/js/ui-components/Button.tsx
index 33dec43026..36ce4b6dae 100644
--- a/frontend/src/js/ui-components/Button.tsx
+++ b/frontend/src/js/ui-components/Button.tsx
@@ -19,7 +19,7 @@ const button = tv({
"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",
@@ -39,10 +39,6 @@ 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
link: [
"bg-transparent border-transparent px-0",
@@ -58,8 +54,18 @@ const button = tv({
},
// an icon-only button is a square
iconOnly: { true: "px-0" },
+ // a destructive or warning action, in the look of its intent
+ danger: { true: "aria-pressed:text-red" },
},
compoundVariants: [
+ { danger: true, intent: "primary", class: "bg-red border-red" },
+ {
+ 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" },
+ { danger: true, intent: "link", 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" },
@@ -70,7 +76,9 @@ const button = tv({
export interface ButtonProps
extends Omit
{
/** what the button does in its context; the look follows */
- intent?: "primary" | "secondary" | "tertiary" | "danger" | "link";
+ intent?: "primary" | "secondary" | "tertiary" | "link";
+ /** a destructive or warning action: red, in the look of the intent */
+ danger?: boolean;
size?: "sm" | "md" | "lg";
style?: CSSProperties;
children?: ReactNode;
@@ -97,15 +105,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. `danger` turns any intent
+ * red for destructive or warning actions.
*
* 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, children, ...props }: ButtonProps) => (
+export const Button = ({
+ intent,
+ size,
+ danger,
+ children,
+ ...props
+}: ButtonProps) => (
{children}
diff --git a/frontend/src/js/ui-components/EditableTextForm.tsx b/frontend/src/js/ui-components/EditableTextForm.tsx
index 5e9887d3cf..367529e2bd 100644
--- a/frontend/src/js/ui-components/EditableTextForm.tsx
+++ b/frontend/src/js/ui-components/EditableTextForm.tsx
@@ -8,7 +8,7 @@ import { Icon } from "./Icon";
import { Tooltip, TooltipTrigger } from "./Tooltip";
const input = tv({
- base: ["h-[28px]", "px-2", "rounded", "border border-gray-500", "text-sm"],
+ base: ["h-[30px]", "px-2", "rounded", "border border-gray-500", "text-sm"],
});
const form = tv({
diff --git a/frontend/src/js/ui-components/Icon.stories.tsx b/frontend/src/js/ui-components/Icon.stories.tsx
index ecc7886ed4..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 = {
inside a button
-
+
red icon button
From d2dd114c2f8465f139157cf0717b282e233b3ee4 Mon Sep 17 00:00:00 2001
From: Kai Rollmann
Date: Thu, 3 Sep 2026 17:31:09 +0200
Subject: [PATCH 03/16] link buttons flow with the text: auto height, inherited
size and line-height
---
frontend/src/js/ui-components/Button.stories.tsx | 14 ++++++++++++++
frontend/src/js/ui-components/Button.tsx | 8 +++++---
2 files changed, 19 insertions(+), 3 deletions(-)
diff --git a/frontend/src/js/ui-components/Button.stories.tsx b/frontend/src/js/ui-components/Button.stories.tsx
index 721811b4d1..38336e879d 100644
--- a/frontend/src/js/ui-components/Button.stories.tsx
+++ b/frontend/src/js/ui-components/Button.stories.tsx
@@ -32,6 +32,20 @@ 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: import a list or{" "}
+
+
+ add one by hand
+
+ .
+
+ ),
+};
+
export const Danger: Story = {
render: () => (
diff --git a/frontend/src/js/ui-components/Button.tsx b/frontend/src/js/ui-components/Button.tsx
index 36ce4b6dae..5a8b2c453b 100644
--- a/frontend/src/js/ui-components/Button.tsx
+++ b/frontend/src/js/ui-components/Button.tsx
@@ -39,9 +39,11 @@ const button = tv({
"bg-transparent text-gray-800 border-transparent",
"hover:bg-gray-50",
],
- // reads as a text link, aligns with text: no padding, no box
+ // reads as a text link and sits in flowing text: no box, no padding,
+ // no fixed height, the surrounding text's size and line-height
link: [
"bg-transparent border-transparent px-0",
+ "h-auto leading-[inherit] text-[length:inherit] align-baseline",
"text-gray-500",
"hover:text-gray-800 hover:underline",
],
@@ -105,8 +107,8 @@ 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. `danger` turns any intent
- * red for destructive or warning actions.
+ * `link` is for a button that reads as a text link and sits in flowing text.
+ * `danger` turns any intent red for destructive or warning actions.
*
* 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
From ec6489f0ce62a00d727cd1dd621f7887cb6be3c1 Mon Sep 17 00:00:00 2001
From: Kai Rollmann
Date: Thu, 3 Sep 2026 17:37:22 +0200
Subject: [PATCH 04/16] ToggleButton in Button's look; danger only for
secondary and tertiary; link buttons flow with text
---
frontend/src/js/button/DownloadButton.tsx | 7 +-
frontend/src/js/editor-v2/EditorV2.tsx | 29 +++--
.../form-concept-group/FormConceptGroup.tsx | 4 +-
.../form/fields/ConceptListField.tsx | 4 +-
frontend/src/js/info-pane/InfoPane.tsx | 10 +-
.../list/FoldersToggleButton.tsx | 10 +-
.../js/query-node-editor/MenuColumnItem.tsx | 9 +-
.../ResetAllSettingsButton.tsx | 2 +-
.../QueryGroupActions.tsx | 23 ++--
.../SecondaryIdSelector.tsx | 5 +-
.../src/js/ui-components/Button.stories.tsx | 47 ++------
frontend/src/js/ui-components/Button.tsx | 43 ++++---
.../ui-components/InputDate/CustomHeader.tsx | 7 +-
frontend/src/js/ui-components/InputRange.tsx | 6 +-
.../js/ui-components/ToggleButton.stories.tsx | 46 +++++++
.../src/js/ui-components/ToggleButton.tsx | 113 +++++++++---------
.../js/ui-components/ToggleButtonGroup.tsx | 71 +++++++++++
17 files changed, 269 insertions(+), 167 deletions(-)
create mode 100644 frontend/src/js/ui-components/ToggleButton.stories.tsx
create mode 100644 frontend/src/js/ui-components/ToggleButtonGroup.tsx
diff --git a/frontend/src/js/button/DownloadButton.tsx b/frontend/src/js/button/DownloadButton.tsx
index 6cd6dd3db0..d8193e9f85 100644
--- a/frontend/src/js/button/DownloadButton.tsx
+++ b/frontend/src/js/button/DownloadButton.tsx
@@ -43,12 +43,13 @@ export function getFileIcon(url: string): FileIcon {
return { icon: faFileDownload };
}
-interface Props extends Omit {
+interface Props {
resultUrl: ResultUrlWithLabel;
className?: string;
children?: ReactNode;
simpleIcon?: boolean;
showColoredIcon?: boolean;
+ size?: ButtonProps["size"];
}
const DownloadButton = ({
@@ -58,7 +59,7 @@ const DownloadButton = ({
className,
children,
showColoredIcon,
- ...restProps
+ size,
}: Props & { ref?: Ref }) => {
const { authToken } = useContext(AuthTokenContext);
@@ -68,7 +69,7 @@ const DownloadButton = ({
return (
-
+
{
onOpenQueryNodeEditor();
}}
>
-
+
{t("editorV2.edit")}
@@ -319,28 +324,34 @@ export function EditorV2({
{
onOpen();
}}
>
-
+
{t("editorV2.dates")}
)}
{featureNegate && selectedNode && (
-
{t("editorV2.negate")}
-
+
)}
{featureConnectorRotate && selectedNode && connection && (
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 1ec00cbc26..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";
@@ -362,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/info-pane/InfoPane.tsx b/frontend/src/js/info-pane/InfoPane.tsx
index 64f1e02067..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";
@@ -188,14 +188,14 @@ const InfoPane = () => {
conceptIcon={mainIcon}
tackIcon={
toggleAdditionalInfos && (
-
-
+
)
}
/>
diff --git a/frontend/src/js/previous-queries/list/FoldersToggleButton.tsx b/frontend/src/js/previous-queries/list/FoldersToggleButton.tsx
index 402b22c30f..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,14 +16,14 @@ const FoldersToggleButton = ({
return (
-
-
+
{t("previousQueriesFolderButton.tooltip")}
);
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 && (
- {
+ isSelected
+ onChange={() => {
// To prevent selecting the table as well, see above
if (isDisabled) {
@@ -105,7 +106,7 @@ const MenuColumnItem = ({
}}
>
-
+
{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 = (
-
+
{compact ? null : text}
diff --git a/frontend/src/js/standard-query-editor/QueryGroupActions.tsx b/frontend/src/js/standard-query-editor/QueryGroupActions.tsx
index 63720a1ae8..8272b0b604 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,
@@ -38,26 +39,24 @@ const QueryGroupActions = ({
-
{t("queryEditor.exclude")}
-
+
{t("help.queryEditorExclude")}
-
-
+
+
{t("queryEditor.date")}
{t("help.queryEditorDate")}
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")}
- (
- {intents.map((intent) => (
-
-
- {intent}
-
- ))}
-
+
- pressed
+ secondary
+
+
+
+ tertiary
),
@@ -84,32 +80,3 @@ export const Sizes: Story = {
),
};
-
-const Pressed = () => {
- const [pressed, setPressed] = useState(true);
- return (
-
- setPressed((p) => !p)}
- >
-
- toggle me
-
-
- setPressed((p) => !p)}
- >
-
-
- 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 5a8b2c453b..dc5dec271a 100644
--- a/frontend/src/js/ui-components/Button.tsx
+++ b/frontend/src/js/ui-components/Button.tsx
@@ -13,7 +13,7 @@ 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",
@@ -23,7 +23,6 @@ const button = tv({
"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,11 +38,9 @@ const button = tv({
"bg-transparent text-gray-800 border-transparent",
"hover:bg-gray-50",
],
- // reads as a text link and sits in flowing text: no box, no padding,
- // no fixed height, the surrounding text's size and line-height
+ // reads as a text link, see the compound variant for how it flows with text
link: [
- "bg-transparent border-transparent px-0",
- "h-auto leading-[inherit] text-[length:inherit] align-baseline",
+ "bg-transparent border-transparent",
"text-gray-500",
"hover:text-gray-800 hover:underline",
],
@@ -57,17 +54,22 @@ const button = tv({
// an icon-only button is a square
iconOnly: { true: "px-0" },
// a destructive or warning action, in the look of its intent
- danger: { true: "aria-pressed:text-red" },
+ danger: { true: "" },
},
compoundVariants: [
- { danger: true, intent: "primary", class: "bg-red border-red" },
+ // 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 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" },
- { danger: true, intent: "link", 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" },
@@ -75,18 +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" | "link";
- /** a destructive or warning action: red, in the look of the intent */
- danger?: boolean;
size?: "sm" | "md" | "lg";
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 (
@@ -106,9 +115,9 @@ 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 and sits in flowing text.
- * `danger` turns any intent red for destructive or warning actions.
+ * `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
@@ -122,7 +131,7 @@ export const Button = ({
...props
}: ButtonProps) => (
{options.map((option) => (
- onSelect(option.value as number)}
>
{option.label}
-
+
))}
diff --git a/frontend/src/js/ui-components/InputRange.tsx b/frontend/src/js/ui-components/InputRange.tsx
index f3ae54c205..5eaafe025a 100644
--- a/frontend/src/js/ui-components/InputRange.tsx
+++ b/frontend/src/js/ui-components/InputRange.tsx
@@ -1,12 +1,10 @@
import { useTranslation } from "react-i18next";
import { tv } from "tailwind-variants";
-
import type { CurrencyConfigT } from "../api/types";
import { exists } from "../common/helpers/exists";
-
import InputPlain from "./InputPlain/InputPlain";
import InputRangeHeader from "./InputRangeHeader";
-import ToggleButton from "./ToggleButton";
+import ToggleButtonGroup from "./ToggleButtonGroup";
const container = tv({ base: ["flex flex-row", "w-full", "-mt-[3px]"] });
@@ -119,7 +117,7 @@ const InputRange = ({
unit={unit}
tooltip={tooltip}
/>
- onSwitchMode(mode as ModeT)}
options={[
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..a4cdf79f7c
--- /dev/null
+++ b/frontend/src/js/ui-components/ToggleButton.stories.tsx
@@ -0,0 +1,46 @@
+import {
+ faBan,
+ 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 } from "./ToggleButton";
+import { Tooltip, TooltipTrigger } from "./Tooltip";
+
+export default {
+ title: "UiComponents/ToggleButton",
+ component: ToggleButton,
+ parameters: { layout: "centered" },
+} as Meta;
+
+type Story = StoryObj;
+
+const Demo = () => {
+ const [pinned, setPinned] = useState(true);
+ const [open, setOpen] = useState(false);
+ const [negated, setNegated] = useState(true);
+
+ return (
+
+
+
+
+
+ Tertiary, icon only
+
+
+
+ folders
+
+
+
+ negate
+
+
+ );
+};
+
+export const Default: Story = { render: () => };
diff --git a/frontend/src/js/ui-components/ToggleButton.tsx b/frontend/src/js/ui-components/ToggleButton.tsx
index dccd194477..cfc8ac36ef 100644
--- a/frontend/src/js/ui-components/ToggleButton.tsx
+++ b/frontend/src/js/ui-components/ToggleButton.tsx
@@ -1,71 +1,70 @@
-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",
- ],
+// selected: the primary color, or red for a warning toggle
+const toggleStyle = tv({
+ extend: buttonStyle,
variants: {
- active: {
- true: ["text-gray-800", "bg-white hover:bg-white"],
- false: ["text-gray-500", "bg-gray-50 hover:bg-bg-50"],
+ danger: {
+ true: "data-selected:text-red",
+ false: "data-selected:text-primary-500",
},
- 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;
}
-const ToggleButton = ({
- options,
- value: inputValue,
- onChange,
- className,
-}: {
- className?: string;
- options: OptionsT[];
- value: string;
- onChange: (value: string) => void;
-}) => {
+export type ToggleButtonProps = CommonProps & {
+ intent?: "secondary" | "tertiary";
+ /** a warning state: red while selected */
+ danger?: boolean;
+};
+
+const isIconOnly = (children: ReactNode) => {
+ const items = Children.toArray(children);
return (
-
- {options.map(({ value, label, description }, i) => (
-
-
- {
- if (value !== inputValue) onChange(value);
- }}
- >
- {label}
-
-
- {description}
-
- ))}
-
+ items.length > 0 &&
+ items.every((child) => isValidElement(child) && child.type === Icon)
);
};
-export default ToggleButton;
+/**
+ * A button 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.
+ *
+ *
+ *
+ *
+ */
+export const ToggleButton = ({
+ intent = "tertiary",
+ size,
+ danger = false,
+ 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) => (
+
+
+ {
+ if (value !== inputValue) onChange(value);
+ }}
+ >
+ {label}
+
+
+ {description}
+
+ ))}
+
+ );
+};
+
+export default ToggleButtonGroup;
From bf6638c6070e2cc0b0ca9bdb0dd1fea4de0f1eb8 Mon Sep 17 00:00:00 2001
From: Kai Rollmann
Date: Thu, 3 Sep 2026 17:38:15 +0200
Subject: [PATCH 05/16] editor actions: hoist the state icon helper, keeps the
component under the complexity limit
---
frontend/src/js/editor-v2/EditorV2.tsx | 14 ++++++--------
1 file changed, 6 insertions(+), 8 deletions(-)
diff --git a/frontend/src/js/editor-v2/EditorV2.tsx b/frontend/src/js/editor-v2/EditorV2.tsx
index 8c3c07fda1..b0dbab492c 100644
--- a/frontend/src/js/editor-v2/EditorV2.tsx
+++ b/frontend/src/js/editor-v2/EditorV2.tsx
@@ -102,6 +102,10 @@ const useResetOnDatasetChange = (onReset: () => void) => {
}, [datasetId, onReset]);
};
+// an action's icon shows in the primary color while its setting is in effect
+const stateIcon = (active: boolean) =>
+ active ? "text-primary-500" : undefined;
+
export function EditorV2({
featureDates,
featureNegate,
@@ -311,9 +315,7 @@ export function EditorV2({
>
{t("editorV2.edit")}
@@ -330,11 +332,7 @@ export function EditorV2({
>
{t("editorV2.dates")}
From 485116498c439af36ccfbf19cbcdf106315438df Mon Sep 17 00:00:00 2001
From: Kai Rollmann
Date: Thu, 3 Sep 2026 17:45:15 +0200
Subject: [PATCH 06/16] the results download in the queries list is a link
---
frontend/src/js/button/DownloadButton.tsx | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/frontend/src/js/button/DownloadButton.tsx b/frontend/src/js/button/DownloadButton.tsx
index d8193e9f85..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" });
@@ -49,7 +49,6 @@ interface Props {
children?: ReactNode;
simpleIcon?: boolean;
showColoredIcon?: boolean;
- size?: ButtonProps["size"];
}
const DownloadButton = ({
@@ -59,7 +58,6 @@ const DownloadButton = ({
className,
children,
showColoredIcon,
- size,
}: Props & { ref?: Ref }) => {
const { authToken } = useContext(AuthTokenContext);
@@ -69,7 +67,7 @@ const DownloadButton = ({
return (
-
+
Date: Thu, 3 Sep 2026 17:53:28 +0200
Subject: [PATCH 07/16] ToggleButton: shared intents and sizes, a highlight
axis (primary or danger) for the selected look; date and edit indicators are
toggles
---
frontend/src/js/editor-v2/EditorV2.tsx | 30 +++++++------------
.../QueryGroupActions.tsx | 16 +++++-----
.../js/ui-components/ToggleButton.stories.tsx | 7 ++++-
.../src/js/ui-components/ToggleButton.tsx | 28 +++++++++--------
4 files changed, 41 insertions(+), 40 deletions(-)
diff --git a/frontend/src/js/editor-v2/EditorV2.tsx b/frontend/src/js/editor-v2/EditorV2.tsx
index b0dbab492c..ac35bca776 100644
--- a/frontend/src/js/editor-v2/EditorV2.tsx
+++ b/frontend/src/js/editor-v2/EditorV2.tsx
@@ -102,10 +102,6 @@ const useResetOnDatasetChange = (onReset: () => void) => {
}, [datasetId, onReset]);
};
-// an action's icon shows in the primary color while its setting is in effect
-const stateIcon = (active: boolean) =>
- active ? "text-primary-500" : undefined;
-
export function EditorV2({
featureDates,
featureNegate,
@@ -302,40 +298,36 @@ export function EditorV2({
- {
+ isSelected={selectedNodeActive}
+ onChange={() => {
onOpenQueryNodeEditor();
}}
>
-
+
{t("editorV2.edit")}
-
+
)}
{featureDates && selectedNode && (
- {
+ isSelected={!!selectedNode.dates?.restriction}
+ onChange={() => {
onOpen();
}}
>
-
+
{t("editorV2.dates")}
-
+
)}
{featureNegate && selectedNode && (
@@ -343,7 +335,7 @@ export function EditorV2({
diff --git a/frontend/src/js/standard-query-editor/QueryGroupActions.tsx b/frontend/src/js/standard-query-editor/QueryGroupActions.tsx
index 8272b0b604..1bdeac572b 100644
--- a/frontend/src/js/standard-query-editor/QueryGroupActions.tsx
+++ b/frontend/src/js/standard-query-editor/QueryGroupActions.tsx
@@ -42,7 +42,7 @@ const QueryGroupActions = ({
@@ -52,13 +52,15 @@ const QueryGroupActions = ({
{t("help.queryEditorExclude")}
-
-
+
+
{t("queryEditor.date")}
-
+
{t("help.queryEditorDate")}
diff --git a/frontend/src/js/ui-components/ToggleButton.stories.tsx b/frontend/src/js/ui-components/ToggleButton.stories.tsx
index a4cdf79f7c..8a70f4f5e8 100644
--- a/frontend/src/js/ui-components/ToggleButton.stories.tsx
+++ b/frontend/src/js/ui-components/ToggleButton.stories.tsx
@@ -35,7 +35,12 @@ const Demo = () => {
folders
-
+
negate
diff --git a/frontend/src/js/ui-components/ToggleButton.tsx b/frontend/src/js/ui-components/ToggleButton.tsx
index cfc8ac36ef..9dad45c90b 100644
--- a/frontend/src/js/ui-components/ToggleButton.tsx
+++ b/frontend/src/js/ui-components/ToggleButton.tsx
@@ -8,13 +8,13 @@ import { tv } from "tailwind-variants";
import { buttonStyle } from "./Button";
import { Icon } from "./Icon";
-// selected: the primary color, or red for a warning toggle
+// while selected, the button shows its highlight color
const toggleStyle = tv({
extend: buttonStyle,
variants: {
- danger: {
- true: "data-selected:text-red",
- false: "data-selected:text-primary-500",
+ highlight: {
+ primary: "data-selected:text-primary-500",
+ danger: "data-selected:text-red",
},
},
});
@@ -26,11 +26,12 @@ interface CommonProps
ref?: Ref;
}
-export type ToggleButtonProps = CommonProps & {
+export interface ToggleButtonProps extends CommonProps {
+ /** Button's intents that can be switched on and off */
intent?: "secondary" | "tertiary";
- /** a warning state: red while selected */
- danger?: boolean;
-};
+ /** how it shows while selected: the primary color, or red for a warning state */
+ highlight?: "primary" | "danger";
+}
const isIconOnly = (children: ReactNode) => {
const items = Children.toArray(children);
@@ -41,9 +42,10 @@ const isIconOnly = (children: ReactNode) => {
};
/**
- * A button 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.
+ * 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.
*
*
*
@@ -52,7 +54,7 @@ const isIconOnly = (children: ReactNode) => {
export const ToggleButton = ({
intent = "tertiary",
size,
- danger = false,
+ highlight = "primary",
children,
...props
}: ToggleButtonProps) => (
@@ -60,7 +62,7 @@ export const ToggleButton = ({
className={toggleStyle({
intent,
size,
- danger,
+ highlight,
iconOnly: isIconOnly(children),
})}
{...props}
From 23b13ee3061d6573cfd3d1d4e83a6c36477b40f1 Mon Sep 17 00:00:00 2001
From: Kai Rollmann
Date: Thu, 3 Sep 2026 18:00:34 +0200
Subject: [PATCH 08/16] ToggleButton stories: intents, highlights, sizes,
icon-only, state with editor
---
.../js/ui-components/ToggleButton.stories.tsx | 146 +++++++++++++++---
1 file changed, 124 insertions(+), 22 deletions(-)
diff --git a/frontend/src/js/ui-components/ToggleButton.stories.tsx b/frontend/src/js/ui-components/ToggleButton.stories.tsx
index 8a70f4f5e8..7506513bc3 100644
--- a/frontend/src/js/ui-components/ToggleButton.stories.tsx
+++ b/frontend/src/js/ui-components/ToggleButton.stories.tsx
@@ -1,5 +1,7 @@
import {
faBan,
+ faCalendar,
+ faEdit,
faFolder,
faThumbtack,
} from "@fortawesome/free-solid-svg-icons";
@@ -7,7 +9,7 @@ import type { Meta, StoryObj } from "@storybook/react";
import { useState } from "react";
import { Icon } from "./Icon";
-import { ToggleButton } from "./ToggleButton";
+import { ToggleButton, type ToggleButtonProps } from "./ToggleButton";
import { Tooltip, TooltipTrigger } from "./Tooltip";
export default {
@@ -18,34 +20,134 @@ export default {
type Story = StoryObj;
-const Demo = () => {
- const [pinned, setPinned] = useState(true);
- const [open, setOpen] = useState(false);
- const [negated, setNegated] = useState(true);
-
+// 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: () => (
+
-
+
-
- Tertiary, icon only
+
+ Pin
-
-
- folders
-
+
+ ),
+};
+
+const OpensAnEditor = () => {
+ const [restriction, setRestriction] = useState
(null);
+ return (
+
+ setRestriction((current) =>
+ current === null ? window.prompt("Date restriction") : null,
+ )
+ }
>
-
- negate
+
+ {restriction ?? "date"}
-
+
);
};
-export const Default: Story = { render: () => };
+/** The selected look reflects a state the user set; pressing edits it. */
+export const StateWithEditor: Story = { render: () => };
From c892dc0a34b8c262ec7fe4353da120f9f121c3b4 Mon Sep 17 00:00:00 2001
From: Kai Rollmann
Date: Thu, 3 Sep 2026 18:01:34 +0200
Subject: [PATCH 09/16] link buttons keep their icon close: gap-1
---
frontend/src/js/ui-components/Button.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frontend/src/js/ui-components/Button.tsx b/frontend/src/js/ui-components/Button.tsx
index dc5dec271a..6fbaeb2fe8 100644
--- a/frontend/src/js/ui-components/Button.tsx
+++ b/frontend/src/js/ui-components/Button.tsx
@@ -62,7 +62,7 @@ export const buttonStyle = tv({
{
intent: "link",
class:
- "h-auto px-0 leading-[inherit] text-[length:inherit] align-baseline",
+ "h-auto px-0 gap-1 leading-[inherit] text-[length:inherit] align-baseline",
},
{
danger: true,
From b34afae0512891deae1ef95226ba2260f4e183ab Mon Sep 17 00:00:00 2001
From: Kai Rollmann
Date: Thu, 3 Sep 2026 18:03:08 +0200
Subject: [PATCH 10/16] toggle buttons are bold while selected
---
frontend/src/js/ui-components/ToggleButton.tsx | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/frontend/src/js/ui-components/ToggleButton.tsx b/frontend/src/js/ui-components/ToggleButton.tsx
index 9dad45c90b..3ced06a0e0 100644
--- a/frontend/src/js/ui-components/ToggleButton.tsx
+++ b/frontend/src/js/ui-components/ToggleButton.tsx
@@ -8,9 +8,10 @@ import { tv } from "tailwind-variants";
import { buttonStyle } from "./Button";
import { Icon } from "./Icon";
-// while selected, the button shows its highlight color
+// while selected, the button is bold and shows its highlight color
const toggleStyle = tv({
extend: buttonStyle,
+ base: "data-selected:font-bold",
variants: {
highlight: {
primary: "data-selected:text-primary-500",
From de701a3647e1b92831c4924fef9f475cb1f875b5 Mon Sep 17 00:00:00 2001
From: Kai Rollmann
Date: Thu, 3 Sep 2026 18:06:10 +0200
Subject: [PATCH 11/16] button icon gap: 8 px for sm and md, 12 px for lg
---
frontend/src/js/ui-components/Button.tsx | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/frontend/src/js/ui-components/Button.tsx b/frontend/src/js/ui-components/Button.tsx
index 6fbaeb2fe8..04f63f27cc 100644
--- a/frontend/src/js/ui-components/Button.tsx
+++ b/frontend/src/js/ui-components/Button.tsx
@@ -47,9 +47,9 @@ export const buttonStyle = tv({
},
// 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" },
From 4a31fa807b8f25b308efc8d93cec0d81fa501868 Mon Sep 17 00:00:00 2001
From: Kai Rollmann
Date: Thu, 3 Sep 2026 18:08:33 +0200
Subject: [PATCH 12/16] editor-v2 toolbar buttons are regular sized
---
frontend/src/js/editor-v2/EditorV2.tsx | 8 --------
1 file changed, 8 deletions(-)
diff --git a/frontend/src/js/editor-v2/EditorV2.tsx b/frontend/src/js/editor-v2/EditorV2.tsx
index ac35bca776..d214ab2cea 100644
--- a/frontend/src/js/editor-v2/EditorV2.tsx
+++ b/frontend/src/js/editor-v2/EditorV2.tsx
@@ -300,7 +300,6 @@ export function EditorV2({
>
{
onOpen();
@@ -334,7 +332,6 @@ export function EditorV2({
{
onRotateConnector();
@@ -367,7 +363,6 @@ export function EditorV2({
>
{
onOpenTimeModal();
}}
@@ -381,7 +376,6 @@ export function EditorV2({
{
onExpand();
}}
@@ -397,7 +391,6 @@ export function EditorV2({
{
onFlip();
@@ -414,7 +407,6 @@ export function EditorV2({
>
{
onDelete();
}}
From ec8549e524c022af17d04902407f19bdcdf487b8 Mon Sep 17 00:00:00 2001
From: Kai Rollmann
Date: Thu, 3 Sep 2026 18:11:48 +0200
Subject: [PATCH 13/16] upload results button sits in the row's gap
---
frontend/src/js/previous-queries/list/ProjectItemsTab.tsx | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/frontend/src/js/previous-queries/list/ProjectItemsTab.tsx b/frontend/src/js/previous-queries/list/ProjectItemsTab.tsx
index f929a67786..2513aa0e0c 100644
--- a/frontend/src/js/previous-queries/list/ProjectItemsTab.tsx
+++ b/frontend/src/js/previous-queries/list/ProjectItemsTab.tsx
@@ -76,9 +76,7 @@ const ProjectItemsTab = ({ datasetId }: PropsT) => {
onClick={onToggleFoldersOpen}
/>
- {hasPermissionToUpload && (
-
- )}
+ {hasPermissionToUpload && }
From adf31d2479e5afd497f9bf4150da99b04f04b17d Mon Sep 17 00:00:00 2001
From: Kai Rollmann
Date: Thu, 3 Sep 2026 18:13:48 +0200
Subject: [PATCH 14/16] query name row without the extra vertical margin
---
frontend/src/js/previous-queries/list/ProjectItem.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frontend/src/js/previous-queries/list/ProjectItem.tsx b/frontend/src/js/previous-queries/list/ProjectItem.tsx
index c5c63305ce..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({
From 9240816a4011428f5e32e16c6161524c3f945628 Mon Sep 17 00:00:00 2001
From: Kai Rollmann
Date: Thu, 3 Sep 2026 18:37:42 +0200
Subject: [PATCH 15/16] edit button wrapper is flex, the query name row is 24
px
---
frontend/src/js/ui-components/EditableText.tsx | 2 ++
1 file changed, 2 insertions(+)
diff --git a/frontend/src/js/ui-components/EditableText.tsx b/frontend/src/js/ui-components/EditableText.tsx
index d98a0e13dc..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]",
From b1c8f89bcd7bb8130247bff0fb0ef6cc64187057 Mon Sep 17 00:00:00 2001
From: Kai Rollmann
Date: Thu, 3 Sep 2026 22:48:35 +0200
Subject: [PATCH 16/16] history sidebar on ToggleButton and Button, no local
control style
---
.../src/js/entity-history/ContentControl.tsx | 11 +++----
.../src/js/entity-history/DetailControl.tsx | 6 ++--
.../js/entity-history/InteractionControl.tsx | 18 ++++++++---
.../src/js/entity-history/SidebarControl.tsx | 31 -------------------
.../js/entity-history/VisibilityControl.tsx | 6 ++--
.../timeline-search/SearchControl.tsx | 6 ++--
6 files changed, 26 insertions(+), 52 deletions(-)
delete mode 100644 frontend/src/js/entity-history/SidebarControl.tsx
diff --git a/frontend/src/js/entity-history/ContentControl.tsx b/frontend/src/js/entity-history/ContentControl.tsx
index 100ccc6fed..f36349178a 100644
--- a/frontend/src/js/entity-history/ContentControl.tsx
+++ b/frontend/src/js/entity-history/ContentControl.tsx
@@ -8,8 +8,8 @@ import { memo, useMemo, useState } from "react";
import { ToggleButtonGroup } from "react-aria-components";
import { useTranslation } from "react-i18next";
import { Icon } from "../ui-components/Icon";
+import { ToggleButton } from "../ui-components/ToggleButton";
import { Tooltip, TooltipTrigger } from "../ui-components/Tooltip";
-import { SidebarToggle } from "./SidebarControl";
export type ContentType =
| "groupId"
@@ -69,12 +69,9 @@ const ContentControl = ({ value, onChange }: Props) => {
>
{options.map((option) => (
-
-
-
+
+
+
{option.tooltip}
))}
diff --git a/frontend/src/js/entity-history/DetailControl.tsx b/frontend/src/js/entity-history/DetailControl.tsx
index 79baeea773..9b860615a7 100644
--- a/frontend/src/js/entity-history/DetailControl.tsx
+++ b/frontend/src/js/entity-history/DetailControl.tsx
@@ -9,8 +9,8 @@ import { type Key, ToggleButtonGroup } from "react-aria-components";
import { useTranslation } from "react-i18next";
import { tv } from "tailwind-variants";
import { Icon } from "../ui-components/Icon";
+import { ToggleButton } from "../ui-components/ToggleButton";
import { Tooltip, TooltipTrigger } from "../ui-components/Tooltip";
-import { SidebarToggle } from "./SidebarControl";
const root = tv({ base: "flex flex-col items-center" });
@@ -70,9 +70,9 @@ export const DetailControl = memo(
>
{navOptions.map(({ value, icon, tooltip }) => (
-
+
-
+
{tooltip}
))}
diff --git a/frontend/src/js/entity-history/InteractionControl.tsx b/frontend/src/js/entity-history/InteractionControl.tsx
index 218c5b9d32..9e7332e58b 100644
--- a/frontend/src/js/entity-history/InteractionControl.tsx
+++ b/frontend/src/js/entity-history/InteractionControl.tsx
@@ -1,9 +1,9 @@
import { faChevronRight, faHome } 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 { Tooltip, TooltipTrigger } from "../ui-components/Tooltip";
-import { SidebarAction } from "./SidebarControl";
const InteractionControl = ({
onCloseAll,
@@ -17,15 +17,23 @@ const InteractionControl = ({
return (
-
+
-
+
{t("history.closeAll")}
-
+
-
+
{t("history.openAll")}
diff --git a/frontend/src/js/entity-history/SidebarControl.tsx b/frontend/src/js/entity-history/SidebarControl.tsx
deleted file mode 100644
index 1fa2f93d12..0000000000
--- a/frontend/src/js/entity-history/SidebarControl.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import {
- Button as RacButton,
- type ButtonProps as RacButtonProps,
- ToggleButton as RacToggleButton,
- type ToggleButtonProps as RacToggleButtonProps,
-} from "react-aria-components";
-import { tv } from "tailwind-variants";
-
-// The history sidebar's vertical toolbar: square controls, a selected one
-// shows in the primary color. Local to the history, not a design-system part.
-const control = tv({
- base: [
- "inline-flex items-center justify-center",
- "size-[30px]",
- "rounded",
- "border border-transparent",
- "text-gray-800",
- "cursor-pointer",
- "hover:bg-gray-50",
- "data-selected:bg-gray-100 data-selected:text-primary-500",
- "transition-colors duration-100",
- ],
-});
-
-export const SidebarToggle = (props: RacToggleButtonProps) => (
-
-);
-
-export const SidebarAction = (props: RacButtonProps) => (
-
-);
diff --git a/frontend/src/js/entity-history/VisibilityControl.tsx b/frontend/src/js/entity-history/VisibilityControl.tsx
index 275154c5eb..c87ea0efd1 100644
--- a/frontend/src/js/entity-history/VisibilityControl.tsx
+++ b/frontend/src/js/entity-history/VisibilityControl.tsx
@@ -2,8 +2,8 @@ import { faEye, faEyeSlash } from "@fortawesome/free-regular-svg-icons";
import { memo } from "react";
import { useTranslation } from "react-i18next";
import { Icon } from "../ui-components/Icon";
+import { ToggleButton } from "../ui-components/ToggleButton";
import { Tooltip, TooltipTrigger } from "../ui-components/Tooltip";
-import { SidebarToggle } from "./SidebarControl";
const VisibilityControl = ({
blurred,
@@ -17,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 c29d648c44..746a128cf4 100644
--- a/frontend/src/js/entity-history/timeline-search/SearchControl.tsx
+++ b/frontend/src/js/entity-history/timeline-search/SearchControl.tsx
@@ -2,8 +2,8 @@ import { faSearch } from "@fortawesome/free-solid-svg-icons";
import { memo } from "react";
import { useTranslation } from "react-i18next";
import { Icon } from "../../ui-components/Icon";
+import { ToggleButton } from "../../ui-components/ToggleButton";
import { Tooltip, TooltipTrigger } from "../../ui-components/Tooltip";
-import { SidebarToggle } from "../SidebarControl";
import { useTimelineSearch } from "./timelineSearchState";
const SearchControl = () => {
@@ -14,13 +14,13 @@ const SearchControl = () => {
return (
-
-
+
{t("history.search")}