@conscience-technology/ctds 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +128 -2
- package/assets/components/AppFrame/AppFrame.module.css +177 -0
- package/assets/components/AppFrame/AppFrame.tsx +93 -0
- package/assets/components/AppFrame/index.ts +6 -0
- package/assets/components/AttachmentChip/AttachmentChip.module.css +71 -0
- package/assets/components/AttachmentChip/AttachmentChip.tsx +74 -0
- package/assets/components/AttachmentChip/index.ts +2 -0
- package/assets/components/AttachmentThumb/AttachmentThumb.module.css +107 -0
- package/assets/components/AttachmentThumb/AttachmentThumb.tsx +91 -0
- package/assets/components/AttachmentThumb/index.ts +2 -0
- package/assets/components/Avatar/Avatar.module.css +167 -0
- package/assets/components/Avatar/Avatar.tsx +228 -0
- package/assets/components/Avatar/index.ts +2 -0
- package/assets/components/AvatarUpload/AvatarUpload.module.css +130 -0
- package/assets/components/AvatarUpload/AvatarUpload.tsx +139 -0
- package/assets/components/AvatarUpload/index.ts +2 -0
- package/assets/components/Badge/Badge.module.css +76 -0
- package/assets/components/Badge/Badge.tsx +68 -0
- package/assets/components/Badge/index.ts +2 -0
- package/assets/components/Breadcrumb/Breadcrumb.module.css +213 -0
- package/assets/components/Breadcrumb/Breadcrumb.tsx +279 -0
- package/assets/components/Breadcrumb/index.ts +8 -0
- package/assets/components/Button/Button.module.css +216 -0
- package/assets/components/Button/Button.tsx +149 -0
- package/assets/components/Button/index.ts +2 -0
- package/assets/components/Callout/Callout.module.css +146 -0
- package/assets/components/Callout/Callout.tsx +138 -0
- package/assets/components/Callout/index.ts +2 -0
- package/assets/components/ChatInput/ChatInput.module.css +254 -0
- package/assets/components/ChatInput/ChatInput.tsx +322 -0
- package/assets/components/ChatInput/ModelSelector.tsx +40 -0
- package/assets/components/ChatInput/SendButton.tsx +41 -0
- package/assets/components/ChatInput/index.ts +10 -0
- package/assets/components/Checkbox/Checkbox.module.css +66 -0
- package/assets/components/Checkbox/Checkbox.tsx +36 -0
- package/assets/components/Checkbox/index.ts +2 -0
- package/assets/components/Chip/Chip.module.css +128 -0
- package/assets/components/Chip/Chip.tsx +103 -0
- package/assets/components/Chip/index.ts +2 -0
- package/assets/components/ChoiceCard/ChoiceCard.module.css +142 -0
- package/assets/components/ChoiceCard/ChoiceCard.tsx +132 -0
- package/assets/components/ChoiceCard/index.ts +2 -0
- package/assets/components/ContextMenu/ContextMenu.module.css +102 -0
- package/assets/components/ContextMenu/ContextMenu.tsx +392 -0
- package/assets/components/ContextMenu/index.ts +9 -0
- package/assets/components/DatePicker/Calendar.tsx +234 -0
- package/assets/components/DatePicker/DESIGN_NOTES.md +106 -0
- package/assets/components/DatePicker/DESIGN_NOTES_TIME.md +125 -0
- package/assets/components/DatePicker/DatePicker.module.css +295 -0
- package/assets/components/DatePicker/DatePicker.tsx +387 -0
- package/assets/components/DatePicker/TimeSelector.tsx +69 -0
- package/assets/components/DatePicker/index.ts +8 -0
- package/assets/components/DatePicker/presets.ts +80 -0
- package/assets/components/DatePicker/utils.ts +148 -0
- package/assets/components/Disclosure/Disclosure.module.css +73 -0
- package/assets/components/Disclosure/Disclosure.tsx +129 -0
- package/assets/components/Disclosure/index.ts +6 -0
- package/assets/components/Drawer/Drawer.module.css +257 -0
- package/assets/components/Drawer/Drawer.tsx +126 -0
- package/assets/components/Drawer/index.ts +7 -0
- package/assets/components/EmptyState/EmptyState.module.css +102 -0
- package/assets/components/EmptyState/EmptyState.tsx +80 -0
- package/assets/components/EmptyState/index.ts +2 -0
- package/assets/components/FileIcon/FileIcon.module.css +66 -0
- package/assets/components/FileIcon/FileIcon.tsx +84 -0
- package/assets/components/FileIcon/fileExtMap.ts +112 -0
- package/assets/components/FileIcon/index.ts +9 -0
- package/assets/components/FilterButton/FilterButton.module.css +130 -0
- package/assets/components/FilterButton/FilterButton.tsx +94 -0
- package/assets/components/FilterButton/index.ts +2 -0
- package/assets/components/GalleryItem/GalleryItem.module.css +220 -0
- package/assets/components/GalleryItem/GalleryItem.tsx +163 -0
- package/assets/components/GalleryItem/index.ts +7 -0
- package/assets/components/Icon/Icon.tsx +616 -0
- package/assets/components/Icon/context.tsx +39 -0
- package/assets/components/Icon/index.ts +4 -0
- package/assets/components/IconButton/IconButton.module.css +109 -0
- package/assets/components/IconButton/IconButton.tsx +110 -0
- package/assets/components/IconButton/index.ts +2 -0
- package/assets/components/Input/Input.module.css +338 -0
- package/assets/components/Input/Input.tsx +371 -0
- package/assets/components/Input/index.ts +2 -0
- package/assets/components/ListItem/ListItem.module.css +318 -0
- package/assets/components/ListItem/ListItem.tsx +262 -0
- package/assets/components/ListItem/index.ts +9 -0
- package/assets/components/MenuItem/MenuItem.module.css +437 -0
- package/assets/components/MenuItem/MenuItem.tsx +529 -0
- package/assets/components/MenuItem/index.ts +8 -0
- package/assets/components/MetricCard/MetricCard.module.css +153 -0
- package/assets/components/MetricCard/MetricCard.tsx +140 -0
- package/assets/components/MetricCard/index.ts +2 -0
- package/assets/components/Modal/Modal.module.css +175 -0
- package/assets/components/Modal/Modal.tsx +98 -0
- package/assets/components/Modal/index.ts +2 -0
- package/assets/components/Onboarding/DocsIndex.tsx +82 -0
- package/assets/components/Onboarding/LayoutBuilder.tsx +160 -0
- package/assets/components/Onboarding/Onboarding.module.css +1394 -0
- package/assets/components/Onboarding/OnboardingAxisPanel.tsx +515 -0
- package/assets/components/Onboarding/OnboardingPage.tsx +1193 -0
- package/assets/components/Onboarding/PresetsGallery.tsx +65 -0
- package/assets/components/Onboarding/actions.ts +250 -0
- package/assets/components/Onboarding/index.ts +8 -0
- package/assets/components/Onboarding/palette.css +40 -0
- package/assets/components/Onboarding/wizardData.ts +265 -0
- package/assets/components/Pagination/Pagination.module.css +82 -0
- package/assets/components/Pagination/Pagination.tsx +248 -0
- package/assets/components/Pagination/index.ts +6 -0
- package/assets/components/ProgressBar/ProgressBar.module.css +64 -0
- package/assets/components/ProgressBar/ProgressBar.tsx +54 -0
- package/assets/components/ProgressBar/index.ts +2 -0
- package/assets/components/QuotationBar/QuotationBar.module.css +61 -0
- package/assets/components/QuotationBar/QuotationBar.tsx +48 -0
- package/assets/components/QuotationBar/index.ts +2 -0
- package/assets/components/Radio/Radio.module.css +63 -0
- package/assets/components/Radio/Radio.tsx +33 -0
- package/assets/components/Radio/index.ts +2 -0
- package/assets/components/Select/Select.module.css +151 -0
- package/assets/components/Select/Select.tsx +425 -0
- package/assets/components/Select/index.ts +10 -0
- package/assets/components/SelectableCard/SelectableCard.module.css +177 -0
- package/assets/components/SelectableCard/SelectableCard.tsx +83 -0
- package/assets/components/SelectableCard/index.ts +2 -0
- package/assets/components/Shortcut/Shortcut.module.css +56 -0
- package/assets/components/Shortcut/Shortcut.tsx +156 -0
- package/assets/components/Shortcut/index.ts +2 -0
- package/assets/components/Sidebar/Sidebar.module.css +242 -0
- package/assets/components/Sidebar/Sidebar.tsx +264 -0
- package/assets/components/Sidebar/index.ts +7 -0
- package/assets/components/SidebarSplit/SidebarSplit.module.css +126 -0
- package/assets/components/SidebarSplit/SidebarSplit.tsx +222 -0
- package/assets/components/SidebarSplit/index.ts +6 -0
- package/assets/components/Slider/Slider.module.css +120 -0
- package/assets/components/Slider/Slider.tsx +88 -0
- package/assets/components/Slider/index.ts +2 -0
- package/assets/components/SplitButton/SplitButton.module.css +190 -0
- package/assets/components/SplitButton/SplitButton.tsx +145 -0
- package/assets/components/SplitButton/index.ts +6 -0
- package/assets/components/Stepper/Stepper.module.css +182 -0
- package/assets/components/Stepper/Stepper.tsx +185 -0
- package/assets/components/Stepper/index.ts +2 -0
- package/assets/components/Switch/Switch.module.css +116 -0
- package/assets/components/Switch/Switch.tsx +58 -0
- package/assets/components/Switch/index.ts +2 -0
- package/assets/components/TableCell/TableCell.module.css +137 -0
- package/assets/components/TableCell/TableCellAvatar.tsx +42 -0
- package/assets/components/TableCell/TableCellBadge.tsx +23 -0
- package/assets/components/TableCell/TableCellIcon.tsx +39 -0
- package/assets/components/TableCell/TableCellImage.tsx +49 -0
- package/assets/components/TableCell/TableCellLink.tsx +33 -0
- package/assets/components/TableCell/TableCellMeter.tsx +55 -0
- package/assets/components/TableCell/TableCellNumber.tsx +43 -0
- package/assets/components/TableCell/TableCellText.tsx +30 -0
- package/assets/components/TableCell/index.ts +23 -0
- package/assets/components/TableHeader/TableHeader.module.css +287 -0
- package/assets/components/TableHeader/TableHeader.tsx +590 -0
- package/assets/components/TableHeader/index.ts +9 -0
- package/assets/components/TableRow/TableRow.module.css +290 -0
- package/assets/components/TableRow/TableRow.tsx +253 -0
- package/assets/components/TableRow/index.ts +8 -0
- package/assets/components/TableRowEdit/TableRowEdit.module.css +126 -0
- package/assets/components/TableRowEdit/TableRowEdit.tsx +247 -0
- package/assets/components/TableRowEdit/index.ts +2 -0
- package/assets/components/TableRowSelect/TableRowSelect.module.css +23 -0
- package/assets/components/TableRowSelect/TableRowSelect.tsx +175 -0
- package/assets/components/TableRowSelect/index.ts +2 -0
- package/assets/components/Tabs/Tabs.module.css +234 -0
- package/assets/components/Tabs/Tabs.tsx +426 -0
- package/assets/components/Tabs/index.ts +9 -0
- package/assets/components/Textarea/Textarea.module.css +318 -0
- package/assets/components/Textarea/Textarea.tsx +426 -0
- package/assets/components/Textarea/index.ts +7 -0
- package/assets/components/ThemeController.tsx +271 -0
- package/assets/components/TimePicker/TimePicker.module.css +317 -0
- package/assets/components/TimePicker/TimePicker.tsx +821 -0
- package/assets/components/TimePicker/index.ts +8 -0
- package/assets/components/TimePicker/utils.ts +115 -0
- package/assets/components/Toast/Toast.module.css +264 -0
- package/assets/components/Toast/Toast.tsx +243 -0
- package/assets/components/Toast/index.ts +15 -0
- package/assets/components/Toolbar/Toolbar.module.css +72 -0
- package/assets/components/Toolbar/Toolbar.tsx +75 -0
- package/assets/components/Toolbar/index.ts +2 -0
- package/assets/components/Tooltip/Tooltip.module.css +42 -0
- package/assets/components/Tooltip/Tooltip.tsx +332 -0
- package/assets/components/Tooltip/index.ts +2 -0
- package/assets/components/Topbar/Topbar.module.css +26 -0
- package/assets/components/Topbar/Topbar.tsx +47 -0
- package/assets/components/Topbar/index.ts +2 -0
- package/assets/components/_cn.ts +3 -0
- package/assets/components/_color.ts +48 -0
- package/assets/components/_data.ts +659 -0
- package/assets/components/_utils.css +19 -0
- package/assets/components/axisOptions.ts +24 -0
- package/assets/components/tokens.css +742 -0
- package/assets/prompts/GUIDE.md +251 -0
- package/assets/prompts/OVERRIDE.md +170 -0
- package/assets/prompts/PRINCIPLES.md +130 -0
- package/assets/prompts/axes/density.md +38 -0
- package/assets/prompts/axes/font.md +36 -0
- package/assets/prompts/axes/hue.md +36 -0
- package/assets/prompts/axes/icon.md +48 -0
- package/assets/prompts/axes/mode.md +50 -0
- package/assets/prompts/axes/motion.md +77 -0
- package/assets/prompts/axes/palette.md +35 -0
- package/assets/prompts/axes/shape.md +33 -0
- package/assets/prompts/axes/voice.md +54 -0
- package/assets/prompts/components/README.md +84 -0
- package/assets/prompts/components/app-frame.md +94 -0
- package/assets/prompts/components/attachment-chip.md +46 -0
- package/assets/prompts/components/attachment-thumb.md +49 -0
- package/assets/prompts/components/avatar.md +86 -0
- package/assets/prompts/components/badge.md +104 -0
- package/assets/prompts/components/breadcrumb.md +77 -0
- package/assets/prompts/components/button.md +99 -0
- package/assets/prompts/components/callout.md +81 -0
- package/assets/prompts/components/chat-input.md +84 -0
- package/assets/prompts/components/checkbox.md +46 -0
- package/assets/prompts/components/chip.md +84 -0
- package/assets/prompts/components/choice-card.md +127 -0
- package/assets/prompts/components/context-menu.md +145 -0
- package/assets/prompts/components/date-picker.md +104 -0
- package/assets/prompts/components/drawer.md +152 -0
- package/assets/prompts/components/empty-state.md +69 -0
- package/assets/prompts/components/file-icon.md +73 -0
- package/assets/prompts/components/filter-button.md +42 -0
- package/assets/prompts/components/form-field.md +74 -0
- package/assets/prompts/components/gallery-item.md +158 -0
- package/assets/prompts/components/icon-button.md +82 -0
- package/assets/prompts/components/icon.md +68 -0
- package/assets/prompts/components/input.md +95 -0
- package/assets/prompts/components/list-item.md +235 -0
- package/assets/prompts/components/menu-item.md +234 -0
- package/assets/prompts/components/metric-card.md +132 -0
- package/assets/prompts/components/modal.md +131 -0
- package/assets/prompts/components/pagination.md +92 -0
- package/assets/prompts/components/progress-bar.md +61 -0
- package/assets/prompts/components/quotation-bar.md +39 -0
- package/assets/prompts/components/radio.md +41 -0
- package/assets/prompts/components/select.md +104 -0
- package/assets/prompts/components/selectable-card.md +67 -0
- package/assets/prompts/components/shortcut.md +76 -0
- package/assets/prompts/components/sidebar.md +107 -0
- package/assets/prompts/components/slider.md +89 -0
- package/assets/prompts/components/split-button.md +71 -0
- package/assets/prompts/components/stepper.md +93 -0
- package/assets/prompts/components/switch.md +86 -0
- package/assets/prompts/components/table-cell.md +87 -0
- package/assets/prompts/components/table-header.md +157 -0
- package/assets/prompts/components/table-row-edit.md +129 -0
- package/assets/prompts/components/table-row-select.md +82 -0
- package/assets/prompts/components/table-row.md +156 -0
- package/assets/prompts/components/tabs.md +122 -0
- package/assets/prompts/components/textarea.md +105 -0
- package/assets/prompts/components/toast.md +149 -0
- package/assets/prompts/components/tooltip.md +71 -0
- package/assets/prompts/components/topbar.md +148 -0
- package/assets/prompts/primitives/emerge.md +43 -0
- package/assets/prompts/primitives/fade.md +31 -0
- package/assets/prompts/primitives/scale.md +32 -0
- package/assets/prompts/primitives/scroll.md +75 -0
- package/assets/prompts/primitives/slide.md +31 -0
- package/assets/prompts/themes/_SCHEMA.md +135 -0
- package/assets/prompts/themes/_TEMPLATE.md +73 -0
- package/assets/prompts/themes/expressive.md +118 -0
- package/assets/prompts/themes/pure.md +87 -0
- package/assets/prompts/themes/spare.md +73 -0
- package/assets/prompts/tokens/breakpoint.md +29 -0
- package/assets/prompts/tokens/color.md +78 -0
- package/assets/prompts/tokens/radius.md +33 -0
- package/assets/prompts/tokens/shadow.md +35 -0
- package/assets/prompts/tokens/spacing.md +86 -0
- package/assets/prompts/tokens/typography.md +76 -0
- package/assets/prompts/tokens/z-index.md +35 -0
- package/assets/public/expressive.png +0 -0
- package/assets/public/pure.png +0 -0
- package/assets/public/spare.png +0 -0
- package/assets/skill/SKILL.md +36 -0
- package/bin/ctds.js +76 -0
- package/package.json +35 -4
- package/src/commands/init.js +215 -0
- package/src/commands/update.js +275 -0
- package/src/utils/assets.js +78 -0
- package/src/utils/deps.js +51 -0
- package/src/utils/detect.js +47 -0
- package/src/utils/diff.js +52 -0
- package/src/utils/hash.js +16 -0
- package/src/utils/log.js +41 -0
- package/src/utils/meta.js +73 -0
- package/src/utils/prompt.js +28 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
cloneElement,
|
|
5
|
+
forwardRef,
|
|
6
|
+
isValidElement,
|
|
7
|
+
type ButtonHTMLAttributes,
|
|
8
|
+
type ReactElement,
|
|
9
|
+
type ReactNode,
|
|
10
|
+
} from "react";
|
|
11
|
+
|
|
12
|
+
import { Icon } from "../Icon";
|
|
13
|
+
import styles from "./SplitButton.module.css";
|
|
14
|
+
|
|
15
|
+
export type SplitButtonVariant =
|
|
16
|
+
| "primary"
|
|
17
|
+
| "inverse"
|
|
18
|
+
| "secondary"
|
|
19
|
+
| "outline";
|
|
20
|
+
|
|
21
|
+
export type SplitButtonSize = "sm" | "md" | "lg";
|
|
22
|
+
|
|
23
|
+
type NativeButtonProps = Omit<ButtonHTMLAttributes<HTMLButtonElement>, "size" | "onClick">;
|
|
24
|
+
|
|
25
|
+
export interface SplitButtonProps extends NativeButtonProps {
|
|
26
|
+
size?: SplitButtonSize;
|
|
27
|
+
variant?: SplitButtonVariant;
|
|
28
|
+
fullWidth?: boolean;
|
|
29
|
+
/** main 라벨 앞 아이콘 (선택) */
|
|
30
|
+
leftIcon?: ReactNode;
|
|
31
|
+
/** main 버튼 라벨 */
|
|
32
|
+
children: ReactNode;
|
|
33
|
+
/** main 영역 클릭 */
|
|
34
|
+
onMainClick?: ButtonHTMLAttributes<HTMLButtonElement>["onClick"];
|
|
35
|
+
/** menu(caret) 영역 클릭 — 드롭다운 트리거 */
|
|
36
|
+
onMenuClick?: ButtonHTMLAttributes<HTMLButtonElement>["onClick"];
|
|
37
|
+
/** menu 영역 스크린리더 라벨. 예: "옵션 더보기" */
|
|
38
|
+
menuLabel: string;
|
|
39
|
+
/** menu 영역 아이콘. 기본 caret-down 대체용 */
|
|
40
|
+
menuIcon?: ReactNode;
|
|
41
|
+
/** 로딩·disabled는 main·menu 모두에 적용 */
|
|
42
|
+
loading?: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const variantClass: Record<SplitButtonVariant, string> = {
|
|
46
|
+
primary: styles.primary,
|
|
47
|
+
inverse: styles.inverse,
|
|
48
|
+
secondary: styles.secondary,
|
|
49
|
+
outline: styles.outline,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const sizeClass: Record<SplitButtonSize, string> = {
|
|
53
|
+
sm: styles.sm,
|
|
54
|
+
md: styles.md,
|
|
55
|
+
lg: styles.lg,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const ICON_SIZE: Record<SplitButtonSize, number> = {
|
|
59
|
+
sm: 16,
|
|
60
|
+
md: 16,
|
|
61
|
+
lg: 20,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
function sizedIcon(node: ReactNode, size: number): ReactNode {
|
|
65
|
+
if (!isValidElement(node)) return node;
|
|
66
|
+
const el = node as ReactElement<{ size?: number | string }>;
|
|
67
|
+
if (el.props.size !== undefined) return el;
|
|
68
|
+
return cloneElement(el, { size });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* SplitButton — main 액션 + 부가 메뉴(caret) 2-part 버튼.
|
|
73
|
+
* 시각적으로 하나로 보이지만 클릭 영역이 분리되고, 각 영역이 독립 hover.
|
|
74
|
+
* 접근성: 각 영역이 별개 <button>이며 menuLabel은 필수.
|
|
75
|
+
*/
|
|
76
|
+
const SplitButton = forwardRef<HTMLDivElement, SplitButtonProps>(
|
|
77
|
+
(
|
|
78
|
+
{
|
|
79
|
+
className,
|
|
80
|
+
variant = "primary",
|
|
81
|
+
size = "md",
|
|
82
|
+
fullWidth = false,
|
|
83
|
+
leftIcon,
|
|
84
|
+
children,
|
|
85
|
+
onMainClick,
|
|
86
|
+
onMenuClick,
|
|
87
|
+
menuLabel,
|
|
88
|
+
menuIcon,
|
|
89
|
+
loading = false,
|
|
90
|
+
disabled,
|
|
91
|
+
type,
|
|
92
|
+
...rest
|
|
93
|
+
},
|
|
94
|
+
ref,
|
|
95
|
+
) => {
|
|
96
|
+
const rootClass = [
|
|
97
|
+
styles.root,
|
|
98
|
+
variantClass[variant],
|
|
99
|
+
sizeClass[size],
|
|
100
|
+
fullWidth ? styles.fullWidth : "",
|
|
101
|
+
className ?? "",
|
|
102
|
+
]
|
|
103
|
+
.filter(Boolean)
|
|
104
|
+
.join(" ");
|
|
105
|
+
|
|
106
|
+
const partDisabled = disabled || loading;
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<div ref={ref} className={rootClass}>
|
|
110
|
+
<button
|
|
111
|
+
{...rest}
|
|
112
|
+
type={type ?? "button"}
|
|
113
|
+
className={`${styles.part} ${styles.main}`}
|
|
114
|
+
onClick={onMainClick}
|
|
115
|
+
disabled={partDisabled}
|
|
116
|
+
data-loading={loading || undefined}
|
|
117
|
+
>
|
|
118
|
+
{leftIcon && (
|
|
119
|
+
<span className={styles.icon} aria-hidden>
|
|
120
|
+
{sizedIcon(leftIcon, ICON_SIZE[size])}
|
|
121
|
+
</span>
|
|
122
|
+
)}
|
|
123
|
+
<span className={styles.label}>{children}</span>
|
|
124
|
+
</button>
|
|
125
|
+
<span className={styles.divider} aria-hidden />
|
|
126
|
+
<button
|
|
127
|
+
type="button"
|
|
128
|
+
className={`${styles.part} ${styles.menu}`}
|
|
129
|
+
onClick={onMenuClick}
|
|
130
|
+
disabled={partDisabled}
|
|
131
|
+
aria-label={menuLabel}
|
|
132
|
+
aria-haspopup="menu"
|
|
133
|
+
>
|
|
134
|
+
<span className={styles.icon} aria-hidden>
|
|
135
|
+
{sizedIcon(menuIcon ?? <Icon name="caret-down" />, ICON_SIZE[size])}
|
|
136
|
+
</span>
|
|
137
|
+
</button>
|
|
138
|
+
</div>
|
|
139
|
+
);
|
|
140
|
+
},
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
SplitButton.displayName = "SplitButton";
|
|
144
|
+
|
|
145
|
+
export { SplitButton };
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/* Stepper — 다단계 진행 표시 컴포넌트
|
|
2
|
+
구성: root(container) > item(button) > marker(dot|number) + body(title + hint)
|
|
3
|
+
축 반응:
|
|
4
|
+
─ Density → --space-scale, --font-scale (여백·라벨 크기)
|
|
5
|
+
─ Shape → focus ring radius (marker는 항상 full)
|
|
6
|
+
─ Motion → 상태 전환 duration/easing (transition-colors)
|
|
7
|
+
|
|
8
|
+
로컬 색 변수 (root에서 오버라이드 가능):
|
|
9
|
+
─ --stepper-fg : 활성/진행중 스텝 텍스트
|
|
10
|
+
─ --stepper-fg-muted : 완료 스텝 텍스트
|
|
11
|
+
─ --stepper-fg-subtle : 미도달 스텝 텍스트 · hint
|
|
12
|
+
─ --stepper-line : 스텝 사이 dashed 트리 라인
|
|
13
|
+
─ --stepper-dot-idle : 미도달 dot border
|
|
14
|
+
─ --stepper-marker-fg : done marker 안쪽 텍스트/아이콘 색 (배경 위)
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
.root {
|
|
18
|
+
--stepper-fg: var(--fg);
|
|
19
|
+
--stepper-fg-muted: var(--fg-muted);
|
|
20
|
+
--stepper-fg-subtle: var(--fg-subtle);
|
|
21
|
+
--stepper-line: var(--border);
|
|
22
|
+
--stepper-dot-idle: var(--border);
|
|
23
|
+
--stepper-marker-fg: var(--bg);
|
|
24
|
+
|
|
25
|
+
--stepper-marker-size: var(--space-5);
|
|
26
|
+
--stepper-marker-gap: var(--space-4);
|
|
27
|
+
--stepper-item-gap: var(--space-6);
|
|
28
|
+
|
|
29
|
+
display: flex;
|
|
30
|
+
min-width: 0;
|
|
31
|
+
min-height: 0;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* Orientation ─────────────────────────────────────────── */
|
|
35
|
+
|
|
36
|
+
.vertical { flex-direction: column; }
|
|
37
|
+
.horizontal { flex-direction: row; align-items: flex-start; }
|
|
38
|
+
|
|
39
|
+
/* Item ─────────────────────────────────────────────────── */
|
|
40
|
+
|
|
41
|
+
.item {
|
|
42
|
+
appearance: none;
|
|
43
|
+
background: transparent;
|
|
44
|
+
border: none;
|
|
45
|
+
padding: 0;
|
|
46
|
+
color: var(--stepper-fg-subtle);
|
|
47
|
+
cursor: pointer;
|
|
48
|
+
transition: var(--transition-colors);
|
|
49
|
+
position: relative;
|
|
50
|
+
text-align: left;
|
|
51
|
+
font-family: inherit;
|
|
52
|
+
min-width: 0;
|
|
53
|
+
}
|
|
54
|
+
.item:hover:not(:disabled) { color: var(--stepper-fg-muted); }
|
|
55
|
+
.item[data-state="done"] { color: var(--stepper-fg-muted); }
|
|
56
|
+
.item[data-state="active"] { color: var(--stepper-fg); }
|
|
57
|
+
.item:disabled { cursor: not-allowed; }
|
|
58
|
+
.item:focus-visible {
|
|
59
|
+
outline: none;
|
|
60
|
+
border-radius: var(--radius-sm);
|
|
61
|
+
box-shadow: var(--ring-shadow);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* Vertical item: 2-column grid, 아래로 dashed line */
|
|
65
|
+
.vertical .item {
|
|
66
|
+
display: grid;
|
|
67
|
+
grid-template-columns: var(--stepper-marker-size) 1fr;
|
|
68
|
+
gap: var(--stepper-marker-gap);
|
|
69
|
+
align-items: start;
|
|
70
|
+
padding-bottom: var(--stepper-item-gap);
|
|
71
|
+
}
|
|
72
|
+
.vertical .item:last-child { padding-bottom: 0; }
|
|
73
|
+
.vertical .item:not(:last-child)::before {
|
|
74
|
+
content: "";
|
|
75
|
+
position: absolute;
|
|
76
|
+
left: calc(var(--stepper-marker-size) / 2 - var(--hairline) / 2);
|
|
77
|
+
top: var(--stepper-marker-size);
|
|
78
|
+
bottom: 0;
|
|
79
|
+
width: var(--hairline);
|
|
80
|
+
background-image: linear-gradient(
|
|
81
|
+
to bottom,
|
|
82
|
+
var(--stepper-line) 50%,
|
|
83
|
+
transparent 50%
|
|
84
|
+
);
|
|
85
|
+
background-size: var(--hairline) var(--space-1_5);
|
|
86
|
+
background-repeat: repeat-y;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* Horizontal item: marker 위, body 아래로 쌓임, 옆으로 dashed line */
|
|
90
|
+
.horizontal .item {
|
|
91
|
+
display: grid;
|
|
92
|
+
grid-template-rows: var(--stepper-marker-size) 1fr;
|
|
93
|
+
gap: var(--space-2);
|
|
94
|
+
justify-items: start;
|
|
95
|
+
flex: 1 1 0;
|
|
96
|
+
min-width: 0;
|
|
97
|
+
padding-right: var(--stepper-item-gap);
|
|
98
|
+
}
|
|
99
|
+
.horizontal .item:last-child {
|
|
100
|
+
flex: 0 0 auto;
|
|
101
|
+
padding-right: 0;
|
|
102
|
+
}
|
|
103
|
+
.horizontal .item:not(:last-child)::before {
|
|
104
|
+
content: "";
|
|
105
|
+
position: absolute;
|
|
106
|
+
top: calc(var(--stepper-marker-size) / 2 - var(--hairline) / 2);
|
|
107
|
+
left: var(--stepper-marker-size);
|
|
108
|
+
right: 0;
|
|
109
|
+
height: var(--hairline);
|
|
110
|
+
background-image: linear-gradient(
|
|
111
|
+
to right,
|
|
112
|
+
var(--stepper-line) 50%,
|
|
113
|
+
transparent 50%
|
|
114
|
+
);
|
|
115
|
+
background-size: var(--space-1_5) var(--hairline);
|
|
116
|
+
background-repeat: repeat-x;
|
|
117
|
+
margin-inline: var(--space-2);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/* Marker ───────────────────────────────────────────────── */
|
|
121
|
+
|
|
122
|
+
.marker {
|
|
123
|
+
width: var(--stepper-marker-size);
|
|
124
|
+
height: var(--stepper-marker-size);
|
|
125
|
+
border-radius: var(--radius-full);
|
|
126
|
+
border: var(--hairline) solid var(--stepper-dot-idle);
|
|
127
|
+
display: inline-flex;
|
|
128
|
+
align-items: center;
|
|
129
|
+
justify-content: center;
|
|
130
|
+
background: transparent;
|
|
131
|
+
flex-shrink: 0;
|
|
132
|
+
transition: var(--transition-colors);
|
|
133
|
+
font-size: var(--text-xs);
|
|
134
|
+
font-weight: var(--font-weight-semibold);
|
|
135
|
+
line-height: 1;
|
|
136
|
+
color: inherit;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/* Active marker: 굵은 링 */
|
|
140
|
+
.item[data-state="active"] .marker {
|
|
141
|
+
border-color: var(--stepper-fg);
|
|
142
|
+
border-width: calc(var(--hairline) * 2);
|
|
143
|
+
color: var(--stepper-fg);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/* dot marker의 active: 안쪽 작은 점 */
|
|
147
|
+
.markerInnerDot {
|
|
148
|
+
width: var(--space-1_5);
|
|
149
|
+
height: var(--space-1_5);
|
|
150
|
+
border-radius: var(--radius-full);
|
|
151
|
+
background: var(--stepper-fg);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/* Done marker: 배경 채움, 안에 check (dot) 또는 숫자 유지 (number) */
|
|
155
|
+
.item[data-state="done"] .marker {
|
|
156
|
+
border-color: transparent;
|
|
157
|
+
background: var(--stepper-fg);
|
|
158
|
+
color: var(--stepper-marker-fg);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/* Body ─────────────────────────────────────────────────── */
|
|
162
|
+
|
|
163
|
+
.body {
|
|
164
|
+
display: flex;
|
|
165
|
+
flex-direction: column;
|
|
166
|
+
gap: var(--space-0_5);
|
|
167
|
+
min-width: 0;
|
|
168
|
+
padding-right: var(--space-2);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.title {
|
|
172
|
+
font-size: var(--text-sm);
|
|
173
|
+
font-weight: var(--font-weight-semibold);
|
|
174
|
+
}
|
|
175
|
+
.hint {
|
|
176
|
+
font-size: var(--text-xs);
|
|
177
|
+
color: var(--stepper-fg-subtle);
|
|
178
|
+
line-height: var(--lh-xs);
|
|
179
|
+
}
|
|
180
|
+
.item[data-state="active"] .hint {
|
|
181
|
+
color: var(--stepper-fg-muted);
|
|
182
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Children,
|
|
5
|
+
cloneElement,
|
|
6
|
+
createContext,
|
|
7
|
+
forwardRef,
|
|
8
|
+
isValidElement,
|
|
9
|
+
useContext,
|
|
10
|
+
useMemo,
|
|
11
|
+
type ButtonHTMLAttributes,
|
|
12
|
+
type HTMLAttributes,
|
|
13
|
+
type ReactElement,
|
|
14
|
+
type ReactNode,
|
|
15
|
+
} from "react";
|
|
16
|
+
import { Icon } from "../Icon";
|
|
17
|
+
import styles from "./Stepper.module.css";
|
|
18
|
+
|
|
19
|
+
type Orientation = "vertical" | "horizontal";
|
|
20
|
+
type Marker = "dot" | "number";
|
|
21
|
+
type ItemState = "upcoming" | "active" | "done";
|
|
22
|
+
|
|
23
|
+
type StepperContextValue = {
|
|
24
|
+
orientation: Orientation;
|
|
25
|
+
marker: Marker;
|
|
26
|
+
activeIndex: number;
|
|
27
|
+
onStepClick?: (index: number) => void;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const StepperContext = createContext<StepperContextValue | null>(null);
|
|
31
|
+
|
|
32
|
+
function useStepperContext(): StepperContextValue {
|
|
33
|
+
const ctx = useContext(StepperContext);
|
|
34
|
+
if (!ctx) {
|
|
35
|
+
throw new Error("Stepper.Item must be used inside <Stepper>.");
|
|
36
|
+
}
|
|
37
|
+
return ctx;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
export type StepperProps = Omit<HTMLAttributes<HTMLDivElement>, "onSelect"> & {
|
|
43
|
+
/** 현재 활성 스텝 index (0-based). */
|
|
44
|
+
activeIndex: number;
|
|
45
|
+
/** 세로/가로 배치. 기본 vertical. */
|
|
46
|
+
orientation?: Orientation;
|
|
47
|
+
/** marker 종류. dot(기본) | number. */
|
|
48
|
+
marker?: Marker;
|
|
49
|
+
/** 스텝 클릭 시. 자식 Item에 onClick이 있으면 그쪽이 우선. */
|
|
50
|
+
onStepClick?: (index: number) => void;
|
|
51
|
+
children: ReactNode;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const StepperRoot = forwardRef<HTMLDivElement, StepperProps>(
|
|
55
|
+
(
|
|
56
|
+
{
|
|
57
|
+
activeIndex,
|
|
58
|
+
orientation = "vertical",
|
|
59
|
+
marker = "dot",
|
|
60
|
+
onStepClick,
|
|
61
|
+
className,
|
|
62
|
+
children,
|
|
63
|
+
...rest
|
|
64
|
+
},
|
|
65
|
+
ref,
|
|
66
|
+
) => {
|
|
67
|
+
const ctxValue = useMemo<StepperContextValue>(
|
|
68
|
+
() => ({ orientation, marker, activeIndex, onStepClick }),
|
|
69
|
+
[orientation, marker, activeIndex, onStepClick],
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
// 자식 Item에 index를 자동 주입
|
|
73
|
+
const items = Children.toArray(children).filter(isValidElement);
|
|
74
|
+
const injected = items.map((child, i) =>
|
|
75
|
+
cloneElement(child as ReactElement<StepperItemProps>, {
|
|
76
|
+
index: (child as ReactElement<StepperItemProps>).props.index ?? i,
|
|
77
|
+
}),
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<StepperContext.Provider value={ctxValue}>
|
|
82
|
+
<div
|
|
83
|
+
ref={ref}
|
|
84
|
+
role="list"
|
|
85
|
+
className={[
|
|
86
|
+
styles.root,
|
|
87
|
+
orientation === "vertical" ? styles.vertical : styles.horizontal,
|
|
88
|
+
className ?? "",
|
|
89
|
+
]
|
|
90
|
+
.filter(Boolean)
|
|
91
|
+
.join(" ")}
|
|
92
|
+
data-orientation={orientation}
|
|
93
|
+
data-marker={marker}
|
|
94
|
+
{...rest}
|
|
95
|
+
>
|
|
96
|
+
{injected}
|
|
97
|
+
</div>
|
|
98
|
+
</StepperContext.Provider>
|
|
99
|
+
);
|
|
100
|
+
},
|
|
101
|
+
);
|
|
102
|
+
StepperRoot.displayName = "Stepper";
|
|
103
|
+
|
|
104
|
+
// ────────────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
export type StepperItemProps = Omit<
|
|
107
|
+
ButtonHTMLAttributes<HTMLButtonElement>,
|
|
108
|
+
"title"
|
|
109
|
+
> & {
|
|
110
|
+
/** Stepper가 자동 주입. 수동 override 가능. */
|
|
111
|
+
index?: number;
|
|
112
|
+
title: ReactNode;
|
|
113
|
+
hint?: ReactNode;
|
|
114
|
+
/** 명시적 상태 지정. 미지정이면 activeIndex 기준으로 자동 결정. */
|
|
115
|
+
state?: ItemState;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const StepperItem = forwardRef<HTMLButtonElement, StepperItemProps>(
|
|
119
|
+
(
|
|
120
|
+
{
|
|
121
|
+
index,
|
|
122
|
+
title,
|
|
123
|
+
hint,
|
|
124
|
+
state: stateProp,
|
|
125
|
+
disabled,
|
|
126
|
+
onClick,
|
|
127
|
+
className,
|
|
128
|
+
children: _children,
|
|
129
|
+
...rest
|
|
130
|
+
},
|
|
131
|
+
ref,
|
|
132
|
+
) => {
|
|
133
|
+
const { orientation, marker, activeIndex, onStepClick } =
|
|
134
|
+
useStepperContext();
|
|
135
|
+
|
|
136
|
+
const idx = index ?? 0;
|
|
137
|
+
const state: ItemState =
|
|
138
|
+
stateProp ??
|
|
139
|
+
(idx === activeIndex ? "active" : idx < activeIndex ? "done" : "upcoming");
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<button
|
|
143
|
+
ref={ref}
|
|
144
|
+
type="button"
|
|
145
|
+
role="listitem"
|
|
146
|
+
className={[styles.item, className ?? ""].filter(Boolean).join(" ")}
|
|
147
|
+
data-state={state}
|
|
148
|
+
data-orientation={orientation}
|
|
149
|
+
disabled={disabled}
|
|
150
|
+
onClick={(e) => {
|
|
151
|
+
onClick?.(e);
|
|
152
|
+
if (!e.defaultPrevented) onStepClick?.(idx);
|
|
153
|
+
}}
|
|
154
|
+
aria-current={state === "active" ? "step" : undefined}
|
|
155
|
+
{...rest}
|
|
156
|
+
>
|
|
157
|
+
<span className={styles.marker} aria-hidden>
|
|
158
|
+
{state === "done" ? (
|
|
159
|
+
<Icon name="check" size={12} />
|
|
160
|
+
) : marker === "number" ? (
|
|
161
|
+
idx + 1
|
|
162
|
+
) : state === "active" ? (
|
|
163
|
+
<span className={styles.markerInnerDot} />
|
|
164
|
+
) : null}
|
|
165
|
+
</span>
|
|
166
|
+
<span className={styles.body}>
|
|
167
|
+
<span className={styles.title}>{title}</span>
|
|
168
|
+
{hint != null && <span className={styles.hint}>{hint}</span>}
|
|
169
|
+
</span>
|
|
170
|
+
</button>
|
|
171
|
+
);
|
|
172
|
+
},
|
|
173
|
+
);
|
|
174
|
+
StepperItem.displayName = "Stepper.Item";
|
|
175
|
+
|
|
176
|
+
// ────────────────────────────────────────────────────────
|
|
177
|
+
|
|
178
|
+
type StepperComponent = typeof StepperRoot & {
|
|
179
|
+
Item: typeof StepperItem;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const Stepper = StepperRoot as StepperComponent;
|
|
183
|
+
Stepper.Item = StepperItem;
|
|
184
|
+
|
|
185
|
+
export { Stepper };
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/* Switch — 즉시 적용되는 on/off 컨트롤.
|
|
2
|
+
시각: 알약 모양 트랙 + 원형 썸. checked면 트랙이 --primary로 채워지고 썸이 우측 이동.
|
|
3
|
+
시맨틱: role=switch가 붙은 checkbox input. label 래퍼가 hitbox.
|
|
4
|
+
|
|
5
|
+
축 반응:
|
|
6
|
+
─ Density → --control-h-* (트랙 세로 크기 비례)
|
|
7
|
+
─ Motion → 썸 이동·색 전환 duration
|
|
8
|
+
─ Palette → 켜졌을 때 --primary
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
.root {
|
|
12
|
+
--_track-h: var(--space-4);
|
|
13
|
+
--_track-w: var(--space-8);
|
|
14
|
+
--_thumb: var(--space-3);
|
|
15
|
+
--_pad: var(--space-0_5);
|
|
16
|
+
display: inline-flex;
|
|
17
|
+
align-items: center;
|
|
18
|
+
cursor: pointer;
|
|
19
|
+
user-select: none;
|
|
20
|
+
-webkit-tap-highlight-color: transparent;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/* Sizes */
|
|
24
|
+
.size_sm {
|
|
25
|
+
--_track-h: var(--space-3);
|
|
26
|
+
--_track-w: var(--space-6);
|
|
27
|
+
--_thumb: var(--space-2);
|
|
28
|
+
}
|
|
29
|
+
.size_md {
|
|
30
|
+
--_track-h: var(--space-4);
|
|
31
|
+
--_track-w: var(--space-8);
|
|
32
|
+
--_thumb: var(--space-3);
|
|
33
|
+
}
|
|
34
|
+
.size_lg {
|
|
35
|
+
--_track-h: var(--space-5);
|
|
36
|
+
--_track-w: var(--space-10);
|
|
37
|
+
--_thumb: var(--space-4);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/* Native input: visually-hidden but focusable & form-submittable */
|
|
41
|
+
.input {
|
|
42
|
+
position: absolute;
|
|
43
|
+
width: var(--hairline);
|
|
44
|
+
height: var(--hairline);
|
|
45
|
+
padding: 0;
|
|
46
|
+
margin: 0;
|
|
47
|
+
overflow: hidden;
|
|
48
|
+
clip: rect(0, 0, 0, 0);
|
|
49
|
+
white-space: nowrap;
|
|
50
|
+
border: 0;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.track {
|
|
54
|
+
position: relative;
|
|
55
|
+
display: inline-block;
|
|
56
|
+
width: var(--_track-w);
|
|
57
|
+
height: var(--_track-h);
|
|
58
|
+
background: var(--bg-muted);
|
|
59
|
+
border: var(--hairline) solid var(--border);
|
|
60
|
+
border-radius: var(--radius-full);
|
|
61
|
+
transition:
|
|
62
|
+
background var(--duration-slow) var(--ease-out),
|
|
63
|
+
border-color var(--duration-slow) var(--ease-out),
|
|
64
|
+
box-shadow var(--duration-fast) var(--ease-out);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.thumb {
|
|
68
|
+
position: absolute;
|
|
69
|
+
top: 50%;
|
|
70
|
+
left: var(--_pad);
|
|
71
|
+
width: var(--_thumb);
|
|
72
|
+
height: var(--_thumb);
|
|
73
|
+
background: var(--bg);
|
|
74
|
+
border-radius: var(--radius-full);
|
|
75
|
+
box-shadow: var(--shadow-xs);
|
|
76
|
+
transform: translateY(-50%);
|
|
77
|
+
transition:
|
|
78
|
+
left var(--duration-base) var(--ease-out),
|
|
79
|
+
background var(--duration-slow) var(--ease-out);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/* Checked — 트랙은 primary로 채우고, 썸을 우측 끝 - pad로 이동. */
|
|
83
|
+
.input:checked + .track {
|
|
84
|
+
background: var(--primary);
|
|
85
|
+
border-color: var(--primary);
|
|
86
|
+
}
|
|
87
|
+
.input:checked + .track .thumb {
|
|
88
|
+
left: calc(var(--_track-w) - var(--_thumb) - var(--_pad) - var(--hairline) * 2);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/* Hover */
|
|
92
|
+
.root:hover .input:not(:disabled) + .track {
|
|
93
|
+
border-color: var(--border-strong);
|
|
94
|
+
}
|
|
95
|
+
.root:hover .input:not(:disabled):checked + .track {
|
|
96
|
+
background: var(--primary-hover);
|
|
97
|
+
border-color: var(--primary-hover);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/* Focus — 트랙에 링 표시 */
|
|
101
|
+
.input:focus-visible + .track {
|
|
102
|
+
box-shadow: var(--ring-shadow);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/* Disabled — Button과 동일한 통일 스펙 (실버 · 회색) */
|
|
106
|
+
.root:has(.input:disabled) {
|
|
107
|
+
cursor: not-allowed;
|
|
108
|
+
}
|
|
109
|
+
.input:disabled + .track {
|
|
110
|
+
background: var(--bg-muted);
|
|
111
|
+
border-color: transparent;
|
|
112
|
+
}
|
|
113
|
+
.input:disabled + .track .thumb {
|
|
114
|
+
background: var(--fg-disabled);
|
|
115
|
+
box-shadow: none;
|
|
116
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
forwardRef,
|
|
5
|
+
useCallback,
|
|
6
|
+
type CSSProperties,
|
|
7
|
+
type InputHTMLAttributes,
|
|
8
|
+
} from "react";
|
|
9
|
+
|
|
10
|
+
import styles from "./Switch.module.css";
|
|
11
|
+
|
|
12
|
+
export type SwitchSize = "sm" | "md" | "lg";
|
|
13
|
+
|
|
14
|
+
export type SwitchProps = Omit<
|
|
15
|
+
InputHTMLAttributes<HTMLInputElement>,
|
|
16
|
+
"type" | "size"
|
|
17
|
+
> & {
|
|
18
|
+
size?: SwitchSize;
|
|
19
|
+
onCheckedChange?: (checked: boolean) => void;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const Switch = forwardRef<HTMLInputElement, SwitchProps>(
|
|
23
|
+
(
|
|
24
|
+
{ className, size = "md", onCheckedChange, onChange, style, ...rest },
|
|
25
|
+
ref,
|
|
26
|
+
) => {
|
|
27
|
+
const handleChange = useCallback(
|
|
28
|
+
(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
29
|
+
onChange?.(e);
|
|
30
|
+
if (!e.defaultPrevented) onCheckedChange?.(e.target.checked);
|
|
31
|
+
},
|
|
32
|
+
[onChange, onCheckedChange],
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const cls = [styles.root, styles[`size_${size}`], className ?? ""]
|
|
36
|
+
.filter(Boolean)
|
|
37
|
+
.join(" ");
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<label className={cls} style={style as CSSProperties}>
|
|
41
|
+
<input
|
|
42
|
+
ref={ref}
|
|
43
|
+
type="checkbox"
|
|
44
|
+
role="switch"
|
|
45
|
+
className={styles.input}
|
|
46
|
+
onChange={handleChange}
|
|
47
|
+
{...rest}
|
|
48
|
+
/>
|
|
49
|
+
<span className={styles.track} aria-hidden>
|
|
50
|
+
<span className={styles.thumb} />
|
|
51
|
+
</span>
|
|
52
|
+
</label>
|
|
53
|
+
);
|
|
54
|
+
},
|
|
55
|
+
);
|
|
56
|
+
Switch.displayName = "Switch";
|
|
57
|
+
|
|
58
|
+
export { Switch };
|