refactor(ui): extract shared IconButton from duplicated .icon-btn copies
Five files hand-rolled a near-identical 32px `.icon-btn` (same size, hover, and primary/danger variants). Extract a single IconButton.svelte component so the treatment lives in one place. Converts the four sites with the standard 32px form: collections detail, manga edit, upload, and the chapter-pages editor. The component takes a `variant` (plain/primary/danger) and spreads any button attributes (onclick, disabled, aria-label, title, data-testid) straight through; `type="button"` defaults but a caller can override. The rendered button keeps the same class, styles, and DOM position, so layout and behaviour are unchanged — no version bump. Three icon-button sites are intentionally left out: - The header (+layout) and home search button are 36px / different radius — size outliers that need a size/radius prop before folding in. - profile/history's copy is being removed in the shared-HistoryList change; touching it here would just conflict. A component (not a global class) avoids colliding with those remaining local `.icon-btn` definitions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
import type { Manga } from '$lib/api/client';
|
||||
import { fileUrl } from '$lib/api/client';
|
||||
import MangaCard from '$lib/components/MangaCard.svelte';
|
||||
import IconButton from '$lib/components/IconButton.svelte';
|
||||
import ArrowLeft from '@lucide/svelte/icons/arrow-left';
|
||||
import Pencil from '@lucide/svelte/icons/pencil';
|
||||
import Check from '@lucide/svelte/icons/check';
|
||||
@@ -148,26 +149,23 @@
|
||||
{:else}
|
||||
<div class="title-row">
|
||||
<h1 data-testid="collection-name">{collection.name}</h1>
|
||||
<button
|
||||
type="button"
|
||||
class="icon-btn"
|
||||
<IconButton
|
||||
onclick={startEdit}
|
||||
aria-label="Edit collection"
|
||||
title="Edit"
|
||||
data-testid="collection-edit-open"
|
||||
>
|
||||
<Pencil size={16} aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="icon-btn danger"
|
||||
</IconButton>
|
||||
<IconButton
|
||||
variant="danger"
|
||||
onclick={onDeleteCollection}
|
||||
aria-label="Delete collection"
|
||||
title="Delete"
|
||||
data-testid="collection-delete"
|
||||
>
|
||||
<Trash2 size={16} aria-hidden="true" />
|
||||
</button>
|
||||
</IconButton>
|
||||
</div>
|
||||
{#if collection.description}
|
||||
<p class="description" data-testid="collection-description">
|
||||
@@ -406,25 +404,4 @@
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.icon-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
color: var(--text-muted);
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.icon-btn:hover {
|
||||
background: var(--surface-elevated);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.icon-btn.danger:hover {
|
||||
color: var(--danger);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
import { session } from '$lib/session.svelte';
|
||||
import { formatBytes, validateImageFile } from '$lib/upload-validation';
|
||||
import Chip from '$lib/components/Chip.svelte';
|
||||
import IconButton from '$lib/components/IconButton.svelte';
|
||||
import Plus from '@lucide/svelte/icons/plus';
|
||||
import Trash2 from '@lucide/svelte/icons/trash-2';
|
||||
|
||||
@@ -190,16 +191,15 @@
|
||||
maxlength="200"
|
||||
data-testid="manga-author-input"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="icon-btn primary"
|
||||
<IconButton
|
||||
variant="primary"
|
||||
onclick={addAuthor}
|
||||
disabled={!authorDraft.trim()}
|
||||
aria-label="Add author"
|
||||
title="Add author"
|
||||
>
|
||||
<Plus size={16} aria-hidden="true" />
|
||||
</button>
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -240,16 +240,15 @@
|
||||
maxlength="200"
|
||||
data-testid="manga-alt-input"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="icon-btn primary"
|
||||
<IconButton
|
||||
variant="primary"
|
||||
onclick={addAltTitle}
|
||||
disabled={!altTitleDraft.trim()}
|
||||
aria-label="Add alternative title"
|
||||
title="Add alternative title"
|
||||
>
|
||||
<Plus size={16} aria-hidden="true" />
|
||||
</button>
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -270,16 +269,15 @@
|
||||
src={fileUrl(currentCoverPath)}
|
||||
alt="Current cover"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="icon-btn danger"
|
||||
<IconButton
|
||||
variant="danger"
|
||||
onclick={markCoverForRemoval}
|
||||
aria-label="Remove cover"
|
||||
title="Remove cover"
|
||||
data-testid="cover-remove"
|
||||
>
|
||||
<Trash2 size={16} aria-hidden="true" />
|
||||
</button>
|
||||
</IconButton>
|
||||
</div>
|
||||
{:else if pendingCoverRemoval}
|
||||
<p class="hint" data-testid="cover-pending-removal">
|
||||
@@ -419,39 +417,6 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.icon-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
color: var(--text-muted);
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.icon-btn:hover:not(:disabled) {
|
||||
background: var(--surface-elevated);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.icon-btn.primary {
|
||||
background: var(--primary);
|
||||
color: var(--primary-contrast);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.icon-btn.primary:hover:not(:disabled) {
|
||||
background: var(--primary-hover);
|
||||
border-color: var(--primary-hover);
|
||||
}
|
||||
|
||||
.icon-btn.danger:hover:not(:disabled) {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.cover-preview {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import ChapterPagesEditor, {
|
||||
type PendingPage
|
||||
} from '$lib/components/ChapterPagesEditor.svelte';
|
||||
import IconButton from '$lib/components/IconButton.svelte';
|
||||
import Plus from '@lucide/svelte/icons/plus';
|
||||
import Trash2 from '@lucide/svelte/icons/trash-2';
|
||||
|
||||
@@ -236,16 +237,15 @@
|
||||
maxlength="200"
|
||||
data-testid="manga-author-input"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="icon-btn primary"
|
||||
<IconButton
|
||||
variant="primary"
|
||||
onclick={addAuthor}
|
||||
disabled={!authorDraft.trim()}
|
||||
aria-label="Add author"
|
||||
title="Add author"
|
||||
>
|
||||
<Plus size={16} aria-hidden="true" />
|
||||
</button>
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -286,16 +286,15 @@
|
||||
maxlength="200"
|
||||
data-testid="manga-alt-input"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="icon-btn primary"
|
||||
<IconButton
|
||||
variant="primary"
|
||||
onclick={addAltTitle}
|
||||
disabled={!altTitleDraft.trim()}
|
||||
aria-label="Add alternative title"
|
||||
title="Add alternative title"
|
||||
>
|
||||
<Plus size={16} aria-hidden="true" />
|
||||
</button>
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -378,16 +377,15 @@
|
||||
Failed
|
||||
{/if}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
class="icon-btn danger"
|
||||
<IconButton
|
||||
variant="danger"
|
||||
onclick={() => removeChapter(c.id)}
|
||||
aria-label="Remove chapter"
|
||||
title="Remove chapter"
|
||||
data-testid="staged-chapter-remove"
|
||||
>
|
||||
<Trash2 size={16} aria-hidden="true" />
|
||||
</button>
|
||||
</IconButton>
|
||||
</div>
|
||||
{#if c.error}
|
||||
<p class="field-error" role="alert">{c.error}</p>
|
||||
@@ -513,39 +511,6 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.icon-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
color: var(--text-muted);
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.icon-btn:hover:not(:disabled) {
|
||||
background: var(--surface-elevated);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.icon-btn.primary {
|
||||
background: var(--primary);
|
||||
color: var(--primary-contrast);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.icon-btn.primary:hover:not(:disabled) {
|
||||
background: var(--primary-hover);
|
||||
border-color: var(--primary-hover);
|
||||
}
|
||||
|
||||
.icon-btn.danger:hover:not(:disabled) {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.chapters-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
Reference in New Issue
Block a user