sh-ui-cli 0.45.2 → 0.45.3

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 (91) hide show
  1. package/data/changelog/versions.json +13 -0
  2. package/data/registry/react/components/accordion/index.tailwind.tsx +5 -7
  3. package/data/registry/react/components/accordion/index.tsx +5 -7
  4. package/data/registry/react/components/avatar/index.tailwind.tsx +4 -6
  5. package/data/registry/react/components/avatar/index.tsx +4 -6
  6. package/data/registry/react/components/badge/index.tailwind.tsx +2 -4
  7. package/data/registry/react/components/badge/index.tsx +2 -4
  8. package/data/registry/react/components/breadcrumb/index.tailwind.tsx +8 -10
  9. package/data/registry/react/components/breadcrumb/index.tsx +8 -10
  10. package/data/registry/react/components/button/index.tailwind.tsx +2 -1
  11. package/data/registry/react/components/button/index.tsx +3 -4
  12. package/data/registry/react/components/calendar/index.tailwind.tsx +10 -12
  13. package/data/registry/react/components/calendar/index.tsx +9 -11
  14. package/data/registry/react/components/card/index.tailwind.tsx +8 -10
  15. package/data/registry/react/components/card/index.tsx +8 -10
  16. package/data/registry/react/components/carousel/index.tailwind.tsx +7 -9
  17. package/data/registry/react/components/carousel/index.tsx +7 -9
  18. package/data/registry/react/components/checkbox/index.tailwind.tsx +3 -5
  19. package/data/registry/react/components/checkbox/index.tsx +3 -5
  20. package/data/registry/react/components/code-editor/index.tailwind.tsx +2 -4
  21. package/data/registry/react/components/code-editor/index.tsx +2 -4
  22. package/data/registry/react/components/code-panel/index.tailwind.tsx +5 -7
  23. package/data/registry/react/components/code-panel/index.tsx +5 -7
  24. package/data/registry/react/components/color-picker/index.tailwind.tsx +7 -6
  25. package/data/registry/react/components/color-picker/index.tsx +7 -6
  26. package/data/registry/react/components/combobox/index.tailwind.tsx +8 -10
  27. package/data/registry/react/components/combobox/index.tsx +8 -10
  28. package/data/registry/react/components/context-menu/index.tailwind.tsx +10 -12
  29. package/data/registry/react/components/context-menu/index.tsx +10 -12
  30. package/data/registry/react/components/date-picker/index.tailwind.tsx +7 -9
  31. package/data/registry/react/components/date-picker/index.tsx +7 -9
  32. package/data/registry/react/components/dialog/index.tailwind.tsx +6 -8
  33. package/data/registry/react/components/dialog/index.tsx +6 -8
  34. package/data/registry/react/components/dropdown-menu/index.tailwind.tsx +10 -12
  35. package/data/registry/react/components/dropdown-menu/index.tsx +10 -12
  36. package/data/registry/react/components/file-upload/index.tailwind.tsx +6 -8
  37. package/data/registry/react/components/file-upload/index.tsx +6 -8
  38. package/data/registry/react/components/form/field.tailwind.tsx +2 -1
  39. package/data/registry/react/components/form/field.tsx +2 -3
  40. package/data/registry/react/components/header/index.tailwind.tsx +17 -19
  41. package/data/registry/react/components/header/index.tsx +17 -19
  42. package/data/registry/react/components/input/index.tailwind.tsx +4 -6
  43. package/data/registry/react/components/input/index.tsx +4 -6
  44. package/data/registry/react/components/label/index.tailwind.tsx +6 -8
  45. package/data/registry/react/components/label/index.tsx +6 -8
  46. package/data/registry/react/components/markdown-editor/index.tailwind.tsx +2 -4
  47. package/data/registry/react/components/markdown-editor/index.tsx +2 -4
  48. package/data/registry/react/components/menubar/index.tailwind.tsx +2 -4
  49. package/data/registry/react/components/menubar/index.tsx +2 -4
  50. package/data/registry/react/components/numeric-input/index.tailwind.tsx +2 -4
  51. package/data/registry/react/components/numeric-input/index.tsx +2 -4
  52. package/data/registry/react/components/page-toc/index.tailwind.tsx +3 -2
  53. package/data/registry/react/components/page-toc/index.tsx +2 -3
  54. package/data/registry/react/components/pagination/index.tailwind.tsx +8 -10
  55. package/data/registry/react/components/pagination/index.tsx +8 -10
  56. package/data/registry/react/components/popover/index.tailwind.tsx +4 -6
  57. package/data/registry/react/components/popover/index.tsx +4 -6
  58. package/data/registry/react/components/progress/index.tailwind.tsx +3 -5
  59. package/data/registry/react/components/progress/index.tsx +2 -4
  60. package/data/registry/react/components/radio/index.tailwind.tsx +3 -5
  61. package/data/registry/react/components/radio/index.tsx +3 -5
  62. package/data/registry/react/components/rich-text-editor/index.tailwind.tsx +3 -5
  63. package/data/registry/react/components/rich-text-editor/index.tsx +3 -5
  64. package/data/registry/react/components/select/index.tailwind.tsx +8 -10
  65. package/data/registry/react/components/select/index.tsx +8 -10
  66. package/data/registry/react/components/separator/index.tailwind.tsx +2 -4
  67. package/data/registry/react/components/separator/index.tsx +2 -4
  68. package/data/registry/react/components/sidebar/index.tailwind.tsx +32 -43
  69. package/data/registry/react/components/sidebar/index.tsx +29 -46
  70. package/data/registry/react/components/skeleton/index.tailwind.tsx +2 -4
  71. package/data/registry/react/components/skeleton/index.tsx +2 -4
  72. package/data/registry/react/components/slider/index.tailwind.tsx +5 -7
  73. package/data/registry/react/components/slider/index.tsx +5 -7
  74. package/data/registry/react/components/spinner/index.tailwind.tsx +3 -5
  75. package/data/registry/react/components/spinner/index.tsx +2 -4
  76. package/data/registry/react/components/switch/index.tailwind.tsx +3 -5
  77. package/data/registry/react/components/switch/index.tsx +2 -4
  78. package/data/registry/react/components/tabs/index.tailwind.tsx +6 -8
  79. package/data/registry/react/components/tabs/index.tsx +6 -8
  80. package/data/registry/react/components/textarea/index.tailwind.tsx +2 -4
  81. package/data/registry/react/components/textarea/index.tsx +2 -4
  82. package/data/registry/react/components/toggle/index.tailwind.tsx +4 -6
  83. package/data/registry/react/components/toggle/index.tsx +4 -6
  84. package/data/registry/react/components/tooltip/index.tailwind.tsx +2 -4
  85. package/data/registry/react/components/tooltip/index.tsx +2 -4
  86. package/data/registry/react/lib/cn.tailwind.ts +17 -0
  87. package/data/registry/react/peer-versions.json +3 -1
  88. package/data/registry/react/registry.json +159 -43
  89. package/package.json +1 -1
  90. package/src/add.mjs +25 -1
  91. package/templates/ui-app-template/sh-ui.config.json +5 -0
