@flamingo-stack/openframe-frontend-core 0.0.312 → 0.0.313-snapshot.20260623203621

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/dist/{chunk-7KKIACLD.cjs → chunk-2LFQJYLQ.cjs} +7 -7
  2. package/dist/{chunk-7KKIACLD.cjs.map → chunk-2LFQJYLQ.cjs.map} +1 -1
  3. package/dist/{chunk-UFJVTOGS.cjs → chunk-46UZAYUT.cjs} +29 -29
  4. package/dist/{chunk-UFJVTOGS.cjs.map → chunk-46UZAYUT.cjs.map} +1 -1
  5. package/dist/{chunk-G56GYN7Z.cjs → chunk-5ATH263N.cjs} +4 -1
  6. package/dist/chunk-5ATH263N.cjs.map +1 -0
  7. package/dist/{chunk-52MEECZB.cjs → chunk-AD7TII2A.cjs} +5 -5
  8. package/dist/{chunk-52MEECZB.cjs.map → chunk-AD7TII2A.cjs.map} +1 -1
  9. package/dist/{chunk-AHVG5CFA.cjs → chunk-BHOGI57O.cjs} +38 -38
  10. package/dist/{chunk-AHVG5CFA.cjs.map → chunk-BHOGI57O.cjs.map} +1 -1
  11. package/dist/{chunk-CGR2DPPQ.js → chunk-BJ6JXN5Z.js} +5 -5
  12. package/dist/{chunk-7G7QJNLY.cjs → chunk-DD35H7HA.cjs} +40 -40
  13. package/dist/{chunk-7G7QJNLY.cjs.map → chunk-DD35H7HA.cjs.map} +1 -1
  14. package/dist/{chunk-F45P357Q.js → chunk-E2LC43T3.js} +2 -2
  15. package/dist/{chunk-JQ2EYXWR.js → chunk-E4CQ4RUG.js} +4 -1
  16. package/dist/chunk-E4CQ4RUG.js.map +1 -0
  17. package/dist/{chunk-NQDC366J.cjs → chunk-EC4DGRN6.cjs} +88 -63
  18. package/dist/chunk-EC4DGRN6.cjs.map +1 -0
  19. package/dist/{chunk-GRBFBBSX.js → chunk-JWX6NIQ4.js} +2 -2
  20. package/dist/{chunk-PH2RLC4E.js → chunk-L7BROXZ7.js} +2 -2
  21. package/dist/{chunk-MI6TET5N.js → chunk-NH2RY6VM.js} +38 -13
  22. package/dist/{chunk-MI6TET5N.js.map → chunk-NH2RY6VM.js.map} +1 -1
  23. package/dist/{chunk-GZPOUZAY.js → chunk-OD3BEWDQ.js} +3 -3
  24. package/dist/{chunk-JQLC2FVM.js → chunk-TRSDXD23.js} +2 -2
  25. package/dist/{chunk-64JGK22Q.cjs → chunk-UNKIRZVY.cjs} +19 -19
  26. package/dist/{chunk-64JGK22Q.cjs.map → chunk-UNKIRZVY.cjs.map} +1 -1
  27. package/dist/{chunk-DJBMLHN7.js → chunk-UO27TVAO.js} +3 -3
  28. package/dist/{chunk-SPFV5TFS.cjs → chunk-VCJOLKED.cjs} +12 -12
  29. package/dist/{chunk-SPFV5TFS.cjs.map → chunk-VCJOLKED.cjs.map} +1 -1
  30. package/dist/{chunk-6GKJXZZM.cjs → chunk-WJCOWYAP.cjs} +14 -14
  31. package/dist/{chunk-6GKJXZZM.cjs.map → chunk-WJCOWYAP.cjs.map} +1 -1
  32. package/dist/{chunk-BX4MDVBL.js → chunk-XKVSR3IV.js} +4 -4
  33. package/dist/{chunk-2ZHDP22R.cjs → chunk-ZPK5HW7B.cjs} +3 -3
  34. package/dist/{chunk-2ZHDP22R.cjs.map → chunk-ZPK5HW7B.cjs.map} +1 -1
  35. package/dist/{chunk-IHCOTCIG.js → chunk-ZW3NHMG7.js} +3 -3
  36. package/dist/components/case-studies/index.cjs +9 -9
  37. package/dist/components/case-studies/index.js +3 -3
  38. package/dist/components/chat/index.cjs +3 -3
  39. package/dist/components/chat/index.js +2 -2
  40. package/dist/components/contact/index.cjs +4 -4
  41. package/dist/components/contact/index.js +3 -3
  42. package/dist/components/docs/index.cjs +6 -6
  43. package/dist/components/docs/index.js +5 -5
  44. package/dist/components/embeds/index.cjs +4 -4
  45. package/dist/components/embeds/index.js +3 -3
  46. package/dist/components/faq/index.cjs +5 -5
  47. package/dist/components/faq/index.js +4 -4
  48. package/dist/components/features/index.cjs +3 -3
  49. package/dist/components/features/index.js +2 -2
  50. package/dist/components/features/time-tracker/time-tracker-panel.d.ts +1 -1
  51. package/dist/components/features/time-tracker/time-tracker-panel.d.ts.map +1 -1
  52. package/dist/components/features/time-tracker/types.d.ts +2 -2
  53. package/dist/components/features/time-tracker/types.d.ts.map +1 -1
  54. package/dist/components/index.cjs +190 -188
  55. package/dist/components/index.cjs.map +1 -1
  56. package/dist/components/index.js +12 -10
  57. package/dist/components/index.js.map +1 -1
  58. package/dist/components/layout/page-layout.d.ts +1 -1
  59. package/dist/components/layout/page-layout.d.ts.map +1 -1
  60. package/dist/components/layout/title-block.d.ts +10 -0
  61. package/dist/components/layout/title-block.d.ts.map +1 -1
  62. package/dist/components/navigation/index.cjs +3 -3
  63. package/dist/components/navigation/index.js +2 -2
  64. package/dist/components/onboarding-guides/index.cjs +29 -29
  65. package/dist/components/onboarding-guides/index.js +5 -5
  66. package/dist/components/related-content/index.cjs +5 -5
  67. package/dist/components/related-content/index.js +4 -4
  68. package/dist/components/tickets/help-center-list.d.ts +5 -1
  69. package/dist/components/tickets/help-center-list.d.ts.map +1 -1
  70. package/dist/components/tickets/index.cjs +84 -73
  71. package/dist/components/tickets/index.cjs.map +1 -1
  72. package/dist/components/tickets/index.js +21 -10
  73. package/dist/components/tickets/index.js.map +1 -1
  74. package/dist/components/tool-icon.d.ts.map +1 -1
  75. package/dist/components/ui/dashboard-info-card.d.ts +3 -1
  76. package/dist/components/ui/dashboard-info-card.d.ts.map +1 -1
  77. package/dist/components/ui/index.cjs +5 -3
  78. package/dist/components/ui/index.cjs.map +1 -1
  79. package/dist/components/ui/index.js +4 -2
  80. package/dist/hooks/index.cjs +2 -2
  81. package/dist/hooks/index.js +1 -1
  82. package/dist/index.cjs +5 -3
  83. package/dist/index.cjs.map +1 -1
  84. package/dist/index.js +4 -2
  85. package/dist/types/index.cjs +2 -0
  86. package/dist/types/index.cjs.map +1 -1
  87. package/dist/types/index.js +2 -0
  88. package/dist/types/index.js.map +1 -1
  89. package/dist/types/tool.types.d.ts +1 -0
  90. package/dist/types/tool.types.d.ts.map +1 -1
  91. package/dist/utils/index.cjs +11 -0
  92. package/dist/utils/index.cjs.map +1 -1
  93. package/dist/utils/index.js +11 -0
  94. package/dist/utils/index.js.map +1 -1
  95. package/dist/utils/tool-utils.d.ts.map +1 -1
  96. package/package.json +1 -1
  97. package/src/components/features/time-tracker/time-tracker-panel.tsx +27 -9
  98. package/src/components/features/time-tracker/types.ts +2 -2
  99. package/src/components/layout/page-layout.tsx +1 -1
  100. package/src/components/layout/title-block.tsx +12 -1
  101. package/src/components/tickets/help-center-list.tsx +16 -4
  102. package/src/components/tool-icon.tsx +1 -0
  103. package/src/components/ui/dashboard-info-card.tsx +9 -1
  104. package/src/stories/TimeTracker.stories.tsx +10 -10
  105. package/src/types/tool.types.ts +2 -0
  106. package/src/utils/tool-utils.ts +11 -0
  107. package/dist/chunk-G56GYN7Z.cjs.map +0 -1
  108. package/dist/chunk-JQ2EYXWR.js.map +0 -1
  109. package/dist/chunk-NQDC366J.cjs.map +0 -1
  110. /package/dist/{chunk-CGR2DPPQ.js.map → chunk-BJ6JXN5Z.js.map} +0 -0
  111. /package/dist/{chunk-F45P357Q.js.map → chunk-E2LC43T3.js.map} +0 -0
  112. /package/dist/{chunk-GRBFBBSX.js.map → chunk-JWX6NIQ4.js.map} +0 -0
  113. /package/dist/{chunk-PH2RLC4E.js.map → chunk-L7BROXZ7.js.map} +0 -0
  114. /package/dist/{chunk-GZPOUZAY.js.map → chunk-OD3BEWDQ.js.map} +0 -0
  115. /package/dist/{chunk-JQLC2FVM.js.map → chunk-TRSDXD23.js.map} +0 -0
  116. /package/dist/{chunk-DJBMLHN7.js.map → chunk-UO27TVAO.js.map} +0 -0
  117. /package/dist/{chunk-BX4MDVBL.js.map → chunk-XKVSR3IV.js.map} +0 -0
  118. /package/dist/{chunk-IHCOTCIG.js.map → chunk-ZW3NHMG7.js.map} +0 -0
