@book.dev/sdk 1.76.2 → 2.1.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/ai.d.ts +108 -4
- package/dist/authorize.js +18 -7
- package/dist/authorize.js.map +1 -1
- 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 +53 -5
- package/dist/client.js +35 -5
- package/dist/client.js.map +1 -1
- package/dist/database.d.ts +9 -3
- package/dist/database.js +23 -7
- 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/identity.d.ts +44 -1
- package/dist/identity.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 +7 -7
- 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 +34 -3
- package/dist/routes.js +36 -4
- package/dist/routes.js.map +1 -1
- package/dist/templates.d.ts +22 -6
- package/dist/templates.js +461 -14
- package/dist/templates.js.map +1 -1
- package/package.json +1 -1
package/dist/templates.js
CHANGED
|
@@ -1,8 +1,37 @@
|
|
|
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
|
+
dashboard: 'This dashboard reads a sample sales database: the KPI tiles total the rows, and the bar, pie and trend charts group them — all live. Try it: edit a deal on the “… data” sub-page and watch a tile move.',
|
|
22
|
+
};
|
|
23
|
+
/** The guidance callout as a block-doc block (ids are stable per template). */
|
|
24
|
+
const guidanceCallout = (id, text) => ({
|
|
25
|
+
id,
|
|
26
|
+
type: 'callout',
|
|
27
|
+
text: [{ t: text }],
|
|
28
|
+
props: { variant: 'info' },
|
|
29
|
+
});
|
|
30
|
+
/** A host-page snapshot: empty, or a single leading guidance callout rendered
|
|
31
|
+
* (block-doc) above the hosted database view. */
|
|
32
|
+
const guidanceSnapshot = (guide) => guide
|
|
33
|
+
? { editorjs: { blocks: [] }, values: [], names: [], editor: 'blocks', blockdoc: { blocks: [guidanceCallout(guide.id, guide.text)] } }
|
|
34
|
+
: emptySnapshot([]);
|
|
6
35
|
/** A local `YYYY-MM-DD` day string offset by `days` from today. */
|
|
7
36
|
const day = (days) => {
|
|
8
37
|
const d = new Date();
|
|
@@ -304,6 +333,151 @@ const SAVINGS_BLOCKS = [
|
|
|
304
333
|
{ 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
334
|
{ id: 's-notes-4', type: 'notes', text: [{ t: 'One ask to close on: automate the monthly contribution so the plan happens without willpower.' }] },
|
|
306
335
|
];
|
|
336
|
+
// ── 📽️ Pitch deck ────────────────────────────────────────────────────────────
|
|
337
|
+
// A present-first showcase: five slides (title · agenda · a live revenue-mix
|
|
338
|
+
// donut · a quote · the ask), each with a speaker `notes` block, so the ⋯ →
|
|
339
|
+
// Present flow lands on a real deck. The donut slide is the interactive core:
|
|
340
|
+
// three sliders feed the chart and a recurring-revenue status light.
|
|
341
|
+
const PITCH_DECK_BLOCKS = [
|
|
342
|
+
// Slide 1 — title
|
|
343
|
+
{ id: 'pd-h1', type: 'heading', text: [{ t: 'Brightloop' }], props: { level: 1 } },
|
|
344
|
+
{ 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.' }] },
|
|
345
|
+
{ id: 'pd-call', type: 'callout', text: [{ t: 'Open the ⋯ menu → Present to run this as a deck.' }], props: { variant: 'info' } },
|
|
346
|
+
{ 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.' }] },
|
|
347
|
+
{ id: 'pd-div-1', type: 'divider' },
|
|
348
|
+
// Slide 2 — agenda
|
|
349
|
+
{ id: 'pd-h2', type: 'heading', text: [{ t: 'Agenda' }], props: { level: 2 } },
|
|
350
|
+
{ id: 'pd-ag1', type: 'list', text: [{ t: 'The problem — decks go stale the moment they’re exported.' }], props: { kind: 'number' } },
|
|
351
|
+
{ id: 'pd-ag2', type: 'list', text: [{ t: 'The product — one page that is both the model and the deck.' }], props: { kind: 'number' } },
|
|
352
|
+
{ id: 'pd-ag3', type: 'list', text: [{ t: 'Revenue mix — live, draggable, no screenshots.' }], props: { kind: 'number' } },
|
|
353
|
+
{ id: 'pd-ag4', type: 'list', text: [{ t: 'What early users say.' }], props: { kind: 'number' } },
|
|
354
|
+
{ id: 'pd-ag5', type: 'list', text: [{ t: 'The ask.' }], props: { kind: 'number' } },
|
|
355
|
+
{ 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.' }] },
|
|
356
|
+
{ id: 'pd-div-2', type: 'divider' },
|
|
357
|
+
// Slide 3 — the live donut
|
|
358
|
+
{ id: 'pd-h3', type: 'heading', text: [{ t: 'Revenue mix — live' }], props: { level: 2 } },
|
|
359
|
+
{ id: 'pd-recurring', type: 'code', text: [{ t: 'Math.round(subs / (subs + services + partners) * 100)' }], props: { live: true, name: 'recurring', language: 'js', collapsed: true } },
|
|
360
|
+
{
|
|
361
|
+
id: 'pd-cols',
|
|
362
|
+
type: 'columns',
|
|
363
|
+
children: [
|
|
364
|
+
{
|
|
365
|
+
id: 'pd-col-l',
|
|
366
|
+
type: 'column',
|
|
367
|
+
props: { span: 5 },
|
|
368
|
+
children: [
|
|
369
|
+
{ id: 'pd-subs', type: 'slider', props: { name: 'subs', label: 'Subscriptions (£k/yr)', value: 62, min: 0, max: 200 } },
|
|
370
|
+
{ id: 'pd-services', type: 'slider', props: { name: 'services', label: 'Services (£k/yr)', value: 26, min: 0, max: 200 } },
|
|
371
|
+
{ id: 'pd-partners', type: 'slider', props: { name: 'partners', label: 'Partnerships (£k/yr)', value: 12, min: 0, max: 200 } },
|
|
372
|
+
],
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
id: 'pd-col-r',
|
|
376
|
+
type: 'column',
|
|
377
|
+
props: { span: 7 },
|
|
378
|
+
children: [
|
|
379
|
+
{ id: 'pd-donut', type: 'kitchart', props: { kind: 'donut', title: 'Revenue by stream (£k/yr)', labels: 'Subscriptions, Services, Partnerships', source: '[subs, services, partners]' } },
|
|
380
|
+
{ id: 'pd-status', type: 'statuslight', props: { label: 'Recurring revenue ≥ 60%', source: 'recurring', okAt: 60, warnAt: 45 } },
|
|
381
|
+
],
|
|
382
|
+
},
|
|
383
|
+
],
|
|
384
|
+
},
|
|
385
|
+
{ 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.' }] },
|
|
386
|
+
{ id: 'pd-div-3', type: 'divider' },
|
|
387
|
+
// Slide 4 — the quote
|
|
388
|
+
{ id: 'pd-h4', type: 'heading', text: [{ t: 'What early users say' }], props: { level: 2 } },
|
|
389
|
+
{ 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.' }] },
|
|
390
|
+
{ 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.' }] },
|
|
391
|
+
{ id: 'pd-div-4', type: 'divider' },
|
|
392
|
+
// Slide 5 — the ask
|
|
393
|
+
{ id: 'pd-h5', type: 'heading', text: [{ t: 'The ask' }], props: { level: 2 } },
|
|
394
|
+
{ 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.' }] },
|
|
395
|
+
{ 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' } },
|
|
396
|
+
{ 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.' }] },
|
|
397
|
+
];
|
|
398
|
+
// ── 🚦 Team status dashboard ─────────────────────────────────────────────────
|
|
399
|
+
// The kit-breadth showcase, as a single-page dashboard (no slides): a **locked
|
|
400
|
+
// group** whose controls stay live for readers (toggle, dropdown, a kudos
|
|
401
|
+
// counter driven by an action button, a formula and a status light reading it),
|
|
402
|
+
// a **funnel** chart (a kind no other template uses), a **tabs** container, and
|
|
403
|
+
// a cross-page **sync** key — the same Pulse group pasted on another page stays
|
|
404
|
+
// in lockstep under `team-pulse`.
|
|
405
|
+
const TEAM_STATUS_BLOCKS = [
|
|
406
|
+
{ 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.' }] },
|
|
407
|
+
{ 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' } },
|
|
408
|
+
// The locked, synced control panel.
|
|
409
|
+
{ id: 'td-h2', type: 'heading', text: [{ t: 'Team pulse' }], props: { level: 2 } },
|
|
410
|
+
{
|
|
411
|
+
id: 'td-group',
|
|
412
|
+
type: 'group',
|
|
413
|
+
props: { name: 'Pulse', locked: true, sync: 'team-pulse' },
|
|
414
|
+
children: [
|
|
415
|
+
{ 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.' }] },
|
|
416
|
+
{ id: 'td-oncall', type: 'toggle', props: { name: 'onCall', label: 'On-call rotation active', value: true } },
|
|
417
|
+
{ id: 'td-focus', type: 'dropdown', props: { name: 'focus', label: 'Focus this week', value: 'shipping', opts: [{ label: 'Shipping' }, { label: 'Stability' }, { label: 'Growth' }] } },
|
|
418
|
+
{ id: 'td-kudos', type: 'number', props: { name: 'kudos', label: 'Kudos given', value: 2, min: 0, max: 99, step: 1 } },
|
|
419
|
+
{ id: 'td-give', type: 'actionbutton', props: { btnlabel: 'Give kudos', action: 'increment', target: 'kudos', amount: 1 } },
|
|
420
|
+
// Inputs inside a named group publish namespaced — pulse.kudos.value —
|
|
421
|
+
// which is exactly what this formula (and the light below) read.
|
|
422
|
+
{ id: 'td-score', type: 'formula', props: { name: 'morale', source: 'pulse.kudos.value * 10 + (pulse.onCall.value ? 5 : 0)' } },
|
|
423
|
+
{ id: 'td-light', type: 'statuslight', props: { label: 'Momentum', source: 'pulse.kudos.value', okAt: 3, warnAt: 1 } },
|
|
424
|
+
],
|
|
425
|
+
},
|
|
426
|
+
// The delivery funnel: a chart kind no other template exercises.
|
|
427
|
+
{ id: 'td-h3', type: 'heading', text: [{ t: 'Delivery pipeline' }], props: { level: 2 } },
|
|
428
|
+
{ 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 } },
|
|
429
|
+
{
|
|
430
|
+
id: 'td-cols',
|
|
431
|
+
type: 'columns',
|
|
432
|
+
children: [
|
|
433
|
+
{
|
|
434
|
+
id: 'td-col-l',
|
|
435
|
+
type: 'column',
|
|
436
|
+
props: { span: 5 },
|
|
437
|
+
children: [
|
|
438
|
+
{ id: 'td-shipped', type: 'number', props: { name: 'shipped', label: 'Shipped this quarter', value: 5, min: 0, max: 50, step: 1 } },
|
|
439
|
+
{ id: 'td-tip', type: 'tooltipcard', props: { term: 'Funnel', tip: 'Each stage narrows: ideas → building → review → shipped. Step the shipped count and the funnel redraws.' } },
|
|
440
|
+
],
|
|
441
|
+
},
|
|
442
|
+
{
|
|
443
|
+
id: 'td-col-r',
|
|
444
|
+
type: 'column',
|
|
445
|
+
props: { span: 7 },
|
|
446
|
+
children: [
|
|
447
|
+
{ id: 'td-funnel', type: 'kitchart', props: { kind: 'funnel', title: 'Ideas → Shipped', source: 'pipeline' } },
|
|
448
|
+
],
|
|
449
|
+
},
|
|
450
|
+
],
|
|
451
|
+
},
|
|
452
|
+
// The week's rituals, in a tabs container.
|
|
453
|
+
{ id: 'td-h4', type: 'heading', text: [{ t: 'Rituals' }], props: { level: 2 } },
|
|
454
|
+
{
|
|
455
|
+
id: 'td-tabs',
|
|
456
|
+
type: 'tabs',
|
|
457
|
+
props: { name: 'Rituals', active: 0 },
|
|
458
|
+
children: [
|
|
459
|
+
{
|
|
460
|
+
id: 'td-tab-week',
|
|
461
|
+
type: 'tab',
|
|
462
|
+
props: { label: 'This week' },
|
|
463
|
+
children: [
|
|
464
|
+
{ id: 'td-t1', type: 'todo', text: [{ t: 'Monday kick-off — pick the focus in the Pulse panel' }], props: { checked: true } },
|
|
465
|
+
{ id: 'td-t2', type: 'todo', text: [{ t: 'Thursday demo — show, don’t tell' }], props: { checked: false } },
|
|
466
|
+
],
|
|
467
|
+
},
|
|
468
|
+
{
|
|
469
|
+
id: 'td-tab-next',
|
|
470
|
+
type: 'tab',
|
|
471
|
+
props: { label: 'Next week' },
|
|
472
|
+
children: [
|
|
473
|
+
{ id: 'td-n1', type: 'list', text: [{ t: 'Rotate the on-call — flip the Pulse toggle' }], props: { kind: 'bullet' } },
|
|
474
|
+
{ id: 'td-n2', type: 'list', text: [{ t: 'Reset the kudos counter at retro' }], props: { kind: 'bullet' } },
|
|
475
|
+
],
|
|
476
|
+
},
|
|
477
|
+
],
|
|
478
|
+
},
|
|
479
|
+
{ 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' } },
|
|
480
|
+
];
|
|
307
481
|
// ════════════════════════════════════════════════════════════════════════════
|
|
308
482
|
// Databases (the task board, reading list, and the swimlane + map e2e fixtures)
|
|
309
483
|
// ════════════════════════════════════════════════════════════════════════════
|
|
@@ -337,9 +511,11 @@ const TASK_BOARD_SCHEMA = {
|
|
|
337
511
|
{ id: 'p_effort', name: 'Effort', type: 'number', numberDisplay: 'bar', numberTarget: 8 },
|
|
338
512
|
],
|
|
339
513
|
views: [
|
|
340
|
-
// Board first → the page opens as a kanban grouped by status; a table backs
|
|
514
|
+
// Board first → the page opens as a kanban grouped by status; a table backs
|
|
515
|
+
// it, and a calendar lays the same tasks out on a month grid by due date.
|
|
341
516
|
{ id: 'v_board', name: 'Board', type: 'board', filters: [], sorts: [], groupByPropertyId: 'p_status' },
|
|
342
517
|
{ id: 'v_table', name: 'Table', type: 'table', filters: [], sorts: [] },
|
|
518
|
+
{ id: 'v_calendar', name: 'Calendar', type: 'calendar', filters: [], sorts: [], datePropertyId: 'p_due' },
|
|
343
519
|
],
|
|
344
520
|
};
|
|
345
521
|
const TASK_BOARD_ROWS = [
|
|
@@ -353,6 +529,19 @@ const TASK_BOARD_ROWS = [
|
|
|
353
529
|
];
|
|
354
530
|
// ── 📚 Reading list ──────────────────────────────────────────────────────────
|
|
355
531
|
// A shelf-grouped gallery of books, with authors and star ratings; a table backs it.
|
|
532
|
+
//
|
|
533
|
+
// Covers (so the gallery renders real cards, not empty slots): tiny (<300 B) raster
|
|
534
|
+
// PNGs, inlined as `data:` URLs on the `files`-typed `p_cover` cells. Deliberately
|
|
535
|
+
// NOT routed through the content-addressed asset store — the `files` property and
|
|
536
|
+
// the gallery cover render a URL string straight into `<img src>` with no
|
|
537
|
+
// asset-resolution seam, so a store `assetId` wouldn't load there, and this seeds
|
|
538
|
+
// identically on both transports (web PGlite + desktop IPC) with no upload call.
|
|
539
|
+
// PNG, never SVG — honouring the store's image allowlist even though nothing is
|
|
540
|
+
// stored (an `<img src="data:image/png…">` executes no script). One per shelf so
|
|
541
|
+
// each gallery group leads with a cover.
|
|
542
|
+
const COVER_TEAL = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAAB4CAIAAADqjOKhAAAAsklEQVR42u3ZMQ2AMBRF0YpgR1INoAQPGKidrt2R0KQq2FlJmvJzkmvgjC8vbUd+tV9n4BIwMDAwMDAwMDAwMDAwMDAwMDAwMDAw8BLge/S/BAwMDAwMDAwMDAwMDAwMDAwMDAwMHADseQAGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYG/gIurc4PGBgYGBgYGBgYOADYWgIGBgYGBgYGBgYGBgYO0wNsxNp6TrTOmAAAAABJRU5ErkJggg==';
|
|
543
|
+
const COVER_ORANGE = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAAB4CAIAAADqjOKhAAAAsklEQVR42u3ZMQ2AMBRF0SpBAlKqBw24wEo1dKkClu5d2VlJmvJzkmvgjC8vnXl7VY89cAkYGBgYGBgYGBgYGBgYGBgYGBgYGBgYeAnwuNtfAgYGBgYGBgYGBgYGBgYGBgYGBgYGDgD2PAADAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDfwH3cs0PGBgYGBgYGBgYOADYWgIGBgYGBgYGBgYGBgYO0wM4vRK/kEih/QAAAABJRU5ErkJggg==';
|
|
544
|
+
const COVER_BLUE = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAAB4CAIAAADqjOKhAAAAsklEQVR42u3ZMQ2AMBRF0WpBAxqqBgEIQkSXGunCVANVwM5K0pSfk1wDZ3x5acvnq/0ogUvAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwEuAWx9/CRgYGBgYGBgYGBgYGBgYGBgYGBgYOADY8wAMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwM/AV81Xt+wMDAwMDAwMDAwAHA1hIwMDAwMDAwMDAwMDBwmB4f7fGQa+V+2QAAAABJRU5ErkJggg==';
|
|
356
545
|
const READING_SCHEMA = {
|
|
357
546
|
properties: [
|
|
358
547
|
{
|
|
@@ -375,11 +564,11 @@ const READING_SCHEMA = {
|
|
|
375
564
|
],
|
|
376
565
|
};
|
|
377
566
|
const READING_ROWS = [
|
|
378
|
-
{ name: 'The Design of Everyday Things', properties: { p_shelf: 'opt_reading', p_author: 'Don Norman', p_rating: 4 } },
|
|
567
|
+
{ name: 'The Design of Everyday Things', properties: { p_shelf: 'opt_reading', p_author: 'Don Norman', p_rating: 4, p_cover: [COVER_ORANGE] } },
|
|
379
568
|
{ 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' } },
|
|
569
|
+
{ name: 'Thinking, Fast and Slow', properties: { p_shelf: 'opt_toread', p_author: 'Daniel Kahneman', p_cover: [COVER_TEAL] } },
|
|
381
570
|
{ 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 } },
|
|
571
|
+
{ name: 'The Pragmatic Programmer', properties: { p_shelf: 'opt_done', p_author: 'Hunt & Thomas', p_rating: 5, p_cover: [COVER_BLUE] } },
|
|
383
572
|
{ name: 'Deep Work', properties: { p_shelf: 'opt_done', p_author: 'Cal Newport', p_rating: 4 } },
|
|
384
573
|
];
|
|
385
574
|
// ── Product roadmap ──────────────────────────────────────────────────────────
|
|
@@ -485,14 +674,250 @@ const FIELD_MAP_ROWS = [
|
|
|
485
674
|
{ 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
675
|
{ 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
676
|
];
|
|
677
|
+
// ── 🎯 Product HQ ────────────────────────────────────────────────────────────
|
|
678
|
+
// Two databases wired together: Initiatives (the page you land on) and Tasks
|
|
679
|
+
// (a sub-page). A 1:n relation links them BOTH ways (forward `Tasks` column +
|
|
680
|
+
// reverse `Initiative` column), two rollups on Initiatives fold the linked
|
|
681
|
+
// tasks (% done + task count), and a `dependency` property chains the tasks —
|
|
682
|
+
// surfaced as arrows on the Tasks page's timeline view.
|
|
683
|
+
/** Initiatives: status + the forward 1:n relation to Tasks + two rollups over it. */
|
|
684
|
+
const productHqInitiativesSchema = (tasksDbId) => ({
|
|
685
|
+
properties: [
|
|
686
|
+
{
|
|
687
|
+
id: 'p_status',
|
|
688
|
+
name: 'Status',
|
|
689
|
+
type: 'status',
|
|
690
|
+
options: [
|
|
691
|
+
{ id: 'opt_next', label: 'Up next', color: 'gray', group: 'todo' },
|
|
692
|
+
{ id: 'opt_track', label: 'On track', color: 'blue', group: 'in_progress' },
|
|
693
|
+
{ id: 'opt_risk', label: 'At risk', color: 'red', group: 'in_progress' },
|
|
694
|
+
{ id: 'opt_shipped', label: 'Shipped', color: 'green', group: 'complete' },
|
|
695
|
+
],
|
|
696
|
+
},
|
|
697
|
+
// The forward side of the two-way link: one initiative → many tasks.
|
|
698
|
+
{ id: 'p_tasks', name: 'Tasks', type: 'relation', relationDatabaseId: tasksDbId, relationCardinality: '1:n', reversePropertyId: 'p_initiative' },
|
|
699
|
+
// Rollups fold the linked tasks: how done, and how many.
|
|
700
|
+
{ id: 'p_progress', name: 'Progress', type: 'rollup', rollup: { relationPropertyId: 'p_tasks', targetPropertyId: 'p_done', function: 'percent_checked' } },
|
|
701
|
+
{ id: 'p_count', name: 'Task count', type: 'rollup', rollup: { relationPropertyId: 'p_tasks', targetPropertyId: TITLE_PROPERTY_ID, function: 'count' } },
|
|
702
|
+
],
|
|
703
|
+
views: [
|
|
704
|
+
// The table leads (relation chips + rollups in one glance); a board backs it.
|
|
705
|
+
{ id: 'v_table', name: 'Table', type: 'table', filters: [], sorts: [] },
|
|
706
|
+
{ id: 'v_board', name: 'Board', type: 'board', filters: [], sorts: [], groupByPropertyId: 'p_status' },
|
|
707
|
+
],
|
|
708
|
+
});
|
|
709
|
+
/** Tasks: the reverse (single) side of the relation, a Done checkbox the
|
|
710
|
+
* rollup folds, a date range, and the dependency chain the timeline draws. */
|
|
711
|
+
const productHqTasksSchema = (initiativesDbId) => ({
|
|
712
|
+
properties: [
|
|
713
|
+
{ id: 'p_initiative', name: 'Initiative', type: 'relation', relationDatabaseId: initiativesDbId, relationSingle: true, reversePropertyId: 'p_tasks' },
|
|
714
|
+
{ id: 'p_owner', name: 'Owner', type: 'text' },
|
|
715
|
+
{ id: 'p_done', name: 'Done', type: 'checkbox' },
|
|
716
|
+
{ id: 'p_when', name: 'When', type: 'date', dateRange: true },
|
|
717
|
+
{ id: 'p_blockedby', name: 'Blocked by', type: 'dependency' },
|
|
718
|
+
],
|
|
719
|
+
views: [
|
|
720
|
+
// Timeline first: bars from the `When` range, dependency arrows from
|
|
721
|
+
// `Blocked by` (predecessor end → dependent start).
|
|
722
|
+
{ id: 'v_timeline', name: 'Timeline', type: 'timeline', filters: [], sorts: [], datePropertyId: 'p_when', dependencyPropertyId: 'p_blockedby' },
|
|
723
|
+
{ id: 'v_table', name: 'Table', type: 'table', filters: [], sorts: [] },
|
|
724
|
+
],
|
|
725
|
+
});
|
|
726
|
+
/** Build the two databases, then seed rows across both so the relation, the
|
|
727
|
+
* rollups, and the dependency arrows all render non-empty out of the box. */
|
|
728
|
+
const createProductHq = async (client, name, guidance = GUIDANCE.productHq) => {
|
|
729
|
+
// Pre-minted ids let each schema reference the OTHER database with no
|
|
730
|
+
// second-pass schema update (createDatabase honours a client-supplied id).
|
|
731
|
+
const initiativesDbId = globalThis.crypto.randomUUID();
|
|
732
|
+
const tasksDbId = globalThis.crypto.randomUUID();
|
|
733
|
+
const tasksName = `${name} Tasks`;
|
|
734
|
+
// The guidance callout leads the Initiatives host page; the Tasks sub-page
|
|
735
|
+
// stays bare (it's reached from the guided page).
|
|
736
|
+
const page = await client.savePage({ name, data: guidanceSnapshot({ id: 'hq-guide', text: guidance }) });
|
|
737
|
+
const tasksPage = await client.savePage({ name: tasksName, data: emptySnapshot([]), parentId: page.id });
|
|
738
|
+
await client.createDatabase({ id: initiativesDbId, pageId: page.id, name, schema: productHqInitiativesSchema(tasksDbId) });
|
|
739
|
+
await client.createDatabase({ id: tasksDbId, pageId: tasksPage.id, name: tasksName, schema: productHqTasksSchema(initiativesDbId) });
|
|
740
|
+
const seedRow = async (dbId, rowName, properties) => (await client.createRow(dbId, { name: rowName, properties })).id;
|
|
741
|
+
// Initiatives first (the tasks link back to them)…
|
|
742
|
+
const revamp = await seedRow(initiativesDbId, 'Onboarding revamp', { p_status: 'opt_track' });
|
|
743
|
+
const perf = await seedRow(initiativesDbId, 'Performance push', { p_status: 'opt_risk' });
|
|
744
|
+
const billing = await seedRow(initiativesDbId, 'Billing v2', { p_status: 'opt_shipped' });
|
|
745
|
+
// …then the tasks: linked 1:n, dated for the timeline, chained by `Blocked by`.
|
|
746
|
+
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) } });
|
|
747
|
+
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] });
|
|
748
|
+
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) } });
|
|
749
|
+
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] });
|
|
750
|
+
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) } });
|
|
751
|
+
// Mirror the reverse side of the two-way link. Seeding writes both sides
|
|
752
|
+
// explicitly — the live mirror only runs on relation-cell edits. (updateRow
|
|
753
|
+
// replaces the whole properties bag, so re-send the status too.)
|
|
754
|
+
await client.updateRow(initiativesDbId, revamp, { properties: { p_status: 'opt_track', p_tasks: [t1, t2] } });
|
|
755
|
+
await client.updateRow(initiativesDbId, perf, { properties: { p_status: 'opt_risk', p_tasks: [t3, t4] } });
|
|
756
|
+
await client.updateRow(initiativesDbId, billing, { properties: { p_status: 'opt_shipped', p_tasks: [t5] } });
|
|
757
|
+
return page;
|
|
758
|
+
};
|
|
759
|
+
// ── 📊 Dashboard ─────────────────────────────────────────────────────────────
|
|
760
|
+
// A composite dashboard: a KPI row + DB-backed bar/pie/trend charts laid out in
|
|
761
|
+
// the 12-col column blocks, all reading LIVE from a sample "sales" database the
|
|
762
|
+
// template also seeds (a sub-page). The charts are ordinary in-doc `kitchart`
|
|
763
|
+
// blocks in DATABASE source mode (DASH-3) — their `dbId` is stamped at
|
|
764
|
+
// instantiation, when the sample database's id is minted. Unlike the database
|
|
765
|
+
// fixtures you land on a composed DOCUMENT, not a table/board.
|
|
766
|
+
/** The sample sales database the dashboard charts. One row = one deal, with a
|
|
767
|
+
* Region/Channel/Stage/Quarter to group by and Amount/Units to total. */
|
|
768
|
+
const SALES_SCHEMA = {
|
|
769
|
+
properties: [
|
|
770
|
+
{
|
|
771
|
+
id: 'p_region',
|
|
772
|
+
name: 'Region',
|
|
773
|
+
type: 'select',
|
|
774
|
+
options: [
|
|
775
|
+
{ id: 'opt_north', label: 'North', color: 'blue' },
|
|
776
|
+
{ id: 'opt_south', label: 'South', color: 'green' },
|
|
777
|
+
{ id: 'opt_east', label: 'East', color: 'orange' },
|
|
778
|
+
{ id: 'opt_west', label: 'West', color: 'purple' },
|
|
779
|
+
],
|
|
780
|
+
},
|
|
781
|
+
{
|
|
782
|
+
id: 'p_channel',
|
|
783
|
+
name: 'Channel',
|
|
784
|
+
type: 'select',
|
|
785
|
+
options: [
|
|
786
|
+
{ id: 'opt_online', label: 'Online', color: 'blue' },
|
|
787
|
+
{ id: 'opt_retail', label: 'Retail', color: 'pink' },
|
|
788
|
+
{ id: 'opt_partner', label: 'Partner', color: 'yellow' },
|
|
789
|
+
],
|
|
790
|
+
},
|
|
791
|
+
{
|
|
792
|
+
id: 'p_stage',
|
|
793
|
+
name: 'Stage',
|
|
794
|
+
type: 'status',
|
|
795
|
+
options: [
|
|
796
|
+
{ id: 'opt_pipeline', label: 'Pipeline', color: 'gray', group: 'todo' },
|
|
797
|
+
{ id: 'opt_committed', label: 'Committed', color: 'blue', group: 'in_progress' },
|
|
798
|
+
{ id: 'opt_won', label: 'Won', color: 'green', group: 'complete' },
|
|
799
|
+
],
|
|
800
|
+
},
|
|
801
|
+
{
|
|
802
|
+
id: 'p_quarter',
|
|
803
|
+
name: 'Quarter',
|
|
804
|
+
type: 'select',
|
|
805
|
+
options: [
|
|
806
|
+
{ id: 'opt_q1', label: 'Q1', color: 'gray' },
|
|
807
|
+
{ id: 'opt_q2', label: 'Q2', color: 'gray' },
|
|
808
|
+
{ id: 'opt_q3', label: 'Q3', color: 'gray' },
|
|
809
|
+
{ id: 'opt_q4', label: 'Q4', color: 'gray' },
|
|
810
|
+
],
|
|
811
|
+
},
|
|
812
|
+
{ id: 'p_amount', name: 'Amount', type: 'number', numberDisplay: 'number' },
|
|
813
|
+
{ id: 'p_units', name: 'Units', type: 'number' },
|
|
814
|
+
],
|
|
815
|
+
views: [
|
|
816
|
+
// The data page opens on the table; a bar view backs it (and exercises the
|
|
817
|
+
// DB chart-view kit engine on the sample data itself).
|
|
818
|
+
{ id: 'v_table', name: 'Table', type: 'table', filters: [], sorts: [] },
|
|
819
|
+
{ id: 'v_bar', name: 'By region', type: 'bar', filters: [], sorts: [], groupByPropertyId: 'p_region', aggregate: { type: 'sum', propertyId: 'p_amount' } },
|
|
820
|
+
],
|
|
821
|
+
};
|
|
822
|
+
const SALES_ROWS = [
|
|
823
|
+
{ name: 'Northwind renewal', properties: { p_region: 'opt_north', p_channel: 'opt_online', p_stage: 'opt_won', p_quarter: 'opt_q1', p_amount: 24000, p_units: 120 } },
|
|
824
|
+
{ name: 'Acme retail order', properties: { p_region: 'opt_north', p_channel: 'opt_retail', p_stage: 'opt_committed', p_quarter: 'opt_q2', p_amount: 18000, p_units: 90 } },
|
|
825
|
+
{ name: 'Globex expansion', properties: { p_region: 'opt_south', p_channel: 'opt_online', p_stage: 'opt_won', p_quarter: 'opt_q1', p_amount: 31000, p_units: 150 } },
|
|
826
|
+
{ name: 'Initech pilot', properties: { p_region: 'opt_south', p_channel: 'opt_partner', p_stage: 'opt_pipeline', p_quarter: 'opt_q3', p_amount: 12000, p_units: 60 } },
|
|
827
|
+
{ name: 'Umbrella upsell', properties: { p_region: 'opt_east', p_channel: 'opt_online', p_stage: 'opt_committed', p_quarter: 'opt_q2', p_amount: 27000, p_units: 130 } },
|
|
828
|
+
{ name: 'Soylent reorder', properties: { p_region: 'opt_east', p_channel: 'opt_retail', p_stage: 'opt_won', p_quarter: 'opt_q4', p_amount: 22000, p_units: 110 } },
|
|
829
|
+
{ name: 'Hooli trial', properties: { p_region: 'opt_west', p_channel: 'opt_partner', p_stage: 'opt_pipeline', p_quarter: 'opt_q3', p_amount: 9000, p_units: 45 } },
|
|
830
|
+
{ name: 'Stark contract', properties: { p_region: 'opt_west', p_channel: 'opt_online', p_stage: 'opt_won', p_quarter: 'opt_q4', p_amount: 35000, p_units: 170 } },
|
|
831
|
+
{ name: 'Wayne partnership', properties: { p_region: 'opt_north', p_channel: 'opt_partner', p_stage: 'opt_committed', p_quarter: 'opt_q2', p_amount: 15000, p_units: 70 } },
|
|
832
|
+
{ name: 'Cyberdyne restock', properties: { p_region: 'opt_south', p_channel: 'opt_retail', p_stage: 'opt_won', p_quarter: 'opt_q1', p_amount: 20000, p_units: 100 } },
|
|
833
|
+
{ name: 'Tyrell evaluation', properties: { p_region: 'opt_east', p_channel: 'opt_online', p_stage: 'opt_pipeline', p_quarter: 'opt_q3', p_amount: 14000, p_units: 68 } },
|
|
834
|
+
{ name: 'Massive Dynamic order', properties: { p_region: 'opt_west', p_channel: 'opt_retail', p_stage: 'opt_committed', p_quarter: 'opt_q4', p_amount: 17000, p_units: 82 } },
|
|
835
|
+
];
|
|
836
|
+
/** A database-bound `kitchart` block (DASH-3 source mode). `dbId` is stamped at
|
|
837
|
+
* instantiation; `count` needs no numeric property, so `aggProp` is optional. A
|
|
838
|
+
* one-line `description` keeps the edit view clean (no ghost "Add a description…"). */
|
|
839
|
+
const dashboardChart = (dbId, id, kind, title, description, groupBy, aggType, aggProp) => ({
|
|
840
|
+
id,
|
|
841
|
+
type: 'kitchart',
|
|
842
|
+
props: { kind, title, description, sourceMode: 'database', dbId, dbGroupBy: groupBy, dbAggType: aggType, ...(aggProp ? { dbAggProp: aggProp } : {}) },
|
|
843
|
+
});
|
|
844
|
+
/** The dashboard document: a leading guidance callout, a KPI row across the top
|
|
845
|
+
* (three tiles in a 12-col columns block), then the bar+pie pair and a
|
|
846
|
+
* full-width quarterly trend — every chart bound to the seeded sales database. */
|
|
847
|
+
const dashboardBlocks = (dbId, guidance) => {
|
|
848
|
+
const chart = (id, kind, title, description, groupBy, aggType, aggProp) => dashboardChart(dbId, id, kind, title, description, groupBy, aggType, aggProp);
|
|
849
|
+
return [
|
|
850
|
+
guidanceCallout('db-guide', guidance),
|
|
851
|
+
{ id: 'db-h1', type: 'heading', text: [{ t: 'This quarter at a glance' }], props: { level: 2 } },
|
|
852
|
+
// KPI row — three number tiles across the top (the DASH-5 `kpi` kind). Each
|
|
853
|
+
// folds a grouped DB series to one grand total; the tile's title names it.
|
|
854
|
+
{
|
|
855
|
+
id: 'db-kpis',
|
|
856
|
+
type: 'columns',
|
|
857
|
+
children: [
|
|
858
|
+
{ id: 'db-kc1', type: 'column', props: { span: 4 }, children: [chart('db-k-rev', 'kpi', 'Total revenue (£)', 'Summed across every region', 'p_region', 'sum', 'p_amount')] },
|
|
859
|
+
{ id: 'db-kc2', type: 'column', props: { span: 4 }, children: [chart('db-k-deals', 'kpi', 'Deals', 'Every row, across all stages', 'p_stage', 'count')] },
|
|
860
|
+
{ id: 'db-kc3', type: 'column', props: { span: 4 }, children: [chart('db-k-units', 'kpi', 'Units sold', 'Summed across every channel', 'p_channel', 'sum', 'p_units')] },
|
|
861
|
+
],
|
|
862
|
+
},
|
|
863
|
+
{ id: 'db-h2', type: 'heading', text: [{ t: 'Breakdown' }], props: { level: 2 } },
|
|
864
|
+
// Bar + pie side by side in a 6/6 split.
|
|
865
|
+
{
|
|
866
|
+
id: 'db-cols',
|
|
867
|
+
type: 'columns',
|
|
868
|
+
children: [
|
|
869
|
+
{ id: 'db-cl', type: 'column', props: { span: 6 }, children: [chart('db-bar', 'bar', 'Revenue by region (£)', 'Amount summed per sales region', 'p_region', 'sum', 'p_amount')] },
|
|
870
|
+
{ id: 'db-cr', type: 'column', props: { span: 6 }, children: [chart('db-pie', 'pie', 'Deals by channel', 'Share of deals won online, retail and via partners', 'p_channel', 'count')] },
|
|
871
|
+
],
|
|
872
|
+
},
|
|
873
|
+
// A full-width quarterly trend closes the dashboard.
|
|
874
|
+
chart('db-line', 'line', 'Revenue by quarter (£)', 'Amount summed by quarter, Q1 → Q4', 'p_quarter', 'sum', 'p_amount'),
|
|
875
|
+
// Trailing pointer: how to ADD a chart (distinct from the lead callout, which
|
|
876
|
+
// is about editing the data). The slash menu inserts a Chart, then its ⚙
|
|
877
|
+
// Source toggle switches it to Database — there is no "/chart → Database".
|
|
878
|
+
{ id: 'db-note', type: 'callout', text: [{ t: 'Want another cut of the data? Type /chart to insert one, then switch its Source to Database in the chart’s ⚙ settings and pick this database.' }], props: { variant: 'info' } },
|
|
879
|
+
];
|
|
880
|
+
};
|
|
881
|
+
/** Build the dashboard document, then seed the sample sales database it charts.
|
|
882
|
+
* The dashboard host page is saved FIRST (so its charts' `dbId` is the minted
|
|
883
|
+
* sample-db id), and the sample database lands on a sub-page. */
|
|
884
|
+
const createDashboard = async (client, name, guidance = GUIDANCE.dashboard) => {
|
|
885
|
+
const dataDbId = globalThis.crypto.randomUUID();
|
|
886
|
+
const dataName = `${name} data`;
|
|
887
|
+
const page = await client.savePage({
|
|
888
|
+
name,
|
|
889
|
+
data: { editorjs: { blocks: [] }, values: [], names: [], editor: 'blocks', blockdoc: { blocks: dashboardBlocks(dataDbId, guidance) } },
|
|
890
|
+
});
|
|
891
|
+
const dataPage = await client.savePage({ name: dataName, data: emptySnapshot([]), parentId: page.id });
|
|
892
|
+
await client.createDatabase({ id: dataDbId, pageId: dataPage.id, name: dataName, schema: SALES_SCHEMA });
|
|
893
|
+
for (const row of SALES_ROWS) {
|
|
894
|
+
let rowName = row.name;
|
|
895
|
+
for (let attempt = 2;; attempt += 1) {
|
|
896
|
+
try {
|
|
897
|
+
await client.createRow(dataDbId, { ...row, name: rowName });
|
|
898
|
+
break;
|
|
899
|
+
}
|
|
900
|
+
catch {
|
|
901
|
+
if (attempt > 5) {
|
|
902
|
+
await client.createRow(dataDbId, { ...row, name: null });
|
|
903
|
+
break;
|
|
904
|
+
}
|
|
905
|
+
rowName = `${row.name} ${attempt}`;
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
return page;
|
|
910
|
+
};
|
|
488
911
|
// ── The gallery ──────────────────────────────────────────────────────────────
|
|
489
912
|
/** Create a block-editor template page from a JSON block projection. */
|
|
490
913
|
const createBlockDocPage = (blocks) => (client, name) => client.savePage({ name, data: { editorjs: { blocks: [] }, values: [], names: [], editor: 'blocks', blockdoc: { blocks } } });
|
|
491
914
|
/** Create a database template: host page + database + sample rows. Names are
|
|
492
915
|
* not unique, so a plain create always lands; the retry ladder below survives
|
|
493
|
-
* transient failures (untitled as a last resort).
|
|
494
|
-
|
|
495
|
-
|
|
916
|
+
* transient failures (untitled as a last resort). `guide` (id + canonical
|
|
917
|
+
* English text) puts the standardized guidance callout on the host page —
|
|
918
|
+
* where a database template's doc surface renders, above the view. */
|
|
919
|
+
const createDatabasePage = (schema, rows, guide) => async (client, name, guidance) => {
|
|
920
|
+
const page = await client.savePage({ name, data: guidanceSnapshot(guide && { id: guide.id, text: guidance ?? guide.text }) });
|
|
496
921
|
const db = await client.createDatabase({ pageId: page.id, name, schema });
|
|
497
922
|
for (const row of rows) {
|
|
498
923
|
let rowName = row.name;
|
|
@@ -512,14 +937,34 @@ const createDatabasePage = (schema, rows) => async (client, name) => {
|
|
|
512
937
|
}
|
|
513
938
|
return page;
|
|
514
939
|
};
|
|
940
|
+
/** A fresh copy of the sample document under its own gallery name. It already
|
|
941
|
+
* opens with its own intro paragraph (`sample-intro`), so — unlike the database
|
|
942
|
+
* fixtures — it carries no standardized guidance callout of its own (that would
|
|
943
|
+
* double-guide, stacking a near-duplicate lead above the intro). */
|
|
944
|
+
const createCompoundGrowth = (client, name) => {
|
|
945
|
+
const input = buildSampleDocument();
|
|
946
|
+
return client.savePage({ ...input, name });
|
|
947
|
+
};
|
|
515
948
|
export const PAGE_TEMPLATES = [
|
|
516
949
|
{ id: 'grocery-tracker', icon: '🛒', pageName: 'Grocery price tracker', tags: ['interactive', 'slides'], create: createBlockDocPage(GROCERY_BLOCKS) },
|
|
517
|
-
{ id: 'task-board', icon: '🗂️', pageName: 'Project task board', tags: ['database'], create: createDatabasePage(TASK_BOARD_SCHEMA, TASK_BOARD_ROWS) },
|
|
518
|
-
{ id: 'reading-list', icon: '📚', pageName: 'Reading list', tags: ['database'], create: createDatabasePage(READING_SCHEMA, READING_ROWS) },
|
|
950
|
+
{ 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 }) },
|
|
951
|
+
{ id: 'reading-list', icon: '📚', pageName: 'Reading list', tags: ['database'], guidance: GUIDANCE.readingList, create: createDatabasePage(READING_SCHEMA, READING_ROWS, { id: 'rl-guide', text: GUIDANCE.readingList }) },
|
|
519
952
|
{ id: 'project-intake', icon: '📋', pageName: 'Project intake', tags: ['interactive', 'slides'], create: createBlockDocPage(PROJECT_INTAKE_BLOCKS) },
|
|
520
953
|
{ id: 'savings-planner', icon: '💰', pageName: 'Savings & investing', tags: ['interactive', 'slides'], create: createBlockDocPage(SAVINGS_BLOCKS) },
|
|
521
|
-
{ id: 'roadmap', icon: '🗺️', pageName: 'Product roadmap', tags: ['database'], create: createDatabasePage(ROADMAP_SCHEMA, ROADMAP_ROWS) },
|
|
522
|
-
{ id: 'field-map', icon: '📍', pageName: 'Field map', tags: ['database'], create: createDatabasePage(FIELD_MAP_SCHEMA, FIELD_MAP_ROWS) },
|
|
954
|
+
{ id: 'roadmap', icon: '🗺️', pageName: 'Product roadmap', tags: ['database'], guidance: GUIDANCE.roadmap, create: createDatabasePage(ROADMAP_SCHEMA, ROADMAP_ROWS, { id: 'rm-guide', text: GUIDANCE.roadmap }) },
|
|
955
|
+
{ 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 }) },
|
|
956
|
+
{ id: 'pitch-deck', icon: '📽️', pageName: 'Pitch deck', tags: ['interactive', 'slides'], create: createBlockDocPage(PITCH_DECK_BLOCKS) },
|
|
957
|
+
{ id: 'team-status', icon: '🚦', pageName: 'Team status dashboard', tags: ['interactive'], create: createBlockDocPage(TEAM_STATUS_BLOCKS) },
|
|
958
|
+
{ id: 'product-hq', icon: '🎯', pageName: 'Product HQ', tags: ['database'], guidance: GUIDANCE.productHq, create: createProductHq },
|
|
959
|
+
// A composite dashboard: KPI tiles + DB-backed charts over a seeded sales
|
|
960
|
+
// database. Tagged `interactive` (you land on a document, not a table), so it
|
|
961
|
+
// groups under Interactive documents; the description names its data backing.
|
|
962
|
+
{ id: 'dashboard', icon: '📊', pageName: 'Sales dashboard', tags: ['interactive'], guidance: GUIDANCE.dashboard, create: createDashboard },
|
|
963
|
+
// The classic sample document, folded into the gallery. Unlike the Home
|
|
964
|
+
// starter's open-or-create (which targets the canonical sample name and never
|
|
965
|
+
// overwrites), the gallery card always mints a FRESH copy under its own
|
|
966
|
+
// display name — the two entry points never race or shadow each other.
|
|
967
|
+
{ id: 'compound-growth', icon: '📈', pageName: 'Compound growth', tags: ['interactive'], create: createCompoundGrowth },
|
|
523
968
|
];
|
|
524
969
|
/** Courtesy numbering (names are not unique): a second instance becomes
|
|
525
970
|
* `name 2`, `name 3`… so repeated instantiations stay tellable-apart. */
|
|
@@ -536,13 +981,15 @@ async function availableName(client, base) {
|
|
|
536
981
|
/**
|
|
537
982
|
* Instantiate a template: pick a distinct display name (courtesy numbering —
|
|
538
983
|
* duplicates are allowed but unhelpful for ready-made pages) and build the page
|
|
539
|
-
* through the client, retrying transient failures.
|
|
984
|
+
* through the client, retrying transient failures. `opts.guidance` localizes
|
|
985
|
+
* the leading guidance callout of templates that carry one (the gallery passes
|
|
986
|
+
* the user's locale text; absent, the canonical English default applies).
|
|
540
987
|
*/
|
|
541
|
-
export async function instantiateTemplate(client, template) {
|
|
988
|
+
export async function instantiateTemplate(client, template, opts) {
|
|
542
989
|
let name = await availableName(client, template.pageName);
|
|
543
990
|
for (let attempt = 0;; attempt += 1) {
|
|
544
991
|
try {
|
|
545
|
-
return await template.create(client, name);
|
|
992
|
+
return await template.create(client, name, opts?.guidance);
|
|
546
993
|
}
|
|
547
994
|
catch (err) {
|
|
548
995
|
// A concurrent create can win the name between the check and the save;
|