Feat/design system (#3051)

* Storybook Boilerplate setup

* Inital Setup

* First story

* Color Design System

* Badge Story + Comp

* Checkbox UI + Stories

* Update Red colors + Button Group

* Switch+Stories / Default brand color

* Update Version + Button Group combined

* Compact Butotn Group

* Tidy up Selectors

* Adds Tooltip to Button

* TextInput

* Update SB

* Prefix Input

* Match text area styles

* Prefix Controls

* Update spacing on text area

* Text Input Suffix

* Color Picker

* Update storybook

* Icon Suffix/Prefix

* Datepicker + move components to monorepo

* Text color on labels

* Move Radio over to monorepo

* Move CustomBranding to calcom/ib

* Radio

* IconBadge Component

* Update radio indicator background

* Disabled radio state

* Delete yarn.lock

* Revert "Delete yarn.lock"

This reverts commit 9b99d244b7.

* Fix webhook test

* Replace old toast location

* Update radio path

* Empty State

* Update Badge.tsx

* Update Badge.tsx

* Banner Component+story

* Creation Modal

* Creation Dialog updated

* Button hover dialog

* Confirmation Modal

* Datepicker (Booking)

* PageHeader

* Fix border width

* PageHeader update search bar

* Fix input height

* Fix button group size

* Add spacing between badges - font smoothing

* Update button position on banner

* Banner update

* Fixing focus state on suffix/prefix inputs

* Implement A11y addon

* Add aria label

* error && "text-red-800"

* Fix button hover

* Change colors

* Generate snapshot tests for on hover button

* Revert colors to demo

* Change colors

* Fix Linear Issues

* Form Stepper component

* Add padding back to input

* Move ui to UI_V2

* Use V2

* Update imports for v1

* Update imports for v1

* Upgrade to nextjs in storybook root

* Update website submodule

* Avatar Groups

* Fix webpack again

* Vertical Tab Item

[WIP] - active state on small item is not working currently

* Vertical Tab Group

* Add Github action

* Fix website submodule

* Fix GH action

* Rename Workflow

* Adds lint report for CI

* Lint report fixes

* NavigationItem comments

* VerticalTabItem type fixes

* Fix avatar blur

* Fix comments

* Adding isEmbed to window object

* Disable components that use router mock.

* Load inter via google fonts

* Started select

* Adding base Breadcrumb

* Update readme

* Formatting

* Fixes

* Dependencies matching

* Linting

* Update FormStep.stories.tsx

* Linting

* Update MultiSelectCheckboxes.tsx

Co-authored-by: zomars <zomars@me.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
pull/3500/head^2
sean-brydon 2022-07-23 01:39:50 +01:00 committed by GitHub
parent dd9adff32a
commit 277b0c4c92
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
100 changed files with 9966 additions and 340 deletions

View File

@ -0,0 +1,50 @@
# .github/workflows/chromatic.yml
# Workflow name
name: 'Chromatic'
# Event for the workflow
on: push
# List of jobs
jobs:
check-if-ui-has-changed:
runs-on: ubuntu-latest
# Declare outputs for next jobs
outputs:
docs_changed: ${{ steps.check_file_changed.outputs.docs_changed }}
steps:
- uses: actions/checkout@v2
with:
# Checkout as many commits as needed for the diff
fetch-depth: 2
- shell: pwsh
id: check_file_changed
run: |
# Diff HEAD with the previous commit
$diff = git diff --name-only HEAD^ HEAD
# Check if a file under /packages/ui or apps/storybook has been modified since the previous commit
$SourceDiff = $diff | Where-Object { $_ -match '^packages/ui/' -or $_ -match '^apps/storybook/' }
$HasDiff = $SourceDiff.Length -gt 0
# Set the output named "hasUiChanges"
Write-Host "::set-output name=hasUiChanges::$HasDiff"
chromatic-deployment:
runs-on: ubuntu-latest
needs: [ check-if-ui-has-changed ]
if: needs.checkIfUiHasChanged.outputs.hasUiChanges== 'True'
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
fetch-depth: 0 # 👈 Required to retrieve git history
- name: Install dependencies
run: yarn
# 👇 Adds Chromatic as a step in the workflow
- name: Publish to Chromatic
uses: chromaui/action@v1
# Options required to the GitHub Chromatic Action
with:
# 👇 Chromatic projectToken, refer to the manage page to obtain it.
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}

3
.gitignore vendored
View File

@ -73,6 +73,9 @@ dist
# Linting
lint-results
#Storybook
apps/storybook/build-storybook.log
# Snaplet
.snaplet/snapshots
.snaplet/structure.d.ts

View File

