refactor(settings): SettingsSubCard primitives across all of Settings (#1182)

* 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.
This commit is contained in:
Psychotoxical
2026-06-24 23:13:10 +02:00
committed by GitHub
parent 4e8f16b8ae
commit b6ac879b14
16 changed files with 700 additions and 624 deletions
+66 -65
View File
@@ -5,6 +5,7 @@ import { useThemeStore } from '../../store/themeStore';
import SettingsSubSection from '../SettingsSubSection';
import { SettingsGroup } from './SettingsGroup';
import { SettingsToggle } from './SettingsToggle';
import { SettingsSubCard, SettingsField } from './SettingsSubCard';
import { MusicNetworkSection } from './musicNetwork/MusicNetworkSection';
import { purgeExternalArtworkAllServers } from '../../api/coverCache';
@@ -77,46 +78,44 @@ export function IntegrationsTab() {
</SettingsGroup>
<SettingsGroup title={t('settings.discordTemplates')} desc={t('settings.discordTemplatesDesc')}>
<div className="form-group" style={{ marginBottom: '0.75rem' }}>
<label style={{ fontSize: 12 }}>{t('settings.discordTemplateName')}</label>
<input
className="input"
type="text"
value={auth.discordTemplateName}
onChange={e => auth.setDiscordTemplateName(e.target.value)}
placeholder="{title}"
/>
</div>
<div className="form-group" style={{ marginBottom: '0.75rem' }}>
<label style={{ fontSize: 12 }}>{t('settings.discordTemplateDetails')}</label>
<input
className="input"
type="text"
value={auth.discordTemplateDetails}
onChange={e => auth.setDiscordTemplateDetails(e.target.value)}
placeholder="{artist}"
/>
</div>
<div className="form-group" style={{ marginBottom: '0.75rem' }}>
<label style={{ fontSize: 12 }}>{t('settings.discordTemplateState')}</label>
<input
className="input"
type="text"
value={auth.discordTemplateState}
onChange={e => auth.setDiscordTemplateState(e.target.value)}
placeholder="{title}"
/>
</div>
<div className="form-group">
<label style={{ fontSize: 12 }}>{t('settings.discordTemplateLargeText')}</label>
<input
className="input"
type="text"
value={auth.discordTemplateLargeText}
onChange={e => auth.setDiscordTemplateLargeText(e.target.value)}
placeholder="{album}"
/>
</div>
<SettingsSubCard>
<SettingsField label={t('settings.discordTemplateName')}>
<input
className="input"
type="text"
value={auth.discordTemplateName}
onChange={e => auth.setDiscordTemplateName(e.target.value)}
placeholder="{title}"
/>
</SettingsField>
<SettingsField label={t('settings.discordTemplateDetails')}>
<input
className="input"
type="text"
value={auth.discordTemplateDetails}
onChange={e => auth.setDiscordTemplateDetails(e.target.value)}
placeholder="{artist}"
/>
</SettingsField>
<SettingsField label={t('settings.discordTemplateState')}>
<input
className="input"
type="text"
value={auth.discordTemplateState}
onChange={e => auth.setDiscordTemplateState(e.target.value)}
placeholder="{title}"
/>
</SettingsField>
<SettingsField label={t('settings.discordTemplateLargeText')}>
<input
className="input"
type="text"
value={auth.discordTemplateLargeText}
onChange={e => auth.setDiscordTemplateLargeText(e.target.value)}
placeholder="{album}"
/>
</SettingsField>
</SettingsSubCard>
</SettingsGroup>
</>
)}
@@ -166,31 +165,33 @@ export function IntegrationsTab() {
title={t('settings.externalArtworkByokTitle')}
desc={t('settings.externalArtworkByokDesc')}
>
<div className="form-group">
<input
className="input"
type="password"
value={theme.externalArtworkByok}
onChange={e => theme.setExternalArtworkByok(e.target.value)}
placeholder="fanart.tv personal API key"
spellCheck={false}
autoComplete="off"
/>
{theme.externalArtworkByok.trim() && (
<div
style={{
fontSize: 12,
color: 'var(--text-muted)',
marginTop: 6,
display: 'flex',
alignItems: 'center',
gap: 4,
}}
>
<Check size={13} /> {t('settings.externalArtworkByokSaved')}
</div>
)}
</div>
<SettingsSubCard>
<SettingsField>
<input
className="input"
type="password"
value={theme.externalArtworkByok}
onChange={e => theme.setExternalArtworkByok(e.target.value)}
placeholder="fanart.tv personal API key"
spellCheck={false}
autoComplete="off"
/>
{theme.externalArtworkByok.trim() && (
<div
style={{
fontSize: 12,
color: 'var(--text-muted)',
marginTop: 6,
display: 'flex',
alignItems: 'center',
gap: 4,
}}
>
<Check size={13} /> {t('settings.externalArtworkByokSaved')}
</div>
)}
</SettingsField>
</SettingsSubCard>
</SettingsGroup>
)}
</div>