feat(Board): add CandidateCard mini switcher

This commit is contained in:
Ilia Mashkov
2026-06-24 15:33:33 +03:00
parent f3a2a6a7bd
commit 5ace4aee07
2 changed files with 98 additions and 0 deletions
@@ -0,0 +1,36 @@
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf';
import CandidateCard from './CandidateCard.svelte';
const { Story } = defineMeta({
title: 'Widgets/Board/CandidateCard',
component: CandidateCard,
tags: ['autodocs'],
parameters: {
docs: {
description: {
component:
'Compact rail switcher for one pairing: the two font names in their own fonts. Click makes the pairing focal (aria-current). Not an evaluation surface — no real-length specimen.',
},
story: { inline: false },
},
},
});
</script>
<script lang="ts">
import type { ComponentProps } from 'svelte';
</script>
<Story
name="Default"
args={{
pairing: { id: 'demo-1', headerFontId: 'Playfair Display', bodyFontId: 'Source Sans Pro' },
}}
>
{#snippet template(args: ComponentProps<typeof CandidateCard>)}
<div style="max-width: 220px;">
<CandidateCard {...args} />
</div>
{/snippet}
</Story>
@@ -0,0 +1,62 @@
<!--
Component: CandidateCard
Compact switcher for one pairing in the rail — NOT an evaluation surface. Shows
the two font names rendered in their own fonts at a small decorative size
(clamp/cqi is fine here: chrome, not the honest-measure specimen). Click makes
the pairing focal. Container-query driven so the same card works anywhere.
-->
<script lang="ts">
import {
FontApplicator,
type UnifiedFont,
getFontLifecycleManager,
} from '$entities/Font';
import type {
Pairing,
Role,
} from '$entities/Pairing';
import { getBoard } from '$features/CompareBoard';
interface Props {
/**
* The pairing this card switches to.
*/
pairing: Pairing;
}
let { pairing }: Props = $props();
const board = getBoard();
const lifecycle = getFontLifecycleManager();
const isFocal = $derived(board.focalId === pairing.id);
const fonts = $derived(board.resolvePairingFonts(pairing));
</script>
<button
type="button"
class="
@container flex w-full flex-col gap-1 rounded-lg border p-3 text-left transition-colors
aria-current:border-indigo-500 aria-current:bg-indigo-50
border-slate-200 hover:border-slate-300
"
aria-current={isFocal ? 'true' : undefined}
onclick={() => board.setFocal(pairing.id)}
>
{@render name('header', fonts.header?.name ?? pairing.headerFontId, fonts.header)}
{@render name('body', fonts.body?.name ?? pairing.bodyFontId, fonts.body)}
</button>
{#snippet name(role: Role, label: string, font: UnifiedFont | undefined)}
{@const size = role === 'header' ? 'clamp(0.9rem, 5cqi, 1.25rem)' : 'clamp(0.75rem, 4cqi, 1rem)'}
{#if font}
<FontApplicator
{font}
status={lifecycle.getFontStatus(font.id, board.typo[role].weight, font.features?.isVariable)}
>
<span class="block truncate" style:font-size={size}>{label}</span>
</FontApplicator>
{:else}
<span class="block truncate text-slate-500" style:font-size={size}>{label}</span>
{/if}
{/snippet}