diff --git a/apps/ui-playground/.gitignore b/apps/ui-playground/.gitignore new file mode 100644 index 0000000000..b4ae1e735b --- /dev/null +++ b/apps/ui-playground/.gitignore @@ -0,0 +1,44 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +# Source maps +*.source/* diff --git a/apps/ui-playground/README.md b/apps/ui-playground/README.md new file mode 100644 index 0000000000..e215bc4ccf --- /dev/null +++ b/apps/ui-playground/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/apps/ui-playground/app/(home)/layout.tsx b/apps/ui-playground/app/(home)/layout.tsx new file mode 100644 index 0000000000..a44af49c2a --- /dev/null +++ b/apps/ui-playground/app/(home)/layout.tsx @@ -0,0 +1,9 @@ +import { HomeLayout } from "fumadocs-ui/layouts/home"; +import type { ReactNode } from "react"; + +import { baseOptions } from "../layout.config"; + +export default function Layout({ children }: { children: ReactNode }) { + // @ts-expect-error weird type check + return {children}; +} diff --git a/apps/ui-playground/app/(home)/page.tsx b/apps/ui-playground/app/(home)/page.tsx new file mode 100644 index 0000000000..8cff0ff1c8 --- /dev/null +++ b/apps/ui-playground/app/(home)/page.tsx @@ -0,0 +1,12 @@ +export default function Page() { + return ( +
+
+

+ Open UI for absolutely +
Everyone +

