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