npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@absolutejs/tour

v0.3.0-beta.4

Published

Element-level, cross-page product tour engine for AbsoluteJS apps. A serializable step protocol plus a spotlight positioning engine (pixel-accurate, follows late layout shifts) and a sessionStorage-backed controller that resumes across full-page navigatio

Readme

@absolutejs/tour

Element-level, cross-page product tour engine for AbsoluteJS apps.

A tour is described by a small, serializable protocol (steps with a target selector, route, placement, and copy) so it can live in code, in a database, or be authored in an admin UI — and rendered by the same engine. The engine gives you:

  • Spotlight positioning — dims the page, highlights one real element, and stays pixel-accurate. It re-measures on a light interval so the highlight follows late layout shifts (a page loading its data and reflowing after the first measure), not just scroll/resize.
  • Cross-page resume — in an MPA each page is its own mount, so moving between steps is a full reload. The controller persists progress in sessionStorage and resumes on the next page at the right step.
  • A shared controller — start/stop the tour from anywhere (a first-visit trigger, a "replay" button), all driving one overlay.

Vue 3 and vue-router are peer dependencies. The engine ships as composables; you render a tiny overlay component with your own styling (see below).

Install

bun add @absolutejs/tour

The protocol

import type { Tutorial } from "@absolutejs/tour";

const tour: Tutorial = {
	slug: "portal-intro",
	trigger: { firstVisitOnly: true, onRoutePrefix: "/portal" },
	steps: [
		{ title: "Welcome", body: "A quick tour.", placement: "center", route: "/dashboard" },
		{
			title: "Your command center",
			body: "Your single best next step is always one click here.",
			route: "/dashboard",
			target: '[data-tour=\"hero\"]',
			placement: "bottom",
		},
		// …intake, matches, network — each navigates and spotlights a real element
	],
};

