@codrstudio/openclaude-chat 0.1.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 (171) hide show
  1. package/dist/components/Chat.d.ts +23 -0
  2. package/dist/components/Chat.js +12 -0
  3. package/dist/components/ErrorNote.d.ts +6 -0
  4. package/dist/components/ErrorNote.js +6 -0
  5. package/dist/components/LazyRender.d.ts +8 -0
  6. package/dist/components/LazyRender.js +22 -0
  7. package/dist/components/Markdown.d.ts +5 -0
  8. package/dist/components/Markdown.js +65 -0
  9. package/dist/components/MessageBubble.d.ts +9 -0
  10. package/dist/components/MessageBubble.js +45 -0
  11. package/dist/components/MessageInput.d.ts +19 -0
  12. package/dist/components/MessageInput.js +214 -0
  13. package/dist/components/MessageList.d.ts +13 -0
  14. package/dist/components/MessageList.js +72 -0
  15. package/dist/components/StreamingIndicator.d.ts +1 -0
  16. package/dist/components/StreamingIndicator.js +9 -0
  17. package/dist/display/AlertRenderer.d.ts +2 -0
  18. package/dist/display/AlertRenderer.js +13 -0
  19. package/dist/display/CarouselRenderer.d.ts +2 -0
  20. package/dist/display/CarouselRenderer.js +41 -0
  21. package/dist/display/ChartRenderer.d.ts +2 -0
  22. package/dist/display/ChartRenderer.js +76 -0
  23. package/dist/display/ChoiceButtonsRenderer.d.ts +6 -0
  24. package/dist/display/ChoiceButtonsRenderer.js +23 -0
  25. package/dist/display/CodeBlockRenderer.d.ts +2 -0
  26. package/dist/display/CodeBlockRenderer.js +17 -0
  27. package/dist/display/ComparisonTableRenderer.d.ts +2 -0
  28. package/dist/display/ComparisonTableRenderer.js +26 -0
  29. package/dist/display/DataTableRenderer.d.ts +2 -0
  30. package/dist/display/DataTableRenderer.js +74 -0
  31. package/dist/display/DisplayReactRenderer.d.ts +26 -0
  32. package/dist/display/DisplayReactRenderer.js +192 -0
  33. package/dist/display/FileCardRenderer.d.ts +2 -0
  34. package/dist/display/FileCardRenderer.js +31 -0
  35. package/dist/display/GalleryRenderer.d.ts +2 -0
  36. package/dist/display/GalleryRenderer.js +11 -0
  37. package/dist/display/ImageViewerRenderer.d.ts +2 -0
  38. package/dist/display/ImageViewerRenderer.js +15 -0
  39. package/dist/display/LinkPreviewRenderer.d.ts +2 -0
  40. package/dist/display/LinkPreviewRenderer.js +20 -0
  41. package/dist/display/MapViewRenderer.d.ts +2 -0
  42. package/dist/display/MapViewRenderer.js +20 -0
  43. package/dist/display/MetricCardRenderer.d.ts +2 -0
  44. package/dist/display/MetricCardRenderer.js +12 -0
  45. package/dist/display/PriceHighlightRenderer.d.ts +2 -0
  46. package/dist/display/PriceHighlightRenderer.js +30 -0
  47. package/dist/display/ProductCardRenderer.d.ts +2 -0
  48. package/dist/display/ProductCardRenderer.js +23 -0
  49. package/dist/display/ProgressStepsRenderer.d.ts +2 -0
  50. package/dist/display/ProgressStepsRenderer.js +14 -0
  51. package/dist/display/SourcesListRenderer.d.ts +2 -0
  52. package/dist/display/SourcesListRenderer.js +5 -0
  53. package/dist/display/SpreadsheetRenderer.d.ts +2 -0
  54. package/dist/display/SpreadsheetRenderer.js +32 -0
  55. package/dist/display/StepTimelineRenderer.d.ts +2 -0
  56. package/dist/display/StepTimelineRenderer.js +21 -0
  57. package/dist/display/index.d.ts +21 -0
  58. package/dist/display/index.js +20 -0
  59. package/dist/display/react-sandbox/bootstrap.d.ts +1 -0
  60. package/dist/display/react-sandbox/bootstrap.js +154 -0
  61. package/dist/display/registry.d.ts +5 -0
  62. package/dist/display/registry.js +52 -0
  63. package/dist/display/sdk-types.d.ts +187 -0
  64. package/dist/display/sdk-types.js +4 -0
  65. package/dist/hooks/ChatProvider.d.ts +9 -0
  66. package/dist/hooks/ChatProvider.js +14 -0
  67. package/dist/hooks/useIsMobile.d.ts +1 -0
  68. package/dist/hooks/useIsMobile.js +12 -0
  69. package/dist/hooks/useOpenClaudeChat.d.ts +36 -0
  70. package/dist/hooks/useOpenClaudeChat.js +361 -0
  71. package/dist/index.d.ts +47 -0
  72. package/dist/index.js +42 -0
  73. package/dist/lib/utils.d.ts +2 -0
  74. package/dist/lib/utils.js +5 -0
  75. package/dist/parts/PartErrorBoundary.d.ts +21 -0
  76. package/dist/parts/PartErrorBoundary.js +27 -0
  77. package/dist/parts/PartRenderer.d.ts +8 -0
  78. package/dist/parts/PartRenderer.js +99 -0
  79. package/dist/parts/ReasoningBlock.d.ts +6 -0
  80. package/dist/parts/ReasoningBlock.js +18 -0
  81. package/dist/parts/ToolActivity.d.ts +11 -0
  82. package/dist/parts/ToolActivity.js +52 -0
  83. package/dist/parts/ToolResult.d.ts +7 -0
  84. package/dist/parts/ToolResult.js +38 -0
  85. package/dist/styles.css +2 -0
  86. package/dist/types.d.ts +40 -0
  87. package/dist/types.js +4 -0
  88. package/dist/ui/alert.d.ts +12 -0
  89. package/dist/ui/alert.js +28 -0
  90. package/dist/ui/badge.d.ts +9 -0
  91. package/dist/ui/badge.js +20 -0
  92. package/dist/ui/button.d.ts +11 -0
  93. package/dist/ui/button.js +31 -0
  94. package/dist/ui/card.d.ts +8 -0
  95. package/dist/ui/card.js +21 -0
  96. package/dist/ui/collapsible.d.ts +1 -0
  97. package/dist/ui/collapsible.js +2 -0
  98. package/dist/ui/dialog.d.ts +19 -0
  99. package/dist/ui/dialog.js +23 -0
  100. package/dist/ui/dropdown-menu.d.ts +11 -0
  101. package/dist/ui/dropdown-menu.js +15 -0
  102. package/dist/ui/input.d.ts +3 -0
  103. package/dist/ui/input.js +6 -0
  104. package/dist/ui/progress.d.ts +7 -0
  105. package/dist/ui/progress.js +9 -0
  106. package/dist/ui/scroll-area.d.ts +5 -0
  107. package/dist/ui/scroll-area.js +12 -0
  108. package/dist/ui/separator.d.ts +4 -0
  109. package/dist/ui/separator.js +8 -0
  110. package/dist/ui/skeleton.d.ts +3 -0
  111. package/dist/ui/skeleton.js +6 -0
  112. package/dist/ui/table.d.ts +10 -0
  113. package/dist/ui/table.js +27 -0
  114. package/package.json +61 -0
  115. package/src/components/Chat.tsx +107 -0
  116. package/src/components/ErrorNote.tsx +35 -0
  117. package/src/components/LazyRender.tsx +42 -0
  118. package/src/components/Markdown.tsx +114 -0
  119. package/src/components/MessageBubble.tsx +107 -0
  120. package/src/components/MessageInput.tsx +421 -0
  121. package/src/components/MessageList.tsx +153 -0
  122. package/src/components/StreamingIndicator.tsx +19 -0
  123. package/src/display/AlertRenderer.tsx +23 -0
  124. package/src/display/CarouselRenderer.tsx +141 -0
  125. package/src/display/ChartRenderer.tsx +195 -0
  126. package/src/display/ChoiceButtonsRenderer.tsx +114 -0
  127. package/src/display/CodeBlockRenderer.tsx +49 -0
  128. package/src/display/ComparisonTableRenderer.tsx +132 -0
  129. package/src/display/DataTableRenderer.tsx +144 -0
  130. package/src/display/DisplayReactRenderer.tsx +269 -0
  131. package/src/display/FileCardRenderer.tsx +55 -0
  132. package/src/display/GalleryRenderer.tsx +65 -0
  133. package/src/display/ImageViewerRenderer.tsx +114 -0
  134. package/src/display/LinkPreviewRenderer.tsx +74 -0
  135. package/src/display/MapViewRenderer.tsx +75 -0
  136. package/src/display/MetricCardRenderer.tsx +29 -0
  137. package/src/display/PriceHighlightRenderer.tsx +62 -0
  138. package/src/display/ProductCardRenderer.tsx +112 -0
  139. package/src/display/ProgressStepsRenderer.tsx +59 -0
  140. package/src/display/SourcesListRenderer.tsx +47 -0
  141. package/src/display/SpreadsheetRenderer.tsx +86 -0
  142. package/src/display/StepTimelineRenderer.tsx +75 -0
  143. package/src/display/index.ts +21 -0
  144. package/src/display/react-sandbox/bootstrap.ts +155 -0
  145. package/src/display/registry.ts +84 -0
  146. package/src/display/sdk-types.ts +217 -0
  147. package/src/hooks/ChatProvider.tsx +21 -0
  148. package/src/hooks/useIsMobile.ts +15 -0
  149. package/src/hooks/useOpenClaudeChat.ts +476 -0
  150. package/src/index.ts +76 -0
  151. package/src/lib/utils.ts +6 -0
  152. package/src/parts/PartErrorBoundary.tsx +51 -0
  153. package/src/parts/PartRenderer.tsx +145 -0
  154. package/src/parts/ReasoningBlock.tsx +41 -0
  155. package/src/parts/ToolActivity.tsx +78 -0
  156. package/src/parts/ToolResult.tsx +79 -0
  157. package/src/styles.css +2 -0
  158. package/src/types.ts +41 -0
  159. package/src/ui/alert.tsx +77 -0
  160. package/src/ui/badge.tsx +36 -0
  161. package/src/ui/button.tsx +54 -0
  162. package/src/ui/card.tsx +68 -0
  163. package/src/ui/collapsible.tsx +7 -0
  164. package/src/ui/dialog.tsx +122 -0
  165. package/src/ui/dropdown-menu.tsx +76 -0
  166. package/src/ui/input.tsx +24 -0
  167. package/src/ui/progress.tsx +36 -0
  168. package/src/ui/scroll-area.tsx +48 -0
  169. package/src/ui/separator.tsx +31 -0
  170. package/src/ui/skeleton.tsx +9 -0
  171. package/src/ui/table.tsx +114 -0
