@obelism/content 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bridge.js CHANGED
@@ -308,46 +308,158 @@ const DEFAULT_PROMPT_LABELS = {
308
308
  viewingDrafts: 'Viewing drafts',
309
309
  selectHint: 'Click a block to change only that block'
310
310
  };
311
- const ACCENT = '#d58a1f';
311
+ // Icons (Lucide), drawn in the text colour.
312
+ const ICONS = {
313
+ 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"/>',
314
+ block: '<rect width="18" height="18" x="3" y="3" rx="2"/><path d="M7 8h10"/><path d="M7 12h6"/>',
315
+ undo: '<path d="M9 14 4 9l5-5"/><path d="M4 9h10.5a5.5 5.5 0 0 1 0 11H11"/>',
316
+ close: '<path d="M18 6 6 18"/><path d="m6 6 12 12"/>',
317
+ 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"/>'
318
+ };
319
+ // Obelism's design tokens (obelism.studio/design.md), under their own names:
320
+ // beige and near-black, one orange accent, Inter, pill buttons.
312
321
  const CSS = `
313
322
  :host { all: initial; }
314
- * { box-sizing: border-box; font: 14px/1.4 system-ui, -apple-system, 'Segoe UI', sans-serif; }
323
+ * { box-sizing: border-box; font: inherit; color: inherit; margin: 0; }
315
324
  .root {
316
- --bg: #fff; --fg: #1d1b19; --muted: #6b655e; --line: #e4ded6; --error: #b3261e;
317
- position: fixed; left: 50%; bottom: 16px; transform: translateX(-50%);
318
- z-index: 2147483647; width: min(640px, calc(100vw - 32px)); color: var(--fg);
325
+ --color-white-rgb: 248 248 248; --color-beige-rgb: 238 236 233; --color-beige-off-rgb: 228 225 217;
326
+ --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;
327
+ --color-black-off-rgb: 20 20 20; --color-black-rgb: 12 12 12;
328
+ --color-orange-rgb: 212 130 30; --color-red-rgb: 240 93 93;
329
+ --theme-foreground-rgb: var(--color-black-rgb);
330
+ --theme-background-alt-rgb: var(--color-white-rgb);
331
+ --theme-background-actions-rgb: var(--color-beige-off-rgb);
332
+ --theme-border-rgb: var(--color-grey-light-rgb);
333
+ --theme-accent-rgb: var(--color-orange-rgb);
334
+ --theme-foreground-muted-rgb: var(--color-grey-mid-rgb);
335
+ --theme-error: #b3261e;
336
+ --border-radius-medium: 20px; --border-radius-pill: 5em;
337
+ --ease-roll: cubic-bezier(0.65, 0, 0.35, 1);
338
+ --shadow: 0 18px 40px -16px rgb(var(--color-black-rgb) / .3), 0 2px 6px rgb(var(--color-black-rgb) / .05);
339
+ --font-inter: 'InterVariable', 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI',
340
+ Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif;
341
+ position: fixed; left: 50%; bottom: 20px; transform: translateX(-50%);
342
+ z-index: 2147483647; width: min(640px, calc(100vw - 32px)); color-scheme: light dark;
343
+ font: 400 14px/1.4 var(--font-inter); color: rgb(var(--theme-foreground-rgb));
344
+ -webkit-font-smoothing: antialiased;
319
345
  }
320
346
  @media (prefers-color-scheme: dark) {
321
- .root { --bg: #23201d; --fg: #f3efe9; --muted: #a9a198; --line: #3b3631; --error: #ff8a80; }
347
+ .root {
348
+ --theme-foreground-rgb: var(--color-white-rgb);
349
+ --theme-background-alt-rgb: var(--color-black-off-rgb);
350
+ --theme-background-actions-rgb: var(--color-grey-dark-rgb);
351
+ --theme-border-rgb: var(--color-grey-rgb);
352
+ --theme-foreground-muted-rgb: var(--color-grey-light-rgb);
353
+ --theme-error: rgb(var(--color-red-rgb));
354
+ --shadow: 0 18px 40px -14px rgb(0 0 0 / .7), 0 2px 6px rgb(0 0 0 / .3);
355
+ }
356
+ }
357
+ .icon { display: inline-flex; flex: none; }
358
+ .icon svg {
359
+ width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 1.75;
360
+ stroke-linecap: round; stroke-linejoin: round;
322
361
  }
323
- .pill, .panel { background: var(--bg); border: 1px solid var(--line); box-shadow: 0 8px 30px rgba(0,0,0,.18); }
362
+ .visually-hidden {
363
+ position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
364
+ }
365
+ button { cursor: pointer; background: none; border: 0; padding: 0; }
366
+ button:disabled { cursor: not-allowed; }
367
+ button:focus-visible { outline: 2px solid rgb(var(--theme-accent-rgb)); outline-offset: 2px; }
368
+
369
+ /* Closed: one floating pill that reads like an empty field, with the mark. */
370
+ .bar { display: flex; justify-content: center; }
324
371
  .pill {
325
- display: block; margin: 0 auto; border-radius: 999px; padding: 8px 16px; cursor: pointer;
326
- color: var(--fg);
372
+ display: flex; align-items: center; gap: 12px; width: min(560px, 100%); height: 56px;
373
+ padding: 0 22px 0 26px; border-radius: var(--border-radius-pill);
374
+ background: rgb(var(--theme-background-alt-rgb));
375
+ border: 1px solid rgb(var(--theme-border-rgb) / .25); box-shadow: var(--shadow);
376
+ color: rgb(var(--theme-foreground-muted-rgb)); font-size: 16px; text-align: left;
377
+ transition: transform .25s var(--ease-roll), color .25s var(--ease-roll);
378
+ }
379
+ button.pill:hover { color: rgb(var(--theme-foreground-rgb)); transform: translateY(-2px); }
380
+ .pill > span:first-child { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
381
+ .mark { color: rgb(var(--theme-accent-rgb)); font-size: 22px; line-height: 1; }
382
+ .pill.compact { width: auto; height: 40px; padding: 0 18px; font-size: 14px; gap: 4px; }
383
+
384
+ /* Open: a floating card, the text on top and a row of pill buttons below. */
385
+ .panel {
386
+ display: grid; background: rgb(var(--theme-background-alt-rgb));
387
+ border: 1px solid rgb(var(--theme-border-rgb) / .25); border-radius: var(--border-radius-medium);
388
+ box-shadow: var(--shadow); overflow: hidden;
389
+ transition: border-color .25s var(--ease-roll), box-shadow .25s var(--ease-roll);
390
+ animation: rise .25s var(--ease-roll);
391
+ }
392
+ .panel:focus-within {
393
+ border-color: rgb(var(--theme-accent-rgb) / .6);
394
+ box-shadow: 0 0 0 3px rgb(var(--theme-accent-rgb) / .18), var(--shadow);
395
+ }
396
+ @keyframes rise { from { opacity: 0; transform: translateY(8px) scale(.98); } }
397
+ @keyframes spin { to { transform: rotate(1turn); } }
398
+ @media (prefers-reduced-motion: reduce) {
399
+ .panel { animation: none; }
400
+ .pill { transition: none; }
327
401
  }
328
- .pill:hover { border-color: ${ACCENT}; }
329
- .panel { border-radius: 14px; padding: 10px; display: grid; gap: 8px; }
330
- .row { display: flex; align-items: center; gap: 8px; }
331
- .scope { color: var(--muted); font-size: 12px; flex: 1; min-width: 0; }
332
- .scope button { color: inherit; font-size: 12px; text-decoration: underline; background: none; border: 0; padding: 0 0 0 6px; cursor: pointer; }
402
+ .status {
403
+ padding: 16px 22px 0; font-size: 14px; color: rgb(var(--theme-foreground-muted-rgb));
404
+ display: flex; align-items: center; gap: 8px;
405
+ }
406
+ .status.working::before {
407
+ content: ''; width: 12px; height: 12px; flex: none; border-radius: 50%;
408
+ border: 2px solid rgb(var(--theme-border-rgb) / .3); border-top-color: rgb(var(--theme-accent-rgb));
409
+ animation: spin .7s linear infinite;
410
+ }
411
+ .status.error { color: var(--theme-error); }
333
412
  textarea {
334
- width: 100%; resize: none; min-height: 44px; max-height: 160px; padding: 8px 10px;
335
- border-radius: 10px; border: 1px solid var(--line); background: transparent; color: var(--fg);
413
+ display: block; width: 100%; resize: none; min-height: 76px; max-height: 200px;
414
+ padding: 16px 22px 6px; border: 0; outline: 0; background: transparent;
415
+ font-size: 16px; line-height: 1.6; color: rgb(var(--theme-foreground-rgb));
416
+ }
417
+ textarea::placeholder { color: rgb(var(--theme-foreground-muted-rgb)); }
418
+ textarea:disabled { opacity: .5; }
419
+ .toolbar { display: flex; align-items: center; gap: 6px; padding: 8px 12px 12px; }
420
+ .spacer { flex: 1; }
421
+ .chip {
422
+ display: inline-flex; align-items: center; gap: 6px; height: 34px; padding: .4em 1.1em .4em .9em;
423
+ border: 1px solid rgb(var(--theme-border-rgb) / .2); border-radius: var(--border-radius-pill);
424
+ background: rgb(var(--theme-background-actions-rgb) / .6); white-space: nowrap;
425
+ transition: background-color .25s var(--ease-roll);
426
+ }
427
+ button.chip:hover:not(:disabled) { background: rgb(var(--theme-background-actions-rgb)); }
428
+ button.chip:disabled { opacity: .5; }
429
+ .scope { min-width: 0; background: transparent; }
430
+ .scope > span:not(.icon) { overflow: hidden; text-overflow: ellipsis; }
431
+ .scope button {
432
+ display: inline-flex; margin-right: -6px; padding: 3px; border-radius: 50%;
433
+ color: rgb(var(--theme-foreground-muted-rgb));
336
434
  }
337
- textarea:focus { outline: 2px solid ${ACCENT}; outline-offset: -1px; }
338
- button.action {
339
- border-radius: 8px; border: 1px solid var(--line); background: transparent; color: var(--fg);
340
- padding: 6px 12px; cursor: pointer;
435
+ .scope button:hover { background: rgb(var(--theme-background-actions-rgb)); color: rgb(var(--theme-foreground-rgb)); }
436
+ .scope button svg { width: 14px; height: 14px; }
437
+ .icon-button {
438
+ display: inline-flex; align-items: center; justify-content: center;
439
+ width: 34px; height: 34px; border-radius: 50%; color: rgb(var(--theme-foreground-muted-rgb));
440
+ transition: background-color .25s var(--ease-roll), color .25s var(--ease-roll), opacity .25s var(--ease-roll);
441
+ }
442
+ .icon-button:hover:not(:disabled) { background: rgb(var(--theme-background-actions-rgb)); color: rgb(var(--theme-foreground-rgb)); }
443
+ /* The one call to action: white on orange. */
444
+ .icon-button.primary { background: rgb(var(--theme-accent-rgb)); color: #fff; }
445
+ .icon-button.primary:hover:not(:disabled) { background: rgb(var(--theme-accent-rgb)); color: #fff; opacity: .88; }
446
+ .icon-button.primary:disabled { opacity: .35; }
447
+ .divider { width: 1px; height: 20px; background: rgb(var(--theme-border-rgb) / .3); margin: 0 4px; }
448
+
449
+ /* On the site: storing the draft, and the way out. */
450
+ .footer {
451
+ display: flex; align-items: center; gap: 8px; padding: 10px 12px 10px 22px;
452
+ border-top: 1px solid rgb(var(--theme-border-rgb) / .2);
453
+ background: rgb(var(--theme-background-actions-rgb) / .45);
454
+ }
455
+ .footer .chip { height: 30px; padding: .4em 1.2em; background: rgb(var(--theme-background-alt-rgb)); }
456
+ .unsaved { color: rgb(var(--theme-foreground-muted-rgb)); display: inline-flex; align-items: center; }
457
+ .link { color: rgb(var(--theme-foreground-muted-rgb)); text-decoration: underline; text-underline-offset: 3px; padding: 0 6px; }
458
+ .link:hover { color: rgb(var(--theme-foreground-rgb)); }
459
+ .dot {
460
+ display: inline-block; width: 7px; height: 7px; border-radius: 50%;
461
+ background: rgb(var(--theme-accent-rgb)); margin-right: 8px;
341
462
  }
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
463
  [hidden] { display: none !important; }
352
464
  `;
