@momentco-ai/moment-sdk 0.2.2-dev.17 → 0.3.0-dev.18

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.
@@ -124,10 +124,11 @@ class E {
124
124
  * Programmatically open the sync modal.
125
125
  */
126
126
  open(e) {
127
+ const t = this.scopeOpenPayload(e);
127
128
  this.emitAnalytics("embed.open", {
128
- teamSlug: e.teamSlug,
129
- triggerType: e.triggerType
130
- }), this.createModal(e);
129
+ teamSlug: t.teamSlug,
130
+ triggerType: t.triggerType
131
+ }), this.createModal(t);
131
132
  }
132
133
  /**
133
134
  * Close the modal and clean up all resources.
@@ -164,34 +165,42 @@ class E {
164
165
  handleTriggerClick(e) {
165
166
  const t = e.getAttribute("data-moment-team-slug") ?? "", i = e.getAttribute("data-moment-slug") ?? void 0, o = e.getAttribute("data-moment-list-slug") ?? void 0, n = e.getAttribute("data-moment-trigger-type");
166
167
  let r = "moment";
167
- n != null && n !== "" && (n === "moment" || n === "list" || n === "team" ? r = n : console.warn(
168
+ n != null && n !== "" && (n === "moment" || n === "list" || n === "team" || n === "brand-playlist" ? r = n : console.warn(
168
169
  "[MomentSdk] Invalid data-moment-trigger-type, falling back to 'moment':",
169
170
  n
170
171
  ));
171
- const d = e.getAttribute("data-moment-ids") ?? e.getAttribute("data-moment-game-id") ?? "", u = d ? d.split(",").filter(Boolean) : [], a = e.getAttribute("data-moment-calendar");
172
+ const d = e.getAttribute("data-moment-ids") ?? e.getAttribute("data-moment-game-id") ?? "", c = d ? d.split(",").filter(Boolean) : [], a = e.getAttribute("data-moment-calendar");
172
173
  let m;
173
174
  a != null && a !== "" && (a === "google" || a === "outlook" ? m = a : console.warn("[MomentSdk] Invalid data-moment-calendar, ignoring value:", a));
174
- const l = e.getAttribute("data-api-source-id") ?? void 0, c = e.getAttribute("data-external-event-id") ?? void 0;
175
+ const l = e.getAttribute("data-api-source-id") ?? void 0, u = e.getAttribute("data-external-event-id") ?? void 0;
175
176
  if (!t) {
176
177
  console.warn("[MomentSdk] Missing data-moment-team-slug on trigger element");
177
178
  return;
178
179
  }
179
- const h = r === "team" ? void 0 : i, g = r === "team" ? void 0 : o, p = r === "team" ? [] : u;
180
+ const p = r === "team" || r === "brand-playlist";
180
181
  this.emitAnalytics("embed.trigger.click", {
181
182
  teamSlug: t,
182
183
  triggerType: r,
183
- momentIds: p
184
+ momentIds: p ? [] : c
184
185
  }), this.open({
185
186
  teamSlug: t,
186
- momentSlug: h,
187
- listSlug: g,
187
+ momentSlug: i,
188
+ listSlug: o,
188
189
  triggerType: r,
189
- ids: p,
190
+ ids: c,
190
191
  calendar: m ?? void 0,
191
192
  apiSourceId: l,
192
- externalEventId: c
193
+ externalEventId: u
193
194
  });
194
195
  }
196
+ /** Drop moment/list/source scoping for brand-level subscription triggers. */
197
+ scopeOpenPayload(e) {
198
+ return e.triggerType === "team" || e.triggerType === "brand-playlist" ? {
199
+ teamSlug: e.teamSlug,
200
+ triggerType: e.triggerType,
201
+ calendar: e.calendar
202
+ } : e;
203
+ }
195
204
  // ── Private: modal creation ───────────────────────────────────────
196
205
  createModal(e) {
197
206
  this.cleanup(), this.previousFocusedElement = document.activeElement || null, this.activePayload = e;
@@ -270,8 +279,8 @@ class E {
270
279
  gap: "6px",
271
280
  padding: "12px 0"
272
281
  });
