sh-ui-cli 0.45.2 → 0.46.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.tailwind.tsx +5 -7
- package/data/registry/react/components/accordion/index.tsx +5 -7
- package/data/registry/react/components/avatar/index.tailwind.tsx +4 -6
- package/data/registry/react/components/avatar/index.tsx +4 -6
- package/data/registry/react/components/badge/index.tailwind.tsx +2 -4
- package/data/registry/react/components/badge/index.tsx +2 -4
- package/data/registry/react/components/breadcrumb/index.tailwind.tsx +8 -10
- package/data/registry/react/components/breadcrumb/index.tsx +8 -10
- package/data/registry/react/components/button/index.module.tsx +45 -0
- package/data/registry/react/components/button/index.tailwind.tsx +2 -1
- package/data/registry/react/components/button/index.tsx +3 -4
- package/data/registry/react/components/button/styles.module.css +92 -0
- package/data/registry/react/components/calendar/index.tailwind.tsx +10 -12
- package/data/registry/react/components/calendar/index.tsx +9 -11
- package/data/registry/react/components/card/index.module.tsx +63 -0
- package/data/registry/react/components/card/index.tailwind.tsx +8 -10
- package/data/registry/react/components/card/index.tsx +8 -10
- package/data/registry/react/components/card/styles.module.css +73 -0
- package/data/registry/react/components/carousel/index.tailwind.tsx +7 -9
- package/data/registry/react/components/carousel/index.tsx +7 -9
- package/data/registry/react/components/checkbox/index.tailwind.tsx +3 -5
- package/data/registry/react/components/checkbox/index.tsx +3 -5
- package/data/registry/react/components/code-editor/index.tailwind.tsx +2 -4
- package/data/registry/react/components/code-editor/index.tsx +2 -4
- package/data/registry/react/components/code-panel/index.tailwind.tsx +5 -7
- package/data/registry/react/components/code-panel/index.tsx +5 -7
- package/data/registry/react/components/color-picker/index.tailwind.tsx +7 -6
- package/data/registry/react/components/color-picker/index.tsx +7 -6
- package/data/registry/react/components/combobox/index.tailwind.tsx +8 -10
- package/data/registry/react/components/combobox/index.tsx +8 -10
- package/data/registry/react/components/context-menu/index.tailwind.tsx +10 -12
- package/data/registry/react/components/context-menu/index.tsx +10 -12
- package/data/registry/react/components/date-picker/index.tailwind.tsx +7 -9
- package/data/registry/react/components/date-picker/index.tsx +7 -9
- package/data/registry/react/components/dialog/index.tailwind.tsx +6 -8
- package/data/registry/react/components/dialog/index.tsx +6 -8
- package/data/registry/react/components/dropdown-menu/index.tailwind.tsx +10 -12
- package/data/registry/react/components/dropdown-menu/index.tsx +10 -12
- package/data/registry/react/components/file-upload/index.tailwind.tsx +6 -8
- package/data/registry/react/components/file-upload/index.tsx +6 -8
- package/data/registry/react/components/form/field.tailwind.tsx +2 -1
- package/data/registry/react/components/form/field.tsx +2 -3
- package/data/registry/react/components/header/index.tailwind.tsx +17 -19
- package/data/registry/react/components/header/index.tsx +17 -19
- package/data/registry/react/components/input/index.module.tsx +486 -0
- package/data/registry/react/components/input/index.tailwind.tsx +4 -6
- package/data/registry/react/components/input/index.tsx +4 -6
- package/data/registry/react/components/input/styles.module.css +200 -0
- package/data/registry/react/components/label/index.tailwind.tsx +6 -8
- package/data/registry/react/components/label/index.tsx +6 -8
- package/data/registry/react/components/markdown-editor/index.tailwind.tsx +2 -4
- package/data/registry/react/components/markdown-editor/index.tsx +2 -4
- package/data/registry/react/components/menubar/index.tailwind.tsx +2 -4
- package/data/registry/react/components/menubar/index.tsx +2 -4
- package/data/registry/react/components/numeric-input/index.tailwind.tsx +2 -4
- package/data/registry/react/components/numeric-input/index.tsx +2 -4
- package/data/registry/react/components/page-toc/index.tailwind.tsx +3 -2
- package/data/registry/react/components/page-toc/index.tsx +2 -3
- package/data/registry/react/components/pagination/index.tailwind.tsx +8 -10
- package/data/registry/react/components/pagination/index.tsx +8 -10
- package/data/registry/react/components/popover/index.tailwind.tsx +4 -6
- package/data/registry/react/components/popover/index.tsx +4 -6
- package/data/registry/react/components/progress/index.tailwind.tsx +3 -5
- package/data/registry/react/components/progress/index.tsx +2 -4
- package/data/registry/react/components/radio/index.tailwind.tsx +3 -5
- package/data/registry/react/components/radio/index.tsx +3 -5
- package/data/registry/react/components/rich-text-editor/index.tailwind.tsx +3 -5
- package/data/registry/react/components/rich-text-editor/index.tsx +3 -5
- package/data/registry/react/components/select/index.tailwind.tsx +8 -10
- package/data/registry/react/components/select/index.tsx +8 -10
- package/data/registry/react/components/separator/index.tailwind.tsx +2 -4
- package/data/registry/react/components/separator/index.tsx +2 -4
- package/data/registry/react/components/sidebar/index.tailwind.tsx +32 -43
- package/data/registry/react/components/sidebar/index.tsx +29 -46
- package/data/registry/react/components/skeleton/index.tailwind.tsx +2 -4
- package/data/registry/react/components/skeleton/index.tsx +2 -4
- package/data/registry/react/components/slider/index.tailwind.tsx +5 -7
- package/data/registry/react/components/slider/index.tsx +5 -7
- package/data/registry/react/components/spinner/index.tailwind.tsx +3 -5
- package/data/registry/react/components/spinner/index.tsx +2 -4
- package/data/registry/react/components/switch/index.tailwind.tsx +3 -5
- package/data/registry/react/components/switch/index.tsx +2 -4
- package/data/registry/react/components/tabs/index.tailwind.tsx +6 -8
- package/data/registry/react/components/tabs/index.tsx +6 -8
- package/data/registry/react/components/textarea/index.tailwind.tsx +2 -4
- package/data/registry/react/components/textarea/index.tsx +2 -4
- package/data/registry/react/components/toggle/index.tailwind.tsx +4 -6
- package/data/registry/react/components/toggle/index.tsx +4 -6
- package/data/registry/react/components/tooltip/index.tailwind.tsx +2 -4
- package/data/registry/react/components/tooltip/index.tsx +2 -4
- package/data/registry/react/lib/cn.tailwind.ts +17 -0
- package/data/registry/react/peer-versions.json +3 -1
- package/data/registry/react/registry.json +202 -43
- package/data/tokens/build.mjs +4 -0
- package/package.json +1 -1
- package/src/add.mjs +37 -13
- package/templates/ui-app-template/sh-ui.config.json +5 -0
|
@@ -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.46.0",
|
|
7
|
+
"date": "2026-05-04",
|
|
8
|
+
"title": "CSS Modules 변종 파일럿 — button/card/input",
|
|
9
|
+
"type": "minor",
|
|
10
|
+
"highlights": [
|
|
11
|
+
"**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) 를 공유.",
|
|
12
|
+
"**Fallback 일반화** — 기존엔 tailwind 한정이던 `effectiveFramework` 가 모든 변종 공통으로 동작. `cssFramework` 에 변종이 없는 컴포넌트는 plain 으로 자동 fallback 되며 `ℹ <name> — <fw> 변종 미제공, plain 변종으로 설치` 한 줄 안내 출력. 점진적 rollout 패턴 그대로 — 컴포넌트마다 변종을 갖출 필요 없이 가능한 것부터 제공.",
|
|
13
|
+
"**`utils` 의 frameworks 분기** — `lib/cn.ts` 가 `[\"plain\", \"css-modules\"]` 로 매칭. CSS Modules 환경도 zero-dep `cn` 을 그대로 공유 (clsx/tailwind-merge 가 필요한 건 tailwind 변종뿐).",
|
|
14
|
+
"css-modules 는 여전히 `CSS_FRAMEWORKS_PLANNED` 유지 — 파일럿 3개만 변종이 있어 UI 노출은 다음 라운드에서 전체 컴포넌트 롤아웃 후 SUPPORTED 로 승격. 지금도 사용자가 직접 `sh-ui.config.json` 을 손대면 동작."
|
|
15
|
+
],
|
|
16
|
+
"url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.46.0"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"version": "0.45.3",
|
|
20
|
+
"date": "2026-04-30",
|
|
21
|
+
"title": "공유 `cn` 유틸 + CSS 프레임워크 가이드 docs",
|
|
22
|
+
"type": "patch",
|
|
23
|
+
"highlights": [
|
|
24
|
+
"**공유 `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` 도 함께 카피.",
|
|
25
|
+
"**`@SH_UI_UTILS@` placeholder 치환** — 컴포넌트가 `import { cn } from \"@SH_UI_UTILS@\"` 로 import 하면 CLI 가 add 시점에 `aliases.utils` 값(예: `@/lib/utils`)으로 치환. cn 유틸 import 경로가 사용자 프로젝트 alias 와 자동 맞아떨어짐. `aliases.utils` 미설정 시 친절 에러로 안내.",
|
|
26
|
+
"**CSS 프레임워크 docs 페이지 추가** — `apps/docs/(docs)/css-framework/page.tsx` 신설. 변종 시스템 큰 그림 (현재 plain/tailwind + 계획 중 css-modules/vanilla-extract) + 모드별 차이·전환·fallback 설명. 사이드바 nav 에 \"CSS 프레임워크\" 링크 추가, cli 페이지에서 cross-link.",
|
|
27
|
+
"**ui-app-template config 보강** — 누락됐던 `aliases.utils` 추가. 신규 init 도 동일하게 채워짐."
|
|
28
|
+
],
|
|
29
|
+
"url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.45.3"
|
|
30
|
+
},
|
|
5
31
|
{
|
|
6
32
|
"version": "0.45.2",
|
|
7
33
|
"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={
|
|
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={
|
|
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={
|
|
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={
|
|
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={
|
|
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={
|
|
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={
|
|
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={
|
|
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={
|
|
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={
|
|
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={
|
|
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={
|
|
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={
|
|
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={
|
|
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={
|
|
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={
|
|
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={
|
|
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={
|
|
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={
|
|
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={
|
|
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={
|
|
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={
|
|
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={
|
|
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={
|
|
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={
|
|
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={
|
|
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={
|
|
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={
|
|
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={
|
|
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={
|
|
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>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "@SH_UI_UTILS@";
|
|
3
|
+
import styles from "./styles.module.css";
|
|
4
|
+
|
|
5
|
+
type Variant = "primary" | "secondary" | "ghost" | "danger" | "link";
|
|
6
|
+
type Size = "sm" | "md" | "lg";
|
|
7
|
+
|
|
8
|
+
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
9
|
+
/**
|
|
10
|
+
* 시각적 위계.
|
|
11
|
+
* - `primary` — 페이지의 주요 액션. 한 화면에 하나만 권장.
|
|
12
|
+
* - `secondary` — 보조 액션. 약한 배경 + border.
|
|
13
|
+
* - `ghost` — 배경 없는 hover 강조 액션. 툴바/메뉴 항목에 적합.
|
|
14
|
+
* - `danger` — 파괴적 액션(삭제, 취소 등).
|
|
15
|
+
* - `link` — 텍스트 링크처럼 보이는 인라인 버튼.
|
|
16
|
+
*
|
|
17
|
+
* @default "primary"
|
|
18
|
+
*/
|
|
19
|
+
variant?: Variant;
|
|
20
|
+
/**
|
|
21
|
+
* 크기.
|
|
22
|
+
* - `sm` — 조밀한 영역(테이블 행, 툴바)
|
|
23
|
+
* - `md` — 일반
|
|
24
|
+
* - `lg` — CTA·랜딩 영역
|
|
25
|
+
*
|
|
26
|
+
* @default "md"
|
|
27
|
+
*/
|
|
28
|
+
size?: Size;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 사용자 액션을 트리거하는 기본 버튼 (CSS Modules 변종).
|
|
33
|
+
* variant로 시각적 위계(primary/secondary/ghost/danger/link)를,
|
|
34
|
+
* size로 크기를 결정한다. 페이지 이동 목적이면 anchor를 감싼 `link` variant를 사용할 것.
|
|
35
|
+
*/
|
|
36
|
+
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
37
|
+
({ variant = "primary", size = "md", className, ...props }, ref) => (
|
|
38
|
+
<button
|
|
39
|
+
ref={ref}
|
|
40
|
+
className={cn(styles.button, styles[variant], styles[size], className)}
|
|
41
|
+
{...props}
|
|
42
|
+
/>
|
|
43
|
+
),
|
|
44
|
+
);
|
|
45
|
+
Button.displayName = "Button";
|
|
@@ -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={
|
|
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
|
);
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
.button {
|
|
2
|
+
display: inline-flex;
|
|
3
|
+
align-items: center;
|
|
4
|
+
justify-content: center;
|
|
5
|
+
gap: var(--space-2);
|
|
6
|
+
border: 1px solid transparent;
|
|
7
|
+
border-radius: var(--radius);
|
|
8
|
+
font-weight: var(--weight-medium);
|
|
9
|
+
line-height: 1;
|
|
10
|
+
cursor: pointer;
|
|
11
|
+
transition: background-color var(--duration-fast), color var(--duration-fast), border-color var(--duration-fast),
|
|
12
|
+
transform 80ms ease-out, filter 80ms;
|
|
13
|
+
user-select: none;
|
|
14
|
+
-webkit-tap-highlight-color: transparent;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.button:disabled {
|
|
18
|
+
opacity: var(--opacity-disabled);
|
|
19
|
+
pointer-events: none;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.button:focus-visible {
|
|
23
|
+
outline: var(--border-width-strong) solid var(--foreground);
|
|
24
|
+
outline-offset: 2px;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.button:active:not(:disabled) {
|
|
28
|
+
transform: scale(0.97);
|
|
29
|
+
filter: brightness(0.92);
|
|
30
|
+
transition-duration: 40ms;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/* sizes */
|
|
34
|
+
.sm { height: var(--control-sm); padding: 0 var(--space-3); font-size: var(--text-sm); }
|
|
35
|
+
.md { height: var(--control-md); padding: 0 var(--space-4); font-size: var(--text-sm); }
|
|
36
|
+
.lg { height: var(--control-lg); padding: 0 var(--space-5); font-size: var(--text-base); }
|
|
37
|
+
|
|
38
|
+
/* 모바일/터치 디바이스: 최소 탭 영역 보장 */
|
|
39
|
+
@media (hover: none) and (pointer: coarse) {
|
|
40
|
+
.sm { height: 2.25rem; }
|
|
41
|
+
.md { height: 2.75rem; }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/* variants */
|
|
45
|
+
.primary {
|
|
46
|
+
background-color: var(--primary);
|
|
47
|
+
color: var(--primary-foreground);
|
|
48
|
+
}
|
|
49
|
+
.primary:hover { background-color: var(--primary-hover); }
|
|
50
|
+
|
|
51
|
+
.secondary {
|
|
52
|
+
background-color: var(--background-muted);
|
|
53
|
+
color: var(--foreground);
|
|
54
|
+
border-color: var(--border);
|
|
55
|
+
}
|
|
56
|
+
.secondary:hover { background-color: var(--background-subtle); }
|
|
57
|
+
|
|
58
|
+
.ghost {
|
|
59
|
+
background-color: transparent;
|
|
60
|
+
color: var(--foreground);
|
|
61
|
+
}
|
|
62
|
+
.ghost:hover { background-color: var(--background-muted); }
|
|
63
|
+
|
|
64
|
+
.danger {
|
|
65
|
+
background-color: var(--danger);
|
|
66
|
+
color: var(--danger-foreground);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.link {
|
|
70
|
+
background-color: transparent;
|
|
71
|
+
color: var(--foreground);
|
|
72
|
+
border-color: transparent;
|
|
73
|
+
height: auto;
|
|
74
|
+
padding: 0;
|
|
75
|
+
text-underline-offset: 3px;
|
|
76
|
+
}
|
|
77
|
+
.link:hover { text-decoration: underline; }
|
|
78
|
+
.link:active:not(:disabled) {
|
|
79
|
+
transform: none;
|
|
80
|
+
filter: none;
|
|
81
|
+
color: var(--foreground-muted);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
@media (prefers-reduced-motion: reduce) {
|
|
85
|
+
.button {
|
|
86
|
+
transition: none;
|
|
87
|
+
}
|
|
88
|
+
.button:active:not(:disabled) {
|
|
89
|
+
transform: none;
|
|
90
|
+
filter: none;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -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={
|
|
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={
|
|
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={
|
|
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={
|
|
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={
|
|
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={
|
|
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={
|
|
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={
|
|
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={
|
|
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]",
|