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
531 changes: 531 additions & 0 deletions docs/superpowers/plans/2026-08-04-sdk-auto-popup.md

Large diffs are not rendered by default.

107 changes: 107 additions & 0 deletions docs/superpowers/specs/2026-08-04-sdk-auto-popup-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# SDK Auto-Popup (Proactive Teaser) — Design

**Date:** 2026-08-04
**Status:** Approved
**Scope:** `sdk/` only. Integrations (Skyhunter etc.) are follow-up tasks.

## Problem

The floating widget (`mode: 'floating'`) renders a FAB in the corner, but stays
silent until the visitor clicks it. Competitors (reference: Autodesk Assistant)
show a proactive greeting card above the closed FAB — title, message, close
button — which measurably increases engagement. The SDK has `greeting` /
`greetingDelay`, but those render *inside* the opened panel; there is no
pre-open teaser. Integrators must be able to disable the teaser entirely.

## Decision

Add the teaser natively to the SDK custom element (approach chosen over
(a) reusing `greeting` for both roles — conflates in-chat copy with teaser
copy — and (b) leaving it to each integrator site — duplicated work,
inconsistent UX).

## API

Three new `IBridleInitOptions` fields, mirrored as attributes on
`<bridle-chat>` and as `data-*` attributes for script-tag embeds:

| init option | element attribute | data-attr | type | default |
|---|---|---|---|---|
| `popup` | `popup` | `data-popup` | `string` | — (absent ⇒ feature off) |
| `popupTitle` | `popup-title` | `data-popup-title` | `string` | — |
| `popupDelay` | `popup-delay` | `data-popup-delay` | `number` (ms) | `3000` |

- `popup` is the enable switch: no text ⇒ no teaser. This is the "flexible
off-switch" requirement.
- `popup` body renders markdown through the same `marked` + `DOMPurify`
pipeline as chat messages.
- Floating mode only. In `mode: 'inline'` the options are ignored.
- `popupDelay` accepts `number | string` on the prop (attributes arrive as
strings), same coercion pattern as `greetingDelay`.

## Behavior

Show the teaser when ALL hold, `popupDelay` ms after mount:

1. `mode === 'floating'` and the panel is closed (`!isOpen`);
2. `popup` text is non-empty;
3. localStorage flag `bridle:popup-dismissed:<agentId>` is absent.

Transitions:

- Click on the card body → open the panel + set the flag + hide teaser.
- Click on the ✕ button → hide teaser + set the flag.
- Panel opened by any other path (FAB click, `defaultOpen`, programmatic
`open()`) → set the flag and never show; if the timer is pending, cancel it.
- `Escape` while the teaser is visible → same as ✕.

Persistence: dismissal is permanent per agent (localStorage). Storage
unavailable (privacy mode) ⇒ swallow the error and show the teaser anyway —
same try/catch convention as the existing `bridle:anon:<agentId>` key. The
flag is scoped per `agentId` so two widgets on one origin don't interfere.

## Markup & styling

New block inside the `.bridle--floating` root, sibling of `.bridle__fab`:

```html
<div class="bridle__popup" role="status" aria-live="polite">
<button class="bridle__popup-close" aria-label="Dismiss">✕</button>
<div class="bridle__popup-title">👋 Hi, I'm Assistant!</div>
<div class="bridle__popup-body"><!-- sanitized markdown --></div>
</div>
```

- Absolutely positioned above the FAB, right-aligned with it; max-width
~300px; card look (radius, shadow, border) built from existing `--bridle-*`
custom properties so themes, `themeVars`, and `customCss` keep working with
zero changes.
- Enter animation: fade + short upward slide (CSS only, `prefers-reduced-motion`
respected).
- Clickable body gets `cursor: pointer` and hover affordance; the ✕ button
stops propagation so it doesn't open the panel.

## Error handling

- Markdown sanitization identical to message rendering (no new sink).
- All storage reads/writes wrapped in try/catch.
- Timer cleared on unmount (`onBeforeUnmount`), mirroring `greetingTimer`.

## Testing & verification

The SDK has no unit-test harness; verification is:

1. `npm run typecheck` (vue-tsc) and `npm run build` in `sdk/`;
2. manual pass via `example/index.html` — extend the example with
`data-popup*` attributes;
3. manual checks: shows after delay; body click opens chat; ✕ dismisses;
reload after dismiss ⇒ stays hidden; `defaultOpen` ⇒ never shows;
inline mode ⇒ never shows.

