@flamingo-stack/openframe-frontend-core 0.0.215 → 0.0.216
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-2V4SACHE.js +302 -0
- package/dist/chunk-2V4SACHE.js.map +1 -0
- package/dist/chunk-572WQWIX.cjs +348 -0
- package/dist/chunk-572WQWIX.cjs.map +1 -0
- package/dist/{chunk-WT5JV2GS.cjs → chunk-5V6MSE3B.cjs} +39 -39
- package/dist/chunk-5V6MSE3B.cjs.map +1 -0
- package/dist/{chunk-WQZP3JIZ.js → chunk-CDLYRFDE.js} +1894 -1472
- package/dist/chunk-CDLYRFDE.js.map +1 -0
- package/dist/chunk-GVNQAGXB.js +232 -0
- package/dist/chunk-GVNQAGXB.js.map +1 -0
- package/dist/{chunk-P5EE2VJX.cjs → chunk-HOHDXYPR.cjs} +1 -1
- package/dist/chunk-HOHDXYPR.cjs.map +1 -0
- package/dist/chunk-IH76P5R6.cjs +232 -0
- package/dist/chunk-IH76P5R6.cjs.map +1 -0
- package/dist/{chunk-24KCAECR.cjs → chunk-JJR27M56.cjs} +3 -3
- package/dist/{chunk-24KCAECR.cjs.map → chunk-JJR27M56.cjs.map} +1 -1
- package/dist/chunk-K4DFAVSO.cjs +302 -0
- package/dist/chunk-K4DFAVSO.cjs.map +1 -0
- package/dist/{chunk-HICZPTRR.js → chunk-LCLTCCXS.js} +14 -14
- package/dist/chunk-LCLTCCXS.js.map +1 -0
- package/dist/{chunk-VFKQMAUF.cjs → chunk-OB45JHDY.cjs} +3 -3
- package/dist/{chunk-VFKQMAUF.cjs.map → chunk-OB45JHDY.cjs.map} +1 -1
- package/dist/{chunk-4XLJWX2N.js → chunk-ORJREQ2W.js} +4 -4
- package/dist/{chunk-7PCP7YQR.js → chunk-QTKU6ULP.js} +6 -6
- package/dist/{chunk-CIPO6DXK.js → chunk-QY75VKAS.js} +5 -5
- package/dist/{chunk-ZG2YY5E7.js → chunk-RFONYT63.js} +1 -1
- package/dist/chunk-RFONYT63.js.map +1 -0
- package/dist/{chunk-NGFP4RVL.cjs → chunk-SMCG2CCC.cjs} +30 -30
- package/dist/{chunk-NGFP4RVL.cjs.map → chunk-SMCG2CCC.cjs.map} +1 -1
- package/dist/{chunk-MX5MIFWA.js → chunk-UEBM4PC4.js} +5 -5
- package/dist/chunk-VC3ND5RB.js +348 -0
- package/dist/chunk-VC3ND5RB.js.map +1 -0
- package/dist/{chunk-UXZ3ZJ3M.cjs → chunk-XDPSSE4O.cjs} +4 -4
- package/dist/{chunk-UXZ3ZJ3M.cjs.map → chunk-XDPSSE4O.cjs.map} +1 -1
- package/dist/{chunk-D4MNFY67.cjs → chunk-ZGTDUPTW.cjs} +1316 -894
- package/dist/chunk-ZGTDUPTW.cjs.map +1 -0
- package/dist/components/chat/entity-cards/blog-card.d.ts +1 -1
- package/dist/components/chat/entity-cards/blog-card.d.ts.map +1 -1
- package/dist/components/chat/entity-cards/case-study-card.d.ts +1 -1
- package/dist/components/chat/entity-cards/case-study-card.d.ts.map +1 -1
- package/dist/components/chat/entity-cards/customer-interview-card.d.ts +1 -1
- package/dist/components/chat/entity-cards/customer-interview-card.d.ts.map +1 -1
- package/dist/components/chat/entity-cards/dispatch.d.ts.map +1 -1
- package/dist/components/chat/entity-cards/investor-update-card.d.ts +1 -1
- package/dist/components/chat/entity-cards/investor-update-card.d.ts.map +1 -1
- package/dist/components/chat/entity-cards/onboarding-guide-card.d.ts +1 -1
- package/dist/components/chat/entity-cards/onboarding-guide-card.d.ts.map +1 -1
- package/dist/components/chat/entity-cards/program-card.d.ts +1 -1
- package/dist/components/chat/entity-cards/program-card.d.ts.map +1 -1
- package/dist/components/chat/entity-cards/use-entity-card-link.d.ts +14 -0
- package/dist/components/chat/entity-cards/use-entity-card-link.d.ts.map +1 -0
- package/dist/components/chat/entity-cards/use-entity-card-placeholder.d.ts +13 -0
- package/dist/components/chat/entity-cards/use-entity-card-placeholder.d.ts.map +1 -0
- package/dist/components/chat/index.cjs +11 -11
- package/dist/components/chat/index.js +10 -10
- package/dist/components/contact/index.cjs +12 -12
- package/dist/components/contact/index.js +11 -11
- package/dist/components/features/captions-url.d.ts +18 -0
- package/dist/components/features/captions-url.d.ts.map +1 -0
- package/dist/components/features/index.cjs +23 -11
- package/dist/components/features/index.cjs.map +1 -1
- package/dist/components/features/index.d.ts +2 -0
- package/dist/components/features/index.d.ts.map +1 -1
- package/dist/components/features/index.js +24 -12
- package/dist/components/features/mux-origins.cjs +10 -0
- package/dist/components/features/mux-origins.cjs.map +1 -0
- package/dist/components/features/mux-origins.d.ts +26 -0
- package/dist/components/features/mux-origins.d.ts.map +1 -0
- package/dist/components/features/mux-origins.js +7 -0
- package/dist/components/features/mux-origins.js.map +1 -0
- package/dist/components/features/notifications/index.d.ts +2 -0
- package/dist/components/features/notifications/index.d.ts.map +1 -1
- package/dist/components/features/notifications/notification-drawer.d.ts +2 -1
- package/dist/components/features/notifications/notification-drawer.d.ts.map +1 -1
- package/dist/components/features/notifications/notification-popups.d.ts +10 -0
- package/dist/components/features/notifications/notification-popups.d.ts.map +1 -0
- package/dist/components/features/notifications/notifications-context.d.ts +8 -1
- package/dist/components/features/notifications/notifications-context.d.ts.map +1 -1
- package/dist/components/features/notifications/types.d.ts +1 -0
- package/dist/components/features/notifications/types.d.ts.map +1 -1
- package/dist/components/features/use-video-warmup.d.ts +53 -0
- package/dist/components/features/use-video-warmup.d.ts.map +1 -0
- package/dist/components/icons/index.cjs +3 -3
- package/dist/components/icons/index.js +2 -2
- package/dist/components/icons-v2-generated/index.cjs +2 -2
- package/dist/components/icons-v2-generated/index.cjs.map +1 -1
- package/dist/components/icons-v2-generated/index.js +4 -4
- package/dist/components/index.cjs +132 -102
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +94 -64
- package/dist/components/index.js.map +1 -1
- package/dist/components/navigation/index.cjs +11 -11
- package/dist/components/navigation/index.js +10 -10
- package/dist/components/onboarding-guides/build-default-href.d.ts +15 -0
- package/dist/components/onboarding-guides/build-default-href.d.ts.map +1 -0
- package/dist/components/onboarding-guides/hooks/use-onboarding-guides.d.ts +28 -0
- package/dist/components/onboarding-guides/hooks/use-onboarding-guides.d.ts.map +1 -0
- package/dist/components/onboarding-guides/index.cjs +373 -0
- package/dist/components/onboarding-guides/index.cjs.map +1 -0
- package/dist/components/onboarding-guides/index.d.ts +25 -0
- package/dist/components/onboarding-guides/index.d.ts.map +1 -0
- package/dist/components/onboarding-guides/index.js +373 -0
- package/dist/components/onboarding-guides/index.js.map +1 -0
- package/dist/components/onboarding-guides/onboarding-guide-detail-view.d.ts +52 -0
- package/dist/components/onboarding-guides/onboarding-guide-detail-view.d.ts.map +1 -0
- package/dist/components/onboarding-guides/onboarding-guides-catalog-skeleton.d.ts +17 -0
- package/dist/components/onboarding-guides/onboarding-guides-catalog-skeleton.d.ts.map +1 -0
- package/dist/components/onboarding-guides/onboarding-guides-catalog-view.d.ts +43 -0
- package/dist/components/onboarding-guides/onboarding-guides-catalog-view.d.ts.map +1 -0
- package/dist/components/shared/doc-search/doc-search-bar.d.ts +59 -0
- package/dist/components/shared/doc-search/doc-search-bar.d.ts.map +1 -0
- package/dist/components/shared/doc-search/doc-search-result-row.d.ts +18 -0
- package/dist/components/shared/doc-search/doc-search-result-row.d.ts.map +1 -0
- package/dist/components/shared/doc-search/format-relative-path.d.ts +10 -0
- package/dist/components/shared/doc-search/format-relative-path.d.ts.map +1 -0
- package/dist/components/shared/doc-search/index.d.ts +8 -0
- package/dist/components/shared/doc-search/index.d.ts.map +1 -0
- package/dist/components/shared/doc-search/map-doc-search-results.d.ts +15 -0
- package/dist/components/shared/doc-search/map-doc-search-results.d.ts.map +1 -0
- package/dist/components/shared/doc-search/resolve-search-result-action.d.ts +37 -0
- package/dist/components/shared/doc-search/resolve-search-result-action.d.ts.map +1 -0
- package/dist/components/shared/doc-search/types.d.ts +29 -0
- package/dist/components/shared/doc-search/types.d.ts.map +1 -0
- package/dist/components/shared/doc-search/use-doc-search.d.ts +46 -0
- package/dist/components/shared/doc-search/use-doc-search.d.ts.map +1 -0
- package/dist/components/tickets/help-center-card.d.ts +5 -1
- package/dist/components/tickets/help-center-card.d.ts.map +1 -1
- package/dist/components/tickets/hooks/use-ticket-actions.d.ts +8 -0
- package/dist/components/tickets/hooks/use-ticket-actions.d.ts.map +1 -1
- package/dist/components/tickets/index.cjs +316 -145
- package/dist/components/tickets/index.cjs.map +1 -1
- package/dist/components/tickets/index.js +237 -66
- package/dist/components/tickets/index.js.map +1 -1
- package/dist/components/tickets/ticket-detail-drawer.d.ts +11 -2
- package/dist/components/tickets/ticket-detail-drawer.d.ts.map +1 -1
- package/dist/components/tickets/types.d.ts +50 -1
- package/dist/components/tickets/types.d.ts.map +1 -1
- package/dist/components/ui/file-manager/index.cjs +51 -51
- package/dist/components/ui/file-manager/index.cjs.map +1 -1
- package/dist/components/ui/file-manager/index.js +2 -2
- package/dist/components/ui/filter-pill-row.d.ts +20 -0
- package/dist/components/ui/filter-pill-row.d.ts.map +1 -0
- package/dist/components/ui/index.cjs +16 -14
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.d.ts +1 -0
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +21 -19
- package/dist/components/ui/simple-markdown-renderer.d.ts.map +1 -1
- package/dist/contexts/chat-runtime-context.d.ts +42 -0
- package/dist/contexts/chat-runtime-context.d.ts.map +1 -1
- package/dist/contexts/index.cjs +2 -2
- package/dist/contexts/index.js +1 -1
- package/dist/embed-shims/index.cjs +3 -3
- package/dist/embed-shims/index.cjs.map +1 -1
- package/dist/embed-shims/index.js +5 -5
- package/dist/hooks/index.cjs +6 -6
- package/dist/hooks/index.js +5 -5
- package/dist/index.cjs +28 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +59 -45
- package/dist/utils/dev-sections/openframe-dev-sections.d.ts +2 -2
- package/dist/utils/dev-sections/openframe-dev-sections.d.ts.map +1 -1
- package/dist/utils/index.cjs +11 -5
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.js +11 -5
- package/dist/utils/index.js.map +1 -1
- package/package.json +13 -1
- package/src/components/chat/entity-cards/blog-card.tsx +17 -5
- package/src/components/chat/entity-cards/case-study-card.tsx +23 -1
- package/src/components/chat/entity-cards/customer-interview-card.tsx +23 -1
- package/src/components/chat/entity-cards/dispatch.tsx +21 -0
- package/src/components/chat/entity-cards/investor-update-card.tsx +23 -1
- package/src/components/chat/entity-cards/onboarding-guide-card.tsx +30 -4
- package/src/components/chat/entity-cards/program-card.tsx +17 -3
- package/src/components/chat/entity-cards/use-entity-card-link.ts +66 -0
- package/src/components/chat/entity-cards/use-entity-card-placeholder.ts +50 -0
- package/src/components/features/captions-url.ts +25 -0
- package/src/components/features/index.ts +2 -0
- package/src/components/features/mux-origins.ts +27 -0
- package/src/components/features/notifications/index.ts +2 -0
- package/src/components/features/notifications/notification-drawer.tsx +100 -16
- package/src/components/features/notifications/notification-popups.tsx +105 -0
- package/src/components/features/notifications/notifications-context.tsx +16 -0
- package/src/components/features/notifications/types.ts +1 -0
- package/src/components/features/use-video-warmup.ts +176 -0
- package/src/components/index.ts +5 -0
- package/src/components/onboarding-guides/build-default-href.ts +16 -0
- package/src/components/onboarding-guides/hooks/use-onboarding-guides.ts +90 -0
- package/src/components/onboarding-guides/index.ts +39 -0
- package/src/components/onboarding-guides/onboarding-guide-detail-view.tsx +215 -0
- package/src/components/onboarding-guides/onboarding-guides-catalog-skeleton.tsx +62 -0
- package/src/components/onboarding-guides/onboarding-guides-catalog-view.tsx +230 -0
- package/src/components/shared/doc-search/doc-search-bar.tsx +100 -0
- package/src/components/shared/doc-search/doc-search-result-row.tsx +73 -0
- package/src/components/shared/doc-search/format-relative-path.ts +17 -0
- package/src/components/shared/doc-search/index.ts +24 -0
- package/src/components/shared/doc-search/map-doc-search-results.ts +113 -0
- package/src/components/shared/doc-search/resolve-search-result-action.ts +68 -0
- package/src/components/shared/doc-search/types.ts +28 -0
- package/src/components/shared/doc-search/use-doc-search.ts +263 -0
- package/src/components/tickets/help-center-card.tsx +8 -0
- package/src/components/tickets/help-center-list.tsx +17 -3
- package/src/components/tickets/hooks/use-ticket-actions.ts +210 -14
- package/src/components/tickets/ticket-detail-drawer.tsx +145 -5
- package/src/components/tickets/types.ts +55 -0
- package/src/components/ui/filter-pill-row.tsx +72 -0
- package/src/components/ui/index.ts +1 -0
- package/src/components/ui/simple-markdown-renderer.tsx +24 -1
- package/src/components/ui/toaster.tsx +3 -3
- package/src/contexts/chat-runtime-context.tsx +41 -0
- package/src/stories/NotificationDrawer.stories.tsx +18 -2
- package/src/utils/dev-sections/openframe-dev-sections.ts +12 -5
- package/dist/chunk-2G3NXF6J.cjs +0 -521
- package/dist/chunk-2G3NXF6J.cjs.map +0 -1
- package/dist/chunk-D4MNFY67.cjs.map +0 -1
- package/dist/chunk-HICZPTRR.js.map +0 -1
- package/dist/chunk-P5EE2VJX.cjs.map +0 -1
- package/dist/chunk-R6MLPU4A.js +0 -521
- package/dist/chunk-R6MLPU4A.js.map +0 -1
- package/dist/chunk-WQZP3JIZ.js.map +0 -1
- package/dist/chunk-WT5JV2GS.cjs.map +0 -1
- package/dist/chunk-ZG2YY5E7.js.map +0 -1
- /package/dist/{chunk-4XLJWX2N.js.map → chunk-ORJREQ2W.js.map} +0 -0
- /package/dist/{chunk-7PCP7YQR.js.map → chunk-QTKU6ULP.js.map} +0 -0
- /package/dist/{chunk-CIPO6DXK.js.map → chunk-QY75VKAS.js.map} +0 -0
- /package/dist/{chunk-MX5MIFWA.js.map → chunk-UEBM4PC4.js.map} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/chunk-5V6MSE3B.cjs","../src/hooks/ui/use-debounce.ts","../src/hooks/ui/use-local-storage.ts","../src/hooks/ui/use-media-query.ts","../src/hooks/ui/use-memoized-callback.ts","../src/hooks/ui/use-onboarding-state.ts","../src/utils/onboarding-storage.ts","../src/hooks/ui/use-table-pagination.ts","../src/hooks/ui/use-throttle.ts","../src/hooks/ui/use-window-size.ts","../src/hooks/ui/use-horizontal-scrollbar.ts","../src/hooks/ui/use-image-edge-color.ts","../src/hooks/ui/use-search.ts","../src/hooks/ui/use-auto-limit-tags.ts","../src/hooks/platform/use-platform-config.ts","../src/utils/platform-config.tsx","../src/hooks/use-toast.ts","../src/components/ui/toaster.tsx","../src/types/tool.types.ts","../src/components/tool-icon.tsx","../src/hooks/use-contact-submission.ts","../src/hooks/use-quick-action-hint.ts","../src/hooks/use-copy-to-clipboard.ts","../src/hooks/use-batch-images.ts","../src/hooks/use-authenticated-image.ts","../src/hooks/state/use-query-params.ts","../src/hooks/state/graphql-parser.ts","../src/hooks/state/flatten-schema.ts","../src/hooks/state/url-converter.ts","../src/hooks/state/introspection.ts","../src/hooks/state/use-api-params.ts","../src/hooks/state/use-cursor-pagination-state.ts","../src/hooks/nats/use-nats-client.ts","../src/hooks/use-near-viewport.ts","../src/hooks/use-access-code-integration.ts","../src/utils/access-code-client.ts","../src/hooks/use-og-placeholder.ts"],"names":["useState","useEffect","useRef","useCallback","dismissOnboarding","markMultipleComplete","useLayoutEffect","r","g","b","platforms","sonnerToast","jsx","Fragment","toast","useMemo","fetchPromise","introspector","isInputObjectType","clearParams"],"mappings":"AAAA,+8BAAY;AACZ;AACE;AACA;AACA;AACF,wDAA6B;AAC7B;AACE;AACA;AACA;AACA;AACF,wDAA6B;AAC7B;AACE;AACA;AACF,wDAA6B;AAC7B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACF,wDAA6B;AAC7B;AACE;AACF,wDAA6B;AAC7B;AACE;AACF,wDAA6B;AAC7B;AACA;AC9BA,2EAAoC;AAQ7B,SAAS,WAAA,CAAe,KAAA,EAAU,MAAA,EAAQ,GAAA,EAAQ;AACvD,EAAA,MAAM,CAAC,cAAA,EAAgB,iBAAiB,EAAA,EAAI,6BAAA,KAAiB,CAAA;AAE7D,EAAA,8BAAA,CAAU,EAAA,GAAM;AAEd,IAAA,MAAM,MAAA,EAAQ,UAAA,CAAW,CAAA,EAAA,GAAM;AAC7B,MAAA,iBAAA,CAAkB,KAAK,CAAA;AAAA,IACzB,CAAA,EAAG,KAAK,CAAA;AAGR,IAAA,OAAO,CAAA,EAAA,GAAM;AACX,MAAA,YAAA,CAAa,KAAK,CAAA;AAAA,IACpB,CAAA;AAAA,EACF,CAAA,EAAG,CAAC,KAAA,EAAO,KAAK,CAAC,CAAA;AAEjB,EAAA,OAAO,cAAA;AACT;ADoBA;AACA;AE7CA;AAQO,SAAS,eAAA,CAAmB,GAAA,EAAa,YAAA,EAA4D;AAG1G,EAAA,MAAM,CAAC,WAAA,EAAa,cAAc,EAAA,EAAIA,6BAAAA,CAAY,EAAA,GAAM;AACtD,IAAA,IAAI;AACF,MAAA,GAAA,CAAI,OAAO,OAAA,IAAW,WAAA,EAAa;AACjC,QAAA,MAAM,KAAA,EAAO,MAAA,CAAO,YAAA,CAAa,OAAA,CAAQ,GAAG,CAAA;AAE5C,QAAA,OAAO,KAAA,EAAO,IAAA,CAAK,KAAA,CAAM,IAAI,EAAA,EAAI,YAAA;AAAA,MACnC;AAAA,IACF,EAAA,MAAA,CAAS,KAAA,EAAO;AACd,MAAA,OAAA,CAAQ,KAAA,CAAM,CAAA,gCAAA,EAAmC,GAAG,CAAA,EAAA,CAAA,EAAM,KAAK,CAAA;AAAA,IACjE;AACA,IAAA,OAAO,YAAA;AAAA,EACT,CAAC,CAAA;AAGD,EAAA,MAAM,cAAA,EAAgB,2BAAA,IAAW,CAAA;AAEjC,EAAA,MAAM,mBAAA,EAAqB,2BAAA,KAAY,CAAA;AAGvC,EAAAC,8BAAAA,CAAU,EAAA,GAAM;AACd,IAAA,GAAA,CAAI,CAAC,aAAA,CAAc,OAAA,EAAS,MAAA;AAE5B,IAAA,MAAM,oBAAA,EAAsB,CAAC,CAAA,EAAA,GAAoB;AAC/C,MAAA,GAAA,CAAI,CAAA,CAAE,IAAA,IAAQ,IAAA,GAAO,CAAA,CAAE,SAAA,IAAa,IAAA,EAAM;AACxC,QAAA,IAAI;AACF,UAAA,MAAM,SAAA,EAAW,IAAA,CAAK,KAAA,CAAM,CAAA,CAAE,QAAQ,CAAA;AACtC,UAAA,kBAAA,CAAmB,QAAA,EAAU,IAAA;AAC7B,UAAA,cAAA,CAAe,QAAQ,CAAA;AACvB,UAAA,OAAA,CAAQ,GAAA,CAAI,CAAA,oCAAA,EAAgC,GAAG,CAAA,oBAAA,CAAsB,CAAA;AAAA,QACvE,EAAA,MAAA,CAAS,KAAA,EAAO;AACd,UAAA,OAAA,CAAQ,KAAA,CAAM,CAAA,qCAAA,EAAwC,GAAG,CAAA,EAAA,CAAA,EAAM,KAAK,CAAA;AAAA,QACtE;AAAA,MACF;AAAA,IACF,CAAA;AAEA,IAAA,MAAM,0BAAA,EAA4B,CAAC,CAAA,EAAA,GAAmB;AACpD,MAAA,GAAA,CAAI,CAAA,CAAE,MAAA,CAAO,IAAA,IAAQ,GAAA,EAAK;AACxB,QAAA,IAAI;AACF,UAAA,GAAA,CAAI,CAAA,CAAE,MAAA,CAAO,SAAA,IAAa,KAAA,GAAQ,CAAA,CAAE,MAAA,CAAO,SAAA,IAAa,KAAA,CAAA,EAAW;AAEjE,YAAA,kBAAA,CAAmB,QAAA,EAAU,IAAA;AAC7B,YAAA,cAAA,CAAe,YAAY,CAAA;AAC3B,YAAA,OAAA,CAAQ,GAAA,CAAI,CAAA,oCAAA,EAAgC,GAAG,CAAA,2BAAA,CAA6B,CAAA;AAAA,UAC9E,EAAA,KAAO;AACL,YAAA,MAAM,SAAA,EAAW,IAAA,CAAK,KAAA,CAAM,CAAA,CAAE,MAAA,CAAO,QAAQ,CAAA;AAC7C,YAAA,kBAAA,CAAmB,QAAA,EAAU,IAAA;AAC7B,YAAA,cAAA,CAAe,QAAQ,CAAA;AACvB,YAAA,OAAA,CAAQ,GAAA,CAAI,CAAA,oCAAA,EAAgC,GAAG,CAAA,2BAAA,CAA6B,CAAA;AAAA,UAC9E;AAAA,QACF,EAAA,MAAA,CAAS,KAAA,EAAO;AACd,UAAA,OAAA,CAAQ,KAAA,CAAM,CAAA,4CAAA,EAA+C,GAAG,CAAA,EAAA,CAAA,EAAM,KAAK,CAAA;AAAA,QAC7E;AAAA,MACF;AAAA,IACF,CAAA;AAEA,IAAA,MAAA,CAAO,gBAAA,CAAiB,SAAA,EAAW,mBAAmB,CAAA;AACtD,IAAA,MAAA,CAAO,gBAAA,CAAiB,oBAAA,EAAsB,yBAA0C,CAAA;AAExF,IAAA,OAAO,CAAA,EAAA,GAAM;AACX,MAAA,MAAA,CAAO,mBAAA,CAAoB,SAAA,EAAW,mBAAmB,CAAA;AACzD,MAAA,MAAA,CAAO,mBAAA,CAAoB,oBAAA,EAAsB,yBAA0C,CAAA;AAAA,IAC7F,CAAA;AAAA,EACF,CAAA,EAAG,CAAC,GAAG,CAAC,CAAA;AAGR,EAAAA,8BAAAA,CAAU,EAAA,GAAM;AACd,IAAA,GAAA,CAAI,CAAC,aAAA,CAAc,OAAA,EAAS,MAAA;AAG5B,IAAA,GAAA,CAAI,kBAAA,CAAmB,OAAA,EAAS;AAC9B,MAAA,kBAAA,CAAmB,QAAA,EAAU,KAAA;AAC7B,MAAA,MAAA;AAAA,IACF;AAEA,IAAA,IAAI;AAEF,MAAA,GAAA,CAAI,OAAO,OAAA,IAAW,WAAA,EAAa;AACjC,QAAA,MAAA,CAAO,YAAA,CAAa,OAAA,CAAQ,GAAA,EAAK,IAAA,CAAK,SAAA,CAAU,WAAW,CAAC,CAAA;AAAA,MAC9D;AAAA,IACF,EAAA,MAAA,CAAS,KAAA,EAAO;AACd,MAAA,OAAA,CAAQ,KAAA,CAAM,CAAA,mCAAA,EAAsC,GAAG,CAAA,EAAA,CAAA,EAAM,KAAK,CAAA;AAAA,IACpE;AAAA,EACF,CAAA,EAAG,CAAC,GAAA,EAAK,WAAW,CAAC,CAAA;AAIrB,EAAA,MAAM,SAAA,EAAW,CAAC,KAAA,EAAA,GAA+B;AAC/C,IAAA,IAAI;AAEF,MAAA,MAAM,aAAA,EAAe,MAAA,WAAiB,SAAA,EAAW,KAAA,CAAM,WAAW,EAAA,EAAI,KAAA;AAEtE,MAAA,cAAA,CAAe,YAAY,CAAA;AAAA,IAC7B,EAAA,MAAA,CAAS,KAAA,EAAO;AACd,MAAA,OAAA,CAAQ,KAAA,CAAM,CAAA,0CAAA,EAA6C,GAAG,CAAA,EAAA,CAAA,EAAM,KAAK,CAAA;AAAA,IAC3E;AAAA,EACF,CAAA;AAEA,EAAA,OAAO,CAAC,WAAA,EAAa,QAAQ,CAAA;AAC/B;AFeA;AACA;AG7HA;AAOO,SAAS,aAAA,CACd,KAAA,EACqB;AACrB,EAAA,MAAM,CAAC,OAAA,EAAS,UAAU,EAAA,EAAID,6BAAAA,KAA8B,CAAS,CAAA;AAErE,EAAA,oCAAA,CAAgB,EAAA,GAAM;AACpB,IAAA,MAAM,WAAA,EAAa,MAAA,CAAO,UAAA,CAAW,KAAK,CAAA;AAE1C,IAAA,MAAM,aAAA,EAAe,CAAA,EAAA,GAAM;AACzB,MAAA,UAAA,CAAW,UAAA,CAAW,OAAO,CAAA;AAAA,IAC/B,CAAA;AAGA,IAAA,YAAA,CAAa,CAAA;AAEb,IAAA,UAAA,CAAW,gBAAA,CAAiB,QAAA,EAAU,YAAY,CAAA;AAElD,IAAA,OAAO,CAAA,EAAA,GAAM;AACX,MAAA,UAAA,CAAW,mBAAA,CAAoB,QAAA,EAAU,YAAY,CAAA;AAAA,IACvD,CAAA;AAAA,EACF,CAAA,EAAG,CAAC,KAAK,CAAC,CAAA;AAEV,EAAA,OAAO,OAAA;AACT;AAKO,IAAM,YAAA,EAAc;AAAA,EACzB,EAAA,EAAI,oBAAA;AAAA;AAAA,EACJ,EAAA,EAAI;AAAA;AACN,CAAA;AAGO,SAAS,OAAA,CAAA,EAA+B;AAC7C,EAAA,OAAO,OAAA,CAAQ,CAAA;AACjB;AAEO,SAAS,OAAA,CAAA,EAA+B;AAC7C,EAAA,MAAM,QAAA,EAAU,aAAA,CAAc,WAAA,CAAY,EAAE,CAAA;AAC5C,EAAA,OAAO,QAAA,IAAY,KAAA,EAAA,EAAY,KAAA,EAAA,EAAY,OAAA;AAC7C;AAEO,SAAS,OAAA,CAAA,EAA+B;AAC7C,EAAA,MAAM,QAAA,EAAU,aAAA,CAAc,WAAA,CAAY,EAAE,CAAA;AAC5C,EAAA,OAAO,QAAA,IAAY,KAAA,EAAA,EAAY,KAAA,EAAA,EAAY,OAAA;AAC7C;AH0GA;AACA;AIhKA;AASO,SAAS,mBAAA,CAAuD,QAAA,EAAa,YAAA,EAAwB;AAE1G,EAAA,MAAM,YAAA,EAAcE,2BAAAA,QAAkB,CAAA;AACtC,EAAA,MAAM,gBAAA,EAAkBA,2BAAAA,YAA0B,CAAA;AAGlD,EAAA,WAAA,CAAY,QAAA,EAAU,QAAA;AAGtB,EAAA,MAAM,YAAA,EAAc,YAAA,CAAa,IAAA,CAAK,CAAC,GAAA,EAAK,CAAA,EAAA,GAAM,CAAC,MAAA,CAAO,EAAA,CAAG,GAAA,EAAK,eAAA,CAAgB,OAAA,CAAQ,CAAC,CAAC,CAAC,CAAA;AAG7F,EAAA,GAAA,CAAI,WAAA,EAAa;AACf,IAAA,eAAA,CAAgB,QAAA,EAAU,YAAA;AAAA,EAC5B;AAGA,EAAA,OAAO,gCAAA,CAAa,CAAA,GAAI,IAAA,EAAA,GAAgB,WAAA,CAAY,OAAA,CAAQ,GAAG,IAAI,CAAA,CAAA,EAAS,CAAC,WAAW,CAAC,CAAA;AAC3F;AJiJA;AACA;AK7KA;AL+KA;AACA;AMrKA,IAAM,cAAA,EAAiC;AAAA,EACrC,cAAA,EAAgB,CAAC,CAAA;AAAA,EACjB,YAAA,EAAc,CAAC,CAAA;AAAA,EACf,SAAA,EAAW,KAAA;AAAA,EACX,WAAA,EAAA,iBAAa,IAAI,IAAA,CAAK,CAAA,CAAA,CAAE,WAAA,CAAY;AACtC,CAAA;AAKO,SAAS,mBAAA,CAAoB,GAAA,EAAa,KAAA,EAA8B;AAC7E,EAAA,GAAA,CAAI,OAAO,OAAA,IAAW,WAAA,EAAa,MAAA;AAEnC,EAAA,IAAI;AACF,IAAA,YAAA,CAAa,OAAA,CAAQ,GAAA,EAAK,IAAA,CAAK,SAAA,CAAU,KAAK,CAAC,CAAA;AAC/C,IAAA,OAAA,CAAQ,GAAA,CAAI,mCAAA,EAA8B,KAAK,CAAA;AAG/C,IAAA,GAAA,CAAI,OAAO,OAAA,IAAW,WAAA,EAAa;AACjC,MAAA,MAAA,CAAO,aAAA;AAAA,QACL,IAAI,WAAA,CAAY,oBAAA,EAAsB;AAAA,UACpC,MAAA,EAAQ,EAAE,GAAA,EAAK,QAAA,EAAU,IAAA,CAAK,SAAA,CAAU,KAAK,EAAE;AAAA,QACjD,CAAC;AAAA,MACH,CAAA;AAAA,IACF;AAAA,EACF,EAAA,MAAA,CAAS,GAAA,EAAK;AACZ,IAAA,OAAA,CAAQ,IAAA,CAAK,qDAAA,EAAuD,GAAG,CAAA;AAAA,EACzE;AACF;AAKO,SAAS,mBAAA,CAAoB,GAAA,EAA8B;AAChE,EAAA,GAAA,CAAI,OAAO,OAAA,IAAW,WAAA,EAAa,OAAO,aAAA;AAE1C,EAAA,IAAI;AACF,IAAA,MAAM,IAAA,EAAM,YAAA,CAAa,OAAA,CAAQ,GAAG,CAAA;AACpC,IAAA,GAAA,CAAI,CAAC,GAAA,EAAK,OAAO,aAAA;AAEjB,IAAA,MAAM,OAAA,EAAS,IAAA,CAAK,KAAA,CAAM,GAAG,CAAA;AAC7B,IAAA,OAAO,MAAA;AAAA,EACT,EAAA,MAAA,CAAS,GAAA,EAAK;AACZ,IAAA,OAAA,CAAQ,IAAA,CAAK,wDAAA,EAA0D,GAAG,CAAA;AAC1E,IAAA,OAAO,aAAA;AAAA,EACT;AACF;AAKO,SAAS,oBAAA,CACd,GAAA,EACA,OAAA,EACiB;AACjB,EAAA,MAAM,MAAA,EAAQ,mBAAA,CAAoB,GAAG,CAAA;AACrC,EAAA,MAAM,SAAA,EAA4B;AAAA,IAChC,GAAG,KAAA;AAAA,IACH,cAAA,EAAgB,CAAC,mBAAG,IAAI,GAAA,CAAI,CAAC,GAAG,KAAA,CAAM,cAAA,EAAgB,GAAG,OAAO,CAAC,CAAC,CAAA;AAAA,IAClE,YAAA,EAAc,KAAA,CAAM,YAAA,CAAa,MAAA,CAAO,CAAA,EAAA,EAAA,GAAM,CAAC,OAAA,CAAQ,QAAA,CAAS,EAAE,CAAC,CAAA;AAAA,IACnE,WAAA,EAAA,iBAAa,IAAI,IAAA,CAAK,CAAA,CAAA,CAAE,WAAA,CAAY;AAAA,EACtC,CAAA;AACA,EAAA,mBAAA,CAAoB,GAAA,EAAK,QAAQ,CAAA;AACjC,EAAA,OAAO,QAAA;AACT;AAKO,SAAS,gBAAA,CAAiB,GAAA,EAAa,MAAA,EAAiC;AAC7E,EAAA,OAAO,oBAAA,CAAqB,GAAA,EAAK,CAAC,MAAM,CAAC,CAAA;AAC3C;AAKO,SAAS,eAAA,CAAgB,GAAA,EAAa,MAAA,EAAiC;AAC5E,EAAA,MAAM,MAAA,EAAQ,mBAAA,CAAoB,GAAG,CAAA;AACrC,EAAA,MAAM,SAAA,EAA4B;AAAA,IAChC,GAAG,KAAA;AAAA,IACH,YAAA,EAAc,CAAC,mBAAG,IAAI,GAAA,CAAI,CAAC,GAAG,KAAA,CAAM,YAAA,EAAc,MAAM,CAAC,CAAC,CAAA;AAAA,IAC1D,cAAA,EAAgB,KAAA,CAAM,cAAA,CAAe,MAAA,CAAO,CAAA,EAAA,EAAA,GAAM,GAAA,IAAO,MAAM,CAAA;AAAA,IAC/D,WAAA,EAAA,iBAAa,IAAI,IAAA,CAAK,CAAA,CAAA,CAAE,WAAA,CAAY;AAAA,EACtC,CAAA;AACA,EAAA,mBAAA,CAAoB,GAAA,EAAK,QAAQ,CAAA;AACjC,EAAA,OAAO,QAAA;AACT;AAKO,SAAS,iBAAA,CAAkB,GAAA,EAA8B;AAC9D,EAAA,MAAM,MAAA,EAAQ,mBAAA,CAAoB,GAAG,CAAA;AACrC,EAAA,MAAM,SAAA,EAA4B;AAAA,IAChC,GAAG,KAAA;AAAA,IACH,SAAA,EAAW,IAAA;AAAA,IACX,WAAA,EAAA,iBAAa,IAAI,IAAA,CAAK,CAAA,CAAA,CAAE,WAAA,CAAY;AAAA,EACtC,CAAA;AACA,EAAA,mBAAA,CAAoB,GAAA,EAAK,QAAQ,CAAA;AACjC,EAAA,OAAO,QAAA;AACT;ANuIA;AACA;AKxNO,SAAS,kBAAA,CAAmB,WAAA,EAAqB,4BAAA,EAA8B;AACpF,EAAA,MAAM,CAAC,KAAA,EAAO,QAAQ,EAAA,EAAIF,6BAAAA,CAA0B,EAAA,GAAM,mBAAA,CAAoB,UAAU,CAAC,CAAA;AACzF,EAAA,MAAM,CAAC,EAAE,WAAW,EAAA,EAAIA,6BAAAA,CAAU,CAAA;AAGlC,EAAAC,8BAAAA,CAAU,EAAA,GAAM;AACd,IAAA,MAAM,oBAAA,EAAsB,CAAC,CAAA,EAAA,GAAmB;AAC9C,MAAA,GAAA,CAAI,CAAA,CAAE,MAAA,CAAO,IAAA,IAAQ,UAAA,EAAY;AAC/B,QAAA,MAAM,SAAA,EAAW,mBAAA,CAAoB,UAAU,CAAA;AAC/C,QAAA,QAAA,CAAS,QAAQ,CAAA;AACjB,QAAA,WAAA,CAAY,CAAA,IAAA,EAAA,GAAQ,KAAA,EAAO,CAAC,CAAA;AAC5B,QAAA,OAAA,CAAQ,GAAA,CAAI,6CAAA,EAAwC,QAAQ,CAAA;AAAA,MAC9D;AAAA,IACF,CAAA;AAEA,IAAA,MAAA,CAAO,gBAAA,CAAiB,oBAAA,EAAsB,mBAAoC,CAAA;AAClF,IAAA,OAAO,CAAA,EAAA,GAAM;AACX,MAAA,MAAA,CAAO,mBAAA,CAAoB,oBAAA,EAAsB,mBAAoC,CAAA;AAAA,IACvF,CAAA;AAAA,EACF,CAAA,EAAG,CAAC,UAAU,CAAC,CAAA;AAEf,EAAA,MAAM,aAAA,EAAeE,gCAAAA,CAAa,MAAA,EAAA,GAAmB;AACnD,IAAA,OAAA,CAAQ,GAAA,CAAI,CAAA,oCAAA,EAAgC,MAAM,CAAA,CAAA,CAAG,CAAA;AACrD,IAAA,MAAM,SAAA,EAAW,gBAAA,CAAoB,UAAA,EAAY,MAAM,CAAA;AACvD,IAAA,QAAA,CAAS,QAAQ,CAAA;AACjB,IAAA,WAAA,CAAY,CAAA,IAAA,EAAA,GAAQ,KAAA,EAAO,CAAC,CAAA;AAAA,EAC9B,CAAA,EAAG,CAAC,UAAU,CAAC,CAAA;AAEf,EAAA,MAAM,YAAA,EAAcA,gCAAAA,CAAa,MAAA,EAAA,GAAmB;AAClD,IAAA,OAAA,CAAQ,GAAA,CAAI,CAAA,sCAAA,EAA+B,MAAM,CAAA,CAAA,CAAG,CAAA;AACpD,IAAA,MAAM,SAAA,EAAW,eAAA,CAAmB,UAAA,EAAY,MAAM,CAAA;AACtD,IAAA,QAAA,CAAS,QAAQ,CAAA;AACjB,IAAA,WAAA,CAAY,CAAA,IAAA,EAAA,GAAQ,KAAA,EAAO,CAAC,CAAA;AAAA,EAC9B,CAAA,EAAG,CAAC,UAAU,CAAC,CAAA;AAEf,EAAA,MAAMC,mBAAAA,EAAoBD,gCAAAA,CAAY,EAAA,GAAM;AAC1C,IAAA,OAAA,CAAQ,GAAA,CAAI,CAAA,kCAAA,CAA6B,CAAA;AACzC,IAAA,MAAM,SAAA,EAAW,iBAAA,CAAe,UAAU,CAAA;AAC1C,IAAA,QAAA,CAAS,QAAQ,CAAA;AACjB,IAAA,WAAA,CAAY,CAAA,IAAA,EAAA,GAAQ,KAAA,EAAO,CAAC,CAAA;AAAA,EAC9B,CAAA,EAAG,CAAC,UAAU,CAAC,CAAA;AAEf,EAAA,MAAME,sBAAAA,EAAuBF,gCAAAA,CAAa,OAAA,EAAA,GAAsB;AAC9D,IAAA,OAAA,CAAQ,GAAA,CAAI,CAAA,0CAAA,CAAA,EAAuC,OAAO,CAAA;AAC1D,IAAA,MAAM,SAAA,EAAW,oBAAA,CAAoB,UAAA,EAAY,OAAO,CAAA;AACxD,IAAA,QAAA,CAAS,QAAQ,CAAA;AACjB,IAAA,WAAA,CAAY,CAAA,IAAA,EAAA,GAAQ,KAAA,EAAO,CAAC,CAAA;AAC5B,IAAA,OAAA,CAAQ,GAAA,CAAI,CAAA,4BAAA,CAAA,EAAyB,QAAQ,CAAA;AAAA,EAC/C,CAAA,EAAG,CAAC,UAAU,CAAC,CAAA;AAEf,EAAA,MAAM,eAAA,EAAiBA,gCAAAA,CAAa,MAAA,EAAA,GAA4B;AAC9D,IAAA,OAAO,KAAA,CAAM,cAAA,CAAe,QAAA,CAAS,MAAM,CAAA;AAAA,EAC7C,CAAA,EAAG,CAAC,KAAA,CAAM,cAAc,CAAC,CAAA;AAEzB,EAAA,MAAM,cAAA,EAAgBA,gCAAAA,CAAa,MAAA,EAAA,GAA4B;AAC7D,IAAA,OAAO,KAAA,CAAM,YAAA,CAAa,QAAA,CAAS,MAAM,CAAA;AAAA,EAC3C,CAAA,EAAG,CAAC,KAAA,CAAM,YAAY,CAAC,CAAA;AAEvB,EAAA,MAAM,iBAAA,EAAmBA,gCAAAA,CAAa,KAAA,EAAA,GAA2C;AAC/E,IAAA,OAAO,KAAA,CAAM,KAAA,CAAM,CAAA,IAAA,EAAA,GAAQ,cAAA,CAAe,IAAA,CAAK,EAAE,EAAA,GAAK,aAAA,CAAc,IAAA,CAAK,EAAE,CAAC,CAAA;AAAA,EAC9E,CAAA,EAAG,CAAC,cAAA,EAAgB,aAAa,CAAC,CAAA;AAElC,EAAA,OAAO;AAAA,IACL,KAAA;AAAA,IACA,YAAA;AAAA,IACA,WAAA;AAAA,IACA,iBAAA,EAAAC,kBAAAA;AAAA,IACA,cAAA;AAAA,IACA,aAAA;AAAA,IACA,gBAAA;AAAA,IACA,oBAAA,EAAAC;AAAA,EACF,CAAA;AACF;AL+MA;AACA;AOvTA;AAmEO,SAAS,kBAAA,CACd,MAAA,EACmC;AACnC,EAAA,OAAO,4BAAA,CAAQ,EAAA,GAAM;AACnB,IAAA,GAAA,CAAI,CAAC,MAAA,EAAQ,OAAO,KAAA,CAAA;AAEpB,IAAA,GAAA,CAAI,MAAA,CAAO,KAAA,IAAS,QAAA,EAAU;AAE5B,MAAA,GAAA,CAAI,MAAA,CAAO,WAAA,GAAc,CAAA,EAAG,OAAO,KAAA,CAAA;AAEnC,MAAA,OAAO;AAAA,QACL,WAAA,EAAa,MAAA,CAAO,YAAA,EAAc,MAAA,CAAO,UAAA;AAAA,QACzC,eAAA,EAAiB,MAAA,CAAO,YAAA,EAAc,CAAA;AAAA,QACtC,WAAA,EAAa,MAAA,CAAO,YAAA,IAAgB,CAAA;AAAA,QACpC,WAAA,EAAA,CAAc,MAAA,CAAO,YAAA,EAAc,CAAA,CAAA,CAAG,QAAA,CAAS,CAAA;AAAA,QAC/C,SAAA,EAAW,MAAA,CAAO,WAAA,CAAY,QAAA,CAAS,CAAA;AAAA,QACvC,YAAA,EAAc,MAAA,CAAO,SAAA;AAAA,QACrB,QAAA,EAAU,MAAA,CAAO,SAAA,GAAY,OAAA;AAAA,QAC7B,MAAA,EAAQ,CAAA,EAAA,GAAM,MAAA,CAAO,MAAA,CAAO,CAAA;AAAA,QAC5B,UAAA,EAAY,CAAA,EAAA,GAAM,MAAA,CAAO,UAAA,CAAW,CAAA;AAAA,QACpC,QAAA,mBAAU,MAAA,CAAO,QAAA,UAAY;AAAA,MAC/B,CAAA;AAAA,IACF,EAAA,KAAO;AAEL,MAAA,OAAO;AAAA,QACL,WAAA,EAAa,MAAA,CAAO,WAAA;AAAA,QACpB,eAAA,EAAiB,MAAA,CAAO,oBAAA;AAAA,QACxB,WAAA,EAAa,CAAC,MAAA,CAAO,oBAAA;AAAA,QACrB,WAAA,EAAa,MAAA,CAAO,WAAA;AAAA,QACpB,SAAA,EAAW,MAAA,CAAO,SAAA;AAAA,QAClB,YAAA,EAAc,MAAA,CAAO,SAAA;AAAA,QACrB,QAAA,EAAU,MAAA,CAAO,SAAA,GAAY,OAAA;AAAA,QAC7B,MAAA,EAAQ,MAAA,CAAO,MAAA;AAAA,QACf,UAAA,EAAY,MAAA,CAAO,OAAA;AAAA;AAAA,QACnB,QAAA,mBAAU,MAAA,CAAO,QAAA,UAAY;AAAA,MAC/B,CAAA;AAAA,IACF;AAAA,EACF,CAAA,EAAG,CAAC,MAAM,CAAC,CAAA;AACb;APkPA;AACA;AQ5VA;AAQO,SAAS,WAAA,CAAe,KAAA,EAAU,MAAA,EAAQ,GAAA,EAAQ;AACvD,EAAA,MAAM,CAAC,cAAA,EAAgB,iBAAiB,EAAA,EAAIL,6BAAAA,KAAiB,CAAA;AAC7D,EAAA,MAAM,YAAA,EAAcE,2BAAAA,IAAe,CAAK,GAAA,CAAI,CAAC,CAAA;AAE7C,EAAAD,8BAAAA,CAAU,EAAA,GAAM;AACd,IAAA,MAAM,IAAA,EAAM,IAAA,CAAK,GAAA,CAAI,CAAA;AACrB,IAAA,MAAM,QAAA,EAAU,IAAA,EAAM,WAAA,CAAY,OAAA;AAGlC,IAAA,GAAA,CAAI,QAAA,GAAW,KAAA,EAAO;AACpB,MAAA,iBAAA,CAAkB,KAAK,CAAA;AACvB,MAAA,WAAA,CAAY,QAAA,EAAU,GAAA;AAAA,IACxB,EAAA,KAAO;AAEL,MAAA,MAAM,QAAA,EAAU,UAAA,CAAW,CAAA,EAAA,GAAM;AAC/B,QAAA,iBAAA,CAAkB,KAAK,CAAA;AACvB,QAAA,WAAA,CAAY,QAAA,EAAU,IAAA,CAAK,GAAA,CAAI,CAAA;AAAA,MACjC,CAAA,EAAG,MAAA,EAAQ,OAAO,CAAA;AAElB,MAAA,OAAO,CAAA,EAAA,GAAM;AACX,QAAA,YAAA,CAAa,OAAO,CAAA;AAAA,MACtB,CAAA;AAAA,IACF;AAAA,EACF,CAAA,EAAG,CAAC,KAAA,EAAO,KAAK,CAAC,CAAA;AAEjB,EAAA,OAAO,cAAA;AACT;ARiVA;AACA;AStXA;AAMO,SAAS,aAAA,CAAA,EAAgB;AAC9B,EAAA,MAAM,CAAC,UAAA,EAAY,aAAa,EAAA,EAAID,6BAAAA;AAAS,IAC3C,KAAA,EAAO,CAAA;AAAA,IACP,MAAA,EAAQ;AAAA,EACV,CAAC,CAAA;AACD,EAAA,MAAM,CAAC,QAAA,EAAU,WAAW,EAAA,EAAIA,6BAAAA,KAAc,CAAA;AAE9C,EAAAM,oCAAAA,CAAgB,EAAA,GAAM;AACpB,IAAA,WAAA,CAAY,IAAI,CAAA;AAChB,IAAA,GAAA,CAAI,CAAC,QAAA,EAAU,MAAA;AAEf,IAAA,MAAM,aAAA,EAAe,CAAA,EAAA,GAAM;AACzB,MAAA,aAAA,CAAc;AAAA,QACZ,KAAA,EAAO,MAAA,CAAO,UAAA;AAAA,QACd,MAAA,EAAQ,MAAA,CAAO;AAAA,MACjB,CAAC,CAAA;AAAA,IACH,CAAA;AAGA,IAAA,YAAA,CAAa,CAAA;AAGb,IAAA,MAAA,CAAO,gBAAA,CAAiB,QAAA,EAAU,YAAY,CAAA;AAG9C,IAAA,OAAO,CAAA,EAAA,GAAM,MAAA,CAAO,mBAAA,CAAoB,QAAA,EAAU,YAAY,CAAA;AAAA,EAChE,CAAA,EAAG,CAAC,QAAQ,CAAC,CAAA;AAEb,EAAA,OAAO,UAAA;AACT;AT0WA;AACA;AU5YA;AAYO,SAAS,sBAAA,CAAA,EAAyB;AACvC,EAAA,MAAM,YAAA,EAAcJ,2BAAAA,IAAkC,CAAA;AACtD,EAAA,MAAM,SAAA,EAAWA,2BAAAA,IAA2B,CAAA;AAC5C,EAAA,MAAM,SAAA,EAAWA,2BAAAA,IAA2B,CAAA;AAC5C,EAAA,MAAM,MAAA,EAAQA,2BAAAA,IAAkC,CAAA;AAChD,EAAA,MAAM,CAAC,UAAA,EAAY,aAAa,EAAA,EAAIF,6BAAAA,CAAU,CAAA;AAG9C,EAAA,MAAM,CAAC,aAAA,EAAe,gBAAgB,EAAA,EAAIA,6BAAAA,KAAc,CAAA;AACxD,EAAA,MAAM,CAAC,cAAA,EAAgB,iBAAiB,EAAA,EAAIA,6BAAAA,KAAc,CAAA;AAC1D,EAAA,MAAM,qBAAA,EAAuBE,2BAAAA,KAAY,CAAA;AACzC,EAAA,MAAM,sBAAA,EAAwBA,2BAAAA,KAAY,CAAA;AAE1C,EAAA,MAAM,cAAA,EAAgBA,2BAAAA,KAAY,CAAA;AAClC,EAAA,MAAM,aAAA,EAAeA,2BAAAA,EAAS,MAAA,EAAQ,CAAA,EAAG,UAAA,EAAY,EAAE,CAAC,CAAA;AACxD,EAAA,MAAM,SAAA,EAAWA,2BAAAA,CAAgB,CAAA;AAGjC,EAAA,MAAM,eAAA,EAAiBC,gCAAAA,CAAY,EAAA,GAAM;AACvC,IAAA,MAAM,GAAA,EAAK,WAAA,CAAY,OAAA;AACvB,IAAA,MAAM,MAAA,EAAQ,QAAA,CAAS,OAAA;AACvB,IAAA,GAAA,CAAI,CAAC,GAAA,GAAM,CAAC,KAAA,EAAO,MAAA;AAEnB,IAAA,MAAM,UAAA,EAAY,EAAA,CAAG,YAAA,EAAc,EAAA,CAAG,WAAA;AACtC,IAAA,GAAA,CAAI,UAAA,GAAa,CAAA,EAAG;AAClB,MAAA,KAAA,CAAM,KAAA,CAAM,KAAA,EAAO,IAAA;AACnB,MAAA,MAAA;AAAA,IACF;AAEA,IAAA,MAAM,MAAA,EAAQ,EAAA,CAAG,YAAA,EAAc,EAAA,CAAG,WAAA;AAClC,IAAA,MAAM,SAAA,EAAW,IAAA,CAAK,GAAA,CAAI,IAAA,CAAK,GAAA,CAAI,EAAA,CAAG,UAAA,EAAY,CAAC,CAAA,EAAG,SAAS,EAAA,EAAI,SAAA;AACnE,IAAA,KAAA,CAAM,KAAA,CAAM,KAAA,EAAO,CAAA,EAAA;AAChB,EAAA;AAEiBA,EAAAA;AACT,IAAA;AACF,IAAA;AAES,IAAA;AACD,IAAA;AACC,IAAA;AACZ,MAAA;AACF,QAAA;AACA,QAAA;AACF,MAAA;AACI,MAAA;AACF,QAAA;AACA,QAAA;AACF,MAAA;AACA,MAAA;AACF,IAAA;AAEiB,IAAA;AACJ,IAAA;AACC,IAAA;AAED,IAAA;AACX,MAAA;AACiB,MAAA;AACnB,IAAA;AACc,IAAA;AACZ,MAAA;AACkB,MAAA;AACpB,IAAA;AACG,EAAA;AAEWA,EAAAA;AACH,IAAA;AACF,IAAA;AACQ,IAAA;AACH,IAAA;AACC,IAAA;AACD,IAAA;AACI,EAAA;AAGFA,EAAAA;AAEG,IAAA;AACH,MAAA;AACE,MAAA;AAClB,IAAA;AACa,IAAA;AACX,MAAA;AACS,MAAA;AACX,IAAA;AAEY,IAAA;AAEF,IAAA;AACM,MAAA;AACA,MAAA;AAEE,MAAA;AACF,MAAA;AAEd,MAAA;AACiB,QAAA;AACD,QAAA;AACf,MAAA;AACI,IAAA;AACU,MAAA;AACf,MAAA;AACA,MAAA;AACiB,MAAA;AACC,MAAA;AACpB,IAAA;AACW,EAAA;AAGIA,EAAAA;AACG,IAAA;AAEL,IAAA;AACM,IAAA;AACF,MAAA;AACD,MAAA;AACf,IAAA;AACiB,EAAA;AAGCA,EAAAA;AACW,IAAA;AAEhB,IAAA;AACH,IAAA;AACQ,IAAA;AAEF,IAAA;AACD,IAAA;AAEG,IAAA;AACA,IAAA;AACb,IAAA;AACF,IAAA;AAEE,IAAA;AAAsC,MAAA;AACxB,MAAA;AACnB,IAAA;AACK,IAAA;AACc,IAAA;AACjB,EAAA;AAGgBA,EAAAA;AACR,IAAA;AACF,IAAA;AACQ,IAAA;AACE,IAAA;AAChB,EAAA;AAGC,EAAA;AACa,IAAA;AACC,IAAA;AACP,IAAA;AACF,IAAA;AACK,IAAA;AACD,IAAA;AACI,IAAA;AACV,IAAA;AACM,IAAA;AACV,EAAA;AAGC,EAAA;AACe,IAAA;AACR,IAAA;AACG,IAAA;AACK,IAAA;AAEb,IAAA;AACF,IAAA;AAEe,IAAA;AACA,IAAA;AACb,IAAA;AACF,IAAA;AAEe,IAAA;AACC,IAAA;AACJ,IAAA;AACL,MAAA;AACT,MAAA;AACF,IAAA;AAEe,IAAA;AACD,IAAA;AACI,EAAA;AAGd,EAAA;AACU,IAAA;AACG,IAAA;AACV,IAAA;AACM,IAAA;AACV,EAAA;AAEE,EAAA;AACL,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AACF;AV2VwB;AACA;AW5jBfH;AAQA;AACQ,EAAA;AACI,EAAA;AACF,EAAA;AAED,EAAA;AACG,EAAA;AACE,EAAA;AACA,EAAA;AACN,EAAA;AACC,EAAA;AAEM,EAAA;AAEL,EAAA;AAGE,EAAA;AACA,EAAA;AAGA,EAAA;AACH,EAAA;AAEI,EAAA;AACE,IAAA;AAEhB,MAAA;AAGW,MAAA;AAEE,MAAA;AACA,MAAA;AACF,MAAA;AAEG,MAAA;AACD,MAAA;AACA,MAAA;AAGC,MAAA;AACA,MAAA;AACA,MAAA;AACC,MAAA;AAEA,MAAA;AACH,MAAA;AACEO,QAAAA;AACAC,QAAAA;AACAC,QAAAA;AACL,QAAA;AACJ,MAAA;AACO,QAAA;AACd,MAAA;AACF,IAAA;AACF,EAAA;AAEqB,EAAA;AAGuD,EAAA;AACvD,EAAA;AACA,IAAA;AACJ,MAAA;AACf,IAAA;AACF,EAAA;AAEmB,EAAA;AAGE,EAAA;AACA,EAAA;AACA,EAAA;AAEA,EAAA;AACvB;AAcgB;AACQ,EAAA;AAEN,EAAA;AACC,IAAA;AACI,MAAA;AACjB,MAAA;AACF,IAAA;AAEgB,IAAA;AACA,IAAA;AACE,IAAA;AAEC,IAAA;AACF,MAAA;AACX,MAAA;AACO,QAAA;AACH,MAAA;AACG,QAAA;AACX,MAAA;AACF,IAAA;AAEoB,IAAA;AACH,MAAA;AACE,MAAA;AACnB,IAAA;AAEU,IAAA;AAEG,IAAA;AAAc,MAAA;AAAM,IAAA;AACb,EAAA;AAEf,EAAA;AACT;AX8gBwB;AACA;AYlpBfT;AA8BoB;AACT,EAAA;AAEI,EAAA;AACN,EAAA;AACE,EAAA;AACI,EAAA;AAEhB,EAAA;AAEeG,EAAAA;AACN,IAAA;AACA,IAAA;AACV,EAAA;AAEW,EAAA;AAET,IAAA;AACU,MAAA;AACK,MAAA;AACL,MAAA;AACb,MAAA;AACF,IAAA;AAEgB,IAAA;AAEJ,IAAA;AACO,MAAA;AACJ,MAAA;AAET,MAAA;AACI,QAAA;AAEU,QAAA;AACH,UAAA;AACb,QAAA;AACY,MAAA;AACI,QAAA;AACL,UAAA;AACI,UAAA;AACf,QAAA;AACA,MAAA;AACgB,QAAA;AACD,UAAA;AACf,QAAA;AACF,MAAA;AACF,IAAA;AAEI,IAAA;AAES,IAAA;AACC,MAAA;AACd,IAAA;AAEkB,EAAA;AAEJ,EAAA;AAClB;AZ0mBwB;AACA;AalsBfA;AAqCO;AACd,EAAA;AACY,EAAA;AACE,EAAA;AACoC;AAChCD,EAAAA;AACCA,EAAAA;AACb,EAAA;AACWA,EAAAA;AACAA,EAAAA;AACI,EAAA;AAEDC,EAAAA;AAEA,IAAA;AACA,MAAA;AAChB,MAAA;AACF,IAAA;AAEe,IAAA;AACC,IAAA;AACA,IAAA;AACE,MAAA;AAChB,MAAA;AACF,IAAA;AACiB,IAAA;AACE,MAAA;AACjB,MAAA;AACF,IAAA;AAGW,IAAA;AACE,IAAA;AACA,IAAA;AACD,IAAA;AACI,IAAA;AAGF,IAAA;AACI,IAAA;AAGZ,IAAA;AAGY,IAAA;AAGH,IAAA;AACA,IAAA;AAGH,IAAA;AACQ,IAAA;AACD,MAAA;AACnB,IAAA;AACa,IAAA;AACK,MAAA;AAChB,MAAA;AACF,IAAA;AAGe,IAAA;AACT,IAAA;AAEK,IAAA;AACI,IAAA;AACK,IAAA;AACL,MAAA;AACK,MAAA;AACV,MAAA;AACR,MAAA;AACF,IAAA;AAEgB,IAAA;AACI,EAAA;AAGN,EAAA;AACF,IAAA;AACE,EAAA;AAGA,EAAA;AACH,IAAA;AACF,IAAA;AACM,IAAA;AACF,IAAA;AACG,IAAA;AACF,EAAA;AAEP,EAAA;AACX;AbwoBwB;AACA;Ac5wBfH;Ad8wBe;AACA;Ae7wBF;AAMT;AADgB;AAChB,EAAA;AACF,EAAA;AACC,EAAA;AACS,EAAA;AACF,EAAA;AACF,EAAA;AACA,EAAA;AACD,EAAA;AACC,EAAA;AACT,EAAA;AACK,EAAA;AACb;AAG8B;AACnB,EAAA;AACE,EAAA;AACD,EAAA;AACS,EAAA;AACF,EAAA;AACF,EAAA;AACA,EAAA;AACD,EAAA;AACC,EAAA;AACT,EAAA;AACK,EAAA;AACb;AAGa;AACF,EAAA;AACE,EAAA;AACD,EAAA;AACS,EAAA;AACF,EAAA;AACF,EAAA;AACA,EAAA;AACD,EAAA;AACC,EAAA;AACT,EAAA;AACK,EAAA;AACb;AAGa;AACF,EAAA;AACE,EAAA;AACD,EAAA;AACS,EAAA;AACb,EAAA;AACK,EAAA;AACb;AAG+B;AACpB,EAAA;AACE,EAAA;AACD,EAAA;AACS,EAAA;AACb,EAAA;AACK,EAAA;AACb;AAGiC;AACtB,EAAA;AACE,EAAA;AACD,EAAA;AACC,EAAA;AACQ,EAAA;AACF,EAAA;AACF,EAAA;AACA,EAAA;AACD,EAAA;AACC,EAAA;AACT,EAAA;AACR;AAGiC;AACtB,EAAA;AACE,EAAA;AACD,EAAA;AACC,EAAA;AACQ,EAAA;AACF,EAAA;AACF,EAAA;AACA,EAAA;AACD,EAAA;AACC,EAAA;AACT,EAAA;AACR;AAKgB;AACP,EAAA;AACT;AAKgB;AACP,EAAA;AACT;AAEgB;AACP,EAAA;AACQ,IAAA;AAAA;AACE,IAAA;AAAA;AACF,IAAA;AAAS;AACT,IAAA;AACO,IAAA;AACb,IAAA;AACP,EAAA;AACJ;AAKgB;AACO,EAAA;AACvB;AAKgB;AACQ,EAAA;AACxB;AAKgB;AACP,EAAA;AACT;AAKgB;AACP,EAAA;AACT;AAKgB;AACP,EAAA;AACT;AAKgB;AACI,EAAA;AAEI,EAAA;AACf,IAAA;AACI,MAAA;AACJ,IAAA;AACI,MAAA;AACJ,IAAA;AACA,IAAA;AACI,MAAA;AACJ,IAAA;AACI,MAAA;AACJ,IAAA;AACI,MAAA;AACJ,IAAA;AACI,MAAA;AACJ,IAAA;AACI,MAAA;AACJ,IAAA;AACI,MAAA;AACJ,IAAA;AACI,MAAA;AACJ,IAAA;AACL,IAAA;AACS,MAAA;AACX,EAAA;AACF;AAKgB;AACQ,EAAA;AACf,IAAA;AACI,MAAA;AACJ,IAAA;AACI,MAAA;AACJ,IAAA;AACA,IAAA;AACI,MAAA;AACJ,IAAA;AACI,MAAA;AACJ,IAAA;AACI,MAAA;AACJ,IAAA;AACI,MAAA;AACJ,IAAA;AACI,MAAA;AACJ,IAAA;AACI,MAAA;AACJ,IAAA;AACI,MAAA;AACJ,IAAA;AACL,IAAA;AACS,MAAA;AACX,EAAA;AACF;Af6tBwB;AACA;Ac76BqB;AACQ;AASrC;AACI,EAAA;AACA,EAAA;AACI,EAAA;AAEN,EAAA;AAEK,IAAA;AACJ,MAAA;AACK,MAAA;AAClB,MAAA;AACF,IAAA;AAGkB,IAAA;AAER,MAAA;AACS,QAAA;AACA,QAAA;AAED,MAAA;AACA,QAAA;AACC,QAAA;AACd,MAAA;AACH,MAAA;AACF,IAAA;AAGY,IAAA;AAEG,IAAA;AAEO,MAAA;AACA,QAAA;AAClB,MAAA;AACgB,MAAA;AAEJ,IAAA;AACNU,MAAAA;AACM,MAAA;AACIA,MAAAA;AACD,MAAA;AACRA,MAAAA;AACR,IAAA;AAGK,IAAA;AACa,MAAA;AACC,MAAA;AAEN,IAAA;AACE,MAAA;AACF,MAAA;AACM,MAAA;AACH,MAAA;AAChB,IAAA;AACA,EAAA;AAGC,EAAA;AACY,IAAA;AACE,IAAA;AACA,MAAA;AACA,MAAA;AAChB,IAAA;AACJ,EAAA;AAGM,EAAA;AAEC,EAAA;AACL,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AACF;AAKgB;AACQ,EAAA;AACA,EAAA;AACxB;AAKgB;AACQ,EAAA;AACA,EAAA;AACxB;Ad44BwB;AACA;AgBlgCNC;AhBogCM;AACA;AiBngCD;AACH;AjBqgCI;AACA;AkBlgCM;AACd,EAAA;AACH,EAAA;AACE,EAAA;AACF,EAAA;AACA,EAAA;AACK,EAAA;AACE,EAAA;AACT,EAAA;AACD,EAAA;AACV;AAOoD;AACpC,EAAA;AACH,EAAA;AACE,EAAA;AACF,EAAA;AACA,EAAA;AACK,EAAA;AACE,EAAA;AACT,EAAA;AACD,EAAA;AACV;AlB8/BwB;AACA;AmBphCvB;AAFK;AAAsC;AAE3CC,kBAAAA;AAAC,IAAA;AAAA,IAAA;AACW,MAAA;AACI,MAAA;AACA,MAAA;AAAA,IAAA;AAChB,EAAA;AAAA;AAG4F;AAC5E,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACM,EAAA;AACvB;AAQoD;AAG7B;AnBkhCC;AACA;AiBnjCxB;AAoQIC;AAhQ2D;AACpD,EAAA;AACA,EAAA;AACA,EAAA;AACF,EAAA;AACD,EAAA;AACR;AAEa;AACF,EAAA;AACA,EAAA;AACA,EAAA;AACF,EAAA;AACD,EAAA;AACR;AAaqB;AACnB,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACW,EAAA;AACG,EAAA;AACd,EAAA;AACe,EAAA;AACI;AAEjB,EAAA;AAAC,IAAA;AAAA,IAAA;AACY,MAAA;AACT,QAAA;AACA,QAAA;AACF,MAAA;AAEA,MAAA;AAAAD,wBAAAA;AAIA,wBAAA;AAEI,UAAA;AAGA,UAAA;AAEJ,QAAA;AAGE,QAAA;AAAC,UAAA;AAAA,UAAA;AACM,YAAA;AACL,YAAA;AACS,YAAA;AACC,YAAA;AAEV,YAAA;AAAqB,UAAA;AAErB,QAAA;AAEa,QAAA;AACd,UAAA;AAAA,UAAA;AACY,YAAA;AACT,cAAA;AACA,cAAA;AACF,YAAA;AACO,YAAA;AACL,cAAA;AACF,YAAA;AAAA,UAAA;AAEA,QAAA;AAAA,MAAA;AAAA,IAAA;AACN,EAAA;AAEJ;AAY0B;AACxB,EAAA;AACU,EAAA;AACV,EAAA;AACA,EAAA;AACW,EAAA;AACG,EAAA;AACd,EAAA;AACiB;AAEf,EAAA;AAAC,IAAA;AAAA,IAAA;AACM,MAAA;AACM,MAAA;AACT,QAAA;AACA,QAAA;AACF,MAAA;AAEA,MAAA;AAAC,QAAA;AAAA,QAAA;AACC,UAAA;AACA,UAAA;AACA,UAAA;AACA,UAAA;AACA,UAAA;AACA,UAAA;AAAA,QAAA;AACF,MAAA;AAAA,IAAA;AACF,EAAA;AAEJ;AAoBgB;AACd,EAAA;AACU,EAAA;AACF,EAAA;AACM,EAAA;AACd,EAAA;AACA,EAAA;AACA,EAAA;AACe,EAAA;AACD,EAAA;AACd,EAAA;AACA,EAAA;AACW,EAAA;AACG,EAAA;AACI,EAAA;AAClB,EAAA;AAC4B;AACX,EAAA;AAEK,EAAA;AACR,oBAAA;AACQ,IAAA;AACtB,EAAA;AAEqB,EAAA;AACR,oBAAA;AACS,IAAA;AACtB,EAAA;AAGE,EAAA;AAAC,IAAA;AAAA,IAAA;AACM,MAAA;AACM,MAAA;AACT,QAAA;AACA,QAAA;AACF,MAAA;AAEA,MAAA;AAAAA,wBAAAA;AAAC,UAAA;AAAA,UAAA;AACC,YAAA;AACA,YAAA;AACA,YAAA;AACA,YAAA;AACA,YAAA;AACA,YAAA;AACA,YAAA;AACU,YAAA;AAAA,UAAA;AACZ,QAAA;AAEAA,wBAAAA;AAAC,UAAA;AAAA,UAAA;AACW,YAAA;AACD,YAAA;AACT,YAAA;AAEA,YAAA;AACE,8BAAA;AACE,gCAAA;AAGC,gBAAA;AACH,cAAA;AAEA,8BAAA;AACG,gBAAA;AAKD,gCAAA;AACE,kCAAA;AAAC,oBAAA;AAAA,oBAAA;AACC,sBAAA;AACA,sBAAA;AACA,sBAAA;AACA,sBAAA;AAEC,sBAAA;AAAA,oBAAA;AACH,kBAAA;AACA,kCAAA;AAAC,oBAAA;AAAA,oBAAA;AACC,sBAAA;AACA,sBAAA;AACA,sBAAA;AACA,sBAAA;AAEC,sBAAA;AAAA,oBAAA;AACH,kBAAA;AACF,gBAAA;AACF,cAAA;AACF,YAAA;AAAA,UAAA;AACF,QAAA;AAEY,QAAA;AACT,UAAA;AAAA,UAAA;AACM,YAAA;AACI,YAAA;AACC,YAAA;AACV,YAAA;AAEA,YAAA;AAAA,8BAAA;AACA,8BAAA;AAA6B,YAAA;AAAA,UAAA;AAC/B,QAAA;AAAA,MAAA;AAAA,IAAA;AAEJ,EAAA;AAEJ;AAIwB;AACX,EAAA;AACF,EAAA;AACH,EAAA;AACN,EAAA;AACG,EAAA;AACiB;AACA,EAAA;AAGlB,EAAA;AACEA,oBAAAA;AAAQ;AAAA;AAAA;AAAA;AAKN,MAAA;AACFA,oBAAAA;AAAC,MAAA;AAAA,MAAA;AACC,QAAA;AACA,QAAA;AACA,QAAA;AACc,QAAA;AACF,UAAA;AACP,UAAA;AACS,UAAA;AACH,YAAA;AACJ,YAAA;AACL,UAAA;AACF,QAAA;AACI,QAAA;AAAA,MAAA;AACN,IAAA;AACF,EAAA;AAEJ;AAQ0B;AAEf,EAAA;AAEH,EAAA;AACJ,IAAA;AACA,IAAA;AACU,IAAA;AACC,IAAA;AACG,IAAA;AACX,IAAA;AACD,EAAA;AAEe,EAAA;AAEf,IAAA;AAAC,MAAA;AAAA,MAAA;AACC,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AAAA,MAAA;AACF,IAAA;AAEU,IAAA;AACd,EAAA;AACF;AAMgB;AACR,EAAA;AACJ,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACW,IAAA;AACG,IAAA;AACX,IAAA;AACD,EAAA;AAEe,EAAA;AAEf,IAAA;AAAC,MAAA;AAAA,MAAA;AACC,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AAAA,MAAA;AACF,IAAA;AAEU,IAAA;AACd,EAAA;AACF;AjBq/BwB;AACA;AgB91CE;AACR,EAAA;AACK,EAAA;AACd,EAAA;AACT;AAEsB;AACT,EAAA;AACU,IAAA;AACrB,EAAA;AAEe,EAAA;AACE,EAAA;AACf,IAAA;AACA,IAAA;AACS,IAAA;AACT,IAAA;AACA,IAAA;AACD,EAAA;AACH;AAE+B;AAC7B,EAAA;AACqB,EAAA;AACA,EAAA;AACvB;AhB61CwB;AACA;AoBl4CfZ;AAET;AAyCgB;AACE,EAAA;AACF,EAAA;AACC,EAAA;AAGI,EAAA;AACE,EAAA;AACH,EAAA;AAEHG,EAAAA;AACK,IAAA;AAEE,IAAA;AAEhB,IAAA;AACe,MAAA;AACP,QAAA;AACG,QAAA;AACA,QAAA;AACN,UAAA;AACM,UAAA;AACV,QAAA;AACF,MAAA;AAEY,MAAA;AAEK,MAAA;AACA,QAAA;AAClB,MAAA;AAGiB,MAAA;AACD,MAAA;AAIV,MAAA;AACG,QAAA;AACM,QAAA;AACJ,QAAA;AACV,MAAA;AACa,IAAA;AACE,MAAA;AACV,MAAA;AACG,QAAA;AACM,QAAA;AACJ,QAAA;AACV,MAAA;AACK,MAAA;AACN,IAAA;AACgB,MAAA;AAClB,IAAA;AACgBW,EAAAA;AAGF,EAAA;AACG,IAAA;AACH,MAAA;AACE,MAAA;AACA,QAAA;AACR,QAAA;AACK,UAAA;AACF,QAAA;AACO,UAAA;AACd,QAAA;AACK,MAAA;AAEM,MAAA;AACf,IAAA;AACa,EAAA;AAGC,EAAA;AACG,IAAA;AACL,MAAA;AACZ,IAAA;AACa,EAAA;AAEE,EAAA;AACnB;ApBy0CwB;AACA;AqBn8Cfb;AA0BO;AACd,EAAA;AACgB,EAAA;AACN,EAAA;AAC4C;AAC/C,EAAA;AACcC,EAAAA;AACFA,EAAAA;AACGA,EAAAA;AAChB,EAAA;AACcA,EAAAA;AAKd,EAAA;AACY,IAAA;AACH,IAAA;AACI,MAAA;AACF,MAAA;AACf,IAAA;AACO,IAAA;AACJ,EAAA;AAKYC,EAAAA;AACA,IAAA;AACA,MAAA;AACF,MAAA;AACb,IAAA;AACmB,IAAA;AACL,IAAA;AACE,IAAA;AACJ,IAAA;AACT,EAAA;AAKeA,EAAAA;AAEE,IAAA;AACN,MAAA;AACd,IAAA;AAGkB,IAAA;AACC,IAAA;AAGH,IAAA;AAGI,IAAA;AACF,MAAA;AACF,MAAA;AAChB,IAAA;AAGW,IAAA;AACI,EAAA;AAKD,EAAA;AAEE,IAAA;AACd,MAAA;AACF,IAAA;AAEW,IAAA;AACH,MAAA;AACF,MAAA;AACN,IAAA;AAGM,IAAA;AACU,MAAA;AACE,MAAA;AACJ,MAAA;AACR,IAAA;AAGO,IAAA;AACE,MAAA;AACJ,MAAA;AACX,IAAA;AACW,EAAA;AAEN,EAAA;AACL,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AACF;ArB44CwB;AACA;AsBxgDfA;AAWO;AACC,EAAA;AACM,EAAA;AACR,EAAA;AACM,EAAA;AACN,EAAA;AACoB;AACnB,EAAA;AACC,EAAA;AAEFA,EAAAA;AACa,IAAA;AAClB,MAAA;AACc,QAAA;AACF,QAAA;AACC,QAAA;AACJ,QAAA;AACL,MAAA;AACS,QAAA;AACjB,MAAA;AACF,IAAA;AACe,IAAA;AACjB,EAAA;AAEsB,EAAA;AACxB;AtB8/CwB;AACA;AuBriDfF;AAkBL;AAeY;AACd,EAAA;AACF;AAKS;AACA,EAAA;AACU,IAAA;AACA,IAAA;AACC,IAAA;AAClB,EAAA;AACF;AAmBsB;AAIiC,EAAA;AAEvC,EAAA;AACL,IAAA;AACT,EAAA;AAEQ,EAAA;AACH,IAAA;AACA,IAAA;AACL,EAAA;AAEsB,EAAA;AAChB,IAAA;AAEE,MAAA;AACS,MAAA;AACI,QAAA;AACN,MAAA;AACM,QAAA;AACN,MAAA;AACM,QAAA;AACV,MAAA;AACU,QAAA;AACjB,MAAA;AAGM,MAAA;AACS,MAAA;AAGyB,MAAA;AAC5B,QAAA;AACV,QAAA;AACU,QAAA;AACZ,MAAA;AAGI,MAAA;AACE,QAAA;AACI,UAAA;AACF,UAAA;AACM,YAAA;AACV,UAAA;AACc,QAAA;AAEhB,QAAA;AACF,MAAA;AAGiB,MAAA;AACP,QAAA;AACK,QAAA;AAAA;AACb,QAAA;AACD,MAAA;AAEiB,MAAA;AACA,QAAA;AAClB,MAAA;AAGa,MAAA;AACK,MAAA;AAET,MAAA;AACK,IAAA;AACD,MAAA;AACJ,MAAA;AACX,IAAA;AACD,EAAA;AAEoB,EAAA;AAEC,EAAA;AACA,IAAA;AACrB,EAAA;AAEM,EAAA;AACT;AA6BE;AAGsB,EAAA;AACN,EAAA;AAGGc,EAAAA;AACN,IAAA;AACD,IAAA;AACZ,EAAA;AAGsBb,EAAAA;AAEN,EAAA;AACC,IAAA;AACK,MAAA;AAClB,MAAA;AACF,IAAA;AAGoB,IAAA;AAEJ,IAAA;AACd,MAAA;AACF,IAAA;AAGoB,IAAA;AAEL,IAAA;AAEf,IAAA;AAEqB,MAAA;AAEH,IAAA;AACA,MAAA;AAED,IAAA;AACG,MAAA;AACjB,IAAA;AAGU,IAAA;AACG,MAAA;AACG,QAAA;AACT,UAAA;AACN,QAAA;AACD,MAAA;AACH,IAAA;AACoB,EAAA;AAEf,EAAA;AACT;AvB46CwB;AACA;AwBnpDfD;AAmByC;AAY/B;AACK;AAKlB;AAKgB;AAKb;AACc,EAAA;AACC,EAAA;AACV,IAAA;AACJ,MAAA;AACc,MAAA;AACpB,IAAA;AACF,EAAA;AACF;AAKsB;AACR,EAAA;AACd;AAkBgB;AACQ,EAAA;AACxB;AAKS;AACA,EAAA;AACU,IAAA;AACA,IAAA;AACC,IAAA;AAClB,EAAA;AACF;AAsCgB;AASP,EAAA;AACW,EAAA;AACI,EAAA;AAChB,EAAA;AAEU,EAAA;AACC,IAAA;AACb,MAAA;AACkB,MAAA;AACL,MAAA;AAET,MAAA;AACY,QAAA;AACH,QAAA;AACH,UAAA;AACR,QAAA;AACA,QAAA;AACF,MAAA;AACA,MAAA;AACF,IAAA;AAEQ,IAAA;AACY,MAAA;AACf,MAAA;AACL,IAAA;AAGI,IAAA;AACS,IAAA;AACI,MAAA;AACG,IAAA;AACA,MAAA;AACA,IAAA;AACA,MAAA;AACb,IAAA;AACa,MAAA;AACpB,IAAA;AAGiB,IAAA;AAEb,IAAA;AACgB,MAAA;AACH,MAAA;AACH,QAAA;AACZ,MAAA;AACF,IAAA;AAEmB,IAAA;AAEC,IAAA;AACH,IAAA;AACH,MAAA;AACA,MAAA;AAEZ,MAAA;AACkB,MAAA;AACL,MAAA;AACb,MAAA;AACF,IAAA;AAEM,IAAA;AACc,IAAA;AACD,MAAA;AACJ,MAAA;AAGV,MAAA;AACc,QAAA;AACG,UAAA;AACH,UAAA;AACH,YAAA;AACN,YAAA;AACF,UAAA;AACF,QAAA;AACa,QAAA;AAED,MAAA;AACH,QAAA;AACI,QAAA;AACd,MAAA;AACH,MAAA;AACF,IAAA;AAEiB,IAAA;AACJ,IAAA;AAEM,IAAA;AAGqB,IAAA;AAC5B,MAAA;AACO,MAAA;AACP,MAAA;AACZ,IAAA;AAGmB,IAAA;AACb,MAAA;AACI,QAAA;AACF,QAAA;AACM,UAAA;AACV,QAAA;AACc,MAAA;AAEhB,MAAA;AACF,IAAA;AAEMe,IAAAA;AACI,MAAA;AACK,MAAA;AAAA;AACb,MAAA;AAEM,IAAA;AACc,MAAA;AACA,QAAA;AAClB,MAAA;AACgB,MAAA;AAEJ,IAAA;AACM,MAAA;AAEH,MAAA;AACJ,QAAA;AACO,QAAA;AACN,QAAA;AACX,MAAA;AAED,MAAA;AACkB,MAAA;AACX,MAAA;AAEK,IAAA;AACH,MAAA;AACT,MAAA;AACkB,MAAA;AACZ,MAAA;AAEO,IAAA;AACG,MAAA;AACjB,IAAA;AAEiB,IAAA;AAER,EAAA;AAEE,EAAA;AACD,IAAA;AACP,MAAA;AACY,QAAA;AACH,QAAA;AACH,UAAA;AACR,QAAA;AACF,MAAA;AACF,IAAA;AACG,EAAA;AAEc,EAAA;AACrB;AxBsgDwB;AACA;AyB5xDxB;AADSf;AzBgyDe;AACA;A0B9yDxB;AAOE;AACK;AAiDS;AAGyC,EAAA;AAE1C,EAAA;AACQ,IAAA;AACC,MAAA;AACD,MAAA;AAEC,MAAA;AAChB,QAAA;AACM,QAAA;AACI,QAAA;AACD,QAAA;AACT,QAAA;AACF,MAAA;AACF,IAAA;AACD,EAAA;AAEM,EAAA;AACT;AASS;AACQ,EAAA;AACC,EAAA;AACH,EAAA;AAGS,EAAA;AACR,IAAA;AACQ,IAAA;AACtB,EAAA;AAGsB,EAAA;AACX,IAAA;AACW,IAAA;AAGP,IAAA;AACA,MAAA;AACb,IAAA;AACF,EAAA;AAGsB,EAAA;AACA,IAAA;AACtB,EAAA;AAEO,EAAA;AACL,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AACF;AAQS;AAEuC,EAAA;AAClC,IAAA;AACH,IAAA;AACE,IAAA;AACE,IAAA;AACL,IAAA;AACR,EAAA;AAGkB,EAAA;AACT,IAAA;AACT,EAAA;AAIO,EAAA;AACT;AAK6B;AACV,EAAA;AACF,EAAA;AACjB;AAKgB;AACO,EAAA;AACvB;A1B+sDwB;AACA;A2B7zDF;AAI+B,EAAA;AAE9B,EAAA;AAEC,IAAA;AACD,MAAA;AACD,QAAA;AACD,QAAA;AACA,QAAA;AACH,QAAA;AACM,QAAA;AAClB,MAAA;AACA,MAAA;AACF,IAAA;AAIiB,IAAA;AACAgB,MAAAA;AAEH,MAAA;AACA,QAAA;AACM,UAAA;AACD,UAAA;AACP,UAAA;AACI,UAAA;AACD,UAAA;AACX,QAAA;AACF,MAAA;AACK,IAAA;AAGY,MAAA;AACD,QAAA;AACD,QAAA;AACP,QAAA;AACI,QAAA;AACD,QAAA;AACX,MAAA;AACF,IAAA;AACF,EAAA;AAEO,EAAA;AACT;AAUE;AAGgD,EAAA;AAE1B,EAAA;AACN,IAAA;AACT,MAAA;AACW,MAAA;AAChB,IAAA;AACF,EAAA;AAEO,EAAA;AACT;AAU+B;AACP,EAAA;AAEA,EAAA;AACF,IAAA;AACN,MAAA;AACR,QAAA;AACF,MAAA;AACF,IAAA;AACkB,IAAA;AACpB,EAAA;AACF;AAW+B;AACP,EAAA;AAGxB;AAQgB;AACQ,EAAA;AAGxB;AAcgB;AAKQ,EAAA;AACb,IAAA;AACT,EAAA;AAGkB,EAAA;AACT,IAAA;AACT,EAAA;AAGkB,EAAA;AACT,IAAA;AACT,EAAA;AAGU,EAAA;AACD,IAAA;AACT,EAAA;AAEO,EAAA;AACT;A3B+uDwB;AACA;A4Bt6DR;AAI0B,EAAA;AAE5B,EAAA;AAEO,IAAA;AAKC,IAAA;AAEA,MAAA;AACC,QAAA;AACjB,MAAA;AACA,MAAA;AACF,IAAA;AAGc,IAAA;AAGC,IAAA;AACjB,EAAA;AAEO,EAAA;AACT;AAqBgB;AAIK,EAAA;AAEP,EAAA;AAEI,IAAA;AAGT,IAAA;AACH,MAAA;AACF,IAAA;AAGkB,IAAA;AAEF,MAAA;AACF,QAAA;AACM,UAAA;AAChB,QAAA;AACD,MAAA;AACI,IAAA;AAEM,MAAA;AACb,IAAA;AACF,EAAA;AAEO,EAAA;AACT;AAS4B;AAER,EAAA;AACC,IAAA;AACnB,EAAA;AAGc,EAAA;AACP,IAAA;AACS,MAAA;AACI,MAAA;AAEb,IAAA;AACc,MAAA;AAEd,IAAA;AAEU,MAAA;AAEV,IAAA;AACL,IAAA;AACS,MAAA;AACX,EAAA;AACF;AAe+B;AACV,EAAA;AACL,EAAA;AAGM,EAAA;AACE,IAAA;AACN,IAAA;AACK,MAAA;AACnB,IAAA;AACkB,IAAA;AACpB,EAAA;AAGiB,EAAA;AACG,EAAA;AACtB;AAe+B;AACR,EAAA;AACC,IAAA;AAChB,EAAA;AACR;AAaE;AAIoB,EAAA;AAER,EAAA;AACU,IAAA;AACF,IAAA;AAEH,IAAA;AACjB,EAAA;AAEO,EAAA;AACT;AAWE;AAIqB,EAAA;AAEV,EAAA;AACW,IAAA;AACF,IAAA;AAGH,IAAA;AACjB,EAAA;AAEO,EAAA;AACT;AAWgB;AAIY,EAAA;AAEd,EAAA;AACI,IAAA;AAGE,IAAA;AACF,MAAA;AACd,IAAA;AAGc,IAAA;AACN,MAAA;AACA,MAAA;AAEF,MAAA;AACK,QAAA;AACS,UAAA;AAChB,QAAA;AACF,MAAA;AACF,IAAA;AACF,EAAA;AAEO,EAAA;AACT;A5BmxDwB;AACA;A6B5iExB;AACE;AACA;AAKAC;AACK;AAkBM;AAAN,EAAA;AACkC,IAAA;AACpB,IAAA;AACX,IAAA;AACR;AAAQ,IAAA;AAAgB,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAgBtB,EAAA;AAKgB,IAAA;AACC,MAAA;AACD,MAAA;AACR,QAAA;AACY,UAAA;AACd,UAAA;AACc,QAAA;AACD,UAAA;AAEf,QAAA;AACF,MAAA;AACF,IAAA;AAGI,IAAA;AACe,MAAA;AACP,QAAA;AACC,QAAA;AACP,UAAA;AACG,UAAA;AACL,QAAA;AACW,QAAA;AACF,UAAA;AACR,QAAA;AACF,MAAA;AAEiB,MAAA;AACA,QAAA;AAClB,MAAA;AAEc,MAAA;AAEF,MAAA;AACM,QAAA;AAClB,MAAA;AAEW,MAAA;AACO,QAAA;AAClB,MAAA;AAGc,MAAA;AACG,MAAA;AACH,IAAA;AACA,MAAA;AACR,MAAA;AACR,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAemB,EAAA;AACC,IAAA;AACR,MAAA;AACV,IAAA;AAEkB,IAAA;AAEJA,IAAAA;AACJ,MAAA;AACV,IAAA;AAEoD,IAAA;AACjC,IAAA;AAEP,IAAA;AACQ,MAAA;AAEF,MAAA;AACR,QAAA;AACK,QAAA;AACD,QAAA;AACD,QAAA;AACT,QAAA;AACF,MAAA;AACF,IAAA;AAEO,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAKmC,EAAA;AACf,IAAA;AACC,IAAA;AACrB,EAAA;AAAA;AAAA;AAAA;AAKkC,EAAA;AACpB,IAAA;AACd,EAAA;AAAA;AAAA;AAAA;AAKoB,EAAA;AACN,IAAA;AACd,EAAA;AAAA;AAAA;AAAA;AAKmB,EAAA;AACb,IAAA;AACW,MAAA;AACC,MAAA;AACR,IAAA;AAER,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAAA;AAUE,EAAA;AACe,IAAA;AACC,IAAA;AACH,IAAA;AACM,IAAA;AAGP,IAAA;AACE,MAAA;AACF,MAAA;AACZ,IAAA;AAGY,IAAA;AACD,MAAA;AACC,MAAA;AACZ,IAAA;AAGY,IAAA;AACA,MAAA;AACZ,IAAA;AAGmB,IAAA;AAEA,IAAA;AACrB,EAAA;AAAA;AAAA;AAAA;AAK2B,EAAA;AACe,IAAA;AAC5B,MAAA;AACH,MAAA;AACE,MAAA;AACE,MAAA;AACL,MAAA;AACR,IAAA;AAEe,IAAA;AACjB,EAAA;AAAA;AAAA;AAAA;AAK4C,EAAA;AACtC,IAAA;AACa,MAAA;AACF,MAAA;AAEE,MAAA;AAGJ,MAAA;AACF,QAAA;AACT,MAAA;AAEO,MAAA;AACO,IAAA;AACD,MAAA;AACN,MAAA;AACT,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAKsD,EAAA;AAChD,IAAA;AACyB,MAAA;AACzB,QAAA;AACgB,QAAA;AACF,QAAA;AAChB,MAAA;AAEa,MAAA;AACC,IAAA;AACD,MAAA;AAEf,IAAA;AACF,EAAA;AACF;AAgBgC;A7B09DR;AACA;AyBvpEtB;AAGe,EAAA;AACM,EAAA;AAEH,EAAA;AACF,EAAA;AACM,EAAA;AACP,EAAA;AAGO,EAAA;AAChB,EAAA;AACgB,EAAA;AAGN,EAAA;AACC,IAAA;AACT,MAAA;AACS,QAAA;AAGL,QAAA;AAEK,QAAA;AACG,UAAA;AACd,QAAA;AAGK,QAAA;AACG,UAAA;AAKQ,UAAA;AACR,YAAA;AACI,cAAA;AACK,cAAA;AACJ,YAAA;AACC,cAAA;AAEV,YAAA;AACF,UAAA;AACF,QAAA;AAGI,QAAA;AAGQ,QAAA;AACV,UAAA;AACF,QAAA;AAGA,QAAA;AAGe,QAAA;AAEJ,QAAA;AACG,UAAA;AACd,QAAA;AAEU,QAAA;AACK,QAAA;AACH,MAAA;AACE,QAAA;AACA,QAAA;AACA,QAAA;AACd,MAAA;AACa,QAAA;AACf,MAAA;AACF,IAAA;AAEW,IAAA;AACM,EAAA;AAGDH,EAAAA;AACF,IAAA;AACL,MAAA;AACT,IAAA;AAEI,IAAA;AACI,MAAA;AACW,MAAA;AAEN,MAAA;AACG,QAAA;AACd,MAAA;AAEO,MAAA;AACK,IAAA;AACE,MAAA;AACP,MAAA;AACT,IAAA;AACgB,EAAA;AAGHA,EAAAA;AACwB,IAAA;AACxB,IAAA;AACM,MAAA;AAEJ,QAAA;AACK,UAAA;AAChB,QAAA;AACY,QAAA;AACP,MAAA;AACS,QAAA;AAChB,MAAA;AACD,IAAA;AACM,IAAA;AACQ,EAAA;AAGCZ,EAAAA;AACJ,IAAA;AAED,IAAA;AACG,MAAA;AACd,IAAA;AAGoB,IAAA;AACJ,EAAA;AAGDA,EAAAA;AACD,IAAA;AACC,MAAA;AACb,MAAA;AACF,IAAA;AAEI,IAAA;AACI,MAAA;AACU,MAAA;AACE,MAAA;AACR,MAAA;AACE,IAAA;AACE,MAAA;AAChB,IAAA;AACa,EAAA;AAGGA,EAAAA;AACF,IAAA;AACC,MAAA;AACb,MAAA;AACF,IAAA;AAEI,IAAA;AACI,MAAA;AACU,MAAA;AACE,MAAA;AACR,MAAA;AACE,IAAA;AACE,MAAA;AAChB,IAAA;AACa,EAAA;AAGT,EAAA;AACU,IAAA;AACC,MAAA;AACb,MAAA;AACF,IAAA;AAEI,IAAA;AACI,MAAA;AACU,MAAA;AACE,MAAA;AACR,MAAA;AACE,IAAA;AACE,MAAA;AAChB,IAAA;AACa,EAAA;AAGKA,EAAAA;AACP,IAAA;AACG,MAAA;AACd,IAAA;AAEe,IAAA;AACC,EAAA;AAEX,EAAA;AACL,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACa,IAAA;AACb,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AACF;AAKS;AAIyC,EAAA;AAE1B,EAAA;AACL,IAAA;AACE,IAAA;AACZ,MAAA;AACW,MAAA;AAChB,IAAA;AACF,EAAA;AAEO,EAAA;AACT;AzB0lEwB;AACA;A8Bz4ExB;AACSA;AAaA;AACmD,EAAA;AACvC,EAAA;AACH,EAAA;AACT,EAAA;AACT;AAOS;AAIY,EAAA;AACC,EAAA;AACF,IAAA;AAClB,EAAA;AACO,EAAA;AACT;AAsKE;AAGe,EAAA;AACT,EAAA;AACgB,EAAA;AAcD,EAAA;AAIHY,EAAAA;AACG,EAAA;AAMf,EAAA;AAC+C,IAAA;AAElC,IAAA;AACE,MAAA;AACD,QAAA;AACD,QAAA;AACA,QAAA;AACC,QAAA;AACJ,QAAA;AACM,QAAA;AAClB,MAAA;AACF,IAAA;AAEO,IAAA;AACK,EAAA;AAKQb,EAAAA;AAEPa,EAAAA;AACE,IAAA;AAC0B,IAAA;AAC5B,IAAA;AAEI,IAAA;AAEE,MAAA;AAKb,MAAA;AACa,MAAA;AACP,QAAA;AACH,MAAA;AACU,QAAA;AACjB,MAAA;AAIkB,MAAA;AACR,QAAA;AACV,MAAA;AAEc,MAAA;AAChB,IAAA;AAEW,IAAA;AACG,MAAA;AACd,IAAA;AAEc,IAAA;AACP,IAAA;AACS,EAAA;AAGZ,EAAA;AAKU,IAAA;AACZ,MAAA;AACF,IAAA;AAEkB,IAAA;AACF,MAAA;AACF,QAAA;AACK,UAAA;AACf,QAAA;AACD,MAAA;AACe,IAAA;AAEC,MAAA;AACZ,IAAA;AACY,MAAA;AACnB,IAAA;AACG,EAAA;AAMC,EAAA;AACc,IAAA;AAEA,IAAA;AACD,MAAA;AACT,MAAA;AAGD,MAAA;AACH,QAAA;AACF,MAAA;AAEA,MAAA;AACF,IAAA;AAEO,IAAA;AACG,EAAA;AAQMZ,EAAAA;AAEI,IAAA;AAGP,IAAA;AACA,MAAA;AACG,QAAA;AACd,MAAA;AACD,IAAA;AAIkB,IAAA;AAEN,MAAA;AACG,QAAA;AACd,MAAA;AACD,IAAA;AAIkB,IAAA;AAEN,MAAA;AACO,QAAA;AAEF,UAAA;AACP,QAAA;AAEO,UAAA;AACd,QAAA;AACF,MAAA;AACD,IAAA;AAEW,IAAA;AAID,IAAA;AACG,MAAA;AACd,IAAA;AAGoB,IAAA;AACH,EAAA;AAGG,EAAA;AACN,IAAA;AACL,MAAA;AACT,IAAA;AACkB,IAAA;AAEH,MAAA;AACf,IAAA;AACO,IAAA;AACT,EAAA;AAIiBA,EAAAA;AAIA,IAAA;AAEF,IAAA;AACE,MAAA;AACb,MAAA;AACF,IAAA;AAEkB,IAAA;AAED,IAAA;AACL,MAAA;AACL,IAAA;AACL,MAAA;AACU,MAAA;AACZ,IAAA;AACa,EAAA;AAIGA,EAAAA;AAGE,IAAA;AACZ,IAAA;AAEW,IAAA;AACA,MAAA;AAEF,MAAA;AACE,QAAA;AACb,QAAA;AACF,MAAA;AAEiB,MAAA;AACF,QAAA;AACR,MAAA;AACL,QAAA;AACF,MAAA;AACF,IAAA;AAEU,IAAA;AACG,EAAA;AAGKA,EAAAA;AACA,IAAA;AACR,IAAA;AACE,EAAA;AAGMA,EAAAA;AACP,IAAA;AACG,MAAA;AACd,IAAA;AAEe,IAAA;AACC,EAAA;AAEX,EAAA;AACL,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACAgB,IAAAA;AACA,IAAA;AACF,EAAA;AACF;AAUgB;AACO,EAAA;AAEC,EAAA;AACN,IAAA;AACZ,MAAA;AACF,IAAA;AAEkB,IAAA;AACF,MAAA;AACF,QAAA;AACK,UAAA;AACf,QAAA;AACD,MAAA;AACe,IAAA;AAEC,MAAA;AACZ,IAAA;AACY,MAAA;AACnB,IAAA;AACF,EAAA;AAEO,EAAA;AACT;A9BmlEwB;AACA;A+B1kFfhB;AAsBS;AACA,EAAA;AACA,EAAA;AAClB;AA0BgB;AAGR,EAAA;AACJ,IAAA;AACA,IAAA;AACE,EAAA;AAEY,EAAA;AAGI,EAAA;AAGb,EAAA;AAEA,EAAA;AAEeD,EAAAA;AAEhB,EAAA;AAEA,EAAA;AAIU,EAAA;AAEV,IAAA;AAEc,IAAA;AAEA,IAAA;AACC,MAAA;AACF,MAAA;AAEE,MAAA;AAAE,QAAA;AAAqC,MAAA;AAC1D,IAAA;AAEiB,EAAA;AAGH,EAAA;AAEV,IAAA;AAEA,IAAA;AAEgB,IAAA;AACR,MAAA;AACA,QAAA;AACA,QAAA;AAAA;AACT,MAAA;AACH,IAAA;AACe,EAAA;AAGD,EAAA;AACV,IAAA;AACa,MAAA;AACA,MAAA;AAGD,MAAA;AAGF,MAAA;AACV,QAAA;AACF,MAAA;AAIgB,MAAA;AACd,QAAA;AACA,QAAA;AACD,MAAA;AACH,IAAA;AAEG,EAAA;AAGW,EAAA;AAEV,IAAA;AACA,IAAA;AAEE,IAAA;AAIY,IAAA;AACF,MAAA;AACd,MAAA;AACe,MAAA;AACjB,IAAA;AACiB,EAAA;AAGb,EAAA;AACsB,IAAA;AACf,MAAA;AACK,MAAA;AACd,MAAA;AACF,IAAA;AACS,IAAA;AACX,EAAA;AAEM,EAAA;AACuC,IAAA;AAChC,MAAA;AACK,MAAA;AACd,MAAA;AACF,IAAA;AACS,IAAA;AACX,EAAA;AAEO,EAAA;AACL,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AACF;A/Bo/EwB;AACA;AgC/rFfD;AA0BP;AAGsB,EAAA;AAEPc,EAAAA;AACO,IAAA;AACb,IAAA;AACS,EAAA;AAEH,EAAA;AACG,EAAA;AAEF,EAAA;AACD,IAAA;AACD,MAAA;AACO,MAAA;AACjB,MAAA;AACF,IAAA;AAEmB,IAAA;AACD,MAAA;AACN,MAAA;AACX,IAAA;AAEgB,IAAA;AACE,MAAA;AACD,QAAA;AACJ,QAAA;AACX,MAAA;AACH,IAAA;AAEa,IAAA;AACP,MAAA;AACW,MAAA;AAEd,MAAA;AACH,IAAA;AACU,EAAA;AAEL,EAAA;AACL,IAAA;AACA,IAAA;AACa,IAAA;AACb,IAAA;AACF,EAAA;AACF;AhC8pFwB;AACA;AiC1sFfd;AAIS;AACE;AAEI;AACL,EAAA;AACI,EAAA;AAEN,EAAA;AACA,IAAA;AACM,MAAA;AACJ,QAAA;AAGA,QAAA;AACH,QAAA;AACH,UAAA;AACU,UAAA;AACD,UAAA;AACd,QAAA;AACD,MAAA;AACH,IAAA;AACa,IAAA;AACf,EAAA;AACc,EAAA;AACP,EAAA;AACT;AAegB;AAGC,EAAA;AACDC,EAAAA;AAGFC,EAAAA;AACU,IAAA;AACL,MAAA;AAIH,MAAA;AACF,QAAA;AACS,QAAA;AACD,UAAA;AACE,0BAAA;AAChB,QAAA;AACF,MAAA;AAEgB,MAAA;AACL,MAAA;AAEM,MAAA;AACD,MAAA;AACD,MAAA;AACjB,IAAA;AACW,IAAA;AACb,EAAA;AAGgB,EAAA;AACD,IAAA;AACM,MAAA;AACR,MAAA;AACO,MAAA;AACF,QAAA;AACE,wBAAA;AAChB,MAAA;AACF,IAAA;AACa,EAAA;AAEM,EAAA;AACvB;AjC2qFwB;AACA;AkChxFN;AlCkxFM;AACA;AmCtvFF;AAKhB,EAAA;AACe,IAAA;AACP,MAAA;AACC,MAAA;AACS,QAAA;AAClB,MAAA;AACW,MAAA;AACZ,IAAA;AAEiB,IAAA;AACF,MAAA;AACE,MAAA;AAClB,IAAA;AAEa,IAAA;AACC,EAAA;AACP,IAAA;AACE,MAAA;AACE,MAAA;AACX,IAAA;AACF,EAAA;AACF;AAqBsB;AAKhB,EAAA;AACe,IAAA;AACP,MAAA;AACC,MAAA;AACS,QAAA;AAClB,MAAA;AACW,MAAA;AACZ,IAAA;AAEiB,IAAA;AACF,MAAA;AACE,MAAA;AAClB,IAAA;AAEa,IAAA;AACC,EAAA;AACP,IAAA;AACI,MAAA;AACC,MAAA;AACD,MAAA;AACX,IAAA;AACF,EAAA;AACF;AAsBsB;AAMD,EAAA;AAEH,EAAA;AACP,IAAA;AACT,EAAA;AAGoB,EAAA;AAEb,EAAA;AACF,IAAA;AACO,IAAA;AACD,IAAA;AAGX,EAAA;AACF;AnCwrFwB;AACA;AkC9xFR;AACE,EAAA;AACE,EAAA;AACG,EAAA;AACD,EAAA;AAEH,EAAA;AACK,IAAA;AAChB,IAAA;AACW,MAAA;AACb,IAAA;AACgB,MAAA;AAClB,IAAA;AACF,EAAA;AAEgB,EAAA;AACK,IAAA;AACf,IAAA;AACW,MAAA;AACb,IAAA;AACe,MAAA;AACjB,IAAA;AACF,EAAA;AAEM,EAAA;AACgB,IAAA;AACD,IAAA;AACf,IAAA;AACW,MAAA;AACb,IAAA;AACgB,MAAA;AACD,MAAA;AACjB,IAAA;AACF,EAAA;AAEO,EAAA;AACL,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACc,IAAA;AAChB,EAAA;AACF;AlC4xFwB;AACA;AoCr4FfY;AA4BO;AAOO,EAAA;AACF,IAAA;AAC+C,IAAA;AAClD,IAAA;AACC,IAAA;AACC,IAAA;AACG,EAAA;AACvB;ApCs2FwB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/chunk-5V6MSE3B.cjs","sourcesContent":[null,"\"use client\"\n\nimport { useState, useEffect } from \"react\"\n\n/**\n * Hook to debounce a value\n * @param value - Value to debounce\n * @param delay - Delay in milliseconds\n * @returns Debounced value\n */\nexport function useDebounce<T>(value: T, delay = 500): T {\n const [debouncedValue, setDebouncedValue] = useState<T>(value)\n\n useEffect(() => {\n // Set up timeout to update debounced value\n const timer = setTimeout(() => {\n setDebouncedValue(value)\n }, delay)\n\n // Clean up timeout on value change or unmount\n return () => {\n clearTimeout(timer)\n }\n }, [value, delay])\n\n return debouncedValue\n}\n","\"use client\"\n\nimport { useEffect, useRef, useState } from \"react\"\n\n/**\n * Hook to use localStorage with state\n * @param key - localStorage key\n * @param initialValue - Initial value if key doesn't exist\n * @returns [storedValue, setValue] tuple\n */\nexport function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T | ((val: T) => T)) => void] {\n // State to store our value\n // Use lazy initialization to read from localStorage synchronously on first render\n const [storedValue, setStoredValue] = useState<T>(() => {\n try {\n if (typeof window !== \"undefined\") {\n const item = window.localStorage.getItem(key)\n // Parse stored json or if none return initialValue\n return item ? JSON.parse(item) : initialValue\n }\n } catch (error) {\n console.error(`Error reading localStorage key \"${key}\":`, error)\n }\n return initialValue\n })\n\n // Use a ref to track if we've initialized from localStorage\n const isInitialized = useRef(true) // Set to true since we initialize in useState\n // Use a ref to track if the current state change came from a storage event\n const isFromStorageEvent = useRef(false)\n\n // Listen for storage events to sync with other tabs/components\n useEffect(() => {\n if (!isInitialized.current) return\n\n const handleStorageChange = (e: StorageEvent) => {\n if (e.key === key && e.newValue !== null) {\n try {\n const newValue = JSON.parse(e.newValue)\n isFromStorageEvent.current = true\n setStoredValue(newValue)\n console.log(`🔄 Updated localStorage key \"${key}\" from storage event`)\n } catch (error) {\n console.error(`Error parsing storage event for key \"${key}\":`, error)\n }\n }\n }\n\n const handleCustomStorageUpdate = (e: CustomEvent) => {\n if (e.detail.key === key) {\n try {\n if (e.detail.newValue === null || e.detail.newValue === undefined) {\n // localStorage was cleared or key was removed\n isFromStorageEvent.current = true\n setStoredValue(initialValue)\n console.log(`🔄 Cleared localStorage key \"${key}\" from custom storage event`)\n } else {\n const newValue = JSON.parse(e.detail.newValue)\n isFromStorageEvent.current = true\n setStoredValue(newValue)\n console.log(`🔄 Updated localStorage key \"${key}\" from custom storage event`)\n }\n } catch (error) {\n console.error(`Error parsing custom storage event for key \"${key}\":`, error)\n }\n }\n }\n\n window.addEventListener('storage', handleStorageChange)\n window.addEventListener('localStorageUpdate', handleCustomStorageUpdate as EventListener)\n \n return () => {\n window.removeEventListener('storage', handleStorageChange)\n window.removeEventListener('localStorageUpdate', handleCustomStorageUpdate as EventListener)\n }\n }, [key])\n\n // Update localStorage when state changes, but only after initialization and NOT from storage events\n useEffect(() => {\n if (!isInitialized.current) return\n\n // Skip localStorage write if this state change came from a storage event\n if (isFromStorageEvent.current) {\n isFromStorageEvent.current = false\n return\n }\n\n try {\n // Save state to localStorage\n if (typeof window !== \"undefined\") {\n window.localStorage.setItem(key, JSON.stringify(storedValue))\n }\n } catch (error) {\n console.error(`Error writing to localStorage key \"${key}\":`, error)\n }\n }, [key, storedValue])\n\n // Return a wrapped version of useState's setter function that\n // persists the new value to localStorage\n const setValue = (value: T | ((val: T) => T)) => {\n try {\n // Allow value to be a function so we have same API as useState\n const valueToStore = value instanceof Function ? value(storedValue) : value\n // Save state\n setStoredValue(valueToStore)\n } catch (error) {\n console.error(`Error setting value for localStorage key \"${key}\":`, error)\n }\n }\n\n return [storedValue, setValue]\n}\n","\"use client\"\n\nimport { useLayoutEffect, useState } from \"react\"\n\n/**\n * Hook to check if a media query matches\n * @param query - Media query to check\n * @returns Whether the media query matches, or undefined during SSR/initial render\n */\nexport function useMediaQuery(\n query: string,\n): boolean | undefined {\n const [matches, setMatches] = useState<boolean | undefined>(undefined)\n\n useLayoutEffect(() => {\n const matchMedia = window.matchMedia(query)\n\n const handleChange = () => {\n setMatches(matchMedia.matches)\n }\n\n // Set initial value\n handleChange()\n\n matchMedia.addEventListener('change', handleChange)\n\n return () => {\n matchMedia.removeEventListener('change', handleChange)\n }\n }, [query])\n\n return matches\n}\n\n/**\n * Predefined breakpoints for common screen sizes\n */\nexport const breakpoints = {\n md: \"(min-width: 800px)\", // Tablet: 50rem\n lg: \"(min-width: 1280px)\", // Desktop: 80rem\n}\n\n/** @deprecated Use useMdUp instead */\nexport function useSmUp(): boolean | undefined {\n return useMdUp()\n}\n\nexport function useMdUp(): boolean | undefined {\n const matches = useMediaQuery(breakpoints.md)\n return matches === undefined ? undefined : matches\n}\n\nexport function useLgUp(): boolean | undefined {\n const matches = useMediaQuery(breakpoints.lg)\n return matches === undefined ? undefined : matches\n}\n\n","\"use client\"\n\nimport { useCallback, useRef } from \"react\"\n\n/**\n * Hook to memoize a callback with dependencies\n * Similar to useCallback but with deep comparison of dependencies\n * @param callback - Function to memoize\n * @param dependencies - Dependencies array\n * @returns Memoized callback\n */\nexport function useMemoizedCallback<T extends (...args: any[]) => any>(callback: T, dependencies: any[]): T {\n // Store the callback and dependencies\n const callbackRef = useRef<T>(callback)\n const dependenciesRef = useRef<any[]>(dependencies)\n\n // Update the callback if it changes\n callbackRef.current = callback\n\n // Check if dependencies have changed\n const depsChanged = dependencies.some((dep, i) => !Object.is(dep, dependenciesRef.current[i]))\n\n // Update dependencies if they've changed\n if (depsChanged) {\n dependenciesRef.current = dependencies\n }\n\n // Return memoized callback\n return useCallback(((...args: any[]) => callbackRef.current(...args)) as T, [depsChanged])\n}\n","'use client'\n\nimport { useState, useEffect, useCallback } from 'react'\nimport {\n saveOnboardingState,\n loadOnboardingState,\n markStepComplete as storageMarkComplete,\n markStepSkipped as storageMarkSkipped,\n dismissOnboarding as storageDismiss,\n markMultipleComplete as storageMarkMultiple,\n isStepComplete as storageIsComplete,\n isStepSkipped as storageIsSkipped,\n type OnboardingState\n} from '../../utils/onboarding-storage'\n\nexport interface OnboardingStepConfig {\n id: string\n title: string\n description: string\n actionIcon: (color?: string) => React.ReactNode\n actionText: string\n completedText: string\n onAction: () => void | Promise<void>\n onSkip?: () => void\n checkComplete?: () => boolean | Promise<boolean>\n}\n\nexport type { OnboardingState }\n\n/**\n * Hook for managing onboarding state with localStorage persistence\n * Uses simple storage utilities for atomic updates and reliable re-renders\n */\nexport function useOnboardingState(storageKey: string = 'openframe-onboarding-state') {\n const [state, setState] = useState<OnboardingState>(() => loadOnboardingState(storageKey))\n const [, forceUpdate] = useState(0)\n\n // Listen for storage changes from other tabs\n useEffect(() => {\n const handleStorageUpdate = (e: CustomEvent) => {\n if (e.detail.key === storageKey) {\n const newState = loadOnboardingState(storageKey)\n setState(newState)\n forceUpdate(prev => prev + 1)\n console.log('🔄 State updated from storage event:', newState)\n }\n }\n\n window.addEventListener('localStorageUpdate', handleStorageUpdate as EventListener)\n return () => {\n window.removeEventListener('localStorageUpdate', handleStorageUpdate as EventListener)\n }\n }, [storageKey])\n\n const markComplete = useCallback((stepId: string) => {\n console.log(`🎯 markComplete called for: \"${stepId}\"`)\n const newState = storageMarkComplete(storageKey, stepId)\n setState(newState)\n forceUpdate(prev => prev + 1)\n }, [storageKey])\n\n const markSkipped = useCallback((stepId: string) => {\n console.log(`⏭️ markSkipped called for: \"${stepId}\"`)\n const newState = storageMarkSkipped(storageKey, stepId)\n setState(newState)\n forceUpdate(prev => prev + 1)\n }, [storageKey])\n\n const dismissOnboarding = useCallback(() => {\n console.log(`🚫 dismissOnboarding called`)\n const newState = storageDismiss(storageKey)\n setState(newState)\n forceUpdate(prev => prev + 1)\n }, [storageKey])\n\n const markMultipleComplete = useCallback((stepIds: string[]) => {\n console.log(`🎯 markMultipleComplete called for:`, stepIds)\n const newState = storageMarkMultiple(storageKey, stepIds)\n setState(newState)\n forceUpdate(prev => prev + 1)\n console.log(`📝 State after batch:`, newState)\n }, [storageKey])\n\n const isStepComplete = useCallback((stepId: string): boolean => {\n return state.completedSteps.includes(stepId)\n }, [state.completedSteps])\n\n const isStepSkipped = useCallback((stepId: string): boolean => {\n return state.skippedSteps.includes(stepId)\n }, [state.skippedSteps])\n\n const allStepsComplete = useCallback((steps: OnboardingStepConfig[]): boolean => {\n return steps.every(step => isStepComplete(step.id) || isStepSkipped(step.id))\n }, [isStepComplete, isStepSkipped])\n\n return {\n state,\n markComplete,\n markSkipped,\n dismissOnboarding,\n isStepComplete,\n isStepSkipped,\n allStepsComplete,\n markMultipleComplete\n }\n}\n","/**\n * Onboarding state storage utilities\n * Simple localStorage wrapper following announcement-storage.ts pattern\n * Provides atomic writes to prevent race conditions\n */\n\nexport interface OnboardingState {\n completedSteps: string[]\n skippedSteps: string[]\n dismissed: boolean\n lastUpdated: string\n}\n\nconst DEFAULT_STATE: OnboardingState = {\n completedSteps: [],\n skippedSteps: [],\n dismissed: false,\n lastUpdated: new Date().toISOString()\n}\n\n/**\n * Save onboarding state to localStorage (atomic write)\n */\nexport function saveOnboardingState(key: string, state: OnboardingState): void {\n if (typeof window === 'undefined') return\n\n try {\n localStorage.setItem(key, JSON.stringify(state))\n console.log('💾 Saved onboarding state:', state)\n\n // Dispatch custom event for cross-tab sync\n if (typeof window !== 'undefined') {\n window.dispatchEvent(\n new CustomEvent('localStorageUpdate', {\n detail: { key, newValue: JSON.stringify(state) }\n })\n )\n }\n } catch (err) {\n console.warn('[onboarding-storage] Failed saving to localStorage:', err)\n }\n}\n\n/**\n * Load onboarding state from localStorage\n */\nexport function loadOnboardingState(key: string): OnboardingState {\n if (typeof window === 'undefined') return DEFAULT_STATE\n\n try {\n const raw = localStorage.getItem(key)\n if (!raw) return DEFAULT_STATE\n\n const parsed = JSON.parse(raw) as OnboardingState\n return parsed\n } catch (err) {\n console.warn('[onboarding-storage] Failed parsing localStorage data:', err)\n return DEFAULT_STATE\n }\n}\n\n/**\n * Mark multiple steps as complete in a single atomic update\n */\nexport function markMultipleComplete(\n key: string,\n stepIds: string[]\n): OnboardingState {\n const state = loadOnboardingState(key)\n const newState: OnboardingState = {\n ...state,\n completedSteps: [...new Set([...state.completedSteps, ...stepIds])],\n skippedSteps: state.skippedSteps.filter(id => !stepIds.includes(id)),\n lastUpdated: new Date().toISOString()\n }\n saveOnboardingState(key, newState)\n return newState\n}\n\n/**\n * Mark a single step as complete\n */\nexport function markStepComplete(key: string, stepId: string): OnboardingState {\n return markMultipleComplete(key, [stepId])\n}\n\n/**\n * Mark a step as skipped\n */\nexport function markStepSkipped(key: string, stepId: string): OnboardingState {\n const state = loadOnboardingState(key)\n const newState: OnboardingState = {\n ...state,\n skippedSteps: [...new Set([...state.skippedSteps, stepId])],\n completedSteps: state.completedSteps.filter(id => id !== stepId),\n lastUpdated: new Date().toISOString()\n }\n saveOnboardingState(key, newState)\n return newState\n}\n\n/**\n * Dismiss the onboarding walkthrough\n */\nexport function dismissOnboarding(key: string): OnboardingState {\n const state = loadOnboardingState(key)\n const newState: OnboardingState = {\n ...state,\n dismissed: true,\n lastUpdated: new Date().toISOString()\n }\n saveOnboardingState(key, newState)\n return newState\n}\n\n/**\n * Check if a step is complete\n */\nexport function isStepComplete(key: string, stepId: string): boolean {\n const state = loadOnboardingState(key)\n return state.completedSteps.includes(stepId)\n}\n\n/**\n * Check if a step is skipped\n */\nexport function isStepSkipped(key: string, stepId: string): boolean {\n const state = loadOnboardingState(key)\n return state.skippedSteps.includes(stepId)\n}\n\n/**\n * Check if onboarding is dismissed\n */\nexport function isOnboardingDismissed(key: string): boolean {\n const state = loadOnboardingState(key)\n return state.dismissed\n}\n","'use client'\n\nimport { useMemo } from 'react'\nimport type { CursorPaginationProps } from '../../components/ui/cursor-pagination'\n\n/**\n * Client-side pagination configuration\n */\nexport interface ClientPaginationConfig {\n type: 'client'\n currentPage: number\n totalPages: number\n itemCount: number\n itemName?: string\n onNext: () => void\n onPrevious: () => void\n showInfo?: boolean\n}\n\n/**\n * Server-side cursor pagination configuration\n */\nexport interface ServerPaginationConfig {\n type: 'server'\n hasNextPage: boolean\n hasLoadedBeyondFirst: boolean\n startCursor?: string | null\n endCursor?: string | null\n itemCount: number\n itemName?: string\n onNext: () => void | Promise<void>\n onReset: () => void | Promise<void>\n showInfo?: boolean\n}\n\nexport type PaginationConfig = ClientPaginationConfig | ServerPaginationConfig\n\n/**\n * Unified pagination hook that works for both client-side and server-side pagination\n *\n * @example Client-side pagination\n * ```typescript\n * const pagination = useTablePagination({\n * type: 'client',\n * currentPage: 1,\n * totalPages: 10,\n * itemCount: 20,\n * itemName: 'scripts',\n * onNext: () => setPage(p => p + 1),\n * onPrevious: () => setPage(p => p - 1),\n * onReset: () => setPage(1)\n * })\n * ```\n *\n * @example Server-side pagination\n * ```typescript\n * const pagination = useTablePagination({\n * type: 'server',\n * hasNextPage: pageInfo.hasNextPage,\n * hasLoadedBeyondFirst: true,\n * startCursor: pageInfo.startCursor,\n * endCursor: pageInfo.endCursor,\n * itemCount: devices.length,\n * itemName: 'devices',\n * onNext: fetchNextPage,\n * onReset: fetchFirstPage\n * })\n * ```\n */\nexport function useTablePagination(\n config: PaginationConfig | null\n): CursorPaginationProps | undefined {\n return useMemo(() => {\n if (!config) return undefined\n\n if (config.type === 'client') {\n // Client-side pagination: show if more than 1 page\n if (config.totalPages <= 1) return undefined\n\n return {\n hasNextPage: config.currentPage < config.totalPages,\n hasPreviousPage: config.currentPage > 1,\n isFirstPage: config.currentPage === 1,\n startCursor: (config.currentPage - 1).toString(),\n endCursor: config.currentPage.toString(),\n currentCount: config.itemCount,\n itemName: config.itemName || 'items',\n onNext: () => config.onNext(),\n onPrevious: () => config.onPrevious(),\n showInfo: config.showInfo ?? true\n }\n } else {\n // Server-side pagination: show if pageInfo exists\n return {\n hasNextPage: config.hasNextPage,\n hasPreviousPage: config.hasLoadedBeyondFirst,\n isFirstPage: !config.hasLoadedBeyondFirst,\n startCursor: config.startCursor,\n endCursor: config.endCursor,\n currentCount: config.itemCount,\n itemName: config.itemName || 'items',\n onNext: config.onNext,\n onPrevious: config.onReset, // Previous button goes back to first page\n showInfo: config.showInfo ?? true\n }\n }\n }, [config])\n}\n","\"use client\"\n\nimport { useState, useEffect, useRef } from \"react\"\n\n/**\n * Hook to throttle a value\n * @param value - Value to throttle\n * @param limit - Throttle limit in milliseconds\n * @returns Throttled value\n */\nexport function useThrottle<T>(value: T, limit = 200): T {\n const [throttledValue, setThrottledValue] = useState<T>(value)\n const lastUpdated = useRef<number>(Date.now())\n\n useEffect(() => {\n const now = Date.now()\n const elapsed = now - lastUpdated.current\n\n // If enough time has elapsed, update the throttled value\n if (elapsed >= limit) {\n setThrottledValue(value)\n lastUpdated.current = now\n } else {\n // Otherwise, set up a timeout to update after the limit\n const timerId = setTimeout(() => {\n setThrottledValue(value)\n lastUpdated.current = Date.now()\n }, limit - elapsed)\n\n return () => {\n clearTimeout(timerId)\n }\n }\n }, [value, limit])\n\n return throttledValue\n}\n","import { useLayoutEffect, useState } from \"react\"\n\n/**\n * Hook to get window dimensions\n * @returns Window width and height\n */\nexport function useWindowSize() {\n const [windowSize, setWindowSize] = useState({\n width: 0,\n height: 0,\n })\n const [isClient, setIsClient] = useState(false)\n\n useLayoutEffect(() => {\n setIsClient(true)\n if (!isClient) return\n\n const handleResize = () => {\n setWindowSize({\n width: window.innerWidth,\n height: window.innerHeight,\n })\n }\n\n // Set initial size\n handleResize()\n\n // Add event listener\n window.addEventListener(\"resize\", handleResize)\n\n // Remove event listener on cleanup\n return () => window.removeEventListener(\"resize\", handleResize)\n }, [isClient])\n\n return windowSize\n}","'use client'\n\nimport { useRef, useState, useCallback } from 'react'\n\n/**\n * Custom horizontal scrollbar hook that drives a plain-DOM scrollbar.\n *\n * Returns a callback ref (`scrollRef`) to attach to the scrollable container,\n * plus refs, state, and event handlers for the track + thumb.\n *\n * Uses a **callback ref** so measurement and ResizeObserver setup happen\n * automatically when the scroll element mounts — even if it renders later\n * (e.g. after async data loads).\n */\nexport function useHorizontalScrollbar() {\n const scrollElRef = useRef<HTMLDivElement | null>(null)\n const trackRef = useRef<HTMLDivElement>(null)\n const thumbRef = useRef<HTMLDivElement>(null)\n const roRef = useRef<ResizeObserver | null>(null)\n const [thumbRatio, setThumbRatio] = useState(0)\n\n // Edge-fade state\n const [canScrollLeft, setCanScrollLeft] = useState(false)\n const [canScrollRight, setCanScrollRight] = useState(false)\n const prevCanScrollLeftRef = useRef(false)\n const prevCanScrollRightRef = useRef(false)\n\n const isDraggingRef = useRef(false)\n const dragStartRef = useRef({ mouseX: 0, scrollLeft: 0 })\n const rafIdRef = useRef<number>(0)\n\n /** Compute the thumb left% from current scrollLeft and apply to DOM directly */\n const syncThumbToDOM = useCallback(() => {\n const el = scrollElRef.current\n const thumb = thumbRef.current\n if (!el || !thumb) return\n\n const maxScroll = el.scrollWidth - el.clientWidth\n if (maxScroll <= 0) {\n thumb.style.left = '0%'\n return\n }\n\n const ratio = el.clientWidth / el.scrollWidth\n const fraction = Math.min(Math.max(el.scrollLeft, 0), maxScroll) / maxScroll\n thumb.style.left = `${fraction * (1 - ratio) * 100}%`\n }, [])\n\n const syncEdgeFades = useCallback(() => {\n const el = scrollElRef.current\n if (!el) return\n\n const maxScroll = el.scrollWidth - el.clientWidth\n const ratio = el.clientWidth / el.scrollWidth\n if (ratio >= 1 || maxScroll <= 0) {\n if (prevCanScrollLeftRef.current) {\n prevCanScrollLeftRef.current = false\n setCanScrollLeft(false)\n }\n if (prevCanScrollRightRef.current) {\n prevCanScrollRightRef.current = false\n setCanScrollRight(false)\n }\n return\n }\n\n const fraction = Math.min(Math.max(el.scrollLeft, 0), maxScroll) / maxScroll\n const left = fraction > 0.001\n const right = fraction < 0.999\n\n if (left !== prevCanScrollLeftRef.current) {\n prevCanScrollLeftRef.current = left\n setCanScrollLeft(left)\n }\n if (right !== prevCanScrollRightRef.current) {\n prevCanScrollRightRef.current = right\n setCanScrollRight(right)\n }\n }, [])\n\n const measure = useCallback(() => {\n const el = scrollElRef.current\n if (!el) return\n const ratio = el.clientWidth / el.scrollWidth\n setThumbRatio(ratio >= 1 ? 0 : ratio)\n syncThumbToDOM()\n syncEdgeFades()\n }, [syncThumbToDOM, syncEdgeFades])\n\n // Callback ref\n const scrollRef = useCallback((node: HTMLDivElement | null) => {\n // Teardown previous\n if (roRef.current) {\n roRef.current.disconnect()\n roRef.current = null\n }\n if (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 roRef.current = new ResizeObserver(measure)\n roRef.current.observe(node)\n\n requestAnimationFrame(() => {\n syncThumbToDOM()\n syncEdgeFades()\n })\n } else {\n setThumbRatio(0)\n prevCanScrollLeftRef.current = false\n prevCanScrollRightRef.current = false\n setCanScrollLeft(false)\n setCanScrollRight(false)\n }\n }, [measure, syncThumbToDOM, syncEdgeFades])\n\n // RAF-throttled scroll handler\n const onScroll = useCallback(() => {\n if (isDraggingRef.current) return\n\n if (rafIdRef.current) cancelAnimationFrame(rafIdRef.current)\n rafIdRef.current = requestAnimationFrame(() => {\n syncThumbToDOM()\n syncEdgeFades()\n })\n }, [syncThumbToDOM, syncEdgeFades])\n\n // Track click — read ratio from DOM, smooth scroll\n const onTrackClick = useCallback((e: React.MouseEvent) => {\n if ((e.target as HTMLElement).closest('[data-scrollbar-thumb]')) return\n\n const track = trackRef.current\n const el = scrollElRef.current\n if (!track || !el) return\n\n const ratio = el.clientWidth / el.scrollWidth\n if (ratio >= 1) return\n\n const rect = track.getBoundingClientRect()\n const thumbWidth = rect.width * ratio\n const maxThumbOffset = rect.width - thumbWidth\n if (maxThumbOffset <= 0) return\n\n const targetFraction = Math.max(0, Math.min(1,\n (e.clientX - rect.left - thumbWidth / 2) / maxThumbOffset\n ))\n const targetScrollLeft = targetFraction * (el.scrollWidth - el.clientWidth)\n el.scrollTo({ left: targetScrollLeft, behavior: 'smooth' })\n }, [])\n\n // Wheel on track → forward to scroll container\n const onTrackWheel = useCallback((e: React.WheelEvent) => {\n const el = scrollElRef.current\n if (!el) return\n e.preventDefault()\n el.scrollLeft += e.deltaX || e.deltaY\n }, [])\n\n // Drag: pointer down\n const onThumbPointerDown = useCallback((e: React.PointerEvent) => {\n e.preventDefault()\n e.stopPropagation()\n const el = scrollElRef.current\n if (!el) return\n isDraggingRef.current = true\n dragStartRef.current = { mouseX: e.clientX, scrollLeft: el.scrollLeft }\n const target = e.currentTarget as HTMLElement\n target.setPointerCapture(e.pointerId)\n target.style.cursor = 'grabbing'\n }, [])\n\n // Drag: pointer move\n const onThumbPointerMove = useCallback((e: React.PointerEvent) => {\n if (!isDraggingRef.current) return\n const el = scrollElRef.current\n const track = trackRef.current\n if (!el || !track) return\n\n const currentThumbRatio = el.clientWidth / el.scrollWidth\n if (currentThumbRatio >= 1) return\n\n const trackWidth = track.clientWidth\n const thumbWidth = trackWidth * currentThumbRatio\n const maxThumbTravel = trackWidth - thumbWidth\n if (maxThumbTravel <= 0) return\n\n const mouseDelta = e.clientX - dragStartRef.current.mouseX\n const scrollRange = el.scrollWidth - el.clientWidth\n el.scrollLeft = Math.min(\n Math.max(dragStartRef.current.scrollLeft + (mouseDelta / maxThumbTravel) * scrollRange, 0),\n scrollRange\n )\n\n syncThumbToDOM()\n syncEdgeFades()\n }, [syncThumbToDOM, syncEdgeFades])\n\n // Drag: pointer up\n const onThumbPointerUp = useCallback((e: React.PointerEvent) => {\n isDraggingRef.current = false\n const target = e.currentTarget as HTMLElement\n target.releasePointerCapture(e.pointerId)\n target.style.cursor = 'grab'\n }, [])\n\n return {\n scrollRef,\n trackRef,\n thumbRef,\n thumbRatio,\n canScrollLeft,\n canScrollRight,\n onScroll,\n onTrackClick,\n onTrackWheel,\n onThumbPointerDown,\n onThumbPointerMove,\n onThumbPointerUp,\n }\n}\n","\"use client\";\n\nimport { useState, useEffect } from 'react';\n\n/**\n * Extract the dominant edge color from an image using color bucketing.\n * Samples pixels along left and right edges (the visible letterbox areas),\n * groups them into color buckets, and returns the most common bucket.\n * This avoids muddy averages when edges have mixed colors (e.g., sky + sand).\n */\nfunction extractEdgeColor(img: HTMLImageElement): string {\n const canvas = document.createElement('canvas');\n const ctx = canvas.getContext('2d');\n if (!ctx) return '#000000';\n\n const maxSize = 100;\n const scale = Math.min(maxSize / img.naturalWidth, maxSize / img.naturalHeight);\n const w = Math.round(img.naturalWidth * scale);\n const h = Math.round(img.naturalHeight * scale);\n canvas.width = w;\n canvas.height = h;\n\n ctx.drawImage(img, 0, 0, w, h);\n\n const data = ctx.getImageData(0, 0, w, h).data;\n\n // Sample left edge, right edge, top edge, bottom edge (15% band)\n const edgeW = Math.max(2, Math.round(w * 0.15));\n const edgeH = Math.max(2, Math.round(h * 0.15));\n\n // Color bucketing: quantize to 32-step buckets for grouping similar colors\n const bucketSize = 32;\n const buckets = new Map<string, { r: number; g: number; b: number; count: number }>();\n\n for (let y = 0; y < h; y++) {\n for (let x = 0; x < w; x++) {\n const isEdge =\n x < edgeW || x >= w - edgeW ||\n y < edgeH || y >= h - edgeH;\n\n if (!isEdge) continue;\n\n const i = (y * w + x) * 4;\n const a = data[i + 3];\n if (a < 128) continue;\n\n const r = data[i];\n const g = data[i + 1];\n const b = data[i + 2];\n\n // Quantize to bucket\n const br = Math.floor(r / bucketSize) * bucketSize;\n const bg = Math.floor(g / bucketSize) * bucketSize;\n const bb = Math.floor(b / bucketSize) * bucketSize;\n const key = `${br},${bg},${bb}`;\n\n const existing = buckets.get(key);\n if (existing) {\n existing.r += r;\n existing.g += g;\n existing.b += b;\n existing.count++;\n } else {\n buckets.set(key, { r, g, b, count: 1 });\n }\n }\n }\n\n if (buckets.size === 0) return '#000000';\n\n // Find the most common color bucket\n let bestBucket: { r: number; g: number; b: number; count: number } | null = null;\n for (const bucket of buckets.values()) {\n if (!bestBucket || bucket.count > bestBucket.count) {\n bestBucket = bucket;\n }\n }\n\n if (!bestBucket || bestBucket.count === 0) return '#000000';\n\n // Return average color within the winning bucket\n const r = Math.round(bestBucket.r / bestBucket.count);\n const g = Math.round(bestBucket.g / bestBucket.count);\n const b = Math.round(bestBucket.b / bestBucket.count);\n\n return `rgb(${r}, ${g}, ${b})`;\n}\n\n/**\n * Hook that extracts the dominant edge color from an image URL.\n * Returns a CSS color string for use as a background behind object-contain images.\n *\n * Always sets crossOrigin='anonymous' — required for canvas pixel access.\n * Most CDNs (Supabase, Cloudflare, our image proxy) return CORS headers.\n * If the server doesn't support CORS, onerror fires and we use the fallback.\n *\n * @param imageUrl - URL of the image to analyze\n * @param fallback - Fallback color if extraction fails (default: '#000000')\n * @returns CSS color string (e.g., 'rgb(34, 28, 22)')\n */\nexport function useImageEdgeColor(imageUrl: string | undefined | null, fallback = '#000000'): string {\n const [color, setColor] = useState(fallback);\n\n useEffect(() => {\n if (!imageUrl) {\n setColor(fallback);\n return;\n }\n\n let cancelled = false;\n const img = new Image();\n img.crossOrigin = 'anonymous';\n\n img.onload = () => {\n if (cancelled) return;\n try {\n setColor(extractEdgeColor(img));\n } catch {\n setColor(fallback);\n }\n };\n\n img.onerror = () => {\n if (cancelled) return;\n setColor(fallback);\n };\n\n img.src = imageUrl;\n\n return () => { cancelled = true; };\n }, [imageUrl, fallback]);\n\n return color;\n}\n","\"use client\"\n\nimport { useState, useEffect, useCallback } from \"react\"\nimport { useDebounce } from \"./use-debounce\"\nimport type { SearchResult } from \"../../components/ui/search-input\"\n\nexport interface UseSearchConfig<T> {\n /** Async function that performs the search */\n searchFn: (query: string) => Promise<T[]>\n /** Maps each raw item to a SearchResult */\n mapResult: (item: T) => SearchResult\n /** Debounce delay in ms. Default 300 */\n debounceMs?: number\n /** Minimum characters before searching. Default 2 */\n minQueryLength?: number\n}\n\nexport interface UseSearchReturn {\n query: string\n setQuery: (q: string) => void\n results: SearchResult[]\n isLoading: boolean\n error: string | null\n clearResults: () => void\n}\n\n/**\n * Generic search state management hook.\n *\n * Debounces the query, calls `searchFn` when the debounced value meets\n * `minQueryLength`, and maps the raw results via `mapResult`.\n */\nexport function useSearch<T>(config: UseSearchConfig<T>): UseSearchReturn {\n const { searchFn, mapResult, debounceMs = 300, minQueryLength = 2 } = config\n\n const [query, setQuery] = useState(\"\")\n const [results, setResults] = useState<SearchResult[]>([])\n const [isLoading, setIsLoading] = useState(false)\n const [error, setError] = useState<string | null>(null)\n\n const debouncedQuery = useDebounce(query, debounceMs)\n\n const clearResults = useCallback(() => {\n setResults([])\n setError(null)\n }, [])\n\n useEffect(() => {\n // Clear when query is empty or below threshold\n if (!debouncedQuery || debouncedQuery.length < minQueryLength) {\n setResults([])\n setIsLoading(false)\n setError(null)\n return\n }\n\n let cancelled = false\n\n const run = async () => {\n setIsLoading(true)\n setError(null)\n\n try {\n const rawResults = await searchFn(debouncedQuery)\n\n if (!cancelled) {\n setResults(rawResults.map(mapResult))\n }\n } catch (err) {\n if (!cancelled) {\n setError(err instanceof Error ? err.message : \"Search failed\")\n setResults([])\n }\n } finally {\n if (!cancelled) {\n setIsLoading(false)\n }\n }\n }\n\n run()\n\n return () => {\n cancelled = true\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [debouncedQuery, minQueryLength])\n\n return { query, setQuery, results, isLoading, error, clearResults }\n}\n","\"use client\"\n\nimport { useCallback, useEffect, useRef, useState } from \"react\"\n\nexport interface UseAutoLimitTagsOptions {\n /** Total number of tags */\n count: number\n /** Fixed limit or \"auto\" for DOM-based measurement. Default \"auto\" */\n limitTags?: number | \"auto\"\n /** Placeholder text used to reserve input width (only used in \"auto\" mode) */\n placeholder?: string\n}\n\nexport interface UseAutoLimitTagsReturn {\n /** How many tags to show */\n visibleCount: number\n /** Ref for the zone that contains tags + input (must have overflow-hidden, gap, padding) */\n middleRef: React.RefObject<HTMLDivElement | null>\n /** Ref for the off-screen container that holds measurement copies of ALL tags */\n measureRef: React.RefObject<HTMLDivElement | null>\n /** Ref for the hidden span that measures placeholder text width */\n textMeasureRef: React.RefObject<HTMLSpanElement | null>\n /** Ref for the \"+N\" badge element (used to measure its width) */\n badgeRef: React.RefObject<HTMLButtonElement | null>\n /** Ref for the input element (used to read its min-width) */\n inputRef: React.RefObject<HTMLInputElement | null>\n}\n\n/**\n * Calculates how many tags fit in a single-line container.\n *\n * Requires three off-screen measurement elements rendered by the consumer:\n * 1. A `<div ref={measureRef}>` containing Tag copies for every item (to measure widths)\n * 2. A `<span ref={textMeasureRef}>` containing the placeholder text (to reserve input width)\n * 3. The `<button ref={badgeRef}>` for the \"+N\" badge (to measure badge width)\n *\n * The hook reads real CSS values (padding, gap) from the middleRef container,\n * so it works regardless of responsive breakpoints or custom styling.\n */\nexport function useAutoLimitTags({\n count,\n limitTags = \"auto\",\n placeholder = \"\",\n}: UseAutoLimitTagsOptions): UseAutoLimitTagsReturn {\n const middleRef = useRef<HTMLDivElement>(null)\n const measureRef = useRef<HTMLDivElement>(null)\n const textMeasureRef = useRef<HTMLSpanElement>(null)\n const badgeRef = useRef<HTMLButtonElement>(null)\n const inputRef = useRef<HTMLInputElement>(null)\n const [visibleCount, setVisibleCount] = useState(count)\n\n const recalculate = useCallback(() => {\n // Fixed limit — skip DOM measurement\n if (limitTags !== \"auto\") {\n setVisibleCount(Math.min(limitTags, count))\n return\n }\n\n const middle = middleRef.current\n const measure = measureRef.current\n if (!middle || !measure) {\n setVisibleCount(count)\n return\n }\n if (count === 0) {\n setVisibleCount(0)\n return\n }\n\n // Read real CSS metrics from the middle zone\n const cs = getComputedStyle(middle)\n const padL = parseFloat(cs.paddingLeft) || 0\n const padR = parseFloat(cs.paddingRight) || 0\n const gap = parseFloat(cs.gap) || 0\n const middleW = middle.clientWidth\n\n // Reserve space for the input based on placeholder width\n const textW = textMeasureRef.current?.offsetWidth ?? 60\n const inputMinW = inputRef.current\n ? parseFloat(getComputedStyle(inputRef.current).minWidth) || 60\n : 60\n const inputReservedW = Math.max(textW + 8, inputMinW)\n\n // Available = middle zone − padding − input reserved − gap before input\n const available = middleW - padL - padR - inputReservedW - gap\n\n // Measure every tag from the off-screen container\n const tagEls = Array.from(measure.children) as HTMLElement[]\n const widths = tagEls.map((el) => el.offsetWidth)\n\n // Fast check: do ALL tags fit?\n let total = 0\n for (let i = 0; i < widths.length; i++) {\n total += widths[i] + (i > 0 ? gap : 0)\n }\n if (total <= available) {\n setVisibleCount(count)\n return\n }\n\n // Not all fit → reserve space for the \"+N\" badge\n const badgeW = badgeRef.current?.offsetWidth ?? 40\n const spaceWithBadge = available - badgeW - gap\n\n let used = 0\n let fitCount = 0\n for (let i = 0; i < widths.length; i++) {\n const need = widths[i] + (i > 0 ? gap : 0)\n if (used + need > spaceWithBadge) break\n used += need\n fitCount++\n }\n\n setVisibleCount(Math.max(0, fitCount))\n }, [count, limitTags, placeholder])\n\n // Recalculate when inputs change\n useEffect(() => {\n recalculate()\n }, [recalculate])\n\n // Recalculate on container resize\n useEffect(() => {\n const el = middleRef.current\n if (!el) return\n const ro = new ResizeObserver(recalculate)\n ro.observe(el)\n return () => ro.disconnect()\n }, [recalculate])\n\n return { visibleCount, middleRef, measureRef, textMeasureRef, badgeRef, inputRef }\n}\n","import { useState, useEffect } from 'react';\nimport type { PlatformConfig, PlatformOption } from '../../types/platform';\nimport { transformPlatformConfigsToOptions } from '../../utils/platform-config';\nimport type { SelectableOption } from '../../components/features';\n\nexport interface UsePlatformConfigResult {\n platforms: PlatformConfig[];\n platformOptions: PlatformOption[];\n selectableOptions: SelectableOption[]; // Rich options with icons and colors\n isLoading: boolean;\n error: Error | null;\n}\n\n// Cache for platform configs to avoid repeated fetches\nlet platformCache: PlatformConfig[] | null = null;\nlet fetchPromise: Promise<PlatformConfig[]> | null = null;\n\n/**\n * Custom hook to fetch platform configurations from API\n * Provides both full platform configs and simplified options for dropdowns\n * Heavily cached to prevent excessive API calls - should only call once per session\n * \n * NOTE: This hook is designed to work without react-query dependency\n */\nexport function usePlatformConfig(): UsePlatformConfigResult {\n const [platforms, setPlatforms] = useState<PlatformConfig[]>(platformCache || []);\n const [isLoading, setIsLoading] = useState(!platformCache);\n const [error, setError] = useState<Error | null>(null);\n\n useEffect(() => {\n // If we already have cached platforms, use them\n if (platformCache) {\n setPlatforms(platformCache);\n setIsLoading(false);\n return;\n }\n\n // If a fetch is already in progress, wait for it\n if (fetchPromise) {\n fetchPromise\n .then(data => {\n setPlatforms(data);\n setIsLoading(false);\n })\n .catch(err => {\n setError(err);\n setIsLoading(false);\n });\n return;\n }\n\n // Start a new fetch\n console.log('🔧 Fetching platform configurations from API (should only happen once)');\n \n fetchPromise = fetch('/api/config/platforms')\n .then(response => {\n if (!response.ok) {\n throw new Error(`Failed to fetch platform config: ${response.statusText}`);\n }\n return response.json();\n })\n .then(data => {\n const platforms = data.platforms || data;\n console.log('✅ Platform configurations loaded:', platforms.length, 'platforms');\n platformCache = platforms;\n fetchPromise = null;\n return platforms;\n });\n\n fetchPromise\n .then(data => {\n setPlatforms(data);\n setIsLoading(false);\n })\n .catch(err => {\n console.error('❌ Failed to fetch platform config:', err);\n setError(err);\n setIsLoading(false);\n fetchPromise = null;\n });\n }, []);\n \n // Create options for dropdowns with \"All Platforms\" option\n const platformOptions: PlatformOption[] = [\n { value: 'all', label: 'All Platforms' },\n ...platforms.map((platform: PlatformConfig) => ({\n value: platform.value,\n label: platform.label\n }))\n ];\n\n // Create rich selectable options with icons and colors\n const selectableOptions = transformPlatformConfigsToOptions(platforms);\n\n return {\n platforms,\n platformOptions,\n selectableOptions,\n isLoading,\n error\n };\n}\n\n/**\n * Get platform configuration by value\n */\nexport function usePlatformByValue(value: string): PlatformConfig | undefined {\n const { platforms } = usePlatformConfig();\n return platforms.find(platform => platform.value === value);\n}\n\n/**\n * Check if a platform value is valid\n */\nexport function useValidatePlatform(value: string): boolean {\n const { platforms } = usePlatformConfig();\n return platforms.some(platform => platform.value === value);\n} ","import React from 'react';\nimport { OpenmspLogo, FlamingoLogo, OpenFrameLogo, MiamiCyberGangLogoFaceOnly } from '../components/icons';\nimport { Globe } from 'lucide-react';\nimport type { SelectableOption } from '../components/features';\nimport type { PlatformConfig } from '../types/platform';\n\n// Platform icons mapping with consistent colors matching app theme\nexport const platformIcons = {\n openframe: <OpenFrameLogo className=\"h-5 w-5\" lowerPathColor=\"#FFC008\" upperPathColor=\"#ffffff\" />,\n openmsp: <OpenmspLogo className=\"h-5 w-5\" />,\n flamingo: <FlamingoLogo className=\"h-5 w-5\" fill=\"#EC4899\" />,\n 'flamingo-teaser': <FlamingoLogo className=\"h-5 w-5\" fill=\"#EC4899\" />,\n 'marketing-hub': <FlamingoLogo className=\"h-5 w-5\" fill=\"#F357BB\" />,\n 'product-hub': <FlamingoLogo className=\"h-5 w-5\" fill=\"#5EA62E\" />,\n 'revenue-hub': <FlamingoLogo className=\"h-5 w-5\" fill=\"#FFC008\" />,\n 'people-hub': <FlamingoLogo className=\"h-5 w-5\" fill=\"#5EFAF0\" />,\n 'company-hub': <FlamingoLogo className=\"h-5 w-5\" fill=\"#f36666\" />,\n tmcg: <MiamiCyberGangLogoFaceOnly className=\"h-5 w-5\" />,\n universal: <Globe className=\"h-5 w-5 text-[#10B981]\" />\n};\n\n// Platform colors mapping\nexport const platformColors = {\n openmsp: 'bg-[#3B82F6]',\n openframe: 'bg-[#8B5CF6]',\n flamingo: 'bg-[#EC4899]',\n 'flamingo-teaser': 'bg-[#F59E0B]',\n 'marketing-hub': 'bg-[#F357BB]',\n 'product-hub': 'bg-[#5EA62E]',\n 'revenue-hub': 'bg-[#FFC008]',\n 'people-hub': 'bg-[#5EFAF0]',\n 'company-hub': 'bg-[#f36666]',\n tmcg: 'bg-[#FF6B6B]',\n universal: 'bg-[#10B981]'\n};\n\n// Platform display names for consistent naming across the app\nexport const platformDisplayNames = {\n openmsp: 'OpenMSP',\n openframe: 'OpenFrame',\n flamingo: 'Flamingo',\n 'flamingo-teaser': 'Flamingo Teaser',\n 'marketing-hub': 'Flamingo Marketing Hub',\n 'product-hub': 'Flamingo Product Hub',\n 'revenue-hub': 'Flamingo Revenue Hub',\n 'people-hub': 'Flamingo People Hub',\n 'company-hub': 'Flamingo Company Hub',\n tmcg: 'TMCG',\n universal: 'Universal'\n};\n\n// Platform descriptions for consistent messaging across the app\nexport const platformDescriptions = {\n openmsp: 'Comprehensive directory and comparison platform for managed service providers (MSPs) and technology vendors. Reduce vendor costs and discover open-source alternatives.',\n openframe: 'AI-driven open-source security operations center (SOC) and endpoint detection platform for MSPs.',\n flamingo: 'AI-driven open-source OS for MSPs. Swap bloated vendor tools for open ones. Automate the boring crap. Take your margin back.',\n 'flamingo-teaser': 'Preview of Flamingo - the AI-driven open-source OS for MSPs.',\n tmcg: 'The Miami Cyber Gang - A cybersecurity community focused on education and collaboration.',\n universal: 'Cross-platform universal content.'\n};\n\n// Platform slogans for branding consistency\nexport const platformSlogans = {\n openmsp: 'Find Your Perfect MSP Partner',\n openframe: 'Open-Source Security Operations',\n flamingo: 'Open-Source OS for MSPs',\n 'flamingo-teaser': 'Coming Soon: Open-Source OS for MSPs',\n tmcg: 'Miami Cyber Community',\n universal: 'Universal Platform'\n};\n\n// Platform hex colors for default configuration\nexport const platformHexColors = {\n openmsp: '#FFC008',\n openframe: '#FFC008',\n flamingo: '#FF6B9D',\n universal: '#FFC008',\n 'flamingo-teaser': '#F59E0B',\n 'marketing-hub': '#F357BB',\n 'product-hub': '#5EA62E',\n 'revenue-hub': '#FFC008',\n 'people-hub': '#5EFAF0',\n 'company-hub': '#f36666',\n tmcg: '#FF6B6B'\n};\n\n// Platform icon names for default configuration\nexport const platformIconNames = {\n openmsp: 'openmsp-logo',\n openframe: 'openframe-logo',\n flamingo: 'flamingo-logo',\n universal: 'globe',\n 'flamingo-teaser': 'flamingo-logo',\n 'marketing-hub': 'flamingo-logo',\n 'product-hub': 'flamingo-logo',\n 'revenue-hub': 'flamingo-logo',\n 'people-hub': 'flamingo-logo',\n 'company-hub': 'flamingo-logo',\n tmcg: 'tmcg-logo'\n};\n\n/**\n * Get default color for platform\n */\nexport function getDefaultColorForPlatform(platformName: string): string {\n return platformHexColors[platformName as keyof typeof platformHexColors] || platformHexColors.universal;\n}\n\n/**\n * Get default icon name for platform\n */\nexport function getDefaultIconForPlatform(platformName: string): string {\n return platformIconNames[platformName as keyof typeof platformIconNames] || platformIconNames.universal;\n}\n\nexport function transformPlatformConfigsToOptions(platformConfigs: PlatformConfig[]): SelectableOption[] {\n return platformConfigs.map((platform: PlatformConfig) => ({\n id: platform.id, // Database UUID for matching\n name: platform.name, // Platform name enum\n displayName: platform.display_name, // Human-readable name\n description: platform.description,\n icon: platformIcons[platform.name as keyof typeof platformIcons] || platformIcons.universal,\n color: platformColors[platform.name as keyof typeof platformColors] || platformColors.universal\n }));\n}\n\n/**\n * Get platform icon by name\n */\nexport function getPlatformIcon(platformName: string) {\n return platformIcons[platformName as keyof typeof platformIcons] || platformIcons.universal;\n}\n\n/**\n * Get platform color by name\n */\nexport function getPlatformColor(platformName: string) {\n return platformColors[platformName as keyof typeof platformColors] || platformColors.universal;\n}\n\n/**\n * Get platform display name by name\n */\nexport function getPlatformDisplayName(platformName: string): string {\n return platformDisplayNames[platformName as keyof typeof platformDisplayNames] || platformName;\n}\n\n/**\n * Get platform description by name\n */\nexport function getPlatformDescription(platformName: string): string {\n return platformDescriptions[platformName as keyof typeof platformDescriptions] || platformName;\n}\n\n/**\n * Get platform slogan by name\n */\nexport function getPlatformSlogan(platformName: string): string {\n return platformSlogans[platformName as keyof typeof platformSlogans] || platformName;\n}\n\n/**\n * Get small platform icon for filter buttons with white colors (4x4 size)\n */\nexport function getSmallPlatformIcon(platformName: string): React.ReactNode {\n const className = \"h-4 w-4 flex-shrink-0\";\n\n switch (platformName) {\n case 'openframe':\n return <OpenFrameLogo className={className} lowerPathColor=\"#FFC008\" upperPathColor=\"#ffffff\" />;\n case 'openmsp':\n return <OpenmspLogo className={className} frontBubbleColor=\"#f1f1f1\" innerFrontBubbleColor=\"#000000\" backBubbleColor=\"#FFC008\" />;\n case 'flamingo':\n case 'flamingo-teaser':\n return <FlamingoLogo className={`${className}`} fill=\"#EC4899\" />;\n case 'marketing-hub':\n return <FlamingoLogo className={className} fill=\"var(--ods-flamingo-pink-base)\" />;\n case 'product-hub':\n return <FlamingoLogo className={className} fill=\"var(--ods-attention-green-success)\" />;\n case 'revenue-hub':\n return <FlamingoLogo className={className} fill=\"var(--ods-attention-yellow-warning)\" />;\n case 'people-hub':\n return <FlamingoLogo className={className} fill=\"var(--ods-flamingo-cyan-base)\" />;\n case 'company-hub':\n return <FlamingoLogo className={className} fill=\"var(--ods-attention-red-error)\" />;\n case 'tmcg':\n return <MiamiCyberGangLogoFaceOnly className={className} />;\n case 'universal':\n default:\n return <Globe className={className} />;\n }\n}\n\n/**\n * Get platform icon for admin/selector components (standard 6x6 size)\n */\nexport function getPlatformIconComponent(platformName: string, className: string = \"h-6 w-6\"): React.ReactNode {\n switch (platformName) {\n case 'openframe':\n return <OpenFrameLogo className={className} />;\n case 'openmsp':\n return <OpenmspLogo className={className} color=\"#f1f1f1\" />;\n case 'flamingo':\n case 'flamingo-teaser':\n return <FlamingoLogo className={`${className} text-white`} />;\n case 'marketing-hub':\n return <FlamingoLogo className={className} fill=\"var(--ods-flamingo-pink-base)\" />;\n case 'product-hub':\n return <FlamingoLogo className={className} fill=\"var(--ods-attention-green-success)\" />;\n case 'revenue-hub':\n return <FlamingoLogo className={className} fill=\"var(--ods-attention-yellow-warning)\" />;\n case 'people-hub':\n return <FlamingoLogo className={className} fill=\"var(--ods-flamingo-cyan-base)\" />;\n case 'company-hub':\n return <FlamingoLogo className={className} fill=\"var(--ods-attention-red-error)\" />;\n case 'tmcg':\n return <MiamiCyberGangLogoFaceOnly size={24} className={className} />;\n case 'universal':\n default:\n return <Globe className={className} />;\n }\n}","import { toast as sonnerToast } from 'sonner'\nimport { showToast, type ToastVariant } from '../components/ui/toaster'\n\nexport interface ToastOptions {\n title?: string\n description?: string\n variant?: 'default' | 'success' | 'destructive' | 'info' | 'warning' | 'error'\n duration?: number\n dismissible?: boolean\n}\n\nconst normalizeVariant = (variant: ToastOptions['variant']): ToastVariant => {\n if (variant === 'destructive') return 'error'\n if (!variant) return 'default'\n return variant\n}\n\nexport const toast = (options: ToastOptions | string) => {\n if (typeof options === 'string') {\n return showToast({ title: options })\n }\n\n const { title, description, variant, duration, dismissible } = options\n return showToast({\n title,\n description,\n variant: normalizeVariant(variant),\n duration,\n dismissible,\n })\n}\n\nexport const useToast = () => ({\n toast,\n dismiss: sonnerToast.dismiss,\n promise: sonnerToast.promise,\n})\n","'use client'\n\nimport * as React from 'react'\nimport { Toaster as SonnerToaster, toast as sonnerToast } from 'sonner'\nimport type { ExternalToast } from 'sonner'\nimport { Chevron02DownIcon } from '../icons-v2-generated/arrows/chevron-02-down-icon'\nimport { XmarkIcon } from '../icons-v2-generated/signs-and-symbols/xmark-icon'\nimport { ToolIcon } from '../tool-icon'\nimport type { ToolType } from '../../types/tool.types'\nimport { cn } from '../../utils/cn'\n\nexport type ToastVariant = 'default' | 'success' | 'warning' | 'error' | 'info'\n\nexport const dotColorByVariant: Record<ToastVariant, string> = {\n default: 'bg-ods-text-secondary',\n success: 'bg-ods-success',\n warning: 'bg-ods-warning',\n error: 'bg-ods-error',\n info: 'bg-ods-text-secondary',\n}\n\nexport const progressColorByVariant: Record<ToastVariant, string> = {\n default: 'bg-ods-text-secondary',\n success: 'bg-ods-success',\n warning: 'bg-ods-warning',\n error: 'bg-ods-error',\n info: 'bg-ods-text-secondary',\n}\n\ninterface ToastHeaderProps {\n id: string | number\n variant: ToastVariant\n title?: React.ReactNode\n description?: React.ReactNode\n duration?: number\n dismissible?: boolean\n className?: string\n showProgress?: boolean\n}\n\nfunction ToastHeader({\n id,\n variant,\n title,\n description,\n duration = 4000,\n dismissible = true,\n className,\n showProgress = true,\n}: ToastHeaderProps) {\n return (\n <div\n className={cn(\n 'relative flex w-full items-start gap-2 overflow-hidden bg-ods-card p-3',\n className,\n )}\n >\n <div className=\"flex size-6 shrink-0 items-center justify-center\">\n <span className={cn('size-[9px] rounded-full', dotColorByVariant[variant])} />\n </div>\n\n <div className=\"flex min-w-0 flex-1 flex-col justify-center font-['DM_Sans'] font-medium\">\n {title ? (\n <p className=\"truncate pr-5 text-[18px] leading-6 text-ods-text-primary\" title={typeof title === 'string' ? title : undefined}>{title}</p>\n ) : null}\n {description ? (\n <p className=\"text-[14px] leading-5 text-ods-text-secondary line-clamp-3\" title={typeof description === 'string' ? description : undefined}>{description}</p>\n ) : null}\n </div>\n\n {dismissible ? (\n <button\n type=\"button\"\n aria-label=\"Close\"\n onClick={() => sonnerToast.dismiss(id)}\n className=\"absolute right-[7px] top-[7px] flex size-4 items-center justify-center text-ods-text-secondary transition-colors hover:text-ods-text-primary\"\n >\n <XmarkIcon size={16} />\n </button>\n ) : null}\n\n {showProgress && duration !== Infinity && duration > 0 ? (\n <div\n className={cn(\n 'absolute inset-x-0 bottom-0 h-1 origin-left',\n progressColorByVariant[variant],\n )}\n style={{\n animation: `toast-progress ${duration}ms linear forwards`,\n }}\n />\n ) : null}\n </div>\n )\n}\n\nexport interface ToastCardProps {\n id: string | number\n variant?: ToastVariant\n title?: React.ReactNode\n description?: React.ReactNode\n duration?: number\n dismissible?: boolean\n className?: string\n}\n\nexport function ToastCard({\n id,\n variant = 'default',\n title,\n description,\n duration = 4000,\n dismissible = true,\n className,\n}: ToastCardProps) {\n return (\n <div\n role=\"status\"\n className={cn(\n 'w-[368px] max-w-[calc(100vw-32px)] overflow-hidden rounded-md border border-ods-border bg-ods-card shadow-lg',\n className,\n )}\n >\n <ToastHeader\n id={id}\n variant={variant}\n title={title}\n description={description}\n duration={duration}\n dismissible={dismissible}\n />\n </div>\n )\n}\n\nexport interface CommandApprovalToastProps {\n id: string | number\n variant?: ToastVariant\n title?: React.ReactNode\n description?: React.ReactNode\n command: string\n toolType?: ToolType\n approvalDescription?: React.ReactNode\n approveLabel?: string\n rejectLabel?: string\n onApprove?: () => void\n onReject?: () => void\n duration?: number\n dismissible?: boolean\n defaultExpanded?: boolean\n className?: string\n}\n\nexport function CommandApprovalToast({\n id,\n variant = 'warning',\n title = 'Tech Required',\n description = 'Approval is required to execute the command.',\n command,\n toolType,\n approvalDescription,\n approveLabel = 'Approve',\n rejectLabel = 'Reject',\n onApprove,\n onReject,\n duration = Infinity,\n dismissible = true,\n defaultExpanded = false,\n className,\n}: CommandApprovalToastProps) {\n const [expanded, setExpanded] = React.useState(defaultExpanded)\n\n const handleApprove = () => {\n onApprove?.()\n sonnerToast.dismiss(id)\n }\n\n const handleReject = () => {\n onReject?.()\n sonnerToast.dismiss(id)\n }\n\n return (\n <div\n role=\"status\"\n className={cn(\n 'flex w-[368px] max-w-[calc(100vw-32px)] flex-col overflow-hidden rounded-md border border-ods-border bg-ods-bg shadow-lg',\n className,\n )}\n >\n <ToastHeader\n id={id}\n variant={variant}\n title={title}\n description={description}\n duration={duration}\n dismissible={dismissible}\n showProgress={!expanded}\n className=\"border-b border-ods-border\"\n />\n\n <div\n className=\"grid transition-[grid-template-rows] duration-300 ease-out\"\n style={{ gridTemplateRows: expanded ? '1fr' : '0fr' }}\n aria-hidden={!expanded}\n >\n <div className=\"overflow-hidden\">\n <div className=\"flex h-11 w-full items-center gap-2 border-b border-ods-border bg-ods-card px-3 py-2\">\n <p className=\"min-w-0 flex-1 truncate font-['DM_Sans'] text-[14px] font-medium leading-5 text-ods-text-primary\" title={command}>\n {command}\n </p>\n {toolType ? <ToolIcon toolType={toolType} size={16} /> : null}\n </div>\n\n <div className=\"flex flex-col gap-2 bg-ods-bg p-3\">\n {approvalDescription ? (\n <p className=\"font-['DM_Sans'] text-[14px] font-medium leading-5 text-ods-text-secondary\">\n {approvalDescription}\n </p>\n ) : null}\n <div className=\"flex items-center gap-4\">\n <button\n type=\"button\"\n onClick={handleApprove}\n tabIndex={expanded ? 0 : -1}\n className=\"flex flex-1 items-center justify-center rounded-md bg-ods-accent px-2 py-2 font-['Azeret_Mono'] text-[14px] font-medium uppercase tracking-[-0.28px] text-ods-text-on-accent transition-colors hover:bg-ods-accent-hover active:bg-ods-accent-active\"\n >\n {approveLabel}\n </button>\n <button\n type=\"button\"\n onClick={handleReject}\n tabIndex={expanded ? 0 : -1}\n className=\"flex flex-1 items-center justify-center rounded-md border border-ods-border bg-ods-card px-2 py-2 font-['Azeret_Mono'] text-[14px] font-medium uppercase tracking-[-0.28px] text-ods-text-primary transition-colors hover:bg-ods-bg-hover\"\n >\n {rejectLabel}\n </button>\n </div>\n </div>\n </div>\n </div>\n\n {expanded ? null : (\n <button\n type=\"button\"\n onClick={() => setExpanded(true)}\n className=\"flex w-full items-center gap-2 bg-ods-card px-3 py-2 text-left font-['DM_Sans'] text-[14px] font-medium leading-5 text-ods-text-primary transition-colors hover:bg-ods-bg-hover\"\n aria-expanded={false}\n >\n <span className=\"flex-1\">Show Command</span>\n <Chevron02DownIcon size={16} />\n </button>\n )}\n </div>\n )\n}\n\nexport type ToasterProps = React.ComponentProps<typeof SonnerToaster>\n\nexport function Toaster({\n position = 'bottom-right',\n offset = 24,\n gap = 8,\n toastOptions,\n ...rest\n}: ToasterProps = {}) {\n const { classNames: userClassNames, ...restToastOptions } = toastOptions ?? {}\n\n return (\n <>\n <style>{`\n @keyframes toast-progress {\n from { transform: scaleX(1); }\n to { transform: scaleX(0); }\n }\n `}</style>\n <SonnerToaster\n position={position}\n offset={offset}\n gap={gap}\n toastOptions={{\n unstyled: true,\n ...restToastOptions,\n classNames: {\n toast: 'w-full',\n ...userClassNames,\n },\n }}\n {...rest}\n />\n </>\n )\n}\n\nexport interface ShowToastOptions extends Omit<ExternalToast, 'description'> {\n title?: React.ReactNode\n description?: React.ReactNode\n variant?: ToastVariant\n}\n\nexport function showToast(options: ShowToastOptions | string) {\n const opts: ShowToastOptions =\n typeof options === 'string' ? { title: options } : options\n\n const {\n title,\n description,\n variant = 'default',\n duration = 4000,\n dismissible = true,\n ...rest\n } = opts\n\n return sonnerToast.custom(\n (id) => (\n <ToastCard\n id={id}\n variant={variant}\n title={title}\n description={description}\n duration={duration}\n dismissible={dismissible}\n />\n ),\n { duration, dismissible, ...rest },\n )\n}\n\nexport interface ShowCommandApprovalToastOptions\n extends Omit<ExternalToast, 'description'>,\n Omit<CommandApprovalToastProps, 'id'> {}\n\nexport function showCommandApprovalToast(options: ShowCommandApprovalToastOptions) {\n const {\n variant,\n title,\n description,\n command,\n toolType,\n approvalDescription,\n approveLabel,\n rejectLabel,\n onApprove,\n onReject,\n defaultExpanded,\n duration = Infinity,\n dismissible = true,\n ...rest\n } = options\n\n return sonnerToast.custom(\n (id) => (\n <CommandApprovalToast\n id={id}\n variant={variant}\n title={title}\n description={description}\n command={command}\n toolType={toolType}\n approvalDescription={approvalDescription}\n approveLabel={approveLabel}\n rejectLabel={rejectLabel}\n onApprove={onApprove}\n onReject={onReject}\n defaultExpanded={defaultExpanded}\n duration={duration}\n dismissible={dismissible}\n />\n ),\n { duration, dismissible, ...rest },\n )\n}\n","/**\n * Centralized Tool Types\n *\n * Single source of truth for all tool-related types across the entire platform.\n * Used by ToolBadge, ToolIcon, and any component that needs tool type information.\n */\n\nexport const ToolTypeValues = {\n TACTICAL_RMM: 'TACTICAL_RMM',\n FLEET_MDM: 'FLEET_MDM',\n MESHCENTRAL: 'MESHCENTRAL',\n AUTHENTIK: 'AUTHENTIK',\n OPENFRAME: 'OPENFRAME',\n OPENFRAME_CHAT: 'OPENFRAME_CHAT',\n OPENFRAME_CLIENT: 'OPENFRAME_CLIENT',\n OSQUERY: 'OSQUERY',\n SYSTEM: 'SYSTEM'\n} as const\n\nexport type ToolType = (typeof ToolTypeValues)[keyof typeof ToolTypeValues]\n\n/**\n * Maps tool types to display labels\n */\nexport const toolLabels: Record<ToolType, string> = {\n TACTICAL_RMM: 'Tactical',\n FLEET_MDM: 'Fleet',\n MESHCENTRAL: 'MeshCentral',\n AUTHENTIK: 'Authentik',\n OPENFRAME: 'OpenFrame',\n OPENFRAME_CHAT: 'OpenFrame Chat',\n OPENFRAME_CLIENT: 'OpenFrame Client',\n OSQUERY: 'Osquery',\n SYSTEM: 'System'\n}\n","import * as React from \"react\";\nimport { ToolType, ToolTypeValues } from \"../types/tool.types\";\nimport { OpenFrameLogo } from \"./icons\";\nimport {\n\tOsqueryLogoGreyIcon,\n\tTacticalRmmLogoIcon,\n\tMeshcentralLogoGreyIcon,\n\tFleetMdmLogoGreyIcon,\n\tAuthentikLogoGreyIcon,\n} from \"./icons-v2-generated\";\n\nconst renderOpenFrameLogo = (_size: number, className?: string) => (\n\t// eslint-disable-next-line deprecation/deprecation\n\t<OpenFrameLogo\n\t\tclassName={className ?? \"h-4 w-auto\"}\n\t\tlowerPathColor=\"var(--color-accent-primary)\"\n\t\tupperPathColor=\"var(--color-text-primary)\"\n\t/>\n);\n\nconst toolIconMap: Record<ToolType, (size: number, className?: string) => React.ReactNode> = {\n\t[ToolTypeValues.FLEET_MDM]: (size, className) => <FleetMdmLogoGreyIcon size={size} className={className} />,\n\t[ToolTypeValues.MESHCENTRAL]: (size, className) => <MeshcentralLogoGreyIcon size={size} className={className} />,\n\t[ToolTypeValues.TACTICAL_RMM]: (size, className) => <TacticalRmmLogoIcon size={size} className={className} />,\n\t[ToolTypeValues.OPENFRAME]: renderOpenFrameLogo,\n\t[ToolTypeValues.OPENFRAME_CHAT]: renderOpenFrameLogo,\n\t[ToolTypeValues.OPENFRAME_CLIENT]: renderOpenFrameLogo,\n\t[ToolTypeValues.AUTHENTIK]: (size, className) => <AuthentikLogoGreyIcon size={size} className={className} />,\n\t[ToolTypeValues.OSQUERY]: (size, className) => <OsqueryLogoGreyIcon size={size} className={className} />,\n\t[ToolTypeValues.SYSTEM]: () => null,\n};\n\nexport interface ToolIconProps {\n\ttoolType: ToolType;\n\tsize?: number;\n\tclassName?: string;\n}\n\nexport const ToolIcon: React.FC<ToolIconProps> = ({ toolType, size = 16, className }) =>\n\t<>{toolIconMap[toolType]?.(size, className) ?? null}</>;\n\nToolIcon.displayName = \"ToolIcon\";\n","import { useState, useCallback, useEffect } from 'react';\nimport { useToast } from \"./use-toast\";\nimport { useRouter } from '../embed-shims/next-navigation';\nimport { useRequiredEndpointsRuntime } from '../contexts/endpoints-runtime-context';\n\ninterface ContactSubmissionOptions {\n userId?: string;\n successRedirectUrl?: string;\n successToastMessage?: string;\n onSuccess?: () => void;\n}\n\n/**\n * Mirrors the API contract at POST /api/contact (see `ContactBaseSchema` in\n * the consuming app). Keep optional fields aligned with the Zod schema —\n * anything missing here OR there will be silently dropped on submission.\n *\n * Tracking fields (rdt_cid, utm_*, referrer_url) are forwarded as-is and\n * persisted by the API; they don't need to be declared on the form, but\n * the index signature below preserves any extra keys callers spread in.\n */\ninterface ContactFormData {\n name: string;\n email: string;\n helpCategory: string;\n message: string;\n linkedin_url?: string;\n companySize?: string;\n referralSource?: string;\n rdt_cid?: string;\n // Permissive index signature — tracking metadata, A/B variants, and\n // future per-form fields flow through unchanged. The API decides what\n // to keep via its Zod schema.\n [key: string]: unknown;\n}\n\n/**\n * useContactSubmission\n * --------------------\n * Provides a helper for submitting contact form data to `/api/contact`.\n * Handles loading state, success detection, toast notifications, and redirect.\n * Follows the same pattern as useWaitlistRegistration for consistency.\n */\nexport function useContactSubmission(options: ContactSubmissionOptions = {}) {\n const { userId, successRedirectUrl, successToastMessage, onSuccess } = options;\n const { toast } = useToast();\n const router = useRouter();\n // Endpoint URL injected via context — hub provides hub default, embedded\n // app provides its proxied path. Throws if no provider is mounted.\n const { contactUrl } = useRequiredEndpointsRuntime();\n const [isSubmitting, setIsSubmitting] = useState(false);\n const [isSuccess, setIsSuccess] = useState(false);\n\n const submit = useCallback(async (formData: ContactFormData) => {\n if (isSubmitting) return;\n\n setIsSubmitting(true);\n\n try {\n const response = await fetch(contactUrl, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ \n ...formData,\n user_id: userId\n }),\n });\n\n const data = await response.json();\n\n if (!response.ok) {\n throw new Error(data.error || 'Failed to submit contact form');\n }\n\n // Success\n setIsSuccess(true);\n const message = successToastMessage \n ? `Thank you! Your message has been sent. ${successToastMessage}`\n : 'Thank you! Your message has been sent successfully.';\n \n toast({\n title: \"Message sent!\",\n description: message,\n variant: 'success',\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Something went wrong. Please try again.';\n toast({ \n title: 'Failed to send message', \n description: message, \n variant: 'destructive' \n });\n throw error; // allow caller to handle if needed\n } finally {\n setIsSubmitting(false);\n }\n }, [isSubmitting, toast, userId, successToastMessage, contactUrl]);\n\n // Handle redirect after success\n useEffect(() => {\n if (isSuccess && successRedirectUrl) {\n console.log('🚀 Contact submission successful, redirecting to:', successRedirectUrl);\n const timer = setTimeout(() => {\n console.log('🎯 Performing redirect now to:', successRedirectUrl);\n if (successRedirectUrl.startsWith('http')) {\n window.location.href = successRedirectUrl;\n } else {\n router.push(successRedirectUrl);\n }\n }, 1500);\n \n return () => clearTimeout(timer);\n }\n }, [isSuccess, successRedirectUrl, router]);\n\n // Call onSuccess callback if provided\n useEffect(() => {\n if (isSuccess && onSuccess) {\n onSuccess();\n }\n }, [isSuccess, onSuccess]);\n\n return { submit, isSubmitting, isSuccess, setIsSuccess } as const;\n}","'use client'\n\nimport { useEffect, useState, useRef, useCallback } from 'react'\n\ninterface UseQuickActionHintOptions {\n /** Number of quick actions to cycle through */\n actionCount: number\n /** Duration each action is highlighted in milliseconds */\n cycleDuration?: number\n /** Whether the hint should be enabled */\n enabled?: boolean\n}\n\ninterface UseQuickActionHintReturn {\n /** The index of the currently active hint (-1 if none) */\n activeHintIndex: number\n /** Manually stop the hint animation */\n stopHint: () => void\n /** Reference to attach to the container element (not used in simplified version) */\n containerRef: React.RefObject<HTMLDivElement | null>\n}\n\n/**\n * SIMPLIFIED hook for managing quick action hint animations\n *\n * Cycles infinitely through quick actions until user interacts.\n * Simple mount-based trigger - no complex visibility detection.\n */\nexport function useQuickActionHint({\n actionCount,\n cycleDuration = 5000,\n enabled = true\n}: UseQuickActionHintOptions): UseQuickActionHintReturn {\n const [activeHintIndex, setActiveHintIndex] = useState(-1)\n const containerRef = useRef<HTMLDivElement | null>(null)\n const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)\n const cycleCountRef = useRef(0)\n const currentIndexRef = useRef(0)\n const sequenceRef = useRef<number[]>([])\n\n /**\n * Fisher-Yates shuffle for random, non-repeating sequence\n */\n const shuffleIndices = useCallback((length: number): number[] => {\n const indices = Array.from({ length }, (_, i) => i)\n for (let i = indices.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1));\n [indices[i], indices[j]] = [indices[j], indices[i]]\n }\n return indices\n }, [])\n\n /**\n * Stop the hint animation\n */\n const stopHint = useCallback(() => {\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current)\n timeoutRef.current = null\n }\n setActiveHintIndex(-1)\n cycleCountRef.current = 0\n currentIndexRef.current = 0\n sequenceRef.current = []\n }, [])\n\n /**\n * Advance to next hint\n */\n const advanceHint = useCallback(() => {\n // Generate new sequence if starting a new cycle\n if (currentIndexRef.current === 0) {\n sequenceRef.current = shuffleIndices(actionCount)\n }\n\n // Get next index\n const nextIndex = sequenceRef.current[currentIndexRef.current]\n setActiveHintIndex(nextIndex)\n\n // Move to next position\n currentIndexRef.current++\n\n // Check if cycle complete - reset to continue infinitely\n if (currentIndexRef.current >= actionCount) {\n currentIndexRef.current = 0\n cycleCountRef.current++\n }\n\n // Schedule next hint - runs infinitely until stopped by user interaction\n timeoutRef.current = setTimeout(advanceHint, cycleDuration)\n }, [actionCount, cycleDuration, shuffleIndices])\n\n /**\n * Start hint animation when component mounts with actions\n */\n useEffect(() => {\n // Don't start if disabled, no actions, or reduced motion\n if (!enabled || actionCount === 0) {\n return\n }\n\n if (typeof window !== 'undefined') {\n const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches\n if (prefersReducedMotion) return\n }\n\n // Start after a short delay to allow component to settle\n const startTimeout = setTimeout(() => {\n cycleCountRef.current = 0\n currentIndexRef.current = 0\n advanceHint()\n }, 500)\n\n // Cleanup on unmount or when dependencies change\n return () => {\n clearTimeout(startTimeout)\n stopHint()\n }\n }, [enabled, actionCount, advanceHint, stopHint])\n\n return {\n activeHintIndex,\n stopHint,\n containerRef\n }\n}\n","'use client';\n\nimport { useCallback, useState } from 'react';\nimport { useToast } from './use-toast';\n\ninterface UseCopyToClipboardOptions {\n successTitle?: string;\n successDescription?: string;\n errorTitle?: string;\n errorDescription?: string;\n resetDelay?: number;\n}\n\nexport function useCopyToClipboard({\n successTitle = 'Copied',\n successDescription = 'Copied to clipboard',\n errorTitle = 'Copy failed',\n errorDescription = 'Could not copy to clipboard',\n resetDelay = 2000,\n}: UseCopyToClipboardOptions = {}) {\n const { toast } = useToast();\n const [copied, setCopied] = useState(false);\n\n const copy = useCallback(\n async (text: string) => {\n try {\n await navigator.clipboard.writeText(text);\n setCopied(true);\n toast({ title: successTitle, description: successDescription, variant: 'success' });\n setTimeout(() => setCopied(false), resetDelay);\n } catch {\n toast({ title: errorTitle, description: errorDescription, variant: 'destructive' });\n }\n },\n [successTitle, successDescription, errorTitle, errorDescription, resetDelay, toast],\n );\n\n return { copy, copied };\n}\n","import { useEffect, useMemo, useState, useRef } from 'react'\n\n/**\n * Configuration for batch image fetching\n */\nexport interface BatchImageFetchConfig {\n /** Base URL for tenant-specific API calls (e.g., 'https://tenant.openframe.dev' or '') */\n tenantHostUrl?: string\n /** Enable dev mode with Bearer token from localStorage */\n enableDevMode?: boolean\n /** localStorage key for access token (default: 'of_access_token') */\n accessTokenKey?: string\n}\n\n/**\n * Global configuration for batch image fetching\n * Can be set once at app initialization\n */\nlet globalBatchImageConfig: BatchImageFetchConfig = {}\n\n/**\n * Configure global settings for batch image fetching\n * Call this once in your app initialization (e.g., _app.tsx or layout.tsx)\n *\n * @example\n * ```typescript\n * // In app initialization\n * configureBatchImageFetch({\n * tenantHostUrl: process.env.NEXT_PUBLIC_TENANT_HOST_URL || '',\n * enableDevMode: process.env.NEXT_PUBLIC_ENABLE_DEV_TICKET_OBSERVER === 'true'\n * })\n * ```\n */\nexport function configureBatchImageFetch(config: BatchImageFetchConfig): void {\n globalBatchImageConfig = { ...globalBatchImageConfig, ...config }\n}\n\n/**\n * Get current batch image fetch configuration\n */\nfunction getBatchImageConfig(): Required<BatchImageFetchConfig> {\n return {\n tenantHostUrl: globalBatchImageConfig.tenantHostUrl || '',\n enableDevMode: globalBatchImageConfig.enableDevMode ?? false,\n accessTokenKey: globalBatchImageConfig.accessTokenKey || 'of_access_token'\n }\n}\n\n/**\n * Fetch multiple images with authentication in batch\n * Returns a map of original imageUrl to fetched blob URL\n *\n * @param imageUrls - Array of image URLs to fetch\n * @param config - Optional configuration override for this batch\n * @returns Promise resolving to map of original URL → blob URL\n *\n * @example\n * ```typescript\n * const images = await batchFetchAuthenticatedImages([\n * '/api/organizations/123/image',\n * '/api/organizations/456/image'\n * ])\n * // { '/api/organizations/123/image': 'blob:...', '/api/organizations/456/image': 'blob:...' }\n * ```\n */\nexport async function batchFetchAuthenticatedImages(\n imageUrls: string[],\n config?: BatchImageFetchConfig\n): Promise<Record<string, string | undefined>> {\n const results: Record<string, string | undefined> = {}\n\n if (imageUrls.length === 0) {\n return results\n }\n\n const { tenantHostUrl, enableDevMode, accessTokenKey } = {\n ...getBatchImageConfig(),\n ...config\n }\n\n const fetchPromises = imageUrls.map(async (imageUrl) => {\n try {\n // Construct full image URL\n let fullImageUrl: string\n if (imageUrl.startsWith('http://') || imageUrl.startsWith('https://')) {\n fullImageUrl = imageUrl\n } else if (imageUrl.startsWith('/api/')) {\n fullImageUrl = `${tenantHostUrl}${imageUrl}`\n } else if (imageUrl.startsWith('/')) {\n fullImageUrl = `${tenantHostUrl}/api${imageUrl}`\n } else {\n fullImageUrl = `${tenantHostUrl}/api/${imageUrl}`\n }\n\n // Add cache buster\n const cacheBuster = `?t=${Date.now()}`\n fullImageUrl = fullImageUrl + cacheBuster\n\n // Prepare headers\n const headers: Record<string, string> = {\n 'Accept': 'image/*',\n 'Cache-Control': 'no-cache, no-store, must-revalidate',\n 'Pragma': 'no-cache'\n }\n\n // Add Bearer token in dev mode\n if (enableDevMode) {\n try {\n const accessToken = localStorage.getItem(accessTokenKey)\n if (accessToken) {\n headers['Authorization'] = `Bearer ${accessToken}`\n }\n } catch (error) {\n // Silently continue without token\n }\n }\n\n // Fetch image\n const response = await fetch(fullImageUrl, {\n method: 'GET',\n credentials: 'include', // Include cookies for authentication\n headers\n })\n\n if (!response.ok) {\n throw new Error(`Failed to fetch image: ${response.status}`)\n }\n\n // Convert to blob URL\n const blob = await response.blob()\n const objectUrl = URL.createObjectURL(blob)\n\n return { imageUrl, fetchedUrl: objectUrl }\n } catch (error) {\n console.warn(`Failed to fetch image ${imageUrl}:`, error)\n return { imageUrl, fetchedUrl: undefined }\n }\n })\n\n const fetchResults = await Promise.all(fetchPromises)\n\n fetchResults.forEach(({ imageUrl, fetchedUrl }) => {\n results[imageUrl] = fetchedUrl\n })\n\n return results\n}\n\n/**\n * React hook to batch fetch images with authentication\n *\n * Features:\n * - Automatically deduplicates URLs\n * - Caches fetched results\n * - Only fetches new/unfetched URLs\n * - Cleans up blob URLs on unmount\n *\n * @param imageUrls - Array of image URLs (can include null/undefined)\n * @param config - Optional configuration override\n * @returns Map of original URL → fetched blob URL\n *\n * @example\n * ```typescript\n * // In a component\n * const imageUrls = useMemo(() =>\n * organizations.map(org => org.imageUrl).filter(Boolean),\n * [organizations]\n * )\n * const fetchedImages = useBatchImages(imageUrls)\n *\n * // Use in render\n * <img src={fetchedImages[org.imageUrl]} alt={org.name} />\n * ```\n */\nexport function useBatchImages(\n imageUrls: (string | null | undefined)[],\n config?: BatchImageFetchConfig\n): Record<string, string | undefined> {\n const [fetchedImages, setFetchedImages] = useState<Record<string, string | undefined>>({})\n const [loading, setLoading] = useState(false)\n\n // Deduplicate and filter out null/undefined\n const uniqueUrls = useMemo(() =>\n Array.from(new Set(imageUrls.filter((url): url is string => Boolean(url)))),\n [imageUrls]\n )\n\n // Track URLs we've already requested to avoid duplicate fetches\n const requestedUrls = useRef<Set<string>>(new Set())\n\n useEffect(() => {\n if (uniqueUrls.length === 0) {\n setFetchedImages({})\n return\n }\n\n // Find URLs we haven't fetched yet\n const urlsToFetch = uniqueUrls.filter(url => !requestedUrls.current.has(url))\n\n if (urlsToFetch.length === 0) {\n return // All URLs already requested\n }\n\n // Mark these URLs as requested\n urlsToFetch.forEach(url => requestedUrls.current.add(url))\n\n setLoading(true)\n\n batchFetchAuthenticatedImages(urlsToFetch, config)\n .then(newResults => {\n setFetchedImages(prev => ({ ...prev, ...newResults }))\n })\n .catch(error => {\n console.error('Failed to batch fetch images:', error)\n })\n .finally(() => {\n setLoading(false)\n })\n\n // Cleanup blob URLs on unmount\n return () => {\n Object.values(fetchedImages).forEach(blobUrl => {\n if (blobUrl && blobUrl.startsWith('blob:')) {\n URL.revokeObjectURL(blobUrl)\n }\n })\n }\n }, [uniqueUrls, config])\n\n return fetchedImages\n}\n","import { useEffect, useState, useRef } from 'react'\n\n/**\n * Configuration for single image fetching\n * Uses same config as batch image fetching for consistency\n */\nexport interface AuthenticatedImageConfig {\n /** Base URL for tenant-specific API calls (e.g., 'https://tenant.openframe.dev' or '') */\n tenantHostUrl?: string\n /** Enable dev mode with Bearer token from localStorage */\n enableDevMode?: boolean\n /** localStorage key for access token (default: 'of_access_token') */\n accessTokenKey?: string\n}\n\n/**\n * Global configuration for authenticated image fetching\n * Shared with useBatchImages for consistency\n */\nlet globalImageConfig: AuthenticatedImageConfig = {}\n\n/**\n * Global cache for authenticated images\n * Stores blob URLs by cache key\n */\ninterface ImageCacheEntry {\n blobUrl: string\n timestamp: number\n refCount: number\n}\n\nconst imageCache = new Map<string, ImageCacheEntry>()\nconst pendingRequests = new Map<string, Promise<string | undefined>>()\n\n/**\n * Cache cleanup interval (5 minutes)\n */\nconst CACHE_CLEANUP_INTERVAL = 5 * 60 * 1000\n\n/**\n * Cache entry max age (30 minutes)\n */\nconst CACHE_MAX_AGE = 30 * 60 * 1000\n\n/**\n * Clean up expired cache entries\n */\nfunction cleanupImageCache() {\n const now = Date.now()\n for (const [key, entry] of imageCache.entries()) {\n if (entry.refCount === 0 && now - entry.timestamp > CACHE_MAX_AGE) {\n URL.revokeObjectURL(entry.blobUrl)\n imageCache.delete(key)\n }\n }\n}\n\n/**\n * Periodic cache cleanup\n */\nif (typeof window !== 'undefined') {\n setInterval(cleanupImageCache, CACHE_CLEANUP_INTERVAL)\n}\n\n/**\n * Configure global settings for authenticated image fetching\n * Call this once in your app initialization (e.g., _app.tsx or layout.tsx)\n *\n * Note: This uses the same configuration as useBatchImages. If you've already\n * called configureBatchImageFetch(), you don't need to call this separately.\n *\n * @example\n * ```typescript\n * // In app initialization\n * configureAuthenticatedImage({\n * tenantHostUrl: process.env.NEXT_PUBLIC_TENANT_HOST_URL || '',\n * enableDevMode: process.env.NEXT_PUBLIC_ENABLE_DEV_TICKET_OBSERVER === 'true'\n * })\n * ```\n */\nexport function configureAuthenticatedImage(config: AuthenticatedImageConfig): void {\n globalImageConfig = { ...globalImageConfig, ...config }\n}\n\n/**\n * Get current authenticated image configuration\n */\nfunction getImageConfig(): Required<AuthenticatedImageConfig> {\n return {\n tenantHostUrl: globalImageConfig.tenantHostUrl || '',\n enableDevMode: globalImageConfig.enableDevMode ?? false,\n accessTokenKey: globalImageConfig.accessTokenKey || 'of_access_token'\n }\n}\n\n/**\n * React hook to fetch a single image with authentication\n *\n * Features:\n * - Fetches image with cookie authentication\n * - Optional Bearer token in dev mode\n * - Converts to blob URL for img src\n * - Automatic cleanup of blob URLs\n * - Cache-busting with refreshKey\n * - Loading and error states\n * - **Global caching** - Prevents duplicate requests for identical URLs\n * - **Automatic deduplication** - Multiple components using same URL share cached result\n * - **Reference counting** - Cached blobs cleaned up when no longer used\n *\n * @param imageUrl - The image URL to fetch (null/undefined = no fetch)\n * @param refreshKey - Optional key to force re-fetch (e.g., version number, timestamp)\n * @param config - Optional configuration override\n * @returns Object with imageUrl (blob), isLoading, and error\n *\n * @example\n * ```typescript\n * // Basic usage\n * const { imageUrl, isLoading, error } = useAuthenticatedImage(\n * organization?.imageUrl\n * )\n *\n * // With refresh key (e.g., after upload)\n * const { imageUrl } = useAuthenticatedImage(\n * organization?.imageUrl,\n * organization?.imageVersion // Timestamp or version number\n * )\n *\n * // In render\n * {imageUrl && <img src={imageUrl} alt=\"Organization\" />}\n * ```\n */\nexport function useAuthenticatedImage(\n imageUrl?: string | null,\n refreshKey?: string | number,\n config?: AuthenticatedImageConfig\n): {\n imageUrl: string | undefined\n isLoading: boolean\n error: string | null\n} {\n const [fetchedImageUrl, setFetchedImageUrl] = useState<string | undefined>()\n const [isLoading, setIsLoading] = useState(false)\n const [error, setError] = useState<string | null>(null)\n const currentCacheKeyRef = useRef<string | null>(null)\n\n useEffect(() => {\n if (!imageUrl) {\n setFetchedImageUrl(undefined)\n setIsLoading(false)\n setError(null)\n \n if (currentCacheKeyRef.current) {\n const entry = imageCache.get(currentCacheKeyRef.current)\n if (entry) {\n entry.refCount--\n }\n currentCacheKeyRef.current = null\n }\n return\n }\n\n const { tenantHostUrl, enableDevMode, accessTokenKey } = {\n ...getImageConfig(),\n ...config\n }\n\n // Construct full image URL\n let fullImageUrl: string\n if (imageUrl.startsWith('http://') || imageUrl.startsWith('https://')) {\n fullImageUrl = imageUrl\n } else if (imageUrl.startsWith('/api/')) {\n fullImageUrl = `${tenantHostUrl}${imageUrl}`\n } else if (imageUrl.startsWith('/')) {\n fullImageUrl = `${tenantHostUrl}/api${imageUrl}`\n } else {\n fullImageUrl = `${tenantHostUrl}/api/${imageUrl}`\n }\n\n // Create cache key (use refreshKey if provided, otherwise no cache buster for caching)\n const cacheKey = refreshKey ? `${fullImageUrl}?v=${refreshKey}` : fullImageUrl\n \n if (currentCacheKeyRef.current && currentCacheKeyRef.current !== cacheKey) {\n const prevEntry = imageCache.get(currentCacheKeyRef.current)\n if (prevEntry) {\n prevEntry.refCount--\n }\n }\n \n currentCacheKeyRef.current = cacheKey\n\n const cachedEntry = imageCache.get(cacheKey)\n if (cachedEntry) {\n cachedEntry.refCount++\n cachedEntry.timestamp = Date.now()\n \n setFetchedImageUrl(cachedEntry.blobUrl)\n setIsLoading(false)\n setError(null)\n return\n }\n\n const pendingRequest = pendingRequests.get(cacheKey)\n if (pendingRequest) {\n setIsLoading(true)\n setError(null)\n \n pendingRequest\n .then(blobUrl => {\n if (blobUrl) {\n const entry = imageCache.get(cacheKey)\n if (entry) {\n entry.refCount++\n setFetchedImageUrl(blobUrl)\n }\n }\n setIsLoading(false)\n })\n .catch(err => {\n setError(err instanceof Error ? err.message : 'Failed to fetch image')\n setIsLoading(false)\n })\n return\n }\n\n setIsLoading(true)\n setError(null)\n\n const requestUrl = refreshKey ? cacheKey : `${fullImageUrl}?t=${Date.now()}`\n\n // Prepare headers\n const headers: Record<string, string> = {\n 'Accept': 'image/*',\n 'Cache-Control': 'no-cache, no-store, must-revalidate',\n 'Pragma': 'no-cache'\n }\n\n // Add Bearer token in dev mode\n if (enableDevMode) {\n try {\n const accessToken = localStorage.getItem(accessTokenKey)\n if (accessToken) {\n headers['Authorization'] = `Bearer ${accessToken}`\n }\n } catch (error) {\n // Silently continue without token\n }\n }\n\n const fetchPromise = fetch(requestUrl, {\n method: 'GET',\n credentials: 'include', // Include cookies for authentication\n headers\n })\n .then(response => {\n if (!response.ok) {\n throw new Error(`Failed to fetch image: ${response.status}`)\n }\n return response.blob()\n })\n .then(blob => {\n const objectUrl = URL.createObjectURL(blob)\n \n imageCache.set(cacheKey, {\n blobUrl: objectUrl,\n timestamp: Date.now(),\n refCount: 1\n })\n \n setFetchedImageUrl(objectUrl)\n setIsLoading(false)\n return objectUrl\n })\n .catch(err => {\n setError(err instanceof Error ? err.message : 'Failed to fetch image')\n setFetchedImageUrl(undefined)\n setIsLoading(false)\n throw err\n })\n .finally(() => {\n pendingRequests.delete(cacheKey)\n })\n\n pendingRequests.set(cacheKey, fetchPromise)\n\n }, [imageUrl, refreshKey, config])\n\n useEffect(() => {\n return () => {\n if (currentCacheKeyRef.current) {\n const entry = imageCache.get(currentCacheKeyRef.current)\n if (entry) {\n entry.refCount--\n }\n }\n }\n }, [])\n\n return { imageUrl: fetchedImageUrl, isLoading, error }\n}\n","/**\n * useQueryParams Hook - GraphQL Integration for URL State Management\n *\n * Automatically generates URL state management from GraphQL queries.\n * Parses query AST at runtime, flattens nested input types, and syncs with URL.\n *\n * @example\n * const LOGS_QUERY = gql`\n * query GetLogs($search: String, $filter: LogFilterInput) { ... }\n * `\n *\n * const { variables, setParam } = useQueryParams(LOGS_QUERY)\n * const { data } = useQuery(LOGS_QUERY, { variables })\n *\n * // URL: /logs?search=error&severity=critical\n * // variables: { search: 'error', filter: { severity: ['critical'] } }\n */\n\n'use client'\n\nimport { useEffect, useState, useMemo, useCallback } from 'react'\nimport { useRouter, useSearchParams } from '../../embed-shims/next-navigation'\nimport { DocumentNode } from 'graphql'\n\nimport { extractVariablesFromQuery } from './graphql-parser'\nimport { flattenQueryVariables, mergeDefaults, validateSchema, FlattenedParam } from './flatten-schema'\nimport { urlParamsToVariables, variablesToUrlParams, mergeVariables, clearParams } from './url-converter'\nimport { introspector } from './introspection'\n\n/**\n * Options for useQueryParams hook\n */\nexport interface UseQueryParamsOptions {\n /** Default values for parameters */\n defaultValues?: Record<string, any>\n\n /** GraphQL endpoint for introspection (defaults to process.env.NEXT_PUBLIC_API_URL/graphql) */\n introspectionEndpoint?: string\n\n /** HTTP headers for introspection (e.g., authentication) */\n introspectionHeaders?: Record<string, string>\n\n /** Skip introspection (use only AST parsing, no nested type flattening) */\n skipIntrospection?: boolean\n\n /** Custom parameter name mapping (override auto-generated names) */\n paramMapping?: Record<string, string>\n\n /** Enable debug logging */\n debug?: boolean\n}\n\n/**\n * Return type for useQueryParams hook\n */\nexport interface UseQueryParamsReturn<TVariables = Record<string, any>> {\n /** GraphQL variables ready for Apollo Client */\n variables: TVariables\n\n /** Raw URL parameters (before conversion to variables) */\n params: Record<string, any>\n\n /** Flattened parameter schema */\n schema: Record<string, FlattenedParam>\n\n /** Set a single parameter */\n setParam: (key: string, value: any) => void\n\n /** Set multiple parameters at once */\n setParams: (params: Record<string, any>) => void\n\n /** Clear specific parameters */\n clearParams: (keys: string[]) => void\n\n /** Reset all parameters (clear URL) */\n resetParams: () => void\n\n /** Whether schema is ready (introspection complete) */\n isReady: boolean\n\n /** Loading state during initialization */\n isLoading: boolean\n\n /** Error during initialization */\n error: Error | null\n}\n\n/**\n * useQueryParams - Auto-generate URL state from GraphQL query\n *\n * This hook:\n * 1. Parses GraphQL query AST to extract variable definitions\n * 2. Fetches GraphQL schema via introspection (optional, cached)\n * 3. Flattens nested input types to simple URL parameters\n * 4. Syncs URL ↔ GraphQL variables bidirectionally\n * 5. Provides type-safe parameter updates\n *\n * @param query - GraphQL DocumentNode (from gql`` template tag)\n * @param options - Configuration options\n * @returns Hook API for managing URL state\n */\nexport function useQueryParams<TVariables = Record<string, any>>(\n query: DocumentNode,\n options: UseQueryParamsOptions = {}\n): UseQueryParamsReturn<TVariables> {\n const router = useRouter()\n const searchParams = useSearchParams()\n\n const [isLoading, setIsLoading] = useState(true)\n const [isReady, setIsReady] = useState(false)\n const [error, setError] = useState<Error | null>(null)\n const [schema, setSchema] = useState<Record<string, FlattenedParam>>({})\n\n // Extract default values\n const defaultValues = options.defaultValues || {}\n const skipIntrospection = options.skipIntrospection || false\n const debug = options.debug || false\n\n // Initialize: Parse query + fetch schema (once)\n useEffect(() => {\n async function initialize() {\n try {\n if (debug) console.log('[useQueryParams] Initializing...')\n\n // 1. Extract variables from query AST\n const queryVariables = extractVariablesFromQuery(query)\n\n if (debug) {\n console.log('[useQueryParams] Extracted variables:', queryVariables)\n }\n\n // 2. Fetch GraphQL schema via introspection (if needed and not skipped)\n if (!skipIntrospection && !introspector.isLoaded()) {\n const endpoint = options.introspectionEndpoint ||\n (typeof window !== 'undefined' && process.env.NEXT_PUBLIC_API_URL\n ? `${process.env.NEXT_PUBLIC_API_URL}/graphql`\n : '')\n\n if (endpoint) {\n try {\n await introspector.fetchSchema(endpoint, options.introspectionHeaders)\n if (debug) console.log('[useQueryParams] Introspection complete')\n } catch (err) {\n console.warn('[useQueryParams] Introspection failed, continuing without it:', err)\n // Continue without introspection - nested types won't be flattened\n }\n }\n }\n\n // 3. Flatten schema (with or without introspection)\n let flattenedSchema = await flattenQueryVariables(queryVariables, introspector)\n\n // Apply custom param mapping if provided\n if (options.paramMapping) {\n flattenedSchema = applyParamMapping(flattenedSchema, options.paramMapping)\n }\n\n // Merge default values\n flattenedSchema = mergeDefaults(flattenedSchema, defaultValues)\n\n // Validate schema\n validateSchema(flattenedSchema)\n\n if (debug) {\n console.log('[useQueryParams] Flattened schema:', flattenedSchema)\n }\n\n setSchema(flattenedSchema)\n setIsReady(true)\n } catch (err) {\n const error = err instanceof Error ? err : new Error(String(err))\n console.error('[useQueryParams] Initialization failed:', error)\n setError(error)\n } finally {\n setIsLoading(false)\n }\n }\n\n initialize()\n }, [query, options.introspectionEndpoint, skipIntrospection, debug])\n\n // Convert URL params to GraphQL variables\n const variables = useMemo(() => {\n if (!isReady) {\n return defaultValues as TVariables\n }\n\n try {\n const varsFromUrl = urlParamsToVariables(searchParams, schema)\n const merged = { ...defaultValues, ...varsFromUrl }\n\n if (debug) {\n console.log('[useQueryParams] Variables from URL:', merged)\n }\n\n return merged as TVariables\n } catch (err) {\n console.error('[useQueryParams] Failed to convert URL to variables:', err)\n return defaultValues as TVariables\n }\n }, [searchParams, schema, isReady, defaultValues, debug])\n\n // Raw URL params (before conversion)\n const params = useMemo(() => {\n const result: Record<string, any> = {}\n searchParams.forEach((value, key) => {\n if (result[key]) {\n // Multiple values - convert to array\n if (!Array.isArray(result[key])) {\n result[key] = [result[key]]\n }\n result[key].push(value)\n } else {\n result[key] = value\n }\n })\n return result\n }, [searchParams])\n\n // Update URL with new parameters\n const updateUrl = useCallback((newParams: URLSearchParams) => {\n const url = newParams.toString() ? `?${newParams.toString()}` : window.location.pathname\n\n if (debug) {\n console.log('[useQueryParams] Updating URL:', url)\n }\n\n // Use replace for shallow routing (no page reload, no history spam)\n router.replace(url, { scroll: false })\n }, [router, debug])\n\n // Set a single parameter\n const setParam = useCallback((key: string, value: any) => {\n if (!isReady) {\n console.warn('[useQueryParams] Schema not ready, cannot set param')\n return\n }\n\n try {\n const currentVars = variables\n const updated = mergeVariables(currentVars as Record<string, any>, { [key]: value }, schema)\n const newParams = variablesToUrlParams(updated, schema)\n updateUrl(newParams)\n } catch (err) {\n console.error('[useQueryParams] Failed to set param:', err)\n }\n }, [variables, schema, isReady, updateUrl])\n\n // Set multiple parameters\n const setParams = useCallback((updates: Record<string, any>) => {\n if (!isReady) {\n console.warn('[useQueryParams] Schema not ready, cannot set params')\n return\n }\n\n try {\n const currentVars = variables\n const updated = mergeVariables(currentVars as Record<string, any>, updates, schema)\n const newParams = variablesToUrlParams(updated, schema)\n updateUrl(newParams)\n } catch (err) {\n console.error('[useQueryParams] Failed to set params:', err)\n }\n }, [variables, schema, isReady, updateUrl])\n\n // Clear specific parameters\n const clearParamsHandler = useCallback((keys: string[]) => {\n if (!isReady) {\n console.warn('[useQueryParams] Schema not ready, cannot clear params')\n return\n }\n\n try {\n const currentVars = variables\n const updated = clearParams(currentVars as Record<string, any>, keys, schema)\n const newParams = variablesToUrlParams(updated, schema)\n updateUrl(newParams)\n } catch (err) {\n console.error('[useQueryParams] Failed to clear params:', err)\n }\n }, [variables, schema, isReady, updateUrl])\n\n // Reset all parameters\n const resetParams = useCallback(() => {\n if (debug) {\n console.log('[useQueryParams] Resetting params')\n }\n\n router.replace(window.location.pathname, { scroll: false })\n }, [router, debug])\n\n return {\n variables,\n params,\n schema,\n setParam,\n setParams,\n clearParams: clearParamsHandler,\n resetParams,\n isReady,\n isLoading,\n error\n }\n}\n\n/**\n * Apply custom parameter name mapping to schema\n */\nfunction applyParamMapping(\n schema: Record<string, FlattenedParam>,\n mapping: Record<string, string>\n): Record<string, FlattenedParam> {\n const mapped: Record<string, FlattenedParam> = {}\n\n for (const [key, param] of Object.entries(schema)) {\n const newKey = mapping[key] || key\n mapped[newKey] = {\n ...param,\n urlParamName: mapping[key] || param.urlParamName\n }\n }\n\n return mapped\n}\n","/**\n * GraphQL AST Parser for URL State Management\n *\n * Extracts variable definitions from GraphQL DocumentNode at runtime\n * to automatically generate URL parameter handling.\n */\n\nimport {\n DocumentNode,\n VariableDefinitionNode,\n TypeNode,\n NamedTypeNode,\n ListTypeNode,\n NonNullTypeNode,\n visit\n} from 'graphql'\n\n/**\n * JavaScript type that can be represented in URL parameters\n */\nexport type JSType = 'string' | 'number' | 'boolean' | 'array' | 'object'\n\n/**\n * Variable definition extracted from GraphQL query\n */\nexport interface VariableDefinition {\n /** Variable name (e.g., \"search\", \"filter\") */\n name: string\n /** JavaScript type for URL parameter handling */\n type: JSType\n /** Whether the variable is required (non-null in GraphQL) */\n required: boolean\n /** Whether the variable is an array/list */\n isArray: boolean\n /** Original GraphQL type name (e.g., \"String\", \"LogFilterInput\") */\n graphqlTypeName: string\n}\n\n/**\n * Parsed type information from GraphQL TypeNode\n */\ninterface ParsedType {\n typeName: string\n isNonNull: boolean\n isList: boolean\n}\n\n/**\n * Extract all variable definitions from a GraphQL query\n *\n * @param query - GraphQL DocumentNode (from gql template tag)\n * @returns Record of variable definitions keyed by variable name\n *\n * @example\n * const LOGS_QUERY = gql`\n * query GetLogs($search: String, $filter: LogFilterInput) { ... }\n * `\n *\n * const variables = extractVariablesFromQuery(LOGS_QUERY)\n * // {\n * // search: { name: 'search', type: 'string', ... },\n * // filter: { name: 'filter', type: 'object', graphqlTypeName: 'LogFilterInput' }\n * // }\n */\nexport function extractVariablesFromQuery(\n query: DocumentNode\n): Record<string, VariableDefinition> {\n const variables: Record<string, VariableDefinition> = {}\n\n visit(query, {\n VariableDefinition(node: VariableDefinitionNode) {\n const name = node.variable.name.value\n const typeInfo = parseGraphQLType(node.type)\n\n variables[name] = {\n name,\n type: mapGraphQLTypeToJS(typeInfo.typeName),\n required: typeInfo.isNonNull,\n isArray: typeInfo.isList,\n graphqlTypeName: typeInfo.typeName\n }\n }\n })\n\n return variables\n}\n\n/**\n * Parse GraphQL TypeNode to extract type information\n * Handles NonNullType, ListType, and NamedType recursively\n *\n * @param typeNode - GraphQL type node from AST\n * @returns Parsed type information\n */\nfunction parseGraphQLType(typeNode: TypeNode): ParsedType {\n let typeName = ''\n let isNonNull = false\n let isList = false\n\n // Unwrap NonNullType wrapper\n if (typeNode.kind === 'NonNullType') {\n isNonNull = true\n typeNode = typeNode.type\n }\n\n // Handle ListType\n if (typeNode.kind === 'ListType') {\n isList = true\n typeNode = typeNode.type\n\n // ListType can also be wrapped in NonNullType\n if (typeNode.kind === 'NonNullType') {\n typeNode = typeNode.type\n }\n }\n\n // Extract the base type name\n if (typeNode.kind === 'NamedType') {\n typeName = typeNode.name.value\n }\n\n return {\n typeName,\n isNonNull,\n isList\n }\n}\n\n/**\n * Map GraphQL scalar/input types to JavaScript types\n *\n * @param graphqlType - GraphQL type name (e.g., \"String\", \"Int\", \"LogFilterInput\")\n * @returns JavaScript type for URL parameter handling\n */\nfunction mapGraphQLTypeToJS(graphqlType: string): JSType {\n // GraphQL scalar types\n const scalarTypeMap: Record<string, JSType> = {\n 'String': 'string',\n 'Int': 'number',\n 'Float': 'number',\n 'Boolean': 'boolean',\n 'ID': 'string'\n }\n\n // Check if it's a known scalar\n if (scalarTypeMap[graphqlType]) {\n return scalarTypeMap[graphqlType]\n }\n\n // Unknown types are assumed to be input objects\n // These will be flattened using introspection\n return 'object'\n}\n\n/**\n * Check if a GraphQL type is a scalar type\n */\nexport function isScalarType(graphqlTypeName: string): boolean {\n const scalars = ['String', 'Int', 'Float', 'Boolean', 'ID']\n return scalars.includes(graphqlTypeName)\n}\n\n/**\n * Check if a GraphQL type is an input object type\n */\nexport function isInputObjectType(graphqlTypeName: string): boolean {\n return !isScalarType(graphqlTypeName)\n}\n","/**\n * Schema Flattening Utilities for URL State Management\n *\n * Converts nested GraphQL input types to flat URL parameter mappings.\n * Example: { filter: { severity: [...] } } → URL params: ?severity=...\n */\n\nimport { VariableDefinition, JSType } from './graphql-parser'\nimport { GraphQLIntrospector } from './introspection'\n\n/**\n * Flattened URL parameter configuration\n *\n * Maps URL parameter names to their GraphQL variable paths\n */\nexport interface FlattenedParam {\n /** URL parameter name (e.g., \"severity\") */\n urlParamName: string\n /** Path in GraphQL variables (e.g., \"filter.severity\") */\n graphqlPath: string\n /** JavaScript type for URL handling */\n type: JSType\n /** Default value for this parameter */\n defaultValue?: any\n /** Whether this parameter is required */\n required?: boolean\n /** Whether this parameter is an array */\n isArray?: boolean\n}\n\n/**\n * Flatten GraphQL query variables to URL parameter schema\n *\n * Takes top-level query variables and flattens nested input objects\n * to create a simple URL parameter mapping.\n *\n * @param queryVariables - Variables extracted from GraphQL query\n * @param introspector - Introspector instance with loaded schema\n * @returns Flattened parameter schema for URL handling\n *\n * @example\n * Input variables:\n * {\n * search: { type: 'string', ... },\n * filter: { type: 'object', graphqlTypeName: 'LogFilterInput' },\n * cursor: { type: 'string', ... }\n * }\n *\n * Output schema:\n * {\n * search: { urlParamName: 'search', graphqlPath: 'search', type: 'string' },\n * severity: { urlParamName: 'severity', graphqlPath: 'filter.severity', type: 'array' },\n * toolType: { urlParamName: 'toolType', graphqlPath: 'filter.toolType', type: 'array' },\n * cursor: { urlParamName: 'cursor', graphqlPath: 'cursor', type: 'string' }\n * }\n *\n * URL: ?search=error&severity=critical&toolType=tactical&cursor=abc\n */\nexport async function flattenQueryVariables(\n queryVariables: Record<string, VariableDefinition>,\n introspector: GraphQLIntrospector\n): Promise<Record<string, FlattenedParam>> {\n const flattened: Record<string, FlattenedParam> = {}\n\n for (const [varName, varDef] of Object.entries(queryVariables)) {\n // Primitive types or arrays - keep at top level\n if (varDef.type !== 'object') {\n flattened[varName] = {\n urlParamName: varName,\n graphqlPath: varName,\n type: varDef.isArray ? 'array' : varDef.type,\n required: varDef.required,\n isArray: varDef.isArray\n }\n continue\n }\n\n // Input object types - flatten fields to top level\n // This requires introspection to know the input type's fields\n if (introspector.isLoaded() && introspector.hasType(varDef.graphqlTypeName)) {\n const fields = introspector.getInputTypeFields(varDef.graphqlTypeName)\n\n for (const [fieldName, fieldDef] of Object.entries(fields)) {\n flattened[fieldName] = {\n urlParamName: fieldName,\n graphqlPath: `${varName}.${fieldName}`,\n type: fieldDef.isArray ? 'array' : fieldDef.type,\n required: fieldDef.required,\n isArray: fieldDef.isArray\n }\n }\n } else {\n // Introspection not available or type not found\n // Keep as top-level object (will need manual handling)\n flattened[varName] = {\n urlParamName: varName,\n graphqlPath: varName,\n type: 'object',\n required: varDef.required,\n isArray: false\n }\n }\n }\n\n return flattened\n}\n\n/**\n * Merge default values into flattened schema\n *\n * @param schema - Flattened parameter schema\n * @param defaults - Default values keyed by URL param name\n * @returns Updated schema with default values\n */\nexport function mergeDefaults(\n schema: Record<string, FlattenedParam>,\n defaults: Record<string, any>\n): Record<string, FlattenedParam> {\n const merged: Record<string, FlattenedParam> = {}\n\n for (const [key, param] of Object.entries(schema)) {\n merged[key] = {\n ...param,\n defaultValue: defaults[key] !== undefined ? defaults[key] : param.defaultValue\n }\n }\n\n return merged\n}\n\n/**\n * Validate that flattened schema has no conflicts\n *\n * Ensures that no two parameters map to the same URL param name\n *\n * @param schema - Flattened parameter schema\n * @throws Error if conflicts are detected\n */\nexport function validateSchema(schema: Record<string, FlattenedParam>): void {\n const urlParamNames = new Set<string>()\n\n for (const [key, param] of Object.entries(schema)) {\n if (urlParamNames.has(param.urlParamName)) {\n throw new Error(\n `[FlattenSchema] Conflict: Multiple parameters map to URL param \"${param.urlParamName}\"`\n )\n }\n urlParamNames.add(param.urlParamName)\n }\n}\n\n/**\n * Get all array-type parameters from schema\n *\n * Useful for knowing which URL params should use repeated values\n * (e.g., ?severity=error&severity=warning)\n *\n * @param schema - Flattened parameter schema\n * @returns Array parameter names\n */\nexport function getArrayParams(schema: Record<string, FlattenedParam>): string[] {\n return Object.entries(schema)\n .filter(([_, param]) => param.type === 'array' || param.isArray)\n .map(([key]) => key)\n}\n\n/**\n * Get required parameters from schema\n *\n * @param schema - Flattened parameter schema\n * @returns Required parameter names\n */\nexport function getRequiredParams(schema: Record<string, FlattenedParam>): string[] {\n return Object.entries(schema)\n .filter(([_, param]) => param.required)\n .map(([key]) => key)\n}\n\n/**\n * Check if a parameter should be included in URL\n *\n * Excludes:\n * - null/undefined values\n * - Empty arrays\n * - Default values (to keep URLs clean)\n *\n * @param value - Parameter value\n * @param param - Parameter configuration\n * @returns Whether to include in URL\n */\nexport function shouldIncludeInUrl(\n value: any,\n param: FlattenedParam\n): boolean {\n // Null/undefined - exclude\n if (value === null || value === undefined) {\n return false\n }\n\n // Empty arrays - exclude\n if (Array.isArray(value) && value.length === 0) {\n return false\n }\n\n // Empty strings - exclude\n if (value === '') {\n return false\n }\n\n // Default values - exclude to keep URL clean\n if (param.defaultValue !== undefined && value === param.defaultValue) {\n return false\n }\n\n return true\n}\n","/**\n * URL ↔ Variables Converter for URL State Management\n *\n * Bidirectional conversion between URL parameters and GraphQL variables.\n * Handles type coercion, nested paths, and array parameters.\n */\n\nimport { FlattenedParam, shouldIncludeInUrl } from './flatten-schema'\nimport { JSType } from './graphql-parser'\n\n/**\n * Convert URL search params to GraphQL variables\n *\n * Reads URL parameters and reconstructs the nested GraphQL variables object\n * based on the flattened schema mapping.\n *\n * @param searchParams - URLSearchParams from window.location or Next.js\n * @param schema - Flattened parameter schema\n * @returns GraphQL variables object ready for Apollo Client\n *\n * @example\n * URL: ?search=error&severity=critical&severity=error&cursor=abc\n * Schema: {\n * search: { graphqlPath: 'search', type: 'string' },\n * severity: { graphqlPath: 'filter.severity', type: 'array' },\n * cursor: { graphqlPath: 'cursor', type: 'string' }\n * }\n * Result: {\n * search: 'error',\n * filter: { severity: ['critical', 'error'] },\n * cursor: 'abc'\n * }\n */\nexport function urlParamsToVariables(\n searchParams: URLSearchParams,\n schema: Record<string, FlattenedParam>\n): Record<string, any> {\n const variables: Record<string, any> = {}\n\n for (const [paramName, paramConfig] of Object.entries(schema)) {\n // Read value from URL\n const rawValue = paramConfig.type === 'array' || paramConfig.isArray\n ? searchParams.getAll(paramName)\n : searchParams.get(paramName)\n\n // Skip if no value in URL\n if (!rawValue || (Array.isArray(rawValue) && rawValue.length === 0)) {\n // Use default value if available\n if (paramConfig.defaultValue !== undefined) {\n setNestedValue(variables, paramConfig.graphqlPath, paramConfig.defaultValue)\n }\n continue\n }\n\n // Coerce value to correct type\n const value = coerceValue(rawValue, paramConfig.type)\n\n // Set value at nested path\n setNestedValue(variables, paramConfig.graphqlPath, value)\n }\n\n return variables\n}\n\n/**\n * Convert GraphQL variables to URL search params\n *\n * Flattens nested GraphQL variables to URL parameters based on schema mapping.\n * Excludes null/undefined/default values to keep URLs clean.\n *\n * @param variables - GraphQL variables object\n * @param schema - Flattened parameter schema\n * @returns URLSearchParams ready for router.push()\n *\n * @example\n * Variables: {\n * search: 'error',\n * filter: { severity: ['critical'], toolType: [] },\n * cursor: null\n * }\n * Result URL: ?search=error&severity=critical\n * (empty arrays and null values excluded)\n */\nexport function variablesToUrlParams(\n variables: Record<string, any>,\n schema: Record<string, FlattenedParam>\n): URLSearchParams {\n const params = new URLSearchParams()\n\n for (const [paramName, paramConfig] of Object.entries(schema)) {\n // Get value from nested path\n const value = getNestedValue(variables, paramConfig.graphqlPath)\n\n // Skip if should not include in URL\n if (!shouldIncludeInUrl(value, paramConfig)) {\n continue\n }\n\n // Add to URL params\n if (Array.isArray(value)) {\n // Array: Use repeated params (e.g., ?tag=foo&tag=bar)\n value.forEach(v => {\n if (v !== null && v !== undefined && v !== '') {\n params.append(paramName, String(v))\n }\n })\n } else {\n // Single value: Use set\n params.set(paramName, String(value))\n }\n }\n\n return params\n}\n\n/**\n * Coerce URL parameter value to correct JavaScript type\n *\n * @param value - Raw value from URLSearchParams (string or string[])\n * @param type - Target JavaScript type\n * @returns Coerced value\n */\nexport function coerceValue(value: string | string[], type: JSType): any {\n // Array handling\n if (Array.isArray(value)) {\n return value.map(v => coerceValue(v, type === 'array' ? 'string' : type))\n }\n\n // Type coercion for single values\n switch (type) {\n case 'number':\n const num = parseFloat(value)\n return isNaN(num) ? null : num\n\n case 'boolean':\n return value === 'true' || value === '1'\n\n case 'array':\n // Single value treated as array with one element\n return [value]\n\n case 'string':\n default:\n return value\n }\n}\n\n/**\n * Set value at nested path in object\n *\n * Creates intermediate objects as needed.\n *\n * @param obj - Target object to modify\n * @param path - Dot-separated path (e.g., \"filter.severity\")\n * @param value - Value to set\n *\n * @example\n * setNestedValue({}, 'filter.severity', ['error'])\n * // Result: { filter: { severity: ['error'] } }\n */\nexport function setNestedValue(obj: any, path: string, value: any): void {\n const parts = path.split('.')\n let current = obj\n\n // Navigate/create intermediate objects\n for (let i = 0; i < parts.length - 1; i++) {\n const part = parts[i]\n if (!(part in current) || typeof current[part] !== 'object') {\n current[part] = {}\n }\n current = current[part]\n }\n\n // Set final value\n const lastPart = parts[parts.length - 1]\n current[lastPart] = value\n}\n\n/**\n * Get value from nested path in object\n *\n * Returns undefined if path doesn't exist.\n *\n * @param obj - Source object\n * @param path - Dot-separated path (e.g., \"filter.severity\")\n * @returns Value at path or undefined\n *\n * @example\n * getNestedValue({ filter: { severity: ['error'] } }, 'filter.severity')\n * // Result: ['error']\n */\nexport function getNestedValue(obj: any, path: string): any {\n return path.split('.').reduce((current, key) => {\n return current?.[key]\n }, obj)\n}\n\n/**\n * Merge URL parameters into existing variables\n *\n * Useful for updating specific parameters without losing others.\n *\n * @param currentVariables - Current GraphQL variables\n * @param updates - Parameter updates (flat object)\n * @param schema - Flattened parameter schema\n * @returns Updated variables object\n */\nexport function mergeVariables(\n currentVariables: Record<string, any>,\n updates: Record<string, any>,\n schema: Record<string, FlattenedParam>\n): Record<string, any> {\n const merged = { ...currentVariables }\n\n for (const [paramName, value] of Object.entries(updates)) {\n const paramConfig = schema[paramName]\n if (!paramConfig) continue\n\n setNestedValue(merged, paramConfig.graphqlPath, value)\n }\n\n return merged\n}\n\n/**\n * Clear specific parameters from variables\n *\n * @param variables - Current GraphQL variables\n * @param paramNames - Parameter names to clear\n * @param schema - Flattened parameter schema\n * @returns Variables with specified params removed\n */\nexport function clearParams(\n variables: Record<string, any>,\n paramNames: string[],\n schema: Record<string, FlattenedParam>\n): Record<string, any> {\n const cleared = { ...variables }\n\n for (const paramName of paramNames) {\n const paramConfig = schema[paramName]\n if (!paramConfig) continue\n\n // Set to undefined (will be excluded from URL)\n setNestedValue(cleared, paramConfig.graphqlPath, undefined)\n }\n\n return cleared\n}\n\n/**\n * Validate GraphQL variables against schema\n *\n * Checks for required parameters and type consistency.\n *\n * @param variables - GraphQL variables to validate\n * @param schema - Flattened parameter schema\n * @returns Validation errors (empty array if valid)\n */\nexport function validateVariables(\n variables: Record<string, any>,\n schema: Record<string, FlattenedParam>\n): string[] {\n const errors: string[] = []\n\n for (const [paramName, paramConfig] of Object.entries(schema)) {\n const value = getNestedValue(variables, paramConfig.graphqlPath)\n\n // Check required parameters\n if (paramConfig.required && (value === null || value === undefined)) {\n errors.push(`Required parameter \"${paramName}\" is missing`)\n }\n\n // Check type consistency\n if (value !== null && value !== undefined) {\n const actualType = Array.isArray(value) ? 'array' : typeof value\n const expectedType = paramConfig.type === 'array' ? 'array' : paramConfig.type\n\n if (actualType !== expectedType && actualType !== 'object') {\n errors.push(\n `Parameter \"${paramName}\" has wrong type: expected ${expectedType}, got ${actualType}`\n )\n }\n }\n }\n\n return errors\n}\n","/**\n * GraphQL Schema Introspection for URL State Management\n *\n * Fetches GraphQL schema via introspection query at runtime (works behind auth).\n * Caches schema in localStorage + memory for performance.\n */\n\nimport {\n getIntrospectionQuery,\n buildClientSchema,\n IntrospectionQuery,\n GraphQLSchema,\n GraphQLInputObjectType,\n GraphQLInputType,\n isInputObjectType\n} from 'graphql'\nimport { VariableDefinition, JSType } from './graphql-parser'\n\n/**\n * Schema cache structure stored in localStorage\n */\ninterface SchemaCache {\n schema: IntrospectionQuery\n timestamp: number\n version: string\n}\n\n/**\n * GraphQL Introspection Manager\n *\n * Singleton class that handles schema introspection with caching.\n * Fetches schema once per session and caches for 24 hours.\n */\nexport class GraphQLIntrospector {\n private schema: GraphQLSchema | null = null\n private cacheKey = 'graphql-schema-cache-v1'\n private cacheDuration = 24 * 60 * 60 * 1000 // 24 hours\n private schemaVersion = '1.0.0'\n\n /**\n * Fetch GraphQL schema via introspection query\n *\n * @param endpoint - GraphQL endpoint URL\n * @param headers - HTTP headers (e.g., authentication)\n * @param skipCache - Force fresh fetch, ignore cache\n *\n * @example\n * await introspector.fetchSchema(\n * 'http://localhost/api/graphql',\n * { 'Authorization': 'Bearer token123' }\n * )\n */\n async fetchSchema(\n endpoint: string,\n headers: Record<string, string> = {},\n skipCache = false\n ): Promise<void> {\n // Check cache first (unless skipped)\n if (!skipCache) {\n const cached = this.loadFromCache()\n if (cached && Date.now() - cached.timestamp < this.cacheDuration) {\n try {\n this.schema = buildClientSchema(cached.schema)\n return\n } catch (error) {\n console.warn('[Introspector] Failed to build schema from cache:', error)\n // Continue to fetch fresh schema\n }\n }\n }\n\n // Fetch schema via introspection query\n try {\n const response = await fetch(endpoint, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n ...headers\n },\n body: JSON.stringify({\n query: getIntrospectionQuery()\n })\n })\n\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`)\n }\n\n const { data, errors } = await response.json()\n\n if (errors) {\n throw new Error(`GraphQL errors: ${JSON.stringify(errors)}`)\n }\n\n if (!data) {\n throw new Error('No data returned from introspection query')\n }\n\n // Build and cache schema\n this.schema = buildClientSchema(data)\n this.saveToCache(data)\n } catch (error) {\n console.error('[Introspector] Failed to fetch schema:', error)\n throw error\n }\n }\n\n /**\n * Get fields from a GraphQL input object type\n *\n * @param typeName - Name of the input type (e.g., \"LogFilterInput\")\n * @returns Record of field definitions\n *\n * @example\n * const fields = introspector.getInputTypeFields('LogFilterInput')\n * // {\n * // severity: { name: 'severity', type: 'array', ... },\n * // toolType: { name: 'toolType', type: 'array', ... }\n * // }\n */\n getInputTypeFields(typeName: string): Record<string, VariableDefinition> {\n if (!this.schema) {\n return {}\n }\n\n const type = this.schema.getType(typeName)\n\n if (!type || !isInputObjectType(type)) {\n return {}\n }\n\n const fields: Record<string, VariableDefinition> = {}\n const typeFields = type.getFields()\n\n for (const [fieldName, field] of Object.entries(typeFields)) {\n const fieldType = this.unwrapType(field.type)\n\n fields[fieldName] = {\n name: fieldName,\n type: this.mapGraphQLTypeToJS(fieldType.typeName),\n required: fieldType.isNonNull,\n isArray: fieldType.isList,\n graphqlTypeName: fieldType.typeName\n }\n }\n\n return fields\n }\n\n /**\n * Check if a type exists in the schema\n */\n hasType(typeName: string): boolean {\n if (!this.schema) return false\n return this.schema.getType(typeName) !== undefined\n }\n\n /**\n * Get the schema (if loaded)\n */\n getSchema(): GraphQLSchema | null {\n return this.schema\n }\n\n /**\n * Check if schema is loaded\n */\n isLoaded(): boolean {\n return this.schema !== null\n }\n\n /**\n * Clear cached schema\n */\n clearCache(): void {\n try {\n localStorage.removeItem(this.cacheKey)\n this.schema = null\n } catch {\n // Ignore storage errors\n }\n }\n\n /**\n * Unwrap GraphQL type to get base type info\n * Handles NonNull and List wrappers\n */\n private unwrapType(type: GraphQLInputType): {\n typeName: string\n isNonNull: boolean\n isList: boolean\n } {\n let typeName = ''\n let isNonNull = false\n let isList = false\n let current: any = type\n\n // Unwrap NonNull wrapper\n if (current.toString().endsWith('!')) {\n isNonNull = true\n current = 'ofType' in current ? current.ofType : current\n }\n\n // Check for List wrapper\n if (current.toString().startsWith('[')) {\n isList = true\n current = 'ofType' in current ? current.ofType : current\n }\n\n // Handle inner NonNull (e.g., [String!])\n if (current.toString().endsWith('!')) {\n current = 'ofType' in current ? current.ofType : current\n }\n\n // Get base type name\n typeName = current.name || current.toString().replace(/[[\\]!]/g, '')\n\n return { typeName, isNonNull, isList }\n }\n\n /**\n * Map GraphQL types to JavaScript types for URL handling\n */\n private mapGraphQLTypeToJS(graphqlType: string): JSType {\n const typeMap: Record<string, JSType> = {\n 'String': 'string',\n 'Int': 'number',\n 'Float': 'number',\n 'Boolean': 'boolean',\n 'ID': 'string'\n }\n\n return typeMap[graphqlType] || 'object'\n }\n\n /**\n * Load schema from localStorage cache\n */\n private loadFromCache(): SchemaCache | null {\n try {\n const cached = localStorage.getItem(this.cacheKey)\n if (!cached) return null\n\n const parsed = JSON.parse(cached) as SchemaCache\n\n // Validate cache version\n if (parsed.version !== this.schemaVersion) {\n return null\n }\n\n return parsed\n } catch (error) {\n console.warn('[Introspector] Failed to load cache:', error)\n return null\n }\n }\n\n /**\n * Save schema to localStorage cache\n */\n private saveToCache(schema: IntrospectionQuery): void {\n try {\n const cache: SchemaCache = {\n schema,\n timestamp: Date.now(),\n version: this.schemaVersion\n }\n\n localStorage.setItem(this.cacheKey, JSON.stringify(cache))\n } catch (error) {\n console.warn('[Introspector] Failed to save cache:', error)\n // Ignore storage errors (e.g., quota exceeded)\n }\n }\n}\n\n/**\n * Singleton introspector instance\n *\n * Use this instance throughout the application to share schema cache\n *\n * @example\n * import { introspector } from '@flamingo/ui-kit/hooks/state'\n *\n * // Initialize after auth\n * await introspector.fetchSchema(endpoint, { Authorization: token })\n *\n * // Get input type fields\n * const fields = introspector.getInputTypeFields('LogFilterInput')\n */\nexport const introspector = new GraphQLIntrospector()\n","/**\n * useApiParams Hook - REST API Integration for URL State Management\n *\n * Manual schema definition for REST APIs. Provides same URL sync functionality\n * as useQueryParams but without GraphQL dependency.\n *\n * @example\n * const { params, setParam } = useApiParams({\n * search: { type: 'string', default: '' },\n * page: { type: 'number', default: 1 },\n * tags: { type: 'array', default: [] }\n * })\n *\n * fetch(`/api/items?${new URLSearchParams(params)}`)\n *\n * // URL: /items?search=laptop&page=2&tags=electronics&tags=sale\n * // params: { search: 'laptop', page: 2, tags: ['electronics', 'sale'] }\n */\n\n'use client'\n\nimport { useRouter, useSearchParams } from '../../embed-shims/next-navigation'\nimport { useCallback, useMemo, useRef } from 'react'\nimport { FlattenedParam, shouldIncludeInUrl } from './flatten-schema'\nimport { JSType } from './graphql-parser'\nimport { coerceValue } from './url-converter'\n\n/**\n * Returns the previous reference if the JSON-serialized content of `value`\n * hasn't changed across renders. Internal helper used to shield consumers from\n * ref churn caused by:\n * - `useSearchParams()` returning a new `ReadonlyURLSearchParams` instance\n * on every render even when the URL is unchanged (Next.js behavior).\n * - Consumers passing the schema as a fresh object literal on every render.\n */\nfunction useContentStable<T>(value: T, key: string): T {\n const ref = useRef<{ value: T; key: string } | undefined>(undefined)\n if (ref.current && ref.current.key === key) return ref.current.value\n ref.current = { value, key }\n return value\n}\n\n/**\n * Reuses a previous array reference if its content (shallow string equality)\n * matches the freshly parsed array. Lets `params.tier` etc. stay\n * reference-stable across renders that don't actually change those values.\n */\nfunction reuseIfShallowEqual<T extends string | number | boolean>(\n prev: unknown,\n next: T[],\n): T[] {\n if (!Array.isArray(prev) || prev.length !== next.length) return next\n for (let i = 0; i < next.length; i++) {\n if (prev[i] !== next[i]) return next\n }\n return prev as T[]\n}\n\n/**\n * Type mapping from JSType to TypeScript types for OUTPUT (reading params)\n */\ntype OutputTypeMap = {\n string: string\n number: number\n boolean: boolean\n array: string[]\n object: Record<string, unknown>\n}\n\n/**\n * Type mapping from JSType to TypeScript types for INPUT (setting params)\n * More permissive to allow null/undefined in arrays which get filtered\n */\ntype InputTypeMap = {\n string: string | null | undefined\n number: number | null | undefined\n boolean: boolean | null | undefined\n array: (string | null | undefined)[]\n object: Record<string, unknown> | null | undefined\n}\n\n/**\n * Get the TypeScript type for OUTPUT (reading from params)\n */\ntype OutputTypeForJSType<T extends JSType> = OutputTypeMap[T]\n\n/**\n * Get the TypeScript type for INPUT (setting params)\n */\ntype InputTypeForJSType<T extends JSType> = InputTypeMap[T]\n\n/**\n * Get the default value type for a given JSType\n */\ntype DefaultValueForType<T extends JSType> =\n T extends 'array' ? string[] :\n T extends 'object' ? Record<string, unknown> :\n OutputTypeMap[T]\n\n/**\n * Parameter configuration for a single parameter\n */\nexport interface ParamConfig<T extends JSType = JSType> {\n /** JavaScript type for URL parameter */\n type: T\n /** Default value matching the type */\n default?: DefaultValueForType<T>\n /** Whether parameter is required */\n required?: boolean\n}\n\n/**\n * REST API parameter schema definition\n * Maps parameter names to their configuration\n */\nexport type ParamSchema = Record<string, ParamConfig>\n\n/**\n * Helper to create a typed param schema (preserves literal types)\n */\nexport function defineParamSchema<T extends ParamSchema>(schema: T): T {\n return schema\n}\n\n/**\n * Options for useApiParams hook\n */\nexport interface UseApiParamsOptions {\n /** Enable debug logging */\n debug?: boolean\n}\n\n/**\n * Infer the OUTPUT params type from a ParamSchema (for reading)\n * Maps each key in the schema to its corresponding TypeScript type\n *\n * @example\n * const schema = defineParamSchema({\n * search: { type: 'string', default: '' },\n * page: { type: 'number', default: 1 },\n * tags: { type: 'array', default: [] }\n * })\n * type Params = InferParamsFromSchema<typeof schema>\n * // { search: string; page: number; tags: string[] }\n */\nexport type InferParamsFromSchema<TSchema extends ParamSchema> = {\n [K in keyof TSchema]: TSchema[K]['type'] extends infer T\n ? T extends JSType\n ? OutputTypeForJSType<T>\n : never\n : never\n}\n\n/**\n * Infer the INPUT params type from a ParamSchema (for setting)\n * More permissive to allow null/undefined values\n */\nexport type InferInputParamsFromSchema<TSchema extends ParamSchema> = {\n [K in keyof TSchema]: TSchema[K]['type'] extends infer T\n ? T extends JSType\n ? InputTypeForJSType<T>\n : never\n : never\n}\n\n/**\n * Type for parameter values that can be set\n * Allows setting values that match the schema types or can be coerced to them\n */\nexport type ParamValue = \n | string \n | number \n | boolean \n | string[] \n | (string | null | undefined)[]\n | Record<string, unknown> \n | null \n | undefined\n\n/**\n * Return type for useApiParams hook with strict typing\n */\nexport interface UseApiParamsReturn<\n TSchema extends ParamSchema,\n TParams = InferParamsFromSchema<TSchema>\n> {\n /** Parsed parameters object with strict typing */\n params: TParams\n\n /** URLSearchParams for fetch/axios */\n urlSearchParams: URLSearchParams\n\n /** Set a single parameter with type-safe key and value */\n setParam: <K extends keyof TSchema & string>(\n key: K,\n value: InferInputParamsFromSchema<Pick<TSchema, K>>[K]\n ) => void\n\n /** Set multiple parameters at once */\n setParams: (updates: Partial<InferInputParamsFromSchema<TSchema>>) => void\n\n /** Clear specific parameters */\n clearParams: (keys: (keyof TSchema & string)[]) => void\n\n /** Reset all parameters (clear URL) */\n resetParams: () => void\n}\n\n/**\n * useApiParams - Manual URL state for REST APIs\n *\n * This hook:\n * 1. Reads URL search parameters\n * 2. Coerces to correct types based on schema\n * 3. Provides type-safe parameter updates\n * 4. Syncs changes to URL automatically\n *\n * @param schema - Parameter schema definition\n * @param options - Configuration options\n * @returns Hook API for managing URL state\n */\nexport function useApiParams<TSchema extends ParamSchema>(\n schema: TSchema,\n options: UseApiParamsOptions = {}\n): UseApiParamsReturn<TSchema> {\n const router = useRouter()\n const searchParamsLive = useSearchParams()\n const debug = options.debug || false\n\n // ───── Reference-stability layer ──────────────────────────────────────\n //\n // Goal: `params`, `params.<arrayField>`, and the setter callbacks must keep\n // the SAME reference across renders unless the URL or schema content\n // actually changes. Otherwise consumer `useMemo`/`useEffect` deps that\n // include `params.foo` invalidate on every parent re-render.\n //\n // Without this, every call site has to defensively `JSON.stringify` filter\n // arrays into a content-key — a known footgun. The stability is provided\n // here, once, instead of in 17 consumers.\n\n // 1. URL string is the canonical, value-stable representation of search params.\n const searchString = searchParamsLive.toString()\n\n // 2. Schema reference stabilized by content. Consumers commonly pass an\n // object literal each render, which would otherwise invalidate every memo.\n const schemaKey = useMemo(() => JSON.stringify(schema), [schema])\n const stableSchema = useContentStable(schema, schemaKey)\n\n // ──────────────────────────────────────────────────────────────────────\n\n // Convert schema to flattened format for reuse\n // biome-ignore lint/correctness/useExhaustiveDependencies: schemaKey is the content-stable key for `stableSchema`.\n const flattenedSchema = useMemo((): Record<string, FlattenedParam> => {\n const flattened: Record<string, FlattenedParam> = {}\n\n for (const [key, config] of Object.entries(stableSchema)) {\n flattened[key] = {\n urlParamName: key,\n graphqlPath: key,\n type: config.type,\n defaultValue: config.default,\n required: config.required,\n isArray: config.type === 'array'\n }\n }\n\n return flattened\n }, [schemaKey])\n\n // Parse URL parameters with type coercion. Reuse previous array refs when\n // their content is unchanged so `params.<arrayField>` stays stable across\n // renders that don't touch that specific field.\n const prevParamsRef = useRef<Record<string, unknown> | undefined>(undefined)\n // biome-ignore lint/correctness/useExhaustiveDependencies: `searchString` and `schemaKey` are content-stable representations of `searchParamsLive` and `stableSchema`.\n const params = useMemo((): InferParamsFromSchema<TSchema> => {\n const sp = new URLSearchParams(searchString)\n const result: Record<string, unknown> = {}\n const prev = prevParamsRef.current\n\n for (const [key, config] of Object.entries(stableSchema)) {\n // Read from URL\n const rawValue = config.type === 'array'\n ? sp.getAll(key)\n : sp.get(key)\n\n // Use value from URL or default\n let value: unknown\n if (rawValue && (Array.isArray(rawValue) ? rawValue.length > 0 : true)) {\n value = coerceValue(rawValue, config.type)\n } else {\n value = config.default\n }\n\n // Reuse previous reference when content matches — keeps array fields\n // reference-stable when an unrelated param changed.\n if (Array.isArray(value) && prev) {\n value = reuseIfShallowEqual(prev[key], value as (string | number | boolean)[])\n }\n\n result[key] = value\n }\n\n if (debug) {\n console.log('[useApiParams] Parsed params:', result)\n }\n\n prevParamsRef.current = result\n return result as InferParamsFromSchema<TSchema>\n }, [searchString, schemaKey, debug])\n\n // Helper: Add parameter value to URLSearchParams\n const addParamToSearchParams = useCallback((\n searchParams: URLSearchParams,\n key: string,\n value: ParamValue\n ): void => {\n if (value === undefined || value === '' || value === null) {\n return\n }\n\n if (Array.isArray(value)) {\n value.forEach(v => {\n if (v !== undefined && v !== '' && v !== null) {\n searchParams.append(key, String(v))\n }\n })\n } else if (typeof value === 'object') {\n // For objects, convert to JSON string\n searchParams.set(key, JSON.stringify(value))\n } else {\n searchParams.set(key, String(value))\n }\n }, [])\n\n // Get URLSearchParams for fetch/axios. Iterates `stableSchema` (not raw\n // `schema`) so consumers passing an inline schema literal don't invalidate\n // this memo on every render.\n // biome-ignore lint/correctness/useExhaustiveDependencies: `schemaKey` is the content-stable key for `stableSchema`.\n const urlSearchParams = useMemo((): URLSearchParams => {\n const newParams = new URLSearchParams()\n\n for (const key of Object.keys(stableSchema)) {\n const value = (params as Record<string, unknown>)[key]\n const paramConfig = flattenedSchema[key]\n\n // Skip if should not include\n if (!shouldIncludeInUrl(value, paramConfig)) {\n continue\n }\n\n addParamToSearchParams(newParams, key, value as ParamValue)\n }\n\n return newParams\n }, [params, schemaKey, flattenedSchema, addParamToSearchParams])\n\n // Update URL with new parameters (preserve other params not managed by this\n // hook). Depends only on value-stable inputs (`searchString`, `schemaKey`),\n // so the callback ref itself is stable across renders that don't change URL\n // or schema — important for consumers that put `setParam`/`setParams` in\n // `useEffect` deps.\n // biome-ignore lint/correctness/useExhaustiveDependencies: `searchString` and `schemaKey` are content-stable representations of `searchParamsLive` and `stableSchema`.\n const updateUrl = useCallback((newParams: URLSearchParams, keysToRemove: string[] = []) => {\n // Preserve all existing params, then override with new ones\n const finalParams = new URLSearchParams(searchString)\n\n // Remove keys that are explicitly marked for removal\n keysToRemove.forEach(key => {\n if (key in stableSchema) {\n finalParams.delete(key)\n }\n })\n\n // Remove keys that are being updated (from newParams)\n // This preserves other schema parameters that aren't being changed\n newParams.forEach((_, key) => {\n // Only remove keys that are in our schema\n if (key in stableSchema) {\n finalParams.delete(key)\n }\n })\n\n // Add all new values (including multiple values for array params)\n // Only add parameters that are in our schema to avoid duplicating external params\n newParams.forEach((value, key) => {\n // Only process keys that are in our schema\n if (key in stableSchema) {\n if (finalParams.has(key)) {\n // Key already exists (from array params), append\n finalParams.append(key, value)\n } else {\n // First value for this key, use set\n finalParams.set(key, value)\n }\n }\n })\n\n const url = finalParams.toString()\n ? `?${finalParams.toString()}`\n : window.location.pathname\n\n if (debug) {\n console.log('[useApiParams] Updating URL:', url)\n }\n\n // Use replace for shallow routing (no page reload, no history spam)\n router.replace(url, { scroll: false })\n }, [router, debug, searchString, schemaKey])\n\n // Helper to check if value is empty\n const isEmptyValue = (value: unknown): boolean => {\n if (value === undefined || value === null || value === '') {\n return true\n }\n if (Array.isArray(value)) {\n // Empty array or array with all empty/null/undefined values\n return value.length === 0 || value.every(v => v === undefined || v === null || v === '')\n }\n return false\n }\n\n // Set a single parameter\n // biome-ignore lint/correctness/useExhaustiveDependencies: `schemaKey` is the content-stable key for `stableSchema`.\n const setParam = useCallback(<K extends keyof TSchema & string>(\n key: K,\n value: InferInputParamsFromSchema<Pick<TSchema, K>>[K]\n ) => {\n const config = stableSchema[key]\n\n if (!config) {\n console.warn(`[useApiParams] Unknown parameter: ${key}`)\n return\n }\n\n const newParams = new URLSearchParams()\n\n if (isEmptyValue(value)) {\n updateUrl(newParams, [key])\n } else {\n addParamToSearchParams(newParams, key, value as ParamValue)\n updateUrl(newParams)\n }\n }, [schemaKey, updateUrl, addParamToSearchParams])\n\n // Set multiple parameters\n // biome-ignore lint/correctness/useExhaustiveDependencies: `schemaKey` is the content-stable key for `stableSchema`.\n const setParams = useCallback((\n updates: Partial<InferInputParamsFromSchema<TSchema>>\n ) => {\n const newParams = new URLSearchParams()\n const keysToRemove: string[] = []\n\n for (const [key, value] of Object.entries(updates)) {\n const config = stableSchema[key]\n\n if (!config) {\n console.warn(`[useApiParams] Unknown parameter: ${key}`)\n continue\n }\n\n if (isEmptyValue(value)) {\n keysToRemove.push(key)\n } else {\n addParamToSearchParams(newParams, key, value as ParamValue)\n }\n }\n\n updateUrl(newParams, keysToRemove)\n }, [schemaKey, updateUrl, addParamToSearchParams])\n\n // Clear specific parameters\n const clearParams = useCallback((keys: (keyof TSchema & string)[]) => {\n const newParams = new URLSearchParams()\n updateUrl(newParams, keys)\n }, [updateUrl])\n\n // Reset all parameters\n const resetParams = useCallback(() => {\n if (debug) {\n console.log('[useApiParams] Resetting params')\n }\n\n router.replace(window.location.pathname, { scroll: false })\n }, [router, debug])\n\n return {\n params,\n urlSearchParams,\n setParam,\n setParams,\n clearParams,\n resetParams\n }\n}\n\n/**\n * Helper: Create URLSearchParams from object\n *\n * Handles arrays as repeated parameters. Filters out undefined, and empty values.\n *\n * @param params - Parameters object\n * @returns URLSearchParams\n */\nexport function createSearchParams(params: Record<string, ParamValue>): URLSearchParams {\n const searchParams = new URLSearchParams()\n\n for (const [key, value] of Object.entries(params)) {\n if (value === undefined || value === '' || value === null) {\n continue\n }\n\n if (Array.isArray(value)) {\n value.forEach(v => {\n if (v !== undefined && v !== '' && v !== null) {\n searchParams.append(key, String(v))\n }\n })\n } else if (typeof value === 'object') {\n // For objects, convert to JSON string\n searchParams.set(key, JSON.stringify(value))\n } else {\n searchParams.set(key, String(value))\n }\n }\n\n return searchParams\n}","/**\n * Unified Cursor Pagination State Hook\n *\n * Manages all common cursor-based pagination state logic:\n * - URL state management with useApiParams\n * - Debounced search input\n * - hasLoadedBeyondFirst tracking\n * - Initial load detection\n * - Search change detection\n * - Pagination handlers (next, reset)\n *\n * This eliminates ~60-80 lines of boilerplate from each paginated component.\n *\n * @example\n * const {\n * searchInput, setSearchInput,\n * hasLoadedBeyondFirst,\n * handleNextPage, handleResetToFirstPage\n * } = useCursorPaginationState({\n * onInitialLoad: (search, cursor) => fetchDialogs(false, search, true, cursor),\n * onSearchChange: (search) => fetchDialogs(false, search)\n * })\n */\n\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport { useApiParams, type UseApiParamsReturn } from './use-api-params';\n\nexport interface UseCursorPaginationStateOptions {\n /**\n * Debounce delay for search input (default: 300ms)\n */\n debounceMs?: number\n\n /**\n * Callback for initial page load\n * Called once on mount with current search and cursor from URL\n */\n onInitialLoad: (search: string, cursor: string | null) => void | Promise<unknown>\n\n /**\n * Callback when search term changes (after debounce)\n * Called after URL is updated, cursor is already reset\n */\n onSearchChange: (search: string) => void | Promise<unknown>\n}\n\nconst urlSchema = {\n search: { type: 'string' as const, default: '' },\n cursor: { type: 'string' as const, default: '' },\n}\n\ntype ApiParamsReturn = UseApiParamsReturn<typeof urlSchema>\n\n/** Pagination params managed by this hook */\nexport type PaginationParams = ApiParamsReturn['params']\n\nexport interface CursorPaginationStateReturn {\n // Search\n searchInput: string\n setSearchInput: (value: string) => void\n\n // Pagination tracking\n hasLoadedBeyondFirst: boolean\n setHasLoadedBeyondFirst: (value: boolean) => void\n\n // Handlers for useTablePagination\n handleNextPage: (endCursor: string, fetchFn: () => Promise<unknown>) => Promise<void>\n handleResetToFirstPage: (fetchFn: () => Promise<unknown>) => Promise<void>\n\n // URL params access (for advanced use cases)\n params: PaginationParams\n setParam: ApiParamsReturn['setParam']\n setParams: ApiParamsReturn['setParams']\n}\n\nexport function useCursorPaginationState(\n options: UseCursorPaginationStateOptions\n): CursorPaginationStateReturn {\n const {\n onInitialLoad,\n onSearchChange,\n } = options\n\n const { params, setParam, setParams } = useApiParams(urlSchema)\n\n // Local search input with debounce\n const [searchInput, setSearchInput] = useState(params.search || '')\n\n // Pagination tracking\n const [hasLoadedBeyondFirst, setHasLoadedBeyondFirst] = useState(false)\n // Use a counter instead of boolean to ensure effects see the latest state\n const [initialLoadCount, setInitialLoadCount] = useState(0)\n // Initialize to null to distinguish \"never set\" from \"set to empty string\"\n const lastSearchRef = useRef<string | null>(null)\n // Track if we're syncing from URL to prevent loops\n const isSyncingFromUrl = useRef(false)\n // Track if initial load is in progress to block ALL other effects\n const isInitialLoadInProgress = useRef(true)\n\n // Sync local input with URL param (for tab switches that clear params)\n // Only sync if the URL changed externally (not from our own debounce update)\n useEffect(() => {\n // Block during initial load\n if (isInitialLoadInProgress.current) return\n\n const urlSearch = params.search || ''\n // Only sync if URL differs from current input\n if (urlSearch !== searchInput) {\n isSyncingFromUrl.current = true\n setSearchInput(urlSearch)\n // Reset the flag after a tick to allow normal operation\n setTimeout(() => { isSyncingFromUrl.current = false }, 0)\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [params.search, initialLoadCount]) // Add initialLoadCount to re-run after initial load\n\n // Sync debounced search to URL, reset cursor when search changes\n useEffect(() => {\n // Block during initial load\n if (isInitialLoadInProgress.current) return\n // Skip if we're syncing from URL to prevent loops\n if (isSyncingFromUrl.current) return\n\n if (searchInput !== params.search) {\n setParams({\n search: searchInput,\n cursor: '' // Reset cursor when search changes\n })\n }\n }, [searchInput, params.search, setParams, initialLoadCount])\n\n // Initial load effect - runs once and blocks all other effects until complete\n useEffect(() => {\n if (initialLoadCount === 0) {\n const cursor = params.cursor || null\n const search = params.search || ''\n\n // Set all refs BEFORE calling onInitialLoad\n lastSearchRef.current = search\n\n // If we have a cursor in URL (page refresh), we're beyond first page\n if (cursor) {\n setHasLoadedBeyondFirst(true)\n }\n\n // Call the initial load and wait for it to complete before\n // marking initial load as done (handles async onInitialLoad)\n Promise.resolve(onInitialLoad(search, cursor)).finally(() => {\n isInitialLoadInProgress.current = false\n setInitialLoadCount(1)\n })\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [])\n\n // Search change detection - only after initial load is fully complete\n useEffect(() => {\n // Block during initial load\n if (isInitialLoadInProgress.current) return\n if (initialLoadCount === 0) return\n\n const currentSearch = params.search || ''\n // Only trigger search change if:\n // 1. lastSearchRef has been set (not null - means initial load happened)\n // 2. The search actually changed\n if (lastSearchRef.current !== null && currentSearch !== lastSearchRef.current) {\n lastSearchRef.current = currentSearch\n setHasLoadedBeyondFirst(false)\n onSearchChange(currentSearch)\n }\n }, [params.search, onSearchChange, initialLoadCount])\n\n // Pagination handlers\n const handleNextPage = useCallback(\n async (endCursor: string, fetchFn: () => Promise<unknown>) => {\n setParam('cursor', endCursor)\n await fetchFn()\n setHasLoadedBeyondFirst(true)\n },\n [setParam]\n )\n\n const handleResetToFirstPage = useCallback(\n async (fetchFn: () => Promise<unknown>) => {\n setParam('cursor', '')\n await fetchFn()\n setHasLoadedBeyondFirst(false)\n },\n [setParam]\n )\n\n return {\n searchInput,\n setSearchInput,\n hasLoadedBeyondFirst,\n setHasLoadedBeyondFirst,\n handleNextPage,\n handleResetToFirstPage,\n params,\n setParam,\n setParams,\n }\n}\n","import { useEffect, useMemo, useState } from 'react'\n\nimport type { NatsClient, NatsClientOptions, NatsStatus } from '../../nats'\nimport { createNatsClient } from '../../nats'\n\nexport interface UseNatsClientOptions {\n /**\n * When true, connect on mount and close on unmount.\n */\n autoConnect?: boolean\n}\n\nexport interface UseNatsClientResult {\n client: NatsClient | null\n status: NatsStatus\n isConnected: boolean\n lastError: unknown\n}\n\n/**\n * React hook for managing a shared NATS WebSocket connection lifecycle.\n *\n * Important: pass a memoized `clientOptions` (e.g. via `useMemo`) to avoid\n * reconnecting on every render.\n */\nexport function useNatsClient(\n clientOptions: NatsClientOptions | null,\n options: UseNatsClientOptions = {},\n): UseNatsClientResult {\n const { autoConnect = true } = options\n\n const client = useMemo(() => {\n if (!clientOptions) return null\n return createNatsClient(clientOptions)\n }, [clientOptions])\n\n const [status, setStatus] = useState<NatsStatus>('disconnected')\n const [lastError, setLastError] = useState<unknown>(null)\n\n useEffect(() => {\n if (!client) {\n setStatus('disconnected')\n setLastError(null)\n return\n }\n\n const off = client.onStatus((event) => {\n setStatus(event.status)\n if (event.status === 'error') setLastError(event.data)\n })\n\n if (autoConnect) {\n client.connect().catch((e) => {\n setLastError(e)\n setStatus('error')\n })\n }\n\n return () => {\n off()\n client.close().catch(() => {\n // ignore\n })\n }\n }, [client, autoConnect])\n\n return {\n client,\n status,\n isConnected: status === 'connected',\n lastError,\n }\n}\n","/**\n * useNearViewport — module-level shared IntersectionObserver in hook form.\n *\n * Single IO instance per `rootMargin` value, shared across every component\n * that mounts the hook. Reduces overhead vs. one IO per component on\n * grid/list pages where many subscribers observe the viewport with the same\n * margin. Promoted from the inline singleton at\n * `multi-platform-hub/components/shared/video-bites-display.tsx:21-43`,\n * which is the only IO pattern in either repo today.\n *\n * Usage:\n * ```tsx\n * function MyCard() {\n * const { ref, isNear } = useNearViewport('500px');\n * return <div ref={ref}>{isNear ? <HeavyChild /> : <Placeholder />}</div>;\n * }\n * ```\n *\n * StrictMode safety: cleanup uses an identity check on the registered\n * callback so React's dev double-mount (mount → cleanup → re-mount) does\n * not drop the second mount's freshly-set subscription. The IO callback\n * also checks `subscribers.get(target)` before invoking so a fire that\n * races with unmount cannot crash on a torn-down component.\n *\n * The hook fires once — on first intersection it sets `isNear=true` and\n * unobserves the element. Callers that need re-observation should\n * unmount and remount (or fork the hook for two-way behavior).\n */\n\nimport { useEffect, useRef, useState, useCallback } from 'react';\n\n// Per-rootMargin IO map. Multiple call sites with different margins each\n// get their own singleton observer.\nconst observers = new Map<string, IntersectionObserver>();\nconst subscribers = new WeakMap<Element, () => void>();\n\nfunction getObserverFor(rootMargin: string): IntersectionObserver {\n const existing = observers.get(rootMargin);\n if (existing) return existing;\n\n const io = new IntersectionObserver(\n (entries) => {\n entries.forEach((entry) => {\n if (!entry.isIntersecting) return;\n // Race-safe: re-read the callback at fire time. A late IO firing\n // after cleanup must not invoke a stale callback.\n const cb = subscribers.get(entry.target);\n if (cb) {\n cb();\n io.unobserve(entry.target);\n subscribers.delete(entry.target);\n }\n });\n },\n { rootMargin }\n );\n observers.set(rootMargin, io);\n return io;\n}\n\nexport interface UseNearViewportResult<T extends Element = HTMLElement> {\n /** Ref to attach to the element you want to gate on visibility. */\n ref: (node: T | null) => void;\n /** Flips to `true` once the element enters within `rootMargin` of the viewport. Never flips back. */\n isNear: boolean;\n}\n\n/**\n * @param rootMargin Margin around the viewport (CSS-style string).\n * '500px' = element starts mounting 500px before scroll-in.\n * '1000px' = a full viewport's worth of lookahead.\n * '0px' = strict on-screen detection.\n */\nexport function useNearViewport<T extends Element = HTMLElement>(\n rootMargin: string = '500px'\n): UseNearViewportResult<T> {\n const [isNear, setIsNear] = useState(false);\n const elRef = useRef<T | null>(null);\n\n // Subscribe/unsubscribe on element change.\n const ref = useCallback(\n (node: T | null) => {\n const prev = elRef.current;\n\n // Unsubscribe previous, if any. Identity-check the callback so a\n // StrictMode re-mount that has already re-registered keeps its sub.\n if (prev) {\n const stillOurs = subscribers.get(prev);\n if (stillOurs) {\n subscribers.delete(prev);\n observers.get(rootMargin)?.unobserve(prev);\n }\n }\n\n elRef.current = node;\n if (!node) return;\n\n const cb = () => setIsNear(true);\n subscribers.set(node, cb);\n getObserverFor(rootMargin).observe(node);\n },\n [rootMargin]\n );\n\n // Unsubscribe on unmount. Identity check guards the StrictMode race.\n useEffect(() => {\n return () => {\n const el = elRef.current;\n if (!el) return;\n if (subscribers.get(el)) {\n subscribers.delete(el);\n observers.get(rootMargin)?.unobserve(el);\n }\n };\n }, [rootMargin]);\n\n return { ref, isNear };\n}\n","'use client'\n\n/**\n * Access Code Integration Hook\n *\n * React-side wrapper around the pure `access-code-client` utilities.\n * Lives in `hooks/` (client bundle) so the `createContext()` call in\n * `endpoints-runtime-context` doesn't get pulled into the server-safe\n * `utils/index` bundle.\n *\n * The pure standalone functions (`validateAccessCode`,\n * `consumeAccessCode`, `validateAndConsumeAccessCode`) remain importable\n * from `@flamingo-stack/openframe-frontend-core/utils` — they take the\n * endpoints object as an argument. This hook binds those endpoints from\n * `EndpointsRuntimeContext` so React callers don't have to plumb URLs.\n */\n\nimport React from 'react'\n\nimport { useRequiredEndpointsRuntime } from '../contexts/endpoints-runtime-context'\nimport {\n validateAccessCode,\n consumeAccessCode,\n validateAndConsumeAccessCode,\n} from '../utils/access-code-client'\n\n/**\n * Resolves access-code endpoints from `EndpointsRuntimeContext` (throws\n * if no provider is mounted) and exposes loading-state-aware wrappers\n * around the standalone helpers in `utils/access-code-client`.\n *\n * @returns the following fields. The `validate` / `consume` /\n * `validateAndConsume` functions and the returned object identity are\n * NOT memoized — they're re-created each render. Wrap with\n * `useCallback` / `useMemo` at the call site if downstream effect\n * dep arrays depend on stable identities.\n * - `validate(email, code)`: validates only.\n * - `consume(email, code)`: consumes only.\n * - `validateAndConsume(email, code)`: one-step validate-then-consume.\n * - `isValidating: boolean`: a validate call is in flight.\n * - `isConsuming: boolean`: a consume call is in flight.\n * - `isProcessing: boolean`: convenience — `isValidating || isConsuming`.\n * Use this for a single \"in-flight\" indicator on UI affordances that\n * should disable during both phases.\n *\n * @example\n * const { validate, consume, isProcessing } = useAccessCodeIntegration();\n *\n * const handleRegistration = async (formData) => {\n * const validation = await validate(formData.email, formData.accessCode);\n * if (!validation.valid) {\n * setError(validation.message);\n * return;\n * }\n *\n * // Process registration...\n * const registrationResult = await registerUser(formData);\n *\n * if (registrationResult.success) {\n * await consume(formData.email, formData.accessCode);\n * }\n * };\n */\nexport function useAccessCodeIntegration() {\n const runtime = useRequiredEndpointsRuntime()\n const endpoints = runtime.accessCode\n const [isValidating, setIsValidating] = React.useState(false)\n const [isConsuming, setIsConsuming] = React.useState(false)\n\n const validate = async (email: string, code: string) => {\n setIsValidating(true)\n try {\n return await validateAccessCode(email, code, endpoints)\n } finally {\n setIsValidating(false)\n }\n }\n\n const consume = async (email: string, code: string) => {\n setIsConsuming(true)\n try {\n return await consumeAccessCode(email, code, endpoints)\n } finally {\n setIsConsuming(false)\n }\n }\n\n const validateAndConsume = async (email: string, code: string) => {\n setIsValidating(true)\n setIsConsuming(true)\n try {\n return await validateAndConsumeAccessCode(email, code, endpoints)\n } finally {\n setIsValidating(false)\n setIsConsuming(false)\n }\n }\n\n return {\n validate,\n consume,\n validateAndConsume,\n isValidating,\n isConsuming,\n isProcessing: isValidating || isConsuming,\n }\n}\n","/**\n * Access Code Client Utilities — pure standalone functions.\n *\n * Endpoint paths are NOT hardcoded — every function takes an\n * `endpoints` argument. The React-side wrapper that binds them from\n * `EndpointsRuntimeContext` lives separately at\n * `hooks/use-access-code-integration.ts` (`useAccessCodeIntegration`).\n *\n * Keep this file **free of React imports** — it lives in the\n * server-safe `utils/index` tsup bundle. Any module-top-level call\n * into `createContext()` (which the runtime context file does) would\n * be pulled into the server bundle and crash SSR with\n * `createContext is not a function`.\n */\n\nimport {\n AccessCodeValidation,\n AccessCodeValidationResponse,\n AccessCodeConsumptionResponse\n} from '../types/access-code-cohorts';\n\n/** Endpoints required by the standalone client utilities. The\n * `useAccessCodeIntegration` hook (in `hooks/`) resolves these from\n * `EndpointsRuntimeContext.accessCode` automatically. */\nexport interface AccessCodeEndpoints {\n validateUrl: string\n consumeUrl: string\n}\n\n/**\n * Validate an access code for a given email\n *\n * @param email - User's email address\n * @param code - Access code to validate\n * @returns Promise with validation result\n *\n * @example\n * const result = await validateAccessCode('user@example.com', 'ABC123XY');\n * if (result.valid) {\n * // Allow user to proceed with registration\n * console.log(`Welcome to ${result.cohort_name}!`);\n * } else {\n * // Show error message\n * console.error(result.message);\n * }\n */\nexport async function validateAccessCode(\n email: string,\n code: string,\n endpoints: AccessCodeEndpoints,\n): Promise<AccessCodeValidationResponse> {\n try {\n const response = await fetch(endpoints.validateUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ email, code } as AccessCodeValidation),\n });\n\n if (!response.ok) {\n const error = await response.json().catch(() => ({}));\n throw new Error(error.error || 'Validation request failed');\n }\n\n return await response.json() as AccessCodeValidationResponse;\n } catch (error) {\n return {\n valid: false,\n message: error instanceof Error ? error.message : 'Validation failed',\n };\n }\n}\n\n/**\n * Consume an access code after successful registration\n *\n * Call this ONLY after the user has successfully completed registration.\n * This marks the code as used and prevents further usage.\n *\n * @param email - User's email address\n * @param code - Access code to consume\n * @returns Promise with consumption result\n *\n * @example\n * // After successful registration\n * const result = await consumeAccessCode('user@example.com', 'ABC123XY');\n * if (result.consumed) {\n * console.log('Access code consumed successfully');\n * } else {\n * console.warn('Failed to consume access code:', result.message);\n * }\n */\nexport async function consumeAccessCode(\n email: string,\n code: string,\n endpoints: AccessCodeEndpoints,\n): Promise<AccessCodeConsumptionResponse> {\n try {\n const response = await fetch(endpoints.consumeUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ email, code } as AccessCodeValidation),\n });\n\n if (!response.ok) {\n const error = await response.json().catch(() => ({}));\n throw new Error(error.error || 'Consumption request failed');\n }\n\n return await response.json() as AccessCodeConsumptionResponse;\n } catch (error) {\n return {\n success: false,\n consumed: false,\n message: error instanceof Error ? error.message : 'Consumption failed',\n };\n }\n}\n\n/**\n * Complete access code flow: validate then consume\n *\n * This is a convenience function that validates an access code and,\n * if valid, immediately consumes it. Use this when you want to\n * validate and consume in one step during registration.\n *\n * @param email - User's email address\n * @param code - Access code to validate and consume\n * @returns Promise with validation and consumption results\n *\n * @example\n * const result = await validateAndConsumeAccessCode('user@example.com', 'ABC123XY');\n * if (result.valid && result.consumed) {\n * // Registration successful\n * console.log(`Welcome to ${result.cohort_name}!`);\n * } else {\n * console.error(result.message);\n * }\n */\nexport async function validateAndConsumeAccessCode(\n email: string,\n code: string,\n endpoints: AccessCodeEndpoints,\n): Promise<AccessCodeValidationResponse & { consumed?: boolean }> {\n // First validate\n const validation = await validateAccessCode(email, code, endpoints);\n\n if (!validation.valid) {\n return validation;\n }\n\n // If valid, consume the code\n const consumption = await consumeAccessCode(email, code, endpoints);\n\n return {\n ...validation,\n consumed: consumption.consumed,\n message: consumption.consumed\n ? `Access granted for ${validation.cohort_name}`\n : consumption.message || validation.message,\n };\n}\n\n// `useAccessCodeIntegration` (the React-side wrapper) lives in\n// `hooks/use-access-code-integration.ts`. It binds the endpoints from\n// `EndpointsRuntimeContext` so React callers don't have to plumb URLs.\n","'use client'\n\nimport { useMemo } from 'react'\n\n/**\n * Hook that generates a branded placeholder image URL using the\n * host's `/api/og-placeholder` server-side image generator (or any\n * builder the caller supplies).\n *\n * THIS HOOK IS A THIN CLIENT-SIDE WRAPPER. The hub-side wrapper\n * (`hooks/use-og-placeholder.ts` in multi-platform-hub) injects its\n * canonical `buildOgPlaceholderUrl` from `lib/utils/entity-image.ts`.\n * The lib version takes the builder as an argument so the lib has no\n * hub dependency.\n *\n * @param buildUrl - Function that builds the placeholder URL from a\n * title + options object (`{ site?, aspect? }`).\n * Hub passes `buildOgPlaceholderUrl` from\n * `lib/utils/entity-image.ts`. Embedded apps can\n * wire any equivalent that hits their own placeholder\n * route.\n * @param title - Text to display on the placeholder\n * @param siteName - Site name shown below the title (optional —\n * defaults to empty)\n * @param enabled - Whether to generate the URL (default: true)\n * @param aspect - `'wide'` (1200×630 social-card; default) or `'square'`\n * (1024×1024 — used by compact chat-inline card slots\n * so `object-cover` doesn't crop the title off).\n * @returns Placeholder image URL or null if disabled / no title\n */\nexport function useOgPlaceholder(\n buildUrl: (title: string, options: { site?: string; aspect?: 'wide' | 'square' }) => string,\n title: string | undefined | null,\n siteName: string = '',\n enabled: boolean = true,\n aspect: 'wide' | 'square' = 'wide',\n): string | null {\n return useMemo(() => {\n if (!enabled || !title) return null\n const options: { site?: string; aspect?: 'wide' | 'square' } = {}\n if (siteName) options.site = siteName\n if (aspect === 'square') options.aspect = 'square'\n return buildUrl(title, options)\n }, [buildUrl, title, siteName, enabled, aspect])\n}\n"]}
|