feat: project bootstrap

This commit is contained in:
Khairul Hidayat 2024-02-18 21:14:41 +07:00
parent 1de8ccd2fd
commit 6b278b092e
19 changed files with 1009 additions and 262 deletions

17
components.json Normal file
View File

@ -0,0 +1,17 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "src/app/globals.css",
"baseColor": "slate",
"cssVariables": false,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils"
}
}

View File

@ -9,19 +9,38 @@
"lint": "next lint" "lint": "next lint"
}, },
"dependencies": { "dependencies": {
"@codemirror/lang-css": "^6.2.1",
"@codemirror/lang-html": "^6.4.8",
"@codemirror/lang-javascript": "^6.2.1",
"@tanstack/react-query": "^5.21.7",
"@trpc/client": "11.0.0-next-beta.289",
"@trpc/next": "11.0.0-next-beta.289",
"@trpc/react-query": "11.0.0-next-beta.289",
"@trpc/server": "11.0.0-next-beta.289",
"@uiw/codemirror-theme-tokyo-night": "^4.21.22",
"@uiw/react-codemirror": "^4.21.22",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"lucide-react": "^0.331.0",
"next": "14.1.0",
"prettier": "^3.2.5",
"react": "^18", "react": "^18",
"react-dom": "^18", "react-dom": "^18",
"next": "14.1.0" "react-resizable-panels": "^2.0.9",
"tailwind-merge": "^2.2.1",
"tailwindcss-animate": "^1.0.7",
"usehooks-ts": "^2.14.0",
"zod": "^3.22.4"
}, },
"devDependencies": { "devDependencies": {
"typescript": "^5",
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^18", "@types/react": "^18",
"@types/react-dom": "^18", "@types/react-dom": "^18",
"autoprefixer": "^10.0.1", "autoprefixer": "^10.0.1",
"eslint": "^8",
"eslint-config-next": "14.1.0",
"postcss": "^8", "postcss": "^8",
"tailwindcss": "^3.3.0", "tailwindcss": "^3.3.0",
"eslint": "^8", "typescript": "^5"
"eslint-config-next": "14.1.0"
} }
} }

544
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,13 @@
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
import { appRouter } from "@/server/routers/_app";
import { createContext } from "@/server/context";
const handler = (req: Request) =>
fetchRequestHandler({
endpoint: "/api/trpc",
req,
router: appRouter,
createContext,
});
export { handler as GET, handler as POST };

View File

@ -2,32 +2,10 @@
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
:root {
--foreground-rgb: 0, 0, 0;
--background-start-rgb: 214, 219, 220;
--background-end-rgb: 255, 255, 255;
}
@media (prefers-color-scheme: dark) {
:root {
--foreground-rgb: 255, 255, 255;
--background-start-rgb: 0, 0, 0;
--background-end-rgb: 0, 0, 0;
}
}
body { body {
color: rgb(var(--foreground-rgb)); @apply bg-slate-400 text-white;
background: linear-gradient(
to bottom,
transparent,
rgb(var(--background-end-rgb))
)
rgb(var(--background-start-rgb));
} }
@layer utilities { .cm-theme {
.text-balance { @apply h-full;
text-wrap: balance;
}
} }

View File

@ -1,22 +1,27 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { Inter } from "next/font/google"; import { Inter } from "next/font/google";
import Providers from "./providers";
import "./globals.css"; import "./globals.css";
const inter = Inter({ subsets: ["latin"] }); const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Create Next App", title: "Code Share",
description: "Generated by create next app", description: "Code sharing app",
}; };
export default function RootLayout({ type Props = {
children,
}: Readonly<{
children: React.ReactNode; children: React.ReactNode;
}>) { };
const RootLayout = ({ children }: Props) => {
return ( return (
<html lang="en"> <html lang="en" className="dark">
<body className={inter.className}>{children}</body> <body className={inter.className}>
<Providers>{children}</Providers>
</body>
</html> </html>
); );
} };
export default RootLayout;

View File

