diff --git a/skills/busker/SKILL.md b/skills/busker/SKILL.md
index 508a2c2..7488411 100644
--- a/skills/busker/SKILL.md
+++ b/skills/busker/SKILL.md
@@ -9,8 +9,8 @@ disable-model-invocation: true
The [README](https://raw.githubusercontent.com/logfoxai/busker/main/README.md) indexes the guides (§ Contents). Read the ones you need before changing a routine.
1. **Do not time page changes.** If a click causes it, use `steps` and let the real click do it. A scene that changes on a timer will drift the first time a duration changes.
-2. **`routes` is the only list of clickable things.** Adding a route wires the scene change *and* the pointer affordance. Never hand-maintain a parallel CSS list of `cursor: pointer` selectors.
-3. **Routes are only for scenes.** Modals, filters, and toggles belong in your own click handlers — busker clicked the element, your code does the rest.
+2. **`routes` is the only list of clickable things.** Adding a route wires the pointer affordance (and a scene change when `scene` is set). Never hand-maintain a parallel CSS list of `cursor: pointer` selectors.
+3. **Filters and modals still use your handlers.** Put them in `routes` without `scene` so they look clickable and do not trip the miss hint; busker clicks the element, your code does the rest.
4. **Put time in `wait`, not `moveFor`.** `wait` is reading time for whatever the last press opened. A slow glide reads as lag.
5. **`press` in `moves` does not click.** Hand-timed mode animates the press only. If you want a real click, it is a `step`.
6. **Never use `event.isTrusted` to tell busker's clicks from a visitor's.** Scripted clicks from any source are untrusted. Busker already tracks its own.
diff --git a/src/busk.spec.ts b/src/busk.spec.ts
index 45bb078..4d57fb9 100644
--- a/src/busk.spec.ts
+++ b/src/busk.spec.ts
@@ -231,6 +231,68 @@ test('a visitor click on nothing lights up what is clickable', (assert) => {
});
+test('a route without a scene is still clickable and does not light the miss hint', (assert) => {
+
+ document.body.innerHTML = `
+
+
+
home
+
+
+
+
+
`;
+
+ const root = document.getElementById('root');
+
+ if (!root) throw new Error('no root');
+
+ root.getBoundingClientRect = (): DOMRect => new DOMRect(0, 0, 800, 600);
+ root.querySelectorAll('*').forEach((el) => {
+ el.getBoundingClientRect = (): DOMRect => {
+ const scene = el.closest('[data-scene]');
+
+ return scene && !scene.classList.contains('is-active')
+ ? new DOMRect(0, 0, 0, 0)
+ : new DOMRect(100, 50, 80, 20);
+ };
+ });
+
+ observers.length = 0;
+
+ const now = 0;
+ const queued: FrameRequestCallback[] = [];
+
+ globalThis.IntersectionObserver = FakeObserver;
+ globalThis.requestAnimationFrame = (cb: FrameRequestCallback): number => queued.push(cb);
+ globalThis.cancelAnimationFrame = (): void => {};
+ performance.now = (): number => now;
+
+ busk(root, {
+ initialScene: 'home',
+ steps: [{click: '[data-nav-item="alerts"]', moveFor: 100, dwell: 0}],
+ routes: [
+ {click: '[data-nav-item="home"]', scene: 'home'},
+ {click: '[data-nav-item="alerts"]', scene: 'list'},
+ {click: '[data-filter]'},
+ ],
+ });
+
+ observers[0]?.fire();
+
+ assert.equal(root.querySelector('[data-filter]')?.classList.contains('is-interactive'), true);
+
+ root.querySelector('[data-filter]')?.click();
+
+ assert.equal(root.classList.contains('is-aside'), true);
+ assert.equal(root.querySelector('[data-scene="home"]')?.classList.contains('is-active'), true);
+ assert.equal(root.querySelector('[data-nav-item="alerts"]')?.classList.contains('is-hint'), false);
+
+});
+
test('every clickable thing looks clickable', (assert) => {
const {root} = stage(routine);
diff --git a/src/busk.ts b/src/busk.ts
index f30efc3..99ac1a6 100644
--- a/src/busk.ts
+++ b/src/busk.ts
@@ -267,7 +267,7 @@ export function busk(root: HTMLElement, routine: Routine): Busker {
return el !== null && root.contains(el);
});
- if (hit) activate(hit.scene);
+ if (hit?.scene) activate(hit.scene);
// The show's own clicks route the mock but must not take it away from itself.
if (clickingItself) return;
diff --git a/src/content/docs/api-reference.md b/src/content/docs/api-reference.md
index 68a2417..b74533e 100644
--- a/src/content/docs/api-reference.md
+++ b/src/content/docs/api-reference.md
@@ -62,9 +62,9 @@ Beats run back to back: a step sets off `wait` after the last one finished.
| Field | Type | What it does |
|---|---|---|
| `click` | `string` | Selector of the clickable element. |
-| `scene` | `string` | `data-scene` to show when it is clicked. |
+| `scene` | `string` | Optional. `data-scene` to show when it is clicked. |
-Every route target also gets `is-interactive`, which is what makes it look clickable.
+Every route target gets `is-interactive`, which is what makes it look clickable. Omit `scene` for filters and other controls your own handlers drive — they still count as a hit, so a visitor click does not light the miss hint.
## `Move`
diff --git a/src/content/docs/routines.md b/src/content/docs/routines.md
index ee2815a..ace1301 100644
--- a/src/content/docs/routines.md
+++ b/src/content/docs/routines.md
@@ -52,20 +52,21 @@ Because the beats are relative, you can drop a step into the middle of a routine
## Routes
-A route says what a click does:
+A route says a click is live. When it also names a scene, that scene comes up:
```typescript
routes: [
{click: '[data-nav-item="alerts"]', scene: 'alerts'},
+ {click: '[data-filter]'},
]
```
Routes do two jobs, which is the reason they are one list:
-1. **They switch scenes** — for the cursor's clicks and for a visitor's, identically.
-2. **They mark what is clickable.** Every route target gets `is-interactive`, which is what gives it a pointer cursor. So the things that look clickable are exactly the things that are, with no CSS list to maintain alongside.
+1. **They mark what is clickable.** Every route target gets `is-interactive`, which is what gives it a pointer cursor. So the things that look clickable are exactly the things that are, with no CSS list to maintain alongside.
+2. **They switch scenes** when `scene` is set — for the cursor's clicks and for a visitor's, identically.
-Anything your own handlers do — opening a modal, filtering a table, toggling a row — needs no route at all. Busker clicked the element; your code took it from there. Routes are only for the part busker owns, which is which scene is up.
+Filters, modals, and toggles still belong in your own handlers. Put them in `routes` *without* a `scene` so they get the pointer and do not trip the miss hint; busker clicked the element, your code took it from there.
## Where the cursor starts
diff --git a/src/content/docs/taking-over.md b/src/content/docs/taking-over.md
index 40e9345..8474dcd 100644
--- a/src/content/docs/taking-over.md
+++ b/src/content/docs/taking-over.md
@@ -4,7 +4,7 @@ A busker plays to whoever is passing. When someone actually walks up, they stop
## What happens on a click
-1. If the click hit a [route](./routines.md#routes) target, that scene comes up — the same code path the cursor's own clicks use.
+1. If the click hit a [route](./routines.md#routes) target with a `scene`, that scene comes up — the same code path the cursor's own clicks use. A route without a `scene` still counts as a hit (no miss hint); your handlers do the rest.
2. The loop stops for good and the root gets `is-aside`, which hides the cursor.
3. If the click hit nothing clickable, every route target gets `is-hint` for 1.5s, so they can see what is live.
diff --git a/src/types.ts b/src/types.ts
index 25662e7..5ad7283 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -29,12 +29,16 @@ export type Step =
dwell?: never;
};
-/** A click on `click` shows scene `scene`. */
+/** A click on `click` is interactive; when `scene` is set, it also shows that scene. */
export interface Route {
/** Selector of the clickable element. */
click: string;
- /** `data-scene` value to show. */
- scene: string;
+ /**
+ * `data-scene` value to show. Omit for filters, modals, and other controls
+ * that your own handlers drive — the target still gets the pointer and
+ * counts as a hit, so a visitor click does not light the miss hint.
+ */
+ scene?: string;
}
/** A cursor glide on a hand-set timeline. */