@obelism/content 0.1.0 → 0.2.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 +11 -0
- package/dist/bridge.d.mts +17 -2
- package/dist/bridge.d.mts.map +1 -1
- package/dist/bridge.d.ts +17 -2
- package/dist/bridge.d.ts.map +1 -1
- package/dist/bridge.js +367 -92
- package/dist/bridge.mjs +367 -92
- package/dist/site.js +4 -0
- package/dist/site.mjs +4 -0
- package/package.json +1 -1
package/dist/bridge.mjs
CHANGED
|
@@ -62,6 +62,7 @@ const onlyOnSite = ()=>Promise.reject(new Error('Save and publish in the visual
|
|
|
62
62
|
// What every bridge without a feature does for it.
|
|
63
63
|
const INERT_METHODS = {
|
|
64
64
|
prompt: inertPrompt,
|
|
65
|
+
createPage: ()=>Promise.reject(new Error('New pages are made on the site itself')),
|
|
65
66
|
undo: ()=>{},
|
|
66
67
|
save: onlyOnSite,
|
|
67
68
|
publish: onlyOnSite,
|
|
@@ -304,48 +305,182 @@ const DEFAULT_PROMPT_LABELS = {
|
|
|
304
305
|
unsaved: 'Unsaved changes',
|
|
305
306
|
signOut: 'Exit editing',
|
|
306
307
|
viewingDrafts: 'Viewing drafts',
|
|
307
|
-
selectHint: 'Click a block to change only that block'
|
|
308
|
+
selectHint: 'Click a block to change only that block',
|
|
309
|
+
newPage: 'New page',
|
|
310
|
+
newPageCancel: 'Edit this page instead',
|
|
311
|
+
newPagePlaceholder: 'Describe the new page, e.g. “a pricing page for agencies”',
|
|
312
|
+
path: 'Path',
|
|
313
|
+
pathPlaceholder: '/pricing',
|
|
314
|
+
creating: 'Writing the page…',
|
|
315
|
+
created: 'Opening the new page…'
|
|
308
316
|
};
|
|
309
|
-
|
|
317
|
+
// Icons (Lucide), drawn in the text colour.
|
|
318
|
+
const ICONS = {
|
|
319
|
+
page: '<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/>',
|
|
320
|
+
newPage: '<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M9 15h6"/><path d="M12 18v-6"/>',
|
|
321
|
+
block: '<rect width="18" height="18" x="3" y="3" rx="2"/><path d="M7 8h10"/><path d="M7 12h6"/>',
|
|
322
|
+
undo: '<path d="M9 14 4 9l5-5"/><path d="M4 9h10.5a5.5 5.5 0 0 1 0 11H11"/>',
|
|
323
|
+
close: '<path d="M18 6 6 18"/><path d="m6 6 12 12"/>',
|
|
324
|
+
send: '<path d="M3.714 3.048a.498.498 0 0 0-.683.627l2.843 7.627a2 2 0 0 1 0 1.396l-2.842 7.627a.498.498 0 0 0 .682.627l18-8.5a.5.5 0 0 0 0-.904z"/><path d="M6 12h16"/>'
|
|
325
|
+
};
|
|
326
|
+
// Obelism's design tokens (obelism.studio/design.md), under their own names:
|
|
327
|
+
// beige and near-black, one orange accent, Inter, pill buttons.
|
|
310
328
|
const CSS = `
|
|
311
329
|
:host { all: initial; }
|
|
312
|
-
* { box-sizing: border-box; font:
|
|
330
|
+
* { box-sizing: border-box; font: inherit; color: inherit; margin: 0; }
|
|
313
331
|
.root {
|
|
314
|
-
--
|
|
315
|
-
|
|
316
|
-
|
|
332
|
+
--color-white-rgb: 248 248 248; --color-beige-rgb: 238 236 233; --color-beige-off-rgb: 228 225 217;
|
|
333
|
+
--color-grey-light-rgb: 132 132 132; --color-grey-rgb: 108 108 108; --color-grey-mid-rgb: 100 100 100; --color-grey-dark-rgb: 40 40 40;
|
|
334
|
+
--color-black-off-rgb: 20 20 20; --color-black-rgb: 12 12 12;
|
|
335
|
+
--color-orange-rgb: 212 130 30; --color-red-rgb: 240 93 93;
|
|
336
|
+
--theme-foreground-rgb: var(--color-black-rgb);
|
|
337
|
+
--theme-background-alt-rgb: var(--color-white-rgb);
|
|
338
|
+
--theme-background-actions-rgb: var(--color-beige-off-rgb);
|
|
339
|
+
--theme-border-rgb: var(--color-grey-light-rgb);
|
|
340
|
+
--theme-accent-rgb: var(--color-orange-rgb);
|
|
341
|
+
--theme-foreground-muted-rgb: var(--color-grey-mid-rgb);
|
|
342
|
+
--theme-error: #b3261e;
|
|
343
|
+
--border-radius-medium: 20px; --border-radius-pill: 5em;
|
|
344
|
+
--ease-roll: cubic-bezier(0.65, 0, 0.35, 1);
|
|
345
|
+
--shadow: 0 18px 40px -16px rgb(var(--color-black-rgb) / .3), 0 2px 6px rgb(var(--color-black-rgb) / .05);
|
|
346
|
+
--font-inter: 'InterVariable', 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI',
|
|
347
|
+
Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif;
|
|
348
|
+
position: fixed; left: 50%; bottom: 20px; transform: translateX(-50%);
|
|
349
|
+
z-index: 2147483647; width: min(640px, calc(100vw - 32px)); color-scheme: light dark;
|
|
350
|
+
font: 400 14px/1.4 var(--font-inter); color: rgb(var(--theme-foreground-rgb));
|
|
351
|
+
-webkit-font-smoothing: antialiased;
|
|
317
352
|
}
|
|
318
353
|
@media (prefers-color-scheme: dark) {
|
|
319
|
-
.root {
|
|
354
|
+
.root {
|
|
355
|
+
--theme-foreground-rgb: var(--color-white-rgb);
|
|
356
|
+
--theme-background-alt-rgb: var(--color-black-off-rgb);
|
|
357
|
+
--theme-background-actions-rgb: var(--color-grey-dark-rgb);
|
|
358
|
+
--theme-border-rgb: var(--color-grey-rgb);
|
|
359
|
+
--theme-foreground-muted-rgb: var(--color-grey-light-rgb);
|
|
360
|
+
--theme-error: rgb(var(--color-red-rgb));
|
|
361
|
+
--shadow: 0 18px 40px -14px rgb(0 0 0 / .7), 0 2px 6px rgb(0 0 0 / .3);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
.icon { display: inline-flex; flex: none; }
|
|
365
|
+
.icon svg {
|
|
366
|
+
width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 1.75;
|
|
367
|
+
stroke-linecap: round; stroke-linejoin: round;
|
|
320
368
|
}
|
|
321
|
-
.
|
|
369
|
+
.visually-hidden {
|
|
370
|
+
position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
|
|
371
|
+
}
|
|
372
|
+
button { cursor: pointer; background: none; border: 0; padding: 0; }
|
|
373
|
+
button:disabled { cursor: not-allowed; }
|
|
374
|
+
button:focus-visible { outline: 2px solid rgb(var(--theme-accent-rgb)); outline-offset: 2px; }
|
|
375
|
+
|
|
376
|
+
/* Closed: one floating pill that reads like an empty field, with the mark. */
|
|
377
|
+
.bar { display: flex; justify-content: center; }
|
|
322
378
|
.pill {
|
|
323
|
-
display:
|
|
324
|
-
|
|
379
|
+
display: flex; align-items: center; gap: 12px; width: min(560px, 100%); height: 56px;
|
|
380
|
+
padding: 0 22px 0 26px; border-radius: var(--border-radius-pill);
|
|
381
|
+
background: rgb(var(--theme-background-alt-rgb));
|
|
382
|
+
border: 1px solid rgb(var(--theme-border-rgb) / .25); box-shadow: var(--shadow);
|
|
383
|
+
color: rgb(var(--theme-foreground-muted-rgb)); font-size: 16px; text-align: left;
|
|
384
|
+
transition: transform .25s var(--ease-roll), color .25s var(--ease-roll);
|
|
385
|
+
}
|
|
386
|
+
button.pill:hover { color: rgb(var(--theme-foreground-rgb)); transform: translateY(-2px); }
|
|
387
|
+
.pill > span:first-child { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
388
|
+
.mark { color: rgb(var(--theme-accent-rgb)); font-size: 22px; line-height: 1; }
|
|
389
|
+
.pill.compact { width: auto; height: 40px; padding: 0 18px; font-size: 14px; gap: 4px; }
|
|
390
|
+
|
|
391
|
+
/* Open: a floating card, the text on top and a row of pill buttons below. */
|
|
392
|
+
.panel {
|
|
393
|
+
display: grid; background: rgb(var(--theme-background-alt-rgb));
|
|
394
|
+
border: 1px solid rgb(var(--theme-border-rgb) / .25); border-radius: var(--border-radius-medium);
|
|
395
|
+
box-shadow: var(--shadow); overflow: hidden;
|
|
396
|
+
transition: border-color .25s var(--ease-roll), box-shadow .25s var(--ease-roll);
|
|
397
|
+
animation: rise .25s var(--ease-roll);
|
|
398
|
+
}
|
|
399
|
+
.panel:focus-within {
|
|
400
|
+
border-color: rgb(var(--theme-accent-rgb) / .6);
|
|
401
|
+
box-shadow: 0 0 0 3px rgb(var(--theme-accent-rgb) / .18), var(--shadow);
|
|
325
402
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
.
|
|
330
|
-
.
|
|
403
|
+
@keyframes rise { from { opacity: 0; transform: translateY(8px) scale(.98); } }
|
|
404
|
+
@keyframes spin { to { transform: rotate(1turn); } }
|
|
405
|
+
@media (prefers-reduced-motion: reduce) {
|
|
406
|
+
.panel { animation: none; }
|
|
407
|
+
.pill { transition: none; }
|
|
408
|
+
}
|
|
409
|
+
.status {
|
|
410
|
+
padding: 16px 22px 0; font-size: 14px; color: rgb(var(--theme-foreground-muted-rgb));
|
|
411
|
+
display: flex; align-items: center; gap: 8px;
|
|
412
|
+
}
|
|
413
|
+
.status.working::before {
|
|
414
|
+
content: ''; width: 12px; height: 12px; flex: none; border-radius: 50%;
|
|
415
|
+
border: 2px solid rgb(var(--theme-border-rgb) / .3); border-top-color: rgb(var(--theme-accent-rgb));
|
|
416
|
+
animation: spin .7s linear infinite;
|
|
417
|
+
}
|
|
418
|
+
.status.error { color: var(--theme-error); }
|
|
331
419
|
textarea {
|
|
332
|
-
width: 100%; resize: none; min-height:
|
|
333
|
-
|
|
420
|
+
display: block; width: 100%; resize: none; min-height: 76px; max-height: 200px;
|
|
421
|
+
padding: 16px 22px 6px; border: 0; outline: 0; background: transparent;
|
|
422
|
+
font-size: 16px; line-height: 1.6; color: rgb(var(--theme-foreground-rgb));
|
|
423
|
+
}
|
|
424
|
+
textarea::placeholder { color: rgb(var(--theme-foreground-muted-rgb)); }
|
|
425
|
+
/* New page: where it goes, above what it is for. */
|
|
426
|
+
.path {
|
|
427
|
+
display: flex; align-items: center; gap: 10px; padding: 16px 22px 0;
|
|
428
|
+
color: rgb(var(--theme-foreground-muted-rgb));
|
|
429
|
+
}
|
|
430
|
+
.path input {
|
|
431
|
+
flex: 1; min-width: 0; height: 34px; padding: 0 14px; outline: 0;
|
|
432
|
+
border: 1px solid rgb(var(--theme-border-rgb) / .25); border-radius: var(--border-radius-pill);
|
|
433
|
+
background: rgb(var(--theme-background-actions-rgb) / .45); color: rgb(var(--theme-foreground-rgb));
|
|
434
|
+
transition: border-color .25s var(--ease-roll);
|
|
435
|
+
}
|
|
436
|
+
.path input:focus-visible { border-color: rgb(var(--theme-accent-rgb)); }
|
|
437
|
+
.path input::placeholder { color: rgb(var(--theme-foreground-muted-rgb)); }
|
|
438
|
+
.path input:disabled { opacity: .5; }
|
|
439
|
+
textarea:disabled { opacity: .5; }
|
|
440
|
+
.toolbar { display: flex; align-items: center; gap: 6px; padding: 8px 12px 12px; }
|
|
441
|
+
.spacer { flex: 1; }
|
|
442
|
+
.chip {
|
|
443
|
+
display: inline-flex; align-items: center; gap: 6px; height: 34px; padding: .4em 1.1em .4em .9em;
|
|
444
|
+
border: 1px solid rgb(var(--theme-border-rgb) / .2); border-radius: var(--border-radius-pill);
|
|
445
|
+
background: rgb(var(--theme-background-actions-rgb) / .6); white-space: nowrap;
|
|
446
|
+
transition: background-color .25s var(--ease-roll);
|
|
334
447
|
}
|
|
335
|
-
|
|
336
|
-
button.
|
|
337
|
-
|
|
338
|
-
|
|
448
|
+
button.chip:hover:not(:disabled) { background: rgb(var(--theme-background-actions-rgb)); }
|
|
449
|
+
button.chip:disabled { opacity: .5; }
|
|
450
|
+
.scope { min-width: 0; background: transparent; }
|
|
451
|
+
.scope > span:not(.icon) { overflow: hidden; text-overflow: ellipsis; }
|
|
452
|
+
.scope button {
|
|
453
|
+
display: inline-flex; margin-right: -6px; padding: 3px; border-radius: 50%;
|
|
454
|
+
color: rgb(var(--theme-foreground-muted-rgb));
|
|
455
|
+
}
|
|
456
|
+
.scope button:hover { background: rgb(var(--theme-background-actions-rgb)); color: rgb(var(--theme-foreground-rgb)); }
|
|
457
|
+
.scope button svg { width: 14px; height: 14px; }
|
|
458
|
+
.icon-button {
|
|
459
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
460
|
+
width: 34px; height: 34px; border-radius: 50%; color: rgb(var(--theme-foreground-muted-rgb));
|
|
461
|
+
transition: background-color .25s var(--ease-roll), color .25s var(--ease-roll), opacity .25s var(--ease-roll);
|
|
462
|
+
}
|
|
463
|
+
.icon-button:hover:not(:disabled) { background: rgb(var(--theme-background-actions-rgb)); color: rgb(var(--theme-foreground-rgb)); }
|
|
464
|
+
/* The one call to action: white on orange. */
|
|
465
|
+
.icon-button.primary { background: rgb(var(--theme-accent-rgb)); color: #fff; }
|
|
466
|
+
.icon-button.primary:hover:not(:disabled) { background: rgb(var(--theme-accent-rgb)); color: #fff; opacity: .88; }
|
|
467
|
+
.icon-button.primary:disabled { opacity: .35; }
|
|
468
|
+
.divider { width: 1px; height: 20px; background: rgb(var(--theme-border-rgb) / .3); margin: 0 4px; }
|
|
469
|
+
|
|
470
|
+
/* On the site: storing the draft, and the way out. */
|
|
471
|
+
.footer {
|
|
472
|
+
display: flex; align-items: center; gap: 8px; padding: 10px 12px 10px 22px;
|
|
473
|
+
border-top: 1px solid rgb(var(--theme-border-rgb) / .2);
|
|
474
|
+
background: rgb(var(--theme-background-actions-rgb) / .45);
|
|
475
|
+
}
|
|
476
|
+
.footer .chip { height: 30px; padding: .4em 1.2em; background: rgb(var(--theme-background-alt-rgb)); }
|
|
477
|
+
.unsaved { color: rgb(var(--theme-foreground-muted-rgb)); display: inline-flex; align-items: center; }
|
|
478
|
+
.link { color: rgb(var(--theme-foreground-muted-rgb)); text-decoration: underline; text-underline-offset: 3px; padding: 0 6px; }
|
|
479
|
+
.link:hover { color: rgb(var(--theme-foreground-rgb)); }
|
|
480
|
+
.dot {
|
|
481
|
+
display: inline-block; width: 7px; height: 7px; border-radius: 50%;
|
|
482
|
+
background: rgb(var(--theme-accent-rgb)); margin-right: 8px;
|
|
339
483
|
}
|
|
340
|
-
button.primary { background: ${ACCENT}; border-color: ${ACCENT}; color: #fff; }
|
|
341
|
-
button:disabled { opacity: .5; cursor: default; }
|
|
342
|
-
.close { border: 0; background: none; color: var(--muted); cursor: pointer; font-size: 18px; line-height: 1; padding: 2px 6px; }
|
|
343
|
-
.status { font-size: 13px; color: var(--muted); flex: 1; min-width: 0; }
|
|
344
|
-
.status.error { color: var(--error); }
|
|
345
|
-
.bar { display: flex; justify-content: center; gap: 8px; }
|
|
346
|
-
.row > .link { margin-right: auto; }
|
|
347
|
-
.link { border: 0; background: none; color: var(--muted); cursor: pointer; font-size: 12px; text-decoration: underline; padding: 0; }
|
|
348
|
-
.dot { display: inline-block; width: 7px; height: 7px; border-radius: 50%; background: ${ACCENT}; margin-right: 6px; vertical-align: 1px; }
|
|
349
484
|
[hidden] { display: none !important; }
|
|
350
485
|
`;
|
|
351
486
|
const element = (doc, tag, props = {}, ...children)=>{
|
|
@@ -358,6 +493,14 @@ const element = (doc, tag, props = {}, ...children)=>{
|
|
|
358
493
|
}
|
|
359
494
|
return node;
|
|
360
495
|
};
|
|
496
|
+
const icon = (doc, name)=>{
|
|
497
|
+
const span = element(doc, 'span', {
|
|
498
|
+
className: 'icon'
|
|
499
|
+
});
|
|
500
|
+
span.setAttribute('aria-hidden', 'true');
|
|
501
|
+
span.innerHTML = `<svg viewBox="0 0 24 24">${ICONS[name]}</svg>`;
|
|
502
|
+
return span;
|
|
503
|
+
};
|
|
361
504
|
// Mounts the box on the page; returns a function that removes it.
|
|
362
505
|
const mountPromptBox = (doc, bridge, options = {})=>{
|
|
363
506
|
const labels = {
|
|
@@ -370,15 +513,21 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
370
513
|
mode: 'open'
|
|
371
514
|
});
|
|
372
515
|
const site = bridge.mode === 'site';
|
|
516
|
+
const pillText = element(doc, 'span', {
|
|
517
|
+
textContent: labels.open
|
|
518
|
+
});
|
|
373
519
|
const pill = element(doc, 'button', {
|
|
374
520
|
className: 'pill',
|
|
375
|
-
type: 'button'
|
|
376
|
-
|
|
377
|
-
|
|
521
|
+
type: 'button'
|
|
522
|
+
}, pillText, // The Obelism mark.
|
|
523
|
+
element(doc, 'span', {
|
|
524
|
+
className: 'mark',
|
|
525
|
+
textContent: '⁜'
|
|
526
|
+
}));
|
|
378
527
|
// Someone signed in who may only read: a note that drafts show, and a
|
|
379
528
|
// way out.
|
|
380
529
|
const viewing = element(doc, 'span', {
|
|
381
|
-
className: 'pill'
|
|
530
|
+
className: 'pill compact'
|
|
382
531
|
}, `${labels.viewingDrafts} · `);
|
|
383
532
|
const pillSignOut = element(doc, 'button', {
|
|
384
533
|
className: 'link',
|
|
@@ -386,71 +535,111 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
386
535
|
textContent: labels.signOut
|
|
387
536
|
});
|
|
388
537
|
viewing.appendChild(pillSignOut);
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
type: 'button',
|
|
392
|
-
textContent: labels.clearBlock
|
|
393
|
-
});
|
|
394
|
-
const scope = element(doc, 'div', {
|
|
395
|
-
className: 'scope'
|
|
396
|
-
}, '✨ ', scopeText, clearBlock);
|
|
397
|
-
const close = element(doc, 'button', {
|
|
398
|
-
className: 'close',
|
|
399
|
-
type: 'button',
|
|
400
|
-
textContent: '×',
|
|
401
|
-
title: labels.close
|
|
538
|
+
const status = element(doc, 'p', {
|
|
539
|
+
className: 'status'
|
|
402
540
|
});
|
|
403
|
-
|
|
541
|
+
status.setAttribute('role', 'status');
|
|
404
542
|
const input = element(doc, 'textarea', {
|
|
405
543
|
placeholder: labels.placeholder,
|
|
406
544
|
rows: 1
|
|
407
545
|
});
|
|
408
546
|
input.setAttribute('aria-label', labels.open);
|
|
409
|
-
|
|
410
|
-
|
|
547
|
+
// What the prompt acts on: the whole page, or the selected block with a
|
|
548
|
+
// way back to the page.
|
|
549
|
+
const pageIcon = icon(doc, 'page');
|
|
550
|
+
const blockIcon = icon(doc, 'block');
|
|
551
|
+
const scopeText = element(doc, 'span');
|
|
552
|
+
const clearBlock = element(doc, 'button', {
|
|
553
|
+
type: 'button',
|
|
554
|
+
title: labels.clearBlock
|
|
411
555
|
});
|
|
412
|
-
|
|
556
|
+
clearBlock.setAttribute('aria-label', labels.clearBlock);
|
|
557
|
+
clearBlock.appendChild(icon(doc, 'close'));
|
|
558
|
+
const scope = element(doc, 'span', {
|
|
559
|
+
className: 'chip scope'
|
|
560
|
+
}, pageIcon, blockIcon, scopeText, clearBlock);
|
|
413
561
|
const undo = element(doc, 'button', {
|
|
414
|
-
className: 'action',
|
|
562
|
+
className: 'action chip',
|
|
563
|
+
type: 'button'
|
|
564
|
+
}, icon(doc, 'undo'), labels.undo);
|
|
565
|
+
// New page (on the site, for people who may add pages): the scope becomes
|
|
566
|
+
// the new page, with a way back, and a path field shows above the prompt.
|
|
567
|
+
const newPage = element(doc, 'button', {
|
|
568
|
+
className: 'action chip',
|
|
569
|
+
type: 'button'
|
|
570
|
+
}, icon(doc, 'newPage'), labels.newPage);
|
|
571
|
+
const cancelNew = element(doc, 'button', {
|
|
415
572
|
type: 'button',
|
|
416
|
-
|
|
573
|
+
title: labels.newPageCancel
|
|
417
574
|
});
|
|
575
|
+
cancelNew.setAttribute('aria-label', labels.newPageCancel);
|
|
576
|
+
cancelNew.appendChild(icon(doc, 'close'));
|
|
577
|
+
const newScope = element(doc, 'span', {
|
|
578
|
+
className: 'chip scope'
|
|
579
|
+
}, icon(doc, 'newPage'), element(doc, 'span', {
|
|
580
|
+
textContent: labels.newPage
|
|
581
|
+
}), cancelNew);
|
|
582
|
+
const pathInput = element(doc, 'input', {
|
|
583
|
+
type: 'text',
|
|
584
|
+
placeholder: labels.pathPlaceholder,
|
|
585
|
+
autocomplete: 'off',
|
|
586
|
+
spellcheck: false
|
|
587
|
+
});
|
|
588
|
+
const pathRow = element(doc, 'label', {
|
|
589
|
+
className: 'path'
|
|
590
|
+
}, element(doc, 'span', {
|
|
591
|
+
textContent: labels.path
|
|
592
|
+
}), pathInput);
|
|
593
|
+
const close = element(doc, 'button', {
|
|
594
|
+
className: 'close icon-button',
|
|
595
|
+
type: 'button',
|
|
596
|
+
title: labels.close
|
|
597
|
+
}, icon(doc, 'close'));
|
|
598
|
+
close.setAttribute('aria-label', labels.close);
|
|
418
599
|
const send = element(doc, 'button', {
|
|
419
|
-
className: 'action primary',
|
|
420
|
-
type: 'submit'
|
|
600
|
+
className: 'action primary icon-button',
|
|
601
|
+
type: 'submit'
|
|
602
|
+
}, element(doc, 'span', {
|
|
603
|
+
className: 'visually-hidden',
|
|
421
604
|
textContent: labels.send
|
|
605
|
+
}), icon(doc, 'send'));
|
|
606
|
+
send.title = labels.send;
|
|
607
|
+
const unsaved = element(doc, 'span', {
|
|
608
|
+
className: 'unsaved'
|
|
609
|
+
});
|
|
610
|
+
unsaved.appendChild(element(doc, 'span', {
|
|
611
|
+
className: 'dot'
|
|
612
|
+
}));
|
|
613
|
+
unsaved.appendChild(doc.createTextNode(labels.unsaved));
|
|
614
|
+
const signOut = element(doc, 'button', {
|
|
615
|
+
className: 'link',
|
|
616
|
+
type: 'button',
|
|
617
|
+
textContent: labels.signOut
|
|
422
618
|
});
|
|
423
619
|
const save = element(doc, 'button', {
|
|
424
|
-
className: 'action',
|
|
620
|
+
className: 'action chip',
|
|
425
621
|
type: 'button',
|
|
426
622
|
textContent: labels.save
|
|
427
623
|
});
|
|
428
624
|
const publish = element(doc, 'button', {
|
|
429
|
-
className: 'action',
|
|
625
|
+
className: 'action chip',
|
|
430
626
|
type: 'button',
|
|
431
627
|
textContent: labels.publish
|
|
432
628
|
});
|
|
433
|
-
const
|
|
434
|
-
className: '
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
});
|
|
438
|
-
const unsaved = element(doc, 'span', {
|
|
439
|
-
className: 'scope'
|
|
440
|
-
});
|
|
441
|
-
unsaved.appendChild(element(doc, 'span', {
|
|
442
|
-
className: 'dot'
|
|
443
|
-
}));
|
|
444
|
-
unsaved.appendChild(doc.createTextNode(labels.unsaved));
|
|
629
|
+
const footer = element(doc, 'div', {
|
|
630
|
+
className: 'footer'
|
|
631
|
+
}, unsaved, element(doc, 'span', {
|
|
632
|
+
className: 'spacer'
|
|
633
|
+
}), signOut, save, publish);
|
|
445
634
|
const panel = element(doc, 'form', {
|
|
446
635
|
className: 'panel'
|
|
447
|
-
}, element(doc, 'div', {
|
|
448
|
-
className: '
|
|
449
|
-
}, scope,
|
|
450
|
-
className: '
|
|
451
|
-
},
|
|
452
|
-
className: '
|
|
453
|
-
},
|
|
636
|
+
}, status, pathRow, input, element(doc, 'div', {
|
|
637
|
+
className: 'toolbar'
|
|
638
|
+
}, scope, newScope, undo, newPage, element(doc, 'span', {
|
|
639
|
+
className: 'spacer'
|
|
640
|
+
}), close, element(doc, 'span', {
|
|
641
|
+
className: 'divider'
|
|
642
|
+
}), send), footer);
|
|
454
643
|
const root = element(doc, 'div', {
|
|
455
644
|
className: 'root'
|
|
456
645
|
}, element(doc, 'div', {
|
|
@@ -460,6 +649,7 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
460
649
|
textContent: CSS
|
|
461
650
|
}), root);
|
|
462
651
|
let open = false;
|
|
652
|
+
let creating = false;
|
|
463
653
|
let message = null;
|
|
464
654
|
const render = (state)=>{
|
|
465
655
|
const available = state.features.includes('prompt');
|
|
@@ -469,23 +659,43 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
469
659
|
pill.hidden = open || !available;
|
|
470
660
|
viewing.hidden = available || !site;
|
|
471
661
|
panel.hidden = !open || !available;
|
|
662
|
+
const canCreate = site && state.features.includes('create');
|
|
663
|
+
// A page of the site that isn't an entry (no `editable(entry)`, like a
|
|
664
|
+
// hand-written /about) has nothing to edit: only New page.
|
|
665
|
+
const pageless = site && bridge.entryId === null;
|
|
666
|
+
const onlyNew = pageless && canCreate;
|
|
667
|
+
if (!canCreate) creating = false;
|
|
668
|
+
if (onlyNew) creating = true;
|
|
669
|
+
pillText.textContent = onlyNew ? labels.newPage : labels.open;
|
|
670
|
+
cancelNew.hidden = onlyNew;
|
|
671
|
+
scope.hidden = creating;
|
|
672
|
+
newScope.hidden = !creating;
|
|
673
|
+
newPage.hidden = creating || !canCreate;
|
|
674
|
+
newPage.disabled = busy;
|
|
675
|
+
pathRow.hidden = !creating;
|
|
676
|
+
pathInput.disabled = busy;
|
|
677
|
+
input.placeholder = creating ? labels.newPagePlaceholder : labels.placeholder;
|
|
472
678
|
scopeText.textContent = state.selectedUid ? labels.scopeBlock : labels.scopeEntry;
|
|
473
679
|
clearBlock.hidden = !state.selectedUid;
|
|
474
|
-
|
|
680
|
+
pageIcon.hidden = !!state.selectedUid;
|
|
681
|
+
blockIcon.hidden = !state.selectedUid;
|
|
682
|
+
undo.hidden = creating || !state.canUndo;
|
|
475
683
|
undo.disabled = busy;
|
|
476
|
-
send.disabled = busy || input.value.trim() === '';
|
|
684
|
+
send.disabled = busy || input.value.trim() === '' || creating && pathInput.value.trim() === '';
|
|
477
685
|
input.disabled = busy;
|
|
478
|
-
save.hidden = !state.features.includes('save');
|
|
686
|
+
save.hidden = !state.features.includes('save') || pageless;
|
|
479
687
|
save.disabled = busy || !state.unsaved;
|
|
480
688
|
save.textContent = state.saving ? labels.saving : labels.save;
|
|
481
|
-
publish.hidden = !state.features.includes('publish');
|
|
689
|
+
publish.hidden = !state.features.includes('publish') || pageless;
|
|
482
690
|
publish.disabled = busy;
|
|
483
691
|
publish.textContent = state.publishing ? labels.publishing : labels.publish;
|
|
484
692
|
signOut.hidden = !site;
|
|
693
|
+
footer.hidden = !site;
|
|
485
694
|
unsaved.hidden = !state.unsaved;
|
|
486
|
-
const text = state.prompting ? labels.working : message?.text ?? (site && !state.selectedUid ? labels.selectHint : '');
|
|
695
|
+
const text = state.prompting ? creating ? labels.creating : labels.working : message?.text ?? (site && !creating && !state.selectedUid ? labels.selectHint : '');
|
|
487
696
|
status.textContent = text;
|
|
488
|
-
status.
|
|
697
|
+
status.hidden = !text;
|
|
698
|
+
status.className = `status${state.prompting ? ' working' : ''}${!busy && message?.error ? ' error' : ''}`;
|
|
489
699
|
};
|
|
490
700
|
const refresh = ()=>render(bridge.getState());
|
|
491
701
|
const grow = ()=>{
|
|
@@ -497,11 +707,22 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
497
707
|
if (!prompt || bridge.getState().prompting) return;
|
|
498
708
|
message = null;
|
|
499
709
|
try {
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
710
|
+
if (creating) {
|
|
711
|
+
const path = pathInput.value.trim();
|
|
712
|
+
if (!path) return pathInput.focus();
|
|
713
|
+
// The bridge opens the new page; this shows until it does.
|
|
714
|
+
await bridge.createPage(path, prompt);
|
|
715
|
+
message = {
|
|
716
|
+
text: labels.created,
|
|
717
|
+
error: false
|
|
718
|
+
};
|
|
719
|
+
} else {
|
|
720
|
+
const { summary } = await bridge.prompt(prompt);
|
|
721
|
+
message = {
|
|
722
|
+
text: summary,
|
|
723
|
+
error: false
|
|
724
|
+
};
|
|
725
|
+
}
|
|
505
726
|
input.value = '';
|
|
506
727
|
grow();
|
|
507
728
|
} catch (error) {
|
|
@@ -515,8 +736,16 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
515
736
|
};
|
|
516
737
|
const setOpen = (next)=>{
|
|
517
738
|
open = next;
|
|
518
|
-
if (site) bridge.setSelecting(open);
|
|
519
739
|
refresh();
|
|
740
|
+
if (site) bridge.setSelecting(open && !creating);
|
|
741
|
+
};
|
|
742
|
+
const setCreating = (next)=>{
|
|
743
|
+
creating = next;
|
|
744
|
+
message = null;
|
|
745
|
+
bridge.setSelecting(open && !creating);
|
|
746
|
+
if (creating) bridge.select(null);
|
|
747
|
+
refresh();
|
|
748
|
+
(creating ? pathInput : input).focus();
|
|
520
749
|
};
|
|
521
750
|
const store = async (action)=>{
|
|
522
751
|
message = null;
|
|
@@ -536,7 +765,7 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
536
765
|
};
|
|
537
766
|
pill.addEventListener('click', ()=>{
|
|
538
767
|
setOpen(true);
|
|
539
|
-
input.focus();
|
|
768
|
+
(creating ? pathInput : input).focus();
|
|
540
769
|
});
|
|
541
770
|
close.addEventListener('click', ()=>setOpen(false));
|
|
542
771
|
save.addEventListener('click', ()=>void store('save'));
|
|
@@ -544,6 +773,17 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
544
773
|
signOut.addEventListener('click', ()=>bridge.signOut());
|
|
545
774
|
pillSignOut.addEventListener('click', ()=>bridge.signOut());
|
|
546
775
|
clearBlock.addEventListener('click', ()=>bridge.select(null));
|
|
776
|
+
newPage.addEventListener('click', ()=>setCreating(true));
|
|
777
|
+
cancelNew.addEventListener('click', ()=>setCreating(false));
|
|
778
|
+
pathInput.addEventListener('input', refresh);
|
|
779
|
+
pathInput.addEventListener('keydown', (event)=>{
|
|
780
|
+
if (event.key === 'Enter' && !event.isComposing) {
|
|
781
|
+
event.preventDefault();
|
|
782
|
+
input.focus();
|
|
783
|
+
} else if (event.key === 'Escape') {
|
|
784
|
+
setOpen(false);
|
|
785
|
+
}
|
|
786
|
+
});
|
|
547
787
|
undo.addEventListener('click', ()=>{
|
|
548
788
|
message = null;
|
|
549
789
|
bridge.undo();
|
|
@@ -597,7 +837,7 @@ const readSiteEditing = (doc)=>{
|
|
|
597
837
|
return null;
|
|
598
838
|
};
|
|
599
839
|
const same = (a, b)=>JSON.stringify(a) === JSON.stringify(b);
|
|
600
|
-
const createSiteBridge = (win, basePath)=>{
|
|
840
|
+
const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path}` } = {})=>{
|
|
601
841
|
const doc = win.document;
|
|
602
842
|
const emitter = createEmitter();
|
|
603
843
|
const cleanup = [];
|
|
@@ -760,6 +1000,39 @@ const createSiteBridge = (win, basePath)=>{
|
|
|
760
1000
|
});
|
|
761
1001
|
}
|
|
762
1002
|
},
|
|
1003
|
+
async createPage (path, prompt) {
|
|
1004
|
+
const text = prompt.trim();
|
|
1005
|
+
if (!text) throw new Error('Say what the page is for');
|
|
1006
|
+
if (!path.trim()) throw new Error('Say where the page goes, like /pricing');
|
|
1007
|
+
if (!has('create')) throw new Error('You may not add pages here');
|
|
1008
|
+
// Opening the new page would lose them.
|
|
1009
|
+
if (state.unsaved) {
|
|
1010
|
+
throw new Error('Save or undo your changes before adding a page');
|
|
1011
|
+
}
|
|
1012
|
+
setState({
|
|
1013
|
+
prompting: true
|
|
1014
|
+
});
|
|
1015
|
+
try {
|
|
1016
|
+
const created = await api('entries', {
|
|
1017
|
+
path,
|
|
1018
|
+
prompt: text,
|
|
1019
|
+
locale: locale(),
|
|
1020
|
+
from: pageEntryId()
|
|
1021
|
+
});
|
|
1022
|
+
const url = pagePath(created.path);
|
|
1023
|
+
win.location.assign(url);
|
|
1024
|
+
return {
|
|
1025
|
+
summary: created.summary,
|
|
1026
|
+
entryId: created.entryId,
|
|
1027
|
+
path: created.path,
|
|
1028
|
+
url
|
|
1029
|
+
};
|
|
1030
|
+
} finally{
|
|
1031
|
+
setState({
|
|
1032
|
+
prompting: false
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1035
|
+
},
|
|
763
1036
|
undo () {
|
|
764
1037
|
const last = history.pop();
|
|
765
1038
|
const entry = last && working.get(last.entryId);
|
|
@@ -886,7 +1159,7 @@ const peekContentBridge = ()=>typeof window === 'undefined' ? null : window[KEY]
|
|
|
886
1159
|
// if (bridge.isEditing) bridge.on('update', ({ entry }) => render(entry))
|
|
887
1160
|
//
|
|
888
1161
|
// The first call's options win: there is one bridge per page.
|
|
889
|
-
const getContentBridge = ({ url, prompt = true } = {})=>{
|
|
1162
|
+
const getContentBridge = ({ url, prompt = true, pagePath } = {})=>{
|
|
890
1163
|
if (typeof window === 'undefined') return SERVER_BRIDGE;
|
|
891
1164
|
const win = window;
|
|
892
1165
|
if (win[KEY]) return win[KEY];
|
|
@@ -898,7 +1171,9 @@ const getContentBridge = ({ url, prompt = true } = {})=>{
|
|
|
898
1171
|
// Not in the visual editor, but signed in to edit through the site's
|
|
899
1172
|
// own `/cms/login` (@obelism/content/site).
|
|
900
1173
|
const sitePath = bridge.isEditing || win.parent !== win ? null : readSiteEditing(win.document);
|
|
901
|
-
if (sitePath) bridge = createSiteBridge(win, sitePath
|
|
1174
|
+
if (sitePath) bridge = createSiteBridge(win, sitePath, {
|
|
1175
|
+
pagePath
|
|
1176
|
+
});
|
|
902
1177
|
if (bridge.isEditing && prompt) {
|
|
903
1178
|
const unmount = mountPromptBox(win.document, bridge, prompt === true ? {} : prompt);
|
|
904
1179
|
const destroy = bridge.destroy;
|
package/dist/site.js
CHANGED
package/dist/site.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@obelism/content",
|
|
3
3
|
"description": "Obelism Content SDK: the delivery API client, the visual editor bridge with prompt editing, editing on your own site, and the content model CLI",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.2.0",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cms",
|
|
7
7
|
"headless-cms",
|