🧱Extensiones de página
Una extensión de página añade una página completa a la app, servida en /x/<id>. Es un módulo de JavaScript
normal –sin paso de compilación– más una API opcional en Python.
Dónde aparece: todas las páginas se listan en Extensiones en la barra lateral (la página /x). Haz clic en la
chincheta de la parte superior derecha de una página (o en la página de Extensiones, o activa En la barra lateral en
Ajustes -> Extensiones) para darle su propia entrada en la barra lateral, justo debajo de Extensiones; vuelve a hacer clic para quitarla.
/root/maxxing/extensions/notes-board/
manifest.json
page.js the page (ES module)
api.py optional: its HTTP API
Pídele a Claude "crea una página que ..." y la skill maxxing-extension la escribe por ti, o sigue
estos pasos.
Una instalación nueva incluye una página, Tus páginas (extensions/pages/): qué es una extensión de página, y
ejemplos (archivo de cliente, seguimiento de pedidos, facturas...) que abren una sesión con la petición ya escrita. Es
una extensión normal: borra su carpeta, o desactívala en Ajustes -> Extensiones, cuando ya no la necesites
- las actualizaciones no la devuelven.
1. manifest.json
{"id": "notes-board", "name": "Notes board", "type": "page", "icon": "sticky-note", "version": "1.0.0"}
name también puede ser {"en": "Notes board", "fr": "Tableau de notes"}: la app muestra el de su idioma.
"api": 1 (opcional, 1 por defecto) es la versión del contrato de extensiones para la que está escrita la página: si una versión
futura deja de admitirla, la comprobación previa a la actualización te lo avisa antes de actualizar (Ajustes -> Sistema).
2. api.py (opcional)
Cada petición a /api/ext/<id>/<path> llama a:
def handle(ctx, method, path, query, body):
...
return {...} # sent as JSON (200)
# or: return 404, {"error": "not_found", "message": "..."} to choose the status code
| Argumento | Valor |
|---|---|
ctx |
igual que en los widgets: id, workspace, state_dir, secret(name), config |
method |
GET, POST, PUT, PATCH, DELETE |
path |
la parte posterior a /api/ext/<id>/ (por ejemplo, notes, notes/42) |
query |
parámetros de la query string como un diccionario |
body |
JSON interpretado (o texto), None para GET; máximo 5 MB |
Se ejecuta en un hilo de trabajo con un límite de 30 segundos. Las peticiones las autentica la app: solo un usuario con sesión iniciada puede llamarla.
Ejemplo:
import json
import time
from pathlib import Path
def handle(ctx, method, path, query, body):
f = Path(ctx["state_dir"]) / "notes.json"
notes = json.loads(f.read_text()) if f.exists() else []
if path == "notes" and method == "GET":
return {"notes": notes}
if path == "notes" and method == "POST":
text = str((body or {}).get("text", "")).strip()[:500]
if text:
notes.insert(0, {"text": text, "at": time.time()})
tmp = f.with_suffix(".tmp")
tmp.write_text(json.dumps(notes[:200]))
tmp.replace(f)
return {"notes": notes}
return 404, {"error": "not_found", "message": f"{method} {path}"}
3. page.js
La app carga /ext/<id>/page.js y llama a mount(el, ctx):
ctx |
|
|---|---|
api(path, opts?) |
llama a /api/ext/<id>/<path> con autenticación y cabecera CSRF; opts = {method, body} (el body se envía como JSON); devuelve el JSON interpretado y se rechaza ante errores HTTP |
navigate(route) |
ir a otra página de la app, por ejemplo navigate("/tasks"); /sessions?path=<topic path>&new=1&prompt=<text> abre una sesión nueva con el texto ya escrito |
theme |
"dark" o "light" |
lang |
el idioma de la app, "en" o "fr" |
t(key) |
función de traducción de la app |
icon(name) |
un icono de la app como elemento <svg> (mismos nombres que el icon del manifest), por ejemplo button.prepend(ctx.icon("plus")) |
Devuelve una función de limpieza: se ejecuta cuando el usuario sale de la página.
Por seguridad, la app solo ejecuta scripts que ella misma sirve: page.js no puede añadir etiquetas <script> en línea a la página
ni construir código a partir de texto (eval); en su lugar, divide el código en módulos de la carpeta de la extensión. Los widgets HTML siguen
funcionando como antes.
El módulo se descarga una vez por versión de page.js y se conserva: el código de la parte superior del archivo se ejecuta una vez,
mount se ejecuta en cada visita. Al guardar page.js se crea una versión nueva, que se recoge en la siguiente visita.
export function mount(el, ctx) {
el.innerHTML = `
<div class="ui-page" style="max-width:640px">
<h1 class="ui-h1">Notes board</h1>
<form class="ui-row">
<input class="ui-input" name="text" placeholder="New note" style="flex:1">
<button class="ui-btn">Add</button>
</form>
<ul class="ui-list"></ul>
<p class="ui-muted" hidden>No note yet: write the first one above.</p>
</div>`;
const list = el.querySelector("ul");
const empty = el.querySelector("p");
const form = el.querySelector("form");
form.querySelector("button").prepend(ctx.icon("plus"));
const render = ({ notes }) => {
list.hidden = !notes.length;
empty.hidden = !!notes.length;
list.replaceChildren(...notes.map((n) => {
const li = document.createElement("li");
li.textContent = n.text; // never inject data with innerHTML
return li;
}));
};
ctx.api("notes").then(render);
const onSubmit = async (e) => {
e.preventDefault();
const text = form.text.value.trim();
if (!text) return;
render(await ctx.api("notes", { method: "POST", body: { text } }));
form.reset();
};
form.addEventListener("submit", onSubmit);
return () => form.removeEventListener("submit", onSubmit);
}
Estilo
La página recibe el kit de interfaz de la app: clases CSS que se ven exactamente igual que el resto de la app y siguen por sí solas el tema, el color de acento y la densidad. Úsalas mejor que los estilos escritos a mano:
| Clases | |
|---|---|
ui-page ui-section ui-grid ui-row ui-stack ui-spacer |
diseño: columna de página, sección, cuadrícula adaptable de tarjetas, fila, columna |
ui-h1 ui-h2 ui-h3 ui-text ui-muted ui-small ui-label ui-num |
texto (título, título de sección, cuerpo, secundario, etiquetas, cifras en columnas) |
ui-card ui-card-header ui-card-footer |
una tarjeta; la cabecera lleva un ui-icon-box y un <div> con el título y una línea |
ui-tile |
una tarjeta en la que se puede hacer clic: <button class="ui-tile"><div class="ui-icon-box">svg</div><div><b>Title</b><span>line</span></div></button> |
ui-icon-box (+ ui-lg, ui-neutral) |
un icono sobre un cuadrado suave del color de acento |
ui-btn (+ ui-secondary ui-outline ui-ghost ui-danger, ui-sm, ui-icon-only) |
botones (ui-btn solo = acción principal) |
ui-field ui-label ui-input ui-select ui-textarea ui-check |
formularios |
ui-badge ui-dot (+ ui-success ui-warning ui-error ui-primary ui-outline) |
estados |
ui-list ui-table ui-table-wrap ui-stat ui-stat-value ui-stat-label ui-progress |
datos |
ui-steps ui-step-num ui-callout (+ ui-info ui-success ui-warning ui-error) ui-empty ui-skeleton |
pasos, avisos, estados vacíos y de carga |
ui-code ui-kbd ui-link ui-separator |
el resto |
Los iconos vienen de ctx.icon(name); úsalos en lugar de emojis. Tu propio CSS siempre se impone al del kit, y las variables CSS de la app
están disponibles para el resto: --background, --foreground, --card, --border, --muted,
--muted-foreground, --primary, --primary-foreground, --success, --warning, --destructive. Los widgets HTML
reciben el mismo kit. Los campos del kit son de 16 px en móviles (sin zoom en iPhone).
4. Probar y cargar
- Lado Python: llama a
handle()directamente (mira el fragmento de prueba en Widgets del panel, conapi.handle(ctx, "GET", "notes", {}, None)). - Sintaxis de JavaScript:
node --check page.jssi Node está instalado. - Ajustes -> Extensiones -> Recargar, y luego abre la página desde la barra lateral (Extensiones, o su propia entrada una vez fijada). Los errores aparecen en la fila de la extensión.