@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,279 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Children,
|
|
5
|
+
Fragment,
|
|
6
|
+
cloneElement,
|
|
7
|
+
forwardRef,
|
|
8
|
+
isValidElement,
|
|
9
|
+
type AnchorHTMLAttributes,
|
|
10
|
+
type ButtonHTMLAttributes,
|
|
11
|
+
type HTMLAttributes,
|
|
12
|
+
type ReactElement,
|
|
13
|
+
type ReactNode,
|
|
14
|
+
} from "react";
|
|
15
|
+
import * as RPopover from "@radix-ui/react-popover";
|
|
16
|
+
import { Slot, Slottable } from "@radix-ui/react-slot";
|
|
17
|
+
|
|
18
|
+
import { Icon, IconAxisProvider, useIconAxis } from "../Icon";
|
|
19
|
+
import { MenuItem } from "../MenuItem";
|
|
20
|
+
import styles from "./Breadcrumb.module.css";
|
|
21
|
+
|
|
22
|
+
/** 트리거 내부 아이콘 렌더 사이즈 — Button xs와 동일한 12px. */
|
|
23
|
+
const ICON_SIZE = 12;
|
|
24
|
+
|
|
25
|
+
function sizedIcon(node: ReactNode): ReactNode {
|
|
26
|
+
if (!isValidElement(node)) return node;
|
|
27
|
+
const el = node as ReactElement<{ size?: number | string }>;
|
|
28
|
+
if (el.props.size !== undefined) return el;
|
|
29
|
+
return cloneElement(el, { size: ICON_SIZE });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ────────────────────────────────────────────────────────────
|
|
33
|
+
// Root — <nav aria-label="…"> + <ol>. Item 사이에 separator를 자동 삽입.
|
|
34
|
+
// ────────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
export interface BreadcrumbProps extends Omit<HTMLAttributes<HTMLElement>, "children"> {
|
|
37
|
+
/** 스크린리더용 라벨. 기본 "Breadcrumb". */
|
|
38
|
+
"aria-label"?: string;
|
|
39
|
+
/** 항목 사이 구분자. 기본은 caret-right 아이콘. */
|
|
40
|
+
separator?: ReactNode;
|
|
41
|
+
children: ReactNode;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const Root = forwardRef<HTMLElement, BreadcrumbProps>(
|
|
45
|
+
({ className, separator, children, "aria-label": ariaLabel = "Breadcrumb", ...rest }, ref) => {
|
|
46
|
+
const sep = separator ?? <Icon name="caret-right" size={12} />;
|
|
47
|
+
const items = Children.toArray(children).filter(isValidElement);
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<nav
|
|
51
|
+
ref={ref}
|
|
52
|
+
aria-label={ariaLabel}
|
|
53
|
+
className={[styles.root, className ?? ""].filter(Boolean).join(" ")}
|
|
54
|
+
{...rest}
|
|
55
|
+
>
|
|
56
|
+
<ol className={styles.list}>
|
|
57
|
+
{items.map((child, i) => (
|
|
58
|
+
<Fragment key={(child as ReactElement).key ?? i}>
|
|
59
|
+
<li className={styles.item}>{child}</li>
|
|
60
|
+
{i < items.length - 1 && (
|
|
61
|
+
<li className={styles.separator} aria-hidden>
|
|
62
|
+
{sep}
|
|
63
|
+
</li>
|
|
64
|
+
)}
|
|
65
|
+
</Fragment>
|
|
66
|
+
))}
|
|
67
|
+
</ol>
|
|
68
|
+
</nav>
|
|
69
|
+
);
|
|
70
|
+
},
|
|
71
|
+
);
|
|
72
|
+
Root.displayName = "Breadcrumb";
|
|
73
|
+
|
|
74
|
+
// ────────────────────────────────────────────────────────────
|
|
75
|
+
// Link — Button xs ghost와 동일한 토큰으로 스타일. asChild로 Next Link 등에 위임 가능.
|
|
76
|
+
// ────────────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
export interface BreadcrumbLinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
|
|
79
|
+
/** 자식 요소로 렌더 (예: Next.js <Link>). */
|
|
80
|
+
asChild?: boolean;
|
|
81
|
+
leftIcon?: ReactNode;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const Link = forwardRef<HTMLAnchorElement, BreadcrumbLinkProps>(
|
|
85
|
+
({ asChild = false, leftIcon, className, children, ...rest }, ref) => {
|
|
86
|
+
const Comp = asChild ? Slot : "a";
|
|
87
|
+
const hasChildren = children !== undefined && children !== null && children !== false;
|
|
88
|
+
// 아이콘 단독 모드 — children 없이 leftIcon만: 원형 hit box + 접근성 라벨은 aria-label에 위임.
|
|
89
|
+
const iconOnly = !hasChildren && leftIcon != null;
|
|
90
|
+
const cls = [styles.link, iconOnly ? styles.linkIconOnly : "", className ?? ""]
|
|
91
|
+
.filter(Boolean)
|
|
92
|
+
.join(" ");
|
|
93
|
+
// asChild일 때 label을 Slottable로 감싸면 Slot이 자식 요소의 children으로 병합.
|
|
94
|
+
return (
|
|
95
|
+
<Comp ref={ref} className={cls} {...rest}>
|
|
96
|
+
{leftIcon && (
|
|
97
|
+
<span className={styles.linkIcon} aria-hidden>
|
|
98
|
+
{sizedIcon(leftIcon)}
|
|
99
|
+
</span>
|
|
100
|
+
)}
|
|
101
|
+
{hasChildren && (
|
|
102
|
+
<Slottable>
|
|
103
|
+
<span className={styles.linkLabel}>{children}</span>
|
|
104
|
+
</Slottable>
|
|
105
|
+
)}
|
|
106
|
+
</Comp>
|
|
107
|
+
);
|
|
108
|
+
},
|
|
109
|
+
);
|
|
110
|
+
Link.displayName = "Breadcrumb.Link";
|
|
111
|
+
|
|
112
|
+
// ────────────────────────────────────────────────────────────
|
|
113
|
+
// Current — 현재 페이지. 링크 아님. aria-current="page".
|
|
114
|
+
// ────────────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
export interface BreadcrumbCurrentProps extends HTMLAttributes<HTMLSpanElement> {
|
|
117
|
+
leftIcon?: ReactNode;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const Current = forwardRef<HTMLSpanElement, BreadcrumbCurrentProps>(
|
|
121
|
+
({ className, leftIcon, children, ...rest }, ref) => {
|
|
122
|
+
const hasChildren = children !== undefined && children !== null && children !== false;
|
|
123
|
+
const iconOnly = !hasChildren && leftIcon != null;
|
|
124
|
+
const cls = [styles.current, iconOnly ? styles.linkIconOnly : "", className ?? ""]
|
|
125
|
+
.filter(Boolean)
|
|
126
|
+
.join(" ");
|
|
127
|
+
return (
|
|
128
|
+
<span ref={ref} aria-current="page" className={cls} {...rest}>
|
|
129
|
+
{leftIcon && (
|
|
130
|
+
<span className={styles.linkIcon} aria-hidden>
|
|
131
|
+
{sizedIcon(leftIcon)}
|
|
132
|
+
</span>
|
|
133
|
+
)}
|
|
134
|
+
{hasChildren && <span className={styles.linkLabel}>{children}</span>}
|
|
135
|
+
</span>
|
|
136
|
+
);
|
|
137
|
+
},
|
|
138
|
+
);
|
|
139
|
+
Current.displayName = "Breadcrumb.Current";
|
|
140
|
+
|
|
141
|
+
// ────────────────────────────────────────────────────────────
|
|
142
|
+
// Ellipsis — 항목이 많을 때 중간 축약. 클릭 시 Select와 동일한 팝오버 표면으로
|
|
143
|
+
// 숨긴 항목 리스트를 연다. items는 필수 — 클릭 불가한 정적 마커는 지원하지 않는다.
|
|
144
|
+
// ────────────────────────────────────────────────────────────
|
|
145
|
+
|
|
146
|
+
export type BreadcrumbEllipsisItem = {
|
|
147
|
+
label: ReactNode;
|
|
148
|
+
href?: string;
|
|
149
|
+
onSelect?: () => void;
|
|
150
|
+
leftIcon?: ReactNode;
|
|
151
|
+
disabled?: boolean;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
export interface BreadcrumbEllipsisProps
|
|
155
|
+
extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, "onSelect"> {
|
|
156
|
+
/** 숨긴 항목 리스트. 최소 1개. */
|
|
157
|
+
items: BreadcrumbEllipsisItem[];
|
|
158
|
+
/** 팝오버 side offset (px). 기본 6. */
|
|
159
|
+
sideOffset?: number;
|
|
160
|
+
/** aria-label. 기본 "Show hidden pages". */
|
|
161
|
+
"aria-label"?: string;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const Ellipsis = forwardRef<HTMLButtonElement, BreadcrumbEllipsisProps>(
|
|
165
|
+
(
|
|
166
|
+
{
|
|
167
|
+
className,
|
|
168
|
+
items,
|
|
169
|
+
sideOffset = 6,
|
|
170
|
+
"aria-label": ariaLabel = "Show hidden pages",
|
|
171
|
+
...rest
|
|
172
|
+
},
|
|
173
|
+
ref,
|
|
174
|
+
) => {
|
|
175
|
+
return (
|
|
176
|
+
<RPopover.Root>
|
|
177
|
+
<RPopover.Trigger asChild>
|
|
178
|
+
<button
|
|
179
|
+
ref={ref}
|
|
180
|
+
type="button"
|
|
181
|
+
aria-label={ariaLabel}
|
|
182
|
+
className={[styles.ellipsisTrigger, className ?? ""].filter(Boolean).join(" ")}
|
|
183
|
+
{...rest}
|
|
184
|
+
>
|
|
185
|
+
…
|
|
186
|
+
</button>
|
|
187
|
+
</RPopover.Trigger>
|
|
188
|
+
<EllipsisContent items={items} sideOffset={sideOffset} />
|
|
189
|
+
</RPopover.Root>
|
|
190
|
+
);
|
|
191
|
+
},
|
|
192
|
+
);
|
|
193
|
+
Ellipsis.displayName = "Breadcrumb.Ellipsis";
|
|
194
|
+
|
|
195
|
+
/** Portal 하위에서 IconAxis 재주입 — Radix Portal이 트리 컨텍스트를 끊는다. */
|
|
196
|
+
function EllipsisContent({
|
|
197
|
+
items,
|
|
198
|
+
sideOffset,
|
|
199
|
+
}: {
|
|
200
|
+
items: BreadcrumbEllipsisItem[];
|
|
201
|
+
sideOffset: number;
|
|
202
|
+
}) {
|
|
203
|
+
const iconAxis = useIconAxis();
|
|
204
|
+
return (
|
|
205
|
+
<RPopover.Portal>
|
|
206
|
+
<RPopover.Content
|
|
207
|
+
align="start"
|
|
208
|
+
sideOffset={sideOffset}
|
|
209
|
+
collisionPadding={8}
|
|
210
|
+
className={[styles.popoverContent, "m-float"].join(" ")}
|
|
211
|
+
>
|
|
212
|
+
<IconAxisProvider value={iconAxis}>
|
|
213
|
+
<ul className={styles.popoverList} role="menu">
|
|
214
|
+
{items.map((item, i) => {
|
|
215
|
+
const leading = item.leftIcon
|
|
216
|
+
? [
|
|
217
|
+
isValidElement(item.leftIcon) &&
|
|
218
|
+
(item.leftIcon as ReactElement<{ size?: number | string }>).props.size === undefined
|
|
219
|
+
? cloneElement(item.leftIcon as ReactElement<{ size?: number | string }>, {
|
|
220
|
+
size: 16,
|
|
221
|
+
})
|
|
222
|
+
: item.leftIcon,
|
|
223
|
+
]
|
|
224
|
+
: undefined;
|
|
225
|
+
const inner = (
|
|
226
|
+
<MenuItem size="sm" disabled={item.disabled} leadingActions={leading}>
|
|
227
|
+
{item.label}
|
|
228
|
+
</MenuItem>
|
|
229
|
+
);
|
|
230
|
+
return (
|
|
231
|
+
<li key={i} role="none">
|
|
232
|
+
{item.href ? (
|
|
233
|
+
<RPopover.Close asChild>
|
|
234
|
+
<a
|
|
235
|
+
href={item.href}
|
|
236
|
+
role="menuitem"
|
|
237
|
+
className={styles.popoverItemLink}
|
|
238
|
+
aria-disabled={item.disabled || undefined}
|
|
239
|
+
>
|
|
240
|
+
{inner}
|
|
241
|
+
</a>
|
|
242
|
+
</RPopover.Close>
|
|
243
|
+
) : (
|
|
244
|
+
<RPopover.Close asChild>
|
|
245
|
+
<button
|
|
246
|
+
type="button"
|
|
247
|
+
role="menuitem"
|
|
248
|
+
disabled={item.disabled}
|
|
249
|
+
onClick={() => item.onSelect?.()}
|
|
250
|
+
className={styles.popoverItemButton}
|
|
251
|
+
>
|
|
252
|
+
{inner}
|
|
253
|
+
</button>
|
|
254
|
+
</RPopover.Close>
|
|
255
|
+
)}
|
|
256
|
+
</li>
|
|
257
|
+
);
|
|
258
|
+
})}
|
|
259
|
+
</ul>
|
|
260
|
+
</IconAxisProvider>
|
|
261
|
+
</RPopover.Content>
|
|
262
|
+
</RPopover.Portal>
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ────────────────────────────────────────────────────────────
|
|
267
|
+
|
|
268
|
+
type BreadcrumbCompound = typeof Root & {
|
|
269
|
+
Link: typeof Link;
|
|
270
|
+
Current: typeof Current;
|
|
271
|
+
Ellipsis: typeof Ellipsis;
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
const Breadcrumb = Root as BreadcrumbCompound;
|
|
275
|
+
Breadcrumb.Link = Link;
|
|
276
|
+
Breadcrumb.Current = Current;
|
|
277
|
+
Breadcrumb.Ellipsis = Ellipsis;
|
|
278
|
+
|
|
279
|
+
export { Breadcrumb };
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/* Base ─────────────────────────────────────────────────────── */
|
|
2
|
+
.root {
|
|
3
|
+
display: inline-flex;
|
|
4
|
+
align-items: center;
|
|
5
|
+
justify-content: center;
|
|
6
|
+
gap: var(--space-2);
|
|
7
|
+
font-family: var(--label-font);
|
|
8
|
+
font-weight: var(--label-weight);
|
|
9
|
+
line-height: var(--lh-tight);
|
|
10
|
+
letter-spacing: var(--label-tracking);
|
|
11
|
+
white-space: nowrap;
|
|
12
|
+
cursor: pointer;
|
|
13
|
+
user-select: none;
|
|
14
|
+
border: 1px solid transparent;
|
|
15
|
+
/* 인터랙션 트랜지션은 globals.css의 시맨틱 프리셋 3종 조합.
|
|
16
|
+
값 튜닝은 --transition-* 토큰에서만. */
|
|
17
|
+
transition: var(--transition-colors), var(--transition-ring), var(--transition-press);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.root:focus-visible {
|
|
21
|
+
outline: none;
|
|
22
|
+
box-shadow: var(--ring-shadow);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/* Press: 진입·해제 모두 base + ease-out → 대칭 부드러움 */
|
|
26
|
+
.root:active:not(:disabled) {
|
|
27
|
+
transform: translateY(var(--space-0_5));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/* Hover 시 아이콘 밝기 상승 (0.75 → 1.0) */
|
|
31
|
+
.root:hover:not(:disabled) .icon {
|
|
32
|
+
opacity: 1;
|
|
33
|
+
}
|
|
34
|
+
.icon {
|
|
35
|
+
transition: opacity var(--duration-slow) var(--ease-out);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/* Disabled — variant 무관 통일 스타일.
|
|
39
|
+
회색 버튼(--bg-muted) + 실버 글자(--fg-disabled) + 투명 보더.
|
|
40
|
+
loading은 opacity로 유지 (스피너 컨텍스트 보존). */
|
|
41
|
+
.root:disabled {
|
|
42
|
+
cursor: not-allowed;
|
|
43
|
+
background: var(--bg-muted);
|
|
44
|
+
color: var(--fg-disabled);
|
|
45
|
+
border-color: transparent;
|
|
46
|
+
box-shadow: none;
|
|
47
|
+
}
|
|
48
|
+
.root:disabled .icon {
|
|
49
|
+
opacity: 1;
|
|
50
|
+
}
|
|
51
|
+
.root[data-loading="true"] {
|
|
52
|
+
cursor: not-allowed;
|
|
53
|
+
opacity: 0.5;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* Full width ─────────────────────────────────────────────────── */
|
|
57
|
+
.fullWidth {
|
|
58
|
+
width: 100%;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* Sizes ──────────────────────────────────────────────────────── */
|
|
62
|
+
/* Size는 높이·패딩·타이포·gap·radius 담당.
|
|
63
|
+
Shape 축(soft/standard/crisp)이 각 --radius-*의 실제 값을 스왑. */
|
|
64
|
+
.xs {
|
|
65
|
+
height: var(--control-h-xs);
|
|
66
|
+
padding-inline: var(--space-1);
|
|
67
|
+
gap: var(--space-1);
|
|
68
|
+
font-size: var(--text-xs);
|
|
69
|
+
border-radius: var(--radius-xs);
|
|
70
|
+
--btn-icon-size: var(--space-3);
|
|
71
|
+
}
|
|
72
|
+
.sm {
|
|
73
|
+
height: var(--control-h-sm);
|
|
74
|
+
padding-inline: var(--space-2);
|
|
75
|
+
gap: var(--space-1_5);
|
|
76
|
+
font-size: var(--label-sm-size);
|
|
77
|
+
border-radius: var(--radius-xs);
|
|
78
|
+
--btn-icon-size: var(--space-4);
|
|
79
|
+
}
|
|
80
|
+
.md {
|
|
81
|
+
height: var(--control-h-md);
|
|
82
|
+
padding-inline: var(--space-2);
|
|
83
|
+
gap: var(--space-1);
|
|
84
|
+
font-size: var(--label-md-size);
|
|
85
|
+
border-radius: var(--radius-sm);
|
|
86
|
+
--btn-icon-size: var(--space-4);
|
|
87
|
+
}
|
|
88
|
+
.lg {
|
|
89
|
+
height: var(--control-h-lg);
|
|
90
|
+
padding-inline: var(--space-4);
|
|
91
|
+
gap: var(--space-1_5);
|
|
92
|
+
font-size: var(--label-md-size);
|
|
93
|
+
border-radius: var(--radius-md);
|
|
94
|
+
--btn-icon-size: var(--space-5);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
/* Icon / label slots ─────────────────────────────────────────── */
|
|
99
|
+
/* 아이콘 슬롯을 고정 정사각 박스로 강제. SVG viewBox 내부 여백이
|
|
100
|
+
라이브러리·아이콘마다 달라도 슬롯 밖 레이아웃(gap·padding)은 항상 균일.
|
|
101
|
+
슬롯 안에서 SVG는 100% 채움. */
|
|
102
|
+
.icon {
|
|
103
|
+
display: inline-flex;
|
|
104
|
+
align-items: center;
|
|
105
|
+
justify-content: center;
|
|
106
|
+
flex-shrink: 0;
|
|
107
|
+
width: var(--btn-icon-size);
|
|
108
|
+
height: var(--btn-icon-size);
|
|
109
|
+
/* 아이콘은 텍스트보다 한 단계 옅게 (Silver 톤).
|
|
110
|
+
currentColor 위에 opacity로 조절해서 filled/ghost 모두 대응. */
|
|
111
|
+
opacity: 0.75;
|
|
112
|
+
}
|
|
113
|
+
.icon > svg {
|
|
114
|
+
width: 100%;
|
|
115
|
+
height: 100%;
|
|
116
|
+
display: block;
|
|
117
|
+
}
|
|
118
|
+
.label {
|
|
119
|
+
display: inline-flex;
|
|
120
|
+
align-items: center;
|
|
121
|
+
}
|
|
122
|
+
/* Badge 슬롯: 라벨 오른쪽 트레일링. Badge 컴포넌트가 자체 색·radius를 가지므로
|
|
123
|
+
슬롯은 정렬만 담당. gap은 sizeClass 기본값(--space-1~2) 사용. */
|
|
124
|
+
.badge {
|
|
125
|
+
display: inline-flex;
|
|
126
|
+
align-items: center;
|
|
127
|
+
flex-shrink: 0;
|
|
128
|
+
}
|
|
129
|
+
/* 아이콘 크기는 Button.tsx가 Icon size prop으로 직접 주입.
|
|
130
|
+
여기서 CSS로 강제 축소하면 stroke가 뭉개져 두꺼워 보임. */
|
|
131
|
+
|
|
132
|
+
/* Variants ───────────────────────────────────────────────────── */
|
|
133
|
+
/* primary — 채움, 브랜드 컬러. 위 텍스트/아이콘은 --bg 사용(라이트=흰, 다크=거의 검정) */
|
|
134
|
+
.primary {
|
|
135
|
+
background: var(--primary);
|
|
136
|
+
color: var(--bg);
|
|
137
|
+
border-color: var(--primary);
|
|
138
|
+
}
|
|
139
|
+
.primary:hover:not(:disabled) {
|
|
140
|
+
background: var(--primary-hover);
|
|
141
|
+
border-color: var(--primary-hover);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/* inverse — 흑백 반전. mono Accent 프리셋에서 CTA 외 강조 버튼용.
|
|
145
|
+
라이트: 검정 배경/흰 텍스트, 다크: 흰 배경/검정 텍스트. Hue 축과 무관. */
|
|
146
|
+
.inverse {
|
|
147
|
+
background: var(--fg);
|
|
148
|
+
color: var(--bg);
|
|
149
|
+
border-color: var(--fg);
|
|
150
|
+
}
|
|
151
|
+
.inverse:hover:not(:disabled) {
|
|
152
|
+
background: var(--fg-strong);
|
|
153
|
+
border-color: var(--fg-strong);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/* secondary — primary 8% 알파 표면 + primary 텍스트.
|
|
157
|
+
Accent=mono일 때는 무채색(--bg-muted + --fg)으로 자동 폴백. */
|
|
158
|
+
.secondary {
|
|
159
|
+
background: var(--secondary-bg);
|
|
160
|
+
color: var(--secondary-fg);
|
|
161
|
+
border-color: transparent;
|
|
162
|
+
}
|
|
163
|
+
.secondary:hover:not(:disabled) {
|
|
164
|
+
background: var(--secondary-bg-hover);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/* outline — 경계만 */
|
|
168
|
+
.outline {
|
|
169
|
+
background: transparent;
|
|
170
|
+
color: var(--fg);
|
|
171
|
+
border-color: var(--border);
|
|
172
|
+
}
|
|
173
|
+
.outline:hover:not(:disabled) {
|
|
174
|
+
background: var(--hover);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/* ghost — 무게 최소, hover에만 표면 */
|
|
178
|
+
.ghost {
|
|
179
|
+
background: transparent;
|
|
180
|
+
color: var(--fg);
|
|
181
|
+
border-color: transparent;
|
|
182
|
+
}
|
|
183
|
+
.ghost:hover:not(:disabled) {
|
|
184
|
+
background: var(--hover);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/* destructive — 위험 액션. 평상시 destructive 8% 알파 + destructive 텍스트,
|
|
188
|
+
hover 시 solid 빨강 배경 + fg로 전환 (진짜 위험함을 강조). */
|
|
189
|
+
.destructive {
|
|
190
|
+
background: var(--destructive-subtle);
|
|
191
|
+
color: var(--destructive);
|
|
192
|
+
border-color: transparent;
|
|
193
|
+
}
|
|
194
|
+
.destructive:hover:not(:disabled) {
|
|
195
|
+
background: var(--destructive);
|
|
196
|
+
color: var(--destructive-fg);
|
|
197
|
+
border-color: var(--destructive);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/* destructive-ghost — 평상시 fg 텍스트(위험 아님을 감춤),
|
|
201
|
+
hover 시 destructive 톤(8% 배경 + destructive 텍스트)으로 전환. */
|
|
202
|
+
.destructiveGhost {
|
|
203
|
+
background: transparent;
|
|
204
|
+
color: var(--fg);
|
|
205
|
+
border-color: transparent;
|
|
206
|
+
}
|
|
207
|
+
.destructiveGhost:hover:not(:disabled) {
|
|
208
|
+
background: var(--destructive-subtle);
|
|
209
|
+
color: var(--destructive);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/* Loading spinner slot ───────────────────────────────────────── */
|
|
213
|
+
.spinner {
|
|
214
|
+
display: inline-flex;
|
|
215
|
+
animation: m-spin var(--duration-slower) var(--ease-linear) infinite;
|
|
216
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
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
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
12
|
+
|
|
13
|
+
import styles from "./Button.module.css";
|
|
14
|
+
|
|
15
|
+
export type ButtonVariant =
|
|
16
|
+
| "primary"
|
|
17
|
+
| "inverse"
|
|
18
|
+
| "secondary"
|
|
19
|
+
| "outline"
|
|
20
|
+
| "ghost"
|
|
21
|
+
| "destructive"
|
|
22
|
+
| "destructive-ghost";
|
|
23
|
+
|
|
24
|
+
export type ButtonSize = "xs" | "sm" | "md" | "lg";
|
|
25
|
+
|
|
26
|
+
export type ButtonProps = Omit<
|
|
27
|
+
ButtonHTMLAttributes<HTMLButtonElement>,
|
|
28
|
+
"size"
|
|
29
|
+
> & {
|
|
30
|
+
size?: ButtonSize;
|
|
31
|
+
variant?: ButtonVariant;
|
|
32
|
+
fullWidth?: boolean;
|
|
33
|
+
loading?: boolean;
|
|
34
|
+
leftIcon?: ReactNode;
|
|
35
|
+
rightIcon?: ReactNode;
|
|
36
|
+
/** 라벨 오른쪽 트레일링 슬롯에 붙는 카운트·상태 뱃지.
|
|
37
|
+
* rightIcon과 공존 시 badge → rightIcon 순으로 배치. */
|
|
38
|
+
badge?: ReactNode;
|
|
39
|
+
asChild?: boolean;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const variantClass: Record<ButtonVariant, string> = {
|
|
43
|
+
primary: styles.primary,
|
|
44
|
+
inverse: styles.inverse,
|
|
45
|
+
secondary: styles.secondary,
|
|
46
|
+
outline: styles.outline,
|
|
47
|
+
ghost: styles.ghost,
|
|
48
|
+
destructive: styles.destructive,
|
|
49
|
+
"destructive-ghost": styles.destructiveGhost,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const sizeClass: Record<ButtonSize, string> = {
|
|
53
|
+
xs: styles.xs,
|
|
54
|
+
sm: styles.sm,
|
|
55
|
+
md: styles.md,
|
|
56
|
+
lg: styles.lg,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const ICON_SIZE: Record<ButtonSize, number> = {
|
|
60
|
+
xs: 12,
|
|
61
|
+
sm: 16,
|
|
62
|
+
md: 16,
|
|
63
|
+
lg: 20,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
function sizedIcon(node: ReactNode, size: number): ReactNode {
|
|
67
|
+
if (!isValidElement(node)) return node;
|
|
68
|
+
const el = node as ReactElement<{ size?: number | string }>;
|
|
69
|
+
if (el.props.size !== undefined) return el;
|
|
70
|
+
return cloneElement(el, { size });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
|
74
|
+
(
|
|
75
|
+
{
|
|
76
|
+
className,
|
|
77
|
+
variant = "primary",
|
|
78
|
+
size = "md",
|
|
79
|
+
fullWidth = false,
|
|
80
|
+
loading = false,
|
|
81
|
+
leftIcon,
|
|
82
|
+
rightIcon,
|
|
83
|
+
badge,
|
|
84
|
+
asChild = false,
|
|
85
|
+
disabled,
|
|
86
|
+
type,
|
|
87
|
+
children,
|
|
88
|
+
...rest
|
|
89
|
+
},
|
|
90
|
+
ref,
|
|
91
|
+
) => {
|
|
92
|
+
const Comp = asChild ? Slot : "button";
|
|
93
|
+
|
|
94
|
+
const rootClass = [
|
|
95
|
+
styles.root,
|
|
96
|
+
variantClass[variant],
|
|
97
|
+
sizeClass[size],
|
|
98
|
+
fullWidth ? styles.fullWidth : "",
|
|
99
|
+
className ?? "",
|
|
100
|
+
]
|
|
101
|
+
.filter(Boolean)
|
|
102
|
+
.join(" ");
|
|
103
|
+
|
|
104
|
+
/* asChild 모드: Slot은 자식이 정확히 하나여야 한다. leftIcon/label/badge/rightIcon을
|
|
105
|
+
모두 넘기면 자식이 여러 개가 되어 터지므로, asChild일 때는 children을 그대로
|
|
106
|
+
Slot에 넘기고(= 소비 측 anchor/div 하나) icon·badge prop은 무시. 아이콘은 소비
|
|
107
|
+
측에서 anchor 내부에 직접 꽂아 single-child 조건을 지키도록 설계. */
|
|
108
|
+
if (asChild) {
|
|
109
|
+
return (
|
|
110
|
+
<Slot
|
|
111
|
+
ref={ref}
|
|
112
|
+
className={rootClass}
|
|
113
|
+
data-loading={loading || undefined}
|
|
114
|
+
{...rest}
|
|
115
|
+
>
|
|
116
|
+
{children}
|
|
117
|
+
</Slot>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<Comp
|
|
123
|
+
ref={ref}
|
|
124
|
+
className={rootClass}
|
|
125
|
+
type={type ?? "button"}
|
|
126
|
+
disabled={disabled || loading}
|
|
127
|
+
data-loading={loading || undefined}
|
|
128
|
+
{...rest}
|
|
129
|
+
>
|
|
130
|
+
{leftIcon && (
|
|
131
|
+
<span className={styles.icon} aria-hidden>
|
|
132
|
+
{sizedIcon(leftIcon, ICON_SIZE[size])}
|
|
133
|
+
</span>
|
|
134
|
+
)}
|
|
135
|
+
<span className={styles.label}>{children}</span>
|
|
136
|
+
{badge && <span className={styles.badge}>{badge}</span>}
|
|
137
|
+
{rightIcon && (
|
|
138
|
+
<span className={styles.icon} aria-hidden>
|
|
139
|
+
{sizedIcon(rightIcon, ICON_SIZE[size])}
|
|
140
|
+
</span>
|
|
141
|
+
)}
|
|
142
|
+
</Comp>
|
|
143
|
+
);
|
|
144
|
+
},
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
Button.displayName = "Button";
|
|
148
|
+
|
|
149
|
+
export { Button };
|