Skip to content

Commit b29f182

Browse files
committed
Docs Steps, code-tab groups, and API method chips
Three docs components join the vocabulary: sim:steps renders the numbered timeline (muted circle markers, hairline connector, title and content per step); sim:tabs renders the docs' grouped code block (mono tab chips, one pane at a time, the icon copy control targeting the visible pane); and a METHOD prefix on a set-nav page entry renders the API-reference chip — sidebar entries only, on the platform badge tokens (blue and purple added to the mirrors and the live bridge). Also: preview images switched from blob: to data: URIs — blob URLs are origin-bound and the sandboxed frame's origin is opaque, so Chromium refused to render them — and the page view-lock is sticky per file so a patch stream cannot flash raw source.
1 parent c7ebfc9 commit b29f182

4 files changed

Lines changed: 165 additions & 9 deletions

File tree

‎apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/preview-panel.tsx‎

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -244,14 +244,28 @@ function useInlinedWorkspaceImages(content: string): string {
244244
if (missing.length === 0) return
245245
let cancelled = false
246246
for (const url of missing) {
247-
// boundary-raw-fetch: binary image bytes, converted to blob: URLs for the cookie-less sandboxed preview
247+
// boundary-raw-fetch: binary image bytes for the cookie-less sandboxed preview
248248
fetch(url)
249249
.then((response) => (response.ok ? response.blob() : null))
250-
.then((blob) => {
251-
if (!blob || cancelled) return
252-
const blobUrl = URL.createObjectURL(blob)
253-
cache.set(url, blobUrl)
254-
setResolved((prev) => ({ ...prev, [url]: blobUrl }))
250+
.then(
251+
(blob) =>
252+
new Promise<string | null>((resolve) => {
253+
if (!blob) return resolve(null)
254+
// data: URIs, NOT blob: URLs — blob URLs are origin-bound and
255+
// the sandboxed frame's origin is opaque, so Chromium refuses
256+
// to render a parent-origin blob inside it. data: is
257+
// origin-independent and already allowed by the preview CSP.
258+
const reader = new FileReader()
259+
reader.onload = () =>
260+
resolve(typeof reader.result === 'string' ? reader.result : null)
261+
reader.onerror = () => resolve(null)
262+
reader.readAsDataURL(blob)
263+
})
264+
)
265+
.then((dataUri) => {
266+
if (!dataUri || cancelled) return
267+
cache.set(url, dataUri)
268+
setResolved((prev) => ({ ...prev, [url]: dataUri }))
255269
})
256270
.catch(() => {})
257271
}

‎apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/text-editor.tsx‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ import { PreviewLoadingFrame } from './preview-shared'
2121
import { useEditableFileContent } from './use-editable-file-content'
2222
import { useSelectionCopyBridge } from './use-selection-copy-bridge'
2323

24+
/** File ids observed rendering as Sim pages this session (see the sticky lock). */
25+
const KNOWN_PAGE_FILE_IDS = new Set<string>()
26+
2427
const SIM_DARK_RULES: MonacoEditorTypes.ITokenThemeRule[] = [
2528
{ token: 'comment', foreground: '606060', fontStyle: 'italic' },
2629
{ token: 'string', foreground: '3ab872' },
@@ -595,11 +598,17 @@ export const TextEditor = memo(function TextEditor({
595598
// the raw-header flash at stream start and the raw flips between an
596599
// agent's tool calls. Bespoke raw HTML keeps the old behavior.
597600
const trimmedContent = content.trimStart()
598-
const isSimPageFile =
601+
const detectedSimPage =
599602
file.type === SIM_PAGE_CONTENT_TYPE ||
600603
(previewType === 'html' &&
601604
(isSimPageSource(content) ||
602605
(isStreaming && (trimmedContent === '' || trimmedContent.startsWith('-')))))
606+
// Sticky per file: a PATCH stream carries only the replacement snippet,
607+
// which is not frontmatter-shaped — without memory the lock would drop
608+
// mid-edit and flash raw source. Once a file is known to be a page, it
609+
// stays one for the session.
610+
if (detectedSimPage) KNOWN_PAGE_FILE_IDS.add(file.id)
611+
const isSimPageFile = detectedSimPage || KNOWN_PAGE_FILE_IDS.has(file.id)
603612
const effectiveMode = isSimPageFile
604613
? 'preview'
605614
: isStreaming && isIframeRendered

‎apps/sim/lib/workspace-files/artifact-stylesheet.ts‎

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ const ARTIFACT_TOKENS = [
5757
'--badge-error-text',
5858
'--badge-gray-bg',
5959
'--badge-gray-text',
60+
'--badge-blue-bg',
61+
'--badge-blue-text',
62+
'--badge-purple-bg',
63+
'--badge-purple-text',
6064
'--text-icon-muted',
6165
'--code-bg',
6266
'--selection-bg',
@@ -134,6 +138,10 @@ export const SIM_ARTIFACT_STYLESHEET = `
134138
--badge-error-text: #dc2626;
135139
--badge-gray-bg: #e7e5e4;
136140
--badge-gray-text: #57534e;
141+
--badge-blue-bg: #bfdbfe;
142+
--badge-blue-text: #1d4ed8;
143+
--badge-purple-bg: #e9d5ff;
144+
--badge-purple-text: #7c3aed;
137145
--text-icon-muted: #5c5c5c;
138146
--code-bg: #f5f5f5;
139147
--code-surface: var(--surface-5);
@@ -188,6 +196,10 @@ export const SIM_ARTIFACT_STYLESHEET = `
188196
--badge-error-text: #fca5a5;
189197
--badge-gray-bg: #3a3a3a;
190198
--badge-gray-text: #a8a8a8;
199+
--badge-blue-bg: rgba(59, 130, 246, 0.2);
200+
--badge-blue-text: #93c5fd;
201+
--badge-purple-bg: rgba(168, 85, 247, 0.2);
202+
--badge-purple-text: #d8b4fe;
191203
--bg: #1b1b1b;
192204
--surface-1: #1e1e1e;
193205
--surface-2: #232323;
@@ -609,6 +621,49 @@ figcaption { font-size: 0.875em; line-height: 1.4285714; color: var(--text-prima
609621
.codeblock-copy.is-copied { color: var(--brand-accent); }
610622
.codeblock pre { border: none; border-radius: 0; margin: 0; background: transparent; }
611623
624+
/* Steps — the docs' numbered timeline: a muted numbered circle per step,
625+
a hairline connector between them, title + content beside. */
626+
.steps { list-style: none; margin: 1.25rem 0; padding: 0; }
627+
.step { position: relative; display: flex; gap: 1rem; padding-bottom: 1.9rem; }
628+
.step:last-child { padding-bottom: 0.25rem; }
629+
.step::before {
630+
content: ''; position: absolute; left: 13.5px; top: 32px; bottom: 4px;
631+
width: 1px; background: var(--border);
632+
}
633+
.step:last-child::before { display: none; }
634+
.step-marker {
635+
flex-shrink: 0; width: 28px; height: 28px; border-radius: 50%;
636+
display: flex; align-items: center; justify-content: center;
637+
background: var(--surface-5); color: var(--text-muted);
638+
font-size: var(--text-small); font-weight: 500;
639+
}
640+
.step-body { min-width: 0; padding-top: 3px; }
641+
.step-title { font-weight: 500; color: var(--text-primary); margin-bottom: 0.4rem; }
642+
643+
/* Code tabs — the docs' grouped code block: mono tab chips in the header,
644+
one pane visible at a time. */
645+
.codetabs-head { display: flex; align-items: center; gap: 2px; padding: 0.3rem 0.4rem; }
646+
.codetab {
647+
font-family: var(--font-mono); font-size: var(--text-small);
648+
color: var(--text-muted); background: none; border: none;
649+
border-radius: 0.375rem; padding: 0.2rem 0.6rem; cursor: pointer;
650+
}
651+
.codetab:hover { background: var(--surface-hover); color: var(--text-body); }
652+
.codetab.is-active { background: var(--surface-active); color: var(--text-primary); }
653+
.codetabs-head .codetabs-copy { position: static; margin-left: auto; }
654+
655+
/* API method chips — sidebar entries only, the docs' reference nav. */
656+
.rail .method {
657+
display: inline-block; margin-right: 8px; padding: 1px 6px;
658+
border-radius: 6px; font-size: 10px; font-weight: 600; letter-spacing: 0.02em;
659+
vertical-align: 1px;
660+
}
661+
.rail .method-get { background: var(--badge-success-bg); color: var(--badge-success-text); }
662+
.rail .method-post { background: var(--badge-blue-bg); color: var(--badge-blue-text); }
663+
.rail .method-put { background: var(--badge-purple-bg); color: var(--badge-purple-text); }
664+
.rail .method-patch { background: var(--badge-orange-bg); color: var(--badge-orange-text); }
665+
.rail .method-delete { background: var(--badge-error-bg); color: var(--badge-error-text); }
666+
612667
/* Footer pagination — the docs' PageFooter verbatim: name + chevron on a
613668
hover pill (mt-12 flex gap-2 py-3; cards flex-1 gap-1.5 rounded-lg px-3
614669
py-3 text-sm --text-body, hover --surface-active; chevrons 14px
@@ -827,6 +882,43 @@ export const SIM_ARTIFACT_SHELL = `<script>
827882
frame.append(copy, pre)
828883
}
829884
885+
// Code-tab groups: switch panes; the docs' icon copy control targets the
886+
// visible pane.
887+
for (const group of [...main.querySelectorAll('.codetabs')]) {
888+
const head = group.querySelector('.codetabs-head')
889+
if (!head) continue
890+
const groupCopy = document.createElement('button')
891+
groupCopy.className = 'codeblock-copy codetabs-copy'
892+
groupCopy.type = 'button'
893+
groupCopy.setAttribute('aria-label', 'Copy Text')
894+
groupCopy.innerHTML = COPY_ICON
895+
let groupResetTimer
896+
groupCopy.addEventListener('click', async () => {
897+
const pane = group.querySelector('pre:not([hidden])')
898+
try {
899+
await navigator.clipboard.writeText((pane && pane.textContent) || '')
900+
groupCopy.innerHTML = COPIED_ICON
901+
groupCopy.classList.add('is-copied')
902+
clearTimeout(groupResetTimer)
903+
groupResetTimer = setTimeout(() => {
904+
groupCopy.innerHTML = COPY_ICON
905+
groupCopy.classList.remove('is-copied')
906+
}, 1500)
907+
} catch {}
908+
})
909+
head.appendChild(groupCopy)
910+
head.addEventListener('click', (event) => {
911+
const origin = event.target
912+
const btn = origin && origin.closest ? origin.closest('.codetab') : null
913+
if (!btn) return
914+
const tabs = [...head.querySelectorAll('.codetab')]
915+
const panes = [...group.querySelectorAll('pre')]
916+
const index = tabs.indexOf(btn)
917+
tabs.forEach((tab, i) => tab.classList.toggle('is-active', i === index))
918+
panes.forEach((pane, i) => { pane.hidden = i !== index })
919+
})
920+
}
921+
830922
const cols = document.createElement('div')
831923
cols.className = 'art-cols'
832924
const mid = document.createElement('div')

‎apps/sim/lib/workspace-files/page-compile.ts‎

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,16 @@ function setNavHtml(nav: NonNullable<z.infer<typeof frontmatterSchema>['nav']>):
6767
.map((group) => {
6868
const links = group.pages
6969
.map((entry) => {
70-
const match = entry.match(MD_LINK)
70+
// An optional METHOD prefix ("GET [List Workflows](sim:file/x)")
71+
// renders the docs' API-reference chip — sidebar entries only.
72+
const methodMatch = entry.match(/^\s*(GET|POST|PUT|PATCH|DELETE)\s+(.*)$/)
73+
const method = methodMatch?.[1]
74+
const match = (methodMatch?.[2] ?? entry).match(MD_LINK)
7175
if (!match) return ''
72-
return `<a href="${escapeHtml(match[2])}">${escapeHtml(match[1])}</a>`
76+
const chip = method
77+
? `<span class="method method-${method.toLowerCase()}">${method}</span>`
78+
: ''
79+
return `<a href="${escapeHtml(match[2])}">${chip}${escapeHtml(match[1])}</a>`
7380
})
7481
.filter(Boolean)
7582
.join('')
@@ -125,6 +132,12 @@ const tablePayloadSchema = z.object({
125132
rows: z.array(z.array(textCell)).min(1),
126133
})
127134
const kvItemsSchema = z.array(z.object({ key: textCell, value: textCell })).min(1)
135+
const stepsItemsSchema = z
136+
.array(z.object({ title: textCell, markdown: textCell.optional() }))
137+
.min(1)
138+
const tabsItemsSchema = z
139+
.array(z.object({ label: textCell, lang: textCell.optional(), code: textCell }))
140+
.min(1)
128141
const accordionItemsSchema = z
129142
.array(
130143
z
@@ -228,6 +241,34 @@ const FENCE_RENDERERS: Record<string, FenceRenderer> = {
228241
},
229242
faq: renderAccordion,
230243
accordion: renderAccordion,
244+
steps: (payload) => {
245+
const parsed = stepsItemsSchema.safeParse(payload)
246+
if (!parsed.success) return null
247+
const items = parsed.data
248+
.map(
249+
(step, index) =>
250+
`<li class="step"><div class="step-marker">${index + 1}</div><div class="step-body"><div class="step-title">${renderInlineMarkdown(step.title)}</div>${step.markdown ? renderMarkdown(step.markdown) : ''}</div></li>`
251+
)
252+
.join('')
253+
return `<ol class="steps">${items}</ol>`
254+
},
255+
tabs: (payload) => {
256+
const parsed = tabsItemsSchema.safeParse(payload)
257+
if (!parsed.success) return null
258+
const buttons = parsed.data
259+
.map(
260+
(tab, index) =>
261+
`<button type="button" class="codetab${index === 0 ? ' is-active' : ''}">${escapeHtml(tab.label)}</button>`
262+
)
263+
.join('')
264+
const panes = parsed.data
265+
.map(
266+
(tab, index) =>
267+
`<pre${index === 0 ? '' : ' hidden'}><code${tab.lang ? ` class="language-${escapeHtml(tab.lang)}"` : ''}>${escapeHtml(tab.code)}</code></pre>`
268+
)
269+
.join('')
270+
return `<figure class="codeblock codetabs"><div class="codeblock-head codetabs-head">${buttons}</div>${panes}</figure>`
271+
},
231272
}
232273

233274
/** `sim:faq` and `sim:accordion` share the docs' expandable-rows treatment. */

0 commit comments

Comments
 (0)