@conscience-technology/ctds 0.1.9 → 0.1.10

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 (76) hide show
  1. package/assets/components/AIMessage/AIMessage.module.css +117 -0
  2. package/assets/components/AIMessage/AIMessage.tsx +90 -0
  3. package/assets/components/AIMessage/index.ts +2 -0
  4. package/assets/components/AttachmentChip/AttachmentChip.module.css +3 -1
  5. package/assets/components/AttachmentThumb/AttachmentThumb.module.css +13 -7
  6. package/assets/components/AttachmentThumb/AttachmentThumb.tsx +4 -1
  7. package/assets/components/Bubble/Bubble.module.css +65 -0
  8. package/assets/components/Bubble/Bubble.tsx +63 -0
  9. package/assets/components/Bubble/index.ts +2 -0
  10. package/assets/components/Button/Button.module.css +15 -2
  11. package/assets/components/Button/Button.tsx +13 -0
  12. package/assets/components/Calendar/Calendar.module.css +163 -0
  13. package/assets/components/Calendar/Calendar.tsx +323 -0
  14. package/assets/components/Calendar/index.ts +2 -0
  15. package/assets/components/ChatInput/ChatInput.module.css +82 -45
  16. package/assets/components/ChatInput/ChatInput.tsx +46 -34
  17. package/assets/components/ChatInput/ModelSelector.tsx +23 -27
  18. package/assets/components/ChatInput/index.ts +0 -1
  19. package/assets/components/CodeBlock/CodeBlock.module.css +79 -0
  20. package/assets/components/CodeBlock/CodeBlock.tsx +71 -0
  21. package/assets/components/CodeBlock/index.ts +2 -0
  22. package/assets/components/ContextMenu/ContextMenu.tsx +10 -1
  23. package/assets/components/DropdownMenu/DropdownMenu.module.css +26 -0
  24. package/assets/components/DropdownMenu/DropdownMenu.tsx +367 -0
  25. package/assets/components/DropdownMenu/index.ts +9 -0
  26. package/assets/components/EventChip/EventChip.module.css +57 -0
  27. package/assets/components/EventChip/EventChip.tsx +41 -0
  28. package/assets/components/EventChip/index.ts +2 -0
  29. package/assets/components/Icon/Icon.tsx +3 -0
  30. package/assets/components/IdentityButton/IdentityButton.module.css +119 -0
  31. package/assets/components/IdentityButton/IdentityButton.tsx +113 -0
  32. package/assets/components/IdentityButton/index.ts +6 -0
  33. package/assets/components/MenuItem/MenuItem.module.css +67 -2
  34. package/assets/components/MenuItem/MenuItem.tsx +22 -2
  35. package/assets/components/MessageActions/MessageActions.module.css +43 -0
  36. package/assets/components/MessageActions/MessageActions.tsx +94 -0
  37. package/assets/components/MessageActions/index.ts +6 -0
  38. package/assets/components/MessageQuote/MessageQuote.module.css +18 -0
  39. package/assets/components/MessageQuote/MessageQuote.tsx +33 -0
  40. package/assets/components/MessageQuote/index.ts +2 -0
  41. package/assets/components/MessageSources/MessageSources.module.css +63 -0
  42. package/assets/components/MessageSources/MessageSources.tsx +50 -0
  43. package/assets/components/MessageSources/index.ts +2 -0
  44. package/assets/components/MessageTable/MessageTable.module.css +46 -0
  45. package/assets/components/MessageTable/MessageTable.tsx +21 -0
  46. package/assets/components/MessageTable/index.ts +2 -0
  47. package/assets/components/QuotationBar/QuotationBar.module.css +2 -2
  48. package/assets/components/Sidebar/Sidebar.module.css +3 -57
  49. package/assets/components/Sidebar/Sidebar.tsx +18 -21
  50. package/assets/components/Spinner/LottiePlayer.tsx +75 -0
  51. package/assets/components/Spinner/Spinner.module.css +167 -0
  52. package/assets/components/Spinner/Spinner.tsx +119 -0
  53. package/assets/components/Spinner/index.ts +10 -0
  54. package/assets/components/Spinner/lottieColor.ts +76 -0
  55. package/assets/components/Spinner/lottieManifest.ts +99 -0
  56. package/assets/components/Tabs/Tabs.module.css +3 -3
  57. package/assets/components/UserMessage/UserMessage.module.css +41 -0
  58. package/assets/components/UserMessage/UserMessage.tsx +66 -0
  59. package/assets/components/UserMessage/index.ts +2 -0
  60. package/assets/components/_data.ts +18 -4
  61. package/assets/components/tokens.css +20 -0
  62. package/assets/prompts/components/ai-message.md +43 -0
  63. package/assets/prompts/components/bubble.md +56 -0
  64. package/assets/prompts/components/calendar.md +123 -0
  65. package/assets/prompts/components/code-block.md +35 -0
  66. package/assets/prompts/components/dropdown-menu.md +145 -0
  67. package/assets/prompts/components/identity-button.md +168 -0
  68. package/assets/prompts/components/message-actions.md +49 -0
  69. package/assets/prompts/components/message-quote.md +26 -0
  70. package/assets/prompts/components/message-sources.md +26 -0
  71. package/assets/prompts/components/message-table.md +27 -0
  72. package/assets/prompts/components/sidebar.md +48 -3
  73. package/assets/prompts/components/spinner.md +109 -0
  74. package/assets/prompts/components/user-message.md +45 -0
  75. package/assets/prompts/primitives/scroll.md +22 -11
  76. package/package.json +1 -1
