From 277b0c4c92f322676aa31f6fa8bf71cc251ee127 Mon Sep 17 00:00:00 2001 From: sean-brydon <55134778+sean-brydon@users.noreply.github.com> Date: Sat, 23 Jul 2022 01:39:50 +0100 Subject: [PATCH] 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 9b99d244b70872153a16bec1f1f3bc651e94be7a. * 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 Co-authored-by: Peer Richelsen --- .github/workflows/check-if-ui-has-changed.yml | 50 + .gitignore | 3 + README.md | 12 +- apps/api | 2 +- apps/console | 2 +- apps/storybook/.gitignore | 37 + apps/storybook/.gitkeep | 0 apps/storybook/.storybook/main.js | 57 + apps/storybook/.storybook/preview-head.html | 8 + apps/storybook/.storybook/preview.js | 20 + apps/storybook/README.md | 21 + apps/storybook/next-env.d.ts | 5 + apps/storybook/next.config.js | 7 + apps/storybook/package.json | 62 + apps/storybook/pages/_app.tsx | 9 + apps/storybook/pages/index.tsx | 63 + apps/storybook/postcss.config.js | 6 + apps/storybook/public/favicon.ico | Bin 0 -> 25931 bytes apps/storybook/public/vercel.svg | 4 + apps/storybook/stories/Avatar.stories.tsx | 26 + .../storybook/stories/AvatarGroup.stories.tsx | 60 + apps/storybook/stories/Badge.stories.tsx | 58 + apps/storybook/stories/Banner.stories.tsx | 46 + apps/storybook/stories/Breadcrumb.stories.tsx | 20 + apps/storybook/stories/Button.stories.tsx | 95 + .../storybook/stories/ButtonGroup.stories.tsx | 31 + apps/storybook/stories/Checkbox.stories.tsx | 31 + apps/storybook/stories/Colors.stories.tsx | 208 + apps/storybook/stories/DatePicker.stories.tsx | 17 + .../storybook/stories/EmptyScreen.stories.tsx | 26 + apps/storybook/stories/FormStep.stories.tsx | 27 + apps/storybook/stories/Input.stories.tsx | 68 + apps/storybook/stories/Modal.stories.tsx | 52 + .../stories/Notifcations.stories.tsx | 25 + apps/storybook/stories/PageHeader.stories.tsx | 52 + apps/storybook/stories/Radio.stories.tsx | 19 + apps/storybook/stories/Select.stories.tsx | 10 + apps/storybook/stories/Switch.stories.tsx | 15 + apps/storybook/stories/TabItem.stories.tsx | 67 + .../storybook/stories/VerticalTab.stories.tsx | 85 + apps/storybook/styles/globals.css | 13 + apps/storybook/tailwind.config.js | 13 + apps/storybook/tsconfig.json | 9 + apps/web/components/Embed.tsx | 6 +- apps/web/components/Shell.tsx | 2 +- .../booking/pages/AvailabilityPage.tsx | 8 +- .../components/booking/pages/BookingPage.tsx | 2 +- apps/web/components/ui/colorpicker.tsx | 6 +- .../ui/form/MultiSelectCheckboxes.tsx | 7 +- apps/web/pages/[user].tsx | 4 +- apps/web/pages/cancel/[uid].tsx | 2 +- apps/web/pages/success.tsx | 2 +- apps/web/test/README.md | 2 +- apps/website | 2 +- packages/app-store-cli/package.json | 2 +- packages/config/ColorPalletGenerator.js | 163 + packages/config/tailwind-preset.js | 110 +- packages/embeds/embed-react/src/Cal.tsx | 2 +- .../lib}/CustomBranding.tsx | 0 packages/lib/defaultAvatarImage.ts | 11 + packages/ui/Button.tsx | 11 +- packages/ui/Loader.tsx | 2 +- packages/ui/form/fields.tsx | 4 +- packages/ui/package.json | 12 +- packages/ui/v2/Alert.tsx | 48 + packages/ui/v2/Avatar.tsx | 57 + packages/ui/v2/AvatarGroup.tsx | 64 + packages/ui/v2/Badge.tsx | 48 + packages/ui/v2/Breadcrumb.tsx | 69 + packages/ui/v2/Button.tsx | 147 + packages/ui/v2/ButtonGroup.tsx | 9 + packages/ui/v2/Dialog.tsx | 153 + packages/ui/v2/Dropdown.tsx | 94 + packages/ui/v2/EmptyScreen.tsx | 37 + packages/ui/v2/Loader.tsx | 7 + packages/ui/v2/PageHeader.tsx | 29 + .../components => packages/ui/v2}/Swatch.tsx | 2 +- packages/ui/v2/Switch.tsx | 40 + packages/ui/v2/Tooltip.tsx | 36 + packages/ui/v2/banner.tsx | 60 + packages/ui/v2/booker/DatePicker.tsx | 176 + packages/ui/v2/colorpicker.tsx | 102 + packages/ui/v2/form/Checkbox.tsx | 73 + packages/ui/v2/form/DatePicker.tsx | 33 + packages/ui/v2/form/FormStep.tsx | 35 + packages/ui/v2/form/Select.tsx | 112 + packages/ui/v2/form/fields.tsx | 294 + packages/ui/v2/form/index.ts | 5 + packages/ui/v2/form/radio-area/Radio.tsx | 57 + .../ui/v2/form/radio-area/RadioAreaGroup.tsx | 79 + packages/ui/v2/form/radio-area/Select.tsx | 59 + packages/ui/v2/form/radio-area/index.ts | 2 + packages/ui/v2/index.tsx | 22 + packages/ui/v2/navigation/NavigationItem.tsx | 2 + .../ui/v2/navigation/tabs/VerticalTabItem.tsx | 85 + .../ui/v2/navigation/tabs/VerticalTabs.tsx | 21 + packages/ui/v2/navigation/tabs/index.tsx | 2 + packages/ui/v2/notfications.tsx | 51 + packages/ui/v2/skeleton/index.tsx | 56 + yarn.lock | 6339 ++++++++++++++++- 100 files changed, 9966 insertions(+), 340 deletions(-) create mode 100644 .github/workflows/check-if-ui-has-changed.yml create mode 100755 apps/storybook/.gitignore delete mode 100644 apps/storybook/.gitkeep create mode 100644 apps/storybook/.storybook/main.js create mode 100644 apps/storybook/.storybook/preview-head.html create mode 100644 apps/storybook/.storybook/preview.js create mode 100644 apps/storybook/README.md create mode 100755 apps/storybook/next-env.d.ts create mode 100755 apps/storybook/next.config.js create mode 100644 apps/storybook/package.json create mode 100755 apps/storybook/pages/_app.tsx create mode 100755 apps/storybook/pages/index.tsx create mode 100644 apps/storybook/postcss.config.js create mode 100755 apps/storybook/public/favicon.ico create mode 100755 apps/storybook/public/vercel.svg create mode 100644 apps/storybook/stories/Avatar.stories.tsx create mode 100644 apps/storybook/stories/AvatarGroup.stories.tsx create mode 100644 apps/storybook/stories/Badge.stories.tsx create mode 100644 apps/storybook/stories/Banner.stories.tsx create mode 100644 apps/storybook/stories/Breadcrumb.stories.tsx create mode 100644 apps/storybook/stories/Button.stories.tsx create mode 100644 apps/storybook/stories/ButtonGroup.stories.tsx create mode 100644 apps/storybook/stories/Checkbox.stories.tsx create mode 100644 apps/storybook/stories/Colors.stories.tsx create mode 100644 apps/storybook/stories/DatePicker.stories.tsx create mode 100644 apps/storybook/stories/EmptyScreen.stories.tsx create mode 100644 apps/storybook/stories/FormStep.stories.tsx create mode 100644 apps/storybook/stories/Input.stories.tsx create mode 100644 apps/storybook/stories/Modal.stories.tsx create mode 100644 apps/storybook/stories/Notifcations.stories.tsx create mode 100644 apps/storybook/stories/PageHeader.stories.tsx create mode 100644 apps/storybook/stories/Radio.stories.tsx create mode 100644 apps/storybook/stories/Select.stories.tsx create mode 100644 apps/storybook/stories/Switch.stories.tsx create mode 100644 apps/storybook/stories/TabItem.stories.tsx create mode 100644 apps/storybook/stories/VerticalTab.stories.tsx create mode 100755 apps/storybook/styles/globals.css create mode 100644 apps/storybook/tailwind.config.js create mode 100755 apps/storybook/tsconfig.json create mode 100644 packages/config/ColorPalletGenerator.js rename {apps/web/components => packages/lib}/CustomBranding.tsx (100%) create mode 100644 packages/lib/defaultAvatarImage.ts create mode 100644 packages/ui/v2/Alert.tsx create mode 100644 packages/ui/v2/Avatar.tsx create mode 100644 packages/ui/v2/AvatarGroup.tsx create mode 100644 packages/ui/v2/Badge.tsx create mode 100644 packages/ui/v2/Breadcrumb.tsx create mode 100644 packages/ui/v2/Button.tsx create mode 100644 packages/ui/v2/ButtonGroup.tsx create mode 100644 packages/ui/v2/Dialog.tsx create mode 100644 packages/ui/v2/Dropdown.tsx create mode 100644 packages/ui/v2/EmptyScreen.tsx create mode 100644 packages/ui/v2/Loader.tsx create mode 100644 packages/ui/v2/PageHeader.tsx rename {apps/web/components => packages/ui/v2}/Swatch.tsx (92%) create mode 100644 packages/ui/v2/Switch.tsx create mode 100644 packages/ui/v2/Tooltip.tsx create mode 100644 packages/ui/v2/banner.tsx create mode 100644 packages/ui/v2/booker/DatePicker.tsx create mode 100644 packages/ui/v2/colorpicker.tsx create mode 100644 packages/ui/v2/form/Checkbox.tsx create mode 100644 packages/ui/v2/form/DatePicker.tsx create mode 100644 packages/ui/v2/form/FormStep.tsx create mode 100644 packages/ui/v2/form/Select.tsx create mode 100644 packages/ui/v2/form/fields.tsx create mode 100644 packages/ui/v2/form/index.ts create mode 100644 packages/ui/v2/form/radio-area/Radio.tsx create mode 100644 packages/ui/v2/form/radio-area/RadioAreaGroup.tsx create mode 100644 packages/ui/v2/form/radio-area/Select.tsx create mode 100644 packages/ui/v2/form/radio-area/index.ts create mode 100644 packages/ui/v2/index.tsx create mode 100644 packages/ui/v2/navigation/NavigationItem.tsx create mode 100644 packages/ui/v2/navigation/tabs/VerticalTabItem.tsx create mode 100644 packages/ui/v2/navigation/tabs/VerticalTabs.tsx create mode 100644 packages/ui/v2/navigation/tabs/index.tsx create mode 100644 packages/ui/v2/notfications.tsx create mode 100644 packages/ui/v2/skeleton/index.tsx diff --git a/.github/workflows/check-if-ui-has-changed.yml b/.github/workflows/check-if-ui-has-changed.yml new file mode 100644 index 0000000000..cc371b74e2 --- /dev/null +++ b/.github/workflows/check-if-ui-has-changed.yml @@ -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 }} diff --git a/.gitignore b/.gitignore index a99226a783..b61bab8872 100644 --- a/.gitignore +++ b/.gitignore @@ -73,6 +73,9 @@ dist # Linting lint-results +#Storybook +apps/storybook/build-storybook.log + # Snaplet .snaplet/snapshots .snaplet/structure.d.ts diff --git a/README.md b/README.md index b758dea3e9..5d66ec2e3b 100644 --- a/README.md +++ b/README.md @@ -424,14 +424,14 @@ Next make sure you have your app running `yarn dx`. Then in the slack chat type 1. Select all events for the webhook you interested, e.g. `sleep_created` 1. Copy the webhook secret (`sec...`) to `VITAL_WEBHOOK_SECRET` in the .env.appStore file. -## Workflows +## Workflows ### Setting up SendGrid for Email reminders 1. Create a SendGrid account (https://signup.sendgrid.com/) -2. Go to Settings -> API keys and create an API key +2. Go to Settings -> API keys and create an API key 3. Copy API key to your .env file into the SENDGRID_API_KEY field -4. Go to Settings -> Sender Authentication and verify a single sender +4. Go to Settings -> Sender Authentication and verify a single sender 5. Copy the verified E-Mail to your .env file into the SENDGRID_EMAIL field ### Setting up Twilio for SMS reminders @@ -445,9 +445,9 @@ Next make sure you have your app running `yarn dx`. Then in the slack chat type 7. Click 'Add Senders' 8. Choose phone number as sender type 9. Add the listed phone number -9. Leave all other fields as they are -10. Complete setup and click ‘View my new Messaging Service’ -11. Copy Messaging Service SID to your .env file into the TWILIO_MESSAGING_SID field +10. Leave all other fields as they are +11. Complete setup and click ‘View my new Messaging Service’ +12. Copy Messaging Service SID to your .env file into the TWILIO_MESSAGING_SID field diff --git a/apps/api b/apps/api index a8e8acd053..aba7b1ec1c 160000 --- a/apps/api +++ b/apps/api @@ -1 +1 @@ -Subproject commit a8e8acd053e0de1da9ad623c3664a837950d6a06 +Subproject commit aba7b1ec1c9b5122609dea916c7b114e9a3ba66f diff --git a/apps/console b/apps/console index a26db083fa..ac2567263d 160000 --- a/apps/console +++ b/apps/console @@ -1 +1 @@ -Subproject commit a26db083faaa79a40f96dddac888ba2c2bea921e +Subproject commit ac2567263de74449c6e4b98b468415ce1c1815d4 diff --git a/apps/storybook/.gitignore b/apps/storybook/.gitignore new file mode 100755 index 0000000000..0bcd6d7e0a --- /dev/null +++ b/apps/storybook/.gitignore @@ -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/ diff --git a/apps/storybook/.gitkeep b/apps/storybook/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/apps/storybook/.storybook/main.js b/apps/storybook/.storybook/main.js new file mode 100644 index 0000000000..55a0703097 --- /dev/null +++ b/apps/storybook/.storybook/main.js @@ -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; + }, +}; diff --git a/apps/storybook/.storybook/preview-head.html b/apps/storybook/.storybook/preview-head.html new file mode 100644 index 0000000000..d14ce9f633 --- /dev/null +++ b/apps/storybook/.storybook/preview-head.html @@ -0,0 +1,8 @@ + + diff --git a/apps/storybook/.storybook/preview.js b/apps/storybook/.storybook/preview.js new file mode 100644 index 0000000000..829c0b8026 --- /dev/null +++ b/apps/storybook/.storybook/preview.js @@ -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) => , +}); + +export const parameters = { + actions: { argTypesRegex: "^on[A-Z].*" }, + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/, + }, + }, +}; diff --git a/apps/storybook/README.md b/apps/storybook/README.md new file mode 100644 index 0000000000..97ce36c5aa --- /dev/null +++ b/apps/storybook/README.md @@ -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 +``` diff --git a/apps/storybook/next-env.d.ts b/apps/storybook/next-env.d.ts new file mode 100755 index 0000000000..4f11a03dc6 --- /dev/null +++ b/apps/storybook/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/apps/storybook/next.config.js b/apps/storybook/next.config.js new file mode 100755 index 0000000000..c79b3b3772 --- /dev/null +++ b/apps/storybook/next.config.js @@ -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); diff --git a/apps/storybook/package.json b/apps/storybook/package.json new file mode 100644 index 0000000000..becdf9d6a3 --- /dev/null +++ b/apps/storybook/package.json @@ -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" +} diff --git a/apps/storybook/pages/_app.tsx b/apps/storybook/pages/_app.tsx new file mode 100755 index 0000000000..e9a8374578 --- /dev/null +++ b/apps/storybook/pages/_app.tsx @@ -0,0 +1,9 @@ +import type { AppProps } from "next/app"; + +import "../styles/globals.css"; + +function MyApp({ Component, pageProps }: AppProps) { + return ; +} + +export default MyApp; diff --git a/apps/storybook/pages/index.tsx b/apps/storybook/pages/index.tsx new file mode 100755 index 0000000000..785828e44a --- /dev/null +++ b/apps/storybook/pages/index.tsx @@ -0,0 +1,63 @@ +import type { NextPage } from "next"; +import Head from "next/head"; +import Image from "next/image"; + +const Home: NextPage = () => { + return ( + + ); +}; + +export default Home; diff --git a/apps/storybook/postcss.config.js b/apps/storybook/postcss.config.js new file mode 100644 index 0000000000..12a703d900 --- /dev/null +++ b/apps/storybook/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/apps/storybook/public/favicon.ico b/apps/storybook/public/favicon.ico new file mode 100755 index 0000000000000000000000000000000000000000..718d6fea4835ec2d246af9800eddb7ffb276240c GIT binary patch literal 25931 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 LrJugUA?W`A8`#=m literal 0 HcmV?d00001 diff --git a/apps/storybook/public/vercel.svg b/apps/storybook/public/vercel.svg new file mode 100755 index 0000000000..fbf0e25a65 --- /dev/null +++ b/apps/storybook/public/vercel.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/apps/storybook/stories/Avatar.stories.tsx b/apps/storybook/stories/Avatar.stories.tsx new file mode 100644 index 0000000000..252f5bd640 --- /dev/null +++ b/apps/storybook/stories/Avatar.stories.tsx @@ -0,0 +1,26 @@ +import { ComponentMeta } from "@storybook/react"; + +import Avatar from "@calcom/ui/v2/Avatar"; + +export default { + title: "Avatar", + component: Avatar, +} as ComponentMeta; + +export const Default = () => { + return ( + <> + + + + ); +}; + +export const Accepted = () => { + return ( + <> + + + + ); +}; diff --git a/apps/storybook/stories/AvatarGroup.stories.tsx b/apps/storybook/stories/AvatarGroup.stories.tsx new file mode 100644 index 0000000000..1c322867d9 --- /dev/null +++ b/apps/storybook/stories/AvatarGroup.stories.tsx @@ -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; + +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 ( + <> + + + ); +}; diff --git a/apps/storybook/stories/Badge.stories.tsx b/apps/storybook/stories/Badge.stories.tsx new file mode 100644 index 0000000000..e4e6103c86 --- /dev/null +++ b/apps/storybook/stories/Badge.stories.tsx @@ -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; + +export const All = () => ( +
+

