@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.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +133 -0
  3. package/dist/widget-react-AgErOR36.js +325 -0
  4. package/dist/widget-react-AgErOR36.js.map +1 -0
  5. package/dist/widget-react-BAOIDvhV.js +4250 -0
  6. package/dist/widget-react-BAOIDvhV.js.map +1 -0
  7. package/dist/widget-react-BfPdgAQ-.js +5767 -0
  8. package/dist/widget-react-BfPdgAQ-.js.map +1 -0
  9. package/dist/widget-react-BxvW64LH.js +4429 -0
  10. package/dist/widget-react-BxvW64LH.js.map +1 -0
  11. package/dist/widget-react-C5COLy3m.js +23847 -0
  12. package/dist/widget-react-C5COLy3m.js.map +1 -0
  13. package/dist/widget-react-CAWxn8I0.js +247 -0
  14. package/dist/widget-react-CAWxn8I0.js.map +1 -0
  15. package/dist/widget-react-CDPzwV1i.js +11685 -0
  16. package/dist/widget-react-CDPzwV1i.js.map +1 -0
  17. package/dist/widget-react-CPYwD6FR.js +498 -0
  18. package/dist/widget-react-CPYwD6FR.js.map +1 -0
  19. package/dist/widget-react-CUBFPjaV.js +23 -0
  20. package/dist/widget-react-CUBFPjaV.js.map +1 -0
  21. package/dist/widget-react-CUuKGDZN.js +355 -0
  22. package/dist/widget-react-CUuKGDZN.js.map +1 -0
  23. package/dist/widget-react-CZs0xRrr.js +1336 -0
  24. package/dist/widget-react-CZs0xRrr.js.map +1 -0
  25. package/dist/widget-react-Cj_zbk_F.js +325 -0
  26. package/dist/widget-react-Cj_zbk_F.js.map +1 -0
  27. package/dist/widget-react-D-QrEVpv.js +125 -0
  28. package/dist/widget-react-D-QrEVpv.js.map +1 -0
  29. package/dist/widget-react-D8TPnM9r.js +26 -0
  30. package/dist/widget-react-D8TPnM9r.js.map +1 -0
  31. package/dist/widget-react-DE7AVrCo.js +5737 -0
  32. package/dist/widget-react-DE7AVrCo.js.map +1 -0
  33. package/dist/widget-react-DOX6j7_Q.js +30 -0
  34. package/dist/widget-react-DOX6j7_Q.js.map +1 -0
  35. package/dist/widget-react-DSw03RG7.js +262 -0
  36. package/dist/widget-react-DSw03RG7.js.map +1 -0
  37. package/dist/widget-react-Dew7YaLU.js +16 -0
  38. package/dist/widget-react-Dew7YaLU.js.map +1 -0
  39. package/dist/widget-react-DtOeNw5z.js +325 -0
  40. package/dist/widget-react-DtOeNw5z.js.map +1 -0
  41. package/dist/widget-react-DwL_rsx_.js +630 -0
  42. package/dist/widget-react-DwL_rsx_.js.map +1 -0
  43. package/dist/widget-react.d.ts +301 -0
  44. package/dist/widget-react.js +22285 -0
  45. package/dist/widget-react.js.map +1 -0
  46. package/package.json +41 -0
