@docentjs/dom 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -279,6 +279,8 @@ function holePath(viewport, hole, radius) {
279
279
  var Overlay = class {
280
280
  el;
281
281
  blocker;
282
+ /** Hairline of light around the cutout. */
283
+ ring;
282
284
  lastHole = null;
283
285
  constructor(doc) {
284
286
  this.el = doc.createElement("div");
@@ -287,6 +289,20 @@ var Overlay = class {
287
289
  this.blocker = doc.createElement("div");
288
290
  this.blocker.className = "blocker";
289
291
  this.blocker.hidden = true;
292
+ this.ring = doc.createElement("div");
293
+ this.ring.className = "ring";
294
+ this.ring.setAttribute("part", "ring");
295
+ this.ring.style.opacity = "0";
296
+ }
297
+ /** Move the ring to a rect; a zero-size rect collapses it (modal steps). */
298
+ placeRing(rect, radius, visible) {
299
+ Object.assign(this.ring.style, {
300
+ transform: `translate(${rect.x}px, ${rect.y}px)`,
301
+ width: `${rect.width}px`,
302
+ height: `${rect.height}px`,
303
+ borderRadius: `${radius}px`,
304
+ opacity: visible ? "1" : "0"
305
+ });
290
306
  }
291
307
  /** Current hole, padded, in viewport coordinates. */
292
308
  get hole() {
@@ -305,11 +321,18 @@ var Overlay = class {
305
321
  width: 0,
306
322
  height: 0
307
323
  }, 0);
324
+ this.placeRing({
325
+ x: cx,
326
+ y: cy,
327
+ width: 0,
328
+ height: 0
329
+ }, 0, false);
308
330
  this.blocker.hidden = true;
309
331
  return;
310
332
  }
311
333
  const hole = this.lastHole;
312
334
  this.el.style.clipPath = holePath(viewport, hole, radius);
335
+ this.placeRing(hole, Math.max(0, Math.min(radius, hole.width / 2, hole.height / 2)), true);
313
336
  this.blocker.hidden = !block;
314
337
  if (block) {
315
338
  this.blocker.style.transform = `translate(${hole.x}px, ${hole.y}px)`;
@@ -340,6 +363,37 @@ function slot(doc, name, fallback) {
340
363
  if (fallback) s.appendChild(fallback);
341
364
  return s;
342
365
  }
366
+ const SVG_NS = "http://www.w3.org/2000/svg";
367
+ /** A 14px stroked X, drawn rather than typed so it centers optically in any font. */
368
+ function closeIcon(doc) {
369
+ const svg = doc.createElementNS(SVG_NS, "svg");
370
+ svg.setAttribute("viewBox", "0 0 14 14");
371
+ svg.setAttribute("aria-hidden", "true");
372
+ svg.setAttribute("fill", "none");
373
+ const path = doc.createElementNS(SVG_NS, "path");
374
+ path.setAttribute("d", "M3.5 3.5l7 7m0-7l-7 7");
375
+ path.setAttribute("stroke", "currentColor");
376
+ path.setAttribute("stroke-width", "1.6");
377
+ path.setAttribute("stroke-linecap", "round");
378
+ svg.appendChild(path);
379
+ return svg;
380
+ }
381
+ /** A small forward arrow for the primary action, so direction reads at a glance. */
382
+ function arrowIcon(doc) {
383
+ const svg = doc.createElementNS(SVG_NS, "svg");
384
+ svg.setAttribute("viewBox", "0 0 12 12");
385
+ svg.setAttribute("aria-hidden", "true");
386
+ svg.setAttribute("fill", "none");
387
+ svg.setAttribute("class", "icon");
388
+ const path = doc.createElementNS(SVG_NS, "path");
389
+ path.setAttribute("d", "M2.5 6h7m-3-3l3 3-3 3");
390
+ path.setAttribute("stroke", "currentColor");
391
+ path.setAttribute("stroke-width", "1.5");
392
+ path.setAttribute("stroke-linecap", "round");
393
+ path.setAttribute("stroke-linejoin", "round");
394
+ svg.appendChild(path);
395
+ return svg;
396
+ }
343
397
  function formatProgress(template, current, total) {
344
398
  return template.replace("{current}", String(current)).replace("{total}", String(total));
345
399
  }
@@ -397,7 +451,7 @@ function buildPopover(doc, ctx, labels = {}, slots = {}) {
397
451
  const close = h(doc, "button", "close", "close");
398
452
  close.type = "button";
399
453
  close.setAttribute("aria-label", text.close);
400
- close.textContent = "×";
454
+ close.appendChild(closeIcon(doc));
401
455
  close.addEventListener("click", () => actions.skip());
402
456
  closeNode = close;
403
457
  }
@@ -424,7 +478,15 @@ function buildPopover(doc, ctx, labels = {}, slots = {}) {
424
478
  el.appendChild(slot(doc, "media", mediaNode));
425
479
  const footer = h(doc, "div", "footer", "footer");
426
480
  const progress = h(doc, "div", "progress", "progress");
427
- if (options.showProgress !== false) progress.textContent = formatProgress(text.progress, ctx.progress.current, ctx.progress.total);
481
+ if (options.showProgress !== false) {
482
+ const meter = h(doc, "span", "meter", "meter");
483
+ meter.setAttribute("aria-hidden", "true");
484
+ progress.style.setProperty("--docent-step", String(ctx.progress.current));
485
+ progress.style.setProperty("--docent-steps", String(Math.max(1, ctx.progress.total)));
486
+ const count = h(doc, "span", "count", "count");
487
+ count.textContent = formatProgress(text.progress, ctx.progress.current, ctx.progress.total);
488
+ progress.append(meter, count);
489
+ }
428
490
  footer.appendChild(slot(doc, "progress", progress));
429
491
  const group = h(doc, "div", "buttons", "buttons");
430
492
  let initialFocus = el;
@@ -436,9 +498,12 @@ function buildPopover(doc, ctx, labels = {}, slots = {}) {
436
498
  group.appendChild(b);
437
499
  return b;
438
500
  };
439
- if (buttons.back !== false && ctx.canGoBack) button(text.back, "button-back", false, actions.back);
440
501
  if (buttons.skip !== false && !ctx.isLast) button(text.skip, "button-skip", false, actions.skip);
441
- if (buttons.next !== false) initialFocus = button(ctx.isLast ? text.done : text.next, "button-next", true, actions.next);
502
+ if (buttons.back !== false && ctx.canGoBack) button(text.back, "button-back", false, actions.back);
503
+ if (buttons.next !== false) {
504
+ initialFocus = button(ctx.isLast ? text.done : text.next, "button-next", true, actions.next);
505
+ if (!ctx.isLast) initialFocus.appendChild(arrowIcon(doc));
506
+ }
442
507
  footer.appendChild(slot(doc, "buttons", group));
443
508
  el.appendChild(slot(doc, "footer", footer));
444
509
  const slotted = resolveSlots(doc, slots, ctx);
@@ -466,146 +531,15 @@ function buildHeadlessShell(doc) {
466
531
  }
467
532
  //#endregion
468
533
  //#region src/styles.ts
469
- /** Styles injected into the shadow root. Theme through the custom properties. */
470
- const STYLES = `
471
- :host {
472
- --docent-font: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
473
- --docent-bg: #ffffff;
474
- --docent-fg: #111827;
475
- --docent-muted: #6b7280;
476
- --docent-accent: #2563eb;
477
- --docent-accent-fg: #ffffff;
478
- --docent-radius: 12px;
479
- --docent-shadow: 0 10px 30px rgba(0, 0, 0, 0.18), 0 2px 6px rgba(0, 0, 0, 0.08);
480
- --docent-width: 320px;
481
- --docent-overlay: #000;
482
- --docent-overlay-opacity: 0.55;
483
- --docent-duration: 220ms;
484
- /* Fast start, gentle stop: movement begins the moment you click. */
485
- --docent-easing: cubic-bezier(0.2, 0.8, 0.2, 1);
486
- position: fixed;
487
- inset: 0;
488
- z-index: var(--docent-z, 2147483000);
489
- pointer-events: none;
490
- font: 14px/1.5 var(--docent-font);
491
- color: var(--docent-fg);
492
- }
493
- @media (prefers-color-scheme: dark) {
494
- :host {
495
- --docent-bg: #1f2937;
496
- --docent-fg: #f9fafb;
497
- --docent-muted: #9ca3af;
498
- --docent-accent: #60a5fa;
499
- --docent-accent-fg: #0b1220;
500
- }
501
- }
502
- * { box-sizing: border-box; }
503
- .overlay {
504
- position: absolute;
505
- inset: 0;
506
- background: var(--docent-overlay);
507
- opacity: var(--docent-overlay-opacity);
508
- pointer-events: auto;
509
- transition: clip-path var(--docent-duration) var(--docent-easing);
510
- }
511
- .blocker {
512
- position: absolute;
513
- left: 0;
514
- top: 0;
515
- pointer-events: auto;
516
- }
517
- .popover {
518
- position: absolute;
519
- left: 0;
520
- top: 0;
521
- width: var(--docent-width);
522
- max-width: calc(100vw - 32px);
523
- background: var(--docent-bg);
524
- border-radius: var(--docent-radius);
525
- box-shadow: var(--docent-shadow);
526
- padding: 16px;
527
- pointer-events: auto;
528
- outline: none;
529
- transition: transform var(--docent-duration) var(--docent-easing), opacity var(--docent-duration) var(--docent-easing);
530
- }
531
- .popover[data-entering] { opacity: 0; transition: none; }
532
- /* Between steps the popover slides; only its content cross-fades, briefly. */
533
- .popover[data-moving] > * { animation: docent-swap 160ms ease-out; }
534
- @keyframes docent-swap { from { opacity: 0; } to { opacity: 1; } }
535
- .popover.headless {
536
- width: auto;
537
- max-width: none;
538
- padding: 0;
539
- background: none;
540
- box-shadow: none;
541
- border-radius: 0;
542
- }
543
- .arrow {
544
- position: absolute;
545
- width: 12px;
546
- height: 12px;
547
- background: var(--docent-bg);
548
- transform: rotate(45deg);
549
- }
550
- .popover[data-side="top"] .arrow { bottom: -6px; }
551
- .popover[data-side="bottom"] .arrow { top: -6px; }
552
- .popover[data-side="left"] .arrow { right: -6px; }
553
- .popover[data-side="right"] .arrow { left: -6px; }
554
- .popover[data-side="center"] .arrow, .popover[data-side="sheet"] .arrow { display: none; }
555
- .popover.sheet {
556
- max-width: none;
557
- border-radius: var(--docent-radius) var(--docent-radius) 0 0;
558
- padding-bottom: max(16px, env(safe-area-inset-bottom));
559
- }
560
- .header { display: flex; align-items: flex-start; gap: 8px; }
561
- .title { flex: 1; margin: 0; font-size: 16px; font-weight: 600; }
562
- .close {
563
- appearance: none;
564
- border: 0;
565
- background: transparent;
566
- color: var(--docent-muted);
567
- font-size: 18px;
568
- line-height: 1;
569
- cursor: pointer;
570
- padding: 2px 4px;
571
- margin: -4px -6px 0 0;
572
- border-radius: 6px;
573
- }
574
- .close:hover, .close:focus-visible { color: var(--docent-fg); background: rgba(127, 127, 127, 0.15); }
575
- .body { margin-top: 6px; }
576
- .body p { margin: 0 0 8px; }
577
- .body p:last-child { margin-bottom: 0; }
578
- .body code {
579
- font-family: ui-monospace, monospace;
580
- font-size: 0.9em;
581
- padding: 1px 4px;
582
- border-radius: 4px;
583
- background: rgba(127, 127, 127, 0.15);
584
- }
585
- .body a { color: var(--docent-accent); }
586
- .media { margin: 10px 0 0; }
587
- .media img, .media video { display: block; max-width: 100%; border-radius: 8px; }
588
- .footer { display: flex; align-items: center; gap: 8px; margin-top: 14px; }
589
- .progress { flex: 1; color: var(--docent-muted); font-size: 12px; }
590
- .buttons { display: flex; gap: 8px; }
591
- .button {
592
- appearance: none;
593
- border: 0;
594
- border-radius: 8px;
595
- padding: 7px 12px;
596
- font: inherit;
597
- font-weight: 500;
598
- cursor: pointer;
599
- background: rgba(127, 127, 127, 0.15);
600
- color: var(--docent-fg);
601
- }
602
- .button.primary { background: var(--docent-accent); color: var(--docent-accent-fg); }
603
- .button:focus-visible, .close:focus-visible { outline: 2px solid var(--docent-accent); outline-offset: 2px; }
604
- @media (prefers-reduced-motion: reduce) {
605
- .overlay, .popover { transition: none; }
606
- .popover[data-moving] > * { animation: none; }
607
- }
608
- `;
534
+ /**
535
+ * Styles injected into the shadow root. Theme through the custom properties.
536
+ *
537
+ * Design: quiet precision (see .impeccable.md). The popover inherits the host
538
+ * site's font; hierarchy comes from size, weight, tracking and color. Colors
539
+ * are ink tinted toward the Docent hue (OKLCH 285). Light is the default;
540
+ * dark is opt-in through tokens or the `dark` preset.
541
+ */
542
+ const STYLES = `:host{--docent-bg:oklch(99.4% 0.003 285);--docent-fg:oklch(23% 0.018 285);--docent-muted:oklch(52% 0.014 285);--docent-accent:oklch(26% 0.02 285);--docent-accent-fg:oklch(98.5% 0.004 285);--docent-radius:14px;--docent-shadow:0 1px 2px oklch(23% 0.02 285 / 0.06),0 8px 24px -6px oklch(23% 0.02 285 / 0.16),0 28px 56px -16px oklch(23% 0.02 285 / 0.24);--docent-width:344px;--docent-overlay:oklch(20% 0.02 285);--docent-overlay-opacity:0.52;--docent-duration:220ms;--docent-easing:cubic-bezier(0.2,0.8,0.2,1);--_line:color-mix(in oklch,var(--docent-fg) 11%,transparent);--_soft:color-mix(in oklch,var(--docent-fg) 6%,transparent);--_body:color-mix(in oklch,var(--docent-fg) 80%,var(--docent-bg));position:fixed;inset:0;z-index:var(--docent-z,2147483000);pointer-events:none;color:var(--docent-fg);font-family:var(--docent-font);font-size:14px;font-weight:400;font-style:normal;line-height:1.55;letter-spacing:normal;text-transform:none;text-align:start;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}*{box-sizing:border-box}.overlay{position:absolute;inset:0;background:var(--docent-overlay);opacity:var(--docent-overlay-opacity);pointer-events:auto;transition:clip-path var(--docent-duration) var(--docent-easing)}.blocker{position:absolute;left:0;top:0;pointer-events:auto}.ring{position:absolute;left:0;top:0;pointer-events:none;box-shadow:0 0 0 1px oklch(98% 0.004 285 / 0.58),0 0 0 6px oklch(98% 0.004 285 / 0.08);transition:transform var(--docent-duration) var(--docent-easing),width var(--docent-duration) var(--docent-easing),height var(--docent-duration) var(--docent-easing),border-radius var(--docent-duration) var(--docent-easing),opacity var(--docent-duration) var(--docent-easing)}.popover{position:absolute;left:0;top:0;display:flex;flex-direction:column;width:var(--docent-width);max-width:calc(100vw - 32px);padding:20px 20px 16px;background:var(--docent-bg);border-radius:var(--docent-radius);box-shadow:0 0 0 1px var(--_line),var(--docent-shadow);pointer-events:auto;outline:none;transition:transform var(--docent-duration) var(--docent-easing),opacity var(--docent-duration) var(--docent-easing),scale var(--docent-duration) var(--docent-easing)}.popover[data-side="bottom"]{transform-origin:50% 0}.popover[data-side="top"]{transform-origin:50% 100%}.popover[data-side="right"]{transform-origin:0 50%}.popover[data-side="left"]{transform-origin:100% 50%}.popover[data-entering]{opacity:0;scale:0.97;transition:none}.popover[data-moving]>*{animation:docent-swap 160ms ease-out}@keyframes docent-swap{from{opacity:0}to{opacity:1}}.popover.headless{width:auto;max-width:none;padding:0;background:none;box-shadow:none;border-radius:0}.arrow{position:absolute;width:12px;height:12px;background:var(--docent-bg);transform:rotate(45deg)}.popover[data-side="bottom"] .arrow{top:-6px;border-top:1px solid var(--_line);border-left:1px solid var(--_line);border-top-left-radius:2px}.popover[data-side="top"] .arrow{bottom:-6px;border-bottom:1px solid var(--_line);border-right:1px solid var(--_line);border-bottom-right-radius:2px}.popover[data-side="right"] .arrow{left:-6px;border-bottom:1px solid var(--_line);border-left:1px solid var(--_line);border-bottom-left-radius:2px}.popover[data-side="left"] .arrow{right:-6px;border-top:1px solid var(--_line);border-right:1px solid var(--_line);border-top-right-radius:2px}.popover[data-side="center"] .arrow,.popover[data-side="sheet"] .arrow{display:none}.header{display:flex;align-items:flex-start;gap:12px}.title{flex:1;min-width:0;margin:0;font-size:18px;font-weight:600;line-height:1.3;letter-spacing:-0.012em;color:var(--docent-fg);text-wrap:balance}.close{flex:none;display:grid;place-items:center;width:28px;height:28px;margin:-3px -8px -3px 0;padding:0;border:0;border-radius:8px;background:transparent;color:var(--docent-muted);cursor:pointer;transition:background-color 120ms ease-out,color 120ms ease-out}.close svg{width:14px;height:14px}.close:hover{background:var(--_soft);color:var(--docent-fg)}.body{margin-top:6px;color:var(--_body);text-wrap:pretty}.body p{margin:0}.body p + p{margin-top:8px}.body strong{font-weight:600;color:var(--docent-fg)}.body a{color:var(--docent-fg);text-decoration:underline;text-decoration-color:color-mix(in oklch,var(--docent-fg) 32%,transparent);text-decoration-thickness:1px;text-underline-offset:3px;transition:text-decoration-color 120ms ease-out}.body a:hover{text-decoration-color:currentColor}.body code{font-family:ui-monospace,"SF Mono",SFMono-Regular,Menlo,Consolas,monospace;font-size:0.88em;padding:1px 5px;border-radius:5px;background:var(--_soft);color:var(--docent-fg)}.media{margin-top:14px}.media img,.media video{display:block;width:100%;border-radius:8px;box-shadow:0 0 0 1px var(--_line)}.footer{display:flex;align-items:center;gap:12px;margin-top:20px}.progress{flex:1;display:flex;align-items:center;gap:8px;min-width:0;color:var(--docent-muted);font-size:12px;font-variant-numeric:tabular-nums;letter-spacing:0.01em;white-space:nowrap}.meter{flex:none;width:28px;height:3px;border-radius:999px;background:linear-gradient(var(--docent-fg),var(--docent-fg)) 0 0 / calc(var(--docent-step) / var(--docent-steps) * 100%) 100% no-repeat,var(--_line)}.buttons{display:flex;align-items:center;gap:6px}.button{appearance:none;display:inline-flex;align-items:center;justify-content:center;height:32px;padding:0 12px;border:0;border-radius:8px;background:transparent;color:var(--docent-fg);font:inherit;font-size:13px;font-weight:500;line-height:1;letter-spacing:-0.003em;white-space:nowrap;cursor:pointer;transition:background-color 120ms ease-out,color 120ms ease-out,scale 80ms ease-out}.button:hover{background:var(--_soft)}.button:active{scale:0.97}[part~="button-skip"]{color:var(--docent-muted);padding:0 8px}[part~="button-skip"]:hover{color:var(--docent-fg);background:transparent}.button.primary{padding:0 14px;background:var(--docent-accent);color:var(--docent-accent-fg);font-weight:600;box-shadow:inset 0 1px 0 color-mix(in oklch,var(--docent-accent-fg) 14%,transparent)}.button.primary .icon{width:12px;height:12px;margin:0 -2px 0 6px;transition:translate 160ms var(--docent-easing)}.button.primary:hover .icon{translate:2px 0}.button.primary:hover{background:color-mix(in oklch,var(--docent-accent) 86%,var(--docent-accent-fg))}.button:focus-visible,.close:focus-visible{outline:2px solid var(--docent-accent);outline-offset:2px}.popover.sheet{max-width:none;padding:20px 20px max(16px,env(safe-area-inset-bottom));border-radius:var(--docent-radius) var(--docent-radius) 0 0;box-shadow:0 0 0 1px var(--_line),0 -12px 40px -12px oklch(23% 0.02 285 / 0.28)}@media (pointer:coarse){:host{font-size:15px}.button{min-height:44px;padding:0 16px;font-size:14px}.close{width:40px;height:40px;margin:-9px -12px -9px 0}}@media (prefers-reduced-motion:reduce){.overlay,.popover,.ring{transition:none}.popover[data-moving]>*{animation:none}}`;
609
543
  //#endregion
610
544
  //#region src/target.ts
611
545
  /** Attribute that `{ name }` targets resolve through. */
@@ -820,8 +754,8 @@ var DomRenderer = class {
820
754
  ...ctx.tour.options?.spotlight,
821
755
  ...ctx.step.spotlight
822
756
  };
823
- const padding = spotlight.padding ?? 6;
824
- const radius = spotlight.radius ?? 6;
757
+ const padding = spotlight.padding ?? 8;
758
+ const radius = spotlight.radius ?? 10;
825
759
  const external = this.headlessContainer;
826
760
  const sheet = this.isSheet(viewport);
827
761
  popover.classList.toggle("sheet", sheet);
@@ -1017,6 +951,7 @@ var DomRenderer = class {
1017
951
  shadow.appendChild(style);
1018
952
  const overlay = new Overlay(this.doc);
1019
953
  shadow.appendChild(overlay.el);
954
+ shadow.appendChild(overlay.ring);
1020
955
  shadow.appendChild(overlay.blocker);
1021
956
  this.doc.body.appendChild(host);
1022
957
  this.host = host;