sh-ui-cli 0.63.1 → 0.64.1
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 +22 -0
- package/data/registry/react/components/input/index.module.tsx +3 -3
- package/data/registry/react/components/input/index.tailwind.tsx +3 -3
- package/data/registry/react/components/input/index.tsx +3 -3
- package/data/registry/react/components/input/index.vanilla-extract.tsx +3 -3
- package/data/summaries/react.json +5 -5
- package/package.json +1 -1
- package/src/create/generator.js +13 -1
- package/src/create/plugins/authJwt.js +12 -0
- package/src/create/plugins/nextIntl.js +28 -1
|
@@ -2,6 +2,28 @@
|
|
|
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.64.1",
|
|
7
|
+
"date": "2026-05-08",
|
|
8
|
+
"title": "patch — llms summary 5종 dot-syntax 정정 + Dialog render-prop 함정 명시",
|
|
9
|
+
"type": "patch",
|
|
10
|
+
"highlights": [
|
|
11
|
+
"**summary 5건 정정 (card / select / dialog / tabs / breadcrumb)** — `compound (Card.Header / ...)` 같이 dot syntax 가 있는 것처럼 적혀 있었지만 실제 구현은 namespace attach 없이 separate export(`CardHeader`, `DialogTitle` 등). MCP `sh_ui_list_components` 와 `apps/docs/public/llms.txt` 가 같은 source 를 읽어 AI 가 잘못된 API 로 코드 생성하던 문제를 차단. card 의 `Body` 도 실제로는 `CardContent` 였던 부분 같이 정정.",
|
|
12
|
+
"**Dialog summary 에 `render` prop 함정 명시** — `DialogTrigger` / `DialogClose` 는 자체로 `<button>` 을 렌더하므로 자식으로 커스텀 Button 을 넣으면 button 중첩(invalid HTML). Base UI 의 `render` prop 으로 슬롯 교체하는 패턴 (`<DialogClose render={<Button>닫기</Button>} />`) 을 summary 에 박아 AI 가 onClick 우회 대신 정석을 사용하도록. SelectTrigger 도 동일 노트."
|
|
13
|
+
],
|
|
14
|
+
"url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.64.1"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"version": "0.64.0",
|
|
18
|
+
"date": "2026-05-08",
|
|
19
|
+
"title": "minor — proxy.ts 에 `HOME_REDIRECT` 자리 + input strict-indexing 회귀 fix",
|
|
20
|
+
"type": "minor",
|
|
21
|
+
"highlights": [
|
|
22
|
+
"**templates proxy.ts 에 `HOME_REDIRECT` 상수 추가** — `/` 진입 시 redirect 할 path 를 사용자가 한 줄로 지정. 빈 문자열이 기본이라 동작 변화 없음(= `app/page.tsx` 그대로 노출). `auth-jwt` 단독 / `next-intl` 단독 / `auth-jwt + next-intl` 합성 세 케이스 모두 동일 패턴. `/[locale]/page.tsx` 에 redirect RSC 를 따로 만들 필요 없이 미들웨어 한 곳에서 라우팅 게이트 처리.",
|
|
23
|
+
"**fix(input)**: `isValidBRN` 의 배열 인덱싱 4곳에 non-null assertion 추가 — `noUncheckedIndexedAccess: true` 인 사용자 프로젝트에서 `parseInt(d[i], 10)` 가 `string | undefined` 로 접혀 typecheck 실패하던 회귀. 직전 길이 가드(`d.length !== 10`) 로 모든 접근이 안전한 구간이라 런타임 동작 변경 없음. plain / tailwind / vanilla-extract / css-modules + `apps/docs` 듀얼카피본 동기화."
|
|
24
|
+
],
|
|
25
|
+
"url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.64.0"
|
|
26
|
+
},
|
|
5
27
|
{
|
|
6
28
|
"version": "0.63.1",
|
|
7
29
|
"date": "2026-05-08",
|
|
@@ -352,10 +352,10 @@ export function isValidBRN(digits: string): boolean {
|
|
|
352
352
|
if (d.length !== 10) return false;
|
|
353
353
|
const w = [1, 3, 7, 1, 3, 7, 1, 3, 5];
|
|
354
354
|
let sum = 0;
|
|
355
|
-
for (let i = 0; i < 9; i++) sum += parseInt(d[i]
|
|
356
|
-
sum += Math.floor((parseInt(d[8]
|
|
355
|
+
for (let i = 0; i < 9; i++) sum += parseInt(d[i]!, 10) * w[i]!;
|
|
356
|
+
sum += Math.floor((parseInt(d[8]!, 10) * 5) / 10);
|
|
357
357
|
const check = (10 - (sum % 10)) % 10;
|
|
358
|
-
return check === parseInt(d[9]
|
|
358
|
+
return check === parseInt(d[9]!, 10);
|
|
359
359
|
}
|
|
360
360
|
|
|
361
361
|
export interface BusinessNumberInputProps
|
|
@@ -298,10 +298,10 @@ export function isValidBRN(digits: string): boolean {
|
|
|
298
298
|
if (d.length !== 10) return false;
|
|
299
299
|
const w = [1, 3, 7, 1, 3, 7, 1, 3, 5];
|
|
300
300
|
let sum = 0;
|
|
301
|
-
for (let i = 0; i < 9; i++) sum += parseInt(d[i]
|
|
302
|
-
sum += Math.floor((parseInt(d[8]
|
|
301
|
+
for (let i = 0; i < 9; i++) sum += parseInt(d[i]!, 10) * w[i]!;
|
|
302
|
+
sum += Math.floor((parseInt(d[8]!, 10) * 5) / 10);
|
|
303
303
|
const check = (10 - (sum % 10)) % 10;
|
|
304
|
-
return check === parseInt(d[9]
|
|
304
|
+
return check === parseInt(d[9]!, 10);
|
|
305
305
|
}
|
|
306
306
|
|
|
307
307
|
export interface BusinessNumberInputProps
|
|
@@ -377,10 +377,10 @@ export function isValidBRN(digits: string): boolean {
|
|
|
377
377
|
if (d.length !== 10) return false;
|
|
378
378
|
const w = [1, 3, 7, 1, 3, 7, 1, 3, 5];
|
|
379
379
|
let sum = 0;
|
|
380
|
-
for (let i = 0; i < 9; i++) sum += parseInt(d[i]
|
|
381
|
-
sum += Math.floor((parseInt(d[8]
|
|
380
|
+
for (let i = 0; i < 9; i++) sum += parseInt(d[i]!, 10) * w[i]!;
|
|
381
|
+
sum += Math.floor((parseInt(d[8]!, 10) * 5) / 10);
|
|
382
382
|
const check = (10 - (sum % 10)) % 10;
|
|
383
|
-
return check === parseInt(d[9]
|
|
383
|
+
return check === parseInt(d[9]!, 10);
|
|
384
384
|
}
|
|
385
385
|
|
|
386
386
|
export interface BusinessNumberInputProps
|
|
@@ -307,10 +307,10 @@ export function isValidBRN(digits: string): boolean {
|
|
|
307
307
|
if (d.length !== 10) return false;
|
|
308
308
|
const w = [1, 3, 7, 1, 3, 7, 1, 3, 5];
|
|
309
309
|
let sum = 0;
|
|
310
|
-
for (let i = 0; i < 9; i++) sum += parseInt(d[i]
|
|
311
|
-
sum += Math.floor((parseInt(d[8]
|
|
310
|
+
for (let i = 0; i < 9; i++) sum += parseInt(d[i]!, 10) * w[i]!;
|
|
311
|
+
sum += Math.floor((parseInt(d[8]!, 10) * 5) / 10);
|
|
312
312
|
const check = (10 - (sum % 10)) % 10;
|
|
313
|
-
return check === parseInt(d[9]
|
|
313
|
+
return check === parseInt(d[9]!, 10);
|
|
314
314
|
}
|
|
315
315
|
|
|
316
316
|
export interface BusinessNumberInputProps
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$description": "React 컴포넌트 summary — llms.txt 생성용. key는 registry.json의 name과 동일.",
|
|
3
3
|
"summaries": {
|
|
4
4
|
"button": "기본 버튼 — variant(primary/secondary/ghost/danger/link) + size(sm/md/lg).",
|
|
5
|
-
"card": "카드 컨테이너 —
|
|
5
|
+
"card": "카드 컨테이너 — separate exports: Card / CardHeader / CardTitle / CardDescription / CardAction / CardContent / CardFooter. dot syntax(`Card.Header`) 아님.",
|
|
6
6
|
"input": "단일 행 텍스트 입력 — hasError 지원. 같은 모듈에 NumberInput / PhoneInput / BusinessNumberInput 변형 포함. 비밀번호 토글은 InputGroup + InputAdornment 레시피로 조립.",
|
|
7
7
|
"numeric-input": "슬라이더 동반·토큰 편집기용 컴팩트 숫자 입력 — onChange 즉시 min/max clamp, focus select-all, 단위(px/ms/%/° 등) suffix. 일반 폼 입력은 NumberInput 권장.",
|
|
8
8
|
"textarea": "여러 행 텍스트 입력 — rows, autoResize.",
|
|
@@ -12,24 +12,24 @@
|
|
|
12
12
|
"switch": "토글 스위치 — controlled/uncontrolled 모두 지원 (Base UI).",
|
|
13
13
|
"toggle": "단일 토글 / 토글 그룹 — pressed 상태 (Base UI).",
|
|
14
14
|
"slider": "수평 슬라이더 — 단일/범위 값, step.",
|
|
15
|
-
"select": "네이티브 대체 셀렉트 —
|
|
15
|
+
"select": "네이티브 대체 셀렉트 — separate exports: Select / SelectTrigger / SelectValue / SelectContent / SelectGroup / SelectLabel / SelectItem / SelectSeparator (Base UI). MultiSelect 변형 동봉. SelectTrigger 는 그 자체로 button 이라 자식 button 중첩 금지 — 다른 엘리먼트로 바꾸려면 Base UI 의 `render` prop 사용.",
|
|
16
16
|
"combobox": "검색 가능 셀렉트 — 자동 필터링, 키보드 내비 (Base UI).",
|
|
17
17
|
"color-picker": "색상 선택 — hex/rgb, 프리셋 팔레트.",
|
|
18
18
|
"date-picker": "날짜 선택 — 캘린더 팝업 (Base UI).",
|
|
19
19
|
"file-upload": "파일 업로드 — 드롭존, 다중 파일, 진행률.",
|
|
20
|
-
"dialog": "모달 다이얼로그 —
|
|
20
|
+
"dialog": "모달 다이얼로그 — separate exports: Dialog / DialogTrigger / DialogClose / DialogContent / DialogTitle / DialogDescription / DialogFooter / DialogCloseX (Base UI, 포커스 트랩). DialogTrigger·DialogClose 는 그 자체로 button 을 렌더 — 자식으로 다른 button 을 넣으면 button 중첩(invalid HTML). 다른 엘리먼트(예: 커스텀 Button)로 슬롯하려면 Base UI 의 `render` prop 사용: `<DialogClose render={<Button>닫기</Button>} />`.",
|
|
21
21
|
"popover": "floating 팝오버 — 트리거 기준 위치 (Base UI).",
|
|
22
22
|
"tooltip": "짧은 힌트 — hover/focus 표시, delay/closeDelay (Base UI).",
|
|
23
23
|
"toast": "임시 알림 — useToast 훅 + ToastProvider. aria-live 자동.",
|
|
24
24
|
"dropdown-menu": "드롭다운 메뉴 — compound, sub-menu 지원 (Base UI).",
|
|
25
25
|
"context-menu": "우클릭 컨텍스트 메뉴 — DropdownMenu와 같은 구조 (Base UI).",
|
|
26
26
|
"menubar": "수평 메뉴바 — dropdown-menu 위에 구성 (Base UI).",
|
|
27
|
-
"tabs": "탭 —
|
|
27
|
+
"tabs": "탭 — separate exports: Tabs / TabsList / TabsTrigger / TabsContent / TabsIndicator (Base UI). dot syntax(`Tabs.List`) 아님.",
|
|
28
28
|
"accordion": "펼침/접힘 아코디언 — single/multiple (Base UI).",
|
|
29
29
|
"carousel": "슬라이드 캐러셀 — Embla 기반, autoplay/autoscroll.",
|
|
30
30
|
"sidebar": "앱 사이드바 — collapsible, SidebarMenu/SidebarGroup 조합.",
|
|
31
31
|
"header": "앱 헤더 — 로고/네비/액션 compound, 데스크탑 inline / 모바일 drawer 자동 전환. drawer focus trap·ESC·focus restore. HeaderNav value(controlled) / defaultValue+onValueChange(uncontrolled) 로 자식 HeaderItem active 자동 매칭 (aria-current 자동, match 커스터마이즈). HeaderMenu(서브메뉴, 데스크탑 portal dropdown / drawer collapsible) · HeaderNavGroup(섹션 라벨) · HeaderDesktopOnly/HeaderMobileOnly(가시성 토글, drawer 이동 없음). variant(solid/transparent/blur) · stickyHide(prefers-reduced-motion 존중, 컨테이너 스크롤 자동 감지) 정식 지원. backdrop-filter @supports 폴백.",
|
|
32
|
-
"breadcrumb": "경로 내비게이션 —
|
|
32
|
+
"breadcrumb": "경로 내비게이션 — separate exports: Breadcrumb / BreadcrumbList / BreadcrumbItem / BreadcrumbLink / BreadcrumbPage / BreadcrumbSeparator / BreadcrumbEllipsis. dot syntax 아님. aria-current 자동.",
|
|
33
33
|
"pagination": "페이지 단위 내비게이션 — compound (PaginationContent/Item/Link/Previous/Next/Ellipsis). getPaginationRange 유틸 동봉. aria-current 자동.",
|
|
34
34
|
"avatar": "프로필 아바타 — 이미지 fallback → initials (Base UI).",
|
|
35
35
|
"badge": "상태 뱃지 — variant, size.",
|
package/package.json
CHANGED
package/src/create/generator.js
CHANGED
|
@@ -1348,6 +1348,13 @@ import { routing } from '${configAlias}/i18n/routing';
|
|
|
1348
1348
|
|
|
1349
1349
|
const AUTH_ROUTES = ['/sign-in', '/sign-up'];
|
|
1350
1350
|
|
|
1351
|
+
/**
|
|
1352
|
+
* 홈(\`/\`, \`/{locale}\`) 진입 시 redirect 할 path. 빈 문자열이면
|
|
1353
|
+
* \`app/[locale]/page.tsx\` 가 그대로 노출. 예: '/dashboard', '/projects'.
|
|
1354
|
+
* 인증 가드 위에서 동작하므로 미인증이면 그대로 \`/sign-in\` 으로 빠진다.
|
|
1355
|
+
*/
|
|
1356
|
+
const HOME_REDIRECT = '';
|
|
1357
|
+
|
|
1351
1358
|
const intl = createIntlMiddleware(routing);
|
|
1352
1359
|
|
|
1353
1360
|
/**
|
|
@@ -1368,7 +1375,8 @@ const stripLocalePrefix = (pathname: string): string => {
|
|
|
1368
1375
|
* Next 16+ proxy.ts (구 middleware.ts).
|
|
1369
1376
|
* next-intl 라우팅 + auth-jwt 토큰 존재 체크 합성 버전.
|
|
1370
1377
|
*
|
|
1371
|
-
* -
|
|
1378
|
+
* - \`/\` + HOME_REDIRECT 설정 → 해당 경로로 리다이렉트 (인증 가드보다 먼저)
|
|
1379
|
+
* - intl 이 로케일 prefix 처리 + NEXT_LOCALE 쿠키 set
|
|
1372
1380
|
* - 그 위에 인증 가드 — 토큰 없고 인증 라우트도 아니면 /sign-in 으로 redirect
|
|
1373
1381
|
* - AT 만료 검사나 refresh 는 하지 않는다 (BFF 가 처리)
|
|
1374
1382
|
*/
|
|
@@ -1378,6 +1386,10 @@ export default function proxy(req: NextRequest) {
|
|
|
1378
1386
|
const hasToken = !!req.cookies.get('accessToken')?.value;
|
|
1379
1387
|
const isAuthRoute = AUTH_ROUTES.some((r) => pathname.startsWith(r));
|
|
1380
1388
|
|
|
1389
|
+
if (pathname === '/' && HOME_REDIRECT) {
|
|
1390
|
+
return NextResponse.redirect(new URL(HOME_REDIRECT, req.url));
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1381
1393
|
if (isAuthRoute) return intlRes;
|
|
1382
1394
|
if (!hasToken) return NextResponse.redirect(new URL('/sign-in', req.url));
|
|
1383
1395
|
|
|
@@ -87,10 +87,18 @@ export default function SignInPage() {
|
|
|
87
87
|
|
|
88
88
|
const AUTH_ROUTES = ['/sign-in', '/sign-up'];
|
|
89
89
|
|
|
90
|
+
/**
|
|
91
|
+
* 홈(\`/\`) 진입 시 redirect 할 path. 빈 문자열이면 \`app/page.tsx\` 가 그대로 노출.
|
|
92
|
+
* 예: '/dashboard', '/projects'. 인증 가드 위에서 동작하므로 미인증이면
|
|
93
|
+
* 그대로 \`/sign-in\` 으로 빠진다.
|
|
94
|
+
*/
|
|
95
|
+
const HOME_REDIRECT = '';
|
|
96
|
+
|
|
90
97
|
/**
|
|
91
98
|
* Next 16+ 의 proxy.ts (구 middleware.ts).
|
|
92
99
|
* 토큰 존재 여부만 검사한다 — 만료 검사나 refresh 는 하지 않는다.
|
|
93
100
|
*
|
|
101
|
+
* - \`/\` + HOME_REDIRECT 설정 → 해당 경로로 리다이렉트
|
|
94
102
|
* - AT 쿠키 없음 + 인증 라우트 아님 → /sign-in 으로 리다이렉트
|
|
95
103
|
* - AT 쿠키 있음 또는 인증 라우트 → 통과
|
|
96
104
|
*
|
|
@@ -102,6 +110,10 @@ export default function proxy(req: NextRequest) {
|
|
|
102
110
|
const hasToken = !!req.cookies.get('accessToken')?.value;
|
|
103
111
|
const isAuthRoute = AUTH_ROUTES.some((r) => pathname.startsWith(r));
|
|
104
112
|
|
|
113
|
+
if (pathname === '/' && HOME_REDIRECT) {
|
|
114
|
+
return NextResponse.redirect(new URL(HOME_REDIRECT, req.url));
|
|
115
|
+
}
|
|
116
|
+
|
|
105
117
|
if (isAuthRoute) return NextResponse.next();
|
|
106
118
|
if (!hasToken) return NextResponse.redirect(new URL('/sign-in', req.url));
|
|
107
119
|
|
|
@@ -325,11 +325,38 @@ export const { Link, redirect, usePathname, useRouter, getPathname } =
|
|
|
325
325
|
`,
|
|
326
326
|
|
|
327
327
|
'proxy.ts': `import createIntlMiddleware from 'next-intl/middleware';
|
|
328
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
329
|
+
|
|
328
330
|
import { routing } from '${arch.aliases.config}/i18n/routing';
|
|
329
331
|
|
|
332
|
+
/**
|
|
333
|
+
* 홈(\`/\`, \`/{locale}\`) 진입 시 redirect 할 path. 빈 문자열이면
|
|
334
|
+
* \`app/[locale]/page.tsx\` 가 그대로 노출. 예: '/dashboard', '/projects'.
|
|
335
|
+
*/
|
|
336
|
+
const HOME_REDIRECT = '';
|
|
337
|
+
|
|
330
338
|
const intl = createIntlMiddleware(routing);
|
|
331
339
|
|
|
332
|
-
|
|
340
|
+
/**
|
|
341
|
+
* 로케일 prefix (/ko, /en) 를 벗겨 홈 매칭 (\`/\`) 에 사용한다.
|
|
342
|
+
* 예: /ko → /, /ko/posts → /posts.
|
|
343
|
+
*/
|
|
344
|
+
const stripLocalePrefix = (pathname: string): string => {
|
|
345
|
+
const locales = routing.locales as readonly string[];
|
|
346
|
+
const segments = pathname.split('/').filter(Boolean);
|
|
347
|
+
if (segments[0] && locales.includes(segments[0])) {
|
|
348
|
+
const rest = segments.slice(1).join('/');
|
|
349
|
+
return \`/\${rest}\`.replace(/\\/$/, '') || '/';
|
|
350
|
+
}
|
|
351
|
+
return pathname;
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
export default function proxy(req: NextRequest) {
|
|
355
|
+
if (HOME_REDIRECT && stripLocalePrefix(req.nextUrl.pathname) === '/') {
|
|
356
|
+
return NextResponse.redirect(new URL(HOME_REDIRECT, req.url));
|
|
357
|
+
}
|
|
358
|
+
return intl(req);
|
|
359
|
+
}
|
|
333
360
|
|
|
334
361
|
export const config = {
|
|
335
362
|
matcher: '/((?!api|trpc|_next|_vercel|monitoring|.*\\\\..*).*)',
|