@rimelight/cms 0.0.1

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 (87) hide show
  1. package/README.md +19 -0
  2. package/package.json +69 -0
  3. package/src/astro/BlockRenderer.astro +111 -0
  4. package/src/astro/PageRenderer.astro +23 -0
  5. package/src/astro/blocks/CalloutBlock.astro +78 -0
  6. package/src/astro/blocks/CardBlock.astro +43 -0
  7. package/src/astro/blocks/CardsBlock.astro +24 -0
  8. package/src/astro/blocks/CodeBlock.astro +16 -0
  9. package/src/astro/blocks/DialogueBlock.astro +23 -0
  10. package/src/astro/blocks/FileTreeBlock.astro +45 -0
  11. package/src/astro/blocks/ImageBlock.astro +17 -0
  12. package/src/astro/blocks/OrderedListBlock.astro +55 -0
  13. package/src/astro/blocks/ParagraphBlock.astro +65 -0
  14. package/src/astro/blocks/SceneBlock.astro +65 -0
  15. package/src/astro/blocks/ScriptBlock.astro +59 -0
  16. package/src/astro/blocks/SectionBlock.astro +46 -0
  17. package/src/astro/blocks/StepItemBlock.astro +25 -0
  18. package/src/astro/blocks/StepsBlock.astro +14 -0
  19. package/src/astro/blocks/TabItemBlock.astro +19 -0
  20. package/src/astro/blocks/TableBlock.astro +49 -0
  21. package/src/astro/blocks/TabsBlock.astro +67 -0
  22. package/src/astro/blocks/UnorderedListBlock.astro +55 -0
  23. package/src/auth/editor-guards.ts +148 -0
  24. package/src/auth/filter-blocks.ts +44 -0
  25. package/src/auth/filter-templates.ts +81 -0
  26. package/src/auth/permissions.ts +40 -0
  27. package/src/cache/purge.ts +37 -0
  28. package/src/client/components/RimelightBlock.tsx +487 -0
  29. package/src/client/components/RimelightBlockPicker.tsx +305 -0
  30. package/src/client/components/RimelightEditor.tsx +131 -0
  31. package/src/client/components/RimelightEditorLayout.tsx +143 -0
  32. package/src/client/components/RimelightPreview.tsx +354 -0
  33. package/src/client/components/RimelightPropertiesPanel.tsx +408 -0
  34. package/src/client/components/RimelightTemplateEditor.tsx +245 -0
  35. package/src/client/components/editors/CalloutEditor.tsx +185 -0
  36. package/src/client/components/editors/CardEditor.tsx +60 -0
  37. package/src/client/components/editors/CardsEditor.tsx +132 -0
  38. package/src/client/components/editors/CodeEditor.tsx +101 -0
  39. package/src/client/components/editors/DialogueEditor.tsx +52 -0
  40. package/src/client/components/editors/FileTreeEditor.tsx +65 -0
  41. package/src/client/components/editors/ImageEditor.tsx +70 -0
  42. package/src/client/components/editors/ParagraphEditor.tsx +314 -0
  43. package/src/client/components/editors/SceneEditor.tsx +245 -0
  44. package/src/client/components/editors/ScriptEditor.tsx +245 -0
  45. package/src/client/components/editors/SectionEditor.tsx +139 -0
  46. package/src/client/components/editors/StepItemEditor.tsx +117 -0
  47. package/src/client/components/editors/StepsEditor.tsx +105 -0
  48. package/src/client/components/editors/TabItemEditor.tsx +113 -0
  49. package/src/client/components/editors/TableEditor.tsx +181 -0
  50. package/src/client/components/editors/TabsEditor.tsx +105 -0
  51. package/src/client/components/index.ts +7 -0
  52. package/src/client/loader.ts +19 -0
  53. package/src/client/state/autosave.ts +122 -0
  54. package/src/client/state/editor-store.ts +411 -0
  55. package/src/client/state/history.ts +251 -0
  56. package/src/client/state/lock-manager.ts +89 -0
  57. package/src/client/styles/cms-editor.css +653 -0
  58. package/src/client/toast.ts +12 -0
  59. package/src/client/utils/sortable.ts +184 -0
  60. package/src/client/utils/splitpane.ts +87 -0
  61. package/src/client/utils/tree-sanitizer.ts +33 -0
  62. package/src/core/page-definitions.ts +502 -0
  63. package/src/core/registry.ts +99 -0
  64. package/src/core/template-sync.ts +75 -0
  65. package/src/core/types/blocks.ts +367 -0
  66. package/src/core/types/inline.ts +23 -0
  67. package/src/core/types/pages.ts +36 -0
  68. package/src/core/types/templates.ts +32 -0
  69. package/src/core/types/versioning.ts +44 -0
  70. package/src/core/validator.ts +28 -0
  71. package/src/env.d.ts +4 -0
  72. package/src/index.ts +47 -0
  73. package/src/markdown/index.ts +2 -0
  74. package/src/markdown/parser.ts +347 -0
  75. package/src/markdown/serializer.ts +219 -0
  76. package/src/migration.ts +155 -0
  77. package/src/schema/index.ts +8 -0
  78. package/src/schema/page_draft_locks.ts +13 -0
  79. package/src/schema/page_drafts.ts +18 -0
  80. package/src/schema/page_templates.ts +27 -0
  81. package/src/schema/page_version_approvals.ts +12 -0
  82. package/src/schema/page_version_comments.ts +14 -0
  83. package/src/schema/page_versions.ts +34 -0
  84. package/src/schema/pages.ts +42 -0
  85. package/src/schema/search.ts +23 -0
  86. package/src/search/query.ts +10 -0
  87. package/src/search/sync.ts +32 -0
