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.
@@ -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
- /** 상위 단계로 이동하는 링크. 라우터 사용 시 `asChild` 패턴 대신 직접 `<a>` 속성으로 전달. */
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
- React.AnchorHTMLAttributes<HTMLAnchorElement>
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
- ref={ref}
70
- className={cn(styles.breadcrumb__link, className)}
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
- React.AnchorHTMLAttributes<HTMLAnchorElement>
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
- ref={ref}
55
- className={cn(
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
- /** 상위 단계로 이동하는 링크. 라우터 사용 시 `asChild` 패턴 대신 직접 `<a>` 속성으로 전달. */
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
- React.AnchorHTMLAttributes<HTMLAnchorElement>
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
- ref={ref}
70
- className={cn("sh-ui-breadcrumb__link", className)}
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
- * Radix asChild 패턴. children에 `<a>` 다른 요소를 넘겨 button 스타일만 입힐 때 사용.
595
- * Next.js Link와 결합 유용 (`<SidebarMenuButton asChild><Link href=...>`).
594
+ * 다른 엘리먼트(예: Next.js `Link`)로 대체. 자체로 `<button>` 렌더하므로
595
+ * 자식으로 다른 button/anchor 넣지 것. sh-ui 의 모든 슬롯 패턴은
596
+ * `render` 로 통일 (Base UI 표준).
596
597
  *
597
- * @default false
598
+ * <SidebarMenuButton render={<Link href='/'>홈</Link>} />
598
599
  */
599
- asChild?: boolean;
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
- * 메뉴 한 줄을 누를 수 있는 버튼. `asChild`로 `<a>` 등 다른 요소에 스타일만 입힐 수 있고,
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", asChild, sectionId, panelId, onClick, children, ...props },
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 (asChild && React.isValidElement(children)) {
644
- const child = children as React.ReactElement<Record<string, unknown>>;
645
- const merged: Record<string, unknown> = {
646
- ...props,
647
- onClick: handleClick,
648
- className: cn((child.props.className as string) || "", cls),
649
- "data-active": resolvedIsActive || undefined,
650
- };
651
- return React.cloneElement(child, merged);
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
- * Radix asChild 패턴. children에 다른 anchor 컴포넌트(예: Next.js Link) 넘길 사용.
701
- * @default false
706
+ * 다른 anchor 컴포넌트(예: Next.js `Link`) 대체. sh-ui 의 슬롯 패턴은 `render` 로 통일.
702
707
  */
703
- asChild?: boolean;
708
+ render?: React.ReactElement;
704
709
  /** `SidebarTOC`의 활성 섹션 id 자동 동기화. 일치하면 `isActive`가 자동으로 `true`. */
705
710
  sectionId?: string;
706
711
  }
707
712
 
708
- /** 서브 메뉴 항목 내부의 링크(`<a>`). `sectionId`로 SidebarTOC 활성 상태와 연동. */
713
+ /** 서브 메뉴 항목 내부의 링크. `render` prop 으로 다른 엘리먼트 슬롯 가능. `sectionId`로 TOC 활성 연동. */
709
714
  export const SidebarMenuSubButton = React.forwardRef<HTMLAnchorElement, SidebarMenuSubButtonProps>(
710
715
  function SidebarMenuSubButton(
711
- { className, isActive, size = "md", asChild, sectionId, children, ...props },
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 (asChild && React.isValidElement(children)) {
722
- const child = children as React.ReactElement<Record<string, unknown>>;
723
- const merged: Record<string, unknown> = {
724
- ...props,
725
- className: cn((child.props.className as string) || "", cls),
726
- "data-active": resolvedIsActive || undefined,
727
- };
728
- return React.cloneElement(child, merged);
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" asChild>
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
- asChild?: boolean;
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", asChild, sectionId, panelId, onClick, children, ...props }, ref) {
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 (asChild && React.isValidElement(children)) {
382
- const child = children as React.ReactElement<Record<string, unknown>>;
383
- const merged: Record<string, unknown> = {
384
- ...props,
385
- onClick: handleClick,
386
- className: cn((child.props.className as string) || "", cls),
387
- "data-active": resolvedIsActive || undefined,
388
- };
389
- return React.cloneElement(child, merged);
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
- asChild?: boolean;
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", asChild, sectionId, children, ...props }, ref) {
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 (asChild && React.isValidElement(children)) {
431
- const child = children as React.ReactElement<Record<string, unknown>>;
432
- const merged: Record<string, unknown> = {
433
- ...props,
434
- className: cn((child.props.className as string) || "", cls),
435
- "data-active": resolvedIsActive || undefined,
436
- };
437
- return React.cloneElement(child, merged);
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
- * Radix asChild 패턴. children에 `<a>` 다른 요소를 넘겨 button 스타일만 입힐 때 사용.
595
- * Next.js Link와 결합 유용 (`<SidebarMenuButton asChild><Link href=...>`).
594
+ * 다른 엘리먼트(예: Next.js `Link`)로 대체. 자체로 `<button>` 렌더하므로
595
+ * 자식으로 다른 button/anchor 넣지 것. sh-ui 의 모든 슬롯 패턴은
596
+ * `render` 로 통일 (Base UI 표준).
596
597
  *
597
- * @default false
598
+ * <SidebarMenuButton render={<Link href='/'>홈</Link>} />
598
599
  */
599
- asChild?: boolean;
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
- * 메뉴 한 줄을 누를 수 있는 버튼. `asChild`로 `<a>` 등 다른 요소에 스타일만 입힐 수 있고,
614
- * `sectionId`(SidebarTOC 활성 동기화) / `panelId`(SidebarPanel 토글)를 지정해 활성 상태를 자동 결정한다.
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", asChild, sectionId, panelId, onClick, children, ...props },
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 (asChild && React.isValidElement(children)) {
644
- const child = children as React.ReactElement<Record<string, unknown>>;
645
- const merged: Record<string, unknown> = {
646
- ...props,
647
- onClick: handleClick,
648
- className: cn((child.props.className as string) || "", cls),
649
- "data-active": resolvedIsActive || undefined,
650
- };
651
- return React.cloneElement(child, merged);
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
- * Radix asChild 패턴. children에 다른 anchor 컴포넌트(예: Next.js Link) 넘길 사용.
701
- * @default false
710
+ * 다른 anchor 컴포넌트(예: Next.js `Link`) 대체. sh-ui 의 모든 슬롯
711
+ * 패턴은 `render` 로 통일 (Base UI 표준).
712
+ *
713
+ * <SidebarMenuSubButton render={<Link href='/...'>서브</Link>} />
702
714
  */
703
- asChild?: boolean;
715
+ render?: React.ReactElement;
704
716
  /** `SidebarTOC`의 활성 섹션 id 자동 동기화. 일치하면 `isActive`가 자동으로 `true`. */
705
717
  sectionId?: string;
706
718
  }
707
719
 
708
- /** 서브 메뉴 항목 내부의 링크(`<a>`). `sectionId`로 SidebarTOC 활성 상태와 연동. */
720
+ /** 서브 메뉴 항목 내부의 링크. `render` prop 으로 다른 엘리먼트 슬롯 가능. `sectionId`로 TOC 활성 연동. */
709
721
  export const SidebarMenuSubButton = React.forwardRef<HTMLAnchorElement, SidebarMenuSubButtonProps>(
710
722
  function SidebarMenuSubButton(
711
- { className, isActive, size = "md", asChild, sectionId, children, ...props },
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 (asChild && React.isValidElement(children)) {
722
- const child = children as React.ReactElement<Record<string, unknown>>;
723
- const merged: Record<string, unknown> = {
724
- ...props,
725
- className: cn((child.props.className as string) || "", cls),
726
- "data-active": resolvedIsActive || undefined,
727
- };
728
- return React.cloneElement(child, merged);
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" asChild>
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": "앱 사이드바 — collapsible, SidebarMenu/SidebarGroup 조합.",
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. dot syntax 아님. aria-current 자동.",
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.74.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
  {
@@ -16,6 +16,7 @@
16
16
  "dependencies": {
17
17
  "@tanstack/react-query": "^5.90.21",
18
18
  "@workspace/ui-app-name": "workspace:*",
19
+ "@workspace/ui-core": "workspace:*",
19
20
  "lucide-react": "^0.563.0",
20
21
  "next": "16.1.6",
21
22
  "next-themes": "^0.4.6",
@@ -9,6 +9,10 @@ export default defineConfig({
9
9
  __dirname,
10
10
  '../../packages/ui/ui-apps/ui-app-name/src',
11
11
  ),
12
+ '@workspace/ui-core': path.resolve(
13
+ __dirname,
14
+ '../../packages/ui/ui-core/src',
15
+ ),
12
16
  },
13
17
  },
14
18
  test: {