diff --git a/src/base.css b/src/base.css index 04d56db4..af1fede3 100644 --- a/src/base.css +++ b/src/base.css @@ -305,7 +305,7 @@ --border-neutral-hover: var(--color-neutral-500); --border-neutral-default: var(--color-neutral-400); --border-neutral-disabled: var(--color-neutral-400-40); - --border-neutral-softest: var(--color-neutral-300); + --border-neutral-softest: var(--color-neutral-200); --border-neutral-inset: var(--color-base-white); --border-neutral-alpha: hsla( 0, @@ -698,7 +698,7 @@ --border-neutral-hover: var(--color-neutral-500); --border-neutral-default: var(--color-neutral-600); --border-neutral-disabled: var(--color-neutral-600-40); - --border-neutral-softest: var(--color-neutral-700); + --border-neutral-softest: var(--color-neutral-800-56); --border-neutral-inset: var(--color-base-black); --border-neutral-alpha: hsla( 255, diff --git a/src/components/Input/index.tsx b/src/components/Input/index.tsx index 42a437af..eb144def 100644 --- a/src/components/Input/index.tsx +++ b/src/components/Input/index.tsx @@ -58,7 +58,7 @@ export function Input({ {...commonProps} {...props} className={cn( - 'bg-surface-primary-default placeholder:text-placeholder text-default h-full w-full rounded-md text-sm shadow-none outline-none placeholder:transition-colors placeholder:duration-500 disabled:cursor-not-allowed disabled:opacity-50', + 'bg-surface-primary-default placeholder:text-placeholder text-default h-full w-full text-sm shadow-none outline-none disabled:cursor-not-allowed disabled:opacity-50', isFocused && 'placeholder:text-default' )} /> @@ -72,7 +72,7 @@ export function Input({ cols={30} rows={10} className={cn( - 'bg-surface-primary-default placeholder:text-placeholder text-default my-2 h-full max-h-60 min-h-16 w-full rounded-md px-3 py-3 text-sm shadow-none outline-none placeholder:transition-colors placeholder:duration-500 disabled:cursor-not-allowed disabled:opacity-50', + 'bg-surface-primary-default placeholder:text-placeholder text-default my-2 h-full max-h-60 min-h-16 w-full px-3 py-3 text-sm shadow-none outline-none disabled:cursor-not-allowed disabled:opacity-50', isFocused && 'placeholder:text-default' )} /> @@ -82,7 +82,7 @@ export function Input({ return (
+ +const meta: Meta = { + component: SegmentedButton, + tags: ['autodocs'], +} + +export default meta + +const StoryRenderer = (args: SegmentedButtonProps) => { + const getInitialActiveId = () => { + let foundActiveId: string | null = null + React.Children.forEach(args.children, (child) => { + if ( + foundActiveId === null && + React.isValidElement(child) && + child.props.active && + child.props.id + ) { + foundActiveId = child.props.id as string + } + }) + + if (foundActiveId) return foundActiveId + + let firstId: string | null = null + React.Children.forEach(args.children, (child) => { + if (firstId === null && React.isValidElement(child) && child.props.id) { + firstId = child.props.id as string + } + }) + + return firstId + } + + const [activeId, setActiveId] = useState(() => + getInitialActiveId() + ) + return ( + + {React.Children.map(args.children, (child) => { + if (!React.isValidElement(child)) { + return child + } + + const props: SegmentedButtonItemProps = { + id: child.props.id ?? '', + active: child.props.id === activeId, + children: child.props.children, + onClick: () => setActiveId(child.props.id ?? ''), + } + return React.cloneElement(child, props) + })} + + ) +} + +export const Default: Story = { + args: { + children: [ + Chat, + + Split + , + Code, + ], + }, + render: (args) => , +} + +export const TwoItems: Story = { + args: { + children: [ + Chat, + + Split + , + ], + }, + render: (args) => , +} + +export const FourItems: Story = { + args: { + children: [ + Chat, + + Split + , + Code, + Beast Mode, + ], + }, + render: (args) => , +} diff --git a/src/components/SegmentedButton/index.tsx b/src/components/SegmentedButton/index.tsx new file mode 100644 index 00000000..1661387d --- /dev/null +++ b/src/components/SegmentedButton/index.tsx @@ -0,0 +1,108 @@ +import { cn } from '@/lib/utils' +import { motion, MotionProps } from 'framer-motion' +import React from 'react' +import styles from './index.module.css' + +export interface SegmentedButtonProps { + children: React.ReactNode + className?: string +} + +const SegmentedButtonBase = ({ children, className }: SegmentedButtonProps) => { + const [hoveredId, setHoveredId] = React.useState(null) + + const enhancedChildren = React.Children.map(children, (child) => { + if (!React.isValidElement(child)) return child + const childId: string | undefined = child.props.id + + const handleMouseEnter: React.MouseEventHandler = ( + e + ) => { + setHoveredId(childId ?? null) + child.props.onMouseEnter?.(e) + } + + const handleMouseLeave: React.MouseEventHandler = ( + e + ) => { + setHoveredId((current) => (current === childId ? null : current)) + child.props.onMouseLeave?.(e) + } + + const highlighted = hoveredId ? childId === hoveredId : child.props.active + + return React.cloneElement( + child as React.ReactElement, + { + highlighted, + onMouseEnter: handleMouseEnter, + onMouseLeave: handleMouseLeave, + } + ) + }) + + return ( +
setHoveredId(null)} + > + {enhancedChildren} +
+ ) +} + +SegmentedButtonBase.displayName = 'SegmentedButton' + +export interface SegmentedButtonItemProps extends MotionProps { + children: React.ReactNode + className?: string + active?: boolean + highlighted?: boolean + id: string + onClick?: () => void + onMouseEnter?: React.MouseEventHandler + onMouseLeave?: React.MouseEventHandler +} + +const SegmentedButtonItem = ({ + children, + className, + highlighted, + ...props +}: SegmentedButtonItemProps) => { + return ( + + {highlighted && ( + + )} + + {children} + + + ) +} + +SegmentedButtonItem.displayName = 'SegmentedButtonItem' + +export const SegmentedButton = Object.assign(SegmentedButtonBase, { + Item: SegmentedButtonItem, +}) diff --git a/src/index.ts b/src/index.ts index ca8b5489..bd7cefda 100644 --- a/src/index.ts +++ b/src/index.ts @@ -107,7 +107,7 @@ export { Facepile, type FacepileProps } from '@/components/Facepile' export { Link, type LinkProps } from '@/components/Link' export { Dialog } from '@/components/Dialog' export { Switch, type SwitchProps } from '@/components/Switch' - +export { SegmentedButton } from '@/components/SegmentedButton' // AppLayout export { AppLayout,