sh-ui-cli 0.116.0 → 0.118.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.
Files changed (28) hide show
  1. package/data/changelog/versions.json +24 -0
  2. package/data/registry/flutter/registry.json +9 -0
  3. package/data/registry/flutter/widgets/sh_ui_tree.dart +428 -0
  4. package/data/registry/react/components/rich-text-editor/index.module.tsx +98 -8
  5. package/data/registry/react/components/rich-text-editor/index.tailwind.tsx +87 -4
  6. package/data/registry/react/components/rich-text-editor/index.tsx +98 -8
  7. package/data/registry/react/components/rich-text-editor/rich-text-editor.test.tsx +283 -0
  8. package/data/registry/react/components/table/index.module.tsx +69 -0
  9. package/data/registry/react/components/table/index.tailwind.tsx +68 -0
  10. package/data/registry/react/components/table/index.tsx +69 -0
  11. package/data/registry/react/components/table/styles.css +57 -0
  12. package/data/registry/react/components/table/styles.module.css +57 -0
  13. package/data/registry/react/components/table/table.test.tsx +59 -0
  14. package/data/registry/react/components/tree/flatten.test.ts +72 -0
  15. package/data/registry/react/components/tree/flatten.ts +69 -0
  16. package/data/registry/react/components/tree/index.module.tsx +215 -0
  17. package/data/registry/react/components/tree/index.tailwind.tsx +224 -0
  18. package/data/registry/react/components/tree/index.tsx +215 -0
  19. package/data/registry/react/components/tree/styles.css +69 -0
  20. package/data/registry/react/components/tree/styles.module.css +69 -0
  21. package/data/registry/react/components/tree/tree.test.tsx +110 -0
  22. package/data/registry/react/components/tree/types.ts +36 -0
  23. package/data/registry/react/peer-versions.json +9 -1
  24. package/data/registry/react/registry.json +78 -0
  25. package/data/registry/react/tokens-used.json +76 -1
  26. package/data/summaries/flutter.json +2 -1
  27. package/data/summaries/react.json +3 -1
  28. package/package.json +3 -3
