@flamingo-stack/openframe-frontend-core 0.0.285 → 0.0.286-snapshot.20260618154650
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/dist/chunk-3MCHAFHB.js +89 -0
- package/dist/chunk-3MCHAFHB.js.map +1 -0
- package/dist/chunk-3SDBXXDP.cjs +1594 -0
- package/dist/chunk-3SDBXXDP.cjs.map +1 -0
- package/dist/{chunk-IMNGGLKL.js → chunk-3ZXUQQL4.js} +2 -2
- package/dist/chunk-5KD3S25X.cjs +312 -0
- package/dist/chunk-5KD3S25X.cjs.map +1 -0
- package/dist/{chunk-44ZFLMYC.cjs → chunk-6AW25OS6.cjs} +30 -30
- package/dist/{chunk-44ZFLMYC.cjs.map → chunk-6AW25OS6.cjs.map} +1 -1
- package/dist/{chunk-XDI5JSWB.cjs → chunk-6CSW5TMS.cjs} +45 -41
- package/dist/chunk-6CSW5TMS.cjs.map +1 -0
- package/dist/chunk-7EYWERFT.js +1594 -0
- package/dist/chunk-7EYWERFT.js.map +1 -0
- package/dist/{chunk-XX7P4IL2.js → chunk-7NM7DEUK.js} +4 -143
- package/dist/chunk-7NM7DEUK.js.map +1 -0
- package/dist/{chunk-GVSTRN36.cjs → chunk-D3LEFMOA.cjs} +3 -3
- package/dist/{chunk-GVSTRN36.cjs.map → chunk-D3LEFMOA.cjs.map} +1 -1
- package/dist/chunk-D6RK5YXX.cjs +2439 -0
- package/dist/chunk-D6RK5YXX.cjs.map +1 -0
- package/dist/{chunk-VQW7CEGS.js → chunk-EFYXPR43.js} +1831 -3672
- package/dist/chunk-EFYXPR43.js.map +1 -0
- package/dist/{chunk-PVXCVPJD.cjs → chunk-EI4WALN2.cjs} +67 -1626
- package/dist/chunk-EI4WALN2.cjs.map +1 -0
- package/dist/{chunk-ZIODAAFY.cjs → chunk-EL5GCMPU.cjs} +20 -159
- package/dist/chunk-EL5GCMPU.cjs.map +1 -0
- package/dist/{chunk-53CFL7PH.js → chunk-GJDXIVEQ.js} +11 -7
- package/dist/{chunk-53CFL7PH.js.map → chunk-GJDXIVEQ.js.map} +1 -1
- package/dist/chunk-I6ZPGKZ2.cjs +151 -0
- package/dist/chunk-I6ZPGKZ2.cjs.map +1 -0
- package/dist/{chunk-P45Q4QCU.cjs → chunk-JQ4I743L.cjs} +19 -15
- package/dist/chunk-JQ4I743L.cjs.map +1 -0
- package/dist/{chunk-J66U3LPZ.cjs → chunk-KBKZYJRI.cjs} +33 -332
- package/dist/chunk-KBKZYJRI.cjs.map +1 -0
- package/dist/chunk-MV67MBV3.js +290 -0
- package/dist/chunk-MV67MBV3.js.map +1 -0
- package/dist/chunk-MWS25U4U.cjs +290 -0
- package/dist/chunk-MWS25U4U.cjs.map +1 -0
- package/dist/{chunk-USPATRSC.js → chunk-N6ZM5PYZ.js} +45 -344
- package/dist/chunk-N6ZM5PYZ.js.map +1 -0
- package/dist/chunk-ODR6A6FC.js +2439 -0
- package/dist/chunk-ODR6A6FC.js.map +1 -0
- package/dist/{chunk-DPKUSBQH.cjs → chunk-OXC72UIP.cjs} +1073 -2914
- package/dist/chunk-OXC72UIP.cjs.map +1 -0
- package/dist/{chunk-RGALV4UX.js → chunk-POKKCWKF.js} +3 -82
- package/dist/chunk-POKKCWKF.js.map +1 -0
- package/dist/{chunk-Z3V2SGQZ.js → chunk-R2KT5GDD.js} +324 -1883
- package/dist/chunk-R2KT5GDD.js.map +1 -0
- package/dist/{chunk-RJUGNL2A.cjs → chunk-RG6FNZUA.cjs} +73 -31
- package/dist/chunk-RG6FNZUA.cjs.map +1 -0
- package/dist/{chunk-23UOJUDF.js → chunk-RWCA2ZQK.js} +4 -4
- package/dist/chunk-TFSYSWPS.cjs +89 -0
- package/dist/chunk-TFSYSWPS.cjs.map +1 -0
- package/dist/{chunk-KNCDUWY2.js → chunk-TY2EB7VK.js} +65 -23
- package/dist/chunk-TY2EB7VK.js.map +1 -0
- package/dist/chunk-TYZEMPPH.js +312 -0
- package/dist/chunk-TYZEMPPH.js.map +1 -0
- package/dist/chunk-VRSXJ5QJ.js +1599 -0
- package/dist/chunk-VRSXJ5QJ.js.map +1 -0
- package/dist/chunk-Y4JNA4W6.cjs +1599 -0
- package/dist/chunk-Y4JNA4W6.cjs.map +1 -0
- package/dist/{chunk-ZSUNQJRO.cjs → chunk-YETA25JW.cjs} +2 -81
- package/dist/chunk-YETA25JW.cjs.map +1 -0
- package/dist/chunk-ZLN6SM2U.js +151 -0
- package/dist/chunk-ZLN6SM2U.js.map +1 -0
- package/dist/{chunk-O5N57CRV.js → chunk-ZYLQMCHW.js} +12 -8
- package/dist/{chunk-O5N57CRV.js.map → chunk-ZYLQMCHW.js.map} +1 -1
- package/dist/components/chat/chat-attachment-bar.d.ts.map +1 -1
- package/dist/components/chat/index.cjs +12 -6
- package/dist/components/chat/index.cjs.map +1 -1
- package/dist/components/chat/index.js +45 -39
- package/dist/components/contact/index.cjs +11 -7
- package/dist/components/contact/index.cjs.map +1 -1
- package/dist/components/contact/index.js +10 -6
- package/dist/components/docs/doc-navigation-context.d.ts +39 -0
- package/dist/components/docs/doc-navigation-context.d.ts.map +1 -0
- package/dist/components/docs/doc-viewer.d.ts +66 -0
- package/dist/components/docs/doc-viewer.d.ts.map +1 -0
- package/dist/components/docs/index.cjs +34 -0
- package/dist/components/docs/index.cjs.map +1 -0
- package/dist/components/docs/index.d.ts +8 -0
- package/dist/components/docs/index.d.ts.map +1 -0
- package/dist/components/docs/index.js +34 -0
- package/dist/components/docs/index.js.map +1 -0
- package/dist/components/docs/use-document-tree.d.ts +41 -0
- package/dist/components/docs/use-document-tree.d.ts.map +1 -0
- package/dist/components/docs/use-scroll-spy.d.ts +16 -0
- package/dist/components/docs/use-scroll-spy.d.ts.map +1 -0
- package/dist/components/embeds/embed-iframe.d.ts +29 -0
- package/dist/components/embeds/embed-iframe.d.ts.map +1 -0
- package/dist/components/embeds/figma-embed.d.ts +27 -0
- package/dist/components/embeds/figma-embed.d.ts.map +1 -0
- package/dist/components/embeds/google-sheets-viewer.d.ts +8 -0
- package/dist/components/embeds/google-sheets-viewer.d.ts.map +1 -0
- package/dist/components/embeds/index.cjs +37 -0
- package/dist/components/embeds/index.cjs.map +1 -0
- package/dist/components/embeds/index.d.ts +9 -0
- package/dist/components/embeds/index.d.ts.map +1 -0
- package/dist/components/embeds/index.js +37 -0
- package/dist/components/embeds/index.js.map +1 -0
- package/dist/components/embeds/pdf-viewer.d.ts +10 -0
- package/dist/components/embeds/pdf-viewer.d.ts.map +1 -0
- package/dist/components/faq/faq-section.d.ts.map +1 -1
- package/dist/components/faq/index.cjs +12 -8
- package/dist/components/faq/index.cjs.map +1 -1
- package/dist/components/faq/index.js +11 -7
- package/dist/components/faq-accordion.d.ts.map +1 -1
- package/dist/components/features/index.cjs +10 -6
- package/dist/components/features/index.cjs.map +1 -1
- package/dist/components/features/index.js +9 -5
- package/dist/components/index.cjs +247 -1120
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +645 -1518
- package/dist/components/index.js.map +1 -1
- package/dist/components/navigation/index.cjs +15 -6
- package/dist/components/navigation/index.cjs.map +1 -1
- package/dist/components/navigation/index.d.ts +2 -0
- package/dist/components/navigation/index.d.ts.map +1 -1
- package/dist/components/navigation/index.js +15 -6
- package/dist/components/navigation/multi-level-navigation.d.ts +28 -0
- package/dist/components/navigation/multi-level-navigation.d.ts.map +1 -0
- package/dist/components/onboarding-guides/index.cjs +34 -29
- package/dist/components/onboarding-guides/index.cjs.map +1 -1
- package/dist/components/onboarding-guides/index.js +13 -8
- package/dist/components/onboarding-guides/index.js.map +1 -1
- package/dist/components/related-content/index.cjs +12 -8
- package/dist/components/related-content/index.cjs.map +1 -1
- package/dist/components/related-content/index.js +11 -7
- package/dist/components/tickets/index.cjs +75 -70
- package/dist/components/tickets/index.cjs.map +1 -1
- package/dist/components/tickets/index.js +16 -11
- package/dist/components/tickets/index.js.map +1 -1
- package/dist/components/ui/index.cjs +12 -6
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.js +65 -59
- package/dist/embed-shims/index.cjs +3 -3
- package/dist/embed-shims/index.cjs.map +1 -1
- package/dist/embed-shims/index.js +8 -8
- package/dist/hooks/index.cjs +6 -3
- package/dist/hooks/index.cjs.map +1 -1
- package/dist/hooks/index.js +7 -4
- package/dist/index.cjs +68 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +263 -201
- package/dist/types/doc-source.cjs +4 -0
- package/dist/types/doc-source.cjs.map +1 -0
- package/dist/types/doc-source.d.ts +86 -0
- package/dist/types/doc-source.d.ts.map +1 -0
- package/dist/types/doc-source.js +3 -0
- package/dist/types/doc-source.js.map +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/marketing.d.ts +1 -1
- package/dist/types/marketing.d.ts.map +1 -1
- package/dist/utils/doc-path-utils.d.ts +24 -0
- package/dist/utils/doc-path-utils.d.ts.map +1 -0
- package/dist/utils/doc-tree-nav.d.ts +35 -0
- package/dist/utils/doc-tree-nav.d.ts.map +1 -0
- package/dist/utils/embed-url-converters.d.ts +16 -0
- package/dist/utils/embed-url-converters.d.ts.map +1 -0
- package/dist/utils/faq-anchor.d.ts +51 -0
- package/dist/utils/faq-anchor.d.ts.map +1 -0
- package/dist/utils/format.d.ts +9 -0
- package/dist/utils/format.d.ts.map +1 -1
- package/dist/utils/index.cjs +320 -1
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.d.ts +9 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +296 -2
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/list-url.d.ts.map +1 -1
- package/dist/utils/markdown-section-extractor.d.ts +23 -0
- package/dist/utils/markdown-section-extractor.d.ts.map +1 -0
- package/dist/utils/markdown-to-plain.d.ts +32 -0
- package/dist/utils/markdown-to-plain.d.ts.map +1 -0
- package/dist/utils/tree-builder.d.ts +30 -0
- package/dist/utils/tree-builder.d.ts.map +1 -0
- package/package.json +19 -1
- package/src/components/chat/chat-attachment-bar.tsx +6 -11
- package/src/components/docs/doc-navigation-context.tsx +123 -0
- package/src/components/docs/doc-viewer.tsx +345 -0
- package/src/components/docs/index.ts +12 -0
- package/src/components/docs/use-document-tree.ts +427 -0
- package/src/components/docs/use-scroll-spy.ts +93 -0
- package/src/components/embeds/embed-iframe.tsx +103 -0
- package/src/components/embeds/figma-embed.tsx +147 -0
- package/src/components/embeds/google-sheets-viewer.tsx +54 -0
- package/src/components/embeds/index.ts +11 -0
- package/src/components/embeds/pdf-viewer.tsx +64 -0
- package/src/components/faq/faq-section.tsx +78 -26
- package/src/components/faq-accordion.tsx +8 -1
- package/src/components/index.ts +2 -0
- package/src/components/navigation/index.ts +5 -0
- package/src/components/navigation/multi-level-navigation.tsx +358 -0
- package/src/types/doc-source.ts +88 -0
- package/src/types/index.ts +4 -0
- package/src/types/marketing.ts +1 -0
- package/src/utils/doc-path-utils.ts +47 -0
- package/src/utils/doc-tree-nav.ts +111 -0
- package/src/utils/embed-url-converters.ts +77 -0
- package/src/utils/faq-anchor.ts +70 -0
- package/src/utils/format.ts +16 -0
- package/src/utils/index.ts +18 -0
- package/src/utils/list-url.ts +1 -0
- package/src/utils/markdown-section-extractor.ts +106 -0
- package/src/utils/markdown-to-plain.ts +69 -0
- package/src/utils/tree-builder.ts +114 -0
- package/dist/chunk-DPKUSBQH.cjs.map +0 -1
- package/dist/chunk-J66U3LPZ.cjs.map +0 -1
- package/dist/chunk-KNCDUWY2.js.map +0 -1
- package/dist/chunk-P45Q4QCU.cjs.map +0 -1
- package/dist/chunk-PVXCVPJD.cjs.map +0 -1
- package/dist/chunk-RGALV4UX.js.map +0 -1
- package/dist/chunk-RJUGNL2A.cjs.map +0 -1
- package/dist/chunk-USPATRSC.js.map +0 -1
- package/dist/chunk-VQW7CEGS.js.map +0 -1
- package/dist/chunk-XDI5JSWB.cjs.map +0 -1
- package/dist/chunk-XX7P4IL2.js.map +0 -1
- package/dist/chunk-Z3V2SGQZ.js.map +0 -1
- package/dist/chunk-ZIODAAFY.cjs.map +0 -1
- package/dist/chunk-ZSUNQJRO.cjs.map +0 -1
- /package/dist/{chunk-IMNGGLKL.js.map → chunk-3ZXUQQL4.js.map} +0 -0
- /package/dist/{chunk-23UOJUDF.js.map → chunk-RWCA2ZQK.js.map} +0 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utilities for converting external service URLs to embeddable formats.
|
|
3
|
+
* Used by lib's embed components and the hub's admin document editor.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export function toGoogleSheetsEmbedUrl(url: string): string {
|
|
7
|
+
if (url.includes('/htmlembed')) return url
|
|
8
|
+
|
|
9
|
+
const match = url.match(/\/spreadsheets\/d\/([a-zA-Z0-9-_]+)/)
|
|
10
|
+
if (!match) return url
|
|
11
|
+
|
|
12
|
+
const gidMatch = url.match(/[#?&]gid=(\d+)/)
|
|
13
|
+
const gid = gidMatch ? gidMatch[1] : '0'
|
|
14
|
+
|
|
15
|
+
return `https://docs.google.com/spreadsheets/d/${match[1]}/htmlembed?widget=true&chrome=false&headers=false&gid=${gid}`
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function toGoogleSheetsOriginalUrl(url: string): string {
|
|
19
|
+
const match = url.match(/\/spreadsheets\/d\/([a-zA-Z0-9-_]+)/)
|
|
20
|
+
if (!match) return url
|
|
21
|
+
|
|
22
|
+
const gidMatch = url.match(/[#?&]gid=(\d+)/)
|
|
23
|
+
const gid = gidMatch ? gidMatch[1] : '0'
|
|
24
|
+
|
|
25
|
+
return `https://docs.google.com/spreadsheets/d/${match[1]}/edit#gid=${gid}`
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Convert a Figma URL to an embeddable URL.
|
|
30
|
+
* Slides/deck URLs map to `deck` (present) by default; `slidesView: 'browse'` switches to `slides` (browse).
|
|
31
|
+
*/
|
|
32
|
+
export function toFigmaEmbedUrl(
|
|
33
|
+
url: string,
|
|
34
|
+
opts?: { slidesView?: 'present' | 'browse' }
|
|
35
|
+
): string {
|
|
36
|
+
if (url.includes('embed.figma.com')) return url
|
|
37
|
+
if (url.includes('figma.com/embed')) return url
|
|
38
|
+
|
|
39
|
+
const match = url.match(
|
|
40
|
+
/figma\.com\/(design|file|proto|board|slides|deck)\/([a-zA-Z0-9]+)(?:\/([^?]*))?(\?.*)?$/
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
if (match) {
|
|
44
|
+
const [, urlType, fileKey, titleSlug, queryString] = match
|
|
45
|
+
const isSlides = urlType === 'slides' || urlType === 'deck'
|
|
46
|
+
const embedType =
|
|
47
|
+
urlType === 'proto' ? 'proto'
|
|
48
|
+
: isSlides ? (opts?.slidesView === 'browse' ? 'slides' : 'deck')
|
|
49
|
+
: 'design'
|
|
50
|
+
const pathSuffix = titleSlug ? `/${titleSlug}` : ''
|
|
51
|
+
|
|
52
|
+
const params = new URLSearchParams(queryString?.replace(/^\?/, '') || '')
|
|
53
|
+
if (!params.has('embed-host')) {
|
|
54
|
+
params.set('embed-host', 'flamingo')
|
|
55
|
+
}
|
|
56
|
+
const clientId = process.env.NEXT_PUBLIC_FIGMA_CLIENT_ID
|
|
57
|
+
if (clientId && !params.has('client-id')) {
|
|
58
|
+
params.set('client-id', clientId)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return `https://embed.figma.com/${embedType}/${fileKey}${pathSuffix}?${params.toString()}`
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return `https://www.figma.com/embed?embed-host=flamingo&url=${encodeURIComponent(url)}`
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function isFigmaSlidesUrl(url: string): boolean {
|
|
68
|
+
if (!url) return false
|
|
69
|
+
return /(?:www\.|embed\.)?figma\.com\/(?:slides|deck)\/[a-zA-Z0-9]+/.test(url)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function toFigmaOriginalUrl(url: string): string {
|
|
73
|
+
if (url.includes('embed.figma.com')) {
|
|
74
|
+
return url.replace('embed.figma.com', 'www.figma.com').replace(/\?.*$/, '')
|
|
75
|
+
}
|
|
76
|
+
return url.replace(/\?.*$/, '')
|
|
77
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FAQ anchor SSOTs — TWO complementary deep-link kinds on the `/faqs` page,
|
|
3
|
+
* both rendered straight onto the DOM and recognised by ONE parser. Keeping
|
|
4
|
+
* the format helpers + parser in one file means the page, the chat RAG
|
|
5
|
+
* mapper, and any future consumer can't drift on what a hash means.
|
|
6
|
+
*
|
|
7
|
+
* `/faqs#faq-pricing` → category section header (jump-nav pills,
|
|
8
|
+
* scroll-spy)
|
|
9
|
+
* `/faqs#faq-item-42` → individual question (chat citation chips —
|
|
10
|
+
* auto-expands the row + scrolls to it)
|
|
11
|
+
*
|
|
12
|
+
* Reserved namespaces — `faq-item-<digits>` is the item shape; everything
|
|
13
|
+
* else starting with `faq-` is a section slug. `faqSectionSlug` lowercases
|
|
14
|
+
* + dash-collapses + dash-trims, so a section name would only collide with
|
|
15
|
+
* the item shape if it slugified to `faq-item-<digits-only>` (e.g. a
|
|
16
|
+
* category called "Item 42") — none of the 21 production sections do, and
|
|
17
|
+
* `parseFaqHash`'s regex is digits-only so future word-suffixed names like
|
|
18
|
+
* "Item Whatever" (slugifies to `faq-item-whatever`) are also safe.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** Stable, URL-safe anchor id for a category. Prefixed with `faq-` so it
|
|
22
|
+
* can't collide with other in-page ids, and so a bare numeric/blank
|
|
23
|
+
* section still yields a valid id. The helper assumes the caller has
|
|
24
|
+
* already verified the section is a non-blank string (matches
|
|
25
|
+
* `faq-section.tsx`'s `groupFaqsBySection` precondition). */
|
|
26
|
+
export function faqSectionSlug(section: string): string {
|
|
27
|
+
return (
|
|
28
|
+
'faq-' +
|
|
29
|
+
section
|
|
30
|
+
.toLowerCase()
|
|
31
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
32
|
+
.replace(/^-+|-+$/g, '')
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Stable anchor for an individual FAQ row. Rendered as the row container's
|
|
37
|
+
* `id` attribute by `FaqAccordion`; consumed by `FaqSection` (auto-open +
|
|
38
|
+
* auto-scroll on mount) AND by the hub's RAG mapper so chat citations
|
|
39
|
+
* deep-link to the specific question, not just its category. The id is
|
|
40
|
+
* stringified verbatim — the FAQ schema uses integer PKs so `parseFaqHash`'s
|
|
41
|
+
* digits-only regex always matches a real row. */
|
|
42
|
+
export function faqItemAnchor(id: number | string): string {
|
|
43
|
+
return `faq-item-${id}`
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Discriminated parse of a `/faqs#…` hash. Returns null for an empty,
|
|
47
|
+
* missing, or unrecognised hash so callers can early-out without
|
|
48
|
+
* scattering string parsing across the file.
|
|
49
|
+
*
|
|
50
|
+
* parseFaqHash('#faq-item-42') → { kind: 'item', rawId: '42' }
|
|
51
|
+
* parseFaqHash('#faq-pricing') → { kind: 'section', slug: 'faq-pricing' }
|
|
52
|
+
* parseFaqHash('#anything-else') → null
|
|
53
|
+
*
|
|
54
|
+
* `rawId` is the matched digit run as a string — the caller compares it
|
|
55
|
+
* to `String(item.id)` so coercion stays at the comparison site. */
|
|
56
|
+
export type FaqHashTarget =
|
|
57
|
+
| { kind: 'item'; rawId: string }
|
|
58
|
+
| { kind: 'section'; slug: string }
|
|
59
|
+
|
|
60
|
+
const FAQ_ITEM_HASH_RE = /^faq-item-(\d+)$/
|
|
61
|
+
|
|
62
|
+
export function parseFaqHash(hash: string | null | undefined): FaqHashTarget | null {
|
|
63
|
+
if (!hash) return null
|
|
64
|
+
const trimmed = hash.replace(/^#/, '')
|
|
65
|
+
if (!trimmed) return null
|
|
66
|
+
const itemMatch = FAQ_ITEM_HASH_RE.exec(trimmed)
|
|
67
|
+
if (itemMatch) return { kind: 'item', rawId: itemMatch[1] }
|
|
68
|
+
if (trimmed.startsWith('faq-')) return { kind: 'section', slug: trimmed }
|
|
69
|
+
return null
|
|
70
|
+
}
|
package/src/utils/format.ts
CHANGED
|
@@ -81,6 +81,22 @@ export function formatBytesShort(bytes: number): string {
|
|
|
81
81
|
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
/**
|
|
85
|
+
* File-size formatter (1 decimal place, `'B'` unit, caps at `GB`).
|
|
86
|
+
* Hub historical semantics — distinct from `formatBytesShort` (2dp) and
|
|
87
|
+
* `formatBytes` (2dp / `'Bytes'`). Lifted from the hub during the doc-viewer
|
|
88
|
+
* unification so all upload UIs, publication cards, and data-room file-size
|
|
89
|
+
* displays render the same way.
|
|
90
|
+
* @example formatFileSize(0) → "0 B"; formatFileSize(1500) → "1.5 KB"
|
|
91
|
+
*/
|
|
92
|
+
export function formatFileSize(bytes: number): string {
|
|
93
|
+
if (bytes === 0) return '0 B'
|
|
94
|
+
const k = 1024
|
|
95
|
+
const sizes = ['B', 'KB', 'MB', 'GB']
|
|
96
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
|
97
|
+
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`
|
|
98
|
+
}
|
|
99
|
+
|
|
84
100
|
/**
|
|
85
101
|
* Format large numbers to abbreviated form (K, M, B) with no decimal points
|
|
86
102
|
* @param num - The number to format
|
package/src/utils/index.ts
CHANGED
|
@@ -102,6 +102,7 @@ export {
|
|
|
102
102
|
formatWholeDollars,
|
|
103
103
|
formatLegalDate,
|
|
104
104
|
formatBytesShort,
|
|
105
|
+
formatFileSize,
|
|
105
106
|
formatDateTimeAt,
|
|
106
107
|
formatDurationFromMs,
|
|
107
108
|
type MetricFormat,
|
|
@@ -252,6 +253,13 @@ export {
|
|
|
252
253
|
// Pure + server-safe (the hub imports it server-side from this barrel).
|
|
253
254
|
export { buildListUrl, canonicalContentRefType } from './list-url'
|
|
254
255
|
|
|
256
|
+
// FAQ anchor SSOTs — section (`faq-<slug>`) AND item (`faq-item-<id>`)
|
|
257
|
+
// formats plus the parser, all rendered by `FaqSection`/`FaqAccordion`
|
|
258
|
+
// and recognised by the hub's RAG mapper. One algo per kind, one parser,
|
|
259
|
+
// zero drift across page + chat + future consumers.
|
|
260
|
+
export { faqSectionSlug, faqItemAnchor, parseFaqHash } from './faq-anchor'
|
|
261
|
+
export type { FaqHashTarget } from './faq-anchor'
|
|
262
|
+
|
|
255
263
|
// Content-ref group registry (labels/order/layout per rail type) + list-API
|
|
256
264
|
// response normalizers + the shared suggestion-fetch URL composer — all
|
|
257
265
|
// pure + server-safe; the hub re-exports these from its config/util shims.
|
|
@@ -283,3 +291,13 @@ export {
|
|
|
283
291
|
// decision fn the lib forms feed. Also exported via the granular subpath
|
|
284
292
|
// `./utils/humanity-signals` for server-only consumers.
|
|
285
293
|
export * from './humanity-signals'
|
|
294
|
+
|
|
295
|
+
// Doc-source viewer utilities (path parsing, tree building, section extraction,
|
|
296
|
+
// embed-URL conversion) — single home for all doc-viewer pure helpers across
|
|
297
|
+
// hub + lib consumers (knowledge-base, data-room, and future sources).
|
|
298
|
+
export * from './doc-path-utils'
|
|
299
|
+
export * from './doc-tree-nav'
|
|
300
|
+
export * from './tree-builder'
|
|
301
|
+
export * from './markdown-section-extractor'
|
|
302
|
+
export * from './markdown-to-plain'
|
|
303
|
+
export * from './embed-url-converters'
|
package/src/utils/list-url.ts
CHANGED
|
@@ -70,6 +70,7 @@ const BUILDERS: Record<string, (ids: string[], base: string) => string> = {
|
|
|
70
70
|
product_release: (ids, b) => `${b}/api/releases?ids=${ids.join(',')}&limit=${ids.length}`,
|
|
71
71
|
customer_interview: (ids, b) => `${b}/api/customer-interviews?ids=${ids.join(',')}&limit=${ids.length}`,
|
|
72
72
|
investor_update: (ids, b) => `${b}/api/investor-updates?ids=${ids.join(',')}&limit=${ids.length}`,
|
|
73
|
+
faq: (ids, b) => `${b}/api/faqs?ids=${ids.join(',')}&limit=${ids.length}`,
|
|
73
74
|
}
|
|
74
75
|
|
|
75
76
|
/**
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared markdown section extractor
|
|
3
|
+
* Extracts heading sections from markdown content for navigation.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { stripInlineMarkdown } from './markdown-to-plain'
|
|
7
|
+
|
|
8
|
+
export interface MarkdownSection {
|
|
9
|
+
id: string
|
|
10
|
+
title: string
|
|
11
|
+
level: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ExtractSectionsOptions {
|
|
15
|
+
/** Maximum heading level to include (default: 2 = H1-H2) */
|
|
16
|
+
maxLevel?: number
|
|
17
|
+
/** Remove emoji characters from IDs (default: true) */
|
|
18
|
+
removeEmojis?: boolean
|
|
19
|
+
/** Handle duplicate IDs by appending a counter suffix (default: true) */
|
|
20
|
+
handleDuplicateIds?: boolean
|
|
21
|
+
/** Strip bold, italic, and inline code markers from titles (default: true) */
|
|
22
|
+
stripFormattingMarkers?: boolean
|
|
23
|
+
/** Skip code blocks and YAML frontmatter (default: true) */
|
|
24
|
+
skipCodeAndYamlBlocks?: boolean
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const DEFAULT_OPTIONS: Required<ExtractSectionsOptions> = {
|
|
28
|
+
maxLevel: 2,
|
|
29
|
+
removeEmojis: true,
|
|
30
|
+
handleDuplicateIds: true,
|
|
31
|
+
stripFormattingMarkers: true,
|
|
32
|
+
skipCodeAndYamlBlocks: true,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function extractSections(
|
|
36
|
+
markdown: string,
|
|
37
|
+
options: ExtractSectionsOptions = {},
|
|
38
|
+
): MarkdownSection[] {
|
|
39
|
+
const opts = { ...DEFAULT_OPTIONS, ...options }
|
|
40
|
+
const sections: MarkdownSection[] = []
|
|
41
|
+
const lines = markdown.split('\n')
|
|
42
|
+
const idCounts: Record<string, number> = {}
|
|
43
|
+
|
|
44
|
+
let inCodeBlock = false
|
|
45
|
+
let inYamlBlock = false
|
|
46
|
+
|
|
47
|
+
for (const line of lines) {
|
|
48
|
+
if (opts.skipCodeAndYamlBlocks) {
|
|
49
|
+
if (line.startsWith('```')) {
|
|
50
|
+
inCodeBlock = !inCodeBlock
|
|
51
|
+
continue
|
|
52
|
+
}
|
|
53
|
+
if (line === '---') {
|
|
54
|
+
inYamlBlock = !inYamlBlock
|
|
55
|
+
continue
|
|
56
|
+
}
|
|
57
|
+
if (inCodeBlock || inYamlBlock) continue
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const pattern = new RegExp(`^(#{1,${opts.maxLevel}})\\s+(.+)`)
|
|
61
|
+
const match = line.match(pattern)
|
|
62
|
+
if (!match) continue
|
|
63
|
+
|
|
64
|
+
const level = match[1].length
|
|
65
|
+
if (level > opts.maxLevel) continue
|
|
66
|
+
|
|
67
|
+
let title = match[2].trim()
|
|
68
|
+
|
|
69
|
+
if (opts.stripFormattingMarkers) {
|
|
70
|
+
title = stripInlineMarkdown(title).trim()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let baseId = title
|
|
74
|
+
|
|
75
|
+
if (opts.removeEmojis) {
|
|
76
|
+
baseId = baseId
|
|
77
|
+
.replace(
|
|
78
|
+
/[\u{1F300}-\u{1F9FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]/gu,
|
|
79
|
+
'',
|
|
80
|
+
)
|
|
81
|
+
.trim()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
baseId = baseId
|
|
85
|
+
.toLowerCase()
|
|
86
|
+
.replace(/[^\w\s-]/g, '')
|
|
87
|
+
.replace(/\s+/g, '-')
|
|
88
|
+
.replace(/^-+|-+$/g, '')
|
|
89
|
+
|
|
90
|
+
const cleanId = baseId || `section-${sections.length + 1}`
|
|
91
|
+
|
|
92
|
+
let id = cleanId
|
|
93
|
+
if (opts.handleDuplicateIds) {
|
|
94
|
+
if (idCounts[cleanId]) {
|
|
95
|
+
idCounts[cleanId]++
|
|
96
|
+
id = `${cleanId}-${idCounts[cleanId]}`
|
|
97
|
+
} else {
|
|
98
|
+
idCounts[cleanId] = 1
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
sections.push({ id, title, level })
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return sections
|
|
106
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert markdown source to plain text. Used by both the SEO server component
|
|
3
|
+
* (article-body echo in JSON-LD) and chat entity-card preview (clamped previews
|
|
4
|
+
* inside chat cards).
|
|
5
|
+
*
|
|
6
|
+
* The two consumers differ only in pre/post processing — `mode: 'article'`
|
|
7
|
+
* strips code blocks + images + headings + list bullets and preserves paragraph
|
|
8
|
+
* breaks; `mode: 'preview'` collapses everything to a single line. Both share
|
|
9
|
+
* the same inline-marker strip (link / bold / italic / inline-code).
|
|
10
|
+
*
|
|
11
|
+
* All regex patterns are linear (negated character classes, non-greedy with
|
|
12
|
+
* bounded targets) — no nested quantifiers, no catastrophic backtracking on
|
|
13
|
+
* pathological input.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export interface MarkdownToPlainOptions {
|
|
17
|
+
/**
|
|
18
|
+
* - `'article'` (default): preserve paragraph structure (`\n\n` as separator),
|
|
19
|
+
* strip code blocks + images + headings + list bullets. Used for the
|
|
20
|
+
* article-body echo on SEO surfaces.
|
|
21
|
+
* - `'preview'`: collapse all whitespace to single spaces; result is a single
|
|
22
|
+
* line. Used for chat-card previews / OG-card snippets where line breaks
|
|
23
|
+
* would wrap awkwardly.
|
|
24
|
+
*/
|
|
25
|
+
mode?: 'article' | 'preview'
|
|
26
|
+
/** Optional max-character cap. Truncates with no ellipsis (caller-decided). */
|
|
27
|
+
maxChars?: number
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Strip inline markdown markers (bold, italic, inline-code, links). Shared
|
|
31
|
+
* by `extractSections`'s `stripFormattingMarkers` and `markdownToPlainText`. */
|
|
32
|
+
export function stripInlineMarkdown(s: string): string {
|
|
33
|
+
return s
|
|
34
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') // [text](url) → text
|
|
35
|
+
.replace(/\*\*([^*]+)\*\*/g, '$1') // **bold** → bold
|
|
36
|
+
.replace(/\*([^*]+)\*/g, '$1') // *italic* → italic
|
|
37
|
+
.replace(/`([^`]+)`/g, '$1') // `inline code` → inline code
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function markdownToPlainText(
|
|
41
|
+
markdown: string,
|
|
42
|
+
options: MarkdownToPlainOptions = {},
|
|
43
|
+
): string {
|
|
44
|
+
const { mode = 'article', maxChars } = options
|
|
45
|
+
|
|
46
|
+
let out = markdown
|
|
47
|
+
// Fenced code blocks — must run BEFORE inline strippers (otherwise the
|
|
48
|
+
// backtick-grave strip below grazes them).
|
|
49
|
+
.replace(/```[\s\S]*?```/g, '')
|
|
50
|
+
// Images — `` → empty (we drop them entirely in plain text).
|
|
51
|
+
.replace(/!\[[^\]]*\]\([^)]+\)/g, '')
|
|
52
|
+
|
|
53
|
+
out = stripInlineMarkdown(out)
|
|
54
|
+
// Heading markers (any depth).
|
|
55
|
+
.replace(/^#+\s*/gm, '')
|
|
56
|
+
// List bullets at start-of-line.
|
|
57
|
+
.replace(/^\s*[-*+]\s+/gm, '')
|
|
58
|
+
|
|
59
|
+
if (mode === 'preview') {
|
|
60
|
+
out = out.replace(/\s+/g, ' ').trim()
|
|
61
|
+
} else {
|
|
62
|
+
out = out.replace(/\n{3,}/g, '\n\n').trim()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (maxChars != null && out.length > maxChars) {
|
|
66
|
+
out = out.slice(0, maxChars)
|
|
67
|
+
}
|
|
68
|
+
return out
|
|
69
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Shared Document Tree Builder
|
|
3
|
+
// =============================================================================
|
|
4
|
+
// Generic 3-pass algorithm for building hierarchical trees from flat records.
|
|
5
|
+
// Used by all doc-source DALs (openframe-docs, data-room-docs, future).
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Base interface for tree nodes. Consumers extend this with additional fields.
|
|
9
|
+
*/
|
|
10
|
+
export interface TreeNodeBase {
|
|
11
|
+
id: string
|
|
12
|
+
name: string
|
|
13
|
+
path: string
|
|
14
|
+
type: 'file' | 'folder'
|
|
15
|
+
sortOrder?: number
|
|
16
|
+
children?: TreeNodeBase[]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Format a document name for display: remove .md extension and capitalize.
|
|
21
|
+
*/
|
|
22
|
+
export function formatDocName(name: string): string {
|
|
23
|
+
let displayName = name
|
|
24
|
+
if (displayName.endsWith('.md')) {
|
|
25
|
+
displayName = displayName.slice(0, -3)
|
|
26
|
+
}
|
|
27
|
+
if (displayName.toLowerCase() === 'readme') {
|
|
28
|
+
return 'README'
|
|
29
|
+
}
|
|
30
|
+
return displayName.charAt(0).toUpperCase() + displayName.slice(1).replace(/-/g, ' ')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Sort children: README first, then folders, then files — each group by sort_order then alphabetical.
|
|
35
|
+
* Recurses into folder children to ensure consistent ordering at all levels.
|
|
36
|
+
*/
|
|
37
|
+
export function sortTreeChildren<TNode extends TreeNodeBase>(nodes: TNode[]): TNode[] {
|
|
38
|
+
nodes.sort((a, b) => {
|
|
39
|
+
const aIsReadme = a.name.toLowerCase() === 'readme' || a.name.toLowerCase() === 'readme.md'
|
|
40
|
+
const bIsReadme = b.name.toLowerCase() === 'readme' || b.name.toLowerCase() === 'readme.md'
|
|
41
|
+
if (aIsReadme && !bIsReadme) return -1
|
|
42
|
+
if (!aIsReadme && bIsReadme) return 1
|
|
43
|
+
|
|
44
|
+
if (a.type === 'folder' && b.type !== 'folder') return -1
|
|
45
|
+
if (a.type !== 'folder' && b.type === 'folder') return 1
|
|
46
|
+
|
|
47
|
+
const aOrder = a.sortOrder ?? Infinity
|
|
48
|
+
const bOrder = b.sortOrder ?? Infinity
|
|
49
|
+
if (aOrder !== bOrder) return aOrder - bOrder
|
|
50
|
+
|
|
51
|
+
return a.name.localeCompare(b.name, undefined, { sensitivity: 'base' })
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
for (const node of nodes) {
|
|
55
|
+
if (node.type === 'folder' && node.children) {
|
|
56
|
+
node.children = sortTreeChildren(node.children as TNode[])
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return nodes
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Build a hierarchical document tree from a flat list of records.
|
|
65
|
+
*
|
|
66
|
+
* 3-pass algorithm:
|
|
67
|
+
* 1. Create node map from flat records using the provided `mapFn`
|
|
68
|
+
* 2. Build parent-child hierarchy using `getParentPath`
|
|
69
|
+
* 3. Sort children (folders first, README, then others)
|
|
70
|
+
*/
|
|
71
|
+
export function buildDocumentTree<TNode extends TreeNodeBase, TDoc>(
|
|
72
|
+
docs: TDoc[],
|
|
73
|
+
mapFn: (doc: TDoc) => TNode,
|
|
74
|
+
getParentPath: (doc: TDoc) => string | null,
|
|
75
|
+
getPath: (doc: TDoc) => string
|
|
76
|
+
): TNode[] {
|
|
77
|
+
const nodeMap = new Map<string, TNode>()
|
|
78
|
+
const rootNodes: TNode[] = []
|
|
79
|
+
|
|
80
|
+
for (const doc of docs) {
|
|
81
|
+
const node = mapFn(doc)
|
|
82
|
+
nodeMap.set(getPath(doc), node)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
for (const doc of docs) {
|
|
86
|
+
const node = nodeMap.get(getPath(doc))!
|
|
87
|
+
let resolvedParentPath = getParentPath(doc)
|
|
88
|
+
|
|
89
|
+
if (!resolvedParentPath) {
|
|
90
|
+
const nodePath = getPath(doc)
|
|
91
|
+
const lastSlash = nodePath.lastIndexOf('/')
|
|
92
|
+
if (lastSlash > 0) {
|
|
93
|
+
resolvedParentPath = nodePath.substring(0, lastSlash)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (resolvedParentPath) {
|
|
98
|
+
const parent = nodeMap.get(resolvedParentPath)
|
|
99
|
+
if (parent) {
|
|
100
|
+
// Lazily initialize `children` so callers whose `mapFn` doesn't
|
|
101
|
+
// pre-seed it still get a proper tree (previously a missing
|
|
102
|
+
// `parent.children` silently flattened the child to the root).
|
|
103
|
+
if (!parent.children) parent.children = []
|
|
104
|
+
;(parent.children as TNode[]).push(node)
|
|
105
|
+
} else {
|
|
106
|
+
rootNodes.push(node)
|
|
107
|
+
}
|
|
108
|
+
} else {
|
|
109
|
+
rootNodes.push(node)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return sortTreeChildren(rootNodes)
|
|
114
|
+
}
|