mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-26 02:06:49 +00:00
feat(ui): replace Switch with TogglePill per design audit (#687)
* chore: ignore local design-system references Adds frontend/DESIGN.md and .design-bundle/ to .gitignore so the design-system reference and the audit handoff bundle stay local to each contributor's machine. * feat(ui): replace Switch with TogglePill per design audit Adds a mono-uppercase ON/OFF pill that replaces the sliding Switch across every call site. The pill uses role="switch" plus aria-checked, tints success-green when on and neutral card-tone when off, and holds a stable 60px min-width so the label flip does not shift layout. It reinforces the tracked-mono uppercase pattern already used for kicker labels and table column headers, and echoes the UP/DN status language of the sidebar stack list. - Add TogglePill component (frontend/src/components/ui/toggle-pill.tsx) - Replace 17 Switch call sites across 10 files (settings sections, SSO providers, scheduled operations, network topology, resources, stack auto-heal) - Remove unused Switch wrapper and radix primitive - Drop the @radix-ui/react-switch dependency
This commit is contained in:
@@ -1,156 +0,0 @@
|
||||
/* eslint-disable */
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { Switch as SwitchPrimitives } from 'radix-ui';
|
||||
import {
|
||||
motion,
|
||||
type TargetAndTransition,
|
||||
type VariantLabels,
|
||||
type HTMLMotionProps,
|
||||
type LegacyAnimationControls,
|
||||
} from 'motion/react';
|
||||
|
||||
import { getStrictContext } from '@/lib/get-strict-context';
|
||||
import { useControlledState } from '@/hooks/use-controlled-state';
|
||||
|
||||
type SwitchContextType = {
|
||||
isChecked: boolean;
|
||||
setIsChecked: (isChecked: boolean) => void;
|
||||
isPressed: boolean;
|
||||
setIsPressed: (isPressed: boolean) => void;
|
||||
};
|
||||
|
||||
const [SwitchProvider, useSwitch] =
|
||||
getStrictContext<SwitchContextType>('SwitchContext');
|
||||
|
||||
type SwitchProps = Omit<
|
||||
React.ComponentProps<typeof SwitchPrimitives.Root>,
|
||||
'asChild'
|
||||
> &
|
||||
HTMLMotionProps<'button'>;
|
||||
|
||||
function Switch(props: SwitchProps) {
|
||||
// Destructure Radix-only props so they don't leak onto the motion.button DOM element
|
||||
const {
|
||||
checked,
|
||||
defaultChecked,
|
||||
onCheckedChange,
|
||||
disabled,
|
||||
required,
|
||||
name,
|
||||
value,
|
||||
form,
|
||||
...motionProps
|
||||
} = props;
|
||||
|
||||
const [isPressed, setIsPressed] = React.useState(false);
|
||||
const [isChecked, setIsChecked] = useControlledState({
|
||||
value: checked,
|
||||
defaultValue: defaultChecked,
|
||||
onChange: onCheckedChange,
|
||||
});
|
||||
|
||||
return (
|
||||
<SwitchProvider
|
||||
value={{ isChecked, setIsChecked, isPressed, setIsPressed }}
|
||||
>
|
||||
<SwitchPrimitives.Root
|
||||
checked={checked}
|
||||
defaultChecked={defaultChecked}
|
||||
onCheckedChange={setIsChecked}
|
||||
disabled={disabled}
|
||||
required={required}
|
||||
name={name}
|
||||
value={value}
|
||||
form={form}
|
||||
asChild
|
||||
>
|
||||
<motion.button
|
||||
data-slot="switch"
|
||||
whileTap="tap"
|
||||
initial={false}
|
||||
onTapStart={() => setIsPressed(true)}
|
||||
onTapCancel={() => setIsPressed(false)}
|
||||
onTap={() => setIsPressed(false)}
|
||||
{...motionProps}
|
||||
/>
|
||||
</SwitchPrimitives.Root>
|
||||
</SwitchProvider>
|
||||
);
|
||||
}
|
||||
|
||||
type SwitchThumbProps = Omit<
|
||||
React.ComponentProps<typeof SwitchPrimitives.Thumb>,
|
||||
'asChild'
|
||||
> &
|
||||
HTMLMotionProps<'div'> & {
|
||||
pressedAnimation?:
|
||||
| TargetAndTransition
|
||||
| VariantLabels
|
||||
| boolean
|
||||
| LegacyAnimationControls;
|
||||
};
|
||||
|
||||
function SwitchThumb({
|
||||
pressedAnimation,
|
||||
transition = { type: 'spring', stiffness: 300, damping: 25 },
|
||||
...props
|
||||
}: SwitchThumbProps) {
|
||||
const { isPressed } = useSwitch();
|
||||
|
||||
return (
|
||||
<SwitchPrimitives.Thumb asChild>
|
||||
<motion.div
|
||||
data-slot="switch-thumb"
|
||||
whileTap="tab"
|
||||
layout
|
||||
transition={transition}
|
||||
animate={isPressed ? pressedAnimation : undefined}
|
||||
{...props}
|
||||
/>
|
||||
</SwitchPrimitives.Thumb>
|
||||
);
|
||||
}
|
||||
|
||||
type SwitchIconPosition = 'left' | 'right' | 'thumb';
|
||||
|
||||
type SwitchIconProps = HTMLMotionProps<'div'> & {
|
||||
position: SwitchIconPosition;
|
||||
};
|
||||
|
||||
function SwitchIcon({
|
||||
position,
|
||||
transition = { type: 'spring', bounce: 0 },
|
||||
...props
|
||||
}: SwitchIconProps) {
|
||||
const { isChecked } = useSwitch();
|
||||
|
||||
const isAnimated = React.useMemo(() => {
|
||||
if (position === 'right') return !isChecked;
|
||||
if (position === 'left') return isChecked;
|
||||
if (position === 'thumb') return true;
|
||||
return false;
|
||||
}, [position, isChecked]);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
data-slot={`switch-${position}-icon`}
|
||||
animate={isAnimated ? { scale: 1, opacity: 1 } : { scale: 0, opacity: 0 }}
|
||||
transition={transition}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Switch,
|
||||
SwitchThumb,
|
||||
SwitchIcon,
|
||||
useSwitch,
|
||||
type SwitchProps,
|
||||
type SwitchThumbProps,
|
||||
type SwitchIconProps,
|
||||
type SwitchIconPosition,
|
||||
type SwitchContextType,
|
||||
};
|
||||
Reference in New Issue
Block a user