@smicolon/fasla-ui 0.5.1 → 0.7.0
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/README.md +42 -14
- package/dist/cli/index.js +2286 -195
- package/dist/index.cjs +50 -12
- package/dist/index.js +50 -12
- package/dist/motion/index.cjs +12 -0
- package/dist/motion/index.d.cts +7 -0
- package/dist/motion/index.d.ts +7 -0
- package/dist/motion/index.js +12 -0
- package/dist/registry/index.cjs +4 -4
- package/dist/registry/index.js +4 -4
- package/dist/tokens/index.cjs +34 -8
- package/dist/tokens/index.d.cts +23 -1
- package/dist/tokens/index.d.ts +23 -1
- package/dist/tokens/index.js +34 -8
- package/package.json +12 -3
- package/registry/blocks/about-approach-stack-cards/AboutApproachStackCards.tsx +305 -0
- package/registry/blocks/about-approach-stack-cards/index.ts +9 -0
- package/registry/blocks/about-bento/AboutBento.tsx +442 -0
- package/registry/blocks/about-bento/index.ts +9 -0
- package/registry/blocks/about-collage-hero/AboutCollageHero.tsx +285 -0
- package/registry/blocks/about-collage-hero/index.ts +8 -0
- package/registry/blocks/about-color-bento/AboutColorBento.tsx +235 -0
- package/registry/blocks/about-color-bento/index.ts +6 -0
- package/registry/blocks/about-culture-masonry/AboutCultureMasonry.tsx +305 -0
- package/registry/blocks/about-culture-masonry/index.ts +9 -0
- package/registry/blocks/about-headline-photos/AboutHeadlinePhotos.tsx +270 -0
- package/registry/blocks/about-headline-photos/index.ts +8 -0
- package/registry/blocks/about-impact-stats-band/AboutImpactStatsBand.test.tsx +45 -0
- package/registry/blocks/about-impact-stats-band/AboutImpactStatsBand.tsx +348 -0
- package/registry/blocks/about-impact-stats-band/index.ts +9 -0
- package/registry/blocks/about-letter-card/AboutLetterCard.tsx +403 -0
- package/registry/blocks/about-letter-card/index.ts +9 -0
- package/registry/blocks/about-mission-statement-hero/AboutMissionStatementHero.tsx +282 -0
- package/registry/blocks/about-mission-statement-hero/index.ts +8 -0
- package/registry/blocks/about-numbered-rows/AboutNumberedRows.tsx +289 -0
- package/registry/blocks/about-numbered-rows/index.ts +7 -0
- package/registry/blocks/about-split-panel/AboutSplitPanel.tsx +247 -0
- package/registry/blocks/about-split-panel/index.ts +8 -0
- package/registry/blocks/about-spotlight/AboutSpotlight.tsx +260 -0
- package/registry/blocks/about-spotlight/index.ts +7 -0
- package/registry/blocks/about-statement-band/AboutStatementBand.tsx +236 -0
- package/registry/blocks/about-statement-band/index.ts +7 -0
- package/registry/blocks/about-story-photos/AboutStoryPhotos.test.tsx +12 -0
- package/registry/blocks/about-story-photos/AboutStoryPhotos.tsx +252 -0
- package/registry/blocks/about-story-photos/index.ts +8 -0
- package/registry/blocks/about-story-snapshots/AboutStorySnapshots.tsx +239 -0
- package/registry/blocks/about-story-snapshots/index.ts +8 -0
- package/registry/blocks/about-type-stack/AboutTypeStack.tsx +205 -0
- package/registry/blocks/about-type-stack/index.ts +6 -0
- package/registry/blocks/about-values-connector-list/AboutValuesConnectorList.tsx +316 -0
- package/registry/blocks/about-values-connector-list/index.ts +7 -0
- package/registry/blocks/about-values-focus/AboutValuesFocus.tsx +303 -0
- package/registry/blocks/about-values-focus/index.ts +8 -0
- package/registry/blocks/about-values-spotlight/AboutValuesSpotlight.tsx +265 -0
- package/registry/blocks/about-values-spotlight/index.ts +8 -0
- package/registry/blocks/auth-centered-card-sign-in/AuthCenteredCardSignIn.tsx +373 -0
- package/registry/blocks/auth-centered-card-sign-in/index.ts +9 -0
- package/registry/blocks/auth-check-your-email/AuthCheckYourEmail.tsx +366 -0
- package/registry/blocks/auth-check-your-email/index.ts +7 -0
- package/registry/blocks/auth-forgot-password/AuthForgotPassword.tsx +265 -0
- package/registry/blocks/auth-forgot-password/index.ts +8 -0
- package/registry/blocks/auth-locked-session-re-authenticate/AuthLockedSessionReAuthenticate.tsx +305 -0
- package/registry/blocks/auth-locked-session-re-authenticate/index.ts +8 -0
- package/registry/blocks/auth-magic-link-request/AuthMagicLinkRequest.tsx +336 -0
- package/registry/blocks/auth-magic-link-request/index.ts +8 -0
- package/registry/blocks/auth-minimal-centered-sign-in/AuthMinimalCenteredSignIn.tsx +261 -0
- package/registry/blocks/auth-minimal-centered-sign-in/index.ts +8 -0
- package/registry/blocks/auth-multi-step-sign-up-wizard/AuthMultiStepSignUpWizard.tsx +460 -0
- package/registry/blocks/auth-multi-step-sign-up-wizard/index.ts +8 -0
- package/registry/blocks/auth-otp-verification/AuthOtpVerification.tsx +348 -0
- package/registry/blocks/auth-otp-verification/index.ts +7 -0
- package/registry/blocks/auth-passkey-sign-in/AuthPasskeySignIn.tsx +294 -0
- package/registry/blocks/auth-passkey-sign-in/index.ts +8 -0
- package/registry/blocks/auth-password-changed/AuthPasswordChanged.tsx +239 -0
- package/registry/blocks/auth-password-changed/index.ts +8 -0
- package/registry/blocks/auth-phone-number-sign-in/AuthPhoneNumberSignIn.tsx +402 -0
- package/registry/blocks/auth-phone-number-sign-in/index.ts +10 -0
- package/registry/blocks/auth-photo-background-sign-in/AuthPhotoBackgroundSignIn.tsx +398 -0
- package/registry/blocks/auth-photo-background-sign-in/index.ts +10 -0
- package/registry/blocks/auth-profile-setup-onboarding/AuthProfileSetupOnboarding.tsx +420 -0
- package/registry/blocks/auth-profile-setup-onboarding/index.ts +7 -0
- package/registry/blocks/auth-qr-device-login/AuthQrDeviceLogin.tsx +303 -0
- package/registry/blocks/auth-qr-device-login/index.ts +7 -0
- package/registry/blocks/auth-reset-password/AuthResetPassword.tsx +313 -0
- package/registry/blocks/auth-reset-password/index.ts +7 -0
- package/registry/blocks/auth-sign-in-error-state/AuthSignInErrorState.tsx +419 -0
- package/registry/blocks/auth-sign-in-error-state/index.ts +9 -0
- package/registry/blocks/auth-sign-up-benefits-column/AuthSignUpBenefitsColumn.tsx +481 -0
- package/registry/blocks/auth-sign-up-benefits-column/index.ts +10 -0
- package/registry/blocks/auth-sign-up-password-strength/AuthSignUpPasswordStrength.tsx +511 -0
- package/registry/blocks/auth-sign-up-password-strength/index.ts +8 -0
- package/registry/blocks/auth-social-first-sign-in/AuthSocialFirstSignIn.tsx +337 -0
- package/registry/blocks/auth-social-first-sign-in/index.ts +9 -0
- package/registry/blocks/auth-split-photo-sign-in/AuthSplitPhotoSignIn.tsx +472 -0
- package/registry/blocks/auth-split-photo-sign-in/index.ts +11 -0
- package/registry/blocks/auth-split-testimonial-sign-up/AuthSplitTestimonialSignUp.tsx +548 -0
- package/registry/blocks/auth-split-testimonial-sign-up/index.ts +11 -0
- package/registry/blocks/auth-tabbed-sign-in-sign-up/AuthTabbedSignInSignUp.tsx +566 -0
- package/registry/blocks/auth-tabbed-sign-in-sign-up/index.ts +11 -0
- package/registry/blocks/auth-team-invite-accept/AuthTeamInviteAccept.tsx +372 -0
- package/registry/blocks/auth-team-invite-accept/index.ts +9 -0
- package/registry/blocks/auth-two-factor-authenticator/AuthTwoFactorAuthenticator.tsx +333 -0
- package/registry/blocks/auth-two-factor-authenticator/index.ts +7 -0
- package/registry/blocks/auth-two-factor-method-chooser/AuthTwoFactorMethodChooser.tsx +290 -0
- package/registry/blocks/auth-two-factor-method-chooser/index.ts +8 -0
- package/registry/blocks/auth-waitlist-early-access/AuthWaitlistEarlyAccess.tsx +430 -0
- package/registry/blocks/auth-waitlist-early-access/index.ts +11 -0
- package/registry/blocks/blog-cards-alongside.test.tsx +176 -0
- package/registry/blocks/blog-cards-author-spotlight/BlogCardsAuthorSpotlight.tsx +342 -0
- package/registry/blocks/blog-cards-author-spotlight/index.ts +6 -0
- package/registry/blocks/blog-cards-bento-editorial-grid/BlogCardsBentoEditorialGrid.tsx +345 -0
- package/registry/blocks/blog-cards-bento-editorial-grid/index.ts +7 -0
- package/registry/blocks/blog-cards-browse.test.tsx +367 -0
- package/registry/blocks/blog-cards-classic-3-up-grid/BlogCardsClassic3UpGrid.tsx +287 -0
- package/registry/blocks/blog-cards-classic-3-up-grid/index.ts +7 -0
- package/registry/blocks/blog-cards-compact-4-up-thumb-grid/BlogCardsCompact4UpThumbGrid.tsx +265 -0
- package/registry/blocks/blog-cards-compact-4-up-thumb-grid/index.ts +7 -0
- package/registry/blocks/blog-cards-dark-editorial-feature/BlogCardsDarkEditorialFeature.tsx +287 -0
- package/registry/blocks/blog-cards-dark-editorial-feature/index.ts +6 -0
- package/registry/blocks/blog-cards-featured-two-up/BlogCardsFeaturedTwoUp.tsx +295 -0
- package/registry/blocks/blog-cards-featured-two-up/index.ts +7 -0
- package/registry/blocks/blog-cards-features.test.tsx +183 -0
- package/registry/blocks/blog-cards-filter-sidebar-results/BlogCardsFilterSidebarResults.tsx +582 -0
- package/registry/blocks/blog-cards-filter-sidebar-results/index.ts +6 -0
- package/registry/blocks/blog-cards-grids.test.tsx +110 -0
- package/registry/blocks/blog-cards-hairline-text-index/BlogCardsHairlineTextIndex.tsx +193 -0
- package/registry/blocks/blog-cards-hairline-text-index/index.ts +7 -0
- package/registry/blocks/blog-cards-journal-bento/BlogCardsJournalBento.tsx +409 -0
- package/registry/blocks/blog-cards-journal-bento/index.ts +8 -0
- package/registry/blocks/blog-cards-latest-popular-split/BlogCardsLatestPopularSplit.tsx +258 -0
- package/registry/blocks/blog-cards-latest-popular-split/index.ts +7 -0
- package/registry/blocks/blog-cards-lead-ranked-picks/BlogCardsLeadRankedPicks.tsx +276 -0
- package/registry/blocks/blog-cards-lead-ranked-picks/index.ts +6 -0
- package/registry/blocks/blog-cards-left-thumb-row-cards/BlogCardsLeftThumbRowCards.tsx +288 -0
- package/registry/blocks/blog-cards-left-thumb-row-cards/index.ts +7 -0
- package/registry/blocks/blog-cards-lists.test.tsx +93 -0
- package/registry/blocks/blog-cards-magazine-cover-story/BlogCardsMagazineCoverStory.tsx +326 -0
- package/registry/blocks/blog-cards-magazine-cover-story/index.ts +6 -0
- package/registry/blocks/blog-cards-newsletter-latest/BlogCardsNewsletterLatest.tsx +291 -0
- package/registry/blocks/blog-cards-newsletter-latest/index.ts +6 -0
- package/registry/blocks/blog-cards-numbered-reading-list/BlogCardsNumberedReadingList.tsx +275 -0
- package/registry/blocks/blog-cards-numbered-reading-list/index.ts +7 -0
- package/registry/blocks/blog-cards-overlay-photo-trio/BlogCardsOverlayPhotoTrio.tsx +279 -0
- package/registry/blocks/blog-cards-overlay-photo-trio/index.ts +7 -0
- package/registry/blocks/blog-cards-post-carousel-with-controls/BlogCardsPostCarouselWithControls.tsx +257 -0
- package/registry/blocks/blog-cards-post-carousel-with-controls/index.ts +6 -0
- package/registry/blocks/blog-cards-pull-quote-feature/BlogCardsPullQuoteFeature.tsx +245 -0
- package/registry/blocks/blog-cards-pull-quote-feature/index.ts +6 -0
- package/registry/blocks/blog-cards-related-posts-strip/BlogCardsRelatedPostsStrip.tsx +247 -0
- package/registry/blocks/blog-cards-related-posts-strip/index.ts +6 -0
- package/registry/blocks/blog-cards-search-chips-results/BlogCardsSearchChipsResults.tsx +473 -0
- package/registry/blocks/blog-cards-search-chips-results/index.ts +6 -0
- package/registry/blocks/blog-cards-split-feature/BlogCardsSplitFeature.tsx +214 -0
- package/registry/blocks/blog-cards-split-feature/index.ts +6 -0
- package/registry/blocks/careers/careers.test.tsx +414 -0
- package/registry/blocks/careers/careers.tsx +391 -0
- package/registry/blocks/careers/index.ts +27 -0
- package/registry/blocks/careers-about-card-role-list/CareersAboutCardRoleList.tsx +235 -0
- package/registry/blocks/careers-about-card-role-list/index.ts +8 -0
- package/registry/blocks/careers-featured-role-spotlight/CareersFeaturedRoleSpotlight.tsx +271 -0
- package/registry/blocks/careers-featured-role-spotlight/index.ts +8 -0
- package/registry/blocks/careers-no-open-roles-empty-state/CareersNoOpenRolesEmptyState.tsx +330 -0
- package/registry/blocks/careers-no-open-roles-empty-state/index.ts +8 -0
- package/registry/blocks/careers-open-roles-hairline-list/CareersOpenRolesHairlineList.tsx +320 -0
- package/registry/blocks/careers-open-roles-hairline-list/index.ts +6 -0
- package/registry/blocks/careers-open-vacancies-table/CareersOpenVacanciesTable.tsx +493 -0
- package/registry/blocks/careers-open-vacancies-table/index.ts +6 -0
- package/registry/blocks/careers-perks-benefits/CareersPerksBenefits.tsx +181 -0
- package/registry/blocks/careers-perks-benefits/index.ts +8 -0
- package/registry/blocks/careers-role-cards-grid/CareersRoleCardsGrid.tsx +405 -0
- package/registry/blocks/careers-role-cards-grid/index.ts +6 -0
- package/registry/blocks/careers-role-detail-page/CareersRoleDetailPage.tsx +541 -0
- package/registry/blocks/careers-role-detail-page/index.ts +10 -0
- package/registry/blocks/careers-role-detail-panel/CareersRoleDetailPanel.tsx +255 -0
- package/registry/blocks/careers-role-detail-panel/index.ts +6 -0
- package/registry/blocks/careers-roles-by-department/CareersRolesByDepartment.tsx +242 -0
- package/registry/blocks/careers-roles-by-department/index.ts +7 -0
- package/registry/blocks/careers-roles-list-accordion-expanded/CareersRolesListAccordionExpanded.tsx +558 -0
- package/registry/blocks/careers-roles-list-accordion-expanded/index.ts +9 -0
- package/registry/blocks/contact-us/contact-us.test.tsx +239 -0
- package/registry/blocks/contact-us/contact-us.tsx +989 -0
- package/registry/blocks/contact-us/index.ts +34 -0
- package/registry/blocks/contact-us-bento-grid/ContactUsBentoGrid.tsx +304 -0
- package/registry/blocks/contact-us-bento-grid/index.ts +5 -0
- package/registry/blocks/contact-us-bento-grid.test.tsx +102 -0
- package/registry/blocks/contact-us-book-a-call/ContactUsBookACall.tsx +403 -0
- package/registry/blocks/contact-us-book-a-call/index.ts +7 -0
- package/registry/blocks/contact-us-book-a-call.test.tsx +134 -0
- package/registry/blocks/contact-us-classic-centered/ContactUsClassicCentered.tsx +199 -0
- package/registry/blocks/contact-us-classic-centered/index.ts +6 -0
- package/registry/blocks/contact-us-dark-statement/ContactUsDarkStatement.tsx +218 -0
- package/registry/blocks/contact-us-dark-statement/index.ts +6 -0
- package/registry/blocks/contact-us-editorial-and-address-card/ContactUsEditorialAndAddressCard.tsx +212 -0
- package/registry/blocks/contact-us-editorial-and-address-card/index.ts +6 -0
- package/registry/blocks/contact-us-editorial-statement/ContactUsEditorialStatement.tsx +205 -0
- package/registry/blocks/contact-us-editorial-statement/index.ts +6 -0
- package/registry/blocks/contact-us-form-contact-methods/ContactUsFormContactMethods.tsx +272 -0
- package/registry/blocks/contact-us-form-contact-methods/index.ts +6 -0
- package/registry/blocks/contact-us-form-with-faq/ContactUsFormWithFaq.tsx +383 -0
- package/registry/blocks/contact-us-form-with-faq/index.ts +7 -0
- package/registry/blocks/contact-us-form-with-faq.test.tsx +161 -0
- package/registry/blocks/contact-us-forms.test.tsx +333 -0
- package/registry/blocks/contact-us-global-offices-table/ContactUsGlobalOfficesTable.tsx +301 -0
- package/registry/blocks/contact-us-global-offices-table/index.ts +5 -0
- package/registry/blocks/contact-us-global-offices-table.test.tsx +100 -0
- package/registry/blocks/contact-us-grand-headline-form/ContactUsGrandHeadlineForm.tsx +180 -0
- package/registry/blocks/contact-us-grand-headline-form/index.ts +6 -0
- package/registry/blocks/contact-us-hours-and-availability/ContactUsHoursAndAvailability.tsx +412 -0
- package/registry/blocks/contact-us-hours-and-availability/index.ts +5 -0
- package/registry/blocks/contact-us-hours-and-availability.test.tsx +123 -0
- package/registry/blocks/contact-us-hq-feature/ContactUsHqFeature.tsx +281 -0
- package/registry/blocks/contact-us-hq-feature/index.ts +5 -0
- package/registry/blocks/contact-us-hq-feature.test.tsx +102 -0
- package/registry/blocks/contact-us-map-split/ContactUsMapSplit.tsx +211 -0
- package/registry/blocks/contact-us-map-split/index.ts +5 -0
- package/registry/blocks/contact-us-map-split.test.tsx +70 -0
- package/registry/blocks/contact-us-named-owners/ContactUsNamedOwners.tsx +324 -0
- package/registry/blocks/contact-us-named-owners/index.ts +6 -0
- package/registry/blocks/contact-us-named-owners.test.tsx +116 -0
- package/registry/blocks/contact-us-newsletter-combo/ContactUsNewsletterCombo.tsx +363 -0
- package/registry/blocks/contact-us-newsletter-combo/index.ts +6 -0
- package/registry/blocks/contact-us-newsletter-combo.test.tsx +165 -0
- package/registry/blocks/contact-us-office-cards/ContactUsOfficeCards.tsx +235 -0
- package/registry/blocks/contact-us-office-cards/index.ts +5 -0
- package/registry/blocks/contact-us-office-cards.test.tsx +71 -0
- package/registry/blocks/contact-us-oversized-wordmark/ContactUsOversizedWordmark.tsx +222 -0
- package/registry/blocks/contact-us-oversized-wordmark/index.ts +6 -0
- package/registry/blocks/contact-us-photo-and-dark-panel/ContactUsPhotoAndDarkPanel.tsx +185 -0
- package/registry/blocks/contact-us-photo-and-dark-panel/index.ts +6 -0
- package/registry/blocks/contact-us-photo-forms.test.tsx +311 -0
- package/registry/blocks/contact-us-pitch-and-inquiry/ContactUsPitchAndInquiry.tsx +202 -0
- package/registry/blocks/contact-us-pitch-and-inquiry/index.ts +6 -0
- package/registry/blocks/contact-us-rtl.test.tsx +105 -0
- package/registry/blocks/contact-us-ruled-split-form/ContactUsRuledSplitForm.tsx +214 -0
- package/registry/blocks/contact-us-ruled-split-form/index.ts +6 -0
- package/registry/blocks/contact-us-social-rail/ContactUsSocialRail.tsx +270 -0
- package/registry/blocks/contact-us-social-rail/index.ts +7 -0
- package/registry/blocks/contact-us-social-rail.test.tsx +121 -0
- package/registry/blocks/contact-us-split-photo-dark/ContactUsSplitPhotoDark.tsx +185 -0
- package/registry/blocks/contact-us-split-photo-dark/index.ts +6 -0
- package/registry/blocks/contact-us-support-channels/ContactUsSupportChannels.tsx +267 -0
- package/registry/blocks/contact-us-support-channels/index.ts +5 -0
- package/registry/blocks/contact-us-support-channels.test.tsx +81 -0
- package/registry/blocks/data-table/DataTable.test.tsx +1 -34
- package/registry/blocks/data-table/DataTable.tsx +0 -139
- package/registry/blocks/data-table/index.ts +1 -8
- package/registry/blocks/error-state-access-denied-403/ErrorStateAccessDenied403.tsx +324 -0
- package/registry/blocks/error-state-access-denied-403/index.ts +10 -0
- package/registry/blocks/error-state-broken-trail-breadcrumb/ErrorStateBrokenTrailBreadcrumb.tsx +179 -0
- package/registry/blocks/error-state-broken-trail-breadcrumb/index.ts +8 -0
- package/registry/blocks/error-state-centered-minimal/ErrorStateCenteredMinimal.tsx +119 -0
- package/registry/blocks/error-state-centered-minimal/index.ts +7 -0
- package/registry/blocks/error-state-colour-field-scatter/ErrorStateColourFieldScatter.tsx +144 -0
- package/registry/blocks/error-state-colour-field-scatter/index.ts +7 -0
- package/registry/blocks/error-state-countdown-redirect/ErrorStateCountdownRedirect.tsx +272 -0
- package/registry/blocks/error-state-countdown-redirect/index.ts +7 -0
- package/registry/blocks/error-state-dark-spotlight/ErrorStateDarkSpotlight.tsx +156 -0
- package/registry/blocks/error-state-dark-spotlight/index.ts +7 -0
- package/registry/blocks/error-state-display-numeral-split/ErrorStateDisplayNumeralSplit.tsx +156 -0
- package/registry/blocks/error-state-display-numeral-split/index.ts +8 -0
- package/registry/blocks/error-state-editorial-photo-split/ErrorStateEditorialPhotoSplit.tsx +156 -0
- package/registry/blocks/error-state-editorial-photo-split/index.ts +8 -0
- package/registry/blocks/error-state-empty-search-results/ErrorStateEmptySearchResults.tsx +243 -0
- package/registry/blocks/error-state-empty-search-results/index.ts +6 -0
- package/registry/blocks/error-state-error-card-panel/ErrorStateErrorCardPanel.tsx +213 -0
- package/registry/blocks/error-state-error-card-panel/index.ts +7 -0
- package/registry/blocks/error-state-full-bleed-photo/ErrorStateFullBleedPhoto.tsx +130 -0
- package/registry/blocks/error-state-full-bleed-photo/index.ts +8 -0
- package/registry/blocks/error-state-ghost-watermark/ErrorStateGhostWatermark.tsx +115 -0
- package/registry/blocks/error-state-ghost-watermark/index.ts +7 -0
- package/registry/blocks/error-state-helpful-links-list/ErrorStateHelpfulLinksList.tsx +185 -0
- package/registry/blocks/error-state-helpful-links-list/index.ts +8 -0
- package/registry/blocks/error-state-illustrated-empty-state/ErrorStateIllustratedEmptyState.tsx +201 -0
- package/registry/blocks/error-state-illustrated-empty-state/index.ts +7 -0
- package/registry/blocks/error-state-layered-badge-centered/ErrorStateLayeredBadgeCentered.tsx +138 -0
- package/registry/blocks/error-state-layered-badge-centered/index.ts +7 -0
- package/registry/blocks/error-state-light-editorial/ErrorStateLightEditorial.tsx +126 -0
- package/registry/blocks/error-state-light-editorial/index.ts +7 -0
- package/registry/blocks/error-state-maintenance-mode/ErrorStateMaintenanceMode.tsx +255 -0
- package/registry/blocks/error-state-maintenance-mode/index.ts +6 -0
- package/registry/blocks/error-state-marquee-404-band/ErrorStateMarquee404Band.tsx +316 -0
- package/registry/blocks/error-state-marquee-404-band/index.ts +7 -0
- package/registry/blocks/error-state-offline-state/ErrorStateOfflineState.tsx +188 -0
- package/registry/blocks/error-state-offline-state/index.ts +7 -0
- package/registry/blocks/error-state-photo-collage-scatter/ErrorStatePhotoCollageScatter.tsx +224 -0
- package/registry/blocks/error-state-photo-collage-scatter/index.ts +8 -0
- package/registry/blocks/error-state-popular-pages-grid/ErrorStatePopularPagesGrid.tsx +195 -0
- package/registry/blocks/error-state-popular-pages-grid/index.ts +9 -0
- package/registry/blocks/error-state-recovery-bento/ErrorStateRecoveryBento.tsx +274 -0
- package/registry/blocks/error-state-recovery-bento/index.ts +7 -0
- package/registry/blocks/error-state-search-recovery/ErrorStateSearchRecovery.tsx +173 -0
- package/registry/blocks/error-state-search-recovery/index.ts +7 -0
- package/registry/blocks/error-state-server-error-500/ErrorStateServerError500.tsx +277 -0
- package/registry/blocks/error-state-server-error-500/index.ts +9 -0
- package/registry/blocks/error-state-sitemap-columns/ErrorStateSitemapColumns.tsx +192 -0
- package/registry/blocks/error-state-sitemap-columns/index.ts +9 -0
- package/registry/blocks/error-state-split-colour-panel/ErrorStateSplitColourPanel.tsx +156 -0
- package/registry/blocks/error-state-split-colour-panel/index.ts +7 -0
- package/registry/blocks/error-state-support-escalation-band/ErrorStateSupportEscalationBand.tsx +186 -0
- package/registry/blocks/error-state-support-escalation-band/index.ts +8 -0
- package/registry/blocks/error-state-terminal-console/ErrorStateTerminalConsole.tsx +208 -0
- package/registry/blocks/error-state-terminal-console/index.ts +8 -0
- package/registry/blocks/faq-accent-active-card/FaqAccentActiveCard.tsx +186 -0
- package/registry/blocks/faq-accent-active-card/index.ts +1 -0
- package/registry/blocks/faq-category-chip-grid/FaqCategoryChipGrid.tsx +425 -0
- package/registry/blocks/faq-category-chip-grid/index.ts +8 -0
- package/registry/blocks/faq-category-columns/FaqCategoryColumns.tsx +248 -0
- package/registry/blocks/faq-category-columns/index.ts +8 -0
- package/registry/blocks/faq-chat-thread/FaqChatThread.tsx +273 -0
- package/registry/blocks/faq-chat-thread/index.ts +7 -0
- package/registry/blocks/faq-classic-centered/FaqClassicCentered.tsx +190 -0
- package/registry/blocks/faq-classic-centered/index.ts +7 -0
- package/registry/blocks/faq-contact-cta-card/FaqContactCtaCard.tsx +229 -0
- package/registry/blocks/faq-contact-cta-card/index.ts +9 -0
- package/registry/blocks/faq-dark-panel/FaqDarkPanel.tsx +186 -0
- package/registry/blocks/faq-dark-panel/index.ts +1 -0
- package/registry/blocks/faq-featured-question/FaqFeaturedQuestion.tsx +221 -0
- package/registry/blocks/faq-featured-question/index.ts +1 -0
- package/registry/blocks/faq-grouped-sections/FaqGroupedSections.tsx +266 -0
- package/registry/blocks/faq-grouped-sections/index.ts +8 -0
- package/registry/blocks/faq-help-hero-popular/FaqHelpHeroPopular.tsx +212 -0
- package/registry/blocks/faq-help-hero-popular/index.ts +7 -0
- package/registry/blocks/faq-masonry-cards/FaqMasonryCards.tsx +268 -0
- package/registry/blocks/faq-masonry-cards/index.ts +7 -0
- package/registry/blocks/faq-media-split/FaqMediaSplit.tsx +197 -0
- package/registry/blocks/faq-media-split/index.ts +8 -0
- package/registry/blocks/faq-mono-index/FaqMonoIndex.tsx +247 -0
- package/registry/blocks/faq-mono-index/index.ts +1 -0
- package/registry/blocks/faq-newsletter-close/FaqNewsletterClose.tsx +314 -0
- package/registry/blocks/faq-newsletter-close/index.ts +8 -0
- package/registry/blocks/faq-numbered-editorial/FaqNumberedEditorial.tsx +190 -0
- package/registry/blocks/faq-numbered-editorial/index.ts +1 -0
- package/registry/blocks/faq-search-categories/FaqSearchCategories.tsx +312 -0
- package/registry/blocks/faq-search-categories/index.ts +8 -0
- package/registry/blocks/faq-sidebar-nav/FaqSidebarNav.tsx +390 -0
- package/registry/blocks/faq-sidebar-nav/index.ts +8 -0
- package/registry/blocks/faq-soft-filled-rows/FaqSoftFilledRows.tsx +209 -0
- package/registry/blocks/faq-soft-filled-rows/index.ts +1 -0
- package/registry/blocks/faq-split-sticky/FaqSplitSticky.tsx +205 -0
- package/registry/blocks/faq-split-sticky/index.ts +7 -0
- package/registry/blocks/faq-support-channels/FaqSupportChannels.tsx +266 -0
- package/registry/blocks/faq-support-channels/index.ts +8 -0
- package/registry/blocks/faq-support-stats/FaqSupportStats.tsx +242 -0
- package/registry/blocks/faq-support-stats/index.ts +8 -0
- package/registry/blocks/faq-table-compact/FaqTableCompact.tsx +220 -0
- package/registry/blocks/faq-table-compact/index.ts +1 -0
- package/registry/blocks/faq-tabs-by-topic/FaqTabsByTopic.tsx +347 -0
- package/registry/blocks/faq-tabs-by-topic/index.ts +8 -0
- package/registry/blocks/faq-topic-tiles/FaqTopicTiles.tsx +254 -0
- package/registry/blocks/faq-topic-tiles/index.ts +7 -0
- package/registry/blocks/faq-two-column-grid/FaqTwoColumnGrid.tsx +256 -0
- package/registry/blocks/faq-two-column-grid/index.ts +7 -0
- package/registry/blocks/faq-uppercase-dense/FaqUppercaseDense.tsx +213 -0
- package/registry/blocks/faq-uppercase-dense/index.ts +1 -0
- package/registry/blocks/features/features.tsx +164 -0
- package/registry/blocks/features/index.ts +12 -0
- package/registry/blocks/features-accordion-photo/FeaturesAccordionPhoto.tsx +149 -0
- package/registry/blocks/features-accordion-photo/index.ts +7 -0
- package/registry/blocks/features-colour-block-cards/FeaturesColourBlockCards.tsx +118 -0
- package/registry/blocks/features-colour-block-cards/index.ts +8 -0
- package/registry/blocks/features-compact-icon-index/FeaturesCompactIconIndex.tsx +136 -0
- package/registry/blocks/features-compact-icon-index/index.ts +7 -0
- package/registry/blocks/features-compare-panels-chat-answer/FeaturesComparePanelsChatAnswer.tsx +346 -0
- package/registry/blocks/features-compare-panels-chat-answer/index.ts +8 -0
- package/registry/blocks/features-editorial-photo-split/FeaturesEditorialPhotoSplit.tsx +118 -0
- package/registry/blocks/features-editorial-photo-split/index.ts +7 -0
- package/registry/blocks/features-expanding-step-cards/FeaturesExpandingStepCards.tsx +300 -0
- package/registry/blocks/features-expanding-step-cards/index.ts +7 -0
- package/registry/blocks/features-feature-checklist-columns/FeaturesFeatureChecklistColumns.tsx +158 -0
- package/registry/blocks/features-feature-checklist-columns/index.ts +7 -0
- package/registry/blocks/features-feature-list-thumbnails/FeaturesFeatureListThumbnails.tsx +137 -0
- package/registry/blocks/features-feature-list-thumbnails/index.ts +7 -0
- package/registry/blocks/features-full-bleed-photo-banner/FeaturesFullBleedPhotoBanner.tsx +116 -0
- package/registry/blocks/features-full-bleed-photo-banner/index.ts +7 -0
- package/registry/blocks/features-icon-card-grid/FeaturesIconCardGrid.tsx +120 -0
- package/registry/blocks/features-icon-card-grid/index.ts +7 -0
- package/registry/blocks/features-icon-titled-preview-rail/FeaturesIconTitledPreviewRail.tsx +321 -0
- package/registry/blocks/features-icon-titled-preview-rail/index.ts +7 -0
- package/registry/blocks/features-icons-numbers.test.tsx +107 -0
- package/registry/blocks/features-interactive.test.tsx +175 -0
- package/registry/blocks/features-media-rail/FeaturesMediaRail.tsx +119 -0
- package/registry/blocks/features-media-rail/index.ts +7 -0
- package/registry/blocks/features-notched-photo-columns/FeaturesNotchedPhotoColumns.tsx +209 -0
- package/registry/blocks/features-notched-photo-columns/index.ts +7 -0
- package/registry/blocks/features-numbered-feature-rows/FeaturesNumberedFeatureRows.tsx +107 -0
- package/registry/blocks/features-numbered-feature-rows/index.ts +7 -0
- package/registry/blocks/features-numbered-steps-floating-cards/FeaturesNumberedStepsFloatingCards.tsx +324 -0
- package/registry/blocks/features-numbered-steps-floating-cards/index.ts +8 -0
- package/registry/blocks/features-perforated-photo-strip/FeaturesPerforatedPhotoStrip.tsx +218 -0
- package/registry/blocks/features-perforated-photo-strip/index.ts +7 -0
- package/registry/blocks/features-photo-bento/FeaturesPhotoBento.tsx +160 -0
- package/registry/blocks/features-photo-bento/index.ts +8 -0
- package/registry/blocks/features-photo-carousel/FeaturesPhotoCarousel.tsx +249 -0
- package/registry/blocks/features-photo-carousel/index.ts +7 -0
- package/registry/blocks/features-photo-collage/FeaturesPhotoCollage.tsx +143 -0
- package/registry/blocks/features-photo-collage/index.ts +7 -0
- package/registry/blocks/features-photo-feature-chips/FeaturesPhotoFeatureChips.tsx +125 -0
- package/registry/blocks/features-photo-feature-chips/index.ts +7 -0
- package/registry/blocks/features-photo-led.test.tsx +177 -0
- package/registry/blocks/features-photo-tabs-panel/FeaturesPhotoTabsPanel.tsx +197 -0
- package/registry/blocks/features-photo-tabs-panel/index.ts +7 -0
- package/registry/blocks/features-portrait-photo-cards-link-row/FeaturesPortraitPhotoCardsLinkRow.tsx +202 -0
- package/registry/blocks/features-portrait-photo-cards-link-row/index.ts +7 -0
- package/registry/blocks/features-product-in-use.test.tsx +188 -0
- package/registry/blocks/features-showcase-card-pair/FeaturesShowcaseCardPair.tsx +161 -0
- package/registry/blocks/features-showcase-card-pair/index.ts +7 -0
- package/registry/blocks/features-solid-colour-cards/FeaturesSolidColourCards.tsx +264 -0
- package/registry/blocks/features-solid-colour-cards/index.ts +7 -0
- package/registry/blocks/features-split-photo-icon-list/FeaturesSplitPhotoIconList.tsx +117 -0
- package/registry/blocks/features-split-photo-icon-list/index.ts +7 -0
- package/registry/blocks/features-stat-feature-band/FeaturesStatFeatureBand.tsx +114 -0
- package/registry/blocks/features-stat-feature-band/index.ts +7 -0
- package/registry/blocks/features-tinted-product-bento/FeaturesTintedProductBento.tsx +252 -0
- package/registry/blocks/features-tinted-product-bento/index.ts +7 -0
- package/registry/blocks/footer/footer.test.tsx +253 -0
- package/registry/blocks/footer/footer.tsx +554 -0
- package/registry/blocks/footer/index.ts +31 -0
- package/registry/blocks/footer-accordion-sitemap/FooterAccordionSitemap.tsx +260 -0
- package/registry/blocks/footer-accordion-sitemap/index.ts +7 -0
- package/registry/blocks/footer-bars.test.tsx +121 -0
- package/registry/blocks/footer-bento-tile/FooterBentoTile.tsx +331 -0
- package/registry/blocks/footer-bento-tile/index.ts +6 -0
- package/registry/blocks/footer-big-question-cta/FooterBigQuestionCta.tsx +250 -0
- package/registry/blocks/footer-big-question-cta/index.ts +6 -0
- package/registry/blocks/footer-big-type-link-stack/FooterBigTypeLinkStack.tsx +275 -0
- package/registry/blocks/footer-big-type-link-stack/index.ts +6 -0
- package/registry/blocks/footer-brand-panel-split/FooterBrandPanelSplit.tsx +163 -0
- package/registry/blocks/footer-brand-panel-split/index.ts +6 -0
- package/registry/blocks/footer-brand.test.tsx +608 -0
- package/registry/blocks/footer-careers-culture/FooterCareersCulture.tsx +297 -0
- package/registry/blocks/footer-careers-culture/index.ts +7 -0
- package/registry/blocks/footer-centered-brand-minimal/FooterCenteredBrandMinimal.tsx +121 -0
- package/registry/blocks/footer-centered-brand-minimal/index.ts +6 -0
- package/registry/blocks/footer-commerce.test.tsx +208 -0
- package/registry/blocks/footer-community-events/FooterCommunityEvents.tsx +263 -0
- package/registry/blocks/footer-community-events/index.ts +8 -0
- package/registry/blocks/footer-community.test.tsx +309 -0
- package/registry/blocks/footer-contact-first/FooterContactFirst.tsx +242 -0
- package/registry/blocks/footer-contact-first/index.ts +7 -0
- package/registry/blocks/footer-contact.test.tsx +285 -0
- package/registry/blocks/footer-controls.test.tsx +78 -0
- package/registry/blocks/footer-cta-band-columns/FooterCtaBandColumns.tsx +225 -0
- package/registry/blocks/footer-cta-band-columns/index.ts +6 -0
- package/registry/blocks/footer-cta.test.tsx +445 -0
- package/registry/blocks/footer-dashboard-slim-bar/FooterDashboardSlimBar.tsx +148 -0
- package/registry/blocks/footer-dashboard-slim-bar/index.ts +6 -0
- package/registry/blocks/footer-docs-page/FooterDocsPage.tsx +261 -0
- package/registry/blocks/footer-docs-page/index.ts +9 -0
- package/registry/blocks/footer-ecommerce-mega/FooterEcommerceMega.tsx +289 -0
- package/registry/blocks/footer-ecommerce-mega/index.ts +6 -0
- package/registry/blocks/footer-floating-demo-card/FooterFloatingDemoCard.tsx +261 -0
- package/registry/blocks/footer-floating-demo-card/index.ts +6 -0
- package/registry/blocks/footer-four-column-directory/FooterFourColumnDirectory.tsx +104 -0
- package/registry/blocks/footer-four-column-directory/index.ts +6 -0
- package/registry/blocks/footer-giant-wordmark/FooterGiantWordmark.tsx +252 -0
- package/registry/blocks/footer-giant-wordmark/index.ts +6 -0
- package/registry/blocks/footer-image-backed-brand/FooterImageBackedBrand.tsx +251 -0
- package/registry/blocks/footer-image-backed-brand/index.ts +6 -0
- package/registry/blocks/footer-index-numbered-columns/FooterIndexNumberedColumns.tsx +236 -0
- package/registry/blocks/footer-index-numbered-columns/index.ts +7 -0
- package/registry/blocks/footer-inline-preferences/FooterInlinePreferences.tsx +164 -0
- package/registry/blocks/footer-inline-preferences/index.ts +7 -0
- package/registry/blocks/footer-legal-first-slim/FooterLegalFirstSlim.tsx +174 -0
- package/registry/blocks/footer-legal-first-slim/index.ts +6 -0
- package/registry/blocks/footer-legal.test.tsx +180 -0
- package/registry/blocks/footer-local-business/FooterLocalBusiness.tsx +258 -0
- package/registry/blocks/footer-local-business/index.ts +7 -0
- package/registry/blocks/footer-marquee-strip/FooterMarqueeStrip.tsx +244 -0
- package/registry/blocks/footer-marquee-strip/index.ts +6 -0
- package/registry/blocks/footer-multi-region/FooterMultiRegion.tsx +310 -0
- package/registry/blocks/footer-multi-region/index.ts +7 -0
- package/registry/blocks/footer-newsletter-split/FooterNewsletterSplit.tsx +255 -0
- package/registry/blocks/footer-newsletter-split/index.ts +6 -0
- package/registry/blocks/footer-product-family-switcher/FooterProductFamilySwitcher.tsx +241 -0
- package/registry/blocks/footer-product-family-switcher/index.ts +7 -0
- package/registry/blocks/footer-service-promise-band/FooterServicePromiseBand.tsx +237 -0
- package/registry/blocks/footer-service-promise-band/index.ts +7 -0
- package/registry/blocks/footer-single-line-bar/FooterSingleLineBar.tsx +107 -0
- package/registry/blocks/footer-single-line-bar/index.ts +6 -0
- package/registry/blocks/footer-sitemap.test.tsx +390 -0
- package/registry/blocks/footer-split-hairline-micro/FooterSplitHairlineMicro.tsx +100 -0
- package/registry/blocks/footer-split-hairline-micro/index.ts +6 -0
- package/registry/blocks/footer-support-contact/FooterSupportContact.tsx +204 -0
- package/registry/blocks/footer-support-contact/index.ts +7 -0
- package/registry/blocks/footer-trust-compliance/FooterTrustCompliance.tsx +234 -0
- package/registry/blocks/footer-trust-compliance/index.ts +7 -0
- package/registry/blocks/footer-two-row-compact/FooterTwoRowCompact.tsx +124 -0
- package/registry/blocks/footer-two-row-compact/index.ts +6 -0
- package/registry/blocks/footer-two-tier-doormat/FooterTwoTierDoormat.tsx +232 -0
- package/registry/blocks/footer-two-tier-doormat/index.ts +8 -0
- package/registry/blocks/footer-utility.test.tsx +406 -0
- package/registry/blocks/footer-wide-sitemap-mega/FooterWideSitemapMega.tsx +227 -0
- package/registry/blocks/footer-wide-sitemap-mega/index.ts +6 -0
- package/registry/blocks/gallery-album-cards/GalleryAlbumCards.tsx +243 -0
- package/registry/blocks/gallery-album-cards/index.ts +8 -0
- package/registry/blocks/gallery-before-after-compare/GalleryBeforeAfterCompare.tsx +269 -0
- package/registry/blocks/gallery-before-after-compare/index.ts +7 -0
- package/registry/blocks/gallery-contact-sheet-wall/GalleryContactSheetWall.tsx +299 -0
- package/registry/blocks/gallery-contact-sheet-wall/index.ts +9 -0
- package/registry/blocks/gallery-culture-photo-grid/GalleryCulturePhotoGrid.tsx +203 -0
- package/registry/blocks/gallery-culture-photo-grid/index.ts +8 -0
- package/registry/blocks/gallery-dark-cinema-wall/GalleryDarkCinemaWall.tsx +180 -0
- package/registry/blocks/gallery-dark-cinema-wall/index.ts +7 -0
- package/registry/blocks/gallery-editorial-split/GalleryEditorialSplit.tsx +188 -0
- package/registry/blocks/gallery-editorial-split/index.ts +8 -0
- package/registry/blocks/gallery-featured-thumb-rail/GalleryFeaturedThumbRail.tsx +397 -0
- package/registry/blocks/gallery-featured-thumb-rail/index.ts +8 -0
- package/registry/blocks/gallery-filmstrip-carousel/GalleryFilmstripCarousel.tsx +384 -0
- package/registry/blocks/gallery-filmstrip-carousel/index.ts +8 -0
- package/registry/blocks/gallery-filter-tabs-grid/GalleryFilterTabsGrid.tsx +403 -0
- package/registry/blocks/gallery-filter-tabs-grid/index.ts +9 -0
- package/registry/blocks/gallery-full-bleed-slider/GalleryFullBleedSlider.tsx +309 -0
- package/registry/blocks/gallery-full-bleed-slider/index.ts +8 -0
- package/registry/blocks/gallery-horizontal-scroll-rail/GalleryHorizontalScrollRail.tsx +300 -0
- package/registry/blocks/gallery-horizontal-scroll-rail/index.ts +8 -0
- package/registry/blocks/gallery-hover-caption-cards/GalleryHoverCaptionCards.tsx +244 -0
- package/registry/blocks/gallery-hover-caption-cards/index.ts +8 -0
- package/registry/blocks/gallery-hover-index-list/GalleryHoverIndexList.tsx +253 -0
- package/registry/blocks/gallery-hover-index-list/index.ts +9 -0
- package/registry/blocks/gallery-lightbox-viewer/GalleryLightboxViewer.tsx +329 -0
- package/registry/blocks/gallery-lightbox-viewer/index.ts +7 -0
- package/registry/blocks/gallery-masonry-gallery/GalleryMasonryGallery.tsx +251 -0
- package/registry/blocks/gallery-masonry-gallery/index.ts +7 -0
- package/registry/blocks/gallery-mosaic-statement-tile/GalleryMosaicStatementTile.tsx +159 -0
- package/registry/blocks/gallery-mosaic-statement-tile/index.ts +8 -0
- package/registry/blocks/gallery-photo-bento/GalleryPhotoBento.tsx +166 -0
- package/registry/blocks/gallery-photo-bento/index.ts +8 -0
- package/registry/blocks/gallery-photo-spotlight/GalleryPhotoSpotlight.tsx +305 -0
- package/registry/blocks/gallery-photo-spotlight/index.ts +9 -0
- package/registry/blocks/gallery-scattered-collage/GalleryScatteredCollage.tsx +286 -0
- package/registry/blocks/gallery-scattered-collage/index.ts +9 -0
- package/registry/blocks/gallery-social-feed-strip/GallerySocialFeedStrip.tsx +288 -0
- package/registry/blocks/gallery-social-feed-strip/index.ts +8 -0
- package/registry/blocks/gallery-timeline-gallery/GalleryTimelineGallery.tsx +243 -0
- package/registry/blocks/gallery-timeline-gallery/index.ts +8 -0
- package/registry/blocks/gallery-video-gallery/GalleryVideoGallery.tsx +362 -0
- package/registry/blocks/gallery-video-gallery/index.ts +8 -0
- package/registry/blocks/hero-annotated-object/HeroAnnotatedObject.tsx +310 -0
- package/registry/blocks/hero-annotated-object/index.ts +10 -0
- package/registry/blocks/hero-arc-gallery/HeroArcGallery.tsx +244 -0
- package/registry/blocks/hero-arc-gallery/index.ts +9 -0
- package/registry/blocks/hero-bento-split/HeroBentoSplit.tsx +266 -0
- package/registry/blocks/hero-bento-split/index.ts +9 -0
- package/registry/blocks/hero-brand-color-block/HeroBrandColorBlock.tsx +259 -0
- package/registry/blocks/hero-brand-color-block/index.ts +8 -0
- package/registry/blocks/hero-colour-stage/HeroColourStage.tsx +292 -0
- package/registry/blocks/hero-colour-stage/index.ts +8 -0
- package/registry/blocks/hero-component-pattern/HeroComponentPattern.tsx +248 -0
- package/registry/blocks/hero-component-pattern/index.ts +9 -0
- package/registry/blocks/hero-curved-media-rail/HeroCurvedMediaRail.tsx +272 -0
- package/registry/blocks/hero-curved-media-rail/index.ts +8 -0
- package/registry/blocks/hero-duo-card/HeroDuoCard.tsx +210 -0
- package/registry/blocks/hero-duo-card/index.ts +8 -0
- package/registry/blocks/hero-editorial-arch/HeroEditorialArch.tsx +226 -0
- package/registry/blocks/hero-editorial-arch/index.ts +10 -0
- package/registry/blocks/hero-editorial-triptych/HeroEditorialTriptych.tsx +252 -0
- package/registry/blocks/hero-editorial-triptych/index.ts +9 -0
- package/registry/blocks/hero-floating-integrations/HeroFloatingIntegrations.tsx +454 -0
- package/registry/blocks/hero-floating-integrations/index.ts +11 -0
- package/registry/blocks/hero-framed-full-bleed/HeroFramedFullBleed.tsx +226 -0
- package/registry/blocks/hero-framed-full-bleed/index.ts +9 -0
- package/registry/blocks/hero-framed-studio-card/HeroFramedStudioCard.tsx +264 -0
- package/registry/blocks/hero-framed-studio-card/index.ts +8 -0
- package/registry/blocks/hero-illustration-split/HeroIllustrationSplit.tsx +200 -0
- package/registry/blocks/hero-illustration-split/index.ts +8 -0
- package/registry/blocks/hero-inline-image-headline/HeroInlineImageHeadline.tsx +305 -0
- package/registry/blocks/hero-inline-image-headline/index.ts +8 -0
- package/registry/blocks/hero-masked-type-reveal/HeroMaskedTypeReveal.tsx +323 -0
- package/registry/blocks/hero-masked-type-reveal/index.ts +9 -0
- package/registry/blocks/hero-motion.test.tsx +185 -0
- package/registry/blocks/hero-orbit-track/HeroOrbitTrack.tsx +238 -0
- package/registry/blocks/hero-orbit-track/index.ts +8 -0
- package/registry/blocks/hero-photo-constellation/HeroPhotoConstellation.tsx +228 -0
- package/registry/blocks/hero-photo-constellation/index.ts +8 -0
- package/registry/blocks/hero-scramble-decode/HeroScrambleDecode.tsx +381 -0
- package/registry/blocks/hero-scramble-decode/index.ts +7 -0
- package/registry/blocks/hero-shape-grid/HeroShapeGrid.tsx +279 -0
- package/registry/blocks/hero-shape-grid/index.ts +8 -0
- package/registry/blocks/hero-split-band-headline/HeroSplitBandHeadline.tsx +242 -0
- package/registry/blocks/hero-split-band-headline/index.ts +8 -0
- package/registry/blocks/hero-split-flap-counters/HeroSplitFlapCounters.tsx +323 -0
- package/registry/blocks/hero-split-flap-counters/index.ts +8 -0
- package/registry/blocks/hero-studio-mosaic/HeroStudioMosaic.tsx +332 -0
- package/registry/blocks/hero-studio-mosaic/index.ts +9 -0
- package/registry/blocks/hero-textured-dark/HeroTexturedDark.tsx +378 -0
- package/registry/blocks/hero-textured-dark/index.ts +10 -0
- package/registry/blocks/hero-twin-panels/HeroTwinPanels.tsx +231 -0
- package/registry/blocks/hero-twin-panels/index.ts +9 -0
- package/registry/blocks/hero-velocity-marquee/HeroVelocityMarquee.tsx +392 -0
- package/registry/blocks/hero-velocity-marquee/index.ts +8 -0
- package/registry/blocks/hero-video-in-type/HeroVideoInType.tsx +374 -0
- package/registry/blocks/hero-video-in-type/index.ts +9 -0
- package/registry/blocks/hero-video-spotlight/HeroVideoSpotlight.tsx +292 -0
- package/registry/blocks/hero-video-spotlight/index.ts +9 -0
- package/registry/blocks/hero-wave-band/HeroWaveBand.tsx +309 -0
- package/registry/blocks/hero-wave-band/index.ts +8 -0
- package/registry/blocks/how-it-works/how-it-works.tsx +330 -0
- package/registry/blocks/how-it-works/index.ts +28 -0
- package/registry/blocks/how-it-works-accordion-steps/HowItWorksAccordionSteps.tsx +213 -0
- package/registry/blocks/how-it-works-accordion-steps/index.ts +7 -0
- package/registry/blocks/how-it-works-center-spine-timeline/HowItWorksCenterSpineTimeline.tsx +200 -0
- package/registry/blocks/how-it-works-center-spine-timeline/index.ts +7 -0
- package/registry/blocks/how-it-works-column-panels/HowItWorksColumnPanels.tsx +299 -0
- package/registry/blocks/how-it-works-column-panels/index.ts +6 -0
- package/registry/blocks/how-it-works-contained-timeline/HowItWorksContainedTimeline.tsx +223 -0
- package/registry/blocks/how-it-works-contained-timeline/index.ts +8 -0
- package/registry/blocks/how-it-works-diagonal-photo-collage/HowItWorksDiagonalPhotoCollage.tsx +170 -0
- package/registry/blocks/how-it-works-diagonal-photo-collage/index.ts +7 -0
- package/registry/blocks/how-it-works-fanned-dissolve-cards/HowItWorksFannedDissolveCards.tsx +258 -0
- package/registry/blocks/how-it-works-fanned-dissolve-cards/index.ts +8 -0
- package/registry/blocks/how-it-works-full-bleed-step-panel/HowItWorksFullBleedStepPanel.tsx +319 -0
- package/registry/blocks/how-it-works-full-bleed-step-panel/index.ts +7 -0
- package/registry/blocks/how-it-works-grids.test.tsx +292 -0
- package/registry/blocks/how-it-works-interactive.test.tsx +262 -0
- package/registry/blocks/how-it-works-numbered-editorial-rows/HowItWorksNumberedEditorialRows.tsx +177 -0
- package/registry/blocks/how-it-works-numbered-editorial-rows/index.ts +7 -0
- package/registry/blocks/how-it-works-numbered-instruction-panels/HowItWorksNumberedInstructionPanels.tsx +226 -0
- package/registry/blocks/how-it-works-numbered-instruction-panels/index.ts +6 -0
- package/registry/blocks/how-it-works-numbered-photo-steps/HowItWorksNumberedPhotoSteps.tsx +150 -0
- package/registry/blocks/how-it-works-numbered-photo-steps/index.ts +7 -0
- package/registry/blocks/how-it-works-orbital-process/HowItWorksOrbitalProcess.tsx +317 -0
- package/registry/blocks/how-it-works-orbital-process/index.ts +7 -0
- package/registry/blocks/how-it-works-overlay-numeral-cards/HowItWorksOverlayNumeralCards.tsx +145 -0
- package/registry/blocks/how-it-works-overlay-numeral-cards/index.ts +7 -0
- package/registry/blocks/how-it-works-phase-line/HowItWorksPhaseLine.tsx +147 -0
- package/registry/blocks/how-it-works-phase-line/index.ts +8 -0
- package/registry/blocks/how-it-works-photo-bento-steps/HowItWorksPhotoBentoSteps.tsx +178 -0
- package/registry/blocks/how-it-works-photo-bento-steps/index.ts +7 -0
- package/registry/blocks/how-it-works-photo-rail-progress/HowItWorksPhotoRailProgress.tsx +231 -0
- package/registry/blocks/how-it-works-photo-rail-progress/index.ts +7 -0
- package/registry/blocks/how-it-works-pill-timeline/HowItWorksPillTimeline.tsx +181 -0
- package/registry/blocks/how-it-works-pill-timeline/index.ts +8 -0
- package/registry/blocks/how-it-works-rounded-cards-device-crop/HowItWorksRoundedCardsDeviceCrop.tsx +286 -0
- package/registry/blocks/how-it-works-rounded-cards-device-crop/index.ts +6 -0
- package/registry/blocks/how-it-works-six-card-grid/HowItWorksSixCardGrid.tsx +385 -0
- package/registry/blocks/how-it-works-six-card-grid/index.ts +6 -0
- package/registry/blocks/how-it-works-split-feature-band/HowItWorksSplitFeatureBand.tsx +316 -0
- package/registry/blocks/how-it-works-split-feature-band/index.ts +6 -0
- package/registry/blocks/how-it-works-split-photo-step-list/HowItWorksSplitPhotoStepList.tsx +179 -0
- package/registry/blocks/how-it-works-split-photo-step-list/index.ts +7 -0
- package/registry/blocks/how-it-works-stacked-media-text/HowItWorksStackedMediaText.tsx +310 -0
- package/registry/blocks/how-it-works-stacked-media-text/index.ts +8 -0
- package/registry/blocks/how-it-works-step-tabs-photo-panel/HowItWorksStepTabsPhotoPanel.tsx +245 -0
- package/registry/blocks/how-it-works-step-tabs-photo-panel/index.ts +7 -0
- package/registry/blocks/how-it-works-tall-visual-rail/HowItWorksTallVisualRail.tsx +349 -0
- package/registry/blocks/how-it-works-tall-visual-rail/index.ts +7 -0
- package/registry/blocks/how-it-works-timelines.test.tsx +265 -0
- package/registry/blocks/how-it-works-vertical-timeline-card/HowItWorksVerticalTimelineCard.tsx +169 -0
- package/registry/blocks/how-it-works-vertical-timeline-card/index.ts +8 -0
- package/registry/blocks/how-it-works-visual-steps.test.tsx +368 -0
- package/registry/blocks/logo-cloud-bento-logo-wall/LogoCloudBentoLogoWall.tsx +159 -0
- package/registry/blocks/logo-cloud-bento-logo-wall/index.ts +7 -0
- package/registry/blocks/logo-cloud-bordered-cell-grid/LogoCloudBorderedCellGrid.tsx +132 -0
- package/registry/blocks/logo-cloud-bordered-cell-grid/index.ts +7 -0
- package/registry/blocks/logo-cloud-centered-caption-row/LogoCloudCenteredCaptionRow.tsx +141 -0
- package/registry/blocks/logo-cloud-centered-caption-row/index.ts +7 -0
- package/registry/blocks/logo-cloud-contained-card/LogoCloudContainedCard.tsx +106 -0
- package/registry/blocks/logo-cloud-contained-card/index.ts +7 -0
- package/registry/blocks/logo-cloud-dark-logo-wall/LogoCloudDarkLogoWall.tsx +123 -0
- package/registry/blocks/logo-cloud-dark-logo-wall/index.ts +7 -0
- package/registry/blocks/logo-cloud-divider-rule-row/LogoCloudDividerRuleRow.tsx +156 -0
- package/registry/blocks/logo-cloud-divider-rule-row/index.ts +7 -0
- package/registry/blocks/logo-cloud-editorial-eyebrow-split/LogoCloudEditorialEyebrowSplit.tsx +121 -0
- package/registry/blocks/logo-cloud-editorial-eyebrow-split/index.ts +7 -0
- package/registry/blocks/logo-cloud-enterprise-wall/LogoCloudEnterpriseWall.tsx +114 -0
- package/registry/blocks/logo-cloud-enterprise-wall/index.ts +7 -0
- package/registry/blocks/logo-cloud-flanked-heading/LogoCloudFlankedHeading.tsx +162 -0
- package/registry/blocks/logo-cloud-flanked-heading/index.ts +8 -0
- package/registry/blocks/logo-cloud-grid-closing-cta/LogoCloudGridClosingCta.tsx +173 -0
- package/registry/blocks/logo-cloud-grid-closing-cta/index.ts +8 -0
- package/registry/blocks/logo-cloud-heading-grid/LogoCloudHeadingGrid.tsx +124 -0
- package/registry/blocks/logo-cloud-heading-grid/index.ts +7 -0
- package/registry/blocks/logo-cloud-heading-left-grid-right/LogoCloudHeadingLeftGridRight.tsx +172 -0
- package/registry/blocks/logo-cloud-heading-left-grid-right/index.ts +8 -0
- package/registry/blocks/logo-cloud-logo-metric-tiles/LogoCloudLogoMetricTiles.tsx +159 -0
- package/registry/blocks/logo-cloud-logo-metric-tiles/index.ts +8 -0
- package/registry/blocks/logo-cloud-logo-quote-cards/LogoCloudLogoQuoteCards.tsx +162 -0
- package/registry/blocks/logo-cloud-logo-quote-cards/index.ts +8 -0
- package/registry/blocks/logo-cloud-logo-testimonial-strip/LogoCloudLogoTestimonialStrip.tsx +159 -0
- package/registry/blocks/logo-cloud-logo-testimonial-strip/index.ts +8 -0
- package/registry/blocks/logo-cloud-named-integration-cards/LogoCloudNamedIntegrationCards.tsx +186 -0
- package/registry/blocks/logo-cloud-named-integration-cards/index.ts +9 -0
- package/registry/blocks/logo-cloud-pyramid-row-stack/LogoCloudPyramidRowStack.tsx +156 -0
- package/registry/blocks/logo-cloud-pyramid-row-stack/index.ts +7 -0
- package/registry/blocks/logo-cloud-quote-panel-grid/LogoCloudQuotePanelGrid.tsx +177 -0
- package/registry/blocks/logo-cloud-quote-panel-grid/index.ts +8 -0
- package/registry/blocks/logo-cloud-scatter-field/LogoCloudScatterField.tsx +190 -0
- package/registry/blocks/logo-cloud-scatter-field/index.ts +8 -0
- package/registry/blocks/logo-cloud-side-label-rail/LogoCloudSideLabelRail.tsx +139 -0
- package/registry/blocks/logo-cloud-side-label-rail/index.ts +7 -0
- package/registry/blocks/logo-cloud-stat-rail-above-grid/LogoCloudStatRailAboveGrid.tsx +173 -0
- package/registry/blocks/logo-cloud-stat-rail-above-grid/index.ts +8 -0
- package/registry/blocks/logo-cloud-tabbed-industry-filter/LogoCloudTabbedIndustryFilter.tsx +200 -0
- package/registry/blocks/logo-cloud-tabbed-industry-filter/index.ts +8 -0
- package/registry/blocks/navbar/index.ts +22 -11
- package/registry/blocks/navbar/navbar.test.tsx +42 -0
- package/registry/blocks/navbar/navbar.tsx +455 -0
- package/registry/blocks/navbar-active-action-with-border/NavbarActiveActionWithBorder.tsx +120 -0
- package/registry/blocks/navbar-active-action-with-border/index.ts +6 -0
- package/registry/blocks/navbar-active-action-with-round/NavbarActiveActionWithRound.tsx +120 -0
- package/registry/blocks/navbar-active-action-with-round/index.ts +6 -0
- package/registry/blocks/navbar-centered-floating/NavbarCenteredFloating.tsx +121 -0
- package/registry/blocks/navbar-centered-floating/index.ts +6 -0
- package/registry/blocks/navbar-default/NavbarDefault.tsx +102 -0
- package/registry/blocks/navbar-default/index.ts +6 -0
- package/registry/blocks/navbar-e-commerce/NavbarECommerce.tsx +212 -0
- package/registry/blocks/navbar-e-commerce/index.ts +6 -0
- package/registry/blocks/navbar-extended-at-top/NavbarExtendedAtTop.tsx +155 -0
- package/registry/blocks/navbar-extended-at-top/index.ts +7 -0
- package/registry/blocks/navbar-multiple-action/NavbarMultipleAction.tsx +133 -0
- package/registry/blocks/navbar-multiple-action/index.ts +6 -0
- package/registry/blocks/navbar-multiple-icon/NavbarMultipleIcon.tsx +132 -0
- package/registry/blocks/navbar-multiple-icon/index.ts +6 -0
- package/registry/blocks/navbar-rounded/NavbarRounded.tsx +140 -0
- package/registry/blocks/navbar-rounded/index.ts +6 -0
- package/registry/blocks/navbar-rounded-small/NavbarRoundedSmall.tsx +143 -0
- package/registry/blocks/navbar-rounded-small/index.ts +6 -0
- package/registry/blocks/navbar-small/NavbarSmall.tsx +142 -0
- package/registry/blocks/navbar-small/index.ts +6 -0
- package/registry/blocks/navbar-with-centered-logo/NavbarWithCenteredLogo.tsx +118 -0
- package/registry/blocks/navbar-with-centered-logo/index.ts +6 -0
- package/registry/blocks/navbar-with-cta-button/NavbarWithCtaButton.tsx +131 -0
- package/registry/blocks/navbar-with-cta-button/index.ts +6 -0
- package/registry/blocks/navbar-with-dropdown/NavbarWithDropdown.tsx +118 -0
- package/registry/blocks/navbar-with-dropdown/index.ts +6 -0
- package/registry/blocks/navbar-with-icon-indicator-avatar/NavbarWithIconIndicatorAvatar.tsx +85 -0
- package/registry/blocks/navbar-with-icon-indicator-avatar/index.ts +6 -0
- package/registry/blocks/navbar-with-search-input/NavbarWithSearchInput.tsx +90 -0
- package/registry/blocks/navbar-with-search-input/index.ts +6 -0
- package/registry/blocks/navbar-with-submenu/NavbarWithSubmenu.tsx +156 -0
- package/registry/blocks/navbar-with-submenu/index.ts +6 -0
- package/registry/blocks/page-header/PageHeader.tsx +5 -9
- package/registry/blocks/pricing/index.ts +20 -0
- package/registry/blocks/pricing/pricing.test.tsx +82 -0
- package/registry/blocks/pricing/pricing.tsx +343 -0
- package/registry/blocks/pricing-comparison-matrix/PricingComparisonMatrix.tsx +336 -0
- package/registry/blocks/pricing-comparison-matrix/index.ts +10 -0
- package/registry/blocks/pricing-plan-picker-summary/PricingPlanPickerSummary.tsx +399 -0
- package/registry/blocks/pricing-plan-picker-summary/index.ts +8 -0
- package/registry/blocks/pricing-plan-spotlight/PricingPlanSpotlight.tsx +281 -0
- package/registry/blocks/pricing-plan-spotlight/index.ts +7 -0
- package/registry/blocks/pricing-tiers/PricingTiers.tsx +264 -0
- package/registry/blocks/pricing-tiers/index.ts +8 -0
- package/registry/blocks/pricing-two-plan-compare/PricingTwoPlanCompare.tsx +187 -0
- package/registry/blocks/pricing-two-plan-compare/index.ts +7 -0
- package/registry/blocks/social-proof/index.ts +36 -0
- package/registry/blocks/social-proof/social-proof.test.tsx +504 -0
- package/registry/blocks/social-proof/social-proof.tsx +759 -0
- package/registry/blocks/social-proof-app-review-row/SocialProofAppReviewRow.tsx +168 -0
- package/registry/blocks/social-proof-app-review-row/index.ts +7 -0
- package/registry/blocks/social-proof-arch-portrait-quote/SocialProofArchPortraitQuote.tsx +114 -0
- package/registry/blocks/social-proof-arch-portrait-quote/index.ts +6 -0
- package/registry/blocks/social-proof-avatar-selector-row/SocialProofAvatarSelectorRow.tsx +276 -0
- package/registry/blocks/social-proof-avatar-selector-row/index.ts +7 -0
- package/registry/blocks/social-proof-benefit-title-cards/SocialProofBenefitTitleCards.tsx +182 -0
- package/registry/blocks/social-proof-benefit-title-cards/index.ts +8 -0
- package/registry/blocks/social-proof-bracket-framed-quote/SocialProofBracketFramedQuote.tsx +120 -0
- package/registry/blocks/social-proof-bracket-framed-quote/index.ts +6 -0
- package/registry/blocks/social-proof-case-study-row/SocialProofCaseStudyRow.tsx +210 -0
- package/registry/blocks/social-proof-case-study-row/index.ts +7 -0
- package/registry/blocks/social-proof-centered-logo-spotlight/SocialProofCenteredLogoSpotlight.tsx +103 -0
- package/registry/blocks/social-proof-centered-logo-spotlight/index.ts +6 -0
- package/registry/blocks/social-proof-classic-trio-grid/SocialProofClassicTrioGrid.tsx +138 -0
- package/registry/blocks/social-proof-classic-trio-grid/index.ts +7 -0
- package/registry/blocks/social-proof-dark-editorial-split/SocialProofDarkEditorialSplit.tsx +111 -0
- package/registry/blocks/social-proof-dark-editorial-split/index.ts +6 -0
- package/registry/blocks/social-proof-giant-quote-duo/SocialProofGiantQuoteDuo.tsx +156 -0
- package/registry/blocks/social-proof-giant-quote-duo/index.ts +8 -0
- package/registry/blocks/social-proof-highlighted-phrase-spotlight/SocialProofHighlightedPhraseSpotlight.tsx +130 -0
- package/registry/blocks/social-proof-highlighted-phrase-spotlight/index.ts +7 -0
- package/registry/blocks/social-proof-overlay-photo-quote/SocialProofOverlayPhotoQuote.tsx +111 -0
- package/registry/blocks/social-proof-overlay-photo-quote/index.ts +7 -0
- package/registry/blocks/social-proof-persona-pillars/SocialProofPersonaPillars.tsx +253 -0
- package/registry/blocks/social-proof-persona-pillars/index.ts +7 -0
- package/registry/blocks/social-proof-progress-carousel/SocialProofProgressCarousel.tsx +249 -0
- package/registry/blocks/social-proof-progress-carousel/index.ts +8 -0
- package/registry/blocks/social-proof-proof-bento/SocialProofProofBento.tsx +218 -0
- package/registry/blocks/social-proof-proof-bento/index.ts +6 -0
- package/registry/blocks/social-proof-spotlight-carousel/SocialProofSpotlightCarousel.tsx +212 -0
- package/registry/blocks/social-proof-spotlight-carousel/index.ts +7 -0
- package/registry/blocks/social-proof-staggered-columns/SocialProofStaggeredColumns.tsx +227 -0
- package/registry/blocks/social-proof-staggered-columns/index.ts +7 -0
- package/registry/blocks/social-proof-stat-story-cards/SocialProofStatStoryCards.tsx +218 -0
- package/registry/blocks/social-proof-stat-story-cards/index.ts +7 -0
- package/registry/blocks/social-proof-tinted-panel-portrait/SocialProofTintedPanelPortrait.tsx +130 -0
- package/registry/blocks/social-proof-tinted-panel-portrait/index.ts +7 -0
- package/registry/blocks/social-proof-tweet-wall/SocialProofTweetWall.tsx +179 -0
- package/registry/blocks/social-proof-tweet-wall/index.ts +7 -0
- package/registry/blocks/social-proof-vertical-column-wall/SocialProofVerticalColumnWall.tsx +220 -0
- package/registry/blocks/social-proof-vertical-column-wall/index.ts +7 -0
- package/registry/blocks/social-proof-video-story-cards/SocialProofVideoStoryCards.tsx +177 -0
- package/registry/blocks/social-proof-video-story-cards/index.ts +7 -0
- package/registry/blocks/social-proof-workplace-portrait-stories/SocialProofWorkplacePortraitStories.tsx +320 -0
- package/registry/blocks/social-proof-workplace-portrait-stories/index.ts +7 -0
- package/registry/blocks/team/index.ts +21 -0
- package/registry/blocks/team/team-classes.test.ts +35 -0
- package/registry/blocks/team/team.tsx +315 -0
- package/registry/blocks/team-annotated-portrait-lineup/TeamAnnotatedPortraitLineup.tsx +310 -0
- package/registry/blocks/team-annotated-portrait-lineup/index.ts +8 -0
- package/registry/blocks/team-annotated-portrait-lineup/team-annotated-portrait-lineup.test.tsx +71 -0
- package/registry/blocks/team-compact-directory-rows/TeamCompactDirectoryRows.tsx +190 -0
- package/registry/blocks/team-compact-directory-rows/compact-directory-rows.test.tsx +40 -0
- package/registry/blocks/team-compact-directory-rows/index.ts +7 -0
- package/registry/blocks/team-department-tabs-directory/TeamDepartmentTabsDirectory.tsx +414 -0
- package/registry/blocks/team-department-tabs-directory/department-tabs-directory.test.tsx +87 -0
- package/registry/blocks/team-department-tabs-directory/index.ts +10 -0
- package/registry/blocks/team-founder-spotlight/TeamFounderSpotlight.tsx +176 -0
- package/registry/blocks/team-founder-spotlight/index.ts +8 -0
- package/registry/blocks/team-founder-spotlight/team-founder-spotlight.test.tsx +70 -0
- package/registry/blocks/team-grid-backdrop-roster/TeamGridBackdropRoster.tsx +234 -0
- package/registry/blocks/team-grid-backdrop-roster/grid-backdrop-roster.test.tsx +72 -0
- package/registry/blocks/team-grid-backdrop-roster/index.ts +8 -0
- package/registry/blocks/team-hairline-grid/TeamHairlineGrid.tsx +190 -0
- package/registry/blocks/team-hairline-grid/hairline-grid.test.tsx +49 -0
- package/registry/blocks/team-hairline-grid/index.ts +1 -0
- package/registry/blocks/team-leadership-grid/TeamLeadershipGrid.tsx +183 -0
- package/registry/blocks/team-leadership-grid/index.ts +7 -0
- package/registry/blocks/team-leadership-grid/team-leadership-grid.test.tsx +38 -0
- package/registry/blocks/team-member-carousel/TeamMemberCarousel.tsx +178 -0
- package/registry/blocks/team-member-carousel/index.ts +7 -0
- package/registry/blocks/team-member-carousel/team-member-carousel.test.tsx +58 -0
- package/registry/blocks/team-member-table/TeamMemberTable.tsx +184 -0
- package/registry/blocks/team-member-table/index.ts +7 -0
- package/registry/blocks/team-member-table/member-table.test.tsx +43 -0
- package/registry/blocks/team-meta-profile-cards/TeamMetaProfileCards.tsx +155 -0
- package/registry/blocks/team-meta-profile-cards/index.ts +7 -0
- package/registry/blocks/team-meta-profile-cards/team-meta-profile-cards.test.tsx +29 -0
- package/registry/blocks/team-overlay-portrait-cards/TeamOverlayPortraitCards.tsx +145 -0
- package/registry/blocks/team-overlay-portrait-cards/index.ts +7 -0
- package/registry/blocks/team-overlay-portrait-cards/team-overlay-portrait-cards.test.tsx +49 -0
- package/registry/blocks/team-portrait-wall/TeamPortraitWall.tsx +131 -0
- package/registry/blocks/team-portrait-wall/index.ts +1 -0
- package/registry/blocks/team-portrait-wall/portrait-wall.test.tsx +45 -0
- package/registry/blocks/team-split-intro-member-rail/TeamSplitIntroMemberRail.tsx +203 -0
- package/registry/blocks/team-split-intro-member-rail/index.ts +7 -0
- package/registry/blocks/team-split-intro-member-rail/split-intro-member-rail.test.tsx +52 -0
- package/registry/blocks/team-we-re-hiring-cta/TeamWeReHiringCta.tsx +192 -0
- package/registry/blocks/team-we-re-hiring-cta/index.ts +7 -0
- package/registry/blocks/team-we-re-hiring-cta/we-re-hiring-cta.test.tsx +44 -0
- package/registry/blocks/video-call/index.ts +53 -0
- package/registry/blocks/video-call/video-call-around.test.tsx +427 -0
- package/registry/blocks/video-call/video-call-before.test.tsx +438 -0
- package/registry/blocks/video-call/video-call-hero.test.tsx +236 -0
- package/registry/blocks/video-call/video-call-stages.test.tsx +477 -0
- package/registry/blocks/video-call/video-call.test.tsx +794 -0
- package/registry/blocks/video-call/video-call.tsx +1282 -0
- package/registry/blocks/video-call-ai-meeting-recap/VideoCallAiMeetingRecap.tsx +466 -0
- package/registry/blocks/video-call-ai-meeting-recap/index.ts +9 -0
- package/registry/blocks/video-call-call-controls-dock/VideoCallCallControlsDock.tsx +211 -0
- package/registry/blocks/video-call-call-controls-dock/index.ts +6 -0
- package/registry/blocks/video-call-call-ended-summary/VideoCallCallEndedSummary.tsx +464 -0
- package/registry/blocks/video-call-call-ended-summary/index.ts +10 -0
- package/registry/blocks/video-call-call-with-side-panel/VideoCallCallWithSidePanel.tsx +357 -0
- package/registry/blocks/video-call-call-with-side-panel/index.ts +6 -0
- package/registry/blocks/video-call-captions-reactions/VideoCallCaptionsReactions.tsx +467 -0
- package/registry/blocks/video-call-captions-reactions/index.ts +10 -0
- package/registry/blocks/video-call-grid-gallery-call/VideoCallGridGalleryCall.tsx +312 -0
- package/registry/blocks/video-call-grid-gallery-call/index.ts +6 -0
- package/registry/blocks/video-call-hero-call-mockup/VideoCallHeroCallMockup.tsx +349 -0
- package/registry/blocks/video-call-hero-call-mockup/index.ts +7 -0
- package/registry/blocks/video-call-incoming-call/VideoCallIncomingCall.tsx +336 -0
- package/registry/blocks/video-call-incoming-call/index.ts +7 -0
- package/registry/blocks/video-call-join-link-dial-in/VideoCallJoinLinkDialIn.tsx +409 -0
- package/registry/blocks/video-call-join-link-dial-in/index.ts +9 -0
- package/registry/blocks/video-call-pre-join-lobby/VideoCallPreJoinLobby.tsx +345 -0
- package/registry/blocks/video-call-pre-join-lobby/index.ts +7 -0
- package/registry/blocks/video-call-screen-share-stage/VideoCallScreenShareStage.tsx +379 -0
- package/registry/blocks/video-call-screen-share-stage/index.ts +7 -0
- package/registry/blocks/video-call-speaker-filmstrip/VideoCallSpeakerFilmstrip.tsx +303 -0
- package/registry/blocks/video-call-speaker-filmstrip/index.ts +6 -0
- package/registry/blocks/video-call-upcoming-meetings/VideoCallUpcomingMeetings.tsx +349 -0
- package/registry/blocks/video-call-upcoming-meetings/index.ts +7 -0
- package/registry/blocks/video-call-waiting-room-knock/VideoCallWaitingRoomKnock.tsx +412 -0
- package/registry/blocks/video-call-waiting-room-knock/index.ts +7 -0
- package/registry/text-props-undefined.test.tsx +3 -26
- package/registry/ui/accordion/accordion.test.tsx +320 -0
- package/registry/ui/accordion/accordion.tsx +353 -0
- package/registry/ui/accordion/index.ts +15 -0
- package/registry/ui/alert/alert.test.tsx +325 -0
- package/registry/ui/alert/alert.tsx +384 -0
- package/registry/ui/alert/index.ts +10 -0
- package/registry/ui/alert-dialog/alert-dialog.test.tsx +396 -0
- package/registry/ui/alert-dialog/alert-dialog.tsx +423 -0
- package/registry/ui/alert-dialog/index.ts +21 -0
- package/registry/ui/avatar/avatar.test.tsx +2 -2
- package/registry/ui/avatar/avatar.tsx +3 -7
- package/registry/ui/breadcrumb/breadcrumb.test.tsx +305 -0
- package/registry/ui/breadcrumb/breadcrumb.tsx +205 -0
- package/registry/ui/breadcrumb/index.ts +11 -0
- package/registry/ui/button/button.test.tsx +353 -40
- package/registry/ui/button/button.tsx +304 -30
- package/registry/ui/calendar/calendar.test.tsx +192 -0
- package/registry/ui/calendar/calendar.tsx +405 -0
- package/registry/ui/calendar/index.ts +1 -0
- package/registry/ui/carousel/carousel.test.tsx +720 -0
- package/registry/ui/carousel/carousel.tsx +554 -0
- package/registry/ui/carousel/index.ts +11 -0
- package/registry/ui/checkbox/checkbox.test.tsx +318 -2
- package/registry/ui/checkbox/checkbox.tsx +253 -67
- package/registry/ui/checkbox/index.ts +1 -1
- package/registry/ui/content-carousel/content-carousel.test.tsx +303 -0
- package/registry/ui/content-carousel/content-carousel.tsx +180 -0
- package/registry/ui/content-carousel/index.ts +8 -0
- package/registry/ui/date-picker/date-picker.test.tsx +216 -0
- package/registry/ui/date-picker/date-picker.tsx +461 -0
- package/registry/ui/date-picker/index.ts +8 -0
- package/registry/ui/dialog/dialog.test.tsx +438 -0
- package/registry/ui/dialog/dialog.tsx +533 -0
- package/registry/ui/dialog/index.ts +20 -0
- package/registry/ui/dropdown-menu/dropdown-menu.test.tsx +502 -0
- package/registry/ui/dropdown-menu/dropdown-menu.tsx +627 -0
- package/registry/ui/dropdown-menu/index.ts +32 -0
- package/registry/ui/fasla-logo/fasla-logo.tsx +101 -0
- package/registry/ui/fasla-logo/index.ts +1 -0
- package/registry/ui/input/index.ts +10 -1
- package/registry/ui/input/input.test.tsx +251 -44
- package/registry/ui/input/input.tsx +311 -40
- package/registry/ui/input-number/index.ts +1 -0
- package/registry/ui/input-number/input-number.test.tsx +302 -0
- package/registry/ui/input-number/input-number.tsx +398 -0
- package/registry/ui/input-otp/index.ts +7 -0
- package/registry/ui/input-otp/input-otp.test.tsx +299 -0
- package/registry/ui/input-otp/input-otp.tsx +279 -0
- package/registry/ui/pagination/index.ts +12 -0
- package/registry/ui/pagination/pagination.test.tsx +330 -0
- package/registry/ui/pagination/pagination.tsx +336 -0
- package/registry/ui/progress/index.ts +8 -0
- package/registry/ui/progress/progress.test.tsx +168 -0
- package/registry/ui/progress/progress.tsx +165 -0
- package/registry/ui/select/index.ts +12 -1
- package/registry/ui/select/select.test.tsx +505 -0
- package/registry/ui/select/select.tsx +658 -56
- package/registry/ui/sheet/index.ts +19 -0
- package/registry/ui/sheet/sheet.test.tsx +411 -0
- package/registry/ui/sheet/sheet.tsx +496 -0
- package/registry/{blocks → ui}/sidebar/index.ts +10 -5
- package/registry/ui/sidebar/sidebar.test.tsx +224 -0
- package/registry/ui/sidebar/sidebar.tsx +754 -0
- package/registry/ui/slider/index.ts +8 -0
- package/registry/ui/slider/slider.test.tsx +231 -0
- package/registry/ui/slider/slider.tsx +319 -0
- package/registry/ui/social-icons/index.ts +1 -0
- package/registry/ui/social-icons/social-icons.tsx +284 -0
- package/registry/ui/steps/index.ts +16 -0
- package/registry/ui/steps/steps.test.tsx +435 -0
- package/registry/ui/steps/steps.tsx +571 -0
- package/registry/ui/switch/switch.tsx +3 -2
- package/registry/ui/tabs/index.ts +4 -0
- package/registry/ui/tabs/tabs.test.tsx +576 -0
- package/registry/ui/tabs/tabs.tsx +324 -112
- package/registry/ui/textarea/index.ts +1 -1
- package/registry/ui/textarea/textarea.test.tsx +294 -0
- package/registry/ui/textarea/textarea.tsx +329 -56
- package/registry/ui/tooltip/index.ts +14 -0
- package/registry/ui/tooltip/tooltip.test.tsx +301 -0
- package/registry/ui/tooltip/tooltip.tsx +409 -0
- package/registry.json +9703 -140
- package/tailwind-preset.ts +2 -2
- package/registry/blocks/navbar/Navbar.test.tsx +0 -12
- package/registry/blocks/navbar/Navbar.tsx +0 -175
- package/registry/blocks/sidebar/Sidebar.test.tsx +0 -39
- package/registry/blocks/sidebar/Sidebar.tsx +0 -212
|
@@ -0,0 +1,1282 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { Ellipsis, Hand, MessageSquare, Mic, MicOff, PhoneOff, Pin, ScreenShare, Search, Smile, Star, UserPlus, Video, VideoOff, X } from "lucide-react"
|
|
5
|
+
import { cn } from "../../../src/lib/utils"
|
|
6
|
+
import { Badge } from "../../ui/badge/badge"
|
|
7
|
+
import { Avatar } from "../../ui/avatar/avatar"
|
|
8
|
+
import { Button } from "../../ui/button/button"
|
|
9
|
+
import { Input } from "../../ui/input/input"
|
|
10
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../../ui/tabs/tabs"
|
|
11
|
+
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "../../ui/dropdown-menu/dropdown-menu"
|
|
12
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from "../../ui/tooltip/tooltip"
|
|
13
|
+
|
|
14
|
+
/*
|
|
15
|
+
* Figma: `Video Call` (42869:1033). The parts every `video-call-*` block is
|
|
16
|
+
* built from, one per thing the Figma set repeats: the section and its 1248px
|
|
17
|
+
* column, the heading, the stage bar, the participant tile, the controls dock
|
|
18
|
+
* (compact and labelled, with a Leave that needs a hold), and the small helpers
|
|
19
|
+
* that keep Latin words and figures whole inside Arabic.
|
|
20
|
+
*
|
|
21
|
+
* Mobile first, and sized by the block's own width, not the window's, as the
|
|
22
|
+
* Careers and Pricing blocks are: the section is a query container, so a block
|
|
23
|
+
* lays out for the room it is given. The base classes are Figma's Mobile (360);
|
|
24
|
+
* `[@container(min-width:40rem)]:` is its Tablet, from 640px of content
|
|
25
|
+
* (Figma's Tablet, 768 less its padding), and `[@container(min-width:64rem)]:`
|
|
26
|
+
* its Desktop, from 1024px of content. The section's own padding still follows
|
|
27
|
+
* the window, since an element can't query itself. The page's `dir` sets the
|
|
28
|
+
* direction; nothing here takes one.
|
|
29
|
+
*
|
|
30
|
+
* STAND-INS. The code library has no Toast, Rating or Separator yet, so this
|
|
31
|
+
* file carries three small ones, named here so they are easy to swap:
|
|
32
|
+
* - `VideoCallToast` (and `useVideoCallToast`): a polite live region that
|
|
33
|
+
* disappears on its own. Replace it with the Toast atom when there is one.
|
|
34
|
+
* - `StarRow`: five toggle stars that fill up to the pointer. Replace it with
|
|
35
|
+
* the Rating atom.
|
|
36
|
+
* - `VideoCallSeparator`: a 1px `border` line. Replace it with Separator.
|
|
37
|
+
* The real atoms come in a later PR, and none of the shared atoms in
|
|
38
|
+
* `registry/ui` is changed here.
|
|
39
|
+
*
|
|
40
|
+
* Stills only: a tile shows a photo, never a video element, and nothing here
|
|
41
|
+
* asks for the camera or the microphone. Every real action (leave, mute, share,
|
|
42
|
+
* react) is a callback; what the controls show (pressed, open, hovered) is state
|
|
43
|
+
* the block keeps itself.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
export type VideoCallLocale = "en" | "ar"
|
|
47
|
+
|
|
48
|
+
/* ───────────────────────── Latin inside Arabic ───────────────────────── */
|
|
49
|
+
|
|
50
|
+
const ARABIC = /[-ۿ]/
|
|
51
|
+
|
|
52
|
+
/** Whether a string has no Arabic letters. */
|
|
53
|
+
export const isLatin = (text: React.ReactNode) => typeof text === "string" && !ARABIC.test(text)
|
|
54
|
+
|
|
55
|
+
/** A run of Latin letters, figures and the marks that join them: `Fasla`, `⌘ D`, `fasla.to/room`, `+44 20 7946 0212`, `2,100` (a comma only between digits, so `Karim, Mazen` still splits). */
|
|
56
|
+
const LATIN_RUN = /([A-Za-z0-9⌘](?:[A-Za-z0-9⌘ .:@/+'’&_–-]|,(?=\d))*[A-Za-z0-9⌘]|[A-Za-z0-9⌘])/
|
|
57
|
+
|
|
58
|
+
/** One Latin run kept whole and left-to-right inside any sentence, as Figma's LRI…PDI marks do. */
|
|
59
|
+
export function Latin({ children, className }: { children: React.ReactNode; className?: string }) {
|
|
60
|
+
return (
|
|
61
|
+
<bdi dir="ltr" className={cn("whitespace-nowrap", className)}>
|
|
62
|
+
{children}
|
|
63
|
+
</bdi>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Text with each Latin run or figure set in its own left-to-right isolate, so
|
|
69
|
+
* a name, a shortcut or a number inside an Arabic sentence keeps its order and
|
|
70
|
+
* never glues to the Arabic word beside it. A string with no Arabic, or
|
|
71
|
+
* anything that is not a string, passes through untouched.
|
|
72
|
+
*/
|
|
73
|
+
export function Figures({ children }: { children: React.ReactNode }) {
|
|
74
|
+
if (typeof children !== "string" || !ARABIC.test(children)) return <>{children}</>
|
|
75
|
+
const parts = children.split(LATIN_RUN)
|
|
76
|
+
if (parts.length === 1) return <>{children}</>
|
|
77
|
+
return (
|
|
78
|
+
<>
|
|
79
|
+
{parts.map((part, index) =>
|
|
80
|
+
index % 2 === 1 ? (
|
|
81
|
+
<Latin key={index}>{part}</Latin>
|
|
82
|
+
) : (
|
|
83
|
+
<React.Fragment key={index}>{part}</React.Fragment>
|
|
84
|
+
)
|
|
85
|
+
)}
|
|
86
|
+
</>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/* ─────────────────────────────── Motion ─────────────────────────────── */
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The motion presets, copied in because a block must not import `src/motion`:
|
|
94
|
+
* those files do not resolve after a CLI install. 120ms for a fade, 180ms for
|
|
95
|
+
* a move, 60ms between siblings, and the standard ease.
|
|
96
|
+
*/
|
|
97
|
+
export const FAST_MS = 120
|
|
98
|
+
export const BASE_MS = 180
|
|
99
|
+
export const STAGGER_MS = 60
|
|
100
|
+
export const STANDARD = "cubic-bezier(0.2, 0, 0, 1)"
|
|
101
|
+
|
|
102
|
+
/** Whether the visitor asked for less motion. False where there is no `matchMedia`, as in a test. */
|
|
103
|
+
export function prefersReducedMotion() {
|
|
104
|
+
return typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const useIsomorphicLayoutEffect = typeof window !== "undefined" ? React.useLayoutEffect : React.useEffect
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Fades the nodes in together, or in a staggered rise of 8px. With reduced
|
|
111
|
+
* motion every node fades in together in 120ms and nothing moves. Returns the
|
|
112
|
+
* running animations so a caller can cancel them.
|
|
113
|
+
*/
|
|
114
|
+
export function playEntrance(nodes: HTMLElement[], { rise = 8, stagger = true }: { rise?: number; stagger?: boolean } = {}) {
|
|
115
|
+
const reduced = prefersReducedMotion()
|
|
116
|
+
return nodes
|
|
117
|
+
.filter((node) => typeof node.animate === "function")
|
|
118
|
+
.map((node, index) =>
|
|
119
|
+
node.animate(reduced || !rise ? [{ opacity: 0 }, { opacity: 1 }] : [{ opacity: 0, transform: `translateY(${rise}px)` }, { opacity: 1, transform: "none" }], {
|
|
120
|
+
duration: reduced ? FAST_MS : BASE_MS,
|
|
121
|
+
delay: reduced || !stagger ? 0 : index * STAGGER_MS,
|
|
122
|
+
easing: STANDARD,
|
|
123
|
+
fill: "backwards",
|
|
124
|
+
})
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Plays `playEntrance` once, the first time the element scrolls into view, on
|
|
130
|
+
* every child marked `data-reveal`. Content that is already on screen when
|
|
131
|
+
* there is no IntersectionObserver is simply shown.
|
|
132
|
+
*/
|
|
133
|
+
export function useRevealOnce<T extends HTMLElement>() {
|
|
134
|
+
const ref = React.useRef<T>(null)
|
|
135
|
+
useIsomorphicLayoutEffect(() => {
|
|
136
|
+
const node = ref.current
|
|
137
|
+
if (!node || typeof IntersectionObserver === "undefined") return
|
|
138
|
+
let animations: Animation[] = []
|
|
139
|
+
const observer = new IntersectionObserver(
|
|
140
|
+
(entries) => {
|
|
141
|
+
if (!entries.some((entry) => entry.isIntersecting)) return
|
|
142
|
+
observer.disconnect()
|
|
143
|
+
animations = playEntrance(Array.from(node.querySelectorAll<HTMLElement>("[data-reveal]")))
|
|
144
|
+
},
|
|
145
|
+
{ threshold: 0.2 }
|
|
146
|
+
)
|
|
147
|
+
observer.observe(node)
|
|
148
|
+
return () => {
|
|
149
|
+
observer.disconnect()
|
|
150
|
+
animations.forEach((animation) => animation.cancel())
|
|
151
|
+
}
|
|
152
|
+
}, [])
|
|
153
|
+
return ref
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/* ─────────────────────── Section, heading, rules ─────────────────────── */
|
|
157
|
+
|
|
158
|
+
export interface VideoCallSectionProps extends React.HTMLAttributes<HTMLElement> {
|
|
159
|
+
/** The id of the heading that names the section. */
|
|
160
|
+
labelledBy?: string
|
|
161
|
+
/** Classes for the 1248px column inside the padding. */
|
|
162
|
+
contentClassName?: string
|
|
163
|
+
/** The space between the parts in the column: 48px (`lg`) or 16px (`sm`), as Figma sets each Type. */
|
|
164
|
+
gap?: "lg" | "sm"
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Figma's section: `theme/background`, padding 32 on phones, 64 on Tablet and
|
|
169
|
+
* 96 on Desktop, and a 1248px column.
|
|
170
|
+
*/
|
|
171
|
+
export function VideoCallSection({ labelledBy, className, contentClassName, gap = "lg", children, ...props }: VideoCallSectionProps) {
|
|
172
|
+
return (
|
|
173
|
+
<section aria-labelledby={labelledBy} data-video-call="" className={cn("[container-type:inline-size] bg-background p-8 md:p-16 lg:p-24", className)} {...props}>
|
|
174
|
+
<div className={cn("mx-auto flex w-full max-w-[1248px] flex-col", gap === "lg" ? "gap-12" : "gap-4", contentClassName)}>{children}</div>
|
|
175
|
+
</section>
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export interface VideoCallHeadingProps extends Omit<React.HTMLAttributes<HTMLElement>, "title"> {
|
|
180
|
+
/** The id the section's `aria-labelledby` points at. */
|
|
181
|
+
headingId: string
|
|
182
|
+
/** The small line above the heading, in `primary`. */
|
|
183
|
+
eyebrow?: React.ReactNode
|
|
184
|
+
/** The section's heading, an `h2`. */
|
|
185
|
+
heading: React.ReactNode
|
|
186
|
+
/** The line under the heading. */
|
|
187
|
+
description?: React.ReactNode
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** The start-aligned heading: an eyebrow, an `h2` (2xl on phones, 3xl from Tablet) and a line in `muted-foreground`, 12px apart. */
|
|
191
|
+
export function VideoCallHeading({ headingId, eyebrow, heading, description, className, ...props }: VideoCallHeadingProps) {
|
|
192
|
+
return (
|
|
193
|
+
<header className={cn("flex w-full flex-col items-start gap-3", className)} {...props}>
|
|
194
|
+
{eyebrow ? <p className="text-xs font-medium text-primary">{eyebrow}</p> : null}
|
|
195
|
+
<h2 id={headingId} className="text-xl font-semibold text-foreground [@container(min-width:40rem)]:text-3xl">
|
|
196
|
+
{heading}
|
|
197
|
+
</h2>
|
|
198
|
+
{description ? <p className="max-w-[720px] text-base text-muted-foreground">{description}</p> : null}
|
|
199
|
+
</header>
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Figma's `Separator`: a 1px `border` line across its container. A stand-in until the library has the atom. */
|
|
204
|
+
export function VideoCallSeparator({ className }: { className?: string }) {
|
|
205
|
+
return <div role="separator" aria-orientation="horizontal" className={cn("h-px w-full shrink-0 bg-border", className)} />
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/* ───────────────────────────── Toast (stand-in) ───────────────────────────── */
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Holds a short message for `ms` (1.6s by default) and then clears it. A
|
|
212
|
+
* stand-in for the Toast atom: the dock uses it where a phone has no hover and
|
|
213
|
+
* a tooltip becomes a toast above the dock.
|
|
214
|
+
*/
|
|
215
|
+
export function useVideoCallToast(ms = 1600) {
|
|
216
|
+
const [message, setMessage] = React.useState<React.ReactNode>(null)
|
|
217
|
+
const timer = React.useRef<ReturnType<typeof setTimeout>>(undefined)
|
|
218
|
+
const show = React.useCallback(
|
|
219
|
+
(next: React.ReactNode) => {
|
|
220
|
+
setMessage(next)
|
|
221
|
+
clearTimeout(timer.current)
|
|
222
|
+
timer.current = setTimeout(() => setMessage(null), ms)
|
|
223
|
+
},
|
|
224
|
+
[ms]
|
|
225
|
+
)
|
|
226
|
+
React.useEffect(() => () => clearTimeout(timer.current), [])
|
|
227
|
+
return { message, show }
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** The toast itself: a pill in `foreground` that rises 8px as it appears. Renders nothing without a message. */
|
|
231
|
+
export function VideoCallToast({ message, className }: { message: React.ReactNode; className?: string }) {
|
|
232
|
+
const ref = React.useRef<HTMLDivElement>(null)
|
|
233
|
+
useIsomorphicLayoutEffect(() => {
|
|
234
|
+
if (message && ref.current) playEntrance([ref.current], { stagger: false })
|
|
235
|
+
}, [message])
|
|
236
|
+
return (
|
|
237
|
+
<div role="status" aria-live="polite" className="contents">
|
|
238
|
+
{message ? (
|
|
239
|
+
<div ref={ref} className={cn("pointer-events-none rounded-lg bg-foreground px-3 py-1.5 text-center text-xs text-background", className)}>
|
|
240
|
+
{message}
|
|
241
|
+
</div>
|
|
242
|
+
) : null}
|
|
243
|
+
</div>
|
|
244
|
+
)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/* ──────────────────────────── Star row (stand-in) ──────────────────────────── */
|
|
248
|
+
|
|
249
|
+
export interface StarRowProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "onChange"> {
|
|
250
|
+
/** How many stars are filled, 0 to `count`. */
|
|
251
|
+
value?: number
|
|
252
|
+
/** Where the row starts when `value` is not passed. */
|
|
253
|
+
defaultValue?: number
|
|
254
|
+
/** Called with the new number when a star is chosen. */
|
|
255
|
+
onValueChange?: (value: number) => void
|
|
256
|
+
/** The number of stars. */
|
|
257
|
+
count?: number
|
|
258
|
+
/** Names the group for assistive technology. */
|
|
259
|
+
label: string
|
|
260
|
+
/** Names star `n`: `Rate 3 of 5`. */
|
|
261
|
+
starLabel?: (n: number, count: number) => string
|
|
262
|
+
/** The star's drawn size in pixels: Figma's rating row is 28, the stand-in's default 20. */
|
|
263
|
+
starSize?: number
|
|
264
|
+
/** The gap between one star's fill and the next while hovering: 60ms, and 40 on a phone. */
|
|
265
|
+
cascadeMs?: number
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Five toggle stars. The chosen ones are drawn in `warning` outline, the rest
|
|
270
|
+
* in `soft-warning-20` (the amber at 20%, as the Rating atom's unfilled star); hovering fills every star up to the pointer, on a
|
|
271
|
+
* 60ms cascade, and choosing one keeps it. The row is a plain flex row, so it
|
|
272
|
+
* fills from the start in either direction. A stand-in for the Rating atom.
|
|
273
|
+
*/
|
|
274
|
+
export function StarRow({ value, defaultValue = 0, onValueChange, count = 5, label, starLabel, starSize = 20, cascadeMs = 60, className, ...props }: StarRowProps) {
|
|
275
|
+
const [own, setOwn] = React.useState(defaultValue)
|
|
276
|
+
const [hover, setHover] = React.useState<number | null>(null)
|
|
277
|
+
const current = value ?? own
|
|
278
|
+
const shown = hover ?? current
|
|
279
|
+
const choose = (n: number) => {
|
|
280
|
+
setOwn(n)
|
|
281
|
+
onValueChange?.(n)
|
|
282
|
+
}
|
|
283
|
+
return (
|
|
284
|
+
<div role="radiogroup" aria-label={label} className={cn("flex items-center gap-1", className)} onPointerLeave={() => setHover(null)} {...props}>
|
|
285
|
+
{Array.from({ length: count }, (_, index) => {
|
|
286
|
+
const n = index + 1
|
|
287
|
+
const filled = n <= shown
|
|
288
|
+
return (
|
|
289
|
+
<button
|
|
290
|
+
key={n}
|
|
291
|
+
type="button"
|
|
292
|
+
role="radio"
|
|
293
|
+
aria-checked={n === current}
|
|
294
|
+
aria-label={starLabel ? starLabel(n, count) : `${n} / ${count}`}
|
|
295
|
+
data-filled={filled ? "" : undefined}
|
|
296
|
+
onPointerEnter={() => setHover(n)}
|
|
297
|
+
onClick={() => choose(n)}
|
|
298
|
+
onKeyDown={(event) => {
|
|
299
|
+
const step = event.key === "ArrowRight" || event.key === "ArrowUp" ? 1 : event.key === "ArrowLeft" || event.key === "ArrowDown" ? -1 : 0
|
|
300
|
+
if (!step) return
|
|
301
|
+
event.preventDefault()
|
|
302
|
+
const dir = getComputedStyle(event.currentTarget).direction === "rtl" ? -1 : 1
|
|
303
|
+
choose(Math.min(count, Math.max(1, (current || 1) + step * dir)))
|
|
304
|
+
}}
|
|
305
|
+
style={{ transitionDelay: hover !== null ? `${index * cascadeMs}ms` : undefined, width: starSize + 4, height: starSize + 4 }}
|
|
306
|
+
className={cn(
|
|
307
|
+
"inline-flex items-center justify-center rounded-sm outline-none transition-[color] duration-150 focus-visible:ring-2 focus-visible:ring-ring motion-reduce:transition-none",
|
|
308
|
+
filled ? "text-warning" : "text-soft-warning-20"
|
|
309
|
+
)}
|
|
310
|
+
>
|
|
311
|
+
<Star size={starSize} strokeWidth={1.5} absoluteStrokeWidth aria-hidden className={cn("transition-[fill] duration-150 motion-reduce:transition-none", filled && hover !== null ? "fill-current" : "fill-transparent")} />
|
|
312
|
+
</button>
|
|
313
|
+
)
|
|
314
|
+
})}
|
|
315
|
+
</div>
|
|
316
|
+
)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/* ────────────────────────────── Stage bar ────────────────────────────── */
|
|
320
|
+
|
|
321
|
+
export interface StageBarProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "title"> {
|
|
322
|
+
/** Something before the name: the REC badge. */
|
|
323
|
+
leading?: React.ReactNode
|
|
324
|
+
/** The meeting's name. */
|
|
325
|
+
title: React.ReactNode
|
|
326
|
+
/** The elapsed time, `24:18`, set in `muted-foreground` beside the name. */
|
|
327
|
+
time?: React.ReactNode
|
|
328
|
+
/** Live state beside the name: a REC badge, a connection badge. */
|
|
329
|
+
state?: React.ReactNode
|
|
330
|
+
/** View switcher, participant count and fullscreen, at the end. */
|
|
331
|
+
actions?: React.ReactNode
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** Figma's Stage Bar: the meeting name and its live state at the start, the view controls at the end, wrapping to a second row on phones. */
|
|
335
|
+
export function StageBar({ leading, title, time, state, actions, className, ...props }: StageBarProps) {
|
|
336
|
+
return (
|
|
337
|
+
<div className={cn("flex w-full flex-wrap items-center justify-between gap-x-4 gap-y-2", className)} {...props}>
|
|
338
|
+
<div className="flex min-w-0 flex-wrap items-center gap-x-3 gap-y-1">
|
|
339
|
+
{leading}
|
|
340
|
+
<p className="min-w-0 truncate text-sm font-semibold text-foreground">{title}</p>
|
|
341
|
+
{time ? <p className="text-sm text-muted-foreground tabular-nums">{time}</p> : null}
|
|
342
|
+
{state}
|
|
343
|
+
</div>
|
|
344
|
+
{actions ? <div className="flex items-center gap-2">{actions}</div> : null}
|
|
345
|
+
</div>
|
|
346
|
+
)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/* ──────────────────────────── Participant tile ──────────────────────────── */
|
|
350
|
+
|
|
351
|
+
export interface VideoCallPhoto {
|
|
352
|
+
src: string
|
|
353
|
+
/** Describes the photo. Pass "" for a purely decorative one. */
|
|
354
|
+
alt: string
|
|
355
|
+
/** The focal point of a crop, as a CSS `object-position` such as `"46% 12%"`: the point of the photo that stays put whatever the tile's shape. Without one a block uses its own default. */
|
|
356
|
+
position?: string
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export interface ParticipantTileProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "onChange"> {
|
|
360
|
+
/** The participant's name, shown in the name chip. */
|
|
361
|
+
name: string
|
|
362
|
+
/** The camera still. Without one the tile shows `initials` on `muted`, as Figma does for a camera that is off. */
|
|
363
|
+
photo?: VideoCallPhoto
|
|
364
|
+
/** One or two letters for a camera that is off. */
|
|
365
|
+
initials?: string
|
|
366
|
+
/** Shows the mic-off mark on the name chip. */
|
|
367
|
+
muted?: boolean
|
|
368
|
+
/** Draws the speaking ring. */
|
|
369
|
+
speaking?: boolean
|
|
370
|
+
/** Where the name chip sits: bottom start, bottom end, or not at all. */
|
|
371
|
+
chip?: "start" | "end" | "none"
|
|
372
|
+
/** Extra classes for the name chip, such as one that hides it on phones. */
|
|
373
|
+
chipClassName?: string
|
|
374
|
+
/** Whether the tile is pinned. A pinned tile keeps the pin on show; an unpinned one shows it on hover or focus, and always on a touch screen. */
|
|
375
|
+
pinned?: boolean
|
|
376
|
+
/** Called when the pin is pressed; without it the tile has no pin. */
|
|
377
|
+
onPinnedChange?: (pinned: boolean) => void
|
|
378
|
+
/** Names the pin button: `Pin Adam`. */
|
|
379
|
+
pinLabel?: string
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* A camera tile: a photo (or initials) filling a rounded box, a name chip in
|
|
384
|
+
* `primary` at the bottom, an optional speaking ring that fades in over 90ms,
|
|
385
|
+
* and an optional pin. Anything passed as children sits on top, such as a
|
|
386
|
+
* caption bar or a reaction.
|
|
387
|
+
*/
|
|
388
|
+
export function ParticipantTile({
|
|
389
|
+
name,
|
|
390
|
+
photo,
|
|
391
|
+
initials,
|
|
392
|
+
muted = false,
|
|
393
|
+
speaking = false,
|
|
394
|
+
chip = "start",
|
|
395
|
+
chipClassName,
|
|
396
|
+
pinned = false,
|
|
397
|
+
onPinnedChange,
|
|
398
|
+
pinLabel,
|
|
399
|
+
className,
|
|
400
|
+
children,
|
|
401
|
+
...props
|
|
402
|
+
}: ParticipantTileProps) {
|
|
403
|
+
return (
|
|
404
|
+
<div
|
|
405
|
+
data-speaking={speaking ? "" : undefined}
|
|
406
|
+
className={cn(
|
|
407
|
+
"group/tile relative isolate overflow-hidden rounded-lg bg-muted ring-2 ring-inset ring-transparent transition-shadow duration-100 motion-reduce:transition-none",
|
|
408
|
+
speaking && "ring-border",
|
|
409
|
+
className
|
|
410
|
+
)}
|
|
411
|
+
{...props}
|
|
412
|
+
>
|
|
413
|
+
{photo ? (
|
|
414
|
+
<img src={photo.src} alt={photo.alt} loading="lazy" decoding="async" style={photo.position ? { objectPosition: photo.position } : undefined} className="absolute inset-0 -z-10 size-full object-cover object-[50%_42%]" />
|
|
415
|
+
) : initials ? (
|
|
416
|
+
<span aria-hidden className="absolute inset-0 -z-10 flex items-center justify-center pb-10 text-xl leading-none font-semibold text-foreground [@container(min-width:40rem)]:pb-6 [@container(min-width:40rem)]:text-3xl">
|
|
417
|
+
{initials}
|
|
418
|
+
</span>
|
|
419
|
+
) : null}
|
|
420
|
+
{onPinnedChange ? (
|
|
421
|
+
<Button
|
|
422
|
+
type="button"
|
|
423
|
+
size="icon-xs"
|
|
424
|
+
radius="rounded"
|
|
425
|
+
aria-label={pinLabel ?? `Pin ${name}`}
|
|
426
|
+
aria-pressed={pinned}
|
|
427
|
+
onClick={() => onPinnedChange(!pinned)}
|
|
428
|
+
className={cn(
|
|
429
|
+
"absolute start-3 top-3 transition-[transform,opacity] duration-100 motion-reduce:transition-none",
|
|
430
|
+
pinned ? "rotate-[15deg]" : "opacity-0 focus-visible:opacity-100 group-focus-within/tile:opacity-100 group-hover/tile:opacity-100 [@media(pointer:coarse)]:opacity-100"
|
|
431
|
+
)}
|
|
432
|
+
>
|
|
433
|
+
<Pin aria-hidden />
|
|
434
|
+
</Button>
|
|
435
|
+
) : null}
|
|
436
|
+
{chip !== "none" ? (
|
|
437
|
+
<Badge
|
|
438
|
+
variant="solid"
|
|
439
|
+
tone="primary"
|
|
440
|
+
size="lg"
|
|
441
|
+
radius="standard"
|
|
442
|
+
icon={muted ? <MicOff /> : <Mic />}
|
|
443
|
+
className={cn("absolute bottom-3", chip === "start" ? "start-3" : "end-3", chipClassName)}
|
|
444
|
+
>
|
|
445
|
+
<Figures>{name}</Figures>
|
|
446
|
+
</Badge>
|
|
447
|
+
) : null}
|
|
448
|
+
{children}
|
|
449
|
+
</div>
|
|
450
|
+
)
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/* ─────────────────────────────── Controls dock ─────────────────────────────── */
|
|
454
|
+
|
|
455
|
+
export interface DockControl {
|
|
456
|
+
id: string
|
|
457
|
+
/** The glyph, from your icon set. */
|
|
458
|
+
icon: React.ElementType
|
|
459
|
+
/** The glyph while pressed, such as a crossed-out mic. It cross-fades with `icon` over 100ms. */
|
|
460
|
+
pressedIcon?: React.ElementType
|
|
461
|
+
/** The button's name and its tooltip: "Mute". */
|
|
462
|
+
label: string
|
|
463
|
+
/** The name while pressed: "Unmute". */
|
|
464
|
+
pressedLabel?: string
|
|
465
|
+
/** What the tooltip says while pressed, when it says more: "You're muted. Press ⌘ D to talk". */
|
|
466
|
+
pressedTip?: string
|
|
467
|
+
/** The short line under the button in the labelled dock. Defaults to `label`. */
|
|
468
|
+
caption?: string
|
|
469
|
+
/** The caption while pressed. */
|
|
470
|
+
pressedCaption?: string
|
|
471
|
+
/** `off` controls (mic, camera) turn destructive when pressed; `on` controls (hand, captions) turn secondary. */
|
|
472
|
+
kind?: "off" | "on"
|
|
473
|
+
/** Makes it a toggle that keeps its own pressed state. Without it the control only calls back. */
|
|
474
|
+
toggle?: boolean
|
|
475
|
+
/** Where it starts. */
|
|
476
|
+
defaultPressed?: boolean
|
|
477
|
+
/** Leaves it out of the phone layout, as Figma trims the dock to five controls. */
|
|
478
|
+
hideOnPhone?: boolean
|
|
479
|
+
/** Items for a menu that opens from the control: the More menu. */
|
|
480
|
+
menu?: { id: string; label: string }[]
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/** The dock's controls are the Icon Button atom's own sizes: `md` (36px) or `lg` (40px). */
|
|
484
|
+
export type DockSize = "md" | "lg"
|
|
485
|
+
|
|
486
|
+
export interface ControlsDockProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "onChange"> {
|
|
487
|
+
controls: DockControl[]
|
|
488
|
+
/** `compact` is the round buttons alone; `labelled` stacks a short caption under each. */
|
|
489
|
+
density?: "compact" | "labelled"
|
|
490
|
+
/** The control size from Tablet up: the Icon Button's `md` (36px) or `lg` (40px). */
|
|
491
|
+
size?: DockSize
|
|
492
|
+
/** The control size on phones. Defaults to `size`; a stage dock sets `md` here and keeps `lg` from Tablet. */
|
|
493
|
+
phoneSize?: DockSize
|
|
494
|
+
/** Names the Leave button and is its text: "Leave". */
|
|
495
|
+
leaveLabel: string
|
|
496
|
+
/** What the Leave button says while it waits for the second press: "Press again to leave". */
|
|
497
|
+
leaveConfirmLabel: string
|
|
498
|
+
/** A line for the first mouse click on Leave: "Hold to leave". */
|
|
499
|
+
leaveHint: string
|
|
500
|
+
/** How long Leave must be held, in ms. */
|
|
501
|
+
holdMs?: number
|
|
502
|
+
/** Draws Leave's corners as a pill (`rounded`) or 8px (`standard`), as the compact and labelled rows differ. */
|
|
503
|
+
leaveRadius?: "rounded" | "standard"
|
|
504
|
+
/** Keeps Leave labelled on phones too, as the labelled dock does. */
|
|
505
|
+
leaveLabelled?: boolean
|
|
506
|
+
/** Keeps Leave out of the dock, for a row that is only controls. */
|
|
507
|
+
hideLeave?: boolean
|
|
508
|
+
/** The accessible name of the dock. */
|
|
509
|
+
label?: string
|
|
510
|
+
/** Called when a control is pressed; toggles also say whether they are now pressed. */
|
|
511
|
+
onControl?: (id: string, pressed: boolean) => void
|
|
512
|
+
/** Called with the id of the chosen More-menu item. */
|
|
513
|
+
onMenuAction?: (id: string) => void
|
|
514
|
+
/** Called once Leave is held for `holdMs`, or confirmed with a second press from the keyboard. */
|
|
515
|
+
onLeave?: () => void
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/** Whether the device can hover. Phones can't, so a tooltip becomes a toast. False before the first read. */
|
|
519
|
+
function useCanHover() {
|
|
520
|
+
const [can, setCan] = React.useState(true)
|
|
521
|
+
React.useEffect(() => {
|
|
522
|
+
if (typeof window === "undefined" || typeof window.matchMedia !== "function") return
|
|
523
|
+
setCan(window.matchMedia("(hover: hover)").matches)
|
|
524
|
+
}, [])
|
|
525
|
+
return can
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
interface DockButtonProps {
|
|
529
|
+
control: DockControl
|
|
530
|
+
size: DockSize
|
|
531
|
+
pressed: boolean
|
|
532
|
+
canHover: boolean
|
|
533
|
+
onPress: () => void
|
|
534
|
+
onMenuAction?: (id: string) => void
|
|
535
|
+
onHint: (text: string) => void
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function DockButton({ control, size, pressed, canHover, onPress, onMenuAction, onHint }: DockButtonProps) {
|
|
539
|
+
const Icon = control.icon
|
|
540
|
+
const PressedIcon = control.pressedIcon
|
|
541
|
+
const name = pressed ? (control.pressedLabel ?? control.label) : control.label
|
|
542
|
+
const tip = pressed ? (control.pressedTip ?? name) : name
|
|
543
|
+
const tone = pressed ? (control.kind === "on" ? "secondary" : "destructive") : "primary"
|
|
544
|
+
const button = (
|
|
545
|
+
<Button
|
|
546
|
+
type="button"
|
|
547
|
+
size={size === "md" ? "icon-md" : "icon-lg"}
|
|
548
|
+
radius="rounded"
|
|
549
|
+
tone={tone}
|
|
550
|
+
aria-label={name}
|
|
551
|
+
aria-pressed={control.toggle ? pressed : undefined}
|
|
552
|
+
data-control={control.id}
|
|
553
|
+
onClick={() => {
|
|
554
|
+
onPress()
|
|
555
|
+
if (!canHover) onHint(tip)
|
|
556
|
+
}}
|
|
557
|
+
className={cn("transition-[transform,background-color] duration-150 motion-reduce:transition-none", canHover && "hover:motion-safe:scale-[1.06]", "active:motion-safe:scale-[0.96]")}
|
|
558
|
+
>
|
|
559
|
+
<span className="grid size-4 place-items-center" aria-hidden>
|
|
560
|
+
<Icon className={cn("col-start-1 row-start-1 transition-opacity duration-100 motion-reduce:transition-none", PressedIcon && pressed && "opacity-0")} />
|
|
561
|
+
{PressedIcon ? <PressedIcon className={cn("col-start-1 row-start-1 transition-opacity duration-100 motion-reduce:transition-none", !pressed && "opacity-0")} /> : null}
|
|
562
|
+
</span>
|
|
563
|
+
</Button>
|
|
564
|
+
)
|
|
565
|
+
if (control.menu) {
|
|
566
|
+
return (
|
|
567
|
+
<DropdownMenu>
|
|
568
|
+
<DropdownMenuTrigger
|
|
569
|
+
render={
|
|
570
|
+
<Button
|
|
571
|
+
type="button"
|
|
572
|
+
size={size === "md" ? "icon-md" : "icon-lg"}
|
|
573
|
+
radius="rounded"
|
|
574
|
+
aria-label={name}
|
|
575
|
+
data-control={control.id}
|
|
576
|
+
className={cn("transition-[transform,background-color] duration-150 motion-reduce:transition-none hover:motion-safe:scale-[1.06] active:motion-safe:scale-[0.96]")}
|
|
577
|
+
/>
|
|
578
|
+
}
|
|
579
|
+
>
|
|
580
|
+
<Icon aria-hidden />
|
|
581
|
+
</DropdownMenuTrigger>
|
|
582
|
+
<DropdownMenuContent side="top" align="center">
|
|
583
|
+
{control.menu.map((item) => (
|
|
584
|
+
<DropdownMenuItem key={item.id} onClick={() => onMenuAction?.(item.id)}>
|
|
585
|
+
{item.label}
|
|
586
|
+
</DropdownMenuItem>
|
|
587
|
+
))}
|
|
588
|
+
</DropdownMenuContent>
|
|
589
|
+
</DropdownMenu>
|
|
590
|
+
)
|
|
591
|
+
}
|
|
592
|
+
if (!canHover) return button
|
|
593
|
+
return (
|
|
594
|
+
<Tooltip>
|
|
595
|
+
<TooltipTrigger delay={400} render={button} />
|
|
596
|
+
<TooltipContent side="top">
|
|
597
|
+
<Figures>{tip}</Figures>
|
|
598
|
+
</TooltipContent>
|
|
599
|
+
</Tooltip>
|
|
600
|
+
)
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Leave, held. A mouse or finger has to stay down for `holdMs` (400ms) before
|
|
605
|
+
* `onLeave` is called, with a fill that grows across the button; a quick click
|
|
606
|
+
* only shows the hint. From the keyboard a hold is unreliable, so the first
|
|
607
|
+
* press arms the button ("Press again to leave") and a second press within
|
|
608
|
+
* three seconds leaves. On phones it is the Icon Button's `icon-lg` (40px),
|
|
609
|
+
* icon-only; from Tablet the Button's labelled `lg` (40px).
|
|
610
|
+
*/
|
|
611
|
+
export function LeaveButton({
|
|
612
|
+
label,
|
|
613
|
+
confirmLabel,
|
|
614
|
+
hint,
|
|
615
|
+
holdMs = 400,
|
|
616
|
+
radius = "rounded",
|
|
617
|
+
alwaysLabelled = false,
|
|
618
|
+
onLeave,
|
|
619
|
+
onHint,
|
|
620
|
+
className,
|
|
621
|
+
}: {
|
|
622
|
+
label: string
|
|
623
|
+
confirmLabel: string
|
|
624
|
+
hint: string
|
|
625
|
+
holdMs?: number
|
|
626
|
+
radius?: "rounded" | "standard"
|
|
627
|
+
/** Keeps the label on phones too, as Figma's labelled dock does; otherwise a phone gets an icon-only button. */
|
|
628
|
+
alwaysLabelled?: boolean
|
|
629
|
+
onLeave?: () => void
|
|
630
|
+
onHint?: (text: string) => void
|
|
631
|
+
className?: string
|
|
632
|
+
}) {
|
|
633
|
+
const [armed, setArmed] = React.useState(false)
|
|
634
|
+
const [buttonRef, tier, measured] = useContainerTier<HTMLButtonElement>()
|
|
635
|
+
const fill = React.useRef<HTMLSpanElement>(null)
|
|
636
|
+
const timer = React.useRef<ReturnType<typeof setTimeout>>(undefined)
|
|
637
|
+
const armTimer = React.useRef<ReturnType<typeof setTimeout>>(undefined)
|
|
638
|
+
const animation = React.useRef<Animation>(undefined)
|
|
639
|
+
const held = React.useRef(false)
|
|
640
|
+
|
|
641
|
+
const stop = () => {
|
|
642
|
+
clearTimeout(timer.current)
|
|
643
|
+
animation.current?.cancel()
|
|
644
|
+
}
|
|
645
|
+
React.useEffect(() => () => { stop(); clearTimeout(armTimer.current) }, [])
|
|
646
|
+
|
|
647
|
+
const start = () => {
|
|
648
|
+
held.current = false
|
|
649
|
+
stop()
|
|
650
|
+
if (fill.current && typeof fill.current.animate === "function" && !prefersReducedMotion()) {
|
|
651
|
+
animation.current = fill.current.animate([{ width: "0%" }, { width: "100%" }], { duration: holdMs, easing: "linear", fill: "forwards" })
|
|
652
|
+
}
|
|
653
|
+
timer.current = setTimeout(() => {
|
|
654
|
+
held.current = true
|
|
655
|
+
onLeave?.()
|
|
656
|
+
}, holdMs)
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
return (
|
|
660
|
+
<Button
|
|
661
|
+
ref={buttonRef}
|
|
662
|
+
type="button"
|
|
663
|
+
variant="destructive"
|
|
664
|
+
size={!alwaysLabelled && measured && tier === "mobile" ? "icon-lg" : "lg"}
|
|
665
|
+
radius={radius}
|
|
666
|
+
aria-label={armed ? confirmLabel : label}
|
|
667
|
+
data-control="leave"
|
|
668
|
+
data-armed={armed ? "" : undefined}
|
|
669
|
+
onPointerDown={(event) => {
|
|
670
|
+
if (event.pointerType === "mouse" && event.button !== 0) return
|
|
671
|
+
start()
|
|
672
|
+
}}
|
|
673
|
+
onPointerUp={stop}
|
|
674
|
+
onPointerLeave={stop}
|
|
675
|
+
onPointerCancel={stop}
|
|
676
|
+
onClick={(event) => {
|
|
677
|
+
// A real pointer click has `detail >= 1`; Enter and Space report 0.
|
|
678
|
+
if (event.detail > 0) {
|
|
679
|
+
if (!held.current) onHint?.(hint)
|
|
680
|
+
return
|
|
681
|
+
}
|
|
682
|
+
if (armed) {
|
|
683
|
+
clearTimeout(armTimer.current)
|
|
684
|
+
setArmed(false)
|
|
685
|
+
onLeave?.()
|
|
686
|
+
} else {
|
|
687
|
+
setArmed(true)
|
|
688
|
+
onHint?.(confirmLabel)
|
|
689
|
+
armTimer.current = setTimeout(() => setArmed(false), 3000)
|
|
690
|
+
}
|
|
691
|
+
}}
|
|
692
|
+
// An icon-only phone button and a labelled Tablet one are different atom sizes: wait for the first reading before showing either.
|
|
693
|
+
// Until measured the button is the labelled `lg`; its box is narrowed with the container query so the room it reserves is already the phone's 40px or Tablet's labelled width.
|
|
694
|
+
className={cn("relative overflow-hidden", !alwaysLabelled && !measured && "invisible [@container(max-width:39.99rem)]:w-10 [@container(max-width:39.99rem)]:px-0", className)}
|
|
695
|
+
>
|
|
696
|
+
<span ref={fill} aria-hidden className="pointer-events-none absolute inset-y-0 start-0 w-0 bg-destructive-foreground opacity-20" />
|
|
697
|
+
<PhoneOff aria-hidden className="relative" />
|
|
698
|
+
<span className={cn("relative", !alwaysLabelled && "hidden [@container(min-width:40rem)]:inline")}>{armed ? confirmLabel : label}</span>
|
|
699
|
+
</Button>
|
|
700
|
+
)
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* Figma's controls dock. Toggles keep their own pressed state; every press is
|
|
705
|
+
* reported with `onControl`. A mic or camera that is off turns `destructive`
|
|
706
|
+
* over 160ms while its glyph cross-fades in 100ms; hovering lifts a control to
|
|
707
|
+
* 1.06 and pressing settles it to 0.96; a tooltip opens after 400ms. On a phone
|
|
708
|
+
* there is no hover: the controls only settle on press, a tooltip becomes a
|
|
709
|
+
* 1.6s toast above the dock, and the dock drops the controls marked
|
|
710
|
+
* `hideOnPhone`. In RTL the whole dock mirrors, so Leave stays on the side the
|
|
711
|
+
* eye leaves from.
|
|
712
|
+
*/
|
|
713
|
+
export function ControlsDock({
|
|
714
|
+
controls,
|
|
715
|
+
density = "compact",
|
|
716
|
+
size = "lg",
|
|
717
|
+
phoneSize,
|
|
718
|
+
leaveLabel,
|
|
719
|
+
leaveConfirmLabel,
|
|
720
|
+
leaveHint,
|
|
721
|
+
holdMs,
|
|
722
|
+
leaveRadius = "rounded",
|
|
723
|
+
leaveLabelled = false,
|
|
724
|
+
hideLeave = false,
|
|
725
|
+
label,
|
|
726
|
+
onControl,
|
|
727
|
+
onMenuAction,
|
|
728
|
+
onLeave,
|
|
729
|
+
className,
|
|
730
|
+
...props
|
|
731
|
+
}: ControlsDockProps) {
|
|
732
|
+
const [pressed, setPressed] = React.useState<Record<string, boolean>>(() => Object.fromEntries(controls.map((c) => [c.id, Boolean(c.defaultPressed)])))
|
|
733
|
+
const canHover = useCanHover()
|
|
734
|
+
const toast = useVideoCallToast()
|
|
735
|
+
const [dockRef, tier, measured] = useContainerTier<HTMLDivElement>()
|
|
736
|
+
const buttonSize = tier === "mobile" ? (phoneSize ?? size) : size
|
|
737
|
+
// Only a dock whose size changes with the tier waits for its first reading.
|
|
738
|
+
const waiting = !measured && phoneSize !== undefined && phoneSize !== size
|
|
739
|
+
const isPressed = (c: DockControl) => pressed[c.id] ?? Boolean(c.defaultPressed)
|
|
740
|
+
return (
|
|
741
|
+
<div ref={dockRef} className={cn("relative", className)} {...props}>
|
|
742
|
+
<div role="group" aria-label={label} className={cn("flex items-start justify-center", density === "labelled" ? "gap-2 [@container(min-width:40rem)]:gap-5" : "gap-2", waiting && "invisible")}>
|
|
743
|
+
{controls.map((control) => {
|
|
744
|
+
const on = isPressed(control)
|
|
745
|
+
const button = (
|
|
746
|
+
<DockButton
|
|
747
|
+
control={control}
|
|
748
|
+
size={buttonSize}
|
|
749
|
+
pressed={on}
|
|
750
|
+
canHover={canHover}
|
|
751
|
+
onHint={toast.show}
|
|
752
|
+
onMenuAction={onMenuAction}
|
|
753
|
+
onPress={() => {
|
|
754
|
+
const next = control.toggle ? !on : false
|
|
755
|
+
if (control.toggle) setPressed((state) => ({ ...state, [control.id]: next }))
|
|
756
|
+
onControl?.(control.id, next)
|
|
757
|
+
}}
|
|
758
|
+
/>
|
|
759
|
+
)
|
|
760
|
+
return (
|
|
761
|
+
<div
|
|
762
|
+
key={control.id}
|
|
763
|
+
className={cn(
|
|
764
|
+
density === "labelled" ? "flex flex-col items-center gap-1.5" : "block",
|
|
765
|
+
control.hideOnPhone && (density === "labelled" ? "hidden [@container(min-width:40rem)]:flex" : "hidden [@container(min-width:40rem)]:block")
|
|
766
|
+
)}
|
|
767
|
+
>
|
|
768
|
+
{button}
|
|
769
|
+
{density === "labelled" ? <span className="text-xs text-muted-foreground">{on ? (control.pressedCaption ?? control.caption ?? control.label) : (control.caption ?? control.label)}</span> : null}
|
|
770
|
+
</div>
|
|
771
|
+
)
|
|
772
|
+
})}
|
|
773
|
+
{hideLeave ? null : (
|
|
774
|
+
<LeaveButton label={leaveLabel} confirmLabel={leaveConfirmLabel} hint={leaveHint} holdMs={holdMs} radius={leaveRadius} alwaysLabelled={leaveLabelled} onLeave={onLeave} onHint={toast.show} />
|
|
775
|
+
)}
|
|
776
|
+
</div>
|
|
777
|
+
<VideoCallToast message={toast.message} className="absolute inset-x-0 bottom-full mx-auto mb-2 w-fit max-w-full" />
|
|
778
|
+
</div>
|
|
779
|
+
)
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/* ═══════════════════════════ Call stages ═══════════════════════════
|
|
783
|
+
* What the four call-stage Types share: the stage card, the people on it, the
|
|
784
|
+
* stage dock's controls, the container tier a script can read, the FLIP that
|
|
785
|
+
* moves tiles when people come and go, and the side panel.
|
|
786
|
+
*/
|
|
787
|
+
|
|
788
|
+
/** Fills `{name}`-style holes in a copy string. */
|
|
789
|
+
export const fillTemplate = (template: string, values: Record<string, string | number>) =>
|
|
790
|
+
template.replace(/\{(\w+)\}/g, (_, key: string) => String(values[key] ?? ""))
|
|
791
|
+
|
|
792
|
+
/** The surface of a card in Figma's Video Call set: `card`, a 1px `border`, a 16px radius. */
|
|
793
|
+
export const VIDEO_CALL_CARD = "rounded-2xl border border-border bg-card"
|
|
794
|
+
|
|
795
|
+
/** The copy props a caller passed, so they win over the defaults. `keys` are the copy's own keys. */
|
|
796
|
+
export function passedCopy(keys: readonly string[], props: Record<string, unknown>) {
|
|
797
|
+
return Object.fromEntries(Object.entries(props).filter(([key, value]) => keys.includes(key) && value !== undefined))
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/** The props that are not copy, for the element the block renders. */
|
|
801
|
+
export function restOf<T extends Record<string, unknown>>(keys: readonly string[], props: T) {
|
|
802
|
+
return Object.fromEntries(Object.entries(props).filter(([key]) => !keys.includes(key))) as Partial<T>
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/** A person on a call stage. Their photo comes from the block's `photos`, by `id`. */
|
|
806
|
+
export interface VideoCallParticipant {
|
|
807
|
+
/** Keys the person, and picks their photo. */
|
|
808
|
+
id: string
|
|
809
|
+
/** The name on the chip: `Karim Nasser`. */
|
|
810
|
+
name: string
|
|
811
|
+
/** The short form a phone or a strip shows: `Adam F.`. */
|
|
812
|
+
shortName?: string
|
|
813
|
+
/** One or two letters for a camera that is off. A person with no photo and no initials shows a plain `muted` tile. */
|
|
814
|
+
initials?: string
|
|
815
|
+
/** Shows the mic-off mark on their chip. */
|
|
816
|
+
muted?: boolean
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/** The photos a stage shows, by participant `id`. A person without one shows their initials. */
|
|
820
|
+
export type VideoCallPhotos = Record<string, VideoCallPhoto>
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* Figma's Call Stage: a `card` surface, 16px radius, 1px `border`, 16px apart
|
|
824
|
+
* inside. A phone's stage hugs its tiles, 4px at the sides.
|
|
825
|
+
*/
|
|
826
|
+
export function CallStage({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
|
827
|
+
return (
|
|
828
|
+
<div
|
|
829
|
+
data-call-stage=""
|
|
830
|
+
className={cn("flex w-full flex-col gap-4 rounded-2xl border border-border bg-card px-1 py-4 [@container(min-width:40rem)]:p-4", className)}
|
|
831
|
+
{...props}
|
|
832
|
+
/>
|
|
833
|
+
)
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/* ───────────────────────────── Stage dock ───────────────────────────── */
|
|
837
|
+
|
|
838
|
+
export interface StageDockCopy {
|
|
839
|
+
/** Names the dock. */
|
|
840
|
+
label: string
|
|
841
|
+
mic: { label: string; pressedLabel: string; tip: string }
|
|
842
|
+
video: { label: string; pressedLabel: string }
|
|
843
|
+
share: string
|
|
844
|
+
chat: string
|
|
845
|
+
reactions: string
|
|
846
|
+
hand: { label: string; pressedLabel: string }
|
|
847
|
+
more: string
|
|
848
|
+
menu: { id: string; label: string }[]
|
|
849
|
+
/** Leave's label, and its two lines of help. */
|
|
850
|
+
leave: string
|
|
851
|
+
leaveConfirm: string
|
|
852
|
+
leaveHint: string
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
/** The dock every call stage carries, English and Arabic. */
|
|
856
|
+
export const STAGE_DOCK_COPY: Record<VideoCallLocale, StageDockCopy> = {
|
|
857
|
+
en: {
|
|
858
|
+
label: "Call controls",
|
|
859
|
+
mic: { label: "Mute", pressedLabel: "Unmute", tip: "You're muted. Press ⌘ D to talk" },
|
|
860
|
+
video: { label: "Stop video", pressedLabel: "Start video" },
|
|
861
|
+
share: "Share screen",
|
|
862
|
+
chat: "Chat",
|
|
863
|
+
reactions: "Reactions",
|
|
864
|
+
hand: { label: "Raise hand", pressedLabel: "Lower hand" },
|
|
865
|
+
more: "More",
|
|
866
|
+
menu: [
|
|
867
|
+
{ id: "settings", label: "Settings" },
|
|
868
|
+
{ id: "report", label: "Report a problem" },
|
|
869
|
+
],
|
|
870
|
+
leave: "Leave",
|
|
871
|
+
leaveConfirm: "Press again to leave",
|
|
872
|
+
leaveHint: "Hold to leave",
|
|
873
|
+
},
|
|
874
|
+
ar: {
|
|
875
|
+
label: "عناصر التحكم في المكالمة",
|
|
876
|
+
mic: { label: "كتم", pressedLabel: "إلغاء الكتم", tip: "الميكروفون مكتوم. اضغط ⌘ D للتحدث" },
|
|
877
|
+
video: { label: "إيقاف الفيديو", pressedLabel: "تشغيل الفيديو" },
|
|
878
|
+
share: "مشاركة الشاشة",
|
|
879
|
+
chat: "الدردشة",
|
|
880
|
+
reactions: "التفاعلات",
|
|
881
|
+
hand: { label: "رفع اليد", pressedLabel: "خفض اليد" },
|
|
882
|
+
more: "المزيد",
|
|
883
|
+
menu: [
|
|
884
|
+
{ id: "settings", label: "الإعدادات" },
|
|
885
|
+
{ id: "report", label: "الإبلاغ عن مشكلة" },
|
|
886
|
+
],
|
|
887
|
+
leave: "خروج",
|
|
888
|
+
leaveConfirm: "اضغط مرة أخرى للخروج",
|
|
889
|
+
leaveHint: "اضغط مطولًا للخروج",
|
|
890
|
+
},
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
/**
|
|
894
|
+
* The seven controls of a call stage's dock, in Figma's order: mic, camera,
|
|
895
|
+
* share, chat, reactions, hand and More. A phone keeps five: share and
|
|
896
|
+
* reactions drop out. `chat: "toggle"` makes Chat a toggle that keeps its own
|
|
897
|
+
* pressed state, for a stage whose chat opens a panel.
|
|
898
|
+
*/
|
|
899
|
+
export function stageDockControls(d: StageDockCopy, { chat = "button" }: { chat?: "button" | "toggle" } = {}): DockControl[] {
|
|
900
|
+
return [
|
|
901
|
+
{ id: "mic", icon: Mic, pressedIcon: MicOff, label: d.mic.label, pressedLabel: d.mic.pressedLabel, pressedTip: d.mic.tip, toggle: true },
|
|
902
|
+
{ id: "video", icon: Video, pressedIcon: VideoOff, label: d.video.label, pressedLabel: d.video.pressedLabel, toggle: true },
|
|
903
|
+
{ id: "screen-share", icon: ScreenShare, label: d.share, hideOnPhone: true },
|
|
904
|
+
{ id: "chat", icon: MessageSquare, label: d.chat, kind: "on", toggle: chat === "toggle" },
|
|
905
|
+
{ id: "reactions", icon: Smile, label: d.reactions, hideOnPhone: true },
|
|
906
|
+
{ id: "hand", icon: Hand, label: d.hand.label, pressedLabel: d.hand.pressedLabel, kind: "on", toggle: true },
|
|
907
|
+
{ id: "more", icon: Ellipsis, label: d.more, menu: d.menu },
|
|
908
|
+
]
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
/* ───────────────────── Container tier, FLIP and debounce ───────────────────── */
|
|
912
|
+
|
|
913
|
+
export type ContainerTier = "mobile" | "tablet" | "desktop"
|
|
914
|
+
|
|
915
|
+
/**
|
|
916
|
+
* Which of Figma's three breakpoints the block's own width is at, the same
|
|
917
|
+
* 640px and 1024px of content the container queries use. A script reads this
|
|
918
|
+
* where a class can't act: which way a wipe runs, which axis a panel opens on.
|
|
919
|
+
*/
|
|
920
|
+
export function containerTier(node: Element | null): ContainerTier {
|
|
921
|
+
const root = node?.closest<HTMLElement>("[data-video-call]")
|
|
922
|
+
if (!root || typeof getComputedStyle !== "function") return "mobile"
|
|
923
|
+
const style = getComputedStyle(root)
|
|
924
|
+
const width = root.clientWidth - (parseFloat(style.paddingInlineStart) || 0) - (parseFloat(style.paddingInlineEnd) || 0)
|
|
925
|
+
return width >= 1024 ? "desktop" : width >= 640 ? "tablet" : "mobile"
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
/**
|
|
929
|
+
* The tier of the section a node is in, kept up to date as the section resizes.
|
|
930
|
+
* The third value is `false` until the first reading: the layout effect sets
|
|
931
|
+
* it before the browser paints, so on the client nothing wrong is ever drawn,
|
|
932
|
+
* but server-rendered HTML is painted before React hydrates. A part whose atom
|
|
933
|
+
* size depends on the tier hides itself until then (`invisible`), so a reader
|
|
934
|
+
* on Tablet never sees the phone size flash first.
|
|
935
|
+
*/
|
|
936
|
+
export function useContainerTier<T extends HTMLElement>() {
|
|
937
|
+
const ref = React.useRef<T>(null)
|
|
938
|
+
const [tier, setTier] = React.useState<ContainerTier>("mobile")
|
|
939
|
+
const [measured, setMeasured] = React.useState(false)
|
|
940
|
+
useIsomorphicLayoutEffect(() => {
|
|
941
|
+
const node = ref.current
|
|
942
|
+
if (!node) return
|
|
943
|
+
const read = () => {
|
|
944
|
+
setTier(containerTier(node))
|
|
945
|
+
setMeasured(true)
|
|
946
|
+
}
|
|
947
|
+
read()
|
|
948
|
+
const root = node.closest("[data-video-call]")
|
|
949
|
+
if (!root || typeof ResizeObserver === "undefined") return
|
|
950
|
+
const observer = new ResizeObserver(read)
|
|
951
|
+
observer.observe(root)
|
|
952
|
+
return () => observer.disconnect()
|
|
953
|
+
}, [])
|
|
954
|
+
return [ref, tier, measured] as const
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
/**
|
|
958
|
+
* FLIP for a list: every child marked `data-flip="<id>"` slides from where it
|
|
959
|
+
* was to where it is, over 260ms ease-out, whenever `key` changes, so the
|
|
960
|
+
* tiles that stay travel instead of jumping when someone joins or leaves. A
|
|
961
|
+
* tile that is new fades in. Reduced motion skips it. It measures, so it works
|
|
962
|
+
* in either direction.
|
|
963
|
+
*/
|
|
964
|
+
export function useFlip<T extends HTMLElement>(key: string, duration = 260) {
|
|
965
|
+
const ref = React.useRef<T>(null)
|
|
966
|
+
const rects = React.useRef(new Map<string, DOMRect>())
|
|
967
|
+
const last = React.useRef<string | undefined>(undefined)
|
|
968
|
+
useIsomorphicLayoutEffect(() => {
|
|
969
|
+
const node = ref.current
|
|
970
|
+
if (!node) return
|
|
971
|
+
const items = Array.from(node.querySelectorAll<HTMLElement>("[data-flip]"))
|
|
972
|
+
const next = new Map<string, DOMRect>()
|
|
973
|
+
for (const el of items) next.set(el.dataset.flip!, el.getBoundingClientRect())
|
|
974
|
+
if (last.current !== undefined && last.current !== key && !prefersReducedMotion()) {
|
|
975
|
+
for (const el of items) {
|
|
976
|
+
if (typeof el.animate !== "function") continue
|
|
977
|
+
const before = rects.current.get(el.dataset.flip!)
|
|
978
|
+
const after = next.get(el.dataset.flip!)!
|
|
979
|
+
if (!before) {
|
|
980
|
+
el.animate([{ opacity: 0 }, { opacity: 1 }], { duration: BASE_MS, easing: STANDARD })
|
|
981
|
+
continue
|
|
982
|
+
}
|
|
983
|
+
const dx = before.left - after.left
|
|
984
|
+
const dy = before.top - after.top
|
|
985
|
+
if (Math.abs(dx) < 1 && Math.abs(dy) < 1) continue
|
|
986
|
+
el.animate([{ transform: `translate(${dx}px, ${dy}px)` }, { transform: "none" }], { duration, easing: "ease-out" })
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
rects.current = next
|
|
990
|
+
last.current = key
|
|
991
|
+
}, [key, duration])
|
|
992
|
+
return ref
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
/** A value that follows `value` after it has held still for `ms`: the active-speaker ring never strobes between two voices. */
|
|
996
|
+
export function useDebouncedValue<T>(value: T, ms: number): T {
|
|
997
|
+
const [held, setHeld] = React.useState(value)
|
|
998
|
+
React.useEffect(() => {
|
|
999
|
+
if (Object.is(value, held)) return
|
|
1000
|
+
const timer = setTimeout(() => setHeld(value), ms)
|
|
1001
|
+
return () => clearTimeout(timer)
|
|
1002
|
+
}, [value, ms, held])
|
|
1003
|
+
return held
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
/* ───────────────────────────── Side panel ───────────────────────────── */
|
|
1007
|
+
|
|
1008
|
+
export interface SidePanelPerson {
|
|
1009
|
+
id: string
|
|
1010
|
+
name: string
|
|
1011
|
+
/** Their role: Host, Guest. */
|
|
1012
|
+
role?: string
|
|
1013
|
+
photo?: VideoCallPhoto
|
|
1014
|
+
initials?: string
|
|
1015
|
+
/** Shows the mic as off, in `destructive`. */
|
|
1016
|
+
muted?: boolean
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
export interface SidePanelMessage {
|
|
1020
|
+
id: string
|
|
1021
|
+
author: string
|
|
1022
|
+
text: string
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
export interface SidePanelCopy {
|
|
1026
|
+
/** The panel's heading, and its name. */
|
|
1027
|
+
title: string
|
|
1028
|
+
/** Names the close button. */
|
|
1029
|
+
close: string
|
|
1030
|
+
/** Names the tab list. */
|
|
1031
|
+
tabsLabel: string
|
|
1032
|
+
participants: string
|
|
1033
|
+
chat: string
|
|
1034
|
+
/** Names the search field and is its placeholder. */
|
|
1035
|
+
search: string
|
|
1036
|
+
/** The line above the list; `{count}` is filled in. */
|
|
1037
|
+
inCall: string
|
|
1038
|
+
/** What the list says when the search finds no one. */
|
|
1039
|
+
noResults: string
|
|
1040
|
+
/** What the Chat tab says with no messages. */
|
|
1041
|
+
noMessages: string
|
|
1042
|
+
/** Names the Chat tab's dot. */
|
|
1043
|
+
unread: string
|
|
1044
|
+
invite: string
|
|
1045
|
+
/** Names a person's menu button; `{name}` is filled in. */
|
|
1046
|
+
more: string
|
|
1047
|
+
/** Names the mic mark of someone who is muted. */
|
|
1048
|
+
muted: string
|
|
1049
|
+
/** Names the handle that resizes the sheet on a phone. */
|
|
1050
|
+
handle: string
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
export type SidePanelSnap = "60" | "92"
|
|
1054
|
+
|
|
1055
|
+
export interface SidePanelProps extends Omit<React.HTMLAttributes<HTMLElement>, "title" | "onChange"> {
|
|
1056
|
+
copy: SidePanelCopy
|
|
1057
|
+
people: SidePanelPerson[]
|
|
1058
|
+
/** The Chat tab's messages. */
|
|
1059
|
+
messages?: SidePanelMessage[]
|
|
1060
|
+
/** Puts the dot on the Chat tab, scaling in from nothing, until the Chat tab is opened. */
|
|
1061
|
+
unread?: boolean
|
|
1062
|
+
/** The open tab, when you control it. */
|
|
1063
|
+
tab?: "participants" | "chat"
|
|
1064
|
+
defaultTab?: "participants" | "chat"
|
|
1065
|
+
onTabChange?: (tab: "participants" | "chat") => void
|
|
1066
|
+
/** Makes the panel a bottom sheet on a phone: a handle on top, and a height of 60% that a drag or a press steps to 92%. From Tablet it is a plain card. */
|
|
1067
|
+
sheet?: boolean
|
|
1068
|
+
/** The sheet's height step, when you control it. */
|
|
1069
|
+
snap?: SidePanelSnap
|
|
1070
|
+
defaultSnap?: SidePanelSnap
|
|
1071
|
+
onSnapChange?: (snap: SidePanelSnap) => void
|
|
1072
|
+
/** Called when the close button is pressed; without it the panel has none. */
|
|
1073
|
+
onClose?: () => void
|
|
1074
|
+
/** Called when Invite is pressed. */
|
|
1075
|
+
onInvite?: () => void
|
|
1076
|
+
/** Called with a person's id when their menu button is pressed. */
|
|
1077
|
+
onPersonMenu?: (id: string) => void
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
function useControlled<T>(value: T | undefined, initial: T, onChange?: (next: T) => void) {
|
|
1081
|
+
const [own, setOwn] = React.useState(initial)
|
|
1082
|
+
const current = value ?? own
|
|
1083
|
+
const set = (next: T) => {
|
|
1084
|
+
if (value === undefined) setOwn(next)
|
|
1085
|
+
onChange?.(next)
|
|
1086
|
+
}
|
|
1087
|
+
return [current, set] as const
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
/** The sheet's grab handle: a press steps between the two heights, and a drag up or down of 24px picks one. */
|
|
1091
|
+
function SheetHandle({ label, snap, onSnap }: { label: string; snap: SidePanelSnap; onSnap: (snap: SidePanelSnap) => void }) {
|
|
1092
|
+
const start = React.useRef<number | null>(null)
|
|
1093
|
+
const dragged = React.useRef(false)
|
|
1094
|
+
return (
|
|
1095
|
+
<button
|
|
1096
|
+
type="button"
|
|
1097
|
+
aria-label={label}
|
|
1098
|
+
aria-expanded={snap === "92"}
|
|
1099
|
+
onPointerDown={(event) => {
|
|
1100
|
+
start.current = event.clientY
|
|
1101
|
+
dragged.current = false
|
|
1102
|
+
}}
|
|
1103
|
+
onPointerUp={(event) => {
|
|
1104
|
+
if (start.current === null) return
|
|
1105
|
+
const dy = event.clientY - start.current
|
|
1106
|
+
start.current = null
|
|
1107
|
+
if (Math.abs(dy) >= 24) {
|
|
1108
|
+
dragged.current = true
|
|
1109
|
+
onSnap(dy < 0 ? "92" : "60")
|
|
1110
|
+
}
|
|
1111
|
+
}}
|
|
1112
|
+
onPointerCancel={() => (start.current = null)}
|
|
1113
|
+
onClick={() => {
|
|
1114
|
+
if (dragged.current) {
|
|
1115
|
+
dragged.current = false
|
|
1116
|
+
return
|
|
1117
|
+
}
|
|
1118
|
+
onSnap(snap === "92" ? "60" : "92")
|
|
1119
|
+
}}
|
|
1120
|
+
className="-mt-2 flex h-6 w-full shrink-0 items-center justify-center rounded-md outline-none [touch-action:none] focus-visible:ring-2 focus-visible:ring-ring [@container(min-width:40rem)]:hidden"
|
|
1121
|
+
>
|
|
1122
|
+
<span aria-hidden className="h-1 w-9 rounded-full bg-muted-foreground" />
|
|
1123
|
+
</button>
|
|
1124
|
+
)
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
/** A person's row. A row that arrives after the first render grows in over 180ms, height and opacity together. */
|
|
1128
|
+
function PersonRow({ person, copy, isNew, onMenu }: { person: SidePanelPerson; copy: SidePanelCopy; isNew: boolean; onMenu?: (id: string) => void }) {
|
|
1129
|
+
const ref = React.useRef<HTMLLIElement>(null)
|
|
1130
|
+
useIsomorphicLayoutEffect(() => {
|
|
1131
|
+
const node = ref.current
|
|
1132
|
+
if (!isNew || !node || typeof node.animate !== "function") return
|
|
1133
|
+
const height = node.offsetHeight
|
|
1134
|
+
node.animate(prefersReducedMotion() ? [{ opacity: 0 }, { opacity: 1 }] : [{ height: "0px", opacity: 0, overflow: "hidden" }, { height: `${height}px`, opacity: 1, overflow: "hidden" }], {
|
|
1135
|
+
duration: BASE_MS,
|
|
1136
|
+
easing: STANDARD,
|
|
1137
|
+
})
|
|
1138
|
+
}, [])
|
|
1139
|
+
const Mark = person.muted ? MicOff : Mic
|
|
1140
|
+
return (
|
|
1141
|
+
<li ref={ref} data-person={person.id} className="flex items-center gap-3 rounded-lg p-2">
|
|
1142
|
+
<Avatar size="32" radius="standard" variant={person.photo ? "image" : "initials"} src={person.photo?.src} name={person.name} />
|
|
1143
|
+
<div className="flex min-w-0 flex-1 flex-col">
|
|
1144
|
+
<span className="truncate text-sm font-medium text-foreground">
|
|
1145
|
+
<Figures>{person.name}</Figures>
|
|
1146
|
+
</span>
|
|
1147
|
+
{person.role ? <span className="truncate text-xs text-muted-foreground">{person.role}</span> : null}
|
|
1148
|
+
</div>
|
|
1149
|
+
<Mark aria-hidden className={cn("size-4 shrink-0", person.muted ? "text-destructive" : "text-muted-foreground")} />
|
|
1150
|
+
{person.muted ? <span className="sr-only">{copy.muted}</span> : null}
|
|
1151
|
+
<Button type="button" variant="ghost" tone="secondary" size="icon-xs" aria-label={fillTemplate(copy.more, { name: person.name })} onClick={() => onMenu?.(person.id)}>
|
|
1152
|
+
<Ellipsis aria-hidden />
|
|
1153
|
+
</Button>
|
|
1154
|
+
</li>
|
|
1155
|
+
)
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
/**
|
|
1159
|
+
* Figma's side panel: a heading with a close button, a Participants / Chat
|
|
1160
|
+
* tab pair, a people search and the list, and Invite at the foot. The search
|
|
1161
|
+
* filters for real; the tabs cross-fade their content in 120ms; the Chat tab's
|
|
1162
|
+
* dot scales in from 0; a row that arrives grows in. As a sheet on a phone it
|
|
1163
|
+
* is 60% tall with a handle that steps it to 92%. A card, 360px wide beside the
|
|
1164
|
+
* stage on Desktop and full width below it from Tablet.
|
|
1165
|
+
*/
|
|
1166
|
+
export function SidePanel({
|
|
1167
|
+
copy,
|
|
1168
|
+
people,
|
|
1169
|
+
messages = [],
|
|
1170
|
+
unread = false,
|
|
1171
|
+
tab,
|
|
1172
|
+
defaultTab = "participants",
|
|
1173
|
+
onTabChange,
|
|
1174
|
+
sheet = false,
|
|
1175
|
+
snap,
|
|
1176
|
+
defaultSnap = "60",
|
|
1177
|
+
onSnapChange,
|
|
1178
|
+
onClose,
|
|
1179
|
+
onInvite,
|
|
1180
|
+
onPersonMenu,
|
|
1181
|
+
className,
|
|
1182
|
+
...props
|
|
1183
|
+
}: SidePanelProps) {
|
|
1184
|
+
const [current, setTab] = useControlled(tab, defaultTab, onTabChange)
|
|
1185
|
+
const [size, setSize] = useControlled(snap, defaultSnap, onSnapChange)
|
|
1186
|
+
const [query, setQuery] = React.useState("")
|
|
1187
|
+
const content = React.useRef<HTMLDivElement>(null)
|
|
1188
|
+
const dot = React.useRef<HTMLSpanElement>(null)
|
|
1189
|
+
const seen = React.useRef<Set<string> | null>(null)
|
|
1190
|
+
const firstTab = React.useRef(true)
|
|
1191
|
+
const shown = people.filter((person) => person.name.toLowerCase().includes(query.trim().toLowerCase()))
|
|
1192
|
+
const fresh = new Set(seen.current ? people.filter((person) => !seen.current!.has(person.id)).map((person) => person.id) : [])
|
|
1193
|
+
React.useEffect(() => {
|
|
1194
|
+
seen.current = new Set(people.map((person) => person.id))
|
|
1195
|
+
})
|
|
1196
|
+
React.useEffect(() => {
|
|
1197
|
+
if (firstTab.current) {
|
|
1198
|
+
firstTab.current = false
|
|
1199
|
+
return
|
|
1200
|
+
}
|
|
1201
|
+
if (content.current) playEntrance([content.current], { rise: 0, stagger: false })
|
|
1202
|
+
}, [current])
|
|
1203
|
+
React.useEffect(() => {
|
|
1204
|
+
const node = dot.current
|
|
1205
|
+
if (!unread || !node || typeof node.animate !== "function") return
|
|
1206
|
+
node.animate(prefersReducedMotion() ? [{ opacity: 0 }, { opacity: 1 }] : [{ transform: "scale(0)" }, { transform: "scale(1)" }], { duration: BASE_MS, easing: STANDARD })
|
|
1207
|
+
}, [unread, current])
|
|
1208
|
+
|
|
1209
|
+
return (
|
|
1210
|
+
<aside
|
|
1211
|
+
aria-label={copy.title}
|
|
1212
|
+
data-side-panel=""
|
|
1213
|
+
data-snap={sheet ? size : undefined}
|
|
1214
|
+
className={cn(
|
|
1215
|
+
"flex min-h-0 w-full flex-col gap-4 rounded-2xl border border-border bg-card p-5",
|
|
1216
|
+
sheet && "h-[468px] transition-[height] duration-200 motion-reduce:transition-none data-[snap=92]:h-[600px] [@container(min-width:40rem)]:h-auto",
|
|
1217
|
+
className
|
|
1218
|
+
)}
|
|
1219
|
+
{...props}
|
|
1220
|
+
>
|
|
1221
|
+
{sheet ? <SheetHandle label={copy.handle} snap={size} onSnap={setSize} /> : null}
|
|
1222
|
+
<div className="flex items-center justify-between gap-3">
|
|
1223
|
+
<h3 className="text-base font-semibold text-foreground">{copy.title}</h3>
|
|
1224
|
+
{onClose ? (
|
|
1225
|
+
<Button type="button" variant="ghost" tone="secondary" size="icon-xs" aria-label={copy.close} onClick={onClose}>
|
|
1226
|
+
<X aria-hidden />
|
|
1227
|
+
</Button>
|
|
1228
|
+
) : null}
|
|
1229
|
+
</div>
|
|
1230
|
+
<Tabs value={current} onValueChange={(value) => setTab(value as "participants" | "chat")} className="flex min-h-0 flex-1 flex-col gap-4">
|
|
1231
|
+
<TabsList variant="bordered" aria-label={copy.tabsLabel}>
|
|
1232
|
+
<TabsTrigger value="participants">{copy.participants}</TabsTrigger>
|
|
1233
|
+
<TabsTrigger value="chat">
|
|
1234
|
+
<span className="relative">
|
|
1235
|
+
{copy.chat}
|
|
1236
|
+
{unread && current !== "chat" ? <span ref={dot} role="img" aria-label={copy.unread} className="absolute -top-1 -end-2 size-2 rounded-full bg-destructive" /> : null}
|
|
1237
|
+
</span>
|
|
1238
|
+
</TabsTrigger>
|
|
1239
|
+
</TabsList>
|
|
1240
|
+
<div ref={content} className="flex min-h-0 flex-1 flex-col gap-4">
|
|
1241
|
+
<TabsContent value="participants" className="flex min-h-0 flex-1 flex-col gap-4">
|
|
1242
|
+
<Input size="lg" aria-label={copy.search} placeholder={copy.search} startIcon={<Search aria-hidden />} value={query} onChange={(event) => setQuery(event.target.value)} />
|
|
1243
|
+
<div className="flex min-h-0 flex-1 flex-col gap-1 overflow-y-auto [@container(min-width:40rem)]:max-h-[22rem]">
|
|
1244
|
+
<p className="text-xs font-medium text-muted-foreground">
|
|
1245
|
+
<Figures>{fillTemplate(copy.inCall, { count: people.length })}</Figures>
|
|
1246
|
+
</p>
|
|
1247
|
+
{shown.length ? (
|
|
1248
|
+
<ul className="flex flex-col gap-1">
|
|
1249
|
+
{shown.map((person) => (
|
|
1250
|
+
<PersonRow key={person.id} person={person} copy={copy} isNew={fresh.has(person.id)} onMenu={onPersonMenu} />
|
|
1251
|
+
))}
|
|
1252
|
+
</ul>
|
|
1253
|
+
) : (
|
|
1254
|
+
<p role="status" className="py-4 text-sm text-muted-foreground">{copy.noResults}</p>
|
|
1255
|
+
)}
|
|
1256
|
+
</div>
|
|
1257
|
+
</TabsContent>
|
|
1258
|
+
<TabsContent value="chat" className="flex min-h-0 flex-1 flex-col gap-3 overflow-y-auto">
|
|
1259
|
+
{messages.length ? (
|
|
1260
|
+
messages.map((message) => (
|
|
1261
|
+
<p key={message.id} className="flex flex-col gap-0.5 text-sm">
|
|
1262
|
+
<span className="font-semibold text-foreground">
|
|
1263
|
+
<Figures>{message.author}</Figures>
|
|
1264
|
+
</span>
|
|
1265
|
+
<span className="text-muted-foreground">
|
|
1266
|
+
<Figures>{message.text}</Figures>
|
|
1267
|
+
</span>
|
|
1268
|
+
</p>
|
|
1269
|
+
))
|
|
1270
|
+
) : (
|
|
1271
|
+
<p className="py-4 text-sm text-muted-foreground">{copy.noMessages}</p>
|
|
1272
|
+
)}
|
|
1273
|
+
</TabsContent>
|
|
1274
|
+
</div>
|
|
1275
|
+
</Tabs>
|
|
1276
|
+
<Button type="button" size="lg" className="w-full" onClick={onInvite}>
|
|
1277
|
+
<UserPlus aria-hidden />
|
|
1278
|
+
{copy.invite}
|
|
1279
|
+
</Button>
|
|
1280
|
+
</aside>
|
|
1281
|
+
)
|
|
1282
|
+
}
|