Files
pulse/frontend-modern/src/components/shared/toggleModel.ts
T
rcourtman 531a34f5cb Keep a disabled toggle's track visible and true to its state
A disabled toggle switched its track to bg-base, which names no theme
colour (the base alias was removed because it collided with text-base),
so the track rendered transparent. Every disabled toggle showed only a
faint knob: a plan-locked setting such as Patrol's Container update risk
was nearly invisible, and for a read-only session a setting that was on
looked the same as one that was off.

A disabled toggle now keeps its on or off track colour and dims, and the
knob relies on the track's opacity instead of dimming again.
2026-10-05 21:48:58 +01:00

80 lines
2.8 KiB
TypeScript

export type ToggleSize = 'xs' | 'sm' | 'md';
export interface ToggleChangeEvent {
currentTarget: {
checked: boolean;
};
preventDefault: () => void;
stopPropagation: () => void;
readonly defaultPrevented: boolean;
}
const TOGGLE_CHECKED_CLASS = 'bg-blue-500';
const TOGGLE_UNCHECKED_CLASS = 'bg-border';
// A disabled switch (read-only session, plan lock, save in flight) keeps the
// track colour of its state and dims, so it still says whether the setting is
// on. The old bg-base track named no theme colour and rendered transparent:
// a disabled switch lost its track and an enabled-but-locked one read as off.
const TOGGLE_DISABLED_CLASS = 'cursor-not-allowed opacity-50';
const TOGGLE_KNOB_BASE_CLASS = 'bg-surface border border-border-subtle';
const TOGGLE_CONTAINER_CLASS = 'flex items-center gap-3';
const TOGGLE_LABEL_CLASS = 'flex flex-col text-sm text-base-content';
const TOGGLE_DESCRIPTION_CLASS = 'text-xs text-muted';
const toggleSizeConfig: Record<ToggleSize, { track: string; knob: string; translateOn: string }> = {
xs: {
track: 'h-6 w-10 sm:h-5 sm:w-9',
knob: 'h-5 w-5 sm:h-4 sm:w-4',
translateOn: 'translate-x-4',
},
sm: {
track: 'h-7 w-11 sm:h-6 sm:w-10',
knob: 'h-6 w-6 sm:h-5 sm:w-5',
translateOn: 'translate-x-4',
},
md: {
track: 'h-8 w-12 sm:h-7 sm:w-12',
knob: 'h-7 w-7 sm:h-6 sm:w-6',
translateOn: 'translate-x-4 sm:translate-x-5',
},
};
export function resolveToggleSize(
size: ToggleSize | undefined,
fallback: ToggleSize = 'sm',
): ToggleSize {
return size ?? fallback;
}
export function getToggleTrackClass(
size: ToggleSize,
checked: boolean,
disabled: boolean,
className?: string,
): string {
const config = toggleSizeConfig[size];
return `relative inline-flex ${config.track} shrink-0 items-center rounded-full p-0.5 transition-all duration-300 ease-[cubic-bezier(0.34,1.56,0.64,1)] after:absolute after:-inset-y-2.5 after:inset-x-0 after:content-[''] focus:outline-hidden focus:ring-0 ${
checked ? TOGGLE_CHECKED_CLASS : TOGGLE_UNCHECKED_CLASS
} ${disabled ? TOGGLE_DISABLED_CLASS : ''} ${className ?? ''}`.trim();
}
// The knob sits inside the track, so a disabled track's opacity already dims it.
export function getToggleKnobClass(size: ToggleSize, checked: boolean, _disabled: boolean): string {
const config = toggleSizeConfig[size];
return `inline-block ${config.knob} rounded-full transition-transform duration-300 ease-[cubic-bezier(0.34,1.56,0.64,1)] ${TOGGLE_KNOB_BASE_CLASS} ${
checked ? config.translateOn : 'translate-x-0'
}`.trim();
}
export function getToggleContainerClass(containerClass?: string): string {
return `${TOGGLE_CONTAINER_CLASS} ${containerClass ?? ''}`.trim();
}
export function getToggleLabelClass(): string {
return TOGGLE_LABEL_CLASS;
}
export function getToggleDescriptionClass(): string {
return TOGGLE_DESCRIPTION_CLASS;
}