feat(Board): add constant-size FocalFrame
This commit is contained in:
@@ -2,6 +2,7 @@ export { fitColumns } from './lib';
|
||||
export {
|
||||
__resetBoard,
|
||||
type BoardStore,
|
||||
FRAME_ROLE_GAP,
|
||||
getBoard,
|
||||
MAX_COLUMNS,
|
||||
type RoleTypography,
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
export { MAX_COLUMNS } from './const/const';
|
||||
export {
|
||||
FRAME_ROLE_GAP,
|
||||
MAX_COLUMNS,
|
||||
} from './const/const';
|
||||
export {
|
||||
__resetBoard,
|
||||
type BoardStore,
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
<script module>
|
||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||
import FocalFrame from './FocalFrame.svelte';
|
||||
|
||||
const { Story } = defineMeta({
|
||||
title: 'Widgets/Board/FocalFrame',
|
||||
component: FocalFrame,
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
"The constant-size focal pairing (header over body, each in its own font). Height is reserved from the board store's Pretext measurement before paint. Reads the board singleton, which self-seeds a curated pairing from the catalog.",
|
||||
},
|
||||
story: { inline: false },
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<Story name="Default">
|
||||
{#snippet template()}
|
||||
<div style="max-width: 900px; margin: 2rem auto; padding: 0 1rem;">
|
||||
<FocalFrame />
|
||||
</div>
|
||||
{/snippet}
|
||||
</Story>
|
||||
@@ -0,0 +1,74 @@
|
||||
<!--
|
||||
Component: FocalFrame
|
||||
The constant-size focal pairing: header RoleField over body RoleField, each in
|
||||
its own font. The frame's height is reserved from the board's Pretext-measured
|
||||
`focalFrameHeight` BEFORE content paints — this is the zero-shift mechanism, so
|
||||
cycling candidates of equal typography never reflows. Sizes are absolute px
|
||||
(honest measure), never cqi/clamp.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import {
|
||||
FontApplicator,
|
||||
type UnifiedFont,
|
||||
getFontLifecycleManager,
|
||||
} from '$entities/Font';
|
||||
import type { Role } from '$entities/Pairing';
|
||||
import {
|
||||
FRAME_ROLE_GAP,
|
||||
getBoard,
|
||||
} from '$features/CompareBoard';
|
||||
import RoleField from '../RoleField/RoleField.svelte';
|
||||
|
||||
const board = getBoard();
|
||||
const lifecycle = getFontLifecycleManager();
|
||||
|
||||
let frameWidth = $state(0);
|
||||
|
||||
const focal = $derived(board.focal);
|
||||
const fonts = $derived(focal ? board.resolvePairingFonts(focal) : { header: undefined, body: undefined });
|
||||
// Reserve the measured height up front; 0 (unmeasured) leaves the frame to grow
|
||||
// naturally until the warm measurement lands.
|
||||
const reservedHeight = $derived(board.focalFrameHeight(frameWidth));
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="flex w-full flex-col"
|
||||
style:gap="{FRAME_ROLE_GAP}px"
|
||||
style:min-height={reservedHeight > 0 ? `${reservedHeight}px` : undefined}
|
||||
bind:clientWidth={frameWidth}
|
||||
>
|
||||
{#if focal}
|
||||
{@render roleBlock('header', fonts.header)}
|
||||
{@render roleBlock('body', fonts.body)}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#snippet roleBlock(role: Role, font: UnifiedFont | undefined)}
|
||||
{@const typo = board.typo[role]}
|
||||
{#if font}
|
||||
<FontApplicator {font} status={lifecycle.getFontStatus(font.id, typo.weight, font.features?.isVariable)}>
|
||||
<RoleField
|
||||
{role}
|
||||
text={board.specimen[role]}
|
||||
fontName={font.name}
|
||||
size={typo.size}
|
||||
weight={typo.weight}
|
||||
leading={typo.leading}
|
||||
tracking={typo.tracking}
|
||||
oncommit={text => board.setSpecimen(role, text)}
|
||||
/>
|
||||
</FontApplicator>
|
||||
{:else}
|
||||
<!-- Font not yet resolved: render in system font so the field stays live. -->
|
||||
<RoleField
|
||||
{role}
|
||||
text={board.specimen[role]}
|
||||
fontName="system-ui"
|
||||
size={typo.size}
|
||||
weight={typo.weight}
|
||||
leading={typo.leading}
|
||||
tracking={typo.tracking}
|
||||
oncommit={text => board.setSpecimen(role, text)}
|
||||
/>
|
||||
{/if}
|
||||
{/snippet}
|
||||
Reference in New Issue
Block a user