feat(Board): add constant-size FocalFrame

This commit is contained in:
Ilia Mashkov
2026-06-24 15:29:52 +03:00
parent 59097ca9ad
commit 118c588859
4 changed files with 105 additions and 1 deletions
+1
View File
@@ -2,6 +2,7 @@ export { fitColumns } from './lib';
export { export {
__resetBoard, __resetBoard,
type BoardStore, type BoardStore,
FRAME_ROLE_GAP,
getBoard, getBoard,
MAX_COLUMNS, MAX_COLUMNS,
type RoleTypography, type RoleTypography,
+4 -1
View File
@@ -1,4 +1,7 @@
export { MAX_COLUMNS } from './const/const'; export {
FRAME_ROLE_GAP,
MAX_COLUMNS,
} from './const/const';
export { export {
__resetBoard, __resetBoard,
type BoardStore, 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}