sh-ui-cli 0.116.0 → 0.117.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.
@@ -0,0 +1,215 @@
1
+ import * as React from "react";
2
+ import "./styles.css";
3
+ import { cn } from "@SH_UI_UTILS@";
4
+ import type { TreeNode, TreeProps } from "./types";
5
+ export type { TreeNode, TreeProps, TreeSize } from "./types";
6
+ import { flattenVisible, nextFocusable, prevFocusable, findByTypeahead } from "./flatten";
7
+
8
+ function useControllableSet(
9
+ controlled: string[] | undefined,
10
+ defaultValue: string[] | undefined,
11
+ onChange?: (ids: string[]) => void,
12
+ ) {
13
+ const [internal, setInternal] = React.useState<Set<string>>(
14
+ () => new Set(controlled ?? defaultValue ?? []),
15
+ );
16
+ const set = controlled ? new Set(controlled) : internal;
17
+ const update = (next: Set<string>) => {
18
+ if (!controlled) setInternal(next);
19
+ onChange?.([...next]);
20
+ };
21
+ return [set, update] as const;
22
+ }
23
+
24
+ export const Tree = React.forwardRef<HTMLDivElement, TreeProps>(function Tree(
25
+ {
26
+ nodes,
27
+ expandedIds,
28
+ defaultExpandedIds,
29
+ onExpandedChange,
30
+ selectedId,
31
+ defaultSelectedId,
32
+ onSelect,
33
+ renderLabel,
34
+ size = "md",
35
+ className,
36
+ },
37
+ ref,
38
+ ) {
39
+ const [expanded, setExpanded] = useControllableSet(
40
+ expandedIds,
41
+ defaultExpandedIds,
42
+ onExpandedChange,
43
+ );
44
+
45
+ const isSelectedControlled = selectedId !== undefined;
46
+ const [selInternal, setSelInternal] = React.useState<string | null>(
47
+ defaultSelectedId ?? null,
48
+ );
49
+ const selected = isSelectedControlled ? selectedId! : selInternal;
50
+ const selectNode = (id: string | null) => {
51
+ if (!isSelectedControlled) setSelInternal(id);
52
+ onSelect?.(id);
53
+ };
54
+
55
+ const visible = flattenVisible(nodes, expanded);
56
+ const visibleMap = React.useMemo(() => new Map(visible.map((v) => [v.id, v])), [visible]);
57
+ const [focusId, setFocusId] = React.useState<string | null>(visible[0]?.id ?? null);
58
+
59
+ React.useEffect(() => {
60
+ if (focusId && !visibleMap.has(focusId)) {
61
+ setFocusId(visible[0]?.id ?? null);
62
+ }
63
+ }, [visibleMap, focusId, visible]);
64
+
65
+ const toggle = (id: string) => {
66
+ const next = new Set(expanded);
67
+ if (next.has(id)) next.delete(id);
68
+ else next.add(id);
69
+ setExpanded(next);
70
+ };
71
+
72
+ const onItemClick = (n: TreeNode, hasChildren: boolean) => {
73
+ if (n.disabled) return;
74
+ setFocusId(n.id);
75
+ if (hasChildren) toggle(n.id);
76
+ selectNode(n.id);
77
+ };
78
+
79
+ const itemRefs = React.useRef<Map<string, HTMLDivElement>>(new Map());
80
+ const focusNode = (id: string) => {
81
+ setFocusId(id);
82
+ itemRefs.current.get(id)?.focus();
83
+ };
84
+
85
+ const onItemKeyDown = (e: React.KeyboardEvent, n: TreeNode, hasChildren: boolean) => {
86
+ switch (e.key) {
87
+ case "ArrowDown": {
88
+ e.preventDefault();
89
+ const nx = nextFocusable(visible, n.id);
90
+ if (nx) focusNode(nx.id);
91
+ break;
92
+ }
93
+ case "ArrowUp": {
94
+ e.preventDefault();
95
+ const pv = prevFocusable(visible, n.id);
96
+ if (pv) focusNode(pv.id);
97
+ break;
98
+ }
99
+ case "ArrowRight": {
100
+ e.preventDefault();
101
+ if (hasChildren && !expanded.has(n.id)) toggle(n.id);
102
+ else if (hasChildren) {
103
+ const first = n.children!.find((c) => !c.disabled);
104
+ if (first) focusNode(first.id);
105
+ }
106
+ break;
107
+ }
108
+ case "ArrowLeft": {
109
+ e.preventDefault();
110
+ if (hasChildren && expanded.has(n.id)) toggle(n.id);
111
+ else {
112
+ const meta = visibleMap.get(n.id);
113
+ if (meta?.parentId) focusNode(meta.parentId);
114
+ }
115
+ break;
116
+ }
117
+ case "Home": {
118
+ e.preventDefault();
119
+ const first = visible.find((v) => !v.disabled);
120
+ if (first) focusNode(first.id);
121
+ break;
122
+ }
123
+ case "End": {
124
+ e.preventDefault();
125
+ for (let i = visible.length - 1; i >= 0; i--)
126
+ if (!visible[i].disabled) {
127
+ focusNode(visible[i].id);
128
+ break;
129
+ }
130
+ break;
131
+ }
132
+ case "Enter":
133
+ case " ": {
134
+ e.preventDefault();
135
+ if (!n.disabled) selectNode(n.id);
136
+ break;
137
+ }
138
+ default: {
139
+ if (e.key.length === 1 && /\S/.test(e.key)) {
140
+ const hit = findByTypeahead(visible, e.key, n.id);
141
+ if (hit) focusNode(hit.id);
142
+ }
143
+ }
144
+ }
145
+ };
146
+
147
+ const renderNodes = (siblings: TreeNode[], level: number): React.ReactNode => (
148
+ <div role={level === 0 ? undefined : "group"} className="sh-ui-tree__group">
149
+ {siblings.map((n) => {
150
+ const hasChildren = !!n.children?.length;
151
+ const isExpanded = hasChildren && expanded.has(n.id);
152
+ const meta = visibleMap.get(n.id);
153
+ const depth = meta?.level ?? level;
154
+ return (
155
+ <div key={n.id}>
156
+ <div
157
+ role="treeitem"
158
+ ref={(el) => {
159
+ if (el) itemRefs.current.set(n.id, el);
160
+ else itemRefs.current.delete(n.id);
161
+ }}
162
+ aria-level={depth + 1}
163
+ aria-setsize={meta?.setSize}
164
+ aria-posinset={meta?.posInSet}
165
+ aria-expanded={hasChildren ? isExpanded : undefined}
166
+ aria-selected={selected === n.id}
167
+ aria-disabled={n.disabled || undefined}
168
+ aria-label={typeof n.label === "string" ? n.label : undefined}
169
+ tabIndex={focusId === n.id ? 0 : -1}
170
+ data-disabled={n.disabled || undefined}
171
+ className={cn(
172
+ "sh-ui-tree__item",
173
+ selected === n.id && "sh-ui-tree__item--selected",
174
+ )}
175
+ onClick={() => onItemClick(n, hasChildren)}
176
+ onKeyDown={(e) => onItemKeyDown(e, n, hasChildren)}
177
+ >
178
+ <span
179
+ className="sh-ui-tree__indent"
180
+ style={{ width: `calc(var(--space-4) * ${depth})` }}
181
+ aria-hidden
182
+ />
183
+ {hasChildren ? (
184
+ <span
185
+ className="sh-ui-tree__chevron"
186
+ data-expanded={isExpanded || undefined}
187
+ aria-hidden
188
+ >
189
+
190
+ </span>
191
+ ) : (
192
+ <span className="sh-ui-tree__chevron sh-ui-tree__chevron--leaf" aria-hidden />
193
+ )}
194
+ {n.icon ? (
195
+ <span className="sh-ui-tree__icon" aria-hidden>
196
+ {n.icon}
197
+ </span>
198
+ ) : null}
199
+ <span className="sh-ui-tree__label">
200
+ {renderLabel ? renderLabel(n) : n.label}
201
+ </span>
202
+ </div>
203
+ {isExpanded ? renderNodes(n.children!, level + 1) : null}
204
+ </div>
205
+ );
206
+ })}
207
+ </div>
208
+ );
209
+
210
+ return (
211
+ <div ref={ref} role="tree" data-size={size} className={cn("sh-ui-tree", className)}>
212
+ {renderNodes(nodes, 0)}
213
+ </div>
214
+ );
215
+ });
@@ -0,0 +1,69 @@
1
+ .sh-ui-tree {
2
+ display: flex;
3
+ flex-direction: column;
4
+ width: 100%;
5
+ color: var(--foreground);
6
+ font-size: 0.9375rem;
7
+ }
8
+ .sh-ui-tree[data-size="sm"] {
9
+ font-size: var(--text-xs);
10
+ }
11
+
12
+ .sh-ui-tree__group {
13
+ display: flex;
14
+ flex-direction: column;
15
+ }
16
+
17
+ .sh-ui-tree__item {
18
+ display: flex;
19
+ align-items: center;
20
+ gap: var(--space-2);
21
+ padding: var(--space-2) var(--space-2);
22
+ border-radius: calc(var(--radius) - 2px);
23
+ cursor: pointer;
24
+ user-select: none;
25
+ transition: background-color var(--duration-fast) var(--ease-standard);
26
+ }
27
+ .sh-ui-tree__item:not([data-disabled]):hover {
28
+ background: var(--background-muted);
29
+ }
30
+ .sh-ui-tree__item:focus-visible {
31
+ outline: var(--border-width-strong) solid var(--ring);
32
+ outline-offset: -2px;
33
+ }
34
+ .sh-ui-tree__item--selected {
35
+ background: var(--background-muted);
36
+ font-weight: var(--weight-medium);
37
+ }
38
+ .sh-ui-tree__item[data-disabled] {
39
+ color: var(--foreground-muted);
40
+ cursor: not-allowed;
41
+ }
42
+
43
+ .sh-ui-tree__chevron {
44
+ display: inline-flex;
45
+ width: var(--space-4);
46
+ justify-content: center;
47
+ transition: transform var(--duration-fast) var(--ease-standard);
48
+ }
49
+ .sh-ui-tree__chevron[data-expanded] {
50
+ transform: rotate(90deg);
51
+ }
52
+ .sh-ui-tree__chevron--leaf {
53
+ visibility: hidden;
54
+ }
55
+
56
+ .sh-ui-tree__icon {
57
+ display: inline-flex;
58
+ }
59
+ .sh-ui-tree__label {
60
+ min-width: 0;
61
+ overflow-wrap: anywhere;
62
+ }
63
+
64
+ @media (prefers-reduced-motion: reduce) {
65
+ .sh-ui-tree__item,
66
+ .sh-ui-tree__chevron {
67
+ transition: none;
68
+ }
69
+ }
@@ -0,0 +1,69 @@
1
+ .tree {
2
+ display: flex;
3
+ flex-direction: column;
4
+ width: 100%;
5
+ color: var(--foreground);
6
+ font-size: 0.9375rem;
7
+ }
8
+ .tree[data-size="sm"] {
9
+ font-size: var(--text-xs);
10
+ }
11
+
12
+ .tree__group {
13
+ display: flex;
14
+ flex-direction: column;
15
+ }
16
+
17
+ .tree__item {
18
+ display: flex;
19
+ align-items: center;
20
+ gap: var(--space-2);
21
+ padding: var(--space-2) var(--space-2);
22
+ border-radius: calc(var(--radius) - 2px);
23
+ cursor: pointer;
24
+ user-select: none;
25
+ transition: background-color var(--duration-fast) var(--ease-standard);
26
+ }
27
+ .tree__item:not([data-disabled]):hover {
28
+ background: var(--background-muted);
29
+ }
30
+ .tree__item:focus-visible {
31
+ outline: var(--border-width-strong) solid var(--ring);
32
+ outline-offset: -2px;
33
+ }
34
+ .tree__item--selected {
35
+ background: var(--background-muted);
36
+ font-weight: var(--weight-medium);
37
+ }
38
+ .tree__item[data-disabled] {
39
+ color: var(--foreground-muted);
40
+ cursor: not-allowed;
41
+ }
42
+
43
+ .tree__chevron {
44
+ display: inline-flex;
45
+ width: var(--space-4);
46
+ justify-content: center;
47
+ transition: transform var(--duration-fast) var(--ease-standard);
48
+ }
49
+ .tree__chevron[data-expanded] {
50
+ transform: rotate(90deg);
51
+ }
52
+ .tree__chevron--leaf {
53
+ visibility: hidden;
54
+ }
55
+
56
+ .tree__icon {
57
+ display: inline-flex;
58
+ }
59
+ .tree__label {
60
+ min-width: 0;
61
+ overflow-wrap: anywhere;
62
+ }
63
+
64
+ @media (prefers-reduced-motion: reduce) {
65
+ .tree__item,
66
+ .tree__chevron {
67
+ transition: none;
68
+ }
69
+ }
@@ -0,0 +1,110 @@
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 { Tree } from "./index";
5
+ import type { TreeNode } from "./types";
6
+
7
+ const nodes: TreeNode[] = [
8
+ { id: "a", label: "Apple", children: [{ id: "a1", label: "Ant" }] },
9
+ { id: "b", label: "Banana" },
10
+ ];
11
+
12
+ describe("Tree 렌더 + 상태", () => {
13
+ it("role=tree 와 최상위 treeitem 을 렌더", () => {
14
+ render(<Tree nodes={nodes} />);
15
+ expect(screen.getByRole("tree")).toBeTruthy();
16
+ const items = screen.getAllByRole("treeitem");
17
+ expect(items.length).toBe(2);
18
+ });
19
+
20
+ it("부모 toggle 클릭으로 비제어 확장 → 자식 노출", () => {
21
+ render(<Tree nodes={nodes} />);
22
+ fireEvent.click(screen.getByText("Apple"));
23
+ expect(screen.getByText("Ant")).toBeTruthy();
24
+ expect(screen.getByRole("treeitem", { name: /Apple/ }).getAttribute("aria-expanded")).toBe("true");
25
+ });
26
+
27
+ it("제어 selectedId 가 aria-selected 로 반영", () => {
28
+ render(<Tree nodes={nodes} selectedId="b" />);
29
+ expect(screen.getByRole("treeitem", { name: /Banana/ }).getAttribute("aria-selected")).toBe("true");
30
+ });
31
+
32
+ it("노드 클릭이 onSelect 를 호출", () => {
33
+ const onSelect = vi.fn();
34
+ render(<Tree nodes={nodes} onSelect={onSelect} />);
35
+ fireEvent.click(screen.getByText("Banana"));
36
+ expect(onSelect).toHaveBeenCalledWith("b");
37
+ });
38
+
39
+ it("aria-level 이 깊이를 반영", () => {
40
+ render(<Tree nodes={nodes} defaultExpandedIds={["a"]} />);
41
+ expect(screen.getByRole("treeitem", { name: /Ant/ }).getAttribute("aria-level")).toBe("2");
42
+ });
43
+
44
+ it("aria-setsize / aria-posinset 가 형제 집합을 반영", () => {
45
+ render(<Tree nodes={nodes} />);
46
+ const apple = screen.getByRole("treeitem", { name: /Apple/ });
47
+ const banana = screen.getByRole("treeitem", { name: /Banana/ });
48
+ expect(apple.getAttribute("aria-setsize")).toBe("2");
49
+ expect(apple.getAttribute("aria-posinset")).toBe("1");
50
+ expect(banana.getAttribute("aria-posinset")).toBe("2");
51
+ });
52
+
53
+ it("제어 expandedIds 는 외부 값이 우선 (클릭 무관)", () => {
54
+ const { rerender } = render(<Tree nodes={nodes} expandedIds={["a"]} />);
55
+ expect(screen.getByText("Ant")).toBeTruthy();
56
+ rerender(<Tree nodes={nodes} expandedIds={[]} />);
57
+ expect(screen.queryByText("Ant")).toBeNull();
58
+ });
59
+
60
+ it("disabled 노드 클릭은 onSelect 를 호출하지 않는다", () => {
61
+ const onSelect = vi.fn();
62
+ const dnodes = [
63
+ { id: "x", label: "Xenon", disabled: true },
64
+ { id: "y", label: "Yttrium" },
65
+ ];
66
+ render(<Tree nodes={dnodes} onSelect={onSelect} />);
67
+ fireEvent.click(screen.getByText("Xenon"));
68
+ expect(onSelect).not.toHaveBeenCalled();
69
+ });
70
+ });
71
+
72
+ describe("Tree 키보드", () => {
73
+ function setup() {
74
+ render(<Tree nodes={nodes} defaultExpandedIds={["a"]} defaultSelectedId="a" />);
75
+ return screen.getByRole("tree");
76
+ }
77
+
78
+ it("ArrowDown 이 다음 가시 노드로 포커스 이동", () => {
79
+ setup();
80
+ const a = screen.getByRole("treeitem", { name: /Apple/ });
81
+ a.focus();
82
+ fireEvent.keyDown(a, { key: "ArrowDown" });
83
+ expect(screen.getByRole("treeitem", { name: /Ant/ }).getAttribute("tabindex")).toBe("0");
84
+ });
85
+
86
+ it("ArrowLeft 가 열린 부모를 축소", () => {
87
+ setup();
88
+ const a = screen.getByRole("treeitem", { name: /Apple/ });
89
+ a.focus();
90
+ fireEvent.keyDown(a, { key: "ArrowLeft" });
91
+ expect(a.getAttribute("aria-expanded")).toBe("false");
92
+ });
93
+
94
+ it("ArrowRight 가 닫힌 부모를 확장", () => {
95
+ render(<Tree nodes={nodes} />);
96
+ const a = screen.getByRole("treeitem", { name: /Apple/ });
97
+ a.focus();
98
+ fireEvent.keyDown(a, { key: "ArrowRight" });
99
+ expect(a.getAttribute("aria-expanded")).toBe("true");
100
+ });
101
+
102
+ it("Enter 가 포커스 노드를 선택", () => {
103
+ const onSelect = vi.fn();
104
+ render(<Tree nodes={nodes} onSelect={onSelect} />);
105
+ const b = screen.getByRole("treeitem", { name: /Banana/ });
106
+ b.focus();
107
+ fireEvent.keyDown(b, { key: "Enter" });
108
+ expect(onSelect).toHaveBeenCalledWith("b");
109
+ });
110
+ });
@@ -0,0 +1,36 @@
1
+ import type * as React from "react";
2
+
3
+ export interface TreeNode {
4
+ id: string;
5
+ label: React.ReactNode;
6
+ children?: TreeNode[];
7
+ icon?: React.ReactNode;
8
+ disabled?: boolean;
9
+ }
10
+
11
+ export type TreeSize = "sm" | "md";
12
+
13
+ export interface TreeProps {
14
+ nodes: TreeNode[];
15
+ expandedIds?: string[];
16
+ defaultExpandedIds?: string[];
17
+ onExpandedChange?: (ids: string[]) => void;
18
+ selectedId?: string | null;
19
+ defaultSelectedId?: string | null;
20
+ onSelect?: (id: string | null) => void;
21
+ renderLabel?: (node: TreeNode) => React.ReactNode;
22
+ size?: TreeSize;
23
+ className?: string;
24
+ }
25
+
26
+ export interface VisibleNode {
27
+ id: string;
28
+ node: TreeNode;
29
+ level: number;
30
+ parentId: string | null;
31
+ hasChildren: boolean;
32
+ expanded: boolean;
33
+ disabled: boolean;
34
+ setSize: number;
35
+ posInSet: number;
36
+ }
@@ -3,12 +3,20 @@
3
3
  "versions": {
4
4
  "@base-ui/react": "^1.4.1",
5
5
  "@tanstack/react-form": "^1.29.1",
6
+ "@tiptap/core": "^3.26.0",
7
+ "@tiptap/extension-link": "^3.26.0",
8
+ "@tiptap/extension-placeholder": "^3.26.0",
9
+ "@tiptap/extension-text-style": "^3.26.0",
10
+ "@tiptap/pm": "^3.26.0",
11
+ "@tiptap/react": "^3.26.0",
12
+ "@tiptap/starter-kit": "^3.26.0",
6
13
  "@vanilla-extract/css": "^1.16.0",
7
14
  "class-variance-authority": "^0.7.1",
8
15
  "clsx": "^2.1.1",
9
16
  "lucide-react": "^1.11.0",
10
17
  "react-hook-form": "^7.74.0",
11
18
  "shiki": "^4.0.2",
12
- "tailwind-merge": "^3.5.0"
19
+ "tailwind-merge": "^3.5.0",
20
+ "tiptap-markdown": "^0.9.0"
13
21
  }
