@docentjs/dom 0.3.1 → 0.5.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
@@ -1,4 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_arrows = require("./arrows-DdDu3O3y.cjs");
2
3
  let _docentjs_core = require("@docentjs/core");
3
4
  //#region src/content.ts
4
5
  const SAFE_SCHEMES = /* @__PURE__ */ new Set([
@@ -267,19 +268,46 @@ function clipToViewport(rect, viewport) {
267
268
  }
268
269
  //#endregion
269
270
  //#region src/overlay.ts
270
- /**
271
- * Full-viewport backdrop with a rounded cutout. The cutout is a `clip-path`
272
- * so pointer events pass through the hole to the page for free; a separate
273
- * blocker element covers it when the step forbids interaction.
274
- */
275
271
  function holePath(viewport, hole, radius) {
276
272
  const r = Math.max(0, Math.min(radius, hole.width / 2, hole.height / 2));
277
273
  const { x, y, width: w, height: h } = hole;
278
274
  return `path(evenodd, "${`M0 0H${viewport.width}V${viewport.height}H0Z`}${`M${x + r} ${y}H${x + w - r}A${r} ${r} 0 0 1 ${x + w} ${y + r}V${y + h - r}A${r} ${r} 0 0 1 ${x + w - r} ${y + h}H${x + r}A${r} ${r} 0 0 1 ${x} ${y + h - r}V${y + r}A${r} ${r} 0 0 1 ${x + r} ${y}Z`}")`;
279
275
  }
276
+ /** The padded cutout and its corner radius for a shape. */
277
+ function holeFor(target, padding, radius, shape = "rounded") {
278
+ if (shape === "circle") {
279
+ const cx = target.x + target.width / 2;
280
+ const cy = target.y + target.height / 2;
281
+ const rr = Math.hypot(target.width, target.height) / 2 + padding;
282
+ return {
283
+ hole: {
284
+ x: cx - rr,
285
+ y: cy - rr,
286
+ width: rr * 2,
287
+ height: rr * 2
288
+ },
289
+ radius: rr
290
+ };
291
+ }
292
+ const hole = inflate(target, padding);
293
+ if (shape === "rect") return {
294
+ hole,
295
+ radius: 0
296
+ };
297
+ if (shape === "pill") return {
298
+ hole,
299
+ radius: Math.min(hole.width, hole.height) / 2
300
+ };
301
+ return {
302
+ hole,
303
+ radius: Math.max(0, Math.min(radius, hole.width / 2, hole.height / 2))
304
+ };
305
+ }
280
306
  var Overlay = class {
281
307
  el;
282
308
  blocker;
309
+ /** Hairline of light around the cutout. */
310
+ ring;
283
311
  lastHole = null;
284
312
  constructor(doc) {
285
313
  this.el = doc.createElement("div");
@@ -288,29 +316,52 @@ var Overlay = class {
288
316
  this.blocker = doc.createElement("div");
289
317
  this.blocker.className = "blocker";
290
318
  this.blocker.hidden = true;
319
+ this.ring = doc.createElement("div");
320
+ this.ring.className = "ring";
321
+ this.ring.setAttribute("part", "ring");
322
+ this.ring.style.opacity = "0";
323
+ }
324
+ /** Move the ring to a rect; a zero-size rect collapses it (modal steps). */
325
+ placeRing(rect, radius, visible) {
326
+ Object.assign(this.ring.style, {
327
+ transform: `translate(${rect.x}px, ${rect.y}px)`,
328
+ width: `${rect.width}px`,
329
+ height: `${rect.height}px`,
330
+ borderRadius: `${radius}px`,
331
+ opacity: visible ? "1" : "0"
332
+ });
291
333
  }
292
334
  /** Current hole, padded, in viewport coordinates. */
293
335
  get hole() {
294
336
  return this.lastHole;
295
337
  }
296
- update(viewport, { target, padding, radius }, block) {
297
- if (target) this.lastHole = inflate(target, padding);
298
- else {
338
+ update(viewport, { target, padding, radius, shape }, block) {
339
+ if (!target) {
299
340
  const c = this.lastHole;
300
341
  const cx = c ? c.x + c.width / 2 : viewport.width / 2;
301
342
  const cy = c ? c.y + c.height / 2 : viewport.height / 2;
302
343
  this.lastHole = null;
344
+ this.setCentre(cx, cy);
303
345
  this.el.style.clipPath = holePath(viewport, {
304
346
  x: cx,
305
347
  y: cy,
306
348
  width: 0,
307
349
  height: 0
308
350
  }, 0);
351
+ this.placeRing({
352
+ x: cx,
353
+ y: cy,
354
+ width: 0,
355
+ height: 0
356
+ }, 0, false);
309
357
  this.blocker.hidden = true;
310
358
  return;
311
359
  }
312
- const hole = this.lastHole;
313
- this.el.style.clipPath = holePath(viewport, hole, radius);
360
+ const { hole, radius: r } = holeFor(target, padding, radius, shape);
361
+ this.lastHole = hole;
362
+ this.setCentre(hole.x + hole.width / 2, hole.y + hole.height / 2);
363
+ this.el.style.clipPath = holePath(viewport, hole, r);
364
+ this.placeRing(hole, r, true);
314
365
  this.blocker.hidden = !block;
315
366
  if (block) {
316
367
  this.blocker.style.transform = `translate(${hole.x}px, ${hole.y}px)`;
@@ -318,6 +369,11 @@ var Overlay = class {
318
369
  this.blocker.style.height = `${hole.height}px`;
319
370
  }
320
371
  }
372
+ /** Exposed for the vignette style, which is centred on the cutout. */
373
+ setCentre(x, y) {
374
+ this.el.style.setProperty("--docent-hole-x", `${Math.round(x)}px`);
375
+ this.el.style.setProperty("--docent-hole-y", `${Math.round(y)}px`);
376
+ }
321
377
  };
322
378
  //#endregion
323
379
  //#region src/popover.ts
@@ -341,6 +397,37 @@ function slot(doc, name, fallback) {
341
397
  if (fallback) s.appendChild(fallback);
342
398
  return s;
343
399
  }
400
+ const SVG_NS = "http://www.w3.org/2000/svg";
401
+ /** A 14px stroked X, drawn rather than typed so it centers optically in any font. */
402
+ function closeIcon(doc) {
403
+ const svg = doc.createElementNS(SVG_NS, "svg");
404
+ svg.setAttribute("viewBox", "0 0 14 14");
405
+ svg.setAttribute("aria-hidden", "true");
406
+ svg.setAttribute("fill", "none");
407
+ const path = doc.createElementNS(SVG_NS, "path");
408
+ path.setAttribute("d", "M3.5 3.5l7 7m0-7l-7 7");
409
+ path.setAttribute("stroke", "currentColor");
410
+ path.setAttribute("stroke-width", "1.6");
411
+ path.setAttribute("stroke-linecap", "round");
412
+ svg.appendChild(path);
413
+ return svg;
414
+ }
415
+ /** A small forward arrow for the primary action, so direction reads at a glance. */
416
+ function arrowIcon(doc) {
417
+ const svg = doc.createElementNS(SVG_NS, "svg");
418
+ svg.setAttribute("viewBox", "0 0 12 12");
419
+ svg.setAttribute("aria-hidden", "true");
420
+ svg.setAttribute("fill", "none");
421
+ svg.setAttribute("class", "icon");
422
+ const path = doc.createElementNS(SVG_NS, "path");
423
+ path.setAttribute("d", "M2.5 6h7m-3-3l3 3-3 3");
424
+ path.setAttribute("stroke", "currentColor");
425
+ path.setAttribute("stroke-width", "1.5");
426
+ path.setAttribute("stroke-linecap", "round");
427
+ path.setAttribute("stroke-linejoin", "round");
428
+ svg.appendChild(path);
429
+ return svg;
430
+ }
344
431
  function formatProgress(template, current, total) {
345
432
  return template.replace("{current}", String(current)).replace("{total}", String(total));
346
433
  }
@@ -398,7 +485,7 @@ function buildPopover(doc, ctx, labels = {}, slots = {}) {
398
485
  const close = h(doc, "button", "close", "close");
399
486
  close.type = "button";
400
487
  close.setAttribute("aria-label", text.close);
401
- close.textContent = "×";
488
+ close.appendChild(closeIcon(doc));
402
489
  close.addEventListener("click", () => actions.skip());
403
490
  closeNode = close;
404
491
  }
@@ -425,7 +512,15 @@ function buildPopover(doc, ctx, labels = {}, slots = {}) {
425
512
  el.appendChild(slot(doc, "media", mediaNode));
426
513
  const footer = h(doc, "div", "footer", "footer");
427
514
  const progress = h(doc, "div", "progress", "progress");
428
- if (options.showProgress !== false) progress.textContent = formatProgress(text.progress, ctx.progress.current, ctx.progress.total);
515
+ if (options.showProgress !== false) {
516
+ const meter = h(doc, "span", "meter", "meter");
517
+ meter.setAttribute("aria-hidden", "true");
518
+ progress.style.setProperty("--docent-step", String(ctx.progress.current));
519
+ progress.style.setProperty("--docent-steps", String(Math.max(1, ctx.progress.total)));
520
+ const count = h(doc, "span", "count", "count");
521
+ count.textContent = formatProgress(text.progress, ctx.progress.current, ctx.progress.total);
522
+ progress.append(meter, count);
523
+ }
429
524
  footer.appendChild(slot(doc, "progress", progress));
430
525
  const group = h(doc, "div", "buttons", "buttons");
431
526
  let initialFocus = el;
@@ -437,9 +532,12 @@ function buildPopover(doc, ctx, labels = {}, slots = {}) {
437
532
  group.appendChild(b);
438
533
  return b;
439
534
  };
440
- if (buttons.back !== false && ctx.canGoBack) button(text.back, "button-back", false, actions.back);
441
535
  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);
536
+ if (buttons.back !== false && ctx.canGoBack) button(text.back, "button-back", false, actions.back);
537
+ if (buttons.next !== false) {
538
+ initialFocus = button(ctx.isLast ? text.done : text.next, "button-next", true, actions.next);
539
+ if (!ctx.isLast) initialFocus.appendChild(arrowIcon(doc));
540
+ }
443
541
  footer.appendChild(slot(doc, "buttons", group));
444
542
  el.appendChild(slot(doc, "footer", footer));
445
543
  const slotted = resolveSlots(doc, slots, ctx);
@@ -467,146 +565,15 @@ function buildHeadlessShell(doc) {
467
565
  }
468
566
  //#endregion
469
567
  //#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
- `;
568
+ /**
569
+ * Styles injected into the shadow root. Theme through the custom properties.
570
+ *
571
+ * Design: quiet precision (see .impeccable.md). The popover inherits the host
572
+ * site's font; hierarchy comes from size, weight, tracking and color. Colors
573
+ * are ink tinted toward the Docent hue (OKLCH 285). Light is the default;
574
+ * dark is opt-in through tokens or the `dark` preset.
575
+ */
576
+ 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{--_scrim:color-mix(in oklch,var(--docent-overlay) calc(var(--docent-overlay-opacity) * 100%),transparent);position:absolute;inset:0;background:var(--_scrim);pointer-events:auto;transition:clip-path var(--docent-duration) var(--docent-easing)}:host([data-overlay="blur"]) .overlay{-webkit-backdrop-filter:blur(var(--docent-blur,4px));backdrop-filter:blur(var(--docent-blur,4px))}:host([data-overlay="vignette"]) .overlay{background:radial-gradient( circle at var(--docent-hole-x,50%) var(--docent-hole-y,50%),transparent 0,color-mix(in oklch,var(--docent-overlay) calc(var(--docent-overlay-opacity) * 30%),transparent) 22%,var(--_scrim) 78% )}:host([data-overlay="none"]) .overlay{background:transparent;pointer-events:none}.blocker{position:absolute;left:0;top:0;pointer-events:auto}.ring{position:absolute;left:0;top:0;pointer-events:none;color:var(--docent-ring,oklch(98% 0.004 285));box-shadow:0 0 0 1px color-mix(in oklch,currentColor 58%,transparent),0 0 0 6px color-mix(in oklch,currentColor 8%,transparent);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)}:host([data-overlay="none"]) .ring{color:var(--docent-ring,var(--docent-accent))}:host([data-ring="none"]) .ring{box-shadow:none}:host([data-ring="glow"]) .ring{box-shadow:0 0 0 1.5px color-mix(in oklch,currentColor 85%,transparent),0 0 20px 4px color-mix(in oklch,currentColor 42%,transparent)}:host([data-ring="solid"]) .ring{box-shadow:0 0 0 2px currentColor}:host([data-ring="dashed"]) .ring{box-shadow:none;outline:1.5px dashed color-mix(in oklch,currentColor 85%,transparent);outline-offset:3px}:host([data-ring="pulse"]) .ring::after{content:"";position:absolute;inset:0;border-radius:inherit;animation:docent-pulse 1.8s var(--docent-easing) infinite}@keyframes docent-pulse{from{box-shadow:0 0 0 0 color-mix(in oklch,currentColor 60%,transparent)}to{box-shadow:0 0 0 14px transparent}}:host(:not([data-arrow="caret"])) .arrow{display:none}:host([data-tracking]) .overlay,:host([data-tracking]) .ring,:host([data-tracking]) .popover{transition:none}.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}.ring::after{animation:none !important}.popover[data-moving]>*{animation:none}}`;
610
577
  //#endregion
611
578
  //#region src/target.ts
612
579
  /** Attribute that `{ name }` targets resolve through. */
@@ -707,7 +674,9 @@ const THEME_VARS = {
707
674
  overlay: "overlay",
708
675
  overlayOpacity: "overlay-opacity",
709
676
  duration: "duration",
710
- zIndex: "z"
677
+ zIndex: "z",
678
+ connector: "connector",
679
+ ring: "ring"
711
680
  };
712
681
  /** Write theme tokens as inline custom properties on an element. Clears unset ones. */
713
682
  function applyTheme(el, theme) {
@@ -723,6 +692,11 @@ function mergeThemes(...themes) {
723
692
  }
724
693
  //#endregion
725
694
  //#region src/renderer.ts
695
+ const DEFAULT_LOOK = {
696
+ arrow: "caret",
697
+ spotlight: {},
698
+ overlay: {}
699
+ };
726
700
  const FOCUSABLE = "a[href], button:not([disabled]), input, select, textarea, [tabindex]:not([tabindex=\"-1\"])";
727
701
  var DomRenderer = class {
728
702
  doc;
@@ -741,6 +715,17 @@ var DomRenderer = class {
741
715
  previousFocus = null;
742
716
  /** Set once per step after the sheet has scrolled the target clear. */
743
717
  sheetAdjusted = false;
718
+ connector;
719
+ /** Loaded on first use: connector styles cost nothing for tours that never use them. */
720
+ connectorModule;
721
+ connectorLoading;
722
+ /** Arrow, spotlight and overlay settings for the current step. */
723
+ look = DEFAULT_LOOK;
724
+ /** Until then the step's own transition runs; scroll updates may animate. */
725
+ settleUntil = 0;
726
+ /** Play the connector draw-in on its next render. */
727
+ drawConnector = false;
728
+ trackingFrame;
744
729
  constructor(options = {}) {
745
730
  this.options = options;
746
731
  this.doc = options.document ?? document;
@@ -768,6 +753,9 @@ var DomRenderer = class {
768
753
  const template = this.template(ctx);
769
754
  applyTheme(host, mergeThemes(this.options.theme, template?.theme, ctx.tour.options?.theme));
770
755
  this.setTemplateCss(template?.css);
756
+ this.applyLook(host, this.resolveLook(ctx, template));
757
+ this.settleUntil = performance.now() + this.duration(host) * 1.5;
758
+ this.drawConnector = true;
771
759
  const initialFocus = this.options.headless ? this.buildHeadless(ctx, host, this.options.headless) : this.buildDefault(ctx, host, template);
772
760
  if (this.popover && from) {
773
761
  this.popover.style.transform = from;
@@ -798,31 +786,34 @@ var DomRenderer = class {
798
786
  this.host = void 0;
799
787
  this.shadow = void 0;
800
788
  this.overlay = void 0;
789
+ this.connector = void 0;
801
790
  this.templateStyle = void 0;
802
791
  }
803
792
  const prev = this.previousFocus;
804
793
  this.previousFocus = null;
805
794
  if (prev instanceof HTMLElement && prev.isConnected) prev.focus({ preventScroll: true });
806
795
  }
807
- /** Re-measure and re-position everything. Safe to call often. */
808
- update() {
796
+ /**
797
+ * Re-measure and re-position everything. Safe to call often. `tracking`
798
+ * marks updates caused by scroll or resize: once the step's own transition
799
+ * has finished, those follow the target instantly instead of trailing it.
800
+ */
801
+ update(tracking = false) {
809
802
  const ctx = this.ctx;
810
803
  const overlay = this.overlay;
811
804
  const popover = this.popover;
812
805
  const win = this.doc.defaultView;
813
806
  if (!ctx || !overlay || !popover || !win) return;
807
+ if (tracking && performance.now() > this.settleUntil) this.markTracking();
814
808
  const viewport = this.viewport();
815
809
  const overlaySize = {
816
810
  width: overlay.el.offsetWidth,
817
811
  height: overlay.el.offsetHeight
818
812
  };
819
- const spotlight = {
820
- ...this.options.spotlight,
821
- ...ctx.tour.options?.spotlight,
822
- ...ctx.step.spotlight
823
- };
824
- const padding = spotlight.padding ?? 6;
825
- const radius = spotlight.radius ?? 6;
813
+ const spotlight = this.look.spotlight;
814
+ const padding = spotlight.padding ?? 8;
815
+ const radius = spotlight.radius ?? 10;
816
+ const shape = spotlight.shape ?? "rounded";
826
817
  const external = this.headlessContainer;
827
818
  const sheet = this.isSheet(viewport);
828
819
  popover.classList.toggle("sheet", sheet);
@@ -836,8 +827,10 @@ var DomRenderer = class {
836
827
  overlay.update(overlaySize, {
837
828
  target: rect,
838
829
  padding,
839
- radius
830
+ radius,
831
+ shape
840
832
  }, this.blocksInteraction(ctx.step));
833
+ this.connector?.clear();
841
834
  const vx = viewport.x ?? 0;
842
835
  const top = (viewport.y ?? 0) + viewport.height - floating.height;
843
836
  popover.style.transform = `translate(${vx}px, ${top}px)`;
@@ -852,6 +845,7 @@ var DomRenderer = class {
852
845
  padding,
853
846
  radius
854
847
  }, false);
848
+ this.connector?.clear();
855
849
  const { x, y } = centerPosition(floating, viewport);
856
850
  popover.style.transform = `translate(${x}px, ${y}px)`;
857
851
  popover.setAttribute("data-side", "center");
@@ -862,14 +856,16 @@ var DomRenderer = class {
862
856
  overlay.update(overlaySize, {
863
857
  target: rect,
864
858
  padding,
865
- radius
859
+ radius,
860
+ shape
866
861
  }, this.blocksInteraction(ctx.step));
862
+ const hole = overlay.hole ?? rect;
867
863
  const pos = computePosition({
868
- anchor: clipToViewport(overlay.hole ?? rect, viewport),
864
+ anchor: clipToViewport(hole, viewport),
869
865
  floating,
870
866
  viewport,
871
867
  placement: ctx.step.placement ?? "auto",
872
- gap: this.options.gap ?? 12
868
+ gap: this.options.gap ?? require_arrows.arrowGap(this.look.arrow)
873
869
  });
874
870
  popover.style.transform = `translate(${pos.x}px, ${pos.y}px)`;
875
871
  popover.setAttribute("data-side", pos.side);
@@ -882,6 +878,142 @@ var DomRenderer = class {
882
878
  external.setAttribute("data-side", pos.side);
883
879
  external.style.setProperty("--docent-arrow", `${pos.arrow}px`);
884
880
  }
881
+ this.renderConnector(pos, floating, hole);
882
+ }
883
+ /** Draw the connector for connector arrow styles; clear it otherwise. */
884
+ renderConnector(pos, floating, hole) {
885
+ const style = this.look.arrow;
886
+ if (!require_arrows.isConnector(style)) {
887
+ this.connector?.clear();
888
+ return;
889
+ }
890
+ const mod = this.connectorModule;
891
+ const connector = this.connector;
892
+ if (!mod || !connector) {
893
+ this.loadConnector();
894
+ return;
895
+ }
896
+ const inset = 6;
897
+ const clampX = (x) => Math.min(Math.max(x, hole.x + 10), hole.x + hole.width - 10);
898
+ const clampY = (y) => Math.min(Math.max(y, hole.y + 10), hole.y + hole.height - 10);
899
+ const cx = hole.x + hole.width / 2;
900
+ const cy = hole.y + hole.height / 2;
901
+ const alongX = pos.x + floating.width * (cx < pos.x + floating.width / 2 ? .3 : .7);
902
+ const alongY = pos.y + floating.height * (cy < pos.y + floating.height / 2 ? .3 : .7);
903
+ let from;
904
+ let to;
905
+ switch (pos.side) {
906
+ case "bottom":
907
+ from = {
908
+ x: alongX,
909
+ y: pos.y
910
+ };
911
+ to = {
912
+ x: clampX(cx),
913
+ y: hole.y + hole.height + inset
914
+ };
915
+ break;
916
+ case "top":
917
+ from = {
918
+ x: alongX,
919
+ y: pos.y + floating.height
920
+ };
921
+ to = {
922
+ x: clampX(cx),
923
+ y: hole.y - inset
924
+ };
925
+ break;
926
+ case "right":
927
+ from = {
928
+ x: pos.x,
929
+ y: alongY
930
+ };
931
+ to = {
932
+ x: hole.x + hole.width + inset,
933
+ y: clampY(cy)
934
+ };
935
+ break;
936
+ default:
937
+ from = {
938
+ x: pos.x + floating.width,
939
+ y: alongY
940
+ };
941
+ to = {
942
+ x: hole.x - inset,
943
+ y: clampY(cy)
944
+ };
945
+ }
946
+ const bend = pos.side === "top" || pos.side === "bottom" ? to.x < from.x ? 1 : -1 : to.y < from.y ? -1 : 1;
947
+ connector.render(mod.connectorShape(style, from, to, pos.side === "top" || pos.side === "left" ? -bend : bend), this.drawConnector);
948
+ this.drawConnector = false;
949
+ }
950
+ /** Fetch the connector module once, then draw with it. */
951
+ loadConnector() {
952
+ this.connectorLoading ??= Promise.resolve().then(() => require("./connector-D6Al4Deu.cjs")).then((mod) => {
953
+ this.connectorModule = mod;
954
+ if (this.shadow) this.attachConnector(this.shadow, mod);
955
+ this.update();
956
+ });
957
+ }
958
+ /** Add the connector layer and its styles, beneath any popover. */
959
+ attachConnector(shadow, mod) {
960
+ if (this.connector) return;
961
+ const style = this.doc.createElement("style");
962
+ style.textContent = mod.CONNECTOR_STYLES_CSS;
963
+ this.connector = new mod.Connector(this.doc);
964
+ const before = this.popover ?? null;
965
+ shadow.insertBefore(style, before);
966
+ shadow.insertBefore(this.connector.el, before);
967
+ }
968
+ resolveLook(ctx, template) {
969
+ const tour = ctx.tour.options ?? {};
970
+ const step = ctx.step;
971
+ return {
972
+ arrow: step.arrow ?? tour.arrow ?? template?.arrow ?? this.options.arrow ?? "caret",
973
+ spotlight: {
974
+ ...this.options.spotlight,
975
+ ...template?.spotlight,
976
+ ...tour.spotlight,
977
+ ...step.spotlight
978
+ },
979
+ overlay: {
980
+ ...this.options.overlay,
981
+ ...template?.overlay,
982
+ ...tour.overlay,
983
+ ...step.overlay
984
+ }
985
+ };
986
+ }
987
+ /** Expose the look to the stylesheet as host attributes and variables. */
988
+ applyLook(host, look) {
989
+ this.look = look;
990
+ host.setAttribute("data-arrow", look.arrow);
991
+ host.setAttribute("data-ring", look.spotlight.ring ?? "hairline");
992
+ host.setAttribute("data-shape", look.spotlight.shape ?? "rounded");
993
+ host.setAttribute("data-overlay", look.overlay.style ?? "dim");
994
+ const { color, opacity, blur } = look.overlay;
995
+ if (color !== void 0) host.style.setProperty("--docent-overlay", color);
996
+ if (opacity !== void 0) host.style.setProperty("--docent-overlay-opacity", String(opacity));
997
+ if (blur !== void 0) host.style.setProperty("--docent-blur", `${blur}px`);
998
+ else host.style.removeProperty("--docent-blur");
999
+ }
1000
+ /** The current transition duration in ms, from the --docent-duration token. */
1001
+ duration(host) {
1002
+ const raw = this.doc.defaultView?.getComputedStyle(host).getPropertyValue("--docent-duration").trim() ?? "";
1003
+ const n = Number.parseFloat(raw);
1004
+ if (Number.isNaN(n)) return 220;
1005
+ return raw.endsWith("ms") ? n : n * 1e3;
1006
+ }
1007
+ /** Disable transitions for this frame so scroll-driven moves stay glued to the target. */
1008
+ markTracking() {
1009
+ const host = this.host;
1010
+ if (!host) return;
1011
+ host.setAttribute("data-tracking", "");
1012
+ if (this.trackingFrame !== void 0) cancelAnimationFrame(this.trackingFrame);
1013
+ this.trackingFrame = requestAnimationFrame(() => {
1014
+ this.trackingFrame = void 0;
1015
+ host.removeAttribute("data-tracking");
1016
+ });
885
1017
  }
886
1018
  /**
887
1019
  * The visible area in layout-viewport coordinates. Uses the visual viewport
@@ -1018,7 +1150,9 @@ var DomRenderer = class {
1018
1150
  shadow.appendChild(style);
1019
1151
  const overlay = new Overlay(this.doc);
1020
1152
  shadow.appendChild(overlay.el);
1153
+ shadow.appendChild(overlay.ring);
1021
1154
  shadow.appendChild(overlay.blocker);
1155
+ if (this.connectorModule) this.attachConnector(shadow, this.connectorModule);
1022
1156
  this.doc.body.appendChild(host);
1023
1157
  this.host = host;
1024
1158
  this.shadow = shadow;
@@ -1032,6 +1166,7 @@ var DomRenderer = class {
1032
1166
  this.frame = void 0;
1033
1167
  this.popover?.remove();
1034
1168
  this.popover = void 0;
1169
+ this.connector?.clear();
1035
1170
  this.arrow = void 0;
1036
1171
  this.ctx = void 0;
1037
1172
  this.target = null;
@@ -1075,7 +1210,7 @@ var DomRenderer = class {
1075
1210
  if (this.frame !== void 0) return;
1076
1211
  this.frame = requestAnimationFrame(() => {
1077
1212
  this.frame = void 0;
1078
- this.update();
1213
+ this.update(true);
1079
1214
  });
1080
1215
  };
1081
1216
  listen() {
@@ -1346,6 +1481,7 @@ function createDocent(options = {}) {
1346
1481
  });
1347
1482
  }
1348
1483
  //#endregion
1484
+ exports.CONNECTOR_STYLES = require_arrows.CONNECTOR_STYLES;
1349
1485
  exports.DEFAULT_LABELS = DEFAULT_LABELS;
1350
1486
  exports.DomRenderer = DomRenderer;
1351
1487
  exports.DomTourController = DomTourController;
@@ -1353,6 +1489,7 @@ exports.NAME_ATTRIBUTE = NAME_ATTRIBUTE;
1353
1489
  exports.Overlay = Overlay;
1354
1490
  exports.THEME_VARS = THEME_VARS;
1355
1491
  exports.applyTheme = applyTheme;
1492
+ exports.arrowGap = require_arrows.arrowGap;
1356
1493
  exports.availableSpace = availableSpace;
1357
1494
  exports.buildHeadlessShell = buildHeadlessShell;
1358
1495
  exports.buildPopover = buildPopover;
@@ -1373,6 +1510,7 @@ exports.findOccluder = findOccluder;
1373
1510
  exports.formatProgress = formatProgress;
1374
1511
  exports.holePath = holePath;
1375
1512
  exports.inflate = inflate;
1513
+ exports.isConnector = require_arrows.isConnector;
1376
1514
  exports.isSafeUrl = isSafeUrl;
1377
1515
  exports.mergeThemes = mergeThemes;
1378
1516
  exports.parsePlacement = parsePlacement;