Chore: Patch all Svelte-Warnings & Add Aria-Labels

This commit is contained in:
Youwes09
2026-05-01 00:38:15 -05:00
parent 1801fecdbb
commit a71cc719ba
23 changed files with 77 additions and 66 deletions
@@ -39,8 +39,12 @@
class:row-error={isError} class:row-error={isError}
class:row-selected={isSelected} class:row-selected={isSelected}
class:row-removing={isRemoving} class:row-removing={isRemoving}
role="option"
aria-selected={isSelected}
tabindex="0"
use:rowLongPress use:rowLongPress
onclick={(e) => { e.stopPropagation(); onSelect(item.chapter.id, e); }} onclick={(e) => { e.stopPropagation(); onSelect(item.chapter.id, e); }}
onkeydown={(e) => { if (e.key === ' ' || e.key === 'Enter') { e.preventDefault(); onSelect(item.chapter.id, e as unknown as MouseEvent); } }}
> >
{#if manga?.thumbnailUrl} {#if manga?.thumbnailUrl}
<div class="thumb"> <div class="thumb">
@@ -115,7 +115,7 @@
</div> </div>
<div class="bar-wrap"> <div class="bar-wrap">
<div class="status-bar" onclick={handleClickOff} role="presentation"> <div class="status-bar" role="none">
<div class="status-dot" class:active={downloadStore.isRunning}></div> <div class="status-dot" class:active={downloadStore.isRunning}></div>
<span class="status-text"> <span class="status-text">
{downloadStore.togglingPlay {downloadStore.togglingPlay
@@ -127,7 +127,7 @@
<button class="sel-action-btn" disabled={downloadStore.batchWorking} onclick={(e) => { e.stopPropagation(); downloadStore.reorderSelectedToEdge("top"); }} title="Move to top"> <button class="sel-action-btn" disabled={downloadStore.batchWorking} onclick={(e) => { e.stopPropagation(); downloadStore.reorderSelectedToEdge("top"); }} title="Move to top">
<ArrowLineUp size={12} weight="bold" /> <ArrowLineUp size={12} weight="bold" />
</button> </button>
<div class="move-step" onclick={(e) => e.stopPropagation()} role="presentation"> <div class="move-step" onclick={(e) => e.stopPropagation()} onkeydown={(e) => e.stopPropagation()} role="none">
<button class="sel-action-btn" disabled={downloadStore.batchWorking} onclick={(e) => { e.stopPropagation(); downloadStore.reorderSelected("up", moveBy); }} title="Move up"> <button class="sel-action-btn" disabled={downloadStore.batchWorking} onclick={(e) => { e.stopPropagation(); downloadStore.reorderSelected("up", moveBy); }} title="Move up">
<CaretUp size={12} weight="bold" /> <CaretUp size={12} weight="bold" />
</button> </button>
@@ -168,7 +168,7 @@
</div> </div>
</div> </div>
<div class="content" onclick={handleClickOff}> <div class="content" role="none" onclick={handleClickOff} onkeydown={(e) => e.key === 'Escape' && handleClickOff()}>
<DownloadQueue <DownloadQueue
queue={downloadStore.queue} queue={downloadStore.queue}
loading={downloadStore.loading} loading={downloadStore.loading}
@@ -204,9 +204,6 @@
.sel-controls { display: flex; align-items: center; gap: var(--sp-2); } .sel-controls { display: flex; align-items: center; gap: var(--sp-2); }
.status-bar { cursor: default; } .status-bar { cursor: default; }
.bar-sep { width: 1px; height: 12px; background: var(--border-dim); flex-shrink: 0; } .bar-sep { width: 1px; height: 12px; background: var(--border-dim); flex-shrink: 0; }
.sel-count { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-secondary); letter-spacing: var(--tracking-wide); white-space: nowrap; }
.sel-text-btn { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-faint); background: none; border: none; cursor: pointer; padding: 2px 4px; border-radius: var(--radius-sm); transition: color var(--t-base); white-space: nowrap; }
.sel-text-btn:hover { color: var(--text-primary); }
.sel-action-btn { display: flex; align-items: center; gap: 4px; font-family: var(--font-ui); font-size: var(--text-xs); padding: 3px 8px; border-radius: var(--radius-sm); border: 1px solid var(--border-dim); background: var(--bg-overlay); color: var(--text-muted); cursor: pointer; transition: color var(--t-base), border-color var(--t-base), background var(--t-base); white-space: nowrap; } .sel-action-btn { display: flex; align-items: center; gap: 4px; font-family: var(--font-ui); font-size: var(--text-xs); padding: 3px 8px; border-radius: var(--radius-sm); border: 1px solid var(--border-dim); background: var(--bg-overlay); color: var(--text-muted); cursor: pointer; transition: color var(--t-base), border-color var(--t-base), background var(--t-base); white-space: nowrap; }
.sel-action-btn:hover:not(:disabled) { color: var(--text-primary); border-color: var(--border-strong); } .sel-action-btn:hover:not(:disabled) { color: var(--text-primary); border-color: var(--border-strong); }
.sel-action-btn:disabled { opacity: 0.35; cursor: not-allowed; } .sel-action-btn:disabled { opacity: 0.35; cursor: not-allowed; }
@@ -323,8 +323,8 @@
.root { display: flex; flex-direction: column; height: 100%; overflow: hidden; } .root { display: flex; flex-direction: column; height: 100%; overflow: hidden; }
.list { flex: 1; overflow-y: auto; padding: 0 var(--sp-4) var(--sp-4); display: flex; flex-direction: column; gap: 1px; } .list { flex: 1; overflow-y: auto; padding: 0 var(--sp-4) var(--sp-4); display: flex; flex-direction: column; gap: 1px; }
.empty { display: flex; align-items: center; justify-content: center; flex: 1; color: var(--text-faint); font-family: var(--font-ui); font-size: var(--text-xs); letter-spacing: var(--tracking-wide); } .empty { display: flex; align-items: center; justify-content: center; flex: 1; color: var(--text-faint); font-family: var(--font-ui); font-size: var(--text-xs); letter-spacing: var(--tracking-wide); }
.icon-btn { display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; border-radius: var(--radius-md); color: var(--text-muted); transition: color var(--t-base), background var(--t-base); } :global(.icon-btn) { display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; border-radius: var(--radius-md); color: var(--text-muted); transition: color var(--t-base), background var(--t-base); }
.icon-btn:hover:not(:disabled) { color: var(--text-primary); background: var(--bg-raised); } :global(.icon-btn:hover:not(:disabled)) { color: var(--text-primary); background: var(--bg-raised); }
.ext-panel { display: flex; flex-direction: column; gap: var(--sp-2); padding: var(--sp-3) var(--sp-6); flex-shrink: 0; border-bottom: 1px solid var(--border-dim); background: var(--bg-raised); opacity: 1; } .ext-panel { display: flex; flex-direction: column; gap: var(--sp-2); padding: var(--sp-3) var(--sp-6); flex-shrink: 0; border-bottom: 1px solid var(--border-dim); background: var(--bg-raised); opacity: 1; }
.ext-panel-anim { animation: panelSlide 0.18s cubic-bezier(0.16,1,0.3,1) both; } .ext-panel-anim { animation: panelSlide 0.18s cubic-bezier(0.16,1,0.3,1) both; }
.panel-header { display: flex; align-items: center; padding-bottom: var(--sp-1); } .panel-header { display: flex; align-items: center; padding-bottom: var(--sp-1); }
@@ -44,7 +44,7 @@
let search: string = $state(""); let search: string = $state("");
let renderVisible: number = $state(store.settings.renderLimit ?? 48); let renderVisible: number = $state(store.settings.renderLimit ?? 48);
let scrollEl: HTMLDivElement; let scrollEl: HTMLDivElement;
let tabsEl: HTMLDivElement; let tabsEl = $state<HTMLDivElement>(null!);
let containerWidth: number = $state(800); let containerWidth: number = $state(800);
let ctx: { x: number; y: number; manga: Manga } | null = $state(null); let ctx: { x: number; y: number; manga: Manga } | null = $state(null);
let emptyCtx: { x: number; y: number } | null = $state(null); let emptyCtx: { x: number; y: number } | null = $state(null);
@@ -95,7 +95,7 @@
</div> </div>
{/if} {/if}
<div class="content" onclick={(e) => { if (selectMode && !(e.target as HTMLElement).closest(".card")) onExitSelectMode(); }}> <div class="content" role="presentation" onclick={(e) => { if (selectMode && !(e.target as HTMLElement).closest(".card")) onExitSelectMode(); }}>
{#if loading} {#if loading}
<div class="grid"> <div class="grid">
{#each Array(12) as _} {#each Array(12) as _}
@@ -184,7 +184,6 @@
.grid { position: relative; z-index: 1; isolation: isolate; display: grid; grid-template-columns: repeat(var(--cols, auto-fill), minmax(130px, 1fr)); gap: var(--sp-4); } .grid { position: relative; z-index: 1; isolation: isolate; display: grid; grid-template-columns: repeat(var(--cols, auto-fill), minmax(130px, 1fr)); gap: var(--sp-4); }
.card { background: none; border: none; padding: 0; cursor: pointer; text-align: left; } .card { background: none; border: none; padding: 0; cursor: pointer; text-align: left; }
.card.anims:not(.select-mode):hover .cover-wrap { transform: translateY(-3px); border-color: var(--border-strong); box-shadow: 0 6px 20px rgba(0,0,0,0.35); } .card.anims:not(.select-mode):hover .cover-wrap { transform: translateY(-3px); border-color: var(--border-strong); box-shadow: 0 6px 20px rgba(0,0,0,0.35); }
.card.anims:not(.select-mode):hover .cover { filter: brightness(1.1); }
.card:not(.select-mode):hover .title { color: var(--text-primary); } .card:not(.select-mode):hover .title { color: var(--text-primary); }
.card.select-mode { cursor: default; } .card.select-mode { cursor: default; }
.card.card-selected .cover-wrap { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: var(--radius-md); } .card.card-selected .cover-wrap { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: var(--radius-md); }
@@ -192,7 +191,6 @@
.cover-wrap { position: relative; aspect-ratio: 2/3; overflow: hidden; border-radius: var(--radius-md); background: var(--bg-raised); border: 1px solid var(--border-dim); will-change: transform; } .cover-wrap { position: relative; aspect-ratio: 2/3; overflow: hidden; border-radius: var(--radius-md); background: var(--bg-raised); border: 1px solid var(--border-dim); will-change: transform; }
.card.anims .cover-wrap { transition: transform 0.18s cubic-bezier(0.16,1,0.3,1), border-color var(--t-base), box-shadow 0.18s cubic-bezier(0.16,1,0.3,1); } .card.anims .cover-wrap { transition: transform 0.18s cubic-bezier(0.16,1,0.3,1), border-color var(--t-base), box-shadow 0.18s cubic-bezier(0.16,1,0.3,1); }
.cover-wrap.completed { box-shadow: inset 0 -2px 0 0 var(--accent); } .cover-wrap.completed { box-shadow: inset 0 -2px 0 0 var(--accent); }
.card.anims .cover { transition: filter var(--t-base); }
.card-info-overlay { position: absolute; bottom: -4px; left: 0; right: 0; z-index: 2; padding: 32px 6px 10px; background: linear-gradient(to top, rgba(0,0,0,0.88) 0%, rgba(0,0,0,0.5) 50%, transparent 100%); opacity: 0; pointer-events: none; } .card-info-overlay { position: absolute; bottom: -4px; left: 0; right: 0; z-index: 2; padding: 32px 6px 10px; background: linear-gradient(to top, rgba(0,0,0,0.88) 0%, rgba(0,0,0,0.5) 50%, transparent 100%); opacity: 0; pointer-events: none; }
.card-info-overlay.anim { transition: opacity 0.18s ease; } .card-info-overlay.anim { transition: opacity 0.18s ease; }
.card-info-overlay.instant { transition: none; } .card-info-overlay.instant { transition: none; }
@@ -266,11 +266,6 @@
.panel-divider { height: 1px; background: var(--border-dim); margin: 4px 2px; } .panel-divider { height: 1px; background: var(--border-dim); margin: 4px 2px; }
.panel-header { display: flex; align-items: center; justify-content: space-between; padding: 6px 10px 4px; } .panel-header { display: flex; align-items: center; justify-content: space-between; padding: 6px 10px 4px; }
.panel-heading { font-family: var(--font-ui); font-size: var(--text-xs); letter-spacing: var(--tracking-wide); color: var(--text-secondary); font-weight: var(--weight-medium, 500); } .panel-heading { font-family: var(--font-ui); font-size: var(--text-xs); letter-spacing: var(--tracking-wide); color: var(--text-secondary); font-weight: var(--weight-medium, 500); }
.panel-clear-btn { font-family: var(--font-ui); font-size: var(--text-2xs); letter-spacing: var(--tracking-wide); color: var(--text-faint); background: none; border: none; cursor: pointer; padding: 0; transition: color var(--t-base); }
.panel-clear-btn:hover { color: var(--color-error); }
.panel-item-check { justify-content: flex-start; gap: var(--sp-2); }
.panel-check { width: 13px; height: 13px; border-radius: 2px; border: 1px solid var(--border-strong); background: transparent; flex-shrink: 0; transition: background var(--t-base), border-color var(--t-base); display: flex; align-items: center; justify-content: center; color: var(--bg-base); }
.panel-check-on { background: var(--accent); border-color: var(--accent); }
.dir-toggle { color: var(--text-secondary); justify-content: flex-start; gap: var(--sp-2); border-top: 1px solid var(--border-dim); border-radius: 0 0 var(--radius-sm) var(--radius-sm); margin-top: 2px; padding-top: 9px; } .dir-toggle { color: var(--text-secondary); justify-content: flex-start; gap: var(--sp-2); border-top: 1px solid var(--border-dim); border-radius: 0 0 var(--radius-sm) var(--radius-sm); margin-top: 2px; padding-top: 9px; }
:global(.sort-caret) { flex-shrink: 0; } :global(.sort-caret) { flex-shrink: 0; }
</style> </style>
@@ -63,7 +63,7 @@
let inspectPanStartX = 0; let inspectPanStartX = 0;
let inspectPanStartY = 0; let inspectPanStartY = 0;
let stripDragging = false; let stripDragging = $state(false);
let stripDragMoved = false; let stripDragMoved = false;
let stripDragStartY = 0; let stripDragStartY = 0;
let stripScrollStart = 0; let stripScrollStart = 0;
+1 -1
View File
@@ -139,7 +139,7 @@
let containerEl: HTMLDivElement | null = null; let containerEl: HTMLDivElement | null = null;
let pageViewRef: PageView; let pageViewRef: PageView;
let zoomAnchor = { el: null as HTMLElement | null, offset: 0 }; let zoomAnchor = { el: null as HTMLElement | null, offset: 0 };
let hideTimer: ReturnType<typeof setTimeout> | null = null; let hideTimer = $state<ReturnType<typeof setTimeout> | null>(null);
let markedRead = new Set<number>(); let markedRead = new Set<number>();
let appending = false; let appending = false;
let abortCtrl = { current: null as AbortController | null }; let abortCtrl = { current: null as AbortController | null };
@@ -306,6 +306,7 @@
{#if readerState.winOpen} {#if readerState.winOpen}
<div <div
class="wc-clip wc-clip-{popoverSide}" class="wc-clip wc-clip-{popoverSide}"
role="presentation"
onmouseenter={wcResetTimer} onmouseenter={wcResetTimer}
onmousemove={wcResetTimer} onmousemove={wcResetTimer}
> >
@@ -108,7 +108,7 @@
]; ];
</script> </script>
<div class="backdrop" role="presentation" onclick={close} transition:fade={{ duration: 150 }}></div> <div class="backdrop" role="button" tabindex="-1" aria-label="Close settings" onclick={close} onkeydown={(e) => e.key === 'Escape' && close()} transition:fade={{ duration: 150 }}></div>
<div <div
class="panel" class="panel"
@@ -130,12 +130,13 @@
<p class="section-label">Page Style</p> <p class="section-label">Page Style</p>
<div class="option-grid"> <div class="option-grid">
{#each styleOptions as o} {#each styleOptions as o}
{@const Icon = o.icon}
<button <button
class="option-tile" class="option-tile"
class:active={style === o.value} class:active={style === o.value}
onclick={() => onApplySettings({ pageStyle: o.value as typeof PAGE_STYLES[number] })} onclick={() => onApplySettings({ pageStyle: o.value as typeof PAGE_STYLES[number] })}
> >
<div class="tile-icon"><svelte:component this={o.icon} size={18} weight={style === o.value ? "fill" : "light"} /></div> <div class="tile-icon"><Icon size={18} weight={style === o.value ? "fill" : "light"} /></div>
<span class="tile-label">{o.label}</span> <span class="tile-label">{o.label}</span>
</button> </button>
{/each} {/each}
@@ -149,6 +150,7 @@
class:on={effectiveSettings.offsetDoubleSpreads} class:on={effectiveSettings.offsetDoubleSpreads}
onclick={() => onApplySettings({ offsetDoubleSpreads: !effectiveSettings.offsetDoubleSpreads })} onclick={() => onApplySettings({ offsetDoubleSpreads: !effectiveSettings.offsetDoubleSpreads })}
role="switch" role="switch"
aria-label="Offset double spreads"
aria-checked={effectiveSettings.offsetDoubleSpreads} aria-checked={effectiveSettings.offsetDoubleSpreads}
><span class="toggle-knob"></span></button> ><span class="toggle-knob"></span></button>
</label> </label>
@@ -161,6 +163,7 @@
class:on={effectiveSettings.pageGap ?? true} class:on={effectiveSettings.pageGap ?? true}
onclick={() => onApplySettings({ pageGap: !(effectiveSettings.pageGap ?? true) })} onclick={() => onApplySettings({ pageGap: !(effectiveSettings.pageGap ?? true) })}
role="switch" role="switch"
aria-label="Gap between pages"
aria-checked={effectiveSettings.pageGap ?? true} aria-checked={effectiveSettings.pageGap ?? true}
><span class="toggle-knob"></span></button> ><span class="toggle-knob"></span></button>
</label> </label>
@@ -171,6 +174,7 @@
class:on={store.settings.autoNextChapter ?? false} class:on={store.settings.autoNextChapter ?? false}
onclick={() => updateSettings({ autoNextChapter: !(store.settings.autoNextChapter ?? false) })} onclick={() => updateSettings({ autoNextChapter: !(store.settings.autoNextChapter ?? false) })}
role="switch" role="switch"
aria-label="Auto next chapter"
aria-checked={store.settings.autoNextChapter ?? false} aria-checked={store.settings.autoNextChapter ?? false}
><span class="toggle-knob"></span></button> ><span class="toggle-knob"></span></button>
</label> </label>
@@ -181,12 +185,13 @@
<p class="section-label">Fit Mode</p> <p class="section-label">Fit Mode</p>
<div class="option-grid"> <div class="option-grid">
{#each fitOptions as o} {#each fitOptions as o}
{@const Icon = o.icon}
<button <button
class="option-tile" class="option-tile"
class:active={fit === o.value} class:active={fit === o.value}
onclick={() => onApplySettings({ fitMode: o.value })} onclick={() => onApplySettings({ fitMode: o.value })}
> >
<div class="tile-icon"><svelte:component this={o.icon} size={18} weight={fit === o.value ? "fill" : "light"} /></div> <div class="tile-icon"><Icon size={18} weight={fit === o.value ? "fill" : "light"} /></div>
<span class="tile-label">{o.label}</span> <span class="tile-label">{o.label}</span>
</button> </button>
{/each} {/each}
@@ -240,7 +245,7 @@
<span class="zoom-readout">{zoomPct}%</span> <span class="zoom-readout">{zoomPct}%</span>
</div> </div>
<div class="zoom-row"> <div class="zoom-row">
<button class="zoom-step" onclick={() => setZoom(zoom - 0.1)} disabled={zoom <= ZOOM_MIN}></button> <button class="zoom-step" aria-label="Zoom out" onclick={() => setZoom(zoom - 0.1)} disabled={zoom <= ZOOM_MIN}></button>
<input <input
type="range" type="range"
class="zoom-slider" class="zoom-slider"
@@ -250,7 +255,7 @@
value={zoomPct} value={zoomPct}
oninput={(e) => setZoom(Number(e.currentTarget.value) / 100)} oninput={(e) => setZoom(Number(e.currentTarget.value) / 100)}
/> />
<button class="zoom-step" onclick={() => setZoom(zoom + 0.1)} disabled={zoom >= ZOOM_MAX}>+</button> <button class="zoom-step" aria-label="Zoom in" onclick={() => setZoom(zoom + 0.1)} disabled={zoom >= ZOOM_MAX}>+</button>
</div> </div>
</section> </section>
@@ -263,6 +268,7 @@
class:on={effectiveSettings.optimizeContrast} class:on={effectiveSettings.optimizeContrast}
onclick={() => onApplySettings({ optimizeContrast: !effectiveSettings.optimizeContrast })} onclick={() => onApplySettings({ optimizeContrast: !effectiveSettings.optimizeContrast })}
role="switch" role="switch"
aria-label="Optimize contrast"
aria-checked={effectiveSettings.optimizeContrast} aria-checked={effectiveSettings.optimizeContrast}
><span class="toggle-knob"></span></button> ><span class="toggle-knob"></span></button>
</label> </label>
@@ -273,6 +279,7 @@
class:on={store.settings.pinchZoom ?? false} class:on={store.settings.pinchZoom ?? false}
onclick={() => updateSettings({ pinchZoom: !(store.settings.pinchZoom ?? false) })} onclick={() => updateSettings({ pinchZoom: !(store.settings.pinchZoom ?? false) })}
role="switch" role="switch"
aria-label="Pinch to zoom"
aria-checked={store.settings.pinchZoom ?? false} aria-checked={store.settings.pinchZoom ?? false}
><span class="toggle-knob"></span></button> ><span class="toggle-knob"></span></button>
</label> </label>
@@ -283,6 +290,7 @@
class:on={store.settings.markReadOnNext ?? true} class:on={store.settings.markReadOnNext ?? true}
onclick={() => updateSettings({ markReadOnNext: !(store.settings.markReadOnNext ?? true) })} onclick={() => updateSettings({ markReadOnNext: !(store.settings.markReadOnNext ?? true) })}
role="switch" role="switch"
aria-label="Mark read on chapter advance"
aria-checked={store.settings.markReadOnNext ?? true} aria-checked={store.settings.markReadOnNext ?? true}
><span class="toggle-knob"></span></button> ><span class="toggle-knob"></span></button>
</label> </label>
@@ -297,6 +305,7 @@
class:on={perMangaEnabled} class:on={perMangaEnabled}
onclick={onTogglePerManga} onclick={onTogglePerManga}
role="switch" role="switch"
aria-label="Per-manga settings"
aria-checked={perMangaEnabled} aria-checked={perMangaEnabled}
><span class="toggle-knob"></span></button> ><span class="toggle-knob"></span></button>
</label> </label>
@@ -319,8 +328,8 @@
bind:value={presetNameInput} bind:value={presetNameInput}
onkeydown={(e) => { if (e.key === "Enter") commitSavePreset(); if (e.key === "Escape") presetSaving = false; }} onkeydown={(e) => { if (e.key === "Enter") commitSavePreset(); if (e.key === "Escape") presetSaving = false; }}
/> />
<button class="small-btn" disabled={!presetNameInput.trim()} onclick={commitSavePreset}><Check size={12} weight="bold" /></button> <button class="small-btn" aria-label="Confirm" disabled={!presetNameInput.trim()} onclick={commitSavePreset}><Check size={12} weight="bold" /></button>
<button class="small-btn" onclick={() => presetSaving = false}><X size={12} weight="light" /></button> <button class="small-btn" aria-label="Cancel" onclick={() => presetSaving = false}><X size={12} weight="light" /></button>
</div> </div>
{/if} {/if}
@@ -336,8 +345,8 @@
bind:value={presetEditName} bind:value={presetEditName}
onkeydown={(e) => { if (e.key === "Enter") commitRenamePreset(); if (e.key === "Escape") presetEditId = null; }} onkeydown={(e) => { if (e.key === "Enter") commitRenamePreset(); if (e.key === "Escape") presetEditId = null; }}
/> />
<button class="small-btn" disabled={!presetEditName.trim()} onclick={commitRenamePreset}><Check size={12} weight="bold" /></button> <button class="small-btn" aria-label="Confirm" disabled={!presetEditName.trim()} onclick={commitRenamePreset}><Check size={12} weight="bold" /></button>
<button class="small-btn" onclick={() => presetEditId = null}><X size={12} weight="light" /></button> <button class="small-btn" aria-label="Cancel" onclick={() => presetEditId = null}><X size={12} weight="light" /></button>
</div> </div>
{:else} {:else}
<div class="preset-row"> <div class="preset-row">
@@ -60,11 +60,13 @@
<div <div
class="backdrop" class="backdrop"
role="presentation" role="button"
tabindex="-1"
aria-label="Close cover picker"
onclick={(e) => { if (e.target === e.currentTarget) onClose(); }} onclick={(e) => { if (e.target === e.currentTarget) onClose(); }}
onkeydown={(e) => e.key === "Escape" && onClose()} onkeydown={(e) => e.key === "Escape" && onClose()}
> >
<div class="modal" role="dialog" aria-label="Choose cover image" onkeydown={onKeydown}> <div class="modal" role="dialog" aria-label="Choose cover image" tabindex="-1" onkeydown={onKeydown}>
<div class="header"> <div class="header">
<span class="title">Cover Image</span> <span class="title">Cover Image</span>
<button class="close-btn" onclick={onClose}><X size={14} weight="light" /></button> <button class="close-btn" onclick={onClose}><X size={14} weight="light" /></button>
@@ -142,12 +144,6 @@
font-size: var(--text-sm); font-weight: var(--weight-medium); font-size: var(--text-sm); font-weight: var(--weight-medium);
color: var(--text-secondary); flex: 1; color: var(--text-secondary); flex: 1;
} }
.comparing {
font-family: var(--font-ui); font-size: 9px;
color: var(--text-faint); letter-spacing: var(--tracking-wider);
text-transform: uppercase;
animation: pulse 1.2s ease-in-out infinite;
}
.close-btn { .close-btn {
display: flex; align-items: center; justify-content: center; display: flex; align-items: center; justify-content: center;
width: 26px; height: 26px; border-radius: var(--radius-sm); width: 26px; height: 26px; border-radius: var(--radius-sm);
@@ -493,8 +493,8 @@
{#if confirmUnbindId !== null} {#if confirmUnbindId !== null}
{@const rec = records.find(r => r.id === confirmUnbindId)} {@const rec = records.find(r => r.id === confirmUnbindId)}
{@const trk = rec ? trackerFor(rec.trackerId) : null} {@const trk = rec ? trackerFor(rec.trackerId) : null}
<div class="confirm-backdrop" role="presentation" onclick={() => confirmUnbindId = null}> <div class="confirm-backdrop" role="button" tabindex="-1" aria-label="Cancel" onclick={() => confirmUnbindId = null} onkeydown={(e) => { if (e.key === 'Escape') confirmUnbindId = null; }}>
<div class="confirm-modal" role="dialog" onclick={(e) => e.stopPropagation()}> <div class="confirm-modal" role="dialog" tabindex="-1" onclick={(e) => e.stopPropagation()} onkeydown={(e) => e.stopPropagation()}>
<p class="confirm-title">Unlink from {trk?.name ?? "tracker"}?</p> <p class="confirm-title">Unlink from {trk?.name ?? "tracker"}?</p>
<p class="confirm-body">Your progress on {trk?.name} is unaffected.</p> <p class="confirm-body">Your progress on {trk?.name} is unaffected.</p>
<div class="confirm-row"> <div class="confirm-row">
@@ -139,8 +139,8 @@
<svelte:window onkeydown={onKey} /> <svelte:window onkeydown={onKey} />
<div class="backdrop" role="presentation" onclick={onClose} onkeydown={(e) => e.key === "Escape" && onClose()}> <div class="backdrop" role="button" tabindex="-1" aria-label="Close theme editor" onclick={onClose} onkeydown={(e) => e.key === "Escape" && onClose()}>
<div class="shell" role="dialog" aria-label="Theme editor" tabindex="0" style={toCssVars(tokens)} onclick={(e) => e.stopPropagation()}> <div class="shell" role="dialog" aria-label="Theme editor" tabindex="0" style={toCssVars(tokens)} onclick={(e) => e.stopPropagation()} onkeydown={(e) => e.stopPropagation()}>
<header class="header"> <header class="header">
<div class="header-left"> <div class="header-left">
@@ -32,8 +32,8 @@
mountSystemThemeSync(); mountSystemThemeSync();
} }
let triggerDark: HTMLButtonElement; let triggerDark = $state<HTMLButtonElement>(null!);
let triggerLight: HTMLButtonElement; let triggerLight = $state<HTMLButtonElement>(null!);
</script> </script>
<div class="s-panel"> <div class="s-panel">
@@ -49,6 +49,7 @@
class:on={store.settings.systemThemeSync} class:on={store.settings.systemThemeSync}
onclick={toggleSync} onclick={toggleSync}
role="switch" role="switch"
aria-label="Match system theme"
aria-checked={store.settings.systemThemeSync} aria-checked={store.settings.systemThemeSync}
><span class="s-toggle-thumb"></span></button> ><span class="s-toggle-thumb"></span></button>
</div> </div>
@@ -11,7 +11,7 @@
let { selectOpen, closingSelect, toggleSelect, anims }: Props = $props(); let { selectOpen, closingSelect, toggleSelect, anims }: Props = $props();
let triggerIdleTimeout: HTMLButtonElement; let triggerIdleTimeout = $state<HTMLButtonElement>(null!);
</script> </script>
<div class="s-panel"> <div class="s-panel">
@@ -11,7 +11,7 @@
let { selectOpen, toggleSelect, anims }: Props = $props(); let { selectOpen, toggleSelect, anims }: Props = $props();
let triggerSortDir: HTMLButtonElement; let triggerSortDir = $state<HTMLButtonElement>(null!);
</script> </script>
<div class="s-panel"> <div class="s-panel">
@@ -11,9 +11,9 @@
let { selectOpen, toggleSelect, anims }: Props = $props(); let { selectOpen, toggleSelect, anims }: Props = $props();
let triggerPageStyle: HTMLButtonElement; let triggerPageStyle = $state<HTMLButtonElement>(null!);
let triggerReadingDir: HTMLButtonElement; let triggerReadingDir = $state<HTMLButtonElement>(null!);
let triggerFitMode: HTMLButtonElement; let triggerFitMode = $state<HTMLButtonElement>(null!);
</script> </script>
<div class="s-panel"> <div class="s-panel">
@@ -200,7 +200,7 @@
<div class="s-row-info"><span class="s-label">Password</span></div> <div class="s-row-info"><span class="s-label">Password</span></div>
<div class="s-field-wrap"> <div class="s-field-wrap">
<input class="s-input" type={showAuthPass ? "text" : "password"} bind:value={authPassword} placeholder="Password" autocomplete="off" spellcheck="false" disabled={secLoading} /> <input class="s-input" type={showAuthPass ? "text" : "password"} bind:value={authPassword} placeholder="Password" autocomplete="off" spellcheck="false" disabled={secLoading} />
<button class="s-eye-btn" onclick={() => showAuthPass = !showAuthPass} tabindex="-1">{@html showAuthPass ? EyeClose : EyeOpen}</button> <button class="s-eye-btn" onclick={() => showAuthPass = !showAuthPass} tabindex="-1" aria-label={showAuthPass ? "Hide password" : "Show password"}>{@html showAuthPass ? EyeClose : EyeOpen}</button>
</div> </div>
</div> </div>
{/if} {/if}
@@ -236,7 +236,7 @@
<div class="s-section-body"> <div class="s-section-body">
<label class="s-row"> <label class="s-row">
<div class="s-row-info"><span class="s-label">Enable SOCKS proxy</span><span class="s-desc">Route Suwayomi traffic through a SOCKS4/5 proxy</span></div> <div class="s-row-info"><span class="s-label">Enable SOCKS proxy</span><span class="s-desc">Route Suwayomi traffic through a SOCKS4/5 proxy</span></div>
<button role="switch" aria-checked={socksEnabled} class="s-toggle" class:on={socksEnabled} <button role="switch" aria-checked={socksEnabled} aria-label="Enable SOCKS proxy" class="s-toggle" class:on={socksEnabled}
onclick={() => { socksEnabled = !socksEnabled; saveSocksProxy(); }}><span class="s-toggle-thumb"></span></button> onclick={() => { socksEnabled = !socksEnabled; saveSocksProxy(); }}><span class="s-toggle-thumb"></span></button>
</label> </label>
{#if socksEnabled} {#if socksEnabled}
@@ -272,7 +272,7 @@
<div class="s-row-info"><span class="s-label">Password</span><span class="s-desc">Optional</span></div> <div class="s-row-info"><span class="s-label">Password</span><span class="s-desc">Optional</span></div>
<div class="s-field-wrap"> <div class="s-field-wrap">
<input class="s-input" type={showSocksPass ? "text" : "password"} bind:value={socksPassword} placeholder="Password" autocomplete="off" spellcheck="false" /> <input class="s-input" type={showSocksPass ? "text" : "password"} bind:value={socksPassword} placeholder="Password" autocomplete="off" spellcheck="false" />
<button class="s-eye-btn" onclick={() => showSocksPass = !showSocksPass} tabindex="-1">{@html showSocksPass ? EyeClose : EyeOpen}</button> <button class="s-eye-btn" onclick={() => showSocksPass = !showSocksPass} tabindex="-1" aria-label={showSocksPass ? "Hide password" : "Show password"}>{@html showSocksPass ? EyeClose : EyeOpen}</button>
</div> </div>
</div> </div>
<div class="s-row"> <div class="s-row">
@@ -290,7 +290,7 @@
<div class="s-section-body"> <div class="s-section-body">
<label class="s-row"> <label class="s-row">
<div class="s-row-info"><span class="s-label">Enable FlareSolverr</span><span class="s-desc">Bypass Cloudflare challenges for sources that require it</span></div> <div class="s-row-info"><span class="s-label">Enable FlareSolverr</span><span class="s-desc">Bypass Cloudflare challenges for sources that require it</span></div>
<button role="switch" aria-checked={flareEnabled} class="s-toggle" class:on={flareEnabled} <button role="switch" aria-checked={flareEnabled} aria-label="Enable FlareSolverr" class="s-toggle" class:on={flareEnabled}
onclick={() => { flareEnabled = !flareEnabled; saveFlareSolverr(); }}><span class="s-toggle-thumb"></span></button> onclick={() => { flareEnabled = !flareEnabled; saveFlareSolverr(); }}><span class="s-toggle-thumb"></span></button>
</label> </label>
{#if flareEnabled} {#if flareEnabled}
@@ -320,7 +320,7 @@
</div> </div>
<label class="s-row"> <label class="s-row">
<div class="s-row-info"><span class="s-label">Response fallback</span><span class="s-desc">Use FlareSolverr's response when the direct request fails</span></div> <div class="s-row-info"><span class="s-label">Response fallback</span><span class="s-desc">Use FlareSolverr's response when the direct request fails</span></div>
<button role="switch" aria-checked={flareFallback} class="s-toggle" class:on={flareFallback} <button role="switch" aria-checked={flareFallback} aria-label="Response fallback" class="s-toggle" class:on={flareFallback}
onclick={() => flareFallback = !flareFallback}><span class="s-toggle-thumb"></span></button> onclick={() => flareFallback = !flareFallback}><span class="s-toggle-thumb"></span></button>
</label> </label>
<div class="s-row"> <div class="s-row">
@@ -210,7 +210,7 @@
</div> </div>
<button class="s-toggle" class:on={store.settings.trackerSyncBack} <button class="s-toggle" class:on={store.settings.trackerSyncBack}
onclick={() => updateSettings({ trackerSyncBack: !store.settings.trackerSyncBack })} onclick={() => updateSettings({ trackerSyncBack: !store.settings.trackerSyncBack })}
role="switch" aria-checked={store.settings.trackerSyncBack}> role="switch" aria-checked={store.settings.trackerSyncBack} aria-label="Enable sync back">
<span class="s-toggle-thumb"></span> <span class="s-toggle-thumb"></span>
</button> </button>
</div> </div>
@@ -221,7 +221,7 @@
<span class="s-label">Chapter number tolerance</span> <span class="s-label">Chapter number tolerance</span>
<span class="s-desc">Allow source and tracker chapter numbers to differ by up to the set amount. When off, the tracker number is used as-is with no range check.</span> <span class="s-desc">Allow source and tracker chapter numbers to differ by up to the set amount. When off, the tracker number is used as-is with no range check.</span>
</div> </div>
<button role="switch" aria-checked={store.settings.trackerSyncBackThreshold !== null} class="s-toggle" class:on={store.settings.trackerSyncBackThreshold !== null} <button role="switch" aria-checked={store.settings.trackerSyncBackThreshold !== null} aria-label="Chapter number tolerance" class="s-toggle" class:on={store.settings.trackerSyncBackThreshold !== null}
onclick={() => updateSettings({ trackerSyncBackThreshold: store.settings.trackerSyncBackThreshold !== null ? null : 20 })}> onclick={() => updateSettings({ trackerSyncBackThreshold: store.settings.trackerSyncBackThreshold !== null ? null : 20 })}>
<span class="s-toggle-thumb"></span> <span class="s-toggle-thumb"></span>
</button> </button>
@@ -244,7 +244,7 @@
</div> </div>
<button class="s-toggle" class:on={store.settings.trackerRespectScanlatorFilter} <button class="s-toggle" class:on={store.settings.trackerRespectScanlatorFilter}
onclick={() => updateSettings({ trackerRespectScanlatorFilter: !store.settings.trackerRespectScanlatorFilter })} onclick={() => updateSettings({ trackerRespectScanlatorFilter: !store.settings.trackerRespectScanlatorFilter })}
role="switch" aria-checked={store.settings.trackerRespectScanlatorFilter}> role="switch" aria-checked={store.settings.trackerRespectScanlatorFilter} aria-label="Respect scanlator filter">
<span class="s-toggle-thumb"></span> <span class="s-toggle-thumb"></span>
</button> </button>
</div> </div>
@@ -20,8 +20,13 @@
let updatingId = $state<number | null>(null); let updatingId = $state<number | null>(null);
let syncingId = $state<number | null>(null); let syncingId = $state<number | null>(null);
let editingChapter = $state(false); let editingChapter = $state(false);
let chapterDraft = $state(record.lastChapterRead); let chapterDraft = $state(0);
let scoreDraft = $state(record.displayScore ?? ""); let scoreDraft = $state("");
$effect(() => {
chapterDraft = record.lastChapterRead;
scoreDraft = record.displayScore ?? "";
});
let confirmUnbind = $state(false); let confirmUnbind = $state(false);
const isBusy = $derived(updatingId === record.id); const isBusy = $derived(updatingId === record.id);
@@ -123,8 +128,11 @@
<div <div
class="backdrop" class="backdrop"
role="presentation" role="button"
tabindex="-1"
aria-label="Close tracking detail"
onclick={(e) => { if (e.target === e.currentTarget) onClose(); }} onclick={(e) => { if (e.target === e.currentTarget) onClose(); }}
onkeydown={(e) => { if (e.key === 'Escape') onClose(); }}
> >
<div class="modal" role="dialog" aria-label="Tracking detail"> <div class="modal" role="dialog" aria-label="Tracking detail">
@@ -323,8 +331,8 @@
</div> </div>
{#if confirmUnbind} {#if confirmUnbind}
<div class="confirm-backdrop" role="presentation" onclick={() => confirmUnbind = false}> <div class="confirm-backdrop" role="button" tabindex="-1" aria-label="Cancel" onclick={() => confirmUnbind = false} onkeydown={(e) => { if (e.key === 'Escape') confirmUnbind = false; }}>
<div class="confirm-modal" role="dialog" aria-modal="true" onclick={(e) => e.stopPropagation()}> <div class="confirm-modal" role="dialog" aria-modal="true" tabindex="-1" onclick={(e) => e.stopPropagation()} onkeydown={(e) => e.stopPropagation()}>
<div class="confirm-icon"><X size={16} weight="bold" /></div> <div class="confirm-icon"><X size={16} weight="bold" /></div>
<p class="confirm-title">Unlink from {record.tracker.name}?</p> <p class="confirm-title">Unlink from {record.tracker.name}?</p>
<p class="confirm-body"><strong>{record.title}</strong> will be removed from your list. Your progress on {record.tracker.name} is unaffected.</p> <p class="confirm-body"><strong>{record.title}</strong> will be removed from your list. Your progress on {record.tracker.name} is unaffected.</p>
+2 -2
View File
@@ -49,7 +49,7 @@
{#if store.toasts.length} {#if store.toasts.length}
<div class="toaster" aria-live="polite"> <div class="toaster" aria-live="polite">
{#each store.toasts as t (t.id)} {#each store.toasts as t (t.id)}
<div role="alert" class="toast toast-{t.kind}" data-toast-id={t.id} onclick={() => dismiss(t.id)}> <button class="toast toast-{t.kind}" data-toast-id={t.id} aria-label="{t.title}{t.body ? ': ' + t.body : ''}" onclick={() => dismiss(t.id)}>
<div class="accent-bar"></div> <div class="accent-bar"></div>
<span class="icon"> <span class="icon">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
@@ -60,7 +60,7 @@
<p class="title">{t.title}</p> <p class="title">{t.title}</p>
<p class="sub">{t.body ?? '\u00a0'}</p> <p class="sub">{t.body ?? '\u00a0'}</p>
</div> </div>
</div> </button>
{/each} {/each}
</div> </div>
{/if} {/if}
+3 -1
View File
@@ -355,7 +355,9 @@
{#if store.previewManga} {#if store.previewManga}
<div <div
class="backdrop" class="backdrop"
role="presentation" role="button"
tabindex="-1"
aria-label="Close preview"
onclick={(e) => { if (e.target === e.currentTarget) close(); }} onclick={(e) => { if (e.target === e.currentTarget) close(); }}
onkeydown={(e) => { if (e.key === "Escape") close(); }} onkeydown={(e) => { if (e.key === "Escape") close(); }}
> >
+3 -3
View File
@@ -23,7 +23,7 @@
let focused = $state(-1); let focused = $state(-1);
let el = $state<HTMLDivElement | undefined>(undefined); let el = $state<HTMLDivElement | undefined>(undefined);
let measured = $state(false); let measured = $state(false);
let pos = $state({ left: x, top: y }); let pos = $state({ left: 0, top: 0 });
let subOpen = $state(-1); let subOpen = $state(-1);
let subEls = $state<(HTMLDivElement | null)[]>([]); let subEls = $state<(HTMLDivElement | null)[]>([]);
@@ -159,7 +159,7 @@
{#if hasSub}<span class="sub-arrow"></span>{/if} {#if hasSub}<span class="sub-arrow"></span>{/if}
</button> </button>
{#if hasSub && subOpen === i} {#if hasSub && subOpen === i}
<div bind:this={subEls[i]} class="menu submenu" role="menu" <div bind:this={subEls[i]} class="menu submenu" role="menu" tabindex="-1"
onmouseenter={() => { subOpen = i; }}> onmouseenter={() => { subOpen = i; }}>
{#each mi.children as child} {#each mi.children as child}
{#if "separator" in child} {#if "separator" in child}
@@ -204,7 +204,7 @@
animation: scaleIn 0.08s ease both; animation: scaleIn 0.08s ease both;
transform-origin: top left; transform-origin: top left;
} }
.submenu.sub-flip { :global(.submenu.sub-flip) {
left: auto; left: auto;
right: 100%; right: 100%;
transform-origin: top right; transform-origin: top right;