feat(Board): add constant-size FocalFrame
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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}
|
||||||
Reference in New Issue
Block a user