@docentjs/dom 0.3.0 → 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,140 +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: 250ms;
484
- position: fixed;
485
- inset: 0;
486
- z-index: var(--docent-z, 2147483000);
487
- pointer-events: none;
488
- font: 14px/1.5 var(--docent-font);
489
- color: var(--docent-fg);
490
- }
491
- @media (prefers-color-scheme: dark) {
492
- :host {
493
- --docent-bg: #1f2937;
494
- --docent-fg: #f9fafb;
495
- --docent-muted: #9ca3af;
496
- --docent-accent: #60a5fa;
497
- --docent-accent-fg: #0b1220;
498
- }
499
- }
500
- * { box-sizing: border-box; }
501
- .overlay {
502
- position: absolute;
503
- inset: 0;
504
- background: var(--docent-overlay);
505
- opacity: var(--docent-overlay-opacity);
506
- pointer-events: auto;
507
- transition: clip-path var(--docent-duration) ease;
508
- }
509
- .blocker {
510
- position: absolute;
511
- left: 0;
512
- top: 0;
513
- pointer-events: auto;
514
- }
515
- .popover {
516
- position: absolute;
517
- left: 0;
518
- top: 0;
519
- width: var(--docent-width);
520
- max-width: calc(100vw - 32px);
521
- background: var(--docent-bg);
522
- border-radius: var(--docent-radius);
523
- box-shadow: var(--docent-shadow);
524
- padding: 16px;
525
- pointer-events: auto;
526
- outline: none;
527
- transition: transform var(--docent-duration) ease, opacity var(--docent-duration) ease;
528
- }
529
- .popover[data-entering] { opacity: 0; transition: none; }
530
- .popover.headless {
531
- width: auto;
532
- max-width: none;
533
- padding: 0;
534
- background: none;
535
- box-shadow: none;
536
- border-radius: 0;
537
- }
538
- .arrow {
539
- position: absolute;
540
- width: 12px;
541
- height: 12px;
542
- background: var(--docent-bg);
543
- transform: rotate(45deg);
544
- }
545
- .popover[data-side="top"] .arrow { bottom: -6px; }
546
- .popover[data-side="bottom"] .arrow { top: -6px; }
547
- .popover[data-side="left"] .arrow { right: -6px; }
548
- .popover[data-side="right"] .arrow { left: -6px; }
549
- .popover[data-side="center"] .arrow, .popover[data-side="sheet"] .arrow { display: none; }
550
- .popover.sheet {
551
- max-width: none;
552
- border-radius: var(--docent-radius) var(--docent-radius) 0 0;
553
- padding-bottom: max(16px, env(safe-area-inset-bottom));
554
- }
555
- .header { display: flex; align-items: flex-start; gap: 8px; }
556
- .title { flex: 1; margin: 0; font-size: 16px; font-weight: 600; }
557
- .close {
558
- appearance: none;
559
- border: 0;
560
- background: transparent;
561
- color: var(--docent-muted);
562
- font-size: 18px;
563
- line-height: 1;
564
- cursor: pointer;
565
- padding: 2px 4px;
566
- margin: -4px -6px 0 0;
567
- border-radius: 6px;
568
- }
569
- .close:hover, .close:focus-visible { color: var(--docent-fg); background: rgba(127, 127, 127, 0.15); }
570
- .body { margin-top: 6px; }
571
- .body p { margin: 0 0 8px; }
572
- .body p:last-child { margin-bottom: 0; }
573
- .body code {
574
- font-family: ui-monospace, monospace;
575
- font-size: 0.9em;
576
- padding: 1px 4px;
577
- border-radius: 4px;
578
- background: rgba(127, 127, 127, 0.15);
579
- }
580
- .body a { color: var(--docent-accent); }
581
- .media { margin: 10px 0 0; }
582
- .media img, .media video { display: block; max-width: 100%; border-radius: 8px; }
583
- .footer { display: flex; align-items: center; gap: 8px; margin-top: 14px; }
584
- .progress { flex: 1; color: var(--docent-muted); font-size: 12px; }
585
- .buttons { display: flex; gap: 8px; }
586
- .button {
587
- appearance: none;
588
- border: 0;
589
- border-radius: 8px;
590
- padding: 7px 12px;
591
- font: inherit;
592
- font-weight: 500;
593
- cursor: pointer;
594
- background: rgba(127, 127, 127, 0.15);
595
- color: var(--docent-fg);
596
- }
597
- .button.primary { background: var(--docent-accent); color: var(--docent-accent-fg); }
598
- .button:focus-visible, .close:focus-visible { outline: 2px solid var(--docent-accent); outline-offset: 2px; }
599
- @media (prefers-reduced-motion: reduce) {
600
- .overlay, .popover { transition: none; }
601
- }
602
- `;
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}}`;
603
543
  //#endregion
604
544
  //#region src/target.ts
605
545
  /** Attribute that `{ name }` targets resolve through. */
@@ -754,6 +694,7 @@ var DomRenderer = class {
754
694
  show(ctx) {
755
695
  const firstStep = !this.host;
756
696
  const host = this.mount();
697
+ const from = this.popover?.style.transform || null;
757
698
  this.teardownStep();
758
699
  this.ctx = ctx;
759
700
  this.target = ctx.step.target === void 0 ? null : resolveTarget(ctx.step.target, this.doc);
@@ -761,7 +702,10 @@ var DomRenderer = class {
761
702
  applyTheme(host, mergeThemes(this.options.theme, template?.theme, ctx.tour.options?.theme));
762
703
  this.setTemplateCss(template?.css);
763
704
  const initialFocus = this.options.headless ? this.buildHeadless(ctx, host, this.options.headless) : this.buildDefault(ctx, host, template);
764
- this.popover?.setAttribute("data-entering", "");
705
+ if (this.popover && from) {
706
+ this.popover.style.transform = from;
707
+ this.popover.setAttribute("data-moving", "");
708
+ } else this.popover?.setAttribute("data-entering", "");
765
709
  if (this.target) {
766
710
  const smooth = this.scrollIntoView(this.target, ctx.step);
767
711
  if (this.options.avoidOcclusion !== false) {
@@ -810,8 +754,8 @@ var DomRenderer = class {
810
754
  ...ctx.tour.options?.spotlight,
811
755
  ...ctx.step.spotlight
812
756
  };
813
- const padding = spotlight.padding ?? 6;
814
- const radius = spotlight.radius ?? 6;
757
+ const padding = spotlight.padding ?? 8;
758
+ const radius = spotlight.radius ?? 10;
815
759
  const external = this.headlessContainer;
816
760
  const sheet = this.isSheet(viewport);
817
761
  popover.classList.toggle("sheet", sheet);
@@ -1007,6 +951,7 @@ var DomRenderer = class {
1007
951
  shadow.appendChild(style);
1008
952
  const overlay = new Overlay(this.doc);
1009
953
  shadow.appendChild(overlay.el);
954
+ shadow.appendChild(overlay.ring);
1010
955
  shadow.appendChild(overlay.blocker);
1011
956
  this.doc.body.appendChild(host);
1012
957
  this.host = host;