From e84e2dc8e20b511c086b46d36770ed222646df26 Mon Sep 17 00:00:00 2001 From: Vincent Fretin Date: Sun, 28 Jun 2026 19:30:47 +0200 Subject: [PATCH] Add a Remix dropdown (CodeSandbox/Krabbel) to the gh-pages examples Inject examples/js/remix-dropdown.js into the published examples that reference the local A-Frame build. The script adds a REMIX button above the AR/VR enter buttons with a dropdown to open the current example on CodeSandbox or Krabbel, deriving the example path from the page URL. It also repositions the CodeSandbox "Open Sandbox" button and hides itself when already running inside a remix preview (*.csb.app, *.krabbel.fun). Co-Authored-By: Claude Opus 4.8 (1M context) --- examples/js/remix-dropdown.js | 113 ++++++++++++++++++++++++++++++++++ scripts/preghpages.js | 13 +++- 2 files changed, 124 insertions(+), 2 deletions(-) create mode 100644 examples/js/remix-dropdown.js diff --git a/examples/js/remix-dropdown.js b/examples/js/remix-dropdown.js new file mode 100644 index 00000000000..34e583c0884 --- /dev/null +++ b/examples/js/remix-dropdown.js @@ -0,0 +1,113 @@ +/* Adds a "Remix" dropdown to the gh-pages examples letting visitors open the + current example on CodeSandbox or Krabbel. Injected into the published + examples by scripts/preghpages.js. */ +(function () { + // Derive the example location (relative to the examples directory) from the + // current URL, splitting it into the directory the remix services open and + // the html filename. e.g. ".../examples/test/text/msdf2.html" -> + // { dir: "test/text", file: "msdf2.html" }. + // In prod the examples live under /examples/ (e.g. aframe.io/examples/ or + // aframe.io/aframe/examples/); the local dev server serves the examples + // directory as the web root, so fall back to the whole pathname. + function getExampleLocation () { + var pathname = window.location.pathname; + var marker = '/examples/'; + var index = pathname.indexOf(marker); + var rel = (index === -1 ? pathname : pathname.slice(index + marker.length)).replace(/^\//, ''); + var match = rel.match(/([^/]+\.html)$/); + return { + dir: rel.replace(/[^/]+\.html$/, '').replace(/\/$/, ''), + file: match ? match[1] : '' + }; + } + + function createMenuItem (label, href) { + var item = document.createElement('a'); + item.classList.add('a-remix-menu-item'); + item.setAttribute('href', href); + item.setAttribute('target', '_blank'); + item.setAttribute('rel', 'noopener'); + item.textContent = label; + return item; + } + + function addStyles () { + var css = + // Reposition the CodeSandbox "Open Sandbox" button to the top left so it + // does not overlap the VR/AR enter buttons. + 'iframe[id^="sb__open-sandbox"]{inset: 16px auto auto 16px;}' + + + // Sit above the AR/VR enter buttons in the bottom right corner. + '.a-remix-container{position: absolute; right: 20px; bottom: 70px; z-index: 9999;' + + 'font-size: 20px; line-height: 20px;' + + 'font-family: \'Helvetica Neue\', Helvetica, Arial, sans-serif;}' + + + '.a-remix-button{cursor: pointer; padding: 0 10px; font-weight: bold; color: #666;' + + 'font-size: large; border: 3px solid #666; box-sizing: border-box; min-width: 58px; min-height: 38px;' + + 'border-radius: 8px; background-color: white;}' + + + '.a-remix-button:active, .a-remix-button:hover{border-color: #ef2d5e; color: #ef2d5e;}' + + + '.a-remix-menu{position: absolute; right: 0; bottom: 40px; min-width: 200px;' + + 'background-color: white; border: 3px solid #666; border-radius: 8px;' + + 'overflow: hidden;}' + + + '.a-remix-menu-item{display: block; padding: 10px 15px; color: rgb(51, 51, 51);' + + 'text-decoration: none; font-size: 11pt; line-height: 20pt;}' + + + '.a-remix-menu-item:hover{background-color: #ef2d5e; color: white;}'; + var style = document.createElement('style'); + + style.appendChild(document.createTextNode(css)); + document.getElementsByTagName('head')[0].appendChild(style); + } + + function init () { + // Don't show the button when the example is already running inside a remix + // service preview (CodeSandbox *.csb.app, Krabbel *.krabbel.fun). + if (/\.(csb\.app|krabbel\.fun)$/.test(window.location.hostname)) { return; } + + var loc = getExampleLocation(); + if (!loc) { return; } + + // Point at the directory, adding a file parameter for non-index.html pages + // so the specific example file is opened. The leading slash is URL-encoded + // (CodeSandbox expects %2F). + var fileParam = (loc.file && loc.file !== 'index.html') ? '?file=' + encodeURIComponent('/' + loc.file) : ''; + var treePath = 'tree/gh-pages/examples/' + loc.dir + fileParam; + var codesandboxUrl = 'https://codesandbox.io/p/sandbox/github/aframevr/aframe/' + treePath; + var krabbelUrl = 'https://krabbel.dev/github/aframevr/aframe/' + treePath; + + addStyles(); + + var menu = document.createElement('div'); + menu.classList.add('a-remix-menu'); + menu.style.display = 'none'; + menu.appendChild(createMenuItem('Remix on CodeSandbox', codesandboxUrl)); + menu.appendChild(createMenuItem('Remix on Krabbel', krabbelUrl)); + + var button = document.createElement('button'); + button.classList.add('a-remix-button'); + button.setAttribute('title', 'Remix this example'); + button.textContent = 'REMIX'; + button.addEventListener('click', function (evt) { + evt.stopPropagation(); + menu.style.display = menu.style.display === 'none' ? '' : 'none'; + }); + + // Close the menu when clicking elsewhere. + document.addEventListener('click', function () { menu.style.display = 'none'; }); + + var container = document.createElement('div'); + container.classList.add('a-remix-container'); + container.appendChild(menu); + container.appendChild(button); + document.body.appendChild(container); + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); + } else { + init(); + } +})(); diff --git a/scripts/preghpages.js b/scripts/preghpages.js index 13b4cfd7871..6381a428544 100755 --- a/scripts/preghpages.js +++ b/scripts/preghpages.js @@ -27,10 +27,15 @@ shell.cp('-r', [ '*.md' ], 'gh-pages'); -function replaceInFileSync ({ files, processor }) { +function replaceInFileSync ({ files, processor, processorIfChanged = [] }) { for (const file of fs.globSync(files)) { const input = fs.readFileSync(file, 'utf8'); - const output = processor.reduce((content, process) => process(content), input); + let output = processor.reduce((content, process) => process(content), input); + // processorIfChanged only runs on files the processors above modified, i.e. + // the examples that reference the local A-Frame build. + if (output !== input) { + output = processorIfChanged.reduce((content, process) => process(content), output); + } if (output !== input) { fs.writeFileSync(file, output); } @@ -49,5 +54,9 @@ replaceInFileSync({ htmlReplace(/"\.\.\/\.\.\/\.\.\/super-three-package/g, `"https://cdn.jsdelivr.net/npm/super-three@${threeVersion}`), htmlReplace(/"\.\.\/\.\.\/js\//g, '"https://cdn.jsdelivr.net/gh/aframevr/aframe@gh-pages/examples/js/'), htmlReplace(/"\.\.\/\.\.\/assets\//g, '"https://cdn.jsdelivr.net/gh/aframevr/aframe@gh-pages/examples/assets/') + ], + // Add the "Remix" dropdown (CodeSandbox/Krabbel) to the published examples. + processorIfChanged: [ + htmlReplace('', ' \n ') ] });