+
+
+ ); +} diff --git a/apps/ui-playground/app/api/search/route.ts b/apps/ui-playground/app/api/search/route.ts new file mode 100644 index 0000000000..e10d2f3b12 --- /dev/null +++ b/apps/ui-playground/app/api/search/route.ts @@ -0,0 +1,16 @@ +import { componentSource } from "@/app/source"; +import { createSearchAPI } from "fumadocs-core/search/server"; + +const indexes = [componentSource].flatMap((src) => + src.getPages().map((page) => ({ + title: page.data.title, + description: page.data.description, + structuredData: page.data.structuredData, + id: page.url, + url: page.url, + })) +); + +export const { GET } = createSearchAPI("advanced", { + indexes, +}); diff --git a/apps/ui-playground/app/components/icon-swatch.tsx b/apps/ui-playground/app/components/icon-swatch.tsx new file mode 100644 index 0000000000..0fbc6cce8a --- /dev/null +++ b/apps/ui-playground/app/components/icon-swatch.tsx @@ -0,0 +1,21 @@ +import type { PropsWithChildren } from "react"; + +type Props = { + name: string; +}; +export const Icon: React.FC> = (props) => { + return ( +
+
+ {props.children} +
+ + {"<"} + {props.name} + {"/>"} + +
+ ); +}; + +Icon.displayName = "Icon"; diff --git a/apps/ui-playground/app/components/render.tsx b/apps/ui-playground/app/components/render.tsx new file mode 100644 index 0000000000..652a80e84b --- /dev/null +++ b/apps/ui-playground/app/components/render.tsx @@ -0,0 +1,57 @@ +"use client"; + +import { type PropsWithChildren, useState } from "react"; + +import className from "@calcom/ui/classNames"; +import { Button } from "@calcom/ui/components/button"; + +type Props = { + customCodeSnippet?: string; + className?: string; +}; + +export const RenderComponentWithSnippet: React.FC> = (props) => { + const [open, setOpen] = useState(false); + // const snippet = + // props.customCodeSnippet ?? + // reactElementToJSXString(props.children, { + // showFunctions: true, + // useBooleanShorthandSyntax: true, + + // displayName: (node) => { + // // @ts-ignore + // return node?.type.displayName ?? node?.type ?? "Unknown"; + // }, + // }); + return ( +
+
{props.children}
+ +
+ +
+ +
+
+ {/*
+            {snippet
+              .split("\n")
+              .map((_line, i) => i + 1)
+              .join("\n")}
+          
+
{snippet}
*/} +
+
+
+ ); +}; diff --git a/apps/ui-playground/app/components/row.tsx b/apps/ui-playground/app/components/row.tsx new file mode 100644 index 0000000000..f5ca400e44 --- /dev/null +++ b/apps/ui-playground/app/components/row.tsx @@ -0,0 +1,7 @@ +import type { PropsWithChildren } from "react"; + +export const Row: React.FC = (props) => { + return
{props.children}
; +}; + +Row.displayName = "Row"; diff --git a/apps/ui-playground/app/design/[[...slug]]/page.tsx b/apps/ui-playground/app/design/[[...slug]]/page.tsx new file mode 100644 index 0000000000..6a14cb8c87 --- /dev/null +++ b/apps/ui-playground/app/design/[[...slug]]/page.tsx @@ -0,0 +1,56 @@ +import { componentSource } from "@/app/source"; +import { TooltipProvider } from "@radix-ui/react-tooltip"; +import { createTypeTable } from "fumadocs-typescript/ui"; +import defaultMdxComponents from "fumadocs-ui/mdx"; +import { DocsBody, DocsDescription, DocsPage, DocsTitle } from "fumadocs-ui/page"; +import type { Metadata } from "next"; +import { notFound } from "next/navigation"; +import { Toaster } from "react-hot-toast"; + +export default async function Page(props: { params: Promise<{ slug?: string[] }> }) { + const params = await props.params; + + const page = componentSource.getPage(params.slug); + + if (!page) { + notFound(); + } + const { AutoTypeTable } = createTypeTable(); + + const MDX = page.data.body; + + return ( + <> + {/* @ts-expect-error idk why fumadocs doersnt like this */} + + + {page.data.title} + + {page.data.description} + + + + {/* @ts-expect-error idk why fumadocs doersnt like this */} + + + + + + ); +} + +export async function generateStaticParams() { + return componentSource.generateParams(); +} + +export async function generateMetadata({ params }: { params: Promise<{ slug?: string[] }> }) { + const page = componentSource.getPage((await params).slug); + if (!page) { + notFound(); + } + + return { + title: page.data.title, + description: page.data.description, + } satisfies Metadata; +} diff --git a/apps/ui-playground/app/design/layout.tsx b/apps/ui-playground/app/design/layout.tsx new file mode 100644 index 0000000000..270fa5319f --- /dev/null +++ b/apps/ui-playground/app/design/layout.tsx @@ -0,0 +1,17 @@ +import { componentSource } from "@/app/source"; +import { TooltipProvider } from "@radix-ui/react-tooltip"; +import { DocsLayout } from "fumadocs-ui/layouts/notebook"; +import type { ReactNode } from "react"; + +import { IconSprites } from "@calcom/ui/components/icon"; + +import { baseOptions } from "../layout.config"; + +export default function Layout({ children }: { children: ReactNode }) { + return ( + + {children} + + + ); +} diff --git a/apps/ui-playground/app/global.css b/apps/ui-playground/app/global.css new file mode 100644 index 0000000000..c995348e16 --- /dev/null +++ b/apps/ui-playground/app/global.css @@ -0,0 +1,1030 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; +/** +* Add css variables here as well for light mode in addition to tailwind.config to avoid FOUC +*/ +:root { + /* Spacing Scale */ + --cal-spacing-0: 0px; + --cal-spacing-px: 1px; + --cal-spacing-0_5: 0.125rem; /* 2px */ + --cal-spacing-1: 0.25rem; /* 4px */ + --cal-spacing-1_5: 0.375rem; /* 6px */ + --cal-spacing-2: 0.5rem; /* 8px */ + --cal-spacing-2_5: 0.625rem; /* 10px */ + --cal-spacing-3: 0.75rem; /* 12px */ + --cal-spacing-3_5: 0.875rem; /* 14px */ + --cal-spacing-4: 1rem; /* 16px */ + --cal-spacing-5: 1.25rem; /* 20px */ + --cal-spacing-6: 1.5rem; /* 24px */ + --cal-spacing-7: 1.75rem; /* 28px */ + --cal-spacing-8: 2rem; /* 32px */ + --cal-spacing-9: 2.25rem; /* 36px */ + --cal-spacing-10: 2.5rem; /* 40px */ + --cal-spacing-11: 2.75rem; /* 44px */ + --cal-spacing-12: 3rem; /* 48px */ + --cal-spacing-14: 3.5rem; /* 56px */ + --cal-spacing-16: 4rem; /* 64px */ + --cal-spacing-20: 5rem; /* 80px */ + --cal-spacing-24: 6rem; /* 96px */ + --cal-spacing-28: 7rem; /* 112px */ + --cal-spacing-32: 8rem; /* 128px */ + --cal-spacing-36: 9rem; /* 144px */ + --cal-spacing-40: 10rem; /* 160px */ + --cal-spacing-44: 11rem; /* 176px */ + --cal-spacing-48: 12rem; /* 192px */ + --cal-spacing-52: 13rem; /* 208px */ + --cal-spacing-56: 14rem; /* 224px */ + --cal-spacing-60: 15rem; /* 240px */ + --cal-spacing-64: 16rem; /* 256px */ + --cal-spacing-72: 18rem; /* 288px */ + --cal-spacing-80: 20rem; /* 320px */ + --cal-spacing-96: 24rem; /* 384px */ + + /* Border Radius */ + --cal-radius-none: 0px; + --cal-radius-sm: 0.125rem; /* 2px */ + --cal-radius: 0.25rem; /* 4px */ + --cal-radius-md: 0.375rem; /* 6px */ + --cal-radius-lg: 0.5rem; /* 8px */ + --cal-radius-xl: 0.75rem; /* 12px */ + --cal-radius-2xl: 1rem; /* 16px */ + --cal-radius-3xl: 1.5rem; /* 24px */ + --cal-radius-full: 9999px; + + /* Background Standard */ + --cal-bg-emphasis: hsla(220, 13%, 91%, 1); /* gray.200 - active items and emphasising */ + --cal-bg: hsla(0, 0%, 100%, 1); /* white - default background */ + --cal-bg-subtle: hsla(220, 14%, 94%, 1); /* gray.100 - hovering on items, subtle raising */ + --cal-bg-muted: hsla(210, 20%, 97%, 1); /* gray.50 - large items, sidebar, sections */ + --cal-bg-inverted: hsla(210, 30%, 4%, 1); /* gray.900 - tooltips */ + + /* Background Primary */ + --cal-bg-primary: hsla(214, 30%, 16%, 1); /* stone-100 */ + --cal-bg-primary-emphasis: hsla(220, 6%, 25%, 1); /* stone-800 */ + --cal-bg-primary-muted: hsla(220, 14%, 94%, 1); /* stone-400 */ + + /* Background Brand */ + --cal-bg-brand: hsla(214, 30%, 16%, 1); /* gray-50 */ + --cal-bg-brand-emphasis: hsla(220, 6%, 25%, 1); /* stone-100 */ + --cal-bg-brand-muted: hsla(220, 14%, 94%, 1); /* stone-100 */ + + /* Background Semantic */ + --cal-bg-semantic-info-subtle: hsla(212, 88%, 97%, 1); /* blue-100 */ + --cal-bg-semantic-info-emphasis: hsla(236, 80%, 25%, 1); /* blue-500 */ + --cal-bg-semantic-success-subtle: hsla(167, 54%, 93%, 1); /* green-100 */ + --cal-bg-semantic-success-emphasis: hsla(158, 74%, 38%, 1); /* green-500 */ + --cal-bg-semantic-attention-subtle: hsla(34, 100%, 92%, 1); /* orange-100 */ + --cal-bg-semantic-attention-emphasis: hsla(15, 79%, 34%, 1); /* orange-500 */ + --cal-bg-semantic-error-subtle: hsla(0, 93%, 94%, 1); /* red-100 */ + --cal-bg-semantic-error-emphasis: hsla(0, 63%, 24%, 1); /* red-500 */ + + /* Background Visualization */ + --cal-bg-visualization-1-subtle: hsla(326, 78%, 95%, 1); /* pink-100 */ + --cal-bg-visualization-1-emphasis: hsla(330, 81%, 60%, 1); /* pink-500 */ + --cal-bg-visualization-2-subtle: hsla(256, 86%, 91%, 1); /* purple-100 */ + --cal-bg-visualization-2-emphasis: hsla(256, 85%, 57%, 1); /* purple-500 */ + --cal-bg-visualization-3-subtle: hsla(217, 87%, 91%, 1); /* blue-100 */ + --cal-bg-visualization-3-emphasis: hsla(235, 100%, 63%, 1); /* blue-500 */ + --cal-bg-visualization-4-subtle: hsla(167, 54%, 93%, 1); /* green-100 */ + --cal-bg-visualization-4-emphasis: hsla(158, 74%, 38%, 1); /* green-500 */ + --cal-bg-visualization-5-subtle: hsla(55, 97%, 88%, 1); /* yellow-100 */ + --cal-bg-visualization-5-emphasis: hsla(45, 93%, 47%, 1); /* yellow-500 */ + --cal-bg-visualization-6-subtle: hsla(34, 100%, 92%, 1); /* orange-100 */ + --cal-bg-visualization-6-emphasis: hsla(25, 95%, 53%, 1); /* orange-500 */ + --cal-bg-visualization-7-subtle: hsla(0, 96%, 89%, 1); /* red-100 */ + --cal-bg-visualization-7-emphasis: hsla(0, 84%, 60%, 1); /* red-500 */ + + /* Legacy Background Components - Consider deprecating */ + --cal-bg-info: hsla(221, 91%, 93%, 1); /* #dee9fc - info backgrounds */ + --cal-bg-success: hsla(142, 76%, 94%, 1); /* #e2fbe8 - success backgrounds */ + --cal-bg-attention: hsla(33, 100%, 92%, 1); /* #fceed8 - attention backgrounds */ + --cal-bg-error: hsla(3, 66%, 93%, 1); /* #f9e3e2 - error backgrounds */ + --cal-bg-dark-error: hsla(2, 55%, 30%, 1); /* Keeping existing dark error */ + + /* Borders */ + --cal-border-emphasis: hsla(218, 11%, 65%, 1); /* gray.400 - input:hover */ + --cal-border: hsla(216, 12%, 84%, 1); /* gray.300 - inputs */ + --cal-border-subtle: hsla(220, 13%, 91%, 1); /* gray.200 - container borders */ + --cal-border-muted: hsla(220, 14%, 94%, 1); /* gray.100 */ + --cal-border-error: hsla(0, 96%, 89%, 1); /* Keeping existing error border */ + --cal-border-semantic-error: hsla(0, 96%, 89%, 1); + + /* Content/Text Standard */ + --cal-text-emphasis: hsla(210, 30%, 4%, 1); /* gray-900 */ + --cal-text: hsla(220, 6%, 25%, 1); /* gray-700 */ + --cal-text-subtle: hsla(220, 9%, 46%, 1); /* gray-500 */ + --cal-text-muted: hsla(218, 11%, 65%, 1); /* gray-400 */ + --cal-text-inverted: hsla(0, 0%, 100%, 1); /* white */ + + /* Content/Text Semantic */ + --cal-text-semantic-info: hsla(236, 80%, 25%, 1); /* blue-800 */ + --cal-text-semantic-success: hsla(150, 84%, 22%, 1); /* green-800 */ + --cal-text-semantic-attention: hsla(15, 79%, 34%, 1); /* orange-800 */ + --cal-text-semantic-error: hsla(0, 63%, 24%, 1); /* red-800 */ + + /* Border/Semantic Subtle */ + --cal-border-semantic-attention-subtle: hsla(32, 98%, 83%, 1); + --cal-border-semantic-error-subtle: hsla(0, 96%, 89%, 1); + + /* Content/Text Visualization */ + --cal-text-visualization-1: hsla(332, 79%, 25%, 1); /* pink-800 */ + --cal-text-visualization-2: hsla(270, 91%, 25%, 1); /* purple-800 */ + --cal-text-visualization-3: hsla(217, 91%, 25%, 1); /* blue-800 */ + --cal-text-visualization-4: hsla(142, 71%, 25%, 1); /* green-800 */ + --cal-text-visualization-5: hsla(45, 93%, 25%, 1); /* yellow-800 */ + --cal-text-visualization-6: hsla(24, 95%, 25%, 1); /* orange-800 */ + --cal-text-visualization-7: hsla(0, 84%, 25%, 1); /* red-800 */ + + /* Legacy Content/Text Components - Consider deprecating */ + --cal-text-info: hsla(225, 57%, 33%, 1); /* #253985 - interactive text/icons */ + --cal-text-success: hsla(144, 34%, 24%, 1); /* #285231 */ + --cal-text-attention: hsla(16, 62%, 28%, 1); /* Keeping existing attention text */ + --cal-text-error: hsla(0, 63%, 31%, 1); /* Keeping existing error text */ + + /* Brand */ + --cal-brand: hsla(221, 39%, 11%, 1); /* Keeping existing brand color */ + --cal-brand-emphasis: hsla(0, 0%, 6%, 1); /* Keeping existing brand emphasis */ + --cal-brand-text: hsla(0, 0%, 100%, 1); /* white */ +} +.dark { + /* Background Standard */ + --cal-bg-emphasis: hsla(0, 0%, 25%, 1); /* stone-700 */ + --cal-bg: hsla(0, 0%, 6%, 1); /* stone-950 */ + --cal-bg-subtle: hsla(0, 0%, 15%, 1); /* stone-800 */ + --cal-bg-muted: hsla(0, 0%, 9%, 1); /* stone-900 */ + --cal-bg-inverted: hsla(0, 0%, 98%, 1); /* stone-50 */ + + /* Background Primary */ + --cal-bg-primary: hsla(0, 0%, 96%, 1); /* stone-100 */ + --cal-bg-primary-emphasis: hsla(0, 0%, 64%, 1); /* stone-800 */ + --cal-bg-primary-muted: hsla(0, 0%, 15%, 1); /* stone-400 */ + + /* Background Brand */ + --cal-bg-brand: hsla(0, 0%, 98%, 1); /* gray-50 */ + --cal-bg-brand-emphasis: hsla(0, 0%, 96%, 1); /* stone-100 */ + --cal-bg-brand-muted: hsla(0, 0%, 96%, 1); /* stone-100 */ + + /* Background Semantic */ + --cal-bg-semantic-info-subtle: hsla(236, 80%, 8%, 1); /* blue-800 */ + --cal-bg-semantic-info-emphasis: hsla(229, 90%, 74%, 1); /* blue-500 */ + --cal-bg-semantic-success-subtle: hsla(148, 88%, 16%, 1); /* green-800 */ + --cal-bg-semantic-success-emphasis: hsla(158, 74%, 38%, 1); /* green-500 */ + --cal-bg-semantic-attention-subtle: hsla(21, 86%, 8%, 1); /* orange-800 */ + --cal-bg-semantic-attention-emphasis: hsla(27, 96%, 61%, 1); /* orange-500 */ + --cal-bg-semantic-error-subtle: hsla(0, 70%, 8%, 1); /* red-800 */ + --cal-bg-semantic-error-emphasis: hsla(0, 91%, 71%, 1); /* red-500 */ + + /* Background Visualization */ + --cal-bg-visualization-1-subtle: hsla(336, 74%, 35%, 1); /* pink-200 */ + --cal-bg-visualization-1-emphasis: hsla(330, 81%, 60%, 1); /* pink-500 */ + --cal-bg-visualization-2-subtle: hsla(252, 83%, 23%, 1); /* purple-200 */ + --cal-bg-visualization-2-emphasis: hsla(256, 85%, 57%, 1); /* purple-500 */ + --cal-bg-visualization-3-subtle: hsla(236, 74%, 35%, 1); /* blue-200 */ + --cal-bg-visualization-3-emphasis: hsla(235, 100%, 63%, 1); /* blue-500 */ + --cal-bg-visualization-4-subtle: hsla(150, 84%, 22%, 1); /* green-200 */ + --cal-bg-visualization-4-emphasis: hsla(158, 74%, 38%, 1); /* green-500 */ + --cal-bg-visualization-5-subtle: hsla(28, 73%, 26%, 1); /* yellow-200 */ + --cal-bg-visualization-5-emphasis: hsla(45, 93%, 47%, 1); /* yellow-500 */ + --cal-bg-visualization-6-subtle: hsla(15, 75%, 23%, 1); /* orange-200 */ + --cal-bg-visualization-6-emphasis: hsla(25, 95%, 53%, 1); /* orange-500 */ + --cal-bg-visualization-7-subtle: hsla(0, 70%, 35%, 1); /* red-200 */ + --cal-bg-visualization-7-emphasis: hsla(0, 84%, 60%, 1); /* red-500 */ + + /* Legacy Background Components - Consider deprecating */ + --cal-bg-info: hsla(228, 56%, 33%, 1); /* Keeping existing info background */ + --cal-bg-success: hsla(133, 34%, 24%, 1); /* Keeping existing success background */ + --cal-bg-attention: hsla(16, 62%, 28%, 1); /* Keeping existing attention background */ + --cal-bg-error: hsla(2, 55%, 30%, 1); /* Keeping existing error background */ + --cal-bg-dark-error: hsla(2, 55%, 30%, 1); /* Keeping existing dark error */ + + /* Border Standard */ + --cal-border: hsla(0, 0%, 30%, 1); /* stone-600 */ + --cal-border-muted: hsla(0, 0%, 9%, 1); /* stone-500 */ + --cal-border-subtle: hsla(0, 0%, 15%, 1); /* stone-700 */ + --cal-border-emphasis: hsla(0, 0%, 45%, 1); /* stone-800 */ + /* Border Semantic */ + --cal-border-semantic-error: hsla(0, 63%, 24%, 1); /* red-800 */ + + /* Legacy Border - Consider deprecating */ + --cal-border-error: hsla(0, 63%, 24%, 1); /* Keeping existing error border */ + + /* Content/Text Standard */ + --cal-text-emphasis: hsla(0, 0%, 98%, 1); /* stone-50 */ + --cal-text: hsla(0, 0%, 83%, 1); /* stone-300 */ + --cal-text-subtle: hsla(0, 0%, 64%, 1); /* stone-400 */ + --cal-text-muted: hsla(0, 0%, 64%, 1); /* stone-400 */ + --cal-text-inverted: hsla(0, 0%, 0%, 1); /* black */ + + /* Content/Text Semantic */ + --cal-text-semantic-info: hsla(229, 90%, 74%, 1); /* blue-100 */ + --cal-text-semantic-success: hsla(161, 49%, 54%, 1); /* green-100 */ + --cal-text-semantic-attention: hsla(27, 96%, 61%, 1); /* orange-100 */ + --cal-text-semantic-error: hsla(0, 91%, 71%, 1); /* red-100 */ + + /* Content/Text Semantic Emphasis */ + --cal-text-semantic-info-emphasis: hsla(217, 91%, 25%, 1); /* blue-800 */ + --cal-text-semantic-success-emphasis: hsla(142, 71%, 25%, 1); /* green-800 */ + --cal-text-semantic-attention-emphasis: hsla(24, 95%, 25%, 1); /* orange-800 */ + --cal-text-semantic-error-emphasis: hsla(0, 84%, 25%, 1); /* red-800 */ + + /* Border/Semantic Subtle */ + --cal-border-semantic-attention-subtle: hsla(15, 75%, 23%, 1); + --cal-border-semantic-error-subtle: hsla(0, 63%, 24%, 1); + + /* Legacy Content/Text Components - Consider deprecating */ + --cal-text-info: hsla(218, 83%, 93%, 1); /* Keeping existing info text */ + --cal-text-success: hsla(134, 76%, 94%, 1); /* Keeping existing success text */ + --cal-text-attention: hsla(37, 86%, 92%, 1); /* Keeping existing attention text */ + --cal-text-error: hsla(3, 66%, 93%, 1); /* Keeping existing error text */ + + /* Brand */ + --cal-brand: hsla(0, 0%, 100%, 1); /* white */ + --cal-brand-emphasis: hsla(218, 11%, 65%, 1); /* Keeping existing brand emphasis */ + --cal-brand-text: hsla(0, 0%, 0%, 1); /* black */ +} + +@layer base { + * { + @apply border-default + } +} + +::-moz-selection { + color: var(--cal-brand-text); + background: var(--cal-brand); +} + +::selection { + color: var(--cal-brand-text); + background: var(--cal-brand); +} + +body  { + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; +} + +/* + Desktop App specific CSS + https://docs.todesktop.com/ +*/ + +html.todesktop-platform-win32 .todesktop\:\!bg-transparent{ + background: inherit !important; +} + +/* disable user selection on buttons, links and images on desktop app */ +html.todesktop button, +html.todesktop a, +html.todesktop img, +html.todesktop header { + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + cursor: default !important; +} + +html.todesktop, +html.todesktop div { + cursor: default !important; +} + +/* make header draggable on desktop app */ +html.todesktop header { + -webkit-app-region: drag; +} + +html.todesktop header button, +html.todesktop header a { + -webkit-app-region: no-drag; +} + +html.todesktop-platform-darwin body, html.todesktop-platform-darwin aside { + background: transparent !important; +} + +html.todesktop-platform-darwin.dark main.bg-default { + background: rgba(0, 0, 0, 0.6) !important; +} + +html.todesktop-platform-darwin.light main.bg-default { + background: rgba(255, 255, 255, 0.8) !important; +} + +html.todesktop.light { + --cal-bg-emphasis: hsla(0, 0%, 11%, 0.1); +} + +html.todesktop.dark { + --cal-bg-emphasis: hsla(220, 2%, 26%, 0.3); +} + +/* + Adds Utility to hide scrollbar to tailwind + https://github.com/tailwindlabs/tailwindcss/discussions/2394 + https://github.com/tailwindlabs/tailwindcss/pull/5732 +*/ +@layer utilities { + /* Chrome, Safari and Opera */ + .no-scrollbar::-webkit-scrollbar { + display: none; + } + + .no-scrollbar { + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ + } +} + +/* + * Override the default tailwindcss-forms styling (default is: 'colors.blue.600') + * @see: https://github.com/tailwindlabs/tailwindcss-forms/issues/14#issuecomment-1005376006 + */ +[type="text"]:focus, +[type="email"]:focus, +[type="url"]:focus, +[type="password"]:focus, +[type="number"]:focus, +[type="date"]:focus, +[type="datetime-local"]:focus, +[type="month"]:focus, +[type="search"]:focus, +[type="tel"]:focus, +[type="checkbox"]:focus, +[type="radio"]:focus, +[type="time"]:focus, +[type="week"]:focus, +[multiple]:focus, +textarea:focus, +select:focus { + border-color: var(--cal-border-emphasis); + box-shadow: 0px 0px 0px 1px rgba(255, 255, 255, 0.20), 0px 0px 0px 2px rgba(0, 0, 0, 0.10); +} + +@layer components { + .scroll-bar { + @apply scrollbar-thin scrollbar-thumb-rounded-md dark:scrollbar-thumb-darkgray-300 scrollbar-thumb-gray-300 scrollbar-track-transparent; + } +} + +/* TODO: avoid global specific css */ +/* button[role="switch"][data-state="checked"] span { + transform: translateX(16px); +} */ + +@layer components { + /* slider */ + .slider { + @apply relative flex h-4 w-40 select-none items-center; + } + + .slider > .slider-track { + @apply relative h-1 flex-grow rounded-md bg-gray-400; + } + + .slider .slider-range { + @apply absolute h-full rounded-full bg-gray-700; + } + + .slider .slider-thumb { + @apply block h-3 w-3 cursor-pointer rounded-full bg-gray-700 transition-all; + } + + .slider .slider-thumb:hover { + @apply bg-gray-600; + } + + .slider .slider-thumb:focus { + box-shadow: 0 0 0 4px rgba(0, 0, 0, 0.2); + } +} + +/* hide chat bubble on mobile */ +@media only screen and (max-width: 768px) { + /* Intercom FAB*/ + #launcher { + display: none !important; + } + + /* Zendesk FAB*/ + div[role="presentation"] > iframe { + display: none !important; + } + + /* Helpscout FAB*/ + .BeaconFabButtonFrame { + margin-left: -30px; + left: 50%; + bottom: 28px !important; + z-index: 1058 !important; + } +} + +/* TODO: implement styling for react-multi-email */ + +/* !important to override react-dates */ +.DateRangePickerInput__withBorder { + border: 0 !important; +} +.DateInput_input { + border: 1px solid #d1d5db !important; + border-radius: 2px !important; + font-size: inherit !important; + font-weight: inherit !important; + color: #000; + padding: 11px ​11px 9px !important; + line-height: 16px !important; +} + +.DateInput_input__focused { + border: 2px solid #000 !important; + border-radius: 2px !important; + box-shadow: none !important; + padding: 10px ​10px 9px !important; +} + +.DateRangePickerInput_arrow { + padding: 0px 10px; +} + +.loader { + display: block; + width: 30px; + height: 30px; + margin: 60px auto; + position: relative; + border-width: 4px; + border-style: solid; + animation: loader 2s infinite ease; +} + +.loader-inner { + vertical-align: top; + display: inline-block; + width: 100%; + animation: loader-inner 2s infinite ease-in; +} + +.no-ring-inset { + --tw-ring-inset: unset; +} + +@keyframes loader { + 0% { + transform: rotate(0deg); + } + + 25% { + transform: rotate(180deg); + } + + 50% { + transform: rotate(180deg); + } + + 75% { + transform: rotate(360deg); + } + + 100% { + transform: rotate(360deg); + } +} + +@keyframes loader-inner { + 0% { + height: 0%; + } + + 25% { + height: 0%; + } + + 50% { + height: 100%; + } + + 75% { + height: 100%; + } + + 100% { + height: 0%; + } +} + +.text-inverted-important { + color: white !important; +} + +@layer utilities { + .transition-max-width { + -webkit-transition-property: max-width; + transition-property: max-width; + } +} + +#timeZone input:focus { + box-shadow: none; +} + +/* react-date-picker forces a border upon us, cast it away */ +.react-date-picker__wrapper { + border: none !important; +} + +.react-date-picker__inputGroup__input { + padding-top: 0; + padding-bottom: 0; +} + +/* animations */ +.slideInBottom { + animation-duration: 0.3s; + animation-fill-mode: both; + animation-name: slideInBottom; +} + +@keyframes slideInBottom { + from { + opacity: 0; + transform: translateY(30%); + pointer-events: none; + } + to { + opacity: 1; + pointer-events: auto; + } +} + +/* animations */ +.slideInTop { + animation-duration: 0.3s; + animation-fill-mode: both; + animation-name: slideInTop; +} + +@keyframes slideInTop { + from { + opacity: 0; + transform: translateY(-20%); + pointer-events: none; + } + to { + opacity: 1; + pointer-events: auto; + transform: translateY(0%); + } +} + +.fadeIn { + animation-duration: 0.3s; + animation-fill-mode: both; + animation-name: fadeIn; + animation-timing-function: ease-in-out; +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +/** + * Makes sure h-screen works on mobile Safari. By default h-screen + * does not take into account the height of the address bar, causing + * weird behaviour when scrolling — sometimes the height will be correct + * and sometimes it won't, depending on whether the address bar is + * in 'collapsed' state or not. + * @see: https://benborgers.com/posts/tailwind-h-screen + */ +@supports (-webkit-touch-callout: none) { + .h-screen { + height: -webkit-fill-available; + } +} + +::-webkit-search-cancel-button { + -webkit-appearance: none; +} + +.react-tel-input .country-list .country:hover, +.react-tel-input .country-list .country.highlight { + @apply !bg-emphasis; +} + +.react-tel-input .flag-dropdown .selected-flag, +.react-tel-input .flag-dropdown.open .selected-flag { + @apply !bg-default; +} + +.react-tel-input .flag-dropdown { + @apply !border-r-default left-0.5 !border-y-0 !border-l-0; +} + +.intercom-lightweight-app { + @apply z-40 !important; +} + +@import "../../web/styles/tokens.css"; + +@layer base { + * { + @apply border-default + } +} + +::-moz-selection { + color: var(--cal-brand-text); + background: var(--cal-brand); +} + +::selection { + color: var(--cal-brand-text); + background: var(--cal-brand); +} + +body  { + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; +} + +/* + Desktop App specific CSS + https://docs.todesktop.com/ +*/ + +html.todesktop-platform-win32 .todesktop\:\!bg-transparent{ + background: inherit !important; +} + +/* disable user selection on buttons, links and images on desktop app */ +html.todesktop button, +html.todesktop a, +html.todesktop img, +html.todesktop header { + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + cursor: default !important; +} + +html.todesktop, +html.todesktop div { + cursor: default !important; +} + +/* make header draggable on desktop app */ +html.todesktop header { + -webkit-app-region: drag; +} + +html.todesktop header button, +html.todesktop header a { + -webkit-app-region: no-drag; +} + +html.todesktop-platform-darwin body, html.todesktop-platform-darwin aside { + background: transparent !important; +} + +html.todesktop-platform-darwin.dark main.bg-default { + background: rgba(0, 0, 0, 0.6) !important; +} + +html.todesktop-platform-darwin.light main.bg-default { + background: rgba(255, 255, 255, 0.8) !important; +} + +html.todesktop.light { + --cal-bg-emphasis: hsla(0, 0%, 11%, 0.1); +} + +html.todesktop.dark { + --cal-bg-emphasis: hsla(220, 2%, 26%, 0.3); +} + +/* + Adds Utility to hide scrollbar to tailwind + https://github.com/tailwindlabs/tailwindcss/discussions/2394 + https://github.com/tailwindlabs/tailwindcss/pull/5732 +*/ +@layer utilities { + /* Chrome, Safari and Opera */ + .no-scrollbar::-webkit-scrollbar { + display: none; + } + + .no-scrollbar { + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ + } +} + +/* + * Override the default tailwindcss-forms styling (default is: 'colors.blue.600') + * @see: https://github.com/tailwindlabs/tailwindcss-forms/issues/14#issuecomment-1005376006 + */ +[type="text"]:focus, +[type="email"]:focus, +[type="url"]:focus, +[type="password"]:focus, +[type="number"]:focus, +[type="date"]:focus, +[type="datetime-local"]:focus, +[type="month"]:focus, +[type="search"]:focus, +[type="tel"]:focus, +[type="checkbox"]:focus, +[type="radio"]:focus, +[type="time"]:focus, +[type="week"]:focus, +[multiple]:focus, +textarea:focus, +select:focus { + --tw-ring-color: var(--brand-color); + border-color: var(--brand-color); +} + +@layer components { + .scroll-bar { + @apply scrollbar-thin scrollbar-thumb-rounded-md dark:scrollbar-thumb-darkgray-300 scrollbar-thumb-gray-300 scrollbar-track-transparent; + } +} + +/* TODO: avoid global specific css */ +/* button[role="switch"][data-state="checked"] span { + transform: translateX(16px); +} */ + +@layer components { + /* slider */ + .slider { + @apply relative flex h-4 w-40 select-none items-center; + } + + .slider > .slider-track { + @apply relative h-1 flex-grow rounded-md bg-gray-400; + } + + .slider .slider-range { + @apply absolute h-full rounded-full bg-gray-700; + } + + .slider .slider-thumb { + @apply block h-3 w-3 cursor-pointer rounded-full bg-gray-700 transition-all; + } + + .slider .slider-thumb:hover { + @apply bg-gray-600; + } + + .slider .slider-thumb:focus { + box-shadow: 0 0 0 4px rgba(0, 0, 0, 0.2); + } +} + +/* hide chat bubble on mobile */ +@media only screen and (max-width: 768px) { + /* Intercom FAB*/ + #launcher { + display: none !important; + } + + /* Zendesk FAB*/ + div[role="presentation"] > iframe { + display: none !important; + } + + /* Helpscout FAB*/ + .BeaconFabButtonFrame { + margin-left: -30px; + left: 50%; + bottom: 28px !important; + z-index: 1058 !important; + } +} + +/* TODO: implement styling for react-multi-email */ + +/* !important to override react-dates */ +.DateRangePickerInput__withBorder { + border: 0 !important; +} +.DateInput_input { + border: 1px solid #d1d5db !important; + border-radius: 2px !important; + font-size: inherit !important; + font-weight: inherit !important; + color: #000; + padding: 11px ​11px 9px !important; + line-height: 16px !important; +} + +.DateInput_input__focused { + border: 2px solid #000 !important; + border-radius: 2px !important; + box-shadow: none !important; + padding: 10px ​10px 9px !important; +} + +.DateRangePickerInput_arrow { + padding: 0px 10px; +} + +.loader { + display: block; + width: 30px; + height: 30px; + margin: 60px auto; + position: relative; + border-width: 4px; + border-style: solid; + animation: loader 2s infinite ease; +} + +.loader-inner { + vertical-align: top; + display: inline-block; + width: 100%; + animation: loader-inner 2s infinite ease-in; +} + +.no-ring-inset { + --tw-ring-inset: unset; +} + +@keyframes loader { + 0% { + transform: rotate(0deg); + } + + 25% { + transform: rotate(180deg); + } + + 50% { + transform: rotate(180deg); + } + + 75% { + transform: rotate(360deg); + } + + 100% { + transform: rotate(360deg); + } +} + +@keyframes loader-inner { + 0% { + height: 0%; + } + + 25% { + height: 0%; + } + + 50% { + height: 100%; + } + + 75% { + height: 100%; + } + + 100% { + height: 0%; + } +} + +.text-inverted-important { + color: white !important; +} + +@layer utilities { + .transition-max-width { + -webkit-transition-property: max-width; + transition-property: max-width; + } +} + +#timeZone input:focus { + box-shadow: none; +} + +/* react-date-picker forces a border upon us, cast it away */ +.react-date-picker__wrapper { + border: none !important; +} + +.react-date-picker__inputGroup__input { + padding-top: 0; + padding-bottom: 0; +} + +/* animations */ +.slideInBottom { + animation-duration: 0.3s; + animation-fill-mode: both; + animation-name: slideInBottom; +} + +@keyframes slideInBottom { + from { + opacity: 0; + transform: translateY(30%); + pointer-events: none; + } + to { + opacity: 1; + pointer-events: auto; + } +} + +/* animations */ +.slideInTop { + animation-duration: 0.3s; + animation-fill-mode: both; + animation-name: slideInTop; +} + +@keyframes slideInTop { + from { + opacity: 0; + transform: translateY(-20%); + pointer-events: none; + } + to { + opacity: 1; + pointer-events: auto; + transform: translateY(0%); + } +} + +.fadeIn { + animation-duration: 0.3s; + animation-fill-mode: both; + animation-name: fadeIn; + animation-timing-function: ease-in-out; +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +/** + * Makes sure h-screen works on mobile Safari. By default h-screen + * does not take into account the height of the address bar, causing + * weird behaviour when scrolling — sometimes the height will be correct + * and sometimes it won't, depending on whether the address bar is + * in 'collapsed' state or not. + * @see: https://benborgers.com/posts/tailwind-h-screen + */ +@supports (-webkit-touch-callout: none) { + .h-screen { + height: -webkit-fill-available; + } +} + +::-webkit-search-cancel-button { + -webkit-appearance: none; +} + +.react-tel-input .country-list .country:hover, +.react-tel-input .country-list .country.highlight { + @apply !bg-emphasis; +} + +.react-tel-input .flag-dropdown .selected-flag, +.react-tel-input .flag-dropdown.open .selected-flag { + @apply !bg-default; +} + +.react-tel-input .flag-dropdown { + @apply !border-r-default left-0.5 !border-y-0 !border-l-0; +} + +.intercom-lightweight-app { + @apply z-40 !important; +} + +.prose :where(a:not([data-card])):not(:where([class~="not-prose"] *)) { + text-decoration: none; +} \ No newline at end of file diff --git a/apps/ui-playground/app/layout.config.tsx b/apps/ui-playground/app/layout.config.tsx new file mode 100644 index 0000000000..4426a6bf60 --- /dev/null +++ b/apps/ui-playground/app/layout.config.tsx @@ -0,0 +1,26 @@ +import type { HomeLayoutProps } from "fumadocs-ui/layouts/home"; + +/** + * Shared layout configurations + * + * you can configure layouts individually from: + * Home Layout: app/(home)/layout.tsx + * Docs Layout: app/docs/layout.tsx + */ +export const baseOptions: HomeLayoutProps = { + nav: { + title: "Cal.com", + }, + githubUrl: "https://github.com/calcom/cal.com", + links: [ + { + text: "Design", + url: "/design", + active: "nested-url", + }, + { + text: "GitHub", + url: "https://github.com/calcom/cal.com", + }, + ], +}; diff --git a/apps/ui-playground/app/layout.tsx b/apps/ui-playground/app/layout.tsx new file mode 100644 index 0000000000..9c45cca71e --- /dev/null +++ b/apps/ui-playground/app/layout.tsx @@ -0,0 +1,34 @@ +import { RootProvider } from "fumadocs-ui/provider"; +import type { Metadata } from "next"; +import { Geist, Geist_Mono } from "next/font/google"; + +import "./global.css"; + +const geistSans = Geist({ + variable: "--font-geist-sans", + subsets: ["latin"], +}); + +const geistMono = Geist_Mono({ + variable: "--font-geist-mono", + subsets: ["latin"], +}); + +export const metadata: Metadata = { + title: "Create Next App", + description: "Generated by create next app", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + {children} + + + ); +} diff --git a/apps/ui-playground/app/source.ts b/apps/ui-playground/app/source.ts new file mode 100644 index 0000000000..ac2303ab9d --- /dev/null +++ b/apps/ui-playground/app/source.ts @@ -0,0 +1,13 @@ +import { components, docs, meta } from "@/.source"; +import { loader } from "fumadocs-core/source"; +import { createMDXSource } from "fumadocs-mdx"; + +export const source = loader({ + baseUrl: "/docs", + source: createMDXSource(docs, meta), +}); + +export const componentSource = loader({ + baseUrl: "/design", + source: createMDXSource(components, []), +}); diff --git a/apps/ui-playground/components/MDXLayout.tsx b/apps/ui-playground/components/MDXLayout.tsx new file mode 100644 index 0000000000..260e668812 --- /dev/null +++ b/apps/ui-playground/components/MDXLayout.tsx @@ -0,0 +1,19 @@ +import { Toaster } from "react-hot-toast"; + +import { Status } from "./Status"; + +export function MDXLayout({ children, frontmatter }: { children: React.ReactNode; frontmatter: any }) { + return ( +
+ + {frontmatter.airtableId && ( + + )} + {children} +
+ ); +} diff --git a/apps/ui-playground/components/ui/BorderRadiusTable.tsx b/apps/ui-playground/components/ui/BorderRadiusTable.tsx new file mode 100644 index 0000000000..e22df848ab --- /dev/null +++ b/apps/ui-playground/components/ui/BorderRadiusTable.tsx @@ -0,0 +1,65 @@ +"use client"; + +import React from "react"; +import { Toaster } from "react-hot-toast"; + +import { showToast } from "@calcom/ui/components/toast"; + +interface BorderRadiusToken { + name: string; + value: string; + className: string; +} + +interface BorderRadiusTableProps { + tokens: BorderRadiusToken[]; +} + +const tokens: BorderRadiusToken[] = [ + { name: "None", value: "0px", className: "rounded-none" }, + { name: "Small", value: "0.125rem", className: "rounded-sm" }, + { name: "Default", value: "0.25rem", className: "rounded" }, + { name: "Medium", value: "0.375rem", className: "rounded-md" }, + { name: "Large", value: "0.5rem", className: "rounded-lg" }, + { name: "XLarge", value: "0.75rem", className: "rounded-xl" }, + { name: "2XLarge", value: "1rem", className: "rounded-2xl" }, + { name: "3XLarge", value: "1.5rem", className: "rounded-3xl" }, + { name: "Full", value: "9999px", className: "rounded-full" }, +]; + +export const BorderRadiusTable: React.FC = () => { + const handleCopy = async (value: string) => { + try { + await navigator.clipboard.writeText(value); + showToast(`Copied ${value}`, "success"); + } catch (error) { + console.error("Failed to copy:", error); + showToast("Failed to copy", "error"); + } + }; + + return ( + <> + +
+ {tokens.map((token) => ( +
handleCopy(token.className)} + className="border-subtle bg-default hover:bg-subtle group relative cursor-pointer overflow-hidden rounded-lg border p-4"> +
+
+
+

{token.name}

+

{token.className}

+
+

{token.value}

+
+
+
+
+ ))} +
+ + ); +}; diff --git a/apps/ui-playground/components/ui/IconGrid.tsx b/apps/ui-playground/components/ui/IconGrid.tsx new file mode 100644 index 0000000000..6bcdb153d0 --- /dev/null +++ b/apps/ui-playground/components/ui/IconGrid.tsx @@ -0,0 +1,371 @@ +"use client"; + +import React, { useState, useMemo } from "react"; +import { Toaster } from "react-hot-toast"; + +import { Icon } from "@calcom/ui/components/icon"; +import { showToast } from "@calcom/ui/components/toast"; + +type IconName = + | "activity" + | "arrow-down" + | "arrow-left" + | "arrow-right" + | "arrow-up-right" + | "arrow-up" + | "asterisk" + | "at-sign" + | "atom" + | "badge-check" + | "ban" + | "bell" + | "binary" + | "blocks" + | "bold" + | "book-open-check" + | "book-open" + | "book-user" + | "building" + | "calendar-check-2" + | "calendar-days" + | "calendar-heart" + | "calendar-range" + | "calendar-search" + | "calendar-x-2" + | "calendar" + | "chart-bar" + | "chart-line" + | "check-check" + | "check" + | "chevron-down" + | "chevron-left" + | "chevron-right" + | "chevron-up" + | "chevrons-down-up" + | "chevrons-left" + | "chevrons-right" + | "chevrons-up-down" + | "circle-alert" + | "circle-arrow-up" + | "circle-check-big" + | "circle-check" + | "circle-help" + | "circle-x" + | "circle" + | "clipboard-check" + | "clipboard" + | "clock" + | "code" + | "columns-3" + | "command" + | "contact" + | "copy" + | "corner-down-left" + | "corner-down-right" + | "credit-card" + | "disc" + | "dot" + | "download" + | "ellipsis-vertical" + | "ellipsis" + | "external-link" + | "eye-off" + | "eye" + | "file-down" + | "file-text" + | "file" + | "filter" + | "fingerprint" + | "flag" + | "folder" + | "gift" + | "git-merge" + | "github" + | "globe" + | "grid-3x3" + | "handshake" + | "info" + | "italic" + | "key" + | "layers" + | "layout-dashboard" + | "link-2" + | "link" + | "loader" + | "lock-open" + | "lock" + | "log-out" + | "mail-open" + | "mail" + | "map-pin" + | "map" + | "menu" + | "message-circle" + | "messages-square" + | "moon" + | "paintbrush" + | "paperclip" + | "pencil" + | "phone-call" + | "phone" + | "plus" + | "refresh-ccw" + | "refresh-cw" + | "repeat" + | "rocket" + | "rotate-ccw" + | "rotate-cw" + | "search" + | "send" + | "settings" + | "share-2" + | "shield-check" + | "shield" + | "shuffle" + | "sliders-horizontal" + | "sliders-vertical" + | "smartphone" + | "sparkles" + | "square-check" + | "star" + | "sun" + | "sunrise" + | "sunset" + | "tags" + | "terminal" + | "trash-2" + | "trash" + | "trello" + | "triangle-alert" + | "upload" + | "user-check" + | "user-plus" + | "user-x" + | "user" + | "users" + | "venetian-mask" + | "video" + | "waypoints" + | "webhook" + | "x" + | "zap"; + +interface IconGridProps { + className?: string; +} + +interface CopyMenuProps { + name: IconName; + onCopy: (format: string) => void; +} + +const CopyMenu: React.FC = ({ name, onCopy }) => { + return ( +
+
+ +
+
+ ); +}; + +export const IconGrid: React.FC = ({ className }) => { + const [searchQuery, setSearchQuery] = useState(""); + + const handleCopy = async (value: string) => { + try { + await navigator.clipboard.writeText(value); + showToast(`Copied ${value}`, "success"); + } catch (error) { + console.error("Failed to copy:", error); + showToast("Failed to copy", "error"); + } + }; + + const icons: IconName[] = [ + "activity", + "arrow-down", + "arrow-left", + "arrow-right", + "arrow-up-right", + "arrow-up", + "asterisk", + "at-sign", + "atom", + "badge-check", + "ban", + "bell", + "binary", + "blocks", + "bold", + "book-open-check", + "book-open", + "book-user", + "building", + "calendar-check-2", + "calendar-days", + "calendar-heart", + "calendar-range", + "calendar-search", + "calendar-x-2", + "calendar", + "chart-bar", + "chart-line", + "check-check", + "check", + "chevron-down", + "chevron-left", + "chevron-right", + "chevron-up", + "chevrons-down-up", + "chevrons-left", + "chevrons-right", + "chevrons-up-down", + "circle-alert", + "circle-arrow-up", + "circle-check-big", + "circle-check", + "circle-help", + "circle-x", + "circle", + "clipboard-check", + "clipboard", + "clock", + "code", + "columns-3", + "command", + "contact", + "copy", + "corner-down-left", + "corner-down-right", + "credit-card", + "disc", + "dot", + "download", + "ellipsis-vertical", + "ellipsis", + "external-link", + "eye-off", + "eye", + "file-down", + "file-text", + "file", + "filter", + "fingerprint", + "flag", + "folder", + "gift", + "git-merge", + "github", + "globe", + "grid-3x3", + "handshake", + "info", + "italic", + "key", + "layers", + "layout-dashboard", + "link-2", + "link", + "loader", + "lock-open", + "lock", + "log-out", + "mail-open", + "mail", + "map-pin", + "map", + "menu", + "message-circle", + "messages-square", + "moon", + "paintbrush", + "paperclip", + "pencil", + "phone-call", + "phone", + "plus", + "refresh-ccw", + "refresh-cw", + "repeat", + "rocket", + "rotate-ccw", + "rotate-cw", + "search", + "send", + "settings", + "share-2", + "shield-check", + "shield", + "shuffle", + "sliders-horizontal", + "sliders-vertical", + "smartphone", + "sparkles", + "square-check", + "star", + "sun", + "sunrise", + "sunset", + "tags", + "terminal", + "trash-2", + "trash", + "trello", + "triangle-alert", + "upload", + "user-check", + "user-plus", + "user-x", + "user", + "users", + "venetian-mask", + "video", + "waypoints", + "webhook", + "x", + "zap", + ]; + + const filteredIcons = useMemo(() => { + if (!searchQuery) return icons; + const query = searchQuery.toLowerCase(); + return icons.filter((icon) => icon.toLowerCase().includes(query)); + }, [searchQuery]); + + return ( + <> + +
+
+ setSearchQuery(e.target.value)} + className="border-subtle bg-default text-emphasis focus:ring-emphasis placeholder:text-subtle w-full rounded-md border px-4 py-2 focus:outline-none focus:ring-2" + /> +
+
+ {filteredIcons.map((iconName) => ( +
+
+
+ +
+

{iconName}

+
+ +
+ ))} +
+
+ + ); +}; diff --git a/apps/ui-playground/components/ui/ShadowTable.tsx b/apps/ui-playground/components/ui/ShadowTable.tsx new file mode 100644 index 0000000000..4443fd0ac0 --- /dev/null +++ b/apps/ui-playground/components/ui/ShadowTable.tsx @@ -0,0 +1,126 @@ +"use client"; + +import React from "react"; +import { Toaster } from "react-hot-toast"; + +import { showToast } from "@calcom/ui/components/toast"; + +interface ShadowToken { + name: string; + className: string; + description: string; +} + +interface ShadowTableProps { + tokens: ShadowToken[]; +} + +const tokens: ShadowToken[] = [ + { + name: "Dropdown", + className: "shadow-dropdown", + description: "Used for dropdown menus and popovers", + }, + { + name: "Solid Gray Rested", + className: "shadow-solid-gray-rested", + description: "Default state for solid gray buttons", + }, + { + name: "Solid Gray Hover", + className: "shadow-solid-gray-hover", + description: "Hover state for solid gray buttons", + }, + { + name: "Solid Gray Active", + className: "shadow-solid-gray-active", + description: "Active/pressed state for solid gray buttons", + }, + { + name: "Outline Gray Rested", + className: "shadow-outline-gray-rested", + description: "Default state for outline gray buttons", + }, + { + name: "Outline Gray Hover", + className: "shadow-outline-gray-hover", + description: "Hover state for outline gray buttons", + }, + { + name: "Outline Gray Active", + className: "shadow-outline-gray-active", + description: "Active/pressed state for outline gray buttons", + }, + { + name: "Outline Red Rested", + className: "shadow-outline-red-rested", + description: "Default state for outline red buttons", + }, + { + name: "Outline Red Hover", + className: "shadow-outline-red-hover", + description: "Hover state for outline red buttons", + }, + { + name: "Outline Red Active", + className: "shadow-outline-red-active", + description: "Active/pressed state for outline red buttons", + }, + { + name: "Elevation Low", + className: "shadow-elevation-low", + description: "Subtle elevation for UI elements", + }, + { + name: "Button Solid Brand Default", + className: "shadow-button-solid-brand-default", + description: "Default state for solid brand buttons", + }, + { + name: "Button Solid Brand Hover", + className: "shadow-button-solid-brand-hover", + description: "Hover state for solid brand buttons", + }, + { + name: "Button Solid Brand Active", + className: "shadow-button-solid-brand-active", + description: "Active/pressed state for solid brand buttons", + }, +]; + +export const ShadowTable: React.FC = () => { + const handleCopy = async (value: string) => { + try { + await navigator.clipboard.writeText(value); + showToast(`Copied ${value}`, "success"); + } catch (error) { + console.error("Failed to copy:", error); + showToast("Failed to copy", "error"); + } + }; + + return ( + <> + +
+ {tokens.map((token) => ( +
handleCopy(token.className)} + className="group relative cursor-pointer"> +
+
+
+

{token.name}

+

{token.className}

+
+

{token.description}

+
+
+
+ ))} +
+ + ); +}; diff --git a/apps/ui-playground/components/ui/SpacingTable.tsx b/apps/ui-playground/components/ui/SpacingTable.tsx new file mode 100644 index 0000000000..6d8f296a63 --- /dev/null +++ b/apps/ui-playground/components/ui/SpacingTable.tsx @@ -0,0 +1,45 @@ +"use client"; + +import React from "react"; +import { Toaster } from "react-hot-toast"; + +interface SpacingToken { + name: string; + value: string; + pixels: string; +} + +interface SpacingTableProps { + tokens: SpacingToken[]; +} + +export const SpacingTable: React.FC = ({ tokens }) => { + return ( + <> + +
+
+ {tokens.map((token, index) => ( +
+
+
+

+ {token.name === "px" ? "px" : `${token.name} (${token.pixels})`} +

+
+
+
+
+
+
+ ))} +
+
+ + ); +}; diff --git a/apps/ui-playground/components/ui/TypeTable.tsx b/apps/ui-playground/components/ui/TypeTable.tsx new file mode 100644 index 0000000000..f9293e50a8 --- /dev/null +++ b/apps/ui-playground/components/ui/TypeTable.tsx @@ -0,0 +1,103 @@ +"use client"; + +import { getColorFormats, copyToClipboard } from "@/lib/colorUtils"; +import React from "react"; +import { Toaster } from "react-hot-toast"; + +import { showToast } from "@calcom/ui/components/toast"; + +interface ColorSwatch { + name: string; + value: string; + className: string; +} + +interface ColorSection { + title: string; + description?: string; + colors: ColorSwatch[]; +} + +interface TypeTableProps { + sections: ColorSection[]; +} + +interface CopyMenuProps { + color: string; + className: string; + onCopy: (format: string) => void; +} + +const CopyMenu: React.FC = ({ color, className, onCopy }) => { + const formats = getColorFormats(color, className); + if (!formats) return null; + + return ( +
+
+ + + + +
+
+ ); +}; + +export const TypeTable: React.FC = ({ sections }) => { + const handleCopy = async (colorValue: string) => { + try { + await copyToClipboard(colorValue); + showToast(`Copied ${colorValue}`, "success"); + } catch (error) { + console.error("Failed to copy color:", error); + showToast("Failed to copy color", "error"); + } + }; + + return ( + <> + +
+ {sections.map((section, sectionIndex) => ( +
+
+

{section.title}

+ {section.description &&

{section.description}

} +
+
+ {section.colors.map((color, colorIndex) => ( +
+
+
+ +
+
+

{color.name}

+

{color.value}

+
+
+ ))} +
+
+ ))} +
+ + ); +}; diff --git a/apps/ui-playground/components/ui/TypographyTable.tsx b/apps/ui-playground/components/ui/TypographyTable.tsx new file mode 100644 index 0000000000..15b6f5a456 --- /dev/null +++ b/apps/ui-playground/components/ui/TypographyTable.tsx @@ -0,0 +1,109 @@ +"use client"; + +import React from "react"; +import { Toaster } from "react-hot-toast"; + +import { showToast } from "@calcom/ui/components/toast"; + +interface TypographyStyle { + name: string; + className: string; + specs: { + fontFamily: string; + fontSize: string; + lineHeight: string; + weight: string; + }; + category?: string; +} + +interface TypographySection { + title: string; + description?: string; + styles: TypographyStyle[]; +} + +interface TypographyTableProps { + sections: TypographySection[]; +} + +interface CopyMenuProps { + style: TypographyStyle; + onCopy: (format: string) => void; +} + +const CopyMenu: React.FC = ({ style, onCopy }) => { + return ( +
+
+ + +
+
+ ); +}; + +export const TypographyTable: React.FC = ({ sections }) => { + const handleCopy = async (value: string) => { + try { + await navigator.clipboard.writeText(value); + showToast(`Copied to clipboard`, "success"); + } catch (error) { + console.error("Failed to copy:", error); + showToast("Failed to copy", "error"); + } + }; + + return ( + <> + +
+ {sections.map((section, sectionIndex) => ( +
+
+

{section.title}

+ {section.description &&

{section.description}

} +
+
+ {section.styles.map((style, styleIndex) => ( +
+
+
+
+

{style.name}

+

{style.className}

+
+
The quick brown fox jumps over the lazy dog
+
+
+

Font: {style.specs.fontFamily}

+

Size: {style.specs.fontSize}px

+

Line Height: {style.specs.lineHeight}

+

Weight: {style.specs.weight}

+
+
+ +
+ ))} +
+
+ ))} +
+ + ); +}; diff --git a/apps/ui-playground/content/design/border-radius.mdx b/apps/ui-playground/content/design/border-radius.mdx new file mode 100644 index 0000000000..804233da3f --- /dev/null +++ b/apps/ui-playground/content/design/border-radius.mdx @@ -0,0 +1,32 @@ +--- +title: Border Radius +description: Cal.com's border radius system +--- + +import { BorderRadiusTable } from '@/components/ui/BorderRadiusTable'; + +# Border Radius + +Our design system uses a consistent set of border radius values to create a cohesive and modern interface. The border radius scale ranges from none (sharp corners) to full (completely rounded). + +## Usage + +```tsx +// No border radius +
Sharp corners
+ +// Default radius +
Default rounded corners
+ +// Larger radius +
Larger rounded corners
+ +// Fully rounded (useful for pills, badges) +
Completely rounded
+``` + +## Border Radius Scale + +Click on any radius to copy its Tailwind class name. Each preview shows how the radius appears when applied. + + diff --git a/apps/ui-playground/content/design/colors.mdx b/apps/ui-playground/content/design/colors.mdx new file mode 100644 index 0000000000..d751b7f121 --- /dev/null +++ b/apps/ui-playground/content/design/colors.mdx @@ -0,0 +1,144 @@ +--- +title: Colors +description: Cal.com Color System Documentation +--- + +import { TypeTable } from '@/components/ui/TypeTable'; + +export const backgroundTokens = { + title: 'Background Tokens', + colors: [ + { name: 'emphasis', value: 'var(--cal-bg-emphasis)', className: 'bg-emphasis' }, + { name: 'default', value: 'var(--cal-bg, white)', className: 'bg-default' }, + { name: 'subtle', value: 'var(--cal-bg-subtle)', className: 'bg-subtle' }, + { name: 'muted', value: 'var(--cal-bg-muted)', className: 'bg-muted' }, + { name: 'inverted', value: 'var(--cal-bg-inverted)', className: 'bg-inverted' }, + ], +}; + +export const primaryBackgroundTokens = { + title: 'Primary Background Tokens', + colors: [ + { name: 'primary.default', value: 'var(--cal-bg-primary)', className: 'bg-primary-default' }, + { name: 'primary.emphasis', value: 'var(--cal-bg-primary-emphasis)', className: 'bg-primary-emphasis' }, + { name: 'primary.muted', value: 'var(--cal-bg-primary-muted)', className: 'bg-primary-muted' }, + ], +}; + +export const brandTokens = { + title: 'Brand Colors', + colors: [ + { name: 'brand.default', value: 'var(--cal-brand,#111827)', className: 'bg-brand-default' }, + { name: 'brand.emphasis', value: 'var(--cal-brand-emphasis,#101010)', className: 'bg-brand-emphasis' }, + { name: 'brand.subtle', value: 'var(--cal-brand-subtle,#9CA3AF)', className: 'bg-brand-subtle' }, + { name: 'brand.accent', value: 'var(--cal-brand-accent,white)', className: 'bg-brand-accent' }, + ], +}; + +export const semanticBackgroundTokens = { + title: 'Semantic Background Tokens', + colors: [ + { name: 'semantic.info.subtle', value: 'var(--cal-bg-semantic-info-subtle)', className: 'bg-semantic-info-subtle' }, + { name: 'semantic.info.emphasis', value: 'var(--cal-bg-semantic-info-emphasis)', className: 'bg-semantic-info-emphasis' }, + { name: 'semantic.success.subtle', value: 'var(--cal-bg-semantic-success-subtle)', className: 'bg-semantic-success-subtle' }, + { name: 'semantic.success.emphasis', value: 'var(--cal-bg-semantic-success-emphasis)', className: 'bg-semantic-success-emphasis' }, + { name: 'semantic.attention.subtle', value: 'var(--cal-bg-semantic-attention-subtle)', className: 'bg-semantic-attention-subtle' }, + { name: 'semantic.attention.emphasis', value: 'var(--cal-bg-semantic-attention-emphasis)', className: 'bg-semantic-attention-emphasis' }, + { name: 'semantic.error.subtle', value: 'var(--cal-bg-semantic-error-subtle)', className: 'bg-semantic-error-subtle' }, + { name: 'semantic.error.emphasis', value: 'var(--cal-bg-semantic-error-emphasis)', className: 'bg-semantic-error-emphasis' }, + ], +}; + +export const borderTokens = { + title: 'Border Tokens', + colors: [ + { name: 'border.emphasis', value: 'var(--cal-border-emphasis)', className: 'border-emphasis' }, + { name: 'border.default', value: 'var(--cal-border)', className: 'border-default' }, + { name: 'border.subtle', value: 'var(--cal-border-subtle)', className: 'border-subtle' }, + { name: 'border.muted', value: 'var(--cal-border-muted)', className: 'border-muted' }, + { name: 'border.semantic.error', value: 'var(--cal-border-semantic-error)', className: 'border-semantic-error' }, + ], +}; + +export const textTokens = { + title: 'Text Tokens', + description: 'Semantic text colors for various purposes', + colors: [ + { name: 'text.emphasis', value: 'var(--cal-text-emphasis)', className: 'text-emphasis' }, + { name: 'text.default', value: 'var(--cal-text)', className: 'text-default' }, + { name: 'text.subtle', value: 'var(--cal-text-subtle)', className: 'text-subtle' }, + { name: 'text.muted', value: 'var(--cal-text-muted)', className: 'text-muted' }, + { name: 'text.inverted', value: 'var(--cal-text-inverted)', className: 'text-inverted' }, + ], +}; + +# Color System + +Our color system is built on semantic tokens that provide consistent theming across light and dark modes. The system is organized into five main categories: + +1. Background Colors +2. Primary Colors +3. Brand Colors +4. Semantic Colors +5. Text & Border Colors + +## Usage Guidelines + +1. **Use Semantic Tokens**: Always prefer semantic tokens over direct color values to maintain consistency and theme support. +2. **Dark Mode Support**: Our semantic tokens automatically handle dark mode transitions. +3. **Component Design**: When creating new components, follow the semantic token system: + - Use `bg-*` tokens for backgrounds + - Use `text-*` tokens for text content + - Use `border-*` tokens for borders +4. **Semantic States**: Use semantic color variations to convey meaning: + - Info: For informational UI elements + - Success: For successful actions and positive states + - Attention: For warnings and important notices + - Error: For error states and destructive actions +5. **Brand Colors**: Use brand colors sparingly and primarily for: + - Primary CTAs + - Brand identity elements + - Key interactive components + + +## Using Semantic Tokens + +Our color tokens are CSS variables that automatically handle light and dark mode transitions. Here's how to use them: + +### Background Colors +```tsx +// Basic backgrounds +
Default background
+
Subtle background
+
Emphasis background
+ +// Primary backgrounds +
Primary background
+
Primary emphasis
+``` + +### Semantic Colors +```tsx +// Info states +
Info subtle
+
Info emphasis
+ +// Success states +
Success subtle
+
Success emphasis
+ +// Error states +
Error subtle
+
Error emphasis
+``` + +### Text Colors +```tsx +

