Files
Mangalord/frontend/src/lib/components/SegmentedControl.svelte
MechaCat02 6dcb720a0f feat(frontend): mobile primitives + bottom-nav chrome (0.56.0)
Phase 1 of the mobile redesign: add the reusable primitives every
later phase plugs into, and switch the layout to a 4-tab bottom nav
+ compact AppBar on phone viewports while leaving the desktop header
untouched above 640px.

- New primitives: BottomNav, Sheet, AppBar, SegmentedControl, all
  with vitest unit tests.
- Layout swaps the desktop header for AppBar + BottomNav under the
  existing 640px breakpoint. Login / register / reader routes opt
  out of the mobile chrome.
- Library tab currently points at /bookmarks; the curated /library
  wrapper lands in Phase 5.
- Independent ResizeObservers publish --app-header-h,
  --mobile-app-bar-h, --app-bottom-nav-h, with a zero-height skip so
  hidden bars don't clobber the visible one's value.
- viewport-fit=cover + env(safe-area-inset-*) tokens for notched
  devices and the iOS home indicator.
- Playwright spec covers visibility at 390px / 1280px viewports and
  tab navigation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-06 19:46:16 +02:00

75 lines
1.7 KiB
Svelte

<script lang="ts" generics="T extends string">
interface Option {
label: string;
value: T;
}
let {
options,
value,
onchange,
ariaLabel,
testid
}: {
options: Option[];
value: T;
onchange: (next: T) => void;
ariaLabel: string;
testid?: string;
} = $props();
</script>
<div class="segmented" role="radiogroup" aria-label={ariaLabel} data-testid={testid}>
{#each options as opt (opt.value)}
<button
type="button"
role="radio"
aria-checked={opt.value === value}
class="seg"
class:active={opt.value === value}
onclick={() => {
if (opt.value !== value) onchange(opt.value);
}}
data-testid={testid ? `${testid}-${opt.value}` : undefined}
>
{opt.label}
</button>
{/each}
</div>
<style>
.segmented {
display: inline-flex;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius-md);
padding: 2px;
gap: 2px;
}
.seg {
height: 32px;
padding: 0 var(--space-3);
border: none;
border-radius: var(--radius-sm);
background: transparent;
color: var(--text-muted);
font-size: var(--font-sm);
font-weight: var(--weight-medium);
cursor: pointer;
transition:
background var(--transition),
color var(--transition);
}
.seg:hover:not(.active) {
color: var(--text);
}
.seg.active {
background: var(--surface-elevated);
color: var(--text);
box-shadow: var(--shadow-sm);
}
</style>