fix many UI issues (#6569)

# Description of Changes

- Tool action button truncation - fixed by allowing Mantine <Button>
label to wrap (whiteSpace: normal, height: auto) instead of clipping
- Role badge truncation on People page - fixed by dropping the column's
fixed w={100} and letting the badge size to its content
- Settings nav item wraps to 3 lines - fixed by hiding the inline ALPHA
badge by default and revealing it on :hover/:focus-within/.active
- Zoom slider cramped on narrow desktop - fixed by removing the
toolbar's hardcoded minWidth: 30rem and giving the slider flexShrink: 0
+ minWidth: 6rem
- "Swipe left or right" hint on desktop - fixed by adding a useIsTouch()
hook (pointer: coarse) and gating the hint on isMobile && isTouch
- Logout doesn't redirect - fixed by replacing navigate('/login') with
window.location.assign('/login') in a finally block so auth context
fully re-bootstraps
- Viewer top toolbar clips icons on mobile - fixed by switching the
wrapped state to justify-content: flex-start + overflow-x: auto so the
icon strip is momentum-scrollable
- Mobile bottom toolbar overflows - fixed by gating layout on
useIsPhone() and reducing the inline bar to prev / page / next / ⋮ only
- Lost controls when shrinking mobile toolbar - fixed by adding a
Mantine <Menu> behind ⋮ that groups First/Last page, Zoom in/out (with
%), Dual-page, Dark/Sepia filter under Page navigation / Zoom / View
labels
- "Upload from computer" label clipped on hover - fixed by unmounting
the Add Files button entirely while Upload is hovered, so Upload claims
width: 100%
- Settings rows clip controls off-screen - fixed by adding flex: 1,
minWidth: 0 to the inner text-block <div> on 44 rows across 10 -files,
so labels shrink and wrap while controls stay anchored to the right

---
Screenshots 

[report-before-after.html](https://github.com/user-attachments/files/28687621/report-before-after.html)

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have run `task check` to verify linters, typechecks, and tests
pass
- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#7-testing)
for more details.
This commit is contained in:
Anthony Stirling
2026-06-09 13:18:57 +00:00
committed by GitHub
parent 800a411167
commit 347ae9ebbf
21 changed files with 399 additions and 215 deletions
@@ -8643,12 +8643,16 @@ enableDarkFilter = "Enable Dark Filter"
enableSepiaFilter = "Enable Sepia Filter"
firstPage = "First Page"
lastPage = "Last Page"
moreOptions = "More"
nextPage = "Next Page"
onlyPdfSupported = "This file format is not supported for preview."
pageNavigation = "Page navigation"
previousPage = "Previous Page"
resetZoom = "Reset zoom"
singlePageView = "Single Page View"
unknownFile = "Unknown file"
view = "View"
zoom = "Zoom"
zoomIn = "Zoom In"
zoomOut = "Zoom Out"
@@ -104,44 +104,44 @@ const AddFileCard = ({
}}
onMouseLeave={() => setIsUploadHover(false)}
>
<Button
style={{
backgroundColor: "var(--landing-button-bg)",
color: "var(--landing-button-color)",
border: "1px solid var(--landing-button-border)",
borderRadius: "2rem",
height: "38px",
paddingLeft: isUploadHover ? 0 : "1rem",
paddingRight: isUploadHover ? 0 : "1rem",
width: isUploadHover ? "58px" : "calc(100% - 58px - 0.6rem)",
minWidth: isUploadHover ? "58px" : undefined,
display: "flex",
alignItems: "center",
justifyContent: "center",
transition: "width .5s ease, padding .5s ease",
}}
onClick={handleOpenFilesModal}
onMouseEnter={() => setIsUploadHover(false)}
>
<LocalIcon
icon="add"
width="1.5rem"
height="1.5rem"
className="text-[var(--accent-interactive)]"
/>
{!isUploadHover && (
{!isUploadHover && (
<Button
style={{
backgroundColor: "var(--landing-button-bg)",
color: "var(--landing-button-color)",
border: "1px solid var(--landing-button-border)",
borderRadius: "2rem",
height: "38px",
paddingLeft: "1rem",
paddingRight: "1rem",
width: "calc(100% - 58px - 0.6rem)",
display: "flex",
alignItems: "center",
justifyContent: "center",
transition: "width .5s ease, padding .5s ease",
}}
onClick={handleOpenFilesModal}
onMouseEnter={() => setIsUploadHover(false)}
>
<LocalIcon
icon="add"
width="1.5rem"
height="1.5rem"
className="text-[var(--accent-interactive)]"
/>
<span>{t("landing.addFiles", "Add Files")}</span>
)}
</Button>
</Button>
)}
<Button
aria-label={t("addFileCard.upload", "Upload")}
title={terminology.uploadFromComputer}
style={{
backgroundColor: "var(--landing-button-bg)",
color: "var(--landing-button-color)",
border: "1px solid var(--landing-button-border)",
borderRadius: "1rem",
height: "38px",
width: isUploadHover ? "calc(100% - 58px - 0.6rem)" : "58px",
width: isUploadHover ? "100%" : "58px",
minWidth: "58px",
paddingLeft: isUploadHover ? "1rem" : 0,
paddingRight: isUploadHover ? "1rem" : 0,
@@ -149,6 +149,7 @@ const AddFileCard = ({
alignItems: "center",
justifyContent: "center",
transition: "width .5s ease, padding .5s ease",
overflow: "hidden",
}}
onClick={handleNativeUploadClick}
onMouseEnter={() => setIsUploadHover(true)}
@@ -157,10 +158,18 @@ const AddFileCard = ({
icon={icons.uploadIconName}
width="1.25rem"
height="1.25rem"
style={{ color: "var(--accent-interactive)" }}
style={{ color: "var(--accent-interactive)", flexShrink: 0 }}
/>
{isUploadHover && (
<span style={{ marginLeft: ".5rem" }}>
<span
style={{
marginLeft: ".5rem",
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
minWidth: 0,
}}
>
{terminology.uploadFromComputer}
</span>
)}
@@ -106,6 +106,15 @@
margin-bottom: 0.25rem; /* 4px */
}
.modal-nav-item-badge {
display: none;
}
.modal-nav-item:hover .modal-nav-item-badge,
.modal-nav-item:focus-within .modal-nav-item-badge,
.modal-nav-item.active .modal-nav-item-badge {
display: inline-flex;
}
.modal-content {
flex: 1;
display: flex;
@@ -242,7 +242,7 @@ const AppConfigModalInner: React.FC<AppConfigModalProps> = ({
handleNavigation(item.key);
}
}}
className={`modal-nav-item ${isMobile ? "mobile" : ""}`}
className={`modal-nav-item ${isActive ? "active" : ""} ${isMobile ? "mobile" : ""}`}
style={{
background: isActive
? colors.navItemActiveBg
@@ -259,8 +259,19 @@ const AppConfigModalInner: React.FC<AppConfigModalProps> = ({
style={{ color }}
/>
{!isMobile && (
<Group gap={4} align="center" wrap="nowrap">
<Text size="sm" fw={500} style={{ color }}>
<Group
gap={4}
align="center"
wrap="nowrap"
style={{ minWidth: 0, flex: 1 }}
>
<Text
size="sm"
fw={500}
truncate
style={{ color, minWidth: 0, flex: 1 }}
title={item.label}
>
{item.label}
</Text>
{item.badge && (
@@ -268,6 +279,7 @@ const AppConfigModalInner: React.FC<AppConfigModalProps> = ({
size="xs"
variant="light"
color={item.badgeColor ?? "orange"}
className="modal-nav-item-badge"
style={{ flexShrink: 0 }}
>
{item.badge}
@@ -97,9 +97,16 @@
order: 3;
flex: 0 0 100%;
height: auto;
overflow: visible;
padding: 4px 0;
border-top: 1px solid var(--border-subtle);
justify-content: flex-start;
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: thin;
-webkit-overflow-scrolling: touch;
}
.workbench-bar[data-wrapped="true"] .workbench-bar-center > * {
flex-shrink: 0;
}
/* ---- Right: global buttons (theme / language / download) ---- */
@@ -492,7 +492,7 @@ const GeneralSection: React.FC<GeneralSectionProps> = ({
justifyContent: "space-between",
}}
>
<div>
<div style={{ flex: 1, minWidth: 0 }}>
<Text fw={500} size="sm">
{t("settings.general.theme", "Theme")}
</Text>
@@ -527,7 +527,7 @@ const GeneralSection: React.FC<GeneralSectionProps> = ({
justifyContent: "space-between",
}}
>
<div>
<div style={{ flex: 1, minWidth: 0 }}>
<Text fw={500} size="sm">
{t("settings.general.language", "Language")}
</Text>
@@ -552,7 +552,7 @@ const GeneralSection: React.FC<GeneralSectionProps> = ({
justifyContent: "space-between",
}}
>
<div>
<div style={{ flex: 1, minWidth: 0 }}>
<Text fw={500} size="sm">
{t(
"settings.general.defaultToolPickerMode",
@@ -590,7 +590,7 @@ const GeneralSection: React.FC<GeneralSectionProps> = ({
justifyContent: "space-between",
}}
>
<div>
<div style={{ flex: 1, minWidth: 0 }}>
<Text fw={500} size="sm">
{t(
"settings.general.defaultStartupView",
@@ -632,7 +632,7 @@ const GeneralSection: React.FC<GeneralSectionProps> = ({
justifyContent: "space-between",
}}
>
<div>
<div style={{ flex: 1, minWidth: 0 }}>
<Text fw={500} size="sm">
{t("settings.general.defaultViewerZoom", "Default reader zoom")}
</Text>
@@ -687,7 +687,7 @@ const GeneralSection: React.FC<GeneralSectionProps> = ({
justifyContent: "space-between",
}}
>
<div>
<div style={{ flex: 1, minWidth: 0 }}>
<Text fw={500} size="sm">
{t(
"settings.general.hideUnavailableTools",
@@ -718,7 +718,7 @@ const GeneralSection: React.FC<GeneralSectionProps> = ({
justifyContent: "space-between",
}}
>
<div>
<div style={{ flex: 1, minWidth: 0 }}>
<Text fw={500} size="sm">
{t(
"settings.general.hideUnavailableConversions",
@@ -759,7 +759,7 @@ const GeneralSection: React.FC<GeneralSectionProps> = ({
cursor: "help",
}}
>
<div>
<div style={{ flex: 1, minWidth: 0 }}>
<Text fw={500} size="sm">
{t("settings.general.autoUnzip", "Auto-unzip API responses")}
</Text>
@@ -796,7 +796,7 @@ const GeneralSection: React.FC<GeneralSectionProps> = ({
cursor: "help",
}}
>
<div>
<div style={{ flex: 1, minWidth: 0 }}>
<Text fw={500} size="sm">
{t(
"settings.general.autoUnzipFileLimit",
@@ -101,7 +101,7 @@ export default function ProviderCard({
justifyContent: "space-between",
}}
>
<div>
<div style={{ flex: 1, minWidth: 0 }}>
<Text fw={500} size="sm">
{field.label}
</Text>
@@ -91,7 +91,18 @@ const OperationButton = ({
color={color}
data-testid={dataTestId}
data-tour={dataTour}
style={{ minHeight: "2.5rem", position: "relative" }}
style={{ minHeight: "2.5rem", height: "auto", position: "relative" }}
styles={{
label: {
whiteSpace: "normal",
textAlign: "center",
lineHeight: 1.2,
},
inner: {
paddingTop: "0.25rem",
paddingBottom: "0.25rem",
},
}}
>
{isLoading
? loadingText || t("loading", "Loading...")
@@ -4,11 +4,13 @@ import {
Button,
Paper,
Group,
Menu,
NumberInput,
Slider,
} from "@mantine/core";
import { useTranslation } from "react-i18next";
import { useViewer } from "@app/contexts/ViewerContext";
import { useIsPhone } from "@app/hooks/useIsMobile";
import { Tooltip } from "@app/components/shared/Tooltip";
import FirstPageIcon from "@mui/icons-material/FirstPage";
import ArrowBackIosIcon from "@mui/icons-material/ArrowBackIos";
@@ -21,6 +23,7 @@ import WbSunnyIcon from "@mui/icons-material/WbSunny";
import WbTwilightIcon from "@mui/icons-material/WbTwilight";
import ZoomInIcon from "@mui/icons-material/ZoomIn";
import ZoomOutIcon from "@mui/icons-material/ZoomOut";
import MoreVertIcon from "@mui/icons-material/MoreVert";
interface PdfViewerToolbarProps {
// Page navigation props (placeholders for now)
@@ -35,6 +38,9 @@ export function PdfViewerToolbar({
onPageChange,
}: PdfViewerToolbarProps) {
const { t } = useTranslation();
const isPhone = useIsPhone();
const buttonMinWidth = isPhone ? "3rem" : "2.5rem";
const buttonSize = isPhone ? "lg" : "md";
const {
getScrollState,
getZoomState,
@@ -141,41 +147,45 @@ export function PdfViewerToolbar({
style={{
display: "flex",
alignItems: "center",
flexWrap: "wrap",
rowGap: 8,
gap: 10,
justifyContent: "center",
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
boxShadow: "0 -2px 8px rgba(0,0,0,0.04)",
pointerEvents: "auto",
minWidth: "30rem",
}}
>
{/* First Page Button */}
<Button
variant="subtle"
color="blue"
size="md"
px={8}
radius="xl"
onClick={handleFirstPage}
disabled={scrollState.currentPage === 1}
style={{ minWidth: "2.5rem" }}
title={t("viewer.firstPage", "First Page")}
>
<FirstPageIcon fontSize="small" />
</Button>
{!isPhone && (
<Button
variant="subtle"
color="blue"
size={buttonSize}
px={8}
radius="xl"
onClick={handleFirstPage}
disabled={scrollState.currentPage === 1}
style={{ minWidth: buttonMinWidth }}
title={t("viewer.firstPage", "First Page")}
>
<FirstPageIcon fontSize="small" />
</Button>
)}
{/* Previous Page Button */}
<Button
variant="subtle"
color="blue"
size="md"
size={buttonSize}
px={8}
radius="xl"
onClick={handlePreviousPage}
disabled={scrollState.currentPage === 1}
style={{ minWidth: "2.5rem" }}
style={{ minWidth: buttonMinWidth }}
title={t("viewer.previousPage", "Previous Page")}
>
<ArrowBackIosIcon fontSize="small" />
@@ -212,137 +222,242 @@ export function PdfViewerToolbar({
<Button
variant="subtle"
color="blue"
size="md"
size={buttonSize}
px={8}
radius="xl"
onClick={handleNextPage}
disabled={scrollState.currentPage === scrollState.totalPages}
style={{ minWidth: "2.5rem" }}
style={{ minWidth: buttonMinWidth }}
title={t("viewer.nextPage", "Next Page")}
>
<ArrowForwardIosIcon fontSize="small" />
</Button>
{/* Last Page Button */}
<Button
variant="subtle"
color="blue"
size="md"
px={8}
radius="xl"
onClick={handleLastPage}
disabled={scrollState.currentPage === scrollState.totalPages}
style={{ minWidth: "2.5rem" }}
title={t("viewer.lastPage", "Last Page")}
>
<LastPageIcon fontSize="small" />
</Button>
{!isPhone && (
<Button
variant="subtle"
color="blue"
size={buttonSize}
px={8}
radius="xl"
onClick={handleLastPage}
disabled={scrollState.currentPage === scrollState.totalPages}
style={{ minWidth: buttonMinWidth }}
title={t("viewer.lastPage", "Last Page")}
>
<LastPageIcon fontSize="small" />
</Button>
)}
{/* Dual Page Toggle */}
<Tooltip
content={
isDualPageActive
? t("viewer.singlePageView", "Single Page View")
: t("viewer.dualPageView", "Dual Page View")
}
position="top"
arrow
>
<Button
variant={isDualPageActive ? "filled" : "light"}
color="blue"
size="md"
radius="xl"
onClick={handleDualPageToggle}
disabled={scrollState.totalPages <= 1}
style={{ minWidth: "2.5rem" }}
{!isPhone && (
<Tooltip
content={
isDualPageActive
? t("viewer.singlePageView", "Single Page View")
: t("viewer.dualPageView", "Dual Page View")
}
position="top"
arrow
>
{isDualPageActive ? (
<DescriptionIcon fontSize="small" />
) : (
<ViewWeekIcon fontSize="small" />
)}
</Button>
</Tooltip>
<Button
variant={isDualPageActive ? "filled" : "light"}
color="blue"
size={buttonSize}
radius="xl"
onClick={handleDualPageToggle}
disabled={scrollState.totalPages <= 1}
style={{ minWidth: buttonMinWidth }}
>
{isDualPageActive ? (
<DescriptionIcon fontSize="small" />
) : (
<ViewWeekIcon fontSize="small" />
)}
</Button>
</Tooltip>
)}
{/* PDF Render Mode Toggle */}
<Tooltip
content={
pdfRenderMode === "normal"
? t("viewer.enableDarkFilter", "Enable Dark Filter")
: pdfRenderMode === "dark"
? t("viewer.enableSepiaFilter", "Enable Sepia Filter")
: t("viewer.disableColorFilter", "Disable Color Filter")
}
position="top"
arrow
>
<Button
variant={pdfRenderMode !== "normal" ? "filled" : "light"}
color="blue"
size="md"
radius="xl"
onClick={cyclePdfRenderMode}
style={{ minWidth: "2.5rem" }}
aria-label={
{!isPhone && (
<Tooltip
content={
pdfRenderMode === "normal"
? t("viewer.enableDarkFilter", "Enable Dark Filter")
: pdfRenderMode === "dark"
? t("viewer.enableSepiaFilter", "Enable Sepia Filter")
: t("viewer.disableColorFilter", "Disable Color Filter")
}
position="top"
arrow
>
{pdfRenderMode === "normal" && <DarkModeIcon fontSize="small" />}
{pdfRenderMode === "dark" && <WbTwilightIcon fontSize="small" />}
{pdfRenderMode === "sepia" && <WbSunnyIcon fontSize="small" />}
</Button>
</Tooltip>
<Button
variant={pdfRenderMode !== "normal" ? "filled" : "light"}
color="blue"
size={buttonSize}
radius="xl"
onClick={cyclePdfRenderMode}
style={{ minWidth: buttonMinWidth }}
aria-label={
pdfRenderMode === "normal"
? t("viewer.enableDarkFilter", "Enable Dark Filter")
: pdfRenderMode === "dark"
? t("viewer.enableSepiaFilter", "Enable Sepia Filter")
: t("viewer.disableColorFilter", "Disable Color Filter")
}
>
{pdfRenderMode === "normal" && <DarkModeIcon fontSize="small" />}
{pdfRenderMode === "dark" && <WbTwilightIcon fontSize="small" />}
{pdfRenderMode === "sepia" && <WbSunnyIcon fontSize="small" />}
</Button>
</Tooltip>
)}
{/* Zoom Controls */}
<Group gap={4} align="center" style={{ marginLeft: 16 }}>
<ActionIcon
variant="subtle"
color="blue"
radius="md"
onClick={handleZoomOut}
aria-label={t("viewer.zoomOut", "Zoom out")}
{/* Desktop zoom controls (slider + buttons) */}
{!isPhone && (
<Group
gap={4}
align="center"
wrap="nowrap"
style={{ marginLeft: 16, flexShrink: 0 }}
>
<ZoomOutIcon fontSize="small" />
</ActionIcon>
<Slider
value={Math.min(Math.max(displayZoomPercent, 20), 500)}
min={20}
max={500}
step={5}
onChange={(val) => zoomActions.setZoomLevel?.(val / 100)}
size="xs"
styles={{
root: { width: "6rem" },
thumb: { width: 14, height: 14 },
track: { height: 3 },
}}
label={null}
/>
<ActionIcon
variant="subtle"
color="blue"
radius="md"
onClick={handleZoomIn}
aria-label={t("viewer.zoomIn", "Zoom in")}
<ActionIcon
variant="subtle"
color="blue"
radius="md"
onClick={handleZoomOut}
aria-label={t("viewer.zoomOut", "Zoom out")}
>
<ZoomOutIcon fontSize="small" />
</ActionIcon>
<Slider
value={Math.min(Math.max(displayZoomPercent, 20), 500)}
min={20}
max={500}
step={5}
onChange={(val) => zoomActions.setZoomLevel?.(val / 100)}
size="xs"
styles={{
root: { minWidth: "6rem", width: "6rem", flexShrink: 0 },
thumb: { width: 14, height: 14 },
track: { height: 3 },
}}
label={null}
/>
<ActionIcon
variant="subtle"
color="blue"
radius="md"
onClick={handleZoomIn}
aria-label={t("viewer.zoomIn", "Zoom in")}
>
<ZoomInIcon fontSize="small" />
</ActionIcon>
<span
style={{
minWidth: "2.5rem",
textAlign: "center",
fontSize: 12,
color: "var(--text-muted)",
}}
>
{displayZoomPercent}%
</span>
</Group>
)}
{isPhone && (
<Menu
shadow="md"
width={240}
position="top-end"
closeOnItemClick={false}
>
<ZoomInIcon fontSize="small" />
</ActionIcon>
<span
style={{
minWidth: "2.5rem",
textAlign: "center",
fontSize: 12,
color: "var(--text-muted)",
}}
>
{displayZoomPercent}%
</span>
</Group>
<Menu.Target>
<ActionIcon
variant="light"
color="blue"
radius="md"
size="lg"
aria-label={t("viewer.moreOptions", "More")}
style={{ marginLeft: 4 }}
>
<MoreVertIcon fontSize="small" />
</ActionIcon>
</Menu.Target>
<Menu.Dropdown>
<Menu.Label>
{t("viewer.pageNavigation", "Page navigation")}
</Menu.Label>
<Menu.Item
leftSection={<FirstPageIcon fontSize="small" />}
disabled={scrollState.currentPage === 1}
onClick={handleFirstPage}
>
{t("viewer.firstPage", "First page")}
</Menu.Item>
<Menu.Item
leftSection={<LastPageIcon fontSize="small" />}
disabled={scrollState.currentPage === scrollState.totalPages}
onClick={handleLastPage}
>
{t("viewer.lastPage", "Last page")}
</Menu.Item>
<Menu.Divider />
<Menu.Label>{t("viewer.zoom", "Zoom")}</Menu.Label>
<Menu.Item
leftSection={<ZoomOutIcon fontSize="small" />}
onClick={handleZoomOut}
>
{t("viewer.zoomOut", "Zoom out")}
</Menu.Item>
<Menu.Item
leftSection={<ZoomInIcon fontSize="small" />}
onClick={handleZoomIn}
>
{t("viewer.zoomIn", "Zoom in")} ({displayZoomPercent}%)
</Menu.Item>
<Menu.Divider />
<Menu.Label>{t("viewer.view", "View")}</Menu.Label>
<Menu.Item
leftSection={
isDualPageActive ? (
<DescriptionIcon fontSize="small" />
) : (
<ViewWeekIcon fontSize="small" />
)
}
disabled={scrollState.totalPages <= 1}
onClick={handleDualPageToggle}
>
{isDualPageActive
? t("viewer.singlePageView", "Single Page View")
: t("viewer.dualPageView", "Dual Page View")}
</Menu.Item>
<Menu.Item
leftSection={
pdfRenderMode === "normal" ? (
<DarkModeIcon fontSize="small" />
) : pdfRenderMode === "dark" ? (
<WbTwilightIcon fontSize="small" />
) : (
<WbSunnyIcon fontSize="small" />
)
}
onClick={cyclePdfRenderMode}
>
{pdfRenderMode === "normal"
? t("viewer.enableDarkFilter", "Enable Dark Filter")
: pdfRenderMode === "dark"
? t("viewer.enableSepiaFilter", "Enable Sepia Filter")
: t("viewer.disableColorFilter", "Disable Color Filter")}
</Menu.Item>
</Menu.Dropdown>
</Menu>
)}
</Paper>
);
}
@@ -15,3 +15,11 @@ export const useIsMobile = (): boolean => {
export const useIsPhone = (): boolean => {
return useMediaQuery("(max-width: 768px)") ?? false;
};
/**
* Custom hook to detect a coarse pointer (touch device)
* Use to gate touch-only UI hints when combined with useIsMobile
*/
export const useIsTouch = (): boolean => {
return useMediaQuery("(pointer: coarse)") ?? false;
};
+10 -7
View File
@@ -5,7 +5,7 @@ import { Group } from "@mantine/core";
import { useSidebarContext } from "@app/contexts/SidebarContext";
import { useDocumentMeta } from "@app/hooks/useDocumentMeta";
import { useBaseUrl } from "@app/hooks/useBaseUrl";
import { useIsMobile } from "@app/hooks/useIsMobile";
import { useIsMobile, useIsTouch } from "@app/hooks/useIsMobile";
import { useAppConfig } from "@app/contexts/AppConfigContext";
import { LogoIcon } from "@app/components/shared/LogoIcon";
import { Wordmark } from "@app/components/shared/Wordmark";
@@ -84,6 +84,7 @@ export default function HomePage() {
const navigate = useNavigate();
const { config } = useAppConfig();
const isMobile = useIsMobile();
const isTouch = useIsTouch();
const sliderRef = useRef<HTMLDivElement | null>(null);
const [activeMobileView, setActiveMobileView] = useState<MobileView>("tools");
const isProgrammaticScroll = useRef(false);
@@ -352,12 +353,14 @@ export default function HomePage() {
{t("home.mobile.workspace", "Workspace")}
</button>
</div>
<span className="mobile-toggle-hint">
{t(
"home.mobile.swipeHint",
"Swipe left or right to switch views",
)}
</span>
{isTouch && (
<span className="mobile-toggle-hint">
{t(
"home.mobile.swipeHint",
"Swipe left or right to switch views",
)}
</span>
)}
</div>
)}
{navigationState.workbench === "myFiles" ? (
@@ -1,19 +1,18 @@
import { Text, Button } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { useAuth } from "@app/auth/UseSession";
import { useNavigate } from "react-router-dom";
export function OverviewHeader() {
const { t } = useTranslation();
const { signOut, user } = useAuth();
const navigate = useNavigate();
const handleLogout = async () => {
try {
await signOut();
navigate("/login");
} catch (error) {
console.error("Logout error:", error);
} finally {
window.location.assign("/login");
}
};
@@ -503,7 +503,7 @@ export default function AdminAdvancedSection() {
justifyContent: "space-between",
}}
>
<div>
<div style={{ flex: 1, minWidth: 0 }}>
<Text fw={500} size="sm">
{t(
"admin.settings.advanced.enableAlphaFunctionality.label",
@@ -543,7 +543,7 @@ export default function AdminAdvancedSection() {
justifyContent: "space-between",
}}
>
<div>
<div style={{ flex: 1, minWidth: 0 }}>
<Text fw={500} size="sm">
{t(
"admin.settings.advanced.enableUrlToPDF.label",
@@ -581,7 +581,7 @@ export default function AdminAdvancedSection() {
justifyContent: "space-between",
}}
>
<div>
<div style={{ flex: 1, minWidth: 0 }}>
<Text fw={500} size="sm">
{t(
"admin.settings.advanced.disableSanitize.label",
@@ -959,7 +959,7 @@ export default function AdminAdvancedSection() {
justifyContent: "space-between",
}}
>
<div>
<div style={{ flex: 1, minWidth: 0 }}>
<Text fw={500} size="sm">
{t(
"admin.settings.advanced.tempFileManagement.startupCleanup.label",
@@ -1002,7 +1002,7 @@ export default function AdminAdvancedSection() {
justifyContent: "space-between",
}}
>
<div>
<div style={{ flex: 1, minWidth: 0 }}>
<Text fw={500} size="sm">
{t(
"admin.settings.advanced.tempFileManagement.cleanupSystemTemp.label",
@@ -603,7 +603,7 @@ export default function AdminConnectionsSection() {
justifyContent: "space-between",
}}
>
<div>
<div style={{ flex: 1, minWidth: 0 }}>
<Text fw={500} size="sm">
{t(
"admin.settings.connections.ssoAutoLogin.enable",
@@ -674,7 +674,7 @@ export default function AdminConnectionsSection() {
justifyContent: "space-between",
}}
>
<div>
<div style={{ flex: 1, minWidth: 0 }}>
<Text fw={500} size="sm">
{t(
"admin.settings.connections.mobileScanner.enable",
@@ -496,7 +496,7 @@ export default function AdminDatabaseSection() {
justifyContent: "space-between",
}}
>
<div>
<div style={{ flex: 1, minWidth: 0 }}>
<Text fw={500} size="sm">
{t(
"admin.settings.database.enableCustom.label",
@@ -200,7 +200,7 @@ export default function AdminFeaturesSection() {
justifyContent: "space-between",
}}
>
<div>
<div style={{ flex: 1, minWidth: 0 }}>
<Text fw={500} size="sm">
{t(
"admin.settings.features.serverCertificate.enabled.label",
@@ -316,7 +316,7 @@ export default function AdminFeaturesSection() {
justifyContent: "space-between",
}}
>
<div>
<div style={{ flex: 1, minWidth: 0 }}>
<Text fw={500} size="sm">
{t(
"admin.settings.features.serverCertificate.regenerateOnStartup.label",
@@ -703,7 +703,7 @@ export default function AdminGeneralSection() {
marginBottom: "1rem",
}}
>
<div>
<div style={{ flex: 1, minWidth: 0 }}>
<Text fw={500} size="sm">
{t(
"admin.settings.general.hideDisabledTools.googleDrive.label",
@@ -747,7 +747,7 @@ export default function AdminGeneralSection() {
justifyContent: "space-between",
}}
>
<div>
<div style={{ flex: 1, minWidth: 0 }}>
<Text fw={500} size="sm">
{t(
"admin.settings.general.hideDisabledTools.mobileScanner.label",
@@ -793,7 +793,7 @@ export default function AdminGeneralSection() {
justifyContent: "space-between",
}}
>
<div>
<div style={{ flex: 1, minWidth: 0 }}>
<Text fw={500} size="sm">
{t(
"admin.settings.general.showUpdate.label",
@@ -832,7 +832,7 @@ export default function AdminGeneralSection() {
justifyContent: "space-between",
}}
>
<div>
<div style={{ flex: 1, minWidth: 0 }}>
<Text fw={500} size="sm">
{t(
"admin.settings.general.showUpdateOnlyAdmin.label",
@@ -873,7 +873,7 @@ export default function AdminGeneralSection() {
justifyContent: "space-between",
}}
>
<div>
<div style={{ flex: 1, minWidth: 0 }}>
<Text fw={500} size="sm">
{t(
"admin.settings.general.customHTMLFiles.label",
@@ -938,7 +938,7 @@ export default function AdminGeneralSection() {
justifyContent: "space-between",
}}
>
<div>
<div style={{ flex: 1, minWidth: 0 }}>
<Text fw={500} size="sm">
{t(
"admin.settings.general.customMetadata.autoUpdate.label",
@@ -197,7 +197,7 @@ export default function AdminPremiumSection() {
justifyContent: "space-between",
}}
>
<div>
<div style={{ flex: 1, minWidth: 0 }}>
<Text fw={500} size="sm">
{t(
"admin.settings.premium.enabled.label",
@@ -165,7 +165,7 @@ export default function AdminPrivacySection() {
justifyContent: "space-between",
}}
>
<div>
<div style={{ flex: 1, minWidth: 0 }}>
<Text fw={500} size="sm">
{t(
"admin.settings.privacy.enableAnalytics.label",
@@ -203,7 +203,7 @@ export default function AdminPrivacySection() {
justifyContent: "space-between",
}}
>
<div>
<div style={{ flex: 1, minWidth: 0 }}>
<Text fw={500} size="sm">
{t(
"admin.settings.privacy.metricsEnabled.label",
@@ -253,7 +253,7 @@ export default function AdminPrivacySection() {
justifyContent: "space-between",
}}
>
<div>
<div style={{ flex: 1, minWidth: 0 }}>
<Text fw={500} size="sm">
{t(
"admin.settings.privacy.googleVisibility.label",
@@ -292,7 +292,7 @@ export default function AdminSecuritySection() {
justifyContent: "space-between",
}}
>
<div>
<div style={{ flex: 1, minWidth: 0 }}>
<Text fw={500} size="sm">
{t(
"admin.settings.security.enableLogin.label",
@@ -519,7 +519,7 @@ export default function AdminSecuritySection() {
justifyContent: "space-between",
}}
>
<div>
<div style={{ flex: 1, minWidth: 0 }}>
<Text fw={500} size="sm">
{t(
"admin.settings.security.jwt.persistence.label",
@@ -556,7 +556,7 @@ export default function AdminSecuritySection() {
justifyContent: "space-between",
}}
>
<div>
<div style={{ flex: 1, minWidth: 0 }}>
<Text fw={500} size="sm">
{t(
"admin.settings.security.jwt.enableKeyRotation.label",
@@ -596,7 +596,7 @@ export default function AdminSecuritySection() {
justifyContent: "space-between",
}}
>
<div>
<div style={{ flex: 1, minWidth: 0 }}>
<Text fw={500} size="sm">
{t(
"admin.settings.security.jwt.enableKeyCleanup.label",
@@ -780,7 +780,7 @@ export default function AdminSecuritySection() {
justifyContent: "space-between",
}}
>
<div>
<div style={{ flex: 1, minWidth: 0 }}>
<Text fw={500} size="sm">
{t(
"admin.settings.security.jwt.secureCookie.label",
@@ -840,7 +840,7 @@ export default function AdminSecuritySection() {
justifyContent: "space-between",
}}
>
<div>
<div style={{ flex: 1, minWidth: 0 }}>
<Text fw={500} size="sm">
{t(
"admin.settings.security.audit.enabled.label",
@@ -955,7 +955,7 @@ export default function AdminSecuritySection() {
justifyContent: "space-between",
}}
>
<div>
<div style={{ flex: 1, minWidth: 0 }}>
<Text fw={500} size="sm">
{t(
"admin.settings.security.audit.captureFileHash.label",
@@ -995,7 +995,7 @@ export default function AdminSecuritySection() {
justifyContent: "space-between",
}}
>
<div>
<div style={{ flex: 1, minWidth: 0 }}>
<Text fw={500} size="sm">
{t(
"admin.settings.security.audit.capturePdfAuthor.label",
@@ -1035,7 +1035,7 @@ export default function AdminSecuritySection() {
justifyContent: "space-between",
}}
>
<div>
<div style={{ flex: 1, minWidth: 0 }}>
<Text fw={500} size="sm">
{t(
"admin.settings.security.audit.captureOperationResults.label",
@@ -1097,7 +1097,7 @@ export default function AdminSecuritySection() {
justifyContent: "space-between",
}}
>
<div>
<div style={{ flex: 1, minWidth: 0 }}>
<Text fw={500} size="sm">
{t(
"admin.settings.security.htmlUrlSecurity.enabled.label",
@@ -1374,7 +1374,7 @@ export default function AdminSecuritySection() {
justifyContent: "space-between",
}}
>
<div>
<div style={{ flex: 1, minWidth: 0 }}>
<Text fw={500} size="sm">
{t(
"admin.settings.security.htmlUrlSecurity.blockPrivateNetworks.label",
@@ -1424,7 +1424,7 @@ export default function AdminSecuritySection() {
justifyContent: "space-between",
}}
>
<div>
<div style={{ flex: 1, minWidth: 0 }}>
<Text fw={500} size="sm">
{t(
"admin.settings.security.htmlUrlSecurity.blockLocalhost.label",
@@ -1473,7 +1473,7 @@ export default function AdminSecuritySection() {
justifyContent: "space-between",
}}
>
<div>
<div style={{ flex: 1, minWidth: 0 }}>
<Text fw={500} size="sm">
{t(
"admin.settings.security.htmlUrlSecurity.blockLinkLocal.label",
@@ -1522,7 +1522,7 @@ export default function AdminSecuritySection() {
justifyContent: "space-between",
}}
>
<div>
<div style={{ flex: 1, minWidth: 0 }}>
<Text fw={500} size="sm">
{t(
"admin.settings.security.htmlUrlSecurity.blockCloudMetadata.label",
@@ -585,9 +585,12 @@ export default function PeopleSection() {
{t("workspace.people.user")}
</Table.Th>
<Table.Th
style={{ fontWeight: 600, color: "var(--mantine-color-gray-7)" }}
style={{
fontWeight: 600,
color: "var(--mantine-color-gray-7)",
whiteSpace: "nowrap",
}}
fz="sm"
w={100}
>
{t("workspace.people.role")}
</Table.Th>
@@ -690,7 +693,7 @@ export default function PeopleSection() {
</Box>
</Group>
</Table.Td>
<Table.Td w={100}>
<Table.Td style={{ whiteSpace: "nowrap" }}>
<Badge
size="sm"
variant="light"
@@ -699,6 +702,10 @@ export default function PeopleSection() {
? "blue"
: "cyan"
}
styles={{
root: { maxWidth: "none" },
label: { overflow: "visible" },
}}
>
{(user.rolesAsString || "").includes("ROLE_ADMIN")
? t("workspace.people.admin", "Admin")