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