Modernize tooling and add an Astro examples playground

Library:
- Update all dev dependencies to latest (vite 4->8, vitest 0.34->4,
  typescript 5.2->6, @types/node 26, dts-bundle-generator 9, prettier 3.8)
- Replace deprecated @vitest/coverage-c8 with @vitest/coverage-v8
- Drop unused ts-node; add jsdom and a jsdom test environment
- ESM vite config, modern tsconfig (ES2020, Bundler resolution),
  Rollup 4 asset-name handling so the CSS still emits as driver.css

Playground:
- Replace the 1,165-line index.html with playground/, a self-contained
  Astro app that imports the library from source with HMR
- Each scenario is its own file under playground/src/examples grouped into
  Highlight / Popover & Buttons / Tour / API, listed in a sidebar and run
  on its own page; shared demo markup lives in Stage.astro
- All 41 original scenarios ported and type-checked against the library types
- Wire `pnpm dev` to the playground; document local development in the readme
This commit is contained in:
Kamran Ahmed
2026-06-23 00:57:27 +01:00
parent 4e4d07d481
commit af71fdd4d1
25 changed files with 5775 additions and 2154 deletions
-1165
View File
File diff suppressed because it is too large Load Diff
+13 -12
View File
@@ -25,7 +25,8 @@
}
},
"scripts": {
"dev": "vite --host",
"dev": "pnpm --dir playground dev",
"playground:install": "pnpm --dir playground install",
"build": "tsc && vite build && dts-bundle-generator --config ./dts-bundle-generator.config.ts",
"test": "vitest",
"format": "prettier . --write"
@@ -37,17 +38,17 @@
"!dist/**/*.js.map"
],
"devDependencies": {
"@types/jsdom": "^21.1.2",
"@types/node": "^20.5.9",
"@vitest/coverage-c8": "^0.32.0",
"dts-bundle-generator": "^8.0.1",
"postcss": "^8.4.29",
"postcss-scss": "^4.0.7",
"prettier": "^3.0.3",
"ts-node": "^10.9.1",
"typescript": "^5.2.2",
"vite": "^4.4.9",
"vitest": "^0.34.3"
"@types/jsdom": "^28.0.3",
"@types/node": "^26.0.0",
"@vitest/coverage-v8": "^4.1.9",
"dts-bundle-generator": "^9.5.1",
"jsdom": "^29.1.1",
"postcss": "^8.5.15",
"postcss-scss": "^4.0.9",
"prettier": "^3.8.4",
"typescript": "^6.0.3",
"vite": "^8.0.16",
"vitest": "^4.1.9"
},
"keywords": [
"driver.js",
+3
View File
@@ -0,0 +1,3 @@
dist/
.astro/
node_modules/
+23
View File
@@ -0,0 +1,23 @@
import { defineConfig } from "astro/config";
import { fileURLToPath } from "node:url";
// The playground imports the library straight from source (../src) so changes
// to driver.js are reflected instantly with HMR — no build step in between.
const driverSource = fileURLToPath(new URL("../src/driver.ts", import.meta.url));
// https://astro.build/config
export default defineConfig({
vite: {
resolve: {
alias: {
"driver.js": driverSource,
},
},
server: {
fs: {
// Allow importing the library source that lives outside this project.
allow: [".."],
},
},
},
});
+17
View File
@@ -0,0 +1,17 @@
{
"name": "driver-playground",
"type": "module",
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "astro dev",
"start": "astro dev",
"build": "astro build",
"preview": "astro preview"
},
"devDependencies": {
"@astrojs/check": "^0.9.9",
"astro": "^7.0.0",
"typescript": "^6.0.3"
}
}
+3369
View File
File diff suppressed because it is too large Load Diff
+9
View File
@@ -0,0 +1,9 @@
allowBuilds:
esbuild: true
sharp: true
minimumReleaseAgeExclude:
- "@astrojs/markdown-satteri@0.3.1"
- astro@7.0.0
onlyBuiltDependencies:
- esbuild
- sharp
+34
View File
@@ -0,0 +1,34 @@
---
import { exampleGroups } from "../examples";
interface Props {
activeId?: string;
}
const { activeId } = Astro.props;
---
<aside class="sidebar">
<a class="sidebar-brand" href="/">
driver.js <span>playground</span>
</a>
<nav>
{
exampleGroups.map(group => (
<section class="sidebar-group">
<h2>{group.title}</h2>
<ul>
{group.examples.map(example => (
<li>
<a href={`/${example.id}`} class={example.id === activeId ? "is-active" : ""}>
{example.title}
</a>
</li>
))}
</ul>
</section>
))
}
</nav>
</aside>
+59
View File
@@ -0,0 +1,59 @@
---
// The shared demo content every example highlights against. Examples reference
// these elements by selector, so keep ids/classes stable when editing.
---
<div class="demo-container">
<header class="page-header">
<h1>driver.js <sup>v1</sup></h1>
<p>A lightweight, no-dependency library to drive the user's focus across the page.</p>
</header>
<h2>Highlight any element</h2>
<p id="large-paragraph-text">
Highlight anything, anywhere on the page — literally anything, including SVG portions, scrollable items and
off-screen elements. Pick an example from the sidebar to see it in action.
</p>
<div class="buttons">
<button id="card-1" type="button">Card One</button>
<button id="card-2" type="button">Card Two</button>
<button id="card-3" type="button">Card Three</button>
<button id="card-4" type="button">Card Four</button>
<button id="card-5" type="button">Card Five</button>
<button id="card-6" type="button">Card Six</button>
</div>
<ul class="feature-list">
<li>Written in TypeScript</li>
<li>Lightweight — only ~5kb gzipped</li>
<li>No dependencies</li>
<li>MIT Licensed</li>
</ul>
<ul id="hooks-list">
<li>Open the console to follow the hooks fired by an example.</li>
</ul>
<p>
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Animi blanditiis consectetur ea eligendi id in inventore
ipsa iure laudantium libero, minus molestias necessitatibus nesciunt non omnis, quasi recusandae tempore voluptates!
</p>
<div id="scrollable-area">
<p>First — scroll down inside this box to reach the highlighted paragraph.</p>
<p>Second — Lorem ipsum dolor sit amet, consectetur adipisicing elit.</p>
<p id="third-scroll-paragraph">Third — even nested scrollable elements are handled correctly.</p>
<p>Fourth — Lorem ipsum dolor sit amet, consectetur adipisicing elit.</p>
<p>Fifth — Lorem ipsum dolor sit amet, consectetur adipisicing elit.</p>
<p>Sixth — Lorem ipsum dolor sit amet, consectetur adipisicing elit.</p>
<p>Seventh — Lorem ipsum dolor sit amet, consectetur adipisicing elit.</p>
<p>Eighth — Lorem ipsum dolor sit amet, consectetur adipisicing elit.</p>
<p>Ninth — Lorem ipsum dolor sit amet, consectetur adipisicing elit.</p>
</div>
<p>
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Animi blanditiis consectetur ea eligendi id in inventore
ipsa iure laudantium libero, minus molestias necessitatibus nesciunt non omnis, quasi recusandae tempore voluptates!
</p>
</div>
+2
View File
@@ -0,0 +1,2 @@
/// <reference path="../.astro/types.d.ts" />
/// <reference types="astro/client" />
+111
View File
@@ -0,0 +1,111 @@
import { driver } from "driver.js";
import type { ExampleGroup } from "./types";
import { basicTourSteps } from "./shared";
export const apiGroup: ExampleGroup = {
title: "API & Hooks",
examples: [
{
id: "hooks",
title: "Lifecycle Hooks",
description: "Log every lifecycle hook to the console as elements are highlighted.",
run() {
const describe = (element?: Element) => element?.textContent?.slice(0, 10) || " - N/A -";
const driverObj = driver({
animate: true,
onDeselected: (element, step) => console.log(`Deselected: ${describe(element)}\n${JSON.stringify(step)}`),
onHighlightStarted: (element, step) =>
console.log(`Highlight Started: ${describe(element)}\n${JSON.stringify(step)}`),
onHighlighted: (element, step) => console.log(`Highlighted: ${describe(element)}\n${JSON.stringify(step)}`),
onDestroyed: (element, step) => console.log(`Destroyed: ${describe(element)}\n${JSON.stringify(step)}`),
});
driverObj.highlight({
element: "#hooks-list",
popover: { title: "Hooks", description: "Open the console to follow each hook as it fires." },
});
window.setTimeout(() => {
driverObj.highlight({
popover: { title: "Popup Hook", description: "There is no element below this popover." },
});
}, 1000);
window.setTimeout(() => {
driverObj.highlight({
element: "ul.feature-list",
popover: { description: "Back to an element again." },
});
}, 2000);
},
},
{
id: "api-test",
title: "API Test",
description: "Read tour state (index, first/last step) inside onPopoverRender.",
run() {
const driverObj = driver({
animate: true,
steps: basicTourSteps,
disableActiveInteraction: true,
showProgress: true,
progressText: "{{current}} of {{total}} done",
onPopoverRender: popover => {
popover.title.innerHTML = `${driverObj.getActiveIndex()} ${driverObj.hasNextStep() ? "Yes" : "No"} ${
driverObj.hasPreviousStep() ? "Yes" : "No"
}`;
popover.description.innerHTML = `${driverObj.isFirstStep() ? "Yes" : "No"} ${
driverObj.isLastStep() ? "Yes" : "No"
}`;
console.log(driverObj.getActiveIndex());
console.log(driverObj.getActiveStep());
},
});
driverObj.drive(4);
},
},
{
id: "is-active",
title: "Is Active?",
description: "Check whether a driver instance is currently active (uses an alert).",
run() {
alert(driver().isActive());
},
},
{
id: "activate-check",
title: "Activate and Check",
description: "Highlight, then report the active status before and after destroying.",
run() {
const driverObj = driver({ showButtons: [] });
driverObj.highlight({
element: "#card-1",
popover: {
title: "Check if driver is active",
description: "This will alert the status after 2 seconds.",
side: "bottom",
align: "start",
},
});
setTimeout(() => {
alert(`Status: ${driverObj.isActive()}. Destroying driver...`);
driverObj.destroy();
setTimeout(() => alert(`Status: ${driverObj.isActive()}`), 0);
}, 2000);
},
},
{
id: "destroy",
title: "Destroy",
description: "Tear down any active driver instance.",
run() {
driver().destroy();
},
},
],
};
+182
View File
@@ -0,0 +1,182 @@
import { driver } from "driver.js";
import type { ExampleGroup } from "./types";
export const highlightGroup: ExampleGroup = {
title: "Highlight",
examples: [
{
id: "simple-highlight",
title: "Simple Highlight",
description: "Highlight a single element without animation.",
run() {
driver({ animate: false }).highlight({
element: "#large-paragraph-text",
popover: {
title: "driver.js",
description:
"Highlight anything, anywhere on the page. Yes, literally anything including SVG portions and scrollable items.",
align: "start",
side: "top",
},
});
},
},
{
id: "animated-highlight",
title: "Animated Highlight",
description: "Highlight with animation and lifecycle hooks logged to the console.",
run() {
driver({
animate: true,
popoverOffset: 10,
stagePadding: 10,
onDeselected: (element, step) => console.log("Deselected element", element, step),
onHighlightStarted: (element, step) => console.log("Started highlighting element", element, step),
onHighlighted: (element, step) => console.log("Highlighted element", element, step),
}).highlight({
element: "h2",
popover: {
title: "MIT License",
description: "A lightweight, no-dependency JavaScript engine to drive the user's focus.",
side: "bottom",
align: "start",
},
});
},
},
{
id: "transition-highlight",
title: "Transition Highlight",
description: "Re-highlight different elements to watch the popover transition between them.",
run() {
const driverObj = driver({
animate: true,
onDeselected: (element, step) => console.log("Deselected element", element, step),
onHighlightStarted: (element, step) => console.log("Started highlighting element", element, step),
onHighlighted: (element, step) => console.log("Highlighted element", element, step),
});
driverObj.highlight({
popover: { title: "driver.js", description: "Highlight anything, anywhere on the page." },
});
window.setTimeout(() => {
driverObj.highlight({
element: ".buttons button:first-child",
popover: { title: "driver.js", description: "Highlight anything, anywhere on the page." },
});
}, 2000);
window.setTimeout(() => {
driverObj.highlight({
popover: { title: "driver.js", description: "Highlight anything, anywhere on the page." },
});
}, 4000);
window.setTimeout(() => {
driverObj.highlight({ element: "h2", popover: { description: "driver.js" } });
}, 6000);
},
},
{
id: "off-screen-highlight",
title: "Off Screen Highlight",
description: "Highlight an element that is taller than the viewport.",
run() {
driver().highlight({
element: ".demo-container",
popover: {
title: "Off Screen Highlight",
description: "The page scrolls to bring partially off-screen elements into view automatically.",
side: "bottom",
align: "start",
},
});
},
},
{
id: "nested-highlight",
title: "Nested Element Highlight",
description: "Highlight a deeply nested inline element.",
run() {
driver().highlight({
element: ".page-header h1 sup",
popover: {
title: "Nested Highlight",
description: "Even tiny nested elements are highlighted precisely without z-index hacks.",
side: "bottom",
align: "start",
},
});
},
},
{
id: "dark-highlight",
title: "Super Dark Highlight",
description: "Increase the overlay opacity for a darker backdrop.",
run() {
driver({ animate: true, overlayOpacity: 0.9 }).highlight({ element: "ul.feature-list" });
},
},
{
id: "dim-highlight",
title: "Super Dim Highlight",
description: "Lower the overlay opacity for a subtle backdrop.",
run() {
driver({ animate: true, overlayOpacity: 0.2 }).highlight({ element: ".buttons" });
},
},
{
id: "backdrop-color",
title: "Backdrop Color",
description: "Tint the overlay with a custom colour.",
run() {
driver({ overlayColor: "blue", overlayOpacity: 0.3 }).highlight({ element: "#card-1" });
},
},
{
id: "scrollable-area",
title: "Scrollable Area",
description: "Highlight an element inside a scrollable container.",
run() {
driver({ animate: true }).highlight({ element: "#scrollable-area" });
},
},
{
id: "inner-scroll-area",
title: "Inner Scroll Area",
description: "Highlight a paragraph nested inside the scrollable container.",
run() {
driver({ animate: true }).highlight({ element: "#third-scroll-paragraph" });
},
},
{
id: "no-element",
title: "No Element",
description: "Show a popover that is not attached to any element.",
run() {
driver({
animate: true,
onDestroyed: (element, step) => console.log("Close modal", element, step),
onDeselected: (element, step) => console.log("Deselected element", element, step),
onHighlightStarted: (element, step) => console.log("Started highlighting element", element, step),
onHighlighted: (element, step) => console.log("Highlighted element", element, step),
}).highlight({
popover: {
showButtons: [],
description:
"<div class='gif-popover'><img style='max-width: 100%' src='https://i.imgur.com/EAQhHu5.gif' /><p>Go and build something cool!</p></div>",
},
});
},
},
{
id: "disallow-close",
title: "Disallow Close",
description: "Prevent the overlay click from closing the highlight.",
run() {
driver({ animate: true, allowClose: false }).highlight({ element: ".buttons" });
},
},
],
};
+15
View File
@@ -0,0 +1,15 @@
import type { Example, ExampleGroup } from "./types";
import { highlightGroup } from "./highlight";
import { popoverGroup } from "./popover";
import { tourGroup } from "./tour";
import { apiGroup } from "./api";
export const exampleGroups: ExampleGroup[] = [highlightGroup, popoverGroup, tourGroup, apiGroup];
export const examples: Example[] = exampleGroups.flatMap(group => group.examples);
export function findExample(id: string | undefined): Example | undefined {
return examples.find(example => example.id === id);
}
export type { Example, ExampleGroup };
+233
View File
@@ -0,0 +1,233 @@
import { driver } from "driver.js";
import type { ExampleGroup } from "./types";
export const popoverGroup: ExampleGroup = {
title: "Popover & Buttons",
examples: [
{
id: "no-buttons",
title: "No Buttons",
description: "Highlight shows no buttons by default.",
run() {
driver().highlight({
element: "#card-1",
popover: {
title: "No Buttons",
description: "No buttons are shown by default for a highlight. Opt into the ones you want.",
},
});
},
},
{
id: "selected-buttons",
title: "Selected Buttons",
description: "Choose which buttons appear via the popover config.",
run() {
driver().highlight({
element: "#card-2",
popover: {
title: "Close Only",
showButtons: ["close"],
description: "Pass `showButtons` on the popover to display only the buttons you need.",
},
});
},
},
{
id: "next-button",
title: "Next Button",
description: "Show only the next and close buttons.",
run() {
const driverObj = driver();
driverObj.highlight({
element: "#card-3",
popover: {
title: "Next and Close",
showButtons: ["close", "next"],
description: "This one only has next and close buttons, nothing else.",
onNextClick: (step, element, opts) => {
console.log("Next Clicked", step, element, opts);
opts.driver.destroy();
},
},
});
},
},
{
id: "previous-button",
title: "Previous Button",
description: "Show only the previous and close buttons.",
run() {
driver().highlight({
element: "#card-4",
popover: {
title: "Previous and Close",
showButtons: ["close", "previous"],
description: "This one only has previous and close buttons, nothing else.",
},
});
},
},
{
id: "next-prev-button",
title: "Next & Previous",
description: "Show the next and previous buttons together.",
run() {
driver().highlight({
element: "#card-5",
popover: {
title: "Next and Previous Only",
showButtons: ["next", "previous"],
description: "This one only has next and previous buttons.",
},
});
},
},
{
id: "close-button",
title: "Close Button",
description: "Show only the close button.",
run() {
driver().highlight({
element: "#card-6",
popover: {
title: "Close Only",
showButtons: ["close"],
description: "This one only has the close button.",
},
});
},
},
{
id: "button-texts",
title: "Button Texts",
description: "Override the button labels from config and popover.",
run() {
driver({ prevBtnText: "<——", nextBtnText: "——>" }).highlight({
element: "#card-1",
popover: {
side: "left",
title: "Custom Button Text",
showButtons: ["close", "next", "previous"],
nextBtnText: "==>",
prevBtnText: "<==",
description: "These button labels are configured on the popover.",
},
});
},
},
{
id: "disabled-buttons",
title: "Disabled Buttons",
description: "Selectively disable individual buttons.",
run() {
driver().highlight({
element: "#card-2",
popover: {
title: "Disabled Buttons",
description: "You can selectively disable buttons as well.",
showButtons: ["next", "previous", "close"],
disableButtons: ["next", "previous"],
},
});
},
},
{
id: "button-listeners",
title: "Button Listeners",
description: "Listen to button clicks globally (uses alerts).",
run() {
const driverObj = driver({
onNextClick: () => alert("Next Clicked"),
onPrevClick: () => alert("Previous Clicked"),
onCloseClick: () => driverObj.destroy(),
});
driverObj.highlight({
popover: {
title: "Global Button Listener",
description: "You can listen to the button clicks globally.",
showButtons: ["close", "next", "previous"],
onPrevClick: () => alert("Overriding — Previous Clicked"),
},
});
},
},
{
id: "custom-classes",
title: "Custom Classes",
description: "Apply a custom class to style the popover.",
run() {
driver({ popoverClass: "custom-driver-popover" }).highlight({
popover: {
popoverClass: "custom-driver-popover",
title: "Custom Classes",
description: "The popover and its buttons use custom classes for styling.",
showButtons: ["close", "next", "previous"],
},
});
},
},
{
id: "popover-hook",
title: "Popover Hook",
description: "Modify the popover in the onPopoverRender hook.",
run() {
driver({
onPopoverRender: popover => {
popover.title.innerText = "Modified Parent";
},
}).highlight({
element: ".page-header",
popover: {
title: "Page Title",
description: "The title is rewritten in the render hook.",
side: "bottom",
align: "start",
onPopoverRender: popover => {
popover.title.innerText = "Modified";
},
},
});
},
},
{
id: "popover-custom-button",
title: "Custom Footer Button",
description: "Append a custom button to the popover footer.",
run() {
const driverObj = driver({
onPopoverRender: popover => {
const firstButton = document.createElement("button");
firstButton.innerText = "First";
popover.footerButtons.appendChild(firstButton);
firstButton.addEventListener("click", () => console.log("First Button Clicked"));
},
steps: [
{ popover: { title: "Some title", description: "Some description" } },
{ popover: { title: "Another title", description: "Some description" } },
{ popover: { title: "Yet another title", description: "Some description" } },
],
});
driverObj.drive();
},
},
{
id: "padding-change",
title: "Padding & Radius",
description: "Tweak the stage padding, radius and popover offset.",
run() {
driver({ stagePadding: 0, popoverOffset: 20, stageRadius: 10 }).highlight({
element: "#card-3",
popover: {
title: "Padding & Radius",
description: "The stage padding, radius and popover offset are all customised here.",
side: "bottom",
align: "start",
},
});
},
},
],
};
+60
View File
@@ -0,0 +1,60 @@
import type { DriveStep } from "driver.js";
// Reused across the tour examples so they stay in sync. Targets elements that
// live in the shared <Stage /> component.
export const basicTourSteps: DriveStep[] = [
{
element: ".page-header",
popover: {
title: "Driver.js",
description: "A powerful, highly customisable and dependency-free way to drive the user's focus across the page.",
side: "bottom",
align: "start",
},
},
{
element: ".page-header h1",
popover: {
title: "No Stacking Issues",
description: "The popover is positioned without juggling z-indexes, so there are no positional surprises.",
side: "left",
align: "start",
},
},
{
element: ".page-header sup",
popover: {
title: "Improved Hooks",
description: "Hooks let you run logic before and after each step is highlighted.",
side: "bottom",
align: "start",
},
},
{
popover: {
title: "No Element",
description: "You can have popovers without elements as well — handy for intros and outros.",
},
},
{
element: ".buttons",
popover: {
title: "Buttons",
description: "Here are some buttons to highlight as a group.",
},
},
{
element: "#scrollable-area",
popover: {
title: "Scrollable Areas",
description: "Tours through scrollable elements work without any extra configuration.",
},
},
{
element: "#third-scroll-paragraph",
popover: {
title: "Nested Scrolls",
description: "Even nested scrollable elements are brought into view.",
},
},
];
+254
View File
@@ -0,0 +1,254 @@
import { driver } from "driver.js";
import type { ExampleGroup } from "./types";
import { basicTourSteps } from "./shared";
export const tourGroup: ExampleGroup = {
title: "Tour",
examples: [
{
id: "animated-tour",
title: "Animated Tour",
description: "A multi-step tour with progress and navigation buttons.",
run() {
driver({
showProgress: true,
showButtons: ["next", "previous", "close"],
steps: basicTourSteps,
}).drive();
},
},
{
id: "non-animated-tour",
title: "Non-Animated Tour",
description: "The same tour with animation disabled and a tinted overlay.",
run() {
driver({
animate: false,
overlayColor: "blue",
overlayOpacity: 0.3,
showProgress: true,
steps: basicTourSteps,
}).drive();
},
},
{
id: "async-tour",
title: "Asynchronous Tour",
description: "Control the tour flow and create elements on the fly via onNextClick.",
run() {
const driverObj = driver({
animate: true,
overlayOpacity: 0.3,
showProgress: true,
progressText: "{{current}} / {{total}}",
steps: [
{
element: ".page-header",
popover: {
title: "Async Driver.js",
description: "Override `onNextClick` to take full control over when the tour advances.",
side: "bottom",
align: "start",
},
},
{
element: ".page-header h1",
popover: {
title: "Async Test",
description: "By overriding `onNextClick` you get control over the tour.",
side: "left",
align: "start",
onNextClick: () => {
const newDiv = document.querySelector<HTMLDivElement>(".dynamic-el") || document.createElement("div");
newDiv.innerHTML = "This is a new Element";
newDiv.style.display = "block";
newDiv.style.padding = "20px";
newDiv.style.backgroundColor = "black";
newDiv.style.color = "white";
newDiv.style.fontSize = "14px";
newDiv.style.position = "fixed";
newDiv.style.top = `${Math.random() * (500 - 30) + 30}px`;
newDiv.style.left = `${Math.random() * (500 - 30) + 30}px`;
newDiv.className = "dynamic-el";
document.body.appendChild(newDiv);
driverObj.moveNext();
},
},
},
{
element: ".dynamic-el",
onDeselected: element => {
element?.parentElement?.removeChild(element);
},
popover: {
title: "Dynamic Elements",
description: "This element was created right before we moved here.",
},
},
{
element: ".page-header sup",
popover: {
title: "Improved Hooks",
description: "Hooks let you run logic before and after each step is highlighted.",
side: "bottom",
align: "start",
},
},
{
popover: {
title: "No Element",
description: "You can now have popovers without elements as well.",
},
},
{
element: "#scrollable-area",
popover: {
title: "Scrollable Areas",
description: "There are no issues with scrollable element tours either.",
},
},
{
element: "#third-scroll-paragraph",
popover: {
title: "Nested Scrolls",
description: "Even nested scrollable elements work.",
},
},
],
});
driverObj.drive();
},
},
{
id: "confirm-exit-tour",
title: "Confirm on Exit",
description: "Ask for confirmation before the tour is destroyed.",
run() {
const driverObj = driver({
animate: true,
overlayColor: "green",
overlayOpacity: 0.3,
steps: basicTourSteps,
onDestroyStarted: () => {
if (driverObj.hasNextStep() && confirm("Are you sure?")) {
driverObj.destroy();
} else {
driverObj.destroy();
}
},
});
driverObj.drive();
},
},
{
id: "progress-tour",
title: "Progress Text",
description: "Show the default progress indicator.",
run() {
driver({ animate: true, steps: basicTourSteps, showProgress: true }).drive();
},
},
{
id: "progress-tour-template",
title: "Progress Text Template",
description: "Customise the progress text template.",
run() {
driver({
animate: true,
steps: basicTourSteps,
showProgress: true,
progressText: "{{current}} of {{total}} done",
}).drive();
},
},
{
id: "reconfigure-steps",
title: "Re-Configuring Steps",
description: "Replace the steps after the driver has been created.",
run() {
const driverObj = driver({ animate: true, steps: basicTourSteps, showProgress: true });
driverObj.setSteps([
{ element: "h1", popover: { description: "This is a new description" } },
{ element: "p", popover: { description: "This is another new description" } },
]);
driverObj.drive();
},
},
{
id: "disable-keyboard-control",
title: "Disable Keyboard Control",
description: "Turn off arrow-key and escape navigation.",
run() {
const driverObj = driver({
animate: true,
steps: basicTourSteps,
showProgress: true,
allowKeyboardControl: false,
});
driverObj.setSteps([
{ element: "h1", popover: { description: "This is a new description" } },
{ element: "p", popover: { description: "This is another new description" } },
]);
driverObj.drive();
},
},
{
id: "tour-button-listeners",
title: "Tour Button Listeners",
description: "Drive the tour manually from global button listeners (uses alerts).",
run() {
const driverObj = driver({
onNextClick: () => {
alert("Next Clicked");
driverObj.moveNext();
},
onPrevClick: () => {
alert("Previous Clicked");
driverObj.movePrevious();
},
onCloseClick: () => driverObj.destroy(),
steps: [
{ popover: { title: "Some title", description: "Some description" } },
{ popover: { title: "Another title", description: "Some description" } },
{ popover: { title: "Yet another title", description: "Some description" } },
],
});
driverObj.drive();
},
},
{
id: "click-overlay-to-next",
title: "Click Overlay to Next",
description: "Advance the tour when the overlay is clicked.",
run() {
driver({
animate: true,
overlayClickBehavior: "nextStep",
steps: basicTourSteps,
}).drive();
},
},
{
id: "click-overlay-to-handle",
title: "Custom Overlay Click",
description: "Handle overlay clicks with a custom callback.",
run() {
driver({
animate: true,
overlayClickBehavior: () => alert("Clicking me"),
steps: basicTourSteps,
}).drive();
},
},
],
};
+15
View File
@@ -0,0 +1,15 @@
export type Example = {
// Unique slug, used as the example's URL (/highlight-simple) and DOM hook.
id: string;
// Sidebar label.
title: string;
// One-line explanation shown above the stage.
description: string;
// Runs the example against the shared demo stage.
run: () => void;
};
export type ExampleGroup = {
title: string;
examples: Example[];
};
@@ -0,0 +1,275 @@
---
import Sidebar from "../components/Sidebar.astro";
import Stage from "../components/Stage.astro";
interface Props {
title?: string;
description?: string;
activeId?: string;
}
const { title, description, activeId } = Astro.props;
const pageTitle = title ? `${title} · driver.js playground` : "driver.js playground";
---
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="robots" content="noindex" />
<title>{pageTitle}</title>
</head>
<body>
<div class="layout">
<Sidebar activeId={activeId} />
<main class="content">
{
title && (
<div class="example-heading">
<h1>{title}</h1>
{description && <p>{description}</p>}
</div>
)
}
<slot />
<Stage />
</main>
</div>
<style is:global>
:root {
--border: #e5e7eb;
--muted: #6b7280;
--accent: #2563eb;
--bg: #ffffff;
--sidebar-bg: #f9fafb;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans",
"Helvetica Neue", sans-serif;
font-size: 14px;
color: #111827;
background: var(--bg);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
*:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.layout {
display: grid;
grid-template-columns: 260px 1fr;
min-height: 100vh;
}
/* Sidebar */
.sidebar {
position: sticky;
top: 0;
align-self: start;
height: 100vh;
overflow-y: auto;
padding: 20px;
border-right: 1px solid var(--border);
background: var(--sidebar-bg);
}
.sidebar-brand {
display: block;
margin-bottom: 20px;
font-size: 18px;
font-weight: 600;
color: #111827;
text-decoration: none;
}
.sidebar-brand span {
color: var(--muted);
font-weight: 400;
}
.sidebar-group {
margin-bottom: 22px;
}
.sidebar-group h2 {
margin: 0 0 8px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--muted);
}
.sidebar-group ul {
list-style: none;
}
.sidebar-group li a {
display: block;
padding: 5px 10px;
border-radius: 6px;
color: #374151;
text-decoration: none;
}
.sidebar-group li a:hover {
background: #eef2ff;
color: var(--accent);
}
.sidebar-group li a.is-active {
background: var(--accent);
color: #fff;
}
/* Content */
.content {
padding: 40px;
}
.example-heading {
max-width: 640px;
margin: 0 auto 10px;
}
.example-heading h1 {
font-size: 24px;
font-weight: 600;
}
.example-heading p {
margin-top: 6px;
color: var(--muted);
}
/* Demo stage */
.demo-container {
display: flex;
flex-direction: column;
max-width: 640px;
margin: 0 auto;
text-align: left;
}
.page-header {
text-align: center;
margin-bottom: 10px;
}
.demo-container p {
line-height: 1.5;
margin-bottom: 15px;
}
.demo-container h1,
.demo-container h2 {
margin: 30px 0 10px;
}
.demo-container h1 {
font-size: 48px;
font-weight: 600;
text-align: center;
}
.demo-container h1 sup {
font-size: 18px;
font-weight: 400;
}
.feature-list,
#hooks-list {
list-style: none;
padding: 0;
margin: 20px 10px 0;
line-height: 1.5;
}
.feature-list li:before,
#hooks-list li:before {
content: "•";
margin-right: 10px;
}
.buttons {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.demo-container button {
all: unset;
border: 1px solid #ccc;
padding: 5px 15px;
border-radius: 5px;
cursor: pointer;
}
#scrollable-area {
height: 300px;
overflow: auto;
border: 1px solid #ccc;
padding: 10px;
border-radius: 5px;
margin: 50px 0;
}
/* Custom popover theme used by the "Custom Classes" example */
.driver-popover.custom-driver-popover {
min-width: 450px;
background: #2d2d2d;
color: white;
}
.driver-popover.custom-driver-popover .driver-popover-title {
font-size: 26px;
}
.driver-popover.custom-driver-popover .driver-popover-description {
font-size: 14px;
}
.driver-popover.custom-driver-popover button {
background: #454545;
border-color: #454545;
text-shadow: none;
color: white;
font-size: 13px;
padding: 7px 10px;
}
.driver-popover.custom-driver-popover button:hover {
background: #575757;
}
.gif-popover {
display: flex;
flex-direction: column;
text-align: center;
}
.gif-popover img {
width: 100%;
height: auto;
margin-bottom: 10px;
}
.gif-popover p {
margin-bottom: 0;
}
</style>
</body>
</html>
+29
View File
@@ -0,0 +1,29 @@
---
import PlaygroundLayout from "../layouts/PlaygroundLayout.astro";
import { examples } from "../examples";
export function getStaticPaths() {
return examples.map(example => ({
params: { example: example.id },
props: { title: example.title, description: example.description },
}));
}
const { example } = Astro.params;
const { title, description } = Astro.props;
---
<PlaygroundLayout title={title} description={description} activeId={example}>
<div id="example-runner" data-example-id={example}></div>
</PlaygroundLayout>
<script>
import { findExample } from "../examples";
const runner = document.getElementById("example-runner");
const example = findExample(runner?.dataset.exampleId);
// Each example page auto-runs its scenario against the shared stage. Navigating
// to another example is a full page load, so the stage always starts clean.
example?.run();
</script>
+8
View File
@@ -0,0 +1,8 @@
---
import PlaygroundLayout from "../layouts/PlaygroundLayout.astro";
---
<PlaygroundLayout
title="Playground"
description="Pick an example from the sidebar — each one runs against the demo content below. The library is imported straight from ../src, so edits hot-reload instantly."
/>
+10
View File
@@ -0,0 +1,10 @@
{
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"paths": {
"driver.js": ["../src/driver.ts"]
}
},
"include": [".astro/types.d.ts", "**/*"],
"exclude": ["dist"]
}
+1019 -965
View File
File diff suppressed because it is too large Load Diff
+19
View File
@@ -48,6 +48,25 @@ Driver.js is written in Vanilla TypeScript, has zero dependencies and is highly
<br>
## Developing locally
The library lives in `src/`. To try your changes against a set of live examples, run the playground — an Astro app in `playground/` that imports the library straight from source, so edits hot-reload instantly:
```sh
pnpm install # install the library's dev dependencies
pnpm run playground:install # one-time install of the playground's dependencies
pnpm dev # start the playground
```
Each example lives in its own file under `playground/src/examples/` and shows up as a page in the sidebar. To add one, drop an entry into the relevant group (`highlight.ts`, `popover.ts`, `tour.ts`, `api.ts`).
Other useful scripts:
```sh
pnpm build # type-check and build the distributable library into dist/
pnpm test # run the unit tests (Vitest)
```
## Contributions
Feel free to submit pull requests, create issues or spread the word.
+8 -7
View File
@@ -1,23 +1,24 @@
{
"compilerOptions": {
"rootDir": "./src",
"target": "ES2019",
"target": "ES2020",
"useDefineForClassFields": true,
"module": "CommonJS",
"lib": ["ES2019", "DOM"],
"moduleResolution": "Node",
"module": "ESNext",
"lib": ["ES2020", "DOM"],
"moduleResolution": "Bundler",
"strict": true,
"sourceMap": true,
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"skipLibCheck": true,
"noEmit": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"forceConsistentCasingInFileNames": true,
"types": ["vite/client", "node"]
"types": ["vite/client"]
},
"include": ["src"],
"exclude": ["**/*.test.ts", "node_modules",
"tests/**", ".history/**"]
"exclude": ["**/*.test.ts", "node_modules", "tests/**", ".history/**"]
}
+8 -5
View File
@@ -1,4 +1,4 @@
/// <reference types="vitest" />
/// <reference types="vitest/config" />
import path from "path";
import { defineConfig } from "vite";
@@ -12,10 +12,10 @@ const fileName = {
const formats = Object.keys(fileName) as Array<keyof typeof fileName>;
module.exports = defineConfig({
export default defineConfig({
base: "./",
build: {
target: "ES2019",
target: "es2020",
lib: {
entry: path.resolve(__dirname, "src/driver.ts"),
name: packageName,
@@ -25,10 +25,13 @@ module.exports = defineConfig({
rollupOptions: {
output: {
assetFileNames: assetInfo => {
return assetInfo.name === "style.css" ? `driver.css` : assetInfo.name as string;
const name = assetInfo.names?.[0] ?? assetInfo.name;
return name?.endsWith(".css") ? `driver.css` : (name as string);
},
},
},
},
test: {},
test: {
environment: "jsdom",
},
});