sh-ui-cli 0.49.0 → 0.50.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.
Files changed (85) hide show
  1. package/data/changelog/versions.json +14 -0
  2. package/data/registry/react/components/accordion/index.vanilla-extract.tsx +97 -0
  3. package/data/registry/react/components/accordion/styles.css.ts +131 -0
  4. package/data/registry/react/components/avatar/index.vanilla-extract.tsx +73 -0
  5. package/data/registry/react/components/avatar/styles.css.ts +68 -0
  6. package/data/registry/react/components/badge/index.vanilla-extract.tsx +40 -0
  7. package/data/registry/react/components/badge/styles.css.ts +71 -0
  8. package/data/registry/react/components/breadcrumb/index.vanilla-extract.tsx +152 -0
  9. package/data/registry/react/components/breadcrumb/styles.css.ts +95 -0
  10. package/data/registry/react/components/calendar/index.vanilla-extract.tsx +806 -0
  11. package/data/registry/react/components/calendar/styles.css.ts +250 -0
  12. package/data/registry/react/components/carousel/index.vanilla-extract.tsx +430 -0
  13. package/data/registry/react/components/carousel/styles.css.ts +169 -0
  14. package/data/registry/react/components/checkbox/index.vanilla-extract.tsx +96 -0
  15. package/data/registry/react/components/checkbox/styles.css.ts +74 -0
  16. package/data/registry/react/components/code-editor/index.vanilla-extract.tsx +230 -0
  17. package/data/registry/react/components/code-editor/styles.css.ts +97 -0
  18. package/data/registry/react/components/code-panel/index.vanilla-extract.tsx +191 -0
  19. package/data/registry/react/components/code-panel/styles.css.ts +151 -0
  20. package/data/registry/react/components/color-picker/index.vanilla-extract.tsx +467 -0
  21. package/data/registry/react/components/color-picker/styles.css.ts +169 -0
  22. package/data/registry/react/components/combobox/index.vanilla-extract.tsx +165 -0
  23. package/data/registry/react/components/combobox/styles.css.ts +174 -0
  24. package/data/registry/react/components/context-menu/index.vanilla-extract.tsx +251 -0
  25. package/data/registry/react/components/context-menu/styles.css.ts +167 -0
  26. package/data/registry/react/components/date-picker/index.vanilla-extract.tsx +520 -0
  27. package/data/registry/react/components/date-picker/styles.css.ts +111 -0
  28. package/data/registry/react/components/dialog/index.vanilla-extract.tsx +95 -0
  29. package/data/registry/react/components/dialog/styles.css.ts +140 -0
  30. package/data/registry/react/components/dropdown-menu/index.vanilla-extract.tsx +255 -0
  31. package/data/registry/react/components/dropdown-menu/styles.css.ts +175 -0
  32. package/data/registry/react/components/file-upload/index.vanilla-extract.tsx +487 -0
  33. package/data/registry/react/components/file-upload/styles.css.ts +193 -0
  34. package/data/registry/react/components/form/index.vanilla-extract.tsx +61 -0
  35. package/data/registry/react/components/form/styles.css.ts +56 -0
  36. package/data/registry/react/components/header/index.vanilla-extract.tsx +805 -0
  37. package/data/registry/react/components/header/styles.css.ts +413 -0
  38. package/data/registry/react/components/label/index.vanilla-extract.tsx +52 -0
  39. package/data/registry/react/components/label/styles.css.ts +141 -0
  40. package/data/registry/react/components/markdown-editor/index.vanilla-extract.tsx +119 -0
  41. package/data/registry/react/components/markdown-editor/styles.css.ts +231 -0
  42. package/data/registry/react/components/menubar/index.vanilla-extract.tsx +32 -0
  43. package/data/registry/react/components/menubar/styles.css.ts +53 -0
  44. package/data/registry/react/components/numeric-input/index.vanilla-extract.tsx +148 -0
  45. package/data/registry/react/components/numeric-input/styles.css.ts +65 -0
  46. package/data/registry/react/components/page-toc/index.vanilla-extract.tsx +174 -0
  47. package/data/registry/react/components/page-toc/styles.css.ts +97 -0
  48. package/data/registry/react/components/pagination/index.vanilla-extract.tsx +269 -0
  49. package/data/registry/react/components/pagination/styles.css.ts +113 -0
  50. package/data/registry/react/components/popover/index.vanilla-extract.tsx +113 -0
  51. package/data/registry/react/components/popover/styles.css.ts +78 -0
  52. package/data/registry/react/components/progress/index.vanilla-extract.tsx +54 -0
  53. package/data/registry/react/components/progress/styles.css.ts +53 -0
  54. package/data/registry/react/components/radio/index.vanilla-extract.tsx +65 -0
  55. package/data/registry/react/components/radio/styles.css.ts +79 -0
  56. package/data/registry/react/components/rich-text-editor/index.vanilla-extract.tsx +348 -0
  57. package/data/registry/react/components/rich-text-editor/styles.css.ts +243 -0
  58. package/data/registry/react/components/select/index.vanilla-extract.tsx +234 -0
  59. package/data/registry/react/components/select/styles.css.ts +225 -0
  60. package/data/registry/react/components/separator/index.vanilla-extract.tsx +46 -0
  61. package/data/registry/react/components/separator/styles.css.ts +24 -0
  62. package/data/registry/react/components/sidebar/index.vanilla-extract.tsx +1067 -0
  63. package/data/registry/react/components/sidebar/styles.css.ts +578 -0
  64. package/data/registry/react/components/skeleton/index.vanilla-extract.tsx +22 -0
  65. package/data/registry/react/components/skeleton/styles.css.ts +30 -0
  66. package/data/registry/react/components/slider/index.vanilla-extract.tsx +298 -0
  67. package/data/registry/react/components/slider/styles.css.ts +75 -0
  68. package/data/registry/react/components/spinner/index.vanilla-extract.tsx +38 -0
  69. package/data/registry/react/components/spinner/styles.css.ts +60 -0
  70. package/data/registry/react/components/switch/index.vanilla-extract.tsx +39 -0
  71. package/data/registry/react/components/switch/styles.css.ts +87 -0
  72. package/data/registry/react/components/tabs/index.vanilla-extract.tsx +91 -0
  73. package/data/registry/react/components/tabs/styles.css.ts +145 -0
  74. package/data/registry/react/components/textarea/index.vanilla-extract.tsx +23 -0
  75. package/data/registry/react/components/textarea/styles.css.ts +55 -0
  76. package/data/registry/react/components/toast/index.vanilla-extract.tsx +258 -0
  77. package/data/registry/react/components/toast/styles.css.ts +307 -0
  78. package/data/registry/react/components/toggle/index.vanilla-extract.tsx +131 -0
  79. package/data/registry/react/components/toggle/styles.css.ts +109 -0
  80. package/data/registry/react/components/tooltip/index.vanilla-extract.tsx +83 -0
  81. package/data/registry/react/components/tooltip/styles.css.ts +59 -0
  82. package/data/registry/react/registry.json +853 -36
  83. package/package.json +1 -1
  84. package/src/api.d.ts +4 -3
  85. package/src/constants.js +4 -3
