src/handbook/presentation.js
Rama main · réplica de lectura del repositorio original.
const escape = value => String(value).replace(/[&<>"']/g, char => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[char]));
const banners = {
handbook: new URL('./banners/banner-contour.svg', import.meta.url),
showcase: new URL('./banners/banner-blue.svg', import.meta.url),
development: new URL('./banners/banner-band.svg', import.meta.url),
quality: new URL('./banners/banner-light.svg', import.meta.url),
wiki: new URL('./banners/banner-topo.svg', import.meta.url),
};
const link = (label, href, detail = '') => href ? `<a href="${escape(href)}" target="_blank" rel="noopener"><strong>${escape(label)} <span aria-hidden="true">↗</span></strong>${detail ? `<span>${escape(detail)}</span>` : ''}</a>` : '';
const panel = (title, text, links, extra = '') => `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><style>
:root{color-scheme:light dark;font-family:system-ui,sans-serif;color:light-dark(#202630,#e9edf4);background:light-dark(#fff,#181b21)}*{box-sizing:border-box}body{margin:0;padding:4px 0 12px}h2{font-size:clamp(21px,4vw,28px);line-height:1.2;letter-spacing:-.025em;margin:0 0 8px;font-weight:650}p{line-height:1.6;margin:0 0 16px;max-width:72ch;color:light-dark(#526071,#b4bfce)}nav{display:flex;flex-wrap:wrap;gap:8px}a{color:inherit;text-decoration:none;display:block;padding:11px 14px;border:1px solid light-dark(#d9dee6,#353d48);border-radius:8px;background:light-dark(#f7f9fc,#202630);flex:1 1 165px}a:hover{border-color:#5288ef;background:light-dark(#edf3ff,#243146)}a:focus-visible,summary:focus-visible{outline:3px solid #5288ef;outline-offset:3px}a strong{font-size:14px;font-weight:600}a span{display:block;font-size:12px;line-height:1.5;margin-top:4px;color:light-dark(#526071,#b4bfce)}a strong span{display:inline;color:#5288ef}details{border-top:1px solid light-dark(#d9dee6,#353d48);padding:12px 0}summary{cursor:pointer;font-size:14px;font-weight:600}details p{font-size:14px;margin:10px 0}code{overflow-wrap:anywhere;font-size:12px}ol{padding-left:22px;font-size:14px;line-height:1.7}small{display:block;margin:12px 0;color:light-dark(#526071,#b4bfce)}@media(max-width:440px){a{flex-basis:45%;padding:10px}h2{font-size:22px}}
</style></head><body><h2>${escape(title)}</h2><p>${escape(text)}</p><nav aria-label="Explore this topic">${links.join('')}</nav>${extra}</body></html>`;
export function buildPresentation(w) {
const table = name => { const t = w.findTable(name); return t && !t.deletedAt ? `/w/weave/#/table/${t.id}` : ''; };
const space = name => { const s = w.listSpaces().find(x => x.name === name); return s ? `/w/weave/#/space/${s.id ?? s.spaceId}` : ''; };
const record = (name, title) => { const t = w.findTable(name); const r = t && w.findEntity(t, title); return r && !r.deletedAt ? `/w/weave/e/${r.id}` : ''; };
const guide = name => record('Handbook/Guide', name);
const go = (name, text) => link(name, guide(name), text);
const publicWork = [link('Issues', table('Development/Issue'), 'Report, follow, and investigate bugs.'), link('Roadmap', table('Development/Feature'), 'See what is proposed and being built.'), link('Release notes', table('Development/Release'), 'Read the changes in each release.'), link('Test suites', table('Quality/Suite'), 'Explore the public test documentation.'), link('Test cases', table('Quality/Case'), 'Follow the individual checks.')];
const examples = [link('Projects', table('Showcase/Projects'), 'Follow tasks back to their project.'), link('Notes', table('Showcase/Notes'), 'One relation can point to different tables.'), link('Expenses', table('Showcase/Expenses'), 'Read a total computed from line items.'), link('Field types', table('Showcase/Field Types'), 'Compare fields using real records.')];
const templateLinks = [link('CRM template', space('CRM'), 'Open the space, then choose Use template.'), link('Money template', space('Money'), 'Copy its structure into your workspace.')];
const start = panel('Explore weave by using weave.', 'Read a guide, follow a working example, or inspect the public work behind the product.', [go('Quickstart', 'Find your way around.'), go('A project and its linked work', 'Follow a connected example.'), go('Connect an agent', 'Set up access to your workspace.'), ...publicWork]);
const showcase = panel('Follow the connections.', 'These are working tables. Open a record, follow a relation, then read how the field is configured.', [...examples, go('A project and its linked work', 'A guided route through the examples.'), ...templateLinks]);
const development = panel('Built in public.', 'Issues, the roadmap, and release notes connect requests to the work that reaches users.', publicWork.slice(0, 3));
const quality = panel('Read the checks behind the product.', 'Suites group the tests by behavior. Cases describe the individual checks. Both remain part of the public workspace.', publicWork.slice(3));
const wiki = panel('Understand the design.', 'Read the architecture and the decisions behind the data model, then return to a working example.', [link('Articles', table('Wiki/Article')), go('A project and its linked work'), link('Showcase', space('Showcase'))]);
const setup = `<details open><summary>Browser-based agent</summary><ol><li>Open your own workspace’s agent connection guide.</li><li>Copy its MCP address into your client’s connector setup.</li><li>Sign in when the client opens weave. Your workspace role determines access.</li></ol></details><details><summary>Claude Code</summary><p>Use the command in the native connection guide for your workspace. It adds the workspace’s HTTP MCP server; sign in when prompted.</p></details><details><summary>Codex</summary><p>Add your workspace’s MCP URL with <code>codex mcp add weave --url <workspace-mcp-url></code>, then run <code>codex mcp login weave</code>.</p></details><details><summary>Check the connection</summary><p>Ask: “Read the workspace outline and describe its spaces and tables. Do not change anything.” Confirm it names your workspace before asking it to edit records.</p></details>`;
const projectUrl = record('Showcase/Projects', 'Apollo Launch');
return {
spaces: [
{ name: 'Handbook', previousDescription: "**Official documentation and how-tos** — the pages you would hand to someone using Weave for the first time.\n\nPrescriptive rather than exploratory: how to install it, how the data model works, what each field type does. Design rationale and open questions belong in **Wiki** instead.", description: 'Guides to using weave: build a workspace, connect an agent, and learn from working examples.', bannerPath: banners.handbook, html: start },
{ name: 'Showcase', previousDescription: "What a field can be, and what a document can hold. `Field Types` puts every field type and several configurations of the same type side by side in one grid; `Formatting` gives every document construct a row written in the construct it names.", description: 'Working examples of relations, workflows, computed fields, documents, and templates. Open the records to see how they connect.', bannerPath: banners.showcase, html: showcase },
{ name: 'Development', bannerPath: banners.development, html: development },
{ name: 'Quality', bannerPath: banners.quality, html: quality },
{ name: 'Wiki', previousDescription: "**Design notes and architecture** — why Weave is shaped the way it is.\n\nWhere the Handbook says *how*, the Wiki says *why*: the trade-offs behind the storage layer, the reasoning behind the API surface, notes that are still being argued with. Content graduates to a Handbook guide once it stops changing.", description: 'Architecture, data-model decisions, and the reasoning behind weave.', bannerPath: banners.wiki, html: wiki },
],
tables: [
{ name: 'Handbook/Guide', previousDescription: "**Handbook pages** — the prescriptive documentation set.\n\n| Field | Purpose |\n| --- | --- |\n| `Description` | The page body, in markdown |\n| `Audience` | Who it is written for |\n| `Order` | Reading sequence within the Handbook |\n\nA guide should be safe to follow verbatim. If something is still being decided, it belongs in a Wiki article until it settles.", description: "Practical guides for building a workspace, using linked records, and connecting an agent.", bannerPath: banners.handbook, html: start },
{ name: 'Handbook/Fields', previousDescription: "**One page per field type** — the living reference for every field weave can make.\n\nThe storage primitives (`text`, `number`, `date`, `daterange`, `checkbox`, `url`, `email`), the choosers (`select`, `multiselect`, `workflow`), `relation` (always a bidirectional pair), the computed types (`lookup`, `rollup`, `formula`), `document`, and the three the meta-model earned: `field` (a value that IS a field definition), `key` (the name of a secret, never the secret) and `attachments`.\n\n`Kind` groups them. Each page names every config key the vocabulary lists for its type, the closed set each key draws from, and what bites. `test/handbook.test.mjs` fails if a type ships without a page, or a page quotes a vocabulary the engine no longer has.", description: "Every field type, with its configuration, examples, and limitations.", bannerPath: banners.quality, html: panel('Choose a field. See what it does.', 'Each field reference covers its configuration, rendering, and limitations.', [link('Working field examples', table('Showcase/Field Types')), go('A project and its linked work')]) },
{ name: 'Wiki/Article', previousDescription: "**Wiki articles** — design notes, architecture write-ups and postmortems.\n\n`Topic` groups them. Unlike Handbook guides these are allowed to be provisional: an article can argue a position, record a decision that was later reversed, or capture an investigation that went nowhere. That history is the point.", description: "Architecture, design decisions, and investigations. Articles preserve the reasoning and its history.", bannerPath: banners.wiki, html: wiki },
{ name: 'Development/Release', bannerPath: banners.development, html: panel('What changed, and how to try it.', 'Open a release for its changes and linked work. A merged pull request alone does not confirm a production deployment.', [go('Reading a release', 'Follow a change from evidence to example.'), ...publicWork.slice(0, 2)]) },
{ name: 'Showcase/Projects', bannerPath: banners.showcase, html: panel('A project holds the context.', 'Follow its tasks, then open a related note in the Notes table.', [go('A project and its linked work'), link('Tasks', table('Showcase/Tasks')), link('Notes', table('Showcase/Notes'))]) },
],
releases: [{ name: 'v0.4.128', bannerPath: banners.development, html: panel('Workflows you can inspect. Schemas you can copy.', 'This release records changes to workflow health, schema round-trips, and document loading. Read the original notes below for fixes, limits, and test evidence.', [go('Automations', 'Read how rules, actions, and health work.'), go('Document formatting', 'Try the editor with a maintained guide.'), go('Reading a release', 'Understand the evidence behind a release.')]) }],
guides: [
{ name: 'Quickstart', bannerPath: banners.handbook, html: panel('Start with one example.', 'Open the guided project, then choose whether to build a workspace yourself or connect an agent.', [go('A project and its linked work'), go('Connect an agent'), ...templateLinks]) },
{ name: 'A project and its linked work', bannerPath: banners.showcase, html: panel('One project, several views of the work.', 'Follow a project to its tasks and notes. A separate expense example shows how a rollup totals linked line items.', [link('Apollo Launch', projectUrl), ...examples.slice(0, 3)]) },
{ name: 'Connect an agent', bannerPath: banners.handbook, html: panel('Give your agent a workspace.', 'Use your own workspace’s MCP address. Sign-in and permissions are handled by weave.', [link('Connection instructions', '/docs'), go('Agent access (MCP)', 'Tool reference and access roles.')], setup) },
{ name: 'Reading a release', bannerPath: banners.development, html: panel('Read a change in context.', 'Start with what changed for a user, then follow its issue, tests, and working example.', publicWork) },
{ name: 'Templates', bannerPath: banners.showcase, html: panel('Start with a structure you can change.', 'Use template copies the space’s schema into another workspace. Records and attachments stay in the source.', templateLinks) },
],
};
}