sh-ui-cli 0.117.0 → 0.119.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,30 @@
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.119.0",
7
+ "date": "2026-06-19",
8
+ "title": "Command 컴포넌트 — Cmd+K 명령 팔레트",
9
+ "type": "minor",
10
+ "highlights": [
11
+ "신규 Command — cmdk 위 sh-ui 명령 팔레트(9 컴포넌트). CommandDialog 로 Cmd+K 팝업(sh-ui Dialog 기반)",
12
+ "검색 필터·키보드 네비·접근성은 cmdk 에 위임, sh-ui 토큰 스타일. CommandItem value/keywords/onSelect 로 액션 실행",
13
+ "shadcn command 모델. docs dogfooding·액션 자동화는 후속 phase. Flutter 별도"
14
+ ],
15
+ "url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.119.0"
16
+ },
17
+ {
18
+ "version": "0.118.0",
19
+ "date": "2026-06-18",
20
+ "title": "Table 컴포넌트 — TanStack 데이터 테이블 (정렬·선택·페이지)",
21
+ "type": "minor",
22
+ "highlights": [
23
+ "신규 Table — presentational 프리미티브(Table/TableHeader/TableBody/TableRow/TableHead/TableCell/TableFooter/TableCaption). 네이티브 <table> + sh-ui 토큰, 의존성 0",
24
+ "TanStack Table v8 통합 예제 — 정렬·행 선택·페이지네이션이 동작하는 데이터 테이블(docs). 컬럼/셀 렌더는 100% 자유",
25
+ "shadcn 모델(headless 엔진 + presentational 레이어). 풀 기능(필터·고정·그룹화·DnD)은 후속 phase"
26
+ ],
27
+ "url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.118.0"
28
+ },
5
29
  {
6
30
  "version": "0.117.0",
7
31
  "date": "2026-06-18",
@@ -0,0 +1,56 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { render, screen, fireEvent } from "@testing-library/react";
3
+ import * as React from "react";
4
+ import {
5
+ Command, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem, CommandSeparator, CommandShortcut,
6
+ } from "./index";
7
+
8
+ function Sample({ onSelect }: { onSelect?: (v: string) => void }) {
9
+ return (
10
+ <Command>
11
+ <CommandInput placeholder="검색" />
12
+ <CommandList>
13
+ <CommandEmpty>결과 없음</CommandEmpty>
14
+ <CommandGroup heading="페이지">
15
+ <CommandItem value="components" keywords={["컴포넌트"]} onSelect={() => onSelect?.("components")}>
16
+ 컴포넌트 <CommandShortcut>⌘C</CommandShortcut>
17
+ </CommandItem>
18
+ <CommandItem value="tokens" keywords={["토큰"]} onSelect={() => onSelect?.("tokens")}>토큰</CommandItem>
19
+ </CommandGroup>
20
+ <CommandSeparator />
21
+ <CommandGroup heading="액션">
22
+ <CommandItem value="theme" keywords={["테마 전환"]} onSelect={() => onSelect?.("theme")}>테마 전환</CommandItem>
23
+ </CommandGroup>
24
+ </CommandList>
25
+ </Command>
26
+ );
27
+ }
28
+
29
+ describe("Command", () => {
30
+ it("그룹/아이템을 렌더", () => {
31
+ render(<Sample />);
32
+ expect(screen.getByText("컴포넌트")).toBeTruthy();
33
+ expect(screen.getByText("페이지")).toBeTruthy();
34
+ expect(screen.getByRole("option", { name: /토큰/ })).toBeTruthy();
35
+ });
36
+
37
+ it("입력으로 아이템을 필터", () => {
38
+ render(<Sample />);
39
+ fireEvent.change(screen.getByPlaceholderText("검색"), { target: { value: "테마" } });
40
+ expect(screen.getByText("테마 전환")).toBeTruthy();
41
+ expect(screen.queryByText("토큰")).toBeNull();
42
+ });
43
+
44
+ it("매칭 없으면 CommandEmpty 표시", () => {
45
+ render(<Sample />);
46
+ fireEvent.change(screen.getByPlaceholderText("검색"), { target: { value: "zzzzz" } });
47
+ expect(screen.getByText("결과 없음")).toBeTruthy();
48
+ });
49
+
50
+ it("아이템 클릭이 onSelect 호출", () => {
51
+ const onSelect = vi.fn();
52
+ render(<Sample onSelect={onSelect} />);
53
+ fireEvent.click(screen.getByText("토큰"));
54
+ expect(onSelect).toHaveBeenCalledWith("tokens");
55
+ });
56
+ });
@@ -0,0 +1,86 @@
1
+ import * as React from "react";
2
+ import { Command as CommandPrimitive } from "cmdk";
3
+ import styles from "./styles.module.css";
4
+ import { cn } from "@SH_UI_UTILS@";
5
+ import { Dialog, DialogContent, DialogTitle } from "../dialog";
6
+
7
+ export const Command = React.forwardRef<
8
+ React.ElementRef<typeof CommandPrimitive>,
9
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive>
10
+ >(({ className, ...props }, ref) => (
11
+ <CommandPrimitive ref={ref} className={cn(styles.command, className)} {...props} />
12
+ ));
13
+ Command.displayName = "Command";
14
+
15
+ export interface CommandDialogProps
16
+ extends React.ComponentPropsWithoutRef<typeof CommandPrimitive> {
17
+ open?: boolean;
18
+ onOpenChange?: (open: boolean) => void;
19
+ title?: string;
20
+ }
21
+
22
+ export function CommandDialog({ open, onOpenChange, title = "명령 팔레트", children, ...props }: CommandDialogProps) {
23
+ return (
24
+ <Dialog open={open} onOpenChange={onOpenChange}>
25
+ <DialogContent className={styles["command__dialog"]}>
26
+ <DialogTitle className={styles["command__sr-only"]}>{title}</DialogTitle>
27
+ <Command {...props}>{children}</Command>
28
+ </DialogContent>
29
+ </Dialog>
30
+ );
31
+ }
32
+
33
+ export const CommandInput = React.forwardRef<
34
+ React.ElementRef<typeof CommandPrimitive.Input>,
35
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
36
+ >(({ className, ...props }, ref) => (
37
+ <div className={styles["command__input-wrapper"]}>
38
+ <CommandPrimitive.Input ref={ref} className={cn(styles.command__input, className)} {...props} />
39
+ </div>
40
+ ));
41
+ CommandInput.displayName = "CommandInput";
42
+
43
+ export const CommandList = React.forwardRef<
44
+ React.ElementRef<typeof CommandPrimitive.List>,
45
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
46
+ >(({ className, ...props }, ref) => (
47
+ <CommandPrimitive.List ref={ref} className={cn(styles.command__list, className)} {...props} />
48
+ ));
49
+ CommandList.displayName = "CommandList";
50
+
51
+ export const CommandEmpty = React.forwardRef<
52
+ React.ElementRef<typeof CommandPrimitive.Empty>,
53
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
54
+ >(({ className, ...props }, ref) => (
55
+ <CommandPrimitive.Empty ref={ref} className={cn(styles.command__empty, className)} {...props} />
56
+ ));
57
+ CommandEmpty.displayName = "CommandEmpty";
58
+
59
+ export const CommandGroup = React.forwardRef<
60
+ React.ElementRef<typeof CommandPrimitive.Group>,
61
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
62
+ >(({ className, ...props }, ref) => (
63
+ <CommandPrimitive.Group ref={ref} className={cn(styles.command__group, className)} {...props} />
64
+ ));
65
+ CommandGroup.displayName = "CommandGroup";
66
+
67
+ export const CommandSeparator = React.forwardRef<
68
+ React.ElementRef<typeof CommandPrimitive.Separator>,
69
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
70
+ >(({ className, ...props }, ref) => (
71
+ <CommandPrimitive.Separator ref={ref} className={cn(styles.command__separator, className)} {...props} />
72
+ ));
73
+ CommandSeparator.displayName = "CommandSeparator";
74
+
75
+ export const CommandItem = React.forwardRef<
76
+ React.ElementRef<typeof CommandPrimitive.Item>,
77
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
78
+ >(({ className, ...props }, ref) => (
79
+ <CommandPrimitive.Item ref={ref} className={cn(styles.command__item, className)} {...props} />
80
+ ));
81
+ CommandItem.displayName = "CommandItem";
82
+
83
+ export function CommandShortcut({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) {
84
+ return <span className={cn(styles.command__shortcut, className)} {...props} />;
85
+ }
86
+ CommandShortcut.displayName = "CommandShortcut";
@@ -0,0 +1,130 @@
1
+ import * as React from "react";
2
+ import { Command as CommandPrimitive } from "cmdk";
3
+ import { cn } from "@SH_UI_UTILS@";
4
+ import { Dialog, DialogContent, DialogTitle } from "../dialog";
5
+
6
+ export const Command = React.forwardRef<
7
+ React.ElementRef<typeof CommandPrimitive>,
8
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive>
9
+ >(({ className, ...props }, ref) => (
10
+ <CommandPrimitive
11
+ ref={ref}
12
+ className={cn(
13
+ "flex flex-col w-full bg-[var(--popover,var(--background))] text-foreground rounded-[var(--radius)] overflow-hidden",
14
+ className
15
+ )}
16
+ {...props}
17
+ />
18
+ ));
19
+ Command.displayName = "Command";
20
+
21
+ export interface CommandDialogProps
22
+ extends React.ComponentPropsWithoutRef<typeof CommandPrimitive> {
23
+ open?: boolean;
24
+ onOpenChange?: (open: boolean) => void;
25
+ title?: string;
26
+ }
27
+
28
+ export function CommandDialog({ open, onOpenChange, title = "명령 팔레트", children, ...props }: CommandDialogProps) {
29
+ return (
30
+ <Dialog open={open} onOpenChange={onOpenChange}>
31
+ <DialogContent className="p-0 max-w-2xl">
32
+ <DialogTitle className="sr-only">{title}</DialogTitle>
33
+ <Command {...props}>{children}</Command>
34
+ </DialogContent>
35
+ </Dialog>
36
+ );
37
+ }
38
+
39
+ export const CommandInput = React.forwardRef<
40
+ React.ElementRef<typeof CommandPrimitive.Input>,
41
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
42
+ >(({ className, ...props }, ref) => (
43
+ <div className="border-b border-border px-[var(--space-3)]">
44
+ <CommandPrimitive.Input
45
+ ref={ref}
46
+ className={cn(
47
+ "w-full h-[var(--control-md)] bg-transparent border-none outline-none text-foreground text-[length:var(--text-sm)] placeholder:text-foreground-muted",
48
+ className
49
+ )}
50
+ {...props}
51
+ />
52
+ </div>
53
+ ));
54
+ CommandInput.displayName = "CommandInput";
55
+
56
+ export const CommandList = React.forwardRef<
57
+ React.ElementRef<typeof CommandPrimitive.List>,
58
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
59
+ >(({ className, ...props }, ref) => (
60
+ <CommandPrimitive.List
61
+ ref={ref}
62
+ className={cn("max-h-80 overflow-y-auto p-[var(--space-1)]", className)}
63
+ {...props}
64
+ />
65
+ ));
66
+ CommandList.displayName = "CommandList";
67
+
68
+ export const CommandEmpty = React.forwardRef<
69
+ React.ElementRef<typeof CommandPrimitive.Empty>,
70
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
71
+ >(({ className, ...props }, ref) => (
72
+ <CommandPrimitive.Empty
73
+ ref={ref}
74
+ className={cn("p-[var(--space-4)] text-center text-foreground-muted text-[length:var(--text-sm)]", className)}
75
+ {...props}
76
+ />
77
+ ));
78
+ CommandEmpty.displayName = "CommandEmpty";
79
+
80
+ export const CommandGroup = React.forwardRef<
81
+ React.ElementRef<typeof CommandPrimitive.Group>,
82
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
83
+ >(({ className, ...props }, ref) => (
84
+ <CommandPrimitive.Group
85
+ ref={ref}
86
+ className={cn(
87
+ "[&_[cmdk-group-heading]]:px-[var(--space-2)] [&_[cmdk-group-heading]]:py-[var(--space-1)] [&_[cmdk-group-heading]]:text-[length:var(--text-xs)] [&_[cmdk-group-heading]]:text-foreground-muted",
88
+ className
89
+ )}
90
+ {...props}
91
+ />
92
+ ));
93
+ CommandGroup.displayName = "CommandGroup";
94
+
95
+ export const CommandSeparator = React.forwardRef<
96
+ React.ElementRef<typeof CommandPrimitive.Separator>,
97
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
98
+ >(({ className, ...props }, ref) => (
99
+ <CommandPrimitive.Separator
100
+ ref={ref}
101
+ className={cn("h-px bg-border my-[var(--space-1)]", className)}
102
+ {...props}
103
+ />
104
+ ));
105
+ CommandSeparator.displayName = "CommandSeparator";
106
+
107
+ export const CommandItem = React.forwardRef<
108
+ React.ElementRef<typeof CommandPrimitive.Item>,
109
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
110
+ >(({ className, ...props }, ref) => (
111
+ <CommandPrimitive.Item
112
+ ref={ref}
113
+ className={cn(
114
+ "flex items-center gap-[var(--space-2)] px-[var(--space-2)] py-[var(--space-2)] rounded-[calc(var(--radius)-2px)] text-[length:var(--text-sm)] cursor-pointer select-none data-[selected=true]:bg-background-muted aria-selected:bg-background-muted data-[disabled=true]:text-foreground-muted",
115
+ className
116
+ )}
117
+ {...props}
118
+ />
119
+ ));
120
+ CommandItem.displayName = "CommandItem";
121
+
122
+ export function CommandShortcut({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) {
123
+ return (
124
+ <span
125
+ className={cn("ms-auto text-[length:var(--text-xs)] text-foreground-muted tracking-wider", className)}
126
+ {...props}
127
+ />
128
+ );
129
+ }
130
+ CommandShortcut.displayName = "CommandShortcut";
@@ -0,0 +1,86 @@
1
+ import * as React from "react";
2
+ import { Command as CommandPrimitive } from "cmdk";
3
+ import "./styles.css";
4
+ import { cn } from "@SH_UI_UTILS@";
5
+ import { Dialog, DialogContent, DialogTitle } from "../dialog";
6
+
7
+ export const Command = React.forwardRef<
8
+ React.ElementRef<typeof CommandPrimitive>,
9
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive>
10
+ >(({ className, ...props }, ref) => (
11
+ <CommandPrimitive ref={ref} className={cn("sh-ui-command", className)} {...props} />
12
+ ));
13
+ Command.displayName = "Command";
14
+
15
+ export interface CommandDialogProps
16
+ extends React.ComponentPropsWithoutRef<typeof CommandPrimitive> {
17
+ open?: boolean;
18
+ onOpenChange?: (open: boolean) => void;
19
+ title?: string;
20
+ }
21
+
22
+ export function CommandDialog({ open, onOpenChange, title = "명령 팔레트", children, ...props }: CommandDialogProps) {
23
+ return (
24
+ <Dialog open={open} onOpenChange={onOpenChange}>
25
+ <DialogContent className="sh-ui-command__dialog">
26
+ <DialogTitle className="sh-ui-command__sr-only">{title}</DialogTitle>
27
+ <Command {...props}>{children}</Command>
28
+ </DialogContent>
29
+ </Dialog>
30
+ );
31
+ }
32
+
33
+ export const CommandInput = React.forwardRef<
34
+ React.ElementRef<typeof CommandPrimitive.Input>,
35
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
36
+ >(({ className, ...props }, ref) => (
37
+ <div className="sh-ui-command__input-wrapper">
38
+ <CommandPrimitive.Input ref={ref} className={cn("sh-ui-command__input", className)} {...props} />
39
+ </div>
40
+ ));
41
+ CommandInput.displayName = "CommandInput";
42
+
43
+ export const CommandList = React.forwardRef<
44
+ React.ElementRef<typeof CommandPrimitive.List>,
45
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
46
+ >(({ className, ...props }, ref) => (
47
+ <CommandPrimitive.List ref={ref} className={cn("sh-ui-command__list", className)} {...props} />
48
+ ));
49
+ CommandList.displayName = "CommandList";
50
+
51
+ export const CommandEmpty = React.forwardRef<
52
+ React.ElementRef<typeof CommandPrimitive.Empty>,
53
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
54
+ >(({ className, ...props }, ref) => (
55
+ <CommandPrimitive.Empty ref={ref} className={cn("sh-ui-command__empty", className)} {...props} />
56
+ ));
57
+ CommandEmpty.displayName = "CommandEmpty";
58
+
59
+ export const CommandGroup = React.forwardRef<
60
+ React.ElementRef<typeof CommandPrimitive.Group>,
61
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
62
+ >(({ className, ...props }, ref) => (
63
+ <CommandPrimitive.Group ref={ref} className={cn("sh-ui-command__group", className)} {...props} />
64
+ ));
65
+ CommandGroup.displayName = "CommandGroup";
66
+
67
+ export const CommandSeparator = React.forwardRef<
68
+ React.ElementRef<typeof CommandPrimitive.Separator>,
69
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
70
+ >(({ className, ...props }, ref) => (
71
+ <CommandPrimitive.Separator ref={ref} className={cn("sh-ui-command__separator", className)} {...props} />
72
+ ));
73
+ CommandSeparator.displayName = "CommandSeparator";
74
+
75
+ export const CommandItem = React.forwardRef<
76
+ React.ElementRef<typeof CommandPrimitive.Item>,
77
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
78
+ >(({ className, ...props }, ref) => (
79
+ <CommandPrimitive.Item ref={ref} className={cn("sh-ui-command__item", className)} {...props} />
80
+ ));
81
+ CommandItem.displayName = "CommandItem";
82
+
83
+ export function CommandShortcut({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) {
84
+ return <span className={cn("sh-ui-command__shortcut", className)} {...props} />;
85
+ }
86
+ CommandShortcut.displayName = "CommandShortcut";
@@ -0,0 +1,33 @@
1
+ .sh-ui-command {
2
+ display: flex; flex-direction: column; width: 100%;
3
+ background: var(--popover, var(--background)); color: var(--foreground);
4
+ border-radius: var(--radius); overflow: hidden;
5
+ }
6
+ .sh-ui-command__dialog { padding: 0; max-width: 40rem; }
7
+ .sh-ui-command__sr-only {
8
+ position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
9
+ overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
10
+ }
11
+ .sh-ui-command__input-wrapper { border-bottom: 1px solid var(--border); padding: 0 var(--space-3); }
12
+ .sh-ui-command__input {
13
+ width: 100%; height: var(--control-md); background: transparent; border: none; outline: none;
14
+ color: var(--foreground); font-size: var(--text-sm);
15
+ }
16
+ .sh-ui-command__input::placeholder { color: var(--foreground-muted); }
17
+ .sh-ui-command__list { max-height: 20rem; overflow-y: auto; padding: var(--space-1); }
18
+ .sh-ui-command__empty { padding: var(--space-4); text-align: center; color: var(--foreground-muted); font-size: var(--text-sm); }
19
+ .sh-ui-command__group [cmdk-group-heading] {
20
+ padding: var(--space-2) var(--space-2) var(--space-1);
21
+ font-size: var(--text-xs); color: var(--foreground-muted);
22
+ }
23
+ .sh-ui-command__item {
24
+ display: flex; align-items: center; gap: var(--space-2);
25
+ padding: var(--space-2) var(--space-2); border-radius: calc(var(--radius) - 2px);
26
+ font-size: var(--text-sm); cursor: pointer; user-select: none;
27
+ }
28
+ .sh-ui-command__item[data-selected="true"],
29
+ .sh-ui-command__item[aria-selected="true"] { background: var(--background-muted); }
30
+ .sh-ui-command__item[data-disabled="true"],
31
+ .sh-ui-command__item[aria-disabled="true"] { color: var(--foreground-muted); cursor: not-allowed; }
32
+ .sh-ui-command__separator { height: 1px; background: var(--border); margin: var(--space-1) 0; }
33
+ .sh-ui-command__shortcut { margin-inline-start: auto; font-size: var(--text-xs); color: var(--foreground-muted); letter-spacing: 0.05em; }
@@ -0,0 +1,33 @@
1
+ .command {
2
+ display: flex; flex-direction: column; width: 100%;
3
+ background: var(--popover, var(--background)); color: var(--foreground);
4
+ border-radius: var(--radius); overflow: hidden;
5
+ }
6
+ .command__dialog { padding: 0; max-width: 40rem; }
7
+ .command__sr-only {
8
+ position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
9
+ overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
10
+ }
11
+ .command__input-wrapper { border-bottom: 1px solid var(--border); padding: 0 var(--space-3); }
12
+ .command__input {
13
+ width: 100%; height: var(--control-md); background: transparent; border: none; outline: none;
14
+ color: var(--foreground); font-size: var(--text-sm);
15
+ }
16
+ .command__input::placeholder { color: var(--foreground-muted); }
17
+ .command__list { max-height: 20rem; overflow-y: auto; padding: var(--space-1); }
18
+ .command__empty { padding: var(--space-4); text-align: center; color: var(--foreground-muted); font-size: var(--text-sm); }
19
+ .command__group [cmdk-group-heading] {
20
+ padding: var(--space-2) var(--space-2) var(--space-1);
21
+ font-size: var(--text-xs); color: var(--foreground-muted);
22
+ }
23
+ .command__item {
24
+ display: flex; align-items: center; gap: var(--space-2);
25
+ padding: var(--space-2) var(--space-2); border-radius: calc(var(--radius) - 2px);
26
+ font-size: var(--text-sm); cursor: pointer; user-select: none;
27
+ }
28
+ .command__item[data-selected="true"],
29
+ .command__item[aria-selected="true"] { background: var(--background-muted); }
30
+ .command__item[data-disabled="true"],
31
+ .command__item[aria-disabled="true"] { color: var(--foreground-muted); cursor: not-allowed; }
32
+ .command__separator { height: 1px; background: var(--border); margin: var(--space-1) 0; }
33
+ .command__shortcut { margin-inline-start: auto; font-size: var(--text-xs); color: var(--foreground-muted); letter-spacing: 0.05em; }
@@ -0,0 +1,69 @@
1
+ import * as React from "react";
2
+ import styles from "./styles.module.css";
3
+ import { cn } from "@SH_UI_UTILS@";
4
+
5
+ export const Table = React.forwardRef<
6
+ HTMLTableElement,
7
+ React.TableHTMLAttributes<HTMLTableElement>
8
+ >(({ className, ...props }, ref) => (
9
+ <div className={styles["table__wrapper"]}>
10
+ <table ref={ref} className={cn(styles.table, className)} {...props} />
11
+ </div>
12
+ ));
13
+ Table.displayName = "Table";
14
+
15
+ export const TableHeader = React.forwardRef<
16
+ HTMLTableSectionElement,
17
+ React.HTMLAttributes<HTMLTableSectionElement>
18
+ >(({ className, ...props }, ref) => (
19
+ <thead ref={ref} className={cn(styles.table__header, className)} {...props} />
20
+ ));
21
+ TableHeader.displayName = "TableHeader";
22
+
23
+ export const TableBody = React.forwardRef<
24
+ HTMLTableSectionElement,
25
+ React.HTMLAttributes<HTMLTableSectionElement>
26
+ >(({ className, ...props }, ref) => (
27
+ <tbody ref={ref} className={cn(styles.table__body, className)} {...props} />
28
+ ));
29
+ TableBody.displayName = "TableBody";
30
+
31
+ export const TableFooter = React.forwardRef<
32
+ HTMLTableSectionElement,
33
+ React.HTMLAttributes<HTMLTableSectionElement>
34
+ >(({ className, ...props }, ref) => (
35
+ <tfoot ref={ref} className={cn(styles.table__footer, className)} {...props} />
36
+ ));
37
+ TableFooter.displayName = "TableFooter";
38
+
39
+ export const TableRow = React.forwardRef<
40
+ HTMLTableRowElement,
41
+ React.HTMLAttributes<HTMLTableRowElement>
42
+ >(({ className, ...props }, ref) => (
43
+ <tr ref={ref} className={cn(styles.table__row, className)} {...props} />
44
+ ));
45
+ TableRow.displayName = "TableRow";
46
+
47
+ export const TableHead = React.forwardRef<
48
+ HTMLTableCellElement,
49
+ React.ThHTMLAttributes<HTMLTableCellElement>
50
+ >(({ className, scope = "col", ...props }, ref) => (
51
+ <th ref={ref} scope={scope} className={cn(styles.table__head, className)} {...props} />
52
+ ));
53
+ TableHead.displayName = "TableHead";
54
+
55
+ export const TableCell = React.forwardRef<
56
+ HTMLTableCellElement,
57
+ React.TdHTMLAttributes<HTMLTableCellElement>
58
+ >(({ className, ...props }, ref) => (
59
+ <td ref={ref} className={cn(styles.table__cell, className)} {...props} />
60
+ ));
61
+ TableCell.displayName = "TableCell";
62
+
63
+ export const TableCaption = React.forwardRef<
64
+ HTMLTableCaptionElement,
65
+ React.HTMLAttributes<HTMLTableCaptionElement>
66
+ >(({ className, ...props }, ref) => (
67
+ <caption ref={ref} className={cn(styles.table__caption, className)} {...props} />
68
+ ));
69
+ TableCaption.displayName = "TableCaption";
@@ -0,0 +1,68 @@
1
+ import * as React from "react";
2
+ import { cn } from "@SH_UI_UTILS@";
3
+
4
+ export const Table = React.forwardRef<
5
+ HTMLTableElement,
6
+ React.TableHTMLAttributes<HTMLTableElement>
7
+ >(({ className, ...props }, ref) => (
8
+ <div className="w-full overflow-x-auto">
9
+ <table ref={ref} className={cn("w-full border-collapse text-[length:var(--text-sm)] text-foreground", className)} {...props} />
10
+ </div>
11
+ ));
12
+ Table.displayName = "Table";
13
+
14
+ export const TableHeader = React.forwardRef<
15
+ HTMLTableSectionElement,
16
+ React.HTMLAttributes<HTMLTableSectionElement>
17
+ >(({ className, ...props }, ref) => (
18
+ <thead ref={ref} className={cn(className)} {...props} />
19
+ ));
20
+ TableHeader.displayName = "TableHeader";
21
+
22
+ export const TableBody = React.forwardRef<
23
+ HTMLTableSectionElement,
24
+ React.HTMLAttributes<HTMLTableSectionElement>
25
+ >(({ className, ...props }, ref) => (
26
+ <tbody ref={ref} className={cn(className)} {...props} />
27
+ ));
28
+ TableBody.displayName = "TableBody";
29
+
30
+ export const TableFooter = React.forwardRef<
31
+ HTMLTableSectionElement,
32
+ React.HTMLAttributes<HTMLTableSectionElement>
33
+ >(({ className, ...props }, ref) => (
34
+ <tfoot ref={ref} className={cn("border-t border-border font-medium", className)} {...props} />
35
+ ));
36
+ TableFooter.displayName = "TableFooter";
37
+
38
+ export const TableRow = React.forwardRef<
39
+ HTMLTableRowElement,
40
+ React.HTMLAttributes<HTMLTableRowElement>
41
+ >(({ className, ...props }, ref) => (
42
+ <tr ref={ref} className={cn("border-b border-border transition-[background-color] duration-[var(--duration-fast)] hover:bg-background-muted data-[state=selected]:bg-background-muted motion-reduce:transition-none", className)} {...props} />
43
+ ));
44
+ TableRow.displayName = "TableRow";
45
+
46
+ export const TableHead = React.forwardRef<
47
+ HTMLTableCellElement,
48
+ React.ThHTMLAttributes<HTMLTableCellElement>
49
+ >(({ className, scope = "col", ...props }, ref) => (
50
+ <th ref={ref} scope={scope} className={cn("h-[var(--control-md)] px-[var(--space-3)] text-start font-medium text-foreground-muted align-middle whitespace-nowrap", className)} {...props} />
51
+ ));
52
+ TableHead.displayName = "TableHead";
53
+
54
+ export const TableCell = React.forwardRef<
55
+ HTMLTableCellElement,
56
+ React.TdHTMLAttributes<HTMLTableCellElement>
57
+ >(({ className, ...props }, ref) => (
58
+ <td ref={ref} className={cn("p-[var(--space-3)] align-middle", className)} {...props} />
59
+ ));
60
+ TableCell.displayName = "TableCell";
61
+
62
+ export const TableCaption = React.forwardRef<
63
+ HTMLTableCaptionElement,
64
+ React.HTMLAttributes<HTMLTableCaptionElement>
65
+ >(({ className, ...props }, ref) => (
66
+ <caption ref={ref} className={cn("mt-[var(--space-3)] text-foreground-muted text-[length:var(--text-xs)] text-start", className)} {...props} />
67
+ ));
68
+ TableCaption.displayName = "TableCaption";
@@ -0,0 +1,69 @@
1
+ import * as React from "react";
2
+ import "./styles.css";
3
+ import { cn } from "@SH_UI_UTILS@";
4
+
5
+ export const Table = React.forwardRef<
6
+ HTMLTableElement,
7
+ React.TableHTMLAttributes<HTMLTableElement>
8
+ >(({ className, ...props }, ref) => (
9
+ <div className="sh-ui-table__wrapper">
10
+ <table ref={ref} className={cn("sh-ui-table", className)} {...props} />
11
+ </div>
12
+ ));
13
+ Table.displayName = "Table";
14
+
15
+ export const TableHeader = React.forwardRef<
16
+ HTMLTableSectionElement,
17
+ React.HTMLAttributes<HTMLTableSectionElement>
18
+ >(({ className, ...props }, ref) => (
19
+ <thead ref={ref} className={cn("sh-ui-table__header", className)} {...props} />
20
+ ));
21
+ TableHeader.displayName = "TableHeader";
22
+
23
+ export const TableBody = React.forwardRef<
24
+ HTMLTableSectionElement,
25
+ React.HTMLAttributes<HTMLTableSectionElement>
26
+ >(({ className, ...props }, ref) => (
27
+ <tbody ref={ref} className={cn("sh-ui-table__body", className)} {...props} />
28
+ ));
29
+ TableBody.displayName = "TableBody";
30
+
31
+ export const TableFooter = React.forwardRef<
32
+ HTMLTableSectionElement,
33
+ React.HTMLAttributes<HTMLTableSectionElement>
34
+ >(({ className, ...props }, ref) => (
35
+ <tfoot ref={ref} className={cn("sh-ui-table__footer", className)} {...props} />
36
+ ));
37
+ TableFooter.displayName = "TableFooter";
38
+
39
+ export const TableRow = React.forwardRef<
40
+ HTMLTableRowElement,
41
+ React.HTMLAttributes<HTMLTableRowElement>
42
+ >(({ className, ...props }, ref) => (
43
+ <tr ref={ref} className={cn("sh-ui-table__row", className)} {...props} />
44
+ ));
45
+ TableRow.displayName = "TableRow";
46
+
47
+ export const TableHead = React.forwardRef<
48
+ HTMLTableCellElement,
49
+ React.ThHTMLAttributes<HTMLTableCellElement>
50
+ >(({ className, scope = "col", ...props }, ref) => (
51
+ <th ref={ref} scope={scope} className={cn("sh-ui-table__head", className)} {...props} />
52
+ ));
53
+ TableHead.displayName = "TableHead";
54
+
55
+ export const TableCell = React.forwardRef<
56
+ HTMLTableCellElement,
57
+ React.TdHTMLAttributes<HTMLTableCellElement>
58
+ >(({ className, ...props }, ref) => (
59
+ <td ref={ref} className={cn("sh-ui-table__cell", className)} {...props} />
60
+ ));
61
+ TableCell.displayName = "TableCell";
62
+
63
+ export const TableCaption = React.forwardRef<
64
+ HTMLTableCaptionElement,
65
+ React.HTMLAttributes<HTMLTableCaptionElement>
66
+ >(({ className, ...props }, ref) => (
67
+ <caption ref={ref} className={cn("sh-ui-table__caption", className)} {...props} />
68
+ ));
69
+ TableCaption.displayName = "TableCaption";
@@ -0,0 +1,57 @@
1
+ .sh-ui-table__wrapper {
2
+ width: 100%;
3
+ overflow-x: auto;
4
+ }
5
+
6
+ .sh-ui-table {
7
+ width: 100%;
8
+ border-collapse: collapse;
9
+ font-size: var(--text-sm);
10
+ color: var(--foreground);
11
+ }
12
+
13
+ .sh-ui-table__head {
14
+ height: var(--control-md);
15
+ padding: 0 var(--space-3);
16
+ text-align: start;
17
+ font-weight: var(--weight-medium);
18
+ color: var(--foreground-muted);
19
+ vertical-align: middle;
20
+ white-space: nowrap;
21
+ }
22
+
23
+ .sh-ui-table__cell {
24
+ padding: var(--space-3);
25
+ vertical-align: middle;
26
+ }
27
+
28
+ .sh-ui-table__row {
29
+ border-bottom: 1px solid var(--border);
30
+ transition: background-color var(--duration-fast) var(--ease-standard);
31
+ }
32
+
33
+ .sh-ui-table__body .sh-ui-table__row:hover {
34
+ background: var(--background-muted);
35
+ }
36
+
37
+ .sh-ui-table__row[data-state="selected"] {
38
+ background: var(--background-muted);
39
+ }
40
+
41
+ .sh-ui-table__footer {
42
+ border-top: 1px solid var(--border);
43
+ font-weight: var(--weight-medium);
44
+ }
45
+
46
+ .sh-ui-table__caption {
47
+ margin-top: var(--space-3);
48
+ color: var(--foreground-muted);
49
+ font-size: var(--text-xs);
50
+ text-align: start;
51
+ }
52
+
53
+ @media (prefers-reduced-motion: reduce) {
54
+ .sh-ui-table__row {
55
+ transition: none;
56
+ }
57
+ }
@@ -0,0 +1,57 @@
1
+ .table__wrapper {
2
+ width: 100%;
3
+ overflow-x: auto;
4
+ }
5
+
6
+ .table {
7
+ width: 100%;
8
+ border-collapse: collapse;
9
+ font-size: var(--text-sm);
10
+ color: var(--foreground);
11
+ }
12
+
13
+ .table__head {
14
+ height: var(--control-md);
15
+ padding: 0 var(--space-3);
16
+ text-align: start;
17
+ font-weight: var(--weight-medium);
18
+ color: var(--foreground-muted);
19
+ vertical-align: middle;
20
+ white-space: nowrap;
21
+ }
22
+
23
+ .table__cell {
24
+ padding: var(--space-3);
25
+ vertical-align: middle;
26
+ }
27
+
28
+ .table__row {
29
+ border-bottom: 1px solid var(--border);
30
+ transition: background-color var(--duration-fast) var(--ease-standard);
31
+ }
32
+
33
+ .table__body .table__row:hover {
34
+ background: var(--background-muted);
35
+ }
36
+
37
+ .table__row[data-state="selected"] {
38
+ background: var(--background-muted);
39
+ }
40
+
41
+ .table__footer {
42
+ border-top: 1px solid var(--border);
43
+ font-weight: var(--weight-medium);
44
+ }
45
+
46
+ .table__caption {
47
+ margin-top: var(--space-3);
48
+ color: var(--foreground-muted);
49
+ font-size: var(--text-xs);
50
+ text-align: start;
51
+ }
52
+
53
+ @media (prefers-reduced-motion: reduce) {
54
+ .table__row {
55
+ transition: none;
56
+ }
57
+ }
@@ -0,0 +1,59 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { render, screen } from "@testing-library/react";
3
+ import * as React from "react";
4
+ import {
5
+ Table, TableHeader, TableBody, TableFooter, TableRow, TableHead, TableCell, TableCaption,
6
+ } from "./index";
7
+
8
+ function Sample(props: { selected?: boolean }) {
9
+ return (
10
+ <Table>
11
+ <TableCaption>사용자 목록</TableCaption>
12
+ <TableHeader>
13
+ <TableRow>
14
+ <TableHead>이름</TableHead>
15
+ </TableRow>
16
+ </TableHeader>
17
+ <TableBody>
18
+ <TableRow data-state={props.selected ? "selected" : undefined}>
19
+ <TableCell>Kim</TableCell>
20
+ </TableRow>
21
+ </TableBody>
22
+ <TableFooter>
23
+ <TableRow><TableCell>합계 1</TableCell></TableRow>
24
+ </TableFooter>
25
+ </Table>
26
+ );
27
+ }
28
+
29
+ describe("Table primitives", () => {
30
+ it("네이티브 table 구조를 렌더", () => {
31
+ const { container } = render(<Sample />);
32
+ expect(container.querySelector("table")).toBeTruthy();
33
+ expect(container.querySelector("thead")).toBeTruthy();
34
+ expect(container.querySelector("tbody")).toBeTruthy();
35
+ expect(container.querySelector("tfoot")).toBeTruthy();
36
+ expect(container.querySelector("caption")).toBeTruthy();
37
+ });
38
+
39
+ it("TableHead 는 th[scope=col]", () => {
40
+ const { container } = render(<Sample />);
41
+ const th = container.querySelector("th");
42
+ expect(th?.getAttribute("scope")).toBe("col");
43
+ });
44
+
45
+ it("columnheader role 로 헤더 셀 접근", () => {
46
+ render(<Sample />);
47
+ expect(screen.getByRole("columnheader", { name: "이름" })).toBeTruthy();
48
+ });
49
+
50
+ it("data-state=selected 가 행에 반영", () => {
51
+ const { container } = render(<Sample selected />);
52
+ expect(container.querySelector('tbody tr[data-state="selected"]')).toBeTruthy();
53
+ });
54
+
55
+ it("className 이 병합된다", () => {
56
+ const { container } = render(<Table className="custom"><TableBody><TableRow><TableCell>x</TableCell></TableRow></TableBody></Table>);
57
+ expect(container.querySelector("table.custom")).toBeTruthy();
58
+ });
59
+ });
@@ -874,6 +874,39 @@
874
874
  "dependencies": ["@base-ui/react"],
875
875
  "registryDependencies": ["utils"]
876
876
  },
877
+ "command": {
878
+ "name": "command",
879
+ "type": "component",
880
+ "files": [
881
+ {
882
+ "src": "components/command/index.tsx",
883
+ "dest": "{components}/command/index.tsx",
884
+ "frameworks": ["plain"]
885
+ },
886
+ {
887
+ "src": "components/command/styles.css",
888
+ "dest": "{components}/command/styles.css",
889
+ "frameworks": ["plain"]
890
+ },
891
+ {
892
+ "src": "components/command/index.tailwind.tsx",
893
+ "dest": "{components}/command/index.tsx",
894
+ "frameworks": ["tailwind"]
895
+ },
896
+ {
897
+ "src": "components/command/index.module.tsx",
898
+ "dest": "{components}/command/index.tsx",
899
+ "frameworks": ["css-modules"]
900
+ },
901
+ {
902
+ "src": "components/command/styles.module.css",
903
+ "dest": "{components}/command/styles.module.css",
904
+ "frameworks": ["css-modules"]
905
+ }
906
+ ],
907
+ "dependencies": ["cmdk"],
908
+ "registryDependencies": ["utils", "dialog"]
909
+ },
877
910
  "popover": {
878
911
  "name": "popover",
879
912
  "type": "component",
@@ -1704,6 +1737,39 @@
1704
1737
  "dependencies": [],
1705
1738
  "registryDependencies": ["utils"]
1706
1739
  },
1740
+ "table": {
1741
+ "name": "table",
1742
+ "type": "component",
1743
+ "files": [
1744
+ {
1745
+ "src": "components/table/index.tsx",
1746
+ "dest": "{components}/table/index.tsx",
1747
+ "frameworks": ["plain"]
1748
+ },
1749
+ {
1750
+ "src": "components/table/styles.css",
1751
+ "dest": "{components}/table/styles.css",
1752
+ "frameworks": ["plain"]
1753
+ },
1754
+ {
1755
+ "src": "components/table/index.tailwind.tsx",
1756
+ "dest": "{components}/table/index.tsx",
1757
+ "frameworks": ["tailwind"]
1758
+ },
1759
+ {
1760
+ "src": "components/table/index.module.tsx",
1761
+ "dest": "{components}/table/index.tsx",
1762
+ "frameworks": ["css-modules"]
1763
+ },
1764
+ {
1765
+ "src": "components/table/styles.module.css",
1766
+ "dest": "{components}/table/styles.module.css",
1767
+ "frameworks": ["css-modules"]
1768
+ }
1769
+ ],
1770
+ "dependencies": [],
1771
+ "registryDependencies": ["utils"]
1772
+ },
1707
1773
  "carousel": {
1708
1774
  "name": "carousel",
1709
1775
  "type": "component",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$description": "컴포넌트별 토큰 의존성 (var(--*) 추출). build-registry-tokens.mjs 가 자동 생성.",
3
- "$generated": "2026-06-18T06:02:23.309Z",
3
+ "$generated": "2026-06-19T05:19:57.653Z",
4
4
  "components": {
5
5
  "button": {
6
6
  "plain": [
@@ -1081,6 +1081,50 @@
1081
1081
  ],
1082
1082
  "vanilla-extract": []
1083
1083
  },
1084
+ "command": {
1085
+ "plain": [
1086
+ "--background",
1087
+ "--background-muted",
1088
+ "--border",
1089
+ "--control-md",
1090
+ "--foreground",
1091
+ "--foreground-muted",
1092
+ "--radius",
1093
+ "--space-1",
1094
+ "--space-2",
1095
+ "--space-3",
1096
+ "--space-4",
1097
+ "--text-sm",
1098
+ "--text-xs"
1099
+ ],
1100
+ "tailwind": [
1101
+ "--background",
1102
+ "--control-md",
1103
+ "--radius",
1104
+ "--space-1",
1105
+ "--space-2",
1106
+ "--space-3",
1107
+ "--space-4",
1108
+ "--text-sm",
1109
+ "--text-xs"
1110
+ ],
1111
+ "css-modules": [
1112
+ "--background",
1113
+ "--background-muted",
1114
+ "--border",
1115
+ "--control-md",
1116
+ "--foreground",
1117
+ "--foreground-muted",
1118
+ "--radius",
1119
+ "--space-1",
1120
+ "--space-2",
1121
+ "--space-3",
1122
+ "--space-4",
1123
+ "--text-sm",
1124
+ "--text-xs"
1125
+ ],
1126
+ "vanilla-extract": []
1127
+ },
1084
1128
  "popover": {
1085
1129
  "plain": [
1086
1130
  "--background",
@@ -1970,6 +2014,42 @@
1970
2014
  ],
1971
2015
  "vanilla-extract": []
1972
2016
  },
2017
+ "table": {
2018
+ "plain": [
2019
+ "--background-muted",
2020
+ "--border",
2021
+ "--control-md",
2022
+ "--duration-fast",
2023
+ "--ease-standard",
2024
+ "--foreground",
2025
+ "--foreground-muted",
2026
+ "--space-3",
2027
+ "--text-sm",
2028
+ "--text-xs",
2029
+ "--weight-medium"
2030
+ ],
2031
+ "tailwind": [
2032
+ "--control-md",
2033
+ "--duration-fast",
2034
+ "--space-3",
2035
+ "--text-sm",
2036
+ "--text-xs"
2037
+ ],
2038
+ "css-modules": [
2039
+ "--background-muted",
2040
+ "--border",
2041
+ "--control-md",
2042
+ "--duration-fast",
2043
+ "--ease-standard",
2044
+ "--foreground",
2045
+ "--foreground-muted",
2046
+ "--space-3",
2047
+ "--text-sm",
2048
+ "--text-xs",
2049
+ "--weight-medium"
2050
+ ],
2051
+ "vanilla-extract": []
2052
+ },
1973
2053
  "carousel": {
1974
2054
  "plain": [
1975
2055
  "--background",
@@ -37,6 +37,7 @@
37
37
  "progress": "ShUiProgress — 선형 진행률, indeterminate.",
38
38
  "spinner": "ShUiSpinner — 원형 로딩.",
39
39
  "separator": "ShUiSeparator — 구분선 (horizontal/vertical).",
40
- "skeleton": "ShUiSkeleton — 로딩 플레이스홀더."
40
+ "skeleton": "ShUiSkeleton — 로딩 플레이스홀더.",
41
+ "tree": "ShUiTree — 계층 데이터 트리. 확장/축소 + 단일 선택(제어·비제어). ShUiTreeNode(id/label/children/icon/disabled) 재귀. 키보드(화살표·Home/End·Enter/Space)·Semantics. ShUiTreeSize sm/md."
41
42
  }
42
43
  }
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "$description": "React 컴포넌트 summary — llms.txt 생성용. key는 registry.json의 name과 동일.",
3
3
  "summaries": {
4
+ "command": "Cmd+K 명령 팔레트 — cmdk 위 sh-ui. Command/CommandDialog/CommandInput/CommandList/CommandEmpty/CommandGroup/CommandItem/CommandSeparator/CommandShortcut. CommandDialog=sh-ui Dialog+Command(open/onOpenChange), 검색 필터·키보드 네비는 cmdk. CommandItem value/keywords/onSelect — 한국어 라벨엔 keywords 필요.",
4
5
  "button": "기본 버튼 — variant(primary/secondary/ghost/danger/link) + size(sm/md/lg).",
5
6
  "card": "카드 컨테이너 — separate exports: Card / CardHeader / CardTitle / CardDescription / CardAction / CardContent / CardFooter. dot syntax(`Card.Header`) 아님.",
6
7
  "input": "단일 행 텍스트 입력 — hasError 지원. 같은 모듈에 NumberInput / PhoneInput / BusinessNumberInput 변형 포함. 비밀번호 토글은 InputGroup + InputAdornment 레시피로 조립.",
@@ -58,6 +59,7 @@
58
59
  "calendar": "내부 캘린더 위젯 (DatePicker 가 사용). single / multiple / range 모드. CalendarMessages 로 a11y 텍스트 override. 일반적으로 직접 사용보다 DatePicker / DateRangePicker 권장.",
59
60
  "scroll-area": "커스텀 스크롤 컨테이너 — composite export ScrollArea (Base UI). 내부에서 viewport + scrollbar + thumb + corner 를 자동 구성하며 OS-native 스크롤바를 대체한다. orientation: \"vertical\" | \"horizontal\" | \"both\" (기본 vertical). 외부 height/width 가 정해진 컨테이너 안에서 사용. viewportClassName 으로 viewport 의 패딩/레이아웃 분리 적용. 스크롤바는 hover/scrolling 시 fade in, prefers-reduced-motion 존중.",
60
61
  "tree": "계층 데이터 트리 뷰 — Tree 단일 export + TreeNode 타입(`@/components/ui/tree/types`). nodes(TreeNode[]: id/label/children/icon/disabled) 를 평탄화해 role=tree 로 렌더. 확장: expandedIds/onExpandedChange(controlled) 또는 defaultExpandedIds(uncontrolled). 선택: selectedId/onSelect(controlled) 또는 defaultSelectedId(uncontrolled). 키보드 네비(화살표·Home/End·Enter·typeahead), renderLabel 커스텀 렌더러, size(sm/md). Base UI 비의존 자체 구현.",
62
+ "table": "데이터 테이블 프리미티브 — Table / TableHeader / TableBody / TableFooter / TableRow / TableHead / TableCell / TableCaption 8종 presentational (네이티브 <table> + sh-ui 토큰, 의존성 0). 정렬·행선택·페이지네이션은 TanStack Table v8 로 조립 — docs 의 data-table 예제 참고. shadcn 모델(headless 엔진 + presentational 레이어).",
61
63
  "sheet": "화면 가장자리에서 슬라이드 인 하는 side drawer — separate exports: Sheet / SheetTrigger / SheetClose / SheetContent / SheetTitle / SheetDescription / SheetHeader / SheetFooter / SheetCloseX (Base UI Drawer 래핑, 포커스 트랩). 글로벌 알림함 · 작업 큐 · 보조 패널 같은 사이드바 무관 모달 시트용. 사이드바 인근 detail 패널은 SidebarPanel, 중앙 강제 모달은 Dialog 권장. SheetContent 의 side: \"right\" | \"left\" | \"top\" | \"bottom\" (기본 right) 으로 진입 방향 지정. SheetTrigger·SheetClose 는 자체 button — 다른 엘리먼트 슬롯은 `render` prop. ESC/바깥 클릭/포커스 복귀 자동, prefers-reduced-motion 시 transform 트랜지션 제거."
62
64
  }
63
65
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sh-ui-cli",
3
- "version": "0.117.0",
3
+ "version": "0.119.0",
4
4
  "description": "sh-ui CLI — 프로젝트 스캐폴드(create) + 컴포넌트 추가(add/list/remove) + IDE-내 AI용 MCP 서버",
5
5
  "license": "MIT",
6
6
  "repository": {