diff --git a/src/components/segmented-control/index.tsx b/src/components/segmented-control/index.tsx index 8195804..2e6b7c4 100644 --- a/src/components/segmented-control/index.tsx +++ b/src/components/segmented-control/index.tsx @@ -1,4 +1,4 @@ -import { type ComponentPropsWithoutRef } from 'react'; +import { type ComponentPropsWithoutRef, useEffect, useRef } from 'react'; import { ToggleGroup as RToggleGroup } from 'radix-ui'; import { cx } from '../utils'; @@ -18,22 +18,49 @@ export const SegmentedControl = ({ items, className, ...props -}: SegProps) => ( - v && onValueChange(v)} - {...props} - > - {items.map((it) => ( - - {it.label} - - ))} - -); +}: SegProps) => { + const rootRef = useRef(null); + const thumbRef = useRef(null); + const initialized = useRef(false); + + useEffect(() => { + const root = rootRef.current; + const thumb = thumbRef.current; + if (!root || !thumb) return; + const selected = root.querySelector('[data-state="on"]'); + if (!selected) return; + + if (!initialized.current) { + thumb.style.transition = 'none'; + } + thumb.style.transform = `translateX(${selected.offsetLeft}px)`; + thumb.style.width = `${selected.offsetWidth}px`; + if (!initialized.current) { + thumb.getBoundingClientRect(); + thumb.style.transition = ''; + initialized.current = true; + } + }, [value]); + + return ( + v && onValueChange(v)} + {...props} + > + + {items.map((it) => ( + + {it.label} + + ))} + + ); +}; diff --git a/src/components/slider/index.tsx b/src/components/slider/index.tsx index 0697090..ce4d6e4 100644 --- a/src/components/slider/index.tsx +++ b/src/components/slider/index.tsx @@ -1,14 +1,62 @@ import { type ComponentPropsWithoutRef } from 'react'; import { Slider as RSlider } from 'radix-ui'; -export const Slider = (props: ComponentPropsWithoutRef) => ( - - - - - - -); +type Step = { value: number; label?: string }; + +type SliderProps = ComponentPropsWithoutRef & { + steps?: number | Step[]; +}; + +function resolveSteps(steps: number | Step[], min: number, max: number): Step[] { + if (Array.isArray(steps)) return steps; + if (steps < 2) return []; + return Array.from({ length: steps }, (_, i) => ({ + value: min + (i / (steps - 1)) * (max - min), + })); +} + +export const Slider = ({ steps, min = 0, max = 100, ...props }: SliderProps) => { + const resolved = steps != null ? resolveSteps(steps, min, max) : []; + const hasSteps = resolved.length > 0; + + return ( + + + + {hasSteps && resolved.map((step) => ( +
+ ))} + + + {hasSteps && ( +
+ {resolved.map((step) => ( +
+
+ {step.label != null && ( + {step.label} + )} +
+ ))} +
+ )} + + ); +}; export const Stepper = ({ onDecrement, diff --git a/src/components/tooltip/index.tsx b/src/components/tooltip/index.tsx index 1261fe9..16af9d3 100644 --- a/src/components/tooltip/index.tsx +++ b/src/components/tooltip/index.tsx @@ -9,7 +9,7 @@ type TooltipProps = { open?: boolean; defaultOpen?: boolean; onOpenChange?: (o: boolean) => void; -} & Omit, 'children'>; +} & Omit, 'children' | 'content'>; export const Tooltip = ({ content, diff --git a/src/stories/Dialog.stories.tsx b/src/stories/Dialog.stories.tsx index a123f36..d0ef54c 100644 --- a/src/stories/Dialog.stories.tsx +++ b/src/stories/Dialog.stories.tsx @@ -40,7 +40,7 @@ export const WithBody: Story = { description="Choose a new name for this project." footer={} > - + ), }; diff --git a/src/stories/Overlays.stories.tsx b/src/stories/Overlays.stories.tsx index 4511122..cbd15d2 100644 --- a/src/stories/Overlays.stories.tsx +++ b/src/stories/Overlays.stories.tsx @@ -23,6 +23,14 @@ const meta = { }, }, }, + argTypes: { + children: { control: false }, + content: { control: false }, + }, + args: { + content: '', + children: null, + }, } satisfies Meta; export default meta; diff --git a/src/styles/components.css b/src/styles/components.css index ef88cd2..14149fd 100644 --- a/src/styles/components.css +++ b/src/styles/components.css @@ -165,10 +165,11 @@ textarea.modern-sk-field{ resize:vertical; min-height:64px; line-height:1.5; } .modern-sk-control{ display:inline-flex; align-items:center; gap:10px; font-size:14px; color:var(--fg-2); cursor:pointer; } /* ---------- SEGMENTED CONTROL (Radix ToggleGroup) ---------- */ -.modern-sk-seg{ display:inline-flex; background:var(--steel-900); border:1px solid var(--edge-inset); border-radius:var(--r-md); padding:3px; box-shadow:var(--shadow-inset-well); gap:2px; } -.modern-sk-seg__item{ font-family:var(--font-sans); font-size:13px; font-weight:600; color:var(--fg-2); background:transparent; border:none; padding:var(--seg-pad-y) 14px; border-radius:var(--r-sm); cursor:pointer; transition:color var(--dur-quick), background var(--dur-quick); } +.modern-sk-seg{ position:relative; display:inline-flex; background:var(--steel-900); border:1px solid var(--edge-inset); border-radius:var(--r-md); padding:3px; box-shadow:var(--shadow-inset-well); gap:2px; } +.modern-sk-seg__thumb{ position:absolute; top:3px; left:0; height:calc(100% - 6px); background:var(--grad-key); border-radius:var(--r-sm); box-shadow:var(--shadow-raised); pointer-events:none; transition:transform 180ms var(--ease-snap), width 180ms var(--ease-snap); will-change:transform,width; } +.modern-sk-seg__item{ position:relative; z-index:1; font-family:var(--font-sans); font-size:13px; font-weight:600; color:var(--fg-2); background:transparent; border:none; padding:var(--seg-pad-y) 14px; border-radius:var(--r-sm); cursor:pointer; transition:color var(--dur-quick); } .modern-sk-seg__item:hover{ color:var(--fg-1); } -.modern-sk-seg__item[data-state="on"]{ color:var(--fg-1); background:var(--grad-key); box-shadow:var(--shadow-raised); } +.modern-sk-seg__item[data-state="on"]{ color:var(--fg-1); } /* ---------- SLIDER ---------- */ .modern-sk-slider{ position:relative; display:flex; align-items:center; width:200px; height:20px; user-select:none; touch-action:none; } @@ -176,6 +177,12 @@ textarea.modern-sk-field{ resize:vertical; min-height:64px; line-height:1.5; } .modern-sk-slider__range{ position:absolute; height:100%; border-radius:3px; background:linear-gradient(90deg,var(--lime-deep),var(--lime)); box-shadow:0 0 10px rgba(190,242,100,.4); } .modern-sk-slider__thumb{ display:block; width:20px; height:20px; border-radius:50%; background:linear-gradient(180deg,#fff,#e6e8dd); box-shadow:0 2px 5px rgba(0,0,0,.5), 0 1px 0 rgba(255,255,255,.9) inset; cursor:pointer; outline:none; } .modern-sk-slider__thumb:focus-visible{ box-shadow:0 2px 5px rgba(0,0,0,.5), 0 1px 0 rgba(255,255,255,.9) inset, var(--focus-ring); } +.modern-sk-slider--has-steps{ padding-bottom:22px; } +.modern-sk-slider__step-dot{ position:absolute; top:50%; width:4px; height:4px; border-radius:50%; background:var(--steel-500); transform:translate(-50%,-50%); z-index:1; pointer-events:none; } +.modern-sk-slider__steps{ position:absolute; left:10px; right:10px; top:calc(50% + 6px); pointer-events:none; } +.modern-sk-slider__step{ position:absolute; transform:translateX(-50%); display:flex; flex-direction:column; align-items:center; gap:3px; } +.modern-sk-slider__step-tick{ width:1px; height:5px; background:var(--steel-600); } +.modern-sk-slider__step-label{ font-family:var(--font-mono); font-size:10px; line-height:1; color:var(--fg-3); white-space:nowrap; } /* ---------- STEPPER ---------- */ .modern-sk-stepper{ display:inline-flex; border-radius:var(--r-md); overflow:hidden; box-shadow:var(--shadow-raised); border:1px solid var(--hair-strong); }