diff --git a/CHANGELOG.md b/CHANGELOG.md
index e7bf8104..c80b89b8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -115,6 +115,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* Album details now surface every genre a release spans instead of just the first one: the main genre shows inline with a **+N** chip that opens the full, clickable list, each genre linking to its genre page.
* Genres combine album and track tags (matching the genre browser) and read from the local library index when it is ready, so they also work offline.
+### Compact buttons — switch action and toolbar buttons to icon-only
+
+**By [@Psychotoxical](https://github.com/Psychotoxical), PR [#1189](https://github.com/Psychotoxical/psysonic/pull/1189)**
+
+* New **Compact buttons** setting under Settings → Appearance. Switch the action and toolbar buttons between large labelled buttons and small icon-only ones — across album, artist and playlist headers, the shared browse toolbars (sort, filters, multi-select), and the Most Played sort/filter controls. Defaults to large, so nothing changes unless you turn it on. On phones the album header keeps its large touch targets.
+
## Changed
diff --git a/src/App.tsx b/src/App.tsx
index 7109a497..eae0b414 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -17,6 +17,7 @@ export default function App() {
useThemeStore(s => s.theme);
const effectiveTheme = useThemeScheduler();
const font = useFontStore(s => s.font);
+ const buttonSize = useThemeStore(s => s.buttonSize);
const installedThemes = useInstalledThemesStore(s => s.themes);
// Document-attribute hooks are shared between both window kinds — each
@@ -81,6 +82,10 @@ export default function App() {
document.documentElement.setAttribute('data-font', font);
}, [font]);
+ useEffect(() => {
+ document.documentElement.setAttribute('data-button-size', buttonSize);
+ }, [buttonSize]);
+
// Hide all inline track-preview buttons when the user opts out — single
// CSS hook (`html[data-track-previews="off"]`) instead of conditional
// rendering in every tracklist. Per-location toggles use additional
diff --git a/src/components/AlbumHeader.tsx b/src/components/AlbumHeader.tsx
index cef34bd2..c23fe639 100644
--- a/src/components/AlbumHeader.tsx
+++ b/src/components/AlbumHeader.tsx
@@ -475,7 +475,7 @@ export default function AlbumHeader({
) : (
-
+
{onShuffleAll && (
)}
@@ -548,7 +548,7 @@ export default function AlbumHeader({
onClick={onDownload}
{...tooltipAttrs(t('albumDetail.downloadTooltip'))}
>
- {t('albumDetail.download')}{totalSize > 0 ? ` · ${formatMb(totalSize)}` : ''}
+ {t('albumDetail.download')}{totalSize > 0 ? ` · ${formatMb(totalSize)}` : ''}
)
)}
@@ -562,28 +562,31 @@ export default function AlbumHeader({
) : offlineStatus === 'cached' ? (
) : (
)
)}
diff --git a/src/components/SelectionToggleButton.test.tsx b/src/components/SelectionToggleButton.test.tsx
new file mode 100644
index 00000000..701f01cf
--- /dev/null
+++ b/src/components/SelectionToggleButton.test.tsx
@@ -0,0 +1,47 @@
+import { describe, expect, it, vi } from 'vitest';
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import SelectionToggleButton from './SelectionToggleButton';
+
+describe('SelectionToggleButton', () => {
+ it('shows the select label and calls onToggle on click', async () => {
+ const onToggle = vi.fn();
+ const user = userEvent.setup();
+ render(
+ ,
+ );
+ const btn = screen.getByRole('button', { name: 'Multi-select' });
+ await user.click(btn);
+ expect(onToggle).toHaveBeenCalledOnce();
+ });
+
+ it('shows the cancel label and active styling when active', () => {
+ render(
+ {}}
+ selectLabel="Multi-select"
+ cancelLabel="Cancel selection"
+ />,
+ );
+ const btn = screen.getByRole('button', { name: 'Cancel selection' });
+ expect(btn).toHaveClass('btn-sort-active');
+ });
+
+ it('keeps the label in a toolbar-btn-label span for compact-mode hiding', () => {
+ render(
+ {}}
+ selectLabel="Multi-select"
+ cancelLabel="Cancel"
+ />,
+ );
+ expect(document.querySelector('.toolbar-btn-label')?.textContent).toBe('Multi-select');
+ });
+});
diff --git a/src/components/SelectionToggleButton.tsx b/src/components/SelectionToggleButton.tsx
new file mode 100644
index 00000000..9dda07de
--- /dev/null
+++ b/src/components/SelectionToggleButton.tsx
@@ -0,0 +1,44 @@
+import { CheckSquare2 } from 'lucide-react';
+
+interface Props {
+ /** Whether selection mode is currently active. */
+ active: boolean;
+ onToggle: () => void;
+ /** Label when not selecting (e.g. "Multi-select"). */
+ selectLabel: string;
+ /** Label while selecting (e.g. "Cancel selection"). */
+ cancelLabel: string;
+ /** Tooltip when inactive — defaults to `selectLabel`. */
+ startTooltip?: string;
+ iconSize?: number;
+}
+
+/**
+ * Shared multi-select toggle for browse-page toolbars (Albums, Artists,
+ * New Releases, Random Albums, Lossless Albums). The label sits in a
+ * `toolbar-btn-label` span so the existing mobile / compact-mode rule can
+ * collapse it to icon-only while keeping the icon + tooltip + aria-label.
+ */
+export default function SelectionToggleButton({
+ active,
+ onToggle,
+ selectLabel,
+ cancelLabel,
+ startTooltip,
+ iconSize = 15,
+}: Props) {
+ const label = active ? cancelLabel : selectLabel;
+ return (
+
+ );
+}
diff --git a/src/components/WikipediaIcon.tsx b/src/components/WikipediaIcon.tsx
new file mode 100644
index 00000000..bb692ae4
--- /dev/null
+++ b/src/components/WikipediaIcon.tsx
@@ -0,0 +1,7 @@
+export default function WikipediaIcon({ size = 16 }: { size?: number }) {
+ return (
+
+ );
+}
diff --git a/src/components/artistDetail/ArtistDetailHero.tsx b/src/components/artistDetail/ArtistDetailHero.tsx
index fcad1fff..d7b6892d 100644
--- a/src/components/artistDetail/ArtistDetailHero.tsx
+++ b/src/components/artistDetail/ArtistDetailHero.tsx
@@ -2,7 +2,7 @@ import React, { useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useAlbumDetailBack } from '../../hooks/useAlbumDetailBack';
import {
- ArrowLeft, Camera, Check, ExternalLink, HardDriveDownload, Heart,
+ ArrowLeft, Camera, Check, HardDriveDownload, Heart,
Loader2, Play, Radio, Share2, Shuffle, Users,
} from 'lucide-react';
import type { SubsonicAlbum, SubsonicArtist, SubsonicArtistInfo } from '../../api/subsonicTypes';
@@ -17,6 +17,7 @@ import { useCachedUrl } from '../CachedImage';
import { useCoverLightboxSrc } from '../../cover/lightbox';
import type { CoverArtRef } from '../../cover/types';
import LastfmIcon from '../LastfmIcon';
+import WikipediaIcon from '../WikipediaIcon';
import StarRating from '../StarRating';
import { tooltipAttrs } from '../tooltipAttrs';
import { offlineActionPolicy, type OfflineActionPolicy } from '../../utils/offline/offlineActionPolicy';
@@ -241,7 +242,7 @@ export default function ArtistDetailHero({
/>