mirror of
https://github.com/sourcebot-dev/sourcebot.git
synced 2025-12-12 04:15:30 +00:00
Add repo carousel and "how to search" section to homepage (#5)
This commit is contained in:
parent
71717ac4b5
commit
1c756d2010
8 changed files with 559 additions and 5 deletions
|
|
@ -34,6 +34,8 @@
|
|||
"class-variance-authority": "^0.7.0",
|
||||
"client-only": "^0.0.1",
|
||||
"clsx": "^2.1.1",
|
||||
"embla-carousel-auto-scroll": "^8.3.0",
|
||||
"embla-carousel-react": "^8.3.0",
|
||||
"escape-string-regexp": "^5.0.0",
|
||||
"http-status-codes": "^2.3.0",
|
||||
"lucide-react": "^0.435.0",
|
||||
|
|
|
|||
|
|
@ -30,11 +30,13 @@ export const NavigationMenu = () => {
|
|||
src={logoDark}
|
||||
className="h-11 w-auto hidden dark:block"
|
||||
alt={"Sourcebot logo"}
|
||||
priority={true}
|
||||
/>
|
||||
<Image
|
||||
src={logoLight}
|
||||
className="h-11 w-auto block dark:hidden"
|
||||
alt={"Sourcebot logo"}
|
||||
priority={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
155
src/app/page.tsx
155
src/app/page.tsx
|
|
@ -1,26 +1,34 @@
|
|||
import { listRepositories } from "@/lib/server/searchService";
|
||||
import { isServiceError } from "@/lib/utils";
|
||||
import Image from "next/image";
|
||||
import { Suspense } from "react";
|
||||
import logoDark from "../../public/sb_logo_dark_large.png";
|
||||
import logoLight from "../../public/sb_logo_light_large.png";
|
||||
import { NavigationMenu } from "./navigationMenu";
|
||||
import { RepositoryCarousel } from "./repositoryCarousel";
|
||||
import { SearchBar } from "./searchBar";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
|
||||
export default function Home() {
|
||||
|
||||
export default async function Home() {
|
||||
return (
|
||||
<div className="h-screen flex flex-col items-center">
|
||||
{/* TopBar */}
|
||||
<NavigationMenu />
|
||||
|
||||
<div className="flex flex-col justify-center items-center p-4 mt-48">
|
||||
|
||||
<div className="flex flex-col justify-center items-center mt-8 md:mt-32 max-w-[90%]">
|
||||
<div className="max-h-44 w-auto">
|
||||
<Image
|
||||
src={logoDark}
|
||||
className="w-full h-full hidden dark:block"
|
||||
alt={"Sourcebot logo"}
|
||||
priority={true}
|
||||
/>
|
||||
<Image
|
||||
src={logoLight}
|
||||
className="w-full h-full block dark:hidden"
|
||||
alt={"Sourcebot logo"}
|
||||
priority={true}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full flex flex-row mt-4">
|
||||
|
|
@ -28,7 +36,148 @@ export default function Home() {
|
|||
autoFocus={true}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-8">
|
||||
<Suspense fallback={<div>...</div>}>
|
||||
<RepositoryList />
|
||||
</Suspense>
|
||||
</div>
|
||||
<Separator className="mt-5 mb-8" />
|
||||
<div className="flex flex-col items-center w-fit gap-6">
|
||||
<span className="font-semibold">How to search</span>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||
<HowToSection
|
||||
title="Search in files or paths"
|
||||
>
|
||||
<QueryExample>
|
||||
<Query query="test todo">test todo</Query> <QueryExplanation>(both test and todo)</QueryExplanation>
|
||||
</QueryExample>
|
||||
<QueryExample>
|
||||
<Query query="test or todo">test <Highlight>or</Highlight> todo</Query> <QueryExplanation>(either test or todo)</QueryExplanation>
|
||||
</QueryExample>
|
||||
<QueryExample>
|
||||
<Query query={`"exit boot"`}>{`"exit boot"`}</Query> <QueryExplanation>(exact match)</QueryExplanation>
|
||||
</QueryExample>
|
||||
<QueryExample>
|
||||
<Query query="TODO case:yes">TODO <Highlight>case:</Highlight>yes</Query> <QueryExplanation>(case sensitive)</QueryExplanation>
|
||||
</QueryExample>
|
||||
</HowToSection>
|
||||
<HowToSection
|
||||
title="Filter results"
|
||||
>
|
||||
<QueryExample>
|
||||
<Query query="file:README setup"><Highlight>file:</Highlight>README setup</Query> <QueryExplanation>(by filename)</QueryExplanation>
|
||||
</QueryExample>
|
||||
<QueryExample>
|
||||
<Query query="repo:torvalds/linux test"><Highlight>repo:</Highlight>torvalds/linux test</Query> <QueryExplanation>(by repo)</QueryExplanation>
|
||||
</QueryExample>
|
||||
<QueryExample>
|
||||
<Query query="lang:typescript"><Highlight>lang:</Highlight>typescript</Query> <QueryExplanation>(by language)</QueryExplanation>
|
||||
</QueryExample>
|
||||
<QueryExample>
|
||||
<Query query="branch:HEAD"><Highlight>branch:</Highlight>HEAD</Query> <QueryExplanation>(by branch)</QueryExplanation>
|
||||
</QueryExample>
|
||||
</HowToSection>
|
||||
<HowToSection
|
||||
title="Advanced"
|
||||
>
|
||||
<QueryExample>
|
||||
<Query query="file:\.py$"><Highlight>file:</Highlight>{`\\.py$`}</Query> <QueryExplanation>{`(files that end in ".py")`}</QueryExplanation>
|
||||
</QueryExample>
|
||||
<QueryExample>
|
||||
<Query query="sym:main"><Highlight>sym:</Highlight>main</Query> <QueryExplanation>{`(symbols named "main")`}</QueryExplanation>
|
||||
</QueryExample>
|
||||
<QueryExample>
|
||||
<Query query="todo -lang:c">todo <Highlight>-lang:c</Highlight></Query> <QueryExplanation>(negate filter)</QueryExplanation>
|
||||
</QueryExample>
|
||||
<QueryExample>
|
||||
<Query query="content:README"><Highlight>content:</Highlight>README</Query> <QueryExplanation>(search content only)</QueryExplanation>
|
||||
</QueryExample>
|
||||
</HowToSection>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const RepositoryList = async () => {
|
||||
const _repos = await listRepositories();
|
||||
|
||||
if (isServiceError(_repos)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const repos = _repos.List.Repos.map((repo) => repo.Repository);
|
||||
|
||||
if (repos.length === 0) {
|
||||
return <span>
|
||||
Get started
|
||||
<a
|
||||
href="https://github.com/TaqlaAI/sourcebot/blob/main/README.md"
|
||||
className="text-blue-500"
|
||||
>
|
||||
{` configuring Sourcebot.`}
|
||||
</a>
|
||||
</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<span className="text-sm">
|
||||
{`Search ${repos.length} `}
|
||||
<a
|
||||
href="/repos"
|
||||
className="text-blue-500"
|
||||
>
|
||||
{repos.length > 1 ? 'repositories' : 'repository'}
|
||||
</a>
|
||||
</span>
|
||||
<RepositoryCarousel repos={repos} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const HowToSection = ({ title, children }: { title: string, children: React.ReactNode }) => {
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="dark:text-gray-300 text-sm mb-2 underline">{title}</span>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
const Highlight = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<span className="text-blue-700 dark:text-blue-500">
|
||||
{children}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
const QueryExample = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<span className="text-sm font-mono">
|
||||
{children}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
const QueryExplanation = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<span className="text-gray-500 dark:text-gray-400 ml-3">
|
||||
{children}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
const Query = ({ query, children }: { query: string, children: React.ReactNode }) => {
|
||||
return (
|
||||
<a
|
||||
href={`/search?query=${query}`}
|
||||
className="cursor-pointer hover:underline"
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
'use client';
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { getRepoCodeHostInfo } from "@/lib/utils";
|
||||
import { Column, ColumnDef } from "@tanstack/react-table"
|
||||
import { ArrowUpDown } from "lucide-react"
|
||||
|
||||
|
||||
|
||||
export type RepositoryColumnInfo = {
|
||||
name: string;
|
||||
branches: {
|
||||
|
|
@ -25,6 +24,24 @@ export const columns: ColumnDef<RepositoryColumnInfo>[] = [
|
|||
{
|
||||
accessorKey: "name",
|
||||
header: "Name",
|
||||
cell: ({ row }) => {
|
||||
const repo = row.original;
|
||||
const info = getRepoCodeHostInfo(repo.name);
|
||||
return (
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<span
|
||||
className={info?.repoLink ? "cursor-pointer text-blue-500 hover:underline": ""}
|
||||
onClick={() => {
|
||||
if (info?.repoLink) {
|
||||
window.open(info.repoLink, "_blank");
|
||||
}
|
||||
}}
|
||||
>
|
||||
{repo.name}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "branches",
|
||||
|
|
|
|||
98
src/app/repositoryCarousel.tsx
Normal file
98
src/app/repositoryCarousel.tsx
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
'use client';
|
||||
|
||||
import { Repository } from "@/lib/schemas";
|
||||
import {
|
||||
Carousel,
|
||||
CarouselContent,
|
||||
CarouselItem,
|
||||
} from "@/components/ui/carousel";
|
||||
import Autoscroll from "embla-carousel-auto-scroll";
|
||||
import { getRepoCodeHostInfo } from "@/lib/utils";
|
||||
import Image from "next/image";
|
||||
import { FileIcon } from "@radix-ui/react-icons";
|
||||
import clsx from "clsx";
|
||||
|
||||
interface RepositoryCarouselProps {
|
||||
repos: Repository[];
|
||||
}
|
||||
|
||||
export const RepositoryCarousel = ({
|
||||
repos,
|
||||
}: RepositoryCarouselProps) => {
|
||||
return (
|
||||
<Carousel
|
||||
opts={{
|
||||
align: "start",
|
||||
loop: true,
|
||||
}}
|
||||
className="w-full max-w-lg"
|
||||
plugins={[
|
||||
Autoscroll({
|
||||
startDelay: 0,
|
||||
speed: 1,
|
||||
stopOnMouseEnter: true,
|
||||
stopOnInteraction: false,
|
||||
}),
|
||||
]}
|
||||
>
|
||||
<CarouselContent>
|
||||
{repos.map((repo, index) => (
|
||||
<CarouselItem key={index} className="basis-auto">
|
||||
<RepositoryBadge
|
||||
key={index}
|
||||
repo={repo}
|
||||
/>
|
||||
</CarouselItem>
|
||||
))}
|
||||
</CarouselContent>
|
||||
</Carousel>
|
||||
)
|
||||
};
|
||||
|
||||
interface RepositoryBadgeProps {
|
||||
repo: Repository;
|
||||
}
|
||||
|
||||
const RepositoryBadge = ({
|
||||
repo
|
||||
}: RepositoryBadgeProps) => {
|
||||
const { repoIcon, repoName, repoLink } = (() => {
|
||||
const info = getRepoCodeHostInfo(repo.Name);
|
||||
|
||||
if (info) {
|
||||
return {
|
||||
repoIcon: <Image
|
||||
src={info.icon}
|
||||
alt={info.costHostName}
|
||||
className="w-4 h-4 dark:invert"
|
||||
/>,
|
||||
repoName: info.repoName,
|
||||
repoLink: info.repoLink,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
repoIcon: <FileIcon className="w-4 h-4" />,
|
||||
repoName: repo.Name,
|
||||
repoLink: undefined,
|
||||
}
|
||||
})();
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={() => {
|
||||
if (repoLink !== undefined) {
|
||||
window.open(repoLink, "_blank");
|
||||
}
|
||||
}}
|
||||
className={clsx("flex flex-row items-center gap-2 border rounded-md p-2 text-clip", {
|
||||
"cursor-pointer": repoLink !== undefined,
|
||||
})}
|
||||
>
|
||||
{repoIcon}
|
||||
<span className="text-sm font-mono">
|
||||
{repoName}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
262
src/components/ui/carousel.tsx
Normal file
262
src/components/ui/carousel.tsx
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import useEmblaCarousel, {
|
||||
type UseEmblaCarouselType,
|
||||
} from "embla-carousel-react"
|
||||
import { ArrowLeft, ArrowRight } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
|
||||
type CarouselApi = UseEmblaCarouselType[1]
|
||||
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
|
||||
type CarouselOptions = UseCarouselParameters[0]
|
||||
type CarouselPlugin = UseCarouselParameters[1]
|
||||
|
||||
type CarouselProps = {
|
||||
opts?: CarouselOptions
|
||||
plugins?: CarouselPlugin
|
||||
orientation?: "horizontal" | "vertical"
|
||||
setApi?: (api: CarouselApi) => void
|
||||
}
|
||||
|
||||
type CarouselContextProps = {
|
||||
carouselRef: ReturnType<typeof useEmblaCarousel>[0]
|
||||
api: ReturnType<typeof useEmblaCarousel>[1]
|
||||
scrollPrev: () => void
|
||||
scrollNext: () => void
|
||||
canScrollPrev: boolean
|
||||
canScrollNext: boolean
|
||||
} & CarouselProps
|
||||
|
||||
const CarouselContext = React.createContext<CarouselContextProps | null>(null)
|
||||
|
||||
function useCarousel() {
|
||||
const context = React.useContext(CarouselContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useCarousel must be used within a <Carousel />")
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
const Carousel = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement> & CarouselProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
orientation = "horizontal",
|
||||
opts,
|
||||
setApi,
|
||||
plugins,
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const [carouselRef, api] = useEmblaCarousel(
|
||||
{
|
||||
...opts,
|
||||
axis: orientation === "horizontal" ? "x" : "y",
|
||||
},
|
||||
plugins
|
||||
)
|
||||
const [canScrollPrev, setCanScrollPrev] = React.useState(false)
|
||||
const [canScrollNext, setCanScrollNext] = React.useState(false)
|
||||
|
||||
const onSelect = React.useCallback((api: CarouselApi) => {
|
||||
if (!api) {
|
||||
return
|
||||
}
|
||||
|
||||
setCanScrollPrev(api.canScrollPrev())
|
||||
setCanScrollNext(api.canScrollNext())
|
||||
}, [])
|
||||
|
||||
const scrollPrev = React.useCallback(() => {
|
||||
api?.scrollPrev()
|
||||
}, [api])
|
||||
|
||||
const scrollNext = React.useCallback(() => {
|
||||
api?.scrollNext()
|
||||
}, [api])
|
||||
|
||||
const handleKeyDown = React.useCallback(
|
||||
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (event.key === "ArrowLeft") {
|
||||
event.preventDefault()
|
||||
scrollPrev()
|
||||
} else if (event.key === "ArrowRight") {
|
||||
event.preventDefault()
|
||||
scrollNext()
|
||||
}
|
||||
},
|
||||
[scrollPrev, scrollNext]
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!api || !setApi) {
|
||||
return
|
||||
}
|
||||
|
||||
setApi(api)
|
||||
}, [api, setApi])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!api) {
|
||||
return
|
||||
}
|
||||
|
||||
onSelect(api)
|
||||
api.on("reInit", onSelect)
|
||||
api.on("select", onSelect)
|
||||
|
||||
return () => {
|
||||
api?.off("select", onSelect)
|
||||
}
|
||||
}, [api, onSelect])
|
||||
|
||||
return (
|
||||
<CarouselContext.Provider
|
||||
value={{
|
||||
carouselRef,
|
||||
api: api,
|
||||
opts,
|
||||
orientation:
|
||||
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
|
||||
scrollPrev,
|
||||
scrollNext,
|
||||
canScrollPrev,
|
||||
canScrollNext,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
ref={ref}
|
||||
onKeyDownCapture={handleKeyDown}
|
||||
className={cn("relative", className)}
|
||||
role="region"
|
||||
aria-roledescription="carousel"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</CarouselContext.Provider>
|
||||
)
|
||||
}
|
||||
)
|
||||
Carousel.displayName = "Carousel"
|
||||
|
||||
const CarouselContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { carouselRef, orientation } = useCarousel()
|
||||
|
||||
return (
|
||||
<div ref={carouselRef} className="overflow-hidden">
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex",
|
||||
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
CarouselContent.displayName = "CarouselContent"
|
||||
|
||||
const CarouselItem = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { orientation } = useCarousel()
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
role="group"
|
||||
aria-roledescription="slide"
|
||||
className={cn(
|
||||
"min-w-0 shrink-0 grow-0 basis-full",
|
||||
orientation === "horizontal" ? "pl-4" : "pt-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
CarouselItem.displayName = "CarouselItem"
|
||||
|
||||
const CarouselPrevious = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<typeof Button>
|
||||
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
|
||||
const { orientation, scrollPrev, canScrollPrev } = useCarousel()
|
||||
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={cn(
|
||||
"absolute h-8 w-8 rounded-full",
|
||||
orientation === "horizontal"
|
||||
? "-left-12 top-1/2 -translate-y-1/2"
|
||||
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||
className
|
||||
)}
|
||||
disabled={!canScrollPrev}
|
||||
onClick={scrollPrev}
|
||||
{...props}
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
<span className="sr-only">Previous slide</span>
|
||||
</Button>
|
||||
)
|
||||
})
|
||||
CarouselPrevious.displayName = "CarouselPrevious"
|
||||
|
||||
const CarouselNext = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<typeof Button>
|
||||
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
|
||||
const { orientation, scrollNext, canScrollNext } = useCarousel()
|
||||
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={cn(
|
||||
"absolute h-8 w-8 rounded-full",
|
||||
orientation === "horizontal"
|
||||
? "-right-12 top-1/2 -translate-y-1/2"
|
||||
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||
className
|
||||
)}
|
||||
disabled={!canScrollNext}
|
||||
onClick={scrollNext}
|
||||
{...props}
|
||||
>
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
<span className="sr-only">Next slide</span>
|
||||
</Button>
|
||||
)
|
||||
})
|
||||
CarouselNext.displayName = "CarouselNext"
|
||||
|
||||
export {
|
||||
type CarouselApi,
|
||||
Carousel,
|
||||
CarouselContent,
|
||||
CarouselItem,
|
||||
CarouselPrevious,
|
||||
CarouselNext,
|
||||
}
|
||||
|
|
@ -93,6 +93,7 @@ export const fileSourceResponseSchema = z.object({
|
|||
|
||||
|
||||
export type ListRepositoriesResponse = z.infer<typeof listRepositoriesResponseSchema>;
|
||||
export type Repository = z.infer<typeof repositorySchema>;
|
||||
|
||||
// @see : https://github.com/TaqlaAI/zoekt/blob/3780e68cdb537d5a7ed2c84d9b3784f80c7c5d04/api.go#L728
|
||||
const repoStatsSchema = z.object({
|
||||
|
|
|
|||
23
yarn.lock
23
yarn.lock
|
|
@ -1631,6 +1631,29 @@ eastasianwidth@^0.2.0:
|
|||
resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb"
|
||||
integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==
|
||||
|
||||
embla-carousel-auto-scroll@^8.3.0:
|
||||
version "8.3.0"
|
||||
resolved "https://registry.yarnpkg.com/embla-carousel-auto-scroll/-/embla-carousel-auto-scroll-8.3.0.tgz#604b85ae028c896c98b9949b30588f3c9eb798b0"
|
||||
integrity sha512-ybXWqCTWvl+DeGwtGw0tUU1bOKglS/Ov8F5fueMkiwySKrSFAHizdqSrlMR1SQEXNZHMPH9LqCz7MajNYkdeAQ==
|
||||
|
||||
embla-carousel-react@^8.3.0:
|
||||
version "8.3.0"
|
||||
resolved "https://registry.yarnpkg.com/embla-carousel-react/-/embla-carousel-react-8.3.0.tgz#8aa6b9b77c3e900349a7215cb31b7ead6a84a715"
|
||||
integrity sha512-P1FlinFDcIvggcErRjNuVqnUR8anyo8vLMIH8Rthgofw7Nj8qTguCa2QjFAbzxAUTQTPNNjNL7yt0BGGinVdFw==
|
||||
dependencies:
|
||||
embla-carousel "8.3.0"
|
||||
embla-carousel-reactive-utils "8.3.0"
|
||||
|
||||
embla-carousel-reactive-utils@8.3.0:
|
||||
version "8.3.0"
|
||||
resolved "https://registry.yarnpkg.com/embla-carousel-reactive-utils/-/embla-carousel-reactive-utils-8.3.0.tgz#75f177ed2f6bdafbaab8f869f936692d08cd488e"
|
||||
integrity sha512-EYdhhJ302SC4Lmkx8GRsp0sjUhEN4WyFXPOk0kGu9OXZSRMmcBlRgTvHcq8eKJE1bXWBsOi1T83B+BSSVZSmwQ==
|
||||
|
||||
embla-carousel@8.3.0:
|
||||
version "8.3.0"
|
||||
resolved "https://registry.yarnpkg.com/embla-carousel/-/embla-carousel-8.3.0.tgz#dc27c63c405aa98320cb36893e4be2fbdc787ee1"
|
||||
integrity sha512-Ve8dhI4w28qBqR8J+aMtv7rLK89r1ZA5HocwFz6uMB/i5EiC7bGI7y+AM80yAVUJw3qqaZYK7clmZMUR8kM3UA==
|
||||
|
||||
emoji-regex@^8.0.0:
|
||||
version "8.0.0"
|
||||
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
|
||||
|
|
|
|||
Loading…
Reference in a new issue