state of startups 2025 (#36047)

* state of startups page init

* state of startups page design

* finish dark aware design

* embed placeholder typescript form

* embed typeform in popup

* newsletter form to hubspot

* update typeform id and increase circle size on mobile

* update meta description

* promo

* fix line height and yc

* update subheading

* fix conflict and error handling

* fix anime js
This commit is contained in:
Francesco Sansalvadore
2025-06-03 15:42:43 +02:00
committed by GitHub
parent 6ac53686cb
commit 4f6e65a6f5
14 changed files with 803 additions and 10 deletions

View File

@@ -0,0 +1,75 @@
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
}
const isValidEmail = (email: string): boolean => {
const emailPattern = /^[\w-\.+]+@([\w-]+\.)+[\w-]{2,8}$/
return emailPattern.test(email)
}
export async function POST(req: Request) {
const HUBSPOT_PORTAL_ID = process.env.HUBSPOT_PORTAL_ID
const HUBSPOT_FORM_GUID = '721fc4aa-13eb-4c25-91be-4fe9b530bed1'
const body = await req.json()
const { email } = body
if (!email) {
return new Response(JSON.stringify({ message: 'All fields are required' }), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 422,
})
}
// Validate email
if (email && !isValidEmail(email)) {
return new Response(JSON.stringify({ message: 'Invalid email address' }), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 422,
})
}
try {
const response = await fetch(
`https://api.hsforms.com/submissions/v3/integration/submit/${HUBSPOT_PORTAL_ID}/${HUBSPOT_FORM_GUID}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
fields: [{ objectTypeId: '0-1', name: 'email', value: email }],
context: {
pageUri: 'https://supabase.com/state-of-startups',
pageName: 'State of Startups 2025',
},
legalConsentOptions: {
consent: {
consentToProcess: true,
text: 'By submitting this form, I confirm that I have read and understood the Privacy Policy.',
},
},
}),
}
)
if (!response.ok) {
const errorData = await response.json()
return new Response(JSON.stringify({ message: errorData.message }), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: response.status,
})
}
return new Response(JSON.stringify({ message: 'Submission successful' }), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 200,
})
} catch (error: any) {
return new Response(JSON.stringify({ error: error.message }), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 500,
})
}
}

View File

@@ -16,6 +16,12 @@ const Hero = () => {
<div className="mx-auto max-w-2xl lg:col-span-6 lg:flex lg:items-center justify-center text-center">
<div className="relative z-10 lg:h-auto pt-[90px] lg:pt-[90px] lg:min-h-[300px] flex flex-col items-center justify-center sm:mx-auto md:w-3/4 lg:mx-0 lg:w-full gap-4 lg:gap-8">
<div className="flex flex-col items-center">
<AnnouncementBadge
url="/state-of-startups"
announcement="Take the survey"
badge="State of Startups 2025"
className="mb-8 -mt-4 lg:-mt-8"
/>
<h1 className="text-foreground text-4xl sm:text-5xl sm:leading-none lg:text-7xl">
<span className="block text-foreground">Build in a weekend</span>
<span className="text-brand block md:ml-0">Scale to millions</span>

View File

@@ -24,7 +24,8 @@ import MobileMenu from './MobileMenu'
import RightClickBrandLogo from './RightClickBrandLogo'
import { useSendTelemetryEvent } from '~/lib/telemetry'
import useDropdownMenu from './useDropdownMenu'
import { AuthenticatedDropdownMenu } from 'ui-patterns'
import { AnnouncementBanner, AuthenticatedDropdownMenu } from 'ui-patterns'
import Announcement from '../LaunchWeek/7/LaunchSection/Announcement'
interface Props {
hideNavbar: boolean
@@ -71,6 +72,7 @@ const Nav = ({ hideNavbar, stickyNavbar = true }: Props) => {
return (
<>
<AnnouncementBanner />
<div
className={cn('sticky top-0 z-40 transform', disableStickyNav && 'relative')}
style={{ transform: 'translate3d(0,0,999px)' }}

View File

@@ -1,6 +1,6 @@
import React, { useEffect, useRef, useState } from 'react'
import Image from 'next/image'
import anime from 'animejs'
import { createTimeline } from 'animejs'
import { Check, Copy } from 'lucide-react'
import CopyToClipboard from 'react-copy-to-clipboard'
import Typed from 'typed.js'
@@ -127,7 +127,7 @@ const FunctionsHero = () => {
}, [])
const animate = () => {
const tl = anime.timeline({
const tl = createTimeline({
loop: false,
autoplay: true,
})

View File

@@ -0,0 +1,25 @@
export default (isMobile?: boolean) => ({
metaTitle: 'State of Startups 2025',
metaDescription:
'Take the survey and learn the latest trends among builders in tech stacks, AI usage, problem domains, and more.',
metaImage: '/images/modules/vector/og.png',
docsUrl: '',
heroSection: {
title: 'State of Startups 2025',
h1: (
<>
The Postgres Vector database <br className="hidden md:block" />
and AI Toolkit
</>
),
subheader: (
<>
There's never been a better time to build.
<br />
Take our State of Startups survey to receive an exclusive Supabase t-shirt and a report on
the survey results.
</>
),
className: '[&_h1]:max-w-2xl',
},
})

View File

@@ -33,7 +33,7 @@
"@supabase/supabase-js": "catalog:",
"@vercel/og": "^0.6.2",
"ai-commands": "workspace:*",
"animejs": "^3.2.2",
"animejs": "^4.0.2",
"class-variance-authority": "^0.7.1",
"classnames": "^2.3.1",
"clsx": "^1.2.1",

View File

@@ -0,0 +1,534 @@
import { useEffect, useRef, useState } from 'react'
import { animate, createSpring, createTimeline, stagger } from 'animejs'
import Link from 'next/link'
import Image from 'next/image'
import { NextSeo } from 'next-seo'
import { Button, Checkbox, cn } from 'ui'
import { PopupFrame } from 'ui-patterns'
import { Input } from 'ui/src/components/shadcn/ui/input'
import { Label } from 'ui/src/components/shadcn/ui/label'
import DefaultLayout from '~/components/Layouts/Default'
import SectionContainer from '~/components/Layouts/SectionContainer'
import { useSendTelemetryEvent } from '~/lib/telemetry'
import data from '~/data/surveys/state-of-startups-2025'
interface FormData {
email: string
terms: boolean
}
interface FormItem {
type: 'email' | 'checkbox'
label: string
placeholder: string
required: boolean
className?: string
component: typeof Input | typeof Checkbox
}
type FormConfig = {
[K in keyof FormData]: FormItem
}
const formConfig: FormConfig = {
email: {
type: 'email',
label: 'Email',
placeholder: 'Email',
required: true,
className: '',
component: Input,
},
terms: {
type: 'checkbox',
label: '',
placeholder: '',
required: true,
className: '',
component: Checkbox,
},
}
const defaultFormValue: FormData = {
email: '',
terms: false,
}
const isValidEmail = (email: string): boolean => {
const emailPattern = /^[\w-\.+]+@([\w-]+\.)+[\w-]{2,8}$/
return emailPattern.test(email)
}
function StateOfStartupsPage() {
const pageData = data()
const meta_title = pageData.metaTitle
const meta_description = pageData.metaDescription
const meta_image = pageData.metaImage
const [formData, setFormData] = useState<FormData>(defaultFormValue)
const [honeypot, setHoneypot] = useState<string>('') // field to prevent spam
const [errors, setErrors] = useState<{ [key: string]: string }>({})
const [isSubmitting, setIsSubmitting] = useState(false)
const [success, setSuccess] = useState<string | null>(null)
const [startTime, setStartTime] = useState<number>(0)
const sendTelemetryEvent = useSendTelemetryEvent()
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target
setErrors({})
setFormData((prev) => ({ ...prev, [name]: value }))
}
const handleReset = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.preventDefault()
setFormData(defaultFormValue)
setSuccess(null)
setErrors({})
}
const validate = (): boolean => {
const newErrors: { [key in keyof FormData]?: string } = {}
// Check required fields
for (const key in formConfig) {
if (formConfig[key as keyof FormData].required && !formData[key as keyof FormData]) {
if (key === 'email') {
newErrors[key as keyof FormData] = `Email is required`
} else if (key === 'terms') {
newErrors[key as keyof FormData] = `You must agree to the terms`
}
}
}
// Validate email
if (formData.email && !isValidEmail(formData.email)) {
newErrors.email = 'Invalid email address'
}
setErrors(newErrors)
// Return validation status, also check if honeypot is filled (indicating a bot)
return Object.keys(newErrors).length === 0 && honeypot === ''
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
const currentTime = Date.now()
const timeElapsed = (currentTime - startTime) / 1000
// Spam prevention: Reject form if submitted too quickly (less than 3 seconds)
if (timeElapsed < 3) {
setErrors({ general: 'Submission too fast. Please fill the form correctly.' })
return
}
if (!validate()) {
return
}
setIsSubmitting(true)
setSuccess(null)
try {
const response = await fetch('/api-v2/submit-form-sos2025-newsletter', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(formData),
})
if (response.ok) {
setSuccess('Thank you for your submission!')
setFormData({ email: '', terms: false })
} else {
const errorData = await response.json()
setErrors({ general: `Submission failed: ${errorData.message}` })
}
} catch (error) {
setErrors({ general: 'An unexpected error occurred. Please try again.' })
} finally {
setIsSubmitting(false)
}
}
useEffect(() => {
setStartTime(Date.now())
}, [])
return (
<>
<NextSeo
title={meta_title}
description={meta_description}
openGraph={{
title: meta_title,
description: meta_description,
url: `https://supabase.com/modules/vector`,
images: [
{
url: meta_image,
},
],
}}
/>
<DefaultLayout className="!bg-alternative overflow-hidden sm:!overflow-visible">
<Hero {...pageData.heroSection} />
<SectionContainer>
<div className="flex flex-col text-center gap-4 py-8 items-center justify-center">
<h2 className="heading-gradient text-2xl sm:text-3xl xl:text-4xl">Stay in touch</h2>
<p className="mx-auto text-foreground-lighter w-full">
Sign up for our newsletter to be notified when the survey results are available.
</p>
<div className="w-full mt-4 flex items-center justify-center text-center gap-4">
{success ? (
<div className="flex flex-col h-full w-full min-w-[300px] gap-4 items-center justify-center opacity-0 transition-opacity animate-fade-in scale-1">
<p className="text-center text-sm">{success}</p>
<Button onClick={handleReset}>Reset</Button>
</div>
) : (
<form
noValidate
id="state-of-startups-form"
onSubmit={handleSubmit}
className="w-full max-w-md flex flex-col gap-4 items-center"
>
<div className="w-full flex flex-col sm:flex-row sm:items-center gap-2">
{/* Spam prevention */}
<input
type="text"
name="honeypot"
value={honeypot}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setHoneypot(e.target.value)
}
style={{ display: 'none' }}
aria-hidden="true"
/>
<Input
required
onChange={handleChange}
value={formData['email']}
name="email"
type="email"
placeholder="Email"
/>
<Button
htmlType="submit"
disabled={isSubmitting}
size="small"
onClick={() =>
sendTelemetryEvent({
action: 'register_for_state_of_startups_newsletter_clicked',
properties: { buttonLocation: 'State of Startups 2025 Newsletter Form' },
})
}
>
Register
</Button>
</div>
<div className="flex items-center gap-2 text-left">
<Checkbox
required
name="terms"
id="terms"
onChange={handleChange}
className="[&>input]:m-0"
/>
<Label htmlFor="terms" className="text-foreground-lighter leading-5">
We process your information in accordance with our{' '}
<Link href="/privacy" className="text-foreground-light hover:underline">
Privacy Policy
</Link>
.
</Label>
</div>
<div
className={cn(
'flex flex-nowrap text-right gap-1 items-center text-xs leading-none transition-opacity opacity-0 text-foreground-lighter',
errors['email'] && 'opacity-100 animate-fade-in',
errors['terms'] && 'opacity-100 animate-fade-in'
)}
>
{errors['email'] ? errors['email'] : errors['terms']}
</div>
</form>
)}
</div>
</div>
</SectionContainer>
</DefaultLayout>
</>
)
}
const Hero = (props: any) => {
const animRef = useRef<HTMLDivElement | null>(null)
useEffect(() => {
if (!animRef.current) return
const strings = [
"What's your tech stack?",
"What's your favorite AI developer tool?",
'Which vector database are you using?',
'Are you building AI Agents?',
'Do you use OpenTelemetry?',
'Where do you go to learn?',
]
let currentIndex = 0
const animateText = () => {
const animatedText = animRef.current?.querySelector('#anim')
if (!animatedText) return
const currentText = strings[currentIndex]
animatedText.textContent = currentText
// Split by words and wrap each word, then wrap letters within each word
animatedText.innerHTML = currentText
.split(' ')
.map((word) => {
if (word.trim() === '') return ' '
const wrappedLetters = word.replace(
/\S/g,
"<span class='letter' style='opacity: 0; transform: translateY(-6px); display: inline-block;'>$&</span>"
)
return `<span class="word" style="display: inline-block; white-space: nowrap;">${wrappedLetters}</span>`
})
.join(' ')
createTimeline({
onComplete: () => {
currentIndex = (currentIndex + 1) % strings.length
setTimeout(() => {
animateOut()
}, 100)
},
}).add(animatedText.querySelectorAll('.letter'), {
opacity: [0, 1],
translateY: [-8, 0],
ease: createSpring({ stiffness: 150, damping: 15 }),
duration: 500,
delay: stagger(10),
})
}
const animateOut = () => {
const animatedText = animRef.current?.querySelector('#anim')
if (!animatedText) return
animate(animatedText.querySelectorAll('.letter'), {
opacity: [1, 0],
translateY: [0, 8],
ease: 'inExpo',
duration: 500,
delay: stagger(10),
onComplete: () => {
setTimeout(animateText, -100)
},
})
}
animateText()
return () => {}
}, [])
return (
<>
<div
className={cn(
'container relative w-full mx-auto px-6 py-8 md:py-16 sm:px-16 xl:px-20',
props.className
)}
>
<div
ref={animRef}
className="flex flex-col text-center items-center justify-center min-h-[70vh]"
>
<div className="absolute overflow-hidden -mx-[15vw] sm:mx-0 inset-0 w-[calc(100%+30vw)] sm:w-full h-full col-span-12 lg:col-span-7 xl:col-span-6 xl:col-start-7 flex justify-center">
<svg
width="558"
height="392"
viewBox="0 0 558 392"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="absolute w-full h-full inset-0 -top-40 lg:top-0 xl:-left-40 animate-pulse"
style={{
animationDuration: '20000ms',
}}
>
<circle
cx="278.831"
cy="112.952"
r="278.5"
transform="rotate(75 278.831 112.952)"
fill="url(#paint0_radial_183_1691)"
fillOpacity="0.2"
/>
<defs>
<radialGradient
id="paint0_radial_183_1691"
cx="0"
cy="0"
r="1"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(349.764 144.755) rotate(-132.179) scale(202.74 202.839)"
>
<stop stopColor="hsl(var(--foreground-default))" />
<stop offset="1" stopColor="hsl(var(--foreground-default))" stopOpacity="0" />
</radialGradient>
</defs>
</svg>
<div className="sm:w-full sm_h-full sm:flex sm:justify-center">
<svg
width="1119"
height="1119"
viewBox="0 0 1119 1119"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="sm:w-auto -mb-72 sm:-mt-60 md:-mt-40 lg:-mt-12 xl:mt-0 animate-spinner !ease-linear transform"
style={{
animationDuration: '20000ms',
}}
>
<g clipPath="url(#clip0_183_1690)">
<circle cx="559.5" cy="559.5" r="496" fill="url(#paint1_radial_183_1690)" />
<path
d="M982.759 -15.7995C1100.79 61.9162 1134.95 153.728 1129.8 236.892C1124.68 319.611 1080.66 393.869 1041.31 437.283C968.75 168.701 692.591 9.3387 423.687 80.9161C430.529 20.4699 450.367 -27.8768 480.826 -63.4144C511.422 -99.1129 552.763 -121.922 602.496 -131.075C701.21 -149.241 833.009 -113.601 979.3 -18.0675L982.759 -15.7995Z"
stroke="url(#paint2_radial_183_1690)"
strokeWidth="1.15887"
/>
</g>
<defs>
<radialGradient
id="paint1_radial_183_1690"
cx="0"
cy="0"
r="1"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(571.212 579.87) rotate(122.182) scale(542.117 690.275)"
>
<stop stopColor="hsl(var(--border-muted))" />
<stop
offset="0.716346"
stopColor="hsl(var(--background-alternative-default))"
/>
<stop
offset="0.754808"
stopColor="hsl(var(--background-alternative-default))"
/>
<stop offset="1" stopColor="hsl(var(--border-strong))" />
</radialGradient>
<radialGradient
id="paint2_radial_183_1690"
cx="0"
cy="0"
r="1"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(814.301 175.03) rotate(-38.9601) scale(142.974 294.371)"
>
<stop stopColor="hsl(var(--foreground-default))" />
<stop offset="1" stopColor="hsl(var(--foreground-default))" stopOpacity="0" />
</radialGradient>
<clipPath id="clip0_183_1690">
<rect width="1119" height="1119" fill="white" />
</clipPath>
</defs>
</svg>
</div>
<svg
width="1096"
height="482"
viewBox="0 0 1096 482"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="absolute min-w-full inset-0 top-auto z-10"
>
<rect x="0.500488" width="1095" height="482" fill="url(#paint0_linear_183_1694)" />
<defs>
<linearGradient
id="paint0_linear_183_1694"
x1="922.165"
y1="63.3564"
x2="922.165"
y2="419.772"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="hsl(var(--background-alternative-default))" stopOpacity="0" />
<stop offset="1" stopColor="hsl(var(--background-alternative-default))" />
</linearGradient>
</defs>
</svg>
</div>
<div className="relative w-full z-10 flex flex-col items-center mx-auto">
<div className="flex gap-2 mb-4 md:mb-8">
<div className="w-11 h-11 relative flex items-center justify-center bg-default border rounded-lg">
<Image
src="/images/supabase-logo-icon.svg"
alt="Supabase icon"
width={60}
height={60}
className="w-6 h-6"
/>
</div>
</div>
<div>
{props.icon || props.title ? (
<div className="mb-2 flex justify-center items-center gap-3">
{props.title && (
<h1
className="text-brand font-mono uppercase tracking-widest text-sm"
key={`product-name-${props.title}`}
>
{props.title}
</h1>
)}
</div>
) : null}
</div>
<div className={cn('flex flex-col gap-4 items-center')}>
<div className="flex h-[150px] items-center">
<div
id="anim"
className="will-change-transform leading-[120%] text-4xl sm:text-5xl md:text-6xl min-h-[4rem] max-w-2xl [&_.letter]:transform"
>
State of Startups
</div>
</div>
<p className="p !text-foreground-light max-w-md">{props.subheader}</p>
</div>
<div className="w-full sm:w-auto flex flex-col items-stretch sm:flex-row pt-2 sm:items-center gap-2">
<PopupFrame
trigger={
<Button size="small" asChild>
<span>Take the survey</span>
</Button>
}
className="[&_.modal-content]:min-h-[650px] [&_.modal-content]:!h-[75vh] [&_.modal-content]:flex [&_.modal-content]:flex-col"
>
<div className="w-full !h-full flex-1 flex flex-col">
<iframe
src={`https://form.typeform.com/to/si6Z7XlW?embedded=true`}
width="100%"
height="100%"
frameBorder="0"
className="w-full !min-h-full flex-1"
/>
</div>
</PopupFrame>
</div>
</div>
</div>
</div>
</>
)
}
export default StateOfStartupsPage