353
465
  const element = (doc, tag, props = {}, ...children)=>{
@@ -360,6 +472,14 @@ const element = (doc, tag, props = {}, ...children)=>{
360
472
  }
361
473
  return node;
362
474
  };
475
+ const icon = (doc, name)=>{
476
+ const span = element(doc, 'span', {
477
+ className: 'icon'
478
+ });
479
+ span.setAttribute('aria-hidden', 'true');
480
+ span.innerHTML = `<svg viewBox="0 0 24 24">${ICONS[name]}</svg>`;
481
+ return span;
482
+ };
363
483
  // Mounts the box on the page; returns a function that removes it.
364
484
  const mountPromptBox = (doc, bridge, options = {})=>{
365
485
  const labels = {
@@ -374,13 +494,18 @@ const mountPromptBox = (doc, bridge, options = {})=>{
374
494
  const site = bridge.mode === 'site';
375
495
  const pill = element(doc, 'button', {
376
496
  className: 'pill',
377
- type: 'button',
378
- textContent: `✨ ${labels.open}`
379
- });
497
+ type: 'button'
498
+ }, element(doc, 'span', {
499
+ textContent: labels.open
500
+ }), // The Obelism mark.
501
+ element(doc, 'span', {
502
+ className: 'mark',
503
+ textContent: '⁜'
504
+ }));
380
505
  // Someone signed in who may only read: a note that drafts show, and a
381
506
  // way out.
382
507
  const viewing = element(doc, 'span', {
383
- className: 'pill'
508
+ className: 'pill compact'
384
509
  }, `${labels.viewingDrafts} · `);
385
510
  const pillSignOut = element(doc, 'button', {
386
511
  className: 'link',
@@ -388,71 +513,83 @@ const mountPromptBox = (doc, bridge, options = {})=>{
388
513
  textContent: labels.signOut
389
514
  });
390
515
  viewing.appendChild(pillSignOut);
391
- const scopeText = element(doc, 'span');
392
- const clearBlock = element(doc, 'button', {
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
516
+ const status = element(doc, 'p', {
517
+ className: 'status'
404
518
  });
405
- close.setAttribute('aria-label', labels.close);
519
+ status.setAttribute('role', 'status');
406
520
  const input = element(doc, 'textarea', {
407
521
  placeholder: labels.placeholder,
408
522
  rows: 1
409
523
  });
410
524
  input.setAttribute('aria-label', labels.open);
411
- const status = element(doc, 'div', {
412
- className: 'status'
525
+ // What the prompt acts on: the whole page, or the selected block with a
526
+ // way back to the page.
527
+ const pageIcon = icon(doc, 'page');
528
+ const blockIcon = icon(doc, 'block');
529
+ const scopeText = element(doc, 'span');
530
+ const clearBlock = element(doc, 'button', {
531
+ type: 'button',
532
+ title: labels.clearBlock
413
533
  });
414
- status.setAttribute('role', 'status');
534
+ clearBlock.setAttribute('aria-label', labels.clearBlock);
535
+ clearBlock.appendChild(icon(doc, 'close'));
536
+ const scope = element(doc, 'span', {
537
+ className: 'chip scope'
538
+ }, pageIcon, blockIcon, scopeText, clearBlock);
415
539
  const undo = element(doc, 'button', {
416
- className: 'action',
540
+ className: 'action chip',
541
+ type: 'button'
542
+ }, icon(doc, 'undo'), labels.undo);
543
+ const close = element(doc, 'button', {
544
+ className: 'close icon-button',
417
545
  type: 'button',
418
- textContent: labels.undo
419
- });
546
+ title: labels.close
547
+ }, icon(doc, 'close'));
548
+ close.setAttribute('aria-label', labels.close);
420
549
  const send = element(doc, 'button', {
421
- className: 'action primary',
422
- type: 'submit',
550
+ className: 'action primary icon-button',
551
+ type: 'submit'
552
+ }, element(doc, 'span', {
553
+ className: 'visually-hidden',
423
554
  textContent: labels.send
555
+ }), icon(doc, 'send'));
556
+ send.title = labels.send;
557
+ const unsaved = element(doc, 'span', {
558
+ className: 'unsaved'
559
+ });
560
+ unsaved.appendChild(element(doc, 'span', {
561
+ className: 'dot'
562
+ }));
563
+ unsaved.appendChild(doc.createTextNode(labels.unsaved));
564
+ const signOut = element(doc, 'button', {
565
+ className: 'link',
566
+ type: 'button',
567
+ textContent: labels.signOut
424
568
  });
425
569
  const save = element(doc, 'button', {
426
- className: 'action',
570
+ className: 'action chip',
427
571
  type: 'button',
428
572
  textContent: labels.save
429
573
  });
430
574
  const publish = element(doc, 'button', {
431
- className: 'action',
575
+ className: 'action chip',
432
576
  type: 'button',
433
577
  textContent: labels.publish
434
578
  });
435
- const signOut = element(doc, 'button', {
436
- className: 'link',
437
- type: 'button',
438
- textContent: labels.signOut
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));
579
+ const footer = element(doc, 'div', {
580
+ className: 'footer'
581
+ }, unsaved, element(doc, 'span', {
582
+ className: 'spacer'
583
+ }), signOut, save, publish);
447
584
  const panel = element(doc, 'form', {
448
585
  className: 'panel'
449
- }, element(doc, 'div', {
450
- className: 'row'
451
- }, scope, close), input, element(doc, 'div', {
452
- className: 'row'
453
- }, status, undo, send), element(doc, 'div', {
454
- className: 'row'
455
- }, unsaved, signOut, save, publish));
586
+ }, status, input, element(doc, 'div', {
587
+ className: 'toolbar'
588
+ }, scope, undo, element(doc, 'span', {
589
+ className: 'spacer'
590
+ }), close, element(doc, 'span', {
591
+ className: 'divider'
592
+ }), send), footer);
456
593
  const root = element(doc, 'div', {
457
594
  className: 'root'
458
595
  }, element(doc, 'div', {
@@ -473,6 +610,8 @@ const mountPromptBox = (doc, bridge, options = {})=>{
473
610
  panel.hidden = !open || !available;
474
611
  scopeText.textContent = state.selectedUid ? labels.scopeBlock : labels.scopeEntry;
475
612
  clearBlock.hidden = !state.selectedUid;
613
+ pageIcon.hidden = !!state.selectedUid;
614
+ blockIcon.hidden = !state.selectedUid;
476
615
  undo.hidden = !state.canUndo;
477
616
  undo.disabled = busy;
478
617
  send.disabled = busy || input.value.trim() === '';
@@ -484,10 +623,12 @@ const mountPromptBox = (doc, bridge, options = {})=>{
484
623
  publish.disabled = busy;
485
624
  publish.textContent = state.publishing ? labels.publishing : labels.publish;
486
625
  signOut.hidden = !site;
626
+ footer.hidden = !site;
487
627
  unsaved.hidden = !state.unsaved;
488
628
  const text = state.prompting ? labels.working : message?.text ?? (site && !state.selectedUid ? labels.selectHint : '');
489
629
  status.textContent = text;
490
- status.className = `status${!busy && message?.error ? ' error' : ''}`;
630
+ status.hidden = !text;
631
+ status.className = `status${state.prompting ? ' working' : ''}${!busy && message?.error ? ' error' : ''}`;
491
632
  };
492
633
  const refresh = ()=>render(bridge.getState());
493
634
  const grow = ()=>{
package/dist/bridge.mjs CHANGED
@@ -306,46 +306,158 @@ const DEFAULT_PROMPT_LABELS = {
306
306
  viewingDrafts: 'Viewing drafts',
307
307
  selectHint: 'Click a block to change only that block'
308
308
  };
309
- const ACCENT = '#d58a1f';
309
+ // Icons (Lucide), drawn in the text colour.
310
+ const ICONS = {
311
+ 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"/>',
312
+ block: '<rect width="18" height="18" x="3" y="3" rx="2"/><path d="M7 8h10"/><path d="M7 12h6"/>',
313
+ undo: '<path d="M9 14 4 9l5-5"/><path d="M4 9h10.5a5.5 5.5 0 0 1 0 11H11"/>',
314
+ close: '<path d="M18 6 6 18"/><path d="m6 6 12 12"/>',
315
+ 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"/>'
316
+ };
317
+ // Obelism's design tokens (obelism.studio/design.md), under their own names:
318
+ // beige and near-black, one orange accent, Inter, pill buttons.
310
319
  const CSS = `
311
320
  :host { all: initial; }
312
- * { box-sizing: border-box; font: 14px/1.4 system-ui, -apple-system, 'Segoe UI', sans-serif; }
321
+ * { box-sizing: border-box; font: inherit; color: inherit; margin: 0; }
313
322
  .root {
314
- --bg: #fff; --fg: #1d1b19; --muted: #6b655e; --line: #e4ded6; --error: #b3261e;
315
- position: fixed; left: 50%; bottom: 16px; transform: translateX(-50%);
316
- z-index: 2147483647; width: min(640px, calc(100vw - 32px)); color: var(--fg);
323
+ --color-white-rgb: 248 248 248; --color-beige-rgb: 238 236 233; --color-beige-off-rgb: 228 225 217;
324
+ --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;
325
+ --color-black-off-rgb: 20 20 20; --color-black-rgb: 12 12 12;
326
+ --color-orange-rgb: 212 130 30; --color-red-rgb: 240 93 93;
327
+ --theme-foreground-rgb: var(--color-black-rgb);
328
+ --theme-background-alt-rgb: var(--color-white-rgb);
329
+ --theme-background-actions-rgb: var(--color-beige-off-rgb);
330
+ --theme-border-rgb: var(--color-grey-light-rgb);
331
+ --theme-accent-rgb: var(--color-orange-rgb);
332
+ --theme-foreground-muted-rgb: var(--color-grey-mid-rgb);
333
+ --theme-error: #b3261e;
334
+ --border-radius-medium: 20px; --border-radius-pill: 5em;
335
+ --ease-roll: cubic-bezier(0.65, 0, 0.35, 1);
336
+ --shadow: 0 18px 40px -16px rgb(var(--color-black-rgb) / .3), 0 2px 6px rgb(var(--color-black-rgb) / .05);
337
+ --font-inter: 'InterVariable', 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI',
338
+ Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif;
339
+ position: fixed; left: 50%; bottom: 20px; transform: translateX(-50%);
340
+ z-index: 2147483647; width: min(640px, calc(100vw - 32px)); color-scheme: light dark;
341
+ font: 400 14px/1.4 var(--font-inter); color: rgb(var(--theme-foreground-rgb));
342
+ -webkit-font-smoothing: antialiased;
317
343
  }
318
344
  @media (prefers-color-scheme: dark) {
319
- .root { --bg: #23201d; --fg: #f3efe9; --muted: #a9a198; --line: #3b3631; --error: #ff8a80; }
345
+ .root {
346
+ --theme-foreground-rgb: var(--color-white-rgb);
347
+ --theme-background-alt-rgb: var(--color-black-off-rgb);
348
+ --theme-background-actions-rgb: var(--color-grey-dark-rgb);
349
+ --theme-border-rgb: var(--color-grey-rgb);
350
+ --theme-foreground-muted-rgb: var(--color-grey-light-rgb);
351
+ --theme-error: rgb(var(--color-red-rgb));
352
+ --shadow: 0 18px 40px -14px rgb(0 0 0 / .7), 0 2px 6px rgb(0 0 0 / .3);
353
+ }
354
+ }
355
+ .icon { display: inline-flex; flex: none; }
356
+ .icon svg {
357
+ width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 1.75;
358
+ stroke-linecap: round; stroke-linejoin: round;
320
359
  }
321
- .pill, .panel { background: var(--bg); border: 1px solid var(--line); box-shadow: 0 8px 30px rgba(0,0,0,.18); }
360
+ .visually-hidden {
361
+ position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
362
+ }
363
+ button { cursor: pointer; background: none; border: 0; padding: 0; }
364
+ button:disabled { cursor: not-allowed; }
365
+ button:focus-visible { outline: 2px solid rgb(var(--theme-accent-rgb)); outline-offset: 2px; }
366
+
367
+ /* Closed: one floating pill that reads like an empty field, with the mark. */
368
+ .bar { display: flex; justify-content: center; }
322
369
  .pill {
323
- display: block; margin: 0 auto; border-radius: 999px; padding: 8px 16px; cursor: pointer;
324
- color: var(--fg);
370
+ display: flex; align-items: center; gap: 12px; width: min(560px, 100%); height: 56px;
371
+ padding: 0 22px 0 26px; border-radius: var(--border-radius-pill);
372
+ background: rgb(var(--theme-background-alt-rgb));
373
+ border: 1px solid rgb(var(--theme-border-rgb) / .25); box-shadow: var(--shadow);
374
+ color: rgb(var(--theme-foreground-muted-rgb)); font-size: 16px; text-align: left;
375
+ transition: transform .25s var(--ease-roll), color .25s var(--ease-roll);
376
+ }
377
+ button.pill:hover { color: rgb(var(--theme-foreground-rgb)); transform: translateY(-2px); }
378
+ .pill > span:first-child { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
379
+ .mark { color: rgb(var(--theme-accent-rgb)); font-size: 22px; line-height: 1; }
380
+ .pill.compact { width: auto; height: 40px; padding: 0 18px; font-size: 14px; gap: 4px; }
381
+
382
+ /* Open: a floating card, the text on top and a row of pill buttons below. */
383
+ .panel {
384
+ display: grid; background: rgb(var(--theme-background-alt-rgb));
385
+ border: 1px solid rgb(var(--theme-border-rgb) / .25); border-radius: var(--border-radius-medium);
386
+ box-shadow: var(--shadow); overflow: hidden;
387
+ transition: border-color .25s var(--ease-roll), box-shadow .25s var(--ease-roll);
388
+ animation: rise .25s var(--ease-roll);
389
+ }
390
+ .panel:focus-within {
391
+ border-color: rgb(var(--theme-accent-rgb) / .6);
392
+ box-shadow: 0 0 0 3px rgb(var(--theme-accent-rgb) / .18), var(--shadow);
393
+ }
394
+ @keyframes rise { from { opacity: 0; transform: translateY(8px) scale(.98); } }
395
+ @keyframes spin { to { transform: rotate(1turn); } }
396
+ @media (prefers-reduced-motion: reduce) {
397
+ .panel { animation: none; }
398
+ .pill { transition: none; }
325
399
  }
326
- .pill:hover { border-color: ${ACCENT}; }
327
- .panel { border-radius: 14px; padding: 10px; display: grid; gap: 8px; }
328
- .row { display: flex; align-items: center; gap: 8px; }
329
- .scope { color: var(--muted); font-size: 12px; flex: 1; min-width: 0; }
330
- .scope button { color: inherit; font-size: 12px; text-decoration: underline; background: none; border: 0; padding: 0 0 0 6px; cursor: pointer; }
400
+ .status {
401
+ padding: 16px 22px 0; font-size: 14px; color: rgb(var(--theme-foreground-muted-rgb));
402
+ display: flex; align-items: center; gap: 8px;
403
+ }
404
+ .status.working::before {
405
+ content: ''; width: 12px; height: 12px; flex: none; border-radius: 50%;
406
+ border: 2px solid rgb(var(--theme-border-rgb) / .3); border-top-color: rgb(var(--theme-accent-rgb));
407
+ animation: spin .7s linear infinite;
408
+ }
409
+ .status.error { color: var(--theme-error); }
331
410
  textarea {
332
- width: 100%; resize: none; min-height: 44px; max-height: 160px; padding: 8px 10px;
333
- border-radius: 10px; border: 1px solid var(--line); background: transparent; color: var(--fg);
411
+ display: block; width: 100%; resize: none; min-height: 76px; max-height: 200px;
412
+ padding: 16px 22px 6px; border: 0; outline: 0; background: transparent;
413
+ font-size: 16px; line-height: 1.6; color: rgb(var(--theme-foreground-rgb));
414
+ }
415
+ textarea::placeholder { color: rgb(var(--theme-foreground-muted-rgb)); }
416
+ textarea:disabled { opacity: .5; }
417
+ .toolbar { display: flex; align-items: center; gap: 6px; padding: 8px 12px 12px; }
418
+ .spacer { flex: 1; }
419
+ .chip {
420
+ display: inline-flex; align-items: center; gap: 6px; height: 34px; padding: .4em 1.1em .4em .9em;
421
+ border: 1px solid rgb(var(--theme-border-rgb) / .2); border-radius: var(--border-radius-pill);
422
+ background: rgb(var(--theme-background-actions-rgb) / .6); white-space: nowrap;
423
+ transition: background-color .25s var(--ease-roll);
424
+ }
425
+ button.chip:hover:not(:disabled) { background: rgb(var(--theme-background-actions-rgb)); }
426
+ button.chip:disabled { opacity: .5; }
427
+ .scope { min-width: 0; background: transparent; }
428
+ .scope > span:not(.icon) { overflow: hidden; text-overflow: ellipsis; }
429
+ .scope button {
430
+ display: inline-flex; margin-right: -6px; padding: 3px; border-radius: 50%;
431
+ color: rgb(var(--theme-foreground-muted-rgb));
334
432
  }
335
- textarea:focus { outline: 2px solid ${ACCENT}; outline-offset: -1px; }
336
- button.action {
337
- border-radius: 8px; border: 1px solid var(--line); background: transparent; color: var(--fg);
338
- padding: 6px 12px; cursor: pointer;
433
+ .scope button:hover { background: rgb(var(--theme-background-actions-rgb)); color: rgb(var(--theme-foreground-rgb)); }
434
+ .scope button svg { width: 14px; height: 14px; }
435
+ .icon-button {
436
+ display: inline-flex; align-items: center; justify-content: center;
437
+ width: 34px; height: 34px; border-radius: 50%; color: rgb(var(--theme-foreground-muted-rgb));
438
+ transition: background-color .25s var(--ease-roll), color .25s var(--ease-roll), opacity .25s var(--ease-roll);
439
+ }
440
+ .icon-button:hover:not(:disabled) { background: rgb(var(--theme-background-actions-rgb)); color: rgb(var(--theme-foreground-rgb)); }
441
+ /* The one call to action: white on orange. */
442
+ .icon-button.primary { background: rgb(var(--theme-accent-rgb)); color: #fff; }
443
+ .icon-button.primary:hover:not(:disabled) { background: rgb(var(--theme-accent-rgb)); color: #fff; opacity: .88; }
444
+ .icon-button.primary:disabled { opacity: .35; }
445
+ .divider { width: 1px; height: 20px; background: rgb(var(--theme-border-rgb) / .3); margin: 0 4px; }
446
+
447
+ /* On the site: storing the draft, and the way out. */
448
+ .footer {
449
+ display: flex; align-items: center; gap: 8px; padding: 10px 12px 10px 22px;
450
+ border-top: 1px solid rgb(var(--theme-border-rgb) / .2);
451
+ background: rgb(var(--theme-background-actions-rgb) / .45);
452
+ }
453
+ .footer .chip { height: 30px; padding: .4em 1.2em; background: rgb(var(--theme-background-alt-rgb)); }
454
+ .unsaved { color: rgb(var(--theme-foreground-muted-rgb)); display: inline-flex; align-items: center; }
455
+ .link { color: rgb(var(--theme-foreground-muted-rgb)); text-decoration: underline; text-underline-offset: 3px; padding: 0 6px; }
456
+ .link:hover { color: rgb(var(--theme-foreground-rgb)); }
457
+ .dot {
458
+ display: inline-block; width: 7px; height: 7px; border-radius: 50%;
459
+ background: rgb(var(--theme-accent-rgb)); margin-right: 8px;
339
460
  }
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
461
  [hidden] { display: none !important; }
350
462
  `;
351
463
  const element = (doc, tag, props = {}, ...children)=>{
@@ -358,6 +470,14 @@ const element = (doc, tag, props = {}, ...children)=>{
358
470
  }
359
471
  return node;
360
472
  };
473
+ const icon = (doc, name)=>{
474
+ const span = element(doc, 'span', {
475
+ className: 'icon'
476
+ });
477
+ span.setAttribute('aria-hidden', 'true');
478
+ span.innerHTML = `<svg viewBox="0 0 24 24">${ICONS[name]}</svg>`;
479
+ return span;
480
+ };
361
481
  // Mounts the box on the page; returns a function that removes it.
362
482
  const mountPromptBox = (doc, bridge, options = {})=>{
363
483
  const labels = {
@@ -372,13 +492,18 @@ const mountPromptBox = (doc, bridge, options = {})=>{
372
492
  const site = bridge.mode === 'site';
373
493
  const pill = element(doc, 'button', {
374
494
  className: 'pill',
375
- type: 'button',
376
- textContent: `✨ ${labels.open}`
377
- });
495
+ type: 'button'
496
+ }, element(doc, 'span', {
497
+ textContent: labels.open
498
+ }), // The Obelism mark.
499
+ element(doc, 'span', {
500
+ className: 'mark',
501
+ textContent: '⁜'
502
+ }));
378
503
  // Someone signed in who may only read: a note that drafts show, and a
379
504
  // way out.
380
505
  const viewing = element(doc, 'span', {
381
- className: 'pill'
506
+ className: 'pill compact'
382
507
  }, `${labels.viewingDrafts} · `);
383
508
  const pillSignOut = element(doc, 'button', {
384
509
  className: 'link',
@@ -386,71 +511,83 @@ const mountPromptBox = (doc, bridge, options = {})=>{
386
511
  textContent: labels.signOut
387
512
  });
388
513
  viewing.appendChild(pillSignOut);
389
- const scopeText = element(doc, 'span');
390
- const clearBlock = element(doc, 'button', {
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
514
+ const status = element(doc, 'p', {
515
+ className: 'status'
402
516
  });
403
- close.setAttribute('aria-label', labels.close);
517
+ status.setAttribute('role', 'status');
404
518
  const input = element(doc, 'textarea', {
405
519
  placeholder: labels.placeholder,
406
520
  rows: 1
407
521
  });
408
522
  input.setAttribute('aria-label', labels.open);
409
- const status = element(doc, 'div', {
410
- className: 'status'
523
+ // What the prompt acts on: the whole page, or the selected block with a
524
+ // way back to the page.
525
+ const pageIcon = icon(doc, 'page');
526
+ const blockIcon = icon(doc, 'block');
527
+ const scopeText = element(doc, 'span');
528
+ const clearBlock = element(doc, 'button', {
529
+ type: 'button',
530
+ title: labels.clearBlock
411
531
  });
412
- status.setAttribute('role', 'status');
532
+ clearBlock.setAttribute('aria-label', labels.clearBlock);
533
+ clearBlock.appendChild(icon(doc, 'close'));
534
+ const scope = element(doc, 'span', {
535
+ className: 'chip scope'
536
+ }, pageIcon, blockIcon, scopeText, clearBlock);
413
537
  const undo = element(doc, 'button', {
414
- className: 'action',
538
+ className: 'action chip',
539
+ type: 'button'
540
+ }, icon(doc, 'undo'), labels.undo);
541
+ const close = element(doc, 'button', {
542
+ className: 'close icon-button',
415
543
  type: 'button',
416
- textContent: labels.undo
417
- });
544
+ title: labels.close
545
+ }, icon(doc, 'close'));
546
+ close.setAttribute('aria-label', labels.close);
418
547
  const send = element(doc, 'button', {
419
- className: 'action primary',
420
- type: 'submit',
548
+ className: 'action primary icon-button',
549
+ type: 'submit'
550
+ }, element(doc, 'span', {
551
+ className: 'visually-hidden',
421
552
  textContent: labels.send
553
+ }), icon(doc, 'send'));
554
+ send.title = labels.send;
555
+ const unsaved = element(doc, 'span', {
556
+ className: 'unsaved'
557
+ });
558
+ unsaved.appendChild(element(doc, 'span', {
559
+ className: 'dot'
560
+ }));
561
+ unsaved.appendChild(doc.createTextNode(labels.unsaved));
562
+ const signOut = element(doc, 'button', {
563
+ className: 'link',
564
+ type: 'button',
565
+ textContent: labels.signOut
422
566
  });
423
567
  const save = element(doc, 'button', {
424
- className: 'action',
568
+ className: 'action chip',
425
569
  type: 'button',
426
570
  textContent: labels.save
427
571
  });
428
572
  const publish = element(doc, 'button', {
429
- className: 'action',
573
+ className: 'action chip',
430
574
  type: 'button',
431
575
  textContent: labels.publish
432
576
  });
433
- const signOut = element(doc, 'button', {
434
- className: 'link',
435
- type: 'button',
436
- textContent: labels.signOut
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));
577
+ const footer = element(doc, 'div', {
578
+ className: 'footer'
579
+ }, unsaved, element(doc, 'span', {
580
+ className: 'spacer'
581
+ }), signOut, save, publish);
445
582
  const panel = element(doc, 'form', {
446
583
  className: 'panel'
447
- }, element(doc, 'div', {
448
- className: 'row'
449
- }, scope, close), input, element(doc, 'div', {
450
- className: 'row'
451
- }, status, undo, send), element(doc, 'div', {
452
- className: 'row'
453
- }, unsaved, signOut, save, publish));
584
+ }, status, input, element(doc, 'div', {
585
+ className: 'toolbar'
586
+ }, scope, undo, element(doc, 'span', {
587
+ className: 'spacer'
588
+ }), close, element(doc, 'span', {
589
+ className: 'divider'
590
+ }), send), footer);
454
591
  const root = element(doc, 'div', {
455
592
  className: 'root'
456
593
  }, element(doc, 'div', {
@@ -471,6 +608,8 @@ const mountPromptBox = (doc, bridge, options = {})=>{
471
608
  panel.hidden = !open || !available;
472
609
  scopeText.textContent = state.selectedUid ? labels.scopeBlock : labels.scopeEntry;
473
610
  clearBlock.hidden = !state.selectedUid;
611
+ pageIcon.hidden = !!state.selectedUid;
612
+ blockIcon.hidden = !state.selectedUid;
474
613
  undo.hidden = !state.canUndo;
475
614
  undo.disabled = busy;
476
615
  send.disabled = busy || input.value.trim() === '';
@@ -482,10 +621,12 @@ const mountPromptBox = (doc, bridge, options = {})=>{
482
621
  publish.disabled = busy;
483
622
  publish.textContent = state.publishing ? labels.publishing : labels.publish;
484
623
  signOut.hidden = !site;
624
+ footer.hidden = !site;
485
625
  unsaved.hidden = !state.unsaved;
486
626
  const text = state.prompting ? labels.working : message?.text ?? (site && !state.selectedUid ? labels.selectHint : '');
487
627
  status.textContent = text;
488
- status.className = `status${!busy && message?.error ? ' error' : ''}`;
628
+ status.hidden = !text;
629
+ status.className = `status${state.prompting ? ' working' : ''}${!busy && message?.error ? ' error' : ''}`;
489
630
  };
490
631
  const refresh = ()=>render(bridge.getState());
491
632
  const grow = ()=>{
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.1.0",
4
+ "version": "0.1.1",
5
5
  "keywords": [
6
6
  "cms",
7
7
  "headless-cms",