@tangle-network/ui 1.0.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 (220) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/LICENSE +21 -0
  3. package/README.md +33 -0
  4. package/dist/active-sessions-store-CeOmXgv5.d.ts +85 -0
  5. package/dist/artifact-pane-DvJyPWV4.d.ts +24 -0
  6. package/dist/auth.d.ts +74 -0
  7. package/dist/auth.js +15 -0
  8. package/dist/button-CMQuQEW_.d.ts +17 -0
  9. package/dist/chat.d.ts +232 -0
  10. package/dist/chat.js +30 -0
  11. package/dist/chunk-2NFQRQOD.js +1009 -0
  12. package/dist/chunk-2VH6PUXD.js +186 -0
  13. package/dist/chunk-34A66VBG.js +214 -0
  14. package/dist/chunk-3OI2QKFD.js +0 -0
  15. package/dist/chunk-4CLN43XT.js +45 -0
  16. package/dist/chunk-54SQQMMM.js +156 -0
  17. package/dist/chunk-5Z5ZYMOJ.js +0 -0
  18. package/dist/chunk-66BNMOVT.js +167 -0
  19. package/dist/chunk-6BGQA4BQ.js +0 -0
  20. package/dist/chunk-7UO2ZMRQ.js +133 -0
  21. package/dist/chunk-BX6AQMUS.js +183 -0
  22. package/dist/chunk-CD53GZOM.js +59 -0
  23. package/dist/chunk-CSAIKY36.js +54 -0
  24. package/dist/chunk-EEE55AVS.js +1201 -0
  25. package/dist/chunk-GYPQXTJU.js +230 -0
  26. package/dist/chunk-HFL6R6IF.js +37 -0
  27. package/dist/chunk-HJKCSXCH.js +737 -0
  28. package/dist/chunk-LISXUB4D.js +1222 -0
  29. package/dist/chunk-LQS34IGP.js +0 -0
  30. package/dist/chunk-MKTSMWVD.js +109 -0
  31. package/dist/chunk-NKDZ7GZE.js +192 -0
  32. package/dist/chunk-OEX7NZE3.js +321 -0
  33. package/dist/chunk-Q56BYXQF.js +61 -0
  34. package/dist/chunk-Q7EIIWTC.js +0 -0
  35. package/dist/chunk-REJESC5U.js +117 -0
  36. package/dist/chunk-RQGKSCEZ.js +0 -0
  37. package/dist/chunk-RQHJBTEU.js +10 -0
  38. package/dist/chunk-TMFOPHHN.js +299 -0
  39. package/dist/chunk-XGKULLYE.js +40 -0
  40. package/dist/chunk-XIHMJ7ZQ.js +614 -0
  41. package/dist/chunk-YJ2G3XO5.js +1048 -0
  42. package/dist/chunk-YNN4O57I.js +754 -0
  43. package/dist/code-block-DjXf8eOG.d.ts +19 -0
  44. package/dist/document-editor-pane-A5LT5H4N.js +12 -0
  45. package/dist/document-editor-pane-DyDEX_Zm.d.ts +124 -0
  46. package/dist/editor.d.ts +120 -0
  47. package/dist/editor.js +34 -0
  48. package/dist/files.d.ts +175 -0
  49. package/dist/files.js +20 -0
  50. package/dist/hooks.d.ts +56 -0
  51. package/dist/hooks.js +41 -0
  52. package/dist/index.d.ts +43 -0
  53. package/dist/index.js +446 -0
  54. package/dist/markdown.d.ts +15 -0
  55. package/dist/markdown.js +14 -0
  56. package/dist/message-BHWbxBtT.d.ts +15 -0
  57. package/dist/openui.d.ts +115 -0
  58. package/dist/openui.js +12 -0
  59. package/dist/parts-dj7AcUg0.d.ts +36 -0
  60. package/dist/primitives.d.ts +332 -0
  61. package/dist/primitives.js +191 -0
  62. package/dist/run-PfLmDAox.d.ts +41 -0
  63. package/dist/run.d.ts +69 -0
  64. package/dist/run.js +36 -0
  65. package/dist/sdk-hooks.d.ts +285 -0
  66. package/dist/sdk-hooks.js +31 -0
  67. package/dist/stores.d.ts +17 -0
  68. package/dist/stores.js +76 -0
  69. package/dist/tool-call-feed-Bs3MyQMT.d.ts +68 -0
  70. package/dist/tool-display-z4JcDmMQ.d.ts +32 -0
  71. package/dist/tool-previews.d.ts +48 -0
  72. package/dist/tool-previews.js +21 -0
  73. package/dist/types.d.ts +19 -0
  74. package/dist/types.js +1 -0
  75. package/dist/utils.d.ts +45 -0
  76. package/dist/utils.js +32 -0
  77. package/package.json +193 -0
  78. package/src/auth/auth.tsx +228 -0
  79. package/src/auth/index.ts +13 -0
  80. package/src/auth/login-layout.tsx +46 -0
  81. package/src/chat/agent-timeline.stories.tsx +429 -0
  82. package/src/chat/agent-timeline.tsx +360 -0
  83. package/src/chat/chat-container.tsx +486 -0
  84. package/src/chat/chat-input.stories.tsx +142 -0
  85. package/src/chat/chat-input.tsx +389 -0
  86. package/src/chat/chat-message.stories.tsx +237 -0
  87. package/src/chat/chat-message.tsx +129 -0
  88. package/src/chat/index.ts +18 -0
  89. package/src/chat/message-list.stories.tsx +336 -0
  90. package/src/chat/message-list.tsx +79 -0
  91. package/src/chat/thinking-indicator.stories.tsx +56 -0
  92. package/src/chat/thinking-indicator.tsx +30 -0
  93. package/src/chat/user-message.stories.tsx +92 -0
  94. package/src/chat/user-message.tsx +43 -0
  95. package/src/editor/document-editor-pane.tsx +351 -0
  96. package/src/editor/editor-provider.tsx +428 -0
  97. package/src/editor/editor-toolbar.tsx +130 -0
  98. package/src/editor/index.ts +31 -0
  99. package/src/editor/markdown-conversion.ts +21 -0
  100. package/src/editor/markdown-document-editor.tsx +137 -0
  101. package/src/editor/tiptap-editor.tsx +331 -0
  102. package/src/editor/use-editor.ts +221 -0
  103. package/src/files/file-artifact-pane.tsx +183 -0
  104. package/src/files/file-preview.tsx +342 -0
  105. package/src/files/file-tabs.tsx +71 -0
  106. package/src/files/file-tree.tsx +258 -0
  107. package/src/files/index.ts +17 -0
  108. package/src/files/rich-file-tree.stories.tsx +104 -0
  109. package/src/files/rich-file-tree.test.tsx +42 -0
  110. package/src/files/rich-file-tree.tsx +232 -0
  111. package/src/hooks/index.ts +10 -0
  112. package/src/hooks/use-auth.ts +153 -0
  113. package/src/hooks/use-auto-scroll.ts +59 -0
  114. package/src/hooks/use-dropdown-menu.ts +40 -0
  115. package/src/hooks/use-live-time.test.tsx +40 -0
  116. package/src/hooks/use-live-time.ts +27 -0
  117. package/src/hooks/use-realtime-session.ts +319 -0
  118. package/src/hooks/use-run-collapse-state.ts +25 -0
  119. package/src/hooks/use-run-groups.ts +111 -0
  120. package/src/hooks/use-sdk-session.ts +575 -0
  121. package/src/hooks/use-sse-stream.ts +475 -0
  122. package/src/hooks/use-tool-call-stream.ts +96 -0
  123. package/src/index.ts +14 -0
  124. package/src/lib/utils.ts +6 -0
  125. package/src/markdown/code-block.tsx +198 -0
  126. package/src/markdown/index.ts +2 -0
  127. package/src/markdown/markdown.stories.tsx +190 -0
  128. package/src/markdown/markdown.tsx +62 -0
  129. package/src/openui/index.ts +20 -0
  130. package/src/openui/openui-artifact-renderer.tsx +542 -0
  131. package/src/primitives/artifact-pane.tsx +91 -0
  132. package/src/primitives/avatar.stories.tsx +95 -0
  133. package/src/primitives/avatar.tsx +47 -0
  134. package/src/primitives/badge.stories.tsx +57 -0
  135. package/src/primitives/badge.tsx +97 -0
  136. package/src/primitives/button.stories.tsx +48 -0
  137. package/src/primitives/button.tsx +115 -0
  138. package/src/primitives/card.stories.tsx +53 -0
  139. package/src/primitives/card.tsx +98 -0
  140. package/src/primitives/code-block.stories.tsx +115 -0
  141. package/src/primitives/code-block.tsx +22 -0
  142. package/src/primitives/design-tokens.stories.tsx +162 -0
  143. package/src/primitives/dialog.stories.tsx +176 -0
  144. package/src/primitives/dialog.tsx +137 -0
  145. package/src/primitives/drop-zone.stories.tsx +123 -0
  146. package/src/primitives/drop-zone.tsx +131 -0
  147. package/src/primitives/dropdown-menu.stories.tsx +122 -0
  148. package/src/primitives/dropdown-menu.tsx +214 -0
  149. package/src/primitives/empty-state.stories.tsx +81 -0
  150. package/src/primitives/empty-state.tsx +40 -0
  151. package/src/primitives/index.ts +118 -0
  152. package/src/primitives/input.stories.tsx +113 -0
  153. package/src/primitives/input.tsx +136 -0
  154. package/src/primitives/label.stories.tsx +84 -0
  155. package/src/primitives/label.tsx +24 -0
  156. package/src/primitives/progress.stories.tsx +93 -0
  157. package/src/primitives/progress.tsx +50 -0
  158. package/src/primitives/segmented-control.test.tsx +328 -0
  159. package/src/primitives/segmented-control.tsx +154 -0
  160. package/src/primitives/select.stories.tsx +164 -0
  161. package/src/primitives/select.tsx +158 -0
  162. package/src/primitives/sidebar-drop-zone.stories.tsx +100 -0
  163. package/src/primitives/sidebar-drop-zone.tsx +149 -0
  164. package/src/primitives/skeleton.stories.tsx +79 -0
  165. package/src/primitives/skeleton.tsx +55 -0
  166. package/src/primitives/stat-card.stories.tsx +137 -0
  167. package/src/primitives/stat-card.tsx +97 -0
  168. package/src/primitives/switch.stories.tsx +85 -0
  169. package/src/primitives/switch.tsx +28 -0
  170. package/src/primitives/table.stories.tsx +170 -0
  171. package/src/primitives/table.tsx +116 -0
  172. package/src/primitives/tabs.stories.tsx +180 -0
  173. package/src/primitives/tabs.tsx +71 -0
  174. package/src/primitives/terminal-display.stories.tsx +191 -0
  175. package/src/primitives/terminal-display.tsx +189 -0
  176. package/src/primitives/theme-toggle.stories.tsx +32 -0
  177. package/src/primitives/theme-toggle.tsx +96 -0
  178. package/src/primitives/toast.stories.tsx +155 -0
  179. package/src/primitives/toast.tsx +190 -0
  180. package/src/primitives/upload-progress.stories.tsx +120 -0
  181. package/src/primitives/upload-progress.tsx +110 -0
  182. package/src/run/expanded-tool-detail.stories.tsx +182 -0
  183. package/src/run/expanded-tool-detail.tsx +186 -0
  184. package/src/run/index.ts +13 -0
  185. package/src/run/inline-thinking-item.stories.tsx +136 -0
  186. package/src/run/inline-thinking-item.tsx +120 -0
  187. package/src/run/inline-tool-item.stories.tsx +222 -0
  188. package/src/run/inline-tool-item.tsx +190 -0
  189. package/src/run/run-group.stories.tsx +322 -0
  190. package/src/run/run-group.tsx +569 -0
  191. package/src/run/run-item-primitives.tsx +17 -0
  192. package/src/run/tool-call-feed.stories.tsx +294 -0
  193. package/src/run/tool-call-feed.tsx +192 -0
  194. package/src/run/tool-call-step.stories.tsx +198 -0
  195. package/src/run/tool-call-step.tsx +240 -0
  196. package/src/sdk-hooks.ts +38 -0
  197. package/src/stores/active-sessions-store.ts +455 -0
  198. package/src/stores/chat-store.ts +43 -0
  199. package/src/stores/index.ts +2 -0
  200. package/src/tool-previews/command-preview.tsx +116 -0
  201. package/src/tool-previews/diff-preview.tsx +85 -0
  202. package/src/tool-previews/glob-results-preview.tsx +98 -0
  203. package/src/tool-previews/grep-results-preview.tsx +157 -0
  204. package/src/tool-previews/index.ts +22 -0
  205. package/src/tool-previews/preview-primitives.tsx +84 -0
  206. package/src/tool-previews/question-preview.tsx +101 -0
  207. package/src/tool-previews/web-search-preview.tsx +117 -0
  208. package/src/tool-previews/write-file-preview.tsx +80 -0
  209. package/src/types/branding.ts +11 -0
  210. package/src/types/index.ts +5 -0
  211. package/src/types/message.ts +13 -0
  212. package/src/types/parts.ts +51 -0
  213. package/src/types/run.ts +56 -0
  214. package/src/types/tool-display.ts +41 -0
  215. package/src/utils/copy-text.ts +30 -0
  216. package/src/utils/format.test.ts +43 -0
  217. package/src/utils/format.ts +56 -0
  218. package/src/utils/index.ts +10 -0
  219. package/src/utils/time-ago.ts +9 -0
  220. package/src/utils/tool-display.ts +238 -0
