Compare commits
5 Commits
c5fa159c14
...
dde187e0b2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dde187e0b2 | ||
|
|
5a7c61ade7 | ||
|
|
d2bce85f9c | ||
|
|
e509463911 | ||
|
|
db08f523f6 |
@@ -1,5 +1,5 @@
|
|||||||
import type { ControlModel } from '$shared/lib';
|
import type { ControlModel } from '$shared/lib';
|
||||||
import type { ControlId } from '..';
|
import type { ControlId } from '../types/typography';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Font size constants
|
* Font size constants
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
|
export * from './const/const';
|
||||||
export * from './store';
|
export * from './store';
|
||||||
export * from './types';
|
export * from './types';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Applied fonts manager
|
// Applied fonts manager
|
||||||
export { appliedFontsManager } from './appliedFontsStore/appliedFontsStore.svelte';
|
export * from './appliedFontsStore/appliedFontsStore.svelte';
|
||||||
|
|
||||||
// Batch font store
|
// Batch font store
|
||||||
export { BatchFontStore } from './batchFontStore.svelte';
|
export { BatchFontStore } from './batchFontStore.svelte';
|
||||||
|
|||||||
@@ -33,3 +33,4 @@ export type {
|
|||||||
} from './store';
|
} from './store';
|
||||||
|
|
||||||
export * from './store/appliedFonts';
|
export * from './store/appliedFonts';
|
||||||
|
export * from './typography';
|
||||||
|
|||||||
1
src/entities/Font/model/types/typography.ts
Normal file
1
src/entities/Font/model/types/typography.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export type ControlId = 'font_size' | 'font_weight' | 'line_height' | 'letter_spacing';
|
||||||
@@ -10,6 +10,7 @@ import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
|||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
import { prefersReducedMotion } from 'svelte/motion';
|
import { prefersReducedMotion } from 'svelte/motion';
|
||||||
import {
|
import {
|
||||||
|
DEFAULT_FONT_WEIGHT,
|
||||||
type UnifiedFont,
|
type UnifiedFont,
|
||||||
appliedFontsManager,
|
appliedFontsManager,
|
||||||
} from '../../model';
|
} from '../../model';
|
||||||
@@ -36,7 +37,7 @@ interface Props {
|
|||||||
|
|
||||||
let {
|
let {
|
||||||
font,
|
font,
|
||||||
weight = 400,
|
weight = DEFAULT_FONT_WEIGHT,
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ import {
|
|||||||
type FontLoadRequestConfig,
|
type FontLoadRequestConfig,
|
||||||
type UnifiedFont,
|
type UnifiedFont,
|
||||||
appliedFontsManager,
|
appliedFontsManager,
|
||||||
|
fontStore,
|
||||||
} from '../../model';
|
} from '../../model';
|
||||||
import { fontStore } from '../../model/store';
|
|
||||||
|
|
||||||
interface Props extends
|
interface Props extends
|
||||||
Omit<
|
Omit<
|
||||||
@@ -53,30 +53,42 @@ const isLoading = $derived(
|
|||||||
fontStore.isFetching || fontStore.isLoading,
|
fontStore.isFetching || fontStore.isLoading,
|
||||||
);
|
);
|
||||||
|
|
||||||
function handleInternalVisibleChange(visibleItems: UnifiedFont[]) {
|
let visibleFonts = $state<UnifiedFont[]>([]);
|
||||||
const configs: FontLoadRequestConfig[] = [];
|
|
||||||
|
|
||||||
visibleItems.forEach(item => {
|
|
||||||
const url = getFontUrl(item, weight);
|
|
||||||
|
|
||||||
if (url) {
|
|
||||||
configs.push({
|
|
||||||
id: item.id,
|
|
||||||
name: item.name,
|
|
||||||
weight,
|
|
||||||
url,
|
|
||||||
isVariable: item.features?.isVariable,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Auto-register fonts with the manager
|
|
||||||
appliedFontsManager.touch(configs);
|
|
||||||
|
|
||||||
|
function handleInternalVisibleChange(items: UnifiedFont[]) {
|
||||||
|
visibleFonts = items;
|
||||||
// Forward the call to any external listener
|
// Forward the call to any external listener
|
||||||
// onVisibleItemsChange?.(visibleItems);
|
onVisibleItemsChange?.(items);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Re-touch whenever visible set or weight changes — fixes weight-change gap
|
||||||
|
$effect(() => {
|
||||||
|
const configs: FontLoadRequestConfig[] = visibleFonts.flatMap(item => {
|
||||||
|
const url = getFontUrl(item, weight);
|
||||||
|
if (!url) return [];
|
||||||
|
return [{ id: item.id, name: item.name, weight, url, isVariable: item.features?.isVariable }];
|
||||||
|
});
|
||||||
|
if (configs.length > 0) {
|
||||||
|
appliedFontsManager.touch(configs);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Pin visible fonts so the eviction policy never removes on-screen entries.
|
||||||
|
// Cleanup captures the snapshot values, so a weight change unpins the old
|
||||||
|
// weight before pinning the new one.
|
||||||
|
$effect(() => {
|
||||||
|
const w = weight;
|
||||||
|
const fonts = visibleFonts;
|
||||||
|
for (const f of fonts) {
|
||||||
|
appliedFontsManager.pin(f.id, w, f.features?.isVariable);
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
for (const f of fonts) {
|
||||||
|
appliedFontsManager.unpin(f.id, w, f.features?.isVariable);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load more fonts by moving to the next page
|
* Load more fonts by moving to the next page
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,28 +1,6 @@
|
|||||||
export { TypographyMenu } from './ui';
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
type ControlId,
|
createTypographySettingsManager,
|
||||||
controlManager,
|
type TypographySettingsManager,
|
||||||
DEFAULT_FONT_SIZE,
|
|
||||||
DEFAULT_FONT_WEIGHT,
|
|
||||||
DEFAULT_LETTER_SPACING,
|
|
||||||
DEFAULT_LINE_HEIGHT,
|
|
||||||
DEFAULT_TYPOGRAPHY_CONTROLS_DATA,
|
|
||||||
FONT_SIZE_STEP,
|
|
||||||
FONT_WEIGHT_STEP,
|
|
||||||
LINE_HEIGHT_STEP,
|
|
||||||
MAX_FONT_SIZE,
|
|
||||||
MAX_FONT_WEIGHT,
|
|
||||||
MAX_LINE_HEIGHT,
|
|
||||||
MIN_FONT_SIZE,
|
|
||||||
MIN_FONT_WEIGHT,
|
|
||||||
MIN_LINE_HEIGHT,
|
|
||||||
MULTIPLIER_L,
|
|
||||||
MULTIPLIER_M,
|
|
||||||
MULTIPLIER_S,
|
|
||||||
} from './model';
|
|
||||||
|
|
||||||
export {
|
|
||||||
createTypographyControlManager,
|
|
||||||
type TypographyControlManager,
|
|
||||||
} from './lib';
|
} from './lib';
|
||||||
|
export { typographySettingsStore } from './model';
|
||||||
|
export { TypographyMenu } from './ui';
|
||||||
|
|||||||
@@ -10,6 +10,13 @@
|
|||||||
* when displaying/editing, but the base size is what's stored.
|
* when displaying/editing, but the base size is what's stored.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
type ControlId,
|
||||||
|
DEFAULT_FONT_SIZE,
|
||||||
|
DEFAULT_FONT_WEIGHT,
|
||||||
|
DEFAULT_LETTER_SPACING,
|
||||||
|
DEFAULT_LINE_HEIGHT,
|
||||||
|
} from '$entities/Font';
|
||||||
import {
|
import {
|
||||||
type ControlDataModel,
|
type ControlDataModel,
|
||||||
type ControlModel,
|
type ControlModel,
|
||||||
@@ -19,13 +26,6 @@ import {
|
|||||||
createTypographyControl,
|
createTypographyControl,
|
||||||
} from '$shared/lib';
|
} from '$shared/lib';
|
||||||
import { SvelteMap } from 'svelte/reactivity';
|
import { SvelteMap } from 'svelte/reactivity';
|
||||||
import {
|
|
||||||
type ControlId,
|
|
||||||
DEFAULT_FONT_SIZE,
|
|
||||||
DEFAULT_FONT_WEIGHT,
|
|
||||||
DEFAULT_LETTER_SPACING,
|
|
||||||
DEFAULT_LINE_HEIGHT,
|
|
||||||
} from '../../model';
|
|
||||||
|
|
||||||
type ControlOnlyFields<T extends string = string> = Omit<ControlModel<T>, keyof ControlDataModel>;
|
type ControlOnlyFields<T extends string = string> = Omit<ControlModel<T>, keyof ControlDataModel>;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
/** @vitest-environment jsdom */
|
/** @vitest-environment jsdom */
|
||||||
|
import {
|
||||||
|
DEFAULT_FONT_SIZE,
|
||||||
|
DEFAULT_FONT_WEIGHT,
|
||||||
|
DEFAULT_LETTER_SPACING,
|
||||||
|
DEFAULT_LINE_HEIGHT,
|
||||||
|
DEFAULT_TYPOGRAPHY_CONTROLS_DATA,
|
||||||
|
} from '$entities/Font';
|
||||||
import {
|
import {
|
||||||
afterEach,
|
afterEach,
|
||||||
beforeEach,
|
beforeEach,
|
||||||
@@ -7,13 +14,6 @@ import {
|
|||||||
it,
|
it,
|
||||||
vi,
|
vi,
|
||||||
} from 'vitest';
|
} from 'vitest';
|
||||||
import {
|
|
||||||
DEFAULT_FONT_SIZE,
|
|
||||||
DEFAULT_FONT_WEIGHT,
|
|
||||||
DEFAULT_LETTER_SPACING,
|
|
||||||
DEFAULT_LINE_HEIGHT,
|
|
||||||
DEFAULT_TYPOGRAPHY_CONTROLS_DATA,
|
|
||||||
} from '../../model';
|
|
||||||
import {
|
import {
|
||||||
type TypographySettings,
|
type TypographySettings,
|
||||||
TypographySettingsManager,
|
TypographySettingsManager,
|
||||||
|
|||||||
@@ -1,24 +1 @@
|
|||||||
export {
|
export { typographySettingsStore } from './state/typographySettingsStore';
|
||||||
DEFAULT_FONT_SIZE,
|
|
||||||
DEFAULT_FONT_WEIGHT,
|
|
||||||
DEFAULT_LETTER_SPACING,
|
|
||||||
DEFAULT_LINE_HEIGHT,
|
|
||||||
DEFAULT_TYPOGRAPHY_CONTROLS_DATA,
|
|
||||||
FONT_SIZE_STEP,
|
|
||||||
FONT_WEIGHT_STEP,
|
|
||||||
LINE_HEIGHT_STEP,
|
|
||||||
MAX_FONT_SIZE,
|
|
||||||
MAX_FONT_WEIGHT,
|
|
||||||
MAX_LINE_HEIGHT,
|
|
||||||
MIN_FONT_SIZE,
|
|
||||||
MIN_FONT_WEIGHT,
|
|
||||||
MIN_LINE_HEIGHT,
|
|
||||||
MULTIPLIER_L,
|
|
||||||
MULTIPLIER_M,
|
|
||||||
MULTIPLIER_S,
|
|
||||||
} from './const/const';
|
|
||||||
|
|
||||||
export {
|
|
||||||
type ControlId,
|
|
||||||
typographySettingsStore,
|
|
||||||
} from './state/typographySettingsStore';
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
|
import { DEFAULT_TYPOGRAPHY_CONTROLS_DATA } from '$entities/Font';
|
||||||
import { createTypographySettingsManager } from '../../lib';
|
import { createTypographySettingsManager } from '../../lib';
|
||||||
import { DEFAULT_TYPOGRAPHY_CONTROLS_DATA } from '../const/const';
|
|
||||||
|
|
||||||
export type ControlId = 'font_size' | 'font_weight' | 'line_height' | 'letter_spacing';
|
|
||||||
export const typographySettingsStore = createTypographySettingsManager(
|
export const typographySettingsStore = createTypographySettingsManager(
|
||||||
DEFAULT_TYPOGRAPHY_CONTROLS_DATA,
|
DEFAULT_TYPOGRAPHY_CONTROLS_DATA,
|
||||||
'glyphdiff:comparison:typography',
|
'glyphdiff:comparison:typography',
|
||||||
|
|||||||
@@ -6,6 +6,11 @@
|
|||||||
Desktop: inline bar with combo controls.
|
Desktop: inline bar with combo controls.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import {
|
||||||
|
MULTIPLIER_L,
|
||||||
|
MULTIPLIER_M,
|
||||||
|
MULTIPLIER_S,
|
||||||
|
} from '$entities/Font';
|
||||||
import type { ResponsiveManager } from '$shared/lib';
|
import type { ResponsiveManager } from '$shared/lib';
|
||||||
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
||||||
import {
|
import {
|
||||||
@@ -19,12 +24,7 @@ import { Popover } from 'bits-ui';
|
|||||||
import { getContext } from 'svelte';
|
import { getContext } from 'svelte';
|
||||||
import { cubicOut } from 'svelte/easing';
|
import { cubicOut } from 'svelte/easing';
|
||||||
import { fly } from 'svelte/transition';
|
import { fly } from 'svelte/transition';
|
||||||
import {
|
import { typographySettingsStore } from '../../model';
|
||||||
MULTIPLIER_L,
|
|
||||||
MULTIPLIER_M,
|
|
||||||
MULTIPLIER_S,
|
|
||||||
typographySettingsStore,
|
|
||||||
} from '../../model';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -134,6 +134,19 @@ export class ComparisonStore {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Effect 4: Pin fontA/fontB so eviction never removes on-screen fonts
|
||||||
|
$effect(() => {
|
||||||
|
const fa = this.#fontA;
|
||||||
|
const fb = this.#fontB;
|
||||||
|
const w = typographySettingsStore.weight;
|
||||||
|
if (fa) appliedFontsManager.pin(fa.id, w, fa.features?.isVariable);
|
||||||
|
if (fb) appliedFontsManager.pin(fb.id, w, fb.features?.isVariable);
|
||||||
|
return () => {
|
||||||
|
if (fa) appliedFontsManager.unpin(fa.id, w, fa.features?.isVariable);
|
||||||
|
if (fb) appliedFontsManager.unpin(fb.id, w, fb.features?.isVariable);
|
||||||
|
};
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,15 +53,19 @@ vi.mock('$shared/lib/helpers/createPersistentStore/createPersistentStore.svelte'
|
|||||||
|
|
||||||
// ── $entities/Font mock — keep real BatchFontStore, stub singletons ───────────
|
// ── $entities/Font mock — keep real BatchFontStore, stub singletons ───────────
|
||||||
|
|
||||||
vi.mock('$entities/Font', async () => {
|
vi.mock('$entities/Font', async importOriginal => {
|
||||||
|
const actual = await importOriginal<typeof import('$entities/Font')>();
|
||||||
const { BatchFontStore } = await import(
|
const { BatchFontStore } = await import(
|
||||||
'$entities/Font/model/store/batchFontStore.svelte'
|
'$entities/Font/model/store/batchFontStore.svelte'
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
|
...actual,
|
||||||
BatchFontStore,
|
BatchFontStore,
|
||||||
fontStore: { fonts: [] },
|
fontStore: { fonts: [] },
|
||||||
appliedFontsManager: {
|
appliedFontsManager: {
|
||||||
touch: vi.fn(),
|
touch: vi.fn(),
|
||||||
|
pin: vi.fn(),
|
||||||
|
unpin: vi.fn(),
|
||||||
getFontStatus: vi.fn(),
|
getFontStatus: vi.fn(),
|
||||||
ready: vi.fn(() => Promise.resolve()),
|
ready: vi.fn(() => Promise.resolve()),
|
||||||
},
|
},
|
||||||
@@ -80,9 +84,20 @@ vi.mock('$features/SetupFont', () => ({
|
|||||||
})),
|
})),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock('$features/SetupFont/model', () => ({
|
||||||
|
typographySettingsStore: {
|
||||||
|
weight: 400,
|
||||||
|
renderedSize: 48,
|
||||||
|
reset: vi.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
// ── Imports (after mocks) ─────────────────────────────────────────────────────
|
// ── Imports (after mocks) ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
import { fontStore } from '$entities/Font';
|
import {
|
||||||
|
appliedFontsManager,
|
||||||
|
fontStore,
|
||||||
|
} from '$entities/Font';
|
||||||
import * as proxyFonts from '$entities/Font/api/proxy/proxyFonts';
|
import * as proxyFonts from '$entities/Font/api/proxy/proxyFonts';
|
||||||
import { ComparisonStore } from './comparisonStore.svelte';
|
import { ComparisonStore } from './comparisonStore.svelte';
|
||||||
|
|
||||||
@@ -209,4 +224,55 @@ describe('ComparisonStore', () => {
|
|||||||
expect(store.fontB).toBeUndefined();
|
expect(store.fontB).toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Pin / Unpin ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('Pin / Unpin (eviction guard)', () => {
|
||||||
|
it('pins fontA and fontB when they are loaded', async () => {
|
||||||
|
mockStorage._value.fontAId = mockFontA.id;
|
||||||
|
mockStorage._value.fontBId = mockFontB.id;
|
||||||
|
vi.spyOn(proxyFonts, 'fetchFontsByIds').mockResolvedValue([mockFontA, mockFontB]);
|
||||||
|
|
||||||
|
new ComparisonStore();
|
||||||
|
|
||||||
|
await vi.waitFor(() => {
|
||||||
|
expect(appliedFontsManager.pin).toHaveBeenCalledWith(
|
||||||
|
mockFontA.id,
|
||||||
|
400,
|
||||||
|
mockFontA.features?.isVariable,
|
||||||
|
);
|
||||||
|
expect(appliedFontsManager.pin).toHaveBeenCalledWith(
|
||||||
|
mockFontB.id,
|
||||||
|
400,
|
||||||
|
mockFontB.features?.isVariable,
|
||||||
|
);
|
||||||
|
}, { timeout: 2000 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('unpins the old font when fontA is replaced', async () => {
|
||||||
|
mockStorage._value.fontAId = mockFontA.id;
|
||||||
|
mockStorage._value.fontBId = mockFontB.id;
|
||||||
|
vi.spyOn(proxyFonts, 'fetchFontsByIds').mockResolvedValue([mockFontA, mockFontB]);
|
||||||
|
|
||||||
|
const store = new ComparisonStore();
|
||||||
|
await vi.waitFor(() => expect(store.fontA?.id).toBe(mockFontA.id), { timeout: 2000 });
|
||||||
|
|
||||||
|
const mockFontC: typeof mockFontA = { ...mockFontA, id: 'playfair', name: 'Playfair Display' };
|
||||||
|
vi.spyOn(proxyFonts, 'fetchFontsByIds').mockResolvedValue([mockFontC, mockFontB]);
|
||||||
|
store.fontA = mockFontC;
|
||||||
|
|
||||||
|
await vi.waitFor(() => {
|
||||||
|
expect(appliedFontsManager.unpin).toHaveBeenCalledWith(
|
||||||
|
mockFontA.id,
|
||||||
|
400,
|
||||||
|
mockFontA.features?.isVariable,
|
||||||
|
);
|
||||||
|
expect(appliedFontsManager.pin).toHaveBeenCalledWith(
|
||||||
|
mockFontC.id,
|
||||||
|
400,
|
||||||
|
mockFontC.features?.isVariable,
|
||||||
|
);
|
||||||
|
}, { timeout: 2000 });
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,11 +4,11 @@
|
|||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {
|
import {
|
||||||
|
DEFAULT_FONT_WEIGHT,
|
||||||
FontApplicator,
|
FontApplicator,
|
||||||
FontVirtualList,
|
FontVirtualList,
|
||||||
type UnifiedFont,
|
type UnifiedFont,
|
||||||
} from '$entities/Font';
|
} from '$entities/Font';
|
||||||
import { typographySettingsStore } from '$features/SetupFont';
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Label,
|
Label,
|
||||||
@@ -19,8 +19,6 @@ import { crossfade } from 'svelte/transition';
|
|||||||
import { comparisonStore } from '../../model';
|
import { comparisonStore } from '../../model';
|
||||||
|
|
||||||
const side = $derived(comparisonStore.side);
|
const side = $derived(comparisonStore.side);
|
||||||
const typography = $derived(typographySettingsStore);
|
|
||||||
|
|
||||||
let prevIndexA: number | null = null;
|
let prevIndexA: number | null = null;
|
||||||
let prevIndexB: number | null = null;
|
let prevIndexB: number | null = null;
|
||||||
let selectedIndexA: number | null = null;
|
let selectedIndexA: number | null = null;
|
||||||
@@ -80,7 +78,7 @@ $effect(() => {
|
|||||||
</div>
|
</div>
|
||||||
<FontVirtualList
|
<FontVirtualList
|
||||||
data-font-list
|
data-font-list
|
||||||
weight={typography.weight}
|
weight={DEFAULT_FONT_WEIGHT}
|
||||||
itemHeight={45}
|
itemHeight={45}
|
||||||
class="bg-transparent min-h-0 h-full scroll-stable pr-4"
|
class="bg-transparent min-h-0 h-full scroll-stable pr-4"
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user