sh-ui-cli 0.45.3 → 0.47.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/data/changelog/versions.json +26 -0
- package/data/registry/react/components/accordion/index.module.tsx +97 -0
- package/data/registry/react/components/accordion/styles.module.css +111 -0
- package/data/registry/react/components/avatar/index.module.tsx +73 -0
- package/data/registry/react/components/avatar/styles.module.css +36 -0
- package/data/registry/react/components/badge/index.module.tsx +40 -0
- package/data/registry/react/components/badge/styles.module.css +57 -0
- package/data/registry/react/components/breadcrumb/index.module.tsx +152 -0
- package/data/registry/react/components/breadcrumb/styles.module.css +82 -0
- package/data/registry/react/components/button/index.module.tsx +45 -0
- package/data/registry/react/components/button/styles.module.css +92 -0
- package/data/registry/react/components/calendar/index.module.tsx +806 -0
- package/data/registry/react/components/calendar/styles.module.css +213 -0
- package/data/registry/react/components/card/index.module.tsx +63 -0
- package/data/registry/react/components/card/styles.module.css +73 -0
- package/data/registry/react/components/carousel/index.module.tsx +430 -0
- package/data/registry/react/components/carousel/styles.module.css +155 -0
- package/data/registry/react/components/checkbox/index.module.tsx +96 -0
- package/data/registry/react/components/checkbox/styles.module.css +75 -0
- package/data/registry/react/components/code-editor/index.module.tsx +230 -0
- package/data/registry/react/components/code-editor/styles.module.css +76 -0
- package/data/registry/react/components/code-panel/index.module.tsx +191 -0
- package/data/registry/react/components/code-panel/styles.module.css +124 -0
- package/data/registry/react/components/color-picker/index.module.tsx +467 -0
- package/data/registry/react/components/color-picker/styles.module.css +166 -0
- package/data/registry/react/components/combobox/index.module.tsx +165 -0
- package/data/registry/react/components/combobox/styles.module.css +151 -0
- package/data/registry/react/components/context-menu/index.module.tsx +251 -0
- package/data/registry/react/components/context-menu/styles.module.css +140 -0
- package/data/registry/react/components/date-picker/index.module.tsx +520 -0
- package/data/registry/react/components/date-picker/styles.module.css +103 -0
- package/data/registry/react/components/dialog/index.module.tsx +95 -0
- package/data/registry/react/components/dialog/styles.module.css +127 -0
- package/data/registry/react/components/dropdown-menu/index.module.tsx +255 -0
- package/data/registry/react/components/dropdown-menu/styles.module.css +150 -0
- package/data/registry/react/components/file-upload/index.module.tsx +487 -0
- package/data/registry/react/components/file-upload/styles.module.css +170 -0
- package/data/registry/react/components/form/index.module.tsx +61 -0
- package/data/registry/react/components/form/styles.module.css +47 -0
- package/data/registry/react/components/header/index.module.tsx +805 -0
- package/data/registry/react/components/header/styles.module.css +350 -0
- package/data/registry/react/components/input/index.module.tsx +486 -0
- package/data/registry/react/components/input/styles.module.css +200 -0
- package/data/registry/react/components/label/index.module.tsx +52 -0
- package/data/registry/react/components/label/styles.module.css +90 -0
- package/data/registry/react/components/markdown-editor/index.module.tsx +119 -0
- package/data/registry/react/components/markdown-editor/styles.module.css +160 -0
- package/data/registry/react/components/menubar/index.module.tsx +32 -0
- package/data/registry/react/components/menubar/styles.module.css +45 -0
- package/data/registry/react/components/numeric-input/index.module.tsx +148 -0
- package/data/registry/react/components/numeric-input/styles.module.css +56 -0
- package/data/registry/react/components/page-toc/index.module.tsx +174 -0
- package/data/registry/react/components/page-toc/styles.module.css +82 -0
- package/data/registry/react/components/pagination/index.module.tsx +269 -0
- package/data/registry/react/components/pagination/styles.module.css +105 -0
- package/data/registry/react/components/popover/index.module.tsx +113 -0
- package/data/registry/react/components/popover/styles.module.css +65 -0
- package/data/registry/react/components/progress/index.module.tsx +54 -0
- package/data/registry/react/components/progress/styles.module.css +41 -0
- package/data/registry/react/components/radio/index.module.tsx +65 -0
- package/data/registry/react/components/radio/styles.module.css +80 -0
- package/data/registry/react/components/rich-text-editor/index.module.tsx +348 -0
- package/data/registry/react/components/rich-text-editor/styles.module.css +196 -0
- package/data/registry/react/components/select/index.module.tsx +234 -0
- package/data/registry/react/components/select/styles.module.css +193 -0
- package/data/registry/react/components/separator/index.module.tsx +46 -0
- package/data/registry/react/components/separator/styles.module.css +15 -0
- package/data/registry/react/components/sidebar/index.module.tsx +1067 -0
- package/data/registry/react/components/sidebar/styles.module.css +502 -0
- package/data/registry/react/components/skeleton/index.module.tsx +22 -0
- package/data/registry/react/components/skeleton/styles.module.css +24 -0
- package/data/registry/react/components/slider/index.module.tsx +298 -0
- package/data/registry/react/components/slider/styles.module.css +64 -0
- package/data/registry/react/components/spinner/index.module.tsx +38 -0
- package/data/registry/react/components/spinner/styles.module.css +37 -0
- package/data/registry/react/components/switch/index.module.tsx +39 -0
- package/data/registry/react/components/switch/styles.module.css +83 -0
- package/data/registry/react/components/tabs/index.module.tsx +91 -0
- package/data/registry/react/components/tabs/styles.module.css +148 -0
- package/data/registry/react/components/textarea/index.module.tsx +23 -0
- package/data/registry/react/components/textarea/styles.module.css +54 -0
- package/data/registry/react/components/toast/index.module.tsx +258 -0
- package/data/registry/react/components/toast/styles.module.css +290 -0
- package/data/registry/react/components/toggle/index.module.tsx +131 -0
- package/data/registry/react/components/toggle/styles.module.css +85 -0
- package/data/registry/react/components/tooltip/index.module.tsx +83 -0
- package/data/registry/react/components/tooltip/styles.module.css +44 -0
- package/data/registry/react/registry.json +604 -1
- package/data/tokens/build.mjs +4 -0
- package/package.json +1 -1
- package/src/add.mjs +12 -12
- package/src/api.d.ts +4 -3
- package/src/constants.js +4 -3
|
@@ -2,6 +2,32 @@
|
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"$description": "sh-ui 릴리즈 노트 단일 소스. docs(React)와 showcase(Flutter)가 함께 읽는다. 새 릴리즈마다 맨 앞에 추가.",
|
|
4
4
|
"versions": [
|
|
5
|
+
{
|
|
6
|
+
"version": "0.47.0",
|
|
7
|
+
"date": "2026-05-04",
|
|
8
|
+
"title": "CSS Modules 전수 롤아웃 — SUPPORTED 승격",
|
|
9
|
+
"type": "minor",
|
|
10
|
+
"highlights": [
|
|
11
|
+
"**css-modules 전 styled 컴포넌트 변종 보유** — v0.46.0 의 button/card/input 파일럿에 이어 나머지 40 개 styled 컴포넌트(accordion, avatar, badge, calendar, carousel, checkbox, color-picker, combobox, context-menu, date-picker, dialog, dropdown-menu, file-upload, form, header, label, menubar, popover, progress, radio, select, sidebar, skeleton, slider, spinner, switch, tabs, textarea, toast, toggle, tooltip 외) 에 `index.module.tsx` + `styles.module.css` 추가. 총 43/43 styled 컴포넌트가 plain · tailwind · css-modules 3 변종을 모두 갖춤.",
|
|
12
|
+
"**css-modules SUPPORTED 승격** — `CSS_FRAMEWORKS_SUPPORTED` 에 추가, `CSS_FRAMEWORKS_PLANNED` 에서 제거. CLI 의 `--cssFramework css-modules` 와 init 인터랙티브 메뉴에서 정상 선택 가능. CreateProjectDialog 의 토글에서도 활성화됨 (vanilla-extract 만 disabled).",
|
|
13
|
+
"**docs 페이지 업데이트** — `apps/docs/(docs)/css-framework/page.tsx` 에 css-modules 섹션 추가 (.module.css + styles.X 사용 예시·적합한 경우·내부 동작). PLANNED 섹션은 vanilla-extract 만 남김. fallback 메시지·registry.json 예시도 css-modules 분기 포함하도록 갱신.",
|
|
14
|
+
"**Fallback 메시지 일반화** — 기존 `Tailwind 변종 미제공, plain 변종으로 설치 (Tailwind v4 환경에서 그대로 동작)` → `<framework> 변종 미제공, plain 변종으로 설치 (어떤 환경에서도 그대로 동작)`. 이전 마이너 (v0.46.0) 에서 `effectiveFramework` 일반화한 효과가 사용자 안내 문구에까지 반영."
|
|
15
|
+
],
|
|
16
|
+
"url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.47.0"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"version": "0.46.0",
|
|
20
|
+
"date": "2026-05-04",
|
|
21
|
+
"title": "CSS Modules 변종 파일럿 — button/card/input",
|
|
22
|
+
"type": "minor",
|
|
23
|
+
"highlights": [
|
|
24
|
+
"**CSS Modules 변종 파일럿** — `button`, `card`, `input` 에 `index.module.tsx` + `styles.module.css` 변종 추가. `sh-ui.config.json` 의 `cssFramework` 를 `\"css-modules\"` 로 두면 CLI 가 `import styles from \"./styles.module.css\"` 형태로 설치하고, 클래스 이름은 모듈 해시로 격리됨. 토큰은 plain/tailwind 와 동일한 `tokens.css` (`:root` CSS custom properties) 를 공유.",
|
|
25
|
+
"**Fallback 일반화** — 기존엔 tailwind 한정이던 `effectiveFramework` 가 모든 변종 공통으로 동작. `cssFramework` 에 변종이 없는 컴포넌트는 plain 으로 자동 fallback 되며 `ℹ <name> — <fw> 변종 미제공, plain 변종으로 설치` 한 줄 안내 출력. 점진적 rollout 패턴 그대로 — 컴포넌트마다 변종을 갖출 필요 없이 가능한 것부터 제공.",
|
|
26
|
+
"**`utils` 의 frameworks 분기** — `lib/cn.ts` 가 `[\"plain\", \"css-modules\"]` 로 매칭. CSS Modules 환경도 zero-dep `cn` 을 그대로 공유 (clsx/tailwind-merge 가 필요한 건 tailwind 변종뿐).",
|
|
27
|
+
"css-modules 는 여전히 `CSS_FRAMEWORKS_PLANNED` 유지 — 파일럿 3개만 변종이 있어 UI 노출은 다음 라운드에서 전체 컴포넌트 롤아웃 후 SUPPORTED 로 승격. 지금도 사용자가 직접 `sh-ui.config.json` 을 손대면 동작."
|
|
28
|
+
],
|
|
29
|
+
"url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.46.0"
|
|
30
|
+
},
|
|
5
31
|
{
|
|
6
32
|
"version": "0.45.3",
|
|
7
33
|
"date": "2026-04-30",
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Accordion as BaseAccordion } from "@base-ui/react/accordion";
|
|
3
|
+
import styles from "./styles.module.css";
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
import { cn } from "@SH_UI_UTILS@";
|
|
7
|
+
type WithStringClassName<T> = Omit<T, "className"> & { className?: string };
|
|
8
|
+
|
|
9
|
+
export type AccordionSize = "sm" | "md";
|
|
10
|
+
|
|
11
|
+
type AccordionProps = WithStringClassName<
|
|
12
|
+
React.ComponentPropsWithoutRef<typeof BaseAccordion.Root>
|
|
13
|
+
> & {
|
|
14
|
+
/**
|
|
15
|
+
* 트리거 + chevron + content 의 패딩·폰트 크기 묶음.
|
|
16
|
+
* - `md` (기본) — padding 16/4, font 15px, chevron 16px
|
|
17
|
+
* - `sm` — padding 8/4, font 12px, chevron 12px. 좁은 사이드바·다중 섹션에 적합.
|
|
18
|
+
* @default "md"
|
|
19
|
+
*/
|
|
20
|
+
size?: AccordionSize;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const Accordion = React.forwardRef<HTMLDivElement, AccordionProps>(
|
|
24
|
+
({ className, size = "md", ...props }, ref) => (
|
|
25
|
+
<BaseAccordion.Root
|
|
26
|
+
ref={ref}
|
|
27
|
+
className={cn(styles.accordion, className)}
|
|
28
|
+
data-size={size}
|
|
29
|
+
{...props}
|
|
30
|
+
/>
|
|
31
|
+
),
|
|
32
|
+
);
|
|
33
|
+
Accordion.displayName = "Accordion";
|
|
34
|
+
|
|
35
|
+
export const AccordionItem = React.forwardRef<
|
|
36
|
+
HTMLDivElement,
|
|
37
|
+
WithStringClassName<React.ComponentPropsWithoutRef<typeof BaseAccordion.Item>>
|
|
38
|
+
>(({ className, ...props }, ref) => (
|
|
39
|
+
<BaseAccordion.Item
|
|
40
|
+
ref={ref}
|
|
41
|
+
className={cn(styles.accordion__item, className)}
|
|
42
|
+
{...props}
|
|
43
|
+
/>
|
|
44
|
+
));
|
|
45
|
+
AccordionItem.displayName = "AccordionItem";
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Trigger: 헤더 버튼. 우측에 chevron이 자동으로 붙고 expanded 상태에서 회전한다.
|
|
49
|
+
* Base UI의 AccordionHeader(h3)로 감싸 의미론적 헤더 구조를 유지한다.
|
|
50
|
+
*/
|
|
51
|
+
export const AccordionTrigger = React.forwardRef<
|
|
52
|
+
HTMLButtonElement,
|
|
53
|
+
WithStringClassName<
|
|
54
|
+
React.ComponentPropsWithoutRef<typeof BaseAccordion.Trigger>
|
|
55
|
+
>
|
|
56
|
+
>(({ className, children, ...props }, ref) => (
|
|
57
|
+
<BaseAccordion.Header className={styles.accordion__header}>
|
|
58
|
+
<BaseAccordion.Trigger
|
|
59
|
+
ref={ref}
|
|
60
|
+
className={cn(styles.accordion__trigger, className)}
|
|
61
|
+
{...props}
|
|
62
|
+
>
|
|
63
|
+
<span className={styles["accordion__trigger-label"]}>{children}</span>
|
|
64
|
+
<svg
|
|
65
|
+
className={styles.accordion__chevron}
|
|
66
|
+
width="16"
|
|
67
|
+
height="16"
|
|
68
|
+
viewBox="0 0 16 16"
|
|
69
|
+
fill="none"
|
|
70
|
+
aria-hidden="true"
|
|
71
|
+
>
|
|
72
|
+
<path
|
|
73
|
+
d="M4 6l4 4 4-4"
|
|
74
|
+
stroke="currentColor"
|
|
75
|
+
strokeWidth="1.5"
|
|
76
|
+
strokeLinecap="round"
|
|
77
|
+
strokeLinejoin="round"
|
|
78
|
+
/>
|
|
79
|
+
</svg>
|
|
80
|
+
</BaseAccordion.Trigger>
|
|
81
|
+
</BaseAccordion.Header>
|
|
82
|
+
));
|
|
83
|
+
AccordionTrigger.displayName = "AccordionTrigger";
|
|
84
|
+
|
|
85
|
+
export const AccordionContent = React.forwardRef<
|
|
86
|
+
HTMLDivElement,
|
|
87
|
+
WithStringClassName<React.ComponentPropsWithoutRef<typeof BaseAccordion.Panel>>
|
|
88
|
+
>(({ className, children, ...props }, ref) => (
|
|
89
|
+
<BaseAccordion.Panel
|
|
90
|
+
ref={ref}
|
|
91
|
+
className={cn(styles.accordion__panel, className)}
|
|
92
|
+
{...props}
|
|
93
|
+
>
|
|
94
|
+
<div className={styles.accordion__content}>{children}</div>
|
|
95
|
+
</BaseAccordion.Panel>
|
|
96
|
+
));
|
|
97
|
+
AccordionContent.displayName = "AccordionContent";
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
.accordion {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
width: 100%;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.accordion__item {
|
|
8
|
+
border-bottom: 1px solid var(--border);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.accordion__item:first-child {
|
|
12
|
+
border-top: 1px solid var(--border);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.accordion__header {
|
|
16
|
+
margin: 0;
|
|
17
|
+
font: inherit;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.accordion__trigger {
|
|
21
|
+
display: flex;
|
|
22
|
+
align-items: center;
|
|
23
|
+
justify-content: space-between;
|
|
24
|
+
gap: var(--space-4);
|
|
25
|
+
width: 100%;
|
|
26
|
+
padding: var(--space-4) var(--space-1);
|
|
27
|
+
background: transparent;
|
|
28
|
+
border: none;
|
|
29
|
+
color: var(--foreground);
|
|
30
|
+
font-size: 0.9375rem;
|
|
31
|
+
font-weight: var(--weight-medium);
|
|
32
|
+
line-height: 1.4;
|
|
33
|
+
text-align: left;
|
|
34
|
+
cursor: pointer;
|
|
35
|
+
transition: background-color var(--duration-fast) var(--ease-standard);
|
|
36
|
+
-webkit-tap-highlight-color: transparent;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/* hover는 enabled 일 때만. background tint — 다른 hover 효과를 원하면 className/style 로 override. */
|
|
40
|
+
.accordion__trigger:not([disabled]):not([data-disabled]):hover {
|
|
41
|
+
background: var(--background-muted);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.accordion__trigger:focus-visible {
|
|
45
|
+
outline: var(--border-width-strong) solid var(--foreground);
|
|
46
|
+
outline-offset: 2px;
|
|
47
|
+
border-radius: calc(var(--radius) - 2px);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.accordion__trigger[disabled],
|
|
51
|
+
.accordion__trigger[data-disabled] {
|
|
52
|
+
cursor: not-allowed;
|
|
53
|
+
color: var(--foreground-muted);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.accordion__trigger-label {
|
|
57
|
+
min-width: 0;
|
|
58
|
+
overflow-wrap: anywhere;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.accordion__chevron {
|
|
62
|
+
flex-shrink: 0;
|
|
63
|
+
color: var(--foreground-muted);
|
|
64
|
+
transition: transform 180ms var(--ease-standard);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.accordion__trigger[data-panel-open] .accordion__chevron {
|
|
68
|
+
transform: rotate(180deg);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/* 패널: Base UI가 --accordion-panel-height CSS 변수를 제공. grid trick으로 열림/닫힘 전환. */
|
|
72
|
+
.accordion__panel {
|
|
73
|
+
overflow: hidden;
|
|
74
|
+
height: var(--accordion-panel-height);
|
|
75
|
+
transition: height var(--duration-slow) var(--ease-standard);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.accordion__panel[data-starting-style],
|
|
79
|
+
.accordion__panel[data-ending-style] {
|
|
80
|
+
height: 0;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.accordion__content {
|
|
84
|
+
padding: 0 var(--space-1) var(--space-4);
|
|
85
|
+
font-size: var(--text-sm);
|
|
86
|
+
line-height: 1.6;
|
|
87
|
+
color: var(--foreground-muted);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/* size="sm" — 좁은 사이드바·다중 섹션에 적합한 컴팩트 변형. */
|
|
91
|
+
.accordion[data-size="sm"] .accordion__trigger {
|
|
92
|
+
padding: var(--space-2) var(--space-1);
|
|
93
|
+
font-size: var(--text-xs);
|
|
94
|
+
line-height: 1.2;
|
|
95
|
+
}
|
|
96
|
+
.accordion[data-size="sm"] .accordion__chevron {
|
|
97
|
+
width: 12px;
|
|
98
|
+
height: 12px;
|
|
99
|
+
}
|
|
100
|
+
.accordion[data-size="sm"] .accordion__content {
|
|
101
|
+
padding: 0 var(--space-1) var(--space-2);
|
|
102
|
+
font-size: var(--text-xs);
|
|
103
|
+
line-height: 1.5;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
@media (prefers-reduced-motion: reduce) {
|
|
107
|
+
.accordion__panel,
|
|
108
|
+
.accordion__chevron {
|
|
109
|
+
transition: none;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Avatar as BaseAvatar } from "@base-ui/react/avatar";
|
|
3
|
+
import styles from "./styles.module.css";
|
|
4
|
+
|
|
5
|
+
import { cn } from "@SH_UI_UTILS@";
|
|
6
|
+
type WithStringClassName<T> = Omit<T, "className"> & { className?: string };
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
export type AvatarSize = "sm" | "md" | "lg" | "xl";
|
|
10
|
+
|
|
11
|
+
export interface AvatarProps
|
|
12
|
+
extends WithStringClassName<
|
|
13
|
+
React.ComponentPropsWithoutRef<typeof BaseAvatar.Root>
|
|
14
|
+
> {
|
|
15
|
+
/**
|
|
16
|
+
* 크기.
|
|
17
|
+
* - `sm` (24px) — 댓글·리스트 행
|
|
18
|
+
* - `md` (32px) — 일반 (기본)
|
|
19
|
+
* - `lg` (40px) — 헤더·프로필 카드
|
|
20
|
+
* - `xl` (56px) — 프로필 페이지
|
|
21
|
+
*
|
|
22
|
+
* @default "md"
|
|
23
|
+
*/
|
|
24
|
+
size?: AvatarSize;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 사용자/엔티티를 대표하는 원형 이미지. `Avatar` 안에 `AvatarImage`와
|
|
29
|
+
* `AvatarFallback`을 함께 둬, 이미지 로드 실패 시 자동으로 fallback이 표시되도록 한다.
|
|
30
|
+
*/
|
|
31
|
+
export const Avatar = React.forwardRef<HTMLSpanElement, AvatarProps>(
|
|
32
|
+
function Avatar({ className, size = "md", ...props }, ref) {
|
|
33
|
+
return (
|
|
34
|
+
<BaseAvatar.Root
|
|
35
|
+
ref={ref}
|
|
36
|
+
className={cn(styles.avatar, styles[`avatar--${size}`], className)}
|
|
37
|
+
{...props}
|
|
38
|
+
/>
|
|
39
|
+
);
|
|
40
|
+
},
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
/** Avatar 내부의 실제 이미지. 로드 실패 시 자동으로 가려지고 fallback이 노출된다. */
|
|
44
|
+
export const AvatarImage = React.forwardRef<
|
|
45
|
+
HTMLImageElement,
|
|
46
|
+
WithStringClassName<
|
|
47
|
+
React.ComponentPropsWithoutRef<typeof BaseAvatar.Image>
|
|
48
|
+
>
|
|
49
|
+
>(function AvatarImage({ className, ...props }, ref) {
|
|
50
|
+
return (
|
|
51
|
+
<BaseAvatar.Image
|
|
52
|
+
ref={ref}
|
|
53
|
+
className={cn(styles.avatar__image, className)}
|
|
54
|
+
{...props}
|
|
55
|
+
/>
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
/** 이미지가 로드되지 않을 때 표시되는 대체 콘텐츠. 이니셜이나 아이콘을 권장. */
|
|
60
|
+
export const AvatarFallback = React.forwardRef<
|
|
61
|
+
HTMLSpanElement,
|
|
62
|
+
WithStringClassName<
|
|
63
|
+
React.ComponentPropsWithoutRef<typeof BaseAvatar.Fallback>
|
|
64
|
+
>
|
|
65
|
+
>(function AvatarFallback({ className, ...props }, ref) {
|
|
66
|
+
return (
|
|
67
|
+
<BaseAvatar.Fallback
|
|
68
|
+
ref={ref}
|
|
69
|
+
className={cn(styles.avatar__fallback, className)}
|
|
70
|
+
{...props}
|
|
71
|
+
/>
|
|
72
|
+
);
|
|
73
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
.avatar {
|
|
2
|
+
position: relative;
|
|
3
|
+
display: inline-flex;
|
|
4
|
+
align-items: center;
|
|
5
|
+
justify-content: center;
|
|
6
|
+
flex-shrink: 0;
|
|
7
|
+
vertical-align: middle;
|
|
8
|
+
overflow: hidden;
|
|
9
|
+
border-radius: 999px;
|
|
10
|
+
background: var(--background-muted);
|
|
11
|
+
color: var(--foreground-muted);
|
|
12
|
+
font-weight: var(--weight-medium);
|
|
13
|
+
user-select: none;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.avatar--sm { width: 1.75rem; height: 1.75rem; font-size: var(--text-xs); }
|
|
17
|
+
.avatar--md { width: 2.5rem; height: 2.5rem; font-size: 0.8125rem; }
|
|
18
|
+
.avatar--lg { width: 3rem; height: 3rem; font-size: var(--text-sm); }
|
|
19
|
+
.avatar--xl { width: 4rem; height: 4rem; font-size: var(--text-base); }
|
|
20
|
+
|
|
21
|
+
.avatar__image {
|
|
22
|
+
width: 100%;
|
|
23
|
+
height: 100%;
|
|
24
|
+
object-fit: cover;
|
|
25
|
+
display: block;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.avatar__fallback {
|
|
29
|
+
display: inline-flex;
|
|
30
|
+
align-items: center;
|
|
31
|
+
justify-content: center;
|
|
32
|
+
width: 100%;
|
|
33
|
+
height: 100%;
|
|
34
|
+
text-transform: uppercase;
|
|
35
|
+
letter-spacing: 0.02em;
|
|
36
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import styles from "./styles.module.css";
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
import { cn } from "@SH_UI_UTILS@";
|
|
6
|
+
export type BadgeVariant =
|
|
7
|
+
| "primary"
|
|
8
|
+
| "secondary"
|
|
9
|
+
| "success"
|
|
10
|
+
| "warning"
|
|
11
|
+
| "danger"
|
|
12
|
+
| "outline";
|
|
13
|
+
|
|
14
|
+
export type BadgeSize = "sm" | "md";
|
|
15
|
+
|
|
16
|
+
export interface BadgeProps extends React.HTMLAttributes<HTMLSpanElement> {
|
|
17
|
+
variant?: BadgeVariant;
|
|
18
|
+
size?: BadgeSize;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 상태·카테고리·수량 등을 짧게 표기하는 인라인 라벨. 의미 전달이 색에만
|
|
23
|
+
* 의존하지 않도록 텍스트나 아이콘과 함께 사용할 것.
|
|
24
|
+
*/
|
|
25
|
+
export const Badge = React.forwardRef<HTMLSpanElement, BadgeProps>(
|
|
26
|
+
function Badge({ className, variant = "primary", size = "md", ...props }, ref) {
|
|
27
|
+
return (
|
|
28
|
+
<span
|
|
29
|
+
ref={ref}
|
|
30
|
+
className={cn(
|
|
31
|
+
styles.badge,
|
|
32
|
+
styles[`badge--${variant}`],
|
|
33
|
+
styles[`badge--${size}`],
|
|
34
|
+
className,
|
|
35
|
+
)}
|
|
36
|
+
{...props}
|
|
37
|
+
/>
|
|
38
|
+
);
|
|
39
|
+
},
|
|
40
|
+
);
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
.badge {
|
|
2
|
+
display: inline-flex;
|
|
3
|
+
align-items: center;
|
|
4
|
+
gap: 0.25rem;
|
|
5
|
+
padding: 0 0.5rem;
|
|
6
|
+
border: 1px solid transparent;
|
|
7
|
+
border-radius: 999px;
|
|
8
|
+
font-weight: var(--weight-medium);
|
|
9
|
+
line-height: 1;
|
|
10
|
+
white-space: nowrap;
|
|
11
|
+
vertical-align: middle;
|
|
12
|
+
user-select: none;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/* sizes */
|
|
16
|
+
.badge--sm {
|
|
17
|
+
height: 1.25rem;
|
|
18
|
+
font-size: 0.6875rem;
|
|
19
|
+
padding: 0 0.375rem;
|
|
20
|
+
}
|
|
21
|
+
.badge--md {
|
|
22
|
+
height: 1.5rem;
|
|
23
|
+
font-size: var(--text-xs);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/* variants */
|
|
27
|
+
.badge--primary {
|
|
28
|
+
background: var(--primary);
|
|
29
|
+
color: var(--primary-foreground);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.badge--secondary {
|
|
33
|
+
background: var(--background-muted);
|
|
34
|
+
color: var(--foreground);
|
|
35
|
+
border-color: var(--border);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.badge--success {
|
|
39
|
+
background: var(--success, #16a34a);
|
|
40
|
+
color: #fff;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.badge--warning {
|
|
44
|
+
background: var(--warning, #d97706);
|
|
45
|
+
color: #fff;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.badge--danger {
|
|
49
|
+
background: var(--danger);
|
|
50
|
+
color: var(--danger-foreground, #fff);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.badge--outline {
|
|
54
|
+
background: transparent;
|
|
55
|
+
color: var(--foreground);
|
|
56
|
+
border-color: var(--border-strong);
|
|
57
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import styles from "./styles.module.css";
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
import { cn } from "@SH_UI_UTILS@";
|
|
6
|
+
/* ───────── Breadcrumb (nav) ─────────
|
|
7
|
+
* 시맨틱: <nav aria-label="Breadcrumb"><ol>...</ol></nav>.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 현재 페이지의 위치를 사이트 계층 위에서 보여주는 내비게이션. 항상 `BreadcrumbList`로
|
|
12
|
+
* 감싸고, 마지막 항목은 링크 대신 `BreadcrumbPage`로 표기해 현재 위치를 알린다.
|
|
13
|
+
*/
|
|
14
|
+
export const Breadcrumb = React.forwardRef<
|
|
15
|
+
HTMLElement,
|
|
16
|
+
React.HTMLAttributes<HTMLElement>
|
|
17
|
+
>(function Breadcrumb({ className, ...props }, ref) {
|
|
18
|
+
return (
|
|
19
|
+
<nav
|
|
20
|
+
ref={ref}
|
|
21
|
+
aria-label="Breadcrumb"
|
|
22
|
+
className={cn(styles.breadcrumb, className)}
|
|
23
|
+
{...props}
|
|
24
|
+
/>
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
/* ───────── List (ol) ───────── */
|
|
29
|
+
|
|
30
|
+
/** 항목들을 담는 정렬 리스트(`<ol>`). Breadcrumb 직계 자식으로 사용. */
|
|
31
|
+
export const BreadcrumbList = React.forwardRef<
|
|
32
|
+
HTMLOListElement,
|
|
33
|
+
React.OlHTMLAttributes<HTMLOListElement>
|
|
34
|
+
>(function BreadcrumbList({ className, ...props }, ref) {
|
|
35
|
+
return (
|
|
36
|
+
<ol
|
|
37
|
+
ref={ref}
|
|
38
|
+
className={cn(styles.breadcrumb__list, className)}
|
|
39
|
+
{...props}
|
|
40
|
+
/>
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
/* ───────── Item (li) ───────── */
|
|
45
|
+
|
|
46
|
+
/** 한 단계의 항목(`<li>`). 안에 `BreadcrumbLink` 또는 `BreadcrumbPage`를 둔다. */
|
|
47
|
+
export const BreadcrumbItem = React.forwardRef<
|
|
48
|
+
HTMLLIElement,
|
|
49
|
+
React.LiHTMLAttributes<HTMLLIElement>
|
|
50
|
+
>(function BreadcrumbItem({ className, ...props }, ref) {
|
|
51
|
+
return (
|
|
52
|
+
<li
|
|
53
|
+
ref={ref}
|
|
54
|
+
className={cn(styles.breadcrumb__item, className)}
|
|
55
|
+
{...props}
|
|
56
|
+
/>
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
/* ───────── Link ───────── */
|
|
61
|
+
|
|
62
|
+
/** 상위 단계로 이동하는 링크. 라우터 사용 시 `asChild` 패턴 대신 직접 `<a>` 속성으로 전달. */
|
|
63
|
+
export const BreadcrumbLink = React.forwardRef<
|
|
64
|
+
HTMLAnchorElement,
|
|
65
|
+
React.AnchorHTMLAttributes<HTMLAnchorElement>
|
|
66
|
+
>(function BreadcrumbLink({ className, ...props }, ref) {
|
|
67
|
+
return (
|
|
68
|
+
<a
|
|
69
|
+
ref={ref}
|
|
70
|
+
className={cn(styles.breadcrumb__link, className)}
|
|
71
|
+
{...props}
|
|
72
|
+
/>
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
/* ───────── Page (현재 위치 — 링크 아님) ───────── */
|
|
77
|
+
|
|
78
|
+
/** 마지막(현재) 항목. 링크가 아니므로 `aria-current="page"`가 자동 부여된다. */
|
|
79
|
+
export const BreadcrumbPage = React.forwardRef<
|
|
80
|
+
HTMLSpanElement,
|
|
81
|
+
React.HTMLAttributes<HTMLSpanElement>
|
|
82
|
+
>(function BreadcrumbPage({ className, ...props }, ref) {
|
|
83
|
+
return (
|
|
84
|
+
<span
|
|
85
|
+
ref={ref}
|
|
86
|
+
role="link"
|
|
87
|
+
aria-current="page"
|
|
88
|
+
aria-disabled="true"
|
|
89
|
+
className={cn(styles.breadcrumb__page, className)}
|
|
90
|
+
{...props}
|
|
91
|
+
/>
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
/* ───────── Separator ───────── */
|
|
96
|
+
|
|
97
|
+
/** 항목 사이 구분자. 기본은 `>` 아이콘이며 children으로 교체 가능. 스크린리더에서는 무시된다. */
|
|
98
|
+
export const BreadcrumbSeparator = React.forwardRef<
|
|
99
|
+
HTMLLIElement,
|
|
100
|
+
React.LiHTMLAttributes<HTMLLIElement>
|
|
101
|
+
>(function BreadcrumbSeparator({ className, children, ...props }, ref) {
|
|
102
|
+
return (
|
|
103
|
+
<li
|
|
104
|
+
ref={ref}
|
|
105
|
+
role="presentation"
|
|
106
|
+
aria-hidden="true"
|
|
107
|
+
className={cn(styles.breadcrumb__separator, className)}
|
|
108
|
+
{...props}
|
|
109
|
+
>
|
|
110
|
+
{children ?? <ChevronRightIcon />}
|
|
111
|
+
</li>
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
/* ───────── Ellipsis — 중간 항목 축약 ───────── */
|
|
116
|
+
|
|
117
|
+
/** 깊은 경로에서 중간 항목들을 축약하는 점 3개 표시. 클릭 가능한 전체 경로 메뉴와 함께 쓰면 유용. */
|
|
118
|
+
export const BreadcrumbEllipsis = React.forwardRef<
|
|
119
|
+
HTMLSpanElement,
|
|
120
|
+
React.HTMLAttributes<HTMLSpanElement>
|
|
121
|
+
>(function BreadcrumbEllipsis({ className, ...props }, ref) {
|
|
122
|
+
return (
|
|
123
|
+
<span
|
|
124
|
+
ref={ref}
|
|
125
|
+
role="presentation"
|
|
126
|
+
aria-hidden="true"
|
|
127
|
+
className={cn(styles.breadcrumb__ellipsis, className)}
|
|
128
|
+
{...props}
|
|
129
|
+
>
|
|
130
|
+
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor" aria-hidden>
|
|
131
|
+
<circle cx="3" cy="8" r="1.25" />
|
|
132
|
+
<circle cx="8" cy="8" r="1.25" />
|
|
133
|
+
<circle cx="13" cy="8" r="1.25" />
|
|
134
|
+
</svg>
|
|
135
|
+
<span className={styles["breadcrumb__ellipsis-sr"]}>더 보기</span>
|
|
136
|
+
</span>
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
function ChevronRightIcon() {
|
|
141
|
+
return (
|
|
142
|
+
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" aria-hidden>
|
|
143
|
+
<path
|
|
144
|
+
d="M6 4l4 4-4 4"
|
|
145
|
+
stroke="currentColor"
|
|
146
|
+
strokeWidth="1.5"
|
|
147
|
+
strokeLinecap="round"
|
|
148
|
+
strokeLinejoin="round"
|
|
149
|
+
/>
|
|
150
|
+
</svg>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
.breadcrumb {
|
|
2
|
+
font-size: var(--text-sm);
|
|
3
|
+
color: var(--foreground-muted);
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
.breadcrumb__list {
|
|
7
|
+
display: flex;
|
|
8
|
+
align-items: center;
|
|
9
|
+
flex-wrap: wrap;
|
|
10
|
+
gap: 0.375rem;
|
|
11
|
+
margin: 0;
|
|
12
|
+
padding: 0;
|
|
13
|
+
list-style: none;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.breadcrumb__item {
|
|
17
|
+
display: inline-flex;
|
|
18
|
+
align-items: center;
|
|
19
|
+
gap: 0.375rem;
|
|
20
|
+
min-width: 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.breadcrumb__link {
|
|
24
|
+
color: var(--foreground-muted);
|
|
25
|
+
text-decoration: none;
|
|
26
|
+
border-radius: calc(var(--radius) - 2px);
|
|
27
|
+
padding: 0 0.125rem;
|
|
28
|
+
transition: color var(--duration-fast);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.breadcrumb__link:hover {
|
|
32
|
+
color: var(--foreground);
|
|
33
|
+
text-decoration: underline;
|
|
34
|
+
text-underline-offset: 3px;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.breadcrumb__link:focus-visible {
|
|
38
|
+
outline: var(--border-width-strong) solid var(--foreground);
|
|
39
|
+
outline-offset: 2px;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.breadcrumb__page {
|
|
43
|
+
color: var(--foreground);
|
|
44
|
+
font-weight: var(--weight-medium);
|
|
45
|
+
overflow: hidden;
|
|
46
|
+
text-overflow: ellipsis;
|
|
47
|
+
white-space: nowrap;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.breadcrumb__separator {
|
|
51
|
+
display: inline-flex;
|
|
52
|
+
align-items: center;
|
|
53
|
+
color: var(--foreground-muted);
|
|
54
|
+
opacity: 0.6;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.breadcrumb__ellipsis {
|
|
58
|
+
display: inline-flex;
|
|
59
|
+
align-items: center;
|
|
60
|
+
width: 1.5rem;
|
|
61
|
+
height: 1.5rem;
|
|
62
|
+
justify-content: center;
|
|
63
|
+
color: var(--foreground-muted);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.breadcrumb__ellipsis-sr {
|
|
67
|
+
position: absolute;
|
|
68
|
+
width: 1px;
|
|
69
|
+
height: 1px;
|
|
70
|
+
padding: 0;
|
|
71
|
+
margin: -1px;
|
|
72
|
+
overflow: hidden;
|
|
73
|
+
clip: rect(0, 0, 0, 0);
|
|
74
|
+
white-space: nowrap;
|
|
75
|
+
border: 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
@media (prefers-reduced-motion: reduce) {
|
|
79
|
+
.breadcrumb__link {
|
|
80
|
+
transition: none;
|
|
81
|
+
}
|
|
82
|
+
}
|