Javascript
Reglas de JavaScript vanilla ES6+ - ES2025, programación defensiva, módulos, DOM, fetch, APIs modernas, estructuras de datos, patrones avanzadosFrom its SKILL.md
npx -y skills add 14BryanEspinoza/agent-stack --skill javascriptAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
26.3 KB, ~6.9k tokens by cl100k_base, as published. Nobody here has run it
JavaScript Vanilla ES6+ - Reglas
JavaScript vanilla ES6+ es el enfoque preferido.
1. Instalación y Versión
Versión mínima
| Tecnología | Versión Mínima |
|---|---|
| JavaScript | ES2025+ (moderno) |
| Node.js | 22+ |
2. Cuándo USAR JavaScript
| Caso | Justificación |
|---|---|
| Interactividad dinámica | Modales, acordeones, tabs |
| Manipulación del DOM | Crear/actualizar contenido dinámicamente |
| Fetch/AJAX | Obtener/enviar datos sin recargar |
| Validación cliente | Validación en tiempo real |
| APIs del navegador | LocalStorage, Geolocation, etc. |
3. Cuándo NO USAR JavaScript
| En lugar de... | Usar... |
|---|---|
| Animaciones simples | CSS Transitions/Animations |
| Tooltips simples | CSS-only o title attribute |
| Modales simples | HTML <dialog> |
| Acordeones simples | HTML <details>/<summary> |
| Carousels simples | CSS scroll snap |
| Dropdowns simples | HTML <select> |
4. APIs Preferidas
| En lugar de... | Usar... |
|---|---|
var | const (por defecto), let (si reasigna) |
function | Arrow functions cuando corresponda |
| Callbacks | async/await + Promise |
$.ajax | fetch |
for loops | forEach, map, filter, reduce |
| Strings concat | Template literals `hello ${name}` |
setTimeout para animaciones | CSS transitions |
| Animaciones con JS | Web Animations API |
| Detectar elementos en vista | IntersectionObserver |
5. Estructuras de Datos Modernas
Map (claves de cualquier tipo)
const users = new Map();
users.set(42, { name: "Ana" });
users.set("admin", { name: "Admin" });
users.get(42); // { name: "Ana" }
users.has("admin"); // true
users.size; // 2
users.delete(42);
users.clear();
// Iteración
for (const [key, value] of users) {
console.log(key, value);
}
WeakMap (claves objeto, sin prevenir GC)
let element = document.querySelector(".card");
const metadata = new WeakMap();
metadata.set(element, { timestamp: Date.now() });
// Cuando element se elimina del DOM y se recolecta,
// la entrada en WeakMap también se limpia automáticamente
metadata.get(element);
Set (valores únicos)
const tags = new Set(["js", "css", "html", "js"]);
// Set(3) { "js", "css", "html" }
tags.add("react");
tags.has("css"); // true
tags.size; // 4
tags.delete("css");
// Convertir array a valores únicos
const unique = [...new Set([1, 2, 2, 3, 3, 3])];
// [1, 2, 3]
Set Operations (ES2025)
const setA = new Set([1, 2, 3, 4]);
const setB = new Set([3, 4, 5, 6]);
setA.union(setB);
// Set(6) { 1, 2, 3, 4, 5, 6 }
setA.intersection(setB);
// Set(2) { 3, 4 }
setA.difference(setB);
// Set(2) { 1, 2 }
setA.symmetricDifference(setB);
// Set(4) { 1, 2, 5, 6 }
setA.isSubsetOf(setB); // false
setA.isSupersetOf(setB); // false
setA.isDisjointFrom(setB); // false
WeakRef + FinalizationRegistry
// WeakRef: referencia débil que no impide GC
const ref = new WeakRef({ data: "pesado" });
const obj = ref.deref(); // undefined si ya fue recolectado
// FinalizationRegistry: callback cuando un objeto es recolectado
const registry = new FinalizationRegistry((heldValue) => {
console.log("Recolectado:", heldValue);
});
let tracked = { id: 1 };
registry.register(tracked, "cache-item-1", tracked);
registry.unregister(tracked); // Cancelar registro
6. Nuevas APIs del Lenguaje (ES2024+)
Promise.withResolvers()
Reemplaza el patrón verbose de promesas externas:
// Antes
const promise = new Promise((resolve, reject) => {
externalCallback((err, data) => {
if (err) reject(err);
else resolve(data);
});
});
// Ahora
const { promise, resolve, reject } = Promise.withResolvers();
externalCallback((err, data) => {
if (err) reject(err);
else resolve(data);
});
Object.groupBy() / Map.groupBy()
const products = [
{ name: "Laptop", category: "tech" },
{ name: "Shirt", category: "clothing" },
{ name: "Phone", category: "tech" },
];
// Object.groupBy (cuando las claves son strings)
const byCategory = Object.groupBy(products, (p) => p.category);
// { tech: [Laptop, Phone], clothing: [Shirt] }
// Map.groupBy (cuando las claves son objetos)
const byPriceRange = Map.groupBy(products, (p) => {
if (p.price < 50) return "budget";
if (p.price < 500) return "mid";
return "premium";
});
structuredClone() — Deep cloning nativo
const original = { user: { name: "Ana", tags: ["js"] } };
const cloned = structuredClone(original);
cloned.user.name = "Luis"; // No afecta al original
cloned.user.tags.push("ts"); // No afecta al original
// ✅ Seguro con fechas, Map, Set, RegExp, ArrayBuffer
const complex = {
date: new Date(),
map: new Map([["key", "value"]]),
set: new Set([1, 2, 3]),
};
structuredClone(complex); // ✅ Funciona
// ❌ No clona funciones, Symbol, WeakMap, DOM nodes
Array.fromAsync()
async function* asyncGenerator() {
yield await Promise.resolve(1);
yield await Promise.resolve(2);
yield await Promise.resolve(3);
}
const arr = await Array.fromAsync(asyncGenerator());
// [1, 2, 3] — secuencial, respeta orden
// Equivalente a:
const results = [];
for await (const item of asyncGenerator()) {
results.push(item);
}
Error.cause
try {
await fetch("/api/data");
} catch (error) {
// Preservar el error original con contexto adicional
throw new Error("Failed to load data", { cause: error });
}
// Al capturar:
// error.message → "Failed to load data"
// error.cause.message → error original de fetch
JSON.parse con context.source
const json = '{"count": 42, "name": "test"}';
JSON.parse(json, (key, value, context) => {
console.log(`Key: ${key}, Source: ${context.source}`);
// "count", "42"
// "name", "\"test\""
return value;
});
7. Patrones Avanzados
Proxy + Reflect (metaprogramación)
// Validación de propiedades con Proxy
const validator = {
set(target, key, value) {
if (key === "age" && (typeof value !== "number" || value < 0)) {
throw new Error("Age must be a positive number");
}
return Reflect.set(target, key, value);
},
};
const user = new Proxy({}, validator);
user.age = 25; // ✅
user.age = -5; // ❌ Error
// Logging automático con Proxy
const logger = {
get(target, property, receiver) {
console.log(`GET ${property}`);
return Reflect.get(target, property, receiver);
},
};
const tracked = new Proxy({ name: "Ana" }, logger);
tracked.name; // console: "GET name"
Intl.Segmenter (segmentación de texto)
// Segmentar por palabras
const wordSegmenter = new Intl.Segmenter("es", { granularity: "word" });
const words = [...wordSegmenter.segment("Hola mundo, ¿cómo estás?")];
// [{ segment: "Hola" }, { segment: " " }, { segment: "mundo" }, ...]
// Segmentar por oraciones
const sentenceSegmenter = new Intl.Segmenter("es", { granularity: "sentence" });
const text = "Primera oración. Segunda oración. ¿Tercera?";
const sentences = [...sentenceSegmenter.segment(text)];
// [{ segment: "Primera oración. " }, { segment: "Segunda oración. " }, ...]
// Segmentar por grafemas (caracteres visibles)
const graphemeSegmenter = new Intl.Segmenter("es", { granularity: "grapheme" });
const chars = [...graphemeSegmenter.segment("café")].map((s) => s.segment);
// ["c", "a", "f", "é"]
Iterator Helpers (ES2025)
function* naturals() {
let i = 1;
while (true) yield i++;
}
// take() — tomar N elementos
const first5 = naturals().take(5).toArray();
// [1, 2, 3, 4, 5]
// map() / filter() sobre iteradores (sin crear arrays intermedios)
const evens = naturals()
.filter((n) => n % 2 === 0)
.take(5)
.toArray();
// [2, 4, 6, 8, 10]
// reduce()
const sum = naturals()
.take(100)
.reduce((a, b) => a + b, 0);
// 5050
// zip() — combinar iteradores
const names = ["Ana", "Luis", "Carlos"];
const ages = [25, 30, 28];
const paired = Iterator.zip(names, ages).toArray();
// [["Ana", 25], ["Luis", 30], ["Carlos", 28]]
// flatMap()
const words = ["hello", "world"];
const letters = words
.values()
.flatMap((w) => w.split(""))
.toArray();
// ["h", "e", "l", "l", "o", "w", "o", "r", "l", "d"]
// drop()
const withoutFirst = naturals().drop(10).take(3).toArray();
// [11, 12, 13]
8. Selección DOM
// Selección simple
const element = document.querySelector(".class");
const elements = document.querySelectorAll(".class");
// Por ID (más rápido)
const el = document.getElementById("my-id");
// Con dataset
element.dataset.property = "value";
const value = element.dataset.property;
9. Manipulación del DOM
Crear elementos
// Crear elemento
const div = document.createElement("div");
div.className = "card";
div.textContent = "Contenido";
// Crear con innerHTML (solo si no hay datos de usuario)
container.innerHTML = '<div class="card">Contenido seguro</div>';
// Evitar XSS con textContent
const userInput = '<script>alert("xss")</script>';
element.textContent = userInput; // Seguro
element.innerHTML = userInput; // Peligroso
// insertAdjacentHTML (más eficiente que innerHTML)
container.insertAdjacentHTML("beforeend", '<div class="card">Nuevo</div>');
// Posiciones: "beforebegin", "afterbegin", "beforeend", "afterend"
Agregar elementos
// Append (al final)
parent.appendChild(element);
parent.append("Texto", element);
// Prepend (al inicio)
parent.prepend(element);
// Before/After
sibling.before(element);
sibling.after(element);
// Reemplazar
oldElement.replaceWith(newElement);
// Reemplazar todo el contenido
parent.replaceChildren(element1, element2);
// Remover
element.remove();
Templates
// Usar template para estructuras complejas
const template = document.querySelector("#card-template");
const card = template.content.cloneNode(true);
card.querySelector(".title").textContent = "Nuevo título";
document.querySelector(".container").appendChild(card);
ARIA properties modernas
// En lugar de setAttribute, usar propiedades aria directas
button.ariaLabel = "Cerrar";
button.ariaExpanded = "false";
button.ariaPressed = "true";
button.role = "tab";
button.tabIndex = 0;
// Popover API
element.showPopover();
element.hidePopover();
element.togglePopover();
10. Eventos
Event listener
// Basic
element.addEventListener("click", (event) => {
console.log(event.target);
});
// Con opciones
element.addEventListener("click", handler, { once: true, passive: true });
// Cleanup con AbortSignal
const controller = new AbortController();
const { signal } = controller;
element.addEventListener("click", handler, { signal });
// Para remover todos los listeners asociados:
controller.abort();
Evitar onclick en HTML
<!-- ❌ Malo - JavaScript inline -->
<a onclick="guardar()">Guardar</a>
<button onclick="hacerAlgo()">Acción</button>
<!-- ✅ Bueno - Event listener -->
<button type="button" data-action="save">Guardar</button>
<button type="button" data-action="do-something">Acción</button>
<script>
document.querySelectorAll("[data-action]").forEach((el) => {
el.addEventListener("click", (e) => {
// acción...
});
});
</script>
Event Delegation
// Un solo listener en el padre en lugar de uno por hijo
document.querySelector(".list")?.addEventListener("click", (event) => {
const item = event.target.closest(".list__item");
if (!item) return; // No era un item
console.log("Item clicked:", item.dataset.id);
});
Pointer Events (unifican mouse + touch)
// Reemplaza mouse + touch events
element.addEventListener("pointerdown", (e) => {
console.log(e.pointerType); // "mouse" | "touch" | "pen"
console.log(e.clientX, e.clientY);
});
element.addEventListener("pointerup", handlePointerUp);
element.addEventListener("pointermove", handlePointerMove);
// CSS: touch-action: none si se va a prevenir scroll
Eventos de teclado
// Keyboard accessible
element.addEventListener("keydown", (event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
// acción...
}
});
11. Módulos ES (ESM)
Estructura de archivos
src/
├── js/
│ ├── main.js # Entry point
│ ├── editor.js # Lógica del editor
│ └── utils.js # Funciones helper
Export/Import
// editor.js
export function initEditor() {
// ...
}
export const EDITOR_CONFIG = {
maxLength: 10000,
};
// main.js
import { initEditor } from "./editor.js";
document.addEventListener("DOMContentLoaded", () => {
initEditor();
});
Dynamic import()
// Carga bajo demanda (código splitting)
const module = await import("./editor.js");
module.initEditor();
// Solo importar cuando se necesita
button.addEventListener("click", async () => {
const { showToast } = await import("./toast.js");
showToast("Guardado");
});
import.meta
// URL del módulo actual
console.log(import.meta.url);
// "https://ejemplo.com/js/utils.js"
// Metadatos (Vite: import.meta.env, import.meta.glob)
if (import.meta.env?.DEV) {
console.log("Modo desarrollo");
}
Import Maps
<!-- Controlar resolución de módulos en HTML -->
<script type="importmap">
{
"imports": {
"lodash-es": "/node_modules/lodash-es/lodash.js"
}
}
</script>
<script type="module">
import { debounce } from "lodash-es";
</script>
12. Fetch API
Solicitud básica
async function fetchData(url) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
console.error("Fetch failed:", error);
return null;
}
}
Con headers
async function postData(url, data) {
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer token123",
},
body: JSON.stringify(data),
});
return response.json();
}
Abort controller (cancelar petición)
const controller = new AbortController();
const signal = controller.signal;
fetch(url, { signal })
.then((response) => response.json())
.catch((error) => {
if (error.name === "AbortError") {
console.log("Request cancelled");
}
});
// Cancelar después de 5 segundos
setTimeout(() => controller.abort(), 5000);
Retry automático
async function fetchWithRetry(url, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return await response.json();
} catch (error) {
if (i === retries - 1) throw error;
await new Promise((r) => setTimeout(r, 1000 * (i + 1)));
}
}
}
Streaming (respuestas grandes)
// Leer respuesta como stream (sin esperar el body completo)
const response = await fetch("/large-file.json");
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value, { stream: true });
// Procesar chunk progresivamente
}
// O usando Response.json() con reviver
const data = await response.json({
reviver: (key, value, context) => {
if (key === "timestamp") return new Date(value);
return value;
},
});
13. Programación Defensiva
Validar inputs
function processData(data) {
if (!data || typeof data !== "object") {
console.error("Invalid data");
return null;
}
// procesar...
}
Optional chaining y nullish
// Optional chaining
const value = obj?.nested?.property;
const arr = data?.items?.[0];
const method = obj?.doSomething?.();
// Nullish coalescing
const name = user.name ?? "Anonymous";
const count = items.length ?? 0;
Validar elementos
// Verificar que elemento existe antes de usar
const button = document.querySelector(".btn-submit");
if (button) {
button.addEventListener("click", handleSubmit);
}
14. Storage
localStorage
// Guardar
localStorage.setItem("user", JSON.stringify({ name: "John" }));
// Leer
const user = JSON.parse(localStorage.getItem("user") || "{}");
// Remover
localStorage.removeItem("user");
// Verificar disponibilidad
const hasStorage = () => {
try {
localStorage.setItem("test", "test");
localStorage.removeItem("test");
return true;
} catch (e) {
return false;
}
};
sessionStorage
// Similar a localStorage pero se limpia al cerrar pestaña
sessionStorage.setItem("tempData", "value");
const temp = sessionStorage.getItem("tempData");
IndexedDB (wrapper promisificado)
// Wrapper funcional para operaciones comunes
const db = {
async open(name, version = 1, upgrade) {
return new Promise((resolve, reject) => {
const request = indexedDB.open(name, version);
request.onupgradeneeded = upgrade;
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
},
async get(dbName, storeName, id) {
const db = await this.open(dbName);
return new Promise((resolve, reject) => {
const tx = db.transaction(storeName, "readonly");
const store = tx.objectStore(storeName);
const request = store.get(id);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
},
async set(dbName, storeName, value) {
const db = await this.open(dbName);
return new Promise((resolve, reject) => {
const tx = db.transaction(storeName, "readwrite");
const store = tx.objectStore(storeName);
const request = store.put(value);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
},
};
// Uso
await db.set("myapp", "items", { id: 1, name: "test" });
const item = await db.get("myapp", "items", 1);
15. Date/Time (Intl API)
Formatear fechas
const date = new Date();
// Formato local
new Intl.DateTimeFormat("es-ES", {
year: "numeric",
month: "long",
day: "numeric",
}).format(date);
// Formato corto
new Intl.DateTimeFormat("en-US", {
month: "short",
day: "numeric",
}).format(date);
Formatear números
// Moneda
new Intl.NumberFormat("es-ES", {
style: "currency",
currency: "EUR",
}).format(1234.56);
// Porcentaje
new Intl.NumberFormat("en-US", {
style: "percent",
}).format(0.75);
RelativeTimeFormat
const rtf = new Intl.RelativeTimeFormat("es", { numeric: "auto" });
rtf.format(-1, "day"); // "ayer"
rtf.format(3, "month"); // "dentro de 3 meses"
rtf.format(-5, "minute"); // "hace 5 minutos"
16. Intersection Observer
Detectar cuando un elemento entra en el viewport:
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add("visible");
// O cargar contenido lazy
}
});
},
{
root: null,
rootMargin: "0px",
threshold: 0.1,
},
);
observer.observe(document.querySelector(".lazy-load"));
17. Web Animations API
element.animate(
[
{ opacity: 0, transform: "translateY(20px)" },
{ opacity: 1, transform: "translateY(0)" },
],
{
duration: 300,
easing: "ease-out",
fill: "forwards",
},
);
18. APIs de Navegador Modernas
Clipboard API
// Escribir al portapapeles
await navigator.clipboard.writeText("Texto a copiar");
// Leer del portapapeles
const text = await navigator.clipboard.readText();
// Escribir imagen
const blob = new Blob(["<svg>...</svg>"], { type: "image/svg+xml" });
await navigator.clipboard.write([new ClipboardItem({ "image/svg+xml": blob })]);
Web Share API
// Compartir contenido (nativo del SO)
await navigator.share({
title: "Mi artículo",
text: "Mira esto",
url: "https://ejemplo.com",
});
BroadcastChannel (comunicación entre tabs)
// Tab A — enviar
const channel = new BroadcastChannel("app-updates");
channel.postMessage({ type: "LOGOUT", userId: 42 });
// Tab B — recibir
const channel = new BroadcastChannel("app-updates");
channel.addEventListener("message", (event) => {
if (event.data.type === "LOGOUT") {
cerrarSesion();
}
});
WebSocket
const ws = new WebSocket("wss://api.ejemplo.com/ws");
ws.addEventListener("open", () => {
ws.send(JSON.stringify({ type: "join", room: "chat" }));
});
ws.addEventListener("message", (event) => {
const data = JSON.parse(event.data);
mostrarMensaje(data);
});
ws.addEventListener("close", () => {
console.log("Desconectado");
// Reconexión automática después de 3s
setTimeout(conectarWebSocket, 3000);
});
View Transition API
// Transiciones SPA sin librerías
document.addEventListener("click", async (e) => {
const link = e.target.closest("a[data-transition]");
if (!link) return;
e.preventDefault();
const url = link.href;
// El navegador captura el estado actual
const transition = document.startViewTransition(async () => {
const html = await fetch(url).then((r) => r.text());
document.body.innerHTML = html;
});
await transition.finished;
});
Popover API
// Controlar popover desde JS
const popover = document.getElementById("menu-popover");
popover.showPopover();
popover.hidePopover();
popover.togglePopover();
// Cerrar con hidePopover() desde cualquier evento
popover.addEventListener("beforetoggle", (e) => {
if (e.newState === "open") {
console.log("Popover abierto");
}
});
File System Access API
// Abrir selector de archivos y leer
const [handle] = await window.showOpenFilePicker();
const file = await handle.getFile();
const content = await file.text();
// Guardar archivo
const saveHandle = await window.showSaveFilePicker();
const writable = await saveHandle.createWritable();
await writable.write("Contenido del archivo");
await writable.close();
19. Performance
Evitar Reflows/Repaints
// ❌ Malo - múltiples reflows
element.style.width = "100px";
element.style.height = "100px";
element.style.padding = "10px";
// ✅ Bueno - usar CSS classes o cssText
element.style.cssText = "width: 100px; height: 100px; padding: 10px;";
// ✅ Mejor - usar classList
element.classList.add("active");
requestIdleCallback (tareas no críticas)
// Ejecutar cuando el navegador esté inactivo
requestIdleCallback(
(deadline) => {
while (deadline.timeRemaining() > 0 && tasks.length > 0) {
processTask(tasks.shift());
}
if (tasks.length > 0) {
requestIdleCallback(processTasks); // Continuar después
}
},
{ timeout: 2000 },
);
requestAnimationFrame (animaciones JS necesarias)
// Animación fluida con delta time
let start = null;
function animate(timestamp) {
if (!start) start = timestamp;
const progress = timestamp - start;
element.style.transform = `translateX(${Math.min(progress * 0.1, 200)}px)`;
if (progress < 2000) {
requestAnimationFrame(animate);
}
}
requestAnimationFrame(animate);
Debounce
function debounce(fn, delay) {
let timeoutId;
return (...args) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn(...args), delay);
};
}
// Uso
const debouncedSearch = debounce(search, 300);
input.addEventListener("input", debouncedSearch);
Throttle
function throttle(fn, limit) {
let inThrottle;
return (...args) => {
if (!inThrottle) {
fn(...args);
inThrottle = true;
setTimeout(() => (inThrottle = false), limit);
}
};
}
// Uso
const throttledScroll = throttle(handleScroll, 100);
window.addEventListener("scroll", throttledScroll);
20. Prohibiciones
- ❌ NO usar: Vue, Svelte, Angular (sin autorización)
- ❌ NO usar: React (sin cargar skill
react) - ❌ NO usar: TypeScript para vanilla (sin cargar skill
typescript) - ❌ No usar
var - ❌ No dejar
console.log,debuggeren código final - ❌ No usar jQuery para selección básica (usar vanilla)
- ❌ No usar JavaScript para animaciones simples (usar CSS)
- ❌ No usar
onclicken HTML (usar addEventListener) - ❌ No usar
innerHTMLcon datos de usuario (riesgo XSS) - ❌ No usar
JSON.parse(JSON.stringify(obj))para clonar (usarstructuredClone) - ❌ No anidar
Promiseconstructors si se puede usarPromise.withResolvers()
21. Dependencias Comunes
JavaScript vanilla típicamente no requiere dependencias. Para proyectos específicos, considerar:
| Paquete | Uso |
|---|---|
| date-fns | Fechas (si Intl.DateTimeFormat no basta) |
| idb | IndexedDB con promesas (wrapper) |
Nota: Con structuredClone(), Object.groupBy(), Set operations e Iterator helpers, lodash ya no es necesario en la mayoría de proyectos vanilla.
22. Referencias
Nota: Para HTML semántico, ver HTML Nota: Para estilos CSS, ver CSS Nota: Para despliegue, ver Deploy
Última actualización: 2026-07
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.