@wexio/messenger-widget-react 1.0.4 → 1.0.6

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.
Files changed (71) hide show
  1. package/README.md +22 -150
  2. package/dist/{widget-react-BfPdgAQ-.js → widget-react-A2LiLuw0.js} +1151 -1006
  3. package/dist/{widget-react-DwL_rsx_.js → widget-react-B0fjhAJD.js} +72 -56
  4. package/dist/{widget-react-D-QrEVpv.js → widget-react-BC-2nOW4.js} +1 -1
  5. package/dist/{widget-react-CZs0xRrr.js → widget-react-BMpc2uNT2.js} +1 -1
  6. package/dist/widget-react-BNG2P8TD.js +337 -0
  7. package/dist/widget-react-BcPE3ZoY.js +337 -0
  8. package/dist/widget-react-BdgqHrUV.js +337 -0
  9. package/dist/widget-react-BdohGq4s2.js +337 -0
  10. package/dist/widget-react-BthaWcns.js +337 -0
  11. package/dist/widget-react-C-8yYmmX2.js +337 -0
  12. package/dist/widget-react-C2atp9Wc.js +337 -0
  13. package/dist/widget-react-C7J17SWR.js +337 -0
  14. package/dist/{widget-react-BAOIDvhV.js → widget-react-CB6Jxluy.js} +1362 -1328
  15. package/dist/widget-react-CKZ5tNDZ.js +337 -0
  16. package/dist/widget-react-CLRkytx8.js +337 -0
  17. package/dist/widget-react-CMvMg68M.js +337 -0
  18. package/dist/widget-react-COu0Pgwj2.js +337 -0
  19. package/dist/widget-react-CSqqxBrZ2.js +337 -0
  20. package/dist/widget-react-CVLlTbB_2.js +337 -0
  21. package/dist/{widget-react-DtOeNw5z.js → widget-react-CVx50FQP2.js} +18 -9
  22. package/dist/{widget-react-AgErOR36.js → widget-react-CYlAGnu_2.js} +19 -7
  23. package/dist/{widget-react-CPYwD6FR.js → widget-react-CdjAa7d12.js} +2 -2
  24. package/dist/widget-react-Cfgp7JeP.js +337 -0
  25. package/dist/widget-react-CozbWJT5.js +337 -0
  26. package/dist/widget-react-Crxd4qSM.js +337 -0
  27. package/dist/widget-react-D3ZFysG9.js +337 -0
  28. package/dist/widget-react-DAT3UgdW2.js +285 -0
  29. package/dist/widget-react-DRyfg-bL.js +337 -0
  30. package/dist/widget-react-DhC6mv1r.js +337 -0
  31. package/dist/widget-react-DiF8_rWm.js +337 -0
  32. package/dist/widget-react-DjXlAiu5.js +337 -0
  33. package/dist/widget-react-DkhMvmdX2.js +337 -0
  34. package/dist/{widget-react-CUuKGDZN.js → widget-react-DqRO9d652.js} +2 -2
  35. package/dist/widget-react-Dq_sYldT.js +337 -0
  36. package/dist/{widget-react-Cj_zbk_F.js → widget-react-Dr-NACA0.js} +19 -7
  37. package/dist/widget-react-DviuEPoq.js +337 -0
  38. package/dist/{widget-react-CDPzwV1i.js → widget-react-DxkbC0xE.js} +1823 -1833
  39. package/dist/widget-react-JOt2DXbR.js +337 -0
  40. package/dist/widget-react-L8PMkUr7.js +337 -0
  41. package/dist/widget-react-LWisk9g6.js +337 -0
  42. package/dist/{widget-react-DSw03RG7.js → widget-react-_xdGdTzK.js} +1 -1
  43. package/dist/widget-react-fNts-U3I2.js +337 -0
  44. package/dist/widget-react-iKCOmnGh.js +337 -0
  45. package/dist/{widget-react-C5COLy3m.js → widget-react-wTlmXnex2.js} +1 -1
  46. package/dist/widget-react-xzOjEdXk2.js +337 -0
  47. package/dist/widget-react.d.ts +3 -21
  48. package/dist/widget-react.js +4028 -3450
  49. package/package.json +1 -1
  50. package/dist/widget-react-AgErOR36.js.map +0 -1
  51. package/dist/widget-react-BAOIDvhV.js.map +0 -1
  52. package/dist/widget-react-BfPdgAQ-.js.map +0 -1
  53. package/dist/widget-react-BxvW64LH.js.map +0 -1
  54. package/dist/widget-react-C5COLy3m.js.map +0 -1
  55. package/dist/widget-react-CAWxn8I0.js +0 -247
  56. package/dist/widget-react-CAWxn8I0.js.map +0 -1
  57. package/dist/widget-react-CDPzwV1i.js.map +0 -1
  58. package/dist/widget-react-CPYwD6FR.js.map +0 -1
  59. package/dist/widget-react-CUBFPjaV.js.map +0 -1
  60. package/dist/widget-react-CUuKGDZN.js.map +0 -1
  61. package/dist/widget-react-CZs0xRrr.js.map +0 -1
  62. package/dist/widget-react-Cj_zbk_F.js.map +0 -1
  63. package/dist/widget-react-D-QrEVpv.js.map +0 -1
  64. package/dist/widget-react-D8TPnM9r.js.map +0 -1
  65. package/dist/widget-react-DE7AVrCo.js.map +0 -1
  66. package/dist/widget-react-DOX6j7_Q.js.map +0 -1
  67. package/dist/widget-react-DSw03RG7.js.map +0 -1
  68. package/dist/widget-react-Dew7YaLU.js.map +0 -1
  69. package/dist/widget-react-DtOeNw5z.js.map +0 -1
  70. package/dist/widget-react-DwL_rsx_.js.map +0 -1
  71. package/dist/widget-react.js.map +0 -1