273
- const u = document.createElement("span");
274
- u.textContent = "Powered by", Object.assign(u.style, {
282
+ const c = document.createElement("span");
283
+ c.textContent = "Powered by", Object.assign(c.style, {
275
284
  fontFamily: "Inter, system-ui, -apple-system, sans-serif",
276
285
  fontSize: "12px",
277
286
  lineHeight: "16px",
@@ -281,18 +290,18 @@ class E {
281
290
  a.alt = "Moment", a.src = `${this.liveBaseUrl}/brand/moment-text-white.png`, Object.assign(a.style, {
282
291
  height: "auto",
283
292
  width: "67px"
284
- }), d.appendChild(u), d.appendChild(a), o.appendChild(n), o.appendChild(d), this.overlay.appendChild(o), document.body.appendChild(this.overlay);
293
+ }), d.appendChild(c), d.appendChild(a), o.appendChild(n), o.appendChild(d), this.overlay.appendChild(o), document.body.appendChild(this.overlay);
285
294
  const m = (l) => {
286
295
  if (l.key !== "Tab") return;
287
- const c = n.querySelectorAll(
296
+ const u = n.querySelectorAll(
288
297
  'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
289
298
  );
290
- if (c.length === 0) {
299
+ if (u.length === 0) {
291
300
  l.preventDefault(), n.focus();
292
301
  return;
293
302
  }
294
- const h = c[0], g = c[c.length - 1], p = document.activeElement;
295
- l.shiftKey && p === h ? (l.preventDefault(), g.focus()) : !l.shiftKey && p === g && (l.preventDefault(), h.focus());
303
+ const p = u[0], h = u[u.length - 1], g = document.activeElement;
304
+ l.shiftKey && g === p ? (l.preventDefault(), h.focus()) : !l.shiftKey && g === h && (l.preventDefault(), p.focus());
296
305
  };
297
306
  n.addEventListener("keydown", m), r.focus(), this.previousBodyOverflow = document.body.style.overflow, document.body.style.overflow = "hidden", this.didManageBodyOverflow = !0, this.setupMessageListener(), this.overlay._escHandler = i, this.overlay._focusTrapHandler = m;
298
307
  }
@@ -39,6 +39,8 @@ export declare class MomentSdk {
39
39
  private bindTriggers;
40
40
  private unbindTriggers;
41
41
  private handleTriggerClick;
42
+ /** Drop moment/list/source scoping for brand-level subscription triggers. */
43
+ private scopeOpenPayload;
42
44
  private createModal;
43
45
  private buildIframeUrl;
44
46
  private setupMessageListener;
@@ -5,8 +5,9 @@
5
5
  * - `moment` → subscribe to one or more specific moments
6
6
  * - `list` → subscribe to a curated list of moments
7
7
  * - `team` → subscribe to a team / brand schedule (all published moments)
8
+ * - `brand-playlist` → subscribe to the brand's default playlist
8
9
  */
9
- export type MomentTriggerType = 'moment' | 'list' | 'team';
10
+ export type MomentTriggerType = 'moment' | 'list' | 'team' | 'brand-playlist';
10
11
  export type CalendarProvider = 'google' | 'outlook';
11
12
  export interface MomentSdkInitOptions {
12
13
  /** CSS selector for trigger elements. Default: ".moment-sync-trigger" */
@@ -30,7 +31,10 @@ export interface MomentSdkInitOptions {
30
31
  * - `list` → `teamSlug` plus `listSlug` (`ids` not required — list resolves
31
32
  * to a single list-level subscription)
32
33
  * - `team` → `teamSlug` only (resolves to the brand's team-level
33
- * subscription; `listSlug`, `momentSlug`, and `ids` are ignored)
34
+ * subscription). Omit `listSlug`, `momentSlug`, `ids`, `apiSourceId`, and
35
+ * `externalEventId` for this trigger type.
36
+ * - `brand-playlist` → `teamSlug` only (Live resolves the brand's
37
+ * `defaultPlaylistId`). Omit the same fields as `team`.
34
38
  */
35
39
  export interface MomentSdkOpenPayload {
36
40
  teamSlug: string;
@@ -55,7 +59,7 @@ export interface MomentSdkOpenPayload {
55
59
  externalEventId?: string;
56
60
  }
57
61
  /** Backend subscription resource type that the embed will create */
58
- export type MomentSubscriptionType = 'moment' | 'list' | 'team';
62
+ export type MomentSubscriptionType = 'moment' | 'list' | 'team' | 'playlist';
59
63
  /** Iframe → Parent: request to start OAuth via popup */
60
64
  export interface MomentOAuthStartPayload {
61
65
  source: 'moment-live-embed';
@@ -0,0 +1,36 @@
1
+ # Moment SDK examples
2
+
3
+ Interactive demos shipped inside `@momentco-ai/moment-sdk`. They use the **sdk-demo** fixture brand and the SDK build from the same package you installed.
4
+
5
+ ## Run from npm
6
+
7
+ ```bash
8
+ npx moment-sdk-examples
9
+ ```
10
+
11
+ Or after installing the package:
12
+
13
+ ```bash
14
+ npm install @momentco-ai/moment-sdk
15
+ npx moment-sdk-examples
16
+ ```
17
+
18
+ Opens **http://localhost:5051** with:
19
+
20
+ | Page | Trigger type |
21
+ | ------------------ | ---------------- |
22
+ | `/` | Hub |
23
+ | `/moment/` | `moment` |
24
+ | `/collection/` | `list` |
25
+ | `/brand-schedule/` | `brand-playlist` |
26
+
27
+ Demo slugs are in `shared/fixtures.js`. Edit that file if your account uses different values.
28
+
29
+ ## Maintainer workflow (repo clone)
30
+
31
+ ```bash
32
+ pnpm build
33
+ pnpm examples
34
+ ```
35
+
36
+ `pnpm examples` builds the SDK, then runs the same server npm users get.
@@ -0,0 +1,41 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Moment SDK — Brand schedule trigger</title>
7
+ <link rel="stylesheet" href="/shared/demo.css" />
8
+ </head>
9
+ <body>
10
+ <main class="demo-shell">
11
+ <div id="demo-nav" class="demo-top"></div>
12
+
13
+ <h1>Brand schedule trigger</h1>
14
+ <p class="lead">
15
+ Follow the brand's default schedule. Only <code>data-moment-team-slug</code> and
16
+ <code>data-moment-trigger-type="brand-playlist"</code> are required.
17
+ </p>
18
+
19
+ <article class="hero-card">
20
+ <div class="card-info">
21
+ <div class="card-title">Follow all sdk-demo events</div>
22
+ <div class="card-meta">Team slug <code id="team-slug-label"></code></div>
23
+ </div>
24
+ <button type="button" id="schedule-trigger" class="moment-sync-trigger demo-btn">
25
+ Follow All Schedule
26
+ </button>
27
+ </article>
28
+
29
+ <section class="section">
30
+ <h2>Embed snippet</h2>
31
+ <div class="snippet">
32
+ <button class="copy-btn" type="button" data-copy-target="snippet-code">Copy</button>
33
+ <pre id="snippet-code">Loading snippet…</pre>
34
+ </div>
35
+ </section>
36
+
37
+ <div id="demo-log-host"></div>
38
+ </main>
39
+ <script type="module" src="/brand-schedule/main.js"></script>
40
+ </body>
41
+ </html>
@@ -0,0 +1,18 @@
1
+ import { DEMO_FIXTURES } from '../shared/fixtures.js';
2
+ import { embedSnippetHtml, initDemoSdk, mountLogPanel, mountNav } from '../shared/boot.js';
3
+
4
+ mountNav({ title: 'Brand schedule' });
5
+ mountLogPanel();
6
+
7
+ document.getElementById('team-slug-label').textContent = DEMO_FIXTURES.teamSlug;
8
+
9
+ const trigger = document.getElementById('schedule-trigger');
10
+ trigger.setAttribute('data-moment-team-slug', DEMO_FIXTURES.teamSlug);
11
+ trigger.setAttribute('data-moment-trigger-type', 'brand-playlist');
12
+
13
+ document.getElementById('snippet-code').textContent = await embedSnippetHtml({
14
+ triggerType: 'brand-playlist',
15
+ label: 'Follow All Schedule',
16
+ });
17
+
18
+ await initDemoSdk();
@@ -0,0 +1,41 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Moment SDK — Collection trigger</title>
7
+ <link rel="stylesheet" href="/shared/demo.css" />
8
+ </head>
9
+ <body>
10
+ <main class="demo-shell">
11
+ <div id="demo-nav" class="demo-top"></div>
12
+
13
+ <h1>Collection trigger</h1>
14
+ <p class="lead">
15
+ Sync a curated list of moments. Requires <code>data-moment-list-slug</code> with
16
+ <code>triggerType: list</code>.
17
+ </p>
18
+
19
+ <article class="hero-card">
20
+ <div class="card-info">
21
+ <div class="card-title" id="collection-title"></div>
22
+ <div class="card-meta" id="collection-meta"></div>
23
+ </div>
24
+ <button type="button" id="collection-trigger" class="moment-sync-trigger demo-btn">
25
+ Sync Collection
26
+ </button>
27
+ </article>
28
+
29
+ <section class="section">
30
+ <h2>Embed snippet</h2>
31
+ <div class="snippet">
32
+ <button class="copy-btn" type="button" data-copy-target="snippet-code">Copy</button>
33
+ <pre id="snippet-code">Loading snippet…</pre>
34
+ </div>
35
+ </section>
36
+
37
+ <div id="demo-log-host"></div>
38
+ </main>
39
+ <script type="module" src="/collection/main.js"></script>
40
+ </body>
41
+ </html>
@@ -0,0 +1,24 @@
1
+ import { DEMO_FIXTURES } from '../shared/fixtures.js';
2
+ import { embedSnippetHtml, initDemoSdk, mountLogPanel, mountNav } from '../shared/boot.js';
3
+
4
+ const { collection, teamSlug } = DEMO_FIXTURES;
5
+
6
+ mountNav({ title: 'Collection' });
7
+ mountLogPanel();
8
+
9
+ document.getElementById('collection-title').textContent = collection.title;
10
+ document.getElementById('collection-meta').innerHTML =
11
+ `${collection.description} · <code>${collection.slug}</code>`;
12
+
13
+ const trigger = document.getElementById('collection-trigger');
14
+ trigger.setAttribute('data-moment-team-slug', teamSlug);
15
+ trigger.setAttribute('data-moment-list-slug', collection.slug);
16
+ trigger.setAttribute('data-moment-trigger-type', 'list');
17
+
18
+ document.getElementById('snippet-code').textContent = await embedSnippetHtml({
19
+ triggerType: 'list',
20
+ listSlug: collection.slug,
21
+ label: 'Sync Collection',
22
+ });
23
+
24
+ await initDemoSdk();
@@ -0,0 +1,38 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Moment SDK — Examples</title>
7
+ <link rel="stylesheet" href="/shared/demo.css" />
8
+ </head>
9
+ <body>
10
+ <main class="demo-shell">
11
+ <div id="demo-nav" class="demo-top"></div>
12
+
13
+ <h1>Moment SDK examples</h1>
14
+ <p class="lead">
15
+ Interactive demos using the <strong>sdk-demo</strong> fixture brand. Each page loads the SDK
16
+ from this package and opens the same calendar sync flow your site will use.
17
+ </p>
18
+
19
+ <div class="hub-grid">
20
+ <a class="hub-card" href="/moment/">
21
+ <h2>Moment</h2>
22
+ <p>Single-event sync with <code>triggerType: moment</code>.</p>
23
+ </a>
24
+ <a class="hub-card" href="/collection/">
25
+ <h2>Collection</h2>
26
+ <p>List sync with <code>triggerType: list</code>.</p>
27
+ </a>
28
+ <a class="hub-card" href="/brand-schedule/">
29
+ <h2>Brand schedule</h2>
30
+ <p>Follow the full schedule with <code>triggerType: brand-playlist</code>.</p>
31
+ </a>
32
+ </div>
33
+
34
+ <div id="demo-log-host"></div>
35
+ </main>
36
+ <script type="module" src="/index.js"></script>
37
+ </body>
38
+ </html>
@@ -0,0 +1,11 @@
1
+ import { appendLog, initDemoSdk, mountLogPanel, mountNav } from './shared/boot.js';
2
+
3
+ mountNav();
4
+ mountLogPanel('SDK callbacks');
5
+
6
+ try {
7
+ await initDemoSdk();
8
+ } catch (error) {
9
+ console.error(error);
10
+ appendLog?.(String(error), 'error');
11
+ }
@@ -0,0 +1,33 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Moment SDK — Moment trigger</title>
7
+ <link rel="stylesheet" href="/shared/demo.css" />
8
+ </head>
9
+ <body>
10
+ <main class="demo-shell">
11
+ <div id="demo-nav" class="demo-top"></div>
12
+
13
+ <h1>Moment trigger</h1>
14
+ <p class="lead">
15
+ Sync one published event. Requires <code>data-moment-team-slug</code> and
16
+ <code>data-moment-slug</code>.
17
+ </p>
18
+
19
+ <div id="events" class="card-grid"></div>
20
+
21
+ <section class="section">
22
+ <h2>Embed snippet</h2>
23
+ <div class="snippet">
24
+ <button class="copy-btn" type="button" data-copy-target="snippet-code">Copy</button>
25
+ <pre id="snippet-code">Loading snippet…</pre>
26
+ </div>
27
+ </section>
28
+
29
+ <div id="demo-log-host"></div>
30
+ </main>
31
+ <script type="module" src="/moment/main.js"></script>
32
+ </body>
33
+ </html>
@@ -0,0 +1,20 @@
1
+ import { DEMO_FIXTURES } from '../shared/fixtures.js';
2
+ import {
3
+ embedSnippetHtml,
4
+ initDemoSdk,
5
+ mountLogPanel,
6
+ mountNav,
7
+ renderMomentCards,
8
+ } from '../shared/boot.js';
9
+
10
+ mountNav({ title: 'Moment' });
11
+ mountLogPanel();
12
+ renderMomentCards('#events', DEMO_FIXTURES.moments, DEMO_FIXTURES.teamSlug);
13
+
14
+ document.getElementById('snippet-code').textContent = await embedSnippetHtml({
15
+ triggerType: 'moment',
16
+ momentSlug: DEMO_FIXTURES.moments[0].slug,
17
+ label: 'Add to Calendar',
18
+ });
19
+
20
+ await initDemoSdk();
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env node
2
+ import { createReadStream, existsSync, readFileSync, statSync } from 'node:fs';
3
+ import { createServer } from 'node:http';
4
+ import { dirname, extname, join, normalize, sep } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+ const examplesRoot = __dirname;
9
+ const packageRoot = join(__dirname, '..');
10
+ const sdkPath = join(packageRoot, 'dist', 'moment-sdk.js');
11
+ const packageJson = JSON.parse(readFileSync(join(packageRoot, 'package.json'), 'utf8'));
12
+
13
+ const PORT = Number(process.env.PORT || 5051);
14
+ const MIME = {
15
+ '.css': 'text/css; charset=utf-8',
16
+ '.html': 'text/html; charset=utf-8',
17
+ '.js': 'application/javascript; charset=utf-8',
18
+ '.json': 'application/json; charset=utf-8',
19
+ };
20
+
21
+ const ROUTES = {
22
+ '/': 'index.html',
23
+ '/moment': 'moment/index.html',
24
+ '/moment/': 'moment/index.html',
25
+ '/collection': 'collection/index.html',
26
+ '/collection/': 'collection/index.html',
27
+ '/brand-schedule': 'brand-schedule/index.html',
28
+ '/brand-schedule/': 'brand-schedule/index.html',
29
+ };
30
+
31
+ function send(res, status, body, type = 'text/plain; charset=utf-8') {
32
+ res.writeHead(status, { 'Content-Type': type });
33
+ res.end(body);
34
+ }
35
+
36
+ function safePath(relativePath) {
37
+ const resolved = normalize(join(examplesRoot, relativePath));
38
+ const rootPrefix = examplesRoot.endsWith(sep) ? examplesRoot : `${examplesRoot}${sep}`;
39
+ if (resolved !== examplesRoot && !resolved.startsWith(rootPrefix)) {
40
+ return null;
41
+ }
42
+ return resolved;
43
+ }
44
+
45
+ function serveFile(res, relativePath) {
46
+ const filePath = safePath(relativePath);
47
+ if (!filePath || !existsSync(filePath) || statSync(filePath).isDirectory()) {
48
+ return false;
49
+ }
50
+
51
+ const type = MIME[extname(filePath)] ?? 'application/octet-stream';
52
+ res.writeHead(200, { 'Content-Type': type });
53
+ createReadStream(filePath).pipe(res);
54
+ return true;
55
+ }
56
+
57
+ const server = createServer((req, res) => {
58
+ const pathname = req.url?.split('?')[0] ?? '/';
59
+
60
+ if (pathname === '/moment-sdk.js') {
61
+ if (!existsSync(sdkPath)) {
62
+ send(
63
+ res,
64
+ 503,
65
+ 'Moment SDK build not found in this package. Reinstall @momentco-ai/moment-sdk.',
66
+ );
67
+ return;
68
+ }
69
+ res.writeHead(200, { 'Content-Type': 'application/javascript; charset=utf-8' });
70
+ createReadStream(sdkPath).pipe(res);
71
+ return;
72
+ }
73
+
74
+ if (pathname === '/package-meta.json') {
75
+ send(
76
+ res,
77
+ 200,
78
+ JSON.stringify({
79
+ name: packageJson.name,
80
+ version: packageJson.version,
81
+ cdnUrl: `https://cdn.jsdelivr.net/npm/${packageJson.name}@${packageJson.version}`,
82
+ }),
83
+ 'application/json; charset=utf-8',
84
+ );
85
+ return;
86
+ }
87
+
88
+ if (ROUTES[pathname]) {
89
+ if (serveFile(res, ROUTES[pathname])) return;
90
+ }
91
+
92
+ const relative = pathname.replace(/^\//, '');
93
+ if (relative && serveFile(res, relative)) return;
94
+
95
+ send(res, 404, 'Not found');
96
+ });
97
+
98
+ server.listen(PORT, () => {
99
+ console.log(`Moment SDK examples → http://localhost:${PORT}`);
100
+ console.log(`Package ${packageJson.name}@${packageJson.version}`);
101
+ if (!existsSync(sdkPath)) {
102
+ console.warn('Warning: dist/moment-sdk.js missing — triggers will not load the SDK.');
103
+ }
104
+ });