@obelism/content 0.1.1 → 0.3.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/README.md CHANGED
@@ -55,12 +55,35 @@ bridge.on('update', ({ entry }) => render(entry)) // unsaved edits, as typed
55
55
 
56
56
  Mark blocks so editors can click them: `<section {...editable(block)}>`
57
57
  (adds `data-cms-uid`). Without an `update` listener the page reloads after
58
- each save. It replaces the `<script src=".../bridge.js">` tag; when a page has
59
- both, it wraps the script's bridge instead of starting a second one. React
60
- sites use `@obelism/content-react`.
58
+ each save. React sites use `@obelism/content-react`.
59
+
60
+ Without a bundler, the same bridge (prompt box included) is one script tag:
61
+
62
+ ```html
63
+ <script src="https://content.obelism.studio/bridge.js"></script>
64
+ <script>
65
+ ObelismBridge.on('update', ({ entry }) => render(entry))
66
+ </script>
67
+ ```
68
+
69
+ The CMS serves this package's `dist/bridge.global.js` there; npm CDNs serve
70
+ it too (then add `data-url` if your CMS isn't content.obelism.studio).
71
+ `data-prompt="false"` leaves the prompt box out. The tag puts the bridge on
72
+ `window.ObelismBridge` and `getContentBridge` / `mountPromptBox` on
73
+ `window.ObelismContent`; a bundled copy on the same page shares its bridge.
61
74
 
62
75
  ## Prompt editing
63
76
 
77
+ The prompt box is plain DOM in a shadow root, no framework. Start the
78
+ bridge with `prompt: false` to leave it out, and mount it yourself later:
79
+
80
+ ```ts
81
+ import { getContentBridge, mountPromptBox } from '@obelism/content/bridge'
82
+
83
+ const bridge = getContentBridge({ prompt: false })
84
+ const unmount = mountPromptBox(document, bridge, { labels: { open: 'Ask' } })
85
+ ```
86
+
64
87
  Inside the visual editor, people who may edit get a prompt box at the bottom
65
88
  of the page: they type what to change and it shows on the page right away,
66
89
  unsaved, with undo. It acts on the selected block, or the whole entry. The
@@ -77,6 +100,17 @@ const { summary } = await bridge.prompt('Make the heading shorter')
77
100
  bridge.undo()
78
101
  ```
79
102
 
103
+ On a site signed in to edit (`@obelism/content/site`), the box also has **New
104
+ page**: a path (`/pricing`, or under an existing page) and what the page is
105
+ for. The CMS writes the page like the one the editor is on and stores it as a
106
+ draft, and the bridge opens it at `/` + its path. When your routes put CMS
107
+ pages elsewhere, say where:
108
+
109
+ ```ts
110
+ getContentBridge({ pagePath: (path) => `/en/${path}` })
111
+ await bridge.createPage('/pricing', 'Our plans, for agencies') // or your own UI
112
+ ```
113
+
80
114
  The CMS runs prompts on Workers AI. To use your own model, set a prompt
81
115
  endpoint under Space settings → Prompt editing and answer it with
82
116
  `createPromptHandler` (it verifies the signed request; you call the model):
@@ -98,12 +132,50 @@ export const POST = createPromptHandler({
98
132
  - `richTextToHtml(doc, { renderEntry?, renderAsset? })`, `richTextToPlainText(doc)`
99
133
  - `imageUrl(asset, { w, h, fit, format, q })`, `imageSrcSet(asset, [480, 960])`
100
134
 
135
+ ## A/B tests and feature flags
136
+
137
+ Any blocks field can hold the built-in improve block (`ImproveBlock`,
138
+ `_type: '_improve'`): one list of blocks per option of an improve test or
139
+ flag. Ask improve's SDK for the visitor's option and render that variant:
140
+
141
+ ```ts
142
+ import { isImproveBlock, pickImproveVariant } from '@obelism/content'
143
+
144
+ // `improve` is improve's SDK (@obelism/improve-sdk), set up for the visitor.
145
+ const blocks = layout.flatMap((block) => {
146
+ if (!isImproveBlock(block)) return [block]
147
+ const option =
148
+ block.kind === 'flag'
149
+ ? improve.getFlagValue(block.slug)
150
+ : improve.getTestValue(block.slug)
151
+ return pickImproveVariant(block, option)
152
+ })
153
+ ```
154
+
155
+ `pickImproveVariant(block, option, fallback?)` falls back to `fallback`'s
156
+ variant, else the first one, when the option is `null` (the test isn't
157
+ active or the visitor isn't in it) or has no variant.
158
+
101
159
  ## CLI
102
160
 
103
- Keeps a content model file in Git in step with an environment.
161
+ Start a site's repository on a space, in its root:
162
+
163
+ ```bash
164
+ export CONTENT_MANAGEMENT_TOKEN=cms_m_… # a management token, Space settings
165
+ npx @obelism/content init
166
+ ```
167
+
168
+ It writes the model file (`content/content-model.json`), an example of what
169
+ the delivery API answers for every content type (`content/examples/`, with
170
+ the A/B test or flag block while your organization has improve on),
171
+ installs `@obelism/content` with your package manager, and adds the token variables to
172
+ `.env.example`. Options: `--dir` (default `content`), `--url`,
173
+ `--skip-install`. Agents get the same steps from the CMS's
174
+ `getProjectSetup` MCP tool.
175
+
176
+ Then keep the model file in Git in step with an environment:
104
177
 
105
178
  ```bash
106
- export CONTENT_MANAGEMENT_TOKEN=cms_m_…
107
179
  npx @obelism/content model pull # write content/content-model.json
108
180
  npx @obelism/content model plan # what applying would change
109
181
  npx @obelism/content model apply # apply it, all or nothing
@@ -1,9 +1,190 @@
1
1
  #!/usr/bin/env node
2
2
  var node_util = require('node:util');
3
3
  var config = require('../config-ci2hxmas.js');
4
+ var node_child_process = require('node:child_process');
4
5
  var promises = require('node:fs/promises');
5
6
  var node_path = require('node:path');
6
7
 
8
+ // Blocks inside blocks stop here, so a type that allows itself ends.
9
+ const MAX_DEPTH = 3;
10
+ const EXAMPLE_DATE = '2026-01-01';
11
+ const EXAMPLE_DATE_TIME = '2026-01-01T09:00:00.000Z';
12
+ // An image unless the field only takes other files; then its first exact
13
+ // type, or a PDF.
14
+ const exampleAsset = (mimeTypes)=>{
15
+ const image = !mimeTypes || mimeTypes.some((type)=>type.startsWith('image/'));
16
+ const exact = mimeTypes?.find((type)=>/^[a-z]+\/[^*]+$/.test(type));
17
+ const mimeType = image ? 'image/jpeg' : exact ?? 'application/pdf';
18
+ const filename = image ? 'example.jpg' : `example.${mimeType.split('/')[1]?.split('+')[0] ?? 'bin'}`;
19
+ return {
20
+ id: 'asset_example',
21
+ url: `https://content.obelism.studio/files/asset_example/${filename}`,
22
+ filename,
23
+ mimeType,
24
+ size: 120_000,
25
+ width: image ? 1600 : null,
26
+ height: image ? 900 : null,
27
+ title: null,
28
+ alt: image ? 'An example image' : null
29
+ };
30
+ };
31
+ const exampleRichText = (name)=>({
32
+ type: 'doc',
33
+ content: [
34
+ {
35
+ type: 'paragraph',
36
+ content: [
37
+ {
38
+ type: 'text',
39
+ text: `Example ${name}, with `
40
+ },
41
+ {
42
+ type: 'text',
43
+ text: 'bold',
44
+ marks: [
45
+ {
46
+ type: 'bold'
47
+ }
48
+ ]
49
+ },
50
+ {
51
+ type: 'text',
52
+ text: ' text.'
53
+ }
54
+ ]
55
+ }
56
+ ]
57
+ });
58
+ const truncate = (text, maxLength)=>maxLength === undefined ? text : text.slice(0, maxLength);
59
+ const buildExamples = (model, improve)=>{
60
+ const types = model.contentTypes;
61
+ const blockTypes = types.filter(({ kind })=>kind === 'block');
62
+ const entryTypes = types.filter(({ kind })=>kind !== 'block');
63
+ // Uids are unique within one example, like within one entry.
64
+ let next = 0;
65
+ const uid = ()=>`example${++next}`;
66
+ const allowedBlocks = (allowed)=>allowed ? blockTypes.filter(({ apiId })=>allowed.includes(apiId)) : blockTypes;
67
+ const improveBlock = (allowed, depth)=>{
68
+ const source = improve.sources[0] ?? {
69
+ kind: 'test',
70
+ slug: 'example-test',
71
+ options: [
72
+ {
73
+ slug: 'control',
74
+ name: 'Control'
75
+ },
76
+ {
77
+ slug: 'variation',
78
+ name: 'Variation'
79
+ }
80
+ ]
81
+ };
82
+ const first = allowedBlocks(allowed)[0];
83
+ return {
84
+ _uid: uid(),
85
+ _type: '_improve',
86
+ kind: source.kind,
87
+ slug: source.slug,
88
+ variants: Object.fromEntries(source.options.map((option)=>[
89
+ option.slug,
90
+ first && depth < MAX_DEPTH ? [
91
+ block(first, depth + 1)
92
+ ] : []
93
+ ]))
94
+ };
95
+ };
96
+ const value = (field, depth)=>{
97
+ const name = field.name.toLowerCase();
98
+ switch(field.type){
99
+ case 'text':
100
+ return truncate(`Example ${name}`, field.maxLength);
101
+ case 'textarea':
102
+ return truncate(`Example ${name}. A sentence or two of plain text.`, field.maxLength);
103
+ case 'richtext':
104
+ return exampleRichText(name);
105
+ case 'number':
106
+ return field.min ?? (field.integer ? 1 : 1.5);
107
+ case 'boolean':
108
+ return true;
109
+ case 'datetime':
110
+ return field.withTime ? EXAMPLE_DATE_TIME : EXAMPLE_DATE;
111
+ case 'select':
112
+ {
113
+ const first = field.options[0]?.value ?? null;
114
+ return field.multiple ? first ? [
115
+ first
116
+ ] : [] : first;
117
+ }
118
+ case 'asset':
119
+ {
120
+ const asset = exampleAsset(field.mimeTypes);
121
+ return field.multiple ? [
122
+ asset
123
+ ] : asset;
124
+ }
125
+ case 'reference':
126
+ {
127
+ const target = field.contentTypes?.[0] ?? entryTypes[0]?.apiId ?? 'page';
128
+ const stub = {
129
+ id: 'entry_example',
130
+ contentType: target
131
+ };
132
+ return field.multiple ? [
133
+ stub
134
+ ] : stub;
135
+ }
136
+ case 'blocks':
137
+ {
138
+ if (depth >= MAX_DEPTH) return [];
139
+ const allowed = allowedBlocks(field.allowedBlocks);
140
+ // At the top, one of every block the field allows (and the
141
+ // improve block); deeper, only the first.
142
+ const blocks = depth === 0 ? allowed.map((type)=>block(type, depth + 1)) : allowed.slice(0, 1).map((type)=>block(type, depth + 1));
143
+ return depth === 0 && improve.enabled ? [
144
+ ...blocks,
145
+ improveBlock(field.allowedBlocks, depth + 1)
146
+ ] : blocks;
147
+ }
148
+ case 'json':
149
+ return {};
150
+ }
151
+ };
152
+ const fields = (type, depth)=>Object.fromEntries(type.fields.map((field)=>[
153
+ field.apiId,
154
+ value(field, depth)
155
+ ]));
156
+ const block = (type, depth)=>({
157
+ _uid: uid(),
158
+ _type: type.apiId,
159
+ ...fields(type, depth)
160
+ });
161
+ const examples = {};
162
+ for (const type of types){
163
+ next = 0;
164
+ if (type.kind === 'block') {
165
+ examples[type.apiId] = block(type, 0);
166
+ continue;
167
+ }
168
+ const slug = type.kind === 'page' ? 'example' : null;
169
+ examples[type.apiId] = {
170
+ id: 'entry_example',
171
+ contentType: type.apiId,
172
+ name: `Example ${type.name.toLowerCase()}`,
173
+ slug,
174
+ path: slug,
175
+ publishedAt: EXAMPLE_DATE_TIME,
176
+ firstPublishedAt: EXAMPLE_DATE_TIME,
177
+ updatedAt: EXAMPLE_DATE_TIME,
178
+ fields: fields(type, 0)
179
+ };
180
+ }
181
+ if (improve.enabled) {
182
+ next = 0;
183
+ examples._improve = improveBlock(undefined, 0);
184
+ }
185
+ return examples;
186
+ };
187
+
7
188
  // Turns a model plan from the management API into text for a terminal, or
8
189
  // Markdown for a GitHub step summary or pull request comment.
9
190
  const ACTION_SIGN = {
@@ -146,15 +327,205 @@ const runModelCommand = async ({ command, file, url, token, prune, log = console
146
327
  return !ok || (plan.issues?.length ?? 0) > 0 ? 1 : 0;
147
328
  };
148
329
 
149
- const USAGE = `Usage: content model <pull|plan|apply> [--file path] [--url origin] [--prune]
330
+ const PACKAGE_MANAGERS = [
331
+ {
332
+ lockfile: 'pnpm-lock.yaml',
333
+ command: 'pnpm',
334
+ add: [
335
+ 'add'
336
+ ]
337
+ },
338
+ {
339
+ lockfile: 'yarn.lock',
340
+ command: 'yarn',
341
+ add: [
342
+ 'add'
343
+ ]
344
+ },
345
+ {
346
+ lockfile: 'bun.lock',
347
+ command: 'bun',
348
+ add: [
349
+ 'add'
350
+ ]
351
+ },
352
+ {
353
+ lockfile: 'bun.lockb',
354
+ command: 'bun',
355
+ add: [
356
+ 'add'
357
+ ]
358
+ },
359
+ {
360
+ lockfile: 'package-lock.json',
361
+ command: 'npm',
362
+ add: [
363
+ 'install'
364
+ ]
365
+ }
366
+ ];
367
+ const ENV_VARIABLES = [
368
+ [
369
+ 'CONTENT_DELIVERY_TOKEN',
370
+ 'reads published content (cms_d_…)'
371
+ ],
372
+ [
373
+ 'CONTENT_PREVIEW_TOKEN',
374
+ 'also reads drafts, server-side only (cms_p_…)'
375
+ ],
376
+ [
377
+ 'CONTENT_MANAGEMENT_TOKEN',
378
+ 'changes the content model: content model pull|plan|apply (cms_m_…)'
379
+ ]
380
+ ];
381
+ const exists = (path)=>promises.access(path).then(()=>true, ()=>false);
382
+ const runCommand = (command, args, cwd)=>new Promise((resolve)=>{
383
+ const child = node_child_process.spawn(command, args, {
384
+ cwd,
385
+ stdio: 'inherit',
386
+ shell: process.platform === 'win32'
387
+ });
388
+ child.on('error', ()=>resolve(127));
389
+ child.on('close', (code)=>resolve(code ?? 1));
390
+ });
391
+ const writeJson = async (path, value)=>promises.writeFile(path, `${JSON.stringify(value, null, '\t')}\n`);
392
+ // Whether the organization has improve on, and its tests and flags. A CMS
393
+ // without the endpoint has no improve block.
394
+ const readImprove = async (url, token, fetchImpl)=>{
395
+ const response = await fetchImpl(`${url}${config.MANAGEMENT_PATH}/improve`, {
396
+ headers: {
397
+ Authorization: `Bearer ${token}`
398
+ }
399
+ }).catch((error)=>{
400
+ throw new CliError(`Could not reach ${url}: ${error.message}`);
401
+ });
402
+ if (response.status === 404) return {
403
+ enabled: false,
404
+ sources: []
405
+ };
406
+ const json = await response.json().catch(()=>({}));
407
+ if (!response.ok) {
408
+ throw new CliError(String(json.error ?? `${response.status} from ${url}`));
409
+ }
410
+ return {
411
+ enabled: json.enabled === true,
412
+ sources: json.sources ?? []
413
+ };
414
+ };
415
+ const install = async (cwd, log, run)=>{
416
+ const manifestPath = node_path.join(cwd, 'package.json');
417
+ let manifest;
418
+ try {
419
+ manifest = JSON.parse(await promises.readFile(manifestPath, 'utf8'));
420
+ } catch {
421
+ log('No package.json here, so nothing was installed: add @obelism/content to your project yourself.');
422
+ return;
423
+ }
424
+ const dependencies = {
425
+ ...manifest.dependencies,
426
+ ...manifest.devDependencies
427
+ };
428
+ const wanted = [
429
+ '@obelism/content'
430
+ ].filter((name)=>!dependencies[name]);
431
+ if (wanted.length === 0) {
432
+ log('The SDK is already installed.');
433
+ return;
434
+ }
435
+ let manager = PACKAGE_MANAGERS.at(-1);
436
+ for (const candidate of PACKAGE_MANAGERS){
437
+ if (await exists(node_path.join(cwd, candidate.lockfile))) {
438
+ manager = candidate;
439
+ break;
440
+ }
441
+ }
442
+ const args = [
443
+ ...manager.add,
444
+ ...wanted
445
+ ];
446
+ log(`Installing: ${manager.command} ${args.join(' ')}`);
447
+ const code = await run(manager.command, args, cwd);
448
+ if (code !== 0) {
449
+ throw new CliError(`${manager.command} ${args.join(' ')} failed (exit ${code}). The files are written; install the SDK yourself.`);
450
+ }
451
+ };
452
+ // Appends the variables .env.example doesn't have yet. Never values.
453
+ const writeEnvExample = async (cwd, url)=>{
454
+ const path = node_path.join(cwd, '.env.example');
455
+ const current = await promises.readFile(path, 'utf8').catch(()=>'');
456
+ const has = (name)=>new RegExp(`^\\s*${name}\\s*=`, 'm').test(current);
457
+ const lines = [
458
+ ...ENV_VARIABLES.filter(([name])=>!has(name)).flatMap(([name, about])=>[
459
+ `# ${about}`,
460
+ `${name}=`
461
+ ]),
462
+ ...url !== config.DEFAULT_URL && !has('CONTENT_URL') ? [
463
+ `CONTENT_URL=${url}`
464
+ ] : []
465
+ ];
466
+ if (lines.length === 0) return false;
467
+ const header = '# Obelism Content (written by `content init`)';
468
+ const separator = current === '' || current.endsWith('\n') ? '' : '\n';
469
+ await promises.writeFile(path, `${current}${separator}${current ? '\n' : ''}${header}\n${lines.join('\n')}\n`);
470
+ return true;
471
+ };
472
+ const runInitCommand = async ({ url, token, dir, install: shouldInstall, cwd = process.cwd(), log = console.log, fetch: fetchImpl = fetch, run = runCommand })=>{
473
+ const base = node_path.join(cwd, dir);
474
+ const modelFile = node_path.join(base, 'content-model.json');
475
+ await runModelCommand({
476
+ command: 'pull',
477
+ // Relative, so the log names it the way the user would.
478
+ file: node_path.relative(process.cwd(), modelFile) || modelFile,
479
+ url,
480
+ token,
481
+ prune: false,
482
+ log,
483
+ fetch: fetchImpl
484
+ });
485
+ const model = JSON.parse(await promises.readFile(modelFile, 'utf8'));
486
+ const improve = await readImprove(url, token, fetchImpl);
487
+ const examples = buildExamples(model, improve);
488
+ const examplesDir = node_path.join(base, 'examples');
489
+ await promises.mkdir(examplesDir, {
490
+ recursive: true
491
+ });
492
+ for (const [apiId, example] of Object.entries(examples)){
493
+ await writeJson(node_path.join(examplesDir, `${apiId}.json`), example);
494
+ }
495
+ const count = Object.keys(examples).length;
496
+ log(`Wrote ${count} ${count === 1 ? 'example' : 'examples'} to ${node_path.join(dir, 'examples')}${improve.enabled ? ', with the A/B test or flag block (_improve.json): improve is on' : ''}.`);
497
+ if (await writeEnvExample(cwd, url)) {
498
+ log('Added the token variables to .env.example.');
499
+ }
500
+ if (shouldInstall) await install(cwd, log, run);
501
+ log([
502
+ '',
503
+ 'Next:',
504
+ '- Create a delivery token (and a preview token for drafts) in Space settings, and set CONTENT_DELIVERY_TOKEN and CONTENT_PREVIEW_TOKEN.',
505
+ '- Fetch content with ContentClient from @obelism/content; the examples show what each type looks like.',
506
+ ...improve.enabled ? [
507
+ "- Render _improve blocks: ask improve's SDK for the visitor's option (getTestValue / getFlagValue with the block's slug) and render pickImproveVariant(block, option)."
508
+ ] : [],
509
+ `- Keep the model in step with \`content model pull|plan|apply\` (${node_path.join(dir, 'content-model.json')}).`
510
+ ].join('\n'));
511
+ return 0;
512
+ };
150
513
 
