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 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
- "style": "default",
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-calendar__day sh-ui-calendar__day--hidden" aria-hidden />;
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
- <button
752
+ <div
753
753
  key={i}
754
- type="button"
755
754
  className={cx(
756
- "sh-ui-calendar__day",
757
- !current && "sh-ui-calendar__day--outside",
758
- selected && "sh-ui-calendar__day--selected",
759
- isToday && "sh-ui-calendar__day--today",
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
- {date.getDate()}
774
- </button>
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
- /* ── Day cell ── */
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";