View File

@@ -916,6 +916,23 @@ export interface RequestDemoButtonClickedEvent {
}
}
/**
* User clicked the "Register" button in the State of Startups 2025 newsletter form.
*
* @group Events
* @source www
*/
export interface RegisterStateOfStartups2025NewsletterClicked {
action: 'register_for_state_of_startups_newsletter_clicked'
properties: {
/**
* The source of the button click, e.g. homepage hero, cta banner, product page header.
* If it states it came from the request demo form, it can come from different pages so refer to path name to determine.
*/
buttonLocation: string
}
}
/**
* User clicked the sign-in button in various locations described in properties.
*
@@ -1380,6 +1397,7 @@ export type TelemetryEvent =
| StartProjectButtonClickedEvent
| SeeDocumentationButtonClickedEvent
| RequestDemoButtonClickedEvent
| RegisterStateOfStartups2025NewsletterClicked
| SignInButtonClickedEvent
| HelpButtonClickedEvent
| ExampleProjectCardClickedEvent

View File

@@ -0,0 +1,10 @@
import { Announcement } from 'ui/src/layout/banners'
import SOSBanner from './SOSBanner'
export const AnnouncementBanner = () => {
return (
<Announcement show={true} announcementKey="announcement_sos25">
<SOSBanner />
</Announcement>
)
}

View File

@@ -0,0 +1,30 @@
'use client'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { Button } from 'ui/src/components/Button'
export function SOSBanner() {
const pathname = usePathname()
const isHomePage = pathname === '/'
const isSurveyPage = pathname === '/state-of-startups'
if (isHomePage || isSurveyPage) return null
return (
<div className="relative w-full p-2 flex items-center group justify-center text-foreground bg-alternative border-b border-muted transition-colors overflow-hidden">
<div className="relative z-10 flex items-center justify-center">
<div className="w-full flex gap-5 md:gap-10 items-center md:justify-center text-sm">
<p className="flex gap-1.5 items-center text-brand font-mono uppercase tracking-widest text-sm">
State of Startups 2025
</p>
<Button size="tiny" type="default" className="px-2 !leading-none text-xs" asChild>
<Link href="/state-of-startups">Take the survey</Link>
</Button>
</div>
</div>
</div>
)
}
export default SOSBanner

View File

@@ -0,0 +1 @@
export * from './AnnouncementBanner'

View File

@@ -0,0 +1,90 @@
import { useBreakpoint } from 'common'
import React, { ReactNode } from 'react'
import { Modal, cn } from 'ui'
interface PopupFrameProps {
triggerContainerClassName?: string
trigger?: ReactNode
onOpenCallback?: any
className?: string
children: ReactNode
}
export function PopupFrame({
triggerContainerClassName = '',
trigger,
onOpenCallback,
className,
children,
}: PopupFrameProps) {
const [open, setOpen] = React.useState(false)
const isMobile = useBreakpoint(768)
React.useEffect(() => {
function onKeyDown(e: KeyboardEvent) {
switch (e.key) {
case 'Escape':
return setOpen(false)
default:
return
}
}
window.addEventListener('keydown', onKeyDown)
return () => {
window.removeEventListener('keydown', onKeyDown)
}
}, [])
React.useEffect(() => {
if (isMobile) setOpen(false)
}, [isMobile])
return (
<>
<Modal
visible={open}
hideFooter
showCloseButton={false}
className={cn(
'!bg-[#f8f9fa]/95 dark:!bg-[#1c1c1c]/80',
'!border-[#e6e8eb]/90 dark:!border-[#282828]/90',
'transition ease-out',
'mx-auto backdrop-blur-md w-[calc(100%-2rem)]',
className
)}
onInteractOutside={(e) => {
// Only hide menu when clicking outside, not focusing outside
// Prevents Firefox dropdown issue that immediately closes menu after opening
if (e.type === 'dismissableLayer.pointerDownOutside') {
setOpen(!open)
}
}}
size="xxlarge"
>
<div className="device-frame !w-full h-full flex items-center justify-center">
<div className="modal-group relative w-full h-full">
<button
onClick={() => setOpen(false)}
className="text-foreground-light hover:text-foreground absolute -top-8 right-0"
>
<p className="text-xs">Close</p>
</button>
<div className="modal-content h-full !rounded-lg !border-none !overflow-hidden">
{children}
</div>
</div>
</div>
</Modal>
<button
onClick={() => {
if (onOpenCallback) onOpenCallback()
setOpen(true)
}}
className={cn('w-full', triggerContainerClassName)}
>
{trigger ?? 'Expand'}
</button>
</>
)
}

View File

@@ -5,11 +5,13 @@
export * from './src/admonition'
export * from './src/AssistantChat/AssistantChatForm'
export * from './src/AuthenticatedDropdownMenu'
export * from './Banners'
export * from './src/CommandMenu'
export * from './src/ComputeBadge'
export * from './src/FilterBar'
export * from './src/GlassPanel'
export * from './src/InnerSideMenu'
export * from './PopupFrame'
export * from './src/ShimmeringLoader'
export * from './src/TimestampInfo'
export * from './src/Toc'

12
pnpm-lock.yaml generated
View File

@@ -1352,8 +1352,8 @@ importers:
specifier: workspace:*
version: link:../../packages/ai-commands
animejs:
specifier: ^3.2.2
version: 3.2.2
specifier: ^4.0.2
version: 4.0.2
class-variance-authority:
specifier: ^0.7.1
version: 0.7.1
@@ -9426,8 +9426,8 @@ packages:
amazon-cognito-identity-js@6.3.15:
resolution: {integrity: sha512-G2mzTlGYHKYh9oZDO0Gk94xVQ4iY9GYWBaYScbDYvz05ps6dqi0IvdNx1Lxi7oA3tjS5X+mUN7/svFJJdOB9YA==}
animejs@3.2.2:
resolution: {integrity: sha512-Ao95qWLpDPXXM+WrmwcKbl6uNlC5tjnowlaRYtuVDHHoygjtIPfDUoK9NthrlZsQSKjZXlmji2TrBUAVbiH0LQ==}
animejs@4.0.2:
resolution: {integrity: sha512-f0L/kSya2RF23iMSF/VO01pMmLwlAFoiQeNAvBXhEyLzIPd2/QTBRatwGUqkVCC6seaAJYzAkGir55N4SL+h3A==}
anser@2.3.2:
resolution: {integrity: sha512-PMqBCBvrOVDRqLGooQb+z+t1Q0PiPyurUQeZRR5uHBOVZcW8B04KMmnT12USnhpNX2wCPagWzLVppQMUG3u0Dw==}
@@ -27828,8 +27828,8 @@ snapshots:
js-cookie: 2.2.1
transitivePeerDependencies:
- encoding
animejs@3.2.2: {}
animejs@4.0.2: {}
anser@2.3.2: {}