## Docs & release

- Document the options in `sdk/README.md` (init options table + script-tag
attrs) and in the example.
- Version: minor bump to `0.14.0` in `sdk/package.json`.
- Release: push tag `sdk-v0.14.0` after merge (CI verifies tag ↔ version and
publishes to npm). Tag is pushed only after explicit user confirmation.
11 changes: 11 additions & 0 deletions example/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ <h1 class="mt-2 text-4xl font-bold tracking-tight">Bridle integration examples</
</p>
<p class="mx-auto mt-3 max-w-xl text-xs text-slate-500">
Latest:
<code>data-popup</code> (v0.14.0) ·
interactive forms — radio / checkbox / select (v0.12.0) ·
empty-state suggestions (v0.11.0) ·
<code>data-greeting</code> (v0.10.0) ·
Expand Down Expand Up @@ -63,6 +64,11 @@ <h2 class="mt-1 text-2xl font-semibold">Basic — one <code>&lt;script&gt;</code
data-greeting="Hi! Drop a screenshot or ask me anything."
data-greeting-delay="2500"

&lt;!-- v0.14.0: proactive teaser above the closed bubble --&gt;
data-popup="Have a question? I can compare plans or book a demo."
data-popup-title="👋 Hi, I'm the Bridle assistant!"
data-popup-delay="2000"

&lt;!-- v0.8.2: replace the default FAB glyph --&gt;
data-fab-icon="/icons/chat.svg"
&gt;&lt;/script&gt;</code></pre>
Expand Down Expand Up @@ -320,6 +326,11 @@ <h2 class="mt-1 text-2xl font-semibold">Interactive forms — radio, checkbox, s
sdk.dataset.greeting =
'Hi! Drop a screenshot or ask me anything.';
sdk.dataset.greetingDelay = '2500';
// v0.14.0 — proactive teaser above the closed bubble
sdk.dataset.popup =
'Have a question? I can compare plans or book a demo.';
sdk.dataset.popupTitle = '👋 Hi, I\'m the Bridle assistant!';
sdk.dataset.popupDelay = '2000';

sdk.onerror = () => setError('Failed to load /sdk/latest.js');
sdk.onload = () => {
Expand Down
24 changes: 24 additions & 0 deletions sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ The script auto-mounts a floating chat bubble in the bottom-right corner. If the
| `data-placeholder` | `Type a message...` | Input placeholder |
| `data-custom-css` | optional | Inline CSS injected into the shadow root |
| `data-stylesheet` | optional | Stylesheet URL(s) loaded into the shadow root (comma-separate for multiple) |
| `data-popup` | off | Proactive teaser card above the closed FAB. Setting a text enables it; markdown supported |
| `data-popup-title` | optional | Bold headline of the teaser |
| `data-popup-delay` | `3000` | Milliseconds after load before the teaser appears; `0` = immediately |

## Programmatic init

Expand All @@ -53,6 +56,27 @@ chat.close()
chat.destroy()
```

## Auto-popup (proactive teaser)

In floating mode the widget can show a small dismissible card above the
closed bubble inviting the visitor to chat:

```js
init({
agentId: 'agent-…',
popup: 'Have a question? I can compare plans or book a demo.',
popupTitle: "👋 Hi, I'm the assistant!",
popupDelay: 2000, // ms, default 3000
})
```

Omit `popup` to disable the teaser entirely. Clicking the card opens the
chat; the ✕ button (or Escape) dismisses it. Either way the choice is
remembered per agent in `localStorage` (`bridle:popup-dismissed:<agentId>`)
and the teaser never re-appears for that visitor. Restyle it via
`customCss` targeting `.bridle__popup`, `.bridle__popup-card`,
`.bridle__popup-title`, `.bridle__popup-body`, `.bridle__popup-close`.

## Headless client (no UI)

```js
Expand Down
4 changes: 2 additions & 2 deletions sdk/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion sdk/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@cleanslice/bridle",
"version": "0.13.3",
"version": "0.14.0",
"description": "Embeddable web chat for Bridle — drop-in <script> or programmatic init.",
"type": "module",
"main": "./dist/bridle.mjs",
Expand Down
Loading