sh-ui-cli 0.43.0 → 0.44.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 (33) hide show
  1. package/data/changelog/versions.json +12 -0
  2. package/data/registry/react/components/accordion/index.tailwind.tsx +88 -0
  3. package/data/registry/react/components/avatar/index.tailwind.tsx +74 -0
  4. package/data/registry/react/components/badge/index.tailwind.tsx +47 -0
  5. package/data/registry/react/components/breadcrumb/index.tailwind.tsx +138 -0
  6. package/data/registry/react/components/checkbox/index.tailwind.tsx +72 -0
  7. package/data/registry/react/components/code-panel/index.tailwind.tsx +107 -0
  8. package/data/registry/react/components/combobox/index.tailwind.tsx +160 -0
  9. package/data/registry/react/components/context-menu/index.tailwind.tsx +170 -0
  10. package/data/registry/react/components/date-picker/index.tailwind.tsx +294 -0
  11. package/data/registry/react/components/dialog/index.tailwind.tsx +96 -0
  12. package/data/registry/react/components/dropdown-menu/index.tailwind.tsx +205 -0
  13. package/data/registry/react/components/label/index.tailwind.tsx +78 -0
  14. package/data/registry/react/components/menubar/index.tailwind.tsx +32 -0
  15. package/data/registry/react/components/numeric-input/index.tailwind.tsx +113 -0
  16. package/data/registry/react/components/page-toc/index.tailwind.tsx +149 -0
  17. package/data/registry/react/components/pagination/index.tailwind.tsx +148 -0
  18. package/data/registry/react/components/popover/index.tailwind.tsx +77 -0
  19. package/data/registry/react/components/progress/index.tailwind.tsx +60 -0
  20. package/data/registry/react/components/radio/index.tailwind.tsx +54 -0
  21. package/data/registry/react/components/select/index.tailwind.tsx +199 -0
  22. package/data/registry/react/components/separator/index.tailwind.tsx +42 -0
  23. package/data/registry/react/components/skeleton/index.tailwind.tsx +39 -0
  24. package/data/registry/react/components/slider/index.tailwind.tsx +255 -0
  25. package/data/registry/react/components/spinner/index.tailwind.tsx +63 -0
  26. package/data/registry/react/components/switch/index.tailwind.tsx +62 -0
  27. package/data/registry/react/components/tabs/index.tailwind.tsx +113 -0
  28. package/data/registry/react/components/textarea/index.tailwind.tsx +21 -0
  29. package/data/registry/react/components/toggle/index.tailwind.tsx +111 -0
  30. package/data/registry/react/components/tooltip/index.tailwind.tsx +55 -0
  31. package/data/registry/react/registry.json +509 -74
  32. package/package.json +1 -1
  33. package/src/mcp.mjs +1 -1
