{"version":3,"sources":["/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/chunk-JJUV7Z4L.cjs","../src/hooks/ui/use-auto-limit-tags.ts","../src/hooks/ui/use-deferred-error.ts","../src/hooks/ui/use-drag-and-drop-enabled.ts","../src/hooks/ui/use-focus-trap.ts","../src/hooks/ui/use-horizontal-scrollbar.ts","../src/hooks/ui/use-keyboard-collision-padding.ts","../src/hooks/ui/use-local-storage.ts","../src/utils/storage-event.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/ui/index.ts","../src/hooks/platform/use-platform-config.ts","../src/utils/platform-config.tsx","../src/hooks/use-toast.ts","../src/components/ui/toaster.tsx","../src/hooks/use-contact-submission.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/flatten-schema.ts","../src/hooks/state/graphql-parser.ts","../src/hooks/state/introspection.ts","../src/hooks/state/url-converter.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-access-code-integration.ts","../src/utils/access-code-client.ts","../src/hooks/use-claude-mirror-src.ts","../src/utils/embed-url-converters.ts","../src/hooks/use-scroll-to-hash.ts","../src/hooks/use-humanity-signals.ts","../src/utils/humanity-signals.ts"],"names":["measure","useState","useEffect","useCallback","dismissOnboarding","markMultipleComplete","jsx","useMemo","isInputObjectType","clearParams"],"mappings":"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CP,IAAA;AACgB,IAAA;AACqC,IAAA;AACxC,IAAA;AAC6C,MAAA;AAC7C,MAAA;AACb,IAAA;AACyB,EAAA;AAC7B;AtCs1FyF;AACA;AuC75F3C;AvC+5F2C;AACA;AwC73F3D;AAEE;AAEG;AASkC;AAGgD;AAiCf;AAChF,EAAA;AACU,EAAA;AAMkD,EAAA;AAChD,EAAA;AACgD,EAAA;AACnD,EAAA;AAC/B;AAU4F;AACjC;AAG7B;AAEL;AAQqB;AAcyC;AACxC,EAAA;AACX,EAAA;AACQ,EAAA;AAC2B,EAAA;AAExB,EAAA;AACkB,IAAA;AAGT,IAAA;AACe,IAAA;AACrE,EAAA;AAC4F,EAAA;AAExE,EAAA;AAC0B,EAAA;AACd,IAAA;AACH,IAAA;AACW,IAAA;AAC2B,IAAA;AACqB,MAAA;AACnC,QAAA;AACK,QAAA;AACtD,MAAA;AACF,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAUqG;AACxC,EAAA;AACX,EAAA;AACrB,IAAA;AAC+B,IAAA;AAC1D,EAAA;AACmC,EAAA;AACmC,EAAA;AACU,EAAA;AACG,EAAA;AAExB,EAAA;AAE7D;AAOyB;AxCyxFgE;AACA;AuCv8FpD;AACM,EAAA;AAU4C,EAAA;AACxC,EAAA;AAE1BA,EAAAA;AACO,IAAA;AACkB,MAAA;AACwC,MAAA;AAClF,IAAA;AACC,IAAA;AACH,EAAA;AAEuC,EAAA;AACA,IAAA;AAC0C,IAAA;AAC5E,EAAA;AAEgF,EAAA;AACvF;AvC67FyF;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-JJUV7Z4L.cjs","sourcesContent":[null,"'use client';\n\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport type { RefObject } 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: RefObject<HTMLDivElement | null>;\n  /** Ref for the off-screen container that holds measurement copies of ALL tags */\n  measureRef: RefObject<HTMLDivElement | null>;\n  /** Ref for the hidden span that measures placeholder text width */\n  textMeasureRef: RefObject<HTMLSpanElement | null>;\n  /** Ref for the \"+N\" badge element (used to measure its width) */\n  badgeRef: RefObject<HTMLButtonElement | null>;\n  /** Ref for the input element (used to read its min-width) */\n  inputRef: 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  // `null` = never measured (no container rendered, or a fixed `limitTags` that\n  // needs no measurement). Kept distinct from `0` so the \"show everything\"\n  // fallback below stays derivable during render instead of having to be\n  // written into state from an effect.\n  const [measuredCount, setMeasuredCount] = useState<number | null>(null);\n\n  const recalculate = useCallback(() => {\n    const middle = middleRef.current;\n    const measure = measureRef.current;\n    if (!middle || !measure) {\n      setMeasuredCount(null);\n      return;\n    }\n    if (count === 0) {\n      setMeasuredCount(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 ? parseFloat(getComputedStyle(inputRef.current).minWidth) || 60 : 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      setMeasuredCount(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    setMeasuredCount(Math.max(0, fitCount));\n  }, [count, placeholder, reserveInputWidth]);\n\n  // ONE effect, not two. `ResizeObserver` delivers an initial notification as\n  // soon as it starts observing, so `observe()` IS the mount-time measurement —\n  // the separate `recalculate()` call this replaces was a synchronous setState\n  // in an effect body, i.e. a guaranteed second render pass on every mount and\n  // on every change of `count`. `recalculate`'s identity changes with `count` /\n  // `placeholder` / `reserveInputWidth`, so a re-observe (and therefore a fresh\n  // measurement) still happens for input changes that do not resize the box.\n  useEffect(() => {\n    if (limitTags !== 'auto') return undefined;\n    const el = middleRef.current;\n    if (!el) return undefined;\n    const ro = new ResizeObserver(recalculate);\n    ro.observe(el);\n    return () => ro.disconnect();\n  }, [recalculate, limitTags]);\n\n  // A fixed limit is pure arithmetic on props and never needed the DOM at all.\n  // The `Math.min(…, count)` clamp matters because the measurement lags a\n  // shrinking `count` by one commit: unclamped, callers computing\n  // `count - visibleCount` for the \"+N\" badge briefly get a NEGATIVE number.\n  const visibleCount =\n    limitTags !== 'auto' ? Math.min(limitTags, count) : measuredCount === null ? count : Math.min(measuredCount, count);\n\n  return { visibleCount, middleRef, measureRef, textMeasureRef, badgeRef, inputRef };\n}\n","'use client';\n\nimport { useState, useEffect, useCallback } from 'react';\n\nexport interface UseDeferredErrorResult {\n  /** The message to render (undefined while the reveal is deferred or the field is valid). */\n  error: string | undefined;\n  /** Attach to the field's onBlur — reveals validation immediately and keeps it live from then on. */\n  onBlur: () => void;\n}\n\n/**\n * Defers showing a field validation message while the user is still typing.\n *\n * The message is revealed when the field loses focus (touched) or when the\n * value hasn't changed for `delay` ms; every keystroke restarts the timer.\n * Once touched, validation stays live. A message that clears (the value became\n * valid) disappears immediately.\n */\nexport function useDeferredError(error: string | undefined, value: string, delay = 1500): UseDeferredErrorResult {\n  const [touched, setTouched] = useState(false);\n  const [paused, setPaused] = useState(false);\n  const [pausedFor, setPausedFor] = useState(value);\n\n  // Typing hides the message: React's \"adjust state while rendering\" reset, not\n  // an effect. The keystroke has already scheduled this render, so clearing\n  // `paused` here costs nothing, whereas doing it from the effect below commits\n  // a render with the stale message still on screen and then immediately\n  // re-renders to remove it.\n  if (pausedFor !== value) {\n    setPausedFor(value);\n    setPaused(false);\n  }\n\n  // …and restarts the pause timer.\n  useEffect(() => {\n    if (touched) return undefined;\n    const timer = setTimeout(() => setPaused(true), delay);\n    return () => clearTimeout(timer);\n  }, [value, delay, touched]);\n\n  const onBlur = useCallback(() => setTouched(true), []);\n\n  return {\n    error: error && (touched || paused) ? error : undefined,\n    onBlur,\n  };\n}\n","'use client';\n\nimport { breakpoints, useMediaQuery } from './use-media-query';\n\n/**\n * The one rule for whether drag-and-drop is initialized at all.\n *\n * Pointer type AND breakpoint, never either alone:\n * - A breakpoint alone fails on tablets: an iPad in landscape is 1024-1366px\n *   wide and would pass any sane `min-width` — exactly the case being excluded,\n *   because the HTML5 drag-and-drop API Pragmatic builds on does not fire under\n *   touch.\n * - `pointer: coarse` alone fails the other way: a touchscreen laptop with a\n *   trackpad reports `pointer: fine` and should keep DnD, but in a narrow\n *   window its lists collapse to the mobile layout where a drag handle is out\n *   of place.\n * - No `'ontouchstart' in window`, no UA sniffing: the first is true on touch\n *   laptops, the second is neither accurate nor reactive. A media query\n *   re-evaluates live when a mouse is attached or the window resizes.\n *\n * `breakpoints.md` (800px) is the existing tablet step — no new breakpoint.\n */\nexport const DRAG_AND_DROP_MEDIA_QUERY = `(pointer: fine) and (hover: hover) and ${breakpoints.md}`;\n\n/**\n * Whether drag-and-drop should be available on this device/viewport.\n *\n * `false` on touch/narrow screens — there the reordering surfaces render\n * explicit controls (`SortableMoveButtons`) instead of a drag handle, and no\n * DnD is initialized at all. Also `false` during SSR and the first client\n * render (`useMediaQuery` is still `undefined`): defaulting to \"off\" renders\n * no dead drag handle, and the underlying layout effect settles the real\n * answer before first paint, so a desktop user never sees the fallback flash.\n */\nexport function useDragAndDropEnabled(): boolean {\n  return useMediaQuery(DRAG_AND_DROP_MEDIA_QUERY) ?? false;\n}\n","'use client';\n\nimport { useEffect, useRef, type RefObject } from 'react';\n\nconst FOCUSABLE_SELECTOR = [\n  'a[href]',\n  'button:not([disabled])',\n  'input:not([disabled])',\n  'select:not([disabled])',\n  'textarea:not([disabled])',\n  '[tabindex]:not([tabindex=\"-1\"])',\n].join(', ');\n\nexport interface UseFocusTrapOptions {\n  /** Called when Escape is pressed while focus is inside the container.\n   *  When omitted, Escape passes through untouched so document-level\n   *  listeners still fire. */\n  onEscape?: () => void;\n  /**\n   * When false, Tab/Shift+Tab are NOT cycled within the container — for\n   * non-modal dialogs (e.g. SlidingSidebar, whose z-50 header stays clickable\n   * above the open drawer). Escape / initial focus / guarded restore still apply.\n   * @default true\n   */\n  contain?: boolean;\n}\n\n/**\n * Focus management for overlay surfaces (mobile nav panel, sliding sidebar,\n * burger menu, modals): initial focus on activate, container-scoped Tab\n * cycling + Escape, and guarded focus restore on deactivate.\n *\n * CONTRACT: the container element MUST have `tabIndex={-1}`. After a\n * click/tap on a non-focusable area inside the surface, browsers move focus\n * to the nearest focusable ancestor — with the container script-focusable\n * that ancestor is the container itself, so this keydown listener keeps\n * hearing Escape/Tab. Without it, focus falls to `body` and the trap goes\n * deaf.\n *\n * The listener being container-scoped makes it stacking-safe: it only fires\n * while focus is inside the surface, so a modal open above never leaks its\n * Escape into the surface below.\n */\nexport function useFocusTrap(\n  containerRef: RefObject<HTMLElement | null>,\n  active: boolean,\n  { onEscape, contain = true }: UseFocusTrapOptions = {},\n): void {\n  // Latest-ref so unstable inline callbacks never tear the trap down and\n  // re-yank focus — same stable-callback discipline as `useCloseOnNavigation`.\n  // Refreshed after every commit, declared before the trap effect so it wins\n  // the same flush. Not in the render body: the reader is a keydown listener,\n  // which cannot fire before a commit.\n  const onEscapeRef = useRef(onEscape);\n  useEffect(() => {\n    onEscapeRef.current = onEscape;\n  });\n\n  useEffect(() => {\n    if (!active) return undefined;\n    const container = containerRef.current;\n    if (!container) return undefined;\n\n    const previouslyFocused = document.activeElement as HTMLElement | null;\n\n    const focusables = () =>\n      Array.from(container.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR)).filter(\n        el => el.getClientRects().length > 0,\n      );\n\n    const first = focusables()[0];\n    (first ?? container).focus({ preventScroll: true });\n\n    const handleKeyDown = (e: KeyboardEvent) => {\n      if (e.key === 'Escape') {\n        // No preventDefault/stopPropagation — pass-through by design.\n        onEscapeRef.current?.();\n        return;\n      }\n      if (e.key !== 'Tab' || !contain) return;\n      const items = focusables();\n      if (items.length === 0) {\n        e.preventDefault();\n        container.focus({ preventScroll: true });\n        return;\n      }\n      const firstEl = items[0];\n      const lastEl = items[items.length - 1];\n      const current = document.activeElement;\n      if (e.shiftKey && (current === firstEl || current === container)) {\n        e.preventDefault();\n        lastEl.focus({ preventScroll: true });\n      } else if (!e.shiftKey && current === lastEl) {\n        e.preventDefault();\n        firstEl.focus({ preventScroll: true });\n      }\n    };\n\n    container.addEventListener('keydown', handleKeyDown);\n\n    return () => {\n      container.removeEventListener('keydown', handleKeyDown);\n      // Guarded restore: only when focus is still inside the surface (or fell\n      // to body because the surface unmounted) — never steal focus from a\n      // dialog that opened meanwhile (e.g. the nav-footer → SSO modal flow).\n      const current = document.activeElement;\n      if (\n        previouslyFocused &&\n        previouslyFocused.isConnected &&\n        (current === null || current === document.body || container.contains(current))\n      ) {\n        previouslyFocused.focus({ preventScroll: true });\n      }\n    };\n  }, [containerRef, active, contain]);\n}\n","'use client';\n\nimport { useRef, useState, useCallback } from 'react';\nimport type { MouseEvent, PointerEvent, WheelEvent } 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(\n    (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    },\n    [measure, syncThumbToDOM, syncEdgeFades],\n  );\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: 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, (e.clientX - rect.left - thumbWidth / 2) / maxThumbOffset));\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: 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: 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(\n    (e: 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    },\n    [syncThumbToDOM, syncEdgeFades],\n  );\n\n  // Drag: pointer up\n  const onThumbPointerUp = useCallback((e: 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 { useSyncExternalStore } from 'react';\n\n/**\n * Extra `collisionPadding.bottom` (px) a Radix popper needs to clear the\n * software keyboard. 0 on desktop and anywhere the keyboard is down.\n *\n * Radix positions every popper with floating-ui at `strategy: 'fixed'`, and\n * floating-ui takes its collision viewport from `window.visualViewport`.\n * Neither mobile shell shrinks the box a keyboard opens into: iOS runs\n * Capacitor's Keyboard plugin at `resize: 'none'`, which leaves the WKWebView\n * frame alone, and Android's window is edge-to-edge, where the IME is only a\n * WindowInsets type. So `flip`/`shift`/`size` all believe the screen still\n * reaches the bottom, and a dropdown whose own search field just raised the\n * keyboard is placed straight behind it — worst inside modals, which sit near\n * the bottom of the screen by design.\n *\n * The shell publishes the keyboard height as `--of-keyboard-inset` on <html>\n * (openframe-frontend's `keyboard-inset.ts`; unset everywhere else, hence the\n * 0 default). Handing the part floating-ui does not already know about back as\n * collision padding is what makes the top of the keyboard the bottom edge of\n * the screen.\n *\n * ```tsx\n * const keyboardPadding = useKeyboardCollisionPadding()\n * <PopoverPrimitive.Content collisionPadding={{ bottom: keyboardPadding }} />\n * ```\n *\n * Pair it with `max-h-[var(--radix-popper-available-height)]` on the content:\n * padding alone flips a dropdown above its trigger, but a list taller than what\n * is left above the keyboard would then spill back over it.\n */\n\nconst KEYBOARD_INSET_VAR = '--of-keyboard-inset';\n\nfunction measure(): number {\n  if (typeof document === 'undefined') return 0;\n  const inset = Number.parseFloat(getComputedStyle(document.documentElement).getPropertyValue(KEYBOARD_INSET_VAR));\n  // Also catches the NaN of an unset variable.\n  if (!(inset > 0)) return 0;\n  // How far floating-ui's own viewport bottom sits below the top of the\n  // keyboard. iOS reports the keyboard on the visual viewport and Android does\n  // not; measuring the difference rather than branching on the platform keeps\n  // both correct and can never reserve the same keyboard twice.\n  const layoutHeight = document.documentElement.clientHeight;\n  const viewport = window.visualViewport;\n  const viewportBottom = viewport ? viewport.offsetTop + viewport.height : layoutHeight;\n  return Math.max(0, Math.round(viewportBottom - (layoutHeight - inset)));\n}\n\n// One document-wide subscription for every popper on the page: the keyboard is\n// a single global, and each open dropdown re-reading it would mean another\n// MutationObserver plus a forced style resolve per keystroke-driven re-render.\nconst listeners = new Set<() => void>();\nlet padding = 0;\nlet teardown: (() => void) | null = null;\n\nfunction sync(): void {\n  const next = measure();\n  if (next === padding) return;\n  padding = next;\n  for (const notify of listeners) notify();\n}\n\nfunction subscribe(onStoreChange: () => void): () => void {\n  listeners.add(onStoreChange);\n  if (listeners.size === 1) {\n    padding = measure();\n    // Two independent signals. The inset lands on <html>'s inline style, so the\n    // attribute is the only notification for it; the visual viewport moves on\n    // its own schedule (iOS reports the keyboard there, and a pinch-zoom shifts\n    // it with no keyboard involved at all) and changes what is left to reserve.\n    const observer = new MutationObserver(sync);\n    observer.observe(document.documentElement, { attributes: true, attributeFilter: ['style'] });\n    const viewport = window.visualViewport;\n    viewport?.addEventListener('resize', sync);\n    viewport?.addEventListener('scroll', sync);\n    teardown = () => {\n      observer.disconnect();\n      viewport?.removeEventListener('resize', sync);\n      viewport?.removeEventListener('scroll', sync);\n    };\n  }\n  return () => {\n    listeners.delete(onStoreChange);\n    if (listeners.size > 0) return;\n    teardown?.();\n    teardown = null;\n  };\n}\n\nexport function useKeyboardCollisionPadding(): number {\n  return useSyncExternalStore(\n    subscribe,\n    () => padding,\n    () => 0,\n  );\n}\n","'use client';\n\nimport { useEffect, useMemo, useRef, useState } from 'react';\n\nimport { createLocalStorageAdapter } from '../../utils/local-storage-adapter';\nimport { readLocalStorageUpdateDetail } from '../../utils/storage-event';\n\nexport interface UseLocalStorageOptions<T> {\n  /**\n   * Runtime shape check applied to every value decoded from storage — the\n   * first read and each cross-tab update. Returning false discards the stored\n   * value and falls back to `initialValue`.\n   *\n   * Without it the stored JSON is trusted to be a `T`, which only holds while\n   * nothing else writes the key and the persisted shape never changes. Pass a\n   * guard for anything whose shape has versions.\n   *\n   * Keep the identity stable (module scope or `useCallback`) — an inline\n   * predicate re-subscribes the cross-tab listeners on every render.\n   */\n  validate?: (parsed: unknown) => parsed is T;\n}\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 * @param options - Optional `validate` guard for the stored payload\n * @returns [storedValue, setValue] tuple\n */\nexport function useLocalStorage<T>(\n  key: string,\n  initialValue: T,\n  options?: UseLocalStorageOptions<T>,\n): [T, (value: T | ((val: T) => T)) => void] {\n  // Latest `initialValue` for the \"key was cleared elsewhere\" reset path. It\n  // cannot be an effect dependency: the listeners register once per `key`, and\n  // any caller passing an object or array literal would re-subscribe on every\n  // render.\n  const initialValueRef = useRef(initialValue);\n  useEffect(() => {\n    initialValueRef.current = initialValue;\n  });\n\n  // Single decode path — SSR guard, JSON parse, validation and the quota-safe\n  // write all live in the adapter, so nothing here re-implements them.\n  const validate = options?.validate;\n  const storage = useMemo(\n    () => createLocalStorageAdapter<T>({ key, validate, logTag: `[useLocalStorage:${key}]` }),\n    [key, validate],\n  );\n\n  // Lazy initialization reads from localStorage synchronously on first render.\n  const [storedValue, setStoredValue] = useState<T>(() => storage.load() ?? initialValue);\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 undefined;\n\n    // Both events only say \"this key changed\"; the value is read back through\n    // the adapter so the payload is decoded and validated in exactly one place.\n    // A removed key reads back as null and resets to `initialValue`, whichever\n    // event reported it.\n    const syncFromStorage = (source: string) => {\n      const next = storage.load();\n      isFromStorageEvent.current = true;\n      setStoredValue(next ?? initialValueRef.current);\n      console.log(`🔄 ${next === null ? 'Cleared' : 'Updated'} localStorage key \"${key}\" from ${source}`);\n    };\n\n    const handleStorageChange = (e: StorageEvent) => {\n      if (e.key !== key) return;\n      syncFromStorage('storage event');\n    };\n\n    const handleCustomStorageUpdate = (event: Event) => {\n      const detail = readLocalStorageUpdateDetail(event);\n      if (detail?.key !== key) return;\n      syncFromStorage('custom storage event');\n    };\n\n    window.addEventListener('storage', handleStorageChange);\n    window.addEventListener('localStorageUpdate', handleCustomStorageUpdate);\n\n    return () => {\n      window.removeEventListener('storage', handleStorageChange);\n      window.removeEventListener('localStorageUpdate', handleCustomStorageUpdate);\n    };\n  }, [key, storage]);\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    storage.save(storedValue);\n  }, [storage, 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","/**\n * The `localStorageUpdate` CustomEvent contract.\n *\n * `CustomEvent.detail` is typed `any`, and `addEventListener` types a\n * non-standard event name as a bare `Event` — so every listener used to\n * re-derive the payload shape by hand off an `any`. The decode lives here\n * once: an event dispatched by anything on the page is untrusted input, so it\n * is validated rather than asserted.\n *\n * Dispatchers: `saveOnboardingState` (utils/onboarding-storage.ts).\n * Listeners: `useLocalStorage`, `useOnboardingState`.\n */\n\nexport interface LocalStorageUpdateDetail {\n  /** Storage key that changed. */\n  key: string;\n  /** Serialized new value; `null` when the key was removed or cleared. */\n  newValue: string | null;\n}\n\n/**\n * Decode a `localStorageUpdate` event payload.\n *\n * Returns `null` — never throws — when the event is not a CustomEvent, carries\n * no object detail, or the detail does not match the contract above. A missing\n * or `undefined` `newValue` is normalized to `null`, which is the \"key was\n * removed\" signal.\n */\nexport function readLocalStorageUpdateDetail(event: Event): LocalStorageUpdateDetail | null {\n  if (!('detail' in event)) return null;\n\n  const detail: unknown = event.detail;\n  if (typeof detail !== 'object' || detail === null) return null;\n  if (!('key' in detail) || typeof detail.key !== 'string') return null;\n\n  const newValue: unknown = 'newValue' in detail ? detail.newValue : null;\n  if (newValue !== null && newValue !== undefined && typeof newValue !== 'string') return null;\n\n  return { key: detail.key, newValue: newValue ?? null };\n}\n","'use client';\n\nimport { useCallback, useEffect, useRef } from 'react';\n\n/**\n * A callback whose IDENTITY never changes and whose BODY is always the latest\n * one you passed — the \"latest ref\" pattern.\n *\n * Rewritten 2026-08-25. The previous implementation was broken in three ways\n * that only showed up once the React Compiler rules were switched on:\n *\n *   1. It wrote `callbackRef.current` and `dependenciesRef.current` DURING\n *      RENDER. A render that React throws away (Strict Mode, a suspended\n *      transition, a re-render that never commits) still left those writes\n *      behind, so the \"memoized\" callback could be one from a discarded render.\n *   2. Its dependency array was `[depsChanged]` — a BOOLEAN. That flips true\n *      on the render where deps change and false on the next one, so the\n *      returned identity changed TWICE per dependency change and was never\n *      stable, which is the one thing the hook exists to provide.\n *   3. The doc promised \"deep comparison\"; it compared with `Object.is`.\n *\n * The signature is unchanged, so no call site has to move. `dependencies` is\n * now ignored: the returned function always calls the newest callback, which\n * is what every reader of \"stable callback reference\" expected it to do. If\n * you actually WANT the identity to change with deps, use `useCallback`\n * directly — that is what it is for.\n */\nexport function useMemoizedCallback<T extends (...args: never[]) => unknown>(\n  callback: T,\n  _dependencies?: readonly unknown[],\n): T {\n  const callbackRef = useRef(callback);\n\n  // In an effect, not during render: this runs only for renders that commit.\n  useEffect(() => {\n    callbackRef.current = callback;\n  });\n\n  const stable = useCallback((...args: Parameters<T>) => callbackRef.current(...args), []);\n  return stable as T;\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 undefined;\n    // Both values are reads of an external system (the browser's permission\n    // store), so they belong in the same sync callback rather than as a\n    // separate mount-time setState: `supported` cannot be computed during\n    // render without a hydration mismatch, and folding it into `sync` means the\n    // initial read costs one render instead of two. Re-syncs are free — React\n    // bails out when the next value is identical, which it is for every\n    // visibility/focus event that did not actually change the permission.\n    const sync = () => {\n      setSupported(true);\n      setPermission(Notification.permission);\n    };\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' })\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';\n    }\n    // Older Safari only implements the callback form; resolving from both is harmless.\n    // The DOM lib types `requestPermission` as always returning a promise, so testing\n    // the value itself is a conditional on something the types call always-truthy —\n    // the thenable probe is the check that actually means something at runtime, since\n    // the callback-only implementations return undefined here.\n    const result = await new Promise<NotificationPermission>(resolve => {\n      const maybePromise = Notification.requestPermission(resolve);\n      if (typeof maybePromise?.then === 'function') {\n        // A rejection here would otherwise leave this promise forever pending, and\n        // `request()` never settles: the caller's permission toggle silently stays\n        // off. Fall back to whatever the browser currently reports.\n        void maybePromise.then(resolve, () => resolve(Notification.permission));\n      }\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 type { ReactNode } from 'react';\nimport {\n  loadOnboardingState,\n  markStepComplete as storageMarkComplete,\n  markStepSkipped as storageMarkSkipped,\n  dismissOnboarding as storageDismiss,\n  markMultipleComplete as storageMarkMultiple,\n  type OnboardingState,\n} from '../../utils/onboarding-storage';\nimport { readLocalStorageUpdateDetail } from '../../utils/storage-event';\n\nexport interface OnboardingStepConfig {\n  id: string;\n  title: string;\n  description: string;\n  actionIcon: (color?: string) => 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 = (event: Event) => {\n      const detail = readLocalStorageUpdateDetail(event);\n      if (detail?.key !== storageKey) return;\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    window.addEventListener('localStorageUpdate', handleStorageUpdate);\n    return () => {\n      window.removeEventListener('localStorageUpdate', handleStorageUpdate);\n    };\n  }, [storageKey]);\n\n  const markComplete = useCallback(\n    (stepId: string) => {\n      console.log(`🎯 markComplete called for: \"${stepId}\"`);\n      const newState = storageMarkComplete(storageKey, stepId);\n      setState(newState);\n      forceUpdate(prev => prev + 1);\n    },\n    [storageKey],\n  );\n\n  const markSkipped = useCallback(\n    (stepId: string) => {\n      console.log(`⏭️ markSkipped called for: \"${stepId}\"`);\n      const newState = storageMarkSkipped(storageKey, stepId);\n      setState(newState);\n      forceUpdate(prev => prev + 1);\n    },\n    [storageKey],\n  );\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(\n    (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    },\n    [storageKey],\n  );\n\n  const isStepComplete = useCallback(\n    (stepId: string): boolean => {\n      return state.completedSteps.includes(stepId);\n    },\n    [state.completedSteps],\n  );\n\n  const isStepSkipped = useCallback(\n    (stepId: string): boolean => {\n      return state.skippedSteps.includes(stepId);\n    },\n    [state.skippedSteps],\n  );\n\n  const allStepsComplete = useCallback(\n    (steps: OnboardingStepConfig[]): boolean => {\n      return steps.every(step => isStepComplete(step.id) || isStepSkipped(step.id));\n    },\n    [isStepComplete, isStepSkipped],\n  );\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(key: string, stepIds: string[]): 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, useRef } from 'react';\nimport type { SearchResult } from '../../components/ui/search-input';\nimport { useDebounce } from './use-debounce';\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// One shared empty array: `setResults([])` with a fresh literal is a new\n// identity every time, which re-renders every consumer that keys off `results`.\nconst NO_RESULTS: SearchResult[] = [];\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[]>(NO_RESULTS);\n  const [isLoading, setIsLoading] = useState(false);\n  const [error, setError] = useState<string | null>(null);\n\n  // `searchFn` and `mapResult` come from the caller and are almost always\n  // written inline, so a fresh identity every render. As effect dependencies\n  // they would re-fire the search on every parent render; read through refs the\n  // effect keys on the QUERY only and still calls the current functions.\n  const searchFnRef = useRef(searchFn);\n  const mapResultRef = useRef(mapResult);\n  useEffect(() => {\n    searchFnRef.current = searchFn;\n    mapResultRef.current = mapResult;\n  });\n\n  const debouncedQuery = useDebounce(query, debounceMs);\n\n  const clearResults = useCallback(() => {\n    setResults(NO_RESULTS);\n    setError(null);\n  }, []);\n\n  // Falling below the threshold clears the panel. That is a reset driven by a\n  // value we already have during render, so it uses React's \"adjust state while\n  // rendering\" pattern rather than an effect: the keystroke that shortened the\n  // query has already scheduled this render, and clearing here means the stale\n  // result list never reaches the screen. From an effect it would paint once\n  // with the old results still showing.\n  const searchable = Boolean(debouncedQuery) && debouncedQuery.length >= minQueryLength;\n  const [wasSearchable, setWasSearchable] = useState(searchable);\n  if (wasSearchable !== searchable) {\n    setWasSearchable(searchable);\n    if (!searchable) {\n      setResults(NO_RESULTS);\n      setIsLoading(false);\n      setError(null);\n    }\n  }\n\n  useEffect(() => {\n    if (!searchable) return undefined;\n\n    let cancelled = false;\n\n    const run = async () => {\n      setIsLoading(true);\n      setError(null);\n\n      try {\n        const rawResults = await searchFnRef.current(debouncedQuery);\n\n        if (!cancelled) {\n          setResults(rawResults.map(mapResultRef.current));\n        }\n      } catch (err) {\n        if (!cancelled) {\n          setError(err instanceof Error ? err.message : 'Search failed');\n          setResults(NO_RESULTS);\n        }\n      } finally {\n        if (!cancelled) {\n          setIsLoading(false);\n        }\n      }\n    };\n\n    // Never rejects — try/catch/finally, every write gated on `cancelled`.\n    void run();\n\n    return () => {\n      cancelled = true;\n    };\n  }, [debouncedQuery, minQueryLength, searchable]);\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(config: PaginationConfig | null): 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  // The clock is read in a lazy `useState` initialiser, which runs exactly\n  // once. `useRef(Date.now())` looks equivalent but is not: the argument is\n  // evaluated on EVERY render even though only the first call uses it, so the\n  // component reads the clock during render forever after.\n  const [mountedAt] = useState(() => Date.now());\n  const lastUpdated = useRef<number>(mountedAt);\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      return undefined;\n    }\n\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  }, [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\n  useLayoutEffect(() => {\n    // The size lives on `window`, not in React — this is the initial read of an\n    // external system plus a subscription to it. The identity bail-out matters:\n    // `resize` fires for height-only changes (mobile URL bar) and for the same\n    // dimensions on orientation lock, and without it every one of those would\n    // re-render every consumer of this hook.\n    const handleResize = () => {\n      setWindowSize(prev =>\n        prev.width === window.innerWidth && prev.height === window.innerHeight\n          ? prev\n          : { width: window.innerWidth, 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  }, []);\n\n  return windowSize;\n}\n","// UI Hooks exports\nexport * from './use-auto-limit-tags';\nexport * from './use-debounce';\nexport * from './use-deferred-error';\nexport * from './use-drag-and-drop-enabled';\nexport * from './use-focus-trap';\nexport * from './use-header-height';\nexport * from './use-horizontal-scrollbar';\nexport * from './use-image-edge-color';\nexport * from './use-is-truncated';\nexport * from './use-isomorphic-layout-effect';\nexport * from './use-keyboard-collision-padding';\nexport * from './use-local-storage';\nexport * from './use-marquee-engine';\nexport * from './use-media-query';\nexport * from './use-memoized-callback';\nexport * from './use-notification-permission';\nexport * from './use-onboarding-state';\nexport * from './use-reset-on-page-hidden';\nexport * from './use-search';\nexport * from './use-suppress-clone-focus';\nexport * from './use-table-pagination';\nexport * from './use-throttle';\nexport * from './use-window-size';\n\n// Shared ref-counted, iOS-aware body scroll lock (react-aria). Re-exported so\n// hub/app consumers have one import path; lib internals may import the\n// package directly (npm dedupes to one module instance either way).\nexport { usePreventScroll } from '@react-aria/overlays';\n","import { useState, useEffect } from 'react';\nimport type { SelectableOption } from '../../components/features';\nimport type { PlatformConfig, PlatformOption } from '../../types/platform';\nimport { transformPlatformConfigsToOptions } from '../../utils/platform-config';\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\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n  return typeof value === 'object' && value !== null;\n}\n\n/** `Array.isArray` narrows `unknown` to `any[]`; this keeps the elements `unknown`. */\nfunction isUnknownArray(value: unknown): value is readonly unknown[] {\n  return Array.isArray(value);\n}\n\n/**\n * Decode `/api/config/platforms`.\n *\n * `Response.json()` is typed `any`, so `data.platforms || data` used to be\n * cached verbatim — including when it was neither an array nor a list of rows\n * (`{ platforms: null }` fell through the `||` and cached the ENVELOPE).\n * That matters more here than at a normal fetch boundary because\n * `platformCache` is MODULE-level: a bad payload is committed once and then\n * served to every later mount for the rest of the session, and\n * `platforms.map(...)` in the hook body throws on each one. Returning null\n * instead routes the payload into the hook's existing `error` state and leaves\n * the cache empty, so a later mount can retry.\n *\n * Row decoding is deliberately tolerant: `value`/`name` and `label`/\n * `display_name` are documented as the same string, so either spelling\n * satisfies the pair, and the purely presentational fields fall back to '' —\n * a platform missing its description should still appear in the filter rather\n * than vanish from it or render `undefined`.\n */\nfunction toPlatformConfigs(payload: unknown): PlatformConfig[] | null {\n  const rows = isRecord(payload) && 'platforms' in payload ? payload.platforms : payload;\n  if (!isUnknownArray(rows)) return null;\n\n  const configs: PlatformConfig[] = [];\n  for (const row of rows) {\n    if (!isRecord(row)) continue;\n    const read = (key: string): string | undefined => {\n      const value = row[key];\n      return typeof value === 'string' && value ? value : undefined;\n    };\n\n    const value = read('value') ?? read('name');\n    if (value === undefined) continue;\n    const label = read('label') ?? read('display_name') ?? value;\n\n    configs.push({\n      id: read('id') ?? value,\n      value,\n      label,\n      name: read('name') ?? value,\n      display_name: read('display_name') ?? label,\n      default_color: read('default_color') ?? '',\n      default_icon: read('default_icon') ?? '',\n      description: read('description') ?? '',\n    });\n  }\n  return configs;\n}\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    // `platformCache` / `fetchPromise` are module-level: an external store, as\n    // far as React is concerned. Every path below hands its outcome to the same\n    // pair of commit callbacks instead of repeating three near-identical\n    // setState pairs.\n    //\n    // The identity bail-out is what keeps the cached path free: the useState\n    // initialisers above already read `platformCache`, so re-committing the\n    // very same array must not schedule a render. It still commits when the\n    // cache was filled by another instance's fetch *between* this component's\n    // first render and this effect — the only case where the branch does\n    // anything at all.\n    const commit = (data: PlatformConfig[]) => {\n      setPlatforms(prev => (prev === data ? prev : data));\n      setIsLoading(false);\n    };\n    const fail = (err: unknown) => {\n      setError(err instanceof Error ? err : new Error(String(err)));\n      setIsLoading(false);\n    };\n\n    // If we already have cached platforms, use them\n    if (platformCache) {\n      commit(platformCache);\n      return;\n    }\n\n    // If a fetch is already in progress, wait for it\n    if (fetchPromise) {\n      fetchPromise.then(commit).catch(fail);\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): Promise<unknown> => {\n        if (!response.ok) {\n          throw new Error(`Failed to fetch platform config: ${response.statusText}`);\n        }\n        return response.json();\n      })\n      .then(payload => {\n        const loadedPlatforms = toPlatformConfigs(payload);\n        if (!loadedPlatforms) {\n          // Deliberately thrown rather than cached: see `toPlatformConfigs`.\n          throw new Error('Platform config response did not contain a platform list');\n        }\n        console.log('✅ Platform configurations loaded:', loadedPlatforms.length, 'platforms');\n        platformCache = loadedPlatforms;\n        fetchPromise = null;\n        return loadedPlatforms;\n      });\n\n    fetchPromise.then(commit).catch((err: unknown) => {\n      console.error('❌ Failed to fetch platform config:', err);\n      fail(err);\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}\n","import { Globe } from 'lucide-react';\nimport type React from 'react';\nimport type { SelectableOption } from '../components/features';\nimport { OpenmspLogo, FlamingoLogo, OpenFrameLogo, MiamiCyberGangLogoFaceOnly, MlgLogo } from '../components/icons';\nimport type { PlatformConfig, PlatformName } from '../types/platform';\nimport { cn } from './cn';\nimport { getPlatformBrandClasses } from './platform-identity';\n\n/**\n * Per-platform ICON rendering. The colour decision lives in `platform-identity`\n * (`PLATFORM_BRAND` → `ODS_STEM_CLASSES`), never here: each mark is tinted by the\n * platform's accent CLASS and paints itself with `currentColor`. There is ONE\n * lookup, parameterized by `className`; the three former hand-written switches\n * (`getPlatformIcon`, `getSmallPlatformIcon`, `getPlatformIconComponent`) are\n * thin wrappers that only pick a size.\n *\n * Two marks cannot take an accent fill and are named exceptions:\n *   - `universal` is a lucide `Globe`; lucide spreads `...rest` AFTER `fill: 'none'`,\n *     so a `fill` prop would paint a solid disc. It takes the class only.\n *   - `tmcg` is a two-colour mark (`MiamiCyberGangLogoFaceOnly`) with no fill prop.\n */\nfunction renderPlatformIcon(platformName: string, className: string): React.ReactNode {\n  const tinted = cn(className, getPlatformBrandClasses(platformName).accentText);\n  switch (platformName) {\n    case 'openframe':\n      // `lowerPathColor` is the accent; the upper path is the neutral greys white.\n      return (\n        <OpenFrameLogo\n          className={tinted}\n          lowerPathColor=\"currentColor\"\n          upperPathColor=\"var(--ods-system-greys-white)\"\n        />\n      );\n    case 'openmsp':\n      // `OpenmspLogo`'s paths set their own fills, so a root `fill` is a no-op:\n      // the accent is the BACK bubble. The other two are the neutral greys.\n      return (\n        <OpenmspLogo\n          className={tinted}\n          backBubbleColor=\"currentColor\"\n          frontBubbleColor=\"var(--ods-system-greys-white)\"\n          innerFrontBubbleColor=\"var(--ods-system-greys-black)\"\n        />\n      );\n    case 'tmcg':\n      return <MiamiCyberGangLogoFaceOnly className={className} />;\n    case 'mlg':\n      return <MlgLogo className={tinted} size={20} />;\n    case 'universal':\n      return <Globe className={tinted} />;\n    default:\n      return <FlamingoLogo className={tinted} fill=\"currentColor\" />;\n  }\n}\n\n/** Platform icons mapping (5x5), tinted by each platform's accent class. */\nexport const platformIcons: Record<PlatformName, React.ReactNode> = {\n  openframe: renderPlatformIcon('openframe', 'h-5 w-5'),\n  openmsp: renderPlatformIcon('openmsp', 'h-5 w-5'),\n  flamingo: renderPlatformIcon('flamingo', 'h-5 w-5'),\n  'flamingo-teaser': renderPlatformIcon('flamingo-teaser', 'h-5 w-5'),\n  'marketing-hub': renderPlatformIcon('marketing-hub', 'h-5 w-5'),\n  'product-hub': renderPlatformIcon('product-hub', 'h-5 w-5'),\n  'revenue-hub': renderPlatformIcon('revenue-hub', 'h-5 w-5'),\n  'people-hub': renderPlatformIcon('people-hub', 'h-5 w-5'),\n  'company-hub': renderPlatformIcon('company-hub', 'h-5 w-5'),\n  tmcg: renderPlatformIcon('tmcg', 'h-5 w-5'),\n  mlg: renderPlatformIcon('mlg', 'h-5 w-5'),\n  universal: renderPlatformIcon('universal', 'h-5 w-5'),\n};\n\n// Identity + brand strings/colours live in ONE place; re-exported here so every\n// existing importer of `platform-config` keeps working unchanged.\nexport {\n  platformDisplayNames,\n  platformShortNames,\n  platformDescriptions,\n  platformSlogans,\n  platformIconNames,\n  platformHexColors,\n  platformColors,\n  getPlatformDisplayName,\n  getPlatformShortName,\n  getPlatformDescription,\n  getPlatformSlogan,\n  getPlatformColor,\n  getDefaultColorForPlatform,\n  getDefaultIconForPlatform,\n} from './platform-identity';\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: getPlatformIcon(platform.name),\n    color: getPlatformColorClass(platform.name),\n  }));\n}\n\n/** Get platform icon by name (5x5). */\nexport function getPlatformIcon(platformName: string): React.ReactNode {\n  return platformIcons[platformName as PlatformName] ?? platformIcons.universal;\n}\n\n/** Get small platform icon for filter buttons (4x4). */\nexport function getSmallPlatformIcon(platformName: string): React.ReactNode {\n  return renderPlatformIcon(platformName, 'h-4 w-4 flex-shrink-0');\n}\n\n/** Get platform icon for admin/selector components (standard 6x6 size). */\nexport function getPlatformIconComponent(platformName: string, className: string = 'h-6 w-6'): React.ReactNode {\n  return renderPlatformIcon(platformName, className);\n}\n\n// `getPlatformColor` is re-exported from the identity leaf above; this local alias\n// keeps `transformPlatformConfigsToOptions` readable without shadowing that export.\nfunction getPlatformColorClass(platformName: string): string {\n  return getPlatformBrandClasses(platformName).accentBg;\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 { type ComponentProps, type ReactNode, useState } from 'react';\nimport { Toaster as SonnerToaster, toast as sonnerToast } from 'sonner';\nimport type { ExternalToast } from 'sonner';\nimport type { ToolType } from '../../types/tool.types';\nimport { cn } from '../../utils/cn';\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';\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?: ReactNode;\n  description?: 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 className={cn('relative flex w-full items-start gap-2 overflow-hidden bg-ods-card p-3', className)}>\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\">\n        {title ? (\n          <p\n            className=\"truncate pr-5 text-ods-text-primary text-h4\"\n            title={typeof title === 'string' ? title : undefined}\n          >\n            {title}\n          </p>\n        ) : null}\n        {description ? (\n          <p\n            className=\"line-clamp-3 text-ods-text-secondary text-h6\"\n            title={typeof description === 'string' ? description : undefined}\n          >\n            {description}\n          </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('absolute inset-x-0 bottom-0 h-1 origin-left', progressColorByVariant[variant])}\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?: ReactNode;\n  description?: 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?: ReactNode;\n  description?: ReactNode;\n  command: string;\n  toolType?: ToolType;\n  approvalDescription?: 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] = 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 text-ods-text-primary text-h6\" 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 ? <p className=\"text-ods-text-secondary text-h6\">{approvalDescription}</p> : 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 text-ods-text-on-accent transition-colors text-h5 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 text-ods-text-primary transition-colors text-h5 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 text-ods-text-primary transition-colors text-h6 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 = ComponentProps<typeof SonnerToaster>;\n\nexport function Toaster({ position = 'bottom-right', offset = 24, gap = 8, toastOptions, ...rest }: 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?: ReactNode;\n  description?: ReactNode;\n  variant?: ToastVariant;\n}\n\nexport function showToast(options: ShowToastOptions | string) {\n  const opts: ShowToastOptions = typeof options === 'string' ? { title: options } : options;\n\n  const { title, description, variant = 'default', duration = 4000, dismissible = true, ...rest } = 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'>, 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","import { useState, useCallback, useEffect } from 'react';\nimport { useRequiredEndpointsRuntime } from '../contexts/endpoints-runtime-context';\nimport { useRouter } from '../embed-shims/next-navigation';\nimport { apiErrorMessage } from '../utils/common';\nimport { contentFetch } from '../utils/embed-content-fetch';\nimport { useToast } from './use-toast';\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(\n    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        // Read the body ONLY on failure — nothing here consumes the success\n        // payload, and parsing it meant a 2xx with an empty (or non-JSON)\n        // body threw and toasted a failure for a message that was delivered.\n        if (!response.ok) {\n          const failure: unknown = await response.json().catch(() => null);\n          throw new Error(apiErrorMessage(failure, '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    },\n    [isSubmitting, toast, userId, successToastMessage, contactUrl],\n  );\n\n  // Handle redirect after success\n  useEffect(() => {\n    if (!isSuccess || !successRedirectUrl) return undefined;\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  }, [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","'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 {\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\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  // Revoke on UNMOUNT only, reading the latest map through a ref.\n  //\n  // This used to be the cleanup of the fetch effect below, closing over\n  // `fetchedImages` from the render the effect happened to run in. Two bugs\n  // fell out of that: URLs fetched after that render were never revoked\n  // (a leak), and a change to `imageUrls`/`config` revoked the whole captured\n  // set while state still pointed at those blobs — every image on screen broke.\n  const fetchedImagesRef = useRef(fetchedImages);\n  useEffect(() => {\n    fetchedImagesRef.current = fetchedImages;\n  }, [fetchedImages]);\n  useEffect(\n    () => () => {\n      Object.values(fetchedImagesRef.current).forEach(blobUrl => {\n        if (blobUrl && blobUrl.startsWith('blob:')) {\n          URL.revokeObjectURL(blobUrl);\n        }\n      });\n    },\n    [],\n  );\n\n  useEffect(() => {\n    // An empty `imageUrls` used to wipe `fetchedImages` back to `{}` here. That\n    // was wrong three ways, and removing it fixes all three:\n    //  * `requestedUrls` was NOT cleared alongside it, so once a url came back\n    //    it was filtered out as \"already requested\" and never re-fetched — the\n    //    image stayed permanently blank;\n    //  * the wiped blob URLs were dropped from the map the unmount cleanup\n    //    revokes from, so they leaked;\n    //  * it was a synchronous setState in an effect body, i.e. a second render\n    //    pass for a map nobody is indexing into (there are no urls to look up).\n    // The map is a url-keyed cache: stale entries are unreachable, not stale\n    // data, and it is bounded by the same url set `requestedUrls` already holds.\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 => {\n      requestedUrls.current.add(url);\n    });\n\n    batchFetchAuthenticatedImages(urlsToFetch, config)\n      .then(newResults => {\n        setFetchedImages(prev => ({ ...prev, ...newResults }));\n      })\n      .catch((error: unknown) => {\n        console.error('Failed to batch fetch images:', error);\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  // Result of the LAST completed attempt, tagged with the cache key it belongs\n  // to. Tagging is what lets `isLoading` / `error` be derived below instead of\n  // being written from the effect: \"still loading\" is simply \"nothing settled\n  // for the key we are currently asked about\", which the render already knows.\n  const [settled, setSettled] = useState<{ key: string; blobUrl?: string; error?: string } | null>(null);\n  const currentCacheKeyRef = useRef<string | null>(null);\n\n  const { tenantHostUrl, enableDevMode, accessTokenKey } = {\n    ...getImageConfig(),\n    ...config,\n  };\n\n  // Construct full image URL. Pure string work on the arguments — it belongs in\n  // render so both the effect and the derived result below agree on one key.\n  let fullImageUrl: string | null = null;\n  if (imageUrl) {\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\n  // Create cache key (use refreshKey if provided, otherwise no cache buster for caching)\n  const cacheKey = fullImageUrl === null ? null : refreshKey ? `${fullImageUrl}?v=${refreshKey}` : fullImageUrl;\n\n  useEffect(() => {\n    if (cacheKey === null) {\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    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      // Ref-counting only — the blob is already on screen, because the render\n      // below reads the same cache. Publishing it from here instead would mean\n      // a cache HIT still cost a spinner frame plus a second render pass.\n      cachedEntry.refCount++;\n      cachedEntry.timestamp = Date.now();\n      return;\n    }\n\n    const pendingRequest = pendingRequests.get(cacheKey);\n    if (pendingRequest) {\n      pendingRequest\n        .then(blobUrl => {\n          const entry = blobUrl ? imageCache.get(cacheKey) : undefined;\n          if (entry) entry.refCount++;\n          // Settled either way: a request that resolves without a usable blob\n          // still ends the spinner, exactly as the old `setIsLoading(false)` in\n          // the non-`if` tail of this handler did.\n          setSettled({ key: cacheKey, blobUrl: entry ? blobUrl : undefined });\n        })\n        .catch((err: unknown) => {\n          setSettled({ key: cacheKey, error: err instanceof Error ? err.message : 'Failed to fetch image' });\n        });\n      return;\n    }\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 {\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        setSettled({ key: cacheKey, blobUrl: objectUrl });\n        return objectUrl;\n      })\n      .catch((err: unknown) => {\n        setSettled({ key: cacheKey, error: err instanceof Error ? err.message : 'Failed to fetch image' });\n        throw err;\n      })\n      .finally(() => {\n        pendingRequests.delete(cacheKey);\n      });\n\n    pendingRequests.set(cacheKey, fetchPromise);\n    // Keyed on the derived `cacheKey` and the two PRIMITIVE config fields the\n    // request itself needs — not on the `config` OBJECT. Callers pass `config`\n    // inline, so a fresh identity every render used to re-run this effect on\n    // every render, and each re-run incremented `refCount` again on a cache hit\n    // (see above). Entries whose count never returned to 0 were never eligible\n    // for `cleanupImageCache`, so their blob URLs leaked for the whole session.\n  }, [cacheKey, fullImageUrl, refreshKey, enableDevMode, accessTokenKey]);\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  // Everything below is derived, not stored.\n  //\n  // `active` is the settled result for the key we are being asked about right\n  // now; a stale one (the caller bumped `refreshKey`) does not count as an\n  // answer. The cache is consulted directly so a HIT renders the blob on the\n  // very first pass. The final `settled?.blobUrl` fallback keeps the previously\n  // resolved image on screen while a new `refreshKey` reloads it — the old code\n  // got that for free by never clearing `fetchedImageUrl` on a key change.\n  const active = settled && settled.key === cacheKey ? settled : null;\n  const cachedBlobUrl = cacheKey === null ? undefined : imageCache.get(cacheKey)?.blobUrl;\n  const resolvedBlobUrl = active?.blobUrl ?? cachedBlobUrl ?? settled?.blobUrl;\n\n  return {\n    imageUrl: cacheKey === null ? undefined : resolvedBlobUrl,\n    isLoading: cacheKey !== null && active === null && cachedBlobUrl === undefined,\n    error: (cacheKey === null ? undefined : active?.error) ?? null,\n  };\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 type { DocumentNode } from 'graphql';\nimport { useEffect, useState, useMemo, useCallback, useRef } from 'react';\nimport { useRouter, useSearchParams } from '../../embed-shims/next-navigation';\n\nimport { flattenQueryVariables, mergeDefaults, validateSchema, type FlattenedParam } from './flatten-schema';\nimport { extractVariablesFromQuery } from './graphql-parser';\nimport { introspector } from './introspection';\nimport {\n  urlParamsToVariables,\n  variablesToUrlParams,\n  mergeVariables,\n  clearParams,\n  type GraphQLVariables,\n} from './url-converter';\n\n/**\n * Options for useQueryParams hook\n */\nexport interface UseQueryParamsOptions {\n  /** Default values for parameters */\n  defaultValues?: GraphQLVariables;\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 = GraphQLVariables> {\n  /** GraphQL variables ready for Apollo Client */\n  variables: TVariables;\n\n  /** Raw URL parameters (before conversion to variables). Repeated keys\n   *  collapse into an array, in URL order. */\n  params: Record<string, string | string[]>;\n\n  /** Flattened parameter schema */\n  schema: Record<string, FlattenedParam>;\n\n  /** Set a single parameter */\n  setParam: (key: string, value: unknown) => void;\n\n  /** Set multiple parameters at once */\n  setParams: (params: GraphQLVariables) => 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 = GraphQLVariables>(\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. Memoised: `|| {}` minted a new object on every\n  // render, so every memo and effect keyed on it re-ran every time.\n  const defaultValues = useMemo(() => options.defaultValues || {}, [options.defaultValues]);\n  const skipIntrospection = options.skipIntrospection || false;\n  const debug = options.debug || false;\n\n  // Latest option bag for the one-time initialiser below. These cannot be\n  // dependencies: `introspectionHeaders`, `paramMapping` and `defaultValues`\n  // are almost always object literals, so as deps they would re-run schema\n  // introspection on every render of the calling component.\n  const initSourceRef = useRef({\n    introspectionHeaders: options.introspectionHeaders,\n    paramMapping: options.paramMapping,\n    defaultValues,\n  });\n  useEffect(() => {\n    initSourceRef.current = {\n      introspectionHeaders: options.introspectionHeaders,\n      paramMapping: options.paramMapping,\n      defaultValues,\n    };\n  });\n\n  // Initialize: Parse query + fetch schema (once)\n  useEffect(() => {\n    // `initialize` awaits introspection and schema flattening, so a `query`\n    // change can start a second run while the first is still in flight. Without\n    // this guard the SUPERSEDED run's `setSchema`/`setIsReady` could land last\n    // and leave the hook parsing the URL against the PREVIOUS query's schema —\n    // params for the new query then silently drop or coerce to the wrong type.\n    let cancelled = false;\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 =\n            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, initSourceRef.current.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). Synchronous — the\n        // schema fetch it depends on was already awaited in step 2 above.\n        let flattenedSchema = flattenQueryVariables(queryVariables, introspector);\n\n        // Apply custom param mapping if provided\n        const { paramMapping, defaultValues: initialDefaults } = initSourceRef.current;\n        if (paramMapping) {\n          flattenedSchema = applyParamMapping(flattenedSchema, paramMapping);\n        }\n\n        // Merge default values\n        flattenedSchema = mergeDefaults(flattenedSchema, initialDefaults);\n\n        // Validate schema\n        validateSchema(flattenedSchema);\n\n        if (debug) {\n          console.log('[useQueryParams] Flattened schema:', flattenedSchema);\n        }\n\n        if (cancelled) return;\n        setSchema(flattenedSchema);\n        setIsReady(true);\n      } catch (err) {\n        if (cancelled) return;\n        const initError = err instanceof Error ? err : new Error(String(err));\n        console.error('[useQueryParams] Initialization failed:', initError);\n        setError(initError);\n      } finally {\n        if (!cancelled) setIsLoading(false);\n      }\n    }\n\n    // Never rejects — settles every failure into `error` state.\n    void initialize();\n    return () => {\n      cancelled = true;\n    };\n  }, [query, options.introspectionEndpoint, skipIntrospection, debug]);\n\n  // Convert URL params to GraphQL variables. Kept as the plain record shape\n  // internally (that is what the converters take); the caller-chosen\n  // `TVariables` view is applied once, at the returned value.\n  const variables = useMemo<GraphQLVariables>(() => {\n    if (!isReady) {\n      return defaultValues;\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;\n    } catch (err) {\n      console.error('[useQueryParams] Failed to convert URL to variables:', err);\n      return defaultValues;\n    }\n  }, [searchParams, schema, isReady, defaultValues, debug]);\n\n  // Raw URL params (before conversion)\n  const params = useMemo(() => {\n    const result: Record<string, string | string[]> = {};\n    searchParams.forEach((value, key) => {\n      const existing = result[key];\n      if (existing === undefined) {\n        result[key] = value;\n      } else if (Array.isArray(existing)) {\n        // Multiple values - append to the array\n        existing.push(value);\n      } else {\n        result[key] = [existing, value];\n      }\n    });\n    return result;\n  }, [searchParams]);\n\n  // Update URL with new parameters\n  const updateUrl = useCallback(\n    (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    },\n    [router, debug],\n  );\n\n  // Set a single parameter\n  const setParam = useCallback(\n    (key: string, value: unknown) => {\n      if (!isReady) {\n        console.warn('[useQueryParams] Schema not ready, cannot set param');\n        return;\n      }\n\n      try {\n        const updated = mergeVariables(variables, { [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    },\n    [variables, schema, isReady, updateUrl],\n  );\n\n  // Set multiple parameters\n  const setParams = useCallback(\n    (updates: GraphQLVariables) => {\n      if (!isReady) {\n        console.warn('[useQueryParams] Schema not ready, cannot set params');\n        return;\n      }\n\n      try {\n        const updated = mergeVariables(variables, 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    },\n    [variables, schema, isReady, updateUrl],\n  );\n\n  // Clear specific parameters\n  const clearParamsHandler = useCallback(\n    (keys: string[]) => {\n      if (!isReady) {\n        console.warn('[useQueryParams] Schema not ready, cannot clear params');\n        return;\n      }\n\n      try {\n        const updated = clearParams(variables, 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    },\n    [variables, schema, isReady, updateUrl],\n  );\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    // The only place the caller's `TVariables` view is applied — this hook\n    // builds the object from the URL, so its exact shape is the caller's\n    // claim about their own query, not something we can verify here.\n    variables: variables as TVariables,\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 * 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 type { JSType } from '../../utils/search-params';\nimport type { VariableDefinition } from './graphql-parser';\nimport type { 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?: unknown;\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=rmm&cursor=abc\n */\nexport function flattenQueryVariables(\n  queryVariables: Record<string, VariableDefinition>,\n  introspector: GraphQLIntrospector,\n): 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, unknown>,\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 param of Object.values(schema)) {\n    if (urlParamNames.has(param.urlParamName)) {\n      throw new Error(`[FlattenSchema] Conflict: Multiple parameters map to URL param \"${param.urlParamName}\"`);\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// THE omission rule lives in the search-params leaf (the server serializer and\n// this flattened-schema path must agree); re-exported for existing importers.\nexport { shouldIncludeInUrl } from '../../utils/search-params';\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 { type DocumentNode, type VariableDefinitionNode, type TypeNode, visit } from 'graphql';\n\n// `JSType` is OWNED by the search-params leaf (server and client share it).\n// Imported, not re-exported: a second export site is a second place to look\n// when you want to know where the type lives.\nimport type { JSType } from '../../utils/search-params';\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(query: DocumentNode): 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  let current: TypeNode = typeNode;\n\n  // Unwrap NonNullType wrapper\n  if (current.kind === 'NonNullType') {\n    isNonNull = true;\n    current = current.type;\n  }\n\n  // Handle ListType\n  if (current.kind === 'ListType') {\n    isList = true;\n    current = current.type;\n\n    // ListType can also be wrapped in NonNullType\n    if (current.kind === 'NonNullType') {\n      current = current.type;\n    }\n  }\n\n  // Extract the base type name\n  if (current.kind === 'NamedType') {\n    typeName = current.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 * 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  type IntrospectionQuery,\n  type GraphQLSchema,\n  type GraphQLInputType,\n  isInputObjectType,\n} from 'graphql';\nimport type { JSType } from '../../utils/search-params';\nimport type { VariableDefinition } 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(endpoint: string, headers: Record<string, string> = {}, skipCache = false): 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      // Standard GraphQL execution result. `data` carries the introspection\n      // payload `buildClientSchema` expects; `errors`, when present, is only\n      // ever stringified for the thrown message.\n      const { data, errors } = (await response.json()) as {\n        data?: IntrospectionQuery;\n        errors?: readonly unknown[];\n      };\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: GraphQLInputType = 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    // Only NAMED types carry `name`; list/non-null wrappers fall back to\n    // stripping the wrapper punctuation off their printed form.\n    const named = 'name' in current ? current.name : undefined;\n    typeName = named || 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 * 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 { positiveInt } from '../../utils/search-params';\nimport type { JSType } from '../../utils/search-params';\nimport { type FlattenedParam, shouldIncludeInUrl } from './flatten-schema';\n\n/** A single URL-decodable value. */\nexport type UrlScalar = string | number | boolean | null;\n/** What `coerceValue` can produce for one URL parameter. */\nexport type UrlParamValue = UrlScalar | UrlScalar[];\n\n/** GraphQL variables object — nested, values are opaque to this module. */\nexport type GraphQLVariables = Record<string, unknown>;\n\n/** Narrow an unknown to something indexable by string key. Arrays qualify\n *  (path segments may address array indices), `null` does not. */\nfunction isIndexable(value: unknown): value is Record<string, unknown> {\n  return typeof value === 'object' && value !== null;\n}\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): GraphQLVariables {\n  const variables: GraphQLVariables = {};\n\n  for (const [paramName, paramConfig] of Object.entries(schema)) {\n    // Read value from URL\n    const rawValue =\n      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: GraphQLVariables,\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): UrlParamValue {\n  // Array handling — each element is coerced as a scalar ('array' degrades\n  // to 'string', same as before).\n  if (Array.isArray(value)) {\n    return value.map(v => coerceScalar(v, type === 'array' ? 'string' : type));\n  }\n\n  // Single value under an array-typed param becomes a one-element array.\n  if (type === 'array') return [value];\n\n  return coerceScalar(value, type);\n}\n\n/** Scalar half of {@link coerceValue}. `'array'` never reaches here. */\nfunction coerceScalar(value: string, type: JSType): UrlScalar {\n  switch (type) {\n    case 'number': {\n      const num = parseFloat(value);\n      return isNaN(num) ? null : num;\n    }\n\n    case 'int':\n      // THE integer grammar has ONE owner. This used to inline its own copy,\n      // which then drifted (the shared one honours a configured `max`).\n      // `positiveInt` returns the fallback for anything unparseable, and this\n      // path has no schema config to read a default from, so `null` it is.\n      return positiveInt<null>(value, null);\n\n    case 'boolean':\n      return value === 'true' || value === '1';\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: GraphQLVariables, path: string, value: unknown): 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    const existing = current[part];\n    // `null` counts as \"not an object\" here — the old `typeof x === 'object'`\n    // check let a null segment through and then dereferenced it.\n    const next = isIndexable(existing) ? existing : {};\n    if (next !== existing) current[part] = next;\n    current = next;\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: unknown, path: string): unknown {\n  return path.split('.').reduce<unknown>((current, key) => {\n    return isIndexable(current) ? current[key] : undefined;\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: GraphQLVariables,\n  updates: Record<string, unknown>,\n  schema: Record<string, FlattenedParam>,\n): GraphQLVariables {\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: GraphQLVariables,\n  paramNames: string[],\n  schema: Record<string, FlattenedParam>,\n): GraphQLVariables {\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(variables: GraphQLVariables, schema: Record<string, FlattenedParam>): 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(`Parameter \"${paramName}\" has wrong type: expected ${expectedType}, got ${actualType}`);\n      }\n    }\n  }\n\n  return errors;\n}\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 { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport { useRouter, useSearchParams } from '../../embed-shims/next-navigation';\nimport { createSearchParams, parseSchemaParams, type ParamSchema, type JSType } from '../../utils/search-params';\nimport { type FlattenedParam, shouldIncludeInUrl } from './flatten-schema';\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  // React's \"adjust state while rendering\" pattern rather than a ref written\n  // during render: the cache must not be updated by a render attempt React\n  // discards (concurrent interruption, error retry), or a value that never\n  // committed would be handed to every later render as the stable one. The\n  // extra render pass only happens when the content key actually changes,\n  // which for the one caller (the param schema) is essentially never after\n  // mount.\n  const [cached, setCached] = useState<{ value: T; key: string }>({ value, key });\n  if (cached.key !== key) setCached({ value, key });\n  // Same object either way: the pass that stores `value` also returns it.\n  return cached.key === key ? cached.value : 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>(prev: unknown, next: T[]): 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/** Cached params snapshot plus the raw parse it was derived from. */\ninterface ParamsCache {\n  src: Record<string, unknown>;\n  value: Record<string, unknown>;\n}\n\n/**\n * Copy of `next` in which every array field whose content matches `prev`'s\n * keeps `prev`'s instance. Returns `next` untouched when nothing was reusable,\n * so an unrelated change costs no extra allocation.\n */\nfunction reuseArrayRefs(prev: Record<string, unknown>, next: Record<string, unknown>): Record<string, unknown> {\n  let merged: Record<string, unknown> | undefined;\n  for (const [key, value] of Object.entries(next)) {\n    if (!Array.isArray(value)) continue;\n    const reused = reuseIfShallowEqual(prev[key], value as (string | number | boolean)[]);\n    if (reused === value) continue;\n    merged ??= { ...next };\n    merged[key] = reused;\n  }\n  return merged ?? next;\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  int: number;\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  int: number | 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// Schema types + the schema helper live in the search-params leaf, so the hook's\n// `params` ARE the same parsed contract a server route produces for that URL.\nexport type { ParamConfig, ParamSchema } from '../../utils/search-params';\nexport { defineParamSchema } from '../../utils/search-params';\n\n/**\n * Options for useApiParams hook\n */\nexport interface UseApiParamsOptions {\n  /** Enable debug logging */\n  debug?: boolean;\n  /**\n   * What an ABSENT scalar with no declared `default` reads as. Passed straight\n   * through to `parseSchemaParams`, so the hook's `params` ARE the same parsed\n   * contract the server produces for the same URL.\n   */\n  absent?: 'undefined' | 'null';\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 ? (T extends JSType ? InputTypeForJSType<T> : never) : 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 | number | boolean | string[] | (string | null | undefined)[] | Record<string, unknown> | null | undefined;\n\n/**\n * Return type for useApiParams hook with strict typing\n */\nexport interface UseApiParamsReturn<TSchema extends ParamSchema, TParams = InferParamsFromSchema<TSchema>> {\n  /** Parsed parameters object with strict typing */\n  params: TParams;\n\n  /**\n   * `params` of the last write this hook issued, or `params` itself when no\n   * write is in flight. The URL is authoritative; this is the latest INTENT.\n   */\n  pendingParams: 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>(key: K, value: InferInputParamsFromSchema<Pick<TSchema, K>>[K]) => 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  // Serialized query strings this hook has written and not yet seen committed.\n  const pendingRef = useRef<string[]>([]);\n  // The queue's TAIL, mirrored into state: `pendingParams` is render output, and\n  // reading a ref during render is both a lint error and a correctness hazard\n  // (the value would not re-render the consumer). The ref stays the mutable\n  // queue the commit effect drains; this is the one part render may see.\n  const [pendingTail, setPendingTail] = useState<string | undefined>(undefined);\n  const syncPendingTail = useCallback(() => {\n    setPendingTail(pendingRef.current[pendingRef.current.length - 1]);\n  }, []);\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  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  }, [stableSchema]);\n\n  // Parse URL parameters. PURE — it reads nothing but its own dependencies, so\n  // a render attempt React discards leaves no trace.\n  //\n  // THE reader, and the SAME one `pendingParams` uses. This used to walk the\n  // schema itself through `coerceValue`, which knows only a param's TYPE — so\n  // `min`/`max`/`default` were applied to the pending params and not to the\n  // committed ones. `?pageSize=9999` then read as 9999 here and 100 there, and\n  // `?page=abc` as `null` here and `1` there, which is enough to desynchronize\n  // any adapter that compares the two.\n  const rawParams = useMemo((): Record<string, unknown> => {\n    const result = parseSchemaParams(stableSchema, new URLSearchParams(searchString), {\n      absent: options.absent === 'null' ? 'null' : 'undefined',\n    }) as Record<string, unknown>;\n\n    if (debug) {\n      console.log('[useApiParams] Parsed params:', result);\n    }\n\n    return result;\n  }, [searchString, debug, stableSchema, options.absent]);\n\n  // Carry the previously COMMITTED array instances forward when their content\n  // is unchanged, so `params.<arrayField>` stays reference-stable across a URL\n  // change that touched some OTHER param. Held in state and updated with\n  // React's \"adjust state while rendering\" pattern rather than in a ref: the\n  // carry-forward source must be a params object that actually committed, or a\n  // discarded render attempt would seed it with array instances no consumer\n  // ever saw. The extra render pass only runs when `rawParams` is recomputed,\n  // i.e. when the URL or the schema really changed.\n  const [paramsCache, setParamsCache] = useState<ParamsCache>(() => ({ src: rawParams, value: rawParams }));\n  let stableParams = paramsCache.value;\n  if (paramsCache.src !== rawParams) {\n    stableParams = reuseArrayRefs(paramsCache.value, rawParams);\n    setParamsCache({ src: rawParams, value: stableParams });\n  }\n  const params = stableParams as InferParamsFromSchema<TSchema>;\n\n  // Helper: Add parameter value to URLSearchParams.\n  //\n  // Delegates to `createSearchParams`, which owns the ENCODING rule (skip\n  // null/undefined/'', repeat the key per array element, JSON objects,\n  // `String()` scalars). The rule used to be spelled out here as well; the two\n  // sat side by side in one file, and hoisting the shared one into the leaf\n  // put them in different modules where drift is much easier.\n  const addParamToSearchParams = useCallback((searchParams: URLSearchParams, key: string, value: ParamValue): void => {\n    for (const [k, v] of createSearchParams({ [key]: value })) searchParams.append(k, v);\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  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, flattenedSchema, addParamToSearchParams, stableSchema]);\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  const updateUrl = useCallback(\n    (newParams: URLSearchParams, keysToRemove: string[] = []) => {\n      // Base the write on the LAST WRITE WE ISSUED, not on the committed URL.\n      // Next commits each `router.replace` as its own navigation, so two writes\n      // fired before the first commits would both rebase on the pre-first URL\n      // and the first one's keys would be lost.\n      const base = pendingRef.current[pendingRef.current.length - 1] ?? searchString;\n      const finalParams = new URLSearchParams(base);\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 merged = finalParams.toString();\n\n      // A NO-OP write (re-selecting the current option, clearing an already\n      // empty picker) must not enter the queue: `router.replace` with an\n      // identical URL produces no commit, so the entry would never drain and\n      // every later write would rebase on a phantom.\n      if (merged === base) return;\n\n      pendingRef.current = [...pendingRef.current, merged];\n      syncPendingTail();\n\n      const url = merged ? `?${merged}` : 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    },\n    [router, debug, searchString, stableSchema, syncPendingTail],\n  );\n\n  // Drain the queue as commits arrive. A commit EQUAL to a queued write drops\n  // that entry and everything before it (so `[A, B, A']` survives A's commit\n  // with `[B, A']` intact); a commit matching NOTHING is a FOREIGN navigation\n  // (Back/Forward, another hook's replace) and invalidates the whole base.\n  useEffect(() => {\n    const queue = pendingRef.current;\n    if (queue.length === 0) return;\n    const hit = queue.indexOf(searchString);\n    pendingRef.current = hit === -1 ? [] : queue.slice(hit + 1);\n    syncPendingTail();\n  }, [searchString, syncPendingTail]);\n\n  // Set a single parameter\n  const setParam = useCallback(\n    <K extends keyof TSchema & string>(key: K, value: InferInputParamsFromSchema<Pick<TSchema, K>>[K]) => {\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      // THE omission rule, the same one `setParams` uses. This branch moved\n      // `setParams` onto `shouldIncludeInUrl` (so a value equal to its schema\n      // default is dropped) and left `setParam` on a local emptiness check\n      // that knew nothing about defaults — so `setParam('page', 1)` kept\n      // `?page=1` on a URL `setParams({ page: 1 })` left clean.\n      if (shouldIncludeInUrl(value, config)) {\n        addParamToSearchParams(newParams, key, value);\n        updateUrl(newParams);\n      } else {\n        updateUrl(newParams, [key]);\n      }\n    },\n    [updateUrl, addParamToSearchParams, stableSchema],\n  );\n\n  // Set multiple parameters\n  const setParams = useCallback(\n    (updates: Partial<InferInputParamsFromSchema<TSchema>>) => {\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        // The SAME omission rule the reader uses (`urlSearchParams`): empty\n        // values AND values equal to the schema default drop out. Writing a\n        // default back into the URL left `?page=1&pageSize=15` on a pristine\n        // board and made the canonical URL un-shareable.\n        if (shouldIncludeInUrl(value, flattenedSchema[key])) {\n          addParamToSearchParams(newParams, key, value);\n        } else {\n          keysToRemove.push(key);\n        }\n      }\n\n      updateUrl(newParams, keysToRemove);\n    },\n    [updateUrl, addParamToSearchParams, stableSchema, flattenedSchema],\n  );\n\n  // Clear specific parameters\n  const clearParams = useCallback(\n    (keys: (keyof TSchema & string)[]) => {\n      const newParams = new URLSearchParams();\n      updateUrl(newParams, keys);\n    },\n    [updateUrl],\n  );\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  // The params of the LAST write we issued, or `params` when nothing is in\n  // flight. Adapters read this as \"the user's latest intent\" so a decision made\n  // while a `router.replace` is uncommitted compares against the click, not the\n  // stale URL.\n  const pendingParams = useMemo((): InferParamsFromSchema<TSchema> => {\n    if (pendingTail === undefined || pendingTail === searchString) return params;\n    return parseSchemaParams(stableSchema, new URLSearchParams(pendingTail), {\n      absent: options.absent === 'null' ? 'null' : 'undefined',\n    }) as InferParamsFromSchema<TSchema>;\n  }, [pendingTail, searchString, params, stableSchema, options.absent]);\n\n  return {\n    params,\n    pendingParams,\n    urlSearchParams,\n    setParam,\n    setParams,\n    clearParams,\n    resetParams,\n  };\n}\n\n// THE serializer lives in the search-params leaf (one array/object/omission\n// rule for the hook and for server code); re-exported for existing importers.\nexport { createSearchParams } from '../../utils/search-params';\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(options: UseCursorPaginationStateOptions): CursorPaginationStateReturn {\n  const { onInitialLoad, onSearchChange } = 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  // Latest values for the effects below, none of which may take them as\n  // dependencies: the URL->input sync would revert the user's keystrokes the\n  // moment they typed, and the mount-once initial load would re-fire on every\n  // param change. Declared first so the sync runs before either of them.\n  const searchInputRef = useRef(searchInput);\n  const onInitialLoadRef = useRef(onInitialLoad);\n  const paramsRef = useRef(params);\n  useEffect(() => {\n    searchInputRef.current = searchInput;\n    onInitialLoadRef.current = onInitialLoad;\n    paramsRef.current = params;\n  });\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 !== searchInputRef.current) {\n      isSyncingFromUrl.current = true;\n      setSearchInput(urlSearch);\n      // Reset the flag after a tick to allow normal operation\n      setTimeout(() => {\n        isSyncingFromUrl.current = false;\n      }, 0);\n    }\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 = paramsRef.current.cursor || null;\n      const search = paramsRef.current.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      // `.catch` BEFORE `.finally` so a rejecting `onInitialLoad` is reported\n      // instead of becoming an unhandled rejection, while the unblock below\n      // still runs on both paths (it must: otherwise every other effect in\n      // this hook stays permanently gated behind `isInitialLoadInProgress`).\n      void Promise.resolve(onInitialLoadRef.current(search, cursor))\n        .catch((err: unknown) => {\n          console.error('[useCursorPaginationState] onInitialLoad failed:', err);\n        })\n        .finally(() => {\n          isInitialLoadInProgress.current = false;\n          setInitialLoadCount(1);\n        });\n    }\n  }, [initialLoadCount]);\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` is declared `void | Promise<unknown>`; normalise so a\n      // rejecting async implementation is logged rather than left unhandled.\n      void Promise.resolve(onSearchChange(currentSearch)).catch((err: unknown) => {\n        console.error('[useCursorPaginationState] onSearchChange failed:', err);\n      });\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  const [statusClient, setStatusClient] = useState(client);\n\n  // Reset while rendering (React's documented prop-sync pattern) rather than\n  // from the effect below. `client.onStatus` does NOT replay the current status\n  // to a new listener, so a swapped-in client emits nothing until it actually\n  // changes state — leaving `status`/`lastError` describing the PREVIOUS\n  // client. A fresh client from `createNatsClient` is genuinely disconnected\n  // and error-free at this point, so that is what we report until it says\n  // otherwise.\n  if (statusClient !== client) {\n    setStatusClient(client);\n    setStatus('disconnected');\n    setLastError(null);\n  }\n\n  useEffect(() => {\n    if (!client) return undefined;\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: unknown) => {\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","'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 { validateAccessCode, consumeAccessCode, validateAndConsumeAccessCode } 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 type { AccessCodeValidationResponse, AccessCodeConsumptionResponse } from '../types/access-code-cohorts';\nimport { apiErrorMessage } from './common';\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 }),\n    });\n\n    if (!response.ok) {\n      const failure: unknown = await response.json().catch(() => null);\n      throw new Error(apiErrorMessage(failure, '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 }),\n    });\n\n    if (!response.ok) {\n      const failure: unknown = await response.json().catch(() => null);\n      throw new Error(apiErrorMessage(failure, '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 { useEffect, useState } from 'react';\nimport { useEndpointsRuntime } from '../contexts/endpoints-runtime-context';\nimport { toClaudeMirrorPath } from '../utils/embed-url-converters';\n\nexport type ClaudeMirrorStatus =\n  /** No mirror candidate at all (no proxy configured, or a non-mirrorable url). */\n  | 'absent'\n  /** Probing the host for a self-hosted mirror (or minting one on first view). */\n  | 'probing'\n  /** A mirror exists and `src` points at it. */\n  | 'found';\n\nexport interface ClaudeMirrorState {\n  /** The mirror src when found (with a cache-bust once a re-publish is\n   *  detected), else `null`. */\n  src: string | null;\n  status: ClaudeMirrorStatus;\n}\n\n/**\n * Ask the host's storage-view proxy whether the cached mirror is still the\n * artifact's CURRENT version, re-minting it silently if a re-publish moved\n * it. Returns a cache-bust token (the new version) when the mirror changed,\n * else null. Best-effort: any failure just leaves the shown mirror in place.\n */\nasync function revalidateMirror(mirrorPath: string): Promise<string | null> {\n  try {\n    const res = await fetch(`${mirrorPath}?revalidate=1`);\n    if (!res.ok) return null;\n    const body = (await res.json()) as { updated?: boolean; version?: string | null };\n    return body?.updated ? String(body.version ?? Date.now()) : null;\n  } catch {\n    return null;\n  }\n}\n\n/**\n * The host's SELF-HOSTED MIRROR src for a claude artifact url, plus a\n * `status` so the viewer can show a loading state while it resolves — THE\n * hook behind `ClaudeEmbed`'s transparent mirror leg, extracted so the\n * component stays presentational.\n *\n * Fully under the hood, three moves:\n *   1. Derive the candidate mirror path from the url's artifact id under the\n *      host's configured storage-view proxy base\n *      (`EndpointsRuntime.storageViewBaseUrl`) and probe it with a 1-byte\n *      ranged GET. On a cold first view that probe IS the server-side\n *      self-heal (~1-2s), which is exactly why `status: 'probing'` exists —\n *      the viewer shows a spinner instead of the empty \"no embeddable view\"\n *      flash. Exists → `found`; missing / no proxy / blip → `absent`, and the\n *      caller's claude.ai fallback stands (the pre-mirror behavior).\n *   2. Once found, fire ONE background revalidation. If the artifact was\n *      re-published since the mirror was cached, the server re-mints it and\n *      reports the new version; the hook bumps a cache-bust on `src` so the\n *      iframe silently reloads the fresh bytes. Unchanged → nothing happens.\n *\n * A host that mounts no EndpointsRuntime provider (or omits the entry) skips\n * the mirror leg entirely (`absent`).\n */\nexport function useClaudeMirrorSrc(url: string): ClaudeMirrorState {\n  const endpoints = useEndpointsRuntime();\n  const mirrorPath = toClaudeMirrorPath(url, endpoints?.storageViewBaseUrl);\n\n  // Keyed on `mirrorPath` and DERIVED rather than seeded by the effect: when\n  // the url changes, the fallback below is what a reader sees, so a previous\n  // artifact's `found` src can never be shown for a new one. Doing that with a\n  // synchronous `setState` in the effect body is the cascading-render pattern\n  // `react-hooks/set-state-in-effect` rules out; the effect now only settles\n  // state from its async callbacks.\n  const [resolved, setResolved] = useState<{ key: string; value: ClaudeMirrorState } | null>(null);\n  const state: ClaudeMirrorState =\n    resolved && resolved.key === mirrorPath ? resolved.value : { src: null, status: mirrorPath ? 'probing' : 'absent' };\n\n  useEffect(() => {\n    if (!mirrorPath) return undefined;\n    let cancelled = false;\n    const settle = (value: ClaudeMirrorState) => {\n      if (!cancelled) setResolved({ key: mirrorPath, value });\n    };\n    fetch(mirrorPath, { headers: { Range: 'bytes=0-0' } })\n      .then(res => {\n        if (cancelled) return;\n        if (!(res.ok || res.status === 206)) {\n          settle({ src: null, status: 'absent' });\n          return;\n        }\n        settle({ src: mirrorPath, status: 'found' });\n        // Background freshness check — reload in place only if it changed.\n        void revalidateMirror(mirrorPath).then(bust => {\n          if (bust) settle({ src: `${mirrorPath}?v=${encodeURIComponent(bust)}`, status: 'found' });\n        });\n      })\n      .catch(() => {\n        // No proxy on this host / network blip — the caller's claude.ai\n        // fallback stands, same as before mirrors existed.\n        settle({ src: null, status: 'absent' });\n      });\n    return () => {\n      cancelled = true;\n    };\n  }, [mirrorPath]);\n\n  return state;\n}\n","/**\n * Shared utilities for converting external service URLs to embeddable formats.\n * Used by lib's embed components and the hub's admin document editor.\n */\n\nexport function toGoogleSheetsEmbedUrl(url: string): string {\n  if (url.includes('/htmlembed')) return url;\n\n  const match = url.match(/\\/spreadsheets\\/d\\/([a-zA-Z0-9-_]+)/);\n  if (!match) return url;\n\n  const gidMatch = url.match(/[#?&]gid=(\\d+)/);\n  const gid = gidMatch ? gidMatch[1] : '0';\n\n  return `https://docs.google.com/spreadsheets/d/${match[1]}/htmlembed?widget=true&chrome=false&headers=false&gid=${gid}`;\n}\n\nexport function toGoogleSheetsOriginalUrl(url: string): string {\n  const match = url.match(/\\/spreadsheets\\/d\\/([a-zA-Z0-9-_]+)/);\n  if (!match) return url;\n\n  const gidMatch = url.match(/[#?&]gid=(\\d+)/);\n  const gid = gidMatch ? gidMatch[1] : '0';\n\n  return `https://docs.google.com/spreadsheets/d/${match[1]}/edit#gid=${gid}`;\n}\n\n/**\n * Convert a Figma URL to an embeddable URL.\n * Slides/deck URLs map to `deck` (present) by default; `slidesView: 'browse'` switches to `slides` (browse).\n */\nexport function toFigmaEmbedUrl(url: string, opts?: { slidesView?: 'present' | 'browse' }): string {\n  if (url.includes('embed.figma.com')) return url;\n  if (url.includes('figma.com/embed')) return url;\n\n  const match = url.match(/figma\\.com\\/(design|file|proto|board|slides|deck)\\/([a-zA-Z0-9]+)(?:\\/([^?]*))?(\\?.*)?$/);\n\n  if (match) {\n    const [, urlType, fileKey, titleSlug, queryString] = match;\n    const isSlides = urlType === 'slides' || urlType === 'deck';\n    const embedType =\n      urlType === 'proto' ? 'proto' : isSlides ? (opts?.slidesView === 'browse' ? 'slides' : 'deck') : 'design';\n    const pathSuffix = titleSlug ? `/${titleSlug}` : '';\n\n    const params = new URLSearchParams(queryString?.replace(/^\\?/, '') || '');\n    if (!params.has('embed-host')) {\n      params.set('embed-host', 'flamingo');\n    }\n    const clientId = process.env.NEXT_PUBLIC_FIGMA_CLIENT_ID;\n    if (clientId && !params.has('client-id')) {\n      params.set('client-id', clientId);\n    }\n\n    return `https://embed.figma.com/${embedType}/${fileKey}${pathSuffix}?${params.toString()}`;\n  }\n\n  return `https://www.figma.com/embed?embed-host=flamingo&url=${encodeURIComponent(url)}`;\n}\n\nexport function isFigmaSlidesUrl(url: string): boolean {\n  if (!url) return false;\n  return /(?:www\\.|embed\\.)?figma\\.com\\/(?:slides|deck)\\/[a-zA-Z0-9]+/.test(url);\n}\n\nexport function toFigmaOriginalUrl(url: string): string {\n  if (url.includes('embed.figma.com')) {\n    return url.replace('embed.figma.com', 'www.figma.com').replace(/\\?.*$/, '');\n  }\n  return url.replace(/\\?.*$/, '');\n}\n\n/**\n * The self-hosted MIRROR url for a claude artifact url — the whole\n * mirror mechanism is UNDER THE HOOD: the viewer detects a claude url,\n * DERIVES the mirror location from the artifact's own id under the\n * host's configured storage-view proxy base, and frames the downloaded\n * copy from there. No per-link data, no props, no shortcode changes —\n * 100% transparent to every consumer.\n *\n * `storageViewBaseUrl` comes from `EndpointsRuntime` — the SAME\n * host-overridable proxy-path mechanism every other lib→API call uses\n * (a reverse-proxied embedder remaps it; a host without the proxy omits\n * it and gets `null` here, i.e. no mirror leg at all).\n *\n * `null` when the base is absent or the url carries no artifact id. The\n * derived url is the configured base + a validated uuid — safe as a\n * frame source without further checks. KEEP THE OBJECT NAMING IN SYNC\n * with the hub's `lib/data/design-brief-mirror.cjs` (bucket +\n * `<uuid>.html`) — that module owns the ingest/audit half.\n */\nexport function toClaudeMirrorPath(\n  url: string | null | undefined,\n  storageViewBaseUrl: string | null | undefined,\n): string | null {\n  if (!url || !storageViewBaseUrl) return null;\n  try {\n    const u = new URL(url);\n    const host = u.hostname.toLowerCase();\n    const id =\n      (host === 'claude.ai' &&\n        /^\\/(?:code\\/artifact|public\\/artifacts)\\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(?:[/?#]|$)/i.exec(\n          u.pathname,\n        )?.[1]) ||\n      (host === 'claude.site' &&\n        /^\\/artifacts\\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(?:[/?#]|$)/i.exec(\n          u.pathname,\n        )?.[1]) ||\n      null;\n    return id ? `${storageViewBaseUrl.replace(/\\/$/, '')}/design-briefs/${id.toLowerCase()}.html` : null;\n  } catch {\n    return null;\n  }\n}\n\n/**\n * The EMBED url for a published Claude artifact, or `null` when the link is not\n * one we can frame — the Claude counterpart of `toFigmaEmbedUrl`.\n *\n * claude.ai serves `frame-ancestors 'self'` on the artifact page itself, but a\n * PUBLISHED artifact also has an `/embed` route whose `frame-ancestors` carries\n * the domains its author allow-listed under \"Get embed code → Allowed domains\".\n * That is Anthropic's supported way to put an artifact on another site, so that\n * is what the iframe points at.\n *\n * Returns `null` for everything else, including a Claude CODE url\n * (`claude.ai/code/artifact/…`), whose Share dialog exposes no Embed settings\n * and whose `/embed` path answers `frame-ancestors 'self'` (verified against\n * real artifacts, 2026-08). A trailing `/embed` is accepted because that is the\n * url \"Get embed code\" hands the author.\n */\nexport function toClaudeEmbedUrl(url: string | null | undefined): string | null {\n  if (!url) return null;\n  try {\n    const u = new URL(url);\n    const host = u.hostname.toLowerCase();\n    // `claude.site/artifacts/<id>` 308-redirects to the claude.ai public url.\n    const publicId =\n      (host === 'claude.ai' && /^\\/public\\/artifacts\\/([\\w-]+)(?:\\/embed)?\\/?$/.exec(u.pathname)?.[1]) ||\n      (host === 'claude.site' && /^\\/artifacts\\/([\\w-]+)(?:\\/embed)?\\/?$/.exec(u.pathname)?.[1]) ||\n      null;\n    return publicId ? `https://claude.ai/public/artifacts/${publicId}/embed` : null;\n  } catch {\n    return null;\n  }\n}\n","'use client';\n\nimport { useEffect } from 'react';\nimport { getHashTargetElement, normalizeHashFragment } from '../utils/same-page-hash-nav';\nimport { scrollElementIntoView } from '../utils/scroll-into-view';\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(readyDep: unknown = true, options?: UseScrollToHashOptions): void {\n  const headerOffset = options?.headerOffset ?? 0;\n  useEffect(() => {\n    if (typeof window === 'undefined') return undefined;\n    if (readyDep === null || readyDep === false) return undefined;\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        // Raw-then-decoded lookup — encoded fragments from href\n        // composers must match the unencoded ids rows render.\n        const el = getHashTargetElement(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","'use client';\n\nimport { useCallback, useRef, useState } from 'react';\nimport { HONEYPOT_FIELD, ELAPSED_MS_FIELD, type HumanitySignals } 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  //\n  // Sampled in a lazy `useState` initialiser, then copied into a ref. Not\n  // `useRef(performance.now())`: that argument is re-evaluated on EVERY render\n  // even though only the first call uses it, so the component would read the\n  // clock during render forever. And not state alone: `resetSignals` restarts\n  // the window after a successful submit, which through state would re-render\n  // the whole form for a value nothing renders.\n  const [mountedAt] = useState(() => (typeof performance !== 'undefined' ? performance.now() : 0));\n  const startedAtRef = useRef<number>(mountedAt);\n\n  const getSignals = useCallback(\n    (): HumanitySignals => ({\n      [HONEYPOT_FIELD]: ref.current?.value ?? '',\n      [ELAPSED_MS_FIELD]: typeof performance !== 'undefined' ? Math.round(performance.now() - startedAtRef.current) : 0,\n    }),\n    [],\n  );\n\n  const resetSignals = useCallback(() => {\n    if (ref.current) ref.current.value = '';\n    if (typeof performance !== 'undefined') startedAtRef.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 * FALSE-POSITIVE HISTORY (2026-08-27): the honeypot was named\n * `contact_url_confirm`, and browser/password-manager autofill (which ignores\n * `autocomplete=\"off\"` and matches \"url\"/\"confirm\" name heuristics) filled it\n * for REAL users — every production `BOT_DETECTED` in the log window was a\n * legitimate Chrome user whose autofill tripped the decoy. Three layers now\n * prevent a recurrence; keep all three when touching this system:\n *   1. The field name avoids every autofill-heuristic token (name/email/\n *      phone/url/website/confirm/company/address/code/…).\n *   2. `HoneypotField` renders `readOnly`-until-focus + password-manager\n *      ignore attributes — autofill skips read-only inputs.\n *   3. `evaluateHumanitySignals` forgives a filled decoy whose value was\n *      COPIED from another field in the same body (the autofill signature —\n *      a human, not a bot). See `findHoneypotCopySource` for the match rules.\n *\n * ACCEPTED TRADEOFF of layer 3 (do not \"fix\" by removing the forgiveness): a\n * bot that fills EVERY field with one identical value now passes the honeypot\n * check. That bot class always had a strictly easier evasion — send the decoy\n * empty — so no new attacker capability is admitted; it remains covered by the\n * timing check, per-IP rate limits, route Zod validation, and first-party\n * BotID. Every forgiven allow is warn-logged by the hub gate for monitoring.\n */\n\n/** Hidden honeypot field name. Deliberately free of autofill-heuristic tokens (see module doc). */\nexport const HONEYPOT_FIELD = 'form_extra_note';\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/**\n * Every humanity-signal key that rides in a public form's POST body.\n * Server-side handlers that forward form payloads upstream (HubSpot booking,\n * CRM pushes, …) MUST strip by THIS array — never hand-typed strings — so a\n * field rename here propagates everywhere and the honeypot value can never\n * silently leak into an upstream record.\n */\nexport const HUMANITY_SIGNAL_KEYS = [HONEYPOT_FIELD, ELAPSED_MS_FIELD] as const;\n\n/** Is this body key one of the humanity-signal wire fields? */\nexport const isHumanitySignalKey = (key: string): boolean => (HUMANITY_SIGNAL_KEYS as readonly string[]).includes(key);\n\n/** Keyed wire object produced by `useHumanitySignals().getSignals()` and spread into the POST body. */\nexport type HumanitySignals = Record<string, string | number>;\n\n/**\n * Diagnostics every verdict carries so callers LOG what this module already\n * computed instead of re-deriving it (a re-derived predicate silently diverges\n * the day the rules here change):\n * - `honeypotLength`: decoy length — never the typed value (log-safe by\n *   construction).\n * - `timingAffirmed`: the submission POSITIVELY proved human timing (a PRESENT\n *   elapsed-ms at/above the floor — merely-missing timing does not affirm).\n *   The hub gate keys its BotID form-downgrade on this.\n */\nexport type HumanityVerdictDiagnostics = {\n  honeypotLength: number;\n  timingAffirmed: boolean;\n};\n\n/** Result of {@link evaluateHumanitySignals}. */\nexport type HumanityVerdict = HumanityVerdictDiagnostics &\n  (\n    | {\n        ok: true;\n        /** Present when a filled decoy was forgiven as autofill; `sourceField` names the body field it was copied from. */\n        note?: 'honeypot_autofill';\n        sourceField?: string;\n      }\n    | { ok: false; reason: 'honeypot' | 'too_fast' }\n  );\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 dodging the empty-check — coerce\n  // to a NON-EMPTY string so it still trips (JSON.stringify keeps `[]`/`{}`\n  // non-empty where String() would collapse them to '').\n  // null/undefined → '' = the correct \"field absent / unfilled\" allow case.\n  const honeypot = rawHp == null ? '' : typeof rawHp === 'string' ? rawHp : (JSON.stringify(rawHp) ?? 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 * Comparison normalization for the copy-match: autofill may fill the decoy\n * with a differently-FORMATTED rendition of the value the client posts (the\n * waitlist normalizes phones to E.164 before POST while a manager fills the\n * stored \"(555) 123-4567\"), so exact equality misses real humans. Normalized\n * equality still requires the decoy to mirror a real field's CONTENT, which a\n * bot gains nothing from — it could always send the decoy empty instead.\n */\nconst normalizeForCopyMatch = (s: string): string => s.normalize('NFKC').trim().toLowerCase();\nconst digitsOf = (s: string): string => s.replace(/\\D/g, '');\n\n/** Minimum normalized length for a copy-match — a 1-char echo is coincidence, not autofill. */\nconst MIN_COPY_MATCH_LENGTH = 2;\n/** Digits-only phone matching needs a real phone-sized run to be meaningful. */\nconst MIN_PHONE_DIGITS = 7;\n\n/**\n * Phone-sized digit runs match when one ENDS WITH the other: a manager fills\n * the stored national format (\"(555) 123-4567\") while the client posts E.164\n * (\"+15551234567\") — same phone, differing only by the country-code prefix.\n */\nconst phoneDigitsMatch = (a: string, b: string): boolean =>\n  a.length >= MIN_PHONE_DIGITS && b.length >= MIN_PHONE_DIGITS && (a.endsWith(b) || b.endsWith(a));\n\n/**\n * Find the body field the decoy value was COPIED from — the autofill\n * signature: browsers and password-manager extensions fill the hidden input\n * with the same datum they put in a visible field (email, phone, …) — a human\n * with autofill, not a bot. Scans top-level string values, string arrays, and\n * one nested level (the booking form's custom `formFields` object). Matches\n * normalized equality, plus digits-only equality for phone-sized values.\n *\n * Returns the matched field's path (`email`, `formFields.phone`, `tags[]`) —\n * the SSOT for both the verdict and the hub gate's `sameAs` log diagnostic —\n * or `null` when nothing matches.\n */\nexport function findHoneypotCopySource(body: unknown, honeypot: string): string | null {\n  const hpNorm = normalizeForCopyMatch(honeypot);\n  const hpDigits = digitsOf(honeypot);\n  const phoneCandidate = hpDigits.length >= MIN_PHONE_DIGITS;\n  if (hpNorm.length < MIN_COPY_MATCH_LENGTH && !phoneCandidate) return null;\n\n  const matches = (value: unknown): boolean => {\n    if (typeof value !== 'string' || value.trim() === '') return false;\n    // No length re-check: the top guard already rejected sub-minimum values\n    // (phoneCandidate implies ≥7 chars survive normalization — digits do).\n    if (normalizeForCopyMatch(value) === hpNorm) return true;\n    return phoneCandidate && phoneDigitsMatch(digitsOf(value), hpDigits);\n  };\n  const matchInArray = (value: unknown): boolean => Array.isArray(value) && value.some(matches);\n\n  const b = (body ?? {}) as Record<string, unknown>;\n  for (const [key, value] of Object.entries(b)) {\n    if (isHumanitySignalKey(key)) continue;\n    if (matches(value)) return key;\n    if (matchInArray(value)) return `${key}[]`;\n    if (value && typeof value === 'object' && !Array.isArray(value)) {\n      for (const [nestedKey, nested] of Object.entries(value as Record<string, unknown>)) {\n        if (matches(nested)) return `${key}.${nestedKey}`;\n        if (matchInArray(nested)) return `${key}.${nestedKey}[]`;\n      }\n    }\n  }\n  return null;\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) — UNLESS the\n *   value was copied from another field in the body (autofill reached the decoy → human;\n *   the verdict carries `note: 'honeypot_autofill'` + the `sourceField` so callers log it)\n * - elapsed below `minFillMs` → bot (humans take time; a MISSING timing value never\n *   blocks — and the too-fast check still applies to autofill-forgiven submissions)\n */\nexport function evaluateHumanitySignals(body: unknown, opts: { minFillMs: number }): HumanityVerdict {\n  const { honeypot, elapsedMs } = extractHumanitySignals(body);\n  const diagnostics: HumanityVerdictDiagnostics = {\n    honeypotLength: honeypot.length,\n    timingAffirmed: elapsedMs !== null && elapsedMs >= opts.minFillMs,\n  };\n  const filled = honeypot.trim() !== '';\n  const sourceField = filled ? findHoneypotCopySource(body, honeypot) : null;\n  if (filled && sourceField === null) return { ...diagnostics, ok: false, reason: 'honeypot' };\n  if (elapsedMs !== null && elapsedMs < opts.minFillMs) return { ...diagnostics, ok: false, reason: 'too_fast' };\n  return sourceField !== null\n    ? { ...diagnostics, ok: true, note: 'honeypot_autofill', sourceField }\n    : { ...diagnostics, ok: true };\n}\n\n/** Parse a comma-separated env string → trimmed, non-empty entries (undefined → []). */\nexport const splitCsvEnv = (s?: string): string[] =>\n  s\n    ?.split(',')\n    .map(t => t.trim())\n    .filter(Boolean) ?? [];\n"]}