@mylikita/booking-widget 0.1.0 → 0.1.2

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/README.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # @mylikita/booking-widget
2
2
 
3
+ **Live on npm:** [`@mylikita/booking-widget`](https://www.npmjs.com/package/@mylikita/booking-widget) ·
4
+ [unpkg CDN](https://unpkg.com/@mylikita/booking-widget) ·
5
+ [React wrapper `@mylikita/booking-widget-react`](https://www.npmjs.com/package/@mylikita/booking-widget-react)
6
+
3
7
  Drop-in appointment booking for hospital websites built on MyLikita. A
4
8
  zero-dependency vanilla-JS widget (no framework required) that:
5
9
 
@@ -8,8 +12,9 @@ zero-dependency vanilla-JS widget (no framework required) that:
8
12
  3. polls the booking (`GET /v1/bookings/:ref`) until the hospital confirms,
9
13
  cancels, reschedules, marks no-show, or the request expires.
10
14
 
11
- It implements the [MyLikita Website Booking API (v1)](/backend/WEBSITE_BOOKING_API.md)
12
- exactly — same auth, same fields, same idempotency rules, same statuses.
15
+ It implements the **MyLikita Website Booking API (v1)** exactly — same auth,
16
+ same fields, same idempotency rules, same statuses — so the hospital side
17
+ needs no custom integration work.
13
18
 
14
19
  ---
15
20
 
@@ -22,7 +27,7 @@ exactly — same auth, same fields, same idempotency rules, same statuses.
22
27
  <div id="booking"></div>
23
28
  <script>
24
29
  MyLikitaBookingWidget.createBookingWidget(document.getElementById('booking'), {
25
- relayUrl: 'https://api.mylikita.clinic',
30
+ relayUrl: 'https://api.mylikita.com',
26
31
  websiteKey: 'wk_9f2k…', // public client id — not a secret
27
32
  facilityId: 'F1',
28
33
  });
@@ -40,7 +45,7 @@ import { createBookingWidget } from '@mylikita/booking-widget';
40
45
  createBookingWidget(document.getElementById('booking'), { relayUrl, websiteKey, facilityId });
41
46
  ```
42
47
 
43
- **React sites:** use [`@mylikita/booking-widget-react`](/packages/booking-widget-react) — the
48
+ **React sites:** use [`@mylikita/booking-widget-react`](https://www.npmjs.com/package/@mylikita/booking-widget-react) — the
44
49
  same widget as a `<BookingWidget relayUrl=… websiteKey=… facilityId=… />`
45
50
  component (all options are props, SSR-safe, imperative ref).
46
51
 
@@ -48,15 +53,16 @@ component (all options are props, SSR-safe, imperative ref).
48
53
 
49
54
  | Option | Type | Default | Description |
50
55
  |---|---|---|---|
51
- | `relayUrl` | string | — | **required** — relay base URL, e.g. `https://api.mylikita.clinic` |
56
+ | `relayUrl` | string | — | **required** — relay base URL, e.g. `https://api.mylikita.com` |
52
57
  | `websiteKey` | string | — | **required** — your public client id (Bearer on every request) |
53
58
  | `facilityId` | string | — | **required** — the hospital's public facility id |
54
59
  | `providers` | array | `[]` | `[{ external_id, label }]` — shown as a "Preferred doctor" select; unmapped slugs simply arrive unassigned (never block on them). When provided, it wins and no fetch happens |
55
- | `loadProviders` | bool | `false` | **Phase C2/C3** — fetch the facility's mapped providers from the relay (`GET /v1/providers`) on mount and populate the doctor dropdown automatically. Ignored when `providers` is non-empty. A fetch failure is non-fatal: the widget keeps "No preference" and still works |
60
+ | `loadProviders` | bool | `false` | fetch the facility's mapped providers from the relay (`GET /v1/providers`) on mount and populate the doctor dropdown automatically. Ignored when `providers` is non-empty. A fetch failure is non-fatal: the widget keeps "No preference" and still works |
56
61
  | `services` | array | `[]` | `['General consultation', …]` — shown as a service select (free-text input otherwise) |
57
62
  | `durationMins` | number | `30` | sent with the booking |
58
63
  | `pollIntervalMs` | number | `5000` | how often to poll the booking status |
59
64
  | `maxTries` | number | `12` | poll budget (~1 min at the default); the widget then shows "request received" |
65
+ | `showBrand` | bool | `true` | set `false` to white-label the widget — hides the MyLikita mark + wordmark header (for premium-tier hosts). Everything else (title, form, status view) is untouched |
60
66
  | `theme` | object | defaults | see [Theming](#theming) |
61
67
  | `text` | object | defaults | i18n overrides for every label/message (see `src/widget.js` `DEFAULT_TEXT`) |
62
68
  | `externalRef` | fn | auto | override the idempotency-key generator |
@@ -140,25 +146,11 @@ createBookingWidget(el, {
140
146
  });
141
147
  ```
142
148
 
143
- ## Demo
144
-
145
- `demo/demo.html` runs the widget **fully offline** against an in-page mock of
146
- the relay — submit a booking and watch it flip to *confirmed* after two polls.
147
- Open it in a browser directly:
148
-
149
- ```bash
150
- open packages/booking-widget/demo/demo.html
151
- ```
152
-
153
- ## Development
154
-
155
- ```bash
156
- npm run build # esbuild → dist/ (iife, iife.min, esm, cjs)
157
- npm test # mocked-fetch suite for the core logic
158
- ```
149
+ ## Support
159
150
 
160
- `dist/` is committed so the package works before any install; rebuild after
161
- touching `src/`.
151
+ - **Issues:** report through your MyLikita account manager or the MyLikita
152
+ support channel you were onboarded with.
153
+ - **The React wrapper:** [`@mylikita/booking-widget-react`](https://www.npmjs.com/package/@mylikita/booking-widget-react).
162
154
 
163
155
  ## Security notes
164
156
 
package/dist/demo.html CHANGED
@@ -69,8 +69,8 @@
69
69
  </div>
70
70
 
71
71
  <script>
72
- /*! @mylikita/booking-widget v0.1.0 | MIT */
73
- var MyLikitaBookingWidget=(()=>{var Y=Object.defineProperty;var pe=Object.getOwnPropertyDescriptor;var fe=Object.getOwnPropertyNames;var ge=Object.prototype.hasOwnProperty;var we=(i,e)=>{for(var t in e)Y(i,t,{get:e[t],enumerable:!0})},ye=(i,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let a of fe(e))!ge.call(i,a)&&a!==t&&Y(i,a,{get:()=>e[a],enumerable:!(r=pe(e,a))||r.enumerable});return i};var _e=i=>ye(Y({},"__esModule",{value:!0}),i);var Ee={};we(Ee,{DEFAULT_THEME:()=>W,STATUS_COPY:()=>C,TERMINAL_STATUSES:()=>ie,createBooking:()=>S,createBookingWidget:()=>oe,fetchProviders:()=>E,fetchStatus:()=>T,newExternalRef:()=>P,pollStatus:()=>N,resolveTheme:()=>$,statusCopy:()=>A});async function S({relayUrl:i,websiteKey:e,payload:t,signal:r}){let a=await fetch(`${H(i)}/v1/bookings`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${e}`},body:JSON.stringify(t),signal:r}),o=await F(a);if(a.status===409)return{ok:!0,duplicate:!0,booking_ref:o?.booking_ref,error:o?.message};if(!a.ok)throw O(a.status,o,"create");return{ok:!0,duplicate:!1,booking_ref:o?.booking_ref,status:o?.status||"pending_confirmation"}}async function E({relayUrl:i,websiteKey:e,signal:t}){let r=await fetch(`${H(i)}/v1/providers`,{headers:{Authorization:`Bearer ${e}`},signal:t}),a=await F(r);if(!r.ok)throw O(r.status,a,"providers");return(Array.isArray(a?.providers)?a.providers:[]).map(d=>({external_id:d.external_id,name:d.name||d.external_id,specialty:d.specialty||null,module:d.module||"general"}))}async function T({relayUrl:i,websiteKey:e,bookingRef:t,signal:r}){let a=await fetch(`${H(i)}/v1/bookings/${encodeURIComponent(t)}`,{headers:{Authorization:`Bearer ${e}`},signal:r}),o=await F(a);if(!a.ok)throw O(a.status,o,"status");return{booking_ref:o?.booking_ref,status:o?.status||"pending_confirmation",appt_ref:o?.appt_ref||null}}async function N(i,{intervalMs:e=5e3,maxTries:t=12,signal:r}={}){for(let a=0;a<t;a++){if(r?.aborted)return{status:"aborted",resolved:!1,data:null};let o=await i();if(o.status!=="pending_confirmation")return{status:o.status,resolved:!0,data:o};a<t-1&&await be(e,r)}return{status:"pending_confirmation",resolved:!1,data:null}}function P(){return`BK-${Date.now()}-${Math.random().toString(36).slice(2,6)}`}function H(i){return String(i||"").replace(/\/+$/,"")}function O(i,e,t){let r=new Error(e?.message||e?.error||`Relay ${t} failed (HTTP ${i})`);return r.code=e?.error||"http_error",r.status=i,r}async function F(i){try{return await i.json()}catch{return null}}function be(i,e){return new Promise(t=>{if(e?.aborted)return t();let r=()=>{clearTimeout(a),t()},a=setTimeout(()=>{e?.removeEventListener("abort",r),t()},i);e?.addEventListener("abort",r,{once:!0})})}var C={pending_confirmation:{title:"Request received",message:"We've received your booking request \u2014 we'll confirm shortly.",kind:"info"},confirmed:{title:"Appointment confirmed",message:"Your appointment is confirmed. See you at the clinic!",kind:"success"},cancelled:{title:"Appointment cancelled",message:"This appointment was cancelled. Please contact the clinic if this was unexpected.",kind:"danger"},rescheduled:{title:"Appointment rescheduled",message:"This appointment was moved \u2014 the new time was sent to you.",kind:"info"},no_show:{title:"Missed appointment",message:"This appointment was marked as missed.",kind:"danger"},expired:{title:"Request expired",message:"This booking request expired \u2014 please call the clinic to book.",kind:"danger"},poll_error:{title:"Something went wrong",message:"We could not check your booking right now. Please try again shortly.",kind:"danger"}};function A(i){return C[i]||C.pending_confirmation}var ie=["confirmed","cancelled","rescheduled","no_show","expired"];var W={primary:"#0d6efd",primaryDark:"#0b5ed7",primaryText:"#ffffff",bg:"#ffffff",text:"#1e293b",muted:"#64748b",border:"#e2e8f0",danger:"#dc3545",success:"#15803d",radius:10,font:"system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif"};function $(i={}){let e={...W,...i||{}};return{"--mlw-primary":e.primary,"--mlw-primary-dark":e.primaryDark,"--mlw-primary-text":e.primaryText,"--mlw-bg":e.bg,"--mlw-text":e.text,"--mlw-muted":e.muted,"--mlw-border":e.border,"--mlw-danger":e.danger,"--mlw-success":e.success,"--mlw-radius":`${e.radius}px`,"--mlw-font":e.font}}var re=`
72
+ /*! @mylikita/booking-widget v0.1.1 | MIT */
73
+ var MyLikitaBookingWidget=(()=>{var Y=Object.defineProperty;var we=Object.getOwnPropertyDescriptor;var ye=Object.getOwnPropertyNames;var xe=Object.prototype.hasOwnProperty;var be=(i,e)=>{for(var t in e)Y(i,t,{get:e[t],enumerable:!0})},_e=(i,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let a of ye(e))!xe.call(i,a)&&a!==t&&Y(i,a,{get:()=>e[a],enumerable:!(r=we(e,a))||r.enumerable});return i};var ve=i=>_e(Y({},"__esModule",{value:!0}),i);var Ae={};be(Ae,{DEFAULT_THEME:()=>O,MYLIKITA_MARK_DATA_URI:()=>G,STATUS_COPY:()=>N,TERMINAL_STATUSES:()=>ae,createBooking:()=>I,createBookingWidget:()=>le,createBrandHeader:()=>M,fetchProviders:()=>S,fetchStatus:()=>E,newExternalRef:()=>A,pollStatus:()=>T,resolveTheme:()=>P,statusCopy:()=>C});async function I({relayUrl:i,websiteKey:e,payload:t,signal:r}){let a=await fetch(`${q(i)}/v1/bookings`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${e}`},body:JSON.stringify(t),signal:r}),o=await Z(a);if(a.status===409)return{ok:!0,duplicate:!0,booking_ref:o?.booking_ref,error:o?.message};if(!a.ok)throw K(a.status,o,"create");return{ok:!0,duplicate:!1,booking_ref:o?.booking_ref,status:o?.status||"pending_confirmation"}}async function S({relayUrl:i,websiteKey:e,signal:t}){let r=await fetch(`${q(i)}/v1/providers`,{headers:{Authorization:`Bearer ${e}`},signal:t}),a=await Z(r);if(!r.ok)throw K(r.status,a,"providers");return(Array.isArray(a?.providers)?a.providers:[]).map(d=>({external_id:d.external_id,name:d.name||d.external_id,specialty:d.specialty||null,module:d.module||"general"}))}async function E({relayUrl:i,websiteKey:e,bookingRef:t,signal:r}){let a=await fetch(`${q(i)}/v1/bookings/${encodeURIComponent(t)}`,{headers:{Authorization:`Bearer ${e}`},signal:r}),o=await Z(a);if(!a.ok)throw K(a.status,o,"status");return{booking_ref:o?.booking_ref,status:o?.status||"pending_confirmation",appt_ref:o?.appt_ref||null}}async function T(i,{intervalMs:e=5e3,maxTries:t=12,signal:r}={}){for(let a=0;a<t;a++){if(r?.aborted)return{status:"aborted",resolved:!1,data:null};let o=await i();if(o.status!=="pending_confirmation")return{status:o.status,resolved:!0,data:o};a<t-1&&await he(e,r)}return{status:"pending_confirmation",resolved:!1,data:null}}function A(){return`BK-${Date.now()}-${Math.random().toString(36).slice(2,6)}`}function q(i){return String(i||"").replace(/\/+$/,"")}function K(i,e,t){let r=new Error(e?.message||e?.error||`Relay ${t} failed (HTTP ${i})`);return r.code=e?.error||"http_error",r.status=i,r}async function Z(i){try{return await i.json()}catch{return null}}function he(i,e){return new Promise(t=>{if(e?.aborted)return t();let r=()=>{clearTimeout(a),t()},a=setTimeout(()=>{e?.removeEventListener("abort",r),t()},i);e?.addEventListener("abort",r,{once:!0})})}var N={pending_confirmation:{title:"Request received",message:"We've received your booking request \u2014 we'll confirm shortly.",kind:"info"},confirmed:{title:"Appointment confirmed",message:"Your appointment is confirmed. See you at the clinic!",kind:"success"},cancelled:{title:"Appointment cancelled",message:"This appointment was cancelled. Please contact the clinic if this was unexpected.",kind:"danger"},rescheduled:{title:"Appointment rescheduled",message:"This appointment was moved \u2014 the new time was sent to you.",kind:"info"},no_show:{title:"Missed appointment",message:"This appointment was marked as missed.",kind:"danger"},expired:{title:"Request expired",message:"This booking request expired \u2014 please call the clinic to book.",kind:"danger"},poll_error:{title:"Something went wrong",message:"We could not check your booking right now. Please try again shortly.",kind:"danger"}};function C(i){return N[i]||N.pending_confirmation}var ae=["confirmed","cancelled","rescheduled","no_show","expired"];var O={primary:"#0d6efd",primaryDark:"#0b5ed7",primaryText:"#ffffff",bg:"#ffffff",text:"#1e293b",muted:"#64748b",border:"#e2e8f0",danger:"#dc3545",success:"#15803d",radius:10,font:"system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif"};function P(i={}){let e={...O,...i||{}};return{"--mlw-primary":e.primary,"--mlw-primary-dark":e.primaryDark,"--mlw-primary-text":e.primaryText,"--mlw-bg":e.bg,"--mlw-text":e.text,"--mlw-muted":e.muted,"--mlw-border":e.border,"--mlw-danger":e.danger,"--mlw-success":e.success,"--mlw-radius":`${e.radius}px`,"--mlw-font":e.font}}var ne=`
74
74
  .mylikita-widget {
75
75
  --mlw-primary: #0d6efd;
76
76
  --mlw-primary-dark: #0b5ed7;
@@ -97,6 +97,27 @@ var MyLikitaBookingWidget=(()=>{var Y=Object.defineProperty;var pe=Object.getOwn
97
97
  .mylikita-widget *::before,
98
98
  .mylikita-widget *::after { box-sizing: border-box; }
99
99
 
100
+ .mylikita-widget__brand {
101
+ display: flex;
102
+ align-items: center;
103
+ gap: 8px;
104
+ margin-bottom: 14px;
105
+ padding-bottom: 12px;
106
+ border-bottom: 1px solid var(--mlw-border);
107
+ }
108
+ .mylikita-widget__brand-mark {
109
+ display: block;
110
+ flex: 0 0 auto;
111
+ border-radius: 5px;
112
+ box-shadow: 0 1px 3px rgba(15, 23, 42, .12);
113
+ }
114
+ .mylikita-widget__brand-word {
115
+ font-size: 14px;
116
+ font-weight: 700;
117
+ letter-spacing: .3px;
118
+ color: var(--mlw-muted);
119
+ }
120
+
100
121
  .mylikita-widget__title {
101
122
  font-size: 18px;
102
123
  font-weight: 700;
@@ -210,7 +231,7 @@ var MyLikitaBookingWidget=(()=>{var Y=Object.defineProperty;var pe=Object.getOwn
210
231
  cursor: pointer;
211
232
  }
212
233
  .mylikita-widget__link-btn:hover { border-color: var(--mlw-primary); color: var(--mlw-primary); }
213
- `;var xe={title:"Book an appointment",subtitle:"Request a slot and we will confirm shortly.",name:"Full name",phone:"Phone number",email:"Email address",provider:"Preferred doctor (optional)",noPreference:"No preference",service:"Service (optional)",datetime:"Preferred date & time",visitType:"Appointment type",visitPhysical:"In person",visitTelemedicine:"Video call",visitHome:"Home visit",notes:"Notes (optional)",submit:"Request appointment",submitting:"Submitting\u2026",bookAnother:"Book another appointment",requiredPhoneOrEmail:"Please provide a phone number or an email address.",requiredName:"Please enter your name.",requiredDatetime:"Please choose a date and time.",networkError:"Could not reach the booking service. Please try again.",rateLimited:"Too many requests \u2014 please wait a moment and try again."},ae="mylikita-widget-styles";function oe(i,e={}){if(!i)throw new Error("createBookingWidget: a container element is required");let t={relayUrl:e.relayUrl,websiteKey:e.websiteKey,facilityId:e.facilityId,providers:e.providers||[],services:e.services||[],loadProviders:e.loadProviders===!0&&!(e.providers&&e.providers.length),pollIntervalMs:e.pollIntervalMs??5e3,maxTries:e.maxTries??12,text:{...xe,...e.text||{}},theme:e.theme||{},onStatus:typeof e.onStatus=="function"?e.onStatus:null,onError:typeof e.onError=="function"?e.onError:null,onBooking:typeof e.onBooking=="function"?e.onBooking:null,externalRef:typeof e.externalRef=="function"?e.externalRef:P};if(!t.relayUrl||!t.websiteKey||!t.facilityId)throw new Error("createBookingWidget: relayUrl, websiteKey and facilityId are required");ke();let r=t.text,a=i;a.classList.add("mylikita-widget"),Se(a,t.theme);let o=!0,d=!1,u=null,f=Math.random().toString(36).slice(2,8),I=`mylikita_ref_${t.facilityId}_${f}`,y=l("form",{className:"mylikita-widget__form"}),B=l("h3",{className:"mylikita-widget__title",text:r.title}),U=l("p",{className:"mylikita-widget__subtitle",text:r.subtitle}),L=l("div",{className:"mylikita-widget__error",attrs:{role:"alert"}}),q=_("name",r.name,{required:!0}),V=l("div",{className:"mylikita-widget__row"}),z=_("phone",r.phone,{type:"tel",inputmode:"tel"}),R=_("email",r.email,{type:"email"});V.append(z.wrap,R.wrap);let g=j("provider",r.provider,[{value:"",label:r.noPreference},...t.providers.map(n=>({value:n.external_id,label:n.label||n.name||n.external_id}))]);function se(n){let s=g.input.value;g.input.replaceChildren();let c=document.createElement("option");c.value="",c.textContent=r.noPreference,g.input.append(c);for(let m of n||[]){let p=document.createElement("option");p.value=m.external_id,p.textContent=m.label||m.name||m.external_id,g.input.append(p)}s&&(g.input.value=s)}let J=t.services.length?j("service",r.service,[{value:"",label:"\u2014"},...t.services.map(n=>({value:n,label:n}))]):_("service",r.service),v=_("datetime",r.datetime,{type:"datetime-local",required:!0});v.input.min=ne(new Date);let G=j("visitType",r.visitType,[{value:"physical",label:r.visitPhysical},{value:"telemedicine",label:r.visitTelemedicine},{value:"home_visit",label:r.visitHome}]),X=_("notes",r.notes,{type:"textarea",maxlength:500}),w=l("button",{className:"mylikita-widget__submit",type:"submit",text:r.submit}),Q=l("div");Q.append(w);let M=l("p",{className:"mylikita-widget__hint"});y.append(L,q.wrap,V,g.wrap,J.wrap,v.wrap,G.wrap,X.wrap,Q,M);let b=l("div",{className:"mylikita-widget__status",attrs:{"aria-live":"polite"},hidden:!0});y.addEventListener("submit",n=>{n.preventDefault(),!d&&le()});async function le(){d=!0,D(null),w.disabled=!0,w.textContent=r.submitting;let n={facility_id:t.facilityId,patient_name:q.input.value.trim(),patient_phone:z.input.value.trim(),patient_email:R.input.value.trim(),provider_external_id:g.input.value||void 0,service_name:J.input.value.trim()||void 0,appt_datetime:v.input.value,visit_type:G.input.value||"physical",duration_mins:t.durationMins||void 0,notes:X.input.value.trim()||void 0};if(!n.patient_name)return k(r.requiredName);if(!n.patient_phone&&!n.patient_email)return k(r.requiredPhoneOrEmail);if(!n.appt_datetime||Number.isNaN(Date.parse(n.appt_datetime)))return k(r.requiredDatetime);let s=he(I);if(!s){s=t.externalRef();try{sessionStorage.setItem(I,s)}catch{}}n.external_ref=s,u=new AbortController;let c=null;try{let m=await S({relayUrl:t.relayUrl,websiteKey:t.websiteKey,payload:n,signal:u.signal});c=m.booking_ref,m.duplicate?(M.textContent="",Z("pending_confirmation",c,"We found an existing booking request for this slot \u2014 checking it\u2026")):(t.onBooking&&x(t.onBooking,m,n),Z("pending_confirmation",c,null)),de(c)}catch(m){if(!o||m?.name==="AbortError")return;let p=m.status===429?r.rateLimited:m.message||r.networkError;t.onError&&x(t.onError,m),k(p)}}async function de(n){let s;try{s=await N(()=>T({relayUrl:t.relayUrl,websiteKey:t.websiteKey,bookingRef:n,signal:u.signal}),{intervalMs:t.pollIntervalMs,maxTries:t.maxTries,signal:u.signal})}catch(c){if(!o||c?.name==="AbortError")return;t.onError&&x(t.onError,c),h("poll_error",n,c.message||r.networkError),d=!1;return}if(!(!o||s.status==="aborted")){if(t.onStatus&&s.data&&x(t.onStatus,s.data),s.resolved){try{sessionStorage.removeItem(I)}catch{}h(s.status,n)}else h("pending_confirmation",n);d=!1}}function Z(n,s,c){y.hidden=!0,B.hidden=!0,U.hidden=!0,b.hidden=!1,h(n,s,c)}function h(n,s,c){let m=A(n),p=l("div",{className:`mylikita-widget__status-icon ${m.kind}`});p.textContent=ve(m.kind);let me=l("p",{className:"mylikita-widget__status-title",text:m.title}),ce=l("p",{className:"mylikita-widget__status-message",text:c||m.message}),ue=l("p",{className:"mylikita-widget__status-ref",text:s?`Booking ref: ${s}`:""}),te=l("button",{className:"mylikita-widget__link-btn",type:"button",text:r.bookAnother});te.addEventListener("click",()=>ee()),b.replaceChildren(p,me,ce,ue,te)}function k(n){d=!1,w.disabled=!1,w.textContent=r.submit,D(n)}function D(n){L.textContent=n||"",L.classList.toggle("visible",!!n)}function ee(){try{sessionStorage.removeItem(`mylikita_ref_${t.facilityId}`)}catch{}y.reset(),D(null),b.replaceChildren(),b.hidden=!0,y.hidden=!1,B.hidden=!1,U.hidden=!1,M.textContent="",d=!1,w.disabled=!1,w.textContent=r.submit,v.input.min=ne(new Date)}a.replaceChildren(B,U,y,b);let K=null;if(t.loadProviders){let n=new AbortController;(async()=>{try{let s=await E({relayUrl:t.relayUrl,websiteKey:t.websiteKey,signal:n.signal});o&&!n.signal.aborted&&se(s)}catch(s){if(!o||s?.name==="AbortError")return;t.onError&&x(t.onError,s)}})(),K=()=>n.abort()}return{destroy(){o=!1,u&&u.abort(),K&&K(),a.replaceChildren(),a.classList.remove("mylikita-widget")},reset:ee,getForm(){return{name:q.input.value,phone:z.input.value,email:R.input.value}}}}function _(i,e,{type:t="text",required:r=!1,maxlength:a,inputmode:o}={}){let d=l("div",{className:"mylikita-widget__field"}),u=l("label",{className:"mylikita-widget__label",attrs:{for:`mlw-${i}`}});u.append(document.createTextNode(e)),r&&u.append(l("span",{className:"req",text:" *"}));let f;return t==="textarea"?f=l("textarea",{className:"mylikita-widget__textarea",attrs:{id:`mlw-${i}`,rows:3,maxlength:a||""}}):f=l("input",{className:"mylikita-widget__input",attrs:{id:`mlw-${i}`,type:t,inputmode:o||""}}),r&&f.setAttribute("required",""),d.append(u,f),{wrap:d,input:f}}function j(i,e,t){let r=l("div",{className:"mylikita-widget__field"}),a=l("label",{className:"mylikita-widget__label",attrs:{for:`mlw-${i}`}});a.textContent=e;let o=l("select",{className:"mylikita-widget__select",attrs:{id:`mlw-${i}`}});for(let d of t){let u=l("option",{text:d.label});u.value=d.value,o.append(u)}return r.append(a,o),{wrap:r,input:o}}function l(i,{className:e,text:t,attrs:r={}}={}){let a=document.createElement(i);e&&(a.className=e),t!==void 0&&(a.textContent=t);for(let[o,d]of Object.entries(r))d===void 0||d===""||a.setAttribute(o,d);return a}function ve(i){return i==="success"?"\u2713":i==="danger"?"!":"\u2026"}function he(i){try{return sessionStorage.getItem(i)||null}catch{return null}}function x(i,...e){try{i(...e)}catch{}}function ne(i){let e=t=>String(t).padStart(2,"0");return`${i.getFullYear()}-${e(i.getMonth()+1)}-${e(i.getDate())}T${e(i.getHours())}:${e(i.getMinutes())}`}function ke(){if(document.getElementById(ae))return;let i=document.createElement("style");i.id=ae,i.textContent=re,document.head.appendChild(i)}function Se(i,e){for(let[t,r]of Object.entries($(e)))i.style.setProperty(t,r)}return _e(Ee);})();
234
+ `;var G="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0ODMgODA0IiB3aWR0aD0iNDgzIiBoZWlnaHQ9IjgwNCIgcm9sZT0iaW1nIiBhcmlhLWxhYmVsPSJNeUxpa2l0YSI+CiAgICA8cmVjdCB4PSIxNjEiIHk9IjAiIHdpZHRoPSIxNjEiIGhlaWdodD0iMTYxIiBmaWxsPSIjMDE2OURBIi8+CiAgICA8cmVjdCB4PSIwIiB5PSIxNjAiIHdpZHRoPSIxNjIiIGhlaWdodD0iMTYxIiBmaWxsPSIjMDE2OURBIi8+CiAgICA8cmVjdCB4PSIzMjEiIHk9IjE2MCIgd2lkdGg9IjE2MiIgaGVpZ2h0PSIxNjEiIGZpbGw9IiMwMTY5REEiLz4KICAgIDxyZWN0IHg9IjAiIHk9IjQ4MiIgd2lkdGg9IjE2MiIgaGVpZ2h0PSIxNjIiIGZpbGw9IiMwNDk4RkIiLz4KICAgIDxyZWN0IHg9IjMyMSIgeT0iNDgyIiB3aWR0aD0iMTYyIiBoZWlnaHQ9IjE2MiIgZmlsbD0iIzA0OThGQiIvPgogICAgPHJlY3QgeD0iMTYxIiB5PSI2NDMiIHdpZHRoPSIxNjEiIGhlaWdodD0iMTYxIiBmaWxsPSIjMDQ5OEZCIi8+Cjwvc3ZnPg==";function M(){let i=document.createElement("div");i.className="mylikita-widget__brand";let e=document.createElement("img");e.className="mylikita-widget__brand-mark",e.src=G,e.alt="MyLikita",e.width=24,e.height=24;let t=document.createElement("span");return t.className="mylikita-widget__brand-word",t.textContent="MyLikita",i.append(e,t),i}var ke={title:"Book an appointment",subtitle:"Request a slot and we will confirm shortly.",name:"Full name",phone:"Phone number",email:"Email address",provider:"Preferred doctor (optional)",noPreference:"No preference",service:"Service (optional)",datetime:"Preferred date & time",visitType:"Appointment type",visitPhysical:"In person",visitTelemedicine:"Video call",visitHome:"Home visit",notes:"Notes (optional)",submit:"Request appointment",submitting:"Submitting\u2026",bookAnother:"Book another appointment",requiredPhoneOrEmail:"Please provide a phone number or an email address.",requiredName:"Please enter your name.",requiredDatetime:"Please choose a date and time.",networkError:"Could not reach the booking service. Please try again.",rateLimited:"Too many requests \u2014 please wait a moment and try again."},oe="mylikita-widget-styles";function le(i,e={}){if(!i)throw new Error("createBookingWidget: a container element is required");let t={relayUrl:e.relayUrl,websiteKey:e.websiteKey,facilityId:e.facilityId,providers:e.providers||[],services:e.services||[],loadProviders:e.loadProviders===!0&&!(e.providers&&e.providers.length),pollIntervalMs:e.pollIntervalMs??5e3,maxTries:e.maxTries??12,text:{...ke,...e.text||{}},theme:e.theme||{},onStatus:typeof e.onStatus=="function"?e.onStatus:null,onError:typeof e.onError=="function"?e.onError:null,onBooking:typeof e.onBooking=="function"?e.onBooking:null,externalRef:typeof e.externalRef=="function"?e.externalRef:A};if(!t.relayUrl||!t.websiteKey||!t.facilityId)throw new Error("createBookingWidget: relayUrl, websiteKey and facilityId are required");Ee();let r=t.text,a=i;a.classList.add("mylikita-widget"),Te(a,t.theme);let o=!0,d=!1,u=null,f=Math.random().toString(36).slice(2,8),B=`mylikita_ref_${t.facilityId}_${f}`,de=M(),y=l("form",{className:"mylikita-widget__form"}),D=l("h3",{className:"mylikita-widget__title",text:r.title}),j=l("p",{className:"mylikita-widget__subtitle",text:r.subtitle}),R=l("div",{className:"mylikita-widget__error",attrs:{role:"alert"}}),H=x("name",r.name,{required:!0}),F=l("div",{className:"mylikita-widget__row"}),L=x("phone",r.phone,{type:"tel",inputmode:"tel"}),$=x("email",r.email,{type:"email"});F.append(L.wrap,$.wrap);let g=V("provider",r.provider,[{value:"",label:r.noPreference},...t.providers.map(n=>({value:n.external_id,label:n.label||n.name||n.external_id}))]);function me(n){let s=g.input.value;g.input.replaceChildren();let c=document.createElement("option");c.value="",c.textContent=r.noPreference,g.input.append(c);for(let m of n||[]){let p=document.createElement("option");p.value=m.external_id,p.textContent=m.label||m.name||m.external_id,g.input.append(p)}s&&(g.input.value=s)}let Q=t.services.length?V("service",r.service,[{value:"",label:"\u2014"},...t.services.map(n=>({value:n,label:n}))]):x("service",r.service),v=x("datetime",r.datetime,{type:"datetime-local",required:!0});v.input.min=se(new Date);let J=V("visitType",r.visitType,[{value:"physical",label:r.visitPhysical},{value:"telemedicine",label:r.visitTelemedicine},{value:"home_visit",label:r.visitHome}]),X=x("notes",r.notes,{type:"textarea",maxlength:500}),w=l("button",{className:"mylikita-widget__submit",type:"submit",text:r.submit}),ee=l("div");ee.append(w);let U=l("p",{className:"mylikita-widget__hint"});y.append(R,H.wrap,F,g.wrap,Q.wrap,v.wrap,J.wrap,X.wrap,ee,U);let b=l("div",{className:"mylikita-widget__status",attrs:{"aria-live":"polite"},hidden:!0});y.addEventListener("submit",n=>{n.preventDefault(),!d&&ce()});async function ce(){d=!0,z(null),w.disabled=!0,w.textContent=r.submitting;let n={facility_id:t.facilityId,patient_name:H.input.value.trim(),patient_phone:L.input.value.trim(),patient_email:$.input.value.trim(),provider_external_id:g.input.value||void 0,service_name:Q.input.value.trim()||void 0,appt_datetime:v.input.value,visit_type:J.input.value||"physical",duration_mins:t.durationMins||void 0,notes:X.input.value.trim()||void 0};if(!n.patient_name)return k(r.requiredName);if(!n.patient_phone&&!n.patient_email)return k(r.requiredPhoneOrEmail);if(!n.appt_datetime||Number.isNaN(Date.parse(n.appt_datetime)))return k(r.requiredDatetime);let s=Se(B);if(!s){s=t.externalRef();try{sessionStorage.setItem(B,s)}catch{}}n.external_ref=s,u=new AbortController;let c=null;try{let m=await I({relayUrl:t.relayUrl,websiteKey:t.websiteKey,payload:n,signal:u.signal});c=m.booking_ref,m.duplicate?(U.textContent="",te("pending_confirmation",c,"We found an existing booking request for this slot \u2014 checking it\u2026")):(t.onBooking&&_(t.onBooking,m,n),te("pending_confirmation",c,null)),ue(c)}catch(m){if(!o||m?.name==="AbortError")return;let p=m.status===429?r.rateLimited:m.message||r.networkError;t.onError&&_(t.onError,m),k(p)}}async function ue(n){let s;try{s=await T(()=>E({relayUrl:t.relayUrl,websiteKey:t.websiteKey,bookingRef:n,signal:u.signal}),{intervalMs:t.pollIntervalMs,maxTries:t.maxTries,signal:u.signal})}catch(c){if(!o||c?.name==="AbortError")return;t.onError&&_(t.onError,c),h("poll_error",n,c.message||r.networkError),d=!1;return}if(!(!o||s.status==="aborted")){if(t.onStatus&&s.data&&_(t.onStatus,s.data),s.resolved){try{sessionStorage.removeItem(B)}catch{}h(s.status,n)}else h("pending_confirmation",n);d=!1}}function te(n,s,c){y.hidden=!0,D.hidden=!0,j.hidden=!0,b.hidden=!1,h(n,s,c)}function h(n,s,c){let m=C(n),p=l("div",{className:`mylikita-widget__status-icon ${m.kind}`});p.textContent=Ie(m.kind);let pe=l("p",{className:"mylikita-widget__status-title",text:m.title}),fe=l("p",{className:"mylikita-widget__status-message",text:c||m.message}),ge=l("p",{className:"mylikita-widget__status-ref",text:s?`Booking ref: ${s}`:""}),re=l("button",{className:"mylikita-widget__link-btn",type:"button",text:r.bookAnother});re.addEventListener("click",()=>ie()),b.replaceChildren(p,pe,fe,ge,re)}function k(n){d=!1,w.disabled=!1,w.textContent=r.submit,z(n)}function z(n){R.textContent=n||"",R.classList.toggle("visible",!!n)}function ie(){try{sessionStorage.removeItem(`mylikita_ref_${t.facilityId}`)}catch{}y.reset(),z(null),b.replaceChildren(),b.hidden=!0,y.hidden=!1,D.hidden=!1,j.hidden=!1,U.textContent="",d=!1,w.disabled=!1,w.textContent=r.submit,v.input.min=se(new Date)}a.replaceChildren(de,D,j,y,b);let W=null;if(t.loadProviders){let n=new AbortController;(async()=>{try{let s=await S({relayUrl:t.relayUrl,websiteKey:t.websiteKey,signal:n.signal});o&&!n.signal.aborted&&me(s)}catch(s){if(!o||s?.name==="AbortError")return;t.onError&&_(t.onError,s)}})(),W=()=>n.abort()}return{destroy(){o=!1,u&&u.abort(),W&&W(),a.replaceChildren(),a.classList.remove("mylikita-widget")},reset:ie,getForm(){return{name:H.input.value,phone:L.input.value,email:$.input.value}}}}function x(i,e,{type:t="text",required:r=!1,maxlength:a,inputmode:o}={}){let d=l("div",{className:"mylikita-widget__field"}),u=l("label",{className:"mylikita-widget__label",attrs:{for:`mlw-${i}`}});u.append(document.createTextNode(e)),r&&u.append(l("span",{className:"req",text:" *"}));let f;return t==="textarea"?f=l("textarea",{className:"mylikita-widget__textarea",attrs:{id:`mlw-${i}`,rows:3,maxlength:a||""}}):f=l("input",{className:"mylikita-widget__input",attrs:{id:`mlw-${i}`,type:t,inputmode:o||""}}),r&&f.setAttribute("required",""),d.append(u,f),{wrap:d,input:f}}function V(i,e,t){let r=l("div",{className:"mylikita-widget__field"}),a=l("label",{className:"mylikita-widget__label",attrs:{for:`mlw-${i}`}});a.textContent=e;let o=l("select",{className:"mylikita-widget__select",attrs:{id:`mlw-${i}`}});for(let d of t){let u=l("option",{text:d.label});u.value=d.value,o.append(u)}return r.append(a,o),{wrap:r,input:o}}function l(i,{className:e,text:t,attrs:r={}}={}){let a=document.createElement(i);e&&(a.className=e),t!==void 0&&(a.textContent=t);for(let[o,d]of Object.entries(r))d===void 0||d===""||a.setAttribute(o,d);return a}function Ie(i){return i==="success"?"\u2713":i==="danger"?"!":"\u2026"}function Se(i){try{return sessionStorage.getItem(i)||null}catch{return null}}function _(i,...e){try{i(...e)}catch{}}function se(i){let e=t=>String(t).padStart(2,"0");return`${i.getFullYear()}-${e(i.getMonth()+1)}-${e(i.getDate())}T${e(i.getHours())}:${e(i.getMinutes())}`}function Ee(){if(document.getElementById(oe))return;let i=document.createElement("style");i.id=oe,i.textContent=ne,document.head.appendChild(i)}function Te(i,e){for(let[t,r]of Object.entries(P(e)))i.style.setProperty(t,r)}return ve(Ae);})();
214
235
 
215
236
  </script>
216
237
  <script>
@@ -290,7 +311,7 @@ var MyLikitaBookingWidget=(()=>{var Y=Object.defineProperty;var pe=Object.getOwn
290
311
  MyLikitaBookingWidget.createBookingWidget(document.getElementById('booking'), {
291
312
  // The in-page mock relay overrides fetch, so this URL is never actually
292
313
  // contacted — the widget still requires a non-empty relay URL.
293
- relayUrl: 'https://api.mylikita.clinic',
314
+ relayUrl: 'https://api.mylikita.com',
294
315
  websiteKey: KEY,
295
316
  facilityId: FACILITY,
296
317
  providers: [{ external_id: 'dr-khalil', label: 'Dr. Khalil' }, { external_id: 'dr-amina', label: 'Dr. Amina' }],
@@ -302,7 +323,7 @@ var MyLikitaBookingWidget=(()=>{var Y=Object.defineProperty;var pe=Object.getOwn
302
323
  });
303
324
 
304
325
  const w2 = MyLikitaBookingWidget.createBookingWidget(document.getElementById('booking2'), {
305
- relayUrl: 'https://api.mylikita.clinic',
326
+ relayUrl: 'https://api.mylikita.com',
306
327
  websiteKey: KEY,
307
328
  facilityId: FACILITY,
308
329
  // Phase C2/C3: no static providers — load them from the relay's
@@ -1,4 +1,4 @@
1
- /*! @mylikita/booking-widget v0.1.0 | MIT */
1
+ /*! @mylikita/booking-widget v0.1.2 | MIT */
2
2
  var __defProp = Object.defineProperty;
3
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
@@ -21,10 +21,12 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
23
  DEFAULT_THEME: () => DEFAULT_THEME,
24
+ MYLIKITA_MARK_DATA_URI: () => MYLIKITA_MARK_DATA_URI,
24
25
  STATUS_COPY: () => STATUS_COPY,
25
26
  TERMINAL_STATUSES: () => TERMINAL_STATUSES,
26
27
  createBooking: () => createBooking,
27
28
  createBookingWidget: () => createBookingWidget,
29
+ createBrandHeader: () => createBrandHeader,
28
30
  fetchProviders: () => fetchProviders,
29
31
  fetchStatus: () => fetchStatus,
30
32
  newExternalRef: () => newExternalRef,
@@ -226,6 +228,27 @@ var STYLES = `
226
228
  .mylikita-widget *::before,
