sh-ui-cli 0.42.0 → 0.43.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/button/index.tailwind.tsx +70 -0
- package/data/registry/react/components/calendar/index.tsx +26 -20
- package/data/registry/react/components/calendar/styles.css +30 -44
- package/data/registry/react/components/card/index.tailwind.tsx +111 -0
- package/data/registry/react/components/input/index.tailwind.tsx +405 -0
- package/data/registry/react/peer-versions.json +1 -0
- package/data/registry/react/registry.json +31 -8
- 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.43.0",
|
|
7
|
+
"date": "2026-04-30",
|
|
8
|
+
"title": "CSS 프레임워크 변종 시스템 + Tailwind 1차 지원",
|
|
9
|
+
"type": "minor",
|
|
10
|
+
"highlights": [
|
|
11
|
+
"**`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 (무시).",
|
|
12
|
+
"**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 환경에서 그대로 동작.",
|
|
13
|
+
"**`@theme inline` 단일 소스** — `tokens.css` 가 Tailwind v4 의 `@theme inline { --color-*: var(--*); --radius-{sm,md,lg,xl}; }` 블록을 자동 emit. 템플릿(`nextjs-standalone`, `ui-app-template`)의 하드코딩 `@theme` 제거 — 토큰이 추가/변경돼도 매핑이 자동 동기화.",
|
|
14
|
+
"**토큰 emitter 디스패처** — `packages/tokens/build.mjs` 에 `(platform × cssFramework) → emitter` 테이블. 향후 `react/css-modules`, `react/vanilla-extract` 등 추가 시 한 줄로 등록."
|
|
15
|
+
],
|
|
16
|
+
"url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.43.0"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"version": "0.42.1",
|
|
20
|
+
"date": "2026-04-30",
|
|
21
|
+
"title": "Calendar range 셀 연결 + 검색 다이얼로그 픽스",
|
|
22
|
+
"type": "patch",
|
|
23
|
+
"highlights": [
|
|
24
|
+
"**Calendar range 시각 픽스** — `mode=\"range\"` 에서 시작/종료 사이 셀이 끊김 없는 한 줄의 띠로 이어지도록 수정. 이전엔 그리드 칼럼(1fr) 안에 가운데 정렬된 2.25rem 버튼에 배경을 직접 칠해 칼럼 사이마다 흰 틈이 보였던 이슈",
|
|
25
|
+
"각 날짜 셀을 wrapper `<div class=\"sh-ui-calendar__cell\">` 로 감싸고 in-range / range-start / range-end 배경을 cell 에 적용 — 칼럼을 100% 채우므로 인접 셀이 자연스럽게 맞닿고, 진한 pill (selected) 은 그대로 버튼에 올라감",
|
|
26
|
+
"docs 사이트: 헤더 검색 다이얼로그 버그 픽스 + 디자인 다듬기"
|
|
27
|
+
],
|
|
28
|
+
"url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.42.1"
|
|
29
|
+
},
|
|
5
30
|
{
|
|
6
31
|
"version": "0.42.0",
|
|
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,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";
|
|
@@ -745,33 +745,39 @@ export const CalendarGrid = React.forwardRef<HTMLDivElement, CalendarGridProps>(
|
|
|
745
745
|
const hidden = !current && !ctx.showOutsideDays;
|
|
746
746
|
|
|
747
747
|
if (hidden) {
|
|
748
|
-
return <span key={i} className="sh-ui-
|
|
748
|
+
return <span key={i} className="sh-ui-calendar__cell sh-ui-calendar__cell--hidden" aria-hidden />;
|
|
749
749
|
}
|
|
750
750
|
|
|
751
751
|
return (
|
|
752
|
-
<
|
|
752
|
+
<div
|
|
753
753
|
key={i}
|
|
754
|
-
type="button"
|
|
755
754
|
className={cx(
|
|
756
|
-
"sh-ui-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
inRange && "sh-ui-calendar__day--in-range",
|
|
761
|
-
isStart && "sh-ui-calendar__day--range-start",
|
|
762
|
-
isEnd && "sh-ui-calendar__day--range-end",
|
|
755
|
+
"sh-ui-calendar__cell",
|
|
756
|
+
inRange && "sh-ui-calendar__cell--in-range",
|
|
757
|
+
isStart && "sh-ui-calendar__cell--range-start",
|
|
758
|
+
isEnd && "sh-ui-calendar__cell--range-end",
|
|
763
759
|
)}
|
|
764
|
-
disabled={dDisabled}
|
|
765
|
-
tabIndex={-1}
|
|
766
|
-
onClick={() => { if (!dDisabled) ctx.handleSelect(date); }}
|
|
767
|
-
onMouseEnter={() => ctx.setHoverDate(date)}
|
|
768
|
-
onMouseLeave={() => ctx.setHoverDate(undefined)}
|
|
769
|
-
aria-label={formatIsoDate(date)}
|
|
770
|
-
aria-selected={selected || inRange || undefined}
|
|
771
|
-
data-today={isToday || undefined}
|
|
772
760
|
>
|
|
773
|
-
|
|
774
|
-
|
|
761
|
+
<button
|
|
762
|
+
type="button"
|
|
763
|
+
className={cx(
|
|
764
|
+
"sh-ui-calendar__day",
|
|
765
|
+
!current && "sh-ui-calendar__day--outside",
|
|
766
|
+
selected && "sh-ui-calendar__day--selected",
|
|
767
|
+
isToday && "sh-ui-calendar__day--today",
|
|
768
|
+
)}
|
|
769
|
+
disabled={dDisabled}
|
|
770
|
+
tabIndex={-1}
|
|
771
|
+
onClick={() => { if (!dDisabled) ctx.handleSelect(date); }}
|
|
772
|
+
onMouseEnter={() => ctx.setHoverDate(date)}
|
|
773
|
+
onMouseLeave={() => ctx.setHoverDate(undefined)}
|
|
774
|
+
aria-label={formatIsoDate(date)}
|
|
775
|
+
aria-selected={selected || inRange || undefined}
|
|
776
|
+
data-today={isToday || undefined}
|
|
777
|
+
>
|
|
778
|
+
{date.getDate()}
|
|
779
|
+
</button>
|
|
780
|
+
</div>
|
|
775
781
|
);
|
|
776
782
|
})}
|
|
777
783
|
</div>
|
|
@@ -128,7 +128,36 @@
|
|
|
128
128
|
border-radius: calc(var(--radius) - 2px);
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
-
/* ──
|
|
131
|
+
/* ── Cell (column slot, range strip carrier) ── */
|
|
132
|
+
|
|
133
|
+
.sh-ui-calendar__cell {
|
|
134
|
+
display: flex;
|
|
135
|
+
align-items: center;
|
|
136
|
+
justify-content: center;
|
|
137
|
+
width: 100%;
|
|
138
|
+
height: 2.375rem;
|
|
139
|
+
min-width: 0;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.sh-ui-calendar__cell--in-range,
|
|
143
|
+
.sh-ui-calendar__cell--range-start,
|
|
144
|
+
.sh-ui-calendar__cell--range-end {
|
|
145
|
+
background: color-mix(in srgb, var(--primary) 12%, transparent);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.sh-ui-calendar__cell--range-start:not(.sh-ui-calendar__cell--range-end) {
|
|
149
|
+
border-radius: calc(var(--radius) - 2px) 0 0 calc(var(--radius) - 2px);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.sh-ui-calendar__cell--range-end:not(.sh-ui-calendar__cell--range-start) {
|
|
153
|
+
border-radius: 0 calc(var(--radius) - 2px) calc(var(--radius) - 2px) 0;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.sh-ui-calendar__cell--range-start.sh-ui-calendar__cell--range-end {
|
|
157
|
+
border-radius: calc(var(--radius) - 2px);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/* ── Day button ── */
|
|
132
161
|
|
|
133
162
|
.sh-ui-calendar__day {
|
|
134
163
|
display: flex;
|
|
@@ -136,7 +165,6 @@
|
|
|
136
165
|
justify-content: center;
|
|
137
166
|
width: 2.25rem;
|
|
138
167
|
height: 2.25rem;
|
|
139
|
-
margin: 0.0625rem auto;
|
|
140
168
|
padding: 0;
|
|
141
169
|
border: none;
|
|
142
170
|
border-radius: calc(var(--radius) - 2px);
|
|
@@ -162,13 +190,6 @@
|
|
|
162
190
|
opacity: 0.4;
|
|
163
191
|
}
|
|
164
192
|
|
|
165
|
-
.sh-ui-calendar__day--hidden {
|
|
166
|
-
visibility: hidden;
|
|
167
|
-
pointer-events: none;
|
|
168
|
-
cursor: default;
|
|
169
|
-
background: transparent;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
193
|
.sh-ui-calendar__day--today {
|
|
173
194
|
font-weight: var(--weight-bold);
|
|
174
195
|
text-decoration: underline;
|
|
@@ -190,38 +211,3 @@
|
|
|
190
211
|
opacity: 0.3;
|
|
191
212
|
cursor: not-allowed;
|
|
192
213
|
}
|
|
193
|
-
|
|
194
|
-
/* ── Range ── */
|
|
195
|
-
|
|
196
|
-
.sh-ui-calendar__day--in-range {
|
|
197
|
-
background: color-mix(in srgb, var(--primary) 12%, transparent);
|
|
198
|
-
border-radius: 0;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
.sh-ui-calendar__day--in-range:hover:not(:disabled) {
|
|
202
|
-
background: color-mix(in srgb, var(--primary) 22%, transparent);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
.sh-ui-calendar__day--range-start {
|
|
206
|
-
background: var(--primary);
|
|
207
|
-
color: var(--primary-foreground);
|
|
208
|
-
font-weight: var(--weight-semibold);
|
|
209
|
-
border-radius: calc(var(--radius) - 2px) 0 0 calc(var(--radius) - 2px);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
.sh-ui-calendar__day--range-end {
|
|
213
|
-
background: var(--primary);
|
|
214
|
-
color: var(--primary-foreground);
|
|
215
|
-
font-weight: var(--weight-semibold);
|
|
216
|
-
border-radius: 0 calc(var(--radius) - 2px) calc(var(--radius) - 2px) 0;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
.sh-ui-calendar__day--range-start.sh-ui-calendar__day--range-end {
|
|
220
|
-
border-radius: calc(var(--radius) - 2px);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
.sh-ui-calendar__day--range-start:hover:not(:disabled),
|
|
224
|
-
.sh-ui-calendar__day--range-end:hover:not(:disabled) {
|
|
225
|
-
background: var(--primary-hover);
|
|
226
|
-
color: var(--primary-foreground);
|
|
227
|
-
}
|
|
@@ -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";
|