@retoo/borda 0.0.2 → 0.1.1

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/borda.umd.js CHANGED
@@ -5384,6 +5384,8 @@ createHTML: (html) => {
5384
5384
  BordaEvent["ON_TOUR_NEXT"] = "tour:next";
5385
5385
  /** Fired when navigating to the previous step. */
5386
5386
  BordaEvent["ON_TOUR_PREV"] = "tour:prev";
5387
+ /** Fired when a step's target fails to resolve to a DOM element, just before the tour closes. */
5388
+ BordaEvent["ON_TOUR_ERROR"] = "tour:error";
5387
5389
  /** Fired when the ArrowRight key is pressed during an active tour. */
5388
5390
  BordaEvent["ON_KEYBOARD_NEXT"] = "keyboard:next";
5389
5391
  /** Fired when the ArrowLeft key is pressed during an active tour. */
@@ -6186,6 +6188,14 @@ createHTML: (html) => {
6186
6188
  }
6187
6189
  //#endregion
6188
6190
  //#region src/entities/step/model/lib/resolveBordaStepTarget.ts
6191
+ /** Runs `querySelector`, treating a malformed selector as "no match" instead of throwing. */
6192
+ function queryTarget(selector) {
6193
+ try {
6194
+ return document.querySelector(selector);
6195
+ } catch {
6196
+ return null;
6197
+ }
6198
+ }
6189
6199
  function buildSelector(query) {
6190
6200
  const parts = [];
6191
6201
  if (query.id) parts.push(`#${CSS.escape(query.id)}`);
@@ -6209,10 +6219,10 @@ createHTML: (html) => {
6209
6219
  function resolveBordaStepTarget(target) {
6210
6220
  if (!target) return null;
6211
6221
  if (target instanceof HTMLElement) return target;
6212
- if (typeof target === "string") return document.querySelector(target);
6222
+ if (typeof target === "string") return queryTarget(target);
6213
6223
  const selector = buildSelector(target);
6214
6224
  if (!selector) return null;
6215
- return document.querySelector(selector);
6225
+ return queryTarget(selector);
6216
6226
  }
6217
6227
  //#endregion
6218
6228
  //#region src/app/entrypoint/modules/borda-controller/lib/useBordaController.svelte.ts
@@ -6231,6 +6241,23 @@ createHTML: (html) => {
6231
6241
  const hasNextStep = /* @__PURE__ */ user_derived(() => !get(isLastStep));
6232
6242
  const hasPrevStep = /* @__PURE__ */ user_derived(() => get(stepIndex) > 0);
6233
6243
  const tourConfig = /* @__PURE__ */ user_derived(() => config.getConfig().tour);
6244
+ /**
6245
+ * Verifies the current step's target resolved to a DOM element.
6246
+ *
6247
+ * Emits {@link BordaEvent.ON_TOUR_ERROR} followed by {@link BordaEvent.ON_TOUR_CLOSE}
6248
+ * when it didn't, instead of leaving the widget rendering nothing for a step
6249
+ * the user can't see or dismiss.
6250
+ *
6251
+ * @returns `true` when there is no step, or its target resolved; `false` when the tour was closed due to an error.
6252
+ */
6253
+ function checkCurrentTarget() {
6254
+ if (!get(step) || get(currentTarget)) return true;
6255
+ const error = new BordaError(`Borda: target for step ${get(currentStep)} did not resolve to an element.`);
6256
+ console.error(error);
6257
+ eventEmitter.emit(BordaEvent.ON_TOUR_ERROR, error);
6258
+ eventEmitter.emit(BordaEvent.ON_TOUR_CLOSE);
6259
+ return false;
6260
+ }
6234
6261
  function apply() {
6235
6262
  set(stepIndex, get(tourConfig)?.startStep ?? 0, true);
6236
6263
  get(tourConfig)?.onStart?.();
@@ -6244,6 +6271,7 @@ createHTML: (html) => {
6244
6271
  await entering?.prepareElement?.();
6245
6272
  entering?.onBeforeHighlighted?.();
6246
6273
  set(stepIndex, index, true);
6274
+ if (!checkCurrentTarget()) return;
6247
6275
  entering?.onHighlighted?.();
6248
6276
  eventEmitter.emit(BordaEvent.ON_TOUR_STEP_CHANGE, get(currentStep));
6249
6277
  }
@@ -6335,7 +6363,8 @@ createHTML: (html) => {
6335
6363
  prev,
6336
6364
  next,
6337
6365
  finish
6338
- }
6366
+ },
6367
+ checkCurrentTarget
6339
6368
  };
6340
6369
  }
6341
6370
  //#endregion
@@ -8416,11 +8445,9 @@ createHTML: (html) => {
8416
8445
  const padding = /* @__PURE__ */ user_derived(getPadding);
8417
8446
  const autoPlacement = /* @__PURE__ */ user_derived(getAutoPlacement);
8418
8447
  const arrowSide = /* @__PURE__ */ user_derived(getArrowSide);
8419
- user_effect(() => {
8448
+ /** Reads the live target/tooltip rects and writes the resolved position into state. */
8449
+ function recompute() {
8420
8450
  if (!get(target) || !get(tooltip)) return;
8421
- /** Re-run on scroll so auto-flip/shift recompute against the live viewport. */
8422
- scroll.x;
8423
- scroll.y;
8424
8451
  const layout = useTooltipLayout({
8425
8452
  target: get(target),
8426
8453
  tooltip: get(tooltip),
@@ -8439,6 +8466,19 @@ createHTML: (html) => {
8439
8466
  set(left, layout.position.left, true);
8440
8467
  set(effectivePlacement, layout.placement, true);
8441
8468
  set(arrowOffset, layout.arrowOffset, true);
8469
+ }
8470
+ user_effect(() => {
8471
+ if (!get(tooltip)) return;
8472
+ /** Re-run when the tooltip's own size changes (e.g. an image finishes loading). */
8473
+ const observer = new ResizeObserver(recompute);
8474
+ observer.observe(get(tooltip));
8475
+ return () => observer.disconnect();
8476
+ });
8477
+ user_effect(() => {
8478
+ /** Re-run on scroll so auto-flip/shift recompute against the live viewport. */
8479
+ scroll.x;
8480
+ scroll.y;
8481
+ recompute();
8442
8482
  });
8443
8483
  return {
8444
8484
  get top() {
@@ -8923,9 +8963,8 @@ createHTML: (html) => {
8923
8963
  getDuration: () => $$props.animation.overlay.transitionDuration
8924
8964
  });
8925
8965
  useFocusTrap({ getContainer: () => get(bordaContainer)?.getElements()?.root ?? null });
8926
- const size = /* @__PURE__ */ user_derived(() => $$props.config.size);
8927
- const shape = /* @__PURE__ */ user_derived(() => $$props.config.shape);
8928
8966
  const rootClasses = /* @__PURE__ */ user_derived(() => ["rb"]);
8967
+ /** Container chrome isn't per-step content, so it always tracks the live config. */
8929
8968
  const containerProps = /* @__PURE__ */ user_derived(() => ({
8930
8969
  ...$$props.config.container,
8931
8970
  customClasses: {
@@ -8933,13 +8972,6 @@ createHTML: (html) => {
8933
8972
  root: [$$props.config.container?.customClasses?.root, containerAnimator.classes]
8934
8973
  }
8935
8974
  }));
8936
- const tourOverlayProps = /* @__PURE__ */ user_derived(() => $$props.config.tourOverlay);
8937
- const tourTooltipProps = /* @__PURE__ */ user_derived(() => $$props.config.tourTooltip);
8938
- const tourImageProps = /* @__PURE__ */ user_derived(() => $$props.config.tourImage);
8939
- const tourButtonsProps = /* @__PURE__ */ user_derived(() => $$props.config.tourButtons);
8940
- const tourProgressProps = /* @__PURE__ */ user_derived(() => $$props.config.tourProgress);
8941
- const tourSkipProps = /* @__PURE__ */ user_derived(() => $$props.config.tourSkip);
8942
- const closeButtonProps = /* @__PURE__ */ user_derived(() => $$props.config.closeButton);
8943
8975
  const tourTooltipAnimation = /* @__PURE__ */ user_derived(() => $$props.animation.isEnabled ? $$props.animation.tooltip : false);
8944
8976
  const isAnimated = /* @__PURE__ */ user_derived(() => $$props.animation.isEnabled);
8945
8977
  const hasGlide = /* @__PURE__ */ user_derived(() => $$props.animation.tooltip.transition === BordaTooltipTransition.GLIDE);
@@ -8947,7 +8979,22 @@ createHTML: (html) => {
8947
8979
  const isStepSettling = /* @__PURE__ */ user_derived(() => $$props.scroll.isScrolling || overlayTransition.isActive);
8948
8980
  /** In fade mode, hide the tooltip while the step settles, then fade it in at the new position. */
8949
8981
  const isHidden = /* @__PURE__ */ user_derived(() => !get(hasGlide) && get(isAnimated) && get(isStepSettling) && !get(isShownOnScroll));
8950
- const step = /* @__PURE__ */ user_derived(() => $$props.controller.step);
8982
+ /** Seeded once from the initial step so the first render has content; kept in sync by the effect below. */
8983
+ let displayed = /* @__PURE__ */ state(proxy({
8984
+ step: untrack(() => $$props.controller.step),
8985
+ target: untrack(() => $$props.controller.currentTarget),
8986
+ currentStep: untrack(() => $$props.controller.currentStep),
8987
+ config: untrack(() => $$props.config)
8988
+ }));
8989
+ const size = /* @__PURE__ */ user_derived(() => get(displayed).config.size);
8990
+ const shape = /* @__PURE__ */ user_derived(() => get(displayed).config.shape);
8991
+ const tourOverlayProps = /* @__PURE__ */ user_derived(() => get(displayed).config.tourOverlay);
8992
+ const tourTooltipProps = /* @__PURE__ */ user_derived(() => get(displayed).config.tourTooltip);
8993
+ const tourImageProps = /* @__PURE__ */ user_derived(() => get(displayed).config.tourImage);
8994
+ const tourButtonsProps = /* @__PURE__ */ user_derived(() => get(displayed).config.tourButtons);
8995
+ const tourProgressProps = /* @__PURE__ */ user_derived(() => get(displayed).config.tourProgress);
8996
+ const tourSkipProps = /* @__PURE__ */ user_derived(() => get(displayed).config.tourSkip);
8997
+ const closeButtonProps = /* @__PURE__ */ user_derived(() => get(displayed).config.closeButton);
8951
8998
  const tourComponents = /* @__PURE__ */ user_derived(() => {
8952
8999
  const components = get(bordaTour)?.getComponents();
8953
9000
  return {
@@ -8979,6 +9026,34 @@ createHTML: (html) => {
8979
9026
  if ($$props.config.visibility?.isHidden) return;
8980
9027
  $$props.scroll.scrollTo($$props.controller.currentTarget);
8981
9028
  });
9029
+ user_effect(() => {
9030
+ if (!get(isHidden)) {
9031
+ set(displayed, {
9032
+ step: $$props.controller.step,
9033
+ target: $$props.controller.currentTarget,
9034
+ currentStep: $$props.controller.currentStep,
9035
+ config: $$props.config
9036
+ }, true);
9037
+ return;
9038
+ }
9039
+ /**
9040
+ * `step`/`currentTarget`/`currentStep`/`config` (incl. per-step overrides
9041
+ * like `tourProgress`) are live and swap the instant the step index
9042
+ * changes, so binding the tooltip/overlay to them directly would render
9043
+ * the new step's content immediately — visible for a frame before the
9044
+ * fade-out even starts. Keep showing the outgoing step's snapshot while
9045
+ * it fades out, and only swap once it's actually invisible.
9046
+ */
9047
+ const timeoutId = setTimeout(() => {
9048
+ set(displayed, {
9049
+ step: $$props.controller.step,
9050
+ target: $$props.controller.currentTarget,
9051
+ currentStep: $$props.controller.currentStep,
9052
+ config: $$props.config
9053
+ }, true);
9054
+ }, $$props.animation.tooltip.exitDuration);
9055
+ return () => clearTimeout(timeoutId);
9056
+ });
8982
9057
  onMount(() => {
8983
9058
  $$props.mount();
8984
9059
  });
@@ -8998,7 +9073,7 @@ createHTML: (html) => {
8998
9073
  var consequent = ($$anchor) => {
8999
9074
  bind_this(BordaTourOverlay($$anchor, spread_props({
9000
9075
  get targetElement() {
9001
- return $$props.controller.currentTarget;
9076
+ return get(displayed).target;
9002
9077
  },
9003
9078
  get isAnimated() {
9004
9079
  return get(isAnimated);
@@ -9012,13 +9087,13 @@ createHTML: (html) => {
9012
9087
  var consequent_1 = ($$anchor) => {
9013
9088
  bind_this(BordaTour($$anchor, {
9014
9089
  get step() {
9015
- return get(step);
9090
+ return get(displayed).step;
9016
9091
  },
9017
9092
  get currentTarget() {
9018
- return $$props.controller.currentTarget;
9093
+ return get(displayed).target;
9019
9094
  },
9020
9095
  get currentStep() {
9021
- return $$props.controller.currentStep;
9096
+ return get(displayed).currentStep;
9022
9097
  },
9023
9098
  get totalSteps() {
9024
9099
  return $$props.controller.totalSteps;
@@ -9082,7 +9157,7 @@ createHTML: (html) => {
9082
9157
  append($$anchor, fragment_1);
9083
9158
  };
9084
9159
  if_block(node_1, ($$render) => {
9085
- if ($$props.controller.currentTarget && get(step)) $$render(consequent_2);
9160
+ if ($$props.controller.currentTarget && $$props.controller.step) $$render(consequent_2);
9086
9161
  });
9087
9162
  append($$anchor, fragment);
9088
9163
  },
@@ -9213,6 +9288,11 @@ createHTML: (html) => {
9213
9288
  config.getConfig().tour?.onClose?.();
9214
9289
  await unmount$1(instance);
9215
9290
  });
9291
+ eventEmitter.once(BordaEvent.ON_TOUR_ERROR, (error) => {
9292
+ config.getConfig().tour?.onError?.(error);
9293
+ });
9294
+ /** Validate the initial step now that ON_TOUR_ERROR/ON_TOUR_CLOSE listeners above are registered. */
9295
+ controller.checkCurrentTarget();
9216
9296
  resolve(instance);
9217
9297
  },
9218
9298
  unmount: () => {
@@ -9255,7 +9335,7 @@ createHTML: (html) => {
9255
9335
  }
9256
9336
  return {
9257
9337
  NAME: "@retoo/borda",
9258
- VERSION: "0.0.2",
9338
+ VERSION: "0.1.1",
9259
9339
  mount: mount$1,
9260
9340
  unmount: unmount$1,
9261
9341
  highlight: useBordaHighlight(mount$1).highlight
package/dist/index.d.ts CHANGED
@@ -16,6 +16,8 @@ declare enum BordaEvent {
16
16
  ON_TOUR_NEXT = "tour:next",
17
17
  /** Fired when navigating to the previous step. */
18
18
  ON_TOUR_PREV = "tour:prev",
19
+ /** Fired when a step's target fails to resolve to a DOM element, just before the tour closes. */
20
+ ON_TOUR_ERROR = "tour:error",
19
21
  /** Fired when the ArrowRight key is pressed during an active tour. */
20
22
  ON_KEYBOARD_NEXT = "keyboard:next",
21
23
  /** Fired when the ArrowLeft key is pressed during an active tour. */
@@ -39,11 +41,11 @@ type BordaEventMap<T = unknown> = Map<BordaEvent, Set<BordaEventHandler<T>>>;
39
41
  /** Pub/sub event bus for communication between borda components. */
40
42
  interface BordaEventEmitter {
41
43
  /** Subscribe a handler to an event. */
42
- on(eventName: BordaEvent, handler: BordaEventHandler): void;
44
+ on<T = unknown>(eventName: BordaEvent, handler: BordaEventHandler<T>): void;
43
45
  /** Subscribe a handler to an event; auto-unsubscribes after first call. */
44
- once(eventName: BordaEvent, handler: BordaEventHandler): void;
46
+ once<T = unknown>(eventName: BordaEvent, handler: BordaEventHandler<T>): void;
45
47
  /** Unsubscribe a specific handler from an event. */
46
- off(eventName: BordaEvent, handler: BordaEventHandler): void;
48
+ off<T = unknown>(eventName: BordaEvent, handler: BordaEventHandler<T>): void;
47
49
  /** Emit an event, calling all subscribed handlers with optional data. */
48
50
  emit<T>(eventName: BordaEvent, data?: T): void;
49
51
  /** Remove all handlers for a specific event. */
@@ -888,6 +890,8 @@ interface UseBordaControllerReturns {
888
890
  apply: () => void;
889
891
  /** Reactive public API for controlling the tour. */
890
892
  api: BordaControllerApi;
893
+ /** Verifies the current step's target resolved; closes the tour and returns `false` if not. */
894
+ checkCurrentTarget: () => boolean;
891
895
  }
892
896
 
893
897
  /**
@@ -1226,6 +1230,17 @@ interface BordaTourProgressRef {
1226
1230
  getElements: () => BordaTourProgressElements;
1227
1231
  }
1228
1232
 
1233
+ /**
1234
+ * Base error class for all borda runtime errors.
1235
+ *
1236
+ * Using a named subclass makes borda errors easy to identify
1237
+ * in stack traces and `instanceof` checks.
1238
+ */
1239
+ declare class BordaError extends Error {
1240
+ readonly name = "BordaError";
1241
+ constructor(message: string);
1242
+ }
1243
+
1229
1244
  /** Props for the BordaTour step composition component. */
1230
1245
  interface BordaTourProps {
1231
1246
  step: BordaStep;
@@ -1235,9 +1250,7 @@ interface BordaTourProps {
1235
1250
  size: ComponentSize;
1236
1251
  shape: ComponentShape;
1237
1252
  isSkipChecked: boolean;
1238
- /** Hides the tooltip (fade transition) while scrolling/transitioning between steps. */
1239
1253
  isHidden: boolean;
1240
- /** Enables position gliding of the tooltip between steps (glide transition). */
1241
1254
  hasGlide: boolean;
1242
1255
  tourTooltipAnimation: BordaTourTooltipAnimation | false;
1243
1256
  tourTooltipProps: Partial<BordaTourTooltipProps>;
@@ -1280,6 +1293,8 @@ interface BordaTourConfig {
1280
1293
  onFinish: () => void;
1281
1294
  /** Called when the tour is dismissed before completion (close button, backdrop or Escape), before the unmount animation. */
1282
1295
  onClose: () => void;
1296
+ /** Called when a step's target fails to resolve to a DOM element, just before the tour closes. */
1297
+ onError: (error: BordaError) => void;
1283
1298
  /**
1284
1299
  * Enable keyboard navigation: ArrowRight/ArrowLeft for next/prev, Escape to close.
1285
1300
  * Defaults to `true`. Set to `false` to disable all keyboard handling.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@retoo/borda",
3
- "version": "0.0.2",
3
+ "version": "0.1.1",
4
4
  "description": "Embeddable onboarding widget for any website. Framework-agnostic, SSR-safe.",
5
5
  "keywords": [
6
6
  "embeddable",