brainerce 1.32.0 → 1.34.0

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
@@ -409,6 +409,33 @@ The `create-brainerce-store` scaffold ships ready-made components (`<Announcemen
409
409
 
410
410
  Full guide: [Core Integration "Content"](https://brainerce.com/docs/integration/core#content). Advanced patterns (channel scoping, custom fields, translations, admin writes): [Optional Features "Content"](https://brainerce.com/docs/integration/optional). Validation + sanitize rules: [Rules & Reference "Content"](https://brainerce.com/docs/integration/rules).
411
411
 
412
+ ### Blog
413
+
414
+ Merchants publish blog posts in **Content → Blog**. Storefronts choose their own URL scheme — render posts at `/blog/[slug]`, `/articles/[slug]`, or whatever fits the brand.
415
+
416
+ ```typescript
417
+ // List published posts (any SDK mode)
418
+ const { data: posts, meta } = await client.blog.getPosts({ page: 1, limit: 10 });
419
+
420
+ // Filter by category or tag
421
+ const { data: news } = await client.blog.getPosts({ category: 'news' });
422
+ const { data: tips } = await client.blog.getPosts({ tag: 'tutorial' });
423
+
424
+ // Fetch one by slug — returns null on 404
425
+ const post = await client.blog.getPost(params.slug);
426
+ if (!post) notFound();
427
+ ```
428
+
429
+ **Security**: `post.content` is merchant-authored HTML. Always sanitize before rendering:
430
+
431
+ ```tsx
432
+ import DOMPurify from 'isomorphic-dompurify';
433
+ const safeHtml = DOMPurify.sanitize(post.content);
434
+ return <div dangerouslySetInnerHTML={{ __html: safeHtml }} className="prose" />;
435
+ ```
436
+
437
+ **Scheduling**: A post is visible once `status === 'PUBLISHED'` and `publishedAt <= now()`. Set a future `publishedAt` when publishing to schedule — no cron needed.
438
+
412
439
  ---
413
440
 
414
441
  ## Common Mistakes to Avoid
@@ -4867,11 +4894,12 @@ import { BrainerceBot } from 'brainerce/bot';
4867
4894
 
4868
4895
  const bot = await BrainerceBot.mount({
4869
4896
  connectionId: 'vc_abc123',
4870
- // Optional: make the cards' "Add to cart" button use YOUR cart so your
4871
- // header count stays in sync. Return false to make the widget fall back
4872
- // to navigating to the product page.
4873
- onAddToCart: async ({ productId, quantity }) => {
4874
- await client.smartAddToCart({ productId, quantity });
4897
+ // Optional: make cart adds (card buttons, the in-card variant picker, and
4898
+ // bot-initiated adds) go through YOUR cart so your header count stays in
4899
+ // sync. variantId is null for simple products. Return false to make the
4900
+ // widget fall back to navigating to the product page.
4901
+ onAddToCart: async ({ productId, variantId, quantity }) => {
4902
+ await client.smartAddToCart({ productId, variantId: variantId ?? undefined, quantity });
4875
4903
  return true;
4876
4904
  },
4877
4905
  });
@@ -4879,9 +4907,11 @@ const bot = await BrainerceBot.mount({
4879
4907
  bot?.destroy(); // optional teardown
4880
4908
  ```
4881
4909
 
4882
- The widget persists an anonymous session in `localStorage`, restores conversations on revisit, streams answers, and shows product recommendation cards (image, price, add-to-cart / view buttons). It also includes a leave-a-message form that lands in the merchant's Inquiries inbox.
4910
+ The widget persists an anonymous session in `localStorage`, restores conversations on revisit, streams answers, and shows product recommendation cards (image, price, add-to-cart / view buttons). Multi-variant products get an **in-card variant picker** (attribute chips, live variant price/image) so shoppers can choose options and add without leaving the chat; shoppers can also just ask the assistant to add an item ("add the moka to my cart") and it happens through the same chain. A leave-a-message form lands in the merchant's Inquiries inbox, and zero-result searches feed the merchant's "unmet demand" analytics.
4911
+
4912
+ **Add to cart resolution** (never a dead button): the widget first calls your `onAddToCart` option; without one it dispatches a cancelable `brainerce:bot:add-to-cart` `CustomEvent` on `window` (`detail: { productId, variantId, quantity, connectionId }` — call `preventDefault()` after handling it); if nothing handles either, it navigates to the product page. Products too complex for in-chat picking (3+ attribute dimensions or 25+ variants) always navigate. Aside from your own cart handler, the widget is read-only by design — shoppers can never mutate the store through it.
4883
4913
 
4884
- **Add to cart resolution** (never a dead button): the widget first calls your `onAddToCart` option; without one it dispatches a cancelable `brainerce:bot:add-to-cart` `CustomEvent` on `window` (`detail: { productId, quantity, connectionId }` — call `preventDefault()` after handling it); if nothing handles either, it navigates to the product page. Products that need option selection always navigate. Aside from your own cart handler, the widget is read-only by design — shoppers can never mutate the store through it.
4914
+ Merchant-side display controls (Studio Storefront Bot): chat size (compact / full screen / shopper's choice), auto-open, position, and whether shoppers may expand the window (`allowExpand`).
4885
4915
 
4886
4916
  ## Webhooks
4887
4917
 
@@ -1,5 +1,5 @@
1
- "use strict";(()=>{var A="https://api.brainerce.com",R=new Set(["he","ar"]),$={en:{online:"Online",placeholder:"Ask anything\u2026",error:"Something went wrong \u2014 please try again.",leaveMessage:"Leave a message for the team",yourEmail:"Your email",yourMessage:"Your message",send:"Send",sent:"Thanks! The team will get back to you by email.",close:"Close",expand:"Expand",collapse:"Collapse",searching:"Searching the store\u2026",addToCart:"Add to cart",added:"Added",view:"View",chooseOptions:"View product",results:"From the store"},he:{online:"\u05DE\u05D7\u05D5\u05D1\u05E8",placeholder:"\u05E9\u05D0\u05DC\u05D5 \u05D0\u05D5\u05EA\u05D9 \u05D4\u05DB\u05DC\u2026",error:"\u05DE\u05E9\u05D4\u05D5 \u05D4\u05E9\u05EA\u05D1\u05E9 \u2014 \u05E0\u05E1\u05D5 \u05E9\u05D5\u05D1.",leaveMessage:"\u05D4\u05E9\u05D0\u05D9\u05E8\u05D5 \u05D4\u05D5\u05D3\u05E2\u05D4 \u05DC\u05E6\u05D5\u05D5\u05EA",yourEmail:"\u05D4\u05D0\u05D9\u05DE\u05D9\u05D9\u05DC \u05E9\u05DC\u05DB\u05DD",yourMessage:"\u05D4\u05D4\u05D5\u05D3\u05E2\u05D4 \u05E9\u05DC\u05DB\u05DD",send:"\u05E9\u05DC\u05D9\u05D7\u05D4",sent:"\u05EA\u05D5\u05D3\u05D4! \u05D4\u05E6\u05D5\u05D5\u05EA \u05D9\u05D7\u05D6\u05D5\u05E8 \u05D0\u05DC\u05D9\u05DB\u05DD \u05D1\u05DE\u05D9\u05D9\u05DC.",close:"\u05E1\u05D2\u05D9\u05E8\u05D4",expand:"\u05D4\u05E8\u05D7\u05D1\u05D4",collapse:"\u05DB\u05D9\u05D5\u05D5\u05E5",searching:"\u05DE\u05D7\u05E4\u05E9 \u05D1\u05D7\u05E0\u05D5\u05EA\u2026",addToCart:"\u05D4\u05D5\u05E1\u05E4\u05D4 \u05DC\u05E1\u05DC",added:"\u05E0\u05D5\u05E1\u05E3",view:"\u05E6\u05E4\u05D9\u05D9\u05D4",chooseOptions:"\u05DC\u05E6\u05E4\u05D5\u05EA \u05D1\u05DE\u05D5\u05E6\u05E8",results:"\u05DE\u05EA\u05D5\u05DA \u05D4\u05D7\u05E0\u05D5\u05EA"}},U={chat:'<svg viewBox="0 0 24 24" fill="none"><path d="M12 3C7.03 3 3 6.58 3 11c0 2.04.86 3.9 2.28 5.32-.15 1.23-.62 2.39-1.1 3.21-.13.23.05.52.31.47 1.56-.27 3.07-.93 4.13-1.62A10.6 10.6 0 0 0 12 19c4.97 0 9-3.58 9-8s-4.03-8-9-8Z" fill="currentColor"/></svg>',close:'<svg viewBox="0 0 24 24" fill="none"><path d="M6 6l12 12M18 6L6 18" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>',expand:'<svg viewBox="0 0 24 24" fill="none"><path d="M14 4h6v6M10 20H4v-6M20 4l-7 7M4 20l7-7" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>',collapse:'<svg viewBox="0 0 24 24" fill="none"><path d="M20 10h-6V4M4 14h6v6M20 4l-6 6M4 20l6-6" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>',mail:'<svg viewBox="0 0 24 24" fill="none"><rect x="3.5" y="5.5" width="17" height="13" rx="2.5" stroke="currentColor" stroke-width="1.7"/><path d="m4.5 7.5 7.5 5.5 7.5-5.5" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg>',send:'<svg viewBox="0 0 24 24" fill="none"><path d="M4.4 11.2 19 4.6c.7-.3 1.4.4 1.1 1.1l-6.6 14.6c-.3.7-1.3.6-1.5-.1l-1.7-5.4a1 1 0 0 0-.6-.6l-5.4-1.7c-.7-.2-.8-1.2-.1-1.5Z" fill="currentColor"/></svg>',cart:'<svg viewBox="0 0 24 24" fill="none"><path d="M3 4h2l2.4 11.2A2 2 0 0 0 9.36 17H17.5a2 2 0 0 0 1.95-1.55L21 8H6" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/><circle cx="10" cy="20.5" r="1.4" fill="currentColor"/><circle cx="17" cy="20.5" r="1.4" fill="currentColor"/></svg>',check:'<svg viewBox="0 0 24 24" fill="none"><path d="m5 12.5 4.5 4.5L19 7.5" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>',arrow:'<svg viewBox="0 0 24 24" fill="none"><path d="M7 17 17 7M9 7h8v8" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>'},z=typeof DOMParser<"u"?new DOMParser:null;function f(b){let e=(U[b]??U.chat).replace("<svg ",'<svg xmlns="http://www.w3.org/2000/svg" '),t=z?.parseFromString(e,"image/svg+xml")?.documentElement;return!t||t.nodeName==="parsererror"?document.createTextNode(""):(t.setAttribute("aria-hidden","true"),t.setAttribute("class","bb-ic"),t)}function O(b){let e=new Uint8Array(16);crypto.getRandomValues(e);let n=btoa(String.fromCharCode(...e)).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"");return`${b}${n}`}function T(b){return/^\/(?!\/)/.test(b)||/^https?:\/\//i.test(b)}var S=/\*\*([^*\n]+)\*\*|\[([^\]\n]+)\]\(([^)\s]+)\)/g;function H(b,e){let n=0;S.lastIndex=0;for(let t=S.exec(e);t;t=S.exec(e)){if(t.index>n&&b.appendChild(document.createTextNode(e.slice(n,t.index))),t[1]!==void 0){let i=document.createElement("strong");i.textContent=t[1],b.appendChild(i)}else if(T(t[3])){let i=document.createElement("a");i.href=t[3],i.target="_blank",i.rel="noopener noreferrer",i.textContent=t[2],b.appendChild(i)}else b.appendChild(document.createTextNode(t[2]));n=t.index+t[0].length}n<e.length&&b.appendChild(document.createTextNode(e.slice(n)))}function B(b,e){b.replaceChildren();let n=e.split(`
2
- `),t=null,i=null;for(let o of n){let r=/^\s*[-•*]\s+(.*)$/.exec(o);if(r){i=null,t||(t=document.createElement("ul"),b.appendChild(t));let d=document.createElement("li");H(d,r[1]),t.appendChild(d);continue}if(t=null,!o.trim()){i=null;continue}i?i.appendChild(document.createElement("br")):(i=document.createElement("p"),b.appendChild(i)),H(i,o)}}var N=class b{constructor(e){this.settings={enabled:!1};this.locale="en";this.sessionId=null;this.conversationId=null;this.busy=!1;this.opened=!1;this.expanded=!1;this.destroyed=!1;this.pendingText="";this.cardsRow=null;this.cardIds=new Set;this.connectionId=e.connectionId,this.baseUrl=(e.baseUrl||A).replace(/\/$/,""),this.storageKey=`brainerce-bot:${this.connectionId}`,this.onAddToCart=e.onAddToCart}static async mount(e){if(!e?.connectionId)return console.warn("[BrainerceBot] connectionId is required"),null;let n=new b(e);return await n.boot(e.target??document.body)?n:null}destroy(){this.destroyed=!0,this.host?.remove()}async boot(e){try{let n=await fetch(`${this.baseUrl}/api/storefront-bot/${encodeURIComponent(this.connectionId)}/settings`);if(!n.ok)return!1;this.settings=await n.json()}catch{return!1}return this.settings.enabled?(this.locale=this.settings.languages?.[0]??"en",this.restoreIds(),this.render(e),this.settings.displayMode==="auto_open"&&setTimeout(()=>!this.destroyed&&this.open(),3e3),!0):!1}t(e){return($[this.locale]??$.en)[e]??$.en[e]??e}restoreIds(){try{let e=localStorage.getItem(this.storageKey);if(e){let n=JSON.parse(e);this.sessionId=n.sessionId??null,this.conversationId=n.conversationId??null}}catch{}}persistIds(){try{localStorage.setItem(this.storageKey,JSON.stringify({sessionId:this.sessionId,conversationId:this.conversationId}))}catch{}}css(e,n,t){let i=this.settings.bubbleShape==="square",o=i?"14px":"22px",r=i?"8px":"15px",d="5px",a=this.settings.displayMode??"floating",l=a==="side_rail",s=a==="full_screen";return`
1
+ "use strict";(()=>{var R="https://api.brainerce.com",O=new Set(["he","ar"]),S={en:{online:"Online",placeholder:"Ask anything\u2026",error:"Something went wrong \u2014 please try again.",leaveMessage:"Leave a message for the team",yourEmail:"Your email",yourMessage:"Your message",send:"Send",sent:"Thanks! The team will get back to you by email.",close:"Close",expand:"Expand",collapse:"Collapse",searching:"Searching the store\u2026",addToCart:"Add to cart",added:"Added",view:"View",chooseOptions:"View product",results:"From the store",poweredBy:"Powered by Brainerce",addFailed:"I couldn\u2019t add that to the cart \u2014 try the button on the product card."},he:{online:"\u05DE\u05D7\u05D5\u05D1\u05E8",placeholder:"\u05E9\u05D0\u05DC\u05D5 \u05D0\u05D5\u05EA\u05D9 \u05D4\u05DB\u05DC\u2026",error:"\u05DE\u05E9\u05D4\u05D5 \u05D4\u05E9\u05EA\u05D1\u05E9 \u2014 \u05E0\u05E1\u05D5 \u05E9\u05D5\u05D1.",leaveMessage:"\u05D4\u05E9\u05D0\u05D9\u05E8\u05D5 \u05D4\u05D5\u05D3\u05E2\u05D4 \u05DC\u05E6\u05D5\u05D5\u05EA",yourEmail:"\u05D4\u05D0\u05D9\u05DE\u05D9\u05D9\u05DC \u05E9\u05DC\u05DB\u05DD",yourMessage:"\u05D4\u05D4\u05D5\u05D3\u05E2\u05D4 \u05E9\u05DC\u05DB\u05DD",send:"\u05E9\u05DC\u05D9\u05D7\u05D4",sent:"\u05EA\u05D5\u05D3\u05D4! \u05D4\u05E6\u05D5\u05D5\u05EA \u05D9\u05D7\u05D6\u05D5\u05E8 \u05D0\u05DC\u05D9\u05DB\u05DD \u05D1\u05DE\u05D9\u05D9\u05DC.",close:"\u05E1\u05D2\u05D9\u05E8\u05D4",expand:"\u05D4\u05E8\u05D7\u05D1\u05D4",collapse:"\u05DB\u05D9\u05D5\u05D5\u05E5",searching:"\u05DE\u05D7\u05E4\u05E9 \u05D1\u05D7\u05E0\u05D5\u05EA\u2026",addToCart:"\u05D4\u05D5\u05E1\u05E4\u05D4 \u05DC\u05E1\u05DC",added:"\u05E0\u05D5\u05E1\u05E3",view:"\u05E6\u05E4\u05D9\u05D9\u05D4",chooseOptions:"\u05DC\u05E6\u05E4\u05D5\u05EA \u05D1\u05DE\u05D5\u05E6\u05E8",results:"\u05DE\u05EA\u05D5\u05DA \u05D4\u05D7\u05E0\u05D5\u05EA",poweredBy:"\u05DE\u05D5\u05E4\u05E2\u05DC \u05E2\u05DC \u05D9\u05D3\u05D9 Brainerce",addFailed:"\u05DC\u05D0 \u05D4\u05E6\u05DC\u05D7\u05EA\u05D9 \u05DC\u05D4\u05D5\u05E1\u05D9\u05E3 \u05DC\u05E1\u05DC \u2014 \u05E0\u05E1\u05D5 \u05D3\u05E8\u05DA \u05D4\u05DB\u05E4\u05EA\u05D5\u05E8 \u05D1\u05DB\u05E8\u05D8\u05D9\u05E1 \u05D4\u05DE\u05D5\u05E6\u05E8."}},H={chat:'<svg viewBox="0 0 24 24" fill="none"><path d="M12 3C7.03 3 3 6.58 3 11c0 2.04.86 3.9 2.28 5.32-.15 1.23-.62 2.39-1.1 3.21-.13.23.05.52.31.47 1.56-.27 3.07-.93 4.13-1.62A10.6 10.6 0 0 0 12 19c4.97 0 9-3.58 9-8s-4.03-8-9-8Z" fill="currentColor"/></svg>',close:'<svg viewBox="0 0 24 24" fill="none"><path d="M6 6l12 12M18 6L6 18" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>',expand:'<svg viewBox="0 0 24 24" fill="none"><path d="M14 4h6v6M10 20H4v-6M20 4l-7 7M4 20l7-7" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>',collapse:'<svg viewBox="0 0 24 24" fill="none"><path d="M20 10h-6V4M4 14h6v6M20 4l-6 6M4 20l6-6" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>',mail:'<svg viewBox="0 0 24 24" fill="none"><rect x="3.5" y="5.5" width="17" height="13" rx="2.5" stroke="currentColor" stroke-width="1.7"/><path d="m4.5 7.5 7.5 5.5 7.5-5.5" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg>',send:'<svg viewBox="0 0 24 24" fill="none"><path d="M4.4 11.2 19 4.6c.7-.3 1.4.4 1.1 1.1l-6.6 14.6c-.3.7-1.3.6-1.5-.1l-1.7-5.4a1 1 0 0 0-.6-.6l-5.4-1.7c-.7-.2-.8-1.2-.1-1.5Z" fill="currentColor"/></svg>',cart:'<svg viewBox="0 0 24 24" fill="none"><path d="M3 4h2l2.4 11.2A2 2 0 0 0 9.36 17H17.5a2 2 0 0 0 1.95-1.55L21 8H6" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/><circle cx="10" cy="20.5" r="1.4" fill="currentColor"/><circle cx="17" cy="20.5" r="1.4" fill="currentColor"/></svg>',check:'<svg viewBox="0 0 24 24" fill="none"><path d="m5 12.5 4.5 4.5L19 7.5" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>',arrow:'<svg viewBox="0 0 24 24" fill="none"><path d="M7 17 17 7M9 7h8v8" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>',brand:'<svg viewBox="0 0 24 24"><defs><linearGradient id="bb-brand-g" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#34d399"/><stop offset="1" stop-color="#8b5cf6"/></linearGradient></defs><rect x="1.5" y="1.5" width="21" height="21" rx="6" fill="url(#bb-brand-g)"/><text x="12" y="17" text-anchor="middle" font-family="ui-sans-serif,system-ui,sans-serif" font-size="14" font-weight="700" fill="#fff">B</text></svg>'},z=typeof DOMParser<"u"?new DOMParser:null;function m(b){let e=(H[b]??H.chat).replace("<svg ",'<svg xmlns="http://www.w3.org/2000/svg" '),t=z?.parseFromString(e,"image/svg+xml")?.documentElement;return!t||t.nodeName==="parsererror"?document.createTextNode(""):(t.setAttribute("aria-hidden","true"),t.setAttribute("class","bb-ic"),t)}function P(b){let e=new Uint8Array(16);crypto.getRandomValues(e);let n=btoa(String.fromCharCode(...e)).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"");return`${b}${n}`}function T(b){return/^\/(?!\/)/.test(b)||/^https?:\/\//i.test(b)}var $=/\*\*([^*\n]+)\*\*|\[([^\]\n]+)\]\(([^)\s]+)\)/g;function A(b,e){let n=0;$.lastIndex=0;for(let t=$.exec(e);t;t=$.exec(e)){if(t.index>n&&b.appendChild(document.createTextNode(e.slice(n,t.index))),t[1]!==void 0){let i=document.createElement("strong");i.textContent=t[1],b.appendChild(i)}else if(T(t[3])){let i=document.createElement("a");i.href=t[3],i.target="_blank",i.rel="noopener noreferrer",i.textContent=t[2],b.appendChild(i)}else b.appendChild(document.createTextNode(t[2]));n=t.index+t[0].length}n<e.length&&b.appendChild(document.createTextNode(e.slice(n)))}function U(b,e){b.replaceChildren();let n=e.split(`
2
+ `),t=null,i=null;for(let o of n){let r=/^\s*[-•*]\s+(.*)$/.exec(o);if(r){i=null,t||(t=document.createElement("ul"),b.appendChild(t));let d=document.createElement("li");A(d,r[1]),t.appendChild(d);continue}if(t=null,!o.trim()){i=null;continue}i?i.appendChild(document.createElement("br")):(i=document.createElement("p"),b.appendChild(i)),A(i,o)}}var M=class b{constructor(e){this.settings={enabled:!1};this.locale="en";this.sessionId=null;this.conversationId=null;this.busy=!1;this.opened=!1;this.expanded=!1;this.destroyed=!1;this.pendingText="";this.cardsRow=null;this.cardIds=new Set;this.prevBodyOverflow=null;this.connectionId=e.connectionId,this.baseUrl=(e.baseUrl||R).replace(/\/$/,""),this.storageKey=`brainerce-bot:${this.connectionId}`,this.onAddToCart=e.onAddToCart}static async mount(e){if(!e?.connectionId)return console.warn("[BrainerceBot] connectionId is required"),null;let n=new b(e);return await n.boot(e.target??document.body)?n:null}destroy(){this.destroyed=!0,this.prevBodyOverflow!==null&&(document.body.style.overflow=this.prevBodyOverflow,this.prevBodyOverflow=null),this.host?.remove()}async boot(e){try{let n=await fetch(`${this.baseUrl}/api/storefront-bot/${encodeURIComponent(this.connectionId)}/settings`);if(!n.ok)return!1;this.settings=await n.json()}catch{return!1}return this.settings.enabled?(this.locale=this.settings.languages?.[0]??"en",this.restoreIds(),this.render(e),this.settings.displayMode==="auto_open"&&setTimeout(()=>!this.destroyed&&this.open(),3e3),!0):!1}t(e){return(S[this.locale]??S.en)[e]??S.en[e]??e}restoreIds(){try{let e=localStorage.getItem(this.storageKey);if(e){let n=JSON.parse(e);this.sessionId=n.sessionId??null,this.conversationId=n.conversationId??null}}catch{}}persistIds(){try{localStorage.setItem(this.storageKey,JSON.stringify({sessionId:this.sessionId,conversationId:this.conversationId}))}catch{}}css(e,n,t){let i=this.settings.bubbleShape==="square",o=i?"14px":"22px",r=i?"8px":"15px",d="5px",a=this.settings.displayMode??"floating",l=a==="side_rail",s=a==="full_screen";return`
3
3
  :host { all: initial; }
4
4
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0;
5
5
  font-family: -apple-system, "SF Pro Text", "Segoe UI Variable Text", "Segoe UI", system-ui, "Helvetica Neue", sans-serif;
@@ -237,6 +237,15 @@
237
237
  .bb-send .bb-ic { width: 17px; height: 17px; ${n==="rtl"?"transform: scaleX(-1);":""} }
238
238
  .bb-send:hover { filter: brightness(1.08); transform: scale(1.06); }
239
239
  .bb-send:disabled { opacity: .4; pointer-events: none; }
240
- `}render(e){let n=this.settings.accentColor||"#6366F1",t=R.has(this.locale)?"rtl":"ltr",i=this.settings.position==="start"?"left":"right",o=t==="rtl"?i==="left"?"right":"left":i,r=this.settings.displayName||"Assistant";this.host=document.createElement("div"),this.host.setAttribute("data-brainerce-bot",this.connectionId),this.root=this.host.attachShadow({mode:"open"});let d=document.createElement("style");d.textContent=this.css(n,t,o),this.root.appendChild(d);let a=document.createElement("div");a.className="bb",this.root.appendChild(a);let l=document.createElement("div");l.className="bb-scrim",l.addEventListener("click",()=>{this.expanded?this.toggleExpand():this.close()}),a.appendChild(l),this.windowEl=document.createElement("div"),this.windowEl.className="bb-window",a.appendChild(this.windowEl);let s=document.createElement("div");s.className="bb-header";let h=document.createElement("span");if(h.className="bb-avatar",this.settings.avatarUrl&&T(this.settings.avatarUrl)){let p=document.createElement("img");p.src=this.settings.avatarUrl,p.alt="",h.appendChild(p)}else h.appendChild(document.createTextNode(r.charAt(0).toUpperCase()));let u=document.createElement("span");u.className="bb-head-main";let g=document.createElement("span");g.className="bb-name",g.textContent=r;let v=document.createElement("span");v.className="bb-status",v.textContent=this.t("online"),u.appendChild(g),u.appendChild(v);let x=document.createElement("span");x.className="bb-actions",(this.settings.displayMode??"floating")!=="full_screen"&&this.settings.allowExpand!==!1&&(this.expandBtn=this.iconButton("expand",this.t("expand"),()=>this.toggleExpand()),x.appendChild(this.expandBtn)),x.appendChild(this.iconButton("mail",this.t("leaveMessage"),()=>this.toggleEscalation())),x.appendChild(this.iconButton("close",this.t("close"),()=>this.close())),s.appendChild(h),s.appendChild(u),s.appendChild(x),this.windowEl.appendChild(s),this.messagesEl=document.createElement("div"),this.messagesEl.className="bb-msgs",this.windowEl.appendChild(this.messagesEl),this.chipsEl=document.createElement("div"),this.chipsEl.className="bb-chips";for(let p of this.settings.starterQuestions??[]){let I=document.createElement("button");I.className="bb-chip",I.textContent=p,I.addEventListener("click",()=>this.send(p)),this.chipsEl.appendChild(I)}this.windowEl.appendChild(this.chipsEl);let c=document.createElement("div");c.className="bb-esc";let E=document.createElement("span");E.className="bb-esc-title",E.textContent=this.t("leaveMessage");let y=document.createElement("input");y.type="email",y.name="email",y.placeholder=this.t("yourEmail");let C=document.createElement("textarea");C.name="message",C.rows=2,C.placeholder=this.t("yourMessage");let k=document.createElement("button");k.type="button",k.className="bb-esc-send",k.textContent=this.t("send"),k.addEventListener("click",()=>this.submitEscalation(c)),c.appendChild(E),c.appendChild(y),c.appendChild(C),c.appendChild(k),this.windowEl.appendChild(c);let m=document.createElement("div");if(m.className="bb-composer",this.inputEl=document.createElement("textarea"),this.inputEl.className="bb-input",this.inputEl.rows=1,this.inputEl.placeholder=this.t("placeholder"),this.inputEl.addEventListener("keydown",p=>{p.key==="Enter"&&!p.shiftKey&&(p.preventDefault(),this.send(this.inputEl?.value??""))}),this.inputEl.addEventListener("input",()=>this.syncSendState()),this.sendBtn=document.createElement("button"),this.sendBtn.className="bb-send",this.sendBtn.disabled=!0,this.sendBtn.setAttribute("aria-label",this.t("send")),this.sendBtn.appendChild(f("send")),this.sendBtn.addEventListener("click",()=>this.send(this.inputEl?.value??"")),m.appendChild(this.inputEl),m.appendChild(this.sendBtn),this.windowEl.appendChild(m),this.launcherEl=document.createElement("button"),this.launcherEl.className="bb-launcher",this.launcherEl.setAttribute("aria-label",r),this.settings.avatarUrl&&T(this.settings.avatarUrl)){let p=document.createElement("img");p.src=this.settings.avatarUrl,p.alt="",this.launcherEl.appendChild(p)}else{let p=f("chat");p.classList.add("bb-l-chat"),this.launcherEl.appendChild(p)}let L=f("close");L.classList.add("bb-l-close"),this.launcherEl.appendChild(L),this.launcherEl.addEventListener("click",()=>this.opened?this.close():this.open()),a.appendChild(this.launcherEl),e.appendChild(this.host)}iconButton(e,n,t){let i=document.createElement("button");return i.className="bb-iconbtn",i.title=n,i.setAttribute("aria-label",n),i.appendChild(f(e)),i.addEventListener("click",t),i}syncSendState(){this.sendBtn&&(this.sendBtn.disabled=!(this.inputEl?.value??"").trim()||this.busy)}open(){if(!this.windowEl||this.opened)return;this.opened=!0;let e=this.root?.querySelector(".bb");e?.classList.add("open");let n=(this.settings.displayMode??"floating")==="full_screen"||window.innerWidth<=520;e?.classList.toggle("big",n||this.expanded),this.messagesEl&&this.messagesEl.childElementCount===0&&this.primeThread(),this.inputEl?.focus()}close(){this.opened=!1;let e=this.root?.querySelector(".bb");e?.classList.remove("open"),this.expanded||e?.classList.remove("big")}toggleExpand(){if(this.expanded=!this.expanded,this.root?.querySelector(".bb")?.classList.toggle("expanded",this.expanded),this.root?.querySelector(".bb")?.classList.toggle("big",this.expanded),this.expandBtn){this.expandBtn.replaceChildren(f(this.expanded?"collapse":"expand"));let e=this.t(this.expanded?"collapse":"expand");this.expandBtn.title=e,this.expandBtn.setAttribute("aria-label",e)}}async primeThread(){if(this.conversationId&&this.sessionId)try{let e=await fetch(`${this.baseUrl}/api/storefront-bot/${encodeURIComponent(this.connectionId)}/conversations/${encodeURIComponent(this.conversationId)}?limit=50`,{headers:{"X-Bot-Session":this.sessionId}});if(e.ok){let n=await e.json();for(let t of n.data){let i=this.appendMessage(t.role==="assistant"?"bot":"user","");B(i,t.content)}if(n.data.length>0){this.chipsEl?.remove();return}}else this.conversationId=null,this.sessionId=null,this.persistIds()}catch{}if(this.settings.greeting){let e=this.appendMessage("bot","");B(e,this.settings.greeting)}}async send(e){let n=e.trim();if(!n||this.busy)return;this.busy=!0,this.inputEl&&(this.inputEl.value=""),this.syncSendState(),this.chipsEl?.remove(),this.appendMessage("user",n);let t=this.appendTyping();this.pendingText="",this.cardsRow=null,this.cardIds=new Set;let i=null;try{let o=await fetch(`${this.baseUrl}/api/storefront-bot/${encodeURIComponent(this.connectionId)}/chat`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({message:n,turnId:O("trn_"),...this.conversationId?{conversationId:this.conversationId}:{},...this.sessionId?{anonymousSessionId:this.sessionId}:{},locale:this.locale})});if(!o.ok||!o.body)throw new Error(`chat failed (${o.status})`);let r=o.body.getReader(),d=new TextDecoder,a="";for(;;){let{value:l,done:s}=await r.read();if(s)break;a+=d.decode(l,{stream:!0});let h;for(;(h=a.indexOf(`
241
240
 
242
- `))>=0;){let u=a.slice(0,h);if(a=a.slice(h+2),!u.startsWith("data: "))continue;let g;try{g=JSON.parse(u.slice(6))}catch{continue}i=this.handleFrame(g,t,i)}}i&&this.pendingText&&B(i,this.pendingText)}catch{this.appendMessage("err",this.t("error"))}finally{t.remove(),this.busy=!1,this.syncSendState()}}handleFrame(e,n,t){switch(e.type){case"connected":return this.conversationId=e.conversationId||this.conversationId,this.sessionId=e.anonymousSessionId||this.sessionId,this.persistIds(),t;case"token":return t||(n.remove(),t=this.appendMessage("bot","")),this.pendingText+=e.text,t.textContent=this.pendingText,this.scrollDown(),t;case"tool":return n.classList.toggle("searching",e.status==="running"),t;case"card":return this.appendCard(e.card),t;case"error":return this.appendMessage("err",e.message||this.t("error")),t;case"done":default:return t}}appendCard(e){if(!this.messagesEl||this.cardIds.has(e.productId))return;if(this.cardIds.add(e.productId),!this.cardsRow){let s=document.createElement("div");s.className="bb-shelf";let h=document.createElement("div");h.className="bb-shelf-cap",h.textContent=this.t("results"),this.cardsRow=document.createElement("div"),this.cardsRow.className="bb-cards",s.appendChild(h),s.appendChild(this.cardsRow),this.messagesEl.appendChild(s)}let n=T(e.url)?e.url:null,t=document.createElement("div");t.className="bb-card";let i=()=>{this.beacon(e.botRef),n&&(window.location.href=n)},o=document.createElement("span");if(o.className="bb-card-img",e.imageUrl&&T(e.imageUrl)){let s=document.createElement("img");s.src=e.imageUrl,s.alt="",s.loading="lazy",o.appendChild(s)}else o.appendChild(f("cart"));o.addEventListener("click",i),t.appendChild(o);let r=document.createElement("span");r.className="bb-card-body";let d=document.createElement("span");d.className="bb-card-title",d.textContent=e.title,d.addEventListener("click",i);let a=document.createElement("span");a.className="bb-card-price",a.textContent=e.price.formatted,r.appendChild(d),r.appendChild(a);let l=document.createElement("span");if(l.className="bb-card-cta",e.requiresOptions){let s=document.createElement("button");s.className="bb-btn bb-btn-add",s.setAttribute("aria-label",this.t("addToCart")),s.appendChild(f("cart")),e.variants?.length?s.addEventListener("click",()=>this.togglePicker(r,e,o)):s.addEventListener("click",i),l.appendChild(s)}else{let s=document.createElement("button");s.className="bb-btn bb-btn-add",s.appendChild(f("cart")),s.appendChild(document.createTextNode(this.t("addToCart"))),s.addEventListener("click",()=>void this.addToCart(e,s));let h=document.createElement("button");h.className="bb-btn bb-btn-ghost",h.appendChild(document.createTextNode(this.t("view"))),h.addEventListener("click",i),l.appendChild(s),l.appendChild(h)}r.appendChild(l),t.appendChild(r),this.cardsRow.appendChild(t),this.scrollDown()}togglePicker(e,n,t){let i=e.parentElement,o=e.querySelector(".bb-pick");if(o){o.remove(),i?.classList.remove("picking");return}i?.classList.add("picking");let r=n.variants??[],d=[];for(let u of r)for(let g of Object.keys(u.attributes))d.includes(g)||d.push(g);let a={},l=document.createElement("span");l.className="bb-pick";let s=()=>r.find(u=>d.every(g=>a[g]&&u.attributes[g]===a[g]))??null,h=()=>{l.replaceChildren();for(let c of d){let E=document.createElement("span");E.className="bb-pick-key",E.textContent=c;let y=document.createElement("span");y.className="bb-pick-vals";let C=new Set;for(let k of r){let m=k.attributes[c];if(!m||C.has(m))continue;C.add(m);let L=r.some(I=>I.attributes[c]===m&&d.every(M=>M===c||!a[M]||I.attributes[M]===a[M])),p=document.createElement("button");p.className=`bb-chipv${a[c]===m?" sel":""}${L?"":" off"}`,p.textContent=m,p.addEventListener("click",()=>{a[c]===m?delete a[c]:a[c]=m,h()}),y.appendChild(p)}l.appendChild(E),l.appendChild(y)}let u=document.createElement("button");u.className="bb-pick-close",u.setAttribute("aria-label",this.t("close")),u.appendChild(f("close")),u.addEventListener("click",()=>{l.remove(),i?.classList.remove("picking")}),l.appendChild(u);let g=s(),v=document.createElement("span");v.className="bb-pick-foot";let x=document.createElement("span");x.className="bb-pick-price",x.textContent=g?g.price.formatted:"";let w=document.createElement("button");if(w.className="bb-btn bb-btn-add",w.appendChild(f("cart")),w.appendChild(document.createTextNode(this.t("addToCart"))),g||(w.disabled=!0),w.addEventListener("click",()=>{let c=s();c&&this.addToCart(n,w,c.id)}),v.appendChild(x),v.appendChild(w),l.appendChild(v),g?.imageUrl&&T(g.imageUrl)){let c=t.querySelector("img");c&&(c.src=g.imageUrl)}this.scrollDown()};h(),e.appendChild(l),this.scrollDown()}async addToCart(e,n,t){this.beacon(e.botRef),n.dataset.state="busy";let i=!1;try{if(this.onAddToCart)i=await this.onAddToCart({productId:e.productId,variantId:t??null,quantity:1})!==!1;else{let o=new CustomEvent("brainerce:bot:add-to-cart",{detail:{productId:e.productId,variantId:t??null,quantity:1,connectionId:this.connectionId},cancelable:!0,bubbles:!0,composed:!0});i=!window.dispatchEvent(o)}}catch{i=!1}i?(n.dataset.state="done",n.replaceChildren(f("check"),document.createTextNode(this.t("added"))),setTimeout(()=>{!this.destroyed&&n.isConnected&&(delete n.dataset.state,n.replaceChildren(f("cart"),document.createTextNode(this.t("addToCart"))))},2200)):(delete n.dataset.state,T(e.url)&&(window.location.href=e.url))}beacon(e){try{navigator.sendBeacon?.(`${this.baseUrl}/api/storefront-bot/attribution/click`,new Blob([JSON.stringify({botRef:e})],{type:"application/json"}))}catch{}}toggleEscalation(){this.root?.querySelector(".bb-esc")?.classList.toggle("open")}async submitEscalation(e){let n=e.querySelector('input[name="email"]')?.value.trim(),t=e.querySelector('textarea[name="message"]')?.value.trim();if(!(!n||!t||!this.conversationId||!this.sessionId))try{if((await fetch(`${this.baseUrl}/api/storefront-bot/${encodeURIComponent(this.connectionId)}/escalate`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({email:n,message:t,conversationId:this.conversationId,anonymousSessionId:this.sessionId,locale:this.locale})})).ok){let o=document.createElement("span");o.className="bb-esc-note",o.appendChild(f("check")),o.appendChild(document.createTextNode(this.t("sent"))),e.replaceChildren(o)}}catch{}}appendMessage(e,n){let t=document.createElement("div");return t.className=`bb-msg ${e}`,t.setAttribute("dir","auto"),t.textContent=n,this.messagesEl?.appendChild(t),this.scrollDown(),t}appendTyping(){let e=document.createElement("div");e.className="bb-typing";let n=document.createElement("span");n.className="bb-dots";for(let i=0;i<3;i++)n.appendChild(document.createElement("i"));let t=document.createElement("span");return t.className="bb-tool",t.textContent=this.t("searching"),e.appendChild(n),e.appendChild(t),this.messagesEl?.appendChild(e),this.scrollDown(),e}scrollDown(){this.messagesEl&&(this.messagesEl.scrollTop=this.messagesEl.scrollHeight)}};(()=>{let b=document.currentScript,e=b?.dataset.connectionId;if(!e){console.warn("[BrainerceBot] missing data-connection-id on the bot.js script tag");return}let n=b?.dataset.apiBase||void 0,t=()=>void N.mount({connectionId:e,baseUrl:n});document.readyState==="loading"?document.addEventListener("DOMContentLoaded",t,{once:!0}):t()})();})();
241
+ /* Powered-by footer */
242
+ .bb-foot { flex-shrink: 0; display: flex; justify-content: center; align-items: center;
243
+ padding: 5px 12px 7px; background: #fff; }
244
+ .bb-foot a { display: inline-flex; align-items: center; gap: 5px; font-size: 10.5px;
245
+ font-weight: 500; color: #aab0bd; text-decoration: none; letter-spacing: .005em;
246
+ transition: color .12s ease; }
247
+ .bb-foot a:hover { color: #6b7280; }
248
+ .bb-foot .bb-ic { width: 13px; height: 13px; border-radius: 4px; }
249
+ `}render(e){let n=this.settings.accentColor||"#6366F1",t=O.has(this.locale)?"rtl":"ltr",i=this.settings.position==="start"?"left":"right",o=t==="rtl"?i==="left"?"right":"left":i,r=this.settings.displayName||"Assistant";this.host=document.createElement("div"),this.host.setAttribute("data-brainerce-bot",this.connectionId),this.root=this.host.attachShadow({mode:"open"});let d=document.createElement("style");d.textContent=this.css(n,t,o),this.root.appendChild(d);let a=document.createElement("div");a.className="bb",this.root.appendChild(a);let l=document.createElement("div");l.className="bb-scrim",l.addEventListener("click",()=>{this.expanded?this.toggleExpand():this.close()}),a.appendChild(l),this.windowEl=document.createElement("div"),this.windowEl.className="bb-window",a.appendChild(this.windowEl);let s=document.createElement("div");s.className="bb-header";let h=document.createElement("span");if(h.className="bb-avatar",this.settings.avatarUrl&&T(this.settings.avatarUrl)){let p=document.createElement("img");p.src=this.settings.avatarUrl,p.alt="",h.appendChild(p)}else h.appendChild(document.createTextNode(r.charAt(0).toUpperCase()));let u=document.createElement("span");u.className="bb-head-main";let g=document.createElement("span");g.className="bb-name",g.textContent=r;let w=document.createElement("span");w.className="bb-status",w.textContent=this.t("online"),u.appendChild(g),u.appendChild(w);let v=document.createElement("span");v.className="bb-actions",(this.settings.displayMode??"floating")!=="full_screen"&&this.settings.allowExpand!==!1&&(this.expandBtn=this.iconButton("expand",this.t("expand"),()=>this.toggleExpand()),v.appendChild(this.expandBtn)),v.appendChild(this.iconButton("mail",this.t("leaveMessage"),()=>this.toggleEscalation())),v.appendChild(this.iconButton("close",this.t("close"),()=>this.close())),s.appendChild(h),s.appendChild(u),s.appendChild(v),this.windowEl.appendChild(s),this.messagesEl=document.createElement("div"),this.messagesEl.className="bb-msgs",this.windowEl.appendChild(this.messagesEl),this.chipsEl=document.createElement("div"),this.chipsEl.className="bb-chips";for(let p of this.settings.starterQuestions??[]){let N=document.createElement("button");N.className="bb-chip",N.textContent=p,N.addEventListener("click",()=>this.send(p)),this.chipsEl.appendChild(N)}this.windowEl.appendChild(this.chipsEl);let c=document.createElement("div");c.className="bb-esc";let C=document.createElement("span");C.className="bb-esc-title",C.textContent=this.t("leaveMessage");let E=document.createElement("input");E.type="email",E.name="email",E.placeholder=this.t("yourEmail");let k=document.createElement("textarea");k.name="message",k.rows=2,k.placeholder=this.t("yourMessage");let I=document.createElement("button");I.type="button",I.className="bb-esc-send",I.textContent=this.t("send"),I.addEventListener("click",()=>this.submitEscalation(c)),c.appendChild(C),c.appendChild(E),c.appendChild(k),c.appendChild(I),this.windowEl.appendChild(c);let f=document.createElement("div");f.className="bb-composer",this.inputEl=document.createElement("textarea"),this.inputEl.className="bb-input",this.inputEl.rows=1,this.inputEl.placeholder=this.t("placeholder"),this.inputEl.addEventListener("keydown",p=>{p.key==="Enter"&&!p.shiftKey&&(p.preventDefault(),this.send(this.inputEl?.value??""))}),this.inputEl.addEventListener("input",()=>this.syncSendState()),this.sendBtn=document.createElement("button"),this.sendBtn.className="bb-send",this.sendBtn.disabled=!0,this.sendBtn.setAttribute("aria-label",this.t("send")),this.sendBtn.appendChild(m("send")),this.sendBtn.addEventListener("click",()=>this.send(this.inputEl?.value??"")),f.appendChild(this.inputEl),f.appendChild(this.sendBtn),this.windowEl.appendChild(f);let B=document.createElement("div");B.className="bb-foot";let x=document.createElement("a");if(x.href="https://brainerce.com",x.target="_blank",x.rel="noopener noreferrer",x.appendChild(m("brand")),x.appendChild(document.createTextNode(this.t("poweredBy"))),B.appendChild(x),this.windowEl.appendChild(B),this.launcherEl=document.createElement("button"),this.launcherEl.className="bb-launcher",this.launcherEl.setAttribute("aria-label",r),this.settings.avatarUrl&&T(this.settings.avatarUrl)){let p=document.createElement("img");p.src=this.settings.avatarUrl,p.alt="",this.launcherEl.appendChild(p)}else{let p=m("chat");p.classList.add("bb-l-chat"),this.launcherEl.appendChild(p)}let L=m("close");L.classList.add("bb-l-close"),this.launcherEl.appendChild(L),this.launcherEl.addEventListener("click",()=>this.opened?this.close():this.open()),a.appendChild(this.launcherEl),e.appendChild(this.host)}iconButton(e,n,t){let i=document.createElement("button");return i.className="bb-iconbtn",i.title=n,i.setAttribute("aria-label",n),i.appendChild(m(e)),i.addEventListener("click",t),i}syncSendState(){this.sendBtn&&(this.sendBtn.disabled=!(this.inputEl?.value??"").trim()||this.busy)}open(){if(!this.windowEl||this.opened)return;this.opened=!0;let e=this.root?.querySelector(".bb");e?.classList.add("open");let n=(this.settings.displayMode??"floating")==="full_screen"||window.innerWidth<=520;e?.classList.toggle("big",n||this.expanded),this.syncBodyScroll(),this.messagesEl&&this.messagesEl.childElementCount===0&&this.primeThread(),this.inputEl?.focus()}close(){this.opened=!1;let e=this.root?.querySelector(".bb");e?.classList.remove("open"),this.expanded||e?.classList.remove("big"),this.syncBodyScroll()}syncBodyScroll(){let e=this.opened&&!!this.root?.querySelector(".bb")?.classList.contains("big");e&&this.prevBodyOverflow===null?(this.prevBodyOverflow=document.body.style.overflow||"",document.body.style.overflow="hidden"):!e&&this.prevBodyOverflow!==null&&(document.body.style.overflow=this.prevBodyOverflow,this.prevBodyOverflow=null)}toggleExpand(){if(this.expanded=!this.expanded,this.root?.querySelector(".bb")?.classList.toggle("expanded",this.expanded),this.root?.querySelector(".bb")?.classList.toggle("big",this.expanded),this.syncBodyScroll(),this.expandBtn){this.expandBtn.replaceChildren(m(this.expanded?"collapse":"expand"));let e=this.t(this.expanded?"collapse":"expand");this.expandBtn.title=e,this.expandBtn.setAttribute("aria-label",e)}}async primeThread(){if(this.conversationId&&this.sessionId)try{let e=await fetch(`${this.baseUrl}/api/storefront-bot/${encodeURIComponent(this.connectionId)}/conversations/${encodeURIComponent(this.conversationId)}?limit=50`,{headers:{"X-Bot-Session":this.sessionId}});if(e.ok){let n=await e.json();for(let t of n.data){let i=this.appendMessage(t.role==="assistant"?"bot":"user","");U(i,t.content)}if(n.data.length>0){this.chipsEl?.remove();return}}else this.conversationId=null,this.sessionId=null,this.persistIds()}catch{}if(this.settings.greeting){let e=this.appendMessage("bot","");U(e,this.settings.greeting)}}async send(e){let n=e.trim();if(!n||this.busy)return;this.busy=!0,this.inputEl&&(this.inputEl.value=""),this.syncSendState(),this.chipsEl?.remove(),this.appendMessage("user",n);let t=this.appendTyping();this.pendingText="",this.cardsRow=null,this.cardIds=new Set;let i=null;try{let o=await fetch(`${this.baseUrl}/api/storefront-bot/${encodeURIComponent(this.connectionId)}/chat`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({message:n,turnId:P("trn_"),...this.conversationId?{conversationId:this.conversationId}:{},...this.sessionId?{anonymousSessionId:this.sessionId}:{},locale:this.locale})});if(!o.ok||!o.body)throw new Error(`chat failed (${o.status})`);let r=o.body.getReader(),d=new TextDecoder,a="";for(;;){let{value:l,done:s}=await r.read();if(s)break;a+=d.decode(l,{stream:!0});let h;for(;(h=a.indexOf(`
250
+
251
+ `))>=0;){let u=a.slice(0,h);if(a=a.slice(h+2),!u.startsWith("data: "))continue;let g;try{g=JSON.parse(u.slice(6))}catch{continue}i=this.handleFrame(g,t,i)}}i&&this.pendingText&&U(i,this.pendingText)}catch{this.appendMessage("err",this.t("error"))}finally{t.remove(),this.busy=!1,this.syncSendState()}}handleFrame(e,n,t){switch(e.type){case"connected":return this.conversationId=e.conversationId||this.conversationId,this.sessionId=e.anonymousSessionId||this.sessionId,this.persistIds(),t;case"token":return t||(n.remove(),t=this.appendMessage("bot","")),this.pendingText+=e.text,t.textContent=this.pendingText,this.scrollDown(),t;case"tool":return n.classList.toggle("searching",e.status==="running"),t;case"card":return this.appendCard(e.card),t;case"action":return this.handleAction(e),t;case"error":return this.appendMessage("err",e.message||this.t("error")),t;case"done":default:return t}}appendCard(e){if(!this.messagesEl||this.cardIds.has(e.productId))return;if(this.cardIds.add(e.productId),!this.cardsRow){let s=document.createElement("div");s.className="bb-shelf";let h=document.createElement("div");h.className="bb-shelf-cap",h.textContent=this.t("results"),this.cardsRow=document.createElement("div"),this.cardsRow.className="bb-cards",s.appendChild(h),s.appendChild(this.cardsRow),this.messagesEl.appendChild(s)}let n=T(e.url)?e.url:null,t=document.createElement("div");t.className="bb-card";let i=()=>{this.beacon(e.botRef),n&&(window.location.href=n)},o=document.createElement("span");if(o.className="bb-card-img",e.imageUrl&&T(e.imageUrl)){let s=document.createElement("img");s.src=e.imageUrl,s.alt="",s.loading="lazy",o.appendChild(s)}else o.appendChild(m("cart"));o.addEventListener("click",i),t.appendChild(o);let r=document.createElement("span");r.className="bb-card-body";let d=document.createElement("span");d.className="bb-card-title",d.textContent=e.title,d.addEventListener("click",i);let a=document.createElement("span");a.className="bb-card-price",a.textContent=e.price.formatted,r.appendChild(d),r.appendChild(a);let l=document.createElement("span");if(l.className="bb-card-cta",e.requiresOptions){let s=document.createElement("button");s.className="bb-btn bb-btn-add",s.setAttribute("aria-label",this.t("addToCart")),s.appendChild(m("cart")),e.variants?.length?s.addEventListener("click",()=>this.togglePicker(r,e,o)):s.addEventListener("click",i),l.appendChild(s)}else{let s=document.createElement("button");s.className="bb-btn bb-btn-add",s.appendChild(m("cart")),s.appendChild(document.createTextNode(this.t("addToCart"))),s.addEventListener("click",()=>void this.addToCart(e,s));let h=document.createElement("button");h.className="bb-btn bb-btn-ghost",h.appendChild(document.createTextNode(this.t("view"))),h.addEventListener("click",i),l.appendChild(s),l.appendChild(h)}r.appendChild(l),t.appendChild(r),this.cardsRow.appendChild(t),this.scrollDown()}togglePicker(e,n,t){let i=e.parentElement,o=e.querySelector(".bb-pick");if(o){o.remove(),i?.classList.remove("picking");return}i?.classList.add("picking");let r=n.variants??[],d=[];for(let u of r)for(let g of Object.keys(u.attributes))d.includes(g)||d.push(g);let a={},l=document.createElement("span");l.className="bb-pick";let s=()=>r.find(u=>d.every(g=>a[g]&&u.attributes[g]===a[g]))??null,h=()=>{l.replaceChildren();for(let c of d){let C=document.createElement("span");C.className="bb-pick-key",C.textContent=c;let E=document.createElement("span");E.className="bb-pick-vals";let k=new Set;for(let I of r){let f=I.attributes[c];if(!f||k.has(f))continue;k.add(f);let B=r.some(L=>L.attributes[c]===f&&d.every(p=>p===c||!a[p]||L.attributes[p]===a[p])),x=document.createElement("button");x.className=`bb-chipv${a[c]===f?" sel":""}${B?"":" off"}`,x.textContent=f,x.addEventListener("click",()=>{a[c]===f?delete a[c]:a[c]=f,h()}),E.appendChild(x)}l.appendChild(C),l.appendChild(E)}let u=document.createElement("button");u.className="bb-pick-close",u.setAttribute("aria-label",this.t("close")),u.appendChild(m("close")),u.addEventListener("click",()=>{l.remove(),i?.classList.remove("picking")}),l.appendChild(u);let g=s(),w=document.createElement("span");w.className="bb-pick-foot";let v=document.createElement("span");v.className="bb-pick-price",v.textContent=g?g.price.formatted:"";let y=document.createElement("button");if(y.className="bb-btn bb-btn-add",y.appendChild(m("cart")),y.appendChild(document.createTextNode(this.t("addToCart"))),g||(y.disabled=!0),y.addEventListener("click",()=>{let c=s();c&&this.addToCart(n,y,c.id)}),w.appendChild(v),w.appendChild(y),l.appendChild(w),g?.imageUrl&&T(g.imageUrl)){let c=t.querySelector("img");c&&(c.src=g.imageUrl)}this.scrollDown()};h(),e.appendChild(l),this.scrollDown()}async addToCart(e,n,t){this.beacon(e.botRef),n.dataset.state="busy",await this.dispatchAdd(e.productId,t??null)?(n.dataset.state="done",n.replaceChildren(m("check"),document.createTextNode(this.t("added"))),setTimeout(()=>{!this.destroyed&&n.isConnected&&(delete n.dataset.state,n.replaceChildren(m("cart"),document.createTextNode(this.t("addToCart"))))},2200)):(delete n.dataset.state,T(e.url)&&(window.location.href=e.url))}async dispatchAdd(e,n){try{if(this.onAddToCart)return await this.onAddToCart({productId:e,variantId:n,quantity:1})!==!1;let t=new CustomEvent("brainerce:bot:add-to-cart",{detail:{productId:e,variantId:n,quantity:1,connectionId:this.connectionId},cancelable:!0,bubbles:!0,composed:!0});return!window.dispatchEvent(t)}catch{return!1}}async handleAction(e){if(e.action!=="add_to_cart")return;e.botRef&&this.beacon(e.botRef),await this.dispatchAdd(e.productId,e.variantId)||this.appendMessage("err",this.t("addFailed"))}beacon(e){try{navigator.sendBeacon?.(`${this.baseUrl}/api/storefront-bot/attribution/click`,new Blob([JSON.stringify({botRef:e})],{type:"application/json"}))}catch{}}toggleEscalation(){this.root?.querySelector(".bb-esc")?.classList.toggle("open")}async submitEscalation(e){let n=e.querySelector('input[name="email"]')?.value.trim(),t=e.querySelector('textarea[name="message"]')?.value.trim();if(!(!n||!t||!this.conversationId||!this.sessionId))try{if((await fetch(`${this.baseUrl}/api/storefront-bot/${encodeURIComponent(this.connectionId)}/escalate`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({email:n,message:t,conversationId:this.conversationId,anonymousSessionId:this.sessionId,locale:this.locale})})).ok){let o=document.createElement("span");o.className="bb-esc-note",o.appendChild(m("check")),o.appendChild(document.createTextNode(this.t("sent"))),e.replaceChildren(o)}}catch{}}appendMessage(e,n){let t=document.createElement("div");return t.className=`bb-msg ${e}`,t.setAttribute("dir","auto"),t.textContent=n,this.messagesEl?.appendChild(t),this.scrollDown(),t}appendTyping(){let e=document.createElement("div");e.className="bb-typing";let n=document.createElement("span");n.className="bb-dots";for(let i=0;i<3;i++)n.appendChild(document.createElement("i"));let t=document.createElement("span");return t.className="bb-tool",t.textContent=this.t("searching"),e.appendChild(n),e.appendChild(t),this.messagesEl?.appendChild(e),this.scrollDown(),e}scrollDown(){this.messagesEl&&(this.messagesEl.scrollTop=this.messagesEl.scrollHeight)}};(()=>{let b=document.currentScript,e=b?.dataset.connectionId;if(!e){console.warn("[BrainerceBot] missing data-connection-id on the bot.js script tag");return}let n=b?.dataset.apiBase||void 0,t=()=>void M.mount({connectionId:e,baseUrl:n});document.readyState==="loading"?document.addEventListener("DOMContentLoaded",t,{once:!0}):t()})();})();
@@ -74,6 +74,12 @@ declare class BrainerceBot {
74
74
  private syncSendState;
75
75
  private open;
76
76
  private close;
77
+ /**
78
+ * The large dialog owns the screen — the page behind must not scroll.
79
+ * Locks <body> while a big surface is open; restores on close/collapse.
80
+ */
81
+ private prevBodyOverflow;
82
+ private syncBodyScroll;
77
83
  private toggleExpand;
78
84
  /** First open: restore the server thread, or show the greeting. */
79
85
  private primeThread;
@@ -93,6 +99,14 @@ declare class BrainerceBot {
93
99
  * 3. fallback: navigate to the product page
94
100
  */
95
101
  private addToCart;
102
+ /**
103
+ * The host-cart chain shared by card buttons and bot-initiated actions:
104
+ * onAddToCart option -> cancelable CustomEvent. Returns whether a host
105
+ * took the add.
106
+ */
107
+ private dispatchAdd;
108
+ /** Bot-initiated widget actions (the model called the addToCart tool). */
109
+ private handleAction;
96
110
  /** The durable conversion signal — fire-and-forget, never blocks. */
97
111
  private beacon;
98
112
  private toggleEscalation;
@@ -74,6 +74,12 @@ declare class BrainerceBot {
74
74
  private syncSendState;
75
75
  private open;
76
76
  private close;
77
+ /**
78
+ * The large dialog owns the screen — the page behind must not scroll.
79
+ * Locks <body> while a big surface is open; restores on close/collapse.
80
+ */
81
+ private prevBodyOverflow;
82
+ private syncBodyScroll;
77
83
  private toggleExpand;
78
84
  /** First open: restore the server thread, or show the greeting. */
79
85
  private primeThread;
@@ -93,6 +99,14 @@ declare class BrainerceBot {
93
99
  * 3. fallback: navigate to the product page
94
100
  */
95
101
  private addToCart;
102
+ /**
103
+ * The host-cart chain shared by card buttons and bot-initiated actions:
104
+ * onAddToCart option -> cancelable CustomEvent. Returns whether a host
105
+ * took the add.
106
+ */
107
+ private dispatchAdd;
108
+ /** Bot-initiated widget actions (the model called the addToCart tool). */
109
+ private handleAction;
96
110
  /** The durable conversion signal — fire-and-forget, never blocks. */
97
111
  private beacon;
98
112
  private toggleEscalation;
package/dist/bot/index.js CHANGED
@@ -45,7 +45,9 @@ var CHROME = {
45
45
  added: "Added",
46
46
  view: "View",
47
47
  chooseOptions: "View product",
48
- results: "From the store"
48
+ results: "From the store",
49
+ poweredBy: "Powered by Brainerce",
50
+ addFailed: "I couldn\u2019t add that to the cart \u2014 try the button on the product card."
49
51
  },
50
52
  he: {
51
53
  online: "\u05DE\u05D7\u05D5\u05D1\u05E8",
@@ -64,7 +66,9 @@ var CHROME = {
64
66
  added: "\u05E0\u05D5\u05E1\u05E3",
65
67
  view: "\u05E6\u05E4\u05D9\u05D9\u05D4",
66
68
  chooseOptions: "\u05DC\u05E6\u05E4\u05D5\u05EA \u05D1\u05DE\u05D5\u05E6\u05E8",
67
- results: "\u05DE\u05EA\u05D5\u05DA \u05D4\u05D7\u05E0\u05D5\u05EA"
69
+ results: "\u05DE\u05EA\u05D5\u05DA \u05D4\u05D7\u05E0\u05D5\u05EA",
70
+ poweredBy: "\u05DE\u05D5\u05E4\u05E2\u05DC \u05E2\u05DC \u05D9\u05D3\u05D9 Brainerce",
71
+ addFailed: "\u05DC\u05D0 \u05D4\u05E6\u05DC\u05D7\u05EA\u05D9 \u05DC\u05D4\u05D5\u05E1\u05D9\u05E3 \u05DC\u05E1\u05DC \u2014 \u05E0\u05E1\u05D5 \u05D3\u05E8\u05DA \u05D4\u05DB\u05E4\u05EA\u05D5\u05E8 \u05D1\u05DB\u05E8\u05D8\u05D9\u05E1 \u05D4\u05DE\u05D5\u05E6\u05E8."
68
72
  }
69
73
  };
70
74
  var ICONS = {
@@ -76,7 +80,10 @@ var ICONS = {
76
80
  send: '<svg viewBox="0 0 24 24" fill="none"><path d="M4.4 11.2 19 4.6c.7-.3 1.4.4 1.1 1.1l-6.6 14.6c-.3.7-1.3.6-1.5-.1l-1.7-5.4a1 1 0 0 0-.6-.6l-5.4-1.7c-.7-.2-.8-1.2-.1-1.5Z" fill="currentColor"/></svg>',
77
81
  cart: '<svg viewBox="0 0 24 24" fill="none"><path d="M3 4h2l2.4 11.2A2 2 0 0 0 9.36 17H17.5a2 2 0 0 0 1.95-1.55L21 8H6" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/><circle cx="10" cy="20.5" r="1.4" fill="currentColor"/><circle cx="17" cy="20.5" r="1.4" fill="currentColor"/></svg>',
78
82
  check: '<svg viewBox="0 0 24 24" fill="none"><path d="m5 12.5 4.5 4.5L19 7.5" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>',
79
- arrow: '<svg viewBox="0 0 24 24" fill="none"><path d="M7 17 17 7M9 7h8v8" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>'
83
+ arrow: '<svg viewBox="0 0 24 24" fill="none"><path d="M7 17 17 7M9 7h8v8" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>',
84
+ // Brainerce brand mark for the Powered-by footer: the gradient "B"
85
+ // (emerald→violet), matching the dashboard BrandedLoader identity.
86
+ brand: '<svg viewBox="0 0 24 24"><defs><linearGradient id="bb-brand-g" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#34d399"/><stop offset="1" stop-color="#8b5cf6"/></linearGradient></defs><rect x="1.5" y="1.5" width="21" height="21" rx="6" fill="url(#bb-brand-g)"/><text x="12" y="17" text-anchor="middle" font-family="ui-sans-serif,system-ui,sans-serif" font-size="14" font-weight="700" fill="#fff">B</text></svg>'
80
87
  };
81
88
  var svgParser = typeof DOMParser !== "undefined" ? new DOMParser() : null;
82
89
  function icon(name) {
@@ -170,6 +177,11 @@ var BrainerceBot = class _BrainerceBot {
170
177
  this.pendingText = "";
171
178
  this.cardsRow = null;
172
179
  this.cardIds = /* @__PURE__ */ new Set();
180
+ /**
181
+ * The large dialog owns the screen — the page behind must not scroll.
182
+ * Locks <body> while a big surface is open; restores on close/collapse.
183
+ */
184
+ this.prevBodyOverflow = null;
173
185
  this.connectionId = options.connectionId;
174
186
  this.baseUrl = (options.baseUrl || DEFAULT_BASE_URL).replace(/\/$/, "");
175
187
  this.storageKey = `brainerce-bot:${this.connectionId}`;
@@ -187,6 +199,10 @@ var BrainerceBot = class _BrainerceBot {
187
199
  }
188
200
  destroy() {
189
201
  this.destroyed = true;
202
+ if (this.prevBodyOverflow !== null) {
203
+ document.body.style.overflow = this.prevBodyOverflow;
204
+ this.prevBodyOverflow = null;
205
+ }
190
206
  this.host?.remove();
191
207
  }
192
208
  // ---------------------------------------------------------------------------
@@ -481,6 +497,15 @@ var BrainerceBot = class _BrainerceBot {
481
497
  .bb-send .bb-ic { width: 17px; height: 17px; ${dir === "rtl" ? "transform: scaleX(-1);" : ""} }
482
498
  .bb-send:hover { filter: brightness(1.08); transform: scale(1.06); }
483
499
  .bb-send:disabled { opacity: .4; pointer-events: none; }
500
+
501
+ /* Powered-by footer */
502
+ .bb-foot { flex-shrink: 0; display: flex; justify-content: center; align-items: center;
503
+ padding: 5px 12px 7px; background: #fff; }
504
+ .bb-foot a { display: inline-flex; align-items: center; gap: 5px; font-size: 10.5px;
505
+ font-weight: 500; color: #aab0bd; text-decoration: none; letter-spacing: .005em;
506
+ transition: color .12s ease; }
507
+ .bb-foot a:hover { color: #6b7280; }
508
+ .bb-foot .bb-ic { width: 13px; height: 13px; border-radius: 4px; }
484
509
  `;
485
510
  }
486
511
  render(target) {
@@ -601,6 +626,16 @@ var BrainerceBot = class _BrainerceBot {
601
626
  composer.appendChild(this.inputEl);
602
627
  composer.appendChild(this.sendBtn);
603
628
  this.windowEl.appendChild(composer);
629
+ const foot = document.createElement("div");
630
+ foot.className = "bb-foot";
631
+ const credit = document.createElement("a");
632
+ credit.href = "https://brainerce.com";
633
+ credit.target = "_blank";
634
+ credit.rel = "noopener noreferrer";
635
+ credit.appendChild(icon("brand"));
636
+ credit.appendChild(document.createTextNode(this.t("poweredBy")));
637
+ foot.appendChild(credit);
638
+ this.windowEl.appendChild(foot);
604
639
  this.launcherEl = document.createElement("button");
605
640
  this.launcherEl.className = "bb-launcher";
606
641
  this.launcherEl.setAttribute("aria-label", name);
@@ -640,6 +675,7 @@ var BrainerceBot = class _BrainerceBot {
640
675
  el?.classList.add("open");
641
676
  const takeover = (this.settings.displayMode ?? "floating") === "full_screen" || window.innerWidth <= 520;
642
677
  el?.classList.toggle("big", takeover || this.expanded);
678
+ this.syncBodyScroll();
643
679
  if (this.messagesEl && this.messagesEl.childElementCount === 0) {
644
680
  void this.primeThread();
645
681
  }
@@ -650,11 +686,23 @@ var BrainerceBot = class _BrainerceBot {
650
686
  const el = this.root?.querySelector(".bb");
651
687
  el?.classList.remove("open");
652
688
  if (!this.expanded) el?.classList.remove("big");
689
+ this.syncBodyScroll();
690
+ }
691
+ syncBodyScroll() {
692
+ const big = this.opened && !!this.root?.querySelector(".bb")?.classList.contains("big");
693
+ if (big && this.prevBodyOverflow === null) {
694
+ this.prevBodyOverflow = document.body.style.overflow || "";
695
+ document.body.style.overflow = "hidden";
696
+ } else if (!big && this.prevBodyOverflow !== null) {
697
+ document.body.style.overflow = this.prevBodyOverflow;
698
+ this.prevBodyOverflow = null;
699
+ }
653
700
  }
654
701
  toggleExpand() {
655
702
  this.expanded = !this.expanded;
656
703
  this.root?.querySelector(".bb")?.classList.toggle("expanded", this.expanded);
657
704
  this.root?.querySelector(".bb")?.classList.toggle("big", this.expanded);
705
+ this.syncBodyScroll();
658
706
  if (this.expandBtn) {
659
707
  this.expandBtn.replaceChildren(icon(this.expanded ? "collapse" : "expand"));
660
708
  const label = this.t(this.expanded ? "collapse" : "expand");
@@ -780,6 +828,9 @@ var BrainerceBot = class _BrainerceBot {
780
828
  case "card":
781
829
  this.appendCard(frame.card);
782
830
  return botBubble;
831
+ case "action":
832
+ void this.handleAction(frame);
833
+ return botBubble;
783
834
  case "error":
784
835
  this.appendMessage("err", frame.message || this.t("error"));
785
836
  return botBubble;
@@ -967,31 +1018,7 @@ var BrainerceBot = class _BrainerceBot {
967
1018
  async addToCart(card, btn, variantId) {
968
1019
  this.beacon(card.botRef);
969
1020
  btn.dataset.state = "busy";
970
- let handled = false;
971
- try {
972
- if (this.onAddToCart) {
973
- handled = await this.onAddToCart({
974
- productId: card.productId,
975
- variantId: variantId ?? null,
976
- quantity: 1
977
- }) !== false;
978
- } else {
979
- const ev = new CustomEvent("brainerce:bot:add-to-cart", {
980
- detail: {
981
- productId: card.productId,
982
- variantId: variantId ?? null,
983
- quantity: 1,
984
- connectionId: this.connectionId
985
- },
986
- cancelable: true,
987
- bubbles: true,
988
- composed: true
989
- });
990
- handled = !window.dispatchEvent(ev);
991
- }
992
- } catch {
993
- handled = false;
994
- }
1021
+ const handled = await this.dispatchAdd(card.productId, variantId ?? null);
995
1022
  if (handled) {
996
1023
  btn.dataset.state = "done";
997
1024
  btn.replaceChildren(icon("check"), document.createTextNode(this.t("added")));
@@ -1006,6 +1033,34 @@ var BrainerceBot = class _BrainerceBot {
1006
1033
  if (isSafeUrl(card.url)) window.location.href = card.url;
1007
1034
  }
1008
1035
  }
1036
+ /**
1037
+ * The host-cart chain shared by card buttons and bot-initiated actions:
1038
+ * onAddToCart option -> cancelable CustomEvent. Returns whether a host
1039
+ * took the add.
1040
+ */
1041
+ async dispatchAdd(productId, variantId) {
1042
+ try {
1043
+ if (this.onAddToCart) {
1044
+ return await this.onAddToCart({ productId, variantId, quantity: 1 }) !== false;
1045
+ }
1046
+ const ev = new CustomEvent("brainerce:bot:add-to-cart", {
1047
+ detail: { productId, variantId, quantity: 1, connectionId: this.connectionId },
1048
+ cancelable: true,
1049
+ bubbles: true,
1050
+ composed: true
1051
+ });
1052
+ return !window.dispatchEvent(ev);
1053
+ } catch {
1054
+ return false;
1055
+ }
1056
+ }
1057
+ /** Bot-initiated widget actions (the model called the addToCart tool). */
1058
+ async handleAction(frame) {
1059
+ if (frame.action !== "add_to_cart") return;
1060
+ if (frame.botRef) this.beacon(frame.botRef);
1061
+ const ok = await this.dispatchAdd(frame.productId, frame.variantId);
1062
+ if (!ok) this.appendMessage("err", this.t("addFailed"));
1063
+ }
1009
1064
  /** The durable conversion signal — fire-and-forget, never blocks. */
1010
1065
  beacon(botRef) {
1011
1066
  try {
@@ -19,7 +19,9 @@ var CHROME = {
19
19
  added: "Added",
20
20
  view: "View",
21
21
  chooseOptions: "View product",
22
- results: "From the store"
22
+ results: "From the store",
23
+ poweredBy: "Powered by Brainerce",
24
+ addFailed: "I couldn\u2019t add that to the cart \u2014 try the button on the product card."
23
25
  },
24
26
  he: {
25
27
  online: "\u05DE\u05D7\u05D5\u05D1\u05E8",
@@ -38,7 +40,9 @@ var CHROME = {
38
40
  added: "\u05E0\u05D5\u05E1\u05E3",
39
41
  view: "\u05E6\u05E4\u05D9\u05D9\u05D4",
40
42
  chooseOptions: "\u05DC\u05E6\u05E4\u05D5\u05EA \u05D1\u05DE\u05D5\u05E6\u05E8",
41
- results: "\u05DE\u05EA\u05D5\u05DA \u05D4\u05D7\u05E0\u05D5\u05EA"
43
+ results: "\u05DE\u05EA\u05D5\u05DA \u05D4\u05D7\u05E0\u05D5\u05EA",
44
+ poweredBy: "\u05DE\u05D5\u05E4\u05E2\u05DC \u05E2\u05DC \u05D9\u05D3\u05D9 Brainerce",
45
+ addFailed: "\u05DC\u05D0 \u05D4\u05E6\u05DC\u05D7\u05EA\u05D9 \u05DC\u05D4\u05D5\u05E1\u05D9\u05E3 \u05DC\u05E1\u05DC \u2014 \u05E0\u05E1\u05D5 \u05D3\u05E8\u05DA \u05D4\u05DB\u05E4\u05EA\u05D5\u05E8 \u05D1\u05DB\u05E8\u05D8\u05D9\u05E1 \u05D4\u05DE\u05D5\u05E6\u05E8."
42
46
  }
43
47
  };
44
48
  var ICONS = {
@@ -50,7 +54,10 @@ var ICONS = {
50
54
  send: '<svg viewBox="0 0 24 24" fill="none"><path d="M4.4 11.2 19 4.6c.7-.3 1.4.4 1.1 1.1l-6.6 14.6c-.3.7-1.3.6-1.5-.1l-1.7-5.4a1 1 0 0 0-.6-.6l-5.4-1.7c-.7-.2-.8-1.2-.1-1.5Z" fill="currentColor"/></svg>',
51
55
  cart: '<svg viewBox="0 0 24 24" fill="none"><path d="M3 4h2l2.4 11.2A2 2 0 0 0 9.36 17H17.5a2 2 0 0 0 1.95-1.55L21 8H6" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/><circle cx="10" cy="20.5" r="1.4" fill="currentColor"/><circle cx="17" cy="20.5" r="1.4" fill="currentColor"/></svg>',
52
56
  check: '<svg viewBox="0 0 24 24" fill="none"><path d="m5 12.5 4.5 4.5L19 7.5" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>',
53
- arrow: '<svg viewBox="0 0 24 24" fill="none"><path d="M7 17 17 7M9 7h8v8" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>'
57
+ arrow: '<svg viewBox="0 0 24 24" fill="none"><path d="M7 17 17 7M9 7h8v8" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>',
58
+ // Brainerce brand mark for the Powered-by footer: the gradient "B"
59
+ // (emerald→violet), matching the dashboard BrandedLoader identity.
60
+ brand: '<svg viewBox="0 0 24 24"><defs><linearGradient id="bb-brand-g" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#34d399"/><stop offset="1" stop-color="#8b5cf6"/></linearGradient></defs><rect x="1.5" y="1.5" width="21" height="21" rx="6" fill="url(#bb-brand-g)"/><text x="12" y="17" text-anchor="middle" font-family="ui-sans-serif,system-ui,sans-serif" font-size="14" font-weight="700" fill="#fff">B</text></svg>'
54
61
  };
55
62
  var svgParser = typeof DOMParser !== "undefined" ? new DOMParser() : null;
56
63
  function icon(name) {
@@ -144,6 +151,11 @@ var BrainerceBot = class _BrainerceBot {
144
151
  this.pendingText = "";
145
152
  this.cardsRow = null;
146
153
  this.cardIds = /* @__PURE__ */ new Set();
154
+ /**
155
+ * The large dialog owns the screen — the page behind must not scroll.
156
+ * Locks <body> while a big surface is open; restores on close/collapse.
157
+ */
158
+ this.prevBodyOverflow = null;
147
159
  this.connectionId = options.connectionId;
148
160
  this.baseUrl = (options.baseUrl || DEFAULT_BASE_URL).replace(/\/$/, "");
149
161
  this.storageKey = `brainerce-bot:${this.connectionId}`;
@@ -161,6 +173,10 @@ var BrainerceBot = class _BrainerceBot {
161
173
  }
162
174
  destroy() {
163
175
  this.destroyed = true;
176
+ if (this.prevBodyOverflow !== null) {
177
+ document.body.style.overflow = this.prevBodyOverflow;
178
+ this.prevBodyOverflow = null;
179
+ }
164
180
  this.host?.remove();
165
181
  }
166
182
  // ---------------------------------------------------------------------------
@@ -455,6 +471,15 @@ var BrainerceBot = class _BrainerceBot {
455
471
  .bb-send .bb-ic { width: 17px; height: 17px; ${dir === "rtl" ? "transform: scaleX(-1);" : ""} }
456
472
  .bb-send:hover { filter: brightness(1.08); transform: scale(1.06); }
457
473
  .bb-send:disabled { opacity: .4; pointer-events: none; }
474
+
475
+ /* Powered-by footer */
476
+ .bb-foot { flex-shrink: 0; display: flex; justify-content: center; align-items: center;
477
+ padding: 5px 12px 7px; background: #fff; }
478
+ .bb-foot a { display: inline-flex; align-items: center; gap: 5px; font-size: 10.5px;
479
+ font-weight: 500; color: #aab0bd; text-decoration: none; letter-spacing: .005em;
480
+ transition: color .12s ease; }
481
+ .bb-foot a:hover { color: #6b7280; }
482
+ .bb-foot .bb-ic { width: 13px; height: 13px; border-radius: 4px; }
458
483
  `;
459
484
  }
460
485
  render(target) {
@@ -575,6 +600,16 @@ var BrainerceBot = class _BrainerceBot {
575
600
  composer.appendChild(this.inputEl);
576
601
  composer.appendChild(this.sendBtn);
577
602
  this.windowEl.appendChild(composer);
603
+ const foot = document.createElement("div");
604
+ foot.className = "bb-foot";
605
+ const credit = document.createElement("a");
606
+ credit.href = "https://brainerce.com";
607
+ credit.target = "_blank";
608
+ credit.rel = "noopener noreferrer";
609
+ credit.appendChild(icon("brand"));
610
+ credit.appendChild(document.createTextNode(this.t("poweredBy")));
611
+ foot.appendChild(credit);
612
+ this.windowEl.appendChild(foot);
578
613
  this.launcherEl = document.createElement("button");
579
614
  this.launcherEl.className = "bb-launcher";
580
615
  this.launcherEl.setAttribute("aria-label", name);
@@ -614,6 +649,7 @@ var BrainerceBot = class _BrainerceBot {
614
649
  el?.classList.add("open");
615
650
  const takeover = (this.settings.displayMode ?? "floating") === "full_screen" || window.innerWidth <= 520;
616
651
  el?.classList.toggle("big", takeover || this.expanded);
652
+ this.syncBodyScroll();
617
653
  if (this.messagesEl && this.messagesEl.childElementCount === 0) {
618
654
  void this.primeThread();
619
655
  }
@@ -624,11 +660,23 @@ var BrainerceBot = class _BrainerceBot {
624
660
  const el = this.root?.querySelector(".bb");
625
661
  el?.classList.remove("open");
626
662
  if (!this.expanded) el?.classList.remove("big");
663
+ this.syncBodyScroll();
664
+ }
665
+ syncBodyScroll() {
666
+ const big = this.opened && !!this.root?.querySelector(".bb")?.classList.contains("big");
667
+ if (big && this.prevBodyOverflow === null) {
668
+ this.prevBodyOverflow = document.body.style.overflow || "";
669
+ document.body.style.overflow = "hidden";
670
+ } else if (!big && this.prevBodyOverflow !== null) {
671
+ document.body.style.overflow = this.prevBodyOverflow;
672
+ this.prevBodyOverflow = null;
673
+ }
627
674
  }
628
675
  toggleExpand() {
629
676
  this.expanded = !this.expanded;
630
677
  this.root?.querySelector(".bb")?.classList.toggle("expanded", this.expanded);
631
678
  this.root?.querySelector(".bb")?.classList.toggle("big", this.expanded);
679
+ this.syncBodyScroll();
632
680
  if (this.expandBtn) {
633
681
  this.expandBtn.replaceChildren(icon(this.expanded ? "collapse" : "expand"));
634
682
  const label = this.t(this.expanded ? "collapse" : "expand");
@@ -754,6 +802,9 @@ var BrainerceBot = class _BrainerceBot {
754
802
  case "card":
755
803
  this.appendCard(frame.card);
756
804
  return botBubble;
805
+ case "action":
806
+ void this.handleAction(frame);
807
+ return botBubble;
757
808
  case "error":
758
809
  this.appendMessage("err", frame.message || this.t("error"));
759
810
  return botBubble;
@@ -941,31 +992,7 @@ var BrainerceBot = class _BrainerceBot {
941
992
  async addToCart(card, btn, variantId) {
942
993
  this.beacon(card.botRef);
943
994
  btn.dataset.state = "busy";
944
- let handled = false;
945
- try {
946
- if (this.onAddToCart) {
947
- handled = await this.onAddToCart({
948
- productId: card.productId,
949
- variantId: variantId ?? null,
950
- quantity: 1
951
- }) !== false;
952
- } else {
953
- const ev = new CustomEvent("brainerce:bot:add-to-cart", {
954
- detail: {
955
- productId: card.productId,
956
- variantId: variantId ?? null,
957
- quantity: 1,
958
- connectionId: this.connectionId
959
- },
960
- cancelable: true,
961
- bubbles: true,
962
- composed: true
963
- });
964
- handled = !window.dispatchEvent(ev);
965
- }
966
- } catch {
967
- handled = false;
968
- }
995
+ const handled = await this.dispatchAdd(card.productId, variantId ?? null);
969
996
  if (handled) {
970
997
  btn.dataset.state = "done";
971
998
  btn.replaceChildren(icon("check"), document.createTextNode(this.t("added")));
@@ -980,6 +1007,34 @@ var BrainerceBot = class _BrainerceBot {
980
1007
  if (isSafeUrl(card.url)) window.location.href = card.url;
981
1008
  }
982
1009
  }
1010
+ /**
1011
+ * The host-cart chain shared by card buttons and bot-initiated actions:
1012
+ * onAddToCart option -> cancelable CustomEvent. Returns whether a host
1013
+ * took the add.
1014
+ */
1015
+ async dispatchAdd(productId, variantId) {
1016
+ try {
1017
+ if (this.onAddToCart) {
1018
+ return await this.onAddToCart({ productId, variantId, quantity: 1 }) !== false;
1019
+ }
1020
+ const ev = new CustomEvent("brainerce:bot:add-to-cart", {
1021
+ detail: { productId, variantId, quantity: 1, connectionId: this.connectionId },
1022
+ cancelable: true,
1023
+ bubbles: true,
1024
+ composed: true
1025
+ });
1026
+ return !window.dispatchEvent(ev);
1027
+ } catch {
1028
+ return false;
1029
+ }
1030
+ }
1031
+ /** Bot-initiated widget actions (the model called the addToCart tool). */
1032
+ async handleAction(frame) {
1033
+ if (frame.action !== "add_to_cart") return;
1034
+ if (frame.botRef) this.beacon(frame.botRef);
1035
+ const ok = await this.dispatchAdd(frame.productId, frame.variantId);
1036
+ if (!ok) this.appendMessage("err", this.t("addFailed"));
1037
+ }
983
1038
  /** The durable conversion signal — fire-and-forget, never blocks. */
984
1039
  beacon(botRef) {
985
1040
  try {
package/dist/index.d.mts CHANGED
@@ -5168,6 +5168,61 @@ interface ModifierValidationError {
5168
5168
  /** Modifier the error belongs to (when applicable). */
5169
5169
  modifierId?: string;
5170
5170
  }
5171
+ type BlogPostStatus = 'DRAFT' | 'SCHEDULED' | 'PUBLISHED';
5172
+ interface BlogPost {
5173
+ id: string;
5174
+ storeId: string;
5175
+ title: string;
5176
+ slug: string;
5177
+ category?: string;
5178
+ content: string;
5179
+ excerpt?: string;
5180
+ coverImageUrl?: string;
5181
+ coverImageAlt?: string;
5182
+ author?: string;
5183
+ tags: string[];
5184
+ metadata: Record<string, unknown>;
5185
+ status: BlogPostStatus;
5186
+ publishedAt?: string;
5187
+ seoTitle?: string;
5188
+ seoDescription?: string;
5189
+ ogImageUrl?: string;
5190
+ createdAt: string;
5191
+ updatedAt: string;
5192
+ }
5193
+ interface CreateBlogPostInput {
5194
+ title: string;
5195
+ slug?: string;
5196
+ category?: string;
5197
+ content?: string;
5198
+ excerpt?: string;
5199
+ coverImageUrl?: string;
5200
+ coverImageAlt?: string;
5201
+ author?: string;
5202
+ tags?: string[];
5203
+ metadata?: Record<string, unknown>;
5204
+ status?: BlogPostStatus;
5205
+ publishedAt?: string | null;
5206
+ seoTitle?: string;
5207
+ seoDescription?: string;
5208
+ ogImageUrl?: string;
5209
+ }
5210
+ type UpdateBlogPostInput = Partial<CreateBlogPostInput>;
5211
+ interface BlogPostListParams {
5212
+ page?: number;
5213
+ limit?: number;
5214
+ category?: string;
5215
+ tag?: string;
5216
+ }
5217
+ interface BlogPostListResponse {
5218
+ data: BlogPost[];
5219
+ meta: {
5220
+ page: number;
5221
+ limit: number;
5222
+ total: number;
5223
+ totalPages: number;
5224
+ };
5225
+ }
5171
5226
  interface BrainerceApiError {
5172
5227
  statusCode: number;
5173
5228
  message: string;
@@ -6637,6 +6692,42 @@ declare class BrainerceClient {
6637
6692
  /** Hard delete the row. Admin mode only. */
6638
6693
  remove: (id: string) => Promise<void>;
6639
6694
  };
6695
+ /**
6696
+ * Read and manage blog posts.
6697
+ *
6698
+ * ```typescript
6699
+ * // Storefront / vibe-coded: list published posts
6700
+ * const { data: posts } = await brainerce.blog.getPosts({ category: 'news' });
6701
+ *
6702
+ * // Fetch one by slug
6703
+ * const post = await brainerce.blog.getPost('my-first-post');
6704
+ *
6705
+ * // Admin: create a draft
6706
+ * const draft = await brainerce.blog.create({ title: 'Hello World' });
6707
+ * ```
6708
+ */
6709
+ blog: {
6710
+ /**
6711
+ * List published posts. Works in all modes.
6712
+ * Filters: `category`, `tag`, `page`, `limit`.
6713
+ */
6714
+ getPosts: (params?: BlogPostListParams) => Promise<BlogPostListResponse>;
6715
+ /**
6716
+ * Fetch one published post by its slug. Returns `null` on 404.
6717
+ * Works in all modes.
6718
+ */
6719
+ getPost: (slug: string) => Promise<BlogPost | null>;
6720
+ /** Create a blog post in DRAFT status. Admin mode only. */
6721
+ create: (input: CreateBlogPostInput) => Promise<BlogPost>;
6722
+ /** Update a blog post by ID. Admin mode only. */
6723
+ update: (id: string, input: UpdateBlogPostInput) => Promise<BlogPost>;
6724
+ /** Transition status → PUBLISHED (sets publishedAt = now if unset). Admin mode only. */
6725
+ publish: (id: string) => Promise<BlogPost>;
6726
+ /** Transition status PUBLISHED → DRAFT. Admin mode only. */
6727
+ unpublish: (id: string) => Promise<BlogPost>;
6728
+ /** Hard-delete a blog post. Admin mode only. */
6729
+ remove: (id: string) => Promise<void>;
6730
+ };
6640
6731
  /**
6641
6732
  * Submit a contact inquiry from a storefront contact form.
6642
6733
  *
package/dist/index.d.ts CHANGED
@@ -5168,6 +5168,61 @@ interface ModifierValidationError {
5168
5168
  /** Modifier the error belongs to (when applicable). */
5169
5169
  modifierId?: string;
5170
5170
  }
5171
+ type BlogPostStatus = 'DRAFT' | 'SCHEDULED' | 'PUBLISHED';
5172
+ interface BlogPost {
5173
+ id: string;
5174
+ storeId: string;
5175
+ title: string;
5176
+ slug: string;
5177
+ category?: string;
5178
+ content: string;
5179
+ excerpt?: string;
5180
+ coverImageUrl?: string;
5181
+ coverImageAlt?: string;
5182
+ author?: string;
5183
+ tags: string[];
5184
+ metadata: Record<string, unknown>;
5185
+ status: BlogPostStatus;
5186
+ publishedAt?: string;
5187
+ seoTitle?: string;
5188
+ seoDescription?: string;
5189
+ ogImageUrl?: string;
5190
+ createdAt: string;
5191
+ updatedAt: string;
5192
+ }
5193
+ interface CreateBlogPostInput {
5194
+ title: string;
5195
+ slug?: string;
5196
+ category?: string;
5197
+ content?: string;
5198
+ excerpt?: string;
5199
+ coverImageUrl?: string;
5200
+ coverImageAlt?: string;
5201
+ author?: string;
5202
+ tags?: string[];
5203
+ metadata?: Record<string, unknown>;
5204
+ status?: BlogPostStatus;
5205
+ publishedAt?: string | null;
5206
+ seoTitle?: string;
5207
+ seoDescription?: string;
5208
+ ogImageUrl?: string;
5209
+ }
5210
+ type UpdateBlogPostInput = Partial<CreateBlogPostInput>;
5211
+ interface BlogPostListParams {
5212
+ page?: number;
5213
+ limit?: number;
5214
+ category?: string;
5215
+ tag?: string;
5216
+ }
5217
+ interface BlogPostListResponse {
5218
+ data: BlogPost[];
5219
+ meta: {
5220
+ page: number;
5221
+ limit: number;
5222
+ total: number;
5223
+ totalPages: number;
5224
+ };
5225
+ }
5171
5226
  interface BrainerceApiError {
5172
5227
  statusCode: number;
5173
5228
  message: string;
@@ -6637,6 +6692,42 @@ declare class BrainerceClient {
6637
6692
  /** Hard delete the row. Admin mode only. */
6638
6693
  remove: (id: string) => Promise<void>;
6639
6694
  };
6695
+ /**
6696
+ * Read and manage blog posts.
6697
+ *
6698
+ * ```typescript
6699
+ * // Storefront / vibe-coded: list published posts
6700
+ * const { data: posts } = await brainerce.blog.getPosts({ category: 'news' });
6701
+ *
6702
+ * // Fetch one by slug
6703
+ * const post = await brainerce.blog.getPost('my-first-post');
6704
+ *
6705
+ * // Admin: create a draft
6706
+ * const draft = await brainerce.blog.create({ title: 'Hello World' });
6707
+ * ```
6708
+ */
6709
+ blog: {
6710
+ /**
6711
+ * List published posts. Works in all modes.
6712
+ * Filters: `category`, `tag`, `page`, `limit`.
6713
+ */
6714
+ getPosts: (params?: BlogPostListParams) => Promise<BlogPostListResponse>;
6715
+ /**
6716
+ * Fetch one published post by its slug. Returns `null` on 404.
6717
+ * Works in all modes.
6718
+ */
6719
+ getPost: (slug: string) => Promise<BlogPost | null>;
6720
+ /** Create a blog post in DRAFT status. Admin mode only. */
6721
+ create: (input: CreateBlogPostInput) => Promise<BlogPost>;
6722
+ /** Update a blog post by ID. Admin mode only. */
6723
+ update: (id: string, input: UpdateBlogPostInput) => Promise<BlogPost>;
6724
+ /** Transition status → PUBLISHED (sets publishedAt = now if unset). Admin mode only. */
6725
+ publish: (id: string) => Promise<BlogPost>;
6726
+ /** Transition status PUBLISHED → DRAFT. Admin mode only. */
6727
+ unpublish: (id: string) => Promise<BlogPost>;
6728
+ /** Hard-delete a blog post. Admin mode only. */
6729
+ remove: (id: string) => Promise<void>;
6730
+ };
6640
6731
  /**
6641
6732
  * Submit a contact inquiry from a storefront contact form.
6642
6733
  *
package/dist/index.js CHANGED
@@ -489,6 +489,120 @@ var BrainerceClient = class {
489
489
  remove: removeById
490
490
  };
491
491
  })();
492
+ // -------------------- Blog --------------------
493
+ /**
494
+ * Read and manage blog posts.
495
+ *
496
+ * ```typescript
497
+ * // Storefront / vibe-coded: list published posts
498
+ * const { data: posts } = await brainerce.blog.getPosts({ category: 'news' });
499
+ *
500
+ * // Fetch one by slug
501
+ * const post = await brainerce.blog.getPost('my-first-post');
502
+ *
503
+ * // Admin: create a draft
504
+ * const draft = await brainerce.blog.create({ title: 'Hello World' });
505
+ * ```
506
+ */
507
+ this.blog = /* @__PURE__ */ (() => {
508
+ const publicBase = "/blog/posts";
509
+ const adminBase = "/blog/posts";
510
+ const requireAdmin = (action) => {
511
+ if (this.isVibeCodedMode() || this.storeId && !this.apiKey) {
512
+ throw new BrainerceError(
513
+ `client.blog.${action}() requires admin mode (apiKey). Vibe-coded and storefront modes are read-only.`,
514
+ 403
515
+ );
516
+ }
517
+ };
518
+ const onNotFound = (err) => {
519
+ if (err instanceof BrainerceError && err.statusCode === 404) return null;
520
+ throw err;
521
+ };
522
+ const buildQuery = (params) => Object.fromEntries(
523
+ Object.entries(params).filter(([, v]) => v != null).map(([k, v]) => [k, String(v)])
524
+ );
525
+ return {
526
+ /**
527
+ * List published posts. Works in all modes.
528
+ * Filters: `category`, `tag`, `page`, `limit`.
529
+ */
530
+ getPosts: (params = {}) => {
531
+ const query = buildQuery(params);
532
+ if (this.isVibeCodedMode()) {
533
+ return this.vibeCodedRequest(
534
+ "GET",
535
+ publicBase,
536
+ void 0,
537
+ query
538
+ );
539
+ }
540
+ if (this.storeId && !this.apiKey) {
541
+ return this.storefrontRequest(
542
+ "GET",
543
+ publicBase,
544
+ void 0,
545
+ query
546
+ );
547
+ }
548
+ return this.adminRequest(
549
+ "GET",
550
+ adminBase,
551
+ void 0,
552
+ query
553
+ );
554
+ },
555
+ /**
556
+ * Fetch one published post by its slug. Returns `null` on 404.
557
+ * Works in all modes.
558
+ */
559
+ getPost: (slug) => {
560
+ const path = `${publicBase}/${encodeURIComponent(slug)}`;
561
+ if (this.isVibeCodedMode()) {
562
+ return this.vibeCodedRequest("GET", path).catch(onNotFound);
563
+ }
564
+ if (this.storeId && !this.apiKey) {
565
+ return this.storefrontRequest("GET", path).catch(onNotFound);
566
+ }
567
+ return this.adminRequest("GET", path).catch(onNotFound);
568
+ },
569
+ /** Create a blog post in DRAFT status. Admin mode only. */
570
+ create: (input) => {
571
+ requireAdmin("create");
572
+ return this.adminRequest("POST", adminBase, input);
573
+ },
574
+ /** Update a blog post by ID. Admin mode only. */
575
+ update: (id, input) => {
576
+ requireAdmin("update");
577
+ return this.adminRequest(
578
+ "PATCH",
579
+ `${adminBase}/${encodeURIComponent(id)}`,
580
+ input
581
+ );
582
+ },
583
+ /** Transition status → PUBLISHED (sets publishedAt = now if unset). Admin mode only. */
584
+ publish: (id) => {
585
+ requireAdmin("publish");
586
+ return this.adminRequest(
587
+ "POST",
588
+ `${adminBase}/${encodeURIComponent(id)}/publish`
589
+ );
590
+ },
591
+ /** Transition status PUBLISHED → DRAFT. Admin mode only. */
592
+ unpublish: (id) => {
593
+ requireAdmin("unpublish");
594
+ return this.adminRequest(
595
+ "POST",
596
+ `${adminBase}/${encodeURIComponent(id)}/unpublish`
597
+ );
598
+ },
599
+ /** Hard-delete a blog post. Admin mode only. */
600
+ remove: async (id) => {
601
+ requireAdmin("remove");
602
+ await this.adminRequest("DELETE", `${adminBase}/${encodeURIComponent(id)}`);
603
+ }
604
+ };
605
+ })();
492
606
  // -------------------- Local Cart (Client-Side for Guests) --------------------
493
607
  // These methods store cart data in localStorage - NO API calls!
494
608
  // Use for guest users in vibe-coded sites
@@ -8112,6 +8226,9 @@ var ALLOWED_PAYMENT_HOSTS = [
8112
8226
  "grow.security",
8113
8227
  // CreditGuard
8114
8228
  "creditguard.co.il",
8229
+ // Takbull
8230
+ "takbull.co.il",
8231
+ "api.takbull.co.il",
8115
8232
  // Brainerce-hosted payment embeds (backend payment-embed proxy at
8116
8233
  // `/api/payment/embed/...` that fronts provider apps' embed shells —
8117
8234
  // e.g. cardcom-payments OpenFields wrapper). The match also covers
package/dist/index.mjs CHANGED
@@ -419,6 +419,120 @@ var BrainerceClient = class {
419
419
  remove: removeById
420
420
  };
421
421
  })();
422
+ // -------------------- Blog --------------------
423
+ /**
424
+ * Read and manage blog posts.
425
+ *
426
+ * ```typescript
427
+ * // Storefront / vibe-coded: list published posts
428
+ * const { data: posts } = await brainerce.blog.getPosts({ category: 'news' });
429
+ *
430
+ * // Fetch one by slug
431
+ * const post = await brainerce.blog.getPost('my-first-post');
432
+ *
433
+ * // Admin: create a draft
434
+ * const draft = await brainerce.blog.create({ title: 'Hello World' });
435
+ * ```
436
+ */
437
+ this.blog = /* @__PURE__ */ (() => {
438
+ const publicBase = "/blog/posts";
439
+ const adminBase = "/blog/posts";
440
+ const requireAdmin = (action) => {
441
+ if (this.isVibeCodedMode() || this.storeId && !this.apiKey) {
442
+ throw new BrainerceError(
443
+ `client.blog.${action}() requires admin mode (apiKey). Vibe-coded and storefront modes are read-only.`,
444
+ 403
445
+ );
446
+ }
447
+ };
448
+ const onNotFound = (err) => {
449
+ if (err instanceof BrainerceError && err.statusCode === 404) return null;
450
+ throw err;
451
+ };
452
+ const buildQuery = (params) => Object.fromEntries(
453
+ Object.entries(params).filter(([, v]) => v != null).map(([k, v]) => [k, String(v)])
454
+ );
455
+ return {
456
+ /**
457
+ * List published posts. Works in all modes.
458
+ * Filters: `category`, `tag`, `page`, `limit`.
459
+ */
460
+ getPosts: (params = {}) => {
461
+ const query = buildQuery(params);
462
+ if (this.isVibeCodedMode()) {
463
+ return this.vibeCodedRequest(
464
+ "GET",
465
+ publicBase,
466
+ void 0,
467
+ query
468
+ );
469
+ }
470
+ if (this.storeId && !this.apiKey) {
471
+ return this.storefrontRequest(
472
+ "GET",
473
+ publicBase,
474
+ void 0,
475
+ query
476
+ );
477
+ }
478
+ return this.adminRequest(
479
+ "GET",
480
+ adminBase,
481
+ void 0,
482
+ query
483
+ );
484
+ },
485
+ /**
486
+ * Fetch one published post by its slug. Returns `null` on 404.
487
+ * Works in all modes.
488
+ */
489
+ getPost: (slug) => {
490
+ const path = `${publicBase}/${encodeURIComponent(slug)}`;
491
+ if (this.isVibeCodedMode()) {
492
+ return this.vibeCodedRequest("GET", path).catch(onNotFound);
493
+ }
494
+ if (this.storeId && !this.apiKey) {
495
+ return this.storefrontRequest("GET", path).catch(onNotFound);
496
+ }
497
+ return this.adminRequest("GET", path).catch(onNotFound);
498
+ },
499
+ /** Create a blog post in DRAFT status. Admin mode only. */
500
+ create: (input) => {
501
+ requireAdmin("create");
502
+ return this.adminRequest("POST", adminBase, input);
503
+ },
504
+ /** Update a blog post by ID. Admin mode only. */
505
+ update: (id, input) => {
506
+ requireAdmin("update");
507
+ return this.adminRequest(
508
+ "PATCH",
509
+ `${adminBase}/${encodeURIComponent(id)}`,
510
+ input
511
+ );
512
+ },
513
+ /** Transition status → PUBLISHED (sets publishedAt = now if unset). Admin mode only. */
514
+ publish: (id) => {
515
+ requireAdmin("publish");
516
+ return this.adminRequest(
517
+ "POST",
518
+ `${adminBase}/${encodeURIComponent(id)}/publish`
519
+ );
520
+ },
521
+ /** Transition status PUBLISHED → DRAFT. Admin mode only. */
522
+ unpublish: (id) => {
523
+ requireAdmin("unpublish");
524
+ return this.adminRequest(
525
+ "POST",
526
+ `${adminBase}/${encodeURIComponent(id)}/unpublish`
527
+ );
528
+ },
529
+ /** Hard-delete a blog post. Admin mode only. */
530
+ remove: async (id) => {
531
+ requireAdmin("remove");
532
+ await this.adminRequest("DELETE", `${adminBase}/${encodeURIComponent(id)}`);
533
+ }
534
+ };
535
+ })();
422
536
  // -------------------- Local Cart (Client-Side for Guests) --------------------
423
537
  // These methods store cart data in localStorage - NO API calls!
424
538
  // Use for guest users in vibe-coded sites
@@ -8042,6 +8156,9 @@ var ALLOWED_PAYMENT_HOSTS = [
8042
8156
  "grow.security",
8043
8157
  // CreditGuard
8044
8158
  "creditguard.co.il",
8159
+ // Takbull
8160
+ "takbull.co.il",
8161
+ "api.takbull.co.il",
8045
8162
  // Brainerce-hosted payment embeds (backend payment-embed proxy at
8046
8163
  // `/api/payment/embed/...` that fronts provider apps' embed shells —
8047
8164
  // e.g. cardcom-payments OpenFields wrapper). The match also covers
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brainerce",
3
- "version": "1.32.0",
3
+ "version": "1.34.0",
4
4
  "description": "Official SDK for building e-commerce storefronts with Brainerce Platform. Perfect for vibe-coded sites, AI-built stores (Cursor, Lovable, v0), and custom storefronts.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",