@@ -1,40 +1,36 @@
1
1
  "use client";
2
2
 
3
- import type { MouseEvent } from "react";
3
+ import { forwardRef, type ComponentPropsWithoutRef } from "react";
4
4
  import {
5
5
  Infinity as PhInfinity,
6
6
  CaretDown as PhCaretDown,
7
7
  } from "@phosphor-icons/react";
8
8
  import styles from "./ChatInput.module.css";
9
9
 
10
- interface ModelSelectorProps {
10
+ interface ModelSelectorProps extends ComponentPropsWithoutRef<"button"> {
11
11
  label: string;
12
12
  /** true면 라벨 숨기고 아이콘만 (SingleLine에서 공간 부족할 때). */
13
13
  compact?: boolean;
14
- onClick?: (e: MouseEvent<HTMLButtonElement>) => void;
15
- disabled?: boolean;
16
14
  }
17
15
 
18
- export function ModelSelector({
19
- label,
20
- compact = false,
21
- onClick,
22
- disabled,
23
- }: ModelSelectorProps) {
24
- return (
25
- <button
26
- type="button"
27
- className={styles.modelBtn}
28
- data-compact={compact || undefined}
29
- onClick={onClick}
30
- disabled={disabled}
31
- aria-label={compact ? `모델: ${label}` : undefined}
32
- >
33
- <PhInfinity className={styles.modelLead} weight="regular" aria-hidden />
34
- {!compact ? <span className={styles.modelLabel}>{label}</span> : null}
35
- {!compact ? (
36
- <PhCaretDown className={styles.modelCaret} weight="bold" aria-hidden />
37
- ) : null}
38
- </button>
39
- );
40
- }
16
+ /* forwardRef — DropdownMenu.Trigger asChild 위임을 위해 ref·props 전달 가능해야. */
17
+ export const ModelSelector = forwardRef<HTMLButtonElement, ModelSelectorProps>(
18
+ function ModelSelector({ label, compact = false, ...rest }, ref) {
19
+ return (
20
+ <button
21
+ ref={ref}
22
+ type="button"
23
+ className={styles.modelBtn}
24
+ data-compact={compact || undefined}
25
+ aria-label={compact ? `모델: ${label}` : undefined}
26
+ {...rest}
27
+ >
28
+ <PhInfinity className={styles.modelLead} weight="regular" aria-hidden />
29
+ {!compact ? <span className={styles.modelLabel}>{label}</span> : null}
30
+ {!compact ? (
31
+ <PhCaretDown className={styles.modelCaret} weight="bold" aria-hidden />
32
+ ) : null}
33
+ </button>
34
+ );
35
+ },
36
+ );
@@ -2,7 +2,6 @@ export { ChatInput } from "./ChatInput";
2
2
  export type {
3
3
  ChatInputProps,
4
4
  ChatInputShape,
5
- ChatInputRadius,
6
5
  ChatInputModel,
7
6
  ChatAttachment,
8
7
  ChatAttachmentImage,
@@ -0,0 +1,79 @@
1
+ /* CodeBlock — AIMessage 안의 코드 블록. 상단 언어 라벨 + copy, 본문 pre+code. */
2
+
3
+ .root {
4
+ position: relative;
5
+ background: var(--primary-subtle);
6
+ border-radius: var(--radius-md);
7
+ overflow: hidden;
8
+ width: 100%;
9
+ min-width: 0;
10
+ }
11
+
12
+ .header {
13
+ display: flex;
14
+ align-items: center;
15
+ justify-content: space-between;
16
+ padding: var(--space-2) var(--space-3);
17
+ color: var(--fg-muted);
18
+ }
19
+
20
+ .lang {
21
+ display: inline-flex;
22
+ align-items: center;
23
+ gap: var(--space-1);
24
+ font-family: var(--font-code);
25
+ font-size: var(--text-xs);
26
+ }
27
+ .langIcon {
28
+ width: var(--space-4);
29
+ height: var(--space-4);
30
+ }
31
+ .langLabel {
32
+ line-height: 1;
33
+ }
34
+
35
+ .copyBtn {
36
+ display: inline-flex;
37
+ align-items: center;
38
+ justify-content: center;
39
+ padding: 0;
40
+ border: 0;
41
+ background: var(--bg-muted);
42
+ color: var(--fg-muted);
43
+ border-radius: var(--radius-sm);
44
+ cursor: pointer;
45
+ width: var(--space-6);
46
+ height: var(--space-6);
47
+ transition: var(--transition-colors);
48
+ }
49
+ .copyBtn:hover {
50
+ background: var(--bg);
51
+ color: var(--fg);
52
+ }
53
+ .copyBtn:focus-visible {
54
+ outline: none;
55
+ box-shadow: var(--ring);
56
+ }
57
+ .copyIcon {
58
+ width: 60%;
59
+ height: 60%;
60
+ }
61
+
62
+ /* 코드 본문 — pre + mono font. 가로 스크롤 가능. */
63
+ .code {
64
+ margin: 0;
65
+ padding: var(--space-2) var(--space-4) var(--space-4);
66
+ font-family: var(--font-code);
67
+ font-size: var(--text-sm);
68
+ line-height: 1.6;
69
+ color: var(--fg-strong);
70
+ overflow-x: auto;
71
+ white-space: pre;
72
+ word-wrap: normal;
73
+ }
74
+ .code code {
75
+ font: inherit;
76
+ color: inherit;
77
+ background: transparent;
78
+ padding: 0;
79
+ }
@@ -0,0 +1,71 @@
1
+ "use client";
2
+
3
+ import { useCallback, useState, type HTMLAttributes, type ReactNode } from "react";
4
+ import {
5
+ CodeSimple as PhCodeSimple,
6
+ Copy as PhCopy,
7
+ Check as PhCheck,
8
+ } from "@phosphor-icons/react";
9
+ import { Tooltip } from "@/components/ctds/Tooltip";
10
+ import styles from "./CodeBlock.module.css";
11
+
12
+ export interface CodeBlockProps extends Omit<HTMLAttributes<HTMLDivElement>, "children"> {
13
+ /** 보여줄 코드 문자열. 하이라이팅은 CodeBlock이 따로 하지 않음 — 사용처가 ReactNode(span 조합)로 전달해도 됨. */
14
+ code: string | ReactNode;
15
+ /** 상단 언어 라벨 (예: "html", "tsx"). 없으면 숨김. */
16
+ language?: string;
17
+ /** copy 버튼 클릭 시 호출. 미지정이면 clipboard API로 자동 복사. */
18
+ onCopyCode?: (code: string) => void;
19
+ }
20
+
21
+ export function CodeBlock({
22
+ code,
23
+ language,
24
+ onCopyCode,
25
+ className,
26
+ ...rest
27
+ }: CodeBlockProps) {
28
+ const [copied, setCopied] = useState(false);
29
+ const codeText = typeof code === "string" ? code : "";
30
+
31
+ const handleCopy = useCallback(() => {
32
+ if (onCopyCode) {
33
+ onCopyCode(codeText);
34
+ } else if (typeof navigator !== "undefined" && navigator.clipboard) {
35
+ navigator.clipboard.writeText(codeText).catch(() => {});
36
+ }
37
+ setCopied(true);
38
+ const t = setTimeout(() => setCopied(false), 1500);
39
+ return () => clearTimeout(t);
40
+ }, [codeText, onCopyCode]);
41
+
42
+ const cls = [styles.root, className].filter(Boolean).join(" ");
43
+
44
+ return (
45
+ <div className={cls} {...rest}>
46
+ <div className={styles.header}>
47
+ <div className={styles.lang}>
48
+ <PhCodeSimple className={styles.langIcon} weight="regular" aria-hidden />
49
+ {language ? <span className={styles.langLabel}>{language}</span> : null}
50
+ </div>
51
+ <Tooltip content={copied ? "복사됨" : "복사"}>
52
+ <button
53
+ type="button"
54
+ className={styles.copyBtn}
55
+ onClick={handleCopy}
56
+ aria-label={copied ? "복사됨" : "코드 복사"}
57
+ >
58
+ {copied ? (
59
+ <PhCheck className={styles.copyIcon} weight="bold" />
60
+ ) : (
61
+ <PhCopy className={styles.copyIcon} weight="regular" />
62
+ )}
63
+ </button>
64
+ </Tooltip>
65
+ </div>
66
+ <pre className={styles.code}>
67
+ <code>{code}</code>
68
+ </pre>
69
+ </div>
70
+ );
71
+ }
@@ -0,0 +1,2 @@
1
+ export { CodeBlock } from "./CodeBlock";
2
+ export type { CodeBlockProps } from "./CodeBlock";
@@ -181,6 +181,8 @@ type ItemPassthrough = {
181
181
  leadingActions?: MenuItemAction[];
182
182
  trailingActions?: MenuItemAction[];
183
183
  shortcut?: string;
184
+ /** 라벨 아래 보조 설명 한 줄. MenuItem.description으로 그대로 전달. */
185
+ description?: ReactNode;
184
186
  disabled?: boolean;
185
187
  children?: ReactNode;
186
188
  };
@@ -196,6 +198,7 @@ const Item = forwardRef<HTMLDivElement, ItemProps>(function ContextMenuItem(
196
198
  leadingActions,
197
199
  trailingActions,
198
200
  shortcut,
201
+ description,
199
202
  disabled,
200
203
  children,
201
204
  ...rest
@@ -212,6 +215,7 @@ const Item = forwardRef<HTMLDivElement, ItemProps>(function ContextMenuItem(
212
215
  leadingActions={leadingActions}
213
216
  trailingActions={trailingActions}
214
217
  shortcut={shortcut}
218
+ description={description}
215
219
  disabled={disabled}
216
220
  >
217
221
  {children}
@@ -233,6 +237,7 @@ const CheckboxItem = forwardRef<HTMLDivElement, CheckboxItemProps>(function Cont
233
237
  leadingActions = [],
234
238
  trailingActions,
235
239
  shortcut,
240
+ description,
236
241
  disabled,
237
242
  checked,
238
243
  onCheckedChange,
@@ -276,6 +281,7 @@ const CheckboxItem = forwardRef<HTMLDivElement, CheckboxItemProps>(function Cont
276
281
  leadingActions={composedLeading}
277
282
  trailingActions={trailingActions}
278
283
  shortcut={shortcut}
284
+ description={description}
279
285
  disabled={disabled}
280
286
  >
281
287
  {children}
@@ -297,6 +303,7 @@ const RadioItem = forwardRef<HTMLDivElement, RadioItemProps>(function ContextMen
297
303
  leadingActions = [],
298
304
  trailingActions,
299
305
  shortcut,
306
+ description,
300
307
  disabled,
301
308
  children,
302
309
  ...rest
@@ -322,6 +329,7 @@ const RadioItem = forwardRef<HTMLDivElement, RadioItemProps>(function ContextMen
322
329
  leadingActions={composedLeading}
323
330
  trailingActions={trailingActions}
324
331
  shortcut={shortcut}
332
+ description={description}
325
333
  disabled={disabled}
326
334
  >
327
335
  {children}
@@ -337,7 +345,7 @@ export interface SubTriggerProps
337
345
  Omit<ItemPassthrough, "shortcut"> {}
338
346
 
339
347
  const SubTrigger = forwardRef<HTMLDivElement, SubTriggerProps>(function ContextMenuSubTrigger(
340
- { size, variant, leadingActions, trailingActions, disabled, children, ...rest },
348
+ { size, variant, leadingActions, trailingActions, description, disabled, children, ...rest },
341
349
  ref,
342
350
  ) {
343
351
  const effectiveSize = useMenuSize(size);
@@ -349,6 +357,7 @@ const SubTrigger = forwardRef<HTMLDivElement, SubTriggerProps>(function ContextM
349
357
  variant={variant}
350
358
  leadingActions={leadingActions}
351
359
  trailingActions={trailingActions}
360
+ description={description}
352
361
  hasSubmenu
353
362
  disabled={disabled}
354
363
  >
@@ -0,0 +1,26 @@
1
+ /* DropdownMenu — Radix DropdownMenu 위에 얹은 시각 레이어.
2
+ 표면·분리선·indicator 전부 ContextMenu CSS에서 composes해 재사용 —
3
+ 두 메뉴의 시각이 한 소스에서 흐르도록. 여기선 transform-origin·z-index 등
4
+ dropdown-menu primitive 전용 CSS 변수만 지정. */
5
+
6
+ .content {
7
+ composes: content from "../ContextMenu/ContextMenu.module.css";
8
+ z-index: var(--z-popover);
9
+ transform-origin: var(--radix-dropdown-menu-content-transform-origin);
10
+ }
11
+
12
+ .content[data-radix-dropdown-menu-sub-content] {
13
+ transform-origin: var(--radix-dropdown-menu-sub-content-transform-origin);
14
+ }
15
+
16
+ .separator {
17
+ composes: separator from "../ContextMenu/ContextMenu.module.css";
18
+ }
19
+
20
+ .checkIndicator {
21
+ composes: checkIndicator from "../ContextMenu/ContextMenu.module.css";
22
+ }
23
+
24
+ .radioIndicator {
25
+ composes: radioIndicator from "../ContextMenu/ContextMenu.module.css";
26
+ }
@@ -0,0 +1,367 @@
1
+ "use client";
2
+
3
+ /**
4
+ * DropdownMenu ─ 버튼 클릭으로 열리는 메뉴.
5
+ * Radix `@radix-ui/react-dropdown-menu` 위에 얇게 얹은 compound 컴포넌트.
6
+ *
7
+ * ContextMenu와의 관계
8
+ * - 시각 표면(.surface) · MenuItem 어댑터 패턴 · Checkbox/Radio 인디케이터 전부 동일.
9
+ * - 차이는 "트리거 방식"뿐: ContextMenu는 우클릭, DropdownMenu는 좌클릭/스페이스/엔터.
10
+ * - 두 컴포넌트의 Content·Item·CheckboxItem·RadioItem·SubTrigger API는 1:1 미러.
11
+ *
12
+ * 소비 표면
13
+ * <DropdownMenu>
14
+ * <DropdownMenu.Trigger asChild>
15
+ * <Button>Open</Button>
16
+ * </DropdownMenu.Trigger>
17
+ * <DropdownMenu.Content>
18
+ * <DropdownMenu.Item onSelect={...}>복사</DropdownMenu.Item>
19
+ * <DropdownMenu.Separator />
20
+ * <DropdownMenu.CheckboxItem checked={grid} onCheckedChange={setGrid}>
21
+ * 그리드
22
+ * </DropdownMenu.CheckboxItem>
23
+ * </DropdownMenu.Content>
24
+ * </DropdownMenu>
25
+ */
26
+
27
+ import * as RDM from "@radix-ui/react-dropdown-menu";
28
+ import {
29
+ createContext,
30
+ forwardRef,
31
+ useContext,
32
+ type ComponentPropsWithoutRef,
33
+ type ReactNode,
34
+ } from "react";
35
+
36
+ import { Icon, IconAxisProvider, useIconAxis } from "../Icon";
37
+ import { MenuItem, type MenuItemAction, type MenuItemSize, type MenuItemVariant } from "../MenuItem";
38
+ import styles from "./DropdownMenu.module.css";
39
+
40
+ /* Size context — Content가 하위 Item 기본 size 지정. 개별 아이템 size가 우선. */
41
+ const SizeContext = createContext<MenuItemSize>("md");
42
+ function useMenuSize(override?: MenuItemSize): MenuItemSize {
43
+ const ctx = useContext(SizeContext);
44
+ return override ?? ctx;
45
+ }
46
+
47
+ /* Root · Trigger ─────────────────────────────────────────── */
48
+
49
+ type RootProps = ComponentPropsWithoutRef<typeof RDM.Root>;
50
+ const Root = (props: RootProps) => <RDM.Root {...props} />;
51
+
52
+ type TriggerProps = ComponentPropsWithoutRef<typeof RDM.Trigger>;
53
+ const Trigger = forwardRef<HTMLButtonElement, TriggerProps>(function DropdownMenuTrigger(
54
+ props,
55
+ ref,
56
+ ) {
57
+ return <RDM.Trigger ref={ref} {...props} />;
58
+ });
59
+
60
+ /* Content ─────────────────────────────────────────────────────────── */
61
+
62
+ export interface ContentProps extends ComponentPropsWithoutRef<typeof RDM.Content> {
63
+ size?: MenuItemSize;
64
+ }
65
+
66
+ const Content = forwardRef<HTMLDivElement, ContentProps>(function DropdownMenuContent(
67
+ { className, size = "md", collisionPadding = 8, sideOffset = 6, children, ...rest },
68
+ ref,
69
+ ) {
70
+ const iconAxis = useIconAxis();
71
+ return (
72
+ <RDM.Portal>
73
+ <RDM.Content
74
+ ref={ref}
75
+ className={[styles.content, "m-float", className ?? ""].filter(Boolean).join(" ")}
76
+ collisionPadding={collisionPadding}
77
+ sideOffset={sideOffset}
78
+ data-size={size}
79
+ {...rest}
80
+ >
81
+ <IconAxisProvider value={iconAxis}>
82
+ <SizeContext.Provider value={size}>{children}</SizeContext.Provider>
83
+ </IconAxisProvider>
84
+ </RDM.Content>
85
+ </RDM.Portal>
86
+ );
87
+ });
88
+
89
+ /* SubContent ──────────────────────────────────────────────────────── */
90
+
91
+ export interface SubContentProps extends ComponentPropsWithoutRef<typeof RDM.SubContent> {
92
+ size?: MenuItemSize;
93
+ }
94
+
95
+ const SubContent = forwardRef<HTMLDivElement, SubContentProps>(function DropdownMenuSubContent(
96
+ { className, size, collisionPadding = 8, children, ...rest },
97
+ ref,
98
+ ) {
99
+ const parentSize = useContext(SizeContext);
100
+ const effective = size ?? parentSize;
101
+ const iconAxis = useIconAxis();
102
+ return (
103
+ <RDM.Portal>
104
+ <RDM.SubContent
105
+ ref={ref}
106
+ className={[styles.content, "m-float", className ?? ""].filter(Boolean).join(" ")}
107
+ collisionPadding={collisionPadding}
108
+ data-size={effective}
109
+ {...rest}
110
+ >
111
+ <IconAxisProvider value={iconAxis}>
112
+ <SizeContext.Provider value={effective}>{children}</SizeContext.Provider>
113
+ </IconAxisProvider>
114
+ </RDM.SubContent>
115
+ </RDM.Portal>
116
+ );
117
+ });
118
+
119
+ /* Label ───────────────────────────────────────────────────────────── */
120
+
121
+ const Label = forwardRef<HTMLDivElement, ComponentPropsWithoutRef<typeof RDM.Label>>(
122
+ function DropdownMenuLabel({ className, children, ...rest }, ref) {
123
+ const size = useMenuSize();
124
+ return (
125
+ <RDM.Label ref={ref} asChild {...rest}>
126
+ <MenuItem variant="section" size={size} className={className}>
127
+ {children}
128
+ </MenuItem>
129
+ </RDM.Label>
130
+ );
131
+ },
132
+ );
133
+
134
+ /* Separator ───────────────────────────────────────────────────────── */
135
+
136
+ const Separator = forwardRef<HTMLDivElement, ComponentPropsWithoutRef<typeof RDM.Separator>>(
137
+ function DropdownMenuSeparator({ className, ...rest }, ref) {
138
+ return (
139
+ <RDM.Separator
140
+ ref={ref}
141
+ className={[styles.separator, className ?? ""].filter(Boolean).join(" ")}
142
+ {...rest}
143
+ />
144
+ );
145
+ },
146
+ );
147
+
148
+ /* Group · RadioGroup · Sub ───────────────────────────────────────── */
149
+
150
+ const Group = RDM.Group;
151
+ const RadioGroup = RDM.RadioGroup;
152
+ const Sub = RDM.Sub;
153
+
154
+ /* Item ───────────────────────────────────────────────────────────── */
155
+
156
+ type ItemPassthrough = {
157
+ size?: MenuItemSize;
158
+ variant?: MenuItemVariant;
159
+ leadingActions?: MenuItemAction[];
160
+ trailingActions?: MenuItemAction[];
161
+ shortcut?: string;
162
+ description?: ReactNode;
163
+ disabled?: boolean;
164
+ children?: ReactNode;
165
+ };
166
+
167
+ export interface ItemProps
168
+ extends Omit<ComponentPropsWithoutRef<typeof RDM.Item>, "asChild" | "children">,
169
+ ItemPassthrough {}
170
+
171
+ const Item = forwardRef<HTMLDivElement, ItemProps>(function DropdownMenuItem(
172
+ {
173
+ size,
174
+ variant,
175
+ leadingActions,
176
+ trailingActions,
177
+ shortcut,
178
+ description,
179
+ disabled,
180
+ children,
181
+ ...rest
182
+ },
183
+ ref,
184
+ ) {
185
+ const effectiveSize = useMenuSize(size);
186
+ return (
187
+ <RDM.Item asChild disabled={disabled} {...rest}>
188
+ <MenuItem
189
+ ref={ref}
190
+ size={effectiveSize}
191
+ variant={variant}
192
+ leadingActions={leadingActions}
193
+ trailingActions={trailingActions}
194
+ shortcut={shortcut}
195
+ description={description}
196
+ disabled={disabled}
197
+ >
198
+ {children}
199
+ </MenuItem>
200
+ </RDM.Item>
201
+ );
202
+ });
203
+
204
+ /* CheckboxItem ────────────────────────────────────────────────────── */
205
+
206
+ export interface CheckboxItemProps
207
+ extends Omit<ComponentPropsWithoutRef<typeof RDM.CheckboxItem>, "asChild" | "children">,
208
+ ItemPassthrough {}
209
+
210
+ const CheckboxItem = forwardRef<HTMLDivElement, CheckboxItemProps>(function DropdownMenuCheckboxItem(
211
+ {
212
+ size,
213
+ variant,
214
+ leadingActions = [],
215
+ trailingActions,
216
+ shortcut,
217
+ description,
218
+ disabled,
219
+ checked,
220
+ onCheckedChange,
221
+ children,
222
+ ...rest
223
+ },
224
+ ref,
225
+ ) {
226
+ const effectiveSize = useMenuSize(size);
227
+ const indicatorSlot: MenuItemAction = {
228
+ node: (
229
+ <span
230
+ className={styles.checkIndicator}
231
+ data-checked={checked === true ? "true" : undefined}
232
+ aria-hidden
233
+ >
234
+ <Icon name="check" />
235
+ </span>
236
+ ),
237
+ key: "__checkbox__",
238
+ };
239
+ const composedLeading: MenuItemAction[] = [indicatorSlot, ...leadingActions];
240
+ return (
241
+ <RDM.CheckboxItem
242
+ asChild
243
+ disabled={disabled}
244
+ checked={checked}
245
+ onCheckedChange={onCheckedChange}
246
+ {...rest}
247
+ >
248
+ <MenuItem
249
+ ref={ref}
250
+ size={effectiveSize}
251
+ variant={variant}
252
+ leadingActions={composedLeading}
253
+ trailingActions={trailingActions}
254
+ shortcut={shortcut}
255
+ description={description}
256
+ disabled={disabled}
257
+ >
258
+ {children}
259
+ </MenuItem>
260
+ </RDM.CheckboxItem>
261
+ );
262
+ });
263
+
264
+ /* RadioItem ───────────────────────────────────────────────────────── */
265
+
266
+ export interface RadioItemProps
267
+ extends Omit<ComponentPropsWithoutRef<typeof RDM.RadioItem>, "asChild" | "children">,
268
+ ItemPassthrough {}
269
+
270
+ const RadioItem = forwardRef<HTMLDivElement, RadioItemProps>(function DropdownMenuRadioItem(
271
+ {
272
+ size,
273
+ variant,
274
+ leadingActions = [],
275
+ trailingActions,
276
+ shortcut,
277
+ description,
278
+ disabled,
279
+ children,
280
+ ...rest
281
+ },
282
+ ref,
283
+ ) {
284
+ const effectiveSize = useMenuSize(size);
285
+ const composedLeading: MenuItemAction[] = [
286
+ { node: <span className={styles.radioIndicator} aria-hidden />, key: "__radio__" },
287
+ ...leadingActions,
288
+ ];
289
+ return (
290
+ <RDM.RadioItem asChild disabled={disabled} {...rest}>
291
+ <MenuItem
292
+ ref={ref}
293
+ size={effectiveSize}
294
+ variant={variant}
295
+ leadingActions={composedLeading}
296
+ trailingActions={trailingActions}
297
+ shortcut={shortcut}
298
+ description={description}
299
+ disabled={disabled}
300
+ >
301
+ {children}
302
+ </MenuItem>
303
+ </RDM.RadioItem>
304
+ );
305
+ });
306
+
307
+ /* SubTrigger ──────────────────────────────────────────────────────── */
308
+
309
+ export interface SubTriggerProps
310
+ extends Omit<ComponentPropsWithoutRef<typeof RDM.SubTrigger>, "asChild" | "children">,
311
+ Omit<ItemPassthrough, "shortcut"> {}
312
+
313
+ const SubTrigger = forwardRef<HTMLDivElement, SubTriggerProps>(function DropdownMenuSubTrigger(
314
+ { size, variant, leadingActions, trailingActions, description, disabled, children, ...rest },
315
+ ref,
316
+ ) {
317
+ const effectiveSize = useMenuSize(size);
318
+ return (
319
+ <RDM.SubTrigger asChild disabled={disabled} {...rest}>
320
+ <MenuItem
321
+ ref={ref}
322
+ size={effectiveSize}
323
+ variant={variant}
324
+ leadingActions={leadingActions}
325
+ trailingActions={trailingActions}
326
+ description={description}
327
+ hasSubmenu
328
+ disabled={disabled}
329
+ >
330
+ {children}
331
+ </MenuItem>
332
+ </RDM.SubTrigger>
333
+ );
334
+ });
335
+
336
+ /* Compound export ─────────────────────────────────────────────────── */
337
+
338
+ type DropdownMenuComponent = typeof Root & {
339
+ Trigger: typeof Trigger;
340
+ Content: typeof Content;
341
+ Item: typeof Item;
342
+ CheckboxItem: typeof CheckboxItem;
343
+ RadioGroup: typeof RadioGroup;
344
+ RadioItem: typeof RadioItem;
345
+ Separator: typeof Separator;
346
+ Label: typeof Label;
347
+ Group: typeof Group;
348
+ Sub: typeof Sub;
349
+ SubTrigger: typeof SubTrigger;
350
+ SubContent: typeof SubContent;
351
+ };
352
+
353
+ const DropdownMenu = Root as DropdownMenuComponent;
354
+ DropdownMenu.Trigger = Trigger;
355
+ DropdownMenu.Content = Content;
356
+ DropdownMenu.Item = Item;
357
+ DropdownMenu.CheckboxItem = CheckboxItem;
358
+ DropdownMenu.RadioGroup = RadioGroup;
359
+ DropdownMenu.RadioItem = RadioItem;
360
+ DropdownMenu.Separator = Separator;
361
+ DropdownMenu.Label = Label;
362
+ DropdownMenu.Group = Group;
363
+ DropdownMenu.Sub = Sub;
364
+ DropdownMenu.SubTrigger = SubTrigger;
365
+ DropdownMenu.SubContent = SubContent;
366
+
367
+ export { DropdownMenu };