🧱Estensioni pagina
Un'estensione pagina aggiunge un'intera pagina all'app, servita su /x/<id>. È un semplice modulo JavaScript
– senza fase di build – più un'API Python opzionale.
Dove compare: ogni pagina è elencata sotto Estensioni nella barra laterale (la pagina /x). Clicca sulla
puntina in alto a destra di una pagina (o nella pagina Estensioni, oppure attiva Nella barra laterale in
Impostazioni -> Estensioni) per darle una voce dedicata nella barra laterale, subito sotto Estensioni; cliccala di nuovo per rimuoverla.
/root/maxxing/extensions/notes-board/
manifest.json
page.js the page (ES module)
api.py optional: its HTTP API
Chiedi a Claude "crea una pagina che ..." e la skill maxxing-extension la scrive per te, oppure segui
questi passaggi.
Una nuova installazione include una pagina, Le tue pagine (extensions/pages/): spiega che cos'è un'estensione pagina e mostra
alcuni esempi (scheda cliente, tracciamento ordini, fatture...) che aprono una sessione con la richiesta già scritta. È
un'estensione come le altre: elimina la sua cartella, oppure disattivala in Impostazioni -> Estensioni, quando non ti serve più
- gli aggiornamenti non la reinstallano.
1. manifest.json
{"id": "notes-board", "name": "Notes board", "type": "page", "icon": "sticky-note", "version": "1.0.0"}
name può anche essere {"en": "Notes board", "fr": "Tableau de notes"}: l'app mostra quello della sua lingua.
"api": 1 (opzionale, predefinito 1) è la versione del contratto delle estensioni per cui è scritta la pagina: se una versione
futura smette di supportarla, il controllo preliminare dell'aggiornamento te lo segnala prima che tu aggiorni (Impostazioni -> Sistema).
2. api.py (opzionale)
Ogni richiesta a /api/ext/<id>/<path> chiama:
def handle(ctx, method, path, query, body):
...
return {...} # sent as JSON (200)
# or: return 404, {"error": "not_found", "message": "..."} to choose the status code
| Argomento | Valore |
|---|---|
ctx |
uguale a quello dei widget: id, workspace, state_dir, secret(name), config |
method |
GET, POST, PUT, PATCH, DELETE |
path |
la parte dopo /api/ext/<id>/ (es. notes, notes/42) |
query |
i parametri della query string come dizionario |
body |
JSON già interpretato (o testo), None per GET; max 5 MB |
Viene eseguita in un thread di lavoro con un limite di 30 secondi. Le richieste sono autenticate dall'app: solo un utente connesso può chiamarla.
Esempio:
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
L'app carica /ext/<id>/page.js e chiama mount(el, ctx):
ctx |
|
|---|---|
api(path, opts?) |
chiama /api/ext/<id>/<path> con autenticazione e header CSRF; opts = {method, body} (il body viene inviato come JSON); restituisce il JSON interpretato, viene rifiutata in caso di errori HTTP |
navigate(route) |
va a un'altra pagina dell'app, es. navigate("/tasks"); /sessions?path=<topic path>&new=1&prompt=<text> apre una nuova sessione con il testo già scritto |
theme |
"dark" o "light" |
lang |
la lingua dell'app, "en" o "fr" |
t(key) |
funzione di traduzione dell'app |
icon(name) |
un'icona dell'app come elemento <svg> (stessi nomi dell'icon del manifest), es. button.prepend(ctx.icon("plus")) |
Restituisci una funzione di pulizia: viene eseguita quando l'utente lascia la pagina.
Per sicurezza l'app esegue solo gli script che serve lei stessa: page.js non può aggiungere tag <script> inline alla pagina
né costruire codice a partire da un testo (eval); suddividi invece il codice in moduli nella cartella dell'estensione. I widget HTML continuano
a funzionare come prima.
Il modulo viene scaricato una sola volta per ogni versione di page.js e conservato: il codice all'inizio del file viene eseguito una volta,
mount a ogni visita. Salvando page.js si crea una nuova versione, che viene caricata alla visita successiva.
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);
}
Stile
La pagina riceve lo UI kit dell'app: classi CSS che hanno esattamente lo stesso aspetto del resto dell'app e seguono da sole il tema, il colore d'accento e la densità. Usale al posto di stili scritti a mano:
| Classi | |
|---|---|
ui-page ui-section ui-grid ui-row ui-stack ui-spacer |
layout: colonna della pagina, sezione, griglia adattiva di card, riga, colonna |
ui-h1 ui-h2 ui-h3 ui-text ui-muted ui-small ui-label ui-num |
testo (titolo, titolo di sezione, corpo, secondario, etichette, cifre in colonna) |
ui-card ui-card-header ui-card-footer |
una card; l'intestazione accetta un ui-icon-box e un <div> con il titolo e una riga |
ui-tile |
una card cliccabile: <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'icona su un quadrato tenue del colore d'accento |
ui-btn (+ ui-secondary ui-outline ui-ghost ui-danger, ui-sm, ui-icon-only) |
pulsanti (ui-btn da solo = azione principale) |
ui-field ui-label ui-input ui-select ui-textarea ui-check |
moduli |
ui-badge ui-dot (+ ui-success ui-warning ui-error ui-primary ui-outline) |
stati |
ui-list ui-table ui-table-wrap ui-stat ui-stat-value ui-stat-label ui-progress |
dati |
ui-steps ui-step-num ui-callout (+ ui-info ui-success ui-warning ui-error) ui-empty ui-skeleton |
passaggi, avvisi, stati vuoti e di caricamento |
ui-code ui-kbd ui-link ui-separator |
il resto |
Le icone provengono da ctx.icon(name); usale al posto delle emoji. Il tuo CSS ha sempre la precedenza sul kit, e le variabili CSS
dell'app sono a disposizione per il resto: --background, --foreground, --card, --border, --muted,
--muted-foreground, --primary, --primary-foreground, --success, --warning, --destructive. I widget HTML
ricevono lo stesso kit. Gli input del kit sono a 16 px sui telefoni (niente zoom su iPhone).
4. Prova e caricamento
- Lato Python: chiama direttamente
handle()(vedi l'esempio di test in Widget della dashboard, conapi.handle(ctx, "GET", "notes", {}, None)). - Sintassi JavaScript:
node --check page.jsse Node è installato. - Impostazioni -> Estensioni -> Ricarica, poi apri la pagina dalla barra laterale (Estensioni, oppure la sua voce dedicata una volta fissata). Gli errori compaiono nella riga dell'estensione.