@ -424,14 +424,14 @@ Next make sure you have your app running `yarn dx`. Then in the slack chat type
1. Select all events for the webhook you interested, e.g. `sleep_created`
1. Copy the webhook secret (`sec...`) to `VITAL_WEBHOOK_SECRET` in the .env.appStore file.
## Workflows
## Workflows
### Setting up SendGrid for Email reminders
1. Create a SendGrid account (https://signup.sendgrid.com/)
2. Go to Settings -> API keys and create an API key
2. Go to Settings -> API keys and create an API key
3. Copy API key to your .env file into the SENDGRID_API_KEY field
4. Go to Settings -> Sender Authentication and verify a single sender
4. Go to Settings -> Sender Authentication and verify a single sender
5. Copy the verified E-Mail to your .env file into the SENDGRID_EMAIL field
### Setting up Twilio for SMS reminders
@ -445,9 +445,9 @@ Next make sure you have your app running `yarn dx`. Then in the slack chat type
7. Click 'Add Senders'
8. Choose phone number as sender type
9. Add the listed phone number
9. Leave all other fields as they are
10. Complete setup and click View my new Messaging Service
11. Copy Messaging Service SID to your .env file into the TWILIO_MESSAGING_SID field
10. Leave all other fields as they are
11. Complete setup and click View my new Messaging Service
12. Copy Messaging Service SID to your .env file into the TWILIO_MESSAGING_SID field
<!-- LICENSE -->

@ -1 +1 @@
Subproject commit a8e8acd053e0de1da9ad623c3664a837950d6a06
Subproject commit aba7b1ec1c9b5122609dea916c7b114e9a3ba66f

@ -1 +1 @@
Subproject commit a26db083faaa79a40f96dddac888ba2c2bea921e
Subproject commit ac2567263de74449c6e4b98b468415ce1c1815d4

37
apps/storybook/.gitignore vendored Executable file
View File

@ -0,0 +1,37 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# 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*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
storybook-static/

View File

@ -0,0 +1,57 @@
const path = require("path");
module.exports = {
stories: ["../stories/**/*.stories.mdx", "../stories/**/*.stories.@(js|jsx|ts|tsx)"],
addons: [
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/addon-interactions",
"storybook-addon-designs",
"@storybook/addon-a11y",
"storybook-addon-next",
],
framework: "@storybook/react",
core: {
builder: "@storybook/builder-webpack5",
},
webpackFinal: async (config) => {
/**
* Fixes font import with /
* @see https://github.com/storybookjs/storybook/issues/12844#issuecomment-867544160
*/
config.resolve.roots = [path.resolve(__dirname, "../public"), "node_modules"];
/**
* Why webpack5... Just why?
* @type {{console: boolean, process: boolean, timers: boolean, os: boolean, querystring: boolean, sys: boolean, fs: boolean, url: boolean, crypto: boolean, path: boolean, zlib: boolean, punycode: boolean, util: boolean, stream: boolean, assert: boolean, string_decoder: boolean, domain: boolean, vm: boolean, tty: boolean, http: boolean, buffer: boolean, constants: boolean, https: boolean, events: boolean}}
*/
config.resolve.fallback = {
fs: false,
assert: false,
buffer: false,
console: false,
constants: false,
crypto: false,
domain: false,
events: false,
http: false,
https: false,
os: false,
path: false,
punycode: false,
process: false,
querystring: false,
stream: false,
string_decoder: false,
sys: false,
timers: false,
tty: false,
url: false,
util: false,
vm: false,
zlib: false,
};
return config;
},
};

View File

@ -0,0 +1,8 @@
<script>
window.isEmbed = ()=> {
return location.search.includes("embed=")
}
</script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@500&display=swap');
</style>

View File

@ -0,0 +1,20 @@
import * as NextImage from "next/image";
import "../styles/globals.css";
const OriginalNextImage = NextImage.default;
Object.defineProperty(NextImage, "default", {
configurable: true,
value: (props) => <OriginalNextImage {...props} unoptimized />,
});
export const parameters = {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
};

21
apps/storybook/README.md Normal file
View File

@ -0,0 +1,21 @@
# Storybook & UI
Storybook is home to all of our commonly used components. We use this app to visually show all components that we have within the project + automatic type documentation generated for each component.
All changes to storybook/ui must require a visual/code review.
- Visual reviews happen on a tool called [Chormatic](http://chromatic.com/) and these reviews will happen by our product team.
- Code reviews happen as normal on any changes to storybook/ui designs
## Deployment
To deploy this project run
```bash
cd apps/storybook
```
```bash
yarn dev
```

5
apps/storybook/next-env.d.ts vendored Executable file
View File

@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.

7
apps/storybook/next.config.js Executable file
View File

@ -0,0 +1,7 @@
const withTM = require("next-transpile-modules")(["@calcom/dayjs", "@calcom/ui"]);
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
};
module.exports = withTM(nextConfig);

View File

@ -0,0 +1,62 @@
{
"name": "@calcom/storybook",
"version": "0.1.0",
"private": true,
"scripts": {
"dev:next": "next dev",
"build:next": "next build",
"start": "next start",
"lint": "next lint",
"lint:report": "eslint . --format json --output-file ../../lint-results/storybook.json",
"dev": "start-storybook -p 6006",
"build": "build-storybook",
"chromatic": "npx chromatic --project-token=56fadc9ab496"
},
"dependencies": {
"@calcom/dayjs": "*",
"@calcom/ui": "*",
"@radix-ui/react-avatar": "^0.1.4",
"@radix-ui/react-collapsible": "^0.1.0",
"@radix-ui/react-dialog": "^0.1.0",
"@radix-ui/react-dropdown-menu": "^0.1.1",
"@radix-ui/react-id": "^0.1.0",
"@radix-ui/react-radio-group": "^0.1.1",
"@radix-ui/react-slider": "^0.1.1",
"@radix-ui/react-switch": "^0.1.1",
"@radix-ui/react-tooltip": "^0.1.0",
"next": "12.2.0",
"next-transpile-modules": "^9.0.0",
"react": "18.1.0",
"react-dom": "18.1.0",
"react-feather": "^2.0.9",
"react-hot-toast": "^2.2.0",
"storybook-addon-next": "^1.6.7"
},
"devDependencies": {
"@babel/core": "^7.18.6",
"@calcom/config": "*",
"@storybook/addon-a11y": "^6.5.9",
"@storybook/addon-actions": "^6.5.9",
"@storybook/addon-essentials": "^6.5.9",
"@storybook/addon-interactions": "^6.5.9",
"@storybook/addon-links": "^6.5.9",
"@storybook/addon-postcss": "^2.0.0",
"@storybook/builder-webpack5": "^6.5.9",
"@storybook/manager-webpack5": "^6.5.9",
"@storybook/react": "^6.5.9",
"@types/node": "16.9.1",
"@types/react": "18.0.9",
"@types/react-dom": "18.0.4",
"autoprefixer": "^10.4.7",
"babel-loader": "^8.2.5",
"chromatic": "^6.6.4",
"eslint": "^8.15.0",
"postcss": "^8.4.13",
"postcss-loader": "^7.0.0",
"tailwindcss": "^3.1.3",
"tsconfig-paths-webpack-plugin": "^3.5.2",
"typescript": "^4.6.4"
},
"readme": "ERROR: No README data found!",
"_id": "@calcom/storybook@0.1.0"
}

9
apps/storybook/pages/_app.tsx Executable file
View File

@ -0,0 +1,9 @@
import type { AppProps } from "next/app";
import "../styles/globals.css";
function MyApp({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />;
}
export default MyApp;

63
apps/storybook/pages/index.tsx Executable file
View File

@ -0,0 +1,63 @@
import type { NextPage } from "next";
import Head from "next/head";
import Image from "next/image";
const Home: NextPage = () => {
return (
<div className={styles.container}>
<Head>
<title>Create Next App</title>
<meta name="description" content="Generated by create next app" />
<link rel="icon" href="/favicon.ico" />
</Head>
<main className={styles.main}>
<h1 className={styles.title}>
Welcome to <a href="https://nextjs.org">Next.js!</a>
</h1>
<p className={styles.description}>
Get started by editing <code className={styles.code}>pages/index.tsx</code>
</p>
<div className={styles.grid}>
<a href="https://nextjs.org/docs" className={styles.card}>
<h2>Documentation &rarr;</h2>
<p>Find in-depth information about Next.js features and API.</p>
</a>
<a href="https://nextjs.org/learn" className={styles.card}>
<h2>Learn &rarr;</h2>
<p>Learn about Next.js in an interactive course with quizzes!</p>
</a>
<a href="https://github.com/vercel/next.js/tree/canary/examples" className={styles.card}>
<h2>Examples &rarr;</h2>
<p>Discover and deploy boilerplate example Next.js projects.</p>
</a>
<a
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
className={styles.card}>
<h2>Deploy &rarr;</h2>
<p>Instantly deploy your Next.js site to a public URL with Vercel.</p>
</a>
</div>
</main>
<footer className={styles.footer}>
<a
href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer">
Powered by{" "}
<span className={styles.logo}>
<Image src="/vercel.svg" alt="Vercel Logo" width={72} height={16} />
</span>
</a>
</footer>
</div>
);
};
export default Home;

View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

BIN
apps/storybook/public/favicon.ico Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -0,0 +1,4 @@
<svg width="283" height="64" viewBox="0 0 283 64" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path d="M141.04 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.46 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM248.72 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.45 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM200.24 34c0 6 3.92 10 10 10 4.12 0 7.21-1.87 8.8-4.92l7.68 4.43c-3.18 5.3-9.14 8.49-16.48 8.49-11.05 0-19-7.2-19-18s7.96-18 19-18c7.34 0 13.29 3.19 16.48 8.49l-7.68 4.43c-1.59-3.05-4.68-4.92-8.8-4.92-6.07 0-10 4-10 10zm82.48-29v46h-9V5h9zM36.95 0L73.9 64H0L36.95 0zm92.38 5l-27.71 48L73.91 5H84.3l17.32 30 17.32-30h10.39zm58.91 12v9.69c-1-.29-2.06-.49-3.2-.49-5.81 0-10 4-10 10V51h-9V17h9v9.2c0-5.08 5.91-9.2 13.2-9.2z" fill="#000"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,26 @@
import { ComponentMeta } from "@storybook/react";
import Avatar from "@calcom/ui/v2/Avatar";
export default {
title: "Avatar",
component: Avatar,
} as ComponentMeta<typeof Avatar>;
export const Default = () => {
return (
<>
<Avatar size="sm" alt="Avatar Story" gravatarFallbackMd5="Ui@CAL.com" />
<Avatar size="lg" alt="Avatar Story" gravatarFallbackMd5="Ui@CAL.com" />
</>
);
};
export const Accepted = () => {
return (
<>
<Avatar size="sm" alt="Avatar Story" gravatarFallbackMd5="Ui@CAL.com" accepted />
<Avatar size="lg" alt="Avatar Story" gravatarFallbackMd5="Ui@CAL.com" accepted />
</>
);
};

View File

@ -0,0 +1,60 @@
import { ComponentMeta } from "@storybook/react";
import Avatar from "@calcom/ui/v2/Avatar";
import AvatarGroup from "@calcom/ui/v2/AvatarGroup";
export default {
title: "Avatar/Group",
component: AvatarGroup,
} as ComponentMeta<typeof AvatarGroup>;
const IMAGES = [
{
image: "https://cal.com/stakeholder/peer.jpg",
alt: "Peer",
title: "Peer Richelsen",
},
{
image: "https://cal.com/stakeholder/bailey.jpg",
alt: "Bailey",
title: "Bailey Pumfleet",
},
{
image: "https://cal.com/stakeholder/alex-van-andel.jpg",
alt: "Alex",
title: "Alex Van Andel",
},
{
image: "https://cal.com/stakeholder/ciaran.jpg",
alt: "Ciarán",
title: "Ciarán Hanrahan",
},
{
image: "https://cal.com/stakeholder/peer.jpg",
alt: "Peer",
title: "Peer Richelsen",
},
{
image: "https://cal.com/stakeholder/bailey.jpg",
alt: "Bailey",
title: "Bailey Pumfleet",
},
{
image: "https://cal.com/stakeholder/alex-van-andel.jpg",
alt: "Alex",
title: "Alex Van Andel",
},
{
image: "https://cal.com/stakeholder/ciaran.jpg",
alt: "Ciarán",
title: "Ciarán Hanrahan",
},
];
export const Default = () => {
return (
<>
<AvatarGroup size="lg" items={IMAGES} />
</>
);
};

View File

@ -0,0 +1,58 @@
import { ComponentMeta } from "@storybook/react";
import { Bell } from "react-feather";
import { Badge } from "@calcom/ui/v2";
export default {
title: "Badge",
component: Badge,
} as ComponentMeta<typeof Badge>;
export const All = () => (
<div className="">
<h1>Default</h1>
<div className="mb-4 flex space-x-2">
<Badge variant="gray">Badge</Badge>
<Badge variant="red">Badge</Badge>
<Badge variant="green">Badge</Badge>
<Badge variant="orange">Badge</Badge>
<Badge variant="blue">Badge</Badge>
</div>
<h1>Icons</h1>
<div className="mb-4 flex space-x-2">
<Badge variant="gray" StartIcon={Bell}>
Badge
</Badge>
<Badge variant="red" StartIcon={Bell}>
Badge
</Badge>
<Badge variant="green" StartIcon={Bell}>
Badge
</Badge>
<Badge variant="orange" StartIcon={Bell}>
Badge
</Badge>
<Badge variant="blue" StartIcon={Bell}>
Badge
</Badge>
</div>
<h1>Large</h1>
<div className="flex space-x-2">
<Badge variant="gray" size="lg">
Badge
</Badge>
<Badge variant="red" size="lg">
Badge
</Badge>
<Badge variant="green" size="lg">
Badge
</Badge>
<Badge variant="orange" size="lg">
Badge
</Badge>
<Badge variant="blue" size="lg">
Badge
</Badge>
</div>
</div>
);

View File

@ -0,0 +1,46 @@
import { ComponentStory, ComponentMeta } from "@storybook/react";
import { Info } from "react-feather";
import { Banner } from "@calcom/ui/v2";
export default {
title: "Banner",
component: Banner,
} as ComponentMeta<typeof Banner>;
export const Default = () => {
return (
<Banner
variant="neutral"
title="Summarise what happened"
description="Describe what can be done about it here."
Icon={Info}
onDismiss={() => console.log("dismissed")}
/>
);
};
export const Warning = () => {
return (
<Banner
variant="warning"
title="Summarise what happened"
description="Describe what can be done about it here."
Icon={Info}
onDismiss={() => console.log("dismissed")}
/>
);
};
export const Error = () => {
return (
<Banner
variant="error"
title="Summarise what happened"
description="Describe what can be done about it here."
errorMessage="Event creation failed"
Icon={Info}
onDismiss={() => console.log("dismissed")}
/>
);
};

View File

@ -0,0 +1,20 @@
import { Breadcrumb, BreadcrumbItem } from "@calcom/ui/v2";
export default {
title: "Breadcrumbs",
component: Breadcrumb,
};
export const Default = () => (
<Breadcrumb>
<BreadcrumbItem href="/">Home</BreadcrumbItem>
<BreadcrumbItem href="/">Test</BreadcrumbItem>
</Breadcrumb>
);
Default.parameters = {
nextRouter: {
path: "/test",
asPath: "/test",
},
};

View File

@ -0,0 +1,95 @@
import { ComponentMeta, ComponentStory } from "@storybook/react";
import { Trash2 } from "react-feather";
import { Button as ButtonComponent } from "@calcom/ui/v2";
export default {
title: "Button",
component: ButtonComponent,
argTypes: {
color: {
options: ["primary", "secondary", "minimal", "destructive"],
control: { type: "select" },
},
disabled: {
options: [true, false],
control: { type: "boolean" },
},
loading: {
options: [true, false],
control: { type: "boolean" },
},
size: {
options: ["base", "lg", "icon"],
control: { type: "radio" },
},
},
} as ComponentMeta<typeof ButtonComponent>;
const Template: ComponentStory<typeof ButtonComponent> = (args) => <ButtonComponent {...args} />;
export const All = () => (
<div>
<h1>Primary</h1>
<div className="flex space-x-2">
<ButtonComponent aria-label="Button Text">Button Text</ButtonComponent>
<ButtonComponent disabled aria-label="Button Text">
Button Text
</ButtonComponent>
</div>
<h1>Secondary</h1>
<div className="flex space-x-2">
<ButtonComponent color="secondary" aria-label="Button Text">
Button Text
</ButtonComponent>
<ButtonComponent disabled color="secondary" aria-label="Button Text">
Button Text
</ButtonComponent>
<ButtonComponent size="icon" color="secondary" StartIcon={Trash2} aria-label="Button Text" />
</div>
<h1>Minimal</h1>
<div className="flex">
<ButtonComponent color="minimal" aria-label="Button Text">
Button Text
</ButtonComponent>
<ButtonComponent disabled color="minimal" aria-label="Button Text">
Button Text
</ButtonComponent>
<ButtonComponent size="icon" color="minimal" StartIcon={Trash2} aria-label="Button Text" />
</div>
<h1>Destructive</h1>
<ButtonComponent size="icon" color="destructive" StartIcon={Trash2} aria-label="Button Text" />
<h1>Tooltip</h1>
<ButtonComponent
tooltip="Deletes EventTypes"
size="icon"
color="destructive"
StartIcon={Trash2}
aria-label="Button Text"
/>
</div>
);
export const Button = Template.bind({});
Button.args = {
color: "primary",
children: "Button Text",
};
export const Disabled = Template.bind({});
Disabled.args = {
...Button.args,
disabled: true,
};
export const Loading = Template.bind({});
Loading.args = {
...Button.args,
loading: true,
};
export const Icon = Template.bind({});
Icon.args = {
color: "secondary",
StartIcon: Trash2,
size: "icon",
};

View File

@ -0,0 +1,31 @@
import { ComponentMeta } from "@storybook/react";
import { ArrowLeft, ArrowRight, Clipboard, Navigation2, Trash2 } from "react-feather";
import { Button, ButtonGroup } from "@calcom/ui/v2";
export default {
title: "Button Group",
component: ButtonGroup,
} as ComponentMeta<typeof ButtonGroup>;
export const Default = () => (
<ButtonGroup>
<Button StartIcon={Trash2} size="icon" color="secondary" />
<Button StartIcon={Navigation2} size="icon" color="secondary" />
<Button StartIcon={Clipboard} size="icon" color="secondary" />
</ButtonGroup>
);
export const Combined = () => (
<div className="flex flex-col space-y-2">
<ButtonGroup combined>
<Button StartIcon={Trash2} size="icon" color="secondary" combined />
<Button StartIcon={Navigation2} size="icon" color="secondary" combined />
<Button StartIcon={Clipboard} size="icon" color="secondary" combined />
</ButtonGroup>
<ButtonGroup combined>
<Button StartIcon={ArrowLeft} size="icon" color="secondary" combined />
<Button StartIcon={ArrowRight} size="icon" color="secondary" combined />
</ButtonGroup>
</div>
);

View File

@ -0,0 +1,31 @@
import { ComponentMeta } from "@storybook/react";
import { Checkbox } from "@calcom/ui/v2";
export default {
title: "Checkbox",
component: Checkbox,
} as ComponentMeta<typeof Checkbox>;
export const All = () => (
<div className="flex flex-col space-y-2">
<Checkbox label="Default" description="Toggle on and off something" />
<Checkbox label="Error" description="Toggle on and off something" error />
<Checkbox label="Disabled" description="Toggle on and off something" disabled />
<Checkbox label="Disabled Checked" description="Toggle on and off something" checked disabled />
<hr />
<Checkbox description="Default" descriptionAsLabel />
<Checkbox description="Error" descriptionAsLabel error />
<Checkbox description="Disabled" descriptionAsLabel disabled />
<Checkbox description="Disabled" descriptionAsLabel disabled checked />
</div>
);
export const CheckboxField = () => <Checkbox description="Default" descriptionAsLabel />;
export const CheckboxError = () => <Checkbox description="Error" descriptionAsLabel />;
export const CheckboxDisabled = () => (
<>
<Checkbox description="Disabled" descriptionAsLabel disabled />
<Checkbox description="Disabled" descriptionAsLabel disabled checked />
</>
);

View File

@ -0,0 +1,208 @@
import { useState } from "react";
import ColorPicker from "@calcom/ui/v2/colorpicker";
// eslint-disable-next-line import/no-anonymous-default-export
export default {
title: "Colors",
component: ColorPicker,
};
const COLORS = {
brand: {
50: "#f3f3f4",
100: "#e7e8e9",
200: "#c4c5c9",
300: "#a0a3a9",
400: "#585d68",
500: "#111827", // Brand color
600: "#0f1623",
700: "#0d121d",
800: "#0a0e17",
900: "#080c13",
},
gray: {
50: "#F8F8F8",
100: "#F5F5F5",
200: "#E1E1E1",
300: "#CFCFCF",
400: "#ACACAC",
500: "#888888",
600: "#494949",
700: "#3E3E3E",
800: "#313131",
900: "#292929",
},
neutral: {
50: "#F8F8F8",
100: "#F5F5F5",
200: "#E1E1E1",
300: "#CFCFCF",
400: "#ACACAC",
500: "#888888",
600: "#494949",
700: "#3E3E3E",
800: "#313131",
900: "#292929",
},
primary: {
50: "#F4F4F4",
100: "#E8E8E8",
200: "#C6C6C6",
300: "#A3A3A3",
400: "#5F5F5F",
500: "#1A1A1A",
600: "#171717",
700: "#141414",
800: "#101010",
900: "#0D0D0D",
},
secondary: {
50: "#F5F8F7",
100: "#EBF0F0",
200: "#CDDAD9",
300: "#AEC4C2",
400: "#729894",
500: "#356C66",
600: "#30615C",
700: "#28514D",
800: "#20413D",
900: "#223B41",
},
red: {
50: "#FEF2F2",
100: "#FEE2E2",
200: "#FECACA",
300: "#FCA5A5",
400: "#F87171",
500: "#EF4444",
600: "#DC2626",
700: "#B91C1C",
800: "#991B1B",
900: "#7F1D1D",
},
orange: {
50: "#FFF7ED",
100: "#FFEDD5",
200: "#FED7AA",
300: "#FDBA74",
400: "#FB923C",
500: "#F97316",
600: "#EA580C",
700: "#C2410C",
800: "#9A3412",
900: "#7C2D12",
},
green: {
50: "#ECFDF5",
100: "#D1FAE5",
200: "#A7F3D0",
300: "#6EE7B7",
400: "#34D399",
500: "#10B981",
600: "#059669",
700: "#047857",
800: "#065F46",
900: "#064E3B",
},
};
export const All = () => {
return (
<div className="w-full">
<div>
{Object.keys(COLORS.brand).map((color) => (
<div className="flex flex-row space-x-2" key={COLORS.brand[color]}>
<div className="w-full">Brand</div>
<div className="w-full">{color}</div>
<div className="w-full">{COLORS.brand[color]}</div>
<div
style={{
backgroundColor: COLORS.brand[color],
width: "100%",
height: "32px",
}}
/>
</div>
))}
<hr />
{Object.keys(COLORS.gray).map((color) => (
<div className="flex flex-row space-x-2" key={COLORS.gray[color]}>
<div className="w-full">gray</div>
<div className="w-full">{color}</div>
<div className="w-full">{COLORS.gray[color]}</div>
<div
style={{
backgroundColor: COLORS.gray[color],
width: "100%",
height: "32px",
}}
/>
</div>
))}
<hr />
{Object.keys(COLORS.secondary).map((color) => (
<div className="flex flex-row space-x-2" key={COLORS.secondary[color]}>
<div className="w-full">secondary</div>
<div className="w-full">{color}</div>
<div className="w-full">{COLORS.secondary[color]}</div>
<div
style={{
backgroundColor: COLORS.secondary[color],
width: "100%",
height: "32px",
}}
/>
</div>
))}
<hr />
{Object.keys(COLORS.red).map((color) => (
<div className="flex flex-row space-x-2" key={COLORS.red[color]}>
<div className="w-full">red</div>
<div className="w-full">{color}</div>
<div className="w-full">{COLORS.red[color]}</div>
<div
style={{
backgroundColor: COLORS.red[color],
width: "100%",
height: "32px",
}}
/>
</div>
))}
<hr />
{Object.keys(COLORS.orange).map((color) => (
<div className="flex flex-row space-x-2" key={COLORS.orange[color]}>
<div className="w-full">orange</div>
<div className="w-full">{color}</div>
<div className="w-full">{COLORS.orange[color]}</div>
<div
style={{
backgroundColor: COLORS.orange[color],
width: "100%",
height: "32px",
}}
/>
</div>
))}
<hr />
{Object.keys(COLORS.green).map((color) => (
<div className="flex flex-row space-x-2" key={COLORS.green[color]}>
<div className="w-full">green</div>
<div className="w-full">{color}</div>
<div className="w-full">{COLORS.green[color]}</div>
<div
style={{
backgroundColor: COLORS.green[color],
width: "100%",
height: "32px",
}}
/>
</div>
))}
</div>
</div>
);
};
export const ColorPickerComponent = () => {
const [color, setColor] = useState("3B82F6");
return <ColorPicker defaultValue={color} onChange={(val) => setColor(val)} />;
};

View File

@ -0,0 +1,17 @@
import { useState } from "react";
import DatePicker from "@calcom/ui/v2/booker/DatePicker";
export default {
title: "Datepicker",
component: DatePicker,
};
export const Default = () => {
const [selected, setSelected] = useState<Date | undefined>(undefined);
return (
<div style={{ width: "455px" }}>
<DatePicker selected={selected} onChange={setSelected} locale="en" />
</div>
);
};

View File

@ -0,0 +1,26 @@
import { ComponentMeta } from "@storybook/react";
import { Bell } from "react-feather";
import { EmptyScreen as EmptyScreenPattern } from "@calcom/ui/v2";
export default {
title: "pattern/Empty Screen",
component: EmptyScreenPattern,
decorators: [
(Story) => (
<div className="flex items-center justify-center">
<Story />
</div>
),
],
} as ComponentMeta<typeof EmptyScreenPattern>;
export const EmptyScreenS = () => (
<EmptyScreenPattern
Icon={Bell}
headline="Empty State Header"
description="Ullamco dolor nulla sint nulla occaecat aliquip id elit fugiat et excepteur magna. Nisi tempor anim do tempor irure fugiat ad occaecat. Mollit ea eiusmod pariatur sunt deserunt eu eiusmod. Sit reprehenderit cupidatat consequat commodo in aliqua ea et. Et quis sit enim proident dolor mollit consectetur tempor dolore reprehenderit consequat adipisicing reprehenderit officia. Sint eu sunt fugiat laborum Lorem irure aute nulla et. Do non in enim ipsum ea."
buttonText="Veniam ut ipsum"
buttonOnClick={() => console.log("Button Clicked")}
/>
);

View File

@ -0,0 +1,27 @@
import { ComponentMeta } from "@storybook/react";
import { useState } from "react";
import { FormStep } from "@calcom/ui/v2";
export default {
title: "Form Step",
component: FormStep,
} as ComponentMeta<typeof FormStep>;
export const Default = () => {
const STEPS = 4;
const [currentStep, setCurrentStep] = useState(1);
return (
<div className="flex flex-col items-center justify-center space-y-14 p-20">
<div className="w-1/2">
<FormStep steps={STEPS} currentStep={currentStep} />
<div className="flex space-x-2 pt-4">
<button onClick={() => currentStep - 1 > 0 && setCurrentStep((old) => old - 1)}>Previous</button>
<button onClick={() => currentStep + 1 < STEPS + 1 && setCurrentStep((old) => old + 1)}>
Next
</button>
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,68 @@
import { ComponentMeta, ComponentStory } from "@storybook/react";
import { Copy } from "react-feather";
import DatePicker from "@calcom/ui/v2/form/DatePicker";
import { TextAreaField, TextField } from "@calcom/ui/v2/form/fields";
export default {
title: "Inputs",
component: TextField,
argTypes: {
disabled: {
options: [false, true],
},
},
} as ComponentMeta<typeof TextField>;
const TextInputTemplate: ComponentStory<typeof TextField> = (args) => <TextField {...args} />;
// name="demo" label="Demo Label" hint="Hint text"
export const TextInput = TextInputTemplate.bind({});
TextInput.args = {
name: "demo",
label: "Demo Label",
hint: "Hint Text",
disabled: false,
};
export const TextInputPrefix = TextInputTemplate.bind({});
TextInputPrefix.args = {
name: "demo",
label: "Demo Label",
hint: "Hint Text",
addOnLeading: "https://",
disabled: false,
};
export const TextInputSuffix = TextInputTemplate.bind({});
TextInputSuffix.args = {
name: "demo",
label: "Demo Label",
hint: "Hint Text",
addOnSuffix: "Minutes",
disabled: false,
};
export const TextInputPrefixIcon = TextInputTemplate.bind({});
TextInputPrefixIcon.args = {
name: "demo",
label: "Demo Label",
hint: "Hint Text",
addOnFilled: false,
addOnSuffix: <Copy />,
disabled: false,
};
export const TextInputSuffixIcon = TextInputTemplate.bind({});
TextInputSuffixIcon.args = {
name: "demo",
label: "Demo Label",
hint: "Hint Text",
addOnFilled: false,
addOnLeading: <Copy />,
disabled: false,
};
export const TextAreaInput: ComponentStory<typeof TextAreaField> = () => (
<TextAreaField name="Text-area-input" label="Text Area" />
);
export const DatePickerInput: ComponentStory<typeof DatePicker> = () => <DatePicker date={new Date()} />;

View File

@ -0,0 +1,52 @@
import { useState } from "react";
import { Info } from "react-feather";
import { Button } from "@calcom/ui/v2";
import { Dialog, DialogContent, DialogTrigger } from "@calcom/ui/v2/Dialog";
import { TextField } from "@calcom/ui/v2/form/fields";
export default {
title: "pattern/Modal",
component: Dialog,
};
export const Creation = () => {
const [open, setOpen] = useState(false);
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button onClick={() => setOpen(true)}>Open Modal</Button>
</DialogTrigger>
<DialogContent
title="Header"
description="Optional Description"
type="creation"
actionText="Create"
actionOnClick={() => setOpen(false)}>
<TextField name="Label" />
<TextField name="Label" />
<TextField name="Label" />
<TextField name="Label" />
</DialogContent>
</Dialog>
);
};
export const Confirmation = () => {
const [open, setOpen] = useState(false);
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button>Open Modal</Button>
</DialogTrigger>
<DialogContent
title="Header"
description="Optional Description"
type="confirmation"
actionText="Confirm"
Icon={Info}
actionOnClick={() => setOpen(false)}
/>
</Dialog>
);
};

View File

@ -0,0 +1,25 @@
import { ComponentMeta } from "@storybook/react";
import { Toaster } from "react-hot-toast";
import { Button } from "@calcom/ui/v2";
import showToast from "@calcom/ui/v2/notfications";
export default {
title: "Notifcations-Toasts",
decorators: [
(Story) => (
<>
<Story />
<Toaster />
</>
),
],
} as ComponentMeta<typeof All>; // We have to fake this type as the story for this component isn't really a component.
export const All = () => (
<div className="flex space-x-2">
<Button onClick={() => showToast("This is a Neutral toast", "warning")}>Neutral</Button>
<Button onClick={() => showToast("This is a Success toast", "success")}>Success</Button>
<Button onClick={() => showToast("This is a Error toast", "error")}>Error</Button>
</div>
);

View File

@ -0,0 +1,52 @@
import { ComponentMeta } from "@storybook/react";
import { Search } from "react-feather";
import { Button, PageHeader } from "@calcom/ui/v2";
import { TextField } from "@calcom/ui/v2/form";
export default {
title: "pattern/Page Header",
component: PageHeader,
decorators: [
(Story) => (
<div className="">
<Story />
</div>
),
],
} as ComponentMeta<typeof PageHeader>;
export const Default = () => (
<PageHeader title="Title" description="Some description about the header above" />
);
export const ButtonRight = () => (
<PageHeader
title="Title"
description="Some description about the header above"
rightAlignedComponent={<Button>Button Text</Button>}
/>
);
export const ComingSoon = () => (
<PageHeader
title="Title"
description="Some description about the header above"
badgeText="Coming Soon"
badgeVariant="gray"
/>
);
export const SearchInstalledApps = () => (
<PageHeader
title="Search booking"
description="See upcoming and past events booking through your event type link"
rightAlignedComponent={
<TextField
containerClassName="h-9 max-h-9"
addOnLeading={<Search />}
name="search"
labelSrOnly
placeholder="WIP"
/>
}
/>
);

View File

@ -0,0 +1,19 @@
import * as Radio from "@calcom/ui/v2/form/radio-area/Radio";
import { RadioField } from "@calcom/ui/v2/form/radio-area/Radio";
export default {
title: "Radio",
component: RadioField,
};
export const RadioGroupDemo = () => {
return (
<form>
<Radio.Group aria-label="View density" defaultValue="default">
<RadioField label="Default" id="r1" value="1" />
<RadioField label="Next" id="r2" value="2" />
<RadioField label="Disabled" id="r3" disabled value="1" />
</Radio.Group>
</form>
);
};

View File

@ -0,0 +1,10 @@
import { ComponentMeta } from "@storybook/react";
import { Select } from "@calcom/ui/v2";
export default {
title: "Form/Select",
component: Select,
} as ComponentMeta<typeof Select>;
export const Single = () => <Select items={[{ value: "Test Test" }]} />;

View File

@ -0,0 +1,15 @@
import { ComponentMeta } from "@storybook/react";
import { Switch } from "@calcom/ui/v2";
export default {
title: "Switch",
component: Switch,
} as ComponentMeta<typeof Switch>;
export const All = () => (
<div className="flex flex-col space-y-2">
<p>Checked works in app but storybook doesnt like it</p>
<Switch label="Default" />
</div>
);

View File

@ -0,0 +1,67 @@
// Disabling until we figure out what is happening with the RouterMock.
import { ComponentMeta } from "@storybook/react";
import { Link } from "react-feather";
import { VerticalTabItem } from "@calcom/ui/v2";
export default {
title: "VerticalTabItem",
component: VerticalTabItem,
} as ComponentMeta<typeof VerticalTabItem>;
const TabItemProps = {
name: "Event Setup",
icon: Link,
href: "/settings/event",
info: "60 mins, Zoom",
};
// export const Default = () => (
// <div className="h-20 w-full bg-gray-100 p-4">
// <VerticalTabItem {...TabItemProps}></VerticalTabItem>
// </div>
// );
// export const Active = () => (
// <div className="h-20 w-full bg-gray-100 p-4">
// <VerticalTabItem {...TabItemProps}></VerticalTabItem>
// </div>
// );
// Mocking next router to show active state
// Active.parameters = {
// nextRouter: {
// path: "/settings/[tab]",
// asPath: "/settings/event",
// },
// };
// export const Disabled = () => (
// <div className="h-20 w-full bg-gray-100 p-4">
// <VerticalTabItem {...TabItemProps} disabled></VerticalTabItem>
// </div>
// );
// export const NoIconNoInfo = () => (
// <div className="h-20 w-full bg-gray-100 p-4">
// <VerticalTabItem name="Event Setup" href="/settings/event"></VerticalTabItem>
// </div>
// );
// export const IconNoInfo = () => (
// <div className="h-20 w-full bg-gray-100 p-4">
// <VerticalTabItem name="Event Setup" href="/settings/event" icon={Link}></VerticalTabItem>
// </div>
// );
// export const IconNoInfoActive = () => (
// <div className="h-20 w-full bg-gray-100 p-4">
// <VerticalTabItem name="Event Setup" href="/settings/events" icon={Link}></VerticalTabItem>
// </div>
// );
// IconNoInfoActive.paramaters = {
// nextRouter: {
// path: "/settings/[tab]",
// asPath: "/settings/events",
// },
// };

View File

@ -0,0 +1,85 @@
// Disabling until we figure out what is happening with the RouterMock.
import { ComponentMeta } from "@storybook/react";
import { Calendar, Clock, Grid, Link, RefreshCw, User, Users } from "react-feather";
import { VerticalTabs } from "@calcom/ui/v2";
export default {
title: "Vertical Tabs",
component: VerticalTabs,
} as ComponentMeta<typeof VerticalTabs>;
const VerticalTabsPropsDefault = [
{
name: "Security",
icon: Link,
href: "/security",
},
{
name: "Profile",
icon: User,
href: "/profile",
},
{
name: "Calendar Sync",
icon: RefreshCw,
href: "/cal-sync",
},
{
name: "Teams",
icon: Users,
href: "/Teams",
},
];
// export const Default = () => (
// <div className="w-full p-4" style={{ backgroundColor: "#F9FAFB" }}>
// <VerticalTabs tabs={VerticalTabsPropsDefault}></VerticalTabs>
// </div>
// );
// Default.parameters = {
// nextRouter: {
// path: "/[page]",
// asPath: "/profile",
// },
// };
const VerticalTabsPropsInfo = [
{
name: "Event Setup",
icon: Link,
href: "/events",
info: "60 Mins, Zoom",
},
{
name: "Availability",
icon: Calendar,
href: "/availability",
info: "Working Hours",
},
{
name: "Limits",
icon: Clock,
href: "/limits",
info: "Buffers, Limits & more...",
},
{
name: "Apps",
icon: Grid,
href: "/apps",
info: "3 apps - 0 active",
},
];
// export const Info = () => (
// <div className="w-full p-4" style={{ backgroundColor: "#F9FAFB" }}>
// <VerticalTabs tabs={VerticalTabsPropsInfo}></VerticalTabs>
// </div>
// );
// Info.parameters = {
// nextRouter: {
// path: "/[page]",
// asPath: "/events",
// },
// };

View File

@ -0,0 +1,13 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--brand-color: #292929;
--brand-text-color: #ffffff;
--brand-color-dark-mode: #fafafa;
--brand-text-color-dark-mode: #292929;
font-family: 'Inter', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

View File

@ -0,0 +1,13 @@
const base = require("@calcom/config/tailwind-preset");
// const colorGen = require('@calcom/config/ColorPalletGenerator')
module.exports = {
...base,
content: ["../../packages/ui/**/*.{js,ts,jsx,tsx}", "./stories/**/*.{js,ts,tsx,jsx}"],
// theme: { . ** Figure this out doesnt seem to work in current state
// extends: {
// color: {
// brand: colorGen("#111827"),
// },
// },
// },
};

9
apps/storybook/tsconfig.json Executable file
View File

@ -0,0 +1,9 @@
{
"extends": "@calcom/tsconfig/nextjs.json",
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"],
"compilerOptions": {
"noImplicitAny": false,
"baseUrl": "."
}
}

View File

@ -143,7 +143,7 @@ function MyComponent() {
const cal = await getCalApi();
${uiInstructionCode}
})();
}, [])
}, [])
return <Cal calLink="${calLink}" style={{width:"${width}",height:"${height}",overflow:"scroll"}} />;
};`;
},
@ -164,7 +164,7 @@ function MyComponent() {
Cal("floatingButton", ${floatingButtonArg});
${uiInstructionCode}
})();
}, [])
}, [])
};`;
},
"element-click": ({ calLink, uiInstructionCode }: { calLink: string; uiInstructionCode: string }) => {
@ -177,7 +177,7 @@ function MyComponent() {
const cal = await getCalApi();
${uiInstructionCode}
})();
}, [])
}, [])
return <button data-cal-link="${calLink}" />;
};`;
},

View File

@ -22,6 +22,7 @@ import React, { Fragment, ReactNode, useEffect, useState } from "react";
import { Toaster } from "react-hot-toast";
import { useIsEmbed } from "@calcom/embed-core/embed-iframe";
import CustomBranding from "@calcom/lib/CustomBranding";
import { WEBAPP_URL, JOIN_SLACK, ROADMAP } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
@ -41,7 +42,6 @@ import classNames from "@lib/classNames";
import { shouldShowOnboarding } from "@lib/getting-started";
import useMeQuery from "@lib/hooks/useMeQuery";
import CustomBranding from "@components/CustomBranding";
import { KBarRoot, KBarContent, KBarTrigger } from "@components/Kbar";
import Loader from "@components/Loader";
import { HeadSeo } from "@components/seo/head-seo";

View File

@ -25,11 +25,12 @@ import { z } from "zod";
import { AppStoreLocationType, LocationObject, LocationType } from "@calcom/app-store/locations";
import dayjs, { Dayjs } from "@calcom/dayjs";
import {
useEmbedNonStylesConfig,
useEmbedStyles,
useIsBackgroundTransparent,
useIsEmbed,
useEmbedStyles,
useEmbedNonStylesConfig,
useIsBackgroundTransparent,
} from "@calcom/embed-core/embed-iframe";
import CustomBranding from "@calcom/lib/CustomBranding";
import classNames from "@calcom/lib/classNames";
import { CAL_URL, WEBSITE_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
@ -46,7 +47,6 @@ import { isBrandingHidden } from "@lib/isBrandingHidden";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry";
import { detectBrowserTimeFormat } from "@lib/timeFormat";
import CustomBranding from "@components/CustomBranding";
import AvailableTimes from "@components/booking/AvailableTimes";
import TimeOptions from "@components/booking/TimeOptions";
import { HeadSeo } from "@components/seo/head-seo";

View File

@ -30,6 +30,7 @@ import {
useIsBackgroundTransparent,
useIsEmbed,
} from "@calcom/embed-core/embed-iframe";
import CustomBranding from "@calcom/lib/CustomBranding";
import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { HttpError } from "@calcom/lib/http-error";
@ -50,7 +51,6 @@ import { parseDate, parseRecurringDates } from "@lib/parseDate";
import slugify from "@lib/slugify";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry";
import CustomBranding from "@components/CustomBranding";
import AvatarGroup from "@components/ui/AvatarGroup";
import type PhoneInputType from "@components/ui/form/PhoneInput";

View File

@ -2,8 +2,8 @@ import { useCallback, useRef, useState } from "react";
import { useEffect } from "react";
import { HexColorInput, HexColorPicker } from "react-colorful";
import { isValidHexCode, fallBackHex } from "@components/CustomBranding";
import Swatch from "@components/Swatch";
import { isValidHexCode, fallBackHex } from "@calcom/lib/CustomBranding";
import Swatch from "@calcom/ui/v2/Swatch";
type Handler = (event: MouseEvent | Event) => void;
function useEventListener<
@ -87,7 +87,7 @@ const ColorPicker = (props: ColorPickerProps) => {
</div>
)}
<HexColorInput
className="ml-1 block w-full rounded-sm border border-gray-300 px-3 py-2 sm:text-sm"
className="ml-1 block w-full rounded-r-md border border-gray-300 px-3 py-2 sm:text-sm"
color={color}
onChange={(val) => {
setColor(val);

View File

@ -2,23 +2,22 @@ import React, { Dispatch, SetStateAction } from "react";
import { components, GroupBase, OptionProps } from "react-select";
import { Props } from "react-select";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import Select from "@calcom/ui/form/Select";
import { useLocale } from "@lib/hooks/useLocale";
export type Option = {
value: string;
label: string;
};
const InputOption = ({
const InputOption: React.FC<OptionProps<any, boolean, GroupBase<any>>> = ({
isDisabled,
isFocused,
isSelected,
children,
innerProps,
...rest
}: OptionProps<any, boolean, GroupBase<any>>) => {
}) => {
const style = {
alignItems: "center",
backgroundColor: isFocused ? "rgba(244, 245, 246, var(--tw-bg-opacity))" : "transparent",

View File

@ -16,6 +16,7 @@ import {
useEmbedStyles,
useIsEmbed,
} from "@calcom/embed-core/embed-iframe";
import CustomBranding from "@calcom/lib/CustomBranding";
import defaultEvents, {
getDynamicEventDescription,
getGroupName,
@ -23,7 +24,7 @@ import defaultEvents, {
getUsernameSlugLink,
} from "@calcom/lib/defaultEvents";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { baseEventTypeSelect } from "@calcom/prisma/selects";
import { baseEventTypeSelect } from "@calcom/prisma/selects/event-types";
import { useExposePlanGlobally } from "@lib/hooks/useExposePlanGlobally";
import useTheme from "@lib/hooks/useTheme";
@ -31,7 +32,6 @@ import prisma from "@lib/prisma";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry";
import { inferSSRProps } from "@lib/types/inferSSRProps";
import CustomBranding from "@components/CustomBranding";
import AvatarGroup from "@components/ui/AvatarGroup";
import { AvatarSSR } from "@components/ui/AvatarSSR";

View File

@ -6,6 +6,7 @@ import { useState } from "react";
import z from "zod";
import dayjs from "@calcom/dayjs";
import CustomBranding from "@calcom/lib/CustomBranding";
import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { parseRecurringEvent } from "@calcom/lib/isRecurringEvent";
@ -19,7 +20,6 @@ import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/t
import { detectBrowserTimeFormat } from "@lib/timeFormat";
import { inferSSRProps } from "@lib/types/inferSSRProps";
import CustomBranding from "@components/CustomBranding";
import { HeadSeo } from "@components/seo/head-seo";
import { ssrInit } from "@server/lib/ssr";

View File

@ -19,6 +19,7 @@ import {
useIsEmbed,
} from "@calcom/embed-core/embed-iframe";
import { parseRecurringEvent } from "@calcom/lib";
import CustomBranding from "@calcom/lib/CustomBranding";
import { WEBSITE_URL } from "@calcom/lib/constants";
import { getDefaultEvent } from "@calcom/lib/defaultEvents";
import { useLocale } from "@calcom/lib/hooks/useLocale";
@ -38,7 +39,6 @@ import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/t
import { isBrowserLocale24h } from "@lib/timeFormat";
import { inferSSRProps } from "@lib/types/inferSSRProps";
import CustomBranding from "@components/CustomBranding";
import CancelBooking from "@components/booking/CancelBooking";
import { HeadSeo } from "@components/seo/head-seo";

View File

@ -9,4 +9,4 @@ You can run all jest tests as
You can run tests matching specific description by following command
`yarn test -t getSchedule`
Tip: Use `--watchAll` flag to run tests on every change
Tip: Use `--watchAll` flag to run tests on every change

@ -1 +1 @@
Subproject commit e567969feb55ebcedb3061fc156a2e9bc7d4fd03
Subproject commit 4ca4a62771c2c9174014668ca6557f8ff8a95bf6

View File

@ -21,7 +21,7 @@
"ink-select-input": "^4.2.1",
"ink-text-input": "^4.0.3",
"meow": "^9.0.0",
"react": "^17.0.2"
"react": "18.1.0"
},
"devDependencies": {
"chokidar": "^3.5.3",

View File

@ -0,0 +1,163 @@
const tailwindcssPaletteGenerator = (input) => {
// addColor function
const addColor = (color) => {
let colorParams = {
hex: "",
name: "",
shades: params.shades,
};
// check input params
if (typeof color === "string") colorParams.hex = color;
if (typeof color === "object" && Array.isArray(color)) {
colorParams.name = color.shift();
colorParams.hex = color.shift();
}
if (typeof color === "object" && !Array.isArray(color)) {
if (Object.keys(color).length === 1) {
colorParams.name = Object.keys(color)[0];
colorParams.hex = Object.values(color)[0];
}
if (Object.keys(color).length !== 1) colorParams = Object.assign(colorParams, color);
}
// check if string is invalid
if (!/^#?([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$/.test(colorParams.hex)) return;
// add hash tag if needed
if (!/^#/.test(colorParams.hex)) colorParams.hex = "#" + colorParams.hex;
// if no name present, get a default name
if (colorParams.name === "") colorParams.name = params.colorNames.shift();
// set palette name
palette[colorParams.name] = {};
// generate shades
Object.keys(colorParams.shades).forEach((shade) => {
palette[colorParams.name][shade] =
colorParams.shades[shade].type === "lighten"
? lighten(colorParams.hex, colorParams.shades[shade].intensity)
: darken(colorParams.hex, colorParams.shades[shade].intensity);
});
};
// darken function
const darken = (hex, intensity) => {
// get r, g, b values
let { r, g, b } = hexToRgb(hex);
// darken the r, g, b values
r = Math.round(r * (1 - intensity));
g = Math.round(g * (1 - intensity));
b = Math.round(b * (1 - intensity));
// return the new hex color
return rgbToHex(r, g, b);
};
// lighten function
const lighten = (hex, intensity) => {
// get r, g, b values
let { r, g, b } = hexToRgb(hex);
// lighten the r, g, b values
r = Math.round(r + (255 - r) * intensity);
g = Math.round(g + (255 - g) * intensity);
b = Math.round(b + (255 - b) * intensity);
// return the new hex color
return rgbToHex(r, g, b);
};
// hexToRgb function
const hexToRgb = (string) => {
// get the r,g,b values
const [r, g, b] = string
.replace("#", "")
.match(/.{1,2}/g)
.map((a) => parseInt(a, 16));
return { r, g, b };
};
// rgbToHex function
const rgbToHex = (r, g, b) => `#${toHex(r)}${toHex(g)}${toHex(b)}`;
// toHex function
const toHex = (n) => `0${n.toString(16)}`.slice(-2).toUpperCase();
// initiate palette
const palette = {};
// set default params
let params = {
colors: [],
colorNames: [
"primary",
"secondary",
"tertiary",
"quaternary",
"quinary",
"senary",
"septenary",
"octonary",
"nonary",
"denary",
],
shades: {
50: {
intensity: 0.95,
type: "lighten",
},
100: {
intensity: 0.9,
type: "lighten",
},
200: {
intensity: 0.75,
type: "lighten",
},
300: {
intensity: 0.6,
type: "lighten",
},
400: {
intensity: 0.3,
type: "lighten",
},
500: {
intensity: 0,
type: "lighten",
},
600: {
intensity: 0.1,
type: "darken",
},
700: {
intensity: 0.25,
type: "darken",
},
800: {
intensity: 0.4,
type: "darken",
},
900: {
intensity: 0.51,
type: "darken",
},
},
};
// check input params
if (typeof input === "string") params.colors.push(input);
if (typeof input === "object" && Array.isArray(input)) params.colors = input;
if (typeof input === "object" && !Array.isArray(input)) params = Object.assign(params, input);
// loop through colors
params.colors.forEach(addColor);
return palette;
};
module.exports = tailwindcssPaletteGenerator;

