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