onboardme-sdk 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE-v2.md +225 -0
- package/dist/sdk.iife.js +348 -0
- package/package.json +22 -0
- package/src/__tests__/day1.test.ts +37 -0
- package/src/__tests__/day2.test.ts +447 -0
- package/src/__tests__/day3.test.ts +110 -0
- package/src/__tests__/day4.test.ts +115 -0
- package/src/__tests__/day5.test.ts +102 -0
- package/src/__tests__/snapshot-dom-collector.test.ts +153 -0
- package/src/__tests__/snapshot-sender.test.ts +111 -0
- package/src/__tests__/v2-integration.test.ts +305 -0
- package/src/__tests__/v2-positioner.test.ts +115 -0
- package/src/__tests__/v2-renderer.test.ts +189 -0
- package/src/__tests__/v2-types.test.ts +74 -0
- package/src/__tests__/week2-day1.test.ts +62 -0
- package/src/__tests__/week2-day2.test.ts +128 -0
- package/src/__tests__/week2-day3.test.ts +128 -0
- package/src/__tests__/week2-day4.test.ts +177 -0
- package/src/__tests__/week2-day5.test.ts +294 -0
- package/src/__tests__/week3-day1.test.ts +169 -0
- package/src/__tests__/week3-day2.test.ts +267 -0
- package/src/__tests__/week3-day3.test.ts +213 -0
- package/src/__tests__/week3-day4.test.ts +213 -0
- package/src/__tests__/week3-day5.test.ts +350 -0
- package/src/__tests__/week4-day1.test.ts +277 -0
- package/src/__tests__/week4-day2.test.ts +227 -0
- package/src/__tests__/week4-day3.test.ts +323 -0
- package/src/__tests__/week4-day4.test.ts +210 -0
- package/src/__tests__/week4-day5.test.ts +503 -0
- package/src/__tests__/week5-day1.test.ts +152 -0
- package/src/__tests__/week5-day2.test.ts +222 -0
- package/src/__tests__/week5-day3.test.ts +297 -0
- package/src/__tests__/week5-day4.test.ts +306 -0
- package/src/__tests__/week5-day5.test.ts +345 -0
- package/src/__tests__/week7-day5-api-flows.test.ts +353 -0
- package/src/auto-generate/context-collector.ts +47 -0
- package/src/auto-generate/flow-generator-client.ts +97 -0
- package/src/browser.ts +5 -0
- package/src/components/celebration.ts +44 -0
- package/src/components/checklist-css.ts +159 -0
- package/src/components/checklist.ts +295 -0
- package/src/components/modal-css.ts +96 -0
- package/src/components/modal.ts +171 -0
- package/src/components/shadow-host.ts +30 -0
- package/src/core/api-client.ts +39 -0
- package/src/core/api-flows.ts +204 -0
- package/src/core/config.ts +37 -0
- package/src/core/event-batcher.ts +169 -0
- package/src/core/sdk.ts +301 -0
- package/src/detection/user-detection.ts +55 -0
- package/src/index.ts +95 -0
- package/src/snapshot/dom-collector.ts +193 -0
- package/src/snapshot/sender.ts +105 -0
- package/src/storage/event-listener.ts +59 -0
- package/src/storage/progress-tracker.ts +78 -0
- package/src/styles/checklist-css.ts +159 -0
- package/src/styles/checklist.css +166 -0
- package/src/styles/modal-css.ts +96 -0
- package/src/styles/modal.css +102 -0
- package/src/utils/dom.ts +49 -0
- package/src/utils/fingerprint.ts +20 -0
- package/src/utils/logger.ts +17 -0
- package/src/v2/positioner.ts +105 -0
- package/src/v2/renderer.ts +287 -0
- package/src/v2/styles.ts +89 -0
- package/src/v2/types.ts +53 -0
- package/tsconfig.json +11 -0
- package/vite.config.ts +28 -0
- package/vitest.config.ts +7 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# SDK Architecture — v2 (Phase F)
|
|
2
|
+
|
|
3
|
+
This document describes the OnboardMe SDK's v2 architecture, introduced in the Phase A–F rewrite (May 2026). The SDK now acts as a **thin renderer** for server-driven flows, partitioning between v1 (legacy) and v2 (new) rendering paths.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The SDK's job is simple: **fetch flows from the API and render them**. All flow logic, targeting, and publishing decisions happen server-side (Phases A–E). The SDK just receives the final, validated flow and displays it.
|
|
8
|
+
|
|
9
|
+
**Key points:**
|
|
10
|
+
- SDK initialization is synchronous and lightweight
|
|
11
|
+
- v2 flows are fetched via polling (every 60 seconds)
|
|
12
|
+
- Each step type (tooltip, modal, highlight) is rendered into the Shadow DOM
|
|
13
|
+
- User actions (next, skip, complete) trigger step transitions or flow teardown
|
|
14
|
+
- Bundle size: 9.82 KB gzipped (well under 50 KB limit)
|
|
15
|
+
|
|
16
|
+
## Initialization Flow
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
OnboardMe.init(config)
|
|
20
|
+
↓
|
|
21
|
+
validateConfig()
|
|
22
|
+
↓
|
|
23
|
+
getAnonymousId() → generate or retrieve existing ID
|
|
24
|
+
↓
|
|
25
|
+
detectUser() → is this a new user?
|
|
26
|
+
↓
|
|
27
|
+
configureBatcher() → wire event streaming
|
|
28
|
+
↓
|
|
29
|
+
fetchFlowsWithFallback() [async] → GET /v1/flows with checksumHash
|
|
30
|
+
↓
|
|
31
|
+
_renderFlows() → partition & render (v2 or legacy)
|
|
32
|
+
↓
|
|
33
|
+
startFlowsPolling() [async] → poll every 60s, re-render on hash change
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
All async work is deferred via `setTimeout(0)` so `init()` returns immediately and doesn't block the host app's first paint.
|
|
37
|
+
|
|
38
|
+
## v2 vs Legacy Flow Partition
|
|
39
|
+
|
|
40
|
+
The SDK supports both v1 (legacy) and v2 flows. The `_renderFlows()` function in `src/core/sdk.ts` routes each flow to the correct renderer:
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
for (const flow of flows) {
|
|
44
|
+
if (isV2FlowConfig(flow.config ?? flow)) {
|
|
45
|
+
// v2 schema: { steps: [ { type: 'tooltip' | 'modal' | 'highlight', ... } ] }
|
|
46
|
+
renderV2Flow(config, shadowRoot) // → src/v2/renderer.ts
|
|
47
|
+
} else {
|
|
48
|
+
// v1 schema: { steps: [ { type: 'modal' | 'checklist', ... } ] }
|
|
49
|
+
showModal(step, shadowRoot) // → src/components/modal.ts
|
|
50
|
+
renderChecklist(flow, shadowRoot) // → src/components/checklist.ts
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Detection is via `isV2FlowConfig()`, which checks if the flow has the v2 structure.
|
|
56
|
+
|
|
57
|
+
## v2 Renderer (`src/v2/renderer.ts`)
|
|
58
|
+
|
|
59
|
+
The v2 renderer walks the steps array and mounts one DOM element per step.
|
|
60
|
+
|
|
61
|
+
### Step Types
|
|
62
|
+
|
|
63
|
+
| Type | Rendering | User Interaction |
|
|
64
|
+
|------|-----------|------------------|
|
|
65
|
+
| **tooltip** | Card anchored to a target element via selector + placement | Click action button → advance |
|
|
66
|
+
| **modal** | Centered card with semi-transparent backdrop | Click action button → advance |
|
|
67
|
+
| **highlight** | Outline ring around a target element, no card | Click action button → advance |
|
|
68
|
+
|
|
69
|
+
### Rendering Loop
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
renderCurrentStep(index)
|
|
73
|
+
↓
|
|
74
|
+
switch (step.type)
|
|
75
|
+
→ tooltip: mountTooltip(ctx, step)
|
|
76
|
+
→ modal: mountModal(ctx, step)
|
|
77
|
+
→ other: mountHighlight(ctx, step)
|
|
78
|
+
↓
|
|
79
|
+
Wire action button → advance(ctx, step)
|
|
80
|
+
→ action.type = 'next' → ctx.index++, render next step
|
|
81
|
+
→ action.type = 'skip' → teardown (dismiss whole flow)
|
|
82
|
+
→ action.type = 'complete' → teardown (flow finished)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### DOM Structure
|
|
86
|
+
|
|
87
|
+
All steps render into a shared Shadow Root. When advancing to the next step, the previous step's DOM is cleaned up before the new one is mounted — no visual overlap.
|
|
88
|
+
|
|
89
|
+
```html
|
|
90
|
+
<shadow-root>
|
|
91
|
+
<style>v2-styles</style>
|
|
92
|
+
<div class="om2-tooltip">
|
|
93
|
+
<h2>Tooltip title</h2>
|
|
94
|
+
<p>Tooltip content</p>
|
|
95
|
+
<button>Next</button>
|
|
96
|
+
</div>
|
|
97
|
+
</shadow-root>
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Positioning
|
|
101
|
+
|
|
102
|
+
- **Tooltip placement:** Top, bottom, left, right relative to target element. Computed by `positioner.ts` to avoid viewport overflow.
|
|
103
|
+
- **Modal placement:** Always centered on screen (no positioning needed).
|
|
104
|
+
- **Highlight placement:** Outline computed from target element's bounding rect.
|
|
105
|
+
|
|
106
|
+
## Files & Responsibilities
|
|
107
|
+
|
|
108
|
+
| File | Responsibility |
|
|
109
|
+
|------|-----------------|
|
|
110
|
+
| `src/index.ts` | Public SDK API (`OnboardMe.init`, `.identify`, `.track`) |
|
|
111
|
+
| `src/core/sdk.ts` | Core orchestration (init, flow polling, v2/legacy partition) |
|
|
112
|
+
| `src/core/event-batcher.ts` | Collect events, batch, send to API (fire-and-forget) |
|
|
113
|
+
| `src/v2/renderer.ts` | Walk steps, mount/teardown DOM per step, wire actions |
|
|
114
|
+
| `src/v2/positioner.ts` | Compute tooltip position relative to target |
|
|
115
|
+
| `src/v2/types.ts` | TypeScript types for v2 flow schema |
|
|
116
|
+
| `src/v2/styles.ts` | Injected CSS for tooltip/modal/highlight |
|
|
117
|
+
| `src/components/shadow-host.ts` | Create/retrieve shadow root (shared by v1 and v2) |
|
|
118
|
+
| `src/components/modal.ts` | Legacy v1: welcome modal component |
|
|
119
|
+
| `src/components/checklist.ts` | Legacy v1: progress checklist component |
|
|
120
|
+
| `src/components/celebration.ts` | Legacy v1: completion banner |
|
|
121
|
+
|
|
122
|
+
## Data Flow
|
|
123
|
+
|
|
124
|
+
### Polling & Updates
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
every 60 seconds:
|
|
128
|
+
fetchFlows(GET /v1/flows)
|
|
129
|
+
↓
|
|
130
|
+
compute checksumHash = SHA256(flows).slice(0, 8)
|
|
131
|
+
↓
|
|
132
|
+
if (checksumHash changed):
|
|
133
|
+
teardownV2Renders() // destroy old renders
|
|
134
|
+
_renderFlows(newFlows) // mount new renders
|
|
135
|
+
else:
|
|
136
|
+
(no-op, skip re-render)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
The SDK only re-renders when the flow data actually changed. This avoids unnecessary DOM operations and keeps re-renders silent to the user.
|
|
140
|
+
|
|
141
|
+
### Event Tracking
|
|
142
|
+
|
|
143
|
+
```
|
|
144
|
+
user clicks "Next" button on tooltip
|
|
145
|
+
↓
|
|
146
|
+
pushEvent('step_action_taken', { eventName: '...', ... })
|
|
147
|
+
↓
|
|
148
|
+
event-batcher collects, debounces, batches
|
|
149
|
+
↓
|
|
150
|
+
POST /v1/events with batch every 5 seconds (or earlier if queue fills)
|
|
151
|
+
↓
|
|
152
|
+
API writes to analytics DB (ClickHouse)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Events are fire-and-forget — the user sees the step advance immediately, regardless of whether the API call succeeds.
|
|
156
|
+
|
|
157
|
+
## Extending for New Step Types
|
|
158
|
+
|
|
159
|
+
To add a new step type (e.g., `sidebar`), follow this checklist:
|
|
160
|
+
|
|
161
|
+
1. Add the type to `src/v2/types.ts`:
|
|
162
|
+
```typescript
|
|
163
|
+
export type V2StepType = 'tooltip' | 'modal' | 'highlight' | 'sidebar'
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
2. Add rendering logic to `src/v2/renderer.ts`:
|
|
167
|
+
```typescript
|
|
168
|
+
} else if (step.type === 'sidebar') {
|
|
169
|
+
mountSidebar(ctx, step)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function mountSidebar(ctx: RenderContext, step: V2Step): void {
|
|
173
|
+
// Mount sidebar DOM, wire action button, etc.
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
3. Update `src/v2/styles.ts` with CSS for the new component.
|
|
178
|
+
|
|
179
|
+
4. Test via `src/__tests__/v2-renderer.test.ts`.
|
|
180
|
+
|
|
181
|
+
5. Verify bundle size is still <50 KB: `pnpm --filter @onboardme/sdk build`
|
|
182
|
+
|
|
183
|
+
## Testing
|
|
184
|
+
|
|
185
|
+
### Unit Tests
|
|
186
|
+
|
|
187
|
+
Run `pnpm --filter @onboardme/sdk test` to execute:
|
|
188
|
+
- `v2-renderer.test.ts` — step rendering, DOM cleanup, action wiring
|
|
189
|
+
- `v2-positioner.test.ts` — tooltip positioning edge cases
|
|
190
|
+
- `v2-types.test.ts` — flow schema validation
|
|
191
|
+
|
|
192
|
+
### Manual Testing
|
|
193
|
+
|
|
194
|
+
Open `scripts/test-v2-rendering.html` in a browser to verify:
|
|
195
|
+
1. DOM elements render correctly
|
|
196
|
+
2. Step positioning is correct (tooltips don't clip, highlights are visible)
|
|
197
|
+
3. Action buttons advance the flow
|
|
198
|
+
4. Cleanup happens on complete/skip
|
|
199
|
+
|
|
200
|
+
### Integration with API
|
|
201
|
+
|
|
202
|
+
Phase E (PM dashboard) creates v2 flows and publishes them. The SDK fetches and renders them within seconds. End-to-end flow: code snapshot → AI generation → PM publish → SDK fetch & render.
|
|
203
|
+
|
|
204
|
+
## Performance
|
|
205
|
+
|
|
206
|
+
- **Init:** <50ms (no network calls)
|
|
207
|
+
- **First fetch (async):** ~200–500ms (network latency)
|
|
208
|
+
- **Re-render on flow change:** <20ms (DOM operations)
|
|
209
|
+
- **Bundle size:** 9.82 KB gzipped (target: <15 KB, limit: <50 KB)
|
|
210
|
+
|
|
211
|
+
## Future Enhancements
|
|
212
|
+
|
|
213
|
+
These are deferred beyond Phase F:
|
|
214
|
+
|
|
215
|
+
- **User targeting:** Show flows only to users matching certain segments/traits
|
|
216
|
+
- **Completion tracking:** Track which step each user is on, compute funnel drop-off
|
|
217
|
+
- **Step branching:** Different flows based on user properties
|
|
218
|
+
- **Analytics dashboards:** Visualize drop-off, completion rates, dwell time per step
|
|
219
|
+
|
|
220
|
+
All of these can layer on top of the current v2 foundation without architectural changes.
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
**Version:** Phase F (May 2026)
|
|
225
|
+
**Status:** Production-ready for v2 flows; legacy v1 flows still supported.
|
package/dist/sdk.iife.js
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
var OnboardMe=function(){"use strict";let M=!1;function Te(e){M=e}const c={log(e,...t){M&&console.log(`[OnboardMe] ${e}`,...t)},warn(e,...t){M&&console.warn(`[OnboardMe] ${e}`,...t)},error(e,...t){M&&console.error(`[OnboardMe] ${e}`,...t)}};function $e(e){if(!e||typeof e!="object")return c.warn("init() called with no config — OnboardMe will not run."),null;const t=e;return!t.productId||typeof t.productId!="string"?(c.warn("config.productId is required and must be a string — OnboardMe will not run."),null):(t.flows!==void 0&&!Array.isArray(t.flows)&&(c.warn("config.flows must be an array — defaulting to []."),t.flows=[]),{productId:t.productId,eventsEndpoint:t.eventsEndpoint,autoGenerate:t.autoGenerate??void 0,apiFlows:t.apiFlows??void 0,flows:t.flows??[],user:t.user??void 0,debug:typeof t.debug=="boolean"?t.debug:!1})}const Ie=48*60*60*1e3;function Ne(e){var o;const t=`onboardme_seen_${e.productId}`;try{if(localStorage.getItem(t)!==null)return{isNew:!1,reason:"returning"}}catch{}const n=(o=e.user)==null?void 0:o.createdAt;return n?Date.now()-new Date(n).getTime()>=Ie?{isNew:!1,reason:"returning"}:(Q(t),{isNew:!0,reason:"new_account"}):(Q(t),{isNew:!0,reason:"first_visit"})}function Q(e){try{localStorage.setItem(e,"1")}catch{}}function Me(e){const t=`onboardme_anon_${e}`;try{const n=localStorage.getItem(t);if(n)return n;const o=crypto.randomUUID();return localStorage.setItem(t,o),o}catch{return crypto.randomUUID()}}const Z="onboardme-root";function O(){const e=document.getElementById(Z);if(e!=null&&e.shadowRoot)return e.shadowRoot;const t=document.createElement("div");t.id=Z,document.body.appendChild(t);const n=t.attachShadow({mode:"open"}),o=document.createElement("style");return o.dataset.onboardme="styles",n.appendChild(o),n}const Oe=`
|
|
2
|
+
.om-overlay {
|
|
3
|
+
position: fixed;
|
|
4
|
+
inset: 0;
|
|
5
|
+
background: rgba(0, 0, 0, 0.45);
|
|
6
|
+
display: flex;
|
|
7
|
+
align-items: center;
|
|
8
|
+
justify-content: center;
|
|
9
|
+
z-index: 2147483647;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.om-modal {
|
|
13
|
+
background: #ffffff;
|
|
14
|
+
border-radius: 12px;
|
|
15
|
+
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.2);
|
|
16
|
+
max-width: 480px;
|
|
17
|
+
width: calc(100% - 32px);
|
|
18
|
+
padding: 32px;
|
|
19
|
+
box-sizing: border-box;
|
|
20
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
21
|
+
color: #111827;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.om-modal__title {
|
|
25
|
+
margin: 0 0 12px;
|
|
26
|
+
font-size: 20px;
|
|
27
|
+
font-weight: 700;
|
|
28
|
+
line-height: 1.3;
|
|
29
|
+
color: #111827;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.om-modal__body {
|
|
33
|
+
margin: 0 0 24px;
|
|
34
|
+
font-size: 15px;
|
|
35
|
+
line-height: 1.6;
|
|
36
|
+
color: #4b5563;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.om-modal__actions {
|
|
40
|
+
display: flex;
|
|
41
|
+
flex-direction: column;
|
|
42
|
+
gap: 10px;
|
|
43
|
+
align-items: stretch;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.om-btn-primary {
|
|
47
|
+
display: block;
|
|
48
|
+
width: 100%;
|
|
49
|
+
padding: 12px 20px;
|
|
50
|
+
background: #4f46e5;
|
|
51
|
+
color: #ffffff;
|
|
52
|
+
font-size: 15px;
|
|
53
|
+
font-weight: 600;
|
|
54
|
+
border: none;
|
|
55
|
+
border-radius: 8px;
|
|
56
|
+
cursor: pointer;
|
|
57
|
+
text-align: center;
|
|
58
|
+
transition: background 0.15s ease;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.om-btn-primary:hover {
|
|
62
|
+
background: #4338ca;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.om-btn-primary:focus-visible {
|
|
66
|
+
outline: 3px solid #818cf8;
|
|
67
|
+
outline-offset: 2px;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.om-btn-skip {
|
|
71
|
+
display: block;
|
|
72
|
+
background: none;
|
|
73
|
+
border: none;
|
|
74
|
+
padding: 6px 0;
|
|
75
|
+
color: #6b7280;
|
|
76
|
+
font-size: 14px;
|
|
77
|
+
cursor: pointer;
|
|
78
|
+
text-align: center;
|
|
79
|
+
text-decoration: underline;
|
|
80
|
+
transition: color 0.15s ease;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.om-btn-skip:hover {
|
|
84
|
+
color: #374151;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.om-btn-skip:focus-visible {
|
|
88
|
+
outline: 2px solid #818cf8;
|
|
89
|
+
outline-offset: 2px;
|
|
90
|
+
border-radius: 4px;
|
|
91
|
+
}
|
|
92
|
+
`,He=1e4;async function Le(e,t){const n=new AbortController,o=setTimeout(()=>n.abort(),He);try{const s=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({events:t}),signal:n.signal});return s.ok?!0:(c.warn(`event flush failed — server responded ${s.status}`),!1)}catch(s){return c.warn(`event flush failed — ${s.message}`),!1}finally{clearTimeout(o)}}const qe=100,Fe=5e3;let E="",ee="",P,b=[],_=null,te=!1,ne=null,oe=null;function Ue(e,t){E=e,ee=t}function ze(e){P=e}function y(e,t={}){const n={eventId:crypto.randomUUID(),anonymousId:ee,...P!==void 0?{userId:P}:{},eventType:e,pageUrl:typeof window<"u"?window.location.href:"",timestamp:Date.now(),...t};b.length>=qe&&(b.shift(),c.warn("event queue full — oldest event dropped")),b.push(n),c.log(`event queued: ${e} (queue size: ${b.length})`),b.length===1&&_===null&&(_=setTimeout(()=>{_=null,se().catch(()=>{})},Fe))}async function se(){if(b.length===0)return;if(!E){c.warn("flushEvents called before configureBatcher — events not sent");return}_!==null&&(clearTimeout(_),_=null);const e=[...b];await Le(E,e)&&(b=[],c.log(`flushed ${e.length} event(s)`))}function De(){te||(te=!0,ne=()=>{document.visibilityState==="hidden"&&se().catch(()=>{})},oe=()=>{if(b.length===0||!E)return;const e=JSON.stringify({events:b});navigator.sendBeacon(E,new Blob([e],{type:"application/json"}))&&(b=[],_!==null&&(clearTimeout(_),_=null))},document.addEventListener("visibilitychange",ne),window.addEventListener("beforeunload",oe))}const j=new WeakMap;function ie(e){return`onboardme_modal_shown_${e}`}function Pe(e){const t=e.querySelector('style[data-onboardme="styles"]');t&&!t.textContent&&(t.textContent=Oe)}function je(e,t){Pe(t);const n=document.createElement("div");n.className="om-overlay",n.dataset.onboardme="overlay";const o=document.createElement("div");o.className="om-modal",o.setAttribute("role","dialog"),o.setAttribute("aria-modal","true"),o.setAttribute("aria-labelledby","om-modal-title");const s=document.createElement("h2");s.id="om-modal-title",s.className="om-modal__title",s.textContent=e.title;const i=document.createElement("p");i.className="om-modal__body",i.textContent=e.body;const r=document.createElement("div");r.className="om-modal__actions";const a=document.createElement("button");if(a.className="om-btn-primary",a.type="button",a.textContent=e.primaryCta??"Get started",a.dataset.onboardme="primary-cta",r.appendChild(a),e.dismissible!==!1){const l=document.createElement("button");l.className="om-btn-skip",l.type="button",l.textContent=e.secondaryCta??"Skip for now",l.dataset.onboardme="skip-cta",r.appendChild(l)}o.appendChild(s),o.appendChild(i),o.appendChild(r),n.appendChild(o),t.appendChild(n)}function B(e){const t=e.querySelector('[data-onboardme="overlay"]');t&&e.removeChild(t);const n=j.get(e);n&&(document.removeEventListener("keydown",n),j.delete(e))}function Be(e,t,n,o){if(sessionStorage.getItem(ie(n))!==null)return;sessionStorage.setItem(ie(n),"1"),y("flow_started",{flowId:o}),y("step_viewed",{flowId:o,stepId:e.id,stepIndex:0}),je(e,t);const s=t.querySelector('[data-onboardme="primary-cta"]');s==null||s.addEventListener("click",()=>{y("step_completed",{flowId:o,stepId:e.id}),B(t)});const i=t.querySelector('[data-onboardme="skip-cta"]');i==null||i.addEventListener("click",()=>{y("step_skipped",{flowId:o,stepId:e.id}),B(t)});const r=a=>{a.key==="Escape"&&(y("step_skipped",{flowId:o,stepId:e.id}),B(t))};j.set(t,r),document.addEventListener("keydown",r),s==null||s.focus()}const re={completedSteps:[],lastUpdated:0};function le(e,t){return`onboardme_progress_${e}_${t}`}function ae(e,t){try{const n=localStorage.getItem(le(e,t));return n?JSON.parse(n):{...re,completedSteps:[]}}catch{return{...re,completedSteps:[]}}}function Ge(e,t,n){const o={completedSteps:n,lastUpdated:Date.now()};localStorage.setItem(le(e,t),JSON.stringify(o))}function ce(e,t,n){const o=ae(e,t);o.completedSteps.includes(n)||Ge(e,t,[...o.completedSteps,n])}function Re(e,t){const n=e.steps.find(r=>r.type==="checklist"),o=(n==null?void 0:n.items)??[],s=new Map;for(const r of o)r.completionEvent&&s.set(r.completionEvent,r.id);const i=x.track;return x.track=(r,a)=>{i.call(x,r,a);const l=s.get(r);l!==void 0&&t(l)},()=>{x.track=i,s.clear()}}const Xe=`
|
|
93
|
+
.om-checklist {
|
|
94
|
+
position: fixed;
|
|
95
|
+
bottom: 24px;
|
|
96
|
+
right: 24px;
|
|
97
|
+
width: 320px;
|
|
98
|
+
background: #ffffff;
|
|
99
|
+
border-radius: 12px;
|
|
100
|
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12), 0 2px 8px rgba(0, 0, 0, 0.08);
|
|
101
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
102
|
+
font-size: 14px;
|
|
103
|
+
color: #1a1a2e;
|
|
104
|
+
z-index: 9999;
|
|
105
|
+
overflow: hidden;
|
|
106
|
+
transition: width 0.2s ease, height 0.2s ease;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.om-checklist__header {
|
|
110
|
+
display: flex;
|
|
111
|
+
align-items: center;
|
|
112
|
+
justify-content: space-between;
|
|
113
|
+
padding: 14px 16px 10px;
|
|
114
|
+
border-bottom: 1px solid #f0f0f0;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.om-checklist__title {
|
|
118
|
+
font-weight: 600;
|
|
119
|
+
font-size: 14px;
|
|
120
|
+
color: #1a1a2e;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.om-checklist__collapse-btn {
|
|
124
|
+
background: none;
|
|
125
|
+
border: none;
|
|
126
|
+
cursor: pointer;
|
|
127
|
+
font-size: 18px;
|
|
128
|
+
color: #888;
|
|
129
|
+
padding: 0 2px;
|
|
130
|
+
line-height: 1;
|
|
131
|
+
transition: color 0.15s;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.om-checklist__collapse-btn:hover {
|
|
135
|
+
color: #1a1a2e;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.om-checklist__progress {
|
|
139
|
+
padding: 10px 16px 6px;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.om-checklist__progress-bar {
|
|
143
|
+
width: 100%;
|
|
144
|
+
height: 6px;
|
|
145
|
+
background: #ebebeb;
|
|
146
|
+
border-radius: 3px;
|
|
147
|
+
overflow: hidden;
|
|
148
|
+
margin-bottom: 6px;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.om-checklist__progress-bar-fill {
|
|
152
|
+
height: 100%;
|
|
153
|
+
background: #4f46e5;
|
|
154
|
+
border-radius: 3px;
|
|
155
|
+
transition: width 0.35s ease;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.om-checklist__progress-label {
|
|
159
|
+
font-size: 12px;
|
|
160
|
+
color: #888;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.om-checklist__items {
|
|
164
|
+
list-style: none;
|
|
165
|
+
margin: 0;
|
|
166
|
+
padding: 6px 0 10px;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.om-checklist__item {
|
|
170
|
+
display: flex;
|
|
171
|
+
align-items: center;
|
|
172
|
+
gap: 10px;
|
|
173
|
+
padding: 8px 16px;
|
|
174
|
+
cursor: pointer;
|
|
175
|
+
transition: background 0.1s;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.om-checklist__item:hover {
|
|
179
|
+
background: #f8f8fb;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.om-checklist__item-check {
|
|
183
|
+
flex-shrink: 0;
|
|
184
|
+
width: 18px;
|
|
185
|
+
height: 18px;
|
|
186
|
+
border: 2px solid #d1d5db;
|
|
187
|
+
border-radius: 50%;
|
|
188
|
+
display: flex;
|
|
189
|
+
align-items: center;
|
|
190
|
+
justify-content: center;
|
|
191
|
+
font-size: 10px;
|
|
192
|
+
color: transparent;
|
|
193
|
+
transition: background 0.15s, border-color 0.15s, color 0.15s;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.om-checklist__item-label {
|
|
197
|
+
flex: 1;
|
|
198
|
+
font-size: 13px;
|
|
199
|
+
color: #1a1a2e;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.om-checklist__item--done .om-checklist__item-check {
|
|
203
|
+
background: #4f46e5;
|
|
204
|
+
border-color: #4f46e5;
|
|
205
|
+
color: #ffffff;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.om-checklist__item--done .om-checklist__item-label {
|
|
209
|
+
text-decoration: line-through;
|
|
210
|
+
color: #aaa;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.om-checklist__item--optional .om-checklist__item-label {
|
|
214
|
+
color: #888;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.om-badge {
|
|
218
|
+
flex-shrink: 0;
|
|
219
|
+
background: #f3f4f6;
|
|
220
|
+
color: #6b7280;
|
|
221
|
+
font-size: 11px;
|
|
222
|
+
font-weight: 500;
|
|
223
|
+
padding: 2px 7px;
|
|
224
|
+
border-radius: 10px;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.om-checklist--collapsed {
|
|
228
|
+
width: auto;
|
|
229
|
+
height: auto;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.om-checklist--collapsed .om-checklist__progress,
|
|
233
|
+
.om-checklist--collapsed .om-checklist__items {
|
|
234
|
+
display: none;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.om-checklist--collapsed .om-checklist__header {
|
|
238
|
+
border-bottom: none;
|
|
239
|
+
padding: 10px 14px;
|
|
240
|
+
cursor: pointer;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.om-checklist--collapsed .om-checklist__collapse-btn {
|
|
244
|
+
display: none;
|
|
245
|
+
}
|
|
246
|
+
`,G=new WeakMap;function We(e){var n;const t=e.querySelector('style[data-onboardme="styles"]');t&&((n=t.textContent)!=null&&n.includes(".om-checklist")||(t.textContent=(t.textContent??"")+Xe))}function Ye(e){const t=e.steps.find(n=>n.type==="checklist");return(t==null?void 0:t.items)??[]}function de(e){return e.filter(t=>t.required!==!1).length}function Je(e,t){const n=document.createElement("li"),o=["om-checklist__item"];t&&o.push("om-checklist__item--done"),e.required===!1&&o.push("om-checklist__item--optional"),n.className=o.join(" "),n.dataset.itemId=e.id;const s=document.createElement("span");s.className="om-checklist__item-check",s.setAttribute("aria-hidden","true"),s.textContent=t?"✓":"";const i=document.createElement("span");if(i.className="om-checklist__item-label",i.textContent=e.label,n.appendChild(s),n.appendChild(i),e.required===!1){const r=document.createElement("span");r.className="om-badge",r.textContent="Optional",n.appendChild(r)}return n}function ue(e,t,n){const o=de(t),s=t.filter(w=>w.required!==!1&&n.includes(w.id)).length,i=t.length,r=t.filter(w=>n.includes(w.id)).length,a=o>0?Math.round(s/o*100):0,l=e.querySelector(".om-checklist__progress-bar-fill");l&&(l.style.width=`${a}%`);const d=e.querySelector('[data-onboardme="progress-label"]');d&&(d.textContent=`${r} of ${i} complete`);const p=e.querySelector('[data-onboardme="collapsed-count"]');p&&(p.textContent=`${r} / ${i}`)}function Ke(e,t,n,o=!1,s="",i=""){var Ee,Ae;We(n),(Ee=G.get(n))==null||Ee(),G.delete(n);const r=n.querySelector('[data-onboardme="checklist"]');r&&n.removeChild(r);const a=Ye(e);o&&a.length>7&&console.warn("[OnboardMe] Checklist has more than 7 items — consider splitting the flow.");const l=[...a].sort((u,m)=>u.order-m.order),d=de(l),p=l.filter(u=>u.required!==!1&&t.completedSteps.includes(u.id)).length,w=l.length,T=l.filter(u=>t.completedSteps.includes(u.id)).length,Bt=d>0?Math.round(p/d*100):0,C=[...t.completedSteps],g=document.createElement("div");g.className="om-checklist",g.dataset.onboardme="checklist";const $=document.createElement("div");$.className="om-checklist__header";const I=document.createElement("span");I.className="om-checklist__title",I.textContent="Getting started";const v=document.createElement("span");v.className="om-checklist__collapsed-count",v.dataset.onboardme="collapsed-count",v.style.display="none",v.textContent=`${T} / ${w}`;const S=document.createElement("button");S.className="om-checklist__collapse-btn",S.type="button",S.dataset.onboardme="checklist-collapse",S.setAttribute("aria-label","Collapse checklist"),S.textContent="−",$.appendChild(I),$.appendChild(v),$.appendChild(S);const z=document.createElement("div");z.className="om-checklist__progress";const J=document.createElement("div");J.className="om-checklist__progress-bar";const K=document.createElement("div");K.className="om-checklist__progress-bar-fill",K.style.width=`${Bt}%`,J.appendChild(K);const D=document.createElement("span");D.className="om-checklist__progress-label",D.dataset.onboardme="progress-label",D.textContent=`${T} of ${w} complete`,z.appendChild(J),z.appendChild(D);const V=document.createElement("ul");V.className="om-checklist__items";for(const u of l){const m=t.completedSteps.includes(u.id);V.appendChild(Je(u,m))}g.appendChild($),g.appendChild(z),g.appendChild(V),n.appendChild(g);const Ce=((Ae=e.steps.find(u=>u.type==="checklist"))==null?void 0:Ae.id)??"";Ce&&i&&y("step_viewed",{flowId:i,stepId:Ce});const Gt=g.querySelectorAll(".om-checklist__item");for(const u of Gt)u.addEventListener("click",()=>{const m=u.dataset.itemId;if(!m||C.includes(m))return;s&&i&&(ce(s,i,m),y("checklist_item_done",{flowId:i,stepId:m})),C.push(m),u.classList.add("om-checklist__item--done");const N=u.querySelector(".om-checklist__item-check");N&&(N.textContent="✓"),ue(n,l,C)});S.addEventListener("click",u=>{u.stopPropagation(),g.classList.add("om-checklist--collapsed"),I.style.display="none",v.style.display="inline"}),g.addEventListener("click",()=>{g.classList.contains("om-checklist--collapsed")&&(g.classList.remove("om-checklist--collapsed"),I.style.display="",v.style.display="none")});const Rt=Re(e,u=>{if(C.includes(u))return;s&&i&&(ce(s,i,u),y("checklist_item_done",{flowId:i,stepId:u})),C.push(u);const m=g.querySelector(`[data-item-id="${u}"]`);if(m){m.classList.add("om-checklist__item--done");const N=m.querySelector(".om-checklist__item-check");N&&(N.textContent="✓")}ue(n,l,C)});G.set(n,Rt)}const Ve=`
|
|
247
|
+
.om-celebration {
|
|
248
|
+
position: fixed;
|
|
249
|
+
top: 1.25rem;
|
|
250
|
+
left: 50%;
|
|
251
|
+
transform: translateX(-50%);
|
|
252
|
+
background: #16a34a;
|
|
253
|
+
color: #ffffff;
|
|
254
|
+
font-family: system-ui, sans-serif;
|
|
255
|
+
font-size: 1rem;
|
|
256
|
+
font-weight: 600;
|
|
257
|
+
padding: 0.75rem 1.5rem;
|
|
258
|
+
border-radius: 0.5rem;
|
|
259
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
260
|
+
z-index: 999999;
|
|
261
|
+
text-align: center;
|
|
262
|
+
white-space: nowrap;
|
|
263
|
+
}
|
|
264
|
+
`;function Qe(e){if(!e.querySelector("#om-celebration-styles")){const n=document.createElement("style");n.id="om-celebration-styles",n.textContent=Ve,e.appendChild(n)}const t=document.createElement("div");t.className="om-celebration",t.setAttribute("role","status"),t.setAttribute("aria-live","polite"),t.textContent="🎉 You're all set!",e.appendChild(t),setTimeout(()=>{t.remove()},3e3)}const Ze=10,et=10,tt=15;function R(e,t){const n=new Set,o=[];for(const s of e){const i=(s.textContent??"").trim();if(i&&!n.has(i)&&(n.add(i),o.push(i),o.length>=t))break}return o}function nt(){const e=document.title??"",t=R(document.querySelectorAll("h1, h2, h3"),Ze),n=R(document.querySelectorAll("nav a"),et),o=R(document.querySelectorAll('button, [role="button"]'),tt);return{title:e,headings:t,navLinks:n,buttons:o}}const ot=7*24*60*60*1e3,st=1e4;function pe(e){return`onboardme_autogen_${e}`}function it(e,t){try{const n=localStorage.getItem(pe(e));if(!n)return null;const o=JSON.parse(n);return Date.now()-o.cachedAt>t?null:o.flows}catch{return null}}function rt(e,t){try{const n={flows:t,cachedAt:Date.now()};localStorage.setItem(pe(e),JSON.stringify(n))}catch{c.warn("auto-generate: localStorage quota exceeded — cache not saved")}}async function lt(e,t,n,o=ot){const s=it(t,o);if(s)return s;const i=new AbortController,r=setTimeout(()=>i.abort(),st);try{const a=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({productId:t,context:n}),signal:i.signal});if(!a.ok)return c.warn(`auto-generate: server responded ${a.status}`),[];const l=await a.json(),d=l!==null&&typeof l=="object"&&"flows"in l&&Array.isArray(l.flows)?l.flows:[];return rt(t,d),d}catch(a){return c.warn(`auto-generate: fetch failed — ${a.message}`),[]}finally{clearTimeout(r)}}function at(e,t){const n=new Set(e.map(s=>s.id)),o=t.filter(s=>!n.has(s.id));return[...e,...o]}let fe=null,H="";const ct=6e4,dt=36e5;async function X(e,t){try{const n=new AbortController,o=setTimeout(()=>n.abort(),1e4),s=await fetch(`${e}/v1/flows`,{method:"GET",headers:{"x-api-key":t,"Content-Type":"application/json"},signal:n.signal});return clearTimeout(o),s.ok?await s.json():(c.warn(`Failed to fetch flows: HTTP ${s.status}`),null)}catch(n){return n instanceof Error&&(n.name==="AbortError"?c.warn("Fetch flows timeout (10s)"):c.warn(`Fetch flows error: ${n.message}`)),null}}function ut(e){try{const t=localStorage.getItem(e);if(!t)return null;const n=JSON.parse(t);return Date.now()-n.timestamp>dt?(c.log("Flows cache expired (1h)"),localStorage.removeItem(e),null):n}catch{return c.warn("Failed to parse flows cache"),null}}function W(e,t,n){try{const o={flows:t,checksumHash:n,timestamp:Date.now()};localStorage.setItem(e,JSON.stringify(o))}catch{c.warn("Failed to save flows to cache")}}async function pt(e,t,n){const o=await X(e,t);if(o)return W(n,o.flows,o.checksumHash),o.flows,o.checksumHash,Date.now(),o;const s=ut(n);return s?(c.log("Using cached flows from localStorage"),{flows:s.flows,checksumHash:s.checksumHash}):(c.warn("No flows available (API down, cache empty)"),{flows:[],checksumHash:""})}function ft(e,t,n,o){fe||(c.log("Starting flows polling (60s interval)"),(async()=>{const s=await X(e,t);s&&s.checksumHash!==H&&(H=s.checksumHash,W(n,s.flows,s.checksumHash),o(s.flows,s.checksumHash))})(),fe=setInterval(async()=>{const s=await X(e,t);s&&s.checksumHash!==H&&(c.log("Flows updated (new checksum detected)"),H=s.checksumHash,W(n,s.flows,s.checksumHash),o(s.flows,s.checksumHash))},ct))}const me=500,he=200,mt=[{selector:"h1, h2, h3, h4, h5, h6",role:"heading"},{selector:'button, [role="button"]',role:"button"},{selector:"a[href]",role:"link"},{selector:"input, textarea, select",role:"input"},{selector:"form",role:"form"},{selector:'nav, [role="navigation"]',role:"nav"},{selector:"img[alt]",role:"image"}],ht={heading:[],button:["type","name","aria-label","data-testid"],link:["href","aria-label","data-testid"],input:["type","name","placeholder","aria-label","data-testid"],form:["name","action","method"],nav:["aria-label"],image:["alt","src"],other:[]};function gt(e=document,t=window){var s,i;const n=new Set,o=[];for(const{selector:r,role:a}of mt){const l=xt(e,r);for(const d of l){if(n.has(d)||!kt(d,t))continue;n.add(d);const p={selector:bt(d),role:a},w=wt(d);w&&(p.text=w);const T=_t(d,a);if(Object.keys(T).length>0&&(p.attributes=T),o.push(p),o.length>=me)break}if(o.length>=me)break}return{pageUrl:((s=t.location)==null?void 0:s.href)??"",pageTitle:e.title??"",collectedAt:Date.now(),elements:o,meta:{userAgent:(i=t.navigator)==null?void 0:i.userAgent,viewport:{width:t.innerWidth??0,height:t.innerHeight??0}}}}function bt(e){const t=e.getAttribute("data-testid");if(t)return`[data-testid="${ge(t)}"]`;const n=e.getAttribute("id");if(n&&!yt(n))return`#${ge(n)}`;const o=e.tagName.toLowerCase(),s=e.parentElement;if(!s)return o;const i=Array.from(s.children).filter(a=>a.tagName===e.tagName);if(i.length===1)return o;const r=i.indexOf(e)+1;return`${o}:nth-of-type(${r})`}function yt(e){return e.startsWith("__")||e.startsWith("_react")?!0:e.match(/\d{6,}/)!==null}function ge(e){return e.replace(/[^a-zA-Z0-9_-]/g,t=>`\\${t}`)}function wt(e){const n=(e.innerText??e.textContent??"").replace(/\s+/g," ").trim();return n.length===0?"":n.length<=he?n:n.slice(0,he-1)+"…"}function _t(e,t){const n={};for(const o of ht[t]){const s=e.getAttribute(o);s!==null&&s.length>0&&(n[o]=s.slice(0,200))}return n}function kt(e,t){var n;try{const o=(n=t.getComputedStyle)==null?void 0:n.call(t,e);if(!o)return!0;if(o.display==="none"||o.visibility==="hidden"||o.opacity==="0")return!1}catch{return!0}return!0}function xt(e,t){try{return Array.from(e.querySelectorAll(t))}catch{return[]}}const vt=1e4;function be(e,t){return`onboardme_snapshot_${e}_${t}`}function St(e,t){try{return sessionStorage.getItem(be(e,t))!==null}catch{return!1}}function Ct(e,t){try{sessionStorage.setItem(be(e,t),String(Date.now()))}catch{}}async function Et(e,t,n){const o=new AbortController,s=setTimeout(()=>o.abort(),vt);try{const i=await fetch(`${e}/v1/code-sources/snapshot`,{method:"POST",headers:{"Content-Type":"application/json","x-api-key":t},body:JSON.stringify(n),signal:o.signal});return{ok:i.ok,status:i.status}}catch(i){return c.warn(`snapshot send failed — ${i.message}`),{ok:!1}}finally{clearTimeout(s)}}async function At(e,t,n){if(typeof window>"u"||typeof document>"u")return{sent:!1,skipped:"error"};const o=window.location.href;if(St(e,o))return{sent:!1,skipped:"already_sent"};const s=gt(document,window);if(s.elements.length===0)return{sent:!1,skipped:"no_elements"};const{ok:i,status:r}=await Et(t,n,s);return i?(Ct(e,o),c.log(`snapshot sent (${s.elements.length} elements)`),{sent:!0,status:r}):{sent:!1,skipped:"error",status:r}}function ye(e){if(!e||typeof e!="object")return!1;const t=e;if(!Array.isArray(t.steps)||t.steps.length===0)return!1;const n=t.steps[0];if(!n)return!1;const o=n.position,s=n.action;return!(!o||typeof o.selector!="string"||typeof o.placement!="string"||!s||typeof s.type!="string"||typeof s.label!="string")}const Tt=`
|
|
265
|
+
.om2-overlay {
|
|
266
|
+
position: fixed;
|
|
267
|
+
inset: 0;
|
|
268
|
+
background: rgba(0, 0, 0, 0.4);
|
|
269
|
+
z-index: 2147483646;
|
|
270
|
+
}
|
|
271
|
+
.om2-tooltip,
|
|
272
|
+
.om2-modal-card {
|
|
273
|
+
position: absolute;
|
|
274
|
+
background: #ffffff;
|
|
275
|
+
color: #111827;
|
|
276
|
+
border-radius: 8px;
|
|
277
|
+
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15), 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
278
|
+
padding: 16px 18px;
|
|
279
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
|
|
280
|
+
font-size: 14px;
|
|
281
|
+
line-height: 1.5;
|
|
282
|
+
max-width: 320px;
|
|
283
|
+
z-index: 2147483647;
|
|
284
|
+
box-sizing: border-box;
|
|
285
|
+
}
|
|
286
|
+
.om2-modal-card {
|
|
287
|
+
max-width: 440px;
|
|
288
|
+
padding: 24px 28px;
|
|
289
|
+
}
|
|
290
|
+
.om2-title {
|
|
291
|
+
margin: 0 0 6px 0;
|
|
292
|
+
font-weight: 600;
|
|
293
|
+
font-size: 15px;
|
|
294
|
+
line-height: 1.3;
|
|
295
|
+
}
|
|
296
|
+
.om2-modal-card .om2-title {
|
|
297
|
+
font-size: 18px;
|
|
298
|
+
}
|
|
299
|
+
.om2-description {
|
|
300
|
+
margin: 0 0 14px 0;
|
|
301
|
+
color: #4b5563;
|
|
302
|
+
font-size: 13px;
|
|
303
|
+
}
|
|
304
|
+
.om2-modal-card .om2-description {
|
|
305
|
+
font-size: 14px;
|
|
306
|
+
margin-bottom: 18px;
|
|
307
|
+
}
|
|
308
|
+
.om2-actions {
|
|
309
|
+
display: flex;
|
|
310
|
+
gap: 8px;
|
|
311
|
+
justify-content: flex-end;
|
|
312
|
+
align-items: center;
|
|
313
|
+
}
|
|
314
|
+
.om2-step-counter {
|
|
315
|
+
margin-right: auto;
|
|
316
|
+
font-size: 11px;
|
|
317
|
+
color: #9ca3af;
|
|
318
|
+
font-variant-numeric: tabular-nums;
|
|
319
|
+
}
|
|
320
|
+
.om2-btn {
|
|
321
|
+
font: inherit;
|
|
322
|
+
padding: 6px 14px;
|
|
323
|
+
border-radius: 6px;
|
|
324
|
+
border: none;
|
|
325
|
+
cursor: pointer;
|
|
326
|
+
font-weight: 500;
|
|
327
|
+
font-size: 13px;
|
|
328
|
+
}
|
|
329
|
+
.om2-btn-primary {
|
|
330
|
+
background: #2563eb;
|
|
331
|
+
color: #ffffff;
|
|
332
|
+
}
|
|
333
|
+
.om2-btn-primary:hover { background: #1d4ed8; }
|
|
334
|
+
.om2-btn-secondary {
|
|
335
|
+
background: transparent;
|
|
336
|
+
color: #6b7280;
|
|
337
|
+
}
|
|
338
|
+
.om2-btn-secondary:hover { color: #374151; }
|
|
339
|
+
.om2-highlight {
|
|
340
|
+
position: absolute;
|
|
341
|
+
pointer-events: none;
|
|
342
|
+
border: 2px solid #2563eb;
|
|
343
|
+
border-radius: 6px;
|
|
344
|
+
box-shadow: 0 0 0 4px rgba(37, 99, 235, 0.18);
|
|
345
|
+
z-index: 2147483646;
|
|
346
|
+
transition: top 0.15s ease, left 0.15s ease, width 0.15s ease, height 0.15s ease;
|
|
347
|
+
}
|
|
348
|
+
`,h=8;function we(e){const{targetRect:t,tooltipWidth:n,tooltipHeight:o,scrollX:s,scrollY:i,placement:r,viewportWidth:a,viewportHeight:l}=e;let d,p;switch(r){case"top":d=t.top-o-h,p=t.left+t.width/2-n/2;break;case"bottom":d=t.top+t.height+h,p=t.left+t.width/2-n/2;break;case"left":d=t.top+t.height/2-o/2,p=t.left-n-h;break;case"right":d=t.top+t.height/2-o/2,p=t.left+t.width+h;break;case"center":default:d=l/2-o/2,p=a/2-n/2;break}return p<h&&(p=h),p+n>a-h&&(p=a-n-h),d<h&&(d=h),d+o>l-h&&(d=l-o-h),{top:d+i,left:p+s}}const L=4;function $t(e){const{targetRect:t,scrollX:n,scrollY:o}=e;return{top:t.top+o-L,left:t.left+n-L,width:t.width+L*2,height:t.height+L*2}}const _e="data-onboardme-v2-styles",It="om2-root";function Nt(e,t){if(!e.steps||e.steps.length===0)return c.warn("v2 flow has no steps — nothing to render"),null;qt(t);const n={shadowRoot:t,steps:e.steps,index:0,mounted:[],destroyed:!1};return ke(n),{destroy(){A(n)}}}function ke(e){if(e.destroyed)return;for(const n of e.mounted)n.remove();e.mounted=[];const t=e.steps[e.index];if(!t){A(e);return}t.type==="modal"?Ot(e,t):t.type==="tooltip"?Ht(e,t):Lt(e,t)}function Mt(e,t){if(t.action.type==="skip"||t.action.type==="complete"){A(e);return}if(e.index+=1,e.index>=e.steps.length){A(e);return}ke(e)}function A(e){if(!e.destroyed){e.destroyed=!0;for(const t of e.mounted)t.remove();e.mounted=[]}}function Ot(e,t){const n=k("div",{class:"om2-overlay"}),o=q(e,t,!0);o.style.top="50%",o.style.left="50%",o.style.transform="translate(-50%, -50%)",e.shadowRoot.appendChild(n),e.shadowRoot.appendChild(o),e.mounted.push(n,o)}function Ht(e,t){const n=q(e,t,!1);e.shadowRoot.appendChild(n),e.mounted.push(n);const o=xe(t.position.selector);if(!o){c.warn(`v2 tooltip selector did not match: ${t.position.selector} — centring`),ve(n);return}const s=o.getBoundingClientRect(),i=n.getBoundingClientRect(),r=we({targetRect:{top:s.top,left:s.left,width:s.width,height:s.height},tooltipWidth:i.width||280,tooltipHeight:i.height||100,scrollX:window.scrollX,scrollY:window.scrollY,placement:t.position.placement,viewportWidth:window.innerWidth||1280,viewportHeight:window.innerHeight||800});n.style.top=`${r.top}px`,n.style.left=`${r.left}px`}function Lt(e,t){const n=xe(t.position.selector);if(!n){c.warn(`v2 highlight selector did not match: ${t.position.selector}`);const d=q(e,t,!0);ve(d),e.shadowRoot.appendChild(d),e.mounted.push(d);return}const o=n.getBoundingClientRect(),s=$t({targetRect:{top:o.top,left:o.left,width:o.width,height:o.height},scrollX:window.scrollX,scrollY:window.scrollY}),i=k("div",{class:"om2-highlight"});Object.assign(i.style,{top:`${s.top}px`,left:`${s.left}px`,width:`${s.width}px`,height:`${s.height}px`});const r=q(e,t,!1);e.shadowRoot.appendChild(i),e.shadowRoot.appendChild(r),e.mounted.push(i,r);const a=r.getBoundingClientRect(),l=we({targetRect:{top:o.top,left:o.left,width:o.width,height:o.height},tooltipWidth:a.width||280,tooltipHeight:a.height||100,scrollX:window.scrollX,scrollY:window.scrollY,placement:t.position.placement,viewportWidth:window.innerWidth||1280,viewportHeight:window.innerHeight||800});r.style.top=`${l.top}px`,r.style.left=`${l.left}px`}function q(e,t,n){const o=k("div",{class:`${It} ${n?"om2-modal-card":"om2-tooltip"}`,"data-step-id":t.id}),s=k("h3",{class:"om2-title"});if(s.textContent=t.title||"",o.appendChild(s),t.description){const l=k("p",{class:"om2-description"});l.textContent=t.description,o.appendChild(l)}const i=k("div",{class:"om2-actions"}),r=k("span",{class:"om2-step-counter"});if(r.textContent=`${e.index+1} / ${e.steps.length}`,i.appendChild(r),t.action.type!=="skip"&&e.steps.length>1&&e.index<e.steps.length-1){const l=k("button",{class:"om2-btn om2-btn-secondary",type:"button"});l.textContent="Skip",l.addEventListener("click",()=>A(e)),i.appendChild(l)}const a=k("button",{class:"om2-btn om2-btn-primary",type:"button"});return a.textContent=t.action.label||(t.action.type==="complete"?"Done":"Next"),a.addEventListener("click",()=>Mt(e,t)),i.appendChild(a),o.appendChild(i),o}function k(e,t={}){const n=document.createElement(e);for(const[o,s]of Object.entries(t))o==="class"?n.className=s:n.setAttribute(o,s);return n}function xe(e){try{return document.querySelector(e)}catch{return null}}function qt(e){if(e.querySelector(`style[${_e}]`))return;const n=document.createElement("style");n.setAttribute(_e,""),n.textContent=Tt,e.appendChild(n)}function ve(e){e.style.top="50%",e.style.left="50%",e.style.transform="translate(-50%, -50%)"}let Se=!1,f=null;const Y=new Map;function Ft(e){if(Se)return c.warn("OnboardMe already initialised — ignoring duplicate init() call."),null;const t=$e(e);if(!t)return null;Se=!0,f=t,c.log(`initialised for product: ${t.productId}`);const n=Me(t.productId);c.log(`anonymous ID: ${n}`),Ue(t.eventsEndpoint??"http://localhost:3001/events",n),De();const o=Ne(t),s=o.isNew;return o.isNew?c.log(`new user detected (reason: ${o.reason})`):c.log("returning user, skipping onboarding"),t.apiFlows?(zt(t,s),Ut(t)):t.flows.length===0?c.warn("no flows defined in config — nothing to show"):F(t.flows,t,s),t.autoGenerate&&Dt(t,s),{config:t,anonymousId:n,showOnboarding:s}}async function Ut(e){e.apiFlows&&(await new Promise(t=>setTimeout(t,0)),await At(e.productId,e.apiFlows.endpoint,e.apiFlows.apiKey))}function F(e,t,n){var s;if(e.length===0)return;const o=[];for(const i of e)if(ye(i.config??i)){(s=Y.get(i.id))==null||s.destroy(),Y.delete(i.id);const r=ye(i.config)?i.config:i.steps?{steps:i.steps}:null;if(!r)continue;setTimeout(()=>{const a=Nt(r,O());a&&Y.set(i.id,a)},0)}else o.push(i);if(o.length!==0){if(e=o,n){const r=e.flatMap(a=>a.steps.filter(l=>l.type==="modal").map(l=>({step:l,flowId:a.id}))).sort((a,l)=>a.step.order-l.step.order)[0];r?setTimeout(()=>{Be(r.step,O(),t.productId,r.flowId)},0):c.log("no modal step found — skipping welcome modal")}for(const i of e){if(!i.steps.some(a=>a.type==="checklist"))continue;const r=ae(t.productId,i.id);setTimeout(()=>{Ke(i,r,O(),t.debug??!1,t.productId,i.id)},0);break}}}async function zt(e,t){if(!e.apiFlows)return;const n=`onboardme_flows_${e.productId}`,o=await pt(e.apiFlows.endpoint,e.apiFlows.apiKey,n);if(o.flows.length===0){c.warn("no flows from API and no cache available");return}f&&(f={...f,flows:o.flows}),F(o.flows,e,t),ft(e.apiFlows.endpoint,e.apiFlows.apiKey,n,s=>{f&&(f={...f,flows:s}),F(s,e,t)})}async function Dt(e,t){if(!e.autoGenerate)return;const n=nt(),o=await lt(e.autoGenerate.endpoint,e.productId,n,e.autoGenerate.cacheTtlMs);if(o.length===0)return;const s=new Set(e.flows.map(r=>r.id)),i=o.filter(r=>!s.has(r.id));i.length!==0&&(f&&(f={...f,flows:at(f.flows,o)}),F(i,e,t))}function Pt(e){if(f)for(const t of f.flows){if(e!==t.completionGoal)continue;const n=`onboardme_flow_done_${f.productId}_${t.id}`;localStorage.getItem(n)||(localStorage.setItem(n,"1"),y("flow_completed",{flowId:t.id}),y("goal_reached",{flowId:t.id}),Qe(O()))}}let U=!1;const x={init(e){if(U){c.warn("init() called more than once — ignoring duplicate call.");return}e!=null&&e.debug&&Te(!0),Ft(e)&&(U=!0,jt())},identify(e,t){if(!U){c.warn("identify() called before init() — call will be ignored.");return}ze(e),c.log(`identify: ${e}`,t)},track(e,t){if(!U){c.warn("track() called before init() — call will be ignored.");return}y("step_action_taken",{properties:{eventName:e,...t??{}}}),Pt(e),c.log(`track: ${e}`,t)}};function jt(){const e=window.OnboardMe,t=e==null?void 0:e._q;if(!(!Array.isArray(t)||t.length===0)){c.log(`replaying ${t.length} queued call(s)`);for(const[n,o]of t)n==="identify"?x.identify(o[0],o[1]):n==="track"&&x.track(o[0],o[1]);e&&(e._q=[])}}return window.OnboardMe=x,x}();
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "onboardme-sdk",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": false,
|
|
5
|
+
"main": "./dist/sdk.iife.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build": "vite build && node ../../scripts/check-bundle-size.mjs",
|
|
8
|
+
"dev": "vite build --watch",
|
|
9
|
+
"test": "vitest run",
|
|
10
|
+
"typecheck": "tsc --noEmit"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@onboardme/types": "workspace:*"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@types/node": "^20.0.0",
|
|
17
|
+
"jsdom": "^29.0.1",
|
|
18
|
+
"typescript": "^5.4.0",
|
|
19
|
+
"vite": "^5.2.0",
|
|
20
|
+
"vitest": "^1.4.0"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Day 1 Tests — Monorepo Setup
|
|
3
|
+
*
|
|
4
|
+
* These tests check that the SDK package itself is wired up correctly:
|
|
5
|
+
* the entry point exports the right thing and the init function exists.
|
|
6
|
+
* (Bundle size is verified separately by the build script.)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, it, expect } from 'vitest';
|
|
10
|
+
import OnboardMe from '../index.js';
|
|
11
|
+
|
|
12
|
+
describe('Day 1 — SDK entry point', () => {
|
|
13
|
+
it('the SDK exports an object (not undefined, not null)', () => {
|
|
14
|
+
expect(OnboardMe).toBeDefined();
|
|
15
|
+
expect(OnboardMe).not.toBeNull();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('the SDK has an init function', () => {
|
|
19
|
+
expect(typeof OnboardMe.init).toBe('function');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('calling init with a valid config does not throw any error', () => {
|
|
23
|
+
expect(() => {
|
|
24
|
+
OnboardMe.init({
|
|
25
|
+
productId: 'test-product',
|
|
26
|
+
flows: [],
|
|
27
|
+
});
|
|
28
|
+
}).not.toThrow();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('calling init multiple times does not throw any error', () => {
|
|
32
|
+
expect(() => {
|
|
33
|
+
OnboardMe.init({ productId: 'p1', flows: [] });
|
|
34
|
+
OnboardMe.init({ productId: 'p1', flows: [] });
|
|
35
|
+
}).not.toThrow();
|
|
36
|
+
});
|
|
37
|
+
});
|