@momentco-ai/moment-sdk 0.3.0 → 0.3.2-dev.21

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/CHANGELOG.md CHANGED
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [Unreleased]
9
+
10
+ ### Added
11
+
12
+ - `externalEventIdPrefix` on `MomentSdkOpenPayload`, `createMomentButton()`, and `data-external-event-id-prefix` for event-level sync buttons. Live fetches all showings for the upstream event and prompts the user to pick one. Mutually exclusive with `externalEventId`.
13
+
8
14
  ## [0.3.0] - 2026-06-08
9
15
 
10
16
  ### Added
package/README.md CHANGED
@@ -106,19 +106,20 @@ For `team` and `brand-playlist`, omit moment/list/ID and source-resolution attri
106
106
 
107
107
  Place these on any element matching the trigger selector (default: `.moment-sync-trigger`):
108
108
 
109
- | Attribute | Required | Description |
110
- | -------------------------- | ------------ | ----------------------------------------------------------------- |
111
- | `data-moment-team-slug` | Yes | Team/brand slug provided by Moment |
112
- | `data-moment-slug` | For `moment` | Brand-level moment slug (resolved server-side) |
113
- | `data-moment-list-slug` | For `list` | List slug |
114
- | `data-moment-trigger-type` | No | `moment` (default), `list`, `team`, or `brand-playlist` |
115
- | `data-moment-ids` | No | Comma-separated moment IDs (fallback if slugs don't resolve) |
116
- | `data-moment-game-id` | No | Alias for `data-moment-ids` |
117
- | `data-moment-calendar` | No | Pre-select provider: `google` or `outlook` |
118
- | `data-api-source-id` | No | Sync-subscription source ID (pairs with `data-external-event-id`) |
119
- | `data-external-event-id` | No | External event ID from your feed, resolved against the source ID |
120
-
121
- When both `data-api-source-id` and `data-external-event-id` are set, Live resolves the moment server-side — useful when your CMS emits native event IDs instead of Moment slugs.
109
+ | Attribute | Required | Description |
110
+ | ------------------------------- | ------------ | ----------------------------------------------------------------- |
111
+ | `data-moment-team-slug` | Yes | Team/brand slug provided by Moment |
112
+ | `data-moment-slug` | For `moment` | Brand-level moment slug (resolved server-side) |
113
+ | `data-moment-list-slug` | For `list` | List slug |
114
+ | `data-moment-trigger-type` | No | `moment` (default), `list`, `team`, or `brand-playlist` |
115
+ | `data-moment-ids` | No | Comma-separated moment IDs (fallback if slugs don't resolve) |
116
+ | `data-moment-game-id` | No | Alias for `data-moment-ids` |
117
+ | `data-moment-calendar` | No | Pre-select provider: `google` or `outlook` |
118
+ | `data-api-source-id` | No | Sync-subscription source ID (pairs with external id attributes) |
119
+ | `data-external-event-id` | No | Exact external id (`event_id:showing_id`) resolved against source |
120
+ | `data-external-event-id-prefix` | No | Upstream event id only — user picks a showing in the embed modal |
121
+
122
+ When `data-api-source-id` is set with either `data-external-event-id` (one showing) or `data-external-event-id-prefix` (all showings for an event), Live resolves moments server-side — useful when your CMS emits native event IDs instead of Moment slugs.
122
123
 
123
124
  ---
124
125
 
@@ -150,12 +151,20 @@ sdk.open({
150
151
  triggerType: 'moment',
151
152
  });
152
153
 
153
- // Source-based resolution (CMS event ID)
154
+ // Source-based resolution (CMS event ID — single showing)
154
155
  sdk.open({
155
156
  teamSlug: 'your-team-slug',
156
157
  triggerType: 'moment',
157
158
  apiSourceId: 'your-sync-source-id',
158
- externalEventId: 'native-event-12345',
159
+ externalEventId: '1420:2706',
160
+ });
161
+
162
+ // Source-based resolution (event with multiple showings — user picks date)
163
+ sdk.open({
164
+ teamSlug: 'your-team-slug',
165
+ triggerType: 'moment',
166
+ apiSourceId: 'your-sync-source-id',
167
+ externalEventIdPrefix: '1420',
159
168
  });
160
169
 
161
170
  // Follow full team schedule
@@ -1,4 +1,4 @@
1
- (function(h){"use strict";class w{popup=null;pollTimer=null;messageHandler=null;resultReceived=!1;opts;constructor(e){this.opts=e}open(e){this.cleanup(),this.resultReceived=!1;const t=this.buildPopupUrl(e),i=Math.round(window.screenX+(window.outerWidth-500)/2),o=Math.round(window.screenY+(window.outerHeight-700)/2);if(this.popup=window.open(t,"moment-oauth-popup",`width=500,height=700,left=${i},top=${o},menubar=no,toolbar=no,location=yes,status=no`),!this.popup||this.popup.closed){this.sendStatusToIframe("popup-blocked"),this.opts.onPopupBlocked();return}this.sendStatusToIframe("popup-opened"),this.startPolling(),this.messageHandler=n=>{if(n.origin!==this.opts.liveOrigin||n.source!==this.popup)return;const r=n.data;!r||typeof r!="object"||r.source!=="moment-live-embed"||r.type!=="moment.embed.result"||(this.resultReceived=!0,this.sendStatusToIframe("completed"),this.opts.iframeWindow&&this.opts.iframeWindow.postMessage(r,this.opts.liveOrigin),this.opts.onResult(r),this.cleanup())},window.addEventListener("message",this.messageHandler)}cleanup(){if(this.pollTimer&&(clearInterval(this.pollTimer),this.pollTimer=null),this.messageHandler&&(window.removeEventListener("message",this.messageHandler),this.messageHandler=null),this.popup&&!this.popup.closed)try{this.popup.close()}catch{}this.popup=null}buildPopupUrl(e){const t=window.location.origin,i=new URLSearchParams({provider:e.provider,teamSlug:e.teamSlug,ids:e.ids.join(","),triggerType:e.triggerType,returnOrigin:t});return e.subscriptionType&&i.set("subscriptionType",e.subscriptionType),`${this.opts.liveBaseUrl}/en/embed/oauth-popup?${i.toString()}`}startPolling(){this.pollTimer=setInterval(()=>{(!this.popup||this.popup.closed)&&(this.pollTimer&&(clearInterval(this.pollTimer),this.pollTimer=null),this.resultReceived||(this.sendStatusToIframe("popup-closed"),this.opts.onPopupClosed()),this.cleanup())},500)}sendStatusToIframe(e){if(!this.opts.iframeWindow)return;const t={source:"moment-sdk",type:"moment.embed.oauth.status",status:e};this.opts.iframeWindow.postMessage(t,this.opts.liveOrigin)}}const g="moment-sdk-trigger-styles",T=`
1
+ (function(f){"use strict";class w{popup=null;pollTimer=null;messageHandler=null;resultReceived=!1;opts;constructor(e){this.opts=e}open(e){this.cleanup(),this.resultReceived=!1;const t=this.buildPopupUrl(e),i=Math.round(window.screenX+(window.outerWidth-500)/2),l=Math.round(window.screenY+(window.outerHeight-700)/2);if(this.popup=window.open(t,"moment-oauth-popup",`width=500,height=700,left=${i},top=${l},menubar=no,toolbar=no,location=yes,status=no`),!this.popup||this.popup.closed){this.sendStatusToIframe("popup-blocked"),this.opts.onPopupBlocked();return}this.sendStatusToIframe("popup-opened"),this.startPolling(),this.messageHandler=s=>{if(s.origin!==this.opts.liveOrigin||s.source!==this.popup)return;const r=s.data;!r||typeof r!="object"||r.source!=="moment-live-embed"||r.type!=="moment.embed.result"||(this.resultReceived=!0,this.sendStatusToIframe("completed"),this.opts.iframeWindow&&this.opts.iframeWindow.postMessage(r,this.opts.liveOrigin),this.opts.onResult(r),this.cleanup())},window.addEventListener("message",this.messageHandler)}cleanup(){if(this.pollTimer&&(clearInterval(this.pollTimer),this.pollTimer=null),this.messageHandler&&(window.removeEventListener("message",this.messageHandler),this.messageHandler=null),this.popup&&!this.popup.closed)try{this.popup.close()}catch{}this.popup=null}buildPopupUrl(e){const t=window.location.origin,i=new URLSearchParams({provider:e.provider,teamSlug:e.teamSlug,ids:e.ids.join(","),triggerType:e.triggerType,returnOrigin:t});return e.subscriptionType&&i.set("subscriptionType",e.subscriptionType),`${this.opts.liveBaseUrl}/en/embed/oauth-popup?${i.toString()}`}startPolling(){this.pollTimer=setInterval(()=>{(!this.popup||this.popup.closed)&&(this.pollTimer&&(clearInterval(this.pollTimer),this.pollTimer=null),this.resultReceived||(this.sendStatusToIframe("popup-closed"),this.opts.onPopupClosed()),this.cleanup())},500)}sendStatusToIframe(e){if(!this.opts.iframeWindow)return;const t={source:"moment-sdk",type:"moment.embed.oauth.status",status:e};this.opts.iframeWindow.postMessage(t,this.opts.liveOrigin)}}function x(n,e,t=typeof window<"u"?window.location.origin:""){const i=new URLSearchParams;return i.set("teamSlug",e.teamSlug),e.momentSlug&&i.set("momentSlug",e.momentSlug),e.listSlug&&i.set("listSlug",e.listSlug),i.set("triggerType",e.triggerType),e.ids?.length&&i.set("ids",e.ids.join(",")),e.playlistSlugs?.length&&i.set("playlistSlugs",e.playlistSlugs.join(",")),e.calendar&&i.set("calendar",e.calendar),e.apiSourceId&&i.set("subscriptionId",e.apiSourceId),e.externalEventId&&i.set("externalId",e.externalEventId),e.externalEventIdPrefix&&!e.externalEventId&&i.set("externalIdPrefix",e.externalEventIdPrefix),i.set("returnOrigin",t),i.set("embed","1"),`${n}/en/embed/sync?${i.toString()}`}const v="moment-sdk-trigger-styles",S=`
2
2
  :where(.moment-sync-trigger) {
3
3
  background-color: #1c1917;
4
4
  color: #ffffff;
@@ -22,4 +22,4 @@
22
22
  outline: 2px solid #1c1917;
23
23
  outline-offset: 2px;
24
24
  }
25
- `;function S(){if(typeof document>"u"||document.getElementById(g))return;const s=document.createElement("style");s.id=g,s.textContent=T,document.head.appendChild(s)}const E=".moment-sync-trigger",x="https://live.momentco.ai";class f{opts;liveBaseUrl;liveOrigin;overlay=null;iframe=null;popupBridge=null;messageHandler=null;boundClickHandlers=new Map;activePayload=null;previousFocusedElement=null;previousBodyOverflow="";didManageBodyOverflow=!1;closeTimer=null;constructor(e){this.opts={triggerSelector:E,...e},this.liveBaseUrl=x;try{this.liveOrigin=new URL(this.liveBaseUrl).origin}catch{throw new Error(`[MomentSdk] Invalid VITE_LIVE_BASE_URL: "${this.liveBaseUrl}"`)}S(),this.bindTriggers(),this.emitAnalytics("embed.init")}open(e){const t=this.scopeOpenPayload(e);this.emitAnalytics("embed.open",{teamSlug:t.teamSlug,triggerType:t.triggerType}),this.createModal(t)}close(){this.handleCancel()}rebind(){this.unbindTriggers(),this.bindTriggers()}destroy(){this.cleanup(),this.unbindTriggers()}bindTriggers(){document.querySelectorAll(this.opts.triggerSelector).forEach(t=>{const i=n=>{n.preventDefault(),this.handleTriggerClick(t)},o=this.boundClickHandlers.get(t);o&&t.removeEventListener("click",o),t.addEventListener("click",i),this.boundClickHandlers.set(t,i)})}unbindTriggers(){this.boundClickHandlers.forEach((e,t)=>{t.removeEventListener("click",e)}),this.boundClickHandlers.clear()}handleTriggerClick(e){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");let r="moment";n!=null&&n!==""&&(n==="moment"||n==="list"||n==="team"||n==="brand-playlist"?r=n:console.warn("[MomentSdk] Invalid data-moment-trigger-type, falling back to 'moment':",n));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");let m;a!=null&&a!==""&&(a==="google"||a==="outlook"?m=a:console.warn("[MomentSdk] Invalid data-moment-calendar, ignoring value:",a));const l=e.getAttribute("data-api-source-id")??void 0,u=e.getAttribute("data-external-event-id")??void 0;if(!t){console.warn("[MomentSdk] Missing data-moment-team-slug on trigger element");return}const p=r==="team"||r==="brand-playlist";this.emitAnalytics("embed.trigger.click",{teamSlug:t,triggerType:r,momentIds:p?[]:c}),this.open({teamSlug:t,momentSlug:i,listSlug:o,triggerType:r,ids:c,calendar:m??void 0,apiSourceId:l,externalEventId:u})}scopeOpenPayload(e){return e.triggerType==="team"||e.triggerType==="brand-playlist"?{teamSlug:e.teamSlug,triggerType:e.triggerType,calendar:e.calendar}:e}createModal(e){this.cleanup(),this.previousFocusedElement=document.activeElement||null,this.activePayload=e;const t=this.buildIframeUrl(e);this.overlay=document.createElement("div"),this.overlay.setAttribute("role","presentation"),Object.assign(this.overlay.style,{position:"fixed",inset:"0",zIndex:"999999",display:"flex",alignItems:"center",justifyContent:"center",backgroundColor:"rgba(0, 0, 0, 0.5)",backdropFilter:"blur(2px)",padding:"16px"}),this.overlay.addEventListener("click",l=>{l.target===this.overlay&&this.handleCancel()});const i=l=>{l.key==="Escape"&&this.handleCancel()};document.addEventListener("keydown",i);const o=document.createElement("div");Object.assign(o.style,{position:"relative",width:"100%",maxWidth:"380px",borderRadius:"16px",overflow:"hidden",backgroundColor:"#000000",boxShadow:"0 25px 50px -12px rgba(0, 0, 0, 0.25)",display:"flex",flexDirection:"column"});const n=document.createElement("div");n.setAttribute("role","dialog"),n.setAttribute("aria-modal","true"),n.setAttribute("aria-label","Calendar sync modal"),n.tabIndex=-1,Object.assign(n.style,{position:"relative",width:"100%",maxHeight:"80vh",borderRadius:"12px",overflow:"auto",backgroundColor:"#ffffff"});const r=document.createElement("button");r.type="button",r.innerHTML='<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M1 1l12 12M13 1L1 13" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>',r.setAttribute("aria-label","Close"),Object.assign(r.style,{position:"absolute",top:"10px",right:"10px",zIndex:"10",border:"none",backgroundColor:"transparent",color:"#999",cursor:"pointer",display:"flex",alignItems:"center",justifyContent:"center",width:"24px",height:"24px",padding:"0"}),r.addEventListener("mouseenter",()=>{r.style.color="#666"}),r.addEventListener("mouseleave",()=>{r.style.color="#999"}),r.addEventListener("click",()=>this.handleCancel()),this.iframe=document.createElement("iframe"),this.iframe.src=t,this.iframe.setAttribute("allow",""),this.iframe.setAttribute("sandbox","allow-scripts allow-same-origin allow-popups allow-forms"),this.iframe.setAttribute("scrolling","no"),Object.assign(this.iframe.style,{width:"100%",border:"none",display:"block",height:"420px",overflow:"hidden",transition:"height 0.15s ease"}),n.appendChild(r),n.appendChild(this.iframe);const d=document.createElement("div");Object.assign(d.style,{display:"flex",alignItems:"center",justifyContent:"center",gap:"6px",padding:"12px 0"});const c=document.createElement("span");c.textContent="Powered by",Object.assign(c.style,{fontFamily:"Inter, system-ui, -apple-system, sans-serif",fontSize:"12px",lineHeight:"16px",color:"#ffffff"});const a=document.createElement("img");a.alt="Moment",a.src=`${this.liveBaseUrl}/brand/moment-text-white.png`,Object.assign(a.style,{height:"auto",width:"67px"}),d.appendChild(c),d.appendChild(a),o.appendChild(n),o.appendChild(d),this.overlay.appendChild(o),document.body.appendChild(this.overlay);const m=l=>{if(l.key!=="Tab")return;const u=n.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');if(u.length===0){l.preventDefault(),n.focus();return}const p=u[0],b=u[u.length-1],y=document.activeElement;l.shiftKey&&y===p?(l.preventDefault(),b.focus()):!l.shiftKey&&y===b&&(l.preventDefault(),p.focus())};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}buildIframeUrl(e){const t=new URLSearchParams;return t.set("teamSlug",e.teamSlug),e.momentSlug&&t.set("momentSlug",e.momentSlug),e.listSlug&&t.set("listSlug",e.listSlug),t.set("triggerType",e.triggerType),e.ids?.length&&t.set("ids",e.ids.join(",")),e.calendar&&t.set("calendar",e.calendar),e.apiSourceId&&t.set("subscriptionId",e.apiSourceId),e.externalEventId&&t.set("externalId",e.externalEventId),t.set("returnOrigin",window.location.origin),t.set("embed","1"),`${this.liveBaseUrl}/en/embed/sync?${t.toString()}`}setupMessageListener(){this.messageHandler=e=>{if(e.origin!==this.liveOrigin||e.source!==this.iframe?.contentWindow)return;const t=e.data;if(!(!t||typeof t!="object")){if(t.source==="moment-live-embed"&&t.type==="moment.embed.oauth.start"){const i=t;this.emitAnalytics("oauth.start",{provider:i.provider,teamSlug:i.teamSlug}),this.handleOAuthStart(i);return}if(t.source==="moment-live-embed"&&t.type==="moment.embed.resize"){const i=typeof t.height=="number"?t.height:0;if(i>0&&this.iframe){const o=Math.round(window.innerHeight*.8);this.iframe.style.height=`${Math.min(i,o)}px`}return}if(t.source==="moment-live-embed"&&t.type==="moment.embed.open.url"){const{url:i}=t;i&&typeof i=="string"&&i.startsWith("webcal://")&&window.open(i,"_blank");return}if(t.source==="moment-live-embed"&&t.type==="moment.embed.result"){const i=t;this.handleResult(i)}}},window.addEventListener("message",this.messageHandler)}handleOAuthStart(e){this.popupBridge&&(this.popupBridge.cleanup(),this.popupBridge=null),this.popupBridge=new w({liveBaseUrl:this.liveBaseUrl,liveOrigin:this.liveOrigin,iframeWindow:this.iframe?.contentWindow??null,onResult:t=>{this.handleResult(t)},onPopupBlocked:()=>{this.emitAnalytics("oauth.popup.blocked",{provider:e.provider})},onPopupClosed:()=>{this.emitAnalytics("oauth.popup.closed",{provider:e.provider})}}),this.popupBridge.open(e)}handleResult(e){if(e.result==="success")this.emitAnalytics("subscribe.success",{provider:e.provider,triggerType:e.triggerType,momentIds:e.momentIds}),this.opts.onSuccess?.(e);else if(e.result==="error")this.emitAnalytics("subscribe.error",{provider:e.provider,triggerType:e.triggerType,momentIds:e.momentIds}),this.opts.onError?.(e);else if(e.result==="cancelled")return;this.opts.onClose?.(e),this.emitAnalytics("embed.close"),this.closeTimer=setTimeout(()=>this.cleanup(),1500)}handleCancel(){this.emitAnalytics("embed.close");const t={source:"moment-live-embed",type:"moment.embed.result",result:"cancelled",triggerType:this.activePayload?.triggerType??"moment",momentIds:this.activePayload?.ids??[]};this.opts.onClose?.(t),this.cleanup()}cleanup(){if(this.closeTimer&&(clearTimeout(this.closeTimer),this.closeTimer=null),this.popupBridge&&(this.popupBridge.cleanup(),this.popupBridge=null),this.messageHandler&&(window.removeEventListener("message",this.messageHandler),this.messageHandler=null),this.overlay){const{_escHandler:e,_focusTrapHandler:t}=this.overlay;e&&document.removeEventListener("keydown",e);const i=this.overlay.firstElementChild;i&&t&&i.removeEventListener("keydown",t),this.overlay.remove(),this.overlay=null}this.iframe=null,this.activePayload=null,this.didManageBodyOverflow&&(document.body.style.overflow=this.previousBodyOverflow,this.didManageBodyOverflow=!1),this.previousFocusedElement&&typeof this.previousFocusedElement.focus=="function"&&this.previousFocusedElement.focus(),this.previousFocusedElement=null}emitAnalytics(e,t){this.opts.onAnalytics?.({event:e,timestamp:Date.now(),...t})}}function v(s){const e=document.createElement("button");return e.type="button",e.classList.add("moment-sync-trigger"),s.className&&s.className.split(" ").forEach(t=>{t&&e.classList.add(t)}),e.setAttribute("data-moment-team-slug",s.teamSlug),s.momentSlug&&e.setAttribute("data-moment-slug",s.momentSlug),s.listSlug&&e.setAttribute("data-moment-list-slug",s.listSlug),s.triggerType&&e.setAttribute("data-moment-trigger-type",s.triggerType),s.ids?.length&&e.setAttribute("data-moment-ids",s.ids.join(",")),s.calendar&&e.setAttribute("data-moment-calendar",s.calendar),s.apiSourceId&&e.setAttribute("data-api-source-id",s.apiSourceId),s.externalEventId&&e.setAttribute("data-external-event-id",s.externalEventId),e.textContent=s.label??"Add to Calendar",e}if(typeof window<"u"){const s=window;s.MomentSdk=f,s.createMomentButton=v}h.MomentSdk=f,h.createMomentButton=v,Object.defineProperty(h,Symbol.toStringTag,{value:"Module"})})(this.MomentSdk=this.MomentSdk||{});
25
+ `;function T(){if(typeof document>"u"||document.getElementById(v))return;const n=document.createElement("style");n.id=v,n.textContent=S,document.head.appendChild(n)}const E=".moment-sync-trigger",I="https://live.dev.momentco.ai";class b{opts;liveBaseUrl;liveOrigin;overlay=null;iframe=null;popupBridge=null;messageHandler=null;boundClickHandlers=new Map;activePayload=null;previousFocusedElement=null;previousBodyOverflow="";didManageBodyOverflow=!1;closeTimer=null;constructor(e){this.opts={triggerSelector:E,...e},this.liveBaseUrl=I;try{this.liveOrigin=new URL(this.liveBaseUrl).origin}catch{throw new Error(`[MomentSdk] Invalid VITE_LIVE_BASE_URL: "${this.liveBaseUrl}"`)}T(),this.bindTriggers(),this.emitAnalytics("embed.init")}open(e){e.externalEventId&&e.externalEventIdPrefix&&console.warn("[MomentSdk] Both externalEventId and externalEventIdPrefix provided; using externalEventId only.");const t=this.scopeOpenPayload(e);this.emitAnalytics("embed.open",{teamSlug:t.teamSlug,triggerType:t.triggerType}),this.createModal(t)}close(){this.handleCancel()}rebind(){this.unbindTriggers(),this.bindTriggers()}destroy(){this.cleanup(),this.unbindTriggers()}bindTriggers(){document.querySelectorAll(this.opts.triggerSelector).forEach(t=>{const i=s=>{s.preventDefault(),this.handleTriggerClick(t)},l=this.boundClickHandlers.get(t);l&&t.removeEventListener("click",l),t.addEventListener("click",i),this.boundClickHandlers.set(t,i)})}unbindTriggers(){this.boundClickHandlers.forEach((e,t)=>{t.removeEventListener("click",e)}),this.boundClickHandlers.clear()}handleTriggerClick(e){const t=e.getAttribute("data-moment-team-slug")??"",i=e.getAttribute("data-moment-slug")??void 0,l=e.getAttribute("data-moment-list-slug")??void 0,s=e.getAttribute("data-moment-trigger-type");let r="moment";s!=null&&s!==""&&(s==="moment"||s==="list"||s==="team"||s==="brand-playlist"||s==="playlist"?r=s:console.warn("[MomentSdk] Invalid data-moment-trigger-type, falling back to 'moment':",s));const a=e.getAttribute("data-moment-ids")??e.getAttribute("data-moment-game-id")??"",m=a?a.split(",").filter(Boolean):[],d=e.getAttribute("data-moment-playlist-slugs")??"",g=d?d.split(",").map(A=>A.trim()).filter(Boolean):[],o=e.getAttribute("data-moment-calendar");let u;o!=null&&o!==""&&(o==="google"||o==="outlook"?u=o:console.warn("[MomentSdk] Invalid data-moment-calendar, ignoring value:",o));const h=e.getAttribute("data-api-source-id")??void 0,c=e.getAttribute("data-external-event-id")??void 0,p=e.getAttribute("data-external-event-id-prefix")??void 0;if(c&&p&&console.warn("[MomentSdk] Both data-external-event-id and data-external-event-id-prefix are set; using data-external-event-id only."),!t){console.warn("[MomentSdk] Missing data-moment-team-slug on trigger element");return}const P=r==="team"||r==="brand-playlist"||r==="playlist";this.emitAnalytics("embed.trigger.click",{teamSlug:t,triggerType:r,momentIds:P?[]:m}),this.open({teamSlug:t,momentSlug:i,listSlug:l,triggerType:r,ids:m,playlistSlugs:g,calendar:u??void 0,apiSourceId:h,externalEventId:p&&!c?void 0:c,externalEventIdPrefix:c?void 0:p})}scopeOpenPayload(e){return e.triggerType==="playlist"?{teamSlug:e.teamSlug,triggerType:e.triggerType,playlistSlugs:e.playlistSlugs,calendar:e.calendar}:e.triggerType==="team"||e.triggerType==="brand-playlist"?{teamSlug:e.teamSlug,triggerType:e.triggerType,calendar:e.calendar}:e}createModal(e){this.cleanup(),this.previousFocusedElement=document.activeElement||null,this.activePayload=e;const t=this.buildIframeUrl(e);this.overlay=document.createElement("div"),this.overlay.setAttribute("role","presentation"),Object.assign(this.overlay.style,{position:"fixed",inset:"0",zIndex:"999999",display:"flex",alignItems:"center",justifyContent:"center",backgroundColor:"rgba(0, 0, 0, 0.5)",backdropFilter:"blur(2px)",padding:"16px"}),this.overlay.addEventListener("click",o=>{o.target===this.overlay&&this.handleCancel()});const i=o=>{o.key==="Escape"&&this.handleCancel()};document.addEventListener("keydown",i);const l=document.createElement("div");Object.assign(l.style,{position:"relative",width:"100%",maxWidth:"380px",borderRadius:"16px",overflow:"hidden",backgroundColor:"#000000",boxShadow:"0 25px 50px -12px rgba(0, 0, 0, 0.25)",display:"flex",flexDirection:"column"});const s=document.createElement("div");s.setAttribute("role","dialog"),s.setAttribute("aria-modal","true"),s.setAttribute("aria-label","Calendar sync modal"),s.tabIndex=-1,Object.assign(s.style,{position:"relative",width:"100%",maxHeight:"80vh",borderRadius:"12px",overflow:"auto",backgroundColor:"#ffffff"});const r=document.createElement("button");r.type="button",r.innerHTML='<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M1 1l12 12M13 1L1 13" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>',r.setAttribute("aria-label","Close"),Object.assign(r.style,{position:"absolute",top:"10px",right:"10px",zIndex:"10",border:"none",backgroundColor:"transparent",color:"#999",cursor:"pointer",display:"flex",alignItems:"center",justifyContent:"center",width:"24px",height:"24px",padding:"0"}),r.addEventListener("mouseenter",()=>{r.style.color="#666"}),r.addEventListener("mouseleave",()=>{r.style.color="#999"}),r.addEventListener("click",()=>this.handleCancel()),this.iframe=document.createElement("iframe"),this.iframe.src=t,this.iframe.setAttribute("allow",""),this.iframe.setAttribute("sandbox","allow-scripts allow-same-origin allow-popups allow-forms"),this.iframe.setAttribute("scrolling","no"),Object.assign(this.iframe.style,{width:"100%",border:"none",display:"block",height:"420px",overflow:"hidden",transition:"height 0.15s ease"}),s.appendChild(r),s.appendChild(this.iframe);const a=document.createElement("div");Object.assign(a.style,{display:"flex",alignItems:"center",justifyContent:"center",gap:"6px",padding:"12px 0"});const m=document.createElement("span");m.textContent="Powered by",Object.assign(m.style,{fontFamily:"Inter, system-ui, -apple-system, sans-serif",fontSize:"12px",lineHeight:"16px",color:"#ffffff"});const d=document.createElement("img");d.alt="Moment",d.src=`${this.liveBaseUrl}/brand/moment-text-white.png`,Object.assign(d.style,{height:"auto",width:"67px"}),a.appendChild(m),a.appendChild(d),l.appendChild(s),l.appendChild(a),this.overlay.appendChild(l),document.body.appendChild(this.overlay);const g=o=>{if(o.key!=="Tab")return;const u=s.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');if(u.length===0){o.preventDefault(),s.focus();return}const h=u[0],c=u[u.length-1],p=document.activeElement;o.shiftKey&&p===h?(o.preventDefault(),c.focus()):!o.shiftKey&&p===c&&(o.preventDefault(),h.focus())};s.addEventListener("keydown",g),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=g}buildIframeUrl(e){return x(this.liveBaseUrl,e)}setupMessageListener(){this.messageHandler=e=>{if(e.origin!==this.liveOrigin||e.source!==this.iframe?.contentWindow)return;const t=e.data;if(!(!t||typeof t!="object")){if(t.source==="moment-live-embed"&&t.type==="moment.embed.oauth.start"){const i=t;this.emitAnalytics("oauth.start",{provider:i.provider,teamSlug:i.teamSlug}),this.handleOAuthStart(i);return}if(t.source==="moment-live-embed"&&t.type==="moment.embed.resize"){const i=typeof t.height=="number"?t.height:0;if(i>0&&this.iframe){const l=Math.round(window.innerHeight*.8);this.iframe.style.height=`${Math.min(i,l)}px`}return}if(t.source==="moment-live-embed"&&t.type==="moment.embed.open.url"){const{url:i}=t;i&&typeof i=="string"&&i.startsWith("webcal://")&&window.open(i,"_blank");return}if(t.source==="moment-live-embed"&&t.type==="moment.embed.result"){const i=t;this.handleResult(i)}}},window.addEventListener("message",this.messageHandler)}handleOAuthStart(e){this.popupBridge&&(this.popupBridge.cleanup(),this.popupBridge=null),this.popupBridge=new w({liveBaseUrl:this.liveBaseUrl,liveOrigin:this.liveOrigin,iframeWindow:this.iframe?.contentWindow??null,onResult:t=>{this.handleResult(t)},onPopupBlocked:()=>{this.emitAnalytics("oauth.popup.blocked",{provider:e.provider})},onPopupClosed:()=>{this.emitAnalytics("oauth.popup.closed",{provider:e.provider})}}),this.popupBridge.open(e)}handleResult(e){if(e.result==="success")this.emitAnalytics("subscribe.success",{provider:e.provider,triggerType:e.triggerType,momentIds:e.momentIds}),this.opts.onSuccess?.(e);else if(e.result==="error")this.emitAnalytics("subscribe.error",{provider:e.provider,triggerType:e.triggerType,momentIds:e.momentIds}),this.opts.onError?.(e);else if(e.result==="cancelled")return;this.opts.onClose?.(e),this.emitAnalytics("embed.close"),this.closeTimer=setTimeout(()=>this.cleanup(),1500)}handleCancel(){this.emitAnalytics("embed.close");const t={source:"moment-live-embed",type:"moment.embed.result",result:"cancelled",triggerType:this.activePayload?.triggerType??"moment",momentIds:this.activePayload?.ids??[]};this.opts.onClose?.(t),this.cleanup()}cleanup(){if(this.closeTimer&&(clearTimeout(this.closeTimer),this.closeTimer=null),this.popupBridge&&(this.popupBridge.cleanup(),this.popupBridge=null),this.messageHandler&&(window.removeEventListener("message",this.messageHandler),this.messageHandler=null),this.overlay){const{_escHandler:e,_focusTrapHandler:t}=this.overlay;e&&document.removeEventListener("keydown",e);const i=this.overlay.firstElementChild;i&&t&&i.removeEventListener("keydown",t),this.overlay.remove(),this.overlay=null}this.iframe=null,this.activePayload=null,this.didManageBodyOverflow&&(document.body.style.overflow=this.previousBodyOverflow,this.didManageBodyOverflow=!1),this.previousFocusedElement&&typeof this.previousFocusedElement.focus=="function"&&this.previousFocusedElement.focus(),this.previousFocusedElement=null}emitAnalytics(e,t){this.opts.onAnalytics?.({event:e,timestamp:Date.now(),...t})}}function y(n){const e=document.createElement("button");return e.type="button",e.classList.add("moment-sync-trigger"),n.className&&n.className.split(" ").forEach(t=>{t&&e.classList.add(t)}),e.setAttribute("data-moment-team-slug",n.teamSlug),n.momentSlug&&e.setAttribute("data-moment-slug",n.momentSlug),n.listSlug&&e.setAttribute("data-moment-list-slug",n.listSlug),n.triggerType&&e.setAttribute("data-moment-trigger-type",n.triggerType),n.ids?.length&&e.setAttribute("data-moment-ids",n.ids.join(",")),n.playlistSlugs?.length&&e.setAttribute("data-moment-playlist-slugs",n.playlistSlugs.join(",")),n.calendar&&e.setAttribute("data-moment-calendar",n.calendar),n.apiSourceId&&e.setAttribute("data-api-source-id",n.apiSourceId),n.externalEventId&&e.setAttribute("data-external-event-id",n.externalEventId),n.externalEventIdPrefix&&e.setAttribute("data-external-event-id-prefix",n.externalEventIdPrefix),e.textContent=n.label??"Add to Calendar",e}if(typeof window<"u"){const n=window;n.MomentSdk=b,n.createMomentButton=y}f.MomentSdk=b,f.createMomentButton=y,Object.defineProperty(f,Symbol.toStringTag,{value:"Module"})})(this.MomentSdk=this.MomentSdk||{});
@@ -1,4 +1,4 @@
1
- class v {
1
+ class y {
2
2
  popup = null;
3
3
  pollTimer = null;
4
4
  messageHandler = null;
@@ -12,18 +12,18 @@ class v {
12
12
  */
13
13
  open(e) {
14
14
  this.cleanup(), this.resultReceived = !1;
15
- const t = this.buildPopupUrl(e), i = Math.round(window.screenX + (window.outerWidth - 500) / 2), o = Math.round(window.screenY + (window.outerHeight - 700) / 2);
15
+ const t = this.buildPopupUrl(e), i = Math.round(window.screenX + (window.outerWidth - 500) / 2), l = Math.round(window.screenY + (window.outerHeight - 700) / 2);
16
16
  if (this.popup = window.open(
17
17
  t,
18
18
  "moment-oauth-popup",
19
- `width=500,height=700,left=${i},top=${o},menubar=no,toolbar=no,location=yes,status=no`
19
+ `width=500,height=700,left=${i},top=${l},menubar=no,toolbar=no,location=yes,status=no`
20
20
  ), !this.popup || this.popup.closed) {
21
21
  this.sendStatusToIframe("popup-blocked"), this.opts.onPopupBlocked();
22
22
  return;
23
23
  }
24
- this.sendStatusToIframe("popup-opened"), this.startPolling(), this.messageHandler = (n) => {
25
- if (n.origin !== this.opts.liveOrigin || n.source !== this.popup) return;
26
- const r = n.data;
24
+ this.sendStatusToIframe("popup-opened"), this.startPolling(), this.messageHandler = (s) => {
25
+ if (s.origin !== this.opts.liveOrigin || s.source !== this.popup) return;
26
+ const r = s.data;
27
27
  !r || typeof r != "object" || r.source !== "moment-live-embed" || r.type !== "moment.embed.result" || (this.resultReceived = !0, this.sendStatusToIframe("completed"), this.opts.iframeWindow && this.opts.iframeWindow.postMessage(r, this.opts.liveOrigin), this.opts.onResult(r), this.cleanup());
28
28
  }, window.addEventListener("message", this.messageHandler);
29
29
  }
@@ -63,7 +63,11 @@ class v {
63
63
  this.opts.iframeWindow.postMessage(t, this.opts.liveOrigin);
64
64
  }
65
65
  }
66
- const f = "moment-sdk-trigger-styles", b = `
66
+ function w(n, e, t = typeof window < "u" ? window.location.origin : "") {
67
+ const i = new URLSearchParams();
68
+ return i.set("teamSlug", e.teamSlug), e.momentSlug && i.set("momentSlug", e.momentSlug), e.listSlug && i.set("listSlug", e.listSlug), i.set("triggerType", e.triggerType), e.ids?.length && i.set("ids", e.ids.join(",")), e.playlistSlugs?.length && i.set("playlistSlugs", e.playlistSlugs.join(",")), e.calendar && i.set("calendar", e.calendar), e.apiSourceId && i.set("subscriptionId", e.apiSourceId), e.externalEventId && i.set("externalId", e.externalEventId), e.externalEventIdPrefix && !e.externalEventId && i.set("externalIdPrefix", e.externalEventIdPrefix), i.set("returnOrigin", t), i.set("embed", "1"), `${n}/en/embed/sync?${i.toString()}`;
69
+ }
70
+ const f = "moment-sdk-trigger-styles", x = `
67
71
  :where(.moment-sync-trigger) {
68
72
  background-color: #1c1917;
69
73
  color: #ffffff;
@@ -88,13 +92,13 @@ const f = "moment-sdk-trigger-styles", b = `
88
92
  outline-offset: 2px;
89
93
  }
90
94
  `;
91
- function y() {
95
+ function T() {
92
96
  if (typeof document > "u" || document.getElementById(f)) return;
93
- const s = document.createElement("style");
94
- s.id = f, s.textContent = b, document.head.appendChild(s);
97
+ const n = document.createElement("style");
98
+ n.id = f, n.textContent = x, document.head.appendChild(n);
95
99
  }
96
- const w = ".moment-sync-trigger", T = "https://live.momentco.ai";
97
- class E {
100
+ const E = ".moment-sync-trigger", S = "https://live.dev.momentco.ai";
101
+ class I {
98
102
  opts;
99
103
  liveBaseUrl;
100
104
  liveOrigin;
@@ -110,20 +114,23 @@ class E {
110
114
  closeTimer = null;
111
115
  constructor(e) {
112
116
  this.opts = {
113
- triggerSelector: w,
117
+ triggerSelector: E,
114
118
  ...e
115
- }, this.liveBaseUrl = T;
119
+ }, this.liveBaseUrl = S;
116
120
  try {
117
121
  this.liveOrigin = new URL(this.liveBaseUrl).origin;
118
122
  } catch {
119
123
  throw new Error(`[MomentSdk] Invalid VITE_LIVE_BASE_URL: "${this.liveBaseUrl}"`);
120
124
  }
121
- y(), this.bindTriggers(), this.emitAnalytics("embed.init");
125
+ T(), this.bindTriggers(), this.emitAnalytics("embed.init");
122
126
  }
123
127
  /**
124
128
  * Programmatically open the sync modal.
125
129
  */
126
130
  open(e) {
131
+ e.externalEventId && e.externalEventIdPrefix && console.warn(
132
+ "[MomentSdk] Both externalEventId and externalEventIdPrefix provided; using externalEventId only."
133
+ );
127
134
  const t = this.scopeOpenPayload(e);
128
135
  this.emitAnalytics("embed.open", {
129
136
  teamSlug: t.teamSlug,
@@ -151,10 +158,10 @@ class E {
151
158
  // ── Private: trigger binding ──────────────────────────────────────
152
159
  bindTriggers() {
153
160
  document.querySelectorAll(this.opts.triggerSelector).forEach((t) => {
154
- const i = (n) => {
155
- n.preventDefault(), this.handleTriggerClick(t);
156
- }, o = this.boundClickHandlers.get(t);
157
- o && t.removeEventListener("click", o), t.addEventListener("click", i), this.boundClickHandlers.set(t, i);
161
+ const i = (s) => {
162
+ s.preventDefault(), this.handleTriggerClick(t);
163
+ }, l = this.boundClickHandlers.get(t);
164
+ l && t.removeEventListener("click", l), t.addEventListener("click", i), this.boundClickHandlers.set(t, i);
158
165
  });
159
166
  }
160
167
  unbindTriggers() {
@@ -163,39 +170,48 @@ class E {
163
170
  }), this.boundClickHandlers.clear();
164
171
  }
165
172
  handleTriggerClick(e) {
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");
173
+ const t = e.getAttribute("data-moment-team-slug") ?? "", i = e.getAttribute("data-moment-slug") ?? void 0, l = e.getAttribute("data-moment-list-slug") ?? void 0, s = e.getAttribute("data-moment-trigger-type");
167
174
  let r = "moment";
168
- n != null && n !== "" && (n === "moment" || n === "list" || n === "team" || n === "brand-playlist" ? r = n : console.warn(
175
+ s != null && s !== "" && (s === "moment" || s === "list" || s === "team" || s === "brand-playlist" || s === "playlist" ? r = s : console.warn(
169
176
  "[MomentSdk] Invalid data-moment-trigger-type, falling back to 'moment':",
170
- n
177
+ s
171
178
  ));
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");
173
- let m;
174
- a != null && a !== "" && (a === "google" || a === "outlook" ? m = a : console.warn("[MomentSdk] Invalid data-moment-calendar, ignoring value:", a));
175
- const l = e.getAttribute("data-api-source-id") ?? void 0, u = e.getAttribute("data-external-event-id") ?? void 0;
176
- if (!t) {
179
+ const a = e.getAttribute("data-moment-ids") ?? e.getAttribute("data-moment-game-id") ?? "", m = a ? a.split(",").filter(Boolean) : [], d = e.getAttribute("data-moment-playlist-slugs") ?? "", g = d ? d.split(",").map((b) => b.trim()).filter(Boolean) : [], o = e.getAttribute("data-moment-calendar");
180
+ let u;
181
+ o != null && o !== "" && (o === "google" || o === "outlook" ? u = o : console.warn("[MomentSdk] Invalid data-moment-calendar, ignoring value:", o));
182
+ const h = e.getAttribute("data-api-source-id") ?? void 0, c = e.getAttribute("data-external-event-id") ?? void 0, p = e.getAttribute("data-external-event-id-prefix") ?? void 0;
183
+ if (c && p && console.warn(
184
+ "[MomentSdk] Both data-external-event-id and data-external-event-id-prefix are set; using data-external-event-id only."
185
+ ), !t) {
177
186
  console.warn("[MomentSdk] Missing data-moment-team-slug on trigger element");
178
187
  return;
179
188
  }
180
- const p = r === "team" || r === "brand-playlist";
189
+ const v = r === "team" || r === "brand-playlist" || r === "playlist";
181
190
  this.emitAnalytics("embed.trigger.click", {
182
191
  teamSlug: t,
183
192
  triggerType: r,
184
- momentIds: p ? [] : c
193
+ momentIds: v ? [] : m
185
194
  }), this.open({
186
195
  teamSlug: t,
187
196
  momentSlug: i,
188
- listSlug: o,
197
+ listSlug: l,
189
198
  triggerType: r,
190
- ids: c,
191
- calendar: m ?? void 0,
192
- apiSourceId: l,
193
- externalEventId: u
199
+ ids: m,
200
+ playlistSlugs: g,
201
+ calendar: u ?? void 0,
202
+ apiSourceId: h,
203
+ externalEventId: p && !c ? void 0 : c,
204
+ externalEventIdPrefix: c ? void 0 : p
194
205
  });
195
206
  }
196
207
  /** Drop moment/list/source scoping for brand-level subscription triggers. */
197
208
  scopeOpenPayload(e) {
198
- return e.triggerType === "team" || e.triggerType === "brand-playlist" ? {
209
+ return e.triggerType === "playlist" ? {
210
+ teamSlug: e.teamSlug,
211
+ triggerType: e.triggerType,
212
+ playlistSlugs: e.playlistSlugs,
213
+ calendar: e.calendar
214
+ } : e.triggerType === "team" || e.triggerType === "brand-playlist" ? {
199
215
  teamSlug: e.teamSlug,
200
216
  triggerType: e.triggerType,
201
217
  calendar: e.calendar
@@ -215,15 +231,15 @@ class E {
215
231
  backgroundColor: "rgba(0, 0, 0, 0.5)",
216
232
  backdropFilter: "blur(2px)",
217
233
  padding: "16px"
218
- }), this.overlay.addEventListener("click", (l) => {
219
- l.target === this.overlay && this.handleCancel();
234
+ }), this.overlay.addEventListener("click", (o) => {
235
+ o.target === this.overlay && this.handleCancel();
220
236
  });
221
- const i = (l) => {
222
- l.key === "Escape" && this.handleCancel();
237
+ const i = (o) => {
238
+ o.key === "Escape" && this.handleCancel();
223
239
  };
224
240
  document.addEventListener("keydown", i);
225
- const o = document.createElement("div");
226
- Object.assign(o.style, {
241
+ const l = document.createElement("div");
242
+ Object.assign(l.style, {
227
243
  position: "relative",
228
244
  width: "100%",
229
245
  maxWidth: "380px",
@@ -234,8 +250,8 @@ class E {
234
250
  display: "flex",
235
251
  flexDirection: "column"
236
252
  });
237
- const n = document.createElement("div");
238
- n.setAttribute("role", "dialog"), n.setAttribute("aria-modal", "true"), n.setAttribute("aria-label", "Calendar sync modal"), n.tabIndex = -1, Object.assign(n.style, {
253
+ const s = document.createElement("div");
254
+ s.setAttribute("role", "dialog"), s.setAttribute("aria-modal", "true"), s.setAttribute("aria-label", "Calendar sync modal"), s.tabIndex = -1, Object.assign(s.style, {
239
255
  position: "relative",
240
256
  width: "100%",
241
257
  maxHeight: "80vh",
@@ -270,44 +286,43 @@ class E {
270
286
  height: "420px",
271
287
  overflow: "hidden",
272
288
  transition: "height 0.15s ease"
273
- }), n.appendChild(r), n.appendChild(this.iframe);
274
- const d = document.createElement("div");
275
- Object.assign(d.style, {
289
+ }), s.appendChild(r), s.appendChild(this.iframe);
290
+ const a = document.createElement("div");
291
+ Object.assign(a.style, {
276
292
  display: "flex",
277
293
  alignItems: "center",
278
294
  justifyContent: "center",
279
295
  gap: "6px",
280
296
  padding: "12px 0"
281
297
  });
282
- const c = document.createElement("span");
283
- c.textContent = "Powered by", Object.assign(c.style, {
298
+ const m = document.createElement("span");
299
+ m.textContent = "Powered by", Object.assign(m.style, {
284
300
  fontFamily: "Inter, system-ui, -apple-system, sans-serif",
285
301
  fontSize: "12px",
286
302
  lineHeight: "16px",
287
303
  color: "#ffffff"
288
304
  });
289
- const a = document.createElement("img");
290
- a.alt = "Moment", a.src = `${this.liveBaseUrl}/brand/moment-text-white.png`, Object.assign(a.style, {
305
+ const d = document.createElement("img");
306
+ d.alt = "Moment", d.src = `${this.liveBaseUrl}/brand/moment-text-white.png`, Object.assign(d.style, {
291
307
  height: "auto",
292
308
  width: "67px"
293
- }), d.appendChild(c), d.appendChild(a), o.appendChild(n), o.appendChild(d), this.overlay.appendChild(o), document.body.appendChild(this.overlay);
294
- const m = (l) => {
295
- if (l.key !== "Tab") return;
296
- const u = n.querySelectorAll(
309
+ }), a.appendChild(m), a.appendChild(d), l.appendChild(s), l.appendChild(a), this.overlay.appendChild(l), document.body.appendChild(this.overlay);
310
+ const g = (o) => {
311
+ if (o.key !== "Tab") return;
312
+ const u = s.querySelectorAll(
297
313
  'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
298
314
  );
299
315
  if (u.length === 0) {
300
- l.preventDefault(), n.focus();
316
+ o.preventDefault(), s.focus();
301
317
  return;
302
318
  }
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());
319
+ const h = u[0], c = u[u.length - 1], p = document.activeElement;
320
+ o.shiftKey && p === h ? (o.preventDefault(), c.focus()) : !o.shiftKey && p === c && (o.preventDefault(), h.focus());
305
321
  };
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;
322
+ s.addEventListener("keydown", g), 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 = g;
307
323
  }
308
324
  buildIframeUrl(e) {
309
- const t = new URLSearchParams();
310
- return t.set("teamSlug", e.teamSlug), e.momentSlug && t.set("momentSlug", e.momentSlug), e.listSlug && t.set("listSlug", e.listSlug), t.set("triggerType", e.triggerType), e.ids?.length && t.set("ids", e.ids.join(",")), e.calendar && t.set("calendar", e.calendar), e.apiSourceId && t.set("subscriptionId", e.apiSourceId), e.externalEventId && t.set("externalId", e.externalEventId), t.set("returnOrigin", window.location.origin), t.set("embed", "1"), `${this.liveBaseUrl}/en/embed/sync?${t.toString()}`;
325
+ return w(this.liveBaseUrl, e);
311
326
  }
312
327
  // ── Private: message handling ─────────────────────────────────────
313
328
  setupMessageListener() {
@@ -326,8 +341,8 @@ class E {
326
341
  if (t.source === "moment-live-embed" && t.type === "moment.embed.resize") {
327
342
  const i = typeof t.height == "number" ? t.height : 0;
328
343
  if (i > 0 && this.iframe) {
329
- const o = Math.round(window.innerHeight * 0.8);
330
- this.iframe.style.height = `${Math.min(i, o)}px`;
344
+ const l = Math.round(window.innerHeight * 0.8);
345
+ this.iframe.style.height = `${Math.min(i, l)}px`;
331
346
  }
332
347
  return;
333
348
  }
@@ -344,7 +359,7 @@ class E {
344
359
  }, window.addEventListener("message", this.messageHandler);
345
360
  }
346
361
  handleOAuthStart(e) {
347
- this.popupBridge && (this.popupBridge.cleanup(), this.popupBridge = null), this.popupBridge = new v({
362
+ this.popupBridge && (this.popupBridge.cleanup(), this.popupBridge = null), this.popupBridge = new y({
348
363
  liveBaseUrl: this.liveBaseUrl,
349
364
  liveOrigin: this.liveOrigin,
350
365
  iframeWindow: this.iframe?.contentWindow ?? null,
@@ -410,17 +425,17 @@ class E {
410
425
  });
411
426
  }
412
427
  }
413
- function S(s) {
428
+ function P(n) {
414
429
  const e = document.createElement("button");
415
- return e.type = "button", e.classList.add("moment-sync-trigger"), s.className && s.className.split(" ").forEach((t) => {
430
+ return e.type = "button", e.classList.add("moment-sync-trigger"), n.className && n.className.split(" ").forEach((t) => {
416
431
  t && e.classList.add(t);
417
- }), e.setAttribute("data-moment-team-slug", s.teamSlug), s.momentSlug && e.setAttribute("data-moment-slug", s.momentSlug), s.listSlug && e.setAttribute("data-moment-list-slug", s.listSlug), s.triggerType && e.setAttribute("data-moment-trigger-type", s.triggerType), s.ids?.length && e.setAttribute("data-moment-ids", s.ids.join(",")), s.calendar && e.setAttribute("data-moment-calendar", s.calendar), s.apiSourceId && e.setAttribute("data-api-source-id", s.apiSourceId), s.externalEventId && e.setAttribute("data-external-event-id", s.externalEventId), e.textContent = s.label ?? "Add to Calendar", e;
432
+ }), e.setAttribute("data-moment-team-slug", n.teamSlug), n.momentSlug && e.setAttribute("data-moment-slug", n.momentSlug), n.listSlug && e.setAttribute("data-moment-list-slug", n.listSlug), n.triggerType && e.setAttribute("data-moment-trigger-type", n.triggerType), n.ids?.length && e.setAttribute("data-moment-ids", n.ids.join(",")), n.playlistSlugs?.length && e.setAttribute("data-moment-playlist-slugs", n.playlistSlugs.join(",")), n.calendar && e.setAttribute("data-moment-calendar", n.calendar), n.apiSourceId && e.setAttribute("data-api-source-id", n.apiSourceId), n.externalEventId && e.setAttribute("data-external-event-id", n.externalEventId), n.externalEventIdPrefix && e.setAttribute("data-external-event-id-prefix", n.externalEventIdPrefix), e.textContent = n.label ?? "Add to Calendar", e;
418
433
  }
419
434
  if (typeof window < "u") {
420
- const s = window;
421
- s.MomentSdk = E, s.createMomentButton = S;
435
+ const n = window;
436
+ n.MomentSdk = I, n.createMomentButton = P;
422
437
  }
423
438
  export {
424
- E as MomentSdk,
425
- S as createMomentButton
439
+ I as MomentSdk,
440
+ P as createMomentButton
426
441
  };
@@ -0,0 +1 @@
1
+ export {};
@@ -10,11 +10,15 @@ export declare function createMomentButton(options: {
10
10
  listSlug?: string;
11
11
  triggerType?: MomentTriggerType;
12
12
  ids?: string[];
13
+ /** Playlist slugs offered under a `playlist` trigger (picker when >1). */
14
+ playlistSlugs?: string[];
13
15
  calendar?: CalendarProvider;
14
16
  /** Sync subscription source id; pairs with `externalEventId` for server-side resolution. */
15
17
  apiSourceId?: string;
16
- /** External event identifier resolved against `apiSourceId`. */
18
+ /** External event identifier resolved against `apiSourceId` (exact showing). */
17
19
  externalEventId?: string;
20
+ /** Upstream event id — Live resolves all showings and prompts user to pick. */
21
+ externalEventIdPrefix?: string;
18
22
  label?: string;
19
23
  className?: string;
20
24
  }): HTMLButtonElement;
@@ -0,0 +1,3 @@
1
+ import type { MomentSdkOpenPayload } from './types';
2
+ /** Build the Live embed sync iframe URL from an open payload. */
3
+ export declare function buildSyncIframeUrl(liveBaseUrl: string, payload: MomentSdkOpenPayload, returnOrigin?: string): string;
@@ -6,8 +6,11 @@
6
6
  * - `list` → subscribe to a curated list of moments
7
7
  * - `team` → subscribe to a team / brand schedule (all published moments)
8
8
  * - `brand-playlist` → subscribe to the brand's default playlist
9
+ * - `playlist` → subscribe to one of several specific playlists. When more than
10
+ * one slug is supplied, Live fetches each playlist and prompts the user to
11
+ * pick which one to sync.
9
12
  */
10
- export type MomentTriggerType = 'moment' | 'list' | 'team' | 'brand-playlist';
13
+ export type MomentTriggerType = 'moment' | 'list' | 'team' | 'brand-playlist' | 'playlist';
11
14
  export type CalendarProvider = 'google' | 'outlook';
12
15
  export interface MomentSdkInitOptions {
13
16
  /** CSS selector for trigger elements. Default: ".moment-sync-trigger" */
@@ -35,6 +38,10 @@ export interface MomentSdkInitOptions {
35
38
  * `externalEventId` for this trigger type.
36
39
  * - `brand-playlist` → `teamSlug` only (Live resolves the brand's
37
40
  * `defaultPlaylistId`). Omit the same fields as `team`.
41
+ * - `playlist` → `teamSlug` plus `playlistSlugs` (one or more playlist slugs).
42
+ * With a single slug Live syncs that playlist directly; with several it shows
43
+ * a picker. Omit `momentSlug`, `listSlug`, `ids`, `apiSourceId`, and
44
+ * `externalEventId` for this trigger type.
38
45
  */
39
46
  export interface MomentSdkOpenPayload {
40
47
  teamSlug: string;
@@ -43,6 +50,13 @@ export interface MomentSdkOpenPayload {
43
50
  listSlug?: string;
44
51
  triggerType: MomentTriggerType;
45
52
  ids?: string[];
53
+ /**
54
+ * Playlist slugs to offer under a `playlist` trigger. Live fetches each
55
+ * playlist's details; with more than one slug it renders a picker so the user
56
+ * can choose which playlist to sync. Forwarded to the Live embed as the
57
+ * `playlistSlugs` query param (comma-separated).
58
+ */
59
+ playlistSlugs?: string[];
46
60
  calendar?: CalendarProvider;
47
61
  /**
48
62
  * Sync subscription source identifier (the publisher's sync-subscription id).
@@ -54,9 +68,17 @@ export interface MomentSdkOpenPayload {
54
68
  /**
55
69
  * External event identifier emitted by the publisher's source feed (e.g. the
56
70
  * provider's native event id). Resolved against `apiSourceId` to a moment.
71
+ * Use the composite `event_id:showing_id` format for a single showing.
57
72
  * Forwarded to the Live embed as the `externalId` query param.
73
+ * Mutually exclusive with `externalEventIdPrefix`.
58
74
  */
59
75
  externalEventId?: string;
76
+ /**
77
+ * Upstream event id without showing suffix — Live fetches all showings for
78
+ * that event and prompts the user to pick one. Forwarded as `externalIdPrefix`.
79
+ * Mutually exclusive with `externalEventId`.
80
+ */
81
+ externalEventIdPrefix?: string;
60
82
  }
61
83
  /** Backend subscription resource type that the embed will create */
62
84
  export type MomentSubscriptionType = 'moment' | 'list' | 'team' | 'playlist';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@momentco-ai/moment-sdk",
3
- "version": "0.3.0",
3
+ "version": "0.3.2-dev.21",
4
4
  "type": "module",
5
5
  "description": "Embeddable calendar sync widget for external team websites",
6
6
  "author": "Moment Co. <support@momentco.ai> (https://momentco.ai)",
@@ -62,6 +62,7 @@
62
62
  "format": "prettier --write .",
63
63
  "format:check": "prettier --check .",
64
64
  "quality": "pnpm run type-check && pnpm run lint && pnpm run format",
65
+ "test": "vitest run",
65
66
  "examples": "pnpm build && node examples/serve.mjs",
66
67
  "prepare": "husky"
67
68
  },
@@ -72,6 +73,7 @@
72
73
  "prettier": "^3.2.5",
73
74
  "typescript": "^5.9.2",
74
75
  "typescript-eslint": "^8.32.1",
75
- "vite": "^7.1.5"
76
+ "vite": "^7.1.5",
77
+ "vitest": "^3.2.4"
76
78
  }
77
79
  }