This commit is contained in:
2026-05-31 18:09:55 +03:00
parent 22afa7e1a5
commit a5d2742c7c
6 changed files with 123 additions and 33 deletions
+30 -3
View File
@@ -1,4 +1,4 @@
import { type ComponentPropsWithoutRef } from 'react'; import { type ComponentPropsWithoutRef, useEffect, useRef } from 'react';
import { ToggleGroup as RToggleGroup } from 'radix-ui'; import { ToggleGroup as RToggleGroup } from 'radix-ui';
import { cx } from '../utils'; import { cx } from '../utils';
@@ -18,14 +18,40 @@ export const SegmentedControl = ({
items, items,
className, className,
...props ...props
}: SegProps) => ( }: SegProps) => {
const rootRef = useRef<HTMLDivElement>(null);
const thumbRef = useRef<HTMLSpanElement>(null);
const initialized = useRef(false);
useEffect(() => {
const root = rootRef.current;
const thumb = thumbRef.current;
if (!root || !thumb) return;
const selected = root.querySelector<HTMLElement>('[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 (
<RToggleGroup.Root <RToggleGroup.Root
ref={rootRef}
type="single" type="single"
className={cx('modern-sk-seg', className)} className={cx('modern-sk-seg', className)}
value={value} value={value}
onValueChange={(v) => v && onValueChange(v)} onValueChange={(v) => v && onValueChange(v)}
{...props} {...props}
> >
<span ref={thumbRef} className="modern-sk-seg__thumb" aria-hidden />
{items.map((it) => ( {items.map((it) => (
<RToggleGroup.Item <RToggleGroup.Item
key={it.value} key={it.value}
@@ -36,4 +62,5 @@ export const SegmentedControl = ({
</RToggleGroup.Item> </RToggleGroup.Item>
))} ))}
</RToggleGroup.Root> </RToggleGroup.Root>
); );
};
+51 -3
View File
@@ -1,14 +1,62 @@
import { type ComponentPropsWithoutRef } from 'react'; import { type ComponentPropsWithoutRef } from 'react';
import { Slider as RSlider } from 'radix-ui'; import { Slider as RSlider } from 'radix-ui';
export const Slider = (props: ComponentPropsWithoutRef<typeof RSlider.Root>) => ( type Step = { value: number; label?: string };
<RSlider.Root className="modern-sk-slider" {...props}>
type SliderProps = ComponentPropsWithoutRef<typeof RSlider.Root> & {
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 (
<RSlider.Root
className={`modern-sk-slider${hasSteps ? ' modern-sk-slider--has-steps' : ''}`}
min={min}
max={max}
{...props}
>
<RSlider.Track className="modern-sk-slider__track"> <RSlider.Track className="modern-sk-slider__track">
<RSlider.Range className="modern-sk-slider__range" /> <RSlider.Range className="modern-sk-slider__range" />
{hasSteps && resolved.map((step) => (
<div
key={step.value}
className="modern-sk-slider__step-dot"
aria-hidden
style={{ left: `${((step.value - min) / (max - min)) * 100}%` }}
/>
))}
</RSlider.Track> </RSlider.Track>
<RSlider.Thumb className="modern-sk-slider__thumb" aria-label="Value" /> <RSlider.Thumb className="modern-sk-slider__thumb" aria-label="Value" />
{hasSteps && (
<div className="modern-sk-slider__steps" aria-hidden>
{resolved.map((step) => (
<div
key={step.value}
className="modern-sk-slider__step"
style={{ left: `${((step.value - min) / (max - min)) * 100}%` }}
>
<div className="modern-sk-slider__step-tick" />
{step.label != null && (
<span className="modern-sk-slider__step-label">{step.label}</span>
)}
</div>
))}
</div>
)}
</RSlider.Root> </RSlider.Root>
); );
};
export const Stepper = ({ export const Stepper = ({
onDecrement, onDecrement,
+1 -1
View File
@@ -9,7 +9,7 @@ type TooltipProps = {
open?: boolean; open?: boolean;
defaultOpen?: boolean; defaultOpen?: boolean;
onOpenChange?: (o: boolean) => void; onOpenChange?: (o: boolean) => void;
} & Omit<ComponentPropsWithoutRef<typeof RTooltip.Content>, 'children'>; } & Omit<ComponentPropsWithoutRef<typeof RTooltip.Content>, 'children' | 'content'>;
export const Tooltip = ({ export const Tooltip = ({
content, content,
+1 -1
View File
@@ -40,7 +40,7 @@ export const WithBody: Story = {
description="Choose a new name for this project." description="Choose a new name for this project."
footer={<Button variant="primary">Save</Button>} footer={<Button variant="primary">Save</Button>}
> >
<TextField label="Project name" defaultValue="My project" /> <TextField placeholder="Project name" defaultValue="My project" />
</Dialog> </Dialog>
), ),
}; };
+8
View File
@@ -23,6 +23,14 @@ const meta = {
}, },
}, },
}, },
argTypes: {
children: { control: false },
content: { control: false },
},
args: {
content: '',
children: null,
},
} satisfies Meta<typeof Tooltip>; } satisfies Meta<typeof Tooltip>;
export default meta; export default meta;
+10 -3
View File
@@ -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; } .modern-sk-control{ display:inline-flex; align-items:center; gap:10px; font-size:14px; color:var(--fg-2); cursor:pointer; }
/* ---------- SEGMENTED CONTROL (Radix ToggleGroup) ---------- */ /* ---------- 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{ 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__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__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: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 ---------- */ /* ---------- SLIDER ---------- */
.modern-sk-slider{ position:relative; display:flex; align-items:center; width:200px; height:20px; user-select:none; touch-action:none; } .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__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{ 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__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 ---------- */ /* ---------- 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); } .modern-sk-stepper{ display:inline-flex; border-radius:var(--r-md); overflow:hidden; box-shadow:var(--shadow-raised); border:1px solid var(--hair-strong); }