@leavepulse/ui 0.61.0 → 0.62.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.
@@ -1,7 +1,7 @@
1
1
  import { defineComponent, computed, openBlock, createElementBlock, normalizeClass, createVNode, createElementVNode, toDisplayString, createCommentVNode, renderSlot } from "vue";
2
2
  import { tv } from "tailwind-variants";
3
3
  import { _ as _sfc_main$1 } from "./LpIcon-CTuDVTYv.js";
4
- const _hoisted_1 = ["role"];
4
+ const _hoisted_1 = ["role", "aria-busy"];
5
5
  const _hoisted_2 = { class: "min-w-0" };
6
6
  const _hoisted_3 = {
7
7
  key: 0,
@@ -13,7 +13,8 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
13
13
  variant: { default: "info" },
14
14
  title: {},
15
15
  icon: {},
16
- live: { type: [String, Boolean], default: void 0 }
16
+ live: { type: [String, Boolean], default: void 0 },
17
+ busy: { type: Boolean, default: false }
17
18
  },
18
19
  setup(__props) {
19
20
  const alert = tv({
@@ -42,21 +43,22 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
42
43
  };
43
44
  const props = __props;
44
45
  const role = computed(() => {
45
- const live = props.live ?? (props.variant === "danger" ? "assertive" : false);
46
+ const live = props.live ?? (props.variant === "danger" ? "assertive" : props.busy ? "polite" : false);
46
47
  return live === "assertive" ? "alert" : live === "polite" ? "status" : void 0;
47
48
  });
48
49
  const classes = computed(() => alert({ variant: props.variant }));
49
- const resolvedIcon = computed(() => props.icon ?? ICONS[props.variant ?? "info"]);
50
+ const resolvedIcon = computed(() => props.icon ?? (props.busy ? "lucide:loader-circle" : ICONS[props.variant ?? "info"]));
50
51
  const iconTone = computed(() => ICON_TONE[props.variant ?? "info"]);
51
52
  return (_ctx, _cache) => {
52
53
  return openBlock(), createElementBlock("div", {
53
54
  class: normalizeClass(classes.value),
54
- role: role.value
55
+ role: role.value,
56
+ "aria-busy": __props.busy || void 0
55
57
  }, [
56
58
  createVNode(_sfc_main$1, {
57
59
  name: resolvedIcon.value,
58
60
  size: 18,
59
- class: normalizeClass(["mt-0.5 shrink-0", iconTone.value]),
61
+ class: normalizeClass(["mt-0.5 shrink-0", iconTone.value, __props.busy ? "animate-spin motion-reduce:animate-none" : ""]),
60
62
  "aria-hidden": "true"
61
63
  }, null, 8, ["name", "class"]),
62
64
  createElementVNode("div", _hoisted_2, [
@@ -3,7 +3,7 @@ import { provideHintSystem } from "../composables/useHintSystem.js";
3
3
  import { useLabels } from "../composables/useLpConfig.js";
4
4
  import { _ as _sfc_main$3 } from "./LpActionBar-BsSp66S_.js";
5
5
  import { _ as _sfc_main$2 } from "./LpHintLayer-DsclGYcD.js";
6
- import { _ as _sfc_main$1 } from "./LpTour-C29-1wha.js";
6
+ import { _ as _sfc_main$1 } from "./LpTour-ByaL36fN.js";
7
7
  const _sfc_main = /* @__PURE__ */ defineComponent({
8
8
  __name: "LpHints",
9
9
  props: {
@@ -1,4 +1,4 @@
1
- import { defineComponent, useModel, computed, shallowRef, ref, watch, onBeforeUnmount, openBlock, createBlock, Teleport, createElementBlock, withModifiers, Fragment, renderList, normalizeStyle, normalizeClass, createCommentVNode, unref, withCtx, createVNode, renderSlot, mergeModels, nextTick } from "vue";
1
+ import { defineComponent, useModel, computed, shallowRef, ref, watch, onBeforeUnmount, openBlock, createBlock, Teleport, createVNode, Transition, withCtx, createElementBlock, withModifiers, createElementVNode, normalizeStyle, Fragment, renderList, normalizeClass, createCommentVNode, unref, renderSlot, mergeModels, nextTick } from "vue";
2
2
  import { PopoverRoot, PopoverAnchor, PopoverPortal, PopoverContent } from "reka-ui";
3
3
  import { prefersReducedMotion } from "../composables/easing.js";
4
4
  import { hints } from "../composables/useHints.js";
@@ -8,7 +8,7 @@ import { useMediaQuery } from "../composables/useMediaQuery.js";
8
8
  import { scrollToElement } from "../composables/scroll.js";
9
9
  import { PANEL_SURFACE } from "../components/dropdown.js";
10
10
  import { _ as _sfc_main$1 } from "./TourCard-AjN0uhs1.js";
11
- import { holeOf, panelsAround } from "../components/tour.js";
11
+ import { holeOf, panelsAround, holeClip } from "../components/tour.js";
12
12
  const _hoisted_1 = ["aria-label"];
13
13
  const _sfc_main = /* @__PURE__ */ defineComponent({
14
14
  __name: "LpTour",
@@ -55,6 +55,8 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
55
55
  const panels = computed(
56
56
  () => hole.value ? panelsAround(hole.value, viewport.value) : [{ x: 0, y: 0, width: viewport.value.width, height: viewport.value.height }]
57
57
  );
58
+ const radius = ref(8);
59
+ const clip = computed(() => holeClip(hole.value, viewport.value, radius.value));
58
60
  const reference = {
59
61
  getBoundingClientRect: () => {
60
62
  const h = hole.value;
@@ -173,7 +175,6 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
173
175
  cancelAnimationFrame(frame);
174
176
  frame = 0;
175
177
  target.value = null;
176
- box.value = null;
177
178
  runAll(cleanups);
178
179
  }
179
180
  function finish() {
@@ -194,6 +195,8 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
194
195
  (value) => {
195
196
  if (value) {
196
197
  direction = 1;
198
+ box.value = null;
199
+ radius.value = parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--radius-control")) || 8;
197
200
  document.addEventListener("click", onTargetClick, true);
198
201
  document.addEventListener("contextmenu", onTargetClick, true);
199
202
  frame ||= requestAnimationFrame(track);
@@ -226,28 +229,42 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
226
229
  const CARD = `${PANEL_SURFACE} rounded-card w-80 max-w-[calc(100vw-24px)] p-4 text-sm text-ink outline-none`;
227
230
  return (_ctx, _cache) => {
228
231
  return openBlock(), createBlock(Teleport, { to: "body" }, [
229
- open.value ? (openBlock(), createElementBlock("div", {
232
+ createVNode(Transition, {
233
+ "enter-active-class": "transition-opacity duration-[var(--duration-medium)] ease-[var(--ease-emphasized)] motion-reduce:transition-none",
234
+ "leave-active-class": "transition-opacity duration-[var(--duration-medium)] ease-out motion-reduce:transition-none",
235
+ "enter-from-class": "opacity-0",
236
+ "leave-to-class": "opacity-0"
237
+ }, {
238
+ default: withCtx(() => [
239
+ open.value ? (openBlock(), createElementBlock("div", {
240
+ key: 0,
241
+ class: "lp-tour pointer-events-none fixed inset-0 z-(--z-tour)",
242
+ onPointerdown: _cache[0] || (_cache[0] = withModifiers(() => {
243
+ }, ["stop"]))
244
+ }, [
245
+ createElementVNode("div", {
246
+ class: "lp-scrim absolute inset-0 transition-[clip-path] duration-[var(--duration-medium)] ease-[var(--ease-emphasized)] motion-reduce:transition-none",
247
+ style: normalizeStyle({ clipPath: clip.value })
248
+ }, null, 4),
249
+ (openBlock(true), createElementBlock(Fragment, null, renderList(panels.value, (panel, i) => {
250
+ return openBlock(), createElementBlock("div", {
251
+ key: i,
252
+ class: "pointer-events-auto absolute left-0 top-0",
253
+ style: normalizeStyle({ transform: `translate(${panel.x}px, ${panel.y}px)`, width: `${panel.width}px`, height: `${panel.height}px` })
254
+ }, null, 4);
255
+ }), 128)),
256
+ hole.value ? (openBlock(), createElementBlock("div", {
257
+ key: 0,
258
+ class: normalizeClass(["absolute left-0 top-0 rounded-control ring-2 ring-brand transition-[transform,width,height] duration-[var(--duration-medium)] ease-[var(--ease-emphasized)] motion-reduce:transition-none", step.value?.try ? "pointer-events-none animate-pulse" : "pointer-events-auto"]),
259
+ style: normalizeStyle({ transform: `translate(${hole.value.x}px, ${hole.value.y}px)`, width: `${hole.value.width}px`, height: `${hole.value.height}px` })
260
+ }, null, 6)) : createCommentVNode("", true)
261
+ ], 32)) : createCommentVNode("", true)
262
+ ]),
263
+ _: 1
264
+ }),
265
+ unref(wide) ? (openBlock(), createBlock(unref(PopoverRoot), {
230
266
  key: 0,
231
- class: "lp-tour pointer-events-none fixed inset-0 z-(--z-tour)",
232
- onPointerdown: _cache[0] || (_cache[0] = withModifiers(() => {
233
- }, ["stop"]))
234
- }, [
235
- (openBlock(true), createElementBlock(Fragment, null, renderList(panels.value, (panel, i) => {
236
- return openBlock(), createElementBlock("div", {
237
- key: i,
238
- class: "lp-scrim pointer-events-auto absolute left-0 top-0 transition-[transform,width,height] duration-[var(--duration-medium)] ease-[var(--ease-emphasized)] motion-reduce:transition-none",
239
- style: normalizeStyle({ transform: `translate(${panel.x}px, ${panel.y}px)`, width: `${panel.width}px`, height: `${panel.height}px` })
240
- }, null, 4);
241
- }), 128)),
242
- hole.value ? (openBlock(), createElementBlock("div", {
243
- key: 0,
244
- class: normalizeClass(["absolute left-0 top-0 rounded-control ring-2 ring-brand transition-[transform,width,height] duration-[var(--duration-medium)] ease-[var(--ease-emphasized)] motion-reduce:transition-none", step.value?.try ? "pointer-events-none animate-pulse" : "pointer-events-auto"]),
245
- style: normalizeStyle({ transform: `translate(${hole.value.x}px, ${hole.value.y}px)`, width: `${hole.value.width}px`, height: `${hole.value.height}px` })
246
- }, null, 6)) : createCommentVNode("", true)
247
- ], 32)) : createCommentVNode("", true),
248
- open.value && unref(wide) ? (openBlock(), createBlock(unref(PopoverRoot), {
249
- key: 1,
250
- open: ready.value
267
+ open: open.value && ready.value
251
268
  }, {
252
269
  default: withCtx(() => [
253
270
  createVNode(unref(PopoverAnchor), { reference }),
@@ -259,7 +276,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
259
276
  "collision-padding": 12,
260
277
  "update-position-strategy": "always",
261
278
  "trap-focus": false,
262
- class: normalizeClass([CARD, "z-[calc(var(--z-tour)+1)] data-[state=open]:animate-[popover-in_var(--duration-medium)_var(--ease-emphasized)] motion-reduce:animate-none"]),
279
+ class: normalizeClass([CARD, "z-[calc(var(--z-tour)+1)] data-[state=open]:animate-[popover-in_var(--duration-medium)_var(--ease-emphasized)] data-[state=closed]:animate-[popover-out_var(--duration-fast)_ease-out] motion-reduce:animate-none"]),
263
280
  role: "dialog",
264
281
  "aria-label": step.value?.title,
265
282
  onPointerdown: _cache[1] || (_cache[1] = withModifiers(() => {
@@ -300,34 +317,39 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
300
317
  ]),
301
318
  _: 3
302
319
  }, 8, ["open"])) : createCommentVNode("", true),
303
- open.value && !unref(wide) && ready.value ? (openBlock(), createElementBlock("div", {
304
- key: 2,
305
- class: normalizeClass([CARD, "pointer-events-auto fixed inset-x-3 bottom-3 z-[calc(var(--z-tour)+1)] w-auto animate-[rise-in_var(--duration-medium)_var(--ease-emphasized)] motion-reduce:animate-none"]),
306
- role: "dialog",
307
- "aria-label": step.value?.title,
308
- onPointerdown: _cache[5] || (_cache[5] = withModifiers(() => {
309
- }, ["stop"]))
310
- }, [
311
- renderSlot(_ctx.$slots, "card", {
312
- step: step.value,
313
- index: index.value,
314
- next,
315
- back,
316
- dismiss
317
- }, () => [
318
- createVNode(_sfc_main$1, {
319
- step: step.value,
320
- index: index.value,
321
- last: last.value,
322
- progress: progress.value,
323
- labels: unref(labels),
324
- onNext: next,
325
- onBack: back,
326
- onPerform: perform,
327
- onDismiss: dismiss
328
- }, null, 8, ["step", "index", "last", "progress", "labels"])
329
- ])
330
- ], 42, _hoisted_1)) : createCommentVNode("", true)
320
+ createVNode(Transition, { "leave-active-class": "animate-[rise-out_var(--duration-fast)_ease-out] motion-reduce:animate-none" }, {
321
+ default: withCtx(() => [
322
+ open.value && !unref(wide) && ready.value ? (openBlock(), createElementBlock("div", {
323
+ key: 0,
324
+ class: normalizeClass([CARD, "pointer-events-auto fixed inset-x-3 bottom-3 z-[calc(var(--z-tour)+1)] w-auto animate-[rise-in_var(--duration-medium)_var(--ease-emphasized)] motion-reduce:animate-none"]),
325
+ role: "dialog",
326
+ "aria-label": step.value?.title,
327
+ onPointerdown: _cache[5] || (_cache[5] = withModifiers(() => {
328
+ }, ["stop"]))
329
+ }, [
330
+ renderSlot(_ctx.$slots, "card", {
331
+ step: step.value,
332
+ index: index.value,
333
+ next,
334
+ back,
335
+ dismiss
336
+ }, () => [
337
+ createVNode(_sfc_main$1, {
338
+ step: step.value,
339
+ index: index.value,
340
+ last: last.value,
341
+ progress: progress.value,
342
+ labels: unref(labels),
343
+ onNext: next,
344
+ onBack: back,
345
+ onPerform: perform,
346
+ onDismiss: dismiss
347
+ }, null, 8, ["step", "index", "last", "progress", "labels"])
348
+ ])
349
+ ], 42, _hoisted_1)) : createCommentVNode("", true)
350
+ ]),
351
+ _: 3
352
+ })
331
353
  ]);
332
354
  };
333
355
  }
@@ -34,6 +34,12 @@ type __VLS_Props = {
34
34
  * of the page from the start should not be shouted on load.
35
35
  */
36
36
  live?: "assertive" | "polite" | false;
37
+ /**
38
+ * Work is under way: the icon becomes a spinning loader (unless `icon` is
39
+ * given, which then spins instead) and the alert is announced politely.
40
+ * A loader drawn as a still icon read as a frozen page.
41
+ */
42
+ busy?: boolean;
37
43
  };
38
44
  declare var __VLS_6: {};
39
45
  type __VLS_Slots = {} & {
@@ -42,6 +48,7 @@ type __VLS_Slots = {} & {
42
48
  declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
43
49
  variant: "info" | "danger" | "success" | "warning";
44
50
  live: "assertive" | "polite" | false;
51
+ busy: boolean;
45
52
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
46
53
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
47
54
  declare const _default: typeof __VLS_export;
@@ -1,4 +1,4 @@
1
- import { _ as _sfc_main } from "../chunks/LpAlert--KLL4NLk.js";
1
+ import { _ as _sfc_main } from "../chunks/LpAlert-BAOujnXy.js";
2
2
  export {
3
3
  _sfc_main as default
4
4
  };
@@ -1,4 +1,4 @@
1
- import { _ as _sfc_main } from "../chunks/LpHints-BTpvisgP.js";
1
+ import { _ as _sfc_main } from "../chunks/LpHints-B_85eSmm.js";
2
2
  export {
3
3
  _sfc_main as default
4
4
  };
@@ -1,4 +1,4 @@
1
- import { _ as _sfc_main } from "../chunks/LpTour-C29-1wha.js";
1
+ import { _ as _sfc_main } from "../chunks/LpTour-ByaL36fN.js";
2
2
  export {
3
3
  _sfc_main as default
4
4
  };
@@ -8,6 +8,15 @@ type Size = {
8
8
  export declare function holeOf(target: Box, padding: number, viewport: Size): Box;
9
9
  /** The four dim panels around a hole: top, bottom, left, right. */
10
10
  export declare function panelsAround(hole: Box, viewport: Size): Box[];
11
+ /**
12
+ * The dimmed, blurred layer as one clip: the whole viewport with the hole cut
13
+ * out (evenodd), corners rounded like the ring drawn around it. One layer
14
+ * instead of the four panels, because each panel blurred the page on its own
15
+ * and the blur's edges showed as seams where they met. The point count never
16
+ * changes, so the clip eases between steps on every engine; with no target
17
+ * the hole shrinks to a dot in the middle.
18
+ */
19
+ export declare function holeClip(hole: Box | null, viewport: Size, radius: number): string;
11
20
  /**
12
21
  * Open the context menu of an element as a right-click would, at its centre.
13
22
  * Lets a tour step show a row's menu without the user hunting for it; the menu
@@ -14,6 +14,27 @@ function panelsAround(hole, viewport) {
14
14
  { x: hole.x + hole.width, y: hole.y, width: Math.max(0, viewport.width - hole.x - hole.width), height: hole.height }
15
15
  ];
16
16
  }
17
+ const CORNER_STEPS = 6;
18
+ function holeClip(hole, viewport, radius) {
19
+ const h = hole ?? { x: viewport.width / 2, y: viewport.height / 2, width: 0, height: 0 };
20
+ const r = Math.max(0, Math.min(radius, h.width / 2, h.height / 2));
21
+ const px = (n) => `${Math.round(n * 100) / 100}px`;
22
+ const corners = [
23
+ [h.x + h.width - r, h.y + r, -90],
24
+ [h.x + h.width - r, h.y + h.height - r, 0],
25
+ [h.x + r, h.y + h.height - r, 90],
26
+ [h.x + r, h.y + r, 180]
27
+ ];
28
+ const points = [];
29
+ for (const [cx, cy, start] of corners) {
30
+ for (let i = 0; i <= CORNER_STEPS; i++) {
31
+ const angle = (start + 90 * i / CORNER_STEPS) * Math.PI / 180;
32
+ points.push(`${px(cx + r * Math.cos(angle))} ${px(cy + r * Math.sin(angle))}`);
33
+ }
34
+ }
35
+ const outer = [`0px 0px`, `${px(viewport.width)} 0px`, `${px(viewport.width)} ${px(viewport.height)}`, `0px ${px(viewport.height)}`, `0px 0px`];
36
+ return `polygon(evenodd, ${[...outer, ...points, points[0]].join(", ")})`;
37
+ }
17
38
  function openContextMenu(el) {
18
39
  const box = el.getBoundingClientRect();
19
40
  el.dispatchEvent(
@@ -32,6 +53,7 @@ function openContextMenu(el) {
32
53
  };
33
54
  }
34
55
  export {
56
+ holeClip,
35
57
  holeOf,
36
58
  openContextMenu,
37
59
  panelsAround
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import { _ as _2 } from "./chunks/LpAddressInput-CzdZguBo.js";
3
3
  import { _ as _3 } from "./chunks/LpMapPicker-DybIlWnF.js";
4
4
  import { OSM_TILES, createNominatimProvider, emptyAddress, formatAddress, project, tileUrl, unproject } from "./components/address.js";
5
5
  import { _ as _4 } from "./chunks/LpActionBar-BsSp66S_.js";
6
- import { _ as _5 } from "./chunks/LpAlert--KLL4NLk.js";
6
+ import { _ as _5 } from "./chunks/LpAlert-BAOujnXy.js";
7
7
  import { _ as _6 } from "./chunks/LpAppShell-BXMNDEpF.js";
8
8
  import { _ as _7 } from "./chunks/LpAutocomplete-D9mY5Tqk.js";
9
9
  import { _ as _8 } from "./chunks/LpAvatar-K6MOMfgn.js";
@@ -105,10 +105,10 @@ import { _ as _75 } from "./chunks/LpThemeSwitcher-ZFFftBQn.js";
105
105
  import { _ as _76 } from "./chunks/LpTilt-B7KHX1zH.js";
106
106
  import { _ as _77 } from "./chunks/LpToaster-D3OisETH.js";
107
107
  import { _ as _78 } from "./chunks/LpTooltip-suFCM34F.js";
108
- import { _ as _79 } from "./chunks/LpTour-C29-1wha.js";
108
+ import { _ as _79 } from "./chunks/LpTour-ByaL36fN.js";
109
109
  import { holeOf, openContextMenu, panelsAround } from "./components/tour.js";
110
110
  import { _ as _80 } from "./chunks/LpHintLayer-DsclGYcD.js";
111
- import { _ as _81 } from "./chunks/LpHints-BTpvisgP.js";
111
+ import { _ as _81 } from "./chunks/LpHints-B_85eSmm.js";
112
112
  import { HINT_SIGNALS, contextMenuTip, createHintSystem, hintKey, hintSummary, localHintStorage, paletteTip, provideHintSystem, runningHintSystem, useHintSystem } from "./composables/useHintSystem.js";
113
113
  import { _ as _82 } from "./chunks/LpTooltipProvider-CftjV7Xc.js";
114
114
  import { _ as _83 } from "./chunks/LpTopologyCanvas-6ozNrQKh.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leavepulse/ui",
3
- "version": "0.61.0",
3
+ "version": "0.62.0",
4
4
  "type": "module",
5
5
  "description": "LeavePulse token-driven Vue component kit.",
6
6
  "repository": {
@@ -45,23 +45,34 @@ const props = withDefaults(
45
45
  * of the page from the start should not be shouted on load.
46
46
  */
47
47
  live?: "assertive" | "polite" | false
48
+ /**
49
+ * Work is under way: the icon becomes a spinning loader (unless `icon` is
50
+ * given, which then spins instead) and the alert is announced politely.
51
+ * A loader drawn as a still icon read as a frozen page.
52
+ */
53
+ busy?: boolean
48
54
  }>(),
49
- { variant: "info", live: undefined },
55
+ { variant: "info", live: undefined, busy: false },
50
56
  )
51
57
 
52
58
  const role = computed(() => {
53
- const live = props.live ?? (props.variant === "danger" ? "assertive" : false)
59
+ const live = props.live ?? (props.variant === "danger" ? "assertive" : props.busy ? "polite" : false)
54
60
  return live === "assertive" ? "alert" : live === "polite" ? "status" : undefined
55
61
  })
56
62
 
57
63
  const classes = computed(() => alert({ variant: props.variant }))
58
- const resolvedIcon = computed(() => props.icon ?? ICONS[props.variant ?? "info"])
64
+ const resolvedIcon = computed(() => props.icon ?? (props.busy ? "lucide:loader-circle" : ICONS[props.variant ?? "info"]))
59
65
  const iconTone = computed(() => ICON_TONE[props.variant ?? "info"])
60
66
  </script>
61
67
 
62
68
  <template>
63
- <div :class="classes" :role="role">
64
- <LpIcon :name="resolvedIcon" :size="18" :class="['mt-0.5 shrink-0', iconTone]" aria-hidden="true" />
69
+ <div :class="classes" :role="role" :aria-busy="busy || undefined">
70
+ <LpIcon
71
+ :name="resolvedIcon"
72
+ :size="18"
73
+ :class="['mt-0.5 shrink-0', iconTone, busy ? 'animate-spin motion-reduce:animate-none' : '']"
74
+ aria-hidden="true"
75
+ />
65
76
  <div class="min-w-0">
66
77
  <p v-if="title" class="font-semibold text-ink">{{ title }}</p>
67
78
  <div :class="['text-muted-strong', title ? 'mt-0.5' : '']">
@@ -37,9 +37,11 @@ export interface TourStep {
37
37
  * scrim, a card beside it says what it is, and the user steps through with the
38
38
  * buttons or the arrow keys.
39
39
  *
40
- * The scrim is four panels around a hole rather than a mask, so the target
41
- * stays clickable (a `try` step waits for exactly that click) and the hole can
42
- * glide between targets on any engine, WebKitGTK included. The card is a reka
40
+ * The dim is one blurred layer clipped around the hole (`holeClip`), so the
41
+ * blur has no seams; four transparent panels around the hole take the clicks,
42
+ * so the target stays clickable (a `try` step waits for exactly that click).
43
+ * Both glide between targets on any engine, WebKitGTK included. Closing fades
44
+ * the whole layer out instead of dropping it. The card is a reka
43
45
  * popover anchored to the hole, so flipping and sliding to stay on screen is
44
46
  * floating-ui's job; on a phone it becomes a sheet at the bottom instead.
45
47
  *
@@ -59,7 +61,7 @@ import { scrollToElement } from "../composables/scroll"
59
61
  import { PANEL_SURFACE } from "./dropdown"
60
62
  import TourCard from "./TourCard.vue"
61
63
  import type { Box } from "../composables/spatialNav"
62
- import { holeOf, panelsAround } from "./tour"
64
+ import { holeClip, holeOf, panelsAround } from "./tour"
63
65
 
64
66
  const props = withDefaults(
65
67
  defineProps<{
@@ -113,6 +115,8 @@ const panels = computed(() =>
113
115
  ? panelsAround(hole.value, viewport.value)
114
116
  : [{ x: 0, y: 0, width: viewport.value.width, height: viewport.value.height }],
115
117
  )
118
+ const radius = ref(8)
119
+ const clip = computed(() => holeClip(hole.value, viewport.value, radius.value))
116
120
  const reference = {
117
121
  getBoundingClientRect: () => {
118
122
  const h = hole.value
@@ -244,7 +248,6 @@ function end() {
244
248
  cancelAnimationFrame(frame)
245
249
  frame = 0
246
250
  target.value = null
247
- box.value = null
248
251
  runAll(cleanups)
249
252
  }
250
253
 
@@ -269,6 +272,8 @@ watch(
269
272
  (value) => {
270
273
  if (value) {
271
274
  direction = 1
275
+ box.value = null
276
+ radius.value = parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--radius-control")) || 8
272
277
  document.addEventListener("click", onTargetClick, true)
273
278
  document.addEventListener("contextmenu", onTargetClick, true)
274
279
  frame ||= requestAnimationFrame(track)
@@ -307,11 +312,21 @@ const CARD = `${PANEL_SURFACE} rounded-card w-80 max-w-[calc(100vw-24px)] p-4 te
307
312
 
308
313
  <template>
309
314
  <Teleport to="body">
315
+ <Transition
316
+ enter-active-class="transition-opacity duration-[var(--duration-medium)] ease-[var(--ease-emphasized)] motion-reduce:transition-none"
317
+ leave-active-class="transition-opacity duration-[var(--duration-medium)] ease-out motion-reduce:transition-none"
318
+ enter-from-class="opacity-0"
319
+ leave-to-class="opacity-0"
320
+ >
310
321
  <div v-if="open" class="lp-tour pointer-events-none fixed inset-0 z-(--z-tour)" @pointerdown.stop>
322
+ <div
323
+ class="lp-scrim absolute inset-0 transition-[clip-path] duration-[var(--duration-medium)] ease-[var(--ease-emphasized)] motion-reduce:transition-none"
324
+ :style="{ clipPath: clip }"
325
+ />
311
326
  <div
312
327
  v-for="(panel, i) in panels"
313
328
  :key="i"
314
- class="lp-scrim pointer-events-auto absolute left-0 top-0 transition-[transform,width,height] duration-[var(--duration-medium)] ease-[var(--ease-emphasized)] motion-reduce:transition-none"
329
+ class="pointer-events-auto absolute left-0 top-0"
315
330
  :style="{ transform: `translate(${panel.x}px, ${panel.y}px)`, width: `${panel.width}px`, height: `${panel.height}px` }"
316
331
  />
317
332
  <div
@@ -321,8 +336,9 @@ const CARD = `${PANEL_SURFACE} rounded-card w-80 max-w-[calc(100vw-24px)] p-4 te
321
336
  :style="{ transform: `translate(${hole.x}px, ${hole.y}px)`, width: `${hole.width}px`, height: `${hole.height}px` }"
322
337
  />
323
338
  </div>
339
+ </Transition>
324
340
 
325
- <PopoverRoot v-if="open && wide" :open="ready">
341
+ <PopoverRoot v-if="wide" :open="open && ready">
326
342
  <PopoverAnchor :reference="reference" />
327
343
  <PopoverPortal>
328
344
  <PopoverContent
@@ -331,7 +347,7 @@ const CARD = `${PANEL_SURFACE} rounded-card w-80 max-w-[calc(100vw-24px)] p-4 te
331
347
  :collision-padding="12"
332
348
  update-position-strategy="always"
333
349
  :trap-focus="false"
334
- :class="[CARD, 'z-[calc(var(--z-tour)+1)] data-[state=open]:animate-[popover-in_var(--duration-medium)_var(--ease-emphasized)] motion-reduce:animate-none']"
350
+ :class="[CARD, 'z-[calc(var(--z-tour)+1)] data-[state=open]:animate-[popover-in_var(--duration-medium)_var(--ease-emphasized)] data-[state=closed]:animate-[popover-out_var(--duration-fast)_ease-out] motion-reduce:animate-none']"
335
351
  role="dialog"
336
352
  :aria-label="step?.title"
337
353
  @pointerdown.stop
@@ -346,6 +362,7 @@ const CARD = `${PANEL_SURFACE} rounded-card w-80 max-w-[calc(100vw-24px)] p-4 te
346
362
  </PopoverPortal>
347
363
  </PopoverRoot>
348
364
 
365
+ <Transition leave-active-class="animate-[rise-out_var(--duration-fast)_ease-out] motion-reduce:animate-none">
349
366
  <div
350
367
  v-if="open && !wide && ready"
351
368
  :class="[CARD, 'pointer-events-auto fixed inset-x-3 bottom-3 z-[calc(var(--z-tour)+1)] w-auto animate-[rise-in_var(--duration-medium)_var(--ease-emphasized)] motion-reduce:animate-none']"
@@ -357,5 +374,6 @@ const CARD = `${PANEL_SURFACE} rounded-card w-80 max-w-[calc(100vw-24px)] p-4 te
357
374
  <TourCard :step="step" :index="index" :last="last" :progress="progress" :labels="labels" @next="next" @back="back" @perform="perform" @dismiss="dismiss" />
358
375
  </slot>
359
376
  </div>
377
+ </Transition>
360
378
  </Teleport>
361
379
  </template>
@@ -29,6 +29,37 @@ export function panelsAround(hole: Box, viewport: Size): Box[] {
29
29
  ]
30
30
  }
31
31
 
32
+ const CORNER_STEPS = 6
33
+
34
+ /**
35
+ * The dimmed, blurred layer as one clip: the whole viewport with the hole cut
36
+ * out (evenodd), corners rounded like the ring drawn around it. One layer
37
+ * instead of the four panels, because each panel blurred the page on its own
38
+ * and the blur's edges showed as seams where they met. The point count never
39
+ * changes, so the clip eases between steps on every engine; with no target
40
+ * the hole shrinks to a dot in the middle.
41
+ */
42
+ export function holeClip(hole: Box | null, viewport: Size, radius: number): string {
43
+ const h = hole ?? { x: viewport.width / 2, y: viewport.height / 2, width: 0, height: 0 }
44
+ const r = Math.max(0, Math.min(radius, h.width / 2, h.height / 2))
45
+ const px = (n: number) => `${Math.round(n * 100) / 100}px`
46
+ const corners: [number, number, number][] = [
47
+ [h.x + h.width - r, h.y + r, -90],
48
+ [h.x + h.width - r, h.y + h.height - r, 0],
49
+ [h.x + r, h.y + h.height - r, 90],
50
+ [h.x + r, h.y + r, 180],
51
+ ]
52
+ const points: string[] = []
53
+ for (const [cx, cy, start] of corners) {
54
+ for (let i = 0; i <= CORNER_STEPS; i++) {
55
+ const angle = ((start + (90 * i) / CORNER_STEPS) * Math.PI) / 180
56
+ points.push(`${px(cx + r * Math.cos(angle))} ${px(cy + r * Math.sin(angle))}`)
57
+ }
58
+ }
59
+ const outer = [`0px 0px`, `${px(viewport.width)} 0px`, `${px(viewport.width)} ${px(viewport.height)}`, `0px ${px(viewport.height)}`, `0px 0px`]
60
+ return `polygon(evenodd, ${[...outer, ...points, points[0]].join(", ")})`
61
+ }
62
+
32
63
  /**
33
64
  * Open the context menu of an element as a right-click would, at its centre.
34
65
  * Lets a tour step show a row's menu without the user hunting for it; the menu