Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 42bcc915c7 | |||
| c72b51b1c7 | |||
| 6888f67f14 | |||
| a651d3d16f | |||
| 4d8dcf52e0 | |||
| 907145c655 | |||
| e49148008b | |||
| c613d4cf88 | |||
| 7834c7cbf2 | |||
| 4640d6e521 |
+3
-1
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"plugins": ["import"],
|
||||||
"categories": {
|
"categories": {
|
||||||
"correctness": "error",
|
"correctness": "error",
|
||||||
"suspicious": "warn",
|
"suspicious": "warn",
|
||||||
@@ -22,6 +23,7 @@
|
|||||||
"rules": {
|
"rules": {
|
||||||
"no-console": "off",
|
"no-console": "off",
|
||||||
"no-debugger": "error",
|
"no-debugger": "error",
|
||||||
"no-alert": "warn"
|
"no-alert": "warn",
|
||||||
|
"import/no-cycle": "error"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
import type { ControlModel } from '$shared/lib';
|
|
||||||
import type { ControlId } from '../types/typography';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Font size constants
|
* Font size constants
|
||||||
*/
|
*/
|
||||||
@@ -33,60 +30,6 @@ export const MIN_LETTER_SPACING = -0.1;
|
|||||||
export const MAX_LETTER_SPACING = 0.5;
|
export const MAX_LETTER_SPACING = 0.5;
|
||||||
export const LETTER_SPACING_STEP = 0.01;
|
export const LETTER_SPACING_STEP = 0.01;
|
||||||
|
|
||||||
export const DEFAULT_TYPOGRAPHY_CONTROLS_DATA: ControlModel<ControlId>[] = [
|
|
||||||
{
|
|
||||||
id: 'font_size',
|
|
||||||
value: DEFAULT_FONT_SIZE,
|
|
||||||
max: MAX_FONT_SIZE,
|
|
||||||
min: MIN_FONT_SIZE,
|
|
||||||
step: FONT_SIZE_STEP,
|
|
||||||
|
|
||||||
increaseLabel: 'Increase Font Size',
|
|
||||||
decreaseLabel: 'Decrease Font Size',
|
|
||||||
controlLabel: 'Size',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'font_weight',
|
|
||||||
value: DEFAULT_FONT_WEIGHT,
|
|
||||||
max: MAX_FONT_WEIGHT,
|
|
||||||
min: MIN_FONT_WEIGHT,
|
|
||||||
step: FONT_WEIGHT_STEP,
|
|
||||||
|
|
||||||
increaseLabel: 'Increase Font Weight',
|
|
||||||
decreaseLabel: 'Decrease Font Weight',
|
|
||||||
controlLabel: 'Weight',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'line_height',
|
|
||||||
value: DEFAULT_LINE_HEIGHT,
|
|
||||||
max: MAX_LINE_HEIGHT,
|
|
||||||
min: MIN_LINE_HEIGHT,
|
|
||||||
step: LINE_HEIGHT_STEP,
|
|
||||||
|
|
||||||
increaseLabel: 'Increase Line Height',
|
|
||||||
decreaseLabel: 'Decrease Line Height',
|
|
||||||
controlLabel: 'Leading',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'letter_spacing',
|
|
||||||
value: DEFAULT_LETTER_SPACING,
|
|
||||||
max: MAX_LETTER_SPACING,
|
|
||||||
min: MIN_LETTER_SPACING,
|
|
||||||
step: LETTER_SPACING_STEP,
|
|
||||||
|
|
||||||
increaseLabel: 'Increase Letter Spacing',
|
|
||||||
decreaseLabel: 'Decrease Letter Spacing',
|
|
||||||
controlLabel: 'Tracking',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Font size multipliers
|
|
||||||
*/
|
|
||||||
export const MULTIPLIER_S = 0.5;
|
|
||||||
export const MULTIPLIER_M = 0.75;
|
|
||||||
export const MULTIPLIER_L = 1;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Index value for items not yet loaded in a virtualized list.
|
* Index value for items not yet loaded in a virtualized list.
|
||||||
* Treated as being at the very bottom of the infinite scroll.
|
* Treated as being at the very bottom of the infinite scroll.
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import {
|
|||||||
generateMixedCategoryFonts,
|
generateMixedCategoryFonts,
|
||||||
generateMockFonts,
|
generateMockFonts,
|
||||||
} from '$entities/Font/testing';
|
} from '$entities/Font/testing';
|
||||||
import { QueryClient } from '@tanstack/query-core';
|
|
||||||
import { flushSync } from 'svelte';
|
import { flushSync } from 'svelte';
|
||||||
import {
|
import {
|
||||||
afterEach,
|
afterEach,
|
||||||
@@ -20,6 +19,13 @@ import type { UnifiedFont } from '../../types';
|
|||||||
import { FontCatalogStore } from './fontCatalogStore.svelte';
|
import { FontCatalogStore } from './fontCatalogStore.svelte';
|
||||||
|
|
||||||
vi.mock('$shared/api/queryClient', async importOriginal => {
|
vi.mock('$shared/api/queryClient', async importOriginal => {
|
||||||
|
/**
|
||||||
|
* Import QueryClient inside the factory rather than referencing the top-level binding.
|
||||||
|
* A hoisted vi.mock factory that touches a module-level import can hit that import
|
||||||
|
* before it is initialized (ReferenceError) when the import sits in a circular/eager
|
||||||
|
* barrel chain — which it now does via $shared/lib → BaseQueryStore → query-core.
|
||||||
|
*/
|
||||||
|
const { QueryClient } = await import('@tanstack/query-core');
|
||||||
const actual = await importOriginal<typeof import('$shared/api/queryClient')>();
|
const actual = await importOriginal<typeof import('$shared/api/queryClient')>();
|
||||||
return {
|
return {
|
||||||
...actual,
|
...actual,
|
||||||
|
|||||||
+5
-5
@@ -1,14 +1,14 @@
|
|||||||
|
import { fontKeys } from '$shared/api/queryKeys';
|
||||||
|
import { BaseQueryStore } from '$shared/lib/helpers/BaseQueryStore/BaseQueryStore.svelte';
|
||||||
import {
|
import {
|
||||||
fetchFontsByIds,
|
fetchFontsByIds,
|
||||||
seedFontCache,
|
seedFontCache,
|
||||||
} from '$entities/Font/api/proxy/proxyFonts';
|
} from '../../../api/proxy/proxyFonts';
|
||||||
import {
|
import {
|
||||||
FontNetworkError,
|
FontNetworkError,
|
||||||
FontResponseError,
|
FontResponseError,
|
||||||
} from '$entities/Font/lib/errors/errors';
|
} from '../../../lib/errors/errors';
|
||||||
import type { UnifiedFont } from '$entities/Font/model/types';
|
import type { UnifiedFont } from '../../types';
|
||||||
import { fontKeys } from '$shared/api/queryKeys';
|
|
||||||
import { BaseQueryStore } from '$shared/lib/helpers/BaseQueryStore.svelte';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal fetcher that seeds the cache and handles error wrapping.
|
* Internal fetcher that seeds the cache and handles error wrapping.
|
||||||
+5
-5
@@ -1,8 +1,3 @@
|
|||||||
import * as api from '$entities/Font/api/proxy/proxyFonts';
|
|
||||||
import {
|
|
||||||
FontNetworkError,
|
|
||||||
FontResponseError,
|
|
||||||
} from '$entities/Font/lib/errors/errors';
|
|
||||||
import { queryClient } from '$shared/api/queryClient';
|
import { queryClient } from '$shared/api/queryClient';
|
||||||
import { fontKeys } from '$shared/api/queryKeys';
|
import { fontKeys } from '$shared/api/queryKeys';
|
||||||
import {
|
import {
|
||||||
@@ -12,6 +7,11 @@ import {
|
|||||||
it,
|
it,
|
||||||
vi,
|
vi,
|
||||||
} from 'vitest';
|
} from 'vitest';
|
||||||
|
import * as api from '../../../api/proxy/proxyFonts';
|
||||||
|
import {
|
||||||
|
FontNetworkError,
|
||||||
|
FontResponseError,
|
||||||
|
} from '../../../lib/errors/errors';
|
||||||
import { FontsByIdsStore } from './fontsByIdsStore.svelte';
|
import { FontsByIdsStore } from './fontsByIdsStore.svelte';
|
||||||
|
|
||||||
describe('FontsByIdsStore', () => {
|
describe('FontsByIdsStore', () => {
|
||||||
@@ -7,3 +7,6 @@ export {
|
|||||||
FontCatalogStore,
|
FontCatalogStore,
|
||||||
fontCatalogStore,
|
fontCatalogStore,
|
||||||
} from './fontCatalogStore/fontCatalogStore.svelte';
|
} from './fontCatalogStore/fontCatalogStore.svelte';
|
||||||
|
|
||||||
|
// Batch fetch by IDs (detail-cache seeding)
|
||||||
|
export { FontsByIdsStore } from './fontsByIdsStore/fontsByIdsStore.svelte';
|
||||||
|
|||||||
@@ -24,4 +24,3 @@ export type {
|
|||||||
} from './store';
|
} from './store';
|
||||||
|
|
||||||
export * from './store/fontLifecycle';
|
export * from './store/fontLifecycle';
|
||||||
export * from './typography';
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
export type ControlId = 'font_size' | 'font_weight' | 'line_height' | 'letter_spacing';
|
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
export {
|
export {
|
||||||
createTypographySettingsStore,
|
createTypographySettingsStore,
|
||||||
|
MULTIPLIER_L,
|
||||||
|
MULTIPLIER_M,
|
||||||
|
MULTIPLIER_S,
|
||||||
type TypographySettingsStore,
|
type TypographySettingsStore,
|
||||||
typographySettingsStore,
|
typographySettingsStore,
|
||||||
} from './model';
|
} from './model';
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import {
|
||||||
|
DEFAULT_FONT_SIZE,
|
||||||
|
DEFAULT_FONT_WEIGHT,
|
||||||
|
DEFAULT_LETTER_SPACING,
|
||||||
|
DEFAULT_LINE_HEIGHT,
|
||||||
|
FONT_SIZE_STEP,
|
||||||
|
FONT_WEIGHT_STEP,
|
||||||
|
LETTER_SPACING_STEP,
|
||||||
|
LINE_HEIGHT_STEP,
|
||||||
|
MAX_FONT_SIZE,
|
||||||
|
MAX_FONT_WEIGHT,
|
||||||
|
MAX_LETTER_SPACING,
|
||||||
|
MAX_LINE_HEIGHT,
|
||||||
|
MIN_FONT_SIZE,
|
||||||
|
MIN_FONT_WEIGHT,
|
||||||
|
MIN_LETTER_SPACING,
|
||||||
|
MIN_LINE_HEIGHT,
|
||||||
|
} from '$entities/Font';
|
||||||
|
import type {
|
||||||
|
ControlId,
|
||||||
|
ControlModel,
|
||||||
|
} from '../types/typography';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Responsive font-size scaling factors applied by typographySettingsStore.
|
||||||
|
*/
|
||||||
|
export const MULTIPLIER_S = 0.5;
|
||||||
|
export const MULTIPLIER_M = 0.75;
|
||||||
|
export const MULTIPLIER_L = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default control definitions seeding the typography settings store.
|
||||||
|
* Composed from the font-render ranges/defaults owned by the Font entity.
|
||||||
|
*/
|
||||||
|
export const DEFAULT_TYPOGRAPHY_CONTROLS_DATA: ControlModel<ControlId>[] = [
|
||||||
|
{
|
||||||
|
id: 'font_size',
|
||||||
|
value: DEFAULT_FONT_SIZE,
|
||||||
|
max: MAX_FONT_SIZE,
|
||||||
|
min: MIN_FONT_SIZE,
|
||||||
|
step: FONT_SIZE_STEP,
|
||||||
|
increaseLabel: 'Increase Font Size',
|
||||||
|
decreaseLabel: 'Decrease Font Size',
|
||||||
|
controlLabel: 'Size',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'font_weight',
|
||||||
|
value: DEFAULT_FONT_WEIGHT,
|
||||||
|
max: MAX_FONT_WEIGHT,
|
||||||
|
min: MIN_FONT_WEIGHT,
|
||||||
|
step: FONT_WEIGHT_STEP,
|
||||||
|
increaseLabel: 'Increase Font Weight',
|
||||||
|
decreaseLabel: 'Decrease Font Weight',
|
||||||
|
controlLabel: 'Weight',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'line_height',
|
||||||
|
value: DEFAULT_LINE_HEIGHT,
|
||||||
|
max: MAX_LINE_HEIGHT,
|
||||||
|
min: MIN_LINE_HEIGHT,
|
||||||
|
step: LINE_HEIGHT_STEP,
|
||||||
|
increaseLabel: 'Increase Line Height',
|
||||||
|
decreaseLabel: 'Decrease Line Height',
|
||||||
|
controlLabel: 'Leading',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'letter_spacing',
|
||||||
|
value: DEFAULT_LETTER_SPACING,
|
||||||
|
max: MAX_LETTER_SPACING,
|
||||||
|
min: MIN_LETTER_SPACING,
|
||||||
|
step: LETTER_SPACING_STEP,
|
||||||
|
increaseLabel: 'Increase Letter Spacing',
|
||||||
|
decreaseLabel: 'Decrease Letter Spacing',
|
||||||
|
controlLabel: 'Tracking',
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -1,3 +1,8 @@
|
|||||||
|
export {
|
||||||
|
MULTIPLIER_L,
|
||||||
|
MULTIPLIER_M,
|
||||||
|
MULTIPLIER_S,
|
||||||
|
} from './const/const';
|
||||||
export {
|
export {
|
||||||
createTypographySettingsStore,
|
createTypographySettingsStore,
|
||||||
type TypographySettingsStore,
|
type TypographySettingsStore,
|
||||||
|
|||||||
+9
-8
@@ -11,22 +11,23 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
type ControlId,
|
|
||||||
DEFAULT_FONT_SIZE,
|
DEFAULT_FONT_SIZE,
|
||||||
DEFAULT_FONT_WEIGHT,
|
DEFAULT_FONT_WEIGHT,
|
||||||
DEFAULT_LETTER_SPACING,
|
DEFAULT_LETTER_SPACING,
|
||||||
DEFAULT_LINE_HEIGHT,
|
DEFAULT_LINE_HEIGHT,
|
||||||
DEFAULT_TYPOGRAPHY_CONTROLS_DATA,
|
|
||||||
} from '$entities/Font';
|
} from '$entities/Font';
|
||||||
import {
|
import {
|
||||||
type ControlDataModel,
|
|
||||||
type ControlModel,
|
|
||||||
type PersistentStore,
|
type PersistentStore,
|
||||||
type TypographyControl,
|
|
||||||
createPersistentStore,
|
createPersistentStore,
|
||||||
createTypographyControl,
|
|
||||||
} from '$shared/lib';
|
} from '$shared/lib';
|
||||||
|
import type { NumericControl } from '$shared/ui';
|
||||||
import { SvelteMap } from 'svelte/reactivity';
|
import { SvelteMap } from 'svelte/reactivity';
|
||||||
|
import { DEFAULT_TYPOGRAPHY_CONTROLS_DATA } from '../../const/const';
|
||||||
|
import type {
|
||||||
|
ControlId,
|
||||||
|
ControlModel,
|
||||||
|
} from '../../types/typography';
|
||||||
|
import { createTypographyControl } from '../../typographyControl/createTypographyControl.svelte';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Epsilon for detecting "significant" base-size changes when reconciling
|
* Epsilon for detecting "significant" base-size changes when reconciling
|
||||||
@@ -36,7 +37,7 @@ import { SvelteMap } from 'svelte/reactivity';
|
|||||||
*/
|
*/
|
||||||
const BASE_SIZE_EPSILON = 0.01;
|
const BASE_SIZE_EPSILON = 0.01;
|
||||||
|
|
||||||
type ControlOnlyFields<T extends string = string> = Omit<ControlModel<T>, keyof ControlDataModel>;
|
type ControlOnlyFields<T extends string = string> = Omit<ControlModel<T>, 'value' | 'min' | 'max' | 'step'>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A control with its associated instance
|
* A control with its associated instance
|
||||||
@@ -45,7 +46,7 @@ export interface Control extends ControlOnlyFields<ControlId> {
|
|||||||
/**
|
/**
|
||||||
* The reactive typography control instance
|
* The reactive typography control instance
|
||||||
*/
|
*/
|
||||||
instance: TypographyControl;
|
instance: NumericControl;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,6 @@ import {
|
|||||||
DEFAULT_FONT_WEIGHT,
|
DEFAULT_FONT_WEIGHT,
|
||||||
DEFAULT_LETTER_SPACING,
|
DEFAULT_LETTER_SPACING,
|
||||||
DEFAULT_LINE_HEIGHT,
|
DEFAULT_LINE_HEIGHT,
|
||||||
DEFAULT_TYPOGRAPHY_CONTROLS_DATA,
|
|
||||||
} from '$entities/Font';
|
} from '$entities/Font';
|
||||||
import {
|
import {
|
||||||
beforeEach,
|
beforeEach,
|
||||||
@@ -15,6 +14,7 @@ import {
|
|||||||
it,
|
it,
|
||||||
vi,
|
vi,
|
||||||
} from 'vitest';
|
} from 'vitest';
|
||||||
|
import { DEFAULT_TYPOGRAPHY_CONTROLS_DATA } from '../../const/const';
|
||||||
import {
|
import {
|
||||||
type TypographySettings,
|
type TypographySettings,
|
||||||
TypographySettingsStore,
|
TypographySettingsStore,
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import type {
|
||||||
|
ControlLabels,
|
||||||
|
NumericControl,
|
||||||
|
} from '$shared/ui';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Identifiers for the adjustable typography axes
|
||||||
|
*/
|
||||||
|
export type ControlId = 'font_size' | 'font_weight' | 'line_height' | 'letter_spacing';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Static configuration for one typography control.
|
||||||
|
*
|
||||||
|
* Derived from the SSOT contract types — declares no fields of its own beyond
|
||||||
|
* the domain `id`. Bounds come from NumericControl, labels from ControlLabels.
|
||||||
|
*
|
||||||
|
* @template T - Control identifier type
|
||||||
|
*/
|
||||||
|
export type ControlModel<T extends string = string> =
|
||||||
|
& Pick<NumericControl, 'value' | 'min' | 'max' | 'step'>
|
||||||
|
& ControlLabels
|
||||||
|
& {
|
||||||
|
/**
|
||||||
|
* Unique identifier for the control
|
||||||
|
*/
|
||||||
|
id: T;
|
||||||
|
};
|
||||||
+67
@@ -0,0 +1,67 @@
|
|||||||
|
/**
|
||||||
|
* Bounded numeric control for typography settings.
|
||||||
|
*
|
||||||
|
* Produces a reactive control that clamps to [min, max] and rounds to step.
|
||||||
|
* Implements the NumericControl contract that ComboControl renders.
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
clampNumber,
|
||||||
|
roundToStepPrecision,
|
||||||
|
} from '$shared/lib/utils';
|
||||||
|
import type { NumericControl } from '$shared/ui';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bounds + initial value seed for a control
|
||||||
|
*/
|
||||||
|
type ControlSeed = Pick<NumericControl, 'value' | 'min' | 'max' | 'step'>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a reactive bounded numeric control.
|
||||||
|
*
|
||||||
|
* @param initialState - Initial value and bounds
|
||||||
|
* @returns A NumericControl whose value is always clamped and step-rounded
|
||||||
|
*/
|
||||||
|
export function createTypographyControl(initialState: ControlSeed): NumericControl {
|
||||||
|
let value = $state(initialState.value);
|
||||||
|
let max = $state(initialState.max);
|
||||||
|
let min = $state(initialState.min);
|
||||||
|
let step = $state(initialState.step);
|
||||||
|
|
||||||
|
const { isAtMax, isAtMin } = $derived({
|
||||||
|
isAtMax: value >= max,
|
||||||
|
isAtMin: value <= min,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
get value() {
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
set value(newValue) {
|
||||||
|
const rounded = roundToStepPrecision(clampNumber(newValue, min, max), step);
|
||||||
|
if (value !== rounded) {
|
||||||
|
value = rounded;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
get max() {
|
||||||
|
return max;
|
||||||
|
},
|
||||||
|
get min() {
|
||||||
|
return min;
|
||||||
|
},
|
||||||
|
get step() {
|
||||||
|
return step;
|
||||||
|
},
|
||||||
|
get isAtMax() {
|
||||||
|
return isAtMax;
|
||||||
|
},
|
||||||
|
get isAtMin() {
|
||||||
|
return isAtMin;
|
||||||
|
},
|
||||||
|
increase() {
|
||||||
|
value = roundToStepPrecision(clampNumber(value + step, min, max), step);
|
||||||
|
},
|
||||||
|
decrease() {
|
||||||
|
value = roundToStepPrecision(clampNumber(value - step, min, max), step);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
+3
-5
@@ -1,12 +1,10 @@
|
|||||||
import {
|
import type { NumericControl } from '$shared/ui';
|
||||||
type TypographyControl,
|
|
||||||
createTypographyControl,
|
|
||||||
} from '$shared/lib';
|
|
||||||
import {
|
import {
|
||||||
describe,
|
describe,
|
||||||
expect,
|
expect,
|
||||||
it,
|
it,
|
||||||
} from 'vitest';
|
} from 'vitest';
|
||||||
|
import { createTypographyControl } from './createTypographyControl.svelte';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test Strategy for createTypographyControl Helper
|
* Test Strategy for createTypographyControl Helper
|
||||||
@@ -34,7 +32,7 @@ describe('createTypographyControl - Unit Tests', () => {
|
|||||||
min?: number;
|
min?: number;
|
||||||
max?: number;
|
max?: number;
|
||||||
step?: number;
|
step?: number;
|
||||||
}): TypographyControl {
|
}): NumericControl {
|
||||||
return createTypographyControl({
|
return createTypographyControl({
|
||||||
value: initialValue,
|
value: initialValue,
|
||||||
min: options?.min ?? 0,
|
min: options?.min ?? 0,
|
||||||
@@ -5,11 +5,6 @@
|
|||||||
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/lib';
|
import { cn } from '$shared/lib';
|
||||||
import {
|
import {
|
||||||
@@ -24,7 +19,12 @@ 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 { typographySettingsStore } from '../../model';
|
import {
|
||||||
|
MULTIPLIER_L,
|
||||||
|
MULTIPLIER_M,
|
||||||
|
MULTIPLIER_S,
|
||||||
|
typographySettingsStore,
|
||||||
|
} from '../../model';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
export { FontsByIdsStore } from './model';
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { FontsByIdsStore } from './store/fontsByIdsStore/fontsByIdsStore.svelte';
|
|
||||||
@@ -1,200 +0,0 @@
|
|||||||
/**
|
|
||||||
* Numeric control with bounded values and step precision
|
|
||||||
*
|
|
||||||
* Creates a reactive control for numeric values that enforces min/max bounds
|
|
||||||
* and rounds to a specific step increment. Commonly used for typography controls
|
|
||||||
* like font size, line height, and letter spacing.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* const fontSize = createTypographyControl({
|
|
||||||
* value: 16,
|
|
||||||
* min: 12,
|
|
||||||
* max: 72,
|
|
||||||
* step: 1
|
|
||||||
* });
|
|
||||||
*
|
|
||||||
* // Access current value
|
|
||||||
* fontSize.value; // 16
|
|
||||||
* fontSize.isAtMin; // false
|
|
||||||
*
|
|
||||||
* // Modify value (automatically clamped and rounded)
|
|
||||||
* fontSize.increase();
|
|
||||||
* fontSize.value = 100; // Will be clamped to max (72)
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {
|
|
||||||
clampNumber,
|
|
||||||
roundToStepPrecision,
|
|
||||||
} from '$shared/lib/utils';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Core numeric control configuration
|
|
||||||
* Defines the bounds and stepping behavior for a control
|
|
||||||
*/
|
|
||||||
export interface ControlDataModel {
|
|
||||||
/**
|
|
||||||
* Initial or current numeric value
|
|
||||||
*/
|
|
||||||
value: number;
|
|
||||||
/**
|
|
||||||
* Lower inclusive bound
|
|
||||||
*/
|
|
||||||
min: number;
|
|
||||||
/**
|
|
||||||
* Upper inclusive bound
|
|
||||||
*/
|
|
||||||
max: number;
|
|
||||||
/**
|
|
||||||
* Precision for increment/decrement operations
|
|
||||||
*/
|
|
||||||
step: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Full control model including accessibility labels
|
|
||||||
*
|
|
||||||
* @template T - Type for the control identifier
|
|
||||||
*/
|
|
||||||
export interface ControlModel<T extends string = string> extends ControlDataModel {
|
|
||||||
/**
|
|
||||||
* Unique string identifier for the control
|
|
||||||
*/
|
|
||||||
id: T;
|
|
||||||
/**
|
|
||||||
* Label used by screen readers for the increase button
|
|
||||||
*/
|
|
||||||
increaseLabel?: string;
|
|
||||||
/**
|
|
||||||
* Label used by screen readers for the decrease button
|
|
||||||
*/
|
|
||||||
decreaseLabel?: string;
|
|
||||||
/**
|
|
||||||
* Overall label describing the control's purpose
|
|
||||||
*/
|
|
||||||
controlLabel?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a reactive numeric control with bounds and stepping
|
|
||||||
*
|
|
||||||
* The control automatically:
|
|
||||||
* - Clamps values to the min/max range
|
|
||||||
* - Rounds values to the step precision
|
|
||||||
* - Tracks whether at min/max bounds
|
|
||||||
*
|
|
||||||
* @param initialState - Initial value, bounds, and step configuration
|
|
||||||
* @returns Typography control instance with reactive state and methods
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* // Font size control: 12-72px in 1px increments
|
|
||||||
* const fontSize = createTypographyControl({
|
|
||||||
* value: 16,
|
|
||||||
* min: 12,
|
|
||||||
* max: 72,
|
|
||||||
* step: 1
|
|
||||||
* });
|
|
||||||
*
|
|
||||||
* // Line height control: 1.0-2.0 in 0.1 increments
|
|
||||||
* const lineHeight = createTypographyControl({
|
|
||||||
* value: 1.5,
|
|
||||||
* min: 1.0,
|
|
||||||
* max: 2.0,
|
|
||||||
* step: 0.1
|
|
||||||
* });
|
|
||||||
*
|
|
||||||
* // Direct assignment (auto-clamped)
|
|
||||||
* fontSize.value = 100; // Becomes 72 (max)
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export function createTypographyControl<T extends ControlDataModel>(
|
|
||||||
initialState: T,
|
|
||||||
) {
|
|
||||||
let value = $state(initialState.value);
|
|
||||||
let max = $state(initialState.max);
|
|
||||||
let min = $state(initialState.min);
|
|
||||||
let step = $state(initialState.step);
|
|
||||||
|
|
||||||
// Derived state for boundary detection
|
|
||||||
const { isAtMax, isAtMin } = $derived({
|
|
||||||
isAtMax: value >= max,
|
|
||||||
isAtMin: value <= min,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
/**
|
|
||||||
* Clamped and rounded control value (reactive)
|
|
||||||
*/
|
|
||||||
get value() {
|
|
||||||
return value;
|
|
||||||
},
|
|
||||||
set value(newValue) {
|
|
||||||
const rounded = roundToStepPrecision(clampNumber(newValue, min, max), step);
|
|
||||||
if (value !== rounded) {
|
|
||||||
value = rounded;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Upper limit for the control value
|
|
||||||
*/
|
|
||||||
get max() {
|
|
||||||
return max;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Lower limit for the control value
|
|
||||||
*/
|
|
||||||
get min() {
|
|
||||||
return min;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configured step increment
|
|
||||||
*/
|
|
||||||
get step() {
|
|
||||||
return step;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* True if current value is equal to or greater than max
|
|
||||||
*/
|
|
||||||
get isAtMax() {
|
|
||||||
return isAtMax;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* True if current value is equal to or less than min
|
|
||||||
*/
|
|
||||||
get isAtMin() {
|
|
||||||
return isAtMin;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Increase value by one step (clamped to max)
|
|
||||||
*/
|
|
||||||
increase() {
|
|
||||||
value = roundToStepPrecision(
|
|
||||||
clampNumber(value + step, min, max),
|
|
||||||
step,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decrease value by one step (clamped to min)
|
|
||||||
*/
|
|
||||||
decrease() {
|
|
||||||
value = roundToStepPrecision(
|
|
||||||
clampNumber(value - step, min, max),
|
|
||||||
step,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Type representing a typography control instance
|
|
||||||
*/
|
|
||||||
export type TypographyControl = ReturnType<typeof createTypographyControl>;
|
|
||||||
@@ -3,7 +3,6 @@
|
|||||||
*
|
*
|
||||||
* Provides composable state management patterns for common UI needs:
|
* Provides composable state management patterns for common UI needs:
|
||||||
* - Filter management with multi-selection
|
* - Filter management with multi-selection
|
||||||
* - Typography controls with bounds and stepping
|
|
||||||
* - Virtual scrolling for large lists
|
* - Virtual scrolling for large lists
|
||||||
* - Debounced state for search inputs
|
* - Debounced state for search inputs
|
||||||
* - Entity stores with O(1) lookups
|
* - Entity stores with O(1) lookups
|
||||||
@@ -13,11 +12,10 @@
|
|||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```ts
|
* ```ts
|
||||||
* import { createFilter, createVirtualizer, createTypographyControl } from '$shared/lib/helpers';
|
* import { createFilter, createVirtualizer } from '$shared/lib/helpers';
|
||||||
*
|
*
|
||||||
* const filter = createFilter({ properties: [...] });
|
* const filter = createFilter({ properties: [...] });
|
||||||
* const virtualizer = createVirtualizer(() => ({ count: 1000, estimateSize: () => 50 }));
|
* const virtualizer = createVirtualizer(() => ({ count: 1000, estimateSize: () => 50 }));
|
||||||
* const control = createTypographyControl({ value: 16, min: 12, max: 72, step: 1 });
|
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -43,28 +41,6 @@ export {
|
|||||||
type Property,
|
type Property,
|
||||||
} from './createFilter/createFilter.svelte';
|
} from './createFilter/createFilter.svelte';
|
||||||
|
|
||||||
/**
|
|
||||||
* Bounded numeric controls
|
|
||||||
*/
|
|
||||||
export {
|
|
||||||
/**
|
|
||||||
* Base numeric configuration
|
|
||||||
*/
|
|
||||||
type ControlDataModel,
|
|
||||||
/**
|
|
||||||
* Extended model with labels
|
|
||||||
*/
|
|
||||||
type ControlModel,
|
|
||||||
/**
|
|
||||||
* Reactive control factory
|
|
||||||
*/
|
|
||||||
createTypographyControl,
|
|
||||||
/**
|
|
||||||
* Control instance type
|
|
||||||
*/
|
|
||||||
type TypographyControl,
|
|
||||||
} from './createTypographyControl/createTypographyControl.svelte';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List virtualization
|
* List virtualization
|
||||||
*/
|
*/
|
||||||
@@ -160,3 +136,9 @@ export {
|
|||||||
*/
|
*/
|
||||||
type PerspectiveManager,
|
type PerspectiveManager,
|
||||||
} from './createPerspectiveManager/createPerspectiveManager.svelte';
|
} from './createPerspectiveManager/createPerspectiveManager.svelte';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* BaseQueryStore is intentionally NOT re-exported here.
|
||||||
|
* It pulls @tanstack/query-core, so routing it through this leaf barrel would
|
||||||
|
* make every consumer of the barrel eager-load TanStack. Import it by path.
|
||||||
|
*/
|
||||||
|
|||||||
@@ -5,15 +5,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export {
|
export {
|
||||||
type ControlDataModel,
|
|
||||||
type ControlModel,
|
|
||||||
createDebouncedState,
|
createDebouncedState,
|
||||||
createEntityStore,
|
createEntityStore,
|
||||||
createFilter,
|
createFilter,
|
||||||
createPersistentStore,
|
createPersistentStore,
|
||||||
createPerspectiveManager,
|
createPerspectiveManager,
|
||||||
createResponsiveManager,
|
createResponsiveManager,
|
||||||
createTypographyControl,
|
|
||||||
createVirtualizer,
|
createVirtualizer,
|
||||||
type Entity,
|
type Entity,
|
||||||
type EntityStore,
|
type EntityStore,
|
||||||
@@ -24,7 +21,6 @@ export {
|
|||||||
type Property,
|
type Property,
|
||||||
type ResponsiveManager,
|
type ResponsiveManager,
|
||||||
responsiveManager,
|
responsiveManager,
|
||||||
type TypographyControl,
|
|
||||||
type VirtualItem,
|
type VirtualItem,
|
||||||
type Virtualizer,
|
type Virtualizer,
|
||||||
type VirtualizerOptions,
|
type VirtualizerOptions,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script module>
|
<script module>
|
||||||
import { createTypographyControl } from '$shared/lib';
|
|
||||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||||
import ComboControl from './ComboControl.svelte';
|
import ComboControl from './ComboControl.svelte';
|
||||||
|
import { createNumericControlMock } from './testing/createNumericControlMock.svelte';
|
||||||
|
|
||||||
const { Story } = defineMeta({
|
const { Story } = defineMeta({
|
||||||
title: 'Shared/ComboControl',
|
title: 'Shared/ComboControl',
|
||||||
@@ -23,7 +23,7 @@ const { Story } = defineMeta({
|
|||||||
},
|
},
|
||||||
control: {
|
control: {
|
||||||
control: 'object',
|
control: 'object',
|
||||||
description: 'TypographyControl instance managing the value and bounds',
|
description: 'NumericControl instance managing the value and bounds',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -31,7 +31,7 @@ const { Story } = defineMeta({
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { ComponentProps } from 'svelte';
|
import type { ComponentProps } from 'svelte';
|
||||||
const horizontalControl = createTypographyControl({ min: 0, max: 100, step: 1, value: 50 });
|
const horizontalControl = createNumericControlMock({ min: 0, max: 100, step: 1, value: 50 });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Story
|
<Story
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { createTypographyControl } from '$shared/lib';
|
|
||||||
import {
|
import {
|
||||||
fireEvent,
|
fireEvent,
|
||||||
render,
|
render,
|
||||||
@@ -6,9 +5,10 @@ import {
|
|||||||
waitFor,
|
waitFor,
|
||||||
} from '@testing-library/svelte';
|
} from '@testing-library/svelte';
|
||||||
import ComboControl from './ComboControl.svelte';
|
import ComboControl from './ComboControl.svelte';
|
||||||
|
import { createNumericControlMock } from './testing/createNumericControlMock.svelte';
|
||||||
|
|
||||||
function makeControl(value: number, opts: { min?: number; max?: number; step?: number } = {}) {
|
function makeControl(value: number, opts: { min?: number; max?: number; step?: number } = {}) {
|
||||||
return createTypographyControl({
|
return createNumericControlMock({
|
||||||
value,
|
value,
|
||||||
min: opts.min ?? 0,
|
min: opts.min ?? 0,
|
||||||
max: opts.max ?? 100,
|
max: opts.max ?? 100,
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
/**
|
||||||
|
* Test/story mock implementing the NumericControl contract.
|
||||||
|
*
|
||||||
|
* Lives in shared/ui because ComboControl must be testable without importing
|
||||||
|
* the real typography factory (which sits in a feature, above shared).
|
||||||
|
*/
|
||||||
|
import type { NumericControl } from '../types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a reactive NumericControl mock with clamping and stepping.
|
||||||
|
*/
|
||||||
|
export function createNumericControlMock(
|
||||||
|
init: Pick<NumericControl, 'value' | 'min' | 'max' | 'step'>,
|
||||||
|
): NumericControl {
|
||||||
|
let value = $state(init.value);
|
||||||
|
const clamp = (v: number) => Math.min(Math.max(v, init.min), init.max);
|
||||||
|
|
||||||
|
return {
|
||||||
|
get value() {
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
set value(v) {
|
||||||
|
value = clamp(v);
|
||||||
|
},
|
||||||
|
get min() {
|
||||||
|
return init.min;
|
||||||
|
},
|
||||||
|
get max() {
|
||||||
|
return init.max;
|
||||||
|
},
|
||||||
|
get step() {
|
||||||
|
return init.step;
|
||||||
|
},
|
||||||
|
get isAtMin() {
|
||||||
|
return value <= init.min;
|
||||||
|
},
|
||||||
|
get isAtMax() {
|
||||||
|
return value >= init.max;
|
||||||
|
},
|
||||||
|
increase() {
|
||||||
|
value = clamp(value + init.step);
|
||||||
|
},
|
||||||
|
decrease() {
|
||||||
|
value = clamp(value - init.step);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -15,13 +15,13 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
type FontLoadRequestConfig,
|
type FontLoadRequestConfig,
|
||||||
|
FontsByIdsStore,
|
||||||
type UnifiedFont,
|
type UnifiedFont,
|
||||||
fontCatalogStore,
|
fontCatalogStore,
|
||||||
fontLifecycleManager,
|
fontLifecycleManager,
|
||||||
getFontUrl,
|
getFontUrl,
|
||||||
} from '$entities/Font';
|
} from '$entities/Font';
|
||||||
import { typographySettingsStore } from '$features/AdjustTypography/model';
|
import { typographySettingsStore } from '$features/AdjustTypography/model';
|
||||||
import { FontsByIdsStore } from '$features/FetchFontsByIds';
|
|
||||||
import { createPersistentStore } from '$shared/lib';
|
import { createPersistentStore } from '$shared/lib';
|
||||||
import { untrack } from 'svelte';
|
import { untrack } from 'svelte';
|
||||||
import { getPretextFontString } from '../../lib';
|
import { getPretextFontString } from '../../lib';
|
||||||
|
|||||||
@@ -11,13 +11,15 @@
|
|||||||
import {
|
import {
|
||||||
type ComparisonResult,
|
type ComparisonResult,
|
||||||
DualFontLayout,
|
DualFontLayout,
|
||||||
|
findSplitIndex,
|
||||||
|
} from '$entities/Font';
|
||||||
|
import {
|
||||||
MULTIPLIER_L,
|
MULTIPLIER_L,
|
||||||
MULTIPLIER_M,
|
MULTIPLIER_M,
|
||||||
MULTIPLIER_S,
|
MULTIPLIER_S,
|
||||||
findSplitIndex,
|
TypographyMenu,
|
||||||
} from '$entities/Font';
|
typographySettingsStore,
|
||||||
import { TypographyMenu } from '$features/AdjustTypography';
|
} from '$features/AdjustTypography';
|
||||||
import { typographySettingsStore } from '$features/AdjustTypography/model';
|
|
||||||
import {
|
import {
|
||||||
type ResponsiveManager,
|
type ResponsiveManager,
|
||||||
debounce,
|
debounce,
|
||||||
|
|||||||
Reference in New Issue
Block a user