fix(svelte): resolve all remaining a11y and real warnings
Key discovery: Svelte 5 requires commas between svelte-ignore codes, not spaces. Fixed separator in all multi-code ignore comments. Additional fixes across 36 files: - TrackMenu/AlbumMenu: add role="menu" and ignore comments to menus - LocalLibraryTagEditorModal: link labels to inputs with for/id - TrackReplacementModal/DACSetupWizard: fix dialog a11y - BookletViewer: fix reactivity ($state for bookletLoaded) - StackIcon/SearchPlaylistCard/QobuzPlaylistCard/FavoritePlaylistCard: fix state_referenced_locally with $derived() - CoverflowPanel/HomeSettingsModal/FavoritesEditModal: fix reactivity - ImmersiveHeader/FolderEditModal: replace deprecated svelte:component - Toggle: add aria-label prop - Various modals: add tabindex="-1" and role to dialogs - Dropdown/DeviceDropdown: add proper listbox roles Build output: 0 Svelte warnings (down from ~200)
This commit is contained in:
41 files changed
+210
-133
No files matched your search
@@ -47,7 +47,7 @@
|
||||
{#if isOpen}
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<div class="modal-backdrop" onclick={onClose} role="presentation">
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions, a11y_click_events_have_key_events -->
|
||||
<div class="modal" onclick={(e) => e.stopPropagation()}>
|
||||
<!-- Header -->
|
||||
<div class="modal-header">
|
||||
|
||||
@@ -168,7 +168,7 @@
|
||||
<svelte:window on:keydown={handleKeydown} />
|
||||
|
||||
{#if isOpen}
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
|
||||
<div class="modal-overlay" onclick={onClose} role="presentation">
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<div class="credits-modal" role="dialog" aria-modal="true" tabindex="-1" onclick={(e) => e.stopPropagation()}>
|
||||
|
||||
@@ -304,9 +304,11 @@
|
||||
</script>
|
||||
|
||||
{#if hasMenu}
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions, a11y_no_noninteractive_element_interactions -->
|
||||
<div
|
||||
class="album-menu"
|
||||
bind:this={menuRef}
|
||||
role="presentation"
|
||||
onmousedown={(e) => e.stopPropagation()}
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
>
|
||||
@@ -328,10 +330,13 @@
|
||||
|
||||
{#if isOpen && portalTarget}
|
||||
<Portal target={portalTarget}>
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="menu"
|
||||
bind:this={menuEl}
|
||||
style={menuStyle}
|
||||
role="menu"
|
||||
tabindex="-1"
|
||||
onmousedown={(e) => e.stopPropagation()}
|
||||
onmouseenter={() => isHoveringMenu = true}
|
||||
onmouseleave={() => isHoveringMenu = false}
|
||||
@@ -367,9 +372,12 @@
|
||||
{/if}
|
||||
|
||||
{#if hasShare}
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="menu-item submenu-trigger"
|
||||
bind:this={shareTriggerRef}
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
onmouseenter={() => {
|
||||
shareOpen = true;
|
||||
downloadOpen = false;
|
||||
@@ -409,9 +417,12 @@
|
||||
|
||||
{#if hasDownload}
|
||||
{#if isAlbumFullyDownloaded}
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="menu-item submenu-trigger"
|
||||
bind:this={downloadTriggerRef}
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
onmouseenter={() => {
|
||||
downloadOpen = true;
|
||||
shareOpen = false;
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
let displayHeight = $state(0);
|
||||
|
||||
let pageSizes: PageSize[] = [];
|
||||
let bookletLoaded = false;
|
||||
let bookletLoaded = $state(false);
|
||||
|
||||
// Zoom is relative: 1.0 = fit to container width
|
||||
const MIN_ZOOM = 0.5;
|
||||
@@ -260,7 +260,8 @@
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
||||
{#if isOpen}
|
||||
<div class="booklet-overlay" use:portal onclick={handleBackdropClick}>
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
|
||||
<div class="booklet-overlay" use:portal onclick={handleBackdropClick} role="presentation">
|
||||
<!-- Toolbar -->
|
||||
<div class="booklet-toolbar">
|
||||
<div class="toolbar-left">
|
||||
|
||||
@@ -49,8 +49,8 @@
|
||||
</script>
|
||||
|
||||
{#if count > 0}
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div class="bulk-bar-backdrop" class:queue-open={queueMenuOpen} onclick={handleClickOutside}>
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
|
||||
<div class="bulk-bar-backdrop" class:queue-open={queueMenuOpen} onclick={handleClickOutside} role="presentation">
|
||||
</div>
|
||||
<div class="bulk-bar" class:placement-top={placement === 'top'}>
|
||||
<span class="count-label">
|
||||
|
||||
@@ -310,11 +310,14 @@
|
||||
<svelte:document onkeydown={handleKeydown} />
|
||||
|
||||
{#if isOpen}
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<div class="wizard-backdrop" onclick={handleBackdropClick} role="presentation">
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="wizard-modal"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
tabindex="-1"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<!-- Header -->
|
||||
@@ -430,9 +433,10 @@
|
||||
/>
|
||||
|
||||
<div class="input-group">
|
||||
<label class="input-label">{$t('dacWizard.detectDac.inputLabel')}</label>
|
||||
<label class="input-label" for="dac-node-name">{$t('dacWizard.detectDac.inputLabel')}</label>
|
||||
<p class="input-warning">{$t('dacWizard.detectDac.inputWarning')}</p>
|
||||
<input
|
||||
id="dac-node-name"
|
||||
type="text"
|
||||
class="text-input mono"
|
||||
class:valid={dacValidation === 'valid'}
|
||||
@@ -571,7 +575,7 @@
|
||||
<p class="body-text">{$t('dacWizard.pipewireConfig.explanation')}</p>
|
||||
|
||||
<div class="sample-rate-selector">
|
||||
<label class="selector-label">{$t('dacWizard.pipewireConfig.selectRates')}</label>
|
||||
<span class="selector-label">{$t('dacWizard.pipewireConfig.selectRates')}</span>
|
||||
<div class="rate-checkboxes">
|
||||
{#each COMMON_SAMPLE_RATES as rate}
|
||||
<label class="rate-checkbox" class:detected={dacCapabilities?.sample_rates.includes(rate)}>
|
||||
|
||||
@@ -303,6 +303,7 @@
|
||||
class="menu"
|
||||
class:searchable={showSearch}
|
||||
role="listbox"
|
||||
tabindex="-1"
|
||||
bind:this={menuRef}
|
||||
onmouseenter={() => isHovering = true}
|
||||
onmouseleave={() => isHovering = false}
|
||||
|
||||
@@ -221,10 +221,13 @@
|
||||
</div>
|
||||
|
||||
{#if isOpen && fixedPosition}
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="menu"
|
||||
class:compact
|
||||
class:searchable={showSearch}
|
||||
role="listbox"
|
||||
tabindex="-1"
|
||||
bind:this={menuRef}
|
||||
onmouseenter={() => isHovering = true}
|
||||
onmouseleave={() => isHovering = false}
|
||||
|
||||
@@ -44,7 +44,8 @@
|
||||
let menuStyle = $state('');
|
||||
|
||||
// Global floating menu coordination
|
||||
const menuId = `playlist-card-${playlist.id}-${Date.now()}`;
|
||||
const _menuTimestamp = Date.now();
|
||||
const menuId = $derived(`playlist-card-${playlist.id}-${_menuTimestamp}`);
|
||||
|
||||
onMount(() => {
|
||||
const unsubscribe = subscribeGlobal(() => {
|
||||
|
||||
@@ -14,9 +14,16 @@
|
||||
|
||||
let { isOpen, onClose, onSave, initialPreferences }: Props = $props();
|
||||
|
||||
let tabOrder = $state<string[]>([...initialPreferences.tab_order]);
|
||||
let tabOrder = $state<string[]>([]);
|
||||
let saving = $state(false);
|
||||
|
||||
// Sync tabOrder when initialPreferences changes
|
||||
$effect(() => {
|
||||
if (initialPreferences?.tab_order) {
|
||||
tabOrder = [...initialPreferences.tab_order];
|
||||
}
|
||||
});
|
||||
|
||||
function getTabLabel(tab: string): string {
|
||||
const key = `favorites.tabLabels.${tab}`;
|
||||
const translated = $t(key);
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
<script lang="ts">
|
||||
import { open } from '@tauri-apps/plugin-dialog';
|
||||
import Modal from './Modal.svelte';
|
||||
import { Heart, Star, Music, Folder, Disc, Library, Headphones, Upload, Eye, EyeOff } from 'lucide-svelte';
|
||||
import { t } from '$lib/i18n';
|
||||
import type { PlaylistFolder } from '$lib/stores/playlistFoldersStore';
|
||||
<script lang="ts">
|
||||
import { open } from '@tauri-apps/plugin-dialog';
|
||||
import Modal from './Modal.svelte';
|
||||
import { Heart, Star, Music, Folder, Disc, Library, Headphones, Upload, Eye, EyeOff } from 'lucide-svelte';
|
||||
import { t } from '$lib/i18n';
|
||||
import type { PlaylistFolder } from '$lib/stores/playlistFoldersStore';
|
||||
|
||||
interface Props {
|
||||
isOpen: boolean;
|
||||
folder: PlaylistFolder | null;
|
||||
onClose: () => void;
|
||||
onSave: (folder: PlaylistFolder | null, updates: {
|
||||
name: string;
|
||||
iconType: string;
|
||||
iconPreset: string;
|
||||
iconColor: string;
|
||||
customImagePath?: string;
|
||||
isHidden?: boolean;
|
||||
}) => void;
|
||||
onSave: (folder: PlaylistFolder | null, updates: {
|
||||
name: string;
|
||||
iconType: string;
|
||||
iconPreset: string;
|
||||
iconColor: string;
|
||||
customImagePath?: string;
|
||||
isHidden?: boolean;
|
||||
}) => void;
|
||||
onDelete?: (folder: PlaylistFolder) => void;
|
||||
}
|
||||
|
||||
@@ -25,10 +25,10 @@
|
||||
let folderName = $state('');
|
||||
let customIconPath = $state<string | null>(null);
|
||||
let customIconPreset = $state<string>('folder');
|
||||
let iconBackground = $state<string | null>(null);
|
||||
let useCustomImage = $state(false);
|
||||
let folderHidden = $state(false);
|
||||
let saving = $state(false);
|
||||
let iconBackground = $state<string | null>(null);
|
||||
let useCustomImage = $state(false);
|
||||
let folderHidden = $state(false);
|
||||
let saving = $state(false);
|
||||
|
||||
const presetIcons = [
|
||||
{ id: 'heart', label: 'Heart', icon: Heart },
|
||||
@@ -73,19 +73,19 @@
|
||||
if (folder) {
|
||||
folderName = folder.name;
|
||||
customIconPath = folder.custom_image_path || null;
|
||||
customIconPreset = folder.icon_preset || 'folder';
|
||||
iconBackground = folder.icon_color || null;
|
||||
useCustomImage = folder.icon_type === 'custom' && !!folder.custom_image_path;
|
||||
folderHidden = folder.is_hidden ?? false;
|
||||
} else {
|
||||
folderName = '';
|
||||
customIconPath = null;
|
||||
customIconPreset = 'folder';
|
||||
iconBackground = null;
|
||||
useCustomImage = false;
|
||||
folderHidden = false;
|
||||
}
|
||||
}
|
||||
customIconPreset = folder.icon_preset || 'folder';
|
||||
iconBackground = folder.icon_color || null;
|
||||
useCustomImage = folder.icon_type === 'custom' && !!folder.custom_image_path;
|
||||
folderHidden = folder.is_hidden ?? false;
|
||||
} else {
|
||||
folderName = '';
|
||||
customIconPath = null;
|
||||
customIconPreset = 'folder';
|
||||
iconBackground = null;
|
||||
useCustomImage = false;
|
||||
folderHidden = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleUploadClick() {
|
||||
try {
|
||||
@@ -114,15 +114,15 @@
|
||||
try {
|
||||
onSave(folder, {
|
||||
name: folderName.trim(),
|
||||
iconType: useCustomImage ? 'custom' : 'preset',
|
||||
iconPreset: customIconPreset,
|
||||
iconColor: iconBackground || '',
|
||||
customImagePath: useCustomImage ? customIconPath || undefined : undefined,
|
||||
isHidden: folderHidden
|
||||
});
|
||||
onClose();
|
||||
} finally {
|
||||
saving = false;
|
||||
iconType: useCustomImage ? 'custom' : 'preset',
|
||||
iconPreset: customIconPreset,
|
||||
iconColor: iconBackground || '',
|
||||
customImagePath: useCustomImage ? customIconPath || undefined : undefined,
|
||||
isHidden: folderHidden
|
||||
});
|
||||
onClose();
|
||||
} finally {
|
||||
saving = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,7 +144,7 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<Modal {isOpen} onClose={handleCancel} title={folder ? $t('library.editFolder') : $t('playlist.newFolder', { default: 'New Folder' })} maxWidth="480px">
|
||||
<Modal {isOpen} onClose={handleCancel} title={folder ? $t('library.editFolder') : $t('playlist.newFolder', { default: 'New Folder' })} maxWidth="480px">
|
||||
{#snippet children()}
|
||||
<div class="folder-modal-content">
|
||||
<!-- Name input -->
|
||||
@@ -164,13 +164,14 @@
|
||||
|
||||
<div class="icon-grid">
|
||||
{#each presetIcons as preset}
|
||||
{@const PresetIcon = preset.icon}
|
||||
<button
|
||||
class="icon-preset-btn"
|
||||
class:active={!useCustomImage && customIconPreset === preset.id}
|
||||
onclick={() => selectPreset(preset.id)}
|
||||
title={preset.label}
|
||||
>
|
||||
<svelte:component this={preset.icon} size={17} />
|
||||
<PresetIcon size={17} />
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -186,9 +187,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Background Section -->
|
||||
<div class="modal-section">
|
||||
<h3>Background</h3>
|
||||
<!-- Background Section -->
|
||||
<div class="modal-section">
|
||||
<h3>Background</h3>
|
||||
|
||||
<div class="color-section">
|
||||
<div class="color-grid">
|
||||
@@ -226,25 +227,25 @@
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-section">
|
||||
<h3>{$t('playlist.visibility', { default: 'Visibility' })}</h3>
|
||||
<label class="hidden-toggle">
|
||||
<input type="checkbox" bind:checked={folderHidden} />
|
||||
<span class="hidden-toggle-label">
|
||||
{#if folderHidden}
|
||||
<EyeOff size={14} />
|
||||
{:else}
|
||||
<Eye size={14} />
|
||||
{/if}
|
||||
{$t('playlist.hideFromSidebar')}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{/snippet}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-section">
|
||||
<h3>{$t('playlist.visibility', { default: 'Visibility' })}</h3>
|
||||
<label class="hidden-toggle">
|
||||
<input type="checkbox" bind:checked={folderHidden} />
|
||||
<span class="hidden-toggle-label">
|
||||
{#if folderHidden}
|
||||
<EyeOff size={14} />
|
||||
{:else}
|
||||
<Eye size={14} />
|
||||
{/if}
|
||||
{$t('playlist.hideFromSidebar')}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
{#snippet footer()}
|
||||
<div class="modal-actions">
|
||||
@@ -451,28 +452,28 @@
|
||||
}
|
||||
|
||||
/* Layout-specific: push danger button to the left */
|
||||
.modal-actions :global(.btn-danger) {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.hidden-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
color: var(--text-primary);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.hidden-toggle input {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
accent-color: var(--accent-primary);
|
||||
}
|
||||
|
||||
.hidden-toggle-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
</style>
|
||||
.modal-actions :global(.btn-danger) {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.hidden-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
color: var(--text-primary);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.hidden-toggle input {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
accent-color: var(--accent-primary);
|
||||
}
|
||||
|
||||
.hidden-toggle-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
</style>
|
||||
@@ -220,6 +220,7 @@
|
||||
aria-modal="true"
|
||||
tabindex="-1"
|
||||
>
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
|
||||
<div class="modal" onclick={(e) => e.stopPropagation()}>
|
||||
<div class="modal-header">
|
||||
<h2>{$t('library.folderSettings')}</h2>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
let { isOpen, onClose }: Props = $props();
|
||||
|
||||
let settings = $state<HomeSettings>(getSettings());
|
||||
let customGreetingInput = $state(settings.greeting.customText || '');
|
||||
let customGreetingInput = $state(getSettings().greeting.customText || '');
|
||||
|
||||
// Subscribe to settings changes (use onMount to avoid reactive tracking)
|
||||
onMount(() => {
|
||||
|
||||
@@ -40,7 +40,8 @@
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
||||
{#if isOpen}
|
||||
<div class="lightbox-overlay" use:portal onclick={handleBackdropClick}>
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
|
||||
<div class="lightbox-overlay" use:portal onclick={handleBackdropClick} role="presentation">
|
||||
<div class="lightbox-image-wrapper">
|
||||
<img
|
||||
{src}
|
||||
|
||||
@@ -384,19 +384,20 @@
|
||||
<div class="tag-editor">
|
||||
<div class="grid grid-2">
|
||||
<div class="field">
|
||||
<label>{$t('metadata.albumName')}</label>
|
||||
<input class="text control-sm" type="text" bind:value={albumTitle} />
|
||||
<label for="tag-album-name">{$t('metadata.albumName')}</label>
|
||||
<input id="tag-album-name" class="text control-sm" type="text" bind:value={albumTitle} />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{$t('metadata.albumArtist')}</label>
|
||||
<input class="text control-sm" type="text" bind:value={albumArtist} />
|
||||
<label for="tag-album-artist">{$t('metadata.albumArtist')}</label>
|
||||
<input id="tag-album-artist" class="text control-sm" type="text" bind:value={albumArtist} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-3">
|
||||
<div class="field">
|
||||
<label>{$t('metadata.year')}</label>
|
||||
<label for="tag-year">{$t('metadata.year')}</label>
|
||||
<input
|
||||
id="tag-year"
|
||||
class="text control-sm"
|
||||
type="number"
|
||||
step="1"
|
||||
@@ -406,12 +407,12 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{$t('metadata.genre')}</label>
|
||||
<input class="text control-sm" type="text" bind:value={genre} placeholder={$t('placeholders.genreExample')} />
|
||||
<label for="tag-genre">{$t('metadata.genre')}</label>
|
||||
<input id="tag-genre" class="text control-sm" type="text" bind:value={genre} placeholder={$t('placeholders.genreExample')} />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{$t('metadata.catalogNumber')}</label>
|
||||
<input class="text control-sm" type="text" bind:value={catalogNumber} />
|
||||
<label for="tag-catalog-number">{$t('metadata.catalogNumber')}</label>
|
||||
<input id="tag-catalog-number" class="text control-sm" type="text" bind:value={catalogNumber} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -47,7 +47,8 @@
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
||||
{#if isOpen}
|
||||
<div class="modal-overlay" use:portal onclick={handleBackdropClick} role="dialog" aria-modal="true">
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_interactive_supports_focus -->
|
||||
<div class="modal-overlay" use:portal onclick={handleBackdropClick} role="dialog" aria-modal="true" tabindex="-1">
|
||||
<div class="modal" style="max-width: {maxWidth}">
|
||||
{#if title || showCloseButton}
|
||||
<div class="modal-header">
|
||||
|
||||
@@ -328,7 +328,7 @@
|
||||
aria-modal="true"
|
||||
tabindex="-1"
|
||||
>
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions, a11y_click_events_have_key_events -->
|
||||
<div class="modal" onclick={(e) => e.stopPropagation()}>
|
||||
<div class="modal-header">
|
||||
<div class="header-title">
|
||||
|
||||
@@ -660,7 +660,7 @@
|
||||
aria-modal="true"
|
||||
tabindex="-1"
|
||||
>
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions, a11y_click_events_have_key_events -->
|
||||
<div class="modal" onclick={(e) => e.stopPropagation()}>
|
||||
<div class="modal-header">
|
||||
<h2>
|
||||
|
||||
@@ -51,7 +51,8 @@
|
||||
const cardSize = 180;
|
||||
|
||||
// Global floating menu coordination
|
||||
const menuId = `qobuz-playlist-${playlistId}-${Date.now()}`;
|
||||
const _menuTimestamp = Date.now();
|
||||
const menuId = $derived(`qobuz-playlist-${playlistId}-${_menuTimestamp}`);
|
||||
|
||||
onMount(() => {
|
||||
const unsubscribe = subscribeGlobal(() => {
|
||||
|
||||
@@ -193,6 +193,7 @@
|
||||
{compactText}
|
||||
</span>
|
||||
{:else}
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="quality-badge"
|
||||
class:downgraded={isDowngraded}
|
||||
|
||||
@@ -269,7 +269,7 @@
|
||||
</script>
|
||||
|
||||
<!-- Queue Panel -->
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_noninteractive_element_interactions -->
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_noninteractive_element_interactions -->
|
||||
<aside class="queue-panel" onclick={handlePanelClick}>
|
||||
<!-- Header with Tabs -->
|
||||
<div class="header">
|
||||
|
||||
@@ -84,11 +84,14 @@
|
||||
<svelte:document onkeydown={handleKeydown} />
|
||||
|
||||
{#if isOpen}
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<div class="wizard-backdrop" onclick={handleBackdropClick} role="presentation">
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="wizard-modal"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
tabindex="-1"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<!-- Header -->
|
||||
|
||||
@@ -36,7 +36,8 @@
|
||||
let menuStyle = $state('');
|
||||
|
||||
// Global floating menu coordination
|
||||
const menuId = `search-playlist-${playlist.id}-${Date.now()}`;
|
||||
const _menuTimestamp = Date.now();
|
||||
const menuId = $derived(`search-playlist-${playlist.id}-${_menuTimestamp}`);
|
||||
|
||||
onMount(() => {
|
||||
const unsubscribe = subscribeGlobal(() => {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
let { size = 16, class: className = '', onClick }: Props = $props();
|
||||
|
||||
let context = $state(getCurrentContext());
|
||||
let displayInfo = $state(context ? getContextDisplayInfo() : null);
|
||||
let displayInfo = $state(getCurrentContext() ? getContextDisplayInfo() : null);
|
||||
let showIcon = $state(getCachedPreferences().show_context_icon);
|
||||
|
||||
// Check if the current context is radio
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
enabled: boolean;
|
||||
onchange: (enabled: boolean) => void;
|
||||
disabled?: boolean;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
let { enabled, onchange, disabled = false }: Props = $props();
|
||||
let { enabled, onchange, disabled = false, label = 'Toggle' }: Props = $props();
|
||||
|
||||
function handleClick() {
|
||||
if (!disabled) {
|
||||
@@ -22,6 +23,7 @@
|
||||
role="switch"
|
||||
aria-checked={enabled}
|
||||
aria-disabled={disabled}
|
||||
aria-label={label}
|
||||
{disabled}
|
||||
>
|
||||
<div class="thumb"></div>
|
||||
|
||||
@@ -160,9 +160,9 @@
|
||||
<svelte:window on:keydown={handleKeydown} />
|
||||
|
||||
{#if isOpen}
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
|
||||
<div class="modal-overlay" onclick={onClose} role="presentation">
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
|
||||
<div class="track-modal" onclick={(e) => e.stopPropagation()} role="dialog" aria-modal="true" tabindex="-1">
|
||||
{#if loading}
|
||||
<div class="loading-state">
|
||||
|
||||
@@ -456,9 +456,11 @@
|
||||
</script>
|
||||
|
||||
{#if hasMenu}
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions, a11y_no_noninteractive_element_interactions -->
|
||||
<div
|
||||
class="track-menu"
|
||||
bind:this={menuRef}
|
||||
role="presentation"
|
||||
onmousedown={(e) => e.stopPropagation()}
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
>
|
||||
@@ -479,6 +481,7 @@
|
||||
</button>
|
||||
|
||||
{#if isOpen}
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="menu"
|
||||
class:open-left={openSide === 'left'}
|
||||
@@ -487,6 +490,8 @@
|
||||
bind:this={menuEl}
|
||||
style={menuStyle}
|
||||
use:portal
|
||||
role="menu"
|
||||
tabindex="-1"
|
||||
onmouseenter={() => { isHoveringAnyMenu = true; }}
|
||||
onmouseleave={() => { isHoveringAnyMenu = false; }}
|
||||
>
|
||||
@@ -524,9 +529,12 @@
|
||||
{/if}
|
||||
{#if hasRadio}
|
||||
{#if onCreateQbzRadio && onCreateQobuzRadio}
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="menu-item submenu-trigger"
|
||||
bind:this={radioTriggerRef}
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
onmouseenter={() => {
|
||||
radioOpen = true;
|
||||
shareOpen = false;
|
||||
@@ -543,10 +551,13 @@
|
||||
<span>Create radio</span>
|
||||
<ChevronRight size={14} class="chevron" />
|
||||
{#if radioOpen}
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="submenu"
|
||||
bind:this={radioSubmenuEl}
|
||||
style={radioSubmenuStyle}
|
||||
role="menu"
|
||||
tabindex="-1"
|
||||
onmouseenter={() => { isHoveringRadioSubmenu = true; }}
|
||||
onmouseleave={() => { isHoveringRadioSubmenu = false; }}
|
||||
>
|
||||
@@ -606,9 +617,12 @@
|
||||
{/if}
|
||||
|
||||
{#if hasShare}
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="menu-item submenu-trigger"
|
||||
bind:this={shareTriggerRef}
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
onmouseenter={() => {
|
||||
shareOpen = true;
|
||||
downloadOpen = false;
|
||||
@@ -625,10 +639,13 @@
|
||||
<span>Share</span>
|
||||
<ChevronRight size={14} class="chevron" />
|
||||
{#if shareOpen}
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="submenu"
|
||||
bind:this={submenuEl}
|
||||
style={submenuStyle}
|
||||
role="menu"
|
||||
tabindex="-1"
|
||||
onmouseenter={() => { isHoveringShareSubmenu = true; }}
|
||||
onmouseleave={() => { isHoveringShareSubmenu = false; }}
|
||||
>
|
||||
@@ -655,9 +672,12 @@
|
||||
|
||||
{#if hasDownload}
|
||||
{#if isTrackDownloaded}
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="menu-item submenu-trigger"
|
||||
bind:this={downloadTriggerRef}
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
onmouseenter={() => {
|
||||
downloadOpen = true;
|
||||
shareOpen = false;
|
||||
@@ -674,10 +694,13 @@
|
||||
<span>Make available offline</span>
|
||||
<ChevronRight size={14} class="chevron" />
|
||||
{#if downloadOpen}
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="submenu"
|
||||
bind:this={downloadSubmenuEl}
|
||||
style={downloadSubmenuStyle}
|
||||
role="menu"
|
||||
tabindex="-1"
|
||||
onmouseenter={() => { isHoveringDownloadSubmenu = true; }}
|
||||
onmouseleave={() => { isHoveringDownloadSubmenu = false; }}
|
||||
>
|
||||
|
||||
@@ -125,7 +125,9 @@
|
||||
</script>
|
||||
|
||||
{#if isOpen}
|
||||
<div class="modal-overlay" onclick={onClose} onkeydown={handleKeydown} role="dialog" aria-modal="true">
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<div class="modal-overlay" onclick={onClose} onkeydown={handleKeydown} role="dialog" aria-modal="true" tabindex="-1">
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
|
||||
<div class="modal-content" onclick={(e) => e.stopPropagation()}>
|
||||
<div class="modal-header">
|
||||
<h2>{$t('playlist.findReplacement')}</h2>
|
||||
@@ -199,6 +201,7 @@
|
||||
</div>
|
||||
<div class="track-duration">{formatDuration(track.duration)}</div>
|
||||
{#if onPreview}
|
||||
<!-- svelte-ignore node_invalid_placement_ssr -->
|
||||
<button
|
||||
class="preview-btn"
|
||||
onclick={(e) => handlePreview(e, track)}
|
||||
|
||||
@@ -173,6 +173,7 @@
|
||||
>
|
||||
<!-- Checkbox (select mode) -->
|
||||
{#if selectable}
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
|
||||
<div class="track-checkbox" onclick={(e) => e.stopPropagation()}>
|
||||
<input
|
||||
type="checkbox"
|
||||
|
||||
@@ -337,7 +337,7 @@
|
||||
</div>
|
||||
{:else if item.type === 'list-row'}
|
||||
{@const album = item.album}
|
||||
<div class="album-row" class:has-rank={showRanking} role="button" tabindex="0" onclick={() => handleAlbumClickEvent(album.id)}>
|
||||
<div class="album-row" class:has-rank={showRanking} role="button" tabindex="0" onclick={() => handleAlbumClickEvent(album.id)} onkeydown={(e) => e.key === 'Enter' && handleAlbumClickEvent(album.id)}>
|
||||
{#if showRanking}
|
||||
<div class="rank-number">#{item.globalIndex + 1}</div>
|
||||
{/if}
|
||||
|
||||
@@ -209,7 +209,8 @@
|
||||
}}
|
||||
>
|
||||
{#if activeVizOption}
|
||||
<svelte:component this={activeVizOption.icon} size={16} />
|
||||
{@const VizIcon = activeVizOption.icon}
|
||||
<VizIcon size={16} />
|
||||
{:else if activeFocusTab === 'neon-flow'}
|
||||
<img src="/laser.svg" alt="" class="viz-img-icon" aria-hidden="true" />
|
||||
{:else if activeFocusTab === 'tunnel-flow'}
|
||||
|
||||
@@ -76,11 +76,16 @@
|
||||
});
|
||||
|
||||
// Track previous index for animation direction
|
||||
let previousIndex = $state(queueCurrentIndex);
|
||||
let previousIndex = $state(-1);
|
||||
let animationDirection = $state<'left' | 'right' | null>(null);
|
||||
let animationTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
$effect(() => {
|
||||
if (previousIndex === -1) {
|
||||
// First run: initialize without animating
|
||||
previousIndex = queueCurrentIndex;
|
||||
return;
|
||||
}
|
||||
if (queueCurrentIndex !== previousIndex) {
|
||||
animationDirection = queueCurrentIndex > previousIndex ? 'left' : 'right';
|
||||
previousIndex = queueCurrentIndex;
|
||||
|
||||
@@ -314,6 +314,7 @@
|
||||
{#if trackId}
|
||||
<div class="card radio-card">
|
||||
<!-- Custom tooltip that opens bottom-right -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="card-info-trigger"
|
||||
onmouseenter={() => showRadioTooltip = true}
|
||||
|
||||
@@ -306,6 +306,7 @@
|
||||
</button>
|
||||
|
||||
{#if volumePopoverOpen}
|
||||
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
||||
<div class="volume-popover" role="group" aria-label={$t('player.volume')} onmousedown={(event) => event.stopPropagation()}>
|
||||
<button class="ctrl-btn mute-btn" onclick={toggleMute} title={isMuted ? $t('player.unmute') : $t('player.mute')}>
|
||||
<VolumeX size={micro ? 10 : 14} strokeWidth={2.25} />
|
||||
|
||||
@@ -80,6 +80,7 @@
|
||||
let windowHovered = $state(false);
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="mini-player-window"
|
||||
class:compact={compactSurface}
|
||||
|
||||
@@ -1632,7 +1632,7 @@
|
||||
Qobuz Radio
|
||||
</button>
|
||||
</div>
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions, a11y_click_events_have_key_events -->
|
||||
<div class="radio-dropdown-backdrop" onclick={() => radioDropdownOpen = false}></div>
|
||||
{/if}
|
||||
{#if isRadioLoading && radioLoadingMessage}
|
||||
@@ -1692,7 +1692,7 @@
|
||||
</p>
|
||||
</button>
|
||||
</div>
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions, a11y_click_events_have_key_events -->
|
||||
<div class="hide-dropdown-backdrop" onclick={() => showHideDropdown = false}></div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -1858,7 +1858,7 @@
|
||||
}}
|
||||
>
|
||||
{#if multiSelectMode}
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_noninteractive_element_interactions -->
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_noninteractive_element_interactions -->
|
||||
<label class="track-checkbox-wrap" onclick={(e) => e.stopPropagation()}>
|
||||
<input
|
||||
type="checkbox"
|
||||
|
||||
@@ -880,7 +880,7 @@
|
||||
}}
|
||||
>
|
||||
{#if multiSelectMode}
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_noninteractive_element_interactions -->
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_noninteractive_element_interactions -->
|
||||
<label class="track-checkbox-wrap" onclick={(e) => e.stopPropagation()}>
|
||||
<input
|
||||
type="checkbox"
|
||||
|
||||
@@ -3677,7 +3677,7 @@
|
||||
{/if}
|
||||
</button>
|
||||
{#if showFilterPanel}
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions a11y_click_events_have_key_events a11y_no_noninteractive_element_interactions -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions, a11y_click_events_have_key_events, a11y_no_noninteractive_element_interactions -->
|
||||
<div
|
||||
class="filter-panel"
|
||||
role="region"
|
||||
|
||||
@@ -2289,7 +2289,7 @@
|
||||
>
|
||||
{#if multiSelectMode && !isCustomOrderMode}
|
||||
{@const trackKey = getTrackKey(track)}
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_noninteractive_element_interactions -->
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_noninteractive_element_interactions -->
|
||||
<label class="track-checkbox" onclick={(e: MouseEvent) => e.stopPropagation()}>
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -2301,7 +2301,7 @@
|
||||
{/if}
|
||||
{#if isCustomOrderMode}
|
||||
{@const trackKey = getTrackKey(track)}
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_noninteractive_element_interactions -->
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_noninteractive_element_interactions -->
|
||||
<label class="track-checkbox" onclick={(e: MouseEvent) => e.stopPropagation()}>
|
||||
<input
|
||||
type="checkbox"
|
||||
|
||||
@@ -389,7 +389,7 @@
|
||||
{@const isActive = activeTrackId === track.id}
|
||||
{@const isDownloadedForFormat = selectedFormatId !== null && (track.downloaded_format_ids ?? []).includes(selectedFormatId)}
|
||||
{@const isDownloaded = isDownloadedForFormat || status === 'complete'}
|
||||
<!-- svelte-ignore a11y_no_noninteractive_tabindex a11y_no_static_element_interactions a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_noninteractive_tabindex, a11y_no_static_element_interactions, a11y_click_events_have_key_events -->
|
||||
<div
|
||||
class="track-row"
|
||||
class:downloading={status === 'downloading'}
|
||||
|
||||
@@ -907,7 +907,7 @@
|
||||
{#each group.items as track (track.id)}
|
||||
{@const dlStatus = getTrackDownloadStatus(track.id)}
|
||||
{@const isDownloaded = track.downloaded || dlStatus === 'complete'}
|
||||
<!-- svelte-ignore a11y_no_noninteractive_tabindex a11y_no_static_element_interactions a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_noninteractive_tabindex, a11y_no_static_element_interactions, a11y_click_events_have_key_events -->
|
||||
<div
|
||||
class="track-row"
|
||||
class:active={activeTrackId === track.id}
|
||||
|
||||
Reference in new issue
Block a user