Spaces:
Running
Running
UI: showcase SIE with diagram, code snippets, CTAs
Browse files- web/public/app.js +59 -0
- web/public/index.html +54 -7
- web/public/style.css +98 -5
web/public/app.js
CHANGED
|
@@ -13,9 +13,13 @@ const els = {
|
|
| 13 |
footer: document.getElementById("footer"),
|
| 14 |
sieUrl: document.getElementById("sie-url"),
|
| 15 |
timings: document.getElementById("timings"),
|
|
|
|
|
|
|
|
|
|
| 16 |
};
|
| 17 |
|
| 18 |
let activeSampleId = null;
|
|
|
|
| 19 |
let timings = { recognitionMs: 0, donutMs: 0, glinerMs: 0 };
|
| 20 |
let donutBuf = { entities: [], data: null };
|
| 21 |
let glinerBuf = [];
|
|
@@ -38,6 +42,57 @@ function escapeHtml(s) {
|
|
| 38 |
);
|
| 39 |
}
|
| 40 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
function populateDropdown(selectEl, options, defaultId) {
|
| 42 |
selectEl.innerHTML = "";
|
| 43 |
for (const opt of options) {
|
|
@@ -56,6 +111,7 @@ function populateDropdown(selectEl, options, defaultId) {
|
|
| 56 |
node.title = opt.description;
|
| 57 |
selectEl.appendChild(node);
|
| 58 |
}
|
|
|
|
| 59 |
}
|
| 60 |
|
| 61 |
function renderSamples(samples, onClick) {
|
|
@@ -78,6 +134,8 @@ function renderSamples(samples, onClick) {
|
|
| 78 |
node.addEventListener("click", () => {
|
| 79 |
for (const n of els.events.querySelectorAll(".event")) n.classList.remove("active");
|
| 80 |
node.classList.add("active");
|
|
|
|
|
|
|
| 81 |
onClick(node.dataset.id);
|
| 82 |
});
|
| 83 |
}
|
|
@@ -219,6 +277,7 @@ async function init() {
|
|
| 219 |
populateDropdown(els.selectRecognition, modelConfig.recognition, modelConfig.defaults.recognition);
|
| 220 |
populateDropdown(els.selectStructured, modelConfig.structured, modelConfig.defaults.structured);
|
| 221 |
populateDropdown(els.selectNer, modelConfig.ner, modelConfig.defaults.ner);
|
|
|
|
| 222 |
} catch (e) {
|
| 223 |
console.error("failed to load model config", e);
|
| 224 |
}
|
|
|
|
| 13 |
footer: document.getElementById("footer"),
|
| 14 |
sieUrl: document.getElementById("sie-url"),
|
| 15 |
timings: document.getElementById("timings"),
|
| 16 |
+
snippetRecognition: document.getElementById("snippet-recognition"),
|
| 17 |
+
snippetStructured: document.getElementById("snippet-structured"),
|
| 18 |
+
snippetNer: document.getElementById("snippet-ner"),
|
| 19 |
};
|
| 20 |
|
| 21 |
let activeSampleId = null;
|
| 22 |
+
let activeSample = null;
|
| 23 |
let timings = { recognitionMs: 0, donutMs: 0, glinerMs: 0 };
|
| 24 |
let donutBuf = { entities: [], data: null };
|
| 25 |
let glinerBuf = [];
|
|
|
|
| 42 |
);
|
| 43 |
}
|
| 44 |
|
| 45 |
+
function findModel(list, id) {
|
| 46 |
+
return list ? list.find((m) => m.id === id) : null;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
function snippetRecognition(modelId) {
|
| 50 |
+
if (!modelConfig) return "";
|
| 51 |
+
const opt = findModel(modelConfig.recognition, modelId);
|
| 52 |
+
const hasOpts = opt && opt.options && Object.keys(opt.options).length > 0;
|
| 53 |
+
const lines = [
|
| 54 |
+
'client.extract(',
|
| 55 |
+
` <span class="str">"${escapeHtml(modelId)}"</span>,`,
|
| 56 |
+
' Item(images=[image_bytes]),',
|
| 57 |
+
];
|
| 58 |
+
if (hasOpts) {
|
| 59 |
+
const optsJson = JSON.stringify(opt.options).replace(/"/g, '"');
|
| 60 |
+
lines.push(` <span class="arg">options</span>=${escapeHtml(optsJson)},`);
|
| 61 |
+
}
|
| 62 |
+
lines.push(')');
|
| 63 |
+
return `<span class="com"># Recognition: one call against SIE</span>\n` + lines.join("\n");
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
function snippetStructured(modelId) {
|
| 67 |
+
return (
|
| 68 |
+
`<span class="com"># Structured: same client.extract, different model_id</span>\n` +
|
| 69 |
+
`client.extract(\n` +
|
| 70 |
+
` <span class="str">"${escapeHtml(modelId)}"</span>,\n` +
|
| 71 |
+
` Item(images=[image_bytes]),\n` +
|
| 72 |
+
`)`
|
| 73 |
+
);
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
function snippetNer(modelId, sample) {
|
| 77 |
+
const labels = sample ? sample.labels : ["merchant", "total", "date"];
|
| 78 |
+
const labelsStr = "[" + labels.map((l) => `<span class="str">"${escapeHtml(l)}"</span>`).join(", ") + "]";
|
| 79 |
+
return (
|
| 80 |
+
`<span class="com"># NER: text input this time, declared labels</span>\n` +
|
| 81 |
+
`client.extract(\n` +
|
| 82 |
+
` <span class="str">"${escapeHtml(modelId)}"</span>,\n` +
|
| 83 |
+
` Item(text=recognized_markdown),\n` +
|
| 84 |
+
` <span class="arg">labels</span>=${labelsStr},\n` +
|
| 85 |
+
`)`
|
| 86 |
+
);
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
function updateSnippets() {
|
| 90 |
+
if (!els.snippetRecognition) return;
|
| 91 |
+
els.snippetRecognition.innerHTML = snippetRecognition(els.selectRecognition.value);
|
| 92 |
+
els.snippetStructured.innerHTML = snippetStructured(els.selectStructured.value);
|
| 93 |
+
els.snippetNer.innerHTML = snippetNer(els.selectNer.value, activeSample);
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
function populateDropdown(selectEl, options, defaultId) {
|
| 97 |
selectEl.innerHTML = "";
|
| 98 |
for (const opt of options) {
|
|
|
|
| 111 |
node.title = opt.description;
|
| 112 |
selectEl.appendChild(node);
|
| 113 |
}
|
| 114 |
+
selectEl.addEventListener("change", updateSnippets);
|
| 115 |
}
|
| 116 |
|
| 117 |
function renderSamples(samples, onClick) {
|
|
|
|
| 134 |
node.addEventListener("click", () => {
|
| 135 |
for (const n of els.events.querySelectorAll(".event")) n.classList.remove("active");
|
| 136 |
node.classList.add("active");
|
| 137 |
+
activeSample = samples.find((s) => s.id === node.dataset.id) || null;
|
| 138 |
+
updateSnippets();
|
| 139 |
onClick(node.dataset.id);
|
| 140 |
});
|
| 141 |
}
|
|
|
|
| 277 |
populateDropdown(els.selectRecognition, modelConfig.recognition, modelConfig.defaults.recognition);
|
| 278 |
populateDropdown(els.selectStructured, modelConfig.structured, modelConfig.defaults.structured);
|
| 279 |
populateDropdown(els.selectNer, modelConfig.ner, modelConfig.defaults.ner);
|
| 280 |
+
updateSnippets();
|
| 281 |
} catch (e) {
|
| 282 |
console.error("failed to load model config", e);
|
| 283 |
}
|
web/public/index.html
CHANGED
|
@@ -15,17 +15,54 @@
|
|
| 15 |
</div>
|
| 16 |
<div class="meta" id="models">SIE: <code>...</code></div>
|
| 17 |
<div class="meta" id="sie-state">checking SIE...</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
</header>
|
| 19 |
|
| 20 |
<section class="hero">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
<p>
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
hot-swap them with one identifier change.
|
| 29 |
</p>
|
| 30 |
</section>
|
| 31 |
|
|
@@ -54,6 +91,10 @@
|
|
| 54 |
<h2>Recognition (Markdown)</h2>
|
| 55 |
<span class="hint" id="recognition-meta"></span>
|
| 56 |
</header>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
<div class="markdown" id="recognition">
|
| 58 |
<p class="hint">Click a sample on the left.</p>
|
| 59 |
</div>
|
|
@@ -64,6 +105,12 @@
|
|
| 64 |
<h2>Extraction</h2>
|
| 65 |
<span class="hint" id="extraction-meta"></span>
|
| 66 |
</header>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
<div class="extraction" id="extraction">
|
| 68 |
<p class="hint">Typed fields will appear here.</p>
|
| 69 |
</div>
|
|
|
|
| 15 |
</div>
|
| 16 |
<div class="meta" id="models">SIE: <code>...</code></div>
|
| 17 |
<div class="meta" id="sie-state">checking SIE...</div>
|
| 18 |
+
<div class="cta-row">
|
| 19 |
+
<a class="cta" href="https://github.com/superlinked/brave-new-demos/tree/main/document-ocr" target="_blank" rel="noopener">
|
| 20 |
+
<span>↗</span> Source on GitHub
|
| 21 |
+
</a>
|
| 22 |
+
<a class="cta" href="https://github.com/superlinked/sie" target="_blank" rel="noopener">
|
| 23 |
+
<span>★</span> SIE repo
|
| 24 |
+
</a>
|
| 25 |
+
</div>
|
| 26 |
</header>
|
| 27 |
|
| 28 |
<section class="hero">
|
| 29 |
+
<div class="hero-text">
|
| 30 |
+
<p>
|
| 31 |
+
OCR is rarely a single-model problem. This demo runs three model
|
| 32 |
+
classes through <strong>one SIE server</strong>: a VLM-OCR recognizes
|
| 33 |
+
the document into Markdown, a fine-tuned Donut emits a JSON tree
|
| 34 |
+
directly, and a zero-shot NER (GLiNER) pulls typed fields out of
|
| 35 |
+
the recognition output. Pick a sample on the left, swap any of the
|
| 36 |
+
three models in the dropdowns, watch SIE hot-swap them with
|
| 37 |
+
<em>one identifier change</em>.
|
| 38 |
+
</p>
|
| 39 |
+
</div>
|
| 40 |
+
<div class="hero-diagram">
|
| 41 |
+
<div class="diagram">
|
| 42 |
+
<div class="diagram-input">image</div>
|
| 43 |
+
<div class="diagram-arrow">↓</div>
|
| 44 |
+
<div class="diagram-server">one SIE server · <code>client.extract(model_id, item)</code></div>
|
| 45 |
+
<div class="diagram-arrows">
|
| 46 |
+
<span>↓</span><span>↓</span><span>↓</span>
|
| 47 |
+
</div>
|
| 48 |
+
<div class="diagram-models">
|
| 49 |
+
<div class="diagram-box diagram-recognition">VLM-OCR<br><span>(Florence-2, LightOnOCR, GLM-OCR, ...)</span></div>
|
| 50 |
+
<div class="diagram-box diagram-structured">Donut<br><span>(end-to-end JSON)</span></div>
|
| 51 |
+
<div class="diagram-box diagram-ner">GLiNER<br><span>(zero-shot NER)</span></div>
|
| 52 |
+
</div>
|
| 53 |
+
</div>
|
| 54 |
+
</div>
|
| 55 |
+
</section>
|
| 56 |
+
|
| 57 |
+
<section class="why-sie">
|
| 58 |
+
<h3>Why SIE</h3>
|
| 59 |
<p>
|
| 60 |
+
Three different model architectures (a vision-language model, a
|
| 61 |
+
fine-tuned encoder-decoder, a span-based NER), one inference engine,
|
| 62 |
+
one HTTP API, one SDK call. Without SIE, this demo would be three
|
| 63 |
+
separate inference services with three SDKs, three auth flows, three
|
| 64 |
+
rate limits. With SIE, swap a string in <code>client.extract(...)</code>
|
| 65 |
+
and the underlying architecture changes.
|
|
|
|
| 66 |
</p>
|
| 67 |
</section>
|
| 68 |
|
|
|
|
| 91 |
<h2>Recognition (Markdown)</h2>
|
| 92 |
<span class="hint" id="recognition-meta"></span>
|
| 93 |
</header>
|
| 94 |
+
<details class="sdk-snippet">
|
| 95 |
+
<summary>See the SIE call</summary>
|
| 96 |
+
<pre><code id="snippet-recognition">// pick a recognition model in the dropdown</code></pre>
|
| 97 |
+
</details>
|
| 98 |
<div class="markdown" id="recognition">
|
| 99 |
<p class="hint">Click a sample on the left.</p>
|
| 100 |
</div>
|
|
|
|
| 105 |
<h2>Extraction</h2>
|
| 106 |
<span class="hint" id="extraction-meta"></span>
|
| 107 |
</header>
|
| 108 |
+
<details class="sdk-snippet">
|
| 109 |
+
<summary>See the SIE calls</summary>
|
| 110 |
+
<pre><code id="snippet-structured">// structured (Donut)</code>
|
| 111 |
+
|
| 112 |
+
<code id="snippet-ner">// NER (GLiNER)</code></pre>
|
| 113 |
+
</details>
|
| 114 |
<div class="extraction" id="extraction">
|
| 115 |
<p class="hint">Typed fields will appear here.</p>
|
| 116 |
</div>
|
web/public/style.css
CHANGED
|
@@ -16,13 +16,14 @@
|
|
| 16 |
html, body {
|
| 17 |
margin: 0; padding: 0; background: var(--bg); color: var(--text);
|
| 18 |
font: 14px ui-monospace, SFMono-Regular, Menlo, monospace;
|
| 19 |
-
height: 100%;
|
| 20 |
}
|
| 21 |
-
body { display: flex; flex-direction: column; }
|
| 22 |
|
| 23 |
header {
|
| 24 |
display: flex; align-items: center; gap: 16px;
|
| 25 |
padding: 12px 20px; border-bottom: 1px solid var(--line);
|
|
|
|
| 26 |
}
|
| 27 |
.title { display: flex; align-items: center; gap: 12px; }
|
| 28 |
.logo { font-size: 18px; }
|
|
@@ -37,17 +38,83 @@ h1 { font-size: 16px; margin: 0; font-weight: 600; }
|
|
| 37 |
.badge.red { background: rgba(255, 107, 107, 0.18); color: var(--red); }
|
| 38 |
.meta { color: var(--muted); font-size: 12px; }
|
| 39 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
.hero {
|
| 41 |
-
padding: 14px 20px;
|
|
|
|
| 42 |
border-bottom: 1px solid var(--line);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
}
|
| 44 |
-
.
|
| 45 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
|
| 47 |
main {
|
| 48 |
flex: 1; display: grid;
|
| 49 |
grid-template-columns: 0.95fr 1.4fr 1.2fr;
|
| 50 |
gap: 12px; padding: 12px 20px; overflow: hidden;
|
|
|
|
| 51 |
}
|
| 52 |
|
| 53 |
.panel {
|
|
@@ -73,6 +140,26 @@ main {
|
|
| 73 |
flex: 1; overflow: auto; padding: 12px 14px; margin: 0;
|
| 74 |
}
|
| 75 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
.meta-row {
|
| 77 |
padding: 10px 14px; border-bottom: 1px solid var(--line);
|
| 78 |
font-size: 11px; color: var(--muted);
|
|
@@ -146,3 +233,9 @@ footer {
|
|
| 146 |
display: flex; justify-content: space-between; gap: 12px;
|
| 147 |
}
|
| 148 |
code { background: var(--line); padding: 1px 6px; border-radius: 4px; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
html, body {
|
| 17 |
margin: 0; padding: 0; background: var(--bg); color: var(--text);
|
| 18 |
font: 14px ui-monospace, SFMono-Regular, Menlo, monospace;
|
| 19 |
+
min-height: 100%;
|
| 20 |
}
|
| 21 |
+
body { display: flex; flex-direction: column; min-height: 100vh; }
|
| 22 |
|
| 23 |
header {
|
| 24 |
display: flex; align-items: center; gap: 16px;
|
| 25 |
padding: 12px 20px; border-bottom: 1px solid var(--line);
|
| 26 |
+
flex-wrap: wrap;
|
| 27 |
}
|
| 28 |
.title { display: flex; align-items: center; gap: 12px; }
|
| 29 |
.logo { font-size: 18px; }
|
|
|
|
| 38 |
.badge.red { background: rgba(255, 107, 107, 0.18); color: var(--red); }
|
| 39 |
.meta { color: var(--muted); font-size: 12px; }
|
| 40 |
|
| 41 |
+
.cta-row {
|
| 42 |
+
margin-left: auto; display: flex; gap: 8px;
|
| 43 |
+
}
|
| 44 |
+
.cta {
|
| 45 |
+
display: inline-flex; align-items: center; gap: 6px;
|
| 46 |
+
padding: 5px 10px; border-radius: 6px;
|
| 47 |
+
background: var(--line); color: var(--accent-2);
|
| 48 |
+
font-size: 11px; text-decoration: none;
|
| 49 |
+
border: 1px solid transparent;
|
| 50 |
+
transition: border-color 0.1s;
|
| 51 |
+
}
|
| 52 |
+
.cta:hover { border-color: var(--accent); }
|
| 53 |
+
.cta span { font-size: 12px; }
|
| 54 |
+
|
| 55 |
.hero {
|
| 56 |
+
padding: 14px 20px;
|
| 57 |
+
background: linear-gradient(180deg, rgba(124,93,255,0.10), transparent);
|
| 58 |
border-bottom: 1px solid var(--line);
|
| 59 |
+
display: grid; grid-template-columns: 1.4fr 1fr; gap: 24px; align-items: center;
|
| 60 |
+
}
|
| 61 |
+
.hero-text p { margin: 0; color: var(--muted); line-height: 1.6; }
|
| 62 |
+
.hero-text strong { color: var(--text); }
|
| 63 |
+
.hero-text em { color: var(--accent-2); font-style: normal; font-weight: 600; }
|
| 64 |
+
|
| 65 |
+
.diagram {
|
| 66 |
+
display: flex; flex-direction: column; align-items: center; gap: 2px;
|
| 67 |
+
font-size: 11px;
|
| 68 |
+
}
|
| 69 |
+
.diagram-input {
|
| 70 |
+
padding: 4px 14px; border: 1px dashed var(--muted); border-radius: 6px;
|
| 71 |
+
color: var(--text);
|
| 72 |
+
}
|
| 73 |
+
.diagram-arrow { color: var(--muted); font-size: 14px; line-height: 1; }
|
| 74 |
+
.diagram-server {
|
| 75 |
+
padding: 6px 14px; border: 1px solid var(--accent); border-radius: 6px;
|
| 76 |
+
color: var(--text); background: rgba(124,93,255,0.10);
|
| 77 |
+
font-size: 11px;
|
| 78 |
+
}
|
| 79 |
+
.diagram-server code {
|
| 80 |
+
background: transparent; color: var(--accent-2);
|
| 81 |
+
padding: 0;
|
| 82 |
+
}
|
| 83 |
+
.diagram-arrows {
|
| 84 |
+
display: flex; gap: 80px; color: var(--muted); font-size: 14px;
|
| 85 |
+
line-height: 1;
|
| 86 |
+
}
|
| 87 |
+
.diagram-models {
|
| 88 |
+
display: flex; gap: 10px;
|
| 89 |
+
}
|
| 90 |
+
.diagram-box {
|
| 91 |
+
padding: 6px 10px; border: 1px solid var(--line); border-radius: 6px;
|
| 92 |
+
background: var(--panel); color: var(--text); text-align: center;
|
| 93 |
+
min-width: 110px; font-weight: 600;
|
| 94 |
+
}
|
| 95 |
+
.diagram-box span {
|
| 96 |
+
display: block; color: var(--muted); font-weight: normal;
|
| 97 |
+
font-size: 9px; margin-top: 2px;
|
| 98 |
}
|
| 99 |
+
.diagram-recognition { border-color: rgba(98,182,255,0.4); }
|
| 100 |
+
.diagram-structured { border-color: rgba(200,156,255,0.4); }
|
| 101 |
+
.diagram-ner { border-color: rgba(95,210,139,0.4); }
|
| 102 |
+
|
| 103 |
+
.why-sie {
|
| 104 |
+
padding: 12px 20px; border-bottom: 1px solid var(--line);
|
| 105 |
+
background: rgba(124,93,255,0.04);
|
| 106 |
+
}
|
| 107 |
+
.why-sie h3 {
|
| 108 |
+
margin: 0 0 6px 0; font-size: 11px; letter-spacing: 0.6px;
|
| 109 |
+
text-transform: uppercase; color: var(--accent); font-weight: 600;
|
| 110 |
+
}
|
| 111 |
+
.why-sie p { margin: 0; color: var(--muted); line-height: 1.6; max-width: 1100px; }
|
| 112 |
|
| 113 |
main {
|
| 114 |
flex: 1; display: grid;
|
| 115 |
grid-template-columns: 0.95fr 1.4fr 1.2fr;
|
| 116 |
gap: 12px; padding: 12px 20px; overflow: hidden;
|
| 117 |
+
min-height: 480px;
|
| 118 |
}
|
| 119 |
|
| 120 |
.panel {
|
|
|
|
| 140 |
flex: 1; overflow: auto; padding: 12px 14px; margin: 0;
|
| 141 |
}
|
| 142 |
|
| 143 |
+
.sdk-snippet {
|
| 144 |
+
border-bottom: 1px solid var(--line);
|
| 145 |
+
background: rgba(0,0,0,0.2);
|
| 146 |
+
font-size: 11px;
|
| 147 |
+
}
|
| 148 |
+
.sdk-snippet summary {
|
| 149 |
+
padding: 6px 14px; cursor: pointer;
|
| 150 |
+
color: var(--accent-2); user-select: none;
|
| 151 |
+
font-weight: 500;
|
| 152 |
+
}
|
| 153 |
+
.sdk-snippet summary::marker { color: var(--accent-2); }
|
| 154 |
+
.sdk-snippet pre {
|
| 155 |
+
margin: 0; padding: 8px 14px 10px;
|
| 156 |
+
white-space: pre-wrap; word-break: break-word;
|
| 157 |
+
color: var(--text); font-size: 11px; line-height: 1.45;
|
| 158 |
+
}
|
| 159 |
+
.sdk-snippet code .arg { color: var(--magenta); }
|
| 160 |
+
.sdk-snippet code .str { color: var(--green); }
|
| 161 |
+
.sdk-snippet code .com { color: var(--muted); }
|
| 162 |
+
|
| 163 |
.meta-row {
|
| 164 |
padding: 10px 14px; border-bottom: 1px solid var(--line);
|
| 165 |
font-size: 11px; color: var(--muted);
|
|
|
|
| 233 |
display: flex; justify-content: space-between; gap: 12px;
|
| 234 |
}
|
| 235 |
code { background: var(--line); padding: 1px 6px; border-radius: 4px; }
|
| 236 |
+
|
| 237 |
+
@media (max-width: 900px) {
|
| 238 |
+
.hero { grid-template-columns: 1fr; }
|
| 239 |
+
main { grid-template-columns: 1fr; }
|
| 240 |
+
.cta-row { margin-left: 0; }
|
| 241 |
+
}
|