@@ -2,6 +2,19 @@
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.45.3",
7
+ "date": "2026-04-30",
8
+ "title": "공유 `cn` 유틸 + CSS 프레임워크 가이드 docs",
9
+ "type": "patch",
10
+ "highlights": [
11
+ "**공유 `cn` 유틸** — 컴포넌트 74 곳에 흩어져 있던 로컬 `cx`/`mergeClass` 함수 제거. 모두 `lib/utils.ts` 의 `cn` 으로 통일. plain 변종은 zero-dep custom 구현, tailwind 변종은 `clsx + tailwind-merge` 로 utility 충돌 머지(예: `cn(\"bg-primary\", \"bg-red-500\")` → `\"bg-red-500\"`). registry.json 에 `utils` 엔트리 신설, styled 컴포넌트 43 개가 `registryDependencies: [\"utils\"]` 로 자동 의존 — `add button` 시 `utils` 도 함께 카피.",
12
+ "**`@SH_UI_UTILS@` placeholder 치환** — 컴포넌트가 `import { cn } from \"@SH_UI_UTILS@\"` 로 import 하면 CLI 가 add 시점에 `aliases.utils` 값(예: `@/lib/utils`)으로 치환. cn 유틸 import 경로가 사용자 프로젝트 alias 와 자동 맞아떨어짐. `aliases.utils` 미설정 시 친절 에러로 안내.",
13
+ "**CSS 프레임워크 docs 페이지 추가** — `apps/docs/(docs)/css-framework/page.tsx` 신설. 변종 시스템 큰 그림 (현재 plain/tailwind + 계획 중 css-modules/vanilla-extract) + 모드별 차이·전환·fallback 설명. 사이드바 nav 에 \"CSS 프레임워크\" 링크 추가, cli 페이지에서 cross-link.",
14
+ "**ui-app-template config 보강** — 누락됐던 `aliases.utils` 추가. 신규 init 도 동일하게 채워짐."
15
+ ],
16
+ "url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.45.3"
17
+ },
5
18
  {
6
19
  "version": "0.45.2",
7
20
  "date": "2026-04-30",
@@ -1,10 +1,8 @@
1
1
  import * as React from "react";
2
2
  import { Accordion as BaseAccordion } from "@base-ui/react/accordion";
3
3
 
4
- function cx(...args: (string | undefined | false)[]) {
5
- return args.filter(Boolean).join(" ");
6
- }
7
4
 
5
+ import { cn } from "@SH_UI_UTILS@";
8
6
  type WithStringClassName<T> = Omit<T, "className"> & { className?: string };
9
7
 
10
8
  export type AccordionSize = "sm" | "md";
@@ -19,7 +17,7 @@ export const Accordion = React.forwardRef<HTMLDivElement, AccordionProps>(
19
17
  ({ className, size = "md", ...props }, ref) => (
20
18
  <BaseAccordion.Root
21
19
  ref={ref}
22
- className={cx("flex flex-col w-full", className)}
20
+ className={cn("flex flex-col w-full", className)}
23
21
  data-size={size}
24
22
  {...props}
25
23
  />
@@ -33,7 +31,7 @@ export const AccordionItem = React.forwardRef<
33
31
  >(({ className, ...props }, ref) => (
34
32
  <BaseAccordion.Item
35
33
  ref={ref}
36
- className={cx("border-b border-border first:border-t", className)}
34
+ className={cn("border-b border-border first:border-t", className)}
37
35
  {...props}
38
36
  />
39
37
  ));
@@ -46,7 +44,7 @@ export const AccordionTrigger = React.forwardRef<
46
44
  <BaseAccordion.Header className="m-0 font-[inherit]">
47
45
  <BaseAccordion.Trigger
48
46
  ref={ref}
49
- className={cx(
47
+ className={cn(
50
48
  "flex items-center justify-between gap-[var(--space-4)] w-full px-[var(--space-1)] py-[var(--space-4)] bg-transparent border-none text-foreground text-[0.9375rem] font-medium leading-snug text-left cursor-pointer transition-[background-color] duration-[var(--duration-fast)] hover:not-disabled:not-data-[disabled]:bg-background-muted focus-visible:outline-[length:var(--border-width-strong)] focus-visible:outline-foreground focus-visible:outline-offset-2 focus-visible:rounded-[calc(var(--radius)-2px)] disabled:cursor-not-allowed disabled:text-foreground-muted data-[disabled]:cursor-not-allowed data-[disabled]:text-foreground-muted [[data-size=sm]_&]:py-[var(--space-2)] [[data-size=sm]_&]:text-[length:var(--text-xs)] [[data-size=sm]_&]:leading-[1.2] motion-reduce:transition-none",
51
49
  className,
52
50
  )}
@@ -74,7 +72,7 @@ export const AccordionContent = React.forwardRef<
74
72
  >(({ className, children, ...props }, ref) => (
75
73
  <BaseAccordion.Panel
76
74
  ref={ref}
77
- className={cx(
75
+ className={cn(
78
76
  "overflow-hidden h-[var(--accordion-panel-height)] transition-[height] duration-[var(--duration-slow)] data-[starting-style]:h-0 data-[ending-style]:h-0 motion-reduce:transition-none",
79
77
  className,
80
78
  )}
@@ -2,10 +2,8 @@ import * as React from "react";
2
2
  import { Accordion as BaseAccordion } from "@base-ui/react/accordion";
3
3
  import "./styles.css";
4
4
 
5
- function cx(...args: (string | undefined | false)[]) {
6
- return args.filter(Boolean).join(" ");
7
- }
8
5
 
6
+ import { cn } from "@SH_UI_UTILS@";
9
7
  type WithStringClassName<T> = Omit<T, "className"> & { className?: string };
10
8
 
11
9
  export type AccordionSize = "sm" | "md";
@@ -26,7 +24,7 @@ export const Accordion = React.forwardRef<HTMLDivElement, AccordionProps>(
26
24
  ({ className, size = "md", ...props }, ref) => (
27
25
  <BaseAccordion.Root
28
26
  ref={ref}
29
- className={cx("sh-ui-accordion", className)}
27
+ className={cn("sh-ui-accordion", className)}
30
28
  data-size={size}
31
29
  {...props}
32
30
  />
@@ -40,7 +38,7 @@ export const AccordionItem = React.forwardRef<
40
38
  >(({ className, ...props }, ref) => (
41
39
  <BaseAccordion.Item
42
40
  ref={ref}
43
- className={cx("sh-ui-accordion__item", className)}
41
+ className={cn("sh-ui-accordion__item", className)}
44
42
  {...props}
45
43
  />
46
44
  ));
@@ -59,7 +57,7 @@ export const AccordionTrigger = React.forwardRef<
59
57
  <BaseAccordion.Header className="sh-ui-accordion__header">
60
58
  <BaseAccordion.Trigger
61
59
  ref={ref}
62
- className={cx("sh-ui-accordion__trigger", className)}
60
+ className={cn("sh-ui-accordion__trigger", className)}
63
61
  {...props}
64
62
  >
65
63
  <span className="sh-ui-accordion__trigger-label">{children}</span>
@@ -90,7 +88,7 @@ export const AccordionContent = React.forwardRef<
90
88
  >(({ className, children, ...props }, ref) => (
91
89
  <BaseAccordion.Panel
92
90
  ref={ref}
93
- className={cx("sh-ui-accordion__panel", className)}
91
+ className={cn("sh-ui-accordion__panel", className)}
94
92
  {...props}
95
93
  >
96
94
  <div className="sh-ui-accordion__content">{children}</div>
@@ -2,11 +2,9 @@ import * as React from "react";
2
2
  import { Avatar as BaseAvatar } from "@base-ui/react/avatar";
3
3
  import { cva, type VariantProps } from "class-variance-authority";
4
4
 
5
+ import { cn } from "@SH_UI_UTILS@";
5
6
  type WithStringClassName<T> = Omit<T, "className"> & { className?: string };
6
7
 
7
- function cx(...args: (string | undefined | false | null)[]) {
8
- return args.filter(Boolean).join(" ");
9
- }
10
8
 
11
9
  const avatarVariants = cva(
12
10
  "relative inline-flex items-center justify-center shrink-0 align-middle overflow-hidden rounded-full bg-background-muted text-foreground-muted font-medium select-none",
@@ -37,7 +35,7 @@ export const Avatar = React.forwardRef<HTMLSpanElement, AvatarProps>(
37
35
  return (
38
36
  <BaseAvatar.Root
39
37
  ref={ref}
40
- className={cx(avatarVariants({ size }), className)}
38
+ className={cn(avatarVariants({ size }), className)}
41
39
  {...props}
42
40
  />
43
41
  );
@@ -51,7 +49,7 @@ export const AvatarImage = React.forwardRef<
51
49
  return (
52
50
  <BaseAvatar.Image
53
51
  ref={ref}
54
- className={cx("w-full h-full object-cover block", className)}
52
+ className={cn("w-full h-full object-cover block", className)}
55
53
  {...props}
56
54
  />
57
55
  );
@@ -64,7 +62,7 @@ export const AvatarFallback = React.forwardRef<
64
62
  return (
65
63
  <BaseAvatar.Fallback
66
64
  ref={ref}
67
- className={cx(
65
+ className={cn(
68
66
  "inline-flex items-center justify-center w-full h-full uppercase tracking-[0.02em]",
69
67
  className,
70
68
  )}
@@ -2,11 +2,9 @@ import * as React from "react";
2
2
  import { Avatar as BaseAvatar } from "@base-ui/react/avatar";
3
3
  import "./styles.css";
4
4
 
5
+ import { cn } from "@SH_UI_UTILS@";
5
6
  type WithStringClassName<T> = Omit<T, "className"> & { className?: string };
6
7
 
7
- function cx(...args: (string | undefined | false | null)[]) {
8
- return args.filter(Boolean).join(" ");
9
- }
10
8
 
11
9
  export type AvatarSize = "sm" | "md" | "lg" | "xl";
12
10
 
@@ -35,7 +33,7 @@ export const Avatar = React.forwardRef<HTMLSpanElement, AvatarProps>(
35
33
  return (
36
34
  <BaseAvatar.Root
37
35
  ref={ref}
38
- className={cx("sh-ui-avatar", `sh-ui-avatar--${size}`, className)}
36
+ className={cn("sh-ui-avatar", `sh-ui-avatar--${size}`, className)}
39
37
  {...props}
40
38
  />
41
39
  );
@@ -52,7 +50,7 @@ export const AvatarImage = React.forwardRef<
52
50
  return (
53
51
  <BaseAvatar.Image
54
52
  ref={ref}
55
- className={cx("sh-ui-avatar__image", className)}
53
+ className={cn("sh-ui-avatar__image", className)}
56
54
  {...props}
57
55
  />
58
56
  );
@@ -68,7 +66,7 @@ export const AvatarFallback = React.forwardRef<
68
66
  return (
69
67
  <BaseAvatar.Fallback
70
68
  ref={ref}
71
- className={cx("sh-ui-avatar__fallback", className)}
69
+ className={cn("sh-ui-avatar__fallback", className)}
72
70
  {...props}
73
71
  />
74
72
  );
@@ -1,10 +1,8 @@
1
1
  import * as React from "react";
2
2
  import { cva, type VariantProps } from "class-variance-authority";
3
3
 
4
- function cx(...args: (string | undefined | false | null)[]) {
5
- return args.filter(Boolean).join(" ");
6
- }
7
4
 
5
+ import { cn } from "@SH_UI_UTILS@";
8
6
  const badgeVariants = cva(
9
7
  "inline-flex items-center gap-1 px-2 border border-transparent rounded-full font-medium leading-none whitespace-nowrap align-middle select-none",
10
8
  {
@@ -39,7 +37,7 @@ export const Badge = React.forwardRef<HTMLSpanElement, BadgeProps>(
39
37
  return (
40
38
  <span
41
39
  ref={ref}
42
- className={cx(badgeVariants({ variant, size }), className)}
40
+ className={cn(badgeVariants({ variant, size }), className)}
43
41
  {...props}
44
42
  />
45
43
  );
@@ -1,10 +1,8 @@
1
1
  import * as React from "react";
2
2
  import "./styles.css";
3
3
 
4
- function cx(...args: (string | undefined | false | null)[]) {
5
- return args.filter(Boolean).join(" ");
6
- }
7
4
 
5
+ import { cn } from "@SH_UI_UTILS@";
8
6
  export type BadgeVariant =
9
7
  | "primary"
10
8
  | "secondary"
@@ -29,7 +27,7 @@ export const Badge = React.forwardRef<HTMLSpanElement, BadgeProps>(
29
27
  return (
30
28
  <span
31
29
  ref={ref}
32
- className={cx(
30
+ className={cn(
33
31
  "sh-ui-badge",
34
32
  `sh-ui-badge--${variant}`,
35
33
  `sh-ui-badge--${size}`,
@@ -1,9 +1,7 @@
1
1
  import * as React from "react";
2
2
 
3
- function cx(...args: (string | undefined | false | null)[]) {
4
- return args.filter(Boolean).join(" ");
5
- }
6
3
 
4
+ import { cn } from "@SH_UI_UTILS@";
7
5
  export const Breadcrumb = React.forwardRef<
8
6
  HTMLElement,
9
7
  React.HTMLAttributes<HTMLElement>
@@ -12,7 +10,7 @@ export const Breadcrumb = React.forwardRef<
12
10
  <nav
13
11
  ref={ref}
14
12
  aria-label="Breadcrumb"
15
- className={cx("text-[length:var(--text-sm)] text-foreground-muted", className)}
13
+ className={cn("text-[length:var(--text-sm)] text-foreground-muted", className)}
16
14
  {...props}
17
15
  />
18
16
  );
@@ -25,7 +23,7 @@ export const BreadcrumbList = React.forwardRef<
25
23
  return (
26
24
  <ol
27
25
  ref={ref}
28
- className={cx(
26
+ className={cn(
29
27
  "flex items-center flex-wrap gap-1.5 m-0 p-0 list-none",
30
28
  className,
31
29
  )}
@@ -41,7 +39,7 @@ export const BreadcrumbItem = React.forwardRef<
41
39
  return (
42
40
  <li
43
41
  ref={ref}
44
- className={cx("inline-flex items-center gap-1.5 min-w-0", className)}
42
+ className={cn("inline-flex items-center gap-1.5 min-w-0", className)}
45
43
  {...props}
46
44
  />
47
45
  );
@@ -54,7 +52,7 @@ export const BreadcrumbLink = React.forwardRef<
54
52
  return (
55
53
  <a
56
54
  ref={ref}
57
- className={cx(
55
+ className={cn(
58
56
  "text-foreground-muted no-underline rounded-[calc(var(--radius)-2px)] px-0.5 transition-colors duration-[var(--duration-fast)] hover:text-foreground hover:underline hover:underline-offset-[3px] focus-visible:outline-[length:var(--border-width-strong)] focus-visible:outline-foreground focus-visible:outline-offset-2 motion-reduce:transition-none",
59
57
  className,
60
58
  )}
@@ -73,7 +71,7 @@ export const BreadcrumbPage = React.forwardRef<
73
71
  role="link"
74
72
  aria-current="page"
75
73
  aria-disabled="true"
76
- className={cx(
74
+ className={cn(
77
75
  "text-foreground font-medium overflow-hidden text-ellipsis whitespace-nowrap",
78
76
  className,
79
77
  )}
@@ -91,7 +89,7 @@ export const BreadcrumbSeparator = React.forwardRef<
91
89
  ref={ref}
92
90
  role="presentation"
93
91
  aria-hidden="true"
94
- className={cx(
92
+ className={cn(
95
93
  "inline-flex items-center text-foreground-muted opacity-60",
96
94
  className,
97
95
  )}
@@ -111,7 +109,7 @@ export const BreadcrumbEllipsis = React.forwardRef<
111
109
  ref={ref}
112
110
  role="presentation"
113
111
  aria-hidden="true"
114
- className={cx(
112
+ className={cn(
115
113
  "inline-flex items-center w-6 h-6 justify-center text-foreground-muted",
116
114
  className,
117
115
  )}
@@ -1,10 +1,8 @@
1
1
  import * as React from "react";
2
2
  import "./styles.css";
3
3
 
4
- function cx(...args: (string | undefined | false | null)[]) {
5
- return args.filter(Boolean).join(" ");
6
- }
7
4
 
5
+ import { cn } from "@SH_UI_UTILS@";
8
6
  /* ───────── Breadcrumb (nav) ─────────
9
7
  * 시맨틱: <nav aria-label="Breadcrumb"><ol>...</ol></nav>.
10
8
  */
@@ -21,7 +19,7 @@ export const Breadcrumb = React.forwardRef<
21
19
  <nav
22
20
  ref={ref}
23
21
  aria-label="Breadcrumb"
24
- className={cx("sh-ui-breadcrumb", className)}
22
+ className={cn("sh-ui-breadcrumb", className)}
25
23
  {...props}
26
24
  />
27
25
  );
@@ -37,7 +35,7 @@ export const BreadcrumbList = React.forwardRef<
37
35
  return (
38
36
  <ol
39
37
  ref={ref}
40
- className={cx("sh-ui-breadcrumb__list", className)}
38
+ className={cn("sh-ui-breadcrumb__list", className)}
41
39
  {...props}
42
40
  />
43
41
  );
@@ -53,7 +51,7 @@ export const BreadcrumbItem = React.forwardRef<
53
51
  return (
54
52
  <li
55
53
  ref={ref}
56
- className={cx("sh-ui-breadcrumb__item", className)}
54
+ className={cn("sh-ui-breadcrumb__item", className)}
57
55
  {...props}
58
56
  />
59
57
  );
@@ -69,7 +67,7 @@ export const BreadcrumbLink = React.forwardRef<
69
67
  return (
70
68
  <a
71
69
  ref={ref}
72
- className={cx("sh-ui-breadcrumb__link", className)}
70
+ className={cn("sh-ui-breadcrumb__link", className)}
73
71
  {...props}
74
72
  />
75
73
  );
@@ -88,7 +86,7 @@ export const BreadcrumbPage = React.forwardRef<
88
86
  role="link"
89
87
  aria-current="page"
90
88
  aria-disabled="true"
91
- className={cx("sh-ui-breadcrumb__page", className)}
89
+ className={cn("sh-ui-breadcrumb__page", className)}
92
90
  {...props}
93
91
  />
94
92
  );
@@ -106,7 +104,7 @@ export const BreadcrumbSeparator = React.forwardRef<
106
104
  ref={ref}
107
105
  role="presentation"
108
106
  aria-hidden="true"
109
- className={cx("sh-ui-breadcrumb__separator", className)}
107
+ className={cn("sh-ui-breadcrumb__separator", className)}
110
108
  {...props}
111
109
  >
112
110
  {children ?? <ChevronRightIcon />}
@@ -126,7 +124,7 @@ export const BreadcrumbEllipsis = React.forwardRef<
126
124
  ref={ref}
127
125
  role="presentation"
128
126
  aria-hidden="true"
129
- className={cx("sh-ui-breadcrumb__ellipsis", className)}
127
+ className={cn("sh-ui-breadcrumb__ellipsis", className)}
130
128
  {...props}
131
129
  >
132
130
  <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor" aria-hidden>
@@ -1,4 +1,5 @@
1
1
  import * as React from "react";
2
+ import { cn } from "@SH_UI_UTILS@";
2
3
  import { cva, type VariantProps } from "class-variance-authority";
3
4
 
4
5
  const buttonVariants = cva(
@@ -62,7 +63,7 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
62
63
  ({ variant = "primary", size = "md", className, ...props }, ref) => (
63
64
  <button
64
65
  ref={ref}
65
- className={[buttonVariants({ variant, size }), className].filter(Boolean).join(" ")}
66
+ className={cn(buttonVariants({ variant, size }), className)}
66
67
  {...props}
67
68
  />
68
69
  ),
@@ -1,4 +1,5 @@
1
1
  import * as React from "react";
2
+ import { cn } from "@SH_UI_UTILS@";
2
3
  import "./styles.css";
3
4
 
4
5
  type Variant = "primary" | "secondary" | "ghost" | "danger" | "link";
@@ -33,14 +34,12 @@ export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElemen
33
34
  */
34
35
  export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
35
36
  ({ variant = "primary", size = "md", className, ...props }, ref) => {
36
- const classes = [
37
+ const classes = cn(
37
38
  "sh-ui-button",
38
39
  `sh-ui-button--${variant}`,
39
40
  `sh-ui-button--${size}`,
40
41
  className,
41
- ]
42
- .filter(Boolean)
43
- .join(" ");
42
+ );
44
43
  return <button ref={ref} className={classes} {...props} />;
45
44
  },
46
45
  );
@@ -3,10 +3,8 @@
3
3
  import * as React from "react";
4
4
  import { Select, SelectContent, SelectItem, SelectTrigger } from "../select";
5
5
 
6
- function cx(...args: (string | undefined | false)[]) {
7
- return args.filter(Boolean).join(" ");
8
- }
9
6
 
7
+ import { cn } from "@SH_UI_UTILS@";
10
8
  const DEFAULT_WEEKDAYS_KO = ["일", "월", "화", "수", "목", "금", "토"] as const;
11
9
 
12
10
  const isSameDay = (a: Date, b: Date) =>
@@ -287,7 +285,7 @@ export function Calendar(props: CalendarProps) {
287
285
 
288
286
  return (
289
287
  <div
290
- className={cx("inline-flex gap-[var(--space-4)] select-none", numberOfMonths > 1 && "flex-wrap", className)}
288
+ className={cn("inline-flex gap-[var(--space-4)] select-none", numberOfMonths > 1 && "flex-wrap", className)}
291
289
  aria-label={ariaLabel}
292
290
  >
293
291
  {children
@@ -316,7 +314,7 @@ export function Calendar(props: CalendarProps) {
316
314
  export interface CalendarHeaderProps extends React.HTMLAttributes<HTMLDivElement> {}
317
315
  export const CalendarHeader = React.forwardRef<HTMLDivElement, CalendarHeaderProps>(
318
316
  function CalendarHeader({ className, ...props }, ref) {
319
- return <div ref={ref} className={cx("flex items-center justify-between gap-[var(--space-1)] mb-[var(--space-2)]", className)} {...props} />;
317
+ return <div ref={ref} className={cn("flex items-center justify-between gap-[var(--space-1)] mb-[var(--space-2)]", className)} {...props} />;
320
318
  },
321
319
  );
322
320
 
@@ -324,7 +322,7 @@ const navButtonClasses =
324
322
  "inline-flex items-center justify-center w-7 h-7 p-0 border-none rounded-[calc(var(--radius)-2px)] bg-transparent text-foreground-muted cursor-pointer shrink-0 transition-[background-color,color] duration-[var(--duration-fast)] hover:not-disabled:bg-background-muted hover:not-disabled:text-foreground focus-visible:outline-[length:var(--border-width-strong)] focus-visible:outline-foreground focus-visible:outline-offset-2 motion-reduce:transition-none";
325
323
 
326
324
  function CalendarNavPlaceholder() {
327
- return <span className={cx(navButtonClasses, "invisible pointer-events-none")} aria-hidden />;
325
+ return <span className={cn(navButtonClasses, "invisible pointer-events-none")} aria-hidden />;
328
326
  }
329
327
 
330
328
  export interface CalendarNavButtonProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "children"> {
@@ -344,7 +342,7 @@ function makeNavButton(
344
342
  <button
345
343
  ref={ref}
346
344
  type="button"
347
- className={cx(navButtonClasses, className)}
345
+ className={cn(navButtonClasses, className)}
348
346
  aria-label={ariaLabel ?? defaultLabel}
349
347
  onClick={(e) => { resolveHandler(ctx)(); onClick?.(e); }}
350
348
  {...props}
@@ -377,7 +375,7 @@ export function CalendarYearSelect({ className, formatYear = (y) => `${y}년` }:
377
375
  const items = ctx.yearOptions.includes(year) ? ctx.yearOptions : [...ctx.yearOptions, year].sort((a, b) => a - b);
378
376
  return (
379
377
  <Select value={String(year)} onValueChange={(v) => ctx.setYearForVisible(Number(v))}>
380
- <SelectTrigger className={cx(calendarSelectTriggerClasses, className)} aria-label="연도">
378
+ <SelectTrigger className={cn(calendarSelectTriggerClasses, className)} aria-label="연도">
381
379
  <span>{formatYear(year)}</span>
382
380
  </SelectTrigger>
383
381
  <SelectContent>
@@ -397,7 +395,7 @@ export function CalendarMonthSelect({ className, formatMonth = (m) => `${m + 1}
397
395
  const month = ctx.visibleMonth.getMonth();
398
396
  return (
399
397
  <Select value={String(month)} onValueChange={(v) => ctx.setMonthForVisible(Number(v))}>
400
- <SelectTrigger className={cx(calendarSelectTriggerClasses, className)} aria-label="월">
398
+ <SelectTrigger className={cn(calendarSelectTriggerClasses, className)} aria-label="월">
401
399
  <span>{formatMonth(month)}</span>
402
400
  </SelectTrigger>
403
401
  <SelectContent>
@@ -420,7 +418,7 @@ export const CalendarGrid = React.forwardRef<HTMLDivElement, CalendarGridProps>(
420
418
  const ariaLabel = ctx.ariaLabel ?? monthLabel;
421
419
 
422
420
  return (
423
- <div ref={ref} className={cx("", className)} {...rest}>
421
+ <div ref={ref} className={cn("", className)} {...rest}>
424
422
  <div className="grid grid-cols-7 mb-[var(--space-1)]" role="row">
425
423
  {ctx.weekdayLabels.map((label) => (
426
424
  <span key={label} className="flex items-center justify-center h-8 text-[length:var(--text-xs)] font-medium text-foreground-muted" role="columnheader" aria-label={label}>
@@ -450,10 +448,10 @@ export const CalendarGrid = React.forwardRef<HTMLDivElement, CalendarGridProps>(
450
448
  isStart && isEnd ? "rounded-[calc(var(--radius)-2px)]" : "";
451
449
 
452
450
  return (
453
- <div key={i} className={cx("flex items-center justify-center w-full h-[2.375rem] min-w-0", cellRangeBg, cellRangeRadius)}>
451
+ <div key={i} className={cn("flex items-center justify-center w-full h-[2.375rem] min-w-0", cellRangeBg, cellRangeRadius)}>
454
452
  <button
455
453
  type="button"
456
- className={cx(
454
+ className={cn(
457
455
  "flex items-center justify-center w-9 h-9 p-0 border-none rounded-[calc(var(--radius)-2px)] bg-transparent text-foreground text-[0.8125rem] font-[inherit] cursor-pointer transition-[background-color,color] duration-[var(--duration-fast)] hover:not-disabled:bg-background-muted focus-visible:outline-[length:var(--border-width-strong)] focus-visible:outline-foreground focus-visible:outline-offset-2 disabled:opacity-30 disabled:cursor-not-allowed motion-reduce:transition-none",
458
456
  !current && "text-[var(--foreground-subtle,var(--foreground-muted))] opacity-40",
459
457
  isToday && "font-bold underline underline-offset-[0.125rem]",
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import * as React from "react";
4
+ import { cn } from "@SH_UI_UTILS@";
4
5
  import {
5
6
  Select,
6
7
  SelectContent,
@@ -11,9 +12,6 @@ import "./styles.css";
11
12
 
12
13
  /* ───────── Helpers ───────── */
13
14
 
14
- function cx(...args: (string | undefined | false)[]) {
15
- return args.filter(Boolean).join(" ");
16
- }
17
15
 
18
16
  const DEFAULT_WEEKDAYS_KO = ["일", "월", "화", "수", "목", "금", "토"] as const;
19
17
 
@@ -505,7 +503,7 @@ export function Calendar(props: CalendarProps) {
505
503
 
506
504
  return (
507
505
  <div
508
- className={cx("sh-ui-calendar", numberOfMonths > 1 && "sh-ui-calendar--multi", className)}
506
+ className={cn("sh-ui-calendar", numberOfMonths > 1 && "sh-ui-calendar--multi", className)}
509
507
  aria-label={ariaLabel}
510
508
  >
511
509
  {children
@@ -545,7 +543,7 @@ export interface CalendarHeaderProps extends React.HTMLAttributes<HTMLDivElement
545
543
  /** 헤더 컨테이너. 화살표/dropdown 등을 children 으로 자유롭게 배치. */
546
544
  export const CalendarHeader = React.forwardRef<HTMLDivElement, CalendarHeaderProps>(
547
545
  function CalendarHeader({ className, ...props }, ref) {
548
- return <div ref={ref} className={cx("sh-ui-calendar__header", className)} {...props} />;
546
+ return <div ref={ref} className={cn("sh-ui-calendar__header", className)} {...props} />;
549
547
  },
550
548
  );
551
549
 
@@ -574,7 +572,7 @@ function makeNavButton(
574
572
  <button
575
573
  ref={ref}
576
574
  type="button"
577
- className={cx("sh-ui-calendar__nav", className)}
575
+ className={cn("sh-ui-calendar__nav", className)}
578
576
  aria-label={ariaLabel ?? defaultLabel}
579
577
  onClick={(e) => {
580
578
  resolveHandler(ctx)();
@@ -649,7 +647,7 @@ export function CalendarYearSelect({
649
647
  onValueChange={(v) => ctx.setYearForVisible(Number(v))}
650
648
  >
651
649
  <SelectTrigger
652
- className={cx("sh-ui-calendar__select-trigger", className)}
650
+ className={cn("sh-ui-calendar__select-trigger", className)}
653
651
  aria-label="연도"
654
652
  >
655
653
  <span className="sh-ui-calendar__select-value">{formatYear(year)}</span>
@@ -684,7 +682,7 @@ export function CalendarMonthSelect({
684
682
  onValueChange={(v) => ctx.setMonthForVisible(Number(v))}
685
683
  >
686
684
  <SelectTrigger
687
- className={cx("sh-ui-calendar__select-trigger", className)}
685
+ className={cn("sh-ui-calendar__select-trigger", className)}
688
686
  aria-label="월"
689
687
  >
690
688
  <span className="sh-ui-calendar__select-value">{formatMonth(month)}</span>
@@ -716,7 +714,7 @@ export const CalendarGrid = React.forwardRef<HTMLDivElement, CalendarGridProps>(
716
714
  const ariaLabel = ctx.ariaLabel ?? monthLabel;
717
715
 
718
716
  return (
719
- <div ref={ref} className={cx("sh-ui-calendar__grid-wrap", className)} {...rest}>
717
+ <div ref={ref} className={cn("sh-ui-calendar__grid-wrap", className)} {...rest}>
720
718
  <div className="sh-ui-calendar__weekdays" role="row">
721
719
  {ctx.weekdayLabels.map((label) => (
722
720
  <span
@@ -751,7 +749,7 @@ export const CalendarGrid = React.forwardRef<HTMLDivElement, CalendarGridProps>(
751
749
  return (
752
750
  <div
753
751
  key={i}
754
- className={cx(
752
+ className={cn(
755
753
  "sh-ui-calendar__cell",
756
754
  inRange && "sh-ui-calendar__cell--in-range",
757
755
  isStart && "sh-ui-calendar__cell--range-start",
@@ -760,7 +758,7 @@ export const CalendarGrid = React.forwardRef<HTMLDivElement, CalendarGridProps>(
760
758
  >
761
759
  <button
762
760
  type="button"
763
- className={cx(
761
+ className={cn(
764
762
  "sh-ui-calendar__day",
765
763
  !current && "sh-ui-calendar__day--outside",
766
764
  selected && "sh-ui-calendar__day--selected",
@@ -1,16 +1,14 @@
1
1
  import * as React from "react";
2
2
 
3
+ import { cn } from "@SH_UI_UTILS@";
3
4
  type DivProps = React.HTMLAttributes<HTMLDivElement>;
4
5
 
5
- function mergeClass(base: string, extra?: string) {
6
- return extra ? `${base} ${extra}` : base;
7
- }
8
6
 
9
7
  export const Card = React.forwardRef<HTMLDivElement, DivProps>(
10
8
  ({ className, ...props }, ref) => (
11
9
  <div
12
10
  ref={ref}
13
- className={mergeClass(
11
+ className={cn(
14
12
  "flex flex-col gap-[var(--space-6)] py-[var(--space-6)] bg-background text-foreground border border-border rounded-[var(--radius)] max-sm:gap-[var(--space-4)] max-sm:py-[var(--space-4)]",
15
13
  className,
16
14
  )}
@@ -25,7 +23,7 @@ export const CardHeader = React.forwardRef<HTMLDivElement, DivProps>(
25
23
  <div
26
24
  ref={ref}
27
25
  data-slot="card-header"
28
- className={mergeClass(
26
+ className={cn(
29
27
  "grid grid-cols-1 auto-rows-auto gap-y-1.5 px-[var(--space-6)] has-[[data-slot=card-action]]:grid-cols-[1fr_auto] max-sm:px-[var(--space-4)]",
30
28
  className,
31
29
  )}
@@ -39,7 +37,7 @@ export const CardTitle = React.forwardRef<HTMLDivElement, DivProps>(
39
37
  ({ className, ...props }, ref) => (
40
38
  <div
41
39
  ref={ref}
42
- className={mergeClass(
40
+ className={cn(
43
41
  "text-[length:var(--text-base)] font-semibold leading-tight tracking-tight",
44
42
  className,
45
43
  )}
@@ -53,7 +51,7 @@ export const CardDescription = React.forwardRef<HTMLDivElement, DivProps>(
53
51
  ({ className, ...props }, ref) => (
54
52
  <div
55
53
  ref={ref}
56
- className={mergeClass(
54
+ className={cn(
57
55
  "text-[length:var(--text-sm)] leading-normal text-foreground-muted",
58
56
  className,
59
57
  )}
@@ -72,7 +70,7 @@ export const CardAction = React.forwardRef<HTMLDivElement, DivProps>(
72
70
  <div
73
71
  ref={ref}
74
72
  data-slot="card-action"
75
- className={mergeClass(
73
+ className={cn(
76
74
  "col-start-2 row-span-2 self-start justify-self-end",
77
75
  className,
78
76
  )}
@@ -86,7 +84,7 @@ export const CardContent = React.forwardRef<HTMLDivElement, DivProps>(
86
84
  ({ className, ...props }, ref) => (
87
85
  <div
88
86
  ref={ref}
89
- className={mergeClass(
87
+ className={cn(
90
88
  "px-[var(--space-6)] text-[length:var(--text-sm)] leading-relaxed max-sm:px-[var(--space-4)]",
91
89
  className,
92
90
  )}
@@ -100,7 +98,7 @@ export const CardFooter = React.forwardRef<HTMLDivElement, DivProps>(
100
98
  ({ className, ...props }, ref) => (
101
99
  <div
102
100
  ref={ref}
103
- className={mergeClass(
101
+ className={cn(
104
102
  "px-[var(--space-6)] flex items-center gap-[var(--space-2)] max-sm:px-[var(--space-4)] max-sm:flex-wrap",
105
103
  className,
106
104
  )}