@book.dev/sdk 1.76.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/backup.d.ts +1 -1
- package/dist/bookFolder.d.ts +38 -21
- package/dist/bookFolder.js +42 -18
- package/dist/bookFolder.js.map +1 -1
- package/dist/bookfile.d.ts +1 -1
- package/dist/client.d.ts +4 -4
- package/dist/client.js +4 -4
- package/dist/client.js.map +1 -1
- package/dist/database.d.ts +9 -3
- package/dist/database.js +30 -11
- package/dist/database.js.map +1 -1
- package/dist/forwarding/forwardingClient.d.ts +38 -0
- package/dist/forwarding/forwardingClient.js +57 -4
- package/dist/forwarding/forwardingClient.js.map +1 -1
- package/dist/forwarding/index.d.ts +2 -2
- package/dist/forwarding/index.js +2 -2
- package/dist/forwarding/index.js.map +1 -1
- package/dist/forwarding/rosterAssertion.d.ts +61 -12
- package/dist/forwarding/rosterAssertion.js +77 -26
- package/dist/forwarding/rosterAssertion.js.map +1 -1
- package/dist/import.d.ts +3 -3
- package/dist/import.js +3 -3
- package/dist/import.js.map +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/provenance.d.ts +38 -27
- package/dist/provenance.js.map +1 -1
- package/dist/routes.d.ts +9 -3
- package/dist/routes.js +10 -4
- package/dist/routes.js.map +1 -1
- package/dist/templates.d.ts +35 -8
- package/dist/templates.js +307 -17
- package/dist/templates.js.map +1 -1
- package/package.json +1 -1
package/dist/templates.d.ts
CHANGED
|
@@ -5,32 +5,59 @@ import type { StoredPage } from './types';
|
|
|
5
5
|
* through the normal data APIs (no server involvement, same as the sample
|
|
6
6
|
* document). Two shapes:
|
|
7
7
|
*
|
|
8
|
-
* - **Block-doc artifacts** (the
|
|
8
|
+
* - **Block-doc artifacts** (the showcases) ship a native block-editor
|
|
9
9
|
* JSON projection in `blockdoc: {blocks}`. They lean on the whole editor:
|
|
10
10
|
* reactive inputs feeding *collapsed* live-code, status lights, info/link/
|
|
11
11
|
* tooltip cards, charts, progress bars, multi-column layouts, callouts, and
|
|
12
12
|
* `divider`/`notes` blocks so every page doubles as a slide deck with
|
|
13
13
|
* speaker notes (see blockeditor/present.ts).
|
|
14
|
-
* - **Databases** (roadmap, field map) ship a
|
|
15
|
-
*
|
|
14
|
+
* - **Databases** (task board, reading list, roadmap, field map) ship a
|
|
15
|
+
* schema, views, and sample rows; roadmap and field map back the swimlane
|
|
16
|
+
* and map e2e fixtures.
|
|
16
17
|
*
|
|
17
18
|
* Ids are stable so the gallery, its i18n keys, and the e2e suite can reference
|
|
18
19
|
* a template without depending on display strings.
|
|
19
20
|
*/
|
|
21
|
+
/**
|
|
22
|
+
* What a template demonstrates, surfaced as gallery chips so the reader knows
|
|
23
|
+
* what a card is before inserting it:
|
|
24
|
+
* - `interactive` — reactive inputs feeding live code, charts, status lights.
|
|
25
|
+
* - `slides` — divider-cut slides with speaker notes (present-mode ready).
|
|
26
|
+
* - `database` — a schema with views and sample rows.
|
|
27
|
+
*/
|
|
28
|
+
export type TemplateTag = 'interactive' | 'slides' | 'database';
|
|
20
29
|
export interface PageTemplate {
|
|
21
30
|
/** Stable identifier (i18n keys + tests hang off this). */
|
|
22
|
-
id: 'grocery-tracker' | 'task-board' | 'reading-list' | 'project-intake' | 'savings-planner' | 'roadmap' | 'field-map';
|
|
31
|
+
id: 'grocery-tracker' | 'task-board' | 'reading-list' | 'project-intake' | 'savings-planner' | 'roadmap' | 'field-map' | 'pitch-deck' | 'compound-growth' | 'team-status' | 'product-hq';
|
|
23
32
|
/** Emoji shown on the gallery card and applied to the created page. */
|
|
24
33
|
icon: string;
|
|
25
34
|
/** Canonical (English) page name; suffixed when it collides. */
|
|
26
35
|
pageName: string;
|
|
27
|
-
/**
|
|
28
|
-
|
|
36
|
+
/** What the template shows off — rendered as chips on the gallery card. */
|
|
37
|
+
tags: TemplateTag[];
|
|
38
|
+
/**
|
|
39
|
+
* Canonical (English) text of the leading "how to use this" callout, for
|
|
40
|
+
* templates that don't already open with strong in-doc guidance (the five
|
|
41
|
+
* database fixtures). The gallery passes a localized override through
|
|
42
|
+
* {@link instantiateTemplate}; `create` bakes this English text in by
|
|
43
|
+
* default. Templates whose documents already guide (grocery, pitch deck,
|
|
44
|
+
* team status, and the sample-document copy — which opens with its own
|
|
45
|
+
* intro paragraph) leave it unset.
|
|
46
|
+
*/
|
|
47
|
+
guidance?: string;
|
|
48
|
+
/** Creates the page (and database, if any) and returns the stored page.
|
|
49
|
+
* `guidance` overrides the template's canonical guidance-callout text
|
|
50
|
+
* (ignored by templates that define none). */
|
|
51
|
+
create: (client: DataClient, name: string, guidance?: string) => Promise<StoredPage>;
|
|
29
52
|
}
|
|
30
53
|
export declare const PAGE_TEMPLATES: PageTemplate[];
|
|
31
54
|
/**
|
|
32
55
|
* Instantiate a template: pick a distinct display name (courtesy numbering —
|
|
33
56
|
* duplicates are allowed but unhelpful for ready-made pages) and build the page
|
|
34
|
-
* through the client, retrying transient failures.
|
|
57
|
+
* through the client, retrying transient failures. `opts.guidance` localizes
|
|
58
|
+
* the leading guidance callout of templates that carry one (the gallery passes
|
|
59
|
+
* the user's locale text; absent, the canonical English default applies).
|
|
35
60
|
*/
|
|
36
|
-
export declare function instantiateTemplate(client: DataClient, template: PageTemplate
|
|
61
|
+
export declare function instantiateTemplate(client: DataClient, template: PageTemplate, opts?: {
|
|
62
|
+
guidance?: string;
|
|
63
|
+
}): Promise<StoredPage>;
|
package/dist/templates.js
CHANGED
|
@@ -1,8 +1,36 @@
|
|
|
1
|
+
import { TITLE_PROPERTY_ID } from './database';
|
|
2
|
+
import { buildSampleDocument } from './sampleDocument';
|
|
1
3
|
const emptySnapshot = (blocks) => ({
|
|
2
4
|
editorjs: { blocks },
|
|
3
5
|
values: [],
|
|
4
6
|
names: [],
|
|
5
7
|
});
|
|
8
|
+
// ── The standardized "how to use this" guidance callout ─────────────────────
|
|
9
|
+
//
|
|
10
|
+
// The five database fixtures — which don't open with strong in-doc guidance of
|
|
11
|
+
// their own — lead with one consistent `info` callout: what the template
|
|
12
|
+
// demonstrates, then how to try it. The English text below is the canonical
|
|
13
|
+
// default; the gallery passes a localized override at instantiation (the ui
|
|
14
|
+
// package's `templates.<id>.guidance` i18n keys mirror these strings).
|
|
15
|
+
const GUIDANCE = {
|
|
16
|
+
taskBoard: 'This template shows a task database: the Status property drives the kanban columns, and the same rows back the Table and Calendar views. Try it: drag a card to another column, switch views, or right-click the view to export CSV.',
|
|
17
|
+
readingList: 'Each shelf is a gallery group, and the same books also list in a Table view. Try it: rate a book, move one to another shelf, or add your own.',
|
|
18
|
+
roadmap: 'This template shows swimlanes: the Timeline bands and the Board lanes both split by Area. Try it: drag a bar to reschedule, collapse a lane, or move a card between stages.',
|
|
19
|
+
fieldMap: 'This template shows a map database: rows with a location render as region-coloured pins, and the address-only row waits under Unplaced. Try it: click a pin, geocode the unplaced row, or switch to the Table view.',
|
|
20
|
+
productHq: 'This template shows two linked databases: each initiative relates to tasks on the Tasks sub-page, and the Progress and Task count columns roll those tasks up. Try it: tick a task done on the sub-page and watch the rollup move, or open the Tasks timeline for the dependency arrows.',
|
|
21
|
+
};
|
|
22
|
+
/** The guidance callout as a block-doc block (ids are stable per template). */
|
|
23
|
+
const guidanceCallout = (id, text) => ({
|
|
24
|
+
id,
|
|
25
|
+
type: 'callout',
|
|
26
|
+
text: [{ t: text }],
|
|
27
|
+
props: { variant: 'info' },
|
|
28
|
+
});
|
|
29
|
+
/** A host-page snapshot: empty, or a single leading guidance callout rendered
|
|
30
|
+
* (block-doc) above the hosted database view. */
|
|
31
|
+
const guidanceSnapshot = (guide) => guide
|
|
32
|
+
? { editorjs: { blocks: [] }, values: [], names: [], editor: 'blocks', blockdoc: { blocks: [guidanceCallout(guide.id, guide.text)] } }
|
|
33
|
+
: emptySnapshot([]);
|
|
6
34
|
/** A local `YYYY-MM-DD` day string offset by `days` from today. */
|
|
7
35
|
const day = (days) => {
|
|
8
36
|
const d = new Date();
|
|
@@ -304,6 +332,151 @@ const SAVINGS_BLOCKS = [
|
|
|
304
332
|
{ id: 's-link', type: 'linkcard', props: { title: 'How compound interest works', description: 'A plain-English primer.', url: 'https://www.investor.gov/financial-tools-calculators/calculators/compound-interest-calculator' } },
|
|
305
333
|
{ id: 's-notes-4', type: 'notes', text: [{ t: 'One ask to close on: automate the monthly contribution so the plan happens without willpower.' }] },
|
|
306
334
|
];
|
|
335
|
+
// ── 📽️ Pitch deck ────────────────────────────────────────────────────────────
|
|
336
|
+
// A present-first showcase: five slides (title · agenda · a live revenue-mix
|
|
337
|
+
// donut · a quote · the ask), each with a speaker `notes` block, so the ⋯ →
|
|
338
|
+
// Present flow lands on a real deck. The donut slide is the interactive core:
|
|
339
|
+
// three sliders feed the chart and a recurring-revenue status light.
|
|
340
|
+
const PITCH_DECK_BLOCKS = [
|
|
341
|
+
// Slide 1 — title
|
|
342
|
+
{ id: 'pd-h1', type: 'heading', text: [{ t: 'Brightloop' }], props: { level: 1 } },
|
|
343
|
+
{ id: 'pd-tag', type: 'paragraph', text: [{ t: 'The pitch as a ' }, { t: 'live document', a: { b: true } }, { t: ' — the numbers on slide 3 recompute while you talk.' }] },
|
|
344
|
+
{ id: 'pd-call', type: 'callout', text: [{ t: 'Open the ⋯ menu → Present to run this as a deck.' }], props: { variant: 'info' } },
|
|
345
|
+
{ id: 'pd-notes-1', type: 'notes', text: [{ t: 'Thirty seconds, tops: who you are, what Brightloop is, why the room should care. Land the tagline, then advance — the deck itself makes the “live document” point on slide 3.' }] },
|
|
346
|
+
{ id: 'pd-div-1', type: 'divider' },
|
|
347
|
+
// Slide 2 — agenda
|
|
348
|
+
{ id: 'pd-h2', type: 'heading', text: [{ t: 'Agenda' }], props: { level: 2 } },
|
|
349
|
+
{ id: 'pd-ag1', type: 'list', text: [{ t: 'The problem — decks go stale the moment they’re exported.' }], props: { kind: 'number' } },
|
|
350
|
+
{ id: 'pd-ag2', type: 'list', text: [{ t: 'The product — one page that is both the model and the deck.' }], props: { kind: 'number' } },
|
|
351
|
+
{ id: 'pd-ag3', type: 'list', text: [{ t: 'Revenue mix — live, draggable, no screenshots.' }], props: { kind: 'number' } },
|
|
352
|
+
{ id: 'pd-ag4', type: 'list', text: [{ t: 'What early users say.' }], props: { kind: 'number' } },
|
|
353
|
+
{ id: 'pd-ag5', type: 'list', text: [{ t: 'The ask.' }], props: { kind: 'number' } },
|
|
354
|
+
{ id: 'pd-notes-2', type: 'notes', text: [{ t: 'Signpost, don’t read: five stops, four minutes. Flag that slide 3 is interactive so nobody mistakes the demo for a rehearsed video.' }] },
|
|
355
|
+
{ id: 'pd-div-2', type: 'divider' },
|
|
356
|
+
// Slide 3 — the live donut
|
|
357
|
+
{ id: 'pd-h3', type: 'heading', text: [{ t: 'Revenue mix — live' }], props: { level: 2 } },
|
|
358
|
+
{ id: 'pd-recurring', type: 'code', text: [{ t: 'Math.round(subs / (subs + services + partners) * 100)' }], props: { live: true, name: 'recurring', language: 'js', collapsed: true } },
|
|
359
|
+
{
|
|
360
|
+
id: 'pd-cols',
|
|
361
|
+
type: 'columns',
|
|
362
|
+
children: [
|
|
363
|
+
{
|
|
364
|
+
id: 'pd-col-l',
|
|
365
|
+
type: 'column',
|
|
366
|
+
props: { span: 5 },
|
|
367
|
+
children: [
|
|
368
|
+
{ id: 'pd-subs', type: 'slider', props: { name: 'subs', label: 'Subscriptions (£k/yr)', value: 62, min: 0, max: 200 } },
|
|
369
|
+
{ id: 'pd-services', type: 'slider', props: { name: 'services', label: 'Services (£k/yr)', value: 26, min: 0, max: 200 } },
|
|
370
|
+
{ id: 'pd-partners', type: 'slider', props: { name: 'partners', label: 'Partnerships (£k/yr)', value: 12, min: 0, max: 200 } },
|
|
371
|
+
],
|
|
372
|
+
},
|
|
373
|
+
{
|
|
374
|
+
id: 'pd-col-r',
|
|
375
|
+
type: 'column',
|
|
376
|
+
props: { span: 7 },
|
|
377
|
+
children: [
|
|
378
|
+
{ id: 'pd-donut', type: 'kitchart', props: { kind: 'donut', title: 'Revenue by stream (£k/yr)', labels: 'Subscriptions, Services, Partnerships', source: '[subs, services, partners]' } },
|
|
379
|
+
{ id: 'pd-status', type: 'statuslight', props: { label: 'Recurring revenue ≥ 60%', source: 'recurring', okAt: 60, warnAt: 45 } },
|
|
380
|
+
],
|
|
381
|
+
},
|
|
382
|
+
],
|
|
383
|
+
},
|
|
384
|
+
{ id: 'pd-notes-3', type: 'notes', text: [{ t: 'The money moment: drag Services up until the light drops to amber, then pull Subscriptions back up until recurring clears 60% and watch it recover. The maths is a one-line code block above the columns — open it if anyone asks.' }] },
|
|
385
|
+
{ id: 'pd-div-3', type: 'divider' },
|
|
386
|
+
// Slide 4 — the quote
|
|
387
|
+
{ id: 'pd-h4', type: 'heading', text: [{ t: 'What early users say' }], props: { level: 2 } },
|
|
388
|
+
{ id: 'pd-quote', type: 'quote', text: [{ t: 'We pitched with the model itself — when the room asked “what if churn doubles?”, we dragged a slider instead of promising a follow-up.' }] },
|
|
389
|
+
{ id: 'pd-notes-4', type: 'notes', text: [{ t: 'Pause after reading the quote — let it sit. If pressed for attribution, it’s a composite of three design-partner calls; offer intros rather than names.' }] },
|
|
390
|
+
{ id: 'pd-div-4', type: 'divider' },
|
|
391
|
+
// Slide 5 — the ask
|
|
392
|
+
{ id: 'pd-h5', type: 'heading', text: [{ t: 'The ask' }], props: { level: 2 } },
|
|
393
|
+
{ id: 'pd-ask', type: 'paragraph', text: [{ t: 'We’re raising ' }, { t: '£1.2M', a: { b: true } }, { t: ' to take Brightloop from private beta to launch: two engineers, one designer, and twelve months of runway.' }] },
|
|
394
|
+
{ id: 'pd-call2', type: 'callout', text: [{ t: 'Make it yours: duplicate this page, swap in your numbers, and pitch with live charts instead of screenshots.' }], props: { variant: 'success' } },
|
|
395
|
+
{ id: 'pd-notes-5', type: 'notes', text: [{ t: 'Close with the concrete next step: a 30-minute working session in the live model this week. Stop talking after the ask.' }] },
|
|
396
|
+
];
|
|
397
|
+
// ── 🚦 Team status dashboard ─────────────────────────────────────────────────
|
|
398
|
+
// The kit-breadth showcase, as a single-page dashboard (no slides): a **locked
|
|
399
|
+
// group** whose controls stay live for readers (toggle, dropdown, a kudos
|
|
400
|
+
// counter driven by an action button, a formula and a status light reading it),
|
|
401
|
+
// a **funnel** chart (a kind no other template uses), a **tabs** container, and
|
|
402
|
+
// a cross-page **sync** key — the same Pulse group pasted on another page stays
|
|
403
|
+
// in lockstep under `team-pulse`.
|
|
404
|
+
const TEAM_STATUS_BLOCKS = [
|
|
405
|
+
{ id: 'td-tag', type: 'paragraph', text: [{ t: 'One page the whole team reads: a ' }, { t: 'locked', a: { b: true } }, { t: ' Pulse panel whose controls stay live, a delivery funnel, and the week’s rituals in tabs.' }] },
|
|
406
|
+
{ id: 'td-call', type: 'callout', text: [{ t: 'The Pulse group is locked (the 🔒 in its header): its text and layout are frozen, but readers keep every control. It also syncs across pages under the sync key “team-pulse” — paste the same group on another page and the two stay in lockstep.' }], props: { variant: 'info' } },
|
|
407
|
+
// The locked, synced control panel.
|
|
408
|
+
{ id: 'td-h2', type: 'heading', text: [{ t: 'Team pulse' }], props: { level: 2 } },
|
|
409
|
+
{
|
|
410
|
+
id: 'td-group',
|
|
411
|
+
type: 'group',
|
|
412
|
+
props: { name: 'Pulse', locked: true, sync: 'team-pulse' },
|
|
413
|
+
children: [
|
|
414
|
+
{ id: 'td-g-note', type: 'paragraph', text: [{ t: 'This panel is locked — this very sentence can’t be edited in place — yet every control below still works.' }] },
|
|
415
|
+
{ id: 'td-oncall', type: 'toggle', props: { name: 'onCall', label: 'On-call rotation active', value: true } },
|
|
416
|
+
{ id: 'td-focus', type: 'dropdown', props: { name: 'focus', label: 'Focus this week', value: 'shipping', opts: [{ label: 'Shipping' }, { label: 'Stability' }, { label: 'Growth' }] } },
|
|
417
|
+
{ id: 'td-kudos', type: 'number', props: { name: 'kudos', label: 'Kudos given', value: 2, min: 0, max: 99, step: 1 } },
|
|
418
|
+
{ id: 'td-give', type: 'actionbutton', props: { btnlabel: 'Give kudos', action: 'increment', target: 'kudos', amount: 1 } },
|
|
419
|
+
// Inputs inside a named group publish namespaced — pulse.kudos.value —
|
|
420
|
+
// which is exactly what this formula (and the light below) read.
|
|
421
|
+
{ id: 'td-score', type: 'formula', props: { name: 'morale', source: 'pulse.kudos.value * 10 + (pulse.onCall.value ? 5 : 0)' } },
|
|
422
|
+
{ id: 'td-light', type: 'statuslight', props: { label: 'Momentum', source: 'pulse.kudos.value', okAt: 3, warnAt: 1 } },
|
|
423
|
+
],
|
|
424
|
+
},
|
|
425
|
+
// The delivery funnel: a chart kind no other template exercises.
|
|
426
|
+
{ id: 'td-h3', type: 'heading', text: [{ t: 'Delivery pipeline' }], props: { level: 2 } },
|
|
427
|
+
{ id: 'td-pipe', type: 'code', text: [{ t: '({Ideas: 24, Building: 12, "In review": 7, Shipped: shipped})' }], props: { live: true, name: 'pipeline', language: 'js', collapsed: true } },
|
|
428
|
+
{
|
|
429
|
+
id: 'td-cols',
|
|
430
|
+
type: 'columns',
|
|
431
|
+
children: [
|
|
432
|
+
{
|
|
433
|
+
id: 'td-col-l',
|
|
434
|
+
type: 'column',
|
|
435
|
+
props: { span: 5 },
|
|
436
|
+
children: [
|
|
437
|
+
{ id: 'td-shipped', type: 'number', props: { name: 'shipped', label: 'Shipped this quarter', value: 5, min: 0, max: 50, step: 1 } },
|
|
438
|
+
{ id: 'td-tip', type: 'tooltipcard', props: { term: 'Funnel', tip: 'Each stage narrows: ideas → building → review → shipped. Step the shipped count and the funnel redraws.' } },
|
|
439
|
+
],
|
|
440
|
+
},
|
|
441
|
+
{
|
|
442
|
+
id: 'td-col-r',
|
|
443
|
+
type: 'column',
|
|
444
|
+
props: { span: 7 },
|
|
445
|
+
children: [
|
|
446
|
+
{ id: 'td-funnel', type: 'kitchart', props: { kind: 'funnel', title: 'Ideas → Shipped', source: 'pipeline' } },
|
|
447
|
+
],
|
|
448
|
+
},
|
|
449
|
+
],
|
|
450
|
+
},
|
|
451
|
+
// The week's rituals, in a tabs container.
|
|
452
|
+
{ id: 'td-h4', type: 'heading', text: [{ t: 'Rituals' }], props: { level: 2 } },
|
|
453
|
+
{
|
|
454
|
+
id: 'td-tabs',
|
|
455
|
+
type: 'tabs',
|
|
456
|
+
props: { name: 'Rituals', active: 0 },
|
|
457
|
+
children: [
|
|
458
|
+
{
|
|
459
|
+
id: 'td-tab-week',
|
|
460
|
+
type: 'tab',
|
|
461
|
+
props: { label: 'This week' },
|
|
462
|
+
children: [
|
|
463
|
+
{ id: 'td-t1', type: 'todo', text: [{ t: 'Monday kick-off — pick the focus in the Pulse panel' }], props: { checked: true } },
|
|
464
|
+
{ id: 'td-t2', type: 'todo', text: [{ t: 'Thursday demo — show, don’t tell' }], props: { checked: false } },
|
|
465
|
+
],
|
|
466
|
+
},
|
|
467
|
+
{
|
|
468
|
+
id: 'td-tab-next',
|
|
469
|
+
type: 'tab',
|
|
470
|
+
props: { label: 'Next week' },
|
|
471
|
+
children: [
|
|
472
|
+
{ id: 'td-n1', type: 'list', text: [{ t: 'Rotate the on-call — flip the Pulse toggle' }], props: { kind: 'bullet' } },
|
|
473
|
+
{ id: 'td-n2', type: 'list', text: [{ t: 'Reset the kudos counter at retro' }], props: { kind: 'bullet' } },
|
|
474
|
+
],
|
|
475
|
+
},
|
|
476
|
+
],
|
|
477
|
+
},
|
|
478
|
+
{ id: 'td-call2', type: 'callout', text: [{ t: 'Make it yours: rename the Pulse group, change its sync key, and unlock it (the 🔓 in the group header) to re-arrange the controls.' }], props: { variant: 'success' } },
|
|
479
|
+
];
|
|
307
480
|
// ════════════════════════════════════════════════════════════════════════════
|
|
308
481
|
// Databases (the task board, reading list, and the swimlane + map e2e fixtures)
|
|
309
482
|
// ════════════════════════════════════════════════════════════════════════════
|
|
@@ -337,9 +510,11 @@ const TASK_BOARD_SCHEMA = {
|
|
|
337
510
|
{ id: 'p_effort', name: 'Effort', type: 'number', numberDisplay: 'bar', numberTarget: 8 },
|
|
338
511
|
],
|
|
339
512
|
views: [
|
|
340
|
-
// Board first → the page opens as a kanban grouped by status; a table backs
|
|
513
|
+
// Board first → the page opens as a kanban grouped by status; a table backs
|
|
514
|
+
// it, and a calendar lays the same tasks out on a month grid by due date.
|
|
341
515
|
{ id: 'v_board', name: 'Board', type: 'board', filters: [], sorts: [], groupByPropertyId: 'p_status' },
|
|
342
516
|
{ id: 'v_table', name: 'Table', type: 'table', filters: [], sorts: [] },
|
|
517
|
+
{ id: 'v_calendar', name: 'Calendar', type: 'calendar', filters: [], sorts: [], datePropertyId: 'p_due' },
|
|
343
518
|
],
|
|
344
519
|
};
|
|
345
520
|
const TASK_BOARD_ROWS = [
|
|
@@ -353,6 +528,19 @@ const TASK_BOARD_ROWS = [
|
|
|
353
528
|
];
|
|
354
529
|
// ── 📚 Reading list ──────────────────────────────────────────────────────────
|
|
355
530
|
// A shelf-grouped gallery of books, with authors and star ratings; a table backs it.
|
|
531
|
+
//
|
|
532
|
+
// Covers (so the gallery renders real cards, not empty slots): tiny (<300 B) raster
|
|
533
|
+
// PNGs, inlined as `data:` URLs on the `files`-typed `p_cover` cells. Deliberately
|
|
534
|
+
// NOT routed through the content-addressed asset store — the `files` property and
|
|
535
|
+
// the gallery cover render a URL string straight into `<img src>` with no
|
|
536
|
+
// asset-resolution seam, so a store `assetId` wouldn't load there, and this seeds
|
|
537
|
+
// identically on both transports (web PGlite + desktop IPC) with no upload call.
|
|
538
|
+
// PNG, never SVG — honouring the store's image allowlist even though nothing is
|
|
539
|
+
// stored (an `<img src="data:image/png…">` executes no script). One per shelf so
|
|
540
|
+
// each gallery group leads with a cover.
|
|
541
|
+
const COVER_TEAL = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAAB4CAIAAADqjOKhAAAAsklEQVR42u3ZMQ2AMBRF0YpgR1INoAQPGKidrt2R0KQq2FlJmvJzkmvgjC8vbUd+tV9n4BIwMDAwMDAwMDAwMDAwMDAwMDAwMDAw8BLge/S/BAwMDAwMDAwMDAwMDAwMDAwMDAwMHADseQAGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYG/gIurc4PGBgYGBgYGBgYOADYWgIGBgYGBgYGBgYGBgYO0wNsxNp6TrTOmAAAAABJRU5ErkJggg==';
|
|
542
|
+
const COVER_ORANGE = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAAB4CAIAAADqjOKhAAAAsklEQVR42u3ZMQ2AMBRF0SpBAlKqBw24wEo1dKkClu5d2VlJmvJzkmvgjC8vnXl7VY89cAkYGBgYGBgYGBgYGBgYGBgYGBgYGBgYeAnwuNtfAgYGBgYGBgYGBgYGBgYGBgYGBgYGDgD2PAADAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDfwH3cs0PGBgYGBgYGBgYOADYWgIGBgYGBgYGBgYGBgYO0wM4vRK/kEih/QAAAABJRU5ErkJggg==';
|
|
543
|
+
const COVER_BLUE = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAAB4CAIAAADqjOKhAAAAsklEQVR42u3ZMQ2AMBRF0WpBAxqqBgEIQkSXGunCVANVwM5K0pSfk1wDZ3x5acvnq/0ogUvAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwEuAWx9/CRgYGBgYGBgYGBgYGBgYGBgYGBgYOADY8wAMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwM/AV81Xt+wMDAwMDAwMDAwAHA1hIwMDAwMDAwMDAwMDBwmB4f7fGQa+V+2QAAAABJRU5ErkJggg==';
|
|
356
544
|
const READING_SCHEMA = {
|
|
357
545
|
properties: [
|
|
358
546
|
{
|
|
@@ -375,11 +563,11 @@ const READING_SCHEMA = {
|
|
|
375
563
|
],
|
|
376
564
|
};
|
|
377
565
|
const READING_ROWS = [
|
|
378
|
-
{ name: 'The Design of Everyday Things', properties: { p_shelf: 'opt_reading', p_author: 'Don Norman', p_rating: 4 } },
|
|
566
|
+
{ name: 'The Design of Everyday Things', properties: { p_shelf: 'opt_reading', p_author: 'Don Norman', p_rating: 4, p_cover: [COVER_ORANGE] } },
|
|
379
567
|
{ name: 'Project Hail Mary', properties: { p_shelf: 'opt_reading', p_author: 'Andy Weir', p_rating: 5 } },
|
|
380
|
-
{ name: 'Thinking, Fast and Slow', properties: { p_shelf: 'opt_toread', p_author: 'Daniel Kahneman' } },
|
|
568
|
+
{ name: 'Thinking, Fast and Slow', properties: { p_shelf: 'opt_toread', p_author: 'Daniel Kahneman', p_cover: [COVER_TEAL] } },
|
|
381
569
|
{ name: 'Designing Data-Intensive Applications', properties: { p_shelf: 'opt_toread', p_author: 'Martin Kleppmann' } },
|
|
382
|
-
{ name: 'The Pragmatic Programmer', properties: { p_shelf: 'opt_done', p_author: 'Hunt & Thomas', p_rating: 5 } },
|
|
570
|
+
{ name: 'The Pragmatic Programmer', properties: { p_shelf: 'opt_done', p_author: 'Hunt & Thomas', p_rating: 5, p_cover: [COVER_BLUE] } },
|
|
383
571
|
{ name: 'Deep Work', properties: { p_shelf: 'opt_done', p_author: 'Cal Newport', p_rating: 4 } },
|
|
384
572
|
];
|
|
385
573
|
// ── Product roadmap ──────────────────────────────────────────────────────────
|
|
@@ -485,14 +673,98 @@ const FIELD_MAP_ROWS = [
|
|
|
485
673
|
{ name: 'Tokyo office', properties: { p_region: 'opt_apac', p_kind: 'opt_office', p_headcount: 85, p_address: 'Chiyoda, Tokyo', p_place: { lat: 35.6814, lng: 139.7670, label: 'Tokyo office' } } },
|
|
486
674
|
{ name: 'Sydney partner', properties: { p_region: 'opt_apac', p_kind: 'opt_partner', p_headcount: 15, p_address: 'Circular Quay, Sydney', p_place: { lat: -33.8610, lng: 151.2100, label: 'Sydney partner' } } },
|
|
487
675
|
];
|
|
676
|
+
// ── 🎯 Product HQ ────────────────────────────────────────────────────────────
|
|
677
|
+
// Two databases wired together: Initiatives (the page you land on) and Tasks
|
|
678
|
+
// (a sub-page). A 1:n relation links them BOTH ways (forward `Tasks` column +
|
|
679
|
+
// reverse `Initiative` column), two rollups on Initiatives fold the linked
|
|
680
|
+
// tasks (% done + task count), and a `dependency` property chains the tasks —
|
|
681
|
+
// surfaced as arrows on the Tasks page's timeline view.
|
|
682
|
+
/** Initiatives: status + the forward 1:n relation to Tasks + two rollups over it. */
|
|
683
|
+
const productHqInitiativesSchema = (tasksDbId) => ({
|
|
684
|
+
properties: [
|
|
685
|
+
{
|
|
686
|
+
id: 'p_status',
|
|
687
|
+
name: 'Status',
|
|
688
|
+
type: 'status',
|
|
689
|
+
options: [
|
|
690
|
+
{ id: 'opt_next', label: 'Up next', color: 'gray', group: 'todo' },
|
|
691
|
+
{ id: 'opt_track', label: 'On track', color: 'blue', group: 'in_progress' },
|
|
692
|
+
{ id: 'opt_risk', label: 'At risk', color: 'red', group: 'in_progress' },
|
|
693
|
+
{ id: 'opt_shipped', label: 'Shipped', color: 'green', group: 'complete' },
|
|
694
|
+
],
|
|
695
|
+
},
|
|
696
|
+
// The forward side of the two-way link: one initiative → many tasks.
|
|
697
|
+
{ id: 'p_tasks', name: 'Tasks', type: 'relation', relationDatabaseId: tasksDbId, relationCardinality: '1:n', reversePropertyId: 'p_initiative' },
|
|
698
|
+
// Rollups fold the linked tasks: how done, and how many.
|
|
699
|
+
{ id: 'p_progress', name: 'Progress', type: 'rollup', rollup: { relationPropertyId: 'p_tasks', targetPropertyId: 'p_done', function: 'percent_checked' } },
|
|
700
|
+
{ id: 'p_count', name: 'Task count', type: 'rollup', rollup: { relationPropertyId: 'p_tasks', targetPropertyId: TITLE_PROPERTY_ID, function: 'count' } },
|
|
701
|
+
],
|
|
702
|
+
views: [
|
|
703
|
+
// The table leads (relation chips + rollups in one glance); a board backs it.
|
|
704
|
+
{ id: 'v_table', name: 'Table', type: 'table', filters: [], sorts: [] },
|
|
705
|
+
{ id: 'v_board', name: 'Board', type: 'board', filters: [], sorts: [], groupByPropertyId: 'p_status' },
|
|
706
|
+
],
|
|
707
|
+
});
|
|
708
|
+
/** Tasks: the reverse (single) side of the relation, a Done checkbox the
|
|
709
|
+
* rollup folds, a date range, and the dependency chain the timeline draws. */
|
|
710
|
+
const productHqTasksSchema = (initiativesDbId) => ({
|
|
711
|
+
properties: [
|
|
712
|
+
{ id: 'p_initiative', name: 'Initiative', type: 'relation', relationDatabaseId: initiativesDbId, relationSingle: true, reversePropertyId: 'p_tasks' },
|
|
713
|
+
{ id: 'p_owner', name: 'Owner', type: 'text' },
|
|
714
|
+
{ id: 'p_done', name: 'Done', type: 'checkbox' },
|
|
715
|
+
{ id: 'p_when', name: 'When', type: 'date', dateRange: true },
|
|
716
|
+
{ id: 'p_blockedby', name: 'Blocked by', type: 'dependency' },
|
|
717
|
+
],
|
|
718
|
+
views: [
|
|
719
|
+
// Timeline first: bars from the `When` range, dependency arrows from
|
|
720
|
+
// `Blocked by` (predecessor end → dependent start).
|
|
721
|
+
{ id: 'v_timeline', name: 'Timeline', type: 'timeline', filters: [], sorts: [], datePropertyId: 'p_when', dependencyPropertyId: 'p_blockedby' },
|
|
722
|
+
{ id: 'v_table', name: 'Table', type: 'table', filters: [], sorts: [] },
|
|
723
|
+
],
|
|
724
|
+
});
|
|
725
|
+
/** Build the two databases, then seed rows across both so the relation, the
|
|
726
|
+
* rollups, and the dependency arrows all render non-empty out of the box. */
|
|
727
|
+
const createProductHq = async (client, name, guidance = GUIDANCE.productHq) => {
|
|
728
|
+
// Pre-minted ids let each schema reference the OTHER database with no
|
|
729
|
+
// second-pass schema update (createDatabase honours a client-supplied id).
|
|
730
|
+
const initiativesDbId = globalThis.crypto.randomUUID();
|
|
731
|
+
const tasksDbId = globalThis.crypto.randomUUID();
|
|
732
|
+
const tasksName = `${name} Tasks`;
|
|
733
|
+
// The guidance callout leads the Initiatives host page; the Tasks sub-page
|
|
734
|
+
// stays bare (it's reached from the guided page).
|
|
735
|
+
const page = await client.savePage({ name, data: guidanceSnapshot({ id: 'hq-guide', text: guidance }) });
|
|
736
|
+
const tasksPage = await client.savePage({ name: tasksName, data: emptySnapshot([]), parentId: page.id });
|
|
737
|
+
await client.createDatabase({ id: initiativesDbId, pageId: page.id, name, schema: productHqInitiativesSchema(tasksDbId) });
|
|
738
|
+
await client.createDatabase({ id: tasksDbId, pageId: tasksPage.id, name: tasksName, schema: productHqTasksSchema(initiativesDbId) });
|
|
739
|
+
const seedRow = async (dbId, rowName, properties) => (await client.createRow(dbId, { name: rowName, properties })).id;
|
|
740
|
+
// Initiatives first (the tasks link back to them)…
|
|
741
|
+
const revamp = await seedRow(initiativesDbId, 'Onboarding revamp', { p_status: 'opt_track' });
|
|
742
|
+
const perf = await seedRow(initiativesDbId, 'Performance push', { p_status: 'opt_risk' });
|
|
743
|
+
const billing = await seedRow(initiativesDbId, 'Billing v2', { p_status: 'opt_shipped' });
|
|
744
|
+
// …then the tasks: linked 1:n, dated for the timeline, chained by `Blocked by`.
|
|
745
|
+
const t1 = await seedRow(tasksDbId, 'Ship onboarding checklist', { p_initiative: [revamp], p_owner: 'Ada', p_done: true, p_when: { start: day(-10), end: day(-3) } });
|
|
746
|
+
const t2 = await seedRow(tasksDbId, 'Guided first-run tour', { p_initiative: [revamp], p_owner: 'Lin', p_done: false, p_when: { start: day(-2), end: day(6) }, p_blockedby: [t1] });
|
|
747
|
+
const t3 = await seedRow(tasksDbId, 'Profile the hot paths', { p_initiative: [perf], p_owner: 'Sam', p_done: false, p_when: { start: day(1), end: day(5) } });
|
|
748
|
+
const t4 = await seedRow(tasksDbId, 'Cache the page list', { p_initiative: [perf], p_owner: 'Sam', p_done: false, p_when: { start: day(6), end: day(12) }, p_blockedby: [t3] });
|
|
749
|
+
const t5 = await seedRow(tasksDbId, 'Migrate legacy invoices', { p_initiative: [billing], p_owner: 'Lin', p_done: true, p_when: { start: day(-20), end: day(-12) } });
|
|
750
|
+
// Mirror the reverse side of the two-way link. Seeding writes both sides
|
|
751
|
+
// explicitly — the live mirror only runs on relation-cell edits. (updateRow
|
|
752
|
+
// replaces the whole properties bag, so re-send the status too.)
|
|
753
|
+
await client.updateRow(initiativesDbId, revamp, { properties: { p_status: 'opt_track', p_tasks: [t1, t2] } });
|
|
754
|
+
await client.updateRow(initiativesDbId, perf, { properties: { p_status: 'opt_risk', p_tasks: [t3, t4] } });
|
|
755
|
+
await client.updateRow(initiativesDbId, billing, { properties: { p_status: 'opt_shipped', p_tasks: [t5] } });
|
|
756
|
+
return page;
|
|
757
|
+
};
|
|
488
758
|
// ── The gallery ──────────────────────────────────────────────────────────────
|
|
489
759
|
/** Create a block-editor template page from a JSON block projection. */
|
|
490
760
|
const createBlockDocPage = (blocks) => (client, name) => client.savePage({ name, data: { editorjs: { blocks: [] }, values: [], names: [], editor: 'blocks', blockdoc: { blocks } } });
|
|
491
761
|
/** Create a database template: host page + database + sample rows. Names are
|
|
492
762
|
* not unique, so a plain create always lands; the retry ladder below survives
|
|
493
|
-
* transient failures (untitled as a last resort).
|
|
494
|
-
|
|
495
|
-
|
|
763
|
+
* transient failures (untitled as a last resort). `guide` (id + canonical
|
|
764
|
+
* English text) puts the standardized guidance callout on the host page —
|
|
765
|
+
* where a database template's doc surface renders, above the view. */
|
|
766
|
+
const createDatabasePage = (schema, rows, guide) => async (client, name, guidance) => {
|
|
767
|
+
const page = await client.savePage({ name, data: guidanceSnapshot(guide && { id: guide.id, text: guidance ?? guide.text }) });
|
|
496
768
|
const db = await client.createDatabase({ pageId: page.id, name, schema });
|
|
497
769
|
for (const row of rows) {
|
|
498
770
|
let rowName = row.name;
|
|
@@ -512,14 +784,30 @@ const createDatabasePage = (schema, rows) => async (client, name) => {
|
|
|
512
784
|
}
|
|
513
785
|
return page;
|
|
514
786
|
};
|
|
787
|
+
/** A fresh copy of the sample document under its own gallery name. It already
|
|
788
|
+
* opens with its own intro paragraph (`sample-intro`), so — unlike the database
|
|
789
|
+
* fixtures — it carries no standardized guidance callout of its own (that would
|
|
790
|
+
* double-guide, stacking a near-duplicate lead above the intro). */
|
|
791
|
+
const createCompoundGrowth = (client, name) => {
|
|
792
|
+
const input = buildSampleDocument();
|
|
793
|
+
return client.savePage({ ...input, name });
|
|
794
|
+
};
|
|
515
795
|
export const PAGE_TEMPLATES = [
|
|
516
|
-
{ id: 'grocery-tracker', icon: '🛒', pageName: 'Grocery price tracker', create: createBlockDocPage(GROCERY_BLOCKS) },
|
|
517
|
-
{ id: 'task-board', icon: '🗂️', pageName: 'Project task board', create: createDatabasePage(TASK_BOARD_SCHEMA, TASK_BOARD_ROWS) },
|
|
518
|
-
{ id: 'reading-list', icon: '📚', pageName: 'Reading list', create: createDatabasePage(READING_SCHEMA, READING_ROWS) },
|
|
519
|
-
{ id: 'project-intake', icon: '📋', pageName: 'Project intake', create: createBlockDocPage(PROJECT_INTAKE_BLOCKS) },
|
|
520
|
-
{ id: 'savings-planner', icon: '💰', pageName: 'Savings & investing', create: createBlockDocPage(SAVINGS_BLOCKS) },
|
|
521
|
-
{ id: 'roadmap', icon: '🗺️', pageName: 'Product roadmap', create: createDatabasePage(ROADMAP_SCHEMA, ROADMAP_ROWS) },
|
|
522
|
-
{ id: 'field-map', icon: '📍', pageName: 'Field map', create: createDatabasePage(FIELD_MAP_SCHEMA, FIELD_MAP_ROWS) },
|
|
796
|
+
{ id: 'grocery-tracker', icon: '🛒', pageName: 'Grocery price tracker', tags: ['interactive', 'slides'], create: createBlockDocPage(GROCERY_BLOCKS) },
|
|
797
|
+
{ id: 'task-board', icon: '🗂️', pageName: 'Project task board', tags: ['database'], guidance: GUIDANCE.taskBoard, create: createDatabasePage(TASK_BOARD_SCHEMA, TASK_BOARD_ROWS, { id: 'tb-guide', text: GUIDANCE.taskBoard }) },
|
|
798
|
+
{ id: 'reading-list', icon: '📚', pageName: 'Reading list', tags: ['database'], guidance: GUIDANCE.readingList, create: createDatabasePage(READING_SCHEMA, READING_ROWS, { id: 'rl-guide', text: GUIDANCE.readingList }) },
|
|
799
|
+
{ id: 'project-intake', icon: '📋', pageName: 'Project intake', tags: ['interactive', 'slides'], create: createBlockDocPage(PROJECT_INTAKE_BLOCKS) },
|
|
800
|
+
{ id: 'savings-planner', icon: '💰', pageName: 'Savings & investing', tags: ['interactive', 'slides'], create: createBlockDocPage(SAVINGS_BLOCKS) },
|
|
801
|
+
{ id: 'roadmap', icon: '🗺️', pageName: 'Product roadmap', tags: ['database'], guidance: GUIDANCE.roadmap, create: createDatabasePage(ROADMAP_SCHEMA, ROADMAP_ROWS, { id: 'rm-guide', text: GUIDANCE.roadmap }) },
|
|
802
|
+
{ id: 'field-map', icon: '📍', pageName: 'Field map', tags: ['database'], guidance: GUIDANCE.fieldMap, create: createDatabasePage(FIELD_MAP_SCHEMA, FIELD_MAP_ROWS, { id: 'fm-guide', text: GUIDANCE.fieldMap }) },
|
|
803
|
+
{ id: 'pitch-deck', icon: '📽️', pageName: 'Pitch deck', tags: ['interactive', 'slides'], create: createBlockDocPage(PITCH_DECK_BLOCKS) },
|
|
804
|
+
{ id: 'team-status', icon: '🚦', pageName: 'Team status dashboard', tags: ['interactive'], create: createBlockDocPage(TEAM_STATUS_BLOCKS) },
|
|
805
|
+
{ id: 'product-hq', icon: '🎯', pageName: 'Product HQ', tags: ['database'], guidance: GUIDANCE.productHq, create: createProductHq },
|
|
806
|
+
// The classic sample document, folded into the gallery. Unlike the Home
|
|
807
|
+
// starter's open-or-create (which targets the canonical sample name and never
|
|
808
|
+
// overwrites), the gallery card always mints a FRESH copy under its own
|
|
809
|
+
// display name — the two entry points never race or shadow each other.
|
|
810
|
+
{ id: 'compound-growth', icon: '📈', pageName: 'Compound growth', tags: ['interactive'], create: createCompoundGrowth },
|
|
523
811
|
];
|
|
524
812
|
/** Courtesy numbering (names are not unique): a second instance becomes
|
|
525
813
|
* `name 2`, `name 3`… so repeated instantiations stay tellable-apart. */
|
|
@@ -536,13 +824,15 @@ async function availableName(client, base) {
|
|
|
536
824
|
/**
|
|
537
825
|
* Instantiate a template: pick a distinct display name (courtesy numbering —
|
|
538
826
|
* duplicates are allowed but unhelpful for ready-made pages) and build the page
|
|
539
|
-
* through the client, retrying transient failures.
|
|
827
|
+
* through the client, retrying transient failures. `opts.guidance` localizes
|
|
828
|
+
* the leading guidance callout of templates that carry one (the gallery passes
|
|
829
|
+
* the user's locale text; absent, the canonical English default applies).
|
|
540
830
|
*/
|
|
541
|
-
export async function instantiateTemplate(client, template) {
|
|
831
|
+
export async function instantiateTemplate(client, template, opts) {
|
|
542
832
|
let name = await availableName(client, template.pageName);
|
|
543
833
|
for (let attempt = 0;; attempt += 1) {
|
|
544
834
|
try {
|
|
545
|
-
return await template.create(client, name);
|
|
835
|
+
return await template.create(client, name, opts?.guidance);
|
|
546
836
|
}
|
|
547
837
|
catch (err) {
|
|
548
838
|
// A concurrent create can win the name between the check and the save;
|