@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,82 @@
|
|
|
1
|
+
/* 숫자 아이템은 Button(라벨 있음), prev/next/first/last는 IconButton(정사각 전용).
|
|
2
|
+
Button을 icon-only로 쓰면 가로 padding이 남아 정사각이 안 되므로 IconButton이 맞다.
|
|
3
|
+
숫자 Button의 min-width만 height에 맞춰 1자리에서도 폭이 흔들리지 않게 한다. 2자리+ 자연 확장.
|
|
4
|
+
|
|
5
|
+
사이즈 매핑:
|
|
6
|
+
Pagination md = 내부 Button sm(32px) + IconButton md(실제 width/height를 32로 올림)
|
|
7
|
+
Pagination lg = 내부 Button md(36px) + IconButton lg(실제 width/height를 36으로 올림)
|
|
8
|
+
*/
|
|
9
|
+
.root {
|
|
10
|
+
display: inline-flex;
|
|
11
|
+
align-items: center;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.list {
|
|
15
|
+
display: inline-flex;
|
|
16
|
+
align-items: center;
|
|
17
|
+
gap: var(--space-0_5);
|
|
18
|
+
padding: 0;
|
|
19
|
+
margin: 0;
|
|
20
|
+
list-style: none;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/* Compact variant는 "prev · 3/24 · next" 세 요소뿐이므로 gap을 더 조여 하나의 밀집 블록처럼 보이게. */
|
|
24
|
+
.listCompact {
|
|
25
|
+
gap: 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/* 숫자 Button min-width — 내부 Button height와 맞춘 정사각 hit box.
|
|
29
|
+
padding-inline은 Button 기본값보다 좁혀 1자리 숫자가 정사각으로 떨어지게. */
|
|
30
|
+
.numberBtnMd {
|
|
31
|
+
min-width: var(--control-h-sm);
|
|
32
|
+
padding-inline: var(--space-1);
|
|
33
|
+
}
|
|
34
|
+
.numberBtnLg {
|
|
35
|
+
min-width: var(--control-h-md);
|
|
36
|
+
padding-inline: var(--space-1_5);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/* Disabled 시 숫자 Button은 배경 없이 숫자 색만 fg-disabled로 연해지게.
|
|
40
|
+
Button 기본은 모든 variant를 disabled에서 회색 배경(--bg-muted)으로 통일하지만,
|
|
41
|
+
Pagination 내부의 숫자 아이템은 밀집한 자리라 밑 배경이 가려지면 어색하다.
|
|
42
|
+
IconButton의 ghost disabled와 톤 맞춤.
|
|
43
|
+
.list 접두어로 Button 내부 셀렉터보다 특이도 우위 확보. */
|
|
44
|
+
.list .numberBtnMd:disabled,
|
|
45
|
+
.list .numberBtnLg:disabled {
|
|
46
|
+
background: transparent;
|
|
47
|
+
border-color: transparent;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/* IconButton 리사이즈 — IconButton의 기본 사이즈 체계는 Button보다 한 단계씩 작게 설계되어
|
|
51
|
+
있어서(md=24, lg=32) 숫자 Button(sm=32, md=36)과 세로 리듬이 안 맞는다.
|
|
52
|
+
Pagination 안에서만 width/height를 숫자 Button과 동일하게 올려 리듬 일치. */
|
|
53
|
+
.iconBtnMd {
|
|
54
|
+
width: var(--control-h-sm);
|
|
55
|
+
height: var(--control-h-sm);
|
|
56
|
+
}
|
|
57
|
+
.iconBtnLg {
|
|
58
|
+
width: var(--control-h-md);
|
|
59
|
+
height: var(--control-h-md);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* Compact 상태 라벨 — "3 / 24" 류. 숫자/구분자 모두 fg-muted로 가라앉히고
|
|
63
|
+
인터랙션 영역이 아님을 명확히. gap은 숫자-구분자 사이가 뜨지 않게 짧게. */
|
|
64
|
+
.statusMd,
|
|
65
|
+
.statusLg {
|
|
66
|
+
display: inline-flex;
|
|
67
|
+
align-items: center;
|
|
68
|
+
padding-inline: var(--space-1);
|
|
69
|
+
font-weight: var(--label-weight);
|
|
70
|
+
color: var(--fg-muted);
|
|
71
|
+
font-variant-numeric: tabular-nums;
|
|
72
|
+
white-space: nowrap;
|
|
73
|
+
letter-spacing: var(--label-tracking);
|
|
74
|
+
}
|
|
75
|
+
.statusMd {
|
|
76
|
+
min-height: var(--control-h-sm);
|
|
77
|
+
font-size: var(--label-sm-size);
|
|
78
|
+
}
|
|
79
|
+
.statusLg {
|
|
80
|
+
min-height: var(--control-h-md);
|
|
81
|
+
font-size: var(--label-md-size);
|
|
82
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pagination ─ 긴 목록·테이블의 페이지 네비.
|
|
5
|
+
*
|
|
6
|
+
* 설계 원칙 — "variant로 유즈케이스 조립":
|
|
7
|
+
* DatePicker처럼 분화된 컴포넌트(DateRangePicker)를 만들지 않고 하나의 Pagination에
|
|
8
|
+
* `variant` prop으로 numbered / simple / compact / jump를 커버한다. prop 표면이
|
|
9
|
+
* 예측 가능성이 높다.
|
|
10
|
+
*
|
|
11
|
+
* 시각 규칙:
|
|
12
|
+
* - 숫자 아이템은 Button(라벨 있음). prev/next/first/last는 IconButton(정사각 전용).
|
|
13
|
+
* Button을 icon-only로 쓰면 가로 padding이 남아 정사각으로 안 떨어지므로 IconButton이 맞다.
|
|
14
|
+
* - 사이즈 매핑: Pagination md = Button sm + IconButton md, Pagination lg = Button md + IconButton lg.
|
|
15
|
+
* - 현재 페이지는 `variant="secondary"` (primary 8% 알파 배경 + primary 텍스트).
|
|
16
|
+
* primary filled는 페이지 안 다른 primary 액션과 충돌해 쓰지 않음.
|
|
17
|
+
* - 그 외는 `variant="ghost"`로 시각 무게 최소화.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
forwardRef,
|
|
22
|
+
useMemo,
|
|
23
|
+
type HTMLAttributes,
|
|
24
|
+
type ReactNode,
|
|
25
|
+
} from "react";
|
|
26
|
+
|
|
27
|
+
import { Button } from "../Button";
|
|
28
|
+
import { IconButton, type IconButtonSize } from "../IconButton";
|
|
29
|
+
import { Icon } from "../Icon";
|
|
30
|
+
import styles from "./Pagination.module.css";
|
|
31
|
+
|
|
32
|
+
/** Pagination size → 내부 Button/IconButton size 매핑.
|
|
33
|
+
* Pagination md = Button sm + IconButton md, Pagination lg = Button md + IconButton lg. */
|
|
34
|
+
const BUTTON_SIZE: Record<"md" | "lg", "sm" | "md"> = {
|
|
35
|
+
md: "sm",
|
|
36
|
+
lg: "md",
|
|
37
|
+
};
|
|
38
|
+
const ICON_BTN_SIZE: Record<"md" | "lg", IconButtonSize> = {
|
|
39
|
+
md: "md",
|
|
40
|
+
lg: "lg",
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export type PaginationVariant = "numbered" | "compact" | "jump";
|
|
44
|
+
export type PaginationSize = "md" | "lg";
|
|
45
|
+
|
|
46
|
+
export interface PaginationProps
|
|
47
|
+
extends Omit<HTMLAttributes<HTMLElement>, "onChange"> {
|
|
48
|
+
variant?: PaginationVariant;
|
|
49
|
+
/** 아이템 사이즈. md=내부 Button sm, lg=내부 Button md. */
|
|
50
|
+
size?: PaginationSize;
|
|
51
|
+
/** 현재 페이지 (1-indexed) */
|
|
52
|
+
page: number;
|
|
53
|
+
/** 전체 페이지 수 */
|
|
54
|
+
total: number;
|
|
55
|
+
onChange?: (page: number) => void;
|
|
56
|
+
/** numbered·jump에서 한 번에 보여줄 숫자 개수(5~10). 기본 5.
|
|
57
|
+
* 현재 페이지 중심의 슬라이딩 윈도우 — 생략(…)·양 끝 고정 숫자 없음. */
|
|
58
|
+
windowSize?: number;
|
|
59
|
+
disabled?: boolean;
|
|
60
|
+
/** compact variant의 라벨 포맷터. 기본 `${page} / ${total}`. */
|
|
61
|
+
compactFormatter?: (page: number, total: number) => ReactNode;
|
|
62
|
+
"aria-label"?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 현재 페이지 중심의 슬라이딩 윈도우.
|
|
67
|
+
* 전체가 윈도우보다 작으면 전부 표시. 아니면 현재를 가운데 두되,
|
|
68
|
+
* 왼/오 끝 가까우면 그 쪽으로 밀착해 빈 자리가 생기지 않게 한다.
|
|
69
|
+
* 예: total=100, windowSize=5, page=1 → [1..5]
|
|
70
|
+
* total=100, windowSize=5, page=10 → [8..12]
|
|
71
|
+
* total=100, windowSize=5, page=100 → [96..100]
|
|
72
|
+
*/
|
|
73
|
+
function buildWindow(page: number, total: number, windowSize: number): number[] {
|
|
74
|
+
const size = Math.min(windowSize, total);
|
|
75
|
+
const half = Math.floor((size - 1) / 2);
|
|
76
|
+
let start = page - half;
|
|
77
|
+
let end = start + size - 1;
|
|
78
|
+
if (start < 1) {
|
|
79
|
+
start = 1;
|
|
80
|
+
end = size;
|
|
81
|
+
}
|
|
82
|
+
if (end > total) {
|
|
83
|
+
end = total;
|
|
84
|
+
start = total - size + 1;
|
|
85
|
+
}
|
|
86
|
+
return Array.from({ length: size }, (_, i) => start + i);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** windowSize를 5~10 범위로 클램프. */
|
|
90
|
+
function clampWindow(n: number): number {
|
|
91
|
+
if (!Number.isFinite(n)) return 5;
|
|
92
|
+
return Math.min(10, Math.max(5, Math.round(n)));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export const Pagination = forwardRef<HTMLElement, PaginationProps>(
|
|
96
|
+
(
|
|
97
|
+
{
|
|
98
|
+
variant = "numbered",
|
|
99
|
+
size = "md",
|
|
100
|
+
page,
|
|
101
|
+
total,
|
|
102
|
+
onChange,
|
|
103
|
+
windowSize = 5,
|
|
104
|
+
disabled = false,
|
|
105
|
+
compactFormatter,
|
|
106
|
+
className,
|
|
107
|
+
"aria-label": ariaLabel = "페이지네이션",
|
|
108
|
+
...rest
|
|
109
|
+
},
|
|
110
|
+
ref
|
|
111
|
+
) => {
|
|
112
|
+
const safeTotal = Math.max(1, total);
|
|
113
|
+
const safePage = Math.min(Math.max(1, page), safeTotal);
|
|
114
|
+
const isFirst = safePage <= 1;
|
|
115
|
+
const isLast = safePage >= safeTotal;
|
|
116
|
+
|
|
117
|
+
const go = (p: number) => {
|
|
118
|
+
if (disabled) return;
|
|
119
|
+
const next = Math.min(Math.max(1, p), safeTotal);
|
|
120
|
+
if (next !== safePage) onChange?.(next);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const range = useMemo(
|
|
124
|
+
() =>
|
|
125
|
+
variant === "numbered" || variant === "jump"
|
|
126
|
+
? buildWindow(safePage, safeTotal, clampWindow(windowSize))
|
|
127
|
+
: [],
|
|
128
|
+
[variant, safePage, safeTotal, windowSize]
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
const rootClass = [styles.root, className ?? ""].filter(Boolean).join(" ");
|
|
132
|
+
const numberCls = size === "md" ? styles.numberBtnMd : styles.numberBtnLg;
|
|
133
|
+
const iconCls = size === "md" ? styles.iconBtnMd : styles.iconBtnLg;
|
|
134
|
+
const statusCls = size === "md" ? styles.statusMd : styles.statusLg;
|
|
135
|
+
const buttonSize = BUTTON_SIZE[size];
|
|
136
|
+
const iconSize = ICON_BTN_SIZE[size];
|
|
137
|
+
|
|
138
|
+
/* prev/next는 "보이는 윈도우의 끝 숫자"로 점프 — 현재가 윈도우 중앙이면 윈도우 안에서 끝으로,
|
|
139
|
+
이미 끝 위에 있으면 다음 호출에서 윈도우가 통째로 밀린다. 숫자 윈도우가 없는 compact에선
|
|
140
|
+
기존 ±1 유지. */
|
|
141
|
+
const prevTarget = range.length > 0 ? range[0] : safePage - 1;
|
|
142
|
+
const nextTarget =
|
|
143
|
+
range.length > 0 ? range[range.length - 1] : safePage + 1;
|
|
144
|
+
const prevDisabled = disabled || isFirst || prevTarget === safePage;
|
|
145
|
+
const nextDisabled = disabled || isLast || nextTarget === safePage;
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<nav
|
|
149
|
+
ref={ref}
|
|
150
|
+
className={rootClass}
|
|
151
|
+
aria-label={ariaLabel}
|
|
152
|
+
role="navigation"
|
|
153
|
+
{...rest}
|
|
154
|
+
>
|
|
155
|
+
<ul
|
|
156
|
+
className={[
|
|
157
|
+
styles.list,
|
|
158
|
+
variant === "compact" ? styles.listCompact : "",
|
|
159
|
+
]
|
|
160
|
+
.filter(Boolean)
|
|
161
|
+
.join(" ")}
|
|
162
|
+
>
|
|
163
|
+
{variant === "jump" && (
|
|
164
|
+
<li>
|
|
165
|
+
<IconButton
|
|
166
|
+
size={iconSize}
|
|
167
|
+
variant="ghost"
|
|
168
|
+
aria-label="처음 페이지"
|
|
169
|
+
icon={<Icon name="caret-double-left" />}
|
|
170
|
+
disabled={disabled || isFirst}
|
|
171
|
+
onClick={() => go(1)}
|
|
172
|
+
className={iconCls}
|
|
173
|
+
/>
|
|
174
|
+
</li>
|
|
175
|
+
)}
|
|
176
|
+
|
|
177
|
+
<li>
|
|
178
|
+
<IconButton
|
|
179
|
+
size={iconSize}
|
|
180
|
+
variant="ghost"
|
|
181
|
+
aria-label="이전 페이지"
|
|
182
|
+
icon={<Icon name="caret-left" />}
|
|
183
|
+
disabled={prevDisabled}
|
|
184
|
+
onClick={() => go(prevTarget)}
|
|
185
|
+
className={iconCls}
|
|
186
|
+
/>
|
|
187
|
+
</li>
|
|
188
|
+
|
|
189
|
+
{variant === "compact" && (
|
|
190
|
+
<li className={statusCls} aria-live="polite">
|
|
191
|
+
{compactFormatter
|
|
192
|
+
? compactFormatter(safePage, safeTotal)
|
|
193
|
+
: `${safePage}/${safeTotal}`}
|
|
194
|
+
</li>
|
|
195
|
+
)}
|
|
196
|
+
|
|
197
|
+
{(variant === "numbered" || variant === "jump") &&
|
|
198
|
+
range.map((item) => {
|
|
199
|
+
const isCurrent = item === safePage;
|
|
200
|
+
return (
|
|
201
|
+
<li key={item}>
|
|
202
|
+
<Button
|
|
203
|
+
size={buttonSize}
|
|
204
|
+
variant={isCurrent ? "secondary" : "ghost"}
|
|
205
|
+
aria-label={`${item} 페이지`}
|
|
206
|
+
aria-current={isCurrent ? "page" : undefined}
|
|
207
|
+
disabled={disabled}
|
|
208
|
+
onClick={() => go(item)}
|
|
209
|
+
className={numberCls}
|
|
210
|
+
>
|
|
211
|
+
{item}
|
|
212
|
+
</Button>
|
|
213
|
+
</li>
|
|
214
|
+
);
|
|
215
|
+
})}
|
|
216
|
+
|
|
217
|
+
<li>
|
|
218
|
+
<IconButton
|
|
219
|
+
size={iconSize}
|
|
220
|
+
variant="ghost"
|
|
221
|
+
aria-label="다음 페이지"
|
|
222
|
+
icon={<Icon name="caret-right" />}
|
|
223
|
+
disabled={nextDisabled}
|
|
224
|
+
onClick={() => go(nextTarget)}
|
|
225
|
+
className={iconCls}
|
|
226
|
+
/>
|
|
227
|
+
</li>
|
|
228
|
+
|
|
229
|
+
{variant === "jump" && (
|
|
230
|
+
<li>
|
|
231
|
+
<IconButton
|
|
232
|
+
size={iconSize}
|
|
233
|
+
variant="ghost"
|
|
234
|
+
aria-label="마지막 페이지"
|
|
235
|
+
icon={<Icon name="caret-double-right" />}
|
|
236
|
+
disabled={disabled || isLast}
|
|
237
|
+
onClick={() => go(safeTotal)}
|
|
238
|
+
className={iconCls}
|
|
239
|
+
/>
|
|
240
|
+
</li>
|
|
241
|
+
)}
|
|
242
|
+
</ul>
|
|
243
|
+
</nav>
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
Pagination.displayName = "Pagination";
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
.root {
|
|
2
|
+
height: var(--space-0_5);
|
|
3
|
+
background: transparent;
|
|
4
|
+
overflow: hidden;
|
|
5
|
+
pointer-events: none;
|
|
6
|
+
transition: opacity var(--duration-fast) var(--ease-out);
|
|
7
|
+
opacity: 0;
|
|
8
|
+
}
|
|
9
|
+
.root[data-active] {
|
|
10
|
+
opacity: 1;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.root[data-position="fixed"] {
|
|
14
|
+
position: fixed;
|
|
15
|
+
top: 0;
|
|
16
|
+
left: 0;
|
|
17
|
+
right: 0;
|
|
18
|
+
z-index: var(--z-toast);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.bar {
|
|
22
|
+
height: 100%;
|
|
23
|
+
background: var(--fg-strong);
|
|
24
|
+
border-radius: 0 var(--radius-full) var(--radius-full) 0;
|
|
25
|
+
transition: width var(--duration-fast) var(--ease-out);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.root[data-mode="determinate"] .bar {
|
|
29
|
+
transform-origin: left center;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/* indeterminate — 왼쪽부터 채워지는 NProgress 스타일.
|
|
33
|
+
빠르게 시작해서 (뜨자마자 30% 이미 채움) 서서히 90%까지. */
|
|
34
|
+
.root[data-mode="indeterminate"] .bar {
|
|
35
|
+
width: 0%;
|
|
36
|
+
transform-origin: left center;
|
|
37
|
+
animation: pb-grow calc(var(--duration-slower) * 3) var(--ease-out) forwards;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@keyframes pb-grow {
|
|
41
|
+
0% { width: 0%; }
|
|
42
|
+
10% { width: 30%; }
|
|
43
|
+
30% { width: 55%; }
|
|
44
|
+
60% { width: 75%; }
|
|
45
|
+
100% { width: 90%; }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/* 사라질 때 100%까지 밀고 fade-out — "완료됐다" 느낌 */
|
|
49
|
+
.root:not([data-active])[data-mode="indeterminate"] .bar {
|
|
50
|
+
animation: none;
|
|
51
|
+
width: 100%;
|
|
52
|
+
transition:
|
|
53
|
+
width var(--duration-fast) var(--ease-out),
|
|
54
|
+
opacity var(--duration-base) var(--ease-out);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@media (prefers-reduced-motion: reduce) {
|
|
58
|
+
.root[data-mode="indeterminate"] .bar {
|
|
59
|
+
animation: none;
|
|
60
|
+
width: 100%;
|
|
61
|
+
transform: none;
|
|
62
|
+
opacity: 0.5;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { forwardRef, type HTMLAttributes } from "react";
|
|
4
|
+
import styles from "./ProgressBar.module.css";
|
|
5
|
+
|
|
6
|
+
export type ProgressBarProps = HTMLAttributes<HTMLDivElement> & {
|
|
7
|
+
/**
|
|
8
|
+
* 진행률 (0~100). 미지정이면 무한 왕복 애니메이션(indeterminate).
|
|
9
|
+
* 실제 %가 없는 라우팅/네트워크 로딩용.
|
|
10
|
+
*/
|
|
11
|
+
value?: number;
|
|
12
|
+
/** 화면 최상단에 fixed로 붙임. */
|
|
13
|
+
position?: "fixed" | "inline";
|
|
14
|
+
/** 표시 여부. false면 즉시 숨김 + fade-out. */
|
|
15
|
+
active?: boolean;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 라인 로딩바. 두 가지 모드:
|
|
20
|
+
* - value 있으면 그 %로 채움 (determinate)
|
|
21
|
+
* - value 없으면 무한 왕복 (indeterminate) — 네트워크·라우팅 로딩용
|
|
22
|
+
*/
|
|
23
|
+
const ProgressBar = forwardRef<HTMLDivElement, ProgressBarProps>(
|
|
24
|
+
({ value, position = "fixed", active = true, className, ...rest }, ref) => {
|
|
25
|
+
const isDeterminate = typeof value === "number";
|
|
26
|
+
const clamped = isDeterminate
|
|
27
|
+
? Math.max(0, Math.min(100, value as number))
|
|
28
|
+
: undefined;
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<div
|
|
32
|
+
ref={ref}
|
|
33
|
+
role="progressbar"
|
|
34
|
+
aria-valuemin={0}
|
|
35
|
+
aria-valuemax={100}
|
|
36
|
+
aria-valuenow={clamped}
|
|
37
|
+
data-position={position}
|
|
38
|
+
data-active={active || undefined}
|
|
39
|
+
data-mode={isDeterminate ? "determinate" : "indeterminate"}
|
|
40
|
+
className={[styles.root, className ?? ""].filter(Boolean).join(" ")}
|
|
41
|
+
{...rest}
|
|
42
|
+
>
|
|
43
|
+
<div
|
|
44
|
+
className={styles.bar}
|
|
45
|
+
style={isDeterminate ? { width: `${clamped}%` } : undefined}
|
|
46
|
+
/>
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
},
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
ProgressBar.displayName = "ProgressBar";
|
|
53
|
+
|
|
54
|
+
export { ProgressBar };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/* QuotationBar — ChatInput 상단에 붙는 인용 바.
|
|
2
|
+
↳ icon + 1줄 ellipsis + X 레이아웃. hairline border, radius-md. */
|
|
3
|
+
|
|
4
|
+
.root {
|
|
5
|
+
display: flex;
|
|
6
|
+
align-items: center;
|
|
7
|
+
gap: var(--space-2);
|
|
8
|
+
min-width: 0;
|
|
9
|
+
padding: var(--space-2) var(--space-2) var(--space-2) var(--space-3);
|
|
10
|
+
background: var(--bg);
|
|
11
|
+
border: var(--hairline) solid var(--border);
|
|
12
|
+
border-radius: var(--radius-md);
|
|
13
|
+
color: var(--fg);
|
|
14
|
+
font-size: var(--text-sm);
|
|
15
|
+
transition: var(--transition-colors);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.leadIcon {
|
|
19
|
+
width: var(--space-4);
|
|
20
|
+
height: var(--space-4);
|
|
21
|
+
color: var(--fg-muted);
|
|
22
|
+
flex: 0 0 auto;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.text {
|
|
26
|
+
flex: 1 1 auto;
|
|
27
|
+
min-width: 0;
|
|
28
|
+
overflow: hidden;
|
|
29
|
+
text-overflow: ellipsis;
|
|
30
|
+
white-space: nowrap;
|
|
31
|
+
color: var(--fg-muted);
|
|
32
|
+
line-height: 1.3;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.removeBtn {
|
|
36
|
+
display: inline-flex;
|
|
37
|
+
align-items: center;
|
|
38
|
+
justify-content: center;
|
|
39
|
+
padding: 0;
|
|
40
|
+
border: 0;
|
|
41
|
+
background: transparent;
|
|
42
|
+
color: var(--fg-muted);
|
|
43
|
+
border-radius: var(--radius-sm);
|
|
44
|
+
cursor: pointer;
|
|
45
|
+
transition: var(--transition-colors);
|
|
46
|
+
width: var(--space-5);
|
|
47
|
+
height: var(--space-5);
|
|
48
|
+
flex: 0 0 auto;
|
|
49
|
+
}
|
|
50
|
+
.removeBtn:hover {
|
|
51
|
+
background: var(--bg-subtle);
|
|
52
|
+
color: var(--fg);
|
|
53
|
+
}
|
|
54
|
+
.removeBtn:focus-visible {
|
|
55
|
+
outline: none;
|
|
56
|
+
box-shadow: var(--ring);
|
|
57
|
+
}
|
|
58
|
+
.removeIcon {
|
|
59
|
+
width: 70%;
|
|
60
|
+
height: 70%;
|
|
61
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { HTMLAttributes, MouseEvent } from "react";
|
|
4
|
+
import {
|
|
5
|
+
ArrowBendDownRight as PhArrowBendDownRight,
|
|
6
|
+
X as PhX,
|
|
7
|
+
} from "@phosphor-icons/react";
|
|
8
|
+
import styles from "./QuotationBar.module.css";
|
|
9
|
+
|
|
10
|
+
export interface QuotationBarProps
|
|
11
|
+
extends Omit<HTMLAttributes<HTMLDivElement>, "children" | "onError"> {
|
|
12
|
+
/** 인용 본문. 1줄 ellipsis — 긴 글은 자동 생략. */
|
|
13
|
+
text: string;
|
|
14
|
+
/** 지정 시 우측 X 노출, 클릭 시 호출. */
|
|
15
|
+
onRemove?: (e: MouseEvent<HTMLButtonElement>) => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function QuotationBar({
|
|
19
|
+
text,
|
|
20
|
+
onRemove,
|
|
21
|
+
className,
|
|
22
|
+
...rest
|
|
23
|
+
}: QuotationBarProps) {
|
|
24
|
+
const cls = [styles.root, className].filter(Boolean).join(" ");
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div className={cls} {...rest}>
|
|
28
|
+
<PhArrowBendDownRight
|
|
29
|
+
className={styles.leadIcon}
|
|
30
|
+
weight="regular"
|
|
31
|
+
aria-hidden
|
|
32
|
+
/>
|
|
33
|
+
<span className={styles.text} title={text}>
|
|
34
|
+
{text}
|
|
35
|
+
</span>
|
|
36
|
+
{onRemove ? (
|
|
37
|
+
<button
|
|
38
|
+
type="button"
|
|
39
|
+
className={styles.removeBtn}
|
|
40
|
+
onClick={onRemove}
|
|
41
|
+
aria-label="인용 제거"
|
|
42
|
+
>
|
|
43
|
+
<PhX className={styles.removeIcon} weight="regular" />
|
|
44
|
+
</button>
|
|
45
|
+
) : null}
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/* Radio ────────────────────────────────────────────────────────
|
|
2
|
+
폼 · 필터에서 단일 선택을 표시하는 컨트롤.
|
|
3
|
+
외곽 원 = box-shadow inset, 안쪽 도트 = radial-gradient (indicator의
|
|
4
|
+
같은 배경 레이어에 그려지므로 서브픽셀 어긋남이 없다). 접근성 = <input type="radio">.
|
|
5
|
+
FilterButton type="radio"도 indicator를 재사용한다. */
|
|
6
|
+
.root {
|
|
7
|
+
display: inline-flex;
|
|
8
|
+
align-items: center;
|
|
9
|
+
gap: var(--space-2);
|
|
10
|
+
cursor: pointer;
|
|
11
|
+
user-select: none;
|
|
12
|
+
color: var(--fg);
|
|
13
|
+
font-family: var(--label-font);
|
|
14
|
+
font-size: var(--label-md-size);
|
|
15
|
+
letter-spacing: var(--label-tracking);
|
|
16
|
+
line-height: var(--lh-tight);
|
|
17
|
+
}
|
|
18
|
+
.root[data-disabled="true"] {
|
|
19
|
+
cursor: not-allowed;
|
|
20
|
+
opacity: 0.5;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.indicator {
|
|
24
|
+
display: inline-block;
|
|
25
|
+
flex-shrink: 0;
|
|
26
|
+
width: var(--space-4);
|
|
27
|
+
height: var(--space-4);
|
|
28
|
+
border-radius: var(--radius-full);
|
|
29
|
+
box-shadow: inset 0 0 0 1px var(--border-strong);
|
|
30
|
+
background:
|
|
31
|
+
radial-gradient(circle at center, transparent 0, transparent 25%, transparent 26%),
|
|
32
|
+
var(--bg);
|
|
33
|
+
transition:
|
|
34
|
+
box-shadow var(--duration-slow) var(--ease-out),
|
|
35
|
+
background var(--duration-slow) var(--ease-out);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/* Focus */
|
|
39
|
+
.input:focus-visible + .indicator {
|
|
40
|
+
outline: none;
|
|
41
|
+
box-shadow: var(--ring-shadow);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/* Hover — 링만 selected-fg로 은은하게 물듦 (checked면 이미 채워져 있어 무시) */
|
|
45
|
+
.root:hover:not([data-disabled="true"]) .input:not(:checked) + .indicator {
|
|
46
|
+
box-shadow: inset 0 0 0 1px var(--selected-fg);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* Checked — 링 + 안쪽 도트 (radial-gradient) 켬. 색은 Accent 축에 따라 mono=fg, 나머지=primary */
|
|
50
|
+
.input:checked + .indicator {
|
|
51
|
+
box-shadow: inset 0 0 0 1px var(--selected-fg);
|
|
52
|
+
background:
|
|
53
|
+
radial-gradient(circle at center, var(--selected-fg) 0, var(--selected-fg) 30%, transparent 31%),
|
|
54
|
+
var(--bg);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* Standalone indicator (FilterButton 내부) */
|
|
58
|
+
.indicator[data-checked="true"] {
|
|
59
|
+
box-shadow: inset 0 0 0 1px var(--selected-fg);
|
|
60
|
+
background:
|
|
61
|
+
radial-gradient(circle at center, var(--selected-fg) 0, var(--selected-fg) 30%, transparent 31%),
|
|
62
|
+
var(--bg);
|
|
63
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { forwardRef, type InputHTMLAttributes, type ReactNode } from "react";
|
|
4
|
+
|
|
5
|
+
import styles from "./Radio.module.css";
|
|
6
|
+
|
|
7
|
+
export type RadioProps = Omit<InputHTMLAttributes<HTMLInputElement>, "size" | "type"> & {
|
|
8
|
+
children?: ReactNode;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const Radio = forwardRef<HTMLInputElement, RadioProps>(
|
|
12
|
+
({ className, disabled, children, ...rest }, ref) => {
|
|
13
|
+
const rootClass = [styles.root, className ?? ""].filter(Boolean).join(" ");
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<label className={rootClass} data-disabled={disabled ? "true" : "false"}>
|
|
17
|
+
<input
|
|
18
|
+
ref={ref}
|
|
19
|
+
type="radio"
|
|
20
|
+
className={`${styles.input} visually-hidden`}
|
|
21
|
+
disabled={disabled}
|
|
22
|
+
{...rest}
|
|
23
|
+
/>
|
|
24
|
+
<span className={styles.indicator} aria-hidden />
|
|
25
|
+
{children != null && <span>{children}</span>}
|
|
26
|
+
</label>
|
|
27
|
+
);
|
|
28
|
+
},
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
Radio.displayName = "Radio";
|
|
32
|
+
|
|
33
|
+
export { Radio };
|