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": {
|
||||
"correctness": "error",
|
||||
"suspicious": "warn",
|
||||
@@ -22,6 +23,7 @@
|
||||
"rules": {
|
||||
"no-console": "off",
|
||||
"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
|
||||
*/
|
||||
@@ -33,60 +30,6 @@ export const MIN_LETTER_SPACING = -0.1;
|
||||
export const MAX_LETTER_SPACING = 0.5;
|
||||
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.
|
||||
* Treated as being at the very bottom of the infinite scroll.
|
||||
|
||||
@@ -2,7 +2,6 @@ import {
|
||||
generateMixedCategoryFonts,
|
||||
generateMockFonts,
|
||||
} from '$entities/Font/testing';
|
||||
import { QueryClient } from '@tanstack/query-core';
|
||||
import { flushSync } from 'svelte';
|
||||
import {
|
||||
afterEach,
|
||||
@@ -20,6 +19,13 @@ import type { UnifiedFont } from '../../types';
|
||||
import { FontCatalogStore } from './fontCatalogStore.svelte';
|
||||
|
||||
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')>();
|
||||
return {
|
||||
...actual,
|
||||
|
||||
+5
-5
@@ -1,14 +1,14 @@
|
||||
import { fontKeys } from '$shared/api/queryKeys';
|
||||
import { BaseQueryStore } from '$shared/lib/helpers/BaseQueryStore/BaseQueryStore.svelte';
|
||||
import {
|
||||
fetchFontsByIds,
|
||||
seedFontCache,
|
||||
} from '$entities/Font/api/proxy/proxyFonts';
|
||||
} from '../../../api/proxy/proxyFonts';
|
||||
import {
|
||||
FontNetworkError,
|
||||
FontResponseError,
|
||||
} from '$entities/Font/lib/errors/errors';
|
||||
import type { UnifiedFont } from '$entities/Font/model/types';
|
||||
import { fontKeys } from '$shared/api/queryKeys';
|
||||
import { BaseQueryStore } from '$shared/lib/helpers/BaseQueryStore.svelte';
|
||||
} from '../../../lib/errors/errors';
|
||||
import type { UnifiedFont } from '../../types';
|
||||
|
||||
/**
|
||||
* 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 { fontKeys } from '$shared/api/queryKeys';
|
||||
import {
|
||||
@@ -12,6 +7,11 @@ import {
|
||||
it,
|
||||
vi,
|
||||
} from 'vitest';
|
||||
import * as api from '../../../api/proxy/proxyFonts';
|
||||
import {
|
||||
FontNetworkError,
|
||||
FontResponseError,
|
||||
} from '../../../lib/errors/errors';
|
||||
import { FontsByIdsStore } from './fontsByIdsStore.svelte';
|
||||
|
||||
describe('FontsByIdsStore', () => {
|
||||
@@ -7,3 +7,6 @@ export {
|
||||
FontCatalogStore,
|
||||
fontCatalogStore,
|
||||
} 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';
|
||||
|
||||
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 {
|
||||
createTypographySettingsStore,
|
||||
MULTIPLIER_L,
|
||||
MULTIPLIER_M,
|
||||
MULTIPLIER_S,
|
||||
type TypographySettingsStore,
|
||||
typographySettingsStore,
|
||||
} 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 {
|
||||
createTypographySettingsStore,
|
||||
type TypographySettingsStore,
|
||||
|
||||
+9
-8
@@ -11,22 +11,23 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
type ControlId,
|
||||
DEFAULT_FONT_SIZE,
|
||||
DEFAULT_FONT_WEIGHT,
|
||||
DEFAULT_LETTER_SPACING,
|
||||
DEFAULT_LINE_HEIGHT,
|
||||
DEFAULT_TYPOGRAPHY_CONTROLS_DATA,
|
||||
} from '$entities/Font';
|
||||
import {
|
||||
type ControlDataModel,
|
||||
type ControlModel,
|
||||
type PersistentStore,
|
||||
type TypographyControl,
|
||||
createPersistentStore,
|
||||
createTypographyControl,
|
||||
} from '$shared/lib';
|
||||
import type { NumericControl } from '$shared/ui';
|
||||
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
|
||||
@@ -36,7 +37,7 @@ import { SvelteMap } from 'svelte/reactivity';
|
||||
*/
|
||||
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
|
||||
@@ -45,7 +46,7 @@ export interface Control extends ControlOnlyFields<ControlId> {
|
||||
/**
|
||||
* The reactive typography control instance
|
||||
*/
|
||||
instance: TypographyControl;
|
||||
instance: NumericControl;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+1
-1
@@ -6,7 +6,6 @@ import {
|
||||
DEFAULT_FONT_WEIGHT,
|
||||
DEFAULT_LETTER_SPACING,
|
||||
DEFAULT_LINE_HEIGHT,
|
||||
DEFAULT_TYPOGRAPHY_CONTROLS_DATA,
|
||||
} from '$entities/Font';
|
||||
import {
|
||||
beforeEach,
|
||||
@@ -15,6 +14,7 @@ import {
|
||||
it,
|
||||
vi,
|
||||
} from 'vitest';
|
||||
import { DEFAULT_TYPOGRAPHY_CONTROLS_DATA } from '../../const/const';
|
||||
import {
|
||||
type TypographySettings,
|
||||
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 {
|
||||
type TypographyControl,
|
||||
createTypographyControl,
|
||||
} from '$shared/lib';
|
||||
import type { NumericControl } from '$shared/ui';
|
||||
import {
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
} from 'vitest';
|
||||
import { createTypographyControl } from './createTypographyControl.svelte';
|
||||
|
||||
/**
|
||||
* Test Strategy for createTypographyControl Helper
|
||||
@@ -34,7 +32,7 @@ describe('createTypographyControl - Unit Tests', () => {
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
}): TypographyControl {
|
||||
}): NumericControl {
|
||||
return createTypographyControl({
|
||||
value: initialValue,
|
||||
min: options?.min ?? 0,
|
||||
@@ -5,11 +5,6 @@
|
||||
Desktop: inline bar with combo controls.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import {
|
||||
MULTIPLIER_L,
|
||||
MULTIPLIER_M,
|
||||
MULTIPLIER_S,
|
||||
} from '$entities/Font';
|
||||
import type { ResponsiveManager } from '$shared/lib';
|
||||
import { cn } from '$shared/lib';
|
||||
import {
|
||||
@@ -24,7 +19,12 @@ import { Popover } from 'bits-ui';
|
||||
import { getContext } from 'svelte';
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
import { fly } from 'svelte/transition';
|
||||
import { typographySettingsStore } from '../../model';
|
||||
import {
|
||||
MULTIPLIER_L,
|
||||
MULTIPLIER_M,
|
||||
MULTIPLIER_S,
|
||||
typographySettingsStore,
|
||||
} from '../../model';
|
||||
|
||||
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:
|
||||
* - Filter management with multi-selection
|
||||
* - Typography controls with bounds and stepping
|
||||
* - Virtual scrolling for large lists
|
||||
* - Debounced state for search inputs
|
||||
* - Entity stores with O(1) lookups
|
||||
@@ -13,11 +12,10 @@
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { createFilter, createVirtualizer, createTypographyControl } from '$shared/lib/helpers';
|
||||
* import { createFilter, createVirtualizer } from '$shared/lib/helpers';
|
||||
*
|
||||
* const filter = createFilter({ properties: [...] });
|
||||
* 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,
|
||||
} 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
|
||||
*/
|
||||
@@ -160,3 +136,9 @@ export {
|
||||
*/
|
||||
type PerspectiveManager,
|
||||
} 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 {
|
||||
type ControlDataModel,
|
||||
type ControlModel,
|
||||
createDebouncedState,
|
||||
createEntityStore,
|
||||
createFilter,
|
||||
createPersistentStore,
|
||||
createPerspectiveManager,
|
||||
createResponsiveManager,
|
||||
createTypographyControl,
|
||||
createVirtualizer,
|
||||
type Entity,
|
||||
type EntityStore,
|
||||
@@ -24,7 +21,6 @@ export {
|
||||
type Property,
|
||||
type ResponsiveManager,
|
||||
responsiveManager,
|
||||
type TypographyControl,
|
||||
type VirtualItem,
|
||||
type Virtualizer,
|
||||
type VirtualizerOptions,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script module>
|
||||
import { createTypographyControl } from '$shared/lib';
|
||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||
import ComboControl from './ComboControl.svelte';
|
||||
import { createNumericControlMock } from './testing/createNumericControlMock.svelte';
|
||||
|
||||
const { Story } = defineMeta({
|
||||
title: 'Shared/ComboControl',
|
||||
@@ -23,7 +23,7 @@ const { Story } = defineMeta({
|
||||
},
|
||||
control: {
|
||||
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">
|
||||
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>
|
||||
|
||||
<Story
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { createTypographyControl } from '$shared/lib';
|
||||
import {
|
||||
fireEvent,
|
||||
render,
|
||||
@@ -6,9 +5,10 @@ import {
|
||||
waitFor,
|
||||
} from '@testing-library/svelte';
|
||||
import ComboControl from './ComboControl.svelte';
|
||||
import { createNumericControlMock } from './testing/createNumericControlMock.svelte';
|
||||
|
||||
function makeControl(value: number, opts: { min?: number; max?: number; step?: number } = {}) {
|
||||
return createTypographyControl({
|
||||
return createNumericControlMock({
|
||||
value,
|
||||
min: opts.min ?? 0,
|
||||
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 {
|
||||
type FontLoadRequestConfig,
|
||||
FontsByIdsStore,
|
||||
type UnifiedFont,
|
||||
fontCatalogStore,
|
||||
fontLifecycleManager,
|
||||
getFontUrl,
|
||||
} from '$entities/Font';
|
||||
import { typographySettingsStore } from '$features/AdjustTypography/model';
|
||||
import { FontsByIdsStore } from '$features/FetchFontsByIds';
|
||||
import { createPersistentStore } from '$shared/lib';
|
||||
import { untrack } from 'svelte';
|
||||
import { getPretextFontString } from '../../lib';
|
||||
|
||||
@@ -11,13 +11,15 @@
|
||||
import {
|
||||
type ComparisonResult,
|
||||
DualFontLayout,
|
||||
findSplitIndex,
|
||||
} from '$entities/Font';
|
||||
import {
|
||||
MULTIPLIER_L,
|
||||
MULTIPLIER_M,
|
||||
MULTIPLIER_S,
|
||||
findSplitIndex,
|
||||
} from '$entities/Font';
|
||||
import { TypographyMenu } from '$features/AdjustTypography';
|
||||
import { typographySettingsStore } from '$features/AdjustTypography/model';
|
||||
TypographyMenu,
|
||||
typographySettingsStore,
|
||||
} from '$features/AdjustTypography';
|
||||
import {
|
||||
type ResponsiveManager,
|
||||
debounce,
|
||||
|
||||
Reference in New Issue
Block a user