Javascript
Skills modulares para asistentes de codificación por IA. Define reglas, flujos de trabajo y estándares técnicos mediante archivos Markdowm.
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.
2 things to look at
- 21 days oldThe repository was created 21 days ago. New is not bad, but a brand new repository carrying a familiar-sounding name is the shape a typosquat arrives in, and there has been no time for anyone else to find a problem with it.
- 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.
What its author says it does
Copied from the file, not written here
Reglas de JavaScript vanilla ES6+ - ES2025, programación defensiva, módulos, DOM, fetch, APIs modernas, estructuras de datos, patrones avanzados
SKILL.md
26.3 KB, 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