High emphasis text

+

Default text

+

Subtle text

+

Muted text

+``` + + + diff --git a/apps/ui-playground/content/design/components/alert.actions.tsx b/apps/ui-playground/content/design/components/alert.actions.tsx new file mode 100644 index 0000000000..28e3877c8a --- /dev/null +++ b/apps/ui-playground/content/design/components/alert.actions.tsx @@ -0,0 +1,28 @@ +"use client"; + +import { RenderComponentWithSnippet } from "@/app/components/render"; + +import { Alert } from "@calcom/ui/components/alert"; + +const severities = ["neutral", "info", "warning", "error"] as const; + +export const ActionsExample: React.FC = () => ( + +
+ {severities.map((severity) => ( + + + +
+ } + /> + ))} +
+ +); diff --git a/apps/ui-playground/content/design/components/alert.basic.tsx b/apps/ui-playground/content/design/components/alert.basic.tsx new file mode 100644 index 0000000000..b42cffd50b --- /dev/null +++ b/apps/ui-playground/content/design/components/alert.basic.tsx @@ -0,0 +1,22 @@ +"use client"; + +import { RenderComponentWithSnippet } from "@/app/components/render"; + +import { Alert } from "@calcom/ui/components/alert"; + +const severities = ["neutral", "info", "warning", "error"] as const; + +export const BasicExample: React.FC = () => ( + +
+ {severities.map((severity) => ( + + ))} +
+
+); diff --git a/apps/ui-playground/content/design/components/alert.customIconColor.tsx b/apps/ui-playground/content/design/components/alert.customIconColor.tsx new file mode 100644 index 0000000000..efddf9c22b --- /dev/null +++ b/apps/ui-playground/content/design/components/alert.customIconColor.tsx @@ -0,0 +1,24 @@ +"use client"; + +import { RenderComponentWithSnippet } from "@/app/components/render"; + +import { Alert } from "@calcom/ui/components/alert"; + +const severities = ["neutral", "info", "warning", "error"] as const; + +export const CustomIconColorExample: React.FC = () => ( + +
+ {severities.map((severity) => ( + + ))} +
+
+); diff --git a/apps/ui-playground/content/design/components/alert.customIcons.tsx b/apps/ui-playground/content/design/components/alert.customIcons.tsx new file mode 100644 index 0000000000..1819b0dce3 --- /dev/null +++ b/apps/ui-playground/content/design/components/alert.customIcons.tsx @@ -0,0 +1,25 @@ +"use client"; + +import { RenderComponentWithSnippet } from "@/app/components/render"; + +import { Alert } from "@calcom/ui/components/alert"; + +const severities = ["neutral", "info", "warning", "error"] as const; +const customIcons = ["info", "alert-triangle", "circle-x", "circle-check", "bell"] as const; + +export const CustomIconsExample: React.FC = () => ( + +
+ {severities.map((severity, index) => ( + + ))} +
+
+); diff --git a/apps/ui-playground/content/design/components/alert.mdx b/apps/ui-playground/content/design/components/alert.mdx new file mode 100644 index 0000000000..b5956e59da --- /dev/null +++ b/apps/ui-playground/content/design/components/alert.mdx @@ -0,0 +1,42 @@ +--- +title: Alert +airtableId: recnvwaXqOucqLo9Y +--- +import { Alert } from "@calcom/ui/components/alert" +import { RenderComponentWithSnippet } from "@/app/components/render" +import { Row } from "@/app/components/row" +import { BasicExample } from "./alert.basic" +import { ActionsExample } from "./alert.actions" +import { CustomIconsExample } from "./alert.customIcons" +import { CustomIconColorExample } from "./alert.customIconColor" +import { NoTitleExample } from "./alert.noTitle" + +## Basic Alerts + +Alerts come in different severities to convey different levels of importance. + + + +## Alerts with Actions + +Alerts can include action buttons to provide users with immediate options. + + + +## Custom Icons + +You can customize the alert icon to better match your message context. + + + +## Custom Icon Colors + +The icon color can be customized independently of the alert severity. + + + +## Without Title + +For simpler messages, alerts can be displayed without a title. + + diff --git a/apps/ui-playground/content/design/components/alert.noTitle.tsx b/apps/ui-playground/content/design/components/alert.noTitle.tsx new file mode 100644 index 0000000000..b7229d28eb --- /dev/null +++ b/apps/ui-playground/content/design/components/alert.noTitle.tsx @@ -0,0 +1,21 @@ +"use client"; + +import { RenderComponentWithSnippet } from "@/app/components/render"; + +import { Alert } from "@calcom/ui/components/alert"; + +const severities = ["neutral", "info", "warning", "error"] as const; + +export const NoTitleExample: React.FC = () => ( + +
+ {severities.map((severity) => ( + + ))} +
+
+); diff --git a/apps/ui-playground/content/design/components/avatar.image.tsx b/apps/ui-playground/content/design/components/avatar.image.tsx new file mode 100644 index 0000000000..2c20ec8e52 --- /dev/null +++ b/apps/ui-playground/content/design/components/avatar.image.tsx @@ -0,0 +1,22 @@ +"use client"; + +import { RenderComponentWithSnippet } from "@/app/components/render"; + +import { Avatar } from "@calcom/ui/components/avatar"; + +const sampleImage = "https://cal.com/stakeholder/peer.jpg"; + +export const ImageExample: React.FC = () => ( + +
+
+ + With Image +
+
+ + Without Image +
+
+
+); diff --git a/apps/ui-playground/content/design/components/avatar.indicator.tsx b/apps/ui-playground/content/design/components/avatar.indicator.tsx new file mode 100644 index 0000000000..25f41bad28 --- /dev/null +++ b/apps/ui-playground/content/design/components/avatar.indicator.tsx @@ -0,0 +1,25 @@ +"use client"; + +import { RenderComponentWithSnippet } from "@/app/components/render"; + +import { Avatar } from "@calcom/ui/components/avatar"; + +const sampleImage = "https://cal.com/stakeholder/peer.jpg"; + +export const IndicatorExample: React.FC = () => ( + +
+
+ + } + /> + With Status Indicator +
+
+
+); diff --git a/apps/ui-playground/content/design/components/avatar.link.tsx b/apps/ui-playground/content/design/components/avatar.link.tsx new file mode 100644 index 0000000000..61fc4ecf9e --- /dev/null +++ b/apps/ui-playground/content/design/components/avatar.link.tsx @@ -0,0 +1,18 @@ +"use client"; + +import { RenderComponentWithSnippet } from "@/app/components/render"; + +import { Avatar } from "@calcom/ui/components/avatar"; + +const sampleImage = "https://cal.com/stakeholder/peer.jpg"; + +export const LinkExample: React.FC = () => ( + +
+
+ + Clickable +
+
+
+); diff --git a/apps/ui-playground/content/design/components/avatar.mdx b/apps/ui-playground/content/design/components/avatar.mdx new file mode 100644 index 0000000000..fe8b701693 --- /dev/null +++ b/apps/ui-playground/content/design/components/avatar.mdx @@ -0,0 +1,42 @@ +--- +title: Avatar +airtableId: reciflFrJo5cVgGrS +--- +import { Avatar } from "@calcom/ui/components/avatar" +import { RenderComponentWithSnippet } from "@/app/components/render" +import { Row } from "@/app/components/row" +import { SizesExample } from "./avatar.sizes" +import { ImageExample } from "./avatar.image" +import { TooltipExample } from "./avatar.tooltip" +import { LinkExample } from "./avatar.link" +import { IndicatorExample } from "./avatar.indicator" + +## Size Variations + +Avatars come in different sizes to suit various use cases. + + + +## With and Without Image + +Avatars can display either an image or a fallback placeholder. + + + +## With Tooltip + +Avatars can show additional information on hover using tooltips. + + + +## With Link + +Avatars can be made clickable by providing an href. + + + +## With Indicator + +Avatars can include status indicators or badges. + + diff --git a/apps/ui-playground/content/design/components/avatar.sizes.tsx b/apps/ui-playground/content/design/components/avatar.sizes.tsx new file mode 100644 index 0000000000..078a626e7b --- /dev/null +++ b/apps/ui-playground/content/design/components/avatar.sizes.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { RenderComponentWithSnippet } from "@/app/components/render"; + +import { Avatar } from "@calcom/ui/components/avatar"; + +const sizes = ["xs", "xsm", "sm", "md", "mdLg", "lg", "xl"] as const; +const sampleImage = "https://cal.com/stakeholder/peer.jpg"; + +export const SizesExample: React.FC = () => ( + +
+ {sizes.map((size) => ( +
+ + {size} +
+ ))} +
+
+ {sizes.map((size) => ( +
+ + {size} +
+ ))} +
+
+); diff --git a/apps/ui-playground/content/design/components/avatar.tooltip.tsx b/apps/ui-playground/content/design/components/avatar.tooltip.tsx new file mode 100644 index 0000000000..341d5a06b4 --- /dev/null +++ b/apps/ui-playground/content/design/components/avatar.tooltip.tsx @@ -0,0 +1,18 @@ +"use client"; + +import { RenderComponentWithSnippet } from "@/app/components/render"; + +import { Avatar } from "@calcom/ui/components/avatar"; + +const sampleImage = "https://cal.com/stakeholder/peer.jpg"; + +export const TooltipExample: React.FC = () => ( + +
+
+ + Hover to see tooltip +
+
+
+); diff --git a/apps/ui-playground/content/design/components/badge.dots.tsx b/apps/ui-playground/content/design/components/badge.dots.tsx new file mode 100644 index 0000000000..7f39362a50 --- /dev/null +++ b/apps/ui-playground/content/design/components/badge.dots.tsx @@ -0,0 +1,33 @@ +"use client"; + +import { RenderComponentWithSnippet } from "@/app/components/render"; + +import { Badge } from "@calcom/ui/components/badge"; + +const variants = [ + "default", + "warning", + "orange", + "success", + "green", + "gray", + "blue", + "red", + "error", + "grayWithoutHover", +] as const; + +export const DotsExample: React.FC = () => ( + +
+ {variants.map((variant) => ( +
+ + {variant} + + Dot +
+ ))} +
+
+); diff --git a/apps/ui-playground/content/design/components/badge.icons.tsx b/apps/ui-playground/content/design/components/badge.icons.tsx new file mode 100644 index 0000000000..de3fe76340 --- /dev/null +++ b/apps/ui-playground/content/design/components/badge.icons.tsx @@ -0,0 +1,33 @@ +"use client"; + +import { RenderComponentWithSnippet } from "@/app/components/render"; + +import { Badge } from "@calcom/ui/components/badge"; + +const variants = [ + "default", + "warning", + "orange", + "success", + "green", + "gray", + "blue", + "red", + "error", + "grayWithoutHover", +] as const; + +export const IconsExample: React.FC = () => ( + +
+ {variants.map((variant) => ( +
+ + {variant} + + Icon +
+ ))} +
+
+); diff --git a/apps/ui-playground/content/design/components/badge.interactive.tsx b/apps/ui-playground/content/design/components/badge.interactive.tsx new file mode 100644 index 0000000000..03d94ec368 --- /dev/null +++ b/apps/ui-playground/content/design/components/badge.interactive.tsx @@ -0,0 +1,33 @@ +"use client"; + +import { RenderComponentWithSnippet } from "@/app/components/render"; + +import { Badge } from "@calcom/ui/components/badge"; + +const variants = [ + "default", + "warning", + "orange", + "success", + "green", + "gray", + "blue", + "red", + "error", + "grayWithoutHover", +] as const; + +export const InteractiveExample: React.FC = () => ( + +
+ {variants.map((variant) => ( +
+ alert(`${variant} badge clicked!`)}> + Click me + + Clickable +
+ ))} +
+
+); diff --git a/apps/ui-playground/content/design/components/badge.mdx b/apps/ui-playground/content/design/components/badge.mdx new file mode 100644 index 0000000000..6b6ca51ff5 --- /dev/null +++ b/apps/ui-playground/content/design/components/badge.mdx @@ -0,0 +1,42 @@ +--- +title: Badge +airtableId: recso7QMfD2xwI04K +--- +import { Badge } from "@calcom/ui/components/badge" +import { RenderComponentWithSnippet } from "@/app/components/render" +import { Row } from "@/app/components/row" +import { VariantsExample } from "./badge.variants" +import { IconsExample } from "./badge.icons" +import { DotsExample } from "./badge.dots" +import { InteractiveExample } from "./badge.interactive" +import { RoundedExample } from "./badge.rounded" + +## Variants and Sizes + +Badges come in different variants and sizes to suit various use cases. + + + +## With Icons + +Badges can include icons to provide additional visual context. + + + +## With Dots + +Badges can include dots, useful for showing status or notifications. + + + +## Interactive + +Badges can be made interactive with click handlers. + + + +## Rounded + +Badges can be fully rounded, useful for numerical indicators. + + diff --git a/apps/ui-playground/content/design/components/badge.rounded.tsx b/apps/ui-playground/content/design/components/badge.rounded.tsx new file mode 100644 index 0000000000..e31a1c2e47 --- /dev/null +++ b/apps/ui-playground/content/design/components/badge.rounded.tsx @@ -0,0 +1,33 @@ +"use client"; + +import { RenderComponentWithSnippet } from "@/app/components/render"; + +import { Badge } from "@calcom/ui/components/badge"; + +const variants = [ + "default", + "warning", + "orange", + "success", + "green", + "gray", + "blue", + "red", + "error", + "grayWithoutHover", +] as const; + +export const RoundedExample: React.FC = () => ( + +
+ {variants.map((variant) => ( +
+ + 1 + + {variant} +
+ ))} +
+
+); diff --git a/apps/ui-playground/content/design/components/badge.variants.tsx b/apps/ui-playground/content/design/components/badge.variants.tsx new file mode 100644 index 0000000000..b3e80a386a --- /dev/null +++ b/apps/ui-playground/content/design/components/badge.variants.tsx @@ -0,0 +1,42 @@ +"use client"; + +import { RenderComponentWithSnippet } from "@/app/components/render"; + +import { Badge } from "@calcom/ui/components/badge"; + +const variants = [ + "default", + "warning", + "orange", + "success", + "green", + "gray", + "blue", + "red", + "error", + "grayWithoutHover", +] as const; + +const sizes = ["sm", "md", "lg"] as const; + +export const VariantsExample: React.FC = () => ( + +
+ {variants.map((variant) => ( +
+

{variant}

+
+ {sizes.map((size) => ( +
+ + {variant} + + {size} +
+ ))} +
+
+ ))} +
+
+); diff --git a/apps/ui-playground/content/design/components/button.buttongroup.tsx b/apps/ui-playground/content/design/components/button.buttongroup.tsx new file mode 100644 index 0000000000..2982634791 --- /dev/null +++ b/apps/ui-playground/content/design/components/button.buttongroup.tsx @@ -0,0 +1,63 @@ +import { RenderComponentWithSnippet } from "@/app/components/render"; + +import { Button } from "@calcom/ui/components/button"; +import { ButtonGroup } from "@calcom/ui/components/buttonGroup"; + +export const ButtonGroupExample = () => { + return ( + + {/* Basic Button Group */} + + + Disabled +
+ ))} + +
+ +
+

Disabled with Icons

+ + {colors.map((color) => ( +
+ + With Icon +
+ ))} +
+
+ + +); diff --git a/apps/ui-playground/content/design/components/button.icons.tsx b/apps/ui-playground/content/design/components/button.icons.tsx new file mode 100644 index 0000000000..1c1b24c1e3 --- /dev/null +++ b/apps/ui-playground/content/design/components/button.icons.tsx @@ -0,0 +1,42 @@ +"use client"; + +import { RenderComponentWithSnippet } from "@/app/components/render"; +import { Row } from "@/app/components/row"; + +import { Button } from "@calcom/ui/components/button"; + +const colors = ["primary", "secondary", "minimal", "destructive"] as const; + +export const IconsExample: React.FC = () => ( + +
+
+

Start Icon

+ + {colors.map((color) => ( +
+ + Start Icon +
+ ))} +
+
+ +
+

End Icon

+ + {colors.map((color) => ( +
+ + End Icon +
+ ))} +
+
+
+
+); diff --git a/apps/ui-playground/content/design/components/button.loading.tsx b/apps/ui-playground/content/design/components/button.loading.tsx new file mode 100644 index 0000000000..4a5b33c34e --- /dev/null +++ b/apps/ui-playground/content/design/components/button.loading.tsx @@ -0,0 +1,44 @@ +"use client"; + +import { RenderComponentWithSnippet } from "@/app/components/render"; +import { Row } from "@/app/components/row"; + +import { Button } from "@calcom/ui/components/button"; + +const colors = ["primary", "secondary", "minimal", "destructive"] as const; + +export const LoadingExample: React.FC = () => { + return ( + +
+
+

Loading State

+ + {colors.map((color) => ( +
+ + Loading +
+ ))} +
+
+ +
+

Loading with Icons

+ + {colors.map((color) => ( +
+ + With Icon +
+ ))} +
+
+
+
+ ); +}; diff --git a/apps/ui-playground/content/design/components/button.mdx b/apps/ui-playground/content/design/components/button.mdx new file mode 100644 index 0000000000..b4c8cb5d3b --- /dev/null +++ b/apps/ui-playground/content/design/components/button.mdx @@ -0,0 +1,67 @@ +--- +title: Button +airtableId: recEJyzUeMMXCjyTB +--- +import { Button } from "@calcom/ui/components/button" +import { RenderComponentWithSnippet } from "@/app/components/render" +import { Row } from "@/app/components/row" +import { OnClickExample } from "./button.onClick" +import { VariantExample } from "./button.variant" +import { IconsExample } from "./button.icons" +import { LoadingExample } from "./button.loading" +import { DisabledExample } from "./button.disabled" +import { ButtonGroupExample,ButtonGroupSizes,ButtonGroupSizesCombined} from "./button.buttongroup" + +## Variants and Colors + +Buttons come in different variants and colors to suit different purposes. Each variant can be combined with any color and size. + + + +## Icons + +Buttons can include icons at the start or end. The icon color and size are automatically handled by the button component. + + + +## Loading State + +Buttons can show a loading state to indicate an ongoing action. The loading spinner replaces the button text while maintaining the button's width. + + + +## Disabled State + +Buttons can be disabled to indicate that they are not interactive. The disabled state is visually distinct and prevents any click events. + + + +## onClick Events + +Buttons handle click events like any other button component. You can provide an onClick handler to respond to user interactions. + + + +Providing a loading state will add a spinner on the left side of the button. + + + +## Disabled + + + +## Button Groups + +Buttons can be grouped together using the ButtonGroup component. The ButtonGroup component can be used to create a group of related buttons with consistent spacing and optional combined borders. + + + +### Button Group Sizes + +#### Seperated + + + +#### Combined + + diff --git a/apps/ui-playground/content/design/components/button.onClick.tsx b/apps/ui-playground/content/design/components/button.onClick.tsx new file mode 100644 index 0000000000..35f1418cd9 --- /dev/null +++ b/apps/ui-playground/content/design/components/button.onClick.tsx @@ -0,0 +1,14 @@ +"use client"; + +import { RenderComponentWithSnippet } from "@/app/components/render"; +import { Row } from "@/app/components/row"; + +import { Button } from "@calcom/ui/components/button"; + +export const OnClickExample: React.FC = () => ( + + + + + +); diff --git a/apps/ui-playground/content/design/components/button.variant.tsx b/apps/ui-playground/content/design/components/button.variant.tsx new file mode 100644 index 0000000000..0a8d72ebfe --- /dev/null +++ b/apps/ui-playground/content/design/components/button.variant.tsx @@ -0,0 +1,43 @@ +"use client"; + +import { RenderComponentWithSnippet } from "@/app/components/render"; +import { Row } from "@/app/components/row"; + +import { Button } from "@calcom/ui/components/button"; + +const variants = ["button", "icon"] as const; +const colors = ["primary", "secondary", "minimal", "destructive"] as const; +const sizes = ["xs", "sm", "base", "lg"] as const; + +export const VariantExample: React.FC = () => ( + +
+ {variants.map((variant) => ( +
+

{variant === "button" ? "Default" : variant}

+
+ {colors.map((color) => ( +
+

{color}

+ + {sizes.map((size) => ( +
+ + {size} +
+ ))} +
+
+ ))} +
+
+ ))} +
+
+); diff --git a/apps/ui-playground/content/design/components/checkbox.basic.tsx b/apps/ui-playground/content/design/components/checkbox.basic.tsx new file mode 100644 index 0000000000..e8f353f9ce --- /dev/null +++ b/apps/ui-playground/content/design/components/checkbox.basic.tsx @@ -0,0 +1,31 @@ +"use client"; + +import { RenderComponentWithSnippet } from "@/app/components/render"; + +import { Checkbox } from "@calcom/ui/components/form"; + +export const BasicExample: React.FC = () => ( + +
+
+ + Unchecked +
+ +
+ + Checked +
+ +
+ + Disabled +
+ +
+ + Disabled Checked +
+
+
+); diff --git a/apps/ui-playground/content/design/components/checkbox.description.tsx b/apps/ui-playground/content/design/components/checkbox.description.tsx new file mode 100644 index 0000000000..8ae2473bcf --- /dev/null +++ b/apps/ui-playground/content/design/components/checkbox.description.tsx @@ -0,0 +1,15 @@ +"use client"; + +import { RenderComponentWithSnippet } from "@/app/components/render"; + +import { CheckboxField } from "@calcom/ui/components/form"; + +export const DescriptionExample: React.FC = () => ( + +
+ + + +
+
+); diff --git a/apps/ui-playground/content/design/components/checkbox.infoIcon.tsx b/apps/ui-playground/content/design/components/checkbox.infoIcon.tsx new file mode 100644 index 0000000000..32e9cdcdc5 --- /dev/null +++ b/apps/ui-playground/content/design/components/checkbox.infoIcon.tsx @@ -0,0 +1,16 @@ +"use client"; + +import { RenderComponentWithSnippet } from "@/app/components/render"; + +import { CheckboxField } from "@calcom/ui/components/form"; + +export const InfoIconExample: React.FC = () => ( + + + +); diff --git a/apps/ui-playground/content/design/components/checkbox.labelPosition.tsx b/apps/ui-playground/content/design/components/checkbox.labelPosition.tsx new file mode 100644 index 0000000000..66561cfce2 --- /dev/null +++ b/apps/ui-playground/content/design/components/checkbox.labelPosition.tsx @@ -0,0 +1,24 @@ +"use client"; + +import { RenderComponentWithSnippet } from "@/app/components/render"; + +import { CheckboxField } from "@calcom/ui/components/form"; + +export const LabelPositionExample: React.FC = () => ( + +
+ +
+ +
+
+
+); diff --git a/apps/ui-playground/content/design/components/checkbox.mdx b/apps/ui-playground/content/design/components/checkbox.mdx new file mode 100644 index 0000000000..fc71982bba --- /dev/null +++ b/apps/ui-playground/content/design/components/checkbox.mdx @@ -0,0 +1,35 @@ +--- +title: Checkbox +airtableId: recXGPNAfDJVm8NHo +--- +import { Checkbox } from "@calcom/ui/components/form" +import { RenderComponentWithSnippet } from "@/app/components/render" +import { Row } from "@/app/components/row" +import { BasicExample } from "./checkbox.basic" +import { DescriptionExample } from "./checkbox.description" +import { LabelPositionExample } from "./checkbox.labelPosition" +import { InfoIconExample } from "./checkbox.infoIcon" + +## Basic Usage + +Checkboxes in their various states - unchecked, checked, disabled, and disabled checked. + + + +## With Description + +Checkboxes can include descriptions to provide additional context. + + + +## Label Positions + +Labels can be positioned above or to the side of the checkbox. + + + +## With Information Icon + +Checkboxes can include an information icon with a tooltip for additional details. + + diff --git a/apps/ui-playground/content/design/components/dialog.basic.tsx b/apps/ui-playground/content/design/components/dialog.basic.tsx new file mode 100644 index 0000000000..6c5eb0a568 --- /dev/null +++ b/apps/ui-playground/content/design/components/dialog.basic.tsx @@ -0,0 +1,33 @@ +"use client"; + +import { RenderComponentWithSnippet } from "@/app/components/render"; +import { useState } from "react"; + +import { Button } from "@calcom/ui/components/button"; +import { Dialog, DialogClose, DialogContent, DialogFooter } from "@calcom/ui/components/dialog"; + +export const BasicExample: React.FC = () => { + const [open, setOpen] = useState(false); + + return ( + +
+ + + +

+ Dialogs are used to display content that requires user attention or interaction. They appear + above the page content and must be closed before interacting with the page again. +

+ + + + +
+
+
+
+ ); +}; diff --git a/apps/ui-playground/content/design/components/dialog.confirmation.tsx b/apps/ui-playground/content/design/components/dialog.confirmation.tsx new file mode 100644 index 0000000000..d3acc63ea3 --- /dev/null +++ b/apps/ui-playground/content/design/components/dialog.confirmation.tsx @@ -0,0 +1,37 @@ +"use client"; + +import { RenderComponentWithSnippet } from "@/app/components/render"; +import { useState } from "react"; + +import { Button } from "@calcom/ui/components/button"; +import { Dialog, DialogClose, DialogContent, DialogFooter } from "@calcom/ui/components/dialog"; + +export const ConfirmationExample: React.FC = () => { + const [open, setOpen] = useState(false); + + return ( + +
+ + + + + + + + + +
+
+ ); +}; diff --git a/apps/ui-playground/content/design/components/dialog.form.tsx b/apps/ui-playground/content/design/components/dialog.form.tsx new file mode 100644 index 0000000000..7e7720ea49 --- /dev/null +++ b/apps/ui-playground/content/design/components/dialog.form.tsx @@ -0,0 +1,56 @@ +"use client"; + +import { RenderComponentWithSnippet } from "@/app/components/render"; +import { useState } from "react"; + +import { Button } from "@calcom/ui/components/button"; +import { Dialog, DialogClose, DialogContent, DialogFooter } from "@calcom/ui/components/dialog"; +import { TextAreaField } from "@calcom/ui/components/form"; + +export const FormExample: React.FC = () => { + const [open, setOpen] = useState(false); + const [formInput, setFormInput] = useState(""); + + return ( + +
+ + + +
+ + Your Feedback + (Optional) + + } + value={formInput} + onChange={(e) => setFormInput(e.target.value)} + /> +
+ + + + +
+
+
+

Current Form Input:

+
+            {formInput || "No input yet"}
+          
+
+
+
+ ); +}; diff --git a/apps/ui-playground/content/design/components/dialog.largeContent.tsx b/apps/ui-playground/content/design/components/dialog.largeContent.tsx new file mode 100644 index 0000000000..0d77357d8b --- /dev/null +++ b/apps/ui-playground/content/design/components/dialog.largeContent.tsx @@ -0,0 +1,58 @@ +"use client"; + +import { RenderComponentWithSnippet } from "@/app/components/render"; +import { useState } from "react"; + +import { Button } from "@calcom/ui/components/button"; +import { Dialog, DialogClose, DialogContent, DialogFooter } from "@calcom/ui/components/dialog"; + +export const LargeContentExample: React.FC = () => { + const [open, setOpen] = useState(false); + + return ( + +
+ + + +
+ {Array.from({ length: 5 }).map((_, i) => ( +
+

Section {i + 1}

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam in dui mauris. Vivamus + hendrerit arcu sed erat molestie vehicula. Sed auctor neque eu tellus rhoncus ut eleifend + nibh porttitor. Ut in nulla enim. Phasellus molestie magna non est bibendum non venenatis + nisl tempor. +

+

+ Suspendisse in orci enim. Integer vel sapien at risus ultrices ornare nec sit amet nibh. + Duis blandit lectus ac odio rhoncus non congue diam bibendum. Aliquam erat volutpat. +

+ {i === 2 && ( +
+

Important Notice

+

+ This is a highlighted section within the content to demonstrate how different UI + elements appear within a scrollable dialog. +

+
+ )} +
+ ))} +
+ + Decline + + +
+
+
+
+ ); +}; diff --git a/apps/ui-playground/content/design/components/dialog.mdx b/apps/ui-playground/content/design/components/dialog.mdx new file mode 100644 index 0000000000..5e03bc7ebd --- /dev/null +++ b/apps/ui-playground/content/design/components/dialog.mdx @@ -0,0 +1,35 @@ +--- +title: Dialog +airtableId: rechCoaoEMZLnLChR +--- +import { Dialog } from "@calcom/ui/components/dialog" +import { RenderComponentWithSnippet } from "@/app/components/render" +import { Row } from "@/app/components/row" +import { BasicExample } from "./dialog.basic" +import { LargeContentExample } from "./dialog.largeContent" +import { FormExample } from "./dialog.form" +import { ConfirmationExample } from "./dialog.confirmation" + +## Basic Dialog + +A simple dialog with a title, description, and action buttons. + + + +## Large Content Dialog + +A dialog that can handle scrollable content, such as terms and conditions. + + + +## Form Dialog + +A dialog containing a form for user input. + + + +## Confirmation Dialog + +A dialog for confirming destructive actions. + + diff --git a/apps/ui-playground/content/design/components/dropdown.avatarTrigger.tsx b/apps/ui-playground/content/design/components/dropdown.avatarTrigger.tsx new file mode 100644 index 0000000000..1b980fa104 --- /dev/null +++ b/apps/ui-playground/content/design/components/dropdown.avatarTrigger.tsx @@ -0,0 +1,38 @@ +"use client"; + +import { RenderComponentWithSnippet } from "@/app/components/render"; + +import { Avatar } from "@calcom/ui/components/avatar"; +import { + Dropdown, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownItem, + DropdownMenuLabel, + DropdownMenuSeparator, +} from "@calcom/ui/components/dropdown"; + +export const AvatarTriggerExample: React.FC = () => ( + +
+
+ + + + + + Profile + + View Profile + Settings + + Sign out + + + Avatar Menu +
+
+
+); diff --git a/apps/ui-playground/content/design/components/dropdown.buttonTrigger.tsx b/apps/ui-playground/content/design/components/dropdown.buttonTrigger.tsx new file mode 100644 index 0000000000..10fc247e37 --- /dev/null +++ b/apps/ui-playground/content/design/components/dropdown.buttonTrigger.tsx @@ -0,0 +1,84 @@ +"use client"; + +import { RenderComponentWithSnippet } from "@/app/components/render"; + +import type { IconName } from "@calcom/ui"; +import { Button } from "@calcom/ui/components/button"; +import { + Dropdown, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownItem, + DropdownMenuLabel, +} from "@calcom/ui/components/dropdown"; + +const menuItems = [ + { label: "View", icon: "eye", kbd: "V" }, + { label: "Edit", icon: "edit", kbd: "E" }, + { label: "Share", icon: "share", kbd: "⌘S" }, + { label: "Delete", icon: "trash", destructive: true, kbd: "⌘⌫" }, +] as { + label: string; + icon: IconName; + destructive?: boolean; + kbd?: string; +}[]; + +export const ButtonTriggerExample: React.FC = () => ( + +
+ {/* Default Button */} +
+ + + + + + Actions + {menuItems.map((item) => ( + + {item.label} + + ))} + + + Default +
+ + {/* Button with Icon */} +
+ + + + + + Quick Actions + {menuItems.map((item) => ( + + {item.label} + + ))} + + + With Icon +
+ + {/* Icon Button */} +
+ + +
+
+
+); diff --git a/apps/ui-playground/content/design/components/dropdown.complex.tsx b/apps/ui-playground/content/design/components/dropdown.complex.tsx new file mode 100644 index 0000000000..e060280427 --- /dev/null +++ b/apps/ui-playground/content/design/components/dropdown.complex.tsx @@ -0,0 +1,88 @@ +"use client"; + +import { RenderComponentWithSnippet } from "@/app/components/render"; + +import { Avatar } from "@calcom/ui/components/avatar"; +import { Button } from "@calcom/ui/components/button"; +import { + Dropdown, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownItem, + DropdownMenuLabel, + DropdownMenuSeparator, +} from "@calcom/ui/components/dropdown"; + +export const ComplexExample: React.FC = () => ( + +
+ {/* Example 1: Complex Menu Structure */} +
+ + + + + + My account + Profile + Billing + Settings + Keyboard Shortcuts + + Team + Invite users + New team + + Github + Support + API + + + Complex Menu +
+ + {/* Example 2: Avatar with Icons */} +
+ + + + + + peer@cal.com + + My account + Plan + Billing + Integrations + Module + + + Log out + + + + With Icons +
+ + {/* Example 3: Simple Actions */} +
+ + + + + + Rename + Duplicate + Move + Delete + + + Simple Actions +
+
+
+); diff --git a/apps/ui-playground/content/design/components/dropdown.mdx b/apps/ui-playground/content/design/components/dropdown.mdx new file mode 100644 index 0000000000..ace82eef94 --- /dev/null +++ b/apps/ui-playground/content/design/components/dropdown.mdx @@ -0,0 +1,28 @@ +--- +title: Dropdown +airtableId: recMePWqulsKOCa59 +--- +import { Dropdown } from "@calcom/ui/components/dropdown" +import { RenderComponentWithSnippet } from "@/app/components/render" +import { Row } from "@/app/components/row" +import { ButtonTriggerExample } from "./dropdown.buttonTrigger" +import { AvatarTriggerExample } from "./dropdown.avatarTrigger" +import { ComplexExample } from "./dropdown.complex" + +## Button Trigger + +Dropdowns can be triggered by various types of buttons. + + + +## Avatar Trigger + +Dropdowns can be triggered by avatar components, commonly used for profile menus. + + + +## Complex Examples + +More complex dropdown examples with various combinations of labels, icons, and separators. + + diff --git a/apps/ui-playground/content/design/components/emptyScreen.basic.tsx b/apps/ui-playground/content/design/components/emptyScreen.basic.tsx new file mode 100644 index 0000000000..bf6e465ad1 --- /dev/null +++ b/apps/ui-playground/content/design/components/emptyScreen.basic.tsx @@ -0,0 +1,17 @@ +"use client"; + +import { RenderComponentWithSnippet } from "@/app/components/render"; + +import { EmptyScreen } from "@calcom/ui/components/empty-screen"; + +export const BasicExample: React.FC = () => ( + + alert("Create Meeting clicked")} + /> + +); diff --git a/apps/ui-playground/content/design/components/emptyScreen.border.tsx b/apps/ui-playground/content/design/components/emptyScreen.border.tsx new file mode 100644 index 0000000000..f7c5d987fd --- /dev/null +++ b/apps/ui-playground/content/design/components/emptyScreen.border.tsx @@ -0,0 +1,37 @@ +"use client"; + +import { RenderComponentWithSnippet } from "@/app/components/render"; + +import { EmptyScreen } from "@calcom/ui/components/empty-screen"; + +export const BorderExample: React.FC = () => ( + +
+ {/* Without Border */} +
+

Without Border

+ alert("Browse Apps clicked")} + border={false} + /> +
+ + {/* With Solid Border */} +
+

With Solid Border

+ alert("Compose clicked")} + dashedBorder={false} + /> +
+
+
+); diff --git a/apps/ui-playground/content/design/components/emptyScreen.customIcon.tsx b/apps/ui-playground/content/design/components/emptyScreen.customIcon.tsx new file mode 100644 index 0000000000..876276957d --- /dev/null +++ b/apps/ui-playground/content/design/components/emptyScreen.customIcon.tsx @@ -0,0 +1,18 @@ +"use client"; + +import { RenderComponentWithSnippet } from "@/app/components/render"; + +import { EmptyScreen } from "@calcom/ui/components/empty-screen"; + +export const CustomIconExample: React.FC = () => ( + + alert("Add Members clicked")} + /> + +); diff --git a/apps/ui-playground/content/design/components/emptyScreen.htmlContent.tsx b/apps/ui-playground/content/design/components/emptyScreen.htmlContent.tsx new file mode 100644 index 0000000000..a51833bc0d --- /dev/null +++ b/apps/ui-playground/content/design/components/emptyScreen.htmlContent.tsx @@ -0,0 +1,28 @@ +"use client"; + +import { RenderComponentWithSnippet } from "@/app/components/render"; + +import { EmptyScreen } from "@calcom/ui/components/empty-screen"; + +export const HtmlContentExample: React.FC = () => ( + + + Custom Headline with HTML + + } + description={ +
+

This is a custom description with multiple paragraphs.

+

+ You can include any HTML content here. +

+
+ } + buttonText="Learn More" + buttonOnClick={() => alert("Learn More clicked")} + /> +
+); diff --git a/apps/ui-playground/content/design/components/emptyScreen.mdx b/apps/ui-playground/content/design/components/emptyScreen.mdx new file mode 100644 index 0000000000..94ba61fbd3 --- /dev/null +++ b/apps/ui-playground/content/design/components/emptyScreen.mdx @@ -0,0 +1,41 @@ +--- +title: Empty Screen +--- +import { EmptyScreen } from "@calcom/ui/components/empty-screen"; +import { RenderComponentWithSnippet } from "@/app/components/render" +import { Row } from "@/app/components/row" +import { BasicExample } from "./emptyScreen.basic" +import { CustomIconExample } from "./emptyScreen.customIcon" +import { RawButtonExample } from "./emptyScreen.rawButton" +import { BorderExample } from "./emptyScreen.border" +import { HtmlContentExample } from "./emptyScreen.htmlContent" + +## Basic Usage + +A simple empty screen with an icon, headline, description, and action button. + + + +## Custom Icon + +Empty screen with a custom icon and styling. + + + +## Raw Button + +Empty screen with custom button layout. + + + +## Border Variations + +Empty screen with different border styles. + + + +## HTML Content + +Empty screen with custom HTML content in headline and description. + + diff --git a/apps/ui-playground/content/design/components/emptyScreen.rawButton.tsx b/apps/ui-playground/content/design/components/emptyScreen.rawButton.tsx new file mode 100644 index 0000000000..28e3ee740b --- /dev/null +++ b/apps/ui-playground/content/design/components/emptyScreen.rawButton.tsx @@ -0,0 +1,26 @@ +"use client"; + +import { RenderComponentWithSnippet } from "@/app/components/render"; + +import { Button } from "@calcom/ui/components/button"; +import { EmptyScreen } from "@calcom/ui/components/empty-screen"; + +export const RawButtonExample: React.FC = () => ( + + + + + + } + /> + +); diff --git a/apps/ui-playground/content/design/components/form.addOns.tsx b/apps/ui-playground/content/design/components/form.addOns.tsx new file mode 100644 index 0000000000..74883658f1 --- /dev/null +++ b/apps/ui-playground/content/design/components/form.addOns.tsx @@ -0,0 +1,67 @@ +"use client"; + +import { RenderComponentWithSnippet } from "@/app/components/render"; +import { useForm } from "react-hook-form"; + +import { Button } from "@calcom/ui/components/button"; +import { InputField } from "@calcom/ui/components/form"; +import { Icon } from "@calcom/ui/components/icon"; + +type FormValues = { + website: string; + githubUsername: string; + customDomain: string; +}; + +export const AddOnsExample: React.FC = () => { + const form = useForm({ + defaultValues: { + website: "", + githubUsername: "", + customDomain: "", + }, + }); + + const onSubmit = (data: FormValues) => { + console.log("Form submitted:", data); + }; + + return ( + +
+ + + } + {...form.register("githubUsername")} + error={form.formState.errors.githubUsername?.message} + /> + +
+ + +
+ +
+ ); +}; diff --git a/apps/ui-playground/content/design/components/form.basic.tsx b/apps/ui-playground/content/design/components/form.basic.tsx new file mode 100644 index 0000000000..1768e7d50b --- /dev/null +++ b/apps/ui-playground/content/design/components/form.basic.tsx @@ -0,0 +1,59 @@ +"use client"; + +import { RenderComponentWithSnippet } from "@/app/components/render"; +import { useForm } from "react-hook-form"; + +import { Button } from "@calcom/ui/components/button"; +import { InputField } from "@calcom/ui/components/form"; + +type FormValues = { + username: string; + email: string; + password: string; +}; + +export const BasicExample: React.FC = () => { + const form = useForm({ + defaultValues: { + username: "", + email: "", + password: "", + }, + }); + + const onSubmit = (data: FormValues) => { + console.log("Form submitted:", data); + }; + + return ( + +
+ + + +
+ + +
+ +
+ ); +}; diff --git a/apps/ui-playground/content/design/components/form.mdx b/apps/ui-playground/content/design/components/form.mdx new file mode 100644 index 0000000000..e49856bf05 --- /dev/null +++ b/apps/ui-playground/content/design/components/form.mdx @@ -0,0 +1,26 @@ +--- +title: Form +--- +import { RenderComponentWithSnippet } from "@/app/components/render" +import { Row } from "@/app/components/row" +import { BasicExample } from "./form.basic" +import { AddOnsExample } from "./form.addOns" +import { SpecialFieldsExample } from "./form.specialFields" + +## Basic Fields + +Basic form fields with validation using React Hook Form and Zod. + + + +## Fields with Add-ons + +Form fields with leading and trailing add-ons. + + + +## Number and Special Fields + +Form fields for numbers and special input types. + + diff --git a/apps/ui-playground/content/design/components/form.specialFields.tsx b/apps/ui-playground/content/design/components/form.specialFields.tsx new file mode 100644 index 0000000000..86dd9258e1 --- /dev/null +++ b/apps/ui-playground/content/design/components/form.specialFields.tsx @@ -0,0 +1,53 @@ +"use client"; + +import { RenderComponentWithSnippet } from "@/app/components/render"; +import { useForm } from "react-hook-form"; + +import { Button } from "@calcom/ui/components/button"; +import { InputField } from "@calcom/ui/components/form"; + +type FormValues = { + age?: number; + phone?: string; +}; + +export const SpecialFieldsExample: React.FC = () => { + const form = useForm({ + defaultValues: { + age: undefined, + phone: "", + }, + }); + + const onSubmit = (data: FormValues) => { + console.log("Form submitted:", data); + }; + + return ( + +
+ + +
+ + +
+ +
+ ); +}; diff --git a/apps/ui-playground/content/design/components/input.mdx b/apps/ui-playground/content/design/components/input.mdx new file mode 100644 index 0000000000..b95fb005bf --- /dev/null +++ b/apps/ui-playground/content/design/components/input.mdx @@ -0,0 +1,35 @@ +--- +title: Input +airtableId: rec2yv97ak5hv3kIL +--- +import { Input } from "@calcom/ui/components/form" +import { RenderComponentWithSnippet } from "@/app/components/render" +import { Row } from "@/app/components/row" +import { StatesExample } from "./input.states" +import { TypesExample } from "./input.types" +import { WidthsExample } from "./input.widths" +import { ValuesExample } from "./input.values" + +## States + +Input fields in different states: default, disabled, and readonly. + + + +## Types + +Different types of input fields for various data types. + + + +## Widths + +Input fields with different width configurations. + + + +## With Values + +Input fields with default values and placeholders. + + diff --git a/apps/ui-playground/content/design/components/input.states.tsx b/apps/ui-playground/content/design/components/input.states.tsx new file mode 100644 index 0000000000..f63285e774 --- /dev/null +++ b/apps/ui-playground/content/design/components/input.states.tsx @@ -0,0 +1,37 @@ +"use client"; + +import { RenderComponentWithSnippet } from "@/app/components/render"; + +import { Input } from "@calcom/ui/components/form"; + +const states = ["default", "disabled", "readonly"] as const; +const sizes = ["sm", "md"] as const; + +export const StatesExample: React.FC = () => ( + +
+
+ {states.map((state) => ( +
+

{state}

+
+ {sizes.map((size) => ( +
+ + {size} +
+ ))} +
+
+ ))} +
+
+
+); diff --git a/apps/ui-playground/content/design/components/input.types.tsx b/apps/ui-playground/content/design/components/input.types.tsx new file mode 100644 index 0000000000..e9cbc4e3fb --- /dev/null +++ b/apps/ui-playground/content/design/components/input.types.tsx @@ -0,0 +1,46 @@ +"use client"; + +import { RenderComponentWithSnippet } from "@/app/components/render"; + +import { Input } from "@calcom/ui/components/form"; + +const types = ["text", "email", "password", "number", "tel", "url", "search"] as const; +const sizes = ["sm", "md"] as const; + +export const TypesExample: React.FC = () => ( + +
+
+ {types.map((type) => ( +
+

{type}

+
+ {sizes.map((size) => ( +
+ + {size} +
+ ))} +
+
+ ))} +
+
+
+); diff --git a/apps/ui-playground/content/design/components/input.values.tsx b/apps/ui-playground/content/design/components/input.values.tsx new file mode 100644 index 0000000000..c6835482c6 --- /dev/null +++ b/apps/ui-playground/content/design/components/input.values.tsx @@ -0,0 +1,31 @@ +"use client"; + +import { RenderComponentWithSnippet } from "@/app/components/render"; + +import { Input } from "@calcom/ui/components/form"; + +const sizes = ["sm", "md"] as const; + +export const ValuesExample: React.FC = () => ( + +
+
+ {sizes.map((size) => ( +
+

Size: {size}

+
+
+

Default Value

+ +
+
+

Placeholder

+ +
+
+
+ ))} +
+
+
+); diff --git a/apps/ui-playground/content/design/components/input.widths.tsx b/apps/ui-playground/content/design/components/input.widths.tsx new file mode 100644 index 0000000000..804f31f466 --- /dev/null +++ b/apps/ui-playground/content/design/components/input.widths.tsx @@ -0,0 +1,31 @@ +"use client"; + +import { RenderComponentWithSnippet } from "@/app/components/render"; + +import { Input } from "@calcom/ui/components/form"; + +const sizes = ["sm", "md"] as const; + +export const WidthsExample: React.FC = () => ( + +
+
+ {sizes.map((size) => ( +
+

Size: {size}

+
+
+

Full Width

+ +
+
+

Auto Width

+ +
+
+
+ ))} +
+
+
+); diff --git a/apps/ui-playground/content/design/components/navigation.items.tsx b/apps/ui-playground/content/design/components/navigation.items.tsx new file mode 100644 index 0000000000..cc716d34ee --- /dev/null +++ b/apps/ui-playground/content/design/components/navigation.items.tsx @@ -0,0 +1,92 @@ +"use client"; + +import { RenderComponentWithSnippet } from "@/app/components/render"; + +import { Badge } from "@calcom/ui/components/badge"; +import { NavigationItem } from "@calcom/ui/components/navigation"; + +export const ItemsExample: React.FC = () => ( + +
+
+

Basic Items

+ + +
+ +
+

With Badge

+ 3, + }} + /> + New, + }} + /> +
+ +
+

Active State

+ + 3, + isCurrent: true, + }} + /> + + +
+
+
+); diff --git a/apps/ui-playground/content/design/components/navigation.mdx b/apps/ui-playground/content/design/components/navigation.mdx new file mode 100644 index 0000000000..093eb207ba --- /dev/null +++ b/apps/ui-playground/content/design/components/navigation.mdx @@ -0,0 +1,20 @@ +--- +title: Navigation +airtableId: recwHio1ktKQOdXGL +--- +import { RenderComponentWithSnippet } from "@/app/components/render" +import { Row } from "@/app/components/row" +// import { SidebarExample } from "./navigation.sidebar.tsx" +import { ItemsExample } from "./navigation.items.tsx" + +{/* ## Sidebar Navigation + +Full sidebar navigation with logo, sections, and footer. + + */} + +## Navigation Items + +Individual navigation items with different states and variants. + + diff --git a/apps/ui-playground/content/design/components/navigation.sidebar.tsx b/apps/ui-playground/content/design/components/navigation.sidebar.tsx new file mode 100644 index 0000000000..c5cdcc1aab --- /dev/null +++ b/apps/ui-playground/content/design/components/navigation.sidebar.tsx @@ -0,0 +1,204 @@ +"use client"; + +import { RenderComponentWithSnippet } from "@/app/components/render"; +import { useState } from "react"; + +import { NavigationItem } from "@calcom/ui"; + +export const SidebarExample: React.FC = () => { + const [isDeelExpanded, setIsDeelExpanded] = useState(true); + + return ( + +
+ + + + + +
Manage
+ + + +
+ + +
+
+ + + + + + + + + + + setIsDeelExpanded(!isDeelExpanded), + child: [ + { name: "Profile", href: "#profile" }, + { name: "Members", href: "#members" }, + { name: "Event Types", href: "#event-types" }, + { name: "Appearance", href: "#appearance" }, + { name: "Billing", href: "#billing" }, + { name: "Booking Limits", href: "#booking-limits" }, + { name: "Attributes", href: "#attributes" }, + { name: "Segments", href: "#segments", isCurrent: true }, + ], + }} + /> + + +
+ + ); +}; diff --git a/apps/ui-playground/content/design/components/pagination.mdx b/apps/ui-playground/content/design/components/pagination.mdx new file mode 100644 index 0000000000..e0eb078a58 --- /dev/null +++ b/apps/ui-playground/content/design/components/pagination.mdx @@ -0,0 +1,42 @@ +--- +title: Pagination +--- + +import { BasicExample, CustomPageSizesExample, LargeDatasetExample, WithCallbacksExample } from "./pagination"; + +# Pagination + +The Pagination component provides a consistent way to handle data pagination across the application. It includes rows per page selection and navigation controls. + +## Installation + +```ts +import { Pagination } from "@calcom/ui"; +``` + +## Examples + +### Basic Usage + +A basic pagination component with default settings. + + + +### Custom Page Sizes + +Customize the available page size options. + + + +### With Navigation Callbacks + +Use `onNext` and `onPrevious` callbacks to perform additional actions when navigating pages. + + + +### Large Dataset + +The pagination component handles large datasets efficiently. + + + diff --git a/apps/ui-playground/content/design/components/pagination.tsx b/apps/ui-playground/content/design/components/pagination.tsx new file mode 100644 index 0000000000..f81e8988c1 --- /dev/null +++ b/apps/ui-playground/content/design/components/pagination.tsx @@ -0,0 +1,97 @@ +"use client"; + +import { RenderComponentWithSnippet } from "@/app/components/render"; +import { useState } from "react"; + +import { Pagination } from "@calcom/ui/components/pagination"; + +export const BasicExample = () => { + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(25); + const totalItems = 61; + + return ( + +
+ +
+
+ ); +}; + +export const CustomPageSizesExample = () => { + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(5); + const totalItems = 25; + + return ( + +
+ +
+
+ ); +}; + +export const WithCallbacksExample = () => { + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(25); + const totalItems = 100; + + const handleNext = () => { + console.log("Next page clicked"); + }; + + const handlePrevious = () => { + console.log("Previous page clicked"); + }; + + return ( + +
+ +
+
+ ); +}; + +export const LargeDatasetExample = () => { + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(25); + const totalItems = 1000; + + return ( + +
+ +
+
+ ); +}; diff --git a/apps/ui-playground/content/design/components/rangeslider.basic.tsx b/apps/ui-playground/content/design/components/rangeslider.basic.tsx new file mode 100644 index 0000000000..a290893398 --- /dev/null +++ b/apps/ui-playground/content/design/components/rangeslider.basic.tsx @@ -0,0 +1,72 @@ +"use client"; + +import { RenderComponentWithSnippet } from "@/app/components/render"; +import { useState } from "react"; + +import { RangeSlider } from "@calcom/ui/components/form"; + +export const BasicExample: React.FC = () => { + const [singleValue, setSingleValue] = useState([50]); + const [rangeValue, setRangeValue] = useState([20, 80]); + const [steppedValue, setSteppedValue] = useState([25]); + + return ( + +
+
+
+
+ Single Value: {singleValue[0]}% +
+ +
+
+
+
+
+ + Range: {rangeValue[0]}% - {rangeValue[1]}% + +
+ +
+
+ +
+
+
+ Step Size 25: {steppedValue[0]}% +
+ +
+
+
+
+
+ Disabled: 50% +
+ +
+
+
+
+ ); +}; diff --git a/apps/ui-playground/content/design/components/rangeslider.mdx b/apps/ui-playground/content/design/components/rangeslider.mdx new file mode 100644 index 0000000000..6b1b7410af --- /dev/null +++ b/apps/ui-playground/content/design/components/rangeslider.mdx @@ -0,0 +1,22 @@ +--- +title: Range Slider +airtableId: recPe8JPnGCVmjROA +--- + +import { RenderComponentWithSnippet } from "@/app/components/render"; +import { Row } from "@/app/components/row"; + +import { BasicExample } from "./rangeslider.basic.tsx"; +import { PopoverExample } from "./rangeslider.popover"; + +## Basic + +Basic range slider for selecting numeric values. + + + +## With Popover + +Range slider with a popover showing the current value. + + diff --git a/apps/ui-playground/content/design/components/rangeslider.popover.tsx b/apps/ui-playground/content/design/components/rangeslider.popover.tsx new file mode 100644 index 0000000000..8805ed9548 --- /dev/null +++ b/apps/ui-playground/content/design/components/rangeslider.popover.tsx @@ -0,0 +1,71 @@ +"use client"; + +import { RenderComponentWithSnippet } from "@/app/components/render"; +import { useState } from "react"; + +import { RangeSliderPopover } from "@calcom/ui/components/form"; + +export const PopoverExample: React.FC = () => { + const [defaultRange, setDefaultRange] = useState([15, 30]); + const [customRange, setCustomRange] = useState([5, 20]); + const [largeRange, setLargeRange] = useState([0, 100]); + + return ( + +
+
+

Default Range (15-30 mins)

+ +
+ +
+

Custom Range with Success Badge (5-20 mins)

+ +
+ +
+

Large Range with Warning Badge (0-100 mins)

+ +
+ +
+

Selected Values:

+
+            {JSON.stringify(
+              {
+                defaultRange,
+                customRange,
+                largeRange,
+              },
+              null,
+              2
+            )}
+          
+
+
+
+ ); +}; diff --git a/apps/ui-playground/content/design/components/select.async.tsx b/apps/ui-playground/content/design/components/select.async.tsx new file mode 100644 index 0000000000..e6461cf60a --- /dev/null +++ b/apps/ui-playground/content/design/components/select.async.tsx @@ -0,0 +1,89 @@ +"use client"; + +import { RenderComponentWithSnippet } from "@/app/components/render"; +import { useCallback, useState } from "react"; + +import { Select } from "@calcom/ui/components/form"; +import { showToast } from "@calcom/ui/components/toast"; + +const countries = [ + { value: "us", label: "United States" }, + { value: "uk", label: "United Kingdom" }, + { value: "ca", label: "Canada" }, + { value: "fr", label: "France" }, + { value: "de", label: "Germany" }, + { value: "it", label: "Italy" }, + { value: "es", label: "Spain" }, + { value: "au", label: "Australia" }, + { value: "jp", label: "Japan" }, + { value: "br", label: "Brazil" }, +]; + +// Simulate an API call with delay +const searchCountries = (query: string) => { + return new Promise<{ value: string; label: string }[]>((resolve) => { + setTimeout(() => { + const filtered = countries.filter((country) => + country.label.toLowerCase().includes(query.toLowerCase()) + ); + resolve(filtered); + }, 1000); // Simulate network delay + }); +}; + +export const AsyncExample: React.FC = () => { + const [asyncOptions, setAsyncOptions] = useState<{ value: string; label: string }[]>([]); + const [isLoading, setIsLoading] = useState(false); + + const handleValueChange = (newValue: unknown, actionMeta: { action: string }) => { + showToast(`Selected: ${JSON.stringify(newValue)}, Action: ${actionMeta.action}`, "success"); + }; + + const loadOptions = useCallback(async (inputValue: string) => { + setIsLoading(true); + try { + const results = await searchCountries(inputValue); + setAsyncOptions(results); + } catch (error) { + showToast("Error loading options", "error"); + } finally { + setIsLoading(false); + } + }, []); + + return ( + +
+ { + if (value) { + loadOptions(value); + } + }} + isLoading={isLoading} + placeholder="Search for a country... (small)" + noOptionsMessage={({ inputValue }) => + inputValue ? "No countries found" : "Start typing to search..." + } + onChange={handleValueChange} + size="sm" + /> +
+
+ ); +}; diff --git a/apps/ui-playground/content/design/components/select.basic.tsx b/apps/ui-playground/content/design/components/select.basic.tsx new file mode 100644 index 0000000000..eab7e3da64 --- /dev/null +++ b/apps/ui-playground/content/design/components/select.basic.tsx @@ -0,0 +1,41 @@ +"use client"; + +import { RenderComponentWithSnippet } from "@/app/components/render"; +import { useState } from "react"; + +import { Select } from "@calcom/ui/components/form"; + +const options = [ + { value: "chocolate", label: "Chocolate" }, + { value: "strawberry", label: "Strawberry" }, + { value: "vanilla", label: "Vanilla" }, + { value: "mint", label: "Mint" }, + { value: "coffee", label: "Coffee" }, +]; + +export const BasicExample: React.FC = () => { + const [singleValue, setSingleValue] = useState<{ value: string; label: string } | null>(null); + + return ( + +
+ setSingleValue(newValue)} + isClearable + placeholder="Small size select..." + size="sm" + /> +
+
+ ); +}; diff --git a/apps/ui-playground/content/design/components/select.disabled.tsx b/apps/ui-playground/content/design/components/select.disabled.tsx new file mode 100644 index 0000000000..34da21f6a8 --- /dev/null +++ b/apps/ui-playground/content/design/components/select.disabled.tsx @@ -0,0 +1,27 @@ +"use client"; + +import { RenderComponentWithSnippet } from "@/app/components/render"; + +import { Select, SelectField } from "@calcom/ui/components/form"; + +const options = [ + { value: "chocolate", label: "Chocolate" }, + { value: "strawberry", label: "Strawberry" }, + { value: "vanilla", label: "Vanilla" }, + { value: "mint", label: "Mint" }, + { value: "coffee", label: "Coffee" }, +]; + +export const DisabledExample: React.FC = () => ( + +
+ +
+
+); diff --git a/apps/ui-playground/content/design/components/select.loading.tsx b/apps/ui-playground/content/design/components/select.loading.tsx new file mode 100644 index 0000000000..ba9f72c525 --- /dev/null +++ b/apps/ui-playground/content/design/components/select.loading.tsx @@ -0,0 +1,21 @@ +"use client"; + +import { RenderComponentWithSnippet } from "@/app/components/render"; + +import { Select } from "@calcom/ui/components/form"; + +const options = [ + { value: "chocolate", label: "Chocolate" }, + { value: "strawberry", label: "Strawberry" }, + { value: "vanilla", label: "Vanilla" }, + { value: "mint", label: "Mint" }, + { value: "coffee", label: "Coffee" }, +]; + +export const LoadingExample: React.FC = () => ( + +
+ setMultiValue(newValue as { value: string; label: string }[])} + isMulti + isClearable + placeholder="Choose multiple flavors..." + /> +