Harness MaxxingDocumentação
📚 Todas as páginas

🧩 Estender · 5 min de leitura

🧱Extensões de página

Uma extensão de página adiciona uma página completa ao app, servida em /x/<id>. Ela é um módulo JavaScript simples – sem etapa de build – mais uma API em Python opcional.

Onde ela aparece: todas as páginas ficam listadas em Extensões na barra lateral (a página /x). Clique no alfinete no canto superior direito de uma página (ou na página Extensões, ou ative Na barra lateral em Configurações -> Extensões) para dar a ela uma entrada própria na barra lateral, logo abaixo de Extensões; clique de novo para desafixar.

/root/maxxing/extensions/notes-board/
  manifest.json
  page.js        the page (ES module)
  api.py         optional: its HTTP API

Peça ao Claude "crie uma página que ..." e a skill maxxing-extension escreve tudo para você, ou siga estes passos.

Uma instalação nova já vem com uma página, Suas páginas (extensions/pages/): ela explica o que é uma extensão de página e traz exemplos (ficha de cliente, controle de pedidos, faturas...) que abrem uma sessão com o pedido já digitado. É uma extensão comum: apague a pasta dela, ou desative-a em Configurações -> Extensões, quando não precisar mais dela – as atualizações não a trazem de volta.

1. manifest.json

{"id": "notes-board", "name": "Notes board", "type": "page", "icon": "sticky-note", "version": "1.0.0"}

name também pode ser {"en": "Notes board", "fr": "Tableau de notes"}: o app mostra o nome no idioma em que estiver. "api": 1 (opcional, padrão 1) é a versão do contrato de extensões para a qual a página foi escrita: se uma versão futura deixar de oferecer suporte a ela, a verificação prévia da atualização avisa antes de você atualizar (Configurações -> Sistema).

2. api.py (opcional)

Cada requisição a /api/ext/<id>/<path> chama:

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 ao dos widgets: id, workspace, state_dir, secret(name), config
method GET, POST, PUT, PATCH, DELETE
path a parte depois de /api/ext/<id>/ (por exemplo, notes, notes/42)
query parâmetros da query string em um dict
body JSON interpretado (ou texto), None para GET; máximo de 5 MB

Ela roda em uma thread de trabalho com limite de 30 segundos. As requisições são autenticadas pelo app: somente um usuário conectado pode chamá-la.

Exemplo:

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

O app carrega /ext/<id>/page.js e chama mount(el, ctx):

ctx
api(path, opts?) chama /api/ext/<id>/<path> com autenticação e cabeçalho CSRF; opts = {method, body} (o body é enviado como JSON); resolve com o JSON interpretado e rejeita em caso de erro HTTP
navigate(route) vai para outra página do app, por exemplo navigate("/tasks"); /sessions?path=<topic path>&new=1&prompt=<text> abre uma nova sessão com o texto já digitado
theme "dark" ou "light"
lang o idioma do app, "en" ou "fr"
t(key) função auxiliar de tradução do app
icon(name) um ícone do app como elemento <svg> (mesmos nomes do icon do manifest), por exemplo button.prepend(ctx.icon("plus"))

Retorne uma função de limpeza: ela é executada quando o usuário sai da página.

Por segurança, o app só executa scripts que ele mesmo serve: o page.js não pode adicionar tags <script> inline à página nem montar código a partir de texto (eval); em vez disso, divida o código em módulos dentro da pasta da extensão. Os widgets HTML continuam funcionando como antes.

O módulo é baixado uma vez por versão do page.js e mantido em cache: o código no topo do arquivo roda uma vez, e o mount roda a cada visita. Ao salvar o page.js, ele ganha uma nova versão, que é usada na próxima 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

A página recebe o kit de UI do app: classes CSS que ficam idênticas ao resto do app e acompanham sozinhas o tema, a cor de destaque e a densidade. Prefira essas classes a estilos escritos à mão:

Classes
ui-page ui-section ui-grid ui-row ui-stack ui-spacer layout: coluna da página, seção, grade responsiva de cards, linha, coluna
ui-h1 ui-h2 ui-h3 ui-text ui-muted ui-small ui-label ui-num texto (título, título de seção, corpo, secundário, rótulos, números em colunas)
ui-card ui-card-header ui-card-footer um card; o cabeçalho recebe um ui-icon-box e uma <div> com o título e uma linha
ui-tile um card clicável: <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) um ícone sobre um quadrado suave na cor de destaque
ui-btn (+ ui-secondary ui-outline ui-ghost ui-danger, ui-sm, ui-icon-only) botões (ui-btn sozinho = ação principal)
ui-field ui-label ui-input ui-select ui-textarea ui-check formulários
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 dados
ui-steps ui-step-num ui-callout (+ ui-info ui-success ui-warning ui-error) ui-empty ui-skeleton passos, avisos, estados vazios e de carregamento
ui-code ui-kbd ui-link ui-separator o restante

Os ícones vêm de ctx.icon(name); use-os no lugar de emojis. O seu CSS próprio sempre prevalece sobre o kit, e as variáveis CSS do app estão disponíveis para o resto: --background, --foreground, --card, --border, --muted, --muted-foreground, --primary, --primary-foreground, --success, --warning, --destructive. Os widgets HTML recebem o mesmo kit. Os campos de entrada do kit têm 16 px em celulares (sem zoom no iPhone).

4. Testar e carregar

  • Lado Python: chame handle() diretamente (veja o trecho de teste em Widgets do painel, com api.handle(ctx, "GET", "notes", {}, None)).
  • Sintaxe JavaScript: node --check page.js se o Node estiver instalado.
  • Configurações -> Extensões -> Recarregar, depois abra a página pela barra lateral (Extensões, ou a entrada própria, caso esteja fixada). Os erros aparecem na linha da extensão.
💡 Travou? Abra uma sessão no topic system › help e pergunte com suas palavras: o Claude lê esta documentação no seu servidor e responde.✏️ Melhorar esta página