@@ -0,0 +1,144 @@
1
+ import { useState } from "react";
2
+ import type { DisplayTable } from "./sdk-types.js";
3
+ import { ArrowUpDown, ArrowUp, ArrowDown } from "lucide-react";
4
+ import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from "../ui/table.js";
5
+ import { ScrollArea, ScrollBar } from "../ui/scroll-area.js";
6
+ import { Button } from "../ui/button.js";
7
+ import { Badge } from "../ui/badge.js";
8
+
9
+ type SortDir = "asc" | "desc";
10
+
11
+ function formatMoney(value: number, currency = "BRL") {
12
+ return new Intl.NumberFormat("pt-BR", { style: "currency", currency }).format(value);
13
+ }
14
+
15
+ function renderCellValue(value: unknown, type: string): React.ReactNode {
16
+ if (value == null) return "—";
17
+ switch (type) {
18
+ case "money":
19
+ return typeof value === "number"
20
+ ? formatMoney(value)
21
+ : typeof value === "object" && value !== null && "value" in value
22
+ ? formatMoney((value as { value: number; currency?: string }).value, (value as { currency?: string }).currency)
23
+ : String(value);
24
+ case "image":
25
+ return typeof value === "string" ? (
26
+ <img src={value} alt="" className="rounded-sm max-h-12 object-cover" loading="lazy" decoding="async" />
27
+ ) : null;
28
+ case "link":
29
+ return typeof value === "string" ? (
30
+ <a href={value} target="_blank" rel="noopener noreferrer" className="text-primary underline underline-offset-2 hover:opacity-80">
31
+ {value}
32
+ </a>
33
+ ) : null;
34
+ case "badge":
35
+ return <Badge variant="secondary">{String(value)}</Badge>;
36
+ default:
37
+ return String(value);
38
+ }
39
+ }
40
+
41
+ function compareValues(a: unknown, b: unknown, type: string): number {
42
+ if (a == null && b == null) return 0;
43
+ if (a == null) return 1;
44
+ if (b == null) return -1;
45
+
46
+ if (type === "money") {
47
+ const av = typeof a === "number" ? a : typeof a === "object" && a !== null && "value" in a ? (a as { value: number }).value : 0;
48
+ const bv = typeof b === "number" ? b : typeof b === "object" && b !== null && "value" in b ? (b as { value: number }).value : 0;
49
+ return av - bv;
50
+ }
51
+ if (type === "number") {
52
+ return (Number(a) || 0) - (Number(b) || 0);
53
+ }
54
+ return String(a).localeCompare(String(b), "pt-BR");
55
+ }
56
+
57
+ export function DataTableRenderer({ title, columns, rows, sortable }: DisplayTable) {
58
+ const [sortKey, setSortKey] = useState<string | null>(null);
59
+ const [sortDir, setSortDir] = useState<SortDir>("asc");
60
+
61
+ function handleSort(key: string) {
62
+ if (!sortable) return;
63
+ if (sortKey === key) {
64
+ setSortDir((d) => (d === "asc" ? "desc" : "asc"));
65
+ } else {
66
+ setSortKey(key);
67
+ setSortDir("asc");
68
+ }
69
+ }
70
+
71
+ const sortedRows = sortKey
72
+ ? [...rows].sort((a, b) => {
73
+ const col = columns.find((c) => c.key === sortKey);
74
+ const dir = sortDir === "asc" ? 1 : -1;
75
+ return compareValues(a[sortKey], b[sortKey], col?.type ?? "text") * dir;
76
+ })
77
+ : rows;
78
+
79
+ return (
80
+ <div className="space-y-2">
81
+ {title && <h3 className="text-sm font-semibold text-foreground">{title}</h3>}
82
+
83
+ <ScrollArea className="w-full">
84
+ <Table>
85
+ <TableHeader>
86
+ <TableRow>
87
+ {columns.map((col) => (
88
+ <TableHead
89
+ key={col.key}
90
+ className={col.align === "right" ? "text-right" : col.align === "center" ? "text-center" : "text-left"}
91
+ >
92
+ {sortable ? (
93
+ <Button
94
+ variant="ghost"
95
+ size="sm"
96
+ className="-ml-3 h-8 font-semibold"
97
+ onClick={() => handleSort(col.key)}
98
+ aria-sort={
99
+ sortKey === col.key
100
+ ? sortDir === "asc"
101
+ ? "ascending"
102
+ : "descending"
103
+ : undefined
104
+ }
105
+ >
106
+ {col.label}
107
+ {sortKey === col.key ? (
108
+ sortDir === "asc" ? (
109
+ <ArrowUp className="ml-1 h-3 w-3" />
110
+ ) : (
111
+ <ArrowDown className="ml-1 h-3 w-3" />
112
+ )
113
+ ) : (
114
+ <ArrowUpDown className="ml-1 h-3 w-3" />
115
+ )}
116
+ </Button>
117
+ ) : (
118
+ col.label
119
+ )}
120
+ </TableHead>
121
+ ))}
122
+ </TableRow>
123
+ </TableHeader>
124
+
125
+ <TableBody>
126
+ {sortedRows.map((row, ri) => (
127
+ <TableRow key={ri}>
128
+ {columns.map((col) => (
129
+ <TableCell
130
+ key={col.key}
131
+ className={col.align === "right" ? "text-right" : col.align === "center" ? "text-center" : "text-left"}
132
+ >
133
+ {renderCellValue(row[col.key], col.type)}
134
+ </TableCell>
135
+ ))}
136
+ </TableRow>
137
+ ))}
138
+ </TableBody>
139
+ </Table>
140
+ <ScrollBar orientation="horizontal" />
141
+ </ScrollArea>
142
+ </div>
143
+ );
144
+ }
@@ -0,0 +1,269 @@
1
+ import { memo, useEffect, useMemo, useRef, useState } from "react"
2
+ import { transform as sucraseTransform } from "sucrase"
3
+ import { SANDBOX_BOOTSTRAP } from "./react-sandbox/bootstrap.js"
4
+
5
+ // ─── Types ────────────────────────────────────────────────────────────────────
6
+
7
+ const WHITELIST_MODULES = [
8
+ "react",
9
+ "react-dom",
10
+ "react-dom/client",
11
+ "framer-motion",
12
+ "recharts",
13
+ "lucide-react",
14
+ ] as const
15
+
16
+ type WhitelistedModule = (typeof WHITELIST_MODULES)[number]
17
+
18
+ interface ImportDecl {
19
+ module: WhitelistedModule
20
+ symbols: string[]
21
+ }
22
+
23
+ interface Layout {
24
+ height?: number | "auto"
25
+ aspectRatio?: string
26
+ maxWidth?: number
27
+ }
28
+
29
+ export interface DisplayReactRendererProps {
30
+ // Flattened DisplayReactSchema payload (PartRenderer spreads args as props)
31
+ version?: "1"
32
+ title?: string
33
+ description?: string
34
+ code: string
35
+ language?: "jsx" | "tsx"
36
+ entry?: "default"
37
+ imports?: ImportDecl[]
38
+ initialProps?: Record<string, unknown>
39
+ layout?: Layout
40
+ theme?: "light" | "dark" | "auto"
41
+ // Sandbox bundle base — consumers can override (default: "/sandbox")
42
+ sandboxBase?: string
43
+ }
44
+
45
+ // ─── Size limits (mirror SDK schema) ──────────────────────────────────────────
46
+
47
+ const MAX_CODE_BYTES = 8 * 1024
48
+ const MAX_PROPS_BYTES = 32 * 1024
49
+
50
+ // ─── Module-level bundle cache ────────────────────────────────────────────────
51
+
52
+ interface SandboxBundles {
53
+ react: string
54
+ reactDom: string
55
+ framerMotion: string
56
+ recharts: string
57
+ lucideReact: string
58
+ }
59
+
60
+ const bundleCache = new Map<string, Promise<SandboxBundles>>()
61
+
62
+ function loadBundles(base: string): Promise<SandboxBundles> {
63
+ const key = base
64
+ const cached = bundleCache.get(key)
65
+ if (cached) return cached
66
+ const p = (async () => {
67
+ const [react, reactDom, framerMotion, recharts, lucideReact] = await Promise.all([
68
+ fetch(`${base}/react.js`).then((r) => r.text()),
69
+ fetch(`${base}/react-dom.js`).then((r) => r.text()),
70
+ fetch(`${base}/framer-motion.js`).then((r) => r.text()),
71
+ fetch(`${base}/recharts.js`).then((r) => r.text()),
72
+ fetch(`${base}/lucide-react.js`).then((r) => r.text()),
73
+ ])
74
+ return { react, reactDom, framerMotion, recharts, lucideReact }
75
+ })()
76
+ bundleCache.set(key, p)
77
+ return p
78
+ }
79
+
80
+ // ─── Validation helpers ──────────────────────────────────────────────────────
81
+
82
+ function byteLength(s: string): number {
83
+ return new TextEncoder().encode(s).length
84
+ }
85
+
86
+ function validatePayload(props: DisplayReactRendererProps): string | null {
87
+ if (!props.code || typeof props.code !== "string") return "missing code"
88
+ if (byteLength(props.code) > MAX_CODE_BYTES) return `code exceeds ${MAX_CODE_BYTES} bytes`
89
+ if (props.initialProps) {
90
+ try {
91
+ const s = JSON.stringify(props.initialProps)
92
+ if (byteLength(s) > MAX_PROPS_BYTES) return `initialProps exceeds ${MAX_PROPS_BYTES} bytes`
93
+ } catch {
94
+ return "initialProps not JSON-serializable"
95
+ }
96
+ }
97
+ if (props.imports) {
98
+ for (const imp of props.imports) {
99
+ if (!WHITELIST_MODULES.includes(imp.module)) {
100
+ return `import not in whitelist: ${imp.module}`
101
+ }
102
+ }
103
+ }
104
+ return null
105
+ }
106
+
107
+ // ─── Transpile user code ─────────────────────────────────────────────────────
108
+
109
+ function compile(code: string, language: "jsx" | "tsx"): { ok: true; code: string } | { ok: false; error: string } {
110
+ try {
111
+ const transforms: ("jsx" | "typescript" | "imports")[] = ["jsx", "imports"]
112
+ if (language === "tsx") transforms.unshift("typescript")
113
+ const out = sucraseTransform(code, {
114
+ transforms,
115
+ production: true,
116
+ jsxRuntime: "classic",
117
+ })
118
+ return { ok: true, code: out.code }
119
+ } catch (e) {
120
+ return { ok: false, error: e instanceof Error ? e.message : String(e) }
121
+ }
122
+ }
123
+
124
+ // ─── Build srcdoc ────────────────────────────────────────────────────────────
125
+
126
+ function buildSrcDoc(bundles: SandboxBundles): string {
127
+ return `<!doctype html><html><head><meta charset="utf-8"><style>
128
+ html,body{margin:0;padding:0;}
129
+ body{font:14px/1.5 system-ui,-apple-system,sans-serif;padding:12px;box-sizing:border-box;}
130
+ #root{min-height:0;}
131
+ *{box-sizing:border-box;}
132
+ </style></head><body><div id="root"></div>
133
+ <script>${bundles.react}<\/script>
134
+ <script>${bundles.reactDom}<\/script>
135
+ <script>${bundles.framerMotion}<\/script>
136
+ <script>${bundles.recharts}<\/script>
137
+ <script>${bundles.lucideReact}<\/script>
138
+ <script>${SANDBOX_BOOTSTRAP}<\/script>
139
+ </body></html>`
140
+ }
141
+
142
+ // ─── Component ───────────────────────────────────────────────────────────────
143
+
144
+ export const DisplayReactRenderer = memo(function DisplayReactRenderer(props: DisplayReactRendererProps) {
145
+ const {
146
+ code,
147
+ language = "jsx",
148
+ initialProps,
149
+ layout,
150
+ theme = "auto",
151
+ title,
152
+ description,
153
+ sandboxBase = "/sandbox",
154
+ } = props
155
+
156
+ const iframeRef = useRef<HTMLIFrameElement | null>(null)
157
+ const [height, setHeight] = useState<number | null>(null)
158
+ const [error, setError] = useState<string | null>(null)
159
+ const [status, setStatus] = useState<"loading" | "ready" | "error">("loading")
160
+
161
+ // Validate + compile once per payload
162
+ const compiled = useMemo<{ ok: true; code: string } | { ok: false; error: string }>(() => {
163
+ const validationError = validatePayload(props)
164
+ if (validationError) return { ok: false, error: validationError }
165
+ return compile(code, language)
166
+ // eslint-disable-next-line react-hooks/exhaustive-deps
167
+ }, [code, language, JSON.stringify(props.imports), JSON.stringify(initialProps)])
168
+
169
+ // Build + inject srcdoc once bundles are available
170
+ useEffect(() => {
171
+ if (compiled.ok === false) {
172
+ setError(compiled.error)
173
+ setStatus("error")
174
+ return
175
+ }
176
+ let cancelled = false
177
+ loadBundles(sandboxBase)
178
+ .then((bundles) => {
179
+ if (cancelled) return
180
+ const iframe = iframeRef.current
181
+ if (!iframe) return
182
+ iframe.srcdoc = buildSrcDoc(bundles)
183
+ })
184
+ .catch((e) => {
185
+ if (cancelled) return
186
+ setError(`failed to load sandbox bundles: ${e instanceof Error ? e.message : String(e)}`)
187
+ setStatus("error")
188
+ })
189
+ return () => {
190
+ cancelled = true
191
+ }
192
+ }, [compiled, sandboxBase])
193
+
194
+ // postMessage pump: wait for sandbox-boot → send compiled code. Also capture height + ready.
195
+ useEffect(() => {
196
+ const handler = (ev: MessageEvent) => {
197
+ const iframe = iframeRef.current
198
+ if (!iframe || ev.source !== iframe.contentWindow) return
199
+ const data = ev.data as { type?: string; height?: number } | undefined
200
+ if (!data || typeof data !== "object" || !data.type) return
201
+ if (data.type === "sandbox-boot") {
202
+ if (!compiled.ok) return
203
+ iframe.contentWindow?.postMessage(
204
+ {
205
+ type: "sandbox-render",
206
+ compiledCode: compiled.code,
207
+ payload: {
208
+ initialProps: initialProps ?? {},
209
+ theme,
210
+ },
211
+ },
212
+ "*",
213
+ )
214
+ } else if (data.type === "sandbox-ready") {
215
+ setStatus("ready")
216
+ } else if (data.type === "sandbox-height" && typeof data.height === "number") {
217
+ if (layout?.height !== "auto" && typeof layout?.height === "number") return
218
+ setHeight(data.height + 24) // padding from body
219
+ }
220
+ }
221
+ window.addEventListener("message", handler)
222
+ return () => window.removeEventListener("message", handler)
223
+ }, [compiled, initialProps, theme, layout?.height])
224
+
225
+ // Compute iframe style
226
+ const iframeStyle: React.CSSProperties = {
227
+ width: "100%",
228
+ maxWidth: layout?.maxWidth ? `${layout.maxWidth}px` : undefined,
229
+ aspectRatio: layout?.aspectRatio,
230
+ height:
231
+ typeof layout?.height === "number"
232
+ ? `${layout.height}px`
233
+ : height !== null
234
+ ? `${height}px`
235
+ : "120px",
236
+ border: "1px solid var(--border, #e5e5e5)",
237
+ borderRadius: "8px",
238
+ display: "block",
239
+ background: "var(--bg, #fff)",
240
+ }
241
+
242
+ if (error || (compiled.ok === false)) {
243
+ const msg = error ?? (compiled.ok === false ? compiled.error : "unknown")
244
+ return (
245
+ <div className="rounded-lg border border-red-300 bg-red-50 text-red-900 p-3 text-xs">
246
+ <div className="font-semibold mb-1">React sandbox error</div>
247
+ <pre className="whitespace-pre-wrap break-words font-mono text-[11px]">{msg}</pre>
248
+ </div>
249
+ )
250
+ }
251
+
252
+ return (
253
+ <div className="flex flex-col gap-1.5">
254
+ {(title || description) && (
255
+ <div className="flex flex-col gap-0.5 px-0.5">
256
+ {title && <div className="text-sm font-semibold text-foreground">{title}</div>}
257
+ {description && <div className="text-xs text-muted-foreground">{description}</div>}
258
+ </div>
259
+ )}
260
+ <iframe
261
+ ref={iframeRef}
262
+ sandbox="allow-scripts"
263
+ title={title ?? "react sandbox"}
264
+ style={iframeStyle}
265
+ aria-busy={status === "loading"}
266
+ />
267
+ </div>
268
+ )
269
+ })
@@ -0,0 +1,55 @@
1
+ import type { DisplayFile } from "./sdk-types.js";
2
+ import {
3
+ Download,
4
+ File,
5
+ FileCode,
6
+ FileImage,
7
+ FileMinus,
8
+ FileText,
9
+ FileVideo,
10
+ Music,
11
+ } from "lucide-react";
12
+ import { Button } from "../ui/button.js";
13
+ import { Card } from "../ui/card.js";
14
+
15
+ function getFileIcon(type: string) {
16
+ const mime = type.toLowerCase();
17
+ if (mime.startsWith("image/")) return FileImage;
18
+ if (mime.startsWith("video/")) return FileVideo;
19
+ if (mime.startsWith("audio/")) return Music;
20
+ if (mime === "application/pdf" || mime === "text/plain" || mime.includes("document")) return FileText;
21
+ if (mime.includes("spreadsheet") || mime.includes("csv")) return FileMinus;
22
+ if (mime.includes("javascript") || mime.includes("typescript") || mime.includes("json") || mime.includes("html") || mime.includes("css") || mime.includes("xml")) return FileCode;
23
+ return File;
24
+ }
25
+
26
+ function formatSize(bytes: number): string {
27
+ if (bytes < 1024) return `${bytes} B`;
28
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
29
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
30
+ }
31
+
32
+ export function FileCardRenderer({ name, type, size, url }: DisplayFile) {
33
+ const Icon = getFileIcon(type);
34
+
35
+ return (
36
+ <Card className="flex items-center gap-3 p-3">
37
+ <div className="shrink-0 text-primary">
38
+ <Icon className="h-8 w-8" />
39
+ </div>
40
+ <div className="flex-1 min-w-0">
41
+ <p className="font-medium text-sm truncate">{name}</p>
42
+ <p className="text-xs text-muted-foreground">
43
+ {type}{size !== undefined && ` · ${formatSize(size)}`}
44
+ </p>
45
+ </div>
46
+ {url && (
47
+ <Button variant="ghost" size="icon" asChild>
48
+ <a href={url} download={name} aria-label={`Baixar ${name}`}>
49
+ <Download className="h-4 w-4" />
50
+ </a>
51
+ </Button>
52
+ )}
53
+ </Card>
54
+ );
55
+ }
@@ -0,0 +1,65 @@
1
+ import { useState } from "react";
2
+ import type { DisplayGallery } from "./sdk-types.js";
3
+ import { ZoomIn } from "lucide-react";
4
+ import { cn } from "../lib/utils.js";
5
+ import { Dialog, DialogContent, DialogTitle } from "../ui/dialog.js";
6
+
7
+ export function GalleryRenderer({ title, images }: DisplayGallery) {
8
+ const [lightboxIdx, setLightboxIdx] = useState<number | null>(null);
9
+
10
+ const activeImage = lightboxIdx !== null ? images[lightboxIdx] : null;
11
+
12
+ return (
13
+ <div className="space-y-2">
14
+ {title && <h3 className="text-sm font-medium text-foreground">{title}</h3>}
15
+
16
+ <div className="grid grid-cols-2 md:grid-cols-3 gap-2">
17
+ {images.map((img, i) => (
18
+ <button
19
+ key={i}
20
+ className="relative group cursor-pointer rounded-md overflow-hidden"
21
+ onClick={() => setLightboxIdx(i)}
22
+ aria-label={img.alt ?? `Imagem ${i + 1}`}
23
+ >
24
+ <img
25
+ src={img.url}
26
+ alt={img.alt ?? ""}
27
+ className="w-full h-full object-cover aspect-square"
28
+ loading="lazy"
29
+ />
30
+ <div className="absolute inset-0 bg-background/50 opacity-0 group-hover:opacity-100 flex items-center justify-center transition-opacity">
31
+ <ZoomIn className="h-5 w-5 text-foreground" />
32
+ </div>
33
+ {img.caption && (
34
+ <p className="absolute bottom-0 left-0 right-0 text-sm text-muted-foreground bg-background/80 px-2 py-1 truncate">
35
+ {img.caption}
36
+ </p>
37
+ )}
38
+ </button>
39
+ ))}
40
+ </div>
41
+
42
+ <Dialog open={lightboxIdx !== null} onOpenChange={(open) => { if (!open) setLightboxIdx(null); }}>
43
+ <DialogContent className={cn("max-w-4xl p-0 bg-background/95")}>
44
+ <DialogTitle className="sr-only">
45
+ {activeImage?.alt ?? "Visualizador de imagem"}
46
+ </DialogTitle>
47
+ {activeImage && (
48
+ <div className="flex flex-col">
49
+ <img
50
+ src={activeImage.url}
51
+ alt={activeImage.alt ?? ""}
52
+ className="w-full max-h-[80vh] object-contain"
53
+ />
54
+ {activeImage.caption && (
55
+ <p className="text-sm text-muted-foreground px-4 py-3">
56
+ {activeImage.caption}
57
+ </p>
58
+ )}
59
+ </div>
60
+ )}
61
+ </DialogContent>
62
+ </Dialog>
63
+ </div>
64
+ );
65
+ }
@@ -0,0 +1,114 @@
1
+ import { useState } from "react";
2
+ import type { DisplayImage } from "./sdk-types.js";
3
+ import { ZoomIn, ZoomOut, RotateCcw, X } from "lucide-react";
4
+ import { Dialog, DialogContent, DialogTitle } from "../ui/dialog.js";
5
+ import { Button } from "../ui/button.js";
6
+ import { cn } from "../lib/utils.js";
7
+
8
+ export function ImageViewerRenderer({ url, alt, caption, width, height }: DisplayImage) {
9
+ const [dialogOpen, setDialogOpen] = useState(false);
10
+ const [zoom, setZoom] = useState(1);
11
+
12
+ function handleOpen() {
13
+ setZoom(1);
14
+ setDialogOpen(true);
15
+ }
16
+
17
+ return (
18
+ <div className="flex flex-col gap-1.5">
19
+ <button
20
+ className="relative group cursor-pointer rounded-md overflow-hidden inline-block"
21
+ onClick={handleOpen}
22
+ aria-label={`Ampliar imagem${alt ? `: ${alt}` : ""}`}
23
+ >
24
+ <img
25
+ src={url}
26
+ alt={alt ?? ""}
27
+ className="block max-w-full rounded-md"
28
+ loading="lazy"
29
+ decoding="async"
30
+ width={width}
31
+ height={height}
32
+ />
33
+ <div className="absolute inset-0 bg-background/50 opacity-0 group-hover:opacity-100 flex items-center justify-center transition-opacity">
34
+ <ZoomIn className="h-5 w-5 text-foreground" />
35
+ </div>
36
+ </button>
37
+
38
+ {caption && (
39
+ <p className="text-xs text-muted-foreground">{caption}</p>
40
+ )}
41
+
42
+ <Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
43
+ <DialogContent className="max-w-4xl p-0 bg-background/95 overflow-hidden">
44
+ <DialogTitle className="sr-only">{alt ?? "Visualizador de imagem"}</DialogTitle>
45
+
46
+ <div className="flex flex-col">
47
+ <div className="flex items-center gap-1 p-2 border-b border-border">
48
+ <Button
49
+ variant="ghost"
50
+ size="icon"
51
+ onClick={() => setZoom((z) => Math.max(0.25, z - 0.25))}
52
+ aria-label="Reduzir zoom"
53
+ disabled={zoom <= 0.25}
54
+ >
55
+ <ZoomOut className="h-4 w-4" />
56
+ </Button>
57
+
58
+ <span className={cn("text-xs text-muted-foreground w-12 text-center tabular-nums")}>
59
+ {Math.round(zoom * 100)}%
60
+ </span>
61
+
62
+ <Button
63
+ variant="ghost"
64
+ size="icon"
65
+ onClick={() => setZoom((z) => Math.min(4, z + 0.25))}
66
+ aria-label="Aumentar zoom"
67
+ disabled={zoom >= 4}
68
+ >
69
+ <ZoomIn className="h-4 w-4" />
70
+ </Button>
71
+
72
+ <Button
73
+ variant="ghost"
74
+ size="icon"
75
+ onClick={() => setZoom(1)}
76
+ aria-label="Resetar zoom"
77
+ >
78
+ <RotateCcw className="h-4 w-4" />
79
+ </Button>
80
+
81
+ <div className="flex-1" />
82
+
83
+ <Button
84
+ variant="ghost"
85
+ size="icon"
86
+ onClick={() => setDialogOpen(false)}
87
+ aria-label="Fechar"
88
+ >
89
+ <X className="h-4 w-4" />
90
+ </Button>
91
+ </div>
92
+
93
+ <div className="overflow-auto max-h-[80vh] flex items-center justify-center p-4">
94
+ <img
95
+ src={url}
96
+ alt={alt ?? ""}
97
+ style={{ transform: `scale(${zoom})`, transformOrigin: "center" }}
98
+ className="max-w-full transition-transform"
99
+ width={width}
100
+ height={height}
101
+ />
102
+ </div>
103
+
104
+ {caption && (
105
+ <p className="text-xs text-muted-foreground text-center p-2 border-t border-border">
106
+ {caption}
107
+ </p>
108
+ )}
109
+ </div>
110
+ </DialogContent>
111
+ </Dialog>
112
+ </div>
113
+ );
114
+ }