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
parent
dd9adff32a
commit
277b0c4c92
|
@ -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 }}
|
|
@ -73,6 +73,9 @@ dist
|
|||
# Linting
|
||||
lint-results
|
||||
|
||||
#Storybook
|
||||
apps/storybook/build-storybook.log
|
||||
|
||||
# Snaplet
|
||||
.snaplet/snapshots
|
||||
.snaplet/structure.d.ts
|
||||
|
|
|
@ -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 -->
|
||||
|
||||
|
|
2
apps/api
2
apps/api
|
@ -1 +1 @@
|
|||
Subproject commit a8e8acd053e0de1da9ad623c3664a837950d6a06
|
||||
Subproject commit aba7b1ec1c9b5122609dea916c7b114e9a3ba66f
|
|
@ -1 +1 @@
|
|||
Subproject commit a26db083faaa79a40f96dddac888ba2c2bea921e
|
||||
Subproject commit ac2567263de74449c6e4b98b468415ce1c1815d4
|
|
@ -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/
|
|
@ -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;
|
||||
},
|
||||
};
|
|
@ -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>
|
|
@ -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$/,
|
||||
},
|
||||
},
|
||||
};
|
|
@ -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
|
||||
```
|
|
@ -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.
|
|
@ -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);
|
|
@ -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"
|
||||
}
|
|
@ -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;
|
|
@ -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 →</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 →</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 →</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 →</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;
|
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
|
@ -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 |
|
@ -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 />
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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} />
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
|
@ -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")}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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",
|
||||
},
|
||||
};
|
|
@ -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",
|
||||
};
|
|
@ -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>
|
||||
);
|
|
@ -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 />
|
||||
</>
|
||||
);
|
|
@ -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)} />;
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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")}
|
||||
/>
|
||||
);
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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()} />;
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
|
@ -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"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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" }]} />;
|
|
@ -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>
|
||||
);
|
|
@ -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",
|
||||
// },
|
||||
// };
|
|
@ -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",
|
||||
// },
|
||||
// };
|
|
@ -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;
|
||||
}
|
|
@ -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"),
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
};
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"extends": "@calcom/tsconfig/nextjs.json",
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules"],
|
||||
"compilerOptions": {
|
||||
"noImplicitAny": false,
|
||||
"baseUrl": "."
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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",
|
||||
|
|
|
@ -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;
|
|
@ -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) => ({
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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`;
|
||||
};
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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": "*",
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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}</>
|
||||
);
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
|
@ -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);
|
||||
}, []);
|
||||
};
|
|
@ -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;
|
|
@ -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>;
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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;
|
|
@ -1,4 +1,4 @@
|
|||
import classNames from "@lib/classNames";
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
|
||||
export type SwatchProps = {
|
||||
size?: "base" | "sm" | "lg";
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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 ? <>↑</> : <>↓</>}</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;
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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";
|
|
@ -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>
|
||||
);
|
|
@ -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 };
|
|
@ -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;
|
|
@ -0,0 +1,2 @@
|
|||
export * as RadioGroup from "./RadioAreaGroup";
|
||||
export { default as Select } from "./Select";
|
|
@ -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/";
|
|
@ -0,0 +1,2 @@
|
|||
/* TODO: Implement NavigationItem */
|
||||
export {};
|
|
@ -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;
|
|
@ -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;
|
|
@ -0,0 +1,2 @@
|
|||
export { default as VerticalTabItem } from "./VerticalTabItem";
|
||||
export { default as VerticalTabs } from "./VerticalTabs";
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 };
|
Loading…
Reference in New Issue