@@ -0,0 +1,354 @@
1
+ import { createSignal, onMount, onCleanup, For, type Component } from "solid-js"
2
+ import { type BaseBlock, calculateHeadingLevel } from "../../core/types/blocks"
3
+
4
+ export interface RimelightPreviewProps {
5
+ blocks?: BaseBlock[] | (() => BaseBlock[]) | undefined
6
+ title?: string | undefined
7
+ description?: string | undefined
8
+ banner?: string | undefined
9
+ category?: string | undefined
10
+ }
11
+
12
+ export const RimelightPreview: Component<RimelightPreviewProps> = (props) => {
13
+ const getInitialBlocks = (): BaseBlock[] => {
14
+ if (typeof props.blocks === "function") {
15
+ try {
16
+ return props.blocks() || []
17
+ } catch {
18
+ return []
19
+ }
20
+ }
21
+ if (Array.isArray(props.blocks)) {
22
+ return props.blocks
23
+ }
24
+ return []
25
+ }
26
+
27
+ const [blocks, setBlocks] = createSignal<BaseBlock[]>(getInitialBlocks())
28
+ const [title, setTitle] = createSignal<string>(props.title || "")
29
+ const [description, setDescription] = createSignal<string>(props.description || "")
30
+ const [banner, setBanner] = createSignal<string>(props.banner || "")
31
+ const [category, setCategory] = createSignal<string>(props.category || "")
32
+
33
+ onMount(() => {
34
+ const handleBlocksChange = (e: Event) => {
35
+ if (e instanceof CustomEvent && Array.isArray(e.detail?.blocks)) {
36
+ setBlocks(e.detail.blocks)
37
+ }
38
+ }
39
+
40
+ const handleMetaChange = (e: Event) => {
41
+ if (e instanceof CustomEvent && e.detail) {
42
+ if (typeof e.detail.title === "string") setTitle(e.detail.title)
43
+ if (typeof e.detail.description === "string") setDescription(e.detail.description)
44
+ if (typeof e.detail.banner === "string") setBanner(e.detail.banner)
45
+ if (typeof e.detail.category === "string") setCategory(e.detail.category)
46
+ }
47
+ }
48
+
49
+ const handlePropsChange = (e: Event) => {
50
+ if (e instanceof CustomEvent && e.detail?.properties) {
51
+ if (typeof e.detail.properties.heroImage === "string") {
52
+ setBanner(e.detail.properties.heroImage)
53
+ }
54
+ if (typeof e.detail.properties.category === "string") {
55
+ setCategory(e.detail.properties.category)
56
+ }
57
+ }
58
+ }
59
+
60
+ document.addEventListener("blocks-change", handleBlocksChange)
61
+ document.addEventListener("page-meta-change", handleMetaChange)
62
+ document.addEventListener("properties-change", handlePropsChange)
63
+
64
+ onCleanup(() => {
65
+ document.removeEventListener("blocks-change", handleBlocksChange)
66
+ document.removeEventListener("page-meta-change", handleMetaChange)
67
+ document.removeEventListener("properties-change", handlePropsChange)
68
+ })
69
+ })
70
+
71
+ return (
72
+ <div class="cms-preview-pane flex-1 w-full h-full overflow-y-auto bg-transparent">
73
+ <div class="cms-preview-document p-6 sm:p-8 max-w-4xl mx-auto space-y-6">
74
+ {banner() && (
75
+ <div class="rounded-2xl overflow-hidden border border-default shadow-sm aspect-[21/9]">
76
+ <img src={banner()} alt={title() || "Banner"} class="w-full h-full object-cover" />
77
+ </div>
78
+ )}
79
+
80
+ {(title() || description() || category()) && (
81
+ <header class="border-b border-default pb-6 mb-6">
82
+ {category() && (
83
+ <span class="inline-block px-2.5 py-1 text-xs font-semibold rounded-full bg-primary/10 text-primary mb-3 uppercase tracking-wider">
84
+ {category()}
85
+ </span>
86
+ )}
87
+ {title() && (
88
+ <h1 class="text-3xl sm:text-4xl font-extrabold text-foreground tracking-tight leading-tight">
89
+ {title()}
90
+ </h1>
91
+ )}
92
+ {description() && (
93
+ <p class="text-lg text-dimmed mt-3 leading-relaxed">{description()}</p>
94
+ )}
95
+ </header>
96
+ )}
97
+
98
+ <div class="space-y-4">
99
+ {blocks().length === 0 ? (
100
+ <div class="text-center py-12 text-neutral-500 text-sm italic">
101
+ No blocks in this page yet.
102
+ </div>
103
+ ) : (
104
+ <For each={blocks()}>{(b) => <div innerHTML={renderBlockPreviewHtml(b, 0)} />}</For>
105
+ )}
106
+ </div>
107
+ </div>
108
+ </div>
109
+ )
110
+ }
111
+
112
+ function escapeHtml(str: string): string {
113
+ return (str || "")
114
+ .replace(/&/g, "&amp;")
115
+ .replace(/</g, "&lt;")
116
+ .replace(/>/g, "&gt;")
117
+ .replace(/"/g, "&quot;")
118
+ }
119
+
120
+ function getChildren(b: BaseBlock): BaseBlock[] {
121
+ if (!b) return []
122
+ if (Array.isArray(b.children)) return b.children
123
+ if (Array.isArray(b.props?.children)) return b.props.children
124
+ return []
125
+ }
126
+
127
+ function renderBlockPreviewHtml(b: BaseBlock, sectionDepth: number = 0): string {
128
+ if (!b) return ""
129
+ const props = b.props || {}
130
+ const rawText = props.text
131
+ const textVal = typeof rawText === "object" && rawText !== null ? rawText.en || "" : rawText || ""
132
+
133
+ if (b.type === "ParagraphBlock") {
134
+ const renderNodes = (nodes: any[]): string => {
135
+ return nodes
136
+ .map((node) => {
137
+ if (!node) return ""
138
+ if (node.type === "text") {
139
+ let t = escapeHtml(node.text || "")
140
+ if (node.marks && Array.isArray(node.marks)) {
141
+ if (node.marks.includes("bold")) t = `<strong>${t}</strong>`
142
+ if (node.marks.includes("italic")) t = `<em>${t}</em>`
143
+ if (node.marks.includes("underline")) t = `<u>${t}</u>`
144
+ if (node.marks.includes("strikethrough")) t = `<s>${t}</s>`
145
+ if (node.marks.includes("code"))
146
+ t = `<code class="bg-neutral-800 text-neutral-200 px-1.5 py-0.5 rounded text-sm font-mono">${t}</code>`
147
+ }
148
+ return t
149
+ }
150
+ if (node.type === "link") {
151
+ const inner = node.children ? renderNodes(node.children) : escapeHtml(node.text || "")
152
+ const target = node.openInNewTab ? ' target="_blank" rel="noopener noreferrer"' : ""
153
+ return `<a href="${escapeHtml(node.url || "#")}"${target} class="text-blue-500 hover:text-blue-400 underline underline-offset-2">${inner}</a>`
154
+ }
155
+ if (node.type === "page_mention") {
156
+ const title = escapeHtml(node.displayTitle || node.pageSlug || node.pageId || "Page")
157
+ return `<span class="inline-flex items-center gap-1 px-1.5 py-0.5 rounded bg-blue-500/10 text-blue-400 text-sm font-medium">📄 ${title}</span>`
158
+ }
159
+ return ""
160
+ })
161
+ .join("")
162
+ }
163
+
164
+ let html = ""
165
+ if (Array.isArray(rawText)) {
166
+ html = renderNodes(rawText)
167
+ } else {
168
+ html = escapeHtml(textVal)
169
+ }
170
+ return `<p class="text-neutral-800 dark:text-neutral-200 text-base leading-relaxed my-3 whitespace-pre-wrap break-words">${html || '<span class="text-neutral-500 italic">Empty paragraph</span>'}</p>`
171
+ }
172
+
173
+ if (b.type === "SectionBlock") {
174
+ const level = calculateHeadingLevel(sectionDepth)
175
+ const title = props.title || "Untitled Section"
176
+ const slug = title.toLowerCase().replace(/\W+/g, "-")
177
+ const description = props.description
178
+ let headingClasses = "font-bold text-neutral-900 dark:text-white"
179
+ if (level === 2)
180
+ headingClasses += " text-3xl mt-6 mb-3 border-b border-gray-200 dark:border-gray-800 pb-2"
181
+ else if (level === 3) headingClasses += " text-2xl font-semibold mt-5 mb-2.5"
182
+ else if (level === 4) headingClasses += " text-xl font-medium mt-4 mb-2"
183
+ else if (level === 5) headingClasses += " text-lg font-medium mt-3 mb-1.5"
184
+ else headingClasses += " text-base font-normal mt-2 mb-1"
185
+
186
+ return `<div class="section-block my-6">
187
+ <h${level} id="${slug}" class="${headingClasses}">${escapeHtml(title)}</h${level}>
188
+ ${description ? `<p class="text-gray-500 dark:text-gray-400 text-sm mt-1 mb-4 italic">${escapeHtml(description)}</p>` : ""}
189
+ <div class="mt-4 pl-4 border-l-2 border-gray-200 dark:border-gray-800">
190
+ ${getChildren(b)
191
+ .map((c) => renderBlockPreviewHtml(c, sectionDepth + 1))
192
+ .join("")}
193
+ </div>
194
+ </div>`
195
+ }
196
+
197
+ if (b.type === "CalloutBlock") {
198
+ const variant = props.variant || "info"
199
+ const to = props.to
200
+ const target = props.target || "_self"
201
+ const icons: Record<string, string> = {
202
+ info: "ℹ️",
203
+ success: "✅",
204
+ warning: "⚠️",
205
+ error: "🛑",
206
+ commentary: "💬",
207
+ ideation: "💡",
208
+ source: "💻"
209
+ }
210
+ const icon = icons[variant] || icons.info
211
+
212
+ let variantClasses =
213
+ "bg-blue-50/50 dark:bg-blue-950/10 border-blue-200 dark:border-blue-900/50 text-blue-800 dark:text-blue-200"
214
+ if (variant === "success") {
215
+ variantClasses =
216
+ "bg-green-50/50 dark:bg-green-950/10 border-green-200 dark:border-green-900/50 text-green-800 dark:text-green-200"
217
+ } else if (variant === "warning") {
218
+ variantClasses =
219
+ "bg-yellow-50/50 dark:bg-yellow-950/10 border-yellow-200 dark:border-yellow-900/50 text-yellow-800 dark:text-yellow-200"
220
+ } else if (variant === "error") {
221
+ variantClasses =
222
+ "bg-red-50/50 dark:bg-red-950/10 border-red-200 dark:border-red-900/50 text-red-800 dark:text-red-200"
223
+ } else if (variant === "commentary") {
224
+ variantClasses =
225
+ "bg-purple-50/50 dark:bg-purple-950/10 border-purple-200 dark:border-purple-900/50 text-purple-800 dark:text-purple-200"
226
+ } else if (variant === "ideation") {
227
+ variantClasses =
228
+ "bg-amber-50/50 dark:bg-amber-950/10 border-amber-200 dark:border-amber-900/50 text-amber-800 dark:text-amber-200"
229
+ } else if (variant === "source") {
230
+ variantClasses =
231
+ "bg-indigo-50/50 dark:bg-indigo-950/10 border-indigo-200 dark:border-indigo-900/50 text-indigo-800 dark:text-indigo-200"
232
+ }
233
+
234
+ const tag = to ? `a href="${escapeHtml(to)}" target="${escapeHtml(target)}"` : "div"
235
+ const closeTag = to ? "a" : "div"
236
+
237
+ return `<${tag} class="callout border rounded-xl p-4 my-4 flex items-start gap-3 text-sm no-underline ${variantClasses}">
238
+ <span class="text-base shrink-0 mt-0.5 select-none">${icon}</span>
239
+ <div class="flex-1 min-w-0 text-sm">
240
+ ${getChildren(b)
241
+ .map((c) => renderBlockPreviewHtml(c, sectionDepth))
242
+ .join("")}
243
+ </div>
244
+ </${closeTag}>`
245
+ }
246
+
247
+ if (b.type === "ImageBlock") {
248
+ const src = props.src || props.url || ""
249
+ const alt = props.alt || "Image"
250
+ const caption = props.caption
251
+ return `<figure class="mx-auto my-6 flex flex-col items-center max-w-full">
252
+ <img src="${escapeHtml(src)}" alt="${escapeHtml(alt)}" class="h-auto max-w-full rounded-xl object-cover border border-neutral-200 dark:border-neutral-800 shadow-sm" />
253
+ ${caption ? `<figcaption class="mt-2 text-center text-xs text-neutral-500 dark:text-neutral-400 italic">${escapeHtml(caption)}</figcaption>` : ""}
254
+ </figure>`
255
+ }
256
+
257
+ if (b.type === "CodeBlock") {
258
+ const code = props.code || textVal
259
+ const language = props.language || "plaintext"
260
+ const caption = props.caption
261
+ return `<div class="cms-code my-4 rounded-lg bg-gray-950 p-4 font-mono text-sm border border-gray-800 overflow-x-auto">
262
+ <div class="text-xs text-gray-500 uppercase mb-2 select-none">${escapeHtml(language)}</div>
263
+ <pre class="m-0"><code class="text-green-300 font-mono">${escapeHtml(code)}</code></pre>
264
+ ${caption ? `<div class="mt-2 text-center text-xs text-gray-500 italic">${escapeHtml(caption)}</div>` : ""}
265
+ </div>`
266
+ }
267
+
268
+ if (b.type === "ScriptBlock") {
269
+ const title = props.title || "Untitled Script"
270
+ const synopsis = props.synopsis
271
+ const chars = props.characters || []
272
+ const { aPlot, bPlot, cPlot, stinger } = props
273
+ return `<div class="my-6 p-4 border-l-4 border-amber-400 bg-amber-50/20 dark:bg-amber-950/10 rounded-r-lg">
274
+ <div class="mb-4">
275
+ <h3 class="text-xl font-bold text-amber-900 dark:text-amber-100">${escapeHtml(title)}</h3>
276
+ ${synopsis ? `<p class="text-sm text-gray-500 dark:text-gray-400 mt-1 italic">${escapeHtml(synopsis)}</p>` : ""}
277
+ ${chars.length ? `<div class="flex flex-wrap gap-3 mt-3 text-xs">${chars.map((c: string) => `<span class="px-2 py-0.5 rounded-full bg-amber-100 dark:bg-amber-900/30 text-amber-800 dark:text-amber-200">${escapeHtml(c)}</span>`).join("")}</div>` : ""}
278
+ </div>
279
+ ${
280
+ aPlot || bPlot || cPlot
281
+ ? `<div class="grid grid-cols-1 md:grid-cols-3 gap-3 my-3">
282
+ ${aPlot ? `<div class="bg-white/50 dark:bg-neutral-900/30 rounded p-2 border border-gray-200 dark:border-gray-700"><div class="text-[10px] font-bold uppercase tracking-wider text-gray-500 dark:text-gray-400">A Plot</div><div class="text-sm mt-0.5">${escapeHtml(aPlot)}</div></div>` : ""}
283
+ ${bPlot ? `<div class="bg-white/50 dark:bg-neutral-900/30 rounded p-2 border border-gray-200 dark:border-gray-700"><div class="text-[10px] font-bold uppercase tracking-wider text-gray-500 dark:text-gray-400">B Plot</div><div class="text-sm mt-0.5">${escapeHtml(bPlot)}</div></div>` : ""}
284
+ ${cPlot ? `<div class="bg-white/50 dark:bg-neutral-900/30 rounded p-2 border border-gray-200 dark:border-gray-700"><div class="text-[10px] font-bold uppercase tracking-wider text-gray-500 dark:text-gray-400">C Plot</div><div class="text-sm mt-0.5">${escapeHtml(cPlot)}</div></div>` : ""}
285
+ </div>`
286
+ : ""
287
+ }
288
+ ${stinger ? `<p class="text-sm text-gray-500 dark:text-gray-400 italic mt-2 border-t border-gray-200 dark:border-gray-700 pt-2">Stinger: ${escapeHtml(stinger)}</p>` : ""}
289
+ <div class="mt-4">
290
+ ${getChildren(b)
291
+ .map((c) => renderBlockPreviewHtml(c, sectionDepth))
292
+ .join("")}
293
+ </div>
294
+ </div>`
295
+ }
296
+
297
+ if (b.type === "SceneBlock") {
298
+ const settingLabels: Record<string, string> = {
299
+ INTERIOR: "INT.",
300
+ EXTERIOR: "EXT.",
301
+ OTHER: "Other"
302
+ }
303
+ const timeOfDayLabels: Record<string, string> = {
304
+ MORNING: "Morning",
305
+ NOON: "Noon",
306
+ AFTERNOON: "Afternoon",
307
+ EVENING: "Evening",
308
+ NIGHT: "Night",
309
+ OTHER: "Other"
310
+ }
311
+ const transitionLabels: Record<string, string> = {
312
+ CUT_TO: "CUT TO",
313
+ CUT_BACK_TO: "CUT BACK TO",
314
+ FADE_TO: "FADE TO",
315
+ NONE: ""
316
+ }
317
+
318
+ const setting = settingLabels[props.setting] || props.setting || "INT."
319
+ const location = props.location || ""
320
+ const tod = timeOfDayLabels[props.timeOfDay] || props.timeOfDay || "Morning"
321
+ const transition = transitionLabels[props.transition] || ""
322
+
323
+ const parts: string[] = []
324
+ if (setting && setting !== "Other") parts.push(setting)
325
+ if (location) parts.push(location)
326
+ if (tod && tod !== "Other") parts.push(`- ${tod}`)
327
+
328
+ return `<div class="my-4 p-3 border-l-4 border-indigo-400 bg-indigo-50/20 dark:bg-indigo-950/10 rounded-r-lg">
329
+ <div class="flex flex-wrap items-center gap-2 text-sm font-semibold text-indigo-800 dark:text-indigo-200 mb-2">
330
+ ${parts.map((p) => `<span>${escapeHtml(p)}</span>`).join("")}
331
+ ${transition ? `<span class="ml-auto text-[10px] italic text-gray-500 dark:text-gray-400">${escapeHtml(transition)}</span>` : ""}
332
+ </div>
333
+ ${props.description ? `<p class="text-sm text-gray-700 dark:text-gray-300 mb-2">${escapeHtml(props.description)}</p>` : ""}
334
+ <div class="mt-3">
335
+ ${getChildren(b)
336
+ .map((c) => renderBlockPreviewHtml(c, sectionDepth))
337
+ .join("")}
338
+ </div>
339
+ </div>`
340
+ }
341
+
342
+ if (b.type === "DialogueBlock") {
343
+ const character = props.character || "CHARACTER"
344
+ const parenthetical = props.parenthetical
345
+ const line = props.line || ""
346
+ return `<div class="my-3 mx-8 max-w-md">
347
+ <div class="text-sm font-bold text-center text-pink-800 dark:text-pink-200">${escapeHtml(character)}</div>
348
+ ${parenthetical ? `<div class="text-xs italic text-center text-gray-500 dark:text-gray-400 mt-0.5">(${escapeHtml(parenthetical)})</div>` : ""}
349
+ <div class="text-sm text-center text-gray-700 dark:text-gray-300 mt-1">${escapeHtml(line)}</div>
350
+ </div>`
351
+ }
352
+
353
+ return `<div class="p-3 bg-gray-900 border border-gray-800 rounded text-sm text-gray-400 my-2">${escapeHtml(b.type)}: ${escapeHtml(textVal)}</div>`
354
+ }