refactor(createTypographyControl): createControlStore rewrote to runes
This commit is contained in:
@@ -0,0 +1,22 @@
|
|||||||
|
import {
|
||||||
|
type ControlModel,
|
||||||
|
createTypographyControl,
|
||||||
|
} from '$shared/lib';
|
||||||
|
|
||||||
|
export function createTypographyControlManager(configs: ControlModel[]) {
|
||||||
|
const controls = $state(
|
||||||
|
configs.map(({ id, increaseLabel, decreaseLabel, controlLabel, ...config }) => ({
|
||||||
|
id,
|
||||||
|
increaseLabel,
|
||||||
|
decreaseLabel,
|
||||||
|
controlLabel,
|
||||||
|
instance: createTypographyControl(config),
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
get controls() {
|
||||||
|
return controls;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
56
src/features/SetupFont/model/state/manager.svetle.ts
Normal file
56
src/features/SetupFont/model/state/manager.svetle.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import {
|
||||||
|
createTypographyControlManager,
|
||||||
|
} from '$features/SetupFont/lib/controlManager/controlManager.svelte';
|
||||||
|
import type { ControlModel } from '$shared/lib';
|
||||||
|
import {
|
||||||
|
DEFAULT_FONT_SIZE,
|
||||||
|
DEFAULT_FONT_WEIGHT,
|
||||||
|
DEFAULT_LINE_HEIGHT,
|
||||||
|
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,
|
||||||
|
} from '../const/const';
|
||||||
|
|
||||||
|
const controlData: ControlModel[] = [
|
||||||
|
{
|
||||||
|
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: 'Font 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: 'Font 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: 'Line Height',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const controlManager = createTypographyControlManager(controlData);
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import {
|
|
||||||
type ControlModel,
|
|
||||||
createControlStore,
|
|
||||||
} from '$shared/lib/store/createControlStore/createControlStore';
|
|
||||||
import {
|
|
||||||
DEFAULT_FONT_SIZE,
|
|
||||||
MAX_FONT_SIZE,
|
|
||||||
MIN_FONT_SIZE,
|
|
||||||
} from '../const/const';
|
|
||||||
|
|
||||||
const initialValue: ControlModel = {
|
|
||||||
value: DEFAULT_FONT_SIZE,
|
|
||||||
max: MAX_FONT_SIZE,
|
|
||||||
min: MIN_FONT_SIZE,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const fontSizeStore = createControlStore(initialValue);
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import {
|
|
||||||
type ControlModel,
|
|
||||||
createControlStore,
|
|
||||||
} from '$shared/lib/store/createControlStore/createControlStore';
|
|
||||||
import {
|
|
||||||
DEFAULT_FONT_WEIGHT,
|
|
||||||
FONT_WEIGHT_STEP,
|
|
||||||
MAX_FONT_WEIGHT,
|
|
||||||
MIN_FONT_WEIGHT,
|
|
||||||
} from '../const/const';
|
|
||||||
|
|
||||||
const initialValue: ControlModel = {
|
|
||||||
value: DEFAULT_FONT_WEIGHT,
|
|
||||||
max: MAX_FONT_WEIGHT,
|
|
||||||
min: MIN_FONT_WEIGHT,
|
|
||||||
step: FONT_WEIGHT_STEP,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const fontWeightStore = createControlStore(initialValue);
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import {
|
|
||||||
type ControlModel,
|
|
||||||
createControlStore,
|
|
||||||
} from '$shared/lib/store/createControlStore/createControlStore';
|
|
||||||
import {
|
|
||||||
DEFAULT_LINE_HEIGHT,
|
|
||||||
LINE_HEIGHT_STEP,
|
|
||||||
MAX_LINE_HEIGHT,
|
|
||||||
MIN_LINE_HEIGHT,
|
|
||||||
} from '../const/const';
|
|
||||||
|
|
||||||
const initialValue: ControlModel = {
|
|
||||||
value: DEFAULT_LINE_HEIGHT,
|
|
||||||
max: MAX_LINE_HEIGHT,
|
|
||||||
min: MIN_LINE_HEIGHT,
|
|
||||||
step: LINE_HEIGHT_STEP,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const lineHeightStore = createControlStore(initialValue);
|
|
||||||
@@ -1,55 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import * as Item from '$shared/shadcn/ui/item';
|
|
||||||
import { Separator } from '$shared/shadcn/ui/separator/index';
|
import { Separator } from '$shared/shadcn/ui/separator/index';
|
||||||
import * as Sidebar from '$shared/shadcn/ui/sidebar/index';
|
import * as Sidebar from '$shared/shadcn/ui/sidebar/index';
|
||||||
import ComboControl from '$shared/ui/ComboControl/ComboControl.svelte';
|
import { ComboControl } from '$shared/ui';
|
||||||
import { fontSizeStore } from '../model/stores/fontSizeStore';
|
import { controlManager } from '../model/state/manager.svetle';
|
||||||
import { fontWeightStore } from '../model/stores/fontWeightStore';
|
|
||||||
import { lineHeightStore } from '../model/stores/lineHeightStore';
|
|
||||||
|
|
||||||
const fontSize = $derived($fontSizeStore);
|
|
||||||
const fontWeight = $derived($fontWeightStore);
|
|
||||||
const lineHeight = $derived($lineHeightStore);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="w-full p-2 flex flex-row items-center">
|
<div class="w-full p-2 flex flex-row items-center gap-2">
|
||||||
<Sidebar.Trigger />
|
<Sidebar.Trigger />
|
||||||
<Separator orientation="vertical" class="h-full" />
|
<Separator orientation="vertical" class="h-full" />
|
||||||
<ComboControl
|
{#each controlManager.controls as control (control.id)}
|
||||||
value={fontSize.value}
|
<ComboControl control={control.instance} />
|
||||||
minValue={fontSize.min}
|
{/each}
|
||||||
maxValue={fontSize.max}
|
|
||||||
onChange={fontSizeStore.setValue}
|
|
||||||
onIncrease={fontSizeStore.increase}
|
|
||||||
onDecrease={fontSizeStore.decrease}
|
|
||||||
increaseDisabled={fontSizeStore.isAtMax()}
|
|
||||||
decreaseDisabled={fontSizeStore.isAtMin()}
|
|
||||||
increaseLabel="Increase Font Size"
|
|
||||||
decreaseLabel="Decrease Font Size"
|
|
||||||
/>
|
|
||||||
<ComboControl
|
|
||||||
value={fontWeight.value}
|
|
||||||
minValue={fontWeight.min}
|
|
||||||
maxValue={fontWeight.max}
|
|
||||||
onChange={fontWeightStore.setValue}
|
|
||||||
onIncrease={fontWeightStore.increase}
|
|
||||||
onDecrease={fontWeightStore.decrease}
|
|
||||||
increaseDisabled={fontWeightStore.isAtMax()}
|
|
||||||
decreaseDisabled={fontWeightStore.isAtMin()}
|
|
||||||
increaseLabel="Increase Font Weight"
|
|
||||||
decreaseLabel="Decrease Font Weight"
|
|
||||||
/>
|
|
||||||
<ComboControl
|
|
||||||
value={lineHeight.value}
|
|
||||||
minValue={lineHeight.min}
|
|
||||||
maxValue={lineHeight.max}
|
|
||||||
step={lineHeight.step}
|
|
||||||
onChange={lineHeightStore.setValue}
|
|
||||||
onIncrease={lineHeightStore.increase}
|
|
||||||
onDecrease={lineHeightStore.decrease}
|
|
||||||
increaseDisabled={lineHeightStore.isAtMax()}
|
|
||||||
decreaseDisabled={lineHeightStore.isAtMin()}
|
|
||||||
increaseLabel="Increase Line Height"
|
|
||||||
decreaseLabel="Decrease Line Height"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import {
|
||||||
|
clampNumber,
|
||||||
|
roundToStepPrecision,
|
||||||
|
} from '$shared/lib/utils';
|
||||||
|
|
||||||
|
export interface ControlDataModel {
|
||||||
|
value: number;
|
||||||
|
min: number;
|
||||||
|
max: number;
|
||||||
|
step: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ControlModel extends ControlDataModel {
|
||||||
|
id: string;
|
||||||
|
increaseLabel: string;
|
||||||
|
decreaseLabel: string;
|
||||||
|
controlLabel: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
const { isAtMax, isAtMin } = $derived({
|
||||||
|
isAtMax: value >= max,
|
||||||
|
isAtMin: value <= min,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
get value() {
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
set value(newValue) {
|
||||||
|
value = roundToStepPrecision(
|
||||||
|
clampNumber(newValue, min, max),
|
||||||
|
step,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TypographyControl = ReturnType<typeof createTypographyControl>;
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
import { get } from 'svelte/store';
|
|
||||||
import {
|
|
||||||
beforeEach,
|
|
||||||
describe,
|
|
||||||
expect,
|
|
||||||
it,
|
|
||||||
} from 'vitest';
|
|
||||||
import {
|
|
||||||
type ControlModel,
|
|
||||||
createControlStore,
|
|
||||||
} from './createControlStore';
|
|
||||||
|
|
||||||
describe('createControlStore', () => {
|
|
||||||
let store: ReturnType<typeof createControlStore<number>>;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
const initialState: ControlModel<number> = {
|
|
||||||
value: 10,
|
|
||||||
min: 0,
|
|
||||||
max: 100,
|
|
||||||
step: 5,
|
|
||||||
};
|
|
||||||
store = createControlStore(initialState);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('initializes with correct state', () => {
|
|
||||||
expect(get(store)).toEqual({
|
|
||||||
value: 10,
|
|
||||||
min: 0,
|
|
||||||
max: 100,
|
|
||||||
step: 5,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('increases value by step', () => {
|
|
||||||
store.increase();
|
|
||||||
expect(get(store).value).toBe(15);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('decreases value by step', () => {
|
|
||||||
store.decrease();
|
|
||||||
expect(get(store).value).toBe(5);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('clamps value at maximum', () => {
|
|
||||||
store.setValue(200);
|
|
||||||
expect(get(store).value).toBe(100);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('clamps value at minimum', () => {
|
|
||||||
store.setValue(-10);
|
|
||||||
expect(get(store).value).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rounds to step precision', () => {
|
|
||||||
store.setValue(12.34);
|
|
||||||
// With step=5, 12.34 is clamped and rounded to nearest integer (0 decimal places)
|
|
||||||
expect(get(store).value).toBe(12);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('handles decimal steps correctly', () => {
|
|
||||||
const decimalStore = createControlStore({
|
|
||||||
value: 1.0,
|
|
||||||
min: 0,
|
|
||||||
max: 2,
|
|
||||||
step: 0.05,
|
|
||||||
});
|
|
||||||
decimalStore.increase();
|
|
||||||
expect(get(decimalStore).value).toBe(1.05);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('isAtMax returns true when at maximum', () => {
|
|
||||||
store.setValue(100);
|
|
||||||
expect(store.isAtMax()).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('isAtMax returns false when not at maximum', () => {
|
|
||||||
expect(store.isAtMax()).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('isAtMin returns true when at minimum', () => {
|
|
||||||
store.setValue(0);
|
|
||||||
expect(store.isAtMin()).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('isAtMin returns false when not at minimum', () => {
|
|
||||||
expect(store.isAtMin()).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
import {
|
|
||||||
type Writable,
|
|
||||||
get,
|
|
||||||
writable,
|
|
||||||
} from 'svelte/store';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Model for a control value with min/max bounds
|
|
||||||
*/
|
|
||||||
export type ControlModel<
|
|
||||||
TValue extends number = number,
|
|
||||||
> = {
|
|
||||||
value: TValue;
|
|
||||||
min: TValue;
|
|
||||||
max: TValue;
|
|
||||||
step?: TValue;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Store model with methods for control manipulation
|
|
||||||
*/
|
|
||||||
export type ControlStoreModel<
|
|
||||||
TValue extends number,
|
|
||||||
> =
|
|
||||||
& Writable<ControlModel<TValue>>
|
|
||||||
& {
|
|
||||||
increase: () => void;
|
|
||||||
decrease: () => void;
|
|
||||||
/** Set a specific value */
|
|
||||||
setValue: (newValue: TValue) => void;
|
|
||||||
isAtMax: () => boolean;
|
|
||||||
isAtMin: () => boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a writable store for numeric control values with bounds
|
|
||||||
*
|
|
||||||
* @template TValue - The value type (extends number)
|
|
||||||
* @param initialState - Initial state containing value, min, and max
|
|
||||||
*/
|
|
||||||
/**
|
|
||||||
* Get the number of decimal places in a number
|
|
||||||
*
|
|
||||||
* For example:
|
|
||||||
* - 1 -> 0
|
|
||||||
* - 0.1 -> 1
|
|
||||||
* - 0.01 -> 2
|
|
||||||
* - 0.05 -> 2
|
|
||||||
*
|
|
||||||
* @param step - The step number to analyze
|
|
||||||
* @returns The number of decimal places
|
|
||||||
*/
|
|
||||||
function getDecimalPlaces(step: number): number {
|
|
||||||
const str = step.toString();
|
|
||||||
const decimalPart = str.split('.')[1];
|
|
||||||
return decimalPart ? decimalPart.length : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Round a value to the precision of the given step
|
|
||||||
*
|
|
||||||
* This fixes floating-point precision errors that occur with decimal steps.
|
|
||||||
* For example, with step=0.05, adding it repeatedly can produce values like
|
|
||||||
* 1.3499999999999999 instead of 1.35.
|
|
||||||
*
|
|
||||||
* We use toFixed() to round to the appropriate decimal places instead of
|
|
||||||
* Math.round(value / step) * step, which doesn't always work correctly
|
|
||||||
* due to floating-point arithmetic errors.
|
|
||||||
*
|
|
||||||
* @param value - The value to round
|
|
||||||
* @param step - The step to round to (defaults to 1)
|
|
||||||
* @returns The rounded value
|
|
||||||
*/
|
|
||||||
function roundToStepPrecision(value: number, step: number = 1): number {
|
|
||||||
if (step <= 0) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
const decimals = getDecimalPlaces(step);
|
|
||||||
return parseFloat(value.toFixed(decimals));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createControlStore<
|
|
||||||
TValue extends number = number,
|
|
||||||
>(
|
|
||||||
initialState: ControlModel<TValue>,
|
|
||||||
): ControlStoreModel<TValue> {
|
|
||||||
const store = writable(initialState);
|
|
||||||
const { subscribe, set, update } = store;
|
|
||||||
|
|
||||||
const clamp = (value: number): TValue => {
|
|
||||||
return Math.max(initialState.min, Math.min(value, initialState.max)) as TValue;
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
subscribe,
|
|
||||||
set,
|
|
||||||
update,
|
|
||||||
increase: () =>
|
|
||||||
update(m => {
|
|
||||||
const step = m.step ?? 1;
|
|
||||||
const newValue = clamp(m.value + step);
|
|
||||||
return { ...m, value: roundToStepPrecision(newValue, step) as TValue };
|
|
||||||
}),
|
|
||||||
decrease: () =>
|
|
||||||
update(m => {
|
|
||||||
const step = m.step ?? 1;
|
|
||||||
const newValue = clamp(m.value - step);
|
|
||||||
return { ...m, value: roundToStepPrecision(newValue, step) as TValue };
|
|
||||||
}),
|
|
||||||
setValue: (v: TValue) => {
|
|
||||||
const step = initialState.step ?? 1;
|
|
||||||
update(m => ({ ...m, value: roundToStepPrecision(clamp(v), step) as TValue }));
|
|
||||||
},
|
|
||||||
isAtMin: () => get(store).value === initialState.min,
|
|
||||||
isAtMax: () => get(store).value === initialState.max,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import type { TypographyControl } from '$shared/lib';
|
||||||
import { Button } from '$shared/shadcn/ui/button';
|
import { Button } from '$shared/shadcn/ui/button';
|
||||||
import * as ButtonGroup from '$shared/shadcn/ui/button-group';
|
import * as ButtonGroup from '$shared/shadcn/ui/button-group';
|
||||||
import { Input } from '$shared/shadcn/ui/input';
|
import { Input } from '$shared/shadcn/ui/input';
|
||||||
@@ -9,22 +10,6 @@ import PlusIcon from '@lucide/svelte/icons/plus';
|
|||||||
import type { ChangeEventHandler } from 'svelte/elements';
|
import type { ChangeEventHandler } from 'svelte/elements';
|
||||||
|
|
||||||
interface ComboControlProps {
|
interface ComboControlProps {
|
||||||
/**
|
|
||||||
* Controlled value
|
|
||||||
*/
|
|
||||||
value: number;
|
|
||||||
/**
|
|
||||||
* Callback function to handle value change
|
|
||||||
*/
|
|
||||||
onChange: (value: number) => void;
|
|
||||||
/**
|
|
||||||
* Callback function to handle increase
|
|
||||||
*/
|
|
||||||
onIncrease: () => void;
|
|
||||||
/**
|
|
||||||
* Callback function to handle decrease
|
|
||||||
*/
|
|
||||||
onDecrease: () => void;
|
|
||||||
/**
|
/**
|
||||||
* Text for increase button aria-label
|
* Text for increase button aria-label
|
||||||
*/
|
*/
|
||||||
@@ -33,59 +18,35 @@ interface ComboControlProps {
|
|||||||
* Text for decrease button aria-label
|
* Text for decrease button aria-label
|
||||||
*/
|
*/
|
||||||
decreaseLabel?: string;
|
decreaseLabel?: string;
|
||||||
/**
|
|
||||||
* Flag for disabling increase button
|
|
||||||
*/
|
|
||||||
increaseDisabled?: boolean;
|
|
||||||
/**
|
|
||||||
* Flag for disabling decrease button
|
|
||||||
*/
|
|
||||||
decreaseDisabled?: boolean;
|
|
||||||
/**
|
/**
|
||||||
* Text for control button aria-label
|
* Text for control button aria-label
|
||||||
*/
|
*/
|
||||||
controlLabel?: string;
|
controlLabel?: string;
|
||||||
/**
|
/**
|
||||||
* Minimum value for the input
|
* Control instance
|
||||||
*/
|
*/
|
||||||
minValue?: number;
|
control: TypographyControl;
|
||||||
/**
|
|
||||||
* Maximum value for the input
|
|
||||||
*/
|
|
||||||
maxValue?: number;
|
|
||||||
/**
|
|
||||||
* Step value for the slider
|
|
||||||
*/
|
|
||||||
step?: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
value,
|
control,
|
||||||
onChange,
|
|
||||||
onIncrease,
|
|
||||||
onDecrease,
|
|
||||||
increaseLabel,
|
|
||||||
decreaseLabel,
|
decreaseLabel,
|
||||||
increaseDisabled,
|
increaseLabel,
|
||||||
decreaseDisabled,
|
|
||||||
controlLabel,
|
controlLabel,
|
||||||
minValue = 0,
|
|
||||||
maxValue = 100,
|
|
||||||
step = 1,
|
|
||||||
}: ComboControlProps = $props();
|
}: ComboControlProps = $props();
|
||||||
|
|
||||||
// Local state for the slider to prevent infinite loops
|
// Local state for the slider to prevent infinite loops
|
||||||
let sliderValue = $state(value);
|
let sliderValue = $state(Number(control.value));
|
||||||
|
|
||||||
// Sync sliderValue when external value changes
|
// Sync sliderValue when external value changes
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
sliderValue = value;
|
sliderValue = Number(control.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleInputChange: ChangeEventHandler<HTMLInputElement> = event => {
|
const handleInputChange: ChangeEventHandler<HTMLInputElement> = event => {
|
||||||
const parsedValue = parseFloat(event.currentTarget.value);
|
const parsedValue = parseFloat(event.currentTarget.value);
|
||||||
if (!isNaN(parsedValue)) {
|
if (!isNaN(parsedValue)) {
|
||||||
onChange(parsedValue);
|
control.value = parsedValue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -93,8 +54,8 @@ const handleInputChange: ChangeEventHandler<HTMLInputElement> = event => {
|
|||||||
* Handle slider value change.
|
* Handle slider value change.
|
||||||
* The Slider component passes the value as a number directly.
|
* The Slider component passes the value as a number directly.
|
||||||
*/
|
*/
|
||||||
const handleSliderChange = (value: number) => {
|
const handleSliderChange = (newValue: number) => {
|
||||||
onChange(value);
|
control.value = newValue;
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -103,8 +64,8 @@ const handleSliderChange = (value: number) => {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon"
|
size="icon"
|
||||||
aria-label={decreaseLabel}
|
aria-label={decreaseLabel}
|
||||||
onclick={onDecrease}
|
onclick={control.decrease}
|
||||||
disabled={decreaseDisabled}
|
disabled={control.isAtMin}
|
||||||
>
|
>
|
||||||
<MinusIcon />
|
<MinusIcon />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -117,16 +78,16 @@ const handleSliderChange = (value: number) => {
|
|||||||
size="icon"
|
size="icon"
|
||||||
aria-label={controlLabel}
|
aria-label={controlLabel}
|
||||||
>
|
>
|
||||||
{value}
|
{control.value}
|
||||||
</Button>
|
</Button>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</Popover.Trigger>
|
</Popover.Trigger>
|
||||||
<Popover.Content class="w-auto p-4">
|
<Popover.Content class="w-auto p-4">
|
||||||
<div class="flex flex-col items-center gap-3">
|
<div class="flex flex-col items-center gap-3">
|
||||||
<Slider
|
<Slider
|
||||||
min={minValue}
|
min={control.min}
|
||||||
max={maxValue}
|
max={control.max}
|
||||||
step={step}
|
step={control.step}
|
||||||
value={sliderValue}
|
value={sliderValue}
|
||||||
onValueChange={handleSliderChange}
|
onValueChange={handleSliderChange}
|
||||||
type="single"
|
type="single"
|
||||||
@@ -134,10 +95,10 @@ const handleSliderChange = (value: number) => {
|
|||||||
class="h-48"
|
class="h-48"
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
value={String(value)}
|
value={control.value}
|
||||||
min={minValue}
|
|
||||||
max={maxValue}
|
|
||||||
onchange={handleInputChange}
|
onchange={handleInputChange}
|
||||||
|
min={control.min}
|
||||||
|
max={control.max}
|
||||||
class="w-16 text-center"
|
class="w-16 text-center"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -147,8 +108,8 @@ const handleSliderChange = (value: number) => {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon"
|
size="icon"
|
||||||
aria-label={increaseLabel}
|
aria-label={increaseLabel}
|
||||||
onclick={onIncrease}
|
onclick={control.increase}
|
||||||
disabled={increaseDisabled}
|
disabled={control.isAtMax}
|
||||||
>
|
>
|
||||||
<PlusIcon />
|
<PlusIcon />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
Reference in New Issue
Block a user