sh-ui-cli 0.42.1 → 0.44.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/README.md +6 -1
- package/data/changelog/versions.json +25 -0
- package/data/registry/flutter/registry.json +1 -1
- package/data/registry/react/components/accordion/index.tailwind.tsx +88 -0
- package/data/registry/react/components/avatar/index.tailwind.tsx +74 -0
- package/data/registry/react/components/badge/index.tailwind.tsx +47 -0
- package/data/registry/react/components/breadcrumb/index.tailwind.tsx +138 -0
- package/data/registry/react/components/button/index.tailwind.tsx +70 -0
- package/data/registry/react/components/card/index.tailwind.tsx +111 -0
- package/data/registry/react/components/checkbox/index.tailwind.tsx +72 -0
- package/data/registry/react/components/code-panel/index.tailwind.tsx +107 -0
- package/data/registry/react/components/combobox/index.tailwind.tsx +160 -0
- package/data/registry/react/components/context-menu/index.tailwind.tsx +170 -0
- package/data/registry/react/components/date-picker/index.tailwind.tsx +294 -0
- package/data/registry/react/components/dialog/index.tailwind.tsx +96 -0
- package/data/registry/react/components/dropdown-menu/index.tailwind.tsx +205 -0
- package/data/registry/react/components/input/index.tailwind.tsx +405 -0
- package/data/registry/react/components/label/index.tailwind.tsx +78 -0
- package/data/registry/react/components/menubar/index.tailwind.tsx +32 -0
- package/data/registry/react/components/numeric-input/index.tailwind.tsx +113 -0
- package/data/registry/react/components/page-toc/index.tailwind.tsx +149 -0
- package/data/registry/react/components/pagination/index.tailwind.tsx +148 -0
- package/data/registry/react/components/popover/index.tailwind.tsx +77 -0
- package/data/registry/react/components/progress/index.tailwind.tsx +60 -0
- package/data/registry/react/components/radio/index.tailwind.tsx +54 -0
- package/data/registry/react/components/select/index.tailwind.tsx +199 -0
- package/data/registry/react/components/separator/index.tailwind.tsx +42 -0
- package/data/registry/react/components/skeleton/index.tailwind.tsx +39 -0
- package/data/registry/react/components/slider/index.tailwind.tsx +255 -0
- package/data/registry/react/components/spinner/index.tailwind.tsx +63 -0
- package/data/registry/react/components/switch/index.tailwind.tsx +62 -0
- package/data/registry/react/components/tabs/index.tailwind.tsx +113 -0
- package/data/registry/react/components/textarea/index.tailwind.tsx +21 -0
- package/data/registry/react/components/toggle/index.tailwind.tsx +111 -0
- package/data/registry/react/components/tooltip/index.tailwind.tsx +55 -0
- package/data/registry/react/peer-versions.json +1 -0
- package/data/registry/react/registry.json +530 -72
- package/data/tokens/build.mjs +66 -0
- package/package.json +1 -1
- package/src/add.mjs +54 -6
- package/src/api.d.ts +14 -0
- package/src/api.js +4 -0
- package/src/constants.js +19 -0
- package/src/create/cli-args.js +18 -2
- package/src/create/generator.js +55 -6
- package/src/create/index.mjs +3 -1
- package/src/init.mjs +25 -7
- package/src/mcp.mjs +13 -2
- package/templates/flutter-standalone/sh-ui.config.json +1 -1
- package/templates/nextjs-standalone/app/globals.css +1 -21
- package/templates/nextjs-standalone/sh-ui.config.json +1 -1
- package/templates/ui-app-template/sh-ui.config.json +1 -1
- package/templates/ui-app-template/src/styles/globals.css +1 -21
package/README.md
CHANGED
|
@@ -151,7 +151,7 @@ npx -y sh-ui-cli mcp init --client claude-code --scope user
|
|
|
151
151
|
```json
|
|
152
152
|
{
|
|
153
153
|
"platform": "react",
|
|
154
|
-
"
|
|
154
|
+
"cssFramework": "plain",
|
|
155
155
|
"theme": { "base": "neutral", "radius": "md", "mode": "light-dark" },
|
|
156
156
|
"paths": {
|
|
157
157
|
"tokens": "src/shared/styles/tokens.css",
|
|
@@ -161,6 +161,11 @@ npx -y sh-ui-cli mcp init --client claude-code --scope user
|
|
|
161
161
|
}
|
|
162
162
|
```
|
|
163
163
|
|
|
164
|
+
`cssFramework` 옵션:
|
|
165
|
+
|
|
166
|
+
- `"plain"` — CSS custom properties + 일반 .css 파일. 모든 컴포넌트 지원 (기본).
|
|
167
|
+
- `"tailwind"` — Tailwind v4 utility class TSX 변종 (`class-variance-authority` 기반). button/card/input 부터 시작해 점진적으로 확대 중. 변종 미제공 컴포넌트는 add 시 plain 으로 자동 fallback — Tailwind v4 의 `@theme inline` 브리지가 sh-ui 토큰을 매핑하므로 plain CSS 도 그대로 동작.
|
|
168
|
+
|
|
164
169
|
## 더 알아보기
|
|
165
170
|
|
|
166
171
|
- sh-ui 디자인 시스템: https://github.com/sanghyeonKim0201/sh-ui
|
|
@@ -2,6 +2,31 @@
|
|
|
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.44.0",
|
|
7
|
+
"date": "2026-04-30",
|
|
8
|
+
"title": "Tailwind 변종 대규모 확대 — 32 컴포넌트 utility-class 변종",
|
|
9
|
+
"type": "minor",
|
|
10
|
+
"highlights": [
|
|
11
|
+
"**32 컴포넌트가 Tailwind utility-class 변종 제공** — separator, skeleton, avatar, spinner, progress, tooltip, badge, label, switch, toggle, checkbox, radio, textarea, breadcrumb, menubar, numeric-input, page-toc, slider, popover, accordion, dialog, code-panel, pagination, date-picker, tabs, dropdown-menu, context-menu, select, combobox + 기존 button/card/input. cva 기반 variant 매트릭스로 prop 시그니처는 plain 변종과 100% 동일.",
|
|
12
|
+
"**dependency 분기 일반화** — `class-variance-authority` 같은 Tailwind 전용 의존성은 `{name, frameworks: [\"tailwind\"]}` 객체 형식으로 표기 — plain 사용자에게는 install 안 됨. 6 개 컴포넌트 (button, avatar, badge, spinner, switch, toggle) 가 cva 사용.",
|
|
13
|
+
"**fallback 자연 동작** — Tailwind 변종이 아직 없는 form, code-editor, calendar, color-picker, markdown-editor, rich-text-editor, carousel, file-upload, toast, header, sidebar 는 add 시 plain 변종으로 자동 설치 + `ℹ <name> — Tailwind 변종 미제공` 알림. plain CSS 도 @theme inline 브리지 덕에 Tailwind v4 환경에서 그대로 동작."
|
|
14
|
+
],
|
|
15
|
+
"url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.44.0"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"version": "0.43.0",
|
|
19
|
+
"date": "2026-04-30",
|
|
20
|
+
"title": "CSS 프레임워크 변종 시스템 + Tailwind 1차 지원",
|
|
21
|
+
"type": "minor",
|
|
22
|
+
"highlights": [
|
|
23
|
+
"**`cssFramework` 옵션 신설** — `sh-ui.config.json` 에 `cssFramework: \"plain\" | \"tailwind\"`. CLI `--css` / `sh-ui-cli init --cssFramework` / MCP `sh_ui_create_project` 의 `cssFramework` / playground UI 에서 모두 선택 가능. 기존 `style: \"default\"` 필드는 deprecated (무시).",
|
|
24
|
+
"**Tailwind v4 utility-class 변종** — `button`, `card`, `input` 의 utility-class 변종 (`class-variance-authority` 기반) 추가. registry.json 의 `frameworks: [\"plain\" | \"tailwind\"]` 분기 + dependency 분기 (`{name, frameworks}` 객체 형식) 지원. `cssFramework=\"tailwind\"` 인데 변종이 없는 컴포넌트는 plain 으로 자동 fallback — Tailwind v4 환경에서 그대로 동작.",
|
|
25
|
+
"**`@theme inline` 단일 소스** — `tokens.css` 가 Tailwind v4 의 `@theme inline { --color-*: var(--*); --radius-{sm,md,lg,xl}; }` 블록을 자동 emit. 템플릿(`nextjs-standalone`, `ui-app-template`)의 하드코딩 `@theme` 제거 — 토큰이 추가/변경돼도 매핑이 자동 동기화.",
|
|
26
|
+
"**토큰 emitter 디스패처** — `packages/tokens/build.mjs` 에 `(platform × cssFramework) → emitter` 테이블. 향후 `react/css-modules`, `react/vanilla-extract` 등 추가 시 한 줄로 등록."
|
|
27
|
+
],
|
|
28
|
+
"url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.43.0"
|
|
29
|
+
},
|
|
5
30
|
{
|
|
6
31
|
"version": "0.42.1",
|
|
7
32
|
"date": "2026-04-30",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$description": "Flutter 레지스트리 매니페스트 — CLI가 각 컴포넌트 name으로 조회해 files를 사용자 프로젝트의 lib/ 아래로 복사한다.",
|
|
2
|
+
"$description": "Flutter 레지스트리 매니페스트 — CLI가 각 컴포넌트 name으로 조회해 files를 사용자 프로젝트의 lib/ 아래로 복사한다. 컴포넌트 또는 file 엔트리에 frameworks?: string[] 옵션 — 미지정시 모든 cssFramework 에 적용, 지정시 해당 배열에 포함된 경우만 복사.",
|
|
3
3
|
"components": {
|
|
4
4
|
"tokens": {
|
|
5
5
|
"name": "tokens",
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Accordion as BaseAccordion } from "@base-ui/react/accordion";
|
|
3
|
+
|
|
4
|
+
function cx(...args: (string | undefined | false)[]) {
|
|
5
|
+
return args.filter(Boolean).join(" ");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
type WithStringClassName<T> = Omit<T, "className"> & { className?: string };
|
|
9
|
+
|
|
10
|
+
export type AccordionSize = "sm" | "md";
|
|
11
|
+
|
|
12
|
+
type AccordionProps = WithStringClassName<
|
|
13
|
+
React.ComponentPropsWithoutRef<typeof BaseAccordion.Root>
|
|
14
|
+
> & {
|
|
15
|
+
size?: AccordionSize;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const Accordion = React.forwardRef<HTMLDivElement, AccordionProps>(
|
|
19
|
+
({ className, size = "md", ...props }, ref) => (
|
|
20
|
+
<BaseAccordion.Root
|
|
21
|
+
ref={ref}
|
|
22
|
+
className={cx("flex flex-col w-full", className)}
|
|
23
|
+
data-size={size}
|
|
24
|
+
{...props}
|
|
25
|
+
/>
|
|
26
|
+
),
|
|
27
|
+
);
|
|
28
|
+
Accordion.displayName = "Accordion";
|
|
29
|
+
|
|
30
|
+
export const AccordionItem = React.forwardRef<
|
|
31
|
+
HTMLDivElement,
|
|
32
|
+
WithStringClassName<React.ComponentPropsWithoutRef<typeof BaseAccordion.Item>>
|
|
33
|
+
>(({ className, ...props }, ref) => (
|
|
34
|
+
<BaseAccordion.Item
|
|
35
|
+
ref={ref}
|
|
36
|
+
className={cx("border-b border-border first:border-t", className)}
|
|
37
|
+
{...props}
|
|
38
|
+
/>
|
|
39
|
+
));
|
|
40
|
+
AccordionItem.displayName = "AccordionItem";
|
|
41
|
+
|
|
42
|
+
export const AccordionTrigger = React.forwardRef<
|
|
43
|
+
HTMLButtonElement,
|
|
44
|
+
WithStringClassName<React.ComponentPropsWithoutRef<typeof BaseAccordion.Trigger>>
|
|
45
|
+
>(({ className, children, ...props }, ref) => (
|
|
46
|
+
<BaseAccordion.Header className="m-0 font-[inherit]">
|
|
47
|
+
<BaseAccordion.Trigger
|
|
48
|
+
ref={ref}
|
|
49
|
+
className={cx(
|
|
50
|
+
"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
|
+
className,
|
|
52
|
+
)}
|
|
53
|
+
{...props}
|
|
54
|
+
>
|
|
55
|
+
<span className="min-w-0 [overflow-wrap:anywhere]">{children}</span>
|
|
56
|
+
<svg
|
|
57
|
+
className="shrink-0 text-foreground-muted transition-transform duration-[180ms] data-[panel-open]:rotate-180 [[data-size=sm]_&]:w-3 [[data-size=sm]_&]:h-3 motion-reduce:transition-none"
|
|
58
|
+
width="16"
|
|
59
|
+
height="16"
|
|
60
|
+
viewBox="0 0 16 16"
|
|
61
|
+
fill="none"
|
|
62
|
+
aria-hidden="true"
|
|
63
|
+
>
|
|
64
|
+
<path d="M4 6l4 4 4-4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
65
|
+
</svg>
|
|
66
|
+
</BaseAccordion.Trigger>
|
|
67
|
+
</BaseAccordion.Header>
|
|
68
|
+
));
|
|
69
|
+
AccordionTrigger.displayName = "AccordionTrigger";
|
|
70
|
+
|
|
71
|
+
export const AccordionContent = React.forwardRef<
|
|
72
|
+
HTMLDivElement,
|
|
73
|
+
WithStringClassName<React.ComponentPropsWithoutRef<typeof BaseAccordion.Panel>>
|
|
74
|
+
>(({ className, children, ...props }, ref) => (
|
|
75
|
+
<BaseAccordion.Panel
|
|
76
|
+
ref={ref}
|
|
77
|
+
className={cx(
|
|
78
|
+
"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
|
+
className,
|
|
80
|
+
)}
|
|
81
|
+
{...props}
|
|
82
|
+
>
|
|
83
|
+
<div className="px-[var(--space-1)] pb-[var(--space-4)] text-[length:var(--text-sm)] leading-relaxed text-foreground-muted [[data-size=sm]_&]:pb-[var(--space-2)] [[data-size=sm]_&]:text-[length:var(--text-xs)] [[data-size=sm]_&]:leading-[1.5]">
|
|
84
|
+
{children}
|
|
85
|
+
</div>
|
|
86
|
+
</BaseAccordion.Panel>
|
|
87
|
+
));
|
|
88
|
+
AccordionContent.displayName = "AccordionContent";
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Avatar as BaseAvatar } from "@base-ui/react/avatar";
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
4
|
+
|
|
5
|
+
type WithStringClassName<T> = Omit<T, "className"> & { className?: string };
|
|
6
|
+
|
|
7
|
+
function cx(...args: (string | undefined | false | null)[]) {
|
|
8
|
+
return args.filter(Boolean).join(" ");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const avatarVariants = cva(
|
|
12
|
+
"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",
|
|
13
|
+
{
|
|
14
|
+
variants: {
|
|
15
|
+
size: {
|
|
16
|
+
sm: "w-7 h-7 text-[length:var(--text-xs)]",
|
|
17
|
+
md: "w-10 h-10 text-[0.8125rem]",
|
|
18
|
+
lg: "w-12 h-12 text-[length:var(--text-sm)]",
|
|
19
|
+
xl: "w-16 h-16 text-[length:var(--text-base)]",
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
defaultVariants: { size: "md" },
|
|
23
|
+
},
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
export type AvatarSize = NonNullable<VariantProps<typeof avatarVariants>["size"]>;
|
|
27
|
+
|
|
28
|
+
export interface AvatarProps
|
|
29
|
+
extends WithStringClassName<
|
|
30
|
+
React.ComponentPropsWithoutRef<typeof BaseAvatar.Root>
|
|
31
|
+
> {
|
|
32
|
+
size?: AvatarSize;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const Avatar = React.forwardRef<HTMLSpanElement, AvatarProps>(
|
|
36
|
+
function Avatar({ className, size = "md", ...props }, ref) {
|
|
37
|
+
return (
|
|
38
|
+
<BaseAvatar.Root
|
|
39
|
+
ref={ref}
|
|
40
|
+
className={cx(avatarVariants({ size }), className)}
|
|
41
|
+
{...props}
|
|
42
|
+
/>
|
|
43
|
+
);
|
|
44
|
+
},
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
export const AvatarImage = React.forwardRef<
|
|
48
|
+
HTMLImageElement,
|
|
49
|
+
WithStringClassName<React.ComponentPropsWithoutRef<typeof BaseAvatar.Image>>
|
|
50
|
+
>(function AvatarImage({ className, ...props }, ref) {
|
|
51
|
+
return (
|
|
52
|
+
<BaseAvatar.Image
|
|
53
|
+
ref={ref}
|
|
54
|
+
className={cx("w-full h-full object-cover block", className)}
|
|
55
|
+
{...props}
|
|
56
|
+
/>
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
export const AvatarFallback = React.forwardRef<
|
|
61
|
+
HTMLSpanElement,
|
|
62
|
+
WithStringClassName<React.ComponentPropsWithoutRef<typeof BaseAvatar.Fallback>>
|
|
63
|
+
>(function AvatarFallback({ className, ...props }, ref) {
|
|
64
|
+
return (
|
|
65
|
+
<BaseAvatar.Fallback
|
|
66
|
+
ref={ref}
|
|
67
|
+
className={cx(
|
|
68
|
+
"inline-flex items-center justify-center w-full h-full uppercase tracking-[0.02em]",
|
|
69
|
+
className,
|
|
70
|
+
)}
|
|
71
|
+
{...props}
|
|
72
|
+
/>
|
|
73
|
+
);
|
|
74
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
3
|
+
|
|
4
|
+
function cx(...args: (string | undefined | false | null)[]) {
|
|
5
|
+
return args.filter(Boolean).join(" ");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const badgeVariants = cva(
|
|
9
|
+
"inline-flex items-center gap-1 px-2 border border-transparent rounded-full font-medium leading-none whitespace-nowrap align-middle select-none",
|
|
10
|
+
{
|
|
11
|
+
variants: {
|
|
12
|
+
variant: {
|
|
13
|
+
primary: "bg-primary text-primary-foreground",
|
|
14
|
+
secondary: "bg-background-muted text-foreground border-border",
|
|
15
|
+
success: "bg-[var(--success,#16a34a)] text-white",
|
|
16
|
+
warning: "bg-[var(--warning,#d97706)] text-white",
|
|
17
|
+
danger: "bg-danger text-[var(--danger-foreground,#fff)]",
|
|
18
|
+
outline: "bg-transparent text-foreground border-border-strong",
|
|
19
|
+
},
|
|
20
|
+
size: {
|
|
21
|
+
sm: "h-5 px-1.5 text-[0.6875rem]",
|
|
22
|
+
md: "h-6 text-[length:var(--text-xs)]",
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
defaultVariants: { variant: "primary", size: "md" },
|
|
26
|
+
},
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
export type BadgeVariant = NonNullable<VariantProps<typeof badgeVariants>["variant"]>;
|
|
30
|
+
export type BadgeSize = NonNullable<VariantProps<typeof badgeVariants>["size"]>;
|
|
31
|
+
|
|
32
|
+
export interface BadgeProps extends React.HTMLAttributes<HTMLSpanElement> {
|
|
33
|
+
variant?: BadgeVariant;
|
|
34
|
+
size?: BadgeSize;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const Badge = React.forwardRef<HTMLSpanElement, BadgeProps>(
|
|
38
|
+
function Badge({ className, variant = "primary", size = "md", ...props }, ref) {
|
|
39
|
+
return (
|
|
40
|
+
<span
|
|
41
|
+
ref={ref}
|
|
42
|
+
className={cx(badgeVariants({ variant, size }), className)}
|
|
43
|
+
{...props}
|
|
44
|
+
/>
|
|
45
|
+
);
|
|
46
|
+
},
|
|
47
|
+
);
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
function cx(...args: (string | undefined | false | null)[]) {
|
|
4
|
+
return args.filter(Boolean).join(" ");
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export const Breadcrumb = React.forwardRef<
|
|
8
|
+
HTMLElement,
|
|
9
|
+
React.HTMLAttributes<HTMLElement>
|
|
10
|
+
>(function Breadcrumb({ className, ...props }, ref) {
|
|
11
|
+
return (
|
|
12
|
+
<nav
|
|
13
|
+
ref={ref}
|
|
14
|
+
aria-label="Breadcrumb"
|
|
15
|
+
className={cx("text-[length:var(--text-sm)] text-foreground-muted", className)}
|
|
16
|
+
{...props}
|
|
17
|
+
/>
|
|
18
|
+
);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export const BreadcrumbList = React.forwardRef<
|
|
22
|
+
HTMLOListElement,
|
|
23
|
+
React.OlHTMLAttributes<HTMLOListElement>
|
|
24
|
+
>(function BreadcrumbList({ className, ...props }, ref) {
|
|
25
|
+
return (
|
|
26
|
+
<ol
|
|
27
|
+
ref={ref}
|
|
28
|
+
className={cx(
|
|
29
|
+
"flex items-center flex-wrap gap-1.5 m-0 p-0 list-none",
|
|
30
|
+
className,
|
|
31
|
+
)}
|
|
32
|
+
{...props}
|
|
33
|
+
/>
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
export const BreadcrumbItem = React.forwardRef<
|
|
38
|
+
HTMLLIElement,
|
|
39
|
+
React.LiHTMLAttributes<HTMLLIElement>
|
|
40
|
+
>(function BreadcrumbItem({ className, ...props }, ref) {
|
|
41
|
+
return (
|
|
42
|
+
<li
|
|
43
|
+
ref={ref}
|
|
44
|
+
className={cx("inline-flex items-center gap-1.5 min-w-0", className)}
|
|
45
|
+
{...props}
|
|
46
|
+
/>
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
export const BreadcrumbLink = React.forwardRef<
|
|
51
|
+
HTMLAnchorElement,
|
|
52
|
+
React.AnchorHTMLAttributes<HTMLAnchorElement>
|
|
53
|
+
>(function BreadcrumbLink({ className, ...props }, ref) {
|
|
54
|
+
return (
|
|
55
|
+
<a
|
|
56
|
+
ref={ref}
|
|
57
|
+
className={cx(
|
|
58
|
+
"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
|
+
className,
|
|
60
|
+
)}
|
|
61
|
+
{...props}
|
|
62
|
+
/>
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
export const BreadcrumbPage = React.forwardRef<
|
|
67
|
+
HTMLSpanElement,
|
|
68
|
+
React.HTMLAttributes<HTMLSpanElement>
|
|
69
|
+
>(function BreadcrumbPage({ className, ...props }, ref) {
|
|
70
|
+
return (
|
|
71
|
+
<span
|
|
72
|
+
ref={ref}
|
|
73
|
+
role="link"
|
|
74
|
+
aria-current="page"
|
|
75
|
+
aria-disabled="true"
|
|
76
|
+
className={cx(
|
|
77
|
+
"text-foreground font-medium overflow-hidden text-ellipsis whitespace-nowrap",
|
|
78
|
+
className,
|
|
79
|
+
)}
|
|
80
|
+
{...props}
|
|
81
|
+
/>
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
export const BreadcrumbSeparator = React.forwardRef<
|
|
86
|
+
HTMLLIElement,
|
|
87
|
+
React.LiHTMLAttributes<HTMLLIElement>
|
|
88
|
+
>(function BreadcrumbSeparator({ className, children, ...props }, ref) {
|
|
89
|
+
return (
|
|
90
|
+
<li
|
|
91
|
+
ref={ref}
|
|
92
|
+
role="presentation"
|
|
93
|
+
aria-hidden="true"
|
|
94
|
+
className={cx(
|
|
95
|
+
"inline-flex items-center text-foreground-muted opacity-60",
|
|
96
|
+
className,
|
|
97
|
+
)}
|
|
98
|
+
{...props}
|
|
99
|
+
>
|
|
100
|
+
{children ?? <ChevronRightIcon />}
|
|
101
|
+
</li>
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
export const BreadcrumbEllipsis = React.forwardRef<
|
|
106
|
+
HTMLSpanElement,
|
|
107
|
+
React.HTMLAttributes<HTMLSpanElement>
|
|
108
|
+
>(function BreadcrumbEllipsis({ className, ...props }, ref) {
|
|
109
|
+
return (
|
|
110
|
+
<span
|
|
111
|
+
ref={ref}
|
|
112
|
+
role="presentation"
|
|
113
|
+
aria-hidden="true"
|
|
114
|
+
className={cx(
|
|
115
|
+
"inline-flex items-center w-6 h-6 justify-center text-foreground-muted",
|
|
116
|
+
className,
|
|
117
|
+
)}
|
|
118
|
+
{...props}
|
|
119
|
+
>
|
|
120
|
+
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor" aria-hidden>
|
|
121
|
+
<circle cx="3" cy="8" r="1.25" />
|
|
122
|
+
<circle cx="8" cy="8" r="1.25" />
|
|
123
|
+
<circle cx="13" cy="8" r="1.25" />
|
|
124
|
+
</svg>
|
|
125
|
+
<span className="absolute w-px h-px p-0 -m-px overflow-hidden whitespace-nowrap border-0 [clip:rect(0,0,0,0)]">
|
|
126
|
+
더 보기
|
|
127
|
+
</span>
|
|
128
|
+
</span>
|
|
129
|
+
);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
function ChevronRightIcon() {
|
|
133
|
+
return (
|
|
134
|
+
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" aria-hidden>
|
|
135
|
+
<path d="M6 4l4 4-4 4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
136
|
+
</svg>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
3
|
+
|
|
4
|
+
const buttonVariants = cva(
|
|
5
|
+
"inline-flex items-center justify-center gap-[var(--space-2)] border border-transparent rounded-[var(--radius)] font-medium leading-none cursor-pointer select-none transition-[background-color,color,border-color] duration-[var(--duration-fast)] disabled:opacity-[var(--opacity-disabled)] disabled:pointer-events-none focus-visible:outline-2 focus-visible:outline-foreground focus-visible:outline-offset-2 active:scale-[0.97] active:brightness-90",
|
|
6
|
+
{
|
|
7
|
+
variants: {
|
|
8
|
+
variant: {
|
|
9
|
+
primary:
|
|
10
|
+
"bg-primary text-primary-foreground hover:bg-primary-hover",
|
|
11
|
+
secondary:
|
|
12
|
+
"bg-background-muted text-foreground border-border hover:bg-background-subtle",
|
|
13
|
+
ghost:
|
|
14
|
+
"bg-transparent text-foreground hover:bg-background-muted",
|
|
15
|
+
danger:
|
|
16
|
+
"bg-danger text-danger-foreground hover:brightness-95",
|
|
17
|
+
link:
|
|
18
|
+
"bg-transparent text-primary underline-offset-4 hover:underline",
|
|
19
|
+
},
|
|
20
|
+
size: {
|
|
21
|
+
sm: "h-[var(--control-sm)] px-[var(--space-3)] text-[length:var(--text-sm)]",
|
|
22
|
+
md: "h-[var(--control-md)] px-[var(--space-4)] text-[length:var(--text-sm)]",
|
|
23
|
+
lg: "h-[var(--control-lg)] px-[var(--space-5)] text-[length:var(--text-base)]",
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
defaultVariants: { variant: "primary", size: "md" },
|
|
27
|
+
},
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
type Variant = NonNullable<VariantProps<typeof buttonVariants>["variant"]>;
|
|
31
|
+
type Size = NonNullable<VariantProps<typeof buttonVariants>["size"]>;
|
|
32
|
+
|
|
33
|
+
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
34
|
+
/**
|
|
35
|
+
* 시각적 위계.
|
|
36
|
+
* - `primary` — 페이지의 주요 액션. 한 화면에 하나만 권장.
|
|
37
|
+
* - `secondary` — 보조 액션. 약한 배경 + border.
|
|
38
|
+
* - `ghost` — 배경 없는 hover 강조 액션. 툴바/메뉴 항목에 적합.
|
|
39
|
+
* - `danger` — 파괴적 액션(삭제, 취소 등).
|
|
40
|
+
* - `link` — 텍스트 링크처럼 보이는 인라인 버튼.
|
|
41
|
+
*
|
|
42
|
+
* @default "primary"
|
|
43
|
+
*/
|
|
44
|
+
variant?: Variant;
|
|
45
|
+
/**
|
|
46
|
+
* 크기.
|
|
47
|
+
* - `sm` — 조밀한 영역(테이블 행, 툴바)
|
|
48
|
+
* - `md` — 일반
|
|
49
|
+
* - `lg` — CTA·랜딩 영역
|
|
50
|
+
*
|
|
51
|
+
* @default "md"
|
|
52
|
+
*/
|
|
53
|
+
size?: Size;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 사용자 액션을 트리거하는 기본 버튼 (Tailwind utility class 변종).
|
|
58
|
+
* variant로 시각적 위계(primary/secondary/ghost/danger/link)를,
|
|
59
|
+
* size로 크기를 결정한다. 페이지 이동 목적이면 anchor를 감싼 `link` variant를 사용할 것.
|
|
60
|
+
*/
|
|
61
|
+
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
62
|
+
({ variant = "primary", size = "md", className, ...props }, ref) => (
|
|
63
|
+
<button
|
|
64
|
+
ref={ref}
|
|
65
|
+
className={[buttonVariants({ variant, size }), className].filter(Boolean).join(" ")}
|
|
66
|
+
{...props}
|
|
67
|
+
/>
|
|
68
|
+
),
|
|
69
|
+
);
|
|
70
|
+
Button.displayName = "Button";
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
type DivProps = React.HTMLAttributes<HTMLDivElement>;
|
|
4
|
+
|
|
5
|
+
function mergeClass(base: string, extra?: string) {
|
|
6
|
+
return extra ? `${base} ${extra}` : base;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const Card = React.forwardRef<HTMLDivElement, DivProps>(
|
|
10
|
+
({ className, ...props }, ref) => (
|
|
11
|
+
<div
|
|
12
|
+
ref={ref}
|
|
13
|
+
className={mergeClass(
|
|
14
|
+
"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
|
+
className,
|
|
16
|
+
)}
|
|
17
|
+
{...props}
|
|
18
|
+
/>
|
|
19
|
+
),
|
|
20
|
+
);
|
|
21
|
+
Card.displayName = "Card";
|
|
22
|
+
|
|
23
|
+
export const CardHeader = React.forwardRef<HTMLDivElement, DivProps>(
|
|
24
|
+
({ className, ...props }, ref) => (
|
|
25
|
+
<div
|
|
26
|
+
ref={ref}
|
|
27
|
+
data-slot="card-header"
|
|
28
|
+
className={mergeClass(
|
|
29
|
+
"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
|
+
className,
|
|
31
|
+
)}
|
|
32
|
+
{...props}
|
|
33
|
+
/>
|
|
34
|
+
),
|
|
35
|
+
);
|
|
36
|
+
CardHeader.displayName = "CardHeader";
|
|
37
|
+
|
|
38
|
+
export const CardTitle = React.forwardRef<HTMLDivElement, DivProps>(
|
|
39
|
+
({ className, ...props }, ref) => (
|
|
40
|
+
<div
|
|
41
|
+
ref={ref}
|
|
42
|
+
className={mergeClass(
|
|
43
|
+
"text-[length:var(--text-base)] font-semibold leading-tight tracking-tight",
|
|
44
|
+
className,
|
|
45
|
+
)}
|
|
46
|
+
{...props}
|
|
47
|
+
/>
|
|
48
|
+
),
|
|
49
|
+
);
|
|
50
|
+
CardTitle.displayName = "CardTitle";
|
|
51
|
+
|
|
52
|
+
export const CardDescription = React.forwardRef<HTMLDivElement, DivProps>(
|
|
53
|
+
({ className, ...props }, ref) => (
|
|
54
|
+
<div
|
|
55
|
+
ref={ref}
|
|
56
|
+
className={mergeClass(
|
|
57
|
+
"text-[length:var(--text-sm)] leading-normal text-foreground-muted",
|
|
58
|
+
className,
|
|
59
|
+
)}
|
|
60
|
+
{...props}
|
|
61
|
+
/>
|
|
62
|
+
),
|
|
63
|
+
);
|
|
64
|
+
CardDescription.displayName = "CardDescription";
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 헤더 우측에 배치되는 슬롯. CardHeader 내부에서 grid 2번째 컬럼을 차지.
|
|
68
|
+
* CardHeader가 `has-[[data-slot=card-action]]` 으로 감지해 레이아웃을 전환한다.
|
|
69
|
+
*/
|
|
70
|
+
export const CardAction = React.forwardRef<HTMLDivElement, DivProps>(
|
|
71
|
+
({ className, ...props }, ref) => (
|
|
72
|
+
<div
|
|
73
|
+
ref={ref}
|
|
74
|
+
data-slot="card-action"
|
|
75
|
+
className={mergeClass(
|
|
76
|
+
"col-start-2 row-span-2 self-start justify-self-end",
|
|
77
|
+
className,
|
|
78
|
+
)}
|
|
79
|
+
{...props}
|
|
80
|
+
/>
|
|
81
|
+
),
|
|
82
|
+
);
|
|
83
|
+
CardAction.displayName = "CardAction";
|
|
84
|
+
|
|
85
|
+
export const CardContent = React.forwardRef<HTMLDivElement, DivProps>(
|
|
86
|
+
({ className, ...props }, ref) => (
|
|
87
|
+
<div
|
|
88
|
+
ref={ref}
|
|
89
|
+
className={mergeClass(
|
|
90
|
+
"px-[var(--space-6)] text-[length:var(--text-sm)] leading-relaxed max-sm:px-[var(--space-4)]",
|
|
91
|
+
className,
|
|
92
|
+
)}
|
|
93
|
+
{...props}
|
|
94
|
+
/>
|
|
95
|
+
),
|
|
96
|
+
);
|
|
97
|
+
CardContent.displayName = "CardContent";
|
|
98
|
+
|
|
99
|
+
export const CardFooter = React.forwardRef<HTMLDivElement, DivProps>(
|
|
100
|
+
({ className, ...props }, ref) => (
|
|
101
|
+
<div
|
|
102
|
+
ref={ref}
|
|
103
|
+
className={mergeClass(
|
|
104
|
+
"px-[var(--space-6)] flex items-center gap-[var(--space-2)] max-sm:px-[var(--space-4)] max-sm:flex-wrap",
|
|
105
|
+
className,
|
|
106
|
+
)}
|
|
107
|
+
{...props}
|
|
108
|
+
/>
|
|
109
|
+
),
|
|
110
|
+
);
|
|
111
|
+
CardFooter.displayName = "CardFooter";
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Checkbox as BaseCheckbox } from "@base-ui/react/checkbox";
|
|
3
|
+
import { CheckboxGroup as BaseCheckboxGroup } from "@base-ui/react/checkbox-group";
|
|
4
|
+
|
|
5
|
+
function cx(...args: (string | undefined | false)[]) {
|
|
6
|
+
return args.filter(Boolean).join(" ");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type CheckboxProps = Omit<
|
|
10
|
+
React.ComponentPropsWithoutRef<typeof BaseCheckbox.Root>,
|
|
11
|
+
"className"
|
|
12
|
+
> & {
|
|
13
|
+
className?: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const Checkbox = React.forwardRef<HTMLElement, CheckboxProps>(
|
|
17
|
+
({ className, ...props }, ref) => (
|
|
18
|
+
<BaseCheckbox.Root
|
|
19
|
+
ref={ref}
|
|
20
|
+
className={cx(
|
|
21
|
+
"inline-flex items-center justify-center w-[1.125rem] h-[1.125rem] border border-border-strong rounded-[calc(var(--radius)-2px)] bg-background text-primary-foreground cursor-pointer shrink-0 transition-[background-color,border-color] duration-[var(--duration-fast)] hover:not-data-[disabled]:border-foreground focus-visible:outline-[length:var(--border-width-strong)] focus-visible:outline-foreground focus-visible:outline-offset-2 data-[checked]:bg-primary data-[checked]:border-primary data-[indeterminate]:bg-primary data-[indeterminate]:border-primary data-[disabled]:opacity-[var(--opacity-disabled)] data-[disabled]:cursor-not-allowed motion-reduce:transition-none [@media(hover:none)_and_(pointer:coarse)]:w-5 [@media(hover:none)_and_(pointer:coarse)]:h-5",
|
|
22
|
+
className,
|
|
23
|
+
)}
|
|
24
|
+
{...props}
|
|
25
|
+
>
|
|
26
|
+
<BaseCheckbox.Indicator className="inline-flex items-center justify-center">
|
|
27
|
+
{props.indeterminate ? <MinusIcon /> : <CheckIcon />}
|
|
28
|
+
</BaseCheckbox.Indicator>
|
|
29
|
+
</BaseCheckbox.Root>
|
|
30
|
+
),
|
|
31
|
+
);
|
|
32
|
+
Checkbox.displayName = "Checkbox";
|
|
33
|
+
|
|
34
|
+
export type CheckboxGroupProps = Omit<
|
|
35
|
+
React.ComponentPropsWithoutRef<typeof BaseCheckboxGroup>,
|
|
36
|
+
"className"
|
|
37
|
+
> & {
|
|
38
|
+
className?: string;
|
|
39
|
+
orientation?: "horizontal" | "vertical";
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const CheckboxGroup = React.forwardRef<HTMLDivElement, CheckboxGroupProps>(
|
|
43
|
+
({ className, orientation = "vertical", ...props }, ref) => (
|
|
44
|
+
<BaseCheckboxGroup
|
|
45
|
+
ref={ref}
|
|
46
|
+
className={cx(
|
|
47
|
+
"flex gap-2.5",
|
|
48
|
+
orientation === "vertical" ? "flex-col" : "flex-row flex-wrap",
|
|
49
|
+
className,
|
|
50
|
+
)}
|
|
51
|
+
data-orientation={orientation}
|
|
52
|
+
{...props}
|
|
53
|
+
/>
|
|
54
|
+
),
|
|
55
|
+
);
|
|
56
|
+
CheckboxGroup.displayName = "CheckboxGroup";
|
|
57
|
+
|
|
58
|
+
function CheckIcon() {
|
|
59
|
+
return (
|
|
60
|
+
<svg viewBox="0 0 16 16" width="12" height="12" fill="none" aria-hidden>
|
|
61
|
+
<path d="M3.5 8.5l3 3 6-7" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
|
62
|
+
</svg>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function MinusIcon() {
|
|
67
|
+
return (
|
|
68
|
+
<svg viewBox="0 0 16 16" width="12" height="12" fill="none" aria-hidden>
|
|
69
|
+
<path d="M4 8h8" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
|
|
70
|
+
</svg>
|
|
71
|
+
);
|
|
72
|
+
}
|