@@ -2,6 +2,20 @@
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.50.0",
7
+ "date": "2026-05-04",
8
+ "title": "vanilla-extract 전수 롤아웃 + SUPPORTED 승격",
9
+ "type": "minor",
10
+ "highlights": [
11
+ "**vanilla-extract 전수 롤아웃** — v0.49.0 의 button/card/input 파일럿에 이어 나머지 40 개 styled 컴포넌트에 `index.vanilla-extract.tsx` + `styles.css.ts` 추가. 총 43/43 styled 컴포넌트가 plain · tailwind · css-modules · vanilla-extract 4 변종을 모두 갖춤. CSS Modules `.module.css` 를 vanilla-extract `style/styleVariants/keyframes` API 로 자동 변환하는 스크립트로 일괄 처리.",
12
+ "**SUPPORTED 승격** — `CSS_FRAMEWORKS_SUPPORTED` 에 vanilla-extract 추가. CSS_FRAMEWORKS_PLANNED 는 빈 배열 (시스템은 새 변종 추가에 열려 있음). CLI 의 `--cssFramework vanilla-extract`, init 인터랙티브 메뉴, CreateProjectDialog 토글 모두 활성화.",
13
+ "**동적 키 lookup** — 컴포넌트 tsx 의 `styles[\\`badge--${variant}\\`]` 같은 동적 패턴을 처리하기 위해 `byKey: Record<string, string>` 을 css.ts 가 export. 정적 export 만 허용하는 vanilla-extract 의 한계를 우회.",
14
+ "**docs + UI 갱신** — css-framework 가이드의 vanilla-extract 섹션을 SUPPORTED 로 갱신, PLANNED 안내문 일반화, VariantSource 컴포넌트가 4 번째 탭(vanilla-extract) 자동 노출.",
15
+ "원래 사용자 의도였던 \"취향대로 CSS 라이브러리 선택\" 이 plain · tailwind · css-modules · vanilla-extract 4 가지 SUPPORTED 변종으로 fully 달성됨. 시스템은 frameworks 분기 + fallback + 동적 emitter 등 미래 변종 추가에도 열려 있음."
16
+ ],
17
+ "url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.50.0"
18
+ },
5
19
  {
6
20
  "version": "0.49.0",
7
21
  "date": "2026-05-04",
@@ -0,0 +1,97 @@
1
+ import * as React from "react";
2
+ import { Accordion as BaseAccordion } from "@base-ui/react/accordion";
3
+ import { byKey, accordion, accordion__item, accordion__header, accordion__trigger, accordionTriggerLabel, accordion__chevron, accordion__panel, accordion__content } from "./styles.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(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(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={accordion__header}>
58
+ <BaseAccordion.Trigger
59
+ ref={ref}
60
+ className={cn(accordion__trigger, className)}
61
+ {...props}
62
+ >
63
+ <span className={accordionTriggerLabel}>{children}</span>
64
+ <svg
65
+ className={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(accordion__panel, className)}
92
+ {...props}
93
+ >
94
+ <div className={accordion__content}>{children}</div>
95
+ </BaseAccordion.Panel>
96
+ ));
97
+ AccordionContent.displayName = "AccordionContent";
@@ -0,0 +1,131 @@
1
+ import { style } from "@vanilla-extract/css";
2
+
3
+ export const accordion = style({
4
+ display: "flex",
5
+ flexDirection: "column",
6
+ width: "100%",
7
+ selectors: {
8
+ [`&[data-size="sm"] ${accordion__trigger}`]: {
9
+ padding: "var(--space-2) var(--space-1)",
10
+ fontSize: "var(--text-xs)",
11
+ lineHeight: 1.2,
12
+ },
13
+ [`&[data-size="sm"] ${accordion__chevron}`]: {
14
+ width: "12px",
15
+ height: "12px",
16
+ },
17
+ [`&[data-size="sm"] ${accordion__content}`]: {
18
+ padding: "0 var(--space-1) var(--space-2)",
19
+ fontSize: "var(--text-xs)",
20
+ lineHeight: 1.5,
21
+ },
22
+ },
23
+ });
24
+
25
+ export const accordion__item = style({
26
+ borderBottom: "1px solid var(--border)",
27
+ selectors: {
28
+ "&:first-child": {
29
+ borderTop: "1px solid var(--border)",
30
+ },
31
+ },
32
+ });
33
+
34
+ export const accordion__header = style({
35
+ margin: 0,
36
+ font: "inherit",
37
+ });
38
+
39
+ export const accordion__trigger = style({
40
+ display: "flex",
41
+ alignItems: "center",
42
+ justifyContent: "space-between",
43
+ gap: "var(--space-4)",
44
+ width: "100%",
45
+ padding: "var(--space-4) var(--space-1)",
46
+ background: "transparent",
47
+ border: "none",
48
+ color: "var(--foreground)",
49
+ fontSize: "0.9375rem",
50
+ fontWeight: "var(--weight-medium)",
51
+ lineHeight: 1.4,
52
+ textAlign: "left",
53
+ cursor: "pointer",
54
+ transition: "background-color var(--duration-fast) var(--ease-standard)",
55
+ WebkitTapHighlightColor: "transparent",
56
+ selectors: {
57
+ "&:not([disabled]):not([data-disabled]):hover": {
58
+ background: "var(--background-muted)",
59
+ },
60
+ "&:focus-visible": {
61
+ outline: "var(--border-width-strong) solid var(--foreground)",
62
+ outlineOffset: "2px",
63
+ borderRadius: "calc(var(--radius) - 2px)",
64
+ },
65
+ "&[disabled]": {
66
+ cursor: "not-allowed",
67
+ color: "var(--foreground-muted)",
68
+ },
69
+ "&[data-disabled]": {
70
+ cursor: "not-allowed",
71
+ color: "var(--foreground-muted)",
72
+ },
73
+ [`&[data-panel-open] ${accordion__chevron}`]: {
74
+ transform: "rotate(180deg)",
75
+ },
76
+ },
77
+ });
78
+
79
+ export const accordionTriggerLabel = style({
80
+ minWidth: 0,
81
+ overflowWrap: "anywhere",
82
+ });
83
+
84
+ export const accordion__chevron = style({
85
+ flexShrink: 0,
86
+ color: "var(--foreground-muted)",
87
+ transition: "transform 180ms var(--ease-standard)",
88
+ "@media": {
89
+ "(prefers-reduced-motion: reduce)": {
90
+ transition: "none",
91
+ },
92
+ },
93
+ });
94
+
95
+ export const accordion__panel = style({
96
+ overflow: "hidden",
97
+ height: "var(--accordion-panel-height)",
98
+ transition: "height var(--duration-slow) var(--ease-standard)",
99
+ selectors: {
100
+ "&[data-starting-style]": {
101
+ height: 0,
102
+ },
103
+ "&[data-ending-style]": {
104
+ height: 0,
105
+ },
106
+ },
107
+ "@media": {
108
+ "(prefers-reduced-motion: reduce)": {
109
+ transition: "none",
110
+ },
111
+ },
112
+ });
113
+
114
+ export const accordion__content = style({
115
+ padding: "0 var(--space-1) var(--space-4)",
116
+ fontSize: "var(--text-sm)",
117
+ lineHeight: 1.6,
118
+ color: "var(--foreground-muted)",
119
+ });
120
+
121
+ /** 동적 키로 클래스 참조용 — `byKey[\`badge--${variant}\`]` 같은 패턴 지원. */
122
+ export const byKey: Record<string, string> = {
123
+ "accordion": accordion,
124
+ "accordion__item": accordion__item,
125
+ "accordion__header": accordion__header,
126
+ "accordion__trigger": accordion__trigger,
127
+ "accordion__trigger-label": accordionTriggerLabel,
128
+ "accordion__chevron": accordion__chevron,
129
+ "accordion__panel": accordion__panel,
130
+ "accordion__content": accordion__content,
131
+ };
@@ -0,0 +1,73 @@
1
+ import * as React from "react";
2
+ import { Avatar as BaseAvatar } from "@base-ui/react/avatar";
3
+ import { byKey, avatar, avatarSm, avatarMd, avatarLg, avatarXl, avatar__image, avatar__fallback } from "./styles.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(avatar, byKey[`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(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(avatar__fallback, className)}
70
+ {...props}
71
+ />
72
+ );
73
+ });
@@ -0,0 +1,68 @@
1
+ import { style } from "@vanilla-extract/css";
2
+
3
+ export const avatar = style({
4
+ position: "relative",
5
+ display: "inline-flex",
6
+ alignItems: "center",
7
+ justifyContent: "center",
8
+ flexShrink: 0,
9
+ verticalAlign: "middle",
10
+ overflow: "hidden",
11
+ borderRadius: "999px",
12
+ background: "var(--background-muted)",
13
+ color: "var(--foreground-muted)",
14
+ fontWeight: "var(--weight-medium)",
15
+ userSelect: "none",
16
+ });
17
+
18
+ export const avatarSm = style({
19
+ width: "1.75rem",
20
+ height: "1.75rem",
21
+ fontSize: "var(--text-xs)",
22
+ });
23
+
24
+ export const avatarMd = style({
25
+ width: "2.5rem",
26
+ height: "2.5rem",
27
+ fontSize: "0.8125rem",
28
+ });
29
+
30
+ export const avatarLg = style({
31
+ width: "3rem",
32
+ height: "3rem",
33
+ fontSize: "var(--text-sm)",
34
+ });
35
+
36
+ export const avatarXl = style({
37
+ width: "4rem",
38
+ height: "4rem",
39
+ fontSize: "var(--text-base)",
40
+ });
41
+
42
+ export const avatar__image = style({
43
+ width: "100%",
44
+ height: "100%",
45
+ objectFit: "cover",
46
+ display: "block",
47
+ });
48
+
49
+ export const avatar__fallback = style({
50
+ display: "inline-flex",
51
+ alignItems: "center",
52
+ justifyContent: "center",
53
+ width: "100%",
54
+ height: "100%",
55
+ textTransform: "uppercase",
56
+ letterSpacing: "0.02em",
57
+ });
58
+
59
+ /** 동적 키로 클래스 참조용 — `byKey[\`badge--${variant}\`]` 같은 패턴 지원. */
60
+ export const byKey: Record<string, string> = {
61
+ "avatar": avatar,
62
+ "avatar--sm": avatarSm,
63
+ "avatar--md": avatarMd,
64
+ "avatar--lg": avatarLg,
65
+ "avatar--xl": avatarXl,
66
+ "avatar__image": avatar__image,
67
+ "avatar__fallback": avatar__fallback,
68
+ };
@@ -0,0 +1,40 @@
1
+ import * as React from "react";
2
+ import { byKey, badge, badgeSm, badgeMd, badgePrimary, badgeSecondary, badgeSuccess, badgeWarning, badgeDanger, badgeOutline } from "./styles.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
+ badge,
32
+ byKey[`badge--${variant}`],
33
+ byKey[`badge--${size}`],
34
+ className,
35
+ )}
36
+ {...props}
37
+ />
38
+ );
39
+ },
40
+ );
@@ -0,0 +1,71 @@
1
+ import { style } from "@vanilla-extract/css";
2
+
3
+ export const badge = style({
4
+ display: "inline-flex",
5
+ alignItems: "center",
6
+ gap: "0.25rem",
7
+ padding: "0 0.5rem",
8
+ border: "1px solid transparent",
9
+ borderRadius: "999px",
10
+ fontWeight: "var(--weight-medium)",
11
+ lineHeight: 1,
12
+ whiteSpace: "nowrap",
13
+ verticalAlign: "middle",
14
+ userSelect: "none",
15
+ });
16
+
17
+ export const badgeSm = style({
18
+ height: "1.25rem",
19
+ fontSize: "0.6875rem",
20
+ padding: "0 0.375rem",
21
+ });
22
+
23
+ export const badgeMd = style({
24
+ height: "1.5rem",
25
+ fontSize: "var(--text-xs)",
26
+ });
27
+
28
+ export const badgePrimary = style({
29
+ background: "var(--primary)",
30
+ color: "var(--primary-foreground)",
31
+ });
32
+
33
+ export const badgeSecondary = style({
34
+ background: "var(--background-muted)",
35
+ color: "var(--foreground)",
36
+ borderColor: "var(--border)",
37
+ });
38
+
39
+ export const badgeSuccess = style({
40
+ background: "var(--success, #16a34a)",
41
+ color: "#fff",
42
+ });
43
+
44
+ export const badgeWarning = style({
45
+ background: "var(--warning, #d97706)",
46
+ color: "#fff",
47
+ });
48
+
49
+ export const badgeDanger = style({
50
+ background: "var(--danger)",
51
+ color: "var(--danger-foreground, #fff)",
52
+ });
53
+
54
+ export const badgeOutline = style({
55
+ background: "transparent",
56
+ color: "var(--foreground)",
57
+ borderColor: "var(--border-strong)",
58
+ });
59
+
60
+ /** 동적 키로 클래스 참조용 — `byKey[\`badge--${variant}\`]` 같은 패턴 지원. */
61
+ export const byKey: Record<string, string> = {
62
+ "badge": badge,
63
+ "badge--sm": badgeSm,
64
+ "badge--md": badgeMd,
65
+ "badge--primary": badgePrimary,
66
+ "badge--secondary": badgeSecondary,
67
+ "badge--success": badgeSuccess,
68
+ "badge--warning": badgeWarning,
69
+ "badge--danger": badgeDanger,
70
+ "badge--outline": badgeOutline,
71
+ };
@@ -0,0 +1,152 @@
1
+ import * as React from "react";
2
+ import { byKey, breadcrumb, breadcrumb__list, breadcrumb__item, breadcrumb__link, breadcrumb__page, breadcrumb__separator, breadcrumb__ellipsis, breadcrumbEllipsisSr } from "./styles.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(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(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(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(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(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(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(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={breadcrumbEllipsisSr}>더 보기</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
+ }