@wexio/messenger-widget-react 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +133 -0
- package/dist/widget-react-AgErOR36.js +325 -0
- package/dist/widget-react-AgErOR36.js.map +1 -0
- package/dist/widget-react-BAOIDvhV.js +4250 -0
- package/dist/widget-react-BAOIDvhV.js.map +1 -0
- package/dist/widget-react-BfPdgAQ-.js +5767 -0
- package/dist/widget-react-BfPdgAQ-.js.map +1 -0
- package/dist/widget-react-BxvW64LH.js +4429 -0
- package/dist/widget-react-BxvW64LH.js.map +1 -0
- package/dist/widget-react-C5COLy3m.js +23847 -0
- package/dist/widget-react-C5COLy3m.js.map +1 -0
- package/dist/widget-react-CAWxn8I0.js +247 -0
- package/dist/widget-react-CAWxn8I0.js.map +1 -0
- package/dist/widget-react-CDPzwV1i.js +11685 -0
- package/dist/widget-react-CDPzwV1i.js.map +1 -0
- package/dist/widget-react-CPYwD6FR.js +498 -0
- package/dist/widget-react-CPYwD6FR.js.map +1 -0
- package/dist/widget-react-CUBFPjaV.js +23 -0
- package/dist/widget-react-CUBFPjaV.js.map +1 -0
- package/dist/widget-react-CUuKGDZN.js +355 -0
- package/dist/widget-react-CUuKGDZN.js.map +1 -0
- package/dist/widget-react-CZs0xRrr.js +1336 -0
- package/dist/widget-react-CZs0xRrr.js.map +1 -0
- package/dist/widget-react-Cj_zbk_F.js +325 -0
- package/dist/widget-react-Cj_zbk_F.js.map +1 -0
- package/dist/widget-react-D-QrEVpv.js +125 -0
- package/dist/widget-react-D-QrEVpv.js.map +1 -0
- package/dist/widget-react-D8TPnM9r.js +26 -0
- package/dist/widget-react-D8TPnM9r.js.map +1 -0
- package/dist/widget-react-DE7AVrCo.js +5737 -0
- package/dist/widget-react-DE7AVrCo.js.map +1 -0
- package/dist/widget-react-DOX6j7_Q.js +30 -0
- package/dist/widget-react-DOX6j7_Q.js.map +1 -0
- package/dist/widget-react-DSw03RG7.js +262 -0
- package/dist/widget-react-DSw03RG7.js.map +1 -0
- package/dist/widget-react-Dew7YaLU.js +16 -0
- package/dist/widget-react-Dew7YaLU.js.map +1 -0
- package/dist/widget-react-DtOeNw5z.js +325 -0
- package/dist/widget-react-DtOeNw5z.js.map +1 -0
- package/dist/widget-react-DwL_rsx_.js +630 -0
- package/dist/widget-react-DwL_rsx_.js.map +1 -0
- package/dist/widget-react.d.ts +301 -0
- package/dist/widget-react.js +22285 -0
- package/dist/widget-react.js.map +1 -0
- package/package.json +41 -0
|
@@ -0,0 +1 @@
|
|
|
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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"}
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import type { CSSProperties } from "react";
|
|
2
|
+
|
|
3
|
+
export type WexioWidgetLocale = "en" | "uk";
|
|
4
|
+
/** Public surface — mode is always production. Preview + demo are
|
|
5
|
+
* private-only (dashboard embed) and not reachable from npm. */
|
|
6
|
+
export type WexioWidgetMode = "production";
|
|
7
|
+
|
|
8
|
+
export type WidgetTab = "home" | "messages" | "help" | "news" | "profile";
|
|
9
|
+
|
|
10
|
+
export interface WidgetFeatures {
|
|
11
|
+
home: boolean;
|
|
12
|
+
messenger: boolean;
|
|
13
|
+
help: boolean;
|
|
14
|
+
news: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Designer-friendly colour tokens — mirrors the BE's
|
|
18
|
+
* `WebIntegrationThemePalette`. Every field optional. */
|
|
19
|
+
export interface WidgetThemePalette {
|
|
20
|
+
background?: string;
|
|
21
|
+
surface?: string;
|
|
22
|
+
surfaceElevated?: string;
|
|
23
|
+
text?: string;
|
|
24
|
+
textMuted?: string;
|
|
25
|
+
textSubtle?: string;
|
|
26
|
+
border?: string;
|
|
27
|
+
borderStrong?: string;
|
|
28
|
+
primary?: string;
|
|
29
|
+
primaryHover?: string;
|
|
30
|
+
primaryForeground?: string;
|
|
31
|
+
launcherBackground?: string;
|
|
32
|
+
launcherForeground?: string;
|
|
33
|
+
success?: string;
|
|
34
|
+
danger?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface WidgetThemeRadii {
|
|
38
|
+
sm?: number;
|
|
39
|
+
md?: number;
|
|
40
|
+
lg?: number;
|
|
41
|
+
xl?: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type LauncherPosition = "RIGHT_BOTTOM" | "LEFT_BOTTOM";
|
|
45
|
+
|
|
46
|
+
export interface WidgetTheme {
|
|
47
|
+
light?: WidgetThemePalette;
|
|
48
|
+
dark?: WidgetThemePalette;
|
|
49
|
+
radii?: WidgetThemeRadii;
|
|
50
|
+
/** Which corner the launcher + panel anchor to. Defaults to bottom-right. */
|
|
51
|
+
launcherPosition?: LauncherPosition;
|
|
52
|
+
/** Full CSS `font-family` stack applied to the whole widget.
|
|
53
|
+
* Theme-invariant; falls back to the bundled Geist default when unset. */
|
|
54
|
+
fontFamily?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type PrechatFieldKind = "NAME" | "EMAIL" | "PHONE" | "CUSTOM_TEXT";
|
|
58
|
+
|
|
59
|
+
export interface WidgetPrechatField {
|
|
60
|
+
kind: PrechatFieldKind;
|
|
61
|
+
key: string;
|
|
62
|
+
label: string;
|
|
63
|
+
required: boolean;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface WidgetPrechatForm {
|
|
67
|
+
enabled: boolean;
|
|
68
|
+
fields: WidgetPrechatField[];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface WidgetSounds {
|
|
72
|
+
enabled: boolean;
|
|
73
|
+
inboundSoundId: string | null;
|
|
74
|
+
outboundSoundId: string | null;
|
|
75
|
+
volume: number;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export type WidgetMessengerMessageKind = "HINT" | "SYSTEM";
|
|
79
|
+
|
|
80
|
+
export interface WidgetMessengerMessage {
|
|
81
|
+
kind: WidgetMessengerMessageKind;
|
|
82
|
+
text: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface WidgetMessengerConfig {
|
|
86
|
+
title: string | null;
|
|
87
|
+
description: string | null;
|
|
88
|
+
aiAssistantAvatar: string | null;
|
|
89
|
+
commands: string[];
|
|
90
|
+
messages: WidgetMessengerMessage[];
|
|
91
|
+
showRelatedNews: boolean;
|
|
92
|
+
showRelatedHelpArticles: boolean;
|
|
93
|
+
showReactionCounts: boolean;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export type WidgetLocaleStrategy = "AUTO" | "WEBSITE" | "DEFAULT";
|
|
97
|
+
|
|
98
|
+
/* ── Home-tab block model ─────────────────────────────────────────── */
|
|
99
|
+
|
|
100
|
+
export type HomeNamedIcon =
|
|
101
|
+
| "telegram"
|
|
102
|
+
| "whatsapp"
|
|
103
|
+
| "viber"
|
|
104
|
+
| "instagram"
|
|
105
|
+
| "messenger"
|
|
106
|
+
| "discord"
|
|
107
|
+
| "slack"
|
|
108
|
+
| "chat"
|
|
109
|
+
| "email"
|
|
110
|
+
| "phone"
|
|
111
|
+
| "video-call"
|
|
112
|
+
| "help"
|
|
113
|
+
| "book"
|
|
114
|
+
| "bell"
|
|
115
|
+
| "star"
|
|
116
|
+
| "heart"
|
|
117
|
+
| "bookmark"
|
|
118
|
+
| "arrow-right"
|
|
119
|
+
| "lock"
|
|
120
|
+
| "shield"
|
|
121
|
+
| "user"
|
|
122
|
+
| "users";
|
|
123
|
+
|
|
124
|
+
export type HomeQuickActionIcon =
|
|
125
|
+
| { kind: "named"; name: HomeNamedIcon }
|
|
126
|
+
| { kind: "media"; url: string };
|
|
127
|
+
|
|
128
|
+
export interface HomeQuickActionButton {
|
|
129
|
+
label?: string;
|
|
130
|
+
icon?: HomeQuickActionIcon;
|
|
131
|
+
action: "open-messenger" | "open-help" | "open-news" | "open-url";
|
|
132
|
+
url?: string;
|
|
133
|
+
style?: "default" | "primary" | "ghost";
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export type HomeQuickActionLayout = "stacked" | "inline";
|
|
137
|
+
|
|
138
|
+
export interface HomeHelpSuggestionsConfig {
|
|
139
|
+
mode: "popular" | "recent" | "category" | "manual";
|
|
140
|
+
maxItems: number;
|
|
141
|
+
categoryId?: string;
|
|
142
|
+
articleIds?: string[];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export type HomeBlock =
|
|
146
|
+
| {
|
|
147
|
+
kind: "quick-actions";
|
|
148
|
+
buttons: HomeQuickActionButton[];
|
|
149
|
+
layout?: HomeQuickActionLayout;
|
|
150
|
+
}
|
|
151
|
+
| {
|
|
152
|
+
kind: "help-search";
|
|
153
|
+
placeholder?: string;
|
|
154
|
+
suggestions: HomeHelpSuggestionsConfig;
|
|
155
|
+
}
|
|
156
|
+
| { kind: "pinned-articles"; articleIds: string[]; title?: string }
|
|
157
|
+
| { kind: "news-preview"; maxItems?: number; title?: string }
|
|
158
|
+
| {
|
|
159
|
+
kind: "team-status";
|
|
160
|
+
showResponseTime: boolean;
|
|
161
|
+
showOperatorAvatars: boolean;
|
|
162
|
+
}
|
|
163
|
+
| { kind: "divider" }
|
|
164
|
+
| { kind: "rich-text"; markdown: string }
|
|
165
|
+
| {
|
|
166
|
+
kind: "ask-question";
|
|
167
|
+
title?: string;
|
|
168
|
+
subtitle?: string;
|
|
169
|
+
action?: "open-messenger" | "open-help" | "open-news";
|
|
170
|
+
}
|
|
171
|
+
| { kind: "recent-message"; title?: string }
|
|
172
|
+
| {
|
|
173
|
+
kind: "featured-article";
|
|
174
|
+
newsId?: string;
|
|
175
|
+
ctaLabel?: string;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
export type HomeBlockKind = HomeBlock["kind"];
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Host-injectable subset of `WidgetSecurity`. `requireAuth` /
|
|
182
|
+
* `google.enabled` / `passkey.enabled` are UI conveniences (BE
|
|
183
|
+
* enforces gating server-side regardless). `clientId` and
|
|
184
|
+
* `allowedOrigins` are intentionally NOT injectable — BE-owned.
|
|
185
|
+
*/
|
|
186
|
+
export interface InjectableWidgetSecurity {
|
|
187
|
+
requireAuth?: boolean;
|
|
188
|
+
google?: { enabled?: boolean };
|
|
189
|
+
passkey?: { enabled?: boolean };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Public widget config — the trimmed shape operators can pre-resolve
|
|
194
|
+
* and pass to the component. `status`, `branding`, and
|
|
195
|
+
* `security.allowedOrigins` are intentionally NOT here: those are
|
|
196
|
+
* server-owned and not host-overridable.
|
|
197
|
+
*/
|
|
198
|
+
export interface InjectableWidgetConfig {
|
|
199
|
+
integrationId?: string;
|
|
200
|
+
name?: string;
|
|
201
|
+
defaultTab: WidgetTab;
|
|
202
|
+
features: WidgetFeatures;
|
|
203
|
+
theme: WidgetTheme | null;
|
|
204
|
+
themeMode: "light" | "dark" | "auto";
|
|
205
|
+
operatorAvatars: { src: string; alt: string }[];
|
|
206
|
+
/** Per-mode organisation logo URLs (already flattened from BE Media
|
|
207
|
+
* refs). `null` for the whole object = operator never uploaded one. */
|
|
208
|
+
organizationLogo: { light: string | null; dark: string | null } | null;
|
|
209
|
+
greeting: {
|
|
210
|
+
headline: string;
|
|
211
|
+
subheadline: string;
|
|
212
|
+
};
|
|
213
|
+
/** Operator-configured Home-tab block layout. `null` → the widget's
|
|
214
|
+
* bundled default layout. */
|
|
215
|
+
homeLayout: HomeBlock[] | null;
|
|
216
|
+
prechatForm: WidgetPrechatForm | null;
|
|
217
|
+
sounds: WidgetSounds | null;
|
|
218
|
+
messenger: WidgetMessengerConfig | null;
|
|
219
|
+
locales?: { primary: string; supported: string[]; strategy?: WidgetLocaleStrategy };
|
|
220
|
+
/** Sign-in methods — host can flip `enabled` per method. `clientId`
|
|
221
|
+
* and `requireAuth` actual enforcement are BE-sourced. */
|
|
222
|
+
security?: InjectableWidgetSecurity;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export interface VisitorIdentity {
|
|
226
|
+
/** Google FedCM id_token (preferred). */
|
|
227
|
+
googleIdToken?: string;
|
|
228
|
+
/** Host-signed JWT carrying the visitor's identity claims. */
|
|
229
|
+
jwt?: string;
|
|
230
|
+
/** Legacy userId + HMAC pair — `userHash = HMAC-SHA256(userId, integrationSecret)`. */
|
|
231
|
+
userId?: string;
|
|
232
|
+
userHash?: string;
|
|
233
|
+
name?: string;
|
|
234
|
+
email?: string;
|
|
235
|
+
phone?: string;
|
|
236
|
+
/** Open key/value bag — populates the People profile's context snapshot. */
|
|
237
|
+
attributes?: Record<string, unknown>;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export interface VisitorPrefill {
|
|
241
|
+
name?: string;
|
|
242
|
+
email?: string;
|
|
243
|
+
phone?: string;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export interface WexioWidgetProps {
|
|
247
|
+
publicKey?: string;
|
|
248
|
+
locale?: WexioWidgetLocale;
|
|
249
|
+
/** Always `"production"` on the public package. `preview` and `demo`
|
|
250
|
+
* modes are dashboard-only and not exposed here. */
|
|
251
|
+
mode?: WexioWidgetMode;
|
|
252
|
+
/**
|
|
253
|
+
* Log a known user into the widget (the React equivalent of
|
|
254
|
+
* Intercom's `boot({ user_id, ... })`). Provide ONE proof:
|
|
255
|
+
* `googleIdToken`, `jwt`, or the legacy `userId` + `userHash` pair.
|
|
256
|
+
* `name` / `email` / `phone` populate the People profile;
|
|
257
|
+
* `attributes` is an open bag of extra fields.
|
|
258
|
+
*
|
|
259
|
+
* Memoise this — a fresh object literal every render churns the
|
|
260
|
+
* env (the handshake itself is guarded against re-firing unless the
|
|
261
|
+
* proof actually changes).
|
|
262
|
+
*/
|
|
263
|
+
user?: VisitorIdentity;
|
|
264
|
+
/**
|
|
265
|
+
* Default profile values to PRE-FILL the prechat form — the lighter,
|
|
266
|
+
* unverified cousin of `user` (no `userHash`). When they cover every
|
|
267
|
+
* required prechat field the widget attaches them + skips the form;
|
|
268
|
+
* otherwise it pre-fills what it can.
|
|
269
|
+
*/
|
|
270
|
+
prefill?: VisitorPrefill;
|
|
271
|
+
/**
|
|
272
|
+
* Full, pre-resolved widget config. When provided, the widget boots
|
|
273
|
+
* from it directly and SKIPS the `GET /api/web/config/:pk` bootstrap
|
|
274
|
+
* request — for a host that already has the config server-rendered
|
|
275
|
+
* or fetched once app-wide. Still pass `publicKey` — it's needed for
|
|
276
|
+
* the GraphQL header + visitor handshake; only the config fetch is
|
|
277
|
+
* skipped. Memoise this.
|
|
278
|
+
*/
|
|
279
|
+
config?: InjectableWidgetConfig;
|
|
280
|
+
/**
|
|
281
|
+
* Render the media lightbox as a viewport-`fixed` overlay instead of
|
|
282
|
+
* one contained to this widget's host box. Set `true` when the widget
|
|
283
|
+
* is rendered very small (e.g. a floating mobile preview).
|
|
284
|
+
*/
|
|
285
|
+
lightboxViewport?: boolean;
|
|
286
|
+
onResize?: (size: { width: number; height: number }) => void;
|
|
287
|
+
onClose?: () => void;
|
|
288
|
+
className?: string;
|
|
289
|
+
style?: CSSProperties;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export type WexioWidgetReactProps = WexioWidgetProps;
|
|
293
|
+
|
|
294
|
+
export declare function WexioWidget(props: WexioWidgetProps): JSX.Element;
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Imperatively stamp the public-key onto the apollo singleton.
|
|
298
|
+
* Useful for SSR data fetching paths that need the
|
|
299
|
+
* `x-web-public-key` header before the component mounts.
|
|
300
|
+
*/
|
|
301
|
+
export declare function setWexioPublicKey(key: string | null): void;
|