preferences: Preferência pessoal persistente de densidade
Salve uma preferência pessoal compacta/confortável com revisão compare-and-swap e mostre conflitos entre abas.
O que você vai construir
Salve uma preferência pessoal compacta/confortável com revisão compare-and-swap e mostre conflitos entre abas.
Superfície: preferences · Modo: augment · Gravewright 0.3.0 / SDK 1.x.
Quando monta e quem pode usar
Abra as preferências da mesa, escolha uma densidade e clique em Salvar. storage.user pertence a módulo, mesa e usuário autenticado. Não exige mestre e sobrevive à desativação. A opção controla apenas o painel deste exemplo.
O dono instala o ZIP local em Módulos → Instalar módulo → Instalar por ZIP. Depois, o mestre ativa a versão na lista de extensões da mesa. O exemplo não pede permissões adicionais nem obtém um handle global; todas as leituras usam o api fornecido pela montagem. Mestre e jogador recebem somente os dados autorizados pelo servidor. Streamers não recebem esta montagem autenticada.
Baixe o pacote completo
Baixar ZIP dos fontes · manifest.json · main.mjs · styles.css · LICENSE.txt
Salve os quatro arquivos na mesma pasta. Compacte o conteúdo da pasta, com manifest.json diretamente na raiz do ZIP. Não inclua uma pasta extra, estado local ou chaves privadas. O upload local é uma decisão de confiança do dono; para distribuição por catálogo, use o fluxo de assinatura da referência de módulos.
preferences/
manifest.json
main.mjs
styles.css
LICENSE.txtmanifest.json
{
"id": "example.surface-preferences",
"name": "Persistent personal density preference",
"version": "1.0.0",
"sdk": {
"requires": ">=1.0.0 <2.0.0",
"tested": "1.0.0"
},
"description": "Save a personal compact/comfortable preference with compare-and-swap revisions, and make a concurrent conflict visible.",
"author": "Gravewright documentation",
"license": "MIT",
"entry": "main.mjs"
}main.mjs
// Original documentation example. License: MIT. Target: Gravewright 0.3.0, SDK 1.x.
export default {
start({ api, styles }) {
styles.use("styles.css");
api.i18n.register({
"en": {
"Density": "Density",
"Comfortable": "Comfortable",
"Compact": "Compact",
"Save": "Save",
"Saved": "Saved",
"Conflict: latest value reloaded. Choose again and save.": "Conflict: latest value reloaded. Choose again and save.",
"Refresh": "Refresh",
"Loading…": "Loading…",
"Error: {code}": "Error: {code}",
"Persistent personal density preference": "Persistent personal density preference"
},
"pt-BR": {
"Density": "Densidade",
"Comfortable": "Confortável",
"Compact": "Compacta",
"Save": "Salvar",
"Saved": "Salvo",
"Conflict: latest value reloaded. Choose again and save.": "Conflito: valor recente recarregado. Escolha novamente e salve.",
"Refresh": "Atualizar",
"Loading…": "Carregando…",
"Error: {code}": "Erro: {code}",
"Persistent personal density preference": "Preferência pessoal persistente de densidade"
}
});
},
register({ register }) {
register("preferences", { mode: "augment", mount: this.mount });
},
async mount({ root, context, api, storage, viewport, events, signal, onDispose }) {
const el = (tag, text) => {
const node = root.ownerDocument.createElement(tag);
if (text !== undefined) node.textContent = text;
return node;
};
const t = (key, params) => api.i18n.t(key, params);
const panel = el("section"); panel.className = "gw-example";
const heading = el("h3", t("Persistent personal density preference"));
const output = el("pre"); output.setAttribute("role", "status");
panel.append(heading, output); root.append(panel);
onDispose(() => panel.remove());
const report = error => {
if (!signal.aborted) output.textContent = t("Error: {code}", { code: error.code ?? "unavailable" });
};
const label = el("label", t("Density"));
const select = el("select");
for (const [value, title] of [["comfortable", "Comfortable"], ["compact", "Compact"]]) {
const option = el("option", t(title)); option.value = value; select.append(option);
}
label.append(select);
const save = el("button", t("Save")); save.type = "button";
save.disabled = true;
panel.append(label, save);
let previous = null, ready = false;
const apply = record => {
if (record && (!record.value || record.value.version !== 1 || !["comfortable", "compact"].includes(record.value.density))) {
throw Object.assign(new Error("Unsupported preference data"), { code: "invalid_data" });
}
previous = record;
const density = record?.value.density ?? "comfortable";
select.value = density;
panel.classList.toggle("gw-example--compact", density === "compact");
};
try { const record = await storage.user.get("density"); if (!signal.aborted) { apply(record); ready = true; save.disabled = false; } }
catch (error) { report(error); }
save.addEventListener("click", async () => {
save.disabled = true;
try {
const saved = await storage.user.set("density", { version: 1, density: select.value }, { expectedRevision: previous?.revision ?? null });
if (!signal.aborted) { apply(saved); output.textContent = t("Saved"); }
} catch (error) {
if (error.code === "conflict") {
try { const latest = await storage.user.get("density"); if (!signal.aborted) { apply(latest); output.textContent = t("Conflict: latest value reloaded. Choose again and save."); } }
catch (refreshError) { ready = false; report(refreshError); return; }
} else report(error);
} finally { if (!signal.aborted) save.disabled = !ready; }
}, { signal });
},
stop() {
// Mount and module lifetimes already dispose styles, catalogs and subscriptions.
}
};styles.css
.gw-example { box-sizing: border-box; max-width: 640px; padding: var(--space-3); margin: var(--space-2); border: 1px solid var(--border); border-radius: var(--radius-md); color: var(--text); background: var(--surface); font: 14px/1.5 system-ui, sans-serif; }
.gw-example h3 { font-size: 16px; margin: var(--space-0) var(--space-0) var(--space-2); }
.gw-example pre { white-space: pre-wrap; overflow-wrap: anywhere; font: inherit; }
.gw-example button, .gw-example select, .gw-example input { color: inherit; background: var(--surface); font: inherit; padding: var(--space-1) var(--space-2); margin: var(--space-1); border: 1px solid var(--border); border-radius: var(--radius-sm); }
.gw-example button:focus-visible, .gw-example input:focus-visible, .gw-example select:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.gw-example--compact { padding: var(--space-1); }
.gw-example-center { position: absolute; transform: translate(-50%, -50%); pointer-events: none; color: var(--text); background: var(--surface); padding: var(--space-1); border-radius: var(--radius-sm); font: 14px/1.5 system-ui, sans-serif; }Resultado esperado e liberação
Salve uma preferência pessoal compacta/confortável com revisão compare-and-swap e mostre conflitos entre abas.
A interface nativa continua funcionando. Fechar a ficha, trocar a cena, sair da mesa ou desativar o módulo aborta a montagem. Os listeners usam signal; os eventos e elementos próprios são liberados por onDispose. A leitura assíncrona verifica o aborto e descarta respostas antigas antes de desenhar. Erros autorizados aparecem por error.code, sem esconder falhas de rede.
A gravação usa a revisão lida por get. Em conflict, o exemplo recarrega o valor do servidor e não repete a escrita automaticamente. A preferência tem formato próprio versionado {version: 1, density} e altera somente o painel do exemplo, sem prometer controlar a densidade do host.
Valide em uma mesa descartável
Salve, feche e reabra. Em duas abas, salve na primeira e depois na segunda desatualizada. A segunda deve recarregar o valor mais recente e exigir novo Salvar explícito.
Abra/feche três vezes e desative durante uma leitura ou modal pendente. Confira que nenhum painel ou erro antigo reaparece após o encerramento.
Teste estado vazio, nomes longos, teclado e janela estreita. Compare contas de mestre e jogador.
Confira o console. Estes exemplos têm testes do contrato de runtime; instalação e aparência ainda precisam ser validadas no host alvo.
Como usar a IA para esta superfície
Adapte o exemplo preferences para [objetivo concreto] no Gravewright 0.3.0.
Leia registry.json, o schema de contexto desta superfície, frontend.json e os serviços nativos usados.
Preserve start/register/stop, api da montagem, signal, onDispose e os modais nativos.
Mostre manifesto e arquivos completos. Não use app.shell/inside.content em pacotes.
Não invente campos de payload, regras de RPG no core, permissões ou APIs.
Teste jogador/mestre, dados vazios, aborto, montagem repetida e o comportamento específico do exemplo.
Explique limitações e o que foi realmente executado.Continue
SDK · API · Exemplos por kind