@flamingo-stack/openframe-frontend-core 0.0.481 → 0.0.482
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-H6DFCF44.cjs → chunk-22AED5IC.cjs} +82 -82
- package/dist/{chunk-H6DFCF44.cjs.map → chunk-22AED5IC.cjs.map} +1 -1
- package/dist/{chunk-MGKCS6KS.js → chunk-3WCARCI4.js} +2 -2
- package/dist/{chunk-LNKWY74S.cjs → chunk-4HL6UENH.cjs} +5 -5
- package/dist/{chunk-LNKWY74S.cjs.map → chunk-4HL6UENH.cjs.map} +1 -1
- package/dist/{chunk-Y3Y2DLNO.js → chunk-6UGYBCEN.js} +3 -3
- package/dist/{chunk-BBUEY73L.js → chunk-7I5B5PQ4.js} +6269 -5339
- package/dist/chunk-7I5B5PQ4.js.map +1 -0
- package/dist/{chunk-PSPWT2Q5.js → chunk-7LWXWXEY.js} +4 -4
- package/dist/{chunk-M7O74OWG.cjs → chunk-ACOU5YJI.cjs} +31 -31
- package/dist/{chunk-M7O74OWG.cjs.map → chunk-ACOU5YJI.cjs.map} +1 -1
- package/dist/{chunk-INDFIVDV.cjs → chunk-AHGSN4AT.cjs} +12 -12
- package/dist/{chunk-INDFIVDV.cjs.map → chunk-AHGSN4AT.cjs.map} +1 -1
- package/dist/{chunk-VTSRDC4W.js → chunk-CKC3EAYT.js} +2 -2
- package/dist/{chunk-SASR7SK5.js → chunk-GZK7FWJJ.js} +20 -4
- package/dist/chunk-GZK7FWJJ.js.map +1 -0
- package/dist/{chunk-ZRANZNEA.js → chunk-JJE5TUZK.js} +2 -2
- package/dist/{chunk-66BNAFO6.js → chunk-JQIUBFHL.js} +4 -4
- package/dist/{chunk-MMCBQJKE.cjs → chunk-KUBJHB6I.cjs} +11 -11
- package/dist/{chunk-MMCBQJKE.cjs.map → chunk-KUBJHB6I.cjs.map} +1 -1
- package/dist/{chunk-ZV6E3U3C.cjs → chunk-MRGTMMJ3.cjs} +30 -30
- package/dist/{chunk-ZV6E3U3C.cjs.map → chunk-MRGTMMJ3.cjs.map} +1 -1
- package/dist/{chunk-OZJN54R5.cjs → chunk-NWVKG7QC.cjs} +37 -37
- package/dist/{chunk-OZJN54R5.cjs.map → chunk-NWVKG7QC.cjs.map} +1 -1
- package/dist/{chunk-AN47EIIJ.cjs → chunk-PHTSSHIQ.cjs} +59 -59
- package/dist/{chunk-AN47EIIJ.cjs.map → chunk-PHTSSHIQ.cjs.map} +1 -1
- package/dist/{chunk-N2JZ33V7.cjs → chunk-RUA3OXSJ.cjs} +26 -26
- package/dist/{chunk-N2JZ33V7.cjs.map → chunk-RUA3OXSJ.cjs.map} +1 -1
- package/dist/{chunk-LK623WCC.cjs → chunk-RZPF7MJ2.cjs} +9 -9
- package/dist/{chunk-LK623WCC.cjs.map → chunk-RZPF7MJ2.cjs.map} +1 -1
- package/dist/{chunk-G3UN2DXU.cjs → chunk-STYA77PH.cjs} +20 -4
- package/dist/chunk-STYA77PH.cjs.map +1 -0
- package/dist/{chunk-JUJY3HQS.cjs → chunk-U37UTLP7.cjs} +19 -19
- package/dist/{chunk-JUJY3HQS.cjs.map → chunk-U37UTLP7.cjs.map} +1 -1
- package/dist/{chunk-2DIOV7BR.js → chunk-UPN6EFOM.js} +4 -4
- package/dist/{chunk-OWMLWJQY.cjs → chunk-VPVOJD5X.cjs} +2432 -1502
- package/dist/chunk-VPVOJD5X.cjs.map +1 -0
- package/dist/{chunk-4SMRQ4SN.js → chunk-WBL5Y4PS.js} +3 -3
- package/dist/{chunk-FP4H6Y5O.js → chunk-WPYPSIY2.js} +3 -3
- package/dist/{chunk-R5WJJL4V.js → chunk-X5X43HFV.js} +5 -5
- package/dist/{chunk-DEHZ5CTC.js → chunk-XTWKDMCG.js} +2 -2
- package/dist/components/case-studies/index.cjs +9 -9
- package/dist/components/case-studies/index.js +3 -3
- package/dist/components/chat/index.cjs +3 -3
- package/dist/components/chat/index.js +2 -2
- package/dist/components/contact/index.cjs +4 -4
- package/dist/components/contact/index.js +3 -3
- package/dist/components/docs/index.cjs +6 -6
- package/dist/components/docs/index.js +5 -5
- package/dist/components/embeds/index.cjs +4 -4
- package/dist/components/embeds/index.js +3 -3
- package/dist/components/faq/index.cjs +4 -4
- package/dist/components/faq/index.js +3 -3
- package/dist/components/features/captions-url.cjs +12 -0
- package/dist/components/features/captions-url.cjs.map +1 -0
- package/dist/components/features/captions-url.js +10 -0
- package/dist/components/features/captions-url.js.map +1 -0
- package/dist/components/features/card-hit-layer.d.ts +40 -0
- package/dist/components/features/card-hit-layer.d.ts.map +1 -0
- package/dist/components/features/floating-walkthrough-video.d.ts +71 -0
- package/dist/components/features/floating-walkthrough-video.d.ts.map +1 -0
- package/dist/components/features/index.cjs +11 -3
- package/dist/components/features/index.cjs.map +1 -1
- package/dist/components/features/index.d.ts +4 -0
- package/dist/components/features/index.d.ts.map +1 -1
- package/dist/components/features/index.js +12 -4
- package/dist/components/features/use-walkthrough-video.d.ts +16 -0
- package/dist/components/features/use-walkthrough-video.d.ts.map +1 -0
- package/dist/components/features/video-bites-strip.d.ts.map +1 -1
- package/dist/components/features/video-center-badge.d.ts.map +1 -1
- package/dist/components/features/video-hover-preview.d.ts +68 -0
- package/dist/components/features/video-hover-preview.d.ts.map +1 -0
- package/dist/components/features/video.d.ts +78 -40
- package/dist/components/features/video.d.ts.map +1 -1
- package/dist/components/form.d.ts +0 -1
- package/dist/components/form.d.ts.map +1 -1
- package/dist/components/help-center-pages/index.cjs +21 -21
- package/dist/components/help-center-pages/index.js +12 -12
- package/dist/components/index.cjs +163 -155
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +18 -10
- package/dist/components/index.js.map +1 -1
- package/dist/components/navigation/index.cjs +3 -3
- package/dist/components/navigation/index.js +2 -2
- package/dist/components/onboarding-guides/index.cjs +6 -6
- package/dist/components/onboarding-guides/index.js +5 -5
- package/dist/components/related-content/index.cjs +4 -4
- package/dist/components/related-content/index.js +3 -3
- package/dist/components/tickets/index.cjs +6 -6
- package/dist/components/tickets/index.js +5 -5
- package/dist/components/ui/button/button-styles.d.ts +2 -0
- package/dist/components/ui/button/button-styles.d.ts.map +1 -1
- package/dist/components/ui/button/button.d.ts +2 -2
- package/dist/components/ui/button/button.d.ts.map +1 -1
- package/dist/components/ui/file-manager/index.cjs +50 -50
- package/dist/components/ui/file-manager/index.js +1 -1
- package/dist/components/ui/index.cjs +3 -3
- package/dist/components/ui/index.js +2 -2
- package/dist/components/ui/page-actions.d.ts +2 -1
- package/dist/components/ui/page-actions.d.ts.map +1 -1
- package/dist/index.cjs +27 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +28 -4
- package/dist/types/supabase.d.ts +80 -0
- package/dist/types/supabase.d.ts.map +1 -1
- package/dist/types/video-processing.d.ts +1 -1
- package/dist/types/video-processing.d.ts.map +1 -1
- package/dist/utils/announcement-storage.d.ts +1 -1
- package/dist/utils/announcement-storage.d.ts.map +1 -1
- package/dist/utils/dismiss-cookie.d.ts +27 -0
- package/dist/utils/dismiss-cookie.d.ts.map +1 -0
- package/dist/utils/dismissal-storage.d.ts +19 -0
- package/dist/utils/dismissal-storage.d.ts.map +1 -0
- package/dist/utils/index.cjs +48 -10
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +41 -11
- package/dist/utils/index.js.map +1 -1
- package/package.json +8 -2
- package/src/.index.md +39 -32
- package/src/.platform-domains.md +65 -0
- package/src/assets/.fae-avatar.md +33 -0
- package/src/assets/.index.md +15 -9
- package/src/components/.agent-mark.md +31 -0
- package/src/components/.announcement-bar.md +47 -35
- package/src/components/.calendar.md +19 -15
- package/src/components/.categories-cart.md +34 -30
- package/src/components/.category-card.md +33 -33
- package/src/components/.chart.md +38 -40
- package/src/components/.command.md +47 -23
- package/src/components/.comment-card.md +31 -26
- package/src/components/.content-loading-container.md +45 -20
- package/src/components/.context-menu.md +45 -31
- package/src/components/.date-time-picker.md +36 -30
- package/src/components/.empty-state.md +31 -31
- package/src/components/.faq-accordion.md +20 -30
- package/src/components/.filter-chip.md +36 -33
- package/src/components/.footer-waitlist-button.md +27 -21
- package/src/components/.footer.md +39 -24
- package/src/components/.form.md +32 -20
- package/src/components/.hero-image-uploader.md +35 -31
- package/src/components/.icon-display.md +53 -0
- package/src/components/.image-cropper.md +34 -33
- package/src/components/.index.md +25 -29
- package/src/components/.logs-list.md +27 -23
- package/src/components/.media-carousel.md +47 -26
- package/src/components/.metric-value.md +27 -24
- package/src/components/.msp-display.md +29 -23
- package/src/components/.open-source-features.md +23 -26
- package/src/components/.pagination.md +44 -39
- package/src/components/.persistent-filter-controls.md +61 -28
- package/src/components/.pricing-display.md +34 -33
- package/src/components/.results-count.md +33 -20
- package/src/components/.smooth-accordion.md +24 -20
- package/src/components/.social-icon-row.md +33 -21
- package/src/components/.sonner.md +40 -25
- package/src/components/.steps.md +39 -25
- package/src/components/.tool-icon.md +34 -21
- package/src/components/.unified-filter-logic.md +32 -36
- package/src/components/.user-display.md +33 -28
- package/src/components/.user-summary-stub.md +40 -33
- package/src/components/.vendor-display-button.md +17 -15
- package/src/components/.vendor-icon.md +17 -25
- package/src/components/.vendor-tag.md +39 -34
- package/src/components/.why-it-matters.md +15 -11
- package/src/components/.yes-no-display.md +36 -31
- package/src/components/authors/.author-detail-view.md +50 -0
- package/src/components/case-studies/.index.md +26 -0
- package/src/components/case-studies/.share-experience-section.md +45 -0
- package/src/components/chat/.approval-batch-message.md +48 -33
- package/src/components/chat/.approval-request-message.md +37 -26
- package/src/components/chat/.chat-archive-page.md +56 -0
- package/src/components/chat/.chat-attachment-bar.md +32 -40
- package/src/components/chat/.chat-composer.md +43 -0
- package/src/components/chat/.chat-container.md +41 -25
- package/src/components/chat/.chat-context-memory-bar.md +53 -0
- package/src/components/chat/.chat-context-picker.md +89 -0
- package/src/components/chat/.chat-header-icon-button.md +43 -0
- package/src/components/chat/.chat-header-search-field.md +51 -0
- package/src/components/chat/.chat-input.md +63 -30
- package/src/components/chat/.chat-list-empty-state.md +44 -0
- package/src/components/chat/.chat-message-enhanced.md +29 -31
- package/src/components/chat/.chat-message-list.md +34 -44
- package/src/components/chat/.chat-message-row.md +56 -0
- package/src/components/chat/.chat-message-skeleton.md +32 -26
- package/src/components/chat/.chat-panel-header-mobile.md +46 -0
- package/src/components/chat/.chat-panel-header.md +80 -0
- package/src/components/chat/.chat-sidebar-skeleton.md +31 -30
- package/src/components/chat/.chat-sidebar.md +36 -29
- package/src/components/chat/.chat-ticket-list.md +38 -23
- package/src/components/chat/.chat-typing-indicator.md +37 -19
- package/src/components/chat/.context-compaction-display.md +19 -15
- package/src/components/chat/.context-items-list.md +57 -0
- package/src/components/chat/.embeddable-chat.md +57 -66
- package/src/components/chat/.error-message-display.md +40 -22
- package/src/components/chat/.guide-mode-banner.md +41 -0
- package/src/components/chat/.guide-welcome.md +43 -0
- package/src/components/chat/.index.md +26 -21
- package/src/components/chat/.mingo-chat-history.md +63 -0
- package/src/components/chat/.mingo-chat-modals.md +44 -0
- package/src/components/chat/.mingo-history-rail.md +63 -0
- package/src/components/chat/.mingo-info-card.md +59 -0
- package/src/components/chat/.mingo-onboarding-card.md +21 -27
- package/src/components/chat/.mingo-welcome.md +60 -0
- package/src/components/chat/.model-display.md +33 -25
- package/src/components/chat/.msp-organization-card-skeleton.md +44 -0
- package/src/components/chat/.msp-organization-card.md +51 -0
- package/src/components/chat/.nav-link-anchor-via-runtime.md +36 -25
- package/src/components/chat/.quick-action-chip.md +60 -0
- package/src/components/chat/.quick-action-marquee.md +53 -0
- package/src/components/chat/.quick-action-wall.md +68 -0
- package/src/components/chat/.remark-mention-chips.md +41 -0
- package/src/components/chat/.slash-command-suggestions.md +30 -36
- package/src/components/chat/.source-action-button.md +28 -25
- package/src/components/chat/.tool-call-blocks.md +18 -30
- package/src/components/chat/.tool-execution-display.md +29 -34
- package/src/components/chat/entity-cards/.admin-content-card.md +29 -22
- package/src/components/chat/entity-cards/.blog-card.md +44 -33
- package/src/components/chat/entity-cards/.blog-image-placeholder.md +20 -18
- package/src/components/chat/entity-cards/.case-study-card.md +24 -33
- package/src/components/chat/entity-cards/.chat-ticket-item.md +39 -22
- package/src/components/chat/entity-cards/.customer-interview-card.md +29 -32
- package/src/components/chat/entity-cards/.data-room-doc-card.md +21 -22
- package/src/components/chat/entity-cards/.dispatch.md +41 -24
- package/src/components/chat/entity-cards/.entity-author-card.md +36 -33
- package/src/components/chat/entity-cards/.entity-portrait-card.md +47 -0
- package/src/components/chat/entity-cards/.generic-entity-card.md +26 -25
- package/src/components/chat/entity-cards/.github-activity-card.md +35 -41
- package/src/components/chat/entity-cards/.hubspot-ticket-card.md +22 -28
- package/src/components/chat/entity-cards/.index.md +33 -44
- package/src/components/chat/entity-cards/.investor-update-card.md +36 -25
- package/src/components/chat/entity-cards/.onboarding-guide-card.md +20 -29
- package/src/components/chat/entity-cards/.product-release-card-defaults.md +26 -19
- package/src/components/chat/entity-cards/.program-card.md +30 -33
- package/src/components/chat/entity-cards/.roadmap-card.md +40 -31
- package/src/components/chat/entity-cards/.roadmap-vote-button.md +33 -36
- package/src/components/chat/entity-cards/.slack-message-card.md +31 -32
- package/src/components/chat/entity-cards/.use-cover-image-fallback.md +47 -0
- package/src/components/chat/entity-cards/.use-entity-card-link.md +51 -0
- package/src/components/chat/entity-cards/.use-entity-card-placeholder.md +47 -0
- package/src/components/chat/entity-cards/.what-i-shipped-card.md +43 -0
- package/src/components/chat/hooks/.index.md +33 -30
- package/src/components/chat/hooks/.use-chat-card-item.md +30 -17
- package/src/components/chat/hooks/.use-chat-dialog-manager.md +59 -0
- package/src/components/chat/hooks/.use-chat-identity.md +37 -25
- package/src/components/chat/hooks/.use-chunk-catchup.md +53 -36
- package/src/components/chat/hooks/.use-empty-state-config.md +39 -0
- package/src/components/chat/hooks/.use-jetstream-dialog-subscription.md +65 -30
- package/src/components/chat/hooks/.use-nats-chat-adapter.md +55 -36
- package/src/components/chat/hooks/.use-nats-dialog-subscription.md +66 -50
- package/src/components/chat/hooks/.use-realtime-chunk-processor.md +58 -31
- package/src/components/chat/hooks/.use-scripted-stream.md +46 -0
- package/src/components/chat/hooks/.use-slash-commands.md +32 -25
- package/src/components/chat/hooks/.use-sse-chat-adapter.md +32 -21
- package/src/components/chat/hooks/.use-unified-chat.md +45 -35
- package/src/components/chat/types/.api.types.md +49 -41
- package/src/components/chat/types/.chat.types.md +56 -34
- package/src/components/chat/types/.component.types.md +36 -36
- package/src/components/chat/types/.context-item.types.md +61 -0
- package/src/components/chat/types/.index.md +32 -17
- package/src/components/chat/types/.message.types.md +44 -45
- package/src/components/chat/types/.network.types.md +46 -46
- package/src/components/chat/types/.processing.types.md +49 -37
- package/src/components/chat/types/.unified-chat-state.types.md +41 -26
- package/src/components/chat/types/entities/.content-ref.md +20 -31
- package/src/components/chat/types/entities/.investor-update.md +14 -16
- package/src/components/chat/types/entities/.onboarding-guide.md +35 -34
- package/src/components/chat/types/entities/.program-types.md +55 -43
- package/src/components/chat/utils/.chip-action-class.md +17 -19
- package/src/components/chat/utils/.chip-styles.md +18 -18
- package/src/components/chat/utils/.chunk-parser.md +48 -37
- package/src/components/chat/utils/.compact-card-classes.md +33 -35
- package/src/components/chat/utils/.execute-navigation.md +49 -0
- package/src/components/chat/utils/.extract-incomplete-message-state.md +9 -12
- package/src/components/chat/utils/.history-merge.md +65 -0
- package/src/components/chat/utils/.icon-library.md +33 -31
- package/src/components/chat/utils/.icon-registry.md +44 -0
- package/src/components/chat/utils/.index.md +37 -33
- package/src/components/chat/utils/.is-cross-origin-url.md +26 -22
- package/src/components/chat/utils/.message-segment-accumulator.md +46 -25
- package/src/components/chat/utils/.nav-anchor-props.md +29 -34
- package/src/components/chat/utils/.nav-click-handler.md +29 -33
- package/src/components/chat/utils/.process-historical-messages.md +29 -33
- package/src/components/chat/utils/.scripted-stream.md +52 -0
- package/src/components/chat/utils/.source-row-cta.md +32 -31
- package/src/components/chat/utils/.tool-call-helpers.md +25 -17
- package/src/components/contact/.contact-form.md +24 -19
- package/src/components/docs/.doc-navigation-context.md +49 -0
- package/src/components/docs/.doc-viewer.md +64 -0
- package/src/components/docs/.docs-hub-page.md +49 -0
- package/src/components/docs/.index.md +51 -0
- package/src/components/docs/.skeletons.md +47 -0
- package/src/components/docs/.use-docs-resolve-link.md +61 -0
- package/src/components/docs/.use-document-tree.md +83 -0
- package/src/components/docs/.use-scroll-spy.md +58 -0
- package/src/components/embeds/.embed-container.md +45 -0
- package/src/components/embeds/.embed-iframe.md +51 -0
- package/src/components/embeds/.figma-embed.md +43 -0
- package/src/components/embeds/.file-download-card.md +47 -0
- package/src/components/embeds/.google-sheets-viewer.md +40 -0
- package/src/components/embeds/.index.md +58 -0
- package/src/components/embeds/.linkedin-embed-client.md +50 -0
- package/src/components/embeds/.markdown-image.md +40 -0
- package/src/components/embeds/.og-link-preview.md +54 -0
- package/src/components/embeds/.pdf-viewer.md +43 -0
- package/src/components/embeds/.reddit-embed-client.md +40 -0
- package/src/components/embeds/.rich-markdown-runtime.md +53 -0
- package/src/components/embeds/.twitter-embed-client.md +44 -0
- package/src/components/faq/.faq-document-page.md +68 -0
- package/src/components/faq/.faq-section.md +55 -0
- package/src/components/faq/.index.md +29 -0
- package/src/components/faq/.json-ld.md +48 -0
- package/src/components/features/.array-entry-manager.md +32 -24
- package/src/components/features/.auth-providers-list.md +33 -32
- package/src/components/features/.captions-url.md +43 -0
- package/src/components/features/.cards-strip.md +48 -0
- package/src/components/features/.changelog-manager.md +17 -20
- package/src/components/features/.command-box.md +11 -16
- package/src/components/features/.entity-summary-editor.md +20 -15
- package/src/components/features/.entity-tag-badges.md +43 -0
- package/src/components/features/.entity-video-section.md +31 -41
- package/src/components/features/.figma-prototype-viewer.md +55 -37
- package/src/components/features/.filters-dropdown.md +14 -13
- package/src/components/features/.highlight-config-section.md +19 -16
- package/src/components/features/.highlight-video-combined-section.md +28 -19
- package/src/components/features/.highlight-video-preview.md +13 -11
- package/src/components/features/.highlight-video-section.md +26 -22
- package/src/components/features/.index.md +40 -36
- package/src/components/features/.media-gallery-manager.md +37 -35
- package/src/components/features/.mux-origins.md +46 -0
- package/src/components/features/.og-editor-preview.md +29 -24
- package/src/components/features/.os-type-badge-group.md +26 -18
- package/src/components/features/.os-type-badge.md +41 -24
- package/src/components/features/.paths-display.md +42 -25
- package/src/components/features/.platform-badge.md +26 -17
- package/src/components/features/.policy-configuration-panel.md +33 -31
- package/src/components/features/.provider-button.md +29 -20
- package/src/components/features/.push-button-selector.md +39 -41
- package/src/components/features/.release-media-manager.md +41 -34
- package/src/components/features/.section-selector.md +27 -16
- package/src/components/features/.select-button.md +21 -32
- package/src/components/features/.seo-editor-preview.md +49 -36
- package/src/components/features/.social-links-manager.md +40 -18
- package/src/components/features/.start-with-openframe-button.md +30 -32
- package/src/components/features/.status-filter-component.md +29 -38
- package/src/components/features/.tags-selector.md +25 -29
- package/src/components/features/.transcript-summary-editor.md +24 -18
- package/src/components/features/.use-video-warmup.md +47 -0
- package/src/components/features/.video-bites-display.md +36 -37
- package/src/components/features/.video-bites-shared.md +44 -0
- package/src/components/features/.video-bites-strip.md +63 -0
- package/src/components/features/.video-center-badge.md +38 -0
- package/src/components/features/.video-ratio-tabs.md +38 -31
- package/src/components/features/.video-source-selector.md +24 -27
- package/src/components/features/.video.md +35 -27
- package/src/components/features/.waitlist-form.md +39 -33
- package/src/components/features/.warning-block.md +53 -0
- package/src/components/features/ai-enrich/.AIEnrichSection.md +24 -25
- package/src/components/features/ai-enrich/.AIRequiredBadge.md +22 -21
- package/src/components/features/ai-enrich/.AIStatusIndicator.md +42 -35
- package/src/components/features/ai-enrich/.AIWarningsSection.md +23 -20
- package/src/components/features/ai-enrich/.ConfidenceBadge.md +35 -20
- package/src/components/features/auth/.auth-benefits-panel.md +48 -0
- package/src/components/features/auth/.auth-branding.md +51 -0
- package/src/components/features/auth/.auth-shell.md +51 -0
- package/src/components/features/auth/.back-to-login-link.md +48 -0
- package/src/components/features/auth/.complete-account-form.md +45 -0
- package/src/components/features/auth/.create-organization-form.md +53 -0
- package/src/components/features/auth/.index.md +48 -0
- package/src/components/features/auth/.invite-link-invalid-modal.md +42 -0
- package/src/components/features/auth/.login-form.md +42 -0
- package/src/components/features/auth/.openframe-sso-login-form.md +46 -0
- package/src/components/features/auth/.openframe-sso-signup-form.md +51 -0
- package/src/components/features/auth/.password-reset-form.md +57 -0
- package/src/components/features/auth/.sso-auth-shell.md +35 -0
- package/src/components/features/auth/.sso-providers.md +42 -0
- package/src/components/features/auth/.terms-agreement-label.md +28 -0
- package/src/components/features/board/.board-column-header.md +24 -18
- package/src/components/features/board/.board-column.md +32 -32
- package/src/components/features/board/.board-ticket-approval.md +42 -0
- package/src/components/features/board/.board.md +38 -38
- package/src/components/features/board/.index.md +35 -38
- package/src/components/features/board/.ticket-card.md +33 -36
- package/src/components/features/board/.types.md +38 -31
- package/src/components/features/card-hit-layer.tsx +84 -0
- package/src/components/features/floating-walkthrough-video.tsx +891 -0
- package/src/components/features/index.ts +4 -0
- package/src/components/features/notifications/.approval-request-notification-tile.md +45 -0
- package/src/components/features/notifications/.index.md +35 -31
- package/src/components/features/notifications/.notification-drawer.md +32 -19
- package/src/components/features/notifications/.notification-popups.md +51 -0
- package/src/components/features/notifications/.notification-tile.md +43 -27
- package/src/components/features/notifications/.notifications-context.md +42 -45
- package/src/components/features/notifications/.types.md +51 -31
- package/src/components/features/ticket-status-config-list/.ticket-status-config-list.md +51 -0
- package/src/components/features/time-tracker/.index.md +48 -0
- package/src/components/features/time-tracker/.time-tracker-context.md +55 -0
- package/src/components/features/time-tracker/.time-tracker-header-button.md +51 -0
- package/src/components/features/time-tracker/.time-tracker-panel.md +58 -0
- package/src/components/features/time-tracker/.types.md +51 -0
- package/src/components/features/time-tracker/.use-tracker-clock.md +32 -0
- package/src/components/features/use-walkthrough-video.ts +70 -0
- package/src/components/features/video-bites-strip.tsx +46 -146
- package/src/components/features/video-center-badge.tsx +11 -2
- package/src/components/features/video-hover-preview.tsx +201 -0
- package/src/components/features/video.tsx +368 -73
- package/src/components/help-center-pages/.delivery-page.md +51 -0
- package/src/components/help-center-pages/.index.md +51 -0
- package/src/components/help-center-pages/.onboarding-guides-catalog-page.md +53 -0
- package/src/components/help-center-pages/.product-releases-list-page.md +51 -0
- package/src/components/help-center-pages/.roadmap-page.md +46 -0
- package/src/components/icons/.figma-icon.md +13 -7
- package/src/components/icons/.index.md +26 -22
- package/src/components/icons/.vendor-directory-icon.md +30 -18
- package/src/components/icons/.x-logo.md +26 -10
- package/src/components/icons-v2-generated/.index.md +30 -41
- package/src/components/icons-v2-generated/brand-logos/.index.md +25 -19
- package/src/components/icons-v2-generated/communication/.index.md +26 -30
- package/src/components/icons-v2-generated/logos/.flamingo-logo-icon.md +41 -0
- package/src/components/layout/.article-detail-layout.md +32 -21
- package/src/components/layout/.page-container.md +25 -23
- package/src/components/layout/.page-heading.md +48 -0
- package/src/components/layout/.page-layout.md +30 -25
- package/src/components/layout/.title-block.md +43 -24
- package/src/components/loading/.card-skeleton.md +28 -35
- package/src/components/loading/.category-card-skeleton.md +25 -23
- package/src/components/loading/.device-card-skeleton.md +22 -15
- package/src/components/loading/.margin-report-skeleton.md +30 -35
- package/src/components/loading/.openmsp-heartbeat.md +30 -32
- package/src/components/loading/.organization-card-skeleton.md +22 -25
- package/src/components/loading/.organization-icon-skeleton.md +39 -27
- package/src/components/loading/.page-layout-skeleton.md +50 -28
- package/src/components/loading/.unified-skeleton.md +37 -40
- package/src/components/loading/.users-grid-skeleton.md +19 -38
- package/src/components/navigation/.app-header.md +35 -28
- package/src/components/navigation/.app-layout-context.md +48 -0
- package/src/components/navigation/.app-layout-drawer.md +56 -0
- package/src/components/navigation/.app-layout.md +24 -23
- package/src/components/navigation/.header-mingo-button.md +46 -0
- package/src/components/navigation/.header-organization-filter.md +32 -18
- package/src/components/navigation/.header.md +24 -31
- package/src/components/navigation/.index.md +36 -50
- package/src/components/navigation/.mingo-ai-button.md +41 -0
- package/src/components/navigation/.mobile-burger-menu.md +29 -29
- package/src/components/navigation/.mobile-nav-panel.md +41 -49
- package/src/components/navigation/.multi-level-navigation.md +76 -0
- package/src/components/navigation/.navigation-sidebar-item.md +14 -14
- package/src/components/navigation/.navigation-sidebar.md +30 -23
- package/src/components/navigation/.sliding-sidebar.md +30 -25
- package/src/components/navigation/.sticky-section-nav.md +23 -21
- package/src/components/onboarding-guides/.index.md +40 -0
- package/src/components/onboarding-guides/.onboarding-guide-detail-view.md +55 -0
- package/src/components/onboarding-guides/.onboarding-guides-catalog-skeleton.md +33 -0
- package/src/components/onboarding-guides/.onboarding-guides-catalog-view.md +50 -0
- package/src/components/onboarding-guides/hooks/.use-onboarding-guides.md +54 -0
- package/src/components/platform/.CveLink.md +28 -17
- package/src/components/platform/.ScriptArguments.md +18 -23
- package/src/components/platform/.ScriptInfoSection.md +27 -21
- package/src/components/platform/.ShellTypeBadge.md +34 -18
- package/src/components/platform/.SoftwareInfo.md +23 -21
- package/src/components/platform/.ToolBadge.md +18 -16
- package/src/components/profile/.ProfileLoadingSkeleton.md +24 -23
- package/src/components/related-content/.index.md +41 -0
- package/src/components/related-content/.related-content-section.md +62 -0
- package/src/components/shared/.article-author-byline.md +56 -0
- package/src/components/shared/.detail-page-skeleton.md +32 -43
- package/src/components/shared/.media-gallery-strip.md +43 -0
- package/src/components/shared/delivery/.delivery-lists.md +20 -19
- package/src/components/shared/delivery/.delivery-row.md +24 -20
- package/src/components/shared/delivery/.delivery-table.md +20 -22
- package/src/components/shared/dev-section/.dev-card-row.md +28 -36
- package/src/components/shared/dev-section/.dev-section-page.md +36 -25
- package/src/components/shared/dev-section/.dev-section-view.md +32 -22
- package/src/components/shared/doc-search/.doc-search-bar.md +58 -0
- package/src/components/shared/doc-search/.doc-search-result-row.md +50 -0
- package/src/components/shared/doc-search/.format-relative-path.md +30 -0
- package/src/components/shared/doc-search/.index.md +46 -0
- package/src/components/shared/doc-search/.map-doc-search-results.md +44 -0
- package/src/components/shared/doc-search/.resolve-search-result-action.md +52 -0
- package/src/components/shared/doc-search/.types.md +49 -0
- package/src/components/shared/doc-search/.use-doc-search.md +66 -0
- package/src/components/shared/legal-document/.legal-document-page.md +26 -20
- package/src/components/shared/legal-document/.use-legal-docs.md +36 -30
- package/src/components/shared/onboarding/.onboarding-step-card.md +24 -16
- package/src/components/shared/onboarding/.onboarding-walkthrough.md +42 -28
- package/src/components/shared/product-release/.index.md +33 -24
- package/src/components/shared/product-release/.product-release-card-skeleton.md +22 -28
- package/src/components/shared/product-release/.product-release-card.md +28 -29
- package/src/components/shared/product-release/.product-releases-view.md +49 -0
- package/src/components/shared/product-release/.release-detail-page.md +30 -15
- package/src/components/shared/roadmap/.index.md +29 -24
- package/src/components/shared/roadmap/.roadmap-grid.md +43 -54
- package/src/components/shared/roadmap/.roadmap-view.md +41 -0
- package/src/components/shared/roadmap/.use-roadmap-voting.md +27 -17
- package/src/components/switch.tsx +1 -1
- package/src/components/tickets/.help-center-card.md +32 -28
- package/src/components/tickets/.help-center-create-form.md +34 -22
- package/src/components/tickets/.help-center-list.md +36 -25
- package/src/components/tickets/.index.md +30 -45
- package/src/components/tickets/.ticket-center.md +31 -38
- package/src/components/tickets/.ticket-detail-drawer.md +20 -30
- package/src/components/tickets/.ticket-open-form.md +24 -26
- package/src/components/tickets/.ticket-reply-composer.md +51 -0
- package/src/components/tickets/.ticket-row.md +26 -11
- package/src/components/tickets/.types.md +56 -38
- package/src/components/tickets/hooks/.use-ticket-actions.md +60 -47
- package/src/components/tickets/hooks/.use-ticket-engagements.md +27 -27
- package/src/components/tickets/hooks/.use-tickets-list.md +24 -30
- package/src/components/ui/.accordion.md +28 -21
- package/src/components/ui/.actions-menu.md +36 -54
- package/src/components/ui/.ai-generated-badge.md +25 -0
- package/src/components/ui/.alert-dialog.md +21 -16
- package/src/components/ui/.alert.md +30 -25
- package/src/components/ui/.allowed-domains-input.md +26 -15
- package/src/components/ui/.announcement-bar-view.md +46 -0
- package/src/components/ui/.autocomplete.md +40 -27
- package/src/components/ui/.badge.md +34 -31
- package/src/components/ui/.brand-association-card.md +22 -18
- package/src/components/ui/.breadcrumb.md +18 -14
- package/src/components/ui/.bullet-list.md +35 -23
- package/src/components/ui/.card.md +47 -30
- package/src/components/ui/.checkbox-block.md +34 -22
- package/src/components/ui/.checkbox-with-description.md +18 -20
- package/src/components/ui/.chevron-button.md +29 -29
- package/src/components/ui/.circular-progress.md +30 -41
- package/src/components/ui/.color-preset-select.md +54 -0
- package/src/components/ui/.color-swatch.md +37 -0
- package/src/components/ui/.content-loader.md +34 -27
- package/src/components/ui/.crossfade-panels.md +50 -0
- package/src/components/ui/.cursor-pagination.md +38 -26
- package/src/components/ui/.dashboard-info-card.md +46 -27
- package/src/components/ui/.date-picker.md +52 -36
- package/src/components/ui/.device-card-compact.md +23 -20
- package/src/components/ui/.device-card.md +39 -30
- package/src/components/ui/.dialog.md +14 -14
- package/src/components/ui/.drawer.md +45 -39
- package/src/components/ui/.dropdown-button.md +41 -33
- package/src/components/ui/.dropdown-menu.md +40 -23
- package/src/components/ui/.duration-input.md +41 -0
- package/src/components/ui/.entity-image.md +32 -28
- package/src/components/ui/.error-state.md +30 -26
- package/src/components/ui/.fade-preview.md +44 -0
- package/src/components/ui/.field-wrapper.md +34 -23
- package/src/components/ui/.file-upload.md +30 -18
- package/src/components/ui/.filter-modal.md +30 -32
- package/src/components/ui/.filter-pill-row.md +58 -0
- package/src/components/ui/.floating-tooltip.md +27 -32
- package/src/components/ui/.hidden-tags-popup.md +18 -20
- package/src/components/ui/.highlight-card.md +36 -23
- package/src/components/ui/.honeypot-field.md +41 -0
- package/src/components/ui/.hover-dropdown.md +44 -31
- package/src/components/ui/.image-gallery-modal.md +28 -30
- package/src/components/ui/.image-uploader.md +39 -22
- package/src/components/ui/.index.md +33 -31
- package/src/components/ui/.info-card-row.md +50 -0
- package/src/components/ui/.info-card.md +27 -20
- package/src/components/ui/.info-row.md +30 -24
- package/src/components/ui/.info-section.md +63 -0
- package/src/components/ui/.input.md +29 -28
- package/src/components/ui/.label.md +34 -26
- package/src/components/ui/.markdown-editor.md +26 -25
- package/src/components/ui/.marquee-wall.md +64 -0
- package/src/components/ui/.menubar.md +50 -16
- package/src/components/ui/.modal-v2.md +41 -27
- package/src/components/ui/.modal.md +22 -19
- package/src/components/ui/.more-actions-menu.md +30 -24
- package/src/components/ui/.navigation-menu.md +40 -25
- package/src/components/ui/.organization-card.md +46 -27
- package/src/components/ui/.page-actions.md +55 -41
- package/src/components/ui/.page-loader.md +40 -23
- package/src/components/ui/.password-input.md +30 -0
- package/src/components/ui/.phone-input.md +20 -22
- package/src/components/ui/.portal-container.md +43 -0
- package/src/components/ui/.progress-bar.md +40 -27
- package/src/components/ui/.release-changelog-section.md +32 -26
- package/src/components/ui/.rich-markdown-renderer.md +45 -0
- package/src/components/ui/.scroll-fade.md +56 -0
- package/src/components/ui/.search-input.md +55 -37
- package/src/components/ui/.select.md +25 -26
- package/src/components/ui/.service-card.md +25 -19
- package/src/components/ui/.sheet.md +21 -21
- package/src/components/ui/.simple-markdown-renderer.md +36 -24
- package/src/components/ui/.slack-channel-chip.md +49 -0
- package/src/components/ui/.square-avatar.md +29 -26
- package/src/components/ui/.stacked-rows-panel.md +61 -0
- package/src/components/ui/.status-badge.md +30 -24
- package/src/components/ui/.status-indicator.md +35 -21
- package/src/components/ui/.tab-content.md +34 -22
- package/src/components/ui/.tab-navigation.md +40 -50
- package/src/components/ui/.tab-selector.md +48 -31
- package/src/components/ui/.tabs.md +34 -21
- package/src/components/ui/.tag-search-input.md +20 -20
- package/src/components/ui/.tag-select-dropdown.md +59 -0
- package/src/components/ui/.tag.md +25 -43
- package/src/components/ui/.tags-input.md +31 -31
- package/src/components/ui/.textarea.md +32 -30
- package/src/components/ui/.ticket-attachments-list.md +36 -29
- package/src/components/ui/.ticket-info-section.md +37 -42
- package/src/components/ui/.ticket-status-config-row.md +66 -0
- package/src/components/ui/.ticket-status-tag.md +39 -35
- package/src/components/ui/.toaster.md +21 -15
- package/src/components/ui/.toggle.md +38 -37
- package/src/components/ui/.tooltip.md +16 -13
- package/src/components/ui/button/.button-styles.md +17 -18
- package/src/components/ui/button/.button.md +30 -37
- package/src/components/ui/button/.split-button.md +46 -23
- package/src/components/ui/button/button-styles.ts +13 -0
- package/src/components/ui/button/button.tsx +5 -0
- package/src/components/ui/data-table/.data-table-body.md +50 -35
- package/src/components/ui/data-table/.data-table-empty.md +17 -33
- package/src/components/ui/data-table/.data-table-header.md +43 -40
- package/src/components/ui/data-table/.data-table-row.md +31 -31
- package/src/components/ui/data-table/.types.md +23 -28
- package/src/components/ui/file-manager/.file-manager-breadcrumb.md +16 -24
- package/src/components/ui/file-manager/.file-manager-empty.md +23 -15
- package/src/components/ui/file-manager/.file-manager-skeleton.md +34 -36
- package/src/components/ui/file-manager/.file-manager-table-row.md +29 -29
- package/src/components/ui/file-manager/.file-manager-table.md +39 -24
- package/src/components/ui/no-data/.index.md +22 -0
- package/src/components/ui/no-data/.no-data-actions.md +29 -0
- package/src/components/ui/no-data/.no-data-message.md +37 -0
- package/src/components/ui/no-data/.no-data-styles.md +39 -0
- package/src/components/ui/no-data/.no-data.md +59 -0
- package/src/components/ui/page-actions.tsx +2 -1
- package/src/components/ui/query-report-table/.query-report-table-header.md +15 -15
- package/src/components/ui/query-report-table/.query-report-table-row.md +16 -16
- package/src/components/ui/query-report-table/.query-report-table.md +39 -24
- package/src/components/ui/table/.table-column-filter-dropdown.md +15 -9
- package/src/components/ui/table/.table-description-cell.md +13 -12
- package/src/components/ui/table/.table-empty-state.md +19 -20
- package/src/components/ui/table/.table-row.md +30 -20
- package/src/components/ui/table/.table-skeleton.md +32 -27
- package/src/components/ui/table/.table-timestamp-cell.md +24 -23
- package/src/components/ui/table/.table.md +43 -30
- package/src/components/ui/table/.types.md +49 -45
- package/src/components/ui/table/.use-table-motion.md +45 -0
- package/src/contexts/.chat-runtime-context.md +55 -26
- package/src/contexts/.endpoints-runtime-context.md +40 -44
- package/src/embed-shims/.next-navigation.md +38 -31
- package/src/hooks/.api-hooks.md +28 -21
- package/src/hooks/.index.md +30 -25
- package/src/hooks/.use-authed-image-src.md +38 -0
- package/src/hooks/.use-contact-submission.md +35 -36
- package/src/hooks/.use-humanity-signals.md +47 -0
- package/src/hooks/.use-near-viewport.md +30 -30
- package/src/hooks/.use-og-placeholder-url.md +57 -0
- package/src/hooks/.use-scroll-to-hash.md +46 -0
- package/src/hooks/.use-self-fetch.md +52 -0
- package/src/hooks/state/.flatten-schema.md +37 -29
- package/src/hooks/ui/.index.md +37 -25
- package/src/hooks/ui/.use-auto-limit-tags.md +55 -38
- package/src/hooks/ui/.use-deferred-error.md +44 -0
- package/src/hooks/ui/.use-focus-trap.md +59 -0
- package/src/hooks/ui/.use-header-height.md +37 -0
- package/src/hooks/ui/.use-horizontal-scrollbar.md +25 -34
- package/src/hooks/ui/.use-marquee-engine.md +73 -0
- package/src/hooks/ui/.use-notification-permission.md +49 -0
- package/src/hooks/ui/.use-reset-on-page-hidden.md +40 -0
- package/src/hooks/ui/.use-suppress-clone-focus.md +41 -0
- package/src/nats/.index.md +39 -0
- package/src/nats/.nats-provider.md +46 -0
- package/src/nats/.nats.md +38 -28
- package/src/nats/.shared-connection.md +56 -0
- package/src/nats/.use-nats-subscription.md +57 -0
- package/src/stories/.ActionsMenu.stories.md +57 -0
- package/src/stories/.Alert.stories.md +46 -0
- package/src/stories/.AnnouncementBar.stories.md +59 -32
- package/src/stories/.AppLayoutDrawer.stories.md +60 -0
- package/src/stories/.AppLayoutSidebar.stories.md +45 -0
- package/src/stories/.ApprovalBatchMessage.stories.md +46 -0
- package/src/stories/.ApprovalBatchMessageClient.stories.md +55 -0
- package/src/stories/.ApprovalRequestMessage.stories.md +50 -0
- package/src/stories/.ApprovalRequestMessageClient.stories.md +41 -0
- package/src/stories/.BlogCard.stories.md +52 -0
- package/src/stories/.Board.stories.md +25 -20
- package/src/stories/.Button.stories.md +35 -40
- package/src/stories/.CampaignCardAdmin.stories.md +38 -0
- package/src/stories/.CardsStrip.stories.md +39 -0
- package/src/stories/.CaseStudyCard.stories.md +41 -0
- package/src/stories/.ChatContainer.stories.md +41 -0
- package/src/stories/.ChatInlineVideoPill.stories.md +36 -0
- package/src/stories/.ChatMessageEnhanced.stories.md +51 -0
- package/src/stories/.ChatMingoInfoCards.stories.md +30 -0
- package/src/stories/.ChatTicketList.stories.md +38 -22
- package/src/stories/.ChatTypingIndicator.stories.md +47 -0
- package/src/stories/.CheckboxBlock.stories.md +34 -35
- package/src/stories/.ContextCompactionDisplay.stories.md +28 -0
- package/src/stories/.CustomerInterviewCard.stories.md +36 -0
- package/src/stories/.DashboardInfoCard.stories.md +40 -33
- package/src/stories/.DataRoomDocCard.stories.md +42 -0
- package/src/stories/.DataTable.stories.md +37 -32
- package/src/stories/.DatePicker.stories.md +34 -30
- package/src/stories/.EmbeddableChat.stories.md +45 -28
- package/src/stories/.EmptyState.stories.md +55 -0
- package/src/stories/.ErrorMessageDisplay.stories.md +45 -0
- package/src/stories/.FilterModal.stories.md +28 -22
- package/src/stories/.GenericEntityCard.stories.md +50 -0
- package/src/stories/.GitHubActivityCard.stories.md +41 -0
- package/src/stories/.GuideWelcome.stories.md +38 -0
- package/src/stories/.Header.stories.md +32 -22
- package/src/stories/.HubspotTicketCard.stories.md +41 -0
- package/src/stories/.InfoCard.stories.md +37 -26
- package/src/stories/.InfoCardRow.stories.md +48 -0
- package/src/stories/.InfoSection.stories.md +55 -0
- package/src/stories/.Input.stories.md +45 -35
- package/src/stories/.InvestorUpdateCard.stories.md +41 -0
- package/src/stories/.MarqueeWall.stories.md +53 -0
- package/src/stories/.MingoAiButton.stories.md +31 -0
- package/src/stories/.MingoChatModals.stories.md +47 -0
- package/src/stories/.MingoInfoCard.stories.md +50 -0
- package/src/stories/.MingoOnboardingCard.stories.md +19 -19
- package/src/stories/.MingoWelcome.stories.md +41 -0
- package/src/stories/.MobileNavPanel.stories.md +52 -0
- package/src/stories/.Modal.stories.md +19 -16
- package/src/stories/.MspOrganizationCard.stories.md +45 -0
- package/src/stories/.NotificationDrawer.stories.md +39 -28
- package/src/stories/.OSTypeBadge.stories.md +34 -25
- package/src/stories/.OnboardingGuideCard.stories.md +41 -0
- package/src/stories/.PageActions.stories.md +21 -21
- package/src/stories/.PageContainer.stories.md +29 -24
- package/src/stories/.PageLayout.stories.md +18 -35
- package/src/stories/.PolicyConfigurationPanel.stories.md +58 -0
- package/src/stories/.ProductReleaseCard.stories.md +39 -0
- package/src/stories/.ProgramCard.stories.md +51 -0
- package/src/stories/.QuickActionChip.stories.md +59 -0
- package/src/stories/.QuickActionMarquee.stories.md +51 -0
- package/src/stories/.QuickActionWall.stories.md +58 -0
- package/src/stories/.RoadmapCard.stories.md +41 -0
- package/src/stories/.ShellTypeBadge.stories.md +42 -22
- package/src/stories/.SlackMessageCard.stories.md +42 -0
- package/src/stories/.SlashCommandSuggestions.stories.md +21 -31
- package/src/stories/.SplitButton.stories.md +23 -25
- package/src/stories/.StackedRowsPanel.stories.md +65 -0
- package/src/stories/.TabSelector.stories.md +26 -38
- package/src/stories/.Tag.stories.md +32 -27
- package/src/stories/.ThinkingDisplay.stories.md +43 -0
- package/src/stories/.TicketCard.stories.md +60 -0
- package/src/stories/.TicketInfoSection.stories.md +28 -20
- package/src/stories/.TimeTracker.stories.md +35 -0
- package/src/stories/.Toaster.stories.md +26 -20
- package/src/stories/.ToolBadge.stories.md +34 -24
- package/src/stories/.ToolExecutionDisplay.stories.md +22 -20
- package/src/stories/.ToolExecutionDisplayClient.stories.md +33 -0
- package/src/stories/.VideoBitesStrip.stories.md +37 -0
- package/src/stories/.WarningBlock.stories.md +51 -0
- package/src/stories/__fixtures__/.chat-card-decorator.md +39 -0
- package/src/stories/__fixtures__/.chat-cards.md +57 -0
- package/src/stories/auth/.CompleteAccount.stories.md +36 -0
- package/src/stories/auth/.CreateOrganization.stories.md +52 -0
- package/src/stories/auth/.InviteLinkInvalid.stories.md +21 -0
- package/src/stories/auth/.Login.stories.md +32 -0
- package/src/stories/auth/.OpenFrameSso.stories.md +58 -0
- package/src/stories/auth/.PasswordReset.stories.md +49 -0
- package/src/styles/ods-interaction-states.css +32 -0
- package/src/types/.announcement.md +67 -35
- package/src/types/.blog.md +44 -41
- package/src/types/.case-study.md +31 -17
- package/src/types/.content-ref.md +37 -0
- package/src/types/.customer-interview.md +38 -38
- package/src/types/.doc-source.md +87 -0
- package/src/types/.entity-author.md +53 -0
- package/src/types/.entity-platform.md +40 -0
- package/src/types/.index.md +23 -17
- package/src/types/.marketing.md +32 -35
- package/src/types/.navigation.md +31 -19
- package/src/types/.os.types.md +30 -31
- package/src/types/.permissions.md +49 -35
- package/src/types/.platform.md +44 -28
- package/src/types/.product-release.md +59 -42
- package/src/types/.shell.types.md +46 -30
- package/src/types/.supabase.md +43 -29
- package/src/types/.tool.types.md +43 -23
- package/src/types/.video-processing.md +40 -31
- package/src/types/supabase.ts +80 -0
- package/src/types/video-processing.ts +1 -0
- package/src/utils/.announcement-storage.md +49 -17
- package/src/utils/.app-config.md +22 -18
- package/src/utils/.brand-marks.md +46 -0
- package/src/utils/.cn.md +40 -29
- package/src/utils/.color-analysis.md +34 -25
- package/src/utils/.common.md +43 -23
- package/src/utils/.content-href.md +64 -0
- package/src/utils/.content-ref-groups.md +51 -0
- package/src/utils/.date-utils.md +46 -35
- package/src/utils/.doc-path-utils.md +38 -0
- package/src/utils/.doc-tree-nav.md +51 -0
- package/src/utils/.embed-authed-fetch.md +48 -32
- package/src/utils/.embed-content-fetch.md +36 -0
- package/src/utils/.embed-url-converters.md +52 -0
- package/src/utils/.extract-items.md +49 -0
- package/src/utils/.faq-anchor.md +48 -0
- package/src/utils/.format.md +73 -55
- package/src/utils/.humanity-signals.md +59 -0
- package/src/utils/.index.md +73 -31
- package/src/utils/.list-url.md +43 -0
- package/src/utils/.markdown-section-extractor.md +57 -0
- package/src/utils/.markdown-to-plain.md +51 -0
- package/src/utils/.media-carousel-utils-stub.md +42 -31
- package/src/utils/.ods-color-utils.md +84 -43
- package/src/utils/.og-placeholder.md +45 -0
- package/src/utils/.page-header-constants.md +26 -0
- package/src/utils/.platform-config.md +34 -34
- package/src/utils/.release-badge.md +47 -0
- package/src/utils/.release-cover.md +45 -0
- package/src/utils/.same-page-hash-nav.md +45 -0
- package/src/utils/.scroll-into-view.md +32 -25
- package/src/utils/.seo-description.md +37 -0
- package/src/utils/.seo-title.md +25 -0
- package/src/utils/.social-embed-cache.md +37 -0
- package/src/utils/.source-icons.md +30 -34
- package/src/utils/.suggestion-url.md +46 -0
- package/src/utils/.tool-utils.md +39 -24
- package/src/utils/.tree-builder.md +64 -0
- package/src/utils/.video-bite-id.md +42 -0
- package/src/utils/announcement-storage.ts +9 -18
- package/src/utils/dev-sections/.dev-section-param-keys.md +51 -0
- package/src/utils/dev-sections/.openframe-dev-sections.md +40 -28
- package/src/utils/dismiss-cookie.ts +55 -0
- package/src/utils/dismissal-storage.ts +38 -0
- package/src/utils/index.ts +3 -0
- package/dist/chunk-BBUEY73L.js.map +0 -1
- package/dist/chunk-G3UN2DXU.cjs.map +0 -1
- package/dist/chunk-OWMLWJQY.cjs.map +0 -1
- package/dist/chunk-SASR7SK5.js.map +0 -1
- package/src/components/chat/utils/.onboarding-icons.md +0 -28
- package/src/hooks/.use-og-placeholder.md +0 -51
- /package/dist/{chunk-MGKCS6KS.js.map → chunk-3WCARCI4.js.map} +0 -0
- /package/dist/{chunk-Y3Y2DLNO.js.map → chunk-6UGYBCEN.js.map} +0 -0
- /package/dist/{chunk-PSPWT2Q5.js.map → chunk-7LWXWXEY.js.map} +0 -0
- /package/dist/{chunk-VTSRDC4W.js.map → chunk-CKC3EAYT.js.map} +0 -0
- /package/dist/{chunk-ZRANZNEA.js.map → chunk-JJE5TUZK.js.map} +0 -0
- /package/dist/{chunk-66BNAFO6.js.map → chunk-JQIUBFHL.js.map} +0 -0
- /package/dist/{chunk-2DIOV7BR.js.map → chunk-UPN6EFOM.js.map} +0 -0
- /package/dist/{chunk-4SMRQ4SN.js.map → chunk-WBL5Y4PS.js.map} +0 -0
- /package/dist/{chunk-FP4H6Y5O.js.map → chunk-WPYPSIY2.js.map} +0 -0
- /package/dist/{chunk-R5WJJL4V.js.map → chunk-X5X43HFV.js.map} +0 -0
- /package/dist/{chunk-DEHZ5CTC.js.map → chunk-XTWKDMCG.js.map} +0 -0
|
@@ -0,0 +1,891 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* <FloatingWalkthroughVideo> — THE generic, embeddable per-platform demo-video
|
|
5
|
+
* widget. A collapsed card pinned bottom-left (bite-identical hover grammar via
|
|
6
|
+
* <VideoHoverPreviewSurface>) that opens a large in-page theater (Radix Dialog
|
|
7
|
+
* primitives, NOT native fullscreen) showing ONLY the video: a bare 16:9 stage
|
|
8
|
+
* with the player's own controls + captions. No card chrome, no summary.
|
|
9
|
+
*
|
|
10
|
+
* All UI lives here in the lib so every platform site AND the react-embedding
|
|
11
|
+
* example mount the same component; the host supplies only the video data.
|
|
12
|
+
*
|
|
13
|
+
* STATE MODEL (read before adding a boolean — the recurring bugs in this file
|
|
14
|
+
* all came from ad-hoc flags contradicting each other):
|
|
15
|
+
* `cardMode` — ONE discriminant: 'resume' | 'preview' | 'poster'. Derived,
|
|
16
|
+
* never stored, so the two players can't both be mounted.
|
|
17
|
+
* `cardMuted` / `cardPaused` — transport state for whichever card player is
|
|
18
|
+
* live. Both toggles ALWAYS render in both states; a control
|
|
19
|
+
* that hides itself on use is a dead end (that bug shipped
|
|
20
|
+
* twice). Never gate a control's visibility on its own value.
|
|
21
|
+
* `cardFallback` — autoplay-BLOCKED prompt only. Distinct from `cardMuted`
|
|
22
|
+
* (user intent); conflating them is what made unmute vanish.
|
|
23
|
+
* `handoff` — theater→card continuation snapshot. Clearing it UNMOUNTS the
|
|
24
|
+
* mini player, so never clear it to express "paused".
|
|
25
|
+
* Layering: the media layer is `pointer-events-none` so a paused/idle player
|
|
26
|
+
* can never swallow a click; every non-control click reaches the activation
|
|
27
|
+
* overlay. Corners are exclusive: transport TL, dismiss TR, title+presenter BL.
|
|
28
|
+
*
|
|
29
|
+
* Audio invariant (both directions): never two overlapping streams, nor a blip.
|
|
30
|
+
* - open: pause the hover preview synchronously (pointerdown), force the
|
|
31
|
+
* surface inactive for the whole open duration, reset the YouTube suspend.
|
|
32
|
+
* - close: snapshot then pause the theater player (file) / suspend (YouTube).
|
|
33
|
+
* NOTE: this theater's Content carries no exit animation, so Radix unmounts
|
|
34
|
+
* it in the same commit and the iframe dies before the suspend postMessage
|
|
35
|
+
* can run. The suspend path exists for hosts that DO animate the exit; here
|
|
36
|
+
* the unmount itself is what stops the audio.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
40
|
+
import * as DialogPrimitive from '@radix-ui/react-dialog';
|
|
41
|
+
import { cn } from '../../utils/cn';
|
|
42
|
+
import { Button } from '../ui/button';
|
|
43
|
+
import { SquareAvatar } from '../ui/square-avatar';
|
|
44
|
+
import { DialogPortal, DialogOverlay } from '../ui/dialog';
|
|
45
|
+
import { VideoHoverPreviewSurface } from './video-hover-preview';
|
|
46
|
+
import { CardHitLayer } from './card-hit-layer';
|
|
47
|
+
import { VideoPlayBadge, VideoUnmuteGlyph } from './video-center-badge';
|
|
48
|
+
import { XmarkIcon } from '../icons-v2-generated/signs-and-symbols/xmark-icon';
|
|
49
|
+
import { VolumeUpIcon } from '../icons-v2-generated/audio-and-visual/volume-up-icon';
|
|
50
|
+
import { PauseIcon } from '../icons-v2-generated/media-playback/pause-icon';
|
|
51
|
+
import { PlayIcon } from '../icons-v2-generated/media-playback/play-icon';
|
|
52
|
+
import { VolumeXmarkIcon } from '../icons-v2-generated/audio-and-visual/volume-xmark-icon';
|
|
53
|
+
import { Video, type VideoPlayerHandle, type VideoMutedFallbackState } from './video';
|
|
54
|
+
import {
|
|
55
|
+
WALKTHROUGH_VIDEO_DISMISS_KEY,
|
|
56
|
+
isWalkthroughDismissed,
|
|
57
|
+
dismissWalkthrough,
|
|
58
|
+
} from '../../utils/dismissal-storage';
|
|
59
|
+
|
|
60
|
+
/** Wire-shape data for the widget. Kept as the shared contract the hub DAL
|
|
61
|
+
* re-exports as `PublicWalkthroughVideo & { id }`. `mainVideoUrl`/`youtubeUrl`
|
|
62
|
+
* stay SEPARATE — YouTube wins when both are set (card matches theater). */
|
|
63
|
+
export interface WalkthroughVideoData {
|
|
64
|
+
/** Row id — used for id-match cookie dismissal (a new video re-shows the
|
|
65
|
+
* card even after an old one was dismissed). */
|
|
66
|
+
id?: string | number;
|
|
67
|
+
mainVideoUrl?: string | null;
|
|
68
|
+
youtubeUrl?: string | null;
|
|
69
|
+
posterUrl?: string | null;
|
|
70
|
+
/** RELATIVE VTT path (/api/captions/...); embedders prefix their proxy base. */
|
|
71
|
+
captionsUrl?: string | null;
|
|
72
|
+
title?: string | null;
|
|
73
|
+
presenterAvatarUrl?: string | null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface FloatingWalkthroughVideoProps {
|
|
77
|
+
video: WalkthroughVideoData | null | undefined;
|
|
78
|
+
open?: boolean;
|
|
79
|
+
onOpenChange?: (open: boolean) => void;
|
|
80
|
+
defaultOpen?: boolean;
|
|
81
|
+
label?: string;
|
|
82
|
+
appearDelayMs?: number;
|
|
83
|
+
/** Cookie-based dismissal (id-match, mirrors the announcement bar). `false`
|
|
84
|
+
* disables the X entirely. `storageKey` is the per-platform cookie name. */
|
|
85
|
+
dismissal?: { storageKey?: string } | false;
|
|
86
|
+
hideNearSelector?: string;
|
|
87
|
+
/** Route identity from the host (the lib can't observe navigation). Changing
|
|
88
|
+
* it re-queries the footer IO target. */
|
|
89
|
+
pathname?: string;
|
|
90
|
+
className?: string;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** "Paused at the end" — used by BOTH the close snapshot and the reopen seed,
|
|
94
|
+
* which must agree or a finished video reopens at its own last frame. */
|
|
95
|
+
const isAtEnd = (duration: number, time: number) => duration > 0 && duration - time < 1;
|
|
96
|
+
|
|
97
|
+
const WALKTHROUGH_Z = 'z-[9980]'; // one layer BELOW the chat dock (z-[9990]).
|
|
98
|
+
|
|
99
|
+
interface Handoff {
|
|
100
|
+
time: number;
|
|
101
|
+
muted: boolean;
|
|
102
|
+
playing: boolean;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function FloatingWalkthroughVideo({
|
|
106
|
+
video,
|
|
107
|
+
open: openProp,
|
|
108
|
+
onOpenChange,
|
|
109
|
+
defaultOpen,
|
|
110
|
+
label = 'Play Demo Video',
|
|
111
|
+
appearDelayMs = 3000,
|
|
112
|
+
dismissal = {},
|
|
113
|
+
hideNearSelector = 'footer',
|
|
114
|
+
pathname,
|
|
115
|
+
className,
|
|
116
|
+
}: FloatingWalkthroughVideoProps): React.ReactElement | null {
|
|
117
|
+
const dismissEnabled = dismissal !== false;
|
|
118
|
+
const storageKey = (dismissEnabled && dismissal.storageKey) || WALKTHROUGH_VIDEO_DISMISS_KEY;
|
|
119
|
+
// id-match dismissal key: a new video (new id) re-shows the card. No id
|
|
120
|
+
// (embedder) → a stable presence marker so dismissal still works.
|
|
121
|
+
const dismissalId = video?.id != null ? String(video.id) : 'dismissed';
|
|
122
|
+
|
|
123
|
+
// --- mount gate (never LCP; cookie read happens post-delay, no hydration mismatch) ---
|
|
124
|
+
const [mounted, setMounted] = useState(false);
|
|
125
|
+
const [dismissed, setDismissed] = useState(false);
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
let idleHandle: number | null = null;
|
|
128
|
+
// Wait for the data: on the embedder path `video` starts null, so the
|
|
129
|
+
// dismissal id was 'dismissed' on the first pass and a dismissed card
|
|
130
|
+
// flashed for the length of a second appear-delay before hiding.
|
|
131
|
+
if (!video) return; // wait for data — see the id-based dep below
|
|
132
|
+
const timer = setTimeout(() => {
|
|
133
|
+
const reveal = () => {
|
|
134
|
+
if (dismissEnabled && isWalkthroughDismissed(storageKey, dismissalId)) {
|
|
135
|
+
setDismissed(true);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
// Cleared on the way through: a client-only embedder can swap to a NEW
|
|
139
|
+
// video id in-session (the cookie is id-matched, so the new one isn't
|
|
140
|
+
// dismissed), and a sticky `dismissed` kept the card hidden until a
|
|
141
|
+
// full reload — contradicting the id-match behaviour documented above.
|
|
142
|
+
setDismissed(false);
|
|
143
|
+
setMounted(true);
|
|
144
|
+
};
|
|
145
|
+
const ric = (window as unknown as { requestIdleCallback?: (cb: () => void) => number }).requestIdleCallback;
|
|
146
|
+
if (typeof ric === 'function') idleHandle = ric(reveal);
|
|
147
|
+
else reveal();
|
|
148
|
+
}, appearDelayMs);
|
|
149
|
+
return () => {
|
|
150
|
+
clearTimeout(timer);
|
|
151
|
+
const cic = (window as unknown as { cancelIdleCallback?: (h: number) => void }).cancelIdleCallback;
|
|
152
|
+
if (idleHandle !== null && typeof cic === 'function') cic(idleHandle);
|
|
153
|
+
};
|
|
154
|
+
// `video?.id`, not the object: a new identity (embedder refetch, re-seeded
|
|
155
|
+
// RSC payload) would otherwise restart the appear delay.
|
|
156
|
+
}, [appearDelayMs, dismissEnabled, storageKey, dismissalId, video?.id]);
|
|
157
|
+
|
|
158
|
+
// --- environment preferences (read after mount; no SSR access) ---
|
|
159
|
+
const [previewSuppressed, setPreviewSuppressed] = useState(false);
|
|
160
|
+
useEffect(() => {
|
|
161
|
+
try {
|
|
162
|
+
const reducedMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false;
|
|
163
|
+
const conn = (navigator as unknown as { connection?: { saveData?: boolean; effectiveType?: string } }).connection;
|
|
164
|
+
const saveData = conn?.saveData === true || conn?.effectiveType === '2g' || conn?.effectiveType === 'slow-2g';
|
|
165
|
+
setPreviewSuppressed(Boolean(reducedMotion || saveData));
|
|
166
|
+
} catch { /* ignore */ }
|
|
167
|
+
}, []);
|
|
168
|
+
|
|
169
|
+
// --- controlled/uncontrolled open ---
|
|
170
|
+
const [openState, setOpenState] = useState(Boolean(defaultOpen));
|
|
171
|
+
const open = openProp !== undefined ? openProp : openState;
|
|
172
|
+
|
|
173
|
+
// --- refs & continuation state ---
|
|
174
|
+
const previewHandleRef = useRef<VideoPlayerHandle | null>(null);
|
|
175
|
+
const theaterHandleRef = useRef<VideoPlayerHandle | null>(null);
|
|
176
|
+
// Why the theater reports its fallback state even though it renders no card
|
|
177
|
+
// control: `blocked`/`muted` distinguish "the user muted this" from "the
|
|
178
|
+
// browser refused sound". Only the former may become standing intent.
|
|
179
|
+
const theaterForcedMuteRef = useRef(false);
|
|
180
|
+
const resumeHandleRef = useRef<VideoPlayerHandle | null>(null);
|
|
181
|
+
const closeButtonRef = useRef<HTMLButtonElement | null>(null);
|
|
182
|
+
// TRUE only when the user pressed Mute. An autoplay-forced mute (cardFallback)
|
|
183
|
+
// is NOT intent, so the theater must not inherit it — conflating the two is
|
|
184
|
+
// what made the theater open silently after a blocked hover preview.
|
|
185
|
+
const userMutedRef = useRef(false);
|
|
186
|
+
const endedLatchRef = useRef(false);
|
|
187
|
+
|
|
188
|
+
const [hovered, setHovered] = useState(false);
|
|
189
|
+
const [handoff, setHandoff] = useState<Handoff | null>(null);
|
|
190
|
+
const [suspended, setSuspended] = useState(false);
|
|
191
|
+
const [theaterStart, setTheaterStart] = useState<{ time: number; muted: boolean }>({ time: 0, muted: false });
|
|
192
|
+
const [footerHidden, setFooterHidden] = useState(false);
|
|
193
|
+
const [cardFallback, setCardFallback] = useState<VideoMutedFallbackState>({ muted: false, blocked: false });
|
|
194
|
+
// Card-player transport state, driving the persistent mute/play TOGGLES.
|
|
195
|
+
// Separate from `cardFallback` (which is the autoplay-blocked prompt).
|
|
196
|
+
const [cardMuted, setCardMuted] = useState(false);
|
|
197
|
+
const [cardPaused, setCardPaused] = useState(false);
|
|
198
|
+
// Renderable mirror of `userMutedRef` — the ref is for synchronous reads in
|
|
199
|
+
// callbacks, this is what the player subtree observes. Deliberately NOT
|
|
200
|
+
// `cardMuted`: that also tracks policy-forced mutes.
|
|
201
|
+
const [userMuted, setUserMuted] = useState(false);
|
|
202
|
+
|
|
203
|
+
const isYouTube = Boolean(video?.youtubeUrl);
|
|
204
|
+
// PRESENCE-based: any handoff means "the card owns this video at this
|
|
205
|
+
// timestamp"; `playing` only seeds the pause toggle. Keying it on `playing`
|
|
206
|
+
// made a paused close (or a tab-switch) a one-way door — the card lost its
|
|
207
|
+
// transport AND its hover preview with no way back. `!open` keeps a
|
|
208
|
+
// host-controlled `open` from running the card and theater at once.
|
|
209
|
+
const resumeMode = !open && handoff !== null && !isYouTube;
|
|
210
|
+
const previewAllowed = !previewSuppressed && !isYouTube;
|
|
211
|
+
|
|
212
|
+
// --- footer fade (re-query on pathname change; null-tolerant) ---
|
|
213
|
+
useEffect(() => {
|
|
214
|
+
if (!mounted) return;
|
|
215
|
+
const el = typeof document !== 'undefined' ? document.querySelector(hideNearSelector) : null;
|
|
216
|
+
if (!el || typeof IntersectionObserver === 'undefined') {
|
|
217
|
+
setFooterHidden(false);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
const io = new IntersectionObserver(
|
|
221
|
+
entries => setFooterHidden(entries[0]?.isIntersecting ?? false),
|
|
222
|
+
{ rootMargin: '0px 0px -40px 0px' },
|
|
223
|
+
);
|
|
224
|
+
io.observe(el);
|
|
225
|
+
return () => io.disconnect();
|
|
226
|
+
}, [mounted, hideNearSelector, pathname]);
|
|
227
|
+
|
|
228
|
+
// --- tab-hidden: pause resume-mode playback, keep the timestamp ---
|
|
229
|
+
useEffect(() => {
|
|
230
|
+
if (!resumeMode) return;
|
|
231
|
+
const onVis = () => {
|
|
232
|
+
if (document.visibilityState === 'hidden') {
|
|
233
|
+
const h = resumeHandleRef.current;
|
|
234
|
+
if (h) {
|
|
235
|
+
// Pause in place. Rewriting the handoff here used to unmount the
|
|
236
|
+
// mini player, so a background tab-switch silently destroyed it.
|
|
237
|
+
h.pause();
|
|
238
|
+
setCardPaused(true);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
document.addEventListener('visibilitychange', onVis);
|
|
243
|
+
return () => document.removeEventListener('visibilitychange', onVis);
|
|
244
|
+
}, [resumeMode]);
|
|
245
|
+
|
|
246
|
+
// Faded out behind the footer: stop audio. The card is `pointer-events-none`
|
|
247
|
+
// while hidden, so a still-playing player would be unreachable — the same
|
|
248
|
+
// reason the tab-hidden handler exists.
|
|
249
|
+
// Depends on cardMode too: a player that MOUNTS while the card is already
|
|
250
|
+
// hidden (e.g. the footer scrolled into view during the theater) would
|
|
251
|
+
// otherwise autoplay behind an invisible, pointer-inert card.
|
|
252
|
+
useEffect(() => {
|
|
253
|
+
if (!footerHidden) return;
|
|
254
|
+
const h = resumeHandleRef.current ?? previewHandleRef.current;
|
|
255
|
+
try { h?.pause(); } catch { /* ignore */ }
|
|
256
|
+
setCardPaused(true);
|
|
257
|
+
setHovered(false);
|
|
258
|
+
// `resumeMode`/`hovered` (not the derived cardMode, declared below) are the
|
|
259
|
+
// inputs that decide whether a player is mounted.
|
|
260
|
+
}, [footerHidden, resumeMode, hovered]);
|
|
261
|
+
|
|
262
|
+
// Render-synced mirror of the derived `cardMode`, which is declared BELOW
|
|
263
|
+
// the callbacks that need it. Reading state declared later from a memoized
|
|
264
|
+
// callback is the exact trap that made every hover-preview open restart at
|
|
265
|
+
// 0:00 (the closure kept its render-0 value), so this is a ref, not a dep.
|
|
266
|
+
const cardModeRef = useRef<'resume' | 'preview' | 'poster'>('poster');
|
|
267
|
+
|
|
268
|
+
// Set by the card's OWN handlers, consumed by the controlled-mode sync
|
|
269
|
+
// below. Without it, a controlled host flipping `open` in response to
|
|
270
|
+
// `onOpenChange` looks identical to a genuinely host-originated open, and
|
|
271
|
+
// the sync clobbers the seed/snapshot the handler just computed.
|
|
272
|
+
const selfDrivenForRef = useRef<boolean | null>(null);
|
|
273
|
+
|
|
274
|
+
// Set on every close so the focus Radix restores to the hit layer can't be
|
|
275
|
+
// mistaken for the user tabbing to the card. CONSUMED BY THE FOCUS HANDLER
|
|
276
|
+
// (see commitOpen) — the timer beside it is only a safety valve.
|
|
277
|
+
const justClosedRef = useRef(false);
|
|
278
|
+
const justClosedTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
279
|
+
|
|
280
|
+
const commitOpen = useCallback((next: boolean) => {
|
|
281
|
+
if (!next) {
|
|
282
|
+
// CONSUMED BY THE FOCUS HANDLER, not by a timer racing Radix.
|
|
283
|
+
// Radix's FocusScope restores focus from its OWN `setTimeout(…, 0)`,
|
|
284
|
+
// registered during cleanup — i.e. AFTER any timer we schedule here.
|
|
285
|
+
// A timer-cleared latch was therefore always false by the time the
|
|
286
|
+
// restore landed, and Escape-close still restarted preview audio.
|
|
287
|
+
// The timer below is only a safety valve for the case where no focus
|
|
288
|
+
// restoration arrives at all (host-driven close, no focus to return).
|
|
289
|
+
justClosedRef.current = true;
|
|
290
|
+
if (justClosedTimerRef.current) clearTimeout(justClosedTimerRef.current);
|
|
291
|
+
justClosedTimerRef.current = setTimeout(() => {
|
|
292
|
+
justClosedTimerRef.current = null;
|
|
293
|
+
justClosedRef.current = false;
|
|
294
|
+
}, 400);
|
|
295
|
+
}
|
|
296
|
+
if (openProp === undefined) setOpenState(next);
|
|
297
|
+
onOpenChange?.(next);
|
|
298
|
+
}, [openProp, onOpenChange]);
|
|
299
|
+
|
|
300
|
+
// Synchronous preview pause on pointerdown — the window-capture activation
|
|
301
|
+
// waiter unmutes a pre-activation preview on the first pointerdown; without
|
|
302
|
+
// this the opening/dismissing press itself blips at 50% volume.
|
|
303
|
+
const pausePreviewNow = useCallback(() => {
|
|
304
|
+
// Whichever player is live — in resume mode that is the continuation
|
|
305
|
+
// player, so pausing only the preview left a playing card audible through
|
|
306
|
+
// the whole pointerdown -> click -> commit window.
|
|
307
|
+
const h = resumeHandleRef.current ?? previewHandleRef.current;
|
|
308
|
+
try { h?.pause(); } catch { /* ignore */ }
|
|
309
|
+
// Keep the toggle honest: an aborted press (pointerdown then drag away)
|
|
310
|
+
// otherwise left it reading "Pause" over a paused video.
|
|
311
|
+
setCardPaused(true);
|
|
312
|
+
}, []);
|
|
313
|
+
|
|
314
|
+
const openTheater = useCallback(() => {
|
|
315
|
+
// Captured BEFORE `pausePreviewNow()` below. NOTE this only helps keyboard
|
|
316
|
+
// activation: a mouse click already ran the hit layer's `onPointerDown`
|
|
317
|
+
// pause, so on that path the element IS paused here and the test below
|
|
318
|
+
// still reduces to `liveTime > 0.5`. Kept because Enter/Space dispatches
|
|
319
|
+
// no pointerdown, and there the live playing state is real.
|
|
320
|
+
const handleBeforePause = resumeHandleRef.current ?? previewHandleRef.current;
|
|
321
|
+
const liveWasPlaying = handleBeforePause?.getPaused() === false;
|
|
322
|
+
// Idempotent: keyboard activation (Enter/Space) dispatches no pointerdown,
|
|
323
|
+
// so the synchronous audio-stop guard would otherwise be skipped.
|
|
324
|
+
pausePreviewNow();
|
|
325
|
+
// Seed the theater from whatever the card was ACTUALLY playing, so the
|
|
326
|
+
// theater continues at the same second in every direction:
|
|
327
|
+
// resume player (live) > hover preview (live) > paused handoff snapshot.
|
|
328
|
+
// Reading the preview handle is what makes "hover-preview → click → theater"
|
|
329
|
+
// continue mid-video instead of restarting at 0. Pausing on pointerdown does
|
|
330
|
+
// not reset currentTime, so the value is still accurate here.
|
|
331
|
+
// Source chosen by MODE. Picking "whichever handle reports > 0.5s" let a
|
|
332
|
+
// stale hover-preview position (0:05) shadow a stored handoff (3:20) on
|
|
333
|
+
// desktop while mobile — which never hovers — resumed correctly: the same
|
|
334
|
+
// gesture gave different results per platform.
|
|
335
|
+
// Read the refs directly — no render-scoped state. The previous version
|
|
336
|
+
// consulted `cardActive`, which is declared BELOW this callback and is not
|
|
337
|
+
// a dependency, so the memoized closure always saw its render-0 value
|
|
338
|
+
// (false) and every hover-preview -> theater open restarted at 0:00.
|
|
339
|
+
// Mode-gated: the card is `position: fixed`, so its preview player is
|
|
340
|
+
// permanently in-viewport and is NEVER unmounted — it keeps currentTime
|
|
341
|
+
// from a hover abandoned minutes ago. Reading it unconditionally made
|
|
342
|
+
// "Play Demo Video" silently start 10s in. Only a LIVE mode may seed.
|
|
343
|
+
const liveHandle =
|
|
344
|
+
resumeHandleRef.current ?? (cardModeRef.current === 'preview' ? previewHandleRef.current : null);
|
|
345
|
+
const liveTime = liveHandle?.getCurrentTime() ?? 0;
|
|
346
|
+
// A preview that ran to completion keeps its currentTime, so without the
|
|
347
|
+
// same at-end guard the close path uses, clicking the card would open the
|
|
348
|
+
// theater seeked to the final frame and it would instantly re-end.
|
|
349
|
+
const liveDuration = liveHandle?.getDuration() ?? 0;
|
|
350
|
+
const liveAtEnd = isAtEnd(liveDuration, liveTime);
|
|
351
|
+
const start = {
|
|
352
|
+
// At end -> start over. Falling back to `handoff.time` here would rewind
|
|
353
|
+
// to an unrelated older anchor (e.g. 3:20) instead of restarting.
|
|
354
|
+
time: liveAtEnd ? 0 : (liveTime > 0.5 ? liveTime : (handoff?.time ?? 0)),
|
|
355
|
+
// Live element wins over remembered intent (the theater's own chrome may
|
|
356
|
+
// have changed it) — EXCEPT when the element is muted only because
|
|
357
|
+
// autoplay policy forced it, which is not intent.
|
|
358
|
+
// `cardFallback.muted` is armed BOTH by autoplay policy and by our own
|
|
359
|
+
// `startMuted` (i.e. by the user's mute), so it alone cannot mean
|
|
360
|
+
// "forced". Only a fallback the user never asked for is policy-forced;
|
|
361
|
+
// otherwise an explicitly muted video reopened at full volume.
|
|
362
|
+
// Trust the live element only when it has actually been playing; a
|
|
363
|
+
// freshly remounted poster-mode player always reports muted=false and
|
|
364
|
+
// would silently discard the user's last explicit mute.
|
|
365
|
+
muted: (cardFallback.muted && !userMutedRef.current)
|
|
366
|
+
? false
|
|
367
|
+
: ((liveHandle && (liveTime > 0.5 || liveWasPlaying))
|
|
368
|
+
? liveHandle.getMuted()
|
|
369
|
+
: userMutedRef.current),
|
|
370
|
+
};
|
|
371
|
+
setTheaterStart(start);
|
|
372
|
+
setHovered(false); // force preview inactive
|
|
373
|
+
setSuspended(false); // reset YouTube suspend for this open
|
|
374
|
+
endedLatchRef.current = false;
|
|
375
|
+
setHandoff(null); // resume player unmounts
|
|
376
|
+
selfDrivenForRef.current = true;
|
|
377
|
+
commitOpen(true);
|
|
378
|
+
}, [handoff, cardFallback.muted, commitOpen]);
|
|
379
|
+
|
|
380
|
+
// Sync for open/close transitions the card did NOT originate — a host
|
|
381
|
+
// driving the controlled `open` prop from its own UI. Card-originated
|
|
382
|
+
// transitions are latched by `selfDrivenRef` and skipped here, because the
|
|
383
|
+
// handler already did the seeding (and a controlled host flips `open` in
|
|
384
|
+
// RESPONSE to that handler, which is indistinguishable from a host-driven
|
|
385
|
+
// change without the latch). `defaultOpen` is NOT covered: it is the
|
|
386
|
+
// uncontrolled initial value, and the initial `theaterStart` already equals
|
|
387
|
+
// what this seed would compute. This MUST run during the
|
|
388
|
+
// render that flips `open` — not in an effect: the Dialog's Content (and
|
|
389
|
+
// therefore MuxPlayer) mounts in that same render, and `startTime` /
|
|
390
|
+
// `autoPlay` / `startMuted` are LOAD-TIME props read once at construction.
|
|
391
|
+
// An effect commits one render too late, so the player would already have
|
|
392
|
+
// been built from the previous `theaterStart` and a stale `suspended`
|
|
393
|
+
// would post `pauseVideo` at mount. This is React's documented
|
|
394
|
+
// "adjust state while rendering" pattern; the extra render is discarded
|
|
395
|
+
// before children are committed.
|
|
396
|
+
const [prevOpen, setPrevOpen] = useState(open);
|
|
397
|
+
if (open !== prevOpen) {
|
|
398
|
+
setPrevOpen(open);
|
|
399
|
+
// Compared, not cleared: React can discard and re-run a render attempt
|
|
400
|
+
// (concurrent interruption, error retry, a host wrapping setOpen in
|
|
401
|
+
// startTransition). A read-and-clear would decide differently on the
|
|
402
|
+
// retry; an equality test gives the same answer every time.
|
|
403
|
+
const selfDriven = selfDrivenForRef.current === open;
|
|
404
|
+
if (openProp !== undefined && !selfDriven) {
|
|
405
|
+
if (open) {
|
|
406
|
+
// A host open has no originating gesture, but resume mode DOES have a
|
|
407
|
+
// live element — reading only `handoff` discarded however long the mini
|
|
408
|
+
// player had been running since the close. Mirrors openTheater's seed.
|
|
409
|
+
const liveResume = resumeHandleRef.current;
|
|
410
|
+
const liveTime = liveResume?.getCurrentTime() ?? 0;
|
|
411
|
+
const liveAtEnd = isAtEnd(liveResume?.getDuration() ?? 0, liveTime);
|
|
412
|
+
setTheaterStart({
|
|
413
|
+
time: liveAtEnd ? 0 : (liveTime > 0.5 ? liveTime : (handoff?.time ?? 0)),
|
|
414
|
+
muted: liveResume && liveTime > 0.5 ? liveResume.getMuted() : userMutedRef.current,
|
|
415
|
+
});
|
|
416
|
+
setHovered(false);
|
|
417
|
+
setSuspended(false);
|
|
418
|
+
endedLatchRef.current = false;
|
|
419
|
+
setHandoff(null);
|
|
420
|
+
} else {
|
|
421
|
+
// Falling edge: the host bypassed handleOpenChange, so without this the
|
|
422
|
+
// position, the mute intent and the ended state are all simply lost.
|
|
423
|
+
const h = theaterHandleRef.current;
|
|
424
|
+
if (h && !isYouTube) {
|
|
425
|
+
const time = h.getCurrentTime();
|
|
426
|
+
const muted = h.getMuted();
|
|
427
|
+
const paused = h.getPaused();
|
|
428
|
+
const atEnd = isAtEnd(h.getDuration(), time);
|
|
429
|
+
try { h.pause(); } catch { /* already gone */ }
|
|
430
|
+
if (!(muted && theaterForcedMuteRef.current && !userMutedRef.current)) {
|
|
431
|
+
userMutedRef.current = muted;
|
|
432
|
+
setUserMuted(muted);
|
|
433
|
+
}
|
|
434
|
+
setHandoff(
|
|
435
|
+
(endedLatchRef.current && paused && atEnd) || time < 1
|
|
436
|
+
? null
|
|
437
|
+
: { time, muted, playing: !paused },
|
|
438
|
+
);
|
|
439
|
+
} else if (isYouTube) {
|
|
440
|
+
setSuspended(true);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const handleOpenChange = useCallback((next: boolean) => {
|
|
447
|
+
if (next) { openTheater(); return; }
|
|
448
|
+
// This close IS the card's own — a controlled host will flip `open` in
|
|
449
|
+
// response, and the sync above must not re-snapshot the player we are
|
|
450
|
+
// about to pause (it would read paused=true and downgrade a playing
|
|
451
|
+
// handoff to paused).
|
|
452
|
+
selfDrivenForRef.current = false;
|
|
453
|
+
// Closing: snapshot + stop the theater player BEFORE the exit animation.
|
|
454
|
+
if (isYouTube) {
|
|
455
|
+
setSuspended(true);
|
|
456
|
+
commitOpen(false);
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
const h = theaterHandleRef.current;
|
|
460
|
+
if (h) {
|
|
461
|
+
const time = h.getCurrentTime();
|
|
462
|
+
const muted = h.getMuted();
|
|
463
|
+
// The theater's own chrome can mute/unmute; that IS user intent, so it
|
|
464
|
+
// must flow back or reopening would contradict what they just did.
|
|
465
|
+
// EXCEPT when the element is muted only because unmuted autoplay was
|
|
466
|
+
// rejected: promoting that into intent silently muted every later hover
|
|
467
|
+
// preview and every later theater open, for the whole session, with no
|
|
468
|
+
// gesture from the user. Mirrors the same test `openTheater` already
|
|
469
|
+
// applies in the opposite direction.
|
|
470
|
+
const policyForcedMute = muted && theaterForcedMuteRef.current && !userMutedRef.current;
|
|
471
|
+
if (!policyForcedMute) {
|
|
472
|
+
userMutedRef.current = muted;
|
|
473
|
+
setUserMuted(muted);
|
|
474
|
+
}
|
|
475
|
+
const paused = h.getPaused();
|
|
476
|
+
const duration = h.getDuration();
|
|
477
|
+
const atEnd = isAtEnd(duration, time);
|
|
478
|
+
h.pause();
|
|
479
|
+
if (!paused) {
|
|
480
|
+
// Live playing snapshot always wins (beats a stale ended latch).
|
|
481
|
+
setHandoff({ time, muted, playing: true });
|
|
482
|
+
} else if (endedLatchRef.current && atEnd) {
|
|
483
|
+
setHandoff(null); // finished — plain poster, no continuation
|
|
484
|
+
} else if (time < 1) {
|
|
485
|
+
setHandoff(null); // degenerate close — keep hover preview enabled
|
|
486
|
+
} else {
|
|
487
|
+
setHandoff({ time, muted, playing: false });
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
commitOpen(false);
|
|
491
|
+
}, [isYouTube, commitOpen, openTheater]);
|
|
492
|
+
|
|
493
|
+
const onCardFallbackChange = useCallback((s: VideoMutedFallbackState) => {
|
|
494
|
+
setCardFallback(s);
|
|
495
|
+
setCardMuted(s.muted); // sync the toggle with fallback / chrome unmutes
|
|
496
|
+
}, []);
|
|
497
|
+
|
|
498
|
+
// Seed transport state whenever a card player (re)starts.
|
|
499
|
+
useEffect(() => {
|
|
500
|
+
if (resumeMode) { setCardMuted(handoff?.muted ?? false); setCardPaused(handoff?.playing === false); }
|
|
501
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
502
|
+
}, [resumeMode]);
|
|
503
|
+
// A fresh hover starts a fresh preview — clear the paused flag, and re-seed
|
|
504
|
+
// the mute toggle from the user's standing intent so an explicit mute is not
|
|
505
|
+
// silently undone by the hover-start path (which unmutes the element).
|
|
506
|
+
useEffect(() => {
|
|
507
|
+
if (hovered) { setCardPaused(false); setCardMuted(userMutedRef.current); }
|
|
508
|
+
}, [hovered]);
|
|
509
|
+
|
|
510
|
+
const dismiss = useCallback((e: React.MouseEvent) => {
|
|
511
|
+
e.stopPropagation();
|
|
512
|
+
pausePreviewNow();
|
|
513
|
+
if (dismissEnabled) dismissWalkthrough(storageKey, dismissalId);
|
|
514
|
+
setDismissed(true);
|
|
515
|
+
}, [dismissEnabled, storageKey, dismissalId, pausePreviewNow]);
|
|
516
|
+
|
|
517
|
+
// Card controls act on whichever player is live (resume continuation or the
|
|
518
|
+
// hover preview). The internal glyph is unreachable under the activation
|
|
519
|
+
// overlay (hideMutedBadge), so these are the only affordances.
|
|
520
|
+
const activeHandle = () => (resumeMode ? resumeHandleRef.current : previewHandleRef.current);
|
|
521
|
+
|
|
522
|
+
/** Big centered glyph — the muted-fallback prompt (unmute, or play when even
|
|
523
|
+
* muted autoplay was blocked). Unchanged bite grammar. */
|
|
524
|
+
const onUnmuteOrPlay = useCallback((e: React.MouseEvent) => {
|
|
525
|
+
e.stopPropagation();
|
|
526
|
+
const h = activeHandle();
|
|
527
|
+
if (!h) return;
|
|
528
|
+
try {
|
|
529
|
+
// Branch on the LABEL this button is currently rendering, so the action
|
|
530
|
+
// always matches the words. Guarding the unmute on `!userMutedRef.current`
|
|
531
|
+
// (the previous attempt) turned the "Unmute" case into a no-op — and
|
|
532
|
+
// because the corner mute toggle hides while this glyph is up, that left
|
|
533
|
+
// the card with NO way to restore sound. A control that hides itself on
|
|
534
|
+
// use is a dead end; see the state-model note at the top of this file.
|
|
535
|
+
// Recomputed here, NOT read from the render-scoped `controlIsPlay`
|
|
536
|
+
// (declared below this callback — reading it would repeat the stale
|
|
537
|
+
// closure bug that silently restarted the theater at 0:00).
|
|
538
|
+
const actionIsPlay = cardFallback.blocked || cardPaused;
|
|
539
|
+
if (actionIsPlay) {
|
|
540
|
+
void h.play();
|
|
541
|
+
setCardPaused(false);
|
|
542
|
+
} else {
|
|
543
|
+
h.setMuted(false);
|
|
544
|
+
setCardMuted(false);
|
|
545
|
+
userMutedRef.current = false;
|
|
546
|
+
setUserMuted(false);
|
|
547
|
+
void h.play();
|
|
548
|
+
setCardPaused(false);
|
|
549
|
+
}
|
|
550
|
+
} catch { /* ignore */ }
|
|
551
|
+
}, [resumeMode, cardFallback.blocked, cardPaused]);
|
|
552
|
+
|
|
553
|
+
/** Mute TOGGLE — stays on screen in both states so the user can come back. */
|
|
554
|
+
const onToggleMute = useCallback((e: React.MouseEvent) => {
|
|
555
|
+
e.stopPropagation();
|
|
556
|
+
const h = activeHandle();
|
|
557
|
+
if (!h) return;
|
|
558
|
+
// Live element, not the `cardMuted` snapshot: the module-level activation
|
|
559
|
+
// waiter can unmute between this button's pointerdown and its click, which
|
|
560
|
+
// made the first press of a button labelled "Unmute" mute instead.
|
|
561
|
+
// Read the LIVE element, never `cardMuted`: the two diverge whenever the
|
|
562
|
+
// player's own chrome changed mute without telling us. (`getMuted()`
|
|
563
|
+
// returns `false` rather than nullish on a torn-down element, so a `??`
|
|
564
|
+
// fallback here would silently invert the button — hence the null guard
|
|
565
|
+
// above rather than a default.)
|
|
566
|
+
const next = !h.getMuted();
|
|
567
|
+
try {
|
|
568
|
+
h.setMuted(next);
|
|
569
|
+
if (!next && !cardPaused) void h.play(); // never override an explicit pause
|
|
570
|
+
setCardMuted(next);
|
|
571
|
+
userMutedRef.current = next;
|
|
572
|
+
setUserMuted(next);
|
|
573
|
+
} catch { /* ignore */ }
|
|
574
|
+
}, [resumeMode, cardMuted, cardPaused]);
|
|
575
|
+
|
|
576
|
+
/** Play/pause TOGGLE. Deliberately does NOT tear down the resume player
|
|
577
|
+
* (an earlier version cleared the handoff, which unmounted the mini player
|
|
578
|
+
* and made the controls vanish with no way back). */
|
|
579
|
+
const onTogglePlay = useCallback((e: React.MouseEvent) => {
|
|
580
|
+
e.stopPropagation();
|
|
581
|
+
const h = activeHandle();
|
|
582
|
+
if (!h) return;
|
|
583
|
+
try {
|
|
584
|
+
// Same predicate the label renders from (`controlIsPlay`). Branching on
|
|
585
|
+
// raw `cardPaused` meant that in the blocked state the button read "Play"
|
|
586
|
+
// but took the pause branch, so the first press did nothing.
|
|
587
|
+
if (cardFallback.blocked || cardPaused) { void h.play(); setCardPaused(false); }
|
|
588
|
+
else { h.pause(); setCardPaused(true); }
|
|
589
|
+
} catch { /* ignore */ }
|
|
590
|
+
}, [resumeMode, cardFallback.blocked, cardPaused]);
|
|
591
|
+
|
|
592
|
+
const summaryTitle = video?.title || undefined;
|
|
593
|
+
|
|
594
|
+
// Already proxied by the host (the hub's DAL applies the prefix AND its
|
|
595
|
+
// skip-list). Threading a prefix through the widget meant each host restated
|
|
596
|
+
// it and silently dropped the skip-list.
|
|
597
|
+
const presenterAvatarSrc = video?.presenterAvatarUrl ?? null;
|
|
598
|
+
|
|
599
|
+
const embedUrlKey = useMemo(() => `${video?.mainVideoUrl ?? ''}|${video?.youtubeUrl ?? ''}`, [video?.mainVideoUrl, video?.youtubeUrl]);
|
|
600
|
+
|
|
601
|
+
if (!video || (!video.mainVideoUrl && !video.youtubeUrl)) return null;
|
|
602
|
+
|
|
603
|
+
// The collapsed card is suppressed by the appear-delay mount gate and by
|
|
604
|
+
// dismissal — but the THEATER (Dialog) is NOT. A host that controls
|
|
605
|
+
// `open={true}` must be able to force the theater even before the card has
|
|
606
|
+
// appeared or after it was dismissed, so the gate lives on the card only.
|
|
607
|
+
const showCard = mounted && !dismissed;
|
|
608
|
+
|
|
609
|
+
// Gate on `!resumeMode`, NOT `!handoff`: a non-playing handoff is only a
|
|
610
|
+
// remembered timestamp. Gating on `handoff` froze the card as an inert poster
|
|
611
|
+
// after closing a paused theater (no hover preview, no transport, no way back
|
|
612
|
+
// except reopening) for the rest of the session.
|
|
613
|
+
const cardActive = !open && previewAllowed && hovered && !resumeMode;
|
|
614
|
+
|
|
615
|
+
// ONE derived mode drives every branch below. Deriving a single discriminant
|
|
616
|
+
// (instead of testing raw booleans at each call site) is what keeps the card
|
|
617
|
+
// consistent: 'resume' and 'preview' are mutually exclusive by construction,
|
|
618
|
+
// so a player can never be half-mounted or a control half-shown.
|
|
619
|
+
const cardMode: 'resume' | 'preview' | 'poster' =
|
|
620
|
+
resumeMode ? 'resume' : cardActive ? 'preview' : 'poster';
|
|
621
|
+
|
|
622
|
+
// Big centered glyph = the muted-fallback prompt (bite grammar), nothing else.
|
|
623
|
+
// Gated on an actual player: `cardFallback` is only ever rewritten by a
|
|
624
|
+
// mounted one, so after it unmounts (e.g. the resume clip ended) a stale
|
|
625
|
+
// `muted:true` left an orphan glyph that played from 0 with sound and no
|
|
626
|
+
// transport controls.
|
|
627
|
+
cardModeRef.current = cardMode;
|
|
628
|
+
|
|
629
|
+
const showBigUnmute = cardFallback.muted && cardMode !== 'poster';
|
|
630
|
+
// "Play" whenever the action will start playback: autoplay was blocked, or
|
|
631
|
+
// the user paused. Otherwise the label understated what the button does.
|
|
632
|
+
const controlIsPlay = cardFallback.blocked || cardPaused;
|
|
633
|
+
// Transport toggles stay visible for as long as a card player is mounted —
|
|
634
|
+
// in BOTH states of each toggle, so muting/pausing is always reversible.
|
|
635
|
+
const showPlaybackControls = cardMode !== 'poster';
|
|
636
|
+
|
|
637
|
+
// --- collapsed card (div root + overlay button + sibling X/unmute controls) ---
|
|
638
|
+
const collapsed = (
|
|
639
|
+
<div
|
|
640
|
+
className={cn(
|
|
641
|
+
// `group/card` is the SHARED hover contract: VideoHoverPreviewSurface's
|
|
642
|
+
// badge accents off it. It must live on an element that actually
|
|
643
|
+
// receives pointer events — the media layer is pointer-events-none, so
|
|
644
|
+
// a group inside it can never match :hover.
|
|
645
|
+
'group/card pointer-events-auto relative overflow-hidden rounded-lg border border-ods-border bg-ods-card shadow-2xl',
|
|
646
|
+
'aspect-video w-60 sm:w-80 transition-opacity duration-200',
|
|
647
|
+
footerHidden ? 'opacity-0 pointer-events-none' : 'opacity-100',
|
|
648
|
+
className,
|
|
649
|
+
)}
|
|
650
|
+
// `mouse` only: pointerenter also fires for touch, which started a
|
|
651
|
+
// preview on every tap. Focus mirrors hover so keyboard users can reach
|
|
652
|
+
// the transport controls at all.
|
|
653
|
+
onPointerEnter={e => { if (e.pointerType === 'mouse' && previewAllowed && !resumeMode) setHovered(true); }}
|
|
654
|
+
onPointerLeave={e => { if (e.pointerType === 'mouse') setHovered(false); }}
|
|
655
|
+
// Re-arm hover after a close that left the pointer parked on the card.
|
|
656
|
+
onPointerMove={e => { if (e.pointerType === 'mouse' && previewAllowed && !resumeMode && !hovered) setHovered(true); }}
|
|
657
|
+
// Keyboard intent only. Chrome focuses buttons on click, so a raw focus
|
|
658
|
+
// mirror autostarted an unmuted preview with the pointer nowhere near
|
|
659
|
+
// the card. `:focus-visible` alone is NOT enough: Radix restores focus
|
|
660
|
+
// to the hit layer on close, and after an Escape-close that restored
|
|
661
|
+
// focus IS :focus-visible — the guard admitted precisely the case it was
|
|
662
|
+
// written to block, so closing with Escape restarted audio in the
|
|
663
|
+
// corner. Hence the explicit just-closed latch.
|
|
664
|
+
onFocusCapture={e => {
|
|
665
|
+
const el = e.target as HTMLElement;
|
|
666
|
+
// `matches` THROWS SyntaxError on engines that don't know
|
|
667
|
+
// :focus-visible (Safari < 15.4) — that would escape a React handler.
|
|
668
|
+
let keyboard = false;
|
|
669
|
+
try { keyboard = el.matches?.(':focus-visible') ?? false; } catch { keyboard = false; }
|
|
670
|
+
// Consume the latch: the FIRST focus after a close is Radix returning
|
|
671
|
+
// focus to this element, never the user arriving at it.
|
|
672
|
+
if (justClosedRef.current) {
|
|
673
|
+
justClosedRef.current = false;
|
|
674
|
+
if (justClosedTimerRef.current) {
|
|
675
|
+
clearTimeout(justClosedTimerRef.current);
|
|
676
|
+
justClosedTimerRef.current = null;
|
|
677
|
+
}
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
if (previewAllowed && !resumeMode && keyboard) setHovered(true);
|
|
681
|
+
}}
|
|
682
|
+
onBlurCapture={e => { if (!e.currentTarget.contains(e.relatedTarget as Node | null)) setHovered(false); }}
|
|
683
|
+
>
|
|
684
|
+
{/* Media layer is CLICK-TRANSPARENT: a paused/idle player must never
|
|
685
|
+
swallow the click. Every click that isn't on a control falls through
|
|
686
|
+
to the activation overlay below and opens the theater. */}
|
|
687
|
+
<div className="pointer-events-none absolute inset-0">
|
|
688
|
+
{/* ONE player component for BOTH modes. The card used to render a
|
|
689
|
+
bespoke resume player next to the shared preview surface, and that
|
|
690
|
+
duplication is exactly why desktop (preview) and mobile (poster/
|
|
691
|
+
resume) diverged. Same component, same props, one behaviour. */}
|
|
692
|
+
<VideoHoverPreviewSurface
|
|
693
|
+
key={`card-${embedUrlKey}`}
|
|
694
|
+
// File player only. The theater resolves YouTube-wins; the card must
|
|
695
|
+
// not mount MuxPlayer on a youtube.com URL (it renders the poster).
|
|
696
|
+
url={video.mainVideoUrl || ''}
|
|
697
|
+
posterUrl={video.posterUrl}
|
|
698
|
+
active={cardActive}
|
|
699
|
+
isClone={false}
|
|
700
|
+
badge="play"
|
|
701
|
+
fit="cover"
|
|
702
|
+
posterSizes="320px"
|
|
703
|
+
preload="none"
|
|
704
|
+
hideMutedBadge
|
|
705
|
+
onMutedFallbackChange={onCardFallbackChange}
|
|
706
|
+
previewHandleRef={cardMode === 'resume' ? resumeHandleRef : previewHandleRef}
|
|
707
|
+
mutedIntent={userMuted}
|
|
708
|
+
continuation={cardMode === 'resume'}
|
|
709
|
+
// `!footerHidden`: mounting with autoplay and pausing in an effect
|
|
710
|
+
// loses to MuxPlayer re-issuing play when media is ready, which left
|
|
711
|
+
// audio running behind an invisible, pointer-inert card.
|
|
712
|
+
autoPlay={cardMode === 'resume' && handoff?.playing !== false && !footerHidden}
|
|
713
|
+
startTime={cardMode === 'resume' ? handoff!.time : undefined}
|
|
714
|
+
startMuted={cardMode === 'resume' ? handoff!.muted : false}
|
|
715
|
+
onEnded={cardMode === 'resume' ? () => setHandoff(null) : () => setCardPaused(true)}
|
|
716
|
+
/>
|
|
717
|
+
</div>
|
|
718
|
+
|
|
719
|
+
{/* CONTENTLESS full-card hit layer. Deliberately a bare <button>, NOT the
|
|
720
|
+
DS <Button>: that component is an inline-flex, content-sized,
|
|
721
|
+
centre-aligned control, so as a full-bleed layer it collapsed around
|
|
722
|
+
its child — which dragged the title pill to the top-left (overlapping
|
|
723
|
+
the transport) and left most of the card unclickable. A hit layer has
|
|
724
|
+
no content and no typography; separating it from the label is what
|
|
725
|
+
makes both positions deterministic. */}
|
|
726
|
+
<CardHitLayer
|
|
727
|
+
label={video.title ? `${label}: ${video.title}` : label}
|
|
728
|
+
onPointerDown={pausePreviewNow}
|
|
729
|
+
onClick={openTheater}
|
|
730
|
+
className="z-20"
|
|
731
|
+
/>
|
|
732
|
+
|
|
733
|
+
{/* Title pill — its own positioned element (bottom-left), never a child
|
|
734
|
+
of the hit layer. pointer-events-none so clicks fall through to it. */}
|
|
735
|
+
<span className="pointer-events-none absolute bottom-[var(--spacing-system-xsf)] left-[var(--spacing-system-xsf)] z-20 flex max-w-[calc(100%-2*var(--spacing-system-xsf))] items-center gap-[var(--spacing-system-xsf)] rounded-full bg-ods-overlay py-[var(--spacing-system-xxs)] pl-[var(--spacing-system-xxs)] pr-[var(--spacing-system-sf)] text-h6 text-ods-text-primary">
|
|
736
|
+
{presenterAvatarSrc ? (
|
|
737
|
+
<SquareAvatar src={presenterAvatarSrc} alt="" sizePx={20} variant="round" className="shrink-0 border-0" />
|
|
738
|
+
) : (
|
|
739
|
+
<VideoPlayBadge size="sm" className="h-5 w-5 shrink-0" />
|
|
740
|
+
)}
|
|
741
|
+
<span className="truncate">{label}</span>
|
|
742
|
+
</span>
|
|
743
|
+
|
|
744
|
+
{/* BIG centred glyph — the muted-fallback prompt (bite grammar). It IS
|
|
745
|
+
interactive, but ONLY exists while the card is muted, so the two
|
|
746
|
+
intents never compete: muted → this button unmutes; once unmuted it
|
|
747
|
+
unmounts and the same click lands on the hit layer and opens the
|
|
748
|
+
theater. `size="icon-glyph"` keeps the glyph at its own 56px instead
|
|
749
|
+
of the DS default `[&_svg]:h-5`; `variant="glyph"` means no button
|
|
750
|
+
surface paints behind a glyph that already carries its own scrim. */}
|
|
751
|
+
{showBigUnmute && (
|
|
752
|
+
<Button
|
|
753
|
+
variant="glyph"
|
|
754
|
+
size="icon-glyph"
|
|
755
|
+
aria-label={controlIsPlay ? 'Play' : 'Unmute'}
|
|
756
|
+
title={controlIsPlay ? 'Play' : 'Unmute'}
|
|
757
|
+
onPointerDown={e => e.stopPropagation()}
|
|
758
|
+
onClick={onUnmuteOrPlay}
|
|
759
|
+
className="absolute inset-0 z-30 m-auto"
|
|
760
|
+
>
|
|
761
|
+
{controlIsPlay ? <VideoPlayBadge /> : <VideoUnmuteGlyph />}
|
|
762
|
+
</Button>
|
|
763
|
+
)}
|
|
764
|
+
|
|
765
|
+
{/* Transport toggles — mute/unmute + play/pause. Rendered in BOTH states
|
|
766
|
+
(never self-hiding) so every action is reversible.
|
|
767
|
+
CORNER MAP (each control owns exactly one corner, nothing overlaps):
|
|
768
|
+
top-left = transport top-right = dismiss
|
|
769
|
+
bottom-left = title pill (presenter avatar lives INSIDE it) */}
|
|
770
|
+
{showPlaybackControls && (
|
|
771
|
+
<div className="absolute left-[var(--spacing-system-xsf)] top-[var(--spacing-system-xsf)] z-30 flex items-center gap-[var(--spacing-system-xxs)]">
|
|
772
|
+
{/* Hidden while the big centre glyph owns unmuting, so the two are
|
|
773
|
+
never on screen with the same label and different behaviour. */}
|
|
774
|
+
<Button
|
|
775
|
+
variant="overlay"
|
|
776
|
+
size="icon-sm"
|
|
777
|
+
aria-label={cardMuted ? 'Unmute' : 'Mute'}
|
|
778
|
+
title={cardMuted ? 'Unmute' : 'Mute'}
|
|
779
|
+
onPointerDown={e => e.stopPropagation()}
|
|
780
|
+
onClick={onToggleMute}
|
|
781
|
+
className={cn(showBigUnmute && !controlIsPlay && 'hidden')}
|
|
782
|
+
>
|
|
783
|
+
{cardMuted ? <VolumeXmarkIcon /> : <VolumeUpIcon />}
|
|
784
|
+
</Button>
|
|
785
|
+
<Button
|
|
786
|
+
variant="overlay"
|
|
787
|
+
size="icon-sm"
|
|
788
|
+
aria-label={controlIsPlay ? 'Play' : 'Pause'}
|
|
789
|
+
title={controlIsPlay ? 'Play' : 'Pause'}
|
|
790
|
+
onPointerDown={e => e.stopPropagation()}
|
|
791
|
+
onClick={onTogglePlay}
|
|
792
|
+
>
|
|
793
|
+
{controlIsPlay ? <PlayIcon /> : <PauseIcon />}
|
|
794
|
+
</Button>
|
|
795
|
+
</div>
|
|
796
|
+
)}
|
|
797
|
+
|
|
798
|
+
{/* dismiss (highest) */}
|
|
799
|
+
{dismissEnabled && (
|
|
800
|
+
<Button
|
|
801
|
+
variant="overlay"
|
|
802
|
+
size="icon-sm"
|
|
803
|
+
aria-label="Dismiss video"
|
|
804
|
+
onPointerDown={pausePreviewNow}
|
|
805
|
+
onClick={dismiss}
|
|
806
|
+
className="absolute right-[var(--spacing-system-xsf)] top-[var(--spacing-system-xsf)] z-40"
|
|
807
|
+
>
|
|
808
|
+
<XmarkIcon />
|
|
809
|
+
</Button>
|
|
810
|
+
)}
|
|
811
|
+
</div>
|
|
812
|
+
);
|
|
813
|
+
|
|
814
|
+
return (
|
|
815
|
+
<>
|
|
816
|
+
{showCard && (
|
|
817
|
+
<div className={cn('pointer-events-none fixed bottom-0 left-0 p-[var(--spacing-system-mf)]', WALKTHROUGH_Z)} style={{ paddingBottom: 'max(var(--spacing-system-mf), env(safe-area-inset-bottom))' }}>
|
|
818
|
+
{collapsed}
|
|
819
|
+
</div>
|
|
820
|
+
)}
|
|
821
|
+
|
|
822
|
+
<DialogPrimitive.Root open={open} onOpenChange={handleOpenChange}>
|
|
823
|
+
<DialogPortal>
|
|
824
|
+
<DialogOverlay />
|
|
825
|
+
<DialogPrimitive.Content
|
|
826
|
+
onOpenAutoFocus={e => {
|
|
827
|
+
// Prevent Radix's default (first focusable) and move focus to the
|
|
828
|
+
// close button so keyboard/AT users land inside the dialog.
|
|
829
|
+
e.preventDefault();
|
|
830
|
+
closeButtonRef.current?.focus();
|
|
831
|
+
}}
|
|
832
|
+
className={cn(
|
|
833
|
+
// PURE theater: just the video, centered on a dimmed backdrop.
|
|
834
|
+
// No card chrome, no padding, no summary — the 16:9 stage IS the
|
|
835
|
+
// dialog, so nothing competes with the video at any breakpoint.
|
|
836
|
+
// `158svh` is the width at which a 16:9 stage is exactly 89svh
|
|
837
|
+
// tall, so the height bound is expressed as a WIDTH cap — the
|
|
838
|
+
// stage keeps its aspect ratio and simply gets narrower on short
|
|
839
|
+
// viewports, instead of overflowing a fixed centred box that
|
|
840
|
+
// clips equally off the top and bottom (which put the close
|
|
841
|
+
// button off-screen on 1366x768 laptops and landscape phones).
|
|
842
|
+
// Two declarations on purpose: an engine without `svh` (Safari
|
|
843
|
+
// < 15.4) treats the whole min() as invalid, and on a `fixed`
|
|
844
|
+
// element that falls back to width:auto and the stage collapses.
|
|
845
|
+
// The first rule is the safe floor; the second wins where svh
|
|
846
|
+
// parses.
|
|
847
|
+
'fixed left-1/2 top-1/2 z-[9999] max-w-none -translate-x-1/2 -translate-y-1/2',
|
|
848
|
+
'w-[min(92vw,1400px)] w-[min(92vw,1400px,158svh)]',
|
|
849
|
+
'focus:outline-none',
|
|
850
|
+
)}
|
|
851
|
+
>
|
|
852
|
+
<DialogPrimitive.Title className="sr-only">{summaryTitle || 'Walkthrough video'}</DialogPrimitive.Title>
|
|
853
|
+
<Video
|
|
854
|
+
kind={video.youtubeUrl ? 'youtube' : 'file'}
|
|
855
|
+
url={(video.youtubeUrl || video.mainVideoUrl) as string}
|
|
856
|
+
poster={video.posterUrl}
|
|
857
|
+
captionsUrl={video.captionsUrl}
|
|
858
|
+
title={summaryTitle}
|
|
859
|
+
layout="wide"
|
|
860
|
+
startTime={theaterStart.time}
|
|
861
|
+
playerHandleRef={theaterHandleRef}
|
|
862
|
+
// Muted still means PLAYING: without this a deliberately muted
|
|
863
|
+
// card opened a paused theater, breaking parity with the
|
|
864
|
+
// unmuted path.
|
|
865
|
+
autoPlay={theaterStart.muted}
|
|
866
|
+
autoPlayUnmuted={!theaterStart.muted}
|
|
867
|
+
startMuted={theaterStart.muted}
|
|
868
|
+
autoActivate
|
|
869
|
+
suspended={suspended}
|
|
870
|
+
onMutedFallbackChange={st => { theaterForcedMuteRef.current = st.muted; }}
|
|
871
|
+
onEnded={() => { endedLatchRef.current = true; }}
|
|
872
|
+
/>
|
|
873
|
+
{/* Radix `asChild` so the shared Button IS the close trigger (same
|
|
874
|
+
pattern as every other dialog close in the lib). */}
|
|
875
|
+
<DialogPrimitive.Close asChild>
|
|
876
|
+
<Button
|
|
877
|
+
ref={closeButtonRef}
|
|
878
|
+
variant="overlay"
|
|
879
|
+
size="icon-sm"
|
|
880
|
+
aria-label="Close"
|
|
881
|
+
className="absolute right-[var(--spacing-system-sf)] top-[var(--spacing-system-sf)] z-10"
|
|
882
|
+
>
|
|
883
|
+
<XmarkIcon />
|
|
884
|
+
</Button>
|
|
885
|
+
</DialogPrimitive.Close>
|
|
886
|
+
</DialogPrimitive.Content>
|
|
887
|
+
</DialogPortal>
|
|
888
|
+
</DialogPrimitive.Root>
|
|
889
|
+
</>
|
|
890
|
+
);
|
|
891
|
+
}
|