@conscience-technology/ctds 0.0.0-stage → 0.1.0
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 +241 -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,91 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { CSSProperties, HTMLAttributes, MouseEvent } from "react";
|
|
4
|
+
import { ImageSquare as PhImageSquare, X as PhX } from "@phosphor-icons/react";
|
|
5
|
+
import styles from "./AttachmentThumb.module.css";
|
|
6
|
+
|
|
7
|
+
export type AttachmentThumbSize = "sm" | "md" | "lg";
|
|
8
|
+
|
|
9
|
+
export interface AttachmentThumbProps
|
|
10
|
+
extends Omit<HTMLAttributes<HTMLSpanElement>, "children" | "onError"> {
|
|
11
|
+
/** 이미지 소스. 없으면 image glyph placeholder (블루톤 L/D pair). */
|
|
12
|
+
src?: string;
|
|
13
|
+
alt?: string;
|
|
14
|
+
/** sm=32 / md=40 / lg=56. 기본 md. */
|
|
15
|
+
size?: AttachmentThumbSize;
|
|
16
|
+
/** 로딩 중 pulse. src 디코딩 전·업로드 처리 중. */
|
|
17
|
+
loading?: boolean;
|
|
18
|
+
/** 지정 시 hover에서 코너에 X 노출. */
|
|
19
|
+
onRemove?: (e: MouseEvent<HTMLButtonElement>) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const sizeClass: Record<AttachmentThumbSize, string> = {
|
|
23
|
+
sm: styles.sizeSm,
|
|
24
|
+
md: styles.sizeMd,
|
|
25
|
+
lg: styles.sizeLg,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/* placeholder 블루톤 L/D pair — FileIcon 제너릭 그룹과 같은 패턴.
|
|
29
|
+
브랜드 자산 범주 — CSS 토큰으로 추상화 X. inline style로 주입해 CSS 린터 범위 밖. */
|
|
30
|
+
const PLACEHOLDER_VARS = {
|
|
31
|
+
"--at-ph-bg-light": "#D5E8FD",
|
|
32
|
+
"--at-ph-fg-light": "#2D7FD3",
|
|
33
|
+
"--at-ph-bg-dark": "#1E3A5F",
|
|
34
|
+
"--at-ph-fg-dark": "#C5DFF8",
|
|
35
|
+
} as CSSProperties;
|
|
36
|
+
|
|
37
|
+
export function AttachmentThumb({
|
|
38
|
+
src,
|
|
39
|
+
alt = "",
|
|
40
|
+
size = "md",
|
|
41
|
+
loading = false,
|
|
42
|
+
onRemove,
|
|
43
|
+
className,
|
|
44
|
+
style,
|
|
45
|
+
...rest
|
|
46
|
+
}: AttachmentThumbProps) {
|
|
47
|
+
const isPlaceholder = !loading && !src;
|
|
48
|
+
|
|
49
|
+
const cls = [
|
|
50
|
+
styles.root,
|
|
51
|
+
sizeClass[size],
|
|
52
|
+
loading
|
|
53
|
+
? styles.loading
|
|
54
|
+
: src
|
|
55
|
+
? styles.hasImage
|
|
56
|
+
: styles.placeholder,
|
|
57
|
+
className,
|
|
58
|
+
]
|
|
59
|
+
.filter(Boolean)
|
|
60
|
+
.join(" ");
|
|
61
|
+
|
|
62
|
+
const mergedStyle: CSSProperties = isPlaceholder
|
|
63
|
+
? { ...PLACEHOLDER_VARS, ...style }
|
|
64
|
+
: style ?? {};
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<span
|
|
68
|
+
className={cls}
|
|
69
|
+
style={mergedStyle}
|
|
70
|
+
data-removable={onRemove ? "" : undefined}
|
|
71
|
+
{...rest}
|
|
72
|
+
>
|
|
73
|
+
{loading ? null : src ? (
|
|
74
|
+
<img src={src} alt={alt} className={styles.img} />
|
|
75
|
+
) : (
|
|
76
|
+
<PhImageSquare className={styles.glyph} weight="regular" aria-hidden />
|
|
77
|
+
)}
|
|
78
|
+
|
|
79
|
+
{onRemove && !loading ? (
|
|
80
|
+
<button
|
|
81
|
+
type="button"
|
|
82
|
+
className={styles.removeBtn}
|
|
83
|
+
onClick={onRemove}
|
|
84
|
+
aria-label="첨부 제거"
|
|
85
|
+
>
|
|
86
|
+
<PhX className={styles.removeIcon} weight="bold" />
|
|
87
|
+
</button>
|
|
88
|
+
) : null}
|
|
89
|
+
</span>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/* Avatar ───────────────────────────────────────────────────────
|
|
2
|
+
사람/팀/봇을 시각화하는 라벨.
|
|
3
|
+
이미지 > 이니셜 > 아이콘 순으로 폴백(안쪽 내용만 바뀌고 컨테이너는 동일).
|
|
4
|
+
|
|
5
|
+
shape prop:
|
|
6
|
+
- circle(기본): 사람·개인 계정. 항상 --radius-full.
|
|
7
|
+
- rounded: 팀·조직·봇·서비스. size별로 radius 스왑(Shape 축을 밟음).
|
|
8
|
+
두 모양이 나란히 있으면 "사람 vs 아닌 것"이 한눈에 구분됨. */
|
|
9
|
+
.root {
|
|
10
|
+
display: inline-flex;
|
|
11
|
+
align-items: center;
|
|
12
|
+
justify-content: center;
|
|
13
|
+
flex-shrink: 0;
|
|
14
|
+
background: var(--_avatar-bg, var(--bg-muted));
|
|
15
|
+
color: var(--_avatar-fg, var(--fg));
|
|
16
|
+
font-family: var(--label-font);
|
|
17
|
+
font-weight: var(--font-weight-semibold);
|
|
18
|
+
line-height: 1;
|
|
19
|
+
letter-spacing: 0; /* 이니셜은 tracking 0 — narrow letter 조합에서 시각 균형 */
|
|
20
|
+
user-select: none;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/* Shape ─────────────────────────────────────────────────── */
|
|
24
|
+
.circle { border-radius: var(--radius-full); }
|
|
25
|
+
|
|
26
|
+
/* rounded는 size에 맞춰 radius 크기가 자연스럽게 커짐.
|
|
27
|
+
xs/sm은 컨테이너 자체가 작아 xs radius, md는 sm, lg는 md, xl은 lg. */
|
|
28
|
+
.rounded.xs { border-radius: var(--radius-xs); }
|
|
29
|
+
.rounded.sm { border-radius: var(--radius-sm); }
|
|
30
|
+
.rounded.md { border-radius: var(--radius-md); }
|
|
31
|
+
.rounded.lg { border-radius: var(--radius-lg); }
|
|
32
|
+
.rounded.xl { border-radius: var(--radius-xl); }
|
|
33
|
+
|
|
34
|
+
/* Sizes ────────────────────────────────────────────────────
|
|
35
|
+
space-* 스케일에 붙여 다른 컨트롤과 정렬 유지.
|
|
36
|
+
xs는 Chip 내부용, sm/md는 리스트/일반, lg는 프로필 헤더 등. */
|
|
37
|
+
/* 이니셜 폰트는 컨테이너 폭의 40% — 사이즈가 바뀌어도 시각 비율 유지. */
|
|
38
|
+
.xs {
|
|
39
|
+
width: var(--space-4); /* 16 */
|
|
40
|
+
height: var(--space-4);
|
|
41
|
+
font-size: calc(var(--space-4) * 0.5); /* xs만 최소 가독성 위해 살짝 크게 */
|
|
42
|
+
}
|
|
43
|
+
.sm {
|
|
44
|
+
width: var(--space-6); /* 24 */
|
|
45
|
+
height: var(--space-6);
|
|
46
|
+
font-size: calc(var(--space-6) * 0.48);
|
|
47
|
+
}
|
|
48
|
+
.md {
|
|
49
|
+
width: var(--space-8); /* 32 */
|
|
50
|
+
height: var(--space-8);
|
|
51
|
+
font-size: calc(var(--space-8) * 0.48);
|
|
52
|
+
}
|
|
53
|
+
.lg {
|
|
54
|
+
width: var(--space-12); /* 48 */
|
|
55
|
+
height: var(--space-12);
|
|
56
|
+
font-size: calc(var(--space-12) * 0.48);
|
|
57
|
+
}
|
|
58
|
+
.xl {
|
|
59
|
+
width: var(--space-24); /* 96 */
|
|
60
|
+
height: var(--space-24);
|
|
61
|
+
font-size: calc(var(--space-24) * 0.48);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* Content slots ──────────────────────────────────────────── */
|
|
65
|
+
.root {
|
|
66
|
+
position: relative;
|
|
67
|
+
overflow: visible; /* status 뱃지가 밖으로 삐져나올 수 있게 */
|
|
68
|
+
}
|
|
69
|
+
/* 내부 이미지 모서리는 shape를 따라야 하므로 안쪽 clip은 image에만 적용 */
|
|
70
|
+
.image {
|
|
71
|
+
position: absolute;
|
|
72
|
+
inset: 0;
|
|
73
|
+
width: 100%;
|
|
74
|
+
height: 100%;
|
|
75
|
+
object-fit: cover;
|
|
76
|
+
display: block;
|
|
77
|
+
opacity: 0;
|
|
78
|
+
border-radius: inherit; /* root의 shape를 그대로 따라감 */
|
|
79
|
+
}
|
|
80
|
+
.image[data-loaded="true"] { opacity: 1; }
|
|
81
|
+
.initials {
|
|
82
|
+
display: inline-flex;
|
|
83
|
+
align-items: center;
|
|
84
|
+
justify-content: center;
|
|
85
|
+
color: inherit;
|
|
86
|
+
}
|
|
87
|
+
.icon {
|
|
88
|
+
display: inline-flex;
|
|
89
|
+
align-items: center;
|
|
90
|
+
justify-content: center;
|
|
91
|
+
color: var(--fg-muted);
|
|
92
|
+
}
|
|
93
|
+
.icon > svg {
|
|
94
|
+
display: block;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/* Status badge ───────────────────────────────────────────────
|
|
98
|
+
접속 상태 표시용 작은 동그라미. 우하단 고정.
|
|
99
|
+
- online: --success
|
|
100
|
+
- offline: --fg-muted (톤 다운)
|
|
101
|
+
테두리는 --bg 색 ring으로, 아바타와 시각적으로 분리.
|
|
102
|
+
크기·offset은 각 size별 공간 토큰으로 매핑. */
|
|
103
|
+
.status {
|
|
104
|
+
position: absolute;
|
|
105
|
+
right: 0;
|
|
106
|
+
bottom: 0;
|
|
107
|
+
width: var(--_status-size, var(--space-2));
|
|
108
|
+
height: var(--_status-size, var(--space-2));
|
|
109
|
+
border-radius: var(--radius-full);
|
|
110
|
+
box-shadow: 0 0 0 var(--_status-ring, var(--space-0_5)) var(--bg);
|
|
111
|
+
pointer-events: none;
|
|
112
|
+
}
|
|
113
|
+
.status[data-status="online"] { background: var(--success); }
|
|
114
|
+
.status[data-status="offline"] { background: var(--fg-disabled); }
|
|
115
|
+
|
|
116
|
+
/* size별 뱃지 지름 — 컨테이너의 ~25~30% 범위.
|
|
117
|
+
ring은 작은 사이즈에선 hairline, 큰 사이즈에선 공간 토큰. */
|
|
118
|
+
.xs .status { --_status-size: var(--space-1_5); --_status-ring: var(--hairline); } /* 6 */
|
|
119
|
+
.sm .status { --_status-size: var(--space-2); --_status-ring: var(--hairline); } /* 8 */
|
|
120
|
+
.md .status { --_status-size: var(--space-2_5); --_status-ring: var(--space-0_5); } /* 10 */
|
|
121
|
+
.lg .status { --_status-size: var(--space-3); --_status-ring: var(--space-0_5); } /* 12 */
|
|
122
|
+
.xl .status { --_status-size: var(--space-6); --_status-ring: var(--space-1); } /* 24 */
|
|
123
|
+
|
|
124
|
+
/* rounded shape는 모서리가 각지므로 뱃지가 모서리 바깥으로 살짝 나가도록 */
|
|
125
|
+
.rounded .status {
|
|
126
|
+
right: calc(var(--_status-size) * -0.2);
|
|
127
|
+
bottom: calc(var(--_status-size) * -0.2);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/* ── AvatarGroup ────────────────────────────────────────────
|
|
131
|
+
가로 겹침. 왼쪽 아바타가 앞에 오도록 z-index 역순. bg 링으로 경계 분리.
|
|
132
|
+
겹침 offset은 각 아바타 폭의 ~30%. size별로 자동 스케일. */
|
|
133
|
+
.group {
|
|
134
|
+
display: inline-flex;
|
|
135
|
+
flex-direction: row;
|
|
136
|
+
align-items: center;
|
|
137
|
+
isolation: isolate;
|
|
138
|
+
width: fit-content;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.groupItem {
|
|
142
|
+
position: relative;
|
|
143
|
+
z-index: var(--stack-order, 0);
|
|
144
|
+
box-shadow: 0 0 0 var(--space-0_5) var(--bg);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.groupItem + .groupItem {
|
|
148
|
+
margin-left: calc(var(--space-1) * -1);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/* size별 overlap 강도 조정 */
|
|
152
|
+
.group.xs .groupItem + .groupItem { margin-left: calc(var(--space-1) * -1); } /* -4 */
|
|
153
|
+
.group.sm .groupItem + .groupItem { margin-left: calc(var(--space-1_5) * -1); } /* -6 */
|
|
154
|
+
.group.md .groupItem + .groupItem { margin-left: calc(var(--space-2) * -1); } /* -8 */
|
|
155
|
+
.group.lg .groupItem + .groupItem { margin-left: calc(var(--space-3) * -1); } /* -12 */
|
|
156
|
+
.group.xl .groupItem + .groupItem { margin-left: calc(var(--space-6) * -1); } /* -24 */
|
|
157
|
+
|
|
158
|
+
.overflow {
|
|
159
|
+
background: var(--bg-muted);
|
|
160
|
+
color: var(--fg-subtle);
|
|
161
|
+
font-weight: var(--font-weight-medium);
|
|
162
|
+
}
|
|
163
|
+
.overflow.xs { font-size: calc(var(--space-4) * 0.42); }
|
|
164
|
+
.overflow.sm { font-size: calc(var(--space-6) * 0.36); }
|
|
165
|
+
.overflow.md { font-size: calc(var(--space-8) * 0.36); }
|
|
166
|
+
.overflow.lg { font-size: calc(var(--space-12) * 0.36); }
|
|
167
|
+
.overflow.xl { font-size: calc(var(--space-24) * 0.36); }
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Children,
|
|
5
|
+
cloneElement,
|
|
6
|
+
forwardRef,
|
|
7
|
+
isValidElement,
|
|
8
|
+
useState,
|
|
9
|
+
type HTMLAttributes,
|
|
10
|
+
type ReactElement,
|
|
11
|
+
type ReactNode,
|
|
12
|
+
} from "react";
|
|
13
|
+
|
|
14
|
+
import { Icon } from "../Icon";
|
|
15
|
+
import styles from "./Avatar.module.css";
|
|
16
|
+
|
|
17
|
+
export type AvatarSize = "xs" | "sm" | "md" | "lg" | "xl";
|
|
18
|
+
export type AvatarShape = "circle" | "rounded";
|
|
19
|
+
export type AvatarStatus = "online" | "offline";
|
|
20
|
+
|
|
21
|
+
export type AvatarProps = HTMLAttributes<HTMLSpanElement> & {
|
|
22
|
+
size?: AvatarSize;
|
|
23
|
+
/** circle(기본) — 사람·개인 계정. rounded — 팀·조직·봇·서비스 아이덴티티. */
|
|
24
|
+
shape?: AvatarShape;
|
|
25
|
+
/** 이미지 URL. 로드 실패 시 이니셜 또는 아이콘으로 폴백. */
|
|
26
|
+
src?: string;
|
|
27
|
+
/** 접근성 대체 텍스트. 이미지 alt이자 스크린리더 라벨. */
|
|
28
|
+
alt?: string;
|
|
29
|
+
/** 이름 문자열. src 없으면 여기서 이니셜 추출. */
|
|
30
|
+
name?: string;
|
|
31
|
+
/** 이니셜을 직접 지정. name보다 우선. */
|
|
32
|
+
initials?: string;
|
|
33
|
+
/** 이미지·이니셜 대신 아이콘. */
|
|
34
|
+
icon?: ReactNode;
|
|
35
|
+
/** 접속 상태 뱃지. online — 접속중, offline — 미접속. 미지정 시 뱃지 없음. */
|
|
36
|
+
status?: AvatarStatus;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const sizeClass: Record<AvatarSize, string> = {
|
|
40
|
+
xs: styles.xs,
|
|
41
|
+
sm: styles.sm,
|
|
42
|
+
md: styles.md,
|
|
43
|
+
lg: styles.lg,
|
|
44
|
+
xl: styles.xl,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const shapeClass: Record<AvatarShape, string> = {
|
|
48
|
+
circle: styles.circle,
|
|
49
|
+
rounded: styles.rounded,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const ICON_SIZE: Record<AvatarSize, number> = {
|
|
53
|
+
xs: 10,
|
|
54
|
+
sm: 14,
|
|
55
|
+
md: 18,
|
|
56
|
+
lg: 24,
|
|
57
|
+
xl: 40,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/* 이름에서 이니셜 뽑기.
|
|
61
|
+
- 한글은 첫 글자 1개 ("김희영" → "김")
|
|
62
|
+
- 영문은 첫 단어의 앞 2글자: 대문자 + 소문자 ("Kim Heeyoung" → "Ki", "AB" → "Ab")
|
|
63
|
+
- 실패 시 "?" */
|
|
64
|
+
function extractInitials(name: string): string {
|
|
65
|
+
const trimmed = name.trim();
|
|
66
|
+
if (!trimmed) return "?";
|
|
67
|
+
if (/[ㄱ-힣]/.test(trimmed[0])) return trimmed[0];
|
|
68
|
+
const first = trimmed.split(/\s+/)[0] ?? "";
|
|
69
|
+
if (!first) return "?";
|
|
70
|
+
return (first[0].toUpperCase() + (first[1] ?? "").toLowerCase()) || "?";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/* seed 문자열 → 0~7 안정적 해시.
|
|
74
|
+
같은 이름이면 같은 색, 다른 이름은 되도록 다른 색. */
|
|
75
|
+
const TINT_COUNT = 10;
|
|
76
|
+
function tintIndex(seed: string): number {
|
|
77
|
+
let h = 0;
|
|
78
|
+
for (let i = 0; i < seed.length; i++) {
|
|
79
|
+
h = (h * 31 + seed.charCodeAt(i)) >>> 0;
|
|
80
|
+
}
|
|
81
|
+
return h % TINT_COUNT;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function sizedIcon(node: ReactNode, size: number): ReactNode {
|
|
85
|
+
if (!isValidElement(node)) return node;
|
|
86
|
+
const el = node as ReactElement<{ size?: number | string }>;
|
|
87
|
+
if (el.props.size !== undefined) return el;
|
|
88
|
+
return cloneElement(el, { size });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const Avatar = forwardRef<HTMLSpanElement, AvatarProps>(
|
|
92
|
+
(
|
|
93
|
+
{ className, size = "md", shape = "circle", src, alt, name, initials, icon, status, style, ...rest },
|
|
94
|
+
ref,
|
|
95
|
+
) => {
|
|
96
|
+
/* 이미지는 로드 완료 전까진 감추고 이니셜/아이콘을 아래에 깔아둔다.
|
|
97
|
+
onError로 실패 감지도 하지만, 드물게 발동 안 하는 케이스(DNS 실패 등)에 대비해
|
|
98
|
+
"성공했을 때만 이미지 표시" 전략으로 폴백 UI가 항상 안전. */
|
|
99
|
+
const [imgLoaded, setImgLoaded] = useState(false);
|
|
100
|
+
const [imgFailed, setImgFailed] = useState(false);
|
|
101
|
+
|
|
102
|
+
const cls = [styles.root, sizeClass[size], shapeClass[shape], className ?? ""]
|
|
103
|
+
.filter(Boolean)
|
|
104
|
+
.join(" ");
|
|
105
|
+
|
|
106
|
+
const hasUsableImage = src && !imgFailed;
|
|
107
|
+
const resolvedInitials = initials ?? (name ? extractInitials(name) : undefined);
|
|
108
|
+
const ariaLabel = alt ?? name;
|
|
109
|
+
|
|
110
|
+
const tintSeed = resolvedInitials
|
|
111
|
+
? (name ?? initials ?? resolvedInitials)
|
|
112
|
+
: undefined;
|
|
113
|
+
const tintStyle: React.CSSProperties | undefined = tintSeed
|
|
114
|
+
? {
|
|
115
|
+
["--_avatar-bg" as string]: `var(--avatar-tint-${tintIndex(tintSeed)}-bg)`,
|
|
116
|
+
["--_avatar-fg" as string]: `var(--avatar-tint-${tintIndex(tintSeed)}-fg)`,
|
|
117
|
+
}
|
|
118
|
+
: undefined;
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<span
|
|
122
|
+
ref={ref}
|
|
123
|
+
className={cls}
|
|
124
|
+
role="img"
|
|
125
|
+
aria-label={ariaLabel}
|
|
126
|
+
style={{ ...tintStyle, ...style }}
|
|
127
|
+
{...rest}
|
|
128
|
+
>
|
|
129
|
+
{resolvedInitials ? (
|
|
130
|
+
<span className={styles.initials} aria-hidden>
|
|
131
|
+
{resolvedInitials}
|
|
132
|
+
</span>
|
|
133
|
+
) : icon ? (
|
|
134
|
+
<span className={styles.icon} aria-hidden>
|
|
135
|
+
{sizedIcon(icon, ICON_SIZE[size])}
|
|
136
|
+
</span>
|
|
137
|
+
) : (
|
|
138
|
+
<span className={styles.icon} aria-hidden>
|
|
139
|
+
<Icon name="user" size={ICON_SIZE[size]} />
|
|
140
|
+
</span>
|
|
141
|
+
)}
|
|
142
|
+
{hasUsableImage && (
|
|
143
|
+
<img
|
|
144
|
+
className={styles.image}
|
|
145
|
+
src={src}
|
|
146
|
+
alt=""
|
|
147
|
+
data-loaded={imgLoaded ? "true" : "false"}
|
|
148
|
+
onLoad={() => setImgLoaded(true)}
|
|
149
|
+
onError={() => setImgFailed(true)}
|
|
150
|
+
/>
|
|
151
|
+
)}
|
|
152
|
+
{status && (
|
|
153
|
+
<span
|
|
154
|
+
className={styles.status}
|
|
155
|
+
data-status={status}
|
|
156
|
+
aria-label={status === "online" ? "접속중" : "미접속"}
|
|
157
|
+
/>
|
|
158
|
+
)}
|
|
159
|
+
</span>
|
|
160
|
+
);
|
|
161
|
+
},
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
Avatar.displayName = "Avatar";
|
|
165
|
+
|
|
166
|
+
/* ── AvatarGroup ──────────────────────────────────────────
|
|
167
|
+
여러 Avatar를 겹쳐 표시. max로 잘라내고 나머지는 "+N" 카운터로. */
|
|
168
|
+
export type AvatarGroupProps = HTMLAttributes<HTMLSpanElement> & {
|
|
169
|
+
size?: AvatarSize;
|
|
170
|
+
shape?: AvatarShape;
|
|
171
|
+
/** 넘치면 카운터로 접힘. */
|
|
172
|
+
max?: number;
|
|
173
|
+
children: ReactNode;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const AvatarGroup = forwardRef<HTMLSpanElement, AvatarGroupProps>(
|
|
177
|
+
({ className, size = "md", shape = "circle", max, children, ...rest }, ref) => {
|
|
178
|
+
const all = Children.toArray(children).filter(isValidElement) as ReactElement<AvatarProps>[];
|
|
179
|
+
const visible = max !== undefined ? all.slice(0, max) : all;
|
|
180
|
+
const overflow = max !== undefined ? all.length - visible.length : 0;
|
|
181
|
+
|
|
182
|
+
const cls = [styles.group, sizeClass[size], className ?? ""]
|
|
183
|
+
.filter(Boolean)
|
|
184
|
+
.join(" ");
|
|
185
|
+
|
|
186
|
+
const total = visible.length + (overflow > 0 ? 1 : 0);
|
|
187
|
+
|
|
188
|
+
return (
|
|
189
|
+
<span ref={ref} className={cls} {...rest}>
|
|
190
|
+
{visible.map((child, i) =>
|
|
191
|
+
cloneElement(child, {
|
|
192
|
+
key: child.key ?? i,
|
|
193
|
+
size,
|
|
194
|
+
shape,
|
|
195
|
+
className: [styles.groupItem, child.props.className ?? ""].filter(Boolean).join(" "),
|
|
196
|
+
style: {
|
|
197
|
+
...(child.props.style ?? {}),
|
|
198
|
+
["--stack-order" as string]: total - i,
|
|
199
|
+
},
|
|
200
|
+
}),
|
|
201
|
+
)}
|
|
202
|
+
{overflow > 0 && (
|
|
203
|
+
<span
|
|
204
|
+
className={[
|
|
205
|
+
styles.root,
|
|
206
|
+
sizeClass[size],
|
|
207
|
+
shapeClass[shape],
|
|
208
|
+
styles.groupItem,
|
|
209
|
+
styles.overflow,
|
|
210
|
+
]
|
|
211
|
+
.filter(Boolean)
|
|
212
|
+
.join(" ")}
|
|
213
|
+
role="img"
|
|
214
|
+
aria-label={`외 ${overflow}명`}
|
|
215
|
+
>
|
|
216
|
+
<span className={styles.initials} aria-hidden>
|
|
217
|
+
+{overflow}
|
|
218
|
+
</span>
|
|
219
|
+
</span>
|
|
220
|
+
)}
|
|
221
|
+
</span>
|
|
222
|
+
);
|
|
223
|
+
},
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
AvatarGroup.displayName = "AvatarGroup";
|
|
227
|
+
|
|
228
|
+
export { Avatar, AvatarGroup };
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/* AvatarUpload ────────────────────────────────────────────────
|
|
2
|
+
Avatar를 감싸는 폼 컨트롤. 표시 로직은 Avatar에 위임하고,
|
|
3
|
+
여기선 파일 선택·삭제 인터랙션과 오버레이 UI만 담당.
|
|
4
|
+
|
|
5
|
+
구조: <div>
|
|
6
|
+
<button.trigger> ← 클릭 시 <input type=file>
|
|
7
|
+
<Avatar />
|
|
8
|
+
<span.overlay /> ← hover 시 하단에 카메라
|
|
9
|
+
</button>
|
|
10
|
+
<button.remove /> ← src 있을 때만, 우상단 X
|
|
11
|
+
<input type=file .input /> ← 시각 숨김
|
|
12
|
+
</div> */
|
|
13
|
+
.root {
|
|
14
|
+
position: relative;
|
|
15
|
+
display: inline-flex;
|
|
16
|
+
flex-shrink: 0;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.trigger {
|
|
20
|
+
position: relative;
|
|
21
|
+
padding: 0;
|
|
22
|
+
border: 0;
|
|
23
|
+
background: transparent;
|
|
24
|
+
cursor: pointer;
|
|
25
|
+
overflow: hidden;
|
|
26
|
+
transition:
|
|
27
|
+
box-shadow var(--duration-fast) var(--ease-out);
|
|
28
|
+
}
|
|
29
|
+
/* trigger는 안쪽 Avatar와 border-radius를 맞춰야 focus ring이 정확한 모양으로 감쌈.
|
|
30
|
+
circle이면 full, rounded면 Avatar와 동일한 size별 radius. */
|
|
31
|
+
.circle .trigger { border-radius: var(--radius-full); }
|
|
32
|
+
.rounded.xs .trigger,
|
|
33
|
+
.rounded.sm .trigger { border-radius: var(--radius-xs); }
|
|
34
|
+
.rounded.md .trigger { border-radius: var(--radius-sm); }
|
|
35
|
+
.rounded.lg .trigger { border-radius: var(--radius-md); }
|
|
36
|
+
.rounded.xl .trigger { border-radius: var(--radius-lg); }
|
|
37
|
+
.trigger:focus-visible {
|
|
38
|
+
outline: none;
|
|
39
|
+
box-shadow: var(--ring-shadow);
|
|
40
|
+
}
|
|
41
|
+
.trigger:disabled {
|
|
42
|
+
cursor: not-allowed;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/* Overlay ─────────────────────────────────────────────────
|
|
46
|
+
하단 반쪽에만 오버레이. 기본 숨김, hover/focus에서 등장.
|
|
47
|
+
translateY로 slide-up 애니 — 즉시 등장보다 프로필 사진을 덜 가리는 인상.
|
|
48
|
+
scrim/scrim-fg 토큰으로 mode 무관 다크 표면. */
|
|
49
|
+
.overlay {
|
|
50
|
+
position: absolute;
|
|
51
|
+
left: 0;
|
|
52
|
+
right: 0;
|
|
53
|
+
bottom: 0;
|
|
54
|
+
height: 50%;
|
|
55
|
+
display: inline-flex;
|
|
56
|
+
align-items: center;
|
|
57
|
+
justify-content: center;
|
|
58
|
+
background: var(--scrim);
|
|
59
|
+
color: var(--scrim-fg);
|
|
60
|
+
opacity: 0;
|
|
61
|
+
transform: translateY(20%);
|
|
62
|
+
pointer-events: none;
|
|
63
|
+
transition:
|
|
64
|
+
opacity var(--duration-base) var(--ease-out),
|
|
65
|
+
transform var(--duration-base) var(--ease-out);
|
|
66
|
+
}
|
|
67
|
+
.trigger:hover .overlay,
|
|
68
|
+
.trigger:focus-visible .overlay {
|
|
69
|
+
opacity: 1;
|
|
70
|
+
transform: translateY(0);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/* Remove button ───────────────────────────────────────────
|
|
74
|
+
우상단. X. Avatar 원 바깥으로 살짝 튀어나오게 배치해서
|
|
75
|
+
"부착된 액션"임을 명확히. z-docked로 오버레이보다 위. */
|
|
76
|
+
.remove {
|
|
77
|
+
position: absolute;
|
|
78
|
+
top: 0;
|
|
79
|
+
right: 0;
|
|
80
|
+
display: inline-flex;
|
|
81
|
+
align-items: center;
|
|
82
|
+
justify-content: center;
|
|
83
|
+
padding: 0;
|
|
84
|
+
border: 1px solid var(--border);
|
|
85
|
+
background: var(--bg);
|
|
86
|
+
color: var(--fg);
|
|
87
|
+
border-radius: var(--radius-full);
|
|
88
|
+
cursor: pointer;
|
|
89
|
+
transition:
|
|
90
|
+
border-color var(--duration-fast) var(--ease-out),
|
|
91
|
+
box-shadow var(--duration-fast) var(--ease-out);
|
|
92
|
+
z-index: var(--z-docked);
|
|
93
|
+
}
|
|
94
|
+
.remove:hover:not(:disabled) {
|
|
95
|
+
border-color: var(--border-strong);
|
|
96
|
+
box-shadow: var(--shadow-sm);
|
|
97
|
+
}
|
|
98
|
+
.remove:focus-visible {
|
|
99
|
+
outline: none;
|
|
100
|
+
box-shadow: var(--ring-shadow);
|
|
101
|
+
}
|
|
102
|
+
.remove:disabled {
|
|
103
|
+
cursor: not-allowed;
|
|
104
|
+
opacity: 0.5;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/* Remove 크기 · offset은 Avatar size별로 스왑. 원 바깥으로 살짝 튀어나오도록. */
|
|
108
|
+
.xs .remove { width: var(--space-3); height: var(--space-3); transform: translate(25%, -25%); }
|
|
109
|
+
.sm .remove { width: var(--space-4); height: var(--space-4); transform: translate(25%, -25%); }
|
|
110
|
+
.md .remove { width: var(--space-4); height: var(--space-4); transform: translate(25%, -25%); }
|
|
111
|
+
.lg .remove { width: var(--space-5); height: var(--space-5); transform: translate(20%, -20%); }
|
|
112
|
+
.xl .remove { width: var(--space-6); height: var(--space-6); transform: translate(15%, -15%); }
|
|
113
|
+
|
|
114
|
+
/* Hidden native input — 표준 visually-hidden 유틸을 클래스로 인라인 */
|
|
115
|
+
.input {
|
|
116
|
+
position: absolute;
|
|
117
|
+
width: var(--space-0_5); /* 시각 완전 숨김 — 실제 크기는 무의미 */
|
|
118
|
+
height: var(--space-0_5);
|
|
119
|
+
padding: 0;
|
|
120
|
+
overflow: hidden;
|
|
121
|
+
clip: rect(0, 0, 0, 0);
|
|
122
|
+
white-space: nowrap;
|
|
123
|
+
border: 0;
|
|
124
|
+
opacity: 0;
|
|
125
|
+
pointer-events: none;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/* Disabled ─────────────────────────────────────────────── */
|
|
129
|
+
.disabled .trigger { opacity: 0.5; }
|
|
130
|
+
.disabled .overlay { display: none; }
|