Add data-tour="…" attributes to the elements you want to spotlight. A step with no target (or one that can't be found) renders as a centered card.

Wiring it up

// shared controller (singleton per storage key)
import { useTourController } from "@absolutejs/tour";
const tour = useTourController("myapp.tour");

// auto-play once on first visit, then stamp your own "seen" marker
if (!account.tourSeenAt) tour.start();

// replay from anywhere (does not re-stamp — pass replay=true)
tour.start(true);

Render an overlay component that consumes useSpotlight (style it however you like — the engine only computes geometry):

<script setup lang="ts">
import { useSpotlight, useTourController } from "@absolutejs/tour";
import { PORTAL_TOUR_STEPS } from "./steps";

const controller = useTourController("myapp.tour");
const emit = defineEmits<{ close: [] }>();
const t = useSpotlight({
	steps: () => PORTAL_TOUR_STEPS,
	controller,
	onClose: () => emit("close"),
});
</script>

<template>
	<Teleport to="body">
		<div v-if="t.active.value && t.step.value" class="tour-root">
			<div class="tour-blocker" :class="{ dim: t.isCentered.value }"></div>
			<div class="tour-spotlight" :style="t.spotlightStyle.value"></div>
			<div
				class="tour-tooltip"
				:class="{ centered: t.isCentered.value }"
				:style="t.isCentered.value ? {} : t.tooltipStyle.value"
			>
				<p>Step {{ t.index.value + 1 }} of {{ t.stepCount.value }}</p>
				<h3>{{ t.step.value.title }}</h3>
				<p>{{ t.step.value.body }}</p>
				<button @click="t.skip">Skip</button>
				<button v-if="!t.isFirst.value" @click="t.back">Back</button>
				<button @click="t.next">{{ t.isLast.value ? "Done" : "Next" }}</button>
			</div>
		</div>
	</Teleport>
</template>

The host app owns: the data-tour anchors, the step content, and where the "seen" marker is stored (so finishing a first-visit run stamps it; a replay does not).

Step actions — demo the product, don't just point at it

Steps stay serializable, so a step references actions by name; the host registers the handlers. onEnter actions run once the step is positioned (sequentially, cancelled if the step changes mid-run); onExit actions run when the step is left — cleanup/restore.

// The page that OWNS the surface registers its demo handler (setup):
import { useTourActions } from "@absolutejs/tour";

const actions = useTourActions();
const unregister = actions.register("matches.demo-swipe", async (ctx) => {
	const direction = ctx.args.direction === "left" ? "left" : "right";
	await swiper.value?.demoSwipe(direction); // ctx.signal aborts long demos
});
onBeforeUnmount(unregister);
// The step invokes it — plain JSON, safe to store in a DB / author in an admin UI:
{
	title: "Swipe or list",
	route: "/portal/matches",
	target: '[data-tour="match-view"]',
	onEnter: [
		{ action: "matches.demo-swipe", args: { direction: "right" } },
		{ action: "wait", args: { ms: 700 } },
		{ action: "matches.demo-swipe", args: { direction: "left" } },
	],
}

Built-ins (no host code needed): click, scroll, wait — each takes an optional selector (default: the step's target). Unknown action names warn and skip so a tutorial authored against an unmounted page degrades instead of breaking the tour. Handlers receive { step, target, args, index, signal, next, back, stop } — a handler can drive the tour itself (e.g. auto-advance when its demo finishes).

Demo data — tour an empty account for free

A tour must be able to show a data-backed surface (matches, pipeline) to a viewer who has no data yet — a fresh signup, an unsubscribed user — without the host paying to source anything. useTourDemo swaps in a constant, fully-typed sample dataset while the tour plays and passes the real data through untouched otherwise:

import { useTourDemo, useTourController } from "@absolutejs/tour";

const controller = useTourController("myapp.tour");
const { data: matches, isDemo } = useTourDemo({
	controller,
	demo: DEMO_MATCHES, // typed PartnerMatch[] — same shape the surface renders
	live: () => realMatches.value,
	mode: () => tutorial.value?.dataMode, // optional per-tutorial override
});

Resolution is per TourDataMode: "auto" (default) shows the viewer's real data when they have it — so a member with sourced matches is toured on their literal matches — and the sample when they don't; "demo" / "live" force one side (Tutorial.dataMode carries the choice in the serialized tutorial). Badge the surface when isDemo is true so sample data is never mistaken for real.

Funnel events — see where viewers bail

Pass onEvent to useSpotlight and every lifecycle moment lands in your analytics: tour_started, step_viewed, step_completed, step_target_missing, action_failed, tour_completed, and — the one that matters — tour_skipped, carrying the exact stepIndex, stepTitle, target, and route (the screen the viewer was on when they'd had enough) plus a reason distinguishing the Skip button from Escape.

useSpotlight({
	steps, controller, onClose,
	tutorialSlug: () => activeTutorial.value?.slug,
	onEvent: (event) => analytics.track(event),
});

Branching & readiness — showIf / skipIf / waitFor

Conditions mirror actions: serializable refs resolved by name against a registry (useTourConditions), with element and media built in.

useTourConditions().register("hasDeals", () => dealCount.value > 0);
{
	title: "Your pipeline",
	showIf: [{ condition: "hasDeals" }],           // all must hold, else skipped
	skipIf: [{ condition: "media", args: { query: "(max-width: 640px)" } }],
	waitFor: { selector: ".pipeline-board", timeoutMs: 5000 }, // hold until ready
}

Skipped steps are hopped in the direction of travel; if everything ahead is skipped the tour completes cleanly.

Mobile variants

Below mobileQuery (default (max-width: 640px)) a step's mobile block overrides its target/placement/copy — or skips it where the anchor doesn't exist on small screens:

{ target: '[data-tour="toolbar-btn"]', mobile: { target: '[data-tour="menu"]', placement: "top" } }
{ target: '[data-tour="desktop-panel"]', mobile: { skip: true } }

CTA buttons

cta: { label: "Try it now", actions: [{ action: "click" }] } renders a button in the card (host template: v-if="step.cta"@click="runCta"); it runs the refs through the action registry and advances unless advance: false.

Auto-play gate — stop nagging people

useTourGate owns the auto-play decision across MANY tutorials: dismissal caps (trigger.maxDismissals, default 2 — after that, manual replay only), oncePerSession, priority when several tutorials match a page, audience showIf predicates, and role matching. State persists in localStorage.

const gate = useTourGate({ roles: () => viewer.roles });
const tutorial = gate.pick(publishedTutorials, route.path);
if (tutorial) {
	gate.recordAutoPlay(tutorial.slug ?? "");
	controller.start();
}
// from your onEvent sink:
//   tour_skipped   → gate.recordDismissal(slug)
//   tour_completed → gate.recordCompletion(slug)

Checklist — "getting started" engine

Typed tasks + completion persistence + progress math; the host renders the panel. completeForTutorial(slug) checks off tasks tied to a tutorial (wire it to the tour_completed event).

const checklist = useTourChecklist({
	id: "onboarding",
	items: () => [
		{ id: "intake", title: "Finish your intake", href: "/portal/intake" },
		{ id: "tour", title: "Take the tour", tutorialSlug: "portal-intro" },
	],
});
// checklist.items → [{...item, done}], checklist.progress → {done,total,percent}

Hotspots — always-on help beacons

Persistent pulsing beacons on tricky UI (independent of any tour) that open an explainer card on click. once: true hides a beacon after it's been opened; dismissals persist.

const spots = useTourHotspots({
	hotspots: () => [
		{ id: "trust-fit", target: '[data-tour="trust-fit"]', title: "Trust & Fit", body: "…" },
	],
	enabled: () => !tourController.active.value,
});
// render spots.beacons (beacon per target) and spots.card (open card) yourself

License

Business Source License 1.1 — see LICENSE. Converts to Apache 2.0 on the Change Date.