Consolidate buttons and related components (#6787)

SegmentedControl, Chip, ChipFlow. Bring in the portal dark mode theme
and other small fixes to issues I found during testing
This commit is contained in:
EthanHealy01
2026-07-07 16:06:56 +00:00
committed by GitHub
parent be97268a7c
commit 8ba8f69252
592 changed files with 6399 additions and 5057 deletions
+1
View File
@@ -0,0 +1 @@
declare module "*.css" {}
+30 -21
View File
@@ -14,7 +14,7 @@ void React;
import { TierProvider, type Tier } from "@portal/contexts/TierContext";
import { LinkProvider, type LinkState } from "@portal/contexts/LinkContext";
import { ThemeProvider } from "@portal/contexts/ThemeContext";
import { ThemeProvider, useTheme } from "@portal/contexts/ThemeContext";
import { UIProvider } from "@portal/contexts/UIContext";
import { SuiProvider } from "@portal/theme/SuiProvider";
import { handlers } from "@portal/mocks/handlers";
@@ -78,13 +78,21 @@ function TierKey({
);
}
/** Keeps useTheme() and the data-theme attribute in sync. */
function ThemeWatcher() {
/**
* Makes the Storybook toolbar the SINGLE source of truth for the theme.
*/
function ThemeBridge({
theme,
children,
}: {
theme: "light" | "dark";
children: React.ReactNode;
}) {
const { setTheme } = useTheme();
useEffect(() => {
// The addon-themes decorator already sets data-theme on <html>.
// We just read it on mount so ThemeProvider picks it up.
}, []);
return null;
setTheme(theme);
}, [theme, setTheme]);
return <>{children}</>;
}
const withProviders: Decorator = (Story, context) => {
@@ -101,20 +109,21 @@ const withProviders: Decorator = (Story, context) => {
return (
<MemoryRouter initialEntries={["/"]}>
<ThemeProvider>
<SuiProvider colorScheme={colorScheme}>
{/* LinkProvider must wrap TierProvider: TierContext derives its tier
from useLink() (matches App.tsx's nesting). */}
<LinkProvider key={linkState} initialState={linkState}>
<TierKey tier={tier}>
<UIProvider>
<ThemeWatcher />
<Suspense fallback={null}>
<Story />
</Suspense>
</UIProvider>
</TierKey>
</LinkProvider>
</SuiProvider>
<ThemeBridge theme={colorScheme}>
<SuiProvider colorScheme={colorScheme}>
{/* LinkProvider must wrap TierProvider: TierContext derives its tier
from useLink() (matches App.tsx's nesting). */}
<LinkProvider key={linkState} initialState={linkState}>
<TierKey tier={tier}>
<UIProvider>
<Suspense fallback={null}>
<Story />
</Suspense>
</UIProvider>
</TierKey>
</LinkProvider>
</SuiProvider>
</ThemeBridge>
</ThemeProvider>
</MemoryRouter>
);
+24
View File
@@ -0,0 +1,24 @@
{
"compilerOptions": {
"target": "es2022",
"jsx": "react-jsx",
"module": "esnext",
"moduleResolution": "bundler",
"paths": {
"@app/*": [
"../editor/src/desktop/*",
"../editor/src/proprietary/*",
"../editor/src/core/*"
],
"@core/*": ["../editor/src/core/*"],
"@proprietary/*": ["../editor/src/proprietary/*"],
"@portal/*": ["../editor/src/portal/*"]
},
"resolveJsonModule": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
},
"include": ["./**/*"]
}
@@ -7,6 +7,7 @@ black = "Black"
blue = "Blue"
cancel = "Cancel"
chooseFile = "Choose File"
clear = "Clear"
close = "Close"
comingSoon = "Coming soon"
confirm = "Confirm"
@@ -87,6 +88,7 @@ processingCompleteMultiple = "{{count}} files are ready."
property = "Property"
quickPosition = "Quick Position"
red = "Red"
remove = "Remove"
reset = "Reset"
review = "Review"
save = "Save"
@@ -180,6 +182,7 @@ addMoreFiles = "Add more files..."
attachments = "Select Attachments"
info = "Select files to attach to your PDF. These files will be embedded and accessible through the PDF's attachment panel."
placeholder = "Choose files..."
removeFile = "Remove file"
selectedFiles = "Selected Files"
submit = "Add Attachments"
@@ -1628,6 +1631,9 @@ title = "Do you want to help make Stirling PDF better?"
tags = "annotate,highlight,draw,markup,comment,notes,review,redline,feedback,markup tools,sticky notes,shapes,arrows,text box,freehand"
[annotation]
alignCenter = "Align center"
alignLeft = "Align left"
alignRight = "Align right"
annotationStyle = "Annotation style"
backgroundColor = "Background color"
borderOff = "Border: Off"
@@ -2531,6 +2537,7 @@ title = "Rule of thumb"
[certSign.source]
device = "This device"
noOtherSources = "No other certificate sources are available."
server = "Server"
stepTitle = "Certificate source"
upload = "Upload"
@@ -3595,7 +3602,9 @@ makeCopy = "Make a copy"
mobileShort = "Mobile"
mobileUpload = "Mobile Upload"
mobileUploadNotAvailable = "Mobile upload not enabled"
moreOptions = "More options"
myFiles = "My Files"
nextFile = "Next file"
noFiles = "No files available"
noFilesFound = "No files found matching your search"
noRecentFiles = "No recent files found"
@@ -3605,6 +3614,7 @@ openInFileEditor = "Open in File Editor"
openInPageEditor = "Open in Page Editor"
owner = "Owner"
ownerUnknown = "Unknown"
previousFile = "Previous file"
recent = "Recent"
removeBoth = "Remove from both"
removeFilePrompt = "This file is saved on this device and on your server. Where would you like to remove it from?"
@@ -4614,10 +4624,10 @@ welcomeTitle = "You've been invited!"
[landing]
addFiles = "Add Files"
heroSubtitle = "Drop in or add an existing PDF to get started."
heroTitle = "Stirling PDF"
mobileUpload = "Upload from Mobile"
openFromComputer = "Open from computer"
uploadFromComputer = "Upload from computer"
workbenchEmptyStateHero = "Drop a PDF anywhere"
[language]
direction = "ltr"
@@ -4930,6 +4940,7 @@ title = "Output"
[onboarding]
activeFiles = "The <strong>Active Files</strong> view shows all of the PDFs you have loaded into the tool, and allows you to select which ones to process."
allTools = "This is the <strong>Tools</strong> panel, where you can browse and select from all available PDF tools."
close = "Close"
cropSettings = "Now that we've selected the file we want crop, we can configure the Crop tool to choose the area that we want to crop the PDF to."
fileCheckbox = "Clicking one of the files selects it for processing. You can select multiple files for batch operations."
fileReplacement = "The modified file will replace the original file in the Workbench automatically, allowing you to easily run it through more tools."
@@ -4955,6 +4966,7 @@ skipTheTour = "Skip the tour"
[onboarding.desktopInstall]
body = "Stirling works best as a desktop app. You can use it offline, access documents faster, and make edits locally on your computer."
selectOs = "Select operating system"
title = "Download"
titleWithOs = "Download for {{osLabel}}"
@@ -5566,7 +5578,9 @@ viewLabel = "PDF Editor"
[pdfTextEditor.actions]
applyChanges = "Apply Changes"
clearText = "Clear text"
downloadCopy = "Download Copy"
moreOptions = "More options"
reset = "Reset Changes"
[pdfTextEditor.badges]
@@ -9670,7 +9684,9 @@ memberRemoved = "Member removed successfully"
namePlaceholder = "Enter team name"
personal = "Personal"
removeError = "Failed to remove member"
renameCancel = "Cancel rename"
renameError = "Failed to rename team"
renameSubmit = "Save team name"
renameSuccess = "Team renamed successfully"
[team.invitationBanner]
@@ -9686,6 +9702,7 @@ sendButton = "Send Invitation"
title = "Invite Team Member"
[team.members]
actions = "Member actions"
emailColumn = "Email"
empty = "No team members yet"
nameColumn = "Name"
@@ -10041,14 +10058,23 @@ zoomOut = "Zoom Out"
[viewer.attachments]
addAttachment = "Add attachment"
close = "Close attachments"
closeSidebar = "Close attachments sidebar"
download = "Download attachment"
empty = "No attachments in this document"
loading = "Loading attachments..."
noDocument = "Open a PDF to view its attachments."
noMatch = "No attachments match your search"
noSupport = "Attachment support is unavailable for this viewer."
retry = "Retry"
searchPlaceholder = "Search attachments"
title = "Attachments"
[viewer.bookmarks]
bookmarkTitle = "Bookmark title"
closeSidebar = "Close bookmarks sidebar"
collapseAll = "Collapse all bookmarks"
expandAll = "Expand all bookmarks"
[viewer.comments]
addComment = "Add comment"
addCommentPlaceholder = "Add comment..."
@@ -10059,6 +10085,7 @@ clearAll = "Clear all comments"
clearAllDescription = "This removes comments and replies from the sidebar while keeping any attached annotations in the document."
clearAllTitle = "Clear all comments?"
close = "Close comments"
closeSidebar = "Close comments sidebar"
deleteAnnotationAndComment = "Delete annotation & comment"
deleteDescription = "This annotation has a comment attached. You can remove just the comment from the sidebar while keeping the annotation, or delete everything."
deleteTitle = "Remove annotation from comments?"
@@ -10090,6 +10117,11 @@ title = "Form Fields"
unsavedBadge = "Unsaved"
unsavedDesc = "You have unsaved changes"
[viewer.layers]
closeSidebar = "Close layers sidebar"
hideAll = "Hide all layers"
showAll = "Show all layers"
[viewer.link]
delete = "Delete link"
@@ -10120,6 +10152,9 @@ resultsOf = "of {{total}}"
[viewer.signature]
delete = "Delete signature"
[viewer.thumbnails]
closeSidebar = "Close thumbnails sidebar"
[viewPdf]
tags = "view,read,annotate,text,image,highlight,edit"
title = "View/Edit PDF"
@@ -10546,6 +10581,7 @@ loading = "Loading people..."
locked = "locked"
lockedBadge = "Locked"
loginRequired = "Enable login mode first"
memberActions = "Member actions"
noMembersFound = "No members found"
role = "Role"
searchMembers = "Search members..."
@@ -10555,6 +10591,7 @@ unlockAccount = "Unlock Account"
unlockUserError = "Failed to unlock user account"
unlockUserSuccess = "User account unlocked successfully"
user = "User"
userInfo = "User info"
[workspace.people.actions]
upgrade = "Upgrade"
@@ -10700,6 +10737,7 @@ removeMemberError = "Failed to remove user from team"
removeMemberSuccess = "User removed from team"
renameTeamLabel = "Rename Team"
system = "System"
teamActions = "Team actions"
teamName = "Team Name"
teamNotFound = "Team not found"
title = "Teams"
@@ -1,5 +1,7 @@
import React from "react";
import { Button, Group, ActionIcon } from "@mantine/core";
import { Group } from "@mantine/core";
import { Button } from "@app/ui/Button";
import { ActionIcon } from "@app/ui/ActionIcon";
import ChevronLeftIcon from "@mui/icons-material/ChevronLeft";
import { TFunction } from "i18next";
import {
@@ -31,22 +33,6 @@ export function renderButtons({
(btn) => btn.group === "right",
);
const buttonStyles = (variant: ButtonDefinition["variant"]) =>
variant === "primary"
? {
root: {
background: "var(--onboarding-primary-button-bg)",
color: "var(--onboarding-primary-button-text)",
},
}
: {
root: {
background: "var(--onboarding-secondary-button-bg)",
border: "1px solid var(--onboarding-secondary-button-border)",
color: "var(--onboarding-secondary-button-text)",
},
};
const resolveButtonLabel = (button: ButtonDefinition) => {
// Translate the label (it's a translation key)
const label = button.label ?? "";
@@ -65,20 +51,15 @@ export function renderButtons({
<ActionIcon
key={button.key}
onClick={() => onAction(button.action)}
radius="md"
size={40}
size="lg"
variant="secondary"
accent="neutral"
disabled={disabled}
styles={{
root: {
background: "var(--onboarding-secondary-button-bg)",
border: "1px solid var(--onboarding-secondary-button-border)",
color: "var(--onboarding-secondary-button-text)",
},
}}
aria-label={t("onboarding.buttons.back", "Back")}
>
{button.icon === "chevron-left" && (
{button.icon === "chevron-left" ? (
<ChevronLeftIcon fontSize="small" />
)}
) : null}
</ActionIcon>
);
}
@@ -91,7 +72,8 @@ export function renderButtons({
key={button.key}
onClick={() => onAction(button.action)}
disabled={disabled}
styles={buttonStyles(variant)}
variant={variant === "primary" ? "primary" : "secondary"}
accent="neutral"
>
{label}
</Button>
@@ -1,5 +1,6 @@
import React, { useEffect, useState } from "react";
import { Badge, Button, TextInput } from "@mantine/core";
import { Badge, TextInput } from "@mantine/core";
import { Button } from "@app/ui/Button";
import { useTranslation } from "react-i18next";
import { SlideConfig } from "@app/types/types";
import { createLightSlideBackground } from "@app/components/onboarding/slides/unifiedBackgroundConfig";
@@ -1,5 +1,6 @@
import { useMemo } from "react";
import { Modal, Stack, Button } from "@mantine/core";
import { Modal, Stack } from "@mantine/core";
import { Button } from "@app/ui/Button";
import { useTranslation } from "react-i18next";
import CelebrationIcon from "@mui/icons-material/CelebrationOutlined";
import AnimatedSlideBackground from "@app/components/onboarding/slides/AnimatedSlideBackground";
@@ -169,7 +170,7 @@ export function FreeLimitReachedModal({ onClose }: FreeLimitReachedModalProps) {
>
<Button
onClick={onClose}
variant="default"
variant="secondary"
size="sm"
className="free-limit-modal-button"
style={{
@@ -1,5 +1,6 @@
import { useMemo } from "react";
import { Modal, Stack, Button } from "@mantine/core";
import { Modal, Stack } from "@mantine/core";
import { Button } from "@app/ui/Button";
import { useTranslation } from "react-i18next";
import TrendingUpIcon from "@mui/icons-material/TrendingUpOutlined";
import AnimatedSlideBackground from "@app/components/onboarding/slides/AnimatedSlideBackground";
@@ -167,7 +168,7 @@ export function SpendCapReachedModal({ onClose }: SpendCapReachedModalProps) {
>
<Button
onClick={onClose}
variant="default"
variant="secondary"
size="sm"
className="spend-cap-modal-button"
style={{
@@ -1,5 +1,6 @@
import { useState } from "react";
import { Button, Group, Text } from "@mantine/core";
import { Group, Text } from "@mantine/core";
import { Button } from "@app/ui/Button";
import { useTranslation } from "react-i18next";
import LocalIcon from "@app/components/shared/LocalIcon";
import { InfoBanner } from "@app/components/shared/InfoBanner";
@@ -75,9 +76,9 @@ export function TeamInvitationBanner() {
const actionButtons = (
<Group gap="xs" wrap="nowrap">
<Button
variant="white"
color="gray"
size="xs"
variant="secondary"
accent="neutral"
size="sm"
onClick={handleAccept}
loading={processing}
leftSection={
@@ -88,17 +89,12 @@ export function TeamInvitationBanner() {
style={{ color: "var(--mantine-color-dark-9)" }}
/>
}
styles={{
label: {
color: "var(--mantine-color-dark-9)",
},
}}
>
{t("team.invitationBanner.acceptButton", "Accept")}
</Button>
<Button
variant="subtle"
size="xs"
variant="tertiary"
size="sm"
onClick={handleReject}
loading={processing}
style={{ color: "rgba(255, 255, 255, 0.7)" }}
@@ -14,14 +14,13 @@
--payg-divider: var(--border-subtle);
}
/* Dark mode: the modal content bg is #2a2f36 and so is --bg-surface, so plain
cards vanish. Lift cards a shade above the modal and strengthen borders. */
/* Dark mode: modal content bg is #131729 (--bg-surface), so lift cards above it */
[data-mantine-color-scheme="dark"] .payg {
--payg-card-bg: #313842;
--payg-card-border: #3d444e;
--payg-inset-bg: #272c33;
--payg-accent-soft: rgba(10, 139, 255, 0.16);
--payg-divider: #3d444e;
--payg-card-bg: #1c2340;
--payg-card-border: #2d3560;
--payg-inset-bg: #0d1020;
--payg-accent-soft: rgba(79, 142, 245, 0.16);
--payg-divider: #2d3560;
}
/* ── Header ─────────────────────────────────────────────────────────── */
@@ -258,16 +257,16 @@
/* Dark mode: the stock default button is a flat slab that disappears into the
card. Lift it with a soft top-down gradient + brighter border, and warm the
hover so the buttons feel tactile against --payg-card-bg (#313842). Driven
hover so the buttons feel tactile against --payg-card-bg (#1c2340). Driven
through Mantine's --button-* vars. The same rule covers disabled buttons
(Cancel/Update cap before the form is dirty) so they read identically to the
always-enabled ones — Mantine only fades them via opacity. */
[data-mantine-color-scheme="dark"]
.payg
.mantine-Button-root[data-variant="default"] {
--button-bg: linear-gradient(180deg, #3d4651 0%, #353d48 100%);
--button-hover: linear-gradient(180deg, #475160 0%, #3d4654 100%);
--button-bd: 1px solid #4b5563;
--button-bg: linear-gradient(180deg, #232a50 0%, #1c2340 100%);
--button-hover: linear-gradient(180deg, #2d3560 0%, #232a50 100%);
--button-bd: 1px solid #2d3560;
--button-color: var(--mantine-color-white);
box-shadow:
0 1px 0 rgba(255, 255, 255, 0.04) inset,
@@ -16,7 +16,8 @@
* V1 — so it shows a real empty state, not fabricated rows
*/
import React, { useState } from "react";
import { Button, Group, Stack, Text } from "@mantine/core";
import { Group, Stack, Text } from "@mantine/core";
import { Button } from "@app/ui/Button";
import { useRenderCount } from "@app/hooks/useRenderCount";
import OpenInNewIcon from "@mui/icons-material/OpenInNew";
import LockIcon from "@mui/icons-material/LockOutlined";
@@ -110,16 +111,16 @@ export function DocHelp() {
const [open, setOpen] = useState(false);
return (
<div className="payg-help">
<button
type="button"
<Button
variant="tertiary"
className="payg-help__toggle"
aria-expanded={open}
onClick={() => setOpen((o) => !o)}
leftSection={<HelpOutlineIcon sx={{ fontSize: 15 }} />}
>
<HelpOutlineIcon sx={{ fontSize: 15 }} />
{t("payg.docHelp.toggle", "What counts as a PDF?")}
<ExpandMoreIcon className="payg-help__chevron" sx={{ fontSize: 16 }} />
</button>
</Button>
{open && (
<div className="payg-help__panel">
<ul>
@@ -446,16 +447,16 @@ function CapReachedHelp() {
const [open, setOpen] = useState(false);
return (
<div className="payg-help">
<button
type="button"
<Button
variant="tertiary"
className="payg-help__toggle"
aria-expanded={open}
onClick={() => setOpen((o) => !o)}
leftSection={<HelpOutlineIcon sx={{ fontSize: 15 }} />}
>
<HelpOutlineIcon sx={{ fontSize: 15 }} />
{t("payg.gates.title", "What happens when the cap is reached")}
<ExpandMoreIcon className="payg-help__chevron" sx={{ fontSize: 16 }} />
</button>
</Button>
{open && (
<div className="payg-help__panel">
<div className="payg-gates">
@@ -656,7 +657,7 @@ function StripePortalLink({
onClick={handleClick}
loading={loading}
rightSection={<OpenInNewIcon sx={{ fontSize: 16 }} />}
variant="light"
variant="secondary"
>
{t("payg.stripe.open", "Open billing portal")}
</Button>
@@ -217,25 +217,6 @@
border-top: 1px solid var(--payg-divider);
padding-top: 16px;
}
.paygf-cta__button {
padding: 13px 22px;
border: none;
border-radius: 11px;
background: linear-gradient(135deg, var(--payg-accent) 0%, #6c5ce7 100%);
color: white;
font-weight: 600;
font-size: 0.95rem;
font-family: inherit;
cursor: pointer;
transition:
transform 120ms ease,
box-shadow 120ms ease;
white-space: nowrap;
}
.paygf-cta__button:hover {
transform: translateY(-1px);
box-shadow: 0 8px 22px -6px rgba(10, 139, 255, 0.55);
}
.paygf-cta__reassurance {
margin: 0;
font-size: 0.78rem;
@@ -25,6 +25,7 @@
*/
import React, { useState } from "react";
import { Stack } from "@mantine/core";
import { Button } from "@app/ui/Button";
import BoltIcon from "@mui/icons-material/BoltRounded";
import AllInclusiveIcon from "@mui/icons-material/AllInclusiveRounded";
import CheckIcon from "@mui/icons-material/CheckRounded";
@@ -171,14 +172,14 @@ function ProcessorCard({ snap, isLeader, onTurnOn }: ProcessorCardProps) {
<FreeMeterPanel snap={snap} />
{isLeader ? (
<>
<button
type="button"
className="paygf-cta__button paygf-proc__cta"
<Button
fullWidth
className="paygf-proc__cta"
onClick={onTurnOn}
data-testid="turn-on-processor"
>
{t("payg.free.cta.button", "Turn on Processor →")}
</button>
</Button>
<span className="paygf-cta__reassurance paygf-proc__reassure">
{t(
"payg.free.cta.reassurance",
@@ -22,10 +22,10 @@
gap: 14px;
}
[data-mantine-color-scheme="dark"] .scc {
--scc-accent-text: #66b8ff;
--scc-accent-soft: rgba(10, 139, 255, 0.16);
--scc-chip-bg: #272d35;
--scc-chip-border: #3d444e;
--scc-accent-text: #7ab4ff;
--scc-accent-soft: rgba(79, 142, 245, 0.16);
--scc-chip-bg: #1c2340;
--scc-chip-border: #2d3560;
}
/* ── Inline row: presets · custom · no-cap · (save) ──────────────────── */
@@ -64,6 +64,7 @@
*/
import React, { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { Button } from "@app/ui/Button";
import { useAuth } from "@app/auth/UseSession";
import {
createCheckoutSession,
@@ -267,17 +268,12 @@ const StripeCheckoutPanel: React.FC<StripeCheckoutPanelProps> = ({
)}
</div>
<div style={{ marginTop: 12 }}>
<button
type="button"
className="upm-btn"
data-variant="primary"
onClick={onComplete}
>
<Button onClick={onComplete}>
{t(
"payg.checkout.mock.continue",
"Continue with mock subscription",
)}
</button>
</Button>
</div>
</div>
);
@@ -1,6 +1,5 @@
import React, { useState, useEffect } from "react";
import {
Button,
TextInput,
Group,
Text,
@@ -8,9 +7,10 @@ import {
Alert,
Table,
Badge,
ActionIcon,
Menu,
} from "@mantine/core";
import { Button } from "@app/ui/Button";
import { ActionIcon } from "@app/ui/ActionIcon";
import { useTranslation } from "react-i18next";
import { useSaaSTeam } from "@app/contexts/SaaSTeamContext";
import LocalIcon from "@app/components/shared/LocalIcon";
@@ -233,19 +233,18 @@ const TeamSection: React.FC = () => {
}}
/>
<ActionIcon
variant="filled"
color="blue"
onClick={handleRenameSubmit}
loading={renamingTeam}
disabled={!newTeamName.trim()}
aria-label={t("team.renameSubmit", "Save team name")}
>
<LocalIcon icon="check" width="1rem" height="1rem" />
</ActionIcon>
<ActionIcon
variant="subtle"
color="gray"
variant="tertiary"
onClick={handleCancelRename}
disabled={renamingTeam}
aria-label={t("team.renameCancel", "Cancel rename")}
>
<LocalIcon icon="close" width="1rem" height="1rem" />
</ActionIcon>
@@ -257,7 +256,7 @@ const TeamSection: React.FC = () => {
</Text>
{isTeamLeader && !isPersonalTeam && (
<ActionIcon
variant="subtle"
variant="tertiary"
size="sm"
onClick={handleStartRename}
aria-label={t("team.editName", "Edit team name")}
@@ -285,9 +284,9 @@ const TeamSection: React.FC = () => {
</div>
{!isPersonalTeam && !isTeamLeader && !isEditingName && (
<Button
color="red"
variant="outline"
size="xs"
accent="danger"
variant="secondary"
size="sm"
onClick={handleLeaveTeam}
leftSection={
<LocalIcon icon="logout" width="1rem" height="1rem" />
@@ -449,7 +448,13 @@ const TeamSection: React.FC = () => {
zIndex={Z_INDEX_OVER_CONFIG_MODAL}
>
<Menu.Target>
<ActionIcon variant="subtle">
<ActionIcon
variant="tertiary"
aria-label={t(
"team.members.actions",
"Member actions",
)}
>
<LocalIcon
icon="more-vert"
width="1rem"
@@ -504,8 +509,8 @@ const TeamSection: React.FC = () => {
{isTeamLeader && !isPersonalTeam && (
<Table.Td>
<ActionIcon
variant="subtle"
color="red"
variant="tertiary"
accent="danger"
onClick={() =>
handleCancelInvitation(
invitation.invitationId,
@@ -17,10 +17,10 @@
--upm-muted: var(--text-muted);
}
[data-mantine-color-scheme="dark"] .upm {
--upm-card-bg: #313842;
--upm-border: #3d444e;
--upm-divider: #3d444e;
--upm-accent-soft: rgba(10, 139, 255, 0.14);
--upm-card-bg: #1c2340;
--upm-border: #2d3560;
--upm-divider: #2d3560;
--upm-accent-soft: rgba(79, 142, 245, 0.14);
}
/* Backdrop locks the page and centres the modal. z-index matches
@@ -101,26 +101,6 @@
color: var(--upm-text);
margin: 0;
}
.upm-header__close {
background: transparent;
border: none;
color: var(--upm-muted);
cursor: pointer;
width: 32px;
height: 32px;
border-radius: 8px;
display: inline-flex;
align-items: center;
justify-content: center;
transition:
background 120ms ease,
color 120ms ease;
}
.upm-header__close:hover {
background: var(--upm-divider);
color: var(--upm-text);
}
/* Left cluster: optional back arrow + title. The back arrow only renders on the
checkout step (cap/confirm have no parent step to return to) — it replaces the
old footer "← Back" button. */
@@ -130,28 +110,6 @@
gap: 6px;
min-width: 0;
}
.upm-header__back {
background: transparent;
border: none;
color: var(--upm-muted);
cursor: pointer;
width: 32px;
height: 32px;
border-radius: 8px;
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
margin-left: -6px;
transition:
background 120ms ease,
color 120ms ease;
}
.upm-header__back:hover {
background: var(--upm-divider);
color: var(--upm-text);
}
.upm-steps {
display: flex;
align-items: center;
@@ -357,41 +315,6 @@
display: flex;
gap: 8px;
}
.upm-btn {
padding: 10px 18px;
border-radius: 10px;
border: 1.5px solid transparent;
font-weight: 600;
font-size: 0.9rem;
cursor: pointer;
transition: all 120ms ease;
font-family: inherit;
white-space: nowrap;
}
.upm-btn[data-variant="ghost"] {
background: transparent;
border-color: var(--upm-divider);
color: var(--upm-text);
}
.upm-btn[data-variant="ghost"]:hover {
border-color: var(--upm-accent);
color: var(--upm-accent);
}
.upm-btn[data-variant="primary"] {
background: linear-gradient(135deg, var(--upm-accent), var(--upm-accent-2));
color: white;
}
.upm-btn[data-variant="primary"]:hover {
transform: translateY(-1px);
box-shadow: 0 6px 18px -6px rgba(10, 139, 255, 0.5);
}
.upm-btn[disabled] {
opacity: 0.5;
cursor: not-allowed;
transform: none !important;
box-shadow: none !important;
}
/* ── Step 3: confirmation ─────────────────────────────────────────────── */
.upm-confirm {
@@ -18,6 +18,8 @@
*/
import React, { Suspense, useEffect, useState } from "react";
import { createPortal } from "react-dom";
import { Button } from "@app/ui/Button";
import { ActionIcon } from "@app/ui/ActionIcon";
import CloseIcon from "@mui/icons-material/CloseRounded";
import ArrowBackIcon from "@mui/icons-material/ArrowBackRounded";
import ShieldIcon from "@mui/icons-material/ShieldOutlined";
@@ -152,14 +154,14 @@ export default function UpgradeModal({
<header className="upm-header">
<div className="upm-header__left">
{step === "checkout" && (
<button
type="button"
className="upm-header__back"
<ActionIcon
variant="tertiary"
aria-label={t("payg.upgrade.backAria", "Back")}
onClick={goBackToCap}
style={{ marginLeft: -6 }}
>
<ArrowBackIcon fontSize="small" />
</button>
</ActionIcon>
)}
<h2 className="upm-header__title">
{step === "confirm"
@@ -170,14 +172,13 @@ export default function UpgradeModal({
)}
</h2>
</div>
<button
type="button"
className="upm-header__close"
<ActionIcon
variant="tertiary"
aria-label={t("payg.upgrade.closeAria", "Close")}
onClick={closeAndReset}
>
<CloseIcon fontSize="small" />
</button>
</ActionIcon>
</header>
{/* Step indicator. Hidden on the confirmation panel since the
@@ -198,13 +199,13 @@ export default function UpgradeModal({
"{{symbol}}{{amount}} / month",
{ symbol: sym, amount: effectiveCap },
)}
<button
type="button"
<Button
variant="tertiary"
className="upm-step__edit"
onClick={goBackToCap}
>
{t("payg.upgrade.checkout.edit", "Edit")}
</button>
</Button>
</span>
) : (
<span>
@@ -264,36 +265,23 @@ export default function UpgradeModal({
<div className="upm-footer__actions">
{step === "cap" && (
<>
<button
type="button"
className="upm-btn"
data-variant="ghost"
onClick={closeAndReset}
>
<Button variant="secondary" onClick={closeAndReset}>
{t("payg.upgrade.button.cancel", "Cancel")}
</button>
<button
type="button"
className="upm-btn"
data-variant="primary"
onClick={goToCheckout}
>
</Button>
<Button onClick={goToCheckout}>
{t("payg.upgrade.button.continue", "Continue →")}
</button>
</Button>
</>
)}
{step === "confirm" && (
<button
type="button"
className="upm-btn"
data-variant="primary"
<Button
onClick={() => {
setStep("cap");
onComplete({ capUsd: effectiveCap });
}}
>
{t("payg.upgrade.button.finish", "Finish")}
</button>
</Button>
)}
</div>
</footer>
@@ -1,3 +1,3 @@
<svg width="37" height="36" viewBox="0 0 37 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18.5 3.3125C16.5712 3.3125 14.6613 3.6924 12.8793 4.43052C11.0974 5.16864 9.47823 6.25051 8.11437 7.61437C5.35993 10.3688 3.8125 14.1046 3.8125 18C3.8125 24.4919 8.02781 29.9997 13.8588 31.9531C14.5931 32.0706 14.8281 31.6153 14.8281 31.2187V28.7366C10.7597 29.6178 9.89312 26.7684 9.89312 26.7684C9.2175 25.0647 8.26281 24.6094 8.26281 24.6094C6.92625 23.6987 8.36562 23.7281 8.36562 23.7281C9.83437 23.8309 10.6128 25.2409 10.6128 25.2409C11.8906 27.4734 14.0497 26.8125 14.8869 26.46C15.0191 25.5053 15.4009 24.8591 15.8122 24.4919C12.5516 24.1247 9.12937 22.8616 9.12937 17.2656C9.12937 15.6353 9.6875 14.3281 10.6422 13.2853C10.4953 12.9181 9.98125 11.3906 10.7891 9.40781C10.7891 9.40781 12.0228 9.01125 14.8281 10.9059C15.9884 10.5828 17.2516 10.4212 18.5 10.4212C19.7484 10.4212 21.0116 10.5828 22.1719 10.9059C24.9772 9.01125 26.2109 9.40781 26.2109 9.40781C27.0188 11.3906 26.5047 12.9181 26.3578 13.2853C27.3125 14.3281 27.8706 15.6353 27.8706 17.2656C27.8706 22.8762 24.4338 24.11 21.1584 24.4772C21.6872 24.9325 22.1719 25.8284 22.1719 27.1944V31.2187C22.1719 31.6153 22.4069 32.0853 23.1559 31.9531C28.9869 29.985 33.1875 24.4919 33.1875 18C33.1875 16.0712 32.8076 14.1613 32.0695 12.3793C31.3314 10.5974 30.2495 8.97823 28.8856 7.61437C27.5218 6.25051 25.9026 5.16864 24.1207 4.43052C22.3387 3.6924 20.4288 3.3125 18.5 3.3125Z" fill="black"/>
<path d="M18.5 3.3125C16.5712 3.3125 14.6613 3.6924 12.8793 4.43052C11.0974 5.16864 9.47823 6.25051 8.11437 7.61437C5.35993 10.3688 3.8125 14.1046 3.8125 18C3.8125 24.4919 8.02781 29.9997 13.8588 31.9531C14.5931 32.0706 14.8281 31.6153 14.8281 31.2187V28.7366C10.7597 29.6178 9.89312 26.7684 9.89312 26.7684C9.2175 25.0647 8.26281 24.6094 8.26281 24.6094C6.92625 23.6987 8.36562 23.7281 8.36562 23.7281C9.83437 23.8309 10.6128 25.2409 10.6128 25.2409C11.8906 27.4734 14.0497 26.8125 14.8869 26.46C15.0191 25.5053 15.4009 24.8591 15.8122 24.4919C12.5516 24.1247 9.12937 22.8616 9.12937 17.2656C9.12937 15.6353 9.6875 14.3281 10.6422 13.2853C10.4953 12.9181 9.98125 11.3906 10.7891 9.40781C10.7891 9.40781 12.0228 9.01125 14.8281 10.9059C15.9884 10.5828 17.2516 10.4212 18.5 10.4212C19.7484 10.4212 21.0116 10.5828 22.1719 10.9059C24.9772 9.01125 26.2109 9.40781 26.2109 9.40781C27.0188 11.3906 26.5047 12.9181 26.3578 13.2853C27.3125 14.3281 27.8706 15.6353 27.8706 17.2656C27.8706 22.8762 24.4338 24.11 21.1584 24.4772C21.6872 24.9325 22.1719 25.8284 22.1719 27.1944V31.2187C22.1719 31.6153 22.4069 32.0853 23.1559 31.9531C28.9869 29.985 33.1875 24.4919 33.1875 18C33.1875 16.0712 32.8076 14.1613 32.0695 12.3793C31.3314 10.5974 30.2495 8.97823 28.8856 7.61437C27.5218 6.25051 25.9026 5.16864 24.1207 4.43052C22.3387 3.6924 20.4288 3.3125 18.5 3.3125Z" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -1,5 +1,6 @@
import React from "react";
import { Card, Group, Text, Button, Progress } from "@mantine/core";
import { Card, Group, Text, Progress } from "@mantine/core";
import { Button } from "@app/ui/Button";
import { useTranslation } from "react-i18next";
import StorageIcon from "@mui/icons-material/Storage";
import DeleteIcon from "@mui/icons-material/Delete";
@@ -58,21 +59,16 @@ const StorageStatsCard: React.FC<StorageStatsCardProps> = ({
<Group gap="xs">
{filesCount > 0 && (
<Button
variant="light"
color="red"
size="xs"
variant="secondary"
accent="danger"
size="sm"
onClick={onClearAll}
leftSection={<DeleteIcon style={{ fontSize: 16 }} />}
>
{t("fileManager.clearAll", "Clear All")}
</Button>
)}
<Button
variant="light"
color="blue"
size="xs"
onClick={onReloadFiles}
>
<Button variant="secondary" size="sm" onClick={onReloadFiles}>
Reload Files
</Button>
</Group>
@@ -1,5 +1,4 @@
import {
ActionIcon,
Tooltip,
Popover,
Stack,
@@ -10,6 +9,7 @@ import {
import { useState, useCallback, useEffect } from "react";
import { useTranslation } from "react-i18next";
import ColorizeIcon from "@mui/icons-material/Colorize";
import { ActionIcon } from "@app/ui/ActionIcon";
// safari and firefox do not support the eye dropper API, only edge, chrome and opera do.
// the button is hidden in the UI if the API is not supported.
@@ -66,24 +66,12 @@ export function ColorControl({
<Popover.Target>
<Tooltip label={label}>
<ActionIcon
variant="subtle"
color="gray"
aria-label={label}
variant="secondary"
accent="neutral"
size="md"
onClick={() => setOpened(!opened)}
disabled={disabled}
styles={{
root: {
flexShrink: 0,
backgroundColor: "var(--bg-raised)",
border: "1px solid var(--border-default)",
color: "var(--text-secondary)",
"&:hover": {
backgroundColor: "var(--hover-bg)",
borderColor: "var(--border-strong)",
color: "var(--text-primary)",
},
},
}}
>
<ColorSwatch color={localColor} size={18} />
</ActionIcon>
@@ -117,11 +105,14 @@ export function ColorControl({
label={t("color.eyeDropper.tooltip", "Pick colour from screen")}
>
<ActionIcon
variant="subtle"
color="gray"
aria-label={t(
"color.eyeDropper.tooltip",
"Pick colour from screen",
)}
variant="tertiary"
accent="neutral"
size="sm"
onClick={handleEyeDropper}
style={{ color: "var(--text-primary)" }}
>
<ColorizeIcon style={{ fontSize: 16 }} />
</ActionIcon>
@@ -4,13 +4,12 @@ import {
Stack,
ColorPicker as MantineColorPicker,
Group,
Button,
ColorSwatch,
Slider,
Text,
} from "@mantine/core";
import { useTranslation } from "react-i18next";
import { Button } from "@app/ui/Button";
interface ColorPickerProps {
isOpen: boolean;
onClose: () => void;
@@ -1,6 +1,7 @@
import React, { useEffect, useRef, useState } from "react";
import { Paper, Button, Modal, Stack, Text, Group } from "@mantine/core";
import { Paper, Modal, Stack, Text, Group } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { Button } from "@app/ui/Button";
import { ColorSwatchButton } from "@app/components/annotation/shared/ColorPicker";
import PenSizeSelector from "@app/components/tools/sign/PenSizeSelector";
import SignaturePad from "signature_pad";
@@ -331,7 +332,7 @@ export const DrawingCanvas: React.FC<DrawingCanvasProps> = ({
</PrivateContent>
<div style={{ display: "flex", justifyContent: "space-between" }}>
<Button variant="subtle" color="red" onClick={clear}>
<Button variant="tertiary" accent="danger" onClick={clear}>
{t("sign.canvas.clear", "Clear canvas")}
</Button>
<Button onClick={closeModal}>{t("common.done", "Done")}</Button>
@@ -1,7 +1,9 @@
import React from "react";
import { Group, Button, ActionIcon, Tooltip } from "@mantine/core";
import { Group, Tooltip } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { LocalIcon } from "@app/components/shared/LocalIcon";
import { Button } from "@app/ui/Button";
import { ActionIcon } from "@app/ui/ActionIcon";
interface DrawingControlsProps {
onUndo?: () => void;
@@ -37,12 +39,11 @@ export const DrawingControls: React.FC<DrawingControlsProps> = ({
{onUndo && (
<Tooltip label={t("sign.undo", "Undo")}>
<ActionIcon
variant="subtle"
variant="tertiary"
size="lg"
aria-label={t("sign.undo", "Undo")}
onClick={onUndo}
disabled={undoDisabled}
color={undoDisabled ? "gray" : "blue"}
>
<LocalIcon
icon="undo"
@@ -56,12 +57,11 @@ export const DrawingControls: React.FC<DrawingControlsProps> = ({
{onRedo && (
<Tooltip label={t("sign.redo", "Redo")}>
<ActionIcon
variant="subtle"
variant="tertiary"
size="lg"
aria-label={t("sign.redo", "Redo")}
onClick={onRedo}
disabled={redoDisabled}
color={redoDisabled ? "gray" : "blue"}
>
<LocalIcon
icon="redo"
@@ -78,11 +78,9 @@ export const DrawingControls: React.FC<DrawingControlsProps> = ({
{/* Place Signature Button */}
{showPlaceButton && onPlaceSignature && (
<Button
variant="filled"
color="blue"
onClick={onPlaceSignature}
disabled={disabled || !hasSignatureData}
ml="auto"
style={{ marginLeft: "auto" }}
>
{placeButtonText}
</Button>
@@ -1,14 +1,8 @@
import {
ActionIcon,
Tooltip,
Popover,
Stack,
Slider,
Text,
} from "@mantine/core";
import { Tooltip, Popover, Stack, Slider, Text } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { useState } from "react";
import OpacityIcon from "@mui/icons-material/Opacity";
import { ActionIcon } from "@app/ui/ActionIcon";
interface OpacityControlProps {
value: number; // 0-100
@@ -29,24 +23,12 @@ export function OpacityControl({
<Popover.Target>
<Tooltip label={t("annotation.opacity", "Opacity")}>
<ActionIcon
variant="subtle"
color="gray"
aria-label={t("annotation.opacity", "Opacity")}
variant="secondary"
accent="neutral"
size="md"
onClick={() => setOpened(!opened)}
disabled={disabled}
styles={{
root: {
flexShrink: 0,
backgroundColor: "var(--bg-raised)",
border: "1px solid var(--border-default)",
color: "var(--text-secondary)",
"&:hover": {
backgroundColor: "var(--hover-bg)",
borderColor: "var(--border-strong)",
color: "var(--text-primary)",
},
},
}}
>
<OpacityIcon style={{ fontSize: 18 }} />
</ActionIcon>
@@ -1,14 +1,7 @@
import {
ActionIcon,
Tooltip,
Popover,
Stack,
Slider,
Text,
Group,
Button,
} from "@mantine/core";
import { Tooltip, Popover, Stack, Slider, Text, Group } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { Button } from "@app/ui/Button";
import { ActionIcon } from "@app/ui/ActionIcon";
import { useState } from "react";
import type { TrackedAnnotation } from "@embedpdf/plugin-annotation";
import type { PdfAnnotationObject } from "@embedpdf/models";
@@ -106,21 +99,24 @@ export function PropertiesPopover({
</Text>
<Group gap="xs">
<ActionIcon
variant={currentAlign === "left" ? "filled" : "default"}
aria-label={t("annotation.alignLeft", "Align left")}
variant={currentAlign === "left" ? "primary" : "secondary"}
onClick={() => onUpdate({ textAlign: 0 })}
size="md"
>
<FormatAlignLeftIcon style={{ fontSize: 18 }} />
</ActionIcon>
<ActionIcon
variant={currentAlign === "center" ? "filled" : "default"}
aria-label={t("annotation.alignCenter", "Align center")}
variant={currentAlign === "center" ? "primary" : "secondary"}
onClick={() => onUpdate({ textAlign: 1 })}
size="md"
>
<FormatAlignCenterIcon style={{ fontSize: 18 }} />
</ActionIcon>
<ActionIcon
variant={currentAlign === "right" ? "filled" : "default"}
aria-label={t("annotation.alignRight", "Align right")}
variant={currentAlign === "right" ? "primary" : "secondary"}
onClick={() => onUpdate({ textAlign: 2 })}
size="md"
>
@@ -176,8 +172,8 @@ export function PropertiesPopover({
/>
</div>
<Button
size="xs"
variant={!borderVisible ? "filled" : "light"}
size="sm"
variant={!borderVisible ? "primary" : "secondary"}
onClick={() => {
const newValue = borderVisible ? 0 : 1;
onUpdate({
@@ -201,24 +197,12 @@ export function PropertiesPopover({
<Popover.Target>
<Tooltip label={t("annotation.properties", "Properties")}>
<ActionIcon
variant="subtle"
color="gray"
aria-label={t("annotation.properties", "Properties")}
variant="secondary"
accent="neutral"
size="md"
onClick={() => setOpened(!opened)}
disabled={disabled}
styles={{
root: {
flexShrink: 0,
backgroundColor: "var(--bg-raised)",
border: "1px solid var(--border-default)",
color: "var(--text-secondary)",
"&:hover": {
backgroundColor: "var(--hover-bg)",
borderColor: "var(--border-strong)",
color: "var(--text-primary)",
},
},
}}
>
<TuneIcon style={{ fontSize: 18 }} />
</ActionIcon>
@@ -7,8 +7,8 @@ import {
useCombobox,
Group,
Box,
SegmentedControl,
} from "@mantine/core";
import { SegmentedControl } from "@app/ui/SegmentedControl";
import { useTranslation } from "react-i18next";
import { ColorPicker } from "@app/components/annotation/shared/ColorPicker";
@@ -253,14 +253,14 @@ export const TextInputWithFont: React.FC<TextInputWithFontProps> = ({
{/* Text Alignment */}
{onTextAlignChange && (
<SegmentedControl
<SegmentedControl<"left" | "center" | "right">
value={textAlign}
onChange={(value: string) => {
onTextAlignChange(value as "left" | "center" | "right");
onChange={(value) => {
onTextAlignChange(value);
onAnyChange?.();
}}
disabled={disabled}
data={[
loading={disabled}
options={[
{ label: t("textAlign.left", "Left"), value: "left" },
{ label: t("textAlign.center", "Center"), value: "center" },
{ label: t("textAlign.right", "Right"), value: "right" },
@@ -1,14 +1,8 @@
import {
ActionIcon,
Tooltip,
Popover,
Stack,
Slider,
Text,
} from "@mantine/core";
import { Tooltip, Popover, Stack, Slider, Text } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { useState } from "react";
import LineWeightIcon from "@mui/icons-material/LineWeight";
import { ActionIcon } from "@app/ui/ActionIcon";
interface WidthControlProps {
value: number;
@@ -33,24 +27,12 @@ export function WidthControl({
<Popover.Target>
<Tooltip label={t("annotation.width", "Width")}>
<ActionIcon
variant="subtle"
color="gray"
aria-label={t("annotation.width", "Width")}
variant="secondary"
accent="neutral"
size="md"
onClick={() => setOpened(!opened)}
disabled={disabled}
styles={{
root: {
flexShrink: 0,
backgroundColor: "var(--bg-raised)",
border: "1px solid var(--border-default)",
color: "var(--text-secondary)",
"&:hover": {
backgroundColor: "var(--hover-bg)",
borderColor: "var(--border-strong)",
color: "var(--text-primary)",
},
},
}}
>
<LineWeightIcon style={{ fontSize: 18 }} />
</ActionIcon>
@@ -1,5 +1,6 @@
import React, { useRef, useState } from "react";
import { Button, Group } from "@mantine/core";
import { Group } from "@mantine/core";
import { Button } from "@app/ui/Button";
import { useTranslation } from "react-i18next";
import { useFilesModalContext } from "@app/contexts/FilesModalContext";
import LocalIcon from "@app/components/shared/LocalIcon";
@@ -106,6 +107,7 @@ const AddFileCard = ({
>
{!isUploadHover && (
<Button
variant="tertiary"
style={{
backgroundColor: "var(--landing-button-bg)",
color: "var(--landing-button-color)",
@@ -133,6 +135,7 @@ const AddFileCard = ({
</Button>
)}
<Button
variant="tertiary"
aria-label={t("addFileCard.upload", "Upload")}
title={terminology.uploadFromComputer}
style={{
@@ -291,15 +291,15 @@
DARK MODE OVERRIDES
========================= */
:global([data-mantine-color-scheme="dark"]) .card {
outline-color: #3a4047; /* deselected stroke */
outline-color: #1c2340; /* deselected stroke */
}
:global([data-mantine-color-scheme="dark"]) .card[data-selected="true"] {
outline-color: #4b525a; /* selected stroke (subtle grey) */
outline-color: #2d3560; /* selected stroke (subtle navy) */
}
:global([data-mantine-color-scheme="dark"]) .headerResting {
background: #1f2329; /* requested default unselected color */
background: #0d1020; /* requested default unselected color */
color: var(--tool-header-text); /* #D0D6DC */
border-bottom-color: var(--tool-header-border); /* #3A4047 */
}
@@ -1,13 +1,7 @@
import React, { useState, useCallback, useRef, useMemo } from "react";
import {
Text,
Modal,
Button,
Group,
Stack,
ActionIcon,
Tooltip,
} from "@mantine/core";
import { Text, Modal, Group, Stack, Tooltip } from "@mantine/core";
import { ActionIcon } from "@app/ui/ActionIcon";
import { Button } from "@app/ui/Button";
import { useIsMobile } from "@app/hooks/useIsMobile";
import { alert } from "@app/components/toast";
import { useTranslation } from "react-i18next";
@@ -545,9 +539,12 @@ const FileEditorThumbnail = ({
)}
>
<ActionIcon
size="xs"
variant="filled"
color="yellow"
size="sm"
accent="warning"
aria-label={t(
"encryptedPdfUnlock.unlockPrompt",
"Unlock PDF to continue",
)}
onClick={(e) => {
e.stopPropagation();
openEncryptedUnlockPrompt(file.id);
@@ -606,17 +603,13 @@ const FileEditorThumbnail = ({
<PrivateContent>{file.name}</PrivateContent>
</Text>
<Group justify="flex-end" gap="sm">
<Button variant="light" onClick={handleCancelClose}>
<Button variant="secondary" onClick={handleCancelClose}>
{t("confirmCloseCancel", "Cancel")}
</Button>
<Button
variant="filled"
color="red"
onClick={handleConfirmClose}
>
<Button accent="danger" onClick={handleConfirmClose}>
{t("confirmCloseDiscard", "Discard changes and close")}
</Button>
<Button variant="filled" onClick={handleSaveAndClose}>
<Button onClick={handleSaveAndClose}>
{t("confirmCloseSave", "Save and close")}
</Button>
</Group>
@@ -633,14 +626,10 @@ const FileEditorThumbnail = ({
<PrivateContent>{file.name}</PrivateContent>
</Text>
<Group justify="flex-end" gap="sm">
<Button variant="light" onClick={handleCancelClose}>
<Button variant="secondary" onClick={handleCancelClose}>
{t("confirmCloseCancel", "Cancel")}
</Button>
<Button
variant="filled"
color="red"
onClick={handleConfirmClose}
>
<Button accent="danger" onClick={handleConfirmClose}>
{t("confirmCloseConfirm", "Close File")}
</Button>
</Group>
@@ -1,5 +1,7 @@
import React from "react";
import { Stack, Box, Text, Button, ActionIcon, Center } from "@mantine/core";
import { Stack, Box, Text, Center } from "@mantine/core";
import { Button } from "@app/ui/Button";
import { ActionIcon } from "@app/ui/ActionIcon";
import PictureAsPdfIcon from "@mui/icons-material/PictureAsPdf";
import ChevronLeftIcon from "@mui/icons-material/ChevronLeft";
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
@@ -128,18 +130,20 @@ const CompactFileDetails: React.FC<CompactFileDetailsProps> = ({
{hasMultipleFiles && (
<Box style={{ display: "flex", gap: "0.25rem" }}>
<ActionIcon
variant="subtle"
variant="tertiary"
size="sm"
onClick={onPrevious}
disabled={isAnimating}
aria-label={t("fileManager.previousFile", "Previous file")}
>
<ChevronLeftIcon style={{ fontSize: 16 }} />
</ActionIcon>
<ActionIcon
variant="subtle"
variant="tertiary"
size="sm"
onClick={onNext}
disabled={isAnimating}
aria-label={t("fileManager.nextFile", "Next file")}
>
<ChevronRightIcon style={{ fontSize: 16 }} />
</ActionIcon>
@@ -150,16 +154,10 @@ const CompactFileDetails: React.FC<CompactFileDetailsProps> = ({
{/* Action Button */}
<Button
size="sm"
accent="neutral"
onClick={onOpenFiles}
disabled={!hasSelection && !canCloseAll}
fullWidth
style={{
backgroundColor:
hasSelection || canCloseAll
? "var(--btn-open-file)"
: "var(--mantine-color-gray-4)",
color: "white",
}}
>
{canCloseAll
? t("fileManager.closeAllFiles", "Close all files")
@@ -1,5 +1,6 @@
import React, { useState } from "react";
import { Button, Group, Text, Stack } from "@mantine/core";
import { Group, Text, Stack } from "@mantine/core";
import { Button } from "@app/ui/Button";
import HistoryIcon from "@mui/icons-material/History";
import { useTranslation } from "react-i18next";
import { useFileManagerContext } from "@app/contexts/FileManagerContext";
@@ -77,6 +78,7 @@ const EmptyFilesState: React.FC = () => {
onMouseLeave={() => setIsUploadHover(false)}
>
<Button
variant="tertiary"
aria-label={t("emptyFilesState.upload", "Upload")}
style={{
backgroundColor: "var(--bg-file-manager)",
@@ -1,11 +1,7 @@
import React, { useEffect } from "react";
import {
Group,
Text,
ActionIcon,
Tooltip,
SegmentedControl,
} from "@mantine/core";
import { Group, Text, Tooltip } from "@mantine/core";
import { ActionIcon } from "@app/ui/ActionIcon";
import { SegmentedControl } from "@app/ui/SegmentedControl";
import SelectAllIcon from "@mui/icons-material/SelectAll";
import DeleteIcon from "@mui/icons-material/Delete";
import CloudUploadIcon from "@mui/icons-material/CloudUpload";
@@ -131,26 +127,29 @@ const FileActions: React.FC = () => {
}
>
<ActionIcon
variant="light"
variant="secondary"
size="sm"
color="dimmed"
onClick={handleSelectAll}
disabled={filteredFiles.length === 0}
radius="sm"
aria-label={
allFilesSelected
? t("fileManager.deselectAll", "Deselect All")
: t("fileManager.selectAll", "Select All")
}
>
<SelectAllIcon style={{ fontSize: "1rem" }} />
</ActionIcon>
</Tooltip>
{showStorageFilter && (
<SegmentedControl
size="xs"
size="sm"
value={storageFilter}
onChange={(value) =>
onStorageFilterChange(
value as "all" | "local" | "sharedWithMe" | "sharedByMe",
)
}
data={storageFilterOptions}
options={storageFilterOptions}
/>
)}
</Group>
@@ -177,12 +176,11 @@ const FileActions: React.FC = () => {
{uploadEnabled && (
<Tooltip label={t("fileManager.uploadSelected", "Upload Selected")}>
<ActionIcon
variant="light"
variant="secondary"
size="sm"
color="dimmed"
onClick={() => setShowBulkUploadModal(true)}
disabled={!canBulkUpload}
radius="sm"
aria-label={t("fileManager.uploadSelected", "Upload Selected")}
>
<CloudUploadIcon style={{ fontSize: "1rem" }} />
</ActionIcon>
@@ -191,12 +189,11 @@ const FileActions: React.FC = () => {
{shareLinksEnabled && (
<Tooltip label={t("fileManager.shareSelected", "Share Selected")}>
<ActionIcon
variant="light"
variant="secondary"
size="sm"
color="dimmed"
onClick={() => setShowBulkShareModal(true)}
disabled={!canBulkShare}
radius="sm"
aria-label={t("fileManager.shareSelected", "Share Selected")}
>
<LinkIcon style={{ fontSize: "1rem" }} />
</ActionIcon>
@@ -204,12 +201,12 @@ const FileActions: React.FC = () => {
)}
<Tooltip label={t("fileManager.deleteSelected", "Delete Selected")}>
<ActionIcon
variant="light"
variant="secondary"
size="sm"
color="dimmed"
accent="danger"
onClick={handleDeleteSelected}
disabled={!hasSelection}
radius="sm"
aria-label={t("fileManager.deleteSelected", "Delete Selected")}
>
<DeleteIcon style={{ fontSize: "1rem" }} />
</ActionIcon>
@@ -217,12 +214,11 @@ const FileActions: React.FC = () => {
<Tooltip label={terminology.downloadSelected}>
<ActionIcon
variant="light"
variant="secondary"
size="sm"
color="dimmed"
onClick={handleDownloadSelected}
disabled={!hasSelection || !hasDownloadAccess}
radius="sm"
aria-label={terminology.downloadSelected}
>
<DownloadIcon style={{ fontSize: "1rem" }} />
</ActionIcon>
@@ -1,5 +1,6 @@
import React, { useEffect, useState } from "react";
import { Stack, Button, Box } from "@mantine/core";
import { Stack, Box } from "@mantine/core";
import { Button } from "@app/ui/Button";
import { useTranslation } from "react-i18next";
import { useIndexedDBThumbnail } from "@app/hooks/useIndexedDBThumbnail";
import { useFileManagerContext } from "@app/contexts/FileManagerContext";
@@ -108,16 +109,10 @@ const FileDetails: React.FC<FileDetailsProps> = ({ compact = false }) => {
<Button
size="md"
accent="neutral"
onClick={onOpenFiles}
disabled={!hasSelection && !canCloseAll}
fullWidth
style={{
backgroundColor:
hasSelection || canCloseAll
? "var(--btn-open-file)"
: "var(--mantine-color-gray-4)",
color: "white",
}}
>
{canCloseAll
? t("fileManager.closeAllFiles", "Close all files")
@@ -8,8 +8,8 @@ import {
Group,
Divider,
ScrollArea,
Button,
} from "@mantine/core";
import { Button } from "@app/ui/Button";
import { useTranslation } from "react-i18next";
import { detectFileExtension, getFileSize } from "@app/utils/fileUtils";
import { StirlingFileStub } from "@app/types/fileContext";
@@ -208,7 +208,7 @@ const FileInfoCard: React.FC<FileInfoCardProps> = ({
<Divider />
<Button
size="sm"
variant="light"
variant="secondary"
onClick={() => onMakeCopy(currentFile)}
fullWidth
>
@@ -260,7 +260,7 @@ const FileInfoCard: React.FC<FileInfoCardProps> = ({
</Group>
<Button
size="sm"
variant="light"
variant="secondary"
onClick={() => setShowShareManageModal(true)}
fullWidth
>
@@ -3,12 +3,12 @@ import {
Group,
Box,
Text,
ActionIcon,
Checkbox,
Divider,
Menu,
Badge,
} from "@mantine/core";
import { ActionIcon } from "@app/ui/ActionIcon";
import MoreVertIcon from "@mui/icons-material/MoreVert";
import DeleteIcon from "@mui/icons-material/Delete";
import DownloadIcon from "@mui/icons-material/Download";
@@ -323,10 +323,11 @@ const FileListItem: React.FC<FileListItemProps> = ({
>
<Menu.Target>
<ActionIcon
variant="subtle"
c="dimmed"
variant="tertiary"
accent="neutral"
size="md"
onClick={(e) => e.stopPropagation()}
aria-label={t("fileManager.moreOptions", "More options")}
style={{
opacity: shouldShowHovered ? 1 : 0,
transform: shouldShowHovered ? "scale(1)" : "scale(0.8)",
@@ -1,5 +1,6 @@
import React, { useState } from "react";
import { Stack, Text, Button, Group } from "@mantine/core";
import { Stack, Text, Group } from "@mantine/core";
import { Button } from "@app/ui/Button";
import HistoryIcon from "@mui/icons-material/History";
import PhonelinkIcon from "@mui/icons-material/Phonelink";
import { useTranslation } from "react-i18next";
@@ -65,86 +66,45 @@ const FileSourceButtons: React.FC<FileSourceButtonsProps> = ({
// Determine visibility of Mobile QR Scanner button
const shouldHideMobileQR =
!isMobileUploadEnabled && config?.hideDisabledToolsMobileQRScanner;
const buttonProps = {
variant: (source: string) =>
activeSource === source ? "filled" : "subtle",
getColor: (source: string) =>
activeSource === source ? "var(--mantine-color-gray-2)" : undefined,
getStyles: (source: string) => ({
root: {
backgroundColor: activeSource === source ? undefined : "transparent",
color:
activeSource === source
? "var(--mantine-color-gray-9)"
: "var(--mantine-color-gray-6)",
border: "none",
"&:hover": {
backgroundColor:
activeSource === source ? undefined : "var(--mantine-color-gray-0)",
},
},
}),
};
// Shared Button has no `xs`; map the old horizontal `xs` to `sm`.
const buttonSize = "sm" as const;
const buttonJustify = horizontal ? "center" : "start";
const buttons = (
<>
<Button
variant={activeSource === "recent" ? "primary" : "tertiary"}
accent="neutral"
leftSection={<HistoryIcon />}
justify={horizontal ? "center" : "flex-start"}
justify={buttonJustify}
onClick={() => onSourceChange("recent")}
fullWidth={!horizontal}
size={horizontal ? "xs" : "sm"}
color={buttonProps.getColor("recent")}
styles={buttonProps.getStyles("recent")}
size={buttonSize}
>
{horizontal
? t("fileManager.recent", "Recent")
: t("fileManager.recent", "Recent")}
{t("fileManager.recent", "Recent")}
</Button>
<Button
variant="subtle"
color="var(--mantine-color-gray-6)"
variant="tertiary"
accent="neutral"
leftSection={<UploadIcon />}
justify={horizontal ? "center" : "flex-start"}
justify={buttonJustify}
onClick={onLocalFileClick}
fullWidth={!horizontal}
size={horizontal ? "xs" : "sm"}
styles={{
root: {
backgroundColor: "transparent",
border: "none",
"&:hover": {
backgroundColor: "var(--mantine-color-gray-0)",
},
},
}}
size={buttonSize}
>
{horizontal ? terminology.upload : terminology.uploadFiles}
</Button>
{!shouldHideGoogleDrive && (
<Button
variant="subtle"
color="var(--mantine-color-gray-6)"
variant="tertiary"
accent="neutral"
leftSection={<GoogleDriveIcon colored={isGoogleDriveEnabled} />}
justify={horizontal ? "center" : "flex-start"}
justify={buttonJustify}
onClick={handleGoogleDriveClick}
fullWidth={!horizontal}
size={horizontal ? "xs" : "sm"}
size={buttonSize}
disabled={!isGoogleDriveEnabled}
styles={{
root: {
backgroundColor: "transparent",
border: "none",
"&:hover": {
backgroundColor: isGoogleDriveEnabled
? "var(--mantine-color-gray-0)"
: "transparent",
},
},
}}
title={
!isGoogleDriveEnabled
? t(
@@ -162,25 +122,14 @@ const FileSourceButtons: React.FC<FileSourceButtonsProps> = ({
{!shouldHideMobileQR && (
<Button
variant="subtle"
color="var(--mantine-color-gray-6)"
variant="tertiary"
accent="neutral"
leftSection={<PhonelinkIcon />}
justify={horizontal ? "center" : "flex-start"}
justify={buttonJustify}
onClick={handleMobileUploadClick}
fullWidth={!horizontal}
size={horizontal ? "xs" : "sm"}
size={buttonSize}
disabled={!isMobileUploadEnabled}
styles={{
root: {
backgroundColor: "transparent",
border: "none",
"&:hover": {
backgroundColor: isMobileUploadEnabled
? "var(--mantine-color-gray-0)"
: "transparent",
},
},
}}
title={
!isMobileUploadEnabled
? t(
@@ -1,8 +1,10 @@
import { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { Alert, Button, Group, Modal, Radio, Stack, Text } from "@mantine/core";
import { Alert, Group, Modal, Radio, Stack, Text } from "@mantine/core";
import ErrorOutlineIcon from "@mui/icons-material/ErrorOutlined";
import { Button } from "@app/ui/Button";
import type { StirlingFileStub } from "@app/types/fileContext";
import type { DeleteScope } from "@app/services/serverStorageDelete";
@@ -164,11 +166,11 @@ export function DeleteFilesDialog({
)}
<Group justify="flex-end">
<Button variant="default" onClick={onClose} disabled={submitting}>
<Button variant="secondary" onClick={onClose} disabled={submitting}>
{t("filesPage.cancel", "Cancel")}
</Button>
<Button
color="red"
accent="danger"
loading={submitting}
onClick={() => runConfirm(showChoice ? scope : fixedScope)}
>
@@ -1,16 +1,9 @@
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import {
Alert,
Button,
Checkbox,
Group,
Modal,
Stack,
Text,
} from "@mantine/core";
import { Alert, Checkbox, Group, Modal, Stack, Text } from "@mantine/core";
import ErrorOutlineIcon from "@mui/icons-material/ErrorOutlined";
import { Button } from "@app/ui/Button";
import { FolderRecord } from "@app/types/folder";
interface DeleteFolderDialogProps {
@@ -95,11 +88,11 @@ export function DeleteFolderDialog({
</Alert>
)}
<Group justify="flex-end">
<Button variant="default" onClick={onClose} disabled={submitting}>
<Button variant="secondary" onClick={onClose} disabled={submitting}>
{t("filesPage.cancel", "Cancel")}
</Button>
<Button
color="red"
accent="danger"
loading={submitting}
onClick={async () => {
setSubmitting(true);
@@ -1,6 +1,8 @@
import React, { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { ActionIcon, Badge, Button, Tooltip } from "@mantine/core";
import { Badge, Tooltip } from "@mantine/core";
import { Button } from "@app/ui/Button";
import { ActionIcon } from "@app/ui/ActionIcon";
import CloseIcon from "@mui/icons-material/Close";
import OpenInNewIcon from "@mui/icons-material/OpenInNew";
import DriveFileMoveIcon from "@mui/icons-material/DriveFileMove";
@@ -143,7 +145,12 @@ export function FileDetailsPanel({
label={t("filesPage.closeDetails", "Close details")}
withinPortal
>
<ActionIcon variant="subtle" size="sm" onClick={onClose}>
<ActionIcon
variant="tertiary"
size="sm"
onClick={onClose}
aria-label={t("filesPage.closeDetails", "Close details")}
>
<CloseIcon fontSize="small" />
</ActionIcon>
</Tooltip>
@@ -181,25 +188,27 @@ export function FileDetailsPanel({
<span className="files-page-details-ext-tag">{ext}</span>
)}
{(single.versionNumber ?? 1) > 1 && (
<Badge size="sm" variant="filled" color="blue">
<Badge size="sm" color="blue">
v{single.versionNumber}
</Badge>
)}
</div>
<button
type="button"
<Button
variant="tertiary"
className="files-page-details-collapse-toggle"
onClick={() => setFieldsOpen((o) => !o)}
aria-expanded={fieldsOpen}
rightSection={
<KeyboardArrowDownIcon
className={`files-page-details-collapse-chevron${
fieldsOpen ? " is-open" : ""
}`}
fontSize="small"
/>
}
>
<span>{t("filesPage.fileInfo", "File info")}</span>
<KeyboardArrowDownIcon
className={`files-page-details-collapse-chevron${
fieldsOpen ? " is-open" : ""
}`}
fontSize="small"
/>
</button>
</Button>
{fieldsOpen && (
<div className="files-page-details-fieldlist">
<DetailField
@@ -256,7 +265,7 @@ export function FileDetailsPanel({
(compactVersions && onOpenVersionHistory ? (
<Button
leftSection={<HistoryIcon fontSize="small" />}
variant="default"
variant="secondary"
onClick={onOpenVersionHistory}
>
{t(
@@ -291,7 +300,6 @@ export function FileDetailsPanel({
<div className="files-page-details-actions">
<Button
leftSection={<OpenInNewIcon fontSize="small" />}
variant="filled"
onClick={() => onAddToWorkspace(selectedFileIds)}
>
{files.length === 1
@@ -302,7 +310,7 @@ export function FileDetailsPanel({
</Button>
<Button
leftSection={<DownloadIcon fontSize="small" />}
variant="default"
variant="secondary"
onClick={handleDownload}
loading={downloading}
>
@@ -329,14 +337,12 @@ export function FileDetailsPanel({
>
<Button
leftSection={<LinkIcon fontSize="small" />}
variant="default"
variant="secondary"
disabled={!sharingEnabled}
onClick={() => setShareModalOpen(true)}
styles={{
root: {
// Keep tooltip hoverable while button is disabled.
pointerEvents: sharingEnabled ? undefined : "auto",
},
style={{
// Keep tooltip hoverable while button is disabled.
pointerEvents: sharingEnabled ? undefined : "auto",
}}
>
{t("filesPage.shareManage", "Manage sharing")}
@@ -345,7 +351,7 @@ export function FileDetailsPanel({
)}
<Button
leftSection={<DriveFileMoveIcon fontSize="small" />}
variant="default"
variant="secondary"
onClick={() => onMove(selectedFileIds)}
>
{t("filesPage.moveTo", "Move to…")}
@@ -363,16 +369,12 @@ export function FileDetailsPanel({
>
<Button
leftSection={<CloudUploadIcon fontSize="small" />}
variant="default"
variant="secondary"
disabled={Boolean(saveToServerDisabledReason)}
onClick={() => onSaveToServer(localOnlyFiles)}
styles={{
root: {
// Keep tooltip hoverable while button is disabled.
pointerEvents: saveToServerDisabledReason
? "auto"
: undefined,
},
style={{
// Keep tooltip hoverable while button is disabled.
pointerEvents: saveToServerDisabledReason ? "auto" : undefined,
}}
>
{t("filesPage.saveToServer", "Save to server")}
@@ -381,8 +383,7 @@ export function FileDetailsPanel({
)}
<Button
leftSection={<DeleteIcon fontSize="small" />}
color="red"
variant="light"
accent="danger"
onClick={() => onRemove(selectedFileIds)}
>
{t("filesPage.remove", "Delete")}
@@ -1,6 +1,8 @@
import React, { useCallback, useMemo, useRef } from "react";
import { useTranslation } from "react-i18next";
import { ActionIcon, Button, Checkbox, Menu, Tooltip } from "@mantine/core";
import { Checkbox, Menu, Tooltip } from "@mantine/core";
import { Button } from "@app/ui/Button";
import { ActionIcon } from "@app/ui/ActionIcon";
import MoreVertIcon from "@mui/icons-material/MoreVert";
import ShieldOutlinedIcon from "@mui/icons-material/ShieldOutlined";
import FolderIcon from "@mui/icons-material/Folder";
@@ -297,10 +299,10 @@ function EmptyState({
<span style={{ display: "inline-flex" }}>
<Button
size="md"
variant="default"
variant="secondary"
leftSection={<CreateNewFolderIcon fontSize="small" />}
disabled
styles={{ root: { pointerEvents: "auto" } }}
style={{ pointerEvents: "auto" }}
>
{t("filesPage.empty.newFolderCta", "Create folder")}
</Button>
@@ -309,7 +311,7 @@ function EmptyState({
) : (
<Button
size="md"
variant="default"
variant="secondary"
leftSection={<CreateNewFolderIcon fontSize="small" />}
onClick={onCreateFolder}
>
@@ -528,8 +530,6 @@ function FolderCard({
<Menu.Target>
<ActionIcon
ref={kebabRef}
variant="filled"
color="gray"
size="sm"
onClick={(e) => e.stopPropagation()}
aria-label={t("filesPage.folderMenu", "Folder actions")}
@@ -771,8 +771,6 @@ function FileCard({
<Menu.Target>
<ActionIcon
ref={kebabRef}
variant="filled"
color="gray"
size="sm"
onClick={(e) => e.stopPropagation()}
aria-label={t("filesPage.fileMenu", "File actions")}
@@ -1151,7 +1149,7 @@ function FolderRow({
<Menu.Target>
<ActionIcon
ref={kebabRef}
variant="subtle"
variant="tertiary"
size="sm"
onClick={(e) => e.stopPropagation()}
aria-label={t("filesPage.folderMenu", "Folder actions")}
@@ -1370,7 +1368,7 @@ function FileRow({
<Menu.Target>
<ActionIcon
ref={kebabRef}
variant="subtle"
variant="tertiary"
size="sm"
onClick={(e) => e.stopPropagation()}
aria-label={t("filesPage.fileMenu", "File actions")}
@@ -7,19 +7,12 @@ import React, {
} from "react";
import { useTranslation } from "react-i18next";
import { useLocation, useNavigate } from "react-router-dom";
import {
ActionIcon,
Button,
Drawer,
Group,
MultiSelect,
SegmentedControl,
Select,
Tooltip,
} from "@mantine/core";
import { Drawer, Group, MultiSelect, Select, Tooltip } from "@mantine/core";
import { Button } from "@app/ui/Button";
import { ActionIcon } from "@app/ui/ActionIcon";
import { SegmentedControl } from "@app/ui/SegmentedControl";
import { useMediaQuery } from "@mantine/hooks";
import SearchIcon from "@mui/icons-material/Search";
import CloseIcon from "@mui/icons-material/Close";
import UploadFileIcon from "@mui/icons-material/UploadFile";
import QrCode2Icon from "@mui/icons-material/QrCode2";
import CreateNewFolderIcon from "@mui/icons-material/CreateNewFolder";
@@ -910,8 +903,8 @@ export default function FileManagerView() {
withinPortal
>
<ActionIcon
variant="default"
size="md"
variant="secondary"
size="sm"
loading={refreshing}
disabled={refreshing || Boolean(signInRequiredReason)}
aria-busy={refreshing}
@@ -930,11 +923,11 @@ export default function FileManagerView() {
>
<span style={{ display: "inline-flex" }}>
<Button
variant="default"
variant="secondary"
size="sm"
leftSection={<CreateNewFolderIcon fontSize="small" />}
disabled
styles={{ root: { pointerEvents: "auto" } }}
style={{ pointerEvents: "auto" }}
>
{t("filesPage.newFolder", "New folder")}
</Button>
@@ -942,7 +935,7 @@ export default function FileManagerView() {
</Tooltip>
) : (
<Button
variant="default"
variant="secondary"
size="sm"
leftSection={<CreateNewFolderIcon fontSize="small" />}
onClick={() => openNewFolderDialog()}
@@ -966,9 +959,8 @@ export default function FileManagerView() {
withinPortal
>
<ActionIcon
size="lg"
variant="default"
radius="md"
size="sm"
variant="secondary"
onClick={() => setMobileUploadModalOpen(true)}
aria-label={t(
"filesPage.uploadFromMobile",
@@ -1012,11 +1004,11 @@ export default function FileManagerView() {
<span>{folders.error}</span>
<ActionIcon
size="sm"
variant="subtle"
variant="tertiary"
aria-label={t("filesPage.dismissError", "Dismiss")}
onClick={() => folders.setError(null)}
>
<CloseIcon fontSize="small" />
&times;
</ActionIcon>
</div>
)}
@@ -1147,8 +1139,8 @@ export default function FileManagerView() {
w={280}
>
<Button
variant="subtle"
size="xs"
variant="tertiary"
size="sm"
onClick={() => {
if (allSelected) {
setSelectedFileIds(new Set());
@@ -1210,19 +1202,17 @@ export default function FileManagerView() {
>
<Button
size="sm"
variant="default"
variant="secondary"
leftSection={<CloudUploadIcon fontSize="small" />}
disabled={Boolean(saveToServerDisabledReason)}
onClick={() =>
setSaveToServerTarget(localOnlySelectedStubs)
}
styles={{
root: {
// Keep the tooltip hoverable while disabled.
pointerEvents: saveToServerDisabledReason
? "auto"
: undefined,
},
style={{
// Keep the tooltip hoverable while disabled.
pointerEvents: saveToServerDisabledReason
? "auto"
: undefined,
}}
aria-label={t(
"filesPage.saveToServer",
@@ -1242,7 +1232,7 @@ export default function FileManagerView() {
>
<Button
size="sm"
variant="default"
variant="secondary"
leftSection={
<InfoOutlinedIcon fontSize="small" />
}
@@ -1259,7 +1249,7 @@ export default function FileManagerView() {
<Tooltip label={moveLabel} withinPortal>
<Button
size="sm"
variant="default"
variant="secondary"
leftSection={<DriveFileMoveIcon fontSize="small" />}
onClick={() => promptMoveFiles(selectedFiles)}
aria-label={moveLabel}
@@ -1270,8 +1260,8 @@ export default function FileManagerView() {
<Tooltip label={removeLabel} withinPortal>
<Button
size="sm"
color="red"
variant="light"
accent="danger"
variant="secondary"
leftSection={<DeleteIcon fontSize="small" />}
onClick={() => handleRemoveFiles(selectedFiles)}
aria-label={removeLabel}
@@ -1284,7 +1274,7 @@ export default function FileManagerView() {
withinPortal
>
<ActionIcon
variant="subtle"
variant="tertiary"
size="md"
onClick={() => clearSelection()}
aria-label={t(
@@ -1292,7 +1282,7 @@ export default function FileManagerView() {
"Clear selection",
)}
>
<CloseIcon fontSize="small" />
&times;
</ActionIcon>
</Tooltip>
</Group>
@@ -1388,7 +1378,7 @@ export default function FileManagerView() {
/>
<span className="files-page-toolbar-divider" aria-hidden="true" />
<SegmentedControl
size="xs"
size="sm"
value={viewMode}
onChange={(v) => {
// Mantine only emits values declared in `data[].value`, but
@@ -1401,7 +1391,7 @@ export default function FileManagerView() {
setViewMode(v as (typeof FILES_PAGE_VIEW_MODES)[number]);
}}
aria-label={t("filesPage.viewMode.label", "View mode")}
data={[
options={[
{
value: "grid",
label: (
@@ -1686,12 +1676,12 @@ const SearchField = React.forwardRef<
/>
{value && (
<ActionIcon
variant="subtle"
size="xs"
variant="tertiary"
size="sm"
onClick={() => onChange("")}
aria-label={t("filesPage.clearSearch", "Clear search")}
>
<CloseIcon fontSize="small" />
&times;
</ActionIcon>
)}
</div>
@@ -1712,8 +1702,8 @@ function Breadcrumbs() {
const isLast = idx === trail.length - 1;
return (
<React.Fragment key={entry.id ?? "root"}>
<button
type="button"
<Button
variant="tertiary"
className={`files-page-breadcrumb${isLast ? " is-current" : ""}`}
onClick={() => folders.setCurrentFolderId(entry.id)}
onDragOver={(e) => {
@@ -1772,7 +1762,7 @@ function Breadcrumbs() {
}}
>
{entry.name}
</button>
</Button>
{!isLast && (
<KeyboardArrowRightIcon
className="files-page-breadcrumb-sep"
@@ -13,6 +13,7 @@ import {
FOLDER_ICONS,
FolderIconOption,
} from "@app/components/filesPage/folderIcons";
import { Button } from "@app/ui/Button";
interface FolderAppearancePickerProps {
folder: FolderRecord;
@@ -50,9 +51,9 @@ export function FolderAppearancePicker({
}}
>
{FOLDER_COLOR_PALETTE.map((c) => (
<button
<Button
key={c}
type="button"
variant="secondary"
disabled={disabled}
aria-label={t(
"filesPage.appearance.useColour",
@@ -146,8 +147,8 @@ function IconButton({
}) {
return (
<Tooltip label={icon.label} withinPortal>
<button
type="button"
<Button
variant="tertiary"
disabled={disabled}
aria-label={icon.label}
onClick={(e) => {
@@ -172,7 +173,7 @@ function IconButton({
}}
>
{icon.glyph || "-"}
</button>
</Button>
</Tooltip>
);
}
@@ -1,8 +1,10 @@
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Alert, Button, Group, Modal, Stack, TextInput } from "@mantine/core";
import { Alert, Group, Modal, Stack, TextInput } from "@mantine/core";
import ErrorOutlineIcon from "@mui/icons-material/ErrorOutlined";
import { Button } from "@app/ui/Button";
interface FolderNameDialogProps {
opened: boolean;
title: string;
@@ -94,7 +96,7 @@ export function FolderNameDialog({
</Alert>
)}
<Group justify="flex-end">
<Button variant="default" onClick={onClose}>
<Button variant="secondary" onClick={onClose}>
{t("filesPage.folderName.cancel", "Cancel")}
</Button>
<Button
@@ -1,6 +1,7 @@
import React, { useCallback, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { ActionIcon, Menu } from "@mantine/core";
import { Menu } from "@mantine/core";
import { ActionIcon } from "@app/ui/ActionIcon";
import KeyboardArrowRightIcon from "@mui/icons-material/KeyboardArrowRight";
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
import HomeIcon from "@mui/icons-material/Home";
@@ -409,8 +410,8 @@ function TreeNodeRow({
>
<Menu.Target>
<ActionIcon
size="xs"
variant="subtle"
size="sm"
variant="tertiary"
className="files-page-tree-kebab"
aria-label={t(
"filesPage.treeMenu.actions",
@@ -1,9 +1,7 @@
import React, { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import {
ActionIcon,
Alert,
Button,
Group,
Modal,
Stack,
@@ -15,9 +13,10 @@ import HomeIcon from "@mui/icons-material/Home";
import FolderIcon from "@mui/icons-material/Folder";
import FolderOpenIcon from "@mui/icons-material/FolderOpen";
import CreateNewFolderIcon from "@mui/icons-material/CreateNewFolder";
import CloseIcon from "@mui/icons-material/Close";
import ErrorOutlineIcon from "@mui/icons-material/ErrorOutlined";
import { Button } from "@app/ui/Button";
import { ActionIcon } from "@app/ui/ActionIcon";
import { FolderId, FolderRecord, ROOT_FOLDER_ID } from "@app/types/folder";
interface MoveToFolderDialogProps {
@@ -228,8 +227,7 @@ export function MoveToFolderDialog({
withinPortal
>
<ActionIcon
variant="subtle"
color="gray"
variant="tertiary"
size="lg"
onClick={handleCancel}
disabled={creating}
@@ -238,20 +236,20 @@ export function MoveToFolderDialog({
"Discard",
)}
>
<CloseIcon fontSize="small" />
&times;
</ActionIcon>
</Tooltip>
</Group>
) : (
<Button
variant="subtle"
variant="tertiary"
size="sm"
leftSection={<CreateNewFolderIcon fontSize="small" />}
onClick={() => {
setCreatingFolder(true);
setNewFolderName("");
}}
styles={{ root: { alignSelf: "flex-start" } }}
style={{ alignSelf: "flex-start" }}
data-testid="move-dialog-create-folder-toggle"
>
{t(
@@ -272,7 +270,7 @@ export function MoveToFolderDialog({
</Alert>
)}
<Group justify="flex-end">
<Button variant="default" onClick={onClose} disabled={submitting}>
<Button variant="secondary" onClick={onClose} disabled={submitting}>
{t("filesPage.moveDialog.cancel", "Cancel")}
</Button>
<Button
@@ -325,35 +323,35 @@ function FolderPick({
onPick,
}: FolderPickProps) {
return (
<button
type="button"
<Button
variant="tertiary"
justify="start"
fullWidth
onClick={onPick}
disabled={disabled}
leftSection={
isRoot ? (
<HomeIcon fontSize="small" />
) : isActive ? (
<FolderOpenIcon fontSize="small" style={{ color }} />
) : (
<FolderIcon fontSize="small" style={{ color }} />
)
}
style={{
all: "unset",
cursor: disabled ? "not-allowed" : "pointer",
opacity: disabled ? 0.45 : 1,
display: "flex",
alignItems: "center",
gap: "0.5rem",
padding: `0.4rem 0.75rem 0.4rem ${0.75 + depth * 0.85}rem`,
width: "100%",
background: isActive ? "var(--hover-bg)" : "transparent",
borderBottom: "1px solid var(--border-subtle)",
boxSizing: "border-box",
fontWeight: isActive ? 600 : 400,
}}
>
{isRoot ? (
<HomeIcon fontSize="small" />
) : isActive ? (
<FolderOpenIcon fontSize="small" style={{ color }} />
) : (
<FolderIcon fontSize="small" style={{ color }} />
)}
<span style={{ overflow: "hidden", textOverflow: "ellipsis" }}>
{label}
</span>
</button>
</Button>
);
}
@@ -1,6 +1,8 @@
import { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { ActionIcon, Badge, Menu } from "@mantine/core";
import { Badge, Menu } from "@mantine/core";
import { Button } from "@app/ui/Button";
import { ActionIcon } from "@app/ui/ActionIcon";
import OpenInNewIcon from "@mui/icons-material/OpenInNew";
import DeleteIcon from "@mui/icons-material/Delete";
import DownloadIcon from "@mui/icons-material/Download";
@@ -144,8 +146,8 @@ export function VersionTimeline({
<span className="files-page-details-version-timeline-rail-line" />
)}
</div>
<button
type="button"
<Button
variant="tertiary"
className="files-page-details-version-timeline-ellipsis-btn"
onClick={() => setShowAllCollapsed(true)}
>
@@ -154,7 +156,7 @@ export function VersionTimeline({
"Show {{count}} earlier versions",
{ count: row.hidden },
)}
</button>
</Button>
</li>
);
}
@@ -181,24 +183,31 @@ export function VersionTimeline({
)}
</div>
<div className="files-page-details-version-timeline-body">
<button
type="button"
<Button
variant="tertiary"
className="files-page-details-version-timeline-summary"
onClick={() => toggleExpand(v.id)}
aria-expanded={isExpanded}
leftSection={
<Badge
size="xs"
variant={isActive ? "filled" : "outline"}
color="blue"
>
v{v.versionNumber ?? 1}
</Badge>
}
rightSection={
<KeyboardArrowDownIcon
className={`files-page-details-version-timeline-chevron${
isExpanded ? " is-expanded" : ""
}`}
fontSize="small"
/>
}
>
<Badge
size="xs"
variant={isActive ? "filled" : "outline"}
color="blue"
>
v{v.versionNumber ?? 1}
</Badge>
{delta ? (
<span className="files-page-details-version-timeline-delta">
<span className="files-page-details-version-timeline-delta-plus">
+
</span>
<ToolLabel toolId={delta.toolId} />
</span>
) : (
@@ -206,14 +215,7 @@ export function VersionTimeline({
{t("filesPage.versionOrigin", "Original upload")}
</span>
)}
<span className="files-page-details-version-timeline-spacer" />
<KeyboardArrowDownIcon
className={`files-page-details-version-timeline-chevron${
isExpanded ? " is-expanded" : ""
}`}
fontSize="small"
/>
</button>
</Button>
<div className="files-page-details-version-timeline-meta-line">
<span>{formatFileSize(v.size)}</span>
{v.lastModified ? (
@@ -230,7 +232,7 @@ export function VersionTimeline({
<Menu position="bottom-end" withinPortal shadow="md">
<Menu.Target>
<ActionIcon
variant="subtle"
variant="tertiary"
size="sm"
aria-label={t(
"filesPage.versionActions",
@@ -303,13 +305,13 @@ export function VersionTimeline({
})}
</ol>
{collapsible && showAllCollapsed && (
<button
type="button"
<Button
variant="tertiary"
className="files-page-details-version-timeline-collapse-btn"
onClick={() => setShowAllCollapsed(false)}
>
{t("filesPage.versionCollapse", "Collapse middle versions")}
</button>
</Button>
)}
</div>
);
@@ -1,5 +1,7 @@
import React from "react";
import { Button, Group, ActionIcon } from "@mantine/core";
import { Group } from "@mantine/core";
import { ActionIcon } from "@app/ui/ActionIcon";
import { Button } from "@app/ui/Button";
import ChevronLeftIcon from "@mui/icons-material/ChevronLeft";
import { useTranslation } from "react-i18next";
import {
@@ -33,22 +35,6 @@ export function SlideButtons({
(btn) => btn.group === "right",
);
const buttonStyles = (variant: ButtonDefinition["variant"]) =>
variant === "primary"
? {
root: {
background: "var(--onboarding-primary-button-bg)",
color: "var(--onboarding-primary-button-text)",
},
}
: {
root: {
background: "var(--onboarding-secondary-button-bg)",
border: "1px solid var(--onboarding-secondary-button-border)",
color: "var(--onboarding-secondary-button-text)",
},
};
const resolveButtonLabel = (button: ButtonDefinition) => {
// Special case: override "See Plans" with "Upgrade now" when over limit
if (
@@ -77,20 +63,14 @@ export function SlideButtons({
<ActionIcon
key={button.key}
onClick={() => onAction(button.action)}
radius="md"
size={40}
variant="secondary"
accent="neutral"
disabled={disabled}
styles={{
root: {
background: "var(--onboarding-secondary-button-bg)",
border: "1px solid var(--onboarding-secondary-button-border)",
color: "var(--onboarding-secondary-button-text)",
},
}}
aria-label={t("onboarding.buttons.back", "Back")}
>
{button.icon === "chevron-left" && (
{button.icon === "chevron-left" ? (
<ChevronLeftIcon fontSize="small" />
)}
) : null}
</ActionIcon>
);
}
@@ -103,7 +83,10 @@ export function SlideButtons({
key={button.key}
onClick={() => onAction(button.action)}
disabled={disabled}
styles={buttonStyles(variant)}
variant={variant === "primary" ? "primary" : "secondary"}
accent={
button.accent ?? (variant === "primary" ? "default" : "neutral")
}
>
{label}
</Button>
@@ -6,9 +6,10 @@
*/
import React from "react";
import { Modal, Stack, ActionIcon } from "@mantine/core";
import { Modal, Stack } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { ActionIcon } from "@app/ui/ActionIcon";
import DiamondOutlinedIcon from "@mui/icons-material/DiamondOutlined";
import CloseIcon from "@mui/icons-material/Close";
import type {
SlideDefinition,
@@ -45,6 +46,7 @@ export default function OnboardingModalSlide({
onAction,
allowDismiss = true,
}: OnboardingModalSlideProps) {
const { t } = useTranslation();
const renderHero = () => {
if (slideDefinition.hero.type === "dual-icon") {
return (
@@ -139,8 +141,9 @@ export default function OnboardingModalSlide({
{allowDismiss && (
<ActionIcon
onClick={onSkip}
radius="md"
size={36}
variant="tertiary"
size="lg"
aria-label={t("common.close", "Close")}
style={{
position: "absolute",
top: 16,
@@ -150,15 +153,12 @@ export default function OnboardingModalSlide({
backdropFilter: "blur(4px)",
zIndex: 10,
}}
styles={{
root: {
"&:hover": {
backgroundColor: "rgba(255, 255, 255, 0.3)",
},
},
}}
>
<CloseIcon fontSize="small" />
<LocalIcon
icon="close-rounded"
width="1.25rem"
height="1.25rem"
/>
</ActionIcon>
)}
<div className={styles.heroLogo} key={`logo-${slideContent.key}`}>
@@ -8,7 +8,7 @@
import React from "react";
import { TourProvider, useTour, type StepType } from "@reactour/tour";
import { CloseButton, ActionIcon } from "@mantine/core";
import { ActionIcon } from "@app/ui/ActionIcon";
import ArrowForwardIcon from "@mui/icons-material/ArrowForward";
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import CheckIcon from "@mui/icons-material/Check";
@@ -137,7 +137,7 @@ export default function OnboardingTour({
setIsOpen,
})
}
variant="subtle"
variant="tertiary"
size="lg"
aria-label={
isLast
@@ -151,11 +151,15 @@ export default function OnboardingTour({
}}
components={{
Close: ({ onClick }) => (
<CloseButton
<ActionIcon
onClick={onClick}
variant="tertiary"
size="md"
aria-label={t("onboarding.close", "Close")}
style={{ position: "absolute", top: "8px", right: "8px" }}
/>
>
&times;
</ActionIcon>
),
Content: ({ content }: { content: string }) => (
<div
@@ -8,6 +8,7 @@ import TourOverviewSlide from "@app/components/onboarding/slides/TourOverviewSli
import AnalyticsChoiceSlide from "@app/components/onboarding/slides/AnalyticsChoiceSlide";
import MFASetupSlide from "@app/components/onboarding/slides/MFASetupSlide";
import { SlideConfig, LicenseNotice } from "@app/types/types";
import type { ButtonAccent } from "@app/ui/Button";
export type SlideId =
| "first-login"
@@ -83,6 +84,8 @@ export interface ButtonDefinition {
label?: string;
icon?: "chevron-left";
variant?: "primary" | "secondary" | "default";
/** Accent for the shared Button; defaults to neutral. */
accent?: ButtonAccent;
group: "left" | "right";
action: ButtonAction;
disabledWhen?: (state: FlowState) => boolean;
@@ -238,6 +241,7 @@ export const SLIDE_DEFINITIONS: Record<SlideId, SlideDefinition> = {
type: "button",
label: "onboarding.serverLicense.seePlans",
variant: "primary",
accent: "premium",
group: "right",
action: "see-plans",
},
@@ -1,6 +1,6 @@
import React from "react";
import { Trans } from "react-i18next";
import { Button } from "@mantine/core";
import { Button } from "@app/ui/Button";
import OpenInNewIcon from "@mui/icons-material/OpenInNew";
import i18n from "@app/i18n";
import { SlideConfig } from "@app/types/types";
@@ -36,7 +36,7 @@ export default function AnalyticsChoiceSlide({
<br />
<div style={{ textAlign: "right", marginTop: 0 }}>
<Button
variant="default"
variant="secondary"
size="sm"
onClick={() =>
window.open(
@@ -1,6 +1,7 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { Menu, ActionIcon } from "@mantine/core";
import { Menu } from "@mantine/core";
import { ActionIcon } from "@app/ui/ActionIcon";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
export interface OSOption {
@@ -67,8 +68,12 @@ export const DesktopInstallTitle: React.FC<DesktopInstallTitleProps> = ({
<Menu position="bottom" offset={5} zIndex={10000}>
<Menu.Target>
<ActionIcon
variant="transparent"
variant="tertiary"
size="sm"
aria-label={t(
"onboarding.desktopInstall.selectOs",
"Select operating system",
)}
style={{
background: "transparent",
border: "none",
@@ -1,5 +1,6 @@
import React, { useState } from "react";
import { Stack, PasswordInput, Button, Alert, Text } from "@mantine/core";
import { Stack, PasswordInput, Alert, Text } from "@mantine/core";
import { Button } from "@app/ui/Button";
import { useTranslation } from "react-i18next";
import { SlideConfig } from "@app/types/types";
import LocalIcon from "@app/components/shared/LocalIcon";
@@ -199,8 +200,8 @@ function FirstLoginForm({
/>
<Button
type="submit"
fullWidth
type="submit"
loading={loading}
disabled={
!newPassword ||
@@ -209,7 +210,7 @@ function FirstLoginForm({
confirmPassword.length < 8
}
size="md"
mt="xs"
style={{ marginTop: "var(--mantine-spacing-xs)" }}
>
{t("firstLogin.changePassword", "Change Password")}
</Button>
@@ -8,13 +8,13 @@ import {
import {
Alert,
Box,
Button,
Group,
Loader,
Stack,
Text,
TextInput,
} from "@mantine/core";
import { Button } from "@app/ui/Button";
import { QRCodeSVG } from "qrcode.react";
import { useTranslation } from "react-i18next";
import { SlideConfig } from "@app/types/types";
@@ -210,8 +210,8 @@ function MFASetupContent({ onMfaSetupComplete }: MFASetupSlideProps) {
<Group justify="space-between" wrap="wrap">
<Button
variant="secondary"
type="button"
variant="light"
onClick={fetchMfaSetup}
disabled={mfaLoading || submitting || setupComplete}
>
@@ -226,7 +226,7 @@ function MFASetupContent({ onMfaSetupComplete }: MFASetupSlideProps) {
>
Enable MFA
</Button>
<Button type="button" variant="light" onClick={onLogout}>
<Button variant="secondary" type="button" onClick={onLogout}>
Logout
</Button>
</Group>
@@ -5,7 +5,9 @@ import React, {
useMemo,
useEffect,
} from "react";
import { ActionIcon, CheckboxIndicator } from "@mantine/core";
import { CheckboxIndicator } from "@mantine/core";
import { ActionIcon } from "@app/ui/ActionIcon";
import { Button } from "@app/ui/Button";
import { useTranslation } from "react-i18next";
import MoreVertIcon from "@mui/icons-material/MoreVert";
import DeleteOutlineIcon from "@mui/icons-material/DeleteOutlined";
@@ -259,7 +261,7 @@ const FileThumbnail = ({
{/* Kebab menu */}
<ActionIcon
aria-label={t("moreOptions", "More options")}
variant="subtle"
variant="tertiary"
className={styles.kebab}
onClick={(e) => {
e.stopPropagation();
@@ -277,8 +279,18 @@ const FileThumbnail = ({
style={{ width: actionsWidth }}
onClick={(e) => e.stopPropagation()}
>
<button
<Button
variant="tertiary"
justify="start"
fullWidth
className={styles.actionRow}
leftSection={
isPinned ? (
<PushPinIcon fontSize="small" />
) : (
<PushPinOutlinedIcon fontSize="small" />
)
}
onClick={() => {
if (actualFile) {
if (isPinned) {
@@ -292,38 +304,36 @@ const FileThumbnail = ({
setShowActions(false);
}}
>
{isPinned ? (
<PushPinIcon fontSize="small" />
) : (
<PushPinOutlinedIcon fontSize="small" />
)}
<span>{isPinned ? t("unpin", "Unpin") : t("pin", "Pin")}</span>
</button>
<button
{isPinned ? t("unpin", "Unpin") : t("pin", "Pin")}
</Button>
<Button
variant="tertiary"
justify="start"
fullWidth
className={styles.actionRow}
leftSection={<DownloadOutlinedIcon fontSize="small" />}
onClick={() => {
downloadSelectedFile();
setShowActions(false);
}}
>
<DownloadOutlinedIcon fontSize="small" />
<span>{terminology.download}</span>
</button>
{terminology.download}
</Button>
<div className={styles.actionsDivider} />
<button
<Button
variant="tertiary"
justify="start"
fullWidth
className={`${styles.actionRow} ${styles.actionDanger}`}
leftSection={<DeleteOutlineIcon fontSize="small" />}
onClick={() => {
onDeleteFile(file.id);
onSetStatus(`Deleted ${file.name}`);
setShowActions(false);
}}
>
<DeleteOutlineIcon fontSize="small" />
<span>{t("delete", "Delete")}</span>
</button>
{t("delete", "Delete")}
</Button>
</div>
)}
@@ -1,4 +1,5 @@
import { Tooltip, ActionIcon } from "@mantine/core";
import { Tooltip } from "@mantine/core";
import { ActionIcon } from "@app/ui/ActionIcon";
import UndoIcon from "@mui/icons-material/Undo";
import RedoIcon from "@mui/icons-material/Redo";
import ContentCutIcon from "@mui/icons-material/ContentCut";
@@ -135,28 +136,22 @@ const PageEditorControls = ({
{/* Undo/Redo */}
<Tooltip label={t("pageEditor.toolbar.undo", "Undo")}>
<ActionIcon
variant="tertiary"
size="lg"
onClick={onUndo}
disabled={!canUndo}
variant="subtle"
style={{
color: canUndo ? "var(--text-secondary)" : "var(--text-muted)",
}}
radius="md"
size="lg"
aria-label={t("pageEditor.toolbar.undo", "Undo")}
>
<UndoIcon />
</ActionIcon>
</Tooltip>
<Tooltip label={t("pageEditor.toolbar.redo", "Redo")}>
<ActionIcon
variant="tertiary"
size="lg"
onClick={onRedo}
disabled={!canRedo}
variant="subtle"
style={{
color: canRedo ? "var(--text-secondary)" : "var(--text-muted)",
}}
radius="md"
size="lg"
aria-label={t("pageEditor.toolbar.redo", "Redo")}
>
<RedoIcon />
</ActionIcon>
@@ -176,17 +171,14 @@ const PageEditorControls = ({
label={t("pageEditor.toolbar.rotateLeft", "Rotate Selected Left")}
>
<ActionIcon
variant="tertiary"
size="lg"
onClick={() => onRotate("left")}
disabled={selectedPageIds.length === 0}
variant="subtle"
style={{
color:
selectedPageIds.length > 0
? "var(--text-secondary)"
: "var(--text-muted)",
}}
radius="md"
size="lg"
aria-label={t(
"pageEditor.toolbar.rotateLeft",
"Rotate Selected Left",
)}
>
<RotateLeftIcon />
</ActionIcon>
@@ -195,68 +187,47 @@ const PageEditorControls = ({
label={t("pageEditor.toolbar.rotateRight", "Rotate Selected Right")}
>
<ActionIcon
variant="tertiary"
size="lg"
onClick={() => onRotate("right")}
disabled={selectedPageIds.length === 0}
variant="subtle"
style={{
color:
selectedPageIds.length > 0
? "var(--text-secondary)"
: "var(--text-muted)",
}}
radius="md"
size="lg"
aria-label={t(
"pageEditor.toolbar.rotateRight",
"Rotate Selected Right",
)}
>
<RotateRightIcon />
</ActionIcon>
</Tooltip>
<Tooltip label={t("pageEditor.toolbar.delete", "Delete Selected")}>
<ActionIcon
variant="tertiary"
size="lg"
onClick={onDelete}
disabled={selectedPageIds.length === 0}
variant="subtle"
style={{
color:
selectedPageIds.length > 0
? "var(--text-secondary)"
: "var(--text-muted)",
}}
radius="md"
size="lg"
aria-label={t("pageEditor.toolbar.delete", "Delete Selected")}
>
<DeleteIcon />
</ActionIcon>
</Tooltip>
<Tooltip label={getSplitTooltip()}>
<ActionIcon
variant="tertiary"
size="lg"
onClick={onSplit}
disabled={selectedPageIds.length === 0}
variant="subtle"
style={{
color:
selectedPageIds.length > 0
? "var(--text-secondary)"
: "var(--text-muted)",
}}
radius="md"
size="lg"
aria-label={getSplitTooltip()}
>
<ContentCutIcon />
</ActionIcon>
</Tooltip>
<Tooltip label={getPageBreakTooltip()}>
<ActionIcon
variant="tertiary"
size="lg"
onClick={onPageBreak}
disabled={selectedPageIds.length === 0}
variant="subtle"
style={{
color:
selectedPageIds.length > 0
? "var(--text-secondary)"
: "var(--text-muted)",
}}
radius="md"
size="lg"
aria-label={getPageBreakTooltip()}
>
<InsertPageBreakIcon />
</ActionIcon>
@@ -1,4 +1,5 @@
import { ActionIcon, Popover } from "@mantine/core";
import { Popover } from "@mantine/core";
import { ActionIcon } from "@app/ui/ActionIcon";
import LocalIcon from "@app/components/shared/LocalIcon";
import { Tooltip } from "@app/components/shared/Tooltip";
import BulkSelectionPanel from "@app/components/pageEditor/BulkSelectionPanel";
@@ -37,8 +38,7 @@ export default function PageSelectByNumberButton({
<Popover.Target>
<div style={{ display: "inline-flex" }}>
<ActionIcon
variant="subtle"
radius="md"
variant="tertiary"
disabled={disabled || totalPages === 0}
aria-label={label}
>
@@ -1,4 +1,5 @@
import { Button, Text, Group, Divider } from "@mantine/core";
import { Text, Group, Divider } from "@mantine/core";
import { Button } from "@app/ui/Button";
import { useTranslation } from "react-i18next";
import classes from "@app/components/pageEditor/bulkSelectionPanel/BulkSelectionPanel.module.css";
import { LogicalOperator } from "@app/utils/bulkselection/selectionBuilders";
@@ -22,7 +23,7 @@ const OperatorsSection = ({
<Group gap="sm" wrap="nowrap">
<Button
size="sm"
variant="outline"
variant="secondary"
className={classes.operatorChip}
onClick={() => onInsertOperator("and")}
disabled={!csvInput.trim()}
@@ -37,7 +38,7 @@ const OperatorsSection = ({
</Button>
<Button
size="sm"
variant="outline"
variant="secondary"
className={classes.operatorChip}
onClick={() => onInsertOperator("or")}
disabled={!csvInput.trim()}
@@ -52,7 +53,7 @@ const OperatorsSection = ({
</Button>
<Button
size="sm"
variant="outline"
variant="secondary"
className={classes.operatorChip}
onClick={() => onInsertOperator("not")}
disabled={!csvInput.trim()}
@@ -70,7 +71,7 @@ const OperatorsSection = ({
<Group gap="sm" wrap="nowrap">
<Button
size="sm"
variant="outline"
variant="secondary"
className={classes.operatorChip}
onClick={() => onInsertOperator("even")}
title={t(
@@ -84,7 +85,7 @@ const OperatorsSection = ({
</Button>
<Button
size="sm"
variant="outline"
variant="secondary"
className={classes.operatorChip}
onClick={() => onInsertOperator("odd")}
title={t(
@@ -1,4 +1,5 @@
import { TextInput, Button, Text, Flex, Switch } from "@mantine/core";
import { TextInput, Text, Flex, Switch } from "@mantine/core";
import { ActionIcon } from "@app/ui/ActionIcon";
import { useTranslation } from "react-i18next";
import LocalIcon from "@app/components/shared/LocalIcon";
import { Tooltip } from "@app/components/shared/Tooltip";
@@ -77,20 +78,15 @@ const PageSelectionInput = ({
placeholder="1,3,5-10"
rightSection={
csvInput && (
<Button
variant="subtle"
size="xs"
<ActionIcon
variant="tertiary"
accent="neutral"
size="sm"
onClick={onClear}
style={{
color: "var(--text-muted)",
minWidth: "auto",
width: "24px",
height: "24px",
padding: 0,
}}
aria-label={t("clear", "Clear")}
>
×
</Button>
&times;
</ActionIcon>
)
}
onKeyDown={(e) => e.key === "Enter" && onUpdatePagesFromCSV()}
@@ -1,5 +1,6 @@
import { useState } from "react";
import { Button, Text, NumberInput, Group } from "@mantine/core";
import { Text, NumberInput, Group } from "@mantine/core";
import { Button } from "@app/ui/Button";
import classes from "@app/components/pageEditor/bulkSelectionPanel/BulkSelectionPanel.module.css";
interface SelectPagesProps {
@@ -5,7 +5,8 @@ import React, {
useCallback,
useRef,
} from "react";
import { Badge, Modal, Text, ActionIcon, Tooltip, Group } from "@mantine/core";
import { Badge, Modal, Text, Tooltip, Group } from "@mantine/core";
import { ActionIcon } from "@app/ui/ActionIcon";
import { useNavigate, useLocation } from "react-router-dom";
import { useTranslation } from "react-i18next";
import LocalIcon from "@app/components/shared/LocalIcon";
@@ -365,7 +366,7 @@ const AppConfigModalInner: React.FC<AppConfigModalProps> = ({
/>
<ActionIcon
ref={closeButtonRef}
variant="subtle"
variant="tertiary"
onClick={handleClose}
aria-label={t("settings.close", "Close")}
data-autofocus
@@ -3,13 +3,13 @@ import {
Modal,
Stack,
Text,
Button,
Group,
Alert,
TextInput,
Paper,
Select,
} from "@mantine/core";
import { Button } from "@app/ui/Button";
import LinkIcon from "@mui/icons-material/Link";
import ContentCopyRoundedIcon from "@mui/icons-material/ContentCopyRounded";
import { useTranslation } from "react-i18next";
@@ -243,8 +243,8 @@ const BulkShareModal: React.FC<BulkShareModalProps> = ({
label={t("storageShare.linkLabel", "Share link")}
rightSection={
<Button
variant="subtle"
size="xs"
variant="tertiary"
size="sm"
leftSection={
<ContentCopyRoundedIcon style={{ fontSize: 16 }} />
}
@@ -297,7 +297,7 @@ const BulkShareModal: React.FC<BulkShareModalProps> = ({
</Paper>
<Group justify="flex-end" gap="sm">
<Button variant="default" onClick={onClose} disabled={isWorking}>
<Button variant="secondary" onClick={onClose} disabled={isWorking}>
{t("cancel", "Cancel")}
</Button>
<Button
@@ -1,5 +1,6 @@
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { Modal, Stack, Text, Button, Group, Alert } from "@mantine/core";
import { Modal, Stack, Text, Group, Alert } from "@mantine/core";
import { Button } from "@app/ui/Button";
import CloudUploadIcon from "@mui/icons-material/CloudUpload";
import { useTranslation } from "react-i18next";
@@ -151,7 +152,7 @@ const BulkUploadToServerModal: React.FC<BulkUploadToServerModalProps> = ({
)}
<Group justify="flex-end" gap="sm">
<Button variant="default" onClick={onClose} disabled={isUploading}>
<Button variant="secondary" onClick={onClose} disabled={isUploading}>
{t("cancel", "Cancel")}
</Button>
<Button
@@ -8,6 +8,14 @@ const TestWrapper = ({ children }: { children: React.ReactNode }) => (
<MantineProvider>{children}</MantineProvider>
);
// The shared SegmentedControl renders each option as a radio <input> (inside a
// <label>) whose `value` attribute matches the option value. Select by value
// since it is stable regardless of how the label is wrapped (e.g. FitText).
const getRadioByValue = (container: HTMLElement, value: string) =>
container.querySelector<HTMLInputElement>(
`input[type="radio"][value="${value}"]`,
);
describe("ButtonSelector", () => {
const mockOnChange = vi.fn();
@@ -15,7 +23,7 @@ describe("ButtonSelector", () => {
vi.clearAllMocks();
});
test("should render all options as buttons", () => {
test("should render all options as segments", () => {
const options = [
{ value: "option1", label: "Option 1" },
{ value: "option2", label: "Option 2" },
@@ -37,13 +45,13 @@ describe("ButtonSelector", () => {
expect(screen.getByText("Option 2")).toBeInTheDocument();
});
test("should highlight selected button with filled variant", () => {
test("should mark selected option as checked", () => {
const options = [
{ value: "option1", label: "Option 1" },
{ value: "option2", label: "Option 2" },
];
render(
const { container } = render(
<TestWrapper>
<ButtonSelector
value="option1"
@@ -54,22 +62,22 @@ describe("ButtonSelector", () => {
</TestWrapper>,
);
const selectedButton = screen.getByRole("button", { name: "Option 1" });
const unselectedButton = screen.getByRole("button", { name: "Option 2" });
const selectedRadio = getRadioByValue(container, "option1");
const unselectedRadio = getRadioByValue(container, "option2");
// Check data-variant attribute for filled/outline
expect(selectedButton).toHaveAttribute("data-variant", "filled");
expect(unselectedButton).toHaveAttribute("data-variant", "outline");
// Selected option is marked via the radio's checked state.
expect(selectedRadio).toBeChecked();
expect(unselectedRadio).not.toBeChecked();
expect(screen.getByText("Selection Label")).toBeInTheDocument();
});
test("should call onChange when button is clicked", () => {
test("should call onChange when an option is clicked", () => {
const options = [
{ value: "option1", label: "Option 1" },
{ value: "option2", label: "Option 2" },
];
render(
const { container } = render(
<TestWrapper>
<ButtonSelector
value="option1"
@@ -79,7 +87,7 @@ describe("ButtonSelector", () => {
</TestWrapper>,
);
fireEvent.click(screen.getByRole("button", { name: "Option 2" }));
fireEvent.click(getRadioByValue(container, "option2")!);
expect(mockOnChange).toHaveBeenCalledWith("option2");
});
@@ -90,7 +98,7 @@ describe("ButtonSelector", () => {
{ value: "option2", label: "Option 2" },
];
render(
const { container } = render(
<TestWrapper>
<ButtonSelector
value={undefined}
@@ -100,17 +108,17 @@ describe("ButtonSelector", () => {
</TestWrapper>,
);
// Both buttons should be outlined when no value is selected
const button1 = screen.getByRole("button", { name: "Option 1" });
const button2 = screen.getByRole("button", { name: "Option 2" });
// No option should be checked when no value is selected
const radio1 = getRadioByValue(container, "option1");
const radio2 = getRadioByValue(container, "option2");
expect(button1).toHaveAttribute("data-variant", "outline");
expect(button2).toHaveAttribute("data-variant", "outline");
expect(radio1).not.toBeChecked();
expect(radio2).not.toBeChecked();
});
test.each([
{
description: "disable buttons when disabled prop is true",
description: "disable options when disabled prop is true",
options: [
{ value: "option1", label: "Option 1" },
{ value: "option2", label: "Option 2" },
@@ -128,7 +136,7 @@ describe("ButtonSelector", () => {
expectedStates: [false, true],
},
])("should $description", ({ options, globalDisabled, expectedStates }) => {
render(
const { container } = render(
<TestWrapper>
<ButtonSelector
value="option1"
@@ -140,18 +148,18 @@ describe("ButtonSelector", () => {
);
options.forEach((option, index) => {
const button = screen.getByRole("button", { name: option.label });
expect(button).toHaveProperty("disabled", expectedStates[index]);
const radio = getRadioByValue(container, String(option.value));
expect(radio).toHaveProperty("disabled", expectedStates[index]);
});
});
test("should not call onChange when disabled button is clicked", () => {
test("should not allow selecting a disabled option", () => {
const options = [
{ value: "option1", label: "Option 1" },
{ value: "option2", label: "Option 2", disabled: true },
];
render(
const { container } = render(
<TestWrapper>
<ButtonSelector
value="option1"
@@ -161,12 +169,16 @@ describe("ButtonSelector", () => {
</TestWrapper>,
);
fireEvent.click(screen.getByRole("button", { name: "Option 2" }));
// The disabled option's radio is disabled, so a real user cannot select it
// and onChange will not fire from genuine interaction. (jsdom does not
// replicate the browser's disabled-click blocking, so assert the disabled
// state — that is what prevents selection for real users.)
const disabledRadio = getRadioByValue(container, "option2");
expect(disabledRadio).toBeDisabled();
expect(mockOnChange).not.toHaveBeenCalled();
});
test("should not apply fullWidth styling when fullWidth is false", () => {
test("should render options when fullWidth is false", () => {
const options = [
{ value: "option1", label: "Option 1" },
{ value: "option2", label: "Option 2" },
@@ -184,8 +196,7 @@ describe("ButtonSelector", () => {
</TestWrapper>,
);
const button = screen.getByRole("button", { name: "Option 1" });
expect(button).not.toHaveStyle({ flex: "1" });
expect(screen.getByText("Option 1")).toBeInTheDocument();
expect(screen.getByText("Layout Label")).toBeInTheDocument();
});
@@ -205,14 +216,14 @@ describe("ButtonSelector", () => {
</TestWrapper>,
);
// Should render buttons
// Should render the options
expect(screen.getByText("Option 1")).toBeInTheDocument();
expect(screen.getByText("Option 2")).toBeInTheDocument();
// Stack should only contain the Group (buttons), no Text element for label
// Stack should only contain the SegmentedControl, no label Text element
const stackElement = container.querySelector(
'[class*="mantine-Stack-root"]',
);
expect(stackElement?.children).toHaveLength(1); // Only the Group, no label Text
expect(stackElement?.children).toHaveLength(1); // Only the SegmentedControl, no label Text
});
});
@@ -1,5 +1,6 @@
import { Button, Group, Stack, Text, Tooltip } from "@mantine/core";
import { Stack, Text, Tooltip } from "@mantine/core";
import FitText from "@app/components/shared/FitText";
import { SegmentedControl } from "@app/ui/SegmentedControl";
export interface ButtonOption<T> {
value: T;
@@ -15,7 +16,6 @@ interface ButtonSelectorProps<T> {
label?: string;
disabled?: boolean;
fullWidth?: boolean;
buttonClassName?: string;
textClassName?: string;
}
@@ -26,9 +26,43 @@ const ButtonSelector = <T extends string | number>({
label = undefined,
disabled = false,
fullWidth = true,
buttonClassName,
textClassName,
}: ButtonSelectorProps<T>) => {
const selectedValue = value === undefined ? "" : String(value);
const segmentedOptions = options.map((option) => {
const isDisabled = disabled || option.disabled;
const fitText = (
<FitText
text={option.label}
lines={1}
minimumFontScale={0.5}
fontSize={10}
className={textClassName}
/>
);
return {
value: String(option.value),
disabled: isDisabled,
label:
option.tooltip && isDisabled ? (
<Tooltip label={option.tooltip} position="top" withArrow>
<span>{fitText}</span>
</Tooltip>
) : (
fitText
),
};
});
const handleChange = (next: string) => {
const matched = options.find((option) => String(option.value) === next);
if (matched) {
onChange(matched.value);
}
};
return (
<Stack gap="var(--mantine-spacing-sm)">
{/* Label (if it exists) */}
@@ -44,69 +78,12 @@ const ButtonSelector = <T extends string | number>({
</Text>
)}
{/* Buttons */}
<Group gap="4px">
{options.map((option) => {
const isDisabled = disabled || option.disabled;
const button = (
<Button
variant={value === option.value ? "filled" : "outline"}
color={
value === option.value
? "var(--color-primary-500)"
: "var(--text-muted)"
}
onClick={() => onChange(option.value)}
disabled={isDisabled}
className={buttonClassName}
style={{
flex: fullWidth ? 1 : undefined,
height: "auto",
minHeight: "2.5rem",
fontSize: "var(--mantine-font-size-sm)",
lineHeight: "1.4",
paddingTop: "0.5rem",
paddingBottom: "0.5rem",
}}
>
<FitText
text={option.label}
lines={1}
minimumFontScale={0.5}
fontSize={10}
className={textClassName}
/>
</Button>
);
// Wrap with tooltip if provided (useful for disabled state explanations)
if (option.tooltip && isDisabled) {
return (
<Tooltip
key={option.value}
label={option.tooltip}
position="top"
withArrow
>
<span
style={{ flex: fullWidth ? 1 : undefined, display: "flex" }}
>
{button}
</span>
</Tooltip>
);
}
return (
<span
key={option.value}
style={{ flex: fullWidth ? 1 : undefined, display: "flex" }}
>
{button}
</span>
);
})}
</Group>
<SegmentedControl
options={segmentedOptions}
value={selectedValue}
onChange={handleChange}
fullWidth={fullWidth}
/>
</Stack>
);
};
@@ -1,5 +1,4 @@
import { Button, Stack } from "@mantine/core";
import React from "react";
import { SegmentedControl } from "@app/ui/SegmentedControl";
export interface ButtonToggleOption {
value: string;
@@ -13,70 +12,49 @@ export interface ButtonToggleProps {
value: string;
onChange: (value: string) => void;
disabled?: boolean;
orientation?: "vertical" | "horizontal";
size?: "xs" | "sm" | "md" | "lg";
fullWidth?: boolean;
}
export const ButtonToggle: React.FC<ButtonToggleProps> = ({
export const ButtonToggle = ({
options,
value,
onChange,
disabled = false,
orientation = "vertical",
size = "md",
fullWidth = true,
}) => {
const isVertical = orientation === "vertical";
}: ButtonToggleProps) => {
const segmentedSize = size === "xs" || size === "sm" ? "sm" : "md";
const buttonStyle: React.CSSProperties = {
justifyContent: "flex-start",
height: isVertical ? "auto" : undefined,
minHeight: isVertical ? "50px" : undefined,
padding: isVertical ? "12px 16px" : undefined,
textAlign: "left",
};
const renderButton = (option: ButtonToggleOption) => {
const isSelected = value === option.value;
const isDisabled = disabled || option.disabled;
return (
<Button
key={option.value}
variant={isSelected ? "filled" : "outline"}
onClick={() => !isDisabled && onChange(option.value)}
disabled={isDisabled}
size={size}
fullWidth={fullWidth}
style={buttonStyle}
>
<div style={{ width: "100%" }}>
<div style={{ fontWeight: 600 }}>{option.label}</div>
{option.description && (
<div
style={{
fontSize: "0.85em",
opacity: 0.8,
marginTop: "4px",
fontWeight: 400,
}}
>
{option.description}
</div>
)}
</div>
</Button>
);
};
if (isVertical) {
return <Stack gap="xs">{options.map(renderButton)}</Stack>;
}
const segmentedOptions = options.map((option) => ({
value: option.value,
disabled: disabled || option.disabled,
label: (
<div>
<div style={{ fontWeight: 600 }}>{option.label}</div>
{option.description && (
<div
style={{
fontSize: "0.85em",
opacity: 0.8,
marginTop: 4,
fontWeight: 400,
}}
>
{option.description}
</div>
)}
</div>
),
}));
return (
<div style={{ display: "flex", gap: "8px", flexWrap: "wrap" }}>
{options.map(renderButton)}
</div>
<SegmentedControl
options={segmentedOptions}
value={value}
onChange={onChange}
size={segmentedSize}
fullWidth={fullWidth}
/>
);
};
@@ -1,9 +1,9 @@
import React from "react";
import { Button, Group } from "@mantine/core";
import { Group } from "@mantine/core";
import { Button } from "@app/ui/Button";
import { useTranslation } from "react-i18next";
import { useFileState } from "@app/contexts/FileContext";
import { useFileActions } from "@app/contexts/file/fileHooks";
import CloseIcon from "@mui/icons-material/Close";
import { Z_INDEX_TOAST } from "@app/styles/zIndex";
interface DismissAllErrorsButtonProps {
@@ -32,10 +32,9 @@ const DismissAllErrorsButton: React.FC<DismissAllErrorsButtonProps> = ({
return (
<Group className={className}>
<Button
variant="light"
color="red"
variant="secondary"
accent="danger"
size="sm"
leftSection={<CloseIcon fontSize="small" />}
onClick={handleDismissAllErrors}
style={{
position: "absolute",
@@ -16,7 +16,7 @@ import { Z_INDEX_AUTOMATE_DROPDOWN } from "@app/styles/zIndex";
export interface DropdownItem {
value: string;
name: string;
leftIcon?: ReactNode;
leftSection?: ReactNode;
disabled?: boolean;
}
@@ -232,9 +232,9 @@ const DropdownListWithFooter: React.FC<DropdownListWithFooterProps> = ({
}}
>
<Group gap="sm" style={{ flex: 1 }}>
{item.leftIcon && (
{item.leftSection && (
<Box style={{ display: "flex", alignItems: "center" }}>
{item.leftIcon}
{item.leftSection}
</Box>
)}
<Text size="sm">{item.name}</Text>
@@ -1,12 +1,7 @@
import { useState, useRef, useEffect } from "react";
import {
PasswordInput,
Group,
ActionIcon,
Tooltip,
TextInput,
} from "@mantine/core";
import { PasswordInput, Group, Tooltip, TextInput } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { ActionIcon } from "@app/ui/ActionIcon";
import LocalIcon from "@app/components/shared/LocalIcon";
interface EditableSecretFieldProps {
@@ -95,7 +90,7 @@ export default function EditableSecretField({
<TextInput value="••••••••" disabled style={{ flex: 1 }} readOnly />
<Tooltip label={t("editSecret")} withArrow>
<ActionIcon
variant="light"
variant="secondary"
onClick={handleEdit}
disabled={disabled}
title={t("editableSecretField.edit", "Edit")}
@@ -1,11 +1,5 @@
import {
Modal,
Stack,
Text,
Button,
PasswordInput,
Group,
} from "@mantine/core";
import { Modal, Stack, Text, PasswordInput, Group } from "@mantine/core";
import { Button } from "@app/ui/Button";
import { useTranslation } from "react-i18next";
import { type KeyboardEventHandler } from "react";
import { Z_INDEX_OVER_FULLSCREEN_SURFACE } from "@app/styles/zIndex";
@@ -87,8 +81,8 @@ const EncryptedPdfUnlockModal = ({
<Group justify="space-between">
<Button
variant="light"
color="var(--mantine-color-gray-8)"
variant="secondary"
accent="neutral"
onClick={onSkip}
disabled={isProcessing}
>
@@ -97,7 +91,7 @@ const EncryptedPdfUnlockModal = ({
<Group gap="xs">
{remainingCount > 0 && (
<Button
variant="light"
variant="secondary"
onClick={onUnlockAll}
loading={isProcessing}
disabled={password.trim().length === 0}
@@ -1,5 +1,6 @@
import React from "react";
import { Text, Button, Stack } from "@mantine/core";
import { Text, Stack } from "@mantine/core";
import { Button } from "@app/ui/Button";
interface ErrorBoundaryState {
hasError: boolean;
@@ -128,7 +129,11 @@ export default class ErrorBoundary extends React.Component<
)}
</>
)}
<Button onClick={this.retry} variant="light" mt="md">
<Button
onClick={this.retry}
variant="secondary"
style={{ marginTop: "var(--mantine-spacing-md)" }}
>
Try Again
</Button>
</Stack>
@@ -1,15 +1,7 @@
import { useState } from "react";
import {
Card,
Stack,
Text,
Group,
Badge,
Button,
Box,
ActionIcon,
Tooltip,
} from "@mantine/core";
import { Card, Stack, Text, Group, Badge, Box, Tooltip } from "@mantine/core";
import { Button } from "@app/ui/Button";
import { ActionIcon } from "@app/ui/ActionIcon";
import { useTranslation } from "react-i18next";
import StorageIcon from "@mui/icons-material/Storage";
import VisibilityIcon from "@mui/icons-material/Visibility";
@@ -114,8 +106,8 @@ const FileCard = ({
<Tooltip label={t("fileCard.viewInViewer", "View in Viewer")}>
<ActionIcon
size="sm"
variant="subtle"
color="blue"
variant="tertiary"
aria-label={t("fileCard.viewInViewer", "View in Viewer")}
onClick={(e) => {
e.stopPropagation();
onView();
@@ -131,8 +123,12 @@ const FileCard = ({
>
<ActionIcon
size="sm"
variant="subtle"
color="orange"
variant="tertiary"
accent="warning"
aria-label={t(
"fileCard.openInFileEditor",
"Open in File Editor",
)}
onClick={(e) => {
e.stopPropagation();
onEdit();
@@ -182,14 +178,14 @@ const FileCard = ({
</Group>
<Button
color="red"
size="xs"
variant="light"
accent="danger"
size="sm"
variant="secondary"
onClick={(e) => {
e.stopPropagation();
onRemove();
}}
mt={4}
style={{ marginTop: 4 }}
>
{t("delete", "Remove")}
</Button>
@@ -1,5 +1,6 @@
import React from "react";
import { Menu, Loader, Group, Text, ActionIcon, Tooltip } from "@mantine/core";
import { Menu, Loader, Group, Text, Tooltip } from "@mantine/core";
import { ActionIcon } from "@app/ui/ActionIcon";
import { useTranslation } from "react-i18next";
import InsertDriveFileIcon from "@mui/icons-material/InsertDriveFile";
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
@@ -102,10 +103,14 @@ export const FileDropdownMenu: React.FC<FileDropdownMenuProps> = ({
withArrow
>
<ActionIcon
component="div"
size="xs"
variant="subtle"
color="red"
as="span"
size="sm"
variant="tertiary"
accent="danger"
aria-label={t(
"fileDropdownMenu.closeFile",
"Close file",
)}
onClick={(e) => {
e.stopPropagation();
onFileRemove(file.fileId as FileId);
@@ -1,13 +1,6 @@
import { useState } from "react";
import {
Box,
Flex,
Group,
Text,
Button,
TextInput,
Select,
} from "@mantine/core";
import { Box, Flex, Group, Text, TextInput, Select } from "@mantine/core";
import { Button } from "@app/ui/Button";
import { useTranslation } from "react-i18next";
import SearchIcon from "@mui/icons-material/Search";
import SortIcon from "@mui/icons-material/Sort";
@@ -121,7 +114,7 @@ const FileGrid = ({
</Group>
{onDeleteAll && (
<Button color="red" size="sm" onClick={onDeleteAll}>
<Button accent="danger" size="sm" onClick={onDeleteAll}>
{t("fileManager.deleteAll", "Delete All")}
</Button>
)}
@@ -179,7 +172,7 @@ const FileGrid = ({
{/* Show All Button */}
{hasMoreFiles && onShowAll && (
<Group justify="center" mt="md">
<Button variant="light" onClick={onShowAll}>
<Button variant="secondary" onClick={onShowAll}>
{t("fileManager.showAll", "Show All")} ({sortedFiles.length} files)
</Button>
</Group>
@@ -2,7 +2,6 @@ import { useState, useEffect } from "react";
import {
Modal,
Text,
Button,
Group,
Stack,
Checkbox,
@@ -11,6 +10,7 @@ import {
Badge,
SimpleGrid,
} from "@mantine/core";
import { Button } from "@app/ui/Button";
import { useTranslation } from "react-i18next";
import DocumentThumbnail from "@app/components/shared/filePreview/DocumentThumbnail";
import { FileId } from "@app/types/file";
@@ -151,10 +151,10 @@ const FilePickerModal = ({
)}
</Text>
<Group gap="xs">
<Button size="xs" variant="light" onClick={selectAll}>
<Button size="sm" variant="secondary" onClick={selectAll}>
{t("pageEdit.selectAll", "Select All")}
</Button>
<Button size="xs" variant="light" onClick={selectNone}>
<Button size="sm" variant="secondary" onClick={selectNone}>
{t("pageEdit.deselectAll", "Select None")}
</Button>
</Group>
@@ -249,7 +249,7 @@ const FilePickerModal = ({
{/* Action buttons */}
<Group justify="flex-end" mt="md">
<Button variant="light" onClick={onClose}>
<Button variant="secondary" onClick={onClose}>
{t("close", "Cancel")}
</Button>
<Button
@@ -1,6 +1,7 @@
import { useState, useEffect, useCallback, useMemo, useRef } from "react";
import { createPortal } from "react-dom";
import { Box, Popover, ScrollArea, Text, Loader } from "@mantine/core";
import { Button } from "@app/ui/Button";
import AddIcon from "@mui/icons-material/Add";
import { useTranslation } from "react-i18next";
import {
@@ -566,9 +567,10 @@ export function FileSelectorPicker({
const meta = buildMeta(stub);
const isItemLoading = loadingId === stub.id;
return (
<button
<Button
key={stub.id}
type="button"
variant="tertiary"
hover={false}
className={styles.fileItem}
onClick={() => void loadAndSelect(stub)}
disabled={!!loadingId}
@@ -579,6 +581,9 @@ export function FileSelectorPicker({
})
}
onMouseLeave={() => setHoveredStub(null)}
rightSection={
isItemLoading ? <Loader size="xs" /> : undefined
}
>
<div className={styles.fileItemContent}>
<span className={styles.fileName} title={stub.name}>
@@ -586,8 +591,7 @@ export function FileSelectorPicker({
</span>
{meta && <span className={styles.fileMeta}>{meta}</span>}
</div>
{isItemLoading && <Loader size="xs" />}
</button>
</Button>
);
})
)}
@@ -7,6 +7,7 @@ import React, {
forwardRef,
} from "react";
import { Loader, Tooltip } from "@mantine/core";
import { ActionIcon } from "@app/ui/ActionIcon";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { useFileState, useFileActions } from "@app/contexts/file/fileHooks";
@@ -961,26 +962,31 @@ const FileSidebar = forwardRef<HTMLDivElement, FileSidebarProps>(
<span className="file-sidebar-section-label">
{t("fileSidebar.files", "Files")}
</span>
<button
<ActionIcon
variant="quiet"
className="file-sidebar-section-btn file-sidebar-section-btn-external"
onClick={() => navigate("/files")}
title={t(
"fileSidebar.openFileManager",
"Browse all files & folders",
)}
type="button"
aria-label={t(
"fileSidebar.openFileManager",
"Browse all files & folders",
)}
data-testid="open-files-page"
>
<OpenInNewIcon sx={{ fontSize: "1rem" }} />
</button>
<button
</ActionIcon>
<ActionIcon
variant="quiet"
className="file-sidebar-section-btn file-sidebar-section-btn-add"
onClick={() => nativeFileInputRef.current?.click()}
title={t("fileSidebar.addFiles", "Add files")}
type="button"
aria-label={t("fileSidebar.addFiles", "Add files")}
>
<AddIcon sx={{ fontSize: "1rem" }} />
</button>
</ActionIcon>
</div>
{!stubsLoaded ? (
@@ -989,35 +995,6 @@ const FileSidebar = forwardRef<HTMLDivElement, FileSidebarProps>(
</div>
) : filteredFileStubs.length > 0 ? (
<div className="file-sidebar-file-list">
<button
type="button"
onClick={() => nativeFileInputRef.current?.click()}
data-testid="add-files-row"
style={{
background: "transparent",
border: "none",
cursor: "pointer",
color: "var(--text-muted)",
padding: "4px 6px",
marginBottom: 4,
display: "flex",
alignItems: "center",
gap: 6,
fontSize: 12,
width: "100%",
textAlign: "left",
borderRadius: 4,
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = "var(--hover-bg)";
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = "transparent";
}}
>
<AddIcon sx={{ fontSize: "0.9rem" }} />
{t("fileSidebar.addFiles", "Add files")}
</button>
{filteredFileStubs.map((stub) => {
const workbenchFileId = state.files.ids.find(
(id) => (id as string) === (stub.id as string),
@@ -91,7 +91,7 @@
width: 20px;
height: 20px;
border-radius: 5px;
border: 1.5px solid var(--border-hover, #52525b);
border: 1.5px solid var(--border-hover, var(--border-strong));
}
.file-sidebar-file-item:hover .file-sidebar-file-checkbox-hover {
@@ -1,6 +1,7 @@
import { useState, useCallback, useRef } from "react";
import { createPortal } from "react-dom";
import { Menu, Tooltip } from "@mantine/core";
import { ActionIcon } from "@app/ui/ActionIcon";
import { useTranslation } from "react-i18next";
import VisibilityOutlinedIcon from "@mui/icons-material/VisibilityOutlined";
import VisibilityOffOutlinedIcon from "@mui/icons-material/VisibilityOffOutlined";
@@ -369,14 +370,15 @@ export function FileItem({
</span>
)}
</div>
<button
<ActionIcon
variant="tertiary"
size="sm"
className="file-sidebar-eye-btn"
onClick={(e) => {
e.stopPropagation();
onEyeClick(fileId, e);
}}
tabIndex={-1}
type="button"
aria-label={
isViewedInViewer
? t("fileSidebar.fileItem.closeViewer", "Close viewer")
@@ -391,25 +393,25 @@ export function FileItem({
className="file-sidebar-eye-closed"
sx={{ fontSize: "1.1rem" }}
/>
</button>
</ActionIcon>
{(onDelete ||
onSaveToCloud ||
(hasVersionHistory && onVersionHistory)) && (
<Menu position="bottom-end" withinPortal shadow="md" width={190}>
<Menu.Target>
<button
<ActionIcon
variant="tertiary"
size="sm"
className="file-sidebar-kebab-btn"
onClick={(e) => e.stopPropagation()}
tabIndex={-1}
type="button"
aria-label={t(
"fileSidebar.fileItem.moreActions",
"More actions",
)}
>
<MoreVertIcon sx={{ fontSize: "1.1rem" }} />
</button>
</ActionIcon>
</Menu.Target>
<Menu.Dropdown onClick={(e) => e.stopPropagation()}>
{hasVersionHistory && onVersionHistory && (
@@ -1,6 +1,32 @@
import { useRef } from "react";
import { FileButton, Button } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { FilePicker } from "@app/ui/FilePicker";
import type { ButtonVariant } from "@app/ui/Button";
// Accept both shared DS variants and the legacy Mantine variant names that
// existing callers still pass, mapping the latter onto the DS equivalents.
type LegacyVariant =
| "outline"
| "filled"
| "light"
| "default"
| "subtle"
| "gradient";
const VARIANT_MAP: Record<LegacyVariant, ButtonVariant> = {
filled: "primary",
outline: "secondary",
default: "secondary",
light: "tertiary",
subtle: "tertiary",
gradient: "primary",
};
function resolveVariant(variant: ButtonVariant | LegacyVariant): ButtonVariant {
return variant in VARIANT_MAP
? VARIANT_MAP[variant as LegacyVariant]
: (variant as ButtonVariant);
}
interface FileUploadButtonProps {
file?: File;
@@ -8,7 +34,7 @@ interface FileUploadButtonProps {
accept?: string;
disabled?: boolean;
placeholder?: string;
variant?: "outline" | "filled" | "light" | "default" | "subtle" | "gradient";
variant?: ButtonVariant | LegacyVariant;
fullWidth?: boolean;
}
@@ -18,7 +44,7 @@ const FileUploadButton = ({
accept,
disabled = false,
placeholder,
variant = "outline",
variant = "secondary",
fullWidth = true,
}: FileUploadButtonProps) => {
const { t } = useTranslation();
@@ -27,18 +53,16 @@ const FileUploadButton = ({
const defaultPlaceholder = t("chooseFile", "Choose File");
return (
<FileButton
<FilePicker
resetRef={resetRef}
onChange={onChange}
accept={accept}
disabled={disabled}
variant={resolveVariant(variant)}
fullWidth={fullWidth}
>
{(props) => (
<Button {...props} variant={variant} fullWidth={fullWidth} color="blue">
{file ? file.name : placeholder || defaultPlaceholder}
</Button>
)}
</FileButton>
{file ? file.name : placeholder || defaultPlaceholder}
</FilePicker>
);
};
@@ -1,12 +1,6 @@
import { useState } from "react";
import {
Modal,
Stack,
Text,
PasswordInput,
Button,
Alert,
} from "@mantine/core";
import { Modal, Stack, Text, PasswordInput, Alert } from "@mantine/core";
import { Button } from "@app/ui/Button";
import { useTranslation } from "react-i18next";
import LocalIcon from "@app/components/shared/LocalIcon";
import { accountService } from "@app/services/accountService";
@@ -195,8 +189,8 @@ export default function FirstLoginModal({
/>
<Button
type="submit"
fullWidth
type="submit"
loading={loading}
disabled={
!currentPassword ||
@@ -205,7 +199,7 @@ export default function FirstLoginModal({
newPassword.length < 8 ||
confirmPassword.length < 8
}
mt="md"
style={{ marginTop: "var(--mantine-spacing-md)" }}
>
{t("firstLogin.changePassword", "Change Password")}
</Button>
@@ -2,6 +2,7 @@ import { Flex } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { useCookieConsent } from "@app/hooks/useCookieConsent";
import { useFooterInfo } from "@app/hooks/useFooterInfo";
import { Button } from "@app/ui/Button";
interface FooterProps {
privacyPolicy?: string;
@@ -53,8 +54,8 @@ export default function Footer({
<div
style={{
height: "var(--footer-height)",
backgroundColor: "var(--mantine-color-gray-1)",
borderTop: "1px solid var(--mantine-color-gray-2)",
backgroundColor: "var(--bg-surface)",
borderTop: "1px solid var(--border-subtle)",
display: "flex",
flexDirection: "column",
justifyContent: "center",
@@ -132,13 +133,14 @@ export default function Footer({
</a>
)}
{finalAnalyticsEnabled && (
<button
<Button
variant="tertiary"
className="footer-link px-3"
id="cookieBanner"
onClick={showCookiePreferences}
>
{t("legal.showCookieBanner", "Cookie Preferences")}
</button>
</Button>
)}
</Flex>
</div>
@@ -1,5 +1,6 @@
import React from "react";
import { ActionIcon, Tooltip } from "@mantine/core";
import { Tooltip } from "@mantine/core";
import { ActionIcon } from "@app/ui/ActionIcon";
import styles from "@app/components/shared/HoverActionMenu.module.css";
import { Z_INDEX_HOVER_ACTION_MENU } from "@app/styles/zIndex";
@@ -60,10 +61,9 @@ const HoverActionMenu: React.FC<HoverActionMenuProps> = ({
<Tooltip key={action.id} label={action.label}>
<ActionIcon
size="md"
variant="subtle"
variant="tertiary"
disabled={action.disabled}
onClick={action.onClick}
c={action.color}
aria-label={action.label}
style={{ color: action.color || "var(--text-secondary)" }}
data-tour={action.dataTour}
@@ -1,5 +1,7 @@
import React, { ReactNode } from "react";
import { Paper, Group, Text, Button, ActionIcon, Stack } from "@mantine/core";
import { Paper, Group, Text, Stack } from "@mantine/core";
import { Button, type ButtonVariant, type ButtonAccent } from "@app/ui/Button";
import { ActionIcon } from "@app/ui/ActionIcon";
import { useTranslation } from "react-i18next";
import LocalIcon from "@app/components/shared/LocalIcon";
@@ -31,6 +33,40 @@ const toneStyles: Record<
},
};
function toSharedButtonVariant(
variant: "light" | "filled" | "white" | "outline" | "subtle",
): ButtonVariant {
switch (variant) {
case "filled":
return "primary";
case "outline":
return "secondary";
case "subtle":
return "tertiary";
case "light":
case "white":
default:
return "secondary";
}
}
function toSharedButtonAccent(color: string | undefined): ButtonAccent {
// Mantine colours may carry a shade suffix (e.g. "orange.7"); use the hue.
const hue = (color ?? "").split(".")[0];
switch (hue) {
case "red":
return "danger";
case "green":
return "success";
case "yellow":
case "orange":
return "warning";
case "blue":
default:
return "default";
}
}
interface InfoBannerProps {
/**
* Either a LocalIcon name (string) for the standard sized icon slot, or a
@@ -98,7 +134,6 @@ export const InfoBanner: React.FC<InfoBannerProps> = ({
const iconSize = compact ? "1rem" : "1.2rem";
const textSize = compact ? "xs" : "sm";
const buttonSize = compact ? "xs" : "xs";
return (
<Paper
@@ -167,11 +202,13 @@ export const InfoBanner: React.FC<InfoBannerProps> = ({
<Group gap="xs" align="center" wrap="nowrap">
{buttonText && onButtonClick && (
<Button
variant={buttonVariant}
color={buttonColor ?? toneStyle.buttonColor}
size={buttonSize}
onClick={onButtonClick}
variant={toSharedButtonVariant(buttonVariant)}
accent={toSharedButtonAccent(
buttonColor ?? toneStyle.buttonColor,
)}
size="sm"
loading={loading}
onClick={onButtonClick}
leftSection={
<LocalIcon
icon={buttonIcon}
@@ -179,19 +216,15 @@ export const InfoBanner: React.FC<InfoBannerProps> = ({
height={compact ? "0.75rem" : "0.9rem"}
/>
}
styles={
buttonTextColor
? { label: { color: buttonTextColor } }
: undefined
}
style={buttonTextColor ? { color: buttonTextColor } : undefined}
>
{buttonText}
</Button>
)}
{dismissible && (
<ActionIcon
variant="subtle"
color={closeIconColor ? undefined : "gray"}
variant="tertiary"
accent="neutral"
size="sm"
onClick={handleDismiss}
aria-label={t("infoBanner.dismiss", "Dismiss")}
@@ -1,5 +1,7 @@
import React from "react";
import { Button, Group, Tooltip, ActionIcon } from "@mantine/core";
import { Group, Tooltip } from "@mantine/core";
import { Button } from "@app/ui/Button";
import { ActionIcon } from "@app/ui/ActionIcon";
import LocalIcon from "@app/components/shared/LocalIcon";
import { useFilesModalContext } from "@app/contexts/FilesModalContext";
import { useFileActionTerminology } from "@app/hooks/useFileActionTerminology";
@@ -30,7 +32,7 @@ export function LandingActions({
<>
<Group gap="sm" justify="center" wrap="wrap" mb="xs">
<Button
classNames={{ root: "landing-btn-primary" }}
className="landing-btn-primary"
leftSection={
<LocalIcon
icon={icons.uploadIconName}
@@ -48,8 +50,8 @@ export function LandingActions({
</Button>
<Button
variant="default"
classNames={{ root: "landing-btn-secondary" }}
variant="secondary"
className="landing-btn-secondary"
leftSection={
<LocalIcon
icon="add"
@@ -70,10 +72,9 @@ export function LandingActions({
<Tooltip label={terminology.mobileUpload} position="bottom">
<ActionIcon
size="lg"
variant="default"
radius="md"
variant="secondary"
aria-label={terminology.mobileUpload}
classNames={{ root: "landing-btn-secondary landing-btn-icon" }}
className="landing-btn-secondary landing-btn-icon"
onClick={(e) => {
e.stopPropagation();
onMobileUploadClick();
@@ -5,11 +5,12 @@
/* ── Hero text ───────────────────────────────────────────── */
.landing-title {
display: block;
margin: 1.75rem auto 0.5rem;
height: 3rem;
width: auto;
max-width: 100%;
margin: 1.75rem 0 0.5rem;
text-align: center;
font-size: 2rem;
font-weight: 700;
line-height: 1.25;
color: var(--text-primary);
}
.landing-subtitle {
@@ -8,7 +8,6 @@ import MobileUploadModal from "@app/components/shared/MobileUploadModal";
import { openFilesFromDisk } from "@app/services/openFilesFromDisk";
import { LandingDocumentStack } from "@app/components/shared/LandingDocumentStack";
import { LandingActions } from "@app/components/shared/LandingActions";
import { Wordmark } from "@app/components/shared/Wordmark";
import "@app/components/shared/LandingPage.css";
const LandingPage = () => {
@@ -80,10 +79,9 @@ const LandingPage = () => {
>
<LandingDocumentStack />
<Wordmark
alt={t("landing.heroTitle", "Stirling PDF")}
className="landing-title"
/>
<h3 className="landing-title">
{t("landing.workbenchEmptyStateHero", "Drop a PDF anywhere")}
</h3>
<p className="landing-subtitle">
{t(
"landing.heroSubtitle",
@@ -1,5 +1,7 @@
import React, { useState, useEffect } from "react";
import { Menu, Button, ActionIcon } from "@mantine/core";
import { Menu } from "@mantine/core";
import { Button } from "@app/ui/Button";
import { ActionIcon } from "@app/ui/ActionIcon";
import { Tooltip } from "@app/components/shared/Tooltip";
import { useTranslation } from "react-i18next";
import { supportedLanguages, setUserLanguage } from "@app/i18n";
@@ -73,50 +75,26 @@ const LanguageItem: React.FC<LanguageItemProps> = ({
}}
>
<Button
variant="subtle"
variant="tertiary"
accent="neutral"
size="sm"
fullWidth
onClick={disabled ? undefined : onClick}
data-selected={isSelected}
disabled={disabled}
styles={{
root: {
borderRadius: "4px",
minHeight: "32px",
padding: "4px 8px",
justifyContent: "flex-start",
position: "relative",
overflow: "hidden",
backgroundColor: isSelected
? "light-dark(var(--mantine-color-blue-1), var(--mantine-color-blue-8))"
: "transparent",
color: disabled
? "var(--text-muted)"
: isSelected
? "light-dark(var(--mantine-color-blue-9), var(--mantine-color-blue-3))"
: "var(--text-primary)",
transition: "all 0.12s cubic-bezier(0.25, 0.46, 0.45, 0.94)",
cursor: disabled ? "not-allowed" : "pointer",
"&:hover": !disabled
? {
backgroundColor: isSelected
? "light-dark(var(--mantine-color-blue-2), var(--mantine-color-blue-7))"
: "light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5))",
transform: "translateY(-1px)",
boxShadow: "0 2px 8px rgba(0, 0, 0, 0.1)",
}
: {},
},
label: {
fontSize: "13px",
fontWeight: isSelected ? 600 : 400,
textAlign: "left",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
position: "relative",
zIndex: 2,
},
style={{
justifyContent: "flex-start",
position: "relative",
overflow: "hidden",
fontWeight: isSelected ? 600 : 400,
backgroundColor: isSelected
? "light-dark(var(--mantine-color-blue-1), var(--mantine-color-blue-8))"
: undefined,
color: disabled
? "var(--text-muted)"
: isSelected
? "light-dark(var(--mantine-color-blue-9), var(--mantine-color-blue-3))"
: "var(--text-primary)",
}}
>
{label}
@@ -272,43 +250,23 @@ const LanguageSelector: React.FC<LanguageSelectorProps> = ({
<Menu.Target>
{compact ? (
<ActionIcon
variant="subtle"
radius="md"
variant="tertiary"
accent="neutral"
data-testid="language-selector-button"
title={!opened && tooltip ? tooltip : undefined}
styles={{
root: {
color: "var(--text-secondary)",
"&:hover": {
backgroundColor:
"light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5))",
},
},
}}
aria-label={tooltip ?? currentLanguage}
>
<LocalIcon icon="language" width="1.5rem" height="1.5rem" />
</ActionIcon>
) : (
<Button
variant="subtle"
variant="tertiary"
accent="neutral"
size="sm"
data-testid="language-selector-button"
leftSection={
<LocalIcon icon="language" width="1.5rem" height="1.5rem" />
}
styles={{
root: {
border: "none",
color: "var(--text-primary)",
transition:
"background-color 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94)",
"&:hover": {
backgroundColor:
"light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5))",
},
},
label: { fontSize: "12px", fontWeight: 500 },
}}
>
<span className={styles.languageText}>{currentLanguage}</span>
</Button>
@@ -2,7 +2,6 @@ import { useEffect, useRef, useState } from "react";
import { useLocation } from "react-router-dom";
import {
Box,
Button,
Divider,
Group,
Modal,
@@ -10,6 +9,7 @@ import {
Stack,
Text,
} from "@mantine/core";
import { Button } from "@app/ui/Button";
import { useTranslation } from "react-i18next";
import Markdown, { type Components } from "react-markdown";
import remarkGfm from "remark-gfm";
@@ -188,10 +188,10 @@ export default function LoginAgreementModal() {
)}
</Text>
<Group gap="sm" wrap="nowrap">
<Button variant="default" onClick={handleDecline}>
<Button variant="secondary" onClick={handleDecline}>
{t("loginAgreementDecline", "Decline")}
</Button>
<Button onClick={handleAccept}>
<Button variant="primary" onClick={handleAccept}>
{t("loginAgreementAccept", "Accept")}
</Button>
</Group>
@@ -1,4 +1,5 @@
import { Box, Group, Text, Button } from "@mantine/core";
import { Box, Group, Text } from "@mantine/core";
import { Button } from "@app/ui/Button";
import { useTranslation } from "react-i18next";
interface MultiSelectControlsProps {
@@ -36,20 +37,20 @@ const MultiSelectControls = ({
{selectedCount} {t("fileManager.filesSelected", "files")}
</Text>
<Group>
<Button size="xs" variant="light" onClick={onClearSelection}>
<Button variant="secondary" size="sm" onClick={onClearSelection}>
{t("fileManager.clearSelection", "Clear files")}
</Button>
{onAddToUpload && (
<Button size="xs" color="green" onClick={onAddToUpload}>
<Button accent="success" size="sm" onClick={onAddToUpload}>
{t("fileManager.addToUpload", "Add to Upload")}
</Button>
)}
{onOpenInFileEditor && (
<Button
size="xs"
color="orange"
accent="warning"
size="sm"
onClick={onOpenInFileEditor}
disabled={selectedCount === 0}
>
@@ -59,8 +60,7 @@ const MultiSelectControls = ({
{onOpenInPageEditor && (
<Button
size="xs"
color="blue"
size="sm"
onClick={onOpenInPageEditor}
disabled={selectedCount === 0}
>
@@ -69,7 +69,7 @@ const MultiSelectControls = ({
)}
{onDeleteAll && (
<Button size="xs" color="red" onClick={onDeleteAll}>
<Button accent="danger" size="sm" onClick={onDeleteAll}>
{t("fileManager.deleteAll", "Delete All")}
</Button>
)}
@@ -1,5 +1,6 @@
import { useRef, useEffect } from "react";
import { Modal, Text, Button, Group, Stack } from "@mantine/core";
import { Modal, Text, Group, Stack } from "@mantine/core";
import { Button } from "@app/ui/Button";
import { useNavigationGuard } from "@app/contexts/NavigationContext";
import { useTranslation } from "react-i18next";
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
@@ -103,10 +104,12 @@ const NavigationWarningModal = () => {
<Group justify="space-between" gap="xl" visibleFrom="md">
<Group gap="sm">
<Button
variant="light"
color="var(--mantine-color-gray-8)"
variant="secondary"
accent="neutral"
onClick={handleKeepWorking}
w={BUTTON_WIDTH}
style={{
width: BUTTON_WIDTH,
}}
leftSection={<ArrowBackIcon fontSize="small" />}
>
{t("keepWorking", "Keep Working")}
@@ -114,19 +117,19 @@ const NavigationWarningModal = () => {
</Group>
<Group gap="sm">
<Button
variant="filled"
color="var(--mantine-color-red-9)"
accent="danger"
onClick={handleDiscardChanges}
w={BUTTON_WIDTH}
style={{
width: BUTTON_WIDTH,
}}
leftSection={<DeleteOutlineIcon fontSize="small" />}
>
{t("discardChanges", "Discard Changes")}
</Button>
{hasApply && (
<Button
variant="filled"
onClick={handleApplyAndContinue}
w={BUTTON_WIDTH}
style={{ width: BUTTON_WIDTH }}
leftSection={<CheckCircleOutlineIcon fontSize="small" />}
>
{t("applyAndContinue", "Apply & Leave")}
@@ -134,9 +137,8 @@ const NavigationWarningModal = () => {
)}
{hasExport && (
<Button
variant="filled"
onClick={handleExportAndContinue}
w={BUTTON_WIDTH}
style={{ width: BUTTON_WIDTH }}
leftSection={<CheckCircleOutlineIcon fontSize="small" />}
>
{t("exportAndContinue", "Export & Leave")}
@@ -148,28 +150,28 @@ const NavigationWarningModal = () => {
{/* Mobile layout: centered stack of 4 buttons */}
<Stack align="center" gap="sm" hiddenFrom="md">
<Button
variant="light"
color="var(--mantine-color-gray-8)"
variant="secondary"
accent="neutral"
onClick={handleKeepWorking}
w={BUTTON_WIDTH}
style={{ width: BUTTON_WIDTH }}
leftSection={<ArrowBackIcon fontSize="small" />}
>
{t("keepWorking", "Keep Working")}
</Button>
<Button
variant="filled"
color="var(--mantine-color-red-9)"
accent="danger"
onClick={handleDiscardChanges}
w={BUTTON_WIDTH}
style={{
width: BUTTON_WIDTH,
}}
leftSection={<DeleteOutlineIcon fontSize="small" />}
>
{t("discardChanges", "Discard Changes")}
</Button>
{hasApply && (
<Button
variant="filled"
onClick={handleApplyAndContinue}
w={BUTTON_WIDTH}
style={{ width: BUTTON_WIDTH }}
leftSection={<CheckCircleOutlineIcon fontSize="small" />}
>
{t("applyAndContinue", "Apply & Leave")}
@@ -177,9 +179,8 @@ const NavigationWarningModal = () => {
)}
{hasExport && (
<Button
variant="filled"
onClick={handleExportAndContinue}
w={BUTTON_WIDTH}
style={{ width: BUTTON_WIDTH }}
leftSection={<CheckCircleOutlineIcon fontSize="small" />}
>
{t("exportAndContinue", "Export & Leave")}
@@ -1,4 +1,5 @@
import React from "react";
import { Button } from "@app/ui/Button";
import styles from "@app/components/shared/ObscuredOverlay/ObscuredOverlay.module.css";
type ObscuredOverlayProps = {
@@ -34,13 +35,13 @@ export default function ObscuredOverlay({
<div className={styles.overlayMessage}>{overlayMessage}</div>
)}
{buttonText && onButtonClick && (
<button
type="button"
<Button
variant="tertiary"
onClick={onButtonClick}
className={styles.overlayButton}
>
{buttonText}
</button>
</Button>
)}
</div>
</div>
@@ -3,13 +3,13 @@ import {
Modal,
Stack,
Text,
Button,
Group,
Alert,
TextInput,
Paper,
Select,
} from "@mantine/core";
import { Button } from "@app/ui/Button";
import LinkIcon from "@mui/icons-material/Link";
import ContentCopyRoundedIcon from "@mui/icons-material/ContentCopyRounded";
import { useTranslation } from "react-i18next";
@@ -253,8 +253,8 @@ const ShareFileModal: React.FC<ShareFileModalProps> = ({
label={t("storageShare.linkLabel", "Share link")}
rightSection={
<Button
variant="subtle"
size="xs"
variant="tertiary"
size="sm"
leftSection={
<ContentCopyRoundedIcon style={{ fontSize: 16 }} />
}
@@ -307,7 +307,7 @@ const ShareFileModal: React.FC<ShareFileModalProps> = ({
</Paper>
<Group justify="flex-end" gap="sm">
<Button variant="default" onClick={onClose} disabled={isWorking}>
<Button variant="secondary" onClick={onClose} disabled={isWorking}>
{t("cancel", "Cancel")}
</Button>
<Button
@@ -3,7 +3,6 @@ import {
Modal,
Stack,
Text,
Button,
Group,
Alert,
TextInput,
@@ -13,6 +12,7 @@ import {
ScrollArea,
Select,
} from "@mantine/core";
import { Button } from "@app/ui/Button";
import ContentCopyRoundedIcon from "@mui/icons-material/ContentCopyRounded";
import DeleteIcon from "@mui/icons-material/Delete";
import HistoryIcon from "@mui/icons-material/History";
@@ -648,7 +648,7 @@ const ShareManagementModal: React.FC<ShareManagementModalProps> = ({
</Text>
<Group justify="flex-end" gap="sm">
<Button
variant="default"
variant="secondary"
onClick={() => setShowEmailWarning(false)}
disabled={isLoading}
>
@@ -731,9 +731,8 @@ const ShareManagementModal: React.FC<ShareManagementModalProps> = ({
{confirmRemoveUser === user.username ? (
<Group gap="xs">
<Button
variant="filled"
size="xs"
color="red"
accent="danger"
size="sm"
onClick={() => {
void handleRemoveUser(user.username);
}}
@@ -742,8 +741,8 @@ const ShareManagementModal: React.FC<ShareManagementModalProps> = ({
{t("confirm", "Confirm")}
</Button>
<Button
variant="default"
size="xs"
variant="secondary"
size="sm"
onClick={() => setConfirmRemoveUser(null)}
>
{t("cancel", "Cancel")}
@@ -751,9 +750,9 @@ const ShareManagementModal: React.FC<ShareManagementModalProps> = ({
</Group>
) : (
<Button
variant="light"
size="xs"
color="red"
variant="secondary"
accent="danger"
size="sm"
leftSection={
<DeleteIcon style={{ fontSize: 16 }} />
}
@@ -813,8 +812,8 @@ const ShareManagementModal: React.FC<ShareManagementModalProps> = ({
label={t("storageShare.linkLabel", "Share link")}
rightSection={
<Button
variant="subtle"
size="xs"
variant="tertiary"
size="sm"
leftSection={
<ContentCopyRoundedIcon
style={{ fontSize: 16 }}
@@ -874,8 +873,8 @@ const ShareManagementModal: React.FC<ShareManagementModalProps> = ({
</Stack>
<Group gap="xs">
<Button
variant={isSelected ? "filled" : "light"}
size="xs"
variant={isSelected ? "secondary" : "primary"}
size="sm"
leftSection={
<HistoryIcon style={{ fontSize: 16 }} />
}
@@ -898,9 +897,8 @@ const ShareManagementModal: React.FC<ShareManagementModalProps> = ({
{confirmRevokeToken === link.token ? (
<Group gap="xs">
<Button
variant="filled"
size="xs"
color="red"
accent="danger"
size="sm"
onClick={() => {
void handleRevokeLink(link.token);
}}
@@ -909,8 +907,8 @@ const ShareManagementModal: React.FC<ShareManagementModalProps> = ({
{t("confirm", "Confirm")}
</Button>
<Button
variant="default"
size="xs"
variant="secondary"
size="sm"
onClick={() => setConfirmRevokeToken(null)}
>
{t("cancel", "Cancel")}
@@ -918,9 +916,9 @@ const ShareManagementModal: React.FC<ShareManagementModalProps> = ({
</Group>
) : (
<Button
variant="light"
size="xs"
color="red"
variant="secondary"
accent="danger"
size="sm"
leftSection={
<DeleteIcon style={{ fontSize: 16 }} />
}
@@ -1,5 +1,6 @@
import React, { forwardRef } from "react";
import { useTranslation } from "react-i18next";
import { ActionIcon } from "@app/ui/ActionIcon";
import LocalIcon from "@app/components/shared/LocalIcon";
import styles from "@app/components/shared/textInput/TextInput.module.css";
@@ -110,14 +111,14 @@ export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(
{...props}
/>
{shouldShowClearButton && (
<button
type="button"
<ActionIcon
variant="tertiary"
className={styles.clearButton}
onClick={handleClear}
aria-label={t("textInput.clear", "Clear input")}
>
<LocalIcon icon="close-rounded" width="1.25rem" height="1.25rem" />
</button>
</ActionIcon>
)}
</div>
);
@@ -1,5 +1,5 @@
import type { ReactNode } from "react";
import { ActionIcon } from "@mantine/core";
import { ActionIcon } from "@app/ui/ActionIcon";
import CloseIcon from "@mui/icons-material/Close";
import "@app/components/shared/ToolPanelHeader.css";
@@ -38,12 +38,12 @@ export function ToolPanelHeader({
{onClose && (
<ActionIcon
className="sui-panelhdr__close"
variant="subtle"
color="gray"
radius="xl"
variant="tertiary"
accent="neutral"
shape="circle"
size="md"
onClick={onClose}
aria-label={closeLabel}
aria-label={closeLabel ?? ""}
>
<CloseIcon sx={{ fontSize: 18 }} />
</ActionIcon>
@@ -7,6 +7,7 @@ import React, {
} from "react";
import { createPortal } from "react-dom";
import { useTranslation } from "react-i18next";
import { ActionIcon } from "@app/ui/ActionIcon";
import LocalIcon from "@app/components/shared/LocalIcon";
import { addEventListenerWithCleanup } from "@app/utils/genericUtils";
import { useTooltipPosition } from "@app/hooks/useTooltipPosition";
@@ -396,7 +397,8 @@ export const Tooltip: React.FC<TooltipProps> = ({
}
>
{shouldShowCloseButton && (
<button
<ActionIcon
variant="tertiary"
className={styles["tooltip-pin-button"]}
onClick={(e) => {
e.stopPropagation();
@@ -407,7 +409,7 @@ export const Tooltip: React.FC<TooltipProps> = ({
aria-label={t("tooltip.close", "Close tooltip")}
>
<LocalIcon icon="close-rounded" width="1.25rem" height="1.25rem" />
</button>
</ActionIcon>
)}
{arrow && !sidebarTooltip && (
<div
@@ -1,5 +1,6 @@
import React, { useState, useCallback, useMemo } from "react";
import { SegmentedControl, Loader } from "@mantine/core";
import { Loader } from "@mantine/core";
import { SegmentedControl } from "@app/ui/SegmentedControl";
import InsertDriveFileIcon from "@mui/icons-material/InsertDriveFile";
import GridViewIcon from "@mui/icons-material/GridView";
import FolderIcon from "@mui/icons-material/Folder";
@@ -205,46 +206,16 @@ const TopControls = ({
<div className="flex justify-center">
<SegmentedControl
data-tour="view-switcher"
data={viewOptions}
options={viewOptions}
value={currentView}
onChange={handleViewChange}
color="blue"
variant="secondary"
fullWidth
style={{
transition: "all 0.2s ease",
opacity: switchingTo ? 0.8 : 1,
pointerEvents: "auto",
}}
styles={{
root: {
borderRadius: "0 0 16px 16px",
height: "1.8rem",
backgroundColor: "var(--bg-toolbar)",
border: "1px solid var(--border-default)",
borderTop: "none",
boxShadow: "0 2px 8px rgba(0,0,0,0.1)",
outline: "1px solid rgba(0, 0, 0, 0.1)",
outlineOffset: "-1px",
padding: "0 0",
gap: "0",
},
control: {
borderRadius: "0 0 16px 16px",
padding: "0",
border: "none",
},
indicator: {
borderRadius: "0 0 16px 16px",
height: "100%",
top: "0rem",
margin: "0",
border: "none",
},
label: {
paddingTop: "0",
paddingBottom: "0",
},
}}
/>
</div>
</div>

Some files were not shown because too many files have changed in this diff Show More