feat: structure
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
import { type ReactNode } from 'react';
|
||||
import { AlertDialog as RAlertDialog } from 'radix-ui';
|
||||
import { Button } from '../button';
|
||||
|
||||
export const AlertDialog = ({
|
||||
trigger,
|
||||
title,
|
||||
description,
|
||||
cancelLabel = 'Cancel',
|
||||
actionLabel = 'Confirm',
|
||||
destructive,
|
||||
onAction,
|
||||
open,
|
||||
defaultOpen,
|
||||
onOpenChange,
|
||||
}: {
|
||||
trigger?: ReactNode;
|
||||
title: string;
|
||||
description?: ReactNode;
|
||||
cancelLabel?: string;
|
||||
actionLabel?: string;
|
||||
destructive?: boolean;
|
||||
onAction?: () => void;
|
||||
open?: boolean;
|
||||
defaultOpen?: boolean;
|
||||
onOpenChange?: (o: boolean) => void;
|
||||
}) => (
|
||||
<RAlertDialog.Root open={open} defaultOpen={defaultOpen} onOpenChange={onOpenChange}>
|
||||
{trigger && <RAlertDialog.Trigger asChild>{trigger}</RAlertDialog.Trigger>}
|
||||
<RAlertDialog.Portal>
|
||||
<RAlertDialog.Overlay className="modern-sk-overlay" />
|
||||
<RAlertDialog.Content className="modern-sk-dialog">
|
||||
<RAlertDialog.Title className="modern-sk-dialog__title">
|
||||
{title}
|
||||
</RAlertDialog.Title>
|
||||
{description && (
|
||||
<RAlertDialog.Description className="modern-sk-dialog__desc">
|
||||
{description}
|
||||
</RAlertDialog.Description>
|
||||
)}
|
||||
<div className="modern-sk-dialog__footer">
|
||||
<RAlertDialog.Cancel asChild>
|
||||
<Button variant="ghost">{cancelLabel}</Button>
|
||||
</RAlertDialog.Cancel>
|
||||
<RAlertDialog.Action asChild>
|
||||
<Button
|
||||
variant={destructive ? 'ember' : 'primary'}
|
||||
onClick={onAction}
|
||||
>
|
||||
{actionLabel}
|
||||
</Button>
|
||||
</RAlertDialog.Action>
|
||||
</div>
|
||||
</RAlertDialog.Content>
|
||||
</RAlertDialog.Portal>
|
||||
</RAlertDialog.Root>
|
||||
);
|
||||
@@ -0,0 +1,44 @@
|
||||
import { type ComponentPropsWithoutRef, type ReactNode } from 'react';
|
||||
import { cx } from '../utils';
|
||||
|
||||
type BadgeVariant = 'lime' | 'ember' | 'neutral' | 'outline';
|
||||
|
||||
export const Badge = ({
|
||||
variant = 'neutral',
|
||||
dot,
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: ComponentPropsWithoutRef<'span'> & {
|
||||
variant?: BadgeVariant;
|
||||
dot?: boolean;
|
||||
}) => (
|
||||
<span
|
||||
className={cx(
|
||||
'modern-sk-badge',
|
||||
`modern-sk-badge--${variant}`,
|
||||
dot && 'modern-sk-badge--dot',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
|
||||
export const Chip = ({
|
||||
children,
|
||||
onRemove,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
onRemove?: () => void;
|
||||
}) => (
|
||||
<span className="modern-sk-chip">
|
||||
{children}
|
||||
{onRemove && (
|
||||
<button type="button" className="x" onClick={onRemove} aria-label="Remove">
|
||||
×
|
||||
</button>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
@@ -0,0 +1,27 @@
|
||||
import { forwardRef, type ComponentPropsWithoutRef } from 'react';
|
||||
import { cx } from '../utils';
|
||||
|
||||
export type BtnVariant = 'key' | 'primary' | 'ember' | 'ghost';
|
||||
|
||||
type ButtonProps = ComponentPropsWithoutRef<'button'> & {
|
||||
variant?: BtnVariant;
|
||||
size?: 'sm';
|
||||
iconOnly?: boolean;
|
||||
};
|
||||
|
||||
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ variant = 'key', size, iconOnly, className, ...props }, ref) => (
|
||||
<button
|
||||
ref={ref}
|
||||
className={cx(
|
||||
'modern-sk-btn',
|
||||
variant !== 'key' && `modern-sk-btn--${variant}`,
|
||||
size === 'sm' && 'modern-sk-btn--sm',
|
||||
iconOnly && 'modern-sk-btn--icon',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
);
|
||||
Button.displayName = 'Button';
|
||||
@@ -0,0 +1,19 @@
|
||||
import { type ReactNode } from 'react';
|
||||
import { cx } from '../utils';
|
||||
|
||||
type CalloutVariant = 'info' | 'success' | 'warning' | 'danger';
|
||||
|
||||
export const Callout = ({
|
||||
variant = 'info',
|
||||
icon,
|
||||
children,
|
||||
}: {
|
||||
variant?: CalloutVariant;
|
||||
icon?: ReactNode;
|
||||
children: ReactNode;
|
||||
}) => (
|
||||
<div className={cx('modern-sk-callout', variant !== 'info' && `modern-sk-callout--${variant}`)}>
|
||||
{icon && <span className="modern-sk-callout__icon">{icon}</span>}
|
||||
<div className="modern-sk-callout__body">{children}</div>
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,6 @@
|
||||
import { type ComponentPropsWithoutRef } from 'react';
|
||||
import { cx } from '../utils';
|
||||
|
||||
export const Card = ({ className, ...props }: ComponentPropsWithoutRef<'div'>) => (
|
||||
<div className={cx('modern-sk-card', className)} {...props} />
|
||||
);
|
||||
@@ -0,0 +1,55 @@
|
||||
import { type ReactNode } from 'react';
|
||||
import { Dialog as RDialog } from 'radix-ui';
|
||||
import { X } from '@phosphor-icons/react';
|
||||
import { IconButton } from '../icon-button';
|
||||
|
||||
export const Dialog = ({
|
||||
trigger,
|
||||
title,
|
||||
description,
|
||||
children,
|
||||
footer,
|
||||
open,
|
||||
defaultOpen,
|
||||
onOpenChange,
|
||||
modal,
|
||||
}: {
|
||||
trigger?: ReactNode;
|
||||
title: string;
|
||||
description?: ReactNode;
|
||||
children?: ReactNode;
|
||||
footer?: ReactNode;
|
||||
open?: boolean;
|
||||
defaultOpen?: boolean;
|
||||
onOpenChange?: (o: boolean) => void;
|
||||
modal?: boolean;
|
||||
}) => (
|
||||
<RDialog.Root open={open} defaultOpen={defaultOpen} onOpenChange={onOpenChange} modal={modal}>
|
||||
{trigger && <RDialog.Trigger asChild>{trigger}</RDialog.Trigger>}
|
||||
<RDialog.Portal>
|
||||
<RDialog.Overlay className="modern-sk-overlay" />
|
||||
<RDialog.Content className="modern-sk-dialog">
|
||||
<RDialog.Title className="modern-sk-dialog__title">{title}</RDialog.Title>
|
||||
{description && (
|
||||
<RDialog.Description className="modern-sk-dialog__desc">
|
||||
{description}
|
||||
</RDialog.Description>
|
||||
)}
|
||||
{children && <div className="modern-sk-dialog__body">{children}</div>}
|
||||
{footer && <div className="modern-sk-dialog__footer">{footer}</div>}
|
||||
<RDialog.Close asChild>
|
||||
<IconButton
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="modern-sk-dialog__close"
|
||||
aria-label="Close"
|
||||
>
|
||||
<X size={14} weight="bold" />
|
||||
</IconButton>
|
||||
</RDialog.Close>
|
||||
</RDialog.Content>
|
||||
</RDialog.Portal>
|
||||
</RDialog.Root>
|
||||
);
|
||||
|
||||
export const DialogClose = RDialog.Close;
|
||||
@@ -0,0 +1,25 @@
|
||||
import { forwardRef, type ComponentPropsWithoutRef } from 'react';
|
||||
import { cx } from '../utils';
|
||||
import type { BtnVariant } from '../button';
|
||||
|
||||
type IconButtonProps = ComponentPropsWithoutRef<'button'> & {
|
||||
variant?: BtnVariant;
|
||||
size?: 'sm' | 'lg';
|
||||
};
|
||||
|
||||
export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
|
||||
({ variant = 'key', size, className, ...props }, ref) => (
|
||||
<button
|
||||
ref={ref}
|
||||
className={cx(
|
||||
'modern-sk-btn',
|
||||
'modern-sk-iconbtn',
|
||||
variant !== 'key' && `modern-sk-btn--${variant}`,
|
||||
size && `modern-sk-iconbtn--${size}`,
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
);
|
||||
IconButton.displayName = 'IconButton';
|
||||
@@ -0,0 +1,17 @@
|
||||
import { type ComponentPropsWithoutRef } from 'react';
|
||||
import { cx } from '../utils';
|
||||
|
||||
export const List = ({ className, ...props }: ComponentPropsWithoutRef<'div'>) => (
|
||||
<div className={cx('modern-sk-list', className)} {...props} />
|
||||
);
|
||||
|
||||
export const Row = ({
|
||||
selected,
|
||||
className,
|
||||
...props
|
||||
}: ComponentPropsWithoutRef<'div'> & { selected?: boolean }) => (
|
||||
<div
|
||||
className={cx('modern-sk-row', selected && 'is-selected', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -0,0 +1,56 @@
|
||||
import { type ComponentPropsWithoutRef, type ReactNode } from 'react';
|
||||
import { DropdownMenu as RMenu } from 'radix-ui';
|
||||
|
||||
export const Menu = RMenu.Root;
|
||||
export const MenuTrigger = RMenu.Trigger;
|
||||
|
||||
export const MenuContent = ({
|
||||
children,
|
||||
...props
|
||||
}: ComponentPropsWithoutRef<typeof RMenu.Content>) => (
|
||||
<RMenu.Portal>
|
||||
<RMenu.Content className="modern-sk-menu" sideOffset={6} {...props}>
|
||||
{children}
|
||||
</RMenu.Content>
|
||||
</RMenu.Portal>
|
||||
);
|
||||
|
||||
export const MenuItem = ({
|
||||
icon,
|
||||
shortcut,
|
||||
children,
|
||||
...props
|
||||
}: ComponentPropsWithoutRef<typeof RMenu.Item> & {
|
||||
icon?: ReactNode;
|
||||
shortcut?: string;
|
||||
}) => (
|
||||
<RMenu.Item className="modern-sk-menu-item" {...props}>
|
||||
{icon && <span className="ph">{icon}</span>}
|
||||
{children}
|
||||
{shortcut && <span className="sc">{shortcut}</span>}
|
||||
</RMenu.Item>
|
||||
);
|
||||
|
||||
export const MenuSeparator = () => (
|
||||
<RMenu.Separator className="modern-sk-menu-sep" />
|
||||
);
|
||||
|
||||
export const MenuSurface = ({ children }: { children: ReactNode }) => (
|
||||
<div className="modern-sk-menu">{children}</div>
|
||||
);
|
||||
|
||||
export const MenuRow = ({
|
||||
icon,
|
||||
shortcut,
|
||||
children,
|
||||
}: {
|
||||
icon?: ReactNode;
|
||||
shortcut?: string;
|
||||
children: ReactNode;
|
||||
}) => (
|
||||
<div className="modern-sk-menu-item">
|
||||
{icon && <span className="ph">{icon}</span>}
|
||||
{children}
|
||||
{shortcut && <span className="sc">{shortcut}</span>}
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,16 @@
|
||||
import { type ComponentPropsWithoutRef } from 'react';
|
||||
import { Progress as RProgress } from 'radix-ui';
|
||||
import { cx } from '../utils';
|
||||
|
||||
export const Progress = ({
|
||||
value = 0,
|
||||
className,
|
||||
...props
|
||||
}: ComponentPropsWithoutRef<typeof RProgress.Root>) => (
|
||||
<RProgress.Root className={cx('modern-sk-progress', className)} value={value} {...props}>
|
||||
<RProgress.Indicator
|
||||
className="modern-sk-progress__indicator"
|
||||
style={{ width: `${value}%` }}
|
||||
/>
|
||||
</RProgress.Root>
|
||||
);
|
||||
@@ -0,0 +1,22 @@
|
||||
import { type ComponentPropsWithoutRef } from 'react';
|
||||
import { ScrollArea as RScrollArea } from 'radix-ui';
|
||||
import { cx } from '../utils';
|
||||
|
||||
export const ScrollArea = ({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: ComponentPropsWithoutRef<typeof RScrollArea.Root>) => (
|
||||
<RScrollArea.Root className={cx('modern-sk-scroll', className)} {...props}>
|
||||
<RScrollArea.Viewport className="modern-sk-scroll__viewport">
|
||||
{children}
|
||||
</RScrollArea.Viewport>
|
||||
<RScrollArea.Scrollbar className="modern-sk-scroll__bar" orientation="vertical">
|
||||
<RScrollArea.Thumb className="modern-sk-scroll__thumb" />
|
||||
</RScrollArea.Scrollbar>
|
||||
<RScrollArea.Scrollbar className="modern-sk-scroll__bar" orientation="horizontal">
|
||||
<RScrollArea.Thumb className="modern-sk-scroll__thumb" />
|
||||
</RScrollArea.Scrollbar>
|
||||
<RScrollArea.Corner />
|
||||
</RScrollArea.Root>
|
||||
);
|
||||
@@ -0,0 +1,39 @@
|
||||
import { type ComponentPropsWithoutRef } from 'react';
|
||||
import { ToggleGroup as RToggleGroup } from 'radix-ui';
|
||||
import { cx } from '../utils';
|
||||
|
||||
type SegProps = Omit<
|
||||
ComponentPropsWithoutRef<typeof RToggleGroup.Root>,
|
||||
'type' | 'onValueChange' | 'defaultValue' | 'value'
|
||||
> & {
|
||||
value: string;
|
||||
defaultValue?: string;
|
||||
onValueChange: (v: string) => void;
|
||||
items: Array<{ value: string; label: string }>;
|
||||
};
|
||||
|
||||
export const SegmentedControl = ({
|
||||
value,
|
||||
onValueChange,
|
||||
items,
|
||||
className,
|
||||
...props
|
||||
}: SegProps) => (
|
||||
<RToggleGroup.Root
|
||||
type="single"
|
||||
className={cx('modern-sk-seg', className)}
|
||||
value={value}
|
||||
onValueChange={(v) => v && onValueChange(v)}
|
||||
{...props}
|
||||
>
|
||||
{items.map((it) => (
|
||||
<RToggleGroup.Item
|
||||
key={it.value}
|
||||
value={it.value}
|
||||
className="modern-sk-seg__item"
|
||||
>
|
||||
{it.label}
|
||||
</RToggleGroup.Item>
|
||||
))}
|
||||
</RToggleGroup.Root>
|
||||
);
|
||||
@@ -0,0 +1,42 @@
|
||||
import { type ComponentPropsWithoutRef } from 'react';
|
||||
import { Select as RSelect } from 'radix-ui';
|
||||
import { Check, CaretDown } from '@phosphor-icons/react';
|
||||
|
||||
type SelectProps = ComponentPropsWithoutRef<typeof RSelect.Root> & {
|
||||
placeholder?: string;
|
||||
items: Array<{ value: string; label: string }>;
|
||||
'aria-label'?: string;
|
||||
};
|
||||
|
||||
export const Select = ({ placeholder, items, ...rest }: SelectProps) => (
|
||||
<RSelect.Root {...rest}>
|
||||
<RSelect.Trigger className="modern-sk-select" aria-label={rest['aria-label']}>
|
||||
<RSelect.Value placeholder={placeholder} />
|
||||
<RSelect.Icon className="modern-sk-select__icon">
|
||||
<CaretDown size={12} weight="bold" />
|
||||
</RSelect.Icon>
|
||||
</RSelect.Trigger>
|
||||
<RSelect.Portal>
|
||||
<RSelect.Content
|
||||
className="modern-sk-select__content"
|
||||
position="popper"
|
||||
sideOffset={6}
|
||||
>
|
||||
<RSelect.Viewport>
|
||||
{items.map((it) => (
|
||||
<RSelect.Item
|
||||
key={it.value}
|
||||
value={it.value}
|
||||
className="modern-sk-select__item"
|
||||
>
|
||||
<RSelect.ItemText>{it.label}</RSelect.ItemText>
|
||||
<RSelect.ItemIndicator className="modern-sk-select__item-indicator">
|
||||
<Check size={14} weight="bold" />
|
||||
</RSelect.ItemIndicator>
|
||||
</RSelect.Item>
|
||||
))}
|
||||
</RSelect.Viewport>
|
||||
</RSelect.Content>
|
||||
</RSelect.Portal>
|
||||
</RSelect.Root>
|
||||
);
|
||||
@@ -0,0 +1,53 @@
|
||||
import { type ComponentPropsWithoutRef, type ReactNode } from 'react';
|
||||
import {
|
||||
Switch as RSwitch,
|
||||
Checkbox as RCheckbox,
|
||||
RadioGroup as RRadioGroup,
|
||||
} from 'radix-ui';
|
||||
|
||||
export const Switch = (props: ComponentPropsWithoutRef<typeof RSwitch.Root>) => (
|
||||
<RSwitch.Root className="modern-sk-switch" {...props}>
|
||||
<RSwitch.Thumb className="modern-sk-switch__thumb" />
|
||||
</RSwitch.Root>
|
||||
);
|
||||
|
||||
export const Checkbox = (props: ComponentPropsWithoutRef<typeof RCheckbox.Root>) => (
|
||||
<RCheckbox.Root className="modern-sk-check" {...props}>
|
||||
<RCheckbox.Indicator className="modern-sk-check__indicator">
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="var(--lime-ink)"
|
||||
strokeWidth={3.5}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M4 12l5 5L20 6" />
|
||||
</svg>
|
||||
</RCheckbox.Indicator>
|
||||
</RCheckbox.Root>
|
||||
);
|
||||
|
||||
export const RadioGroup = RRadioGroup.Root;
|
||||
|
||||
export const RadioItem = ({
|
||||
value,
|
||||
...props
|
||||
}: ComponentPropsWithoutRef<typeof RRadioGroup.Item>) => (
|
||||
<RRadioGroup.Item className="modern-sk-radio" value={value} {...props}>
|
||||
<RRadioGroup.Indicator className="modern-sk-radio__indicator" />
|
||||
</RRadioGroup.Item>
|
||||
);
|
||||
|
||||
export const Control = ({
|
||||
children,
|
||||
control,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
control: ReactNode;
|
||||
}) => (
|
||||
<label className="modern-sk-control">
|
||||
{control}
|
||||
{children}
|
||||
</label>
|
||||
);
|
||||
@@ -0,0 +1,28 @@
|
||||
import { type ComponentPropsWithoutRef } from 'react';
|
||||
import { Slider as RSlider } from 'radix-ui';
|
||||
|
||||
export const Slider = (props: ComponentPropsWithoutRef<typeof RSlider.Root>) => (
|
||||
<RSlider.Root className="modern-sk-slider" {...props}>
|
||||
<RSlider.Track className="modern-sk-slider__track">
|
||||
<RSlider.Range className="modern-sk-slider__range" />
|
||||
</RSlider.Track>
|
||||
<RSlider.Thumb className="modern-sk-slider__thumb" aria-label="Value" />
|
||||
</RSlider.Root>
|
||||
);
|
||||
|
||||
export const Stepper = ({
|
||||
onDecrement,
|
||||
onIncrement,
|
||||
}: {
|
||||
onDecrement: () => void;
|
||||
onIncrement: () => void;
|
||||
}) => (
|
||||
<div className="modern-sk-stepper">
|
||||
<button type="button" onClick={onDecrement} aria-label="Decrease">
|
||||
−
|
||||
</button>
|
||||
<button type="button" onClick={onIncrement} aria-label="Increase">
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,45 @@
|
||||
import { useId, type ComponentPropsWithoutRef } from 'react';
|
||||
import { cx } from '../utils';
|
||||
|
||||
export const Spinner = ({
|
||||
size,
|
||||
className,
|
||||
...props
|
||||
}: ComponentPropsWithoutRef<'span'> & { size?: 'sm' | 'lg' }) => {
|
||||
const gid = `modern-sk-groove-${useId()}`;
|
||||
return (
|
||||
<span
|
||||
role="status"
|
||||
aria-label="Loading"
|
||||
className={cx('modern-sk-spinner', size && `modern-sk-spinner--${size}`, className)}
|
||||
{...props}
|
||||
>
|
||||
<svg viewBox="0 0 36 36" fill="none">
|
||||
<defs>
|
||||
<linearGradient
|
||||
id={gid}
|
||||
x1="18"
|
||||
y1="4"
|
||||
x2="18"
|
||||
y2="32"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset="0" stopColor="var(--spin-groove-1)" />
|
||||
<stop offset="1" stopColor="var(--spin-groove-2)" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<circle cx="18" cy="18" r="14" stroke={`url(#${gid})`} strokeWidth="5" />
|
||||
<circle
|
||||
className="modern-sk-spinner__arc"
|
||||
cx="18"
|
||||
cy="18"
|
||||
r="14"
|
||||
stroke="var(--lime)"
|
||||
strokeWidth="3"
|
||||
strokeLinecap="round"
|
||||
strokeDasharray="22 88"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
import { type ComponentPropsWithoutRef } from 'react';
|
||||
import { cx } from '../utils';
|
||||
|
||||
export const Table = ({ children, ...props }: ComponentPropsWithoutRef<'table'>) => (
|
||||
<div className="modern-sk-table-wrap">
|
||||
<table className="modern-sk-table" {...props}>
|
||||
{children}
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const THead = (p: ComponentPropsWithoutRef<'thead'>) => <thead {...p} />;
|
||||
export const TBody = (p: ComponentPropsWithoutRef<'tbody'>) => <tbody {...p} />;
|
||||
|
||||
export const Tr = ({
|
||||
selected,
|
||||
className,
|
||||
...props
|
||||
}: ComponentPropsWithoutRef<'tr'> & { selected?: boolean }) => (
|
||||
<tr className={cx(selected && 'is-selected', className)} {...props} />
|
||||
);
|
||||
|
||||
export const Th = (p: ComponentPropsWithoutRef<'th'>) => <th {...p} />;
|
||||
export const Td = (p: ComponentPropsWithoutRef<'td'>) => <td {...p} />;
|
||||
@@ -0,0 +1,28 @@
|
||||
import { type ComponentPropsWithoutRef } from 'react';
|
||||
import { Tabs as RTabs } from 'radix-ui';
|
||||
import { cx } from '../utils';
|
||||
|
||||
export const Tabs = RTabs.Root;
|
||||
|
||||
export const TabsList = ({
|
||||
items,
|
||||
className,
|
||||
...props
|
||||
}: { items: Array<{ value: string; label: string }> } & Omit<
|
||||
ComponentPropsWithoutRef<typeof RTabs.List>,
|
||||
'children'
|
||||
>) => (
|
||||
<RTabs.List className={cx('modern-sk-tabs', className)} {...props}>
|
||||
{items.map((it) => (
|
||||
<RTabs.Trigger
|
||||
key={it.value}
|
||||
value={it.value}
|
||||
className="modern-sk-tabs__trigger"
|
||||
>
|
||||
{it.label}
|
||||
</RTabs.Trigger>
|
||||
))}
|
||||
</RTabs.List>
|
||||
);
|
||||
|
||||
export const TabsContent = RTabs.Content;
|
||||
@@ -0,0 +1,26 @@
|
||||
import { forwardRef, type ComponentPropsWithoutRef, type ReactNode } from 'react';
|
||||
import { cx } from '../utils';
|
||||
|
||||
export const TextField = forwardRef<HTMLInputElement, ComponentPropsWithoutRef<'input'>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<input ref={ref} className={cx('modern-sk-field', className)} {...props} />
|
||||
),
|
||||
);
|
||||
TextField.displayName = 'TextField';
|
||||
|
||||
export const TextArea = forwardRef<HTMLTextAreaElement, ComponentPropsWithoutRef<'textarea'>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<textarea ref={ref} className={cx('modern-sk-field', className)} {...props} />
|
||||
),
|
||||
);
|
||||
TextArea.displayName = 'TextArea';
|
||||
|
||||
export const SearchField = ({
|
||||
icon,
|
||||
...props
|
||||
}: ComponentPropsWithoutRef<'input'> & { icon: ReactNode }) => (
|
||||
<div className="modern-sk-search">
|
||||
<span className="ph">{icon}</span>
|
||||
<TextField {...props} />
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,42 @@
|
||||
import { type ComponentPropsWithoutRef, type ReactNode } from 'react';
|
||||
import { Tooltip as RTooltip } from 'radix-ui';
|
||||
import { cx } from '../utils';
|
||||
|
||||
type TooltipProps = {
|
||||
content: ReactNode;
|
||||
children: ReactNode;
|
||||
delayDuration?: number;
|
||||
open?: boolean;
|
||||
defaultOpen?: boolean;
|
||||
onOpenChange?: (o: boolean) => void;
|
||||
} & Omit<ComponentPropsWithoutRef<typeof RTooltip.Content>, 'children'>;
|
||||
|
||||
export const Tooltip = ({
|
||||
content,
|
||||
children,
|
||||
delayDuration,
|
||||
open,
|
||||
defaultOpen,
|
||||
onOpenChange,
|
||||
sideOffset = 6,
|
||||
className,
|
||||
...contentProps
|
||||
}: TooltipProps) => (
|
||||
<RTooltip.Root
|
||||
delayDuration={delayDuration}
|
||||
open={open}
|
||||
defaultOpen={defaultOpen}
|
||||
onOpenChange={onOpenChange}
|
||||
>
|
||||
<RTooltip.Trigger asChild>{children}</RTooltip.Trigger>
|
||||
<RTooltip.Portal>
|
||||
<RTooltip.Content
|
||||
className={cx('modern-sk-tooltip', className)}
|
||||
sideOffset={sideOffset}
|
||||
{...contentProps}
|
||||
>
|
||||
{content}
|
||||
</RTooltip.Content>
|
||||
</RTooltip.Portal>
|
||||
</RTooltip.Root>
|
||||
);
|
||||
+21
-679
@@ -1,679 +1,21 @@
|
||||
/* ============================================================
|
||||
ModernSK UI — Radix Primitives wrapped in the ModernSK look.
|
||||
Logic/accessibility from Radix; every pixel from the tokens in
|
||||
styles/tokens.css + styles/components.css.
|
||||
============================================================ */
|
||||
import {
|
||||
forwardRef,
|
||||
useId,
|
||||
type ComponentPropsWithoutRef,
|
||||
type CSSProperties,
|
||||
type ReactNode,
|
||||
} from 'react';
|
||||
import {
|
||||
Switch as RSwitch,
|
||||
Checkbox as RCheckbox,
|
||||
RadioGroup as RRadioGroup,
|
||||
Tabs as RTabs,
|
||||
Slider as RSlider,
|
||||
DropdownMenu as RMenu,
|
||||
Tooltip as RTooltip,
|
||||
Progress as RProgress,
|
||||
Select as RSelect,
|
||||
ToggleGroup as RToggleGroup,
|
||||
Dialog as RDialog,
|
||||
AlertDialog as RAlertDialog,
|
||||
ScrollArea as RScrollArea,
|
||||
} from 'radix-ui';
|
||||
import { Check, CaretDown, X } from '@phosphor-icons/react';
|
||||
|
||||
const cx = (...c: Array<string | false | undefined>) =>
|
||||
c.filter(Boolean).join(' ');
|
||||
|
||||
/* ---------- BUTTON ---------- */
|
||||
type BtnVariant = 'key' | 'primary' | 'ember' | 'ghost';
|
||||
type ButtonProps = ComponentPropsWithoutRef<'button'> & {
|
||||
variant?: BtnVariant;
|
||||
size?: 'sm';
|
||||
iconOnly?: boolean;
|
||||
};
|
||||
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ variant = 'key', size, iconOnly, className, ...props }, ref) => (
|
||||
<button
|
||||
ref={ref}
|
||||
className={cx(
|
||||
'modern-sk-btn',
|
||||
variant !== 'key' && `modern-sk-btn--${variant}`,
|
||||
size === 'sm' && 'modern-sk-btn--sm',
|
||||
iconOnly && 'modern-sk-btn--icon',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
);
|
||||
Button.displayName = 'Button';
|
||||
|
||||
/* ---------- TEXT FIELD ---------- */
|
||||
export const TextField = forwardRef<
|
||||
HTMLInputElement,
|
||||
ComponentPropsWithoutRef<'input'>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<input ref={ref} className={cx('modern-sk-field', className)} {...props} />
|
||||
));
|
||||
TextField.displayName = 'TextField';
|
||||
|
||||
export const TextArea = forwardRef<
|
||||
HTMLTextAreaElement,
|
||||
ComponentPropsWithoutRef<'textarea'>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<textarea ref={ref} className={cx('modern-sk-field', className)} {...props} />
|
||||
));
|
||||
TextArea.displayName = 'TextArea';
|
||||
|
||||
export const SearchField = ({
|
||||
icon,
|
||||
...props
|
||||
}: ComponentPropsWithoutRef<'input'> & { icon: ReactNode }) => (
|
||||
<div className="modern-sk-search">
|
||||
<span className="ph">{icon}</span>
|
||||
<TextField {...props} />
|
||||
</div>
|
||||
);
|
||||
|
||||
/* ---------- SELECT (Radix) ---------- */
|
||||
type SelectProps = {
|
||||
value?: string;
|
||||
defaultValue?: string;
|
||||
onValueChange?: (v: string) => void;
|
||||
placeholder?: string;
|
||||
items: Array<{ value: string; label: string }>;
|
||||
'aria-label'?: string;
|
||||
};
|
||||
export const Select = ({
|
||||
value,
|
||||
defaultValue,
|
||||
onValueChange,
|
||||
placeholder,
|
||||
items,
|
||||
...rest
|
||||
}: SelectProps) => (
|
||||
<RSelect.Root
|
||||
value={value}
|
||||
defaultValue={defaultValue}
|
||||
onValueChange={onValueChange}
|
||||
>
|
||||
<RSelect.Trigger className="modern-sk-select" aria-label={rest['aria-label']}>
|
||||
<RSelect.Value placeholder={placeholder} />
|
||||
<RSelect.Icon className="modern-sk-select__icon">
|
||||
<CaretDown size={12} weight="bold" />
|
||||
</RSelect.Icon>
|
||||
</RSelect.Trigger>
|
||||
<RSelect.Portal>
|
||||
<RSelect.Content
|
||||
className="modern-sk-select__content"
|
||||
position="popper"
|
||||
sideOffset={6}
|
||||
>
|
||||
<RSelect.Viewport>
|
||||
{items.map((it) => (
|
||||
<RSelect.Item
|
||||
key={it.value}
|
||||
value={it.value}
|
||||
className="modern-sk-select__item"
|
||||
>
|
||||
<RSelect.ItemText>{it.label}</RSelect.ItemText>
|
||||
<RSelect.ItemIndicator className="modern-sk-select__item-indicator">
|
||||
<Check size={14} weight="bold" />
|
||||
</RSelect.ItemIndicator>
|
||||
</RSelect.Item>
|
||||
))}
|
||||
</RSelect.Viewport>
|
||||
</RSelect.Content>
|
||||
</RSelect.Portal>
|
||||
</RSelect.Root>
|
||||
);
|
||||
|
||||
/* ---------- SWITCH ---------- */
|
||||
export const Switch = (
|
||||
props: ComponentPropsWithoutRef<typeof RSwitch.Root>,
|
||||
) => (
|
||||
<RSwitch.Root className="modern-sk-switch" {...props}>
|
||||
<RSwitch.Thumb className="modern-sk-switch__thumb" />
|
||||
</RSwitch.Root>
|
||||
);
|
||||
|
||||
/* ---------- CHECKBOX ---------- */
|
||||
export const Checkbox = (
|
||||
props: ComponentPropsWithoutRef<typeof RCheckbox.Root>,
|
||||
) => (
|
||||
<RCheckbox.Root className="modern-sk-check" {...props}>
|
||||
<RCheckbox.Indicator className="modern-sk-check__indicator">
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="var(--lime-ink)"
|
||||
strokeWidth={3.5}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M4 12l5 5L20 6" />
|
||||
</svg>
|
||||
</RCheckbox.Indicator>
|
||||
</RCheckbox.Root>
|
||||
);
|
||||
|
||||
/* ---------- RADIO GROUP ---------- */
|
||||
export const RadioGroup = RRadioGroup.Root;
|
||||
export const RadioItem = ({
|
||||
value,
|
||||
...props
|
||||
}: ComponentPropsWithoutRef<typeof RRadioGroup.Item>) => (
|
||||
<RRadioGroup.Item className="modern-sk-radio" value={value} {...props}>
|
||||
<RRadioGroup.Indicator className="modern-sk-radio__indicator" />
|
||||
</RRadioGroup.Item>
|
||||
);
|
||||
|
||||
/* control + label row helper */
|
||||
export const Control = ({
|
||||
children,
|
||||
control,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
control: ReactNode;
|
||||
}) => (
|
||||
<label className="modern-sk-control">
|
||||
{control}
|
||||
{children}
|
||||
</label>
|
||||
);
|
||||
|
||||
/* ---------- SEGMENTED CONTROL (ToggleGroup single) ---------- */
|
||||
type SegProps = {
|
||||
value: string;
|
||||
onValueChange: (v: string) => void;
|
||||
items: Array<{ value: string; label: string }>;
|
||||
};
|
||||
export const SegmentedControl = ({
|
||||
value,
|
||||
onValueChange,
|
||||
items,
|
||||
}: SegProps) => (
|
||||
<RToggleGroup.Root
|
||||
type="single"
|
||||
className="modern-sk-seg"
|
||||
value={value}
|
||||
onValueChange={(v) => v && onValueChange(v)}
|
||||
>
|
||||
{items.map((it) => (
|
||||
<RToggleGroup.Item
|
||||
key={it.value}
|
||||
value={it.value}
|
||||
className="modern-sk-seg__item"
|
||||
>
|
||||
{it.label}
|
||||
</RToggleGroup.Item>
|
||||
))}
|
||||
</RToggleGroup.Root>
|
||||
);
|
||||
|
||||
/* ---------- SLIDER ---------- */
|
||||
export const Slider = (
|
||||
props: ComponentPropsWithoutRef<typeof RSlider.Root>,
|
||||
) => (
|
||||
<RSlider.Root className="modern-sk-slider" {...props}>
|
||||
<RSlider.Track className="modern-sk-slider__track">
|
||||
<RSlider.Range className="modern-sk-slider__range" />
|
||||
</RSlider.Track>
|
||||
<RSlider.Thumb className="modern-sk-slider__thumb" aria-label="Value" />
|
||||
</RSlider.Root>
|
||||
);
|
||||
|
||||
/* ---------- STEPPER ---------- */
|
||||
export const Stepper = ({
|
||||
onDecrement,
|
||||
onIncrement,
|
||||
}: {
|
||||
onDecrement: () => void;
|
||||
onIncrement: () => void;
|
||||
}) => (
|
||||
<div className="modern-sk-stepper">
|
||||
<button type="button" onClick={onDecrement} aria-label="Decrease">
|
||||
−
|
||||
</button>
|
||||
<button type="button" onClick={onIncrement} aria-label="Increase">
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
/* ---------- TABS ---------- */
|
||||
export const Tabs = RTabs.Root;
|
||||
export const TabsList = ({
|
||||
items,
|
||||
}: {
|
||||
items: Array<{ value: string; label: string }>;
|
||||
}) => (
|
||||
<RTabs.List className="modern-sk-tabs">
|
||||
{items.map((it) => (
|
||||
<RTabs.Trigger
|
||||
key={it.value}
|
||||
value={it.value}
|
||||
className="modern-sk-tabs__trigger"
|
||||
>
|
||||
{it.label}
|
||||
</RTabs.Trigger>
|
||||
))}
|
||||
</RTabs.List>
|
||||
);
|
||||
export const TabsContent = RTabs.Content;
|
||||
|
||||
/* ---------- PROGRESS ---------- */
|
||||
export const Progress = ({ value = 0 }: { value?: number }) => (
|
||||
<RProgress.Root className="modern-sk-progress" value={value}>
|
||||
<RProgress.Indicator
|
||||
className="modern-sk-progress__indicator"
|
||||
style={{ width: `${value}%` }}
|
||||
/>
|
||||
</RProgress.Root>
|
||||
);
|
||||
|
||||
/* ---------- BADGE ---------- */
|
||||
type BadgeVariant = 'lime' | 'ember' | 'neutral' | 'outline';
|
||||
export const Badge = ({
|
||||
variant = 'neutral',
|
||||
dot,
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: ComponentPropsWithoutRef<'span'> & {
|
||||
variant?: BadgeVariant;
|
||||
dot?: boolean;
|
||||
}) => (
|
||||
<span
|
||||
className={cx(
|
||||
'modern-sk-badge',
|
||||
`modern-sk-badge--${variant}`,
|
||||
dot && 'modern-sk-badge--dot',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
|
||||
/* ---------- CHIP ---------- */
|
||||
export const Chip = ({
|
||||
children,
|
||||
onRemove,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
onRemove?: () => void;
|
||||
}) => (
|
||||
<span className="modern-sk-chip">
|
||||
{children}
|
||||
{onRemove && (
|
||||
<button type="button" className="x" onClick={onRemove} aria-label="Remove">
|
||||
×
|
||||
</button>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
|
||||
/* ---------- CARD ---------- */
|
||||
export const Card = ({
|
||||
className,
|
||||
...props
|
||||
}: ComponentPropsWithoutRef<'div'>) => (
|
||||
<div className={cx('modern-sk-card', className)} {...props} />
|
||||
);
|
||||
|
||||
/* ---------- LIST ---------- */
|
||||
export const List = ({
|
||||
className,
|
||||
...props
|
||||
}: ComponentPropsWithoutRef<'div'>) => (
|
||||
<div className={cx('modern-sk-list', className)} {...props} />
|
||||
);
|
||||
export const Row = ({
|
||||
selected,
|
||||
className,
|
||||
...props
|
||||
}: ComponentPropsWithoutRef<'div'> & { selected?: boolean }) => (
|
||||
<div
|
||||
className={cx('modern-sk-row', selected && 'is-selected', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
/* ---------- DROPDOWN MENU (Radix) ---------- */
|
||||
export const Menu = RMenu.Root;
|
||||
export const MenuTrigger = RMenu.Trigger;
|
||||
export const MenuContent = ({
|
||||
children,
|
||||
...props
|
||||
}: ComponentPropsWithoutRef<typeof RMenu.Content>) => (
|
||||
<RMenu.Portal>
|
||||
<RMenu.Content className="modern-sk-menu" sideOffset={6} {...props}>
|
||||
{children}
|
||||
</RMenu.Content>
|
||||
</RMenu.Portal>
|
||||
);
|
||||
export const MenuItem = ({
|
||||
icon,
|
||||
shortcut,
|
||||
children,
|
||||
...props
|
||||
}: ComponentPropsWithoutRef<typeof RMenu.Item> & {
|
||||
icon?: ReactNode;
|
||||
shortcut?: string;
|
||||
}) => (
|
||||
<RMenu.Item className="modern-sk-menu-item" {...props}>
|
||||
{icon && <span className="ph">{icon}</span>}
|
||||
{children}
|
||||
{shortcut && <span className="sc">{shortcut}</span>}
|
||||
</RMenu.Item>
|
||||
);
|
||||
export const MenuSeparator = () => (
|
||||
<RMenu.Separator className="modern-sk-menu-sep" />
|
||||
);
|
||||
|
||||
/* Static menu surface — for showcasing the menu without a trigger. */
|
||||
export const MenuSurface = ({ children }: { children: ReactNode }) => (
|
||||
<div className="modern-sk-menu">{children}</div>
|
||||
);
|
||||
export const MenuRow = ({
|
||||
icon,
|
||||
shortcut,
|
||||
children,
|
||||
}: {
|
||||
icon?: ReactNode;
|
||||
shortcut?: string;
|
||||
children: ReactNode;
|
||||
}) => (
|
||||
<div className="modern-sk-menu-item">
|
||||
{icon && <span className="ph">{icon}</span>}
|
||||
{children}
|
||||
{shortcut && <span className="sc">{shortcut}</span>}
|
||||
</div>
|
||||
);
|
||||
|
||||
/* ---------- TOOLTIP (Radix) ---------- */
|
||||
export const Tooltip = ({
|
||||
content,
|
||||
children,
|
||||
}: {
|
||||
content: ReactNode;
|
||||
children: ReactNode;
|
||||
}) => (
|
||||
<RTooltip.Root>
|
||||
<RTooltip.Trigger asChild>{children}</RTooltip.Trigger>
|
||||
<RTooltip.Portal>
|
||||
<RTooltip.Content className="modern-sk-tooltip" sideOffset={6}>
|
||||
{content}
|
||||
</RTooltip.Content>
|
||||
</RTooltip.Portal>
|
||||
</RTooltip.Root>
|
||||
);
|
||||
|
||||
/* ---------- ICON BUTTON ---------- */
|
||||
type IconButtonProps = ComponentPropsWithoutRef<'button'> & {
|
||||
variant?: BtnVariant;
|
||||
size?: 'sm' | 'lg';
|
||||
};
|
||||
export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
|
||||
({ variant = 'key', size, className, ...props }, ref) => (
|
||||
<button
|
||||
ref={ref}
|
||||
className={cx(
|
||||
'modern-sk-btn',
|
||||
'modern-sk-iconbtn',
|
||||
variant !== 'key' && `modern-sk-btn--${variant}`,
|
||||
size && `modern-sk-iconbtn--${size}`,
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
);
|
||||
IconButton.displayName = 'IconButton';
|
||||
|
||||
/* ---------- SPINNER ----------
|
||||
Carved donut groove (sunk like the switch well, dark rim at top →
|
||||
light catch at the bottom) with a glossy lime arc spinning inside it. */
|
||||
export const Spinner = ({
|
||||
size,
|
||||
className,
|
||||
...props
|
||||
}: ComponentPropsWithoutRef<'span'> & { size?: 'sm' | 'lg' }) => {
|
||||
const gid = `modern-sk-groove-${useId()}`;
|
||||
return (
|
||||
<span
|
||||
role="status"
|
||||
aria-label="Loading"
|
||||
className={cx('modern-sk-spinner', size && `modern-sk-spinner--${size}`, className)}
|
||||
{...props}
|
||||
>
|
||||
<svg viewBox="0 0 36 36" fill="none">
|
||||
<defs>
|
||||
<linearGradient
|
||||
id={gid}
|
||||
x1="18"
|
||||
y1="4"
|
||||
x2="18"
|
||||
y2="32"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset="0" stopColor="var(--spin-groove-1)" />
|
||||
<stop offset="1" stopColor="var(--spin-groove-2)" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
{/* carved channel */}
|
||||
<circle cx="18" cy="18" r="14" stroke={`url(#${gid})`} strokeWidth="5" />
|
||||
{/* glossy lime arc, nested inside the groove with rounded ends */}
|
||||
<circle
|
||||
className="modern-sk-spinner__arc"
|
||||
cx="18"
|
||||
cy="18"
|
||||
r="14"
|
||||
stroke="var(--lime)"
|
||||
strokeWidth="3"
|
||||
strokeLinecap="round"
|
||||
strokeDasharray="22 88"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
/* ---------- CALLOUT ---------- */
|
||||
type CalloutVariant = 'info' | 'success' | 'warning' | 'danger';
|
||||
export const Callout = ({
|
||||
variant = 'info',
|
||||
icon,
|
||||
children,
|
||||
}: {
|
||||
variant?: CalloutVariant;
|
||||
icon?: ReactNode;
|
||||
children: ReactNode;
|
||||
}) => (
|
||||
<div className={cx('modern-sk-callout', variant !== 'info' && `modern-sk-callout--${variant}`)}>
|
||||
{icon && <span className="modern-sk-callout__icon">{icon}</span>}
|
||||
<div className="modern-sk-callout__body">{children}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
/* ---------- TABLE ---------- */
|
||||
export const Table = ({
|
||||
children,
|
||||
...props
|
||||
}: ComponentPropsWithoutRef<'table'>) => (
|
||||
<div className="modern-sk-table-wrap">
|
||||
<table className="modern-sk-table" {...props}>
|
||||
{children}
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
export const THead = (p: ComponentPropsWithoutRef<'thead'>) => <thead {...p} />;
|
||||
export const TBody = (p: ComponentPropsWithoutRef<'tbody'>) => <tbody {...p} />;
|
||||
export const Tr = ({
|
||||
selected,
|
||||
className,
|
||||
...props
|
||||
}: ComponentPropsWithoutRef<'tr'> & { selected?: boolean }) => (
|
||||
<tr className={cx(selected && 'is-selected', className)} {...props} />
|
||||
);
|
||||
export const Th = (p: ComponentPropsWithoutRef<'th'>) => <th {...p} />;
|
||||
export const Td = (p: ComponentPropsWithoutRef<'td'>) => <td {...p} />;
|
||||
|
||||
/* ---------- SCROLL AREA (Radix) ---------- */
|
||||
export const ScrollArea = ({
|
||||
children,
|
||||
className,
|
||||
style,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
}) => (
|
||||
<RScrollArea.Root className={cx('modern-sk-scroll', className)} style={style}>
|
||||
<RScrollArea.Viewport className="modern-sk-scroll__viewport">
|
||||
{children}
|
||||
</RScrollArea.Viewport>
|
||||
<RScrollArea.Scrollbar className="modern-sk-scroll__bar" orientation="vertical">
|
||||
<RScrollArea.Thumb className="modern-sk-scroll__thumb" />
|
||||
</RScrollArea.Scrollbar>
|
||||
<RScrollArea.Scrollbar className="modern-sk-scroll__bar" orientation="horizontal">
|
||||
<RScrollArea.Thumb className="modern-sk-scroll__thumb" />
|
||||
</RScrollArea.Scrollbar>
|
||||
<RScrollArea.Corner />
|
||||
</RScrollArea.Root>
|
||||
);
|
||||
|
||||
/* ---------- DIALOG / MODAL (Radix Dialog) ---------- */
|
||||
export const Dialog = ({
|
||||
trigger,
|
||||
title,
|
||||
description,
|
||||
children,
|
||||
footer,
|
||||
open,
|
||||
onOpenChange,
|
||||
}: {
|
||||
trigger?: ReactNode;
|
||||
title: string;
|
||||
description?: ReactNode;
|
||||
children?: ReactNode;
|
||||
footer?: ReactNode;
|
||||
open?: boolean;
|
||||
onOpenChange?: (o: boolean) => void;
|
||||
}) => (
|
||||
<RDialog.Root open={open} onOpenChange={onOpenChange}>
|
||||
{trigger && <RDialog.Trigger asChild>{trigger}</RDialog.Trigger>}
|
||||
<RDialog.Portal>
|
||||
<RDialog.Overlay className="modern-sk-overlay" />
|
||||
<RDialog.Content className="modern-sk-dialog">
|
||||
<RDialog.Title className="modern-sk-dialog__title">{title}</RDialog.Title>
|
||||
{description && (
|
||||
<RDialog.Description className="modern-sk-dialog__desc">
|
||||
{description}
|
||||
</RDialog.Description>
|
||||
)}
|
||||
{children && <div className="modern-sk-dialog__body">{children}</div>}
|
||||
{footer && <div className="modern-sk-dialog__footer">{footer}</div>}
|
||||
<RDialog.Close asChild>
|
||||
<IconButton
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="modern-sk-dialog__close"
|
||||
aria-label="Close"
|
||||
>
|
||||
<X size={14} weight="bold" />
|
||||
</IconButton>
|
||||
</RDialog.Close>
|
||||
</RDialog.Content>
|
||||
</RDialog.Portal>
|
||||
</RDialog.Root>
|
||||
);
|
||||
/* Low-level Dialog parts (for custom compositions). */
|
||||
export const DialogClose = RDialog.Close;
|
||||
|
||||
/* ---------- ALERT DIALOG (Radix AlertDialog) ---------- */
|
||||
export const AlertDialog = ({
|
||||
trigger,
|
||||
title,
|
||||
description,
|
||||
cancelLabel = 'Cancel',
|
||||
actionLabel = 'Confirm',
|
||||
destructive,
|
||||
onAction,
|
||||
open,
|
||||
onOpenChange,
|
||||
}: {
|
||||
trigger?: ReactNode;
|
||||
title: string;
|
||||
description?: ReactNode;
|
||||
cancelLabel?: string;
|
||||
actionLabel?: string;
|
||||
destructive?: boolean;
|
||||
onAction?: () => void;
|
||||
open?: boolean;
|
||||
onOpenChange?: (o: boolean) => void;
|
||||
}) => (
|
||||
<RAlertDialog.Root open={open} onOpenChange={onOpenChange}>
|
||||
{trigger && <RAlertDialog.Trigger asChild>{trigger}</RAlertDialog.Trigger>}
|
||||
<RAlertDialog.Portal>
|
||||
<RAlertDialog.Overlay className="modern-sk-overlay" />
|
||||
<RAlertDialog.Content className="modern-sk-dialog">
|
||||
<RAlertDialog.Title className="modern-sk-dialog__title">
|
||||
{title}
|
||||
</RAlertDialog.Title>
|
||||
{description && (
|
||||
<RAlertDialog.Description className="modern-sk-dialog__desc">
|
||||
{description}
|
||||
</RAlertDialog.Description>
|
||||
)}
|
||||
<div className="modern-sk-dialog__footer">
|
||||
<RAlertDialog.Cancel asChild>
|
||||
<Button variant="ghost">{cancelLabel}</Button>
|
||||
</RAlertDialog.Cancel>
|
||||
<RAlertDialog.Action asChild>
|
||||
<Button
|
||||
variant={destructive ? 'ember' : 'primary'}
|
||||
onClick={onAction}
|
||||
>
|
||||
{actionLabel}
|
||||
</Button>
|
||||
</RAlertDialog.Action>
|
||||
</div>
|
||||
</RAlertDialog.Content>
|
||||
</RAlertDialog.Portal>
|
||||
</RAlertDialog.Root>
|
||||
);
|
||||
|
||||
/* ---------- WINDOW CHROME ---------- */
|
||||
export const Window = ({
|
||||
title,
|
||||
badge,
|
||||
children,
|
||||
...props
|
||||
}: ComponentPropsWithoutRef<'div'> & {
|
||||
title: string;
|
||||
badge?: ReactNode;
|
||||
}) => (
|
||||
<div className="modern-sk-window" {...props}>
|
||||
<div className="modern-sk-titlebar">
|
||||
<span className="modern-sk-traffic r" />
|
||||
<span className="modern-sk-traffic y" />
|
||||
<span className="modern-sk-traffic g" />
|
||||
<span className="ttl">{title}</span>
|
||||
{badge && (
|
||||
<div style={{ marginLeft: 'auto', display: 'flex', gap: 8 }}>
|
||||
{badge}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
export * from './button';
|
||||
export * from './icon-button';
|
||||
export * from './text-field';
|
||||
export * from './select';
|
||||
export * from './selection';
|
||||
export * from './segmented-control';
|
||||
export * from './slider';
|
||||
export * from './tabs';
|
||||
export * from './progress';
|
||||
export * from './badge';
|
||||
export * from './card';
|
||||
export * from './list';
|
||||
export * from './menu';
|
||||
export * from './tooltip';
|
||||
export * from './spinner';
|
||||
export * from './callout';
|
||||
export * from './table';
|
||||
export * from './scroll-area';
|
||||
export * from './dialog';
|
||||
export * from './alert-dialog';
|
||||
export * from './window';
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export const cx = (...c: Array<string | false | undefined>) =>
|
||||
c.filter(Boolean).join(' ');
|
||||
@@ -0,0 +1,26 @@
|
||||
import { type ComponentPropsWithoutRef, type ReactNode } from 'react';
|
||||
|
||||
export const Window = ({
|
||||
title,
|
||||
badge,
|
||||
children,
|
||||
...props
|
||||
}: ComponentPropsWithoutRef<'div'> & {
|
||||
title: string;
|
||||
badge?: ReactNode;
|
||||
}) => (
|
||||
<div className="modern-sk-window" {...props}>
|
||||
<div className="modern-sk-titlebar">
|
||||
<span className="modern-sk-traffic r" />
|
||||
<span className="modern-sk-traffic y" />
|
||||
<span className="modern-sk-traffic g" />
|
||||
<span className="ttl">{title}</span>
|
||||
{badge && (
|
||||
<div style={{ marginLeft: 'auto', display: 'flex', gap: 8 }}>
|
||||
{badge}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
Reference in New Issue
Block a user