@@ -2,6 +2,18 @@
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.44.0",
7
+ "date": "2026-04-30",
8
+ "title": "Tailwind 변종 대규모 확대 — 32 컴포넌트 utility-class 변종",
9
+ "type": "minor",
10
+ "highlights": [
11
+ "**32 컴포넌트가 Tailwind utility-class 변종 제공** — separator, skeleton, avatar, spinner, progress, tooltip, badge, label, switch, toggle, checkbox, radio, textarea, breadcrumb, menubar, numeric-input, page-toc, slider, popover, accordion, dialog, code-panel, pagination, date-picker, tabs, dropdown-menu, context-menu, select, combobox + 기존 button/card/input. cva 기반 variant 매트릭스로 prop 시그니처는 plain 변종과 100% 동일.",
12
+ "**dependency 분기 일반화** — `class-variance-authority` 같은 Tailwind 전용 의존성은 `{name, frameworks: [\"tailwind\"]}` 객체 형식으로 표기 — plain 사용자에게는 install 안 됨. 6 개 컴포넌트 (button, avatar, badge, spinner, switch, toggle) 가 cva 사용.",
13
+ "**fallback 자연 동작** — Tailwind 변종이 아직 없는 form, code-editor, calendar, color-picker, markdown-editor, rich-text-editor, carousel, file-upload, toast, header, sidebar 는 add 시 plain 변종으로 자동 설치 + `ℹ <name> — Tailwind 변종 미제공` 알림. plain CSS 도 @theme inline 브리지 덕에 Tailwind v4 환경에서 그대로 동작."
14
+ ],
15
+ "url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.44.0"
16
+ },
5
17
  {
6
18
  "version": "0.43.0",
7
19
  "date": "2026-04-30",
@@ -0,0 +1,88 @@
1
+ import * as React from "react";
2
+ import { Accordion as BaseAccordion } from "@base-ui/react/accordion";
3
+
4
+ function cx(...args: (string | undefined | false)[]) {
5
+ return args.filter(Boolean).join(" ");
6
+ }
7
+
8
+ type WithStringClassName<T> = Omit<T, "className"> & { className?: string };
9
+
10
+ export type AccordionSize = "sm" | "md";
11
+
12
+ type AccordionProps = WithStringClassName<
13
+ React.ComponentPropsWithoutRef<typeof BaseAccordion.Root>
14
+ > & {
15
+ size?: AccordionSize;
16
+ };
17
+
18
+ export const Accordion = React.forwardRef<HTMLDivElement, AccordionProps>(
19
+ ({ className, size = "md", ...props }, ref) => (
20
+ <BaseAccordion.Root
21
+ ref={ref}
22
+ className={cx("flex flex-col w-full", className)}
23
+ data-size={size}
24
+ {...props}
25
+ />
26
+ ),
27
+ );
28
+ Accordion.displayName = "Accordion";
29
+
30
+ export const AccordionItem = React.forwardRef<
31
+ HTMLDivElement,
32
+ WithStringClassName<React.ComponentPropsWithoutRef<typeof BaseAccordion.Item>>
33
+ >(({ className, ...props }, ref) => (
34
+ <BaseAccordion.Item
35
+ ref={ref}
36
+ className={cx("border-b border-border first:border-t", className)}
37
+ {...props}
38
+ />
39
+ ));
40
+ AccordionItem.displayName = "AccordionItem";
41
+
42
+ export const AccordionTrigger = React.forwardRef<
43
+ HTMLButtonElement,
44
+ WithStringClassName<React.ComponentPropsWithoutRef<typeof BaseAccordion.Trigger>>
45
+ >(({ className, children, ...props }, ref) => (
46
+ <BaseAccordion.Header className="m-0 font-[inherit]">
47
+ <BaseAccordion.Trigger
48
+ ref={ref}
49
+ className={cx(
50
+ "flex items-center justify-between gap-[var(--space-4)] w-full px-[var(--space-1)] py-[var(--space-4)] bg-transparent border-none text-foreground text-[0.9375rem] font-medium leading-snug text-left cursor-pointer transition-[background-color] duration-[var(--duration-fast)] hover:not-disabled:not-data-[disabled]:bg-background-muted focus-visible:outline-[length:var(--border-width-strong)] focus-visible:outline-foreground focus-visible:outline-offset-2 focus-visible:rounded-[calc(var(--radius)-2px)] disabled:cursor-not-allowed disabled:text-foreground-muted data-[disabled]:cursor-not-allowed data-[disabled]:text-foreground-muted [[data-size=sm]_&]:py-[var(--space-2)] [[data-size=sm]_&]:text-[length:var(--text-xs)] [[data-size=sm]_&]:leading-[1.2] motion-reduce:transition-none",
51
+ className,
52
+ )}
53
+ {...props}
54
+ >
55
+ <span className="min-w-0 [overflow-wrap:anywhere]">{children}</span>
56
+ <svg
57
+ className="shrink-0 text-foreground-muted transition-transform duration-[180ms] data-[panel-open]:rotate-180 [[data-size=sm]_&]:w-3 [[data-size=sm]_&]:h-3 motion-reduce:transition-none"
58
+ width="16"
59
+ height="16"
60
+ viewBox="0 0 16 16"
61
+ fill="none"
62
+ aria-hidden="true"
63
+ >
64
+ <path d="M4 6l4 4 4-4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
65
+ </svg>
66
+ </BaseAccordion.Trigger>
67
+ </BaseAccordion.Header>
68
+ ));
69
+ AccordionTrigger.displayName = "AccordionTrigger";
70
+
71
+ export const AccordionContent = React.forwardRef<
72
+ HTMLDivElement,
73
+ WithStringClassName<React.ComponentPropsWithoutRef<typeof BaseAccordion.Panel>>
74
+ >(({ className, children, ...props }, ref) => (
75
+ <BaseAccordion.Panel
76
+ ref={ref}
77
+ className={cx(
78
+ "overflow-hidden h-[var(--accordion-panel-height)] transition-[height] duration-[var(--duration-slow)] data-[starting-style]:h-0 data-[ending-style]:h-0 motion-reduce:transition-none",
79
+ className,
80
+ )}
81
+ {...props}
82
+ >
83
+ <div className="px-[var(--space-1)] pb-[var(--space-4)] text-[length:var(--text-sm)] leading-relaxed text-foreground-muted [[data-size=sm]_&]:pb-[var(--space-2)] [[data-size=sm]_&]:text-[length:var(--text-xs)] [[data-size=sm]_&]:leading-[1.5]">
84
+ {children}
85
+ </div>
86
+ </BaseAccordion.Panel>
87
+ ));
88
+ AccordionContent.displayName = "AccordionContent";
@@ -0,0 +1,74 @@
1
+ import * as React from "react";
2
+ import { Avatar as BaseAvatar } from "@base-ui/react/avatar";
3
+ import { cva, type VariantProps } from "class-variance-authority";
4
+
5
+ type WithStringClassName<T> = Omit<T, "className"> & { className?: string };
6
+
7
+ function cx(...args: (string | undefined | false | null)[]) {
8
+ return args.filter(Boolean).join(" ");
9
+ }
10
+
11
+ const avatarVariants = cva(
12
+ "relative inline-flex items-center justify-center shrink-0 align-middle overflow-hidden rounded-full bg-background-muted text-foreground-muted font-medium select-none",
13
+ {
14
+ variants: {
15
+ size: {
16
+ sm: "w-7 h-7 text-[length:var(--text-xs)]",
17
+ md: "w-10 h-10 text-[0.8125rem]",
18
+ lg: "w-12 h-12 text-[length:var(--text-sm)]",
19
+ xl: "w-16 h-16 text-[length:var(--text-base)]",
20
+ },
21
+ },
22
+ defaultVariants: { size: "md" },
23
+ },
24
+ );
25
+
26
+ export type AvatarSize = NonNullable<VariantProps<typeof avatarVariants>["size"]>;
27
+
28
+ export interface AvatarProps
29
+ extends WithStringClassName<
30
+ React.ComponentPropsWithoutRef<typeof BaseAvatar.Root>
31
+ > {
32
+ size?: AvatarSize;
33
+ }
34
+
35
+ export const Avatar = React.forwardRef<HTMLSpanElement, AvatarProps>(
36
+ function Avatar({ className, size = "md", ...props }, ref) {
37
+ return (
38
+ <BaseAvatar.Root
39
+ ref={ref}
40
+ className={cx(avatarVariants({ size }), className)}
41
+ {...props}
42
+ />
43
+ );
44
+ },
45
+ );
46
+
47
+ export const AvatarImage = React.forwardRef<
48
+ HTMLImageElement,
49
+ WithStringClassName<React.ComponentPropsWithoutRef<typeof BaseAvatar.Image>>
50
+ >(function AvatarImage({ className, ...props }, ref) {
51
+ return (
52
+ <BaseAvatar.Image
53
+ ref={ref}
54
+ className={cx("w-full h-full object-cover block", className)}
55
+ {...props}
56
+ />
57
+ );
58
+ });
59
+
60
+ export const AvatarFallback = React.forwardRef<
61
+ HTMLSpanElement,
62
+ WithStringClassName<React.ComponentPropsWithoutRef<typeof BaseAvatar.Fallback>>
63
+ >(function AvatarFallback({ className, ...props }, ref) {
64
+ return (
65
+ <BaseAvatar.Fallback
66
+ ref={ref}
67
+ className={cx(
68
+ "inline-flex items-center justify-center w-full h-full uppercase tracking-[0.02em]",
69
+ className,
70
+ )}
71
+ {...props}
72
+ />
73
+ );
74
+ });
@@ -0,0 +1,47 @@
1
+ import * as React from "react";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
+
4
+ function cx(...args: (string | undefined | false | null)[]) {
5
+ return args.filter(Boolean).join(" ");
6
+ }
7
+
8
+ const badgeVariants = cva(
9
+ "inline-flex items-center gap-1 px-2 border border-transparent rounded-full font-medium leading-none whitespace-nowrap align-middle select-none",
10
+ {
11
+ variants: {
12
+ variant: {
13
+ primary: "bg-primary text-primary-foreground",
14
+ secondary: "bg-background-muted text-foreground border-border",
15
+ success: "bg-[var(--success,#16a34a)] text-white",
16
+ warning: "bg-[var(--warning,#d97706)] text-white",
17
+ danger: "bg-danger text-[var(--danger-foreground,#fff)]",
18
+ outline: "bg-transparent text-foreground border-border-strong",
19
+ },
20
+ size: {
21
+ sm: "h-5 px-1.5 text-[0.6875rem]",
22
+ md: "h-6 text-[length:var(--text-xs)]",
23
+ },
24
+ },
25
+ defaultVariants: { variant: "primary", size: "md" },
26
+ },
27
+ );
28
+
29
+ export type BadgeVariant = NonNullable<VariantProps<typeof badgeVariants>["variant"]>;
30
+ export type BadgeSize = NonNullable<VariantProps<typeof badgeVariants>["size"]>;
31
+
32
+ export interface BadgeProps extends React.HTMLAttributes<HTMLSpanElement> {
33
+ variant?: BadgeVariant;
34
+ size?: BadgeSize;
35
+ }
36
+
37
+ export const Badge = React.forwardRef<HTMLSpanElement, BadgeProps>(
38
+ function Badge({ className, variant = "primary", size = "md", ...props }, ref) {
39
+ return (
40
+ <span
41
+ ref={ref}
42
+ className={cx(badgeVariants({ variant, size }), className)}
43
+ {...props}
44
+ />
45
+ );
46
+ },
47
+ );
@@ -0,0 +1,138 @@
1
+ import * as React from "react";
2
+
3
+ function cx(...args: (string | undefined | false | null)[]) {
4
+ return args.filter(Boolean).join(" ");
5
+ }
6
+
7
+ export const Breadcrumb = React.forwardRef<
8
+ HTMLElement,
9
+ React.HTMLAttributes<HTMLElement>
10
+ >(function Breadcrumb({ className, ...props }, ref) {
11
+ return (
12
+ <nav
13
+ ref={ref}
14
+ aria-label="Breadcrumb"
15
+ className={cx("text-[length:var(--text-sm)] text-foreground-muted", className)}
16
+ {...props}
17
+ />
18
+ );
19
+ });
20
+
21
+ export const BreadcrumbList = React.forwardRef<
22
+ HTMLOListElement,
23
+ React.OlHTMLAttributes<HTMLOListElement>
24
+ >(function BreadcrumbList({ className, ...props }, ref) {
25
+ return (
26
+ <ol
27
+ ref={ref}
28
+ className={cx(
29
+ "flex items-center flex-wrap gap-1.5 m-0 p-0 list-none",
30
+ className,
31
+ )}
32
+ {...props}
33
+ />
34
+ );
35
+ });
36
+
37
+ export const BreadcrumbItem = React.forwardRef<
38
+ HTMLLIElement,
39
+ React.LiHTMLAttributes<HTMLLIElement>
40
+ >(function BreadcrumbItem({ className, ...props }, ref) {
41
+ return (
42
+ <li
43
+ ref={ref}
44
+ className={cx("inline-flex items-center gap-1.5 min-w-0", className)}
45
+ {...props}
46
+ />
47
+ );
48
+ });
49
+
50
+ export const BreadcrumbLink = React.forwardRef<
51
+ HTMLAnchorElement,
52
+ React.AnchorHTMLAttributes<HTMLAnchorElement>
53
+ >(function BreadcrumbLink({ className, ...props }, ref) {
54
+ return (
55
+ <a
56
+ ref={ref}
57
+ className={cx(
58
+ "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-foreground focus-visible:outline-offset-2 motion-reduce:transition-none",
59
+ className,
60
+ )}
61
+ {...props}
62
+ />
63
+ );
64
+ });
65
+
66
+ export const BreadcrumbPage = React.forwardRef<
67
+ HTMLSpanElement,
68
+ React.HTMLAttributes<HTMLSpanElement>
69
+ >(function BreadcrumbPage({ className, ...props }, ref) {
70
+ return (
71
+ <span
72
+ ref={ref}
73
+ role="link"
74
+ aria-current="page"
75
+ aria-disabled="true"
76
+ className={cx(
77
+ "text-foreground font-medium overflow-hidden text-ellipsis whitespace-nowrap",
78
+ className,
79
+ )}
80
+ {...props}
81
+ />
82
+ );
83
+ });
84
+
85
+ export const BreadcrumbSeparator = React.forwardRef<
86
+ HTMLLIElement,
87
+ React.LiHTMLAttributes<HTMLLIElement>
88
+ >(function BreadcrumbSeparator({ className, children, ...props }, ref) {
89
+ return (
90
+ <li
91
+ ref={ref}
92
+ role="presentation"
93
+ aria-hidden="true"
94
+ className={cx(
95
+ "inline-flex items-center text-foreground-muted opacity-60",
96
+ className,
97
+ )}
98
+ {...props}
99
+ >
100
+ {children ?? <ChevronRightIcon />}
101
+ </li>
102
+ );
103
+ });
104
+
105
+ export const BreadcrumbEllipsis = React.forwardRef<
106
+ HTMLSpanElement,
107
+ React.HTMLAttributes<HTMLSpanElement>
108
+ >(function BreadcrumbEllipsis({ className, ...props }, ref) {
109
+ return (
110
+ <span
111
+ ref={ref}
112
+ role="presentation"
113
+ aria-hidden="true"
114
+ className={cx(
115
+ "inline-flex items-center w-6 h-6 justify-center text-foreground-muted",
116
+ className,
117
+ )}
118
+ {...props}
119
+ >
120
+ <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor" aria-hidden>
121
+ <circle cx="3" cy="8" r="1.25" />
122
+ <circle cx="8" cy="8" r="1.25" />
123
+ <circle cx="13" cy="8" r="1.25" />
124
+ </svg>
125
+ <span className="absolute w-px h-px p-0 -m-px overflow-hidden whitespace-nowrap border-0 [clip:rect(0,0,0,0)]">
126
+ 더 보기
127
+ </span>
128
+ </span>
129
+ );
130
+ });
131
+
132
+ function ChevronRightIcon() {
133
+ return (
134
+ <svg width="14" height="14" viewBox="0 0 16 16" fill="none" aria-hidden>
135
+ <path d="M6 4l4 4-4 4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
136
+ </svg>
137
+ );
138
+ }
@@ -0,0 +1,72 @@
1
+ import * as React from "react";
2
+ import { Checkbox as BaseCheckbox } from "@base-ui/react/checkbox";
3
+ import { CheckboxGroup as BaseCheckboxGroup } from "@base-ui/react/checkbox-group";
4
+
5
+ function cx(...args: (string | undefined | false)[]) {
6
+ return args.filter(Boolean).join(" ");
7
+ }
8
+
9
+ export type CheckboxProps = Omit<
10
+ React.ComponentPropsWithoutRef<typeof BaseCheckbox.Root>,
11
+ "className"
12
+ > & {
13
+ className?: string;
14
+ };
15
+
16
+ export const Checkbox = React.forwardRef<HTMLElement, CheckboxProps>(
17
+ ({ className, ...props }, ref) => (
18
+ <BaseCheckbox.Root
19
+ ref={ref}
20
+ className={cx(
21
+ "inline-flex items-center justify-center w-[1.125rem] h-[1.125rem] border border-border-strong rounded-[calc(var(--radius)-2px)] bg-background text-primary-foreground cursor-pointer shrink-0 transition-[background-color,border-color] duration-[var(--duration-fast)] hover:not-data-[disabled]:border-foreground focus-visible:outline-[length:var(--border-width-strong)] focus-visible:outline-foreground focus-visible:outline-offset-2 data-[checked]:bg-primary data-[checked]:border-primary data-[indeterminate]:bg-primary data-[indeterminate]:border-primary data-[disabled]:opacity-[var(--opacity-disabled)] data-[disabled]:cursor-not-allowed motion-reduce:transition-none [@media(hover:none)_and_(pointer:coarse)]:w-5 [@media(hover:none)_and_(pointer:coarse)]:h-5",
22
+ className,
23
+ )}
24
+ {...props}
25
+ >
26
+ <BaseCheckbox.Indicator className="inline-flex items-center justify-center">
27
+ {props.indeterminate ? <MinusIcon /> : <CheckIcon />}
28
+ </BaseCheckbox.Indicator>
29
+ </BaseCheckbox.Root>
30
+ ),
31
+ );
32
+ Checkbox.displayName = "Checkbox";
33
+
34
+ export type CheckboxGroupProps = Omit<
35
+ React.ComponentPropsWithoutRef<typeof BaseCheckboxGroup>,
36
+ "className"
37
+ > & {
38
+ className?: string;
39
+ orientation?: "horizontal" | "vertical";
40
+ };
41
+
42
+ export const CheckboxGroup = React.forwardRef<HTMLDivElement, CheckboxGroupProps>(
43
+ ({ className, orientation = "vertical", ...props }, ref) => (
44
+ <BaseCheckboxGroup
45
+ ref={ref}
46
+ className={cx(
47
+ "flex gap-2.5",
48
+ orientation === "vertical" ? "flex-col" : "flex-row flex-wrap",
49
+ className,
50
+ )}
51
+ data-orientation={orientation}
52
+ {...props}
53
+ />
54
+ ),
55
+ );
56
+ CheckboxGroup.displayName = "CheckboxGroup";
57
+
58
+ function CheckIcon() {
59
+ return (
60
+ <svg viewBox="0 0 16 16" width="12" height="12" fill="none" aria-hidden>
61
+ <path d="M3.5 8.5l3 3 6-7" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
62
+ </svg>
63
+ );
64
+ }
65
+
66
+ function MinusIcon() {
67
+ return (
68
+ <svg viewBox="0 0 16 16" width="12" height="12" fill="none" aria-hidden>
69
+ <path d="M4 8h8" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
70
+ </svg>
71
+ );
72
+ }
@@ -0,0 +1,107 @@
1
+ import { codeToHtml } from "shiki";
2
+ import { CodePanelCopyButton } from "./copy";
3
+
4
+ function cx(...args: (string | undefined | false | null)[]) {
5
+ return args.filter(Boolean).join(" ");
6
+ }
7
+
8
+ export interface CodePanelProps
9
+ extends Omit<React.HTMLAttributes<HTMLDivElement>, "children"> {
10
+ code?: string;
11
+ language?: string;
12
+ filename?: string;
13
+ showLineNumbers?: boolean;
14
+ hideCopy?: boolean;
15
+ children?: React.ReactNode;
16
+ }
17
+
18
+ const rootClasses =
19
+ "group relative border border-border rounded-[var(--radius)] bg-background-subtle overflow-hidden text-[0.8125rem] leading-relaxed my-[var(--space-4)] max-sm:text-[length:var(--text-xs)]";
20
+
21
+ export async function CodePanel({
22
+ code, language = "text", filename, showLineNumbers = true, hideCopy, className, children, ...rest
23
+ }: CodePanelProps) {
24
+ const classes = cx(rootClasses, className);
25
+
26
+ if (children !== undefined) {
27
+ return <div className={classes} {...rest}>{children}</div>;
28
+ }
29
+ if (code === undefined) throw new Error("CodePanel: `code` prop 또는 children 중 하나가 필요합니다.");
30
+
31
+ const trimmed = code.replace(/\n$/, "");
32
+
33
+ return (
34
+ <div className={classes} {...rest}>
35
+ {filename ? (
36
+ <CodePanelHeader>
37
+ <CodePanelFilename>{filename}</CodePanelFilename>
38
+ {!hideCopy && <CodePanelCopy code={trimmed} />}
39
+ </CodePanelHeader>
40
+ ) : (
41
+ !hideCopy && (
42
+ <div className="absolute top-[var(--space-2)] right-[var(--space-2)] z-[1] opacity-0 transition-opacity duration-[var(--duration-fast)] group-hover:opacity-100 group-focus-within:opacity-100">
43
+ <CodePanelCopy code={trimmed} />
44
+ </div>
45
+ )
46
+ )}
47
+ <CodePanelBody code={trimmed} language={language} showLineNumbers={showLineNumbers} />
48
+ </div>
49
+ );
50
+ }
51
+
52
+ export function CodePanelHeader({ className, children, ...props }: React.HTMLAttributes<HTMLDivElement>) {
53
+ return (
54
+ <div
55
+ className={cx(
56
+ "flex items-center justify-between gap-[var(--space-2)] py-[var(--space-2)] pl-[var(--space-4)] pr-[var(--space-3)] border-b border-border bg-background-muted text-[length:var(--text-xs)] text-foreground-muted",
57
+ className,
58
+ )}
59
+ {...props}
60
+ >
61
+ {children}
62
+ </div>
63
+ );
64
+ }
65
+
66
+ export function CodePanelFilename({ className, children, ...props }: React.HTMLAttributes<HTMLSpanElement>) {
67
+ return (
68
+ <span className={cx("font-mono text-foreground", className)} {...props}>{children}</span>
69
+ );
70
+ }
71
+
72
+ export interface CodePanelCopyProps { code: string; }
73
+ export function CodePanelCopy({ code }: CodePanelCopyProps) {
74
+ return <CodePanelCopyButton code={code} />;
75
+ }
76
+
77
+ export interface CodePanelBodyProps
78
+ extends Omit<React.HTMLAttributes<HTMLDivElement>, "children" | "dangerouslySetInnerHTML"> {
79
+ code: string;
80
+ language?: string;
81
+ showLineNumbers?: boolean;
82
+ }
83
+
84
+ export async function CodePanelBody({
85
+ code, language = "text", showLineNumbers = true, className, ...rest
86
+ }: CodePanelBodyProps) {
87
+ const trimmed = code.replace(/\n$/, "");
88
+ const html = await codeToHtml(trimmed, {
89
+ lang: language,
90
+ themes: { light: "github-light", dark: "github-dark" },
91
+ defaultColor: false,
92
+ });
93
+
94
+ return (
95
+ <div
96
+ className={cx(
97
+ "overflow-x-auto [&_pre]:m-0 [&_pre]:py-[var(--space-3)] [&_pre]:px-[var(--space-4)] [&_pre]:!bg-transparent [&_pre]:font-mono [&_pre]:text-[length:inherit] [&_pre]:leading-[inherit] [&_pre]:border-none [&_pre]:rounded-none [&_code]:bg-transparent [&_code]:p-0 [&_code]:text-[length:inherit] [&_code]:block [&_.shiki]:!text-[var(--shiki-light)] [&_.shiki_span]:!text-[var(--shiki-light)] [&_.shiki]:!bg-transparent [&_.shiki_span]:!bg-transparent [.dark_&_.shiki]:!text-[var(--shiki-dark)] [.dark_&_.shiki_span]:!text-[var(--shiki-dark)] data-[line-numbers]:[&_pre_code]:[counter-reset:step] data-[line-numbers]:[&_pre_code_.line]:before:[content:counter(step)] data-[line-numbers]:[&_pre_code_.line]:before:[counter-increment:step] data-[line-numbers]:[&_pre_code_.line]:before:inline-block data-[line-numbers]:[&_pre_code_.line]:before:w-7 data-[line-numbers]:[&_pre_code_.line]:before:mr-[var(--space-4)] data-[line-numbers]:[&_pre_code_.line]:before:text-right data-[line-numbers]:[&_pre_code_.line]:before:text-foreground-muted data-[line-numbers]:[&_pre_code_.line]:before:opacity-70 data-[line-numbers]:[&_pre_code_.line]:before:select-none",
98
+ className,
99
+ )}
100
+ data-line-numbers={showLineNumbers || undefined}
101
+ dangerouslySetInnerHTML={{ __html: html }}
102
+ {...rest}
103
+ />
104
+ );
105
+ }
106
+
107
+ export { CodePanelCopyButton };