Delete PlayGround directory
This commit is contained in:
@@ -1,208 +0,0 @@
|
|||||||
# SigPro Playground 🚀
|
|
||||||
|
|
||||||
An interactive online environment to experiment with **SigPro** - a minimalist reactive library for building reactive user interfaces directly in the browser.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## 🌟 Features
|
|
||||||
|
|
||||||
- **Zero Setup** - No installation required, works directly in the browser
|
|
||||||
- **Live Preview** - See your code results in real-time
|
|
||||||
- **Built-in SigPro** - Full library included with `$`, `$$`, `html`, `$component`, and `$router`
|
|
||||||
- **Error Console** - Instant feedback on syntax and runtime errors
|
|
||||||
- **Code Sharing** - Generate shareable URLs with your code
|
|
||||||
- **Auto-execution** - Runs automatically as you type (with debounce)
|
|
||||||
- **Keyboard Shortcuts** - Ctrl+Enter to manually execute
|
|
||||||
- **Clean Interface** - Modern dark-themed editor inspired by VS Code
|
|
||||||
|
|
||||||
## 🎮 Quick Start
|
|
||||||
|
|
||||||
### Online Version
|
|
||||||
Simply open the `play.html` file in your web browser and start coding!
|
|
||||||
|
|
||||||
### Write Your First Code
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// Create reactive signals
|
|
||||||
const name = $('World');
|
|
||||||
const count = $(0);
|
|
||||||
|
|
||||||
// Create an effect that reacts to changes
|
|
||||||
$$(() => {
|
|
||||||
document.body.innerHTML = `
|
|
||||||
<h1>Hello ${name()}!</h1>
|
|
||||||
<p>Count: ${count()}</p>
|
|
||||||
<button onclick="count(c => c + 1)">Increment</button>
|
|
||||||
`;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update signals and watch the magic happen
|
|
||||||
setTimeout(() => name('SigPro'), 2000);
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📖 Usage Guide
|
|
||||||
|
|
||||||
### The Editor Panel
|
|
||||||
|
|
||||||
- **Write Code** - The left panel contains the code editor with syntax highlighting
|
|
||||||
- **Auto-execute** - Code runs automatically 1 second after you stop typing
|
|
||||||
- **Manual Run** - Click the "Run" button or press `Ctrl+Enter`
|
|
||||||
- **Format** - Click "Format" to beautify your code (coming soon)
|
|
||||||
- **Share** - Generate a shareable link with your current code
|
|
||||||
- **Reset** - Restore the default example
|
|
||||||
|
|
||||||
### The Preview Panel
|
|
||||||
|
|
||||||
- **Live Results** - See your code output in real-time
|
|
||||||
- **Error Display** - Any errors appear in the console at the bottom
|
|
||||||
- **Clean Slate** - Each execution starts with a fresh environment
|
|
||||||
|
|
||||||
### Keyboard Shortcuts
|
|
||||||
|
|
||||||
| Shortcut | Action |
|
|
||||||
|----------|--------|
|
|
||||||
| `Ctrl + Enter` | Execute code manually |
|
|
||||||
| `Ctrl + S` | Save current code (coming soon) |
|
|
||||||
|
|
||||||
## 🎯 Example Code Snippets
|
|
||||||
|
|
||||||
### Basic Counter
|
|
||||||
```javascript
|
|
||||||
const counter = $(0);
|
|
||||||
|
|
||||||
setInterval(() => {
|
|
||||||
counter(c => c + 1);
|
|
||||||
}, 1000);
|
|
||||||
|
|
||||||
$$(() => {
|
|
||||||
document.body.innerHTML = `<h1>Counter: ${counter()}</h1>`;
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### Two-way Binding with html Tag
|
|
||||||
```javascript
|
|
||||||
const text = $('Edit me');
|
|
||||||
|
|
||||||
document.body.appendChild(html`
|
|
||||||
<div>
|
|
||||||
<input :value="${text}" placeholder="Type something...">
|
|
||||||
<p>You typed: ${text}</p>
|
|
||||||
</div>
|
|
||||||
`);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Computed Values
|
|
||||||
```javascript
|
|
||||||
const price = $(10);
|
|
||||||
const quantity = $(2);
|
|
||||||
const total = $(() => price() * quantity());
|
|
||||||
|
|
||||||
$$(() => {
|
|
||||||
document.body.innerHTML = `
|
|
||||||
<div>
|
|
||||||
<p>Price: $${price()}</p>
|
|
||||||
<p>Quantity: ${quantity()}</p>
|
|
||||||
<p><strong>Total: $${total()}</strong></p>
|
|
||||||
<button onclick="price(p => p + 1)">+ Price</button>
|
|
||||||
<button onclick="quantity(q => q + 1)">+ Quantity</button>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### Custom Component
|
|
||||||
```javascript
|
|
||||||
$component('my-button', (props, { emit }) => {
|
|
||||||
return html`
|
|
||||||
<button @click="${() => emit('click', props.count())}">
|
|
||||||
Count is: ${() => props.count()}
|
|
||||||
</button>
|
|
||||||
`;
|
|
||||||
}, ['count']);
|
|
||||||
|
|
||||||
document.body.appendChild(html`
|
|
||||||
<my-button :count="${$(5)}"></my-button>
|
|
||||||
`);
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔗 Sharing Code
|
|
||||||
|
|
||||||
1. Write your code in the editor
|
|
||||||
2. Click the **Share** button
|
|
||||||
3. Copy the generated URL
|
|
||||||
4. Share it with anyone - they'll see exactly your code!
|
|
||||||
|
|
||||||
The code is encoded in the URL, so no backend storage is needed.
|
|
||||||
|
|
||||||
## 🛠️ Advanced Features
|
|
||||||
|
|
||||||
### Using the Router
|
|
||||||
```javascript
|
|
||||||
const routes = [
|
|
||||||
{ path: '/', component: () => html`<h1>Home</h1>` },
|
|
||||||
{ path: '/about', component: () => html`<h1>About</h1>` },
|
|
||||||
{ path: /^\/user\/(?<id>.+)/, component: (params) => html`<h1>User: ${params.id}</h1>` }
|
|
||||||
];
|
|
||||||
|
|
||||||
document.body.appendChild($router(routes));
|
|
||||||
$router.go('/about');
|
|
||||||
```
|
|
||||||
|
|
||||||
### Working with Lists
|
|
||||||
```javascript
|
|
||||||
const items = $(['Apple', 'Banana', 'Orange']);
|
|
||||||
|
|
||||||
$$(() => {
|
|
||||||
document.body.innerHTML = `
|
|
||||||
<ul>
|
|
||||||
${items().map(item => `<li>${item}</li>`).join('')}
|
|
||||||
</ul>
|
|
||||||
<button onclick="items([...items(), 'New Fruit'])">Add</button>
|
|
||||||
`;
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📦 SigPro API Reference
|
|
||||||
|
|
||||||
| Function | Description |
|
|
||||||
|----------|-------------|
|
|
||||||
| `$(value)` | Creates a reactive signal |
|
|
||||||
| `$$(effect)` | Creates a reactive effect |
|
|
||||||
| `html\`...\`` | Tagged template for reactive HTML |
|
|
||||||
| `$component(name, setup, attrs)` | Creates a web component |
|
|
||||||
| `$router(routes)` | Creates a hash-based router |
|
|
||||||
|
|
||||||
## 🤔 Troubleshooting
|
|
||||||
|
|
||||||
**Error: "Cannot read property..."**
|
|
||||||
- Make sure you're accessing signal values with `signal()`, not `signal`
|
|
||||||
|
|
||||||
**Nothing shows in preview**
|
|
||||||
- Check the error console for syntax errors
|
|
||||||
- Make sure you're appending to `document.body` or using `html` correctly
|
|
||||||
|
|
||||||
**Changes not updating**
|
|
||||||
- Verify you're using `$$()` to create effects
|
|
||||||
- Signals must be called as functions: `count()` not `count`
|
|
||||||
|
|
||||||
## 🌐 Browser Support
|
|
||||||
|
|
||||||
Works in all modern browsers (Chrome, Firefox, Safari, Edge) that support:
|
|
||||||
- JavaScript ES6+
|
|
||||||
- Custom Elements (for `$component`)
|
|
||||||
- iframe sandboxing
|
|
||||||
|
|
||||||
## 📝 License
|
|
||||||
|
|
||||||
MIT License - feel free to use, modify, and distribute!
|
|
||||||
|
|
||||||
## 🤝 Contributing
|
|
||||||
|
|
||||||
Found a bug or want to improve the playground? Feel free to:
|
|
||||||
- Report issues
|
|
||||||
- Suggest new features
|
|
||||||
- Submit improvements
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Happy Coding with SigPro!** ⚡
|
|
||||||
@@ -1,846 +0,0 @@
|
|||||||
<!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í const nombre = $('Mundo'); $$(() => { document.getElementById('saludo').textContent = `¡Hola ${nombre()}!`; }); // Cambia el nombre después de 2 segundos 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>
|
|
||||||
Reference in New Issue
Block a user