From b6ac879b140f0f3d52108c84c8f79374a071e62c Mon Sep 17 00:00:00 2001
From: Psychotoxical <171614930+Psychotoxical@users.noreply.github.com>
Date: Wed, 24 Jun 2026 23:13:10 +0200
Subject: [PATCH] refactor(settings): SettingsSubCard primitives across all of
Settings (#1182)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* feat(settings): add SettingsSubCard primitives for sub-section controls
Reusable wrappers (SettingsSubCard / SettingsField / SettingsRow /
SettingsValue / SettingsCallout) that encapsulate the settings-norm-* sub-card
styles. Per-mode and detail controls inside a settings section should use these
instead of hand-rolling the box with inline padding or one-off classes — the
lack of a shared primitive is what let recent additions drift off the pattern.
* refactor(settings): move the Audio tab onto the SettingsSubCard primitives
Normalization, Track transitions and Hi-Res now build their sub-cards from the
shared primitives instead of raw settings-norm-* markup. Track Previews, which
rendered its sub-options bare with inline styles and dividers, now uses the same
sub-card so the whole Audio tab is consistent. Behaviour and styling unchanged;
Track Previews' start/duration become labelled slider rows like Normalization.
* refactor(settings): move the remaining tabs onto SettingsSubCard
Apply the shared sub-card primitives across the rest of Settings so every tab
matches the Audio-tab reference look:
- Appearance: grid columns, window buttons, UI scale, font, seekbar
- Library: random-mix blacklist, rating-filter detail
- Input: in-app + global keybind lists
- Integrations: Discord templates, external-artwork BYOK
- System: language, Linux Wayland text render, clock format, logging
- Storage: media dir, hot-cache detail, downloads folder
- Backup: mode picker + per-mode export/import
Detail/per-mode controls now sit in the bordered sub-card; toggle groups stay on
SettingsToggle. Behaviour unchanged.
* refactor(settings): box the data-heavy and shared sections in SettingsSubCard
Bring the previously-skipped 'borderline' sections onto the sub-card too:
- Playback speed: wrap the settings-mode controls in a sub-card; the compact
player-popup path is left untouched (shares the component via the compact flag)
- Theme scheduler: mode picker + theme/time selects move into the sub-card
- Cover-cache + analytics strategy: the per-server tables sit in a sub-card
Behaviour unchanged; the table layouts and the playback-rate-* internals stay as
they are, only the surrounding sub-card is added for visual consistency.
---
.../settings/AnalyticsStrategySection.tsx | 3 +
src/components/settings/AppearanceTab.tsx | 192 +++++++++--------
src/components/settings/BackupSection.tsx | 52 +++--
.../settings/CoverCacheStrategySection.tsx | 3 +
src/components/settings/InputTab.tsx | 5 +
src/components/settings/IntegrationsTab.tsx | 131 ++++++------
src/components/settings/LibraryTab.tsx | 149 +++++++------
src/components/settings/SettingsSubCard.tsx | 104 +++++++++
src/components/settings/StorageTab.tsx | 163 +++++++-------
src/components/settings/SystemTab.tsx | 130 ++++++------
src/components/settings/ThemesTab.tsx | 12 +-
.../audio/HiResCrossfadeResampleBlock.tsx | 20 +-
.../settings/audio/NormalizationBlock.tsx | 199 ++++++++----------
.../settings/audio/PlaybackRateBlock.tsx | 16 +-
.../settings/audio/TrackPreviewsSection.tsx | 107 ++++------
.../settings/audio/TrackTransitionsBlock.tsx | 38 ++--
16 files changed, 700 insertions(+), 624 deletions(-)
create mode 100644 src/components/settings/SettingsSubCard.tsx
diff --git a/src/components/settings/AnalyticsStrategySection.tsx b/src/components/settings/AnalyticsStrategySection.tsx
index c621ba9d..1bcdb767 100644
--- a/src/components/settings/AnalyticsStrategySection.tsx
+++ b/src/components/settings/AnalyticsStrategySection.tsx
@@ -6,6 +6,7 @@ import { save as saveDialog } from '@tauri-apps/plugin-dialog';
import { writeFile } from '@tauri-apps/plugin-fs';
import SettingsSubSection from '../SettingsSubSection';
import { SettingsGroup } from './SettingsGroup';
+import { SettingsSubCard } from './SettingsSubCard';
import { useAnalysisStrategyStore } from '../../store/analysisStrategyStore';
import { useAuthStore } from '../../store/authStore';
import {
@@ -255,6 +256,7 @@ export default function AnalyticsStrategySection() {
>
+
@@ -434,6 +436,7 @@ export default function AnalyticsStrategySection() {
{t('settings.analyticsStrategyAdvancedWarning')}
+
diff --git a/src/components/settings/AppearanceTab.tsx b/src/components/settings/AppearanceTab.tsx
index ae4156fb..955ef5c1 100644
--- a/src/components/settings/AppearanceTab.tsx
+++ b/src/components/settings/AppearanceTab.tsx
@@ -14,6 +14,7 @@ import { IS_LINUX, IS_WINDOWS } from '../../utils/platform';
import SettingsSubSection from '../SettingsSubSection';
import { SettingsGroup } from './SettingsGroup';
import { SettingsToggle } from './SettingsToggle';
+import { SettingsSubCard, SettingsField, SettingsRow, SettingsValue } from './SettingsSubCard';
import { SeekbarPreview } from '../WaveformSeekPreview';
import WindowButtonPreview from '../WindowButtonPreview';
@@ -40,14 +41,15 @@ export function AppearanceTab() {
- >
+
)}
diff --git a/src/components/settings/SettingsSubCard.tsx b/src/components/settings/SettingsSubCard.tsx
new file mode 100644
index 00000000..6ba7ee48
--- /dev/null
+++ b/src/components/settings/SettingsSubCard.tsx
@@ -0,0 +1,104 @@
+import React from 'react';
+
+/**
+ * Settings sub-card primitives — the canonical way to render per-mode / detail
+ * controls as a set-apart group inside a settings section (e.g. the "Target
+ * LUFS" box under the Normalization engine picker). The Audio tab's first four
+ * sections are the reference for this look.
+ *
+ * These wrap the shared `settings-norm-*` styles so callers never hand-roll the
+ * box with inline padding or one-off classes. New settings sub-options should
+ * use these instead of bare `
`s — see `NormalizationBlock` for usage.
+ */
+
+/** Bordered, accent-tinted panel that groups detail controls inside a section. */
+export function SettingsSubCard({
+ children,
+ style,
+ className,
+}: {
+ children: React.ReactNode;
+ style?: React.CSSProperties;
+ className?: string;
+}) {
+ return (
+
+ {children}
+
+ );
+}
+
+/**
+ * One labelled group inside a {@link SettingsSubCard}: an optional title and
+ * description, the controls, and an optional grey note line below them.
+ *
+ * `row` lays the label out beside the controls (for a slider/value pair, like
+ * the Normalization sliders); the default stacks label → description →
+ * controls (for a picker, like the Hi-Res blend-rate buttons).
+ */
+export function SettingsField({
+ label,
+ desc,
+ note,
+ row = false,
+ children,
+}: {
+ label?: React.ReactNode;
+ desc?: React.ReactNode;
+ note?: React.ReactNode;
+ row?: boolean;
+ children?: React.ReactNode;
+}) {
+ return (
+
+ );
+}
+
+/**
+ * A horizontal control row (label and/or control + value side by side), for a
+ * standalone slider inside a {@link SettingsSubCard} or a nested slider/value
+ * pair inside a stacked {@link SettingsField}. For a labelled field whose
+ * description sits below the row, prefer ``.
+ */
+export function SettingsRow({ children }: { children: React.ReactNode }) {
+ return
{children}
;
+}
+
+/** Right-aligned, tabular value readout for a slider row. */
+export function SettingsValue({ children }: { children: React.ReactNode }) {
+ return {children};
+}
+
+/**
+ * Accent callout (border-left + tinted background) for an important caveat
+ * inside a {@link SettingsSubCard} — e.g. the Normalization first-play note.
+ * For a plain grey note line, use the `note` prop on {@link SettingsField}.
+ */
+export function SettingsCallout({ children }: { children: React.ReactNode }) {
+ return
{children}
;
+}
diff --git a/src/components/settings/StorageTab.tsx b/src/components/settings/StorageTab.tsx
index 86a0da1d..924697b5 100644
--- a/src/components/settings/StorageTab.tsx
+++ b/src/components/settings/StorageTab.tsx
@@ -10,6 +10,7 @@ import { formatBytes, snapHotCacheMb } from '../../utils/format/formatBytes';
import SettingsSubSection from '../SettingsSubSection';
import { SettingsGroup } from './SettingsGroup';
import { SettingsToggle } from './SettingsToggle';
+import { SettingsSubCard, SettingsField, SettingsValue } from './SettingsSubCard';
import CoverCacheStrategySection from './CoverCacheStrategySection';
export function StorageTab() {
@@ -77,33 +78,32 @@ export function StorageTab() {
>