Default

+
+ Badge + Badge + Badge + Badge + Badge +
+

Icons

+
+ + Badge + + + Badge + + + Badge + + + Badge + + + Badge + +
+

Large

+
+ + Badge + + + Badge + + + Badge + + + Badge + + + Badge + +
+
+); diff --git a/apps/storybook/stories/Banner.stories.tsx b/apps/storybook/stories/Banner.stories.tsx new file mode 100644 index 0000000000..7c36991343 --- /dev/null +++ b/apps/storybook/stories/Banner.stories.tsx @@ -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; + +export const Default = () => { + return ( + console.log("dismissed")} + /> + ); +}; + +export const Warning = () => { + return ( + console.log("dismissed")} + /> + ); +}; + +export const Error = () => { + return ( + console.log("dismissed")} + /> + ); +}; diff --git a/apps/storybook/stories/Breadcrumb.stories.tsx b/apps/storybook/stories/Breadcrumb.stories.tsx new file mode 100644 index 0000000000..e432f37b32 --- /dev/null +++ b/apps/storybook/stories/Breadcrumb.stories.tsx @@ -0,0 +1,20 @@ +import { Breadcrumb, BreadcrumbItem } from "@calcom/ui/v2"; + +export default { + title: "Breadcrumbs", + component: Breadcrumb, +}; + +export const Default = () => ( + + Home + Test + +); + +Default.parameters = { + nextRouter: { + path: "/test", + asPath: "/test", + }, +}; diff --git a/apps/storybook/stories/Button.stories.tsx b/apps/storybook/stories/Button.stories.tsx new file mode 100644 index 0000000000..673866b581 --- /dev/null +++ b/apps/storybook/stories/Button.stories.tsx @@ -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; + +const Template: ComponentStory = (args) => ; + +export const All = () => ( +
+

Primary

+
+ Button Text + + Button Text + +
+

Secondary

+
+ + Button Text + + + Button Text + + +
+

Minimal

+
+ + Button Text + + + Button Text + + +
+

Destructive

+ +

Tooltip

+ +
+); + +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", +}; diff --git a/apps/storybook/stories/ButtonGroup.stories.tsx b/apps/storybook/stories/ButtonGroup.stories.tsx new file mode 100644 index 0000000000..96c2231837 --- /dev/null +++ b/apps/storybook/stories/ButtonGroup.stories.tsx @@ -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; + +export const Default = () => ( + + + + + + + ); +}; diff --git a/apps/storybook/stories/Input.stories.tsx b/apps/storybook/stories/Input.stories.tsx new file mode 100644 index 0000000000..42c2f17816 --- /dev/null +++ b/apps/storybook/stories/Input.stories.tsx @@ -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; + +const TextInputTemplate: ComponentStory = (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: , + disabled: false, +}; +export const TextInputSuffixIcon = TextInputTemplate.bind({}); +TextInputSuffixIcon.args = { + name: "demo", + label: "Demo Label", + hint: "Hint Text", + addOnFilled: false, + addOnLeading: , + disabled: false, +}; + +export const TextAreaInput: ComponentStory = () => ( + +); + +export const DatePickerInput: ComponentStory = () => ; diff --git a/apps/storybook/stories/Modal.stories.tsx b/apps/storybook/stories/Modal.stories.tsx new file mode 100644 index 0000000000..82b2cdb026 --- /dev/null +++ b/apps/storybook/stories/Modal.stories.tsx @@ -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 ( + + + + + setOpen(false)}> + + + + + + + ); +}; + +export const Confirmation = () => { + const [open, setOpen] = useState(false); + return ( + + + + + setOpen(false)} + /> + + ); +}; diff --git a/apps/storybook/stories/Notifcations.stories.tsx b/apps/storybook/stories/Notifcations.stories.tsx new file mode 100644 index 0000000000..3ce0693a4f --- /dev/null +++ b/apps/storybook/stories/Notifcations.stories.tsx @@ -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) => ( + <> + + + + ), + ], +} as ComponentMeta; // We have to fake this type as the story for this component isn't really a component. + +export const All = () => ( +
+ + + +
+); diff --git a/apps/storybook/stories/PageHeader.stories.tsx b/apps/storybook/stories/PageHeader.stories.tsx new file mode 100644 index 0000000000..6204e9e698 --- /dev/null +++ b/apps/storybook/stories/PageHeader.stories.tsx @@ -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) => ( +
+ +
+ ), + ], +} as ComponentMeta; + +export const Default = () => ( + +); + +export const ButtonRight = () => ( + Button Text} + /> +); +export const ComingSoon = () => ( + +); +export const SearchInstalledApps = () => ( + } + name="search" + labelSrOnly + placeholder="WIP" + /> + } + /> +); diff --git a/apps/storybook/stories/Radio.stories.tsx b/apps/storybook/stories/Radio.stories.tsx new file mode 100644 index 0000000000..fcc328cef4 --- /dev/null +++ b/apps/storybook/stories/Radio.stories.tsx @@ -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 ( +
+ + + + + +
+ ); +}; diff --git a/apps/storybook/stories/Select.stories.tsx b/apps/storybook/stories/Select.stories.tsx new file mode 100644 index 0000000000..1668b48c6a --- /dev/null +++ b/apps/storybook/stories/Select.stories.tsx @@ -0,0 +1,10 @@ +import { ComponentMeta } from "@storybook/react"; + +import { Select } from "@calcom/ui/v2"; + +export default { + title: "Form/Select", + component: Select, +} as ComponentMeta; + +export const Single = () => +
+
+ {severity === "error" && ( +
+
+

{props.title}

+
{props.message}
+
+ {props.actions &&
{props.actions}
} +
+ + ); +} diff --git a/packages/ui/v2/Avatar.tsx b/packages/ui/v2/Avatar.tsx new file mode 100644 index 0000000000..4c2a78e0bb --- /dev/null +++ b/packages/ui/v2/Avatar.tsx @@ -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; + 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 = ( + + + + {gravatarFallbackMd5 && ( + // eslint-disable-next-line @next/next/no-img-element + {alt} + )} + + {props.accepted && ( +
+
+ {size === "lg" && } +
+
+ )} +
+ ); + + return title ? ( + + {avatar} + + + {title} + + + ) : ( + <>{avatar} + ); +} diff --git a/packages/ui/v2/AvatarGroup.tsx b/packages/ui/v2/AvatarGroup.tsx new file mode 100644 index 0000000000..0d3ee36c83 --- /dev/null +++ b/packages/ui/v2/AvatarGroup.tsx @@ -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 ( +
    + {avatars.map((item, enumerator) => { + if (item.image != null) { + if (LENGTH > 4 && enumerator === 3) { + return ( +
  • +
    + +
    +
    + +{LENGTH - 3} +
    +
  • + ); + } + // Always display the first Four items items + return ( +
  • + +
  • + ); + } + })} +
