Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions skills/busker/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
62 changes: 62 additions & 0 deletions src/busk.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = `<div id="root">
<nav>
<button data-nav-item="home">Home</button>
<button data-nav-item="alerts">Alerts</button>
</nav>
<section data-scene="home" data-nav="home">
<p>home</p>
<button data-filter>Filter</button>
</section>
<section data-scene="list" data-nav="alerts"><button data-row="p0">row</button></section>
<span data-cursor></span>
</div>`;

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<HTMLElement>('[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);
Expand Down
2 changes: 1 addition & 1 deletion src/busk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions src/content/docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 &mdash; they still count as a hit, so a visitor click does not light the miss hint.

## `Move`

Expand Down
9 changes: 5 additions & 4 deletions src/content/docs/routines.md
Original file line number Diff line number Diff line change
Expand Up @@ -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** &mdash; 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 &mdash; for the cursor's clicks and for a visitor's, identically.

Anything your own handlers do &mdash; opening a modal, filtering a table, toggling a row &mdash; 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

Expand Down
2 changes: 1 addition & 1 deletion src/content/docs/taking-over.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 &mdash; 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 &mdash; 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.

Expand Down
10 changes: 7 additions & 3 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down
Loading