🧱Seiten-Erweiterungen
Eine Seiten-Erweiterung fügt der App eine vollständige Seite hinzu, die unter /x/<id> bereitgestellt wird. Sie ist ein einfaches JavaScript-Modul – ohne Build-Schritt – plus eine optionale Python-API.
Wo sie erscheint: Jede Seite steht in der Seitenleiste unter Erweiterungen (die Seite /x). Klicke auf die Pinnnadel oben rechts auf einer Seite (oder auf der Seite Erweiterungen, oder aktiviere In der Seitenleiste unter Einstellungen -> Erweiterungen), damit sie einen eigenen Eintrag in der Seitenleiste direkt unter Erweiterungen bekommt; klicke erneut darauf, um sie zu lösen.
/root/maxxing/extensions/notes-board/
manifest.json
page.js the page (ES module)
api.py optional: its HTTP API
Bitte Claude: „Erstelle eine Seite, die ..." und der Skill maxxing-extension schreibt sie für dich, oder folge diesen Schritten.
Eine neue Installation enthält eine Seite, Deine Seiten (extensions/pages/): Sie erklärt, was eine Seiten-Erweiterung ist, und zeigt Beispiele (Kundendatei, Bestellverfolgung, Rechnungen ...), die eine Session mit bereits eingetippter Anfrage öffnen. Sie ist eine ganz normale Erweiterung: Lösche ihren Ordner oder schalte sie unter Einstellungen -> Erweiterungen aus, wenn du sie nicht mehr brauchst – Updates bringen sie nicht zurück.
1. manifest.json
{"id": "notes-board", "name": "Notes board", "type": "page", "icon": "sticky-note", "version": "1.0.0"}
name kann auch {"en": "Notes board", "fr": "Tableau de notes"} sein: Die App zeigt den Namen in ihrer Sprache an. "api": 1 (optional, Standard 1) ist die Version des Erweiterungsvertrags, für die die Seite geschrieben ist: Wenn eine künftige Version sie nicht mehr unterstützt, weist dich der Update-Vorabcheck vor dem Update darauf hin (Einstellungen -> System).
2. api.py (optional)
Jede Anfrage an /api/ext/<id>/<path> ruft Folgendes auf:
def handle(ctx, method, path, query, body):
...
return {...} # sent as JSON (200)
# or: return 404, {"error": "not_found", "message": "..."} to choose the status code
| Argument | Wert |
|---|---|
ctx |
wie bei Widgets: id, workspace, state_dir, secret(name), config |
method |
GET, POST, PUT, PATCH, DELETE |
path |
der Teil nach /api/ext/<id>/ (z. B. notes, notes/42) |
query |
Query-String-Parameter als Dict |
body |
geparstes JSON (oder Text), None bei GET; max. 5 MB |
Sie läuft in einem Worker-Thread mit einem Limit von 30 Sekunden. Die Anfragen werden von der App authentifiziert: Nur ein angemeldeter Nutzer kann sie aufrufen.
Beispiel:
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
Die App lädt /ext/<id>/page.js und ruft mount(el, ctx) auf:
ctx |
|
|---|---|
api(path, opts?) |
ruft /api/ext/<id>/<path> mit Authentifizierung und CSRF-Header auf; opts = {method, body} (der Body wird als JSON gesendet); liefert das geparste JSON zurück und schlägt bei HTTP-Fehlern fehl |
navigate(route) |
wechselt zu einer anderen Seite der App, z. B. navigate("/tasks"); /sessions?path=<topic path>&new=1&prompt=<text> öffnet eine neue Session mit dem eingetippten Text |
theme |
"dark" oder "light" |
lang |
die Sprache der App, "en" oder "fr" |
t(key) |
Übersetzungshilfe der App |
icon(name) |
ein Icon der App als <svg>-Element (dieselben Namen wie beim icon im Manifest), z. B. button.prepend(ctx.icon("plus")) |
Gib eine Aufräumfunktion zurück: Sie läuft, wenn der Nutzer die Seite verlässt.
Aus Sicherheitsgründen führt die App nur Skripte aus, die sie selbst ausliefert: page.js kann der Seite keine Inline-<script>-Tags hinzufügen und keinen Code aus Text erzeugen (eval); teile den Code stattdessen in Module im Ordner der Erweiterung auf. HTML-Widgets funktionieren weiterhin wie bisher.
Das Modul wird einmal pro Version von page.js heruntergeladen und gespeichert: Code am Anfang der Datei läuft einmal, mount läuft bei jedem Besuch. Wenn du page.js speicherst, erhält sie eine neue Version, die beim nächsten Besuch übernommen wird.
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);
}
Gestaltung
Die Seite erhält das UI-Kit der App: CSS-Klassen, die genau wie der Rest der App aussehen und von selbst dem Theme, der Akzentfarbe und der Dichte folgen. Bevorzuge sie gegenüber selbst geschriebenen Styles:
| Klassen | |
|---|---|
ui-page ui-section ui-grid ui-row ui-stack ui-spacer |
Layout: Seitenspalte, Abschnitt, responsives Raster aus Karten, Zeile, Spalte |
ui-h1 ui-h2 ui-h3 ui-text ui-muted ui-small ui-label ui-num |
Text (Titel, Abschnittstitel, Fließtext, Sekundärtext, Beschriftungen, Zahlen in Spalten) |
ui-card ui-card-header ui-card-footer |
eine Karte; der Header nimmt eine ui-icon-box und ein <div> mit dem Titel und einer Zeile auf |
ui-tile |
eine anklickbare Karte: <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) |
ein Icon auf einem weichen Quadrat in der Akzentfarbe |
ui-btn (+ ui-secondary ui-outline ui-ghost ui-danger, ui-sm, ui-icon-only) |
Schaltflächen (ui-btn allein = Hauptaktion) |
ui-field ui-label ui-input ui-select ui-textarea ui-check |
Formulare |
ui-badge ui-dot (+ ui-success ui-warning ui-error ui-primary ui-outline) |
Status |
ui-list ui-table ui-table-wrap ui-stat ui-stat-value ui-stat-label ui-progress |
Daten |
ui-steps ui-step-num ui-callout (+ ui-info ui-success ui-warning ui-error) ui-empty ui-skeleton |
Schritte, Hinweise, leere Zustände und Ladezustände |
ui-code ui-kbd ui-link ui-separator |
der Rest |
Icons kommen von ctx.icon(name); verwende sie statt Emojis. Dein eigenes CSS gewinnt immer gegen das Kit, und die CSS-Variablen der App stehen für alles Weitere bereit: --background, --foreground, --card, --border, --muted, --muted-foreground, --primary, --primary-foreground, --success, --warning, --destructive. HTML-Widgets erhalten dasselbe Kit. Die Eingabefelder des Kits haben auf Smartphones 16 px (kein Zoomen auf dem iPhone).
4. Testen und laden
- Python-Seite: Rufe
handle()direkt auf (siehe das Test-Snippet in Dashboard-Widgets, mitapi.handle(ctx, "GET", "notes", {}, None)). - JavaScript-Syntax:
node --check page.js, wenn Node installiert ist. - Einstellungen -> Erweiterungen -> Neu laden, öffne dann die Seite über die Seitenleiste (Erweiterungen oder ihren eigenen Eintrag, sobald sie angepinnt ist). Fehler erscheinen in der Zeile der Erweiterung.