refactor(font): inject font-load status as a prop, decoupling UI from the store

FontApplicator and FontSampler no longer read fontLifecycleManager. They take a
`status` prop (FontLoadStatus | undefined) supplied by the composing widget;
FontList and SampleList resolve status once per visible row and pass it down.

FSD+ dependency inversion: the entity/feature UI depends on a value, not the
lifecycle store. Removes FontApplicator's value-import of the store (one step
toward an inert ./ui barrel) and drops the duplicate getFontStatus read per row
in FontList. FontSampler is now status-decoupled and trivially relocatable to
entities/Font/ui.
This commit is contained in:
Ilia Mashkov
2026-06-01 12:06:30 +03:00
parent 28a8e49915
commit fcd61be4fa
6 changed files with 52 additions and 46 deletions
@@ -10,14 +10,14 @@ const { Story } = defineMeta({
docs: {
description: {
component:
'Loads a font and applies it to children. Shows blur/scale loading state until font is ready, then reveals with a smooth transition.',
'Applies a font to its children based on the supplied load `status`. Renders the skeleton (or system font) until status is `loaded`/`error`, then reveals the font. The status is provided by the composing widget — the component does not read the lifecycle store itself.',
},
story: { inline: false },
},
layout: 'centered',
},
argTypes: {
weight: { control: 'number' },
status: { control: 'select', options: ['loading', 'loaded', 'error'] },
},
});
</script>
@@ -39,11 +39,11 @@ const fontArialBold = mockUnifiedFont({ id: 'arial-bold', name: 'Arial' });
docs: {
description: {
story:
'Font that has never been loaded by fontLifecycleManager. The component renders in its pending state: blurred, scaled down, and semi-transparent.',
'Status is `loading`: the font file has not resolved yet, so children render in the skeleton (or system font) fallback rather than the target font.',
},
},
}}
args={{ font: fontUnknown, weight: 400 }}
args={{ font: fontUnknown, status: 'loading' }}
>
{#snippet template(args: ComponentProps<typeof FontApplicator>)}
<FontApplicator {...args}>
@@ -58,11 +58,11 @@ const fontArialBold = mockUnifiedFont({ id: 'arial-bold', name: 'Arial' });
docs: {
description: {
story:
'Uses Arial, a system font available in all browsers. Because fontLifecycleManager has not loaded it via FontFace, the manager status may remain pending — meaning the blur/scale state may still show. In a real app the manager would load the font and transition to the revealed state.',
'Status is `loaded`: the component reveals the font, applying it to its children (Arial here, available in all browsers).',
},
},
}}
args={{ font: fontArial, weight: 400 }}
args={{ font: fontArial, status: 'loaded' }}
>
{#snippet template(args: ComponentProps<typeof FontApplicator>)}
<FontApplicator {...args}>
@@ -72,16 +72,16 @@ const fontArialBold = mockUnifiedFont({ id: 'arial-bold', name: 'Arial' });
</Story>
<Story
name="Custom Weight"
name="Error State"
parameters={{
docs: {
description: {
story:
'Demonstrates passing a custom weight (700). The weight is forwarded to fontLifecycleManager for font resolution; visually identical to the loaded state story until the manager confirms the font.',
'Status is `error`: the font failed to load. The component still reveals (it treats `error` like `loaded` for reveal purposes) so children are not stuck behind the skeleton — they fall back to the system font.',
},
},
}}
args={{ font: fontArialBold, weight: 700 }}
args={{ font: fontArialBold, status: 'error' }}
>
{#snippet template(args: ComponentProps<typeof FontApplicator>)}
<FontApplicator {...args}>
@@ -6,11 +6,10 @@
<script lang="ts">
import { cn } from '$shared/lib';
import type { Snippet } from 'svelte';
import {
DEFAULT_FONT_WEIGHT,
type UnifiedFont,
fontLifecycleManager,
} from '../../model';
import type {
FontLoadStatus,
UnifiedFont,
} from '../../model/types';
interface Props {
/**
@@ -18,10 +17,13 @@ interface Props {
*/
font: UnifiedFont;
/**
* Font weight
* @default 400
* Current load status for this font, supplied by the composing layer.
* Kept out of the component so it does not depend on (and import) the
* lifecycle store — the owning widget reads the manager and passes the
* resolved status down. `undefined` means the font is not tracked yet and
* is treated as not-yet-revealed (skeleton / system-font fallback).
*/
weight?: number;
status: FontLoadStatus | undefined;
/**
* CSS classes
*/
@@ -39,20 +41,12 @@ interface Props {
let {
font,
weight = DEFAULT_FONT_WEIGHT,
status,
className,
children,
skeleton,
}: Props = $props();
const status = $derived(
fontLifecycleManager.getFontStatus(
font.id,
weight,
font.features?.isVariable,
),
);
const shouldReveal = $derived(status === 'loaded' || status === 'error');
</script>