@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,80 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { forwardRef, type HTMLAttributes, type ReactNode } from "react";
|
|
4
|
+
|
|
5
|
+
import { Icon, type IconName } from "../Icon";
|
|
6
|
+
|
|
7
|
+
import styles from "./EmptyState.module.css";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* EmptyState — 리스트·테이블·대시보드 영역이 비었을 때 그 영역 전체를 채우는 자리채움.
|
|
11
|
+
* 세로 중앙정렬: 시각 요소(illustration ∨ icon) → 타이틀 → 설명 → 액션.
|
|
12
|
+
*
|
|
13
|
+
* 배치 규칙:
|
|
14
|
+
* - size는 **컨테이너 폭/영역 크기**에 맞춰 선택 (sm/md/lg)
|
|
15
|
+
* - illustration이 있으면 icon은 무시 (병치 금지)
|
|
16
|
+
* - 액션은 Button slot. primary 1 + ghost 1 패턴, destructive 금지
|
|
17
|
+
*/
|
|
18
|
+
export type EmptyStateSize = "sm" | "md";
|
|
19
|
+
|
|
20
|
+
export interface EmptyStateProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
|
|
21
|
+
size?: EmptyStateSize;
|
|
22
|
+
/** 큰 3D 일러스트 등 ReactNode. 지정되면 icon보다 우선. */
|
|
23
|
+
illustration?: ReactNode;
|
|
24
|
+
/** illustration이 없을 때만 렌더되는 Icon slot. */
|
|
25
|
+
icon?: IconName;
|
|
26
|
+
title: ReactNode;
|
|
27
|
+
description?: ReactNode;
|
|
28
|
+
/** Button 1~2개. primary 1 + ghost 1 패턴 권장. */
|
|
29
|
+
actions?: ReactNode;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const sizeClass: Record<EmptyStateSize, string> = {
|
|
33
|
+
sm: styles.sizeSm,
|
|
34
|
+
md: styles.sizeMd,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const EmptyState = forwardRef<HTMLDivElement, EmptyStateProps>(
|
|
38
|
+
(
|
|
39
|
+
{
|
|
40
|
+
className,
|
|
41
|
+
size = "sm",
|
|
42
|
+
illustration,
|
|
43
|
+
icon,
|
|
44
|
+
title,
|
|
45
|
+
description,
|
|
46
|
+
actions,
|
|
47
|
+
...rest
|
|
48
|
+
},
|
|
49
|
+
ref,
|
|
50
|
+
) => {
|
|
51
|
+
const rootClass = [styles.root, sizeClass[size], className ?? ""]
|
|
52
|
+
.filter(Boolean)
|
|
53
|
+
.join(" ");
|
|
54
|
+
|
|
55
|
+
const visual = illustration ? (
|
|
56
|
+
<div className={styles.illustration} aria-hidden>
|
|
57
|
+
{illustration}
|
|
58
|
+
</div>
|
|
59
|
+
) : icon ? (
|
|
60
|
+
<span className={styles.icon} aria-hidden>
|
|
61
|
+
<Icon name={icon} />
|
|
62
|
+
</span>
|
|
63
|
+
) : null;
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<div ref={ref} className={rootClass} {...rest}>
|
|
67
|
+
{visual}
|
|
68
|
+
<div className={styles.text}>
|
|
69
|
+
<div className={styles.title}>{title}</div>
|
|
70
|
+
{description && <div className={styles.description}>{description}</div>}
|
|
71
|
+
</div>
|
|
72
|
+
{actions && <div className={styles.actions}>{actions}</div>}
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
},
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
EmptyState.displayName = "EmptyState";
|
|
79
|
+
|
|
80
|
+
export { EmptyState };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/* FileIcon — 확장자별 색/라벨/아이콘을 담는 둥근 사각 뱃지.
|
|
2
|
+
비율:
|
|
3
|
+
radius: Shape 축 반응. sm(16px)만 --radius-checkbox로 캡해 원 방지.
|
|
4
|
+
md(24) → --radius-xs, lg(32) → --radius-sm
|
|
5
|
+
label font / size = 0.53 — semibold
|
|
6
|
+
glyph / size = 0.60 — 브랜드 아이콘 자산 가독성
|
|
7
|
+
사이즈 리터럴은 FileIcon 고유 치수라 전역 토큰 없음 — 라인별 ignore. */
|
|
8
|
+
|
|
9
|
+
.root {
|
|
10
|
+
--fi-size: 24px; /* lint-hardcoded: ignore */
|
|
11
|
+
--fi-radius: var(--radius-xs);
|
|
12
|
+
|
|
13
|
+
display: inline-flex;
|
|
14
|
+
align-items: center;
|
|
15
|
+
justify-content: center;
|
|
16
|
+
width: var(--fi-size);
|
|
17
|
+
height: var(--fi-size);
|
|
18
|
+
border-radius: var(--fi-radius);
|
|
19
|
+
flex: 0 0 auto;
|
|
20
|
+
overflow: hidden;
|
|
21
|
+
user-select: none;
|
|
22
|
+
transition: var(--transition-colors);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/* 사이즈 — Shape 축 반응하되 soft 과도 방지로 한 단계 내려 사용.
|
|
26
|
+
sm/md: --radius-checkbox (soft에서 6px로 캡)
|
|
27
|
+
lg: --radius-xs */
|
|
28
|
+
.sizeSm {
|
|
29
|
+
--fi-size: 16px; /* lint-hardcoded: ignore */
|
|
30
|
+
--fi-radius: var(--radius-checkbox);
|
|
31
|
+
}
|
|
32
|
+
.sizeMd {
|
|
33
|
+
--fi-size: 24px; /* lint-hardcoded: ignore */
|
|
34
|
+
--fi-radius: var(--radius-checkbox);
|
|
35
|
+
}
|
|
36
|
+
.sizeLg {
|
|
37
|
+
--fi-size: 32px; /* lint-hardcoded: ignore */
|
|
38
|
+
--fi-radius: var(--radius-xs);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/* 브랜드 라벨 — "Ai"/"Ps"/"X"/… 2글자 */
|
|
42
|
+
.label {
|
|
43
|
+
font-size: calc(var(--fi-size) * 0.53);
|
|
44
|
+
font-weight: var(--font-weight-semibold);
|
|
45
|
+
line-height: 1;
|
|
46
|
+
font-family: var(--font-sans);
|
|
47
|
+
/* 2글자 뭉침 방지용 negative tracking. uppercase 아님 — 혼합 대소문자 원본 유지. */
|
|
48
|
+
letter-spacing: -0.02em; /* lint-hardcoded: ignore */
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/* 제너릭 glyph (phosphor) — size × 0.60 */
|
|
52
|
+
.glyph {
|
|
53
|
+
width: calc(var(--fi-size) * 0.60);
|
|
54
|
+
height: calc(var(--fi-size) * 0.60);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* 제너릭 L/D 자동 전환 — CSS 변수를 Mode 축에서 선택 */
|
|
58
|
+
.generic {
|
|
59
|
+
background: var(--fi-bg-light);
|
|
60
|
+
color: var(--fi-fg-light);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
:global([data-mode="dark"]) .generic {
|
|
64
|
+
background: var(--fi-bg-dark);
|
|
65
|
+
color: var(--fi-fg-dark);
|
|
66
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { HTMLAttributes } from "react";
|
|
4
|
+
import {
|
|
5
|
+
File as PhFile,
|
|
6
|
+
FileZip as PhFileZip,
|
|
7
|
+
MusicNote as PhMusicNote,
|
|
8
|
+
} from "@phosphor-icons/react";
|
|
9
|
+
import styles from "./FileIcon.module.css";
|
|
10
|
+
import { resolveFileIcon } from "./fileExtMap";
|
|
11
|
+
|
|
12
|
+
export type FileIconSize = "sm" | "md" | "lg";
|
|
13
|
+
|
|
14
|
+
export interface FileIconProps extends HTMLAttributes<HTMLSpanElement> {
|
|
15
|
+
/** 확장자 또는 파일명. 'psd' / '.psd' / 'report.psd' 다 허용. */
|
|
16
|
+
ext: string;
|
|
17
|
+
/** 16 / 24 / 32. 기본 md=24. */
|
|
18
|
+
size?: FileIconSize;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const sizeClass: Record<FileIconSize, string> = {
|
|
22
|
+
sm: styles.sizeSm,
|
|
23
|
+
md: styles.sizeMd,
|
|
24
|
+
lg: styles.sizeLg,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const PHOSPHOR = {
|
|
28
|
+
file: PhFile,
|
|
29
|
+
"file-zip": PhFileZip,
|
|
30
|
+
"music-note": PhMusicNote,
|
|
31
|
+
} as const;
|
|
32
|
+
|
|
33
|
+
export function FileIcon({
|
|
34
|
+
ext,
|
|
35
|
+
size = "md",
|
|
36
|
+
className,
|
|
37
|
+
style,
|
|
38
|
+
...rest
|
|
39
|
+
}: FileIconProps) {
|
|
40
|
+
const spec = resolveFileIcon(ext);
|
|
41
|
+
|
|
42
|
+
const root = [styles.root, sizeClass[size], className]
|
|
43
|
+
.filter(Boolean)
|
|
44
|
+
.join(" ");
|
|
45
|
+
|
|
46
|
+
if (spec.kind === "brand") {
|
|
47
|
+
return (
|
|
48
|
+
<span
|
|
49
|
+
className={root}
|
|
50
|
+
style={{
|
|
51
|
+
background: spec.bg,
|
|
52
|
+
color: spec.fg,
|
|
53
|
+
...style,
|
|
54
|
+
}}
|
|
55
|
+
data-kind="brand"
|
|
56
|
+
aria-hidden
|
|
57
|
+
{...rest}
|
|
58
|
+
>
|
|
59
|
+
<span className={styles.label}>{spec.label}</span>
|
|
60
|
+
</span>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 제너릭 — L/D pair. CSS 변수로 내보내서 data-mode에 CSS가 반응.
|
|
65
|
+
const Ph = PHOSPHOR[spec.icon];
|
|
66
|
+
const vars = {
|
|
67
|
+
"--fi-bg-light": spec.light.bg,
|
|
68
|
+
"--fi-fg-light": spec.light.fg,
|
|
69
|
+
"--fi-bg-dark": spec.dark.bg,
|
|
70
|
+
"--fi-fg-dark": spec.dark.fg,
|
|
71
|
+
} as React.CSSProperties;
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<span
|
|
75
|
+
className={`${root} ${styles.generic}`}
|
|
76
|
+
style={{ ...vars, ...style }}
|
|
77
|
+
data-kind="generic"
|
|
78
|
+
aria-hidden
|
|
79
|
+
{...rest}
|
|
80
|
+
>
|
|
81
|
+
<Ph className={styles.glyph} weight="regular" />
|
|
82
|
+
</span>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 파일 확장자 → FileIcon 렌더 정보 매핑.
|
|
3
|
+
*
|
|
4
|
+
* 두 종류 — "브랜드"와 "제너릭".
|
|
5
|
+
* - 브랜드(ai/psd/xls/ppt/doc/pdf): 단일 색 + 2글자 라벨. Mode 축 무관하게 고정.
|
|
6
|
+
* - 제너릭(others/zip/mp3): L/D 두 색 pair + 아이콘. Mode 축에 자동 반응.
|
|
7
|
+
*
|
|
8
|
+
* 색은 레퍼런스 디자인(.refs/chat/file-icon-ref.png)에서 직접 추출한 hex.
|
|
9
|
+
* CTDS 하드코딩 금지 원칙의 명시적 예외 — 파일 포맷 식별은 브랜드 자산 성격이라
|
|
10
|
+
* 토큰으로 추상화하면 의미를 잃는다. [[feedback-no-hardcoded-values]]의 예외
|
|
11
|
+
* 범주이며, lint baseline에 등록해 통과시킨다.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export type FileIconKind = "brand" | "generic";
|
|
15
|
+
|
|
16
|
+
/** 브랜드형 — bg·fg 고정, 라벨은 2글자. */
|
|
17
|
+
export interface BrandSpec {
|
|
18
|
+
kind: "brand";
|
|
19
|
+
bg: string;
|
|
20
|
+
fg: string;
|
|
21
|
+
label: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** 제너릭형 — L/D pair, phosphor 아이콘. */
|
|
25
|
+
export interface GenericSpec {
|
|
26
|
+
kind: "generic";
|
|
27
|
+
light: { bg: string; fg: string };
|
|
28
|
+
dark: { bg: string; fg: string };
|
|
29
|
+
icon: "file" | "file-zip" | "music-note";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type FileIconSpec = BrandSpec | GenericSpec;
|
|
33
|
+
|
|
34
|
+
/** 제너릭 키들 (ext → group) */
|
|
35
|
+
const GENERIC_GROUP: Record<string, "zip" | "mp3" | "others"> = {
|
|
36
|
+
zip: "zip",
|
|
37
|
+
rar: "zip",
|
|
38
|
+
"7z": "zip",
|
|
39
|
+
tar: "zip",
|
|
40
|
+
gz: "zip",
|
|
41
|
+
|
|
42
|
+
mp3: "mp3",
|
|
43
|
+
wav: "mp3",
|
|
44
|
+
flac: "mp3",
|
|
45
|
+
m4a: "mp3",
|
|
46
|
+
aac: "mp3",
|
|
47
|
+
ogg: "mp3",
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** 그룹별 제너릭 스펙 */
|
|
51
|
+
const GENERIC_SPEC: Record<"zip" | "mp3" | "others", GenericSpec> = {
|
|
52
|
+
others: {
|
|
53
|
+
kind: "generic",
|
|
54
|
+
light: { bg: "#EDEAB0", fg: "#B2AA54" },
|
|
55
|
+
dark: { bg: "#363229", fg: "#A19B5A" },
|
|
56
|
+
icon: "file",
|
|
57
|
+
},
|
|
58
|
+
zip: {
|
|
59
|
+
kind: "generic",
|
|
60
|
+
light: { bg: "#EDEAB0", fg: "#B2AA54" },
|
|
61
|
+
dark: { bg: "#363229", fg: "#A19B59" },
|
|
62
|
+
icon: "file-zip",
|
|
63
|
+
},
|
|
64
|
+
mp3: {
|
|
65
|
+
kind: "generic",
|
|
66
|
+
light: { bg: "#BEFDA8", fg: "#4B9C2D" },
|
|
67
|
+
dark: { bg: "#254924", fg: "#BDFCA8" },
|
|
68
|
+
icon: "music-note",
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/** 브랜드 파일 — 확장자별 단일 스펙 */
|
|
73
|
+
const BRAND_SPEC: Record<string, BrandSpec> = {
|
|
74
|
+
ai: { kind: "brand", bg: "#2E0402", fg: "#F09E38", label: "Ai" },
|
|
75
|
+
psd: { kind: "brand", bg: "#081D34", fg: "#57A5F8", label: "Ps" },
|
|
76
|
+
xls: { kind: "brand", bg: "#3B804C", fg: "#FFFFFF", label: "X" },
|
|
77
|
+
xlsx: { kind: "brand", bg: "#3B804C", fg: "#FFFFFF", label: "X" },
|
|
78
|
+
ppt: { kind: "brand", bg: "#B84C2C", fg: "#FFFFFF", label: "P" },
|
|
79
|
+
pptx: { kind: "brand", bg: "#B84C2C", fg: "#FFFFFF", label: "P" },
|
|
80
|
+
doc: { kind: "brand", bg: "#3044C7", fg: "#FFFFFF", label: "W" },
|
|
81
|
+
docx: { kind: "brand", bg: "#3044C7", fg: "#FFFFFF", label: "W" },
|
|
82
|
+
pdf: { kind: "brand", bg: "#EA336C", fg: "#FFFFFF", label: "Pd" },
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 확장자 정규화 — 선행 점·대문자 모두 허용.
|
|
87
|
+
* 공백·null 안전.
|
|
88
|
+
*/
|
|
89
|
+
export function normalizeExt(ext: string | null | undefined): string {
|
|
90
|
+
if (!ext) return "";
|
|
91
|
+
return ext.trim().replace(/^\./, "").toLowerCase();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** 파일명(또는 확장자)에서 FileIcon 스펙 조회. */
|
|
95
|
+
export function resolveFileIcon(input: string): FileIconSpec {
|
|
96
|
+
const ext = normalizeExt(
|
|
97
|
+
input.includes(".") ? input.split(".").pop() ?? "" : input
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
if (ext in BRAND_SPEC) return BRAND_SPEC[ext];
|
|
101
|
+
|
|
102
|
+
const group = GENERIC_GROUP[ext] ?? "others";
|
|
103
|
+
return GENERIC_SPEC[group];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** 데모·테스트에서 전체 리스트 돌 때 쓰려고 export. */
|
|
107
|
+
export const KNOWN_BRAND_EXTS = Object.keys(BRAND_SPEC);
|
|
108
|
+
export const KNOWN_GENERIC_GROUPS = Object.keys(GENERIC_SPEC) as (
|
|
109
|
+
| "others"
|
|
110
|
+
| "zip"
|
|
111
|
+
| "mp3"
|
|
112
|
+
)[];
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { FileIcon } from "./FileIcon";
|
|
2
|
+
export type { FileIconProps, FileIconSize } from "./FileIcon";
|
|
3
|
+
export {
|
|
4
|
+
resolveFileIcon,
|
|
5
|
+
normalizeExt,
|
|
6
|
+
KNOWN_BRAND_EXTS,
|
|
7
|
+
KNOWN_GENERIC_GROUPS,
|
|
8
|
+
} from "./fileExtMap";
|
|
9
|
+
export type { FileIconSpec, BrandSpec, GenericSpec } from "./fileExtMap";
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/* FilterButton ────────────────────────────────────────────────────
|
|
2
|
+
토글 상태를 가진 "필터 선택" 컨트롤.
|
|
3
|
+
기본은 outline(회색 선 + fg-muted 텍스트), selected=true면 반전(fg 배경 + bg 텍스트).
|
|
4
|
+
Button과 시각적으로 형제이지만 시맨틱이 다름(액션 실행 vs 선택 상태 유지).
|
|
5
|
+
aria-pressed로 스크린리더에 토글 상태 전달. */
|
|
6
|
+
.root {
|
|
7
|
+
display: inline-flex;
|
|
8
|
+
align-items: center;
|
|
9
|
+
justify-content: center;
|
|
10
|
+
gap: var(--space-1_5);
|
|
11
|
+
font-family: var(--label-font);
|
|
12
|
+
font-weight: var(--label-weight);
|
|
13
|
+
line-height: var(--lh-tight);
|
|
14
|
+
letter-spacing: var(--label-tracking);
|
|
15
|
+
white-space: nowrap;
|
|
16
|
+
cursor: pointer;
|
|
17
|
+
user-select: none;
|
|
18
|
+
background: transparent;
|
|
19
|
+
color: var(--fg);
|
|
20
|
+
border: 1px solid var(--border-strong);
|
|
21
|
+
/* selected 토글은 fg↔bg 반전이라 background/color를 보간하면 중간에 반드시
|
|
22
|
+
저대비 구간(반투명 검정 위 회색 글자)을 지나 글자가 사라져 보임.
|
|
23
|
+
→ 색은 즉시 스위칭, motion은 border/shadow/transform에만. */
|
|
24
|
+
transition:
|
|
25
|
+
border-color var(--duration-fast) var(--ease-out),
|
|
26
|
+
box-shadow var(--duration-fast) var(--ease-out),
|
|
27
|
+
transform var(--duration-base) var(--ease-out);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.root:focus-visible {
|
|
31
|
+
outline: none;
|
|
32
|
+
box-shadow: var(--ring-shadow);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.root:hover:not(:disabled) {
|
|
36
|
+
background: var(--hover);
|
|
37
|
+
color: var(--fg);
|
|
38
|
+
border-color: var(--fg-muted);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.root:active:not(:disabled) {
|
|
42
|
+
transform: translateY(var(--space-0_5));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.root:disabled {
|
|
46
|
+
cursor: not-allowed;
|
|
47
|
+
opacity: 0.5;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/* Selected — inverse 스타일 반전. Hue 축과 무관하게 흑백 대비로 강조.
|
|
51
|
+
필터가 "적용 중"임을 팔레트와 상관없이 명확히 알림. */
|
|
52
|
+
.root[aria-pressed="true"] {
|
|
53
|
+
background: var(--fg);
|
|
54
|
+
color: var(--bg);
|
|
55
|
+
border-color: var(--fg);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* selected hover는 unselected hover의 color/border까지 명시적으로 덮어써야 함 —
|
|
59
|
+
빼먹으면 검정 배경 위 검정 글자로 대비가 무너짐. */
|
|
60
|
+
.root[aria-pressed="true"]:hover:not(:disabled) {
|
|
61
|
+
background: var(--fg-strong);
|
|
62
|
+
color: var(--bg);
|
|
63
|
+
border-color: var(--fg-strong);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* Sizes — Button과 동일 스케일(control-h-*)로 나란히 놓아도 정렬 맞음.
|
|
67
|
+
Shape 축이 radius를 스왑. */
|
|
68
|
+
.sm {
|
|
69
|
+
height: var(--control-h-sm);
|
|
70
|
+
padding-inline: var(--space-2);
|
|
71
|
+
gap: var(--space-1);
|
|
72
|
+
font-size: var(--label-sm-size);
|
|
73
|
+
letter-spacing: var(--label-tracking);
|
|
74
|
+
border-radius: var(--radius-xs);
|
|
75
|
+
--chip-icon-size: var(--space-4);
|
|
76
|
+
}
|
|
77
|
+
.md {
|
|
78
|
+
height: var(--control-h-md);
|
|
79
|
+
padding-inline: var(--space-2_5);
|
|
80
|
+
gap: var(--space-1_5);
|
|
81
|
+
font-size: var(--label-md-size);
|
|
82
|
+
letter-spacing: var(--label-tracking);
|
|
83
|
+
border-radius: var(--radius-sm);
|
|
84
|
+
--chip-icon-size: var(--space-4);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/* Icon slot — Button과 동일 방식(정사각 슬롯, opacity로 톤 조절). */
|
|
88
|
+
.icon {
|
|
89
|
+
display: inline-flex;
|
|
90
|
+
align-items: center;
|
|
91
|
+
justify-content: center;
|
|
92
|
+
flex-shrink: 0;
|
|
93
|
+
width: var(--chip-icon-size);
|
|
94
|
+
height: var(--chip-icon-size);
|
|
95
|
+
opacity: 0.75;
|
|
96
|
+
}
|
|
97
|
+
.icon > svg {
|
|
98
|
+
width: 100%;
|
|
99
|
+
height: 100%;
|
|
100
|
+
display: block;
|
|
101
|
+
}
|
|
102
|
+
.root:hover:not(:disabled) .icon,
|
|
103
|
+
.root[aria-pressed="true"] .icon {
|
|
104
|
+
opacity: 1;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.label {
|
|
108
|
+
display: inline-flex;
|
|
109
|
+
align-items: center;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/* Count — 중립 그레이 pill. selected 시 반전 바탕 위라 bg 배경으로 다시 떠올림. */
|
|
113
|
+
.count {
|
|
114
|
+
display: inline-flex;
|
|
115
|
+
align-items: center;
|
|
116
|
+
justify-content: center;
|
|
117
|
+
min-width: var(--space-4);
|
|
118
|
+
padding: 0 var(--space-1);
|
|
119
|
+
height: var(--space-4);
|
|
120
|
+
border-radius: var(--radius-xs);
|
|
121
|
+
background: var(--bg-subtle);
|
|
122
|
+
color: var(--fg-muted);
|
|
123
|
+
font-size: var(--text-xs);
|
|
124
|
+
font-variant-numeric: tabular-nums;
|
|
125
|
+
line-height: 1;
|
|
126
|
+
}
|
|
127
|
+
.root[aria-pressed="true"] .count {
|
|
128
|
+
background: var(--bg);
|
|
129
|
+
color: var(--fg-muted);
|
|
130
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
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 styles from "./FilterButton.module.css";
|
|
13
|
+
|
|
14
|
+
export type FilterButtonSize = "sm" | "md";
|
|
15
|
+
|
|
16
|
+
export type FilterButtonProps = Omit<
|
|
17
|
+
ButtonHTMLAttributes<HTMLButtonElement>,
|
|
18
|
+
"size"
|
|
19
|
+
> & {
|
|
20
|
+
size?: FilterButtonSize;
|
|
21
|
+
selected?: boolean;
|
|
22
|
+
onToggle?: (next: boolean) => void;
|
|
23
|
+
leftIcon?: ReactNode;
|
|
24
|
+
count?: number;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const sizeClass: Record<FilterButtonSize, string> = {
|
|
28
|
+
sm: styles.sm,
|
|
29
|
+
md: styles.md,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const ICON_SIZE: Record<FilterButtonSize, number> = {
|
|
33
|
+
sm: 14,
|
|
34
|
+
md: 16,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function sizedIcon(node: ReactNode, size: number): ReactNode {
|
|
38
|
+
if (!isValidElement(node)) return node;
|
|
39
|
+
const el = node as ReactElement<{ size?: number | string }>;
|
|
40
|
+
if (el.props.size !== undefined) return el;
|
|
41
|
+
return cloneElement(el, { size });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const FilterButton = forwardRef<HTMLButtonElement, FilterButtonProps>(
|
|
45
|
+
(
|
|
46
|
+
{
|
|
47
|
+
className,
|
|
48
|
+
size = "md",
|
|
49
|
+
selected = false,
|
|
50
|
+
onToggle,
|
|
51
|
+
onClick,
|
|
52
|
+
leftIcon,
|
|
53
|
+
count,
|
|
54
|
+
type,
|
|
55
|
+
children,
|
|
56
|
+
...rest
|
|
57
|
+
},
|
|
58
|
+
ref,
|
|
59
|
+
) => {
|
|
60
|
+
const rootClass = [styles.root, sizeClass[size], className ?? ""]
|
|
61
|
+
.filter(Boolean)
|
|
62
|
+
.join(" ");
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<button
|
|
66
|
+
ref={ref}
|
|
67
|
+
type={type ?? "button"}
|
|
68
|
+
className={rootClass}
|
|
69
|
+
onClick={(e) => {
|
|
70
|
+
onClick?.(e);
|
|
71
|
+
if (!e.defaultPrevented) onToggle?.(!selected);
|
|
72
|
+
}}
|
|
73
|
+
aria-pressed={selected}
|
|
74
|
+
{...rest}
|
|
75
|
+
>
|
|
76
|
+
{leftIcon && (
|
|
77
|
+
<span className={styles.icon} aria-hidden>
|
|
78
|
+
{sizedIcon(leftIcon, ICON_SIZE[size])}
|
|
79
|
+
</span>
|
|
80
|
+
)}
|
|
81
|
+
{children && <span className={styles.label}>{children}</span>}
|
|
82
|
+
{count !== undefined && (
|
|
83
|
+
<span className={styles.count} aria-hidden>
|
|
84
|
+
{count}
|
|
85
|
+
</span>
|
|
86
|
+
)}
|
|
87
|
+
</button>
|
|
88
|
+
);
|
|
89
|
+
},
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
FilterButton.displayName = "FilterButton";
|
|
93
|
+
|
|
94
|
+
export { FilterButton };
|