151
- pull write the environment's model to the file
152
- plan show what applying the file would change, and what stands in the way
153
- apply apply the file in one transaction, or change nothing
514
+ const USAGE = `Usage: content init [--dir content] [--url origin] [--skip-install]
515
+ content model <pull|plan|apply> [--file path] [--url origin] [--prune]
154
516
 
155
- --file the model file (default content/content-model.json)
156
- --url the CMS (default $CONTENT_URL, else ${config.DEFAULT_URL})
157
- --prune plan and apply also delete content types the file leaves out
517
+ init start a site's repository on the token's environment: the
518
+ model file, an example per content type, the SDK installed
519
+ and the token variables in .env.example
520
+ model pull write the environment's model to the file
521
+ model plan show what applying the file would change, and what stands in the way
522
+ model apply apply the file in one transaction, or change nothing
523
+
524
+ --dir where init writes content-model.json and examples/ (default content)
525
+ --skip-install init leaves dependencies alone
526
+ --file the model file (default content/content-model.json)
527
+ --url the CMS (default $CONTENT_URL, else ${config.DEFAULT_URL})
528
+ --prune plan and apply also delete content types the file leaves out
158
529
 
159
530
  Needs CONTENT_MANAGEMENT_TOKEN: a management token from Space settings.`;
160
531
  const main = async ()=>{
@@ -174,6 +545,14 @@ const main = async ()=>{
174
545
  type: 'boolean',
175
546
  default: false
176
547
  },
548
+ dir: {
549
+ type: 'string',
550
+ default: 'content'
551
+ },
552
+ 'skip-install': {
553
+ type: 'boolean',
554
+ default: false
555
+ },
177
556
  help: {
178
557
  type: 'boolean',
179
558
  short: 'h',
@@ -190,17 +569,26 @@ const main = async ()=>{
190
569
  console.log(USAGE);
191
570
  return 0;
192
571
  }
193
- if (group !== 'model' || command !== 'pull' && command !== 'plan' && command !== 'apply') {
194
- throw new CliError(USAGE, 2);
195
- }
572
+ const isInit = group === 'init' && command === undefined;
573
+ const modelCommand = group === 'model' && (command === 'pull' || command === 'plan' || command === 'apply') ? command : null;
574
+ if (!isInit && !modelCommand) throw new CliError(USAGE, 2);
196
575
  const token = process.env.CONTENT_MANAGEMENT_TOKEN;
197
576
  if (!token) {
198
577
  throw new CliError(`CONTENT_MANAGEMENT_TOKEN is not set.\n\n${USAGE}`, 2);
199
578
  }
579
+ const url = config.normalizeUrl(values.url ?? process.env.CONTENT_URL ?? config.DEFAULT_URL);
580
+ if (!modelCommand) {
581
+ return runInitCommand({
582
+ url,
583
+ token,
584
+ dir: values.dir,
585
+ install: !values['skip-install']
586
+ });
587
+ }
200
588
  return runModelCommand({
201
- command,
589
+ command: modelCommand,
202
590
  file: values.file,
203
- url: config.normalizeUrl(values.url ?? process.env.CONTENT_URL ?? config.DEFAULT_URL),
591
+ url,
204
592
  token,
205
593
  prune: values.prune,
206
594
  summaryFile: process.env.GITHUB_STEP_SUMMARY || undefined
package/dist/bridge.d.mts CHANGED
@@ -24,6 +24,12 @@ type BridgeState = {
24
24
  type PromptResult = {
25
25
  summary: string;
26
26
  };
27
+ type CreatePageResult = {
28
+ summary: string;
29
+ entryId: string;
30
+ path: string;
31
+ url: string;
32
+ };
27
33
  type ContentBridge = {
28
34
  readonly isEditing: boolean;
29
35
  readonly mode: 'editor' | 'site' | null;
@@ -34,6 +40,7 @@ type ContentBridge = {
34
40
  prompt(prompt: string, options?: {
35
41
  uid?: string | null;
36
42
  }): Promise<PromptResult>;
43
+ createPage(path: string, prompt: string): Promise<CreatePageResult>;
37
44
  undo(): void;
38
45
  save(): Promise<void>;
39
46
  publish(): Promise<void>;
@@ -64,19 +71,28 @@ type PromptLabels = {
64
71
  signOut: string;
65
72
  viewingDrafts: string;
66
73
  selectHint: string;
74
+ newPage: string;
75
+ newPageCancel: string;
76
+ newPagePlaceholder: string;
77
+ path: string;
78
+ pathPlaceholder: string;
79
+ creating: string;
80
+ created: string;
67
81
  };
68
82
  declare const DEFAULT_PROMPT_LABELS: PromptLabels;
69
83
  type PromptBoxOptions = {
70
84
  labels?: Partial<PromptLabels> | undefined;
71
85
  };
86
+ declare const mountPromptBox: (doc: Document, bridge: ContentBridge, options?: PromptBoxOptions) => () => void;
72
87
 
73
88
  type BridgeOptions = {
74
89
  url?: string | undefined;
75
90
  prompt?: boolean | PromptBoxOptions | undefined;
91
+ pagePath?: ((path: string) => string) | undefined;
76
92
  };
77
93
  declare const peekContentBridge: () => ContentBridge | null;
78
- declare const getContentBridge: ({ url, prompt, }?: BridgeOptions) => ContentBridge;
94
+ declare const getContentBridge: ({ url, prompt, pagePath, }?: BridgeOptions) => ContentBridge;
79
95
 
80
- export { DEFAULT_PROMPT_LABELS, INITIAL_STATE, getContentBridge, peekContentBridge };
81
- export type { BridgeEvent, BridgeEvents, BridgeListener, BridgeOptions, BridgeState, ContentBridge, PromptBoxOptions, PromptLabels, PromptResult };
96
+ export { DEFAULT_PROMPT_LABELS, INITIAL_STATE, getContentBridge, mountPromptBox, peekContentBridge };
97
+ export type { BridgeEvent, BridgeEvents, BridgeListener, BridgeOptions, BridgeState, ContentBridge, CreatePageResult, PromptBoxOptions, PromptLabels, PromptResult };
82
98
  //# sourceMappingURL=bridge.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"bridge.d.mts","sources":["../src/bridge/createBridge.ts","../src/bridge/promptBox.ts","../src/bridge.ts"],"mappings":";;AAIA,KAAK,YAAY;AACjB;AACA,eAAe,KAAK;AACpB;AACA;AACA,WAAW,MAAM;AACjB,eAAe,MAAM;AACrB,YAAY,WAAW;AACvB;AACA,KAAK,WAAW,SAAS,YAAY;AACrC,KAAK,cAAc,WAAW,WAAW,cAAc,YAAY;AACnE,KAAK,WAAW;AAChB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK,YAAY;AACjB;AACA;AACA,KAAK,aAAa;AAClB;AACA;AACA;AACA;AACA,iBAAiB,WAAW,sBAAsB,cAAc;AAChE,gBAAgB,WAAW;AAC3B;AACA;AACA,QAAQ,OAAO,CAAC,YAAY;AAC5B;AACA,YAAY,OAAO;AACnB,eAAe,OAAO;AACtB;AACA;AACA;AACA;AACA;AAEA,cAAc,aAAa,EAAE,WAAW;;AC9CxC,KAAK,YAAY;AACjB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,cAAc,qBAAqB,EAAE,YAAY;AACjD,KAAK,gBAAgB;AACrB,aAAa,OAAO,CAAC,YAAY;AACjC;;ACnBA,KAAK,aAAa;AAClB;AACA,uBAAuB,gBAAgB;AACvC;AACA,cAAc,iBAAiB,QAAQ,aAAa;AACpD,cAAc,gBAAgB,sBAAsB,aAAa,KAAK,aAAa","names":[]}
1
+ {"version":3,"file":"bridge.d.mts","sources":["../src/bridge/createBridge.ts","../src/bridge/promptBox.ts","../src/bridge.ts"],"mappings":";;AAIA,KAAK,YAAY;AACjB;AACA,eAAe,KAAK;AACpB;AACA;AACA,WAAW,MAAM;AACjB,eAAe,MAAM;AACrB,YAAY,WAAW;AACvB;AACA,KAAK,WAAW,SAAS,YAAY;AACrC,KAAK,cAAc,WAAW,WAAW,cAAc,YAAY;AACnE,KAAK,WAAW;AAChB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK,YAAY;AACjB;AACA;AACA,KAAK,gBAAgB;AACrB;AACA;AACA;AACA;AACA;AACA,KAAK,aAAa;AAClB;AACA;AACA;AACA;AACA,iBAAiB,WAAW,sBAAsB,cAAc;AAChE,gBAAgB,WAAW;AAC3B;AACA;AACA,QAAQ,OAAO,CAAC,YAAY;AAC5B,8CAA8C,OAAO,CAAC,gBAAgB;AACtE;AACA,YAAY,OAAO;AACnB,eAAe,OAAO;AACtB;AACA;AACA;AACA;AACA;AAEA,cAAc,aAAa,EAAE,WAAW;;ACrDxC,KAAK,YAAY;AACjB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,cAAc,qBAAqB,EAAE,YAAY;AACjD,KAAK,gBAAgB;AACrB,aAAa,OAAO,CAAC,YAAY;AACjC;AACA,cAAc,cAAc,QAAQ,QAAQ,UAAU,aAAa,YAAY,gBAAgB;;AC3B/F,KAAK,aAAa;AAClB;AACA,uBAAuB,gBAAgB;AACvC;AACA;AACA,cAAc,iBAAiB,QAAQ,aAAa;AACpD,cAAc,gBAAgB,gCAAgC,aAAa,KAAK,aAAa","names":[]}
package/dist/bridge.d.ts CHANGED
@@ -24,6 +24,12 @@ type BridgeState = {
24
24
  type PromptResult = {
25
25
  summary: string;
26
26
  };
27
+ type CreatePageResult = {
28
+ summary: string;
29
+ entryId: string;
30
+ path: string;
31
+ url: string;
32
+ };
27
33
  type ContentBridge = {
28
34
  readonly isEditing: boolean;
29
35
  readonly mode: 'editor' | 'site' | null;
@@ -34,6 +40,7 @@ type ContentBridge = {
34
40
  prompt(prompt: string, options?: {
35
41
  uid?: string | null;
36
42
  }): Promise<PromptResult>;
43
+ createPage(path: string, prompt: string): Promise<CreatePageResult>;
37
44
  undo(): void;
38
45
  save(): Promise<void>;
39
46
  publish(): Promise<void>;
@@ -64,19 +71,28 @@ type PromptLabels = {
64
71
  signOut: string;
65
72
  viewingDrafts: string;
66
73
  selectHint: string;
74
+ newPage: string;
75
+ newPageCancel: string;
76
+ newPagePlaceholder: string;
77
+ path: string;
78
+ pathPlaceholder: string;
79
+ creating: string;
80
+ created: string;
67
81
  };
68
82
  declare const DEFAULT_PROMPT_LABELS: PromptLabels;
69
83
  type PromptBoxOptions = {
70
84
  labels?: Partial<PromptLabels> | undefined;
71
85
  };
86
+ declare const mountPromptBox: (doc: Document, bridge: ContentBridge, options?: PromptBoxOptions) => () => void;
72
87
 
73
88
  type BridgeOptions = {
74
89
  url?: string | undefined;
75
90
  prompt?: boolean | PromptBoxOptions | undefined;
91
+ pagePath?: ((path: string) => string) | undefined;
76
92
  };
77
93
  declare const peekContentBridge: () => ContentBridge | null;
78
- declare const getContentBridge: ({ url, prompt, }?: BridgeOptions) => ContentBridge;
94
+ declare const getContentBridge: ({ url, prompt, pagePath, }?: BridgeOptions) => ContentBridge;
79
95
 
80
- export { DEFAULT_PROMPT_LABELS, INITIAL_STATE, getContentBridge, peekContentBridge };
81
- export type { BridgeEvent, BridgeEvents, BridgeListener, BridgeOptions, BridgeState, ContentBridge, PromptBoxOptions, PromptLabels, PromptResult };
96
+ export { DEFAULT_PROMPT_LABELS, INITIAL_STATE, getContentBridge, mountPromptBox, peekContentBridge };
97
+ export type { BridgeEvent, BridgeEvents, BridgeListener, BridgeOptions, BridgeState, ContentBridge, CreatePageResult, PromptBoxOptions, PromptLabels, PromptResult };
82
98
  //# sourceMappingURL=bridge.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"bridge.d.ts","sources":["../src/bridge/createBridge.ts","../src/bridge/promptBox.ts","../src/bridge.ts"],"mappings":";;AAIA,KAAK,YAAY;AACjB;AACA,eAAe,KAAK;AACpB;AACA;AACA,WAAW,MAAM;AACjB,eAAe,MAAM;AACrB,YAAY,WAAW;AACvB;AACA,KAAK,WAAW,SAAS,YAAY;AACrC,KAAK,cAAc,WAAW,WAAW,cAAc,YAAY;AACnE,KAAK,WAAW;AAChB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK,YAAY;AACjB;AACA;AACA,KAAK,aAAa;AAClB;AACA;AACA;AACA;AACA,iBAAiB,WAAW,sBAAsB,cAAc;AAChE,gBAAgB,WAAW;AAC3B;AACA;AACA,QAAQ,OAAO,CAAC,YAAY;AAC5B;AACA,YAAY,OAAO;AACnB,eAAe,OAAO;AACtB;AACA;AACA;AACA;AACA;AAEA,cAAc,aAAa,EAAE,WAAW;;AC9CxC,KAAK,YAAY;AACjB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,cAAc,qBAAqB,EAAE,YAAY;AACjD,KAAK,gBAAgB;AACrB,aAAa,OAAO,CAAC,YAAY;AACjC;;ACnBA,KAAK,aAAa;AAClB;AACA,uBAAuB,gBAAgB;AACvC;AACA,cAAc,iBAAiB,QAAQ,aAAa;AACpD,cAAc,gBAAgB,sBAAsB,aAAa,KAAK,aAAa","names":[]}
1
+ {"version":3,"file":"bridge.d.ts","sources":["../src/bridge/createBridge.ts","../src/bridge/promptBox.ts","../src/bridge.ts"],"mappings":";;AAIA,KAAK,YAAY;AACjB;AACA,eAAe,KAAK;AACpB;AACA;AACA,WAAW,MAAM;AACjB,eAAe,MAAM;AACrB,YAAY,WAAW;AACvB;AACA,KAAK,WAAW,SAAS,YAAY;AACrC,KAAK,cAAc,WAAW,WAAW,cAAc,YAAY;AACnE,KAAK,WAAW;AAChB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK,YAAY;AACjB;AACA;AACA,KAAK,gBAAgB;AACrB;AACA;AACA;AACA;AACA;AACA,KAAK,aAAa;AAClB;AACA;AACA;AACA;AACA,iBAAiB,WAAW,sBAAsB,cAAc;AAChE,gBAAgB,WAAW;AAC3B;AACA;AACA,QAAQ,OAAO,CAAC,YAAY;AAC5B,8CAA8C,OAAO,CAAC,gBAAgB;AACtE;AACA,YAAY,OAAO;AACnB,eAAe,OAAO;AACtB;AACA;AACA;AACA;AACA;AAEA,cAAc,aAAa,EAAE,WAAW;;ACrDxC,KAAK,YAAY;AACjB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,cAAc,qBAAqB,EAAE,YAAY;AACjD,KAAK,gBAAgB;AACrB,aAAa,OAAO,CAAC,YAAY;AACjC;AACA,cAAc,cAAc,QAAQ,QAAQ,UAAU,aAAa,YAAY,gBAAgB;;AC3B/F,KAAK,aAAa;AAClB;AACA,uBAAuB,gBAAgB;AACvC;AACA;AACA,cAAc,iBAAiB,QAAQ,aAAa;AACpD,cAAc,gBAAgB,gCAAgC,aAAa,KAAK,aAAa","names":[]}