sh-ui-cli 0.74.0 → 0.75.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/breadcrumb/index.module.tsx +33 -8
- package/data/registry/react/components/breadcrumb/index.tailwind.tsx +35 -10
- package/data/registry/react/components/breadcrumb/index.tsx +36 -8
- package/data/registry/react/components/sidebar/index.module.tsx +39 -32
- package/data/registry/react/components/sidebar/index.tailwind.tsx +37 -21
- package/data/registry/react/components/sidebar/index.tsx +49 -32
- package/data/summaries/react.json +2 -2
- package/package.json +2 -1
- package/templates/nextjs-app/_arch/flat/tsconfig.json +2 -1
- package/templates/nextjs-app/_arch/fsd/tsconfig.json +2 -1
- package/templates/nextjs-app/package.json +1 -0
- package/templates/nextjs-app/vitest.config.ts +4 -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.75.0",
|
|
7
|
+
"date": "2026-05-11",
|
|
8
|
+
"title": "minor — 슬롯 패턴 `render` 로 통일 (asChild 제거)",
|
|
9
|
+
"type": "minor",
|
|
10
|
+
"highlights": [
|
|
11
|
+
"**모든 슬롯 패턴이 `render` prop 으로 통일** — Base UI 가 표준으로 사용하는 패턴. 이전엔 Base UI 기반 컴포넌트(Dialog/Popover/Select/DropdownMenu/Accordion/Combobox/Tabs)는 `render`, sh-ui 자체 컴포넌트(Sidebar/Breadcrumb)는 `asChild` 로 혼재. 이번에 후자도 `render` 로 통일해 사용자 mental model 단순화.",
|
|
12
|
+
"**BreadcrumbLink 에 `render` prop 신규 추가** — 이전엔 슬롯 패턴 자체가 없어 Next Link 와 결합하려면 className 만 수동 복사해야 했음. 이제 `<BreadcrumbLink render={<Link href='/projects'>Projects</Link>} />` 정석.",
|
|
13
|
+
"**SidebarMenuButton·SidebarMenuSubButton 의 `asChild` 제거 → `render` 로 마이그레이션** — children 으로 자식 element 넘기던 패턴 대신 `render` prop. cloneElement 로 props/className/ref 자동 머지 동작은 동일. apps/docs 의 app-sidebar / sidebar-toc demo / sidebar API 문서까지 일괄 마이그.",
|
|
14
|
+
"**summary + JSDoc 갱신** — sidebar / breadcrumb summary 에 render prop 사용 패턴 명시. MCP `sh_ui_list_components` 와 `sh_ui_get_component` 둘 다 일관."
|
|
15
|
+
],
|
|
16
|
+
"url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.75.0"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"version": "0.74.1",
|
|
20
|
+
"date": "2026-05-11",
|
|
21
|
+
"title": "patch — apps/* templates 에 `@workspace/ui-core` 의존성/paths 누락 수정",
|
|
22
|
+
"type": "patch",
|
|
23
|
+
"highlights": [
|
|
24
|
+
"**templates 의 apps/* 가 ui-core 를 의존성으로 들고 있지 않던 회귀 수정** — v0.65 에서 컴포넌트 SoT 가 `ui-core` 로 이동했지만 `nextjs-app/package.json` 과 `_arch/{fsd,flat}/tsconfig.json`, `vitest.config.ts` 가 `@workspace/ui-app-name` 만 가리키고 `@workspace/ui-core` 는 박혀 있지 않았음. 결과: 사용자가 ui-core 컴포넌트(`@workspace/ui-core/components/sidebar` 등)를 import 하려면 매번 손으로 deps + tsconfig paths 추가해야 했음.",
|
|
25
|
+
"**fix 후 신규 스캐폴드/`sh_ui_add_app` 의 새 앱이 자동으로 ui-core dep + paths 보유** — `pnpm install` 하자마자 `@workspace/ui-core/components/*` import 동작.",
|
|
26
|
+
"**smoke scenario 2 회귀 가드** — `appPkg.dependencies['@workspace/ui-core']` 와 `tsconfig.compilerOptions.paths['@workspace/ui-core/*']` 단언 추가.",
|
|
27
|
+
"**`pretest` 훅으로 stale bundled data 자동 갱신** — `packages/cli/data/` 는 gitignored 라 개발자 머신에서 stale 일 수 있고, 그 상태에서 `pnpm test` 돌리면 `tokens-validate.test.js` 1건이 `tokens-used.json` 누락으로 false-fail 했음. `pretest` 가 `copy-data.mjs` 를 자동 실행해 회귀 차단."
|
|
28
|
+
],
|
|
29
|
+
"url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.74.1"
|
|
30
|
+
},
|
|
5
31
|
{
|
|
6
32
|
"version": "0.74.0",
|
|
7
33
|
"date": "2026-05-10",
|
|
@@ -59,17 +59,42 @@ export const BreadcrumbItem = React.forwardRef<
|
|
|
59
59
|
|
|
60
60
|
/* ───────── Link ───────── */
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
export interface BreadcrumbLinkProps
|
|
63
|
+
extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
|
|
64
|
+
/**
|
|
65
|
+
* 다른 anchor 컴포넌트(예: Next.js `Link`)로 대체. sh-ui 의 모든 슬롯
|
|
66
|
+
* 패턴은 `render` 로 통일 (Base UI 표준).
|
|
67
|
+
*
|
|
68
|
+
* <BreadcrumbLink render={<Link href='/projects'>Projects</Link>} />
|
|
69
|
+
*/
|
|
70
|
+
render?: React.ReactElement;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** 상위 단계로 이동하는 링크. `render` prop 으로 다른 엘리먼트 슬롯 가능. */
|
|
63
74
|
export const BreadcrumbLink = React.forwardRef<
|
|
64
75
|
HTMLAnchorElement,
|
|
65
|
-
|
|
66
|
-
>(function BreadcrumbLink({ className, ...props }, ref) {
|
|
76
|
+
BreadcrumbLinkProps
|
|
77
|
+
>(function BreadcrumbLink({ className, render, children, ...props }, ref) {
|
|
78
|
+
const mergedClass = cn(styles.breadcrumb__link, className);
|
|
79
|
+
if (render && React.isValidElement(render)) {
|
|
80
|
+
const child = render as React.ReactElement<{
|
|
81
|
+
className?: string;
|
|
82
|
+
children?: React.ReactNode;
|
|
83
|
+
}>;
|
|
84
|
+
return React.cloneElement(
|
|
85
|
+
child,
|
|
86
|
+
{
|
|
87
|
+
ref,
|
|
88
|
+
className: cn(child.props.className, mergedClass),
|
|
89
|
+
...props,
|
|
90
|
+
} as Record<string, unknown>,
|
|
91
|
+
children ?? child.props.children,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
67
94
|
return (
|
|
68
|
-
<a
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
{...props}
|
|
72
|
-
/>
|
|
95
|
+
<a ref={ref} className={mergedClass} {...props}>
|
|
96
|
+
{children}
|
|
97
|
+
</a>
|
|
73
98
|
);
|
|
74
99
|
});
|
|
75
100
|
|
|
@@ -45,19 +45,44 @@ export const BreadcrumbItem = React.forwardRef<
|
|
|
45
45
|
);
|
|
46
46
|
});
|
|
47
47
|
|
|
48
|
+
export interface BreadcrumbLinkProps
|
|
49
|
+
extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
|
|
50
|
+
/**
|
|
51
|
+
* 다른 anchor 컴포넌트(예: Next.js `Link`)로 대체. anchor 중첩 방지 + 라우터
|
|
52
|
+
* 결합용. sh-ui 의 모든 슬롯 패턴은 `render` 로 통일 (Base UI 표준).
|
|
53
|
+
*
|
|
54
|
+
* <BreadcrumbLink render={<Link href='/projects'>Projects</Link>} />
|
|
55
|
+
*/
|
|
56
|
+
render?: React.ReactElement;
|
|
57
|
+
}
|
|
58
|
+
|
|
48
59
|
export const BreadcrumbLink = React.forwardRef<
|
|
49
60
|
HTMLAnchorElement,
|
|
50
|
-
|
|
51
|
-
>(function BreadcrumbLink({ className, ...props }, ref) {
|
|
61
|
+
BreadcrumbLinkProps
|
|
62
|
+
>(function BreadcrumbLink({ className, render, children, ...props }, ref) {
|
|
63
|
+
const mergedClass = cn(
|
|
64
|
+
"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-ring focus-visible:outline-offset-2 motion-reduce:transition-none",
|
|
65
|
+
className,
|
|
66
|
+
);
|
|
67
|
+
if (render && React.isValidElement(render)) {
|
|
68
|
+
const child = render as React.ReactElement<{
|
|
69
|
+
className?: string;
|
|
70
|
+
children?: React.ReactNode;
|
|
71
|
+
}>;
|
|
72
|
+
return React.cloneElement(
|
|
73
|
+
child,
|
|
74
|
+
{
|
|
75
|
+
ref,
|
|
76
|
+
className: cn(child.props.className, mergedClass),
|
|
77
|
+
...props,
|
|
78
|
+
} as Record<string, unknown>,
|
|
79
|
+
children ?? child.props.children,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
52
82
|
return (
|
|
53
|
-
<a
|
|
54
|
-
|
|
55
|
-
|
|
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-ring focus-visible:outline-offset-2 motion-reduce:transition-none",
|
|
57
|
-
className,
|
|
58
|
-
)}
|
|
59
|
-
{...props}
|
|
60
|
-
/>
|
|
83
|
+
<a ref={ref} className={mergedClass} {...props}>
|
|
84
|
+
{children}
|
|
85
|
+
</a>
|
|
61
86
|
);
|
|
62
87
|
});
|
|
63
88
|
|
|
@@ -59,17 +59,45 @@ export const BreadcrumbItem = React.forwardRef<
|
|
|
59
59
|
|
|
60
60
|
/* ───────── Link ───────── */
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
export interface BreadcrumbLinkProps
|
|
63
|
+
extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
|
|
64
|
+
/**
|
|
65
|
+
* 다른 anchor 컴포넌트(예: Next.js `Link`)로 대체. 자체로 `<a>` 를 렌더
|
|
66
|
+
* 하므로 자식으로 또 다른 `<a>` 를 넣으면 anchor 중첩(invalid HTML). 정석은
|
|
67
|
+
* Base UI 패턴과 동일한 `render` prop:
|
|
68
|
+
*
|
|
69
|
+
* <BreadcrumbLink render={<Link href='/projects'>Projects</Link>} />
|
|
70
|
+
*
|
|
71
|
+
* sh-ui 의 모든 슬롯 패턴은 `render` 로 통일 (Base UI 표준 따름).
|
|
72
|
+
*/
|
|
73
|
+
render?: React.ReactElement;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** 상위 단계로 이동하는 링크. `render` prop 으로 다른 엘리먼트(Next Link 등) 슬롯 가능. */
|
|
63
77
|
export const BreadcrumbLink = React.forwardRef<
|
|
64
78
|
HTMLAnchorElement,
|
|
65
|
-
|
|
66
|
-
>(function BreadcrumbLink({ className, ...props }, ref) {
|
|
79
|
+
BreadcrumbLinkProps
|
|
80
|
+
>(function BreadcrumbLink({ className, render, children, ...props }, ref) {
|
|
81
|
+
const mergedClass = cn("sh-ui-breadcrumb__link", className);
|
|
82
|
+
if (render && React.isValidElement(render)) {
|
|
83
|
+
const child = render as React.ReactElement<{
|
|
84
|
+
className?: string;
|
|
85
|
+
children?: React.ReactNode;
|
|
86
|
+
}>;
|
|
87
|
+
return React.cloneElement(
|
|
88
|
+
child,
|
|
89
|
+
{
|
|
90
|
+
ref,
|
|
91
|
+
className: cn(child.props.className, mergedClass),
|
|
92
|
+
...props,
|
|
93
|
+
} as Record<string, unknown>,
|
|
94
|
+
children ?? child.props.children,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
67
97
|
return (
|
|
68
|
-
<a
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
{...props}
|
|
72
|
-
/>
|
|
98
|
+
<a ref={ref} className={mergedClass} {...props}>
|
|
99
|
+
{children}
|
|
100
|
+
</a>
|
|
73
101
|
);
|
|
74
102
|
});
|
|
75
103
|
|
|
@@ -591,12 +591,13 @@ export interface SidebarMenuButtonProps extends React.ButtonHTMLAttributes<HTMLB
|
|
|
591
591
|
*/
|
|
592
592
|
size?: "sm" | "md" | "lg";
|
|
593
593
|
/**
|
|
594
|
-
*
|
|
595
|
-
*
|
|
594
|
+
* 다른 엘리먼트(예: Next.js `Link`)로 대체. 자체로 `<button>` 을 렌더하므로
|
|
595
|
+
* 자식으로 또 다른 button/anchor 를 넣지 말 것. sh-ui 의 모든 슬롯 패턴은
|
|
596
|
+
* `render` 로 통일 (Base UI 표준).
|
|
596
597
|
*
|
|
597
|
-
*
|
|
598
|
+
* <SidebarMenuButton render={<Link href='/'>홈</Link>} />
|
|
598
599
|
*/
|
|
599
|
-
|
|
600
|
+
render?: React.ReactElement;
|
|
600
601
|
/**
|
|
601
602
|
* `SidebarTOC` 안에서 활성 섹션 id를 자동 동기화. 이 값과 TOC active id가 일치하면
|
|
602
603
|
* `isActive`가 자동으로 `true`가 된다.
|
|
@@ -610,12 +611,13 @@ export interface SidebarMenuButtonProps extends React.ButtonHTMLAttributes<HTMLB
|
|
|
610
611
|
}
|
|
611
612
|
|
|
612
613
|
/**
|
|
613
|
-
* 메뉴 한 줄을 누를 수 있는 버튼. `
|
|
614
|
-
* `sectionId`(SidebarTOC 활성 동기화) / `panelId`(SidebarPanel 토글)를
|
|
614
|
+
* 메뉴 한 줄을 누를 수 있는 버튼. `render` prop 으로 `<a>` 등 다른 엘리먼트로
|
|
615
|
+
* 슬롯 가능. `sectionId`(SidebarTOC 활성 동기화) / `panelId`(SidebarPanel 토글)를
|
|
616
|
+
* 지정해 활성 상태를 자동 결정한다.
|
|
615
617
|
*/
|
|
616
618
|
export const SidebarMenuButton = React.forwardRef<HTMLButtonElement, SidebarMenuButtonProps>(
|
|
617
619
|
function SidebarMenuButton(
|
|
618
|
-
{ className, isActive, size = "md",
|
|
620
|
+
{ className, isActive, size = "md", render, sectionId, panelId, onClick, children, ...props },
|
|
619
621
|
ref
|
|
620
622
|
) {
|
|
621
623
|
const tocActive = useTOCActiveId();
|
|
@@ -640,15 +642,19 @@ export const SidebarMenuButton = React.forwardRef<HTMLButtonElement, SidebarMenu
|
|
|
640
642
|
styles[`sidebar__menu-button--${size}`],
|
|
641
643
|
className);
|
|
642
644
|
|
|
643
|
-
if (
|
|
644
|
-
const child =
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
645
|
+
if (render && React.isValidElement(render)) {
|
|
646
|
+
const child = render as React.ReactElement<{ className?: string; children?: React.ReactNode }>;
|
|
647
|
+
return React.cloneElement(
|
|
648
|
+
child,
|
|
649
|
+
{
|
|
650
|
+
ref,
|
|
651
|
+
...props,
|
|
652
|
+
onClick: handleClick,
|
|
653
|
+
className: cn(child.props.className, cls),
|
|
654
|
+
"data-active": resolvedIsActive || undefined,
|
|
655
|
+
} as Record<string, unknown>,
|
|
656
|
+
children ?? child.props.children,
|
|
657
|
+
);
|
|
652
658
|
}
|
|
653
659
|
|
|
654
660
|
return (
|
|
@@ -697,18 +703,17 @@ export interface SidebarMenuSubButtonProps extends React.AnchorHTMLAttributes<HT
|
|
|
697
703
|
*/
|
|
698
704
|
size?: "sm" | "md";
|
|
699
705
|
/**
|
|
700
|
-
*
|
|
701
|
-
* @default false
|
|
706
|
+
* 다른 anchor 컴포넌트(예: Next.js `Link`)로 대체. sh-ui 의 슬롯 패턴은 `render` 로 통일.
|
|
702
707
|
*/
|
|
703
|
-
|
|
708
|
+
render?: React.ReactElement;
|
|
704
709
|
/** `SidebarTOC`의 활성 섹션 id 자동 동기화. 일치하면 `isActive`가 자동으로 `true`. */
|
|
705
710
|
sectionId?: string;
|
|
706
711
|
}
|
|
707
712
|
|
|
708
|
-
/** 서브 메뉴 항목 내부의
|
|
713
|
+
/** 서브 메뉴 항목 내부의 링크. `render` prop 으로 다른 엘리먼트 슬롯 가능. `sectionId`로 TOC 활성 연동. */
|
|
709
714
|
export const SidebarMenuSubButton = React.forwardRef<HTMLAnchorElement, SidebarMenuSubButtonProps>(
|
|
710
715
|
function SidebarMenuSubButton(
|
|
711
|
-
{ className, isActive, size = "md",
|
|
716
|
+
{ className, isActive, size = "md", render, sectionId, children, ...props },
|
|
712
717
|
ref
|
|
713
718
|
) {
|
|
714
719
|
const tocActive = useTOCActiveId();
|
|
@@ -718,14 +723,18 @@ export const SidebarMenuSubButton = React.forwardRef<HTMLAnchorElement, SidebarM
|
|
|
718
723
|
styles[`sidebar__menu-sub-button--${size}`],
|
|
719
724
|
className);
|
|
720
725
|
|
|
721
|
-
if (
|
|
722
|
-
const child =
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
726
|
+
if (render && React.isValidElement(render)) {
|
|
727
|
+
const child = render as React.ReactElement<{ className?: string; children?: React.ReactNode }>;
|
|
728
|
+
return React.cloneElement(
|
|
729
|
+
child,
|
|
730
|
+
{
|
|
731
|
+
ref,
|
|
732
|
+
...props,
|
|
733
|
+
className: cn(child.props.className, cls),
|
|
734
|
+
"data-active": resolvedIsActive || undefined,
|
|
735
|
+
} as Record<string, unknown>,
|
|
736
|
+
children ?? child.props.children,
|
|
737
|
+
);
|
|
729
738
|
}
|
|
730
739
|
|
|
731
740
|
return (
|
|
@@ -930,9 +939,7 @@ export function SidebarCollapsibleContent({ children }: { children: React.ReactN
|
|
|
930
939
|
* <SidebarTOC sectionIds={["intro", "install", "usage"]}>
|
|
931
940
|
* <SidebarMenu>
|
|
932
941
|
* <SidebarMenuItem>
|
|
933
|
-
* <SidebarMenuButton sectionId="intro"
|
|
934
|
-
* <a href="#intro">Intro</a>
|
|
935
|
-
* </SidebarMenuButton>
|
|
942
|
+
* <SidebarMenuButton sectionId="intro" render={<a href="#intro">Intro</a>} />
|
|
936
943
|
* </SidebarMenuItem>
|
|
937
944
|
* ...
|
|
938
945
|
* </SidebarMenu>
|
|
@@ -350,7 +350,12 @@ export function SidebarMenuItem({ className, ...props }: React.HTMLAttributes<HT
|
|
|
350
350
|
export interface SidebarMenuButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
351
351
|
isActive?: boolean;
|
|
352
352
|
size?: "sm" | "md" | "lg";
|
|
353
|
-
|
|
353
|
+
/**
|
|
354
|
+
* 다른 엘리먼트(예: Next.js `Link`)로 대체. sh-ui 의 슬롯 패턴은 `render` 로 통일.
|
|
355
|
+
*
|
|
356
|
+
* <SidebarMenuButton render={<Link href='/'>홈</Link>} />
|
|
357
|
+
*/
|
|
358
|
+
render?: React.ReactElement;
|
|
354
359
|
sectionId?: string;
|
|
355
360
|
panelId?: string;
|
|
356
361
|
}
|
|
@@ -365,7 +370,7 @@ const menuButtonSize = {
|
|
|
365
370
|
};
|
|
366
371
|
|
|
367
372
|
export const SidebarMenuButton = React.forwardRef<HTMLButtonElement, SidebarMenuButtonProps>(
|
|
368
|
-
function SidebarMenuButton({ className, isActive, size = "md",
|
|
373
|
+
function SidebarMenuButton({ className, isActive, size = "md", render, sectionId, panelId, onClick, children, ...props }, ref) {
|
|
369
374
|
const tocActive = useTOCActiveId();
|
|
370
375
|
const ctx = React.useContext(SidebarContext);
|
|
371
376
|
const panelActive = panelId != null && ctx?.activePanel === panelId;
|
|
@@ -378,15 +383,19 @@ export const SidebarMenuButton = React.forwardRef<HTMLButtonElement, SidebarMenu
|
|
|
378
383
|
|
|
379
384
|
const cls = cn(menuButtonBase, menuButtonSize[size], className);
|
|
380
385
|
|
|
381
|
-
if (
|
|
382
|
-
const child =
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
386
|
+
if (render && React.isValidElement(render)) {
|
|
387
|
+
const child = render as React.ReactElement<{ className?: string; children?: React.ReactNode }>;
|
|
388
|
+
return React.cloneElement(
|
|
389
|
+
child,
|
|
390
|
+
{
|
|
391
|
+
ref,
|
|
392
|
+
...props,
|
|
393
|
+
onClick: handleClick,
|
|
394
|
+
className: cn(child.props.className, cls),
|
|
395
|
+
"data-active": resolvedIsActive || undefined,
|
|
396
|
+
} as Record<string, unknown>,
|
|
397
|
+
children ?? child.props.children,
|
|
398
|
+
);
|
|
390
399
|
}
|
|
391
400
|
|
|
392
401
|
return (
|
|
@@ -414,7 +423,10 @@ export function SidebarMenuSubItem({ className, ...props }: React.HTMLAttributes
|
|
|
414
423
|
export interface SidebarMenuSubButtonProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
|
|
415
424
|
isActive?: boolean;
|
|
416
425
|
size?: "sm" | "md";
|
|
417
|
-
|
|
426
|
+
/**
|
|
427
|
+
* 다른 anchor 컴포넌트(예: Next.js `Link`)로 대체. sh-ui 의 슬롯 패턴은 `render` 로 통일.
|
|
428
|
+
*/
|
|
429
|
+
render?: React.ReactElement;
|
|
418
430
|
sectionId?: string;
|
|
419
431
|
}
|
|
420
432
|
|
|
@@ -422,19 +434,23 @@ const menuSubButtonBase =
|
|
|
422
434
|
"flex items-center gap-[var(--space-2)] h-7 px-[var(--space-2)] rounded-[calc(var(--radius)-2px)] text-[0.8125rem] text-[var(--sidebar-fg)] no-underline transition-[background-color,color] duration-[var(--duration-fast)] min-w-0 [&>span]:flex-1 [&>span]:min-w-0 [&>span]:overflow-hidden [&>span]:[text-overflow:ellipsis] [&>span]:whitespace-nowrap hover:bg-[var(--sidebar-accent)] hover:text-[var(--sidebar-accent-fg)] data-[active]:bg-primary data-[active]:text-primary-foreground data-[active]:font-semibold data-[active]:hover:bg-primary-hover motion-reduce:transition-none";
|
|
423
435
|
|
|
424
436
|
export const SidebarMenuSubButton = React.forwardRef<HTMLAnchorElement, SidebarMenuSubButtonProps>(
|
|
425
|
-
function SidebarMenuSubButton({ className, isActive, size = "md",
|
|
437
|
+
function SidebarMenuSubButton({ className, isActive, size = "md", render, sectionId, children, ...props }, ref) {
|
|
426
438
|
const tocActive = useTOCActiveId();
|
|
427
439
|
const resolvedIsActive = isActive ?? (sectionId != null ? tocActive === sectionId : undefined);
|
|
428
440
|
const cls = cn(menuSubButtonBase, size === "sm" && "text-[length:var(--text-xs)]", className);
|
|
429
441
|
|
|
430
|
-
if (
|
|
431
|
-
const child =
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
442
|
+
if (render && React.isValidElement(render)) {
|
|
443
|
+
const child = render as React.ReactElement<{ className?: string; children?: React.ReactNode }>;
|
|
444
|
+
return React.cloneElement(
|
|
445
|
+
child,
|
|
446
|
+
{
|
|
447
|
+
ref,
|
|
448
|
+
...props,
|
|
449
|
+
className: cn(child.props.className, cls),
|
|
450
|
+
"data-active": resolvedIsActive || undefined,
|
|
451
|
+
} as Record<string, unknown>,
|
|
452
|
+
children ?? child.props.children,
|
|
453
|
+
);
|
|
438
454
|
}
|
|
439
455
|
|
|
440
456
|
return <a ref={ref} className={cls} data-active={resolvedIsActive || undefined} {...props}>{children}</a>;
|
|
@@ -591,12 +591,13 @@ export interface SidebarMenuButtonProps extends React.ButtonHTMLAttributes<HTMLB
|
|
|
591
591
|
*/
|
|
592
592
|
size?: "sm" | "md" | "lg";
|
|
593
593
|
/**
|
|
594
|
-
*
|
|
595
|
-
*
|
|
594
|
+
* 다른 엘리먼트(예: Next.js `Link`)로 대체. 자체로 `<button>` 을 렌더하므로
|
|
595
|
+
* 자식으로 또 다른 button/anchor 를 넣지 말 것. sh-ui 의 모든 슬롯 패턴은
|
|
596
|
+
* `render` 로 통일 (Base UI 표준).
|
|
596
597
|
*
|
|
597
|
-
*
|
|
598
|
+
* <SidebarMenuButton render={<Link href='/'>홈</Link>} />
|
|
598
599
|
*/
|
|
599
|
-
|
|
600
|
+
render?: React.ReactElement;
|
|
600
601
|
/**
|
|
601
602
|
* `SidebarTOC` 안에서 활성 섹션 id를 자동 동기화. 이 값과 TOC active id가 일치하면
|
|
602
603
|
* `isActive`가 자동으로 `true`가 된다.
|
|
@@ -610,12 +611,14 @@ export interface SidebarMenuButtonProps extends React.ButtonHTMLAttributes<HTMLB
|
|
|
610
611
|
}
|
|
611
612
|
|
|
612
613
|
/**
|
|
613
|
-
* 메뉴 한 줄을 누를 수 있는 버튼. `
|
|
614
|
-
*
|
|
614
|
+
* 메뉴 한 줄을 누를 수 있는 버튼. `render` prop 으로 `<a>` 등 다른 엘리먼트로
|
|
615
|
+
* 슬롯 가능 (Next.js Link 결합). `sectionId`/`panelId` 로 활성 상태 자동 결정.
|
|
616
|
+
*
|
|
617
|
+
* <SidebarMenuButton render={<Link href='/projects'>All projects</Link>} />
|
|
615
618
|
*/
|
|
616
619
|
export const SidebarMenuButton = React.forwardRef<HTMLButtonElement, SidebarMenuButtonProps>(
|
|
617
620
|
function SidebarMenuButton(
|
|
618
|
-
{ className, isActive, size = "md",
|
|
621
|
+
{ className, isActive, size = "md", render, sectionId, panelId, onClick, children, ...props },
|
|
619
622
|
ref
|
|
620
623
|
) {
|
|
621
624
|
const tocActive = useTOCActiveId();
|
|
@@ -640,15 +643,22 @@ export const SidebarMenuButton = React.forwardRef<HTMLButtonElement, SidebarMenu
|
|
|
640
643
|
`sh-ui-sidebar__menu-button--${size}`,
|
|
641
644
|
className);
|
|
642
645
|
|
|
643
|
-
if (
|
|
644
|
-
const child =
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
646
|
+
if (render && React.isValidElement(render)) {
|
|
647
|
+
const child = render as React.ReactElement<{
|
|
648
|
+
className?: string;
|
|
649
|
+
children?: React.ReactNode;
|
|
650
|
+
}>;
|
|
651
|
+
return React.cloneElement(
|
|
652
|
+
child,
|
|
653
|
+
{
|
|
654
|
+
ref,
|
|
655
|
+
...props,
|
|
656
|
+
onClick: handleClick,
|
|
657
|
+
className: cn(child.props.className, cls),
|
|
658
|
+
"data-active": resolvedIsActive || undefined,
|
|
659
|
+
} as Record<string, unknown>,
|
|
660
|
+
children ?? child.props.children,
|
|
661
|
+
);
|
|
652
662
|
}
|
|
653
663
|
|
|
654
664
|
return (
|
|
@@ -697,18 +707,20 @@ export interface SidebarMenuSubButtonProps extends React.AnchorHTMLAttributes<HT
|
|
|
697
707
|
*/
|
|
698
708
|
size?: "sm" | "md";
|
|
699
709
|
/**
|
|
700
|
-
*
|
|
701
|
-
*
|
|
710
|
+
* 다른 anchor 컴포넌트(예: Next.js `Link`)로 대체. sh-ui 의 모든 슬롯
|
|
711
|
+
* 패턴은 `render` 로 통일 (Base UI 표준).
|
|
712
|
+
*
|
|
713
|
+
* <SidebarMenuSubButton render={<Link href='/...'>서브</Link>} />
|
|
702
714
|
*/
|
|
703
|
-
|
|
715
|
+
render?: React.ReactElement;
|
|
704
716
|
/** `SidebarTOC`의 활성 섹션 id 자동 동기화. 일치하면 `isActive`가 자동으로 `true`. */
|
|
705
717
|
sectionId?: string;
|
|
706
718
|
}
|
|
707
719
|
|
|
708
|
-
/** 서브 메뉴 항목 내부의
|
|
720
|
+
/** 서브 메뉴 항목 내부의 링크. `render` prop 으로 다른 엘리먼트 슬롯 가능. `sectionId`로 TOC 활성 연동. */
|
|
709
721
|
export const SidebarMenuSubButton = React.forwardRef<HTMLAnchorElement, SidebarMenuSubButtonProps>(
|
|
710
722
|
function SidebarMenuSubButton(
|
|
711
|
-
{ className, isActive, size = "md",
|
|
723
|
+
{ className, isActive, size = "md", render, sectionId, children, ...props },
|
|
712
724
|
ref
|
|
713
725
|
) {
|
|
714
726
|
const tocActive = useTOCActiveId();
|
|
@@ -718,14 +730,21 @@ export const SidebarMenuSubButton = React.forwardRef<HTMLAnchorElement, SidebarM
|
|
|
718
730
|
`sh-ui-sidebar__menu-sub-button--${size}`,
|
|
719
731
|
className);
|
|
720
732
|
|
|
721
|
-
if (
|
|
722
|
-
const child =
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
733
|
+
if (render && React.isValidElement(render)) {
|
|
734
|
+
const child = render as React.ReactElement<{
|
|
735
|
+
className?: string;
|
|
736
|
+
children?: React.ReactNode;
|
|
737
|
+
}>;
|
|
738
|
+
return React.cloneElement(
|
|
739
|
+
child,
|
|
740
|
+
{
|
|
741
|
+
ref,
|
|
742
|
+
...props,
|
|
743
|
+
className: cn(child.props.className, cls),
|
|
744
|
+
"data-active": resolvedIsActive || undefined,
|
|
745
|
+
} as Record<string, unknown>,
|
|
746
|
+
children ?? child.props.children,
|
|
747
|
+
);
|
|
729
748
|
}
|
|
730
749
|
|
|
731
750
|
return (
|
|
@@ -935,9 +954,7 @@ export function SidebarCollapsibleContent({ children }: { children: React.ReactN
|
|
|
935
954
|
* <SidebarTOC sectionIds={["intro", "install", "usage"]}>
|
|
936
955
|
* <SidebarMenu>
|
|
937
956
|
* <SidebarMenuItem>
|
|
938
|
-
* <SidebarMenuButton sectionId="intro"
|
|
939
|
-
* <a href="#intro">Intro</a>
|
|
940
|
-
* </SidebarMenuButton>
|
|
957
|
+
* <SidebarMenuButton sectionId="intro" render={<a href="#intro">Intro</a>} />
|
|
941
958
|
* </SidebarMenuItem>
|
|
942
959
|
* ...
|
|
943
960
|
* </SidebarMenu>
|
|
@@ -27,9 +27,9 @@
|
|
|
27
27
|
"tabs": "탭 — separate exports: Tabs / TabsList / TabsTrigger / TabsContent / TabsIndicator (Base UI). dot syntax(`Tabs.List`) 아님.",
|
|
28
28
|
"accordion": "펼침/접힘 아코디언 — separate exports: Accordion / AccordionItem / AccordionTrigger / AccordionContent (Base UI). single/multiple 모드. AccordionTrigger 는 자체로 `<button>` — 다른 엘리먼트로 슬롯하려면 `render` prop 사용.",
|
|
29
29
|
"carousel": "슬라이드 캐러셀 — Embla 기반, autoplay/autoscroll.",
|
|
30
|
-
"sidebar": "앱 사이드바 —
|
|
30
|
+
"sidebar": "앱 사이드바 — SidebarProvider / Sidebar / SidebarInset / SidebarTrigger / SidebarHeader/Content/Footer / SidebarGroup·GroupLabel·GroupContent / SidebarMenu·MenuItem·MenuButton / SidebarMenuSub·SubItem·SubButton / SidebarCollapsible 등. SidebarMenuButton·SidebarMenuSubButton 은 자체 button/anchor — Next Link 등 다른 엘리먼트로 슬롯하려면 `render` prop 사용 (`<SidebarMenuButton render={<Link href='/'>홈</Link>} />`). asChild 는 v0.75 에서 제거됨.",
|
|
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": "경로 내비게이션 — separate exports: Breadcrumb / BreadcrumbList / BreadcrumbItem / BreadcrumbLink / BreadcrumbPage / BreadcrumbSeparator / BreadcrumbEllipsis.
|
|
32
|
+
"breadcrumb": "경로 내비게이션 — separate exports: Breadcrumb / BreadcrumbList / BreadcrumbItem / BreadcrumbLink / BreadcrumbPage / BreadcrumbSeparator / BreadcrumbEllipsis. BreadcrumbLink 는 자체 `<a>` — Next Link 등 다른 엘리먼트로 슬롯하려면 `render` prop 사용 (`<BreadcrumbLink render={<Link href='/...'>Projects</Link>} />`). aria-current 자동.",
|
|
33
33
|
"pagination": "페이지 단위 내비게이션 — separate exports: Pagination / PaginationContent / PaginationItem / PaginationLink / PaginationPrevious / PaginationNext / PaginationEllipsis. getPaginationRange 유틸 동봉. aria-current 자동.",
|
|
34
34
|
"avatar": "프로필 아바타 — 이미지 fallback → initials (Base UI).",
|
|
35
35
|
"badge": "상태 뱃지 — variant, size.",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sh-ui-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.75.0",
|
|
4
4
|
"description": "sh-ui CLI — 프로젝트 스캐폴드(create) + 컴포넌트 추가(add/list/remove) + IDE-내 AI용 MCP 서버",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
},
|
|
49
49
|
"scripts": {
|
|
50
50
|
"bundle-data": "node scripts/copy-data.mjs",
|
|
51
|
+
"pretest": "node scripts/copy-data.mjs",
|
|
51
52
|
"test": "vitest run",
|
|
52
53
|
"prepublishOnly": "node scripts/copy-data.mjs && node --check bin/sh-ui.mjs"
|
|
53
54
|
},
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
"paths": {
|
|
6
6
|
"@/lib/*": ["./lib/*"],
|
|
7
7
|
"@/components/*": ["./components/*"],
|
|
8
|
-
"@workspace/ui-app-name/*": ["../../packages/ui/ui-apps/ui-app-name/src/*"]
|
|
8
|
+
"@workspace/ui-app-name/*": ["../../packages/ui/ui-apps/ui-app-name/src/*"],
|
|
9
|
+
"@workspace/ui-core/*": ["../../packages/ui/ui-core/src/*"]
|
|
9
10
|
},
|
|
10
11
|
"plugins": [
|
|
11
12
|
{
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
"baseUrl": ".",
|
|
5
5
|
"paths": {
|
|
6
6
|
"@/*": ["./*"],
|
|
7
|
-
"@workspace/ui-app-name/*": ["../../packages/ui/ui-apps/ui-app-name/src/*"]
|
|
7
|
+
"@workspace/ui-app-name/*": ["../../packages/ui/ui-apps/ui-app-name/src/*"],
|
|
8
|
+
"@workspace/ui-core/*": ["../../packages/ui/ui-core/src/*"]
|
|
8
9
|
},
|
|
9
10
|
"plugins": [
|
|
10
11
|
{
|