@flamingo-stack/openframe-frontend-core 0.0.266 → 0.0.267

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 (85) hide show
  1. package/dist/{chunk-G5GQVAPI.cjs → chunk-5QJQAFTP.cjs} +126 -57
  2. package/dist/chunk-5QJQAFTP.cjs.map +1 -0
  3. package/dist/chunk-722TTRIJ.cjs +29 -0
  4. package/dist/chunk-722TTRIJ.cjs.map +1 -0
  5. package/dist/{chunk-QXRXIHY6.cjs → chunk-AA77CC2Y.cjs} +12 -12
  6. package/dist/{chunk-QXRXIHY6.cjs.map → chunk-AA77CC2Y.cjs.map} +1 -1
  7. package/dist/{chunk-WB2JXKEI.cjs → chunk-CI5XE4LB.cjs} +37 -37
  8. package/dist/{chunk-WB2JXKEI.cjs.map → chunk-CI5XE4LB.cjs.map} +1 -1
  9. package/dist/{chunk-UUARMJJE.cjs → chunk-CICK3FUU.cjs} +100 -5
  10. package/dist/chunk-CICK3FUU.cjs.map +1 -0
  11. package/dist/{chunk-K4K4X62E.js → chunk-CYMMGPPV.js} +125 -56
  12. package/dist/chunk-CYMMGPPV.js.map +1 -0
  13. package/dist/{chunk-FRU4LQJ4.js → chunk-GJVSLS2R.js} +5 -7
  14. package/dist/{chunk-FRU4LQJ4.js.map → chunk-GJVSLS2R.js.map} +1 -1
  15. package/dist/{chunk-HTR3ARN6.js → chunk-ICOMPUMX.js} +343 -248
  16. package/dist/chunk-ICOMPUMX.js.map +1 -0
  17. package/dist/{chunk-DTJ32U7J.js → chunk-J6O57C7S.js} +8 -72
  18. package/dist/chunk-J6O57C7S.js.map +1 -0
  19. package/dist/{chunk-QAQ7AROS.cjs → chunk-O6UA475M.cjs} +302 -366
  20. package/dist/chunk-O6UA475M.cjs.map +1 -0
  21. package/dist/chunk-OO4ZGDRO.js +29 -0
  22. package/dist/chunk-OO4ZGDRO.js.map +1 -0
  23. package/dist/{chunk-TX2VP43R.js → chunk-P7JHUX4X.js} +2 -2
  24. package/dist/{chunk-NR535XIY.js → chunk-T2URJAX2.js} +66 -1
  25. package/dist/chunk-T2URJAX2.js.map +1 -0
  26. package/dist/{chunk-VYGS24RO.cjs → chunk-TPZI7NBN.cjs} +16 -18
  27. package/dist/chunk-TPZI7NBN.cjs.map +1 -0
  28. package/dist/{chunk-SRM7HE2A.js → chunk-XSK72QRR.js} +3 -3
  29. package/dist/{chunk-7VMEOFEA.cjs → chunk-Y2B6WDU6.cjs} +67 -2
  30. package/dist/chunk-Y2B6WDU6.cjs.map +1 -0
  31. package/dist/{chunk-Q52JZP3P.cjs → chunk-YV55JZAD.cjs} +27 -27
  32. package/dist/{chunk-Q52JZP3P.cjs.map → chunk-YV55JZAD.cjs.map} +1 -1
  33. package/dist/{chunk-KUYYJ2XM.js → chunk-Z6MKG53Z.js} +4 -4
  34. package/dist/components/chat/index.cjs +5 -5
  35. package/dist/components/chat/index.js +4 -4
  36. package/dist/components/contact/index.cjs +6 -6
  37. package/dist/components/contact/index.js +5 -5
  38. package/dist/components/faq/faq-section.d.ts +20 -23
  39. package/dist/components/faq/faq-section.d.ts.map +1 -1
  40. package/dist/components/faq/index.cjs +4 -4
  41. package/dist/components/faq/index.js +3 -3
  42. package/dist/components/faq-accordion.d.ts +0 -5
  43. package/dist/components/faq-accordion.d.ts.map +1 -1
  44. package/dist/components/features/index.cjs +7 -8
  45. package/dist/components/features/index.cjs.map +1 -1
  46. package/dist/components/features/index.js +6 -7
  47. package/dist/components/index.cjs +84 -84
  48. package/dist/components/index.cjs.map +1 -1
  49. package/dist/components/index.js +13 -13
  50. package/dist/components/navigation/index.cjs +5 -5
  51. package/dist/components/navigation/index.js +4 -4
  52. package/dist/components/onboarding-guides/index.cjs +24 -25
  53. package/dist/components/onboarding-guides/index.cjs.map +1 -1
  54. package/dist/components/onboarding-guides/index.js +4 -5
  55. package/dist/components/onboarding-guides/index.js.map +1 -1
  56. package/dist/components/related-content/index.cjs +6 -6
  57. package/dist/components/related-content/index.js +5 -5
  58. package/dist/components/tickets/index.cjs +69 -69
  59. package/dist/components/tickets/index.cjs.map +1 -1
  60. package/dist/components/tickets/index.js +10 -10
  61. package/dist/components/ui/index.cjs +7 -7
  62. package/dist/components/ui/index.cjs.map +1 -1
  63. package/dist/components/ui/index.js +7 -7
  64. package/dist/index.cjs +10 -10
  65. package/dist/index.cjs.map +1 -1
  66. package/dist/index.js +10 -10
  67. package/package.json +1 -1
  68. package/src/components/faq/faq-section.tsx +212 -104
  69. package/src/components/faq-accordion.tsx +1 -15
  70. package/dist/chunk-7VMEOFEA.cjs.map +0 -1
  71. package/dist/chunk-DTJ32U7J.js.map +0 -1
  72. package/dist/chunk-FREG2KXV.js +0 -128
  73. package/dist/chunk-FREG2KXV.js.map +0 -1
  74. package/dist/chunk-G5GQVAPI.cjs.map +0 -1
  75. package/dist/chunk-HTR3ARN6.js.map +0 -1
  76. package/dist/chunk-JMU4RU5J.cjs +0 -128
  77. package/dist/chunk-JMU4RU5J.cjs.map +0 -1
  78. package/dist/chunk-K4K4X62E.js.map +0 -1
  79. package/dist/chunk-NR535XIY.js.map +0 -1
  80. package/dist/chunk-QAQ7AROS.cjs.map +0 -1
  81. package/dist/chunk-UUARMJJE.cjs.map +0 -1
  82. package/dist/chunk-VYGS24RO.cjs.map +0 -1
  83. /package/dist/{chunk-TX2VP43R.js.map → chunk-P7JHUX4X.js.map} +0 -0
  84. /package/dist/{chunk-SRM7HE2A.js.map → chunk-XSK72QRR.js.map} +0 -0
  85. /package/dist/{chunk-KUYYJ2XM.js.map → chunk-Z6MKG53Z.js.map} +0 -0
