@flamingo-stack/openframe-frontend-core 0.0.690 → 0.0.691-snapshot.20261001154617
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-22RSJ57Z.js → chunk-2YVTGPM7.js} +2 -2
- package/dist/{chunk-QNWQQ4RT.cjs → chunk-53O6PQ3F.cjs} +7 -7
- package/dist/{chunk-QNWQQ4RT.cjs.map → chunk-53O6PQ3F.cjs.map} +1 -1
- package/dist/{chunk-54X75QTC.cjs → chunk-5XPK2QGU.cjs} +14 -14
- package/dist/{chunk-54X75QTC.cjs.map → chunk-5XPK2QGU.cjs.map} +1 -1
- package/dist/{chunk-4HBYEZ3I.cjs → chunk-7BNWMOPM.cjs} +82 -82
- package/dist/{chunk-4HBYEZ3I.cjs.map → chunk-7BNWMOPM.cjs.map} +1 -1
- package/dist/{chunk-HLEXYTOE.js → chunk-7EPGJOKN.js} +4 -4
- package/dist/{chunk-XRSJER2B.js → chunk-A5NHAHV6.js} +4 -4
- package/dist/{chunk-P27OGSQL.cjs → chunk-ATREVNIN.cjs} +37 -37
- package/dist/{chunk-P27OGSQL.cjs.map → chunk-ATREVNIN.cjs.map} +1 -1
- package/dist/{chunk-S5M5SEW2.js → chunk-AVAZ6BD6.js} +5 -5
- package/dist/{chunk-DLRE5Y3G.js → chunk-C2SVJVEB.js} +2 -2
- package/dist/{chunk-KZXKT5AX.cjs → chunk-CG7OWW7X.cjs} +14 -14
- package/dist/{chunk-KZXKT5AX.cjs.map → chunk-CG7OWW7X.cjs.map} +1 -1
- package/dist/{chunk-AIXPQT3G.js → chunk-CKO6NKNE.js} +801 -222
- package/dist/chunk-CKO6NKNE.js.map +1 -0
- package/dist/{chunk-UFXV64WQ.cjs → chunk-CKUPANVU.cjs} +9 -9
- package/dist/{chunk-UFXV64WQ.cjs.map → chunk-CKUPANVU.cjs.map} +1 -1
- package/dist/{chunk-3ZAIHRQH.cjs → chunk-DKJDO7YM.cjs} +35 -35
- package/dist/{chunk-3ZAIHRQH.cjs.map → chunk-DKJDO7YM.cjs.map} +1 -1
- package/dist/{chunk-D7BFS7N6.cjs → chunk-DUURFYGS.cjs} +3 -3
- package/dist/{chunk-D7BFS7N6.cjs.map → chunk-DUURFYGS.cjs.map} +1 -1
- package/dist/{chunk-NMXL34Z2.cjs → chunk-DVSPRY2V.cjs} +3 -3
- package/dist/{chunk-NMXL34Z2.cjs.map → chunk-DVSPRY2V.cjs.map} +1 -1
- package/dist/{chunk-VLWUKFQE.cjs → chunk-EXWS2QSD.cjs} +1301 -722
- package/dist/chunk-EXWS2QSD.cjs.map +1 -0
- package/dist/{chunk-5CBQBTGN.cjs → chunk-F6LXGP6S.cjs} +7 -7
- package/dist/{chunk-5CBQBTGN.cjs.map → chunk-F6LXGP6S.cjs.map} +1 -1
- package/dist/{chunk-MDYTAKJ5.cjs → chunk-FTK3S57R.cjs} +22 -22
- package/dist/{chunk-MDYTAKJ5.cjs.map → chunk-FTK3S57R.cjs.map} +1 -1
- package/dist/{chunk-UAKWSR3U.js → chunk-GDZ7IC5B.js} +2 -2
- package/dist/{chunk-OD2WMUAO.cjs → chunk-GZRU2A4L.cjs} +197 -197
- package/dist/{chunk-OD2WMUAO.cjs.map → chunk-GZRU2A4L.cjs.map} +1 -1
- package/dist/{chunk-Q76YVNCQ.js → chunk-HM7ZGABI.js} +2 -2
- package/dist/{chunk-YCKGUQA2.cjs → chunk-I27PWVWE.cjs} +17 -12
- package/dist/chunk-I27PWVWE.cjs.map +1 -0
- package/dist/{chunk-TSENCZUL.js → chunk-IQTQIBZ3.js} +11 -11
- package/dist/{chunk-QUGV2GT3.cjs → chunk-KDCIYRZV.cjs} +34 -34
- package/dist/{chunk-QUGV2GT3.cjs.map → chunk-KDCIYRZV.cjs.map} +1 -1
- package/dist/{chunk-2MGTNYHW.cjs → chunk-KQZMM3QJ.cjs} +100 -100
- package/dist/{chunk-2MGTNYHW.cjs.map → chunk-KQZMM3QJ.cjs.map} +1 -1
- package/dist/{chunk-BJHM2OEY.cjs → chunk-LNKLB7GM.cjs} +17 -17
- package/dist/{chunk-BJHM2OEY.cjs.map → chunk-LNKLB7GM.cjs.map} +1 -1
- package/dist/{chunk-Z622ACFD.js → chunk-LYJJH4D6.js} +5 -5
- package/dist/{chunk-42RQ4NIL.js → chunk-LYQULSK4.js} +6 -6
- package/dist/{chunk-TBJWPNHU.js → chunk-MCNQKS6I.js} +4 -4
- package/dist/chunk-MCNQKS6I.js.map +1 -0
- package/dist/{chunk-JGVJRUGB.cjs → chunk-MIZU2OTO.cjs} +4 -4
- package/dist/{chunk-JGVJRUGB.cjs.map → chunk-MIZU2OTO.cjs.map} +1 -1
- package/dist/{chunk-BOM6MMWB.cjs → chunk-N3526CTH.cjs} +3 -3
- package/dist/{chunk-BOM6MMWB.cjs.map → chunk-N3526CTH.cjs.map} +1 -1
- package/dist/{chunk-2PQKZWWP.js → chunk-PKZGXRU2.js} +6 -6
- package/dist/{chunk-FCQR6VY4.cjs → chunk-Q25VYAEL.cjs} +26 -26
- package/dist/{chunk-FCQR6VY4.cjs.map → chunk-Q25VYAEL.cjs.map} +1 -1
- package/dist/{chunk-25X43LSD.js → chunk-RK47E3PP.js} +4 -4
- package/dist/{chunk-V5NOURSR.js → chunk-RLNNV46K.js} +5 -5
- package/dist/{chunk-ZBQZJAC7.cjs → chunk-RTKF5AYA.cjs} +7 -7
- package/dist/chunk-RTKF5AYA.cjs.map +1 -0
- package/dist/{chunk-RF2EYX3B.js → chunk-SC44YJCF.js} +2 -2
- package/dist/{chunk-2OD2PPNO.cjs → chunk-SDORFSFK.cjs} +37 -37
- package/dist/{chunk-2OD2PPNO.cjs.map → chunk-SDORFSFK.cjs.map} +1 -1
- package/dist/{chunk-5A3WSWZD.js → chunk-TEIHEFXP.js} +8 -8
- package/dist/{chunk-U4OJCJTH.js → chunk-TXFGG47A.js} +4 -4
- package/dist/{chunk-DMXI5D2Q.cjs → chunk-USKQURNF.cjs} +3 -3
- package/dist/{chunk-DMXI5D2Q.cjs.map → chunk-USKQURNF.cjs.map} +1 -1
- package/dist/{chunk-2KILTCND.js → chunk-UVUNBXS6.js} +6 -1
- package/dist/chunk-UVUNBXS6.js.map +1 -0
- package/dist/{chunk-X4WBGDNW.cjs → chunk-WTYQCT4A.cjs} +18 -18
- package/dist/{chunk-X4WBGDNW.cjs.map → chunk-WTYQCT4A.cjs.map} +1 -1
- package/dist/{chunk-OC3QH5WG.js → chunk-X5XFKI6N.js} +2 -2
- package/dist/{chunk-2V4KPBNZ.js → chunk-XAOAYDGR.js} +3 -3
- package/dist/{chunk-4BUPMSLY.js → chunk-Y7ZEVTLC.js} +3 -3
- package/dist/{chunk-DOUG4UEZ.js → chunk-Z7QCNCBN.js} +11 -11
- package/dist/{chunk-OQVHGPHA.js → chunk-ZAV2F4VX.js} +3 -3
- package/dist/components/case-studies/index.cjs +20 -20
- package/dist/components/case-studies/index.js +7 -7
- package/dist/components/chat/index.cjs +4 -4
- package/dist/components/chat/index.js +3 -3
- package/dist/components/contact/index.cjs +8 -8
- package/dist/components/contact/index.js +7 -7
- package/dist/components/docs/index.cjs +11 -11
- package/dist/components/docs/index.js +10 -10
- package/dist/components/embeds/index.cjs +8 -8
- package/dist/components/embeds/index.js +7 -7
- package/dist/components/faq/index.cjs +9 -9
- package/dist/components/faq/index.js +8 -8
- package/dist/components/features/index.cjs +39 -7
- package/dist/components/features/index.cjs.map +1 -1
- package/dist/components/features/index.d.ts +1 -0
- package/dist/components/features/index.d.ts.map +1 -1
- package/dist/components/features/index.js +38 -6
- package/dist/components/features/remote-session/index.d.ts +13 -0
- package/dist/components/features/remote-session/index.d.ts.map +1 -0
- package/dist/components/features/remote-session/remote-access-request-card.d.ts +19 -0
- package/dist/components/features/remote-session/remote-access-request-card.d.ts.map +1 -0
- package/dist/components/features/remote-session/remote-session-block.d.ts +41 -0
- package/dist/components/features/remote-session/remote-session-block.d.ts.map +1 -0
- package/dist/components/features/remote-session/remote-session-chat-panel.d.ts +23 -0
- package/dist/components/features/remote-session/remote-session-chat-panel.d.ts.map +1 -0
- package/dist/components/features/remote-session/remote-session-dialog.d.ts +15 -0
- package/dist/components/features/remote-session/remote-session-dialog.d.ts.map +1 -0
- package/dist/components/features/remote-session/remote-session-overlay.d.ts +12 -0
- package/dist/components/features/remote-session/remote-session-overlay.d.ts.map +1 -0
- package/dist/components/features/remote-session/remote-session-parts.d.ts +34 -0
- package/dist/components/features/remote-session/remote-session-parts.d.ts.map +1 -0
- package/dist/components/features/remote-session/types.d.ts +33 -0
- package/dist/components/features/remote-session/types.d.ts.map +1 -0
- package/dist/components/help-center-pages/index.cjs +101 -101
- package/dist/components/help-center-pages/index.js +21 -21
- package/dist/components/index.cjs +97 -65
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +53 -21
- package/dist/components/index.js.map +1 -1
- package/dist/components/meeting-scheduler/index.cjs +58 -58
- package/dist/components/meeting-scheduler/index.js +8 -8
- package/dist/components/navigation/index.cjs +10 -10
- package/dist/components/navigation/index.js +9 -9
- package/dist/components/onboarding-guides/index.cjs +11 -11
- package/dist/components/onboarding-guides/index.js +10 -10
- package/dist/components/related-content/index.cjs +6 -6
- package/dist/components/related-content/index.js +5 -5
- package/dist/components/tickets/index.cjs +12 -12
- package/dist/components/tickets/index.js +11 -11
- package/dist/components/tickets/ticket-live-provider.d.ts.map +1 -1
- package/dist/components/ui/index.cjs +7 -7
- package/dist/components/ui/index.js +6 -6
- package/dist/hooks/index.cjs +4 -4
- package/dist/hooks/index.js +3 -3
- package/dist/index.cjs +44 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +42 -10
- package/dist/index.js.map +1 -1
- package/dist/utils/embed-authed-fetch.d.ts +18 -0
- package/dist/utils/embed-authed-fetch.d.ts.map +1 -1
- package/dist/utils/index.cjs +50 -6
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.js +50 -6
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/sse-subscription.d.ts +29 -11
- package/dist/utils/sse-subscription.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/features/__tests__/remote-session.test.tsx +97 -0
- package/src/components/features/index.ts +1 -0
- package/src/components/features/remote-session/index.ts +36 -0
- package/src/components/features/remote-session/remote-access-request-card.tsx +102 -0
- package/src/components/features/remote-session/remote-session-block.tsx +182 -0
- package/src/components/features/remote-session/remote-session-chat-panel.tsx +178 -0
- package/src/components/features/remote-session/remote-session-dialog.tsx +77 -0
- package/src/components/features/remote-session/remote-session-overlay.tsx +33 -0
- package/src/components/features/remote-session/remote-session-parts.tsx +138 -0
- package/src/components/features/remote-session/types.ts +41 -0
- package/src/components/tickets/ticket-live-provider.tsx +14 -12
- package/src/stories/RemoteSessionAccessRequest.stories.tsx +71 -0
- package/src/stories/RemoteSessionBlock.stories.tsx +164 -0
- package/src/stories/RemoteSessionDialog.stories.tsx +49 -0
- package/src/stories/RemoteSessionIndicator.stories.tsx +44 -0
- package/src/utils/embed-authed-fetch.ts +21 -0
- package/src/utils/sse-subscription.ts +107 -20
- package/dist/chunk-2KILTCND.js.map +0 -1
- package/dist/chunk-AIXPQT3G.js.map +0 -1
- package/dist/chunk-TBJWPNHU.js.map +0 -1
- package/dist/chunk-VLWUKFQE.cjs.map +0 -1
- package/dist/chunk-YCKGUQA2.cjs.map +0 -1
- package/dist/chunk-ZBQZJAC7.cjs.map +0 -1
- /package/dist/{chunk-22RSJ57Z.js.map → chunk-2YVTGPM7.js.map} +0 -0
- /package/dist/{chunk-HLEXYTOE.js.map → chunk-7EPGJOKN.js.map} +0 -0
- /package/dist/{chunk-XRSJER2B.js.map → chunk-A5NHAHV6.js.map} +0 -0
- /package/dist/{chunk-S5M5SEW2.js.map → chunk-AVAZ6BD6.js.map} +0 -0
- /package/dist/{chunk-DLRE5Y3G.js.map → chunk-C2SVJVEB.js.map} +0 -0
- /package/dist/{chunk-UAKWSR3U.js.map → chunk-GDZ7IC5B.js.map} +0 -0
- /package/dist/{chunk-Q76YVNCQ.js.map → chunk-HM7ZGABI.js.map} +0 -0
- /package/dist/{chunk-TSENCZUL.js.map → chunk-IQTQIBZ3.js.map} +0 -0
- /package/dist/{chunk-Z622ACFD.js.map → chunk-LYJJH4D6.js.map} +0 -0
- /package/dist/{chunk-42RQ4NIL.js.map → chunk-LYQULSK4.js.map} +0 -0
- /package/dist/{chunk-2PQKZWWP.js.map → chunk-PKZGXRU2.js.map} +0 -0
- /package/dist/{chunk-25X43LSD.js.map → chunk-RK47E3PP.js.map} +0 -0
- /package/dist/{chunk-V5NOURSR.js.map → chunk-RLNNV46K.js.map} +0 -0
- /package/dist/{chunk-RF2EYX3B.js.map → chunk-SC44YJCF.js.map} +0 -0
- /package/dist/{chunk-5A3WSWZD.js.map → chunk-TEIHEFXP.js.map} +0 -0
- /package/dist/{chunk-U4OJCJTH.js.map → chunk-TXFGG47A.js.map} +0 -0
- /package/dist/{chunk-OC3QH5WG.js.map → chunk-X5XFKI6N.js.map} +0 -0
- /package/dist/{chunk-2V4KPBNZ.js.map → chunk-XAOAYDGR.js.map} +0 -0
- /package/dist/{chunk-4BUPMSLY.js.map → chunk-Y7ZEVTLC.js.map} +0 -0
- /package/dist/{chunk-DOUG4UEZ.js.map → chunk-Z7QCNCBN.js.map} +0 -0
- /package/dist/{chunk-OQVHGPHA.js.map → chunk-ZAV2F4VX.js.map} +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/chunk-DMXI5D2Q.cjs","../src/hooks/use-self-fetch.ts"],"names":[],"mappings":"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","file":"/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/chunk-DMXI5D2Q.cjs","sourcesContent":[null,"'use client';\n\nimport { useEffect, useRef, useState, type Dispatch, type SetStateAction } from 'react';\nimport { contentFetch } from '../utils/embed-content-fetch';\n\nexport interface UseSelfFetchResult<T> {\n data: T | null;\n /** Imperatively patch the fetched data (e.g. optimistic vote updates). */\n setData: Dispatch<SetStateAction<T | null>>;\n isLoading: boolean;\n error: boolean;\n /** Re-run the fetch (error-retry affordance). */\n reload: () => void;\n}\n\n/**\n * The single source for the 4 self-fetching content views\n * (`ProductReleasesView`, `RoadmapView`, the onboarding catalog/detail). GETs a\n * configured `url` into component state with **plain `fetch` + `useEffect`** —\n * deliberately NO react-query, so embedders don't need a QueryClient. This is\n * the same technology choice the shipped `DeliveryLists` makes; that component\n * predates this hook and still hand-rolls the loop (a future pass can migrate\n * it onto a two-url variant of this hook).\n *\n * Behaviour:\n * - `url = null` → fetching is DISABLED (controlled / SSR mode, or missing\n * config); returns `initialData ?? null` and never fetches.\n * - `initialData` provided → hydrates immediately and SKIPS the first fetch\n * (so the hub's server-rendered data isn't re-fetched on mount); later\n * `url` changes still re-fetch.\n * - a `cancelled` guard ensures an in-flight response from a STALE `url`\n * (e.g. a fast pagination / section toggle) can't overwrite a newer one.\n * - `reload()` bumps an internal key to retry after an error.\n * - `revalidateOnVisibleAfterMs` (opt-in) → re-fetch when the tab becomes\n * visible AND the held data is older than the threshold. Event-driven\n * freshness with ZERO background requests — the replacement for\n * setInterval polling. Data age counts from the last completed fetch, or\n * from mount when `initialData` seeded it (an SSR seed is milliseconds\n * old — without that, the first refocus would always refire).\n *\n * Re-fetches whenever `url` changes, so callers fold all query params INTO the\n * url string (the url IS the cache key).\n */\nexport function useSelfFetch<T>(\n url: string | null,\n options?: { initialData?: T; revalidateOnVisibleAfterMs?: number },\n): UseSelfFetchResult<T> {\n const initialData = options?.initialData;\n const revalidateOnVisibleAfterMs = options?.revalidateOnVisibleAfterMs;\n const [data, setData] = useState<T | null>(initialData ?? null);\n const [isLoading, setIsLoading] = useState(initialData === undefined && url !== null);\n const [error, setError] = useState(false);\n const [reloadKey, setReloadKey] = useState(0);\n // When the held data was obtained: seeded at mount for SSR-hydrated data,\n // stamped after each completed fetch. Drives the visibility revalidation age\n // check. 0 = nothing held yet (first visible event may fetch).\n // The mount timestamp comes from a lazy `useState` initialiser — a\n // `useRef(Date.now())` argument is re-evaluated on every render.\n const [mountedAt] = useState(() => Date.now());\n const dataAtRef = useRef<number>(initialData !== undefined ? mountedAt : 0);\n // The url whose data we currently hold — seeded from SSR `initialData`, then set after\n // each completed fetch. The effect skips the fetch when this equals `url` (so server-\n // rendered data isn't re-fetched on mount). A VALUE compare (not a one-shot flag) so the\n // SSR-hydration skip survives React 18 StrictMode's mount→unmount→remount in dev;\n // `reload()` nulls it to force a re-fetch of the same url.\n const dataUrlRef = useRef<string | null>(initialData !== undefined ? url : null);\n\n // Both resets below are prop-driven, so they use React's \"adjust state while\n // rendering\" pattern rather than an effect. From an effect each one commits a\n // frame showing the superseded data (or a spinner that will never resolve)\n // and then immediately re-renders to correct it; done here the wrong frame\n // never reaches the screen.\n const [syncedProps, setSyncedProps] = useState({ url, initialData });\n if (syncedProps.url !== url || syncedProps.initialData !== initialData) {\n setSyncedProps({ url, initialData });\n // A CONTROLLED `initialData` changed (e.g. the host navigates between\n // detail slugs without remounting). No-op in self-fetch mode, where\n // `initialData` is `undefined`.\n if (syncedProps.initialData !== initialData && initialData !== undefined) {\n setData(initialData);\n }\n // Fetching just got disabled — nothing will land to clear the spinner.\n if (syncedProps.url !== url && url === null) {\n setIsLoading(false);\n }\n }\n\n useEffect(() => {\n if (url === null) return undefined;\n // Already hold data for this exact url (SSR-hydrated, or a prior completed fetch) →\n // skip. `reload()` clears `dataUrlRef` so a retry of the same url still fetches.\n if (dataUrlRef.current === url) return undefined;\n const ctrl = new AbortController();\n let cancelled = false;\n async function load() {\n try {\n setIsLoading(true);\n setError(false);\n const res = await contentFetch(url as string, { signal: ctrl.signal });\n if (!res.ok) throw new Error(`Request failed (${res.status})`);\n const json = (await res.json()) as T;\n if (!cancelled) {\n setData(json);\n dataUrlRef.current = url; // remember the url we now hold data for\n dataAtRef.current = Date.now();\n }\n } catch (err) {\n // AbortError on cleanup (unmount / stale-url change / React StrictMode's dev\n // double-invoke) is EXPECTED — the request was intentionally aborted. Aborting\n // also means the orphaned StrictMode fetch shows as \"cancelled\" instead of\n // completing as a wasted duplicate (parity with useChatIdentity). Only real\n // failures fall through to the error state + console.\n if (cancelled || (err as Error)?.name === 'AbortError') return;\n setError(true);\n console.error('useSelfFetch:', url, err);\n } finally {\n if (!cancelled) setIsLoading(false);\n }\n }\n // Never rejects — try/catch/finally, every write gated on `cancelled`.\n void load();\n return () => {\n cancelled = true;\n ctrl.abort();\n };\n // `url` folds in every query param; `reloadKey` drives retry.\n }, [url, reloadKey]);\n\n // ONE force-refetch mechanic shared by reload() and the visibility\n // revalidation: clear the held-url skip, bump the effect key.\n const forceReload = () => {\n dataUrlRef.current = null;\n setReloadKey(k => k + 1);\n };\n\n // Opt-in visibility-driven revalidation. Registered only when the option is\n // set and fetching is enabled; SSR-safe (effects don't run on the server)\n // and StrictMode-safe (idempotent listener, cleaned up per mount cycle).\n useEffect(() => {\n if (!revalidateOnVisibleAfterMs || url === null) return undefined;\n const onVisible = () => {\n if (document.visibilityState !== 'visible') return;\n if (Date.now() - dataAtRef.current < revalidateOnVisibleAfterMs) return;\n forceReload();\n };\n document.addEventListener('visibilitychange', onVisible);\n return () => document.removeEventListener('visibilitychange', onVisible);\n }, [url, revalidateOnVisibleAfterMs]);\n\n return {\n data,\n setData,\n isLoading,\n error,\n // Force a re-fetch of the current url (error-retry affordance).\n reload: forceReload,\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/chunk-USKQURNF.cjs","../src/hooks/use-self-fetch.ts"],"names":[],"mappings":"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","file":"/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/chunk-USKQURNF.cjs","sourcesContent":[null,"'use client';\n\nimport { useEffect, useRef, useState, type Dispatch, type SetStateAction } from 'react';\nimport { contentFetch } from '../utils/embed-content-fetch';\n\nexport interface UseSelfFetchResult<T> {\n data: T | null;\n /** Imperatively patch the fetched data (e.g. optimistic vote updates). */\n setData: Dispatch<SetStateAction<T | null>>;\n isLoading: boolean;\n error: boolean;\n /** Re-run the fetch (error-retry affordance). */\n reload: () => void;\n}\n\n/**\n * The single source for the 4 self-fetching content views\n * (`ProductReleasesView`, `RoadmapView`, the onboarding catalog/detail). GETs a\n * configured `url` into component state with **plain `fetch` + `useEffect`** —\n * deliberately NO react-query, so embedders don't need a QueryClient. This is\n * the same technology choice the shipped `DeliveryLists` makes; that component\n * predates this hook and still hand-rolls the loop (a future pass can migrate\n * it onto a two-url variant of this hook).\n *\n * Behaviour:\n * - `url = null` → fetching is DISABLED (controlled / SSR mode, or missing\n * config); returns `initialData ?? null` and never fetches.\n * - `initialData` provided → hydrates immediately and SKIPS the first fetch\n * (so the hub's server-rendered data isn't re-fetched on mount); later\n * `url` changes still re-fetch.\n * - a `cancelled` guard ensures an in-flight response from a STALE `url`\n * (e.g. a fast pagination / section toggle) can't overwrite a newer one.\n * - `reload()` bumps an internal key to retry after an error.\n * - `revalidateOnVisibleAfterMs` (opt-in) → re-fetch when the tab becomes\n * visible AND the held data is older than the threshold. Event-driven\n * freshness with ZERO background requests — the replacement for\n * setInterval polling. Data age counts from the last completed fetch, or\n * from mount when `initialData` seeded it (an SSR seed is milliseconds\n * old — without that, the first refocus would always refire).\n *\n * Re-fetches whenever `url` changes, so callers fold all query params INTO the\n * url string (the url IS the cache key).\n */\nexport function useSelfFetch<T>(\n url: string | null,\n options?: { initialData?: T; revalidateOnVisibleAfterMs?: number },\n): UseSelfFetchResult<T> {\n const initialData = options?.initialData;\n const revalidateOnVisibleAfterMs = options?.revalidateOnVisibleAfterMs;\n const [data, setData] = useState<T | null>(initialData ?? null);\n const [isLoading, setIsLoading] = useState(initialData === undefined && url !== null);\n const [error, setError] = useState(false);\n const [reloadKey, setReloadKey] = useState(0);\n // When the held data was obtained: seeded at mount for SSR-hydrated data,\n // stamped after each completed fetch. Drives the visibility revalidation age\n // check. 0 = nothing held yet (first visible event may fetch).\n // The mount timestamp comes from a lazy `useState` initialiser — a\n // `useRef(Date.now())` argument is re-evaluated on every render.\n const [mountedAt] = useState(() => Date.now());\n const dataAtRef = useRef<number>(initialData !== undefined ? mountedAt : 0);\n // The url whose data we currently hold — seeded from SSR `initialData`, then set after\n // each completed fetch. The effect skips the fetch when this equals `url` (so server-\n // rendered data isn't re-fetched on mount). A VALUE compare (not a one-shot flag) so the\n // SSR-hydration skip survives React 18 StrictMode's mount→unmount→remount in dev;\n // `reload()` nulls it to force a re-fetch of the same url.\n const dataUrlRef = useRef<string | null>(initialData !== undefined ? url : null);\n\n // Both resets below are prop-driven, so they use React's \"adjust state while\n // rendering\" pattern rather than an effect. From an effect each one commits a\n // frame showing the superseded data (or a spinner that will never resolve)\n // and then immediately re-renders to correct it; done here the wrong frame\n // never reaches the screen.\n const [syncedProps, setSyncedProps] = useState({ url, initialData });\n if (syncedProps.url !== url || syncedProps.initialData !== initialData) {\n setSyncedProps({ url, initialData });\n // A CONTROLLED `initialData` changed (e.g. the host navigates between\n // detail slugs without remounting). No-op in self-fetch mode, where\n // `initialData` is `undefined`.\n if (syncedProps.initialData !== initialData && initialData !== undefined) {\n setData(initialData);\n }\n // Fetching just got disabled — nothing will land to clear the spinner.\n if (syncedProps.url !== url && url === null) {\n setIsLoading(false);\n }\n }\n\n useEffect(() => {\n if (url === null) return undefined;\n // Already hold data for this exact url (SSR-hydrated, or a prior completed fetch) →\n // skip. `reload()` clears `dataUrlRef` so a retry of the same url still fetches.\n if (dataUrlRef.current === url) return undefined;\n const ctrl = new AbortController();\n let cancelled = false;\n async function load() {\n try {\n setIsLoading(true);\n setError(false);\n const res = await contentFetch(url as string, { signal: ctrl.signal });\n if (!res.ok) throw new Error(`Request failed (${res.status})`);\n const json = (await res.json()) as T;\n if (!cancelled) {\n setData(json);\n dataUrlRef.current = url; // remember the url we now hold data for\n dataAtRef.current = Date.now();\n }\n } catch (err) {\n // AbortError on cleanup (unmount / stale-url change / React StrictMode's dev\n // double-invoke) is EXPECTED — the request was intentionally aborted. Aborting\n // also means the orphaned StrictMode fetch shows as \"cancelled\" instead of\n // completing as a wasted duplicate (parity with useChatIdentity). Only real\n // failures fall through to the error state + console.\n if (cancelled || (err as Error)?.name === 'AbortError') return;\n setError(true);\n console.error('useSelfFetch:', url, err);\n } finally {\n if (!cancelled) setIsLoading(false);\n }\n }\n // Never rejects — try/catch/finally, every write gated on `cancelled`.\n void load();\n return () => {\n cancelled = true;\n ctrl.abort();\n };\n // `url` folds in every query param; `reloadKey` drives retry.\n }, [url, reloadKey]);\n\n // ONE force-refetch mechanic shared by reload() and the visibility\n // revalidation: clear the held-url skip, bump the effect key.\n const forceReload = () => {\n dataUrlRef.current = null;\n setReloadKey(k => k + 1);\n };\n\n // Opt-in visibility-driven revalidation. Registered only when the option is\n // set and fetching is enabled; SSR-safe (effects don't run on the server)\n // and StrictMode-safe (idempotent listener, cleaned up per mount cycle).\n useEffect(() => {\n if (!revalidateOnVisibleAfterMs || url === null) return undefined;\n const onVisible = () => {\n if (document.visibilityState !== 'visible') return;\n if (Date.now() - dataAtRef.current < revalidateOnVisibleAfterMs) return;\n forceReload();\n };\n document.addEventListener('visibilitychange', onVisible);\n return () => document.removeEventListener('visibilitychange', onVisible);\n }, [url, revalidateOnVisibleAfterMs]);\n\n return {\n data,\n setData,\n isLoading,\n error,\n // Force a re-fetch of the current url (error-retry affordance).\n reload: forceReload,\n };\n}\n"]}
|
|
@@ -453,6 +453,10 @@ function setEmbedAuthAdapter(adapter2) {
|
|
|
453
453
|
function hasEmbedAuthAdapter() {
|
|
454
454
|
return getRegisteredAuthAdapter() !== null;
|
|
455
455
|
}
|
|
456
|
+
function subscribeEmbedCredentialChange(listener) {
|
|
457
|
+
return getRegisteredAuthAdapter()?.subscribe?.(listener) ?? (() => {
|
|
458
|
+
});
|
|
459
|
+
}
|
|
456
460
|
function needsBearerAssetFetch(url) {
|
|
457
461
|
if (typeof window === "undefined") return false;
|
|
458
462
|
const adapter2 = getRegisteredAuthAdapter();
|
|
@@ -704,6 +708,7 @@ export {
|
|
|
704
708
|
applyProxyAuth,
|
|
705
709
|
setEmbedAuthAdapter,
|
|
706
710
|
hasEmbedAuthAdapter,
|
|
711
|
+
subscribeEmbedCredentialChange,
|
|
707
712
|
embedAuthedFetch,
|
|
708
713
|
clearAuthedAssetCache,
|
|
709
714
|
useAuthedAssetSrc,
|
|
@@ -712,4 +717,4 @@ export {
|
|
|
712
717
|
buildOgPlaceholderUrl,
|
|
713
718
|
useOgPlaceholderUrl
|
|
714
719
|
};
|
|
715
|
-
//# sourceMappingURL=chunk-
|
|
720
|
+
//# sourceMappingURL=chunk-UVUNBXS6.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/hooks/ui/use-image-edge-color.ts","../src/utils/platform-identity.ts","../src/utils/app-config.ts","../src/utils/local-storage-adapter.ts","../src/utils/embed-proxy-auth-storage.ts","../src/utils/embed-authed-fetch.ts","../src/hooks/use-authed-asset-src.ts","../src/hooks/use-authed-image-src.ts","../src/utils/og-placeholder.ts","../src/hooks/use-og-placeholder-url.ts"],"sourcesContent":["'use client';\n\nimport { useState, useEffect } from 'react';\n\n/**\n * Extract the dominant edge color from an image using color bucketing.\n * Samples pixels along left and right edges (the visible letterbox areas),\n * groups them into color buckets, and returns the most common bucket.\n * This avoids muddy averages when edges have mixed colors (e.g., sky + sand).\n */\nfunction extractEdgeColor(img: HTMLImageElement): string {\n const canvas = document.createElement('canvas');\n const ctx = canvas.getContext('2d');\n if (!ctx) return '#000000';\n\n const maxSize = 100;\n const scale = Math.min(maxSize / img.naturalWidth, maxSize / img.naturalHeight);\n const w = Math.round(img.naturalWidth * scale);\n const h = Math.round(img.naturalHeight * scale);\n canvas.width = w;\n canvas.height = h;\n\n ctx.drawImage(img, 0, 0, w, h);\n\n const data = ctx.getImageData(0, 0, w, h).data;\n\n // Sample left edge, right edge, top edge, bottom edge (15% band)\n const edgeW = Math.max(2, Math.round(w * 0.15));\n const edgeH = Math.max(2, Math.round(h * 0.15));\n\n // Color bucketing: quantize to 32-step buckets for grouping similar colors\n const bucketSize = 32;\n const buckets = new Map<string, { r: number; g: number; b: number; count: number }>();\n\n for (let y = 0; y < h; y++) {\n for (let x = 0; x < w; x++) {\n const isEdge = x < edgeW || x >= w - edgeW || y < edgeH || y >= h - edgeH;\n\n if (!isEdge) continue;\n\n const i = (y * w + x) * 4;\n const a = data[i + 3];\n if (a < 128) continue;\n\n const r = data[i];\n const g = data[i + 1];\n const b = data[i + 2];\n\n // Quantize to bucket\n const br = Math.floor(r / bucketSize) * bucketSize;\n const bg = Math.floor(g / bucketSize) * bucketSize;\n const bb = Math.floor(b / bucketSize) * bucketSize;\n const key = `${br},${bg},${bb}`;\n\n const existing = buckets.get(key);\n if (existing) {\n existing.r += r;\n existing.g += g;\n existing.b += b;\n existing.count++;\n } else {\n buckets.set(key, { r, g, b, count: 1 });\n }\n }\n }\n\n if (buckets.size === 0) return '#000000';\n\n // Find the most common color bucket\n let bestBucket: { r: number; g: number; b: number; count: number } | null = null;\n for (const bucket of buckets.values()) {\n if (!bestBucket || bucket.count > bestBucket.count) {\n bestBucket = bucket;\n }\n }\n\n if (!bestBucket || bestBucket.count === 0) return '#000000';\n\n // Return average color within the winning bucket\n const r = Math.round(bestBucket.r / bestBucket.count);\n const g = Math.round(bestBucket.g / bestBucket.count);\n const b = Math.round(bestBucket.b / bestBucket.count);\n\n return `rgb(${r}, ${g}, ${b})`;\n}\n\n/**\n * Hook that extracts the dominant edge color from an image URL.\n * Returns a CSS color string for use as a background behind object-contain images.\n *\n * Always sets crossOrigin='anonymous' — required for canvas pixel access.\n * Most CDNs (Supabase, Cloudflare, our image proxy) return CORS headers.\n * If the server doesn't support CORS, onerror fires and we use the fallback.\n *\n * @param imageUrl - URL of the image to analyze\n * @param fallback - Fallback color if extraction fails (default: '#000000')\n * @returns CSS color string (e.g., 'rgb(34, 28, 22)')\n */\nexport function useImageEdgeColor(imageUrl: string | undefined | null, fallback = '#000000'): string {\n const [color, setColor] = useState(fallback);\n\n // Losing the url (or being given a different fallback) is a prop-driven\n // reset, so it happens while rendering — React's documented alternative to a\n // sync-from-props effect. From the effect it would commit one frame with the\n // departed image's colour still painted behind an image that is already gone.\n const [resetFor, setResetFor] = useState({ imageUrl, fallback });\n if (resetFor.imageUrl !== imageUrl || resetFor.fallback !== fallback) {\n setResetFor({ imageUrl, fallback });\n if (!imageUrl) setColor(fallback);\n }\n\n useEffect(() => {\n if (!imageUrl) return undefined;\n\n let cancelled = false;\n const img = new Image();\n img.crossOrigin = 'anonymous';\n\n img.onload = () => {\n if (cancelled) return;\n try {\n setColor(extractEdgeColor(img));\n } catch {\n setColor(fallback);\n }\n };\n\n img.onerror = () => {\n if (cancelled) return;\n setColor(fallback);\n };\n\n img.src = imageUrl;\n\n return () => {\n cancelled = true;\n };\n }, [imageUrl, fallback]);\n\n return color;\n}\n","/**\n * Platform identity + brand SSOT (single source of truth).\n *\n * ONE module owns every per-platform STRING (display/short/long name, description,\n * slogan, icon name) and every per-platform COLOUR DECISION (the accent/secondary\n * \"stem\", the theme, the surface). Everything downstream DERIVES from here:\n * `platform-config.tsx`'s icon + colour maps, `PlatformBadge`, `getPlatformConfig`,\n * the hub's `AppConfig.brandColors`, the hub's manifest generator and `theme-color`.\n *\n * JSX-FREE + zero-dependency ON PURPOSE: it carries its own tsup entry and\n * `exports` subpath (`@flamingo-stack/openframe-frontend-core/utils/platform-identity`)\n * so hub SCRIPTS and `server-only` modules can import it without dragging React,\n * the icon components, or the utils barrel into their graph.\n *\n * TWO accepted by-construction copies of the colour decision remain, both LOCKED\n * by `src/__tests__/mlg-platform-leaves.test.ts` in this package:\n * 1. `platformHexColors` below (hand-typed hex mirror of each accent token).\n * 2. The `[data-app-type='…']` blocks in `src/styles/ods-colors.css`.\n */\n\nimport type { PlatformName } from '../types/platform';\n\n// ── Strings ─────────────────────────────────────────────────────────────────\n\n/** Human-readable platform names. THE naming source for lib-side presentation. */\nexport const platformDisplayNames = {\n openmsp: 'OpenMSP',\n openframe: 'OpenFrame',\n flamingo: 'Flamingo',\n 'flamingo-teaser': 'Flamingo Teaser',\n 'marketing-hub': 'Flamingo Marketing Hub',\n 'product-hub': 'Flamingo Product Hub',\n 'revenue-hub': 'Flamingo Revenue Hub',\n 'people-hub': 'Flamingo People Hub',\n 'company-hub': 'Flamingo Company Hub',\n tmcg: 'TMCG',\n mlg: 'Major League GitHub',\n universal: 'Universal',\n};\n\n/**\n * Short-name OVERRIDES only. A platform without a row falls back to its display\n * name, so this map never lists a name that equals `platformDisplayNames`.\n */\nexport const platformShortNames: Partial<Record<PlatformName, string>> = {\n mlg: 'MLG',\n};\n\nexport const platformDescriptions = {\n openmsp:\n 'Comprehensive directory and comparison platform for managed service providers (MSPs) and technology vendors. Reduce vendor costs and discover open-source alternatives.',\n openframe: 'AI-driven open-source security operations center (SOC) and endpoint detection platform for MSPs.',\n flamingo:\n 'AI-driven open-source OS for MSPs. Swap bloated vendor tools for open ones. Automate the boring crap. Take your margin back.',\n 'flamingo-teaser': 'Preview of Flamingo - the AI-driven open-source OS for MSPs.',\n tmcg: 'The Miami Cyber Gang - A cybersecurity community focused on education and collaboration.',\n mlg: 'Scouting report for US open-source talent. Rank GitHub contributors by programming language, city, state, region, and nearest Major League Soccer club.',\n universal: 'Cross-platform universal content.',\n};\n\nexport const platformSlogans = {\n openmsp: 'Find Your Perfect MSP Partner',\n openframe: 'Open-Source Security Operations',\n flamingo: 'Open-Source OS for MSPs',\n 'flamingo-teaser': 'Coming Soon: Open-Source OS for MSPs',\n tmcg: 'Miami Cyber Community',\n mlg: 'GitHub Scouting Report: Major League Edition',\n universal: 'Universal Platform',\n};\n\nexport const platformIconNames = {\n openmsp: 'openmsp-logo',\n openframe: 'openframe-logo',\n flamingo: 'flamingo-logo',\n universal: 'globe',\n 'flamingo-teaser': 'flamingo-logo',\n 'marketing-hub': 'flamingo-logo',\n 'product-hub': 'flamingo-logo',\n 'revenue-hub': 'flamingo-logo',\n 'people-hub': 'flamingo-logo',\n 'company-hub': 'flamingo-logo',\n tmcg: 'tmcg-logo',\n mlg: 'mlg-logo',\n};\n\nexport function getPlatformDisplayName(platformName: string): string {\n return platformDisplayNames[platformName as keyof typeof platformDisplayNames] || platformName;\n}\n\n/** Compact label (nav chips, manifest `short_name`, JSON-LD `alternateName`). */\nexport function getPlatformShortName(platformName: string): string {\n return platformShortNames[platformName as PlatformName] ?? getPlatformDisplayName(platformName);\n}\n\nexport function getPlatformDescription(platformName: string): string {\n return platformDescriptions[platformName as keyof typeof platformDescriptions] || platformName;\n}\n\nexport function getPlatformSlogan(platformName: string): string {\n return platformSlogans[platformName as keyof typeof platformSlogans] || platformName;\n}\n\nexport function getDefaultIconForPlatform(platformName: string): string {\n return platformIconNames[platformName as keyof typeof platformIconNames] || platformIconNames.universal;\n}\n\n// ── Colour stems ────────────────────────────────────────────────────────────\n\n/**\n * The six brand/attention colours (plus the neutral) a platform may adopt, named\n * by STEM rather than by token so the token spelling has exactly one home below.\n */\nexport type OdsColorStem =\n 'flamingo-pink' | 'flamingo-cyan' | 'open-yellow' | 'success' | 'warning' | 'error' | 'text-secondary';\n\n/** Stem → ODS token name (without the `--ods-` prefix). THE token spelling. */\nexport const ODS_STEM_TOKENS: Record<OdsColorStem, string> = {\n 'flamingo-pink': 'flamingo-pink-base',\n 'flamingo-cyan': 'flamingo-cyan-base',\n 'open-yellow': 'open-yellow-base',\n success: 'attention-green-success',\n warning: 'attention-yellow-warning',\n error: 'attention-red-error',\n 'text-secondary': 'system-greys-grey',\n};\n\n/**\n * Stem → Tailwind class strings. STATIC LITERALS on purpose: the Tailwind\n * scanner reads source text, so a template-built class would be purged.\n * This is the ONLY place a stem-to-class literal exists.\n */\nexport const ODS_STEM_CLASSES: Record<OdsColorStem, { text: string; bg: string; bgSoft: string; borderSoft: string }> =\n {\n 'flamingo-pink': {\n text: 'text-ods-flamingo-pink',\n bg: 'bg-ods-flamingo-pink',\n bgSoft: 'bg-ods-flamingo-pink/10',\n borderSoft: 'border-ods-flamingo-pink/30',\n },\n 'flamingo-cyan': {\n text: 'text-ods-flamingo-cyan',\n bg: 'bg-ods-flamingo-cyan',\n bgSoft: 'bg-ods-flamingo-cyan/10',\n borderSoft: 'border-ods-flamingo-cyan/30',\n },\n 'open-yellow': {\n text: 'text-ods-open-yellow',\n bg: 'bg-ods-open-yellow',\n bgSoft: 'bg-ods-open-yellow/10',\n borderSoft: 'border-ods-open-yellow/30',\n },\n success: {\n text: 'text-ods-success',\n bg: 'bg-ods-success',\n bgSoft: 'bg-ods-success/10',\n borderSoft: 'border-ods-success/30',\n },\n warning: {\n text: 'text-ods-warning',\n bg: 'bg-ods-warning',\n bgSoft: 'bg-ods-warning/10',\n borderSoft: 'border-ods-warning/30',\n },\n error: {\n text: 'text-ods-error',\n bg: 'bg-ods-error',\n bgSoft: 'bg-ods-error/10',\n borderSoft: 'border-ods-error/30',\n },\n 'text-secondary': {\n text: 'text-ods-text-secondary',\n bg: 'bg-ods-text-secondary',\n bgSoft: 'bg-ods-text-secondary/10',\n borderSoft: 'border-ods-text-secondary/30',\n },\n };\n\n// ── Theme + surface ─────────────────────────────────────────────────────────\n\n/**\n * Every platform ships the dark theme today (the hub's flamingo config says so\n * explicitly, and every `[data-app-type]` block inherits the dark `:root`).\n * ONE constant instead of a per-platform field: reintroduce the field only when\n * a platform actually differs.\n */\nexport const PLATFORM_THEME = 'dark' as const;\n\n/** The dark surface's tokens, consumed by config `brandColors` and the manifest. */\nexport const PLATFORM_SURFACE = {\n background: 'system-greys-background',\n text: 'system-greys-white',\n} as const;\n\nexport const getPlatformBackgroundVarName = (): string => `--ods-${PLATFORM_SURFACE.background}`;\nexport const getPlatformTextVarName = (): string => `--ods-${PLATFORM_SURFACE.text}`;\n\n// ── Brand record ────────────────────────────────────────────────────────────\n\nexport interface PlatformBrand {\n /** Primary brand colour (`--color-accent-primary` in the platform's CSS block). */\n accentStem: OdsColorStem;\n /** Link colour (`--color-link`). Equals `accentStem` for most platforms. */\n secondaryStem: OdsColorStem;\n}\n\n/**\n * THE accent/link table. Every row is verified against that platform's\n * `[data-app-type='…']` block in `ods-colors.css` by the hub's parity test.\n */\nexport const PLATFORM_BRAND: Record<PlatformName, PlatformBrand> = {\n openframe: { accentStem: 'open-yellow', secondaryStem: 'flamingo-cyan' },\n openmsp: { accentStem: 'open-yellow', secondaryStem: 'open-yellow' },\n flamingo: { accentStem: 'flamingo-pink', secondaryStem: 'flamingo-pink' },\n 'flamingo-teaser': { accentStem: 'flamingo-pink', secondaryStem: 'flamingo-pink' },\n tmcg: { accentStem: 'flamingo-pink', secondaryStem: 'flamingo-pink' },\n 'marketing-hub': { accentStem: 'flamingo-pink', secondaryStem: 'flamingo-pink' },\n 'product-hub': { accentStem: 'success', secondaryStem: 'success' },\n 'revenue-hub': { accentStem: 'warning', secondaryStem: 'warning' },\n 'people-hub': { accentStem: 'flamingo-cyan', secondaryStem: 'flamingo-cyan' },\n 'company-hub': { accentStem: 'error', secondaryStem: 'error' },\n mlg: { accentStem: 'flamingo-pink', secondaryStem: 'flamingo-cyan' },\n universal: { accentStem: 'text-secondary', secondaryStem: 'text-secondary' },\n};\n\n/** The brand row for a platform, falling back to `universal` for anything unknown. */\nexport function getPlatformBrand(platformName: string): PlatformBrand {\n return PLATFORM_BRAND[platformName as PlatformName] ?? PLATFORM_BRAND.universal;\n}\n\nexport function platformAccentToken(platformName: string): string {\n return ODS_STEM_TOKENS[getPlatformBrand(platformName).accentStem];\n}\n\nexport function platformSecondaryToken(platformName: string): string {\n return ODS_STEM_TOKENS[getPlatformBrand(platformName).secondaryStem];\n}\n\n/** `--ods-…` CSS variable NAME for a platform's accent (input to `resolveOdsColor`). */\nexport function platformAccentVarName(platformName: string): string {\n return `--ods-${platformAccentToken(platformName)}`;\n}\n\nexport function platformSecondaryVarName(platformName: string): string {\n return `--ods-${platformSecondaryToken(platformName)}`;\n}\n\n/** `var(--ods-…)` VALUE for a platform's accent (config `brandColors` data). */\nexport function getPlatformAccentColor(platformName: string): string {\n return `var(${platformAccentVarName(platformName)})`;\n}\n\nexport function getPlatformSecondaryColor(platformName: string): string {\n return `var(${platformSecondaryVarName(platformName)})`;\n}\n\n/** Tailwind classes for a platform, DERIVED from its stems. */\nexport function getPlatformBrandClasses(platformName: string): {\n accentText: string;\n accentBg: string;\n accentBgSoft: string;\n accentBorderSoft: string;\n secondaryText: string;\n} {\n const brand = getPlatformBrand(platformName);\n const accent = ODS_STEM_CLASSES[brand.accentStem];\n const secondary = ODS_STEM_CLASSES[brand.secondaryStem];\n return {\n accentText: accent.text,\n accentBg: accent.bg,\n accentBgSoft: accent.bgSoft,\n accentBorderSoft: accent.borderSoft,\n secondaryText: secondary.text,\n };\n}\n\n/** Platform background classes, DERIVED (was a hand-typed hex map). */\nexport const platformColors: Record<PlatformName, string> = Object.fromEntries(\n (Object.keys(PLATFORM_BRAND) as PlatformName[]).map(p => [p, getPlatformBrandClasses(p).accentBg]),\n) as Record<PlatformName, string>;\n\nexport function getPlatformColor(platformName: string) {\n return platformColors[platformName as PlatformName] || platformColors.universal;\n}\n\n/**\n * Hand-typed hex mirror of each platform's accent token (the DB `default_color`\n * and other JS-value consumers). Accepted copy #1 — locked by\n * `src/__tests__/mlg-platform-leaves.test.ts` in this package, which asserts\n * every row against the `[data-app-type]` blocks in `ods-colors.css`.\n * (The hub has its OWN parity test, but it holds different copies: the asset\n * generator's map and the generated manifests.)\n */\nexport const platformHexColors: Record<PlatformName, string> = {\n openmsp: '#ffc008',\n openframe: '#ffc008',\n flamingo: '#f357bb',\n universal: '#888888',\n 'flamingo-teaser': '#f357bb',\n 'marketing-hub': '#f357bb',\n 'product-hub': '#5ea62e',\n 'revenue-hub': '#e1b32f',\n 'people-hub': '#5efaf0',\n 'company-hub': '#f36666',\n tmcg: '#f357bb',\n mlg: '#f357bb',\n};\n\nexport function getDefaultColorForPlatform(platformName: string): string {\n return platformHexColors[platformName as PlatformName] || platformHexColors.universal;\n}\n","// Stub app config\nexport const APP_CONFIG = {\n app: {\n type: 'openmsp',\n name: 'OpenMSP',\n domain: 'openmsp.ai',\n },\n features: {\n announcements: true,\n notifications: true,\n },\n} as const;\n\nexport function getAppConfig() {\n return APP_CONFIG;\n}\n\nexport function getAppType() {\n // The exact `process.env.NEXT_PUBLIC_APP_TYPE` member expression is kept so\n // Next/webpack can statically inline it. The try/catch makes the call safe\n // in non-Next React hosts (Vite/CRA embeds) where `process` does not exist\n // at runtime — those hosts get the 'openmsp' fallback, which is fine for\n // embeds: dismissal cookies are domain-scoped and an embed domain serves\n // one platform's announcements.\n try {\n return process.env.NEXT_PUBLIC_APP_TYPE || 'openmsp';\n } catch {\n return 'openmsp';\n }\n}\n","/**\n * Thin JSON-typed Web-Storage adapter (localStorage or sessionStorage).\n *\n * Centralizes the SSR-guard + try/catch + silent quota-failure pattern\n * that every per-feature storage util would otherwise re-implement.\n *\n * Optional `namespace` prefix supports platform / user partitioning —\n * the resolver runs lazily at call time so the namespace can vary across\n * the lifetime of the page (e.g. proxy-auth switches user, which switches\n * the key suffix).\n *\n * Backend selection (`backend: 'local' | 'session'`):\n * - `'local'` (default): persists across browser sessions. Use for\n * UI state, chat history metadata, feature-flag opt-ins.\n * - `'session'`: cleared when the tab closes. Use for ANY auth-adjacent\n * value (bearer tokens, act-as identity, proxy credentials). Reduces\n * the XSS-exfiltration attack window from \"indefinite\" to \"until tab\n * close\" without losing per-session ergonomics.\n */\n\nexport type WebStorageBackend = 'local' | 'session';\n\nexport interface LocalStorageAdapter<T> {\n load(): T | null;\n save(value: T): void;\n clear(): void;\n /** Resolved storage key for the current call. Useful for tests. */\n resolveKey(): string;\n}\n\nexport interface LocalStorageAdapterOptions<T> {\n /** Base storage key. Combined with `namespace()` when provided. */\n key: string;\n /** Optional dynamic namespace prefix appended via `.` separator.\n * Called on EVERY read/write so the key can vary across the page\n * lifetime (e.g. when the platform or user identity changes). */\n namespace?: () => string | null | undefined;\n /** Runtime shape check. Falsey return → `load()` yields null. */\n validate?: (parsed: unknown) => parsed is T;\n /** Diagnostic prefix written to `console.warn` on parse / write\n * failures. Defaults to `'[local-storage]'`. */\n logTag?: string;\n /** Which Web-Storage backend to use. Defaults to `'local'`. Pass\n * `'session'` for anything auth-adjacent so the value evaporates\n * when the tab closes. */\n backend?: WebStorageBackend;\n}\n\nfunction getStorage(backend: WebStorageBackend): Storage | null {\n if (typeof window === 'undefined') return null;\n try {\n return backend === 'session' ? window.sessionStorage : window.localStorage;\n } catch {\n // Some sandboxed contexts (Safari private mode older versions,\n // strict CSP) throw on storage access — treat as unavailable.\n return null;\n }\n}\n\nexport function createLocalStorageAdapter<T>(options: LocalStorageAdapterOptions<T>): LocalStorageAdapter<T> {\n const tag = options.logTag ?? '[local-storage]';\n const backend: WebStorageBackend = options.backend ?? 'local';\n const resolveKey = (): string => {\n const ns = options.namespace?.();\n return ns ? `${ns}.${options.key}` : options.key;\n };\n\n return {\n resolveKey,\n load() {\n const storage = getStorage(backend);\n if (!storage) return null;\n try {\n const raw = storage.getItem(resolveKey());\n if (!raw) return null;\n const parsed = JSON.parse(raw) as unknown;\n if (options.validate && !options.validate(parsed)) return null;\n return parsed as T;\n } catch (err) {\n console.warn(`${tag} parse failed for key ${resolveKey()}:`, err);\n return null;\n }\n },\n save(value: T) {\n const storage = getStorage(backend);\n if (!storage) return;\n try {\n storage.setItem(resolveKey(), JSON.stringify(value));\n } catch (err) {\n console.warn(`${tag} write failed for key ${resolveKey()}:`, err);\n }\n },\n clear() {\n const storage = getStorage(backend);\n if (!storage) return;\n try {\n storage.removeItem(resolveKey());\n } catch (err) {\n console.warn(`${tag} clear failed for key ${resolveKey()}:`, err);\n }\n },\n };\n}\n","'use client';\n\n/**\n * Client-side persistence for embed-surface proxy credentials (a\n * PLATFORM API KEY + impersonation email). Used by every embedded\n * surface — the chat widget AND the ticket center AND any future\n * embedded React component that needs to identify itself as the\n * impersonated customer.\n *\n * When set, the surface attaches the creds as\n * `Authorization: Bearer <platform API key>` + `X-Chat-Act-As: <email>`\n * on every call to `/api/docs/chat`, `/api/chat/*`, and any other route\n * gated by `requireChatAuth` — proving to the server that this session\n * is acting on behalf of <email>. The key is minted in the hub's\n * `/admin/api-keys` and is platform-bound (`fpk_<platform>_…`): a\n * SERVICE key may act as any email; a PERSONAL key only as its owner.\n *\n * **Naming history:** the stored field is still named `secret` and the\n * wire-side header names are `X-Chat-*`. Those are client/server\n * contracts predating the per-platform key migration (the old shared\n * `CHAT_PROXY_SECRET` env is removed hub-side); renaming them would\n * break persisted state + the wire shape for zero behavior change. The\n * CLIENT-side helpers were renamed `Embed*` so non-chat surfaces (e.g.\n * ticket center) don't have to import a chat-prefixed symbol just to\n * send the same headers.\n *\n * Persists to **`localStorage`** so the bearer token + act-as identity\n * survive tab close, new-tab opens, and browser restarts — the\n * `/debug` paste-creds UI is an admin tool and re-pasting every tab\n * cycle was rejected as a dev-experience tradeoff that wasn't worth\n * the security gain. An XSS sink on this origin can read the value\n * indefinitely (vs only-this-tab with sessionStorage), but `/debug`\n * is admin-gated behind the platform's `askAI.enabled` flag, the key\n * is server-verified (hash-at-rest, platform-bound, revocable in\n * `/admin/api-keys`), and the act-as reach is bounded by the key's\n * tier. Explicit \"Clear\" button on the creds bar is the supported\n * logout path; closing the tab is no longer.\n *\n * Namespaced under `<platform>.chat.proxy-auth.v1` (the storage key is\n * unchanged from the old chat-prefixed helper — that's a storage\n * contract; renaming it would log everyone out).\n */\n\nimport { getAppType } from './app-config';\nimport { createLocalStorageAdapter } from './local-storage-adapter';\n\n/**\n * Every OPTIONAL header of the chat-proxy contract, with the field it is stored\n * in: the ONE list storage, validation and `applyProxyAuth` all loop over, so a\n * new header is one row. `identity` rows are the acting user's display identity\n * (the hub threads them through `resolveChatProxyIdentity`); `visitor` rows are\n * the visitor context a gateway forwards (honoured hub-side only from a SERVICE\n * key). Empty or omitted = not sent.\n */\nexport const EMBED_PROXY_OPTIONAL_HEADERS = [\n { field: 'firstName', group: 'identity', header: 'X-Chat-First-Name' },\n { field: 'lastName', group: 'identity', header: 'X-Chat-Last-Name' },\n { field: 'avatarUrl', group: 'identity', header: 'X-Chat-Avatar-Url' },\n { field: 'originUrl', group: 'visitor', header: 'X-Chat-Origin-Url' },\n { field: 'ip', group: 'visitor', header: 'X-Chat-Ip' },\n { field: 'country', group: 'visitor', header: 'X-Chat-Country' },\n { field: 'visitorId', group: 'visitor', header: 'X-Chat-User-Id' },\n] as const;\n\nexport type EmbedProxyOptionalField = (typeof EMBED_PROXY_OPTIONAL_HEADERS)[number]['field'];\n\nexport type EmbedProxyAuth = {\n secret: string;\n email: string;\n} & Partial<Record<EmbedProxyOptionalField, string>>;\n\nfunction isValidPersistedAuth(value: unknown): value is EmbedProxyAuth {\n if (!value || typeof value !== 'object') return false;\n const v = value as Record<string, unknown>;\n if (\n typeof v.secret !== 'string' ||\n v.secret.trim().length === 0 ||\n typeof v.email !== 'string' ||\n v.email.trim().length === 0\n )\n return false;\n // Optional fields: when present must be strings. Empty string is treated\n // as absent later (in `getEmbedProxyAuth`).\n return EMBED_PROXY_OPTIONAL_HEADERS.every(({ field }) => v[field] == null || typeof v[field] === 'string');\n}\n\nconst adapter = createLocalStorageAdapter<EmbedProxyAuth>({\n // Storage key unchanged from the legacy chat-prefixed helper. Renaming\n // it would silently log every existing admin out — the key is a\n // storage contract, not a code identifier.\n key: 'chat.proxy-auth.v1',\n namespace: () => getAppType(),\n validate: isValidPersistedAuth,\n logTag: '[embed-proxy-auth-storage]',\n // localStorage — survives tab close, new tabs, and browser restarts.\n // Admin re-pasting creds every tab cycle was the dev-experience\n // tradeoff prior `sessionStorage` setup demanded — rejected. See\n // file-level doc comment for the security tradeoff rationale.\n backend: 'local',\n});\n\n/** Trim + null-coerce an optional identity field so consumers can do\n * `auth.firstName ?? ''` without worrying about whitespace-only strings. */\nfunction normalizeOptional(value: string | undefined): string | undefined {\n if (!value) return undefined;\n const trimmed = value.trim();\n return trimmed.length > 0 ? trimmed : undefined;\n}\n\n/** Every optional field of `value`, trimmed, blank ones dropped. */\nfunction normalizedOptionalFields(value: Partial<Record<EmbedProxyOptionalField, string>>) {\n const out: Partial<Record<EmbedProxyOptionalField, string>> = {};\n for (const { field } of EMBED_PROXY_OPTIONAL_HEADERS) {\n const normalized = normalizeOptional(value[field]);\n if (normalized) out[field] = normalized;\n }\n return out;\n}\n\n/**\n * Returns full credentials (secret + email + optional identity passthrough)\n * when secret + email are available. Returns `null` when nothing is saved —\n * callers treat that as \"fall back to cookie auth\".\n */\nexport function getEmbedProxyAuth(): EmbedProxyAuth | null {\n const persisted = adapter.load();\n if (!persisted) return null;\n return {\n secret: persisted.secret,\n email: persisted.email.trim().toLowerCase(),\n ...normalizedOptionalFields(persisted),\n };\n}\n\n/**\n * Returns the LAST email the admin saved. The proxy creds bar reads\n * this to pre-fill the email field on mount.\n */\nexport function getPersistedProxyEmail(): string | null {\n const persisted = adapter.load();\n return persisted?.email.trim().toLowerCase() ?? null;\n}\n\n/** Save the proxy creds. Secret + email are required; identity-passthrough\n * fields are persisted only when non-empty. */\nexport function setEmbedProxyAuth(value: EmbedProxyAuth): void {\n adapter.save({\n secret: value.secret,\n email: value.email.trim().toLowerCase(),\n ...normalizedOptionalFields(value),\n });\n}\n\n/** Drop the persisted creds. */\nexport function clearEmbedProxyAuth(): void {\n adapter.clear();\n}\n\n/**\n * Apply the embed-proxy auth (Bearer + X-Chat-Act-As) to a fetch call's\n * URL + headers. Used by every embedded-surface route that needs to\n * identify itself as the proxied customer (chat stream, agent-* routes,\n * ticket-center actions). When proxy auth is absent (regular\n * cookie-session users), returns the inputs unchanged so the cookie-auth\n * path still works.\n *\n * `X-Chat-Act-As` header (vs a URL query param) keeps PII out of access\n * logs, Sentry breadcrumbs, browser history, and CDN analytics.\n */\nexport function applyProxyAuth(\n url: string,\n baseHeaders: Record<string, string> = { 'Content-Type': 'application/json' },\n): { url: string; headers: Record<string, string> } {\n const auth = getEmbedProxyAuth();\n const headers = { ...baseHeaders };\n if (auth?.secret) {\n headers.Authorization = `Bearer ${auth.secret}`;\n }\n if (auth?.email) {\n headers['X-Chat-Act-As'] = auth.email;\n }\n // Optional headers — only attached when present so the server's\n // \"required vs optional\" header shape stays exact.\n for (const { field, header } of EMBED_PROXY_OPTIONAL_HEADERS) {\n const value = auth?.[field];\n if (value) headers[header] = value;\n }\n return { url, headers };\n}\n","'use client';\n\n/**\n * Shared `fetch` wrapper for any embedded surface (chat, ticket center,\n * future widgets) that needs to carry the bearer-act-as identity\n * (proxy `Authorization` + `X-Chat-Act-As` headers from\n * `embed-proxy-auth-storage.ts`).\n *\n * Wire header names are `X-Chat-*` for historical reasons — that's a\n * server contract, not a UI namespace. The wrapper itself is generic.\n *\n * Drop-in replacement for `fetch()` — `Authorization` / `X-Chat-Act-As`\n * are merged into `init.headers` when proxy creds are stashed in\n * sessionStorage, otherwise the call falls through to the cookie-auth\n * path unchanged.\n *\n * Use this for any client-side fetch hitting `/api/chat/*`, `/api/docs/chat/*`,\n * or `/api/storage/generate-upload-url` (chat-attachment surface — shared\n * with the ticket center). Routes that do NOT need bearer-act-as\n * (e.g. `/api/profile/me`) keep using vanilla `fetch`.\n */\n\nimport { applyProxyAuth } from './embed-proxy-auth-storage';\n\n// =============================================================================\n// Host-supplied auth adapter (opt-in)\n// =============================================================================\n\n/**\n * Hosts that have their own auth model (cookie sessions, app-specific\n * JWT in localStorage, OAuth access tokens, …) can register an adapter\n * to override the lib's default `embedProxyAuth` flow. When set, the\n * adapter's `getHeaders()` result is merged onto every `embedAuthedFetch`\n * call AFTER the default proxy-auth header step (so adapter headers\n * win over both caller and proxy values), and `credentials` overrides\n * the default `'same-origin'` behaviour.\n *\n * Default (no adapter): MPH-style proxy-impersonation — bearer + act-as\n * read from localStorage, `credentials: 'same-origin'`. No consumer\n * needs to touch this unless they want a different auth model.\n *\n * Use cases:\n * - openframe-frontend has its own JWT in `localStorage.of_access_token`\n * and cookie-based session; register an adapter to attach the JWT\n * and request `credentials: 'include'` so cookies travel cross-origin\n * to the openframe gateway.\n * - Future embed hosts with OAuth access tokens, signed URLs, etc.\n *\n * Lifetime: setter is module-level (intentionally — `embedAuthedFetch`\n * is a plain utility, not a hook, so it can't read React context). Host\n * runtime providers should call `setEmbedAuthAdapter(...)` on mount and\n * `setEmbedAuthAdapter(null)` on unmount. Multiple hosts registering at\n * once is a programming error (one chat panel per app).\n */\nexport interface EmbedAuthAdapter {\n /** Headers merged onto every embedded-fetch call. Return `{}` to add\n * nothing. Called per-request so reactive token refresh sees the latest\n * value from your auth store / storage. Values typed as\n * `string | undefined` so the common narrowed shape\n * `{ Authorization: token ? 'Bearer …' : undefined }` (or a conditional\n * `token ? { Authorization: … } : {}`) assigns cleanly — `undefined`\n * values are filtered before being merged into the request headers. */\n getHeaders?: () => Record<string, string | undefined>;\n /** `RequestInit.credentials` mode. Default when no adapter: callers'\n * `init.credentials` or `'same-origin'`. Use `'include'` for cookie\n * auth against a different origin (CORS + `SameSite=None` required). */\n credentials?: RequestCredentials;\n /**\n * Optional 401 self-heal. When a request comes back `401`,\n * `embedAuthedFetch` calls this once, and — if it resolves `true` —\n * retries the SAME request exactly once with freshly-recomputed\n * headers (so a rotated bearer from `getHeaders()` is picked up).\n * Resolve `false` to surface the 401 to the caller unchanged.\n *\n * This is the capability the openframe `apiClient` has had all along\n * (refresh-the-access-token-then-retry); registering it here gives the\n * embedded chat/ticket surfaces the same self-healing auth instead of\n * dying on an expired token. Concurrent 401s are de-duplicated by the\n * wrapper, so this fires at most once per refresh cycle even when a\n * stampede of chat requests all expire together — your implementation\n * does NOT need its own in-flight guard (though a token-refresh manager\n * that already dedups is harmless).\n *\n * Keep it idempotent and side-effect-light: on failure the wrapper just\n * returns the original 401 — logout/redirect decisions belong to the\n * host's own auth layer, not to this fetch wrapper.\n */\n refresh?: () => Promise<boolean>;\n /**\n * Exact extra origins (`scheme://host[:port]`, as produced by `URL.origin`)\n * the same-origin guard additionally accepts. The guard normally rejects\n * every cross-origin URL in production — but native-shell hosts serve the\n * page from a local pseudo-origin (`capacitor://localhost`,\n * `tauri://localhost`) with NO server behind it, so the gateway their\n * bearer already belongs to is unavoidably cross-origin. Listing it here\n * is the host's explicit sanction to send the adapter's credentials there.\n * Compared AFTER the http(s)-protocol check, so non-http(s) schemes can\n * never be allowlisted. Omit (or leave empty) everywhere else — same-origin\n * remains the rule, this is the narrow exception.\n */\n allowedOrigins?: string[];\n /**\n * Optional: call `listener` whenever `getHeaders()` starts returning a NEW\n * credential (a rotation the host ran on its own — e.g. a native shell's\n * background refresh). Returns an unsubscribe. Lets a long-lived surface\n * that stopped after a 401 retry as soon as a usable credential exists,\n * instead of on its next backoff step. Fire only on a real change: a\n * listener may respond with a request, and through `refresh` that request\n * can lead the host to notify again.\n */\n subscribe?: (listener: () => void) => () => void;\n}\n\n/**\n * The registered adapter is parked on `globalThis`, NOT in a module-private\n * `let`. Reason: this lib ships multiple entry points (`/utils`,\n * `/components/chat`, …) and a consumer's bundler can inline this file into\n * more than one chunk — giving each chunk its OWN module scope. If the host\n * calls `setEmbedAuthAdapter` from the `/utils` copy while the chat's\n * `embedAuthedFetch` runs from the `/components/chat` copy, a module-local\n * `let` would be set on one copy and read as `null` on the other (the exact\n * \"credentials: same-origin, no Bearer, no refresh\" symptom). A single\n * `globalThis` slot is shared across every copy, so registration always\n * reaches the fetch path.\n */\nconst ADAPTER_GLOBAL_KEY = '__embedAuthedFetchAdapter__';\n\nfunction getRegisteredAuthAdapter(): EmbedAuthAdapter | null {\n if (typeof globalThis === 'undefined') return null;\n return ((globalThis as Record<string, unknown>)[ADAPTER_GLOBAL_KEY] as EmbedAuthAdapter | null) ?? null;\n}\n\nfunction storeRegisteredAuthAdapter(adapter: EmbedAuthAdapter | null): void {\n if (typeof globalThis === 'undefined') return;\n (globalThis as Record<string, unknown>)[ADAPTER_GLOBAL_KEY] = adapter;\n}\n\n/**\n * Register a host-owned auth adapter for `embedAuthedFetch`. Pass `null`\n * to clear (typically on provider unmount).\n *\n * Module-level state — there is one chat panel per app, so a single\n * registration is sufficient. Calling this twice with different non-null\n * adapters replaces the previous one (the most recent registration wins);\n * a `console.warn` flags the overwrite so duplicate-provider mounts get\n * caught in dev.\n */\nexport function setEmbedAuthAdapter(adapter: EmbedAuthAdapter | null): void {\n if (adapter && getRegisteredAuthAdapter() && process.env.NODE_ENV !== 'production') {\n console.warn(\n '[setEmbedAuthAdapter] overwriting a previously-registered auth ' +\n 'adapter. Two chat-runtime providers should not coexist — verify ' +\n 'mount order and pass `null` from the unmounting provider.',\n );\n }\n storeRegisteredAuthAdapter(adapter);\n}\n\n/**\n * Whether a host auth adapter is currently registered. Lets sibling helpers\n * (e.g. `contentFetch`) route through `embedAuthedFetch` ONLY when a host has\n * opted into embedded auth, and stay a plain `fetch` otherwise — so there's a\n * single auth knob (the adapter), not a second content-fetch registration.\n */\nexport function hasEmbedAuthAdapter(): boolean {\n return getRegisteredAuthAdapter() !== null;\n}\n\n/**\n * Subscribe to the registered adapter's credential changes\n * (`EmbedAuthAdapter.subscribe`). A no-op when no adapter is registered, or\n * the adapter doesn't report changes. Reads the adapter once, at call time:\n * an adapter registered (or replaced) after a subscriber starts is not\n * picked up — register it before the surfaces that need it mount.\n */\nexport function subscribeEmbedCredentialChange(listener: () => void): () => void {\n return getRegisteredAuthAdapter()?.subscribe?.(listener) ?? (() => {});\n}\n\n/**\n * Whether `url` is an asset the browser CANNOT load natively because its\n * auth rides in request headers.\n *\n * Native asset loads (`<img src>`, `<track src>`, CSS `background-image`)\n * can't carry custom headers, so a URL this returns `true` for must go\n * through `embedAuthedFetch` → blob object-URL instead (see\n * `useAuthedAssetSrc` / `useAuthedImageSrc`).\n *\n * The single signal is the registered adapter: when it supplies an\n * `Authorization` header, the host authenticates by HEADER, so nothing the\n * browser fetches on its own is authenticated. That holds for BOTH shapes a\n * header-auth host takes:\n *\n * - a cross-origin gateway URL — sanctioned via `allowedOrigins` (native\n * shells, whose page origin is `capacitor://` / `tauri://`);\n * - a same-origin / relative reverse-proxy path (`/content/api/...`) —\n * dev-ticket web, where the token lives in localStorage and there is NO\n * session cookie to fall back on. This case used to be excluded on the\n * assumption that same-origin implies \"cookies work\"; true for a cookie\n * session, false for every header-auth host, which is exactly when this\n * predicate is consulted at all.\n *\n * Everything else loads natively, unchanged: cookie-auth web (the adapter\n * supplies no `Authorization`), no adapter at all (the hub), and third-party\n * origins the bearer does NOT belong to (public images) — those stay out of\n * reach of the token by the same origin rules `embedAuthedFetch` enforces.\n */\nexport function needsBearerAssetFetch(url: string): boolean {\n if (typeof window === 'undefined') return false;\n const adapter = getRegisteredAuthAdapter();\n if (!adapter || adapter.getHeaders?.().Authorization === undefined) return false;\n let target: URL;\n let pageOrigin: string;\n try {\n target = new URL(url, window.location.href);\n // Same reason `assertSameOrigin` derives it this way: test environments\n // mock `window.location` as a plain object with no `origin` field.\n pageOrigin = new URL(window.location.href).origin;\n } catch {\n return false;\n }\n if (target.protocol !== 'http:' && target.protocol !== 'https:') return false;\n if (target.origin === pageOrigin) return true;\n return adapter.allowedOrigins?.includes(target.origin) ?? false;\n}\n\n/**\n * `fetch` wrapper that attaches embed-proxy bearer headers (when\n * present in sessionStorage) and forces `credentials: 'same-origin'`\n * so Supabase auth cookies travel too.\n *\n * **Header merge direction (proxy WINS over caller):** the implementation\n * spreads `baseHeaders` first inside `applyProxyAuth`, then sets the\n * `Authorization` / `X-Chat-*` keys — so the proxy values take precedence\n * over anything the caller passed. The motivation is that the bearer +\n * act-as identity is the source of truth for embedded auth; a caller\n * accidentally passing a stale `Authorization` header should NOT override\n * the live proxy creds.\n *\n * **Cross-origin defense:** the wrapper assumes a same-origin `/api/…`\n * relative URL. Absolute URLs are accepted only when their origin matches\n * the current window's origin — or appears in the registered adapter's\n * `allowedOrigins` (the native-shell hatch; see that field's doc) — and\n * cross-origin URLs otherwise throw before the bearer leaves the page.\n * This is a defense-in-depth guard for future call sites — outside the\n * allowlisted-shell case there is no legitimate cross-origin use of this\n * fetch wrapper.\n *\n * **401 self-heal:** when a registered adapter supplies `refresh`, a `401`\n * response triggers a single token refresh + retry of the same request\n * (see `EmbedAuthAdapter.refresh`). This is the openframe `apiClient`'s\n * refresh-then-retry behaviour, lifted into the lib so embedded surfaces\n * no longer need a host-side `window.fetch` monkey-patch to survive an\n * expired access token mid-chat. With no adapter (or no `refresh`), the\n * 401 passes straight through unchanged.\n */\nexport function embedAuthedFetch(url: string, init: RequestInit = {}): Promise<Response> {\n // Same-origin guard runs SYNCHRONOUSLY (not awaited inside the async\n // helper below) so a bearer-leaking cross-origin URL throws before any\n // promise is created — callers and tests rely on the synchronous throw.\n assertSameOrigin(url);\n\n // `applyProxyAuth` accepts `Record<string, string>`; normalize the\n // caller's headers to that shape ONCE, up front. RequestInit accepts\n // `HeadersInit` which is broader (Headers instance OR array of tuples).\n // We re-derive the per-request headers from this base on every attempt\n // (initial + post-refresh retry) so a rotated bearer is picked up.\n //\n // When the caller passes no headers, fall back to the same default\n // `applyProxyAuth` uses internally — `Content-Type: application/json` —\n // so JSON POSTs keep their content-type when only `embedAuthedFetch(url)`\n // is used at the call site. GET callers that explicitly want no body\n // headers can pass `init.headers = {}` to opt out.\n let baseHeaders: Record<string, string>;\n if (init.headers === undefined) {\n baseHeaders = { 'Content-Type': 'application/json' };\n } else {\n baseHeaders = {};\n if (init.headers instanceof Headers) {\n init.headers.forEach((v, k) => {\n baseHeaders[k] = v;\n });\n } else if (Array.isArray(init.headers)) {\n for (const [k, v] of init.headers) baseHeaders[k] = v;\n } else {\n Object.assign(baseHeaders, init.headers);\n }\n }\n\n return fetchWithRefresh(url, init, baseHeaders, false);\n}\n\n/**\n * Single in-flight refresh shared across all concurrent `embedAuthedFetch`\n * callers. A stampede of chat requests that all 401 at the same moment must\n * trigger the adapter's `refresh()` ONCE, not N times — otherwise an\n * expiring session fires a thundering herd of refresh calls at the auth\n * server. Resets to `null` once settled so the next genuine expiry can\n * refresh again.\n */\n// Stored on `globalThis` rather than a module-local so the \"single refresh\"\n// guarantee survives module duplication. Bundlers can ship more than one copy\n// of this module (e.g. across chunks or a host + embedded build); a per-module\n// variable would let each copy run its own refresh cycle, re-creating the\n// thundering-herd this dedupe exists to prevent.\nconst IN_FLIGHT_REFRESH_GLOBAL_KEY = '__embedAuthedFetchInFlightRefresh__';\n\nfunction getInFlightRefresh(): Promise<boolean> | null {\n if (typeof globalThis === 'undefined') return null;\n return (\n ((globalThis as Record<string, unknown>)[IN_FLIGHT_REFRESH_GLOBAL_KEY] as Promise<boolean> | null | undefined) ??\n null\n );\n}\n\nfunction setInFlightRefresh(refresh: Promise<boolean> | null): void {\n if (typeof globalThis === 'undefined') return;\n (globalThis as Record<string, unknown>)[IN_FLIGHT_REFRESH_GLOBAL_KEY] = refresh;\n}\n\nfunction dedupedRefresh(): Promise<boolean> {\n const adapter = getRegisteredAuthAdapter();\n // Bound and captured BEFORE the closure below. `refresh` is an optional\n // property, so TypeScript drops the `if (!adapter?.refresh)` narrowing inside\n // the `.then()` callback — the property could in principle be reassigned\n // between the guard and the call. `bind` keeps the adapter as the receiver.\n const refresh = adapter?.refresh?.bind(adapter);\n if (!refresh) return Promise.resolve(false);\n let inFlightRefresh = getInFlightRefresh();\n if (!inFlightRefresh) {\n // Wrap in `Promise.resolve` so an adapter that throws synchronously\n // (rather than rejecting) still funnels through the shared slot and\n // clears it. A rejected refresh is treated as \"could not refresh\".\n inFlightRefresh = Promise.resolve()\n .then(() => refresh())\n .catch(() => false)\n .finally(() => {\n setInFlightRefresh(null);\n });\n setInFlightRefresh(inFlightRefresh);\n }\n return inFlightRefresh;\n}\n\n/**\n * Core fetch path: merge proxy-auth + adapter headers, issue the request,\n * and — on a `401` with a refresh-capable adapter — refresh once and retry\n * the identical request a single time. Mirrors the openframe `apiClient`'s\n * refresh-then-retry contract (`isRetry` guards against infinite loops).\n */\nasync function fetchWithRefresh(\n url: string,\n init: RequestInit,\n baseHeaders: Record<string, string>,\n isRetry: boolean,\n): Promise<Response> {\n // Re-run the merge each attempt: `applyProxyAuth` reads the latest stored\n // proxy creds and `getHeaders()` reads the latest bearer, so a retry after\n // refresh carries the rotated token rather than the stale one. `{...baseHeaders}`\n // keeps the caller's normalized headers immutable across attempts.\n const { url: authedUrl, headers } = applyProxyAuth(url, { ...baseHeaders });\n\n // Host-supplied auth adapter layer. Runs AFTER the proxy-auth merge so\n // adapter headers override both caller and proxy values — the adapter\n // is the host's explicit \"this is my auth model\" override, intentionally\n // last-writer-wins. When no adapter is registered, this is a zero-cost\n // no-op (object spread of `{}`).\n const adapter = getRegisteredAuthAdapter();\n if (adapter?.getHeaders) {\n // Filter `undefined` values — the adapter type allows them so consumers\n // don't have to narrow `{ Authorization: token ? '…' : undefined }`-shaped\n // returns, but `fetch` headers must be strings.\n for (const [k, v] of Object.entries(adapter.getHeaders())) {\n if (v !== undefined) headers[k] = v;\n }\n }\n const credentials = adapter?.credentials ?? init.credentials ?? 'same-origin';\n\n const response = await fetch(authedUrl, {\n ...init,\n headers,\n // Default `same-origin` carries Supabase cookies for the MPH proxy-\n // auth model. Hosts on different origins (openframe-frontend ↔\n // openframe gateway) register `credentials: 'include'` via the\n // adapter to make their own cookies travel cross-origin (CORS +\n // `SameSite=None` must be configured server-side for that to work).\n credentials,\n });\n\n // 401 self-heal: refresh the token once and retry. Only when an adapter\n // opted into `refresh`, and only on the first attempt — a 401 on the\n // retry means the fresh token is also unauthorized, so surface it.\n if (response.status === 401 && !isRetry && adapter?.refresh) {\n const refreshed = await dedupedRefresh();\n if (refreshed) {\n return fetchWithRefresh(url, init, baseHeaders, true);\n }\n }\n\n return response;\n}\n\n/**\n * Reject any URL that resolves to a cross-origin destination or to a\n * non-http(s) scheme. Every input is resolved against\n * `window.location.href` so the same rule covers path-only\n * (`/api/...`), absolute (`https://...`), protocol-relative\n * (`//host/...`), AND whitespace-prefixed forms (`\\t//evil.com/...`) —\n * the WHATWG fetch spec strips leading ASCII whitespace before\n * parsing, so any regex-based \"skip relative\" shortcut is bypassable\n * with a leading `\\t`/`\\n`/`\\r`/space. We resolve unconditionally\n * instead and compare origins.\n *\n * Also blocks `javascript:` / `data:` / `blob:` etc. — only `http(s):`\n * is allowed. This is explicit allowlisting rather than relying on\n * `origin === 'null'` to fall out wrong.\n *\n * Server-side rendering: when `typeof window === 'undefined'` we skip\n * the check — the bearer comes from sessionStorage which doesn't exist\n * on the server, so there's nothing to leak anyway.\n */\nfunction assertSameOrigin(url: string): void {\n if (typeof window === 'undefined') return;\n let target: URL;\n let pageOrigin: string;\n try {\n target = new URL(url, window.location.href);\n // Derive the page origin from `href` rather than reading\n // `window.location.origin` directly so the check works in test\n // environments that mock `window.location` to a plain object\n // without an `origin` field (jsdom setups do this).\n pageOrigin = new URL(window.location.href).origin;\n } catch {\n throw new Error(`embedAuthedFetch: refusing to fetch malformed URL (${JSON.stringify(url)})`);\n }\n if (target.protocol !== 'http:' && target.protocol !== 'https:') {\n throw new Error(\n `embedAuthedFetch: refusing non-http(s) URL (${target.protocol}) — pass a relative /api/* path instead`,\n );\n }\n if (target.origin !== pageOrigin) {\n // Host-sanctioned cross-origin target (`EmbedAuthAdapter.allowedOrigins`).\n // Native shells serve the page from a local pseudo-origin with no server\n // behind it, so their gateway — the same backend the host already trusts\n // with this bearer — is unavoidably cross-origin. Exact `URL.origin`\n // match, and only reachable for http(s) targets (checked above).\n if (getRegisteredAuthAdapter()?.allowedOrigins?.includes(target.origin)) {\n return;\n }\n // Dev-mode escape hatch — embedded apps (e.g. openframe-frontend)\n // run on a different origin from their gateway during local dev,\n // and forcing a Next.js `rewrites()` workaround is more error-prone\n // than relaxing the guard for the dev build. In production\n // (`NODE_ENV === 'production'`) the guard stays absolute — same\n // defense-in-depth bearer-leak protection as before. The check is\n // baked at build time by Next/webpack/Turbopack so prod bundles\n // contain only the throwing branch (no dev string in the artifact).\n if (process.env.NODE_ENV !== 'production') {\n console.warn(\n `[embedAuthedFetch] cross-origin fetch to ${target.origin} ` +\n `allowed in dev (NODE_ENV !== 'production'). Production builds ` +\n `will reject this — wire a same-origin proxy before shipping.`,\n );\n return;\n }\n throw new Error(\n `embedAuthedFetch: refusing cross-origin fetch to ${target.origin} — pass a relative /api/* path instead`,\n );\n }\n}\n","'use client';\n\nimport { useEffect, useReducer } from 'react';\nimport { embedAuthedFetch, needsBearerAssetFetch } from '../utils/embed-authed-fetch';\n\n/**\n * Resolve ANY asset URL the browser would load natively — `<img src>`,\n * `<track src>`, a font, a poster — for hosts whose auth rides in request\n * HEADERS instead of cookies: native shells (`capacitor://`, `tauri://`) and\n * dev-ticket web, both in bearer mode. A native subresource load can't carry\n * an `Authorization` header, so a gateway URL that works on the cookie-auth\n * web 401s there.\n *\n * When `needsBearerAssetFetch` says the URL is bearer-authed, this hook\n * fetches it through `embedAuthedFetch` (bearer + deduped 401-refresh-retry +\n * the adapter's cross-origin guard — the SAME single auth knob every embedded\n * `fetch` in the lib rides) and returns a blob object-URL. Every other URL is\n * returned unchanged, so on the cookie-auth web the hook is a pass-through.\n *\n * Returns `undefined` while the fetch is in flight OR after it fails, so\n * callers show their existing placeholder / \"no captions\" branch instead of a\n * broken asset; a failed fetch retries on the next mount.\n *\n * Cache: module-level, session-lifetime, keyed by full URL — so the two\n * `<track>` URLs of one player, or the same avatar in twenty rows, cost one\n * request. Gateway URLs carry a `?v=<content-hash>` cache-buster, so changed\n * content is a new key; entries are never revoked (a VTT / avatar is a few KB\n * — refcount churn isn't worth it) and are dropped wholesale at session end\n * via `clearAuthedAssetCache`.\n */\n\nconst resolvedCache = new Map<string, string>();\nconst inFlight = new Map<string, Promise<void>>();\nlet cacheGeneration = 0;\n\n/**\n * Drop every cached blob and revoke its object URL. Hosts call this at\n * session end (logout / forced re-login) so a follow-on login as a\n * different identity cannot be served blobs fetched under the previous\n * one's bearer. Fetches still in flight when the clear happens are fenced\n * by a generation counter — their results are revoked instead of cached.\n */\nexport function clearAuthedAssetCache(): void {\n cacheGeneration++;\n for (const url of resolvedCache.values()) URL.revokeObjectURL(url);\n resolvedCache.clear();\n inFlight.clear();\n}\n\nfunction fetchAsBlobUrl(src: string, accept: string): Promise<void> {\n let pending = inFlight.get(src);\n if (!pending) {\n const startedGeneration = cacheGeneration;\n pending = embedAuthedFetch(src, { headers: { Accept: accept } })\n .then(async response => {\n if (!response.ok) throw new Error(`asset fetch failed: ${response.status}`);\n const blobUrl = URL.createObjectURL(await response.blob());\n if (startedGeneration === cacheGeneration) {\n resolvedCache.set(src, blobUrl);\n } else {\n URL.revokeObjectURL(blobUrl);\n }\n })\n .finally(() => {\n if (inFlight.get(src) === pending) inFlight.delete(src);\n });\n inFlight.set(src, pending);\n }\n return pending;\n}\n\n/**\n * @param src the asset URL (relative or absolute), or null/undefined\n * @param accept `Accept` header for the authed fetch — narrow it to what the\n * consumer renders (`image/*`, `text/vtt`); it also replaces\n * `embedAuthedFetch`'s default JSON `Content-Type` on the GET.\n */\nexport function useAuthedAssetSrc(src?: string | null, accept = '*/*'): string | undefined {\n const bearerSrc = src && needsBearerAssetFetch(src) ? src : null;\n const [, rerender] = useReducer((c: number) => c + 1, 0);\n\n useEffect(() => {\n if (!bearerSrc || resolvedCache.has(bearerSrc)) return undefined;\n let cancelled = false;\n fetchAsBlobUrl(bearerSrc, accept)\n .catch(() => {})\n .finally(() => {\n if (!cancelled) rerender();\n });\n return () => {\n cancelled = true;\n };\n }, [bearerSrc, accept]);\n\n if (!src) return undefined;\n if (!bearerSrc) return src;\n return resolvedCache.get(bearerSrc);\n}\n","'use client';\n\nimport { clearAuthedAssetCache, useAuthedAssetSrc } from './use-authed-asset-src';\n\n/**\n * Image flavor of `useAuthedAssetSrc` — see that hook for the full contract.\n * Kept as its own export because it is the lib's most-used consumer (avatars,\n * entity images, markdown `<img>`) and because hosts already import this name.\n *\n * Distinct from the legacy `useAuthenticatedImage`, which ALWAYS blob-fetches\n * with `credentials: 'include'` and its own global config. This hook is\n * pass-through-first and keys off the single auth knob the lib already has:\n * the registered `EmbedAuthAdapter`.\n */\nexport function useAuthedImageSrc(src?: string | null): string | undefined {\n return useAuthedAssetSrc(src, 'image/*');\n}\n\n/**\n * Session-end cache drop. Now an alias of `clearAuthedAssetCache` — one cache\n * backs every authed asset (images AND caption tracks), so hosts keep calling\n * this single function on logout / forced re-login.\n */\nexport const clearAuthedImageCache = clearAuthedAssetCache;\n","/**\n * Branded OG-placeholder URL construction — the DEFAULT cover-image fallback\n * for entity cards (onboarding guides, blog/case-study/release/etc.) that have\n * no image.\n *\n * ALL of the logic lives here, in the lib. A consumer hands over its runtime\n * `endpoints` object and NOTHING else — base resolution AND the `?title=…`\n * concatenation happen inside `buildOgPlaceholderUrl`. For the entity-card +\n * onboarding-detail surfaces this is the single entry point — no card builds an\n * og-placeholder URL itself (that was the bug this replaced: each embedder\n * wired a per-surface callback that concatenated the URL, and a host that\n * forgot it rendered a blank slot with no request). (Other server-side OG paths\n * such as the hub's blog og:image generator construct their own URLs and are\n * out of this module's scope.)\n *\n * The base API URL is taken from the endpoints the host already configures:\n * 1. explicit `endpoints.ogPlaceholderUrl`\n * 2. derived from the sibling `endpoints.imageProxyUrlPrefix` (same API base)\n * 3. same-origin relative `/api/og-placeholder`\n * The base may already carry pre-existing query params; they're preserved and\n * `title` (+ dimensions) are layered on top. Per-platform brand colors are NOT\n * baked into this URL — the `/api/og-placeholder` route resolves them\n * server-side from the platform. Most hosts leave `ogPlaceholderUrl` unset and\n * let the base derive from `imageProxyUrlPrefix`.\n */\n\n/** The slice of `ChatRuntime.endpoints` this module needs. */\nexport interface OgPlaceholderEndpoints {\n /** Explicit base URL for the og-placeholder route. May already carry query\n * params — they're preserved. Per-platform colors are NOT baked here; the\n * route resolves them server-side from the platform. */\n ogPlaceholderUrl?: string;\n /** Sibling image route under the SAME API base. When `ogPlaceholderUrl` is\n * unset, the base is derived from this by swapping the trailing\n * `/image-proxy` segment for `/og-placeholder` — so a host that already\n * proxies images gets the placeholder for free, with zero extra wiring. */\n imageProxyUrlPrefix?: string;\n}\n\nexport interface BuildOgPlaceholderOptions {\n /** Site name shown under the title. Skipped when empty. */\n site?: string;\n /** `'wide'` (1200×630, the route default — no `w`/`h` emitted) or\n * `'square'` (1024×1024, for compact 56×56 chat-inline slots). */\n aspect?: 'wide' | 'square';\n /** Explicit pixel overrides (win over `aspect`). */\n width?: number;\n height?: number;\n}\n\n/** Same-origin default — for hosts that serve the route themselves (the hub).\n * Cross-origin embedders override via `ogPlaceholderUrl` or inherit it from\n * `imageProxyUrlPrefix`. */\nconst DEFAULT_OG_PLACEHOLDER_PATH = '/api/og-placeholder';\n\n/** Resolve the og-placeholder route base from the host's endpoints.\n * Internal — callers go through `buildOgPlaceholderUrl(endpoints, …)`. */\nfunction resolveOgPlaceholderBase(endpoints?: OgPlaceholderEndpoints | null): string {\n if (endpoints?.ogPlaceholderUrl) return endpoints.ogPlaceholderUrl;\n const imageProxy = endpoints?.imageProxyUrlPrefix;\n if (imageProxy) {\n // `/image-proxy` and `/og-placeholder` are sibling API routes under one\n // base. Anchor to a path-segment boundary so we only rewrite the route\n // name, never an incidental substring.\n const derived = imageProxy.replace(/\\/image-proxy(?=$|[?/])/, '/og-placeholder');\n if (derived !== imageProxy) return derived;\n }\n return DEFAULT_OG_PLACEHOLDER_PATH;\n}\n\n/**\n * Build the branded og-placeholder image URL from the host's `endpoints` + a\n * title. This is the single entry point for entity-card + onboarding-detail\n * cover fallbacks: it resolves the route base from `endpoints` AND concatenates\n * `title` (+ dimensions), so those consumers never construct a URL themselves.\n *\n * Pure string construction — SSR- and browser-safe. Always returns a usable\n * URL (relative default at worst), so a missing/unknown image degrades\n * gracefully via the `<img onError>` recovery in the card components.\n */\nexport function buildOgPlaceholderUrl(\n endpoints: OgPlaceholderEndpoints | null | undefined,\n title: string,\n options: BuildOgPlaceholderOptions = {},\n): string {\n const base = resolveOgPlaceholderBase(endpoints);\n const qIndex = base.indexOf('?');\n const path = qIndex === -1 ? base : base.slice(0, qIndex);\n const params = new URLSearchParams(qIndex === -1 ? '' : base.slice(qIndex + 1));\n\n params.set('title', title);\n if (options.site) params.set('site', options.site);\n\n // Square aspect → request a 1024×1024 image so `object-cover` doesn't crop\n // the title off in compact slots. Wide leaves dimensions to the route\n // default (1200×630). Explicit width/height always win.\n const width = options.width ?? (options.aspect === 'square' ? 1024 : undefined);\n const height = options.height ?? (options.aspect === 'square' ? 1024 : undefined);\n // `Number.isFinite` does not coerce, so it already rejects `undefined` — no\n // separate `typeof === 'number'` guard needed.\n if (Number.isFinite(width)) params.set('w', String(width));\n if (Number.isFinite(height)) params.set('h', String(height));\n\n return `${path}?${params.toString()}`;\n}\n","'use client';\n\nimport { useMemo } from 'react';\n\nimport { useChatRuntime } from '../contexts/chat-runtime-context';\nimport { buildOgPlaceholderUrl } from '../utils/og-placeholder';\n\n/**\n * Resolve a branded og-placeholder image URL for a title, driven entirely by\n * the runtime `endpoints` (no injected builder).\n *\n * THE one og-placeholder hook. It reads the host's `endpoints` from\n * `ChatRuntime` and hands them to `buildOgPlaceholderUrl`, which resolves the\n * route base (explicit `ogPlaceholderUrl` → derived from `imageProxyUrlPrefix`\n * → same-origin `/api/og-placeholder`) and appends `?title=…`. Per-platform\n * brand colors are resolved SERVER-SIDE by the route — nothing is baked here.\n *\n * Replaces the old builder-injection `useOgPlaceholder(buildUrl, …)`: callers\n * no longer pass a URL builder. `useEntityCardPlaceholder` delegates here too,\n * so every surface shares one memo + one code path.\n */\nexport interface UseOgPlaceholderUrlArgs {\n /** Text to display on the placeholder. */\n title: string | undefined | null;\n /** Site name shown below the title (optional). */\n siteName?: string;\n /** `'wide'` (1200×630 social-card; default) or `'square'` (1024×1024 — for\n * compact chat-inline slots so `object-cover` doesn't crop the title off). */\n aspect?: 'wide' | 'square';\n /** When `false`, returns `null` instead of a URL. */\n enabled?: boolean;\n}\n\nexport function useOgPlaceholderUrl({\n title,\n siteName = '',\n aspect = 'wide',\n enabled = true,\n}: UseOgPlaceholderUrlArgs): string | null {\n const endpoints = useChatRuntime()?.endpoints;\n\n return useMemo(() => {\n if (!enabled || !title) return null;\n return buildOgPlaceholderUrl(endpoints, title, { site: siteName || undefined, aspect });\n }, [endpoints, title, siteName, aspect, enabled]);\n}\n"],"mappings":"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","names":["r","g","b","adapter","useEffect","useEffect"]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } }"use client";
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
var
|
|
4
|
+
var _chunkDVSPRY2Vcjs = require('./chunk-DVSPRY2V.cjs');
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
|
|
@@ -10,7 +10,7 @@ var _chunkNMXL34Z2cjs = require('./chunk-NMXL34Z2.cjs');
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
var
|
|
13
|
+
var _chunkEXWS2QSDcjs = require('./chunk-EXWS2QSD.cjs');
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
|
|
@@ -20,7 +20,7 @@ var _chunkSADRZ4YIcjs = require('./chunk-SADRZ4YI.cjs');
|
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
|
|
23
|
-
var
|
|
23
|
+
var _chunk53O6PQ3Fcjs = require('./chunk-53O6PQ3F.cjs');
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
var _chunkFCUTPPX5cjs = require('./chunk-FCUTPPX5.cjs');
|
|
@@ -33,7 +33,7 @@ var _jsxruntime = require('react/jsx-runtime');
|
|
|
33
33
|
function ListEmptyState({ isFiltered, filtered, onClearFilters, empty }) {
|
|
34
34
|
if (isFiltered) {
|
|
35
35
|
return /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
36
|
-
|
|
36
|
+
_chunkDVSPRY2Vcjs.EmptyState,
|
|
37
37
|
{
|
|
38
38
|
type: "search",
|
|
39
39
|
title: filtered.title,
|
|
@@ -46,7 +46,7 @@ function ListEmptyState({ isFiltered, filtered, onClearFilters, empty }) {
|
|
|
46
46
|
}
|
|
47
47
|
const hasCta = Boolean(empty.ctaText && (empty.onCtaClick || empty.ctaHref));
|
|
48
48
|
return /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
49
|
-
|
|
49
|
+
_chunkDVSPRY2Vcjs.EmptyState,
|
|
50
50
|
{
|
|
51
51
|
type: "generic",
|
|
52
52
|
title: empty.title,
|
|
@@ -63,7 +63,7 @@ function ListEmptyState({ isFiltered, filtered, onClearFilters, empty }) {
|
|
|
63
63
|
var _react = require('react');
|
|
64
64
|
|
|
65
65
|
function DevSectionView({ sectionKey, hero, preControls, children, showHeading = true }) {
|
|
66
|
-
const section =
|
|
66
|
+
const section = _chunkEXWS2QSDcjs.OPENFRAME_DEV_SECTIONS[sectionKey];
|
|
67
67
|
const router = _chunkSADRZ4YIcjs.useRouter.call(void 0, );
|
|
68
68
|
const pathname = _chunkSADRZ4YIcjs.usePathname.call(void 0, );
|
|
69
69
|
const searchParams = _chunkSADRZ4YIcjs.useSearchParams.call(void 0, );
|
|
@@ -114,7 +114,7 @@ function DevSectionView({ sectionKey, hero, preControls, children, showHeading =
|
|
|
114
114
|
preControls,
|
|
115
115
|
(search || filter) && /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "space-y-4", children: [
|
|
116
116
|
search && /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
117
|
-
|
|
117
|
+
_chunkEXWS2QSDcjs.SearchInput,
|
|
118
118
|
{
|
|
119
119
|
showDropdown: false,
|
|
120
120
|
placeholder: search.placeholder,
|
|
@@ -124,7 +124,7 @@ function DevSectionView({ sectionKey, hero, preControls, children, showHeading =
|
|
|
124
124
|
}
|
|
125
125
|
),
|
|
126
126
|
filter && /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
127
|
-
|
|
127
|
+
_chunkEXWS2QSDcjs.StatusFilterComponent,
|
|
128
128
|
{
|
|
129
129
|
selectedStatus: currentFilterValue,
|
|
130
130
|
onStatusChange: handleFilterChange,
|
|
@@ -148,7 +148,7 @@ function DevSectionPage({
|
|
|
148
148
|
shell = true
|
|
149
149
|
}) {
|
|
150
150
|
const router = _chunkSADRZ4YIcjs.useRouter.call(void 0, );
|
|
151
|
-
const section =
|
|
151
|
+
const section = _chunkEXWS2QSDcjs.OPENFRAME_DEV_SECTIONS[sectionKey];
|
|
152
152
|
const backCfg = backButton === false ? void 0 : {
|
|
153
153
|
label: _nullishCoalesce((backButton ? backButton.label : void 0), () => ( "Back to home")),
|
|
154
154
|
onClick: () => router.push(_nullishCoalesce((backButton ? backButton.href : void 0), () => ( "/")))
|
|
@@ -161,7 +161,7 @@ function DevSectionPage({
|
|
|
161
161
|
// duplicate title. (The hero icon is intentionally dropped: TitleBlock is
|
|
162
162
|
// frozen and renders title-only.)
|
|
163
163
|
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
164
|
-
|
|
164
|
+
_chunkEXWS2QSDcjs.PageLayout,
|
|
165
165
|
{
|
|
166
166
|
title: _nullishCoalesce(title, () => ( section.hero.title)),
|
|
167
167
|
subtitle: _nullishCoalesce(subtitle, () => ( section.hero.description)),
|
|
@@ -172,7 +172,7 @@ function DevSectionPage({
|
|
|
172
172
|
}
|
|
173
173
|
)
|
|
174
174
|
);
|
|
175
|
-
return shell ? /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
175
|
+
return shell ? /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkEXWS2QSDcjs.PageShell, { children: inner }) : /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: "page-shell-content", children: inner });
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
// src/components/shared/dev-section/dev-card-row.tsx
|
|
@@ -230,9 +230,9 @@ function getRelativeTime(timestamp) {
|
|
|
230
230
|
}
|
|
231
231
|
function DeliveryRow({ item, href, target, caption, id, className }) {
|
|
232
232
|
const taskType = item.taskType;
|
|
233
|
-
const typeBadgeLabel =
|
|
234
|
-
const typeBadgeTextColor =
|
|
235
|
-
const statusBadgeScheme =
|
|
233
|
+
const typeBadgeLabel = _chunkEXWS2QSDcjs.TASK_TYPE_LABELS[taskType] || "TASK";
|
|
234
|
+
const typeBadgeTextColor = _chunkEXWS2QSDcjs.TASK_TYPE_TEXT_COLORS[taskType] || "";
|
|
235
|
+
const statusBadgeScheme = _chunk53O6PQ3Fcjs.getStatusColorScheme.call(void 0, item.status);
|
|
236
236
|
const relativeTime = getRelativeTime(item.dateUpdated);
|
|
237
237
|
const subtitle = `ACTIVE ${relativeTime}${item.listNames.length > 0 ? `, ${item.listNames.join(", ")}` : ""}, ${item.id}`;
|
|
238
238
|
const inner = /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "flex w-full flex-col items-start justify-between gap-[12px] md:flex-row md:gap-[16px]", children: [
|
|
@@ -244,7 +244,7 @@ function DeliveryRow({ item, href, target, caption, id, className }) {
|
|
|
244
244
|
] }),
|
|
245
245
|
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "flex w-full flex-shrink-0 flex-row items-center gap-2 self-stretch md:w-auto md:flex-col md:items-end md:self-start", children: [
|
|
246
246
|
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
247
|
-
|
|
247
|
+
_chunk53O6PQ3Fcjs.StatusBadge,
|
|
248
248
|
{
|
|
249
249
|
text: item.status.toUpperCase(),
|
|
250
250
|
colorScheme: statusBadgeScheme,
|
|
@@ -253,7 +253,7 @@ function DeliveryRow({ item, href, target, caption, id, className }) {
|
|
|
253
253
|
}
|
|
254
254
|
),
|
|
255
255
|
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
256
|
-
|
|
256
|
+
_chunk53O6PQ3Fcjs.StatusBadge,
|
|
257
257
|
{
|
|
258
258
|
text: typeBadgeLabel,
|
|
259
259
|
variant: "card",
|
|
@@ -261,7 +261,7 @@ function DeliveryRow({ item, href, target, caption, id, className }) {
|
|
|
261
261
|
}
|
|
262
262
|
),
|
|
263
263
|
item.assignees && item.assignees.length > 0 ? /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
264
|
-
|
|
264
|
+
_chunk53O6PQ3Fcjs.AvatarStack,
|
|
265
265
|
{
|
|
266
266
|
size: "xs",
|
|
267
267
|
className: "ml-auto md:ml-0",
|
|
@@ -302,4 +302,4 @@ function DeliveryRow({ item, href, target, caption, id, className }) {
|
|
|
302
302
|
|
|
303
303
|
|
|
304
304
|
exports.ListEmptyState = ListEmptyState; exports.DevSectionView = DevSectionView; exports.DevSectionPage = DevSectionPage; exports.DevCardRowContent = DevCardRowContent; exports.DevCardRowSkeleton = DevCardRowSkeleton; exports.DevCardRowSkeletonList = DevCardRowSkeletonList; exports.DeliveryRow = DeliveryRow;
|
|
305
|
-
//# sourceMappingURL=chunk-
|
|
305
|
+
//# sourceMappingURL=chunk-WTYQCT4A.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/chunk-X4WBGDNW.cjs","../src/components/list-empty-state.tsx","../src/components/shared/dev-section/dev-section-view.tsx","../src/components/shared/dev-section/dev-section-page.tsx","../src/components/shared/dev-section/dev-card-row.tsx","../src/components/shared/delivery/delivery-row.tsx"],"names":["jsx","jsxs"],"mappings":"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","file":"/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/chunk-X4WBGDNW.cjs","sourcesContent":[null,"'use client';\n\nimport { EmptyState } from './empty-state';\n\nexport interface ListEmptyStateCopy {\n title: string;\n description: string;\n}\n\nexport interface ListEmptyStateProps {\n /** A search or filter is narrowing the list, so \"nothing to show\" is a miss, not an empty list. */\n isFiltered: boolean;\n /** Copy for the filtered miss. `clearText` labels the reset button (default \"Clear filters\"). */\n filtered: ListEmptyStateCopy & { clearText?: string };\n /** Resets every search and filter the list owns. */\n onClearFilters: () => void;\n /**\n * Copy for a genuinely empty list, with an optional call to action (e.g. \"Create the first one\").\n * The CTA renders only when `ctaText` comes with `onCtaClick` or `ctaHref`, so an empty list with\n * no action never falls through to `EmptyState`'s path-sniffed default button.\n */\n empty: ListEmptyStateCopy & { ctaText?: string; onCtaClick?: () => void; ctaHref?: string };\n}\n\n/**\n * THE empty state of a searchable, filterable list: a filtered miss offers to clear the filters\n * (`search` icon, secondary button), a genuinely empty list says so and optionally offers its own\n * action (`generic` icon, primary button). Every list with filters renders this instead of choosing\n * between two `EmptyState`s by hand, so the two cases look and behave the same everywhere.\n */\nexport function ListEmptyState({ isFiltered, filtered, onClearFilters, empty }: ListEmptyStateProps) {\n if (isFiltered) {\n return (\n <EmptyState\n type=\"search\"\n title={filtered.title}\n description={filtered.description}\n ctaText={filtered.clearText ?? 'Clear filters'}\n onCtaClick={onClearFilters}\n ctaVariant=\"secondary\"\n />\n );\n }\n\n const hasCta = Boolean(empty.ctaText && (empty.onCtaClick || empty.ctaHref));\n return (\n <EmptyState\n type=\"generic\"\n title={empty.title}\n description={empty.description}\n showCTA={hasCta}\n ctaText={empty.ctaText}\n onCtaClick={empty.onCtaClick}\n ctaHref={empty.ctaHref}\n />\n );\n}\n","'use client';\n\n/**\n * DevSectionView — the canonical chrome for ANY dev-center section\n * (Roadmap / Delivery / Releases). One component, used in BOTH:\n *\n * - tabbed `/roadmap-and-releases` (compact title mode, no `hero`)\n * - full-page `/roadmap`, `/bug-fixes-and-enhancements`, `/releases`\n * (hero mode with icon + description + back link)\n *\n * Owns: title rendering, the inline search input, the filter pill row,\n * and the URL-param wiring that connects both. The list `children`\n * receive a clean URL contract — they read `?<paramKey>=...` via\n * `useSearchParams()` and refetch on change. No duplicated controls.\n */\n\nimport type { ReactNode } from 'react';\nimport { useState } from 'react';\nimport { useRouter, useSearchParams, usePathname } from '../../../embed-shims';\nimport {\n OPENFRAME_DEV_SECTIONS,\n type OpenframeDevSection,\n type OpenframeDevSectionKey,\n} from '../../../utils/dev-sections/openframe-dev-sections';\nimport { StatusFilterComponent } from '../../features';\nimport { SearchInput } from '../../ui';\n\nexport interface DevSectionViewProps {\n /** Which section to render — drives title, search, and filter\n * config via the `OPENFRAME_DEV_SECTIONS` registry. */\n sectionKey: OpenframeDevSectionKey;\n /** When set, renders the rich page-level hero (icon + h1 + description).\n * Omit for the compact tab-context heading. */\n hero?: {\n /** Pre-rendered icon JSX. Server components render the icon themselves\n * and pass the element here — function references can't cross the\n * server→client boundary, but React elements can. */\n icon: ReactNode;\n /** Hero title. Falls back to `OPENFRAME_DEV_SECTIONS[sectionKey].hero.title`\n * when omitted, so embedders can override the (OpenFrame-specific) default\n * copy without forking the registry. */\n title?: string;\n description: string;\n };\n /** Optional slot rendered BETWEEN the hero and the search/filter\n * controls. Use this for an entry-action surface that should sit\n * above the list (e.g. the Help Center's \"Open a new ticket\" form).\n * The slot is wrapped in the same `gap-10` flex column so spacing\n * matches the surrounding chrome — callers should NOT add their\n * own top/bottom margin. Renders `null` (no DOM) when omitted. */\n preControls?: ReactNode;\n /** The page-specific list body. Reads URL params written by this\n * component (search input + filter pills). */\n children: ReactNode;\n /** Render this component's own title heading (hero or compact `h2`). Default\n * `true` — the tab context renders the compact heading itself. Pass `false`\n * when the host already renders the title elsewhere (e.g. `DevSectionPage`\n * routes it through the unified `PageLayout` `TitleBlock`): then only the\n * search + filter controls + list render, with no duplicate heading. */\n showHeading?: boolean;\n}\n\nexport function DevSectionView({ sectionKey, hero, preControls, children, showHeading = true }: DevSectionViewProps) {\n const section = OPENFRAME_DEV_SECTIONS[sectionKey];\n const router = useRouter();\n const pathname = usePathname();\n const searchParams = useSearchParams();\n\n // Widen from the registry's per-section literal types to the interface —\n // optional fields (`clearParamKeys`) exist on the interface but not on\n // every section's inferred literal. Sound: the registry `satisfies`\n // `Record<string, OpenframeDevSection>`.\n const search: OpenframeDevSection['search'] = section.search;\n const filter = section.filter;\n\n const currentSearch = search ? searchParams.get(search.paramKey) || '' : '';\n const currentFilterValue = filter ? searchParams.get(filter.paramKey) || filter.defaultValue : '';\n\n // Controlled search-input state — TYPING commits to the URL only on\n // Enter (not on every keystroke), but CLEARING commits immediately:\n // an emptied input must not keep filtering the list (the URL param —\n // and any linked `clearParamKeys` companion like the tickets\n // `?ticket=` drawer param — previously survived until Enter).\n // Lazy init from URL avoids a brief flash of stale value on first\n // paint after URL-driven re-render (e.g. tab switch).\n const [searchValue, setSearchValue] = useState(() => currentSearch);\n // Re-sync when the URL changes underneath us (tab switch, back/forward,\n // a companion param cleared elsewhere). Adjusted while rendering — React's\n // documented prop-sync pattern — rather than from an effect, which would\n // commit one frame with the input still showing the previous section's\n // query before replacing it.\n const [syncedSearch, setSyncedSearch] = useState(currentSearch);\n if (syncedSearch !== currentSearch) {\n setSyncedSearch(currentSearch);\n setSearchValue(currentSearch);\n }\n\n const handleSearchSubmit = (value: string) => {\n if (!search) return;\n const params = new URLSearchParams(searchParams.toString());\n if (value.trim()) {\n params.set(search.paramKey, value.trim());\n } else {\n params.delete(search.paramKey);\n // Empty commit tears down the search's linked companion params too\n // (deep links set them TOGETHER — see the config field docs).\n for (const key of search.clearParamKeys ?? []) params.delete(key);\n }\n const qs = params.toString();\n router.replace(qs ? `${pathname}?${qs}` : pathname, { scroll: false });\n };\n\n const handleSearchChange = (value: string) => {\n setSearchValue(value);\n if (value === '' && currentSearch !== '') handleSearchSubmit('');\n };\n\n const handleFilterChange = (value: string) => {\n if (!filter) return;\n const params = new URLSearchParams(searchParams.toString());\n if (value === filter.defaultValue) params.delete(filter.paramKey);\n else params.set(filter.paramKey, value);\n router.replace(`${pathname}?${params.toString()}`, { scroll: false });\n };\n\n return (\n <div className=\"flex w-full flex-col gap-10\">\n {showHeading &&\n (hero ? (\n <div className=\"space-y-4\">\n <h1 className=\"flex items-center gap-3 tracking-[-1.12px] text-ods-text-primary text-h1\">\n {hero.icon}\n {hero.title ?? section.hero.title}\n </h1>\n <p className=\"max-w-3xl text-ods-text-secondary text-h4\">{hero.description}</p>\n </div>\n ) : (\n <div className=\"flex w-full items-center justify-between\">\n <h2 className=\"font-['Azeret_Mono'] text-[32px] font-semibold leading-[40px] tracking-[-0.64px] text-ods-text-primary md:text-[40px] md:leading-[48px] md:tracking-[-0.8px] lg:text-[48px] lg:leading-[56px] lg:tracking-[-0.96px]\">\n {section.hero.title}\n <span className=\"text-ods-accent\">:</span>\n </h2>\n </div>\n ))}\n\n {preControls}\n\n {(search || filter) && (\n <div className=\"space-y-4\">\n {search && (\n <SearchInput\n showDropdown={false}\n placeholder={search.placeholder}\n value={searchValue}\n onChange={handleSearchChange}\n onSubmit={handleSearchSubmit}\n />\n )}\n {filter && (\n <StatusFilterComponent\n selectedStatus={currentFilterValue}\n onStatusChange={handleFilterChange}\n statusOptions={[...filter.options]}\n />\n )}\n </div>\n )}\n\n {children}\n </div>\n );\n}\n","'use client';\n\n/**\n * DevSectionPage — full-page wrapper for a dev-center section\n * (`/roadmap`, `/bug-fixes-and-enhancements`, `/releases`).\n *\n * Mounts the lib's canonical `PageLayout` directly (no in-app wrapper)\n * so the back-button affordance stays in lockstep with whatever the\n * design system ships — any future lib change to BackButton / TitleBlock\n * propagates automatically.\n *\n * Composition: `PageShell` → `PageLayout` (back-to-home wired) →\n * `DevSectionView` (icon hero + search + filter pills) → list body.\n *\n * Adding a new section is one entry in `OPENFRAME_DEV_SECTIONS` plus a\n * single-line page file mounting this factory with the new key.\n */\n\nimport type { ReactNode } from 'react';\nimport { useRouter } from '../../../embed-shims/next-navigation';\nimport {\n OPENFRAME_DEV_SECTIONS,\n type OpenframeDevSectionKey,\n} from '../../../utils/dev-sections/openframe-dev-sections';\nimport { PageShell, PageLayout } from '../../ui';\nimport { DevSectionView } from './dev-section-view';\n\nexport interface DevSectionPageProps {\n sectionKey: OpenframeDevSectionKey;\n /** The page-specific list body (e.g. `<RoadmapList />`). */\n children: ReactNode;\n /** Optional slot rendered BETWEEN the hero and search/filter — see\n * `DevSectionView.preControls`. Used by surfaces that want an entry\n * action (e.g. Help Center's \"Open a new ticket\" form) above the\n * controls instead of below them. */\n preControls?: ReactNode;\n /** Back-button config — same shape as `LegalDocumentPage` /\n * `ReleaseDetailPage`. Pass `false` to hide entirely. Default\n * `{ label: 'Back to home', href: '/' }` — embedders whose \"home\" isn't `/`\n * should override `href`, or pass `false` if the embed has no home page. */\n backButton?: { label?: string; href?: string } | false;\n /** Override the hero title. Defaults to the (OpenFrame-specific) copy in\n * `OPENFRAME_DEV_SECTIONS[sectionKey].hero.title`. Set this to brand the\n * section for a non-OpenFrame embed. */\n title?: string;\n /** Override the hero subtitle/description. Defaults to\n * `OPENFRAME_DEV_SECTIONS[sectionKey].hero.description`. */\n subtitle?: string;\n /** Render the standalone `<PageShell>` (own `<main>` + bg + max-width). Default\n * `true` — the contract for marketing/hub surfaces with no app shell. Pass\n * `false` when the host layout already provides the page container (e.g.\n * openframe-frontend's `AppLayout` `<main>`): then only the\n * `page-shell-content` padding box is rendered, avoiding a nested `<main>`. */\n shell?: boolean;\n}\n\nexport function DevSectionPage({\n sectionKey,\n children,\n preControls,\n backButton,\n title,\n subtitle,\n shell = true,\n}: DevSectionPageProps) {\n const router = useRouter();\n const section = OPENFRAME_DEV_SECTIONS[sectionKey];\n\n // Back-button config — mirrors LegalDocumentPage / ReleaseDetailPage.\n // Default: { label: 'Back to home', href: '/' }. Pass `false` to hide.\n // After `backButton &&` narrowing, inner type is `{ label?, href? } |\n // undefined`; don't re-compare to `false` (TS2367).\n const backCfg =\n backButton === false\n ? undefined\n : {\n label: (backButton ? backButton.label : undefined) ?? 'Back to home',\n onClick: () => router.push((backButton ? backButton.href : undefined) ?? '/'),\n };\n\n const inner = (\n // Unified header: title/description route through the canonical (frozen)\n // `PageLayout` `TitleBlock` (text-h2) — same as FAQ / Legal / detail pages —\n // so every help-center surface shares one header. `DevSectionView` then\n // renders ONLY its search + filter controls (`showHeading={false}`), no\n // duplicate title. (The hero icon is intentionally dropped: TitleBlock is\n // frozen and renders title-only.)\n <PageLayout\n title={title ?? section.hero.title}\n subtitle={subtitle ?? section.hero.description}\n titleSize=\"h1\"\n titleWrap\n backButton={backCfg}\n >\n <DevSectionView sectionKey={sectionKey} showHeading={false} preControls={preControls}>\n {children}\n </DevSectionView>\n </PageLayout>\n );\n\n // `shell` true → standalone `<PageShell>` (own <main> + bg + max-width).\n // false → padding-only box (no nested <main>) for hosts whose layout already\n // provides the container; both consume the host's `--page-shell-*` vars.\n return shell ? <PageShell>{inner}</PageShell> : <div className=\"page-shell-content\">{inner}</div>;\n}\n","'use client';\n\n/**\n * Shared row chrome for any `DevSectionPage` list (delivery, tickets,\n * future sections). One source of truth for the layout that every\n * dev-section card row uses:\n * left column → title (h3) / subtitle (h5 uppercase) / description\n * (h4 line-clamp-3), each in a fixed min-height block\n * so rows align across the grid\n * right column → caller-supplied stacked badges\n *\n * Surface stays small on purpose — `rightBadges` is a `ReactNode` so\n * the caller decides how many badges (delivery: 2, tickets: 1-2,\n * future: anything). No behavior baked in: the caller wraps the row\n * in a `<div>` (static, like delivery) or `<button>` (clickable, like\n * tickets) and renders the row content via this component.\n *\n * Pair with `DevCardRowSkeletonList` for the loading state — the\n * skeleton mirrors the same min-heights so the in-flight UI doesn't\n * shift the layout when real data lands.\n *\n * NOTE: the ticket conversation row is NOT here — it renders the shared\n * `<ChatMessageRow>` (`components/chat/chat-message-row.tsx`), the SAME\n * component the OpenMSP Slack-community feed uses, so the two surfaces stay\n * pixel-identical by construction.\n */\n\nimport type { ReactNode } from 'react';\n\nexport interface DevCardRowContentProps {\n title: string;\n /** Single-line uppercase metadata (e.g. \"UPDATED today, #4271, Code review\"). */\n subtitle: string;\n /** 3-line description block. Empty string renders the fallback. */\n description: string;\n /** Fallback copy when `description` is empty. Defaults to a generic\n * string; ticket / delivery surfaces override. */\n emptyDescription?: string;\n /** Right column — caller renders its own stacked badges. */\n rightBadges: ReactNode;\n}\n\nexport function DevCardRowContent({\n title,\n subtitle,\n description,\n emptyDescription = 'No description provided',\n rightBadges,\n}: DevCardRowContentProps) {\n return (\n <div className=\"flex w-full flex-col items-start justify-between gap-[12px] md:flex-row md:gap-[16px]\">\n <div className=\"flex w-full min-w-0 flex-1 flex-col gap-[12px] md:w-auto md:gap-[16px]\">\n <div className=\"flex min-h-[24px] items-center\">\n <h3 className=\"line-clamp-2 flex-1 break-words tracking-[-0.36px] text-ods-text-primary text-h3 md:truncate\">\n {title}\n </h3>\n </div>\n <div className=\"flex min-h-[20px] items-center\">\n <p className=\"truncate uppercase tracking-[-0.28px] text-ods-text-secondary text-h5\">{subtitle}</p>\n </div>\n <div className=\"flex min-h-[72px] items-center\">\n <p className=\"line-clamp-3 break-words text-ods-text-secondary text-h4\">{description || emptyDescription}</p>\n </div>\n </div>\n <div className=\"flex flex-shrink-0 flex-col gap-2 self-start\">{rightBadges}</div>\n </div>\n );\n}\n\n/**\n * Skeleton rendering for a single row — the bars mirror the same\n * min-heights as `DevCardRowContent` so the loading→loaded swap\n * doesn't reflow.\n */\nexport function DevCardRowSkeleton() {\n return (\n <div className=\"border-b border-ods-border p-[12px] last:border-b-0 md:p-[16px]\">\n <div className=\"flex w-full flex-col items-start justify-between gap-[12px] md:flex-row md:gap-[16px]\">\n <div className=\"flex w-full min-w-0 flex-1 flex-col gap-[12px] md:w-auto md:gap-[16px]\">\n <div className=\"flex min-h-[24px] items-center\">\n <div className=\"h-[20px] w-full animate-pulse rounded bg-ods-border\" />\n </div>\n <div className=\"flex min-h-[20px] items-center\">\n <div className=\"h-[20px] w-1/2 animate-pulse rounded bg-ods-border\" />\n </div>\n <div className=\"flex min-h-[72px] items-center\">\n <div className=\"flex-1 space-y-1\">\n <div className=\"h-[20px] w-full animate-pulse rounded bg-ods-border\" />\n <div className=\"h-[20px] w-full animate-pulse rounded bg-ods-border\" />\n <div className=\"h-[20px] w-2/3 animate-pulse rounded bg-ods-border\" />\n </div>\n </div>\n </div>\n <div className=\"flex flex-shrink-0 flex-col gap-2 self-start\">\n <div className=\"h-[32px] w-[100px] animate-pulse rounded bg-ods-border\" />\n <div className=\"h-[32px] w-[120px] animate-pulse rounded bg-ods-border\" />\n </div>\n </div>\n </div>\n );\n}\n\n/**\n * The standard \"5 skeleton rows inside a bordered card\" loading state\n * used by every list shell. Both delivery (`delivery-table.tsx`) and\n * tickets (`tickets-list.tsx`) mount this directly.\n */\nexport function DevCardRowSkeletonList({ rows = 5 }: { rows?: number }) {\n return (\n <div className=\"w-full overflow-hidden rounded-[6px] border border-ods-border bg-ods-card\">\n {Array.from({ length: rows }, (_, i) => (\n <DevCardRowSkeleton key={i} />\n ))}\n </div>\n );\n}\n","'use client';\n\n/**\n * `<DeliveryRow />` — canonical single-row presentation for a ClickUp\n * delivery item.\n *\n * Single source of truth: both the `/bug-fixes-and-enhancements` page\n * (via `DeliveryTable`) AND the linked-delivery card on a HubSpot ticket\n * (via `TicketLinkedDeliveryCard`) compose this primitive. Visual parity\n * across those two surfaces is the design goal — the user reads the\n * card on their ticket and recognises it as a row from the public\n * delivery list.\n *\n * Behaviors:\n * - `href` set → outer element is an `<a>`, the whole row becomes\n * clickable (used by the linked-card surface to deep-link into\n * `/bug-fixes-and-enhancements?focus=<id>`).\n * - `id` set → outer element gets that DOM id so the consuming page\n * can `scrollIntoView` to it when the URL carries `?focus=<id>`.\n * - `highlighted` true → brief accent border + background pulse\n * (`animate-flash-focus` keyframe defined in `tailwind.config.ts`).\n * - `caption` set → small uppercase label rendered above the title\n * (\"LINKED DELIVERY\" on the ticket-side variant). Omitted on the\n * standard list rendering.\n */\n\nimport Link from '../../../embed-shims/next-link';\nimport { type DeliveryItem, TASK_TYPE_LABELS, TASK_TYPE_TEXT_COLORS } from '../../../types/delivery';\nimport { getStatusColorScheme } from '../../../utils';\nimport { cn } from '../../../utils/cn';\nimport { AvatarStack } from '../../ui/avatar-stack';\nimport { StatusBadge } from '../../ui/status-badge';\n\n/** Same heuristic as DeliveryTable's local helper. Inlined so the row\n * primitive owns its complete rendering contract. */\nfunction getRelativeTime(timestamp: number): string {\n // A missing or zero timestamp is ABSENT data, not the epoch — without this\n // the row reads \"678 months ago\" for a task the API never dated.\n if (!timestamp) return 'recently';\n const now = Date.now();\n const diff = now - timestamp;\n const days = Math.floor(diff / (1000 * 60 * 60 * 24));\n const weeks = Math.floor(days / 7);\n const months = Math.floor(days / 30);\n if (months > 0) return months === 1 ? 'last month' : `${months} months ago`;\n if (weeks > 0) return weeks === 1 ? 'last week' : `${weeks} weeks ago`;\n if (days > 0) return days === 1 ? 'yesterday' : `${days} days ago`;\n return 'today';\n}\n\nexport interface DeliveryRowProps {\n item: DeliveryItem;\n /** When set, the row becomes a clickable anchor. The ticket-side\n * linked-card composes this from `buildDevSectionUrl('delivery', id)`\n * which carries `?search=<id>` — the delivery list filters to that\n * exact task on landing (canonical deep-link mechanism, same one\n * the chat-inline delivery card uses). */\n href?: string;\n /** Small uppercase caption rendered above the title. Used by the\n * linked-delivery card variant (\"LINKED DELIVERY\"). */\n caption?: string;\n /** DOM `id` applied to the row's outer element. `DeliveryTable`\n * always sets `delivery-<external_id>` so chat-card deep-links\n * (`?search=<id>#delivery-<id>`) and the ticket linked-card path\n * both have a target for `useScrollToHash` to scroll to. Always\n * paired with `scroll-mt-24` on the outer element so the row lands\n * BELOW the sticky chrome after the scroll. */\n id?: string;\n /** `'_blank'` renders a plain external anchor (new tab, `rel` hardened) —\n * for rows whose href leaves the app (e.g. the raw ClickUp task URL on the\n * design-doc surfaces). Internal deep-links omit it and keep the Link shim. */\n target?: '_blank';\n className?: string;\n}\n\nexport function DeliveryRow({ item, href, target, caption, id, className }: DeliveryRowProps) {\n const taskType = item.taskType as keyof typeof TASK_TYPE_LABELS;\n const typeBadgeLabel = TASK_TYPE_LABELS[taskType] || 'TASK';\n const typeBadgeTextColor = TASK_TYPE_TEXT_COLORS[taskType] || '';\n const statusBadgeScheme = getStatusColorScheme(item.status);\n const relativeTime = getRelativeTime(item.dateUpdated);\n const subtitle = `ACTIVE ${relativeTime}${item.listNames.length > 0 ? `, ${item.listNames.join(', ')}` : ''}, ${item.id}`;\n\n const inner = (\n <div className=\"flex w-full flex-col items-start justify-between gap-[12px] md:flex-row md:gap-[16px]\">\n {/* Left: caption (optional) + title + subtitle + description */}\n <div className=\"flex w-full min-w-0 flex-1 flex-col gap-[12px] md:w-auto md:gap-[16px]\">\n {caption && <p className=\"text-ods-text-secondary text-h5\">{caption}</p>}\n <div className=\"flex min-h-[24px] items-center md:min-h-[24px]\">\n <h3 className=\"line-clamp-2 flex-1 break-words tracking-[-0.36px] text-ods-text-primary text-h3 md:truncate\">\n {item.title}\n </h3>\n </div>\n <div className=\"flex min-h-[20px] items-center\">\n {/* Relative time is wall-clock-derived: on an SSR host the server and\n client render different strings a moment apart — text-only, so\n suppress instead of forcing a client-only render. */}\n <p className=\"truncate uppercase tracking-[-0.28px] text-ods-text-secondary text-h5\" suppressHydrationWarning>\n {subtitle}\n </p>\n </div>\n <div className=\"flex min-h-[72px] items-center\">\n <p className=\"line-clamp-3 break-words text-ods-text-secondary text-h4\">\n {item.description || 'No description provided'}\n </p>\n </div>\n </div>\n\n {/* Right (desktop): badges + assignee stack in a right-aligned\n column. Mobile: ONE horizontal footer row — badges lead,\n avatars anchor the right edge (kanban-card convention: the\n assignee owns the trailing slot of the meta row; a column of\n right-aligned orphans under the description reads as clutter\n on small screens). */}\n <div className=\"flex w-full flex-shrink-0 flex-row items-center gap-2 self-stretch md:w-auto md:flex-col md:items-end md:self-start\">\n <StatusBadge\n text={item.status.toUpperCase()}\n colorScheme={statusBadgeScheme}\n variant=\"card\"\n className=\"border border-ods-border\"\n />\n <StatusBadge\n text={typeBadgeLabel}\n variant=\"card\"\n className={`border border-ods-border ${typeBadgeTextColor}`}\n />\n {item.assignees && item.assignees.length > 0 ? (\n <AvatarStack\n size=\"xs\"\n className=\"ml-auto md:ml-0\"\n people={item.assignees.map(a => ({\n key: a.id,\n name: a.name ?? 'Unknown',\n avatarUrl: a.avatarUrl,\n }))}\n />\n ) : null}\n </div>\n </div>\n );\n\n const baseClass = cn(\n 'block p-[12px] text-inherit no-underline transition-colors duration-150 md:p-[16px]',\n // `scroll-mt-24` is paid for whether `id` is set or not (it's a\n // single Tailwind utility, no runtime cost). Keeping it\n // unconditional means a future caller adding `id` doesn't also\n // have to remember to ask for the offset.\n 'scroll-mt-24',\n href && 'cursor-pointer hover:bg-ods-bg-hover',\n className,\n );\n\n if (href && target === '_blank') {\n return (\n <a href={href} id={id} className={baseClass} target=\"_blank\" rel=\"noopener noreferrer\">\n {inner}\n </a>\n );\n }\n\n if (href) {\n // `Link` is the env-aware embed-shim — delegates to `next/link` on\n // a Next.js host (soft RSC nav, back-button restores the previous\n // page's React state intact), falls back to a plain `<a>` on\n // non-Next embedders. A raw `<a href>` was hard-navigating +\n // losing TanStack-Query state on back, leaving /tickets stuck on\n // its skeleton.\n return (\n <Link href={href} id={id} className={baseClass} prefetch={false}>\n {inner}\n </Link>\n );\n }\n\n return (\n <div id={id} className={baseClass}>\n {inner}\n </div>\n );\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/chunk-WTYQCT4A.cjs","../src/components/list-empty-state.tsx","../src/components/shared/dev-section/dev-section-view.tsx","../src/components/shared/dev-section/dev-section-page.tsx","../src/components/shared/dev-section/dev-card-row.tsx","../src/components/shared/delivery/delivery-row.tsx"],"names":["jsx","jsxs"],"mappings":"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","file":"/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/chunk-WTYQCT4A.cjs","sourcesContent":[null,"'use client';\n\nimport { EmptyState } from './empty-state';\n\nexport interface ListEmptyStateCopy {\n title: string;\n description: string;\n}\n\nexport interface ListEmptyStateProps {\n /** A search or filter is narrowing the list, so \"nothing to show\" is a miss, not an empty list. */\n isFiltered: boolean;\n /** Copy for the filtered miss. `clearText` labels the reset button (default \"Clear filters\"). */\n filtered: ListEmptyStateCopy & { clearText?: string };\n /** Resets every search and filter the list owns. */\n onClearFilters: () => void;\n /**\n * Copy for a genuinely empty list, with an optional call to action (e.g. \"Create the first one\").\n * The CTA renders only when `ctaText` comes with `onCtaClick` or `ctaHref`, so an empty list with\n * no action never falls through to `EmptyState`'s path-sniffed default button.\n */\n empty: ListEmptyStateCopy & { ctaText?: string; onCtaClick?: () => void; ctaHref?: string };\n}\n\n/**\n * THE empty state of a searchable, filterable list: a filtered miss offers to clear the filters\n * (`search` icon, secondary button), a genuinely empty list says so and optionally offers its own\n * action (`generic` icon, primary button). Every list with filters renders this instead of choosing\n * between two `EmptyState`s by hand, so the two cases look and behave the same everywhere.\n */\nexport function ListEmptyState({ isFiltered, filtered, onClearFilters, empty }: ListEmptyStateProps) {\n if (isFiltered) {\n return (\n <EmptyState\n type=\"search\"\n title={filtered.title}\n description={filtered.description}\n ctaText={filtered.clearText ?? 'Clear filters'}\n onCtaClick={onClearFilters}\n ctaVariant=\"secondary\"\n />\n );\n }\n\n const hasCta = Boolean(empty.ctaText && (empty.onCtaClick || empty.ctaHref));\n return (\n <EmptyState\n type=\"generic\"\n title={empty.title}\n description={empty.description}\n showCTA={hasCta}\n ctaText={empty.ctaText}\n onCtaClick={empty.onCtaClick}\n ctaHref={empty.ctaHref}\n />\n );\n}\n","'use client';\n\n/**\n * DevSectionView — the canonical chrome for ANY dev-center section\n * (Roadmap / Delivery / Releases). One component, used in BOTH:\n *\n * - tabbed `/roadmap-and-releases` (compact title mode, no `hero`)\n * - full-page `/roadmap`, `/bug-fixes-and-enhancements`, `/releases`\n * (hero mode with icon + description + back link)\n *\n * Owns: title rendering, the inline search input, the filter pill row,\n * and the URL-param wiring that connects both. The list `children`\n * receive a clean URL contract — they read `?<paramKey>=...` via\n * `useSearchParams()` and refetch on change. No duplicated controls.\n */\n\nimport type { ReactNode } from 'react';\nimport { useState } from 'react';\nimport { useRouter, useSearchParams, usePathname } from '../../../embed-shims';\nimport {\n OPENFRAME_DEV_SECTIONS,\n type OpenframeDevSection,\n type OpenframeDevSectionKey,\n} from '../../../utils/dev-sections/openframe-dev-sections';\nimport { StatusFilterComponent } from '../../features';\nimport { SearchInput } from '../../ui';\n\nexport interface DevSectionViewProps {\n /** Which section to render — drives title, search, and filter\n * config via the `OPENFRAME_DEV_SECTIONS` registry. */\n sectionKey: OpenframeDevSectionKey;\n /** When set, renders the rich page-level hero (icon + h1 + description).\n * Omit for the compact tab-context heading. */\n hero?: {\n /** Pre-rendered icon JSX. Server components render the icon themselves\n * and pass the element here — function references can't cross the\n * server→client boundary, but React elements can. */\n icon: ReactNode;\n /** Hero title. Falls back to `OPENFRAME_DEV_SECTIONS[sectionKey].hero.title`\n * when omitted, so embedders can override the (OpenFrame-specific) default\n * copy without forking the registry. */\n title?: string;\n description: string;\n };\n /** Optional slot rendered BETWEEN the hero and the search/filter\n * controls. Use this for an entry-action surface that should sit\n * above the list (e.g. the Help Center's \"Open a new ticket\" form).\n * The slot is wrapped in the same `gap-10` flex column so spacing\n * matches the surrounding chrome — callers should NOT add their\n * own top/bottom margin. Renders `null` (no DOM) when omitted. */\n preControls?: ReactNode;\n /** The page-specific list body. Reads URL params written by this\n * component (search input + filter pills). */\n children: ReactNode;\n /** Render this component's own title heading (hero or compact `h2`). Default\n * `true` — the tab context renders the compact heading itself. Pass `false`\n * when the host already renders the title elsewhere (e.g. `DevSectionPage`\n * routes it through the unified `PageLayout` `TitleBlock`): then only the\n * search + filter controls + list render, with no duplicate heading. */\n showHeading?: boolean;\n}\n\nexport function DevSectionView({ sectionKey, hero, preControls, children, showHeading = true }: DevSectionViewProps) {\n const section = OPENFRAME_DEV_SECTIONS[sectionKey];\n const router = useRouter();\n const pathname = usePathname();\n const searchParams = useSearchParams();\n\n // Widen from the registry's per-section literal types to the interface —\n // optional fields (`clearParamKeys`) exist on the interface but not on\n // every section's inferred literal. Sound: the registry `satisfies`\n // `Record<string, OpenframeDevSection>`.\n const search: OpenframeDevSection['search'] = section.search;\n const filter = section.filter;\n\n const currentSearch = search ? searchParams.get(search.paramKey) || '' : '';\n const currentFilterValue = filter ? searchParams.get(filter.paramKey) || filter.defaultValue : '';\n\n // Controlled search-input state — TYPING commits to the URL only on\n // Enter (not on every keystroke), but CLEARING commits immediately:\n // an emptied input must not keep filtering the list (the URL param —\n // and any linked `clearParamKeys` companion like the tickets\n // `?ticket=` drawer param — previously survived until Enter).\n // Lazy init from URL avoids a brief flash of stale value on first\n // paint after URL-driven re-render (e.g. tab switch).\n const [searchValue, setSearchValue] = useState(() => currentSearch);\n // Re-sync when the URL changes underneath us (tab switch, back/forward,\n // a companion param cleared elsewhere). Adjusted while rendering — React's\n // documented prop-sync pattern — rather than from an effect, which would\n // commit one frame with the input still showing the previous section's\n // query before replacing it.\n const [syncedSearch, setSyncedSearch] = useState(currentSearch);\n if (syncedSearch !== currentSearch) {\n setSyncedSearch(currentSearch);\n setSearchValue(currentSearch);\n }\n\n const handleSearchSubmit = (value: string) => {\n if (!search) return;\n const params = new URLSearchParams(searchParams.toString());\n if (value.trim()) {\n params.set(search.paramKey, value.trim());\n } else {\n params.delete(search.paramKey);\n // Empty commit tears down the search's linked companion params too\n // (deep links set them TOGETHER — see the config field docs).\n for (const key of search.clearParamKeys ?? []) params.delete(key);\n }\n const qs = params.toString();\n router.replace(qs ? `${pathname}?${qs}` : pathname, { scroll: false });\n };\n\n const handleSearchChange = (value: string) => {\n setSearchValue(value);\n if (value === '' && currentSearch !== '') handleSearchSubmit('');\n };\n\n const handleFilterChange = (value: string) => {\n if (!filter) return;\n const params = new URLSearchParams(searchParams.toString());\n if (value === filter.defaultValue) params.delete(filter.paramKey);\n else params.set(filter.paramKey, value);\n router.replace(`${pathname}?${params.toString()}`, { scroll: false });\n };\n\n return (\n <div className=\"flex w-full flex-col gap-10\">\n {showHeading &&\n (hero ? (\n <div className=\"space-y-4\">\n <h1 className=\"flex items-center gap-3 tracking-[-1.12px] text-ods-text-primary text-h1\">\n {hero.icon}\n {hero.title ?? section.hero.title}\n </h1>\n <p className=\"max-w-3xl text-ods-text-secondary text-h4\">{hero.description}</p>\n </div>\n ) : (\n <div className=\"flex w-full items-center justify-between\">\n <h2 className=\"font-['Azeret_Mono'] text-[32px] font-semibold leading-[40px] tracking-[-0.64px] text-ods-text-primary md:text-[40px] md:leading-[48px] md:tracking-[-0.8px] lg:text-[48px] lg:leading-[56px] lg:tracking-[-0.96px]\">\n {section.hero.title}\n <span className=\"text-ods-accent\">:</span>\n </h2>\n </div>\n ))}\n\n {preControls}\n\n {(search || filter) && (\n <div className=\"space-y-4\">\n {search && (\n <SearchInput\n showDropdown={false}\n placeholder={search.placeholder}\n value={searchValue}\n onChange={handleSearchChange}\n onSubmit={handleSearchSubmit}\n />\n )}\n {filter && (\n <StatusFilterComponent\n selectedStatus={currentFilterValue}\n onStatusChange={handleFilterChange}\n statusOptions={[...filter.options]}\n />\n )}\n </div>\n )}\n\n {children}\n </div>\n );\n}\n","'use client';\n\n/**\n * DevSectionPage — full-page wrapper for a dev-center section\n * (`/roadmap`, `/bug-fixes-and-enhancements`, `/releases`).\n *\n * Mounts the lib's canonical `PageLayout` directly (no in-app wrapper)\n * so the back-button affordance stays in lockstep with whatever the\n * design system ships — any future lib change to BackButton / TitleBlock\n * propagates automatically.\n *\n * Composition: `PageShell` → `PageLayout` (back-to-home wired) →\n * `DevSectionView` (icon hero + search + filter pills) → list body.\n *\n * Adding a new section is one entry in `OPENFRAME_DEV_SECTIONS` plus a\n * single-line page file mounting this factory with the new key.\n */\n\nimport type { ReactNode } from 'react';\nimport { useRouter } from '../../../embed-shims/next-navigation';\nimport {\n OPENFRAME_DEV_SECTIONS,\n type OpenframeDevSectionKey,\n} from '../../../utils/dev-sections/openframe-dev-sections';\nimport { PageShell, PageLayout } from '../../ui';\nimport { DevSectionView } from './dev-section-view';\n\nexport interface DevSectionPageProps {\n sectionKey: OpenframeDevSectionKey;\n /** The page-specific list body (e.g. `<RoadmapList />`). */\n children: ReactNode;\n /** Optional slot rendered BETWEEN the hero and search/filter — see\n * `DevSectionView.preControls`. Used by surfaces that want an entry\n * action (e.g. Help Center's \"Open a new ticket\" form) above the\n * controls instead of below them. */\n preControls?: ReactNode;\n /** Back-button config — same shape as `LegalDocumentPage` /\n * `ReleaseDetailPage`. Pass `false` to hide entirely. Default\n * `{ label: 'Back to home', href: '/' }` — embedders whose \"home\" isn't `/`\n * should override `href`, or pass `false` if the embed has no home page. */\n backButton?: { label?: string; href?: string } | false;\n /** Override the hero title. Defaults to the (OpenFrame-specific) copy in\n * `OPENFRAME_DEV_SECTIONS[sectionKey].hero.title`. Set this to brand the\n * section for a non-OpenFrame embed. */\n title?: string;\n /** Override the hero subtitle/description. Defaults to\n * `OPENFRAME_DEV_SECTIONS[sectionKey].hero.description`. */\n subtitle?: string;\n /** Render the standalone `<PageShell>` (own `<main>` + bg + max-width). Default\n * `true` — the contract for marketing/hub surfaces with no app shell. Pass\n * `false` when the host layout already provides the page container (e.g.\n * openframe-frontend's `AppLayout` `<main>`): then only the\n * `page-shell-content` padding box is rendered, avoiding a nested `<main>`. */\n shell?: boolean;\n}\n\nexport function DevSectionPage({\n sectionKey,\n children,\n preControls,\n backButton,\n title,\n subtitle,\n shell = true,\n}: DevSectionPageProps) {\n const router = useRouter();\n const section = OPENFRAME_DEV_SECTIONS[sectionKey];\n\n // Back-button config — mirrors LegalDocumentPage / ReleaseDetailPage.\n // Default: { label: 'Back to home', href: '/' }. Pass `false` to hide.\n // After `backButton &&` narrowing, inner type is `{ label?, href? } |\n // undefined`; don't re-compare to `false` (TS2367).\n const backCfg =\n backButton === false\n ? undefined\n : {\n label: (backButton ? backButton.label : undefined) ?? 'Back to home',\n onClick: () => router.push((backButton ? backButton.href : undefined) ?? '/'),\n };\n\n const inner = (\n // Unified header: title/description route through the canonical (frozen)\n // `PageLayout` `TitleBlock` (text-h2) — same as FAQ / Legal / detail pages —\n // so every help-center surface shares one header. `DevSectionView` then\n // renders ONLY its search + filter controls (`showHeading={false}`), no\n // duplicate title. (The hero icon is intentionally dropped: TitleBlock is\n // frozen and renders title-only.)\n <PageLayout\n title={title ?? section.hero.title}\n subtitle={subtitle ?? section.hero.description}\n titleSize=\"h1\"\n titleWrap\n backButton={backCfg}\n >\n <DevSectionView sectionKey={sectionKey} showHeading={false} preControls={preControls}>\n {children}\n </DevSectionView>\n </PageLayout>\n );\n\n // `shell` true → standalone `<PageShell>` (own <main> + bg + max-width).\n // false → padding-only box (no nested <main>) for hosts whose layout already\n // provides the container; both consume the host's `--page-shell-*` vars.\n return shell ? <PageShell>{inner}</PageShell> : <div className=\"page-shell-content\">{inner}</div>;\n}\n","'use client';\n\n/**\n * Shared row chrome for any `DevSectionPage` list (delivery, tickets,\n * future sections). One source of truth for the layout that every\n * dev-section card row uses:\n * left column → title (h3) / subtitle (h5 uppercase) / description\n * (h4 line-clamp-3), each in a fixed min-height block\n * so rows align across the grid\n * right column → caller-supplied stacked badges\n *\n * Surface stays small on purpose — `rightBadges` is a `ReactNode` so\n * the caller decides how many badges (delivery: 2, tickets: 1-2,\n * future: anything). No behavior baked in: the caller wraps the row\n * in a `<div>` (static, like delivery) or `<button>` (clickable, like\n * tickets) and renders the row content via this component.\n *\n * Pair with `DevCardRowSkeletonList` for the loading state — the\n * skeleton mirrors the same min-heights so the in-flight UI doesn't\n * shift the layout when real data lands.\n *\n * NOTE: the ticket conversation row is NOT here — it renders the shared\n * `<ChatMessageRow>` (`components/chat/chat-message-row.tsx`), the SAME\n * component the OpenMSP Slack-community feed uses, so the two surfaces stay\n * pixel-identical by construction.\n */\n\nimport type { ReactNode } from 'react';\n\nexport interface DevCardRowContentProps {\n title: string;\n /** Single-line uppercase metadata (e.g. \"UPDATED today, #4271, Code review\"). */\n subtitle: string;\n /** 3-line description block. Empty string renders the fallback. */\n description: string;\n /** Fallback copy when `description` is empty. Defaults to a generic\n * string; ticket / delivery surfaces override. */\n emptyDescription?: string;\n /** Right column — caller renders its own stacked badges. */\n rightBadges: ReactNode;\n}\n\nexport function DevCardRowContent({\n title,\n subtitle,\n description,\n emptyDescription = 'No description provided',\n rightBadges,\n}: DevCardRowContentProps) {\n return (\n <div className=\"flex w-full flex-col items-start justify-between gap-[12px] md:flex-row md:gap-[16px]\">\n <div className=\"flex w-full min-w-0 flex-1 flex-col gap-[12px] md:w-auto md:gap-[16px]\">\n <div className=\"flex min-h-[24px] items-center\">\n <h3 className=\"line-clamp-2 flex-1 break-words tracking-[-0.36px] text-ods-text-primary text-h3 md:truncate\">\n {title}\n </h3>\n </div>\n <div className=\"flex min-h-[20px] items-center\">\n <p className=\"truncate uppercase tracking-[-0.28px] text-ods-text-secondary text-h5\">{subtitle}</p>\n </div>\n <div className=\"flex min-h-[72px] items-center\">\n <p className=\"line-clamp-3 break-words text-ods-text-secondary text-h4\">{description || emptyDescription}</p>\n </div>\n </div>\n <div className=\"flex flex-shrink-0 flex-col gap-2 self-start\">{rightBadges}</div>\n </div>\n );\n}\n\n/**\n * Skeleton rendering for a single row — the bars mirror the same\n * min-heights as `DevCardRowContent` so the loading→loaded swap\n * doesn't reflow.\n */\nexport function DevCardRowSkeleton() {\n return (\n <div className=\"border-b border-ods-border p-[12px] last:border-b-0 md:p-[16px]\">\n <div className=\"flex w-full flex-col items-start justify-between gap-[12px] md:flex-row md:gap-[16px]\">\n <div className=\"flex w-full min-w-0 flex-1 flex-col gap-[12px] md:w-auto md:gap-[16px]\">\n <div className=\"flex min-h-[24px] items-center\">\n <div className=\"h-[20px] w-full animate-pulse rounded bg-ods-border\" />\n </div>\n <div className=\"flex min-h-[20px] items-center\">\n <div className=\"h-[20px] w-1/2 animate-pulse rounded bg-ods-border\" />\n </div>\n <div className=\"flex min-h-[72px] items-center\">\n <div className=\"flex-1 space-y-1\">\n <div className=\"h-[20px] w-full animate-pulse rounded bg-ods-border\" />\n <div className=\"h-[20px] w-full animate-pulse rounded bg-ods-border\" />\n <div className=\"h-[20px] w-2/3 animate-pulse rounded bg-ods-border\" />\n </div>\n </div>\n </div>\n <div className=\"flex flex-shrink-0 flex-col gap-2 self-start\">\n <div className=\"h-[32px] w-[100px] animate-pulse rounded bg-ods-border\" />\n <div className=\"h-[32px] w-[120px] animate-pulse rounded bg-ods-border\" />\n </div>\n </div>\n </div>\n );\n}\n\n/**\n * The standard \"5 skeleton rows inside a bordered card\" loading state\n * used by every list shell. Both delivery (`delivery-table.tsx`) and\n * tickets (`tickets-list.tsx`) mount this directly.\n */\nexport function DevCardRowSkeletonList({ rows = 5 }: { rows?: number }) {\n return (\n <div className=\"w-full overflow-hidden rounded-[6px] border border-ods-border bg-ods-card\">\n {Array.from({ length: rows }, (_, i) => (\n <DevCardRowSkeleton key={i} />\n ))}\n </div>\n );\n}\n","'use client';\n\n/**\n * `<DeliveryRow />` — canonical single-row presentation for a ClickUp\n * delivery item.\n *\n * Single source of truth: both the `/bug-fixes-and-enhancements` page\n * (via `DeliveryTable`) AND the linked-delivery card on a HubSpot ticket\n * (via `TicketLinkedDeliveryCard`) compose this primitive. Visual parity\n * across those two surfaces is the design goal — the user reads the\n * card on their ticket and recognises it as a row from the public\n * delivery list.\n *\n * Behaviors:\n * - `href` set → outer element is an `<a>`, the whole row becomes\n * clickable (used by the linked-card surface to deep-link into\n * `/bug-fixes-and-enhancements?focus=<id>`).\n * - `id` set → outer element gets that DOM id so the consuming page\n * can `scrollIntoView` to it when the URL carries `?focus=<id>`.\n * - `highlighted` true → brief accent border + background pulse\n * (`animate-flash-focus` keyframe defined in `tailwind.config.ts`).\n * - `caption` set → small uppercase label rendered above the title\n * (\"LINKED DELIVERY\" on the ticket-side variant). Omitted on the\n * standard list rendering.\n */\n\nimport Link from '../../../embed-shims/next-link';\nimport { type DeliveryItem, TASK_TYPE_LABELS, TASK_TYPE_TEXT_COLORS } from '../../../types/delivery';\nimport { getStatusColorScheme } from '../../../utils';\nimport { cn } from '../../../utils/cn';\nimport { AvatarStack } from '../../ui/avatar-stack';\nimport { StatusBadge } from '../../ui/status-badge';\n\n/** Same heuristic as DeliveryTable's local helper. Inlined so the row\n * primitive owns its complete rendering contract. */\nfunction getRelativeTime(timestamp: number): string {\n // A missing or zero timestamp is ABSENT data, not the epoch — without this\n // the row reads \"678 months ago\" for a task the API never dated.\n if (!timestamp) return 'recently';\n const now = Date.now();\n const diff = now - timestamp;\n const days = Math.floor(diff / (1000 * 60 * 60 * 24));\n const weeks = Math.floor(days / 7);\n const months = Math.floor(days / 30);\n if (months > 0) return months === 1 ? 'last month' : `${months} months ago`;\n if (weeks > 0) return weeks === 1 ? 'last week' : `${weeks} weeks ago`;\n if (days > 0) return days === 1 ? 'yesterday' : `${days} days ago`;\n return 'today';\n}\n\nexport interface DeliveryRowProps {\n item: DeliveryItem;\n /** When set, the row becomes a clickable anchor. The ticket-side\n * linked-card composes this from `buildDevSectionUrl('delivery', id)`\n * which carries `?search=<id>` — the delivery list filters to that\n * exact task on landing (canonical deep-link mechanism, same one\n * the chat-inline delivery card uses). */\n href?: string;\n /** Small uppercase caption rendered above the title. Used by the\n * linked-delivery card variant (\"LINKED DELIVERY\"). */\n caption?: string;\n /** DOM `id` applied to the row's outer element. `DeliveryTable`\n * always sets `delivery-<external_id>` so chat-card deep-links\n * (`?search=<id>#delivery-<id>`) and the ticket linked-card path\n * both have a target for `useScrollToHash` to scroll to. Always\n * paired with `scroll-mt-24` on the outer element so the row lands\n * BELOW the sticky chrome after the scroll. */\n id?: string;\n /** `'_blank'` renders a plain external anchor (new tab, `rel` hardened) —\n * for rows whose href leaves the app (e.g. the raw ClickUp task URL on the\n * design-doc surfaces). Internal deep-links omit it and keep the Link shim. */\n target?: '_blank';\n className?: string;\n}\n\nexport function DeliveryRow({ item, href, target, caption, id, className }: DeliveryRowProps) {\n const taskType = item.taskType as keyof typeof TASK_TYPE_LABELS;\n const typeBadgeLabel = TASK_TYPE_LABELS[taskType] || 'TASK';\n const typeBadgeTextColor = TASK_TYPE_TEXT_COLORS[taskType] || '';\n const statusBadgeScheme = getStatusColorScheme(item.status);\n const relativeTime = getRelativeTime(item.dateUpdated);\n const subtitle = `ACTIVE ${relativeTime}${item.listNames.length > 0 ? `, ${item.listNames.join(', ')}` : ''}, ${item.id}`;\n\n const inner = (\n <div className=\"flex w-full flex-col items-start justify-between gap-[12px] md:flex-row md:gap-[16px]\">\n {/* Left: caption (optional) + title + subtitle + description */}\n <div className=\"flex w-full min-w-0 flex-1 flex-col gap-[12px] md:w-auto md:gap-[16px]\">\n {caption && <p className=\"text-ods-text-secondary text-h5\">{caption}</p>}\n <div className=\"flex min-h-[24px] items-center md:min-h-[24px]\">\n <h3 className=\"line-clamp-2 flex-1 break-words tracking-[-0.36px] text-ods-text-primary text-h3 md:truncate\">\n {item.title}\n </h3>\n </div>\n <div className=\"flex min-h-[20px] items-center\">\n {/* Relative time is wall-clock-derived: on an SSR host the server and\n client render different strings a moment apart — text-only, so\n suppress instead of forcing a client-only render. */}\n <p className=\"truncate uppercase tracking-[-0.28px] text-ods-text-secondary text-h5\" suppressHydrationWarning>\n {subtitle}\n </p>\n </div>\n <div className=\"flex min-h-[72px] items-center\">\n <p className=\"line-clamp-3 break-words text-ods-text-secondary text-h4\">\n {item.description || 'No description provided'}\n </p>\n </div>\n </div>\n\n {/* Right (desktop): badges + assignee stack in a right-aligned\n column. Mobile: ONE horizontal footer row — badges lead,\n avatars anchor the right edge (kanban-card convention: the\n assignee owns the trailing slot of the meta row; a column of\n right-aligned orphans under the description reads as clutter\n on small screens). */}\n <div className=\"flex w-full flex-shrink-0 flex-row items-center gap-2 self-stretch md:w-auto md:flex-col md:items-end md:self-start\">\n <StatusBadge\n text={item.status.toUpperCase()}\n colorScheme={statusBadgeScheme}\n variant=\"card\"\n className=\"border border-ods-border\"\n />\n <StatusBadge\n text={typeBadgeLabel}\n variant=\"card\"\n className={`border border-ods-border ${typeBadgeTextColor}`}\n />\n {item.assignees && item.assignees.length > 0 ? (\n <AvatarStack\n size=\"xs\"\n className=\"ml-auto md:ml-0\"\n people={item.assignees.map(a => ({\n key: a.id,\n name: a.name ?? 'Unknown',\n avatarUrl: a.avatarUrl,\n }))}\n />\n ) : null}\n </div>\n </div>\n );\n\n const baseClass = cn(\n 'block p-[12px] text-inherit no-underline transition-colors duration-150 md:p-[16px]',\n // `scroll-mt-24` is paid for whether `id` is set or not (it's a\n // single Tailwind utility, no runtime cost). Keeping it\n // unconditional means a future caller adding `id` doesn't also\n // have to remember to ask for the offset.\n 'scroll-mt-24',\n href && 'cursor-pointer hover:bg-ods-bg-hover',\n className,\n );\n\n if (href && target === '_blank') {\n return (\n <a href={href} id={id} className={baseClass} target=\"_blank\" rel=\"noopener noreferrer\">\n {inner}\n </a>\n );\n }\n\n if (href) {\n // `Link` is the env-aware embed-shim — delegates to `next/link` on\n // a Next.js host (soft RSC nav, back-button restores the previous\n // page's React state intact), falls back to a plain `<a>` on\n // non-Next embedders. A raw `<a href>` was hard-navigating +\n // losing TanStack-Query state on back, leaving /tickets stuck on\n // its skeleton.\n return (\n <Link href={href} id={id} className={baseClass} prefetch={false}>\n {inner}\n </Link>\n );\n }\n\n return (\n <div id={id} className={baseClass}>\n {inner}\n </div>\n );\n}\n"]}
|