@@ -0,0 +1,247 @@
1
+ import "react";
2
+ import "react-dom";
3
+ import "react/jsx-runtime";
4
+ import "react-dom/client";
5
+ import { i as e } from "./widget-react-DE7AVrCo.js";
6
+ import { E as t, d as n, f as r, n as i, x as a } from "./widget-react-BfPdgAQ-.js";
7
+ import { a as o, c as s, i as c, l, n as u, o as d, r as f, s as p, t as m } from "./widget-react-CDPzwV1i.js";
8
+ import { l as h, n as g, o as _, p as v, s as y, t as b } from "./widget-react-BAOIDvhV.js";
9
+ import { i as x, n as S } from "./widget-react-BxvW64LH.js";
10
+ import { t as C } from "./widget-react-D8TPnM9r.js";
11
+ import { useEffect as w, useMemo as T, useState as ee } from "react";
12
+ import { jsx as E, jsxs as D } from "react/jsx-runtime";
13
+ //#region components/widget/help-article-view/helpers.ts
14
+ function O(e) {
15
+ let t = (e) => Array.isArray(e) ? e : [];
16
+ return {
17
+ id: String(e._id ?? ""),
18
+ title: typeof e.title == "string" ? e.title : "",
19
+ excerpt: typeof e.excerpt == "string" ? e.excerpt : "",
20
+ content: typeof e.content == "string" ? e.content : "",
21
+ contentFormat: e.contentFormat === p.HTML ? p.HTML : p.MARKDOWN,
22
+ authors: t(e.authors).filter((e) => !!e && typeof e == "object").map((e) => ({
23
+ _id: String(e._id ?? ""),
24
+ name: typeof e.name == "string" ? e.name : null,
25
+ photo: e.photo
26
+ })),
27
+ tags: t(e.tags).filter((e) => !!e && typeof e == "object").map((e) => ({
28
+ _id: String(e._id ?? ""),
29
+ slug: typeof e.slug == "string" ? e.slug : "",
30
+ label: typeof e.label == "string" ? e.label : ""
31
+ }))
32
+ };
33
+ }
34
+ //#endregion
35
+ //#region components/widget/help-article-view/index.tsx
36
+ function k({ slug: S, locale: k, isDummy: j, onHandoff: M, onOpenArticle: N, onOpenTranslation: P, onTitleResolved: F }) {
37
+ let I = x("help"), L = x("demo"), R = t(), { config: z } = a(), B = z.messenger?.showRelatedHelpArticles ?? !0, V = z.messenger?.showReactionCounts ?? !0, H = C(), [U, W] = ee(k), G = o(U, L), K = T(() => {
38
+ if (j) return null;
39
+ let e = R.previewData?.helpArticles;
40
+ return Array.isArray(e) ? e.find((e) => e && typeof e == "object" && e.slug === S) ?? null : null;
41
+ }, [
42
+ R.previewData,
43
+ j,
44
+ S
45
+ ]), q = !!K, { data: J, loading: Y } = g({
46
+ variables: { args: {
47
+ slug: S,
48
+ locale: k
49
+ } },
50
+ skip: j || q
51
+ }), [X] = b(), Z = T(() => j ? h(G).find((e) => e.slug === S) : null, [
52
+ j,
53
+ S,
54
+ G
55
+ ]), Q = j ? Z ? {
56
+ id: Z.id,
57
+ title: Z.title,
58
+ excerpt: Z.excerpt,
59
+ content: Z.contentMarkdown,
60
+ contentFormat: p.MARKDOWN,
61
+ authors: Z.authors,
62
+ tags: Z.tags,
63
+ reactionCounts: null,
64
+ locale: U,
65
+ translations: []
66
+ } : null : q && K ? {
67
+ ...O(K),
68
+ reactionCounts: K.reactionCounts ?? null,
69
+ locale: k,
70
+ translations: []
71
+ } : J?.visitorHelpArticle ? {
72
+ id: J.visitorHelpArticle._id,
73
+ title: J.visitorHelpArticle.title,
74
+ excerpt: J.visitorHelpArticle.excerpt ?? "",
75
+ content: J.visitorHelpArticle.content,
76
+ contentFormat: J.visitorHelpArticle.contentFormat,
77
+ authors: J.visitorHelpArticle.authors,
78
+ tags: J.visitorHelpArticle.tags,
79
+ reactionCounts: J.visitorHelpArticle.reactionCounts ?? null,
80
+ locale: J.visitorHelpArticle.locale,
81
+ translations: J.visitorHelpArticle.translations
82
+ } : null, { data: te } = _({
83
+ variables: {
84
+ articleId: Q?.id ?? "",
85
+ limit: 5,
86
+ locale: k
87
+ },
88
+ skip: j || q || !Q?.id || !B
89
+ }), $ = te?.visitorHelpRelated ?? [], ne = j || q ? null : J?.visitorHelpArticle?.viewerReaction ?? null, re = !j && !q && J?.visitorHelpArticle ? [J.visitorHelpArticle._id, ...(J.visitorHelpArticle.translations ?? []).map((e) => e._id)] : Q?.id ? [Q.id] : [];
90
+ return w(() => {
91
+ j || !Q?.id || !v() || X({ variables: { articleId: Q.id } }).catch(() => {});
92
+ }, [
93
+ Q?.id,
94
+ j,
95
+ X
96
+ ]), d(Q?.title, F), !Y && !Q ? /* @__PURE__ */ E("div", {
97
+ className: "flex flex-1 flex-col items-center justify-center px-6 py-12",
98
+ children: /* @__PURE__ */ E(n, { children: /* @__PURE__ */ E(r, {
99
+ className: "py-8 text-center",
100
+ children: /* @__PURE__ */ E("p", {
101
+ className: "text-sm font-semibold text-wx-fg",
102
+ children: I("articleNotFound")
103
+ })
104
+ }) })
105
+ }) : Q ? /* @__PURE__ */ E(i, {
106
+ className: "flex-1",
107
+ children: /* @__PURE__ */ D("div", {
108
+ className: "flex flex-col px-5 pt-5 pb-12",
109
+ children: [
110
+ /* @__PURE__ */ E("h2", {
111
+ className: "text-xl leading-tight font-bold text-wx-fg",
112
+ children: Q.title
113
+ }),
114
+ Q.authors.length > 0 && /* @__PURE__ */ D("p", {
115
+ className: "mt-2 flex items-center gap-2 text-xs text-wx-fg-muted",
116
+ children: [/* @__PURE__ */ E("span", {
117
+ className: "flex -space-x-1.5",
118
+ children: Q.authors.slice(0, 3).map((e) => /* @__PURE__ */ E("span", {
119
+ className: "h-5 w-5 overflow-hidden rounded-full bg-wx-bg-elevated ring-1 ring-wx-bg",
120
+ "aria-hidden": "true",
121
+ children: e.photo?.url ? /* @__PURE__ */ E("img", {
122
+ src: e.photo.url,
123
+ alt: "",
124
+ className: "h-full w-full object-cover"
125
+ }) : /* @__PURE__ */ E("span", {
126
+ className: "flex h-full w-full items-center justify-center text-[10px] font-semibold uppercase text-wx-fg-muted",
127
+ children: (e.name ?? "?").charAt(0)
128
+ })
129
+ }, e._id))
130
+ }), /* @__PURE__ */ E("span", { children: I("writtenBy", { names: Q.authors.map((e) => e.name ?? "").filter(Boolean).join(", ") }) })]
131
+ }),
132
+ Q.tags.length > 0 && /* @__PURE__ */ E("div", {
133
+ className: "mt-2 flex flex-wrap gap-1",
134
+ children: Q.tags.map((e) => /* @__PURE__ */ E("span", {
135
+ className: "rounded-full bg-wx-bg-elevated px-2 py-0.5 text-[11px] font-medium text-wx-fg-muted",
136
+ children: e.label
137
+ }, e._id))
138
+ }),
139
+ Q.excerpt && /* @__PURE__ */ E("p", {
140
+ className: "mt-2 mb-5 text-sm text-wx-fg-muted",
141
+ children: Q.excerpt
142
+ }),
143
+ /* @__PURE__ */ E(f, {
144
+ content: Q.content,
145
+ isHtml: Q.contentFormat === p.HTML || l(Q.content),
146
+ markdownMarginClass: "mt-3"
147
+ }),
148
+ /* @__PURE__ */ D(y, {
149
+ type: "button",
150
+ variant: "solid",
151
+ size: "lg",
152
+ className: "mt-6 w-full shrink-0 min-h-12",
153
+ onClick: () => M(),
154
+ ...H.parentHandlers,
155
+ children: [/* @__PURE__ */ E(e, {
156
+ ref: H.iconRef,
157
+ size: 16
158
+ }), /* @__PURE__ */ E("span", { children: I("unresolvedCta") })]
159
+ }),
160
+ Q.id && /* @__PURE__ */ E(m, {
161
+ surface: s.HELP,
162
+ itemId: Q.id,
163
+ reactionCounts: Q.reactionCounts,
164
+ viewerReaction: ne,
165
+ groupItemIds: re,
166
+ isDummy: j,
167
+ showCounts: V,
168
+ className: "mt-4"
169
+ }),
170
+ /* @__PURE__ */ E(u, {
171
+ currentLocale: j ? U : Q.locale,
172
+ locales: j ? [...c] : Q.translations.map((e) => e.locale),
173
+ onSelect: j ? (e) => W(e) : (e) => {
174
+ let t = Q.translations.find((t) => t.locale === e);
175
+ t && P?.(t.slug, t.locale);
176
+ },
177
+ className: "mt-6"
178
+ }),
179
+ B && $.length > 0 && /* @__PURE__ */ D("section", {
180
+ className: "mt-6",
181
+ children: [/* @__PURE__ */ E("p", {
182
+ className: "mb-2 text-xs font-semibold tracking-wide text-wx-fg-muted uppercase",
183
+ children: I("relatedTitle")
184
+ }), /* @__PURE__ */ E("ul", {
185
+ className: "flex flex-col gap-1.5",
186
+ children: $.map((e) => /* @__PURE__ */ E("li", { children: /* @__PURE__ */ D("button", {
187
+ type: "button",
188
+ onClick: () => N?.(e.slug),
189
+ className: "flex w-full items-start 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",
190
+ children: [/* @__PURE__ */ E("span", {
191
+ className: "aspect-square w-10 shrink-0 rounded-wx-sm bg-cover bg-center",
192
+ style: e.coverImageUrl?.url ? { backgroundImage: `url(${e.coverImageUrl.url})` } : { background: "linear-gradient(135deg, #1e1f21, #121314)" },
193
+ "aria-hidden": "true"
194
+ }), /* @__PURE__ */ D("span", {
195
+ className: "min-w-0 flex-1",
196
+ children: [/* @__PURE__ */ E("span", {
197
+ className: "block truncate text-sm font-medium text-wx-fg",
198
+ children: e.title
199
+ }), e.excerpt && /* @__PURE__ */ E("span", {
200
+ className: "line-clamp-1 block text-xs text-wx-fg-muted",
201
+ children: e.excerpt
202
+ })]
203
+ })]
204
+ }) }, e._id))
205
+ })]
206
+ })
207
+ ]
208
+ })
209
+ }) : /* @__PURE__ */ E(A, {});
210
+ }
211
+ function A() {
212
+ return /* @__PURE__ */ E(i, {
213
+ className: "flex-1",
214
+ children: /* @__PURE__ */ D("div", {
215
+ className: "flex flex-col px-5 pt-5 pb-12",
216
+ "aria-busy": "true",
217
+ "aria-live": "polite",
218
+ children: [
219
+ /* @__PURE__ */ D("div", {
220
+ className: "animate-pulse space-y-2.5",
221
+ children: [/* @__PURE__ */ E("div", { className: "h-5 w-3/4 rounded-md bg-wx-bg-elevated" }), /* @__PURE__ */ E("div", { className: "h-5 w-1/2 rounded-md bg-wx-bg-elevated" })]
222
+ }),
223
+ /* @__PURE__ */ D("div", {
224
+ className: "mt-4 flex items-center gap-2 animate-pulse",
225
+ children: [/* @__PURE__ */ E("div", { className: "h-5 w-5 rounded-full bg-wx-bg-elevated" }), /* @__PURE__ */ E("div", { className: "h-3 w-32 rounded-md bg-wx-bg-elevated" })]
226
+ }),
227
+ /* @__PURE__ */ E("div", {
228
+ className: "mt-6 space-y-2.5 animate-pulse",
229
+ children: [
230
+ "w-full",
231
+ "w-11/12",
232
+ "w-10/12",
233
+ "w-full",
234
+ "w-9/12",
235
+ "w-full",
236
+ "w-11/12",
237
+ "w-7/12"
238
+ ].map((e, t) => /* @__PURE__ */ E("div", { className: S("h-3 rounded-md bg-wx-bg-elevated", e) }, t))
239
+ })
240
+ ]
241
+ })
242
+ });
243
+ }
244
+ //#endregion
245
+ export { k as HelpArticleView };
246
+
247
+ //# sourceMappingURL=widget-react-CAWxn8I0.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"widget-react-CAWxn8I0.js","names":[],"sources":["../components/widget/help-article-view/helpers.ts","../components/widget/help-article-view/index.tsx"],"sourcesContent":["import { HelpArticleContentFormat } from \"@/lib/graphql/generated/types\";\n\n/**\n * Project a raw dashboard-preview row (operator-side `HelpArticle`) into\n * the visitor-shaped projection the article view consumes. Defensive on\n * every field — the preview row is loosely-typed operator JSON.\n */\nexport function fromPreview(raw: Record<string, unknown>) {\n const idsOf = (arr: unknown): unknown[] => (Array.isArray(arr) ? arr : []);\n return {\n id: String(raw._id ?? \"\"),\n title: typeof raw.title === \"string\" ? raw.title : \"\",\n excerpt: typeof raw.excerpt === \"string\" ? raw.excerpt : \"\",\n content: typeof raw.content === \"string\" ? raw.content : \"\",\n contentFormat:\n raw.contentFormat === HelpArticleContentFormat.HTML\n ? HelpArticleContentFormat.HTML\n : HelpArticleContentFormat.MARKDOWN,\n authors: idsOf(raw.authors)\n .filter((a): a is Record<string, unknown> => !!a && typeof a === \"object\")\n .map((a) => ({\n _id: String(a._id ?? \"\"),\n name: typeof a.name === \"string\" ? a.name : null,\n photo: a.photo as { url: string } | null | undefined,\n })),\n tags: idsOf(raw.tags)\n .filter((tg): tg is Record<string, unknown> => !!tg && typeof tg === \"object\")\n .map((tg) => ({\n _id: String(tg._id ?? \"\"),\n slug: typeof tg.slug === \"string\" ? tg.slug : \"\",\n label: typeof tg.label === \"string\" ? tg.label : \"\",\n })),\n };\n}\n","\"use client\";\n\nimport { useTranslations } from \"next-intl\";\nimport { useEffect, useMemo, useState } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Card, CardBody } from \"@/components/ui/card\";\nimport { ScrollArea } from \"@/components/ui/scroll-area\";\nimport {\n MessageSquareIcon,\n type MessageSquareIconHandle,\n} from \"@/components/widget/icons/animated\";\nimport { getMockHelpArticles } from \"@/dummy\";\nimport { getVisitorToken } from \"@/lib/apollo-client\";\nimport { looksLikeHtml } from \"@/lib/detect-html\";\nimport { HelpArticleContentFormat, ReactionSurface } from \"@/lib/graphql/generated/types\";\nimport {\n useRecordVisitorHelpViewMutation,\n useVisitorHelpArticleQuery,\n useVisitorHelpRelatedQuery,\n} from \"@/lib/graphql/queries/generated/help.generated\";\nimport { useArticleTitleSink } from \"@/lib/use-article-title-sink\";\nimport { DEMO_LOCALES, useDemoTranslator } from \"@/lib/use-demo-translator\";\nimport { useIconHover } from \"@/lib/use-icon-hover\";\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 { fromPreview } from \"./helpers\";\n\ninterface HelpArticleViewProps {\n slug: string;\n locale: string;\n isDummy: boolean;\n onHandoff: () => void;\n /** Open another article by slug — used by the related-articles\n * rail. Shell wires this to the same route swap the help-tab\n * search results use, so back-nav lands on the index. */\n onOpenArticle?: (slug: string) => void;\n /** Open a locale variant of THIS article — used by the below-article\n * language selector. Distinct from `onOpenArticle` because the\n * visitor lookup is keyed on `slug` + `locale`, so the target\n * locale must travel with the slug. */\n onOpenTranslation?: (slug: string, locale: 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 \"Help\". */\n onTitleResolved?: (title: string | null) => void;\n}\n\n/**\n * Article detail. Markdown body via `react-markdown` + `remark-gfm`,\n * view counter, unresolved handoff, feedback bar.\n *\n * Order matters (Day 6 feedback): **CTA first, feedback below**.\n * The visitor's primary next-step after reading is either \"this\n * answered my question\" (close) or \"I need more help\" — the CTA\n * leads them straight to the messenger. Feedback is a secondary,\n * passive ask underneath.\n *\n * Back button is rendered by the shell's sticky header.\n */\nexport function HelpArticleView({\n slug,\n locale,\n isDummy,\n onHandoff,\n onOpenArticle,\n onOpenTranslation,\n onTitleResolved,\n}: HelpArticleViewProps) {\n const t = useTranslations(\"help\");\n const tDemo = useTranslations(\"demo\");\n const env = useWidgetEnv();\n const { config } = useWidgetConfig();\n const showRelated = config.messenger?.showRelatedHelpArticles ?? true;\n const showReactionCounts = config.messenger?.showReactionCounts ?? true;\n const ctaHover = useIconHover<MessageSquareIconHandle>();\n\n // Article-scoped language switch in demo mode. Defaults to the widget\n // locale; the below-article dropdown changes it WITHOUT touching the\n // widget's own locale — it just re-renders this article from the\n // chosen locale's demo bundle.\n const [demoLocale, setDemoLocale] = useState(locale);\n const demoT = useDemoTranslator(demoLocale, tDemo);\n\n // Preview-mode short-circuit — look up by slug in the dashboard's\n // prefetched articles. Bypasses the visitor query entirely so the\n // preview pane doesn't need dashboard-origin authorization.\n const previewArticle = useMemo(() => {\n if (isDummy) return null;\n const list = env.previewData?.helpArticles;\n if (!Array.isArray(list)) return null;\n return (\n (list.find((a) => a && typeof a === \"object\" && (a as { slug?: unknown }).slug === slug) as\n | Record<string, unknown>\n | undefined) ?? null\n );\n }, [env.previewData, isDummy, slug]);\n const hasPreview = !!previewArticle;\n\n const { data, loading } = useVisitorHelpArticleQuery({\n variables: { args: { slug, locale } },\n skip: isDummy || hasPreview,\n });\n\n const [recordView] = useRecordVisitorHelpViewMutation();\n\n const mockArticle = useMemo(\n () => (isDummy ? getMockHelpArticles(demoT).find((a) => a.slug === slug) : null),\n [isDummy, slug, demoT],\n );\n\n const article = isDummy\n ? mockArticle\n ? {\n id: mockArticle.id,\n title: mockArticle.title,\n excerpt: mockArticle.excerpt,\n content: mockArticle.contentMarkdown,\n contentFormat: HelpArticleContentFormat.MARKDOWN,\n authors: mockArticle.authors as {\n _id: string;\n name: string | null;\n photo?: { url: string } | null;\n }[],\n tags: mockArticle.tags,\n reactionCounts: null as Record<string, number> | null,\n // Demo: the article is shown in the currently-selected demo\n // locale; the switcher's options come from `DEMO_LOCALES`.\n locale: demoLocale,\n translations: [] as { locale: string; slug: string }[],\n }\n : null\n : hasPreview && previewArticle\n ? {\n ...fromPreview(previewArticle),\n reactionCounts:\n (previewArticle.reactionCounts as Record<string, number> | null | undefined) ?? null,\n // Dashboard preview loads one article — no translation set.\n locale,\n translations: [] as { locale: string; slug: string }[],\n }\n : data?.visitorHelpArticle\n ? {\n id: data.visitorHelpArticle._id,\n title: data.visitorHelpArticle.title,\n excerpt: data.visitorHelpArticle.excerpt ?? \"\",\n content: data.visitorHelpArticle.content,\n contentFormat: data.visitorHelpArticle.contentFormat,\n authors: data.visitorHelpArticle.authors,\n tags: data.visitorHelpArticle.tags,\n reactionCounts:\n (data.visitorHelpArticle.reactionCounts as\n | Record<string, number>\n | null\n | undefined) ?? null,\n locale: data.visitorHelpArticle.locale,\n translations: data.visitorHelpArticle.translations,\n }\n : null;\n\n // Related articles — BE-side overlap scoring (shared folder + tags\n // + ancestors). Dummy mode renders nothing here; visitors on\n // production sites get the rail under the body.\n const { data: relatedData } = useVisitorHelpRelatedQuery({\n // Active widget locale (not the article's) so related follows the\n // reader's language even on a fallback (default-locale) article.\n variables: { articleId: article?.id ?? \"\", limit: 5, locale },\n skip: isDummy || hasPreview || !article?.id || !showRelated,\n });\n const related = relatedData?.visitorHelpRelated ?? [];\n\n // Visitor's own reaction slotIndex (BE `viewerReaction`) — authoritative\n // highlight for the reaction bar. Only the real read carries it.\n const viewerReaction =\n isDummy || hasPreview ? null : (data?.visitorHelpArticle?.viewerReaction ?? null);\n // All locale variants of this article (self + translations) — a reaction\n // is per-group, so the reaction bar updates every variant's cache.\n const groupItemIds =\n !isDummy && !hasPreview && data?.visitorHelpArticle\n ? [\n data.visitorHelpArticle._id,\n ...(data.visitorHelpArticle.translations ?? []).map((tr) => tr._id),\n ]\n : article?.id\n ? [article.id]\n : [];\n\n useEffect(() => {\n // View tracking is best-effort + visitor-JWT-guarded. Skip it when\n // there's no session token (e.g. the `requireAuth` gate's empty\n // anonymous session) — firing it would 401 and trip the 401-recovery\n // into a needless re-handshake on every article open.\n if (isDummy || !article?.id || !getVisitorToken()) return;\n recordView({ variables: { articleId: article.id } }).catch(() => {});\n }, [article?.id, isDummy, recordView]);\n\n // Surface the title up to the widget shell's sticky header (swaps\n // \"Help\" → article name). See `useArticleTitleSink` for why the\n // mount/unmount ownership is the single source of truth. Called\n // before the loading/not-found early returns so the hook order is\n // stable across renders — it's a no-op while `article` is undefined.\n useArticleTitleSink(article?.title, onTitleResolved);\n\n // Help-side feedback now flows through the unified reactions\n // surface (`submitVisitorReaction`), wired by the reaction-bar\n // component below. The \"I still need help\" handoff stays — it's a\n // navigation action, not a vote — but no longer records an\n // unresolved event (removed from BE).\n const handleHandoff = () => onHandoff();\n\n if (!loading && !article) {\n return (\n <div className=\"flex flex-1 flex-col items-center justify-center px-6 py-12\">\n <Card>\n <CardBody className=\"py-8 text-center\">\n <p className=\"text-sm font-semibold text-wx-fg\">{t(\"articleNotFound\")}</p>\n </CardBody>\n </Card>\n </div>\n );\n }\n\n if (!article) {\n // Article-shaped skeleton — title block + author meta line +\n // a handful of body lines. Mirrors the real article's spacing\n // (px-5 pt-5) so the layout doesn't jump when content arrives.\n // The `animate-pulse` Tailwind utility runs purely on opacity\n // (no transform, no layout), which keeps the loader cheap and\n // doesn't compete with the shell's route-slide animation.\n return <HelpArticleSkeleton />;\n }\n\n return (\n <ScrollArea className=\"flex-1\">\n {/* Plain `<div>` — the shell's route slide already covers the\n enter animation for this view, and stacking a second\n opacity+translate keyframe on top thrashed the main thread\n during article navigation (the panel was also animating\n its width, the route was sliding, and *then* the body was\n fading — three concurrent timelines). The route slide is\n enough; the article reveal feels snappier without the\n extra Motion subscription. */}\n <div className=\"flex flex-col px-5 pt-5 pb-12\">\n <h2 className=\"text-xl leading-tight font-bold text-wx-fg\">{article.title}</h2>\n {article.authors.length > 0 && (\n <p className=\"mt-2 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 {a.photo?.url ? (\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 ) : (\n <span className=\"flex h-full w-full items-center justify-center text-[10px] font-semibold uppercase text-wx-fg-muted\">\n {(a.name ?? \"?\").charAt(0)}\n </span>\n )}\n </span>\n ))}\n </span>\n <span>\n {t(\"writtenBy\", {\n names: article.authors\n .map((a) => a.name ?? \"\")\n .filter(Boolean)\n .join(\", \"),\n })}\n </span>\n </p>\n )}\n {article.tags.length > 0 && (\n <div className=\"mt-2 flex flex-wrap gap-1\">\n {article.tags.map((tag) => (\n <span\n key={tag._id}\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 && <p className=\"mt-2 mb-5 text-sm text-wx-fg-muted\">{article.excerpt}</p>}\n <ArticleBody\n content={article.content}\n // BE-stamped `contentFormat` is the authoritative signal,\n // but TipTap-authored articles historically save as MARKDOWN\n // (the BE default) with HTML content. Fall back to shape\n // detection so those still render as rich HTML rather than\n // escaped tags.\n isHtml={\n article.contentFormat === HelpArticleContentFormat.HTML ||\n looksLikeHtml(article.content)\n }\n markdownMarginClass=\"mt-3\"\n />\n\n {/* CTA first — primary next-step affordance. `shrink-0` +\n `min-h-12` because the article view is a flex-col with\n overflow-y-auto; without an explicit floor the lg size's\n `h-12` was getting compressed by flex-shrink as the prose\n + feedback bar pushed against it. The icon animates on\n the button's hover via `useIconHover`. */}\n <Button\n type=\"button\"\n variant=\"solid\"\n size=\"lg\"\n className=\"mt-6 w-full shrink-0 min-h-12\"\n onClick={handleHandoff}\n {...ctaHover.parentHandlers}\n >\n <MessageSquareIcon ref={ctaHover.iconRef} size={16} />\n <span>{t(\"unresolvedCta\")}</span>\n </Button>\n\n {/* Reactions bar — operator-curated emoji palette via the\n new unified reactions surface. Defaults seed to 👍 /\n 👎 with sentiment tags so the \"Was this helpful?\" UX\n remains out-of-the-box; operators can edit the palette\n in their Help reactions settings. */}\n {article.id && (\n <ReactionBar\n surface={ReactionSurface.HELP}\n itemId={article.id}\n reactionCounts={article.reactionCounts}\n viewerReaction={viewerReaction}\n groupItemIds={groupItemIds}\n isDummy={isDummy}\n showCounts={showReactionCounts}\n className=\"mt-4\"\n />\n )}\n\n {/* Language switcher — published locale variants of this\n article. In demo mode it switches the article content in\n place (article-scoped, no widget locale change); in live mode\n it opens the chosen translation by slug + locale. Hidden when\n only one language exists. */}\n <LanguageSelector\n currentLocale={isDummy ? demoLocale : article.locale}\n locales={isDummy ? [...DEMO_LOCALES] : article.translations.map((tr) => tr.locale)}\n onSelect={\n isDummy\n ? (loc) => setDemoLocale(loc)\n : (loc) => {\n const target = article.translations.find((tr) => tr.locale === loc);\n if (target) onOpenTranslation?.(target.slug, target.locale);\n }\n }\n className=\"mt-6\"\n />\n\n {showRelated && related.length > 0 && (\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-1.5\">\n {related.map((r) => (\n <li key={r._id}>\n <button\n type=\"button\"\n onClick={() => onOpenArticle?.(r.slug)}\n className=\"flex w-full items-start 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-10 shrink-0 rounded-wx-sm bg-cover bg-center\"\n style={\n r.coverImageUrl?.url\n ? { backgroundImage: `url(${r.coverImageUrl.url})` }\n : {\n background: \"linear-gradient(135deg, #1e1f21, #121314)\",\n }\n }\n aria-hidden=\"true\"\n />\n <span className=\"min-w-0 flex-1\">\n <span className=\"block truncate text-sm font-medium text-wx-fg\">\n {r.title}\n </span>\n {r.excerpt && (\n <span className=\"line-clamp-1 block text-xs text-wx-fg-muted\">\n {r.excerpt}\n </span>\n )}\n </span>\n </button>\n </li>\n ))}\n </ul>\n </section>\n )}\n </div>\n </ScrollArea>\n );\n}\n\n/**\n * Loading state — article-shaped skeleton blocks. The earlier\n * placeholder was a literal `…` glyph, which looked broken next to\n * the polished article body that arrives 200-500 ms later. This\n * version paints a 2-line title, an author-meta strip, and a small\n * stack of body lines using `animate-pulse` (opacity-only, no\n * layout work) so the loader stays cheap and visually previews\n * what's about to show up.\n */\nfunction HelpArticleSkeleton() {\n return (\n <ScrollArea className=\"flex-1\">\n <div className=\"flex flex-col px-5 pt-5 pb-12\" aria-busy=\"true\" aria-live=\"polite\">\n {/* Title — two lines of varying width feels more \"title-ish\"\n than one tall block, and previews real article geometry. */}\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 {/* Author meta */}\n <div className=\"mt-4 flex items-center gap-2 animate-pulse\">\n <div className=\"h-5 w-5 rounded-full bg-wx-bg-elevated\" />\n <div className=\"h-3 w-32 rounded-md bg-wx-bg-elevated\" />\n </div>\n {/* Body — eight thin lines of decaying width simulate\n paragraphs of varying length. */}\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-full\", \"w-11/12\", \"w-7/12\"].map(\n (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 )}\n </div>\n </div>\n </ScrollArea>\n );\n}\n"],"mappings":";;;;;;;;;;;;;AAOA,SAAgB,EAAY,GAA8B;CACxD,IAAM,KAAS,MAA6B,MAAM,QAAQ,EAAI,GAAG,IAAM,EAAE;CACzE,OAAO;EACL,IAAI,OAAO,EAAI,OAAO,GAAG;EACzB,OAAO,OAAO,EAAI,SAAU,WAAW,EAAI,QAAQ;EACnD,SAAS,OAAO,EAAI,WAAY,WAAW,EAAI,UAAU;EACzD,SAAS,OAAO,EAAI,WAAY,WAAW,EAAI,UAAU;EACzD,eACE,EAAI,kBAAkB,EAAyB,OAC3C,EAAyB,OACzB,EAAyB;EAC/B,SAAS,EAAM,EAAI,QAAQ,CACxB,QAAQ,MAAoC,CAAC,CAAC,KAAK,OAAO,KAAM,SAAS,CACzE,KAAK,OAAO;GACX,KAAK,OAAO,EAAE,OAAO,GAAG;GACxB,MAAM,OAAO,EAAE,QAAS,WAAW,EAAE,OAAO;GAC5C,OAAO,EAAE;GACV,EAAE;EACL,MAAM,EAAM,EAAI,KAAK,CAClB,QAAQ,MAAsC,CAAC,CAAC,KAAM,OAAO,KAAO,SAAS,CAC7E,KAAK,OAAQ;GACZ,KAAK,OAAO,EAAG,OAAO,GAAG;GACzB,MAAM,OAAO,EAAG,QAAS,WAAW,EAAG,OAAO;GAC9C,OAAO,OAAO,EAAG,SAAU,WAAW,EAAG,QAAQ;GAClD,EAAE;EACN;;;;AC+BH,SAAgB,EAAgB,EAC9B,SACA,WACA,YACA,cACA,kBACA,sBACA,sBACuB;CACvB,IAAM,IAAI,EAAgB,OAAO,EAC3B,IAAQ,EAAgB,OAAO,EAC/B,IAAM,GAAc,EACpB,EAAE,cAAW,GAAiB,EAC9B,IAAc,EAAO,WAAW,2BAA2B,IAC3D,IAAqB,EAAO,WAAW,sBAAsB,IAC7D,IAAW,GAAuC,EAMlD,CAAC,GAAY,KAAiB,GAAS,EAAO,EAC9C,IAAQ,EAAkB,GAAY,EAAM,EAK5C,IAAiB,QAAc;EACnC,IAAI,GAAS,OAAO;EACpB,IAAM,IAAO,EAAI,aAAa;EAE9B,OADK,MAAM,QAAQ,EAAK,GAErB,EAAK,MAAM,MAAM,KAAK,OAAO,KAAM,YAAa,EAAyB,SAAS,EAAK,IAEtE,OAJa;IAMhC;EAAC,EAAI;EAAa;EAAS;EAAK,CAAC,EAC9B,IAAa,CAAC,CAAC,GAEf,EAAE,SAAM,eAAY,EAA2B;EACnD,WAAW,EAAE,MAAM;GAAE;GAAM;GAAQ,EAAE;EACrC,MAAM,KAAW;EAClB,CAAC,EAEI,CAAC,KAAc,GAAkC,EAEjD,IAAc,QACX,IAAU,EAAoB,EAAM,CAAC,MAAM,MAAM,EAAE,SAAS,EAAK,GAAG,MAC3E;EAAC;EAAS;EAAM;EAAM,CACvB,EAEK,IAAU,IACZ,IACE;EACE,IAAI,EAAY;EAChB,OAAO,EAAY;EACnB,SAAS,EAAY;EACrB,SAAS,EAAY;EACrB,eAAe,EAAyB;EACxC,SAAS,EAAY;EAKrB,MAAM,EAAY;EAClB,gBAAgB;EAGhB,QAAQ;EACR,cAAc,EAAE;EACjB,GACD,OACF,KAAc,IACZ;EACE,GAAG,EAAY,EAAe;EAC9B,gBACG,EAAe,kBAAgE;EAElF;EACA,cAAc,EAAE;EACjB,GACD,GAAM,qBACJ;EACE,IAAI,EAAK,mBAAmB;EAC5B,OAAO,EAAK,mBAAmB;EAC/B,SAAS,EAAK,mBAAmB,WAAW;EAC5C,SAAS,EAAK,mBAAmB;EACjC,eAAe,EAAK,mBAAmB;EACvC,SAAS,EAAK,mBAAmB;EACjC,MAAM,EAAK,mBAAmB;EAC9B,gBACG,EAAK,mBAAmB,kBAGP;EACpB,QAAQ,EAAK,mBAAmB;EAChC,cAAc,EAAK,mBAAmB;EACvC,GACD,MAKF,EAAE,MAAM,OAAgB,EAA2B;EAGvD,WAAW;GAAE,WAAW,GAAS,MAAM;GAAI,OAAO;GAAG;GAAQ;EAC7D,MAAM,KAAW,KAAc,CAAC,GAAS,MAAM,CAAC;EACjD,CAAC,EACI,IAAU,IAAa,sBAAsB,EAAE,EAI/C,KACJ,KAAW,IAAa,OAAQ,GAAM,oBAAoB,kBAAkB,MAGxE,KACJ,CAAC,KAAW,CAAC,KAAc,GAAM,qBAC7B,CACE,EAAK,mBAAmB,KACxB,IAAI,EAAK,mBAAmB,gBAAgB,EAAE,EAAE,KAAK,MAAO,EAAG,IAAI,CACpE,GACD,GAAS,KACP,CAAC,EAAQ,GAAG,GACZ,EAAE;CA+CV,OA7CA,QAAgB;EAKV,KAAW,CAAC,GAAS,MAAM,CAAC,GAAiB,IACjD,EAAW,EAAE,WAAW,EAAE,WAAW,EAAQ,IAAI,EAAE,CAAC,CAAC,YAAY,GAAG;IACnE;EAAC,GAAS;EAAI;EAAS;EAAW,CAAC,EAOtC,EAAoB,GAAS,OAAO,EAAgB,EAShD,CAAC,KAAW,CAAC,IAEb,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD;GAAU,WAAU;aAClB,kBAAC,KAAD;IAAG,WAAU;cAAoC,EAAE,kBAAkB;IAAK,CAAA;GACjE,CAAA,EACN,CAAA;EACH,CAAA,GAIL,IAWH,kBAAC,GAAD;EAAY,WAAU;YASpB,kBAAC,OAAD;GAAK,WAAU;aAAf;IACE,kBAAC,MAAD;KAAI,WAAU;eAA8C,EAAQ;KAAW,CAAA;IAC9E,EAAQ,QAAQ,SAAS,KACxB,kBAAC,KAAD;KAAG,WAAU;eAAb,CACE,kBAAC,QAAD;MAAM,WAAU;gBACb,EAAQ,QAAQ,MAAM,GAAG,EAAE,CAAC,KAAK,MAChC,kBAAC,QAAD;OAEE,WAAU;OACV,eAAY;iBAEX,EAAE,OAAO,MAER,kBAAC,OAAD;QAAK,KAAK,EAAE,MAAM;QAAK,KAAI;QAAG,WAAU;QAA+B,CAAA,GAEvE,kBAAC,QAAD;QAAM,WAAU;mBACZ,EAAE,QAAQ,KAAK,OAAO,EAAE;QACrB,CAAA;OAEJ,EAZA,EAAE,IAYF,CACP;MACG,CAAA,EACP,kBAAC,QAAD,EAAA,UACG,EAAE,aAAa,EACd,OAAO,EAAQ,QACZ,KAAK,MAAM,EAAE,QAAQ,GAAG,CACxB,OAAO,QAAQ,CACf,KAAK,KAAK,EACd,CAAC,EACG,CAAA,CACL;;IAEL,EAAQ,KAAK,SAAS,KACrB,kBAAC,OAAD;KAAK,WAAU;eACZ,EAAQ,KAAK,KAAK,MACjB,kBAAC,QAAD;MAEE,WAAU;gBAET,EAAI;MACA,EAJA,EAAI,IAIJ,CACP;KACE,CAAA;IAEP,EAAQ,WAAW,kBAAC,KAAD;KAAG,WAAU;eAAsC,EAAQ;KAAY,CAAA;IAC3F,kBAAC,GAAD;KACE,SAAS,EAAQ;KAMjB,QACE,EAAQ,kBAAkB,EAAyB,QACnD,EAAc,EAAQ,QAAQ;KAEhC,qBAAoB;KACpB,CAAA;IAQF,kBAAC,GAAD;KACE,MAAK;KACL,SAAQ;KACR,MAAK;KACL,WAAU;KACV,eAxGoB,GAAW;KAyG/B,GAAI,EAAS;eANf,CAQE,kBAAC,GAAD;MAAmB,KAAK,EAAS;MAAS,MAAM;MAAM,CAAA,EACtD,kBAAC,QAAD,EAAA,UAAO,EAAE,gBAAgB,EAAQ,CAAA,CAC1B;;IAOR,EAAQ,MACP,kBAAC,GAAD;KACE,SAAS,EAAgB;KACzB,QAAQ,EAAQ;KAChB,gBAAgB,EAAQ;KACR;KACF;KACL;KACT,YAAY;KACZ,WAAU;KACV,CAAA;IAQJ,kBAAC,GAAD;KACE,eAAe,IAAU,IAAa,EAAQ;KAC9C,SAAS,IAAU,CAAC,GAAG,EAAa,GAAG,EAAQ,aAAa,KAAK,MAAO,EAAG,OAAO;KAClF,UACE,KACK,MAAQ,EAAc,EAAI,IAC1B,MAAQ;MACP,IAAM,IAAS,EAAQ,aAAa,MAAM,MAAO,EAAG,WAAW,EAAI;MACnE,AAAI,KAAQ,IAAoB,EAAO,MAAM,EAAO,OAAO;;KAGnE,WAAU;KACV,CAAA;IAED,KAAe,EAAQ,SAAS,KAC/B,kBAAC,WAAD;KAAS,WAAU;eAAnB,CACE,kBAAC,KAAD;MAAG,WAAU;gBACV,EAAE,eAAe;MAChB,CAAA,EACJ,kBAAC,MAAD;MAAI,WAAU;gBACX,EAAQ,KAAK,MACZ,kBAAC,MAAD,EAAA,UACE,kBAAC,UAAD;OACE,MAAK;OACL,eAAe,IAAgB,EAAE,KAAK;OACtC,WAAU;iBAHZ,CAKE,kBAAC,QAAD;QACE,WAAU;QACV,OACE,EAAE,eAAe,MACb,EAAE,iBAAiB,OAAO,EAAE,cAAc,IAAI,IAAI,GAClD,EACE,YAAY,6CACb;QAEP,eAAY;QACZ,CAAA,EACF,kBAAC,QAAD;QAAM,WAAU;kBAAhB,CACE,kBAAC,QAAD;SAAM,WAAU;mBACb,EAAE;SACE,CAAA,EACN,EAAE,WACD,kBAAC,QAAD;SAAM,WAAU;mBACb,EAAE;SACE,CAAA,CAEJ;UACA;UACN,EA5BI,EAAE,IA4BN,CACL;MACC,CAAA,CACG;;IAER;;EACK,CAAA,GAxKN,kBAAC,GAAD,EAAuB,CAAA;;AAqLlC,SAAS,IAAsB;CAC7B,OACE,kBAAC,GAAD;EAAY,WAAU;YACpB,kBAAC,OAAD;GAAK,WAAU;GAAgC,aAAU;GAAO,aAAU;aAA1E;IAGE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,OAAD,EAAK,WAAU,0CAA2C,CAAA,EAC1D,kBAAC,OAAD,EAAK,WAAU,0CAA2C,CAAA,CACtD;;IAEN,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,OAAD,EAAK,WAAU,0CAA2C,CAAA,EAC1D,kBAAC,OAAD,EAAK,WAAU,yCAA0C,CAAA,CACrD;;IAGN,kBAAC,OAAD;KAAK,WAAU;eACZ;MAAC;MAAU;MAAW;MAAW;MAAU;MAAU;MAAU;MAAW;MAAS,CAAC,KAClF,GAAG,MAEF,kBAAC,OAAD,EAAa,WAAW,EAAG,oCAAoC,EAAE,EAAI,EAA3D,EAA2D,CAExE;KACG,CAAA;IACF;;EACK,CAAA"}