mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-23 20:25:56 +00:00
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.
This commit is contained in:
@@ -5070,6 +5070,10 @@ plus the synthetic toggle change-event runtime, and
|
||||
track/knob/container class policy, and the canonical toggle event type.
|
||||
Future toggle work should extend those owners instead of pushing synthetic
|
||||
event behavior or size/class policy back into the shell.
|
||||
A disabled toggle (a read-only session, a plan lock, a save in flight) keeps
|
||||
its on or off track colour and dims, so it still shows whether the setting is
|
||||
on; it must not swap to a separate disabled background, which hid the track
|
||||
and made a locked-on setting read as off.
|
||||
Binary on/off controls are registry-backed too. Product surfaces must compose
|
||||
`Toggle` or `TogglePrimitive` for shared track/knob styling, disabled behavior,
|
||||
label/description wiring, and synthetic checked events instead of recreating
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"version": 1,
|
||||
"base_sha": "1dcf9418aae87f9427dbfc6bcae67dbde114bf8a",
|
||||
"verified_at": "2026-10-05T20:47:57Z",
|
||||
"result": "passed",
|
||||
"changed_paths": [
|
||||
"frontend-modern/src/components/shared/toggleModel.ts"
|
||||
],
|
||||
"content_sha256": {
|
||||
"frontend-modern/src/components/shared/toggleModel.ts": "141b8981ae6c92eb4a3f3c8d5a82ce9b8a470f675551ec5b93d2638a5330a5d6"
|
||||
},
|
||||
"routes": [
|
||||
"/settings/pulse-intelligence/patrol"
|
||||
],
|
||||
"viewports": [
|
||||
{
|
||||
"width": 1440,
|
||||
"height": 1000
|
||||
},
|
||||
{
|
||||
"width": 390,
|
||||
"height": 1000
|
||||
}
|
||||
],
|
||||
"states": [
|
||||
"Settings > Patrol with the Container update risk toggle disabled by its plan lock and off (the /api/settings/ai response set to what a free-tier backend returns), light and dark",
|
||||
"Before (main): the disabled toggle's track computed to rgba(0,0,0,0), leaving only a faint half-opacity knob on the card",
|
||||
"After: the track keeps its off colour (rgb(226,232,240) light, rgb(51,65,85) dark) at 50% opacity and reads as a dimmed off switch; enabled toggles beside it are unchanged",
|
||||
"With the dev stack's unmasked response (on), the same disabled toggle reads as a dimmed on switch rather than an empty track",
|
||||
"Phone at 390: same rendering"
|
||||
],
|
||||
"interactions": [
|
||||
"Compared main (reverse-applied patch) and the change with the settings response intercepted, at 1440 light and dark and 390"
|
||||
]
|
||||
}
|
||||
@@ -2117,6 +2117,10 @@ describe('shared primitive guardrails', () => {
|
||||
expect(toggleModelSource).toContain('getToggleTrackClass');
|
||||
expect(toggleModelSource).toContain('getToggleKnobClass');
|
||||
expect(toggleModelSource).toContain('ToggleChangeEvent');
|
||||
// A disabled toggle keeps its on/off track and dims; bg-base names no
|
||||
// theme colour, so a disabled track built on it rendered transparent.
|
||||
expect(toggleModelSource).not.toMatch(/TOGGLE_DISABLED_CLASS = '[^']*bg-base/);
|
||||
expect(toggleModelSource).toContain('checked ? TOGGLE_CHECKED_CLASS : TOGGLE_UNCHECKED_CLASS');
|
||||
|
||||
expect(dockerRuntimeSettingsCardSource).toContain('TogglePrimitive');
|
||||
expect(dockerRuntimeSettingsCardSource).toContain('ariaLabelledBy');
|
||||
|
||||
@@ -32,6 +32,23 @@ describe('Toggle', () => {
|
||||
expect(toggleModelSource).toContain('ToggleChangeEvent');
|
||||
});
|
||||
|
||||
it('keeps a disabled toggle on its on or off colour, dimmed', () => {
|
||||
render(() => (
|
||||
<>
|
||||
<Toggle checked disabled ariaLabel="Locked on" />
|
||||
<Toggle checked={false} disabled ariaLabel="Locked off" />
|
||||
</>
|
||||
));
|
||||
|
||||
const lockedOn = screen.getByRole('button', { name: 'Locked on' });
|
||||
const lockedOff = screen.getByRole('button', { name: 'Locked off' });
|
||||
expect(lockedOn).toHaveClass('bg-blue-500', 'opacity-50', 'cursor-not-allowed');
|
||||
expect(lockedOff).toHaveClass('bg-border', 'opacity-50', 'cursor-not-allowed');
|
||||
// bg-base names no theme colour, so it rendered no track at all.
|
||||
expect(lockedOn.className).not.toContain('bg-base');
|
||||
expect(lockedOff.className).not.toContain('bg-base');
|
||||
});
|
||||
|
||||
it('emits the synthetic next checked state and respects preventDefault', () => {
|
||||
const onToggle = vi.fn();
|
||||
const onChange = vi.fn((event) => event.preventDefault());
|
||||
|
||||
@@ -11,7 +11,11 @@ export interface ToggleChangeEvent {
|
||||
|
||||
const TOGGLE_CHECKED_CLASS = 'bg-blue-500';
|
||||
const TOGGLE_UNCHECKED_CLASS = 'bg-border';
|
||||
const TOGGLE_DISABLED_CLASS = 'bg-base cursor-not-allowed opacity-50';
|
||||
// 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';
|
||||
@@ -50,15 +54,16 @@ export function getToggleTrackClass(
|
||||
): 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 ${
|
||||
disabled ? TOGGLE_DISABLED_CLASS : checked ? TOGGLE_CHECKED_CLASS : TOGGLE_UNCHECKED_CLASS
|
||||
} ${className ?? ''}`.trim();
|
||||
checked ? TOGGLE_CHECKED_CLASS : TOGGLE_UNCHECKED_CLASS
|
||||
} ${disabled ? TOGGLE_DISABLED_CLASS : ''} ${className ?? ''}`.trim();
|
||||
}
|
||||
|
||||
export function getToggleKnobClass(size: ToggleSize, checked: boolean, disabled: boolean): string {
|
||||
// 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'
|
||||
} ${disabled ? 'opacity-60' : ''}`.trim();
|
||||
}`.trim();
|
||||
}
|
||||
|
||||
export function getToggleContainerClass(containerClass?: string): string {
|
||||
|
||||
Reference in New Issue
Block a user