Add initial HTML structure for SigPro Playground

This commit is contained in:
Natxo
2026-03-13 15:56:15 +01:00
committed by GitHub
parent 5c43ac67a7
commit a424370549

846
PlayGround/play.html Normal file
View File

@@ -0,0 +1,846 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SigPro Playground - Prueba SigPro Online</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
background: #0d1117;
color: #c9d1d9;
height: 100vh;
display: flex;
flex-direction: column;
}
.navbar {
background: #161b22;
padding: 0.75rem 1.5rem;
border-bottom: 1px solid #30363d;
display: flex;
justify-content: space-between;
align-items: center;
}
.logo {
display: flex;
align-items: center;
gap: 0.75rem;
font-size: 1.25rem;
font-weight: 600;
}
.logo span {
background: #2d9cdb;
color: white;
padding: 0.25rem 0.75rem;
border-radius: 20px;
font-size: 0.875rem;
}
.actions {
display: flex;
gap: 0.75rem;
}
.btn {
padding: 0.5rem 1rem;
border-radius: 6px;
border: 1px solid #30363d;
background: #21262d;
color: #c9d1d9;
cursor: pointer;
font-size: 0.875rem;
font-weight: 500;
transition: all 0.2s;
}
.btn:hover {
background: #30363d;
border-color: #8b949e;
}
.btn-primary {
background: #238636;
border-color: #2ea043;
color: white;
}
.btn-primary:hover {
background: #2ea043;
}
.main-container {
display: flex;
flex: 1;
overflow: hidden;
}
.editor-section {
flex: 1;
display: flex;
flex-direction: column;
border-right: 1px solid #30363d;
background: #0d1117;
}
.editor-header {
padding: 0.75rem 1rem;
background: #161b22;
border-bottom: 1px solid #30363d;
display: flex;
justify-content: space-between;
align-items: center;
}
.editor-tabs {
display: flex;
gap: 1px;
}
.tab {
padding: 0.5rem 1rem;
background: #21262d;
border: 1px solid #30363d;
border-bottom: none;
border-radius: 6px 6px 0 0;
cursor: pointer;
font-size: 0.875rem;
}
.tab.active {
background: #0d1117;
border-bottom-color: #0d1117;
margin-bottom: -1px;
color: #2d9cdb;
}
.editor-container {
flex: 1;
position: relative;
overflow: hidden;
}
.code-editor {
width: 100%;
height: 100%;
background: #0d1117;
color: #c9d1d9;
font-family: 'Fira Code', 'Courier New', monospace;
font-size: 14px;
line-height: 1.6;
padding: 1rem;
border: none;
resize: none;
outline: none;
white-space: pre-wrap;
}
.preview-section {
flex: 1;
display: flex;
flex-direction: column;
background: #0d1117;
}
.preview-header {
padding: 0.75rem 1rem;
background: #161b22;
border-bottom: 1px solid #30363d;
display: flex;
justify-content: space-between;
align-items: center;
}
.preview-iframe {
flex: 1;
width: 100%;
border: none;
background: white;
}
.error-console {
background: #1f1f1f;
border-top: 1px solid #30363d;
max-height: 150px;
overflow: auto;
font-family: monospace;
font-size: 12px;
padding: 0.5rem;
color: #ff7b72;
}
.error-console:empty {
display: none;
}
.status-bar {
background: #161b22;
border-top: 1px solid #30363d;
padding: 0.25rem 1rem;
font-size: 0.75rem;
color: #8b949e;
display: flex;
justify-content: space-between;
}
.share-modal {
display: none;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: #161b22;
border: 1px solid #30363d;
border-radius: 8px;
padding: 2rem;
z-index: 1000;
box-shadow: 0 8px 24px rgba(0,0,0,0.5);
}
.share-modal.show {
display: block;
}
.overlay {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.7);
z-index: 999;
}
.overlay.show {
display: block;
}
</style>
</head>
<body>
<div class="navbar">
<div class="logo">
⚡ SigPro Playground
<span>v1.0.0</span>
</div>
<div class="actions">
<button class="btn" onclick="runCode()">▶ Ejecutar</button>
<button class="btn" onclick="formatCode()">✨ Formatear</button>
<button class="btn" onclick="shareCode()">🔗 Compartir</button>
<button class="btn btn-primary" onclick="resetCode()">↺ Reset</button>
</div>
</div>
<div class="main-container">
<!-- Editor Section -->
<div class="editor-section">
<div class="editor-header">
<div class="editor-tabs">
<div class="tab active">JavaScript (SigPro)</div>
</div>
<span style="font-size: 0.75rem; color: #8b949e;">Ctrl + Enter para ejecutar</span>
</div>
<div class="editor-container">
<textarea id="codeEditor" class="code-editor" spellcheck="false" placeholder="// Escribe tu código SigPro aquí&#10;&#10;const nombre = $('Mundo');&#10;&#10;$$(() => {&#10; document.getElementById('saludo').textContent = `¡Hola ${nombre()}!`;&#10;});&#10;&#10;// Cambia el nombre después de 2 segundos&#10;setTimeout(() => nombre('SigPro'), 2000);"></textarea>
</div>
</div>
<!-- Preview Section -->
<div class="preview-section">
<div class="preview-header">
<span>Resultado</span>
<div>
<span id="autoRunIndicator" style="color: #2ea043;">⚡ Auto-ejecución activada</span>
</div>
</div>
<iframe id="preview" class="preview-iframe" sandbox="allow-scripts allow-same-origin allow-forms"></iframe>
<div id="errorConsole" class="error-console"></div>
</div>
</div>
<div class="status-bar">
<span>📦 SigPro - Librería reactiva minimalista</span>
<span>🔄 Cambios en tiempo real</span>
</div>
<!-- Modal para compartir -->
<div id="overlay" class="overlay" onclick="closeShareModal()"></div>
<div id="shareModal" class="share-modal">
<h3 style="margin-bottom: 1rem;">Comparte tu código</h3>
<input type="text" id="shareUrl" readonly style="width: 100%; padding: 0.5rem; background: #0d1117; border: 1px solid #30363d; color: #c9d1d9; border-radius: 4px; margin-bottom: 1rem;">
<div style="display: flex; gap: 0.5rem; justify-content: flex-end;">
<button class="btn" onclick="copyShareUrl()">Copiar</button>
<button class="btn btn-primary" onclick="closeShareModal()">Cerrar</button>
</div>
</div>
<script>
// ============================================
// CÓDIGO SIGPRO (COMPLETO)
// ============================================
// Global state for tracking the current reactive effect
let activeEffect = null;
// Queue for batched effect updates
const effectQueue = new Set();
let isFlushScheduled = false;
const flushEffectQueue = () => {
isFlushScheduled = false;
try {
for (const effect of effectQueue) {
effect.run();
}
effectQueue.clear();
} catch (error) {
console.error("SigPro Flush Error:", error);
}
};
window.$ = (initialValue) => {
const subscribers = new Set();
if (typeof initialValue === "function") {
let isDirty = true;
let cachedValue;
const computedEffect = {
dependencies: new Set(),
cleanupHandlers: new Set(),
markDirty: () => {
if (!isDirty) {
isDirty = true;
subscribers.forEach((subscriber) => {
if (subscriber.markDirty) subscriber.markDirty();
effectQueue.add(subscriber);
});
}
},
run: () => {
computedEffect.dependencies.forEach((dependencySet) => dependencySet.delete(computedEffect));
computedEffect.dependencies.clear();
const previousEffect = activeEffect;
activeEffect = computedEffect;
try {
cachedValue = initialValue();
} finally {
activeEffect = previousEffect;
isDirty = false;
}
},
};
return () => {
if (activeEffect) {
subscribers.add(activeEffect);
activeEffect.dependencies.add(subscribers);
}
if (isDirty) computedEffect.run();
return cachedValue;
};
}
return (...args) => {
if (args.length) {
const nextValue = typeof args[0] === "function" ? args[0](initialValue) : args[0];
if (!Object.is(initialValue, nextValue)) {
initialValue = nextValue;
subscribers.forEach((subscriber) => {
if (subscriber.markDirty) subscriber.markDirty();
effectQueue.add(subscriber);
});
if (!isFlushScheduled && effectQueue.size) {
isFlushScheduled = true;
queueMicrotask(flushEffectQueue);
}
}
}
if (activeEffect) {
subscribers.add(activeEffect);
activeEffect.dependencies.add(subscribers);
}
return initialValue;
};
};
window.$$ = (effectFn) => {
const effect = {
dependencies: new Set(),
cleanupHandlers: new Set(),
run() {
this.cleanupHandlers.forEach((handler) => handler());
this.cleanupHandlers.clear();
this.dependencies.forEach((dependencySet) => dependencySet.delete(this));
this.dependencies.clear();
const previousEffect = activeEffect;
activeEffect = this;
try {
const result = effectFn();
if (typeof result === "function") this.cleanupFunction = result;
} finally {
activeEffect = previousEffect;
}
},
stop() {
this.cleanupHandlers.forEach((handler) => handler());
this.dependencies.forEach((dependencySet) => dependencySet.delete(this));
this.cleanupFunction?.();
},
};
if (activeEffect) activeEffect.cleanupHandlers.add(() => effect.stop());
effect.run();
return () => effect.stop();
};
window.html = (strings, ...values) => {
const templateCache = html._templateCache ?? (html._templateCache = new WeakMap());
const getNodeByPath = (root, path) =>
path.reduce((node, index) => node?.childNodes?.[index], root);
const applyTextContent = (node, values) => {
const parts = node.textContent.split("{{part}}");
const parent = node.parentNode;
let valueIndex = 0;
parts.forEach((part, index) => {
if (part) parent.insertBefore(document.createTextNode(part), node);
if (index < parts.length - 1) {
const currentValue = values[valueIndex++];
const startMarker = document.createComment("s");
const endMarker = document.createComment("e");
parent.insertBefore(startMarker, node);
parent.insertBefore(endMarker, node);
let lastResult;
$$(() => {
let result = typeof currentValue === "function" ? currentValue() : currentValue;
if (result === lastResult) return;
lastResult = result;
if (typeof result !== "object" && !Array.isArray(result)) {
const textNode = startMarker.nextSibling;
if (textNode !== endMarker && textNode?.nodeType === 3) {
textNode.textContent = result ?? "";
} else {
while (startMarker.nextSibling !== endMarker)
parent.removeChild(startMarker.nextSibling);
parent.insertBefore(document.createTextNode(result ?? ""), endMarker);
}
return;
}
while (startMarker.nextSibling !== endMarker)
parent.removeChild(startMarker.nextSibling);
const items = Array.isArray(result) ? result : [result];
const fragment = document.createDocumentFragment();
items.forEach(item => {
if (item == null || item === false) return;
const nodeItem = item instanceof Node ? item : document.createTextNode(item);
fragment.appendChild(nodeItem);
});
parent.insertBefore(fragment, endMarker);
});
}
});
node.remove();
};
let cachedTemplate = templateCache.get(strings);
if (!cachedTemplate) {
const template = document.createElement("template");
template.innerHTML = strings.join("{{part}}");
const dynamicNodes = [];
const treeWalker = document.createTreeWalker(template.content, 133);
const getNodePath = (node) => {
const path = [];
while (node && node !== template.content) {
let index = 0;
for (let sibling = node.previousSibling; sibling; sibling = sibling.previousSibling)
index++;
path.push(index);
node = node.parentNode;
}
return path.reverse();
};
let currentNode;
while ((currentNode = treeWalker.nextNode())) {
let isDynamic = false;
const nodeInfo = {
type: currentNode.nodeType,
path: getNodePath(currentNode),
parts: []
};
if (currentNode.nodeType === 1) {
for (let i = 0; i < currentNode.attributes.length; i++) {
const attribute = currentNode.attributes[i];
if (attribute.value.includes("{{part}}")) {
nodeInfo.parts.push({ name: attribute.name });
isDynamic = true;
}
}
} else if (currentNode.nodeType === 3 && currentNode.textContent.includes("{{part}}")) {
isDynamic = true;
}
if (isDynamic) dynamicNodes.push(nodeInfo);
}
templateCache.set(strings, (cachedTemplate = { template, dynamicNodes }));
}
const fragment = cachedTemplate.template.content.cloneNode(true);
let valueIndex = 0;
const targets = cachedTemplate.dynamicNodes.map((nodeInfo) => ({
node: getNodeByPath(fragment, nodeInfo.path),
info: nodeInfo
}));
targets.forEach(({ node, info }) => {
if (!node) return;
if (info.type === 1) {
info.parts.forEach((part) => {
const currentValue = values[valueIndex++];
const attributeName = part.name;
const firstChar = attributeName[0];
if (firstChar === "@") {
node.addEventListener(attributeName.slice(1), currentValue);
} else if (firstChar === ":") {
const propertyName = attributeName.slice(1);
const eventType = node.type === "checkbox" || node.type === "radio" ? "change" : "input";
$$(() => {
const value = typeof currentValue === "function" ? currentValue() : currentValue;
if (node[propertyName] !== value) node[propertyName] = value;
});
node.addEventListener(eventType, () => {
const value = eventType === "change" ? node.checked : node.value;
if (typeof currentValue === "function") currentValue(value);
});
} else if (firstChar === "?") {
const attrName = attributeName.slice(1);
$$(() => {
const result = typeof currentValue === "function" ? currentValue() : currentValue;
node.toggleAttribute(attrName, !!result);
});
} else if (firstChar === ".") {
const propertyName = attributeName.slice(1);
$$(() => {
let result = typeof currentValue === "function" ? currentValue() : currentValue;
node[propertyName] = result;
if (result != null && typeof result !== "object" && typeof result !== "boolean") {
node.setAttribute(propertyName, result);
}
});
} else {
if (typeof currentValue === "function") {
$$(() => node.setAttribute(attributeName, currentValue()));
} else {
node.setAttribute(attributeName, currentValue);
}
}
});
} else if (info.type === 3) {
const placeholderCount = node.textContent.split("{{part}}").length - 1;
applyTextContent(node, values.slice(valueIndex, valueIndex + placeholderCount));
valueIndex += placeholderCount;
}
});
return fragment;
};
window.$component = (tagName, setupFunction, observedAttributes = []) => {
if (customElements.get(tagName)) return;
customElements.define(
tagName,
class extends HTMLElement {
static get observedAttributes() {
return observedAttributes;
}
constructor() {
super();
this._propertySignals = {};
this.cleanupFunctions = [];
observedAttributes.forEach((attr) => (this._propertySignals[attr] = $(undefined)));
}
connectedCallback() {
const frozenChildren = [...this.childNodes];
this.innerHTML = "";
observedAttributes.forEach((attr) => {
const initialValue = this.hasOwnProperty(attr) ? this[attr] : this.getAttribute(attr);
Object.defineProperty(this, attr, {
get: () => this._propertySignals[attr](),
set: (value) => {
const processedValue = value === "false" ? false : value === "" && attr !== "value" ? true : value;
this._propertySignals[attr](processedValue);
},
configurable: true,
});
if (initialValue !== null && initialValue !== undefined) this[attr] = initialValue;
});
const context = {
select: (selector) => this.querySelector(selector),
slot: (name) =>
frozenChildren.filter((node) => {
const slotName = node.nodeType === 1 ? node.getAttribute("slot") : null;
return name ? slotName === name : !slotName;
}),
emit: (name, detail) => this.dispatchEvent(new CustomEvent(name, { detail, bubbles: true, composed: true })),
host: this,
onUnmount: (cleanupFn) => this.cleanupFunctions.push(cleanupFn),
};
const result = setupFunction(this._propertySignals, context);
if (result instanceof Node) this.appendChild(result);
}
attributeChangedCallback(name, oldValue, newValue) {
if (this[name] !== newValue) this[name] = newValue;
}
disconnectedCallback() {
this.cleanupFunctions.forEach((cleanupFn) => cleanupFn());
this.cleanupFunctions = [];
}
},
);
};
window.$router = (routes) => {
const getCurrentPath = () => window.location.hash.replace(/^#/, "") || "/";
const currentPath = $(getCurrentPath());
const container = document.createElement("div");
container.style.display = "contents";
window.addEventListener("hashchange", () => {
const nextPath = getCurrentPath();
if (currentPath() !== nextPath) currentPath(nextPath);
});
$$(() => {
const path = currentPath();
let matchedRoute = null;
let routeParams = {};
for (const route of routes) {
if (route.path instanceof RegExp) {
const match = path.match(route.path);
if (match) {
matchedRoute = route;
routeParams = match.groups || { id: match[1] };
break;
}
} else if (route.path === path) {
matchedRoute = route;
break;
}
}
const previousEffect = activeEffect;
activeEffect = null;
try {
const view = matchedRoute
? matchedRoute.component(routeParams)
: html`
<h1>404</h1>
`;
container.replaceChildren(
view instanceof Node ? view : document.createTextNode(view ?? "")
);
} finally {
activeEffect = previousEffect;
}
});
return container;
};
$router.go = (path) => {
const targetPath = path.startsWith("/") ? path : `/${path}`;
if (window.location.hash !== `#${targetPath}`) window.location.hash = targetPath;
};
// ============================================
// LÓGICA DEL PLAYGROUND
// ============================================
const codeEditor = document.getElementById('codeEditor');
const preview = document.getElementById('preview');
const errorConsole = document.getElementById('errorConsole');
let autoRunTimeout;
// Ejecutar código al cargar la página
window.onload = () => {
runCode();
};
// Auto-ejecución mientras se escribe
codeEditor.addEventListener('input', () => {
clearTimeout(autoRunTimeout);
autoRunTimeout = setTimeout(runCode, 1000);
});
// Ejecutar con Ctrl+Enter
codeEditor.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.key === 'Enter') {
e.preventDefault();
runCode();
}
});
function runCode() {
const code = codeEditor.value;
// Crear el contenido del iframe
const htmlContent = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
padding: 20px;
margin: 0;
background: white;
}
* { box-sizing: border-box; }
</style>
</head>
<body>
<div id="app"></div>
<script>
// Copia de SigPro para el iframe
${$?.toString()}
${$$?.toString()}
${html?.toString()}
${$component?.toString()}
${$router?.toString()}
// Configuración adicional
window.onerror = function(msg, url, line, col, error) {
parent.postMessage({
type: 'error',
error: msg + ' (línea ' + line + ')'
}, '*');
};
// Ejecutar el código del usuario
try {
${code}
} catch (error) {
parent.postMessage({
type: 'error',
error: error.toString()
}, '*');
}
<\/script>
</body>
</html>
`;
// Actualizar el iframe
preview.srcdoc = htmlContent;
errorConsole.innerHTML = '';
}
// Escuchar errores del iframe
window.addEventListener('message', (event) => {
if (event.data.type === 'error') {
errorConsole.innerHTML = '❌ ' + event.data.error;
errorConsole.style.display = 'block';
}
});
function formatCode() {
// Formateo básico del código
const code = codeEditor.value;
// Aquí podrías integrar prettier si quieres
alert('Función de formateo próximamente');
}
function shareCode() {
const code = codeEditor.value;
const encoded = btoa(encodeURIComponent(code));
const url = window.location.href.split('?')[0] + '?code=' + encoded;
document.getElementById('shareUrl').value = url;
document.getElementById('shareModal').classList.add('show');
document.getElementById('overlay').classList.add('show');
}
function copyShareUrl() {
const shareUrl = document.getElementById('shareUrl');
shareUrl.select();
document.execCommand('copy');
alert('URL copiada al portapapeles');
}
function closeShareModal() {
document.getElementById('shareModal').classList.remove('show');
document.getElementById('overlay').classList.remove('show');
}
function resetCode() {
codeEditor.value = `// Escribe tu código SigPro aquí
const nombre = $('Mundo');
const contador = $(0);
$$(() => {
document.body.innerHTML = \`
<h1>¡Hola \${nombre()}!</h1>
<p>Contador: \${contador()}</p>
<button onclick="contador(c => c + 1)">Incrementar</button>
\`;
});`;
runCode();
}
// Cargar código desde URL si existe
const urlParams = new URLSearchParams(window.location.search);
const encodedCode = urlParams.get('code');
if (encodedCode) {
try {
const decoded = decodeURIComponent(atob(encodedCode));
codeEditor.value = decoded;
runCode();
} catch (e) {
console.error('Error al cargar código compartido');
}
}
</script>
</body>
</html>