@ -1,113 +1,151 @@
import Image from "next/image"; "use client";
import React, { useCallback, useEffect, useRef, useState } from "react";
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from "@/components/ui/resizable";
import { useMediaQuery } from "@/hooks/useMediaQuery";
import Panel from "@/components/ui/panel";
import CodeEditor from "@/components/ui/code-editor";
import { ReactCodeMirrorRef } from "@uiw/react-codemirror";
import prettier from "prettier/standalone";
import prettierHtmlPlugin from "prettier/plugins/html";
const HomePage = () => {
const codeMirror = useRef<ReactCodeMirrorRef>(null);
const frameRef = useRef<HTMLIFrameElement>(null);
const [isMounted, setMounted] = useState(false);
const isMobile = useMediaQuery("(max-width: 639px)");
const [data, setData] = useState("");
const [lang, setLang] = useState("html");
useEffect(() => {
setMounted(true);
}, []);
useEffect(() => {
if (frameRef.current) {
frameRef.current.srcdoc = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://cdn.tailwindcss.com"></script>
<title>Code-Share</title>
</head>
<body>
${data}
</body>
</html>
`;
}
}, [data]);
const onFormat = useCallback(async () => {
const cursor = codeMirror.current?.view?.state.selection.main.head || 0;
const { formatted, cursorOffset } = await prettier.formatWithCursor(data, {
parser: "html",
plugins: [prettierHtmlPlugin],
cursorOffset: cursor,
});
const cm = codeMirror.current?.view;
setData(formatted);
cm?.dispatch({
changes: { from: 0, to: cm?.state.doc.length, insert: formatted },
});
cm?.dispatch({
selection: { anchor: cursorOffset },
});
// setTimeout(() => {
// }, 100);
}, [data, setData]);
useEffect(() => {
const handleKeyDown = (event: any) => {
if ((event.ctrlKey || event.metaKey) && event.key === "s") {
event.preventDefault();
onFormat();
}
};
window.addEventListener("keydown", handleKeyDown);
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, [onFormat]);
if (!isMounted) {
return null;
}
export default function Home() {
return ( return (
<main className="flex min-h-screen flex-col items-center justify-between p-24"> <ResizablePanelGroup
<div className="z-10 max-w-5xl w-full items-center justify-between font-mono text-sm lg:flex"> autoSaveId="main-panel"
<p className="fixed left-0 top-0 flex w-full justify-center border-b border-gray-300 bg-gradient-to-b from-zinc-200 pb-6 pt-8 backdrop-blur-2xl dark:border-neutral-800 dark:bg-zinc-800/30 dark:from-inherit lg:static lg:w-auto lg:rounded-xl lg:border lg:bg-gray-200 lg:p-4 lg:dark:bg-zinc-800/30"> direction={isMobile ? "vertical" : "horizontal"}
Get started by editing&nbsp; className="w-full !h-dvh"
<code className="font-mono font-bold">src/app/page.tsx</code>
</p>
<div className="fixed bottom-0 left-0 flex h-48 w-full items-end justify-center bg-gradient-to-t from-white via-white dark:from-black dark:via-black lg:static lg:h-auto lg:w-auto lg:bg-none">
<a
className="pointer-events-none flex place-items-center gap-2 p-8 lg:pointer-events-auto lg:p-0"
href="https://vercel.com?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
> >
By{" "} <ResizablePanel
<Image defaultSize={isMobile ? 50 : 60}
src="/vercel.svg" minSize={20}
alt="Vercel Logo" className="p-4 pr-0"
className="dark:invert" >
width={100} <Panel>
height={24} <ResizablePanelGroup
priority autoSaveId="editor-panel"
direction="horizontal"
className="border-t border-t-slate-900"
>
<ResizablePanel
defaultSize={25}
minSize={10}
collapsible
collapsedSize={0}
className="bg-[#1e2536]"
>
File List
</ResizablePanel>
<ResizableHandle className="bg-slate-900" />
<ResizablePanel defaultSize={75}>
<button onClick={() => setLang("html")}>HTML</button>
<button onClick={() => setLang("css")}>CSS</button>
<button onClick={() => setLang("js")}>Javascript</button>
<button onClick={onFormat}>Format</button>
<CodeEditor
ref={codeMirror}
lang={lang}
value={data}
onChange={setData}
/> />
</a> </ResizablePanel>
</div> </ResizablePanelGroup>
</div> </Panel>
</ResizablePanel>
<div className="relative flex place-items-center before:absolute before:h-[300px] before:w-full sm:before:w-[480px] before:-translate-x-1/2 before:rounded-full before:bg-gradient-radial before:from-white before:to-transparent before:blur-2xl before:content-[''] after:absolute after:-z-20 after:h-[180px] after:w-full sm:after:w-[240px] after:translate-x-1/3 after:bg-gradient-conic after:from-sky-200 after:via-blue-200 after:blur-2xl after:content-[''] before:dark:bg-gradient-to-br before:dark:from-transparent before:dark:to-blue-700 before:dark:opacity-10 after:dark:from-sky-900 after:dark:via-[#0141ff] after:dark:opacity-40 before:lg:h-[360px] z-[-1]"> <ResizableHandle className="bg-transparent hover:bg-slate-500 transition-colors mx-1 my-4 w-2 rounded-lg" />
<Image <ResizablePanel
className="relative dark:drop-shadow-[0_0_0.3rem_#ffffff70] dark:invert" defaultSize={isMobile ? 50 : 40}
src="/next.svg" collapsible
alt="Next.js Logo" collapsedSize={0}
width={180} minSize={10}
height={37} >
priority <div className="w-full h-full p-4 pl-0">
<Panel>
<iframe
ref={frameRef}
className="border-none w-full h-full bg-white"
/> />
</Panel>
</div> </div>
</ResizablePanel>
<div className="mb-32 grid text-center lg:max-w-5xl lg:w-full lg:mb-0 lg:grid-cols-4 lg:text-left"> </ResizablePanelGroup>
<a
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
target="_blank"
rel="noopener noreferrer"
>
<h2 className={`mb-3 text-2xl font-semibold`}>
Docs{" "}
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
-&gt;
</span>
</h2>
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
Find in-depth information about Next.js features and API.
</p>
</a>
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
target="_blank"
rel="noopener noreferrer"
>
<h2 className={`mb-3 text-2xl font-semibold`}>
Learn{" "}
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
-&gt;
</span>
</h2>
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
Learn about Next.js in an interactive course with&nbsp;quizzes!
</p>
</a>
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
target="_blank"
rel="noopener noreferrer"
>
<h2 className={`mb-3 text-2xl font-semibold`}>
Templates{" "}
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
-&gt;
</span>
</h2>
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
Explore starter templates for Next.js.
</p>
</a>
<a
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
target="_blank"
rel="noopener noreferrer"
>
<h2 className={`mb-3 text-2xl font-semibold`}>
Deploy{" "}
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
-&gt;
</span>
</h2>
<p className={`m-0 max-w-[30ch] text-sm opacity-50 text-balance`}>
Instantly deploy your Next.js site to a shareable URL with Vercel.
</p>
</a>
</div>
</main>
); );
} };
export default HomePage;

43
src/app/providers.tsx Normal file
View File

@ -0,0 +1,43 @@
"use client";
import React, { useState } from "react";
import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
import trpc, { getBaseUrl } from "@/lib/trpc";
import { httpBatchLink } from "@trpc/react-query";
type Props = {
children: React.ReactNode;
};
const Providers = ({ children }: Props) => {
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
},
},
})
);
const [trpcClient] = useState(() =>
trpc.createClient({
links: [
httpBatchLink({
url: getBaseUrl() + "/api/trpc",
headers() {
return {};
},
}),
],
})
);
return (
<trpc.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
</trpc.Provider>
);
};
export default Providers;

View File

@ -0,0 +1,62 @@
/* eslint-disable react/display-name */
import ReactCodeMirror, { EditorView } from "@uiw/react-codemirror";
import { javascript } from "@codemirror/lang-javascript";
import { css } from "@codemirror/lang-css";
import { html } from "@codemirror/lang-html";
import { forwardRef, useEffect, useMemo, useState } from "react";
import { tokyoNight } from "@uiw/codemirror-theme-tokyo-night";
import { useDebounceCallback } from "usehooks-ts";
type Props = {
lang?: string;
value: string;
onChange: (val: string) => void;
};
const CodeEditor = forwardRef(({ lang, value, onChange }: Props, ref: any) => {
const [data, setData] = useState(value);
const debounceValue = useDebounceCallback(onChange, 100);
const extensions = useMemo(() => {
const e: any[] = [];
switch (lang) {
case "html":
e.push(html({ selfClosingTags: true }));
case "css":
e.push(css());
case "jsx":
case "js":
case "ts":
case "tsx":
e.push(
javascript({
jsx: ["jsx", "tsx"].includes(lang),
typescript: ["tsx", "ts"].includes(lang),
})
);
}
return e;
}, [lang]);
useEffect(() => {
setData(value);
}, [value]);
return (
<ReactCodeMirror
ref={ref}
extensions={[EditorView.lineWrapping, ...extensions]}
value={data}
onChange={(val) => {
setData(val);
debounceValue(val);
}}
height="100%"
theme={tokyoNight}
/>
);
});
export default CodeEditor;

View File

@ -0,0 +1,20 @@
import React from "react";
type Props = {
children?: React.ReactNode;
};
const Panel = ({ children }: Props) => {
return (
<div className="bg-slate-800 rounded-lg pt-9 w-full h-full relative shadow-lg overflow-hidden">
<div className="flex gap-2 absolute top-3 left-4">
<div className="bg-red-500 rounded-full h-3 w-3" />
<div className="bg-yellow-500 rounded-full h-3 w-3" />
<div className="bg-green-500 rounded-full h-3 w-3" />
</div>
{children}
</div>
);
};
export default Panel;

View File

@ -0,0 +1,45 @@
"use client";
import { GripVertical } from "lucide-react";
import * as ResizablePrimitive from "react-resizable-panels";
import { cn } from "@/lib/utils";
const ResizablePanelGroup = ({
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) => (
<ResizablePrimitive.PanelGroup
className={cn(
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
className
)}
{...props}
/>
);
const ResizablePanel = ResizablePrimitive.Panel;
const ResizableHandle = ({
withHandle,
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
withHandle?: boolean;
}) => (
<ResizablePrimitive.PanelResizeHandle
className={cn(
"relative flex w-px items-center justify-center bg-slate-200 after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-slate-950 focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90",
className
)}
{...props}
>
{withHandle && (
<div className="z-10 flex h-4 w-3 items-center justify-center rounded-sm border border-slate-200 bg-slate-200 dark:border-slate-800 dark:bg-slate-800">
<GripVertical className="h-2.5 w-2.5" />
</div>
)}
</ResizablePrimitive.PanelResizeHandle>
);
export { ResizablePanelGroup, ResizablePanel, ResizableHandle };

View File

@ -0,0 +1,95 @@
import { useState } from "react";
import { useIsomorphicLayoutEffect } from "usehooks-ts";
type UseMediaQueryOptions = {
defaultValue?: boolean;
initializeWithValue?: boolean;
};
const IS_SERVER = typeof window === "undefined";
export function useMediaQuery(
query: string,
options?: UseMediaQueryOptions
): boolean;
/**
* Custom hook for tracking the state of a media query.
* @deprecated - this useMediaQuery's signature is deprecated, it now accepts an query parameter and an options object.
* @param {string} query - The media query to track.
* @param {?boolean} [defaultValue] - The default value to return if the hook is being run on the server (default is `false`).
* @returns {boolean} The current state of the media query (true if the query matches, false otherwise).
* @see [Documentation](https://usehooks-ts.com/react-hook/use-media-query)
* @see [MDN Match Media](https://developer.mozilla.org/en-US/docs/Web/API/Window/matchMedia)
* @example
* const isSmallScreen = useMediaQuery('(max-width: 600px)');
* // Use `isSmallScreen` to conditionally apply styles or logic based on the screen size.
*/
export function useMediaQuery(query: string, defaultValue: boolean): boolean; // defaultValue should be false by default
/**
* Custom hook for tracking the state of a media query.
* @param {string} query - The media query to track.
* @param {boolean | ?UseMediaQueryOptions} [options] - The default value to return if the hook is being run on the server (default is `false`).
* @param {?boolean} [options.defaultValue] - The default value to return if the hook is being run on the server (default is `false`).
* @param {?boolean} [options.initializeWithValue] - If `true` (default), the hook will initialize reading the media query. In SSR, you should set it to `false`, returning `options.defaultValue` or `false` initially.
* @returns {boolean} The current state of the media query (true if the query matches, false otherwise).
* @see [Documentation](https://usehooks-ts.com/react-hook/use-media-query)
* @see [MDN Match Media](https://developer.mozilla.org/en-US/docs/Web/API/Window/matchMedia)
* @example
* const isSmallScreen = useMediaQuery('(max-width: 600px)');
* // Use `isSmallScreen` to conditionally apply styles or logic based on the screen size.
*/
export function useMediaQuery(
query: string,
options?: boolean | UseMediaQueryOptions
): boolean {
// TODO: Refactor this code after the deprecated signature has been removed.
const defaultValue =
typeof options === "boolean" ? options : options?.defaultValue ?? false;
const initializeWithValue =
typeof options === "boolean"
? undefined
: options?.initializeWithValue ?? undefined;
const [matches, setMatches] = useState<boolean>(() => {
if (initializeWithValue) {
return getMatches(query);
}
return defaultValue;
});
const getMatches = (query: string): boolean => {
if (IS_SERVER) {
return defaultValue;
}
return window.matchMedia(query).matches;
};
/** Handles the change event of the media query. */
function handleChange() {
setMatches(getMatches(query));
}
useIsomorphicLayoutEffect(() => {
const matchMedia = window.matchMedia(query);
// Triggered at the first client-side load and if query changes
handleChange();
// Use deprecated `addListener` and `removeListener` to support Safari < 14 (#135)
if (matchMedia.addListener) {
matchMedia.addListener(handleChange);
} else {
matchMedia.addEventListener("change", handleChange);
}
return () => {
if (matchMedia.removeListener) {
matchMedia.removeListener(handleChange);
} else {
matchMedia.removeEventListener("change", handleChange);
}
};
}, [query]);
return matches;
}

11
src/lib/trpc.ts Normal file
View File

@ -0,0 +1,11 @@
import { createTRPCReact } from "@trpc/react-query";
import type { AppRouter } from "@/server/routers/_app";
export const getBaseUrl = () => {
if (typeof window !== "undefined") return "";
return `http://localhost:${process.env.PORT ?? 3000}`;
};
const trpc = createTRPCReact<AppRouter>({});
export default trpc;

6
src/lib/utils.ts Normal file
View File

@ -0,0 +1,6 @@
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

10
src/server/context.ts Normal file
View File

@ -0,0 +1,10 @@
import { headers as getHeaders, cookies as getCookies } from "next/headers";
export const createContext = async () => {
// const headers = getHeaders();
// const cookies = getCookies();
return {};
};
export type Context = Awaited<ReturnType<typeof createContext>>;

5
src/server/index.ts Normal file
View File

@ -0,0 +1,5 @@
import { createContext } from "./context";
import { appRouter } from "./routers/_app";
import { createCallerFactory } from "./trpc";
export const trpcServer = createCallerFactory(appRouter)(createContext);

View File

@ -0,0 +1,19 @@
import { z } from "zod";
import { procedure, router } from "../trpc";
export const appRouter = router({
hello: procedure
.input(
z.object({
text: z.string(),
})
)
.query((opts) => {
return {
greeting: `hello ${opts.input.text}`,
};
}),
});
// export type definition of API
export type AppRouter = typeof appRouter;

7
src/server/trpc.ts Normal file
View File

@ -0,0 +1,7 @@
import { initTRPC } from "@trpc/server";
import { Context } from "./context";
const t = initTRPC.context<Context>().create();
// Base router and procedure helpers
export const { router, procedure, createCallerFactory } = t;

View File

@ -1,20 +1,40 @@
import type { Config } from "tailwindcss"; import type { Config } from "tailwindcss"
const config: Config = { const config = {
darkMode: ["class"],
content: [ content: [
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}", './pages/**/*.{ts,tsx}',
"./src/components/**/*.{js,ts,jsx,tsx,mdx}", './components/**/*.{ts,tsx}',
"./src/app/**/*.{js,ts,jsx,tsx,mdx}", './app/**/*.{ts,tsx}',
'./src/**/*.{ts,tsx}',
], ],
prefix: "",
theme: { theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: { extend: {
backgroundImage: { keyframes: {
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))", "accordion-down": {
"gradient-conic": from: { height: "0" },
"conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
}, },
}, },
}, },
plugins: [], plugins: [require("tailwindcss-animate")],
}; } satisfies Config
export default config;
export default config