refactor(entities/Font): relocate FontSampler from DisplayFont, invert typography

DisplayFont was not a feature (FSD+ A-6): the whole slice was one
presentational component that renders a Font styled by typography, with no
model/domain/action. To get typography it reached sideways into a sibling
feature (`$features/AdjustTypography/model`) — a feature->feature edge
(C-1), the symptom of the mislayering, not the disease.

Fix by inversion, mirroring the existing `status` prop pattern:

- move FontSampler into entities/Font/ui (it now uses only entity siblings
  + $shared/ui)
- it accepts a `typography` prop typed to a minimal contract defined in the
  component; the AdjustTypography store satisfies it structurally, so the
  entity has no dependency on the feature
- SampleList (owns both) injects its typographySettingsStore as the prop
- delete the DisplayFont slice; export FontSampler from the Font barrel;
  relocate the story (now passes a mock typography)

Resolves A-6, A-7, and the FontSampler half of C-1. Verified: 0 type
errors, 0 lint (boundary rule satisfied), 905 unit + 213 component tests,
production build OK.
This commit is contained in:
Ilia Mashkov
2026-06-03 08:34:49 +03:00
parent 028853aff5
commit 09869aed00
7 changed files with 61 additions and 25 deletions
+1
View File
@@ -19,6 +19,7 @@ export type { FontRowSizeResolverOptions } from './lib';
export { export {
FontApplicator, FontApplicator,
FontSampler,
FontVirtualList, FontVirtualList,
} from './ui'; } from './ui';
@@ -4,7 +4,7 @@ import { defineMeta } from '@storybook/addon-svelte-csf';
import FontSampler from './FontSampler.svelte'; import FontSampler from './FontSampler.svelte';
const { Story } = defineMeta({ const { Story } = defineMeta({
title: 'Features/FontSampler', title: 'Entities/Font/FontSampler',
component: FontSampler, component: FontSampler,
tags: ['autodocs'], tags: ['autodocs'],
parameters: { parameters: {
@@ -39,8 +39,8 @@ const { Story } = defineMeta({
</script> </script>
<script lang="ts"> <script lang="ts">
import type { UnifiedFont } from '$entities/Font';
import type { ComponentProps } from 'svelte'; import type { ComponentProps } from 'svelte';
import type { UnifiedFont } from '../../model/types';
// Mock fonts for testing // Mock fonts for testing
const mockArial: UnifiedFont = { const mockArial: UnifiedFont = {
@@ -84,6 +84,14 @@ const mockGeorgia: UnifiedFont = {
isVariable: false, isVariable: false,
}, },
}; };
// Stand-in for the AdjustTypography store the composing widget injects.
const mockTypography = {
renderedSize: 48,
weight: 400,
height: 1.5,
spacing: 0,
};
</script> </script>
<Story <Story
@@ -93,6 +101,7 @@ const mockGeorgia: UnifiedFont = {
status: 'loaded', status: 'loaded',
text: 'The quick brown fox jumps over the lazy dog', text: 'The quick brown fox jumps over the lazy dog',
index: 0, index: 0,
typography: mockTypography,
}} }}
> >
{#snippet template(args: ComponentProps<typeof FontSampler>)} {#snippet template(args: ComponentProps<typeof FontSampler>)}
@@ -111,6 +120,7 @@ const mockGeorgia: UnifiedFont = {
text: text:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.', 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.',
index: 1, index: 1,
typography: mockTypography,
}} }}
> >
{#snippet template(args: ComponentProps<typeof FontSampler>)} {#snippet template(args: ComponentProps<typeof FontSampler>)}
@@ -4,12 +4,6 @@
Visual design matches FontCard: sharp corners, red hover accent, header stats. Visual design matches FontCard: sharp corners, red hover accent, header stats.
--> -->
<script lang="ts"> <script lang="ts">
import {
FontApplicator,
type FontLoadStatus,
type UnifiedFont,
} from '$entities/Font';
import { getTypographySettingsStore } from '$features/AdjustTypography/model';
import { import {
Badge, Badge,
ContentEditable, ContentEditable,
@@ -18,6 +12,35 @@ import {
Stat, Stat,
} from '$shared/ui'; } from '$shared/ui';
import { fly } from 'svelte/transition'; import { fly } from 'svelte/transition';
import type {
FontLoadStatus,
UnifiedFont,
} from '../../model/types';
import FontApplicator from '../FontApplicator/FontApplicator.svelte';
/**
* Minimal typography contract this view renders with. The AdjustTypography
* store satisfies it structurally; defining it here keeps the entity decoupled
* from that feature (no entity -> feature import).
*/
interface FontSampleTypography {
/**
* Rendered font size in px
*/
renderedSize: number;
/**
* Numeric font weight
*/
weight: number;
/**
* Line-height multiplier
*/
height: number;
/**
* Letter spacing
*/
spacing: number;
}
interface Props { interface Props {
/** /**
@@ -39,11 +62,15 @@ interface Props {
* @default 0 * @default 0
*/ */
index?: number; index?: number;
/**
* Typography settings to render the sample with. Injected by the composing
* widget (which owns the AdjustTypography store) so this entity view stays
* decoupled from that feature — the same inversion as `status`.
*/
typography: FontSampleTypography;
} }
let { font, status, text = $bindable(), index = 0 }: Props = $props(); let { font, status, text = $bindable(), index = 0, typography }: Props = $props();
const typographySettingsStore = getTypographySettingsStore();
// Extract provider badge with fallback // Extract provider badge with fallback
const providerBadge = $derived( const providerBadge = $derived(
@@ -52,10 +79,10 @@ const providerBadge = $derived(
); );
const stats = $derived([ const stats = $derived([
{ label: 'SZ', value: `${typographySettingsStore.renderedSize}PX` }, { label: 'SZ', value: `${typography.renderedSize}PX` },
{ label: 'WGT', value: `${typographySettingsStore.weight}` }, { label: 'WGT', value: `${typography.weight}` },
{ label: 'LH', value: typographySettingsStore.height?.toFixed(2) }, { label: 'LH', value: typography.height.toFixed(2) },
{ label: 'LTR', value: `${typographySettingsStore.spacing}` }, { label: 'LTR', value: `${typography.spacing}` },
]); ]);
</script> </script>
@@ -73,7 +100,7 @@ const stats = $derived([
min-h-60 min-h-60
rounded-none rounded-none
" "
style:font-weight={typographySettingsStore.weight} style:font-weight={typography.weight}
> >
<!-- ── Header bar ─────────────────────────────────────────────────── --> <!-- ── Header bar ─────────────────────────────────────────────────── -->
<div <div
@@ -141,9 +168,9 @@ const stats = $derived([
<FontApplicator {font} {status}> <FontApplicator {font} {status}>
<ContentEditable <ContentEditable
bind:text bind:text
fontSize={typographySettingsStore.renderedSize} fontSize={typography.renderedSize}
lineHeight={typographySettingsStore.height} lineHeight={typography.height}
letterSpacing={typographySettingsStore.spacing} letterSpacing={typography.spacing}
/> />
</FontApplicator> </FontApplicator>
</div> </div>
+2
View File
@@ -1,7 +1,9 @@
import FontApplicator from './FontApplicator/FontApplicator.svelte'; import FontApplicator from './FontApplicator/FontApplicator.svelte';
import FontSampler from './FontSampler/FontSampler.svelte';
import FontVirtualList from './FontVirtualList/FontVirtualList.svelte'; import FontVirtualList from './FontVirtualList/FontVirtualList.svelte';
export { export {
FontApplicator, FontApplicator,
FontSampler,
FontVirtualList, FontVirtualList,
}; };
-1
View File
@@ -1 +0,0 @@
export { FontSampler } from './ui';
-3
View File
@@ -1,3 +0,0 @@
import FontSampler from './FontSampler/FontSampler.svelte';
export { FontSampler };
@@ -6,6 +6,7 @@
--> -->
<script lang="ts"> <script lang="ts">
import { import {
FontSampler,
FontVirtualList, FontVirtualList,
createFontRowSizeResolver, createFontRowSizeResolver,
getFontCatalog, getFontCatalog,
@@ -15,7 +16,6 @@ import {
TypographyMenu, TypographyMenu,
getTypographySettingsStore, getTypographySettingsStore,
} from '$features/AdjustTypography'; } from '$features/AdjustTypography';
import { FontSampler } from '$features/DisplayFont';
import { throttle } from '$shared/lib/utils'; import { throttle } from '$shared/lib/utils';
import { Skeleton } from '$shared/ui'; import { Skeleton } from '$shared/ui';
import { getLayoutManager } from '../../model'; import { getLayoutManager } from '../../model';
@@ -127,7 +127,7 @@ const fontRowHeight = $derived.by(() =>
getFontStatus reads a $state SvelteMap, so the row stays reactive. getFontStatus reads a $state SvelteMap, so the row stays reactive.
--> -->
{@const status = fontLifecycleManager.getFontStatus(font.id, typographySettingsStore.weight, font.features?.isVariable)} {@const status = fontLifecycleManager.getFontStatus(font.id, typographySettingsStore.weight, font.features?.isVariable)}
<FontSampler bind:text {font} {index} {status} /> <FontSampler bind:text {font} {index} {status} typography={typographySettingsStore} />
{/snippet} {/snippet}
</FontVirtualList> </FontVirtualList>