227
229
  .mylikita-widget *::after { box-sizing: border-box; }
228
230
 
231
+ .mylikita-widget__brand {
232
+ display: flex;
233
+ align-items: center;
234
+ gap: 8px;
235
+ margin-bottom: 14px;
236
+ padding-bottom: 12px;
237
+ border-bottom: 1px solid var(--mlw-border);
238
+ }
239
+ .mylikita-widget__brand-mark {
240
+ display: block;
241
+ flex: 0 0 auto;
242
+ border-radius: 5px;
243
+ box-shadow: 0 1px 3px rgba(15, 23, 42, .12);
244
+ }
245
+ .mylikita-widget__brand-word {
246
+ font-size: 14px;
247
+ font-weight: 700;
248
+ letter-spacing: .3px;
249
+ color: var(--mlw-muted);
250
+ }
251
+
229
252
  .mylikita-widget__title {
230
253
  font-size: 18px;
231
254
  font-weight: 700;
@@ -341,6 +364,24 @@ var STYLES = `
341
364
  .mylikita-widget__link-btn:hover { border-color: var(--mlw-primary); color: var(--mlw-primary); }
342
365
  `;
343
366
 
367
+ // src/brand.js
368
+ var MYLIKITA_MARK_DATA_URI = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0ODMgODA0IiB3aWR0aD0iNDgzIiBoZWlnaHQ9IjgwNCIgcm9sZT0iaW1nIiBhcmlhLWxhYmVsPSJNeUxpa2l0YSI+CiAgICA8cmVjdCB4PSIxNjEiIHk9IjAiIHdpZHRoPSIxNjEiIGhlaWdodD0iMTYxIiBmaWxsPSIjMDE2OURBIi8+CiAgICA8cmVjdCB4PSIwIiB5PSIxNjAiIHdpZHRoPSIxNjIiIGhlaWdodD0iMTYxIiBmaWxsPSIjMDE2OURBIi8+CiAgICA8cmVjdCB4PSIzMjEiIHk9IjE2MCIgd2lkdGg9IjE2MiIgaGVpZ2h0PSIxNjEiIGZpbGw9IiMwMTY5REEiLz4KICAgIDxyZWN0IHg9IjAiIHk9IjQ4MiIgd2lkdGg9IjE2MiIgaGVpZ2h0PSIxNjIiIGZpbGw9IiMwNDk4RkIiLz4KICAgIDxyZWN0IHg9IjMyMSIgeT0iNDgyIiB3aWR0aD0iMTYyIiBoZWlnaHQ9IjE2MiIgZmlsbD0iIzA0OThGQiIvPgogICAgPHJlY3QgeD0iMTYxIiB5PSI2NDMiIHdpZHRoPSIxNjEiIGhlaWdodD0iMTYxIiBmaWxsPSIjMDQ5OEZCIi8+Cjwvc3ZnPg==";
369
+ function createBrandHeader() {
370
+ const header = document.createElement("div");
371
+ header.className = "mylikita-widget__brand";
372
+ const img = document.createElement("img");
373
+ img.className = "mylikita-widget__brand-mark";
374
+ img.src = MYLIKITA_MARK_DATA_URI;
375
+ img.alt = "MyLikita";
376
+ img.width = 24;
377
+ img.height = 24;
378
+ const word = document.createElement("span");
379
+ word.className = "mylikita-widget__brand-word";
380
+ word.textContent = "MyLikita";
381
+ header.append(img, word);
382
+ return header;
383
+ }
384
+
344
385
  // src/widget.js
345
386
  var DEFAULT_TEXT = {
346
387
  title: "Book an appointment",
@@ -379,6 +420,9 @@ function createBookingWidget(element, options = {}) {
379
420
  // (GET /v1/providers) on mount and populate the doctor dropdown. The
380
421
  // static `providers` option, when given, always wins and skips the fetch.
381
422
  loadProviders: options.loadProviders === true && !(options.providers && options.providers.length),
423
+ // White-label flag: premium hosts can drop the MyLikita brand header.
424
+ // Defaults to true (brand shown); pass showBrand:false to hide it.
425
+ showBrand: options.showBrand !== false,
382
426
  pollIntervalMs: options.pollIntervalMs ?? 5e3,
383
427
  maxTries: options.maxTries ?? 12,
384
428
  text: { ...DEFAULT_TEXT, ...options.text || {} },
@@ -401,6 +445,7 @@ function createBookingWidget(element, options = {}) {
401
445
  let pollCtrl = null;
402
446
  const instanceId = Math.random().toString(36).slice(2, 8);
403
447
  const refKey = `mylikita_ref_${opts.facilityId}_${instanceId}`;
448
+ const brand = opts.showBrand ? createBrandHeader() : null;
404
449
  const form = el("form", { className: "mylikita-widget__form" });
405
450
  const title = el("h3", { className: "mylikita-widget__title", text: t.title });
406
451
  const subtitle = el("p", { className: "mylikita-widget__subtitle", text: t.subtitle });
@@ -574,7 +619,7 @@ function createBookingWidget(element, options = {}) {
574
619
  submitBtn.textContent = t.submit;
575
620
  datetime.input.min = toLocalInputValue(/* @__PURE__ */ new Date());
576
621
  }
577
- root.replaceChildren(title, subtitle, form, statusView);
622
+ root.replaceChildren(brand, title, subtitle, form, statusView);
578
623
  let destroyProvidersFetch = null;
579
624
  if (opts.loadProviders) {
580
625
  const provCtrl = new AbortController();
@@ -1,4 +1,4 @@
1
- /*! @mylikita/booking-widget v0.1.0 | MIT */
1
+ /*! @mylikita/booking-widget v0.1.2 | MIT */
2
2
 
3
3
  // src/client.js
4
4
  async function createBooking({ relayUrl, websiteKey, payload, signal }) {
@@ -192,6 +192,27 @@ var STYLES = `
192
192
  .mylikita-widget *::before,
193
193
  .mylikita-widget *::after { box-sizing: border-box; }
194
194
 
195
+ .mylikita-widget__brand {
196
+ display: flex;
197
+ align-items: center;
198
+ gap: 8px;
199
+ margin-bottom: 14px;
200
+ padding-bottom: 12px;
201
+ border-bottom: 1px solid var(--mlw-border);
202
+ }
203
+ .mylikita-widget__brand-mark {
204
+ display: block;
205
+ flex: 0 0 auto;
206
+ border-radius: 5px;
207
+ box-shadow: 0 1px 3px rgba(15, 23, 42, .12);
208
+ }
209
+ .mylikita-widget__brand-word {
210
+ font-size: 14px;
211
+ font-weight: 700;
212
+ letter-spacing: .3px;
213
+ color: var(--mlw-muted);
214
+ }
215
+
195
216
  .mylikita-widget__title {
196
217
  font-size: 18px;
197
218
  font-weight: 700;
@@ -307,6 +328,24 @@ var STYLES = `
307
328
  .mylikita-widget__link-btn:hover { border-color: var(--mlw-primary); color: var(--mlw-primary); }
308
329
  `;
309
330
 
331
+ // src/brand.js
332
+ var MYLIKITA_MARK_DATA_URI = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0ODMgODA0IiB3aWR0aD0iNDgzIiBoZWlnaHQ9IjgwNCIgcm9sZT0iaW1nIiBhcmlhLWxhYmVsPSJNeUxpa2l0YSI+CiAgICA8cmVjdCB4PSIxNjEiIHk9IjAiIHdpZHRoPSIxNjEiIGhlaWdodD0iMTYxIiBmaWxsPSIjMDE2OURBIi8+CiAgICA8cmVjdCB4PSIwIiB5PSIxNjAiIHdpZHRoPSIxNjIiIGhlaWdodD0iMTYxIiBmaWxsPSIjMDE2OURBIi8+CiAgICA8cmVjdCB4PSIzMjEiIHk9IjE2MCIgd2lkdGg9IjE2MiIgaGVpZ2h0PSIxNjEiIGZpbGw9IiMwMTY5REEiLz4KICAgIDxyZWN0IHg9IjAiIHk9IjQ4MiIgd2lkdGg9IjE2MiIgaGVpZ2h0PSIxNjIiIGZpbGw9IiMwNDk4RkIiLz4KICAgIDxyZWN0IHg9IjMyMSIgeT0iNDgyIiB3aWR0aD0iMTYyIiBoZWlnaHQ9IjE2MiIgZmlsbD0iIzA0OThGQiIvPgogICAgPHJlY3QgeD0iMTYxIiB5PSI2NDMiIHdpZHRoPSIxNjEiIGhlaWdodD0iMTYxIiBmaWxsPSIjMDQ5OEZCIi8+Cjwvc3ZnPg==";
333
+ function createBrandHeader() {
334
+ const header = document.createElement("div");
335
+ header.className = "mylikita-widget__brand";
336
+ const img = document.createElement("img");
337
+ img.className = "mylikita-widget__brand-mark";
338
+ img.src = MYLIKITA_MARK_DATA_URI;
339
+ img.alt = "MyLikita";
340
+ img.width = 24;
341
+ img.height = 24;
342
+ const word = document.createElement("span");
343
+ word.className = "mylikita-widget__brand-word";
344
+ word.textContent = "MyLikita";
345
+ header.append(img, word);
346
+ return header;
347
+ }
348
+
310
349
  // src/widget.js
311
350
  var DEFAULT_TEXT = {
312
351
  title: "Book an appointment",
@@ -345,6 +384,9 @@ function createBookingWidget(element, options = {}) {
345
384
  // (GET /v1/providers) on mount and populate the doctor dropdown. The
346
385
  // static `providers` option, when given, always wins and skips the fetch.
347
386
  loadProviders: options.loadProviders === true && !(options.providers && options.providers.length),
387
+ // White-label flag: premium hosts can drop the MyLikita brand header.
388
+ // Defaults to true (brand shown); pass showBrand:false to hide it.
389
+ showBrand: options.showBrand !== false,
348
390
  pollIntervalMs: options.pollIntervalMs ?? 5e3,
349
391
  maxTries: options.maxTries ?? 12,
350
392
  text: { ...DEFAULT_TEXT, ...options.text || {} },
@@ -367,6 +409,7 @@ function createBookingWidget(element, options = {}) {
367
409
  let pollCtrl = null;
368
410
  const instanceId = Math.random().toString(36).slice(2, 8);
369
411
  const refKey = `mylikita_ref_${opts.facilityId}_${instanceId}`;
412
+ const brand = opts.showBrand ? createBrandHeader() : null;
370
413
  const form = el("form", { className: "mylikita-widget__form" });
371
414
  const title = el("h3", { className: "mylikita-widget__title", text: t.title });
372
415
  const subtitle = el("p", { className: "mylikita-widget__subtitle", text: t.subtitle });
@@ -540,7 +583,7 @@ function createBookingWidget(element, options = {}) {
540
583
  submitBtn.textContent = t.submit;
541
584
  datetime.input.min = toLocalInputValue(/* @__PURE__ */ new Date());
542
585
  }
543
- root.replaceChildren(title, subtitle, form, statusView);
586
+ root.replaceChildren(brand, title, subtitle, form, statusView);
544
587
  let destroyProvidersFetch = null;
545
588
  if (opts.loadProviders) {
546
589
  const provCtrl = new AbortController();
@@ -645,10 +688,12 @@ function applyTheme(rootNode, theme) {
645
688
  }
646
689
  export {
647
690
  DEFAULT_THEME,
691
+ MYLIKITA_MARK_DATA_URI,
648
692
  STATUS_COPY,
649
693
  TERMINAL_STATUSES,
650
694
  createBooking,
651
695
  createBookingWidget,
696
+ createBrandHeader,
652
697
  fetchProviders,
653
698
  fetchStatus,
654
699
  newExternalRef,
@@ -1,4 +1,4 @@
1
- /*! @mylikita/booking-widget v0.1.0 | MIT */
1
+ /*! @mylikita/booking-widget v0.1.2 | MIT */
2
2
  var MyLikitaBookingWidget = (() => {
3
3
  var __defProp = Object.defineProperty;
4
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -22,10 +22,12 @@ var MyLikitaBookingWidget = (() => {
22
22
  var index_exports = {};
23
23
  __export(index_exports, {
24
24
  DEFAULT_THEME: () => DEFAULT_THEME,
25
+ MYLIKITA_MARK_DATA_URI: () => MYLIKITA_MARK_DATA_URI,
25
26
  STATUS_COPY: () => STATUS_COPY,
26
27
  TERMINAL_STATUSES: () => TERMINAL_STATUSES,
27
28
  createBooking: () => createBooking,
28
29
  createBookingWidget: () => createBookingWidget,
30
+ createBrandHeader: () => createBrandHeader,
29
31
  fetchProviders: () => fetchProviders,
30
32
  fetchStatus: () => fetchStatus,
31
33
  newExternalRef: () => newExternalRef,
@@ -226,6 +228,27 @@ var MyLikitaBookingWidget = (() => {
226
228
  .mylikita-widget *::before,
227
229
  .mylikita-widget *::after { box-sizing: border-box; }
228
230
 
231
+ .mylikita-widget__brand {
232
+ display: flex;
233
+ align-items: center;
234
+ gap: 8px;
235
+ margin-bottom: 14px;
236
+ padding-bottom: 12px;
237
+ border-bottom: 1px solid var(--mlw-border);
238
+ }
239
+ .mylikita-widget__brand-mark {
240
+ display: block;
241
+ flex: 0 0 auto;
242
+ border-radius: 5px;
243
+ box-shadow: 0 1px 3px rgba(15, 23, 42, .12);
244
+ }
245
+ .mylikita-widget__brand-word {
246
+ font-size: 14px;
247
+ font-weight: 700;
248
+ letter-spacing: .3px;
249
+ color: var(--mlw-muted);
250
+ }
251
+
229
252
  .mylikita-widget__title {
230
253
  font-size: 18px;
231
254
  font-weight: 700;
@@ -341,6 +364,24 @@ var MyLikitaBookingWidget = (() => {
341
364
  .mylikita-widget__link-btn:hover { border-color: var(--mlw-primary); color: var(--mlw-primary); }
342
365
  `;
343
366
 
367
+ // src/brand.js
368
+ var MYLIKITA_MARK_DATA_URI = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0ODMgODA0IiB3aWR0aD0iNDgzIiBoZWlnaHQ9IjgwNCIgcm9sZT0iaW1nIiBhcmlhLWxhYmVsPSJNeUxpa2l0YSI+CiAgICA8cmVjdCB4PSIxNjEiIHk9IjAiIHdpZHRoPSIxNjEiIGhlaWdodD0iMTYxIiBmaWxsPSIjMDE2OURBIi8+CiAgICA8cmVjdCB4PSIwIiB5PSIxNjAiIHdpZHRoPSIxNjIiIGhlaWdodD0iMTYxIiBmaWxsPSIjMDE2OURBIi8+CiAgICA8cmVjdCB4PSIzMjEiIHk9IjE2MCIgd2lkdGg9IjE2MiIgaGVpZ2h0PSIxNjEiIGZpbGw9IiMwMTY5REEiLz4KICAgIDxyZWN0IHg9IjAiIHk9IjQ4MiIgd2lkdGg9IjE2MiIgaGVpZ2h0PSIxNjIiIGZpbGw9IiMwNDk4RkIiLz4KICAgIDxyZWN0IHg9IjMyMSIgeT0iNDgyIiB3aWR0aD0iMTYyIiBoZWlnaHQ9IjE2MiIgZmlsbD0iIzA0OThGQiIvPgogICAgPHJlY3QgeD0iMTYxIiB5PSI2NDMiIHdpZHRoPSIxNjEiIGhlaWdodD0iMTYxIiBmaWxsPSIjMDQ5OEZCIi8+Cjwvc3ZnPg==";
369
+ function createBrandHeader() {
370
+ const header = document.createElement("div");
371
+ header.className = "mylikita-widget__brand";
372
+ const img = document.createElement("img");
373
+ img.className = "mylikita-widget__brand-mark";
374
+ img.src = MYLIKITA_MARK_DATA_URI;
375
+ img.alt = "MyLikita";
376
+ img.width = 24;
377
+ img.height = 24;
378
+ const word = document.createElement("span");
379
+ word.className = "mylikita-widget__brand-word";
380
+ word.textContent = "MyLikita";
381
+ header.append(img, word);
382
+ return header;
383
+ }
384
+
344
385
  // src/widget.js
345
386
  var DEFAULT_TEXT = {
346
387
  title: "Book an appointment",
@@ -379,6 +420,9 @@ var MyLikitaBookingWidget = (() => {
379
420
  // (GET /v1/providers) on mount and populate the doctor dropdown. The
380
421
  // static `providers` option, when given, always wins and skips the fetch.
381
422
  loadProviders: options.loadProviders === true && !(options.providers && options.providers.length),
423
+ // White-label flag: premium hosts can drop the MyLikita brand header.
424
+ // Defaults to true (brand shown); pass showBrand:false to hide it.
425
+ showBrand: options.showBrand !== false,
382
426
  pollIntervalMs: options.pollIntervalMs ?? 5e3,
383
427
  maxTries: options.maxTries ?? 12,
384
428
  text: { ...DEFAULT_TEXT, ...options.text || {} },
@@ -401,6 +445,7 @@ var MyLikitaBookingWidget = (() => {
401
445
  let pollCtrl = null;
402
446
  const instanceId = Math.random().toString(36).slice(2, 8);
403
447
  const refKey = `mylikita_ref_${opts.facilityId}_${instanceId}`;
448
+ const brand = opts.showBrand ? createBrandHeader() : null;
404
449
  const form = el("form", { className: "mylikita-widget__form" });
405
450
  const title = el("h3", { className: "mylikita-widget__title", text: t.title });
406
451
  const subtitle = el("p", { className: "mylikita-widget__subtitle", text: t.subtitle });
@@ -574,7 +619,7 @@ var MyLikitaBookingWidget = (() => {
574
619
  submitBtn.textContent = t.submit;
575
620
  datetime.input.min = toLocalInputValue(/* @__PURE__ */ new Date());
576
621
  }
577
- root.replaceChildren(title, subtitle, form, statusView);
622
+ root.replaceChildren(brand, title, subtitle, form, statusView);
578
623
  let destroyProvidersFetch = null;
579
624
  if (opts.loadProviders) {
580
625
  const provCtrl = new AbortController();
@@ -1,5 +1,5 @@
1
- /*! @mylikita/booking-widget v0.1.0 | MIT */
2
- var MyLikitaBookingWidget=(()=>{var Y=Object.defineProperty;var pe=Object.getOwnPropertyDescriptor;var fe=Object.getOwnPropertyNames;var ge=Object.prototype.hasOwnProperty;var we=(i,e)=>{for(var t in e)Y(i,t,{get:e[t],enumerable:!0})},ye=(i,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let a of fe(e))!ge.call(i,a)&&a!==t&&Y(i,a,{get:()=>e[a],enumerable:!(r=pe(e,a))||r.enumerable});return i};var _e=i=>ye(Y({},"__esModule",{value:!0}),i);var Ee={};we(Ee,{DEFAULT_THEME:()=>W,STATUS_COPY:()=>C,TERMINAL_STATUSES:()=>ie,createBooking:()=>S,createBookingWidget:()=>oe,fetchProviders:()=>E,fetchStatus:()=>T,newExternalRef:()=>P,pollStatus:()=>N,resolveTheme:()=>$,statusCopy:()=>A});async function S({relayUrl:i,websiteKey:e,payload:t,signal:r}){let a=await fetch(`${H(i)}/v1/bookings`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${e}`},body:JSON.stringify(t),signal:r}),o=await F(a);if(a.status===409)return{ok:!0,duplicate:!0,booking_ref:o?.booking_ref,error:o?.message};if(!a.ok)throw O(a.status,o,"create");return{ok:!0,duplicate:!1,booking_ref:o?.booking_ref,status:o?.status||"pending_confirmation"}}async function E({relayUrl:i,websiteKey:e,signal:t}){let r=await fetch(`${H(i)}/v1/providers`,{headers:{Authorization:`Bearer ${e}`},signal:t}),a=await F(r);if(!r.ok)throw O(r.status,a,"providers");return(Array.isArray(a?.providers)?a.providers:[]).map(d=>({external_id:d.external_id,name:d.name||d.external_id,specialty:d.specialty||null,module:d.module||"general"}))}async function T({relayUrl:i,websiteKey:e,bookingRef:t,signal:r}){let a=await fetch(`${H(i)}/v1/bookings/${encodeURIComponent(t)}`,{headers:{Authorization:`Bearer ${e}`},signal:r}),o=await F(a);if(!a.ok)throw O(a.status,o,"status");return{booking_ref:o?.booking_ref,status:o?.status||"pending_confirmation",appt_ref:o?.appt_ref||null}}async function N(i,{intervalMs:e=5e3,maxTries:t=12,signal:r}={}){for(let a=0;a<t;a++){if(r?.aborted)return{status:"aborted",resolved:!1,data:null};let o=await i();if(o.status!=="pending_confirmation")return{status:o.status,resolved:!0,data:o};a<t-1&&await be(e,r)}return{status:"pending_confirmation",resolved:!1,data:null}}function P(){return`BK-${Date.now()}-${Math.random().toString(36).slice(2,6)}`}function H(i){return String(i||"").replace(/\/+$/,"")}function O(i,e,t){let r=new Error(e?.message||e?.error||`Relay ${t} failed (HTTP ${i})`);return r.code=e?.error||"http_error",r.status=i,r}async function F(i){try{return await i.json()}catch{return null}}function be(i,e){return new Promise(t=>{if(e?.aborted)return t();let r=()=>{clearTimeout(a),t()},a=setTimeout(()=>{e?.removeEventListener("abort",r),t()},i);e?.addEventListener("abort",r,{once:!0})})}var C={pending_confirmation:{title:"Request received",message:"We've received your booking request \u2014 we'll confirm shortly.",kind:"info"},confirmed:{title:"Appointment confirmed",message:"Your appointment is confirmed. See you at the clinic!",kind:"success"},cancelled:{title:"Appointment cancelled",message:"This appointment was cancelled. Please contact the clinic if this was unexpected.",kind:"danger"},rescheduled:{title:"Appointment rescheduled",message:"This appointment was moved \u2014 the new time was sent to you.",kind:"info"},no_show:{title:"Missed appointment",message:"This appointment was marked as missed.",kind:"danger"},expired:{title:"Request expired",message:"This booking request expired \u2014 please call the clinic to book.",kind:"danger"},poll_error:{title:"Something went wrong",message:"We could not check your booking right now. Please try again shortly.",kind:"danger"}};function A(i){return C[i]||C.pending_confirmation}var ie=["confirmed","cancelled","rescheduled","no_show","expired"];var W={primary:"#0d6efd",primaryDark:"#0b5ed7",primaryText:"#ffffff",bg:"#ffffff",text:"#1e293b",muted:"#64748b",border:"#e2e8f0",danger:"#dc3545",success:"#15803d",radius:10,font:"system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif"};function $(i={}){let e={...W,...i||{}};return{"--mlw-primary":e.primary,"--mlw-primary-dark":e.primaryDark,"--mlw-primary-text":e.primaryText,"--mlw-bg":e.bg,"--mlw-text":e.text,"--mlw-muted":e.muted,"--mlw-border":e.border,"--mlw-danger":e.danger,"--mlw-success":e.success,"--mlw-radius":`${e.radius}px`,"--mlw-font":e.font}}var re=`
1
+ /*! @mylikita/booking-widget v0.1.2 | MIT */
2
+ var MyLikitaBookingWidget=(()=>{var Y=Object.defineProperty;var we=Object.getOwnPropertyDescriptor;var ye=Object.getOwnPropertyNames;var xe=Object.prototype.hasOwnProperty;var be=(i,e)=>{for(var t in e)Y(i,t,{get:e[t],enumerable:!0})},_e=(i,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let a of ye(e))!xe.call(i,a)&&a!==t&&Y(i,a,{get:()=>e[a],enumerable:!(r=we(e,a))||r.enumerable});return i};var ve=i=>_e(Y({},"__esModule",{value:!0}),i);var Ae={};be(Ae,{DEFAULT_THEME:()=>O,MYLIKITA_MARK_DATA_URI:()=>G,STATUS_COPY:()=>N,TERMINAL_STATUSES:()=>ae,createBooking:()=>I,createBookingWidget:()=>le,createBrandHeader:()=>M,fetchProviders:()=>S,fetchStatus:()=>E,newExternalRef:()=>A,pollStatus:()=>T,resolveTheme:()=>P,statusCopy:()=>C});async function I({relayUrl:i,websiteKey:e,payload:t,signal:r}){let a=await fetch(`${q(i)}/v1/bookings`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${e}`},body:JSON.stringify(t),signal:r}),o=await Z(a);if(a.status===409)return{ok:!0,duplicate:!0,booking_ref:o?.booking_ref,error:o?.message};if(!a.ok)throw K(a.status,o,"create");return{ok:!0,duplicate:!1,booking_ref:o?.booking_ref,status:o?.status||"pending_confirmation"}}async function S({relayUrl:i,websiteKey:e,signal:t}){let r=await fetch(`${q(i)}/v1/providers`,{headers:{Authorization:`Bearer ${e}`},signal:t}),a=await Z(r);if(!r.ok)throw K(r.status,a,"providers");return(Array.isArray(a?.providers)?a.providers:[]).map(d=>({external_id:d.external_id,name:d.name||d.external_id,specialty:d.specialty||null,module:d.module||"general"}))}async function E({relayUrl:i,websiteKey:e,bookingRef:t,signal:r}){let a=await fetch(`${q(i)}/v1/bookings/${encodeURIComponent(t)}`,{headers:{Authorization:`Bearer ${e}`},signal:r}),o=await Z(a);if(!a.ok)throw K(a.status,o,"status");return{booking_ref:o?.booking_ref,status:o?.status||"pending_confirmation",appt_ref:o?.appt_ref||null}}async function T(i,{intervalMs:e=5e3,maxTries:t=12,signal:r}={}){for(let a=0;a<t;a++){if(r?.aborted)return{status:"aborted",resolved:!1,data:null};let o=await i();if(o.status!=="pending_confirmation")return{status:o.status,resolved:!0,data:o};a<t-1&&await he(e,r)}return{status:"pending_confirmation",resolved:!1,data:null}}function A(){return`BK-${Date.now()}-${Math.random().toString(36).slice(2,6)}`}function q(i){return String(i||"").replace(/\/+$/,"")}function K(i,e,t){let r=new Error(e?.message||e?.error||`Relay ${t} failed (HTTP ${i})`);return r.code=e?.error||"http_error",r.status=i,r}async function Z(i){try{return await i.json()}catch{return null}}function he(i,e){return new Promise(t=>{if(e?.aborted)return t();let r=()=>{clearTimeout(a),t()},a=setTimeout(()=>{e?.removeEventListener("abort",r),t()},i);e?.addEventListener("abort",r,{once:!0})})}var N={pending_confirmation:{title:"Request received",message:"We've received your booking request \u2014 we'll confirm shortly.",kind:"info"},confirmed:{title:"Appointment confirmed",message:"Your appointment is confirmed. See you at the clinic!",kind:"success"},cancelled:{title:"Appointment cancelled",message:"This appointment was cancelled. Please contact the clinic if this was unexpected.",kind:"danger"},rescheduled:{title:"Appointment rescheduled",message:"This appointment was moved \u2014 the new time was sent to you.",kind:"info"},no_show:{title:"Missed appointment",message:"This appointment was marked as missed.",kind:"danger"},expired:{title:"Request expired",message:"This booking request expired \u2014 please call the clinic to book.",kind:"danger"},poll_error:{title:"Something went wrong",message:"We could not check your booking right now. Please try again shortly.",kind:"danger"}};function C(i){return N[i]||N.pending_confirmation}var ae=["confirmed","cancelled","rescheduled","no_show","expired"];var O={primary:"#0d6efd",primaryDark:"#0b5ed7",primaryText:"#ffffff",bg:"#ffffff",text:"#1e293b",muted:"#64748b",border:"#e2e8f0",danger:"#dc3545",success:"#15803d",radius:10,font:"system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif"};function P(i={}){let e={...O,...i||{}};return{"--mlw-primary":e.primary,"--mlw-primary-dark":e.primaryDark,"--mlw-primary-text":e.primaryText,"--mlw-bg":e.bg,"--mlw-text":e.text,"--mlw-muted":e.muted,"--mlw-border":e.border,"--mlw-danger":e.danger,"--mlw-success":e.success,"--mlw-radius":`${e.radius}px`,"--mlw-font":e.font}}var ne=`
3
3
  .mylikita-widget {
4
4
  --mlw-primary: #0d6efd;
5
5
  --mlw-primary-dark: #0b5ed7;
@@ -26,6 +26,27 @@ var MyLikitaBookingWidget=(()=>{var Y=Object.defineProperty;var pe=Object.getOwn
26
26
  .mylikita-widget *::before,
27
27
  .mylikita-widget *::after { box-sizing: border-box; }
28
28
 
29
+ .mylikita-widget__brand {
30
+ display: flex;
31
+ align-items: center;
32
+ gap: 8px;
33
+ margin-bottom: 14px;
34
+ padding-bottom: 12px;
35
+ border-bottom: 1px solid var(--mlw-border);
36
+ }
37
+ .mylikita-widget__brand-mark {
38
+ display: block;
39
+ flex: 0 0 auto;
40
+ border-radius: 5px;
41
+ box-shadow: 0 1px 3px rgba(15, 23, 42, .12);
42
+ }
43
+ .mylikita-widget__brand-word {
44
+ font-size: 14px;
45
+ font-weight: 700;
46
+ letter-spacing: .3px;
47
+ color: var(--mlw-muted);
48
+ }
49
+
29
50
  .mylikita-widget__title {
30
51
  font-size: 18px;
31
52
  font-weight: 700;
@@ -139,4 +160,4 @@ var MyLikitaBookingWidget=(()=>{var Y=Object.defineProperty;var pe=Object.getOwn
139
160
  cursor: pointer;
140
161
  }
141
162
  .mylikita-widget__link-btn:hover { border-color: var(--mlw-primary); color: var(--mlw-primary); }
142
- `;var xe={title:"Book an appointment",subtitle:"Request a slot and we will confirm shortly.",name:"Full name",phone:"Phone number",email:"Email address",provider:"Preferred doctor (optional)",noPreference:"No preference",service:"Service (optional)",datetime:"Preferred date & time",visitType:"Appointment type",visitPhysical:"In person",visitTelemedicine:"Video call",visitHome:"Home visit",notes:"Notes (optional)",submit:"Request appointment",submitting:"Submitting\u2026",bookAnother:"Book another appointment",requiredPhoneOrEmail:"Please provide a phone number or an email address.",requiredName:"Please enter your name.",requiredDatetime:"Please choose a date and time.",networkError:"Could not reach the booking service. Please try again.",rateLimited:"Too many requests \u2014 please wait a moment and try again."},ae="mylikita-widget-styles";function oe(i,e={}){if(!i)throw new Error("createBookingWidget: a container element is required");let t={relayUrl:e.relayUrl,websiteKey:e.websiteKey,facilityId:e.facilityId,providers:e.providers||[],services:e.services||[],loadProviders:e.loadProviders===!0&&!(e.providers&&e.providers.length),pollIntervalMs:e.pollIntervalMs??5e3,maxTries:e.maxTries??12,text:{...xe,...e.text||{}},theme:e.theme||{},onStatus:typeof e.onStatus=="function"?e.onStatus:null,onError:typeof e.onError=="function"?e.onError:null,onBooking:typeof e.onBooking=="function"?e.onBooking:null,externalRef:typeof e.externalRef=="function"?e.externalRef:P};if(!t.relayUrl||!t.websiteKey||!t.facilityId)throw new Error("createBookingWidget: relayUrl, websiteKey and facilityId are required");ke();let r=t.text,a=i;a.classList.add("mylikita-widget"),Se(a,t.theme);let o=!0,d=!1,u=null,f=Math.random().toString(36).slice(2,8),I=`mylikita_ref_${t.facilityId}_${f}`,y=l("form",{className:"mylikita-widget__form"}),B=l("h3",{className:"mylikita-widget__title",text:r.title}),U=l("p",{className:"mylikita-widget__subtitle",text:r.subtitle}),L=l("div",{className:"mylikita-widget__error",attrs:{role:"alert"}}),q=_("name",r.name,{required:!0}),V=l("div",{className:"mylikita-widget__row"}),z=_("phone",r.phone,{type:"tel",inputmode:"tel"}),R=_("email",r.email,{type:"email"});V.append(z.wrap,R.wrap);let g=j("provider",r.provider,[{value:"",label:r.noPreference},...t.providers.map(n=>({value:n.external_id,label:n.label||n.name||n.external_id}))]);function se(n){let s=g.input.value;g.input.replaceChildren();let c=document.createElement("option");c.value="",c.textContent=r.noPreference,g.input.append(c);for(let m of n||[]){let p=document.createElement("option");p.value=m.external_id,p.textContent=m.label||m.name||m.external_id,g.input.append(p)}s&&(g.input.value=s)}let J=t.services.length?j("service",r.service,[{value:"",label:"\u2014"},...t.services.map(n=>({value:n,label:n}))]):_("service",r.service),v=_("datetime",r.datetime,{type:"datetime-local",required:!0});v.input.min=ne(new Date);let G=j("visitType",r.visitType,[{value:"physical",label:r.visitPhysical},{value:"telemedicine",label:r.visitTelemedicine},{value:"home_visit",label:r.visitHome}]),X=_("notes",r.notes,{type:"textarea",maxlength:500}),w=l("button",{className:"mylikita-widget__submit",type:"submit",text:r.submit}),Q=l("div");Q.append(w);let M=l("p",{className:"mylikita-widget__hint"});y.append(L,q.wrap,V,g.wrap,J.wrap,v.wrap,G.wrap,X.wrap,Q,M);let b=l("div",{className:"mylikita-widget__status",attrs:{"aria-live":"polite"},hidden:!0});y.addEventListener("submit",n=>{n.preventDefault(),!d&&le()});async function le(){d=!0,D(null),w.disabled=!0,w.textContent=r.submitting;let n={facility_id:t.facilityId,patient_name:q.input.value.trim(),patient_phone:z.input.value.trim(),patient_email:R.input.value.trim(),provider_external_id:g.input.value||void 0,service_name:J.input.value.trim()||void 0,appt_datetime:v.input.value,visit_type:G.input.value||"physical",duration_mins:t.durationMins||void 0,notes:X.input.value.trim()||void 0};if(!n.patient_name)return k(r.requiredName);if(!n.patient_phone&&!n.patient_email)return k(r.requiredPhoneOrEmail);if(!n.appt_datetime||Number.isNaN(Date.parse(n.appt_datetime)))return k(r.requiredDatetime);let s=he(I);if(!s){s=t.externalRef();try{sessionStorage.setItem(I,s)}catch{}}n.external_ref=s,u=new AbortController;let c=null;try{let m=await S({relayUrl:t.relayUrl,websiteKey:t.websiteKey,payload:n,signal:u.signal});c=m.booking_ref,m.duplicate?(M.textContent="",Z("pending_confirmation",c,"We found an existing booking request for this slot \u2014 checking it\u2026")):(t.onBooking&&x(t.onBooking,m,n),Z("pending_confirmation",c,null)),de(c)}catch(m){if(!o||m?.name==="AbortError")return;let p=m.status===429?r.rateLimited:m.message||r.networkError;t.onError&&x(t.onError,m),k(p)}}async function de(n){let s;try{s=await N(()=>T({relayUrl:t.relayUrl,websiteKey:t.websiteKey,bookingRef:n,signal:u.signal}),{intervalMs:t.pollIntervalMs,maxTries:t.maxTries,signal:u.signal})}catch(c){if(!o||c?.name==="AbortError")return;t.onError&&x(t.onError,c),h("poll_error",n,c.message||r.networkError),d=!1;return}if(!(!o||s.status==="aborted")){if(t.onStatus&&s.data&&x(t.onStatus,s.data),s.resolved){try{sessionStorage.removeItem(I)}catch{}h(s.status,n)}else h("pending_confirmation",n);d=!1}}function Z(n,s,c){y.hidden=!0,B.hidden=!0,U.hidden=!0,b.hidden=!1,h(n,s,c)}function h(n,s,c){let m=A(n),p=l("div",{className:`mylikita-widget__status-icon ${m.kind}`});p.textContent=ve(m.kind);let me=l("p",{className:"mylikita-widget__status-title",text:m.title}),ce=l("p",{className:"mylikita-widget__status-message",text:c||m.message}),ue=l("p",{className:"mylikita-widget__status-ref",text:s?`Booking ref: ${s}`:""}),te=l("button",{className:"mylikita-widget__link-btn",type:"button",text:r.bookAnother});te.addEventListener("click",()=>ee()),b.replaceChildren(p,me,ce,ue,te)}function k(n){d=!1,w.disabled=!1,w.textContent=r.submit,D(n)}function D(n){L.textContent=n||"",L.classList.toggle("visible",!!n)}function ee(){try{sessionStorage.removeItem(`mylikita_ref_${t.facilityId}`)}catch{}y.reset(),D(null),b.replaceChildren(),b.hidden=!0,y.hidden=!1,B.hidden=!1,U.hidden=!1,M.textContent="",d=!1,w.disabled=!1,w.textContent=r.submit,v.input.min=ne(new Date)}a.replaceChildren(B,U,y,b);let K=null;if(t.loadProviders){let n=new AbortController;(async()=>{try{let s=await E({relayUrl:t.relayUrl,websiteKey:t.websiteKey,signal:n.signal});o&&!n.signal.aborted&&se(s)}catch(s){if(!o||s?.name==="AbortError")return;t.onError&&x(t.onError,s)}})(),K=()=>n.abort()}return{destroy(){o=!1,u&&u.abort(),K&&K(),a.replaceChildren(),a.classList.remove("mylikita-widget")},reset:ee,getForm(){return{name:q.input.value,phone:z.input.value,email:R.input.value}}}}function _(i,e,{type:t="text",required:r=!1,maxlength:a,inputmode:o}={}){let d=l("div",{className:"mylikita-widget__field"}),u=l("label",{className:"mylikita-widget__label",attrs:{for:`mlw-${i}`}});u.append(document.createTextNode(e)),r&&u.append(l("span",{className:"req",text:" *"}));let f;return t==="textarea"?f=l("textarea",{className:"mylikita-widget__textarea",attrs:{id:`mlw-${i}`,rows:3,maxlength:a||""}}):f=l("input",{className:"mylikita-widget__input",attrs:{id:`mlw-${i}`,type:t,inputmode:o||""}}),r&&f.setAttribute("required",""),d.append(u,f),{wrap:d,input:f}}function j(i,e,t){let r=l("div",{className:"mylikita-widget__field"}),a=l("label",{className:"mylikita-widget__label",attrs:{for:`mlw-${i}`}});a.textContent=e;let o=l("select",{className:"mylikita-widget__select",attrs:{id:`mlw-${i}`}});for(let d of t){let u=l("option",{text:d.label});u.value=d.value,o.append(u)}return r.append(a,o),{wrap:r,input:o}}function l(i,{className:e,text:t,attrs:r={}}={}){let a=document.createElement(i);e&&(a.className=e),t!==void 0&&(a.textContent=t);for(let[o,d]of Object.entries(r))d===void 0||d===""||a.setAttribute(o,d);return a}function ve(i){return i==="success"?"\u2713":i==="danger"?"!":"\u2026"}function he(i){try{return sessionStorage.getItem(i)||null}catch{return null}}function x(i,...e){try{i(...e)}catch{}}function ne(i){let e=t=>String(t).padStart(2,"0");return`${i.getFullYear()}-${e(i.getMonth()+1)}-${e(i.getDate())}T${e(i.getHours())}:${e(i.getMinutes())}`}function ke(){if(document.getElementById(ae))return;let i=document.createElement("style");i.id=ae,i.textContent=re,document.head.appendChild(i)}function Se(i,e){for(let[t,r]of Object.entries($(e)))i.style.setProperty(t,r)}return _e(Ee);})();
163
+ `;var G="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0ODMgODA0IiB3aWR0aD0iNDgzIiBoZWlnaHQ9IjgwNCIgcm9sZT0iaW1nIiBhcmlhLWxhYmVsPSJNeUxpa2l0YSI+CiAgICA8cmVjdCB4PSIxNjEiIHk9IjAiIHdpZHRoPSIxNjEiIGhlaWdodD0iMTYxIiBmaWxsPSIjMDE2OURBIi8+CiAgICA8cmVjdCB4PSIwIiB5PSIxNjAiIHdpZHRoPSIxNjIiIGhlaWdodD0iMTYxIiBmaWxsPSIjMDE2OURBIi8+CiAgICA8cmVjdCB4PSIzMjEiIHk9IjE2MCIgd2lkdGg9IjE2MiIgaGVpZ2h0PSIxNjEiIGZpbGw9IiMwMTY5REEiLz4KICAgIDxyZWN0IHg9IjAiIHk9IjQ4MiIgd2lkdGg9IjE2MiIgaGVpZ2h0PSIxNjIiIGZpbGw9IiMwNDk4RkIiLz4KICAgIDxyZWN0IHg9IjMyMSIgeT0iNDgyIiB3aWR0aD0iMTYyIiBoZWlnaHQ9IjE2MiIgZmlsbD0iIzA0OThGQiIvPgogICAgPHJlY3QgeD0iMTYxIiB5PSI2NDMiIHdpZHRoPSIxNjEiIGhlaWdodD0iMTYxIiBmaWxsPSIjMDQ5OEZCIi8+Cjwvc3ZnPg==";function M(){let i=document.createElement("div");i.className="mylikita-widget__brand";let e=document.createElement("img");e.className="mylikita-widget__brand-mark",e.src=G,e.alt="MyLikita",e.width=24,e.height=24;let t=document.createElement("span");return t.className="mylikita-widget__brand-word",t.textContent="MyLikita",i.append(e,t),i}var ke={title:"Book an appointment",subtitle:"Request a slot and we will confirm shortly.",name:"Full name",phone:"Phone number",email:"Email address",provider:"Preferred doctor (optional)",noPreference:"No preference",service:"Service (optional)",datetime:"Preferred date & time",visitType:"Appointment type",visitPhysical:"In person",visitTelemedicine:"Video call",visitHome:"Home visit",notes:"Notes (optional)",submit:"Request appointment",submitting:"Submitting\u2026",bookAnother:"Book another appointment",requiredPhoneOrEmail:"Please provide a phone number or an email address.",requiredName:"Please enter your name.",requiredDatetime:"Please choose a date and time.",networkError:"Could not reach the booking service. Please try again.",rateLimited:"Too many requests \u2014 please wait a moment and try again."},oe="mylikita-widget-styles";function le(i,e={}){if(!i)throw new Error("createBookingWidget: a container element is required");let t={relayUrl:e.relayUrl,websiteKey:e.websiteKey,facilityId:e.facilityId,providers:e.providers||[],services:e.services||[],loadProviders:e.loadProviders===!0&&!(e.providers&&e.providers.length),showBrand:e.showBrand!==!1,pollIntervalMs:e.pollIntervalMs??5e3,maxTries:e.maxTries??12,text:{...ke,...e.text||{}},theme:e.theme||{},onStatus:typeof e.onStatus=="function"?e.onStatus:null,onError:typeof e.onError=="function"?e.onError:null,onBooking:typeof e.onBooking=="function"?e.onBooking:null,externalRef:typeof e.externalRef=="function"?e.externalRef:A};if(!t.relayUrl||!t.websiteKey||!t.facilityId)throw new Error("createBookingWidget: relayUrl, websiteKey and facilityId are required");Ee();let r=t.text,a=i;a.classList.add("mylikita-widget"),Te(a,t.theme);let o=!0,d=!1,u=null,f=Math.random().toString(36).slice(2,8),B=`mylikita_ref_${t.facilityId}_${f}`,de=t.showBrand?M():null,y=l("form",{className:"mylikita-widget__form"}),D=l("h3",{className:"mylikita-widget__title",text:r.title}),j=l("p",{className:"mylikita-widget__subtitle",text:r.subtitle}),R=l("div",{className:"mylikita-widget__error",attrs:{role:"alert"}}),H=x("name",r.name,{required:!0}),F=l("div",{className:"mylikita-widget__row"}),L=x("phone",r.phone,{type:"tel",inputmode:"tel"}),$=x("email",r.email,{type:"email"});F.append(L.wrap,$.wrap);let g=V("provider",r.provider,[{value:"",label:r.noPreference},...t.providers.map(n=>({value:n.external_id,label:n.label||n.name||n.external_id}))]);function me(n){let s=g.input.value;g.input.replaceChildren();let c=document.createElement("option");c.value="",c.textContent=r.noPreference,g.input.append(c);for(let m of n||[]){let p=document.createElement("option");p.value=m.external_id,p.textContent=m.label||m.name||m.external_id,g.input.append(p)}s&&(g.input.value=s)}let Q=t.services.length?V("service",r.service,[{value:"",label:"\u2014"},...t.services.map(n=>({value:n,label:n}))]):x("service",r.service),v=x("datetime",r.datetime,{type:"datetime-local",required:!0});v.input.min=se(new Date);let J=V("visitType",r.visitType,[{value:"physical",label:r.visitPhysical},{value:"telemedicine",label:r.visitTelemedicine},{value:"home_visit",label:r.visitHome}]),X=x("notes",r.notes,{type:"textarea",maxlength:500}),w=l("button",{className:"mylikita-widget__submit",type:"submit",text:r.submit}),ee=l("div");ee.append(w);let U=l("p",{className:"mylikita-widget__hint"});y.append(R,H.wrap,F,g.wrap,Q.wrap,v.wrap,J.wrap,X.wrap,ee,U);let b=l("div",{className:"mylikita-widget__status",attrs:{"aria-live":"polite"},hidden:!0});y.addEventListener("submit",n=>{n.preventDefault(),!d&&ce()});async function ce(){d=!0,z(null),w.disabled=!0,w.textContent=r.submitting;let n={facility_id:t.facilityId,patient_name:H.input.value.trim(),patient_phone:L.input.value.trim(),patient_email:$.input.value.trim(),provider_external_id:g.input.value||void 0,service_name:Q.input.value.trim()||void 0,appt_datetime:v.input.value,visit_type:J.input.value||"physical",duration_mins:t.durationMins||void 0,notes:X.input.value.trim()||void 0};if(!n.patient_name)return k(r.requiredName);if(!n.patient_phone&&!n.patient_email)return k(r.requiredPhoneOrEmail);if(!n.appt_datetime||Number.isNaN(Date.parse(n.appt_datetime)))return k(r.requiredDatetime);let s=Se(B);if(!s){s=t.externalRef();try{sessionStorage.setItem(B,s)}catch{}}n.external_ref=s,u=new AbortController;let c=null;try{let m=await I({relayUrl:t.relayUrl,websiteKey:t.websiteKey,payload:n,signal:u.signal});c=m.booking_ref,m.duplicate?(U.textContent="",te("pending_confirmation",c,"We found an existing booking request for this slot \u2014 checking it\u2026")):(t.onBooking&&_(t.onBooking,m,n),te("pending_confirmation",c,null)),ue(c)}catch(m){if(!o||m?.name==="AbortError")return;let p=m.status===429?r.rateLimited:m.message||r.networkError;t.onError&&_(t.onError,m),k(p)}}async function ue(n){let s;try{s=await T(()=>E({relayUrl:t.relayUrl,websiteKey:t.websiteKey,bookingRef:n,signal:u.signal}),{intervalMs:t.pollIntervalMs,maxTries:t.maxTries,signal:u.signal})}catch(c){if(!o||c?.name==="AbortError")return;t.onError&&_(t.onError,c),h("poll_error",n,c.message||r.networkError),d=!1;return}if(!(!o||s.status==="aborted")){if(t.onStatus&&s.data&&_(t.onStatus,s.data),s.resolved){try{sessionStorage.removeItem(B)}catch{}h(s.status,n)}else h("pending_confirmation",n);d=!1}}function te(n,s,c){y.hidden=!0,D.hidden=!0,j.hidden=!0,b.hidden=!1,h(n,s,c)}function h(n,s,c){let m=C(n),p=l("div",{className:`mylikita-widget__status-icon ${m.kind}`});p.textContent=Ie(m.kind);let pe=l("p",{className:"mylikita-widget__status-title",text:m.title}),fe=l("p",{className:"mylikita-widget__status-message",text:c||m.message}),ge=l("p",{className:"mylikita-widget__status-ref",text:s?`Booking ref: ${s}`:""}),re=l("button",{className:"mylikita-widget__link-btn",type:"button",text:r.bookAnother});re.addEventListener("click",()=>ie()),b.replaceChildren(p,pe,fe,ge,re)}function k(n){d=!1,w.disabled=!1,w.textContent=r.submit,z(n)}function z(n){R.textContent=n||"",R.classList.toggle("visible",!!n)}function ie(){try{sessionStorage.removeItem(`mylikita_ref_${t.facilityId}`)}catch{}y.reset(),z(null),b.replaceChildren(),b.hidden=!0,y.hidden=!1,D.hidden=!1,j.hidden=!1,U.textContent="",d=!1,w.disabled=!1,w.textContent=r.submit,v.input.min=se(new Date)}a.replaceChildren(de,D,j,y,b);let W=null;if(t.loadProviders){let n=new AbortController;(async()=>{try{let s=await S({relayUrl:t.relayUrl,websiteKey:t.websiteKey,signal:n.signal});o&&!n.signal.aborted&&me(s)}catch(s){if(!o||s?.name==="AbortError")return;t.onError&&_(t.onError,s)}})(),W=()=>n.abort()}return{destroy(){o=!1,u&&u.abort(),W&&W(),a.replaceChildren(),a.classList.remove("mylikita-widget")},reset:ie,getForm(){return{name:H.input.value,phone:L.input.value,email:$.input.value}}}}function x(i,e,{type:t="text",required:r=!1,maxlength:a,inputmode:o}={}){let d=l("div",{className:"mylikita-widget__field"}),u=l("label",{className:"mylikita-widget__label",attrs:{for:`mlw-${i}`}});u.append(document.createTextNode(e)),r&&u.append(l("span",{className:"req",text:" *"}));let f;return t==="textarea"?f=l("textarea",{className:"mylikita-widget__textarea",attrs:{id:`mlw-${i}`,rows:3,maxlength:a||""}}):f=l("input",{className:"mylikita-widget__input",attrs:{id:`mlw-${i}`,type:t,inputmode:o||""}}),r&&f.setAttribute("required",""),d.append(u,f),{wrap:d,input:f}}function V(i,e,t){let r=l("div",{className:"mylikita-widget__field"}),a=l("label",{className:"mylikita-widget__label",attrs:{for:`mlw-${i}`}});a.textContent=e;let o=l("select",{className:"mylikita-widget__select",attrs:{id:`mlw-${i}`}});for(let d of t){let u=l("option",{text:d.label});u.value=d.value,o.append(u)}return r.append(a,o),{wrap:r,input:o}}function l(i,{className:e,text:t,attrs:r={}}={}){let a=document.createElement(i);e&&(a.className=e),t!==void 0&&(a.textContent=t);for(let[o,d]of Object.entries(r))d===void 0||d===""||a.setAttribute(o,d);return a}function Ie(i){return i==="success"?"\u2713":i==="danger"?"!":"\u2026"}function Se(i){try{return sessionStorage.getItem(i)||null}catch{return null}}function _(i,...e){try{i(...e)}catch{}}function se(i){let e=t=>String(t).padStart(2,"0");return`${i.getFullYear()}-${e(i.getMonth()+1)}-${e(i.getDate())}T${e(i.getHours())}:${e(i.getMinutes())}`}function Ee(){if(document.getElementById(oe))return;let i=document.createElement("style");i.id=oe,i.textContent=ne,document.head.appendChild(i)}function Te(i,e){for(let[t,r]of Object.entries(P(e)))i.style.setProperty(t,r)}return ve(Ae);})();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mylikita/booking-widget",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Drop-in appointment booking widget for MyLikita hospital websites. Submits bookings through the MyLikita relay (POST /v1/bookings) and polls status (GET /v1/bookings/:ref). Themeable via CSS variables.",
5
5
  "type": "module",
6
6
  "main": "dist/mylikita-booking-widget.cjs",
package/src/brand.js ADDED
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Brand assets for the widget.
3
+ *
4
+ * The MyLikita mark is embedded as a base64 data URI (not a network fetch) so
5
+ * the widget is fully self-contained — no extra HTTP request, works offline,
6
+ * and renders identically on every host page regardless of their origin.
7
+ * The vector mark stays razor-sharp at any size.
8
+ *
9
+ * The mark (frontend/public/icons/mylikita-mark.svg) is a 6-block pixel-art
10
+ * symbol: dark #0169DA top bar + upper arms, light #0498FB lower arms +
11
+ * bottom bar, with the hollow middle gap preserved.
12
+ *
13
+ * The data URI is base64 of that exact SVG (no URL-encoding pitfalls — the
14
+ * SVG contains `#` and `"` which would need escaping in a raw data URI).
15
+ */
16
+
17
+ /** Base64 data URI of the MyLikita mark — embed directly in an <img src>. */
18
+ export const MYLIKITA_MARK_DATA_URI =
19
+ 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0ODMgODA0IiB3aWR0aD0iNDgzIiBoZWlnaHQ9IjgwNCIgcm9sZT0iaW1nIiBhcmlhLWxhYmVsPSJNeUxpa2l0YSI+CiAgICA8cmVjdCB4PSIxNjEiIHk9IjAiIHdpZHRoPSIxNjEiIGhlaWdodD0iMTYxIiBmaWxsPSIjMDE2OURBIi8+CiAgICA8cmVjdCB4PSIwIiB5PSIxNjAiIHdpZHRoPSIxNjIiIGhlaWdodD0iMTYxIiBmaWxsPSIjMDE2OURBIi8+CiAgICA8cmVjdCB4PSIzMjEiIHk9IjE2MCIgd2lkdGg9IjE2MiIgaGVpZ2h0PSIxNjEiIGZpbGw9IiMwMTY5REEiLz4KICAgIDxyZWN0IHg9IjAiIHk9IjQ4MiIgd2lkdGg9IjE2MiIgaGVpZ2h0PSIxNjIiIGZpbGw9IiMwNDk4RkIiLz4KICAgIDxyZWN0IHg9IjMyMSIgeT0iNDgyIiB3aWR0aD0iMTYyIiBoZWlnaHQ9IjE2MiIgZmlsbD0iIzA0OThGQiIvPgogICAgPHJlY3QgeD0iMTYxIiB5PSI2NDMiIHdpZHRoPSIxNjEiIGhlaWdodD0iMTYxIiBmaWxsPSIjMDQ5OEZCIi8+Cjwvc3ZnPg==';
20
+
21
+ /**
22
+ * Build the widget header branding: the MyLikita mark + "MyLikita" wordmark.
23
+ * Returns a <div class="mylikita-widget__brand"> with the mark <img> and a
24
+ * wordmark <span> — styled by styles.js, themeable via the widget's CSS vars.
25
+ */
26
+ export function createBrandHeader() {
27
+ const header = document.createElement('div');
28
+ header.className = 'mylikita-widget__brand';
29
+ const img = document.createElement('img');
30
+ img.className = 'mylikita-widget__brand-mark';
31
+ img.src = MYLIKITA_MARK_DATA_URI;
32
+ img.alt = 'MyLikita';
33
+ img.width = 24;
34
+ img.height = 24;
35
+ const word = document.createElement('span');
36
+ word.className = 'mylikita-widget__brand-word';
37
+ word.textContent = 'MyLikita';
38
+ header.append(img, word);
39
+ return header;
40
+ }
package/src/index.js CHANGED
@@ -13,7 +13,7 @@
13
13
  * <script src="mylikita-booking-widget.min.js"></script>
14
14
  * <script>
15
15
  * MyLikitaBookingWidget.createBookingWidget(document.getElementById('booking'), {
16
- * relayUrl: 'https://api.mylikita.clinic',
16
+ * relayUrl: 'https://api.mylikita.com',
17
17
  * websiteKey: 'wk_...', // public client id — not a secret
18
18
  * facilityId: 'F1',
19
19
  * providers: [{ external_id: 'dr-khalil', label: 'Dr. Khalil' }],
@@ -28,3 +28,4 @@ export { createBookingWidget } from './widget.js';
28
28
  export { createBooking, fetchStatus, pollStatus, newExternalRef, fetchProviders } from './client.js';
29
29
  export { STATUS_COPY, statusCopy, TERMINAL_STATUSES } from './state.js';
30
30
  export { DEFAULT_THEME, resolveTheme } from './theme.js';
31
+ export { MYLIKITA_MARK_DATA_URI, createBrandHeader } from './brand.js';
package/src/styles.js CHANGED
@@ -31,6 +31,27 @@ export const STYLES = `
31
31
  .mylikita-widget *::before,
32
32
  .mylikita-widget *::after { box-sizing: border-box; }
33
33
 
34
+ .mylikita-widget__brand {
35
+ display: flex;
36
+ align-items: center;
37
+ gap: 8px;
38
+ margin-bottom: 14px;
39
+ padding-bottom: 12px;
40
+ border-bottom: 1px solid var(--mlw-border);
41
+ }
42
+ .mylikita-widget__brand-mark {
43
+ display: block;
44
+ flex: 0 0 auto;
45
+ border-radius: 5px;
46
+ box-shadow: 0 1px 3px rgba(15, 23, 42, .12);
47
+ }
48
+ .mylikita-widget__brand-word {
49
+ font-size: 14px;
50
+ font-weight: 700;
51
+ letter-spacing: .3px;
52
+ color: var(--mlw-muted);
53
+ }
54
+
34
55
  .mylikita-widget__title {
35
56
  font-size: 18px;
36
57
  font-weight: 700;
package/src/widget.js CHANGED
@@ -12,6 +12,7 @@ import { createBooking, fetchStatus, pollStatus, newExternalRef, fetchProviders
12
12
  import { statusCopy, TERMINAL_STATUSES } from './state.js';
13
13
  import { resolveTheme } from './theme.js';
14
14
  import { STYLES as STYLES_CSS } from './styles.js';
15
+ import { createBrandHeader } from './brand.js';
15
16
 
16
17
  const DEFAULT_TEXT = {
17
18
  title: 'Book an appointment',
@@ -53,6 +54,9 @@ export function createBookingWidget(element, options = {}) {
53
54
  // (GET /v1/providers) on mount and populate the doctor dropdown. The
54
55
  // static `providers` option, when given, always wins and skips the fetch.
55
56
  loadProviders: options.loadProviders === true && !(options.providers && options.providers.length),
57
+ // White-label flag: premium hosts can drop the MyLikita brand header.
58
+ // Defaults to true (brand shown); pass showBrand:false to hide it.
59
+ showBrand: options.showBrand !== false,
56
60
  pollIntervalMs: options.pollIntervalMs ?? 5000,
57
61
  maxTries: options.maxTries ?? 12,
58
62
  text: { ...DEFAULT_TEXT, ...(options.text || {}) },
@@ -84,6 +88,9 @@ export function createBookingWidget(element, options = {}) {
84
88
  const refKey = `mylikita_ref_${opts.facilityId}_${instanceId}`;
85
89
 
86
90
  // ── DOM construction ────────────────────────────────────────────────────
91
+ // The brand header is optional (white-label hosts pass showBrand:false);
92
+ // replaceChildren() ignores null, so the rest of the tree is unchanged.
93
+ const brand = opts.showBrand ? createBrandHeader() : null;
87
94
  const form = el('form', { className: 'mylikita-widget__form' });
88
95
 
89
96
  const title = el('h3', { className: 'mylikita-widget__title', text: t.title });
@@ -289,7 +296,7 @@ export function createBookingWidget(element, options = {}) {
289
296
  datetime.input.min = toLocalInputValue(new Date());
290
297
  }
291
298
 
292
- root.replaceChildren(title, subtitle, form, statusView);
299
+ root.replaceChildren(brand, title, subtitle, form, statusView);
293
300
 
294
301
  // ── async provider load (Phase C2/C3) ────────────────────────────────────
295
302
  // When loadProviders is on, fetch the facility's mapped doctors and fill the