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,105 @@
|
|
|
1
|
+
// Snapshot sender (Architecture v2 — Phase B).
|
|
2
|
+
//
|
|
3
|
+
// Sends a DOM snapshot to OnboardMe. Debounced at one snapshot per session
|
|
4
|
+
// per page URL — once a page has reported, we don't keep nagging the API
|
|
5
|
+
// for the same data. Server-side dedup is also in place; the client-side
|
|
6
|
+
// guard saves a network round-trip on subsequent renders/navigations.
|
|
7
|
+
//
|
|
8
|
+
// Like the rest of the SDK, this never throws. On failure we log a debug
|
|
9
|
+
// warning and move on — collecting code is best-effort, not load-bearing
|
|
10
|
+
// for the host app.
|
|
11
|
+
|
|
12
|
+
import { logger } from '../utils/logger.js'
|
|
13
|
+
import { collectSnapshot } from './dom-collector.js'
|
|
14
|
+
import type { SnapshotPayload } from './dom-collector.js'
|
|
15
|
+
|
|
16
|
+
const TIMEOUT_MS = 10_000
|
|
17
|
+
|
|
18
|
+
function sessionKey(productId: string, pageUrl: string): string {
|
|
19
|
+
return `onboardme_snapshot_${productId}_${pageUrl}`
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function hasSentForThisPage(productId: string, pageUrl: string): boolean {
|
|
23
|
+
try {
|
|
24
|
+
return sessionStorage.getItem(sessionKey(productId, pageUrl)) !== null
|
|
25
|
+
} catch {
|
|
26
|
+
// Private mode / disabled storage — re-send is harmless thanks to
|
|
27
|
+
// server-side dedup, so just say "not sent".
|
|
28
|
+
return false
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function markSentForThisPage(productId: string, pageUrl: string): void {
|
|
33
|
+
try {
|
|
34
|
+
sessionStorage.setItem(sessionKey(productId, pageUrl), String(Date.now()))
|
|
35
|
+
} catch {
|
|
36
|
+
// ignore
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface SnapshotSendResult {
|
|
41
|
+
sent: boolean
|
|
42
|
+
skipped?: 'already_sent' | 'no_elements' | 'error'
|
|
43
|
+
status?: number
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function postSnapshot(
|
|
47
|
+
endpoint: string,
|
|
48
|
+
apiKey: string,
|
|
49
|
+
payload: SnapshotPayload,
|
|
50
|
+
): Promise<{ ok: boolean; status?: number }> {
|
|
51
|
+
const controller = new AbortController()
|
|
52
|
+
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS)
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const res = await fetch(`${endpoint}/v1/code-sources/snapshot`, {
|
|
56
|
+
method: 'POST',
|
|
57
|
+
headers: {
|
|
58
|
+
'Content-Type': 'application/json',
|
|
59
|
+
'x-api-key': apiKey,
|
|
60
|
+
},
|
|
61
|
+
body: JSON.stringify(payload),
|
|
62
|
+
signal: controller.signal,
|
|
63
|
+
})
|
|
64
|
+
return { ok: res.ok, status: res.status }
|
|
65
|
+
} catch (err) {
|
|
66
|
+
logger.warn(`snapshot send failed — ${(err as Error).message}`)
|
|
67
|
+
return { ok: false }
|
|
68
|
+
} finally {
|
|
69
|
+
clearTimeout(timer)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Public entry point: collects a snapshot from the current document and
|
|
74
|
+
// sends it. Returns information about what happened so callers (or tests)
|
|
75
|
+
// can verify behaviour, but never throws.
|
|
76
|
+
export async function collectAndSendSnapshot(
|
|
77
|
+
productId: string,
|
|
78
|
+
endpoint: string,
|
|
79
|
+
apiKey: string,
|
|
80
|
+
): Promise<SnapshotSendResult> {
|
|
81
|
+
// sessionStorage / window.location are only available in a browser context.
|
|
82
|
+
if (typeof window === 'undefined' || typeof document === 'undefined') {
|
|
83
|
+
return { sent: false, skipped: 'error' }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const pageUrl = window.location.href
|
|
87
|
+
if (hasSentForThisPage(productId, pageUrl)) {
|
|
88
|
+
return { sent: false, skipped: 'already_sent' }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const payload = collectSnapshot(document, window)
|
|
92
|
+
if (payload.elements.length === 0) {
|
|
93
|
+
// Nothing useful to send — host page is probably empty or pre-render.
|
|
94
|
+
return { sent: false, skipped: 'no_elements' }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const { ok, status } = await postSnapshot(endpoint, apiKey, payload)
|
|
98
|
+
if (ok) {
|
|
99
|
+
markSentForThisPage(productId, pageUrl)
|
|
100
|
+
logger.log(`snapshot sent (${payload.elements.length} elements)`)
|
|
101
|
+
return { sent: true, status }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return { sent: false, skipped: 'error', status }
|
|
105
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* event-listener.ts — Week 3 Day 4
|
|
3
|
+
*
|
|
4
|
+
* Watches OnboardMe.track calls and auto-checks checklist items whose
|
|
5
|
+
* `completionEvent` matches the fired event name.
|
|
6
|
+
*
|
|
7
|
+
* Patches OnboardMe.track so the completion check runs *after* every
|
|
8
|
+
* track call — including calls made before init() (where track is a no-op).
|
|
9
|
+
* Returns an unsubscribe function that restores the original track.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { FlowConfig, ChecklistItem } from '@onboardme/types';
|
|
13
|
+
import OnboardMe from '../index.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Begins watching OnboardMe.track for events that match a checklist item's
|
|
17
|
+
* `completionEvent` field.
|
|
18
|
+
*
|
|
19
|
+
* @param flow The active flow config (reads items from checklist step).
|
|
20
|
+
* @param onItemComplete Called with the item's id when a matching event fires
|
|
21
|
+
* and the item has not already been marked complete.
|
|
22
|
+
* The caller is responsible for de-duplication.
|
|
23
|
+
* @returns An unsubscribe function. Call it to remove the patch
|
|
24
|
+
* (e.g. on flow completion or SDK teardown).
|
|
25
|
+
*/
|
|
26
|
+
export function watchCompletionEvents(
|
|
27
|
+
flow: FlowConfig,
|
|
28
|
+
onItemComplete: (itemId: string) => void,
|
|
29
|
+
): () => void {
|
|
30
|
+
// Build a map: eventName → itemId for every item that has a completionEvent
|
|
31
|
+
const checklistStep = flow.steps.find((s) => s.type === 'checklist');
|
|
32
|
+
const items: ChecklistItem[] = checklistStep?.items ?? [];
|
|
33
|
+
|
|
34
|
+
const watchMap = new Map<string, string>();
|
|
35
|
+
for (const item of items) {
|
|
36
|
+
if (item.completionEvent) {
|
|
37
|
+
watchMap.set(item.completionEvent, item.id);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Patch track
|
|
42
|
+
const originalTrack = OnboardMe.track;
|
|
43
|
+
OnboardMe.track = (eventName: string, properties?: Record<string, unknown>) => {
|
|
44
|
+
// Run original logic first (may be a no-op if SDK not initialised — that's fine)
|
|
45
|
+
originalTrack.call(OnboardMe, eventName, properties);
|
|
46
|
+
|
|
47
|
+
// Completion check always runs
|
|
48
|
+
const itemId = watchMap.get(eventName);
|
|
49
|
+
if (itemId !== undefined) {
|
|
50
|
+
onItemComplete(itemId);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Unsubscribe — restores the original track and clears the watchMap
|
|
55
|
+
return () => {
|
|
56
|
+
OnboardMe.track = originalTrack;
|
|
57
|
+
watchMap.clear();
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* progress-tracker.ts — Week 3 Day 1
|
|
3
|
+
*
|
|
4
|
+
* Owns all localStorage reads/writes for checklist progress.
|
|
5
|
+
* Pure, synchronous, never throws.
|
|
6
|
+
* Key format: onboardme_progress_{productId}_{flowId}
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export interface Progress {
|
|
10
|
+
completedSteps: string[];
|
|
11
|
+
lastUpdated: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const DEFAULT_PROGRESS: Progress = { completedSteps: [], lastUpdated: 0 };
|
|
15
|
+
|
|
16
|
+
function storageKey(productId: string, flowId: string): string {
|
|
17
|
+
return `onboardme_progress_${productId}_${flowId}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Reads progress for a product+flow from localStorage.
|
|
22
|
+
* Returns the empty default if the key is absent or the value is unparseable.
|
|
23
|
+
*/
|
|
24
|
+
export function loadProgress(productId: string, flowId: string): Progress {
|
|
25
|
+
try {
|
|
26
|
+
const raw = localStorage.getItem(storageKey(productId, flowId));
|
|
27
|
+
if (!raw) return { ...DEFAULT_PROGRESS, completedSteps: [] };
|
|
28
|
+
return JSON.parse(raw) as Progress;
|
|
29
|
+
} catch {
|
|
30
|
+
return { ...DEFAULT_PROGRESS, completedSteps: [] };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Writes a fresh progress snapshot to localStorage.
|
|
36
|
+
* Stamps lastUpdated with the current time.
|
|
37
|
+
*/
|
|
38
|
+
export function saveProgress(
|
|
39
|
+
productId: string,
|
|
40
|
+
flowId: string,
|
|
41
|
+
completedSteps: string[],
|
|
42
|
+
): void {
|
|
43
|
+
const data: Progress = { completedSteps, lastUpdated: Date.now() };
|
|
44
|
+
localStorage.setItem(storageKey(productId, flowId), JSON.stringify(data));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Marks a single step as complete.
|
|
49
|
+
* Idempotent — calling it twice for the same stepId has no effect.
|
|
50
|
+
*/
|
|
51
|
+
export function markStepComplete(
|
|
52
|
+
productId: string,
|
|
53
|
+
flowId: string,
|
|
54
|
+
stepId: string,
|
|
55
|
+
): void {
|
|
56
|
+
const current = loadProgress(productId, flowId);
|
|
57
|
+
if (current.completedSteps.includes(stepId)) return;
|
|
58
|
+
saveProgress(productId, flowId, [...current.completedSteps, stepId]);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Returns true if the given step has been marked complete.
|
|
63
|
+
*/
|
|
64
|
+
export function isStepComplete(
|
|
65
|
+
productId: string,
|
|
66
|
+
flowId: string,
|
|
67
|
+
stepId: string,
|
|
68
|
+
): boolean {
|
|
69
|
+
return loadProgress(productId, flowId).completedSteps.includes(stepId);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Removes all progress for a product+flow.
|
|
74
|
+
* Used for testing and explicit flow resets.
|
|
75
|
+
*/
|
|
76
|
+
export function clearProgress(productId: string, flowId: string): void {
|
|
77
|
+
localStorage.removeItem(storageKey(productId, flowId));
|
|
78
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checklist CSS as an injectable string for the Shadow DOM.
|
|
3
|
+
* Source of truth: checklist.css (kept alongside for readability/tooling).
|
|
4
|
+
*/
|
|
5
|
+
export const CHECKLIST_CSS = `
|
|
6
|
+
.om-checklist {
|
|
7
|
+
position: fixed;
|
|
8
|
+
bottom: 24px;
|
|
9
|
+
right: 24px;
|
|
10
|
+
width: 320px;
|
|
11
|
+
background: #ffffff;
|
|
12
|
+
border-radius: 12px;
|
|
13
|
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12), 0 2px 8px rgba(0, 0, 0, 0.08);
|
|
14
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
15
|
+
font-size: 14px;
|
|
16
|
+
color: #1a1a2e;
|
|
17
|
+
z-index: 9999;
|
|
18
|
+
overflow: hidden;
|
|
19
|
+
transition: width 0.2s ease, height 0.2s ease;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.om-checklist__header {
|
|
23
|
+
display: flex;
|
|
24
|
+
align-items: center;
|
|
25
|
+
justify-content: space-between;
|
|
26
|
+
padding: 14px 16px 10px;
|
|
27
|
+
border-bottom: 1px solid #f0f0f0;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.om-checklist__title {
|
|
31
|
+
font-weight: 600;
|
|
32
|
+
font-size: 14px;
|
|
33
|
+
color: #1a1a2e;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.om-checklist__collapse-btn {
|
|
37
|
+
background: none;
|
|
38
|
+
border: none;
|
|
39
|
+
cursor: pointer;
|
|
40
|
+
font-size: 18px;
|
|
41
|
+
color: #888;
|
|
42
|
+
padding: 0 2px;
|
|
43
|
+
line-height: 1;
|
|
44
|
+
transition: color 0.15s;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.om-checklist__collapse-btn:hover {
|
|
48
|
+
color: #1a1a2e;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.om-checklist__progress {
|
|
52
|
+
padding: 10px 16px 6px;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.om-checklist__progress-bar {
|
|
56
|
+
width: 100%;
|
|
57
|
+
height: 6px;
|
|
58
|
+
background: #ebebeb;
|
|
59
|
+
border-radius: 3px;
|
|
60
|
+
overflow: hidden;
|
|
61
|
+
margin-bottom: 6px;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.om-checklist__progress-bar-fill {
|
|
65
|
+
height: 100%;
|
|
66
|
+
background: #4f46e5;
|
|
67
|
+
border-radius: 3px;
|
|
68
|
+
transition: width 0.35s ease;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.om-checklist__progress-label {
|
|
72
|
+
font-size: 12px;
|
|
73
|
+
color: #888;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.om-checklist__items {
|
|
77
|
+
list-style: none;
|
|
78
|
+
margin: 0;
|
|
79
|
+
padding: 6px 0 10px;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.om-checklist__item {
|
|
83
|
+
display: flex;
|
|
84
|
+
align-items: center;
|
|
85
|
+
gap: 10px;
|
|
86
|
+
padding: 8px 16px;
|
|
87
|
+
cursor: pointer;
|
|
88
|
+
transition: background 0.1s;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.om-checklist__item:hover {
|
|
92
|
+
background: #f8f8fb;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.om-checklist__item-check {
|
|
96
|
+
flex-shrink: 0;
|
|
97
|
+
width: 18px;
|
|
98
|
+
height: 18px;
|
|
99
|
+
border: 2px solid #d1d5db;
|
|
100
|
+
border-radius: 50%;
|
|
101
|
+
display: flex;
|
|
102
|
+
align-items: center;
|
|
103
|
+
justify-content: center;
|
|
104
|
+
font-size: 10px;
|
|
105
|
+
color: transparent;
|
|
106
|
+
transition: background 0.15s, border-color 0.15s, color 0.15s;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.om-checklist__item-label {
|
|
110
|
+
flex: 1;
|
|
111
|
+
font-size: 13px;
|
|
112
|
+
color: #1a1a2e;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.om-checklist__item--done .om-checklist__item-check {
|
|
116
|
+
background: #4f46e5;
|
|
117
|
+
border-color: #4f46e5;
|
|
118
|
+
color: #ffffff;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.om-checklist__item--done .om-checklist__item-label {
|
|
122
|
+
text-decoration: line-through;
|
|
123
|
+
color: #aaa;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.om-checklist__item--optional .om-checklist__item-label {
|
|
127
|
+
color: #888;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.om-badge {
|
|
131
|
+
flex-shrink: 0;
|
|
132
|
+
background: #f3f4f6;
|
|
133
|
+
color: #6b7280;
|
|
134
|
+
font-size: 11px;
|
|
135
|
+
font-weight: 500;
|
|
136
|
+
padding: 2px 7px;
|
|
137
|
+
border-radius: 10px;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.om-checklist--collapsed {
|
|
141
|
+
width: auto;
|
|
142
|
+
height: auto;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.om-checklist--collapsed .om-checklist__progress,
|
|
146
|
+
.om-checklist--collapsed .om-checklist__items {
|
|
147
|
+
display: none;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.om-checklist--collapsed .om-checklist__header {
|
|
151
|
+
border-bottom: none;
|
|
152
|
+
padding: 10px 14px;
|
|
153
|
+
cursor: pointer;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.om-checklist--collapsed .om-checklist__collapse-btn {
|
|
157
|
+
display: none;
|
|
158
|
+
}
|
|
159
|
+
`;
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/* ============================================================
|
|
2
|
+
OnboardMe — Checklist panel
|
|
3
|
+
All selectors are scoped inside Shadow DOM, so there is no
|
|
4
|
+
risk of bleeding into the host application's styles.
|
|
5
|
+
============================================================ */
|
|
6
|
+
|
|
7
|
+
/* ---- Floating panel ---------------------------------------- */
|
|
8
|
+
.om-checklist {
|
|
9
|
+
position: fixed;
|
|
10
|
+
bottom: 24px;
|
|
11
|
+
right: 24px;
|
|
12
|
+
width: 320px;
|
|
13
|
+
background: #ffffff;
|
|
14
|
+
border-radius: 12px;
|
|
15
|
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12), 0 2px 8px rgba(0, 0, 0, 0.08);
|
|
16
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
17
|
+
font-size: 14px;
|
|
18
|
+
color: #1a1a2e;
|
|
19
|
+
z-index: 9999;
|
|
20
|
+
overflow: hidden;
|
|
21
|
+
transition: width 0.2s ease, height 0.2s ease;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/* ---- Header ------------------------------------------------ */
|
|
25
|
+
.om-checklist__header {
|
|
26
|
+
display: flex;
|
|
27
|
+
align-items: center;
|
|
28
|
+
justify-content: space-between;
|
|
29
|
+
padding: 14px 16px 10px;
|
|
30
|
+
border-bottom: 1px solid #f0f0f0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.om-checklist__title {
|
|
34
|
+
font-weight: 600;
|
|
35
|
+
font-size: 14px;
|
|
36
|
+
color: #1a1a2e;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.om-checklist__collapse-btn {
|
|
40
|
+
background: none;
|
|
41
|
+
border: none;
|
|
42
|
+
cursor: pointer;
|
|
43
|
+
font-size: 18px;
|
|
44
|
+
color: #888;
|
|
45
|
+
padding: 0 2px;
|
|
46
|
+
line-height: 1;
|
|
47
|
+
transition: color 0.15s;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.om-checklist__collapse-btn:hover {
|
|
51
|
+
color: #1a1a2e;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* ---- Progress area ----------------------------------------- */
|
|
55
|
+
.om-checklist__progress {
|
|
56
|
+
padding: 10px 16px 6px;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.om-checklist__progress-bar {
|
|
60
|
+
width: 100%;
|
|
61
|
+
height: 6px;
|
|
62
|
+
background: #ebebeb;
|
|
63
|
+
border-radius: 3px;
|
|
64
|
+
overflow: hidden;
|
|
65
|
+
margin-bottom: 6px;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.om-checklist__progress-bar-fill {
|
|
69
|
+
height: 100%;
|
|
70
|
+
background: #4f46e5;
|
|
71
|
+
border-radius: 3px;
|
|
72
|
+
transition: width 0.35s ease;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.om-checklist__progress-label {
|
|
76
|
+
font-size: 12px;
|
|
77
|
+
color: #888;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/* ---- Items list -------------------------------------------- */
|
|
81
|
+
.om-checklist__items {
|
|
82
|
+
list-style: none;
|
|
83
|
+
margin: 0;
|
|
84
|
+
padding: 6px 0 10px;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.om-checklist__item {
|
|
88
|
+
display: flex;
|
|
89
|
+
align-items: center;
|
|
90
|
+
gap: 10px;
|
|
91
|
+
padding: 8px 16px;
|
|
92
|
+
cursor: pointer;
|
|
93
|
+
transition: background 0.1s;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.om-checklist__item:hover {
|
|
97
|
+
background: #f8f8fb;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.om-checklist__item-check {
|
|
101
|
+
flex-shrink: 0;
|
|
102
|
+
width: 18px;
|
|
103
|
+
height: 18px;
|
|
104
|
+
border: 2px solid #d1d5db;
|
|
105
|
+
border-radius: 50%;
|
|
106
|
+
display: flex;
|
|
107
|
+
align-items: center;
|
|
108
|
+
justify-content: center;
|
|
109
|
+
font-size: 10px;
|
|
110
|
+
color: transparent;
|
|
111
|
+
transition: background 0.15s, border-color 0.15s, color 0.15s;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.om-checklist__item-label {
|
|
115
|
+
flex: 1;
|
|
116
|
+
font-size: 13px;
|
|
117
|
+
color: #1a1a2e;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/* ---- Done state -------------------------------------------- */
|
|
121
|
+
.om-checklist__item--done .om-checklist__item-check {
|
|
122
|
+
background: #4f46e5;
|
|
123
|
+
border-color: #4f46e5;
|
|
124
|
+
color: #ffffff;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.om-checklist__item--done .om-checklist__item-label {
|
|
128
|
+
text-decoration: line-through;
|
|
129
|
+
color: #aaa;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/* ---- Optional badge ---------------------------------------- */
|
|
133
|
+
.om-checklist__item--optional .om-checklist__item-label {
|
|
134
|
+
color: #888;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.om-badge {
|
|
138
|
+
flex-shrink: 0;
|
|
139
|
+
background: #f3f4f6;
|
|
140
|
+
color: #6b7280;
|
|
141
|
+
font-size: 11px;
|
|
142
|
+
font-weight: 500;
|
|
143
|
+
padding: 2px 7px;
|
|
144
|
+
border-radius: 10px;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/* ---- Collapsed pill state ---------------------------------- */
|
|
148
|
+
.om-checklist--collapsed {
|
|
149
|
+
width: auto;
|
|
150
|
+
height: auto;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.om-checklist--collapsed .om-checklist__progress,
|
|
154
|
+
.om-checklist--collapsed .om-checklist__items {
|
|
155
|
+
display: none;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.om-checklist--collapsed .om-checklist__header {
|
|
159
|
+
border-bottom: none;
|
|
160
|
+
padding: 10px 14px;
|
|
161
|
+
cursor: pointer;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.om-checklist--collapsed .om-checklist__collapse-btn {
|
|
165
|
+
display: none;
|
|
166
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Modal CSS as an injectable string for the Shadow DOM.
|
|
3
|
+
* Source of truth: modal.css (kept alongside for readability/tooling).
|
|
4
|
+
*/
|
|
5
|
+
export const MODAL_CSS = `
|
|
6
|
+
.om-overlay {
|
|
7
|
+
position: fixed;
|
|
8
|
+
inset: 0;
|
|
9
|
+
background: rgba(0, 0, 0, 0.45);
|
|
10
|
+
display: flex;
|
|
11
|
+
align-items: center;
|
|
12
|
+
justify-content: center;
|
|
13
|
+
z-index: 2147483647;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.om-modal {
|
|
17
|
+
background: #ffffff;
|
|
18
|
+
border-radius: 12px;
|
|
19
|
+
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.2);
|
|
20
|
+
max-width: 480px;
|
|
21
|
+
width: calc(100% - 32px);
|
|
22
|
+
padding: 32px;
|
|
23
|
+
box-sizing: border-box;
|
|
24
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
25
|
+
color: #111827;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.om-modal__title {
|
|
29
|
+
margin: 0 0 12px;
|
|
30
|
+
font-size: 20px;
|
|
31
|
+
font-weight: 700;
|
|
32
|
+
line-height: 1.3;
|
|
33
|
+
color: #111827;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.om-modal__body {
|
|
37
|
+
margin: 0 0 24px;
|
|
38
|
+
font-size: 15px;
|
|
39
|
+
line-height: 1.6;
|
|
40
|
+
color: #4b5563;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.om-modal__actions {
|
|
44
|
+
display: flex;
|
|
45
|
+
flex-direction: column;
|
|
46
|
+
gap: 10px;
|
|
47
|
+
align-items: stretch;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.om-btn-primary {
|
|
51
|
+
display: block;
|
|
52
|
+
width: 100%;
|
|
53
|
+
padding: 12px 20px;
|
|
54
|
+
background: #4f46e5;
|
|
55
|
+
color: #ffffff;
|
|
56
|
+
font-size: 15px;
|
|
57
|
+
font-weight: 600;
|
|
58
|
+
border: none;
|
|
59
|
+
border-radius: 8px;
|
|
60
|
+
cursor: pointer;
|
|
61
|
+
text-align: center;
|
|
62
|
+
transition: background 0.15s ease;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.om-btn-primary:hover {
|
|
66
|
+
background: #4338ca;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.om-btn-primary:focus-visible {
|
|
70
|
+
outline: 3px solid #818cf8;
|
|
71
|
+
outline-offset: 2px;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.om-btn-skip {
|
|
75
|
+
display: block;
|
|
76
|
+
background: none;
|
|
77
|
+
border: none;
|
|
78
|
+
padding: 6px 0;
|
|
79
|
+
color: #6b7280;
|
|
80
|
+
font-size: 14px;
|
|
81
|
+
cursor: pointer;
|
|
82
|
+
text-align: center;
|
|
83
|
+
text-decoration: underline;
|
|
84
|
+
transition: color 0.15s ease;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.om-btn-skip:hover {
|
|
88
|
+
color: #374151;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.om-btn-skip:focus-visible {
|
|
92
|
+
outline: 2px solid #818cf8;
|
|
93
|
+
outline-offset: 2px;
|
|
94
|
+
border-radius: 4px;
|
|
95
|
+
}
|
|
96
|
+
`;
|