@@ -1,13 +1,15 @@
1
1
  "use client"
2
2
 
3
- import React, { useMemo } from 'react'
3
+ import React, { useCallback, useEffect, useMemo, useState } from 'react'
4
4
  import type { Faq } from '../../types/faq'
5
5
  import { FaqAccordion, type FaqItem } from '../faq-accordion'
6
6
  import { useSelfFetch } from '../../hooks/use-self-fetch'
7
7
  import { buildSuggestionUrl } from '../../utils/suggestion-url'
8
8
  import { serializeJsonLd } from '../../utils/common'
9
+ import { scrollElementIntoView } from '../../utils/scroll-into-view'
10
+ import { cn } from '../../utils/cn'
9
11
  import { buildFaqJsonLdFromFaqs, type FaqSchemaOptions } from './json-ld'
10
- import { PAGE_HEADING_CLASS, SECTION_HEADING_CLASS } from '../layout/page-heading'
12
+ import { SECTION_HEADING_CLASS } from '../layout/page-heading'
11
13
 
12
14
  export interface FaqSectionProps {
13
15
  /**
@@ -20,23 +22,15 @@ export interface FaqSectionProps {
20
22
  entityType?: string
21
23
  entityId?: number | string
22
24
  /**
23
- * Heading node. `undefined` → variant default (page: <h1>, embedded: <h2>
24
- * "Frequently Asked Questions"); `null` → no heading. React node so
25
- * platforms can drill their local <PageHeading> component without the lib
26
- * referencing platform state.
25
+ * Heading node above the grouped list. `undefined` → default
26
+ * `<h2>`"Frequently Asked Questions". `null` → no heading (the host page
27
+ * owns the `<h1>`, as the standalone /faqs surface does). A React node lets a
28
+ * platform drill its own <PageHeading> without the lib referencing platform
29
+ * state. Also drives category nesting so the document outline stays correct:
30
+ * `null` → categories render `<h2>` (directly under the page `<h1>`);
31
+ * otherwise categories render `<h3>` beneath this heading.
27
32
  */
28
33
  heading?: React.ReactNode | null
29
- /**
30
- * Shell variant — the FAQ LIST itself renders identically everywhere (ONE
31
- * flat accordion, `faq.section` as a per-row chip, never a heading):
32
- * - 'page' — the standalone /faqs surface: <main> shell + page gutters,
33
- * default <h1> heading.
34
- * - 'embedded' — a section inside a host page (entity detail rails, the
35
- * global nested-page block): bare <section>, default <h2> heading.
36
- * Back-compat default: `heading === null` → 'embedded', else 'page' (the
37
- * pre-variant discriminator).
38
- */
39
- variant?: 'page' | 'embedded'
40
34
  /** Inject FAQPage schema.org JSON-LD as a <script>. Off by default so embeds
41
35
  * don't emit duplicate schema. */
42
36
  emitJsonLd?: boolean
@@ -65,16 +59,177 @@ function buildFaqsUrl(
65
59
  return buildSuggestionUrl('/api/faqs', { apiBaseUrl, entityType, entityId, count: minResults })
66
60
  }
67
61
 
68
- /** ONE list shape for every surface standalone /faqs and embeds render the
69
- * IDENTICAL flat accordion (`faq.section` = per-row chip, never a heading),
70
- * so the two can't drift visually. */
71
- function toAccordionItems(faqs: Faq[]): FaqItem[] {
72
- return faqs.map((faq) => ({
73
- id: faq.id,
74
- question: faq.question,
75
- answer: faq.answer,
76
- badge: faq.section || undefined,
77
- }))
62
+ /** Stable, URL-safe anchor id for a category. Prefixed so it can't collide
63
+ * with other in-page ids, and so a bare numeric/blank section still yields a
64
+ * valid id. */
65
+ function sectionSlug(section: string): string {
66
+ return (
67
+ 'faq-' +
68
+ section
69
+ .toLowerCase()
70
+ .replace(/[^a-z0-9]+/g, '-')
71
+ .replace(/^-+|-+$/g, '')
72
+ )
73
+ }
74
+
75
+ interface FaqGroup {
76
+ /** null → the uncategorized bucket: no heading, no jump pill, rendered last. */
77
+ section: string | null
78
+ slug: string | null
79
+ items: FaqItem[]
80
+ }
81
+
82
+ /** Group FAQs by `faq.section`, preserving the server's first-seen
83
+ * (display_order) order for BOTH the section order and the rows within each
84
+ * section. The uncategorized bucket (blank/missing section) sinks to the end
85
+ * since it renders without a heading. Items carry NO badge here — the `<h2>`
86
+ * IS the category, so a per-row chip would be redundant. */
87
+ function groupFaqsBySection(faqs: Faq[]): FaqGroup[] {
88
+ const order: string[] = []
89
+ const byName = new Map<string, FaqGroup>()
90
+ let uncategorized: FaqGroup | null = null
91
+ for (const faq of faqs) {
92
+ const item: FaqItem = { id: faq.id, question: faq.question, answer: faq.answer }
93
+ const name = faq.section?.trim()
94
+ if (!name) {
95
+ if (!uncategorized) uncategorized = { section: null, slug: null, items: [] }
96
+ uncategorized.items.push(item)
97
+ continue
98
+ }
99
+ let group = byName.get(name)
100
+ if (!group) {
101
+ group = { section: name, slug: sectionSlug(name), items: [] }
102
+ byName.set(name, group)
103
+ order.push(name)
104
+ }
105
+ group.items.push(item)
106
+ }
107
+ const groups = order.map((name) => byName.get(name)!)
108
+ if (uncategorized) groups.push(uncategorized)
109
+ return groups
110
+ }
111
+
112
+ /** The standard hub sticky-header height — same offset `useNavLink`'s hash
113
+ * scroll uses, so a category jump lands below the header, not under it. */
114
+ const FAQ_NAV_HEADER_OFFSET = 96
115
+
116
+ /**
117
+ * Grouped FAQ layout: a category jump-nav above stacked `<h2>` category
118
+ * sections (each its own accordion). Isolated into its own component so the
119
+ * scroll-spy hooks only mount in grouped mode — `FaqSection`'s own hooks stay
120
+ * unconditional.
121
+ *
122
+ * The pills are real `<a href="#slug">` anchors (crawlable in-page links,
123
+ * deep-linkable, work without JS); the click handler upgrades the jump to the
124
+ * cancellation-proof `scrollElementIntoView` tween and syncs the URL hash.
125
+ */
126
+ function GroupedFaqList({
127
+ groups,
128
+ categoryHeadingAs,
129
+ }: {
130
+ groups: FaqGroup[]
131
+ /** Heading tag for each category, so the document outline nests correctly
132
+ * under whatever owns the heading above this block: `h2` on the standalone
133
+ * page (the page owns the `<h1>`), `h3` beneath an embed's `<h2>` title.
134
+ * The VISUAL is `SECTION_HEADING_CLASS` either way, so categories look
135
+ * identical on every surface. */
136
+ categoryHeadingAs: 'h2' | 'h3'
137
+ }) {
138
+ const CategoryHeading = categoryHeadingAs
139
+ const navGroups = useMemo(() => groups.filter((g) => g.slug), [groups])
140
+ const [activeSlug, setActiveSlug] = useState<string | null>(navGroups[0]?.slug ?? null)
141
+ // Identity-stable key for the section set so the observer re-binds only when
142
+ // the categories actually change (not on every parent re-render).
143
+ const slugKey = navGroups.map((g) => g.slug).join('|')
144
+
145
+ // Scroll-spy: mark the pill for the category currently at the top of the
146
+ // viewport. rootMargin drops the trigger line just below the sticky header
147
+ // and ignores the bottom ~55% so "active" is the section being read, not the
148
+ // next one peeking in.
149
+ useEffect(() => {
150
+ if (navGroups.length < 2) return
151
+ const tops = new Map<string, number>()
152
+ const observer = new IntersectionObserver(
153
+ (entries) => {
154
+ for (const entry of entries) {
155
+ const id = (entry.target as HTMLElement).id
156
+ if (entry.isIntersecting) tops.set(id, entry.boundingClientRect.top)
157
+ else tops.delete(id)
158
+ }
159
+ let bestId: string | null = null
160
+ let bestTop = Number.POSITIVE_INFINITY
161
+ for (const [id, top] of tops) {
162
+ if (top < bestTop) {
163
+ bestTop = top
164
+ bestId = id
165
+ }
166
+ }
167
+ if (bestId) setActiveSlug(bestId)
168
+ },
169
+ { rootMargin: `-${FAQ_NAV_HEADER_OFFSET}px 0px -55% 0px`, threshold: 0 },
170
+ )
171
+ for (const group of navGroups) {
172
+ const el = group.slug ? document.getElementById(group.slug) : null
173
+ if (el) observer.observe(el)
174
+ }
175
+ return () => observer.disconnect()
176
+ // slugKey encodes the section set; re-observe only when it changes.
177
+ // eslint-disable-next-line react-hooks/exhaustive-deps
178
+ }, [slugKey])
179
+
180
+ const handleJump = useCallback(
181
+ (e: React.MouseEvent<HTMLAnchorElement>, slug: string) => {
182
+ e.preventDefault()
183
+ setActiveSlug(slug)
184
+ scrollElementIntoView(document.getElementById(slug), {
185
+ headerOffset: FAQ_NAV_HEADER_OFFSET,
186
+ })
187
+ if (typeof history !== 'undefined') history.replaceState(null, '', `#${slug}`)
188
+ },
189
+ [],
190
+ )
191
+
192
+ return (
193
+ <div className="space-y-8">
194
+ {navGroups.length > 1 && (
195
+ <nav aria-label="FAQ categories" className="flex flex-wrap gap-2">
196
+ {navGroups.map((group) => {
197
+ const isActive = group.slug === activeSlug
198
+ return (
199
+ <a
200
+ key={group.slug}
201
+ href={`#${group.slug}`}
202
+ aria-current={isActive ? 'true' : undefined}
203
+ onClick={(e) => handleJump(e, group.slug as string)}
204
+ className={cn(
205
+ "rounded-full border px-4 py-2 text-sm font-medium font-['DM_Sans'] transition-colors",
206
+ isActive
207
+ ? 'border-ods-text-primary bg-ods-card text-ods-text-primary'
208
+ : 'border-ods-border bg-ods-card text-ods-text-secondary hover:border-ods-text-secondary hover:text-ods-text-primary',
209
+ )}
210
+ >
211
+ {group.section}
212
+ </a>
213
+ )
214
+ })}
215
+ </nav>
216
+ )}
217
+ <div className="space-y-10">
218
+ {groups.map((group) => (
219
+ <section
220
+ key={group.slug ?? 'faq-uncategorized'}
221
+ id={group.slug ?? undefined}
222
+ className="scroll-mt-24 space-y-4"
223
+ >
224
+ {group.section && (
225
+ <CategoryHeading className={SECTION_HEADING_CLASS}>{group.section}</CategoryHeading>
226
+ )}
227
+ <FaqAccordion items={group.items} />
228
+ </section>
229
+ ))}
230
+ </div>
231
+ </div>
232
+ )
78
233
  }
79
234
 
80
235
  function FaqSkeleton() {
@@ -98,23 +253,27 @@ interface FaqsResponse {
98
253
  }
99
254
 
100
255
  /**
101
- * Generic, embeddable FAQ display surface.
256
+ * The FAQ display surface — ONE rendering on every host: the list grouped by
257
+ * `faq.section` (each category a heading + its own accordion, with a category
258
+ * jump-nav once there are 2+ categories). There is no flat/ungrouped mode and
259
+ * no page-vs-embedded shell fork; the standalone /faqs page and every embed
260
+ * render through this single path, so they cannot drift.
102
261
  *
103
- * - Standalone /faqs page: pass `initialFaqs` from the server + `heading` +
104
- * `emitJsonLd` with `jsonLd` overrides for SEO.
262
+ * - Standalone /faqs page: pass `initialFaqs` (SSR) + `heading={null}` (the
263
+ * page owns the <h1>) + `emitJsonLd` with `jsonLd` overrides for SEO.
105
264
  * - Per-entity embed: pass `entityType` + `entityId` (no `initialFaqs`); the
106
- * hook self-fetches and the public /api/faqs endpoint picks override-vs-fallback.
265
+ * hook self-fetches `GET /api/faqs`, and `heading` is this block's own <h2>.
107
266
  *
108
- * CONTRACT: the consuming app MUST implement `GET /api/faqs`. Without it,
109
- * useSelfFetch reports `error:true` (logged once); in embedded mode (heading
110
- * === null) this component renders nothing so the host page isn't disfigured.
267
+ * CONTRACT: the consuming app MUST implement `GET /api/faqs`. On a fetch error
268
+ * (or zero FAQs) the component renders nothing so the host page isn't
269
+ * disfigured. The host always supplies the page shell this renders a bare
270
+ * <section>.
111
271
  */
112
272
  export function FaqSection({
113
273
  initialFaqs,
114
274
  entityType,
115
275
  entityId,
116
276
  heading,
117
- variant,
118
277
  emitJsonLd = false,
119
278
  jsonLd,
120
279
  className,
@@ -131,86 +290,35 @@ export function FaqSection({
131
290
  const { data, isLoading, error } = useSelfFetch<FaqsResponse>(url, { initialData })
132
291
 
133
292
  const faqs = data?.faqs ?? []
134
- const isEmbedded = variant ? variant === 'embedded' : heading === null
135
- const headingNode =
136
- heading === undefined ? (
137
- isEmbedded ? (
138
- // Embedded default is an <h2> — a host page already owns the <h1>,
139
- // and the schema/heading pair must read "Frequently Asked Questions"
140
- // (one FAQ heading per page, never tag/section names).
141
- <h2 className={SECTION_HEADING_CLASS}>{DEFAULT_HEADING_TEXT}</h2>
142
- ) : (
143
- <h1 className={PAGE_HEADING_CLASS}>{DEFAULT_HEADING_TEXT}</h1>
144
- )
145
- ) : (
146
- heading
147
- )
148
-
149
- // Embedded mode: degrade silently on error.
150
- if (error && isEmbedded) {
151
- return null
152
- }
293
+ // Grouped before the early returns so the hook order stays stable.
294
+ const groups = useMemo(() => (faqs.length > 0 ? groupFaqsBySection(faqs) : []), [faqs])
153
295
 
154
- // Embedded mode, zero FAQs after load: render nothing an embedded host
155
- // page (blog/case-study detail) must not show an empty section shell.
156
- // The standalone /faqs page keeps its existing behavior.
157
- if (!isLoading && !error && faqs.length === 0 && isEmbedded) {
158
- return null
159
- }
296
+ // `undefined` default <h2> title; `null` the host page owns the <h1>, so
297
+ // no title renders here. `heading === null` also makes the category headings
298
+ // <h2> (directly under the page <h1>); otherwise they nest as <h3>.
299
+ const headingNode =
300
+ heading === undefined ? <h2 className={SECTION_HEADING_CLASS}>{DEFAULT_HEADING_TEXT}</h2> : heading
160
301
 
302
+ // Degrade silently — never show an error banner or an empty section shell
303
+ // where FAQs would be (host pages and the standalone surface both rely on it).
304
+ if (error) return null
305
+ if (!isLoading && faqs.length === 0) return null
161
306
  if (isLoading && faqs.length === 0) {
162
- // Embedded mode renders skeletons WITHOUT the standalone page shell —
163
- // a host detail page already provides <main> + gutters; nesting them
164
- // here would emit invalid markup and double padding.
165
- if (isEmbedded) {
166
- return (
167
- <div className={className}>
168
- <FaqSkeleton />
169
- </div>
170
- )
171
- }
172
- return (
173
- <main className={className ?? 'bg-ods-bg'}>
174
- <div className="max-w-[1920px] px-6 md:px-20 py-6 md:py-10 mx-auto">
175
- <FaqSkeleton />
176
- </div>
177
- </main>
178
- )
179
- }
180
-
181
- if (error && faqs.length === 0) {
182
307
  return (
183
- <main className={className ?? 'bg-ods-bg flex items-center justify-center py-20'}>
184
- <p className="text-ods-text-primary font-medium">Failed to load FAQs. Please try again later.</p>
185
- </main>
308
+ <div className={className}>
309
+ <FaqSkeleton />
310
+ </div>
186
311
  )
187
312
  }
188
313
 
189
314
  const schema = emitJsonLd ? buildFaqJsonLdFromFaqs(faqs, jsonLd) : null
190
315
 
191
- // ONE flat accordion on EVERY surface — `faq.section` becomes a per-row
192
- // chip, NOT a heading, so the page outline stays heading + h3(questions).
193
- // Order is the server's (post-specific first, then reused) — grouping by
194
- // section here would destroy it. The variants differ ONLY in shell:
195
- // standalone /faqs keeps its <main> + page gutters, embeds render bare.
196
- const accordion = <FaqAccordion items={toAccordionItems(faqs)} />
197
- const body = isEmbedded ? (
198
- <section className={className ?? 'space-y-10'}>
199
- {headingNode}
200
- {accordion}
201
- </section>
202
- ) : (
203
- <main className={className ?? 'bg-ods-bg'}>
204
- <div className="max-w-[1920px] px-6 md:px-20 py-6 md:py-10 mx-auto space-y-10">
205
- {headingNode}
206
- {accordion}
207
- </div>
208
- </main>
209
- )
210
-
211
316
  return (
212
317
  <>
213
- {body}
318
+ <section className={className ?? 'space-y-10'}>
319
+ {headingNode}
320
+ <GroupedFaqList groups={groups} categoryHeadingAs={heading === null ? 'h2' : 'h3'} />
321
+ </section>
214
322
  {schema && (
215
323
  <script
216
324
  type="application/ld+json"
@@ -2,19 +2,12 @@
2
2
 
3
3
  import React, { useRef, useState, useEffect, useCallback } from 'react'
4
4
  import { ChevronButton } from './ui/chevron-button'
5
- import { StatusBadge } from './ui/status-badge'
6
- import { TAG_BADGE_CLASS } from './features/entity-tag-badges'
7
5
  import { cn } from "../utils/cn"
8
6
 
9
7
  export interface FaqItem {
10
8
  id: number | string
11
9
  question: string
12
10
  answer: string
13
- /** Optional category label (faq.section = tag name) rendered under the
14
- * question as the product's standard tag badge — the same StatusBadge
15
- * card skin EntityTagBadges uses everywhere (never a heading, so the
16
- * document outline stays h2/h3 only). */
17
- badge?: string
18
11
  }
19
12
 
20
13
  interface FaqAccordionProps {
@@ -83,17 +76,10 @@ export function FaqAccordion({ items, defaultOpenIds = [] }: FaqAccordionProps)
83
76
  aria-expanded={isOpen}
84
77
  className="flex w-full items-center justify-between px-6 md:px-8 py-6 text-left focus:outline-none transition-colors cursor-pointer"
85
78
  >
86
- <div className="flex min-w-0 flex-col items-start gap-2 pr-4">
79
+ <div className="min-w-0 pr-4">
87
80
  <h3>
88
81
  {item.question}
89
82
  </h3>
90
- {item.badge && (
91
- <StatusBadge
92
- text={item.badge.toUpperCase()}
93
- variant="card"
94
- className={TAG_BADGE_CLASS}
95
- />
96
- )}
97
83
  </div>
98
84
  <div className="flex-shrink-0">
99
85
  <ChevronButton
@@ -1 +0,0 @@
1
- {"version":3,"sources":["/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/chunk-7VMEOFEA.cjs","../src/utils/common.ts","../src/utils/suggestion-url.ts","../src/components/ui/chevron-button.tsx"],"names":["cn"],"mappings":"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","file":"/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/chunk-7VMEOFEA.cjs","sourcesContent":[null,"import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\n/**\n * Utility functions for common operations\n */\n\n/**\n * Merge class names with Tailwind CSS\n * @param inputs - Class names to merge\n * @returns Merged class names\n */\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs))\n}\n\n/**\n * Delay execution for a specified time\n * @param ms - Milliseconds to delay\n * @returns Promise that resolves after the delay\n */\nexport function delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms))\n}\n\n/**\n * Generate a random string of specified length\n * @param length - Length of the string\n * @returns Random string\n */\nexport function generateRandomString(length = 8): string {\n const chars = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\"\n let result = \"\"\n for (let i = 0; i < length; i++) {\n result += chars.charAt(Math.floor(Math.random() * chars.length))\n }\n return result\n}\n\n/**\n * Truncate a string to a specified length\n * @param str - String to truncate\n * @param maxLength - Maximum length\n * @param suffix - Suffix to add to truncated string\n * @returns Truncated string\n */\nexport function truncateString(str: string, maxLength: number, suffix = \"...\"): string {\n if (str.length <= maxLength) return str\n return str.substring(0, maxLength - suffix.length) + suffix\n}\n\n/**\n * Serialize a JSON-LD schema for a `<script type=\"application/ld+json\">`\n * block — THE one home of the escape rule (the hub re-exports this from\n * lib/utils/breadcrumbs). JSON.stringify does NOT HTML-escape: a stored\n * \"</script>\" inside any admin/user-entered field would terminate the tag\n * early (stored XSS). Every \"<\" becomes the JSON escape sequence\n * backslash-u003c — still valid JSON, inert in HTML.\n */\nexport function serializeJsonLd(schema: unknown): string {\n return JSON.stringify(schema).replace(/</g, \"\\\\u003c\")\n}\n\n/**\n * Deep clone an object\n * @param obj - Object to clone\n * @returns Cloned object\n */\nexport function deepClone<T>(obj: T): T {\n return JSON.parse(JSON.stringify(obj))\n}\n\n/**\n * Get the Slack community join URL from environment variables\n * @returns Slack community join URL or fallback URL\n */\nexport function getSlackCommunityJoinUrl(): string {\n const url = process.env.NEXT_PUBLIC_SLACK_COMMUNITY_JOIN_URL\n if (!url) {\n console.warn('NEXT_PUBLIC_SLACK_COMMUNITY_JOIN_URL is not defined in environment variables')\n return 'https://join.slack.com/t/openmsp/shared_invite/zt-36bl7mx0h-3~U2nFH6nqHqoTPXMaHEHA'\n }\n return url\n}\n\n","/**\n * Shared fetch-URL composer for the two suggestion sections (FaqSection +\n * RelatedContentSection) — ONE owner of the entityType/entityId/count query\n * shape so the sections can never drift. Pure + server-safe.\n *\n * Byte-parity contract with the original `buildFaqsUrl`: when only\n * entityType/entityId are set, the param order is `entityType`, `entityId`;\n * with nothing set, the bare `path` is returned (no `?`).\n */\nexport interface SuggestionUrlOptions {\n /** Fetch-URL prefix for third-party embeds / reverse proxies\n * ('' = same-origin relative). */\n apiBaseUrl?: string\n /** Both required together for entity scope; partial → bare path. */\n entityType?: string\n entityId?: number | string\n /** Maps to the server's `count` param. Undefined → param not sent\n * (server default applies). */\n count?: number\n /** Extra query params appended after the shared trio (e.g. the\n * related-content `excludeTypes` deny-list). Empty/undefined values\n * are skipped. */\n extraParams?: Record<string, string | undefined>\n}\n\nexport function buildSuggestionUrl(path: string, opts: SuggestionUrlOptions = {}): string {\n const qs = new URLSearchParams()\n const { entityType, entityId } = opts\n if (entityType && entityId !== undefined && entityId !== null && entityId !== '') {\n qs.set('entityType', entityType)\n qs.set('entityId', String(entityId))\n }\n if (opts.count !== undefined) qs.set('count', String(opts.count))\n for (const [key, value] of Object.entries(opts.extraParams ?? {})) {\n if (value !== undefined && value !== '') qs.set(key, value)\n }\n const query = qs.toString()\n return `${opts.apiBaseUrl ?? ''}${path}${query ? `?${query}` : ''}`\n}\n","\"use client\"\n\nimport * as React from \"react\"\nimport { ChevronUp, ChevronDown } from \"lucide-react\"\nimport { cn } from \"../../utils/cn\"\n\ninterface ChevronButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {\n direction?: 'up' | 'down';\n size?: 'sm' | 'md' | 'lg';\n isExpanded?: boolean;\n backgroundColor?: string;\n borderColor?: string;\n}\n\nconst ChevronButton = React.forwardRef<HTMLButtonElement, ChevronButtonProps>(\n ({ className, direction = 'down', size = 'md', isExpanded, backgroundColor, borderColor, ...props }, ref) => {\n const Icon = (isExpanded ? ChevronUp : ChevronDown) || (direction === 'up' ? ChevronUp : ChevronDown);\n const sizeClasses = {\n sm: 'h-4 w-4',\n md: 'h-5 w-5', \n lg: 'h-6 w-6'\n };\n\n return (\n <button\n className={cn(\n \"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50\",\n \"h-10 px-4 py-2\",\n className\n )}\n style={{\n backgroundColor: backgroundColor,\n borderColor: borderColor\n }}\n ref={ref}\n {...props}\n >\n <Icon className={sizeClasses[size]} />\n </button>\n )\n }\n)\nChevronButton.displayName = \"ChevronButton\"\n\nexport { ChevronButton }"]}