@@ -1 +0,0 @@
1
- {"version":3,"file":"widget-react-DOX6j7_Q.js","names":["__iconNode","__iconNode"],"sources":["../node_modules/lucide-react/dist/esm/icons/loader-circle.mjs","../node_modules/lucide-react/dist/esm/icons/pause.mjs","../node_modules/lucide-react/dist/esm/icons/play.mjs"],"sourcesContent":["/**\n * @license lucide-react v1.14.0 - ISC\n *\n * This source code is licensed under the ISC license.\n * See the LICENSE file in the root directory of this source tree.\n */\n\nimport createLucideIcon from '../createLucideIcon.mjs';\n\nconst __iconNode = [[\"path\", { d: \"M21 12a9 9 0 1 1-6.219-8.56\", key: \"13zald\" }]];\nconst LoaderCircle = createLucideIcon(\"loader-circle\", __iconNode);\n\nexport { __iconNode, LoaderCircle as default };\n//# sourceMappingURL=loader-circle.mjs.map\n","/**\n * @license lucide-react v1.14.0 - ISC\n *\n * This source code is licensed under the ISC license.\n * See the LICENSE file in the root directory of this source tree.\n */\n\nimport createLucideIcon from '../createLucideIcon.mjs';\n\nconst __iconNode = [\n [\"rect\", { x: \"14\", y: \"3\", width: \"5\", height: \"18\", rx: \"1\", key: \"kaeet6\" }],\n [\"rect\", { x: \"5\", y: \"3\", width: \"5\", height: \"18\", rx: \"1\", key: \"1wsw3u\" }]\n];\nconst Pause = createLucideIcon(\"pause\", __iconNode);\n\nexport { __iconNode, Pause as default };\n//# sourceMappingURL=pause.mjs.map\n","/**\n * @license lucide-react v1.14.0 - ISC\n *\n * This source code is licensed under the ISC license.\n * See the LICENSE file in the root directory of this source tree.\n */\n\nimport createLucideIcon from '../createLucideIcon.mjs';\n\nconst __iconNode = [\n [\n \"path\",\n {\n d: \"M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z\",\n key: \"10ikf1\"\n }\n ]\n];\nconst Play = createLucideIcon(\"play\", __iconNode);\n\nexport { __iconNode, Play as default };\n//# sourceMappingURL=play.mjs.map\n"],"x_google_ignoreList":[0,1,2],"mappings":";;;;;AAUA,IAAM,IAAe,EAAiB,iBAAiBA,CADnC,CAAC,QAAQ;CAAE,GAAG;CAA+B,KAAK;CAAU,CAAC,CAC1BA,CAAW,ECG5D,IAAQ,EAAiB,SAASC,CAHtC,CAAC,QAAQ;CAAE,GAAG;CAAM,GAAG;CAAK,OAAO;CAAK,QAAQ;CAAM,IAAI;CAAK,KAAK;CAAU,CAAC,EAC/E,CAAC,QAAQ;CAAE,GAAG;CAAK,GAAG;CAAK,OAAO;CAAK,QAAQ;CAAM,IAAI;CAAK,KAAK;CAAU,CAAC,CAExCA,CAAW,ECK7C,IAAO,EAAiB,QAAQ,CARpC,CACE,QACA;CACE,GAAG;CACH,KAAK;CACN,CACF,CAEmC,CAAW"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"widget-react-DSw03RG7.js","names":[],"sources":["../messages/demo.en.json"],"sourcesContent":["{\n \"recentMessage\": {\n \"sender\": \"Gaia\",\n \"preview\": \"Hi there 👋 Thanks for reaching out. I'll get back to you shortly…\",\n \"ageLabel\": \"4h\"\n },\n \"news\": {\n \"demo-news-1\": {\n \"title\": \"Pioneer 2026: save the date\",\n \"excerpt\": \"We're back in San Francisco on October 8. Doors at 9, keynote at 10.\",\n \"body\": \"## What to expect\\n\\nThis year's keynote is all about what we shipped in the open and what's coming next. Live demos, not slides.\\n\\n- **The Gaia roadmap** - where our AI is headed over the next two quarters.\\n- **Customer stories** - three teams share how they halved first-response time.\\n- **Hands-on labs** - bring a laptop and build a flow with our engineers.\\n\\n## Getting there\\n\\nThe venue is the Pier 27 Cruise Terminal, right on the Embarcadero. Closest BART stop is Embarcadero, a 12-minute walk along the water.\\n\\n## Tickets\\n\\nEarly-bird seats are limited and go live next week. Existing customers get first dibs, so keep an eye on your inbox.\\n\\n> Can't make it in person? The keynote streams live and the recordings land here the same evening.\"\n },\n \"demo-news-2\": {\n \"title\": \"What shipped in April\",\n \"excerpt\": \"Three new automation triggers and a much faster inbox search.\",\n \"body\": \"<p>April was a big month. Here's everything that shipped, at a glance:</p>\\n<table>\\n<thead><tr><th>Area</th><th>What changed</th><th>Why it matters</th></tr></thead>\\n<tbody>\\n<tr><td>Inbox search</td><td>Instant, as-you-type results</td><td>50k conversations, under 100ms</td></tr>\\n<tr><td>Automation</td><td>Three new triggers</td><td>Cart abandoned, page revisited, quiet visitor</td></tr>\\n<tr><td>Composer</td><td>Draft survives a tab switch</td><td>No more lost half-typed replies</td></tr>\\n<tr><td>Dark mode</td><td>No white flash on first paint</td><td>Cleaner load on dark sites</td></tr>\\n</tbody>\\n</table>\\n<p>Here's the new search in action - results land before you finish typing:</p>\\n<img src=\\\"https://picsum.photos/seed/wexio-search/1024/576\\\" alt=\\\"Inbox search returning results instantly\\\" />\\n<p>Tap the image to take a closer look. Got a request? Reply here - we read every one.</p>\"\n },\n \"demo-news-3\": {\n \"title\": \"Gaia now speaks 28 languages\",\n \"excerpt\": \"Auto-detect the visitor's language or pin one. No extra setup.\",\n \"body\": \"<p>Gaia now replies in 28 languages, detected automatically from the visitor's first message. The questions we hear most:</p>\\n<details><summary>How does language detection work?</summary><p>Gaia reads the opening message and replies in the same language. If your help articles exist in that language it cites them; otherwise it answers from the closest match and flags the gap to you.</p></details>\\n<details><summary>Can I force a single language?</summary><p>Yes. Pin one in <strong>Settings → AI → Language</strong> and Gaia sticks to it no matter what the visitor types.</p></details>\\n<details><summary>What do operators see?</summary><p>Transcripts stay in your team's language, with the original message kept alongside the translation.</p></details>\\n<p>A quick look at Gaia switching languages mid-thread:</p>\\n<img src=\\\"https://picsum.photos/seed/wexio-lang/1024/576\\\" alt=\\\"Gaia replying across several languages\\\" />\"\n },\n \"demo-news-4\": {\n \"title\": \"Pipe conversations straight into Slack\",\n \"excerpt\": \"Connect any incoming webhook to a Slack channel. No glue code.\",\n \"body\": \"## Connect in two clicks\\n\\nHead to **Integrations → Slack**, hit *Connect*, and approve the scopes. Pick the channel new conversations should land in - most teams use `#support`.\\n\\n## What you get\\n\\n- A Slack message the moment a chat starts, with a link back to the inbox.\\n- Operator replies threaded under the original message.\\n- Reply right from Slack and we post it back to the visitor in real time.\\n\\n## Webhooks too\\n\\nAlready pushing events from your own systems? Point any webhook at your Slack channel and skip the custom integration entirely.\"\n },\n \"demo-news-5\": {\n \"title\": \"Inbox 2.0: triage without the mouse\",\n \"excerpt\": \"New shortcuts for assigning, snoozing, and resolving - all from the keyboard.\",\n \"body\": \"<p>Inbox 2.0 is keyboard-first. Here's the cheat sheet, grouped by what you're doing:</p>\\n<div data-type=\\\"tabs\\\">\\n<div data-type=\\\"tab\\\"><div data-tab-label>Navigate</div><ul><li><code>j</code> / <code>k</code> - next / previous conversation</li><li><code>g</code> then <code>i</code> - jump to the inbox</li></ul></div>\\n<div data-type=\\\"tab\\\"><div data-tab-label>Act</div><ul><li><code>e</code> - resolve</li><li><code>a</code> - assign to me</li><li><code>s</code> - snooze</li></ul></div>\\n<div data-type=\\\"tab\\\"><div data-tab-label>Compose</div><ul><li><code>Cmd+Enter</code> - send</li><li><code>/</code> - saved replies</li><li><code>Cmd+K</code> - command palette</li></ul></div>\\n</div>\\n<p>The redesigned inbox - everything within a key's reach:</p>\\n<img src=\\\"https://picsum.photos/seed/wexio-inbox/1024/576\\\" alt=\\\"The redesigned keyboard-first inbox\\\" />\"\n },\n \"demo-news-6\": {\n \"title\": \"Self-serve data exports and deletion\",\n \"excerpt\": \"Handle visitor data requests in a click. GDPR-ready out of the box.\",\n \"body\": \"## Export\\n\\nFrom any visitor's profile, hit **Export data** to generate a JSON bundle of every message, event, and attribute we hold for them. It downloads instantly and logs the request for your records.\\n\\n## Delete\\n\\n**Delete data** removes the visitor and scrubs their messages from the inbox, search, and any connected CRM through the same webhook bridge. The action is logged and irreversible.\\n\\n## For your DPO\\n\\n- Every export and deletion is timestamped and attributed.\\n- Retention windows are configurable per workspace.\\n- A data-processing agreement is available on request.\"\n },\n \"demo-news-7\": {\n \"title\": \"Mobile SDK beta: iOS and Android\",\n \"excerpt\": \"Bring the same conversation into your native app. Beta seats open now.\",\n \"body\": \"## One conversation, every surface\\n\\nA visitor who starts a chat on your website can pick it up in your app, signed in, with full history. Same Gaia, same operators, same inbox.\\n\\n## What's in the beta\\n\\n- Native launcher for SwiftUI and Jetpack Compose.\\n- Push notifications for new replies.\\n- Offline queue, so messages send when the connection comes back.\\n\\n## Join\\n\\nWe're onboarding a handful of teams each week. Reply here with your platform and rough monthly active users and we'll send you a key.\\n\\n> Beta means rough edges. We ship fixes weekly and read every bug report.\"\n },\n \"demo-news-8\": {\n \"title\": \"See which AI answers actually worked\",\n \"excerpt\": \"Track whether each Gaia reply led to a resolution, a handoff, or a bounce.\",\n \"body\": \"## The new view\\n\\n**Analytics → AI outcomes** breaks down every Gaia reply by what happened next:\\n\\n- **Resolved** - the visitor got what they needed and left happy.\\n- **Handed off** - Gaia passed the conversation to a human.\\n- **Bounced** - the visitor closed the widget without resolving.\\n\\n## Per-article insight\\n\\nDrill into any help article to see how often Gaia cites it and whether those citations resolve. Low resolution on a high-traffic article usually means the content needs a rewrite, not the AI.\\n\\n## Act on it\\n\\nSort by bounce rate to find the questions Gaia struggles with most, then add a flow or a sharper article to close the gap.\"\n },\n \"tags\": {\n \"events\": \"Events\",\n \"product\": \"Product\",\n \"release\": \"Release\",\n \"ai\": \"AI\",\n \"languages\": \"Languages\",\n \"integrations\": \"Integrations\",\n \"slack\": \"Slack\",\n \"inbox\": \"Inbox\",\n \"privacy\": \"Privacy\",\n \"security\": \"Security\",\n \"mobile\": \"Mobile\",\n \"beta\": \"Beta\",\n \"analytics\": \"Analytics\"\n },\n \"categories\": {\n \"company\": \"Company\",\n \"product\": \"Product\",\n \"engineering\": \"Engineering\"\n }\n },\n \"help\": {\n \"categories\": {\n \"demo-cat-getting-started\": {\n \"title\": \"Getting started\",\n \"description\": \"Install the widget and configure your first conversation.\"\n },\n \"demo-cat-automation\": {\n \"title\": \"Automation\",\n \"description\": \"Flows, triggers, and the AI auto-reply.\"\n },\n \"demo-cat-integrations\": {\n \"title\": \"Integrations\",\n \"description\": \"Connect your CRM, Slack, and CMS.\"\n },\n \"demo-cat-billing\": {\n \"title\": \"Billing & plans\",\n \"description\": \"Pricing tiers, invoices, and seat management.\"\n }\n },\n \"articles\": {\n \"demo-article-install\": {\n \"title\": \"Install the Wexio widget on your site\",\n \"excerpt\": \"A 30-second snippet you paste before the closing </body> tag.\",\n \"body\": \"## Add the loader script\\n\\nPaste this snippet into every page where you want the widget to appear:\\n\\n```html\\n<script\\n src=\\\"https://cdn.wexio.io/loader.js\\\"\\n data-public-key=\\\"pk_live_YOUR_KEY\\\"\\n></script>\\n```\\n\\nThe loader injects a floating launcher in the bottom-right corner. Click it to open the chat panel.\\n\\n## Verify the install\\n\\n- Visit a page that includes the snippet.\\n- Open the browser console.\\n- You should see `[wexio] widget mounted`.\\n\\n## Customise the look\\n\\nThe widget reads brand colours from your dashboard. Update them in **Settings → Brand**.\\n\\n> Need a hand? Tap \\\"Still need help? Message us\\\" below and we'll walk you through it.\"\n },\n \"demo-article-flows\": {\n \"title\": \"Build your first automation flow\",\n \"excerpt\": \"Drag, drop, and ship a welcome bot in under five minutes.\",\n \"body\": \"## What's a flow?\\n\\nA flow is a tiny tree of conversation steps - questions, replies, and conditions - that runs whenever a visitor matches a trigger.\\n\\n## Three trigger types\\n\\n1. **Visitor opened the widget** - greet first-timers.\\n2. **Visitor asked a question** - Gaia (AI) tries to answer; if it can't, the flow takes over.\\n3. **Webhook received** - your CMS or e-commerce platform pushes an event; the flow kicks off in the right chat.\\n\\n## Best practices\\n\\n- Keep first replies under 80 characters.\\n- Offer 2-3 quick-reply chips, not a free-text question.\\n- Always provide an escape hatch - *\\\"Talk to a human\\\"*.\"\n },\n \"demo-article-slack\": {\n \"title\": \"Forward chats to Slack\",\n \"excerpt\": \"Mirror every new conversation into a Slack channel of your choice.\",\n \"body\": \"## Connect your workspace\\n\\nIn **Integrations → Slack** click *Connect*. You'll be redirected to Slack's OAuth screen - approve the requested scopes (`chat:write`, `channels:read`).\\n\\n## Pick a channel\\n\\nAfter authorising, choose the target channel (or create a new one like `#support`).\\n\\n## What gets forwarded\\n\\n- The opening message, with a deeplink back to the wexio inbox.\\n- Operator replies as threaded messages.\\n- Visitor replies in real time via the SSE bridge.\\n\\n## Reverse direction\\n\\nOperators can reply *from* Slack - wexio listens for Slack thread replies and posts them back to the widget.\"\n },\n \"demo-article-billing\": {\n \"title\": \"Update your plan and billing details\",\n \"excerpt\": \"Switch tiers, change card, or download invoices.\",\n \"body\": \"## Plans\\n\\n- **Free** - 1 widget, basic features, \\\"Powered by Wexio\\\" badge.\\n- **Standard** - unlimited widgets, automations, integrations.\\n- **Enterprise** - white-label, SLA, dedicated support.\\n\\n## Change plan\\n\\n**Settings → Billing → Change plan**. Upgrades take effect immediately; downgrades apply at the end of the current cycle.\\n\\n## Invoices\\n\\nDownload every invoice as PDF from **Billing → History**. We also email a copy on each renewal.\"\n },\n \"demo-article-theme\": {\n \"title\": \"Customise the widget theme\",\n \"excerpt\": \"Match the widget to your brand - colours, radii, dark mode.\",\n \"body\": \"## Brand colours\\n\\nIn **Settings → Brand**, set:\\n\\n- **Primary** - drives the launcher, send button, focus rings.\\n- **Background** - the panel surface (light) and dark-mode pair.\\n\\n## Radii\\n\\nPick from `sm`, `md`, `lg` for a tighter or softer look. The change applies live to every widget on every page.\\n\\n## Dark mode\\n\\nDefault = follow the visitor's system preference. Pin to **always light** or **always dark** if your site doesn't support dark mode itself.\"\n },\n \"demo-article-handoff\": {\n \"title\": \"Hand off from Gaia to a human operator\",\n \"excerpt\": \"When AI isn't enough - route the visitor to a teammate.\",\n \"body\": \"## Triggers\\n\\nGaia hands off automatically when:\\n\\n- The visitor types *\\\"talk to a human\\\"* (or any localised variant).\\n- The same question is asked twice with no resolution.\\n- The visitor's intent matches a routing rule (e.g., billing → finance team).\\n\\n## Routing\\n\\nSet up **Routing → Rules** to direct certain intents to specific teams or operators. Round-robin and least-loaded are both supported.\\n\\n## Out-of-hours\\n\\nIf no operator is online, Gaia offers a callback or email-handoff form instead of pretending someone's available.\"\n },\n \"demo-article-crm\": {\n \"title\": \"Sync conversations to your CRM\",\n \"excerpt\": \"Push every chat into HubSpot, Salesforce, or Pipedrive.\",\n \"body\": \"## Supported CRMs\\n\\n- HubSpot (OAuth)\\n- Salesforce (OAuth)\\n- Pipedrive (API token)\\n- Custom via webhooks\\n\\n## What syncs\\n\\n- New conversations create a contact (if not already present) and a note.\\n- Operator replies append to the contact timeline.\\n- Tags applied in wexio mirror to the CRM.\\n\\n## Two-way\\n\\nOperator notes added in the CRM can post back to wexio via the webhook receiver - see *Integrations → Webhooks → Inbound*.\"\n },\n \"demo-article-shortcuts\": {\n \"title\": \"Operator keyboard shortcuts\",\n \"excerpt\": \"Triage twice as fast - no mouse required.\",\n \"body\": \"## Inbox navigation\\n\\n- `j` / `k` - next / previous conversation\\n- `e` - resolve\\n- `a` - assign to me\\n- `s` - snooze\\n\\n## Composer\\n\\n- `Cmd+Enter` - send\\n- `/` - open saved-reply picker\\n- `Cmd+K` - command palette (everything)\\n\\n## Tip\\n\\nHover any button in the dashboard - the tooltip shows its shortcut.\"\n }\n },\n \"tags\": {\n \"setup\": \"Setup\",\n \"install\": \"Install\",\n \"automation\": \"Automation\",\n \"flows\": \"Flows\",\n \"integrations\": \"Integrations\",\n \"slack\": \"Slack\",\n \"billing\": \"Billing\",\n \"plans\": \"Plans\",\n \"design\": \"Design\",\n \"ai\": \"AI\",\n \"crm\": \"CRM\",\n \"productivity\": \"Productivity\",\n \"inbox\": \"Inbox\"\n }\n },\n \"conversation\": {\n \"visitorName\": \"You\",\n \"messages\": {\n \"m-demo-1\": {\n \"text\": \"Hi there 👋 Thanks for reaching out. I'm Gaia, the wexio AI - happy to help.\",\n \"ageLabel\": \"4h\",\n \"buttons\": [\"How does pricing work?\", \"What is Gaia?\", \"Talk to a human\"]\n },\n \"m-demo-2\": {\n \"text\": \"I'm evaluating the widget for our docs site - does it support dark mode out of the box?\",\n \"ageLabel\": \"4h\"\n },\n \"m-demo-3\": {\n \"text\": \"Yes - the widget follows your visitors' system preference by default, and operators can pin to light or dark from the dashboard.\",\n \"ageLabel\": \"4h\"\n },\n \"m-demo-4\": {\n \"text\": \"Here's a screenshot of the dashboard theme controls 👇\",\n \"ageLabel\": \"4h\",\n \"mediaAlts\": [\"Dashboard theme controls\"]\n },\n \"m-demo-gallery-captioned\": {\n \"text\": \"Three real installs we shipped last week - different industries, same setup time.\",\n \"ageLabel\": \"4h\",\n \"mediaAlts\": [\"SaaS dashboard install\", \"E-commerce checkout install\", \"Docs site install\"]\n },\n \"m-demo-gallery-bare\": {\n \"text\": \"\",\n \"ageLabel\": \"4h\",\n \"mediaAlts\": [\n \"My setup, frame 1\",\n \"My setup, frame 2\",\n \"My setup, frame 3\",\n \"My setup, frame 4\"\n ]\n },\n \"m-demo-5\": {\n \"text\": \"Want a 30-second tour of how visitors interact with the widget? Tap play 👇\",\n \"ageLabel\": \"4h\",\n \"mediaAlts\": [\"Widget walkthrough\"]\n },\n \"m-demo-6\": {\n \"text\": \"Neat. And the docs?\",\n \"ageLabel\": \"3h\"\n },\n \"m-demo-7\": {\n \"text\": \"Here's the full integration guide as a PDF - start with §2 if you've already got a Wexio account.\",\n \"ageLabel\": \"3h\",\n \"mediaAlts\": [\"Wexio integration guide.pdf\"]\n },\n \"m-demo-8\": {\n \"text\": \"And a quick voice note explaining the auth flow - easier than typing it out:\",\n \"ageLabel\": \"3h\",\n \"mediaAlts\": [\"Auth flow voice note\"]\n },\n \"m-demo-9\": {\n \"text\": \"Perfect. One more - what's the easiest way to get started?\",\n \"ageLabel\": \"2h\"\n },\n \"m-demo-10\": {\n \"text\": \"Pick the path that fits - we can spin up a sandbox, send the install snippet, or jump on a 10-minute call:\",\n \"ageLabel\": \"2h\",\n \"buttons\": [\n \"📦 Start a sandbox project\",\n \"📋 Send me the install snippet\",\n \"📞 Book a 10-min call\",\n \"💬 Keep chatting here\"\n ]\n }\n }\n },\n \"messenger\": {\n \"welcome\": {\n \"system\": \"You're now chatting with Gaia, our AI assistant.\",\n \"hint\": \"Tip: attach a file or record a voice note any time.\",\n \"greeting\": \"Hi there 👋 I'm Gaia, the Wexio AI. Ask me anything, or pick a quick action to get started.\"\n },\n \"replies\": {\n \"pricing\": {\n \"text\": \"Wexio is free for one widget; paid plans start at $39/mo and unlock automations, integrations, and white-label.\",\n \"planBtn\": \"Show plan tiers\",\n \"salesBtn\": \"Talk to sales\"\n },\n \"gaia\": {\n \"text\": \"Gaia is the AI agent that answers visitor questions instantly. It learns from your help articles and hands off to a human when it can't help.\",\n \"handoffBtn\": \"How does handoff work?\"\n },\n \"human\": {\n \"text\": \"Of course - looping in Alex from the team. They'll respond within a few minutes during business hours.\"\n },\n \"fallback\": {\n \"text\": \"Got it - let me check on that and circle back in a moment.\"\n }\n }\n }\n}\n"],"mappings":""}
@@ -1 +0,0 @@
1
- {"version":3,"file":"widget-react-Dew7YaLU.js","names":[],"sources":["../node_modules/lucide-react/dist/esm/icons/send.mjs"],"sourcesContent":["/**\n * @license lucide-react v1.14.0 - ISC\n *\n * This source code is licensed under the ISC license.\n * See the LICENSE file in the root directory of this source tree.\n */\n\nimport createLucideIcon from '../createLucideIcon.mjs';\n\nconst __iconNode = [\n [\n \"path\",\n {\n d: \"M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z\",\n key: \"1ffxy3\"\n }\n ],\n [\"path\", { d: \"m21.854 2.147-10.94 10.939\", key: \"12cjpa\" }]\n];\nconst Send = createLucideIcon(\"send\", __iconNode);\n\nexport { __iconNode, Send as default };\n//# sourceMappingURL=send.mjs.map\n"],"x_google_ignoreList":[0],"mappings":";;;;;AAmBA,IAAM,IAAO,EAAiB,QAAQ,CATpC,CACE,QACA;CACE,GAAG;CACH,KAAK;CACN,CACF,EACD,CAAC,QAAQ;CAAE,GAAG;CAA8B,KAAK;CAAU,CAAC,CAExB,CAAW"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"widget-react-DtOeNw5z.js","names":[],"sources":["../components/widget/news-article-view/helpers.ts","../components/widget/news-article-view/related-news-list.tsx","../components/widget/news-article-view/index.tsx"],"sourcesContent":["/**\n * Normalised news-post projection + the extractor that produces it.\n * The same shape is rendered whether the source is mock data, the\n * dashboard's prefetch, or the visitor `visitorNewsPost({ id })` query.\n */\nexport interface NewsArticle {\n id: string;\n title: string;\n excerpt: string;\n contentMarkdown?: string;\n bodyFormat?: \"html\" | \"markdown\" | string;\n coverImageUrl?: string;\n coverGradient?: [string, string];\n publishedAt: string;\n upstreamUpdatedAt?: string | null;\n sourceUrl?: string;\n reactionCounts?: Record<string, number> | null;\n /** The visitor's own reaction slotIndex on this post's group (or null).\n * Drives the reaction-bar active highlight; from the BE read query. */\n viewerReaction?: number | null;\n tags?: { slug: string; label: string }[];\n /** Used by client-side \"related\" computation in preview mode — the\n * BE's `visitorNewsRelated` query handles this in real mode. We\n * project the category id list so the preview can do an overlap\n * count without hitting the visitor surface. */\n categoryIds?: string[];\n tagIds?: string[];\n /** Post byline — one or more authors (demo content; live news posts\n * may not carry authors). */\n authors?: { _id: string; name: string; photo: { url: string } }[];\n /** Section label shown as an eyebrow above the title. */\n category?: { slug: string; label: string };\n /** Locale of THIS post — drives the active chip in the language\n * selector. */\n locale?: string;\n /** Published locale variants (incl. self). The widget opens a\n * variant by `id` (the visitor news lookup is id-keyed, so locale\n * doesn't need to travel with it). */\n translations?: { id: string; locale: string }[];\n}\n\n/** Coerce the BE's raw post JSON (or an operator-side post row) into\n * the `NewsArticle` shape. Defensive on every field — the same\n * extractor handles `_id`-vs-`id` and the populated-Media cover vs\n * the externally-pulled URL fallback. */\nexport function extractArticle(raw: unknown): NewsArticle | null {\n if (!raw || typeof raw !== \"object\") return null;\n const o = raw as Record<string, unknown>;\n const id = String(o._id ?? o.id ?? \"\");\n if (!id) return null;\n const mediaCover = (o.coverImageUrl as Record<string, unknown> | undefined)?.url;\n const externalCover = o.externalCoverImageUrl;\n const cover =\n typeof mediaCover === \"string\"\n ? mediaCover\n : typeof externalCover === \"string\"\n ? externalCover\n : undefined;\n const tags = Array.isArray(o.tags)\n ? (o.tags as unknown[])\n .map((t) => {\n if (!t || typeof t !== \"object\") return null;\n const slug = (t as { slug?: unknown }).slug;\n const label = (t as { label?: unknown }).label;\n if (typeof slug !== \"string\" || typeof label !== \"string\") {\n return null;\n }\n return { slug, label };\n })\n .filter((t): t is { slug: string; label: string } => !!t)\n : undefined;\n const idsFromObjList = (list: unknown): string[] | undefined => {\n if (!Array.isArray(list)) return undefined;\n const out: string[] = [];\n for (const x of list) {\n if (!x) continue;\n if (typeof x === \"string\") {\n out.push(x);\n } else if (typeof x === \"object\") {\n const xid = (x as { _id?: unknown; id?: unknown })._id ?? (x as { id?: unknown }).id;\n if (typeof xid === \"string\") out.push(xid);\n }\n }\n return out.length > 0 ? out : undefined;\n };\n // Published locale variants → `{ id, locale }`. The widget opens a\n // variant by id, so we only need those two fields.\n const translations = Array.isArray(o.translations)\n ? (o.translations as unknown[])\n .map((tr) => {\n if (!tr || typeof tr !== \"object\") return null;\n const tid = String(\n (tr as { _id?: unknown; id?: unknown })._id ?? (tr as { id?: unknown }).id ?? \"\",\n );\n const loc = (tr as { locale?: unknown }).locale;\n if (!tid || typeof loc !== \"string\") return null;\n return { id: tid, locale: loc };\n })\n .filter((tr): tr is { id: string; locale: string } => !!tr)\n : undefined;\n // Authors (operator byline) — `{ _id, name, photo: { url } }`. BE\n // posts may carry 0..N; demo content sets its own. The article view\n // renders up to 3 avatars + a \"Written by …\" line.\n const authors = Array.isArray(o.authors)\n ? (o.authors as unknown[])\n .map((a) => {\n if (!a || typeof a !== \"object\") return null;\n const name = (a as { name?: unknown }).name;\n if (typeof name !== \"string\") return null;\n const aid = (a as { _id?: unknown })._id;\n const url = (a as { photo?: { url?: unknown } }).photo?.url;\n return {\n _id: String(aid ?? name),\n name,\n photo: { url: typeof url === \"string\" ? url : \"\" },\n };\n })\n .filter((a): a is { _id: string; name: string; photo: { url: string } } => !!a)\n : undefined;\n return {\n id,\n title: typeof o.title === \"string\" ? o.title : \"\",\n excerpt: typeof o.excerpt === \"string\" ? o.excerpt : \"\",\n contentMarkdown: typeof o.contentMarkdown === \"string\" ? o.contentMarkdown : undefined,\n bodyFormat: typeof o.bodyFormat === \"string\" ? o.bodyFormat : undefined,\n coverImageUrl: cover,\n publishedAt: typeof o.publishedAt === \"string\" ? o.publishedAt : new Date().toISOString(),\n upstreamUpdatedAt: typeof o.upstreamUpdatedAt === \"string\" ? o.upstreamUpdatedAt : undefined,\n sourceUrl: typeof o.sourceUrl === \"string\" ? o.sourceUrl : undefined,\n reactionCounts: (o.reactionCounts as Record<string, number> | null | undefined) ?? null,\n viewerReaction: typeof o.viewerReaction === \"number\" ? o.viewerReaction : null,\n tags,\n authors,\n categoryIds: idsFromObjList(o.categories),\n tagIds: idsFromObjList(o.tags),\n locale: typeof o.locale === \"string\" ? o.locale : undefined,\n translations,\n };\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\n\n/**\n * Related-articles list rendered at the foot of the article detail\n * view. Two data paths:\n * - **Real mode** → `visitorNewsRelated` (BE pre-sorted by\n * shared-taxonomy overlap, then `publishedAt` desc).\n * - **Preview mode** → client-side overlap pass against\n * `env.previewData.newsPostsById` so the dashboard mirrors what\n * visitors would see without a BE round-trip.\n * Dummy / preview-without-overlap render nothing.\n */\nexport function RelatedNewsList({\n currentId,\n currentCategoryIds,\n currentTagIds,\n previewPostsById,\n visitorRelatedRaw,\n isDummy,\n onOpen,\n}: {\n currentId: string;\n currentCategoryIds: string[];\n currentTagIds: string[];\n previewPostsById: Record<string, unknown> | undefined;\n visitorRelatedRaw: unknown;\n isDummy: boolean;\n onOpen: (id: string) => void;\n}) {\n const t = useTranslations(\"news\");\n\n const items: { id: string; title: string; coverImageUrl?: string }[] = (() => {\n if (isDummy) return [];\n\n // BE result wins when it's a non-empty array. The visitor surface's\n // `visitorNewsRelated` is the authoritative source — we prefer it\n // over the client-side overlap fallback even when both are reachable\n // (i.e. dashboard preview pane with prefetch AND a successful\n // visitor-side fetch).\n if (Array.isArray(visitorRelatedRaw) && visitorRelatedRaw.length > 0) {\n return visitorRelatedRaw\n .filter((x): x is Record<string, unknown> => !!x && typeof x === \"object\")\n .map((r) => {\n const cover =\n (r.coverImageUrl as Record<string, unknown> | undefined)?.url ??\n r.externalCoverImageUrl;\n return {\n id: String(r._id ?? r.id ?? \"\"),\n title: typeof r.title === \"string\" ? r.title : \"\",\n coverImageUrl: typeof cover === \"string\" ? cover : undefined,\n };\n })\n .filter((r) => r.id && r.title);\n }\n\n // Fallback: client-side overlap against the dashboard's prefetched\n // lookup. Skipped when the lookup is missing / empty, or when the\n // seed article has no taxonomy refs.\n if (\n previewPostsById &&\n Object.keys(previewPostsById).length > 0 &&\n (currentCategoryIds.length > 0 || currentTagIds.length > 0)\n ) {\n const seedCats = new Set(currentCategoryIds);\n const seedTags = new Set(currentTagIds);\n const scored: {\n id: string;\n title: string;\n coverImageUrl?: string;\n overlap: number;\n publishedAt: string;\n }[] = [];\n for (const [pid, raw] of Object.entries(previewPostsById)) {\n if (pid === currentId) continue;\n if (!raw || typeof raw !== \"object\") continue;\n const r = raw as Record<string, unknown>;\n const cats = Array.isArray(r.categories) ? r.categories : [];\n const tagsArr = Array.isArray(r.tags) ? r.tags : [];\n const idsFrom = (arr: unknown[]): Set<string> => {\n const s = new Set<string>();\n for (const x of arr) {\n if (x && typeof x === \"object\") {\n const xid = (x as { _id?: unknown; id?: unknown })._id ?? (x as { id?: unknown }).id;\n if (typeof xid === \"string\") s.add(xid);\n }\n }\n return s;\n };\n const catIds = idsFrom(cats);\n const tagIds = idsFrom(tagsArr);\n let overlap = 0;\n for (const c of catIds) if (seedCats.has(c)) overlap++;\n for (const ti of tagIds) if (seedTags.has(ti)) overlap++;\n if (overlap === 0) continue;\n const cover =\n (r.coverImageUrl as Record<string, unknown> | undefined)?.url ?? r.externalCoverImageUrl;\n scored.push({\n id: pid,\n title: typeof r.title === \"string\" ? r.title : \"\",\n coverImageUrl: typeof cover === \"string\" ? cover : undefined,\n overlap,\n publishedAt: typeof r.publishedAt === \"string\" ? r.publishedAt : \"\",\n });\n }\n scored.sort((a, b) =>\n b.overlap !== a.overlap\n ? b.overlap - a.overlap\n : b.publishedAt.localeCompare(a.publishedAt),\n );\n return scored\n .slice(0, 5)\n .map(({ id, title, coverImageUrl }) => ({ id, title, coverImageUrl }));\n }\n\n return [];\n })();\n\n if (items.length === 0) return null;\n\n return (\n <section className=\"mt-6\">\n <p className=\"mb-2 text-xs font-semibold tracking-wide text-wx-fg-muted uppercase\">\n {t(\"relatedTitle\")}\n </p>\n <ul className=\"flex flex-col gap-2\">\n {items.map((item) => (\n <li key={item.id}>\n <button\n type=\"button\"\n onClick={() => onOpen(item.id)}\n className=\"flex w-full items-center gap-3 rounded-wx-lg bg-wx-bg-elevated px-3 py-2 text-left transition-colors hover:bg-wx-bg-elevated/80 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-wx-primary\"\n >\n <span\n className=\"aspect-square w-12 shrink-0 rounded-wx-sm bg-cover bg-center\"\n style={\n item.coverImageUrl\n ? { backgroundImage: `url(${item.coverImageUrl})` }\n : { background: \"linear-gradient(135deg, #1e1f21, #121314)\" }\n }\n aria-hidden=\"true\"\n />\n <span className=\"min-w-0 flex-1 truncate text-sm font-medium text-wx-fg\">\n {item.title}\n </span>\n </button>\n </li>\n ))}\n </ul>\n </section>\n );\n}\n","\"use client\";\n\nimport { ExternalLink } from \"lucide-react\";\nimport { useLocale, useTranslations } from \"next-intl\";\nimport { useState } from \"react\";\nimport { ScrollArea } from \"@/components/ui/scroll-area\";\nimport { getMockNewsItems } from \"@/dummy\";\nimport { looksLikeHtml } from \"@/lib/detect-html\";\nimport { ReactionSurface } from \"@/lib/graphql/generated/types\";\nimport {\n useVisitorNewsPostQuery,\n useVisitorNewsRelatedQuery,\n} from \"@/lib/graphql/queries/generated/news.generated\";\nimport { useArticleTitleSink } from \"@/lib/use-article-title-sink\";\nimport { DEMO_LOCALES, useDemoTranslator } from \"@/lib/use-demo-translator\";\nimport { useWidgetConfig } from \"@/lib/use-widget-config\";\nimport { useWidgetEnv } from \"@/lib/use-widget-env\";\nimport { cn } from \"@/lib/utils\";\nimport { ArticleBody } from \"../article-body\";\nimport { LanguageSelector } from \"../language-selector\";\nimport { ReactionBar } from \"../reaction-bar\";\nimport { formatNewsEditedAgo, isNewsPostEdited } from \"../tabs/news-tab\";\nimport { extractArticle, type NewsArticle } from \"./helpers\";\nimport { RelatedNewsList } from \"./related-news-list\";\n\ninterface NewsArticleViewProps {\n id: string;\n isDummy: boolean;\n /** Tap a related-article tile → swap the view to that post. The\n * shell wires this to the same `news-article` route swap the news\n * card click already uses, so back-navigation lands on the feed. */\n onOpenRelated?: (id: string) => void;\n /** Title sink for the widget shell's sticky header. Called with\n * the article title once data resolves, and with `null` on\n * unmount to clear the header back to the parent tab's name. */\n onTitleResolved?: (title: string | null) => void;\n}\n\n/**\n * News post detail view. Hydration order:\n * 1. **Dummy mode** → match `getMockNewsItems()` by id.\n * 2. **Preview mode** → use `env.previewData.newsPostsById[id]` (the\n * dashboard prefetched the full post via its operator GraphQL).\n * 3. **Live mode** → call `useVisitorNewsPostByIdQuery`.\n *\n * Body renders via `react-markdown` when `bodyFormat` is markdown (or\n * unset — markdown is the default). HTML-format posts route through\n * `dangerouslySetInnerHTML` since the BE ingest already sanitises them.\n *\n * Back navigation lives on the shell's `TabTitleHeader` — this\n * component just owns the scroll surface.\n */\nexport function NewsArticleView({\n id,\n isDummy,\n onOpenRelated,\n onTitleResolved,\n}: NewsArticleViewProps) {\n const t = useTranslations(\"news\");\n const tDemo = useTranslations(\"demo\");\n const locale = useLocale();\n const env = useWidgetEnv();\n const { config } = useWidgetConfig();\n const showRelated = config.messenger?.showRelatedNews ?? true;\n const showReactionCounts = config.messenger?.showReactionCounts ?? true;\n\n // Article-scoped language switch in demo mode — defaults to the widget\n // locale, changed by the below-article dropdown without touching the\n // widget's own locale.\n const [demoLocale, setDemoLocale] = useState(locale);\n const demoT = useDemoTranslator(demoLocale, tDemo);\n\n // Preview takes priority over the live query — the dashboard has\n // already loaded the full post, so we skip the visitor round-trip.\n const previewRaw = !isDummy ? env.previewData?.newsPostsById?.[id] : null;\n const hasPreview = !!previewRaw;\n\n const { data, loading } = useVisitorNewsPostQuery({\n variables: { args: { id, locale } },\n skip: isDummy || hasPreview || !id,\n });\n\n // Related posts — visitor surface in real mode. Preview mode falls\n // back to a client-side overlap pass against the prefetched feed\n // (BE-equivalent: shared category-or-tag id count, then publishedAt\n // desc as tiebreaker) so the dashboard pane mirrors what visitors\n // would see.\n const { data: relatedData } = useVisitorNewsRelatedQuery({\n // Pass the widget's active locale (not the article's) so related\n // follows the reader's language — fixes wrong-language related when\n // reading a fallback (default-locale) article.\n variables: { postId: id, limit: 5, locale },\n skip: isDummy || hasPreview || !id || !showRelated,\n });\n\n const mockArticle = isDummy ? (getMockNewsItems(demoT).find((n) => n.id === id) ?? null) : null;\n\n const article: NewsArticle | null = mockArticle\n ? {\n id: mockArticle.id,\n title: mockArticle.title,\n excerpt: mockArticle.excerpt,\n contentMarkdown: mockArticle.contentMarkdown,\n // `bodyFormat` stays markdown; HTML demo bodies are auto-detected\n // by `looksLikeHtml` below and routed through `ArticleHtml`.\n bodyFormat: \"markdown\",\n coverImageUrl: mockArticle.coverImageUrl,\n coverGradient: mockArticle.coverGradient,\n publishedAt: mockArticle.publishedAt,\n upstreamUpdatedAt: mockArticle.upstreamUpdatedAt,\n tags: mockArticle.tags,\n authors: mockArticle.authors,\n category: mockArticle.category,\n // Demo: article shown in the selected demo locale.\n locale: demoLocale,\n }\n : hasPreview\n ? extractArticle(previewRaw)\n : extractArticle(data?.visitorNewsPost);\n\n // Surface the title up to the widget shell's sticky header (swaps\n // \"News\" → article name). See `useArticleTitleSink` for why the\n // mount/unmount ownership is the single source of truth.\n useArticleTitleSink(article?.title, onTitleResolved);\n\n if (!article) {\n if (loading) {\n // Article-shaped skeleton — see `NewsArticleSkeleton` below\n // for the structure. Same `animate-pulse` opacity-only loop\n // as the help-article skeleton so layouts stay consistent.\n return <NewsArticleSkeleton />;\n }\n return (\n <div className=\"flex flex-1 items-center justify-center px-6 py-12 text-center\">\n <p className=\"text-sm text-wx-fg-muted\">{t(\"articleNotFound\")}</p>\n </div>\n );\n }\n\n const fallbackGradient: [string, string] = article.coverGradient ?? [\"#1e1f21\", \"#121314\"];\n\n return (\n <ScrollArea className=\"flex-1\">\n {/* Plain `<div>` — the widget shell's route slide already\n covers the enter animation. Stacking a second opacity+y\n keyframe on top thrashed the main thread during article\n navigation: panel width animating + route sliding + body\n fading + Apollo query rendering, all on the same frame.\n The route slide alone is enough and visibly snappier. */}\n <div className=\"flex flex-col\">\n <div\n className={cn(\"relative w-full\")}\n style={{\n aspectRatio: \"16 / 9\",\n background: article.coverImageUrl\n ? `url(${article.coverImageUrl}) center/cover`\n : `linear-gradient(135deg, ${fallbackGradient[0]}, ${fallbackGradient[1]})`,\n }}\n aria-hidden=\"true\"\n />\n <div className=\"flex flex-col gap-3 px-5 pt-5 pb-12\">\n {article.category && (\n <p className=\"text-[11px] font-semibold tracking-[0.14em] text-wx-primary uppercase\">\n {article.category.label}\n </p>\n )}\n <p className=\"flex items-center gap-2 text-xs text-wx-fg-subtle\">\n <span>\n {new Date(article.publishedAt).toLocaleDateString(locale, {\n month: \"long\",\n day: \"numeric\",\n year: \"numeric\",\n })}\n </span>\n {isNewsPostEdited(article) && article.upstreamUpdatedAt && (\n <span\n className=\"rounded-full bg-wx-bg-elevated px-1.5 py-0.5 text-[10px] font-medium text-wx-fg-muted\"\n title={new Date(article.upstreamUpdatedAt).toLocaleString(locale)}\n >\n {t(\"editedAt\", {\n when: formatNewsEditedAgo(article.upstreamUpdatedAt),\n })}\n </span>\n )}\n </p>\n <h2 className=\"text-2xl leading-tight font-bold text-wx-fg\">{article.title}</h2>\n {article.authors && article.authors.length > 0 && (\n <p className=\"flex items-center gap-2 text-xs text-wx-fg-muted\">\n <span className=\"flex -space-x-1.5\">\n {article.authors.slice(0, 3).map((a) => (\n <span\n key={a._id}\n className=\"h-5 w-5 overflow-hidden rounded-full bg-wx-bg-elevated ring-1 ring-wx-bg\"\n aria-hidden=\"true\"\n >\n {/* biome-ignore lint/performance/noImgElement: tiny avatar, no LCP impact */}\n <img src={a.photo.url} alt=\"\" className=\"h-full w-full object-cover\" />\n </span>\n ))}\n </span>\n <span>\n {t(\"writtenBy\", { names: article.authors.map((a) => a.name).join(\", \") })}\n </span>\n </p>\n )}\n {article.tags && article.tags.length > 0 && (\n <div className=\"flex flex-wrap gap-1\">\n {article.tags.map((tag) => (\n <span\n key={tag.slug}\n className=\"rounded-full bg-wx-bg-elevated px-2 py-0.5 text-[11px] font-medium text-wx-fg-muted\"\n >\n {tag.label}\n </span>\n ))}\n </div>\n )}\n {article.excerpt && (\n <p className=\"text-base leading-relaxed text-wx-fg-muted\">{article.excerpt}</p>\n )}\n {article.contentMarkdown && (\n <ArticleBody\n content={article.contentMarkdown}\n // BE-stamped `bodyFormat` wins when set explicitly to\n // `html` (RSS `<content:encoded>`), otherwise we sniff\n // the content — TipTap-authored native posts save with\n // the default `markdown` despite producing HTML.\n isHtml={article.bodyFormat === \"html\" || looksLikeHtml(article.contentMarkdown)}\n markdownMarginClass=\"mt-3\"\n />\n )}\n {article.sourceUrl && (\n <a\n href={article.sourceUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"mt-4 flex items-center justify-center gap-1.5 text-xs text-wx-primary underline underline-offset-2 transition-opacity hover:opacity-80\"\n >\n <ExternalLink size={12} />\n <span>{t(\"readOriginal\")}</span>\n </a>\n )}\n\n {showRelated && (\n <RelatedNewsList\n currentId={article.id}\n currentCategoryIds={article.categoryIds ?? []}\n currentTagIds={article.tagIds ?? []}\n previewPostsById={!isDummy ? env.previewData?.newsPostsById : undefined}\n visitorRelatedRaw={relatedData?.visitorNewsRelated}\n isDummy={isDummy}\n onOpen={onOpenRelated ?? (() => {})}\n />\n )}\n\n {/* Reactions bar — unified surface; operator-curated emoji\n palette via the News reactions settings. Defaults seed\n to 🎉 / ❤️ / 👍 NEUTRAL so the row never renders empty\n before an operator customises. */}\n {article.id && (\n <ReactionBar\n surface={ReactionSurface.NEWS}\n itemId={article.id}\n reactionCounts={article.reactionCounts ?? null}\n viewerReaction={article.viewerReaction ?? null}\n // All locale variants of this post — a reaction is per-group,\n // so keep their cached counts/highlight in sync.\n groupItemIds={[article.id, ...(article.translations ?? []).map((tr) => tr.id)]}\n isDummy={isDummy}\n showCounts={showReactionCounts}\n className=\"mt-6\"\n />\n )}\n\n {/* Language switcher — in demo it swaps the post content in\n place (article-scoped); in live mode it opens the chosen\n variant by id. Hidden when the post exists in one language. */}\n <LanguageSelector\n currentLocale={isDummy ? demoLocale : (article.locale ?? locale)}\n locales={\n isDummy ? [...DEMO_LOCALES] : (article.translations ?? []).map((tr) => tr.locale)\n }\n onSelect={\n isDummy\n ? (loc) => setDemoLocale(loc)\n : (loc) => {\n const target = (article.translations ?? []).find((tr) => tr.locale === loc);\n if (target) onOpenRelated?.(target.id);\n }\n }\n className=\"mt-6\"\n />\n </div>\n </div>\n </ScrollArea>\n );\n}\n\n/**\n * Loading state for the news article view. Mirrors the real\n * article's geometry: a 16:9 cover block, then a 2-line title and\n * body lines, all `animate-pulse`-driven (opacity only, no layout\n * thrash). Sits inside a `ScrollArea` so the surrounding shell's\n * scrollbar / chrome doesn't shift between skeleton and real\n * content.\n */\nfunction NewsArticleSkeleton() {\n return (\n <ScrollArea className=\"flex-1\">\n <div className=\"flex flex-col\" aria-busy=\"true\" aria-live=\"polite\">\n {/* Cover block — 16:9 to match the actual hero image. */}\n <div className=\"w-full animate-pulse bg-wx-bg-elevated\" style={{ aspectRatio: \"16 / 9\" }} />\n <div className=\"px-5 pt-5 pb-12\">\n {/* Title — two lines of varying width. */}\n <div className=\"animate-pulse space-y-2.5\">\n <div className=\"h-5 w-3/4 rounded-md bg-wx-bg-elevated\" />\n <div className=\"h-5 w-1/2 rounded-md bg-wx-bg-elevated\" />\n </div>\n {/* Date / source line. */}\n <div className=\"mt-4 flex items-center gap-2 animate-pulse\">\n <div className=\"h-3 w-24 rounded-md bg-wx-bg-elevated\" />\n </div>\n {/* Body — six lines of varying length for the paragraph\n cadence the article will paint into. */}\n <div className=\"mt-6 space-y-2.5 animate-pulse\">\n {[\"w-full\", \"w-11/12\", \"w-10/12\", \"w-full\", \"w-9/12\", \"w-7/12\"].map((w, i) => (\n // biome-ignore lint/suspicious/noArrayIndexKey: static order-stable skeleton with duplicate width values — index is the only stable key\n <div key={i} className={cn(\"h-3 rounded-md bg-wx-bg-elevated\", w)} />\n ))}\n </div>\n </div>\n </div>\n </ScrollArea>\n );\n}\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"widget-react-DwL_rsx_.js","names":[],"sources":["../dummy/news.ts","../node_modules/lucide-react/dist/esm/icons/megaphone.mjs","../components/ui/icon.tsx","../components/ui/skeleton.tsx","../components/widget/author-avatars.tsx","../components/widget/sticky-search-header.tsx","../lib/use-scroll-restore.ts","../lib/graphql/queries/generated/news.generated.tsx","../components/widget/tabs/news-tab/helpers.ts","../components/widget/tabs/news-tab/index.tsx"],"sourcesContent":["/**\n * Demo-mode news content. Surfaced when the widget runs with\n * `forceDummyData` / `mode=demo` — see `lib/use-widget-env.tsx`.\n *\n * Structure (id, date, gradient) lives here; the translatable text\n * (title, excerpt) comes from the `demo.news.*` next-intl namespace\n * (both locales), read via `t.raw()`. See `dummy/translator.ts`.\n */\n\nimport { authorsFor, type DemoAuthor, type DemoAuthorKey } from \"./authors\";\nimport type { DemoTranslator } from \"./translator\";\n\nexport interface MockNewsItem {\n id: string;\n title: string;\n excerpt: string;\n /** Post byline — one or more authors. */\n authors?: DemoAuthor[];\n /** Section the post belongs to (e.g. Product, Company). Rendered as\n * an eyebrow above the title on the detail view. */\n category?: { slug: string; label: string };\n /** Full markdown body shown on the article detail view. Same shape\n * as the help articles' `contentMarkdown`. Optional because the\n * feed-card mapping (`toMockShape` in the news tab) doesn't carry a\n * body — only the demo detail builder (`getMockNewsItems`) sets it. */\n contentMarkdown?: string;\n publishedAt: string;\n coverGradient: [string, string];\n /** Operator-supplied cover image — wins over the gradient when set. */\n coverImageUrl?: string;\n /** External-source edit timestamp. Only set for posts pulled in from\n * RSS / webhook sources whose `fieldMap.updatedAt` resolved. The\n * feed cards + article view derive the \"edited Xh ago\" badge when\n * this is meaningfully newer than `publishedAt`. */\n upstreamUpdatedAt?: string | null;\n /** Operator taxonomy chips — rendered on cards + the detail view. */\n tags?: { slug: string; label: string }[];\n /** Section(s) the post belongs to — rendered as chips on the feed\n * card (the singular `category` above is the detail-view eyebrow). */\n categories?: { slug: string; label: string }[];\n /** Sparse `{ [slotIndex]: count }` map. Surfaced on cards as a\n * read-only chip strip via `ReactionCountsStrip`. */\n reactionCounts?: Record<string, number> | null;\n}\n\n/** Locale-independent fields, defined once. Cover images use seeded\n * `picsum.photos` URLs (same approach as the demo conversation media)\n * so the feed + article hero show real photography in the showcase. */\n/** `tagSlugs` map to `news.tags.<slug>` labels and `categorySlug` to\n * `news.categories.<slug>` in the demo i18n bundle; `authorIds`\n * reference `DEMO_AUTHORS`. */\ntype NewsStruct = Pick<MockNewsItem, \"id\" | \"publishedAt\" | \"coverGradient\" | \"coverImageUrl\"> & {\n authorIds: DemoAuthorKey[];\n categorySlug: string;\n tagSlugs: string[];\n};\n\nconst NEWS_STRUCT: NewsStruct[] = [\n {\n id: \"demo-news-1\",\n publishedAt: \"2026-04-30T10:00:00.000Z\",\n coverGradient: [\"#1e1f21\", \"#121314\"],\n coverImageUrl: \"https://picsum.photos/seed/wexio-pioneer/960/600\",\n // Co-authored — demonstrates the multi-author byline.\n authorIds: [\"maya\", \"sam\"],\n categorySlug: \"company\",\n tagSlugs: [\"events\", \"product\"],\n },\n {\n id: \"demo-news-2\",\n publishedAt: \"2026-04-15T10:00:00.000Z\",\n coverGradient: [\"#1e3a8a\", \"#0c1e4a\"],\n coverImageUrl: \"https://picsum.photos/seed/wexio-april/960/600\",\n authorIds: [\"leo\"],\n categorySlug: \"product\",\n tagSlugs: [\"product\", \"release\"],\n },\n {\n id: \"demo-news-3\",\n publishedAt: \"2026-03-22T10:00:00.000Z\",\n coverGradient: [\"#0f766e\", \"#042f2e\"],\n coverImageUrl: \"https://picsum.photos/seed/wexio-langs/960/600\",\n authorIds: [\"ira\", \"leo\"],\n categorySlug: \"product\",\n tagSlugs: [\"ai\", \"languages\"],\n },\n {\n id: \"demo-news-4\",\n publishedAt: \"2026-03-08T10:00:00.000Z\",\n coverGradient: [\"#7c3aed\", \"#2e1065\"],\n authorIds: [\"leo\"],\n categorySlug: \"engineering\",\n tagSlugs: [\"integrations\", \"slack\"],\n },\n {\n id: \"demo-news-5\",\n publishedAt: \"2026-02-18T10:00:00.000Z\",\n coverGradient: [\"#b45309\", \"#451a03\"],\n coverImageUrl: \"https://picsum.photos/seed/wexio-inbox2/960/600\",\n authorIds: [\"maya\"],\n categorySlug: \"product\",\n tagSlugs: [\"inbox\", \"product\"],\n },\n {\n id: \"demo-news-6\",\n publishedAt: \"2026-02-02T10:00:00.000Z\",\n coverGradient: [\"#155e75\", \"#083344\"],\n authorIds: [\"sam\"],\n categorySlug: \"engineering\",\n tagSlugs: [\"privacy\", \"security\"],\n },\n {\n id: \"demo-news-7\",\n publishedAt: \"2026-01-20T10:00:00.000Z\",\n coverGradient: [\"#be185d\", \"#500724\"],\n coverImageUrl: \"https://picsum.photos/seed/wexio-mobile/960/600\",\n authorIds: [\"ira\"],\n categorySlug: \"product\",\n tagSlugs: [\"mobile\", \"beta\"],\n },\n {\n id: \"demo-news-8\",\n publishedAt: \"2026-01-05T10:00:00.000Z\",\n coverGradient: [\"#15803d\", \"#052e16\"],\n authorIds: [\"maya\", \"leo\"],\n categorySlug: \"engineering\",\n tagSlugs: [\"ai\", \"analytics\"],\n },\n];\n\n/**\n * Build the demo news feed from the active `demo` translator. Returns a\n * fresh array each call — wrap in `useMemo` at the call site if the\n * reference needs to stay stable across renders.\n */\nexport function getMockNewsItems(t: DemoTranslator): MockNewsItem[] {\n return NEWS_STRUCT.map(({ tagSlugs, authorIds, categorySlug, ...s }) => ({\n ...s,\n title: t.raw(`news.${s.id}.title`) as string,\n excerpt: t.raw(`news.${s.id}.excerpt`) as string,\n contentMarkdown: t.raw(`news.${s.id}.body`) as string,\n authors: authorsFor(authorIds),\n category: { slug: categorySlug, label: t.raw(`news.categories.${categorySlug}`) as string },\n tags: tagSlugs.map((slug) => ({ slug, label: t.raw(`news.tags.${slug}`) as string })),\n }));\n}\n","/**\n * @license lucide-react v1.14.0 - ISC\n *\n * This source code is licensed under the ISC license.\n * See the LICENSE file in the root directory of this source tree.\n */\n\nimport createLucideIcon from '../createLucideIcon.mjs';\n\nconst __iconNode = [\n [\n \"path\",\n {\n d: \"M11 6a13 13 0 0 0 8.4-2.8A1 1 0 0 1 21 4v12a1 1 0 0 1-1.6.8A13 13 0 0 0 11 14H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2z\",\n key: \"q8bfy3\"\n }\n ],\n [\"path\", { d: \"M6 14a12 12 0 0 0 2.4 7.2 2 2 0 0 0 3.2-2.4A8 8 0 0 1 10 14\", key: \"1853fq\" }],\n [\"path\", { d: \"M8 6v8\", key: \"15ugcq\" }]\n];\nconst Megaphone = createLucideIcon(\"megaphone\", __iconNode);\n\nexport { __iconNode, Megaphone as default };\n//# sourceMappingURL=megaphone.mjs.map\n","\"use client\";\n\nimport type { LucideIcon } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\ninterface IconProps extends React.SVGProps<SVGSVGElement> {\n /** A static `lucide-react` icon. Pass the component directly. */\n icon: LucideIcon;\n size?: number;\n /** Stroke weight — match Intercom's slightly chunky default. */\n strokeWidth?: number;\n}\n\n/**\n * Static icon wrapper. Standardises sizing + stroke across the\n * widget so we don't sprinkle `className=\"h-5 w-5\"` on every call\n * site. Animated icons (send-success, heart-pop on reaction) come\n * from `lucide-animated` and are imported separately in the surfaces\n * that need them — those are intentional engagement moments, not\n * the default for every icon.\n */\nexport function Icon({\n icon: IconComp,\n size = 20,\n strokeWidth = 2,\n className,\n ...rest\n}: IconProps) {\n return (\n <IconComp\n width={size}\n height={size}\n strokeWidth={strokeWidth}\n className={cn(\"shrink-0\", className)}\n {...rest}\n />\n );\n}\n","\"use client\";\n\nimport { cn } from \"@/lib/utils\";\n\n/**\n * Brand-tinted shimmer placeholder. Used while async data is in\n * flight (chat history, help categories, news feed). The animation\n * is a soft pulse on `bg-wx-bg-elevated` — reads as chrome on any\n * widget surface (light bubble, primary bubble, gradient hero).\n *\n * Compose at the call site to match the shape of the eventual\n * content (e.g. round avatars, line-height bars). Keeps the skeleton\n * footprint zero-cost when not in use; just plain divs with one\n * `animate-pulse` class.\n */\nexport function Skeleton({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {\n return (\n <div\n data-slot=\"skeleton\"\n className={cn(\"animate-pulse rounded-wx-sm bg-wx-bg-elevated\", className)}\n {...props}\n />\n );\n}\n","/**\n * Overlapping author-avatar stack — shared by the help list, help\n * folder drill-in, help search results, and (once news posts carry\n * authors) the news feed. Renders up to `max` avatars; falls back to an\n * initial when a photo is missing. Returns `null` when there are none.\n */\n\nexport interface AuthorChip {\n id: string;\n name: string;\n photoUrl: string | null;\n}\n\n/** Normalise an authors array (demo or BE — both `{ _id, name, photo:\n * { url } }`) to the avatar-chip shape. `undefined` when empty/invalid. */\nexport function toAuthorChips(raw: unknown): AuthorChip[] | undefined {\n if (!Array.isArray(raw)) return undefined;\n const chips = raw\n .map((a): AuthorChip | null => {\n if (!a || typeof a !== \"object\") return null;\n const name = (a as { name?: unknown }).name;\n if (typeof name !== \"string\") return null;\n const id = (a as { _id?: unknown; id?: unknown })._id ?? (a as { id?: unknown }).id;\n const url = (a as { photo?: { url?: unknown } }).photo?.url;\n return {\n id: String(id ?? name),\n name,\n photoUrl: typeof url === \"string\" ? url : null,\n };\n })\n .filter((a): a is AuthorChip => !!a);\n return chips.length > 0 ? chips : undefined;\n}\n\nexport function AuthorAvatars({\n authors,\n max = 3,\n className,\n}: {\n authors: AuthorChip[] | null | undefined;\n max?: number;\n className?: string;\n}) {\n if (!authors || authors.length === 0) return null;\n return (\n <span className={`flex items-center -space-x-1.5 ${className ?? \"\"}`}>\n {authors.slice(0, max).map((author) =>\n author.photoUrl ? (\n // biome-ignore lint/performance/noImgElement: widget is framework-agnostic (no next/image in the embed)\n <img\n key={author.id}\n src={author.photoUrl}\n alt={author.name}\n title={author.name}\n className=\"h-5 w-5 rounded-full border border-wx-bg object-cover\"\n />\n ) : (\n <span\n key={author.id}\n title={author.name}\n className=\"flex h-5 w-5 items-center justify-center rounded-full border border-wx-bg bg-wx-bg-elevated-2 text-[9px] font-semibold text-wx-fg-muted\"\n >\n {author.name.charAt(0).toUpperCase()}\n </span>\n ),\n )}\n </span>\n );\n}\n","\"use client\";\n\nimport { SearchIcon } from \"@/components/widget/icons/animated\";\nimport { cn } from \"@/lib/utils\";\n\n/**\n * Sticky search field used by the Help + News tabs.\n *\n * The input is intentionally NOT a tonal pill — it fills the entire\n * header band edge-to-edge so the search affordance reads as part of\n * the tab's chrome, not as a card floating inside it. A hairline\n * bottom divider separates the band from the scrolling list below.\n *\n * Sticky positioning relies on the parent being a flex column whose\n * scrollable child contains this header — `position: sticky; top: 0`\n * inside a scroll viewport keeps it pinned as the list scrolls. The\n * search icon sits absolutely positioned over the input's left\n * padding so the input itself stays a single flat surface.\n */\nexport function StickySearchHeader({\n value,\n onChange,\n placeholder,\n className,\n}: {\n value: string;\n onChange: (next: string) => void;\n placeholder: string;\n className?: string;\n}) {\n return (\n <div className={cn(\"sticky top-0 z-10 border-wx-border border-b bg-wx-bg\", className)}>\n <span className=\"-translate-y-1/2 pointer-events-none absolute top-1/2 left-4 text-wx-fg-muted\">\n <SearchIcon size={16} />\n </span>\n <input\n type=\"search\"\n value={value}\n onChange={(e) => onChange(e.target.value)}\n placeholder={placeholder}\n // Edge-to-edge, no border, transparent — the band's bottom\n // divider IS the visual separator. Compact py-3 height; pl-10\n // clears the search icon. Suppress the browser's \"search\"\n // cancel-x glyph (we don't need it; the visitor just clears\n // the field by erasing).\n className={cn(\n \"block w-full border-0 bg-transparent px-4 py-3 pl-10 text-sm text-wx-fg\",\n \"placeholder:text-wx-fg-muted focus:outline-none focus:ring-0\",\n \"[&::-webkit-search-cancel-button]:appearance-none\",\n )}\n />\n </div>\n );\n}\n","\"use client\";\n\nimport { useEffect, useRef } from \"react\";\n\n/**\n * Module-local scratchpad for list-view scroll positions. Keyed by a\n * caller-chosen tag (`\"news\"`, `\"help\"`) so each list keeps its own\n * value across list ↔ detail ↔ list navigation cycles.\n *\n * Lives at module scope (NOT React state) so list components can\n * unmount during route transitions without dropping the value — when\n * they re-mount, `useScrollRestore` rehydrates the recorded offset.\n */\nconst scrollPositions = new Map<string, number>();\n\n/**\n * Wire a `ScrollArea` viewport ref to a named slot in the scratchpad.\n * Pass the resulting ref to `<ScrollArea viewportRef={…} />`.\n *\n * - On mount → restores the saved offset (if any) on the next frame\n * so layout has settled before we set `scrollTop`.\n * - On scroll → writes the current offset to the slot.\n * - On unmount → leaves the last seen value intact for the next mount.\n *\n * Restoration waits for content to grow tall enough to honour the\n * saved offset — Apollo cache rehydration on the route transition can\n * paint the list initially empty, so a single `requestAnimationFrame`\n * isn't always enough. We poll for up to ~500 ms.\n */\nexport function useScrollRestore(key: string) {\n const viewportRef = useRef<HTMLDivElement | null>(null);\n\n useEffect(() => {\n const vp = viewportRef.current;\n if (!vp) return;\n const saved = scrollPositions.get(key) ?? 0;\n\n let cancelled = false;\n const start = performance.now();\n const tryRestore = () => {\n if (cancelled || !viewportRef.current) return;\n const el = viewportRef.current;\n // Only restore once the content is at least as tall as where\n // we want to scroll to — otherwise the browser clamps to the\n // current `scrollHeight - clientHeight` and we land short.\n if (el.scrollHeight - el.clientHeight >= saved) {\n el.scrollTop = saved;\n return;\n }\n if (performance.now() - start > 500) {\n // Content never grew tall enough — restore anyway so we land\n // wherever the clamp puts us instead of leaving the user at\n // the top of a half-loaded list.\n el.scrollTop = saved;\n return;\n }\n requestAnimationFrame(tryRestore);\n };\n requestAnimationFrame(tryRestore);\n\n const onScroll = () => {\n scrollPositions.set(key, vp.scrollTop);\n };\n vp.addEventListener(\"scroll\", onScroll, { passive: true });\n return () => {\n cancelled = true;\n vp.removeEventListener(\"scroll\", onScroll);\n };\n }, [key]);\n\n return viewportRef;\n}\n\n/**\n * Wipe a stored position — call when the list's identity changes\n * (e.g. operator switches integration in preview, locale change) so\n * the next mount starts from the top instead of restoring a stale\n * offset against a different dataset.\n */\nexport function clearScrollRestore(key: string) {\n scrollPositions.delete(key);\n}\n","/* eslint-disable */\n/* biome-disable */\n/** Internal type. DO NOT USE DIRECTLY. */\ntype Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };\n/** Internal type. DO NOT USE DIRECTLY. */\nexport type Incremental<T> = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never };\nimport * as Types from '@/lib/graphql/generated/types';\n\nimport { gql } from '@apollo/client';\nimport * as Apollo from '@apollo/client';\nconst defaultOptions = {} as const;\n/** Where the News tab content comes from — NATIVE (wexio-authored), RSS (polled feed), WEBHOOK_PUSH (CMS-pushed). CUSTOM_API reserved for Phase 2+. */\nexport type NewsSourceMode =\n | 'NATIVE'\n | 'RSS'\n | 'WEBHOOK_PUSH';\n\nexport type VisitorNewsListArgs = {\n categorySlug?: string | null | undefined;\n limit?: number | null | undefined;\n locale?: string | null | undefined;\n offset?: number | null | undefined;\n query?: string | null | undefined;\n tagSlug?: string | null | undefined;\n};\n\nexport type VisitorNewsPostArgs = {\n id?: string | null | undefined;\n locale?: string | null | undefined;\n slug?: string | null | undefined;\n};\n\nexport type VisitorNewsListQueryVariables = Exact<{\n args?: Types.VisitorNewsListArgs | null | undefined;\n}>;\n\n\nexport type VisitorNewsListQuery = { __typename: 'Query', visitorNewsList: { __typename: 'PaginatedNewsPosts', items: Array<{ __typename: 'NewsPost', _id: string, title: string, slug: string | null, excerpt: string | null, publishedAt: unknown, upstreamUpdatedAt: unknown, reactionCounts: unknown, externalCoverImageUrl: string | null, coverImageUrl: { __typename: 'Media', _id: string, url: string } | null, authors: Array<{ __typename: 'User', _id: string, name: string | null, photo: { __typename: 'Media', _id: string, url: string } | null }>, categories: Array<{ __typename: 'NewsCategory', _id: string, slug: string, label: string }>, tags: Array<{ __typename: 'NewsTag', _id: string, slug: string, label: string }> }>, meta: { __typename: 'PaginationMeta', totalCount: number, currentPage: number, totalPages: number, hasNextPage: boolean, hasPreviousPage: boolean } } };\n\n/** Slim card variant — see news.graphql `VisitorNewsPostCard` query.\n * Same resolver as `VisitorNewsPost`, narrower selection (no\n * `contentMarkdown` / joins) for the home-tab hero where the article\n * body is dead weight on the wire. Hand-patched per\n * graphql-codegen-handpatch policy (codegen needs a live BE). */\nexport type VisitorNewsPostCardQueryVariables = Exact<{\n args: Types.VisitorNewsPostArgs;\n}>;\n\n\nexport type VisitorNewsPostCardQuery = { __typename: 'Query', visitorNewsPost: { __typename: 'NewsPost', _id: string, title: string, excerpt: string | null, externalCoverImageUrl: string | null, coverImageUrl: { __typename: 'Media', _id: string, url: string } | null } | null };\n\nexport type VisitorNewsPostQueryVariables = Exact<{\n args: Types.VisitorNewsPostArgs;\n}>;\n\n\nexport type VisitorNewsPostQuery = { __typename: 'Query', visitorNewsPost: { __typename: 'NewsPost', _id: string, title: string, slug: string | null, excerpt: string | null, contentMarkdown: string, bodyFormat: string, locale: string, publishedAt: unknown, upstreamUpdatedAt: unknown, viewCount: number, reactionCounts: unknown, viewerReaction: number | null, sourceUrl: string | null, externalCoverImageUrl: string | null, coverImageUrl: { __typename: 'Media', _id: string, url: string } | null, source: { __typename: 'NewsSource', _id: string, name: string, type: Types.NewsSourceMode } | null, authors: Array<{ __typename: 'User', _id: string, name: string | null, photo: { __typename: 'Media', _id: string, url: string } | null }>, categories: Array<{ __typename: 'NewsCategory', _id: string, slug: string, label: string }>, tags: Array<{ __typename: 'NewsTag', _id: string, slug: string, label: string }>, translations: Array<{ __typename: 'NewsPost', _id: string, locale: string, slug: string | null }> } | null };\n\nexport type VisitorNewsRelatedQueryVariables = Exact<{\n postId: string | number;\n limit?: number | null | undefined;\n locale?: string | null | undefined;\n}>;\n\n\nexport type VisitorNewsRelatedQuery = { __typename: 'Query', visitorNewsRelated: Array<{ __typename: 'NewsPost', _id: string, title: string, slug: string | null, excerpt: string | null, publishedAt: unknown, externalCoverImageUrl: string | null, coverImageUrl: { __typename: 'Media', _id: string, url: string } | null }> };\n\nexport type RecordVisitorNewsViewMutationVariables = Exact<{\n postId: string;\n}>;\n\n\nexport type RecordVisitorNewsViewMutation = { __typename: 'Mutation', recordVisitorNewsView: { __typename: 'WebOkResponse', ok: boolean } };\n\n\nexport const VisitorNewsListDocument = gql`\n query VisitorNewsList($args: VisitorNewsListArgs) {\n visitorNewsList(args: $args) {\n items {\n _id\n title\n slug\n excerpt\n publishedAt\n upstreamUpdatedAt\n reactionCounts\n coverImageUrl {\n _id\n url\n }\n externalCoverImageUrl\n authors {\n _id\n name\n photo {\n _id\n url\n }\n }\n categories {\n _id\n slug\n label\n }\n tags {\n _id\n slug\n label\n }\n }\n meta {\n totalCount\n currentPage\n totalPages\n hasNextPage\n hasPreviousPage\n }\n }\n}\n `;\n\n/**\n * __useVisitorNewsListQuery__\n *\n * To run a query within a React component, call `useVisitorNewsListQuery` and pass it any options that fit your needs.\n * When your component renders, `useVisitorNewsListQuery` returns an object from Apollo Client that contains loading, error, and data properties\n * you can use to render your UI.\n *\n * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;\n *\n * @example\n * const { data, loading, error } = useVisitorNewsListQuery({\n * variables: {\n * args: // value for 'args'\n * },\n * });\n */\nexport function useVisitorNewsListQuery(baseOptions?: Apollo.QueryHookOptions<VisitorNewsListQuery, VisitorNewsListQueryVariables>) {\n const options = {...defaultOptions, ...baseOptions}\n return Apollo.useQuery<VisitorNewsListQuery, VisitorNewsListQueryVariables>(VisitorNewsListDocument, options);\n }\nexport function useVisitorNewsListLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<VisitorNewsListQuery, VisitorNewsListQueryVariables>) {\n const options = {...defaultOptions, ...baseOptions}\n return Apollo.useLazyQuery<VisitorNewsListQuery, VisitorNewsListQueryVariables>(VisitorNewsListDocument, options);\n }\n// @ts-ignore\nexport function useVisitorNewsListSuspenseQuery(baseOptions?: Apollo.SuspenseQueryHookOptions<VisitorNewsListQuery, VisitorNewsListQueryVariables>): Apollo.UseSuspenseQueryResult<VisitorNewsListQuery, VisitorNewsListQueryVariables>;\nexport function useVisitorNewsListSuspenseQuery(baseOptions?: Apollo.SkipToken | Apollo.SuspenseQueryHookOptions<VisitorNewsListQuery, VisitorNewsListQueryVariables>): Apollo.UseSuspenseQueryResult<VisitorNewsListQuery | undefined, VisitorNewsListQueryVariables>;\nexport function useVisitorNewsListSuspenseQuery(baseOptions?: Apollo.SkipToken | Apollo.SuspenseQueryHookOptions<VisitorNewsListQuery, VisitorNewsListQueryVariables>) {\n const options = baseOptions === Apollo.skipToken ? baseOptions : {...defaultOptions, ...baseOptions}\n return Apollo.useSuspenseQuery<VisitorNewsListQuery, VisitorNewsListQueryVariables>(VisitorNewsListDocument, options);\n }\nexport type VisitorNewsListQueryHookResult = ReturnType<typeof useVisitorNewsListQuery>;\nexport type VisitorNewsListLazyQueryHookResult = ReturnType<typeof useVisitorNewsListLazyQuery>;\nexport type VisitorNewsListSuspenseQueryHookResult = ReturnType<typeof useVisitorNewsListSuspenseQuery>;\nexport type VisitorNewsListQueryResult = Apollo.QueryResult<VisitorNewsListQuery, VisitorNewsListQueryVariables>;\nexport const VisitorNewsPostCardDocument = gql`\n query VisitorNewsPostCard($args: VisitorNewsPostArgs!) {\n visitorNewsPost(args: $args) {\n _id\n title\n excerpt\n coverImageUrl {\n _id\n url\n }\n externalCoverImageUrl\n }\n}\n `;\n\n/**\n * __useVisitorNewsPostCardQuery__\n *\n * Slim card-only payload for the home-tab featured-article hero.\n * Same resolver as the full `VisitorNewsPost` query; selects only\n * the fields the hero renders (no `contentMarkdown` / joins). Use\n * `useVisitorNewsPostQuery` instead inside the article-detail view.\n */\nexport function useVisitorNewsPostCardQuery(baseOptions: Apollo.QueryHookOptions<VisitorNewsPostCardQuery, VisitorNewsPostCardQueryVariables> & ({ variables: VisitorNewsPostCardQueryVariables; skip?: boolean; } | { skip: boolean; }) ) {\n const options = {...defaultOptions, ...baseOptions}\n return Apollo.useQuery<VisitorNewsPostCardQuery, VisitorNewsPostCardQueryVariables>(VisitorNewsPostCardDocument, options);\n }\nexport type VisitorNewsPostCardQueryHookResult = ReturnType<typeof useVisitorNewsPostCardQuery>;\nexport type VisitorNewsPostCardQueryResult = Apollo.QueryResult<VisitorNewsPostCardQuery, VisitorNewsPostCardQueryVariables>;\n\nexport const VisitorNewsPostDocument = gql`\n query VisitorNewsPost($args: VisitorNewsPostArgs!) {\n visitorNewsPost(args: $args) {\n _id\n title\n slug\n excerpt\n contentMarkdown\n bodyFormat\n locale\n publishedAt\n upstreamUpdatedAt\n viewCount\n reactionCounts\n viewerReaction\n sourceUrl\n coverImageUrl {\n _id\n url\n }\n externalCoverImageUrl\n source {\n _id\n name\n type\n }\n authors {\n _id\n name\n photo {\n _id\n url\n }\n }\n categories {\n _id\n slug\n label\n }\n tags {\n _id\n slug\n label\n }\n translations {\n _id\n locale\n slug\n }\n }\n}\n `;\n\n/**\n * __useVisitorNewsPostQuery__\n *\n * To run a query within a React component, call `useVisitorNewsPostQuery` and pass it any options that fit your needs.\n * When your component renders, `useVisitorNewsPostQuery` returns an object from Apollo Client that contains loading, error, and data properties\n * you can use to render your UI.\n *\n * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;\n *\n * @example\n * const { data, loading, error } = useVisitorNewsPostQuery({\n * variables: {\n * args: // value for 'args'\n * },\n * });\n */\nexport function useVisitorNewsPostQuery(baseOptions: Apollo.QueryHookOptions<VisitorNewsPostQuery, VisitorNewsPostQueryVariables> & ({ variables: VisitorNewsPostQueryVariables; skip?: boolean; } | { skip: boolean; }) ) {\n const options = {...defaultOptions, ...baseOptions}\n return Apollo.useQuery<VisitorNewsPostQuery, VisitorNewsPostQueryVariables>(VisitorNewsPostDocument, options);\n }\nexport function useVisitorNewsPostLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<VisitorNewsPostQuery, VisitorNewsPostQueryVariables>) {\n const options = {...defaultOptions, ...baseOptions}\n return Apollo.useLazyQuery<VisitorNewsPostQuery, VisitorNewsPostQueryVariables>(VisitorNewsPostDocument, options);\n }\n// @ts-ignore\nexport function useVisitorNewsPostSuspenseQuery(baseOptions?: Apollo.SuspenseQueryHookOptions<VisitorNewsPostQuery, VisitorNewsPostQueryVariables>): Apollo.UseSuspenseQueryResult<VisitorNewsPostQuery, VisitorNewsPostQueryVariables>;\nexport function useVisitorNewsPostSuspenseQuery(baseOptions?: Apollo.SkipToken | Apollo.SuspenseQueryHookOptions<VisitorNewsPostQuery, VisitorNewsPostQueryVariables>): Apollo.UseSuspenseQueryResult<VisitorNewsPostQuery | undefined, VisitorNewsPostQueryVariables>;\nexport function useVisitorNewsPostSuspenseQuery(baseOptions?: Apollo.SkipToken | Apollo.SuspenseQueryHookOptions<VisitorNewsPostQuery, VisitorNewsPostQueryVariables>) {\n const options = baseOptions === Apollo.skipToken ? baseOptions : {...defaultOptions, ...baseOptions}\n return Apollo.useSuspenseQuery<VisitorNewsPostQuery, VisitorNewsPostQueryVariables>(VisitorNewsPostDocument, options);\n }\nexport type VisitorNewsPostQueryHookResult = ReturnType<typeof useVisitorNewsPostQuery>;\nexport type VisitorNewsPostLazyQueryHookResult = ReturnType<typeof useVisitorNewsPostLazyQuery>;\nexport type VisitorNewsPostSuspenseQueryHookResult = ReturnType<typeof useVisitorNewsPostSuspenseQuery>;\nexport type VisitorNewsPostQueryResult = Apollo.QueryResult<VisitorNewsPostQuery, VisitorNewsPostQueryVariables>;\nexport const VisitorNewsRelatedDocument = gql`\n query VisitorNewsRelated($postId: ID!, $limit: Int, $locale: String) {\n visitorNewsRelated(postId: $postId, limit: $limit, locale: $locale) {\n _id\n title\n slug\n excerpt\n publishedAt\n coverImageUrl {\n _id\n url\n }\n externalCoverImageUrl\n }\n}\n `;\n\n/**\n * __useVisitorNewsRelatedQuery__\n *\n * To run a query within a React component, call `useVisitorNewsRelatedQuery` and pass it any options that fit your needs.\n * When your component renders, `useVisitorNewsRelatedQuery` returns an object from Apollo Client that contains loading, error, and data properties\n * you can use to render your UI.\n *\n * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;\n *\n * @example\n * const { data, loading, error } = useVisitorNewsRelatedQuery({\n * variables: {\n * postId: // value for 'postId'\n * limit: // value for 'limit'\n * locale: // value for 'locale'\n * },\n * });\n */\nexport function useVisitorNewsRelatedQuery(baseOptions: Apollo.QueryHookOptions<VisitorNewsRelatedQuery, VisitorNewsRelatedQueryVariables> & ({ variables: VisitorNewsRelatedQueryVariables; skip?: boolean; } | { skip: boolean; }) ) {\n const options = {...defaultOptions, ...baseOptions}\n return Apollo.useQuery<VisitorNewsRelatedQuery, VisitorNewsRelatedQueryVariables>(VisitorNewsRelatedDocument, options);\n }\nexport function useVisitorNewsRelatedLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<VisitorNewsRelatedQuery, VisitorNewsRelatedQueryVariables>) {\n const options = {...defaultOptions, ...baseOptions}\n return Apollo.useLazyQuery<VisitorNewsRelatedQuery, VisitorNewsRelatedQueryVariables>(VisitorNewsRelatedDocument, options);\n }\n// @ts-ignore\nexport function useVisitorNewsRelatedSuspenseQuery(baseOptions?: Apollo.SuspenseQueryHookOptions<VisitorNewsRelatedQuery, VisitorNewsRelatedQueryVariables>): Apollo.UseSuspenseQueryResult<VisitorNewsRelatedQuery, VisitorNewsRelatedQueryVariables>;\nexport function useVisitorNewsRelatedSuspenseQuery(baseOptions?: Apollo.SkipToken | Apollo.SuspenseQueryHookOptions<VisitorNewsRelatedQuery, VisitorNewsRelatedQueryVariables>): Apollo.UseSuspenseQueryResult<VisitorNewsRelatedQuery | undefined, VisitorNewsRelatedQueryVariables>;\nexport function useVisitorNewsRelatedSuspenseQuery(baseOptions?: Apollo.SkipToken | Apollo.SuspenseQueryHookOptions<VisitorNewsRelatedQuery, VisitorNewsRelatedQueryVariables>) {\n const options = baseOptions === Apollo.skipToken ? baseOptions : {...defaultOptions, ...baseOptions}\n return Apollo.useSuspenseQuery<VisitorNewsRelatedQuery, VisitorNewsRelatedQueryVariables>(VisitorNewsRelatedDocument, options);\n }\nexport type VisitorNewsRelatedQueryHookResult = ReturnType<typeof useVisitorNewsRelatedQuery>;\nexport type VisitorNewsRelatedLazyQueryHookResult = ReturnType<typeof useVisitorNewsRelatedLazyQuery>;\nexport type VisitorNewsRelatedSuspenseQueryHookResult = ReturnType<typeof useVisitorNewsRelatedSuspenseQuery>;\nexport type VisitorNewsRelatedQueryResult = Apollo.QueryResult<VisitorNewsRelatedQuery, VisitorNewsRelatedQueryVariables>;\nexport const RecordVisitorNewsViewDocument = gql`\n mutation RecordVisitorNewsView($postId: String!) {\n recordVisitorNewsView(postId: $postId) {\n ok\n }\n}\n `;\nexport type RecordVisitorNewsViewMutationFn = Apollo.MutationFunction<RecordVisitorNewsViewMutation, RecordVisitorNewsViewMutationVariables>;\n\n/**\n * __useRecordVisitorNewsViewMutation__\n *\n * To run a mutation, you first call `useRecordVisitorNewsViewMutation` within a React component and pass it any options that fit your needs.\n * When your component renders, `useRecordVisitorNewsViewMutation` returns a tuple that includes:\n * - A mutate function that you can call at any time to execute the mutation\n * - An object with fields that represent the current status of the mutation's execution\n *\n * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;\n *\n * @example\n * const [recordVisitorNewsViewMutation, { data, loading, error }] = useRecordVisitorNewsViewMutation({\n * variables: {\n * postId: // value for 'postId'\n * },\n * });\n */\nexport function useRecordVisitorNewsViewMutation(baseOptions?: Apollo.MutationHookOptions<RecordVisitorNewsViewMutation, RecordVisitorNewsViewMutationVariables>) {\n const options = {...defaultOptions, ...baseOptions}\n return Apollo.useMutation<RecordVisitorNewsViewMutation, RecordVisitorNewsViewMutationVariables>(RecordVisitorNewsViewDocument, options);\n }\nexport type RecordVisitorNewsViewMutationHookResult = ReturnType<typeof useRecordVisitorNewsViewMutation>;\nexport type RecordVisitorNewsViewMutationResult = Apollo.MutationResult<RecordVisitorNewsViewMutation>;\nexport type RecordVisitorNewsViewMutationOptions = Apollo.BaseMutationOptions<RecordVisitorNewsViewMutation, RecordVisitorNewsViewMutationVariables>;","/**\n * Pure helpers for the News surfaces (tab + article view). Kept out of\n * the components so they can be unit-tested in isolation.\n */\n\n/**\n * Whether a news post was meaningfully edited after publication. Only\n * RSS / webhook posts carry `upstreamUpdatedAt`; we treat a >1-minute\n * gap as a real edit (sub-minute deltas are ingest-pipeline noise).\n */\nexport function isNewsPostEdited(post: {\n publishedAt: string;\n upstreamUpdatedAt?: string | null;\n}): boolean {\n if (!post.upstreamUpdatedAt) return false;\n const updated = Date.parse(post.upstreamUpdatedAt);\n const published = Date.parse(post.publishedAt);\n if (Number.isNaN(updated) || Number.isNaN(published)) return false;\n return updated > published + 60_000;\n}\n\n/** Compact relative time for the \"edited Xh ago\" badge. */\nexport function formatNewsEditedAgo(iso: string): string {\n const ts = Date.parse(iso);\n if (Number.isNaN(ts)) return \"\";\n const diffMs = Date.now() - ts;\n const m = Math.floor(diffMs / 60_000);\n if (m < 1) return \"just now\";\n if (m < 60) return `${m}m ago`;\n const h = Math.floor(m / 60);\n if (h < 24) return `${h}h ago`;\n const d = Math.floor(h / 24);\n if (d < 7) return `${d}d ago`;\n return new Date(ts).toLocaleDateString();\n}\n","\"use client\";\n\nimport { Megaphone } from \"lucide-react\";\nimport { motion } from \"motion/react\";\nimport { useLocale, useTranslations } from \"next-intl\";\nimport { useCallback, useEffect, useMemo, useState } from \"react\";\nimport { CardSubtitle, CardTitle } from \"@/components/ui/card\";\nimport { Icon } from \"@/components/ui/icon\";\nimport { ScrollArea } from \"@/components/ui/scroll-area\";\nimport { Skeleton } from \"@/components/ui/skeleton\";\nimport { AuthorAvatars, toAuthorChips } from \"@/components/widget/author-avatars\";\nimport { StickySearchHeader } from \"@/components/widget/sticky-search-header\";\nimport { getMockNewsItems, type MockNewsItem } from \"@/dummy\";\nimport { useVisitorNewsListQuery } from \"@/lib/graphql/queries/generated/news.generated\";\nimport { useScrollRestore } from \"@/lib/use-scroll-restore\";\nimport { useWidgetEnv } from \"@/lib/use-widget-env\";\nimport { cn } from \"@/lib/utils\";\nimport { formatNewsEditedAgo, isNewsPostEdited } from \"./helpers\";\n\n// Re-exported so `news-article-view` keeps importing from `./tabs/news-tab`.\nexport { formatNewsEditedAgo, isNewsPostEdited } from \"./helpers\";\n\nconst PAGE_SIZE = 10;\n/** Trigger fetchMore (real mode) / unhide more (preview mode) when\n * the visitor scrolls within this many pixels of the bottom. */\nconst LOAD_MORE_THRESHOLD = 240;\n\ninterface NewsTabProps {\n isDummy: boolean;\n /** Shell-driven drill-in. Tile click → opens the post detail. */\n onOpenArticle: (id: string) => void;\n}\n\n/**\n * News feed. Real path queries the consolidated `visitorNewsList`\n * which serves both the chronological feed and `$text`-ranked search\n * via the same args (pass `query` for search, omit for feed).\n * Returns `PaginatedNewsPosts` (`{ items, meta }`) — pagination uses\n * `meta.hasNextPage` + `offset` rather than a length heuristic.\n *\n * Dummy + preview modes share the in-memory MOCK list / prefetched\n * array and paginate locally; preview-mode search filters the\n * prefetched list client-side (no BE round-trip).\n */\nexport function NewsTab({ isDummy, onOpenArticle }: NewsTabProps) {\n const t = useTranslations(\"news\");\n const tDemo = useTranslations(\"demo\");\n const locale = useLocale();\n const env = useWidgetEnv();\n const [query, setQuery] = useState(\"\");\n const debounced = useDebounced(query, 300);\n const isSearching = debounced.length >= 2;\n\n // Dashboard prefetch — when the dashboard preview pane mounted the\n // widget with `previewData.newsPosts`, render those instead of\n // round-tripping to the visitor surface.\n //\n // Important: only short-circuit when the prefetch actually carries\n // rows. The dashboard's `useMemo` always emits a (possibly empty)\n // array while its own query is in-flight; treating an empty array\n // as \"use the prefetch\" would skip the BE call and leave the tab\n // permanently empty. An empty prefetch falls through to the BE\n // query, which serves both the standalone-widget path and the\n // preview-still-loading edge case.\n const previewFeed = env.previewData?.newsPosts;\n const usePreview = !isDummy && Array.isArray(previewFeed) && previewFeed.length > 0;\n\n // `visibleCount` paginates the prefetched array in preview/dummy\n // modes. Real mode tracks page progress through Apollo's cache via\n // `fetchMore`.\n const [visibleCount, setVisibleCount] = useState(PAGE_SIZE);\n\n const {\n data: listData,\n error: listError,\n loading: listLoading,\n fetchMore,\n } = useVisitorNewsListQuery({\n variables: {\n args: {\n locale,\n limit: PAGE_SIZE,\n offset: 0,\n ...(isSearching ? { query: debounced } : {}),\n },\n },\n skip: isDummy || usePreview,\n notifyOnNetworkStatusChange: true,\n });\n\n // Reset page counter on filter change so a fresh result set starts\n // at the top. `debounced` (search) + `usePreview` are deliberate\n // re-run triggers (not read in the body): when the filter changes we\n // want pagination reset to the first page.\n // biome-ignore lint/correctness/useExhaustiveDependencies: debounced + usePreview are intentional reset triggers, not unused deps\n useEffect(() => {\n setVisibleCount(PAGE_SIZE);\n }, [debounced, usePreview]);\n\n // ─── Item projection ─────────────────────────────────────────\n const feedItems = useMemo(\n () =>\n (listData?.visitorNewsList.items ?? [])\n .filter((p): p is NonNullable<typeof p> => !!p)\n .map(toMockShape),\n [listData],\n );\n const previewItems = usePreview\n ? (previewFeed ?? [])\n .filter((p): p is Record<string, unknown> => !!p && typeof p === \"object\")\n .map(toMockShape)\n : [];\n\n const matchesQuery = useCallback(\n (n: MockNewsItem) =>\n n.title.toLowerCase().includes(debounced.toLowerCase()) ||\n n.excerpt.toLowerCase().includes(debounced.toLowerCase()),\n [debounced],\n );\n\n const fullList: MockNewsItem[] = isDummy\n ? isSearching\n ? getMockNewsItems(tDemo).filter(matchesQuery)\n : getMockNewsItems(tDemo)\n : usePreview\n ? isSearching\n ? previewItems.filter(matchesQuery)\n : previewItems\n : feedItems;\n\n // Real mode: Apollo cache holds everything across `fetchMore`\n // calls. Preview/dummy slice up to `visibleCount`.\n const items = usePreview || isDummy ? fullList.slice(0, visibleCount) : fullList;\n\n // ─── Load-more ────────────────────────────────────────────────\n const hasMore = useMemo(() => {\n if (isDummy || usePreview) return visibleCount < fullList.length;\n return listData?.visitorNewsList.meta.hasNextPage ?? false;\n }, [isDummy, usePreview, visibleCount, fullList.length, listData]);\n\n const [loadingMore, setLoadingMore] = useState(false);\n const loadMore = useCallback(async () => {\n if (!hasMore || loadingMore) return;\n if (isDummy || usePreview) {\n setVisibleCount((c) => c + PAGE_SIZE);\n return;\n }\n setLoadingMore(true);\n try {\n await fetchMore({\n variables: {\n args: {\n locale,\n limit: PAGE_SIZE,\n offset: listData?.visitorNewsList.items.length ?? 0,\n ...(isSearching ? { query: debounced } : {}),\n },\n },\n updateQuery: (prev, { fetchMoreResult }) => {\n if (!fetchMoreResult) return prev;\n // Dedup by id when merging pages. The BE's `offset` cursor\n // can return overlapping items in two scenarios:\n // 1. A new post lands between requests — every item\n // shifts by one in the next page, so the boundary\n // row appears in both prev + fetchMore.\n // 2. React StrictMode (dev only) fires `fetchMore`\n // twice; the same page lands twice without dedup.\n // Either case surfaces as React's \"two children with the\n // same key\" warning. Keep prev's instance when there's a\n // collision so Apollo's cache references stay stable.\n const seen = new Set<string>();\n const merged: typeof prev.visitorNewsList.items = [];\n for (const item of [\n ...prev.visitorNewsList.items,\n ...fetchMoreResult.visitorNewsList.items,\n ]) {\n // Raw `NewsPost` shape carries Mongo's `_id` — the\n // local projection that the rendered list uses (`item.id`)\n // is computed downstream from this same field. Dedup on\n // `_id` so we match what Apollo's normalisation keys on.\n if (item && !seen.has(item._id)) {\n seen.add(item._id);\n merged.push(item);\n }\n }\n return {\n ...prev,\n visitorNewsList: {\n ...fetchMoreResult.visitorNewsList,\n items: merged,\n },\n };\n },\n });\n } finally {\n setLoadingMore(false);\n }\n }, [\n hasMore,\n loadingMore,\n isDummy,\n usePreview,\n isSearching,\n debounced,\n locale,\n fetchMore,\n listData,\n ]);\n\n const viewportRef = useScrollRestore(\"news\");\n const handleScroll = useCallback(\n (e: React.UIEvent<HTMLDivElement>) => {\n const el = e.currentTarget;\n const remaining = el.scrollHeight - el.clientHeight - el.scrollTop;\n if (remaining < LOAD_MORE_THRESHOLD) void loadMore();\n },\n [loadMore],\n );\n\n const hasAny = fullList.length > 0 || isSearching;\n const firstLoad = !isDummy && !usePreview && listLoading && items.length === 0;\n\n // Surface BE errors (rare — auth issues) as the empty state copy.\n // Apollo's `errorPolicy: 'all'` lets data + errors coexist; we treat\n // a non-empty list as \"render anyway\".\n if (listError && items.length === 0) {\n return (\n <div className=\"flex flex-1 flex-col items-center justify-center gap-3 px-6 py-12 text-center\">\n <p className=\"text-sm text-wx-fg-muted\">{t(\"emptyTitle\")}</p>\n </div>\n );\n }\n\n return (\n <ScrollArea className=\"flex-1\" viewportRef={viewportRef} onScroll={handleScroll}>\n {hasAny && (\n <StickySearchHeader\n value={query}\n onChange={setQuery}\n placeholder={t(\"searchPlaceholder\")}\n />\n )}\n <div className=\"flex flex-col gap-3 px-4 pt-4 pb-6\">\n {firstLoad ? (\n Array.from({ length: 3 }).map((_, idx) => (\n // biome-ignore lint/suspicious/noArrayIndexKey: static placeholder list\n <div key={idx} className=\"overflow-hidden rounded-wx-lg bg-wx-bg-elevated\">\n <Skeleton className=\"aspect-[16/10] w-full rounded-none\" />\n <div className=\"space-y-2 px-5 py-4\">\n <Skeleton className=\"h-3 w-3/4\" />\n <Skeleton className=\"h-3 w-1/2\" />\n <Skeleton className=\"mt-1 h-2 w-1/3\" />\n </div>\n </div>\n ))\n ) : items.length === 0 ? (\n <div className=\"flex flex-1 flex-col items-center justify-center gap-3 px-6 py-12 text-center\">\n <div className=\"flex h-14 w-14 items-center justify-center rounded-full bg-wx-bg-elevated text-wx-fg-muted\">\n <Icon icon={Megaphone} size={28} />\n </div>\n <p className=\"text-base font-semibold text-wx-fg\">\n {isSearching ? t(\"noResults\") : t(\"emptyTitle\")}\n </p>\n {!isSearching && <p className=\"text-sm text-wx-fg-muted\">{t(\"emptyBody\")}</p>}\n </div>\n ) : (\n <>\n {items.map((item, idx) => (\n <NewsCardButton\n key={item.id}\n item={item}\n locale={locale}\n delay={idx < PAGE_SIZE ? idx * 0.04 : 0}\n onClick={() => onOpenArticle(item.id)}\n />\n ))}\n {loadingMore && (\n <div className=\"flex justify-center py-3\">\n <Skeleton className=\"h-3 w-24\" />\n </div>\n )}\n </>\n )}\n </div>\n </ScrollArea>\n );\n}\n\n/** Project a raw `NewsPost` (or prefetched operator-side row) into\n * the in-memory shape the news cards consume. Tags / cover / edit-\n * badge fields all resolve through this single mapper. */\nfunction toMockShape(raw: Record<string, unknown>, idx = 0): MockNewsItem {\n const mediaCover = (raw.coverImageUrl as Record<string, unknown> | undefined)?.url;\n const externalCover = raw.externalCoverImageUrl;\n const cover =\n typeof mediaCover === \"string\"\n ? mediaCover\n : typeof externalCover === \"string\"\n ? externalCover\n : null;\n\n const toChips = (raw_: unknown): { slug: string; label: string }[] | undefined =>\n Array.isArray(raw_)\n ? (raw_ as unknown[])\n .map((t) => {\n if (!t || typeof t !== \"object\") return null;\n const slug = (t as { slug?: unknown }).slug;\n const label = (t as { label?: unknown }).label;\n if (typeof slug !== \"string\" || typeof label !== \"string\") {\n return null;\n }\n return { slug, label };\n })\n .filter((t): t is { slug: string; label: string } => !!t)\n : undefined;\n\n const tags = toChips(raw.tags);\n // BE localizes the embedded category/tag labels against the visitor's\n // locale (the list query passes it), so these arrive already-translated.\n const categories = toChips(raw.categories);\n\n // Authors (operator byline) → `{ _id, name, photo: { url } }`, the\n // shape `MockNewsItem.authors` + `AuthorAvatars` both accept.\n const authors = Array.isArray(raw.authors)\n ? (raw.authors as unknown[])\n .map((a) => {\n if (!a || typeof a !== \"object\") return null;\n const name = (a as { name?: unknown }).name;\n if (typeof name !== \"string\") return null;\n const aid = (a as { _id?: unknown })._id;\n const url = (a as { photo?: { url?: unknown } }).photo?.url;\n return {\n _id: String(aid ?? name),\n name,\n photo: { url: typeof url === \"string\" ? url : \"\" },\n };\n })\n .filter((a): a is { _id: string; name: string; photo: { url: string } } => !!a)\n : undefined;\n\n return {\n id: String(raw._id ?? raw.id ?? `news-${idx}`),\n title: typeof raw.title === \"string\" ? raw.title : \"\",\n excerpt: typeof raw.excerpt === \"string\" ? raw.excerpt : \"\",\n publishedAt: typeof raw.publishedAt === \"string\" ? raw.publishedAt : new Date().toISOString(),\n coverGradient: [\"#1e1f21\", \"#121314\"],\n ...(cover ? { coverImageUrl: cover } : {}),\n ...(typeof raw.upstreamUpdatedAt === \"string\"\n ? { upstreamUpdatedAt: raw.upstreamUpdatedAt }\n : {}),\n ...(tags && tags.length > 0 ? { tags } : {}),\n ...(categories && categories.length > 0 ? { categories } : {}),\n ...(authors && authors.length > 0 ? { authors } : {}),\n reactionCounts: (raw.reactionCounts as Record<string, number> | null | undefined) ?? null,\n };\n}\n\n/**\n * Stock 300 ms debounce — keeps the BE search load proportional to\n * the visitor's typing cadence, not their keystroke rate.\n */\nfunction useDebounced(value: string, delay: number): string {\n const [debounced, setDebounced] = useState(value);\n useEffect(() => {\n const t = setTimeout(() => setDebounced(value), delay);\n return () => clearTimeout(t);\n }, [value, delay]);\n return debounced;\n}\n\nfunction NewsCardButton({\n item,\n locale,\n delay,\n onClick,\n}: {\n item: MockNewsItem;\n locale: string;\n delay: number;\n onClick: () => void;\n}) {\n const t = useTranslations(\"news\");\n const cats = item.categories ?? (item.category ? [item.category] : []);\n const authorChips = toAuthorChips(item.authors);\n return (\n <motion.button\n type=\"button\"\n onClick={onClick}\n initial={{ opacity: 0, y: 8 }}\n animate={{ opacity: 1, y: 0 }}\n whileHover={{ y: -2 }}\n whileTap={{ scale: 0.985 }}\n transition={{ duration: 0.2, delay }}\n className={cn(\n \"block w-full shrink-0 overflow-hidden rounded-wx-lg bg-wx-bg-elevated text-left\",\n \"transition-colors hover:bg-wx-bg-elevated-2\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-wx-primary focus-visible:ring-offset-2 focus-visible:ring-offset-wx-bg\",\n )}\n >\n <div\n className=\"aspect-[16/10] w-full bg-cover bg-center\"\n style={\n item.coverImageUrl\n ? { backgroundImage: `url(${item.coverImageUrl})` }\n : {\n background: `linear-gradient(135deg, ${item.coverGradient[0]}, ${item.coverGradient[1]})`,\n }\n }\n aria-hidden=\"true\"\n />\n <div className=\"px-5 py-4\">\n {(cats.length > 0 || authorChips != null) && (\n // Categories on the left, author byline avatars on the right.\n <div className=\"mb-1.5 flex items-center justify-between gap-2\">\n <div className=\"flex min-w-0 flex-wrap gap-1.5\">\n {cats.slice(0, 2).map((cat) => (\n <span\n key={cat.slug}\n className=\"text-[10px] font-semibold uppercase tracking-wide text-wx-primary\"\n >\n {cat.label}\n </span>\n ))}\n </div>\n <AuthorAvatars authors={authorChips} className=\"shrink-0\" />\n </div>\n )}\n <CardTitle className=\"text-sm\">{item.title}</CardTitle>\n {item.excerpt && <CardSubtitle className=\"mt-1 text-xs\">{item.excerpt}</CardSubtitle>}\n <p className=\"mt-2 flex items-center gap-2 text-xs text-wx-fg-subtle\">\n <span>\n {new Date(item.publishedAt).toLocaleDateString(locale, {\n month: \"short\",\n day: \"numeric\",\n year: \"numeric\",\n })}\n </span>\n {isNewsPostEdited(item) && item.upstreamUpdatedAt && (\n <span\n className=\"rounded-full bg-wx-bg-elevated px-1.5 py-0.5 text-[10px] font-medium text-wx-fg-muted\"\n title={new Date(item.upstreamUpdatedAt).toLocaleString(locale)}\n >\n {t(\"editedAt\", { when: formatNewsEditedAgo(item.upstreamUpdatedAt) })}\n </span>\n )}\n </p>\n {item.tags && item.tags.length > 0 && (\n <div className=\"mt-2.5 flex flex-wrap gap-1\">\n {item.tags.slice(0, 4).map((tag) => (\n <span\n key={tag.slug}\n className=\"rounded-full bg-wx-bg-elevated px-2 py-0.5 text-[10px] font-medium text-wx-fg-muted\"\n >\n {tag.label}\n </span>\n ))}\n </div>\n )}\n </div>\n </motion.button>\n );\n}\n"],"x_google_ignoreList":[1],"mappings":"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"}