Visual cleanup pass after wiring up real data — the placeholder
content used during the five mobile phases hid a few sharp edges
that became obvious as soon as user-supplied tags / author handles
/ descriptions hit the screen.
- Mobile gutter unified at 16px (was 12px) — matches iOS / Material
standard and gives chips, description text, and chapter rows
visible breathing room from the screen edge. Hero negative margin
in /manga/[id] tracks the change so the bleed still hits viewport.
- Catalog grid switched from 2 columns to 4 with
`repeat(4, minmax(0, 1fr))` (per user preference). The `minmax(0,
...)` is load-bearing — without it the implicit `minmax(auto, 1fr)`
lets a long single-word card title push the column past the
viewport edge. MangaCard's `.author` and `.genres` lines hide
below 640px so every card in the 4-col layout has identical height
(cover + 2-line title clamp).
- Same minmax(0, 1fr) trick applied to the detail page's
`.overview` grid track, plus defensive `min-width: 0` /
`max-width: 100%` on `.meta` and `.chip-row` so a long
unbreakable tag or romanized URL can't drag the metadata column
past the article gutter.
- BottomNav swapped from `grid-auto-columns` (= `minmax(auto, 1fr)`)
to an explicit `repeat(4, minmax(0, 1fr))`, plus `min-width: 0` on
`.tab` and an ellipsizing `.label` so a long tab label stays
inside the bar. The Browse placeholder is replaced with Upload —
a real action, no longer a no-op duplicate of Home.
- Hero appbar gets 16px all-around inset (was 8px) so the back / ⋯
circle buttons aren't kissing the screen edge or the hero top.
Hero content gets 20px horizontal padding so the cover thumb +
title sit visibly off the hero scrim. Description gains
`overflow-wrap: anywhere` for long URLs / romanized titles.
- Chip now allows `overflow-wrap: anywhere` on its label and uses
`max-width: 100%` + `min-width: 0` so a long token wraps to its
own line within the chip-row and ellipsizes inside the chip.
- Mobile catalog search-row gains `flex-basis: calc(100% - 36px -
--space-2)` so the search input takes its own line — the Filter
and Sort chips wrap to a second row instead of squeezing the
input down to a few characters.
- Global safety net: `html, body { overflow-x: hidden }` below
640px so any future bug that lets an element overflow the
viewport doesn't expose touch-pan that shifts the whole layout.
Fixed descendants (bottom nav, CTA bar) are unaffected.
- audit.mjs — small dev helper that walks all 12 user-facing
routes at 390px and reports horizontal overflow, internal
scrollers, and edge-bleeding controls. Used to drive these fixes
and useful for future mobile changes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
129 lines
3.3 KiB
Svelte
129 lines
3.3 KiB
Svelte
<script lang="ts">
|
|
import X from '@lucide/svelte/icons/x';
|
|
|
|
type Variant = 'neutral' | 'primary' | 'soft';
|
|
|
|
let {
|
|
label,
|
|
href,
|
|
variant = 'neutral',
|
|
onRemove,
|
|
removeLabel = 'Remove',
|
|
testid
|
|
}: {
|
|
label: string;
|
|
href?: string;
|
|
variant?: Variant;
|
|
onRemove?: () => void;
|
|
removeLabel?: string;
|
|
testid?: string;
|
|
} = $props();
|
|
</script>
|
|
|
|
{#if href}
|
|
<a class="chip {variant}" {href} data-testid={testid}>
|
|
<span class="chip-label">{label}</span>
|
|
{#if onRemove}
|
|
<button
|
|
type="button"
|
|
class="chip-remove"
|
|
aria-label={removeLabel}
|
|
title={removeLabel}
|
|
onclick={(e) => {
|
|
e.preventDefault();
|
|
onRemove?.();
|
|
}}
|
|
>
|
|
<X size={12} aria-hidden="true" />
|
|
</button>
|
|
{/if}
|
|
</a>
|
|
{:else}
|
|
<span class="chip {variant}" data-testid={testid}>
|
|
<span class="chip-label">{label}</span>
|
|
{#if onRemove}
|
|
<button
|
|
type="button"
|
|
class="chip-remove"
|
|
aria-label={removeLabel}
|
|
title={removeLabel}
|
|
onclick={() => onRemove?.()}
|
|
>
|
|
<X size={12} aria-hidden="true" />
|
|
</button>
|
|
{/if}
|
|
</span>
|
|
{/if}
|
|
|
|
<style>
|
|
.chip {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: var(--space-1);
|
|
padding: 2px var(--space-2);
|
|
border-radius: var(--radius-pill);
|
|
font-size: var(--font-xs);
|
|
font-weight: var(--weight-medium);
|
|
line-height: var(--leading-tight);
|
|
background: var(--surface-elevated);
|
|
border: 1px solid var(--border);
|
|
color: var(--text);
|
|
text-decoration: none;
|
|
/* Cap the chip at its row width so a long author handle or
|
|
tag slug doesn't push past the chip-row's edge. min-width: 0
|
|
is the flex-item escape so it can actually shrink. */
|
|
max-width: 100%;
|
|
min-width: 0;
|
|
}
|
|
|
|
a.chip:hover {
|
|
background: var(--primary-soft-bg);
|
|
border-color: var(--primary);
|
|
color: var(--text);
|
|
text-decoration: none;
|
|
}
|
|
|
|
.chip.primary {
|
|
background: var(--primary-soft-bg);
|
|
border-color: var(--primary);
|
|
}
|
|
|
|
.chip.soft {
|
|
background: transparent;
|
|
border-color: var(--border-strong);
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.chip-label {
|
|
min-width: 0;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
/* `anywhere` lets a no-space token (a romanized title, a tag
|
|
slug) break before it ellipsizes — keeps the chip inside
|
|
its row even when the label has no soft-break points. */
|
|
overflow-wrap: anywhere;
|
|
}
|
|
|
|
.chip-remove {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 16px;
|
|
height: 16px;
|
|
padding: 0;
|
|
background: transparent;
|
|
border: 0;
|
|
color: inherit;
|
|
opacity: 0.6;
|
|
cursor: pointer;
|
|
border-radius: var(--radius-pill);
|
|
}
|
|
|
|
.chip-remove:hover {
|
|
opacity: 1;
|
|
background: var(--danger-soft-bg);
|
|
color: var(--danger);
|
|
}
|
|
</style>
|