@@ -1 +0,0 @@
1
- {"version":3,"sources":["/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/chunk-G56GYN7Z.cjs","../src/hooks/ui/use-auto-limit-tags.ts","../src/hooks/ui/use-debounce.ts","../src/hooks/ui/use-header-height.ts","../src/hooks/ui/use-horizontal-scrollbar.ts","../src/hooks/ui/use-image-edge-color.ts","../src/hooks/ui/use-local-storage.ts","../src/hooks/ui/use-media-query.ts","../src/hooks/ui/use-memoized-callback.ts","../src/hooks/ui/use-notification-permission.ts","../src/hooks/ui/use-onboarding-state.ts","../src/utils/onboarding-storage.ts","../src/hooks/ui/use-search.ts","../src/hooks/ui/use-table-pagination.ts","../src/hooks/ui/use-throttle.ts","../src/hooks/ui/use-window-size.ts","../src/hooks/platform/use-platform-config.ts","../src/utils/platform-config.tsx","../src/hooks/use-toast.ts","../src/components/ui/toaster.tsx","../src/types/tool.types.ts","../src/components/tool-icon.tsx","../src/hooks/use-contact-submission.ts","../src/utils/local-storage-adapter.ts","../src/utils/app-config.ts","../src/utils/embed-proxy-auth-storage.ts","../src/utils/embed-authed-fetch.ts","../src/utils/embed-content-fetch.ts","../src/hooks/use-quick-action-hint.ts","../src/hooks/use-copy-to-clipboard.ts","../src/hooks/use-batch-images.ts","../src/hooks/use-authenticated-image.ts","../src/hooks/state/use-query-params.ts","../src/hooks/state/graphql-parser.ts","../src/hooks/state/flatten-schema.ts","../src/hooks/state/url-converter.ts","../src/hooks/state/introspection.ts","../src/hooks/state/use-api-params.ts","../src/hooks/state/use-cursor-pagination-state.ts","../src/hooks/nats/use-nats-client.ts","../src/hooks/use-near-viewport.ts","../src/hooks/use-access-code-integration.ts","../src/utils/access-code-client.ts","../src/hooks/use-og-placeholder-url.ts","../src/utils/og-placeholder.ts","../src/hooks/use-scroll-to-hash.ts","../src/utils/scroll-into-view.ts","../src/utils/same-page-hash-nav.ts","../src/hooks/use-humanity-signals.ts","../src/utils/humanity-signals.ts"],"names":["useState","useEffect","header","bar","useRef","useCallback","r","g","b","dismissOnboarding","markMultipleComplete","platforms","sonnerToast","jsx","Fragment","adapter","toast","useMemo","fetchPromise","introspector","isInputObjectType","clearParams"],"mappings":"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ACL,UAAA;AACH,QAAA;AACG,UAAA;AACV,QAAA;AACF,MAAA;AACK,MAAA;AACP,IAAA;AACgB,IAAA;AACT,IAAA;AACM,IAAA;AACJ,MAAA;AACI,MAAA;AACb,IAAA;AACY,EAAA;AAChB;A7Cg4GwB;AACA;AgDx8GfI;AhD08Ge;AACA;AiD77GM;AAEE;AAEnB;AASG;AACM,EAAA;AACJ,EAAA;AAKC,EAAA;AACD,EAAA;AACE,EAAA;AACC,EAAA;AACrB;AAOgB;AACI,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACpB;AAG4B;AjDy6GJ;AACA;AgDx8GR;AACuB,EAAA;AAGnBD,EAAAA;AAECC,EAAAA;AACO,IAAA;AACJ,MAAA;AACD,MAAA;AAEnB,IAAA;AACC,IAAA;AACH,EAAA;AAEqBA,EAAAA;AACF,IAAA;AACN,IAAA;AACR,EAAA;AAEI,EAAA;AACX;AhDo8GwB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/chunk-G56GYN7Z.cjs","sourcesContent":[null,"\"use client\"\n\nimport { useCallback, useEffect, useRef, useState } from \"react\"\n\nexport interface UseAutoLimitTagsOptions {\n /** Total number of tags */\n count: number\n /** Fixed limit or \"auto\" for DOM-based measurement. Default \"auto\" */\n limitTags?: number | \"auto\"\n /** Placeholder text used to reserve input width (only used in \"auto\" mode) */\n placeholder?: string\n /**\n * Reserve trailing space for a text input (the autocomplete combobox). Set\n * `false` for a pure chip row that has no input — then only the chips and the\n * \"+N\" / \"⋯\" overflow badge compete for the available width. Default `true`.\n */\n reserveInputWidth?: boolean\n}\n\nexport interface UseAutoLimitTagsReturn {\n /** How many tags to show */\n visibleCount: number\n /** Ref for the zone that contains tags + input (must have overflow-hidden, gap, padding) */\n middleRef: React.RefObject<HTMLDivElement | null>\n /** Ref for the off-screen container that holds measurement copies of ALL tags */\n measureRef: React.RefObject<HTMLDivElement | null>\n /** Ref for the hidden span that measures placeholder text width */\n textMeasureRef: React.RefObject<HTMLSpanElement | null>\n /** Ref for the \"+N\" badge element (used to measure its width) */\n badgeRef: React.RefObject<HTMLButtonElement | null>\n /** Ref for the input element (used to read its min-width) */\n inputRef: React.RefObject<HTMLInputElement | null>\n}\n\n/**\n * Calculates how many tags fit in a single-line container.\n *\n * Requires three off-screen measurement elements rendered by the consumer:\n * 1. A `<div ref={measureRef}>` containing Tag copies for every item (to measure widths)\n * 2. A `<span ref={textMeasureRef}>` containing the placeholder text (to reserve input width)\n * 3. The `<button ref={badgeRef}>` for the \"+N\" badge (to measure badge width)\n *\n * The hook reads real CSS values (padding, gap) from the middleRef container,\n * so it works regardless of responsive breakpoints or custom styling.\n */\nexport function useAutoLimitTags({\n count,\n limitTags = \"auto\",\n placeholder = \"\",\n reserveInputWidth = true,\n}: UseAutoLimitTagsOptions): UseAutoLimitTagsReturn {\n const middleRef = useRef<HTMLDivElement>(null)\n const measureRef = useRef<HTMLDivElement>(null)\n const textMeasureRef = useRef<HTMLSpanElement>(null)\n const badgeRef = useRef<HTMLButtonElement>(null)\n const inputRef = useRef<HTMLInputElement>(null)\n const [visibleCount, setVisibleCount] = useState(count)\n\n const recalculate = useCallback(() => {\n // Fixed limit — skip DOM measurement\n if (limitTags !== \"auto\") {\n setVisibleCount(Math.min(limitTags, count))\n return\n }\n\n const middle = middleRef.current\n const measure = measureRef.current\n if (!middle || !measure) {\n setVisibleCount(count)\n return\n }\n if (count === 0) {\n setVisibleCount(0)\n return\n }\n\n // Read real CSS metrics from the middle zone\n const cs = getComputedStyle(middle)\n const padL = parseFloat(cs.paddingLeft) || 0\n const padR = parseFloat(cs.paddingRight) || 0\n const gap = parseFloat(cs.gap) || 0\n const middleW = middle.clientWidth\n\n // Reserve trailing space for the input (autocomplete only). A pure chip row\n // (`reserveInputWidth: false`) has no input, so nothing — and no gap before\n // it — is reserved.\n let inputReservedW = 0\n let trailingGap = 0\n if (reserveInputWidth) {\n const textW = textMeasureRef.current?.offsetWidth ?? 60\n const inputMinW = inputRef.current\n ? parseFloat(getComputedStyle(inputRef.current).minWidth) || 60\n : 60\n inputReservedW = Math.max(textW + 8, inputMinW)\n trailingGap = gap\n }\n\n // Available = middle zone − padding − input reserved − gap before input\n const available = middleW - padL - padR - inputReservedW - trailingGap\n\n // Measure every tag from the off-screen container\n const tagEls = Array.from(measure.children) as HTMLElement[]\n const widths = tagEls.map((el) => el.offsetWidth)\n\n // Fast check: do ALL tags fit?\n let total = 0\n for (let i = 0; i < widths.length; i++) {\n total += widths[i] + (i > 0 ? gap : 0)\n }\n if (total <= available) {\n setVisibleCount(count)\n return\n }\n\n // Not all fit → reserve space for the \"+N\" badge\n const badgeW = badgeRef.current?.offsetWidth ?? 40\n const spaceWithBadge = available - badgeW - gap\n\n let used = 0\n let fitCount = 0\n for (let i = 0; i < widths.length; i++) {\n const need = widths[i] + (i > 0 ? gap : 0)\n if (used + need > spaceWithBadge) break\n used += need\n fitCount++\n }\n\n setVisibleCount(Math.max(0, fitCount))\n }, [count, limitTags, placeholder, reserveInputWidth])\n\n // Recalculate when inputs change\n useEffect(() => {\n recalculate()\n }, [recalculate])\n\n // Recalculate on container resize\n useEffect(() => {\n const el = middleRef.current\n if (!el) return\n const ro = new ResizeObserver(recalculate)\n ro.observe(el)\n return () => ro.disconnect()\n }, [recalculate])\n\n return { visibleCount, middleRef, measureRef, textMeasureRef, badgeRef, inputRef }\n}\n","\"use client\"\n\nimport { useState, useEffect } from \"react\"\n\n/**\n * Hook to debounce a value\n * @param value - Value to debounce\n * @param delay - Delay in milliseconds\n * @returns Debounced value\n */\nexport function useDebounce<T>(value: T, delay = 500): T {\n const [debouncedValue, setDebouncedValue] = useState<T>(value)\n\n useEffect(() => {\n // Set up timeout to update debounced value\n const timer = setTimeout(() => {\n setDebouncedValue(value)\n }, delay)\n\n // Clean up timeout on value change or unmount\n return () => {\n clearTimeout(timer)\n }\n }, [value, delay])\n\n return debouncedValue\n}\n","'use client'\n\nimport { useEffect, useState } from 'react'\n\n/**\n * Returns the combined height (px) of the sticky page header and announcement\n * bar (if present), updated live via ResizeObserver / MutationObserver.\n * Useful for offsetting fixed/absolute-positioned panels so they don't\n * overlap the header.\n */\nexport function useHeaderHeight(defaultHeight = 64): number {\n const [height, setHeight] = useState(defaultHeight)\n\n useEffect(() => {\n const measure = () => {\n let total = 0\n const header = document.querySelector('header')\n if (header) total += header.offsetHeight\n const bar = document.querySelector('[data-announcement-bar]')\n if (bar instanceof HTMLElement) total += bar.offsetHeight\n setHeight(total > 0 ? total : defaultHeight)\n }\n\n measure()\n\n const resizeObserver = new ResizeObserver(measure)\n const header = document.querySelector('header')\n if (header) resizeObserver.observe(header)\n const bar = document.querySelector('[data-announcement-bar]')\n if (bar) resizeObserver.observe(bar)\n\n const mutationObserver = new MutationObserver((mutations) => {\n for (const mutation of mutations) {\n if (mutation.type === 'childList' || mutation.type === 'attributes') {\n measure()\n const newBar = document.querySelector('[data-announcement-bar]')\n if (newBar) resizeObserver.observe(newBar)\n }\n }\n })\n mutationObserver.observe(document.body, {\n childList: true,\n subtree: true,\n attributes: true,\n attributeFilter: ['data-announcement-bar'],\n })\n\n return () => {\n resizeObserver.disconnect()\n mutationObserver.disconnect()\n }\n }, [defaultHeight])\n\n return height\n}\n","'use client'\n\nimport { useRef, useState, useCallback } from 'react'\n\n/**\n * Custom horizontal scrollbar hook that drives a plain-DOM scrollbar.\n *\n * Returns a callback ref (`scrollRef`) to attach to the scrollable container,\n * plus refs, state, and event handlers for the track + thumb.\n *\n * Uses a **callback ref** so measurement and ResizeObserver setup happen\n * automatically when the scroll element mounts — even if it renders later\n * (e.g. after async data loads).\n */\nexport function useHorizontalScrollbar() {\n const scrollElRef = useRef<HTMLDivElement | null>(null)\n const trackRef = useRef<HTMLDivElement>(null)\n const thumbRef = useRef<HTMLDivElement>(null)\n const roRef = useRef<ResizeObserver | null>(null)\n const moRef = useRef<MutationObserver | null>(null)\n const [thumbRatio, setThumbRatio] = useState(0)\n\n // Edge-fade state\n const [canScrollLeft, setCanScrollLeft] = useState(false)\n const [canScrollRight, setCanScrollRight] = useState(false)\n const prevCanScrollLeftRef = useRef(false)\n const prevCanScrollRightRef = useRef(false)\n\n const isDraggingRef = useRef(false)\n const dragStartRef = useRef({ mouseX: 0, scrollLeft: 0 })\n const rafIdRef = useRef<number>(0)\n\n /** Compute the thumb left% from current scrollLeft and apply to DOM directly */\n const syncThumbToDOM = useCallback(() => {\n const el = scrollElRef.current\n const thumb = thumbRef.current\n if (!el || !thumb) return\n\n const maxScroll = el.scrollWidth - el.clientWidth\n if (maxScroll <= 0) {\n thumb.style.left = '0%'\n return\n }\n\n const ratio = el.clientWidth / el.scrollWidth\n const fraction = Math.min(Math.max(el.scrollLeft, 0), maxScroll) / maxScroll\n thumb.style.left = `${fraction * (1 - ratio) * 100}%`\n }, [])\n\n const syncEdgeFades = useCallback(() => {\n const el = scrollElRef.current\n if (!el) return\n\n const maxScroll = el.scrollWidth - el.clientWidth\n const ratio = el.clientWidth / el.scrollWidth\n if (ratio >= 1 || maxScroll <= 0) {\n if (prevCanScrollLeftRef.current) {\n prevCanScrollLeftRef.current = false\n setCanScrollLeft(false)\n }\n if (prevCanScrollRightRef.current) {\n prevCanScrollRightRef.current = false\n setCanScrollRight(false)\n }\n return\n }\n\n const fraction = Math.min(Math.max(el.scrollLeft, 0), maxScroll) / maxScroll\n const left = fraction > 0.001\n const right = fraction < 0.999\n\n if (left !== prevCanScrollLeftRef.current) {\n prevCanScrollLeftRef.current = left\n setCanScrollLeft(left)\n }\n if (right !== prevCanScrollRightRef.current) {\n prevCanScrollRightRef.current = right\n setCanScrollRight(right)\n }\n }, [])\n\n const measure = useCallback(() => {\n const el = scrollElRef.current\n if (!el) return\n const ratio = el.clientWidth / el.scrollWidth\n setThumbRatio(ratio >= 1 ? 0 : ratio)\n syncThumbToDOM()\n syncEdgeFades()\n }, [syncThumbToDOM, syncEdgeFades])\n\n // Callback ref\n const scrollRef = useCallback((node: HTMLDivElement | null) => {\n // Teardown previous\n if (roRef.current) {\n roRef.current.disconnect()\n roRef.current = null\n }\n if (moRef.current) {\n moRef.current.disconnect()\n moRef.current = null\n }\n if (rafIdRef.current) {\n cancelAnimationFrame(rafIdRef.current)\n rafIdRef.current = 0\n }\n\n scrollElRef.current = node\n\n if (node) {\n const ratio = node.clientWidth / node.scrollWidth\n setThumbRatio(ratio >= 1 ? 0 : ratio)\n\n const ro = new ResizeObserver(measure)\n roRef.current = ro\n const observeAll = () => {\n ro.disconnect()\n ro.observe(node)\n for (const child of Array.from(node.children)) ro.observe(child)\n }\n observeAll()\n\n moRef.current = new MutationObserver(() => {\n observeAll()\n measure()\n })\n moRef.current.observe(node, { childList: true })\n\n requestAnimationFrame(() => {\n syncThumbToDOM()\n syncEdgeFades()\n })\n } else {\n setThumbRatio(0)\n prevCanScrollLeftRef.current = false\n prevCanScrollRightRef.current = false\n setCanScrollLeft(false)\n setCanScrollRight(false)\n }\n }, [measure, syncThumbToDOM, syncEdgeFades])\n\n // RAF-throttled scroll handler\n const onScroll = useCallback(() => {\n if (isDraggingRef.current) return\n\n if (rafIdRef.current) cancelAnimationFrame(rafIdRef.current)\n rafIdRef.current = requestAnimationFrame(() => {\n syncThumbToDOM()\n syncEdgeFades()\n })\n }, [syncThumbToDOM, syncEdgeFades])\n\n // Track click — read ratio from DOM, smooth scroll\n const onTrackClick = useCallback((e: React.MouseEvent) => {\n if ((e.target as HTMLElement).closest('[data-scrollbar-thumb]')) return\n\n const track = trackRef.current\n const el = scrollElRef.current\n if (!track || !el) return\n\n const ratio = el.clientWidth / el.scrollWidth\n if (ratio >= 1) return\n\n const rect = track.getBoundingClientRect()\n const thumbWidth = rect.width * ratio\n const maxThumbOffset = rect.width - thumbWidth\n if (maxThumbOffset <= 0) return\n\n const targetFraction = Math.max(0, Math.min(1,\n (e.clientX - rect.left - thumbWidth / 2) / maxThumbOffset\n ))\n const targetScrollLeft = targetFraction * (el.scrollWidth - el.clientWidth)\n el.scrollTo({ left: targetScrollLeft, behavior: 'smooth' })\n }, [])\n\n // Wheel on track → forward to scroll container\n const onTrackWheel = useCallback((e: React.WheelEvent) => {\n const el = scrollElRef.current\n if (!el) return\n e.preventDefault()\n el.scrollLeft += e.deltaX || e.deltaY\n }, [])\n\n // Drag: pointer down\n const onThumbPointerDown = useCallback((e: React.PointerEvent) => {\n e.preventDefault()\n e.stopPropagation()\n const el = scrollElRef.current\n if (!el) return\n isDraggingRef.current = true\n dragStartRef.current = { mouseX: e.clientX, scrollLeft: el.scrollLeft }\n const target = e.currentTarget as HTMLElement\n target.setPointerCapture(e.pointerId)\n target.style.cursor = 'grabbing'\n }, [])\n\n // Drag: pointer move\n const onThumbPointerMove = useCallback((e: React.PointerEvent) => {\n if (!isDraggingRef.current) return\n const el = scrollElRef.current\n const track = trackRef.current\n if (!el || !track) return\n\n const currentThumbRatio = el.clientWidth / el.scrollWidth\n if (currentThumbRatio >= 1) return\n\n const trackWidth = track.clientWidth\n const thumbWidth = trackWidth * currentThumbRatio\n const maxThumbTravel = trackWidth - thumbWidth\n if (maxThumbTravel <= 0) return\n\n const mouseDelta = e.clientX - dragStartRef.current.mouseX\n const scrollRange = el.scrollWidth - el.clientWidth\n el.scrollLeft = Math.min(\n Math.max(dragStartRef.current.scrollLeft + (mouseDelta / maxThumbTravel) * scrollRange, 0),\n scrollRange\n )\n\n syncThumbToDOM()\n syncEdgeFades()\n }, [syncThumbToDOM, syncEdgeFades])\n\n // Drag: pointer up\n const onThumbPointerUp = useCallback((e: React.PointerEvent) => {\n isDraggingRef.current = false\n const target = e.currentTarget as HTMLElement\n target.releasePointerCapture(e.pointerId)\n target.style.cursor = 'grab'\n }, [])\n\n return {\n scrollRef,\n trackRef,\n thumbRef,\n thumbRatio,\n canScrollLeft,\n canScrollRight,\n onScroll,\n onTrackClick,\n onTrackWheel,\n onThumbPointerDown,\n onThumbPointerMove,\n onThumbPointerUp,\n }\n}\n","\"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 =\n x < edgeW || x >= w - edgeW ||\n 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 useEffect(() => {\n if (!imageUrl) {\n setColor(fallback);\n return;\n }\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 () => { cancelled = true; };\n }, [imageUrl, fallback]);\n\n return color;\n}\n","\"use client\"\n\nimport { useEffect, useRef, useState } from \"react\"\n\n/**\n * Hook to use localStorage with state\n * @param key - localStorage key\n * @param initialValue - Initial value if key doesn't exist\n * @returns [storedValue, setValue] tuple\n */\nexport function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T | ((val: T) => T)) => void] {\n // State to store our value\n // Use lazy initialization to read from localStorage synchronously on first render\n const [storedValue, setStoredValue] = useState<T>(() => {\n try {\n if (typeof window !== \"undefined\") {\n const item = window.localStorage.getItem(key)\n // Parse stored json or if none return initialValue\n return item ? JSON.parse(item) : initialValue\n }\n } catch (error) {\n console.error(`Error reading localStorage key \"${key}\":`, error)\n }\n return initialValue\n })\n\n // Use a ref to track if we've initialized from localStorage\n const isInitialized = useRef(true) // Set to true since we initialize in useState\n // Use a ref to track if the current state change came from a storage event\n const isFromStorageEvent = useRef(false)\n\n // Listen for storage events to sync with other tabs/components\n useEffect(() => {\n if (!isInitialized.current) return\n\n const handleStorageChange = (e: StorageEvent) => {\n if (e.key === key && e.newValue !== null) {\n try {\n const newValue = JSON.parse(e.newValue)\n isFromStorageEvent.current = true\n setStoredValue(newValue)\n console.log(`🔄 Updated localStorage key \"${key}\" from storage event`)\n } catch (error) {\n console.error(`Error parsing storage event for key \"${key}\":`, error)\n }\n }\n }\n\n const handleCustomStorageUpdate = (e: CustomEvent) => {\n if (e.detail.key === key) {\n try {\n if (e.detail.newValue === null || e.detail.newValue === undefined) {\n // localStorage was cleared or key was removed\n isFromStorageEvent.current = true\n setStoredValue(initialValue)\n console.log(`🔄 Cleared localStorage key \"${key}\" from custom storage event`)\n } else {\n const newValue = JSON.parse(e.detail.newValue)\n isFromStorageEvent.current = true\n setStoredValue(newValue)\n console.log(`🔄 Updated localStorage key \"${key}\" from custom storage event`)\n }\n } catch (error) {\n console.error(`Error parsing custom storage event for key \"${key}\":`, error)\n }\n }\n }\n\n window.addEventListener('storage', handleStorageChange)\n window.addEventListener('localStorageUpdate', handleCustomStorageUpdate as EventListener)\n \n return () => {\n window.removeEventListener('storage', handleStorageChange)\n window.removeEventListener('localStorageUpdate', handleCustomStorageUpdate as EventListener)\n }\n }, [key])\n\n // Update localStorage when state changes, but only after initialization and NOT from storage events\n useEffect(() => {\n if (!isInitialized.current) return\n\n // Skip localStorage write if this state change came from a storage event\n if (isFromStorageEvent.current) {\n isFromStorageEvent.current = false\n return\n }\n\n try {\n // Save state to localStorage\n if (typeof window !== \"undefined\") {\n window.localStorage.setItem(key, JSON.stringify(storedValue))\n }\n } catch (error) {\n console.error(`Error writing to localStorage key \"${key}\":`, error)\n }\n }, [key, storedValue])\n\n // Return a wrapped version of useState's setter function that\n // persists the new value to localStorage\n const setValue = (value: T | ((val: T) => T)) => {\n try {\n // Allow value to be a function so we have same API as useState\n const valueToStore = value instanceof Function ? value(storedValue) : value\n // Save state\n setStoredValue(valueToStore)\n } catch (error) {\n console.error(`Error setting value for localStorage key \"${key}\":`, error)\n }\n }\n\n return [storedValue, setValue]\n}\n","\"use client\"\n\nimport { useLayoutEffect, useState } from \"react\"\n\n/**\n * Hook to check if a media query matches\n * @param query - Media query to check\n * @returns Whether the media query matches, or undefined during SSR/initial render\n */\nexport function useMediaQuery(\n query: string,\n): boolean | undefined {\n const [matches, setMatches] = useState<boolean | undefined>(undefined)\n\n useLayoutEffect(() => {\n const matchMedia = window.matchMedia(query)\n\n const handleChange = () => {\n setMatches(matchMedia.matches)\n }\n\n // Set initial value\n handleChange()\n\n matchMedia.addEventListener('change', handleChange)\n\n return () => {\n matchMedia.removeEventListener('change', handleChange)\n }\n }, [query])\n\n return matches\n}\n\n/**\n * Predefined breakpoints for common screen sizes\n */\nexport const breakpoints = {\n md: \"(min-width: 800px)\", // Tablet: 50rem\n lg: \"(min-width: 1280px)\", // Desktop: 80rem\n}\n\n/** @deprecated Use useMdUp instead */\nexport function useSmUp(): boolean | undefined {\n return useMdUp()\n}\n\nexport function useMdUp(): boolean | undefined {\n const matches = useMediaQuery(breakpoints.md)\n return matches === undefined ? undefined : matches\n}\n\nexport function useLgUp(): boolean | undefined {\n const matches = useMediaQuery(breakpoints.lg)\n return matches === undefined ? undefined : matches\n}\n\n","\"use client\"\n\nimport { useCallback, useRef } from \"react\"\n\n/**\n * Hook to memoize a callback with dependencies\n * Similar to useCallback but with deep comparison of dependencies\n * @param callback - Function to memoize\n * @param dependencies - Dependencies array\n * @returns Memoized callback\n */\nexport function useMemoizedCallback<T extends (...args: any[]) => any>(callback: T, dependencies: any[]): T {\n // Store the callback and dependencies\n const callbackRef = useRef<T>(callback)\n const dependenciesRef = useRef<any[]>(dependencies)\n\n // Update the callback if it changes\n callbackRef.current = callback\n\n // Check if dependencies have changed\n const depsChanged = dependencies.some((dep, i) => !Object.is(dep, dependenciesRef.current[i]))\n\n // Update dependencies if they've changed\n if (depsChanged) {\n dependenciesRef.current = dependencies\n }\n\n // Return memoized callback\n return useCallback(((...args: any[]) => callbackRef.current(...args)) as T, [depsChanged])\n}\n","'use client'\n\nimport { useCallback, useEffect, useState } from 'react'\n\nexport interface UseNotificationPermissionResult {\n /** Whether the page-context Notification API exists in this browser (false during SSR and on iOS Safari). */\n supported: boolean\n permission: NotificationPermission\n /** Prompt the user for permission. Must be called from a user gesture or browsers will auto-deny. */\n request: () => Promise<NotificationPermission>\n}\n\n/**\n * Tracks the browser's desktop-notification permission, staying in sync when\n * the user changes the site setting externally (Permissions API change event\n * where available, visibility/focus re-read as the Safari fallback).\n */\nexport function useNotificationPermission(): UseNotificationPermissionResult {\n const [supported, setSupported] = useState(false)\n const [permission, setPermission] = useState<NotificationPermission>('default')\n\n useEffect(() => {\n if (typeof window === 'undefined' || !('Notification' in window)) return\n setSupported(true)\n const sync = () => setPermission(Notification.permission)\n sync()\n\n // The query resolves async; without the flag it would attach the listener after unmount.\n let cancelled = false\n let status: PermissionStatus | undefined\n navigator.permissions\n ?.query({ name: 'notifications' as PermissionName })\n .then((s) => {\n if (cancelled) return\n status = s\n s.addEventListener('change', sync)\n })\n .catch(() => undefined)\n\n document.addEventListener('visibilitychange', sync)\n window.addEventListener('focus', sync)\n return () => {\n cancelled = true\n status?.removeEventListener('change', sync)\n document.removeEventListener('visibilitychange', sync)\n window.removeEventListener('focus', sync)\n }\n }, [])\n\n const request = useCallback(async () => {\n if (typeof window === 'undefined' || !('Notification' in window)) {\n return 'denied' as NotificationPermission\n }\n // Older Safari only implements the callback form; resolving from both is harmless.\n const result = await new Promise<NotificationPermission>((resolve) => {\n const maybePromise = Notification.requestPermission(resolve)\n if (maybePromise && typeof maybePromise.then === 'function') void maybePromise.then(resolve)\n })\n setPermission(Notification.permission)\n return result\n }, [])\n\n return { supported, permission, request }\n}\n","'use client'\n\nimport { useState, useEffect, useCallback } from 'react'\nimport {\n saveOnboardingState,\n loadOnboardingState,\n markStepComplete as storageMarkComplete,\n markStepSkipped as storageMarkSkipped,\n dismissOnboarding as storageDismiss,\n markMultipleComplete as storageMarkMultiple,\n isStepComplete as storageIsComplete,\n isStepSkipped as storageIsSkipped,\n type OnboardingState\n} from '../../utils/onboarding-storage'\n\nexport interface OnboardingStepConfig {\n id: string\n title: string\n description: string\n actionIcon: (color?: string) => React.ReactNode\n actionText: string\n completedText: string\n onAction: () => void | Promise<void>\n onSkip?: () => void\n checkComplete?: () => boolean | Promise<boolean>\n}\n\nexport type { OnboardingState }\n\n/**\n * Hook for managing onboarding state with localStorage persistence\n * Uses simple storage utilities for atomic updates and reliable re-renders\n */\nexport function useOnboardingState(storageKey: string = 'openframe-onboarding-state') {\n const [state, setState] = useState<OnboardingState>(() => loadOnboardingState(storageKey))\n const [, forceUpdate] = useState(0)\n\n // Listen for storage changes from other tabs\n useEffect(() => {\n const handleStorageUpdate = (e: CustomEvent) => {\n if (e.detail.key === storageKey) {\n const newState = loadOnboardingState(storageKey)\n setState(newState)\n forceUpdate(prev => prev + 1)\n console.log('🔄 State updated from storage event:', newState)\n }\n }\n\n window.addEventListener('localStorageUpdate', handleStorageUpdate as EventListener)\n return () => {\n window.removeEventListener('localStorageUpdate', handleStorageUpdate as EventListener)\n }\n }, [storageKey])\n\n const markComplete = useCallback((stepId: string) => {\n console.log(`🎯 markComplete called for: \"${stepId}\"`)\n const newState = storageMarkComplete(storageKey, stepId)\n setState(newState)\n forceUpdate(prev => prev + 1)\n }, [storageKey])\n\n const markSkipped = useCallback((stepId: string) => {\n console.log(`⏭️ markSkipped called for: \"${stepId}\"`)\n const newState = storageMarkSkipped(storageKey, stepId)\n setState(newState)\n forceUpdate(prev => prev + 1)\n }, [storageKey])\n\n const dismissOnboarding = useCallback(() => {\n console.log(`🚫 dismissOnboarding called`)\n const newState = storageDismiss(storageKey)\n setState(newState)\n forceUpdate(prev => prev + 1)\n }, [storageKey])\n\n const markMultipleComplete = useCallback((stepIds: string[]) => {\n console.log(`🎯 markMultipleComplete called for:`, stepIds)\n const newState = storageMarkMultiple(storageKey, stepIds)\n setState(newState)\n forceUpdate(prev => prev + 1)\n console.log(`📝 State after batch:`, newState)\n }, [storageKey])\n\n const isStepComplete = useCallback((stepId: string): boolean => {\n return state.completedSteps.includes(stepId)\n }, [state.completedSteps])\n\n const isStepSkipped = useCallback((stepId: string): boolean => {\n return state.skippedSteps.includes(stepId)\n }, [state.skippedSteps])\n\n const allStepsComplete = useCallback((steps: OnboardingStepConfig[]): boolean => {\n return steps.every(step => isStepComplete(step.id) || isStepSkipped(step.id))\n }, [isStepComplete, isStepSkipped])\n\n return {\n state,\n markComplete,\n markSkipped,\n dismissOnboarding,\n isStepComplete,\n isStepSkipped,\n allStepsComplete,\n markMultipleComplete\n }\n}\n","/**\n * Onboarding state storage utilities\n * Simple localStorage wrapper following announcement-storage.ts pattern\n * Provides atomic writes to prevent race conditions\n */\n\nexport interface OnboardingState {\n completedSteps: string[]\n skippedSteps: string[]\n dismissed: boolean\n lastUpdated: string\n}\n\nconst DEFAULT_STATE: OnboardingState = {\n completedSteps: [],\n skippedSteps: [],\n dismissed: false,\n lastUpdated: new Date().toISOString()\n}\n\n/**\n * Save onboarding state to localStorage (atomic write)\n */\nexport function saveOnboardingState(key: string, state: OnboardingState): void {\n if (typeof window === 'undefined') return\n\n try {\n localStorage.setItem(key, JSON.stringify(state))\n console.log('💾 Saved onboarding state:', state)\n\n // Dispatch custom event for cross-tab sync\n if (typeof window !== 'undefined') {\n window.dispatchEvent(\n new CustomEvent('localStorageUpdate', {\n detail: { key, newValue: JSON.stringify(state) }\n })\n )\n }\n } catch (err) {\n console.warn('[onboarding-storage] Failed saving to localStorage:', err)\n }\n}\n\n/**\n * Load onboarding state from localStorage\n */\nexport function loadOnboardingState(key: string): OnboardingState {\n if (typeof window === 'undefined') return DEFAULT_STATE\n\n try {\n const raw = localStorage.getItem(key)\n if (!raw) return DEFAULT_STATE\n\n const parsed = JSON.parse(raw) as OnboardingState\n return parsed\n } catch (err) {\n console.warn('[onboarding-storage] Failed parsing localStorage data:', err)\n return DEFAULT_STATE\n }\n}\n\n/**\n * Mark multiple steps as complete in a single atomic update\n */\nexport function markMultipleComplete(\n key: string,\n stepIds: string[]\n): OnboardingState {\n const state = loadOnboardingState(key)\n const newState: OnboardingState = {\n ...state,\n completedSteps: [...new Set([...state.completedSteps, ...stepIds])],\n skippedSteps: state.skippedSteps.filter(id => !stepIds.includes(id)),\n lastUpdated: new Date().toISOString()\n }\n saveOnboardingState(key, newState)\n return newState\n}\n\n/**\n * Mark a single step as complete\n */\nexport function markStepComplete(key: string, stepId: string): OnboardingState {\n return markMultipleComplete(key, [stepId])\n}\n\n/**\n * Mark a step as skipped\n */\nexport function markStepSkipped(key: string, stepId: string): OnboardingState {\n const state = loadOnboardingState(key)\n const newState: OnboardingState = {\n ...state,\n skippedSteps: [...new Set([...state.skippedSteps, stepId])],\n completedSteps: state.completedSteps.filter(id => id !== stepId),\n lastUpdated: new Date().toISOString()\n }\n saveOnboardingState(key, newState)\n return newState\n}\n\n/**\n * Dismiss the onboarding walkthrough\n */\nexport function dismissOnboarding(key: string): OnboardingState {\n const state = loadOnboardingState(key)\n const newState: OnboardingState = {\n ...state,\n dismissed: true,\n lastUpdated: new Date().toISOString()\n }\n saveOnboardingState(key, newState)\n return newState\n}\n\n/**\n * Check if a step is complete\n */\nexport function isStepComplete(key: string, stepId: string): boolean {\n const state = loadOnboardingState(key)\n return state.completedSteps.includes(stepId)\n}\n\n/**\n * Check if a step is skipped\n */\nexport function isStepSkipped(key: string, stepId: string): boolean {\n const state = loadOnboardingState(key)\n return state.skippedSteps.includes(stepId)\n}\n\n/**\n * Check if onboarding is dismissed\n */\nexport function isOnboardingDismissed(key: string): boolean {\n const state = loadOnboardingState(key)\n return state.dismissed\n}\n","\"use client\"\n\nimport { useState, useEffect, useCallback } from \"react\"\nimport { useDebounce } from \"./use-debounce\"\nimport type { SearchResult } from \"../../components/ui/search-input\"\n\nexport interface UseSearchConfig<T> {\n /** Async function that performs the search */\n searchFn: (query: string) => Promise<T[]>\n /** Maps each raw item to a SearchResult */\n mapResult: (item: T) => SearchResult\n /** Debounce delay in ms. Default 300 */\n debounceMs?: number\n /** Minimum characters before searching. Default 2 */\n minQueryLength?: number\n}\n\nexport interface UseSearchReturn {\n query: string\n setQuery: (q: string) => void\n results: SearchResult[]\n isLoading: boolean\n error: string | null\n clearResults: () => void\n}\n\n/**\n * Generic search state management hook.\n *\n * Debounces the query, calls `searchFn` when the debounced value meets\n * `minQueryLength`, and maps the raw results via `mapResult`.\n */\nexport function useSearch<T>(config: UseSearchConfig<T>): UseSearchReturn {\n const { searchFn, mapResult, debounceMs = 300, minQueryLength = 2 } = config\n\n const [query, setQuery] = useState(\"\")\n const [results, setResults] = useState<SearchResult[]>([])\n const [isLoading, setIsLoading] = useState(false)\n const [error, setError] = useState<string | null>(null)\n\n const debouncedQuery = useDebounce(query, debounceMs)\n\n const clearResults = useCallback(() => {\n setResults([])\n setError(null)\n }, [])\n\n useEffect(() => {\n // Clear when query is empty or below threshold\n if (!debouncedQuery || debouncedQuery.length < minQueryLength) {\n setResults([])\n setIsLoading(false)\n setError(null)\n return\n }\n\n let cancelled = false\n\n const run = async () => {\n setIsLoading(true)\n setError(null)\n\n try {\n const rawResults = await searchFn(debouncedQuery)\n\n if (!cancelled) {\n setResults(rawResults.map(mapResult))\n }\n } catch (err) {\n if (!cancelled) {\n setError(err instanceof Error ? err.message : \"Search failed\")\n setResults([])\n }\n } finally {\n if (!cancelled) {\n setIsLoading(false)\n }\n }\n }\n\n run()\n\n return () => {\n cancelled = true\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [debouncedQuery, minQueryLength])\n\n return { query, setQuery, results, isLoading, error, clearResults }\n}\n","'use client'\n\nimport { useMemo } from 'react'\nimport type { CursorPaginationProps } from '../../components/ui/cursor-pagination'\n\n/**\n * Client-side pagination configuration\n */\nexport interface ClientPaginationConfig {\n type: 'client'\n currentPage: number\n totalPages: number\n itemCount: number\n itemName?: string\n onNext: () => void\n onPrevious: () => void\n showInfo?: boolean\n}\n\n/**\n * Server-side cursor pagination configuration\n */\nexport interface ServerPaginationConfig {\n type: 'server'\n hasNextPage: boolean\n hasLoadedBeyondFirst: boolean\n startCursor?: string | null\n endCursor?: string | null\n itemCount: number\n itemName?: string\n onNext: () => void | Promise<void>\n onReset: () => void | Promise<void>\n showInfo?: boolean\n}\n\nexport type PaginationConfig = ClientPaginationConfig | ServerPaginationConfig\n\n/**\n * Unified pagination hook that works for both client-side and server-side pagination\n *\n * @example Client-side pagination\n * ```typescript\n * const pagination = useTablePagination({\n * type: 'client',\n * currentPage: 1,\n * totalPages: 10,\n * itemCount: 20,\n * itemName: 'scripts',\n * onNext: () => setPage(p => p + 1),\n * onPrevious: () => setPage(p => p - 1),\n * onReset: () => setPage(1)\n * })\n * ```\n *\n * @example Server-side pagination\n * ```typescript\n * const pagination = useTablePagination({\n * type: 'server',\n * hasNextPage: pageInfo.hasNextPage,\n * hasLoadedBeyondFirst: true,\n * startCursor: pageInfo.startCursor,\n * endCursor: pageInfo.endCursor,\n * itemCount: devices.length,\n * itemName: 'devices',\n * onNext: fetchNextPage,\n * onReset: fetchFirstPage\n * })\n * ```\n */\nexport function useTablePagination(\n config: PaginationConfig | null\n): CursorPaginationProps | undefined {\n return useMemo(() => {\n if (!config) return undefined\n\n if (config.type === 'client') {\n // Client-side pagination: show if more than 1 page\n if (config.totalPages <= 1) return undefined\n\n return {\n hasNextPage: config.currentPage < config.totalPages,\n hasPreviousPage: config.currentPage > 1,\n isFirstPage: config.currentPage === 1,\n startCursor: (config.currentPage - 1).toString(),\n endCursor: config.currentPage.toString(),\n currentCount: config.itemCount,\n itemName: config.itemName || 'items',\n onNext: () => config.onNext(),\n onPrevious: () => config.onPrevious(),\n showInfo: config.showInfo ?? true\n }\n } else {\n // Server-side pagination: show if pageInfo exists\n return {\n hasNextPage: config.hasNextPage,\n hasPreviousPage: config.hasLoadedBeyondFirst,\n isFirstPage: !config.hasLoadedBeyondFirst,\n startCursor: config.startCursor,\n endCursor: config.endCursor,\n currentCount: config.itemCount,\n itemName: config.itemName || 'items',\n onNext: config.onNext,\n onPrevious: config.onReset, // Previous button goes back to first page\n showInfo: config.showInfo ?? true\n }\n }\n }, [config])\n}\n","\"use client\"\n\nimport { useState, useEffect, useRef } from \"react\"\n\n/**\n * Hook to throttle a value\n * @param value - Value to throttle\n * @param limit - Throttle limit in milliseconds\n * @returns Throttled value\n */\nexport function useThrottle<T>(value: T, limit = 200): T {\n const [throttledValue, setThrottledValue] = useState<T>(value)\n const lastUpdated = useRef<number>(Date.now())\n\n useEffect(() => {\n const now = Date.now()\n const elapsed = now - lastUpdated.current\n\n // If enough time has elapsed, update the throttled value\n if (elapsed >= limit) {\n setThrottledValue(value)\n lastUpdated.current = now\n } else {\n // Otherwise, set up a timeout to update after the limit\n const timerId = setTimeout(() => {\n setThrottledValue(value)\n lastUpdated.current = Date.now()\n }, limit - elapsed)\n\n return () => {\n clearTimeout(timerId)\n }\n }\n }, [value, limit])\n\n return throttledValue\n}\n","import { useLayoutEffect, useState } from \"react\"\n\n/**\n * Hook to get window dimensions\n * @returns Window width and height\n */\nexport function useWindowSize() {\n const [windowSize, setWindowSize] = useState({\n width: 0,\n height: 0,\n })\n const [isClient, setIsClient] = useState(false)\n\n useLayoutEffect(() => {\n setIsClient(true)\n if (!isClient) return\n\n const handleResize = () => {\n setWindowSize({\n width: window.innerWidth,\n height: window.innerHeight,\n })\n }\n\n // Set initial size\n handleResize()\n\n // Add event listener\n window.addEventListener(\"resize\", handleResize)\n\n // Remove event listener on cleanup\n return () => window.removeEventListener(\"resize\", handleResize)\n }, [isClient])\n\n return windowSize\n}","import { useState, useEffect } from 'react';\nimport type { PlatformConfig, PlatformOption } from '../../types/platform';\nimport { transformPlatformConfigsToOptions } from '../../utils/platform-config';\nimport type { SelectableOption } from '../../components/features';\n\nexport interface UsePlatformConfigResult {\n platforms: PlatformConfig[];\n platformOptions: PlatformOption[];\n selectableOptions: SelectableOption[]; // Rich options with icons and colors\n isLoading: boolean;\n error: Error | null;\n}\n\n// Cache for platform configs to avoid repeated fetches\nlet platformCache: PlatformConfig[] | null = null;\nlet fetchPromise: Promise<PlatformConfig[]> | null = null;\n\n/**\n * Custom hook to fetch platform configurations from API\n * Provides both full platform configs and simplified options for dropdowns\n * Heavily cached to prevent excessive API calls - should only call once per session\n * \n * NOTE: This hook is designed to work without react-query dependency\n */\nexport function usePlatformConfig(): UsePlatformConfigResult {\n const [platforms, setPlatforms] = useState<PlatformConfig[]>(platformCache || []);\n const [isLoading, setIsLoading] = useState(!platformCache);\n const [error, setError] = useState<Error | null>(null);\n\n useEffect(() => {\n // If we already have cached platforms, use them\n if (platformCache) {\n setPlatforms(platformCache);\n setIsLoading(false);\n return;\n }\n\n // If a fetch is already in progress, wait for it\n if (fetchPromise) {\n fetchPromise\n .then(data => {\n setPlatforms(data);\n setIsLoading(false);\n })\n .catch(err => {\n setError(err);\n setIsLoading(false);\n });\n return;\n }\n\n // Start a new fetch\n console.log('🔧 Fetching platform configurations from API (should only happen once)');\n \n fetchPromise = fetch('/api/config/platforms')\n .then(response => {\n if (!response.ok) {\n throw new Error(`Failed to fetch platform config: ${response.statusText}`);\n }\n return response.json();\n })\n .then(data => {\n const platforms = data.platforms || data;\n console.log('✅ Platform configurations loaded:', platforms.length, 'platforms');\n platformCache = platforms;\n fetchPromise = null;\n return platforms;\n });\n\n fetchPromise\n .then(data => {\n setPlatforms(data);\n setIsLoading(false);\n })\n .catch(err => {\n console.error('❌ Failed to fetch platform config:', err);\n setError(err);\n setIsLoading(false);\n fetchPromise = null;\n });\n }, []);\n \n // Create options for dropdowns with \"All Platforms\" option\n const platformOptions: PlatformOption[] = [\n { value: 'all', label: 'All Platforms' },\n ...platforms.map((platform: PlatformConfig) => ({\n value: platform.value,\n label: platform.label\n }))\n ];\n\n // Create rich selectable options with icons and colors\n const selectableOptions = transformPlatformConfigsToOptions(platforms);\n\n return {\n platforms,\n platformOptions,\n selectableOptions,\n isLoading,\n error\n };\n}\n\n/**\n * Get platform configuration by value\n */\nexport function usePlatformByValue(value: string): PlatformConfig | undefined {\n const { platforms } = usePlatformConfig();\n return platforms.find(platform => platform.value === value);\n}\n\n/**\n * Check if a platform value is valid\n */\nexport function useValidatePlatform(value: string): boolean {\n const { platforms } = usePlatformConfig();\n return platforms.some(platform => platform.value === value);\n} ","import React from 'react';\nimport { OpenmspLogo, FlamingoLogo, OpenFrameLogo, MiamiCyberGangLogoFaceOnly } from '../components/icons';\nimport { Globe } from 'lucide-react';\nimport type { SelectableOption } from '../components/features';\nimport type { PlatformConfig } from '../types/platform';\n\n// Platform icons mapping with consistent colors matching app theme\nexport const platformIcons = {\n openframe: <OpenFrameLogo className=\"h-5 w-5\" lowerPathColor=\"#FFC008\" upperPathColor=\"#ffffff\" />,\n openmsp: <OpenmspLogo className=\"h-5 w-5\" />,\n flamingo: <FlamingoLogo className=\"h-5 w-5\" fill=\"#EC4899\" />,\n 'flamingo-teaser': <FlamingoLogo className=\"h-5 w-5\" fill=\"#EC4899\" />,\n 'marketing-hub': <FlamingoLogo className=\"h-5 w-5\" fill=\"#F357BB\" />,\n 'product-hub': <FlamingoLogo className=\"h-5 w-5\" fill=\"#5EA62E\" />,\n 'revenue-hub': <FlamingoLogo className=\"h-5 w-5\" fill=\"#FFC008\" />,\n 'people-hub': <FlamingoLogo className=\"h-5 w-5\" fill=\"#5EFAF0\" />,\n 'company-hub': <FlamingoLogo className=\"h-5 w-5\" fill=\"#f36666\" />,\n tmcg: <MiamiCyberGangLogoFaceOnly className=\"h-5 w-5\" />,\n universal: <Globe className=\"h-5 w-5 text-[#10B981]\" />\n};\n\n// Platform colors mapping\nexport const platformColors = {\n openmsp: 'bg-[#3B82F6]',\n openframe: 'bg-[#8B5CF6]',\n flamingo: 'bg-[#EC4899]',\n 'flamingo-teaser': 'bg-[#F59E0B]',\n 'marketing-hub': 'bg-[#F357BB]',\n 'product-hub': 'bg-[#5EA62E]',\n 'revenue-hub': 'bg-[#FFC008]',\n 'people-hub': 'bg-[#5EFAF0]',\n 'company-hub': 'bg-[#f36666]',\n tmcg: 'bg-[#FF6B6B]',\n universal: 'bg-[#10B981]'\n};\n\n// Platform display names for consistent naming across the app\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 universal: 'Universal'\n};\n\n// Platform descriptions for consistent messaging across the app\nexport const platformDescriptions = {\n openmsp: '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: '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 universal: 'Cross-platform universal content.'\n};\n\n// Platform slogans for branding consistency\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 universal: 'Universal Platform'\n};\n\n// Platform hex colors for default configuration\nexport const platformHexColors = {\n openmsp: '#FFC008',\n openframe: '#FFC008',\n flamingo: '#FF6B9D',\n universal: '#FFC008',\n 'flamingo-teaser': '#F59E0B',\n 'marketing-hub': '#F357BB',\n 'product-hub': '#5EA62E',\n 'revenue-hub': '#FFC008',\n 'people-hub': '#5EFAF0',\n 'company-hub': '#f36666',\n tmcg: '#FF6B6B'\n};\n\n// Platform icon names for default configuration\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};\n\n/**\n * Get default color for platform\n */\nexport function getDefaultColorForPlatform(platformName: string): string {\n return platformHexColors[platformName as keyof typeof platformHexColors] || platformHexColors.universal;\n}\n\n/**\n * Get default icon name for platform\n */\nexport function getDefaultIconForPlatform(platformName: string): string {\n return platformIconNames[platformName as keyof typeof platformIconNames] || platformIconNames.universal;\n}\n\nexport function transformPlatformConfigsToOptions(platformConfigs: PlatformConfig[]): SelectableOption[] {\n return platformConfigs.map((platform: PlatformConfig) => ({\n id: platform.id, // Database UUID for matching\n name: platform.name, // Platform name enum\n displayName: platform.display_name, // Human-readable name\n description: platform.description,\n icon: platformIcons[platform.name as keyof typeof platformIcons] || platformIcons.universal,\n color: platformColors[platform.name as keyof typeof platformColors] || platformColors.universal\n }));\n}\n\n/**\n * Get platform icon by name\n */\nexport function getPlatformIcon(platformName: string) {\n return platformIcons[platformName as keyof typeof platformIcons] || platformIcons.universal;\n}\n\n/**\n * Get platform color by name\n */\nexport function getPlatformColor(platformName: string) {\n return platformColors[platformName as keyof typeof platformColors] || platformColors.universal;\n}\n\n/**\n * Get platform display name by name\n */\nexport function getPlatformDisplayName(platformName: string): string {\n return platformDisplayNames[platformName as keyof typeof platformDisplayNames] || platformName;\n}\n\n/**\n * Get platform description by name\n */\nexport function getPlatformDescription(platformName: string): string {\n return platformDescriptions[platformName as keyof typeof platformDescriptions] || platformName;\n}\n\n/**\n * Get platform slogan by name\n */\nexport function getPlatformSlogan(platformName: string): string {\n return platformSlogans[platformName as keyof typeof platformSlogans] || platformName;\n}\n\n/**\n * Get small platform icon for filter buttons with white colors (4x4 size)\n */\nexport function getSmallPlatformIcon(platformName: string): React.ReactNode {\n const className = \"h-4 w-4 flex-shrink-0\";\n\n switch (platformName) {\n case 'openframe':\n return <OpenFrameLogo className={className} lowerPathColor=\"#FFC008\" upperPathColor=\"#ffffff\" />;\n case 'openmsp':\n return <OpenmspLogo className={className} frontBubbleColor=\"#f1f1f1\" innerFrontBubbleColor=\"#000000\" backBubbleColor=\"#FFC008\" />;\n case 'flamingo':\n case 'flamingo-teaser':\n return <FlamingoLogo className={`${className}`} fill=\"#EC4899\" />;\n case 'marketing-hub':\n return <FlamingoLogo className={className} fill=\"var(--ods-flamingo-pink-base)\" />;\n case 'product-hub':\n return <FlamingoLogo className={className} fill=\"var(--ods-attention-green-success)\" />;\n case 'revenue-hub':\n return <FlamingoLogo className={className} fill=\"var(--ods-attention-yellow-warning)\" />;\n case 'people-hub':\n return <FlamingoLogo className={className} fill=\"var(--ods-flamingo-cyan-base)\" />;\n case 'company-hub':\n return <FlamingoLogo className={className} fill=\"var(--ods-attention-red-error)\" />;\n case 'tmcg':\n return <MiamiCyberGangLogoFaceOnly className={className} />;\n case 'universal':\n default:\n return <Globe className={className} />;\n }\n}\n\n/**\n * Get platform icon for admin/selector components (standard 6x6 size)\n */\nexport function getPlatformIconComponent(platformName: string, className: string = \"h-6 w-6\"): React.ReactNode {\n switch (platformName) {\n case 'openframe':\n return <OpenFrameLogo className={className} />;\n case 'openmsp':\n return <OpenmspLogo className={className} color=\"#f1f1f1\" />;\n case 'flamingo':\n case 'flamingo-teaser':\n return <FlamingoLogo className={`${className} text-white`} />;\n case 'marketing-hub':\n return <FlamingoLogo className={className} fill=\"var(--ods-flamingo-pink-base)\" />;\n case 'product-hub':\n return <FlamingoLogo className={className} fill=\"var(--ods-attention-green-success)\" />;\n case 'revenue-hub':\n return <FlamingoLogo className={className} fill=\"var(--ods-attention-yellow-warning)\" />;\n case 'people-hub':\n return <FlamingoLogo className={className} fill=\"var(--ods-flamingo-cyan-base)\" />;\n case 'company-hub':\n return <FlamingoLogo className={className} fill=\"var(--ods-attention-red-error)\" />;\n case 'tmcg':\n return <MiamiCyberGangLogoFaceOnly size={24} className={className} />;\n case 'universal':\n default:\n return <Globe className={className} />;\n }\n}","import { toast as sonnerToast } from 'sonner'\nimport { showToast, type ToastVariant } from '../components/ui/toaster'\n\nexport interface ToastOptions {\n title?: string\n description?: string\n variant?: 'default' | 'success' | 'destructive' | 'info' | 'warning' | 'error'\n duration?: number\n dismissible?: boolean\n}\n\nconst normalizeVariant = (variant: ToastOptions['variant']): ToastVariant => {\n if (variant === 'destructive') return 'error'\n if (!variant) return 'default'\n return variant\n}\n\nexport const toast = (options: ToastOptions | string) => {\n if (typeof options === 'string') {\n return showToast({ title: options })\n }\n\n const { title, description, variant, duration, dismissible } = options\n return showToast({\n title,\n description,\n variant: normalizeVariant(variant),\n duration,\n dismissible,\n })\n}\n\nexport const useToast = () => ({\n toast,\n dismiss: sonnerToast.dismiss,\n promise: sonnerToast.promise,\n})\n","'use client'\n\nimport * as React from 'react'\nimport { Toaster as SonnerToaster, toast as sonnerToast } from 'sonner'\nimport type { ExternalToast } from 'sonner'\nimport { Chevron02DownIcon } from '../icons-v2-generated/arrows/chevron-02-down-icon'\nimport { XmarkIcon } from '../icons-v2-generated/signs-and-symbols/xmark-icon'\nimport { ToolIcon } from '../tool-icon'\nimport type { ToolType } from '../../types/tool.types'\nimport { cn } from '../../utils/cn'\n\nexport type ToastVariant = 'default' | 'success' | 'warning' | 'error' | 'info'\n\nexport const dotColorByVariant: Record<ToastVariant, string> = {\n default: 'bg-ods-text-secondary',\n success: 'bg-ods-success',\n warning: 'bg-ods-warning',\n error: 'bg-ods-error',\n info: 'bg-ods-text-secondary',\n}\n\nexport const progressColorByVariant: Record<ToastVariant, string> = {\n default: 'bg-ods-text-secondary',\n success: 'bg-ods-success',\n warning: 'bg-ods-warning',\n error: 'bg-ods-error',\n info: 'bg-ods-text-secondary',\n}\n\ninterface ToastHeaderProps {\n id: string | number\n variant: ToastVariant\n title?: React.ReactNode\n description?: React.ReactNode\n duration?: number\n dismissible?: boolean\n className?: string\n showProgress?: boolean\n}\n\nfunction ToastHeader({\n id,\n variant,\n title,\n description,\n duration = 4000,\n dismissible = true,\n className,\n showProgress = true,\n}: ToastHeaderProps) {\n return (\n <div\n className={cn(\n 'relative flex w-full items-start gap-2 overflow-hidden bg-ods-card p-3',\n className,\n )}\n >\n <div className=\"flex size-6 shrink-0 items-center justify-center\">\n <span className={cn('size-[9px] rounded-full', dotColorByVariant[variant])} />\n </div>\n\n <div className=\"flex min-w-0 flex-1 flex-col justify-center font-['DM_Sans'] font-medium\">\n {title ? (\n <p className=\"truncate pr-5 text-[18px] leading-6 text-ods-text-primary\" title={typeof title === 'string' ? title : undefined}>{title}</p>\n ) : null}\n {description ? (\n <p className=\"text-[14px] leading-5 text-ods-text-secondary line-clamp-3\" title={typeof description === 'string' ? description : undefined}>{description}</p>\n ) : null}\n </div>\n\n {dismissible ? (\n <button\n type=\"button\"\n aria-label=\"Close\"\n onClick={() => sonnerToast.dismiss(id)}\n className=\"absolute right-[7px] top-[7px] flex size-4 items-center justify-center text-ods-text-secondary transition-colors hover:text-ods-text-primary\"\n >\n <XmarkIcon size={16} />\n </button>\n ) : null}\n\n {showProgress && duration !== Infinity && duration > 0 ? (\n <div\n className={cn(\n 'absolute inset-x-0 bottom-0 h-1 origin-left',\n progressColorByVariant[variant],\n )}\n style={{\n animation: `toast-progress ${duration}ms linear forwards`,\n }}\n />\n ) : null}\n </div>\n )\n}\n\nexport interface ToastCardProps {\n id: string | number\n variant?: ToastVariant\n title?: React.ReactNode\n description?: React.ReactNode\n duration?: number\n dismissible?: boolean\n className?: string\n}\n\nexport function ToastCard({\n id,\n variant = 'default',\n title,\n description,\n duration = 4000,\n dismissible = true,\n className,\n}: ToastCardProps) {\n return (\n <div\n role=\"status\"\n className={cn(\n 'w-[368px] max-w-[calc(100vw-32px)] overflow-hidden rounded-md border border-ods-border bg-ods-card shadow-lg',\n className,\n )}\n >\n <ToastHeader\n id={id}\n variant={variant}\n title={title}\n description={description}\n duration={duration}\n dismissible={dismissible}\n />\n </div>\n )\n}\n\nexport interface CommandApprovalToastProps {\n id: string | number\n variant?: ToastVariant\n title?: React.ReactNode\n description?: React.ReactNode\n command: string\n toolType?: ToolType\n approvalDescription?: React.ReactNode\n approveLabel?: string\n rejectLabel?: string\n onApprove?: () => void\n onReject?: () => void\n duration?: number\n dismissible?: boolean\n defaultExpanded?: boolean\n className?: string\n}\n\nexport function CommandApprovalToast({\n id,\n variant = 'warning',\n title = 'Tech Required',\n description = 'Approval is required to execute the command.',\n command,\n toolType,\n approvalDescription,\n approveLabel = 'Approve',\n rejectLabel = 'Reject',\n onApprove,\n onReject,\n duration = Infinity,\n dismissible = true,\n defaultExpanded = false,\n className,\n}: CommandApprovalToastProps) {\n const [expanded, setExpanded] = React.useState(defaultExpanded)\n\n const handleApprove = () => {\n onApprove?.()\n sonnerToast.dismiss(id)\n }\n\n const handleReject = () => {\n onReject?.()\n sonnerToast.dismiss(id)\n }\n\n return (\n <div\n role=\"status\"\n className={cn(\n 'flex w-[368px] max-w-[calc(100vw-32px)] flex-col overflow-hidden rounded-md border border-ods-border bg-ods-bg shadow-lg',\n className,\n )}\n >\n <ToastHeader\n id={id}\n variant={variant}\n title={title}\n description={description}\n duration={duration}\n dismissible={dismissible}\n showProgress={!expanded}\n className=\"border-b border-ods-border\"\n />\n\n <div\n className=\"grid transition-[grid-template-rows] duration-300 ease-out\"\n style={{ gridTemplateRows: expanded ? '1fr' : '0fr' }}\n aria-hidden={!expanded}\n >\n <div className=\"overflow-hidden\">\n <div className=\"flex h-11 w-full items-center gap-2 border-b border-ods-border bg-ods-card px-3 py-2\">\n <p className=\"min-w-0 flex-1 truncate font-['DM_Sans'] text-[14px] font-medium leading-5 text-ods-text-primary\" title={command}>\n {command}\n </p>\n {toolType ? <ToolIcon toolType={toolType} size={16} /> : null}\n </div>\n\n <div className=\"flex flex-col gap-2 bg-ods-bg p-3\">\n {approvalDescription ? (\n <p className=\"font-['DM_Sans'] text-[14px] font-medium leading-5 text-ods-text-secondary\">\n {approvalDescription}\n </p>\n ) : null}\n <div className=\"flex items-center gap-4\">\n <button\n type=\"button\"\n onClick={handleApprove}\n tabIndex={expanded ? 0 : -1}\n className=\"flex flex-1 items-center justify-center rounded-md bg-ods-accent px-2 py-2 font-['Azeret_Mono'] text-[14px] font-medium uppercase tracking-[-0.28px] text-ods-text-on-accent transition-colors hover:bg-ods-accent-hover active:bg-ods-accent-active\"\n >\n {approveLabel}\n </button>\n <button\n type=\"button\"\n onClick={handleReject}\n tabIndex={expanded ? 0 : -1}\n className=\"flex flex-1 items-center justify-center rounded-md border border-ods-border bg-ods-card px-2 py-2 font-['Azeret_Mono'] text-[14px] font-medium uppercase tracking-[-0.28px] text-ods-text-primary transition-colors hover:bg-ods-bg-hover\"\n >\n {rejectLabel}\n </button>\n </div>\n </div>\n </div>\n </div>\n\n {expanded ? null : (\n <button\n type=\"button\"\n onClick={() => setExpanded(true)}\n className=\"flex w-full items-center gap-2 bg-ods-card px-3 py-2 text-left font-['DM_Sans'] text-[14px] font-medium leading-5 text-ods-text-primary transition-colors hover:bg-ods-bg-hover\"\n aria-expanded={false}\n >\n <span className=\"flex-1\">Show Command</span>\n <Chevron02DownIcon size={16} />\n </button>\n )}\n </div>\n )\n}\n\nexport type ToasterProps = React.ComponentProps<typeof SonnerToaster>\n\nexport function Toaster({\n position = 'bottom-right',\n offset = 24,\n gap = 8,\n toastOptions,\n ...rest\n}: ToasterProps = {}) {\n const { classNames: userClassNames, ...restToastOptions } = toastOptions ?? {}\n\n return (\n <>\n <style>{`\n @keyframes toast-progress {\n from { transform: scaleX(1); }\n to { transform: scaleX(0); }\n }\n `}</style>\n <SonnerToaster\n position={position}\n offset={offset}\n gap={gap}\n toastOptions={{\n unstyled: true,\n ...restToastOptions,\n classNames: {\n toast: 'w-full',\n ...userClassNames,\n },\n }}\n {...rest}\n />\n </>\n )\n}\n\nexport interface ShowToastOptions extends Omit<ExternalToast, 'description'> {\n title?: React.ReactNode\n description?: React.ReactNode\n variant?: ToastVariant\n}\n\nexport function showToast(options: ShowToastOptions | string) {\n const opts: ShowToastOptions =\n typeof options === 'string' ? { title: options } : options\n\n const {\n title,\n description,\n variant = 'default',\n duration = 4000,\n dismissible = true,\n ...rest\n } = opts\n\n return sonnerToast.custom(\n (id) => (\n <ToastCard\n id={id}\n variant={variant}\n title={title}\n description={description}\n duration={duration}\n dismissible={dismissible}\n />\n ),\n { duration, dismissible, ...rest },\n )\n}\n\nexport interface ShowCommandApprovalToastOptions\n extends Omit<ExternalToast, 'description'>,\n Omit<CommandApprovalToastProps, 'id'> {}\n\nexport function showCommandApprovalToast(options: ShowCommandApprovalToastOptions) {\n const {\n variant,\n title,\n description,\n command,\n toolType,\n approvalDescription,\n approveLabel,\n rejectLabel,\n onApprove,\n onReject,\n defaultExpanded,\n duration = Infinity,\n dismissible = true,\n ...rest\n } = options\n\n return sonnerToast.custom(\n (id) => (\n <CommandApprovalToast\n id={id}\n variant={variant}\n title={title}\n description={description}\n command={command}\n toolType={toolType}\n approvalDescription={approvalDescription}\n approveLabel={approveLabel}\n rejectLabel={rejectLabel}\n onApprove={onApprove}\n onReject={onReject}\n defaultExpanded={defaultExpanded}\n duration={duration}\n dismissible={dismissible}\n />\n ),\n { duration, dismissible, ...rest },\n )\n}\n","/**\n * Centralized Tool Types\n *\n * Single source of truth for all tool-related types across the entire platform.\n * Used by ToolBadge, ToolIcon, and any component that needs tool type information.\n */\n\nexport const ToolTypeValues = {\n TACTICAL_RMM: 'TACTICAL_RMM',\n FLEET_MDM: 'FLEET_MDM',\n MESHCENTRAL: 'MESHCENTRAL',\n AUTHENTIK: 'AUTHENTIK',\n OPENFRAME: 'OPENFRAME',\n OPENFRAME_CHAT: 'OPENFRAME_CHAT',\n OPENFRAME_CLIENT: 'OPENFRAME_CLIENT',\n OSQUERY: 'OSQUERY',\n SYSTEM: 'SYSTEM'\n} as const\n\nexport type ToolType = (typeof ToolTypeValues)[keyof typeof ToolTypeValues]\n\n/**\n * Maps tool types to display labels\n */\nexport const toolLabels: Record<ToolType, string> = {\n TACTICAL_RMM: 'Tactical',\n FLEET_MDM: 'Fleet',\n MESHCENTRAL: 'MeshCentral',\n AUTHENTIK: 'Authentik',\n OPENFRAME: 'OpenFrame',\n OPENFRAME_CHAT: 'OpenFrame Chat',\n OPENFRAME_CLIENT: 'OpenFrame Client',\n OSQUERY: 'Osquery',\n SYSTEM: 'System'\n}\n","import * as React from \"react\";\nimport { ToolType, ToolTypeValues } from \"../types/tool.types\";\nimport { OpenFrameLogo } from \"./icons\";\nimport {\n\tOsqueryLogoGreyIcon,\n\tTacticalRmmLogoIcon,\n\tMeshcentralLogoGreyIcon,\n\tFleetMdmLogoGreyIcon,\n\tAuthentikLogoGreyIcon,\n} from \"./icons-v2-generated\";\n\nconst renderOpenFrameLogo = (_size: number, className?: string) => (\n\t// eslint-disable-next-line deprecation/deprecation\n\t<OpenFrameLogo\n\t\tclassName={className ?? \"h-4 w-auto\"}\n\t\tlowerPathColor=\"var(--color-accent-primary)\"\n\t\tupperPathColor=\"var(--color-text-primary)\"\n\t/>\n);\n\nconst toolIconMap: Record<ToolType, (size: number, className?: string) => React.ReactNode> = {\n\t[ToolTypeValues.FLEET_MDM]: (size, className) => <FleetMdmLogoGreyIcon size={size} className={className} />,\n\t[ToolTypeValues.MESHCENTRAL]: (size, className) => <MeshcentralLogoGreyIcon size={size} className={className} />,\n\t[ToolTypeValues.TACTICAL_RMM]: (size, className) => <TacticalRmmLogoIcon size={size} className={className} />,\n\t[ToolTypeValues.OPENFRAME]: renderOpenFrameLogo,\n\t[ToolTypeValues.OPENFRAME_CHAT]: renderOpenFrameLogo,\n\t[ToolTypeValues.OPENFRAME_CLIENT]: renderOpenFrameLogo,\n\t[ToolTypeValues.AUTHENTIK]: (size, className) => <AuthentikLogoGreyIcon size={size} className={className} />,\n\t[ToolTypeValues.OSQUERY]: (size, className) => <OsqueryLogoGreyIcon size={size} className={className} />,\n\t[ToolTypeValues.SYSTEM]: () => null,\n};\n\nexport interface ToolIconProps {\n\ttoolType: ToolType;\n\tsize?: number;\n\tclassName?: string;\n}\n\nexport const ToolIcon: React.FC<ToolIconProps> = ({ toolType, size = 16, className }) =>\n\t<>{toolIconMap[toolType]?.(size, className) ?? null}</>;\n\nToolIcon.displayName = \"ToolIcon\";\n","import { useState, useCallback, useEffect } from 'react';\nimport { useToast } from \"./use-toast\";\nimport { useRouter } from '../embed-shims/next-navigation';\nimport { useRequiredEndpointsRuntime } from '../contexts/endpoints-runtime-context';\nimport { contentFetch } from '../utils/embed-content-fetch';\n\ninterface ContactSubmissionOptions {\n userId?: string;\n successRedirectUrl?: string;\n successToastMessage?: string;\n onSuccess?: () => void;\n}\n\n/**\n * Mirrors the API contract at POST /api/contact (see `ContactBaseSchema` in\n * the consuming app). Keep optional fields aligned with the Zod schema —\n * anything missing here OR there will be silently dropped on submission.\n *\n * Tracking fields (rdt_cid, utm_*, referrer_url) are forwarded as-is and\n * persisted by the API; they don't need to be declared on the form, but\n * the index signature below preserves any extra keys callers spread in.\n */\ninterface ContactFormData {\n name: string;\n email: string;\n helpCategory: string;\n message: string;\n linkedin_url?: string;\n companySize?: string;\n referralSource?: string;\n rdt_cid?: string;\n // Permissive index signature — tracking metadata, A/B variants, and\n // future per-form fields flow through unchanged. The API decides what\n // to keep via its Zod schema.\n [key: string]: unknown;\n}\n\n/**\n * useContactSubmission\n * --------------------\n * Provides a helper for submitting contact form data to `/api/contact`.\n * Handles loading state, success detection, toast notifications, and redirect.\n * Follows the same pattern as useWaitlistRegistration for consistency.\n */\nexport function useContactSubmission(options: ContactSubmissionOptions = {}) {\n const { userId, successRedirectUrl, successToastMessage, onSuccess } = options;\n const { toast } = useToast();\n const router = useRouter();\n // Endpoint URL injected via context — hub provides hub default, embedded\n // app provides its proxied path. Throws if no provider is mounted.\n const { contactUrl } = useRequiredEndpointsRuntime();\n const [isSubmitting, setIsSubmitting] = useState(false);\n const [isSuccess, setIsSuccess] = useState(false);\n\n const submit = useCallback(async (formData: ContactFormData) => {\n if (isSubmitting) return;\n\n setIsSubmitting(true);\n\n try {\n const response = await contentFetch(contactUrl, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ \n ...formData,\n user_id: userId\n }),\n });\n\n const data = await response.json();\n\n if (!response.ok) {\n throw new Error(data.error || 'Failed to submit contact form');\n }\n\n // Success\n setIsSuccess(true);\n const message = successToastMessage \n ? `Thank you! Your message has been sent. ${successToastMessage}`\n : 'Thank you! Your message has been sent successfully.';\n \n toast({\n title: \"Message sent!\",\n description: message,\n variant: 'success',\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Something went wrong. Please try again.';\n toast({ \n title: 'Failed to send message', \n description: message, \n variant: 'destructive' \n });\n throw error; // allow caller to handle if needed\n } finally {\n setIsSubmitting(false);\n }\n }, [isSubmitting, toast, userId, successToastMessage, contactUrl]);\n\n // Handle redirect after success\n useEffect(() => {\n if (isSuccess && successRedirectUrl) {\n console.log('🚀 Contact submission successful, redirecting to:', successRedirectUrl);\n const timer = setTimeout(() => {\n console.log('🎯 Performing redirect now to:', successRedirectUrl);\n if (successRedirectUrl.startsWith('http')) {\n window.location.href = successRedirectUrl;\n } else {\n router.push(successRedirectUrl);\n }\n }, 1500);\n \n return () => clearTimeout(timer);\n }\n }, [isSuccess, successRedirectUrl, router]);\n\n // Call onSuccess callback if provided\n useEffect(() => {\n if (isSuccess && onSuccess) {\n onSuccess();\n }\n }, [isSuccess, onSuccess]);\n\n return { submit, isSubmitting, isSuccess, setIsSuccess } as const;\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>(\n options: LocalStorageAdapterOptions<T>,\n): 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","// 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 return process.env.NEXT_PUBLIC_APP_TYPE || 'openmsp';\n}","'use client'\n\n/**\n * Client-side persistence for embed-surface proxy credentials\n * (`CHAT_PROXY_SECRET` + 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 <secret>` + `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>.\n *\n * **Naming history:** the wire-side header names are still `X-Chat-*`\n * and the env var is `CHAT_PROXY_SECRET`. Those are server contracts;\n * renaming them would require a coordinated deploy + customer-side\n * env-var migration. The CLIENT-side helpers were renamed `Embed*` so\n * non-chat surfaces (e.g. ticket center) don't have to import a\n * chat-prefixed symbol just to 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 and the\n * impersonation header it sets is server-validated against\n * `CHAT_PROXY_SECRET` anyway. Explicit \"Clear\" button on the creds\n * bar is the supported 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 { createLocalStorageAdapter } from './local-storage-adapter'\nimport { getAppType } from './app-config'\n\nexport interface EmbedProxyAuth {\n secret: string\n email: string\n /** Optional identity passthrough — empty/omitted = not sent. Server\n * parses these as `X-Chat-{First,Last}-Name` / `X-Chat-Avatar-Url` and\n * threads them through `resolveChatProxyIdentity`'s returned user. */\n firstName?: string\n lastName?: string\n avatarUrl?: string\n}\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' || v.secret.trim().length === 0 ||\n typeof v.email !== 'string' || v.email.trim().length === 0\n ) return false\n // Optional fields: when present must be strings. Empty string is treated\n // as absent later (in `getEmbedProxyAuth`).\n if (v.firstName != null && typeof v.firstName !== 'string') return false\n if (v.lastName != null && typeof v.lastName !== 'string') return false\n if (v.avatarUrl != null && typeof v.avatarUrl !== 'string') return false\n return true\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/**\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 firstName: normalizeOptional(persisted.firstName),\n lastName: normalizeOptional(persisted.lastName),\n avatarUrl: normalizeOptional(persisted.avatarUrl),\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 firstName: normalizeOptional(value.firstName),\n lastName: normalizeOptional(value.lastName),\n avatarUrl: normalizeOptional(value.avatarUrl),\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 identity passthrough — only attached when present so the\n // server's \"required vs optional\" header shape stays exact.\n if (auth?.firstName) headers['X-Chat-First-Name'] = auth.firstName\n if (auth?.lastName) headers['X-Chat-Last-Name'] = auth.lastName\n if (auth?.avatarUrl) headers['X-Chat-Avatar-Url'] = auth.avatarUrl\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\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 * `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; cross-origin URLs throw before the bearer\n * leaves the page. This is a defense-in-depth guard for future call sites\n * — there is no legitimate cross-origin use of this 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 as Record<string, string>)\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\n | Promise<boolean>\n | null\n | undefined) ?? 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 if (!adapter?.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(() => adapter.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 // 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\n/**\n * `contentFetch` — the fetch the lib's SELF-FETCHING content surfaces use:\n * roadmap (list + vote + per-task refresh), delivery, product releases,\n * onboarding guides (catalog + detail), legal docs, and the release-detail\n * injected roadmap/delivery sections.\n *\n * It deliberately reuses the SINGLE embed-auth knob — the registered\n * `EmbedAuthAdapter` — instead of introducing a second registration:\n * - adapter registered (host opted into embedded auth, e.g. openframe-frontend\n * for its embedded chat) → route through `embedAuthedFetch`, so content GETs/\n * POSTs carry the SAME bearer/cookie + 401-refresh as the chat.\n * - no adapter (the public hub, or the embedding example whose proxy injects\n * auth server-side) → a plain, byte-for-byte unchanged `fetch`.\n *\n * So a host wires auth ONCE (the chat adapter) and content inherits it; there is\n * no content-specific fetcher to configure.\n */\n\nimport { embedAuthedFetch, hasEmbedAuthAdapter } from './embed-authed-fetch'\n\nexport const contentFetch: typeof fetch = (input, init) => {\n if (!hasEmbedAuthAdapter()) return fetch(input, init)\n // embedAuthedFetch takes a string url; coerce the broader `fetch` input shape\n // (content surfaces always pass strings, but handle URL/Request defensively).\n const url =\n typeof input === 'string' ? input : input instanceof URL ? input.href : (input as Request).url\n return embedAuthedFetch(url, init)\n}\n","'use client'\n\nimport { useEffect, useState, useRef, useCallback } from 'react'\n\ninterface UseQuickActionHintOptions {\n /** Number of quick actions to cycle through */\n actionCount: number\n /** Duration each action is highlighted in milliseconds */\n cycleDuration?: number\n /** Whether the hint should be enabled */\n enabled?: boolean\n}\n\ninterface UseQuickActionHintReturn {\n /** The index of the currently active hint (-1 if none) */\n activeHintIndex: number\n /** Manually stop the hint animation */\n stopHint: () => void\n /** Reference to attach to the container element (not used in simplified version) */\n containerRef: React.RefObject<HTMLDivElement | null>\n}\n\n/**\n * SIMPLIFIED hook for managing quick action hint animations\n *\n * Cycles infinitely through quick actions until user interacts.\n * Simple mount-based trigger - no complex visibility detection.\n */\nexport function useQuickActionHint({\n actionCount,\n cycleDuration = 5000,\n enabled = true\n}: UseQuickActionHintOptions): UseQuickActionHintReturn {\n const [activeHintIndex, setActiveHintIndex] = useState(-1)\n const containerRef = useRef<HTMLDivElement | null>(null)\n const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)\n const cycleCountRef = useRef(0)\n const currentIndexRef = useRef(0)\n const sequenceRef = useRef<number[]>([])\n\n /**\n * Fisher-Yates shuffle for random, non-repeating sequence\n */\n const shuffleIndices = useCallback((length: number): number[] => {\n const indices = Array.from({ length }, (_, i) => i)\n for (let i = indices.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1));\n [indices[i], indices[j]] = [indices[j], indices[i]]\n }\n return indices\n }, [])\n\n /**\n * Stop the hint animation\n */\n const stopHint = useCallback(() => {\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current)\n timeoutRef.current = null\n }\n setActiveHintIndex(-1)\n cycleCountRef.current = 0\n currentIndexRef.current = 0\n sequenceRef.current = []\n }, [])\n\n /**\n * Advance to next hint\n */\n const advanceHint = useCallback(() => {\n // Generate new sequence if starting a new cycle\n if (currentIndexRef.current === 0) {\n sequenceRef.current = shuffleIndices(actionCount)\n }\n\n // Get next index\n const nextIndex = sequenceRef.current[currentIndexRef.current]\n setActiveHintIndex(nextIndex)\n\n // Move to next position\n currentIndexRef.current++\n\n // Check if cycle complete - reset to continue infinitely\n if (currentIndexRef.current >= actionCount) {\n currentIndexRef.current = 0\n cycleCountRef.current++\n }\n\n // Schedule next hint - runs infinitely until stopped by user interaction\n timeoutRef.current = setTimeout(advanceHint, cycleDuration)\n }, [actionCount, cycleDuration, shuffleIndices])\n\n /**\n * Start hint animation when component mounts with actions\n */\n useEffect(() => {\n // Don't start if disabled, no actions, or reduced motion\n if (!enabled || actionCount === 0) {\n return\n }\n\n if (typeof window !== 'undefined') {\n const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches\n if (prefersReducedMotion) return\n }\n\n // Start after a short delay to allow component to settle\n const startTimeout = setTimeout(() => {\n cycleCountRef.current = 0\n currentIndexRef.current = 0\n advanceHint()\n }, 500)\n\n // Cleanup on unmount or when dependencies change\n return () => {\n clearTimeout(startTimeout)\n stopHint()\n }\n }, [enabled, actionCount, advanceHint, stopHint])\n\n return {\n activeHintIndex,\n stopHint,\n containerRef\n }\n}\n","'use client';\n\nimport { useCallback, useState } from 'react';\nimport { useToast } from './use-toast';\n\ninterface UseCopyToClipboardOptions {\n successTitle?: string;\n successDescription?: string;\n errorTitle?: string;\n errorDescription?: string;\n resetDelay?: number;\n}\n\nexport function useCopyToClipboard({\n successTitle = 'Copied',\n successDescription = 'Copied to clipboard',\n errorTitle = 'Copy failed',\n errorDescription = 'Could not copy to clipboard',\n resetDelay = 2000,\n}: UseCopyToClipboardOptions = {}) {\n const { toast } = useToast();\n const [copied, setCopied] = useState(false);\n\n const copy = useCallback(\n async (text: string) => {\n try {\n await navigator.clipboard.writeText(text);\n setCopied(true);\n toast({ title: successTitle, description: successDescription, variant: 'success' });\n setTimeout(() => setCopied(false), resetDelay);\n } catch {\n toast({ title: errorTitle, description: errorDescription, variant: 'destructive' });\n }\n },\n [successTitle, successDescription, errorTitle, errorDescription, resetDelay, toast],\n );\n\n return { copy, copied };\n}\n","import { useEffect, useMemo, useState, useRef } from 'react'\n\n/**\n * Configuration for batch image fetching\n */\nexport interface BatchImageFetchConfig {\n /** Base URL for tenant-specific API calls (e.g., 'https://tenant.openframe.dev' or '') */\n tenantHostUrl?: string\n /** Enable dev mode with Bearer token from localStorage */\n enableDevMode?: boolean\n /** localStorage key for access token (default: 'of_access_token') */\n accessTokenKey?: string\n}\n\n/**\n * Global configuration for batch image fetching\n * Can be set once at app initialization\n */\nlet globalBatchImageConfig: BatchImageFetchConfig = {}\n\n/**\n * Configure global settings for batch image fetching\n * Call this once in your app initialization (e.g., _app.tsx or layout.tsx)\n *\n * @example\n * ```typescript\n * // In app initialization\n * configureBatchImageFetch({\n * tenantHostUrl: process.env.NEXT_PUBLIC_TENANT_HOST_URL || '',\n * enableDevMode: process.env.NEXT_PUBLIC_ENABLE_DEV_TICKET_OBSERVER === 'true'\n * })\n * ```\n */\nexport function configureBatchImageFetch(config: BatchImageFetchConfig): void {\n globalBatchImageConfig = { ...globalBatchImageConfig, ...config }\n}\n\n/**\n * Get current batch image fetch configuration\n */\nfunction getBatchImageConfig(): Required<BatchImageFetchConfig> {\n return {\n tenantHostUrl: globalBatchImageConfig.tenantHostUrl || '',\n enableDevMode: globalBatchImageConfig.enableDevMode ?? false,\n accessTokenKey: globalBatchImageConfig.accessTokenKey || 'of_access_token'\n }\n}\n\n/**\n * Fetch multiple images with authentication in batch\n * Returns a map of original imageUrl to fetched blob URL\n *\n * @param imageUrls - Array of image URLs to fetch\n * @param config - Optional configuration override for this batch\n * @returns Promise resolving to map of original URL → blob URL\n *\n * @example\n * ```typescript\n * const images = await batchFetchAuthenticatedImages([\n * '/api/organizations/123/image',\n * '/api/organizations/456/image'\n * ])\n * // { '/api/organizations/123/image': 'blob:...', '/api/organizations/456/image': 'blob:...' }\n * ```\n */\nexport async function batchFetchAuthenticatedImages(\n imageUrls: string[],\n config?: BatchImageFetchConfig\n): Promise<Record<string, string | undefined>> {\n const results: Record<string, string | undefined> = {}\n\n if (imageUrls.length === 0) {\n return results\n }\n\n const { tenantHostUrl, enableDevMode, accessTokenKey } = {\n ...getBatchImageConfig(),\n ...config\n }\n\n const fetchPromises = imageUrls.map(async (imageUrl) => {\n try {\n // Construct full image URL\n let fullImageUrl: string\n if (imageUrl.startsWith('http://') || imageUrl.startsWith('https://')) {\n fullImageUrl = imageUrl\n } else if (imageUrl.startsWith('/api/')) {\n fullImageUrl = `${tenantHostUrl}${imageUrl}`\n } else if (imageUrl.startsWith('/')) {\n fullImageUrl = `${tenantHostUrl}/api${imageUrl}`\n } else {\n fullImageUrl = `${tenantHostUrl}/api/${imageUrl}`\n }\n\n // Add cache buster\n const cacheBuster = `?t=${Date.now()}`\n fullImageUrl = fullImageUrl + cacheBuster\n\n // Prepare headers\n const headers: Record<string, string> = {\n 'Accept': 'image/*',\n 'Cache-Control': 'no-cache, no-store, must-revalidate',\n 'Pragma': 'no-cache'\n }\n\n // Add Bearer token in dev mode\n if (enableDevMode) {\n try {\n const accessToken = localStorage.getItem(accessTokenKey)\n if (accessToken) {\n headers['Authorization'] = `Bearer ${accessToken}`\n }\n } catch (error) {\n // Silently continue without token\n }\n }\n\n // Fetch image\n const response = await fetch(fullImageUrl, {\n method: 'GET',\n credentials: 'include', // Include cookies for authentication\n headers\n })\n\n if (!response.ok) {\n throw new Error(`Failed to fetch image: ${response.status}`)\n }\n\n // Convert to blob URL\n const blob = await response.blob()\n const objectUrl = URL.createObjectURL(blob)\n\n return { imageUrl, fetchedUrl: objectUrl }\n } catch (error) {\n console.warn(`Failed to fetch image ${imageUrl}:`, error)\n return { imageUrl, fetchedUrl: undefined }\n }\n })\n\n const fetchResults = await Promise.all(fetchPromises)\n\n fetchResults.forEach(({ imageUrl, fetchedUrl }) => {\n results[imageUrl] = fetchedUrl\n })\n\n return results\n}\n\n/**\n * React hook to batch fetch images with authentication\n *\n * Features:\n * - Automatically deduplicates URLs\n * - Caches fetched results\n * - Only fetches new/unfetched URLs\n * - Cleans up blob URLs on unmount\n *\n * @param imageUrls - Array of image URLs (can include null/undefined)\n * @param config - Optional configuration override\n * @returns Map of original URL → fetched blob URL\n *\n * @example\n * ```typescript\n * // In a component\n * const imageUrls = useMemo(() =>\n * organizations.map(org => org.imageUrl).filter(Boolean),\n * [organizations]\n * )\n * const fetchedImages = useBatchImages(imageUrls)\n *\n * // Use in render\n * <img src={fetchedImages[org.imageUrl]} alt={org.name} />\n * ```\n */\nexport function useBatchImages(\n imageUrls: (string | null | undefined)[],\n config?: BatchImageFetchConfig\n): Record<string, string | undefined> {\n const [fetchedImages, setFetchedImages] = useState<Record<string, string | undefined>>({})\n const [loading, setLoading] = useState(false)\n\n // Deduplicate and filter out null/undefined\n const uniqueUrls = useMemo(() =>\n Array.from(new Set(imageUrls.filter((url): url is string => Boolean(url)))),\n [imageUrls]\n )\n\n // Track URLs we've already requested to avoid duplicate fetches\n const requestedUrls = useRef<Set<string>>(new Set())\n\n useEffect(() => {\n if (uniqueUrls.length === 0) {\n setFetchedImages({})\n return\n }\n\n // Find URLs we haven't fetched yet\n const urlsToFetch = uniqueUrls.filter(url => !requestedUrls.current.has(url))\n\n if (urlsToFetch.length === 0) {\n return // All URLs already requested\n }\n\n // Mark these URLs as requested\n urlsToFetch.forEach(url => requestedUrls.current.add(url))\n\n setLoading(true)\n\n batchFetchAuthenticatedImages(urlsToFetch, config)\n .then(newResults => {\n setFetchedImages(prev => ({ ...prev, ...newResults }))\n })\n .catch(error => {\n console.error('Failed to batch fetch images:', error)\n })\n .finally(() => {\n setLoading(false)\n })\n\n // Cleanup blob URLs on unmount\n return () => {\n Object.values(fetchedImages).forEach(blobUrl => {\n if (blobUrl && blobUrl.startsWith('blob:')) {\n URL.revokeObjectURL(blobUrl)\n }\n })\n }\n }, [uniqueUrls, config])\n\n return fetchedImages\n}\n","import { useEffect, useState, useRef } from 'react'\n\n/**\n * Configuration for single image fetching\n * Uses same config as batch image fetching for consistency\n */\nexport interface AuthenticatedImageConfig {\n /** Base URL for tenant-specific API calls (e.g., 'https://tenant.openframe.dev' or '') */\n tenantHostUrl?: string\n /** Enable dev mode with Bearer token from localStorage */\n enableDevMode?: boolean\n /** localStorage key for access token (default: 'of_access_token') */\n accessTokenKey?: string\n}\n\n/**\n * Global configuration for authenticated image fetching\n * Shared with useBatchImages for consistency\n */\nlet globalImageConfig: AuthenticatedImageConfig = {}\n\n/**\n * Global cache for authenticated images\n * Stores blob URLs by cache key\n */\ninterface ImageCacheEntry {\n blobUrl: string\n timestamp: number\n refCount: number\n}\n\nconst imageCache = new Map<string, ImageCacheEntry>()\nconst pendingRequests = new Map<string, Promise<string | undefined>>()\n\n/**\n * Cache cleanup interval (5 minutes)\n */\nconst CACHE_CLEANUP_INTERVAL = 5 * 60 * 1000\n\n/**\n * Cache entry max age (30 minutes)\n */\nconst CACHE_MAX_AGE = 30 * 60 * 1000\n\n/**\n * Clean up expired cache entries\n */\nfunction cleanupImageCache() {\n const now = Date.now()\n for (const [key, entry] of imageCache.entries()) {\n if (entry.refCount === 0 && now - entry.timestamp > CACHE_MAX_AGE) {\n URL.revokeObjectURL(entry.blobUrl)\n imageCache.delete(key)\n }\n }\n}\n\n/**\n * Periodic cache cleanup\n */\nif (typeof window !== 'undefined') {\n setInterval(cleanupImageCache, CACHE_CLEANUP_INTERVAL)\n}\n\n/**\n * Configure global settings for authenticated image fetching\n * Call this once in your app initialization (e.g., _app.tsx or layout.tsx)\n *\n * Note: This uses the same configuration as useBatchImages. If you've already\n * called configureBatchImageFetch(), you don't need to call this separately.\n *\n * @example\n * ```typescript\n * // In app initialization\n * configureAuthenticatedImage({\n * tenantHostUrl: process.env.NEXT_PUBLIC_TENANT_HOST_URL || '',\n * enableDevMode: process.env.NEXT_PUBLIC_ENABLE_DEV_TICKET_OBSERVER === 'true'\n * })\n * ```\n */\nexport function configureAuthenticatedImage(config: AuthenticatedImageConfig): void {\n globalImageConfig = { ...globalImageConfig, ...config }\n}\n\n/**\n * Get current authenticated image configuration\n */\nfunction getImageConfig(): Required<AuthenticatedImageConfig> {\n return {\n tenantHostUrl: globalImageConfig.tenantHostUrl || '',\n enableDevMode: globalImageConfig.enableDevMode ?? false,\n accessTokenKey: globalImageConfig.accessTokenKey || 'of_access_token'\n }\n}\n\n/**\n * React hook to fetch a single image with authentication\n *\n * Features:\n * - Fetches image with cookie authentication\n * - Optional Bearer token in dev mode\n * - Converts to blob URL for img src\n * - Automatic cleanup of blob URLs\n * - Cache-busting with refreshKey\n * - Loading and error states\n * - **Global caching** - Prevents duplicate requests for identical URLs\n * - **Automatic deduplication** - Multiple components using same URL share cached result\n * - **Reference counting** - Cached blobs cleaned up when no longer used\n *\n * @param imageUrl - The image URL to fetch (null/undefined = no fetch)\n * @param refreshKey - Optional key to force re-fetch (e.g., version number, timestamp)\n * @param config - Optional configuration override\n * @returns Object with imageUrl (blob), isLoading, and error\n *\n * @example\n * ```typescript\n * // Basic usage\n * const { imageUrl, isLoading, error } = useAuthenticatedImage(\n * organization?.imageUrl\n * )\n *\n * // With refresh key (e.g., after upload)\n * const { imageUrl } = useAuthenticatedImage(\n * organization?.imageUrl,\n * organization?.imageVersion // Timestamp or version number\n * )\n *\n * // In render\n * {imageUrl && <img src={imageUrl} alt=\"Organization\" />}\n * ```\n */\nexport function useAuthenticatedImage(\n imageUrl?: string | null,\n refreshKey?: string | number,\n config?: AuthenticatedImageConfig\n): {\n imageUrl: string | undefined\n isLoading: boolean\n error: string | null\n} {\n const [fetchedImageUrl, setFetchedImageUrl] = useState<string | undefined>()\n const [isLoading, setIsLoading] = useState(false)\n const [error, setError] = useState<string | null>(null)\n const currentCacheKeyRef = useRef<string | null>(null)\n\n useEffect(() => {\n if (!imageUrl) {\n setFetchedImageUrl(undefined)\n setIsLoading(false)\n setError(null)\n \n if (currentCacheKeyRef.current) {\n const entry = imageCache.get(currentCacheKeyRef.current)\n if (entry) {\n entry.refCount--\n }\n currentCacheKeyRef.current = null\n }\n return\n }\n\n const { tenantHostUrl, enableDevMode, accessTokenKey } = {\n ...getImageConfig(),\n ...config\n }\n\n // Construct full image URL\n let fullImageUrl: string\n if (imageUrl.startsWith('http://') || imageUrl.startsWith('https://')) {\n fullImageUrl = imageUrl\n } else if (imageUrl.startsWith('/api/')) {\n fullImageUrl = `${tenantHostUrl}${imageUrl}`\n } else if (imageUrl.startsWith('/')) {\n fullImageUrl = `${tenantHostUrl}/api${imageUrl}`\n } else {\n fullImageUrl = `${tenantHostUrl}/api/${imageUrl}`\n }\n\n // Create cache key (use refreshKey if provided, otherwise no cache buster for caching)\n const cacheKey = refreshKey ? `${fullImageUrl}?v=${refreshKey}` : fullImageUrl\n \n if (currentCacheKeyRef.current && currentCacheKeyRef.current !== cacheKey) {\n const prevEntry = imageCache.get(currentCacheKeyRef.current)\n if (prevEntry) {\n prevEntry.refCount--\n }\n }\n \n currentCacheKeyRef.current = cacheKey\n\n const cachedEntry = imageCache.get(cacheKey)\n if (cachedEntry) {\n cachedEntry.refCount++\n cachedEntry.timestamp = Date.now()\n \n setFetchedImageUrl(cachedEntry.blobUrl)\n setIsLoading(false)\n setError(null)\n return\n }\n\n const pendingRequest = pendingRequests.get(cacheKey)\n if (pendingRequest) {\n setIsLoading(true)\n setError(null)\n \n pendingRequest\n .then(blobUrl => {\n if (blobUrl) {\n const entry = imageCache.get(cacheKey)\n if (entry) {\n entry.refCount++\n setFetchedImageUrl(blobUrl)\n }\n }\n setIsLoading(false)\n })\n .catch(err => {\n setError(err instanceof Error ? err.message : 'Failed to fetch image')\n setIsLoading(false)\n })\n return\n }\n\n setIsLoading(true)\n setError(null)\n\n const requestUrl = refreshKey ? cacheKey : `${fullImageUrl}?t=${Date.now()}`\n\n // Prepare headers\n const headers: Record<string, string> = {\n 'Accept': 'image/*',\n 'Cache-Control': 'no-cache, no-store, must-revalidate',\n 'Pragma': 'no-cache'\n }\n\n // Add Bearer token in dev mode\n if (enableDevMode) {\n try {\n const accessToken = localStorage.getItem(accessTokenKey)\n if (accessToken) {\n headers['Authorization'] = `Bearer ${accessToken}`\n }\n } catch (error) {\n // Silently continue without token\n }\n }\n\n const fetchPromise = fetch(requestUrl, {\n method: 'GET',\n credentials: 'include', // Include cookies for authentication\n headers\n })\n .then(response => {\n if (!response.ok) {\n throw new Error(`Failed to fetch image: ${response.status}`)\n }\n return response.blob()\n })\n .then(blob => {\n const objectUrl = URL.createObjectURL(blob)\n \n imageCache.set(cacheKey, {\n blobUrl: objectUrl,\n timestamp: Date.now(),\n refCount: 1\n })\n \n setFetchedImageUrl(objectUrl)\n setIsLoading(false)\n return objectUrl\n })\n .catch(err => {\n setError(err instanceof Error ? err.message : 'Failed to fetch image')\n setFetchedImageUrl(undefined)\n setIsLoading(false)\n throw err\n })\n .finally(() => {\n pendingRequests.delete(cacheKey)\n })\n\n pendingRequests.set(cacheKey, fetchPromise)\n\n }, [imageUrl, refreshKey, config])\n\n useEffect(() => {\n return () => {\n if (currentCacheKeyRef.current) {\n const entry = imageCache.get(currentCacheKeyRef.current)\n if (entry) {\n entry.refCount--\n }\n }\n }\n }, [])\n\n return { imageUrl: fetchedImageUrl, isLoading, error }\n}\n","/**\n * useQueryParams Hook - GraphQL Integration for URL State Management\n *\n * Automatically generates URL state management from GraphQL queries.\n * Parses query AST at runtime, flattens nested input types, and syncs with URL.\n *\n * @example\n * const LOGS_QUERY = gql`\n * query GetLogs($search: String, $filter: LogFilterInput) { ... }\n * `\n *\n * const { variables, setParam } = useQueryParams(LOGS_QUERY)\n * const { data } = useQuery(LOGS_QUERY, { variables })\n *\n * // URL: /logs?search=error&severity=critical\n * // variables: { search: 'error', filter: { severity: ['critical'] } }\n */\n\n'use client'\n\nimport { useEffect, useState, useMemo, useCallback } from 'react'\nimport { useRouter, useSearchParams } from '../../embed-shims/next-navigation'\nimport { DocumentNode } from 'graphql'\n\nimport { extractVariablesFromQuery } from './graphql-parser'\nimport { flattenQueryVariables, mergeDefaults, validateSchema, FlattenedParam } from './flatten-schema'\nimport { urlParamsToVariables, variablesToUrlParams, mergeVariables, clearParams } from './url-converter'\nimport { introspector } from './introspection'\n\n/**\n * Options for useQueryParams hook\n */\nexport interface UseQueryParamsOptions {\n /** Default values for parameters */\n defaultValues?: Record<string, any>\n\n /** GraphQL endpoint for introspection (defaults to process.env.NEXT_PUBLIC_API_URL/graphql) */\n introspectionEndpoint?: string\n\n /** HTTP headers for introspection (e.g., authentication) */\n introspectionHeaders?: Record<string, string>\n\n /** Skip introspection (use only AST parsing, no nested type flattening) */\n skipIntrospection?: boolean\n\n /** Custom parameter name mapping (override auto-generated names) */\n paramMapping?: Record<string, string>\n\n /** Enable debug logging */\n debug?: boolean\n}\n\n/**\n * Return type for useQueryParams hook\n */\nexport interface UseQueryParamsReturn<TVariables = Record<string, any>> {\n /** GraphQL variables ready for Apollo Client */\n variables: TVariables\n\n /** Raw URL parameters (before conversion to variables) */\n params: Record<string, any>\n\n /** Flattened parameter schema */\n schema: Record<string, FlattenedParam>\n\n /** Set a single parameter */\n setParam: (key: string, value: any) => void\n\n /** Set multiple parameters at once */\n setParams: (params: Record<string, any>) => void\n\n /** Clear specific parameters */\n clearParams: (keys: string[]) => void\n\n /** Reset all parameters (clear URL) */\n resetParams: () => void\n\n /** Whether schema is ready (introspection complete) */\n isReady: boolean\n\n /** Loading state during initialization */\n isLoading: boolean\n\n /** Error during initialization */\n error: Error | null\n}\n\n/**\n * useQueryParams - Auto-generate URL state from GraphQL query\n *\n * This hook:\n * 1. Parses GraphQL query AST to extract variable definitions\n * 2. Fetches GraphQL schema via introspection (optional, cached)\n * 3. Flattens nested input types to simple URL parameters\n * 4. Syncs URL ↔ GraphQL variables bidirectionally\n * 5. Provides type-safe parameter updates\n *\n * @param query - GraphQL DocumentNode (from gql`` template tag)\n * @param options - Configuration options\n * @returns Hook API for managing URL state\n */\nexport function useQueryParams<TVariables = Record<string, any>>(\n query: DocumentNode,\n options: UseQueryParamsOptions = {}\n): UseQueryParamsReturn<TVariables> {\n const router = useRouter()\n const searchParams = useSearchParams()\n\n const [isLoading, setIsLoading] = useState(true)\n const [isReady, setIsReady] = useState(false)\n const [error, setError] = useState<Error | null>(null)\n const [schema, setSchema] = useState<Record<string, FlattenedParam>>({})\n\n // Extract default values\n const defaultValues = options.defaultValues || {}\n const skipIntrospection = options.skipIntrospection || false\n const debug = options.debug || false\n\n // Initialize: Parse query + fetch schema (once)\n useEffect(() => {\n async function initialize() {\n try {\n if (debug) console.log('[useQueryParams] Initializing...')\n\n // 1. Extract variables from query AST\n const queryVariables = extractVariablesFromQuery(query)\n\n if (debug) {\n console.log('[useQueryParams] Extracted variables:', queryVariables)\n }\n\n // 2. Fetch GraphQL schema via introspection (if needed and not skipped)\n if (!skipIntrospection && !introspector.isLoaded()) {\n const endpoint = options.introspectionEndpoint ||\n (typeof window !== 'undefined' && process.env.NEXT_PUBLIC_API_URL\n ? `${process.env.NEXT_PUBLIC_API_URL}/graphql`\n : '')\n\n if (endpoint) {\n try {\n await introspector.fetchSchema(endpoint, options.introspectionHeaders)\n if (debug) console.log('[useQueryParams] Introspection complete')\n } catch (err) {\n console.warn('[useQueryParams] Introspection failed, continuing without it:', err)\n // Continue without introspection - nested types won't be flattened\n }\n }\n }\n\n // 3. Flatten schema (with or without introspection)\n let flattenedSchema = await flattenQueryVariables(queryVariables, introspector)\n\n // Apply custom param mapping if provided\n if (options.paramMapping) {\n flattenedSchema = applyParamMapping(flattenedSchema, options.paramMapping)\n }\n\n // Merge default values\n flattenedSchema = mergeDefaults(flattenedSchema, defaultValues)\n\n // Validate schema\n validateSchema(flattenedSchema)\n\n if (debug) {\n console.log('[useQueryParams] Flattened schema:', flattenedSchema)\n }\n\n setSchema(flattenedSchema)\n setIsReady(true)\n } catch (err) {\n const error = err instanceof Error ? err : new Error(String(err))\n console.error('[useQueryParams] Initialization failed:', error)\n setError(error)\n } finally {\n setIsLoading(false)\n }\n }\n\n initialize()\n }, [query, options.introspectionEndpoint, skipIntrospection, debug])\n\n // Convert URL params to GraphQL variables\n const variables = useMemo(() => {\n if (!isReady) {\n return defaultValues as TVariables\n }\n\n try {\n const varsFromUrl = urlParamsToVariables(searchParams, schema)\n const merged = { ...defaultValues, ...varsFromUrl }\n\n if (debug) {\n console.log('[useQueryParams] Variables from URL:', merged)\n }\n\n return merged as TVariables\n } catch (err) {\n console.error('[useQueryParams] Failed to convert URL to variables:', err)\n return defaultValues as TVariables\n }\n }, [searchParams, schema, isReady, defaultValues, debug])\n\n // Raw URL params (before conversion)\n const params = useMemo(() => {\n const result: Record<string, any> = {}\n searchParams.forEach((value, key) => {\n if (result[key]) {\n // Multiple values - convert to array\n if (!Array.isArray(result[key])) {\n result[key] = [result[key]]\n }\n result[key].push(value)\n } else {\n result[key] = value\n }\n })\n return result\n }, [searchParams])\n\n // Update URL with new parameters\n const updateUrl = useCallback((newParams: URLSearchParams) => {\n const url = newParams.toString() ? `?${newParams.toString()}` : window.location.pathname\n\n if (debug) {\n console.log('[useQueryParams] Updating URL:', url)\n }\n\n // Use replace for shallow routing (no page reload, no history spam)\n router.replace(url, { scroll: false })\n }, [router, debug])\n\n // Set a single parameter\n const setParam = useCallback((key: string, value: any) => {\n if (!isReady) {\n console.warn('[useQueryParams] Schema not ready, cannot set param')\n return\n }\n\n try {\n const currentVars = variables\n const updated = mergeVariables(currentVars as Record<string, any>, { [key]: value }, schema)\n const newParams = variablesToUrlParams(updated, schema)\n updateUrl(newParams)\n } catch (err) {\n console.error('[useQueryParams] Failed to set param:', err)\n }\n }, [variables, schema, isReady, updateUrl])\n\n // Set multiple parameters\n const setParams = useCallback((updates: Record<string, any>) => {\n if (!isReady) {\n console.warn('[useQueryParams] Schema not ready, cannot set params')\n return\n }\n\n try {\n const currentVars = variables\n const updated = mergeVariables(currentVars as Record<string, any>, updates, schema)\n const newParams = variablesToUrlParams(updated, schema)\n updateUrl(newParams)\n } catch (err) {\n console.error('[useQueryParams] Failed to set params:', err)\n }\n }, [variables, schema, isReady, updateUrl])\n\n // Clear specific parameters\n const clearParamsHandler = useCallback((keys: string[]) => {\n if (!isReady) {\n console.warn('[useQueryParams] Schema not ready, cannot clear params')\n return\n }\n\n try {\n const currentVars = variables\n const updated = clearParams(currentVars as Record<string, any>, keys, schema)\n const newParams = variablesToUrlParams(updated, schema)\n updateUrl(newParams)\n } catch (err) {\n console.error('[useQueryParams] Failed to clear params:', err)\n }\n }, [variables, schema, isReady, updateUrl])\n\n // Reset all parameters\n const resetParams = useCallback(() => {\n if (debug) {\n console.log('[useQueryParams] Resetting params')\n }\n\n router.replace(window.location.pathname, { scroll: false })\n }, [router, debug])\n\n return {\n variables,\n params,\n schema,\n setParam,\n setParams,\n clearParams: clearParamsHandler,\n resetParams,\n isReady,\n isLoading,\n error\n }\n}\n\n/**\n * Apply custom parameter name mapping to schema\n */\nfunction applyParamMapping(\n schema: Record<string, FlattenedParam>,\n mapping: Record<string, string>\n): Record<string, FlattenedParam> {\n const mapped: Record<string, FlattenedParam> = {}\n\n for (const [key, param] of Object.entries(schema)) {\n const newKey = mapping[key] || key\n mapped[newKey] = {\n ...param,\n urlParamName: mapping[key] || param.urlParamName\n }\n }\n\n return mapped\n}\n","/**\n * GraphQL AST Parser for URL State Management\n *\n * Extracts variable definitions from GraphQL DocumentNode at runtime\n * to automatically generate URL parameter handling.\n */\n\nimport {\n DocumentNode,\n VariableDefinitionNode,\n TypeNode,\n NamedTypeNode,\n ListTypeNode,\n NonNullTypeNode,\n visit\n} from 'graphql'\n\n/**\n * JavaScript type that can be represented in URL parameters\n */\nexport type JSType = 'string' | 'number' | 'boolean' | 'array' | 'object'\n\n/**\n * Variable definition extracted from GraphQL query\n */\nexport interface VariableDefinition {\n /** Variable name (e.g., \"search\", \"filter\") */\n name: string\n /** JavaScript type for URL parameter handling */\n type: JSType\n /** Whether the variable is required (non-null in GraphQL) */\n required: boolean\n /** Whether the variable is an array/list */\n isArray: boolean\n /** Original GraphQL type name (e.g., \"String\", \"LogFilterInput\") */\n graphqlTypeName: string\n}\n\n/**\n * Parsed type information from GraphQL TypeNode\n */\ninterface ParsedType {\n typeName: string\n isNonNull: boolean\n isList: boolean\n}\n\n/**\n * Extract all variable definitions from a GraphQL query\n *\n * @param query - GraphQL DocumentNode (from gql template tag)\n * @returns Record of variable definitions keyed by variable name\n *\n * @example\n * const LOGS_QUERY = gql`\n * query GetLogs($search: String, $filter: LogFilterInput) { ... }\n * `\n *\n * const variables = extractVariablesFromQuery(LOGS_QUERY)\n * // {\n * // search: { name: 'search', type: 'string', ... },\n * // filter: { name: 'filter', type: 'object', graphqlTypeName: 'LogFilterInput' }\n * // }\n */\nexport function extractVariablesFromQuery(\n query: DocumentNode\n): Record<string, VariableDefinition> {\n const variables: Record<string, VariableDefinition> = {}\n\n visit(query, {\n VariableDefinition(node: VariableDefinitionNode) {\n const name = node.variable.name.value\n const typeInfo = parseGraphQLType(node.type)\n\n variables[name] = {\n name,\n type: mapGraphQLTypeToJS(typeInfo.typeName),\n required: typeInfo.isNonNull,\n isArray: typeInfo.isList,\n graphqlTypeName: typeInfo.typeName\n }\n }\n })\n\n return variables\n}\n\n/**\n * Parse GraphQL TypeNode to extract type information\n * Handles NonNullType, ListType, and NamedType recursively\n *\n * @param typeNode - GraphQL type node from AST\n * @returns Parsed type information\n */\nfunction parseGraphQLType(typeNode: TypeNode): ParsedType {\n let typeName = ''\n let isNonNull = false\n let isList = false\n\n // Unwrap NonNullType wrapper\n if (typeNode.kind === 'NonNullType') {\n isNonNull = true\n typeNode = typeNode.type\n }\n\n // Handle ListType\n if (typeNode.kind === 'ListType') {\n isList = true\n typeNode = typeNode.type\n\n // ListType can also be wrapped in NonNullType\n if (typeNode.kind === 'NonNullType') {\n typeNode = typeNode.type\n }\n }\n\n // Extract the base type name\n if (typeNode.kind === 'NamedType') {\n typeName = typeNode.name.value\n }\n\n return {\n typeName,\n isNonNull,\n isList\n }\n}\n\n/**\n * Map GraphQL scalar/input types to JavaScript types\n *\n * @param graphqlType - GraphQL type name (e.g., \"String\", \"Int\", \"LogFilterInput\")\n * @returns JavaScript type for URL parameter handling\n */\nfunction mapGraphQLTypeToJS(graphqlType: string): JSType {\n // GraphQL scalar types\n const scalarTypeMap: Record<string, JSType> = {\n 'String': 'string',\n 'Int': 'number',\n 'Float': 'number',\n 'Boolean': 'boolean',\n 'ID': 'string'\n }\n\n // Check if it's a known scalar\n if (scalarTypeMap[graphqlType]) {\n return scalarTypeMap[graphqlType]\n }\n\n // Unknown types are assumed to be input objects\n // These will be flattened using introspection\n return 'object'\n}\n\n/**\n * Check if a GraphQL type is a scalar type\n */\nexport function isScalarType(graphqlTypeName: string): boolean {\n const scalars = ['String', 'Int', 'Float', 'Boolean', 'ID']\n return scalars.includes(graphqlTypeName)\n}\n\n/**\n * Check if a GraphQL type is an input object type\n */\nexport function isInputObjectType(graphqlTypeName: string): boolean {\n return !isScalarType(graphqlTypeName)\n}\n","/**\n * Schema Flattening Utilities for URL State Management\n *\n * Converts nested GraphQL input types to flat URL parameter mappings.\n * Example: { filter: { severity: [...] } } → URL params: ?severity=...\n */\n\nimport { VariableDefinition, JSType } from './graphql-parser'\nimport { GraphQLIntrospector } from './introspection'\n\n/**\n * Flattened URL parameter configuration\n *\n * Maps URL parameter names to their GraphQL variable paths\n */\nexport interface FlattenedParam {\n /** URL parameter name (e.g., \"severity\") */\n urlParamName: string\n /** Path in GraphQL variables (e.g., \"filter.severity\") */\n graphqlPath: string\n /** JavaScript type for URL handling */\n type: JSType\n /** Default value for this parameter */\n defaultValue?: any\n /** Whether this parameter is required */\n required?: boolean\n /** Whether this parameter is an array */\n isArray?: boolean\n}\n\n/**\n * Flatten GraphQL query variables to URL parameter schema\n *\n * Takes top-level query variables and flattens nested input objects\n * to create a simple URL parameter mapping.\n *\n * @param queryVariables - Variables extracted from GraphQL query\n * @param introspector - Introspector instance with loaded schema\n * @returns Flattened parameter schema for URL handling\n *\n * @example\n * Input variables:\n * {\n * search: { type: 'string', ... },\n * filter: { type: 'object', graphqlTypeName: 'LogFilterInput' },\n * cursor: { type: 'string', ... }\n * }\n *\n * Output schema:\n * {\n * search: { urlParamName: 'search', graphqlPath: 'search', type: 'string' },\n * severity: { urlParamName: 'severity', graphqlPath: 'filter.severity', type: 'array' },\n * toolType: { urlParamName: 'toolType', graphqlPath: 'filter.toolType', type: 'array' },\n * cursor: { urlParamName: 'cursor', graphqlPath: 'cursor', type: 'string' }\n * }\n *\n * URL: ?search=error&severity=critical&toolType=tactical&cursor=abc\n */\nexport async function flattenQueryVariables(\n queryVariables: Record<string, VariableDefinition>,\n introspector: GraphQLIntrospector\n): Promise<Record<string, FlattenedParam>> {\n const flattened: Record<string, FlattenedParam> = {}\n\n for (const [varName, varDef] of Object.entries(queryVariables)) {\n // Primitive types or arrays - keep at top level\n if (varDef.type !== 'object') {\n flattened[varName] = {\n urlParamName: varName,\n graphqlPath: varName,\n type: varDef.isArray ? 'array' : varDef.type,\n required: varDef.required,\n isArray: varDef.isArray\n }\n continue\n }\n\n // Input object types - flatten fields to top level\n // This requires introspection to know the input type's fields\n if (introspector.isLoaded() && introspector.hasType(varDef.graphqlTypeName)) {\n const fields = introspector.getInputTypeFields(varDef.graphqlTypeName)\n\n for (const [fieldName, fieldDef] of Object.entries(fields)) {\n flattened[fieldName] = {\n urlParamName: fieldName,\n graphqlPath: `${varName}.${fieldName}`,\n type: fieldDef.isArray ? 'array' : fieldDef.type,\n required: fieldDef.required,\n isArray: fieldDef.isArray\n }\n }\n } else {\n // Introspection not available or type not found\n // Keep as top-level object (will need manual handling)\n flattened[varName] = {\n urlParamName: varName,\n graphqlPath: varName,\n type: 'object',\n required: varDef.required,\n isArray: false\n }\n }\n }\n\n return flattened\n}\n\n/**\n * Merge default values into flattened schema\n *\n * @param schema - Flattened parameter schema\n * @param defaults - Default values keyed by URL param name\n * @returns Updated schema with default values\n */\nexport function mergeDefaults(\n schema: Record<string, FlattenedParam>,\n defaults: Record<string, any>\n): Record<string, FlattenedParam> {\n const merged: Record<string, FlattenedParam> = {}\n\n for (const [key, param] of Object.entries(schema)) {\n merged[key] = {\n ...param,\n defaultValue: defaults[key] !== undefined ? defaults[key] : param.defaultValue\n }\n }\n\n return merged\n}\n\n/**\n * Validate that flattened schema has no conflicts\n *\n * Ensures that no two parameters map to the same URL param name\n *\n * @param schema - Flattened parameter schema\n * @throws Error if conflicts are detected\n */\nexport function validateSchema(schema: Record<string, FlattenedParam>): void {\n const urlParamNames = new Set<string>()\n\n for (const [key, param] of Object.entries(schema)) {\n if (urlParamNames.has(param.urlParamName)) {\n throw new Error(\n `[FlattenSchema] Conflict: Multiple parameters map to URL param \"${param.urlParamName}\"`\n )\n }\n urlParamNames.add(param.urlParamName)\n }\n}\n\n/**\n * Get all array-type parameters from schema\n *\n * Useful for knowing which URL params should use repeated values\n * (e.g., ?severity=error&severity=warning)\n *\n * @param schema - Flattened parameter schema\n * @returns Array parameter names\n */\nexport function getArrayParams(schema: Record<string, FlattenedParam>): string[] {\n return Object.entries(schema)\n .filter(([_, param]) => param.type === 'array' || param.isArray)\n .map(([key]) => key)\n}\n\n/**\n * Get required parameters from schema\n *\n * @param schema - Flattened parameter schema\n * @returns Required parameter names\n */\nexport function getRequiredParams(schema: Record<string, FlattenedParam>): string[] {\n return Object.entries(schema)\n .filter(([_, param]) => param.required)\n .map(([key]) => key)\n}\n\n/**\n * Check if a parameter should be included in URL\n *\n * Excludes:\n * - null/undefined values\n * - Empty arrays\n * - Default values (to keep URLs clean)\n *\n * @param value - Parameter value\n * @param param - Parameter configuration\n * @returns Whether to include in URL\n */\nexport function shouldIncludeInUrl(\n value: any,\n param: FlattenedParam\n): boolean {\n // Null/undefined - exclude\n if (value === null || value === undefined) {\n return false\n }\n\n // Empty arrays - exclude\n if (Array.isArray(value) && value.length === 0) {\n return false\n }\n\n // Empty strings - exclude\n if (value === '') {\n return false\n }\n\n // Default values - exclude to keep URL clean\n if (param.defaultValue !== undefined && value === param.defaultValue) {\n return false\n }\n\n return true\n}\n","/**\n * URL ↔ Variables Converter for URL State Management\n *\n * Bidirectional conversion between URL parameters and GraphQL variables.\n * Handles type coercion, nested paths, and array parameters.\n */\n\nimport { FlattenedParam, shouldIncludeInUrl } from './flatten-schema'\nimport { JSType } from './graphql-parser'\n\n/**\n * Convert URL search params to GraphQL variables\n *\n * Reads URL parameters and reconstructs the nested GraphQL variables object\n * based on the flattened schema mapping.\n *\n * @param searchParams - URLSearchParams from window.location or Next.js\n * @param schema - Flattened parameter schema\n * @returns GraphQL variables object ready for Apollo Client\n *\n * @example\n * URL: ?search=error&severity=critical&severity=error&cursor=abc\n * Schema: {\n * search: { graphqlPath: 'search', type: 'string' },\n * severity: { graphqlPath: 'filter.severity', type: 'array' },\n * cursor: { graphqlPath: 'cursor', type: 'string' }\n * }\n * Result: {\n * search: 'error',\n * filter: { severity: ['critical', 'error'] },\n * cursor: 'abc'\n * }\n */\nexport function urlParamsToVariables(\n searchParams: URLSearchParams,\n schema: Record<string, FlattenedParam>\n): Record<string, any> {\n const variables: Record<string, any> = {}\n\n for (const [paramName, paramConfig] of Object.entries(schema)) {\n // Read value from URL\n const rawValue = paramConfig.type === 'array' || paramConfig.isArray\n ? searchParams.getAll(paramName)\n : searchParams.get(paramName)\n\n // Skip if no value in URL\n if (!rawValue || (Array.isArray(rawValue) && rawValue.length === 0)) {\n // Use default value if available\n if (paramConfig.defaultValue !== undefined) {\n setNestedValue(variables, paramConfig.graphqlPath, paramConfig.defaultValue)\n }\n continue\n }\n\n // Coerce value to correct type\n const value = coerceValue(rawValue, paramConfig.type)\n\n // Set value at nested path\n setNestedValue(variables, paramConfig.graphqlPath, value)\n }\n\n return variables\n}\n\n/**\n * Convert GraphQL variables to URL search params\n *\n * Flattens nested GraphQL variables to URL parameters based on schema mapping.\n * Excludes null/undefined/default values to keep URLs clean.\n *\n * @param variables - GraphQL variables object\n * @param schema - Flattened parameter schema\n * @returns URLSearchParams ready for router.push()\n *\n * @example\n * Variables: {\n * search: 'error',\n * filter: { severity: ['critical'], toolType: [] },\n * cursor: null\n * }\n * Result URL: ?search=error&severity=critical\n * (empty arrays and null values excluded)\n */\nexport function variablesToUrlParams(\n variables: Record<string, any>,\n schema: Record<string, FlattenedParam>\n): URLSearchParams {\n const params = new URLSearchParams()\n\n for (const [paramName, paramConfig] of Object.entries(schema)) {\n // Get value from nested path\n const value = getNestedValue(variables, paramConfig.graphqlPath)\n\n // Skip if should not include in URL\n if (!shouldIncludeInUrl(value, paramConfig)) {\n continue\n }\n\n // Add to URL params\n if (Array.isArray(value)) {\n // Array: Use repeated params (e.g., ?tag=foo&tag=bar)\n value.forEach(v => {\n if (v !== null && v !== undefined && v !== '') {\n params.append(paramName, String(v))\n }\n })\n } else {\n // Single value: Use set\n params.set(paramName, String(value))\n }\n }\n\n return params\n}\n\n/**\n * Coerce URL parameter value to correct JavaScript type\n *\n * @param value - Raw value from URLSearchParams (string or string[])\n * @param type - Target JavaScript type\n * @returns Coerced value\n */\nexport function coerceValue(value: string | string[], type: JSType): any {\n // Array handling\n if (Array.isArray(value)) {\n return value.map(v => coerceValue(v, type === 'array' ? 'string' : type))\n }\n\n // Type coercion for single values\n switch (type) {\n case 'number':\n const num = parseFloat(value)\n return isNaN(num) ? null : num\n\n case 'boolean':\n return value === 'true' || value === '1'\n\n case 'array':\n // Single value treated as array with one element\n return [value]\n\n case 'string':\n default:\n return value\n }\n}\n\n/**\n * Set value at nested path in object\n *\n * Creates intermediate objects as needed.\n *\n * @param obj - Target object to modify\n * @param path - Dot-separated path (e.g., \"filter.severity\")\n * @param value - Value to set\n *\n * @example\n * setNestedValue({}, 'filter.severity', ['error'])\n * // Result: { filter: { severity: ['error'] } }\n */\nexport function setNestedValue(obj: any, path: string, value: any): void {\n const parts = path.split('.')\n let current = obj\n\n // Navigate/create intermediate objects\n for (let i = 0; i < parts.length - 1; i++) {\n const part = parts[i]\n if (!(part in current) || typeof current[part] !== 'object') {\n current[part] = {}\n }\n current = current[part]\n }\n\n // Set final value\n const lastPart = parts[parts.length - 1]\n current[lastPart] = value\n}\n\n/**\n * Get value from nested path in object\n *\n * Returns undefined if path doesn't exist.\n *\n * @param obj - Source object\n * @param path - Dot-separated path (e.g., \"filter.severity\")\n * @returns Value at path or undefined\n *\n * @example\n * getNestedValue({ filter: { severity: ['error'] } }, 'filter.severity')\n * // Result: ['error']\n */\nexport function getNestedValue(obj: any, path: string): any {\n return path.split('.').reduce((current, key) => {\n return current?.[key]\n }, obj)\n}\n\n/**\n * Merge URL parameters into existing variables\n *\n * Useful for updating specific parameters without losing others.\n *\n * @param currentVariables - Current GraphQL variables\n * @param updates - Parameter updates (flat object)\n * @param schema - Flattened parameter schema\n * @returns Updated variables object\n */\nexport function mergeVariables(\n currentVariables: Record<string, any>,\n updates: Record<string, any>,\n schema: Record<string, FlattenedParam>\n): Record<string, any> {\n const merged = { ...currentVariables }\n\n for (const [paramName, value] of Object.entries(updates)) {\n const paramConfig = schema[paramName]\n if (!paramConfig) continue\n\n setNestedValue(merged, paramConfig.graphqlPath, value)\n }\n\n return merged\n}\n\n/**\n * Clear specific parameters from variables\n *\n * @param variables - Current GraphQL variables\n * @param paramNames - Parameter names to clear\n * @param schema - Flattened parameter schema\n * @returns Variables with specified params removed\n */\nexport function clearParams(\n variables: Record<string, any>,\n paramNames: string[],\n schema: Record<string, FlattenedParam>\n): Record<string, any> {\n const cleared = { ...variables }\n\n for (const paramName of paramNames) {\n const paramConfig = schema[paramName]\n if (!paramConfig) continue\n\n // Set to undefined (will be excluded from URL)\n setNestedValue(cleared, paramConfig.graphqlPath, undefined)\n }\n\n return cleared\n}\n\n/**\n * Validate GraphQL variables against schema\n *\n * Checks for required parameters and type consistency.\n *\n * @param variables - GraphQL variables to validate\n * @param schema - Flattened parameter schema\n * @returns Validation errors (empty array if valid)\n */\nexport function validateVariables(\n variables: Record<string, any>,\n schema: Record<string, FlattenedParam>\n): string[] {\n const errors: string[] = []\n\n for (const [paramName, paramConfig] of Object.entries(schema)) {\n const value = getNestedValue(variables, paramConfig.graphqlPath)\n\n // Check required parameters\n if (paramConfig.required && (value === null || value === undefined)) {\n errors.push(`Required parameter \"${paramName}\" is missing`)\n }\n\n // Check type consistency\n if (value !== null && value !== undefined) {\n const actualType = Array.isArray(value) ? 'array' : typeof value\n const expectedType = paramConfig.type === 'array' ? 'array' : paramConfig.type\n\n if (actualType !== expectedType && actualType !== 'object') {\n errors.push(\n `Parameter \"${paramName}\" has wrong type: expected ${expectedType}, got ${actualType}`\n )\n }\n }\n }\n\n return errors\n}\n","/**\n * GraphQL Schema Introspection for URL State Management\n *\n * Fetches GraphQL schema via introspection query at runtime (works behind auth).\n * Caches schema in localStorage + memory for performance.\n */\n\nimport {\n getIntrospectionQuery,\n buildClientSchema,\n IntrospectionQuery,\n GraphQLSchema,\n GraphQLInputObjectType,\n GraphQLInputType,\n isInputObjectType\n} from 'graphql'\nimport { VariableDefinition, JSType } from './graphql-parser'\n\n/**\n * Schema cache structure stored in localStorage\n */\ninterface SchemaCache {\n schema: IntrospectionQuery\n timestamp: number\n version: string\n}\n\n/**\n * GraphQL Introspection Manager\n *\n * Singleton class that handles schema introspection with caching.\n * Fetches schema once per session and caches for 24 hours.\n */\nexport class GraphQLIntrospector {\n private schema: GraphQLSchema | null = null\n private cacheKey = 'graphql-schema-cache-v1'\n private cacheDuration = 24 * 60 * 60 * 1000 // 24 hours\n private schemaVersion = '1.0.0'\n\n /**\n * Fetch GraphQL schema via introspection query\n *\n * @param endpoint - GraphQL endpoint URL\n * @param headers - HTTP headers (e.g., authentication)\n * @param skipCache - Force fresh fetch, ignore cache\n *\n * @example\n * await introspector.fetchSchema(\n * 'http://localhost/api/graphql',\n * { 'Authorization': 'Bearer token123' }\n * )\n */\n async fetchSchema(\n endpoint: string,\n headers: Record<string, string> = {},\n skipCache = false\n ): Promise<void> {\n // Check cache first (unless skipped)\n if (!skipCache) {\n const cached = this.loadFromCache()\n if (cached && Date.now() - cached.timestamp < this.cacheDuration) {\n try {\n this.schema = buildClientSchema(cached.schema)\n return\n } catch (error) {\n console.warn('[Introspector] Failed to build schema from cache:', error)\n // Continue to fetch fresh schema\n }\n }\n }\n\n // Fetch schema via introspection query\n try {\n const response = await fetch(endpoint, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n ...headers\n },\n body: JSON.stringify({\n query: getIntrospectionQuery()\n })\n })\n\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`)\n }\n\n const { data, errors } = await response.json()\n\n if (errors) {\n throw new Error(`GraphQL errors: ${JSON.stringify(errors)}`)\n }\n\n if (!data) {\n throw new Error('No data returned from introspection query')\n }\n\n // Build and cache schema\n this.schema = buildClientSchema(data)\n this.saveToCache(data)\n } catch (error) {\n console.error('[Introspector] Failed to fetch schema:', error)\n throw error\n }\n }\n\n /**\n * Get fields from a GraphQL input object type\n *\n * @param typeName - Name of the input type (e.g., \"LogFilterInput\")\n * @returns Record of field definitions\n *\n * @example\n * const fields = introspector.getInputTypeFields('LogFilterInput')\n * // {\n * // severity: { name: 'severity', type: 'array', ... },\n * // toolType: { name: 'toolType', type: 'array', ... }\n * // }\n */\n getInputTypeFields(typeName: string): Record<string, VariableDefinition> {\n if (!this.schema) {\n return {}\n }\n\n const type = this.schema.getType(typeName)\n\n if (!type || !isInputObjectType(type)) {\n return {}\n }\n\n const fields: Record<string, VariableDefinition> = {}\n const typeFields = type.getFields()\n\n for (const [fieldName, field] of Object.entries(typeFields)) {\n const fieldType = this.unwrapType(field.type)\n\n fields[fieldName] = {\n name: fieldName,\n type: this.mapGraphQLTypeToJS(fieldType.typeName),\n required: fieldType.isNonNull,\n isArray: fieldType.isList,\n graphqlTypeName: fieldType.typeName\n }\n }\n\n return fields\n }\n\n /**\n * Check if a type exists in the schema\n */\n hasType(typeName: string): boolean {\n if (!this.schema) return false\n return this.schema.getType(typeName) !== undefined\n }\n\n /**\n * Get the schema (if loaded)\n */\n getSchema(): GraphQLSchema | null {\n return this.schema\n }\n\n /**\n * Check if schema is loaded\n */\n isLoaded(): boolean {\n return this.schema !== null\n }\n\n /**\n * Clear cached schema\n */\n clearCache(): void {\n try {\n localStorage.removeItem(this.cacheKey)\n this.schema = null\n } catch {\n // Ignore storage errors\n }\n }\n\n /**\n * Unwrap GraphQL type to get base type info\n * Handles NonNull and List wrappers\n */\n private unwrapType(type: GraphQLInputType): {\n typeName: string\n isNonNull: boolean\n isList: boolean\n } {\n let typeName = ''\n let isNonNull = false\n let isList = false\n let current: any = type\n\n // Unwrap NonNull wrapper\n if (current.toString().endsWith('!')) {\n isNonNull = true\n current = 'ofType' in current ? current.ofType : current\n }\n\n // Check for List wrapper\n if (current.toString().startsWith('[')) {\n isList = true\n current = 'ofType' in current ? current.ofType : current\n }\n\n // Handle inner NonNull (e.g., [String!])\n if (current.toString().endsWith('!')) {\n current = 'ofType' in current ? current.ofType : current\n }\n\n // Get base type name\n typeName = current.name || current.toString().replace(/[[\\]!]/g, '')\n\n return { typeName, isNonNull, isList }\n }\n\n /**\n * Map GraphQL types to JavaScript types for URL handling\n */\n private mapGraphQLTypeToJS(graphqlType: string): JSType {\n const typeMap: Record<string, JSType> = {\n 'String': 'string',\n 'Int': 'number',\n 'Float': 'number',\n 'Boolean': 'boolean',\n 'ID': 'string'\n }\n\n return typeMap[graphqlType] || 'object'\n }\n\n /**\n * Load schema from localStorage cache\n */\n private loadFromCache(): SchemaCache | null {\n try {\n const cached = localStorage.getItem(this.cacheKey)\n if (!cached) return null\n\n const parsed = JSON.parse(cached) as SchemaCache\n\n // Validate cache version\n if (parsed.version !== this.schemaVersion) {\n return null\n }\n\n return parsed\n } catch (error) {\n console.warn('[Introspector] Failed to load cache:', error)\n return null\n }\n }\n\n /**\n * Save schema to localStorage cache\n */\n private saveToCache(schema: IntrospectionQuery): void {\n try {\n const cache: SchemaCache = {\n schema,\n timestamp: Date.now(),\n version: this.schemaVersion\n }\n\n localStorage.setItem(this.cacheKey, JSON.stringify(cache))\n } catch (error) {\n console.warn('[Introspector] Failed to save cache:', error)\n // Ignore storage errors (e.g., quota exceeded)\n }\n }\n}\n\n/**\n * Singleton introspector instance\n *\n * Use this instance throughout the application to share schema cache\n *\n * @example\n * import { introspector } from '@flamingo/ui-kit/hooks/state'\n *\n * // Initialize after auth\n * await introspector.fetchSchema(endpoint, { Authorization: token })\n *\n * // Get input type fields\n * const fields = introspector.getInputTypeFields('LogFilterInput')\n */\nexport const introspector = new GraphQLIntrospector()\n","/**\n * useApiParams Hook - REST API Integration for URL State Management\n *\n * Manual schema definition for REST APIs. Provides same URL sync functionality\n * as useQueryParams but without GraphQL dependency.\n *\n * @example\n * const { params, setParam } = useApiParams({\n * search: { type: 'string', default: '' },\n * page: { type: 'number', default: 1 },\n * tags: { type: 'array', default: [] }\n * })\n *\n * fetch(`/api/items?${new URLSearchParams(params)}`)\n *\n * // URL: /items?search=laptop&page=2&tags=electronics&tags=sale\n * // params: { search: 'laptop', page: 2, tags: ['electronics', 'sale'] }\n */\n\n'use client'\n\nimport { useRouter, useSearchParams } from '../../embed-shims/next-navigation'\nimport { useCallback, useMemo, useRef } from 'react'\nimport { FlattenedParam, shouldIncludeInUrl } from './flatten-schema'\nimport { JSType } from './graphql-parser'\nimport { coerceValue } from './url-converter'\n\n/**\n * Returns the previous reference if the JSON-serialized content of `value`\n * hasn't changed across renders. Internal helper used to shield consumers from\n * ref churn caused by:\n * - `useSearchParams()` returning a new `ReadonlyURLSearchParams` instance\n * on every render even when the URL is unchanged (Next.js behavior).\n * - Consumers passing the schema as a fresh object literal on every render.\n */\nfunction useContentStable<T>(value: T, key: string): T {\n const ref = useRef<{ value: T; key: string } | undefined>(undefined)\n if (ref.current && ref.current.key === key) return ref.current.value\n ref.current = { value, key }\n return value\n}\n\n/**\n * Reuses a previous array reference if its content (shallow string equality)\n * matches the freshly parsed array. Lets `params.tier` etc. stay\n * reference-stable across renders that don't actually change those values.\n */\nfunction reuseIfShallowEqual<T extends string | number | boolean>(\n prev: unknown,\n next: T[],\n): T[] {\n if (!Array.isArray(prev) || prev.length !== next.length) return next\n for (let i = 0; i < next.length; i++) {\n if (prev[i] !== next[i]) return next\n }\n return prev as T[]\n}\n\n/**\n * Type mapping from JSType to TypeScript types for OUTPUT (reading params)\n */\ntype OutputTypeMap = {\n string: string\n number: number\n boolean: boolean\n array: string[]\n object: Record<string, unknown>\n}\n\n/**\n * Type mapping from JSType to TypeScript types for INPUT (setting params)\n * More permissive to allow null/undefined in arrays which get filtered\n */\ntype InputTypeMap = {\n string: string | null | undefined\n number: number | null | undefined\n boolean: boolean | null | undefined\n array: (string | null | undefined)[]\n object: Record<string, unknown> | null | undefined\n}\n\n/**\n * Get the TypeScript type for OUTPUT (reading from params)\n */\ntype OutputTypeForJSType<T extends JSType> = OutputTypeMap[T]\n\n/**\n * Get the TypeScript type for INPUT (setting params)\n */\ntype InputTypeForJSType<T extends JSType> = InputTypeMap[T]\n\n/**\n * Get the default value type for a given JSType\n */\ntype DefaultValueForType<T extends JSType> =\n T extends 'array' ? string[] :\n T extends 'object' ? Record<string, unknown> :\n OutputTypeMap[T]\n\n/**\n * Parameter configuration for a single parameter\n */\nexport interface ParamConfig<T extends JSType = JSType> {\n /** JavaScript type for URL parameter */\n type: T\n /** Default value matching the type */\n default?: DefaultValueForType<T>\n /** Whether parameter is required */\n required?: boolean\n}\n\n/**\n * REST API parameter schema definition\n * Maps parameter names to their configuration\n */\nexport type ParamSchema = Record<string, ParamConfig>\n\n/**\n * Helper to create a typed param schema (preserves literal types)\n */\nexport function defineParamSchema<T extends ParamSchema>(schema: T): T {\n return schema\n}\n\n/**\n * Options for useApiParams hook\n */\nexport interface UseApiParamsOptions {\n /** Enable debug logging */\n debug?: boolean\n}\n\n/**\n * Infer the OUTPUT params type from a ParamSchema (for reading)\n * Maps each key in the schema to its corresponding TypeScript type\n *\n * @example\n * const schema = defineParamSchema({\n * search: { type: 'string', default: '' },\n * page: { type: 'number', default: 1 },\n * tags: { type: 'array', default: [] }\n * })\n * type Params = InferParamsFromSchema<typeof schema>\n * // { search: string; page: number; tags: string[] }\n */\nexport type InferParamsFromSchema<TSchema extends ParamSchema> = {\n [K in keyof TSchema]: TSchema[K]['type'] extends infer T\n ? T extends JSType\n ? OutputTypeForJSType<T>\n : never\n : never\n}\n\n/**\n * Infer the INPUT params type from a ParamSchema (for setting)\n * More permissive to allow null/undefined values\n */\nexport type InferInputParamsFromSchema<TSchema extends ParamSchema> = {\n [K in keyof TSchema]: TSchema[K]['type'] extends infer T\n ? T extends JSType\n ? InputTypeForJSType<T>\n : never\n : never\n}\n\n/**\n * Type for parameter values that can be set\n * Allows setting values that match the schema types or can be coerced to them\n */\nexport type ParamValue = \n | string \n | number \n | boolean \n | string[] \n | (string | null | undefined)[]\n | Record<string, unknown> \n | null \n | undefined\n\n/**\n * Return type for useApiParams hook with strict typing\n */\nexport interface UseApiParamsReturn<\n TSchema extends ParamSchema,\n TParams = InferParamsFromSchema<TSchema>\n> {\n /** Parsed parameters object with strict typing */\n params: TParams\n\n /** URLSearchParams for fetch/axios */\n urlSearchParams: URLSearchParams\n\n /** Set a single parameter with type-safe key and value */\n setParam: <K extends keyof TSchema & string>(\n key: K,\n value: InferInputParamsFromSchema<Pick<TSchema, K>>[K]\n ) => void\n\n /** Set multiple parameters at once */\n setParams: (updates: Partial<InferInputParamsFromSchema<TSchema>>) => void\n\n /** Clear specific parameters */\n clearParams: (keys: (keyof TSchema & string)[]) => void\n\n /** Reset all parameters (clear URL) */\n resetParams: () => void\n}\n\n/**\n * useApiParams - Manual URL state for REST APIs\n *\n * This hook:\n * 1. Reads URL search parameters\n * 2. Coerces to correct types based on schema\n * 3. Provides type-safe parameter updates\n * 4. Syncs changes to URL automatically\n *\n * @param schema - Parameter schema definition\n * @param options - Configuration options\n * @returns Hook API for managing URL state\n */\nexport function useApiParams<TSchema extends ParamSchema>(\n schema: TSchema,\n options: UseApiParamsOptions = {}\n): UseApiParamsReturn<TSchema> {\n const router = useRouter()\n const searchParamsLive = useSearchParams()\n const debug = options.debug || false\n\n // ───── Reference-stability layer ──────────────────────────────────────\n //\n // Goal: `params`, `params.<arrayField>`, and the setter callbacks must keep\n // the SAME reference across renders unless the URL or schema content\n // actually changes. Otherwise consumer `useMemo`/`useEffect` deps that\n // include `params.foo` invalidate on every parent re-render.\n //\n // Without this, every call site has to defensively `JSON.stringify` filter\n // arrays into a content-key — a known footgun. The stability is provided\n // here, once, instead of in 17 consumers.\n\n // 1. URL string is the canonical, value-stable representation of search params.\n const searchString = searchParamsLive.toString()\n\n // 2. Schema reference stabilized by content. Consumers commonly pass an\n // object literal each render, which would otherwise invalidate every memo.\n const schemaKey = useMemo(() => JSON.stringify(schema), [schema])\n const stableSchema = useContentStable(schema, schemaKey)\n\n // ──────────────────────────────────────────────────────────────────────\n\n // Convert schema to flattened format for reuse\n // biome-ignore lint/correctness/useExhaustiveDependencies: schemaKey is the content-stable key for `stableSchema`.\n const flattenedSchema = useMemo((): Record<string, FlattenedParam> => {\n const flattened: Record<string, FlattenedParam> = {}\n\n for (const [key, config] of Object.entries(stableSchema)) {\n flattened[key] = {\n urlParamName: key,\n graphqlPath: key,\n type: config.type,\n defaultValue: config.default,\n required: config.required,\n isArray: config.type === 'array'\n }\n }\n\n return flattened\n }, [schemaKey])\n\n // Parse URL parameters with type coercion. Reuse previous array refs when\n // their content is unchanged so `params.<arrayField>` stays stable across\n // renders that don't touch that specific field.\n const prevParamsRef = useRef<Record<string, unknown> | undefined>(undefined)\n // biome-ignore lint/correctness/useExhaustiveDependencies: `searchString` and `schemaKey` are content-stable representations of `searchParamsLive` and `stableSchema`.\n const params = useMemo((): InferParamsFromSchema<TSchema> => {\n const sp = new URLSearchParams(searchString)\n const result: Record<string, unknown> = {}\n const prev = prevParamsRef.current\n\n for (const [key, config] of Object.entries(stableSchema)) {\n // Read from URL\n const rawValue = config.type === 'array'\n ? sp.getAll(key)\n : sp.get(key)\n\n // Use value from URL or default\n let value: unknown\n if (rawValue && (Array.isArray(rawValue) ? rawValue.length > 0 : true)) {\n value = coerceValue(rawValue, config.type)\n } else {\n value = config.default\n }\n\n // Reuse previous reference when content matches — keeps array fields\n // reference-stable when an unrelated param changed.\n if (Array.isArray(value) && prev) {\n value = reuseIfShallowEqual(prev[key], value as (string | number | boolean)[])\n }\n\n result[key] = value\n }\n\n if (debug) {\n console.log('[useApiParams] Parsed params:', result)\n }\n\n prevParamsRef.current = result\n return result as InferParamsFromSchema<TSchema>\n }, [searchString, schemaKey, debug])\n\n // Helper: Add parameter value to URLSearchParams\n const addParamToSearchParams = useCallback((\n searchParams: URLSearchParams,\n key: string,\n value: ParamValue\n ): void => {\n if (value === undefined || value === '' || value === null) {\n return\n }\n\n if (Array.isArray(value)) {\n value.forEach(v => {\n if (v !== undefined && v !== '' && v !== null) {\n searchParams.append(key, String(v))\n }\n })\n } else if (typeof value === 'object') {\n // For objects, convert to JSON string\n searchParams.set(key, JSON.stringify(value))\n } else {\n searchParams.set(key, String(value))\n }\n }, [])\n\n // Get URLSearchParams for fetch/axios. Iterates `stableSchema` (not raw\n // `schema`) so consumers passing an inline schema literal don't invalidate\n // this memo on every render.\n // biome-ignore lint/correctness/useExhaustiveDependencies: `schemaKey` is the content-stable key for `stableSchema`.\n const urlSearchParams = useMemo((): URLSearchParams => {\n const newParams = new URLSearchParams()\n\n for (const key of Object.keys(stableSchema)) {\n const value = (params as Record<string, unknown>)[key]\n const paramConfig = flattenedSchema[key]\n\n // Skip if should not include\n if (!shouldIncludeInUrl(value, paramConfig)) {\n continue\n }\n\n addParamToSearchParams(newParams, key, value as ParamValue)\n }\n\n return newParams\n }, [params, schemaKey, flattenedSchema, addParamToSearchParams])\n\n // Update URL with new parameters (preserve other params not managed by this\n // hook). Depends only on value-stable inputs (`searchString`, `schemaKey`),\n // so the callback ref itself is stable across renders that don't change URL\n // or schema — important for consumers that put `setParam`/`setParams` in\n // `useEffect` deps.\n // biome-ignore lint/correctness/useExhaustiveDependencies: `searchString` and `schemaKey` are content-stable representations of `searchParamsLive` and `stableSchema`.\n const updateUrl = useCallback((newParams: URLSearchParams, keysToRemove: string[] = []) => {\n // Preserve all existing params, then override with new ones\n const finalParams = new URLSearchParams(searchString)\n\n // Remove keys that are explicitly marked for removal\n keysToRemove.forEach(key => {\n if (key in stableSchema) {\n finalParams.delete(key)\n }\n })\n\n // Remove keys that are being updated (from newParams)\n // This preserves other schema parameters that aren't being changed\n newParams.forEach((_, key) => {\n // Only remove keys that are in our schema\n if (key in stableSchema) {\n finalParams.delete(key)\n }\n })\n\n // Add all new values (including multiple values for array params)\n // Only add parameters that are in our schema to avoid duplicating external params\n newParams.forEach((value, key) => {\n // Only process keys that are in our schema\n if (key in stableSchema) {\n if (finalParams.has(key)) {\n // Key already exists (from array params), append\n finalParams.append(key, value)\n } else {\n // First value for this key, use set\n finalParams.set(key, value)\n }\n }\n })\n\n const url = finalParams.toString()\n ? `?${finalParams.toString()}`\n : window.location.pathname\n\n if (debug) {\n console.log('[useApiParams] Updating URL:', url)\n }\n\n // Use replace for shallow routing (no page reload, no history spam)\n router.replace(url, { scroll: false })\n }, [router, debug, searchString, schemaKey])\n\n // Helper to check if value is empty\n const isEmptyValue = (value: unknown): boolean => {\n if (value === undefined || value === null || value === '') {\n return true\n }\n if (Array.isArray(value)) {\n // Empty array or array with all empty/null/undefined values\n return value.length === 0 || value.every(v => v === undefined || v === null || v === '')\n }\n return false\n }\n\n // Set a single parameter\n // biome-ignore lint/correctness/useExhaustiveDependencies: `schemaKey` is the content-stable key for `stableSchema`.\n const setParam = useCallback(<K extends keyof TSchema & string>(\n key: K,\n value: InferInputParamsFromSchema<Pick<TSchema, K>>[K]\n ) => {\n const config = stableSchema[key]\n\n if (!config) {\n console.warn(`[useApiParams] Unknown parameter: ${key}`)\n return\n }\n\n const newParams = new URLSearchParams()\n\n if (isEmptyValue(value)) {\n updateUrl(newParams, [key])\n } else {\n addParamToSearchParams(newParams, key, value as ParamValue)\n updateUrl(newParams)\n }\n }, [schemaKey, updateUrl, addParamToSearchParams])\n\n // Set multiple parameters\n // biome-ignore lint/correctness/useExhaustiveDependencies: `schemaKey` is the content-stable key for `stableSchema`.\n const setParams = useCallback((\n updates: Partial<InferInputParamsFromSchema<TSchema>>\n ) => {\n const newParams = new URLSearchParams()\n const keysToRemove: string[] = []\n\n for (const [key, value] of Object.entries(updates)) {\n const config = stableSchema[key]\n\n if (!config) {\n console.warn(`[useApiParams] Unknown parameter: ${key}`)\n continue\n }\n\n if (isEmptyValue(value)) {\n keysToRemove.push(key)\n } else {\n addParamToSearchParams(newParams, key, value as ParamValue)\n }\n }\n\n updateUrl(newParams, keysToRemove)\n }, [schemaKey, updateUrl, addParamToSearchParams])\n\n // Clear specific parameters\n const clearParams = useCallback((keys: (keyof TSchema & string)[]) => {\n const newParams = new URLSearchParams()\n updateUrl(newParams, keys)\n }, [updateUrl])\n\n // Reset all parameters\n const resetParams = useCallback(() => {\n if (debug) {\n console.log('[useApiParams] Resetting params')\n }\n\n router.replace(window.location.pathname, { scroll: false })\n }, [router, debug])\n\n return {\n params,\n urlSearchParams,\n setParam,\n setParams,\n clearParams,\n resetParams\n }\n}\n\n/**\n * Helper: Create URLSearchParams from object\n *\n * Handles arrays as repeated parameters. Filters out undefined, and empty values.\n *\n * @param params - Parameters object\n * @returns URLSearchParams\n */\nexport function createSearchParams(params: Record<string, ParamValue>): URLSearchParams {\n const searchParams = new URLSearchParams()\n\n for (const [key, value] of Object.entries(params)) {\n if (value === undefined || value === '' || value === null) {\n continue\n }\n\n if (Array.isArray(value)) {\n value.forEach(v => {\n if (v !== undefined && v !== '' && v !== null) {\n searchParams.append(key, String(v))\n }\n })\n } else if (typeof value === 'object') {\n // For objects, convert to JSON string\n searchParams.set(key, JSON.stringify(value))\n } else {\n searchParams.set(key, String(value))\n }\n }\n\n return searchParams\n}","/**\n * Unified Cursor Pagination State Hook\n *\n * Manages all common cursor-based pagination state logic:\n * - URL state management with useApiParams\n * - Debounced search input\n * - hasLoadedBeyondFirst tracking\n * - Initial load detection\n * - Search change detection\n * - Pagination handlers (next, reset)\n *\n * This eliminates ~60-80 lines of boilerplate from each paginated component.\n *\n * @example\n * const {\n * searchInput, setSearchInput,\n * hasLoadedBeyondFirst,\n * handleNextPage, handleResetToFirstPage\n * } = useCursorPaginationState({\n * onInitialLoad: (search, cursor) => fetchDialogs(false, search, true, cursor),\n * onSearchChange: (search) => fetchDialogs(false, search)\n * })\n */\n\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport { useApiParams, type UseApiParamsReturn } from './use-api-params';\n\nexport interface UseCursorPaginationStateOptions {\n /**\n * Debounce delay for search input (default: 300ms)\n */\n debounceMs?: number\n\n /**\n * Callback for initial page load\n * Called once on mount with current search and cursor from URL\n */\n onInitialLoad: (search: string, cursor: string | null) => void | Promise<unknown>\n\n /**\n * Callback when search term changes (after debounce)\n * Called after URL is updated, cursor is already reset\n */\n onSearchChange: (search: string) => void | Promise<unknown>\n}\n\nconst urlSchema = {\n search: { type: 'string' as const, default: '' },\n cursor: { type: 'string' as const, default: '' },\n}\n\ntype ApiParamsReturn = UseApiParamsReturn<typeof urlSchema>\n\n/** Pagination params managed by this hook */\nexport type PaginationParams = ApiParamsReturn['params']\n\nexport interface CursorPaginationStateReturn {\n // Search\n searchInput: string\n setSearchInput: (value: string) => void\n\n // Pagination tracking\n hasLoadedBeyondFirst: boolean\n setHasLoadedBeyondFirst: (value: boolean) => void\n\n // Handlers for useTablePagination\n handleNextPage: (endCursor: string, fetchFn: () => Promise<unknown>) => Promise<void>\n handleResetToFirstPage: (fetchFn: () => Promise<unknown>) => Promise<void>\n\n // URL params access (for advanced use cases)\n params: PaginationParams\n setParam: ApiParamsReturn['setParam']\n setParams: ApiParamsReturn['setParams']\n}\n\nexport function useCursorPaginationState(\n options: UseCursorPaginationStateOptions\n): CursorPaginationStateReturn {\n const {\n onInitialLoad,\n onSearchChange,\n } = options\n\n const { params, setParam, setParams } = useApiParams(urlSchema)\n\n // Local search input with debounce\n const [searchInput, setSearchInput] = useState(params.search || '')\n\n // Pagination tracking\n const [hasLoadedBeyondFirst, setHasLoadedBeyondFirst] = useState(false)\n // Use a counter instead of boolean to ensure effects see the latest state\n const [initialLoadCount, setInitialLoadCount] = useState(0)\n // Initialize to null to distinguish \"never set\" from \"set to empty string\"\n const lastSearchRef = useRef<string | null>(null)\n // Track if we're syncing from URL to prevent loops\n const isSyncingFromUrl = useRef(false)\n // Track if initial load is in progress to block ALL other effects\n const isInitialLoadInProgress = useRef(true)\n\n // Sync local input with URL param (for tab switches that clear params)\n // Only sync if the URL changed externally (not from our own debounce update)\n useEffect(() => {\n // Block during initial load\n if (isInitialLoadInProgress.current) return\n\n const urlSearch = params.search || ''\n // Only sync if URL differs from current input\n if (urlSearch !== searchInput) {\n isSyncingFromUrl.current = true\n setSearchInput(urlSearch)\n // Reset the flag after a tick to allow normal operation\n setTimeout(() => { isSyncingFromUrl.current = false }, 0)\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [params.search, initialLoadCount]) // Add initialLoadCount to re-run after initial load\n\n // Sync debounced search to URL, reset cursor when search changes\n useEffect(() => {\n // Block during initial load\n if (isInitialLoadInProgress.current) return\n // Skip if we're syncing from URL to prevent loops\n if (isSyncingFromUrl.current) return\n\n if (searchInput !== params.search) {\n setParams({\n search: searchInput,\n cursor: '' // Reset cursor when search changes\n })\n }\n }, [searchInput, params.search, setParams, initialLoadCount])\n\n // Initial load effect - runs once and blocks all other effects until complete\n useEffect(() => {\n if (initialLoadCount === 0) {\n const cursor = params.cursor || null\n const search = params.search || ''\n\n // Set all refs BEFORE calling onInitialLoad\n lastSearchRef.current = search\n\n // If we have a cursor in URL (page refresh), we're beyond first page\n if (cursor) {\n setHasLoadedBeyondFirst(true)\n }\n\n // Call the initial load and wait for it to complete before\n // marking initial load as done (handles async onInitialLoad)\n Promise.resolve(onInitialLoad(search, cursor)).finally(() => {\n isInitialLoadInProgress.current = false\n setInitialLoadCount(1)\n })\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [])\n\n // Search change detection - only after initial load is fully complete\n useEffect(() => {\n // Block during initial load\n if (isInitialLoadInProgress.current) return\n if (initialLoadCount === 0) return\n\n const currentSearch = params.search || ''\n // Only trigger search change if:\n // 1. lastSearchRef has been set (not null - means initial load happened)\n // 2. The search actually changed\n if (lastSearchRef.current !== null && currentSearch !== lastSearchRef.current) {\n lastSearchRef.current = currentSearch\n setHasLoadedBeyondFirst(false)\n onSearchChange(currentSearch)\n }\n }, [params.search, onSearchChange, initialLoadCount])\n\n // Pagination handlers\n const handleNextPage = useCallback(\n async (endCursor: string, fetchFn: () => Promise<unknown>) => {\n setParam('cursor', endCursor)\n await fetchFn()\n setHasLoadedBeyondFirst(true)\n },\n [setParam]\n )\n\n const handleResetToFirstPage = useCallback(\n async (fetchFn: () => Promise<unknown>) => {\n setParam('cursor', '')\n await fetchFn()\n setHasLoadedBeyondFirst(false)\n },\n [setParam]\n )\n\n return {\n searchInput,\n setSearchInput,\n hasLoadedBeyondFirst,\n setHasLoadedBeyondFirst,\n handleNextPage,\n handleResetToFirstPage,\n params,\n setParam,\n setParams,\n }\n}\n","import { useEffect, useMemo, useState } from 'react'\n\nimport type { NatsClient, NatsClientOptions, NatsStatus } from '../../nats'\nimport { createNatsClient } from '../../nats'\n\nexport interface UseNatsClientOptions {\n /**\n * When true, connect on mount and close on unmount.\n */\n autoConnect?: boolean\n}\n\nexport interface UseNatsClientResult {\n client: NatsClient | null\n status: NatsStatus\n isConnected: boolean\n lastError: unknown\n}\n\n/**\n * React hook for managing a shared NATS WebSocket connection lifecycle.\n *\n * Important: pass a memoized `clientOptions` (e.g. via `useMemo`) to avoid\n * reconnecting on every render.\n */\nexport function useNatsClient(\n clientOptions: NatsClientOptions | null,\n options: UseNatsClientOptions = {},\n): UseNatsClientResult {\n const { autoConnect = true } = options\n\n const client = useMemo(() => {\n if (!clientOptions) return null\n return createNatsClient(clientOptions)\n }, [clientOptions])\n\n const [status, setStatus] = useState<NatsStatus>('disconnected')\n const [lastError, setLastError] = useState<unknown>(null)\n\n useEffect(() => {\n if (!client) {\n setStatus('disconnected')\n setLastError(null)\n return\n }\n\n const off = client.onStatus((event) => {\n setStatus(event.status)\n if (event.status === 'error') setLastError(event.data)\n })\n\n if (autoConnect) {\n client.connect().catch((e) => {\n setLastError(e)\n setStatus('error')\n })\n }\n\n return () => {\n off()\n client.close().catch(() => {\n // ignore\n })\n }\n }, [client, autoConnect])\n\n return {\n client,\n status,\n isConnected: status === 'connected',\n lastError,\n }\n}\n","/**\n * useNearViewport — module-level shared IntersectionObserver in hook form.\n *\n * Single IO instance per `rootMargin` value, shared across every component\n * that mounts the hook. Reduces overhead vs. one IO per component on\n * grid/list pages where many subscribers observe the viewport with the same\n * margin. Promoted from the inline singleton at\n * `multi-platform-hub/components/shared/video-bites-display.tsx:21-43`,\n * which is the only IO pattern in either repo today.\n *\n * Usage:\n * ```tsx\n * function MyCard() {\n * const { ref, isNear } = useNearViewport('500px');\n * return <div ref={ref}>{isNear ? <HeavyChild /> : <Placeholder />}</div>;\n * }\n * ```\n *\n * StrictMode safety: cleanup uses an identity check on the registered\n * callback so React's dev double-mount (mount → cleanup → re-mount) does\n * not drop the second mount's freshly-set subscription. The IO callback\n * also checks `subscribers.get(target)` before invoking so a fire that\n * races with unmount cannot crash on a torn-down component.\n *\n * The hook fires once — on first intersection it sets `isNear=true` and\n * unobserves the element. Callers that need re-observation should\n * unmount and remount (or fork the hook for two-way behavior).\n */\n\nimport { useEffect, useRef, useState, useCallback } from 'react';\n\n// Per-rootMargin IO map. Multiple call sites with different margins each\n// get their own singleton observer.\nconst observers = new Map<string, IntersectionObserver>();\nconst subscribers = new WeakMap<Element, () => void>();\n\nfunction getObserverFor(rootMargin: string): IntersectionObserver {\n const existing = observers.get(rootMargin);\n if (existing) return existing;\n\n const io = new IntersectionObserver(\n (entries) => {\n entries.forEach((entry) => {\n if (!entry.isIntersecting) return;\n // Race-safe: re-read the callback at fire time. A late IO firing\n // after cleanup must not invoke a stale callback.\n const cb = subscribers.get(entry.target);\n if (cb) {\n cb();\n io.unobserve(entry.target);\n subscribers.delete(entry.target);\n }\n });\n },\n { rootMargin }\n );\n observers.set(rootMargin, io);\n return io;\n}\n\nexport interface UseNearViewportResult<T extends Element = HTMLElement> {\n /** Ref to attach to the element you want to gate on visibility. */\n ref: (node: T | null) => void;\n /** Flips to `true` once the element enters within `rootMargin` of the viewport. Never flips back. */\n isNear: boolean;\n}\n\n/**\n * @param rootMargin Margin around the viewport (CSS-style string).\n * '500px' = element starts mounting 500px before scroll-in.\n * '1000px' = a full viewport's worth of lookahead.\n * '0px' = strict on-screen detection.\n */\nexport function useNearViewport<T extends Element = HTMLElement>(\n rootMargin: string = '500px'\n): UseNearViewportResult<T> {\n const [isNear, setIsNear] = useState(false);\n const elRef = useRef<T | null>(null);\n\n // Subscribe/unsubscribe on element change.\n const ref = useCallback(\n (node: T | null) => {\n const prev = elRef.current;\n\n // Unsubscribe previous, if any. Identity-check the callback so a\n // StrictMode re-mount that has already re-registered keeps its sub.\n if (prev) {\n const stillOurs = subscribers.get(prev);\n if (stillOurs) {\n subscribers.delete(prev);\n observers.get(rootMargin)?.unobserve(prev);\n }\n }\n\n elRef.current = node;\n if (!node) return;\n\n const cb = () => setIsNear(true);\n subscribers.set(node, cb);\n getObserverFor(rootMargin).observe(node);\n },\n [rootMargin]\n );\n\n // Unsubscribe on unmount. Identity check guards the StrictMode race.\n useEffect(() => {\n return () => {\n const el = elRef.current;\n if (!el) return;\n if (subscribers.get(el)) {\n subscribers.delete(el);\n observers.get(rootMargin)?.unobserve(el);\n }\n };\n }, [rootMargin]);\n\n return { ref, isNear };\n}\n","'use client'\n\n/**\n * Access Code Integration Hook\n *\n * React-side wrapper around the pure `access-code-client` utilities.\n * Lives in `hooks/` (client bundle) so the `createContext()` call in\n * `endpoints-runtime-context` doesn't get pulled into the server-safe\n * `utils/index` bundle.\n *\n * The pure standalone functions (`validateAccessCode`,\n * `consumeAccessCode`, `validateAndConsumeAccessCode`) remain importable\n * from `@flamingo-stack/openframe-frontend-core/utils` — they take the\n * endpoints object as an argument. This hook binds those endpoints from\n * `EndpointsRuntimeContext` so React callers don't have to plumb URLs.\n */\n\nimport React from 'react'\n\nimport { useRequiredEndpointsRuntime } from '../contexts/endpoints-runtime-context'\nimport {\n validateAccessCode,\n consumeAccessCode,\n validateAndConsumeAccessCode,\n} from '../utils/access-code-client'\n\n/**\n * Resolves access-code endpoints from `EndpointsRuntimeContext` (throws\n * if no provider is mounted) and exposes loading-state-aware wrappers\n * around the standalone helpers in `utils/access-code-client`.\n *\n * @returns the following fields. The `validate` / `consume` /\n * `validateAndConsume` functions and the returned object identity are\n * NOT memoized — they're re-created each render. Wrap with\n * `useCallback` / `useMemo` at the call site if downstream effect\n * dep arrays depend on stable identities.\n * - `validate(email, code)`: validates only.\n * - `consume(email, code)`: consumes only.\n * - `validateAndConsume(email, code)`: one-step validate-then-consume.\n * - `isValidating: boolean`: a validate call is in flight.\n * - `isConsuming: boolean`: a consume call is in flight.\n * - `isProcessing: boolean`: convenience — `isValidating || isConsuming`.\n * Use this for a single \"in-flight\" indicator on UI affordances that\n * should disable during both phases.\n *\n * @example\n * const { validate, consume, isProcessing } = useAccessCodeIntegration();\n *\n * const handleRegistration = async (formData) => {\n * const validation = await validate(formData.email, formData.accessCode);\n * if (!validation.valid) {\n * setError(validation.message);\n * return;\n * }\n *\n * // Process registration...\n * const registrationResult = await registerUser(formData);\n *\n * if (registrationResult.success) {\n * await consume(formData.email, formData.accessCode);\n * }\n * };\n */\nexport function useAccessCodeIntegration() {\n const runtime = useRequiredEndpointsRuntime()\n const endpoints = runtime.accessCode\n const [isValidating, setIsValidating] = React.useState(false)\n const [isConsuming, setIsConsuming] = React.useState(false)\n\n const validate = async (email: string, code: string) => {\n setIsValidating(true)\n try {\n return await validateAccessCode(email, code, endpoints)\n } finally {\n setIsValidating(false)\n }\n }\n\n const consume = async (email: string, code: string) => {\n setIsConsuming(true)\n try {\n return await consumeAccessCode(email, code, endpoints)\n } finally {\n setIsConsuming(false)\n }\n }\n\n const validateAndConsume = async (email: string, code: string) => {\n setIsValidating(true)\n setIsConsuming(true)\n try {\n return await validateAndConsumeAccessCode(email, code, endpoints)\n } finally {\n setIsValidating(false)\n setIsConsuming(false)\n }\n }\n\n return {\n validate,\n consume,\n validateAndConsume,\n isValidating,\n isConsuming,\n isProcessing: isValidating || isConsuming,\n }\n}\n","/**\n * Access Code Client Utilities — pure standalone functions.\n *\n * Endpoint paths are NOT hardcoded — every function takes an\n * `endpoints` argument. The React-side wrapper that binds them from\n * `EndpointsRuntimeContext` lives separately at\n * `hooks/use-access-code-integration.ts` (`useAccessCodeIntegration`).\n *\n * Keep this file **free of React imports** — it lives in the\n * server-safe `utils/index` tsup bundle. Any module-top-level call\n * into `createContext()` (which the runtime context file does) would\n * be pulled into the server bundle and crash SSR with\n * `createContext is not a function`.\n */\n\nimport {\n AccessCodeValidation,\n AccessCodeValidationResponse,\n AccessCodeConsumptionResponse\n} from '../types/access-code-cohorts';\n\n/** Endpoints required by the standalone client utilities. The\n * `useAccessCodeIntegration` hook (in `hooks/`) resolves these from\n * `EndpointsRuntimeContext.accessCode` automatically. */\nexport interface AccessCodeEndpoints {\n validateUrl: string\n consumeUrl: string\n}\n\n/**\n * Validate an access code for a given email\n *\n * @param email - User's email address\n * @param code - Access code to validate\n * @returns Promise with validation result\n *\n * @example\n * const result = await validateAccessCode('user@example.com', 'ABC123XY');\n * if (result.valid) {\n * // Allow user to proceed with registration\n * console.log(`Welcome to ${result.cohort_name}!`);\n * } else {\n * // Show error message\n * console.error(result.message);\n * }\n */\nexport async function validateAccessCode(\n email: string,\n code: string,\n endpoints: AccessCodeEndpoints,\n): Promise<AccessCodeValidationResponse> {\n try {\n const response = await fetch(endpoints.validateUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ email, code } as AccessCodeValidation),\n });\n\n if (!response.ok) {\n const error = await response.json().catch(() => ({}));\n throw new Error(error.error || 'Validation request failed');\n }\n\n return await response.json() as AccessCodeValidationResponse;\n } catch (error) {\n return {\n valid: false,\n message: error instanceof Error ? error.message : 'Validation failed',\n };\n }\n}\n\n/**\n * Consume an access code after successful registration\n *\n * Call this ONLY after the user has successfully completed registration.\n * This marks the code as used and prevents further usage.\n *\n * @param email - User's email address\n * @param code - Access code to consume\n * @returns Promise with consumption result\n *\n * @example\n * // After successful registration\n * const result = await consumeAccessCode('user@example.com', 'ABC123XY');\n * if (result.consumed) {\n * console.log('Access code consumed successfully');\n * } else {\n * console.warn('Failed to consume access code:', result.message);\n * }\n */\nexport async function consumeAccessCode(\n email: string,\n code: string,\n endpoints: AccessCodeEndpoints,\n): Promise<AccessCodeConsumptionResponse> {\n try {\n const response = await fetch(endpoints.consumeUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ email, code } as AccessCodeValidation),\n });\n\n if (!response.ok) {\n const error = await response.json().catch(() => ({}));\n throw new Error(error.error || 'Consumption request failed');\n }\n\n return await response.json() as AccessCodeConsumptionResponse;\n } catch (error) {\n return {\n success: false,\n consumed: false,\n message: error instanceof Error ? error.message : 'Consumption failed',\n };\n }\n}\n\n/**\n * Complete access code flow: validate then consume\n *\n * This is a convenience function that validates an access code and,\n * if valid, immediately consumes it. Use this when you want to\n * validate and consume in one step during registration.\n *\n * @param email - User's email address\n * @param code - Access code to validate and consume\n * @returns Promise with validation and consumption results\n *\n * @example\n * const result = await validateAndConsumeAccessCode('user@example.com', 'ABC123XY');\n * if (result.valid && result.consumed) {\n * // Registration successful\n * console.log(`Welcome to ${result.cohort_name}!`);\n * } else {\n * console.error(result.message);\n * }\n */\nexport async function validateAndConsumeAccessCode(\n email: string,\n code: string,\n endpoints: AccessCodeEndpoints,\n): Promise<AccessCodeValidationResponse & { consumed?: boolean }> {\n // First validate\n const validation = await validateAccessCode(email, code, endpoints);\n\n if (!validation.valid) {\n return validation;\n }\n\n // If valid, consume the code\n const consumption = await consumeAccessCode(email, code, endpoints);\n\n return {\n ...validation,\n consumed: consumption.consumed,\n message: consumption.consumed\n ? `Access granted for ${validation.cohort_name}`\n : consumption.message || validation.message,\n };\n}\n\n// `useAccessCodeIntegration` (the React-side wrapper) lives in\n// `hooks/use-access-code-integration.ts`. It binds the endpoints from\n// `EndpointsRuntimeContext` so React callers don't have to plumb URLs.\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","/**\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 { useEffect } from 'react'\nimport { scrollElementIntoView } from '../utils/scroll-into-view'\nimport { normalizeHashFragment } from '../utils/same-page-hash-nav'\n\n/** ~1s at 60fps — long enough to outlast Radix accordion expand + SWR\n * mount, short enough that a missed anchor doesn't hang the page. */\nconst MAX_POLL_FRAMES = 60\n\nexport interface UseScrollToHashOptions {\n /** Pixels to subtract for sticky chrome. */\n headerOffset?: number\n}\n\n/**\n * Scroll the page to `window.location.hash` once `readyDep` resolves\n * to a truthy value. Polls via rAF for ~1s so lazy-mounted rows (Radix\n * accordion, SWR fetch) have time to render. Re-runs on `readyDep`\n * reference change AND on `hashchange` (browser back/forward + the\n * synthetic event `navigateSamePageHash` dispatches).\n *\n * Skipped when `readyDep == null || readyDep === false`. Default\n * `true` makes the hook run on mount for pages whose target is in the\n * initial SSR render.\n */\nexport function useScrollToHash(\n readyDep: unknown = true,\n options?: UseScrollToHashOptions,\n): void {\n const headerOffset = options?.headerOffset ?? 0\n useEffect(() => {\n if (typeof window === 'undefined') return\n if (readyDep === null || readyDep === false) return\n let rafId: number | null = null\n const cancelPoll = () => {\n if (rafId !== null) {\n cancelAnimationFrame(rafId)\n rafId = null\n }\n }\n const tryScrollToHash = () => {\n // `normalizeHashFragment` heals a malformed multi-fragment hash\n // so `getElementById` resolves on deep-link entries that bypass\n // `navigateSamePageHash`'s own normalize.\n const hash = normalizeHashFragment(window.location.hash).slice(1)\n if (!hash) return\n // Cancel any in-flight poll from a prior invocation so two\n // concurrent ticks can't both call scrollElementIntoView.\n cancelPoll()\n let frames = 0\n const tick = () => {\n const el = document.getElementById(hash)\n if (el) {\n rafId = null\n scrollElementIntoView(el, { headerOffset })\n return\n }\n if (frames++ < MAX_POLL_FRAMES) {\n rafId = requestAnimationFrame(tick)\n } else {\n rafId = null\n }\n }\n tick()\n }\n tryScrollToHash()\n window.addEventListener('hashchange', tryScrollToHash)\n return () => {\n window.removeEventListener('hashchange', tryScrollToHash)\n cancelPoll()\n }\n }, [readyDep, headerOffset])\n}\n","/**\n * `scrollElementIntoView` — canonical \"scroll an element to the top of the\n * viewport, account for sticky chrome, survive layout shifts\" helper.\n *\n * One shared implementation so every caller (the ticket drawer expand, the\n * hub's `useUnifiedNav` / `use-nav-link` hash scroll, doc-tree, delivery\n * `?focus=`, sticky-section-nav, …) inherits the SAME cancellation-proof\n * motion.\n *\n * WHY A SELF-DRIVEN rAF TWEEN INSTEAD OF `window.scrollTo({behavior:'smooth'})`:\n * the native smooth scroll is CANCELLABLE, and in real pages it gets cancelled\n * constantly:\n *\n * - Browser SCROLL ANCHORING: when content is inserted/removed above or\n * around the target (a collapsible drawer expanding, an async image\n * loading, a list re-rendering) the browser issues a synchronous scrollTop\n * correction to keep the anchored element stable. Per CSSOM-View \"perform a\n * scroll\" step 1 (\"abort any ongoing smooth scroll\"), that correction\n * ABORTS an in-flight native smooth scroll — so it lands as an instant jump.\n * Anchoring is suppressed when the scroll offset is 0, which is exactly why\n * a native smooth scroll appears to work the FIRST time (page at top) and\n * jumps on every repeat (page already scrolled). This was a multi-day\n * \"smooth only works once\" bug on the /tickets drawer.\n * - A second programmatic scroll on the same frame, or a `focus()` without\n * `{preventScroll:true}`, cancels it the same way.\n *\n * A tween that re-asserts the position with INSTANT writes every frame is\n * immune: there is no \"ongoing native smooth scroll\" for anchoring/focus to\n * abort, and any correction that lands between our frames is overwritten on the\n * next frame. We also RECOMPUTE the target each frame, so an element whose\n * final position is still settling (drawer still expanding, images loading)\n * is tracked to its resting place instead of animating to a stale pixel.\n *\n * Honors `prefers-reduced-motion` (jumps instantly) and cancels on genuine user\n * scroll intent (wheel / touch) so we never fight the user.\n *\n * WINDOW *OR* A SCROLLABLE ANCESTOR: the helper is not hard-wired to the window\n * scroller. It walks up from the target to the nearest ancestor that is an\n * actual scroll container (`overflow-y: auto | scroll | overlay` AND\n * `scrollHeight > clientHeight`) and drives THAT element; only when none exists\n * does it fall back to `window`. This is what makes it work inside app shells\n * that put page content in a fixed-height `<main class=\"overflow-y-auto\">`\n * (e.g. OpenFrame's `AppLayout`) where the document/window never scrolls — the\n * old window-only version was a silent no-op there. Note `overflow: clip` /\n * `hidden` are deliberately NOT treated as scroll containers, so a list wrapper\n * that uses `overflow-clip` only to round its corners still bubbles the scroll\n * up to the real container (matches the `<HelpCenterCard>` list intent).\n */\n\nexport interface ScrollElementIntoViewOptions {\n /** Pixels to subtract from the target element's `top` so it lands BELOW\n * sticky chrome. Defaults to 0. Pass `96` for the standard hub header. */\n headerOffset?: number\n /** `'smooth'` (default) runs the self-driven tween; `'instant'` / `'auto'`\n * jump in one synchronous write (deep-link land, programmatic focus moves). */\n behavior?: ScrollBehavior\n /** Optional adjustment applied to the computed pixel target each frame. The\n * callback receives the \"raw\" Y (`element.top + scrollY - headerOffset`) and\n * returns the FINAL target. Use when the caller knows about a layout shift\n * (e.g. a sibling drawer collapsing) the geometry can't yet reflect. */\n adjustTargetY?: (rawTargetY: number) => number\n /** Tween duration in ms (smooth only). Default 320. */\n durationMs?: number\n}\n\n/** Module-level handle to the in-flight tween so a new call (or a user\n * gesture) cancels the previous one — only ever one page-scroll animation at\n * a time. */\nlet activeRaf = 0\nlet teardownActive: (() => void) | null = null\n\nfunction cancelActiveScroll(): void {\n if (activeRaf) {\n cancelAnimationFrame(activeRaf)\n activeRaf = 0\n }\n if (teardownActive) {\n teardownActive()\n teardownActive = null\n }\n}\n\nconst easeOutCubic = (t: number): number => 1 - Math.pow(1 - t, 3)\n\n/** Nearest ancestor that is a *real* scroll container, or `null` when the\n * window/document is the scroller. Only `auto | scroll | overlay` count —\n * `clip` / `hidden` are intentionally excluded (a wrapper using `overflow-clip`\n * purely to round corners must let the scroll bubble to the page). */\nfunction getScrollableAncestor(el: HTMLElement): HTMLElement | null {\n for (let node = el.parentElement; node; node = node.parentElement) {\n const overflowY = getComputedStyle(node).overflowY\n if (\n (overflowY === 'auto' || overflowY === 'scroll' || overflowY === 'overlay') &&\n node.scrollHeight > node.clientHeight\n ) {\n return node\n }\n }\n return null\n}\n\n/**\n * Scroll the page so `target` lands at the top of the viewport (below sticky\n * chrome via `headerOffset`). SSR-safe; `null`/`undefined` target is a no-op so\n * callers can pass refs without defensive branching.\n */\nexport function scrollElementIntoView(\n target: HTMLElement | null | undefined,\n options: ScrollElementIntoViewOptions = {},\n): void {\n if (typeof window === 'undefined' || !target) return\n const { headerOffset = 0, behavior = 'smooth', adjustTargetY, durationMs = 320 } = options\n\n // Pick the scroller ONCE: a fixed-height `<main overflow-y-auto>` shell scrolls\n // the element, a plain document scrolls the window. The choice can't change\n // mid-tween, so resolve it up front and route every read/write through it.\n const container = getScrollableAncestor(target)\n const readCurrent = (): number => (container ? container.scrollTop : window.scrollY)\n const writeTo = (y: number): void => {\n if (container) container.scrollTop = y\n else window.scrollTo(0, y)\n }\n\n // Target is recomputed every frame: the row's absolute position can move as\n // the page reflows (a sibling drawer collapsing) and the reachable max grows\n // as the just-opened drawer expands. Clamp to the LIVE max each frame.\n const computeTarget = (): number => {\n const raw = container\n ? container.scrollTop +\n (target.getBoundingClientRect().top - container.getBoundingClientRect().top) -\n headerOffset\n : target.getBoundingClientRect().top + window.scrollY - headerOffset\n const adjusted = adjustTargetY ? adjustTargetY(raw) : raw\n const maxScroll = container\n ? Math.max(0, container.scrollHeight - container.clientHeight)\n : Math.max(0, document.documentElement.scrollHeight - window.innerHeight)\n return Math.min(Math.max(0, adjusted), maxScroll)\n }\n\n // Any prior animation loses — one page scroll at a time.\n cancelActiveScroll()\n\n const prefersReduced =\n typeof window.matchMedia === 'function' &&\n window.matchMedia('(prefers-reduced-motion: reduce)').matches\n\n // Instant paths: a single synchronous write. No tween, no anchoring race.\n if (behavior === 'instant' || behavior === 'auto' || prefersReduced) {\n writeTo(computeTarget())\n return\n }\n\n // Smooth: self-driven tween with instant per-frame writes (anchoring-proof).\n let startY: number | null = null\n let startTime = 0\n\n // Bail the moment the user takes over with a real scroll gesture — we must\n // never fight them. (Not keydown: the ticket composer auto-focuses on open,\n // and typing there should not abort the scroll.)\n const onUserGesture = () => cancelActiveScroll()\n window.addEventListener('wheel', onUserGesture, { passive: true })\n window.addEventListener('touchmove', onUserGesture, { passive: true })\n teardownActive = () => {\n window.removeEventListener('wheel', onUserGesture)\n window.removeEventListener('touchmove', onUserGesture)\n }\n\n const step = (now: number) => {\n if (startY === null) {\n startY = readCurrent()\n startTime = now\n }\n const targetY = computeTarget()\n const t = Math.min(1, (now - startTime) / durationMs)\n const y = startY + (targetY - startY) * easeOutCubic(t)\n writeTo(y)\n if (t < 1) {\n activeRaf = requestAnimationFrame(step)\n } else {\n // Final exact write in case easing left a sub-pixel gap, then teardown.\n writeTo(computeTarget())\n activeRaf = 0\n if (teardownActive) {\n teardownActive()\n teardownActive = null\n }\n }\n }\n activeRaf = requestAnimationFrame(step)\n}\n","import { scrollElementIntoView } from './scroll-into-view'\n\n/** Pages with a section-nav STRIP on top of the global hub header\n * (dev-center roadmap/delivery/tickets, FAQ category-pill nav).\n * Anchor lands BELOW both layers. */\nexport const STICKY_HEADER_OFFSET_PX = 96\n\n/** Pages with only the global hub header (docs, blog, vendor detail).\n * Anchor lands BELOW the header bar. */\nexport const HUB_HEADER_OFFSET_PX = 80\n\n/**\n * Take only the FIRST hash segment from a fragment that may contain extra\n * `#` characters. `'' → ''`, `'#a' → '#a'`, `'#a#b' → '#a'`.\n *\n * No real DOM id contains `#`, so a multi-fragment hash is always a bug at\n * the composer site; `navigateSamePageHash` + `useScrollToHash` both call\n * this so URL bar and `getElementById` stay in sync.\n */\nexport function normalizeHashFragment(hash: string): string {\n if (!hash) return ''\n const second = hash.indexOf('#', 1)\n return second < 0 ? hash : hash.slice(0, second)\n}\n\nexport interface NavigateSamePageHashOptions {\n /** Pixels to subtract for sticky chrome. */\n headerOffset?: number\n /** `'push'` (default) — new history entry; `'replace'` — overwrite\n * current entry (use for TOC-style in-page navigators). */\n history?: 'push' | 'replace'\n}\n\n/**\n * Same-page hash navigation primitive: pushState + synthetic `hashchange`\n * + anchoring-proof smooth scroll. Replaces `router.push` for hash CTAs\n * (Next.js suppresses smooth-scroll during navigation; `router.push` on\n * an exact-URL match is a no-op). Returns `true` when the helper claimed\n * the nav (same pathname + search); `false` for cross-page targets so\n * callers fall through to `router.push`.\n *\n * `target` accepts an origin-stripped path (`/x#anchor`) or a bare hash\n * (`#anchor`); bare-hash callers don't need to reconstruct `pathname +\n * search` themselves.\n */\nexport function navigateSamePageHash(\n target: string,\n options: NavigateSamePageHashOptions = {},\n): boolean {\n if (typeof window === 'undefined') return false\n const { headerOffset = 0, history: historyMode = 'push' } = options\n const normalizedTarget =\n target.startsWith('#')\n ? window.location.pathname + window.location.search + target\n : target\n // `new URL(absoluteUrl, base)` ignores `base` per RFC 3986; an absolute\n // cross-origin target sharing pathname/search would otherwise pass the\n // check below and trip pushState's same-origin enforcement. Parse with\n // an explicit base so malformed inputs cleanly fall through.\n let url: URL\n try {\n url = new URL(normalizedTarget, window.location.href)\n } catch {\n return false\n }\n if (\n url.origin !== window.location.origin ||\n url.pathname !== window.location.pathname ||\n url.search !== window.location.search\n ) {\n return false\n }\n const current = window.location.pathname + window.location.search + window.location.hash\n // Heal a malformed multi-fragment hash so the URL bar is clean and\n // `getElementById` resolves. Dev-warn fingers the upstream composer.\n const normalizedHash = normalizeHashFragment(url.hash)\n if (process.env.NODE_ENV === 'development' && normalizedHash !== url.hash) {\n // eslint-disable-next-line no-console\n console.warn(\n `[navigateSamePageHash] malformed fragment \"${url.hash}\" → normalizing to \"${normalizedHash}\". Fix the upstream composer.`,\n )\n }\n const next = url.pathname + url.search + normalizedHash\n const id = normalizedHash && normalizedHash !== '#' ? normalizedHash.slice(1) : ''\n // Hash-less targets are only ours on an EXACT URL re-click.\n if (!id && next !== current) return false\n if (next !== current) {\n const oldURL = window.location.href\n if (historyMode === 'replace') {\n window.history.replaceState(null, '', next)\n } else {\n window.history.pushState(null, '', next)\n }\n // Synthetic `hashchange` — `pushState` doesn't fire it (HTML spec),\n // so URL-hash-bound listeners (FAQ auto-expand, etc.) wouldn't react.\n window.dispatchEvent(new HashChangeEvent('hashchange', {\n oldURL,\n newURL: window.location.href,\n }))\n }\n const el = id ? document.getElementById(id) : null\n if (id && !el && process.env.NODE_ENV === 'development') {\n // eslint-disable-next-line no-console\n console.warn(\n `[navigateSamePageHash] anchor \"#${id}\" not found — scrolling to top.`,\n )\n }\n // Missing anchor → tween to page top. `documentElement` is at 0 by\n // definition, so one tween covers both branches.\n scrollElementIntoView(el ?? document.documentElement, {\n behavior: 'smooth',\n headerOffset,\n })\n return true\n}\n","'use client'\n\nimport { useCallback, useRef } from 'react'\nimport {\n HONEYPOT_FIELD,\n ELAPSED_MS_FIELD,\n type HumanitySignals,\n} from '../utils/humanity-signals'\n\n/**\n * useHumanitySignals — client primitive backing the invisible bot-protection\n * layer. Owns the honeypot <input> ref + a mount timestamp, and exposes the\n * props to render the field plus a getter that produces the wire object to\n * merge into a form's POST body. Reused by every public form (contact,\n * waitlist, ticket, review) so the signal shape lives in exactly one place.\n *\n * Usage:\n * const { honeypotInputProps, getSignals, resetSignals } = useHumanitySignals()\n * // render: <HoneypotField {...honeypotInputProps} />\n * // submit: fetch(url, { body: JSON.stringify({ ...formData, ...getSignals() }) })\n * // after a successful submit: resetSignals()\n *\n * The two signals are origin-independent (they ride in the body), so they keep\n * working when the form is embedded behind a reverse-proxy / prefixed URL.\n */\nexport function useHumanitySignals() {\n const ref = useRef<HTMLInputElement>(null)\n // performance.now() is monotonic (immune to wall-clock skew). SSR-guarded\n // even though the hook only executes on the client.\n const mountedAt = useRef<number>(typeof performance !== 'undefined' ? performance.now() : 0)\n\n const getSignals = useCallback(\n (): HumanitySignals => ({\n [HONEYPOT_FIELD]: ref.current?.value ?? '',\n [ELAPSED_MS_FIELD]:\n typeof performance !== 'undefined' ? Math.round(performance.now() - mountedAt.current) : 0,\n }),\n [],\n )\n\n const resetSignals = useCallback(() => {\n if (ref.current) ref.current.value = ''\n if (typeof performance !== 'undefined') mountedAt.current = performance.now()\n }, [])\n\n return { honeypotInputProps: { ref, name: HONEYPOT_FIELD }, getSignals, resetSignals } as const\n}\n","/**\n * Humanity signals — invisible bot-protection primitives shared by the lib's\n * public forms (client) and the hub's per-route `verifyHuman` gate (server).\n *\n * PURE + React-free on purpose: this module is a tsup SERVER entry (no\n * \"use client\" banner) so the hub can import it server-side without pulling a\n * client-reference boundary — same pattern as `schemas/contact-schema` and\n * `components/features/mux-origins`.\n *\n * Two origin-independent signals travel in the POST body: a honeypot (a hidden\n * field real users never fill) and timing (ms from form mount to submit).\n * `evaluateHumanitySignals` is the SINGLE source of truth for the block/allow\n * decision — the hub imports + calls it rather than re-implementing the rules.\n */\n\n/** Hidden honeypot field name. Innocuous + autofill-resistant (deliberately NOT name/email). */\nexport const HONEYPOT_FIELD = 'contact_url_confirm'\n/** Client-measured ms between form mount and submit. */\nexport const ELAPSED_MS_FIELD = 'form_elapsed_ms'\n/** Default minimum fill time (ms). A submit faster than this is treated as a bot. */\nexport const DEFAULT_MIN_FILL_MS = 700\n\n/** Keyed wire object produced by `useHumanitySignals().getSignals()` and spread into the POST body. */\nexport type HumanitySignals = Record<string, string | number>\n\n/** Result of {@link evaluateHumanitySignals}. */\nexport type HumanityVerdict = { ok: true } | { ok: false; reason: 'honeypot' | 'too_fast' }\n\n/** Tolerant reader — never throws; missing/garbage timing → null. */\nexport function extractHumanitySignals(body: unknown): { honeypot: string; elapsedMs: number | null } {\n const b = (body ?? {}) as Record<string, unknown>\n const rawHp = b[HONEYPOT_FIELD]\n // A legit client always sends a STRING here (getSignals → ref.value ?? ''),\n // so ANY present non-string value is a bot filling the decoy with a non-string\n // to dodge the empty-check — coerce to a (non-empty) string so it still trips.\n // null/undefined → '' = the correct \"field absent / unfilled\" allow case.\n const honeypot = rawHp == null ? '' : String(rawHp)\n const rawMs = b[ELAPSED_MS_FIELD]\n const elapsedMs = typeof rawMs === 'number' && Number.isFinite(rawMs) ? rawMs : null\n return { honeypot, elapsedMs }\n}\n\n/**\n * SINGLE decision fn for honeypot + timing (the hub's `verifyHuman` imports + calls this):\n * - honeypot non-empty → bot (real users never fill the off-screen field)\n * - elapsed below `minFillMs` → bot (humans take time; a MISSING timing value never blocks)\n */\nexport function evaluateHumanitySignals(body: unknown, opts: { minFillMs: number }): HumanityVerdict {\n const { honeypot, elapsedMs } = extractHumanitySignals(body)\n if (honeypot.trim() !== '') return { ok: false, reason: 'honeypot' }\n if (elapsedMs !== null && elapsedMs < opts.minFillMs) return { ok: false, reason: 'too_fast' }\n return { ok: true }\n}\n\n/** Parse a comma-separated env string → trimmed, non-empty entries (undefined → []). */\nexport const splitCsvEnv = (s?: string): string[] =>\n s?.split(',').map((t) => t.trim()).filter(Boolean) ?? []\n"]}