@@ -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
+ });
@@ -0,0 +1,72 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { flattenVisible, nextFocusable, prevFocusable, findByTypeahead } from "./flatten";
3
+ import type { TreeNode } from "./types";
4
+
5
+ const tree: TreeNode[] = [
6
+ { id: "a", label: "Apple", children: [
7
+ { id: "a1", label: "Ant" },
8
+ { id: "a2", label: "Arc", disabled: true },
9
+ ] },
10
+ { id: "b", label: "Banana" },
11
+ ];
12
+
13
+ describe("flattenVisible", () => {
14
+ it("닫힌 트리는 최상위만, hasChildren 표기", () => {
15
+ const v = flattenVisible(tree, new Set());
16
+ expect(v.map((n) => n.id)).toEqual(["a", "b"]);
17
+ expect(v[0].hasChildren).toBe(true);
18
+ expect(v[0].expanded).toBe(false);
19
+ expect(v[1].hasChildren).toBe(false);
20
+ });
21
+
22
+ it("부모가 펼쳐지면 자식이 가시 + level/aria 메타", () => {
23
+ const v = flattenVisible(tree, new Set(["a"]));
24
+ expect(v.map((n) => n.id)).toEqual(["a", "a1", "a2", "b"]);
25
+ const a1 = v[1];
26
+ expect(a1.level).toBe(1);
27
+ expect(a1.parentId).toBe("a");
28
+ expect(a1.posInSet).toBe(1);
29
+ expect(a1.setSize).toBe(2);
30
+ });
31
+
32
+ it("부모가 닫혀 있으면 자식은 안 보임", () => {
33
+ const v = flattenVisible(tree, new Set(["a2-only-irrelevant"]));
34
+ expect(v.map((n) => n.id)).toEqual(["a", "b"]);
35
+ });
36
+ });
37
+
38
+ describe("nextFocusable / prevFocusable", () => {
39
+ const v = flattenVisible(tree, new Set(["a"]));
40
+ it("다음 포커스는 disabled 를 건너뛴다", () => {
41
+ expect(nextFocusable(v, "a1")?.id).toBe("b");
42
+ });
43
+ it("이전 포커스도 disabled 를 건너뛴다", () => {
44
+ expect(prevFocusable(v, "b")?.id).toBe("a1");
45
+ });
46
+ it("끝에서 다음은 null", () => {
47
+ expect(nextFocusable(v, "b")).toBeNull();
48
+ });
49
+ it("처음에서 이전은 null", () => {
50
+ expect(prevFocusable(v, "a")).toBeNull();
51
+ });
52
+ it("fromId 가 없으면 next/prev 모두 null", () => {
53
+ expect(nextFocusable(v, "nonexistent")).toBeNull();
54
+ expect(prevFocusable(v, "nonexistent")).toBeNull();
55
+ });
56
+ });
57
+
58
+ describe("findByTypeahead", () => {
59
+ const v = flattenVisible(tree, new Set(["a"]));
60
+ it("접두사로 다음 가시 노드를 찾는다 (대소문자 무시)", () => {
61
+ expect(findByTypeahead(v, "ban", "a")?.id).toBe("b");
62
+ });
63
+ it("매치 없으면 null", () => {
64
+ expect(findByTypeahead(v, "zz", "a")).toBeNull();
65
+ });
66
+ it("disabled 는 typeahead 대상에서 제외", () => {
67
+ expect(findByTypeahead(v, "arc", "a")).toBeNull();
68
+ });
69
+ it("빈 prefix 는 null", () => {
70
+ expect(findByTypeahead(v, "", "a")).toBeNull();
71
+ });
72
+ });
@@ -0,0 +1,69 @@
1
+ import type { TreeNode, VisibleNode } from "./types";
2
+
3
+ /**
4
+ * 노드 label 을 typeahead 비교용 문자열로 변환한다.
5
+ * label 이 string 이면 그 값을, JSX 등 비문자열 ReactNode 면 "" 를 반환한다
6
+ * (비문자열 라벨 노드는 typeahead 검색 대상에서 제외됨).
7
+ */
8
+ function labelText(node: TreeNode): string {
9
+ return typeof node.label === "string" ? node.label : "";
10
+ }
11
+
12
+ export function flattenVisible(nodes: TreeNode[], expanded: Set<string>): VisibleNode[] {
13
+ const out: VisibleNode[] = [];
14
+ const walk = (siblings: TreeNode[], level: number, parentId: string | null) => {
15
+ siblings.forEach((node, i) => {
16
+ const hasChildren = !!node.children && node.children.length > 0;
17
+ const isExpanded = hasChildren && expanded.has(node.id);
18
+ out.push({
19
+ id: node.id,
20
+ node,
21
+ level,
22
+ parentId,
23
+ hasChildren,
24
+ expanded: isExpanded,
25
+ disabled: !!node.disabled,
26
+ setSize: siblings.length,
27
+ posInSet: i + 1,
28
+ });
29
+ if (isExpanded) walk(node.children!, level + 1, node.id);
30
+ });
31
+ };
32
+ walk(nodes, 0, null);
33
+ return out;
34
+ }
35
+
36
+ export function nextFocusable(visible: VisibleNode[], fromId: string): VisibleNode | null {
37
+ const i = visible.findIndex((n) => n.id === fromId);
38
+ if (i === -1) return null;
39
+ for (let j = i + 1; j < visible.length; j++) {
40
+ if (!visible[j].disabled) return visible[j];
41
+ }
42
+ return null;
43
+ }
44
+
45
+ export function prevFocusable(visible: VisibleNode[], fromId: string): VisibleNode | null {
46
+ const i = visible.findIndex((n) => n.id === fromId);
47
+ if (i === -1) return null;
48
+ for (let j = i - 1; j >= 0; j--) {
49
+ if (!visible[j].disabled) return visible[j];
50
+ }
51
+ return null;
52
+ }
53
+
54
+ export function findByTypeahead(
55
+ visible: VisibleNode[],
56
+ prefix: string,
57
+ fromId: string,
58
+ ): VisibleNode | null {
59
+ const p = prefix.toLowerCase();
60
+ if (!p) return null;
61
+ const start = visible.findIndex((n) => n.id === fromId);
62
+ const n = visible.length;
63
+ for (let k = 1; k <= n; k++) {
64
+ const cand = visible[(start + k) % n];
65
+ if (cand.disabled) continue;
66
+ if (labelText(cand.node).toLowerCase().startsWith(p)) return cand;
67
+ }
68
+ return null;
69
+ }