Sandbox
Render raw HTML, CSS, and JavaScript in an isolated iframe. The sandbox handles isolation and optional framework loading — useful for live examples, embedded widgets, and framework demos.
Basic usage
Write HTML directly inside the sandbox tag. The content is rendered in an iframe, completely isolated from the rest of the page.
{% sandbox %}
<style>
.badge {
display: inline-block;
padding: 4px 12px;
background: #7C3AED;
color: white;
border-radius: 9999px;
font-family: system-ui;
font-size: 14px;
}
</style>
<span class="badge">Live HTML</span>
{% /sandbox %}<rf-sandbox data-rune="sandbox" data-source-content="<style>
.badge {
display: inline-block;
padding: 4px 12px;
background: #7C3AED;
color: white;
border-radius: 9999px;
font-family: system-ui;
font-size: 14px;
}
</style>
<span class="badge">Live HTML</span>" data-height="auto" data-security-mode="trusted" data-allow-js="true">
<template data-content="fallback">
<pre data-language="html">
<code data-language="html"><style>
.badge {
display: inline-block;
padding: 4px 12px;
background: #7C3AED;
color: white;
border-radius: 9999px;
font-family: system-ui;
font-size: 14px;
}
</style>
<span class="badge">Live HTML</span></code>
</pre>
</template>
<template data-content="source"><style>
.badge {
display: inline-block;
padding: 4px 12px;
background: #7C3AED;
color: white;
border-radius: 9999px;
font-family: system-ui;
font-size: 14px;
}
</style>
<span class="badge">Live HTML</span></template>
</rf-sandbox><style>
.badge {
display: inline-block;
padding: 4px 12px;
background: #7C3AED;
color: white;
border-radius: 9999px;
font-family: system-ui;
font-size: 14px;
}
</style>
<span class="badge">Live HTML</span><style>
.badge {
display: inline-block;
padding: 4px 12px;
background: #7C3AED;
color: white;
border-radius: 9999px;
font-family: system-ui;
font-size: 14px;
}
</style>
<span class="badge">Live HTML</span><rf-sandbox class="rf-sandbox" data-source-content="<style>
.badge {
display: inline-block;
padding: 4px 12px;
background: #7C3AED;
color: white;
border-radius: 9999px;
font-family: system-ui;
font-size: 14px;
}
</style>
<span class="badge">Live HTML</span>" data-height="auto" data-security-mode="trusted" data-allow-js="true" data-rune="sandbox" data-density="compact">
<template data-content="fallback">
<pre data-language="html"><code data-language="html"><style>
.badge {
display: inline-block;
padding: 4px 12px;
background: #7C3AED;
color: white;
border-radius: 9999px;
font-family: system-ui;
font-size: 14px;
}
</style>
<span class="badge">Live HTML</span></code></pre>
</template>
<template data-content="source"><style>
.badge {
display: inline-block;
padding: 4px 12px;
background: #7C3AED;
color: white;
border-radius: 9999px;
font-family: system-ui;
font-size: 14px;
}
</style>
<span class="badge">Live HTML</span></template>
<meta data-field="design-tokens" content="{"fonts":[{"role":"heading","family":"Inter","weights":[400,600,700],"category":"sans-serif"},{"role":"body","family":"Source Sans Pro","weights":[400,600],"category":"sans-serif"},{"role":"mono","family":"Fira Code","weights":[400],"category":"monospace"}],"colors":[{"name":"Primary","value":"#2563EB","group":"Brand"},{"name":"Secondary","value":"#7C3AED","group":"Brand"},{"name":"Accent","value":"#F59E0B","group":"Brand"},{"name":"Gray","value":"#F9FAFB","group":"Neutral"},{"name":"Gray","value":"#E5E7EB","group":"Neutral"},{"name":"Gray","value":"#9CA3AF","group":"Neutral"},{"name":"Gray","value":"#374151","group":"Neutral"},{"name":"Gray","value":"#111827","group":"Neutral"}],"spacing":{"unit":"4px","scale":["4","8","12","16","24","32","48","64"]},"radii":[{"name":"sm","value":"4px"},{"name":"md","value":"8px"},{"name":"lg","value":"12px"},{"name":"full","value":"9999px"}]}" />
</rf-sandbox>Framework presets
The framework attribute loads a CSS framework from CDN automatically.
{% sandbox framework="tailwind" %}
<div class="flex gap-3 p-4">
<button class="bg-purple-600 text-white px-4 py-2 rounded-lg hover:bg-purple-700 transition-colors">
Primary
</button>
<button class="bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-200 px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
Secondary
</button>
</div>
{% /sandbox %}<rf-sandbox data-rune="sandbox" data-source-content="<div class="flex gap-3 p-4">
<button class="bg-purple-600 text-white px-4 py-2 rounded-lg hover:bg-purple-700 transition-colors">
Primary
</button>
<button class="bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-200 px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
Secondary
</button>
</div>" data-framework="tailwind" data-height="auto" data-security-mode="trusted" data-allow-js="true">
<template data-content="fallback">
<pre data-language="html">
<code data-language="html"><div class="flex gap-3 p-4">
<button class="bg-purple-600 text-white px-4 py-2 rounded-lg hover:bg-purple-700 transition-colors">
Primary
</button>
<button class="bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-200 px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
Secondary
</button>
</div></code>
</pre>
</template>
<template data-content="source"><div class="flex gap-3 p-4">
<button class="bg-purple-600 text-white px-4 py-2 rounded-lg hover:bg-purple-700 transition-colors">
Primary
</button>
<button class="bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-200 px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
Secondary
</button>
</div></template>
</rf-sandbox><div class="flex gap-3 p-4">
<button class="bg-purple-600 text-white px-4 py-2 rounded-lg hover:bg-purple-700 transition-colors">
Primary
</button>
<button class="bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-200 px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
Secondary
</button>
</div><div class="flex gap-3 p-4">
<button class="bg-purple-600 text-white px-4 py-2 rounded-lg hover:bg-purple-700 transition-colors">
Primary
</button>
<button class="bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-200 px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
Secondary
</button>
</div><rf-sandbox class="rf-sandbox" data-source-content="<div class="flex gap-3 p-4">
<button class="bg-purple-600 text-white px-4 py-2 rounded-lg hover:bg-purple-700 transition-colors">
Primary
</button>
<button class="bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-200 px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
Secondary
</button>
</div>" data-framework="tailwind" data-height="auto" data-security-mode="trusted" data-allow-js="true" data-rune="sandbox" data-density="compact">
<template data-content="fallback">
<pre data-language="html"><code data-language="html"><div class="flex gap-3 p-4">
<button class="bg-purple-600 text-white px-4 py-2 rounded-lg hover:bg-purple-700 transition-colors">
Primary
</button>
<button class="bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-200 px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
Secondary
</button>
</div></code></pre>
</template>
<template data-content="source"><div class="flex gap-3 p-4">
<button class="bg-purple-600 text-white px-4 py-2 rounded-lg hover:bg-purple-700 transition-colors">
Primary
</button>
<button class="bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-200 px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
Secondary
</button>
</div></template>
<meta data-field="design-tokens" content="{"fonts":[{"role":"heading","family":"Inter","weights":[400,600,700],"category":"sans-serif"},{"role":"body","family":"Source Sans Pro","weights":[400,600],"category":"sans-serif"},{"role":"mono","family":"Fira Code","weights":[400],"category":"monospace"}],"colors":[{"name":"Primary","value":"#2563EB","group":"Brand"},{"name":"Secondary","value":"#7C3AED","group":"Brand"},{"name":"Accent","value":"#F59E0B","group":"Brand"},{"name":"Gray","value":"#F9FAFB","group":"Neutral"},{"name":"Gray","value":"#E5E7EB","group":"Neutral"},{"name":"Gray","value":"#9CA3AF","group":"Neutral"},{"name":"Gray","value":"#374151","group":"Neutral"},{"name":"Gray","value":"#111827","group":"Neutral"}],"spacing":{"unit":"4px","scale":["4","8","12","16","24","32","48","64"]},"radii":[{"name":"sm","value":"4px"},{"name":"md","value":"8px"},{"name":"lg","value":"12px"},{"name":"full","value":"9999px"}]}" />
</rf-sandbox>Available presets:
| Preset | What's loaded |
|---|---|
tailwind | Tailwind Play CDN |
bootstrap | Bootstrap 5 CSS |
bulma | Bulma CSS |
pico | Pico CSS |
Custom dependencies
Load any script or stylesheet by URL with the dependencies attribute.
{% sandbox dependencies="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css" %}
<main class="container">
<article>
<h2>Pico CSS card</h2>
<p>Loaded via the dependencies attribute.</p>
<button>Click me</button>
</article>
</main>
{% /sandbox %}<rf-sandbox data-rune="sandbox" data-source-content="<main class="container">
<article>
<h2>Pico CSS card</h2>
<p>Loaded via the dependencies attribute.</p>
<button>Click me</button>
</article>
</main>" data-dependencies="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css" data-height="auto" data-security-mode="trusted" data-allow-js="true">
<template data-content="fallback">
<pre data-language="html">
<code data-language="html"><main class="container">
<article>
<h2>Pico CSS card</h2>
<p>Loaded via the dependencies attribute.</p>
<button>Click me</button>
</article>
</main></code>
</pre>
</template>
<template data-content="source"><main class="container">
<article>
<h2>Pico CSS card</h2>
<p>Loaded via the dependencies attribute.</p>
<button>Click me</button>
</article>
</main></template>
</rf-sandbox><main class="container">
<article>
<h2>Pico CSS card</h2>
<p>Loaded via the dependencies attribute.</p>
<button>Click me</button>
</article>
</main><main class="container">
<article>
<h2>Pico CSS card</h2>
<p>Loaded via the dependencies attribute.</p>
<button>Click me</button>
</article>
</main><rf-sandbox class="rf-sandbox" data-source-content="<main class="container">
<article>
<h2>Pico CSS card</h2>
<p>Loaded via the dependencies attribute.</p>
<button>Click me</button>
</article>
</main>" data-dependencies="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css" data-height="auto" data-security-mode="trusted" data-allow-js="true" data-rune="sandbox" data-density="compact">
<template data-content="fallback">
<pre data-language="html"><code data-language="html"><main class="container">
<article>
<h2>Pico CSS card</h2>
<p>Loaded via the dependencies attribute.</p>
<button>Click me</button>
</article>
</main></code></pre>
</template>
<template data-content="source"><main class="container">
<article>
<h2>Pico CSS card</h2>
<p>Loaded via the dependencies attribute.</p>
<button>Click me</button>
</article>
</main></template>
<meta data-field="design-tokens" content="{"fonts":[{"role":"heading","family":"Inter","weights":[400,600,700],"category":"sans-serif"},{"role":"body","family":"Source Sans Pro","weights":[400,600],"category":"sans-serif"},{"role":"mono","family":"Fira Code","weights":[400],"category":"monospace"}],"colors":[{"name":"Primary","value":"#2563EB","group":"Brand"},{"name":"Secondary","value":"#7C3AED","group":"Brand"},{"name":"Accent","value":"#F59E0B","group":"Brand"},{"name":"Gray","value":"#F9FAFB","group":"Neutral"},{"name":"Gray","value":"#E5E7EB","group":"Neutral"},{"name":"Gray","value":"#9CA3AF","group":"Neutral"},{"name":"Gray","value":"#374151","group":"Neutral"},{"name":"Gray","value":"#111827","group":"Neutral"}],"spacing":{"unit":"4px","scale":["4","8","12","16","24","32","48","64"]},"radii":[{"name":"sm","value":"4px"},{"name":"md","value":"8px"},{"name":"lg","value":"12px"},{"name":"full","value":"9999px"}]}" />
</rf-sandbox>With JavaScript
Scripts run inside the sandboxed iframe, fully isolated from the host page.
{% sandbox %}
<style>
.counter {
font-family: system-ui;
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
}
.counter button {
padding: 6px 16px;
border: 1px solid #d1d5db;
border-radius: 6px;
background: white;
cursor: pointer;
font-size: 16px;
}
.counter button:hover { background: #f3f4f6; }
.counter span { font-size: 24px; font-weight: 600; min-width: 3ch; text-align: center; }
@media (prefers-color-scheme: dark) {
.counter button { background: #374151; border-color: #4b5563; color: #f3f4f6; }
.counter button:hover { background: #4b5563; }
}
[data-theme="dark"] .counter button { background: #374151; border-color: #4b5563; color: #f3f4f6; }
[data-theme="dark"] .counter button:hover { background: #4b5563; }
</style>
<div class="counter">
<button onclick="update(-1)">−</button>
<span id="count">0</span>
<button onclick="update(1)">+</button>
</div>
<script>
let count = 0;
function update(delta) {
count += delta;
document.getElementById('count').textContent = count;
}
</script>
{% /sandbox %}<rf-sandbox data-rune="sandbox" data-source-content="<style>
.counter {
font-family: system-ui;
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
}
.counter button {
padding: 6px 16px;
border: 1px solid #d1d5db;
border-radius: 6px;
background: white;
cursor: pointer;
font-size: 16px;
}
.counter button:hover { background: #f3f4f6; }
.counter span { font-size: 24px; font-weight: 600; min-width: 3ch; text-align: center; }
@media (prefers-color-scheme: dark) {
.counter button { background: #374151; border-color: #4b5563; color: #f3f4f6; }
.counter button:hover { background: #4b5563; }
}
[data-theme="dark"] .counter button { background: #374151; border-color: #4b5563; color: #f3f4f6; }
[data-theme="dark"] .counter button:hover { background: #4b5563; }
</style>
<div class="counter">
<button onclick="update(-1)">−</button>
<span id="count">0</span>
<button onclick="update(1)">+</button>
</div>
<script>
let count = 0;
function update(delta) {
count += delta;
document.getElementById('count').textContent = count;
}
</script>" data-height="auto" data-security-mode="trusted" data-allow-js="true">
<template data-content="fallback">
<pre data-language="html">
<code data-language="html"><style>
.counter {
font-family: system-ui;
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
}
.counter button {
padding: 6px 16px;
border: 1px solid #d1d5db;
border-radius: 6px;
background: white;
cursor: pointer;
font-size: 16px;
}
.counter button:hover { background: #f3f4f6; }
.counter span { font-size: 24px; font-weight: 600; min-width: 3ch; text-align: center; }
@media (prefers-color-scheme: dark) {
.counter button { background: #374151; border-color: #4b5563; color: #f3f4f6; }
.counter button:hover { background: #4b5563; }
}
[data-theme="dark"] .counter button { background: #374151; border-color: #4b5563; color: #f3f4f6; }
[data-theme="dark"] .counter button:hover { background: #4b5563; }
</style>
<div class="counter">
<button onclick="update(-1)">−</button>
<span id="count">0</span>
<button onclick="update(1)">+</button>
</div>
<script>
let count = 0;
function update(delta) {
count += delta;
document.getElementById('count').textContent = count;
}
</script></code>
</pre>
</template>
<template data-content="source"><style>
.counter {
font-family: system-ui;
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
}
.counter button {
padding: 6px 16px;
border: 1px solid #d1d5db;
border-radius: 6px;
background: white;
cursor: pointer;
font-size: 16px;
}
.counter button:hover { background: #f3f4f6; }
.counter span { font-size: 24px; font-weight: 600; min-width: 3ch; text-align: center; }
@media (prefers-color-scheme: dark) {
.counter button { background: #374151; border-color: #4b5563; color: #f3f4f6; }
.counter button:hover { background: #4b5563; }
}
[data-theme="dark"] .counter button { background: #374151; border-color: #4b5563; color: #f3f4f6; }
[data-theme="dark"] .counter button:hover { background: #4b5563; }
</style>
<div class="counter">
<button onclick="update(-1)">−</button>
<span id="count">0</span>
<button onclick="update(1)">+</button>
</div>
<script>
let count = 0;
function update(delta) {
count += delta;
document.getElementById('count').textContent = count;
}
</script></template>
</rf-sandbox><style>
.counter {
font-family: system-ui;
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
}
.counter button {
padding: 6px 16px;
border: 1px solid #d1d5db;
border-radius: 6px;
background: white;
cursor: pointer;
font-size: 16px;
}
.counter button:hover { background: #f3f4f6; }
.counter span { font-size: 24px; font-weight: 600; min-width: 3ch; text-align: center; }
@media (prefers-color-scheme: dark) {
.counter button { background: #374151; border-color: #4b5563; color: #f3f4f6; }
.counter button:hover { background: #4b5563; }
}
[data-theme="dark"] .counter button { background: #374151; border-color: #4b5563; color: #f3f4f6; }
[data-theme="dark"] .counter button:hover { background: #4b5563; }
</style>
<div class="counter">
<button onclick="update(-1)">−</button>
<span id="count">0</span>
<button onclick="update(1)">+</button>
</div>
<script>
let count = 0;
function update(delta) {
count += delta;
document.getElementById('count').textContent = count;
}
</script><style>
.counter {
font-family: system-ui;
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
}
.counter button {
padding: 6px 16px;
border: 1px solid #d1d5db;
border-radius: 6px;
background: white;
cursor: pointer;
font-size: 16px;
}
.counter button:hover { background: #f3f4f6; }
.counter span { font-size: 24px; font-weight: 600; min-width: 3ch; text-align: center; }
@media (prefers-color-scheme: dark) {
.counter button { background: #374151; border-color: #4b5563; color: #f3f4f6; }
.counter button:hover { background: #4b5563; }
}
[data-theme="dark"] .counter button { background: #374151; border-color: #4b5563; color: #f3f4f6; }
[data-theme="dark"] .counter button:hover { background: #4b5563; }
</style>
<div class="counter">
<button onclick="update(-1)">−</button>
<span id="count">0</span>
<button onclick="update(1)">+</button>
</div>
<script>
let count = 0;
function update(delta) {
count += delta;
document.getElementById('count').textContent = count;
}
</script><rf-sandbox class="rf-sandbox" data-source-content="<style>
.counter {
font-family: system-ui;
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
}
.counter button {
padding: 6px 16px;
border: 1px solid #d1d5db;
border-radius: 6px;
background: white;
cursor: pointer;
font-size: 16px;
}
.counter button:hover { background: #f3f4f6; }
.counter span { font-size: 24px; font-weight: 600; min-width: 3ch; text-align: center; }
@media (prefers-color-scheme: dark) {
.counter button { background: #374151; border-color: #4b5563; color: #f3f4f6; }
.counter button:hover { background: #4b5563; }
}
[data-theme="dark"] .counter button { background: #374151; border-color: #4b5563; color: #f3f4f6; }
[data-theme="dark"] .counter button:hover { background: #4b5563; }
</style>
<div class="counter">
<button onclick="update(-1)">−</button>
<span id="count">0</span>
<button onclick="update(1)">+</button>
</div>
<script>
let count = 0;
function update(delta) {
count += delta;
document.getElementById('count').textContent = count;
}
</script>" data-height="auto" data-security-mode="trusted" data-allow-js="true" data-rune="sandbox" data-density="compact">
<template data-content="fallback">
<pre data-language="html"><code data-language="html"><style>
.counter {
font-family: system-ui;
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
}
.counter button {
padding: 6px 16px;
border: 1px solid #d1d5db;
border-radius: 6px;
background: white;
cursor: pointer;
font-size: 16px;
}
.counter button:hover { background: #f3f4f6; }
.counter span { font-size: 24px; font-weight: 600; min-width: 3ch; text-align: center; }
@media (prefers-color-scheme: dark) {
.counter button { background: #374151; border-color: #4b5563; color: #f3f4f6; }
.counter button:hover { background: #4b5563; }
}
[data-theme="dark"] .counter button { background: #374151; border-color: #4b5563; color: #f3f4f6; }
[data-theme="dark"] .counter button:hover { background: #4b5563; }
</style>
<div class="counter">
<button onclick="update(-1)">−</button>
<span id="count">0</span>
<button onclick="update(1)">+</button>
</div>
<script>
let count = 0;
function update(delta) {
count += delta;
document.getElementById('count').textContent = count;
}
</script></code></pre>
</template>
<template data-content="source"><style>
.counter {
font-family: system-ui;
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
}
.counter button {
padding: 6px 16px;
border: 1px solid #d1d5db;
border-radius: 6px;
background: white;
cursor: pointer;
font-size: 16px;
}
.counter button:hover { background: #f3f4f6; }
.counter span { font-size: 24px; font-weight: 600; min-width: 3ch; text-align: center; }
@media (prefers-color-scheme: dark) {
.counter button { background: #374151; border-color: #4b5563; color: #f3f4f6; }
.counter button:hover { background: #4b5563; }
}
[data-theme="dark"] .counter button { background: #374151; border-color: #4b5563; color: #f3f4f6; }
[data-theme="dark"] .counter button:hover { background: #4b5563; }
</style>
<div class="counter">
<button onclick="update(-1)">−</button>
<span id="count">0</span>
<button onclick="update(1)">+</button>
</div>
<script>
let count = 0;
function update(delta) {
count += delta;
document.getElementById('count').textContent = count;
}
</script></template>
<meta data-field="design-tokens" content="{"fonts":[{"role":"heading","family":"Inter","weights":[400,600,700],"category":"sans-serif"},{"role":"body","family":"Source Sans Pro","weights":[400,600],"category":"sans-serif"},{"role":"mono","family":"Fira Code","weights":[400],"category":"monospace"}],"colors":[{"name":"Primary","value":"#2563EB","group":"Brand"},{"name":"Secondary","value":"#7C3AED","group":"Brand"},{"name":"Accent","value":"#F59E0B","group":"Brand"},{"name":"Gray","value":"#F9FAFB","group":"Neutral"},{"name":"Gray","value":"#E5E7EB","group":"Neutral"},{"name":"Gray","value":"#9CA3AF","group":"Neutral"},{"name":"Gray","value":"#374151","group":"Neutral"},{"name":"Gray","value":"#111827","group":"Neutral"}],"spacing":{"unit":"4px","scale":["4","8","12","16","24","32","48","64"]},"radii":[{"name":"sm","value":"4px"},{"name":"md","value":"8px"},{"name":"lg","value":"12px"},{"name":"full","value":"9999px"}]}" />
</rf-sandbox>ES modules from a CDN
Because scripts run for real, you can import an ES module straight from a CDN inside a <script type="module"> — no bundler, no install. Here a version-pinned three.js draws a spinning cube:
{% sandbox height=300 %}
<style>html,body{height:100%;margin:0}canvas{display:block;width:100%;height:100%}</style>
<canvas id="c"></canvas>
<script type="module">
import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
const canvas = document.getElementById('c');
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(50, 1, 0.1, 100);
camera.position.z = 3;
const cube = new THREE.Mesh(
new THREE.BoxGeometry(1.4, 1.4, 1.4),
new THREE.MeshStandardMaterial({ color: 0xe15f80, flatShading: true }),
);
scene.add(cube);
const light = new THREE.DirectionalLight(0xffffff, 3);
light.position.set(2, 3, 4);
scene.add(light, new THREE.AmbientLight(0xffffff, 0.6));
function resize() {
const w = canvas.clientWidth, h = canvas.clientHeight;
renderer.setSize(w, h, false);
camera.aspect = w / h;
camera.updateProjectionMatrix();
}
resize();
(function loop() {
cube.rotation.x += 0.01;
cube.rotation.y += 0.013;
renderer.render(scene, camera);
requestAnimationFrame(loop);
})();
</script>
{% /sandbox %}<rf-sandbox data-rune="sandbox" data-source-content="<style>html,body{height:100%;margin:0}canvas{display:block;width:100%;height:100%}</style>
<canvas id="c"></canvas>
<script type="module">
import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
const canvas = document.getElementById('c');
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(50, 1, 0.1, 100);
camera.position.z = 3;
const cube = new THREE.Mesh(
new THREE.BoxGeometry(1.4, 1.4, 1.4),
new THREE.MeshStandardMaterial({ color: 0xe15f80, flatShading: true }),
);
scene.add(cube);
const light = new THREE.DirectionalLight(0xffffff, 3);
light.position.set(2, 3, 4);
scene.add(light, new THREE.AmbientLight(0xffffff, 0.6));
function resize() {
const w = canvas.clientWidth, h = canvas.clientHeight;
renderer.setSize(w, h, false);
camera.aspect = w / h;
camera.updateProjectionMatrix();
}
resize();
(function loop() {
cube.rotation.x += 0.01;
cube.rotation.y += 0.013;
renderer.render(scene, camera);
requestAnimationFrame(loop);
})();
</script>" data-height="300" data-security-mode="trusted" data-allow-js="true">
<template data-content="fallback">
<pre data-language="html">
<code data-language="html"><style>html,body{height:100%;margin:0}canvas{display:block;width:100%;height:100%}</style>
<canvas id="c"></canvas>
<script type="module">
import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
const canvas = document.getElementById('c');
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(50, 1, 0.1, 100);
camera.position.z = 3;
const cube = new THREE.Mesh(
new THREE.BoxGeometry(1.4, 1.4, 1.4),
new THREE.MeshStandardMaterial({ color: 0xe15f80, flatShading: true }),
);
scene.add(cube);
const light = new THREE.DirectionalLight(0xffffff, 3);
light.position.set(2, 3, 4);
scene.add(light, new THREE.AmbientLight(0xffffff, 0.6));
function resize() {
const w = canvas.clientWidth, h = canvas.clientHeight;
renderer.setSize(w, h, false);
camera.aspect = w / h;
camera.updateProjectionMatrix();
}
resize();
(function loop() {
cube.rotation.x += 0.01;
cube.rotation.y += 0.013;
renderer.render(scene, camera);
requestAnimationFrame(loop);
})();
</script></code>
</pre>
</template>
<template data-content="source"><style>html,body{height:100%;margin:0}canvas{display:block;width:100%;height:100%}</style>
<canvas id="c"></canvas>
<script type="module">
import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
const canvas = document.getElementById('c');
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(50, 1, 0.1, 100);
camera.position.z = 3;
const cube = new THREE.Mesh(
new THREE.BoxGeometry(1.4, 1.4, 1.4),
new THREE.MeshStandardMaterial({ color: 0xe15f80, flatShading: true }),
);
scene.add(cube);
const light = new THREE.DirectionalLight(0xffffff, 3);
light.position.set(2, 3, 4);
scene.add(light, new THREE.AmbientLight(0xffffff, 0.6));
function resize() {
const w = canvas.clientWidth, h = canvas.clientHeight;
renderer.setSize(w, h, false);
camera.aspect = w / h;
camera.updateProjectionMatrix();
}
resize();
(function loop() {
cube.rotation.x += 0.01;
cube.rotation.y += 0.013;
renderer.render(scene, camera);
requestAnimationFrame(loop);
})();
</script></template>
</rf-sandbox><style>html,body{height:100%;margin:0}canvas{display:block;width:100%;height:100%}</style>
<canvas id="c"></canvas>
<script type="module">
import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
const canvas = document.getElementById('c');
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(50, 1, 0.1, 100);
camera.position.z = 3;
const cube = new THREE.Mesh(
new THREE.BoxGeometry(1.4, 1.4, 1.4),
new THREE.MeshStandardMaterial({ color: 0xe15f80, flatShading: true }),
);
scene.add(cube);
const light = new THREE.DirectionalLight(0xffffff, 3);
light.position.set(2, 3, 4);
scene.add(light, new THREE.AmbientLight(0xffffff, 0.6));
function resize() {
const w = canvas.clientWidth, h = canvas.clientHeight;
renderer.setSize(w, h, false);
camera.aspect = w / h;
camera.updateProjectionMatrix();
}
resize();
(function loop() {
cube.rotation.x += 0.01;
cube.rotation.y += 0.013;
renderer.render(scene, camera);
requestAnimationFrame(loop);
})();
</script><style>html,body{height:100%;margin:0}canvas{display:block;width:100%;height:100%}</style>
<canvas id="c"></canvas>
<script type="module">
import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
const canvas = document.getElementById('c');
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(50, 1, 0.1, 100);
camera.position.z = 3;
const cube = new THREE.Mesh(
new THREE.BoxGeometry(1.4, 1.4, 1.4),
new THREE.MeshStandardMaterial({ color: 0xe15f80, flatShading: true }),
);
scene.add(cube);
const light = new THREE.DirectionalLight(0xffffff, 3);
light.position.set(2, 3, 4);
scene.add(light, new THREE.AmbientLight(0xffffff, 0.6));
function resize() {
const w = canvas.clientWidth, h = canvas.clientHeight;
renderer.setSize(w, h, false);
camera.aspect = w / h;
camera.updateProjectionMatrix();
}
resize();
(function loop() {
cube.rotation.x += 0.01;
cube.rotation.y += 0.013;
renderer.render(scene, camera);
requestAnimationFrame(loop);
})();
</script><rf-sandbox class="rf-sandbox" data-source-content="<style>html,body{height:100%;margin:0}canvas{display:block;width:100%;height:100%}</style>
<canvas id="c"></canvas>
<script type="module">
import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
const canvas = document.getElementById('c');
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(50, 1, 0.1, 100);
camera.position.z = 3;
const cube = new THREE.Mesh(
new THREE.BoxGeometry(1.4, 1.4, 1.4),
new THREE.MeshStandardMaterial({ color: 0xe15f80, flatShading: true }),
);
scene.add(cube);
const light = new THREE.DirectionalLight(0xffffff, 3);
light.position.set(2, 3, 4);
scene.add(light, new THREE.AmbientLight(0xffffff, 0.6));
function resize() {
const w = canvas.clientWidth, h = canvas.clientHeight;
renderer.setSize(w, h, false);
camera.aspect = w / h;
camera.updateProjectionMatrix();
}
resize();
(function loop() {
cube.rotation.x += 0.01;
cube.rotation.y += 0.013;
renderer.render(scene, camera);
requestAnimationFrame(loop);
})();
</script>" data-height="300" data-security-mode="trusted" data-allow-js="true" data-rune="sandbox" data-density="compact">
<template data-content="fallback">
<pre data-language="html"><code data-language="html"><style>html,body{height:100%;margin:0}canvas{display:block;width:100%;height:100%}</style>
<canvas id="c"></canvas>
<script type="module">
import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
const canvas = document.getElementById('c');
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(50, 1, 0.1, 100);
camera.position.z = 3;
const cube = new THREE.Mesh(
new THREE.BoxGeometry(1.4, 1.4, 1.4),
new THREE.MeshStandardMaterial({ color: 0xe15f80, flatShading: true }),
);
scene.add(cube);
const light = new THREE.DirectionalLight(0xffffff, 3);
light.position.set(2, 3, 4);
scene.add(light, new THREE.AmbientLight(0xffffff, 0.6));
function resize() {
const w = canvas.clientWidth, h = canvas.clientHeight;
renderer.setSize(w, h, false);
camera.aspect = w / h;
camera.updateProjectionMatrix();
}
resize();
(function loop() {
cube.rotation.x += 0.01;
cube.rotation.y += 0.013;
renderer.render(scene, camera);
requestAnimationFrame(loop);
})();
</script></code></pre>
</template>
<template data-content="source"><style>html,body{height:100%;margin:0}canvas{display:block;width:100%;height:100%}</style>
<canvas id="c"></canvas>
<script type="module">
import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
const canvas = document.getElementById('c');
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(50, 1, 0.1, 100);
camera.position.z = 3;
const cube = new THREE.Mesh(
new THREE.BoxGeometry(1.4, 1.4, 1.4),
new THREE.MeshStandardMaterial({ color: 0xe15f80, flatShading: true }),
);
scene.add(cube);
const light = new THREE.DirectionalLight(0xffffff, 3);
light.position.set(2, 3, 4);
scene.add(light, new THREE.AmbientLight(0xffffff, 0.6));
function resize() {
const w = canvas.clientWidth, h = canvas.clientHeight;
renderer.setSize(w, h, false);
camera.aspect = w / h;
camera.updateProjectionMatrix();
}
resize();
(function loop() {
cube.rotation.x += 0.01;
cube.rotation.y += 0.013;
renderer.render(scene, camera);
requestAnimationFrame(loop);
})();
</script></template>
<meta data-field="design-tokens" content="{"fonts":[{"role":"heading","family":"Inter","weights":[400,600,700],"category":"sans-serif"},{"role":"body","family":"Source Sans Pro","weights":[400,600],"category":"sans-serif"},{"role":"mono","family":"Fira Code","weights":[400],"category":"monospace"}],"colors":[{"name":"Primary","value":"#2563EB","group":"Brand"},{"name":"Secondary","value":"#7C3AED","group":"Brand"},{"name":"Accent","value":"#F59E0B","group":"Brand"},{"name":"Gray","value":"#F9FAFB","group":"Neutral"},{"name":"Gray","value":"#E5E7EB","group":"Neutral"},{"name":"Gray","value":"#9CA3AF","group":"Neutral"},{"name":"Gray","value":"#374151","group":"Neutral"},{"name":"Gray","value":"#111827","group":"Neutral"}],"spacing":{"unit":"4px","scale":["4","8","12","16","24","32","48","64"]},"radii":[{"name":"sm","value":"4px"},{"name":"md","value":"8px"},{"name":"lg","value":"12px"},{"name":"full","value":"9999px"}]}" />
</rf-sandbox>Pin the version for reproducibility; for production also honour prefers-reduced-motion and provide a fallback — see the polished three.js scene in Media guests.
Host-owned height — height="fill"
height normally takes pixels (or auto-sizes to the content). Set height="fill" when a host element owns the height — the iframe is pinned to 100% of the host and the auto-resize negotiation is disabled. The flagship case is a sandbox serving as a cover backdrop (a hero or card with media-position="cover"): there the engine applies fill automatically, so you only need it explicitly for custom fixed-height containers.
{% sandbox src="wireframe-waves" height="fill" /%}
Deferred activation — keep heavy sandboxes off the critical path
A sandbox is eager by default: its iframe and every dependency download as the page renders. That's fine for a small demo, but a heavy scene — a three.js render, a large framework playground — shouldn't tax a perf-sensitive page (a landing page, a long article) before the visitor has even scrolled to it.
Set activation to defer the mount. The sandbox shows a poster (and an explicit Run control) in the iframe's place; nothing downloads until it activates:
visible— mounts when scrolled into view (viaIntersectionObserver). Best for below-the-fold scenes.click— mounts only when the visitor presses the control. Best for the heaviest cases.
{% sandbox activation="visible" poster="/img/scene-poster.png" height=400 %}
<canvas id="c"></canvas>
<script type="module">
import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
// … heavy scene; nothing here loads until the sandbox scrolls into view
</script>
{% /sandbox %}
Under prefers-reduced-motion, a non-eager sandbox does not auto-activate even in visible mode — the poster and Run control stay, so motion-sensitive visitors opt in deliberately. Eager sandboxes are unaffected.
Data binding — window.RF_DATA
A sandbox can be fed data from your registry. Set a data query — the same field-match grammar collection uses — and the build resolves it, serializes the result, and exposes it to the iframe as a frozen window.RF_DATA. Your code renders anything from your own content: the registry's third render target, after collection (HTML) and aggregate (SVG) — bring your own renderer.
| Attribute | Effect |
|---|---|
data | A registry query, e.g. type:page or type:work status:done |
data-shape | flat (default — a record array), tree (nested by parentUrl), or graph (nodes + SPEC-072 relationship edges) |
data-fields | Comma-separated data fields to project (keeps the payload lean) |
data-limit | Max records (default 500; over → truncated with a build warning) |
Here a data-shape="tree" binding feeds this site's own rune-section page tree to a three.js scene — a live, navigable 3D star-map: each section is a star and its pages orbit it as a little planetary system, nested by URL depth (drag to rotate, click a node to open it). It's a heavy WebGL scene, so it's set activation="visible" — the scene (and three.js) only loads once you scroll it into view:
{% sandbox src="sitemap-3d" data="type:page url:/runes/*" data-shape="tree" activation="visible" height=440 /%}<rf-sandbox data-rune="sandbox" data-source-content="<div data-source="HTML">
<style>
html, body { height: 100%; margin: 0; overflow: hidden; }
/* A window into space — a deep dark backdrop in both themes so the stars glow. */
body { background: radial-gradient(ellipse at 50% 45%, #1b1a24 0%, #0b0a10 70%); }
#c { display: block; width: 100%; height: 100%; cursor: grab; touch-action: none; }
#c:active { cursor: grabbing; }
#tip {
position: fixed; left: 0; top: 0; pointer-events: none; z-index: 2;
padding: 2px 9px; border-radius: 6px; opacity: 0;
font: 12px/1.4 system-ui, -apple-system, sans-serif;
background: rgba(20,18,23,.92); color: #f6f4ef; white-space: nowrap;
transform: translate(-50%, -160%); transition: opacity .12s;
box-shadow: 0 2px 10px rgba(0,0,0,.5);
}
#fallback {
display: none; height: 100%; box-sizing: border-box; padding: 1.5rem;
align-items: center; justify-content: center; text-align: center;
font: 14px/1.5 system-ui, -apple-system, sans-serif; color: #9a96a4;
}
</style>
<canvas id="c"></canvas>
<div id="tip"></div>
<div id="fallback">A 3D star-map of the page registry renders here — it needs WebGL and JavaScript. The page list below is the accessible equivalent.</div>
<script type="module">
const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
const canvas = document.getElementById('c');
const tip = document.getElementById('tip');
const showFallback = () => { canvas.style.display = 'none'; document.getElementById('fallback').style.display = 'flex'; };
// Deterministic 0..1 hash from a string (stable layout across renders).
const hash = (s) => { let h = 2166136261; for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 16777619); } return ((h >>> 0) % 100000) / 100000; };
try {
const DATA = window.RF_DATA;
if (!DATA || DATA.shape !== 'tree' || !Array.isArray(DATA.tree) || DATA.tree.length === 0) throw new Error('no tree');
const THREE = await import('https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js');
// ── Build node records (parent / children / depth / subtree size) ────────
const all = [];
function build(node, parent, depth) {
const rec = { url: node.url, title: (node.data && node.data.title) || node.url, depth, parent, children: [], size: 1, world: new THREE.Vector3() };
all.push(rec);
(node.children || []).forEach((c) => rec.children.push(build(c, rec, depth + 1)));
rec.size += rec.children.reduce((s, c) => s + c.size, 0);
return rec;
}
const roots = DATA.tree.map((r) => build(r, null, 0));
// ── Lay roots out on a golden-angle galaxy disc (big systems near the core)
const GA = Math.PI * (3 - Math.sqrt(5));
roots.slice().sort((a, b) => b.size - a.size).forEach((r, i) => {
const rad = Math.sqrt(i + 0.7) * 2.7;
const ang = i * GA;
r.basePos = new THREE.Vector3(Math.cos(ang) * rad, (hash(r.url) - 0.5) * 3, Math.sin(ang) * rad);
});
// ── Give every descendant an orbit around its parent (nested, shrinking) ─
function assignOrbits(rec) {
const kids = rec.children;
if (!kids.length) return;
const ring = (1.5 + kids.length * 0.16) / (1 + rec.depth * 0.7);
const h = hash(rec.url || 'core');
const axis = new THREE.Vector3(Math.sin(h * 6.283) * 0.7, 1, Math.cos(h * 6.283) * 0.7).normalize();
const dir = rec.depth % 2 ? -1 : 1;
kids.forEach((k, i) => {
k.orbit = { radius: ring, angle0: (i / kids.length) * 6.283 + h * 6.283, speed: dir * 0.45 / ring, axis };
assignOrbits(k);
});
}
roots.forEach(assignOrbits);
// ── Scene ────────────────────────────────────────────────────────────────
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(55, 1, 0.1, 400);
const group = new THREE.Group();
scene.add(group);
// Galactic core glow + the light that warms the disc.
group.add(new THREE.Mesh(new THREE.SphereGeometry(0.7, 24, 24), new THREE.MeshBasicMaterial({ color: 0xffd9a8 })));
group.add(new THREE.PointLight(0xffe6c0, 2.2, 0, 0.6));
scene.add(new THREE.AmbientLight(0x8088aa, 0.5));
// Nodes — emissive spheres; stars (roots) sized by subtree, planets shrink.
const geo = new THREE.SphereGeometry(1, 18, 18);
const meshes = [];
for (const rec of all) {
const isStar = !rec.parent;
const r = isStar ? 0.22 + Math.min(rec.size, 40) * 0.012 : Math.max(0.07, 0.18 / rec.depth);
const color = isStar
? new THREE.Color().setHSL(0.96 - Math.min(rec.size, 28) * 0.006, 0.72, 0.62)
: new THREE.Color().setHSL(0.55 + rec.depth * 0.05, 0.55, 0.62);
const mat = new THREE.MeshStandardMaterial({ color, emissive: color, emissiveIntensity: isStar ? 0.7 : 0.35, roughness: 0.5, metalness: 0.1 });
const m = new THREE.Mesh(geo, mat);
m.scale.setScalar(r);
m.userData = { rec, baseScale: r, twinkle: hash(rec.url) * 6.283 };
group.add(m);
meshes.push(m);
}
// Orbit edges (parent → child), positions refreshed each frame.
const edgePairs = all.filter((r) => r.parent).map((r) => [r, r.parent]);
const edgePos = new Float32Array(edgePairs.length * 6);
const edgeGeo = new THREE.BufferGeometry();
edgeGeo.setAttribute('position', new THREE.BufferAttribute(edgePos, 3));
group.add(new THREE.LineSegments(edgeGeo, new THREE.LineBasicMaterial({ color: 0x8a86ff, transparent: true, opacity: 0.13 })));
// Distant starfield backdrop.
const SF = 1400, sf = new Float32Array(SF * 3);
for (let i = 0; i < SF; i++) {
const rr = 60 + Math.random() * 120, th = Math.random() * 6.283, ph = Math.acos(2 * Math.random() - 1);
sf[i * 3] = rr * Math.sin(ph) * Math.cos(th); sf[i * 3 + 1] = rr * Math.cos(ph); sf[i * 3 + 2] = rr * Math.sin(ph) * Math.sin(th);
}
const sfGeo = new THREE.BufferGeometry(); sfGeo.setAttribute('position', new THREE.BufferAttribute(sf, 3));
scene.add(new THREE.Points(sfGeo, new THREE.PointsMaterial({ color: 0xc9c6e0, size: 0.5, sizeAttenuation: true, transparent: true, opacity: 0.6 })));
// ── Position update (top-down: roots fixed, children orbit parents) ───────
const _u = new THREE.Vector3(), _v = new THREE.Vector3();
const perp = (n, o) => { o.set(Math.abs(n.x) < 0.9 ? 1 : 0, Math.abs(n.x) < 0.9 ? 0 : 1, 0); return o.crossVectors(n, o).normalize(); };
function place(rec, t) {
if (!rec.parent) rec.world.copy(rec.basePos);
else {
const o = rec.orbit, a = o.angle0 + t * o.speed;
perp(o.axis, _u); _v.crossVectors(o.axis, _u);
rec.world.copy(rec.parent.world).addScaledVector(_u, Math.cos(a) * o.radius).addScaledVector(_v, Math.sin(a) * o.radius);
}
rec.children.forEach((c) => place(c, t));
}
// Camera framing from the root disc bounds.
const box = new THREE.Box3(); roots.forEach((r) => box.expandByPoint(r.basePos));
const sph = box.getBoundingSphere(new THREE.Sphere());
const view = { center: sph.center.clone(), dist: Math.max(8, sph.radius / Math.sin((camera.fov / 2) * Math.PI / 180) * 1.15) };
function resize() {
const w = canvas.clientWidth, h = canvas.clientHeight; if (!w || !h) return;
renderer.setSize(w, h, false); camera.aspect = w / h; camera.updateProjectionMatrix();
}
resize(); window.addEventListener('resize', resize);
// ── Controls: drag to rotate, gentle auto-spin (off for reduced motion) ───
let rotX = 0.5, rotY = 0.5, dragging = false, lx = 0, ly = 0;
canvas.addEventListener('pointerdown', (e) => { dragging = true; lx = e.clientX; ly = e.clientY; canvas.setPointerCapture(e.pointerId); });
canvas.addEventListener('pointerup', (e) => { dragging = false; canvas.releasePointerCapture?.(e.pointerId); });
canvas.addEventListener('pointermove', (e) => {
if (dragging) { rotY += (e.clientX - lx) * 0.005; rotX = Math.max(-1.3, Math.min(1.3, rotX + (e.clientY - ly) * 0.005)); lx = e.clientX; ly = e.clientY; }
hover(e);
});
// ── Hover tooltip + click-to-navigate ────────────────────────────────────
const ray = new THREE.Raycaster(); const ndc = new THREE.Vector2(); let hovered = null;
function pick(e) {
const r = canvas.getBoundingClientRect();
ndc.set(((e.clientX - r.left) / r.width) * 2 - 1, -((e.clientY - r.top) / r.height) * 2 + 1);
ray.setFromCamera(ndc, camera);
return ray.intersectObjects(meshes, false)[0]?.object ?? null;
}
function hover(e) {
const hit = pick(e);
hovered = hit;
if (hovered) { tip.textContent = hovered.userData.rec.title; tip.style.left = e.clientX + 'px'; tip.style.top = e.clientY + 'px'; tip.style.opacity = '1'; canvas.style.cursor = 'pointer'; }
else { tip.style.opacity = '0'; canvas.style.cursor = dragging ? 'grabbing' : 'grab'; }
}
canvas.addEventListener('click', (e) => {
const hit = pick(e);
if (hit?.userData?.rec?.url) { try { window.top.location.href = hit.userData.rec.url; } catch { window.open(hit.userData.rec.url, '_top'); } }
});
// ── Loop ──────────────────────────────────────────────────────────────────
const t0 = performance.now();
function frame(now) {
const t = reduce ? 0 : (now - t0) / 1000;
for (const r of roots) place(r, t);
for (const m of meshes) {
m.position.copy(m.userData.rec.world);
const tw = reduce ? 1 : 1 + Math.sin(t * 2 + m.userData.twinkle) * (m.userData.rec.parent ? 0 : 0.12);
m.scale.setScalar(m.userData.baseScale * (m === hovered ? 1.5 : 1) * tw);
}
edgePairs.forEach(([a, b], i) => { a.world.toArray(edgePos, i * 6); b.world.toArray(edgePos, i * 6 + 3); });
edgeGeo.attributes.position.needsUpdate = true;
if (!reduce && !dragging) rotY += 0.0016;
group.rotation.set(rotX, rotY, 0);
camera.position.set(view.center.x, view.center.y + view.dist * 0.18, view.center.z + view.dist);
camera.lookAt(view.center);
renderer.render(scene, camera);
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
} catch (err) {
showFallback();
}
</script>
</div>" data-height="440" data-source-origins="HTML sitemap-3d/index.html" data-security-mode="trusted" data-allow-js="true" data-activation="visible" data-rf-query="type:page url:/runes/*" data-rf-shape="tree">
<template data-content="fallback">
<pre data-language="html">
<code data-language="html"><div data-source="HTML">
<style>
html, body { height: 100%; margin: 0; overflow: hidden; }
/* A window into space — a deep dark backdrop in both themes so the stars glow. */
body { background: radial-gradient(ellipse at 50% 45%, #1b1a24 0%, #0b0a10 70%); }
#c { display: block; width: 100%; height: 100%; cursor: grab; touch-action: none; }
#c:active { cursor: grabbing; }
#tip {
position: fixed; left: 0; top: 0; pointer-events: none; z-index: 2;
padding: 2px 9px; border-radius: 6px; opacity: 0;
font: 12px/1.4 system-ui, -apple-system, sans-serif;
background: rgba(20,18,23,.92); color: #f6f4ef; white-space: nowrap;
transform: translate(-50%, -160%); transition: opacity .12s;
box-shadow: 0 2px 10px rgba(0,0,0,.5);
}
#fallback {
display: none; height: 100%; box-sizing: border-box; padding: 1.5rem;
align-items: center; justify-content: center; text-align: center;
font: 14px/1.5 system-ui, -apple-system, sans-serif; color: #9a96a4;
}
</style>
<canvas id="c"></canvas>
<div id="tip"></div>
<div id="fallback">A 3D star-map of the page registry renders here — it needs WebGL and JavaScript. The page list below is the accessible equivalent.</div>
<script type="module">
const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
const canvas = document.getElementById('c');
const tip = document.getElementById('tip');
const showFallback = () => { canvas.style.display = 'none'; document.getElementById('fallback').style.display = 'flex'; };
// Deterministic 0..1 hash from a string (stable layout across renders).
const hash = (s) => { let h = 2166136261; for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 16777619); } return ((h >>> 0) % 100000) / 100000; };
try {
const DATA = window.RF_DATA;
if (!DATA || DATA.shape !== 'tree' || !Array.isArray(DATA.tree) || DATA.tree.length === 0) throw new Error('no tree');
const THREE = await import('https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js');
// ── Build node records (parent / children / depth / subtree size) ────────
const all = [];
function build(node, parent, depth) {
const rec = { url: node.url, title: (node.data && node.data.title) || node.url, depth, parent, children: [], size: 1, world: new THREE.Vector3() };
all.push(rec);
(node.children || []).forEach((c) => rec.children.push(build(c, rec, depth + 1)));
rec.size += rec.children.reduce((s, c) => s + c.size, 0);
return rec;
}
const roots = DATA.tree.map((r) => build(r, null, 0));
// ── Lay roots out on a golden-angle galaxy disc (big systems near the core)
const GA = Math.PI * (3 - Math.sqrt(5));
roots.slice().sort((a, b) => b.size - a.size).forEach((r, i) => {
const rad = Math.sqrt(i + 0.7) * 2.7;
const ang = i * GA;
r.basePos = new THREE.Vector3(Math.cos(ang) * rad, (hash(r.url) - 0.5) * 3, Math.sin(ang) * rad);
});
// ── Give every descendant an orbit around its parent (nested, shrinking) ─
function assignOrbits(rec) {
const kids = rec.children;
if (!kids.length) return;
const ring = (1.5 + kids.length * 0.16) / (1 + rec.depth * 0.7);
const h = hash(rec.url || 'core');
const axis = new THREE.Vector3(Math.sin(h * 6.283) * 0.7, 1, Math.cos(h * 6.283) * 0.7).normalize();
const dir = rec.depth % 2 ? -1 : 1;
kids.forEach((k, i) => {
k.orbit = { radius: ring, angle0: (i / kids.length) * 6.283 + h * 6.283, speed: dir * 0.45 / ring, axis };
assignOrbits(k);
});
}
roots.forEach(assignOrbits);
// ── Scene ────────────────────────────────────────────────────────────────
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(55, 1, 0.1, 400);
const group = new THREE.Group();
scene.add(group);
// Galactic core glow + the light that warms the disc.
group.add(new THREE.Mesh(new THREE.SphereGeometry(0.7, 24, 24), new THREE.MeshBasicMaterial({ color: 0xffd9a8 })));
group.add(new THREE.PointLight(0xffe6c0, 2.2, 0, 0.6));
scene.add(new THREE.AmbientLight(0x8088aa, 0.5));
// Nodes — emissive spheres; stars (roots) sized by subtree, planets shrink.
const geo = new THREE.SphereGeometry(1, 18, 18);
const meshes = [];
for (const rec of all) {
const isStar = !rec.parent;
const r = isStar ? 0.22 + Math.min(rec.size, 40) * 0.012 : Math.max(0.07, 0.18 / rec.depth);
const color = isStar
? new THREE.Color().setHSL(0.96 - Math.min(rec.size, 28) * 0.006, 0.72, 0.62)
: new THREE.Color().setHSL(0.55 + rec.depth * 0.05, 0.55, 0.62);
const mat = new THREE.MeshStandardMaterial({ color, emissive: color, emissiveIntensity: isStar ? 0.7 : 0.35, roughness: 0.5, metalness: 0.1 });
const m = new THREE.Mesh(geo, mat);
m.scale.setScalar(r);
m.userData = { rec, baseScale: r, twinkle: hash(rec.url) * 6.283 };
group.add(m);
meshes.push(m);
}
// Orbit edges (parent → child), positions refreshed each frame.
const edgePairs = all.filter((r) => r.parent).map((r) => [r, r.parent]);
const edgePos = new Float32Array(edgePairs.length * 6);
const edgeGeo = new THREE.BufferGeometry();
edgeGeo.setAttribute('position', new THREE.BufferAttribute(edgePos, 3));
group.add(new THREE.LineSegments(edgeGeo, new THREE.LineBasicMaterial({ color: 0x8a86ff, transparent: true, opacity: 0.13 })));
// Distant starfield backdrop.
const SF = 1400, sf = new Float32Array(SF * 3);
for (let i = 0; i < SF; i++) {
const rr = 60 + Math.random() * 120, th = Math.random() * 6.283, ph = Math.acos(2 * Math.random() - 1);
sf[i * 3] = rr * Math.sin(ph) * Math.cos(th); sf[i * 3 + 1] = rr * Math.cos(ph); sf[i * 3 + 2] = rr * Math.sin(ph) * Math.sin(th);
}
const sfGeo = new THREE.BufferGeometry(); sfGeo.setAttribute('position', new THREE.BufferAttribute(sf, 3));
scene.add(new THREE.Points(sfGeo, new THREE.PointsMaterial({ color: 0xc9c6e0, size: 0.5, sizeAttenuation: true, transparent: true, opacity: 0.6 })));
// ── Position update (top-down: roots fixed, children orbit parents) ───────
const _u = new THREE.Vector3(), _v = new THREE.Vector3();
const perp = (n, o) => { o.set(Math.abs(n.x) < 0.9 ? 1 : 0, Math.abs(n.x) < 0.9 ? 0 : 1, 0); return o.crossVectors(n, o).normalize(); };
function place(rec, t) {
if (!rec.parent) rec.world.copy(rec.basePos);
else {
const o = rec.orbit, a = o.angle0 + t * o.speed;
perp(o.axis, _u); _v.crossVectors(o.axis, _u);
rec.world.copy(rec.parent.world).addScaledVector(_u, Math.cos(a) * o.radius).addScaledVector(_v, Math.sin(a) * o.radius);
}
rec.children.forEach((c) => place(c, t));
}
// Camera framing from the root disc bounds.
const box = new THREE.Box3(); roots.forEach((r) => box.expandByPoint(r.basePos));
const sph = box.getBoundingSphere(new THREE.Sphere());
const view = { center: sph.center.clone(), dist: Math.max(8, sph.radius / Math.sin((camera.fov / 2) * Math.PI / 180) * 1.15) };
function resize() {
const w = canvas.clientWidth, h = canvas.clientHeight; if (!w || !h) return;
renderer.setSize(w, h, false); camera.aspect = w / h; camera.updateProjectionMatrix();
}
resize(); window.addEventListener('resize', resize);
// ── Controls: drag to rotate, gentle auto-spin (off for reduced motion) ───
let rotX = 0.5, rotY = 0.5, dragging = false, lx = 0, ly = 0;
canvas.addEventListener('pointerdown', (e) => { dragging = true; lx = e.clientX; ly = e.clientY; canvas.setPointerCapture(e.pointerId); });
canvas.addEventListener('pointerup', (e) => { dragging = false; canvas.releasePointerCapture?.(e.pointerId); });
canvas.addEventListener('pointermove', (e) => {
if (dragging) { rotY += (e.clientX - lx) * 0.005; rotX = Math.max(-1.3, Math.min(1.3, rotX + (e.clientY - ly) * 0.005)); lx = e.clientX; ly = e.clientY; }
hover(e);
});
// ── Hover tooltip + click-to-navigate ────────────────────────────────────
const ray = new THREE.Raycaster(); const ndc = new THREE.Vector2(); let hovered = null;
function pick(e) {
const r = canvas.getBoundingClientRect();
ndc.set(((e.clientX - r.left) / r.width) * 2 - 1, -((e.clientY - r.top) / r.height) * 2 + 1);
ray.setFromCamera(ndc, camera);
return ray.intersectObjects(meshes, false)[0]?.object ?? null;
}
function hover(e) {
const hit = pick(e);
hovered = hit;
if (hovered) { tip.textContent = hovered.userData.rec.title; tip.style.left = e.clientX + 'px'; tip.style.top = e.clientY + 'px'; tip.style.opacity = '1'; canvas.style.cursor = 'pointer'; }
else { tip.style.opacity = '0'; canvas.style.cursor = dragging ? 'grabbing' : 'grab'; }
}
canvas.addEventListener('click', (e) => {
const hit = pick(e);
if (hit?.userData?.rec?.url) { try { window.top.location.href = hit.userData.rec.url; } catch { window.open(hit.userData.rec.url, '_top'); } }
});
// ── Loop ──────────────────────────────────────────────────────────────────
const t0 = performance.now();
function frame(now) {
const t = reduce ? 0 : (now - t0) / 1000;
for (const r of roots) place(r, t);
for (const m of meshes) {
m.position.copy(m.userData.rec.world);
const tw = reduce ? 1 : 1 + Math.sin(t * 2 + m.userData.twinkle) * (m.userData.rec.parent ? 0 : 0.12);
m.scale.setScalar(m.userData.baseScale * (m === hovered ? 1.5 : 1) * tw);
}
edgePairs.forEach(([a, b], i) => { a.world.toArray(edgePos, i * 6); b.world.toArray(edgePos, i * 6 + 3); });
edgeGeo.attributes.position.needsUpdate = true;
if (!reduce && !dragging) rotY += 0.0016;
group.rotation.set(rotX, rotY, 0);
camera.position.set(view.center.x, view.center.y + view.dist * 0.18, view.center.z + view.dist);
camera.lookAt(view.center);
renderer.render(scene, camera);
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
} catch (err) {
showFallback();
}
</script>
</div></code>
</pre>
</template>
<template data-content="source"><div data-source="HTML">
<style>
html, body { height: 100%; margin: 0; overflow: hidden; }
/* A window into space — a deep dark backdrop in both themes so the stars glow. */
body { background: radial-gradient(ellipse at 50% 45%, #1b1a24 0%, #0b0a10 70%); }
#c { display: block; width: 100%; height: 100%; cursor: grab; touch-action: none; }
#c:active { cursor: grabbing; }
#tip {
position: fixed; left: 0; top: 0; pointer-events: none; z-index: 2;
padding: 2px 9px; border-radius: 6px; opacity: 0;
font: 12px/1.4 system-ui, -apple-system, sans-serif;
background: rgba(20,18,23,.92); color: #f6f4ef; white-space: nowrap;
transform: translate(-50%, -160%); transition: opacity .12s;
box-shadow: 0 2px 10px rgba(0,0,0,.5);
}
#fallback {
display: none; height: 100%; box-sizing: border-box; padding: 1.5rem;
align-items: center; justify-content: center; text-align: center;
font: 14px/1.5 system-ui, -apple-system, sans-serif; color: #9a96a4;
}
</style>
<canvas id="c"></canvas>
<div id="tip"></div>
<div id="fallback">A 3D star-map of the page registry renders here — it needs WebGL and JavaScript. The page list below is the accessible equivalent.</div>
<script type="module">
const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
const canvas = document.getElementById('c');
const tip = document.getElementById('tip');
const showFallback = () => { canvas.style.display = 'none'; document.getElementById('fallback').style.display = 'flex'; };
// Deterministic 0..1 hash from a string (stable layout across renders).
const hash = (s) => { let h = 2166136261; for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 16777619); } return ((h >>> 0) % 100000) / 100000; };
try {
const DATA = window.RF_DATA;
if (!DATA || DATA.shape !== 'tree' || !Array.isArray(DATA.tree) || DATA.tree.length === 0) throw new Error('no tree');
const THREE = await import('https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js');
// ── Build node records (parent / children / depth / subtree size) ────────
const all = [];
function build(node, parent, depth) {
const rec = { url: node.url, title: (node.data && node.data.title) || node.url, depth, parent, children: [], size: 1, world: new THREE.Vector3() };
all.push(rec);
(node.children || []).forEach((c) => rec.children.push(build(c, rec, depth + 1)));
rec.size += rec.children.reduce((s, c) => s + c.size, 0);
return rec;
}
const roots = DATA.tree.map((r) => build(r, null, 0));
// ── Lay roots out on a golden-angle galaxy disc (big systems near the core)
const GA = Math.PI * (3 - Math.sqrt(5));
roots.slice().sort((a, b) => b.size - a.size).forEach((r, i) => {
const rad = Math.sqrt(i + 0.7) * 2.7;
const ang = i * GA;
r.basePos = new THREE.Vector3(Math.cos(ang) * rad, (hash(r.url) - 0.5) * 3, Math.sin(ang) * rad);
});
// ── Give every descendant an orbit around its parent (nested, shrinking) ─
function assignOrbits(rec) {
const kids = rec.children;
if (!kids.length) return;
const ring = (1.5 + kids.length * 0.16) / (1 + rec.depth * 0.7);
const h = hash(rec.url || 'core');
const axis = new THREE.Vector3(Math.sin(h * 6.283) * 0.7, 1, Math.cos(h * 6.283) * 0.7).normalize();
const dir = rec.depth % 2 ? -1 : 1;
kids.forEach((k, i) => {
k.orbit = { radius: ring, angle0: (i / kids.length) * 6.283 + h * 6.283, speed: dir * 0.45 / ring, axis };
assignOrbits(k);
});
}
roots.forEach(assignOrbits);
// ── Scene ────────────────────────────────────────────────────────────────
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(55, 1, 0.1, 400);
const group = new THREE.Group();
scene.add(group);
// Galactic core glow + the light that warms the disc.
group.add(new THREE.Mesh(new THREE.SphereGeometry(0.7, 24, 24), new THREE.MeshBasicMaterial({ color: 0xffd9a8 })));
group.add(new THREE.PointLight(0xffe6c0, 2.2, 0, 0.6));
scene.add(new THREE.AmbientLight(0x8088aa, 0.5));
// Nodes — emissive spheres; stars (roots) sized by subtree, planets shrink.
const geo = new THREE.SphereGeometry(1, 18, 18);
const meshes = [];
for (const rec of all) {
const isStar = !rec.parent;
const r = isStar ? 0.22 + Math.min(rec.size, 40) * 0.012 : Math.max(0.07, 0.18 / rec.depth);
const color = isStar
? new THREE.Color().setHSL(0.96 - Math.min(rec.size, 28) * 0.006, 0.72, 0.62)
: new THREE.Color().setHSL(0.55 + rec.depth * 0.05, 0.55, 0.62);
const mat = new THREE.MeshStandardMaterial({ color, emissive: color, emissiveIntensity: isStar ? 0.7 : 0.35, roughness: 0.5, metalness: 0.1 });
const m = new THREE.Mesh(geo, mat);
m.scale.setScalar(r);
m.userData = { rec, baseScale: r, twinkle: hash(rec.url) * 6.283 };
group.add(m);
meshes.push(m);
}
// Orbit edges (parent → child), positions refreshed each frame.
const edgePairs = all.filter((r) => r.parent).map((r) => [r, r.parent]);
const edgePos = new Float32Array(edgePairs.length * 6);
const edgeGeo = new THREE.BufferGeometry();
edgeGeo.setAttribute('position', new THREE.BufferAttribute(edgePos, 3));
group.add(new THREE.LineSegments(edgeGeo, new THREE.LineBasicMaterial({ color: 0x8a86ff, transparent: true, opacity: 0.13 })));
// Distant starfield backdrop.
const SF = 1400, sf = new Float32Array(SF * 3);
for (let i = 0; i < SF; i++) {
const rr = 60 + Math.random() * 120, th = Math.random() * 6.283, ph = Math.acos(2 * Math.random() - 1);
sf[i * 3] = rr * Math.sin(ph) * Math.cos(th); sf[i * 3 + 1] = rr * Math.cos(ph); sf[i * 3 + 2] = rr * Math.sin(ph) * Math.sin(th);
}
const sfGeo = new THREE.BufferGeometry(); sfGeo.setAttribute('position', new THREE.BufferAttribute(sf, 3));
scene.add(new THREE.Points(sfGeo, new THREE.PointsMaterial({ color: 0xc9c6e0, size: 0.5, sizeAttenuation: true, transparent: true, opacity: 0.6 })));
// ── Position update (top-down: roots fixed, children orbit parents) ───────
const _u = new THREE.Vector3(), _v = new THREE.Vector3();
const perp = (n, o) => { o.set(Math.abs(n.x) < 0.9 ? 1 : 0, Math.abs(n.x) < 0.9 ? 0 : 1, 0); return o.crossVectors(n, o).normalize(); };
function place(rec, t) {
if (!rec.parent) rec.world.copy(rec.basePos);
else {
const o = rec.orbit, a = o.angle0 + t * o.speed;
perp(o.axis, _u); _v.crossVectors(o.axis, _u);
rec.world.copy(rec.parent.world).addScaledVector(_u, Math.cos(a) * o.radius).addScaledVector(_v, Math.sin(a) * o.radius);
}
rec.children.forEach((c) => place(c, t));
}
// Camera framing from the root disc bounds.
const box = new THREE.Box3(); roots.forEach((r) => box.expandByPoint(r.basePos));
const sph = box.getBoundingSphere(new THREE.Sphere());
const view = { center: sph.center.clone(), dist: Math.max(8, sph.radius / Math.sin((camera.fov / 2) * Math.PI / 180) * 1.15) };
function resize() {
const w = canvas.clientWidth, h = canvas.clientHeight; if (!w || !h) return;
renderer.setSize(w, h, false); camera.aspect = w / h; camera.updateProjectionMatrix();
}
resize(); window.addEventListener('resize', resize);
// ── Controls: drag to rotate, gentle auto-spin (off for reduced motion) ───
let rotX = 0.5, rotY = 0.5, dragging = false, lx = 0, ly = 0;
canvas.addEventListener('pointerdown', (e) => { dragging = true; lx = e.clientX; ly = e.clientY; canvas.setPointerCapture(e.pointerId); });
canvas.addEventListener('pointerup', (e) => { dragging = false; canvas.releasePointerCapture?.(e.pointerId); });
canvas.addEventListener('pointermove', (e) => {
if (dragging) { rotY += (e.clientX - lx) * 0.005; rotX = Math.max(-1.3, Math.min(1.3, rotX + (e.clientY - ly) * 0.005)); lx = e.clientX; ly = e.clientY; }
hover(e);
});
// ── Hover tooltip + click-to-navigate ────────────────────────────────────
const ray = new THREE.Raycaster(); const ndc = new THREE.Vector2(); let hovered = null;
function pick(e) {
const r = canvas.getBoundingClientRect();
ndc.set(((e.clientX - r.left) / r.width) * 2 - 1, -((e.clientY - r.top) / r.height) * 2 + 1);
ray.setFromCamera(ndc, camera);
return ray.intersectObjects(meshes, false)[0]?.object ?? null;
}
function hover(e) {
const hit = pick(e);
hovered = hit;
if (hovered) { tip.textContent = hovered.userData.rec.title; tip.style.left = e.clientX + 'px'; tip.style.top = e.clientY + 'px'; tip.style.opacity = '1'; canvas.style.cursor = 'pointer'; }
else { tip.style.opacity = '0'; canvas.style.cursor = dragging ? 'grabbing' : 'grab'; }
}
canvas.addEventListener('click', (e) => {
const hit = pick(e);
if (hit?.userData?.rec?.url) { try { window.top.location.href = hit.userData.rec.url; } catch { window.open(hit.userData.rec.url, '_top'); } }
});
// ── Loop ──────────────────────────────────────────────────────────────────
const t0 = performance.now();
function frame(now) {
const t = reduce ? 0 : (now - t0) / 1000;
for (const r of roots) place(r, t);
for (const m of meshes) {
m.position.copy(m.userData.rec.world);
const tw = reduce ? 1 : 1 + Math.sin(t * 2 + m.userData.twinkle) * (m.userData.rec.parent ? 0 : 0.12);
m.scale.setScalar(m.userData.baseScale * (m === hovered ? 1.5 : 1) * tw);
}
edgePairs.forEach(([a, b], i) => { a.world.toArray(edgePos, i * 6); b.world.toArray(edgePos, i * 6 + 3); });
edgeGeo.attributes.position.needsUpdate = true;
if (!reduce && !dragging) rotY += 0.0016;
group.rotation.set(rotX, rotY, 0);
camera.position.set(view.center.x, view.center.y + view.dist * 0.18, view.center.z + view.dist);
camera.lookAt(view.center);
renderer.render(scene, camera);
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
} catch (err) {
showFallback();
}
</script>
</div></template>
</rf-sandbox><div data-source="HTML">
<style>
html, body { height: 100%; margin: 0; overflow: hidden; }
/* A window into space — a deep dark backdrop in both themes so the stars glow. */
body { background: radial-gradient(ellipse at 50% 45%, #1b1a24 0%, #0b0a10 70%); }
#c { display: block; width: 100%; height: 100%; cursor: grab; touch-action: none; }
#c:active { cursor: grabbing; }
#tip {
position: fixed; left: 0; top: 0; pointer-events: none; z-index: 2;
padding: 2px 9px; border-radius: 6px; opacity: 0;
font: 12px/1.4 system-ui, -apple-system, sans-serif;
background: rgba(20,18,23,.92); color: #f6f4ef; white-space: nowrap;
transform: translate(-50%, -160%); transition: opacity .12s;
box-shadow: 0 2px 10px rgba(0,0,0,.5);
}
#fallback {
display: none; height: 100%; box-sizing: border-box; padding: 1.5rem;
align-items: center; justify-content: center; text-align: center;
font: 14px/1.5 system-ui, -apple-system, sans-serif; color: #9a96a4;
}
</style>
<canvas id="c"></canvas>
<div id="tip"></div>
<div id="fallback">A 3D star-map of the page registry renders here — it needs WebGL and JavaScript. The page list below is the accessible equivalent.</div>
<script type="module">
const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
const canvas = document.getElementById('c');
const tip = document.getElementById('tip');
const showFallback = () => { canvas.style.display = 'none'; document.getElementById('fallback').style.display = 'flex'; };
// Deterministic 0..1 hash from a string (stable layout across renders).
const hash = (s) => { let h = 2166136261; for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 16777619); } return ((h >>> 0) % 100000) / 100000; };
try {
const DATA = window.RF_DATA;
if (!DATA || DATA.shape !== 'tree' || !Array.isArray(DATA.tree) || DATA.tree.length === 0) throw new Error('no tree');
const THREE = await import('https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js');
// ── Build node records (parent / children / depth / subtree size) ────────
const all = [];
function build(node, parent, depth) {
const rec = { url: node.url, title: (node.data && node.data.title) || node.url, depth, parent, children: [], size: 1, world: new THREE.Vector3() };
all.push(rec);
(node.children || []).forEach((c) => rec.children.push(build(c, rec, depth + 1)));
rec.size += rec.children.reduce((s, c) => s + c.size, 0);
return rec;
}
const roots = DATA.tree.map((r) => build(r, null, 0));
// ── Lay roots out on a golden-angle galaxy disc (big systems near the core)
const GA = Math.PI * (3 - Math.sqrt(5));
roots.slice().sort((a, b) => b.size - a.size).forEach((r, i) => {
const rad = Math.sqrt(i + 0.7) * 2.7;
const ang = i * GA;
r.basePos = new THREE.Vector3(Math.cos(ang) * rad, (hash(r.url) - 0.5) * 3, Math.sin(ang) * rad);
});
// ── Give every descendant an orbit around its parent (nested, shrinking) ─
function assignOrbits(rec) {
const kids = rec.children;
if (!kids.length) return;
const ring = (1.5 + kids.length * 0.16) / (1 + rec.depth * 0.7);
const h = hash(rec.url || 'core');
const axis = new THREE.Vector3(Math.sin(h * 6.283) * 0.7, 1, Math.cos(h * 6.283) * 0.7).normalize();
const dir = rec.depth % 2 ? -1 : 1;
kids.forEach((k, i) => {
k.orbit = { radius: ring, angle0: (i / kids.length) * 6.283 + h * 6.283, speed: dir * 0.45 / ring, axis };
assignOrbits(k);
});
}
roots.forEach(assignOrbits);
// ── Scene ────────────────────────────────────────────────────────────────
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(55, 1, 0.1, 400);
const group = new THREE.Group();
scene.add(group);
// Galactic core glow + the light that warms the disc.
group.add(new THREE.Mesh(new THREE.SphereGeometry(0.7, 24, 24), new THREE.MeshBasicMaterial({ color: 0xffd9a8 })));
group.add(new THREE.PointLight(0xffe6c0, 2.2, 0, 0.6));
scene.add(new THREE.AmbientLight(0x8088aa, 0.5));
// Nodes — emissive spheres; stars (roots) sized by subtree, planets shrink.
const geo = new THREE.SphereGeometry(1, 18, 18);
const meshes = [];
for (const rec of all) {
const isStar = !rec.parent;
const r = isStar ? 0.22 + Math.min(rec.size, 40) * 0.012 : Math.max(0.07, 0.18 / rec.depth);
const color = isStar
? new THREE.Color().setHSL(0.96 - Math.min(rec.size, 28) * 0.006, 0.72, 0.62)
: new THREE.Color().setHSL(0.55 + rec.depth * 0.05, 0.55, 0.62);
const mat = new THREE.MeshStandardMaterial({ color, emissive: color, emissiveIntensity: isStar ? 0.7 : 0.35, roughness: 0.5, metalness: 0.1 });
const m = new THREE.Mesh(geo, mat);
m.scale.setScalar(r);
m.userData = { rec, baseScale: r, twinkle: hash(rec.url) * 6.283 };
group.add(m);
meshes.push(m);
}
// Orbit edges (parent → child), positions refreshed each frame.
const edgePairs = all.filter((r) => r.parent).map((r) => [r, r.parent]);
const edgePos = new Float32Array(edgePairs.length * 6);
const edgeGeo = new THREE.BufferGeometry();
edgeGeo.setAttribute('position', new THREE.BufferAttribute(edgePos, 3));
group.add(new THREE.LineSegments(edgeGeo, new THREE.LineBasicMaterial({ color: 0x8a86ff, transparent: true, opacity: 0.13 })));
// Distant starfield backdrop.
const SF = 1400, sf = new Float32Array(SF * 3);
for (let i = 0; i < SF; i++) {
const rr = 60 + Math.random() * 120, th = Math.random() * 6.283, ph = Math.acos(2 * Math.random() - 1);
sf[i * 3] = rr * Math.sin(ph) * Math.cos(th); sf[i * 3 + 1] = rr * Math.cos(ph); sf[i * 3 + 2] = rr * Math.sin(ph) * Math.sin(th);
}
const sfGeo = new THREE.BufferGeometry(); sfGeo.setAttribute('position', new THREE.BufferAttribute(sf, 3));
scene.add(new THREE.Points(sfGeo, new THREE.PointsMaterial({ color: 0xc9c6e0, size: 0.5, sizeAttenuation: true, transparent: true, opacity: 0.6 })));
// ── Position update (top-down: roots fixed, children orbit parents) ───────
const _u = new THREE.Vector3(), _v = new THREE.Vector3();
const perp = (n, o) => { o.set(Math.abs(n.x) < 0.9 ? 1 : 0, Math.abs(n.x) < 0.9 ? 0 : 1, 0); return o.crossVectors(n, o).normalize(); };
function place(rec, t) {
if (!rec.parent) rec.world.copy(rec.basePos);
else {
const o = rec.orbit, a = o.angle0 + t * o.speed;
perp(o.axis, _u); _v.crossVectors(o.axis, _u);
rec.world.copy(rec.parent.world).addScaledVector(_u, Math.cos(a) * o.radius).addScaledVector(_v, Math.sin(a) * o.radius);
}
rec.children.forEach((c) => place(c, t));
}
// Camera framing from the root disc bounds.
const box = new THREE.Box3(); roots.forEach((r) => box.expandByPoint(r.basePos));
const sph = box.getBoundingSphere(new THREE.Sphere());
const view = { center: sph.center.clone(), dist: Math.max(8, sph.radius / Math.sin((camera.fov / 2) * Math.PI / 180) * 1.15) };
function resize() {
const w = canvas.clientWidth, h = canvas.clientHeight; if (!w || !h) return;
renderer.setSize(w, h, false); camera.aspect = w / h; camera.updateProjectionMatrix();
}
resize(); window.addEventListener('resize', resize);
// ── Controls: drag to rotate, gentle auto-spin (off for reduced motion) ───
let rotX = 0.5, rotY = 0.5, dragging = false, lx = 0, ly = 0;
canvas.addEventListener('pointerdown', (e) => { dragging = true; lx = e.clientX; ly = e.clientY; canvas.setPointerCapture(e.pointerId); });
canvas.addEventListener('pointerup', (e) => { dragging = false; canvas.releasePointerCapture?.(e.pointerId); });
canvas.addEventListener('pointermove', (e) => {
if (dragging) { rotY += (e.clientX - lx) * 0.005; rotX = Math.max(-1.3, Math.min(1.3, rotX + (e.clientY - ly) * 0.005)); lx = e.clientX; ly = e.clientY; }
hover(e);
});
// ── Hover tooltip + click-to-navigate ────────────────────────────────────
const ray = new THREE.Raycaster(); const ndc = new THREE.Vector2(); let hovered = null;
function pick(e) {
const r = canvas.getBoundingClientRect();
ndc.set(((e.clientX - r.left) / r.width) * 2 - 1, -((e.clientY - r.top) / r.height) * 2 + 1);
ray.setFromCamera(ndc, camera);
return ray.intersectObjects(meshes, false)[0]?.object ?? null;
}
function hover(e) {
const hit = pick(e);
hovered = hit;
if (hovered) { tip.textContent = hovered.userData.rec.title; tip.style.left = e.clientX + 'px'; tip.style.top = e.clientY + 'px'; tip.style.opacity = '1'; canvas.style.cursor = 'pointer'; }
else { tip.style.opacity = '0'; canvas.style.cursor = dragging ? 'grabbing' : 'grab'; }
}
canvas.addEventListener('click', (e) => {
const hit = pick(e);
if (hit?.userData?.rec?.url) { try { window.top.location.href = hit.userData.rec.url; } catch { window.open(hit.userData.rec.url, '_top'); } }
});
// ── Loop ──────────────────────────────────────────────────────────────────
const t0 = performance.now();
function frame(now) {
const t = reduce ? 0 : (now - t0) / 1000;
for (const r of roots) place(r, t);
for (const m of meshes) {
m.position.copy(m.userData.rec.world);
const tw = reduce ? 1 : 1 + Math.sin(t * 2 + m.userData.twinkle) * (m.userData.rec.parent ? 0 : 0.12);
m.scale.setScalar(m.userData.baseScale * (m === hovered ? 1.5 : 1) * tw);
}
edgePairs.forEach(([a, b], i) => { a.world.toArray(edgePos, i * 6); b.world.toArray(edgePos, i * 6 + 3); });
edgeGeo.attributes.position.needsUpdate = true;
if (!reduce && !dragging) rotY += 0.0016;
group.rotation.set(rotX, rotY, 0);
camera.position.set(view.center.x, view.center.y + view.dist * 0.18, view.center.z + view.dist);
camera.lookAt(view.center);
renderer.render(scene, camera);
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
} catch (err) {
showFallback();
}
</script>
</div><div data-source="HTML">
<style>
html, body { height: 100%; margin: 0; overflow: hidden; }
/* A window into space — a deep dark backdrop in both themes so the stars glow. */
body { background: radial-gradient(ellipse at 50% 45%, #1b1a24 0%, #0b0a10 70%); }
#c { display: block; width: 100%; height: 100%; cursor: grab; touch-action: none; }
#c:active { cursor: grabbing; }
#tip {
position: fixed; left: 0; top: 0; pointer-events: none; z-index: 2;
padding: 2px 9px; border-radius: 6px; opacity: 0;
font: 12px/1.4 system-ui, -apple-system, sans-serif;
background: rgba(20,18,23,.92); color: #f6f4ef; white-space: nowrap;
transform: translate(-50%, -160%); transition: opacity .12s;
box-shadow: 0 2px 10px rgba(0,0,0,.5);
}
#fallback {
display: none; height: 100%; box-sizing: border-box; padding: 1.5rem;
align-items: center; justify-content: center; text-align: center;
font: 14px/1.5 system-ui, -apple-system, sans-serif; color: #9a96a4;
}
</style>
<canvas id="c"></canvas>
<div id="tip"></div>
<div id="fallback">A 3D star-map of the page registry renders here — it needs WebGL and JavaScript. The page list below is the accessible equivalent.</div>
<script type="module">
const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
const canvas = document.getElementById('c');
const tip = document.getElementById('tip');
const showFallback = () => { canvas.style.display = 'none'; document.getElementById('fallback').style.display = 'flex'; };
// Deterministic 0..1 hash from a string (stable layout across renders).
const hash = (s) => { let h = 2166136261; for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 16777619); } return ((h >>> 0) % 100000) / 100000; };
try {
const DATA = window.RF_DATA;
if (!DATA || DATA.shape !== 'tree' || !Array.isArray(DATA.tree) || DATA.tree.length === 0) throw new Error('no tree');
const THREE = await import('https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js');
// ── Build node records (parent / children / depth / subtree size) ────────
const all = [];
function build(node, parent, depth) {
const rec = { url: node.url, title: (node.data && node.data.title) || node.url, depth, parent, children: [], size: 1, world: new THREE.Vector3() };
all.push(rec);
(node.children || []).forEach((c) => rec.children.push(build(c, rec, depth + 1)));
rec.size += rec.children.reduce((s, c) => s + c.size, 0);
return rec;
}
const roots = DATA.tree.map((r) => build(r, null, 0));
// ── Lay roots out on a golden-angle galaxy disc (big systems near the core)
const GA = Math.PI * (3 - Math.sqrt(5));
roots.slice().sort((a, b) => b.size - a.size).forEach((r, i) => {
const rad = Math.sqrt(i + 0.7) * 2.7;
const ang = i * GA;
r.basePos = new THREE.Vector3(Math.cos(ang) * rad, (hash(r.url) - 0.5) * 3, Math.sin(ang) * rad);
});
// ── Give every descendant an orbit around its parent (nested, shrinking) ─
function assignOrbits(rec) {
const kids = rec.children;
if (!kids.length) return;
const ring = (1.5 + kids.length * 0.16) / (1 + rec.depth * 0.7);
const h = hash(rec.url || 'core');
const axis = new THREE.Vector3(Math.sin(h * 6.283) * 0.7, 1, Math.cos(h * 6.283) * 0.7).normalize();
const dir = rec.depth % 2 ? -1 : 1;
kids.forEach((k, i) => {
k.orbit = { radius: ring, angle0: (i / kids.length) * 6.283 + h * 6.283, speed: dir * 0.45 / ring, axis };
assignOrbits(k);
});
}
roots.forEach(assignOrbits);
// ── Scene ────────────────────────────────────────────────────────────────
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(55, 1, 0.1, 400);
const group = new THREE.Group();
scene.add(group);
// Galactic core glow + the light that warms the disc.
group.add(new THREE.Mesh(new THREE.SphereGeometry(0.7, 24, 24), new THREE.MeshBasicMaterial({ color: 0xffd9a8 })));
group.add(new THREE.PointLight(0xffe6c0, 2.2, 0, 0.6));
scene.add(new THREE.AmbientLight(0x8088aa, 0.5));
// Nodes — emissive spheres; stars (roots) sized by subtree, planets shrink.
const geo = new THREE.SphereGeometry(1, 18, 18);
const meshes = [];
for (const rec of all) {
const isStar = !rec.parent;
const r = isStar ? 0.22 + Math.min(rec.size, 40) * 0.012 : Math.max(0.07, 0.18 / rec.depth);
const color = isStar
? new THREE.Color().setHSL(0.96 - Math.min(rec.size, 28) * 0.006, 0.72, 0.62)
: new THREE.Color().setHSL(0.55 + rec.depth * 0.05, 0.55, 0.62);
const mat = new THREE.MeshStandardMaterial({ color, emissive: color, emissiveIntensity: isStar ? 0.7 : 0.35, roughness: 0.5, metalness: 0.1 });
const m = new THREE.Mesh(geo, mat);
m.scale.setScalar(r);
m.userData = { rec, baseScale: r, twinkle: hash(rec.url) * 6.283 };
group.add(m);
meshes.push(m);
}
// Orbit edges (parent → child), positions refreshed each frame.
const edgePairs = all.filter((r) => r.parent).map((r) => [r, r.parent]);
const edgePos = new Float32Array(edgePairs.length * 6);
const edgeGeo = new THREE.BufferGeometry();
edgeGeo.setAttribute('position', new THREE.BufferAttribute(edgePos, 3));
group.add(new THREE.LineSegments(edgeGeo, new THREE.LineBasicMaterial({ color: 0x8a86ff, transparent: true, opacity: 0.13 })));
// Distant starfield backdrop.
const SF = 1400, sf = new Float32Array(SF * 3);
for (let i = 0; i < SF; i++) {
const rr = 60 + Math.random() * 120, th = Math.random() * 6.283, ph = Math.acos(2 * Math.random() - 1);
sf[i * 3] = rr * Math.sin(ph) * Math.cos(th); sf[i * 3 + 1] = rr * Math.cos(ph); sf[i * 3 + 2] = rr * Math.sin(ph) * Math.sin(th);
}
const sfGeo = new THREE.BufferGeometry(); sfGeo.setAttribute('position', new THREE.BufferAttribute(sf, 3));
scene.add(new THREE.Points(sfGeo, new THREE.PointsMaterial({ color: 0xc9c6e0, size: 0.5, sizeAttenuation: true, transparent: true, opacity: 0.6 })));
// ── Position update (top-down: roots fixed, children orbit parents) ───────
const _u = new THREE.Vector3(), _v = new THREE.Vector3();
const perp = (n, o) => { o.set(Math.abs(n.x) < 0.9 ? 1 : 0, Math.abs(n.x) < 0.9 ? 0 : 1, 0); return o.crossVectors(n, o).normalize(); };
function place(rec, t) {
if (!rec.parent) rec.world.copy(rec.basePos);
else {
const o = rec.orbit, a = o.angle0 + t * o.speed;
perp(o.axis, _u); _v.crossVectors(o.axis, _u);
rec.world.copy(rec.parent.world).addScaledVector(_u, Math.cos(a) * o.radius).addScaledVector(_v, Math.sin(a) * o.radius);
}
rec.children.forEach((c) => place(c, t));
}
// Camera framing from the root disc bounds.
const box = new THREE.Box3(); roots.forEach((r) => box.expandByPoint(r.basePos));
const sph = box.getBoundingSphere(new THREE.Sphere());
const view = { center: sph.center.clone(), dist: Math.max(8, sph.radius / Math.sin((camera.fov / 2) * Math.PI / 180) * 1.15) };
function resize() {
const w = canvas.clientWidth, h = canvas.clientHeight; if (!w || !h) return;
renderer.setSize(w, h, false); camera.aspect = w / h; camera.updateProjectionMatrix();
}
resize(); window.addEventListener('resize', resize);
// ── Controls: drag to rotate, gentle auto-spin (off for reduced motion) ───
let rotX = 0.5, rotY = 0.5, dragging = false, lx = 0, ly = 0;
canvas.addEventListener('pointerdown', (e) => { dragging = true; lx = e.clientX; ly = e.clientY; canvas.setPointerCapture(e.pointerId); });
canvas.addEventListener('pointerup', (e) => { dragging = false; canvas.releasePointerCapture?.(e.pointerId); });
canvas.addEventListener('pointermove', (e) => {
if (dragging) { rotY += (e.clientX - lx) * 0.005; rotX = Math.max(-1.3, Math.min(1.3, rotX + (e.clientY - ly) * 0.005)); lx = e.clientX; ly = e.clientY; }
hover(e);
});
// ── Hover tooltip + click-to-navigate ────────────────────────────────────
const ray = new THREE.Raycaster(); const ndc = new THREE.Vector2(); let hovered = null;
function pick(e) {
const r = canvas.getBoundingClientRect();
ndc.set(((e.clientX - r.left) / r.width) * 2 - 1, -((e.clientY - r.top) / r.height) * 2 + 1);
ray.setFromCamera(ndc, camera);
return ray.intersectObjects(meshes, false)[0]?.object ?? null;
}
function hover(e) {
const hit = pick(e);
hovered = hit;
if (hovered) { tip.textContent = hovered.userData.rec.title; tip.style.left = e.clientX + 'px'; tip.style.top = e.clientY + 'px'; tip.style.opacity = '1'; canvas.style.cursor = 'pointer'; }
else { tip.style.opacity = '0'; canvas.style.cursor = dragging ? 'grabbing' : 'grab'; }
}
canvas.addEventListener('click', (e) => {
const hit = pick(e);
if (hit?.userData?.rec?.url) { try { window.top.location.href = hit.userData.rec.url; } catch { window.open(hit.userData.rec.url, '_top'); } }
});
// ── Loop ──────────────────────────────────────────────────────────────────
const t0 = performance.now();
function frame(now) {
const t = reduce ? 0 : (now - t0) / 1000;
for (const r of roots) place(r, t);
for (const m of meshes) {
m.position.copy(m.userData.rec.world);
const tw = reduce ? 1 : 1 + Math.sin(t * 2 + m.userData.twinkle) * (m.userData.rec.parent ? 0 : 0.12);
m.scale.setScalar(m.userData.baseScale * (m === hovered ? 1.5 : 1) * tw);
}
edgePairs.forEach(([a, b], i) => { a.world.toArray(edgePos, i * 6); b.world.toArray(edgePos, i * 6 + 3); });
edgeGeo.attributes.position.needsUpdate = true;
if (!reduce && !dragging) rotY += 0.0016;
group.rotation.set(rotX, rotY, 0);
camera.position.set(view.center.x, view.center.y + view.dist * 0.18, view.center.z + view.dist);
camera.lookAt(view.center);
renderer.render(scene, camera);
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
} catch (err) {
showFallback();
}
</script>
</div><rf-sandbox class="rf-sandbox" data-source-content="<div data-source="HTML">
<style>
html, body { height: 100%; margin: 0; overflow: hidden; }
/* A window into space — a deep dark backdrop in both themes so the stars glow. */
body { background: radial-gradient(ellipse at 50% 45%, #1b1a24 0%, #0b0a10 70%); }
#c { display: block; width: 100%; height: 100%; cursor: grab; touch-action: none; }
#c:active { cursor: grabbing; }
#tip {
position: fixed; left: 0; top: 0; pointer-events: none; z-index: 2;
padding: 2px 9px; border-radius: 6px; opacity: 0;
font: 12px/1.4 system-ui, -apple-system, sans-serif;
background: rgba(20,18,23,.92); color: #f6f4ef; white-space: nowrap;
transform: translate(-50%, -160%); transition: opacity .12s;
box-shadow: 0 2px 10px rgba(0,0,0,.5);
}
#fallback {
display: none; height: 100%; box-sizing: border-box; padding: 1.5rem;
align-items: center; justify-content: center; text-align: center;
font: 14px/1.5 system-ui, -apple-system, sans-serif; color: #9a96a4;
}
</style>
<canvas id="c"></canvas>
<div id="tip"></div>
<div id="fallback">A 3D star-map of the page registry renders here — it needs WebGL and JavaScript. The page list below is the accessible equivalent.</div>
<script type="module">
const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
const canvas = document.getElementById('c');
const tip = document.getElementById('tip');
const showFallback = () => { canvas.style.display = 'none'; document.getElementById('fallback').style.display = 'flex'; };
// Deterministic 0..1 hash from a string (stable layout across renders).
const hash = (s) => { let h = 2166136261; for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 16777619); } return ((h >>> 0) % 100000) / 100000; };
try {
const DATA = window.RF_DATA;
if (!DATA || DATA.shape !== 'tree' || !Array.isArray(DATA.tree) || DATA.tree.length === 0) throw new Error('no tree');
const THREE = await import('https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js');
// ── Build node records (parent / children / depth / subtree size) ────────
const all = [];
function build(node, parent, depth) {
const rec = { url: node.url, title: (node.data && node.data.title) || node.url, depth, parent, children: [], size: 1, world: new THREE.Vector3() };
all.push(rec);
(node.children || []).forEach((c) => rec.children.push(build(c, rec, depth + 1)));
rec.size += rec.children.reduce((s, c) => s + c.size, 0);
return rec;
}
const roots = DATA.tree.map((r) => build(r, null, 0));
// ── Lay roots out on a golden-angle galaxy disc (big systems near the core)
const GA = Math.PI * (3 - Math.sqrt(5));
roots.slice().sort((a, b) => b.size - a.size).forEach((r, i) => {
const rad = Math.sqrt(i + 0.7) * 2.7;
const ang = i * GA;
r.basePos = new THREE.Vector3(Math.cos(ang) * rad, (hash(r.url) - 0.5) * 3, Math.sin(ang) * rad);
});
// ── Give every descendant an orbit around its parent (nested, shrinking) ─
function assignOrbits(rec) {
const kids = rec.children;
if (!kids.length) return;
const ring = (1.5 + kids.length * 0.16) / (1 + rec.depth * 0.7);
const h = hash(rec.url || 'core');
const axis = new THREE.Vector3(Math.sin(h * 6.283) * 0.7, 1, Math.cos(h * 6.283) * 0.7).normalize();
const dir = rec.depth % 2 ? -1 : 1;
kids.forEach((k, i) => {
k.orbit = { radius: ring, angle0: (i / kids.length) * 6.283 + h * 6.283, speed: dir * 0.45 / ring, axis };
assignOrbits(k);
});
}
roots.forEach(assignOrbits);
// ── Scene ────────────────────────────────────────────────────────────────
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(55, 1, 0.1, 400);
const group = new THREE.Group();
scene.add(group);
// Galactic core glow + the light that warms the disc.
group.add(new THREE.Mesh(new THREE.SphereGeometry(0.7, 24, 24), new THREE.MeshBasicMaterial({ color: 0xffd9a8 })));
group.add(new THREE.PointLight(0xffe6c0, 2.2, 0, 0.6));
scene.add(new THREE.AmbientLight(0x8088aa, 0.5));
// Nodes — emissive spheres; stars (roots) sized by subtree, planets shrink.
const geo = new THREE.SphereGeometry(1, 18, 18);
const meshes = [];
for (const rec of all) {
const isStar = !rec.parent;
const r = isStar ? 0.22 + Math.min(rec.size, 40) * 0.012 : Math.max(0.07, 0.18 / rec.depth);
const color = isStar
? new THREE.Color().setHSL(0.96 - Math.min(rec.size, 28) * 0.006, 0.72, 0.62)
: new THREE.Color().setHSL(0.55 + rec.depth * 0.05, 0.55, 0.62);
const mat = new THREE.MeshStandardMaterial({ color, emissive: color, emissiveIntensity: isStar ? 0.7 : 0.35, roughness: 0.5, metalness: 0.1 });
const m = new THREE.Mesh(geo, mat);
m.scale.setScalar(r);
m.userData = { rec, baseScale: r, twinkle: hash(rec.url) * 6.283 };
group.add(m);
meshes.push(m);
}
// Orbit edges (parent → child), positions refreshed each frame.
const edgePairs = all.filter((r) => r.parent).map((r) => [r, r.parent]);
const edgePos = new Float32Array(edgePairs.length * 6);
const edgeGeo = new THREE.BufferGeometry();
edgeGeo.setAttribute('position', new THREE.BufferAttribute(edgePos, 3));
group.add(new THREE.LineSegments(edgeGeo, new THREE.LineBasicMaterial({ color: 0x8a86ff, transparent: true, opacity: 0.13 })));
// Distant starfield backdrop.
const SF = 1400, sf = new Float32Array(SF * 3);
for (let i = 0; i < SF; i++) {
const rr = 60 + Math.random() * 120, th = Math.random() * 6.283, ph = Math.acos(2 * Math.random() - 1);
sf[i * 3] = rr * Math.sin(ph) * Math.cos(th); sf[i * 3 + 1] = rr * Math.cos(ph); sf[i * 3 + 2] = rr * Math.sin(ph) * Math.sin(th);
}
const sfGeo = new THREE.BufferGeometry(); sfGeo.setAttribute('position', new THREE.BufferAttribute(sf, 3));
scene.add(new THREE.Points(sfGeo, new THREE.PointsMaterial({ color: 0xc9c6e0, size: 0.5, sizeAttenuation: true, transparent: true, opacity: 0.6 })));
// ── Position update (top-down: roots fixed, children orbit parents) ───────
const _u = new THREE.Vector3(), _v = new THREE.Vector3();
const perp = (n, o) => { o.set(Math.abs(n.x) < 0.9 ? 1 : 0, Math.abs(n.x) < 0.9 ? 0 : 1, 0); return o.crossVectors(n, o).normalize(); };
function place(rec, t) {
if (!rec.parent) rec.world.copy(rec.basePos);
else {
const o = rec.orbit, a = o.angle0 + t * o.speed;
perp(o.axis, _u); _v.crossVectors(o.axis, _u);
rec.world.copy(rec.parent.world).addScaledVector(_u, Math.cos(a) * o.radius).addScaledVector(_v, Math.sin(a) * o.radius);
}
rec.children.forEach((c) => place(c, t));
}
// Camera framing from the root disc bounds.
const box = new THREE.Box3(); roots.forEach((r) => box.expandByPoint(r.basePos));
const sph = box.getBoundingSphere(new THREE.Sphere());
const view = { center: sph.center.clone(), dist: Math.max(8, sph.radius / Math.sin((camera.fov / 2) * Math.PI / 180) * 1.15) };
function resize() {
const w = canvas.clientWidth, h = canvas.clientHeight; if (!w || !h) return;
renderer.setSize(w, h, false); camera.aspect = w / h; camera.updateProjectionMatrix();
}
resize(); window.addEventListener('resize', resize);
// ── Controls: drag to rotate, gentle auto-spin (off for reduced motion) ───
let rotX = 0.5, rotY = 0.5, dragging = false, lx = 0, ly = 0;
canvas.addEventListener('pointerdown', (e) => { dragging = true; lx = e.clientX; ly = e.clientY; canvas.setPointerCapture(e.pointerId); });
canvas.addEventListener('pointerup', (e) => { dragging = false; canvas.releasePointerCapture?.(e.pointerId); });
canvas.addEventListener('pointermove', (e) => {
if (dragging) { rotY += (e.clientX - lx) * 0.005; rotX = Math.max(-1.3, Math.min(1.3, rotX + (e.clientY - ly) * 0.005)); lx = e.clientX; ly = e.clientY; }
hover(e);
});
// ── Hover tooltip + click-to-navigate ────────────────────────────────────
const ray = new THREE.Raycaster(); const ndc = new THREE.Vector2(); let hovered = null;
function pick(e) {
const r = canvas.getBoundingClientRect();
ndc.set(((e.clientX - r.left) / r.width) * 2 - 1, -((e.clientY - r.top) / r.height) * 2 + 1);
ray.setFromCamera(ndc, camera);
return ray.intersectObjects(meshes, false)[0]?.object ?? null;
}
function hover(e) {
const hit = pick(e);
hovered = hit;
if (hovered) { tip.textContent = hovered.userData.rec.title; tip.style.left = e.clientX + 'px'; tip.style.top = e.clientY + 'px'; tip.style.opacity = '1'; canvas.style.cursor = 'pointer'; }
else { tip.style.opacity = '0'; canvas.style.cursor = dragging ? 'grabbing' : 'grab'; }
}
canvas.addEventListener('click', (e) => {
const hit = pick(e);
if (hit?.userData?.rec?.url) { try { window.top.location.href = hit.userData.rec.url; } catch { window.open(hit.userData.rec.url, '_top'); } }
});
// ── Loop ──────────────────────────────────────────────────────────────────
const t0 = performance.now();
function frame(now) {
const t = reduce ? 0 : (now - t0) / 1000;
for (const r of roots) place(r, t);
for (const m of meshes) {
m.position.copy(m.userData.rec.world);
const tw = reduce ? 1 : 1 + Math.sin(t * 2 + m.userData.twinkle) * (m.userData.rec.parent ? 0 : 0.12);
m.scale.setScalar(m.userData.baseScale * (m === hovered ? 1.5 : 1) * tw);
}
edgePairs.forEach(([a, b], i) => { a.world.toArray(edgePos, i * 6); b.world.toArray(edgePos, i * 6 + 3); });
edgeGeo.attributes.position.needsUpdate = true;
if (!reduce && !dragging) rotY += 0.0016;
group.rotation.set(rotX, rotY, 0);
camera.position.set(view.center.x, view.center.y + view.dist * 0.18, view.center.z + view.dist);
camera.lookAt(view.center);
renderer.render(scene, camera);
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
} catch (err) {
showFallback();
}
</script>
</div>" data-height="440" data-source-origins="HTML sitemap-3d/index.html" data-security-mode="trusted" data-allow-js="true" data-activation="visible" data-rf-query="type:page url:/runes/*" data-rf-shape="tree" data-rf-records="{"shape":"tree","tree":[{"id":"/runes/accordion","type":"page","url":"/runes/accordion","data":{"title":"Accordion","description":"Collapsible accordion sections for FAQ-style content","category":"Layout","plugin":"core","status":"stable","url":"/runes/accordion","parentUrl":"/runes/","draft":false},"children":[]},{"id":"/runes/aggregate","type":"page","url":"/runes/aggregate","data":{"title":"Aggregate","description":"Project numbers from the registry — counts and per-group breakdowns — the number-projecting counterpart to collection and relationships, with field-match queries and a sub-filter that drives progress-bar ratios","category":"Registry","plugin":"core","status":"stable","url":"/runes/aggregate","parentUrl":"/runes/","draft":false},"children":[]},{"id":"/runes/annotate","type":"page","url":"/runes/annotate","data":{"title":"Annotate","description":"Content with margin annotations and notes","category":"Content","plugin":"core","status":"stable","url":"/runes/annotate","parentUrl":"/runes/","draft":false},"children":[]},{"id":"/runes/badge","type":"page","url":"/runes/badge","data":{"title":"Badge","description":"Inline pill that flags a piece of content — status, category, recency, tag","category":"Content","plugin":"core","status":"stable","url":"/runes/badge","parentUrl":"/runes/","draft":false},"children":[]},{"id":"/runes/bar","type":"page","url":"/runes/bar","data":{"title":"Bar","description":"Block-level wrapper that renders a horizontal row of content — the bar layout primitive, composable in prose","category":"Content","plugin":"core","status":"stable","url":"/runes/bar","parentUrl":"/runes/","draft":false},"children":[]},{"id":"/runes/bg","type":"page","url":"/runes/bg","data":{"title":"Background","description":"Add background images, video, and overlays to any section rune","category":"Layout","plugin":"core","status":"stable","url":"/runes/bg","parentUrl":"/runes/","draft":false},"children":[]},{"id":"/runes/blog","type":"page","url":"/runes/blog","data":{"title":"Blog","description":"Display a list of blog posts from a content folder with sorting, filtering, and layout options","category":"Site","plugin":"core","status":"stable","url":"/runes/blog","parentUrl":"/runes/","draft":false},"children":[]},{"id":"/runes/breadcrumb","type":"page","url":"/runes/breadcrumb","data":{"title":"Breadcrumb","description":"Navigation breadcrumbs showing page hierarchy","category":"Site","plugin":"core","status":"stable","url":"/runes/breadcrumb","parentUrl":"/runes/","draft":false},"children":[]},{"id":"/runes/budget","type":"page","url":"/runes/budget","data":{"title":"Budget","description":"Structured budgets with categories, line items, and totals","category":"Code & Data","plugin":"core","status":"stable","url":"/runes/budget","parentUrl":"/runes/","draft":false},"children":[]},{"id":"/runes/card","type":"page","url":"/runes/card","data":{"title":"Card","description":"A generic, self-contained content card — optional media, body, and footer zones, with an optional whole-card link","category":"Content","plugin":"core","status":"stable","url":"/runes/card","parentUrl":"/runes/","draft":false},"children":[]},{"id":"/runes/chart","type":"page","url":"/runes/chart","data":{"title":"Chart","description":"Chart visualization from a Markdown table","category":"Code & Data","plugin":"core","status":"stable","url":"/runes/chart","parentUrl":"/runes/","draft":false},"children":[]},{"id":"/runes/codegroup","type":"page","url":"/runes/codegroup","data":{"title":"Codegroup","description":"Tabbed code blocks and styled code chrome","category":"Code & Data","plugin":"core","status":"stable","url":"/runes/codegroup","parentUrl":"/runes/","draft":false},"children":[]},{"id":"/runes/collection","type":"page","url":"/runes/collection","data":{"title":"Collection","description":"Render a list, grid, or table of registry entities — the plural counterpart to ref and expand, with filtering, sorting, grouping, and per-item templates","category":"Registry","plugin":"core","status":"stable","url":"/runes/collection","parentUrl":"/runes/","draft":false},"children":[]},{"id":"/runes/compare","type":"page","url":"/runes/compare","data":{"title":"Compare","description":"Side-by-side code comparison panels","category":"Code & Data","plugin":"core","status":"stable","url":"/runes/compare","parentUrl":"/runes/","draft":false},"children":[]},{"id":"/runes/conversation","type":"page","url":"/runes/conversation","data":{"title":"Conversation","description":"Chat and dialogue display with alternating speaker messages","category":"Content","plugin":"core","status":"stable","url":"/runes/conversation","parentUrl":"/runes/","draft":false},"children":[]},{"id":"/runes/datatable","type":"page","url":"/runes/datatable","data":{"title":"DataTable","description":"Interactive data table with sorting, filtering, and pagination","category":"Code & Data","plugin":"core","status":"stable","url":"/runes/datatable","parentUrl":"/runes/","draft":false},"children":[]},{"id":"/runes/deflist","type":"page","url":"/runes/deflist","data":{"title":"Deflist","description":"Block-level definition list — stacked dt/dd pairs that flow into multiple columns on wider screens","category":"Content","plugin":"core","status":"stable","url":"/runes/deflist","parentUrl":"/runes/","draft":false},"children":[]},{"id":"/runes/details","type":"page","url":"/runes/details","data":{"title":"Details","description":"Collapsible disclosure blocks for supplementary content","category":"Layout","plugin":"core","status":"stable","url":"/runes/details","parentUrl":"/runes/","draft":false},"children":[]},{"id":"/runes/diagram","type":"page","url":"/runes/diagram","data":{"title":"Diagram","description":"Mermaid diagram rendering from code blocks","category":"Code & Data","plugin":"core","status":"stable","url":"/runes/diagram","parentUrl":"/runes/","draft":false},"children":[]},{"id":"/runes/diff","type":"page","url":"/runes/diff","data":{"title":"Diff","description":"Side-by-side or unified diff view between two code blocks","category":"Code & Data","plugin":"core","status":"stable","url":"/runes/diff","parentUrl":"/runes/","draft":false},"children":[]},{"id":"/runes/drawer","type":"page","url":"/runes/drawer","data":{"title":"Drawer","description":"Addressable modal panel — declared once, opened from any xref on the page","category":"Layout","plugin":"core","status":"stable","url":"/runes/drawer","parentUrl":"/runes/","draft":false},"children":[]},{"id":"/runes/embed","type":"page","url":"/runes/embed","data":{"title":"Embed","description":"Embed external content like videos, tweets, and code demos","category":"Content","plugin":"core","status":"stable","url":"/runes/embed","parentUrl":"/runes/","draft":false},"children":[]},{"id":"/runes/expand","type":"page","url":"/runes/expand","data":{"title":"Expand","description":"Substitute a registered entity's source content inline — symmetric with xref but inlines the content instead of linking to it","category":"Registry","plugin":"core","status":"stable","url":"/runes/expand","parentUrl":"/runes/","draft":false},"children":[]},{"id":"/runes/figure","type":"page","url":"/runes/figure","data":{"title":"Figure","description":"Enhanced images with captions, sizing, and alignment","category":"Content","plugin":"core","status":"stable","url":"/runes/figure","parentUrl":"/runes/","draft":false},"children":[]},{"id":"/runes/file-ref","type":"page","url":"/runes/file-ref","data":{"title":"File-ref","description":"Path-based inline references to project files — third member of the Registry family beside xref (one entity) and expand (one entity inlined), with optional drawer preview","category":"Registry","plugin":"core","status":"stable","url":"/runes/file-ref","parentUrl":"/runes/","draft":false},"children":[]},{"id":"/runes/form","type":"page","url":"/runes/form","data":{"title":"Form","description":"Accessible HTML forms from Markdown with smart field type inference","category":"Code & Data","plugin":"core","status":"stable","url":"/runes/form","parentUrl":"/runes/","draft":false},"children":[]},{"id":"/runes/gallery","type":"page","url":"/runes/gallery","data":{"title":"Gallery","description":"Multi-image container with grid, carousel, or masonry layout","category":"Layout","plugin":"core","status":"stable","url":"/runes/gallery","parentUrl":"/runes/","draft":false},"children":[]},{"id":"/runes/grid","type":"page","url":"/runes/grid","data":{"title":"Grid","description":"Flexible grid layout with columns, auto-fill, masonry, and responsive collapse","category":"Layout","plugin":"core","status":"stable","url":"/runes/grid","parentUrl":"/runes/","draft":false},"children":[]},{"id":"/runes/hint","type":"page","url":"/runes/hint","data":{"title":"Hint","description":"Callouts and admonitions for supplementary information","category":"Content","plugin":"core","status":"stable","url":"/runes/hint","parentUrl":"/runes/","draft":false},"children":[]},{"id":"/runes/icon","type":"page","url":"/runes/icon","data":{"title":"Icon","description":"Inline icons from the theme's icon registry","category":"Content","plugin":"core","status":"stable","url":"/runes/icon","parentUrl":"/runes/","draft":false},"children":[]},{"id":"/runes/image-schemes","type":"page","url":"/runes/image-schemes","data":{"title":"Image schemes","description":"Custom URL schemes in Markdown image syntax — generated placeholders and inline icons resolved at build time to inline SVG.","category":"Content","plugin":"core","status":"stable","url":"/runes/image-schemes","parentUrl":"/runes/","draft":false},"children":[]},{"id":"/runes/juxtapose","type":"page","url":"/runes/juxtapose","data":{"title":"Juxtapose","description":"Interactive side-by-side comparison with slider, toggle, fade, and auto modes","category":"Layout","plugin":"core","status":"stable","url":"/runes/juxtapose","parentUrl":"/runes/","draft":false},"children":[]},{"id":"/runes/layout","type":"page","url":"/runes/layout","data":{"title":"Layout & Region","description":"Structural runes for defining page layouts and named content regions","category":"Site","plugin":"core","status":"stable","url":"/runes/layout","parentUrl":"/runes/","draft":false},"children":[]},{"id":"/runes/media-guests","type":"page","url":"/runes/media-guests","data":{"title":"Media guests","description":"Drop any rune into a container's media zone — charts, maps, diagrams, code, comparisons, device mockups. The slot sizes and clips them, name-agnostically.","url":"/runes/media-guests","parentUrl":"/runes/","draft":false},"children":[]},{"id":"/runes/mediatext","type":"page","url":"/runes/mediatext","data":{"title":"MediaText","description":"Side-by-side image and text layouts with configurable ratios","category":"Content","plugin":"core","status":"stable","url":"/runes/mediatext","parentUrl":"/runes/","draft":false},"children":[]},{"id":"/runes/nav","type":"page","url":"/runes/nav","data":{"title":"Nav","description":"One navigation primitive for sidebars, header menubars, footer columns, and section landings","category":"Site","plugin":"core","status":"stable","url":"/runes/nav","parentUrl":"/runes/","draft":false},"children":[]},{"id":"/runes/pagination","type":"page","url":"/runes/pagination","data":{"title":"Pagination","description":"Sequential prev/next links for ordered docs and tutorials","icon":"arrow-right","category":"Site","plugin":"core","status":"stable","url":"/runes/pagination","parentUrl":"/runes/","draft":false},"children":[]},{"id":"/runes/progress","type":"page","url":"/runes/progress","data":{"title":"Progress","description":"A generic completion bar — render a ratio from supplied numbers, with an optional label and sentiment variant","category":"Content","plugin":"core","status":"stable","url":"/runes/progress","parentUrl":"/runes/","draft":false},"children":[]},{"id":"/runes/pullquote","type":"page","url":"/runes/pullquote","data":{"title":"PullQuote","description":"Editorial pull quotes with alignment and style variants","category":"Content","plugin":"core","status":"stable","url":"/runes/pullquote","parentUrl":"/runes/","draft":false},"children":[]},{"id":"/runes/relationships","type":"page","url":"/runes/relationships","data":{"title":"Relationships","description":"Render an entity's relationship edges, grouped by kind — the plural-graph counterpart to ref and expand, generic over any domain's relationship vocabulary","category":"Registry","plugin":"core","status":"stable","url":"/runes/relationships","parentUrl":"/runes/","draft":false},"children":[]},{"id":"/runes/reveal","type":"page","url":"/runes/reveal","data":{"title":"Reveal","description":"Progressive disclosure where content appears step by step","category":"Content","plugin":"core","status":"stable","url":"/runes/reveal","parentUrl":"/runes/","draft":false},"children":[]},{"id":"/runes/rune-catalog","type":"page","url":"/runes/rune-catalog","data":{"title":"Rune Catalog","description":"Browse all available runes — core built-ins and official packages","url":"/runes/rune-catalog","parentUrl":"/runes/","draft":false},"children":[]},{"id":"/runes/sandbox","type":"page","url":"/runes/sandbox","data":{"title":"Sandbox","description":"Isolated HTML/CSS/JS rendering in an iframe with optional framework loading","category":"Layout","plugin":"core","status":"stable","url":"/runes/sandbox","parentUrl":"/runes/","draft":false},"children":[]},{"id":"/runes/section","type":"page","url":"/runes/section","data":{"title":"Section","description":"A generic page section — an eyebrow, headline, and blurb above any content","category":"Layout","plugin":"core","status":"stable","url":"/runes/section","parentUrl":"/runes/","draft":false},"children":[]},{"id":"/runes/showcase","type":"page","url":"/runes/showcase","data":{"title":"Showcase","description":"Present visual content with frame chrome — shadow, displacement (bleed), offset, and aspect ratio","category":"Layout","plugin":"core","status":"stable","url":"/runes/showcase","parentUrl":"/runes/","draft":false},"children":[]},{"id":"/runes/sidenote","type":"page","url":"/runes/sidenote","data":{"title":"Sidenote","description":"Margin notes and footnotes alongside main content","category":"Content","plugin":"core","status":"stable","url":"/runes/sidenote","parentUrl":"/runes/","draft":false},"children":[]},{"id":"/runes/snippet","type":"page","url":"/runes/snippet","data":{"title":"Snippet","description":"Embed a project file as a syntax-highlighted code block — composes inside codegroup, diff, and any future fence-consuming container","category":"Code & Data","plugin":"core","status":"stable","url":"/runes/snippet","parentUrl":"/runes/","draft":false},"children":[]},{"id":"/runes/surfaces","type":"page","url":"/runes/surfaces","data":{"title":"Surfaces","description":"The surface model on one page — chrome (shadow, frame), fills (tint, substrate, gradient), cover layouts, and interaction posture, with the reference tables inline.","url":"/runes/surfaces","parentUrl":"/runes/","draft":false},"children":[]},{"id":"/runes/tabs","type":"page","url":"/runes/tabs","data":{"title":"Tabs","description":"Tabbed content panels with heading-based tab labels","category":"Layout","plugin":"core","status":"stable","url":"/runes/tabs","parentUrl":"/runes/","draft":false},"children":[]},{"id":"/runes/textblock","type":"page","url":"/runes/textblock","data":{"title":"TextBlock","description":"Styled text blocks with drop caps, columns, and lead paragraphs","category":"Content","plugin":"core","status":"stable","url":"/runes/textblock","parentUrl":"/runes/","draft":false},"children":[]},{"id":"/runes/tint","type":"page","url":"/runes/tint","data":{"title":"Tint","description":"Section-level colour overrides via CSS custom properties","category":"Layout","plugin":"core","status":"stable","url":"/runes/tint","parentUrl":"/runes/","draft":false},"children":[]},{"id":"/runes/toc","type":"page","url":"/runes/toc","data":{"title":"Table of Contents","description":"Auto-generated table of contents from page headings","category":"Site","plugin":"core","status":"stable","url":"/runes/toc","parentUrl":"/runes/","draft":false},"children":[]},{"id":"/runes/xref","type":"page","url":"/runes/xref","data":{"title":"Xref","description":"Inline cross-references that resolve entities by ID or name from the entity registry","category":"Registry","plugin":"core","status":"stable","url":"/runes/xref","parentUrl":"/runes/","draft":false},"children":[]},{"id":"/runes/business","type":"page","url":"/runes/business","data":{"title":"@refrakt-md/business","description":"Organizational runes for team directories, company profiles, and timelines","icon":"briefcase","url":"/runes/business","parentUrl":"/runes/","draft":false},"children":[{"id":"/runes/business/cast","type":"page","url":"/runes/business/cast","data":{"title":"Cast / Team","description":"People directory for team pages and speaker lineups","category":"Business","plugin":"business","status":"stable","url":"/runes/business/cast","parentUrl":"/runes/business/","draft":false},"children":[]},{"id":"/runes/business/organization","type":"page","url":"/runes/business/organization","data":{"title":"Organization","description":"Structured business or organization information","category":"Business","plugin":"business","status":"stable","url":"/runes/business/organization","parentUrl":"/runes/business/","draft":false},"children":[]},{"id":"/runes/business/timeline","type":"page","url":"/runes/business/timeline","data":{"title":"Timeline","description":"Chronological events displayed as a timeline","category":"Business","plugin":"business","status":"stable","url":"/runes/business/timeline","parentUrl":"/runes/business/","draft":false},"children":[]}]},{"id":"/runes/design","type":"page","url":"/runes/design","data":{"title":"@refrakt-md/design","description":"Design system runes for color palettes, typography specimens, and token visualization","icon":"palette","url":"/runes/design","parentUrl":"/runes/","draft":false},"children":[{"id":"/runes/design/design-context","type":"page","url":"/runes/design/design-context","data":{"title":"Design Context","description":"Unified design token card composing palette, typography, and spacing runes with cross-page sandbox injection","category":"Design","plugin":"design","status":"stable","url":"/runes/design/design-context","parentUrl":"/runes/design/","draft":false},"children":[]},{"id":"/runes/design/mockup","type":"page","url":"/runes/design/mockup","data":{"title":"Mockup","description":"Wrap content in realistic device frames for phones, tablets, browsers, and laptops","category":"Design","plugin":"design","status":"stable","url":"/runes/design/mockup","parentUrl":"/runes/design/","draft":false},"children":[]},{"id":"/runes/design/palette","type":"page","url":"/runes/design/palette","data":{"title":"Palette","description":"Color swatch grid with optional WCAG contrast and accessibility info","category":"Design","plugin":"design","status":"stable","url":"/runes/design/palette","parentUrl":"/runes/design/","draft":false},"children":[]},{"id":"/runes/design/preview","type":"page","url":"/runes/design/preview","data":{"title":"Preview","description":"Component showcase with theme toggle and responsive viewports","category":"Design","plugin":"design","status":"stable","url":"/runes/design/preview","parentUrl":"/runes/design/","draft":false},"children":[]},{"id":"/runes/design/spacing","type":"page","url":"/runes/design/spacing","data":{"title":"Spacing","description":"Spacing scale, border radius, and shadow token display","category":"Design","plugin":"design","status":"stable","url":"/runes/design/spacing","parentUrl":"/runes/design/","draft":false},"children":[]},{"id":"/runes/design/swatch","type":"page","url":"/runes/design/swatch","data":{"title":"Swatch","description":"Inline color chip for referencing colors in prose","category":"Design","plugin":"design","status":"stable","url":"/runes/design/swatch","parentUrl":"/runes/design/","draft":false},"children":[]},{"id":"/runes/design/typography","type":"page","url":"/runes/design/typography","data":{"title":"Typography","description":"Font specimen display with live Google Fonts loading","category":"Design","plugin":"design","status":"stable","url":"/runes/design/typography","parentUrl":"/runes/design/","draft":false},"children":[]}]},{"id":"/runes/docs","type":"page","url":"/runes/docs","data":{"title":"@refrakt-md/docs","description":"Technical documentation runes for API references, code symbols, and changelogs","icon":"file-text","url":"/runes/docs","parentUrl":"/runes/","draft":false},"children":[{"id":"/runes/docs/api","type":"page","url":"/runes/docs/api","data":{"title":"API","description":"API endpoint documentation with method, path, and parameters","category":"Docs","plugin":"docs","status":"stable","url":"/runes/docs/api","parentUrl":"/runes/docs/","draft":false},"children":[]},{"id":"/runes/docs/changelog","type":"page","url":"/runes/docs/changelog","data":{"title":"Changelog","description":"Version history with release notes","category":"Docs","plugin":"docs","status":"stable","url":"/runes/docs/changelog","parentUrl":"/runes/docs/","draft":false},"children":[]},{"id":"/runes/docs/cli","type":"page","url":"/runes/docs/cli","data":{"title":"Docs CLI Commands","description":"Command reference for refrakt docs extract — generate API reference docs from source code","url":"/runes/docs/cli","parentUrl":"/runes/docs/","draft":false},"children":[]},{"id":"/runes/docs/symbol","type":"page","url":"/runes/docs/symbol","data":{"title":"Symbol","description":"Code construct documentation for functions, classes, interfaces, enums, and type aliases","category":"Docs","plugin":"docs","status":"stable","url":"/runes/docs/symbol","parentUrl":"/runes/docs/","draft":false},"children":[]}]},{"id":"/runes/learning","type":"page","url":"/runes/learning","data":{"title":"@refrakt-md/learning","description":"Educational runes for tutorials, recipes, and instructional content","icon":"book-open","url":"/runes/learning","parentUrl":"/runes/","draft":false},"children":[{"id":"/runes/learning/howto","type":"page","url":"/runes/learning/howto","data":{"title":"HowTo","description":"Step-by-step how-to guide with tools and instructions","category":"Learning","plugin":"learning","status":"stable","url":"/runes/learning/howto","parentUrl":"/runes/learning/","draft":false},"children":[]},{"id":"/runes/learning/recipe","type":"page","url":"/runes/learning/recipe","data":{"title":"Recipe","description":"Structured recipe with ingredients, steps, and chef tips","category":"Learning","plugin":"learning","status":"stable","url":"/runes/learning/recipe","parentUrl":"/runes/learning/","draft":false},"children":[]}]},{"id":"/runes/marketing","type":"page","url":"/runes/marketing","data":{"title":"@refrakt-md/marketing","description":"Landing page and conversion runes for marketing sites and product pages","icon":"rocket","url":"/runes/marketing","parentUrl":"/runes/","draft":false},"children":[{"id":"/runes/marketing/bento","type":"page","url":"/runes/marketing/bento","data":{"title":"Bento","description":"Magazine-style bento grid of cells — heading depth sizes each tile, or author cells explicitly for full control","category":"Marketing","plugin":"marketing","status":"stable","url":"/runes/marketing/bento","parentUrl":"/runes/marketing/","draft":false},"children":[]},{"id":"/runes/marketing/comparison","type":"page","url":"/runes/marketing/comparison","data":{"title":"Comparison","description":"Product and feature comparison matrices from Markdown","category":"Marketing","plugin":"marketing","status":"stable","url":"/runes/marketing/comparison","parentUrl":"/runes/marketing/","draft":false},"children":[]},{"id":"/runes/marketing/cta","type":"page","url":"/runes/marketing/cta","data":{"title":"CTA","description":"Focused call-to-action blocks with headlines, descriptions, and action buttons","category":"Marketing","plugin":"marketing","status":"stable","url":"/runes/marketing/cta","parentUrl":"/runes/marketing/","draft":false},"children":[]},{"id":"/runes/marketing/feature","type":"page","url":"/runes/marketing/feature","data":{"title":"Feature","description":"Feature showcases with name, description, and optional icons","category":"Marketing","plugin":"marketing","status":"stable","url":"/runes/marketing/feature","parentUrl":"/runes/marketing/","draft":false},"children":[]},{"id":"/runes/marketing/hero","type":"page","url":"/runes/marketing/hero","data":{"title":"Hero","description":"Full-width intro sections for landing pages with background support and action buttons","category":"Marketing","plugin":"marketing","status":"stable","url":"/runes/marketing/hero","parentUrl":"/runes/marketing/","draft":false},"children":[]},{"id":"/runes/marketing/pricing","type":"page","url":"/runes/marketing/pricing","data":{"title":"Pricing","description":"Pricing tables with tier comparison","category":"Marketing","plugin":"marketing","status":"stable","url":"/runes/marketing/pricing","parentUrl":"/runes/marketing/","draft":false},"children":[]},{"id":"/runes/marketing/steps","type":"page","url":"/runes/marketing/steps","data":{"title":"Steps","description":"Step-by-step instructions with numbered indicators","category":"Marketing","plugin":"marketing","status":"stable","url":"/runes/marketing/steps","parentUrl":"/runes/marketing/","draft":false},"children":[]},{"id":"/runes/marketing/testimonial","type":"page","url":"/runes/marketing/testimonial","data":{"title":"Testimonial","description":"Customer testimonials and reviews","category":"Marketing","plugin":"marketing","status":"stable","url":"/runes/marketing/testimonial","parentUrl":"/runes/marketing/","draft":false},"children":[]}]},{"id":"/runes/media","type":"page","url":"/runes/media","data":{"title":"@refrakt-md/media","description":"Audio and music runes for playlists, tracks, and audio players","icon":"video","url":"/runes/media","parentUrl":"/runes/","draft":false},"children":[{"id":"/runes/media/audio","type":"page","url":"/runes/media/audio","data":{"title":"Audio","description":"Audio player with optional waveform and chapter markers","category":"Media","plugin":"media","status":"stable","url":"/runes/media/audio","parentUrl":"/runes/media/","draft":false},"children":[]},{"id":"/runes/media/playlist","type":"page","url":"/runes/media/playlist","data":{"title":"Playlist","description":"Curated playlist with track listing for albums, podcasts, audiobooks, and mixes","category":"Media","plugin":"media","status":"stable","url":"/runes/media/playlist","parentUrl":"/runes/media/","draft":false},"children":[]},{"id":"/runes/media/track","type":"page","url":"/runes/media/track","data":{"title":"Track","description":"Standalone track or recording with metadata","category":"Media","plugin":"media","status":"stable","url":"/runes/media/track","parentUrl":"/runes/media/","draft":false},"children":[]}]},{"id":"/runes/places","type":"page","url":"/runes/places","data":{"title":"@refrakt-md/places","description":"Event planning and location runes for travel guides and venue pages","icon":"map-pin","url":"/runes/places","parentUrl":"/runes/","draft":false},"children":[{"id":"/runes/places/event","type":"page","url":"/runes/places/event","data":{"title":"Event","description":"Event information with date, location, and agenda","category":"Places","plugin":"places","status":"stable","url":"/runes/places/event","parentUrl":"/runes/places/","draft":false},"children":[]},{"id":"/runes/places/itinerary","type":"page","url":"/runes/places/itinerary","data":{"title":"Itinerary","description":"Day-by-day travel itineraries with timed stops and locations","category":"Places","plugin":"places","status":"stable","url":"/runes/places/itinerary","parentUrl":"/runes/places/","draft":false},"children":[]},{"id":"/runes/places/map","type":"page","url":"/runes/places/map","data":{"title":"Map","description":"Interactive map visualization from Markdown lists of locations","category":"Places","plugin":"places","status":"stable","url":"/runes/places/map","parentUrl":"/runes/places/","draft":false},"children":[]}]},{"id":"/runes/plan","type":"page","url":"/runes/plan","data":{"title":"@refrakt-md/plan","description":"Spec-driven project planning with AI-native workflows and CLI tooling","icon":"clipboard-list","url":"/runes/plan","parentUrl":"/runes/","draft":false},"children":[{"id":"/runes/plan/backlog","type":"page","url":"/runes/plan/backlog","data":{"title":"Backlog","description":"Aggregation view of plan entities with filtering, sorting, and grouping","category":"Plan","plugin":"plan","status":"stable","url":"/runes/plan/backlog","parentUrl":"/runes/plan/","draft":false},"children":[]},{"id":"/runes/plan/bug","type":"page","url":"/runes/plan/bug","data":{"title":"Bug","description":"Bug report with structured reproduction steps and severity tracking","category":"Plan","plugin":"plan","status":"stable","url":"/runes/plan/bug","parentUrl":"/runes/plan/","draft":false},"children":[]},{"id":"/runes/plan/cli","type":"page","url":"/runes/plan/cli","data":{"title":"Plan CLI Commands","description":"Command reference for refrakt plan — project management from the terminal","url":"/runes/plan/cli","parentUrl":"/runes/plan/","draft":false},"children":[]},{"id":"/runes/plan/decision-log","type":"page","url":"/runes/plan/decision-log","data":{"title":"Decision Log","description":"Chronological view of architecture decision records","category":"Plan","plugin":"plan","status":"stable","url":"/runes/plan/decision-log","parentUrl":"/runes/plan/","draft":false},"children":[]},{"id":"/runes/plan/decision","type":"page","url":"/runes/plan/decision","data":{"title":"Decision","description":"Architecture decision record capturing context, options, rationale, and consequences","category":"Plan","plugin":"plan","status":"stable","url":"/runes/plan/decision","parentUrl":"/runes/plan/","draft":false},"children":[]},{"id":"/runes/plan/examples","type":"page","url":"/runes/plan/examples","data":{"title":"Example Plan Data","description":"Sample plan entities used to populate aggregation rune showcases","url":"/runes/plan/examples","parentUrl":"/runes/plan/","draft":false},"children":[]},{"id":"/runes/plan/milestone","type":"page","url":"/runes/plan/milestone","data":{"title":"Milestone","description":"Named release target with scope, goals, and status tracking","category":"Plan","plugin":"plan","status":"stable","url":"/runes/plan/milestone","parentUrl":"/runes/plan/","draft":false},"children":[]},{"id":"/runes/plan/plan-activity","type":"page","url":"/runes/plan/plan-activity","data":{"title":"Plan Activity","description":"Recent activity feed sorted by file modification time","category":"Plan","plugin":"plan","status":"stable","url":"/runes/plan/plan-activity","parentUrl":"/runes/plan/","draft":false},"children":[]},{"id":"/runes/plan/plan-history","type":"page","url":"/runes/plan/plan-history","data":{"title":"Plan History","description":"Git-native entity history timeline showing lifecycle events derived from commits","category":"Plan","plugin":"plan","status":"stable","url":"/runes/plan/plan-history","parentUrl":"/runes/plan/","draft":false},"children":[]},{"id":"/runes/plan/plan-progress","type":"page","url":"/runes/plan/plan-progress","data":{"title":"Plan Progress","description":"Per-type completion bars and status breakdowns from the plan registry","category":"Plan","plugin":"plan","status":"stable","url":"/runes/plan/plan-progress","parentUrl":"/runes/plan/","draft":false},"children":[]},{"id":"/runes/plan/spec","type":"page","url":"/runes/plan/spec","data":{"title":"Spec","description":"Specification document with status tracking, versioning, and cross-referencing","category":"Plan","plugin":"plan","status":"stable","url":"/runes/plan/spec","parentUrl":"/runes/plan/","draft":false},"children":[]},{"id":"/runes/plan/work","type":"page","url":"/runes/plan/work","data":{"title":"Work","description":"Work item with acceptance criteria, references, and implementation tracking","category":"Plan","plugin":"plan","status":"stable","url":"/runes/plan/work","parentUrl":"/runes/plan/","draft":false},"children":[]},{"id":"/runes/plan/workflow","type":"page","url":"/runes/plan/workflow","data":{"title":"Plan Workflows","description":"Using @refrakt-md/plan with AI assistants, CI pipelines, and team workflows","url":"/runes/plan/workflow","parentUrl":"/runes/plan/","draft":false},"children":[]}]},{"id":"/runes/storytelling","type":"page","url":"/runes/storytelling","data":{"title":"@refrakt-md/storytelling","description":"Worldbuilding and narrative runes for fiction, games, and creative writing","icon":"sparkles","url":"/runes/storytelling","parentUrl":"/runes/","draft":false},"children":[{"id":"/runes/storytelling/bond","type":"page","url":"/runes/storytelling/bond","data":{"title":"Bond","description":"Relationship connections between characters or entities","category":"Storytelling","plugin":"storytelling","status":"stable","url":"/runes/storytelling/bond","parentUrl":"/runes/storytelling/","draft":false},"children":[]},{"id":"/runes/storytelling/character","type":"page","url":"/runes/storytelling/character","data":{"title":"Character","description":"Rich character profiles with sections for backstory, abilities, and more","category":"Storytelling","plugin":"storytelling","status":"stable","url":"/runes/storytelling/character","parentUrl":"/runes/storytelling/","draft":false},"children":[]},{"id":"/runes/storytelling/faction","type":"page","url":"/runes/storytelling/faction","data":{"title":"Faction","description":"Organizations and groups with ranks, holdings, and alignment","category":"Storytelling","plugin":"storytelling","status":"stable","url":"/runes/storytelling/faction","parentUrl":"/runes/storytelling/","draft":false},"children":[]},{"id":"/runes/storytelling/lore","type":"page","url":"/runes/storytelling/lore","data":{"title":"Lore","description":"In-world knowledge entries for myths, prophecies, and historical records","category":"Storytelling","plugin":"storytelling","status":"stable","url":"/runes/storytelling/lore","parentUrl":"/runes/storytelling/","draft":false},"children":[]},{"id":"/runes/storytelling/plot","type":"page","url":"/runes/storytelling/plot","data":{"title":"Plot","description":"Story arcs and quest trackers with progress markers","category":"Storytelling","plugin":"storytelling","status":"stable","url":"/runes/storytelling/plot","parentUrl":"/runes/storytelling/","draft":false},"children":[]},{"id":"/runes/storytelling/realm","type":"page","url":"/runes/storytelling/realm","data":{"title":"Realm","description":"Location profiles for worldbuilding with geography and notable features","category":"Storytelling","plugin":"storytelling","status":"stable","url":"/runes/storytelling/realm","parentUrl":"/runes/storytelling/","draft":false},"children":[]},{"id":"/runes/storytelling/storyboard","type":"page","url":"/runes/storytelling/storyboard","data":{"title":"Storyboard","description":"Comic and storyboard layouts from images and captions","category":"Storytelling","plugin":"storytelling","status":"stable","url":"/runes/storytelling/storyboard","parentUrl":"/runes/storytelling/","draft":false},"children":[]}]}]}" data-rune="sandbox" data-density="compact">
<template data-content="fallback">
<pre data-language="html"><code data-language="html"><div data-source="HTML">
<style>
html, body { height: 100%; margin: 0; overflow: hidden; }
/* A window into space — a deep dark backdrop in both themes so the stars glow. */
body { background: radial-gradient(ellipse at 50% 45%, #1b1a24 0%, #0b0a10 70%); }
#c { display: block; width: 100%; height: 100%; cursor: grab; touch-action: none; }
#c:active { cursor: grabbing; }
#tip {
position: fixed; left: 0; top: 0; pointer-events: none; z-index: 2;
padding: 2px 9px; border-radius: 6px; opacity: 0;
font: 12px/1.4 system-ui, -apple-system, sans-serif;
background: rgba(20,18,23,.92); color: #f6f4ef; white-space: nowrap;
transform: translate(-50%, -160%); transition: opacity .12s;
box-shadow: 0 2px 10px rgba(0,0,0,.5);
}
#fallback {
display: none; height: 100%; box-sizing: border-box; padding: 1.5rem;
align-items: center; justify-content: center; text-align: center;
font: 14px/1.5 system-ui, -apple-system, sans-serif; color: #9a96a4;
}
</style>
<canvas id="c"></canvas>
<div id="tip"></div>
<div id="fallback">A 3D star-map of the page registry renders here — it needs WebGL and JavaScript. The page list below is the accessible equivalent.</div>
<script type="module">
const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
const canvas = document.getElementById('c');
const tip = document.getElementById('tip');
const showFallback = () => { canvas.style.display = 'none'; document.getElementById('fallback').style.display = 'flex'; };
// Deterministic 0..1 hash from a string (stable layout across renders).
const hash = (s) => { let h = 2166136261; for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 16777619); } return ((h >>> 0) % 100000) / 100000; };
try {
const DATA = window.RF_DATA;
if (!DATA || DATA.shape !== 'tree' || !Array.isArray(DATA.tree) || DATA.tree.length === 0) throw new Error('no tree');
const THREE = await import('https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js');
// ── Build node records (parent / children / depth / subtree size) ────────
const all = [];
function build(node, parent, depth) {
const rec = { url: node.url, title: (node.data && node.data.title) || node.url, depth, parent, children: [], size: 1, world: new THREE.Vector3() };
all.push(rec);
(node.children || []).forEach((c) => rec.children.push(build(c, rec, depth + 1)));
rec.size += rec.children.reduce((s, c) => s + c.size, 0);
return rec;
}
const roots = DATA.tree.map((r) => build(r, null, 0));
// ── Lay roots out on a golden-angle galaxy disc (big systems near the core)
const GA = Math.PI * (3 - Math.sqrt(5));
roots.slice().sort((a, b) => b.size - a.size).forEach((r, i) => {
const rad = Math.sqrt(i + 0.7) * 2.7;
const ang = i * GA;
r.basePos = new THREE.Vector3(Math.cos(ang) * rad, (hash(r.url) - 0.5) * 3, Math.sin(ang) * rad);
});
// ── Give every descendant an orbit around its parent (nested, shrinking) ─
function assignOrbits(rec) {
const kids = rec.children;
if (!kids.length) return;
const ring = (1.5 + kids.length * 0.16) / (1 + rec.depth * 0.7);
const h = hash(rec.url || 'core');
const axis = new THREE.Vector3(Math.sin(h * 6.283) * 0.7, 1, Math.cos(h * 6.283) * 0.7).normalize();
const dir = rec.depth % 2 ? -1 : 1;
kids.forEach((k, i) => {
k.orbit = { radius: ring, angle0: (i / kids.length) * 6.283 + h * 6.283, speed: dir * 0.45 / ring, axis };
assignOrbits(k);
});
}
roots.forEach(assignOrbits);
// ── Scene ────────────────────────────────────────────────────────────────
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(55, 1, 0.1, 400);
const group = new THREE.Group();
scene.add(group);
// Galactic core glow + the light that warms the disc.
group.add(new THREE.Mesh(new THREE.SphereGeometry(0.7, 24, 24), new THREE.MeshBasicMaterial({ color: 0xffd9a8 })));
group.add(new THREE.PointLight(0xffe6c0, 2.2, 0, 0.6));
scene.add(new THREE.AmbientLight(0x8088aa, 0.5));
// Nodes — emissive spheres; stars (roots) sized by subtree, planets shrink.
const geo = new THREE.SphereGeometry(1, 18, 18);
const meshes = [];
for (const rec of all) {
const isStar = !rec.parent;
const r = isStar ? 0.22 + Math.min(rec.size, 40) * 0.012 : Math.max(0.07, 0.18 / rec.depth);
const color = isStar
? new THREE.Color().setHSL(0.96 - Math.min(rec.size, 28) * 0.006, 0.72, 0.62)
: new THREE.Color().setHSL(0.55 + rec.depth * 0.05, 0.55, 0.62);
const mat = new THREE.MeshStandardMaterial({ color, emissive: color, emissiveIntensity: isStar ? 0.7 : 0.35, roughness: 0.5, metalness: 0.1 });
const m = new THREE.Mesh(geo, mat);
m.scale.setScalar(r);
m.userData = { rec, baseScale: r, twinkle: hash(rec.url) * 6.283 };
group.add(m);
meshes.push(m);
}
// Orbit edges (parent → child), positions refreshed each frame.
const edgePairs = all.filter((r) => r.parent).map((r) => [r, r.parent]);
const edgePos = new Float32Array(edgePairs.length * 6);
const edgeGeo = new THREE.BufferGeometry();
edgeGeo.setAttribute('position', new THREE.BufferAttribute(edgePos, 3));
group.add(new THREE.LineSegments(edgeGeo, new THREE.LineBasicMaterial({ color: 0x8a86ff, transparent: true, opacity: 0.13 })));
// Distant starfield backdrop.
const SF = 1400, sf = new Float32Array(SF * 3);
for (let i = 0; i < SF; i++) {
const rr = 60 + Math.random() * 120, th = Math.random() * 6.283, ph = Math.acos(2 * Math.random() - 1);
sf[i * 3] = rr * Math.sin(ph) * Math.cos(th); sf[i * 3 + 1] = rr * Math.cos(ph); sf[i * 3 + 2] = rr * Math.sin(ph) * Math.sin(th);
}
const sfGeo = new THREE.BufferGeometry(); sfGeo.setAttribute('position', new THREE.BufferAttribute(sf, 3));
scene.add(new THREE.Points(sfGeo, new THREE.PointsMaterial({ color: 0xc9c6e0, size: 0.5, sizeAttenuation: true, transparent: true, opacity: 0.6 })));
// ── Position update (top-down: roots fixed, children orbit parents) ───────
const _u = new THREE.Vector3(), _v = new THREE.Vector3();
const perp = (n, o) => { o.set(Math.abs(n.x) < 0.9 ? 1 : 0, Math.abs(n.x) < 0.9 ? 0 : 1, 0); return o.crossVectors(n, o).normalize(); };
function place(rec, t) {
if (!rec.parent) rec.world.copy(rec.basePos);
else {
const o = rec.orbit, a = o.angle0 + t * o.speed;
perp(o.axis, _u); _v.crossVectors(o.axis, _u);
rec.world.copy(rec.parent.world).addScaledVector(_u, Math.cos(a) * o.radius).addScaledVector(_v, Math.sin(a) * o.radius);
}
rec.children.forEach((c) => place(c, t));
}
// Camera framing from the root disc bounds.
const box = new THREE.Box3(); roots.forEach((r) => box.expandByPoint(r.basePos));
const sph = box.getBoundingSphere(new THREE.Sphere());
const view = { center: sph.center.clone(), dist: Math.max(8, sph.radius / Math.sin((camera.fov / 2) * Math.PI / 180) * 1.15) };
function resize() {
const w = canvas.clientWidth, h = canvas.clientHeight; if (!w || !h) return;
renderer.setSize(w, h, false); camera.aspect = w / h; camera.updateProjectionMatrix();
}
resize(); window.addEventListener('resize', resize);
// ── Controls: drag to rotate, gentle auto-spin (off for reduced motion) ───
let rotX = 0.5, rotY = 0.5, dragging = false, lx = 0, ly = 0;
canvas.addEventListener('pointerdown', (e) => { dragging = true; lx = e.clientX; ly = e.clientY; canvas.setPointerCapture(e.pointerId); });
canvas.addEventListener('pointerup', (e) => { dragging = false; canvas.releasePointerCapture?.(e.pointerId); });
canvas.addEventListener('pointermove', (e) => {
if (dragging) { rotY += (e.clientX - lx) * 0.005; rotX = Math.max(-1.3, Math.min(1.3, rotX + (e.clientY - ly) * 0.005)); lx = e.clientX; ly = e.clientY; }
hover(e);
});
// ── Hover tooltip + click-to-navigate ────────────────────────────────────
const ray = new THREE.Raycaster(); const ndc = new THREE.Vector2(); let hovered = null;
function pick(e) {
const r = canvas.getBoundingClientRect();
ndc.set(((e.clientX - r.left) / r.width) * 2 - 1, -((e.clientY - r.top) / r.height) * 2 + 1);
ray.setFromCamera(ndc, camera);
return ray.intersectObjects(meshes, false)[0]?.object ?? null;
}
function hover(e) {
const hit = pick(e);
hovered = hit;
if (hovered) { tip.textContent = hovered.userData.rec.title; tip.style.left = e.clientX + 'px'; tip.style.top = e.clientY + 'px'; tip.style.opacity = '1'; canvas.style.cursor = 'pointer'; }
else { tip.style.opacity = '0'; canvas.style.cursor = dragging ? 'grabbing' : 'grab'; }
}
canvas.addEventListener('click', (e) => {
const hit = pick(e);
if (hit?.userData?.rec?.url) { try { window.top.location.href = hit.userData.rec.url; } catch { window.open(hit.userData.rec.url, '_top'); } }
});
// ── Loop ──────────────────────────────────────────────────────────────────
const t0 = performance.now();
function frame(now) {
const t = reduce ? 0 : (now - t0) / 1000;
for (const r of roots) place(r, t);
for (const m of meshes) {
m.position.copy(m.userData.rec.world);
const tw = reduce ? 1 : 1 + Math.sin(t * 2 + m.userData.twinkle) * (m.userData.rec.parent ? 0 : 0.12);
m.scale.setScalar(m.userData.baseScale * (m === hovered ? 1.5 : 1) * tw);
}
edgePairs.forEach(([a, b], i) => { a.world.toArray(edgePos, i * 6); b.world.toArray(edgePos, i * 6 + 3); });
edgeGeo.attributes.position.needsUpdate = true;
if (!reduce && !dragging) rotY += 0.0016;
group.rotation.set(rotX, rotY, 0);
camera.position.set(view.center.x, view.center.y + view.dist * 0.18, view.center.z + view.dist);
camera.lookAt(view.center);
renderer.render(scene, camera);
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
} catch (err) {
showFallback();
}
</script>
</div></code></pre>
</template>
<template data-content="source"><div data-source="HTML">
<style>
html, body { height: 100%; margin: 0; overflow: hidden; }
/* A window into space — a deep dark backdrop in both themes so the stars glow. */
body { background: radial-gradient(ellipse at 50% 45%, #1b1a24 0%, #0b0a10 70%); }
#c { display: block; width: 100%; height: 100%; cursor: grab; touch-action: none; }
#c:active { cursor: grabbing; }
#tip {
position: fixed; left: 0; top: 0; pointer-events: none; z-index: 2;
padding: 2px 9px; border-radius: 6px; opacity: 0;
font: 12px/1.4 system-ui, -apple-system, sans-serif;
background: rgba(20,18,23,.92); color: #f6f4ef; white-space: nowrap;
transform: translate(-50%, -160%); transition: opacity .12s;
box-shadow: 0 2px 10px rgba(0,0,0,.5);
}
#fallback {
display: none; height: 100%; box-sizing: border-box; padding: 1.5rem;
align-items: center; justify-content: center; text-align: center;
font: 14px/1.5 system-ui, -apple-system, sans-serif; color: #9a96a4;
}
</style>
<canvas id="c"></canvas>
<div id="tip"></div>
<div id="fallback">A 3D star-map of the page registry renders here — it needs WebGL and JavaScript. The page list below is the accessible equivalent.</div>
<script type="module">
const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
const canvas = document.getElementById('c');
const tip = document.getElementById('tip');
const showFallback = () => { canvas.style.display = 'none'; document.getElementById('fallback').style.display = 'flex'; };
// Deterministic 0..1 hash from a string (stable layout across renders).
const hash = (s) => { let h = 2166136261; for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 16777619); } return ((h >>> 0) % 100000) / 100000; };
try {
const DATA = window.RF_DATA;
if (!DATA || DATA.shape !== 'tree' || !Array.isArray(DATA.tree) || DATA.tree.length === 0) throw new Error('no tree');
const THREE = await import('https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js');
// ── Build node records (parent / children / depth / subtree size) ────────
const all = [];
function build(node, parent, depth) {
const rec = { url: node.url, title: (node.data && node.data.title) || node.url, depth, parent, children: [], size: 1, world: new THREE.Vector3() };
all.push(rec);
(node.children || []).forEach((c) => rec.children.push(build(c, rec, depth + 1)));
rec.size += rec.children.reduce((s, c) => s + c.size, 0);
return rec;
}
const roots = DATA.tree.map((r) => build(r, null, 0));
// ── Lay roots out on a golden-angle galaxy disc (big systems near the core)
const GA = Math.PI * (3 - Math.sqrt(5));
roots.slice().sort((a, b) => b.size - a.size).forEach((r, i) => {
const rad = Math.sqrt(i + 0.7) * 2.7;
const ang = i * GA;
r.basePos = new THREE.Vector3(Math.cos(ang) * rad, (hash(r.url) - 0.5) * 3, Math.sin(ang) * rad);
});
// ── Give every descendant an orbit around its parent (nested, shrinking) ─
function assignOrbits(rec) {
const kids = rec.children;
if (!kids.length) return;
const ring = (1.5 + kids.length * 0.16) / (1 + rec.depth * 0.7);
const h = hash(rec.url || 'core');
const axis = new THREE.Vector3(Math.sin(h * 6.283) * 0.7, 1, Math.cos(h * 6.283) * 0.7).normalize();
const dir = rec.depth % 2 ? -1 : 1;
kids.forEach((k, i) => {
k.orbit = { radius: ring, angle0: (i / kids.length) * 6.283 + h * 6.283, speed: dir * 0.45 / ring, axis };
assignOrbits(k);
});
}
roots.forEach(assignOrbits);
// ── Scene ────────────────────────────────────────────────────────────────
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(55, 1, 0.1, 400);
const group = new THREE.Group();
scene.add(group);
// Galactic core glow + the light that warms the disc.
group.add(new THREE.Mesh(new THREE.SphereGeometry(0.7, 24, 24), new THREE.MeshBasicMaterial({ color: 0xffd9a8 })));
group.add(new THREE.PointLight(0xffe6c0, 2.2, 0, 0.6));
scene.add(new THREE.AmbientLight(0x8088aa, 0.5));
// Nodes — emissive spheres; stars (roots) sized by subtree, planets shrink.
const geo = new THREE.SphereGeometry(1, 18, 18);
const meshes = [];
for (const rec of all) {
const isStar = !rec.parent;
const r = isStar ? 0.22 + Math.min(rec.size, 40) * 0.012 : Math.max(0.07, 0.18 / rec.depth);
const color = isStar
? new THREE.Color().setHSL(0.96 - Math.min(rec.size, 28) * 0.006, 0.72, 0.62)
: new THREE.Color().setHSL(0.55 + rec.depth * 0.05, 0.55, 0.62);
const mat = new THREE.MeshStandardMaterial({ color, emissive: color, emissiveIntensity: isStar ? 0.7 : 0.35, roughness: 0.5, metalness: 0.1 });
const m = new THREE.Mesh(geo, mat);
m.scale.setScalar(r);
m.userData = { rec, baseScale: r, twinkle: hash(rec.url) * 6.283 };
group.add(m);
meshes.push(m);
}
// Orbit edges (parent → child), positions refreshed each frame.
const edgePairs = all.filter((r) => r.parent).map((r) => [r, r.parent]);
const edgePos = new Float32Array(edgePairs.length * 6);
const edgeGeo = new THREE.BufferGeometry();
edgeGeo.setAttribute('position', new THREE.BufferAttribute(edgePos, 3));
group.add(new THREE.LineSegments(edgeGeo, new THREE.LineBasicMaterial({ color: 0x8a86ff, transparent: true, opacity: 0.13 })));
// Distant starfield backdrop.
const SF = 1400, sf = new Float32Array(SF * 3);
for (let i = 0; i < SF; i++) {
const rr = 60 + Math.random() * 120, th = Math.random() * 6.283, ph = Math.acos(2 * Math.random() - 1);
sf[i * 3] = rr * Math.sin(ph) * Math.cos(th); sf[i * 3 + 1] = rr * Math.cos(ph); sf[i * 3 + 2] = rr * Math.sin(ph) * Math.sin(th);
}
const sfGeo = new THREE.BufferGeometry(); sfGeo.setAttribute('position', new THREE.BufferAttribute(sf, 3));
scene.add(new THREE.Points(sfGeo, new THREE.PointsMaterial({ color: 0xc9c6e0, size: 0.5, sizeAttenuation: true, transparent: true, opacity: 0.6 })));
// ── Position update (top-down: roots fixed, children orbit parents) ───────
const _u = new THREE.Vector3(), _v = new THREE.Vector3();
const perp = (n, o) => { o.set(Math.abs(n.x) < 0.9 ? 1 : 0, Math.abs(n.x) < 0.9 ? 0 : 1, 0); return o.crossVectors(n, o).normalize(); };
function place(rec, t) {
if (!rec.parent) rec.world.copy(rec.basePos);
else {
const o = rec.orbit, a = o.angle0 + t * o.speed;
perp(o.axis, _u); _v.crossVectors(o.axis, _u);
rec.world.copy(rec.parent.world).addScaledVector(_u, Math.cos(a) * o.radius).addScaledVector(_v, Math.sin(a) * o.radius);
}
rec.children.forEach((c) => place(c, t));
}
// Camera framing from the root disc bounds.
const box = new THREE.Box3(); roots.forEach((r) => box.expandByPoint(r.basePos));
const sph = box.getBoundingSphere(new THREE.Sphere());
const view = { center: sph.center.clone(), dist: Math.max(8, sph.radius / Math.sin((camera.fov / 2) * Math.PI / 180) * 1.15) };
function resize() {
const w = canvas.clientWidth, h = canvas.clientHeight; if (!w || !h) return;
renderer.setSize(w, h, false); camera.aspect = w / h; camera.updateProjectionMatrix();
}
resize(); window.addEventListener('resize', resize);
// ── Controls: drag to rotate, gentle auto-spin (off for reduced motion) ───
let rotX = 0.5, rotY = 0.5, dragging = false, lx = 0, ly = 0;
canvas.addEventListener('pointerdown', (e) => { dragging = true; lx = e.clientX; ly = e.clientY; canvas.setPointerCapture(e.pointerId); });
canvas.addEventListener('pointerup', (e) => { dragging = false; canvas.releasePointerCapture?.(e.pointerId); });
canvas.addEventListener('pointermove', (e) => {
if (dragging) { rotY += (e.clientX - lx) * 0.005; rotX = Math.max(-1.3, Math.min(1.3, rotX + (e.clientY - ly) * 0.005)); lx = e.clientX; ly = e.clientY; }
hover(e);
});
// ── Hover tooltip + click-to-navigate ────────────────────────────────────
const ray = new THREE.Raycaster(); const ndc = new THREE.Vector2(); let hovered = null;
function pick(e) {
const r = canvas.getBoundingClientRect();
ndc.set(((e.clientX - r.left) / r.width) * 2 - 1, -((e.clientY - r.top) / r.height) * 2 + 1);
ray.setFromCamera(ndc, camera);
return ray.intersectObjects(meshes, false)[0]?.object ?? null;
}
function hover(e) {
const hit = pick(e);
hovered = hit;
if (hovered) { tip.textContent = hovered.userData.rec.title; tip.style.left = e.clientX + 'px'; tip.style.top = e.clientY + 'px'; tip.style.opacity = '1'; canvas.style.cursor = 'pointer'; }
else { tip.style.opacity = '0'; canvas.style.cursor = dragging ? 'grabbing' : 'grab'; }
}
canvas.addEventListener('click', (e) => {
const hit = pick(e);
if (hit?.userData?.rec?.url) { try { window.top.location.href = hit.userData.rec.url; } catch { window.open(hit.userData.rec.url, '_top'); } }
});
// ── Loop ──────────────────────────────────────────────────────────────────
const t0 = performance.now();
function frame(now) {
const t = reduce ? 0 : (now - t0) / 1000;
for (const r of roots) place(r, t);
for (const m of meshes) {
m.position.copy(m.userData.rec.world);
const tw = reduce ? 1 : 1 + Math.sin(t * 2 + m.userData.twinkle) * (m.userData.rec.parent ? 0 : 0.12);
m.scale.setScalar(m.userData.baseScale * (m === hovered ? 1.5 : 1) * tw);
}
edgePairs.forEach(([a, b], i) => { a.world.toArray(edgePos, i * 6); b.world.toArray(edgePos, i * 6 + 3); });
edgeGeo.attributes.position.needsUpdate = true;
if (!reduce && !dragging) rotY += 0.0016;
group.rotation.set(rotX, rotY, 0);
camera.position.set(view.center.x, view.center.y + view.dist * 0.18, view.center.z + view.dist);
camera.lookAt(view.center);
renderer.render(scene, camera);
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
} catch (err) {
showFallback();
}
</script>
</div></template>
<meta data-field="design-tokens" content="{"fonts":[{"role":"heading","family":"Inter","weights":[400,600,700],"category":"sans-serif"},{"role":"body","family":"Source Sans Pro","weights":[400,600],"category":"sans-serif"},{"role":"mono","family":"Fira Code","weights":[400],"category":"monospace"}],"colors":[{"name":"Primary","value":"#2563EB","group":"Brand"},{"name":"Secondary","value":"#7C3AED","group":"Brand"},{"name":"Accent","value":"#F59E0B","group":"Brand"},{"name":"Gray","value":"#F9FAFB","group":"Neutral"},{"name":"Gray","value":"#E5E7EB","group":"Neutral"},{"name":"Gray","value":"#9CA3AF","group":"Neutral"},{"name":"Gray","value":"#374151","group":"Neutral"},{"name":"Gray","value":"#111827","group":"Neutral"}],"spacing":{"unit":"4px","scale":["4","8","12","16","24","32","48","64"]},"radii":[{"name":"sm","value":"4px"},{"name":"md","value":"8px"},{"name":"lg","value":"12px"},{"name":"full","value":"9999px"}]}" />
</rf-sandbox>Always pair a data-bound visualization with an accessible fallback — the 3D view is progressive enhancement, not the only representation. Author a {% collection %} (or {% aggregate %}) running the same query in the sandbox's fallback slot so no-JS, no-WebGL, and screen-reader users still get the data as an honest list. (The fallbacks for the sandboxes on this page are omitted to keep it short.)
data-shape="graph" — nodes and edges
data-shape="graph" projects the queried entities as nodes and walks their relationship edges into a node-link payload — window.RF_DATA = { shape: "graph", nodes, edges }, where each edge is { from, to, kind }. Only edges whose both endpoints are in the selection are kept, so the graph is closed: ready for a node-link or force-directed layout.
Here a single data="type:spec type:work type:decision type:milestone" data-shape="graph" binding feeds this project's own plan — every spec, work item, decision, and milestone, wired by their SPEC-072 relationships — to a three.js force-directed graph. Specs, decisions, and milestones glow brightest; work items cluster around the specs they implement. Drag to rotate, hover a node for its title and status. Like the star-map, it's a heavy scene, so it mounts on activation="visible":
<style>
html, body { height: 100%; margin: 0; overflow: hidden; }
body { background: radial-gradient(ellipse at 50% 45%, #15151f 0%, #0a0a10 72%); }
#c { display: block; width: 100%; height: 100%; cursor: grab; touch-action: none; }
#c:active { cursor: grabbing; }
#tip {
position: fixed; left: 0; top: 0; pointer-events: none; z-index: 2;
padding: 3px 10px; border-radius: 6px; opacity: 0;
font: 12px/1.4 system-ui, -apple-system, sans-serif;
background: rgba(18,16,22,.94); color: #f6f4ef; white-space: nowrap;
transform: translate(-50%, -160%); transition: opacity .12s;
box-shadow: 0 2px 10px rgba(0,0,0,.55);
}
#tip b { color: #ffd9a8; font-weight: 600; }
#legend {
position: fixed; left: 12px; bottom: 12px; z-index: 2; display: flex; gap: 14px;
font: 11px/1 system-ui, -apple-system, sans-serif; color: #b8b4c4;
background: rgba(18,16,22,.55); padding: 8px 12px; border-radius: 8px;
}
#legend span { display: inline-flex; align-items: center; gap: 5px; }
#legend i { width: 9px; height: 9px; border-radius: 50%; display: inline-block; }
#fallback {
display: none; height: 100%; box-sizing: border-box; padding: 1.5rem;
align-items: center; justify-content: center; text-align: center;
font: 14px/1.5 system-ui, -apple-system, sans-serif; color: #9a96a4;
}
</style>
<canvas id="c"></canvas>
<div id="tip"></div>
<div id="legend">
<span><i style="background:#6ea8fe"></i>spec</span>
<span><i style="background:#e8788f"></i>work</span>
<span><i style="background:#ffd166"></i>decision</span>
<span><i style="background:#8ce99a"></i>milestone</span>
</div>
<div id="fallback">A 3D relationship graph of the plan renders here — it needs WebGL and JavaScript. The entity list below is the accessible equivalent.</div>
<script type="module">
const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
const canvas = document.getElementById('c');
const tip = document.getElementById('tip');
const showFallback = () => { canvas.style.display = 'none'; document.getElementById('legend').style.display = 'none'; document.getElementById('fallback').style.display = 'flex'; };
// Deterministic 0..1 hash — keeps the initial layout stable across renders.
const hash = (s) => { let h = 2166136261; for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 16777619); } return ((h >>> 0) % 100000) / 100000; };
const TYPE = {
spec: { color: 0x6ea8fe, r: 0.55, emissive: 0.7 },
decision: { color: 0xffd166, r: 0.52, emissive: 0.7 },
milestone: { color: 0x8ce99a, r: 0.60, emissive: 0.75 },
work: { color: 0xe8788f, r: 0.26, emissive: 0.35 },
};
const DEFAULT = { color: 0xb8b4c4, r: 0.28, emissive: 0.35 };
try {
const DATA = window.RF_DATA;
if (!DATA || DATA.shape !== 'graph' || !Array.isArray(DATA.nodes) || DATA.nodes.length === 0) throw new Error('no graph');
const THREE = await import('https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js');
const _v = new THREE.Vector3(), _u = new THREE.Vector3();
// Build nodes, spread on an initial sphere from a stable hash.
const byId = new Map();
const nodes = DATA.nodes.map((n) => {
const th = hash(n.id) * 6.283, ph = Math.acos(2 * hash(n.id + '~') - 1), rr = 6 + hash(n.id + '#') * 6;
const rec = {
id: n.id, type: n.type,
title: (n.data && n.data.title) || n.id,
status: n.data && n.data.status,
pos: new THREE.Vector3(rr * Math.sin(ph) * Math.cos(th), rr * Math.cos(ph), rr * Math.sin(ph) * Math.sin(th)),
disp: new THREE.Vector3(), deg: 0,
};
byId.set(n.id, rec);
return rec;
});
const edges = (DATA.edges || [])
.filter((e) => byId.has(e.from) && byId.has(e.to) && e.from !== e.to)
.map((e) => ({ a: byId.get(e.from), b: byId.get(e.to) }));
for (const e of edges) { e.a.deg++; e.b.deg++; }
// Force-directed layout (Fruchterman–Reingold), run up front.
const k = 5.5;
let temp = 8;
const ITERS = nodes.length > 320 ? 120 : 200;
for (let it = 0; it < ITERS; it++) {
for (const n of nodes) n.disp.set(0, 0, 0);
for (let i = 0; i < nodes.length; i++) {
const a = nodes[i];
for (let j = i + 1; j < nodes.length; j++) {
const b = nodes[j];
_v.subVectors(a.pos, b.pos);
let len = _v.length();
if (len < 0.05) { _v.set(hash(a.id + j) - 0.5, hash(b.id + i) - 0.5, hash(a.id + b.id) - 0.5); len = _v.length() || 0.05; }
_u.copy(_v).multiplyScalar((k * k) / (len * len));
a.disp.add(_u); b.disp.sub(_u);
}
}
for (const e of edges) {
_v.subVectors(e.a.pos, e.b.pos);
const len = _v.length() || 0.05;
_u.copy(_v).multiplyScalar((len * len) / k / len);
e.a.disp.sub(_u); e.b.disp.add(_u);
}
for (const n of nodes) {
n.disp.addScaledVector(n.pos, -0.05);
const dl = n.disp.length() || 1e-4;
n.pos.addScaledVector(n.disp, Math.min(dl, temp) / dl);
}
temp *= 0.975;
}
const ctr = new THREE.Vector3();
for (const n of nodes) ctr.add(n.pos);
ctr.multiplyScalar(1 / nodes.length);
for (const n of nodes) n.pos.sub(ctr);
// Normalise to a fixed radius so node sizes read the same at any node count.
let R0 = 0; for (const n of nodes) R0 = Math.max(R0, n.pos.length());
const scl = R0 > 0 ? 13 / R0 : 1;
for (const n of nodes) n.pos.multiplyScalar(scl);
// Scene.
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(55, 1, 0.1, 600);
const group = new THREE.Group();
scene.add(group);
scene.add(new THREE.AmbientLight(0x8088aa, 0.7));
const key = new THREE.PointLight(0xffe6c0, 1.6, 0, 0.4); key.position.set(20, 30, 25); scene.add(key);
const geo = new THREE.SphereGeometry(1, 16, 16);
const meshes = [];
for (const n of nodes) {
const t = TYPE[n.type] || DEFAULT;
const r = t.r + Math.min(n.deg, 12) * 0.012;
const color = new THREE.Color(t.color);
const mat = new THREE.MeshStandardMaterial({ color, emissive: color, emissiveIntensity: t.emissive, roughness: 0.5, metalness: 0.1 });
const m = new THREE.Mesh(geo, mat);
m.scale.setScalar(r);
m.position.copy(n.pos);
m.userData = { rec: n, baseScale: r };
group.add(m);
meshes.push(m);
}
const pos = new Float32Array(edges.length * 6);
edges.forEach((e, i) => { e.a.pos.toArray(pos, i * 6); e.b.pos.toArray(pos, i * 6 + 3); });
const edgeGeo = new THREE.BufferGeometry();
edgeGeo.setAttribute('position', new THREE.BufferAttribute(pos, 3));
group.add(new THREE.LineSegments(edgeGeo, new THREE.LineBasicMaterial({ color: 0x9a96ff, transparent: true, opacity: 0.2 })));
const SF = 900, sf = new Float32Array(SF * 3);
for (let i = 0; i < SF; i++) {
const rr = 80 + Math.random() * 140, th = Math.random() * 6.283, ph = Math.acos(2 * Math.random() - 1);
sf[i * 3] = rr * Math.sin(ph) * Math.cos(th); sf[i * 3 + 1] = rr * Math.cos(ph); sf[i * 3 + 2] = rr * Math.sin(ph) * Math.sin(th);
}
const sfGeo = new THREE.BufferGeometry(); sfGeo.setAttribute('position', new THREE.BufferAttribute(sf, 3));
scene.add(new THREE.Points(sfGeo, new THREE.PointsMaterial({ color: 0xc9c6e0, size: 0.5, sizeAttenuation: true, transparent: true, opacity: 0.5 })));
const box = new THREE.Box3(); nodes.forEach((n) => box.expandByPoint(n.pos));
const sph = box.getBoundingSphere(new THREE.Sphere());
const view = { center: sph.center.clone(), dist: Math.max(10, sph.radius / Math.sin((camera.fov / 2) * Math.PI / 180) * 1.1) };
function resize() {
const w = canvas.clientWidth, h = canvas.clientHeight; if (!w || !h) return;
renderer.setSize(w, h, false); camera.aspect = w / h; camera.updateProjectionMatrix();
}
resize(); window.addEventListener('resize', resize);
let rotX = 0.3, rotY = 0.4, dragging = false, lx = 0, ly = 0;
canvas.addEventListener('pointerdown', (e) => { dragging = true; lx = e.clientX; ly = e.clientY; canvas.setPointerCapture(e.pointerId); });
canvas.addEventListener('pointerup', (e) => { dragging = false; canvas.releasePointerCapture?.(e.pointerId); });
canvas.addEventListener('pointermove', (e) => {
if (dragging) { rotY += (e.clientX - lx) * 0.005; rotX = Math.max(-1.3, Math.min(1.3, rotX + (e.clientY - ly) * 0.005)); lx = e.clientX; ly = e.clientY; }
hover(e);
});
const ray = new THREE.Raycaster(); const ndc = new THREE.Vector2(); let hovered = null;
function pick(e) {
const r = canvas.getBoundingClientRect();
ndc.set(((e.clientX - r.left) / r.width) * 2 - 1, -((e.clientY - r.top) / r.height) * 2 + 1);
ray.setFromCamera(ndc, camera);
return ray.intersectObjects(meshes, false)[0]?.object ?? null;
}
function hover(e) {
hovered = pick(e);
if (hovered) {
const rec = hovered.userData.rec;
tip.innerHTML = '<b>' + rec.id + '</b> ' + rec.title + (rec.status ? ' · ' + rec.status : '');
tip.style.left = e.clientX + 'px'; tip.style.top = e.clientY + 'px'; tip.style.opacity = '1';
} else { tip.style.opacity = '0'; }
canvas.style.cursor = dragging ? 'grabbing' : 'grab';
}
// Under reduced motion the layout is static (no auto-spin); drag still works.
function frame() {
for (const m of meshes) m.scale.setScalar(m.userData.baseScale * (m === hovered ? 1.6 : 1));
if (!reduce && !dragging) rotY += 0.0014;
group.rotation.set(rotX, rotY, 0);
camera.position.set(view.center.x, view.center.y + view.dist * 0.15, view.center.z + view.dist);
camera.lookAt(view.center);
renderer.render(scene, camera);
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
} catch (err) {
showFallback();
}
</script><style>
html, body { height: 100%; margin: 0; overflow: hidden; }
body { background: radial-gradient(ellipse at 50% 45%, #15151f 0%, #0a0a10 72%); }
#c { display: block; width: 100%; height: 100%; cursor: grab; touch-action: none; }
#c:active { cursor: grabbing; }
#tip {
position: fixed; left: 0; top: 0; pointer-events: none; z-index: 2;
padding: 3px 10px; border-radius: 6px; opacity: 0;
font: 12px/1.4 system-ui, -apple-system, sans-serif;
background: rgba(18,16,22,.94); color: #f6f4ef; white-space: nowrap;
transform: translate(-50%, -160%); transition: opacity .12s;
box-shadow: 0 2px 10px rgba(0,0,0,.55);
}
#tip b { color: #ffd9a8; font-weight: 600; }
#legend {
position: fixed; left: 12px; bottom: 12px; z-index: 2; display: flex; gap: 14px;
font: 11px/1 system-ui, -apple-system, sans-serif; color: #b8b4c4;
background: rgba(18,16,22,.55); padding: 8px 12px; border-radius: 8px;
}
#legend span { display: inline-flex; align-items: center; gap: 5px; }
#legend i { width: 9px; height: 9px; border-radius: 50%; display: inline-block; }
#fallback {
display: none; height: 100%; box-sizing: border-box; padding: 1.5rem;
align-items: center; justify-content: center; text-align: center;
font: 14px/1.5 system-ui, -apple-system, sans-serif; color: #9a96a4;
}
</style>
<canvas id="c"></canvas>
<div id="tip"></div>
<div id="legend">
<span><i style="background:#6ea8fe"></i>spec</span>
<span><i style="background:#e8788f"></i>work</span>
<span><i style="background:#ffd166"></i>decision</span>
<span><i style="background:#8ce99a"></i>milestone</span>
</div>
<div id="fallback">A 3D relationship graph of the plan renders here — it needs WebGL and JavaScript. The entity list below is the accessible equivalent.</div>
<script type="module">
const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
const canvas = document.getElementById('c');
const tip = document.getElementById('tip');
const showFallback = () => { canvas.style.display = 'none'; document.getElementById('legend').style.display = 'none'; document.getElementById('fallback').style.display = 'flex'; };
// Deterministic 0..1 hash — keeps the initial layout stable across renders.
const hash = (s) => { let h = 2166136261; for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 16777619); } return ((h >>> 0) % 100000) / 100000; };
const TYPE = {
spec: { color: 0x6ea8fe, r: 0.55, emissive: 0.7 },
decision: { color: 0xffd166, r: 0.52, emissive: 0.7 },
milestone: { color: 0x8ce99a, r: 0.60, emissive: 0.75 },
work: { color: 0xe8788f, r: 0.26, emissive: 0.35 },
};
const DEFAULT = { color: 0xb8b4c4, r: 0.28, emissive: 0.35 };
try {
const DATA = window.RF_DATA;
if (!DATA || DATA.shape !== 'graph' || !Array.isArray(DATA.nodes) || DATA.nodes.length === 0) throw new Error('no graph');
const THREE = await import('https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js');
const _v = new THREE.Vector3(), _u = new THREE.Vector3();
// Build nodes, spread on an initial sphere from a stable hash.
const byId = new Map();
const nodes = DATA.nodes.map((n) => {
const th = hash(n.id) * 6.283, ph = Math.acos(2 * hash(n.id + '~') - 1), rr = 6 + hash(n.id + '#') * 6;
const rec = {
id: n.id, type: n.type,
title: (n.data && n.data.title) || n.id,
status: n.data && n.data.status,
pos: new THREE.Vector3(rr * Math.sin(ph) * Math.cos(th), rr * Math.cos(ph), rr * Math.sin(ph) * Math.sin(th)),
disp: new THREE.Vector3(), deg: 0,
};
byId.set(n.id, rec);
return rec;
});
const edges = (DATA.edges || [])
.filter((e) => byId.has(e.from) && byId.has(e.to) && e.from !== e.to)
.map((e) => ({ a: byId.get(e.from), b: byId.get(e.to) }));
for (const e of edges) { e.a.deg++; e.b.deg++; }
// Force-directed layout (Fruchterman–Reingold), run up front.
const k = 5.5;
let temp = 8;
const ITERS = nodes.length > 320 ? 120 : 200;
for (let it = 0; it < ITERS; it++) {
for (const n of nodes) n.disp.set(0, 0, 0);
for (let i = 0; i < nodes.length; i++) {
const a = nodes[i];
for (let j = i + 1; j < nodes.length; j++) {
const b = nodes[j];
_v.subVectors(a.pos, b.pos);
let len = _v.length();
if (len < 0.05) { _v.set(hash(a.id + j) - 0.5, hash(b.id + i) - 0.5, hash(a.id + b.id) - 0.5); len = _v.length() || 0.05; }
_u.copy(_v).multiplyScalar((k * k) / (len * len));
a.disp.add(_u); b.disp.sub(_u);
}
}
for (const e of edges) {
_v.subVectors(e.a.pos, e.b.pos);
const len = _v.length() || 0.05;
_u.copy(_v).multiplyScalar((len * len) / k / len);
e.a.disp.sub(_u); e.b.disp.add(_u);
}
for (const n of nodes) {
n.disp.addScaledVector(n.pos, -0.05);
const dl = n.disp.length() || 1e-4;
n.pos.addScaledVector(n.disp, Math.min(dl, temp) / dl);
}
temp *= 0.975;
}
const ctr = new THREE.Vector3();
for (const n of nodes) ctr.add(n.pos);
ctr.multiplyScalar(1 / nodes.length);
for (const n of nodes) n.pos.sub(ctr);
// Normalise to a fixed radius so node sizes read the same at any node count.
let R0 = 0; for (const n of nodes) R0 = Math.max(R0, n.pos.length());
const scl = R0 > 0 ? 13 / R0 : 1;
for (const n of nodes) n.pos.multiplyScalar(scl);
// Scene.
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(55, 1, 0.1, 600);
const group = new THREE.Group();
scene.add(group);
scene.add(new THREE.AmbientLight(0x8088aa, 0.7));
const key = new THREE.PointLight(0xffe6c0, 1.6, 0, 0.4); key.position.set(20, 30, 25); scene.add(key);
const geo = new THREE.SphereGeometry(1, 16, 16);
const meshes = [];
for (const n of nodes) {
const t = TYPE[n.type] || DEFAULT;
const r = t.r + Math.min(n.deg, 12) * 0.012;
const color = new THREE.Color(t.color);
const mat = new THREE.MeshStandardMaterial({ color, emissive: color, emissiveIntensity: t.emissive, roughness: 0.5, metalness: 0.1 });
const m = new THREE.Mesh(geo, mat);
m.scale.setScalar(r);
m.position.copy(n.pos);
m.userData = { rec: n, baseScale: r };
group.add(m);
meshes.push(m);
}
const pos = new Float32Array(edges.length * 6);
edges.forEach((e, i) => { e.a.pos.toArray(pos, i * 6); e.b.pos.toArray(pos, i * 6 + 3); });
const edgeGeo = new THREE.BufferGeometry();
edgeGeo.setAttribute('position', new THREE.BufferAttribute(pos, 3));
group.add(new THREE.LineSegments(edgeGeo, new THREE.LineBasicMaterial({ color: 0x9a96ff, transparent: true, opacity: 0.2 })));
const SF = 900, sf = new Float32Array(SF * 3);
for (let i = 0; i < SF; i++) {
const rr = 80 + Math.random() * 140, th = Math.random() * 6.283, ph = Math.acos(2 * Math.random() - 1);
sf[i * 3] = rr * Math.sin(ph) * Math.cos(th); sf[i * 3 + 1] = rr * Math.cos(ph); sf[i * 3 + 2] = rr * Math.sin(ph) * Math.sin(th);
}
const sfGeo = new THREE.BufferGeometry(); sfGeo.setAttribute('position', new THREE.BufferAttribute(sf, 3));
scene.add(new THREE.Points(sfGeo, new THREE.PointsMaterial({ color: 0xc9c6e0, size: 0.5, sizeAttenuation: true, transparent: true, opacity: 0.5 })));
const box = new THREE.Box3(); nodes.forEach((n) => box.expandByPoint(n.pos));
const sph = box.getBoundingSphere(new THREE.Sphere());
const view = { center: sph.center.clone(), dist: Math.max(10, sph.radius / Math.sin((camera.fov / 2) * Math.PI / 180) * 1.1) };
function resize() {
const w = canvas.clientWidth, h = canvas.clientHeight; if (!w || !h) return;
renderer.setSize(w, h, false); camera.aspect = w / h; camera.updateProjectionMatrix();
}
resize(); window.addEventListener('resize', resize);
let rotX = 0.3, rotY = 0.4, dragging = false, lx = 0, ly = 0;
canvas.addEventListener('pointerdown', (e) => { dragging = true; lx = e.clientX; ly = e.clientY; canvas.setPointerCapture(e.pointerId); });
canvas.addEventListener('pointerup', (e) => { dragging = false; canvas.releasePointerCapture?.(e.pointerId); });
canvas.addEventListener('pointermove', (e) => {
if (dragging) { rotY += (e.clientX - lx) * 0.005; rotX = Math.max(-1.3, Math.min(1.3, rotX + (e.clientY - ly) * 0.005)); lx = e.clientX; ly = e.clientY; }
hover(e);
});
const ray = new THREE.Raycaster(); const ndc = new THREE.Vector2(); let hovered = null;
function pick(e) {
const r = canvas.getBoundingClientRect();
ndc.set(((e.clientX - r.left) / r.width) * 2 - 1, -((e.clientY - r.top) / r.height) * 2 + 1);
ray.setFromCamera(ndc, camera);
return ray.intersectObjects(meshes, false)[0]?.object ?? null;
}
function hover(e) {
hovered = pick(e);
if (hovered) {
const rec = hovered.userData.rec;
tip.innerHTML = '<b>' + rec.id + '</b> ' + rec.title + (rec.status ? ' · ' + rec.status : '');
tip.style.left = e.clientX + 'px'; tip.style.top = e.clientY + 'px'; tip.style.opacity = '1';
} else { tip.style.opacity = '0'; }
canvas.style.cursor = dragging ? 'grabbing' : 'grab';
}
// Under reduced motion the layout is static (no auto-spin); drag still works.
function frame() {
for (const m of meshes) m.scale.setScalar(m.userData.baseScale * (m === hovered ? 1.6 : 1));
if (!reduce && !dragging) rotY += 0.0014;
group.rotation.set(rotX, rotY, 0);
camera.position.set(view.center.x, view.center.y + view.dist * 0.15, view.center.z + view.dist);
camera.lookAt(view.center);
renderer.render(scene, camera);
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
} catch (err) {
showFallback();
}
</script>Source code panels with data-source
When used inside a preview with source=true, you can mark elements with data-source to control what appears in the source tab. Unmarked elements (scaffolding, wrappers) are excluded from the source view but still render in the preview.
{% sandbox framework="tailwind" %}
<div class="min-h-[120px] flex items-center justify-center bg-gray-50 dark:bg-gray-900 p-4">
<button data-source class="bg-purple-600 text-white px-6 py-3 rounded-lg hover:bg-purple-700 font-medium transition-colors">
Click me
</button>
</div>
{% /sandbox %}<rf-sandbox data-rune="sandbox" data-source-content="<div class="min-h-[120px] flex items-center justify-center bg-gray-50 dark:bg-gray-900 p-4">
<button data-source class="bg-purple-600 text-white px-6 py-3 rounded-lg hover:bg-purple-700 font-medium transition-colors">
Click me
</button>
</div>" data-framework="tailwind" data-height="auto" data-security-mode="trusted" data-allow-js="true">
<template data-content="fallback">
<pre data-language="html">
<code data-language="html"><div class="min-h-[120px] flex items-center justify-center bg-gray-50 dark:bg-gray-900 p-4">
<button data-source class="bg-purple-600 text-white px-6 py-3 rounded-lg hover:bg-purple-700 font-medium transition-colors">
Click me
</button>
</div></code>
</pre>
</template>
<template data-content="source"><div class="min-h-[120px] flex items-center justify-center bg-gray-50 dark:bg-gray-900 p-4">
<button data-source class="bg-purple-600 text-white px-6 py-3 rounded-lg hover:bg-purple-700 font-medium transition-colors">
Click me
</button>
</div></template>
</rf-sandbox><div class="min-h-[120px] flex items-center justify-center bg-gray-50 dark:bg-gray-900 p-4">
<button data-source class="bg-purple-600 text-white px-6 py-3 rounded-lg hover:bg-purple-700 font-medium transition-colors">
Click me
</button>
</div><div class="min-h-[120px] flex items-center justify-center bg-gray-50 dark:bg-gray-900 p-4">
<button data-source class="bg-purple-600 text-white px-6 py-3 rounded-lg hover:bg-purple-700 font-medium transition-colors">
Click me
</button>
</div><rf-sandbox class="rf-sandbox" data-source-content="<div class="min-h-[120px] flex items-center justify-center bg-gray-50 dark:bg-gray-900 p-4">
<button data-source class="bg-purple-600 text-white px-6 py-3 rounded-lg hover:bg-purple-700 font-medium transition-colors">
Click me
</button>
</div>" data-framework="tailwind" data-height="auto" data-security-mode="trusted" data-allow-js="true" data-rune="sandbox" data-density="compact">
<template data-content="fallback">
<pre data-language="html"><code data-language="html"><div class="min-h-[120px] flex items-center justify-center bg-gray-50 dark:bg-gray-900 p-4">
<button data-source class="bg-purple-600 text-white px-6 py-3 rounded-lg hover:bg-purple-700 font-medium transition-colors">
Click me
</button>
</div></code></pre>
</template>
<template data-content="source"><div class="min-h-[120px] flex items-center justify-center bg-gray-50 dark:bg-gray-900 p-4">
<button data-source class="bg-purple-600 text-white px-6 py-3 rounded-lg hover:bg-purple-700 font-medium transition-colors">
Click me
</button>
</div></template>
<meta data-field="design-tokens" content="{"fonts":[{"role":"heading","family":"Inter","weights":[400,600,700],"category":"sans-serif"},{"role":"body","family":"Source Sans Pro","weights":[400,600],"category":"sans-serif"},{"role":"mono","family":"Fira Code","weights":[400],"category":"monospace"}],"colors":[{"name":"Primary","value":"#2563EB","group":"Brand"},{"name":"Secondary","value":"#7C3AED","group":"Brand"},{"name":"Accent","value":"#F59E0B","group":"Brand"},{"name":"Gray","value":"#F9FAFB","group":"Neutral"},{"name":"Gray","value":"#E5E7EB","group":"Neutral"},{"name":"Gray","value":"#9CA3AF","group":"Neutral"},{"name":"Gray","value":"#374151","group":"Neutral"},{"name":"Gray","value":"#111827","group":"Neutral"}],"spacing":{"unit":"4px","scale":["4","8","12","16","24","32","48","64"]},"radii":[{"name":"sm","value":"4px"},{"name":"md","value":"8px"},{"name":"lg","value":"12px"},{"name":"full","value":"9999px"}]}" />
</rf-sandbox>Named data-source values create labelled tabs in the source panel.
{% sandbox %}
<style data-source="CSS">
.card {
font-family: system-ui;
border: 1px solid #e5e7eb;
border-radius: 12px;
padding: 24px;
max-width: 300px;
}
.card h3 { margin: 0 0 8px; }
.card p { margin: 0; color: #6b7280; }
@media (prefers-color-scheme: dark) {
.card { border-color: #374151; }
.card p { color: #9ca3af; }
}
[data-theme="dark"] .card { border-color: #374151; }
[data-theme="dark"] .card p { color: #9ca3af; }
</style>
<div class="wrapper" style="padding: 24px;">
<div data-source="HTML" class="card">
<h3>Card Title</h3>
<p>Card content goes here.</p>
</div>
</div>
{% /sandbox %}<rf-sandbox data-rune="sandbox" data-source-content="<style data-source="CSS">
.card {
font-family: system-ui;
border: 1px solid #e5e7eb;
border-radius: 12px;
padding: 24px;
max-width: 300px;
}
.card h3 { margin: 0 0 8px; }
.card p { margin: 0; color: #6b7280; }
@media (prefers-color-scheme: dark) {
.card { border-color: #374151; }
.card p { color: #9ca3af; }
}
[data-theme="dark"] .card { border-color: #374151; }
[data-theme="dark"] .card p { color: #9ca3af; }
</style>
<div class="wrapper" style="padding: 24px;">
<div data-source="HTML" class="card">
<h3>Card Title</h3>
<p>Card content goes here.</p>
</div>
</div>" data-height="auto" data-security-mode="trusted" data-allow-js="true">
<template data-content="fallback">
<pre data-language="html">
<code data-language="html"><style data-source="CSS">
.card {
font-family: system-ui;
border: 1px solid #e5e7eb;
border-radius: 12px;
padding: 24px;
max-width: 300px;
}
.card h3 { margin: 0 0 8px; }
.card p { margin: 0; color: #6b7280; }
@media (prefers-color-scheme: dark) {
.card { border-color: #374151; }
.card p { color: #9ca3af; }
}
[data-theme="dark"] .card { border-color: #374151; }
[data-theme="dark"] .card p { color: #9ca3af; }
</style>
<div class="wrapper" style="padding: 24px;">
<div data-source="HTML" class="card">
<h3>Card Title</h3>
<p>Card content goes here.</p>
</div>
</div></code>
</pre>
</template>
<template data-content="source"><style data-source="CSS">
.card {
font-family: system-ui;
border: 1px solid #e5e7eb;
border-radius: 12px;
padding: 24px;
max-width: 300px;
}
.card h3 { margin: 0 0 8px; }
.card p { margin: 0; color: #6b7280; }
@media (prefers-color-scheme: dark) {
.card { border-color: #374151; }
.card p { color: #9ca3af; }
}
[data-theme="dark"] .card { border-color: #374151; }
[data-theme="dark"] .card p { color: #9ca3af; }
</style>
<div class="wrapper" style="padding: 24px;">
<div data-source="HTML" class="card">
<h3>Card Title</h3>
<p>Card content goes here.</p>
</div>
</div></template>
</rf-sandbox><style data-source="CSS">
.card {
font-family: system-ui;
border: 1px solid #e5e7eb;
border-radius: 12px;
padding: 24px;
max-width: 300px;
}
.card h3 { margin: 0 0 8px; }
.card p { margin: 0; color: #6b7280; }
@media (prefers-color-scheme: dark) {
.card { border-color: #374151; }
.card p { color: #9ca3af; }
}
[data-theme="dark"] .card { border-color: #374151; }
[data-theme="dark"] .card p { color: #9ca3af; }
</style>
<div class="wrapper" style="padding: 24px;">
<div data-source="HTML" class="card">
<h3>Card Title</h3>
<p>Card content goes here.</p>
</div>
</div><style data-source="CSS">
.card {
font-family: system-ui;
border: 1px solid #e5e7eb;
border-radius: 12px;
padding: 24px;
max-width: 300px;
}
.card h3 { margin: 0 0 8px; }
.card p { margin: 0; color: #6b7280; }
@media (prefers-color-scheme: dark) {
.card { border-color: #374151; }
.card p { color: #9ca3af; }
}
[data-theme="dark"] .card { border-color: #374151; }
[data-theme="dark"] .card p { color: #9ca3af; }
</style>
<div class="wrapper" style="padding: 24px;">
<div data-source="HTML" class="card">
<h3>Card Title</h3>
<p>Card content goes here.</p>
</div>
</div><rf-sandbox class="rf-sandbox" data-source-content="<style data-source="CSS">
.card {
font-family: system-ui;
border: 1px solid #e5e7eb;
border-radius: 12px;
padding: 24px;
max-width: 300px;
}
.card h3 { margin: 0 0 8px; }
.card p { margin: 0; color: #6b7280; }
@media (prefers-color-scheme: dark) {
.card { border-color: #374151; }
.card p { color: #9ca3af; }
}
[data-theme="dark"] .card { border-color: #374151; }
[data-theme="dark"] .card p { color: #9ca3af; }
</style>
<div class="wrapper" style="padding: 24px;">
<div data-source="HTML" class="card">
<h3>Card Title</h3>
<p>Card content goes here.</p>
</div>
</div>" data-height="auto" data-security-mode="trusted" data-allow-js="true" data-rune="sandbox" data-density="compact">
<template data-content="fallback">
<pre data-language="html"><code data-language="html"><style data-source="CSS">
.card {
font-family: system-ui;
border: 1px solid #e5e7eb;
border-radius: 12px;
padding: 24px;
max-width: 300px;
}
.card h3 { margin: 0 0 8px; }
.card p { margin: 0; color: #6b7280; }
@media (prefers-color-scheme: dark) {
.card { border-color: #374151; }
.card p { color: #9ca3af; }
}
[data-theme="dark"] .card { border-color: #374151; }
[data-theme="dark"] .card p { color: #9ca3af; }
</style>
<div class="wrapper" style="padding: 24px;">
<div data-source="HTML" class="card">
<h3>Card Title</h3>
<p>Card content goes here.</p>
</div>
</div></code></pre>
</template>
<template data-content="source"><style data-source="CSS">
.card {
font-family: system-ui;
border: 1px solid #e5e7eb;
border-radius: 12px;
padding: 24px;
max-width: 300px;
}
.card h3 { margin: 0 0 8px; }
.card p { margin: 0; color: #6b7280; }
@media (prefers-color-scheme: dark) {
.card { border-color: #374151; }
.card p { color: #9ca3af; }
}
[data-theme="dark"] .card { border-color: #374151; }
[data-theme="dark"] .card p { color: #9ca3af; }
</style>
<div class="wrapper" style="padding: 24px;">
<div data-source="HTML" class="card">
<h3>Card Title</h3>
<p>Card content goes here.</p>
</div>
</div></template>
<meta data-field="design-tokens" content="{"fonts":[{"role":"heading","family":"Inter","weights":[400,600,700],"category":"sans-serif"},{"role":"body","family":"Source Sans Pro","weights":[400,600],"category":"sans-serif"},{"role":"mono","family":"Fira Code","weights":[400],"category":"monospace"}],"colors":[{"name":"Primary","value":"#2563EB","group":"Brand"},{"name":"Secondary","value":"#7C3AED","group":"Brand"},{"name":"Accent","value":"#F59E0B","group":"Brand"},{"name":"Gray","value":"#F9FAFB","group":"Neutral"},{"name":"Gray","value":"#E5E7EB","group":"Neutral"},{"name":"Gray","value":"#9CA3AF","group":"Neutral"},{"name":"Gray","value":"#374151","group":"Neutral"},{"name":"Gray","value":"#111827","group":"Neutral"}],"spacing":{"unit":"4px","scale":["4","8","12","16","24","32","48","64"]},"radii":[{"name":"sm","value":"4px"},{"name":"md","value":"8px"},{"name":"lg","value":"12px"},{"name":"full","value":"9999px"}]}" />
</rf-sandbox>Standalone usage
Without a preview wrapper, the sandbox renders inline with no chrome — useful for embedding a live widget in the middle of prose.
<style>
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
.pulse-dot {
width: 12px; height: 12px; border-radius: 50%;
background: #7C3AED; animation: pulse 2s infinite;
}
</style>
<div class="pulse-dot"></div><style>
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
.pulse-dot {
width: 12px; height: 12px; border-radius: 50%;
background: #7C3AED; animation: pulse 2s infinite;
}
</style>
<div class="pulse-dot"></div>{% sandbox %}
<style>
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
.pulse-dot {
width: 12px; height: 12px; border-radius: 50%;
background: #7C3AED; animation: pulse 2s infinite;
}
</style>
<div class="pulse-dot"></div>
{% /sandbox %}
Tailwind card grid
A more complete example using Tailwind's utility classes for a responsive card layout.
{% sandbox framework="tailwind" %}
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 p-6">
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-100 dark:border-gray-700 p-6">
<div class="w-10 h-10 bg-purple-100 dark:bg-purple-900/50 rounded-lg flex items-center justify-center mb-4">
<svg class="w-5 h-5 text-purple-600 dark:text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
</svg>
</div>
<h3 class="font-semibold text-gray-900 dark:text-white mb-1">Fast</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">Built for speed with zero runtime overhead.</p>
</div>
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-100 dark:border-gray-700 p-6">
<div class="w-10 h-10 bg-blue-100 dark:bg-blue-900/50 rounded-lg flex items-center justify-center mb-4">
<svg class="w-5 h-5 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4"/>
</svg>
</div>
<h3 class="font-semibold text-gray-900 dark:text-white mb-1">Flexible</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">Works with any content structure.</p>
</div>
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-100 dark:border-gray-700 p-6">
<div class="w-10 h-10 bg-green-100 dark:bg-green-900/50 rounded-lg flex items-center justify-center mb-4">
<svg class="w-5 h-5 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/>
</svg>
</div>
<h3 class="font-semibold text-gray-900 dark:text-white mb-1">Secure</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">Fully isolated in a sandboxed iframe.</p>
</div>
</div>
{% /sandbox %}<rf-sandbox data-rune="sandbox" data-source-content="<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 p-6">
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-100 dark:border-gray-700 p-6">
<div class="w-10 h-10 bg-purple-100 dark:bg-purple-900/50 rounded-lg flex items-center justify-center mb-4">
<svg class="w-5 h-5 text-purple-600 dark:text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
</svg>
</div>
<h3 class="font-semibold text-gray-900 dark:text-white mb-1">Fast</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">Built for speed with zero runtime overhead.</p>
</div>
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-100 dark:border-gray-700 p-6">
<div class="w-10 h-10 bg-blue-100 dark:bg-blue-900/50 rounded-lg flex items-center justify-center mb-4">
<svg class="w-5 h-5 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4"/>
</svg>
</div>
<h3 class="font-semibold text-gray-900 dark:text-white mb-1">Flexible</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">Works with any content structure.</p>
</div>
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-100 dark:border-gray-700 p-6">
<div class="w-10 h-10 bg-green-100 dark:bg-green-900/50 rounded-lg flex items-center justify-center mb-4">
<svg class="w-5 h-5 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/>
</svg>
</div>
<h3 class="font-semibold text-gray-900 dark:text-white mb-1">Secure</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">Fully isolated in a sandboxed iframe.</p>
</div>
</div>" data-framework="tailwind" data-height="auto" data-security-mode="trusted" data-allow-js="true">
<template data-content="fallback">
<pre data-language="html">
<code data-language="html"><div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 p-6">
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-100 dark:border-gray-700 p-6">
<div class="w-10 h-10 bg-purple-100 dark:bg-purple-900/50 rounded-lg flex items-center justify-center mb-4">
<svg class="w-5 h-5 text-purple-600 dark:text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
</svg>
</div>
<h3 class="font-semibold text-gray-900 dark:text-white mb-1">Fast</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">Built for speed with zero runtime overhead.</p>
</div>
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-100 dark:border-gray-700 p-6">
<div class="w-10 h-10 bg-blue-100 dark:bg-blue-900/50 rounded-lg flex items-center justify-center mb-4">
<svg class="w-5 h-5 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4"/>
</svg>
</div>
<h3 class="font-semibold text-gray-900 dark:text-white mb-1">Flexible</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">Works with any content structure.</p>
</div>
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-100 dark:border-gray-700 p-6">
<div class="w-10 h-10 bg-green-100 dark:bg-green-900/50 rounded-lg flex items-center justify-center mb-4">
<svg class="w-5 h-5 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/>
</svg>
</div>
<h3 class="font-semibold text-gray-900 dark:text-white mb-1">Secure</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">Fully isolated in a sandboxed iframe.</p>
</div>
</div></code>
</pre>
</template>
<template data-content="source"><div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 p-6">
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-100 dark:border-gray-700 p-6">
<div class="w-10 h-10 bg-purple-100 dark:bg-purple-900/50 rounded-lg flex items-center justify-center mb-4">
<svg class="w-5 h-5 text-purple-600 dark:text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
</svg>
</div>
<h3 class="font-semibold text-gray-900 dark:text-white mb-1">Fast</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">Built for speed with zero runtime overhead.</p>
</div>
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-100 dark:border-gray-700 p-6">
<div class="w-10 h-10 bg-blue-100 dark:bg-blue-900/50 rounded-lg flex items-center justify-center mb-4">
<svg class="w-5 h-5 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4"/>
</svg>
</div>
<h3 class="font-semibold text-gray-900 dark:text-white mb-1">Flexible</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">Works with any content structure.</p>
</div>
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-100 dark:border-gray-700 p-6">
<div class="w-10 h-10 bg-green-100 dark:bg-green-900/50 rounded-lg flex items-center justify-center mb-4">
<svg class="w-5 h-5 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/>
</svg>
</div>
<h3 class="font-semibold text-gray-900 dark:text-white mb-1">Secure</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">Fully isolated in a sandboxed iframe.</p>
</div>
</div></template>
</rf-sandbox><div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 p-6">
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-100 dark:border-gray-700 p-6">
<div class="w-10 h-10 bg-purple-100 dark:bg-purple-900/50 rounded-lg flex items-center justify-center mb-4">
<svg class="w-5 h-5 text-purple-600 dark:text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
</svg>
</div>
<h3 class="font-semibold text-gray-900 dark:text-white mb-1">Fast</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">Built for speed with zero runtime overhead.</p>
</div>
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-100 dark:border-gray-700 p-6">
<div class="w-10 h-10 bg-blue-100 dark:bg-blue-900/50 rounded-lg flex items-center justify-center mb-4">
<svg class="w-5 h-5 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4"/>
</svg>
</div>
<h3 class="font-semibold text-gray-900 dark:text-white mb-1">Flexible</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">Works with any content structure.</p>
</div>
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-100 dark:border-gray-700 p-6">
<div class="w-10 h-10 bg-green-100 dark:bg-green-900/50 rounded-lg flex items-center justify-center mb-4">
<svg class="w-5 h-5 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/>
</svg>
</div>
<h3 class="font-semibold text-gray-900 dark:text-white mb-1">Secure</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">Fully isolated in a sandboxed iframe.</p>
</div>
</div><div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 p-6">
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-100 dark:border-gray-700 p-6">
<div class="w-10 h-10 bg-purple-100 dark:bg-purple-900/50 rounded-lg flex items-center justify-center mb-4">
<svg class="w-5 h-5 text-purple-600 dark:text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
</svg>
</div>
<h3 class="font-semibold text-gray-900 dark:text-white mb-1">Fast</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">Built for speed with zero runtime overhead.</p>
</div>
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-100 dark:border-gray-700 p-6">
<div class="w-10 h-10 bg-blue-100 dark:bg-blue-900/50 rounded-lg flex items-center justify-center mb-4">
<svg class="w-5 h-5 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4"/>
</svg>
</div>
<h3 class="font-semibold text-gray-900 dark:text-white mb-1">Flexible</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">Works with any content structure.</p>
</div>
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-100 dark:border-gray-700 p-6">
<div class="w-10 h-10 bg-green-100 dark:bg-green-900/50 rounded-lg flex items-center justify-center mb-4">
<svg class="w-5 h-5 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/>
</svg>
</div>
<h3 class="font-semibold text-gray-900 dark:text-white mb-1">Secure</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">Fully isolated in a sandboxed iframe.</p>
</div>
</div><rf-sandbox class="rf-sandbox" data-source-content="<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 p-6">
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-100 dark:border-gray-700 p-6">
<div class="w-10 h-10 bg-purple-100 dark:bg-purple-900/50 rounded-lg flex items-center justify-center mb-4">
<svg class="w-5 h-5 text-purple-600 dark:text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
</svg>
</div>
<h3 class="font-semibold text-gray-900 dark:text-white mb-1">Fast</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">Built for speed with zero runtime overhead.</p>
</div>
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-100 dark:border-gray-700 p-6">
<div class="w-10 h-10 bg-blue-100 dark:bg-blue-900/50 rounded-lg flex items-center justify-center mb-4">
<svg class="w-5 h-5 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4"/>
</svg>
</div>
<h3 class="font-semibold text-gray-900 dark:text-white mb-1">Flexible</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">Works with any content structure.</p>
</div>
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-100 dark:border-gray-700 p-6">
<div class="w-10 h-10 bg-green-100 dark:bg-green-900/50 rounded-lg flex items-center justify-center mb-4">
<svg class="w-5 h-5 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/>
</svg>
</div>
<h3 class="font-semibold text-gray-900 dark:text-white mb-1">Secure</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">Fully isolated in a sandboxed iframe.</p>
</div>
</div>" data-framework="tailwind" data-height="auto" data-security-mode="trusted" data-allow-js="true" data-rune="sandbox" data-density="compact">
<template data-content="fallback">
<pre data-language="html"><code data-language="html"><div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 p-6">
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-100 dark:border-gray-700 p-6">
<div class="w-10 h-10 bg-purple-100 dark:bg-purple-900/50 rounded-lg flex items-center justify-center mb-4">
<svg class="w-5 h-5 text-purple-600 dark:text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
</svg>
</div>
<h3 class="font-semibold text-gray-900 dark:text-white mb-1">Fast</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">Built for speed with zero runtime overhead.</p>
</div>
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-100 dark:border-gray-700 p-6">
<div class="w-10 h-10 bg-blue-100 dark:bg-blue-900/50 rounded-lg flex items-center justify-center mb-4">
<svg class="w-5 h-5 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4"/>
</svg>
</div>
<h3 class="font-semibold text-gray-900 dark:text-white mb-1">Flexible</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">Works with any content structure.</p>
</div>
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-100 dark:border-gray-700 p-6">
<div class="w-10 h-10 bg-green-100 dark:bg-green-900/50 rounded-lg flex items-center justify-center mb-4">
<svg class="w-5 h-5 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/>
</svg>
</div>
<h3 class="font-semibold text-gray-900 dark:text-white mb-1">Secure</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">Fully isolated in a sandboxed iframe.</p>
</div>
</div></code></pre>
</template>
<template data-content="source"><div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 p-6">
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-100 dark:border-gray-700 p-6">
<div class="w-10 h-10 bg-purple-100 dark:bg-purple-900/50 rounded-lg flex items-center justify-center mb-4">
<svg class="w-5 h-5 text-purple-600 dark:text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
</svg>
</div>
<h3 class="font-semibold text-gray-900 dark:text-white mb-1">Fast</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">Built for speed with zero runtime overhead.</p>
</div>
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-100 dark:border-gray-700 p-6">
<div class="w-10 h-10 bg-blue-100 dark:bg-blue-900/50 rounded-lg flex items-center justify-center mb-4">
<svg class="w-5 h-5 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4"/>
</svg>
</div>
<h3 class="font-semibold text-gray-900 dark:text-white mb-1">Flexible</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">Works with any content structure.</p>
</div>
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-100 dark:border-gray-700 p-6">
<div class="w-10 h-10 bg-green-100 dark:bg-green-900/50 rounded-lg flex items-center justify-center mb-4">
<svg class="w-5 h-5 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/>
</svg>
</div>
<h3 class="font-semibold text-gray-900 dark:text-white mb-1">Secure</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">Fully isolated in a sandboxed iframe.</p>
</div>
</div></template>
<meta data-field="design-tokens" content="{"fonts":[{"role":"heading","family":"Inter","weights":[400,600,700],"category":"sans-serif"},{"role":"body","family":"Source Sans Pro","weights":[400,600],"category":"sans-serif"},{"role":"mono","family":"Fira Code","weights":[400],"category":"monospace"}],"colors":[{"name":"Primary","value":"#2563EB","group":"Brand"},{"name":"Secondary","value":"#7C3AED","group":"Brand"},{"name":"Accent","value":"#F59E0B","group":"Brand"},{"name":"Gray","value":"#F9FAFB","group":"Neutral"},{"name":"Gray","value":"#E5E7EB","group":"Neutral"},{"name":"Gray","value":"#9CA3AF","group":"Neutral"},{"name":"Gray","value":"#374151","group":"Neutral"},{"name":"Gray","value":"#111827","group":"Neutral"}],"spacing":{"unit":"4px","scale":["4","8","12","16","24","32","48","64"]},"radii":[{"name":"sm","value":"4px"},{"name":"md","value":"8px"},{"name":"lg","value":"12px"},{"name":"full","value":"9999px"}]}" />
</rf-sandbox>Design token context
When your site uses {% design-context %} runes to define tokens, a sandbox can receive those tokens automatically at build time. Use the context attribute to reference a named design context by its scope.
{% sandbox context="brand" %}
<div style="color: var(--color-primary); font-family: var(--font-body)">
Styled with brand tokens
</div>
{% /sandbox %}
Tokens are injected into the iframe as CSS custom properties and Google Fonts links, using the same names the design-context defines (--color-*, --font-*, --spacing-unit, --radius-*, --shadow-*). If context is omitted or no matching design context exists for the default scope, no tokens are injected.
External source files
Instead of writing HTML inline, load sandbox content from a directory of source files. The src attribute points to a named subdirectory inside your project's examples directory.
{% sandbox src="login-form" /%}
Directory structure
By default, the examples directory is ../examples relative to your content root. Each example is a subdirectory containing the files for that sandbox.
project/
├── content/
│ └── docs/
│ └── components.md
└── examples/
└── login-form/
├── index.html
├── styles.css
└── app.js
File discovery
The sandbox scans the directory and assembles content from these file types:
| Extension | Role | Behavior |
|---|---|---|
.html | HTML body | If multiple exist, index.html is preferred |
.css | Stylesheet | Multiple files concatenated alphabetically |
.js | Script | Multiple files concatenated alphabetically |
.svg | SVG asset | Injected into the HTML body |
.glsl-vert | Vertex shader | Exposed as a VERTEX_SHADER JavaScript constant |
.glsl-frag | Fragment shader | Exposed as a FRAGMENT_SHADER JavaScript constant |
Discovered content is automatically wrapped in data-source annotated elements, so source panels appear when used inside {% preview source=true %}.
Combining with frameworks
The src attribute works alongside other sandbox attributes. For example, load files from a directory and apply a CSS framework:
{% sandbox src="profile-card" framework="tailwind" /%}<rf-sandbox data-rune="sandbox" data-source-content="<div data-source="HTML">
<div class="min-h-screen bg-[#f5f4f1] dark:bg-[#1a1a17] flex items-center justify-center p-6">
<div class="bg-[#fbfaf7] dark:bg-[#211f1c] rounded-2xl shadow-xl max-w-sm w-full overflow-hidden transition-colors">
<!-- Cover Image -->
<div class="h-32 bg-gradient-to-r from-[#e15f80] to-[#c4501c] dark:from-[#e8788f] dark:to-[#e87a3a]"></div>
<!-- Profile Content -->
<div class="relative px-6 pb-6">
<!-- Avatar -->
<div class="flex justify-center -mt-16 mb-4">
<img
src="https://i.pravatar.cc/120?img=32"
alt="Profile"
class="w-32 h-32 rounded-full border-4 border-[#fbfaf7] dark:border-[#211f1c] shadow-lg"
/>
</div>
<!-- User Info -->
<div class="text-center mb-6">
<h2 class="text-2xl font-bold text-[#1c1a17] dark:text-[#f6f4ef] mb-1">Sarah Johnson</h2>
<p class="text-[#6b6661] dark:text-[#94908a] text-sm mb-3">@sarahjohnson</p>
<p class="text-[#1c1a17]/80 dark:text-[#f6f4ef]/80 leading-relaxed">
Product designer passionate about crafting delightful user experiences.
Coffee enthusiast ☕️
</p>
</div>
<!-- Stats -->
<div class="flex justify-around py-4 border-y border-[#e2e0dd] dark:border-[#2a2825] mb-6">
<div class="text-center">
<div class="text-2xl font-bold text-[#1c1a17] dark:text-[#f6f4ef]">1.2k</div>
<div class="text-xs text-[#6b6661] dark:text-[#94908a] uppercase tracking-wide">Followers</div>
</div>
<div class="text-center">
<div class="text-2xl font-bold text-[#1c1a17] dark:text-[#f6f4ef]">842</div>
<div class="text-xs text-[#6b6661] dark:text-[#94908a] uppercase tracking-wide">Following</div>
</div>
<div class="text-center">
<div class="text-2xl font-bold text-[#1c1a17] dark:text-[#f6f4ef]">94</div>
<div class="text-xs text-[#6b6661] dark:text-[#94908a] uppercase tracking-wide">Posts</div>
</div>
</div>
<!-- Action Buttons -->
<div class="flex gap-3">
<button class="flex-1 bg-gradient-to-r from-[#e15f80] to-[#c84a6c] dark:from-[#e8788f] dark:to-[#ef91a3] text-white font-semibold py-3 rounded-xl hover:from-[#c84a6c] hover:to-[#b35070] dark:hover:from-[#ef91a3] dark:hover:to-[#e89db0] transition-all shadow-lg shadow-[#e15f80]/30 dark:shadow-[#e8788f]/20">
Follow
</button>
<button class="flex-1 border-2 border-[#e2e0dd] dark:border-[#2a2825] text-[#1c1a17] dark:text-[#f6f4ef] font-semibold py-3 rounded-xl hover:bg-[#ecebe8] dark:hover:bg-[#2a2825] transition-all">
Message
</button>
</div>
<!-- Social Links -->
<div class="flex justify-center gap-4 mt-6">
<a href="#" class="w-10 h-10 flex items-center justify-center rounded-full bg-[#ecebe8] dark:bg-[#2a2825] text-[#6b6661] dark:text-[#94908a] hover:bg-[#e15f80]/20 dark:hover:bg-[#e8788f]/20 hover:text-[#e15f80] dark:hover:text-[#e8788f] transition-all">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M22 12c0-5.523-4.477-10-10-10S2 6.477 2 12c0 4.991 3.657 9.128 8.438 9.878v-6.987h-2.54V12h2.54V9.797c0-2.506 1.492-3.89 3.777-3.89 1.094 0 2.238.195 2.238.195v2.46h-1.26c-1.243 0-1.63.771-1.63 1.562V12h2.773l-.443 2.89h-2.33v6.988C18.343 21.128 22 16.991 22 12z"/></svg>
</a>
<a href="#" class="w-10 h-10 flex items-center justify-center rounded-full bg-[#ecebe8] dark:bg-[#2a2825] text-[#6b6661] dark:text-[#94908a] hover:bg-[#e15f80]/20 dark:hover:bg-[#e8788f]/20 hover:text-[#e15f80] dark:hover:text-[#e8788f] transition-all">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M8.29 20.251c7.547 0 11.675-6.253 11.675-11.675 0-.178 0-.355-.012-.53A8.348 8.348 0 0022 5.92a8.19 8.19 0 01-2.357.646 4.118 4.118 0 001.804-2.27 8.224 8.224 0 01-2.605.996 4.107 4.107 0 00-6.993 3.743 11.65 11.65 0 01-8.457-4.287 4.106 4.106 0 001.27 5.477A4.072 4.072 0 012.8 9.713v.052a4.105 4.105 0 003.292 4.022 4.095 4.095 0 01-1.853.07 4.108 4.108 0 003.834 2.85A8.233 8.233 0 012 18.407a11.616 11.616 0 006.29 1.84"/></svg>
</a>
<a href="#" class="w-10 h-10 flex items-center justify-center rounded-full bg-[#ecebe8] dark:bg-[#2a2825] text-[#6b6661] dark:text-[#94908a] hover:bg-[#e15f80]/20 dark:hover:bg-[#e8788f]/20 hover:text-[#e15f80] dark:hover:text-[#e8788f] transition-all">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zm0-2.163c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948 0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072 3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98-1.281-.059-1.69-.073-4.949-.073zm0 5.838c-3.403 0-6.162 2.759-6.162 6.162s2.759 6.163 6.162 6.163 6.162-2.759 6.162-6.163c0-3.403-2.759-6.162-6.162-6.162zm0 10.162c-2.209 0-4-1.79-4 0-2.209 1.791-4 4-4s4 1.791 4 4c0 2.21-1.791 4-4 4zm6.406-11.845c-.796 0-1.441.645-1.441 1.44s.645 1.44 1.441 1.44c.795 0 1.439-.645 1.439-1.44s-.644-1.44-1.439-1.44z"/></svg>
</a>
</div>
</div>
</div>
</div>
</div>" data-framework="tailwind" data-height="auto" data-source-origins="HTML profile-card/index.html" data-security-mode="trusted" data-allow-js="true">
<template data-content="fallback">
<pre data-language="html">
<code data-language="html"><div data-source="HTML">
<div class="min-h-screen bg-[#f5f4f1] dark:bg-[#1a1a17] flex items-center justify-center p-6">
<div class="bg-[#fbfaf7] dark:bg-[#211f1c] rounded-2xl shadow-xl max-w-sm w-full overflow-hidden transition-colors">
<!-- Cover Image -->
<div class="h-32 bg-gradient-to-r from-[#e15f80] to-[#c4501c] dark:from-[#e8788f] dark:to-[#e87a3a]"></div>
<!-- Profile Content -->
<div class="relative px-6 pb-6">
<!-- Avatar -->
<div class="flex justify-center -mt-16 mb-4">
<img
src="https://i.pravatar.cc/120?img=32"
alt="Profile"
class="w-32 h-32 rounded-full border-4 border-[#fbfaf7] dark:border-[#211f1c] shadow-lg"
/>
</div>
<!-- User Info -->
<div class="text-center mb-6">
<h2 class="text-2xl font-bold text-[#1c1a17] dark:text-[#f6f4ef] mb-1">Sarah Johnson</h2>
<p class="text-[#6b6661] dark:text-[#94908a] text-sm mb-3">@sarahjohnson</p>
<p class="text-[#1c1a17]/80 dark:text-[#f6f4ef]/80 leading-relaxed">
Product designer passionate about crafting delightful user experiences.
Coffee enthusiast ☕️
</p>
</div>
<!-- Stats -->
<div class="flex justify-around py-4 border-y border-[#e2e0dd] dark:border-[#2a2825] mb-6">
<div class="text-center">
<div class="text-2xl font-bold text-[#1c1a17] dark:text-[#f6f4ef]">1.2k</div>
<div class="text-xs text-[#6b6661] dark:text-[#94908a] uppercase tracking-wide">Followers</div>
</div>
<div class="text-center">
<div class="text-2xl font-bold text-[#1c1a17] dark:text-[#f6f4ef]">842</div>
<div class="text-xs text-[#6b6661] dark:text-[#94908a] uppercase tracking-wide">Following</div>
</div>
<div class="text-center">
<div class="text-2xl font-bold text-[#1c1a17] dark:text-[#f6f4ef]">94</div>
<div class="text-xs text-[#6b6661] dark:text-[#94908a] uppercase tracking-wide">Posts</div>
</div>
</div>
<!-- Action Buttons -->
<div class="flex gap-3">
<button class="flex-1 bg-gradient-to-r from-[#e15f80] to-[#c84a6c] dark:from-[#e8788f] dark:to-[#ef91a3] text-white font-semibold py-3 rounded-xl hover:from-[#c84a6c] hover:to-[#b35070] dark:hover:from-[#ef91a3] dark:hover:to-[#e89db0] transition-all shadow-lg shadow-[#e15f80]/30 dark:shadow-[#e8788f]/20">
Follow
</button>
<button class="flex-1 border-2 border-[#e2e0dd] dark:border-[#2a2825] text-[#1c1a17] dark:text-[#f6f4ef] font-semibold py-3 rounded-xl hover:bg-[#ecebe8] dark:hover:bg-[#2a2825] transition-all">
Message
</button>
</div>
<!-- Social Links -->
<div class="flex justify-center gap-4 mt-6">
<a href="#" class="w-10 h-10 flex items-center justify-center rounded-full bg-[#ecebe8] dark:bg-[#2a2825] text-[#6b6661] dark:text-[#94908a] hover:bg-[#e15f80]/20 dark:hover:bg-[#e8788f]/20 hover:text-[#e15f80] dark:hover:text-[#e8788f] transition-all">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M22 12c0-5.523-4.477-10-10-10S2 6.477 2 12c0 4.991 3.657 9.128 8.438 9.878v-6.987h-2.54V12h2.54V9.797c0-2.506 1.492-3.89 3.777-3.89 1.094 0 2.238.195 2.238.195v2.46h-1.26c-1.243 0-1.63.771-1.63 1.562V12h2.773l-.443 2.89h-2.33v6.988C18.343 21.128 22 16.991 22 12z"/></svg>
</a>
<a href="#" class="w-10 h-10 flex items-center justify-center rounded-full bg-[#ecebe8] dark:bg-[#2a2825] text-[#6b6661] dark:text-[#94908a] hover:bg-[#e15f80]/20 dark:hover:bg-[#e8788f]/20 hover:text-[#e15f80] dark:hover:text-[#e8788f] transition-all">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M8.29 20.251c7.547 0 11.675-6.253 11.675-11.675 0-.178 0-.355-.012-.53A8.348 8.348 0 0022 5.92a8.19 8.19 0 01-2.357.646 4.118 4.118 0 001.804-2.27 8.224 8.224 0 01-2.605.996 4.107 4.107 0 00-6.993 3.743 11.65 11.65 0 01-8.457-4.287 4.106 4.106 0 001.27 5.477A4.072 4.072 0 012.8 9.713v.052a4.105 4.105 0 003.292 4.022 4.095 4.095 0 01-1.853.07 4.108 4.108 0 003.834 2.85A8.233 8.233 0 012 18.407a11.616 11.616 0 006.29 1.84"/></svg>
</a>
<a href="#" class="w-10 h-10 flex items-center justify-center rounded-full bg-[#ecebe8] dark:bg-[#2a2825] text-[#6b6661] dark:text-[#94908a] hover:bg-[#e15f80]/20 dark:hover:bg-[#e8788f]/20 hover:text-[#e15f80] dark:hover:text-[#e8788f] transition-all">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zm0-2.163c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948 0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072 3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98-1.281-.059-1.69-.073-4.949-.073zm0 5.838c-3.403 0-6.162 2.759-6.162 6.162s2.759 6.163 6.162 6.163 6.162-2.759 6.162-6.163c0-3.403-2.759-6.162-6.162-6.162zm0 10.162c-2.209 0-4-1.79-4 0-2.209 1.791-4 4-4s4 1.791 4 4c0 2.21-1.791 4-4 4zm6.406-11.845c-.796 0-1.441.645-1.441 1.44s.645 1.44 1.441 1.44c.795 0 1.439-.645 1.439-1.44s-.644-1.44-1.439-1.44z"/></svg>
</a>
</div>
</div>
</div>
</div>
</div></code>
</pre>
</template>
<template data-content="source"><div data-source="HTML">
<div class="min-h-screen bg-[#f5f4f1] dark:bg-[#1a1a17] flex items-center justify-center p-6">
<div class="bg-[#fbfaf7] dark:bg-[#211f1c] rounded-2xl shadow-xl max-w-sm w-full overflow-hidden transition-colors">
<!-- Cover Image -->
<div class="h-32 bg-gradient-to-r from-[#e15f80] to-[#c4501c] dark:from-[#e8788f] dark:to-[#e87a3a]"></div>
<!-- Profile Content -->
<div class="relative px-6 pb-6">
<!-- Avatar -->
<div class="flex justify-center -mt-16 mb-4">
<img
src="https://i.pravatar.cc/120?img=32"
alt="Profile"
class="w-32 h-32 rounded-full border-4 border-[#fbfaf7] dark:border-[#211f1c] shadow-lg"
/>
</div>
<!-- User Info -->
<div class="text-center mb-6">
<h2 class="text-2xl font-bold text-[#1c1a17] dark:text-[#f6f4ef] mb-1">Sarah Johnson</h2>
<p class="text-[#6b6661] dark:text-[#94908a] text-sm mb-3">@sarahjohnson</p>
<p class="text-[#1c1a17]/80 dark:text-[#f6f4ef]/80 leading-relaxed">
Product designer passionate about crafting delightful user experiences.
Coffee enthusiast ☕️
</p>
</div>
<!-- Stats -->
<div class="flex justify-around py-4 border-y border-[#e2e0dd] dark:border-[#2a2825] mb-6">
<div class="text-center">
<div class="text-2xl font-bold text-[#1c1a17] dark:text-[#f6f4ef]">1.2k</div>
<div class="text-xs text-[#6b6661] dark:text-[#94908a] uppercase tracking-wide">Followers</div>
</div>
<div class="text-center">
<div class="text-2xl font-bold text-[#1c1a17] dark:text-[#f6f4ef]">842</div>
<div class="text-xs text-[#6b6661] dark:text-[#94908a] uppercase tracking-wide">Following</div>
</div>
<div class="text-center">
<div class="text-2xl font-bold text-[#1c1a17] dark:text-[#f6f4ef]">94</div>
<div class="text-xs text-[#6b6661] dark:text-[#94908a] uppercase tracking-wide">Posts</div>
</div>
</div>
<!-- Action Buttons -->
<div class="flex gap-3">
<button class="flex-1 bg-gradient-to-r from-[#e15f80] to-[#c84a6c] dark:from-[#e8788f] dark:to-[#ef91a3] text-white font-semibold py-3 rounded-xl hover:from-[#c84a6c] hover:to-[#b35070] dark:hover:from-[#ef91a3] dark:hover:to-[#e89db0] transition-all shadow-lg shadow-[#e15f80]/30 dark:shadow-[#e8788f]/20">
Follow
</button>
<button class="flex-1 border-2 border-[#e2e0dd] dark:border-[#2a2825] text-[#1c1a17] dark:text-[#f6f4ef] font-semibold py-3 rounded-xl hover:bg-[#ecebe8] dark:hover:bg-[#2a2825] transition-all">
Message
</button>
</div>
<!-- Social Links -->
<div class="flex justify-center gap-4 mt-6">
<a href="#" class="w-10 h-10 flex items-center justify-center rounded-full bg-[#ecebe8] dark:bg-[#2a2825] text-[#6b6661] dark:text-[#94908a] hover:bg-[#e15f80]/20 dark:hover:bg-[#e8788f]/20 hover:text-[#e15f80] dark:hover:text-[#e8788f] transition-all">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M22 12c0-5.523-4.477-10-10-10S2 6.477 2 12c0 4.991 3.657 9.128 8.438 9.878v-6.987h-2.54V12h2.54V9.797c0-2.506 1.492-3.89 3.777-3.89 1.094 0 2.238.195 2.238.195v2.46h-1.26c-1.243 0-1.63.771-1.63 1.562V12h2.773l-.443 2.89h-2.33v6.988C18.343 21.128 22 16.991 22 12z"/></svg>
</a>
<a href="#" class="w-10 h-10 flex items-center justify-center rounded-full bg-[#ecebe8] dark:bg-[#2a2825] text-[#6b6661] dark:text-[#94908a] hover:bg-[#e15f80]/20 dark:hover:bg-[#e8788f]/20 hover:text-[#e15f80] dark:hover:text-[#e8788f] transition-all">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M8.29 20.251c7.547 0 11.675-6.253 11.675-11.675 0-.178 0-.355-.012-.53A8.348 8.348 0 0022 5.92a8.19 8.19 0 01-2.357.646 4.118 4.118 0 001.804-2.27 8.224 8.224 0 01-2.605.996 4.107 4.107 0 00-6.993 3.743 11.65 11.65 0 01-8.457-4.287 4.106 4.106 0 001.27 5.477A4.072 4.072 0 012.8 9.713v.052a4.105 4.105 0 003.292 4.022 4.095 4.095 0 01-1.853.07 4.108 4.108 0 003.834 2.85A8.233 8.233 0 012 18.407a11.616 11.616 0 006.29 1.84"/></svg>
</a>
<a href="#" class="w-10 h-10 flex items-center justify-center rounded-full bg-[#ecebe8] dark:bg-[#2a2825] text-[#6b6661] dark:text-[#94908a] hover:bg-[#e15f80]/20 dark:hover:bg-[#e8788f]/20 hover:text-[#e15f80] dark:hover:text-[#e8788f] transition-all">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zm0-2.163c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948 0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072 3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98-1.281-.059-1.69-.073-4.949-.073zm0 5.838c-3.403 0-6.162 2.759-6.162 6.162s2.759 6.163 6.162 6.163 6.162-2.759 6.162-6.163c0-3.403-2.759-6.162-6.162-6.162zm0 10.162c-2.209 0-4-1.79-4 0-2.209 1.791-4 4-4s4 1.791 4 4c0 2.21-1.791 4-4 4zm6.406-11.845c-.796 0-1.441.645-1.441 1.44s.645 1.44 1.441 1.44c.795 0 1.439-.645 1.439-1.44s-.644-1.44-1.439-1.44z"/></svg>
</a>
</div>
</div>
</div>
</div>
</div></template>
</rf-sandbox><div data-source="HTML">
<div class="min-h-screen bg-[#f5f4f1] dark:bg-[#1a1a17] flex items-center justify-center p-6">
<div class="bg-[#fbfaf7] dark:bg-[#211f1c] rounded-2xl shadow-xl max-w-sm w-full overflow-hidden transition-colors">
<!-- Cover Image -->
<div class="h-32 bg-gradient-to-r from-[#e15f80] to-[#c4501c] dark:from-[#e8788f] dark:to-[#e87a3a]"></div>
<!-- Profile Content -->
<div class="relative px-6 pb-6">
<!-- Avatar -->
<div class="flex justify-center -mt-16 mb-4">
<img
src="https://i.pravatar.cc/120?img=32"
alt="Profile"
class="w-32 h-32 rounded-full border-4 border-[#fbfaf7] dark:border-[#211f1c] shadow-lg"
/>
</div>
<!-- User Info -->
<div class="text-center mb-6">
<h2 class="text-2xl font-bold text-[#1c1a17] dark:text-[#f6f4ef] mb-1">Sarah Johnson</h2>
<p class="text-[#6b6661] dark:text-[#94908a] text-sm mb-3">@sarahjohnson</p>
<p class="text-[#1c1a17]/80 dark:text-[#f6f4ef]/80 leading-relaxed">
Product designer passionate about crafting delightful user experiences.
Coffee enthusiast ☕️
</p>
</div>
<!-- Stats -->
<div class="flex justify-around py-4 border-y border-[#e2e0dd] dark:border-[#2a2825] mb-6">
<div class="text-center">
<div class="text-2xl font-bold text-[#1c1a17] dark:text-[#f6f4ef]">1.2k</div>
<div class="text-xs text-[#6b6661] dark:text-[#94908a] uppercase tracking-wide">Followers</div>
</div>
<div class="text-center">
<div class="text-2xl font-bold text-[#1c1a17] dark:text-[#f6f4ef]">842</div>
<div class="text-xs text-[#6b6661] dark:text-[#94908a] uppercase tracking-wide">Following</div>
</div>
<div class="text-center">
<div class="text-2xl font-bold text-[#1c1a17] dark:text-[#f6f4ef]">94</div>
<div class="text-xs text-[#6b6661] dark:text-[#94908a] uppercase tracking-wide">Posts</div>
</div>
</div>
<!-- Action Buttons -->
<div class="flex gap-3">
<button class="flex-1 bg-gradient-to-r from-[#e15f80] to-[#c84a6c] dark:from-[#e8788f] dark:to-[#ef91a3] text-white font-semibold py-3 rounded-xl hover:from-[#c84a6c] hover:to-[#b35070] dark:hover:from-[#ef91a3] dark:hover:to-[#e89db0] transition-all shadow-lg shadow-[#e15f80]/30 dark:shadow-[#e8788f]/20">
Follow
</button>
<button class="flex-1 border-2 border-[#e2e0dd] dark:border-[#2a2825] text-[#1c1a17] dark:text-[#f6f4ef] font-semibold py-3 rounded-xl hover:bg-[#ecebe8] dark:hover:bg-[#2a2825] transition-all">
Message
</button>
</div>
<!-- Social Links -->
<div class="flex justify-center gap-4 mt-6">
<a href="#" class="w-10 h-10 flex items-center justify-center rounded-full bg-[#ecebe8] dark:bg-[#2a2825] text-[#6b6661] dark:text-[#94908a] hover:bg-[#e15f80]/20 dark:hover:bg-[#e8788f]/20 hover:text-[#e15f80] dark:hover:text-[#e8788f] transition-all">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M22 12c0-5.523-4.477-10-10-10S2 6.477 2 12c0 4.991 3.657 9.128 8.438 9.878v-6.987h-2.54V12h2.54V9.797c0-2.506 1.492-3.89 3.777-3.89 1.094 0 2.238.195 2.238.195v2.46h-1.26c-1.243 0-1.63.771-1.63 1.562V12h2.773l-.443 2.89h-2.33v6.988C18.343 21.128 22 16.991 22 12z"/></svg>
</a>
<a href="#" class="w-10 h-10 flex items-center justify-center rounded-full bg-[#ecebe8] dark:bg-[#2a2825] text-[#6b6661] dark:text-[#94908a] hover:bg-[#e15f80]/20 dark:hover:bg-[#e8788f]/20 hover:text-[#e15f80] dark:hover:text-[#e8788f] transition-all">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M8.29 20.251c7.547 0 11.675-6.253 11.675-11.675 0-.178 0-.355-.012-.53A8.348 8.348 0 0022 5.92a8.19 8.19 0 01-2.357.646 4.118 4.118 0 001.804-2.27 8.224 8.224 0 01-2.605.996 4.107 4.107 0 00-6.993 3.743 11.65 11.65 0 01-8.457-4.287 4.106 4.106 0 001.27 5.477A4.072 4.072 0 012.8 9.713v.052a4.105 4.105 0 003.292 4.022 4.095 4.095 0 01-1.853.07 4.108 4.108 0 003.834 2.85A8.233 8.233 0 012 18.407a11.616 11.616 0 006.29 1.84"/></svg>
</a>
<a href="#" class="w-10 h-10 flex items-center justify-center rounded-full bg-[#ecebe8] dark:bg-[#2a2825] text-[#6b6661] dark:text-[#94908a] hover:bg-[#e15f80]/20 dark:hover:bg-[#e8788f]/20 hover:text-[#e15f80] dark:hover:text-[#e8788f] transition-all">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zm0-2.163c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948 0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072 3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98-1.281-.059-1.69-.073-4.949-.073zm0 5.838c-3.403 0-6.162 2.759-6.162 6.162s2.759 6.163 6.162 6.163 6.162-2.759 6.162-6.163c0-3.403-2.759-6.162-6.162-6.162zm0 10.162c-2.209 0-4-1.79-4 0-2.209 1.791-4 4-4s4 1.791 4 4c0 2.21-1.791 4-4 4zm6.406-11.845c-.796 0-1.441.645-1.441 1.44s.645 1.44 1.441 1.44c.795 0 1.439-.645 1.439-1.44s-.644-1.44-1.439-1.44z"/></svg>
</a>
</div>
</div>
</div>
</div>
</div><div data-source="HTML">
<div class="min-h-screen bg-[#f5f4f1] dark:bg-[#1a1a17] flex items-center justify-center p-6">
<div class="bg-[#fbfaf7] dark:bg-[#211f1c] rounded-2xl shadow-xl max-w-sm w-full overflow-hidden transition-colors">
<!-- Cover Image -->
<div class="h-32 bg-gradient-to-r from-[#e15f80] to-[#c4501c] dark:from-[#e8788f] dark:to-[#e87a3a]"></div>
<!-- Profile Content -->
<div class="relative px-6 pb-6">
<!-- Avatar -->
<div class="flex justify-center -mt-16 mb-4">
<img
src="https://i.pravatar.cc/120?img=32"
alt="Profile"
class="w-32 h-32 rounded-full border-4 border-[#fbfaf7] dark:border-[#211f1c] shadow-lg"
/>
</div>
<!-- User Info -->
<div class="text-center mb-6">
<h2 class="text-2xl font-bold text-[#1c1a17] dark:text-[#f6f4ef] mb-1">Sarah Johnson</h2>
<p class="text-[#6b6661] dark:text-[#94908a] text-sm mb-3">@sarahjohnson</p>
<p class="text-[#1c1a17]/80 dark:text-[#f6f4ef]/80 leading-relaxed">
Product designer passionate about crafting delightful user experiences.
Coffee enthusiast ☕️
</p>
</div>
<!-- Stats -->
<div class="flex justify-around py-4 border-y border-[#e2e0dd] dark:border-[#2a2825] mb-6">
<div class="text-center">
<div class="text-2xl font-bold text-[#1c1a17] dark:text-[#f6f4ef]">1.2k</div>
<div class="text-xs text-[#6b6661] dark:text-[#94908a] uppercase tracking-wide">Followers</div>
</div>
<div class="text-center">
<div class="text-2xl font-bold text-[#1c1a17] dark:text-[#f6f4ef]">842</div>
<div class="text-xs text-[#6b6661] dark:text-[#94908a] uppercase tracking-wide">Following</div>
</div>
<div class="text-center">
<div class="text-2xl font-bold text-[#1c1a17] dark:text-[#f6f4ef]">94</div>
<div class="text-xs text-[#6b6661] dark:text-[#94908a] uppercase tracking-wide">Posts</div>
</div>
</div>
<!-- Action Buttons -->
<div class="flex gap-3">
<button class="flex-1 bg-gradient-to-r from-[#e15f80] to-[#c84a6c] dark:from-[#e8788f] dark:to-[#ef91a3] text-white font-semibold py-3 rounded-xl hover:from-[#c84a6c] hover:to-[#b35070] dark:hover:from-[#ef91a3] dark:hover:to-[#e89db0] transition-all shadow-lg shadow-[#e15f80]/30 dark:shadow-[#e8788f]/20">
Follow
</button>
<button class="flex-1 border-2 border-[#e2e0dd] dark:border-[#2a2825] text-[#1c1a17] dark:text-[#f6f4ef] font-semibold py-3 rounded-xl hover:bg-[#ecebe8] dark:hover:bg-[#2a2825] transition-all">
Message
</button>
</div>
<!-- Social Links -->
<div class="flex justify-center gap-4 mt-6">
<a href="#" class="w-10 h-10 flex items-center justify-center rounded-full bg-[#ecebe8] dark:bg-[#2a2825] text-[#6b6661] dark:text-[#94908a] hover:bg-[#e15f80]/20 dark:hover:bg-[#e8788f]/20 hover:text-[#e15f80] dark:hover:text-[#e8788f] transition-all">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M22 12c0-5.523-4.477-10-10-10S2 6.477 2 12c0 4.991 3.657 9.128 8.438 9.878v-6.987h-2.54V12h2.54V9.797c0-2.506 1.492-3.89 3.777-3.89 1.094 0 2.238.195 2.238.195v2.46h-1.26c-1.243 0-1.63.771-1.63 1.562V12h2.773l-.443 2.89h-2.33v6.988C18.343 21.128 22 16.991 22 12z"/></svg>
</a>
<a href="#" class="w-10 h-10 flex items-center justify-center rounded-full bg-[#ecebe8] dark:bg-[#2a2825] text-[#6b6661] dark:text-[#94908a] hover:bg-[#e15f80]/20 dark:hover:bg-[#e8788f]/20 hover:text-[#e15f80] dark:hover:text-[#e8788f] transition-all">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M8.29 20.251c7.547 0 11.675-6.253 11.675-11.675 0-.178 0-.355-.012-.53A8.348 8.348 0 0022 5.92a8.19 8.19 0 01-2.357.646 4.118 4.118 0 001.804-2.27 8.224 8.224 0 01-2.605.996 4.107 4.107 0 00-6.993 3.743 11.65 11.65 0 01-8.457-4.287 4.106 4.106 0 001.27 5.477A4.072 4.072 0 012.8 9.713v.052a4.105 4.105 0 003.292 4.022 4.095 4.095 0 01-1.853.07 4.108 4.108 0 003.834 2.85A8.233 8.233 0 012 18.407a11.616 11.616 0 006.29 1.84"/></svg>
</a>
<a href="#" class="w-10 h-10 flex items-center justify-center rounded-full bg-[#ecebe8] dark:bg-[#2a2825] text-[#6b6661] dark:text-[#94908a] hover:bg-[#e15f80]/20 dark:hover:bg-[#e8788f]/20 hover:text-[#e15f80] dark:hover:text-[#e8788f] transition-all">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zm0-2.163c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948 0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072 3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98-1.281-.059-1.69-.073-4.949-.073zm0 5.838c-3.403 0-6.162 2.759-6.162 6.162s2.759 6.163 6.162 6.163 6.162-2.759 6.162-6.163c0-3.403-2.759-6.162-6.162-6.162zm0 10.162c-2.209 0-4-1.79-4 0-2.209 1.791-4 4-4s4 1.791 4 4c0 2.21-1.791 4-4 4zm6.406-11.845c-.796 0-1.441.645-1.441 1.44s.645 1.44 1.441 1.44c.795 0 1.439-.645 1.439-1.44s-.644-1.44-1.439-1.44z"/></svg>
</a>
</div>
</div>
</div>
</div>
</div><rf-sandbox class="rf-sandbox" data-source-content="<div data-source="HTML">
<div class="min-h-screen bg-[#f5f4f1] dark:bg-[#1a1a17] flex items-center justify-center p-6">
<div class="bg-[#fbfaf7] dark:bg-[#211f1c] rounded-2xl shadow-xl max-w-sm w-full overflow-hidden transition-colors">
<!-- Cover Image -->
<div class="h-32 bg-gradient-to-r from-[#e15f80] to-[#c4501c] dark:from-[#e8788f] dark:to-[#e87a3a]"></div>
<!-- Profile Content -->
<div class="relative px-6 pb-6">
<!-- Avatar -->
<div class="flex justify-center -mt-16 mb-4">
<img
src="https://i.pravatar.cc/120?img=32"
alt="Profile"
class="w-32 h-32 rounded-full border-4 border-[#fbfaf7] dark:border-[#211f1c] shadow-lg"
/>
</div>
<!-- User Info -->
<div class="text-center mb-6">
<h2 class="text-2xl font-bold text-[#1c1a17] dark:text-[#f6f4ef] mb-1">Sarah Johnson</h2>
<p class="text-[#6b6661] dark:text-[#94908a] text-sm mb-3">@sarahjohnson</p>
<p class="text-[#1c1a17]/80 dark:text-[#f6f4ef]/80 leading-relaxed">
Product designer passionate about crafting delightful user experiences.
Coffee enthusiast ☕️
</p>
</div>
<!-- Stats -->
<div class="flex justify-around py-4 border-y border-[#e2e0dd] dark:border-[#2a2825] mb-6">
<div class="text-center">
<div class="text-2xl font-bold text-[#1c1a17] dark:text-[#f6f4ef]">1.2k</div>
<div class="text-xs text-[#6b6661] dark:text-[#94908a] uppercase tracking-wide">Followers</div>
</div>
<div class="text-center">
<div class="text-2xl font-bold text-[#1c1a17] dark:text-[#f6f4ef]">842</div>
<div class="text-xs text-[#6b6661] dark:text-[#94908a] uppercase tracking-wide">Following</div>
</div>
<div class="text-center">
<div class="text-2xl font-bold text-[#1c1a17] dark:text-[#f6f4ef]">94</div>
<div class="text-xs text-[#6b6661] dark:text-[#94908a] uppercase tracking-wide">Posts</div>
</div>
</div>
<!-- Action Buttons -->
<div class="flex gap-3">
<button class="flex-1 bg-gradient-to-r from-[#e15f80] to-[#c84a6c] dark:from-[#e8788f] dark:to-[#ef91a3] text-white font-semibold py-3 rounded-xl hover:from-[#c84a6c] hover:to-[#b35070] dark:hover:from-[#ef91a3] dark:hover:to-[#e89db0] transition-all shadow-lg shadow-[#e15f80]/30 dark:shadow-[#e8788f]/20">
Follow
</button>
<button class="flex-1 border-2 border-[#e2e0dd] dark:border-[#2a2825] text-[#1c1a17] dark:text-[#f6f4ef] font-semibold py-3 rounded-xl hover:bg-[#ecebe8] dark:hover:bg-[#2a2825] transition-all">
Message
</button>
</div>
<!-- Social Links -->
<div class="flex justify-center gap-4 mt-6">
<a href="#" class="w-10 h-10 flex items-center justify-center rounded-full bg-[#ecebe8] dark:bg-[#2a2825] text-[#6b6661] dark:text-[#94908a] hover:bg-[#e15f80]/20 dark:hover:bg-[#e8788f]/20 hover:text-[#e15f80] dark:hover:text-[#e8788f] transition-all">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M22 12c0-5.523-4.477-10-10-10S2 6.477 2 12c0 4.991 3.657 9.128 8.438 9.878v-6.987h-2.54V12h2.54V9.797c0-2.506 1.492-3.89 3.777-3.89 1.094 0 2.238.195 2.238.195v2.46h-1.26c-1.243 0-1.63.771-1.63 1.562V12h2.773l-.443 2.89h-2.33v6.988C18.343 21.128 22 16.991 22 12z"/></svg>
</a>
<a href="#" class="w-10 h-10 flex items-center justify-center rounded-full bg-[#ecebe8] dark:bg-[#2a2825] text-[#6b6661] dark:text-[#94908a] hover:bg-[#e15f80]/20 dark:hover:bg-[#e8788f]/20 hover:text-[#e15f80] dark:hover:text-[#e8788f] transition-all">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M8.29 20.251c7.547 0 11.675-6.253 11.675-11.675 0-.178 0-.355-.012-.53A8.348 8.348 0 0022 5.92a8.19 8.19 0 01-2.357.646 4.118 4.118 0 001.804-2.27 8.224 8.224 0 01-2.605.996 4.107 4.107 0 00-6.993 3.743 11.65 11.65 0 01-8.457-4.287 4.106 4.106 0 001.27 5.477A4.072 4.072 0 012.8 9.713v.052a4.105 4.105 0 003.292 4.022 4.095 4.095 0 01-1.853.07 4.108 4.108 0 003.834 2.85A8.233 8.233 0 012 18.407a11.616 11.616 0 006.29 1.84"/></svg>
</a>
<a href="#" class="w-10 h-10 flex items-center justify-center rounded-full bg-[#ecebe8] dark:bg-[#2a2825] text-[#6b6661] dark:text-[#94908a] hover:bg-[#e15f80]/20 dark:hover:bg-[#e8788f]/20 hover:text-[#e15f80] dark:hover:text-[#e8788f] transition-all">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zm0-2.163c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948 0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072 3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98-1.281-.059-1.69-.073-4.949-.073zm0 5.838c-3.403 0-6.162 2.759-6.162 6.162s2.759 6.163 6.162 6.163 6.162-2.759 6.162-6.163c0-3.403-2.759-6.162-6.162-6.162zm0 10.162c-2.209 0-4-1.79-4 0-2.209 1.791-4 4-4s4 1.791 4 4c0 2.21-1.791 4-4 4zm6.406-11.845c-.796 0-1.441.645-1.441 1.44s.645 1.44 1.441 1.44c.795 0 1.439-.645 1.439-1.44s-.644-1.44-1.439-1.44z"/></svg>
</a>
</div>
</div>
</div>
</div>
</div>" data-framework="tailwind" data-height="auto" data-source-origins="HTML profile-card/index.html" data-security-mode="trusted" data-allow-js="true" data-rune="sandbox" data-density="compact">
<template data-content="fallback">
<pre data-language="html"><code data-language="html"><div data-source="HTML">
<div class="min-h-screen bg-[#f5f4f1] dark:bg-[#1a1a17] flex items-center justify-center p-6">
<div class="bg-[#fbfaf7] dark:bg-[#211f1c] rounded-2xl shadow-xl max-w-sm w-full overflow-hidden transition-colors">
<!-- Cover Image -->
<div class="h-32 bg-gradient-to-r from-[#e15f80] to-[#c4501c] dark:from-[#e8788f] dark:to-[#e87a3a]"></div>
<!-- Profile Content -->
<div class="relative px-6 pb-6">
<!-- Avatar -->
<div class="flex justify-center -mt-16 mb-4">
<img
src="https://i.pravatar.cc/120?img=32"
alt="Profile"
class="w-32 h-32 rounded-full border-4 border-[#fbfaf7] dark:border-[#211f1c] shadow-lg"
/>
</div>
<!-- User Info -->
<div class="text-center mb-6">
<h2 class="text-2xl font-bold text-[#1c1a17] dark:text-[#f6f4ef] mb-1">Sarah Johnson</h2>
<p class="text-[#6b6661] dark:text-[#94908a] text-sm mb-3">@sarahjohnson</p>
<p class="text-[#1c1a17]/80 dark:text-[#f6f4ef]/80 leading-relaxed">
Product designer passionate about crafting delightful user experiences.
Coffee enthusiast ☕️
</p>
</div>
<!-- Stats -->
<div class="flex justify-around py-4 border-y border-[#e2e0dd] dark:border-[#2a2825] mb-6">
<div class="text-center">
<div class="text-2xl font-bold text-[#1c1a17] dark:text-[#f6f4ef]">1.2k</div>
<div class="text-xs text-[#6b6661] dark:text-[#94908a] uppercase tracking-wide">Followers</div>
</div>
<div class="text-center">
<div class="text-2xl font-bold text-[#1c1a17] dark:text-[#f6f4ef]">842</div>
<div class="text-xs text-[#6b6661] dark:text-[#94908a] uppercase tracking-wide">Following</div>
</div>
<div class="text-center">
<div class="text-2xl font-bold text-[#1c1a17] dark:text-[#f6f4ef]">94</div>
<div class="text-xs text-[#6b6661] dark:text-[#94908a] uppercase tracking-wide">Posts</div>
</div>
</div>
<!-- Action Buttons -->
<div class="flex gap-3">
<button class="flex-1 bg-gradient-to-r from-[#e15f80] to-[#c84a6c] dark:from-[#e8788f] dark:to-[#ef91a3] text-white font-semibold py-3 rounded-xl hover:from-[#c84a6c] hover:to-[#b35070] dark:hover:from-[#ef91a3] dark:hover:to-[#e89db0] transition-all shadow-lg shadow-[#e15f80]/30 dark:shadow-[#e8788f]/20">
Follow
</button>
<button class="flex-1 border-2 border-[#e2e0dd] dark:border-[#2a2825] text-[#1c1a17] dark:text-[#f6f4ef] font-semibold py-3 rounded-xl hover:bg-[#ecebe8] dark:hover:bg-[#2a2825] transition-all">
Message
</button>
</div>
<!-- Social Links -->
<div class="flex justify-center gap-4 mt-6">
<a href="#" class="w-10 h-10 flex items-center justify-center rounded-full bg-[#ecebe8] dark:bg-[#2a2825] text-[#6b6661] dark:text-[#94908a] hover:bg-[#e15f80]/20 dark:hover:bg-[#e8788f]/20 hover:text-[#e15f80] dark:hover:text-[#e8788f] transition-all">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M22 12c0-5.523-4.477-10-10-10S2 6.477 2 12c0 4.991 3.657 9.128 8.438 9.878v-6.987h-2.54V12h2.54V9.797c0-2.506 1.492-3.89 3.777-3.89 1.094 0 2.238.195 2.238.195v2.46h-1.26c-1.243 0-1.63.771-1.63 1.562V12h2.773l-.443 2.89h-2.33v6.988C18.343 21.128 22 16.991 22 12z"/></svg>
</a>
<a href="#" class="w-10 h-10 flex items-center justify-center rounded-full bg-[#ecebe8] dark:bg-[#2a2825] text-[#6b6661] dark:text-[#94908a] hover:bg-[#e15f80]/20 dark:hover:bg-[#e8788f]/20 hover:text-[#e15f80] dark:hover:text-[#e8788f] transition-all">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M8.29 20.251c7.547 0 11.675-6.253 11.675-11.675 0-.178 0-.355-.012-.53A8.348 8.348 0 0022 5.92a8.19 8.19 0 01-2.357.646 4.118 4.118 0 001.804-2.27 8.224 8.224 0 01-2.605.996 4.107 4.107 0 00-6.993 3.743 11.65 11.65 0 01-8.457-4.287 4.106 4.106 0 001.27 5.477A4.072 4.072 0 012.8 9.713v.052a4.105 4.105 0 003.292 4.022 4.095 4.095 0 01-1.853.07 4.108 4.108 0 003.834 2.85A8.233 8.233 0 012 18.407a11.616 11.616 0 006.29 1.84"/></svg>
</a>
<a href="#" class="w-10 h-10 flex items-center justify-center rounded-full bg-[#ecebe8] dark:bg-[#2a2825] text-[#6b6661] dark:text-[#94908a] hover:bg-[#e15f80]/20 dark:hover:bg-[#e8788f]/20 hover:text-[#e15f80] dark:hover:text-[#e8788f] transition-all">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zm0-2.163c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948 0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072 3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98-1.281-.059-1.69-.073-4.949-.073zm0 5.838c-3.403 0-6.162 2.759-6.162 6.162s2.759 6.163 6.162 6.163 6.162-2.759 6.162-6.163c0-3.403-2.759-6.162-6.162-6.162zm0 10.162c-2.209 0-4-1.79-4 0-2.209 1.791-4 4-4s4 1.791 4 4c0 2.21-1.791 4-4 4zm6.406-11.845c-.796 0-1.441.645-1.441 1.44s.645 1.44 1.441 1.44c.795 0 1.439-.645 1.439-1.44s-.644-1.44-1.439-1.44z"/></svg>
</a>
</div>
</div>
</div>
</div>
</div></code></pre>
</template>
<template data-content="source"><div data-source="HTML">
<div class="min-h-screen bg-[#f5f4f1] dark:bg-[#1a1a17] flex items-center justify-center p-6">
<div class="bg-[#fbfaf7] dark:bg-[#211f1c] rounded-2xl shadow-xl max-w-sm w-full overflow-hidden transition-colors">
<!-- Cover Image -->
<div class="h-32 bg-gradient-to-r from-[#e15f80] to-[#c4501c] dark:from-[#e8788f] dark:to-[#e87a3a]"></div>
<!-- Profile Content -->
<div class="relative px-6 pb-6">
<!-- Avatar -->
<div class="flex justify-center -mt-16 mb-4">
<img
src="https://i.pravatar.cc/120?img=32"
alt="Profile"
class="w-32 h-32 rounded-full border-4 border-[#fbfaf7] dark:border-[#211f1c] shadow-lg"
/>
</div>
<!-- User Info -->
<div class="text-center mb-6">
<h2 class="text-2xl font-bold text-[#1c1a17] dark:text-[#f6f4ef] mb-1">Sarah Johnson</h2>
<p class="text-[#6b6661] dark:text-[#94908a] text-sm mb-3">@sarahjohnson</p>
<p class="text-[#1c1a17]/80 dark:text-[#f6f4ef]/80 leading-relaxed">
Product designer passionate about crafting delightful user experiences.
Coffee enthusiast ☕️
</p>
</div>
<!-- Stats -->
<div class="flex justify-around py-4 border-y border-[#e2e0dd] dark:border-[#2a2825] mb-6">
<div class="text-center">
<div class="text-2xl font-bold text-[#1c1a17] dark:text-[#f6f4ef]">1.2k</div>
<div class="text-xs text-[#6b6661] dark:text-[#94908a] uppercase tracking-wide">Followers</div>
</div>
<div class="text-center">
<div class="text-2xl font-bold text-[#1c1a17] dark:text-[#f6f4ef]">842</div>
<div class="text-xs text-[#6b6661] dark:text-[#94908a] uppercase tracking-wide">Following</div>
</div>
<div class="text-center">
<div class="text-2xl font-bold text-[#1c1a17] dark:text-[#f6f4ef]">94</div>
<div class="text-xs text-[#6b6661] dark:text-[#94908a] uppercase tracking-wide">Posts</div>
</div>
</div>
<!-- Action Buttons -->
<div class="flex gap-3">
<button class="flex-1 bg-gradient-to-r from-[#e15f80] to-[#c84a6c] dark:from-[#e8788f] dark:to-[#ef91a3] text-white font-semibold py-3 rounded-xl hover:from-[#c84a6c] hover:to-[#b35070] dark:hover:from-[#ef91a3] dark:hover:to-[#e89db0] transition-all shadow-lg shadow-[#e15f80]/30 dark:shadow-[#e8788f]/20">
Follow
</button>
<button class="flex-1 border-2 border-[#e2e0dd] dark:border-[#2a2825] text-[#1c1a17] dark:text-[#f6f4ef] font-semibold py-3 rounded-xl hover:bg-[#ecebe8] dark:hover:bg-[#2a2825] transition-all">
Message
</button>
</div>
<!-- Social Links -->
<div class="flex justify-center gap-4 mt-6">
<a href="#" class="w-10 h-10 flex items-center justify-center rounded-full bg-[#ecebe8] dark:bg-[#2a2825] text-[#6b6661] dark:text-[#94908a] hover:bg-[#e15f80]/20 dark:hover:bg-[#e8788f]/20 hover:text-[#e15f80] dark:hover:text-[#e8788f] transition-all">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M22 12c0-5.523-4.477-10-10-10S2 6.477 2 12c0 4.991 3.657 9.128 8.438 9.878v-6.987h-2.54V12h2.54V9.797c0-2.506 1.492-3.89 3.777-3.89 1.094 0 2.238.195 2.238.195v2.46h-1.26c-1.243 0-1.63.771-1.63 1.562V12h2.773l-.443 2.89h-2.33v6.988C18.343 21.128 22 16.991 22 12z"/></svg>
</a>
<a href="#" class="w-10 h-10 flex items-center justify-center rounded-full bg-[#ecebe8] dark:bg-[#2a2825] text-[#6b6661] dark:text-[#94908a] hover:bg-[#e15f80]/20 dark:hover:bg-[#e8788f]/20 hover:text-[#e15f80] dark:hover:text-[#e8788f] transition-all">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M8.29 20.251c7.547 0 11.675-6.253 11.675-11.675 0-.178 0-.355-.012-.53A8.348 8.348 0 0022 5.92a8.19 8.19 0 01-2.357.646 4.118 4.118 0 001.804-2.27 8.224 8.224 0 01-2.605.996 4.107 4.107 0 00-6.993 3.743 11.65 11.65 0 01-8.457-4.287 4.106 4.106 0 001.27 5.477A4.072 4.072 0 012.8 9.713v.052a4.105 4.105 0 003.292 4.022 4.095 4.095 0 01-1.853.07 4.108 4.108 0 003.834 2.85A8.233 8.233 0 012 18.407a11.616 11.616 0 006.29 1.84"/></svg>
</a>
<a href="#" class="w-10 h-10 flex items-center justify-center rounded-full bg-[#ecebe8] dark:bg-[#2a2825] text-[#6b6661] dark:text-[#94908a] hover:bg-[#e15f80]/20 dark:hover:bg-[#e8788f]/20 hover:text-[#e15f80] dark:hover:text-[#e8788f] transition-all">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zm0-2.163c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948 0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072 3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98-1.281-.059-1.69-.073-4.949-.073zm0 5.838c-3.403 0-6.162 2.759-6.162 6.162s2.759 6.163 6.162 6.163 6.162-2.759 6.162-6.163c0-3.403-2.759-6.162-6.162-6.162zm0 10.162c-2.209 0-4-1.79-4 0-2.209 1.791-4 4-4s4 1.791 4 4c0 2.21-1.791 4-4 4zm6.406-11.845c-.796 0-1.441.645-1.441 1.44s.645 1.44 1.441 1.44c.795 0 1.439-.645 1.439-1.44s-.644-1.44-1.439-1.44z"/></svg>
</a>
</div>
</div>
</div>
</div>
</div></template>
<meta data-field="design-tokens" content="{"fonts":[{"role":"heading","family":"Inter","weights":[400,600,700],"category":"sans-serif"},{"role":"body","family":"Source Sans Pro","weights":[400,600],"category":"sans-serif"},{"role":"mono","family":"Fira Code","weights":[400],"category":"monospace"}],"colors":[{"name":"Primary","value":"#2563EB","group":"Brand"},{"name":"Secondary","value":"#7C3AED","group":"Brand"},{"name":"Accent","value":"#F59E0B","group":"Brand"},{"name":"Gray","value":"#F9FAFB","group":"Neutral"},{"name":"Gray","value":"#E5E7EB","group":"Neutral"},{"name":"Gray","value":"#9CA3AF","group":"Neutral"},{"name":"Gray","value":"#374151","group":"Neutral"},{"name":"Gray","value":"#111827","group":"Neutral"}],"spacing":{"unit":"4px","scale":["4","8","12","16","24","32","48","64"]},"radii":[{"name":"sm","value":"4px"},{"name":"md","value":"8px"},{"name":"lg","value":"12px"},{"name":"full","value":"9999px"}]}" />
</rf-sandbox>Merging with inline content
You can combine file-sourced and inline content. Write additional HTML, CSS, or JavaScript between the tags — it merges with the file content.
{% sandbox src="login-form" %}
<style data-source="Overrides">
.form { border: 2px solid red; }
</style>
{% /sandbox %}
Error handling
If the named directory does not exist, the sandbox displays an error message in place of the preview.
Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
src | string | — | Name of a subdirectory in the examples directory to load files from |
framework | string | — | Framework preset to load: tailwind, bootstrap, bulma, pico |
dependencies | string | — | Comma-separated URLs of scripts/stylesheets to load |
label | string | — | Label for the sandbox (used when inside compare) |
height | number | string | auto | Fixed height in pixels, or "fill" to fill the host's height (auto-sizes by default) |
context | string | default | Name of the design context scope to inject tokens from |
activation | string | eager | When to mount the iframe: eager, visible (on scroll-in), or click |
poster | string | — | Image URL shown in the iframe's place until a non-eager sandbox activates |
Common attributes
All block runes share these attributes for layout and theming.
| Attribute | Type | Default | Description |
|---|---|---|---|
width | string | content | Page grid width: content, wide, or full |
spacing | string | — | Vertical spacing: flush, tight, default, loose, or breathe |
inset | string | — | Horizontal padding: flush, tight, default, loose, or breathe |
tint | string | — | Named colour tint from theme configuration |
tint-mode | string | auto | Colour scheme override: auto, dark, or light |
bg | string | — | Named background preset from theme configuration |