@@ -0,0 +1,198 @@
1
+ import {
2
+ memo,
3
+ useCallback,
4
+ useEffect,
5
+ useRef,
6
+ useState,
7
+ type HTMLAttributes,
8
+ type ReactNode,
9
+ } from "react";
10
+ import SyntaxHighlighter from "react-syntax-highlighter";
11
+ import { Check, Copy } from "lucide-react";
12
+ import { cn } from "../lib/utils";
13
+
14
+ // Theme-aware syntax highlighting — reads CSS custom properties at render time.
15
+ // Override --syntax-* tokens in tokens.css per theme.
16
+ function getSyntaxTheme(): { [key: string]: React.CSSProperties } {
17
+ const el = typeof document !== "undefined" ? document.documentElement : null;
18
+ const v = (name: string, fallback: string) =>
19
+ el ? getComputedStyle(el).getPropertyValue(name).trim() || fallback : fallback;
20
+
21
+ const comment = v("--syntax-comment", "#6B7094");
22
+ const keyword = v("--syntax-keyword", "#A78FFF");
23
+ const string = v("--syntax-string", "#10b981");
24
+ const fn = v("--syntax-function", "#6D9FFF");
25
+ const number = v("--syntax-number", "#FFB347");
26
+ const meta = v("--syntax-meta", "#8263FF");
27
+ const error = v("--syntax-error", "#FF4D6D");
28
+ const variable = v("--syntax-variable", "#C4C0D8");
29
+ const fg = v("--syntax-foreground", "#E8E6F6");
30
+
31
+ return {
32
+ "hljs-comment": { color: comment, fontStyle: "italic" },
33
+ "hljs-quote": { color: comment, fontStyle: "italic" },
34
+ "hljs-doctag": { color: comment },
35
+ "hljs-keyword": { color: keyword },
36
+ "hljs-selector-tag": { color: keyword },
37
+ "hljs-literal": { color: keyword },
38
+ "hljs-type": { color: keyword },
39
+ "hljs-class": { color: keyword },
40
+ "hljs-string": { color: string },
41
+ "hljs-template-tag": { color: string },
42
+ "hljs-template-variable": { color: string },
43
+ "hljs-addition": { color: string },
44
+ "hljs-regexp": { color: string },
45
+ "hljs-title": { color: fn },
46
+ "hljs-section": { color: fn },
47
+ "hljs-built_in": { color: fn },
48
+ "hljs-name": { color: fn },
49
+ "hljs-function": { color: fn },
50
+ "hljs-selector-id": { color: fn },
51
+ "hljs-selector-class": { color: fn },
52
+ "hljs-attribute": { color: fn },
53
+ "hljs-number": { color: number },
54
+ "hljs-symbol": { color: number },
55
+ "hljs-bullet": { color: number },
56
+ "hljs-link": { color: number, textDecoration: "underline" },
57
+ "hljs-meta": { color: meta },
58
+ "hljs-selector-pseudo": { color: meta },
59
+ "hljs-deletion": { color: error },
60
+ "hljs-params": { color: variable },
61
+ "hljs-variable": { color: variable },
62
+ "hljs-tag": { color: variable },
63
+ "hljs-attr": { color: variable },
64
+ "hljs-subst": { color: variable },
65
+ "hljs-strong": { fontWeight: "bold" },
66
+ "hljs-emphasis": { fontStyle: "italic" },
67
+ "hljs": { color: fg, background: "transparent" },
68
+ };
69
+ }
70
+
71
+ // tangleLight removed — getSyntaxTheme() reads --syntax-* CSS vars which are overridden
72
+ // per theme in tokens.css (vault, dawn themes set light values).
73
+
74
+ export interface CodeBlockProps extends HTMLAttributes<HTMLDivElement> {
75
+ code: string;
76
+ language?: string;
77
+ showLineNumbers?: boolean;
78
+ /** Force light theme; defaults to dark */
79
+ light?: boolean;
80
+ children?: ReactNode;
81
+ }
82
+
83
+ const LIGHT_THEMES = new Set(["vault", "dawn"]);
84
+
85
+ function detectLightTheme() {
86
+ return (
87
+ typeof document !== "undefined" &&
88
+ LIGHT_THEMES.has(document.documentElement.getAttribute("data-sandbox-theme") ?? "")
89
+ );
90
+ }
91
+
92
+ function useIsLightTheme(): boolean {
93
+ const [isLight, setIsLight] = useState(detectLightTheme);
94
+
95
+ useEffect(() => {
96
+ if (typeof document === "undefined") return;
97
+ setIsLight(detectLightTheme());
98
+ const observer = new MutationObserver(() => setIsLight(detectLightTheme()));
99
+ observer.observe(document.documentElement, {
100
+ attributes: true,
101
+ attributeFilter: ["data-sandbox-theme"],
102
+ });
103
+ return () => observer.disconnect();
104
+ }, []);
105
+
106
+ return isLight;
107
+ }
108
+
109
+ export const CodeBlock = memo(
110
+ ({ code, language, showLineNumbers = false, light: lightProp, className, children, ...props }: CodeBlockProps) => {
111
+ const isLight = useIsLightTheme();
112
+ const light = lightProp ?? isLight;
113
+ const theme = getSyntaxTheme();
114
+ const bg = "bg-card border-border";
115
+ const headerBg = light ? "bg-muted/50 border-border" : "bg-background border-border";
116
+ const langColor = "text-muted-foreground";
117
+
118
+ return (
119
+ <div
120
+ className={cn("group relative overflow-hidden rounded-lg border font-mono", bg, className)}
121
+ {...props}
122
+ >
123
+ {language && (
124
+ <div className={cn("flex items-center justify-between border-b px-3 py-1", headerBg)}>
125
+ <span className={cn("text-[calc(var(--font-size-xs)-1px)] font-mono font-medium uppercase tracking-widest", langColor)}>
126
+ {language}
127
+ </span>
128
+ {children}
129
+ </div>
130
+ )}
131
+ {!language && children && (
132
+ <div className="absolute right-2 top-2 z-10 flex items-center gap-2 opacity-0 transition-opacity group-hover:opacity-100">
133
+ {children}
134
+ </div>
135
+ )}
136
+ <SyntaxHighlighter
137
+ language={language ?? "text"}
138
+ style={theme}
139
+ showLineNumbers={showLineNumbers}
140
+ lineNumberStyle={{
141
+ color: light ? "#8B92B8" : "#4A4D6A",
142
+ minWidth: "2.5em",
143
+ paddingRight: "1em",
144
+ }}
145
+ customStyle={{
146
+ margin: 0,
147
+ padding: "var(--code-padding-y, 0.625rem) var(--code-padding-x, 0.75rem)",
148
+ background: "transparent",
149
+ fontSize: "var(--code-font-size, 0.8125rem)",
150
+ lineHeight: "var(--code-line-height, 1.5)",
151
+ overflowX: "auto",
152
+ }}
153
+ codeTagProps={{ style: { fontFamily: "var(--font-mono, 'JetBrains Mono', ui-monospace, monospace)" } }}
154
+ wrapLines={false}
155
+ >
156
+ {code}
157
+ </SyntaxHighlighter>
158
+ </div>
159
+ );
160
+ },
161
+ );
162
+ CodeBlock.displayName = "CodeBlock";
163
+
164
+ /** Copy-to-clipboard button for use inside CodeBlock. */
165
+ export const CopyButton = memo(({ text }: { text: string }) => {
166
+ const [copied, setCopied] = useState(false);
167
+ const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
168
+
169
+ useEffect(() => {
170
+ return () => { if (timerRef.current) clearTimeout(timerRef.current); };
171
+ }, []);
172
+
173
+ const handleCopy = useCallback(async () => {
174
+ try {
175
+ await navigator.clipboard.writeText(text);
176
+ setCopied(true);
177
+ if (timerRef.current) clearTimeout(timerRef.current);
178
+ timerRef.current = setTimeout(() => setCopied(false), 2000);
179
+ } catch (err) {
180
+ console.warn("Clipboard write failed:", err);
181
+ }
182
+ }, [text]);
183
+
184
+ return (
185
+ <button
186
+ onClick={handleCopy}
187
+ className="flex items-center justify-center w-6 h-6 rounded-md bg-muted border border-border hover:border-primary/20 transition-colors"
188
+ title="Copy to clipboard"
189
+ >
190
+ {copied ? (
191
+ <Check className="w-3.5 h-3.5 text-emerald-500" />
192
+ ) : (
193
+ <Copy className="w-3.5 h-3.5 text-muted-foreground" />
194
+ )}
195
+ </button>
196
+ );
197
+ });
198
+ CopyButton.displayName = "CopyButton";
@@ -0,0 +1,2 @@
1
+ export { Markdown, type MarkdownProps } from "./markdown";
2
+ export { CodeBlock, CopyButton, type CodeBlockProps } from "./code-block";
@@ -0,0 +1,190 @@
1
+ import type { Meta, StoryObj } from '@storybook/react'
2
+ import { Markdown } from './markdown'
3
+ import { CodeBlock, CopyButton } from './code-block'
4
+
5
+ // ─── Markdown ────────────────────────────────────────────────────────────────
6
+
7
+ const markdownMeta: Meta<typeof Markdown> = {
8
+ title: 'Markdown/Markdown',
9
+ component: Markdown,
10
+ parameters: { layout: 'centered', backgrounds: { default: 'dark' } },
11
+ decorators: [
12
+ (Story) => (
13
+ <div className="w-[680px] p-6 rounded-xl bg-card text-foreground">
14
+ <Story />
15
+ </div>
16
+ ),
17
+ ],
18
+ }
19
+
20
+ export default markdownMeta
21
+ type Story = StoryObj<typeof Markdown>
22
+
23
+ export const Prose: Story = {
24
+ name: 'Prose',
25
+ args: {
26
+ children: `# Getting Started
27
+
28
+ Welcome to **sandbox-ui**. This library provides a set of composable components
29
+ for building agent-powered interfaces.
30
+
31
+ ## Installation
32
+
33
+ \`\`\`bash
34
+ pnpm add @tangle/sandbox-ui
35
+ \`\`\`
36
+
37
+ ## Usage
38
+
39
+ Import the components you need:
40
+
41
+ \`\`\`tsx
42
+ import { ChatInput, DropZone, UploadProgress } from '@tangle/sandbox-ui'
43
+ \`\`\`
44
+
45
+ > **Note:** Components assume a Tailwind CSS v4 setup with the design tokens
46
+ > provided by the sandbox theme.
47
+
48
+ ## Features
49
+
50
+ - Composable chat input with drag-and-drop
51
+ - File upload progress indicators
52
+ - Full-window and sidebar drop zones
53
+ - Dashboard primitives for sandbox monitoring
54
+ `,
55
+ },
56
+ }
57
+
58
+ export const InlineCode: Story = {
59
+ name: 'Inline code',
60
+ args: {
61
+ children: `Use the \`onDrop\` prop to handle dropped files. The \`accept\` prop takes a comma-separated
62
+ list of extensions like \`.pdf,.csv,.xlsx\`. When \`disabled\` is \`true\`, all drag events are ignored.`,
63
+ },
64
+ }
65
+
66
+ export const Table: Story = {
67
+ name: 'GFM table',
68
+ args: {
69
+ children: `| Prop | Type | Default | Description |
70
+ |------|------|---------|-------------|
71
+ | \`onDrop\` | \`(files: File[]) => void\` | — | Drop handler |
72
+ | \`accept\` | \`string\` | \`undefined\` | Accepted extensions |
73
+ | \`disabled\` | \`boolean\` | \`false\` | Disable drop zone |
74
+ | \`title\` | \`string\` | \`"Drop files to upload"\` | Overlay title |
75
+ `,
76
+ },
77
+ }
78
+
79
+ export const CodeFences: Story = {
80
+ name: 'Code fences (multiple languages)',
81
+ args: {
82
+ children: `### TypeScript
83
+
84
+ \`\`\`typescript
85
+ interface DropZoneProps {
86
+ onDrop: (files: File[]) => void
87
+ accept?: string
88
+ disabled?: boolean
89
+ children: ReactNode
90
+ }
91
+ \`\`\`
92
+
93
+ ### Shell
94
+
95
+ \`\`\`bash
96
+ pnpm build && pnpm storybook
97
+ \`\`\`
98
+
99
+ ### JSON
100
+
101
+ \`\`\`json
102
+ {
103
+ "name": "@tangle/sandbox-ui",
104
+ "version": "0.4.0",
105
+ "type": "module"
106
+ }
107
+ \`\`\`
108
+ `,
109
+ },
110
+ }
111
+
112
+ export const ListsAndTaskItems: Story = {
113
+ name: 'Lists & task items',
114
+ args: {
115
+ children: `## Checklist
116
+
117
+ - [x] Drop zone overlay
118
+ - [x] Sidebar drop zone
119
+ - [x] Upload progress indicators
120
+ - [ ] Resumable uploads
121
+ - [ ] Multi-part S3 upload
122
+
123
+ ## Unordered list
124
+
125
+ - Item one
126
+ - Item two
127
+ - Nested item
128
+ - Another nested item
129
+ - Item three
130
+
131
+ ## Ordered list
132
+
133
+ 1. Install dependencies
134
+ 2. Configure theme tokens
135
+ 3. Wrap your app with providers
136
+ 4. Import components
137
+ `,
138
+ },
139
+ }
140
+
141
+ export const BlockquoteAndHR: Story = {
142
+ name: 'Blockquote & HR',
143
+ args: {
144
+ children: `> This component is designed to be a drop-in replacement for any file upload flow.
145
+ > It handles edge cases like nested drag events, accept filtering, and disabled states.
146
+
147
+ ---
148
+
149
+ Regular paragraph after a horizontal rule.
150
+ `,
151
+ },
152
+ }
153
+
154
+ // ─── CodeBlock ───────────────────────────────────────────────────────────────
155
+
156
+ export const CodeBlockOnly: Story = {
157
+ name: 'CodeBlock standalone',
158
+ render: () => (
159
+ <div className="w-[680px] p-6 rounded-xl bg-card space-y-4">
160
+ <CodeBlock code={`import { DropZone } from '@tangle/sandbox-ui'\n\nfunction App() {\n return (\n <DropZone onDrop={files => upload(files)}>\n <Dashboard />\n </DropZone>\n )\n}`} language="tsx">
161
+ <CopyButton text="import { DropZone } from '@tangle/sandbox-ui'" />
162
+ </CodeBlock>
163
+ <CodeBlock code={`pnpm add @tangle/sandbox-ui`} language="bash">
164
+ <CopyButton text="pnpm add @tangle/sandbox-ui" />
165
+ </CodeBlock>
166
+ </div>
167
+ ),
168
+ }
169
+
170
+ export const CodeBlockNoLanguage: Story = {
171
+ name: 'CodeBlock — no language',
172
+ render: () => (
173
+ <div className="w-[680px] p-6 rounded-xl bg-card">
174
+ <CodeBlock code="DROP TABLE users; -- don't run this" />
175
+ </div>
176
+ ),
177
+ }
178
+
179
+ export const CodeBlockLight: Story = {
180
+ name: 'CodeBlock — light theme',
181
+ parameters: { backgrounds: { default: 'light' } },
182
+ render: () => (
183
+ <div className="w-[680px] p-6 rounded-xl bg-white space-y-4">
184
+ <CodeBlock light code={`interface SandboxConfig {\n model: string\n timeout: number\n env: Record<string, string>\n}\n\n// Create a new sandbox\nconst sandbox = await Sandbox.create(config)`} language="typescript">
185
+ <CopyButton text="interface SandboxConfig" />
186
+ </CodeBlock>
187
+ <CodeBlock light code={`pnpm add @tangle-network/sandbox-ui`} language="bash" />
188
+ </div>
189
+ ),
190
+ }
@@ -0,0 +1,62 @@
1
+ import { memo } from "react";
2
+ import ReactMarkdown from "react-markdown";
3
+ import remarkGfm from "remark-gfm";
4
+ import rehypeSanitize from "rehype-sanitize";
5
+ import { CodeBlock, CopyButton } from "./code-block";
6
+ import { cn } from "../lib/utils";
7
+
8
+ export interface MarkdownProps {
9
+ children: string;
10
+ className?: string;
11
+ }
12
+
13
+ /**
14
+ * Renders Markdown content with GFM support, XSS sanitisation, and
15
+ * custom code block rendering via our CodeBlock component.
16
+ */
17
+ export const Markdown = memo(({ children, className }: MarkdownProps) => {
18
+ return (
19
+ <div
20
+ className={cn("prose prose-sm dark:prose-invert max-w-none", className)}
21
+ >
22
+ <ReactMarkdown
23
+ remarkPlugins={[remarkGfm]}
24
+ rehypePlugins={[rehypeSanitize]}
25
+ components={{
26
+ pre({ children: preChildren }) {
27
+ return <>{preChildren}</>;
28
+ },
29
+ code({ className: codeClass, children: codeChildren, ...rest }) {
30
+ const match = /language-(\w+)/.exec(codeClass || "");
31
+ const language = match?.[1];
32
+ const code = String(codeChildren).replace(/\n$/, "");
33
+
34
+ // Inline code (no language fence)
35
+ if (!language && !code.includes("\n")) {
36
+ return (
37
+ <code
38
+ className={cn(
39
+ "px-1.5 py-0.5 rounded border border-border bg-card text-[var(--code-keyword)] text-[0.85em] font-mono",
40
+ codeClass,
41
+ )}
42
+ {...rest}
43
+ >
44
+ {codeChildren}
45
+ </code>
46
+ );
47
+ }
48
+
49
+ return (
50
+ <CodeBlock code={code} language={language}>
51
+ <CopyButton text={code} />
52
+ </CodeBlock>
53
+ );
54
+ },
55
+ }}
56
+ >
57
+ {children}
58
+ </ReactMarkdown>
59
+ </div>
60
+ );
61
+ });
62
+ Markdown.displayName = "Markdown";
@@ -0,0 +1,20 @@
1
+ export {
2
+ OpenUIArtifactRenderer,
3
+ type OpenUIArtifactRendererProps,
4
+ type OpenUIAction,
5
+ type OpenUIPrimitive,
6
+ type OpenUIComponentNode,
7
+ type OpenUIHeadingNode,
8
+ type OpenUITextNode,
9
+ type OpenUIBadgeNode,
10
+ type OpenUIStatNode,
11
+ type OpenUIKeyValueNode,
12
+ type OpenUICodeNode,
13
+ type OpenUIMarkdownNode,
14
+ type OpenUITableNode,
15
+ type OpenUIActionsNode,
16
+ type OpenUISeparatorNode,
17
+ type OpenUIStackNode,
18
+ type OpenUIGridNode,
19
+ type OpenUICardNode,
20
+ } from "./openui-artifact-renderer";