+ ); +}; + +export default AvatarGroup; diff --git a/packages/ui/v2/Badge.tsx b/packages/ui/v2/Badge.tsx new file mode 100644 index 0000000000..3cea987958 --- /dev/null +++ b/packages/ui/v2/Badge.tsx @@ -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 ( +
+ <> + {StartIcon && } + {props.children} + +
+ ); +}; + +export default Badge; diff --git a/packages/ui/v2/Breadcrumb.tsx b/packages/ui/v2/Breadcrumb.tsx new file mode 100644 index 0000000000..9a15061201 --- /dev/null +++ b/packages/ui/v2/Breadcrumb.tsx @@ -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 ( + + {child} + / + + ); + } + // Else return just the child + return child; + }); + + return ( + + ); +}; + +type BreadcrumbItemProps = { + children: React.ReactNode; + href: string; + listProps?: JSX.IntrinsicElements["li"]; +}; + +export const BreadcrumbItem = ({ children, href, listProps }: BreadcrumbItemProps) => { + return ( +
  • + + {children} + +
  • + ); +}; + +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); + }, []); +}; diff --git a/packages/ui/v2/Button.tsx b/packages/ui/v2/Button.tsx new file mode 100644 index 0000000000..b0e1ee5e68 --- /dev/null +++ b/packages/ui/v2/Button.tsx @@ -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) => 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 & LinkProps) + | (Omit & { 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(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 ``, otherwise it's a ` + + + + + + ) +); + +type DialogHeaderProps = { + title: React.ReactNode; + subtitle?: React.ReactNode; +}; + +export function DialogHeader(props: DialogHeaderProps) { + return ( + <> + + {props.subtitle &&
    {props.subtitle}
    } + + ); +} + +export function DialogFooter(props: { children: ReactNode }) { + return ( +
    +
    {props.children}
    +
    + ); +} + +DialogContent.displayName = "DialogContent"; + +export const DialogTrigger = DialogPrimitive.Trigger; +export const DialogClose = DialogPrimitive.Close; diff --git a/packages/ui/v2/Dropdown.tsx b/packages/ui/v2/Dropdown.tsx new file mode 100644 index 0000000000..476a1c9082 --- /dev/null +++ b/packages/ui/v2/Dropdown.tsx @@ -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; +export const DropdownMenuTrigger = forwardRef( + ({ className = "", ...props }, forwardedRef) => ( + + ) +); +DropdownMenuTrigger.displayName = "DropdownMenuTrigger"; + +export const DropdownMenuTriggerItem = DropdownMenuPrimitive.TriggerItem; + +type DropdownMenuContentProps = ComponentProps; +export const DropdownMenuContent = forwardRef( + ({ children, ...props }, forwardedRef) => { + return ( + + {children} + + ); + } +); +DropdownMenuContent.displayName = "DropdownMenuContent"; + +type DropdownMenuLabelProps = ComponentProps; +export const DropdownMenuLabel = (props: DropdownMenuLabelProps) => ( + +); + +type DropdownMenuItemProps = ComponentProps; +export const DropdownMenuItem = forwardRef( + ({ className = "", ...props }, forwardedRef) => ( + + ) +); +DropdownMenuItem.displayName = "DropdownMenuItem"; + +export const DropdownMenuGroup = DropdownMenuPrimitive.Group; + +type DropdownMenuCheckboxItemProps = ComponentProps; +export const DropdownMenuCheckboxItem = forwardRef( + ({ children, ...props }, forwardedRef) => { + return ( + + {children} + + + + + ); + } +); +DropdownMenuCheckboxItem.displayName = "DropdownMenuCheckboxItem"; + +export const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup; + +type DropdownMenuRadioItemProps = ComponentProps; +export const DropdownMenuRadioItem = forwardRef( + ({ children, ...props }, forwardedRef) => { + return ( + + {children} + + + + + ); + } +); +DropdownMenuRadioItem.displayName = "DropdownMenuRadioItem"; + +export const DropdownMenuSeparator = DropdownMenuPrimitive.Separator; + +export default Dropdown; diff --git a/packages/ui/v2/EmptyScreen.tsx b/packages/ui/v2/EmptyScreen.tsx new file mode 100644 index 0000000000..bfdc766bd4 --- /dev/null +++ b/packages/ui/v2/EmptyScreen.tsx @@ -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) => void; +}) { + return ( + <> +
    +
    + +
    +
    +

    {headline}

    +

    + {description} +

    + {buttonOnClick && buttonText && } +
    +
    + + ); +} diff --git a/packages/ui/v2/Loader.tsx b/packages/ui/v2/Loader.tsx new file mode 100644 index 0000000000..29f0147d85 --- /dev/null +++ b/packages/ui/v2/Loader.tsx @@ -0,0 +1,7 @@ +export default function Loader() { + return ( +
    + +
    + ); +} diff --git a/packages/ui/v2/PageHeader.tsx b/packages/ui/v2/PageHeader.tsx new file mode 100644 index 0000000000..2cc440e512 --- /dev/null +++ b/packages/ui/v2/PageHeader.tsx @@ -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 ( +
    +
    +
    +

    {title}

    + {badgeText && badgeVariant && {badgeText}} +
    +

    {description}

    +
    +
    {rightAlignedComponent && rightAlignedComponent}
    +
    + ); +} + +export default PageHeader; diff --git a/apps/web/components/Swatch.tsx b/packages/ui/v2/Swatch.tsx similarity index 92% rename from apps/web/components/Swatch.tsx rename to packages/ui/v2/Swatch.tsx index 3cf3970fa5..79f2beb1cb 100644 --- a/apps/web/components/Swatch.tsx +++ b/packages/ui/v2/Swatch.tsx @@ -1,4 +1,4 @@ -import classNames from "@lib/classNames"; +import classNames from "@calcom/lib/classNames"; export type SwatchProps = { size?: "base" | "sm" | "lg"; diff --git a/packages/ui/v2/Switch.tsx b/packages/ui/v2/Switch.tsx new file mode 100644 index 0000000000..d8568a2a9f --- /dev/null +++ b/packages/ui/v2/Switch.tsx @@ -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 & { + label?: string; + } +) => { + const { label, ...primitiveProps } = props; + const id = useId(); + + return ( +
    + + + + {label && ( + + {label} + + )} +
    + ); +}; + +export default Switch; diff --git a/packages/ui/v2/Tooltip.tsx b/packages/ui/v2/Tooltip.tsx new file mode 100644 index 0000000000..a5d9e5058f --- /dev/null +++ b/packages/ui/v2/Tooltip.tsx @@ -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 ( + + {children} + + {content} + + + ); +} + +export default Tooltip; diff --git a/packages/ui/v2/banner.tsx b/packages/ui/v2/banner.tsx new file mode 100644 index 0000000000..3430034958 --- /dev/null +++ b/packages/ui/v2/banner.tsx @@ -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) => void; + onAction?: (event: 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 ( +
    +
    +
    {props.Icon && }
    +
    +

    {title}

    + {description &&

    {description}

    } + {props.variant === "error" &&

    {errorMessage}

    } +
    +
    +
    + {props.actionText && ( + + )} + +
    +
    + ); +}; + +export default Banner; diff --git a/packages/ui/v2/booker/DatePicker.tsx b/packages/ui/v2/booker/DatePicker.tsx new file mode 100644 index 0000000000..acedb16688 --- /dev/null +++ b/packages/ui/v2/booker/DatePicker.tsx @@ -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 ( + + ); +}; + +const Days = ({ + // minDate, + excludedDates = [], + includedDates, + browsingDate, + weekStart, + DayComponent = Day, + selected, + ...props +}: Omit & { + DayComponent?: React.FC>; + 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) => ( +
    + {day === null ? ( +
    + ) : props.isLoading ? ( + + ) : ( + { + 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} + /> + )} +
    + ))} + + ); +}; + +const DatePicker = ({ + weekStart = 0, + className, + locale, + selected, + onMonthChange, + ...passThroughProps +}: DatePickerProps & Partial>) => { + const browsingDate = passThroughProps.browsingDate || dayjs().startOf("month"); + + const changeMonth = (newMonth: number) => { + if (onMonthChange) { + onMonthChange(browsingDate.add(newMonth, "month")); + } + }; + + return ( +
    +
    + + {browsingDate ? ( + <> + {browsingDate.format("MMMM")}{" "} + {browsingDate.format("YYYY")} + + ) : ( + + )} + +
    + + +
    +
    +
    + {weekdayNames(locale, weekStart, "short").map((weekDay) => ( +
    + {weekDay} +
    + ))} +
    +
    + +
    +
    + ); +}; + +export default DatePicker; diff --git a/packages/ui/v2/colorpicker.tsx b/packages/ui/v2/colorpicker.tsx new file mode 100644 index 0000000000..baa2da9d7c --- /dev/null +++ b/packages/ui/v2/colorpicker.tsx @@ -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 +) { + // Create a ref that stores handler + const savedHandler = useRef(); + 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( + ref: React.RefObject, + 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; + const close = useCallback(() => toggle(false), []); + useOnClickOutside(popover, close); + return ( +
    + toggle(!isOpen)} /> + + {isOpen && ( +
    + { + setColor(val); + props.onChange(val); + }} + /> +
    + )} + { + setColor(val); + props.onChange(val); + }} + type="text" + /> +
    + ); +}; + +export default ColorPicker; diff --git a/packages/ui/v2/form/Checkbox.tsx b/packages/ui/v2/form/Checkbox.tsx new file mode 100644 index 0000000000..e83fc4c805 --- /dev/null +++ b/packages/ui/v2/form/Checkbox.tsx @@ -0,0 +1,73 @@ +import React, { forwardRef, InputHTMLAttributes } from "react"; + +import classNames from "@calcom/lib/classNames"; + +type Props = InputHTMLAttributes & { + label?: React.ReactNode; + description: string; + descriptionAsLabel?: boolean; + informationIconText?: string; + error?: boolean; +}; + +const CheckboxField = forwardRef( + ({ label, description, error, informationIconText, disabled, ...rest }, ref) => { + const descriptionAsLabel = !label || rest.descriptionAsLabel; + return ( +
    + {label && ( +
    + {React.createElement( + descriptionAsLabel ? "div" : "label", + { + className: classNames("flex text-sm font-medium text-gray-900"), + ...(!descriptionAsLabel + ? { + htmlFor: rest.id, + } + : {}), + }, + label + )} +
    + )} +
    +
    + {React.createElement( + descriptionAsLabel ? "label" : "div", + { + className: classNames( + "relative flex items-start", + !error && descriptionAsLabel ? "text-gray-900" : "text-neutral-900", + error && "text-red-800" + ), + }, + <> +
    + +
    + {description} + + )} + {/* {informationIconText && } */} +
    +
    +
    + ); + } +); + +CheckboxField.displayName = "CheckboxField"; + +export default CheckboxField; diff --git a/packages/ui/v2/form/DatePicker.tsx b/packages/ui/v2/form/DatePicker.tsx new file mode 100644 index 0000000000..d577aaba41 --- /dev/null +++ b/packages/ui/v2/form/DatePicker.tsx @@ -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 ( + } + value={date} + minDate={minDate} + disabled={disabled} + onChange={onDatesChange} + /> + ); +}; + +export default DatePicker; diff --git a/packages/ui/v2/form/FormStep.tsx b/packages/ui/v2/form/FormStep.tsx new file mode 100644 index 0000000000..0e22ac57df --- /dev/null +++ b/packages/ui/v2/form/FormStep.tsx @@ -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 ( +
    +

    + Step {currentStep} of {steps} +

    +
    + {[...Array(steps)].map((_, j) => { + console.log({ j, currentStep }); + return ( +
    = j ? "bg-black" : "bg-gray-400" + )} + key={j} + /> + ); + })} +
    +
    + ); +} + +export default FormStep; diff --git a/packages/ui/v2/form/Select.tsx b/packages/ui/v2/form/Select.tsx new file mode 100644 index 0000000000..bce4d84775 --- /dev/null +++ b/packages/ui/v2/form/Select.tsx @@ -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([]); + const { + isOpen, + selectedItem, + getToggleButtonProps, + getLabelProps, + getMenuProps, + highlightedIndex, + getItemProps, + } = useSelect({ + 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 ( +
    +
    + + +
    +
      + {isOpen && + items.map((item, index) => { + if (item.itemType === "Spacer") return; // Return a spacer that isnt selectable + return ( +
    • + null} + /> +
      + {item.value} + {/* {{item.description}} */} +
      +
    • + ); + })} +
    +
    + ); +} + +export default Select; diff --git a/packages/ui/v2/form/fields.tsx b/packages/ui/v2/form/fields.tsx new file mode 100644 index 0000000000..dfd7b789d3 --- /dev/null +++ b/packages/ui/v2/form/fields.tsx @@ -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 & { name: string }; + +export const Input = forwardRef(function Input(props, ref) { + return ( + + ); +}); + +export function Label(props: JSX.IntrinsicElements["label"]) { + return ( + + ); +} + +export function InputLeading(props: JSX.IntrinsicElements["div"]) { + return ( + + {props.children} + + ); +} + +type InputFieldProps = { + label?: ReactNode; + hint?: ReactNode; + addOnLeading?: ReactNode; + addOnSuffix?: ReactNode; + addOnFilled?: boolean; + error?: string; + labelSrOnly?: boolean; + containerClassName?: string; +} & React.ComponentProps & { + labelProps?: React.ComponentProps; + }; + +const InputField = forwardRef(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 ( +
    + {!!props.name && ( + + )} + {addOnLeading || addOnSuffix ? ( +
    +
    +
    + {addOnLeading || addOnSuffix} +
    +
    + +
    + ) : ( + + )} + {hint && ( +
    + + {hint} +
    + )} + {methods?.formState?.errors[props.name] && ( + + )} +
    + ); +}); + +export const TextField = forwardRef(function TextField(props, ref) { + return ; +}); + +export const PasswordField = forwardRef(function PasswordField( + props, + ref +) { + return ; +}); + +export const EmailInput = forwardRef(function EmailInput(props, ref) { + return ( + + ); +}); + +export const EmailField = forwardRef(function EmailField(props, ref) { + return ( + + ); +}); + +type TextAreaProps = Omit & { name: string }; + +export const TextArea = forwardRef(function TextAreaInput(props, ref) { + return ( +