@flamingo-stack/openframe-frontend-core 0.0.730 → 0.0.732
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/aicpa-soc2-logo-icon-MLDYZA5S.cjs +7 -0
- package/dist/build/export-map.json +1 -1
- package/dist/{chunk-SKDQ2SLJ.cjs → chunk-22ZK7BA3.cjs} +1345 -1127
- package/dist/{chunk-5RMCAY6Y.cjs → chunk-27TMRMOM.cjs} +6 -4
- package/dist/{chunk-P4YHBHP4.cjs → chunk-3BLBWZ6A.cjs} +7 -7
- package/dist/{chunk-2T2TH5DF.cjs → chunk-52HFGZCI.cjs} +14 -12
- package/dist/{chunk-L7Y7KDTM.cjs → chunk-5HMGK3N6.cjs} +20 -20
- package/dist/{chunk-66QTEDJL.cjs → chunk-5HSMTSHU.cjs} +28 -28
- package/dist/{chunk-WTURHKM2.cjs → chunk-6HHUU56Y.cjs} +16 -16
- package/dist/{chunk-AIW4KZQT.cjs → chunk-6KDGIOLP.cjs} +89 -89
- package/dist/{chunk-4XQXTQUK.cjs → chunk-6N5NQRQA.cjs} +8 -6
- package/dist/{chunk-5KLNEFZZ.cjs → chunk-7DUXR4HL.cjs} +3 -3
- package/dist/chunk-AEVGCB3Y.cjs +417 -0
- package/dist/{chunk-A7T3P5Q5.cjs → chunk-BDTX2OEE.cjs} +32 -32
- package/dist/{chunk-Q5GR7LKV.cjs → chunk-BS4EZZCN.cjs} +15 -15
- package/dist/{chunk-JNDR5N3T.cjs → chunk-C5NERNE6.cjs} +93 -93
- package/dist/{chunk-UHJG2BDI.cjs → chunk-CC3LINHF.cjs} +98 -50
- package/dist/{chunk-FRET5WH3.cjs → chunk-CKR42OBK.cjs} +4 -2
- package/dist/{chunk-GJPAS4RJ.cjs → chunk-CT342EJU.cjs} +6 -4
- package/dist/{chunk-PGFR4CDB.cjs → chunk-CUF3IYIB.cjs} +40 -40
- package/dist/{chunk-YHFXQ5OR.cjs → chunk-E3N24573.cjs} +9 -9
- package/dist/{chunk-24KWUFWA.cjs → chunk-G37LT2ID.cjs} +43 -39
- package/dist/{chunk-UG7Q3HQH.cjs → chunk-GID62YEN.cjs} +3 -3
- package/dist/{chunk-NDHQ7JLZ.cjs → chunk-GKHWXI25.cjs} +11 -15
- package/dist/chunk-HELVKMRG.cjs +28 -0
- package/dist/{chunk-KMMV562E.cjs → chunk-HH4BMYDX.cjs} +32 -379
- package/dist/{chunk-FGA6T2XT.cjs → chunk-I46MQ4R5.cjs} +82 -82
- package/dist/{chunk-YJXUJWHN.cjs → chunk-J7SZOZOS.cjs} +28 -26
- package/dist/{chunk-NXCV35RR.cjs → chunk-KLXHCNDC.cjs} +3 -3
- package/dist/{chunk-6KD2XUQZ.cjs → chunk-LRQITIEC.cjs} +28 -26
- package/dist/chunk-MANNIJ42.cjs +967 -0
- package/dist/{chunk-2V62Y67F.cjs → chunk-MEDDKM3A.cjs} +4 -4
- package/dist/{chunk-CLA3UGKT.cjs → chunk-MVZ6XFPK.cjs} +12 -12
- package/dist/{chunk-TI2IFCZG.cjs → chunk-N466AJGN.cjs} +17 -17
- package/dist/{chunk-TW2SES5W.cjs → chunk-NCGAP3U4.cjs} +4 -2
- package/dist/{chunk-DINGZJ37.cjs → chunk-NYY25425.cjs} +2 -2
- package/dist/{chunk-HVZBIXCZ.cjs → chunk-O2RPIGSG.cjs} +6 -4
- package/dist/{chunk-YRZB4BMZ.cjs → chunk-OI7VCXYU.cjs} +48 -50
- package/dist/{chunk-XA436AER.cjs → chunk-OJ3XU3YZ.cjs} +4 -4
- package/dist/{chunk-UYKL2AXE.cjs → chunk-OKW5OAFJ.cjs} +8 -6
- package/dist/{chunk-6NKLR5QN.cjs → chunk-PRWBVIWW.cjs} +59 -59
- package/dist/chunk-PX7FY655.cjs +114 -0
- package/dist/{chunk-CD5YBUJK.cjs → chunk-QMTC3BK3.cjs} +23 -21
- package/dist/{chunk-VQMWBQUJ.cjs → chunk-R4QMR6S4.cjs} +14 -12
- package/dist/{chunk-NB5BYOTM.cjs → chunk-RC32WBPL.cjs} +6 -4
- package/dist/{chunk-CDLDAQBA.cjs → chunk-RIXGLS5Y.cjs} +7 -7
- package/dist/{chunk-2I3XU2IB.cjs → chunk-SJRR745G.cjs} +63 -60
- package/dist/{chunk-XSPL7RXL.cjs → chunk-TLDCSTTQ.cjs} +12 -12
- package/dist/{chunk-3YHVQ34L.cjs → chunk-XOQ2UUVS.cjs} +93 -95
- package/dist/{chunk-YPM3ALIW.cjs → chunk-YGKVHSNQ.cjs} +42 -42
- package/dist/{chunk-4NFVFTD6.cjs → chunk-ZWMPAFGB.cjs} +238 -238
- package/dist/{chunk-GVTCHXW2.cjs → chunk-ZWUIRQNY.cjs} +28 -26
- package/dist/clickup-icon-3SETH2MM.cjs +7 -0
- package/dist/clickup-logo-icon-DJKBEFMR.cjs +7 -0
- package/dist/{cloud-tenants-VV6UQE7J.cjs → cloud-tenants-ILRE74JY.cjs} +36 -36
- package/dist/components/agent-identity.d.ts +8 -0
- package/dist/components/agent-identity.d.ts.map +1 -1
- package/dist/components/agent-identity.js +13 -1
- package/dist/components/case-studies/index.cjs +49 -49
- package/dist/components/chat/entity-cards/entity-portrait-card.js +1 -1
- package/dist/components/chat/index.cjs +30 -30
- package/dist/components/chat/types/entities/program-types.d.ts +1 -23
- package/dist/components/chat/types/entities/program-types.d.ts.map +1 -1
- package/dist/components/chat/types/entities/program-types.js +1 -9
- package/dist/components/chat/utils/icon-loading.d.ts +35 -2
- package/dist/components/chat/utils/icon-loading.d.ts.map +1 -1
- package/dist/components/chat/utils/icon-loading.js +31 -9
- package/dist/components/contact/index.cjs +37 -37
- package/dist/components/docs/index.cjs +40 -40
- package/dist/components/embeds/index.cjs +37 -37
- package/dist/components/faq/index.cjs +37 -37
- package/dist/components/features/index.cjs +38 -36
- package/dist/components/features/video-bites-shared.cjs +8 -0
- package/dist/components/features/video-bites-shared.d.ts +25 -0
- package/dist/components/features/video-bites-shared.d.ts.map +1 -1
- package/dist/components/features/video-bites-shared.js +8 -0
- package/dist/components/help-center-pages/index.cjs +152 -152
- package/dist/components/icons-v2-generated/brand-logos/clickup-icon.d.ts +1 -1
- package/dist/components/icons-v2-generated/brand-logos/clickup-icon.d.ts.map +1 -1
- package/dist/components/icons-v2-generated/brand-logos/clickup-icon.js +6 -4
- package/dist/components/icons-v2-generated/brand-logos/clickup-logo-icon.d.ts +1 -1
- package/dist/components/icons-v2-generated/brand-logos/clickup-logo-icon.d.ts.map +1 -1
- package/dist/components/icons-v2-generated/brand-logos/clickup-logo-icon.js +6 -4
- package/dist/components/icons-v2-generated/brand-logos/elestio-logo-grey-icon.d.ts +1 -1
- package/dist/components/icons-v2-generated/brand-logos/elestio-logo-grey-icon.d.ts.map +1 -1
- package/dist/components/icons-v2-generated/brand-logos/elestio-logo-grey-icon.js +4 -2
- package/dist/components/icons-v2-generated/brand-logos/elestio-logo-icon.d.ts +1 -1
- package/dist/components/icons-v2-generated/brand-logos/elestio-logo-icon.d.ts.map +1 -1
- package/dist/components/icons-v2-generated/brand-logos/elestio-logo-icon.js +14 -12
- package/dist/components/icons-v2-generated/brand-logos/gemini-logo-icon.d.ts +1 -1
- package/dist/components/icons-v2-generated/brand-logos/gemini-logo-icon.d.ts.map +1 -1
- package/dist/components/icons-v2-generated/brand-logos/gemini-logo-icon.js +28 -26
- package/dist/components/icons-v2-generated/brand-logos/google-gemini-icon.d.ts +1 -1
- package/dist/components/icons-v2-generated/brand-logos/google-gemini-icon.d.ts.map +1 -1
- package/dist/components/icons-v2-generated/brand-logos/google-gemini-icon.js +28 -26
- package/dist/components/icons-v2-generated/brand-logos/google-gemini-logo-icon.d.ts +1 -1
- package/dist/components/icons-v2-generated/brand-logos/google-gemini-logo-icon.d.ts.map +1 -1
- package/dist/components/icons-v2-generated/brand-logos/google-gemini-logo-icon.js +28 -26
- package/dist/components/icons-v2-generated/brand-logos/google-sheets-icon.d.ts +1 -1
- package/dist/components/icons-v2-generated/brand-logos/google-sheets-icon.d.ts.map +1 -1
- package/dist/components/icons-v2-generated/brand-logos/google-sheets-icon.js +4 -2
- package/dist/components/icons-v2-generated/brand-logos/meshcentral-logo-grey-icon.d.ts +1 -1
- package/dist/components/icons-v2-generated/brand-logos/meshcentral-logo-grey-icon.d.ts.map +1 -1
- package/dist/components/icons-v2-generated/brand-logos/meshcentral-logo-grey-icon.js +8 -6
- package/dist/components/icons-v2-generated/brand-logos/meshcentral-logo-icon.d.ts +1 -1
- package/dist/components/icons-v2-generated/brand-logos/meshcentral-logo-icon.d.ts.map +1 -1
- package/dist/components/icons-v2-generated/brand-logos/meshcentral-logo-icon.js +8 -6
- package/dist/components/icons-v2-generated/brand-logos/winget-logo-icon.d.ts +1 -1
- package/dist/components/icons-v2-generated/brand-logos/winget-logo-icon.d.ts.map +1 -1
- package/dist/components/icons-v2-generated/brand-logos/winget-logo-icon.js +14 -12
- package/dist/components/icons-v2-generated/compliance-logos/aicpa-soc2-logo-icon.d.ts +1 -1
- package/dist/components/icons-v2-generated/compliance-logos/aicpa-soc2-logo-icon.d.ts.map +1 -1
- package/dist/components/icons-v2-generated/compliance-logos/aicpa-soc2-logo-icon.js +6 -4
- package/dist/components/icons-v2-generated/compliance-logos/eu-logo-icon.d.ts +1 -1
- package/dist/components/icons-v2-generated/compliance-logos/eu-logo-icon.d.ts.map +1 -1
- package/dist/components/icons-v2-generated/compliance-logos/eu-logo-icon.js +11 -15
- package/dist/components/icons-v2-generated/icon-names.d.ts +5 -0
- package/dist/components/icons-v2-generated/icon-names.d.ts.map +1 -0
- package/dist/components/icons-v2-generated/icon-names.js +10 -0
- package/dist/components/icons-v2-generated/index.cjs +15 -15
- package/dist/components/icons-v2-generated/interface/empty-vendor-icon.d.ts +1 -1
- package/dist/components/icons-v2-generated/interface/empty-vendor-icon.d.ts.map +1 -1
- package/dist/components/icons-v2-generated/interface/empty-vendor-icon.js +6 -4
- package/dist/components/index.cjs +103 -96
- package/dist/components/meeting-scheduler/index.cjs +98 -98
- package/dist/components/navigation/index.cjs +39 -39
- package/dist/components/onboarding-guides/index.cjs +39 -39
- package/dist/components/product-demo/index.cjs +8 -8
- package/dist/components/product-demo/screens/remote-desktop-picture.d.ts.map +1 -1
- package/dist/components/product-demo/screens/remote-desktop-picture.js +2 -1
- package/dist/components/related-content/index.cjs +10 -10
- package/dist/components/shared/product-release/product-release-card.js +1 -1
- package/dist/components/tickets/index.cjs +40 -40
- package/dist/components/ui/device-wallpaper.d.ts +7 -0
- package/dist/components/ui/device-wallpaper.d.ts.map +1 -0
- package/dist/components/ui/device-wallpaper.js +5 -0
- package/dist/components/ui/feature-list.d.ts +9 -4
- package/dist/components/ui/feature-list.d.ts.map +1 -1
- package/dist/components/ui/feature-list.js +26 -35
- package/dist/components/ui/index.cjs +42 -36
- package/dist/components/ui/index.d.ts +3 -1
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +3 -1
- package/dist/components/ui/notification-stack.d.ts +33 -0
- package/dist/components/ui/notification-stack.d.ts.map +1 -0
- package/dist/components/ui/notification-stack.js +78 -0
- package/dist/components/ui/numbered-step.d.ts +29 -0
- package/dist/components/ui/numbered-step.d.ts.map +1 -0
- package/dist/components/ui/numbered-step.js +57 -0
- package/dist/components/ui/phone-frame.d.ts +14 -9
- package/dist/components/ui/phone-frame.d.ts.map +1 -1
- package/dist/components/ui/phone-frame.js +50 -6
- package/dist/components/ui/phone-glyphs/lock-camera-glyph.d.ts +5 -0
- package/dist/components/ui/phone-glyphs/lock-camera-glyph.d.ts.map +1 -0
- package/dist/components/ui/phone-glyphs/lock-camera-glyph.js +14 -0
- package/dist/components/ui/phone-glyphs/lock-flashlight-glyph.d.ts +5 -0
- package/dist/components/ui/phone-glyphs/lock-flashlight-glyph.d.ts.map +1 -0
- package/dist/components/ui/phone-glyphs/lock-flashlight-glyph.js +14 -0
- package/dist/components/ui/phone-glyphs/status-bar-glyphs.d.ts +5 -0
- package/dist/components/ui/phone-glyphs/status-bar-glyphs.d.ts.map +1 -0
- package/dist/components/ui/phone-glyphs/status-bar-glyphs.js +68 -0
- package/dist/components/ui/photo-wall.d.ts.map +1 -1
- package/dist/components/ui/photo-wall.js +4 -1
- package/dist/components/ui/snap-carousel.d.ts +5 -8
- package/dist/components/ui/snap-carousel.d.ts.map +1 -1
- package/dist/components/ui/snap-carousel.js +2 -27
- package/dist/components/ui/tab-navigation.d.ts +8 -1
- package/dist/components/ui/tab-navigation.d.ts.map +1 -1
- package/dist/components/ui/tab-navigation.js +30 -20
- package/dist/{devices-X72Z4QMT.cjs → devices-A6SFNGE4.cjs} +37 -37
- package/dist/elestio-logo-grey-icon-DV7P3JLT.cjs +7 -0
- package/dist/elestio-logo-icon-34ILAIDI.cjs +7 -0
- package/dist/{embeddable-chat-TETIRLQD.cjs → embeddable-chat-BKPYNOIM.cjs} +105 -105
- package/dist/empty-vendor-icon-XZYOZ4EP.cjs +7 -0
- package/dist/eu-logo-icon-EYOX3UC7.cjs +7 -0
- package/dist/gemini-logo-icon-DNACTHIK.cjs +7 -0
- package/dist/google-gemini-icon-TZHJZ4IQ.cjs +7 -0
- package/dist/google-gemini-logo-icon-4XQVV5NN.cjs +7 -0
- package/dist/google-sheets-icon-UAAEBTR7.cjs +7 -0
- package/dist/hooks/index.cjs +9 -9
- package/dist/hooks/ui/use-autoplay.d.ts +50 -13
- package/dist/hooks/ui/use-autoplay.d.ts.map +1 -1
- package/dist/hooks/ui/use-autoplay.js +76 -13
- package/dist/hooks/ui/use-in-view.d.ts +8 -0
- package/dist/hooks/ui/use-in-view.d.ts.map +1 -1
- package/dist/hooks/ui/use-in-view.js +21 -36
- package/dist/hooks/use-agent-identities.d.ts +6 -0
- package/dist/hooks/use-agent-identities.d.ts.map +1 -1
- package/dist/hooks/use-scenario-player.d.ts +26 -9
- package/dist/hooks/use-scenario-player.d.ts.map +1 -1
- package/dist/hooks/use-scenario-player.js +12 -9
- package/dist/{chunk-SI24RMI5.cjs → icon-loaders-OLGJZGRP.cjs} +16 -950
- package/dist/index.cjs +72 -65
- package/dist/{knowledge-HWBSPQ7M.cjs → knowledge-5ZCVKUYV.cjs} +37 -37
- package/dist/{logs-IZ4QTHPI.cjs → logs-CL4OJASB.cjs} +44 -44
- package/dist/meshcentral-logo-grey-icon-UCRYV3IC.cjs +7 -0
- package/dist/meshcentral-logo-icon-76NSLOXF.cjs +7 -0
- package/dist/{policies-VHQR7JR5.cjs → policies-7D44ZVS5.cjs} +40 -40
- package/dist/{remote-session-IR3YCXSR.cjs → remote-session-QB3CIZRM.cjs} +24 -23
- package/dist/schemas/contact-schema.cjs +31 -11
- package/dist/schemas/contact-schema.d.ts +1 -1
- package/dist/schemas/contact-schema.d.ts.map +1 -1
- package/dist/schemas/contact-schema.js +1 -1
- package/dist/schemas/meeting-booking-schema.cjs +34 -14
- package/dist/schemas/meeting-booking-schema.d.ts +1 -1
- package/dist/schemas/meeting-booking-schema.d.ts.map +1 -1
- package/dist/schemas/meeting-booking-schema.js +1 -1
- package/dist/{software-update-QLOVC6EX.cjs → software-update-AX6TZL26.cjs} +40 -40
- package/dist/{tickets-board-YONLQB6F.cjs → tickets-board-7FAV2YWF.cjs} +27 -27
- package/dist/utils/index.cjs +2004 -1959
- package/dist/winget-logo-icon-GBS342G4.cjs +7 -0
- package/package.json +1 -1
- package/src/components/agent-identity.tsx +20 -1
- package/src/components/chat/entity-cards/entity-portrait-card.tsx +1 -1
- package/src/components/chat/types/entities/program-types.ts +4 -30
- package/src/components/chat/utils/icon-loading.tsx +50 -9
- package/src/components/features/video-bites-shared.ts +30 -0
- package/src/components/icons-v2-generated/brand-logos/clickup-icon.tsx +7 -5
- package/src/components/icons-v2-generated/brand-logos/clickup-logo-icon.tsx +7 -5
- package/src/components/icons-v2-generated/brand-logos/elestio-logo-grey-icon.tsx +5 -3
- package/src/components/icons-v2-generated/brand-logos/elestio-logo-icon.tsx +15 -13
- package/src/components/icons-v2-generated/brand-logos/gemini-logo-icon.tsx +29 -27
- package/src/components/icons-v2-generated/brand-logos/google-gemini-icon.tsx +29 -27
- package/src/components/icons-v2-generated/brand-logos/google-gemini-logo-icon.tsx +29 -27
- package/src/components/icons-v2-generated/brand-logos/google-sheets-icon.tsx +5 -3
- package/src/components/icons-v2-generated/brand-logos/meshcentral-logo-grey-icon.tsx +9 -7
- package/src/components/icons-v2-generated/brand-logos/meshcentral-logo-icon.tsx +9 -7
- package/src/components/icons-v2-generated/brand-logos/winget-logo-icon.tsx +15 -13
- package/src/components/icons-v2-generated/compliance-logos/aicpa-soc2-logo-icon.tsx +92 -90
- package/src/components/icons-v2-generated/compliance-logos/eu-logo-icon.tsx +51 -52
- package/src/components/icons-v2-generated/icon-names.ts +11 -0
- package/src/components/icons-v2-generated/interface/empty-vendor-icon.tsx +7 -5
- package/src/components/product-demo/screens/remote-desktop-picture.tsx +2 -1
- package/src/components/shared/product-release/product-release-card.tsx +1 -1
- package/src/components/ui/device-wallpaper.ts +7 -0
- package/src/components/ui/feature-list.tsx +24 -30
- package/src/components/ui/index.ts +3 -1
- package/src/components/ui/notification-stack.tsx +133 -0
- package/src/components/ui/numbered-step.tsx +81 -0
- package/src/components/ui/phone-frame.tsx +83 -19
- package/src/components/ui/phone-glyphs/lock-camera-glyph.tsx +14 -0
- package/src/components/ui/phone-glyphs/lock-flashlight-glyph.tsx +14 -0
- package/src/components/ui/phone-glyphs/status-bar-glyphs.tsx +45 -0
- package/src/components/ui/photo-wall.tsx +6 -0
- package/src/components/ui/snap-carousel.tsx +6 -38
- package/src/components/ui/tab-navigation.tsx +49 -21
- package/src/hooks/ui/use-autoplay.ts +144 -27
- package/src/hooks/ui/use-in-view.ts +31 -38
- package/src/hooks/use-agent-identities.ts +6 -0
- package/src/hooks/use-scenario-player.ts +44 -20
- package/src/schemas/contact-schema.ts +1 -1
- package/src/schemas/meeting-booking-schema.ts +1 -1
- package/src/styles/fonts.css +26 -0
- package/src/styles/ods-design-tokens.css +4 -2
- package/dist/aicpa-soc2-logo-icon-CJ5JRV6J.cjs +0 -7
- package/dist/chunk-73MDSN5A.cjs +0 -99
- package/dist/chunk-BE2WAMG5.cjs +0 -54
- package/dist/chunk-LCTDHLDL.cjs +0 -43
- package/dist/clickup-icon-CBBUZUVQ.cjs +0 -7
- package/dist/clickup-logo-icon-CGDKKISS.cjs +0 -7
- package/dist/components/ui/playback-toggle.d.ts +0 -24
- package/dist/components/ui/playback-toggle.d.ts.map +0 -1
- package/dist/components/ui/playback-toggle.js +0 -32
- package/dist/elestio-logo-grey-icon-Y5OFW4OO.cjs +0 -7
- package/dist/elestio-logo-icon-X7TTQAYP.cjs +0 -7
- package/dist/empty-vendor-icon-3RKCZUUR.cjs +0 -7
- package/dist/eu-logo-icon-N7QVPGG3.cjs +0 -7
- package/dist/gemini-logo-icon-QOPGATSZ.cjs +0 -7
- package/dist/google-gemini-icon-7GWWF6O6.cjs +0 -7
- package/dist/google-gemini-logo-icon-Z3GHPMVN.cjs +0 -7
- package/dist/google-sheets-icon-7MJ3LV4P.cjs +0 -7
- package/dist/meshcentral-logo-grey-icon-3VFJQMV2.cjs +0 -7
- package/dist/meshcentral-logo-icon-MDVBWMP5.cjs +0 -7
- package/dist/winget-logo-icon-XVZJ4QDM.cjs +0 -7
- package/src/components/ui/playback-toggle.tsx +0 -53
|
@@ -58,10 +58,17 @@ export interface TabItem {
|
|
|
58
58
|
/**
|
|
59
59
|
* 0..1. Set on the ACTIVE tab, the underline stops being a selection marker
|
|
60
60
|
* and becomes a progress line: its width is this fraction of the tab, and it
|
|
61
|
-
* moves there linearly over `progressTransitionMs`.
|
|
61
|
+
* moves there linearly over `progressTransitionMs`. Start it above 0 (the
|
|
62
|
+
* first step of N is 1/N): at 0 the open tab has no underline at all. A timed, auto-advancing
|
|
62
63
|
* strip (a looping demo) uses it; `0` snaps back with no animation.
|
|
63
64
|
*/
|
|
64
65
|
progress?: number;
|
|
66
|
+
/**
|
|
67
|
+
* The tab's own colour (a CSS colour, such as the hex an agent's identity
|
|
68
|
+
* stores): its underline and its icon take it while the tab is open. Omitted,
|
|
69
|
+
* they are the platform's accent.
|
|
70
|
+
*/
|
|
71
|
+
accent?: string;
|
|
65
72
|
}
|
|
66
73
|
|
|
67
74
|
export interface TabNavigationUrlSyncOptions {
|
|
@@ -334,7 +341,34 @@ const TabBar = memo(function TabBarImpl({
|
|
|
334
341
|
// A tab that reports `progress` turns the underline into a progress line.
|
|
335
342
|
const activeProgress = tabs.find(tab => tab.id === activeTab)?.progress;
|
|
336
343
|
const hasProgress = typeof activeProgress === 'number';
|
|
337
|
-
|
|
344
|
+
// A progress line always GROWS from the start of its tab. A run that starts
|
|
345
|
+
// over (another tab opened, by the rotation or by choice, or the same tab
|
|
346
|
+
// chosen again: its progress fell back) is first drawn EMPTY, with no
|
|
347
|
+
// transition, and only then let run to its value. Without that the line
|
|
348
|
+
// either slid across from the last tab, shrank back, or appeared already
|
|
349
|
+
// part-way and sat still until the next step.
|
|
350
|
+
const target = hasProgress ? Math.max(0, Math.min(1, activeProgress)) : 1;
|
|
351
|
+
// It starts on no tab at all, so the FIRST paint is a run starting over too:
|
|
352
|
+
// a page load, the rotation and a click all begin the line the same way.
|
|
353
|
+
const [run, setRun] = useState<{ tab: string | null; progress: number }>({ tab: null, progress: 0 });
|
|
354
|
+
const startsOver = hasProgress && (run.tab !== activeTab || target < run.progress);
|
|
355
|
+
// The same run moving forward: follow it (state derived from props during render).
|
|
356
|
+
if (hasProgress && !startsOver && run.progress !== target) setRun({ tab: activeTab, progress: target });
|
|
357
|
+
useEffect(() => {
|
|
358
|
+
if (!startsOver) return undefined;
|
|
359
|
+
// Two frames: the empty line has to be painted before it is let run.
|
|
360
|
+
let second = 0;
|
|
361
|
+
const first = requestAnimationFrame(() => {
|
|
362
|
+
second = requestAnimationFrame(() => setRun({ tab: activeTab, progress: target }));
|
|
363
|
+
});
|
|
364
|
+
return () => {
|
|
365
|
+
cancelAnimationFrame(first);
|
|
366
|
+
cancelAnimationFrame(second);
|
|
367
|
+
};
|
|
368
|
+
}, [startsOver, activeTab, target]);
|
|
369
|
+
const progress = startsOver ? 0 : target;
|
|
370
|
+
// The open tab's own colour, when it has one; else the platform's accent.
|
|
371
|
+
const activeAccent = tabs.find(tab => tab.id === activeTab)?.accent;
|
|
338
372
|
const indicatorStyle: React.CSSProperties = {
|
|
339
373
|
width: `${indicator.width * progress}px`,
|
|
340
374
|
transform: `translateX(${indicator.left}px)`,
|
|
@@ -362,7 +396,7 @@ const TabBar = memo(function TabBarImpl({
|
|
|
362
396
|
`relative` makes this the offsetParent the underline is measured against. */}
|
|
363
397
|
<div
|
|
364
398
|
ref={scrollRef}
|
|
365
|
-
className="scrollbar-hide relative flex h-full items-center justify-start
|
|
399
|
+
className="scrollbar-hide relative flex h-full items-center justify-start overflow-x-auto overflow-y-hidden"
|
|
366
400
|
>
|
|
367
401
|
{tabs.map(tab => {
|
|
368
402
|
const isActive = activeTab === tab.id;
|
|
@@ -375,28 +409,19 @@ const TabBar = memo(function TabBarImpl({
|
|
|
375
409
|
onClick={() => onTabChange(tab.id)}
|
|
376
410
|
aria-label={tab.badge ? `${tab.label} (${tab.badge})` : undefined}
|
|
377
411
|
className={cn(
|
|
378
|
-
'relative flex shrink-0 cursor-pointer items-center gap-[var(--spacing-system-xxs)] p-[var(--spacing-system-m)]',
|
|
412
|
+
'group relative flex shrink-0 cursor-pointer items-center gap-[var(--spacing-system-xxs)] p-[var(--spacing-system-m)]',
|
|
379
413
|
tabAlign === 'start' ? 'justify-start text-left' : 'justify-center',
|
|
380
414
|
// Named rather than `transition-all`: the only thing that moves
|
|
381
|
-
// here is colour.
|
|
382
|
-
// property each frame, including the background GRADIENT — which
|
|
383
|
-
// never animated anyway, since the inactive state has no
|
|
384
|
-
// background-image to interpolate from.
|
|
415
|
+
// here is colour.
|
|
385
416
|
'border-none bg-transparent outline-none transition-colors duration-200',
|
|
386
417
|
// `outline-none` above drops the browser's own focus ring, so
|
|
387
418
|
// put one back or the strip is un-navigable by keyboard — you
|
|
388
419
|
// can tab through it with nothing to show where you are. Inset,
|
|
389
|
-
// so it stays within the button
|
|
390
|
-
// sitting 4px away.
|
|
420
|
+
// so it stays within the button: the tabs touch.
|
|
391
421
|
'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ods-focus',
|
|
392
422
|
stretchTabs && 'flex-1',
|
|
393
423
|
tab.avatar && 'gap-[var(--spacing-system-xsf)]',
|
|
394
|
-
//
|
|
395
|
-
// alpha modifiers (to-ods-accent/10) silently produce no CSS.
|
|
396
|
-
// color-mix() accepts hex, so we derive the 10%-alpha stop from the token.
|
|
397
|
-
isActive
|
|
398
|
-
? 'bg-gradient-to-b from-transparent to-[color-mix(in_srgb,var(--color-accent-primary)_10%,transparent)]'
|
|
399
|
-
: 'hover:bg-gradient-to-b hover:from-transparent hover:to-[color-mix(in_srgb,var(--color-accent-primary)_10%,transparent)]',
|
|
424
|
+
// The open tab is marked by the underline alone: no surface of its own.
|
|
400
425
|
)}
|
|
401
426
|
>
|
|
402
427
|
{tab.avatar ? (
|
|
@@ -409,8 +434,11 @@ const TabBar = memo(function TabBarImpl({
|
|
|
409
434
|
<tab.icon
|
|
410
435
|
className={cn(
|
|
411
436
|
'h-4 w-4 transition-colors content-md:h-6 content-md:w-6',
|
|
412
|
-
isActive
|
|
437
|
+
isActive
|
|
438
|
+
? !tab.accent && 'text-ods-accent'
|
|
439
|
+
: 'text-ods-text-secondary group-hover:text-ods-text-primary',
|
|
413
440
|
)}
|
|
441
|
+
style={isActive && tab.accent ? { color: tab.accent } : undefined}
|
|
414
442
|
/>
|
|
415
443
|
{tab.indicator && <StatusDot indicator={tab.indicator} className="absolute right-0 top-[-3px]" />}
|
|
416
444
|
</div>
|
|
@@ -425,7 +453,7 @@ const TabBar = memo(function TabBarImpl({
|
|
|
425
453
|
<span
|
|
426
454
|
className={cn(
|
|
427
455
|
'whitespace-nowrap transition-colors text-h4',
|
|
428
|
-
isActive ? 'text-ods-text-primary' : 'text-ods-text-secondary',
|
|
456
|
+
isActive ? 'text-ods-text-primary' : 'text-ods-text-secondary group-hover:text-ods-text-primary',
|
|
429
457
|
)}
|
|
430
458
|
>
|
|
431
459
|
{tab.label}
|
|
@@ -471,11 +499,11 @@ const TabBar = memo(function TabBarImpl({
|
|
|
471
499
|
// tests) rather than incidental styling.
|
|
472
500
|
data-testid="tab-navigation-underline"
|
|
473
501
|
className={cn(
|
|
474
|
-
'pointer-events-none absolute bottom-0 left-0
|
|
475
|
-
|
|
502
|
+
'pointer-events-none absolute bottom-0 left-0 h-1 motion-reduce:transition-none',
|
|
503
|
+
!activeAccent && 'bg-ods-accent',
|
|
476
504
|
!hasProgress && shouldAnimateIndicator && 'transition-[transform,width,opacity] duration-200 ease-out',
|
|
477
505
|
)}
|
|
478
|
-
style={indicatorStyle}
|
|
506
|
+
style={activeAccent ? { ...indicatorStyle, backgroundColor: activeAccent } : indicatorStyle}
|
|
479
507
|
/>
|
|
480
508
|
</div>
|
|
481
509
|
|
|
@@ -1,59 +1,176 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { useCallback, useMemo, useRef, useState, useSyncExternalStore } from 'react';
|
|
4
|
+
import type { FocusEvent, PointerEvent } from 'react';
|
|
4
5
|
import { usePrefersReducedMotionState } from './use-prefers-reduced-motion';
|
|
5
6
|
|
|
7
|
+
/** The handlers that hold a moving thing still while a visitor is on it. Spread them on its outer element. */
|
|
8
|
+
export interface AutoplayHoldProps {
|
|
9
|
+
onPointerDown: (event: PointerEvent<HTMLElement>) => void;
|
|
10
|
+
onFocus: (event: FocusEvent<HTMLElement>) => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
6
13
|
export interface UseAutoplayResult {
|
|
7
14
|
/** True while the thing may move by itself. */
|
|
8
15
|
playing: boolean;
|
|
9
|
-
/** The visitor
|
|
10
|
-
|
|
11
|
-
|
|
16
|
+
/** The visitor is holding it still: a finger or the mouse button is down on it, or the keyboard is inside it. */
|
|
17
|
+
held: boolean;
|
|
18
|
+
/** Spread on the element that moves: it is how a visitor stops it, with no control to find. */
|
|
19
|
+
holdProps: AutoplayHoldProps;
|
|
20
|
+
/**
|
|
21
|
+
* Lets go of every hold at once. Called when the visitor CHOOSES something on
|
|
22
|
+
* the thing itself (a tab, a step, an arrow): a choice is an instruction to
|
|
23
|
+
* play from there, and it must never depend on the browser reporting the end
|
|
24
|
+
* of the press or the focus that made it.
|
|
25
|
+
*/
|
|
26
|
+
release: () => void;
|
|
12
27
|
/** True once the system preference is known to be reduced motion. */
|
|
13
28
|
reducedMotion: boolean;
|
|
14
29
|
}
|
|
15
30
|
|
|
31
|
+
/** Subscribes to `events` on `target` for as long as React keeps the subscription; one signal removes them all. */
|
|
32
|
+
function listen(target: EventTarget, events: readonly string[], onChange: () => void): () => void {
|
|
33
|
+
const controller = new AbortController();
|
|
34
|
+
for (const event of events) target.addEventListener(event, onChange, { signal: controller.signal });
|
|
35
|
+
return () => controller.abort();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// The tab: hidden or not is the document's own state, read where it lives (never mirrored into React state).
|
|
39
|
+
const subscribeToTab = (onChange: () => void) => {
|
|
40
|
+
const stopDocument = listen(document, ['visibilitychange'], onChange);
|
|
41
|
+
// `pageshow`: a page restored from the back/forward cache.
|
|
42
|
+
const stopWindow = listen(window, ['pageshow'], onChange);
|
|
43
|
+
return () => {
|
|
44
|
+
stopDocument();
|
|
45
|
+
stopWindow();
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
const tabIsHidden = () => document.visibilityState === 'hidden';
|
|
49
|
+
|
|
50
|
+
const never = () => false;
|
|
51
|
+
|
|
52
|
+
/** Every way a press can end. A drag the browser takes over (an image, selected text) ends in `dragend`, never `pointerup`. */
|
|
53
|
+
const RELEASE_EVENTS = ['pointerup', 'pointercancel', 'mouseup', 'touchend', 'dragend', 'blur'] as const;
|
|
54
|
+
|
|
16
55
|
/**
|
|
17
56
|
* THE rule for anything that moves by itself (a looping demo, an
|
|
18
57
|
* auto-advancing carousel): it plays while four plain facts hold, and nothing
|
|
19
58
|
* else decides it.
|
|
20
59
|
*
|
|
21
60
|
* 1. it is on screen (`onScreen`, the caller's `useInView`);
|
|
22
|
-
* 2. the browser tab is visible
|
|
23
|
-
*
|
|
24
|
-
* 3. the visitor has not pressed pause (`setPaused`: the stop control
|
|
61
|
+
* 2. the browser tab is visible;
|
|
62
|
+
* 3. the visitor is not holding it (`holdProps`: the way to stop it that
|
|
25
63
|
* WCAG 2.2.2 asks of anything that moves for more than five seconds);
|
|
26
64
|
* 4. motion is allowed (never under reduced motion, and not before the
|
|
27
65
|
* preference is known).
|
|
28
66
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
67
|
+
* There is NO pause button. A visitor holds it by PRESSING on it (a finger or
|
|
68
|
+
* the mouse button down, as a story is held) or by having KEYBOARD focus inside
|
|
69
|
+
* it. Each ends by itself (the press is released, wherever that happens; focus
|
|
70
|
+
* moves on), so it can never be left stopped with no visible way to start it again.
|
|
71
|
+
*
|
|
72
|
+
* HOW it knows, which is what keeps it from sticking. Each hold has more than
|
|
73
|
+
* one way to end, and every one of them is checked:
|
|
74
|
+
* - a PRESS ends on its release, listened for on the WINDOW (a drag that
|
|
75
|
+
* leaves the element still ends it), by ANY of the events a release can
|
|
76
|
+
* arrive as (`RELEASE_EVENTS`: a native drag ends in `dragend`, not
|
|
77
|
+
* `pointerup`), and, as the net under those, by the first pointer movement
|
|
78
|
+
* that reports no button down;
|
|
79
|
+
* - the KEYBOARD holds it only when focus ARRIVED by keyboard (the browser
|
|
80
|
+
* drew a ring for it as it came in), and only until focus leaves the
|
|
81
|
+
* element or the visitor reaches for the pointer. "Is `:focus-visible`
|
|
82
|
+
* inside" alone is not the test: a browser turns the ring on for a control
|
|
83
|
+
* that was CLICKED as soon as any key is pressed, which held the demo for a
|
|
84
|
+
* visitor who clicked a tab and then scrolled with the space bar;
|
|
85
|
+
* - a CHOICE made on the thing itself (`release`, called by whoever handles
|
|
86
|
+
* the tab, the step or the arrow) lets go of everything: it plays from
|
|
87
|
+
* there whatever the browser did or did not report;
|
|
88
|
+
* - the tab is read from the document (`useSyncExternalStore`), never copied
|
|
89
|
+
* into state.
|
|
90
|
+
*
|
|
91
|
+
* What does NOT hold it, each tried and removed:
|
|
92
|
+
* - HOVER: a visitor watching a demo rests the mouse on it, so it stopped the
|
|
93
|
+
* moment it had their attention and read as stuck;
|
|
94
|
+
* - a WHEEL event or a passing touch: that is what scrolling the PAGE past
|
|
95
|
+
* the thing produces (the browser cancels a press the moment it becomes a scroll);
|
|
96
|
+
* - focus the browser shows no ring for: a browser re-reports focus when its
|
|
97
|
+
* window is re-activated, and a mouse click on a control is not the keyboard.
|
|
36
98
|
*/
|
|
37
99
|
export function useAutoplay(onScreen: boolean): UseAutoplayResult {
|
|
38
100
|
const reducedState = usePrefersReducedMotionState();
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
101
|
+
const tabHidden = useSyncExternalStore(subscribeToTab, tabIsHidden, never);
|
|
102
|
+
|
|
103
|
+
// The element the handlers are on: known from the first event it receives, so no ref has to be threaded to it.
|
|
104
|
+
const host = useRef<HTMLElement | null>(null);
|
|
105
|
+
// True from the moment focus comes in by keyboard until it leaves or the pointer is used.
|
|
106
|
+
const keyboardCameIn = useRef(false);
|
|
107
|
+
const subscribeToKeyboard = useCallback((onChange: () => void) => {
|
|
108
|
+
const usedPointer = () => {
|
|
109
|
+
keyboardCameIn.current = false;
|
|
110
|
+
onChange();
|
|
111
|
+
};
|
|
112
|
+
const stopFocus = listen(document, ['focusin', 'focusout', 'visibilitychange'], onChange);
|
|
113
|
+
const stopPointer = listen(document, ['pointerdown', 'pointermove'], usedPointer);
|
|
114
|
+
const stopWindow = listen(window, ['focus', 'blur'], onChange);
|
|
47
115
|
return () => {
|
|
48
|
-
|
|
49
|
-
|
|
116
|
+
stopFocus();
|
|
117
|
+
stopPointer();
|
|
118
|
+
stopWindow();
|
|
50
119
|
};
|
|
51
120
|
}, []);
|
|
121
|
+
const keyboardIsInside = useCallback(() => {
|
|
122
|
+
if (!keyboardCameIn.current) return false;
|
|
123
|
+
const inside = !!host.current?.isConnected && host.current.contains(document.activeElement);
|
|
124
|
+
if (!inside) keyboardCameIn.current = false;
|
|
125
|
+
return inside;
|
|
126
|
+
}, []);
|
|
127
|
+
const focused = useSyncExternalStore(subscribeToKeyboard, keyboardIsInside, never);
|
|
128
|
+
|
|
129
|
+
const [pressed, setPressed] = useState(false);
|
|
130
|
+
const holdProps = useMemo<AutoplayHoldProps>(
|
|
131
|
+
() => ({
|
|
132
|
+
onPointerDown: event => {
|
|
133
|
+
host.current = event.currentTarget;
|
|
134
|
+
setPressed(true);
|
|
135
|
+
// One release ends it, wherever and however it happens; the same signal removes every listener.
|
|
136
|
+
const released = new AbortController();
|
|
137
|
+
const release = () => {
|
|
138
|
+
setPressed(false);
|
|
139
|
+
released.abort();
|
|
140
|
+
};
|
|
141
|
+
for (const type of RELEASE_EVENTS) window.addEventListener(type, release, { signal: released.signal });
|
|
142
|
+
// The net: a pointer that moves with no button down has been released, whatever was or was not reported.
|
|
143
|
+
window.addEventListener(
|
|
144
|
+
'pointermove',
|
|
145
|
+
(move: globalThis.PointerEvent) => {
|
|
146
|
+
if (move.buttons === 0) release();
|
|
147
|
+
},
|
|
148
|
+
{ signal: released.signal },
|
|
149
|
+
);
|
|
150
|
+
},
|
|
151
|
+
onFocus: event => {
|
|
152
|
+
host.current = event.currentTarget;
|
|
153
|
+
// Only focus that arrives by keyboard holds it (the browser draws its ring as it comes in).
|
|
154
|
+
if (event.target.matches(':focus-visible')) keyboardCameIn.current = true;
|
|
155
|
+
},
|
|
156
|
+
}),
|
|
157
|
+
[],
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
// Bumped by `release`, so the keyboard store re-reads after it lets go.
|
|
161
|
+
const [, setReleases] = useState(0);
|
|
162
|
+
const release = useCallback(() => {
|
|
163
|
+
keyboardCameIn.current = false;
|
|
164
|
+
setPressed(false);
|
|
165
|
+
setReleases(count => count + 1);
|
|
166
|
+
}, []);
|
|
52
167
|
|
|
168
|
+
const held = pressed || focused;
|
|
53
169
|
return {
|
|
54
|
-
playing: onScreen && !
|
|
55
|
-
|
|
56
|
-
|
|
170
|
+
playing: onScreen && !held && !tabHidden && reducedState === false,
|
|
171
|
+
held,
|
|
172
|
+
holdProps,
|
|
173
|
+
release,
|
|
57
174
|
reducedMotion: reducedState === true,
|
|
58
175
|
};
|
|
59
176
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
4
|
|
|
5
5
|
export interface UseInViewOptions {
|
|
6
6
|
/**
|
|
@@ -43,49 +43,42 @@ const STEPS = Array.from({ length: 21 }, (_, i) => i / 20);
|
|
|
43
43
|
* `useNearViewport` is the fire-once sibling (mount a heavy thing when it gets
|
|
44
44
|
* close, never unmount). This one is for things that should run only WHILE
|
|
45
45
|
* visible, like an auto-advancing carousel or a looping demo.
|
|
46
|
+
*
|
|
47
|
+
* The observer LIVES IN AN EFFECT keyed on the element, never in the ref
|
|
48
|
+
* callback. An observer made in the callback and torn down by a separate
|
|
49
|
+
* effect is torn down every time React re-runs effects without re-attaching
|
|
50
|
+
* refs (a Fast Refresh, an Activity that hides and shows): nothing observed any
|
|
51
|
+
* more, and `inView` froze at its last value, so a demo that happened to be off
|
|
52
|
+
* screen at that moment never played again. An effect that owns it makes it
|
|
53
|
+
* again whenever it is torn down.
|
|
46
54
|
*/
|
|
47
55
|
export function useInView<T extends Element = HTMLElement>({
|
|
48
56
|
threshold = 0,
|
|
49
57
|
rootMargin = '0px',
|
|
50
58
|
initial = false,
|
|
51
59
|
}: UseInViewOptions = {}): UseInViewResult<T> {
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
const disconnect = useCallback(() => {
|
|
56
|
-
observerRef.current?.disconnect();
|
|
57
|
-
observerRef.current = null;
|
|
58
|
-
}, []);
|
|
59
|
-
|
|
60
|
-
const ref = useCallback(
|
|
61
|
-
(node: T | null) => {
|
|
62
|
-
disconnect();
|
|
63
|
-
if (!node) {
|
|
64
|
-
setInView(false);
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
if (typeof IntersectionObserver === 'undefined') {
|
|
68
|
-
setInView(initial);
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
const observer = new IntersectionObserver(
|
|
72
|
-
entries => {
|
|
73
|
-
const entry = entries[entries.length - 1];
|
|
74
|
-
if (!entry) return;
|
|
75
|
-
const ofElement = entry.intersectionRatio >= threshold;
|
|
76
|
-
const viewportHeight = entry.rootBounds?.height ?? 0;
|
|
77
|
-
const ofViewport = viewportHeight > 0 && (entry.intersectionRect?.height ?? 0) >= viewportHeight * threshold;
|
|
78
|
-
setInView(entry.isIntersecting && (ofElement || ofViewport));
|
|
79
|
-
},
|
|
80
|
-
{ threshold: STEPS, rootMargin },
|
|
81
|
-
);
|
|
82
|
-
observer.observe(node);
|
|
83
|
-
observerRef.current = observer;
|
|
84
|
-
},
|
|
85
|
-
[disconnect, threshold, rootMargin, initial],
|
|
86
|
-
);
|
|
60
|
+
// The element, as state: the effect below is keyed on it. `setNode` is the ref (stable).
|
|
61
|
+
const [node, setNode] = useState<T | null>(null);
|
|
62
|
+
const [seen, setSeen] = useState<{ node: T; inView: boolean } | null>(null);
|
|
87
63
|
|
|
88
|
-
useEffect(() =>
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (!node || typeof IntersectionObserver === 'undefined') return undefined;
|
|
66
|
+
const observer = new IntersectionObserver(
|
|
67
|
+
entries => {
|
|
68
|
+
const entry = entries[entries.length - 1];
|
|
69
|
+
if (!entry) return;
|
|
70
|
+
const ofElement = entry.intersectionRatio >= threshold;
|
|
71
|
+
const viewportHeight = entry.rootBounds?.height ?? 0;
|
|
72
|
+
const ofViewport = viewportHeight > 0 && (entry.intersectionRect?.height ?? 0) >= viewportHeight * threshold;
|
|
73
|
+
setSeen({ node, inView: entry.isIntersecting && (ofElement || ofViewport) });
|
|
74
|
+
},
|
|
75
|
+
{ threshold: STEPS, rootMargin },
|
|
76
|
+
);
|
|
77
|
+
observer.observe(node);
|
|
78
|
+
return () => observer.disconnect();
|
|
79
|
+
}, [node, threshold, rootMargin]);
|
|
89
80
|
|
|
90
|
-
|
|
81
|
+
// No element: not in view. An element the observer has not reported on yet (or cannot be observed): `initial`.
|
|
82
|
+
const inView = !node ? false : seen?.node === node ? seen.inView : initial;
|
|
83
|
+
return { ref: setNode, inView };
|
|
91
84
|
}
|
|
@@ -8,6 +8,12 @@ export interface AgentIdentityIcon {
|
|
|
8
8
|
name?: string | null;
|
|
9
9
|
url?: string | null;
|
|
10
10
|
props?: Record<string, unknown> | null;
|
|
11
|
+
/**
|
|
12
|
+
* The identity's accent, travelling with its icon: a hex colour (`#5efaf0`),
|
|
13
|
+
* as the server stores it. What is drawn in the agent's colour (its tab, the
|
|
14
|
+
* step it is on) reads it through `useAgentAccents`; unset, that is the platform's accent.
|
|
15
|
+
*/
|
|
16
|
+
accent?: string | null;
|
|
11
17
|
}
|
|
12
18
|
|
|
13
19
|
/** Each agent's identity icon, keyed by the agent's slug. */
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
4
|
-
import { useAutoplay } from './ui/use-autoplay';
|
|
4
|
+
import { type AutoplayHoldProps, useAutoplay } from './ui/use-autoplay';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* How soon the first step follows the moment the demo becomes playable (it
|
|
8
|
-
* scrolled into view, the tab came back,
|
|
8
|
+
* scrolled into view, the tab came back, the visitor let go of it). A demo that sits
|
|
9
9
|
* still for a whole step after it appears reads as broken; one that moves
|
|
10
10
|
* within a third of a second reads as responding to you.
|
|
11
11
|
*/
|
|
@@ -32,6 +32,14 @@ export interface UseScenarioPlayerOptions {
|
|
|
32
32
|
* never gets to play (it is never scrolled into view) still reads complete.
|
|
33
33
|
*/
|
|
34
34
|
startAtEnd?: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Whether step 0 of every scenario is an EMPTY stage, to be left at once
|
|
37
|
+
* (default true: a stage that sits empty for a whole step after a tap reads
|
|
38
|
+
* as not responding). False when step 0 already shows something to read:
|
|
39
|
+
* every step then lasts a whole `stepMs`, however the scenario was started
|
|
40
|
+
* (a page load, the rotation and a chosen tab all pace it the same).
|
|
41
|
+
*/
|
|
42
|
+
emptyFirstStep?: boolean;
|
|
35
43
|
}
|
|
36
44
|
|
|
37
45
|
export interface UseScenarioPlayerResult {
|
|
@@ -39,9 +47,16 @@ export interface UseScenarioPlayerResult {
|
|
|
39
47
|
scenario: number;
|
|
40
48
|
/** Current step, 0..lastStep. */
|
|
41
49
|
step: number;
|
|
42
|
-
/**
|
|
43
|
-
|
|
44
|
-
|
|
50
|
+
/**
|
|
51
|
+
* False until it has been playable once (it has not been on screen yet).
|
|
52
|
+
* A progress line reads 0 until then, so it starts from empty when the
|
|
53
|
+
* visitor first reaches it, never part-way through a step nobody saw begin.
|
|
54
|
+
*/
|
|
55
|
+
started: boolean;
|
|
56
|
+
/** The visitor is holding it still (pressing on it, or the keyboard is inside it). */
|
|
57
|
+
held: boolean;
|
|
58
|
+
/** Spread on the stage's outer element: how a visitor stops it (`useAutoplay`). */
|
|
59
|
+
holdProps: AutoplayHoldProps;
|
|
45
60
|
/** Jump to a step in the current scenario (a visitor's tap, a stepper button). */
|
|
46
61
|
setStep: (step: number) => void;
|
|
47
62
|
/** Switch scenario and restart it from step 0. */
|
|
@@ -60,14 +75,15 @@ export interface UseScenarioPlayerResult {
|
|
|
60
75
|
* line and the stage itself agree by construction.
|
|
61
76
|
*
|
|
62
77
|
* WHEN IT MOVES is `useAutoplay`'s rule: on screen (`enabled`), tab visible,
|
|
63
|
-
* not
|
|
78
|
+
* not held by the visitor, motion allowed. Under reduced motion it parks on the last step
|
|
64
79
|
* of the current scenario, and every scenario can still be read by choosing it.
|
|
65
80
|
*
|
|
66
|
-
* The moment it becomes playable
|
|
67
|
-
*
|
|
81
|
+
* The moment it becomes playable, and at the top of every scenario (the
|
|
82
|
+
* rotation moved on, or a tab was chosen), its first step follows within
|
|
83
|
+
* `START_MS`, not a whole step later; from there every step takes `stepMs`.
|
|
68
84
|
*
|
|
69
|
-
* A visitor's own choice (`go`, `setStep`) moves the clock there and
|
|
70
|
-
*
|
|
85
|
+
* A visitor's own choice (`go`, `setStep`) moves the clock there and lets go of
|
|
86
|
+
* any hold; the rotation carries on from that point.
|
|
71
87
|
*/
|
|
72
88
|
export function useScenarioPlayer({
|
|
73
89
|
scenarioCount,
|
|
@@ -77,9 +93,10 @@ export function useScenarioPlayer({
|
|
|
77
93
|
enabled = true,
|
|
78
94
|
initialScenario = 0,
|
|
79
95
|
startAtEnd = false,
|
|
96
|
+
emptyFirstStep = true,
|
|
80
97
|
}: UseScenarioPlayerOptions): UseScenarioPlayerResult {
|
|
81
98
|
const auto = useAutoplay(enabled);
|
|
82
|
-
const { reducedMotion,
|
|
99
|
+
const { reducedMotion, held, holdProps, release } = auto;
|
|
83
100
|
|
|
84
101
|
const [scenario, setScenario] = useState(initialScenario);
|
|
85
102
|
const [rawStep, setRawStep] = useState(startAtEnd ? Number.MAX_SAFE_INTEGER : 0);
|
|
@@ -89,6 +106,9 @@ export function useScenarioPlayer({
|
|
|
89
106
|
const step = reducedMotion ? lastStep : Math.min(rawStep, lastStep);
|
|
90
107
|
|
|
91
108
|
const playing = auto.playing && scenarioCount > 0;
|
|
109
|
+
// Latched the first time it may play (state derived during render).
|
|
110
|
+
const [started, setStarted] = useState(false);
|
|
111
|
+
if (playing && !started) setStarted(true);
|
|
92
112
|
|
|
93
113
|
// True from the moment it becomes playable until its first tick has been scheduled.
|
|
94
114
|
const starting = useRef(false);
|
|
@@ -98,9 +118,12 @@ export function useScenarioPlayer({
|
|
|
98
118
|
wasPlaying.current = playing;
|
|
99
119
|
if (!playing) return undefined;
|
|
100
120
|
const atEnd = rawStep >= lastStep;
|
|
101
|
-
// The first step after it becomes playable
|
|
102
|
-
//
|
|
103
|
-
|
|
121
|
+
// The first step comes quickly, both after it becomes playable and at the
|
|
122
|
+
// top of every scenario whose step 0 is the empty stage (the rotation
|
|
123
|
+
// moved on, or the visitor chose a tab): a stage that sits empty for a
|
|
124
|
+
// whole step after a tap reads as not responding. A finished scenario still
|
|
125
|
+
// holds its full time: it is there to be read.
|
|
126
|
+
const quickStart = emptyFirstStep && (starting.current || rawStep === 0) && !atEnd;
|
|
104
127
|
starting.current = false;
|
|
105
128
|
const timer = setTimeout(
|
|
106
129
|
() => {
|
|
@@ -114,24 +137,25 @@ export function useScenarioPlayer({
|
|
|
114
137
|
atEnd ? holdMs : quickStart ? Math.min(START_MS, stepMs) : stepMs,
|
|
115
138
|
);
|
|
116
139
|
return () => clearTimeout(timer);
|
|
117
|
-
}, [playing, rawStep, lastStep, stepMs, holdMs, scenario, scenarioCount]);
|
|
140
|
+
}, [playing, rawStep, lastStep, stepMs, holdMs, scenario, scenarioCount, emptyFirstStep]);
|
|
118
141
|
|
|
142
|
+
// A visitor's choice is an instruction to play from there: it lets go of any hold.
|
|
119
143
|
const setStep = useCallback(
|
|
120
144
|
(next: number) => {
|
|
121
145
|
setRawStep(Math.max(0, Math.min(lastStep, next)));
|
|
122
|
-
|
|
146
|
+
release();
|
|
123
147
|
},
|
|
124
|
-
[lastStep,
|
|
148
|
+
[lastStep, release],
|
|
125
149
|
);
|
|
126
150
|
|
|
127
151
|
const go = useCallback(
|
|
128
152
|
(next: number) => {
|
|
129
153
|
setScenario(next);
|
|
130
154
|
setRawStep(0);
|
|
131
|
-
|
|
155
|
+
release();
|
|
132
156
|
},
|
|
133
|
-
[
|
|
157
|
+
[release],
|
|
134
158
|
);
|
|
135
159
|
|
|
136
|
-
return { scenario, step,
|
|
160
|
+
return { scenario, step, started: started || reducedMotion, held, holdProps, setStep, go, reducedMotion };
|
|
137
161
|
}
|
package/src/styles/fonts.css
CHANGED
|
@@ -59,6 +59,32 @@
|
|
|
59
59
|
src: url('./fonts/dm-sans-latin-700-normal.woff2') format('woff2');
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
/* Metric-matched fallbacks: what is drawn until the two files above arrive
|
|
63
|
+
(`font-display: swap`). A system font scaled to the web font's advance width
|
|
64
|
+
and line box, so the text keeps its line breaks and height when the swap
|
|
65
|
+
happens and nothing below it moves (layout shift). The figures are measured
|
|
66
|
+
from the files in ./fonts: Azeret Mono's advance is 0.65em against 0.60em
|
|
67
|
+
for the system monospaces, DM Sans averages 6% wider than Arial. The
|
|
68
|
+
overrides are the web font's own ascent and descent divided by the scale. */
|
|
69
|
+
@font-face {
|
|
70
|
+
font-family: 'Azeret Mono Fallback';
|
|
71
|
+
src:
|
|
72
|
+
local('Menlo'), local('Monaco'), local('Courier New'), local('Liberation Mono'), local('DejaVu Sans Mono'),
|
|
73
|
+
local('Roboto Mono'), local('Droid Sans Mono');
|
|
74
|
+
size-adjust: 108.33%;
|
|
75
|
+
ascent-override: 86.5%;
|
|
76
|
+
descent-override: 21.2%;
|
|
77
|
+
line-gap-override: 0%;
|
|
78
|
+
}
|
|
79
|
+
@font-face {
|
|
80
|
+
font-family: 'DM Sans Fallback';
|
|
81
|
+
src: local('Arial'), local('Helvetica'), local('Liberation Sans'), local('Roboto');
|
|
82
|
+
size-adjust: 106%;
|
|
83
|
+
ascent-override: 93.6%;
|
|
84
|
+
descent-override: 29.2%;
|
|
85
|
+
line-gap-override: 0%;
|
|
86
|
+
}
|
|
87
|
+
|
|
62
88
|
:root {
|
|
63
89
|
--font-azeret-mono: 'Azeret Mono';
|
|
64
90
|
--font-dm-sans: 'DM Sans';
|
|
@@ -7,9 +7,11 @@
|
|
|
7
7
|
|
|
8
8
|
/* Font Families */
|
|
9
9
|
--font-family-heading:
|
|
10
|
-
'Azeret Mono', 'SF Mono', Monaco, Inconsolata, 'Roboto Mono', Consolas, 'Courier New',
|
|
10
|
+
'Azeret Mono', 'Azeret Mono Fallback', 'SF Mono', Monaco, Inconsolata, 'Roboto Mono', Consolas, 'Courier New',
|
|
11
|
+
monospace;
|
|
11
12
|
--font-family-body:
|
|
12
|
-
'DM Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
|
|
13
|
+
'DM Sans', 'DM Sans Fallback', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
|
|
14
|
+
sans-serif;
|
|
13
15
|
|
|
14
16
|
/* Font Weights */
|
|
15
17
|
--font-weight-medium: 500;
|