View File

@ -15,7 +15,6 @@ module.exports = {
extend: {
colors: {
/* your primary brand color */
brand: "var(--brand-color)",
brandcontrast: "var(--brand-text-color)",
darkmodebrand: "var(--brand-color-dark-mode)",
darkmodebrandcontrast: "var(--brand-text-color-dark-mode)",
@ -27,17 +26,32 @@ module.exports = {
bookingdarker: "var(--booking-darker-color)",
bookinghighlight: "var(--booking-highlight-color)",
black: "#111111",
brand: {
// Figure out a way to automate this for self hosted users
// Goto https://javisperez.github.io/tailwindcolorshades to generate your brand color
50: "#d1d5db",
100: "#9ca3af",
200: "#6b7280",
300: "#4b5563",
400: "#374151",
500: "#111827", // Brand color
600: "#0f1623",
700: "#0d121d",
800: "#0a0e17",
900: "#080c13",
DEFAULT: "#111827",
},
gray: {
50: "#F8F8F8",
100: "#F5F5F5",
200: "#E1E1E1",
300: "#CFCFCF",
400: "#ACACAC",
500: "#888888",
600: "#494949",
700: "#3E3E3E",
800: "#313131",
900: "#292929",
50: "#F9FAFB",
100: "#F3F4F6",
200: "#E5E7EB",
300: "#D1D5DB",
400: "#9CA3AF",
500: "#6B7280",
600: "#4B5563",
700: "#374151",
800: "#1F2937",
900: "#111827",
},
neutral: {
50: "#F8F8F8",
@ -76,44 +90,56 @@ module.exports = {
900: "#223B41",
},
red: {
50: "#FEF2F2",
100: "#FEE2E2",
200: "#FECACA",
300: "#FCA5A5",
400: "#F87171",
500: "#EF4444",
600: "#DC2626",
700: "#B91C1C",
800: "#991B1B",
900: "#7F1D1D",
50: "#FFF5F5",
100: "#FFE3E2",
200: "#FFC9C9",
300: "#FEA8A8",
400: "#FF8787",
500: "#FF6B6B",
600: "#FA5352",
700: "#F03E3F",
800: "#E03130",
900: "#C92B2B",
},
orange: {
50: "#FFF7ED",
100: "#FFEDD5",
200: "#FED7AA",
300: "#FDBA74",
400: "#FB923C",
500: "#F97316",
600: "#EA580C",
700: "#C2410C",
800: "#9A3412",
900: "#7C2D12",
50: "#FFF4E5",
100: "#FFE8CC",
200: "#FFD8A8",
300: "#FFBF78",
400: "#FFA94E",
500: "#FF922B",
600: "#FD7E14",
700: "#F76706",
800: "#E8580C",
900: "#D94810",
},
green: {
50: "#ECFDF5",
100: "#D1FAE5",
200: "#A7F3D0",
300: "#6EE7B7",
400: "#34D399",
500: "#10B981",
600: "#059669",
700: "#047857",
800: "#065F46",
900: "#064E3B",
50: "#EBFCEE",
100: "#D2F9D9",
200: "#B1F2BA",
300: "#8CE99A",
400: "#69DB7C",
500: "#51CF66",
600: "#40C057",
700: "#36B24D",
800: "#2F9E44",
900: "#2B8A3E",
},
blue: {
50: "#E7F5FF",
100: "#D0EBFF",
200: "#A4D8FF",
300: "#74C0FC",
400: "#4DABF7",
500: "#339AF0",
600: "#238BE6",
700: "#1C7ED7",
800: "#1971C2",
900: "#1763AB",
},
},
fontFamily: {
cal: ["Cal Sans", "sans-serif"],
cal: ["inter", "sans-serif"],
mono: ["Roboto Mono", "monospace"],
},
maxHeight: (theme) => ({

View File

@ -40,6 +40,6 @@ const Cal = function Cal(props: CalProps) {
return <div {...restProps}>Loading {calLink} </div>;
}
return <div ref={ref} {...restProps}></div>;
return <div ref={ref} {...restProps} />;
};
export default Cal;

View File

@ -0,0 +1,11 @@
import crypto from "crypto";
export const defaultAvatarSrc = function ({ email, md5 }: { md5?: string; email?: string }) {
if (!email && !md5) return "";
if (email && !md5) {
md5 = crypto.createHash("md5").update(email).digest("hex");
}
return `https://www.gravatar.com/avatar/${md5}?s=160&d=identicon&r=PG`;
};

View File

@ -121,17 +121,12 @@ export const Button = forwardRef<HTMLAnchorElement | HTMLButtonElement, ButtonPr
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24">
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"></circle>
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
</div>
)}

View File

@ -1,7 +1,7 @@
export default function Loader() {
return (
<div className="loader border-brand dark:border-darkmodebrand">
<span className="loader-inner bg-brand dark:bg-darkmodebrand"></span>
<span className="loader-inner bg-brand dark:bg-darkmodebrand" />
</div>
);
}

View File

@ -34,7 +34,7 @@ export function Label(props: JSX.IntrinsicElements["label"]) {
export function InputLeading(props: JSX.IntrinsicElements["div"]) {
return (
<span className="inline-flex items-center flex-shrink-0 px-3 text-gray-500 border border-r-0 border-gray-300 rounded-l-sm bg-gray-50 sm:text-sm">
<span className="inline-flex flex-shrink-0 items-center rounded-l-sm border border-r-0 border-gray-300 bg-gray-50 px-3 text-gray-500 sm:text-sm">
{props.children}
</span>
);
@ -71,7 +71,7 @@ const InputField = forwardRef<HTMLInputElement, InputFieldProps>(function InputF
</Label>
)}
{addOnLeading ? (
<div className="flex mt-1 rounded-md shadow-sm">
<div className="mt-1 flex rounded-md shadow-sm">
{addOnLeading}
<Input
id={id}

View File

@ -4,9 +4,19 @@
"main": "./index.tsx",
"types": "./index.tsx",
"license": "MIT",
"scripts": {
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"lint:report": "eslint . --format json --output-file ../../lint-results/ui.json"
},
"dependencies": {
"@radix-ui/react-dialog": "^0.1.0",
"next": "^12.2.0"
"@radix-ui/react-select": "^0.1.1",
"downshift": "^6.1.7",
"next": "^12.2.0",
"react-colorful": "^5.5.1",
"react-feather": "^2.0.10",
"react-select": "^5.4.0"
},
"devDependencies": {
"@calcom/config": "*",

48
packages/ui/v2/Alert.tsx Normal file
View File

@ -0,0 +1,48 @@
import { CheckCircleIcon, ExclamationIcon, InformationCircleIcon, XCircleIcon } from "@heroicons/react/solid";
import classNames from "classnames";
import { ReactNode } from "react";
export interface AlertProps {
title?: ReactNode;
message?: ReactNode;
actions?: ReactNode;
className?: string;
severity: "success" | "warning" | "error" | "info";
}
export function Alert(props: AlertProps) {
const { severity } = props;
return (
<div
className={classNames(
"rounded-sm border border-opacity-20 p-3",
props.className,
severity === "error" && "border-red-900 bg-red-50 text-red-800",
severity === "warning" && "border-yellow-700 bg-yellow-50 text-yellow-700",
severity === "info" && "border-sky-700 bg-sky-50 text-sky-700",
severity === "success" && "bg-gray-900 text-white"
)}>
<div className="flex">
<div className="flex-shrink-0">
{severity === "error" && (
<XCircleIcon className={classNames("h-5 w-5 text-red-400")} aria-hidden="true" />
)}
{severity === "warning" && (
<ExclamationIcon className={classNames("h-5 w-5 text-yellow-400")} aria-hidden="true" />
)}
{severity === "info" && (
<InformationCircleIcon className={classNames("h-5 w-5 text-sky-400")} aria-hidden="true" />
)}
{severity === "success" && (
<CheckCircleIcon className={classNames("h-5 w-5 text-gray-400")} aria-hidden="true" />
)}
</div>
<div className="ml-3 flex-grow">
<h3 className="text-sm font-medium">{props.title}</h3>
<div className="text-sm">{props.message}</div>
</div>
{props.actions && <div className="text-sm">{props.actions}</div>}
</div>
</div>
);
}

57
packages/ui/v2/Avatar.tsx Normal file
View File

@ -0,0 +1,57 @@
import * as AvatarPrimitive from "@radix-ui/react-avatar";
import * as Tooltip from "@radix-ui/react-tooltip";
import { Check } from "react-feather";
import classNames from "@calcom/lib/classNames";
import { defaultAvatarSrc } from "@calcom/lib/defaultAvatarImage";
import { Maybe } from "@trpc/server";
export type AvatarProps = {
className?: string;
size: "sm" | "lg";
imageSrc?: Maybe<string>;
title?: string;
alt: string;
gravatarFallbackMd5?: string;
accepted?: boolean;
};
export default function Avatar(props: AvatarProps) {
const { imageSrc, gravatarFallbackMd5, size, alt, title } = props;
const rootClass = classNames("rounded-full", props.size === "sm" ? "w-6" : "w-16", "h-auto");
const avatar = (
<AvatarPrimitive.Root className={classNames("relative inline-block overflow-hidden ")}>
<AvatarPrimitive.Image src={imageSrc ?? undefined} alt={alt} className={rootClass} />
<AvatarPrimitive.Fallback delayMs={600}>
{gravatarFallbackMd5 && (
// eslint-disable-next-line @next/next/no-img-element
<img src={defaultAvatarSrc({ md5: gravatarFallbackMd5 })} alt={alt} className={rootClass} />
)}
</AvatarPrimitive.Fallback>
{props.accepted && (
<div
className={classNames(
"absolute bottom-0 right-0 block rounded-full bg-green-400 text-white ring-2 ring-white",
size === "lg" ? "h-5 w-5" : "h-2 w-2"
)}>
<div className="flex h-full items-center justify-center p-[2px]">
{size === "lg" && <Check className="" />}
</div>
</div>
)}
</AvatarPrimitive.Root>
);
return title ? (
<Tooltip.Tooltip delayDuration={300}>
<Tooltip.TooltipTrigger className="cursor-default">{avatar}</Tooltip.TooltipTrigger>
<Tooltip.Content className="rounded-sm bg-black p-2 text-sm text-white shadow-sm">
<Tooltip.Arrow />
{title}
</Tooltip.Content>
</Tooltip.Tooltip>
) : (
<>{avatar}</>
);
}

View File

@ -0,0 +1,64 @@
import classNames from "@calcom/lib/classNames";
import Avatar from "./Avatar";
export type AvatarGroupProps = {
size: "sm" | "lg";
items: {
image: string;
title?: string;
alt?: string;
}[];
className?: string;
accepted?: boolean;
};
export const AvatarGroup = function AvatarGroup(props: AvatarGroupProps) {
const avatars = props.items.slice(0, 4);
const LENGTH = props.items.length;
return (
<ul className={classNames(props.className)}>
{avatars.map((item, enumerator) => {
if (item.image != null) {
if (LENGTH > 4 && enumerator === 3) {
return (
<li key={enumerator} className="relative -mr-4 inline-block ">
<div className="relative overflow-hidden">
<Avatar
className="h-90 relative min-w-full scale-105 transform border-gray-200 object-cover blur filter"
imageSrc={item.image}
alt={item.alt || ""}
size={props.size}
/>
</div>
<div
className={classNames(
"absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-white",
props.size === "sm" ? "text-base" : "text-2xl"
)}>
<span>+{LENGTH - 3}</span>
</div>
</li>
);
}
// Always display the first Four items items
return (
<li key={enumerator} className="-mr-4 inline-block">
<Avatar
className="border-gray-200"
imageSrc={item.image}
title={item.title}
alt={item.alt || ""}
accepted={props.accepted}
size={props.size}
/>
</li>
);
}
})}
</ul>
);
};
export default AvatarGroup;

48
packages/ui/v2/Badge.tsx Normal file
View File

@ -0,0 +1,48 @@
import { Icon } from "react-feather";
import classNames from "@calcom/lib/classNames";
export const badgeClassNameByVariant = {
default: "bg-orange-100 text-orange-800",
warning: "bg-orange-100 text-orange-800",
orange: "bg-orange-100 text-orange-800",
success: "bg-green-100 text-green-800",
green: "bg-green-100 text-green-800",
gray: "bg-gray-200 text-gray-800",
blue: "bg-blue-100 text-blue-800",
red: "bg-red-100 text-red-800",
error: "bg-red-100 text-red-800",
};
const classNameBySize = {
default: "h-5",
lg: "h-6",
};
export type BadgeProps = {
variant: keyof typeof badgeClassNameByVariant;
size?: keyof typeof classNameBySize;
StartIcon?: Icon;
} & JSX.IntrinsicElements["div"];
export const Badge = function Badge(props: BadgeProps) {
const { variant, className, size = "default", StartIcon, ...passThroughProps } = props;
return (
<div
{...passThroughProps}
className={classNames(
"inline-flex items-center justify-center rounded py-0.5 px-[6px] text-sm font-semibold",
!StartIcon ? classNameBySize[size] : "",
badgeClassNameByVariant[variant],
className
)}>
<>
{StartIcon && <StartIcon className="mr-1 h-3 w-3 stroke-[3px]" />}
{props.children}
</>
</div>
);
};
export default Badge;

View File

@ -0,0 +1,69 @@
import Link from "next/link";
import { useRouter } from "next/router";
import { Children, Fragment, useEffect, useState } from "react";
type BreadcrumbProps = {
children: React.ReactNode;
};
export const Breadcrumb = ({ children }: BreadcrumbProps) => {
const childrenArray = Children.toArray(children);
const childrenSeperated = childrenArray.map((child, index) => {
// If not the last item in the array insert a /
if (index !== childrenArray.length - 1) {
return (
<Fragment key={index}>
{child}
<span>/</span>
</Fragment>
);
}
// Else return just the child
return child;
});
return (
<nav className=" mx-8 mt-8 md:mx-16 lg:mx-32">
<ol className="flex items-center space-x-4">{childrenSeperated}</ol>
</nav>
);
};
type BreadcrumbItemProps = {
children: React.ReactNode;
href: string;
listProps?: JSX.IntrinsicElements["li"];
};
export const BreadcrumbItem = ({ children, href, listProps }: BreadcrumbItemProps) => {
return (
<li {...listProps}>
<Link href={href}>
<a>{children}</a>
</Link>
</li>
);
};
export const BreadcrumbContainer = () => {
const router = useRouter();
const [breadcrumbs, setBreadcrumbs] = useState<{ href: string; label: string }[]>();
useEffect(() => {
const rawPath = router.asPath.split("?")[0]; // this will ignore any query params for now?
let pathArray = rawPath.split("/");
pathArray.shift();
pathArray = pathArray.filter((path) => path !== "");
const allBreadcrumbs = pathArray.map((path, idx) => {
const href = `/${pathArray.slice(0, idx + 1).join("/")}`;
return {
href,
label: path.charAt(0).toUpperCase() + path.slice(1),
};
});
setBreadcrumbs(allBreadcrumbs);
}, []);
};

147
packages/ui/v2/Button.tsx Normal file
View File

@ -0,0 +1,147 @@
import Link, { LinkProps } from "next/link";
import React, { forwardRef } from "react";
import { Icon } from "react-feather";
import classNames from "@calcom/lib/classNames";
import { Tooltip } from "./Tooltip";
export type ButtonBaseProps = {
/* Primary: Signals most important actions at any given point in the application.
Secondary: Gives visual weight to actions that are important
Minimal: Used for actions that we want to give very little significane to */
color?: keyof typeof variantClassName;
/**Default: H = 36px (default)
Large: H = 38px (Onboarding, modals)
Icon: Makes the button be an icon button */
size?: "base" | "lg" | "icon";
/**Signals the button is loading */
loading?: boolean;
/** Disables the button from being clicked */
disabled?: boolean;
/** Action that happens when the button is clicked */
onClick?: (event: React.MouseEvent<HTMLElement, MouseEvent>) => void;
/**Left aligned icon*/
StartIcon?: Icon;
/**Right aligned icon */
EndIcon?: Icon;
shallow?: boolean;
/**Tool tip used when icon size is set to small */
tooltip?: string;
combined?: boolean;
};
export type ButtonProps = ButtonBaseProps &
(
| (Omit<JSX.IntrinsicElements["a"], "href" | "onClick"> & LinkProps)
| (Omit<JSX.IntrinsicElements["button"], "onClick"> & { href?: never })
);
const variantClassName = {
primary:
"border border-transparent text-white bg-brand-500 hover:bg-brand-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-brand-500",
secondary: "border border-gray-200 text-brand-900 bg-white hover:bg-gray-100",
minimal:
"text-gray-700 bg-transparent hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-offset-1 focus:bg-gray-100 focus:ring-brand-900",
destructive:
"text-gray-700 bg-transparent hover:bg-red-100 hover:text-red-700 focus:outline-none focus:ring-2 focus:ring-offset-1 focus:bg-red-100 focus:ring-red-700",
};
const variantDisabledClassName = {
primary: "border border-transparent bg-brand-500 bg-opacity-20 text-white",
secondary: "border border-gray-200 text-brand-900 bg-white opacity-30",
minimal: "text-gray-400 bg-transparent",
destructive: "text-red-700 bg-transparent opacity-30",
};
export const Button = forwardRef<HTMLAnchorElement | HTMLButtonElement, ButtonProps>(function Button(
props: ButtonProps,
forwardedRef
) {
const {
loading = false,
color = "primary",
size = "base",
StartIcon,
EndIcon,
shallow,
combined = false,
// attributes propagated from `HTMLAnchorProps` or `HTMLButtonProps`
...passThroughProps
} = props;
// Buttons are **always** disabled if we're in a `loading` state
const disabled = props.disabled || loading;
// If pass an `href`-attr is passed it's `<a>`, otherwise it's a `<button />`
const isLink = typeof props.href !== "undefined";
const elementType = isLink ? "a" : "button";
const element = React.createElement(
elementType,
{
...passThroughProps,
disabled,
ref: forwardedRef,
className: classNames(
// base styles independent what type of button it is
"inline-flex items-center text-sm font-medium",
// different styles depending on size
size === "base" && "h-9 px-4 py-2.5 rounded-md ",
size === "lg" && "h-[36px] px-4 py-2.5 rounded-md",
size === "icon" && "flex justify-center h-[36px] w-[36px] rounded-md",
combined && "rounded-none first:border-r-0 last:border-l-0 first:rounded-l-md last:rounded-r-md ",
// different styles depending on color
// set not-allowed cursor if disabled
disabled ? variantDisabledClassName[color] : variantClassName[color],
loading ? "cursor-wait" : disabled ? "cursor-not-allowed" : "",
props.className
),
// if we click a disabled button, we prevent going through the click handler
onClick: disabled
? (e: React.MouseEvent<HTMLElement, MouseEvent>) => {
e.preventDefault();
}
: props.onClick,
},
<>
{StartIcon && (
<StartIcon className={classNames("inline-flex", size === "icon" ? "h-4 w-4 " : "h-5 w-5 ")} />
)}
{props.children}
{loading && (
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 transform">
<svg
className={classNames(
"mx-4 h-5 w-5 animate-spin",
color === "primary" ? "text-white dark:text-black" : "text-black"
)}
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
</div>
)}
{EndIcon && <EndIcon className="-mr-1 inline h-5 w-5 ltr:ml-2 rtl:mr-2" />}
</>
);
return props.href ? (
<Link passHref href={props.href} shallow={shallow && shallow}>
{element}
</Link>
) : (
<Wrapper tooltip={props.tooltip}>{element}</Wrapper>
);
});
const Wrapper = ({ children, tooltip }: { tooltip?: string; children: React.ReactNode }) => {
if (!tooltip) {
return <>{children}</>;
}
return <Tooltip content={tooltip}>{children}</Tooltip>;
};
export default Button;

View File

@ -0,0 +1,9 @@
import React from "react";
import classNames from "@calcom/lib/classNames";
type Props = { children: React.ReactNode; combined?: boolean };
export default function ButtonGroup({ children, combined = false }: Props) {
return <div className={classNames("flex", !combined && "space-x-2")}>{children}</div>;
}

153
packages/ui/v2/Dialog.tsx Normal file
View File

@ -0,0 +1,153 @@
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { useRouter } from "next/router";
import React, { ReactNode, useState, MouseEvent } from "react";
import { Icon } from "react-feather";
import classNames from "@calcom/lib/classNames";
import Button from "./Button";
export type DialogProps = React.ComponentProps<typeof DialogPrimitive["Root"]> & {
name?: string;
clearQueryParamsOnClose?: string[];
};
export function Dialog(props: DialogProps) {
const router = useRouter();
const { children, name, ...dialogProps } = props;
// only used if name is set
const [open, setOpen] = useState(!!dialogProps.open);
if (name) {
const clearQueryParamsOnClose = ["dialog", ...(props.clearQueryParamsOnClose || [])];
dialogProps.onOpenChange = (open) => {
if (props.onOpenChange) {
props.onOpenChange(open);
}
// toggles "dialog" query param
if (open) {
router.query["dialog"] = name;
} else {
clearQueryParamsOnClose.forEach((queryParam) => {
delete router.query[queryParam];
});
}
router.push(
{
pathname: router.pathname,
query: {
...router.query,
},
},
undefined,
{ shallow: true }
);
setOpen(open);
};
// handles initial state
if (!open && router.query["dialog"] === name) {
setOpen(true);
}
// allow overriding
if (!("open" in dialogProps)) {
dialogProps.open = open;
}
}
return (
<DialogPrimitive.Root {...dialogProps}>
<DialogPrimitive.Overlay className="fadeIn fixed inset-0 z-40 bg-black bg-opacity-50 transition-opacity" />
{children}
</DialogPrimitive.Root>
);
}
type DialogContentProps = React.ComponentProps<typeof DialogPrimitive["Content"]> & {
size?: "xl" | "lg";
type?: "creation" | "confirmation";
title?: string;
description?: string;
closeText?: string;
actionText?: string;
Icon?: Icon;
actionOnClick?: () => void;
};
export const DialogContent = React.forwardRef<HTMLDivElement, DialogContentProps>(
({ children, Icon, ...props }, forwardedRef) => (
<DialogPrimitive.Portal>
<DialogPrimitive.Overlay className="fadeIn fixed inset-0 z-40 bg-gray-500 bg-opacity-75 transition-opacity" />
{/*zIndex one less than Toast */}
<DialogPrimitive.Content
{...props}
className={classNames(
"fadeIn fixed left-1/2 top-1/2 z-[9998] min-w-[360px] -translate-x-1/2 -translate-y-1/2 rounded bg-white text-left shadow-xl focus-visible:outline-none sm:w-full sm:align-middle",
props.size == "xl"
? "p-0.5 sm:max-w-[98vw]"
: props.size == "lg"
? "p-6 sm:max-w-[70rem]"
: "p-6 sm:max-w-[35rem]",
"max-h-[560px] overflow-visible overscroll-auto md:h-auto md:max-h-[inherit]",
`${props.className || ""}`
)}
ref={forwardedRef}>
{props.type === "creation" && (
<div className="pb-8">
{props.title && <DialogHeader title={props.title} />}
{props.description && <p className="pb-8 text-sm text-gray-500">Optional Description</p>}
<div className="flex flex-col gap-6">{children}</div>
</div>
)}
{props.type === "confirmation" && (
<div className="flex ">
{Icon && (
<div className="mr-4 inline-flex h-10 w-10 items-center justify-center rounded-full bg-gray-300">
<Icon className="h-4 w-4 text-black" />
</div>
)}
<div>
{props.title && <DialogHeader title={props.title} />}
{props.description && <p className="mb-6 text-sm text-gray-500">Optional Description</p>}
</div>
</div>
)}
<DialogFooter>
<DialogClose asChild>
{/* This will require the i18n string passed in */}
<Button color="minimal">{props.closeText ?? "Close"}</Button>
</DialogClose>
<Button color="primary" onClick={props.actionOnClick}>
{props.actionText}
</Button>
</DialogFooter>
</DialogPrimitive.Content>
</DialogPrimitive.Portal>
)
);
type DialogHeaderProps = {
title: React.ReactNode;
subtitle?: React.ReactNode;
};
export function DialogHeader(props: DialogHeaderProps) {
return (
<>
<h3 className="leading-20 text-semibold font-cal text-xl text-black" id="modal-title">
{props.title}
</h3>
{props.subtitle && <div className="text-sm text-gray-400">{props.subtitle}</div>}
</>
);
}
export function DialogFooter(props: { children: ReactNode }) {
return (
<div>
<div className="mt-5 flex justify-end space-x-2 rtl:space-x-reverse">{props.children}</div>
</div>
);
}
DialogContent.displayName = "DialogContent";
export const DialogTrigger = DialogPrimitive.Trigger;
export const DialogClose = DialogPrimitive.Close;

View File

@ -0,0 +1,94 @@
import { CheckCircleIcon } from "@heroicons/react/outline";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { ComponentProps, forwardRef } from "react";
export const Dropdown = DropdownMenuPrimitive.Root;
type DropdownMenuTriggerProps = ComponentProps<typeof DropdownMenuPrimitive["Trigger"]>;
export const DropdownMenuTrigger = forwardRef<HTMLButtonElement, DropdownMenuTriggerProps>(
({ className = "", ...props }, forwardedRef) => (
<DropdownMenuPrimitive.Trigger
{...props}
className={
props.asChild
? className
: `inline-flex items-center rounded-sm bg-transparent px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 focus:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-neutral-500 focus:ring-offset-1 group-hover:text-black ${className}`
}
ref={forwardedRef}
/>
)
);
DropdownMenuTrigger.displayName = "DropdownMenuTrigger";
export const DropdownMenuTriggerItem = DropdownMenuPrimitive.TriggerItem;
type DropdownMenuContentProps = ComponentProps<typeof DropdownMenuPrimitive["Content"]>;
export const DropdownMenuContent = forwardRef<HTMLDivElement, DropdownMenuContentProps>(
({ children, ...props }, forwardedRef) => {
return (
<DropdownMenuPrimitive.Content
portalled={props.portalled}
{...props}
className="w-50 relative z-10 mt-1 -ml-0 origin-top-right rounded-sm bg-white text-sm shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"
ref={forwardedRef}>
{children}
</DropdownMenuPrimitive.Content>
);
}
);
DropdownMenuContent.displayName = "DropdownMenuContent";
type DropdownMenuLabelProps = ComponentProps<typeof DropdownMenuPrimitive["Label"]>;
export const DropdownMenuLabel = (props: DropdownMenuLabelProps) => (
<DropdownMenuPrimitive.Label {...props} className="px-3 py-2 text-neutral-500" />
);
type DropdownMenuItemProps = ComponentProps<typeof DropdownMenuPrimitive["CheckboxItem"]>;
export const DropdownMenuItem = forwardRef<HTMLDivElement, DropdownMenuItemProps>(
({ className = "", ...props }, forwardedRef) => (
<DropdownMenuPrimitive.Item
className={`text-sm text-gray-700 hover:bg-gray-100 hover:text-gray-900 ${className}`}
{...props}
ref={forwardedRef}
/>
)
);
DropdownMenuItem.displayName = "DropdownMenuItem";
export const DropdownMenuGroup = DropdownMenuPrimitive.Group;
type DropdownMenuCheckboxItemProps = ComponentProps<typeof DropdownMenuPrimitive["CheckboxItem"]>;
export const DropdownMenuCheckboxItem = forwardRef<HTMLDivElement, DropdownMenuCheckboxItemProps>(
({ children, ...props }, forwardedRef) => {
return (
<DropdownMenuPrimitive.CheckboxItem {...props} ref={forwardedRef}>
{children}
<DropdownMenuPrimitive.ItemIndicator>
<CheckCircleIcon />
</DropdownMenuPrimitive.ItemIndicator>
</DropdownMenuPrimitive.CheckboxItem>
);
}
);
DropdownMenuCheckboxItem.displayName = "DropdownMenuCheckboxItem";
export const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
type DropdownMenuRadioItemProps = ComponentProps<typeof DropdownMenuPrimitive["RadioItem"]>;
export const DropdownMenuRadioItem = forwardRef<HTMLDivElement, DropdownMenuRadioItemProps>(
({ children, ...props }, forwardedRef) => {
return (
<DropdownMenuPrimitive.RadioItem {...props} ref={forwardedRef}>
{children}
<DropdownMenuPrimitive.ItemIndicator>
<CheckCircleIcon />
</DropdownMenuPrimitive.ItemIndicator>
</DropdownMenuPrimitive.RadioItem>
);
}
);
DropdownMenuRadioItem.displayName = "DropdownMenuRadioItem";
export const DropdownMenuSeparator = DropdownMenuPrimitive.Separator;
export default Dropdown;

View File

@ -0,0 +1,37 @@
import React from "react";
import { Icon } from "react-feather";
import { SVGComponent } from "@calcom/types/SVGComponent";
import Button from "./Button";
export default function EmptyScreen({
Icon,
headline,
description,
buttonText,
buttonOnClick,
}: {
Icon: SVGComponent | Icon;
headline: string;
description: string | React.ReactElement;
buttonText?: string;
buttonOnClick?: (event: React.MouseEvent<HTMLElement, MouseEvent>) => void;
}) {
return (
<>
<div className="min-h-80 my-6 flex max-w-[640px] flex-col items-center justify-center rounded-sm border border-dashed p-7 lg:p-20">
<div className="flex h-[72px] w-[72px] items-center justify-center rounded-full bg-gray-600 dark:bg-white">
<Icon className="inline-block h-10 w-10 text-white dark:bg-white dark:text-gray-600" />
</div>
<div className="max-w-[420px] text-center">
<h2 className="text-semibold font-cal mt-6 text-xl dark:text-gray-300">{headline}</h2>
<p className="mt-4 mb-8 text-sm font-normal leading-6 text-gray-700 dark:text-gray-300">
{description}
</p>
{buttonOnClick && buttonText && <Button onClick={(e) => buttonOnClick(e)}>{buttonText}</Button>}
</div>
</div>
</>
);
}

View File

@ -0,0 +1,7 @@
export default function Loader() {
return (
<div className="loader border-brand dark:border-darkmodebrand">
<span className="loader-inner bg-brand dark:bg-darkmodebrand" />
</div>
);
}

View File

@ -0,0 +1,29 @@
import { ReactNode } from "react";
import Badge, { badgeClassNameByVariant } from "./Badge";
type Props = {
title: string;
description?: string;
badgeText?: string;
badgeVariant?: keyof typeof badgeClassNameByVariant;
infoIcon?: string;
rightAlignedComponent?: ReactNode;
};
function PageHeader({ title, description, rightAlignedComponent, badgeText, badgeVariant }: Props) {
return (
<div className="flex items-center">
<div className="mr-4 flex-col">
<div className="flex w-full items-center space-x-2">
<h1 className="font-cal text-xl font-semibold text-black">{title}</h1>
{badgeText && badgeVariant && <Badge variant={badgeVariant}>{badgeText}</Badge>}
</div>
<h2 className="text-sm text-gray-600">{description}</h2>
</div>
<div className="ml-auto ">{rightAlignedComponent && rightAlignedComponent}</div>
</div>
);
}
export default PageHeader;

View File

@ -1,4 +1,4 @@
import classNames from "@lib/classNames";
import classNames from "@calcom/lib/classNames";
export type SwatchProps = {
size?: "base" | "sm" | "lg";

40
packages/ui/v2/Switch.tsx Normal file
View File

@ -0,0 +1,40 @@
import { useId } from "@radix-ui/react-id";
import * as Label from "@radix-ui/react-label";
import * as PrimitiveSwitch from "@radix-ui/react-switch";
import React from "react";
import classNames from "@calcom/lib/classNames";
const Switch = (
props: React.ComponentProps<typeof PrimitiveSwitch.Root> & {
label?: string;
}
) => {
const { label, ...primitiveProps } = props;
const id = useId();
return (
<div className="flex h-[20px] items-center">
<PrimitiveSwitch.Root
className={classNames(
props.checked ? "bg-gray-900" : "bg-gray-200 hover:bg-gray-300",
"focus:ring-brand-800 h-[24px] w-[40px] rounded-full p-0.5 shadow-none focus:ring-1"
)}
{...primitiveProps}>
<PrimitiveSwitch.Thumb
id={id}
className="block h-[18px] w-[18px] translate-x-0 rounded-full bg-white transition-transform"
/>
</PrimitiveSwitch.Root>
{label && (
<Label.Root
htmlFor={id}
className="ml-2 cursor-pointer align-text-top text-sm font-medium text-neutral-700 ltr:ml-3 rtl:mr-3 dark:text-white">
{label}
</Label.Root>
)}
</div>
);
};
export default Switch;

View File

@ -0,0 +1,36 @@
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import React from "react";
export function Tooltip({
children,
content,
open,
defaultOpen,
onOpenChange,
...props
}: {
children: React.ReactNode;
content: React.ReactNode;
open?: boolean;
defaultOpen?: boolean;
onOpenChange?: (open: boolean) => void;
}) {
return (
<TooltipPrimitive.Root
delayDuration={50}
open={open}
defaultOpen={defaultOpen}
onOpenChange={onOpenChange}>
<TooltipPrimitive.Trigger asChild>{children}</TooltipPrimitive.Trigger>
<TooltipPrimitive.Content
className="-mt-2 rounded-md bg-gray-900 px-2 py-1 text-xs font-semibold text-white shadow-lg"
side="top"
align="center"
{...props}>
{content}
</TooltipPrimitive.Content>
</TooltipPrimitive.Root>
);
}
export default Tooltip;

60
packages/ui/v2/banner.tsx Normal file
View File

@ -0,0 +1,60 @@
import { MouseEvent } from "react";
import { Icon } from "react-feather";
import classNames from "@calcom/lib/classNames";
import Button from "./Button";
const stylesByVariant = {
neutral: { background: "bg-gray-100 ", text: "!text-gray-800", hover: "hover:!bg-gray-200" },
warning: { background: "bg-orange-100 ", text: "!text-orange-800", hover: "hover:!bg-orange-200" },
error: { background: "bg-red-100 ", text: "!text-red-800", hover: "hover:!bg-red-200" },
};
export type BannerProps = {
title: string;
description?: string;
variant: keyof typeof stylesByVariant;
errorMessage?: string;
Icon?: Icon;
onDismiss: (event: MouseEvent<HTMLElement, MouseEvent>) => void;
onAction?: (event: MouseEvent<HTMLElement, MouseEvent>) => void;
actionText?: string;
} & JSX.IntrinsicElements["div"];
const Banner = (props: BannerProps) => {
const { variant, errorMessage, title, description, ...rest } = props;
const buttonStyle = classNames(stylesByVariant[variant].text, stylesByVariant[variant].hover);
return (
<div
className={classNames(
"flex items-center rounded-md px-3 py-4",
stylesByVariant[variant].background,
stylesByVariant[variant].text,
variant !== "error" && "h-16",
props.className
)}
{...rest}>
<div className={classNames("flex flex-row text-sm")}>
<div className="mr-2">{props.Icon && <props.Icon className="h-4 w-4" />}</div>
<div className="flex flex-col space-y-1">
<h1 className="font-semibold leading-none">{title}</h1>
{description && <h2 className="font-normal">{description}</h2>}
{props.variant === "error" && <p className="ml-6 pt-2 font-mono italic">{errorMessage}</p>}
</div>
</div>
<div className="ml-auto self-start text-sm font-medium">
{props.actionText && (
<Button color="minimal" className={buttonStyle} onClick={() => props.onAction}>
Action
</Button>
)}
<Button color="minimal" className={buttonStyle} onClick={() => props.onDismiss}>
Dismiss
</Button>
</div>
</div>
);
};
export default Banner;

View File

@ -0,0 +1,176 @@
import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/solid";
import dayjs, { Dayjs } from "@calcom/dayjs";
import classNames from "@calcom/lib/classNames";
import { daysInMonth, yyyymmdd } from "@calcom/lib/date-fns";
import { weekdayNames } from "@calcom/lib/weekday";
import { SkeletonText } from "@calcom/ui/skeleton";
export type DatePickerProps = {
/** which day of the week to render the calendar. Usually Sunday (=0) or Monday (=1) - default: Sunday */
weekStart?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
/** Fires whenever a selected date is changed. */
onChange: (date: Dayjs) => void;
/** Fires when the month is changed. */
onMonthChange?: (date: Dayjs) => void;
/** which date is currently selected (not tracked from here) */
selected?: Dayjs;
/** defaults to current date. */
minDate?: Dayjs;
/** Furthest date selectable in the future, default = UNLIMITED */
maxDate?: Dayjs;
/** locale, any IETF language tag, e.g. "hu-HU" - defaults to Browser settings */
locale: string;
/** Defaults to [], which dates are not bookable. Array of valid dates like: ["2022-04-23", "2022-04-24"] */
excludedDates?: string[];
/** defaults to all, which dates are bookable (inverse of excludedDates) */
includedDates?: string[];
/** allows adding classes to the container */
className?: string;
/** Shows a small loading spinner next to the month name */
isLoading?: boolean;
};
export const Day = ({
date,
active,
...props
}: JSX.IntrinsicElements["button"] & { active: boolean; date: Dayjs }) => {
return (
<button
className={classNames(
"disabled:text-bookinglighter absolute top-0 left-0 right-0 bottom-0 mx-auto w-full rounded-md border-2 border-transparent text-center font-medium hover:bg-gray-300 disabled:cursor-default disabled:border-transparent disabled:font-light dark:hover:border-white disabled:dark:border-transparent",
active
? "dark:bg-darkmodebrand dark:text-darkmodebrandcontrast border-brand-800 border-2 bg-gray-300"
: !props.disabled && "bg-gray-100 dark:bg-gray-600 dark:text-white"
)}
data-testid="day"
data-disabled={props.disabled}
{...props}>
{date.date()}
{date.isToday() && <span className="absolute left-0 bottom-0 mx-auto -mb-px w-full text-4xl">.</span>}
</button>
);
};
const Days = ({
// minDate,
excludedDates = [],
includedDates,
browsingDate,
weekStart,
DayComponent = Day,
selected,
...props
}: Omit<DatePickerProps, "locale" | "className" | "weekStart"> & {
DayComponent?: React.FC<React.ComponentProps<typeof Day>>;
browsingDate: Dayjs;
weekStart: number;
}) => {
// Create placeholder elements for empty days in first week
const weekdayOfFirst = browsingDate.day();
const days: (Dayjs | null)[] = Array((weekdayOfFirst - weekStart + 7) % 7).fill(null);
for (let day = 1, dayCount = daysInMonth(browsingDate); day <= dayCount; day++) {
const date = browsingDate.set("date", day);
days.push(date);
}
return (
<>
{days.map((day, idx) => (
<div key={day === null ? `e-${idx}` : `day-${day.format()}`} className="relative w-full pt-[100%]">
{day === null ? (
<div key={`e-${idx}`} />
) : props.isLoading ? (
<button
className="absolute top-0 left-0 right-0 bottom-0 mx-auto flex w-full items-center justify-center rounded-sm border-transparent bg-gray-50 text-center text-gray-400 opacity-50 dark:bg-gray-900 dark:text-gray-400"
key={`e-${idx}`}
disabled>
<SkeletonText width="5" height="4" />
</button>
) : (
<DayComponent
date={day}
onClick={() => {
props.onChange(day);
window.scrollTo({
top: 360,
behavior: "smooth",
});
}}
disabled={
(includedDates && !includedDates.includes(yyyymmdd(day))) ||
excludedDates.includes(yyyymmdd(day))
}
active={selected ? yyyymmdd(selected) === yyyymmdd(day) : false}
/>
)}
</div>
))}
</>
);
};
const DatePicker = ({
weekStart = 0,
className,
locale,
selected,
onMonthChange,
...passThroughProps
}: DatePickerProps & Partial<React.ComponentProps<typeof Days>>) => {
const browsingDate = passThroughProps.browsingDate || dayjs().startOf("month");
const changeMonth = (newMonth: number) => {
if (onMonthChange) {
onMonthChange(browsingDate.add(newMonth, "month"));
}
};
return (
<div className={className}>
<div className="mb-4 flex justify-between text-xl font-light">
<span className="w-1/2 dark:text-white">
{browsingDate ? (
<>
<strong className="text-bookingdarker dark:text-white">{browsingDate.format("MMMM")}</strong>{" "}
<span className="text-bookinglight">{browsingDate.format("YYYY")}</span>
</>
) : (
<SkeletonText width="24" height="8" />
)}
</span>
<div className="text-black dark:text-white">
<button
onClick={() => changeMonth(-1)}
className={classNames(
"group p-1 opacity-50 hover:opacity-100 ltr:mr-2 rtl:ml-2",
!browsingDate.isAfter(dayjs()) && "disabled:text-bookinglighter hover:opacity-50"
)}
disabled={!browsingDate.isAfter(dayjs())}
data-testid="decrementMonth">
<ChevronLeftIcon className="h-5 w-5" />
</button>
<button
className="group p-1 opacity-50 hover:opacity-100"
onClick={() => changeMonth(+1)}
data-testid="incrementMonth">
<ChevronRightIcon className="h-5 w-5" />
</button>
</div>
</div>
<div className="border-bookinglightest mb-2 grid grid-cols-7 gap-4 border-t border-b text-center dark:border-gray-800 md:mb-0 md:border-0">
{weekdayNames(locale, weekStart, "short").map((weekDay) => (
<div key={weekDay} className="text-bookinglight my-4 text-xs uppercase tracking-widest">
{weekDay}
</div>
))}
</div>
<div className="grid grid-cols-7 gap-2 text-center">
<Days weekStart={weekStart} selected={selected} {...passThroughProps} browsingDate={browsingDate} />
</div>
</div>
);
};
export default DatePicker;

View File

@ -0,0 +1,102 @@
import { useCallback, useRef, useState } from "react";
import { useEffect } from "react";
import { HexColorInput, HexColorPicker } from "react-colorful";
import { isValidHexCode, fallBackHex } from "@calcom/lib/CustomBranding";
import Swatch from "@calcom/ui/v2/Swatch";
type Handler = (event: MouseEvent | Event) => void;
function useEventListener<
KW extends keyof WindowEventMap,
KH extends keyof HTMLElementEventMap,
T extends HTMLElement | void = void
>(
eventName: KW | KH,
handler: (event: WindowEventMap[KW] | HTMLElementEventMap[KH] | Event) => void,
element?: React.RefObject<T>
) {
// Create a ref that stores handler
const savedHandler = useRef<typeof handler>();
useEffect(() => {
// Define the listening target
const targetElement: T | Window = element?.current || window;
if (!(targetElement && targetElement.addEventListener)) {
return;
}
// Update saved handler if necessary
if (savedHandler.current !== handler) {
savedHandler.current = handler;
}
// Create event listener that calls handler function stored in ref
const eventListener: typeof handler = (event) => {
// eslint-disable-next-line no-extra-boolean-cast
if (!!savedHandler?.current) {
savedHandler.current(event);
}
};
targetElement.addEventListener(eventName, eventListener);
// Remove event listener on cleanup
return () => {
targetElement.removeEventListener(eventName, eventListener);
};
}, [eventName, element, handler]);
}
function useOnClickOutside<T extends HTMLElement = HTMLElement>(
ref: React.RefObject<T>,
handler: Handler,
mouseEvent: "mousedown" | "mouseup" = "mousedown"
): void {
useEventListener(mouseEvent, (event) => {
const el = ref?.current;
// Do nothing if clicking ref's element or descendent elements
if (!el || el.contains(event.target as Node)) {
return;
}
handler(event);
});
}
export type ColorPickerProps = {
defaultValue: string;
onChange: (text: string) => void;
};
const ColorPicker = (props: ColorPickerProps) => {
const init = !isValidHexCode(props.defaultValue)
? fallBackHex(props.defaultValue, false)
: props.defaultValue;
const [color, setColor] = useState(init);
const [isOpen, toggle] = useState(false);
const popover = useRef() as React.MutableRefObject<HTMLInputElement>;
const close = useCallback(() => toggle(false), []);
useOnClickOutside(popover, close);
return (
<div className="relative mt-1 flex items-center justify-center">
<Swatch size="sm" backgroundColor={color} onClick={() => toggle(!isOpen)} />
{isOpen && (
<div className="popover" ref={popover}>
<HexColorPicker
className="!absolute !top-10 !left-0 !z-10 !h-32 !w-32"
color={color}
onChange={(val) => {
setColor(val);
props.onChange(val);
}}
/>
</div>
)}
<HexColorInput
className="ml-1 block w-full rounded-r-md border border-gray-300 px-3 py-2 sm:text-sm"
color={color}
onChange={(val) => {
setColor(val);
props.onChange(val);
}}
type="text"
/>
</div>
);
};
export default ColorPicker;

View File

@ -0,0 +1,73 @@
import React, { forwardRef, InputHTMLAttributes } from "react";
import classNames from "@calcom/lib/classNames";
type Props = InputHTMLAttributes<HTMLInputElement> & {
label?: React.ReactNode;
description: string;
descriptionAsLabel?: boolean;
informationIconText?: string;
error?: boolean;
};
const CheckboxField = forwardRef<HTMLInputElement, Props>(
({ label, description, error, informationIconText, disabled, ...rest }, ref) => {
const descriptionAsLabel = !label || rest.descriptionAsLabel;
return (
<div className="block items-center sm:flex">
{label && (
<div className="min-w-48 mb-4 sm:mb-0">
{React.createElement(
descriptionAsLabel ? "div" : "label",
{
className: classNames("flex text-sm font-medium text-gray-900"),
...(!descriptionAsLabel
? {
htmlFor: rest.id,
}
: {}),
},
label
)}
</div>
)}
<div className="w-full">
<div className="relative flex items-start">
{React.createElement(
descriptionAsLabel ? "label" : "div",
{
className: classNames(
"relative flex items-start",
!error && descriptionAsLabel ? "text-gray-900" : "text-neutral-900",
error && "text-red-800"
),
},
<>
<div className="flex h-5 items-center">
<input
{...rest}
ref={ref}
type="checkbox"
className={classNames(
"text-primary-600 focus:ring-primary-500 mr-2 h-4 w-4 rounded border-gray-300 ",
!error && disabled
? "bg-gray-300 checked:bg-gray-300"
: "checked:bg-gray-800 hover:bg-gray-100",
error && "border-red-800 checked:bg-red-800 hover:bg-red-400"
)}
/>
</div>
<span className="text-sm ltr:ml-3 rtl:mr-3">{description}</span>
</>
)}
{/* {informationIconText && <InfoBadge content={informationIconText}></InfoBadge>} */}
</div>
</div>
</div>
);
}
);
CheckboxField.displayName = "CheckboxField";
export default CheckboxField;

View File

@ -0,0 +1,33 @@
import "react-calendar/dist/Calendar.css";
import "react-date-picker/dist/DatePicker.css";
import PrimitiveDatePicker from "react-date-picker/dist/entry.nostyle";
import { Calendar } from "react-feather";
import classNames from "@calcom/lib/classNames";
type Props = {
date: Date;
onDatesChange?: ((date: Date) => void) | undefined;
className?: string;
disabled?: boolean;
minDate?: Date;
};
const DatePicker = ({ minDate, disabled, date, onDatesChange, className }: Props) => {
return (
<PrimitiveDatePicker
className={classNames(
"focus:ring-primary-500 focus:border-primary-500 rounded-sm border border-gray-300 p-1 pl-2 shadow-sm sm:text-sm",
className
)}
clearIcon={null}
calendarIcon={<Calendar className="h-5 w-5 text-gray-500" />}
value={date}
minDate={minDate}
disabled={disabled}
onChange={onDatesChange}
/>
);
};
export default DatePicker;

View File

@ -0,0 +1,35 @@
import React from "react";
import classNames from "@calcom/lib/classNames";
type Props = {
steps: number;
currentStep: number;
};
// It might be worth passing this label string from outside the component so we can translate it?
function FormStep({ currentStep, steps }: Props) {
return (
<div className="w-full">
<p className="text-xs font-medium text-gray-400">
Step {currentStep} of {steps}
</p>
<div className="flex flex-nowrap space-x-1">
{[...Array(steps)].map((_, j) => {
console.log({ j, currentStep });
return (
<div
className={classNames(
"h-1 w-full rounded-sm",
currentStep - 1 >= j ? "bg-black" : "bg-gray-400"
)}
key={j}
/>
);
})}
</div>
</div>
);
}
export default FormStep;

View File

@ -0,0 +1,112 @@
import { useSelect } from "downshift";
import { useState } from "react";
import { classNames } from "@calcom/lib";
type IItem =
| {
itemType: "ListItem";
title: string;
description: string;
value: string;
}
| {
itemType: "Title";
title: string;
value: string;
}
| {
itemType: "NameCard";
img: string;
value: string;
name: string;
}
| {
itemType: "Spacer";
};
interface SelectProps {
items: IItem[];
}
function itemToString(item: IItem | null) {
return item && item.itemType !== "Spacer" && item?.value ? item.value : "";
}
function Select({ items }: SelectProps) {
const [selectedItems, setSelectedItems] = useState<IItem[]>([]);
const {
isOpen,
selectedItem,
getToggleButtonProps,
getLabelProps,
getMenuProps,
highlightedIndex,
getItemProps,
} = useSelect<IItem>({
items,
itemToString,
selectedItem: null,
onSelectedItemChange: ({ selectedItem }) => {
if (!selectedItem) {
return;
}
const index = selectedItems.indexOf(selectedItem);
if (index > 0) {
setSelectedItems([...selectedItems.slice(0, index), ...selectedItems.slice(index + 1)]);
} else if (index === 0) {
setSelectedItems([...selectedItems.slice(1)]);
} else {
setSelectedItems([...selectedItems, selectedItem]);
}
},
});
const buttonText = selectedItems.length ? `${selectedItems.length} elements selected` : "Elements";
return (
<div>
<div className="flex w-72 flex-col gap-1">
<label {...getLabelProps()}>Choose your favorite book:</label>
<button
aria-label="toggle menu"
className="flex justify-between bg-white p-2"
type="button"
{...getToggleButtonProps()}>
<span>{buttonText}</span>
<span className="px-2">{isOpen ? <>&#8593;</> : <>&#8595;</>}</span>
</button>
</div>
<ul {...getMenuProps()} className="absolute max-h-80 w-72 overflow-scroll bg-white shadow-md">
{isOpen &&
items.map((item, index) => {
if (item.itemType === "Spacer") return; // Return a spacer that isnt selectable
return (
<li
className={classNames(
// highlightedIndex == index && 'bg-blue-300',
// selectedItem === item && 'font-bold',
"flex items-center gap-3 py-2 px-3 shadow-sm"
)}
key={`${item.value}${index}`}
{...getItemProps({ item, index })}>
<input
type="checkbox"
className="h-5 w-5"
checked={selectedItems.includes(item)}
value={item.value}
onChange={() => null}
/>
<div className="flex flex-col">
<span>{item.value}</span>
{/* {<span className="text-sm text-gray-700">{item.description}</span>} */}
</div>
</li>
);
})}
</ul>
</div>
);
}
export default Select;

View File

@ -0,0 +1,294 @@
import { useId } from "@radix-ui/react-id";
import React, { forwardRef, ReactElement, ReactNode, Ref } from "react";
import { AlertCircle, Icon } from "react-feather";
import { FieldValues, FormProvider, SubmitHandler, useFormContext, UseFormReturn } from "react-hook-form";
import classNames from "@calcom/lib/classNames";
import { getErrorFromUnknown } from "@calcom/lib/errors";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/ui/v2/notfications";
import { Alert } from "../Alert";
type InputProps = Omit<JSX.IntrinsicElements["input"], "name"> & { name: string };
export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(props, ref) {
return (
<input
{...props}
ref={ref}
className={classNames(
"my-2 block h-9 w-full rounded-md border border-gray-300 py-2 px-3 shadow-sm hover:border-gray-400 focus:border-neutral-300 focus:outline-none focus:ring-2 focus:ring-neutral-800 focus:ring-offset-1 sm:text-sm",
props.className
)}
/>
);
});
export function Label(props: JSX.IntrinsicElements["label"]) {
return (
<label {...props} className={classNames("block text-sm font-medium text-gray-700", props.className)}>
{props.children}
</label>
);
}
export function InputLeading(props: JSX.IntrinsicElements["div"]) {
return (
<span className="inline-flex flex-shrink-0 items-center rounded-l-sm border border-r-0 border-gray-300 bg-gray-50 px-3 text-gray-500 sm:text-sm">
{props.children}
</span>
);
}
type InputFieldProps = {
label?: ReactNode;
hint?: ReactNode;
addOnLeading?: ReactNode;
addOnSuffix?: ReactNode;
addOnFilled?: boolean;
error?: string;
labelSrOnly?: boolean;
containerClassName?: string;
} & React.ComponentProps<typeof Input> & {
labelProps?: React.ComponentProps<typeof Label>;
};
const InputField = forwardRef<HTMLInputElement, InputFieldProps>(function InputField(props, ref) {
const id = useId();
const { t } = useLocale();
const methods = useFormContext();
const {
label = t(props.name),
labelProps,
/** Prevents displaying untranslated placeholder keys */
placeholder = t(props.name + "_placeholder") !== props.name + "_placeholder"
? t(props.name + "_placeholder")
: "",
className,
addOnLeading,
addOnSuffix,
addOnFilled = true,
hint,
labelSrOnly,
containerClassName,
...passThrough
} = props;
return (
<div className={classNames(containerClassName)}>
{!!props.name && (
<Label
htmlFor={id}
{...labelProps}
className={classNames(labelSrOnly && "sr-only", props.error && "text-red-900", "pb-2")}>
{label}
</Label>
)}
{addOnLeading || addOnSuffix ? (
<div
className={classNames(
" mb-2 flex items-center rounded-md focus-within:outline-none focus-within:ring-2 focus-within:ring-neutral-800 focus-within:ring-offset-2",
addOnSuffix && "group flex-row-reverse"
)}>
<div
className={classNames(
"h-9 border border-gray-300",
addOnFilled && "bg-gray-100",
addOnLeading && "rounded-l-md border-r-0",
addOnSuffix && "rounded-r-md border-l-0"
)}>
<div
className={classNames(
"flex h-full flex-col justify-center px-3 text-sm",
props.error && "text-red-900"
)}>
<span>{addOnLeading || addOnSuffix}</span>
</div>
</div>
<Input
id={id}
placeholder={placeholder}
className={classNames(
className,
addOnLeading && "rounded-l-none",
addOnSuffix && "rounded-r-none",
"!my-0 !ring-0"
)}
{...passThrough}
ref={ref}
/>
</div>
) : (
<Input id={id} placeholder={placeholder} className={className} {...passThrough} ref={ref} />
)}
{hint && (
<div className="text-gray flex items-center text-sm text-gray-700">
<AlertCircle className="mr-1 h-3 w-3" />
{hint}
</div>
)}
{methods?.formState?.errors[props.name] && (
<Alert className="mt-1" severity="error" message={methods.formState.errors[props.name].message} />
)}
</div>
);
});
export const TextField = forwardRef<HTMLInputElement, InputFieldProps>(function TextField(props, ref) {
return <InputField ref={ref} {...props} />;
});
export const PasswordField = forwardRef<HTMLInputElement, InputFieldProps>(function PasswordField(
props,
ref
) {
return <InputField type="password" placeholder="•••••••••••••" ref={ref} {...props} />;
});
export const EmailInput = forwardRef<HTMLInputElement, InputFieldProps>(function EmailInput(props, ref) {
return (
<Input
ref={ref}
type="email"
autoCapitalize="none"
autoComplete="email"
autoCorrect="off"
inputMode="email"
{...props}
/>
);
});
export const EmailField = forwardRef<HTMLInputElement, InputFieldProps>(function EmailField(props, ref) {
return (
<InputField
ref={ref}
type="email"
autoCapitalize="none"
autoComplete="email"
autoCorrect="off"
inputMode="email"
{...props}
/>
);
});
type TextAreaProps = Omit<JSX.IntrinsicElements["textarea"], "name"> & { name: string };
export const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(function TextAreaInput(props, ref) {
return (
<textarea
ref={ref}
{...props}
className={classNames(
"my-2 block w-full rounded-md border-gray-300 py-2 focus:border-neutral-300 focus:ring-neutral-800 focus:ring-offset-1 sm:text-sm",
props.className
)}
/>
);
});
type TextAreaFieldProps = {
label?: ReactNode;
} & React.ComponentProps<typeof TextArea> & {
labelProps?: React.ComponentProps<typeof Label>;
};
export const TextAreaField = forwardRef<HTMLTextAreaElement, TextAreaFieldProps>(function TextField(
props,
ref
) {
const id = useId();
const { t } = useLocale();
const methods = useFormContext();
const {
label = t(props.name as string),
labelProps,
/** Prevents displaying untranslated placeholder keys */
placeholder = t(props.name + "_placeholder") !== props.name + "_placeholder"
? t(props.name + "_placeholder")
: "",
...passThrough
} = props;
return (
<div>
{!!props.name && (
<Label htmlFor={id} {...labelProps}>
{label}
</Label>
)}
<TextArea ref={ref} placeholder={placeholder} {...passThrough} />
{methods?.formState?.errors[props.name] && (
<Alert className="mt-1" severity="error" message={methods.formState.errors[props.name].message} />
)}
</div>
);
});
type FormProps<T extends object> = { form: UseFormReturn<T>; handleSubmit: SubmitHandler<T> } & Omit<
JSX.IntrinsicElements["form"],
"onSubmit"
>;
const PlainForm = <T extends FieldValues>(props: FormProps<T>, ref: Ref<HTMLFormElement>) => {
const { form, handleSubmit, ...passThrough } = props;
return (
<FormProvider {...form}>
<form
ref={ref}
onSubmit={(event) => {
event.preventDefault();
form
.handleSubmit(handleSubmit)(event)
.catch((err) => {
showToast(`${getErrorFromUnknown(err).message}`, "error");
});
}}
{...passThrough}>
{
/* @see https://react-hook-form.com/advanced-usage/#SmartFormComponent */
React.Children.map(props.children, (child) => {
return typeof child !== "string" &&
typeof child !== "number" &&
typeof child !== "boolean" &&
child &&
"props" in child &&
child.props.name
? React.createElement(child.type, {
...{
...child.props,
register: form.register,
key: child.props.name,
},
})
: child;
})
}
</form>
</FormProvider>
);
};
export const Form = forwardRef(PlainForm) as <T extends FieldValues>(
p: FormProps<T> & { ref?: Ref<HTMLFormElement> }
) => ReactElement;
export function FieldsetLegend(props: JSX.IntrinsicElements["legend"]) {
return (
<legend {...props} className={classNames("text-sm font-medium text-gray-700", props.className)}>
{props.children}
</legend>
);
}
export function InputGroupBox(props: JSX.IntrinsicElements["div"]) {
return (
<div
{...props}
className={classNames("space-y-2 rounded-sm border border-gray-300 bg-white p-2", props.className)}>
{props.children}
</div>
);
}

View File

@ -0,0 +1,5 @@
export * from "./radio-area";
export * from "./fields";
export { default as Checkbox } from "./Checkbox";
export { default as DatePicker } from "./DatePicker";
export { default as Select } from "./Select";

View File

@ -0,0 +1,57 @@
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
import { ReactNode } from "react";
import classNames from "@calcom/lib/classNames";
export const Group = (props: RadioGroupPrimitive.RadioGroupProps & { children: ReactNode }) => (
<RadioGroupPrimitive.Root {...props}>{props.children}</RadioGroupPrimitive.Root>
);
export const Radio = (props: RadioGroupPrimitive.RadioGroupItemProps & { children: ReactNode }) => (
<RadioGroupPrimitive.Item
{...props}
className={classNames(
"h-4 w-4 rounded-full border border-gray-300 hover:bg-gray-100 focus:ring-2 focus:ring-gray-900",
props.disabled && "opacity-60"
)}>
{props.children}
</RadioGroupPrimitive.Item>
);
export const Indicator = ({ disabled }: { disabled?: boolean }) => (
<RadioGroupPrimitive.Indicator
className={classNames(
"relative flex h-full w-full items-center justify-center rounded-full bg-black after:h-[6px] after:w-[6px] after:rounded-full after:bg-white after:content-['']",
disabled ? "after:bg-gray-500" : "bg-black"
)}
/>
);
export const Label = (props: JSX.IntrinsicElements["label"] & { disabled?: boolean }) => (
<label
{...props}
className={classNames(
"ml-2 text-sm font-medium leading-5 text-gray-900",
props.disabled && "text-gray-500"
)}
/>
);
export const RadioField = ({
label,
disabled,
id,
value,
}: {
label: string;
disabled?: boolean;
id: string;
value: string;
}) => (
<div className="flex items-center">
<Radio value={value} disabled={disabled} id={id}>
<Indicator disabled={disabled} />
</Radio>
<Label htmlFor={id} disabled={disabled}>
{label}
</Label>
</div>
);

View File

@ -0,0 +1,79 @@
import React, { ReactNode, useState } from "react";
import classNames from "@calcom/lib/classNames";
type RadioAreaProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, "onChange"> & {
onChange?: (value: string) => void;
defaultChecked?: boolean;
};
const RadioArea = (props: RadioAreaProps) => {
return (
<label
className={classNames(
"border-1 block border p-4 focus:outline-none focus:ring focus:ring-neutral-500",
props.checked && "border-brand",
props.className
)}>
<input
onChange={(e) => {
if (typeof props.onChange === "function") {
props.onChange(e.target.value);
}
}}
checked={props.checked}
className="float-right ml-3 text-neutral-900 focus:ring-neutral-500"
name={props.name}
value={props.value}
type="radio"
/>
{props.children}
</label>
);
};
type ChildrenProps = {
props: RadioAreaProps;
children?: ReactNode;
};
interface RadioAreaGroupProps extends Omit<React.ComponentPropsWithoutRef<"div">, "onChange" | "children"> {
children: ChildrenProps | ChildrenProps[];
name?: string;
onChange?: (value: string) => void;
}
const RadioAreaGroup = ({ children, name, onChange, ...passThroughProps }: RadioAreaGroupProps) => {
const [checkedIdx, setCheckedIdx] = useState<number | null>(null);
const changeHandler = (value: string, idx: number) => {
if (onChange) {
onChange(value);
}
setCheckedIdx(idx);
};
return (
<div {...passThroughProps}>
{(Array.isArray(children) ? children : [children]).map((child, idx: number) => {
if (checkedIdx === null && child.props.defaultChecked) {
setCheckedIdx(idx);
}
return (
<Item
{...child.props}
key={idx}
name={name}
checked={idx === checkedIdx}
onChange={(value: string) => changeHandler(value, idx)}>
{child.props.children}
</Item>
);
})}
</div>
);
};
const Item = RadioArea;
const Group = RadioAreaGroup;
export { RadioArea, RadioAreaGroup, Item, Group };

View File

@ -0,0 +1,59 @@
import { ChevronDownIcon } from "@heroicons/react/solid";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@radix-ui/react-collapsible";
import React from "react";
import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { RadioArea, RadioAreaGroup } from "@calcom/ui/v2/form/radio-area/RadioAreaGroup";
interface OptionProps
extends Pick<React.OptionHTMLAttributes<HTMLOptionElement>, "value" | "label" | "className"> {
description?: string;
}
interface RadioAreaSelectProps extends Omit<React.SelectHTMLAttributes<HTMLSelectElement>, "onChange"> {
options: OptionProps[]; // allow options to be passed programmatically, like options={}
onChange?: (value: string) => void;
}
export const Select = function RadioAreaSelect(props: RadioAreaSelectProps) {
const { t } = useLocale();
const {
options,
disabled = !options.length, // if not explicitly disabled and the options length is empty, disable anyway
placeholder = t("select"),
} = props;
const getLabel = (value: string | ReadonlyArray<string> | number | undefined) =>
options.find((option: OptionProps) => option.value === value)?.label;
return (
<Collapsible className={classNames("w-full", props.className)}>
<CollapsibleTrigger
type="button"
disabled={disabled}
className={classNames(
"border-1 focus:ring-primary-500 mb-1 block w-full cursor-pointer rounded-sm border border-gray-300 bg-white p-2 text-left shadow-sm sm:text-sm",
disabled && "cursor-default bg-gray-200 focus:ring-0 "
)}>
{getLabel(props.value) ?? placeholder}
<ChevronDownIcon className="float-right h-5 w-5 text-neutral-500" />
</CollapsibleTrigger>
<CollapsibleContent>
<RadioAreaGroup className="space-y-2 text-sm" name={props.name} onChange={props.onChange}>
{options.map((option) => (
<RadioArea
{...option}
key={Array.isArray(option.value) ? option.value.join(",") : `${option.value}`}
defaultChecked={props.value === option.value}>
<strong className="mb-1 block">{option.label}</strong>
<p>{option.description}</p>
</RadioArea>
))}
</RadioAreaGroup>
</CollapsibleContent>
</Collapsible>
);
};
export default Select;

View File

@ -0,0 +1,2 @@
export * as RadioGroup from "./RadioAreaGroup";
export { default as Select } from "./Select";

22
packages/ui/v2/index.tsx Normal file
View File

@ -0,0 +1,22 @@
export { default as Button } from "./Button";
export { default as EmptyScreen } from "./EmptyScreen";
export { default as Select } from "./form/Select";
export { default as Loader } from "./Loader";
export * as Dialog from "./Dialog";
export * from "./skeleton";
export { default as Switch } from "./Switch";
export * from "./Breadcrumb";
export { default as Tooltip } from "./Tooltip";
export { default as Badge } from "./Badge";
export { default as Checkbox } from "./form/Checkbox";
export { default as PageHeader } from "./PageHeader";
export { default as ButtonGroup } from "./ButtonGroup";
export { default as ColorPicker } from "./colorpicker";
export { default as DatePicker } from "./form/DatePicker";
export { default as Notifcations } from "./notfications";
export { default as Banner } from "./banner";
export { default as FormStep } from "./form/FormStep";
export { default as Avatar } from "./Avatar";
export { default as AvatarGroup } from "./AvatarGroup";
export * from "./form/radio-area/";
export * from "./navigation/tabs/";

View File

@ -0,0 +1,2 @@
/* TODO: Implement NavigationItem */
export {};

View File

@ -0,0 +1,85 @@
import noop from "lodash/noop";
import Link from "next/link";
import { useRouter } from "next/router";
import { FC, Fragment, MouseEventHandler } from "react";
import { ChevronRight } from "react-feather";
import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { SVGComponent } from "@calcom/types/SVGComponent";
export type VerticalTabItemProps = {
name: string;
info?: string;
icon?: SVGComponent;
disabled?: boolean;
} & (
| {
/** If you want to change query param tabName as per current tab */
href: string;
tabName?: never;
}
| {
href?: never;
/** If you want to change the path as per current tab */
tabName: string;
}
);
const VerticalTabItem: FC<VerticalTabItemProps> = ({ name, href, tabName, info, ...props }) => {
const router = useRouter();
const { t } = useLocale();
let newHref = "";
let isCurrent;
if (href) {
newHref = href;
isCurrent = router.asPath === href;
} else if (tabName) {
newHref = "";
isCurrent = router.query.tabName === tabName;
}
const onClick: MouseEventHandler = tabName
? (e) => {
e.preventDefault();
router.push({
query: {
...router.query,
tabName,
},
});
}
: noop;
return (
<Fragment key={name}>
<Link key={name} href={props.disabled ? "#" : newHref}>
<a
onClick={onClick}
className={classNames(
isCurrent ? "bg-gray-200 text-gray-900" : "bg-white text-gray-600 hover:bg-gray-100",
"group flex h-14 w-64 flex-row rounded-md px-3 py-[10px]",
props.disabled && "pointer-events-none !opacity-30",
!info ? "h-9" : "h-14"
)}
aria-current={isCurrent ? "page" : undefined}>
{props.icon && <props.icon className="mr-[10px] h-[14px] w-[14px] stroke-[1.5px]" />}
<div
className={classNames(
isCurrent ? "font-bold text-gray-900" : "text-gray-600 group-hover:text-gray-700"
)}>
<p className="pb-1 text-sm font-medium leading-none">{t(name)}</p>
{info && <p className="text-xs font-normal">{t(info)}</p>}
</div>
{isCurrent && (
<div className="ml-auto self-center">
<ChevronRight className="stroke-[1.5px]" />
</div>
)}
</a>
</Link>
</Fragment>
);
};
export default VerticalTabItem;

View File

@ -0,0 +1,21 @@
import { FC } from "react";
import VerticalTabItem, { VerticalTabItemProps } from "./VerticalTabItem";
export interface NavTabProps {
tabs: VerticalTabItemProps[];
}
const NavTabs: FC<NavTabProps> = ({ tabs, ...props }) => {
return (
<>
<nav className="no-scrollbar flex flex-col space-y-1" aria-label="Tabs" {...props}>
{tabs.map((tab, idx) => (
<VerticalTabItem {...tab} key={idx} />
))}
</nav>
</>
);
};
export default NavTabs;

View File

@ -0,0 +1,2 @@
export { default as VerticalTabItem } from "./VerticalTabItem";
export { default as VerticalTabs } from "./VerticalTabs";

View File

@ -0,0 +1,51 @@
import { Check, Info } from "react-feather";
import toast from "react-hot-toast";
export default function showToast(message: string, variant: "success" | "warning" | "error") {
switch (variant) {
case "success":
toast.custom(
() => (
<div className="bg-brand-500 mt-2 flex h-9 items-center space-x-2 rounded-md p-3 text-sm font-semibold text-white shadow-md">
<Check className="h-4 w-4" />
<p>{message}</p>
</div>
),
{ duration: 6000 }
);
break;
case "error":
toast.custom(
() => (
<div className="mt-2 flex h-9 items-center space-x-2 rounded-md bg-red-100 p-3 text-sm font-semibold text-red-900 shadow-md">
<Info className="h-4 w-4" />
<p>{message}</p>
</div>
),
{ duration: 6000 }
);
break;
case "warning":
toast.custom(
() => (
<div className="bg-brand-500 mt-2 flex h-9 items-center space-x-2 rounded-md p-3 text-sm font-semibold text-white shadow-md">
<Info className="h-4 w-4" />
<p>{message}</p>
</div>
),
{ duration: 6000 }
);
break;
default:
toast.custom(
() => (
<div className="bg-brand-500 mt-2 flex h-9 items-center space-x-2 rounded-md p-3 text-sm font-semibold text-white shadow-md">
<Check className="h-4 w-4" />
<p>{message}</p>
</div>
),
{ duration: 6000 }
);
break;
}
}

View File

@ -0,0 +1,56 @@
import classNames from "@calcom/lib/classNames";
type SkeletonBaseProps = {
width: string;
height: string;
className?: string;
};
interface AvatarProps extends SkeletonBaseProps {
// Limit this cause we don't use avatars bigger than thi
width: "2" | "3" | "4" | "5" | "6" | "8" | "12";
height: "2" | "3" | "4" | "5" | "6" | "8" | "12";
}
interface SkeletonContainer {
as?: keyof JSX.IntrinsicElements;
children?: React.ReactNode;
className?: string;
}
const SkeletonAvatar: React.FC<AvatarProps> = ({ width, height, className }) => {
return (
<div
className={classNames(
`mt-1 rounded-full bg-gray-200 ltr:mr-2 rtl:ml-2 w-${width} h-${height}`,
className
)}
/>
);
};
const SkeletonText: React.FC<SkeletonBaseProps> = ({ width, height, className }) => {
return (
<div
className={classNames(
`dark:white-300 animate-pulse rounded-md bg-gray-300 w-${width} h-${height}`,
className
)}
/>
);
};
const SkeletonButton: React.FC<SkeletonBaseProps> = ({ width, height, className }) => {
return (
<SkeletonContainer>
<div className={classNames(`w-${width} h-${height} bg-gray-200`, className)} />
</SkeletonContainer>
);
};
const SkeletonContainer: React.FC<SkeletonContainer> = ({ children, as, className }) => {
const Component = as || "div";
return <Component className={classNames("animate-pulse", className)}>{children}</Component>;
};
export { SkeletonAvatar, SkeletonText, SkeletonButton, SkeletonContainer };

6339
yarn.lock

File diff suppressed because it is too large Load Diff