14
22
  }
@@ -504,10 +504,12 @@
504
504
  "dependencies": [
505
505
  "@tiptap/react",
506
506
  "@tiptap/pm",
507
+ "@tiptap/core",
507
508
  "@tiptap/starter-kit",
508
509
  "@tiptap/extension-placeholder",
509
510
  "@tiptap/extension-link",
510
511
  "@tiptap/extension-text-style",
512
+ "tiptap-markdown",
511
513
  "lucide-react"
512
514
  ],
513
515
  "registryDependencies": ["utils"]
@@ -1659,6 +1661,49 @@
1659
1661
  "dependencies": ["@base-ui/react"],
1660
1662
  "registryDependencies": ["utils"]
1661
1663
  },
1664
+ "tree": {
1665
+ "name": "tree",
1666
+ "type": "component",
1667
+ "files": [
1668
+ {
1669
+ "src": "components/tree/index.tsx",
1670
+ "dest": "{components}/tree/index.tsx",
1671
+ "frameworks": ["plain"]
1672
+ },
1673
+ {
1674
+ "src": "components/tree/types.ts",
1675
+ "dest": "{components}/tree/types.ts",
1676
+ "frameworks": ["plain", "tailwind", "css-modules"]
1677
+ },
1678
+ {
1679
+ "src": "components/tree/flatten.ts",
1680
+ "dest": "{components}/tree/flatten.ts",
1681
+ "frameworks": ["plain", "tailwind", "css-modules"]
1682
+ },
1683
+ {
1684
+ "src": "components/tree/styles.css",
1685
+ "dest": "{components}/tree/styles.css",
1686
+ "frameworks": ["plain"]
1687
+ },
1688
+ {
1689
+ "src": "components/tree/index.tailwind.tsx",
1690
+ "dest": "{components}/tree/index.tsx",
1691
+ "frameworks": ["tailwind"]
1692
+ },
1693
+ {
1694
+ "src": "components/tree/index.module.tsx",
1695
+ "dest": "{components}/tree/index.tsx",
1696
+ "frameworks": ["css-modules"]
1697
+ },
1698
+ {
1699
+ "src": "components/tree/styles.module.css",
1700
+ "dest": "{components}/tree/styles.module.css",
1701
+ "frameworks": ["css-modules"]
1702
+ }
1703
+ ],
1704
+ "dependencies": [],
1705
+ "registryDependencies": ["utils"]
1706
+ },
1662
1707
  "carousel": {
1663
1708
  "name": "carousel",
1664
1709
  "type": "component",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$description": "컴포넌트별 토큰 의존성 (var(--*) 추출). build-registry-tokens.mjs 가 자동 생성.",
3
- "$generated": "2026-06-01T06:54:04.570Z",
3
+ "$generated": "2026-06-18T06:02:23.309Z",
4
4
  "components": {
5
5
  "button": {
6
6
  "plain": [
@@ -1931,6 +1931,45 @@
1931
1931
  ],
1932
1932
  "vanilla-extract": []
1933
1933
  },
1934
+ "tree": {
1935
+ "plain": [
1936
+ "--background-muted",
1937
+ "--border-width-strong",
1938
+ "--duration-fast",
1939
+ "--ease-standard",
1940
+ "--foreground",
1941
+ "--foreground-muted",
1942
+ "--radius",
1943
+ "--ring",
1944
+ "--space-2",
1945
+ "--space-4",
1946
+ "--text-xs",
1947
+ "--weight-medium"
1948
+ ],
1949
+ "tailwind": [
1950
+ "--border-width-strong",
1951
+ "--duration-fast",
1952
+ "--radius",
1953
+ "--space-2",
1954
+ "--space-4",
1955
+ "--text-xs"
1956
+ ],
1957
+ "css-modules": [
1958
+ "--background-muted",
1959
+ "--border-width-strong",
1960
+ "--duration-fast",
1961
+ "--ease-standard",
1962
+ "--foreground",
1963
+ "--foreground-muted",
1964
+ "--radius",
1965
+ "--ring",
1966
+ "--space-2",
1967
+ "--space-4",
1968
+ "--text-xs",
1969
+ "--weight-medium"
1970
+ ],
1971
+ "vanilla-extract": []
1972
+ },
1934
1973
  "carousel": {
1935
1974
  "plain": [
1936
1975
  "--background",
@@ -42,7 +42,7 @@
42
42
  "page-toc": "페이지 자동 목차 — 헤딩 스캔 · slugify · IntersectionObserver active 추적 · smooth scroll. routeKey 로 라우트 변경 자동 재스캔, levels/excludeSelector 커스터마이즈.",
43
43
  "code-editor": "CodeMirror 6 기반 코드 에디터 — js/ts/jsx/tsx/json/css/html/markdown, sh-ui 토큰 테마, readOnly·placeholder·minHeight/maxHeight. controlled(value/onChange) · uncontrolled(defaultValue) 모두 지원.",
44
44
  "markdown-editor": "마크다운 에디터 — CodeEditor + react-markdown 라이브 프리뷰 합성, GFM 지원, raw HTML 차단으로 XSS 방어. controlled(value/onChange) · uncontrolled(defaultValue) 모두 지원.",
45
- "rich-text-editor": "Tiptap 3 기반 WYSIWYG 에디터 — HTML 입출력, 기본 toolbar(헤딩·리스트·인용·코드·링크·강조), readOnly·placeholder. controlled(value/onChange) · uncontrolled(defaultValue) 모두 지원.",
45
+ "rich-text-editor": "Tiptap 3 기반 WYSIWYG 에디터 — format='html'(기본) 또는 format='markdown'(tiptap-markdown 직렬화) I/O, 기본 toolbar(헤딩·리스트·인용·코드·링크·강조), readOnly·placeholder. submitOnEnter+onSubmit(Enter 전송·Shift+Enter 줄바꿈), extensions(추가 확장 주입)·onCreate(에디터 인스턴스 접근). controlled(value/onChange) · uncontrolled(defaultValue) 모두 지원.",
46
46
  "base": "CSS 리셋 — base.css.",
47
47
  "breakpoints": "반응형 미디어 쿼리 토큰 — breakpoints.css.",
48
48
  "focus-ring": "공용 포커스 링 스타일 — focus-ring.css.",
@@ -57,6 +57,7 @@
57
57
  "form-tanstack": "TanStack Form 인스턴스 → sh-ui FormStore 어댑터. `adaptTanstackForm(tanstackInstance, config)` — TanStack Form 이 state owner. @tanstack/react-form 은 peerDependency.",
58
58
  "calendar": "내부 캘린더 위젯 (DatePicker 가 사용). single / multiple / range 모드. CalendarMessages 로 a11y 텍스트 override. 일반적으로 직접 사용보다 DatePicker / DateRangePicker 권장.",
59
59
  "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
+ "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 비의존 자체 구현.",
60
61
  "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 트랜지션 제거."
61
62
  }
62
63
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sh-ui-cli",
3
- "version": "0.116.0",
3
+ "version": "0.117.0",
4
4
  "description": "sh-ui CLI — 프로젝트 스캐폴드(create) + 컴포넌트 추가(add/list/remove) + IDE-내 AI용 MCP 서버",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -25,13 +25,13 @@
25
25
  "components"
26
26
  ],
27
27
  "dependencies": {
28
- "@inquirer/prompts": "^8.4.3",
28
+ "@inquirer/prompts": "^8.5.2",
29
29
  "@modelcontextprotocol/sdk": "^1.0.0",
30
30
  "zod": "^4.4.3"
31
31
  },
32
32
  "devDependencies": {
33
33
  "fs-extra": "^11.3.5",
34
- "vitest": "^3.2.4"
34
+ "vitest": "^4.1.8"
35
35
  },
36
36
  "publishConfig": {
37
37
  "access": "public"