From faa2292d45f42b7fe5d1dbebaa33191a00eceda7 Mon Sep 17 00:00:00 2001 From: Martin Jagodic Date: Tue, 24 Oct 2023 14:51:10 +0200 Subject: [PATCH 001/165] feat: add decap-cms-ui-4 --- jest.config.js | 1 + packages/decap-cms-app/package.json | 51 +- packages/decap-cms-backend-azure/package.json | 1 + .../decap-cms-backend-bitbucket/package.json | 1 + .../package.json | 1 + packages/decap-cms-backend-gitea/package.json | 1 + .../decap-cms-backend-github/package.json | 1 + .../decap-cms-backend-gitlab/package.json | 1 + packages/decap-cms-backend-proxy/package.json | 1 + packages/decap-cms-backend-test/package.json | 1 + packages/decap-cms-core/package.json | 1 + packages/decap-cms-ui-4/CHANGELOG.md | 4 + packages/decap-cms-ui-4/README.md | 6 + packages/decap-cms-ui-4/package.json | 32 + packages/decap-cms-ui-4/src/AppBar/AppBar.jsx | 114 ++ packages/decap-cms-ui-4/src/AppBar/index.js | 1 + packages/decap-cms-ui-4/src/AppBar/story.jsx | 188 +++ .../decap-cms-ui-4/src/AppWrap/AppWrap.jsx | 70 + packages/decap-cms-ui-4/src/AppWrap/index.js | 1 + packages/decap-cms-ui-4/src/Avatar/Avatar.jsx | 18 + packages/decap-cms-ui-4/src/Avatar/index.js | 1 + packages/decap-cms-ui-4/src/Avatar/story.jsx | 18 + .../decap-cms-ui-4/src/Backdrop/Backdrop.jsx | 49 + packages/decap-cms-ui-4/src/Backdrop/index.js | 1 + .../src/Button/AvatarButton.jsx | 17 + packages/decap-cms-ui-4/src/Button/Button.jsx | 182 +++ .../decap-cms-ui-4/src/Button/ButtonGroup.jsx | 14 + .../decap-cms-ui-4/src/Button/IconButton.jsx | 63 + packages/decap-cms-ui-4/src/Button/index.js | 4 + packages/decap-cms-ui-4/src/Button/story.jsx | 94 ++ packages/decap-cms-ui-4/src/Card/Card.jsx | 17 + packages/decap-cms-ui-4/src/Card/index.js | 1 + packages/decap-cms-ui-4/src/Card/story.jsx | 33 + packages/decap-cms-ui-4/src/Dialog/Dialog.jsx | 327 ++++ packages/decap-cms-ui-4/src/Dialog/index.js | 1 + packages/decap-cms-ui-4/src/Dialog/story.jsx | 63 + packages/decap-cms-ui-4/src/Editor/Editor.jsx | 175 ++ packages/decap-cms-ui-4/src/Editor/index.js | 1 + packages/decap-cms-ui-4/src/Field/Field.jsx | 150 ++ packages/decap-cms-ui-4/src/Field/index.js | 2 + .../src/Fullscreen/Fullscreen.jsx | 27 + .../decap-cms-ui-4/src/Fullscreen/index.js | 1 + .../src/GlobalStyles/GlobalStyles.jsx | 82 + .../decap-cms-ui-4/src/GlobalStyles/index.js | 1 + packages/decap-cms-ui-4/src/HOC/index.js | 1 + .../decap-cms-ui-4/src/HOC/withUIContext.jsx | 6 + packages/decap-cms-ui-4/src/Icon/Icon.jsx | 217 +++ .../src/Icon/icons/Activity.jsx | 20 + .../src/Icon/icons/AlertCircle.jsx | 22 + .../src/Icon/icons/AlertTriangle.jsx | 22 + .../src/Icon/icons/ArrowDown.jsx | 21 + .../src/Icon/icons/ArrowLeft.jsx | 21 + .../src/Icon/icons/ArrowRight.jsx | 21 + .../decap-cms-ui-4/src/Icon/icons/ArrowUp.jsx | 21 + .../decap-cms-ui-4/src/Icon/icons/AtSign.jsx | 21 + .../src/Icon/icons/BarChart.jsx | 22 + .../decap-cms-ui-4/src/Icon/icons/Bell.jsx | 21 + .../src/Icon/icons/Bitbucket.jsx | 20 + .../decap-cms-ui-4/src/Icon/icons/Bold.jsx | 21 + .../decap-cms-ui-4/src/Icon/icons/Box.jsx | 22 + .../src/Icon/icons/BulletedList.jsx | 25 + .../src/Icon/icons/Calendar.jsx | 23 + .../decap-cms-ui-4/src/Icon/icons/Check.jsx | 20 + .../src/Icon/icons/ChevronDown.jsx | 20 + .../src/Icon/icons/ChevronRight.jsx | 20 + .../decap-cms-ui-4/src/Icon/icons/Clock.jsx | 21 + .../decap-cms-ui-4/src/Icon/icons/Code.jsx | 21 + .../src/Icon/icons/CodeBlock.jsx | 21 + .../decap-cms-ui-4/src/Icon/icons/Compass.jsx | 21 + .../decap-cms-ui-4/src/Icon/icons/Copy.jsx | 21 + .../src/Icon/icons/Database.jsx | 22 + .../decap-cms-ui-4/src/Icon/icons/Edit.jsx | 21 + .../decap-cms-ui-4/src/Icon/icons/Edit3.jsx | 21 + .../src/Icon/icons/ExternalLink.jsx | 22 + .../decap-cms-ui-4/src/Icon/icons/Eye.jsx | 21 + .../decap-cms-ui-4/src/Icon/icons/EyeOff.jsx | 21 + .../decap-cms-ui-4/src/Icon/icons/File.jsx | 21 + .../src/Icon/icons/FileText.jsx | 24 + .../decap-cms-ui-4/src/Icon/icons/Files.jsx | 22 + .../decap-cms-ui-4/src/Icon/icons/Film.jsx | 27 + .../decap-cms-ui-4/src/Icon/icons/Filter.jsx | 20 + .../decap-cms-ui-4/src/Icon/icons/Flag.jsx | 21 + .../decap-cms-ui-4/src/Icon/icons/Folder.jsx | 20 + .../decap-cms-ui-4/src/Icon/icons/Github.jsx | 20 + .../decap-cms-ui-4/src/Icon/icons/Gitlab.jsx | 20 + .../decap-cms-ui-4/src/Icon/icons/Globe.jsx | 22 + .../decap-cms-ui-4/src/Icon/icons/Grid.jsx | 23 + .../src/Icon/icons/HardDrive.jsx | 23 + .../src/Icon/icons/HeadingOne.jsx | 22 + .../src/Icon/icons/HeadingThree.jsx | 22 + .../src/Icon/icons/HeadingTwo.jsx | 22 + .../src/Icon/icons/HelpCircle.jsx | 22 + .../decap-cms-ui-4/src/Icon/icons/Image.jsx | 22 + .../decap-cms-ui-4/src/Icon/icons/Inbox.jsx | 21 + .../decap-cms-ui-4/src/Icon/icons/Info.jsx | 22 + .../decap-cms-ui-4/src/Icon/icons/Italic.jsx | 22 + .../decap-cms-ui-4/src/Icon/icons/Layout.jsx | 22 + .../decap-cms-ui-4/src/Icon/icons/Link.jsx | 21 + .../decap-cms-ui-4/src/Icon/icons/Lock.jsx | 21 + .../decap-cms-ui-4/src/Icon/icons/LogOut.jsx | 22 + .../decap-cms-ui-4/src/Icon/icons/Mail.jsx | 21 + .../decap-cms-ui-4/src/Icon/icons/MapPin.jsx | 21 + .../src/Icon/icons/Maximize.jsx | 20 + .../decap-cms-ui-4/src/Icon/icons/Menu.jsx | 22 + .../src/Icon/icons/Minimize.jsx | 20 + .../decap-cms-ui-4/src/Icon/icons/Moon.jsx | 20 + .../src/Icon/icons/MoreHorizontal.jsx | 22 + .../src/Icon/icons/MoreVertical.jsx | 22 + .../decap-cms-ui-4/src/Icon/icons/Move.jsx | 25 + .../decap-cms-ui-4/src/Icon/icons/Music.jsx | 22 + .../decap-cms-ui-4/src/Icon/icons/Netlify.jsx | 34 + .../src/Icon/icons/NumberedList.jsx | 24 + .../decap-cms-ui-4/src/Icon/icons/Package.jsx | 23 + .../src/Icon/icons/Paperclip.jsx | 20 + .../src/Icon/icons/PieChart.jsx | 21 + .../decap-cms-ui-4/src/Icon/icons/Pin.jsx | 21 + .../decap-cms-ui-4/src/Icon/icons/Plus.jsx | 21 + .../src/Icon/icons/PlusCircle.jsx | 22 + .../decap-cms-ui-4/src/Icon/icons/Quote.jsx | 25 + .../decap-cms-ui-4/src/Icon/icons/Radio.jsx | 21 + .../decap-cms-ui-4/src/Icon/icons/Save.jsx | 22 + .../decap-cms-ui-4/src/Icon/icons/Search.jsx | 21 + .../decap-cms-ui-4/src/Icon/icons/Server.jsx | 23 + .../src/Icon/icons/Settings.jsx | 21 + .../decap-cms-ui-4/src/Icon/icons/Share2.jsx | 24 + .../src/Icon/icons/ShoppingCart.jsx | 22 + .../decap-cms-ui-4/src/Icon/icons/Sidebar.jsx | 21 + .../decap-cms-ui-4/src/Icon/icons/Star.jsx | 20 + .../decap-cms-ui-4/src/Icon/icons/Sun.jsx | 28 + .../decap-cms-ui-4/src/Icon/icons/Trash2.jsx | 23 + .../src/Icon/icons/Underline.jsx | 21 + .../src/Icon/icons/Unsplash.jsx | 20 + .../decap-cms-ui-4/src/Icon/icons/Upload.jsx | 22 + .../src/Icon/icons/UploadCloud.jsx | 23 + .../decap-cms-ui-4/src/Icon/icons/User.jsx | 21 + .../decap-cms-ui-4/src/Icon/icons/Users.jsx | 23 + .../src/Icon/icons/Workflow.jsx | 22 + packages/decap-cms-ui-4/src/Icon/icons/X.jsx | 21 + .../decap-cms-ui-4/src/Icon/icons/Zap.jsx | 20 + packages/decap-cms-ui-4/src/Icon/index.js | 1 + packages/decap-cms-ui-4/src/Icon/story.jsx | 30 + packages/decap-cms-ui-4/src/Label/Label.jsx | 15 + packages/decap-cms-ui-4/src/Label/index.js | 1 + .../src/LazyLoadModule/LazyLoadModule.jsx | 25 + .../src/LazyLoadModule/index.js | 1 + .../src/LinearProgress/LinearProgress.jsx | 100 ++ .../src/LinearProgress/index.js | 1 + packages/decap-cms-ui-4/src/LoginButton.js | 36 + packages/decap-cms-ui-4/src/Logo/Logo.jsx | 32 + packages/decap-cms-ui-4/src/Logo/index.js | 1 + .../decap-cms-ui-4/src/LogoTile/LogoTile.jsx | 25 + packages/decap-cms-ui-4/src/LogoTile/index.js | 1 + .../src/MediaDialog/MediaDialog.jsx | 416 +++++ .../decap-cms-ui-4/src/MediaDialog/index.js | 1 + .../decap-cms-ui-4/src/MediaDialog/story.jsx | 27 + packages/decap-cms-ui-4/src/Menu/Menu.jsx | 65 + packages/decap-cms-ui-4/src/Menu/MenuItem.jsx | 80 + packages/decap-cms-ui-4/src/Menu/index.js | 2 + packages/decap-cms-ui-4/src/Menu/story.jsx | 103 ++ packages/decap-cms-ui-4/src/Modal/Modal.jsx | 282 ++++ .../decap-cms-ui-4/src/Modal/ModalManager.jsx | 119 ++ packages/decap-cms-ui-4/src/Modal/index.js | 1 + .../decap-cms-ui-4/src/Modal/isOverflowing.js | 24 + .../src/Modal/manageAriaHidden.js | 24 + .../src/NavMenu/MobileNavMenu.jsx | 250 +++ .../decap-cms-ui-4/src/NavMenu/NavMenu.jsx | 91 ++ .../src/NavMenu/NavMenuGroup.jsx | 23 + .../src/NavMenu/NavMenuGroupLabel.jsx | 20 + .../src/NavMenu/NavMenuItem.jsx | 126 ++ packages/decap-cms-ui-4/src/NavMenu/index.js | 4 + packages/decap-cms-ui-4/src/NavMenu/story.jsx | 125 ++ .../ParticleBackground/ParticleBackground.jsx | 51 + .../src/ParticleBackground/index.js | 1 + .../src/ParticleBackground/story.jsx | 23 + .../decap-cms-ui-4/src/Popover/Popover.jsx | 382 +++++ packages/decap-cms-ui-4/src/Popover/index.js | 1 + packages/decap-cms-ui-4/src/Portal/Portal.jsx | 69 + packages/decap-cms-ui-4/src/Portal/index.js | 1 + .../src/ResponsiveLayout/ResponsiveLayout.jsx | 8 + .../src/ResponsiveLayout/index.js | 1 + .../decap-cms-ui-4/src/RootRef/RootRef.jsx | 84 + packages/decap-cms-ui-4/src/RootRef/index.js | 1 + .../src/SearchBar/SearchBar.jsx | 76 + .../decap-cms-ui-4/src/SearchBar/index.js | 1 + .../decap-cms-ui-4/src/SearchBar/story.jsx | 70 + packages/decap-cms-ui-4/src/Table/Table.jsx | 451 +++++ packages/decap-cms-ui-4/src/Table/index.js | 1 + packages/decap-cms-ui-4/src/Table/story.jsx | 258 +++ packages/decap-cms-ui-4/src/Tag/Tag.jsx | 90 + packages/decap-cms-ui-4/src/Tag/TagGroup.jsx | 14 + packages/decap-cms-ui-4/src/Tag/index.js | 2 + packages/decap-cms-ui-4/src/Tag/story.jsx | 110 ++ packages/decap-cms-ui-4/src/TestSandbox.jsx | 241 +++ .../src/Thumbnail/Thumbnail.jsx | 364 +++++ .../src/Thumbnail/ThumbnailGrid.jsx | 20 + .../decap-cms-ui-4/src/Thumbnail/index.js | 2 + .../decap-cms-ui-4/src/Thumbnail/mockData.js | 1410 ++++++++++++++++ .../decap-cms-ui-4/src/Thumbnail/story.jsx | 188 +++ .../decap-cms-ui-4/src/Toast/CloseButton.jsx | 35 + packages/decap-cms-ui-4/src/Toast/Toast.jsx | 327 ++++ .../src/Toast/ToastContainer.jsx | 386 +++++ .../src/Toast/ToastTransition.jsx | 124 ++ packages/decap-cms-ui-4/src/Toast/index.js | 2 + packages/decap-cms-ui-4/src/Toast/story.jsx | 31 + .../decap-cms-ui-4/src/Toast/utils/toast.js | 117 ++ .../src/ToggleSwitch/ToggleSwitch.jsx | 37 + .../decap-cms-ui-4/src/ToggleSwitch/index.js | 1 + .../decap-cms-ui-4/src/ToggleSwitch/story.jsx | 17 + .../decap-cms-ui-4/src/Tooltip/Tooltip.jsx | 87 + packages/decap-cms-ui-4/src/Tooltip/index.js | 1 + packages/decap-cms-ui-4/src/Tree/Tree.jsx | 159 ++ packages/decap-cms-ui-4/src/Tree/index.js | 1 + .../src/UIContext/UIContext.jsx | 40 + .../decap-cms-ui-4/src/UIContext/index.js | 1 + .../decap-cms-ui-4/src/UserMenu/UserMenu.jsx | 74 + packages/decap-cms-ui-4/src/UserMenu/index.js | 1 + .../WindowDimensionsProvider.jsx | 28 + .../src/WindowDimensionsProvider/index.js | 4 + packages/decap-cms-ui-4/src/hooks/index.js | 3 + .../src/hooks/localStorageState.js | 16 + .../decap-cms-ui-4/src/hooks/uiContext.js | 4 + packages/decap-cms-ui-4/src/index.js | 105 ++ .../src/inputs/BooleanInput/BooleanInput.jsx | 22 + .../src/inputs/BooleanInput/index.js | 1 + .../src/inputs/BooleanInput/story.jsx | 29 + .../src/inputs/DateInput/DateInput.jsx | 69 + .../src/inputs/DateInput/DatepickerStyles.jsx | 321 ++++ .../src/inputs/DateInput/index.js | 1 + .../src/inputs/DateInput/story.jsx | 28 + .../src/inputs/ListInput/ListInput.jsx | 183 +++ .../src/inputs/ListInput/ListInputItem.jsx | 176 ++ .../src/inputs/ListInput/index.js | 1 + .../src/inputs/ListInput/story.jsx | 56 + .../src/inputs/MarkdownWidget.jsx | 301 ++++ .../src/inputs/ObjectInput/ObjectInput.jsx | 45 + .../src/inputs/ObjectInput/index.js | 1 + .../src/inputs/ObjectInput/story.jsx | 55 + .../src/inputs/SelectInput/SelectInput.jsx | 109 ++ .../src/inputs/SelectInput/index.js | 1 + .../src/inputs/SelectInput/story.jsx | 46 + .../src/inputs/TextInput/TextInput.jsx | 83 + .../src/inputs/TextInput/index.js | 1 + .../src/inputs/TextInput/story.jsx | 29 + packages/decap-cms-ui-4/src/inputs/index.js | 7 + packages/decap-cms-ui-4/src/styles.js | 48 + packages/decap-cms-ui-4/src/theme.js | 85 + .../decap-cms-ui-4/src/transitions/Fade.jsx | 68 + .../decap-cms-ui-4/src/transitions/Grow.jsx | 77 + .../decap-cms-ui-4/src/transitions/Slide.jsx | 101 ++ .../decap-cms-ui-4/src/transitions/index.js | 3 + packages/decap-cms-ui-4/src/utils/color.js | 91 ++ .../decap-cms-ui-4/src/utils/constants.js | 23 + .../decap-cms-ui-4/src/utils/eventManager.js | 27 + .../decap-cms-ui-4/src/utils/exactProp.js | 32 + .../decap-cms-ui-4/src/utils/getMockData.js | 42 + packages/decap-cms-ui-4/src/utils/helpers.js | 127 ++ .../decap-cms-ui-4/src/utils/mockImages.js | 1444 +++++++++++++++++ .../decap-cms-ui-4/src/utils/ownerDocument.js | 5 + .../decap-cms-ui-4/src/utils/ownerWindow.js | 8 + .../decap-cms-ui-4/src/utils/propValidator.js | 45 + .../decap-cms-ui-4/src/utils/responsive.js | 80 + packages/decap-cms-ui-4/src/utils/shadow.js | 42 + .../decap-cms-ui-4/src/utils/transitions.js | 108 ++ packages/decap-cms-ui-4/webpack.config.js | 3 + .../decap-cms-ui-default/src/GoBackButton.js | 2 +- .../decap-cms-widget-boolean/package.json | 1 + packages/decap-cms-widget-code/package.json | 1 + .../decap-cms-widget-colorstring/package.json | 1 + packages/decap-cms-widget-file/package.json | 1 + packages/decap-cms-widget-image/package.json | 1 + packages/decap-cms-widget-list/package.json | 1 + packages/decap-cms-widget-map/package.json | 1 + .../decap-cms-widget-markdown/package.json | 1 + packages/decap-cms-widget-number/package.json | 1 + packages/decap-cms-widget-object/package.json | 1 + .../decap-cms-widget-relation/package.json | 1 + packages/decap-cms-widget-select/package.json | 1 + packages/decap-cms-widget-string/package.json | 1 + packages/decap-cms-widget-text/package.json | 1 + 279 files changed, 15635 insertions(+), 26 deletions(-) create mode 100644 packages/decap-cms-ui-4/CHANGELOG.md create mode 100644 packages/decap-cms-ui-4/README.md create mode 100644 packages/decap-cms-ui-4/package.json create mode 100644 packages/decap-cms-ui-4/src/AppBar/AppBar.jsx create mode 100644 packages/decap-cms-ui-4/src/AppBar/index.js create mode 100644 packages/decap-cms-ui-4/src/AppBar/story.jsx create mode 100644 packages/decap-cms-ui-4/src/AppWrap/AppWrap.jsx create mode 100644 packages/decap-cms-ui-4/src/AppWrap/index.js create mode 100644 packages/decap-cms-ui-4/src/Avatar/Avatar.jsx create mode 100644 packages/decap-cms-ui-4/src/Avatar/index.js create mode 100644 packages/decap-cms-ui-4/src/Avatar/story.jsx create mode 100644 packages/decap-cms-ui-4/src/Backdrop/Backdrop.jsx create mode 100644 packages/decap-cms-ui-4/src/Backdrop/index.js create mode 100644 packages/decap-cms-ui-4/src/Button/AvatarButton.jsx create mode 100644 packages/decap-cms-ui-4/src/Button/Button.jsx create mode 100644 packages/decap-cms-ui-4/src/Button/ButtonGroup.jsx create mode 100644 packages/decap-cms-ui-4/src/Button/IconButton.jsx create mode 100644 packages/decap-cms-ui-4/src/Button/index.js create mode 100644 packages/decap-cms-ui-4/src/Button/story.jsx create mode 100644 packages/decap-cms-ui-4/src/Card/Card.jsx create mode 100644 packages/decap-cms-ui-4/src/Card/index.js create mode 100644 packages/decap-cms-ui-4/src/Card/story.jsx create mode 100644 packages/decap-cms-ui-4/src/Dialog/Dialog.jsx create mode 100644 packages/decap-cms-ui-4/src/Dialog/index.js create mode 100644 packages/decap-cms-ui-4/src/Dialog/story.jsx create mode 100644 packages/decap-cms-ui-4/src/Editor/Editor.jsx create mode 100644 packages/decap-cms-ui-4/src/Editor/index.js create mode 100644 packages/decap-cms-ui-4/src/Field/Field.jsx create mode 100644 packages/decap-cms-ui-4/src/Field/index.js create mode 100644 packages/decap-cms-ui-4/src/Fullscreen/Fullscreen.jsx create mode 100644 packages/decap-cms-ui-4/src/Fullscreen/index.js create mode 100644 packages/decap-cms-ui-4/src/GlobalStyles/GlobalStyles.jsx create mode 100644 packages/decap-cms-ui-4/src/GlobalStyles/index.js create mode 100644 packages/decap-cms-ui-4/src/HOC/index.js create mode 100644 packages/decap-cms-ui-4/src/HOC/withUIContext.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/Icon.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/Activity.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/AlertCircle.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/AlertTriangle.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/ArrowDown.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/ArrowLeft.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/ArrowRight.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/ArrowUp.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/AtSign.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/BarChart.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/Bell.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/Bitbucket.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/Bold.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/Box.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/BulletedList.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/Calendar.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/Check.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/ChevronDown.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/ChevronRight.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/Clock.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/Code.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/CodeBlock.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/Compass.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/Copy.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/Database.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/Edit.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/Edit3.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/ExternalLink.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/Eye.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/EyeOff.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/File.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/FileText.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/Files.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/Film.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/Filter.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/Flag.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/Folder.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/Github.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/Gitlab.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/Globe.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/Grid.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/HardDrive.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/HeadingOne.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/HeadingThree.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/HeadingTwo.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/HelpCircle.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/Image.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/Inbox.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/Info.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/Italic.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/Layout.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/Link.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/Lock.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/LogOut.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/Mail.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/MapPin.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/Maximize.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/Menu.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/Minimize.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/Moon.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/MoreHorizontal.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/MoreVertical.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/Move.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/Music.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/Netlify.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/NumberedList.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/Package.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/Paperclip.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/PieChart.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/Pin.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/Plus.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/PlusCircle.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/Quote.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/Radio.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/Save.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/Search.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/Server.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/Settings.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/Share2.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/ShoppingCart.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/Sidebar.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/Star.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/Sun.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/Trash2.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/Underline.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/Unsplash.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/Upload.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/UploadCloud.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/User.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/Users.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/Workflow.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/X.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/icons/Zap.jsx create mode 100644 packages/decap-cms-ui-4/src/Icon/index.js create mode 100644 packages/decap-cms-ui-4/src/Icon/story.jsx create mode 100644 packages/decap-cms-ui-4/src/Label/Label.jsx create mode 100644 packages/decap-cms-ui-4/src/Label/index.js create mode 100644 packages/decap-cms-ui-4/src/LazyLoadModule/LazyLoadModule.jsx create mode 100644 packages/decap-cms-ui-4/src/LazyLoadModule/index.js create mode 100644 packages/decap-cms-ui-4/src/LinearProgress/LinearProgress.jsx create mode 100644 packages/decap-cms-ui-4/src/LinearProgress/index.js create mode 100644 packages/decap-cms-ui-4/src/LoginButton.js create mode 100644 packages/decap-cms-ui-4/src/Logo/Logo.jsx create mode 100644 packages/decap-cms-ui-4/src/Logo/index.js create mode 100644 packages/decap-cms-ui-4/src/LogoTile/LogoTile.jsx create mode 100644 packages/decap-cms-ui-4/src/LogoTile/index.js create mode 100644 packages/decap-cms-ui-4/src/MediaDialog/MediaDialog.jsx create mode 100644 packages/decap-cms-ui-4/src/MediaDialog/index.js create mode 100644 packages/decap-cms-ui-4/src/MediaDialog/story.jsx create mode 100644 packages/decap-cms-ui-4/src/Menu/Menu.jsx create mode 100644 packages/decap-cms-ui-4/src/Menu/MenuItem.jsx create mode 100644 packages/decap-cms-ui-4/src/Menu/index.js create mode 100644 packages/decap-cms-ui-4/src/Menu/story.jsx create mode 100644 packages/decap-cms-ui-4/src/Modal/Modal.jsx create mode 100644 packages/decap-cms-ui-4/src/Modal/ModalManager.jsx create mode 100644 packages/decap-cms-ui-4/src/Modal/index.js create mode 100644 packages/decap-cms-ui-4/src/Modal/isOverflowing.js create mode 100644 packages/decap-cms-ui-4/src/Modal/manageAriaHidden.js create mode 100644 packages/decap-cms-ui-4/src/NavMenu/MobileNavMenu.jsx create mode 100644 packages/decap-cms-ui-4/src/NavMenu/NavMenu.jsx create mode 100644 packages/decap-cms-ui-4/src/NavMenu/NavMenuGroup.jsx create mode 100644 packages/decap-cms-ui-4/src/NavMenu/NavMenuGroupLabel.jsx create mode 100644 packages/decap-cms-ui-4/src/NavMenu/NavMenuItem.jsx create mode 100644 packages/decap-cms-ui-4/src/NavMenu/index.js create mode 100644 packages/decap-cms-ui-4/src/NavMenu/story.jsx create mode 100644 packages/decap-cms-ui-4/src/ParticleBackground/ParticleBackground.jsx create mode 100644 packages/decap-cms-ui-4/src/ParticleBackground/index.js create mode 100644 packages/decap-cms-ui-4/src/ParticleBackground/story.jsx create mode 100644 packages/decap-cms-ui-4/src/Popover/Popover.jsx create mode 100644 packages/decap-cms-ui-4/src/Popover/index.js create mode 100644 packages/decap-cms-ui-4/src/Portal/Portal.jsx create mode 100644 packages/decap-cms-ui-4/src/Portal/index.js create mode 100644 packages/decap-cms-ui-4/src/ResponsiveLayout/ResponsiveLayout.jsx create mode 100644 packages/decap-cms-ui-4/src/ResponsiveLayout/index.js create mode 100644 packages/decap-cms-ui-4/src/RootRef/RootRef.jsx create mode 100644 packages/decap-cms-ui-4/src/RootRef/index.js create mode 100644 packages/decap-cms-ui-4/src/SearchBar/SearchBar.jsx create mode 100644 packages/decap-cms-ui-4/src/SearchBar/index.js create mode 100644 packages/decap-cms-ui-4/src/SearchBar/story.jsx create mode 100644 packages/decap-cms-ui-4/src/Table/Table.jsx create mode 100644 packages/decap-cms-ui-4/src/Table/index.js create mode 100644 packages/decap-cms-ui-4/src/Table/story.jsx create mode 100644 packages/decap-cms-ui-4/src/Tag/Tag.jsx create mode 100644 packages/decap-cms-ui-4/src/Tag/TagGroup.jsx create mode 100644 packages/decap-cms-ui-4/src/Tag/index.js create mode 100644 packages/decap-cms-ui-4/src/Tag/story.jsx create mode 100644 packages/decap-cms-ui-4/src/TestSandbox.jsx create mode 100644 packages/decap-cms-ui-4/src/Thumbnail/Thumbnail.jsx create mode 100644 packages/decap-cms-ui-4/src/Thumbnail/ThumbnailGrid.jsx create mode 100644 packages/decap-cms-ui-4/src/Thumbnail/index.js create mode 100644 packages/decap-cms-ui-4/src/Thumbnail/mockData.js create mode 100644 packages/decap-cms-ui-4/src/Thumbnail/story.jsx create mode 100644 packages/decap-cms-ui-4/src/Toast/CloseButton.jsx create mode 100644 packages/decap-cms-ui-4/src/Toast/Toast.jsx create mode 100644 packages/decap-cms-ui-4/src/Toast/ToastContainer.jsx create mode 100644 packages/decap-cms-ui-4/src/Toast/ToastTransition.jsx create mode 100644 packages/decap-cms-ui-4/src/Toast/index.js create mode 100644 packages/decap-cms-ui-4/src/Toast/story.jsx create mode 100644 packages/decap-cms-ui-4/src/Toast/utils/toast.js create mode 100644 packages/decap-cms-ui-4/src/ToggleSwitch/ToggleSwitch.jsx create mode 100644 packages/decap-cms-ui-4/src/ToggleSwitch/index.js create mode 100644 packages/decap-cms-ui-4/src/ToggleSwitch/story.jsx create mode 100644 packages/decap-cms-ui-4/src/Tooltip/Tooltip.jsx create mode 100644 packages/decap-cms-ui-4/src/Tooltip/index.js create mode 100644 packages/decap-cms-ui-4/src/Tree/Tree.jsx create mode 100644 packages/decap-cms-ui-4/src/Tree/index.js create mode 100644 packages/decap-cms-ui-4/src/UIContext/UIContext.jsx create mode 100644 packages/decap-cms-ui-4/src/UIContext/index.js create mode 100644 packages/decap-cms-ui-4/src/UserMenu/UserMenu.jsx create mode 100644 packages/decap-cms-ui-4/src/UserMenu/index.js create mode 100644 packages/decap-cms-ui-4/src/WindowDimensionsProvider/WindowDimensionsProvider.jsx create mode 100644 packages/decap-cms-ui-4/src/WindowDimensionsProvider/index.js create mode 100644 packages/decap-cms-ui-4/src/hooks/index.js create mode 100644 packages/decap-cms-ui-4/src/hooks/localStorageState.js create mode 100644 packages/decap-cms-ui-4/src/hooks/uiContext.js create mode 100644 packages/decap-cms-ui-4/src/index.js create mode 100644 packages/decap-cms-ui-4/src/inputs/BooleanInput/BooleanInput.jsx create mode 100644 packages/decap-cms-ui-4/src/inputs/BooleanInput/index.js create mode 100644 packages/decap-cms-ui-4/src/inputs/BooleanInput/story.jsx create mode 100644 packages/decap-cms-ui-4/src/inputs/DateInput/DateInput.jsx create mode 100644 packages/decap-cms-ui-4/src/inputs/DateInput/DatepickerStyles.jsx create mode 100644 packages/decap-cms-ui-4/src/inputs/DateInput/index.js create mode 100644 packages/decap-cms-ui-4/src/inputs/DateInput/story.jsx create mode 100644 packages/decap-cms-ui-4/src/inputs/ListInput/ListInput.jsx create mode 100644 packages/decap-cms-ui-4/src/inputs/ListInput/ListInputItem.jsx create mode 100644 packages/decap-cms-ui-4/src/inputs/ListInput/index.js create mode 100644 packages/decap-cms-ui-4/src/inputs/ListInput/story.jsx create mode 100644 packages/decap-cms-ui-4/src/inputs/MarkdownWidget.jsx create mode 100644 packages/decap-cms-ui-4/src/inputs/ObjectInput/ObjectInput.jsx create mode 100644 packages/decap-cms-ui-4/src/inputs/ObjectInput/index.js create mode 100644 packages/decap-cms-ui-4/src/inputs/ObjectInput/story.jsx create mode 100644 packages/decap-cms-ui-4/src/inputs/SelectInput/SelectInput.jsx create mode 100644 packages/decap-cms-ui-4/src/inputs/SelectInput/index.js create mode 100644 packages/decap-cms-ui-4/src/inputs/SelectInput/story.jsx create mode 100644 packages/decap-cms-ui-4/src/inputs/TextInput/TextInput.jsx create mode 100644 packages/decap-cms-ui-4/src/inputs/TextInput/index.js create mode 100644 packages/decap-cms-ui-4/src/inputs/TextInput/story.jsx create mode 100644 packages/decap-cms-ui-4/src/inputs/index.js create mode 100644 packages/decap-cms-ui-4/src/styles.js create mode 100644 packages/decap-cms-ui-4/src/theme.js create mode 100644 packages/decap-cms-ui-4/src/transitions/Fade.jsx create mode 100644 packages/decap-cms-ui-4/src/transitions/Grow.jsx create mode 100644 packages/decap-cms-ui-4/src/transitions/Slide.jsx create mode 100644 packages/decap-cms-ui-4/src/transitions/index.js create mode 100644 packages/decap-cms-ui-4/src/utils/color.js create mode 100644 packages/decap-cms-ui-4/src/utils/constants.js create mode 100644 packages/decap-cms-ui-4/src/utils/eventManager.js create mode 100644 packages/decap-cms-ui-4/src/utils/exactProp.js create mode 100644 packages/decap-cms-ui-4/src/utils/getMockData.js create mode 100644 packages/decap-cms-ui-4/src/utils/helpers.js create mode 100644 packages/decap-cms-ui-4/src/utils/mockImages.js create mode 100644 packages/decap-cms-ui-4/src/utils/ownerDocument.js create mode 100644 packages/decap-cms-ui-4/src/utils/ownerWindow.js create mode 100644 packages/decap-cms-ui-4/src/utils/propValidator.js create mode 100644 packages/decap-cms-ui-4/src/utils/responsive.js create mode 100644 packages/decap-cms-ui-4/src/utils/shadow.js create mode 100644 packages/decap-cms-ui-4/src/utils/transitions.js create mode 100644 packages/decap-cms-ui-4/webpack.config.js diff --git a/jest.config.js b/jest.config.js index b1bc986b7a5b..7d2d4655e7e5 100644 --- a/jest.config.js +++ b/jest.config.js @@ -4,6 +4,7 @@ module.exports = { 'decap-cms-lib-auth': '/packages/decap-cms-lib-auth/src/index.js', 'decap-cms-lib-util': '/packages/decap-cms-lib-util/src/index.ts', 'decap-cms-ui-default': '/packages/decap-cms-ui-default/src/index.js', + 'decap-cms-ui-4': '/packages/decap-cms-ui-4/src/index.js', 'decap-cms-backend-github': '/packages/decap-cms-backend-github/src/index.ts', 'decap-cms-lib-widgets': '/packages/decap-cms-lib-widgets/src/index.ts', 'decap-cms-widget-object': '/packages/decap-cms-widget-object/src/index.js', diff --git a/packages/decap-cms-app/package.json b/packages/decap-cms-app/package.json index 47c5ee93e0f9..18792062623a 100644 --- a/packages/decap-cms-app/package.json +++ b/packages/decap-cms-app/package.json @@ -1,7 +1,7 @@ { "name": "decap-cms-app", "description": "An extensible, open source, Git-based, React CMS for static sites. Reusable congiuration with React as peer.", - "version": "3.1.0-beta.0", + "version": "3.0.0", "homepage": "https://www.decapcms.org", "repository": "https://github.com/decaporg/decap-cms/tree/master/packages/decap-cms-app", "bugs": "https://github.com/decaporg/decap-cms/issues", @@ -29,35 +29,36 @@ "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", "codemirror": "^5.46.0", - "decap-cms-backend-azure": "^3.1.0-beta.0", - "decap-cms-backend-bitbucket": "^3.1.0-beta.0", - "decap-cms-backend-git-gateway": "^3.1.0-beta.0", - "decap-cms-backend-github": "^3.1.0-beta.0", - "decap-cms-backend-gitlab": "^3.1.0-beta.0", - "decap-cms-backend-proxy": "^3.1.0-beta.0", - "decap-cms-backend-test": "^3.1.0-beta.0", + "decap-cms-backend-azure": "^3.0.0", + "decap-cms-backend-bitbucket": "^3.0.0", + "decap-cms-backend-git-gateway": "^3.0.0", + "decap-cms-backend-github": "^3.0.0", + "decap-cms-backend-gitlab": "^3.0.0", + "decap-cms-backend-proxy": "^3.0.0", + "decap-cms-backend-test": "^3.0.0", "decap-cms-core": "^3.3.0-beta.0", - "decap-cms-editor-component-image": "^3.1.0-beta.0", + "decap-cms-editor-component-image": "^3.0.0", "decap-cms-lib-auth": "^3.0.2", "decap-cms-lib-util": "^3.0.1", "decap-cms-lib-widgets": "^3.0.0", "decap-cms-locales": "^3.1.2", - "decap-cms-ui-default": "^3.1.0-beta.0", - "decap-cms-widget-boolean": "^3.1.0-beta.0", - "decap-cms-widget-code": "^3.1.0-beta.0", - "decap-cms-widget-colorstring": "^3.1.0-beta.0", - "decap-cms-widget-datetime": "^3.1.0-beta.0", - "decap-cms-widget-file": "^3.1.0-beta.0", - "decap-cms-widget-image": "^3.1.0-beta.0", - "decap-cms-widget-list": "^3.1.0-beta.0", - "decap-cms-widget-map": "^3.1.0-beta.0", - "decap-cms-widget-markdown": "^3.1.0-beta.0", - "decap-cms-widget-number": "^3.1.0-beta.0", - "decap-cms-widget-object": "^3.1.0-beta.0", - "decap-cms-widget-relation": "^3.1.0-beta.0", - "decap-cms-widget-select": "^3.1.0-beta.0", - "decap-cms-widget-string": "^3.1.0-beta.0", - "decap-cms-widget-text": "^3.1.0-beta.0", + "decap-cms-ui-default": "^3.0.0", + "decap-cms-ui-4": "^3.0.0", + "decap-cms-widget-boolean": "^3.0.0", + "decap-cms-widget-code": "^3.0.0", + "decap-cms-widget-colorstring": "^3.0.0", + "decap-cms-widget-datetime": "^3.0.0", + "decap-cms-widget-file": "^3.0.0", + "decap-cms-widget-image": "^3.0.0", + "decap-cms-widget-list": "^3.0.0", + "decap-cms-widget-map": "^3.0.0", + "decap-cms-widget-markdown": "^3.0.0", + "decap-cms-widget-number": "^3.0.0", + "decap-cms-widget-object": "^3.0.0", + "decap-cms-widget-relation": "^3.0.0", + "decap-cms-widget-select": "^3.0.0", + "decap-cms-widget-string": "^3.0.0", + "decap-cms-widget-text": "^3.0.0", "immutable": "^3.7.6", "lodash": "^4.17.11", "moment": "^2.24.0", diff --git a/packages/decap-cms-backend-azure/package.json b/packages/decap-cms-backend-azure/package.json index bc3ee38a10ea..8a7e743e756f 100644 --- a/packages/decap-cms-backend-azure/package.json +++ b/packages/decap-cms-backend-azure/package.json @@ -29,6 +29,7 @@ "decap-cms-lib-auth": "^3.0.0", "decap-cms-lib-util": "^3.0.0", "decap-cms-ui-default": "^3.0.0", + "decap-cms-ui-4": "^3.0.0", "immutable": "^3.7.6", "lodash": "^4.17.11", "prop-types": "^15.7.2", diff --git a/packages/decap-cms-backend-bitbucket/package.json b/packages/decap-cms-backend-bitbucket/package.json index 44a8d6a01f5e..b57f0fde42c9 100644 --- a/packages/decap-cms-backend-bitbucket/package.json +++ b/packages/decap-cms-backend-bitbucket/package.json @@ -30,6 +30,7 @@ "decap-cms-lib-auth": "^3.0.0", "decap-cms-lib-util": "^3.0.0", "decap-cms-ui-default": "^3.0.0", + "decap-cms-ui-4": "^3.0.0", "immutable": "^3.7.6", "lodash": "^4.17.11", "prop-types": "^15.7.2", diff --git a/packages/decap-cms-backend-git-gateway/package.json b/packages/decap-cms-backend-git-gateway/package.json index c075d292e804..ef7f0b348389 100644 --- a/packages/decap-cms-backend-git-gateway/package.json +++ b/packages/decap-cms-backend-git-gateway/package.json @@ -34,6 +34,7 @@ "decap-cms-lib-auth": "^3.0.0", "decap-cms-lib-util": "^3.0.0", "decap-cms-ui-default": "^3.0.0", + "decap-cms-ui-4": "^3.0.0", "lodash": "^4.17.11", "prop-types": "^15.7.2", "react": "^18.2.0" diff --git a/packages/decap-cms-backend-gitea/package.json b/packages/decap-cms-backend-gitea/package.json index 597846d537bd..b86b8b1edf1a 100644 --- a/packages/decap-cms-backend-gitea/package.json +++ b/packages/decap-cms-backend-gitea/package.json @@ -28,6 +28,7 @@ "decap-cms-lib-auth": "^3.0.0", "decap-cms-lib-util": "^3.0.0", "decap-cms-ui-default": "^3.0.0", + "decap-cms-ui-4": "^3.0.0", "immutable": "^3.7.6", "lodash": "^4.17.11", "prop-types": "^15.7.2", diff --git a/packages/decap-cms-backend-github/package.json b/packages/decap-cms-backend-github/package.json index 66e4509d6df8..cf145edfe74c 100644 --- a/packages/decap-cms-backend-github/package.json +++ b/packages/decap-cms-backend-github/package.json @@ -36,6 +36,7 @@ "decap-cms-lib-auth": "^3.0.0", "decap-cms-lib-util": "^3.0.0", "decap-cms-ui-default": "^3.0.0", + "decap-cms-ui-4": "^3.0.0", "lodash": "^4.17.11", "prop-types": "^15.7.2", "react": "^18.2.0" diff --git a/packages/decap-cms-backend-gitlab/package.json b/packages/decap-cms-backend-gitlab/package.json index f2bdae8e3a17..66e285a1c20d 100644 --- a/packages/decap-cms-backend-gitlab/package.json +++ b/packages/decap-cms-backend-gitlab/package.json @@ -32,6 +32,7 @@ "decap-cms-lib-auth": "^3.0.0", "decap-cms-lib-util": "^3.0.0", "decap-cms-ui-default": "^3.0.0", + "decap-cms-ui-4": "^3.0.0", "immutable": "^3.7.6", "lodash": "^4.17.11", "prop-types": "^15.7.2", diff --git a/packages/decap-cms-backend-proxy/package.json b/packages/decap-cms-backend-proxy/package.json index 37a31879bd54..722f28ae7772 100644 --- a/packages/decap-cms-backend-proxy/package.json +++ b/packages/decap-cms-backend-proxy/package.json @@ -22,6 +22,7 @@ "@emotion/styled": "^11.11.0", "decap-cms-lib-util": "^3.0.0", "decap-cms-ui-default": "^3.0.0", + "decap-cms-ui-4": "^3.0.0", "prop-types": "^15.7.2", "react": "^18.2.0" } diff --git a/packages/decap-cms-backend-test/package.json b/packages/decap-cms-backend-test/package.json index 694f70139fef..ab44233bae14 100644 --- a/packages/decap-cms-backend-test/package.json +++ b/packages/decap-cms-backend-test/package.json @@ -22,6 +22,7 @@ "@emotion/styled": "^11.11.0", "decap-cms-lib-util": "^3.0.0", "decap-cms-ui-default": "^3.0.0", + "decap-cms-ui-4": "^3.0.0", "lodash": "^4.17.11", "prop-types": "^15.7.2", "react": "^18.2.0", diff --git a/packages/decap-cms-core/package.json b/packages/decap-cms-core/package.json index 9adecfed1bd0..a0188b72b796 100644 --- a/packages/decap-cms-core/package.json +++ b/packages/decap-cms-core/package.json @@ -82,6 +82,7 @@ "decap-cms-lib-util": "^3.0.0", "decap-cms-lib-widgets": "^3.0.0", "decap-cms-ui-default": "^3.0.0", + "decap-cms-ui-4": "^3.0.0", "immutable": "^3.7.6", "lodash": "^4.17.11", "moment": "^2.24.0", diff --git a/packages/decap-cms-ui-4/CHANGELOG.md b/packages/decap-cms-ui-4/CHANGELOG.md new file mode 100644 index 000000000000..e4d87c4d45c4 --- /dev/null +++ b/packages/decap-cms-ui-4/CHANGELOG.md @@ -0,0 +1,4 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. diff --git a/packages/decap-cms-ui-4/README.md b/packages/decap-cms-ui-4/README.md new file mode 100644 index 000000000000..6e8d3e318ef9 --- /dev/null +++ b/packages/decap-cms-ui-4/README.md @@ -0,0 +1,6 @@ +# Decap CMS UI V4 + +This package contains the default UI elements that are used in Decap CMS v4. + +Part of the 4.0 update is a UI overhaul. This means that the `decap-cms-ui-legacy` package contains the 2.0 and 3.0 styling. +This `decap-cms-ui-4` package is entirely new, and contains the styles, and components for the new UI. diff --git a/packages/decap-cms-ui-4/package.json b/packages/decap-cms-ui-4/package.json new file mode 100644 index 000000000000..7089e3f787e9 --- /dev/null +++ b/packages/decap-cms-ui-4/package.json @@ -0,0 +1,32 @@ +{ + "name": "decap-cms-ui-4", + "description": "UI components for Decap CMS.", + "version": "1.0.0", + "repository": "https://github.com/decaporg/decap-cms/tree/master/packages/decap-cms-ui-4", + "bugs": "https://github.com/decaporg/decap-cms/issues", + "license": "MIT", + "module": "dist/esm/index.js", + "main": "dist/decap-cms-ui-4.js", + "keywords": [ + "decap-cms" + ], + "sideEffects": false, + "scripts": { + "develop": "yarn build:esm --watch", + "build": "cross-env NODE_ENV=production webpack", + "build:esm": "cross-env NODE_ENV=esm babel src --out-dir dist/esm --ignore \"**/__tests__\" --root-mode upward" + }, + "dependencies": { + "react-aria-menubutton": "^6.0.0", + "react-toggled": "^1.1.2", + "react-transition-group": "^2.6.0", + "typeface-inter": "^3.12.0" + }, + "peerDependencies": { + "@emotion/core": "^10.0.9", + "@emotion/styled": "^10.0.9", + "lodash": "^4.17.11", + "prop-types": "^15.7.2", + "react": "^16.8.4" + } +} diff --git a/packages/decap-cms-ui-4/src/AppBar/AppBar.jsx b/packages/decap-cms-ui-4/src/AppBar/AppBar.jsx new file mode 100644 index 000000000000..d1dee56193b2 --- /dev/null +++ b/packages/decap-cms-ui-4/src/AppBar/AppBar.jsx @@ -0,0 +1,114 @@ +import React, { useState, useEffect } from 'react'; +import styled from '@emotion/styled'; +import Icon from '../Icon'; +import Card from '../Card'; +import { IconButton } from '../Button'; +import { isWindowDown } from '../utils/responsive'; +import { useUIContext } from '../hooks'; + +const AppBarWrap = styled(Card)` + background-color: ${({ theme }) => theme.color.surface}; + height: 3.5rem; + display: flex; + position: fixed; + top: 0; + right: 0; + left: 0; + z-index: 100; +`; +AppBarWrap.defaultProps = { rounded: false, elevation: 'xs' }; +const TitleWrap = styled.div` + padding: 0.5rem 0; + margin-right: 1rem; + display: flex; + flex-direction: column; + justify-content: center; +`; +const Title = styled.div` + color: ${({ theme }) => theme.color.highEmphasis}; + font-weight: bold; +`; +const Breadcrumbs = styled.div` + display: flex; + font-size: ${({ hasTitle }) => (hasTitle ? `0.75rem` : `1rem`)}; + font-weight: bold; + color: ${({ theme, hasTitle }) => + hasTitle ? theme.color.lowEmphasis : theme.color.highEmphasis}; + align-items: center; + margin-top: 0.125rem; +`; +const Breadcrumb = styled.div` + margin-right: 0.25rem; +`; +const BreadcrumbSeparator = styled(Icon)` + margin-right: 0.25rem; + color: ${({ theme }) => theme.color.neutral['400']}; +`; +BreadcrumbSeparator.defaultProps = { + name: 'chevron-right', +}; +const ActionsWrap = styled.div` + padding: 0.5rem 1rem; + margin-left: 1rem; + display: flex; + align-items: center; + ${({ noBorder, theme }) => (noBorder ? `` : `box-shadow: -1px 0 0 0 ${theme.color.border}`)}; +`; + +const StartWrap = styled.div` + flex: 1; + display: flex; + align-items: center; +`; +const EndWrap = styled.div` + display: flex; + align-items: center; +`; + +const AppBar = ({ renderStart, renderEnd, renderActions }) => { + const [isMobile, setIsMobile] = useState(isWindowDown('xs')); + const { pageTitle, breadcrumbs } = useUIContext(); + const handleResize = () => setIsMobile(isWindowDown('xs')); + + useEffect(() => { + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, []); + + console.log({ renderStart, renderEnd }); + + return ( + + {isMobile && ( + + + + )} + + + {pageTitle && {pageTitle}} + {breadcrumbs && ( + + {!isMobile && + breadcrumbs.map((breadcrumb, index) => ( + + {breadcrumb.label} + {breadcrumbs.length !== index + 1 && ( + + )} + + ))} + + )} + + {renderStart && renderStart()} + + + {renderEnd && renderEnd()} + {!isMobile && renderActions && {renderActions()}} + + + ); +}; + +export default AppBar; diff --git a/packages/decap-cms-ui-4/src/AppBar/index.js b/packages/decap-cms-ui-4/src/AppBar/index.js new file mode 100644 index 000000000000..f35a4af33e2c --- /dev/null +++ b/packages/decap-cms-ui-4/src/AppBar/index.js @@ -0,0 +1 @@ +export { default } from './AppBar'; diff --git a/packages/decap-cms-ui-4/src/AppBar/story.jsx b/packages/decap-cms-ui-4/src/AppBar/story.jsx new file mode 100644 index 000000000000..55a50fcd583d --- /dev/null +++ b/packages/decap-cms-ui-4/src/AppBar/story.jsx @@ -0,0 +1,188 @@ +import React, { useState } from 'react'; +import styled from '@emotion/styled'; +import { withKnobs, boolean, text, object } from '@storybook/addon-knobs'; + +import AppBar from '.'; + +import { isWindowDown } from '../utils/responsive'; + +import { Button, ButtonGroup } from '../Button'; +import { Menu, MenuItem } from '../Menu'; +import Dialog from '../Dialog'; +import { toast } from '../Toast'; + +const Wrap = styled.div` + width: 100%; + height: 100%; + background-color: ${({ theme }) => theme.color.background}; +`; + +const PublishButton = styled(Button)` + margin-left: 0.5rem; +`; + +export default { + title: 'Components/AppBar', + decorators: [withKnobs], +}; + +export const _AppBar = () => { + const title = text('title', 'Post Title'); + const breadcrumbs = [{ label: 'My Website' }, { label: 'Posts' }]; + const renderStart = boolean('renderStart', false); + const renderEnd = boolean('renderEnd', false); + + return ( + + : null} + renderEnd={renderEnd ? () => : null} + /> + + ); +}; + +_AppBar.story = { + name: 'AppBar', +}; + +const StartContent = () => { + return
Start content
; +}; + +const EndContent = () => { + const languages = [ + { label: 'English', name: 'EN' }, + { label: 'Spanish', name: 'ES' }, + { label: 'Potugeuese', name: 'PT' }, + { label: 'Italian', name: 'IT' }, + { label: 'French', name: 'FR' }, + { label: 'German', name: 'DE' }, + { label: 'Russian', name: 'RU' }, + { label: 'Chinese', name: 'ZH' }, + { label: 'Japanese', name: 'JP' }, + ]; + + const [languageMenuAnchorEl, setLanguageMenuAnchorEl] = useState(null); + const [publishMenuAnchorEl, setPublishMenuAnchorEl] = useState(null); + const [postMenuAnchorEl, setPostMenuAnchorEl] = useState(null); + const [selectedLanguage, setSelectedLanguage] = useState('EN'); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + + const isMobile = isWindowDown('xs'); + + function handleClose() { + setLanguageMenuAnchorEl(null); + setPublishMenuAnchorEl(null); + setPostMenuAnchorEl(null); + } + + return ( +
+ {!isMobile && ( + <> + + setLanguageMenuAnchorEl(null)} + anchorOrigin={{ y: 'bottom', x: 'right' }} + > + {languages.map(language => ( + { + setSelectedLanguage(language.name); + handleClose(); + }} + > + {language.label} + + ))} + + + )} + + + + + } + > + Are you sure you want to delete this post? This action cannot be undone. + +
+ ); +}; diff --git a/packages/decap-cms-ui-4/src/AppWrap/AppWrap.jsx b/packages/decap-cms-ui-4/src/AppWrap/AppWrap.jsx new file mode 100644 index 000000000000..c216b74ab356 --- /dev/null +++ b/packages/decap-cms-ui-4/src/AppWrap/AppWrap.jsx @@ -0,0 +1,70 @@ +import React, { useEffect } from 'react'; +import { ThemeProvider } from 'emotion-theming'; +import styled from '@emotion/styled'; +import { lightTheme, darkTheme } from '../theme'; +import { ToastContainer } from '../Toast'; +import GlobalStyles from '../GlobalStyles'; +import { NavMenu } from '../NavMenu'; +import AppBar from '../AppBar'; +import { UIContext, UIProvider } from '../UIContext'; + +const AppOuter = styled.div` + padding-top: 3.5rem; + display: flex; + flex-direction: column; + height: 100%; +`; +const AppBody = styled.div` + display: flex; + flex-direction: row; + flex: 1; + overflow: hidden; + ${({ theme }) => theme.responsive.mediaQueryDown('xs')} { + flex-direction: column-reverse; + } +`; +const AppContent = styled.div` + flex: 1; + height: 100%; + max-height: 100%; + overflow-y: auto; +`; + +const AppWrap = ({ children }) => { + const handleResize = () => { + const vh = window.innerHeight * 0.01; + + document.documentElement.style.setProperty('--vh', `${vh}px`); + }; + + useEffect(() => { + window.addEventListener('resize', handleResize); + handleResize(); + + return () => window.removeEventListener('resize', handleResize); + }, []); + + return ( + + + {({ darkMode }) => ( + + + + + + + {children} + + + + + )} + + + ); +}; + +export default AppWrap; diff --git a/packages/decap-cms-ui-4/src/AppWrap/index.js b/packages/decap-cms-ui-4/src/AppWrap/index.js new file mode 100644 index 000000000000..7dea3253f92d --- /dev/null +++ b/packages/decap-cms-ui-4/src/AppWrap/index.js @@ -0,0 +1 @@ +export { default } from './AppWrap'; diff --git a/packages/decap-cms-ui-4/src/Avatar/Avatar.jsx b/packages/decap-cms-ui-4/src/Avatar/Avatar.jsx new file mode 100644 index 000000000000..0bdfb0b79e05 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Avatar/Avatar.jsx @@ -0,0 +1,18 @@ +import styled from '@emotion/styled'; + +// TODO: add name prop and display two initials + +export default styled.div` + border-radius: ${props => (props.size ? props.size : 32)}px; + width: ${props => (props.size ? props.size : 32)}px; + height: ${props => (props.size ? props.size : 32)}px; + transition: 200ms; + ${props => + props.src && + ` + background-image: url(${props.src}); + background-size: cover; + background-position: center; + `} + background-color: rgba(0, 0, 0, 0.25); +`; diff --git a/packages/decap-cms-ui-4/src/Avatar/index.js b/packages/decap-cms-ui-4/src/Avatar/index.js new file mode 100644 index 000000000000..ea51b745217b --- /dev/null +++ b/packages/decap-cms-ui-4/src/Avatar/index.js @@ -0,0 +1 @@ +export { default } from './Avatar'; diff --git a/packages/decap-cms-ui-4/src/Avatar/story.jsx b/packages/decap-cms-ui-4/src/Avatar/story.jsx new file mode 100644 index 000000000000..c166aeb33ca7 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Avatar/story.jsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { withKnobs, text, select } from '@storybook/addon-knobs'; + +import Avatar from '.'; + +export default { + title: 'Components/Avatar', + decorators: [withKnobs], +}; + +export const _Avatar = () => { + return ( + + ); +}; diff --git a/packages/decap-cms-ui-4/src/Backdrop/Backdrop.jsx b/packages/decap-cms-ui-4/src/Backdrop/Backdrop.jsx new file mode 100644 index 000000000000..d4dd5cd1d4a7 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Backdrop/Backdrop.jsx @@ -0,0 +1,49 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import styled from '@emotion/styled'; +import color from 'color'; +import Fade from '../transitions/Fade'; + +const StyledBackdrop = styled.div` + z-index: -1; + position: fixed; + right: 0; + bottom: 0; + top: 0; + left: 0; + background-color: ${({ theme, invisible }) => + invisible + ? 'transparent' + : theme.darkMode + ? color(theme.color.neutral['1600']) + .alpha(0.5) + .string() + : 'rgba(14,30,37,0.25)'}; + -webkit-tap-highlight-color: transparent; + touch-action: none; +`; + +const Backdrop = props => { + const { invisible, open, transitionDuration, onClick, ...other } = props; + + return ( + + + ); +}; + +Backdrop.propTypes = { + invisible: PropTypes.bool, + open: PropTypes.bool.isRequired, + transitionDuration: PropTypes.oneOfType([ + PropTypes.number, + PropTypes.shape({ enter: PropTypes.number, exit: PropTypes.number }), + ]), +}; + +Backdrop.defaultProps = { + invisible: false, +}; + +export default Backdrop; diff --git a/packages/decap-cms-ui-4/src/Backdrop/index.js b/packages/decap-cms-ui-4/src/Backdrop/index.js new file mode 100644 index 000000000000..4f57103db252 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Backdrop/index.js @@ -0,0 +1 @@ +export { default } from './Backdrop'; diff --git a/packages/decap-cms-ui-4/src/Button/AvatarButton.jsx b/packages/decap-cms-ui-4/src/Button/AvatarButton.jsx new file mode 100644 index 000000000000..9d3c1dcac836 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Button/AvatarButton.jsx @@ -0,0 +1,17 @@ +import styled from '@emotion/styled'; +import Avatar from '../Avatar'; + +const AvatarButton = styled(Avatar)` + cursor: pointer; + transition: 200ms; + ${({ active, theme }) => + active ? `box-shadow: 0 0 0 4px ${theme.color.elevatedSurfaceHighlight}` : ''} + &:hover { + opacity: 0.9; + } + &:active { + opacity: 0.75; + } +`; + +export default AvatarButton; diff --git a/packages/decap-cms-ui-4/src/Button/Button.jsx b/packages/decap-cms-ui-4/src/Button/Button.jsx new file mode 100644 index 000000000000..c676506ade18 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Button/Button.jsx @@ -0,0 +1,182 @@ +import React from 'react'; +import styled from '@emotion/styled'; +import color from 'color'; +import Icon from '../Icon'; +import ButtonGroup from './ButtonGroup'; + +export const StyledButton = styled.button` + ${({ theme, size, type, primary, transparent, disabled }) => ` + font-family: ${theme.fontFamily}; + font-size: ${size === 'sm' ? 12 : size === 'lg' ? 14 : 14}px; + padding: ${size === 'sm' ? `0 0.5rem` : size === 'lg' ? `0 1rem` : `0 0.75rem`}; + line-height: ${size === 'sm' ? `1.5rem` : size === 'lg' ? `2.5rem` : `2rem`}; + font-weight: 600; + text-align: center; + border-radius: ${size === 'sm' ? 4 : 6}px; + border: 0; + outline: 0; + cursor: ${disabled ? 'not-allowed' : 'pointer'}; + white-space: nowrap; + transition: 200ms; + background-color: ${ + transparent + ? 'transparent' + : primary + ? type === 'success' + ? theme.color.success['900'] + : type === 'danger' + ? theme.color.danger['900'] + : theme.color.neutral[theme.darkMode ? '800' : '800'] + : type === 'success' + ? color(theme.color.success['900']) + .alpha(0.2) + .string() + : type === 'danger' + ? color(theme.color.danger['900']) + .alpha(0.2) + .string() + : color(theme.color.neutral['700']) + .alpha(0.2) + .string() + }; + color: ${ + primary + ? '#FFFFFF' + : type === 'success' + ? theme.color.success[theme.darkMode ? '200' : '700'] + : type === 'danger' + ? theme.color.danger[theme.darkMode ? '200' : '700'] + : theme.color.neutral[theme.darkMode ? '300' : '1000'] + }; + ${ + disabled + ? 'opacity: 0.5;' + : ` + &:hover { + background-color: ${ + transparent + ? 'rgba(0,0,0,0.15)' + : primary + ? type === 'success' + ? theme.color.success['800'] + : type === 'danger' + ? theme.color.danger['800'] + : theme.color.neutral['700'] + : type === 'success' + ? color(theme.color.success['900']) + .alpha(theme.darkMode ? 0.35 : 0.1) + .string() + : type === 'danger' + ? color(theme.color.danger['900']) + .alpha(theme.darkMode ? 0.35 : 0.1) + .string() + : color(theme.color.neutral['700']) + .alpha(theme.darkMode ? 0.35 : 0.1) + .string() + }; + } + &:focus, + &:focus:hover { + background-color: ${ + primary + ? type === 'success' + ? theme.color.success['800'] + : type === 'danger' + ? theme.color.danger['800'] + : theme.color.neutral['700'] + : type === 'success' + ? color(theme.color.success['900']) + .alpha(theme.darkMode ? 0.35 : 0.1) + .string() + : type === 'danger' + ? color(theme.color.danger['900']) + .alpha(theme.darkMode ? 0.35 : 0.1) + .string() + : color(theme.color.neutral['700']) + .alpha(theme.darkMode ? 0.35 : 0.1) + .string() + }; + } + &:active, + &:active:focus { + background-color: ${ + primary + ? type === 'success' + ? theme.color.success['1000'] + : type === 'danger' + ? theme.color.danger['1000'] + : theme.color.neutral['900'] + : type === 'success' + ? color(theme.color.success['500']) + .alpha(theme.darkMode ? 0.1 : 0.35) + .string() + : type === 'danger' + ? color(theme.color.danger['900']) + .alpha(theme.darkMode ? 0.1 : 0.35) + .string() + : color(theme.color.neutral['700']) + .alpha(theme.darkMode ? 0.1 : 0.35) + .string() + }; + } + ` + } + ${ButtonGroup} & { + margin: 4px; + } + `} + ${({ type, theme, iconOnly }) => + iconOnly + ? ` + color: ${theme.color.neutral['700']}; + padding: 0 0.5rem; + &, + &:hover, + &:focus, + &:active, + &:focus:hover, + &:focus:active { + background-color: transparent; + } + &:hover { + color: ${type === 'danger' ? theme.color.danger['900'] : theme.color.primary['900']} + } + ` + : ''} +`; +const Caret = styled.div` + display: inline-block; + vertical-align: middle; + width: 0; + height: 0; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-top: 5px solid; + margin-left: 0.5rem; +`; + +const StyledIcon = styled(Icon)` + ${({ hasText, size }) => + hasText ? `margin-right: ${size === 'sm' ? 0.25 : size === 'lg' ? 0.75 : 0.5}rem;` : ''} + ${({ hasText, size }) => + hasText ? `margin-left: -${size === 'sm' ? 0.125 : size === 'lg' ? 0.375 : 0.25}rem;` : ''} + margin-top: -${({ size }) => (size === 'sm' ? 1.5 : size === 'lg' ? 3 : 2)}px; + margin-bottom: -${({ size }) => (size === 'sm' ? 0.75 : size === 'lg' ? 1.5 : 1)}px; + vertical-align: middle; +`; + +const Button = ({ icon, children, className, hasMenu, size, disabled, ...props }) => ( + + {icon && } + {children} + {hasMenu && } + +); + +export default Button; diff --git a/packages/decap-cms-ui-4/src/Button/ButtonGroup.jsx b/packages/decap-cms-ui-4/src/Button/ButtonGroup.jsx new file mode 100644 index 000000000000..39e5d4ec580a --- /dev/null +++ b/packages/decap-cms-ui-4/src/Button/ButtonGroup.jsx @@ -0,0 +1,14 @@ +import styled from '@emotion/styled'; + +const ButtonGroup = styled.div` + margin: -4px; + display: inline-flex; + align-items: ${({ direction }) => (direction === 'vertical' ? 'stretch' : 'center')}; + flex-wrap: wrap; + ${({ direction }) => (direction === 'vertical' ? `flex-direction: column;` : ``)} + & > * { + margin: 4px; + } +`; + +export default ButtonGroup; diff --git a/packages/decap-cms-ui-4/src/Button/IconButton.jsx b/packages/decap-cms-ui-4/src/Button/IconButton.jsx new file mode 100644 index 000000000000..e9d569ba864c --- /dev/null +++ b/packages/decap-cms-ui-4/src/Button/IconButton.jsx @@ -0,0 +1,63 @@ +import React from 'react'; +import styled from '@emotion/styled'; +import Icon from '../Icon'; +import ButtonGroup from './ButtonGroup'; +import color from 'color'; + +const IconButtonWrap = styled.button` + color: ${({ theme, active }) => + active ? theme.color.success['900'] : theme.color.mediumEmphasis}; + background-color: ${({ theme, active }) => + active + ? color(theme.color.success['900']) + .alpha(0.1) + .string() + : `transparent`}; + border: none; + padding: 0.25rem; + width: ${({ size }) => (size === 'lg' ? 2.5 : size === 'sm' ? 1.5 : 2)}rem; + height: ${({ size }) => (size === 'lg' ? 2.5 : size === 'sm' ? 1.5 : 2)}rem; + border-radius: 6px; + display: flex; + justify-content: center; + align-items: center; + outline: none; + cursor: pointer; + transition: 250ms; + &:hover, + &:focus { + color: ${({ theme, active }) => + active ? theme.color.success['900'] : theme.color.highEmphasis}; + background-color: ${({ theme, active }) => + active + ? color(theme.color.success['900']) + .alpha(0.2) + .string() + : color(theme.color.highEmphasis) + .alpha(0.05) + .string()}; + } + &:active { + color: ${({ theme, active }) => + active ? theme.color.success['900'] : theme.color.highEmphasis}; + background-color: ${({ theme, active }) => + active + ? color(theme.color.success['900']) + .alpha(0.3) + .string() + : color(theme.color.highEmphasis) + .alpha(0.1) + .string()}; + } + ${ButtonGroup} & { + margin: 2px; + } +`; + +const IconButton = ({ icon, size, ...props }) => ( + + + +); + +export default IconButton; diff --git a/packages/decap-cms-ui-4/src/Button/index.js b/packages/decap-cms-ui-4/src/Button/index.js new file mode 100644 index 000000000000..8f8bc9b426c2 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Button/index.js @@ -0,0 +1,4 @@ +export { default as Button } from './Button'; +export { default as ButtonGroup } from './ButtonGroup'; +export { default as IconButton } from './IconButton'; +export { default as AvatarButton } from './AvatarButton'; diff --git a/packages/decap-cms-ui-4/src/Button/story.jsx b/packages/decap-cms-ui-4/src/Button/story.jsx new file mode 100644 index 000000000000..f568df9efc34 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Button/story.jsx @@ -0,0 +1,94 @@ +import React from 'react'; +import { action } from '@storybook/addon-actions'; +import { withKnobs, boolean, select, text } from '@storybook/addon-knobs'; + +import { Button, ButtonGroup, IconButton, AvatarButton } from '.'; +import { iconComponents } from '../Icon/Icon'; + +export default { + title: 'Components/Buttons', + decorators: [withKnobs], +}; + +export const _Button = () => { + return ( + + + + ); +}; + +export const _ButtonGroup = () => { + return ( + + + + + + ); +}; + +_ButtonGroup.story = { + name: 'ButtonGroup', +}; + +export const _IconButton = () => { + return ( + + ({ ...acc, [key]: key }), {}), + }, + null, + )} + size={select('size', { sm: 'sm', 'md (default)': null, lg: 'lg' }, null)} + active={boolean('active', false)} + onClick={action('onClick')} + /> + + ); +}; + +_IconButton.story = { + name: 'IconButton', +}; + +export const _AvatarButton = () => { + return ( + + + + ); +}; + +_AvatarButton.story = { + name: 'AvatarButton', +}; diff --git a/packages/decap-cms-ui-4/src/Card/Card.jsx b/packages/decap-cms-ui-4/src/Card/Card.jsx new file mode 100644 index 000000000000..ed8cce279da8 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Card/Card.jsx @@ -0,0 +1,17 @@ +import styled from '@emotion/styled'; + +const Card = styled.div` + background-color: ${({ theme }) => theme.color.elevatedSurface}; + ${({ rounded }) => + rounded ? `border-radius: ${rounded === 'sm' ? 2 : rounded === 'lg' ? 6 : 4}px` : ``}; + box-shadow: ${({ theme, direction, elevation }) => + theme.shadow({ + direction, + size: elevation && typeof elevation === 'string' ? elevation : 'xs', + theme, + })}; +`; + +Card.defaultProps = { direction: 'down', rounded: 'lg' }; + +export default Card; diff --git a/packages/decap-cms-ui-4/src/Card/index.js b/packages/decap-cms-ui-4/src/Card/index.js new file mode 100644 index 000000000000..c68311df80b2 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Card/index.js @@ -0,0 +1 @@ +export { default } from './Card'; diff --git a/packages/decap-cms-ui-4/src/Card/story.jsx b/packages/decap-cms-ui-4/src/Card/story.jsx new file mode 100644 index 000000000000..12b05530966a --- /dev/null +++ b/packages/decap-cms-ui-4/src/Card/story.jsx @@ -0,0 +1,33 @@ +import React from 'react'; +import styled from '@emotion/styled'; +import { withKnobs, select } from '@storybook/addon-knobs'; + +import Card from '.'; + +const StyledCard = styled(Card)` + width: 20rem; + height: 20rem; +`; + +export default { + title: 'Components/Card', + decorators: [withKnobs], +}; + +export const _Card = () => { + return ( + + ); +}; + +_Card.story = { + name: 'Card', +}; diff --git a/packages/decap-cms-ui-4/src/Dialog/Dialog.jsx b/packages/decap-cms-ui-4/src/Dialog/Dialog.jsx new file mode 100644 index 000000000000..d88999c32bab --- /dev/null +++ b/packages/decap-cms-ui-4/src/Dialog/Dialog.jsx @@ -0,0 +1,327 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import Modal from '../Modal'; +import Card from '../Card'; +import Grow from '../transitions/Grow'; +import Slide from '../transitions/Slide'; +import { ButtonGroup, IconButton } from '../Button'; +import { StyledButton } from '../Button/Button'; +import { isWindowDown } from '../utils/responsive'; + +import styled from '@emotion/styled'; + +const DialogWrap = styled(Card)` + outline: none; + ${({ position, width }) => !width && position.x !== 'stretch' && `min-width: 20rem;`} + ${({ position, width }) => !width && position.x !== 'stretch' && `max-width: 30rem;`} + ${({ width }) => width && `width: ${width};`} + ${({ position }) => position.x === 'stretch' && `flex: 1;`} + border-radius: ${({ position }) => { + if ( + (position.x === 'center' && position.y === 'center') || + (position.x === 'right' && position.y === 'center') || + (position.x === 'center' && position.y === 'bottom') || + (position.x === 'right' && position.y === 'bottom') + ) + return 8; + return 0; + }}px ${({ position }) => { + if ( + (position.x === 'center' && position.y === 'center') || + (position.x === 'left' && position.y === 'center') || + (position.x === 'center' && position.y === 'bottom') || + (position.x === 'left' && position.y === 'bottom') + ) + return 8; + return 0; +}}px ${({ position }) => { + if ( + (position.x === 'center' && position.y === 'center') || + (position.x === 'left' && position.y === 'center') || + (position.x === 'center' && position.y === 'top') || + (position.x === 'left' && position.y === 'top') + ) + return 8; + return 0; +}}px ${({ position }) => { + if ( + (position.x === 'center' && position.y === 'center') || + (position.x === 'right' && position.y === 'center') || + (position.x === 'center' && position.y === 'top') || + (position.x === 'right' && position.y === 'top') + ) + return 8; + return 0; +}}px; +`; + +// const DialogWrap = styled.div` +// background-color: ${({ theme }) => theme.color.elevatedSurface}; +// outline: none; +// +// ${({ position, width }) => !width && position.x !== 'stretch' && `min-width: 20rem;`} +// ${({ position, width }) => !width && position.x !== 'stretch' && `max-width: 30rem;`} +// ${({ width }) => width && `width: ${width};`} +// box-shadow: +// ${({ position }) => { +// if (position.x === 'center') return 0; +// if (position.x === 'left') return 4; +// if (position.x === 'right') return -4; +// if (position.x === 'stretch') return 0; +// }}px ${({ position }) => { +// if (position.y === 'center') return 4; +// if (position.y === 'top') return 4; +// if (position.y === 'bottom') return -4; +// if (position.y === 'stretch') return 0; +// }}px 16px ${({ theme }) => (theme.darkMode ? 'rgba(0, 0, 0, 0.5)' : 'rgba(14,30,37,0.25)')}, ${({ +// position, +// }) => { +// if (position.x === 'center') return 0; +// if (position.x === 'left') return 32; +// if (position.x === 'right') return -32; +// if (position.x === 'stretch') return 0; +// }}px ${({ position }) => { +// if (position.y === 'center') return 32; +// if (position.y === 'top') return 32; +// if (position.y === 'bottom') return -32; +// if (position.y === 'stretch') return 0; +// }}px 64px ${({ theme }) => (theme.darkMode ? 'rgba(0, 0, 0, 0.3)' : 'rgba(14,30,37,0.15)')}; +// ${({ position }) => position.x === 'stretch' && `flex: 1;`} +// `; +const Header = styled.div` + justify-content: space-between; + align-items: center; +`; +const Title = styled.h1` + color: ${({ theme }) => theme.color.highEmphasis}; + font-family: ${({ theme }) => theme.fontFamily}; + font-size: 1.5rem; + font-weight: bold; + line-height: 2rem; + padding: ${({ hasContent, hasCloseButton }) => `1.5rem ${hasCloseButton ? 2 : 1}rem + ${hasContent ? 1 : 2}rem 2rem`}; + margin: 0; + margin-right: 3.5rem; + ${({ theme }) => theme.responsive.mediaQueryDown('xs')} { + margin-right: 0; + padding: 1.25rem ${props => (props.hasCloseButton ? 1 : 0.5)}rem + ${props => (props.hasContent ? 0.5 : 1)}rem 1rem; + } +`; +const CloseButton = styled(IconButton)` + position: absolute; + top: 1.5rem; + right: 1.5rem; + ${({ theme }) => theme.responsive.mediaQueryDown('xs')} { + display: none; + } +`; +CloseButton.defaultProps = { + icon: 'x', + size: 'large', + type: 'danger', +}; +const DialogContent = styled.div` + font-family: ${({ theme }) => theme.fontFamily}; + line-height: 1.5; + color: ${({ theme }) => theme.color.mediumEmphasis}; + padding: ${props => (props.hasTitle || props.noPadding ? 0 : 1.75)}rem + ${props => { + if (props.hasCloseButton && !props.hasTitle && !props.noPadding) return 5; + if (props.noPadding) return 0; + return 2; + }}rem + ${props => { + if (props.noPadding) return 0; + if (props.hasActions) return 1.5; + return 2; + }}rem + ${props => (props.noPadding ? 0 : 2)}rem; + + ${({ theme }) => theme.responsive.mediaQueryDown('xs')} { + padding: ${props => (props.hasTitle || props.noPadding ? 0 : 1)}rem + ${props => { + if (props.hasCloseButton && !props.hasTitle && !props.noPadding) return 1; + if (props.noPadding) return 0; + return 1; + }}rem + ${props => { + if (props.noPadding) return 0; + if (props.hasActions) return 1.25; + return 1.25; + }}rem + ${props => (props.noPadding ? 0 : 1)}rem; + } +`; +const DialogActions = styled.div` + padding: 0 2rem 2rem 2rem; + /* border-top: 1px solid ${({ theme }) => theme.color.border}; */ + display: flex; + justify-content: flex-end; + ${({ theme }) => theme.responsive.mediaQueryDown('xs')} { + padding: 0 1rem 1.25rem 1rem; + & ${StyledButton} { + padding: 10px; + font-size: 1rem; + } + } + ${({ isMobile }) => + isMobile + ? ` + flex: 1; + flex-direction: column; + align-items: stretch; + & ${ButtonGroup} { + flex: 1; + flex-direction: column; + align-items: stretch; + } + ` + : ``} +`; + +class Dialog extends React.Component { + static propTypes = { + title: PropTypes.string, + onClose: PropTypes.func, + }; + + handleBackdropClick = event => { + if (event.target !== event.currentTarget) { + return; + } + + if (this.props.onBackdropClick) { + this.props.onBackdropClick(event); + } + + if (!this.props.disableBackdropClick && this.props.onClose) { + this.props.onClose(event, 'backdropClick'); + } + }; + + render() { + const { + open, + title, + actions, + onClose, + children, + className, + hideCloseButton, + transitionDuration, + BackdropProps, + disableBackdropClick, + disableEscapeKeyDown, + onEscapeKeyDown, + onEnter, + onEntering, + onEntered, + onExit, + onExiting, + onExited, + width, + zIndex, + ...other + } = this.props; + + let { TransitionComponent, TransitionProps } = this.props; + + const isMobile = isWindowDown('xs'); + + const position = isMobile + ? { x: 'stretch', y: 'bottom' } + : this.props.position + ? { ...this.props.position } + : { x: 'center', y: 'center' }; + console.log({ isMobile, position }); + + let direction; + + if (position.x === 'left') direction = 'right'; + if (position.x === 'right') direction = 'left'; + if (position.y === 'top') direction = 'down'; + if (position.y === 'bottom') direction = 'up'; + + if ( + position.x === 'left' || + position.x === 'right' || + position.y === 'top' || + position.y === 'bottom' + ) { + TransitionComponent = Slide; + } + + return ( + + + + {title && ( +
+ + {title} + + {!hideCloseButton && (title || actions) && } +
+ )} + {children && (title || actions) ? ( + + {children} + + ) : ( + children + )} + {actions && {actions}} +
+
+
+ ); + } +} + +Dialog.defaultProps = { + TransitionComponent: Grow, + transitionDuration: 200, +}; + +export default Dialog; diff --git a/packages/decap-cms-ui-4/src/Dialog/index.js b/packages/decap-cms-ui-4/src/Dialog/index.js new file mode 100644 index 000000000000..e3f43f0af41c --- /dev/null +++ b/packages/decap-cms-ui-4/src/Dialog/index.js @@ -0,0 +1 @@ +export { default } from './Dialog'; diff --git a/packages/decap-cms-ui-4/src/Dialog/story.jsx b/packages/decap-cms-ui-4/src/Dialog/story.jsx new file mode 100644 index 000000000000..5a0574097f36 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Dialog/story.jsx @@ -0,0 +1,63 @@ +import React, { useState } from 'react'; +import { withKnobs, boolean, select, text } from '@storybook/addon-knobs'; + +import Dialog from '.'; +import { Button, ButtonGroup } from '../Button'; + +export default { + title: 'Components/Dialog', + decorators: [withKnobs], +}; + +const StoryDialog = () => { + const [dialogOpen, setDialogOpen] = useState(false); + + return ( +
+ + + + setDialogOpen(false)} + actions={ + boolean('actions', true) && ( + + + + + ) + } + > + {text('children', 'Help me, Obi-Wan Kenobi. You’re my only hope.')} + +
+ ); +}; + +export const _Dialog = () => ; diff --git a/packages/decap-cms-ui-4/src/Editor/Editor.jsx b/packages/decap-cms-ui-4/src/Editor/Editor.jsx new file mode 100644 index 000000000000..6229c8b29315 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Editor/Editor.jsx @@ -0,0 +1,175 @@ +import React from 'react'; +import styled from '@emotion/styled'; +import { + TextWidget, + DateWidget, + ListWidget, + BooleanWidget, + ObjectWidget, + SelectWidget, + MarkdownWidget, +} from '../widgets'; + +import TestSandbox from '../TestSandbox'; + +const EditorWrap = styled.div` + background-color: ${({ theme }) => theme.color.surface}; +`; + +class Editor extends React.Component { + state = { entry: {}, debug: true }; + setValue = valObj => + this.setState({ + entry: { + ...this.state.entry, + ...valObj, + }, + }); + + render() { + const { entry, debug } = this.state; + return ( + + this.setValue({ title })} + /> + this.setValue({ datePublished })} + /> + this.setValue({ category })} + options={[ + { name: 'general', label: 'General' }, + { name: 'advice', label: 'Advice' }, + { name: 'opinion', label: 'Opinion' }, + { name: 'technology', label: 'Technology' }, + { name: 'businessFinance', label: 'Business & Finance' }, + { name: 'foodCooking', label: 'Food & Cooking' }, + { name: 'worldPolitics', label: 'World & Politics' }, + { name: 'moviesEntretainment', label: 'Movies & Entertainment' }, + { name: 'lifestyle', label: 'Lifestyle' }, + { name: 'homeGardening', label: 'Home & Gardening' }, + ]} + /> + this.setValue({ description })} + /> + this.setValue({ featured })} + /> + this.setValue({ body })} /> + this.setValue({ features })} + fields={(setItemValue, index) => ( + + setItemValue({ featureTitle }, index)} + /> + setItemValue({ featureDescription }, index)} + /> + + this.setValue({ + features: [ + ...entry.features.filter((feature, i) => i !== index), + { + ...entry.features[index], + featureLinks, + }, + ], + }) + } + fields={(setLinkItemValue, linkIndex) => ( + + setLinkItemValue({ featureLinkText }, linkIndex)} + /> + setLinkItemValue({ featureLinkPath }, linkIndex)} + /> + + )} + /> + setItemValue({ featureNotes }, index)} + /> + + )} + /> + this.setValue({ link })} + fields={setValue => ( + + setValue({ featuredImage })} + /> + setValue({ path })} + /> + + )} + /> + this.setState({ debug })} + /> + {debug && } + + ); + } +} + +export default Editor; diff --git a/packages/decap-cms-ui-4/src/Editor/index.js b/packages/decap-cms-ui-4/src/Editor/index.js new file mode 100644 index 000000000000..e7967e758ab1 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Editor/index.js @@ -0,0 +1 @@ +export { default } from './Editor'; diff --git a/packages/decap-cms-ui-4/src/Field/Field.jsx b/packages/decap-cms-ui-4/src/Field/Field.jsx new file mode 100644 index 000000000000..71c9c777d525 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Field/Field.jsx @@ -0,0 +1,150 @@ +import React from 'react'; +import styled from '@emotion/styled'; +import Label from '../Label'; +import { IconButton } from '../Button'; + +export const FieldContext = React.createContext(); + +const FocusIndicator = styled.div` + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: ${({ focus }) => (focus ? 2 : 1)}px; + background-color: ${({ theme, error }) => + error ? theme.color.danger[900] : theme.color.primary[900]}; + transition: 0.2s; + transform: scaleX(${({ focus }) => (focus ? 1 : 0)}); + backface-visibility: hidden; +`; +const FieldWrap = styled.div` + ${({ inline }) => + inline + ? ` + padding: 0 1rem; + ` + : ``} + position: relative; + ${({ theme, noBorder, inline, error, focus, clickable }) => + inline && !noBorder + ? ` + box-shadow: inset 0 -1px 0 0 ${error ? theme.color.danger[900] : theme.color.border}; + transition: box-shadow 0.2s; + ${ + clickable && !error && !focus + ? ` + &:hover { + box-shadow: inset 0 -1px 0 0 ${theme.color.borderHover}; + } + ` + : `` + } + ` + : ``} + &:hover ${FocusIndicator} { + height: 2px; + } +`; +const FieldInside = styled.div` + ${({ control }) => (control ? `display: flex; align-items: center;` : ``)} + ${({ clickable }) => (clickable ? `cursor: pointer;` : ``)} + max-width: 800px; + margin: 0 auto; + position: relative; + ${({ inline, focus, theme, error, icon, clickable }) => + inline + ? `padding: 1rem 0;` + : ` + padding: 1rem; + ${icon ? `padding-right: 3rem;` : ``} + box-shadow: inset 0 0 0 ${focus ? 2 : 1}px ${ + error + ? theme.color.danger['900'] + : focus + ? theme.color.primary['900'] + : theme.color.border + }; + border-radius: 8px; + transition: 0.2s; + ${ + clickable && !error && !focus + ? ` + &:hover { + box-shadow: inset 0 0 0 1px ${theme.color.borderHover}; + } + ` + : `` + } + `} +`; +const ChildrenWrap = styled.div` + position: relative; +`; +const StyledLabel = styled(Label)` + font-family: ${({ theme }) => theme.fontFamily}; + ${({ control }) => (control ? `flex: 1; margin: 0;` : ``)} + ${({ clickable }) => (clickable ? `cursor: pointer;` : ``)}; + ${({ theme, error }) => (error ? `color: ${theme.color.danger['900']};` : ``)}; +`; +const StyledIconButton = styled(IconButton)` + position: absolute; + right: ${({ inline }) => (inline ? 0 : 0.5)}rem; + bottom: 0.5rem; +`; + +const Field = ({ + focus, + inline, + labelTarget, + label, + children, + control, + onClick, + className, + noBorder, + insideStyle, + error, + icon, + clickable, +}) => ( + + + + {label} + + {children} + {icon && } + + {!noBorder && inline && } + +); + +export const withFieldContext = Component => props => ( + {context => } +); + +export default withFieldContext(Field); diff --git a/packages/decap-cms-ui-4/src/Field/index.js b/packages/decap-cms-ui-4/src/Field/index.js new file mode 100644 index 000000000000..f30d9895cf40 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Field/index.js @@ -0,0 +1,2 @@ +export { default } from './Field'; +export * from './Field'; diff --git a/packages/decap-cms-ui-4/src/Fullscreen/Fullscreen.jsx b/packages/decap-cms-ui-4/src/Fullscreen/Fullscreen.jsx new file mode 100644 index 000000000000..9f812c4bd9ca --- /dev/null +++ b/packages/decap-cms-ui-4/src/Fullscreen/Fullscreen.jsx @@ -0,0 +1,27 @@ +import React from 'react'; +import styled from '@emotion/styled'; + +const FullscreenWrap = styled.div` + ${({ isFullscreen, theme }) => + isFullscreen + ? ` + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 99999; + background-color: ${theme.color.surface}; + ` + : ``} +`; + +const Fullscreen = ({ isFullscreen, children, className }) => { + return ( + + {children} + + ); +}; + +export default Fullscreen; diff --git a/packages/decap-cms-ui-4/src/Fullscreen/index.js b/packages/decap-cms-ui-4/src/Fullscreen/index.js new file mode 100644 index 000000000000..94e000658c5e --- /dev/null +++ b/packages/decap-cms-ui-4/src/Fullscreen/index.js @@ -0,0 +1 @@ +export { default } from './Fullscreen'; diff --git a/packages/decap-cms-ui-4/src/GlobalStyles/GlobalStyles.jsx b/packages/decap-cms-ui-4/src/GlobalStyles/GlobalStyles.jsx new file mode 100644 index 000000000000..0672c9f9cf3a --- /dev/null +++ b/packages/decap-cms-ui-4/src/GlobalStyles/GlobalStyles.jsx @@ -0,0 +1,82 @@ +import React from 'react'; +import { Global, css } from '@emotion/core'; +import color from 'color'; +import { withTheme } from 'emotion-theming'; +import interTypeface from 'typeface-inter/inter.css'; + +const getGlobalStyles = theme => css` + html { + box-sizing: border-box; + } + *, + *:before, + *:after { + box-sizing: inherit; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); + } + + *::selection { + background-color: ${color(theme.color.success['900']) + .alpha(0.3) + .string()}; + color: ${theme.color.success[theme.darkMode ? '200' : '1500']}; + } + + ${interTypeface} + + html, + body { + font-family: ${theme.fontFamily}; + font-size: 16px; + -webkit-font-smoothing: antialiased; + margin: 0; + padding: 0; + color: ${theme.color.neutral['800']}; + background: ${theme.color.background}; + height: 100%; + } + + #root { + height: 100%; + } + + h1, + h2, + h3, + h4, + h5, + h6, + p, + ul, + ol { + margin-top: 0; + -webkit-font-smoothing: antialiased; + } + + h1, + h2, + h3, + h4, + h5, + h6 { + color: ${theme.color.highEmphasis}; + } + + p { + color: ${theme.color.mediumEmphasis}; + margin-bottom: 24px; + } + + a { + color: ${theme.color.primary['1500']}; + text-decoration: none; + } + + #nc-root { + height: 100%; + } +`; + +const GlobalStyles = ({ theme }) => ; + +export default withTheme(GlobalStyles); diff --git a/packages/decap-cms-ui-4/src/GlobalStyles/index.js b/packages/decap-cms-ui-4/src/GlobalStyles/index.js new file mode 100644 index 000000000000..48e201890924 --- /dev/null +++ b/packages/decap-cms-ui-4/src/GlobalStyles/index.js @@ -0,0 +1 @@ +export { default } from './GlobalStyles'; diff --git a/packages/decap-cms-ui-4/src/HOC/index.js b/packages/decap-cms-ui-4/src/HOC/index.js new file mode 100644 index 000000000000..3600949dbb4f --- /dev/null +++ b/packages/decap-cms-ui-4/src/HOC/index.js @@ -0,0 +1 @@ +export * from './withUIContext'; diff --git a/packages/decap-cms-ui-4/src/HOC/withUIContext.jsx b/packages/decap-cms-ui-4/src/HOC/withUIContext.jsx new file mode 100644 index 000000000000..35c15139fa0b --- /dev/null +++ b/packages/decap-cms-ui-4/src/HOC/withUIContext.jsx @@ -0,0 +1,6 @@ +import React from 'react'; +import { UIContext } from '../UIContext'; + +export const withUIContext = Component => props => ( + {context => } +); diff --git a/packages/decap-cms-ui-4/src/Icon/Icon.jsx b/packages/decap-cms-ui-4/src/Icon/Icon.jsx new file mode 100644 index 000000000000..6a99efc6aec3 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/Icon.jsx @@ -0,0 +1,217 @@ +import React from 'react'; +import Activity from './icons/Activity'; +import AlertCircle from './icons/AlertCircle'; +import AlertTriangle from './icons/AlertTriangle'; +import ArrowDown from './icons/ArrowDown'; +import ArrowLeft from './icons/ArrowLeft'; +import ArrowRight from './icons/ArrowRight'; +import ArrowUp from './icons/ArrowUp'; +import AtSign from './icons/AtSign'; +import BarChart from './icons/BarChart'; +import Bell from './icons/Bell'; +import Bitbucket from './icons/Bitbucket'; +import Bold from './icons/Bold'; +import Box from './icons/Box'; +import BulletedList from './icons/BulletedList'; +import Calendar from './icons/Calendar'; +import Check from './icons/Check'; +import ChevronDown from './icons/ChevronDown'; +import ChevronRight from './icons/ChevronRight'; +import Clock from './icons/Clock'; +import Code from './icons/Code'; +import CodeBlock from './icons/CodeBlock'; +import Compass from './icons/Compass'; +import Copy from './icons/Copy'; +import Database from './icons/Database'; +import Edit3 from './icons/Edit3'; +import Edit from './icons/Edit'; +import ExternalLink from './icons/ExternalLink'; +import EyeOff from './icons/EyeOff'; +import Eye from './icons/Eye'; +import File from './icons/File'; +import Files from './icons/Files'; +import FileText from './icons/FileText'; +import Film from './icons/Film'; +import Filter from './icons/Filter'; +import Flag from './icons/Flag'; +import Folder from './icons/Folder'; +import Github from './icons/Github'; +import Gitlab from './icons/Gitlab'; +import Globe from './icons/Globe'; +import Grid from './icons/Grid'; +import HardDrive from './icons/HardDrive'; +import HeadingOne from './icons/HeadingOne'; +import HeadingTwo from './icons/HeadingTwo'; +import HeadingThree from './icons/HeadingThree'; +import HelpCircle from './icons/HelpCircle'; +import Image from './icons/Image'; +import Inbox from './icons/Inbox'; +import Info from './icons/Info'; +import Italic from './icons/Italic'; +import Layout from './icons/Layout'; +import Link from './icons/Link'; +import Lock from './icons/Lock'; +import LogOut from './icons/LogOut'; +import Mail from './icons/Mail'; +import MapPin from './icons/MapPin'; +import Maximize from './icons/Maximize'; +import Menu from './icons/Menu'; +import Minimize from './icons/Minimize'; +import Moon from './icons/Moon'; +import MoreHorizontal from './icons/MoreHorizontal'; +import MoreVertical from './icons/MoreVertical'; +import Move from './icons/Move'; +import Music from './icons/Music'; +import Netlify from './icons/Netlify'; +import NumberedList from './icons/NumberedList'; +import Package from './icons/Package'; +import Paperclip from './icons/Paperclip'; +import PieChart from './icons/PieChart'; +import Pin from './icons/Pin'; +import PlusCircle from './icons/PlusCircle'; +import Plus from './icons/Plus'; +import Quote from './icons/Quote'; +import Radio from './icons/Radio'; +import Save from './icons/Save'; +import Search from './icons/Search'; +import Server from './icons/Server'; +import Settings from './icons/Settings'; +import Share2 from './icons/Share2'; +import ShoppingCart from './icons/ShoppingCart'; +import Sidebar from './icons/Sidebar'; +import Star from './icons/Star'; +import Sun from './icons/Sun'; +import Trash2 from './icons/Trash2'; +import Underline from './icons/Underline'; +import Unsplash from './icons/Unsplash'; +import UploadCloud from './icons/UploadCloud'; +import Upload from './icons/Upload'; +import User from './icons/User'; +import Users from './icons/Users'; +import Workflow from './icons/Workflow'; +import X from './icons/X'; +import Zap from './icons/Zap'; + +// prettier-ignore +export const iconComponents = { + activity: Activity, + 'alert-circle': AlertCircle, + 'alert-triangle': AlertTriangle, + 'arrow-down': ArrowDown, + 'arrow-left': ArrowLeft, + 'arrow-right': ArrowRight, + 'arrow-up': ArrowUp, + 'at-sign': AtSign, + 'bar-chart': BarChart, + bell: Bell, + bitbucket: Bitbucket, + bold: Bold, + box: Box, + 'bulleted-list': BulletedList, + calendar: Calendar, + check: Check, + 'chevron-down': ChevronDown, + 'chevron-right': ChevronRight, + clock: Clock, + code: Code, + 'code-block': CodeBlock, + compass: Compass, + copy: Copy, + database: Database, + 'edit-3': Edit3, + edit: Edit, + 'external-link': ExternalLink, + 'eye-off': EyeOff, + eye: Eye, + file: File, + files: Files, + 'file-text': FileText, + film: Film, + filter: Filter, + flag: Flag, + folder: Folder, + github: Github, + gitlab: Gitlab, + globe: Globe, + grid: Grid, + 'hard-drive': HardDrive, + 'heading-one': HeadingOne, + 'heading-two': HeadingTwo, + 'heading-three': HeadingThree, + 'help-circle': HelpCircle, + image: Image, + inbox: Inbox, + info: Info, + italic: Italic, + layout: Layout, + link: Link, + lock: Lock, + 'log-out': LogOut, + mail: Mail, + 'map-pin': MapPin, + maximize: Maximize, + menu: Menu, + minimize: Minimize, + moon: Moon, + 'more-horizontal': MoreHorizontal, + 'more-vertical': MoreVertical, + move: Move, + music: Music, + netlify: Netlify, + 'numbered-list': NumberedList, + package: Package, + paperclip: Paperclip, + 'pie-chart': PieChart, + pin: Pin, + 'plus-circle': PlusCircle, + plus: Plus, + quote: Quote, + radio: Radio, + save: Save, + search: Search, + server: Server, + settings: Settings, + 'share-2': Share2, + 'shopping-cart': ShoppingCart, + sidebar: Sidebar, + star: Star, + sun: Sun, + 'trash-2': Trash2, + underline: Underline, + unsplash: Unsplash, + 'upload-cloud': UploadCloud, + upload: Upload, + user: User, + users: Users, + workflow: Workflow, + x: X, + zap: Zap, + }; + +const Icon = ({ name, ...props }) => { + const IconComponent = iconComponents[name] || iconComponents.x; + + return ; +}; + +const SizedIcon = ({ size, className, ...props }) => ( + +); + +export default SizedIcon; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/Activity.jsx b/packages/decap-cms-ui-4/src/Icon/icons/Activity.jsx new file mode 100644 index 000000000000..7a75717029f4 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/Activity.jsx @@ -0,0 +1,20 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/AlertCircle.jsx b/packages/decap-cms-ui-4/src/Icon/icons/AlertCircle.jsx new file mode 100644 index 000000000000..f80377d9de51 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/AlertCircle.jsx @@ -0,0 +1,22 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/AlertTriangle.jsx b/packages/decap-cms-ui-4/src/Icon/icons/AlertTriangle.jsx new file mode 100644 index 000000000000..bda367fd4ee2 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/AlertTriangle.jsx @@ -0,0 +1,22 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/ArrowDown.jsx b/packages/decap-cms-ui-4/src/Icon/icons/ArrowDown.jsx new file mode 100644 index 000000000000..822ff1d5a4c5 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/ArrowDown.jsx @@ -0,0 +1,21 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/ArrowLeft.jsx b/packages/decap-cms-ui-4/src/Icon/icons/ArrowLeft.jsx new file mode 100644 index 000000000000..7e9c313cda09 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/ArrowLeft.jsx @@ -0,0 +1,21 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/ArrowRight.jsx b/packages/decap-cms-ui-4/src/Icon/icons/ArrowRight.jsx new file mode 100644 index 000000000000..cde4337f1773 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/ArrowRight.jsx @@ -0,0 +1,21 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/ArrowUp.jsx b/packages/decap-cms-ui-4/src/Icon/icons/ArrowUp.jsx new file mode 100644 index 000000000000..060f6d178fdc --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/ArrowUp.jsx @@ -0,0 +1,21 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/AtSign.jsx b/packages/decap-cms-ui-4/src/Icon/icons/AtSign.jsx new file mode 100644 index 000000000000..756073ef4fbf --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/AtSign.jsx @@ -0,0 +1,21 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/BarChart.jsx b/packages/decap-cms-ui-4/src/Icon/icons/BarChart.jsx new file mode 100644 index 000000000000..a9a856df735e --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/BarChart.jsx @@ -0,0 +1,22 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/Bell.jsx b/packages/decap-cms-ui-4/src/Icon/icons/Bell.jsx new file mode 100644 index 000000000000..1a4c4312c503 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/Bell.jsx @@ -0,0 +1,21 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/Bitbucket.jsx b/packages/decap-cms-ui-4/src/Icon/icons/Bitbucket.jsx new file mode 100644 index 000000000000..f41f075d104a --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/Bitbucket.jsx @@ -0,0 +1,20 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + {' '} + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/Bold.jsx b/packages/decap-cms-ui-4/src/Icon/icons/Bold.jsx new file mode 100644 index 000000000000..b14dbc66602c --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/Bold.jsx @@ -0,0 +1,21 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/Box.jsx b/packages/decap-cms-ui-4/src/Icon/icons/Box.jsx new file mode 100644 index 000000000000..dbfb2063bd3d --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/Box.jsx @@ -0,0 +1,22 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/BulletedList.jsx b/packages/decap-cms-ui-4/src/Icon/icons/BulletedList.jsx new file mode 100644 index 000000000000..e7bfa5b2ef37 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/BulletedList.jsx @@ -0,0 +1,25 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/Calendar.jsx b/packages/decap-cms-ui-4/src/Icon/icons/Calendar.jsx new file mode 100644 index 000000000000..bc431baaf00c --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/Calendar.jsx @@ -0,0 +1,23 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/Check.jsx b/packages/decap-cms-ui-4/src/Icon/icons/Check.jsx new file mode 100644 index 000000000000..a44abe82646b --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/Check.jsx @@ -0,0 +1,20 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/ChevronDown.jsx b/packages/decap-cms-ui-4/src/Icon/icons/ChevronDown.jsx new file mode 100644 index 000000000000..353b110e8fd8 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/ChevronDown.jsx @@ -0,0 +1,20 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/ChevronRight.jsx b/packages/decap-cms-ui-4/src/Icon/icons/ChevronRight.jsx new file mode 100644 index 000000000000..d365348cdfe3 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/ChevronRight.jsx @@ -0,0 +1,20 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/Clock.jsx b/packages/decap-cms-ui-4/src/Icon/icons/Clock.jsx new file mode 100644 index 000000000000..d74204ac1860 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/Clock.jsx @@ -0,0 +1,21 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/Code.jsx b/packages/decap-cms-ui-4/src/Icon/icons/Code.jsx new file mode 100644 index 000000000000..037df98f0444 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/Code.jsx @@ -0,0 +1,21 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/CodeBlock.jsx b/packages/decap-cms-ui-4/src/Icon/icons/CodeBlock.jsx new file mode 100644 index 000000000000..2c3aa4519e94 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/CodeBlock.jsx @@ -0,0 +1,21 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/Compass.jsx b/packages/decap-cms-ui-4/src/Icon/icons/Compass.jsx new file mode 100644 index 000000000000..a020daf07e05 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/Compass.jsx @@ -0,0 +1,21 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/Copy.jsx b/packages/decap-cms-ui-4/src/Icon/icons/Copy.jsx new file mode 100644 index 000000000000..a87c6ee69240 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/Copy.jsx @@ -0,0 +1,21 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/Database.jsx b/packages/decap-cms-ui-4/src/Icon/icons/Database.jsx new file mode 100644 index 000000000000..95f7ad4cbecf --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/Database.jsx @@ -0,0 +1,22 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/Edit.jsx b/packages/decap-cms-ui-4/src/Icon/icons/Edit.jsx new file mode 100644 index 000000000000..e4cb39b464d3 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/Edit.jsx @@ -0,0 +1,21 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/Edit3.jsx b/packages/decap-cms-ui-4/src/Icon/icons/Edit3.jsx new file mode 100644 index 000000000000..6dfa63ec9926 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/Edit3.jsx @@ -0,0 +1,21 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/ExternalLink.jsx b/packages/decap-cms-ui-4/src/Icon/icons/ExternalLink.jsx new file mode 100644 index 000000000000..14ece1a88aa6 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/ExternalLink.jsx @@ -0,0 +1,22 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/Eye.jsx b/packages/decap-cms-ui-4/src/Icon/icons/Eye.jsx new file mode 100644 index 000000000000..59744826699d --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/Eye.jsx @@ -0,0 +1,21 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/EyeOff.jsx b/packages/decap-cms-ui-4/src/Icon/icons/EyeOff.jsx new file mode 100644 index 000000000000..3402c42321e3 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/EyeOff.jsx @@ -0,0 +1,21 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/File.jsx b/packages/decap-cms-ui-4/src/Icon/icons/File.jsx new file mode 100644 index 000000000000..2e6dfb869418 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/File.jsx @@ -0,0 +1,21 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/FileText.jsx b/packages/decap-cms-ui-4/src/Icon/icons/FileText.jsx new file mode 100644 index 000000000000..a8eb2da865e8 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/FileText.jsx @@ -0,0 +1,24 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/Files.jsx b/packages/decap-cms-ui-4/src/Icon/icons/Files.jsx new file mode 100644 index 000000000000..9eea864a8c28 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/Files.jsx @@ -0,0 +1,22 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/Film.jsx b/packages/decap-cms-ui-4/src/Icon/icons/Film.jsx new file mode 100644 index 000000000000..2d54770168b7 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/Film.jsx @@ -0,0 +1,27 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + + + + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/Filter.jsx b/packages/decap-cms-ui-4/src/Icon/icons/Filter.jsx new file mode 100644 index 000000000000..4118195ef44d --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/Filter.jsx @@ -0,0 +1,20 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/Flag.jsx b/packages/decap-cms-ui-4/src/Icon/icons/Flag.jsx new file mode 100644 index 000000000000..808a0742b6c3 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/Flag.jsx @@ -0,0 +1,21 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/Folder.jsx b/packages/decap-cms-ui-4/src/Icon/icons/Folder.jsx new file mode 100644 index 000000000000..3c7ca333843d --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/Folder.jsx @@ -0,0 +1,20 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/Github.jsx b/packages/decap-cms-ui-4/src/Icon/icons/Github.jsx new file mode 100644 index 000000000000..e24f971bfe0d --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/Github.jsx @@ -0,0 +1,20 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/Gitlab.jsx b/packages/decap-cms-ui-4/src/Icon/icons/Gitlab.jsx new file mode 100644 index 000000000000..f167300dab3c --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/Gitlab.jsx @@ -0,0 +1,20 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/Globe.jsx b/packages/decap-cms-ui-4/src/Icon/icons/Globe.jsx new file mode 100644 index 000000000000..b68a16d9f408 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/Globe.jsx @@ -0,0 +1,22 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/Grid.jsx b/packages/decap-cms-ui-4/src/Icon/icons/Grid.jsx new file mode 100644 index 000000000000..e15461a54896 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/Grid.jsx @@ -0,0 +1,23 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/HardDrive.jsx b/packages/decap-cms-ui-4/src/Icon/icons/HardDrive.jsx new file mode 100644 index 000000000000..5db4d9385d97 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/HardDrive.jsx @@ -0,0 +1,23 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/HeadingOne.jsx b/packages/decap-cms-ui-4/src/Icon/icons/HeadingOne.jsx new file mode 100644 index 000000000000..cf5b0fff9c96 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/HeadingOne.jsx @@ -0,0 +1,22 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/HeadingThree.jsx b/packages/decap-cms-ui-4/src/Icon/icons/HeadingThree.jsx new file mode 100644 index 000000000000..bec010f75ee3 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/HeadingThree.jsx @@ -0,0 +1,22 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/HeadingTwo.jsx b/packages/decap-cms-ui-4/src/Icon/icons/HeadingTwo.jsx new file mode 100644 index 000000000000..2cc7737ff69e --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/HeadingTwo.jsx @@ -0,0 +1,22 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/HelpCircle.jsx b/packages/decap-cms-ui-4/src/Icon/icons/HelpCircle.jsx new file mode 100644 index 000000000000..ff35f896f389 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/HelpCircle.jsx @@ -0,0 +1,22 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/Image.jsx b/packages/decap-cms-ui-4/src/Icon/icons/Image.jsx new file mode 100644 index 000000000000..b374910dee83 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/Image.jsx @@ -0,0 +1,22 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/Inbox.jsx b/packages/decap-cms-ui-4/src/Icon/icons/Inbox.jsx new file mode 100644 index 000000000000..68cdcf2c18a3 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/Inbox.jsx @@ -0,0 +1,21 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/Info.jsx b/packages/decap-cms-ui-4/src/Icon/icons/Info.jsx new file mode 100644 index 000000000000..418594ea60ed --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/Info.jsx @@ -0,0 +1,22 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/Italic.jsx b/packages/decap-cms-ui-4/src/Icon/icons/Italic.jsx new file mode 100644 index 000000000000..4007c8d45fd0 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/Italic.jsx @@ -0,0 +1,22 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/Layout.jsx b/packages/decap-cms-ui-4/src/Icon/icons/Layout.jsx new file mode 100644 index 000000000000..b0d503133892 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/Layout.jsx @@ -0,0 +1,22 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/Link.jsx b/packages/decap-cms-ui-4/src/Icon/icons/Link.jsx new file mode 100644 index 000000000000..533f50814b8d --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/Link.jsx @@ -0,0 +1,21 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/Lock.jsx b/packages/decap-cms-ui-4/src/Icon/icons/Lock.jsx new file mode 100644 index 000000000000..312190d9a7d2 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/Lock.jsx @@ -0,0 +1,21 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/LogOut.jsx b/packages/decap-cms-ui-4/src/Icon/icons/LogOut.jsx new file mode 100644 index 000000000000..485a3c5e31a2 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/LogOut.jsx @@ -0,0 +1,22 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/Mail.jsx b/packages/decap-cms-ui-4/src/Icon/icons/Mail.jsx new file mode 100644 index 000000000000..a8531681b965 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/Mail.jsx @@ -0,0 +1,21 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/MapPin.jsx b/packages/decap-cms-ui-4/src/Icon/icons/MapPin.jsx new file mode 100644 index 000000000000..f0d05b91c14c --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/MapPin.jsx @@ -0,0 +1,21 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/Maximize.jsx b/packages/decap-cms-ui-4/src/Icon/icons/Maximize.jsx new file mode 100644 index 000000000000..6008f0a9fb5d --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/Maximize.jsx @@ -0,0 +1,20 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/Menu.jsx b/packages/decap-cms-ui-4/src/Icon/icons/Menu.jsx new file mode 100644 index 000000000000..8a00a37b62ed --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/Menu.jsx @@ -0,0 +1,22 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/Minimize.jsx b/packages/decap-cms-ui-4/src/Icon/icons/Minimize.jsx new file mode 100644 index 000000000000..c487bee8a822 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/Minimize.jsx @@ -0,0 +1,20 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/Moon.jsx b/packages/decap-cms-ui-4/src/Icon/icons/Moon.jsx new file mode 100644 index 000000000000..87745fc5da94 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/Moon.jsx @@ -0,0 +1,20 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/MoreHorizontal.jsx b/packages/decap-cms-ui-4/src/Icon/icons/MoreHorizontal.jsx new file mode 100644 index 000000000000..d872dc9362f3 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/MoreHorizontal.jsx @@ -0,0 +1,22 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/MoreVertical.jsx b/packages/decap-cms-ui-4/src/Icon/icons/MoreVertical.jsx new file mode 100644 index 000000000000..d58d4f285718 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/MoreVertical.jsx @@ -0,0 +1,22 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/Move.jsx b/packages/decap-cms-ui-4/src/Icon/icons/Move.jsx new file mode 100644 index 000000000000..bececc631af8 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/Move.jsx @@ -0,0 +1,25 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/Music.jsx b/packages/decap-cms-ui-4/src/Icon/icons/Music.jsx new file mode 100644 index 000000000000..d05b488c0f51 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/Music.jsx @@ -0,0 +1,22 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/Netlify.jsx b/packages/decap-cms-ui-4/src/Icon/icons/Netlify.jsx new file mode 100644 index 000000000000..0697b9f5235e --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/Netlify.jsx @@ -0,0 +1,34 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + + + + + + + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/NumberedList.jsx b/packages/decap-cms-ui-4/src/Icon/icons/NumberedList.jsx new file mode 100644 index 000000000000..0aa8084daa0a --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/NumberedList.jsx @@ -0,0 +1,24 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/Package.jsx b/packages/decap-cms-ui-4/src/Icon/icons/Package.jsx new file mode 100644 index 000000000000..51b4e1005508 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/Package.jsx @@ -0,0 +1,23 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/Paperclip.jsx b/packages/decap-cms-ui-4/src/Icon/icons/Paperclip.jsx new file mode 100644 index 000000000000..47ff85f14e05 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/Paperclip.jsx @@ -0,0 +1,20 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/PieChart.jsx b/packages/decap-cms-ui-4/src/Icon/icons/PieChart.jsx new file mode 100644 index 000000000000..71bddd7429dc --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/PieChart.jsx @@ -0,0 +1,21 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/Pin.jsx b/packages/decap-cms-ui-4/src/Icon/icons/Pin.jsx new file mode 100644 index 000000000000..14188ad246c9 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/Pin.jsx @@ -0,0 +1,21 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/Plus.jsx b/packages/decap-cms-ui-4/src/Icon/icons/Plus.jsx new file mode 100644 index 000000000000..35dce05c0f74 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/Plus.jsx @@ -0,0 +1,21 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/PlusCircle.jsx b/packages/decap-cms-ui-4/src/Icon/icons/PlusCircle.jsx new file mode 100644 index 000000000000..4a52ecc239d2 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/PlusCircle.jsx @@ -0,0 +1,22 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/Quote.jsx b/packages/decap-cms-ui-4/src/Icon/icons/Quote.jsx new file mode 100644 index 000000000000..82cb7c12fbcf --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/Quote.jsx @@ -0,0 +1,25 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/Radio.jsx b/packages/decap-cms-ui-4/src/Icon/icons/Radio.jsx new file mode 100644 index 000000000000..4f2b3227af7d --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/Radio.jsx @@ -0,0 +1,21 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/Save.jsx b/packages/decap-cms-ui-4/src/Icon/icons/Save.jsx new file mode 100644 index 000000000000..03140ca4d83a --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/Save.jsx @@ -0,0 +1,22 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/Search.jsx b/packages/decap-cms-ui-4/src/Icon/icons/Search.jsx new file mode 100644 index 000000000000..e882e1b0d733 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/Search.jsx @@ -0,0 +1,21 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/Server.jsx b/packages/decap-cms-ui-4/src/Icon/icons/Server.jsx new file mode 100644 index 000000000000..7edf2f922dfe --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/Server.jsx @@ -0,0 +1,23 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/Settings.jsx b/packages/decap-cms-ui-4/src/Icon/icons/Settings.jsx new file mode 100644 index 000000000000..f0bf22c1e756 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/Settings.jsx @@ -0,0 +1,21 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/Share2.jsx b/packages/decap-cms-ui-4/src/Icon/icons/Share2.jsx new file mode 100644 index 000000000000..9c6e7e400c67 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/Share2.jsx @@ -0,0 +1,24 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/ShoppingCart.jsx b/packages/decap-cms-ui-4/src/Icon/icons/ShoppingCart.jsx new file mode 100644 index 000000000000..64fc377fd040 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/ShoppingCart.jsx @@ -0,0 +1,22 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/Sidebar.jsx b/packages/decap-cms-ui-4/src/Icon/icons/Sidebar.jsx new file mode 100644 index 000000000000..e29181d930e8 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/Sidebar.jsx @@ -0,0 +1,21 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/Star.jsx b/packages/decap-cms-ui-4/src/Icon/icons/Star.jsx new file mode 100644 index 000000000000..4768dd95b4eb --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/Star.jsx @@ -0,0 +1,20 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/Sun.jsx b/packages/decap-cms-ui-4/src/Icon/icons/Sun.jsx new file mode 100644 index 000000000000..6b42ea8fef56 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/Sun.jsx @@ -0,0 +1,28 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + + + + + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/Trash2.jsx b/packages/decap-cms-ui-4/src/Icon/icons/Trash2.jsx new file mode 100644 index 000000000000..b6f8d709a806 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/Trash2.jsx @@ -0,0 +1,23 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/Underline.jsx b/packages/decap-cms-ui-4/src/Icon/icons/Underline.jsx new file mode 100644 index 000000000000..67e51c1b98b3 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/Underline.jsx @@ -0,0 +1,21 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/Unsplash.jsx b/packages/decap-cms-ui-4/src/Icon/icons/Unsplash.jsx new file mode 100644 index 000000000000..acb3277bfad5 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/Unsplash.jsx @@ -0,0 +1,20 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/Upload.jsx b/packages/decap-cms-ui-4/src/Icon/icons/Upload.jsx new file mode 100644 index 000000000000..90f3652d9ff0 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/Upload.jsx @@ -0,0 +1,22 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/UploadCloud.jsx b/packages/decap-cms-ui-4/src/Icon/icons/UploadCloud.jsx new file mode 100644 index 000000000000..f5553dc361bf --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/UploadCloud.jsx @@ -0,0 +1,23 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/User.jsx b/packages/decap-cms-ui-4/src/Icon/icons/User.jsx new file mode 100644 index 000000000000..3650f4e8b568 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/User.jsx @@ -0,0 +1,21 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/Users.jsx b/packages/decap-cms-ui-4/src/Icon/icons/Users.jsx new file mode 100644 index 000000000000..801343fbc604 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/Users.jsx @@ -0,0 +1,23 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/Workflow.jsx b/packages/decap-cms-ui-4/src/Icon/icons/Workflow.jsx new file mode 100644 index 000000000000..5e26d9ec5426 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/Workflow.jsx @@ -0,0 +1,22 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/X.jsx b/packages/decap-cms-ui-4/src/Icon/icons/X.jsx new file mode 100644 index 000000000000..788a724d9087 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/X.jsx @@ -0,0 +1,21 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/icons/Zap.jsx b/packages/decap-cms-ui-4/src/Icon/icons/Zap.jsx new file mode 100644 index 000000000000..f6a694d6863f --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/icons/Zap.jsx @@ -0,0 +1,20 @@ +import React from 'react'; + +const SVG = ({ className = '', size = '24' }) => ( + + + +); + +export default SVG; diff --git a/packages/decap-cms-ui-4/src/Icon/index.js b/packages/decap-cms-ui-4/src/Icon/index.js new file mode 100644 index 000000000000..dc6b45c350fa --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/index.js @@ -0,0 +1 @@ +export { default } from './Icon'; diff --git a/packages/decap-cms-ui-4/src/Icon/story.jsx b/packages/decap-cms-ui-4/src/Icon/story.jsx new file mode 100644 index 000000000000..43343ef2bc96 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Icon/story.jsx @@ -0,0 +1,30 @@ +import React from 'react'; +import styled from '@emotion/styled'; +import { withKnobs, select } from '@storybook/addon-knobs'; + +import Icon, { iconComponents } from './Icon'; + +const StyledIcon = styled(Icon)` + color: ${({ theme }) => theme.color.highEmphasis}; +`; + +export default { + title: 'Components/Icon', + decorators: [withKnobs], +}; + +export const _Icon = () => { + return ( + ({ ...acc, [key]: key }), {}), + }, + null, + )} + /> + ); +}; diff --git a/packages/decap-cms-ui-4/src/Label/Label.jsx b/packages/decap-cms-ui-4/src/Label/Label.jsx new file mode 100644 index 000000000000..65c59f09600a --- /dev/null +++ b/packages/decap-cms-ui-4/src/Label/Label.jsx @@ -0,0 +1,15 @@ +import styled from '@emotion/styled'; + +const Label = styled.label` + color: ${({ theme, focus }) => (focus ? theme.color.primary['800'] : theme.color.lowEmphasis)}; + font-size: 12px; + font-weight: bold; + letter-spacing: -0.5px; + display: block; + line-height: 1rem; + transition: 0.2s; + margin-top: -0.25rem; + margin-bottom: 0.25rem; +`; + +export default Label; diff --git a/packages/decap-cms-ui-4/src/Label/index.js b/packages/decap-cms-ui-4/src/Label/index.js new file mode 100644 index 000000000000..a1e5f51f5586 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Label/index.js @@ -0,0 +1 @@ +export { default } from './Label'; diff --git a/packages/decap-cms-ui-4/src/LazyLoadModule/LazyLoadModule.jsx b/packages/decap-cms-ui-4/src/LazyLoadModule/LazyLoadModule.jsx new file mode 100644 index 000000000000..f56816fc1229 --- /dev/null +++ b/packages/decap-cms-ui-4/src/LazyLoadModule/LazyLoadModule.jsx @@ -0,0 +1,25 @@ +// LazyLoadModule.jsx +import * as React from 'react'; +export default class LazyLoadModule extends React.Component { + constructor(props) { + super(props); + + this.state = { + module: null, + }; + } + + // after the initial render, wait for module to load + async componentDidMount() { + const { resolve } = this.props; + const { default: module } = await resolve(); + this.setState({ module }); + } + + render() { + const { module } = this.state; + + if (!module) return
Loading module...
; + if (module.view) return React.createElement(module.view); + } +} diff --git a/packages/decap-cms-ui-4/src/LazyLoadModule/index.js b/packages/decap-cms-ui-4/src/LazyLoadModule/index.js new file mode 100644 index 000000000000..96ba1862b210 --- /dev/null +++ b/packages/decap-cms-ui-4/src/LazyLoadModule/index.js @@ -0,0 +1 @@ +export { default } from './LazyLoadModule'; diff --git a/packages/decap-cms-ui-4/src/LinearProgress/LinearProgress.jsx b/packages/decap-cms-ui-4/src/LinearProgress/LinearProgress.jsx new file mode 100644 index 000000000000..aa5fec0f6291 --- /dev/null +++ b/packages/decap-cms-ui-4/src/LinearProgress/LinearProgress.jsx @@ -0,0 +1,100 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { css, keyframes } from '@emotion/core'; +import styled from '@emotion/styled'; + +import { TYPE } from '../utils/constants'; +import { falseOrDelay } from '../utils/propValidator'; + +const LinearProgressWrap = styled.div` + width: 100%; + height: 3px; + background-color: ${({ theme }) => theme.color.disabled}; + transform-origin: left; +`; + +const countdownAnimation = keyframes` + 0% { + transform: scaleX(1); + } + 100% { + transform: scaleX(0); + } +`; + +const ProgressIndicator = styled.div` + background-color: ${({ type, theme }) => { + if (type === TYPE.SUCCESS) return theme.color.success[900]; + if (type === TYPE.WARNING) return '#FFB81C'; + if (type === TYPE.ERROR) return theme.color.danger[900]; + return theme.color.mediumEmphasis; + }}; + height: 100%; + width: ${props => { + if (props.value) return props.value; + if (props.countdown) return 100; + return 0; + }}%; + ${props => + props.countdown && + css` + transform-origin: left; + animation: ${countdownAnimation} ${props.countdown}ms linear forwards; + animation-play-state: ${props => (props.paused ? 'paused' : 'running')}; + `} +`; + +const LinearProgress = ({ value, type, countdown, pauseCountdown, onCountdown }) => ( + + + +); + +LinearProgress.propTypes = { + /** + * The animation delay which determine when to close the toast + */ + countdown: falseOrDelay, + + /** + * Whether or not the animation is running or paused + */ + pauseCountdown: PropTypes.bool, + + /** + * Func to when countdown animation completes + */ + onCountdown: PropTypes.func.isRequired, + + /** + * Support rtl content + */ + rtl: PropTypes.bool, + + /** + * Optional type : info, success ... + */ + type: PropTypes.string, + + /** + * Optional className + */ + className: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), + + /** + * Progress value (0-100) + */ + value: PropTypes.number, +}; + +LinearProgress.defaultProps = { + type: TYPE.DEFAULT, +}; + +export default LinearProgress; diff --git a/packages/decap-cms-ui-4/src/LinearProgress/index.js b/packages/decap-cms-ui-4/src/LinearProgress/index.js new file mode 100644 index 000000000000..c8bdc40addf1 --- /dev/null +++ b/packages/decap-cms-ui-4/src/LinearProgress/index.js @@ -0,0 +1 @@ +export { default } from './LinearProgress'; diff --git a/packages/decap-cms-ui-4/src/LoginButton.js b/packages/decap-cms-ui-4/src/LoginButton.js new file mode 100644 index 000000000000..07c2b31b3de4 --- /dev/null +++ b/packages/decap-cms-ui-4/src/LoginButton.js @@ -0,0 +1,36 @@ +// import Icon from './Icon'; +// import { buttons, shadows } from './styles'; +import styled from '@emotion/styled'; + +import { Button } from './Button'; + +const LoginButton = styled(Button)``; + +// const LoginButton = styled.button` +// ${buttons.button}; +// ${shadows.dropDeep}; +// ${buttons.default}; +// +// display: flex; +// border-radius: 5px; +// width: 100%; +// align-items: center; +// justify-content: center; +// position: relative; +// margin-bottom: 1rem; +// color: white; +// background-color: ${props => props.color || '#798291'}; +// +// &:last-of-type { +// margin-bottom: 0; +// } +// +// ${Icon} { +// position: absolute; +// left: 1rem; +// top: 50%; +// transform: translateY(-50%); +// } +// `; + +export default LoginButton; diff --git a/packages/decap-cms-ui-4/src/Logo/Logo.jsx b/packages/decap-cms-ui-4/src/Logo/Logo.jsx new file mode 100644 index 000000000000..491adb60069f --- /dev/null +++ b/packages/decap-cms-ui-4/src/Logo/Logo.jsx @@ -0,0 +1,32 @@ +import React from 'react'; +import styled from '@emotion/styled'; + +const StyledPath = styled.path` + ${({ theme }) => (theme.darkMode ? `fill: white;` : ``)} +`; + +const Logo = ({ className = '', size = '32px' }) => ( + + + + + + + + + +); + +export default Logo; diff --git a/packages/decap-cms-ui-4/src/Logo/index.js b/packages/decap-cms-ui-4/src/Logo/index.js new file mode 100644 index 000000000000..a5be7785e1e5 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Logo/index.js @@ -0,0 +1 @@ +export { default } from './Logo'; diff --git a/packages/decap-cms-ui-4/src/LogoTile/LogoTile.jsx b/packages/decap-cms-ui-4/src/LogoTile/LogoTile.jsx new file mode 100644 index 000000000000..e085f0ac0c08 --- /dev/null +++ b/packages/decap-cms-ui-4/src/LogoTile/LogoTile.jsx @@ -0,0 +1,25 @@ +import React from 'react'; +import styled from '@emotion/styled'; +import Logo from '../Logo'; + +const LogoWrap = styled.div` + background: ${({ theme }) => + theme.darkMode + ? 'linear-gradient(135deg, #35ADB1 0%, #4C9ABE 100%)' + : theme.color.neutral['1400']}; + width: 3.5rem; + min-width: 3.5rem; + height: 3.5rem; + display: flex; + justify-content: center; + align-items: center; + margin-right: 1rem; +`; + +const LogoTile = () => ( + + + +); + +export default LogoTile; diff --git a/packages/decap-cms-ui-4/src/LogoTile/index.js b/packages/decap-cms-ui-4/src/LogoTile/index.js new file mode 100644 index 000000000000..8be8da908356 --- /dev/null +++ b/packages/decap-cms-ui-4/src/LogoTile/index.js @@ -0,0 +1 @@ +export { default } from './LogoTile'; diff --git a/packages/decap-cms-ui-4/src/MediaDialog/MediaDialog.jsx b/packages/decap-cms-ui-4/src/MediaDialog/MediaDialog.jsx new file mode 100644 index 000000000000..31b922a3d417 --- /dev/null +++ b/packages/decap-cms-ui-4/src/MediaDialog/MediaDialog.jsx @@ -0,0 +1,416 @@ +import React, { useState, useMemo } from 'react'; +import styled from '@emotion/styled'; + +import { Button, ButtonGroup, IconButton } from '../Button'; +import { Menu, MenuItem } from '../Menu'; +import Dialog from '../Dialog'; +import Icon from '../Icon'; +import Thumbnail, { ThumbnailGrid } from '../Thumbnail'; +import Table from '../Table'; +import SearchBar from '../SearchBar'; +import { NavMenuGroup, NavMenuGroupLabel, NavMenuItem, NavMenu } from '../NavMenu'; + +import getMockData from '../utils/getMockData'; +import { useUIContext } from '../hooks'; + +const MediaDialogWrap = styled(Dialog)` + color: ${({ theme }) => theme.color.text}; + height: 80%; + max-height: 100%; + max-width: 100%; + display: flex; + overflow: hidden; + width: 75vw; + height: 75vh; + ${({ theme }) => theme.responsive.mediaQueryDown('xs')} { + height: 100%; + } +`; +const DialogSidebar = styled.div` + position: relative; + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: center; + ${({ theme }) => theme.responsive.mediaQueryDown('xs')} { + display: none; + } +`; +const DialogBody = styled.div` + background-color: ${({ theme }) => theme.color.surface}; + position: relative; + flex: 1; + display: flex; + flex-direction: column; +`; +const DialogHeader = styled.div` + display: flex; + flex-direction: column; +`; +const DialogTitlebar = styled.div` + position: relative; + display: flex; + justify-content: center; + align-items: center; + min-height: 4rem; + padding: 1rem; +`; +const DialogToolbar = styled.div` + display: flex; + align-items: center; + padding: 0 1rem 1rem 1rem; +`; +const DialogFooter = styled.div` + background-color: ${({ theme }) => theme.color.surface}; + display: flex; + flex-direction: column; + position: absolute; + bottom: 0; + z-index: 10; + width: 100%; +`; +const DialogFooterSelection = styled.div` + display: flex; + align-items: center; + padding-top: 1rem; + overflow-y: hidden; + overflow-x: auto; +`; +const DialogFooterActions = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + padding: 1rem; +`; + +// TODO: Fix the height +const GalleryWrap = styled.div` + overflow-y: auto; + flex: 1; + width: 100%; + display: flex; + align-items: flex-start; + justify-content: center; +`; +const CloseBtn = styled(IconButton)` + position: absolute; + top: 1rem; + right: 1rem; +`; +const TitleWrap = styled.div` + font-weight: bold; +`; +const SidebarTitle = styled.div` + font-weight: bold; + font-size: 1.5rem; + white-space: nowrap; + margin-bottom: 0.75rem; + margin-left: 1.125rem; + margin-top: 0.5rem; + line-height: 1; +`; +const MediaSearchBar = styled(SearchBar)` + flex: 1; + width: auto; + margin-right: 1rem; +`; +const StyledTable = styled(Table)` + width: 100%; + padding-bottom: 4rem; +`; +const Title = styled.div` + color: ${({ theme }) => theme.color.highEmphasis}; + font-weight: bold; +`; +const Subtitle = styled.div` + font-size: 12px; +`; +const FeaturedImage = styled.div` + background-image: url(${({ srcUrl }) => srcUrl}); + background-size: cover; + background-position: center center; + background-repeat: no-repeat; + width: ${({ size }) => + size === 'xs' ? 1.5 : size === 'sm' ? 2 : size === 'lg' ? 3 : size === 'xl' ? 3.5 : 2.5}rem; + height: ${({ size }) => + size === 'xs' ? 1.5 : size === 'sm' ? 2 : size === 'lg' ? 3 : size === 'xl' ? 3.5 : 2.5}rem; + border-radius: 6px; +`; +const StyledThumbnailGrid = styled(ThumbnailGrid)` + padding: 0 1rem ${({ hasSelection }) => (hasSelection ? 8 : 4)}rem 1rem; + grid-template-columns: repeat( + auto-fill, + minmax(min(${({ gridView }) => (gridView ? 12 : 24)}rem, 100%), 1fr) + ); +`; +const UploadButton = styled(Button)` + margin-bottom: 0.25rem; + margin-left: 1rem; + margin-right: 1rem; +`; +const SelectedCount = styled.div` + flex: 1; + white-space: nowrap; + margin-right: 1rem; + padding-left: 1rem; +`; +const SelectedImages = styled.div` + display: flex; + padding-right: 1rem; + ${FeaturedImage} { + margin-left: 0.5rem; + } +`; + +const Gallery = ({ data, gridView, activeSource, selectedItems, setSelectedItems }) => { + const columns = React.useMemo( + () => [ + { + id: 'featuredImage', + Cell({ row: { original: rowData } }) { + return ; + }, + width: `${3.5 * 16}px`, + }, + { + Header: 'Title', + accessor: 'title', + Cell({ row: { original: rowData } }) { + return ( + <> + {rowData.title} + {rowData.description} + + ); + }, + width: 'auto', + }, + { + Header: 'Date Modified', + accessor: 'dateModified', + width: '15%', + }, + { + Header: 'Date Created', + accessor: 'dateCreated', + width: '15%', + }, + { + Header: 'Author', + accessor: 'author', + width: '10%', + }, + ], + [], + ); + + return gridView ? ( + + {data.map((thumb, i) => ( + { + console.log({ selectedItems }); + if (selectedItems.includes(thumb.id)) { + setSelectedItems(selectedItems.filter(item => item !== thumb.id)); + } else { + setSelectedItems([...selectedItems, thumb.id]); + } + }} + previewAspectRatio={'4:3'} + width={'auto'} + gridView={gridView} + titleMaxLines={1} + subtitleMaxLines={1} + /> + ))} + + ) : ( + setSelectedItems(selected)} + selected={selectedItems} + onClick={rowData => console.log(`You just clicked table row ${rowData.id}.`)} + /> + ); +}; + +const sources = [ + { + id: 'media-library', + name: 'Media Library', + icon: 'hard-drive', + }, + { + id: 'unsplash', + name: 'Unsplash', + icon: 'unsplash', + }, + { + id: 'raw-pixel', + name: 'Raw Pixel', + icon: 'image', + }, +]; + +const MediaDialog = ({ open, onClose }) => { + const [gridView, setGridView] = useState(true); + const [selectedItems, setSelectedItems] = useState([]); + const placeholder = 'Search media library'; + const [activeSource, setActiveSource] = useState(sources[0]); + const [searchMenuAnchorEl, setSearchMenuAnchorEl] = useState(null); + const searchMenuOptions = [activeSource, { id: 'all', name: 'All Media Sources' }]; + const [selectedSearchMenuOption, setSelectedSearchMenuOption] = useState(searchMenuOptions[0]); + const data = useMemo(() => getMockData('post', 24), [activeSource]); + + return ( + + + + Choose Media + + Locations + {sources.map(source => ( + { + setActiveSource(source); + if ( + selectedSearchMenuOption.id !== 'all' && + selectedSearchMenuOption.id !== source.id + ) { + setSelectedSearchMenuOption(source); + } + }} + icon={source.icon} + key={source.name} + > + {source.name} + + ))} + + + Media Type + null} icon="image"> + Images + + null} icon="music"> + Music + + null} icon="film"> + Videos + + null} icon="file-text"> + Documents + + + + + Upload New + + + + + + + + {activeSource.name} + + + + console.log(e.target.value)} + renderEnd={() => + sources.length > 1 ? ( + <> + + setSearchMenuAnchorEl(null)} + anchorOrigin={{ y: 'bottom', x: 'right' }} + > + {searchMenuOptions.map(option => ( + { + setSelectedSearchMenuOption(option); + setSearchMenuAnchorEl(null); + }} + > + {option.name} + + ))} + + + ) : null + } + /> + + setGridView(false)} + /> + setGridView(true)} /> + + + + + + + + {selectedItems?.length > 0 && ( + + + {selectedItems?.length || 0} item{selectedItems?.length > 1 ? 's' : ''} selected + + + {selectedItems + ?.map(item => data?.find(row => row.id === item)) + ?.map(item => ( + + ))} + + + )} + + + + + + + + + + + + + ); +}; + +export default MediaDialog; diff --git a/packages/decap-cms-ui-4/src/MediaDialog/index.js b/packages/decap-cms-ui-4/src/MediaDialog/index.js new file mode 100644 index 000000000000..74bb278f72c8 --- /dev/null +++ b/packages/decap-cms-ui-4/src/MediaDialog/index.js @@ -0,0 +1 @@ +export { default } from './MediaDialog'; \ No newline at end of file diff --git a/packages/decap-cms-ui-4/src/MediaDialog/story.jsx b/packages/decap-cms-ui-4/src/MediaDialog/story.jsx new file mode 100644 index 000000000000..0ddf9631ed33 --- /dev/null +++ b/packages/decap-cms-ui-4/src/MediaDialog/story.jsx @@ -0,0 +1,27 @@ +import React, { useState } from 'react'; +import { withKnobs, boolean, select, text } from '@storybook/addon-knobs'; + +import MediaDialog from './MediaDialog'; +import { Button, ButtonGroup } from '../Button'; + +export default { + title: 'Dialogs/MediaDialog', + decorators: [withKnobs], +}; + +const StoryDialog = () => { + const [dialogOpen, setDialogOpen] = useState(false); + + return ( + <> + + + + setDialogOpen(false)} /> + + ); +}; + +export const _MediaDialog = () => { + return ; +}; diff --git a/packages/decap-cms-ui-4/src/Menu/Menu.jsx b/packages/decap-cms-ui-4/src/Menu/Menu.jsx new file mode 100644 index 000000000000..54cf81ded3a1 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Menu/Menu.jsx @@ -0,0 +1,65 @@ +import React from 'react'; +import styled from '@emotion/styled'; +import Card from '../Card'; +import Popover from '../Popover'; +import { isWindowDown } from '../utils/responsive'; +import { MenuItemWrap } from './MenuItem'; + +const MenuWrap = styled(Card)` + padding: ${({ isMobile }) => (isMobile ? '0.5rem' : '0.25rem')}; + min-width: ${({ width }) => (width ? width : '200px')}; + & > ${MenuItemWrap} { + ${({ isMobile }) => (isMobile ? `padding: 0.75rem;` : ``)} + } +`; +MenuWrap.defaultProps = { elevation: 'md' }; + +const Menu = React.forwardRef(function Menu(props, ref) { + const { + children, + className, + onClose, + open, + transitionDuration = 250, + anchorOrigin, + transformOrigin, + width, + ...other + } = props; + const firstValidItemRef = React.useRef(null); + const firstSelectedItemRef = React.useRef(null); + const getContentAnchorEl = () => firstSelectedItemRef.current || firstValidItemRef.current; + const isMobile = isWindowDown('xs'); + + return ( + + + {children} + + + ); +}); + +export default Menu; diff --git a/packages/decap-cms-ui-4/src/Menu/MenuItem.jsx b/packages/decap-cms-ui-4/src/Menu/MenuItem.jsx new file mode 100644 index 000000000000..b914223f5907 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Menu/MenuItem.jsx @@ -0,0 +1,80 @@ +import React from 'react'; +import styled from '@emotion/styled'; +import color from 'color'; +import Icon from '../Icon'; + +export const MenuItemWrap = styled.div` + font-family: ${({ theme }) => theme.fontFamily}; + padding: 0.5rem; + font-size: 0.875rem; + font-weight: 500; + line-height: 1.25rem; + transition: 200ms; + display: flex; + align-items: center; + border-radius: 4px; + color: ${({ theme, type }) => + type === 'danger' + ? theme.color.danger[theme.darkMode ? '300' : '1400'] + : type === 'success' + ? theme.color.success[theme.darkMode ? '300' : '1400'] + : theme.color.highEmphasis}; + cursor: pointer; + &:hover { + background-color: ${({ theme }) => theme.color.elevatedSurfaceHighlight}; + ${({ theme, type }) => + type === 'danger' + ? ` + background-color: ${color(theme.color.danger['900']) + .alpha(0.2) + .string()}; + color: ${theme.color.danger[theme.darkMode ? '300' : '1400']}; + ` + : ``} + ${({ theme, type }) => + type === 'success' + ? ` + background-color: ${color(theme.color.success['900']) + .alpha(0.2) + .string()}; + color: ${theme.color.success[theme.darkMode ? '300' : '1400']}; + ` + : ``} + } + ${({ disabled }) => + disabled + ? ` + pointer-events: none; + opacity: 0.5; + ` + : ``} +`; + +const TextWrap = styled.div` + flex: 1; +`; + +const StyledIcon = styled(Icon)` + margin-right: 0.75rem; + vertical-align: middle; +`; +const SelectedIcon = styled(Icon)` + margin-left: 0.75rem; + vertical-align: middle; +`; + +const MenuItem = ({ children, icon, onClick, selected, type, className, disabled, ...props }) => ( + + {icon && } + {children} + {selected && } + +); + +export default MenuItem; diff --git a/packages/decap-cms-ui-4/src/Menu/index.js b/packages/decap-cms-ui-4/src/Menu/index.js new file mode 100644 index 000000000000..0c15fc3e1caa --- /dev/null +++ b/packages/decap-cms-ui-4/src/Menu/index.js @@ -0,0 +1,2 @@ +export { default as Menu } from './Menu'; +export { default as MenuItem } from './MenuItem'; diff --git a/packages/decap-cms-ui-4/src/Menu/story.jsx b/packages/decap-cms-ui-4/src/Menu/story.jsx new file mode 100644 index 000000000000..cad0d9ecf113 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Menu/story.jsx @@ -0,0 +1,103 @@ +import React, { useState } from 'react'; +import { withKnobs, boolean, select } from '@storybook/addon-knobs'; + +import { Button, ButtonGroup } from '../Button'; +import { Menu, MenuItem } from '.'; +import { iconComponents } from '../Icon/Icon'; + +export default { + title: 'Components/Menu', + decorators: [withKnobs], +}; + +const StoryMenu = () => { + const [menuAnchorEl, setMenuAnchorEl] = useState(); + return ( +
+ + + + setMenuAnchorEl(null)} + anchorOrigin={{ + x: select( + 'anchorOrigin.x', + { + left: 'left', + center: 'center', + 'right (default)': 'right', + }, + 'right', + ), + y: select( + 'anchorOrigin.y', + { + top: 'top', + center: 'center', + 'bottom (default)': 'bottom', + }, + 'bottom', + ), + }} + transformOrigin={{ + x: select( + 'transformOrigin.x', + { + left: 'left', + center: 'center', + 'right (default)': 'right', + }, + 'right', + ), + y: select( + 'transformOrigin.y', + { + 'top (default)': 'top', + center: 'center', + bottom: 'bottom', + }, + 'top', + ), + }} + > + setMenuAnchorEl(null)}>Menu Item 1 + setMenuAnchorEl(null)}>Menu Item 2 + setMenuAnchorEl(null)}>Menu Item 3 + +
+ ); +}; + +export const _Menu = () => ; + +export const _MenuItem = () => { + return ( +
+ + ({ ...acc, [key]: key }), {}), + }, + null, + )} + type={select('type', { default: null, success: 'success', danger: 'danger' }, null)} + selected={boolean('selected', false)} + disabled={boolean('disabled', false)} + > + Menu Item + + +
+ ); +}; diff --git a/packages/decap-cms-ui-4/src/Modal/Modal.jsx b/packages/decap-cms-ui-4/src/Modal/Modal.jsx new file mode 100644 index 000000000000..679af3013208 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Modal/Modal.jsx @@ -0,0 +1,282 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import PropTypes from 'prop-types'; +import styled from '@emotion/styled'; +import warning from 'warning'; +import keycode from 'keycode'; +import ownerDocument from '../utils/ownerDocument'; +import RootRef from '../RootRef'; +import Portal from '../Portal'; +import { createChainedFunction } from '../utils/helpers'; +import ModalManager from './ModalManager'; +import Backdrop from '../Backdrop'; +import { ariaHidden } from './manageAriaHidden'; + +function getContainer(container, defaultContainer) { + container = typeof container === 'function' ? container() : container; + // eslint-disable-next-line react/no-find-dom-node + return ReactDOM.findDOMNode(container) || defaultContainer; +} + +function getHasTransition(props) { + return props.children ? Object.prototype.hasOwnProperty.call(props.children.props, 'in') : false; +} + +const ModalWrap = styled.div` + position: fixed; + z-index: ${props => (props.zIndex ? props.zIndex : 1200)}; + right: 0; + bottom: 0; + top: 0; + left: 0; + display: flex; + align-items: ${props => { + if (props.position.y === 'center') return 'center'; + if (props.position.y === 'top') return 'flex-start'; + if (props.position.y === 'bottom') return 'flex-end'; + if (props.position.y === 'stretch') return 'stretch'; + }}; + justify-content: ${props => { + if (props.position.x === 'center') return 'center'; + if (props.position.x === 'left') return 'flex-start'; + if (props.position.x === 'right') return 'flex-end'; + if (props.position.x === 'stretch') return 'stretch'; + }}; +`; + +class Modal extends React.Component { + mounted = false; + + constructor(props) { + super(); + this.state = { + exited: !props.open, + }; + } + + componentDidMount() { + this.mounted = true; + + if (this.props.open) this.handleOpen(); + } + + componentDidUpdate(prevProps) { + if (prevProps.open && !this.props.open) { + this.handleClose(); + } else if (!prevProps.open && this.props.open) { + this.lastFocus = ownerDocument(this.mountNode).activeElement; + this.handleOpen(); + } + } + + componentWillUnmount() { + this.mounted = false; + + if (this.props.open || (getHasTransition(this.props) && !this.state.exited)) { + this.handleClose(); + } + } + + static getDerivedStateFromProps(nextProps) { + if (nextProps.open) return { exited: false }; + if (!getHasTransition(nextProps)) return { exited: true }; + + return null; + } + + handleOpen = () => { + const doc = ownerDocument(this.mountNode); + const container = getContainer(this.props.container, doc.body); + + this.props.manager.add(this, container); + doc.addEventListener('keydown', this.handleDocumentKeyDown); + doc.addEventListener('focus', this.enforceFocus, true); + + if (this.dialogRef) this.handleOpened(); + }; + + handleRendered = () => { + if (this.props.onRendered) this.props.onRendered(); + if (this.props.open) { + this.handleOpened(); + } else { + ariaHidden(this.modalRef, true); + } + }; + + handleOpened = () => { + this.autoFocus(); + this.modalRef.scrollTop = 0; + }; + + handleClose = () => { + this.props.manager.remove(this); + + const doc = ownerDocument(this.mountNode); + + doc.removeEventListener('keydown', this.handleDocumentKeyDown); + doc.removeEventListener('focus', this.enforceFocus, true); + + this.restoreLastFocus(); + }; + + handleExited = () => { + this.setState({ exited: true }); + }; + + handleBackdropClick = event => { + if (event.target !== event.currentTarget) return; + if (this.props.onBackdropClick) this.props.onBackdropClick(event); + if (!this.props.disableBackdropClick && this.props.onClose) + this.props.onClose(event, 'backdropClick'); + }; + + handleDocumentKeyDown = event => { + if (keycode(event) !== 'esc' || !this.isTopModal() || event.defaultPrevented) return; + if (this.props.onEscapeKeyDown) this.props.onEscapeKeyDown(event); + if (!this.props.disableEscapeKeyDown && this.props.onClose) + this.props.onClose(event, 'escapeKeyDown'); + }; + + enforceFocus = () => { + if (!this.isTopModal() || this.props.disableEnforceFocus || !this.mounted || !this.dialogRef) + return; + + const currentActiveElement = ownerDocument(this.mountNode).activeElement; + + if (!this.dialogRef.contains(currentActiveElement)) this.dialogRef.focus(); + }; + + handlePortalRef = ref => { + this.mountNode = ref ? ref.getMountNode() : ref; + }; + + handleModalRef = ref => { + this.modalRef = ref; + }; + + onRootRef = ref => { + this.dialogRef = ref; + }; + + autoFocus() { + if (this.props.disableAutoFocus || !this.dialogRef) return; + + const currentActiveElement = ownerDocument(this.mountNode).activeElement; + + if (!this.dialogRef.contains(currentActiveElement)) { + if (!this.dialogRef.hasAttribute('tabIndex')) { + warning( + false, + [ + 'The modal content node does not accept focus.', + 'For the benefit of assistive technologies, ' + + 'the tabIndex of the node is being set to "-1".', + ].join('\n'), + ); + this.dialogRef.setAttribute('tabIndex', -1); + } + + this.lastFocus = currentActiveElement; + this.dialogRef.focus(); + } + } + + restoreLastFocus() { + if (this.props.disableRestoreFocus || !this.lastFocus) return; + if (this.lastFocus.focus) this.lastFocus.focus(); + + this.lastFocus = null; + } + + isTopModal() { + return this.props.manager.isTopModal(this); + } + + render() { + const { + BackdropComponent, + BackdropProps, + children, + className, + container, + disablePortal, + hideBackdrop, + keepMounted, + open, + position, + zIndex, + } = this.props; + const { exited } = this.state; + const hasTransition = getHasTransition(this.props); + + if (!keepMounted && !open && (!hasTransition || exited)) return null; + + const childProps = {}; + + if (hasTransition) + childProps.onExited = createChainedFunction(this.handleExited, children.props.onExited); + if (children.props.role === undefined) childProps.role = children.props.role || 'document'; + if (children.props.tabIndex === undefined) + childProps.tabIndex = children.props.tabIndex || '-1'; + + return ( + + + {hideBackdrop ? null : ( + + )} + {React.cloneElement(children, childProps)} + + + ); + } +} + +Modal.propTypes = { + BackdropComponent: PropTypes.oneOfType([PropTypes.string, PropTypes.func, PropTypes.object]), + BackdropProps: PropTypes.object, + children: PropTypes.element, + container: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), + disableAutoFocus: PropTypes.bool, + disableBackdropClick: PropTypes.bool, + disableEnforceFocus: PropTypes.bool, + disableEscapeKeyDown: PropTypes.bool, + disablePortal: PropTypes.bool, + disableRestoreFocus: PropTypes.bool, + hideBackdrop: PropTypes.bool, + keepMounted: PropTypes.bool, + manager: PropTypes.object, + onBackdropClick: PropTypes.func, + onClose: PropTypes.func, + onEscapeKeyDown: PropTypes.func, + onRendered: PropTypes.func, + open: PropTypes.bool.isRequired, + position: PropTypes.object, +}; + +Modal.defaultProps = { + BackdropComponent: Backdrop, + disableAutoFocus: false, + disableBackdropClick: false, + disableEnforceFocus: false, + disableEscapeKeyDown: false, + disablePortal: false, + disableRestoreFocus: false, + hideBackdrop: false, + keepMounted: false, + manager: new ModalManager(), + position: { x: 'center', y: 'center' }, +}; + +export default Modal; diff --git a/packages/decap-cms-ui-4/src/Modal/ModalManager.jsx b/packages/decap-cms-ui-4/src/Modal/ModalManager.jsx new file mode 100644 index 000000000000..137627e7d5c4 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Modal/ModalManager.jsx @@ -0,0 +1,119 @@ +import css from 'dom-helpers/css'; +import getScrollbarSize from 'dom-helpers/scrollbarSize'; +import isOverflowing from './isOverflowing'; +import { ariaHidden, ariaHiddenSiblings } from './manageAriaHidden'; + +function findIndexOf(data, callback) { + let idx = -1; + data.some((item, index) => { + if (callback(item)) { + idx = index; + return true; + } + return false; + }); + return idx; +} + +function getPaddingRight(node) { + return parseInt(css(node, 'paddingRight') || 0, 10); +} + +function setContainerStyle(data, container) { + const style = { overflow: 'hidden' }; + + data.style = { + overflow: container.style.overflow, + paddingRight: container.style.paddingRight, + }; + + if (data.overflowing) { + const scrollbarSize = getScrollbarSize(); + + style.paddingRight = `${getPaddingRight(container) + scrollbarSize}px`; + } + + Object.keys(style).forEach(key => { + container.style[key] = style[key]; + }); +} + +function removeContainerStyle(data) { + Object.keys(data.style).forEach(key => { + data.container.style[key] = data.style[key]; + }); +} + +class ModalManager { + constructor(options = {}) { + const { hideSiblingNodes = true, handleContainerOverflow = true } = options; + this.hideSiblingNodes = hideSiblingNodes; + this.handleContainerOverflow = handleContainerOverflow; + this.modals = []; + this.data = []; + } + + add(modal, container) { + let modalIdx = this.modals.indexOf(modal); + + if (modalIdx !== -1) return modalIdx; + + modalIdx = this.modals.length; + this.modals.push(modal); + + if (modal.modalRef) ariaHidden(modal.modalRef, false); + if (this.hideSiblingNodes) ariaHiddenSiblings(container, modal.mountNode, modal.modalRef, true); + + const containerIdx = findIndexOf(this.data, item => item.container === container); + + if (containerIdx !== -1) { + this.data[containerIdx].modals.push(modal); + return modalIdx; + } + + const data = { + modals: [modal], + container, + overflowing: isOverflowing(container), + prevPaddings: [], + }; + + if (this.handleContainerOverflow) setContainerStyle(data, container); + + this.data.push(data); + + return modalIdx; + } + + remove(modal) { + const modalIdx = this.modals.indexOf(modal); + + if (modalIdx === -1) return modalIdx; + + const containerIdx = findIndexOf(this.data, item => item.modals.indexOf(modal) !== -1); + const data = this.data[containerIdx]; + + data.modals.splice(data.modals.indexOf(modal), 1); + this.modals.splice(modalIdx, 1); + + if (data.modals.length === 0) { + if (this.handleContainerOverflow) removeContainerStyle(data); + if (modal.modalRef) ariaHidden(modal.modalRef, true); + if (this.hideSiblingNodes) + ariaHiddenSiblings(data.container, modal.mountNode, modal.modalRef, false); + this.data.splice(containerIdx, 1); + } else if (this.hideSiblingNodes) { + const nextTop = data.modals[data.modals.length - 1]; + + if (nextTop.modalRef) ariaHidden(nextTop.modalRef, false); + } + + return modalIdx; + } + + isTopModal(modal) { + return !!this.modals.length && this.modals[this.modals.length - 1] === modal; + } +} + +export default ModalManager; diff --git a/packages/decap-cms-ui-4/src/Modal/index.js b/packages/decap-cms-ui-4/src/Modal/index.js new file mode 100644 index 000000000000..0690fecf679c --- /dev/null +++ b/packages/decap-cms-ui-4/src/Modal/index.js @@ -0,0 +1 @@ +export { default } from './Modal'; diff --git a/packages/decap-cms-ui-4/src/Modal/isOverflowing.js b/packages/decap-cms-ui-4/src/Modal/isOverflowing.js new file mode 100644 index 000000000000..21095c71b85d --- /dev/null +++ b/packages/decap-cms-ui-4/src/Modal/isOverflowing.js @@ -0,0 +1,24 @@ +import isWindow from 'dom-helpers/isWindow'; +import ownerDocument from '../utils/ownerDocument'; +import ownerWindow from '../utils/ownerWindow'; + +export function isBody(node) { + return node && node.tagName.toLowerCase() === 'body'; +} + +// Do we have a vertical scroll bar? +export default function isOverflowing(container) { + const doc = ownerDocument(container); + const win = ownerWindow(doc); + + if (!isWindow(doc) && !isBody(container)) { + return container.scrollHeight > container.clientHeight; + } + + // Takes in account potential non zero margin on the body. + const style = win.getComputedStyle(doc.body); + const marginLeft = parseInt(style.getPropertyValue('margin-left'), 10); + const marginRight = parseInt(style.getPropertyValue('margin-right'), 10); + + return marginLeft + doc.body.clientWidth + marginRight < win.innerWidth; +} diff --git a/packages/decap-cms-ui-4/src/Modal/manageAriaHidden.js b/packages/decap-cms-ui-4/src/Modal/manageAriaHidden.js new file mode 100644 index 000000000000..d8c3609d706b --- /dev/null +++ b/packages/decap-cms-ui-4/src/Modal/manageAriaHidden.js @@ -0,0 +1,24 @@ +const BLACKLIST = ['template', 'script', 'style']; + +function isHidable(node) { + return node.nodeType === 1 && BLACKLIST.indexOf(node.tagName.toLowerCase()) === -1; +} + +function siblings(container, mount, currentNode, callback) { + const blacklist = [mount, currentNode]; // eslint-disable-line no-param-reassign + [].forEach.call(container.children, node => { + if (blacklist.indexOf(node) === -1 && isHidable(node)) callback(node); + }); +} + +export function ariaHidden(node, show) { + if (show) { + node.setAttribute('aria-hidden', 'true'); + } else { + node.removeAttribute('aria-hidden'); + } +} + +export function ariaHiddenSiblings(container, mountNode, currentNode, show) { + siblings(container, mountNode, currentNode, node => ariaHidden(node, show)); +} diff --git a/packages/decap-cms-ui-4/src/NavMenu/MobileNavMenu.jsx b/packages/decap-cms-ui-4/src/NavMenu/MobileNavMenu.jsx new file mode 100644 index 000000000000..4a39ef560751 --- /dev/null +++ b/packages/decap-cms-ui-4/src/NavMenu/MobileNavMenu.jsx @@ -0,0 +1,250 @@ +import React, { useState } from 'react'; +import styled from '@emotion/styled'; +import color from 'color'; +import { Transition } from 'react-transition-group'; + +import { ExternalLinkIcon } from './NavMenuItem'; + +import Card from '../Card'; +import Icon from '../Icon'; +import LogoTile from '../LogoTile'; +import UserMenu from '../UserMenu'; +import { Menu, MenuItem } from '../Menu'; +import { useUIContext } from '../hooks'; + +const NavWrap = styled.div` + height: 3.5rem; +`; +const Nav = styled(Card)` + background-color: ${({ theme }) => theme.color.surface}; + height: 3.5rem; + display: flex; + padding: 4px; + position: fixed; + z-index: 200; + bottom: 0; + left: 0; + width: 100%; +`; +Nav.defaultProps = { elevation: 'sm', rounded: false, direction: 'up' }; +const NavIconButtonWrap = styled.button` + margin: 4px; + flex: 1; + padding: 0; + position: relative; + background-color: ${({ active, theme }) => + active + ? color(theme.color.success['900']) + .alpha(0.2) + .string() + : 'transparent'}; + border: 0; + border-radius: 6px; + color: ${({ active, theme }) => (active ? theme.color.success['900'] : theme.color.highEmphasis)}; + overflow: hidden; + outline: none; + cursor: pointer; + transition: 200ms; + &:hover { + background-color: ${({ active, theme }) => + active + ? color(theme.color.success['500']) + .alpha(0.3) + .string() + : theme.color.surfaceHighlight}; + } +`; +const IconWrap = styled.span` + display: block; + display: flex; + align-items: center; + justify-content: center; + transform: translateY(${({ active }) => (active ? '-100%' : '0')}); + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + transition: transform 250ms cubic-bezier(0.4, 0, 0.2, 1); +`; +const ActiveIconWrap = styled(IconWrap)` + transform: translateY(${({ active }) => (active ? '0' : '100%')}); +`; + +const MenuWrap = styled.div` + background-color: ${({ theme }) => theme.color.surface}; + position: fixed; + top: 0; + right: 0; + left: 0; + bottom: 3.5rem; + display: flex; + flex-direction: column; + height: calc(100vh - 3.5rem); /* Fallback for browsers that do not support Custom Properties */ + height: calc(var(--vh, 1vh) * 100 - 3.5rem); + /* height: calc(100vh - 3.5rem); */ + z-index: 199; + transition: 250ms; + ${({ state, theme }) => { + switch (state) { + case 'entering': + return `background-color: transparent;`; + case 'entered': + return `background-color: ${theme.color.surface};`; + case 'exiting': + return `background-color: transparent;`; + case 'exited': + return `background-color: transparent;`; + default: + return ``; + } + }} +`; +const ToolbarWrap = styled(Card)` + background-color: ${({ theme }) => theme.color.surface}; + height: 3.5rem; + display: flex; + position: relative; + z-index: 100; + transform: translateY(0); + transition: 250ms; + ${({ state }) => { + switch (state) { + case 'entering': + return `transform: translateY(-100%);`; + case 'entered': + return `transform: translateY(0);`; + case 'exiting': + return `transform: translateY(-100%);`; + case 'exited': + return `transform: translateY(-100%);`; + default: + return ``; + } + }} +`; +ToolbarWrap.defaultProps = { rounded: false, elevation: 'sm' }; +const SiteNameWrap = styled.div` + flex: 1; + padding: 0.5rem 0; + display: flex; + align-items: center; +`; +const SiteName = styled.div` + color: ${({ theme }) => theme.color.highEmphasis}; + font-weight: bold; + margin-right: 12px; +`; +const UserMenuWrap = styled.div` + padding: 0.5rem 1rem; + display: flex; + align-items: center; +`; + +const MenuContent = styled.div` + padding: 12px 0; + display: flex; + flex-direction: column; + position: relative; + overflow-x: hidden; + overflow-y: auto; + flex: 1; + transform: translateY(0); + transition: 250ms; + ${({ state }) => { + switch (state) { + case 'entering': + return `transform: translateY(64px); opacity: 0;`; + case 'entered': + return `transform: translateY(0); opacity: 1;`; + case 'exiting': + return `transform: translateY(64px); opacity: 0;`; + case 'exited': + return `transform: translateY(64px); opacity: 0;`; + default: + return ``; + } + }} +`; + +const MobileNavMenu = ({ children }) => { + const { darkMode, setDarkMode } = useUIContext(); + const [openMenu, setOpenMenu] = useState(false); + const [addMenuOpen, setAddMenuOpen] = useState(false); + return ( + + + setAddMenuOpen(false)}> + setAddMenuOpen(false)}> + New Post + + setAddMenuOpen(false)}> + New Post Category + + setAddMenuOpen(false)}> + New Page + + setAddMenuOpen(false)}> + New Product + + setAddMenuOpen(false)}> + New Product Category + + setAddMenuOpen(false)}> + New Author + + setAddMenuOpen(false)}> + New Event + + + + {state => ( + + + + + My Website + + + + + + + {children} + + )} + + + ); +}; + +const NavIconButton = ({ active, icon, hasSubmenu, ...props }) => { + return ( + + + + + {hasSubmenu && ( + + + + )} + + ); +}; + +export default MobileNavMenu; diff --git a/packages/decap-cms-ui-4/src/NavMenu/NavMenu.jsx b/packages/decap-cms-ui-4/src/NavMenu/NavMenu.jsx new file mode 100644 index 000000000000..3c1b27ce48dd --- /dev/null +++ b/packages/decap-cms-ui-4/src/NavMenu/NavMenu.jsx @@ -0,0 +1,91 @@ +import React, { useState, useEffect } from 'react'; +import PropTypes from 'prop-types'; +import styled from '@emotion/styled'; +import Card from '../Card'; +import NavMenuGroupLabel from './NavMenuGroupLabel'; +import NavMenuItem, { NavItemContents } from './NavMenuItem'; +import MobileNavMenu from './MobileNavMenu'; +import { isWindowDown } from '../utils/responsive'; +import { useUIContext } from '../hooks'; + +const NavWrap = styled(Card)` + width: ${({ collapsed }) => (collapsed ? '56px' : '240px')}; + height: 100%; + padding: 12px 0; + background-color: ${({ theme }) => theme.color.elevatedSurface}; + display: flex; + flex-direction: column; + position: relative; + transition: width ${({ collapsed }) => (collapsed ? '200ms' : '250ms')} + cubic-bezier(0.4, 0, 0.2, 1); + overflow-x: hidden; + overflow-y: auto; + ${NavMenuGroupLabel} { + margin-top: 0; + opacity: 1; + transition: ${({ collapsed }) => (collapsed ? '200ms' : '250ms')} cubic-bezier(0.4, 0, 0.2, 1); + } + ${NavItemContents} { + width: 13.5rem; + min-width: 13.5rem; + } + ${({ collapsed }) => + collapsed + ? ` + ${NavMenuGroupLabel} { + opacity: 0; + margin-top: -1rem; + } + ` + : ``} +`; +NavWrap.defaultProps = { elevation: 'xs', rounded: false, direction: 'right' }; + +const NavContent = styled.div` + display: flex; + flex-direction: column; + flex: 1; +`; +const CondenseNavMenuItem = styled(NavMenuItem)` + width: 3.5rem; + & svg { + transform: ${({ collapsed }) => (collapsed ? 'rotate(0deg)' : 'rotate(180deg)')}; + transition: 200ms; + } +`; + +const NavMenu = ({ children, collapsable }) => { + const { navCollapsed: collapsed, setNavCollapsed: setCollapsed } = useUIContext(); + const [isMobile, setIsMobile] = useState(isWindowDown('xs')); + const handleResize = () => setIsMobile(isWindowDown('xs')); + + useEffect(() => { + window.addEventListener('resize', handleResize); + + return () => window.removeEventListener('resize', handleResize); + }, []); + + if (isMobile) return {children}; + + return ( + + {children} + {collapsable && ( + setCollapsed(!collapsed)} + /> + )} + + ); +}; + +NavMenu.propTypes = { + collapsable: PropTypes.bool, +}; +NavMenu.defaultProps = { + collapsable: false, +}; + +export default NavMenu; diff --git a/packages/decap-cms-ui-4/src/NavMenu/NavMenuGroup.jsx b/packages/decap-cms-ui-4/src/NavMenu/NavMenuGroup.jsx new file mode 100644 index 000000000000..e86c248b488e --- /dev/null +++ b/packages/decap-cms-ui-4/src/NavMenu/NavMenuGroup.jsx @@ -0,0 +1,23 @@ +import styled from '@emotion/styled'; + +const NavMenuGroup = styled.div` + padding: 0.75rem 0; + display: flex; + flex-direction: column; + &:first-child { + padding-top: 0; + } + ${({ end }) => + end + ? ` + flex: 1; + justify-content: flex-end; + padding-bottom: 0; + ` + : ``} + &:not(:first-child):not(:last-child) { + /* border-top: 1px solid ${({ theme }) => theme.color.border}; */ + } +`; + +export default NavMenuGroup; diff --git a/packages/decap-cms-ui-4/src/NavMenu/NavMenuGroupLabel.jsx b/packages/decap-cms-ui-4/src/NavMenu/NavMenuGroupLabel.jsx new file mode 100644 index 000000000000..62603154005b --- /dev/null +++ b/packages/decap-cms-ui-4/src/NavMenu/NavMenuGroupLabel.jsx @@ -0,0 +1,20 @@ +import styled from '@emotion/styled'; + +const NavMenuGroupLabel = styled.div` + width: 13.5rem; + min-width: 13.5rem; + margin-left: 1.125rem; + margin-bottom: 0.25rem; + text-transform: uppercase; + font-size: 0.75rem; + font-weight: 600; + letter-spacing: 0.5px; + line-height: 1rem; + color: ${({ theme }) => theme.color.lowEmphasis}; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + transition: 200ms; +`; + +export default NavMenuGroupLabel; diff --git a/packages/decap-cms-ui-4/src/NavMenu/NavMenuItem.jsx b/packages/decap-cms-ui-4/src/NavMenu/NavMenuItem.jsx new file mode 100644 index 000000000000..009a7075a03a --- /dev/null +++ b/packages/decap-cms-ui-4/src/NavMenu/NavMenuItem.jsx @@ -0,0 +1,126 @@ +import React from 'react'; +import styled from '@emotion/styled'; +import Tooltip from '../Tooltip'; +import Icon from '../Icon'; +import { ButtonGroup } from '../Button'; +import color from 'color'; +import { useUIContext } from '../hooks'; + +const NavMenuItemWrap = styled.a` + width: 100%; + display: flex; + align-items: center; + height: 2.5rem; + padding: 0 12px; + cursor: pointer; +`; +export const ExternalLinkIcon = styled(Icon)` + color: ${({ theme }) => theme.color.disabled}; +`; +ExternalLinkIcon.defaultProps = { name: 'external-link', size: 'sm' }; + +const NavMenuItemInside = styled.span` + display: flex; + align-items: center; + width: 100%; + overflow: hidden; + color: ${({ theme, active }) => + active ? theme.color.success['900'] : theme.color.mediumEmphasis}; + background-color: ${({ theme, active }) => + active + ? color(theme.color.success['900']) + .alpha(0.1) + .string() + : `transparent`}; + border: none; + height: 2rem; + border-radius: 6px; + outline: none; + transition: 200ms; + ${NavMenuItemWrap}:hover & { + color: ${({ theme, active }) => + active ? theme.color.success['900'] : theme.color.highEmphasis}; + background-color: ${({ theme, active }) => + active + ? color(theme.color.success['900']) + .alpha(0.1) + .string() + : color(theme.color.highEmphasis) + .alpha(0.05) + .string()}; + ${({ active }) => + active + ? ` + cursor: default; + ` + : ``} + } + ${NavMenuItemWrap}:active & { + color: ${({ theme, active }) => + active ? theme.color.success['900'] : theme.color.highEmphasis}; + background-color: ${({ theme, active }) => + active + ? color(theme.color.success['900']) + .alpha(0.1) + .string() + : color(theme.color.highEmphasis) + .alpha(0.1) + .string()}; + } + ${ButtonGroup} & { + margin: 2px; + } + & svg { + margin: 0.375rem; + } + & ${ExternalLinkIcon} { + margin-right: 0.5rem; + } +`; +const Label = styled.span` + margin-left: 0.75rem; + display: flex; + flex: 1; + overflow: hidden; + font-size: 0.875rem; + font-weight: 600; + opacity: ${({ collapsed }) => (collapsed ? '0' : '1')}; + transition: opacity 200ms; +`; +export const NavItemContents = styled.span` + display: flex; + align-items: center; + width: 100%; +`; + +const NavMenuItem = ({ icon, children, className, href, active, onClick }) => { + const { navCollapsed } = useUIContext(); + + return ( + + + + + + + {href && } + + + + + ); +}; + +export default NavMenuItem; diff --git a/packages/decap-cms-ui-4/src/NavMenu/index.js b/packages/decap-cms-ui-4/src/NavMenu/index.js new file mode 100644 index 000000000000..5afcb408e247 --- /dev/null +++ b/packages/decap-cms-ui-4/src/NavMenu/index.js @@ -0,0 +1,4 @@ +export { default as NavMenu } from './NavMenu'; +export { default as NavMenuGroup } from './NavMenuGroup'; +export { default as NavMenuGroupLabel } from './NavMenuGroupLabel'; +export { default as NavMenuItem } from './NavMenuItem'; diff --git a/packages/decap-cms-ui-4/src/NavMenu/story.jsx b/packages/decap-cms-ui-4/src/NavMenu/story.jsx new file mode 100644 index 000000000000..db3c935d1a1a --- /dev/null +++ b/packages/decap-cms-ui-4/src/NavMenu/story.jsx @@ -0,0 +1,125 @@ +import React, { useState } from 'react'; +import styled from '@emotion/styled'; +import { withKnobs, boolean } from '@storybook/addon-knobs'; + +import { NavMenu, NavMenuGroup, NavMenuGroupLabel, NavMenuItem } from '.'; + +export default { + title: 'Components/NavMenu', + decorators: [withKnobs], +}; + +const Wrap = styled.div` + width: 100%; + height: 100%; + background-color: ${({ theme }) => theme.color.background}; +`; + +export const _NavMenu = () => { + const [activeItemId, setActiveItemId] = useState('dashboard'); + const showGroupLabels = boolean('Show group labels', false); + const collapsable = boolean('collapsable', true); + + return ( + + + + {showGroupLabels && Primary items} + setActiveItemId('dashboard')} + icon="layout" + > + Dashboard + + setActiveItemId('workflow')} + icon="workflow" + > + Workflow + + setActiveItemId('media')} + icon="image" + > + Media + + setActiveItemId('posts')} + icon="pin" + > + Posts + + setActiveItemId('post-categories')} + icon="inbox" + > + Post Categories + + setActiveItemId('pages')} + icon="file-text" + > + Pages + + setActiveItemId('products')} + icon="shopping-cart" + > + Products + + setActiveItemId('product-categories')} + icon="package" + > + Product Categories + + setActiveItemId('authors')} + icon="users" + > + Authors + + setActiveItemId('events')} + icon="calendar" + > + Events + + + + {showGroupLabels && Secondary Items} + + Analytics + + + Netlify + + + Github Repository + + setActiveItemId('settings')} + icon="settings" + > + Settings + + + + + ); +}; + +_NavMenu.story = { + name: 'NavMenu', +}; diff --git a/packages/decap-cms-ui-4/src/ParticleBackground/ParticleBackground.jsx b/packages/decap-cms-ui-4/src/ParticleBackground/ParticleBackground.jsx new file mode 100644 index 000000000000..eaa607b66c38 --- /dev/null +++ b/packages/decap-cms-ui-4/src/ParticleBackground/ParticleBackground.jsx @@ -0,0 +1,51 @@ +import React, { useEffect, useRef } from 'react'; +import styled from '@emotion/styled'; +import Particle from 'particleground-light'; +import color from '../utils/color'; + +const ParticleContainer = styled.div` + background-color: ${color.neutral['1400']}; + position: relative; + width: 100%; + height: 100%; +`; +const Particles = styled.div` + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + height: 100%; /* We need a height defined */ +`; + +const ParticleBackground = ({ children, className }) => { + const particlesRef = useRef(null); + useEffect(() => { + if (particlesRef.current) { + setTimeout(() => { + new Particle(particlesRef.current, { + dotColor: color.neutral['1000'], + lineColor: color.neutral['1200'], + minSpeedX: 0.5, + maxSpeedX: 2, + minSpeedY: 0.5, + maxSpeedY: 2, + density: 20000, // One particle every n pixels + curvedLines: false, + proximity: 100, // How close two dots need to be before they join + parallaxMultiplier: 10, // Lower the number is more extreme parallax + particleRadius: 4, // Dot size + }); + }); + } + }, [particlesRef]); + + return ( + + + {children} + + ); +}; + +export default ParticleBackground; diff --git a/packages/decap-cms-ui-4/src/ParticleBackground/index.js b/packages/decap-cms-ui-4/src/ParticleBackground/index.js new file mode 100644 index 000000000000..104a67e00289 --- /dev/null +++ b/packages/decap-cms-ui-4/src/ParticleBackground/index.js @@ -0,0 +1 @@ +export { default } from './ParticleBackground'; diff --git a/packages/decap-cms-ui-4/src/ParticleBackground/story.jsx b/packages/decap-cms-ui-4/src/ParticleBackground/story.jsx new file mode 100644 index 000000000000..b004feeefd0a --- /dev/null +++ b/packages/decap-cms-ui-4/src/ParticleBackground/story.jsx @@ -0,0 +1,23 @@ +import React from 'react'; +import styled from '@emotion/styled'; +import { withKnobs } from '@storybook/addon-knobs'; + +import ParticleBackground from '.'; + +const StyledParticleBackground = styled(ParticleBackground)` + width: 100%; + height: 100%; +`; + +export default { + title: 'Components/ParticleBackground', + decorators: [withKnobs], +}; + +export const _ParticleBackground = () => { + return ; +}; + +_ParticleBackground.story = { + name: 'ParticleBackground', +}; diff --git a/packages/decap-cms-ui-4/src/Popover/Popover.jsx b/packages/decap-cms-ui-4/src/Popover/Popover.jsx new file mode 100644 index 000000000000..95f7424be833 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Popover/Popover.jsx @@ -0,0 +1,382 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import ReactDOM from 'react-dom'; +import warning from 'warning'; +import debounce from 'debounce'; +import EventListener from 'react-event-listener'; +import ownerDocument from '../utils/ownerDocument'; +import ownerWindow from '../utils/ownerWindow'; +import Modal from '../Modal'; +import Grow from '../transitions/Grow'; +import Slide from '../transitions/Slide'; +import { isWindowDown } from '../utils/responsive'; + +function getOffsetTop(rect, y) { + let offset = 0; + + if (typeof y === 'number') { + offset = y; + } else if (y === 'center') { + offset = rect.height / 2; + } else if (y === 'bottom') { + offset = rect.height; + } + + return offset; +} + +function getOffsetLeft(rect, x) { + let offset = 0; + + if (typeof x === 'number') { + offset = x; + } else if (x === 'center') { + offset = rect.width / 2; + } else if (x === 'right') { + offset = rect.width; + } + + return offset; +} + +function getTransformOriginValue(transformOrigin) { + return [transformOrigin.x, transformOrigin.y] + .map(n => (typeof n === 'number' ? `${n}px` : n)) + .join(' '); +} + +function getScrollParent(parent, child) { + let element = child; + let scrollTop = 0; + + while (element && element !== parent) { + element = element.parentNode; + scrollTop += element.scrollTop; + } + return scrollTop; +} + +function getAnchorEl(anchorEl) { + return typeof anchorEl === 'function' ? anchorEl() : anchorEl; +} + +const containerStyles = { + position: 'absolute', + maxWidth: 'calc(100% - 32px)', + maxHeight: 'calc(100% - 32px)', + outline: 'none', +}; +const mobileContainerStyles = { + position: 'absolute', + maxWidth: '100%', + maxHeight: '100%', + outline: 'none', + bottom: 0, + width: '100%', +}; + +class Popover extends Component { + handleGetOffsetTop = getOffsetTop; + handleGetOffsetLeft = getOffsetLeft; + + state = { style: {} }; + + constructor() { + super(); + + if (typeof window !== 'undefined') { + this.handleResize = debounce(() => { + this.setPositioningStyles(this.containerRef); + }, 166); // Corresponds to 10 frames at 60 Hz. + } + } + + componentDidUpdate(prevProps) { + if ( + prevProps.anchorOrigin.x !== this.props.anchorOrigin.x || + prevProps.anchorOrigin.y !== this.props.anchorOrigin.y || + prevProps.transformOrigin.x !== this.props.transformOrigin.x || + prevProps.transformOrigin.y !== this.props.transformOrigin.y + ) { + this.handleResize(); + } + } + + componentDidMount() { + if (this.props.action) { + this.props.action({ + updatePosition: this.handleResize, + }); + } + } + + componentWillUnmount() { + this.handleResize.clear(); + } + + setPositioningStyles = element => { + const newStyle = this.getPositioningStyle(element); + + if (newStyle.top) this.setState({ style: newStyle }); + }; + + getPositioningStyle = element => { + const { anchorEl, anchorReference, marginThreshold, supportsMobile } = this.props; + const contentAnchorOffset = this.getContentAnchorOffset(element); + const elemRect = { + width: element.offsetWidth, + height: element.offsetHeight, + }; + const transformOrigin = this.getTransformOrigin(elemRect, contentAnchorOffset); + const isMobile = isWindowDown('xs') && supportsMobile; + + if (anchorReference === 'none') { + return { + top: null, + left: null, + transformOrigin: getTransformOriginValue(transformOrigin), + }; + } + + if (isMobile) { + return { + top: null, + bottom: 0, + left: 0, + transformOrigin: getTransformOriginValue(transformOrigin), + }; + } + + const anchorOffset = this.getAnchorOffset(contentAnchorOffset); + let top = anchorOffset.top - transformOrigin.y; + let left = anchorOffset.left - transformOrigin.x; + const bottom = top + elemRect.height; + const right = left + elemRect.width; + const containerWindow = ownerWindow(getAnchorEl(anchorEl)); + const heightThreshold = containerWindow.innerHeight - marginThreshold; + const widthThreshold = containerWindow.innerWidth - marginThreshold; + + if (top < marginThreshold) { + const diff = top - marginThreshold; + top -= diff; + transformOrigin.y += diff; + } else if (bottom > heightThreshold) { + const diff = bottom - heightThreshold; + top -= diff; + transformOrigin.y += diff; + } + + warning( + element.height < heightThreshold || !element.height || !heightThreshold, + [ + 'The Popover component is too tall.', + `Some part of it can not be seen on the screen (${(element.height = heightThreshold)}px).)`, + 'Please consider adding a `max-height` to improve the user-experience.', + ].join('\n'), + ); + + if (left < marginThreshold) { + const diff = left - marginThreshold; + left -= diff; + transformOrigin.x += diff; + } else if (right > widthThreshold) { + const diff = right - widthThreshold; + left -= diff; + transformOrigin.x += diff; + } + + const elemStyles = { + top: `${top}px`, + left: `${left}px`, + transformOrigin: getTransformOriginValue(transformOrigin), + }; + + return elemStyles; + }; + + getAnchorOffset(contentAnchorOffset) { + const { anchorEl, anchorOrigin, anchorReference, anchorPosition } = this.props; + + if (anchorReference === 'anchorPosition') { + warning( + anchorPosition, + 'You need to provide a `anchorPosition` property when using ', + ); + return anchorPosition; + } + + const anchorElement = getAnchorEl(anchorEl) || ownerDocument(this.containerRef).body; + const anchorRect = anchorElement.getBoundingClientRect(); + const anchorY = contentAnchorOffset === 0 ? anchorOrigin.y : 'center'; + + return { + top: anchorRect.top + this.handleGetOffsetTop(anchorRect, anchorY), + left: anchorRect.left + this.handleGetOffsetLeft(anchorRect, anchorOrigin.x), + }; + } + + getContentAnchorOffset(element) { + const { getContentAnchorEl, anchorReference } = this.props; + let contentAnchorOffset = 0; + + if (getContentAnchorEl && anchorReference === 'anchorEl') { + const contentAnchorEl = getContentAnchorEl(element); + + if (contentAnchorEl && element.contains(contentAnchorEl)) { + const scrollTop = getScrollParent(element, contentAnchorEl); + contentAnchorOffset = + contentAnchorEl.offsetTop + contentAnchorEl.clientHeight / 2 - scrollTop || 0; + } + } + + return contentAnchorOffset; + } + + getTransformOrigin(elemRect, contentAnchorOffset = 0) { + const { transformOrigin, supportsMobile } = this.props; + const isMobile = isWindowDown('xs') && supportsMobile; + + if (isMobile) { + return { x: 'center', y: 'bottom' }; + } + return { + y: this.handleGetOffsetTop(elemRect, transformOrigin.y) + contentAnchorOffset, + x: this.handleGetOffsetLeft(elemRect, transformOrigin.x), + }; + } + + handleEntering = element => { + if (this.props.onEntering) this.props.onEntering(element); + this.setPositioningStyles(element); + }; + + render() { + const { + anchorEl, + children, + className, + container: containerProp, + modalClasses, + onEnter, + onEntered, + onExit, + onExited, + onExiting, + open, + role, + supportsMobile, + TransitionComponent, + transitionDuration: transitionDurationProp, + TransitionProps, + ...other + } = this.props; + + const { style } = this.state; + const transitionDuration = transitionDurationProp; + const container = + containerProp || (anchorEl ? ownerDocument(getAnchorEl(anchorEl)).body : undefined); + const isMobile = isWindowDown('xs') && supportsMobile; + const PopoverTransitionComponent = isMobile ? Slide : TransitionComponent; + const PopoverTransitionProps = isMobile ? { direction: 'up' } : TransitionProps; + + return ( + + +
{ + // eslint-disable-next-line react/no-find-dom-node + this.containerRef = ReactDOM.findDOMNode(ref); + }} + > + + {children} +
+
+
+ ); + } +} + +Popover.propTypes = { + action: PropTypes.func, + anchorEl: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), + anchorOrigin: PropTypes.shape({ + x: PropTypes.oneOfType([PropTypes.number, PropTypes.oneOf(['left', 'center', 'right'])]) + .isRequired, + y: PropTypes.oneOfType([PropTypes.number, PropTypes.oneOf(['top', 'center', 'bottom'])]) + .isRequired, + }), + anchorPosition: PropTypes.shape({ + left: PropTypes.number.isRequired, + top: PropTypes.number.isRequired, + }), + anchorReference: PropTypes.oneOf(['anchorEl', 'anchorPosition', 'none']), + children: PropTypes.node, + container: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), + elevation: PropTypes.number, + getContentAnchorEl: PropTypes.func, + marginThreshold: PropTypes.number, + ModalClasses: PropTypes.object, + onClose: PropTypes.func, + onEnter: PropTypes.func, + onEntered: PropTypes.func, + onEntering: PropTypes.func, + onExit: PropTypes.func, + onExited: PropTypes.func, + onExiting: PropTypes.func, + open: PropTypes.bool.isRequired, + PaperProps: PropTypes.object, + role: PropTypes.string, + transformOrigin: PropTypes.shape({ + x: PropTypes.oneOfType([PropTypes.number, PropTypes.oneOf(['left', 'center', 'right'])]) + .isRequired, + y: PropTypes.oneOfType([PropTypes.number, PropTypes.oneOf(['top', 'center', 'bottom'])]) + .isRequired, + }), + TransitionComponent: PropTypes.oneOfType([PropTypes.string, PropTypes.func, PropTypes.object]), + transitionDuration: PropTypes.oneOfType([ + PropTypes.number, + PropTypes.shape({ enter: PropTypes.number, exit: PropTypes.number }), + PropTypes.oneOf(['auto']), + ]), + TransitionProps: PropTypes.object, +}; + +Popover.defaultProps = { + anchorReference: 'anchorEl', + anchorOrigin: { + y: 'top', + x: 'left', + }, + elevation: 8, + marginThreshold: 16, + transformOrigin: { + y: 'top', + x: 'left', + }, + TransitionComponent: Grow, + transitionDuration: 200, +}; + +export default Popover; diff --git a/packages/decap-cms-ui-4/src/Popover/index.js b/packages/decap-cms-ui-4/src/Popover/index.js new file mode 100644 index 000000000000..44c04deafa71 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Popover/index.js @@ -0,0 +1 @@ +export { default } from './Popover'; diff --git a/packages/decap-cms-ui-4/src/Portal/Portal.jsx b/packages/decap-cms-ui-4/src/Portal/Portal.jsx new file mode 100644 index 000000000000..9b1740880f20 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Portal/Portal.jsx @@ -0,0 +1,69 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import PropTypes from 'prop-types'; +import ownerDocument from '../utils/ownerDocument'; + +function getContainer(container, defaultContainer) { + container = typeof container === 'function' ? container() : container; + // eslint-disable-next-line react/no-find-dom-node + return ReactDOM.findDOMNode(container) || defaultContainer; +} + +function getOwnerDocument(element) { + // eslint-disable-next-line react/no-find-dom-node + return ownerDocument(ReactDOM.findDOMNode(element)); +} + +class Portal extends React.Component { + componentDidMount() { + this.setMountNode(this.props.container); + if (!this.props.disablePortal) this.forceUpdate(this.props.onRendered); + } + + componentDidUpdate(prevProps) { + if ( + prevProps.container !== this.props.container || + prevProps.disablePortal !== this.props.disablePortal + ) { + this.setMountNode(this.props.container); + if (!this.props.disablePortal) this.forceUpdate(this.props.onRendered); + } + } + + componentWillUnmount() { + this.mountNode = null; + } + + setMountNode(container) { + if (this.props.disablePortal) { + // eslint-disable-next-line react/no-find-dom-node + this.mountNode = ReactDOM.findDOMNode(this).parentElement; + return; + } + this.mountNode = getContainer(container, getOwnerDocument(this).body); + } + + getMountNode = () => this.mountNode; + + render() { + const { children, disablePortal } = this.props; + + if (disablePortal) { + return children; + } + return this.mountNode ? ReactDOM.createPortal(children, this.mountNode) : null; + } +} + +Portal.propTypes = { + children: PropTypes.node.isRequired, + container: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), + disablePortal: PropTypes.bool, + onRendered: PropTypes.func, +}; + +Portal.defaultProps = { + disablePortal: false, +}; + +export default Portal; diff --git a/packages/decap-cms-ui-4/src/Portal/index.js b/packages/decap-cms-ui-4/src/Portal/index.js new file mode 100644 index 000000000000..f227015fb984 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Portal/index.js @@ -0,0 +1 @@ +export { default } from './Portal'; diff --git a/packages/decap-cms-ui-4/src/ResponsiveLayout/ResponsiveLayout.jsx b/packages/decap-cms-ui-4/src/ResponsiveLayout/ResponsiveLayout.jsx new file mode 100644 index 000000000000..4ac6a06aa642 --- /dev/null +++ b/packages/decap-cms-ui-4/src/ResponsiveLayout/ResponsiveLayout.jsx @@ -0,0 +1,8 @@ +import { useWindowDimensions } from '../WindowDimensionsProvider'; + +const ResponsiveLayout = ({ breakPoint = 414, renderMobile, renderDesktop }) => { + const { width } = useWindowDimensions(); + return width > breakPoint ? renderDesktop() : renderMobile(); +}; + +export default ResponsiveLayout; diff --git a/packages/decap-cms-ui-4/src/ResponsiveLayout/index.js b/packages/decap-cms-ui-4/src/ResponsiveLayout/index.js new file mode 100644 index 000000000000..6f7ab12b8856 --- /dev/null +++ b/packages/decap-cms-ui-4/src/ResponsiveLayout/index.js @@ -0,0 +1 @@ +export { default } from './ResponsiveLayout'; diff --git a/packages/decap-cms-ui-4/src/RootRef/RootRef.jsx b/packages/decap-cms-ui-4/src/RootRef/RootRef.jsx new file mode 100644 index 000000000000..8c89d0dbc6d6 --- /dev/null +++ b/packages/decap-cms-ui-4/src/RootRef/RootRef.jsx @@ -0,0 +1,84 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import PropTypes from 'prop-types'; +import exactProp from '../utils/exactProp'; +import { setRef } from '../utils/helpers'; + +/** + * Helper component to allow attaching a ref to a + * wrapped element to access the underlying DOM element. + * + * It's highly inspired by https://github.com/facebook/react/issues/11401#issuecomment-340543801. + * For example: + * ```jsx + * import React from 'react'; + * import RootRef from '@material-ui/core/RootRef'; + * + * class MyComponent extends React.Component { + * constructor() { + * super(); + * this.domRef = React.createRef(); + * } + * + * componentDidMount() { + * console.log(this.domRef.current); // DOM node + * } + * + * render() { + * return ( + * + * + * + * ); + * } + * } + * ``` + */ +class RootRef extends React.Component { + componentDidMount() { + // eslint-disable-next-line react/no-find-dom-node + this.ref = ReactDOM.findDOMNode(this); + setRef(this.props.rootRef, this.ref); + } + + componentDidUpdate(prevProps) { + // eslint-disable-next-line react/no-find-dom-node + const ref = ReactDOM.findDOMNode(this); + + if (prevProps.rootRef !== this.props.rootRef || this.ref !== ref) { + if (prevProps.rootRef !== this.props.rootRef) { + setRef(prevProps.rootRef, null); + } + + this.ref = ref; + setRef(this.props.rootRef, this.ref); + } + } + + componentWillUnmount() { + this.ref = null; + setRef(this.props.rootRef, null); + } + + render() { + return this.props.children; + } +} + +RootRef.propTypes = { + /** + * The wrapped element. + */ + children: PropTypes.element.isRequired, + /** + * Provide a way to access the DOM node of the wrapped element. + * You can provide a callback ref or a `React.createRef()` ref. + */ + rootRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]).isRequired, +}; + +if (process.env.NODE_ENV !== 'production') { + RootRef.propTypes = exactProp(RootRef.propTypes); +} + +export default RootRef; diff --git a/packages/decap-cms-ui-4/src/RootRef/index.js b/packages/decap-cms-ui-4/src/RootRef/index.js new file mode 100644 index 000000000000..97f34c7209e5 --- /dev/null +++ b/packages/decap-cms-ui-4/src/RootRef/index.js @@ -0,0 +1 @@ +export { default } from './RootRef'; diff --git a/packages/decap-cms-ui-4/src/SearchBar/SearchBar.jsx b/packages/decap-cms-ui-4/src/SearchBar/SearchBar.jsx new file mode 100644 index 000000000000..e4e1d64fd2fd --- /dev/null +++ b/packages/decap-cms-ui-4/src/SearchBar/SearchBar.jsx @@ -0,0 +1,76 @@ +import React, { useState } from 'react'; +import styled from '@emotion/styled'; +import Icon from '../Icon'; + +const SearchContainer = styled.div` + display: flex; + align-items: center; + position: relative; + width: 100%; + min-width: 200px; + background-color: ${({ theme, focus }) => + focus ? theme.color.elevatedSurfaceHighlight : theme.color.surfaceHighlight}; + border-radius: 6px; + transition: 200ms; + ${({ theme, focus }) => + focus + ? `box-shadow: inset 0 0 0 2px ${theme.color.primary['900']};` + : `box-shadow: inset 0 0 0 0 ${theme.color.primary['900']};`} + &:hover { + background-color: ${({ theme }) => theme.color.elevatedSurfaceHighlight}; + } +`; +const SearchIcon = styled(Icon)` + position: absolute; + z-index: 2; + top: 0.5rem; + left: 0.5rem; + pointer-events: none; + transition: 200ms; + color: ${({ theme, focus }) => (focus ? theme.color.mediumEmphasis : theme.color.disabled)}; +`; +const EndWrap = styled.div` + margin: 0.375rem; + margin-left: 0; +`; + +const SearchInput = styled.input` + flex: 1; + background-color: transparent; + color: ${({ theme }) => theme.color.highEmphasis}; + border: 0; + caret-color: ${({ theme }) => theme.color.primary['900']}; + outline: none; + font-size: 0.875rem; + z-index: 1; + border: 0; + padding: 0; + padding-top: 0.625rem; + padding-right: 0; + padding-bottom: 0.625rem; + padding-left: 2.25rem; + line-height: 1; + &::placeholder { + color: ${({ theme }) => theme.color.disabled}; + } +`; + +const SearchBar = ({ placeholder, renderEnd, onChange, className }) => { + const [focus, setFocus] = useState(); + + return ( + + + setFocus(true)} + onBlur={() => setFocus(false)} + /> + {renderEnd && renderEnd(focus)} + + ); +}; + +export default SearchBar; diff --git a/packages/decap-cms-ui-4/src/SearchBar/index.js b/packages/decap-cms-ui-4/src/SearchBar/index.js new file mode 100644 index 000000000000..f1c565be775c --- /dev/null +++ b/packages/decap-cms-ui-4/src/SearchBar/index.js @@ -0,0 +1 @@ +export { default } from './SearchBar'; \ No newline at end of file diff --git a/packages/decap-cms-ui-4/src/SearchBar/story.jsx b/packages/decap-cms-ui-4/src/SearchBar/story.jsx new file mode 100644 index 000000000000..92199314cb0a --- /dev/null +++ b/packages/decap-cms-ui-4/src/SearchBar/story.jsx @@ -0,0 +1,70 @@ +import React, { useState } from 'react'; +import { text, boolean } from '@storybook/addon-knobs'; +import styled from '@emotion/styled'; +import SearchBar from './SearchBar'; +import { Menu, MenuItem } from '../Menu'; +import { Button } from '../Button'; + +export default { + title: 'Components/SearchBar', +}; + +export const _SearchBar = () => { + const renderEnd = boolean('renderEnd', false); + const placeholder = text('placeholder', 'Search'); + return ( + + : null} + onChange={e => console.log(e.target.value)} + /> + + ); +}; + +_SearchBar.story = { + name: 'SearchBar', +}; + +const SearchWrap = styled.div` + width: 33%; + display: flex; + justify-content: center; + align-items: center; +`; + +const EndContent = () => { + const [categoryMenuAnchorEl, setCategoryMenuAnchorEl] = useState(null); + const [selectedCategory, setSelectedCategory] = useState('Posts'); + const categories = ['Posts', 'Media', 'Pages', 'Products', 'Authors', 'Everywhere']; + function handleClose() { + setCategoryMenuAnchorEl(null); + } + return ( + <> + + setCategoryMenuAnchorEl(null)} + anchorOrigin={{ y: 'bottom', x: 'right' }} + > + {categories.map(category => ( + { + setSelectedCategory(category); + handleClose(); + }} + > + {category} + + ))} + + + ); +}; diff --git a/packages/decap-cms-ui-4/src/Table/Table.jsx b/packages/decap-cms-ui-4/src/Table/Table.jsx new file mode 100644 index 000000000000..9a2a1ed99a02 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Table/Table.jsx @@ -0,0 +1,451 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import styled from '@emotion/styled'; +import { useTable, useSortBy, useFlexLayout, useRowSelect } from 'react-table'; +import { sortableContainer, sortableElement } from 'react-sortable-hoc'; +import { FixedSizeList as List } from 'react-window'; +import AutoSizer from 'react-virtualized-auto-sizer'; +import arrayMove from 'array-move'; +import color from 'color'; + +import Icon from '../Icon'; +import { IconButton } from '../Button'; + +const TableWrap = styled.div` + display: flex; + flex-direction: column; + height: 100%; +`; +const TableHeader = styled.div` + position: sticky; + top: 0; + z-index: 1; +`; +const TableHeaderRow = styled.div` + display: flex; + align-items: flex-end; + padding: 0.75rem 0.5rem; + border-bottom: 1px solid ${({ theme }) => theme.color.border}; + position: relative; + & * { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } +`; +const TableHeaderCell = styled.div` + text-transform: uppercase; + font-weight: bold; + font-size: 12px; + letter-spacing: 0.5px; + padding: 0 0.5rem; + line-height: 16px; + transition: color 200ms; + color: ${({ theme }) => theme.color.lowEmphasis}; + ${({ width }) => (width ? `width: ${width};` : ``)} + ${({ width }) => (width === 'auto' ? `flex: 1;` : ``)} + ${({ canSort, theme }) => + canSort + ? ` + cursor: pointer; + &:hover { + color: ${theme.color.highEmphasis}; + } + ` + : ` + cursor: default; + `} +`; +const TBody = styled.div` + position: relative; + flex: 1; +`; +const TRow = styled.div` + display: flex; + justify-content: stretch; + height: ${({ rowSize }) => + rowSize === 'xs' + ? 32 + : rowSize === 'sm' + ? 48 + : rowSize === 'lg' + ? 64 + : rowSize === 'xl' + ? 80 + : 56}px; + padding: 0 0.5rem; + align-items: center; + background-color: ${({ isSelected, theme }) => + isSelected + ? color(theme.color.success['900']) + .alpha(theme.darkMode ? 0.1 : 0.15) + .string() + : 'transparent'}; + border-bottom: 1px solid ${({ theme }) => theme.color.border}; + transition-duration: 200ms; + transition-property: background-color; + ${({ clickable, isSelected, theme }) => + clickable + ? ` + cursor: pointer; + &:hover { + background-color: ${ + isSelected + ? color(theme.color.success['900']) + .alpha(theme.darkMode ? 0.15 : 0.05) + .string() + : theme.color.surface + }; + } + ` + : ``}; + &.dragging { + transition: 0ms; + background-color: ${({ theme }) => theme.color.elevatedSurface}; + border: 0; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15), 0 16px 32px rgba(0, 0, 0, 0.15); + } + & * { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } +`; +const TableCell = styled.div` + font-size: ${({ rowSize }) => (rowSize === 'xs' ? `0.75rem` : `0.875rem`)}; + padding: 0 0.5rem; + color: ${({ theme }) => theme.color.mediumEmphasis}; + display: flex; + flex-direction: column; + justify-content: center; + ${({ width }) => (width ? `width: ${width};` : ``)} + ${({ width }) => (width === 'auto' ? `flex: 1;` : ``)} + + ${({ onlyShowOnRowHover }) => + onlyShowOnRowHover + ? ` + opacity: 0; + transition: opacity 200ms; + ${TRow}:hover & { + opacity: 1; + } + ` + : ``} +`; +const SortIcon = styled(Icon)` + margin-left: 0.5rem; + vertical-align: middle; + transform: rotate(${({ desc }) => (desc ? 0 : -180)}deg) + scale(${({ isSorted }) => (isSorted ? 1 : 0)}) translateY(${({ desc }) => (desc ? '-' : '')}1px); + transition: 200ms; +`; +SortIcon.defaultProps = { + name: 'chevron-down', + size: 'sm', +}; + +const SelectToggleWrap = styled.label` + display: block; + position: relative; + width: 1.25rem; + height: 1.25rem; + box-shadow: inset 0 0 0 1.5px + ${({ checked, theme }) => + color( + theme.darkMode + ? theme.color.highEmphasis + : checked + ? theme.color.success[1000] + : theme.color.lowEmphasis, + ) + .alpha(checked ? 1 : 0.75) + .string()}; + border-radius: 0.75rem; + background-color: ${({ checked, theme }) => + checked ? theme.color.success['900'] : `rgba(0, 0, 0, 0.05)`}; + transition: 150ms; + cursor: pointer; + color: white; + &:hover { + background-color: ${({ checked, theme }) => + checked ? theme.color.success['900'] : `rgba(0, 0, 0, 0.1)`}; + box-shadow: inset 0 0 0 2px + ${({ checked, theme }) => + color( + theme.darkMode + ? theme.color.highEmphasis + : checked + ? theme.color.success[1000] + : theme.color.lowEmphasis, + ) + .alpha(checked ? 1 : 0.75) + .string()}; + } + & > svg { + position: absolute; + top: 50%; + left: 50%; + transition: 150ms; + transform: translate(-50%, -50%) scale(${({ checked }) => (checked ? 1 : 0)}); + stroke-width: 3px; + } +`; +const SelectIcon = styled(Icon)``; +SelectIcon.defaultProps = { + name: 'check', + size: 'sm', +}; + +const TableCellInside = styled.div``; + +const SelectToggle = ({ id, onClick, checked, indeterminate, ...props }) => { + return ( + { + console.log(props); + onClick(e); + }} + checked={checked} + indeterminate={indeterminate} + htmlFor={id} + > + + + + ); +}; + +const TableRow = sortableElement(({ children, ...props }) => {children}); +const TableBody = sortableContainer(({ children }) => {children}); + +const Table = ({ + className, + columns, + data, + selectable, + onSelect, + selected, + renderMenu, + onClick, + draggable, + rowSize, +}) => { + const [sortedData, setSortedData] = useState(data); + const { + getTableProps, + getTableBodyProps, + headerGroups, + rows, + prepareRow, + selectedFlatRows, + state: { selectedRowIds }, + } = useTable( + { + columns, + data: sortedData, + initialState: { + selectedRowIds: data.reduce( + (acc, row, index) => ({ ...acc, [index]: selected?.includes(row.id) }), + {}, + ), + }, + }, + useSortBy, + useFlexLayout, + useRowSelect, + hooks => { + hooks.visibleColumns.push(columns => [ + ...(selectable + ? [ + { + id: 'selection', + // The header can use the table's getToggleAllRowsSelectedProps method + // to render a checkbox + Header({ getToggleAllRowsSelectedProps }) { + return ( +
+ +
+ ); + }, + // The cell can use the individual row's getToggleRowSelectedProps method + // to the render a checkbox + Cell({ row }) { + return ( +
+ e.stopPropagation()} + id={row.id} + /> +
+ ); + }, + width: '36px', + }, + ] + : []), + ...columns, + ...(renderMenu + ? [ + { + id: 'rowMenu', + onlyShowOnRowHover: true, + width: rowSize === 'xs' ? '24px' : '48px', + Cell({ row: { original: rowData }, onMenuToggle }) { + const [anchorEl, setAnchorEl] = useState(); + + return ( + <> + { + e.stopPropagation(); + setAnchorEl(e.currentTarget); + onMenuToggle(true); + }} + /> + {renderMenu({ + anchorEl, + closeMenu: () => { + setAnchorEl(null); + onMenuToggle(false); + }, + rowData, + })} + + ); + }, + }, + ] + : []), + ]); + }, + ); + + useEffect(() => setSortedData(data), [data]); + useEffect(() => console.log({ selectedRowIds }), [selectedRowIds]); + + useEffect(() => onSelect(selectedFlatRows.map(row => row.original.id)), [selectedFlatRows]); + + const handleDrop = ({ oldIndex, newIndex }) => + setSortedData( + arrayMove( + rows.map(row => row.original), + oldIndex, + newIndex, + ), + ); + + const RenderRow = useCallback( + ({ index, style }) => { + const row = rows[index]; + + prepareRow(row); + + return ( + row.toggleRowSelected() + : onClick + ? () => onClick(row.original) + : null + } + disabled={!draggable} + isSelected={row.isSelected} + style={style} + rowSize={rowSize} + > + {row.cells.map((cell, cellIndex) => { + const [menuOpen, setMenuOpen] = useState(); + + return ( + + + {cell.render('Cell', { onMenuToggle: open => setMenuOpen(open) })} + + + ); + })} + + ); + }, + [prepareRow, rows], + ); + + // Render the UI for your table + return ( + + + {headerGroups.map((headerGroup, headerGroupIndex) => ( + + {headerGroup.headers.map((column, headerIndex) => { + return ( + + {column.render('Header')} + {column.canSort && ( + + )} + + ); + })} + + ))} + + + + {({ height, width }) => ( + data[index].id} + > + {RenderRow} + + )} + + + + ); +}; + +export default Table; diff --git a/packages/decap-cms-ui-4/src/Table/index.js b/packages/decap-cms-ui-4/src/Table/index.js new file mode 100644 index 000000000000..ae769447ae5c --- /dev/null +++ b/packages/decap-cms-ui-4/src/Table/index.js @@ -0,0 +1 @@ +export { default } from './Table'; diff --git a/packages/decap-cms-ui-4/src/Table/story.jsx b/packages/decap-cms-ui-4/src/Table/story.jsx new file mode 100644 index 000000000000..4d012f7f02a2 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Table/story.jsx @@ -0,0 +1,258 @@ +import React, { useState } from 'react'; +import styled from '@emotion/styled'; +import { withKnobs, boolean, select } from '@storybook/addon-knobs'; +import getMockData from '../utils/getMockData'; + +import Table from '.'; +import Icon from '../Icon'; +import { Menu, MenuItem } from '../Menu'; +import { Tag } from '../Tag'; + +const Title = styled.div` + color: ${({ theme }) => theme.color.highEmphasis}; + font-weight: bold; +`; +const Subtitle = styled.div` + font-size: 12px; +`; + +export default { + title: 'Components/Table', + decorators: [withKnobs], +}; + +const Wrap = styled.div` + width: 100%; + height: 100%; + background-color: ${({ theme }) => theme.color.background}; +`; + +const FeaturedImage = styled.div` + background-image: url(${({ srcUrl }) => srcUrl}); + background-size: cover; + background-position: center center; + background-repeat: no-repeat; + width: ${({ size }) => + size === 'xs' ? 1.5 : size === 'sm' ? 2 : size === 'lg' ? 3 : size === 'xl' ? 3.5 : 2.5}rem; + height: ${({ size }) => + size === 'xs' ? 1.5 : size === 'sm' ? 2 : size === 'lg' ? 3 : size === 'xl' ? 3.5 : 2.5}rem; + border-radius: 6px; +`; +const FeaturedIcon = styled(Icon)` + stroke: none; + fill: #ffc762; +`; +FeaturedIcon.defaultProps = { + size: 'sm', + name: 'star', +}; + +const mockData = getMockData('post', 500); + +export const _Table = () => { + const onClick = boolean('onClick', true); + const draggable = boolean('draggable', true); + const selectable = boolean('selectable', true); + const rowSize = select( + 'size', + { xs: 'xs', sm: 'sm', 'md (default)': null, lg: 'lg', xl: 'xl' }, + null, + ); + + const columns = React.useMemo( + () => [ + { + id: 'featured', + Cell({ row: { original: rowData } }) { + return <>{rowData.featured && }; + }, + width: '32px', + }, + { + id: 'featuredImage', + Cell({ row: { original: rowData } }) { + return ; + }, + width: `${((rowSize === 'xs' + ? 1.5 + : rowSize === 'sm' + ? 2 + : rowSize === 'lg' + ? 3 + : rowSize === 'xl' + ? 3.5 + : 2.5) + + 1) * + 16}px`, + }, + { + Header: 'Title', + accessor: 'title', + Cell({ row: { original: rowData } }) { + return ( + <> + {rowData.title} + {rowSize !== 'xs' && rowSize !== 'sm' && {rowData.description}} + + ); + }, + width: 'auto', + }, + { + Header: 'Category', + accessor: 'category', + width: '10%', + }, + { + Header: 'Status', + accessor: 'status', + width: '10%', + Cell({ row: { original: rowData } }) { + const [menuAnchorEl, setMenuAnchorEl] = useState(); + const [status, setStatus] = useState(rowData.status); + + return ( + <> + { + e.stopPropagation(); + setMenuAnchorEl(e.currentTarget); + }} + hasMenu + color={ + status === 'Published' + ? 'turquoise' + : status === 'In Review' + ? 'yellow' + : status === 'Draft' + ? 'pink' + : null + } + > + {status} + + { + e.stopPropagation(); + setMenuAnchorEl(null); + }} + > + { + e.stopPropagation(); + setStatus('Draft'); + setMenuAnchorEl(null); + }} + > + Draft + + { + e.stopPropagation(); + setStatus('In Review'); + setMenuAnchorEl(null); + }} + > + In Review + + { + e.stopPropagation(); + setStatus('Published'); + setMenuAnchorEl(null); + }} + > + Published + + + + ); + }, + }, + { + Header: 'Date Modified', + accessor: 'dateModified', + width: '15%', + }, + { + Header: 'Date Created', + accessor: 'dateCreated', + width: '15%', + }, + { + Header: 'Author', + accessor: 'author', + width: '10%', + }, + ], + [rowSize], + ); + + return ( + + console.log({ selected })} + onClick={onClick ? rowData => alert(`You just clicked table row ${rowData.id}.`) : null} + renderMenu={({ rowData, anchorEl, closeMenu }) => ( + + { + e.stopPropagation(); + console.log(rowData); + alert(`Editing post ${rowData.id}.`); + closeMenu(); + }} + > + Edit + + { + e.stopPropagation(); + alert(`Duplicating post ${rowData.id}.`); + closeMenu(); + }} + > + Duplicate + + { + e.stopPropagation(); + alert(`Deleting post ${rowData.id}.`); + closeMenu(); + }} + > + Delete + + + )} + /> + + ); +}; + +_Table.story = { + name: 'Table', +}; diff --git a/packages/decap-cms-ui-4/src/Tag/Tag.jsx b/packages/decap-cms-ui-4/src/Tag/Tag.jsx new file mode 100644 index 000000000000..365ad66a2cfb --- /dev/null +++ b/packages/decap-cms-ui-4/src/Tag/Tag.jsx @@ -0,0 +1,90 @@ +import React from 'react'; +import styled from '@emotion/styled'; +import Color from 'color'; + +import TagGroup from './TagGroup'; +import Icon from '../Icon'; + +const TagWrap = styled.div` + display: inline-flex; + line-height: 1.25rem; + background-color: ${({ color, theme }) => + Color(theme.color[color][color === 'neutral' ? 600 : 900]) + .alpha(0.15) + .string()}; + box-shadow: inset 0 0 0 1.5px + ${({ color, theme }) => theme.color[color][color === 'neutral' ? 600 : 900]}; + color: ${({ color, theme }) => theme.color[color][color === 'neutral' ? 600 : 900]}; + font-size: 0.75rem; + font-weight: bold; + padding: 0 0.375rem; + border-radius: 0.25rem; + word-wrap: nowrap; + justify-content: center; + align-items: center; + transition: 200ms; + ${TagGroup} & { + margin: 4px; + } + ${({ onClick, color, theme }) => + onClick + ? ` + cursor: pointer; + &:hover { + background-color: ${Color(theme.color[color][color === 'neutral' ? 600 : 900]) + .alpha(theme.darkMode ? 0.33 : 0.05) + .string()}; + } + &:active { + background-color: ${Color(theme.color[color][color === 'neutral' ? 600 : 900]) + .alpha(theme.darkMode ? 0.05 : 0.33) + .string()}; + } + ` + : ``} +`; +const Caret = styled.div` + display: inline-block; + vertical-align: middle; + width: 0; + height: 0; + border-left: 3px solid transparent; + border-right: 3px solid transparent; + border-top: 4px solid; + margin-left: 0.25rem; +`; +const DeleteWrap = styled.div` + display: flex; + justify-content: center; + align-items: center; +`; +const DeleteButton = styled(Icon)` + stroke-width: 3; + margin-left: 0.125rem; + margin-right: -0.125rem; + cursor: pointer; +`; +DeleteButton.defaultProps = { name: 'x', size: 'xs' }; + +const Tag = ({ color, children, onClick, hasMenu, onDelete }) => ( + + {children} + {hasMenu && } + {onDelete && ( + { + e.stopPropagation(); + onDelete(e); + }} + > + + + )} + +); + +Tag.defaultProps = { + color: 'neutral', +}; + +export default Tag; diff --git a/packages/decap-cms-ui-4/src/Tag/TagGroup.jsx b/packages/decap-cms-ui-4/src/Tag/TagGroup.jsx new file mode 100644 index 000000000000..95c03961a8da --- /dev/null +++ b/packages/decap-cms-ui-4/src/Tag/TagGroup.jsx @@ -0,0 +1,14 @@ +import styled from '@emotion/styled'; + +const TagGroup = styled.div` + margin: -4px; + display: inline-flex; + align-items: ${({ direction }) => (direction === 'vertical' ? 'stretch' : 'center')}; + flex-wrap: wrap; + ${({ direction }) => (direction === 'vertical' ? `flex-direction: column;` : ``)} + & > * { + margin: 4px; + } +`; + +export default TagGroup; diff --git a/packages/decap-cms-ui-4/src/Tag/index.js b/packages/decap-cms-ui-4/src/Tag/index.js new file mode 100644 index 000000000000..208b95dad077 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Tag/index.js @@ -0,0 +1,2 @@ +export { default as Tag } from './Tag'; +export { default as TagGroup } from './TagGroup'; diff --git a/packages/decap-cms-ui-4/src/Tag/story.jsx b/packages/decap-cms-ui-4/src/Tag/story.jsx new file mode 100644 index 000000000000..e696da8d7789 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Tag/story.jsx @@ -0,0 +1,110 @@ +import React from 'react'; +import { withKnobs, boolean, select, text } from '@storybook/addon-knobs'; + +import { Tag, TagGroup } from '.'; +import color from '../utils/color'; + +export default { + title: 'Components/Tag', + decorators: [withKnobs], +}; + +export const _Tag = () => { + const children = text('children', 'Published'); + const availableColors = Object.keys(color).reduce( + (acc, color) => ({ ...acc, [color]: color }), + {}, + ); + const colorKnob = select('color', availableColors, 'neutral'); + const onClick = boolean('onClick', false); + const onDelete = boolean('onDelete', false); + const hasMenu = boolean('hasMenu', false); + + return ( + + alert('Tag clicked.') : null} + hasMenu={hasMenu} + onDelete={onDelete ? () => alert('Deleted tag') : null} + > + {children} + + + ); +}; + +export const _TagGroup = () => { + const onClick = boolean('onClick', false); + const onDelete = boolean('onDelete', false); + const hasMenu = boolean('hasMenu', false); + const size = select('size', { sm: 'sm', 'md (default)': null, lg: 'lg' }, null); + + return ( + + alert('Tag clicked.') : null} + hasMenu={hasMenu} + onDelete={onDelete ? () => alert('Deleted tag') : null} + > + Apple + + alert('Tag clicked.') : null} + hasMenu={hasMenu} + onDelete={onDelete ? () => alert('Deleted tag') : null} + > + Banana + + alert('Tag clicked.') : null} + hasMenu={hasMenu} + onDelete={onDelete ? () => alert('Deleted tag') : null} + > + Orange + + alert('Tag clicked.') : null} + hasMenu={hasMenu} + onDelete={onDelete ? () => alert('Deleted tag') : null} + > + Cherry + + alert('Tag clicked.') : null} + hasMenu={hasMenu} + onDelete={onDelete ? () => alert('Deleted tag') : null} + > + Strawberry + + alert('Tag clicked.') : null} + hasMenu={hasMenu} + onDelete={onDelete ? () => alert('Deleted tag') : null} + > + Grape + + alert('Tag clicked.') : null} + hasMenu={hasMenu} + onDelete={onDelete ? () => alert('Deleted tag') : null} + > + Blueberry + + + ); +}; diff --git a/packages/decap-cms-ui-4/src/TestSandbox.jsx b/packages/decap-cms-ui-4/src/TestSandbox.jsx new file mode 100644 index 000000000000..64cea7d36259 --- /dev/null +++ b/packages/decap-cms-ui-4/src/TestSandbox.jsx @@ -0,0 +1,241 @@ +import React, { useState } from 'react'; +import styled from '@emotion/styled'; + +import Button from './Button'; +import ButtonGroup from './ButtonGroup'; +import Dialog from './Dialog'; +import { toast } from './Toast'; + +const StyledPre = styled.pre` + background-color: ${({ theme }) => theme.color.neutral['50']}; + border-radius: 8px; + max-width: 800px; + margin: 0 auto; +`; +const CenterWrap = styled.div` + padding: 1rem; +`; +const CenterInside = styled.div` + max-width: 800px; + margin: 12px auto; +`; +const Row = styled.div` + padding: 12px 0; +`; + +const TestSandbox = ({ data }) => { + const [defaultDialogOpen, setDefaultDialogOpen] = useState(false); + const [successDialogOpen, setSuccessDialogOpen] = useState(false); + const [dangerDialogOpen, setDangerDialogOpen] = useState(false); + const [defaultSmallDialogOpen, setDefaultSmallDialogOpen] = useState(false); + const [successSmallDialogOpen, setSuccessSmallDialogOpen] = useState(false); + const [dangerSmallDialogOpen, setDangerSmallDialogOpen] = useState(false); + + return ( + + +
Click these buttons to see toast.
+ + + + + + + +
Click these buttons to see small toast.
+ + + + + + + +
Click these buttons to see dialogs.
+ + + + + + + setDefaultDialogOpen(false)} + actions={ + + + + + } + > + Help me, Obi-Wan Kenobi. You’re my only hope. + + setSuccessDialogOpen(false)} + actions={ + + + + + } + > + The Force will be with you. Always. + + setDangerDialogOpen(false)} + actions={ + + + + + } + > + Fear is the path to the dark side. Fear leads to anger; anger leads to hate; hate leads + to suffering. I sense much fear in you. + + +
Click these buttons to see small dialogs.
+ + + + + + + setDefaultSmallDialogOpen(false)} + actions={ + + + + + } + /> + setSuccessSmallDialogOpen(false)} + actions={ + + + + + } + /> + setDangerSmallDialogOpen(false)} + actions={ + + + + + } + /> + + {JSON.stringify(data, null, 2)} +
+
+ ); +}; + +export default TestSandbox; diff --git a/packages/decap-cms-ui-4/src/Thumbnail/Thumbnail.jsx b/packages/decap-cms-ui-4/src/Thumbnail/Thumbnail.jsx new file mode 100644 index 000000000000..460ceff4b643 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Thumbnail/Thumbnail.jsx @@ -0,0 +1,364 @@ +import React, { useState, useEffect } from 'react'; +import styled from '@emotion/styled'; +import { withTheme } from 'emotion-theming'; + +import Card from '../Card'; +import Icon from '../Icon'; + +const ThumbnailWrap = styled(Card)` + overflow: hidden; + position: relative; + display: flex; + ${({ horizontal }) => (horizontal ? `` : `flex-direction: column;`)} + ${({ selected, selectable, theme }) => + selectable + ? ` + &:before { + content: ''; + display: block; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + background-color: ${theme.color.success['900']}; + opacity: ${selected ? 0.1 : 0}; + border-radius: 6px; + pointer-events: none; + transition: 200ms; + } + &:after { + content: ''; + display: block; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + box-shadow: inset 0 0 0 ${selected ? 2 : 0}px ${theme.color.success['900']}; + border-radius: 6px; + pointer-events: none; + transition: 200ms; + } + ` + : ``} + ${({ clickable, theme }) => + clickable + ? ` + cursor: pointer; + transition: 200ms; + z-index: 0; + + &:hover { + z-index: 10; + box-shadow: ${theme.shadow({ size: 'lg', theme })}; + ${theme.darkMode ? `background-color: ${theme.color.surfaceHighlight};` : ``} + } + ` + : ``} +`; +const Content = styled.div` + padding: 1rem; + display: flex; + flex-direction: column; + justify-content: center; + flex: 1; + position: relative; + ${({ selectable, hasPreview, horizontal }) => + `margin-${horizontal ? 'left' : 'top'}: ${!hasPreview && selectable ? `2.5rem` : 0};`} + transition: 200ms; + ${({ featured }) => + featured + ? ` + & > *:last-child { + padding-right: 1.5rem; + } + ` + : ``} +`; +const PreviewWrap = styled.div` + position: relative; + ${({ previewBgColor }) => (previewBgColor ? `background-color: ${previewBgColor};` : ``)} + overflow: hidden; + display: flex; + width: ${({ horizontal }) => (horizontal ? `33.333%;` : `100%`)}; +`; +const Preview = styled.div` + ${({ horizontal, previewAspectRatio }) => + horizontal + ? ` + display: flex; + width: 100%; + ` + : ` + ${ + previewAspectRatio + ? typeof previewAspectRatio === 'string' + ? ` + padding-top: ${(previewAspectRatio.split(':')[1] / previewAspectRatio.split(':')[0]) * + 100}%; + ` + : Array.isArray(previewAspectRatio) + ? ` + padding-top: ${(previewAspectRatio.split[1] / previewAspectRatio.split[0]) * 100}%; + ` + : `` + : `` + } + width: 100%; + ${previewAspectRatio ? `height: 0;` : ``} + `} + + ${({ previewImgSrc }) => + previewImgSrc + ? `background-image: url(${previewImgSrc});` + : ``} + background-position: center center; + background-size: cover; + background-repeat: no-repeat; + ${({ previewImgSrc, previewImgLoaded, previewImgOpacity }) => + previewImgSrc + ? `opacity: ${ + previewImgLoaded + ? previewImgOpacity !== null && + previewImgOpacity !== undefined && + previewImgOpacity !== '' + ? previewImgOpacity + : 1 + : 0 + };` + : ``} + ${({ selected, selectable }) => + selectable ? `transform: scale(${selected ? 1.1 : 1.01});` : ``} + transition: 200ms; +`; +const PreviewText = styled.div` + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + display: flex; + justify-content: center; + align-items: center; + color: ${({ theme }) => theme.color.highEmphasis}; + font-size: 2rem; + font-weight: bold; +`; +const Supertitle = styled.div` + color: ${({ theme }) => theme.color.lowEmphasis}; + font-size: 0.625rem; + font-weight: bold; + text-transform: uppercase; + margin-bottom: 6px; + &:last-child { + margin: 0; + } + ${({ maxLines }) => + maxLines + ? ` + display: -webkit-box; + -webkit-line-clamp: ${maxLines}; + -webkit-box-orient: vertical; + overflow: hidden; + ` + : ``} +`; +const Title = styled.div` + font-size: 0.875rem; + font-weight: bold; + color: ${({ theme }) => theme.color.highEmphasis}; + margin-bottom: 6px; + &:last-child { + margin: 0; + } + ${({ maxLines }) => + maxLines + ? ` + display: -webkit-box; + -webkit-line-clamp: ${maxLines}; + -webkit-box-orient: vertical; + overflow: hidden; + ` + : ``} +`; +const Description = styled.div` + font-size: 0.75rem; + color: ${({ theme }) => theme.color.mediumEmphasis}; + margin-bottom: 6px; + &:last-child { + margin: 0; + } + ${({ maxLines }) => + maxLines + ? ` + display: -webkit-box; + -webkit-line-clamp: ${maxLines}; + -webkit-box-orient: vertical; + overflow: hidden; + ` + : ``} +`; +const Subtitle = styled.div` + font-size: 0.625rem; + color: ${({ theme }) => theme.color.lowEmphasis}; + padding-top: 6px; + ${({ maxLines }) => + maxLines + ? ` + display: -webkit-box; + -webkit-line-clamp: ${maxLines}; + -webkit-box-orient: vertical; + overflow: hidden; + margin-top: auto; + ` + : ` + display: flex; + align-items: flex-end; + flex: 1; + `} +`; +const FeaturedIcon = styled(Icon)` + stroke: none; + fill: #ffc762; + position: absolute; + bottom: 0.875rem; + right: 0.875rem; +`; +FeaturedIcon.defaultProps = { + size: 'sm', + name: 'star', +}; +const SelectToggle = styled.div` + width: 1.5rem; + height: 1.5rem; + position: absolute; + top: 0.75rem; + left: 0.75rem; + box-shadow: inset 0 0 0 1.5px rgba(255, 255, 255, ${({ selected }) => (selected ? 1 : 0.75)}); + border-radius: 0.75rem; + background-color: ${({ selected, theme }) => + selected ? theme.color.success['900'] : `rgba(0, 0, 0, 0.1)`}; + transition: 200ms; + cursor: pointer; + color: white; + &:hover { + box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 1); + } + & > svg { + position: absolute; + top: 50%; + left: 50%; + transition: 200ms; + transform: translate(-50%, -50%) scale(${({ selected }) => (selected ? 1 : 0)}); + stroke-width: 3px; + } +`; + +const SelectIcon = styled(Icon)``; +SelectIcon.defaultProps = { + name: 'check', + size: 'sm', +}; + +const Thumbnail = ({ + previewImgSrc, + previewImgOpacity, + previewAspectRatio, + previewBgColor, + previewText, + supertitle, + title, + description, + subtitle, + featured, + horizontal, + selectable, + selected, + onSelect, + onClick, + supertitleMaxLines, + titleMaxLines, + descriptionMaxLines, + subtitleMaxLines, + theme, + ...props +}) => { + const isCached = src => { + const img = new Image(); + img.src = src; + + return img.complete; + }; + const [previewImgLoaded, setPreviewImageLoaded] = useState( + previewImgSrc && isCached(previewImgSrc), + ); + + useEffect(() => { + if (previewImgSrc) { + const img = new Image(); + img.onload = () => setPreviewImageLoaded(true); + img.src = previewImgSrc; + } + }, []); + + return ( + + {(previewImgSrc || previewText) && ( + + + {!previewAspectRatio && } + + {previewText && {previewText}} + + )} + {(supertitle || title || description || subtitle) && ( + + {supertitle && {supertitle}} + {title && {title}} + {description && {description}} + {subtitle && {subtitle}} + + )} + {featured && } + {selectable && ( + + + + )} + + ); +}; + +Thumbnail.defaultProps = { + previewAspectRatio: '16:9', + supertitleMaxLines: 1, + titleMaxLines: 3, + descriptionMaxLines: 3, + subtitleMaxLines: 1, +}; + +export default withTheme(Thumbnail); diff --git a/packages/decap-cms-ui-4/src/Thumbnail/ThumbnailGrid.jsx b/packages/decap-cms-ui-4/src/Thumbnail/ThumbnailGrid.jsx new file mode 100644 index 000000000000..b93a64eb004c --- /dev/null +++ b/packages/decap-cms-ui-4/src/Thumbnail/ThumbnailGrid.jsx @@ -0,0 +1,20 @@ +import styled from '@emotion/styled'; + +const ThumbnailGrid = styled.div` + display: grid; + grid-gap: 1rem; + width: 100%; + grid-template-columns: repeat( + auto-fill, + minmax(min(${({ horizontal }) => (horizontal ? 24 : 16)}rem, 100%), 1fr) + ); + ${({ theme }) => theme.responsive.mediaQueryDown('xs')} { + grid-gap: 0.5rem; + grid-template-columns: repeat( + auto-fill, + minmax(min(${({ horizontal }) => (horizontal ? 24 : 8)}rem, 100%), 1fr) + ); + } +`; + +export default ThumbnailGrid; diff --git a/packages/decap-cms-ui-4/src/Thumbnail/index.js b/packages/decap-cms-ui-4/src/Thumbnail/index.js new file mode 100644 index 000000000000..2d4b69df4213 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Thumbnail/index.js @@ -0,0 +1,2 @@ +export { default } from './Thumbnail'; +export { default as ThumbnailGrid } from './ThumbnailGrid'; diff --git a/packages/decap-cms-ui-4/src/Thumbnail/mockData.js b/packages/decap-cms-ui-4/src/Thumbnail/mockData.js new file mode 100644 index 000000000000..893b3a299ced --- /dev/null +++ b/packages/decap-cms-ui-4/src/Thumbnail/mockData.js @@ -0,0 +1,1410 @@ +const mockData = [ + { + id: 1, + img: + 'https://images.unsplash.com/photo-1584551882802-ca081b505b49?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Draft', + title: 'enim blandit mi in porttitor pede justo eu massa donec dapibus duis at velit', + description: 'ut massa quis augue luctus tincidunt nulla mollis molestie lorem quisque ut', + subtitle: 'Nadya Cowlishaw', + featured: true, + }, + { + id: 2, + img: + 'https://images.unsplash.com/photo-1518887668165-8fa91a9178be?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Published', + title: + 'elit proin interdum mauris non ligula pellentesque ultrices phasellus id sapien in sapien iaculis', + description: + 'in sagittis dui vel nisl duis ac nibh fusce lacus purus aliquet at feugiat non pretium quis lectus', + subtitle: 'Olag Baumadier', + featured: true, + }, + { + id: 3, + img: + 'https://images.unsplash.com/photo-1433838552652-f9a46b332c40?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Draft', + title: 'nunc commodo placerat praesent blandit nam nulla integer pede justo lacinia eget', + description: + 'luctus rutrum nulla tellus in sagittis dui vel nisl duis ac nibh fusce lacus purus', + subtitle: 'Boote Henlon', + featured: true, + }, + { + id: 4, + img: + 'https://images.unsplash.com/photo-1507608443039-bfde4fbcd142?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'In Review', + title: + 'posuere cubilia curae mauris viverra diam vitae quam suspendisse potenti nullam porttitor lacus at', + description: 'congue eget semper rutrum nulla nunc purus phasellus in felis donec semper', + subtitle: 'Siouxie Crassweller', + featured: true, + }, + { + id: 5, + img: + 'https://images.unsplash.com/photo-1521381802788-d5900db802dc?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Draft', + title: 'aliquet at feugiat non pretium quis lectus suspendisse potenti in eleifend quam a odio', + description: + 'id luctus nec molestie sed justo pellentesque viverra pede ac diam cras pellentesque volutpat dui maecenas tristique est et', + subtitle: 'Urson Scholer', + featured: false, + }, + { + id: 6, + img: + 'https://images.unsplash.com/photo-1452711932549-e7ea7f129399?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'In Review', + title: 'potenti in eleifend quam a odio in hac habitasse platea dictumst maecenas ut massa', + description: + 'sodales sed tincidunt eu felis fusce posuere felis sed lacus morbi sem mauris laoreet', + subtitle: 'Guinevere Noli', + featured: false, + }, + { + id: 7, + img: + 'https://images.unsplash.com/photo-1516496636080-14fb876e029d?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'In Review', + title: 'imperdiet nullam orci pede venenatis non sodales sed tincidunt eu felis fusce', + description: + 'justo morbi ut odio cras mi pede malesuada in imperdiet et commodo vulputate justo in', + subtitle: 'Celestyna Speke', + featured: true, + }, + { + id: 8, + img: + 'https://images.unsplash.com/photo-1468476775582-6bede20f356f?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Published', + title: + 'nibh in quis justo maecenas rhoncus aliquam lacus morbi quis tortor id nulla ultrices aliquet', + description: + 'et ultrices posuere cubilia curae mauris viverra diam vitae quam suspendisse potenti nullam porttitor lacus at turpis donec', + subtitle: 'Janean Checketts', + featured: false, + }, + { + id: 9, + img: + 'https://images.unsplash.com/photo-1517462035531-76bc910a6903?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Draft', + title: 'imperdiet nullam orci pede venenatis non sodales sed tincidunt eu felis fusce', + description: + 'dolor sit amet consectetuer adipiscing elit proin risus praesent lectus vestibulum quam', + subtitle: 'Cherrita Attril', + featured: true, + }, + { + id: 10, + img: + 'https://images.unsplash.com/photo-1577701122197-c9607038bd90?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'In Review', + title: 'iaculis justo in hac habitasse platea dictumst etiam faucibus cursus urna', + description: 'in magna bibendum imperdiet nullam orci pede venenatis non sodales', + subtitle: 'Philipa Crews', + featured: false, + }, + { + id: 11, + img: + 'https://images.unsplash.com/photo-1421930866250-aa0594cea05c?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'In Review', + title: 'dis parturient montes nascetur ridiculus mus vivamus vestibulum sagittis sapien cum', + description: + 'ac nibh fusce lacus purus aliquet at feugiat non pretium quis lectus suspendisse potenti in eleifend quam a odio', + subtitle: 'Petronella Willford', + featured: true, + }, + { + id: 12, + img: + 'https://images.unsplash.com/photo-1518978288375-f36cefcc992e?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Draft', + title: + 'quam fringilla rhoncus mauris enim leo rhoncus sed vestibulum sit amet cursus id turpis', + description: 'praesent id massa id nisl venenatis lacinia aenean sit amet justo morbi ut', + subtitle: 'Deana Alphege', + featured: false, + }, + { + id: 13, + img: + 'https://images.unsplash.com/photo-1511300636408-a63a89df3482?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'In Review', + title: 'nisl ut volutpat sapien arcu sed augue aliquam erat volutpat in congue', + description: + 'luctus et ultrices posuere cubilia curae donec pharetra magna vestibulum aliquet ultrices erat tortor sollicitudin', + subtitle: 'Dory Blackden', + featured: true, + }, + { + id: 14, + img: + 'https://images.unsplash.com/photo-1489573280374-2e193c63726c?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'In Review', + title: 'nulla nisl nunc nisl duis bibendum felis sed interdum venenatis', + description: + 'aliquet maecenas leo odio condimentum id luctus nec molestie sed justo pellentesque viverra pede ac diam cras pellentesque volutpat dui', + subtitle: 'Diane-marie Awmack', + featured: false, + }, + { + id: 15, + img: + 'https://images.unsplash.com/photo-1516054237813-0df813b7f496?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'In Review', + title: + 'lectus suspendisse potenti in eleifend quam a odio in hac habitasse platea dictumst maecenas', + description: 'nulla justo aliquam quis turpis eget elit sodales scelerisque mauris', + subtitle: 'Cal Cape', + featured: false, + }, + { + id: 16, + img: + 'https://images.unsplash.com/photo-1515445702422-3a80ccfdb236?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'In Review', + title: + 'habitasse platea dictumst morbi vestibulum velit id pretium iaculis diam erat fermentum justo nec condimentum', + description: 'vestibulum sit amet cursus id turpis integer aliquet massa id', + subtitle: 'Troy Scholer', + featured: false, + }, + { + id: 17, + img: + 'https://images.unsplash.com/photo-1542038278812-0703a871002a?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Published', + title: 'sagittis dui vel nisl duis ac nibh fusce lacus purus aliquet at feugiat', + description: + 'neque duis bibendum morbi non quam nec dui luctus rutrum nulla tellus in sagittis dui vel nisl duis', + subtitle: 'Analise Creeboe', + featured: true, + }, + { + id: 18, + img: + 'https://images.unsplash.com/photo-1522878129833-838a904a0e9e?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Draft', + title: 'ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae donec', + description: + 'mauris non ligula pellentesque ultrices phasellus id sapien in sapien iaculis congue vivamus metus arcu adipiscing molestie', + subtitle: 'Farra Masi', + featured: true, + }, + { + id: 19, + img: + 'https://images.unsplash.com/photo-1493306454986-c8877a09cbeb?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'In Review', + title: 'fusce congue diam id ornare imperdiet sapien urna pretium nisl ut volutpat sapien arcu', + description: 'tincidunt nulla mollis molestie lorem quisque ut erat curabitur gravida nisi', + subtitle: 'Zebulon Fenn', + featured: true, + }, + { + id: 20, + img: + 'https://images.unsplash.com/photo-1520066592498-348cf9b6374a?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Published', + title: 'tincidunt in leo maecenas pulvinar lobortis est phasellus sit amet erat nulla tempus', + description: 'nec euismod scelerisque quam turpis adipiscing lorem vitae mattis nibh', + subtitle: 'Silvio Schulze', + featured: true, + }, + { + id: 21, + img: + 'https://images.unsplash.com/photo-1556231636-6ffc1fea77bd?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Published', + title: 'nullam orci pede venenatis non sodales sed tincidunt eu felis fusce posuere felis', + description: + 'mus etiam vel augue vestibulum rutrum rutrum neque aenean auctor gravida sem praesent id massa id nisl venenatis lacinia', + subtitle: 'Faunie Lawling', + featured: false, + }, + { + id: 22, + img: + 'https://images.unsplash.com/photo-1543972752-18798f0e93a4?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Published', + title: + 'in hac habitasse platea dictumst morbi vestibulum velit id pretium iaculis diam erat fermentum justo', + description: + 'vestibulum ac est lacinia nisi venenatis tristique fusce congue diam id ornare imperdiet sapien urna pretium nisl ut volutpat sapien', + subtitle: 'Saundra Cribbin', + featured: true, + }, + { + id: 23, + img: + 'https://images.unsplash.com/photo-1504282706065-f5866e9cbeeb?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Published', + title: 'sed ante vivamus tortor duis mattis egestas metus aenean fermentum', + description: + 'luctus et ultrices posuere cubilia curae donec pharetra magna vestibulum aliquet ultrices erat tortor sollicitudin mi sit amet', + subtitle: 'Alejandro Jeandot', + featured: false, + }, + { + id: 24, + img: + 'https://images.unsplash.com/photo-1521062849558-8e32f69ba41d?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Published', + title: 'interdum in ante vestibulum ante ipsum primis in faucibus orci', + description: 'quisque arcu libero rutrum ac lobortis vel dapibus at diam nam tristique', + subtitle: 'Dodie Sercombe', + featured: true, + }, + { + id: 25, + img: + 'https://images.unsplash.com/uploads/14116941824817ba1f28e/78c8dff1?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'In Review', + title: + 'morbi vestibulum velit id pretium iaculis diam erat fermentum justo nec condimentum neque', + description: 'sapien varius ut blandit non interdum in ante vestibulum ante', + subtitle: 'Johnny MacKissack', + featured: false, + }, + { + id: 26, + img: + 'https://images.unsplash.com/photo-1498889444388-e67ea62c464b?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Published', + title: 'curae duis faucibus accumsan odio curabitur convallis duis consequat dui nec', + description: 'in leo maecenas pulvinar lobortis est phasellus sit amet erat nulla tempus', + subtitle: 'Dorree Sidnall', + featured: true, + }, + { + id: 27, + img: + 'https://images.unsplash.com/photo-1455816293708-e4223079f940?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Draft', + title: + 'posuere nonummy integer non velit donec diam neque vestibulum eget vulputate ut ultrices vel augue', + description: + 'accumsan felis ut at dolor quis odio consequat varius integer ac leo pellentesque ultrices mattis odio donec vitae', + subtitle: 'Hatty Fredi', + featured: false, + }, + { + id: 28, + img: + 'https://images.unsplash.com/photo-1508240782898-53ee4351d612?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'In Review', + title: 'sagittis dui vel nisl duis ac nibh fusce lacus purus aliquet at feugiat non', + description: 'amet nulla quisque arcu libero rutrum ac lobortis vel dapibus at', + subtitle: 'Elwin Myrie', + featured: false, + }, + { + id: 29, + img: + 'https://images.unsplash.com/photo-1498550744921-75f79806b8a7?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Published', + title: + 'curabitur at ipsum ac tellus semper interdum mauris ullamcorper purus sit amet nulla quisque', + description: + 'cras pellentesque volutpat dui maecenas tristique est et tempus semper est quam pharetra magna ac consequat metus sapien', + subtitle: 'Emera Berfoot', + featured: false, + }, + { + id: 30, + img: + 'https://images.unsplash.com/photo-1558981359-219d6364c9c8?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Published', + title: + 'amet sem fusce consequat nulla nisl nunc nisl duis bibendum felis sed interdum venenatis', + description: + 'varius ut blandit non interdum in ante vestibulum ante ipsum primis in faucibus orci', + subtitle: 'Dolly Tyt', + featured: false, + }, + { + id: 31, + img: + 'https://images.unsplash.com/photo-1584968153986-3f5fe523b044?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Draft', + title: 'ac nulla sed vel enim sit amet nunc viverra dapibus nulla suscipit', + description: + 'lacus morbi quis tortor id nulla ultrices aliquet maecenas leo odio condimentum id luctus nec', + subtitle: 'Crissy Cressy', + featured: false, + }, + { + id: 32, + img: + 'https://images.unsplash.com/photo-1583838051812-71898f2f7a22?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'In Review', + title: + 'lorem ipsum dolor sit amet consectetuer adipiscing elit proin risus praesent lectus vestibulum', + description: 'suscipit nulla elit ac nulla sed vel enim sit amet', + subtitle: 'Ellis Di Roberto', + featured: false, + }, + { + id: 33, + img: + 'https://images.unsplash.com/photo-1547153388-cb6959ce1a56?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Published', + title: + 'pellentesque eget nunc donec quis orci eget orci vehicula condimentum curabitur in libero ut', + description: 'arcu libero rutrum ac lobortis vel dapibus at diam nam', + subtitle: 'Amelia Brisbane', + featured: false, + }, + { + id: 34, + img: + 'https://images.unsplash.com/photo-1542878447-e2b6df2526fa?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Published', + title: 'vivamus vestibulum sagittis sapien cum sociis natoque penatibus et magnis dis', + description: + 'condimentum id luctus nec molestie sed justo pellentesque viverra pede ac diam cras pellentesque', + subtitle: 'Jorry Engelbrecht', + featured: false, + }, + { + id: 35, + img: + 'https://images.unsplash.com/photo-1516108317508-6788f6a160e4?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Draft', + title: 'sed accumsan felis ut at dolor quis odio consequat varius integer ac leo', + description: + 'nulla suspendisse potenti cras in purus eu magna vulputate luctus cum sociis natoque penatibus et magnis dis', + subtitle: 'Lewie Harniman', + featured: true, + }, + { + id: 36, + img: + 'https://images.unsplash.com/photo-1552599250-0b2c887b3745?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'In Review', + title: 'porttitor id consequat in consequat ut nulla sed accumsan felis ut at dolor quis', + description: + 'erat id mauris vulputate elementum nullam varius nulla facilisi cras non velit nec nisi vulputate nonummy maecenas tincidunt lacus', + subtitle: 'Aylmer Melmeth', + featured: true, + }, + { + id: 37, + img: + 'https://images.unsplash.com/photo-1531352294718-fb57e1b4e148?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Draft', + title: 'felis sed lacus morbi sem mauris laoreet ut rhoncus aliquet pulvinar', + description: 'pede venenatis non sodales sed tincidunt eu felis fusce posuere felis sed', + subtitle: 'Agathe Domerc', + featured: true, + }, + { + id: 38, + img: + 'https://images.unsplash.com/photo-1511135570219-bbad9a02f103?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'In Review', + title: + 'tempor convallis nulla neque libero convallis eget eleifend luctus ultricies eu nibh quisque', + description: + 'ligula in lacus curabitur at ipsum ac tellus semper interdum mauris ullamcorper purus sit amet', + subtitle: 'Neville Boldison', + featured: false, + }, + { + id: 39, + img: + 'https://images.unsplash.com/photo-1584936293040-90352818b0df?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Published', + title: 'aliquam quis turpis eget elit sodales scelerisque mauris sit amet eros', + description: + 'proin risus praesent lectus vestibulum quam sapien varius ut blandit non interdum in', + subtitle: 'Pearline Digance', + featured: false, + }, + { + id: 40, + img: + 'https://images.unsplash.com/photo-1448250735361-4db822114194?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'In Review', + title: 'leo odio condimentum id luctus nec molestie sed justo pellentesque', + description: + 'sapien quis libero nullam sit amet turpis elementum ligula vehicula consequat morbi a ipsum integer a', + subtitle: 'Keeley Graeser', + featured: false, + }, + { + id: 41, + img: + 'https://images.unsplash.com/photo-1584891844136-223372e207af?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Published', + title: 'vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere', + description: + 'primis in faucibus orci luctus et ultrices posuere cubilia curae duis faucibus accumsan odio curabitur convallis duis', + subtitle: 'Joey Irvin', + featured: true, + }, + { + id: 42, + img: + 'https://images.unsplash.com/photo-1584978881961-27af5fb6d7ac?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'In Review', + title: 'leo odio condimentum id luctus nec molestie sed justo pellentesque', + description: + 'leo maecenas pulvinar lobortis est phasellus sit amet erat nulla tempus vivamus in felis eu sapien cursus vestibulum', + subtitle: 'Bathsheba Orchart', + featured: false, + }, + { + id: 43, + img: + 'https://images.unsplash.com/photo-1584553249595-2f2d2c5b3812?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Published', + title: 'sit amet erat nulla tempus vivamus in felis eu sapien cursus vestibulum proin eu mi', + description: + 'vestibulum ac est lacinia nisi venenatis tristique fusce congue diam id ornare imperdiet sapien urna pretium nisl ut', + subtitle: 'Rand Mewburn', + featured: false, + }, + { + id: 44, + img: + 'https://images.unsplash.com/photo-1585042644206-9d9ae9811e37?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Published', + title: 'iaculis justo in hac habitasse platea dictumst etiam faucibus cursus', + description: + 'sagittis nam congue risus semper porta volutpat quam pede lobortis ligula sit amet eleifend pede libero quis orci nullam molestie', + subtitle: 'Fabe Quartermain', + featured: false, + }, + { + id: 45, + img: + 'https://images.unsplash.com/photo-1585067934141-ae65c82e7110?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Draft', + title: 'turpis enim blandit mi in porttitor pede justo eu massa donec dapibus duis at velit', + description: + 'in sapien iaculis congue vivamus metus arcu adipiscing molestie hendrerit at vulputate', + subtitle: 'Cele Fillery', + featured: false, + }, + { + id: 46, + img: + 'https://images.unsplash.com/photo-1585068294277-b408285aee4a?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Draft', + title: 'eget vulputate ut ultrices vel augue vestibulum ante ipsum primis', + description: + 'lacinia sapien quis libero nullam sit amet turpis elementum ligula vehicula consequat morbi a ipsum integer a nibh', + subtitle: 'Quintilla Ivanikov', + featured: false, + }, + { + id: 47, + img: + 'https://images.unsplash.com/photo-1584995907777-633637af2644?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Published', + title: 'in felis donec semper sapien a libero nam dui proin leo odio porttitor', + description: 'condimentum curabitur in libero ut massa volutpat convallis morbi odio odio', + subtitle: 'Augustine Burgoin', + featured: false, + }, + { + id: 48, + img: + 'https://images.unsplash.com/photo-1483651646696-c1b5fe39fc0e?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Published', + title: 'eros viverra eget congue eget semper rutrum nulla nunc purus', + description: + 'cursus vestibulum proin eu mi nulla ac enim in tempor turpis nec euismod scelerisque quam', + subtitle: 'Sandy Silverman', + featured: false, + }, + { + id: 49, + img: + 'https://images.unsplash.com/photo-1470246973918-29a93221c455?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Published', + title: + 'ipsum dolor sit amet consectetuer adipiscing elit proin interdum mauris non ligula pellentesque ultrices phasellus', + description: + 'interdum mauris non ligula pellentesque ultrices phasellus id sapien in sapien iaculis congue vivamus metus arcu adipiscing molestie hendrerit at', + subtitle: 'Gram Ilbert', + featured: true, + }, + { + id: 50, + img: + 'https://images.unsplash.com/photo-1484626753559-5fa3ea273ae8?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Draft', + title: 'luctus nec molestie sed justo pellentesque viverra pede ac diam cras', + description: + 'congue vivamus metus arcu adipiscing molestie hendrerit at vulputate vitae nisl aenean lectus pellentesque eget nunc', + subtitle: 'Guntar Websdale', + featured: false, + }, + { + id: 51, + img: + 'https://images.unsplash.com/photo-1558417991-1dc2ed5b006b?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Published', + title: 'tincidunt in leo maecenas pulvinar lobortis est phasellus sit amet erat nulla', + description: 'odio consequat varius integer ac leo pellentesque ultrices mattis odio', + subtitle: 'Aleda Eynon', + featured: true, + }, + { + id: 52, + img: + 'https://images.unsplash.com/photo-1572889834679-adc187f0a123?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Published', + title: 'est et tempus semper est quam pharetra magna ac consequat metus', + description: + 'nulla neque libero convallis eget eleifend luctus ultricies eu nibh quisque id justo sit amet sapien dignissim', + subtitle: 'Karia Avrahamy', + featured: true, + }, + { + id: 53, + img: + 'https://images.unsplash.com/photo-1509957827398-2e3a14a941f1?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Draft', + title: + 'feugiat et eros vestibulum ac est lacinia nisi venenatis tristique fusce congue diam id ornare', + description: + 'ultrices vel augue vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae donec', + subtitle: 'Susann Yerrill', + featured: true, + }, + { + id: 54, + img: + 'https://images.unsplash.com/photo-1538495435388-104fd74d46a5?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Draft', + title: + 'feugiat et eros vestibulum ac est lacinia nisi venenatis tristique fusce congue diam id', + description: + 'tempus vivamus in felis eu sapien cursus vestibulum proin eu mi nulla ac enim in tempor turpis nec euismod', + subtitle: 'Currey Ashbridge', + featured: false, + }, + { + id: 55, + img: + 'https://images.unsplash.com/photo-1580074100355-c022d08d4677?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Published', + title: + 'vulputate nonummy maecenas tincidunt lacus at velit vivamus vel nulla eget eros elementum pellentesque quisque', + description: + 'eget semper rutrum nulla nunc purus phasellus in felis donec semper sapien a libero nam', + subtitle: 'Uri Martinie', + featured: true, + }, + { + id: 56, + img: + 'https://images.unsplash.com/flagged/photo-1555884762-d6674c39055e?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Published', + title: + 'sapien ut nunc vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere', + description: + 'vivamus vel nulla eget eros elementum pellentesque quisque porta volutpat erat quisque erat eros viverra eget congue eget semper', + subtitle: 'Humfrey Warwicker', + featured: false, + }, + { + id: 57, + img: + 'https://images.unsplash.com/photo-1517408191923-f82a669f4ea1?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Published', + title: 'sapien a libero nam dui proin leo odio porttitor id', + description: + 'orci nullam molestie nibh in lectus pellentesque at nulla suspendisse potenti cras in purus eu magna vulputate luctus', + subtitle: 'Sholom McKilroe', + featured: false, + }, + { + id: 58, + img: + 'https://images.unsplash.com/photo-1489493585363-d69421e0edd3?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'In Review', + title: 'eu sapien cursus vestibulum proin eu mi nulla ac enim', + description: + 'consequat varius integer ac leo pellentesque ultrices mattis odio donec vitae nisi nam', + subtitle: 'Spencer McCutheon', + featured: false, + }, + { + id: 59, + img: + 'https://images.unsplash.com/photo-1536466528142-f752ae7bdd0c?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'In Review', + title: 'ligula vehicula consequat morbi a ipsum integer a nibh in quis justo maecenas', + description: 'faucibus orci luctus et ultrices posuere cubilia curae mauris viverra diam vitae', + subtitle: 'Robena Reace', + featured: false, + }, + { + id: 60, + img: + 'https://images.unsplash.com/photo-1448831338187-78296e6fdc4d?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Draft', + title: 'donec semper sapien a libero nam dui proin leo odio porttitor', + description: 'ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae', + subtitle: 'Penn Chessman', + featured: false, + }, + { + id: 61, + img: + 'https://images.unsplash.com/photo-1505312238910-67e64a4ec582?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Draft', + title: + 'eros vestibulum ac est lacinia nisi venenatis tristique fusce congue diam id ornare imperdiet sapien', + description: + 'nulla elit ac nulla sed vel enim sit amet nunc viverra dapibus nulla suscipit ligula in lacus curabitur at', + subtitle: 'Sollie Labon', + featured: true, + }, + { + id: 62, + img: + 'https://images.unsplash.com/photo-1510711789248-087061cda288?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Published', + title: + 'mi pede malesuada in imperdiet et commodo vulputate justo in blandit ultrices enim lorem ipsum', + description: + 'vivamus in felis eu sapien cursus vestibulum proin eu mi nulla ac enim in tempor turpis nec euismod scelerisque quam', + subtitle: 'Mellisa Piser', + featured: false, + }, + { + id: 63, + img: + 'https://images.unsplash.com/photo-1510752238388-dbb96fc2f7fe?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Draft', + title: + 'malesuada in imperdiet et commodo vulputate justo in blandit ultrices enim lorem ipsum dolor', + description: + 'ante vel ipsum praesent blandit lacinia erat vestibulum sed magna at nunc commodo placerat praesent blandit nam nulla', + subtitle: 'Valry Rabbitt', + featured: false, + }, + { + id: 64, + img: + 'https://images.unsplash.com/photo-1537387788952-cffe9f8d3090?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'In Review', + title: 'dolor quis odio consequat varius integer ac leo pellentesque ultrices mattis', + description: + 'quam sollicitudin vitae consectetuer eget rutrum at lorem integer tincidunt ante vel ipsum praesent', + subtitle: 'Zelda Killbey', + featured: true, + }, + { + id: 65, + img: + 'https://images.unsplash.com/photo-1508757941212-9e403ab28f64?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'In Review', + title: 'platea dictumst etiam faucibus cursus urna ut tellus nulla ut erat id', + description: 'lacus at turpis donec posuere metus vitae ipsum aliquam non', + subtitle: 'Eryn Timcke', + featured: true, + }, + { + id: 66, + img: + 'https://images.unsplash.com/photo-1557887591-0c28fdbd6e79?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'In Review', + title: 'id massa id nisl venenatis lacinia aenean sit amet justo morbi ut odio cras mi', + description: 'et commodo vulputate justo in blandit ultrices enim lorem ipsum dolor sit amet', + subtitle: 'Boris Hebblewaite', + featured: false, + }, + { + id: 67, + img: + 'https://images.unsplash.com/photo-1551156934-d27d9c9cdc30?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Draft', + title: 'nec condimentum neque sapien placerat ante nulla justo aliquam quis turpis eget elit', + description: 'nibh in quis justo maecenas rhoncus aliquam lacus morbi quis tortor id nulla', + subtitle: 'Kacey Stillwell', + featured: false, + }, + { + id: 68, + img: + 'https://images.unsplash.com/photo-1548167390-863d815de934?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Published', + title: 'quis augue luctus tincidunt nulla mollis molestie lorem quisque ut erat curabitur', + description: + 'donec ut mauris eget massa tempor convallis nulla neque libero convallis eget eleifend', + subtitle: 'Everett Collie', + featured: false, + }, + { + id: 69, + img: + 'https://images.unsplash.com/photo-1506374322094-6021fc3926f1?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Published', + title: 'in congue etiam justo etiam pretium iaculis justo in hac habitasse', + description: 'at turpis a pede posuere nonummy integer non velit donec diam', + subtitle: 'Marlo Daniells', + featured: true, + }, + { + id: 70, + img: + 'https://images.unsplash.com/photo-1516550710318-e34a9c74fd6a?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Draft', + title: 'magna vestibulum aliquet ultrices erat tortor sollicitudin mi sit amet', + description: + 'morbi a ipsum integer a nibh in quis justo maecenas rhoncus aliquam lacus morbi quis tortor id nulla', + subtitle: 'Rianon Philips', + featured: true, + }, + { + id: 71, + img: + 'https://images.unsplash.com/photo-1574758400006-cde2710045f0?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'In Review', + title: 'bibendum morbi non quam nec dui luctus rutrum nulla tellus', + description: 'eget elit sodales scelerisque mauris sit amet eros suspendisse accumsan tortor', + subtitle: 'Lizzy Melville', + featured: false, + }, + { + id: 72, + img: + 'https://images.unsplash.com/uploads/141223808515744db9995/3361b5e1?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'In Review', + title: 'tellus in sagittis dui vel nisl duis ac nibh fusce lacus purus aliquet at feugiat', + description: 'eleifend donec ut dolor morbi vel lectus in quam fringilla rhoncus mauris enim', + subtitle: 'Teddi Kleinhandler', + featured: true, + }, + { + id: 73, + img: + 'https://images.unsplash.com/photo-1502528230654-e2161eb9f08a?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Draft', + title: 'ligula in lacus curabitur at ipsum ac tellus semper interdum mauris ullamcorper', + description: + 'commodo vulputate justo in blandit ultrices enim lorem ipsum dolor sit amet consectetuer adipiscing elit proin interdum mauris non ligula', + subtitle: 'Sigmund Goldstone', + featured: true, + }, + { + id: 74, + img: + 'https://images.unsplash.com/photo-1519281032748-605408b238ad?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'In Review', + title: 'mauris enim leo rhoncus sed vestibulum sit amet cursus id turpis integer aliquet massa', + description: 'in hac habitasse platea dictumst maecenas ut massa quis augue luctus tincidunt', + subtitle: 'Didi Maevela', + featured: false, + }, + { + id: 75, + img: + 'https://images.unsplash.com/photo-1524230699147-7e6f131d021e?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'In Review', + title: + 'velit nec nisi vulputate nonummy maecenas tincidunt lacus at velit vivamus vel nulla eget', + description: 'libero nam dui proin leo odio porttitor id consequat in consequat ut', + subtitle: 'Odey Volke', + featured: true, + }, + { + id: 76, + img: + 'https://images.unsplash.com/photo-1532664189809-02133fee698d?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'In Review', + title: 'odio cras mi pede malesuada in imperdiet et commodo vulputate justo in blandit', + description: + 'turpis donec posuere metus vitae ipsum aliquam non mauris morbi non lectus aliquam sit amet diam', + subtitle: 'Gunther Kleinplac', + featured: true, + }, + { + id: 77, + img: + 'https://images.unsplash.com/photo-1549502318-f16240a64378?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Published', + title: 'augue a suscipit nulla elit ac nulla sed vel enim sit amet nunc viverra dapibus', + description: 'in hac habitasse platea dictumst etiam faucibus cursus urna ut tellus nulla', + subtitle: 'Chen McGriele', + featured: true, + }, + { + id: 78, + img: + 'https://images.unsplash.com/photo-1578147872305-53e7cf8bdf80?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Published', + title: 'non quam nec dui luctus rutrum nulla tellus in sagittis dui', + description: + 'nunc viverra dapibus nulla suscipit ligula in lacus curabitur at ipsum ac tellus semper', + subtitle: 'Francisca Took', + featured: true, + }, + { + id: 79, + img: + 'https://images.unsplash.com/photo-1527004013197-933c4bb611b3?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Draft', + title: 'quam nec dui luctus rutrum nulla tellus in sagittis dui', + description: + 'hendrerit at vulputate vitae nisl aenean lectus pellentesque eget nunc donec quis', + subtitle: 'Adler Sigmund', + featured: false, + }, + { + id: 80, + img: + 'https://images.unsplash.com/photo-1518124880777-cf8c82231ffb?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Published', + title: 'vivamus in felis eu sapien cursus vestibulum proin eu mi nulla ac enim in', + description: 'pede ac diam cras pellentesque volutpat dui maecenas tristique est et tempus', + subtitle: 'Arlee Polleye', + featured: false, + }, + { + id: 81, + img: + 'https://images.unsplash.com/photo-1533647326420-d4097513dc42?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'In Review', + title: 'et tempus semper est quam pharetra magna ac consequat metus sapien ut nunc', + description: 'pede ullamcorper augue a suscipit nulla elit ac nulla sed', + subtitle: 'Tobye Sleit', + featured: false, + }, + { + id: 82, + img: + 'https://images.unsplash.com/photo-1505159940484-eb2b9f2588e2?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'In Review', + title: 'tristique est et tempus semper est quam pharetra magna ac consequat metus', + description: + 'nec nisi vulputate nonummy maecenas tincidunt lacus at velit vivamus vel nulla eget eros elementum', + subtitle: 'Matthew Signore', + featured: false, + }, + { + id: 83, + img: + 'https://images.unsplash.com/photo-1520356496838-3d9184d470f4?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Published', + title: + 'convallis duis consequat dui nec nisi volutpat eleifend donec ut dolor morbi vel lectus in', + description: + 'vel augue vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae donec', + subtitle: 'Shayne Langland', + featured: false, + }, + { + id: 84, + img: + 'https://images.unsplash.com/photo-1513105872545-e08ee41691db?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Published', + title: + 'vivamus vel nulla eget eros elementum pellentesque quisque porta volutpat erat quisque erat', + description: + 'fringilla rhoncus mauris enim leo rhoncus sed vestibulum sit amet cursus id turpis integer aliquet massa id lobortis', + subtitle: 'Jerrilyn Cammock', + featured: false, + }, + { + id: 85, + img: + 'https://images.unsplash.com/photo-1505868954261-144157311e7e?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Published', + title: + 'accumsan odio curabitur convallis duis consequat dui nec nisi volutpat eleifend donec ut', + description: + 'amet sapien dignissim vestibulum vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae nulla dapibus', + subtitle: 'Selena Fetters', + featured: true, + }, + { + id: 86, + img: + 'https://images.unsplash.com/photo-1489617482379-fc98cdb77efb?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Draft', + title: + 'imperdiet sapien urna pretium nisl ut volutpat sapien arcu sed augue aliquam erat volutpat in', + description: + 'dapibus duis at velit eu est congue elementum in hac habitasse platea dictumst morbi', + subtitle: 'Constancy Rattenberie', + featured: false, + }, + { + id: 87, + img: + 'https://images.unsplash.com/photo-1503601350100-26336a6beda2?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Published', + title: 'pulvinar sed nisl nunc rhoncus dui vel sem sed sagittis nam congue', + description: + 'potenti cras in purus eu magna vulputate luctus cum sociis natoque penatibus et magnis dis parturient montes nascetur ridiculus', + subtitle: 'Myrta Ojeda', + featured: false, + }, + { + id: 88, + img: + 'https://images.unsplash.com/photo-1584548417149-fdb65186fb14?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Published', + title: 'convallis nulla neque libero convallis eget eleifend luctus ultricies eu nibh quisque', + description: + 'nibh in hac habitasse platea dictumst aliquam augue quam sollicitudin vitae consectetuer', + subtitle: 'Gerri Burridge', + featured: false, + }, + { + id: 89, + img: + 'https://images.unsplash.com/photo-1584645511189-2a471d586ac2?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'In Review', + title: 'odio cras mi pede malesuada in imperdiet et commodo vulputate justo', + description: + 'consequat varius integer ac leo pellentesque ultrices mattis odio donec vitae nisi nam ultrices libero non mattis', + subtitle: 'Buiron Crissil', + featured: false, + }, + { + id: 90, + img: + 'https://images.unsplash.com/photo-1584743241753-a727f5d13ff4?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Published', + title: 'justo morbi ut odio cras mi pede malesuada in imperdiet', + description: + 'elementum pellentesque quisque porta volutpat erat quisque erat eros viverra eget congue eget semper rutrum nulla nunc purus', + subtitle: 'Ermin Lowbridge', + featured: true, + }, + { + id: 91, + img: + 'https://images.unsplash.com/photo-1584757026043-af4cb16782e5?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Draft', + title: + 'in faucibus orci luctus et ultrices posuere cubilia curae donec pharetra magna vestibulum', + description: 'blandit nam nulla integer pede justo lacinia eget tincidunt eget tempus vel pede', + subtitle: 'Domenic Dearnaley', + featured: true, + }, + { + id: 92, + img: + 'https://images.unsplash.com/photo-1584645511184-d2265e1cbaad?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Draft', + title: 'nec dui luctus rutrum nulla tellus in sagittis dui vel', + description: + 'a libero nam dui proin leo odio porttitor id consequat in consequat ut nulla sed accumsan felis ut at', + subtitle: 'Brocky Simonich', + featured: false, + }, + { + id: 93, + img: + 'https://images.unsplash.com/photo-1584799254622-b8d7d02b108f?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Draft', + title: 'aliquet pulvinar sed nisl nunc rhoncus dui vel sem sed sagittis', + description: 'enim blandit mi in porttitor pede justo eu massa donec dapibus duis at velit', + subtitle: 'Elsa Sultana', + featured: true, + }, + { + id: 94, + img: + 'https://images.unsplash.com/photo-1584829344597-7b648c16fe05?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Published', + title: 'lectus vestibulum quam sapien varius ut blandit non interdum in', + description: + 'sed accumsan felis ut at dolor quis odio consequat varius integer ac leo pellentesque ultrices', + subtitle: 'Fay Brunner', + featured: false, + }, + { + id: 95, + img: + 'https://images.unsplash.com/photo-1534709333714-775101d963c8?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Draft', + title: + 'maecenas tristique est et tempus semper est quam pharetra magna ac consequat metus sapien', + description: + 'risus auctor sed tristique in tempus sit amet sem fusce consequat nulla nisl nunc', + subtitle: 'Freemon Bockin', + featured: false, + }, + { + id: 96, + img: + 'https://images.unsplash.com/photo-1584717018755-a4cc42f50311?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Published', + title: 'et ultrices posuere cubilia curae mauris viverra diam vitae quam suspendisse', + description: + 'ac leo pellentesque ultrices mattis odio donec vitae nisi nam ultrices libero non mattis', + subtitle: 'Carola Willatt', + featured: false, + }, + { + id: 97, + img: + 'https://images.unsplash.com/photo-1531215136647-f3657cb605bb?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Draft', + title: 'accumsan felis ut at dolor quis odio consequat varius integer', + description: 'nulla sed accumsan felis ut at dolor quis odio consequat varius integer', + subtitle: 'Justina Brazil', + featured: false, + }, + { + id: 98, + img: + 'https://images.unsplash.com/photo-1501862700950-18382cd41497?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Draft', + title: 'quisque id justo sit amet sapien dignissim vestibulum vestibulum ante ipsum primis', + description: + 'justo in hac habitasse platea dictumst etiam faucibus cursus urna ut tellus nulla ut', + subtitle: 'Dell Balsellie', + featured: false, + }, + { + id: 99, + img: + 'https://images.unsplash.com/photo-1503264116251-35a269479413?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'In Review', + title: 'tempus sit amet sem fusce consequat nulla nisl nunc nisl duis bibendum felis', + description: + 'nisl duis bibendum felis sed interdum venenatis turpis enim blandit mi in porttitor pede justo eu massa donec', + subtitle: 'Kirsti Bault', + featured: true, + }, + { + id: 100, + img: + 'https://images.unsplash.com/photo-1507499739999-097706ad8914?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Published', + title: + 'vivamus tortor duis mattis egestas metus aenean fermentum donec ut mauris eget massa tempor convallis', + description: + 'eros viverra eget congue eget semper rutrum nulla nunc purus phasellus in felis donec semper sapien', + subtitle: 'Reynard Bathoe', + featured: true, + }, + { + id: 101, + img: + 'https://images.unsplash.com/photo-1569817480337-01a8b22cd8d7?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'In Review', + title: 'nunc rhoncus dui vel sem sed sagittis nam congue risus', + description: 'nec condimentum neque sapien placerat ante nulla justo aliquam quis turpis', + subtitle: 'Ashien Jansa', + featured: true, + }, + { + id: 102, + img: + 'https://images.unsplash.com/photo-1550788696-45d0a14c9f9a?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Published', + title: 'nibh in lectus pellentesque at nulla suspendisse potenti cras in purus eu', + description: + 'platea dictumst etiam faucibus cursus urna ut tellus nulla ut erat id mauris vulputate elementum nullam varius nulla facilisi', + subtitle: 'Liane Miell', + featured: false, + }, + { + id: 103, + img: + 'https://images.unsplash.com/photo-1526994113188-348e5961f387?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Published', + title: + 'id nulla ultrices aliquet maecenas leo odio condimentum id luctus nec molestie sed justo', + description: 'ac enim in tempor turpis nec euismod scelerisque quam turpis', + subtitle: 'Menard Postans', + featured: false, + }, + { + id: 104, + img: + 'https://images.unsplash.com/photo-1519293828788-3304a1d1e850?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Published', + title: 'penatibus et magnis dis parturient montes nascetur ridiculus mus etiam vel augue', + description: + 'velit eu est congue elementum in hac habitasse platea dictumst morbi vestibulum velit id pretium iaculis', + subtitle: 'Elroy Creane', + featured: true, + }, + { + id: 105, + img: + 'https://images.unsplash.com/photo-1541449540793-66e313267a72?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Published', + title: 'lobortis est phasellus sit amet erat nulla tempus vivamus in felis eu sapien', + description: 'nibh ligula nec sem duis aliquam convallis nunc proin at turpis', + subtitle: 'Mag Guille', + featured: true, + }, + { + id: 106, + img: + 'https://images.unsplash.com/photo-1544376798-89aa6b82c6cd?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'In Review', + title: + 'eleifend pede libero quis orci nullam molestie nibh in lectus pellentesque at nulla suspendisse', + description: 'morbi quis tortor id nulla ultrices aliquet maecenas leo odio condimentum', + subtitle: 'Enid Mottram', + featured: true, + }, + { + id: 107, + img: + 'https://images.unsplash.com/photo-1496768050990-568b4d02ec18?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'In Review', + title: + 'justo sit amet sapien dignissim vestibulum vestibulum ante ipsum primis in faucibus orci luctus', + description: + 'platea dictumst morbi vestibulum velit id pretium iaculis diam erat fermentum justo nec condimentum neque', + subtitle: 'Rene Vanacci', + featured: false, + }, + { + id: 108, + img: + 'https://images.unsplash.com/photo-1492831379069-0fe9d118b7c5?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Draft', + title: + 'in ante vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia', + description: + 'donec odio justo sollicitudin ut suscipit a feugiat et eros vestibulum ac est lacinia nisi venenatis tristique', + subtitle: 'Brinna Kolakovic', + featured: false, + }, + { + id: 109, + img: + 'https://images.unsplash.com/photo-1553324069-10552f926791?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Published', + title: 'in libero ut massa volutpat convallis morbi odio odio elementum eu interdum', + description: + 'ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae donec pharetra magna vestibulum', + subtitle: 'Stepha Ilyas', + featured: true, + }, + { + id: 110, + img: + 'https://images.unsplash.com/photo-1567816632324-6c5e972d33e3?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Draft', + title: 'aliquam convallis nunc proin at turpis a pede posuere nonummy integer', + description: + 'quam suspendisse potenti nullam porttitor lacus at turpis donec posuere metus vitae ipsum aliquam non mauris morbi non lectus aliquam', + subtitle: 'Halette Scholes', + featured: true, + }, + { + id: 111, + img: + 'https://images.unsplash.com/photo-1584520156104-f9a32b3270aa?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Published', + title: 'dapibus duis at velit eu est congue elementum in hac habitasse platea', + description: + 'quam fringilla rhoncus mauris enim leo rhoncus sed vestibulum sit amet cursus id turpis integer aliquet massa', + subtitle: 'Ky People', + featured: true, + }, + { + id: 112, + img: + 'https://images.unsplash.com/photo-1584583295915-db41d2a5457a?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Published', + title: 'pellentesque at nulla suspendisse potenti cras in purus eu magna vulputate luctus cum', + description: + 'turpis donec posuere metus vitae ipsum aliquam non mauris morbi non lectus aliquam sit amet', + subtitle: 'Alexia Pepineaux', + featured: false, + }, + { + id: 113, + img: + 'https://images.unsplash.com/photo-1579032324464-156c89cc3565?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Draft', + title: + 'consectetuer adipiscing elit proin risus praesent lectus vestibulum quam sapien varius ut blandit non interdum', + description: + 'velit id pretium iaculis diam erat fermentum justo nec condimentum neque sapien placerat ante nulla', + subtitle: 'Florencia Whisson', + featured: true, + }, + { + id: 114, + img: + 'https://images.unsplash.com/photo-1579033060982-1bb5b083f4fa?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Published', + title: + 'pellentesque ultrices phasellus id sapien in sapien iaculis congue vivamus metus arcu adipiscing molestie hendrerit', + description: + 'ut blandit non interdum in ante vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae', + subtitle: 'Wilona MacConnulty', + featured: true, + }, + { + id: 115, + img: + 'https://images.unsplash.com/photo-1579032327795-e3cb02822e38?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Published', + title: 'sapien a libero nam dui proin leo odio porttitor id consequat in', + description: 'penatibus et magnis dis parturient montes nascetur ridiculus mus etiam vel', + subtitle: 'Althea Grundey', + featured: false, + }, + { + id: 116, + img: + 'https://images.unsplash.com/photo-1528734610689-348f9c3fc5a1?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'In Review', + title: 'aliquam erat volutpat in congue etiam justo etiam pretium iaculis', + description: 'sed accumsan felis ut at dolor quis odio consequat varius', + subtitle: 'Zonda Golborn', + featured: false, + }, + { + id: 117, + img: + 'https://images.unsplash.com/photo-1571903753771-ce22acbc441c?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Draft', + title: 'sit amet justo morbi ut odio cras mi pede malesuada in imperdiet et', + description: 'vel est donec odio justo sollicitudin ut suscipit a feugiat', + subtitle: 'Jaquelyn Kinloch', + featured: false, + }, + { + id: 118, + img: + 'https://images.unsplash.com/photo-1584302968712-70f6e2e19033?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'In Review', + title: + 'habitasse platea dictumst maecenas ut massa quis augue luctus tincidunt nulla mollis molestie', + description: + 'nam dui proin leo odio porttitor id consequat in consequat ut nulla sed accumsan felis', + subtitle: 'Allys Hulstrom', + featured: true, + }, + { + id: 119, + img: + 'https://images.unsplash.com/photo-1584303185213-423f6965a646?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'In Review', + title: 'praesent blandit nam nulla integer pede justo lacinia eget tincidunt eget tempus vel', + description: 'nullam molestie nibh in lectus pellentesque at nulla suspendisse potenti cras', + subtitle: 'Shawna Krause', + featured: true, + }, + { + id: 120, + img: + 'https://images.unsplash.com/photo-1584404746700-c1909babc51a?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'In Review', + title: 'nisl nunc rhoncus dui vel sem sed sagittis nam congue risus semper porta', + description: + 'nunc vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae mauris viverra', + subtitle: 'Fay Pixton', + featured: true, + }, + { + id: 121, + img: + 'https://images.unsplash.com/photo-1584551882802-ca081b505b49?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Draft', + title: + 'ipsum dolor sit amet consectetuer adipiscing elit proin interdum mauris non ligula pellentesque ultrices phasellus', + description: + 'mauris lacinia sapien quis libero nullam sit amet turpis elementum ligula vehicula consequat morbi a ipsum integer', + subtitle: 'Rowe Andrzejak', + featured: false, + }, + { + id: 122, + img: + 'https://images.unsplash.com/photo-1518887668165-8fa91a9178be?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Draft', + title: 'mattis pulvinar nulla pede ullamcorper augue a suscipit nulla elit', + description: + 'viverra pede ac diam cras pellentesque volutpat dui maecenas tristique est et tempus semper est', + subtitle: 'Mei Mathivat', + featured: false, + }, + { + id: 123, + img: + 'https://images.unsplash.com/photo-1433838552652-f9a46b332c40?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Published', + title: 'dapibus dolor vel est donec odio justo sollicitudin ut suscipit a feugiat et eros', + description: 'ac nulla sed vel enim sit amet nunc viverra dapibus nulla suscipit ligula in', + subtitle: 'Andie Eastman', + featured: true, + }, + { + id: 124, + img: + 'https://images.unsplash.com/photo-1507608443039-bfde4fbcd142?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Published', + title: 'phasellus sit amet erat nulla tempus vivamus in felis eu sapien', + description: + 'in hac habitasse platea dictumst morbi vestibulum velit id pretium iaculis diam erat', + subtitle: 'Corliss Munday', + featured: false, + }, + { + id: 125, + img: + 'https://images.unsplash.com/photo-1521381802788-d5900db802dc?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'In Review', + title: + 'justo sit amet sapien dignissim vestibulum vestibulum ante ipsum primis in faucibus orci', + description: + 'volutpat eleifend donec ut dolor morbi vel lectus in quam fringilla rhoncus mauris', + subtitle: 'Sheila-kathryn McClenan', + featured: true, + }, + { + id: 126, + img: + 'https://images.unsplash.com/photo-1452711932549-e7ea7f129399?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Draft', + title: 'feugiat non pretium quis lectus suspendisse potenti in eleifend quam', + description: + 'convallis nulla neque libero convallis eget eleifend luctus ultricies eu nibh quisque id justo', + subtitle: 'Daphene Kenvin', + featured: false, + }, + { + id: 127, + img: + 'https://images.unsplash.com/photo-1516496636080-14fb876e029d?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Published', + title: + 'pede justo lacinia eget tincidunt eget tempus vel pede morbi porttitor lorem id ligula suspendisse', + description: + 'justo lacinia eget tincidunt eget tempus vel pede morbi porttitor lorem id ligula suspendisse ornare consequat lectus in est', + subtitle: 'Althea Tesoe', + featured: true, + }, + { + id: 128, + img: + 'https://images.unsplash.com/photo-1468476775582-6bede20f356f?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjEyMDd9', + supertitle: 'Published', + title: + 'commodo vulputate justo in blandit ultrices enim lorem ipsum dolor sit amet consectetuer', + description: + 'donec quis orci eget orci vehicula condimentum curabitur in libero ut massa volutpat convallis morbi odio odio elementum', + subtitle: 'Steffi Asgodby', + featured: false, + }, +]; +export default mockData; diff --git a/packages/decap-cms-ui-4/src/Thumbnail/story.jsx b/packages/decap-cms-ui-4/src/Thumbnail/story.jsx new file mode 100644 index 000000000000..9d151709fee7 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Thumbnail/story.jsx @@ -0,0 +1,188 @@ +import React, { useState } from 'react'; +import { withKnobs, boolean, number, color, text } from '@storybook/addon-knobs'; +import styled from '@emotion/styled'; +import { withTheme } from 'emotion-theming'; + +import Thumbnail, { ThumbnailGrid } from '.'; +import getMockData from '../utils/getMockData'; + +export default { + title: 'Components/Thumbnail', + decorators: [withKnobs], +}; + +const Wrap = styled.div` + width: 100%; + height: 100%; + background-color: ${({ theme }) => theme.color.background}; + display: flex; + align-items: ${({ center }) => (center ? `center` : `flex-start`)}; + justify-content: center; + overflow-y: auto; + padding: 1rem; + box-shadow: inset -1px 0 0 ${({ theme }) => theme.color.surface}, + inset 1px 0 0 ${({ theme }) => theme.color.surface}; +`; +const StyledThumbnail = styled(Thumbnail)` + ${({ width }) => (width ? `width: ${width}` : ``)}; + ${({ height }) => (height ? `height: ${height}` : ``)}; +`; + +const StoryThumbnail = ({ + previewImgSrc, + previewImgOpacity, + previewBgColor, + previewText, + supertitle, + title, + description, + subtitle, + featured, + selectable, + previewAspectRatio, + horizontal, + supertitleMaxLines, + titleMaxLines, + descriptionMaxLines, + subtitleMaxLines, + width, + height, + onClick, +}) => { + const [selected, setSelected] = useState(false); + + return ( + setSelected(!selected)} + previewAspectRatio={previewAspectRatio} + width={width} + height={height} + onClick={onClick} + /> + ); +}; + +const ThumbnailStory = ({ theme }) => { + const previewImgSrc = text( + 'previewImgSrc', + 'https://images.unsplash.com/photo-1518887668165-8fa91a9178be?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=800&q=60', + ); + const previewImgOpacity = text('previewImgOpacity', null); + const previewBgColor = color('previewBgColor', theme.color.disabled); + const previewText = text('previewText', null); + const supertitle = text('supertitle', 'Published'); + const title = text('title', 'How the cow jumped over the moon'); + const description = text( + 'description', + 'Find out how the cow jumped over the moon in this thrilling post…', + ); + const subtitle = text('subtitle', 'John Smith • 08/15/2019 @ 12:27 PM'); + const featured = boolean('featured', true); + const selectable = boolean('selectable', false); + const previewAspectRatio = text('previewAspectRatio', '16:9'); + const horizontal = boolean('horizontal', false); + const supertitleMaxLines = number('supertitleMaxLines', 1); + const titleMaxLines = number('titleMaxLines', 3); + const descriptionMaxLines = number('descriptionMaxLines', 3); + const subtitleMaxLines = number('subtitleMaxLines', 1); + const width = text('width', '16rem'); + const height = text('height', '18rem'); + const onClick = boolean('onClick', false); + + return ( + alert('You just clicked a thumbnail.') : null} + /> + ); +}; +const ThemedThumbnailStory = withTheme(ThumbnailStory); + +export const _Thumbnail = () => ( + + + +); + +const mockData = getMockData('post', 128); + +export const _ThumbnailGrid = () => { + const horizontal = boolean('horizontal', false); + const selectable = boolean('selectable', false); + const onClick = boolean('onClick', false); + const previewAspectRatio = text('previewAspectRatio', '16:9'); + const previewImgSrc = boolean('previewImgSrc', true); + const supertitle = boolean('supertitle', true); + const title = boolean('title', true); + const description = boolean('description', true); + const subtitle = boolean('subtitle', true); + const featured = boolean('featured', true); + const supertitleMaxLines = number('supertitleMaxLines', 1); + const titleMaxLines = number('titleMaxLines', 3); + const descriptionMaxLines = number('descriptionMaxLines', 3); + const subtitleMaxLines = number('subtitleMaxLines', 1); + + return ( + + + {mockData.map((thumb, i) => ( + alert('You just clicked a thumbnail.') : null} + /> + ))} + + + ); +}; + +_ThumbnailGrid.story = { + name: 'ThumbnailGrid', +}; diff --git a/packages/decap-cms-ui-4/src/Toast/CloseButton.jsx b/packages/decap-cms-ui-4/src/Toast/CloseButton.jsx new file mode 100644 index 000000000000..4bf4a2f9dcda --- /dev/null +++ b/packages/decap-cms-ui-4/src/Toast/CloseButton.jsx @@ -0,0 +1,35 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import styled from '@emotion/styled'; +import { IconButton } from '../Button'; + +const CloseBtn = styled(IconButton)` + align-self: flex-start; +`; + +CloseBtn.defaultProps = { + icon: 'close', +}; + +function CloseButton({ closeToast, ariaLabel }) { + return ( + { + e.stopPropagation(); + closeToast(); + }} + aria-label={ariaLabel} + /> + ); +} + +CloseButton.propTypes = { + closeToast: PropTypes.func, + arialLabel: PropTypes.string, +}; + +CloseButton.defaultProps = { + ariaLabel: 'close', +}; + +export default CloseButton; diff --git a/packages/decap-cms-ui-4/src/Toast/Toast.jsx b/packages/decap-cms-ui-4/src/Toast/Toast.jsx new file mode 100644 index 000000000000..110d8ff7bdf3 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Toast/Toast.jsx @@ -0,0 +1,327 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import cx from 'classnames'; +import styled from '@emotion/styled'; +import color from 'color'; + +import LinearProgress from '../LinearProgress'; +import Icon from '../Icon'; +import Card from '../Card'; +import { POSITION, TYPE } from '../utils/constants'; +import { falseOrElement, falseOrDelay, objectValues } from '../utils/propValidator'; + +function getX(e) { + return e.targetTouches && e.targetTouches.length >= 1 ? e.targetTouches[0].clientX : e.clientX; +} + +function getY(e) { + return e.targetTouches && e.targetTouches.length >= 1 ? e.targetTouches[0].clientY : e.clientY; +} + +const noop = () => {}; + +const ToastWrap = styled.div` + padding-bottom: 8px; + box-sizing: content-box; + &:hover { + position: relative; + z-index: 1; + } +`; +const ToastInside = styled(Card)` + position: relative; + box-sizing: border-box; + display: flex; + flex-direction: column; + max-height: 800px; + overflow: hidden; + cursor: pointer; + direction: ltr; + margin-right: 24px; + transition: 200ms; + ${({ theme }) => theme.responsive.mediaQueryDown('xs')} { + margin-right: 16px; + } + ${ToastWrap}:hover & { + box-shadow: ${({ theme }) => theme.shadow({ size: 'lg', theme })}; + } +`; +ToastInside.defaultProps = { elevation: 'sm', rounded: 'lg' }; + +const ToastContentWrap = styled.div` + font-family: ${({ theme }) => theme.fontFamily}; + display: flex; +`; +const ToastContent = styled.div` + flex: 1; + padding: 20px; + align-self: center; +`; +const ToastActions = styled.div` + padding: 12px 12px 12px 0; +`; +const Title = styled.h4` + font-family: ${({ theme }) => theme.fontFamily}; + color: ${({ theme }) => theme.color.highEmphasis}; + padding: 0; + margin: 0 0 ${props => (props.hasContent ? '8px' : 0)} 0; + font-size: 16px; + font-weight: bold; +`; +const Content = styled.div` + color: ${({ theme }) => theme.color.mediumEmphasis}; + font-size: 14px; + line-height: 1rem; +`; +const IconWrap = styled.div` + background-color: ${({ type, theme }) => { + if (type === TYPE.SUCCESS) + return color(theme.color.success['900']) + .alpha(0.2) + .string(); + if (type === TYPE.WARNING) + return color('#FFB81C') + .alpha(0.2) + .string(); + if (type === TYPE.ERROR) + return color(theme.color.danger['900']) + .alpha(0.2) + .string(); + return color(theme.color.neutral['700']) + .alpha(0.2) + .string(); + }}; + color: ${({ type, theme }) => { + if (type === TYPE.SUCCESS) return theme.color.success[900]; + if (type === TYPE.WARNING) return '#FFB81C'; + if (type === TYPE.ERROR) return theme.color.danger[900]; + return theme.color.mediumEmphasis; + }}; + display: flex; + justify-content: center; + align-items: center; + width: 56px; +`; + +class Toast extends Component { + static propTypes = { + closeButton: falseOrElement.isRequired, + autoClose: falseOrDelay.isRequired, + children: PropTypes.node.isRequired, + closeToast: PropTypes.func.isRequired, + position: PropTypes.oneOf(objectValues(POSITION)).isRequired, + pauseOnHover: PropTypes.bool.isRequired, + pauseOnFocusLoss: PropTypes.bool.isRequired, + closeOnClick: PropTypes.bool.isRequired, + transition: PropTypes.func.isRequired, + draggable: PropTypes.bool.isRequired, + draggablePercent: PropTypes.number.isRequired, + in: PropTypes.bool, + onExited: PropTypes.func, + onOpen: PropTypes.func, + onClose: PropTypes.func, + type: PropTypes.oneOf(objectValues(TYPE)), + className: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), + bodyClassName: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), + progressClassName: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), + progressStyle: PropTypes.object, + progress: PropTypes.number, + isProgressDone: PropTypes.bool, + updateId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + ariaLabel: PropTypes.string, + }; + + static defaultProps = { + type: TYPE.DEFAULT, + in: true, + onOpen: noop, + onClose: noop, + className: null, + bodyClassName: null, + progressClassName: null, + updateId: null, + role: 'alert', + }; + + state = { + isRunning: true, + preventExitTransition: false, + }; + + flag = { + canCloseOnClick: true, + canDrag: false, + }; + + drag = { + start: 0, + x: 0, + y: 0, + deltaX: 0, + removalDistance: 0, + }; + + ref = null; + + componentDidMount() { + this.props.onOpen(this.props.children.props); + + if (this.props.draggable) this.bindDragEvents(); + } + + componentDidUpdate(prevProps) { + if (prevProps.draggable !== this.props.draggable) { + if (this.props.draggable) { + this.bindDragEvents(); + } else { + this.unbindDragEvents(); + } + } + } + + componentWillUnmount() { + this.props.onClose(this.props.children.props); + + if (this.props.draggable) this.unbindDragEvents(); + } + + bindDragEvents() { + document.addEventListener('mousemove', this.onDragMove); + document.addEventListener('mouseup', this.onDragEnd); + + document.addEventListener('touchmove', this.onDragMove); + document.addEventListener('touchend', this.onDragEnd); + } + + unbindDragEvents() { + document.removeEventListener('mousemove', this.onDragMove); + document.removeEventListener('mouseup', this.onDragEnd); + + document.removeEventListener('touchmove', this.onDragMove); + document.removeEventListener('touchend', this.onDragEnd); + } + + onDragStart = e => { + this.flag.canCloseOnClick = true; + this.flag.canDrag = true; + + this.ref.style.transition = ''; + + this.drag.start = this.drag.x = getX(e.nativeEvent); + this.drag.removalDistance = this.ref.offsetWidth * (this.props.draggablePercent / 100); + }; + + onDragMove = e => { + if (this.flag.canDrag) { + if (this.state.isRunning) { + this.props.pauseToast(); + } + + this.drag.x = getX(e); + this.drag.deltaX = this.drag.x - this.drag.start; + + // prevent false positif during a toast click + this.drag.start !== this.drag.x && (this.flag.canCloseOnClick = false); + + this.ref.style.transform = `translateX(${this.drag.deltaX}px)`; + this.ref.style.opacity = 1 - Math.abs(this.drag.deltaX / this.drag.removalDistance); + } + }; + + onDragEnd = e => { + if (this.flag.canDrag) { + this.flag.canDrag = false; + + if (Math.abs(this.drag.deltaX) > this.drag.removalDistance) { + this.setState( + { + preventExitTransition: true, + }, + this.props.closeToast, + ); + return; + } + + this.drag.y = getY(e); + this.ref.style.transform = 'translateX(0)'; + this.ref.style.opacity = 1; + } + }; + + render() { + const { + closeButton, + children, + autoClose, + type, + closeToast, + title, + transition: Transition, + position, + onExited, + onClick, + bodyClassName, + role, + } = this.props; + + let icon = 'info'; + + if (type === TYPE.SUCCESS) icon = 'check'; + if (type === TYPE.WARNING) icon = 'alert-triangle'; + if (type === TYPE.ERROR) icon = 'alert-circle'; + + return ( + + + { + onClick && onClick(e); + this.flag.canCloseOnClick && closeToast(); + }} + ref={ref => { + !this.ref && !this.state.el && this.setState({ el: ref }); + return (this.ref = ref); + }} + onMouseDown={this.onDragStart} + onTouchStart={this.onDragStart} + > + + + + + + {title && {title}} + {children && {children}} + + {closeButton && closeButton} + + {autoClose && ( + + )} + + + + ); + } +} + +export default Toast; diff --git a/packages/decap-cms-ui-4/src/Toast/ToastContainer.jsx b/packages/decap-cms-ui-4/src/Toast/ToastContainer.jsx new file mode 100644 index 000000000000..1cbb413889b1 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Toast/ToastContainer.jsx @@ -0,0 +1,386 @@ +import React, { Component, isValidElement, cloneElement } from 'react'; +import PropTypes from 'prop-types'; +import styled from '@emotion/styled'; +import TransitionGroup from 'react-transition-group/TransitionGroup'; + +import Toast from './Toast'; +import CloseButton from './CloseButton'; +import ToastTransition from './ToastTransition'; +import { POSITION, ACTION } from '../utils/constants'; +import eventManager from '../utils/eventManager'; +import { falseOrDelay, falseOrElement, isValidDelay, objectValues } from '../utils/propValidator'; + +const ToastContainerWrap = styled(TransitionGroup)` + z-index: 100000; + position: fixed; + width: 400px; + box-sizing: border-box; + bottom: 12px; + right: 0; + ${({ theme }) => theme.responsive.mediaQueryDown('xs')} { + width: calc(100vw - 16px); + } +`; + +class ToastContainer extends Component { + static propTypes = { + /** + * Set toast position + */ + position: PropTypes.oneOf(objectValues(POSITION)), + + /** + * Disable or set autoClose delay + */ + autoClose: falseOrDelay, + + /** + * Disable or set a custom react element for the close button + */ + closeButton: falseOrElement, + + /** + * Hide or not progress bar when autoClose is enabled + */ + hideProgressBar: PropTypes.bool, + + /** + * Pause toast duration on hover + */ + pauseOnHover: PropTypes.bool, + + /** + * Dismiss toast on click + */ + closeOnClick: PropTypes.bool, + + /** + * An optional className + */ + className: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), + + /** + * An optional style + */ + style: PropTypes.object, + + /** + * An optional className for the toast + */ + toastClassName: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), + + /** + * An optional className for the toast body + */ + bodyClassName: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), + + /** + * An optional className for the toast progress bar + */ + progressClassName: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), + + /** + * An optional style for the toast progress bar + */ + progressStyle: PropTypes.object, + + /** + * Define enter and exit transition using react-transition-group + */ + transition: PropTypes.func, + + /** + * Support rtl display + */ + rtl: PropTypes.bool, + + /** + * Allow toast to be draggable + */ + draggable: PropTypes.bool, + + /** + * The percentage of the toast's width it takes for a drag to dismiss a toast + */ + draggablePercent: PropTypes.number, + + /** + * Pause the toast on focus loss + */ + pauseOnFocusLoss: PropTypes.bool, + }; + + static defaultProps = { + position: POSITION.TOP_RIGHT, + transition: ToastTransition, + rtl: false, + autoClose: 5000, + hideProgressBar: false, + closeButton: , + pauseOnHover: true, + pauseOnFocusLoss: true, + closeOnClick: true, + newestOnTop: false, + draggable: true, + draggablePercent: 80, + className: null, + style: null, + toastClassName: null, + bodyClassName: null, + progressClassName: null, + progressStyle: null, + }; + + /** + * Hold toast ids + */ + state = { + toast: [], + isRunning: true, + }; + + /** + * Keep reference for toastKey + */ + toastKey = 1; + + /** + * Hold toast's informations: + * - what to render + * - position + * - raw content + * - options + */ + collection = {}; + + componentDidMount() { + this.bindFocusEvents(); + eventManager + .on(ACTION.SHOW, options => this.show(options)) + .on(ACTION.CLEAR, id => (!id ? this.clear() : this.removeToast(id))) + .emit(ACTION.DID_MOUNT, this); + } + + componentWillUnmount() { + this.unbindFocusEvents(); + eventManager + .off(ACTION.SHOW) + .off(ACTION.CLEAR) + .emit(ACTION.WILL_UNMOUNT); + } + + isToastActive = id => this.state.toast.indexOf(id) !== -1; + + removeToast(id) { + this.setState( + { + toast: this.state.toast.filter(v => v !== id), + }, + this.dispatchChange, + ); + } + + dispatchChange() { + eventManager.emit(ACTION.ON_CHANGE, this.state.toast.length); + } + + makeCloseButton(toastClose, toastId, type) { + let closeButton = this.props.closeButton; + + if (isValidElement(toastClose) || toastClose === false) { + closeButton = toastClose; + } + + return closeButton === false + ? false + : cloneElement(closeButton, { + closeToast: () => this.removeToast(toastId), + type, + }); + } + + getAutoCloseDelay(toastAutoClose) { + return toastAutoClose === false || isValidDelay(toastAutoClose) + ? toastAutoClose + : this.props.autoClose; + } + + canBeRendered(content) { + return ( + isValidElement(content) || + typeof content === 'string' || + typeof content === 'number' || + typeof content === 'function' + ); + } + + parseClassName(prop) { + if (typeof prop === 'string') { + return prop; + } else if (prop !== null && typeof prop === 'object' && 'toString' in prop) { + return prop.toString(); + } + + return null; + } + + bindFocusEvents() { + window.addEventListener('focus', this.playToast); + window.addEventListener('blur', this.pauseToast); + } + + unbindFocusEvents() { + window.removeEventListener('focus', this.playToast); + window.removeEventListener('blur', this.pauseToast); + } + + pauseToast = () => this.setState({ isRunning: false }); + + playToast = () => this.setState({ isRunning: true }); + + show(options) { + let { content } = options; + if (!this.canBeRendered(content)) { + throw new Error( + `The element you provided cannot be rendered. You provided an element of type ${typeof content}`, + ); + } + const toastId = options.toastId; + const closeToast = () => this.removeToast(toastId); + const toastOptions = { + id: toastId, + // if no options.key, this.toastKey - 1 is assigned + key: options.key || this.toastKey++, + type: options.type, + content: options.content, + title: options.title, + closeToast, + onClick: options.onClick, + updateId: options.updateId, + rtl: this.props.rtl, + position: options.position || this.props.position, + transition: options.transition || this.props.transition, + bodyClassName: this.parseClassName(options.bodyClassName || this.props.bodyClassName), + closeButton: this.makeCloseButton(options.closeButton, toastId, options.type), + pauseOnHover: + typeof options.pauseOnHover === 'boolean' ? options.pauseOnHover : this.props.pauseOnHover, + pauseOnFocusLoss: + typeof options.pauseOnFocusLoss === 'boolean' + ? options.pauseOnFocusLoss + : this.props.pauseOnFocusLoss, + draggable: typeof options.draggable === 'boolean' ? options.draggable : this.props.draggable, + draggablePercent: + typeof options.draggablePercent === 'number' && !isNaN(options.draggablePercent) + ? options.draggablePercent + : this.props.draggablePercent, + closeOnClick: + typeof options.closeOnClick === 'boolean' ? options.closeOnClick : this.props.closeOnClick, + progressClassName: this.parseClassName( + options.progressClassName || this.props.progressClassName, + ), + progressStyle: this.props.progressStyle, + autoClose: this.getAutoCloseDelay(options.autoClose), + progress: parseFloat(options.progress), + isProgressDone: options.isProgressDone, + }; + + typeof options.onOpen === 'function' && (toastOptions.onOpen = options.onOpen); + + typeof options.onClose === 'function' && (toastOptions.onClose = options.onClose); + + // add closeToast function to react component only + if ( + isValidElement(content) && + typeof content.type !== 'string' && + typeof content.type !== 'number' + ) { + content = cloneElement(content, { + closeToast, + }); + } else if (typeof content === 'function') { + content = content({ closeToast }); + } + + this.collection = { + ...this.collection, + [toastId]: { + position: toastOptions.position, + options: toastOptions, + }, + }; + + this.setState( + { + toast: (toastOptions.updateId + ? [...this.state.toast] + : [...this.state.toast, toastId] + ).filter(id => id !== options.staleToastId), + }, + this.dispatchChange, + ); + } + + makeToast(options) { + return ( + + {options && options.content} + + ); + } + + clear() { + this.setState({ toast: [] }); + } + + renderToast() { + const toastToRender = {}; + const { style } = this.props; + const collection = Object.keys(this.collection); + + // group toast by position + collection.forEach(toastId => { + const { position, options } = this.collection[toastId]; + toastToRender[position] || (toastToRender[position] = []); + + if (this.state.toast.indexOf(options.id) !== -1) { + toastToRender[position].push(this.makeToast(options)); + } else { + toastToRender[position].push(null); + delete this.collection[toastId]; + } + }); + + return Object.keys(toastToRender).map(position => { + const disablePointer = + toastToRender[position].length === 1 && toastToRender[position][0] === null; + const props = { + style: disablePointer ? { ...style, pointerEvents: 'none' } : { ...style }, + }; + + return ( + + {toastToRender[position]} + + ); + }); + } + + render() { + return ( +
this.setState({ isRunning: false })} + onMouseLeave={() => this.setState({ isRunning: true })} + > + {this.renderToast()} +
+ ); + } +} + +export default ToastContainer; diff --git a/packages/decap-cms-ui-4/src/Toast/ToastTransition.jsx b/packages/decap-cms-ui-4/src/Toast/ToastTransition.jsx new file mode 100644 index 000000000000..d98644426878 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Toast/ToastTransition.jsx @@ -0,0 +1,124 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Transition from 'react-transition-group/Transition'; + +class ToastTransition extends React.Component { + state = { + elHeight: null, + }; + + componentDidMount() { + if (this.props.el) this.setElHeight(this.props.el); + } + + componentDidUpdate(prevProps) { + if (!prevProps.el && this.props.el) this.setElHeight(this.props.el); + } + + componentWillUnmount() { + clearTimeout(this.timer); + } + + handleEnter = node => { + if (this.props.onEnter) this.props.onEnter(node); + }; + + handleExit = node => { + if (this.props.onExit) this.props.onExit(node); + }; + + elHeight = null; + + setElHeight = el => + this.setState( + { elDimensions: el.getBoundingClientRect() }, + () => (this.elHeight = el.getBoundingClientRect().height), + ); + + render() { + const { children, style: styleProp, timeout, ...other } = this.props; + const styles = { + default: { + height: 0, + paddingBottom: 0, + transform: `translateX(0) translateY(24px)`, + }, + entering: { + height: 0, + paddingBottom: 0, + transform: `translateX(0) translateY(24px)`, + }, + entered: { + height: this.props.el && this.props.el.offsetHeight, + paddingBottom: 8, + transform: `translateX(0) translateY(0)`, + transition: `transform 200ms cubic-bezier(0.4, 0, 0.2, 1), height 200ms cubic-bezier(0.4, 0, 0.2, 1), padding-bottom 200ms cubic-bezier(0.4, 0, 0.2, 1), opacity 200ms`, + }, + exiting: { + transform: `translateY(-${this.props.el && + this.props.el.offsetHeight}px) translateX(${(() => { + if (this.props.direction === 'right') return `-100%`; + if (this.props.direction === 'left') return `100%`; + return 0; + })()})`, + height: 0, + paddingBottom: 0, + zIndex: 0, + opacity: 0, + transition: `transform 200ms cubic-bezier(0.4, 0, 0.2, 1), height 200ms cubic-bezier(0.4, 0, 0.2, 1), padding-bottom 200ms cubic-bezier(0.4, 0, 0.2, 1), opacity 200ms`, + }, + exited: { + transform: `translateY(-${this.props.el && + this.props.el.offsetHeight + 8}px) translateX(${(() => { + if (this.props.direction === 'right') return `-100%`; + if (this.props.direction === 'left') return `100%`; + return 0; + })()})`, + height: 0, + paddingBottom: 0, + zIndex: 0, + opacity: 0, + }, + }; + + const style = { + ...styleProp, + ...styles.default, + ...(React.isValidElement(children) ? children.props.style : {}), + }; + + return ( + + {(state, childProps) => { + return React.cloneElement(children, { + style: { ...style, ...styles[state] }, + ...childProps, + }); + }} + + ); + } +} + +ToastTransition.propTypes = { + children: PropTypes.oneOfType([PropTypes.element, PropTypes.func]), + in: PropTypes.bool, + onEnter: PropTypes.func, + onExit: PropTypes.func, + style: PropTypes.object, + theme: PropTypes.object, + timeout: PropTypes.oneOfType([ + PropTypes.number, + PropTypes.shape({ enter: PropTypes.number, exit: PropTypes.number }), + ]), +}; + +ToastTransition.defaultProps = { timeout: 200, direction: 'down' }; + +export default ToastTransition; diff --git a/packages/decap-cms-ui-4/src/Toast/index.js b/packages/decap-cms-ui-4/src/Toast/index.js new file mode 100644 index 000000000000..263e47766a2f --- /dev/null +++ b/packages/decap-cms-ui-4/src/Toast/index.js @@ -0,0 +1,2 @@ +export { default as ToastContainer } from './ToastContainer'; +export { default as toast } from './utils/toast'; diff --git a/packages/decap-cms-ui-4/src/Toast/story.jsx b/packages/decap-cms-ui-4/src/Toast/story.jsx new file mode 100644 index 000000000000..02de11abc51c --- /dev/null +++ b/packages/decap-cms-ui-4/src/Toast/story.jsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { withKnobs, number, select, text } from '@storybook/addon-knobs'; + +import { toast, ToastContainer } from '.'; +import { Button, ButtonGroup } from '../Button'; + +export default { + title: 'Components/Toast', + decorators: [withKnobs], +}; + +export const _Toast = () => { + const title = text('title', 'Care for some toast?'); + const content = text('content', 'I like my toast with butter and JAM!'); + const type = select( + 'type', + { default: null, success: 'success', warning: 'warning', error: 'error' }, + null, + ); + const autoClose = + number('autoClose', 5000, { range: true, min: 0, max: 10000, step: 250 }) || false; + + return ( +
+ + + + +
+ ); +}; diff --git a/packages/decap-cms-ui-4/src/Toast/utils/toast.js b/packages/decap-cms-ui-4/src/Toast/utils/toast.js new file mode 100644 index 000000000000..c42a81f7fc2c --- /dev/null +++ b/packages/decap-cms-ui-4/src/Toast/utils/toast.js @@ -0,0 +1,117 @@ +import eventManager from '../../utils/eventManager'; +import { POSITION, TYPE, ACTION } from '../../utils/constants'; + +let container = null; +let queue = []; +const noop = () => false; + +/** + * Merge provided options with the defaults settings and generate the toastId + */ +function mergeOptions(options, type) { + return { ...options, type, toastId: getToastId(options) }; +} + +/** + * Generate a random toastId + */ +function generateToastId() { + return (Math.random().toString(36) + Date.now().toString(36)).substr(2, 10); +} + +/** + * Generate the toastId either automatically or by provided toastId + */ +function getToastId(options) { + if ( + options && + (typeof options.toastId === 'string' || + (typeof options.toastId === 'number' && !isNaN(options.toastId))) + ) { + return options.toastId; + } + + return generateToastId(); +} + +/** + * Dispatch toast. If the container is not mounted, the toast is enqueued + */ +function emitEvent(options) { + if (container !== null) { + eventManager.emit(ACTION.SHOW, options); + } else { + queue.push({ action: ACTION.SHOW, options }); + } + + return options.toastId; +} + +const toast = Object.assign( + options => emitEvent(mergeOptions(options, (options && options.type) || TYPE.DEFAULT)), + { + success: options => emitEvent(mergeOptions(options, TYPE.SUCCESS)), + info: options => emitEvent(mergeOptions(options, TYPE.INFO)), + warn: options => emitEvent(mergeOptions(options, TYPE.WARNING)), + warning: options => emitEvent(mergeOptions(options, TYPE.WARNING)), + error: options => emitEvent(mergeOptions(options, TYPE.ERROR)), + dismiss: (id = null) => container && eventManager.emit(ACTION.CLEAR, id), + isActive: noop, + update(toastId, options) { + setTimeout(() => { + if (container && typeof container.collection[toastId] !== 'undefined') { + const { options: oldOptions } = container.collection[toastId]; + + const nextOptions = { + ...oldOptions, + ...options, + toastId: options.toastId || toastId, + }; + + if (!options.toastId || options.toastId === toastId) { + nextOptions.updateId = generateToastId(); + } else { + nextOptions.staleToastId = toastId; + } + + emitEvent(nextOptions); + } + }, 0); + }, + done(id, progress = 1) { + toast.update(id, { + progress, + isProgressDone: true, + }); + }, + onChange(callback) { + if (typeof callback === 'function') { + eventManager.on(ACTION.ON_CHANGE, callback); + } + }, + POSITION, + TYPE, + }, +); + +/** + * Wait until the ToastContainer is mounted to dispatch the toast + * and attach isActive method + */ +eventManager + .on(ACTION.DID_MOUNT, containerInstance => { + container = containerInstance; + toast.isActive = id => container.isToastActive(id); + + queue.forEach(item => { + eventManager.emit(item.action, item.options); + }); + + queue = []; + }) + .on(ACTION.WILL_UNMOUNT, () => { + container = null; + toast.isActive = noop; + }); + +export default toast; diff --git a/packages/decap-cms-ui-4/src/ToggleSwitch/ToggleSwitch.jsx b/packages/decap-cms-ui-4/src/ToggleSwitch/ToggleSwitch.jsx new file mode 100644 index 000000000000..210693e18f2a --- /dev/null +++ b/packages/decap-cms-ui-4/src/ToggleSwitch/ToggleSwitch.jsx @@ -0,0 +1,37 @@ +import React from 'react'; +import styled from '@emotion/styled'; + +const ToggleSwitchInput = styled.div` + width: 2.25rem; + height: 1.5rem; + border-radius: 100px; + position: relative; + background-color: ${({ theme, checked }) => + checked ? theme.color.success['900'] : theme.color.neutral[theme.darkMode ? '1000' : '300']}; + transition: 0.25s; + cursor: pointer; + &:after { + content: ''; + display: block; + position: absolute; + left: ${props => (props.checked ? '0.875rem' : '0.125rem')}; + top: 0.125rem; + border-radius: 50%; + width: 1.25rem; + height: 1.25rem; + background-color: ${({ theme }) => theme.color.surface}; + box-shadow: 0 2px 6px 0 rgba(14, 30, 37, 0.2); + transition: 0.25s; + } + &:hover { + &:after { + box-shadow: 0 4px 8px rgba(14, 30, 37, 0.25); + } + } +`; + +const ToggleSwitch = ({ checked, onChange, className }) => ( + onChange(!checked)} /> +); + +export default ToggleSwitch; diff --git a/packages/decap-cms-ui-4/src/ToggleSwitch/index.js b/packages/decap-cms-ui-4/src/ToggleSwitch/index.js new file mode 100644 index 000000000000..f5c5e4b2ae04 --- /dev/null +++ b/packages/decap-cms-ui-4/src/ToggleSwitch/index.js @@ -0,0 +1 @@ +export { default } from './ToggleSwitch'; diff --git a/packages/decap-cms-ui-4/src/ToggleSwitch/story.jsx b/packages/decap-cms-ui-4/src/ToggleSwitch/story.jsx new file mode 100644 index 000000000000..7fb3f856a52f --- /dev/null +++ b/packages/decap-cms-ui-4/src/ToggleSwitch/story.jsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { withKnobs, boolean } from '@storybook/addon-knobs'; + +import ToggleSwitch from '.'; + +export default { + title: 'Components/ToggleSwitch', + decorators: [withKnobs], +}; + +export const _ToggleSwitch = () => { + return ; +}; + +_ToggleSwitch.story = { + name: 'ToggleSwitch', +}; diff --git a/packages/decap-cms-ui-4/src/Tooltip/Tooltip.jsx b/packages/decap-cms-ui-4/src/Tooltip/Tooltip.jsx new file mode 100644 index 000000000000..56ab28655567 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Tooltip/Tooltip.jsx @@ -0,0 +1,87 @@ +import React, { useEffect } from 'react'; +import styled from '@emotion/styled'; +import Popover from '../Popover'; + +const StyledPopover = styled(Popover)` + pointer-events: none; +`; + +const TooltipWrap = styled.div` + font-size: 12px; + font-weight: 600; + color: ${({ theme }) => theme.color.elevatedSurface}; + background-color: ${({ theme }) => theme.color.mediumEmphasis}; + box-shadow: 0 0 4px 1px + ${({ theme }) => (theme.darkMode ? 'rgba(0, 0, 0, 0.1)' : 'rgba(14, 30, 37, 0.06)')}, + 0 8px 16px 0 ${({ theme }) => (theme.darkMode ? 'rgba(0, 0, 0, 0.4)' : 'rgba(14, 30, 37, 0.2)')}; + border-radius: 4px; + padding: 0.25rem 0.5rem; +`; + +const Menu = React.forwardRef(function Menu(props, ref) { + const { + children, + enterDelay = 0, + leaveDelay = 0, + transitionDuration = 250, + anchorOrigin, + transformOrigin, + label, + ...other + } = props; + const firstSelectedItemRef = React.useRef(null); + const firstValidItemRef = React.useRef(null); + const anchorRef = React.useRef(null); + const [open, setOpen] = React.useState(false); + let mouseEnterTimeout; + let mouseLeaveTimeout; + + const getContentAnchorEl = () => firstSelectedItemRef.current || firstValidItemRef.current; + + const handleMouseEnter = () => { + clearTimeout(mouseLeaveTimeout); + mouseEnterTimeout = setTimeout(() => setOpen(true), enterDelay); + }; + + const handleMouseLeave = () => { + clearTimeout(mouseEnterTimeout); + mouseLeaveTimeout = setTimeout(() => setOpen(false), leaveDelay); + }; + + useEffect(() => { + const anchorEl = anchorRef.current; + + if (anchorEl) { + anchorEl.addEventListener('mouseenter', handleMouseEnter); + anchorEl.addEventListener('mouseleave', handleMouseLeave); + return () => { + anchorEl.removeEventListener('mouseenter', handleMouseEnter); + anchorEl.removeEventListener('mouseleave', handleMouseLeave); + }; + } + }, []); + + return ( + <> + {React.Children.map(children, element => { + return React.cloneElement(element, { ref: anchorRef }); + })} + {label && ( + + {label} + + )} + + ); +}); + +export default Menu; diff --git a/packages/decap-cms-ui-4/src/Tooltip/index.js b/packages/decap-cms-ui-4/src/Tooltip/index.js new file mode 100644 index 000000000000..cdc0fab160f8 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Tooltip/index.js @@ -0,0 +1 @@ +export { default } from './Tooltip'; diff --git a/packages/decap-cms-ui-4/src/Tree/Tree.jsx b/packages/decap-cms-ui-4/src/Tree/Tree.jsx new file mode 100644 index 000000000000..35db6b8e0613 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Tree/Tree.jsx @@ -0,0 +1,159 @@ +import React, { memo } from 'react'; +import styled from '@emotion/styled'; +import { animated, useSpring, a } from 'react-spring'; +import { useMeasure, usePrevious } from '../utils/helpers'; +import Icon from '../Icon'; +import Label from '../Label'; +import { Button } from '../Button'; + +const ExpandButton = styled(Button)` + padding: 0 0.5rem; + margin-left: -0.75rem; + &:hover, + &:focus, + &:active, + &:focus:hover, + &:focus:active { + background-color: transparent; + } +`; +ExpandButton.defaultProps = { + transparent: true, +}; +const ExpandIcon = styled(Icon)` + vertical-align: middle; + transform: rotate(${({ expanded }) => (expanded ? 90 : 0)}deg); + transition: 200ms; +`; +ExpandIcon.defaultProps = { + name: 'chevron-right', +}; +const StyledLabel = styled(Label)` + margin-bottom: -0.25rem; + cursor: move; +`; +const Actions = styled.div``; + +const TreeHeader = styled.div` + display: flex; + align-items: center; + cursor: move; +`; +const TreeHeaderText = styled.div` + font-family: ${({ theme }) => theme.fontFamily}; + color: ${({ theme }) => theme.color.highEmphasis}; + display: flex; + flex-direction: column; + justify-content: center; + flex: 1; + margin-left: -2px; + padding: 0.75rem 0; + height: 3.5rem; + box-sizing: border-box; +`; +const TreeHeaderDescription = styled.div` + font-family: ${({ theme }) => theme.fontFamily}; + color: ${({ theme }) => theme.color.highEmphasis}; + margin-top: 0.375rem; + transition: 200ms; + margin-bottom: ${({ expanded }) => (expanded ? `-1.5rem` : `-0.5rem`)}; + opacity: ${({ expanded }) => (expanded ? `0` : `1`)}; +`; +export const TreeContentWrap = styled(animated.div)` + margin-left: 0.25rem; + box-shadow: inset 0 -1px 0 0 ${({ theme }) => theme.color.border}, + ${({ theme, type }) => + type === 'danger' + ? `inset 2px 0 0 0 ${theme.color.danger['900']}` + : type === 'success' + ? `inset 2px 0 0 0 ${theme.color.success['900']}` + : `inset 1px 0 0 0 ${theme.color.border}`}; + overflow: hidden; + transition: 200ms; + display: flex; + flex-direction: column; + justify-content: flex-end; + width: ${({ single }) => (single ? `calc(100% - 0.25rem)` : `calc(100% + 0.75rem)`)}; +`; +const TreeContent = styled(a.div)` + position: relative; +`; +const TreeWrap = styled.div``; + +const Tree = memo( + ({ + children, + actions, + label, + description, + expanded, + onExpandToggle, + single, + type, + onHeaderMouseEnter, + onHeaderMouseLeave, + }) => { + const previous = usePrevious(expanded); + const [bind, { height: viewHeight }] = useMeasure(); + const { opacity, transform } = useSpring({ + from: { opacity: 1, transform: 'translate3d(0, 0, 0)' }, + to: { + opacity: expanded ? 1 : 0, + transform: `translate3d(0, 0, 0)`, + }, + }); + const { height } = useSpring({ + config: { + mass: 1, + tension: 1000, + friction: 10, + clamp: true, + }, + from: { height: 0 }, + to: { height: expanded ? viewHeight : 1 }, + }); + + return ( + + + + + + + {label && {label}} + {description && typeof description === 'string' && ( + + {description} + + )} + + {actions && {actions()}} + + + + {children} + + + + ); + }, +); + +export default Tree; diff --git a/packages/decap-cms-ui-4/src/Tree/index.js b/packages/decap-cms-ui-4/src/Tree/index.js new file mode 100644 index 000000000000..3c6564eeade5 --- /dev/null +++ b/packages/decap-cms-ui-4/src/Tree/index.js @@ -0,0 +1 @@ +export { default } from './Tree'; diff --git a/packages/decap-cms-ui-4/src/UIContext/UIContext.jsx b/packages/decap-cms-ui-4/src/UIContext/UIContext.jsx new file mode 100644 index 000000000000..403232307fb6 --- /dev/null +++ b/packages/decap-cms-ui-4/src/UIContext/UIContext.jsx @@ -0,0 +1,40 @@ +import React, { createContext, useState } from 'react'; +import { useLocalStorageState } from '../hooks'; + +export const UIContext = createContext(); + +export const UIProvider = ({ children }) => { + const [darkMode, setDarkMode] = useLocalStorageState( + 'darkMode', + window && window.matchMedia('(prefers-color-scheme: dark)').matches, + ); + const [navCollapsed, setNavCollapsed] = useLocalStorageState('navCollapsed', false); + const [pageTitle, setPageTitle] = useState(); + const [breadcrumbs, setBreadcrumbs] = useState(); + const [appBarStart, setAppBarStart] = useState(() => () => null); + const [appBarEnd, setAppBarEnd] = useState(() => () => null); + + const renderAppBarStart = fn => setAppBarStart(() => fn); + const renderAppBarEnd = fn => setAppBarEnd(() => fn); + + return ( + + {children} + + ); +}; diff --git a/packages/decap-cms-ui-4/src/UIContext/index.js b/packages/decap-cms-ui-4/src/UIContext/index.js new file mode 100644 index 000000000000..40b5034627ec --- /dev/null +++ b/packages/decap-cms-ui-4/src/UIContext/index.js @@ -0,0 +1 @@ +export * from './UIContext'; diff --git a/packages/decap-cms-ui-4/src/UserMenu/UserMenu.jsx b/packages/decap-cms-ui-4/src/UserMenu/UserMenu.jsx new file mode 100644 index 000000000000..4a1ea9545441 --- /dev/null +++ b/packages/decap-cms-ui-4/src/UserMenu/UserMenu.jsx @@ -0,0 +1,74 @@ +import React, { useState } from 'react'; + +import { AvatarButton } from '../Button'; +import { Menu, MenuItem } from '../Menu'; +import { useUIContext } from '../hooks'; + +const UserMenu = ({ className }) => { + const [userMenuAnchorEl, setUserMenuAnchorEl] = useState(null); + const { darkMode, setDarkMode } = useUIContext(); + + const handleClose = () => { + setUserMenuAnchorEl(null); + }; + + return ( + <> + setUserMenuAnchorEl(e.currentTarget)} + active={!!userMenuAnchorEl} + className={className} + /> + setUserMenuAnchorEl(null)} + anchorOrigin={{ y: 'bottom', x: 'right' }} + > + { + setDarkMode(!darkMode); + handleClose(); + }} + > + Dark Mode + + { + window.open('https://www.decapcms.org/community/'); + handleClose(); + }} + > + Help + + { + window.open('https://www.decapcms.org/docs/'); + handleClose(); + }} + > + Documentation + + { + window.open('https://github.com/decaporg/decap-cms/issues'); + handleClose(); + }} + > + Report an issue + + + Log out + + + + ); +}; + +export default UserMenu; diff --git a/packages/decap-cms-ui-4/src/UserMenu/index.js b/packages/decap-cms-ui-4/src/UserMenu/index.js new file mode 100644 index 000000000000..63aab39921d7 --- /dev/null +++ b/packages/decap-cms-ui-4/src/UserMenu/index.js @@ -0,0 +1 @@ +export { default } from './UserMenu'; diff --git a/packages/decap-cms-ui-4/src/WindowDimensionsProvider/WindowDimensionsProvider.jsx b/packages/decap-cms-ui-4/src/WindowDimensionsProvider/WindowDimensionsProvider.jsx new file mode 100644 index 000000000000..e028e23e1c56 --- /dev/null +++ b/packages/decap-cms-ui-4/src/WindowDimensionsProvider/WindowDimensionsProvider.jsx @@ -0,0 +1,28 @@ +import React, { createContext, useContext, useState, useEffect } from 'react'; + +export const WindowDimensionsCtx = createContext(null); + +const windowDims = () => ({ + height: window.innerHeight, + width: window.innerWidth, +}); + +const WindowDimensionsProvider = ({ children }) => { + const [dimensions, setDimensions] = useState(windowDims()); + useEffect(() => { + const handleResize = () => { + setDimensions(windowDims()); + }; + window.addEventListener('resize', handleResize); + return () => { + window.removeEventListener('resize', handleResize); + }; + }, []); + return {children}; +}; + +export default WindowDimensionsProvider; + +export const useWindowDimensions = () => { + return useContext(WindowDimensionsCtx); +}; diff --git a/packages/decap-cms-ui-4/src/WindowDimensionsProvider/index.js b/packages/decap-cms-ui-4/src/WindowDimensionsProvider/index.js new file mode 100644 index 000000000000..fb56eab3d383 --- /dev/null +++ b/packages/decap-cms-ui-4/src/WindowDimensionsProvider/index.js @@ -0,0 +1,4 @@ +export { + default as WindowDimensionsProvider, + useWindowDimensions, +} from './WindowDimensionsProvider'; diff --git a/packages/decap-cms-ui-4/src/hooks/index.js b/packages/decap-cms-ui-4/src/hooks/index.js new file mode 100644 index 000000000000..29f37fb14418 --- /dev/null +++ b/packages/decap-cms-ui-4/src/hooks/index.js @@ -0,0 +1,3 @@ +export * from './localStorageState'; + +export * from './uiContext'; diff --git a/packages/decap-cms-ui-4/src/hooks/localStorageState.js b/packages/decap-cms-ui-4/src/hooks/localStorageState.js new file mode 100644 index 000000000000..9ad224986510 --- /dev/null +++ b/packages/decap-cms-ui-4/src/hooks/localStorageState.js @@ -0,0 +1,16 @@ +import { useState, useEffect } from 'react'; + +export const useLocalStorageState = (key, defaultValue) => { + const localStorageKey = `decapCMS.${key}`; + const [value, setValue] = useState( + localStorage.getItem(localStorageKey) + ? JSON.parse(localStorage.getItem(localStorageKey)) + : defaultValue, + ); + + useEffect(() => { + localStorage.setItem(localStorageKey, JSON.stringify(value)); + }, [value]); + + return [value, setValue]; +}; diff --git a/packages/decap-cms-ui-4/src/hooks/uiContext.js b/packages/decap-cms-ui-4/src/hooks/uiContext.js new file mode 100644 index 000000000000..3b2c7173da3c --- /dev/null +++ b/packages/decap-cms-ui-4/src/hooks/uiContext.js @@ -0,0 +1,4 @@ +import { useContext } from 'react'; +import { UIContext } from '../UIContext'; + +export const useUIContext = () => useContext(UIContext); diff --git a/packages/decap-cms-ui-4/src/index.js b/packages/decap-cms-ui-4/src/index.js new file mode 100644 index 000000000000..24ee5ab1729a --- /dev/null +++ b/packages/decap-cms-ui-4/src/index.js @@ -0,0 +1,105 @@ +import LoginButton from './LoginButton'; +import { fonts, buttons, shadows } from './styles'; + +import { lightTheme, darkTheme } from './theme'; + +export { LoginButton, fonts, buttons, shadows, lightTheme, darkTheme }; + +export { default as AppBar } from './AppBar'; + +export { default as AppWrap } from './AppWrap'; + +export { default as Avatar } from './Avatar'; +export * from './Avatar'; + +export { default as Backdrop } from './Backdrop'; +export * from './Backdrop'; + +export * from './Button'; + +export { default as Card } from './Card'; +export * from './Card'; + +export { default as Dialog } from './Dialog'; +export * from './Dialog'; + +// export { default as Editor } from './Editor'; +// export * from './Editor'; + +export { default as Field } from './Field'; +export * from './Field'; + +export { default as Fullscreen } from './Fullscreen'; +export * from './Fullscreen'; + +export { default as GlobalStyles } from './GlobalStyles'; +export * from './GlobalStyles'; + +export * from './HOC'; + +export * from './hooks'; + +export { default as Icon } from './Icon'; +export * from './Icon'; + +export { default as Label } from './Label'; +export * from './Label'; + +export { default as LazyLoadModule } from './LazyLoadModule'; +export * from './LazyLoadModule'; + +export { default as LinearProgress } from './LinearProgress'; +export * from './LinearProgress'; + +export { default as Logo } from './Logo'; +export * from './Logo'; + +export { default as LogoTile } from './LogoTile'; +export * from './LogoTile'; + +export * from './Menu'; + +export { default as Modal } from './Modal'; +export * from './Modal'; + +export * from './NavMenu'; + +export { default as ParticleBackground } from './ParticleBackground'; +export * from './ParticleBackground'; + +export { default as Popover } from './Popover'; +export * from './Popover'; + +export { default as Portal } from './Portal'; +export * from './Portal'; + +export { default as ResponsiveLayout } from './ResponsiveLayout'; +export * from './ResponsiveLayout'; + +export { default as RootRef } from './RootRef'; +export * from './RootRef'; + +export * from './Toast'; + +export { default as ToggleSwitch } from './ToggleSwitch'; +export * from './ToggleSwitch'; + +export { default as Tooltip } from './Tooltip'; +export * from './Tooltip'; + +export { default as Tree } from './Tree'; +export * from './Tree'; + +export * from './UIContext'; + +export { default as UserMenu } from './UserMenu'; +export * from './UserMenu'; + +export { default as TextInput } from './inputs/TextInput'; +export * from './inputs/TextInput'; + +export { Slide, Grow, Fade } from './transitions'; + +export { isWindowDown, isWindowUp } from './utils/responsive'; + +export * from './WindowDimensionsProvider'; diff --git a/packages/decap-cms-ui-4/src/inputs/BooleanInput/BooleanInput.jsx b/packages/decap-cms-ui-4/src/inputs/BooleanInput/BooleanInput.jsx new file mode 100644 index 000000000000..be9881fee9f3 --- /dev/null +++ b/packages/decap-cms-ui-4/src/inputs/BooleanInput/BooleanInput.jsx @@ -0,0 +1,22 @@ +import React from 'react'; +import Field from '../../Field'; +import ToggleSwitch from '../../ToggleSwitch'; + +const BooleanInput = ({ name, onChange, children, value, ...props }) => { + return ( + (value ? onChange(false) : onChange(true))} + > + (value && onChange ? onChange(false) : onChange(true))} + checked={value} + /> + {children && children} + + ); +}; + +export default BooleanInput; diff --git a/packages/decap-cms-ui-4/src/inputs/BooleanInput/index.js b/packages/decap-cms-ui-4/src/inputs/BooleanInput/index.js new file mode 100644 index 000000000000..8bc516e02722 --- /dev/null +++ b/packages/decap-cms-ui-4/src/inputs/BooleanInput/index.js @@ -0,0 +1 @@ +export { default } from './BooleanInput'; diff --git a/packages/decap-cms-ui-4/src/inputs/BooleanInput/story.jsx b/packages/decap-cms-ui-4/src/inputs/BooleanInput/story.jsx new file mode 100644 index 000000000000..045c64991533 --- /dev/null +++ b/packages/decap-cms-ui-4/src/inputs/BooleanInput/story.jsx @@ -0,0 +1,29 @@ +import React from 'react'; +import styled from '@emotion/styled'; +import { withKnobs, boolean } from '@storybook/addon-knobs'; + +import BooleanInput from '.'; + +const StyledBooleanInput = styled(BooleanInput)` + width: 100%; +`; + +export default { + title: 'Inputs/BooleanInput', + decorators: [withKnobs], +}; + +export const _BooleanInput = () => { + return ( + + ); +}; + +_BooleanInput.story = { + name: 'BooleanInput', +}; diff --git a/packages/decap-cms-ui-4/src/inputs/DateInput/DateInput.jsx b/packages/decap-cms-ui-4/src/inputs/DateInput/DateInput.jsx new file mode 100644 index 000000000000..bfba0ba450bb --- /dev/null +++ b/packages/decap-cms-ui-4/src/inputs/DateInput/DateInput.jsx @@ -0,0 +1,69 @@ +import React, { useState } from 'react'; +import styled from '@emotion/styled'; +import moment from 'moment'; +import { DatetimePicker } from 'rc-datetime-picker'; +import TextInput from '../TextInput'; +import { IconButton } from '../../Button'; +import { Menu } from '../../Menu'; +import DatepickerStyles from './DatepickerStyles'; + +const StyledMenu = styled(Menu)` + padding: 0; +`; +const shortcuts = { + Today: moment(), + Yesterday: moment().subtract(1, 'days'), + Tomorrow: moment().add(1, 'days'), + Clear: '', +}; +const StyledDatetimePicker = styled(DatetimePicker)` + width: 100%; + background-color: transparent; +`; + +const DateInput = ({ onChange, ...props }) => { + const [date, setDate] = useState(moment()); + const [anchorEl, setAnchorEl] = useState(null); + + function handleOpenMenu(event) { + // setAnchorElWidth(`${event.currentTarget.offsetWidth}px`); + setAnchorEl(event.currentTarget); + } + + function handleClose(value) { + if (value && typeof value === 'string') { + onChange(value); + } + setAnchorEl(null); + } + + return ( + <> + + + + setDate(date)} + /> + + + ); +}; +export default DateInput; diff --git a/packages/decap-cms-ui-4/src/inputs/DateInput/DatepickerStyles.jsx b/packages/decap-cms-ui-4/src/inputs/DateInput/DatepickerStyles.jsx new file mode 100644 index 000000000000..64120899806a --- /dev/null +++ b/packages/decap-cms-ui-4/src/inputs/DateInput/DatepickerStyles.jsx @@ -0,0 +1,321 @@ +import React from 'react'; +import { Global, css } from '@emotion/core'; +import { withTheme } from 'emotion-theming'; + +const getDatepickerStyles = theme => css` + .datetime-picker { + position: relative; + background-color: ${theme.color.surface}; + border-radius: 8px; + font-size: 14px; + width: 250px; + box-sizing: content-box; + z-index: 100; + } + .datetime-picker .calendar .calendar-nav { + display: flex; + justify-content: space-between; + border-bottom: 1px solid ${theme.color.border}; + min-height: 32px; + padding: 4px; + } + .datetime-picker .calendar .calendar-nav button { + background: none; + border: 0; + width: 32px; + height: 32px; + border-radius: 4px; + outline: none; + cursor: pointer; + } + .datetime-picker .calendar .calendar-nav button .fa { + font-size: 18px; + } + .datetime-picker .calendar .calendar-nav button:hover { + background-color: ${theme.color.surfaceHighlight}; + } + .datetime-picker .calendar .calendar-nav .current-date { + display: flex; + align-items: center; + justify-content: center; + border-radius: 4px; + padding: 0 8px; + cursor: pointer; + color: ${theme.color.highEmphasis}; + } + .datetime-picker .calendar .calendar-nav .current-date:hover { + background-color: ${theme.color.surfaceHighlight}; + } + .datetime-picker .calendar .calendar-nav .current-date.disabled { + cursor: default; + } + .datetime-picker .calendar table { + display: block; + margin: 4px; + } + .datetime-picker .calendar table td, + .datetime-picker .calendar table th { + padding: 0; + } + .datetime-picker .calendar table thead { + display: block; + margin: 8px 0 3px; + } + .datetime-picker .calendar table thead tr { + display: flex; + flex-wrap: wrap; + } + .datetime-picker .calendar table thead th { + color: ${theme.color.primary['900']}; + display: flex; + align-items: center; + justify-content: center; + flex: 1; + text-align: center; + text-transform: uppercase; + font-size: 0.8em; + font-weight: 400; + } + .datetime-picker .calendar table tbody { + display: block; + } + .datetime-picker .calendar table tbody tr { + display: flex; + flex-wrap: wrap; + } + .datetime-picker .calendar table tbody tr td { + display: flex; + align-items: center; + justify-content: center; + height: 32px; + border-radius: 4px; + cursor: pointer; + color: ${theme.color.highEmphasis}; + } + .datetime-picker .calendar table tbody tr td:hover { + background-color: ${theme.color.surfaceHighlight}; + } + .datetime-picker .calendar table tbody tr td.disabled, + .datetime-picker .calendar table tbody tr td.next, + .datetime-picker .calendar table tbody tr td.prev { + color: ${theme.color.disabled}; + } + .datetime-picker .calendar table tbody tr td.disabled:hover, + .datetime-picker .calendar table tbody tr td.next:hover, + .datetime-picker .calendar table tbody tr td.prev:hover { + color: #c5c5c5; + } + .datetime-picker .calendar table tbody tr td.disabled:hover { + color: #dedede; + background-color: transparent; + cursor: not-allowed; + } + .datetime-picker .calendar table tbody tr td.now { + color: ${theme.color.highEmphasis}; + font-weight: 400; + } + .datetime-picker .calendar table tbody tr td.selected { + background-color: ${theme.color.primary['900']}; + color: #fff; + font-weight: 400; + } + .datetime-picker .calendar table tbody tr td.selected:hover { + background-color: ${theme.color.primary['800']}; + color: #fff; + } + .datetime-picker .calendar table tbody tr td.selected.start { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } + .datetime-picker .calendar table tbody tr td.selected.end { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } + .datetime-picker .calendar table tbody tr td.selected.start.end { + border-radius: 4px; + } + .datetime-picker .calendar table tbody tr td.between { + background-color: #f5fbfe; + border-radius: 0; + } + .datetime-picker .calendar .calendar-days table tbody tr td { + width: 14.28571429%; + } + .datetime-picker .calendar .calendar-months table tbody tr td { + width: 33.33333333%; + height: 40px; + } + .datetime-picker .calendar .calendar-years table tbody tr td { + width: 25%; + height: 60px; + } + .datetime-picker .time { + border-top: 1px solid ${theme.color.border}; + padding: 4px; + display: flex; + align-items: center; + position: relative; + } + .datetime-picker .time .show-time { + display: flex; + align-items: center; + flex: 1; + font-size: 14px; + text-align: center; + margin: 0 0 0 10px; + } + .datetime-picker .time .show-time .text { + line-height: 1; + font-size: 19px; + font-family: -apple-system, BlinkMacSystemFont, sans-serif; + position: relative; + z-index: 1; + color: ${theme.color.mediumEmphasis}; + } + .datetime-picker .time .show-time .separater { + margin: 0 2px 3px; + line-height: 1; + } + .datetime-picker .time .sliders { + flex: 0 1 153px; + margin-right: 10px; + max-width: 153px; + } + .datetime-picker .time .sliders .slider-text { + display: none; + } + .datetime-picker .time .sliders .slider { + display: flex; + align-items: center; + font-size: 11px; + height: 17px; + background-image: linear-gradient(90deg, #dedede, #dedede); + background-position: 0 50%; + background-size: 100% 1px; + background-repeat: no-repeat; + } + .datetime-picker .time .sliders .slider .handle { + width: 12px; + height: 12px; + background-color: #fff; + border: 1px solid #dedede; + border-radius: 3px; + cursor: pointer; + } + + .datetime-picker .shortcuts-bar { + border-bottom: 1px solid ${theme.color.border}; + padding: 8px; + } + .datetime-picker .shortcuts-bar .btn { + border: 0; + background: none; + cursor: pointer; + border-radius: 2px; + padding: 2px 4px; + outline: none; + color: ${theme.color.mediumEmphasis}; + } + .datetime-picker .shortcuts-bar .btn:hover { + background-color: ${theme.color.elevatedSurfaceHighlight}; + } + .datetime-picker .shortcuts-bar .btn:last-child { + float: right; + } + .datetime-picker .shortcuts-bar .btn:not(:first-child) { + margin-left: 5px; + } + .datetime-range-picker { + border: 1px solid #dbdbdb; + background-color: #fff; + border-radius: 4px; + box-sizing: content-box; + z-index: 100; + } + .datetime-trigger { + position: relative; + } + .datetime-trigger .datetime-picker { + position: absolute; + top: 100%; + right: 0; + } + .datetime-range-trigger { + position: relative; + } + .datetime-range-trigger .datetime-range-picker { + position: absolute; + top: 100%; + } + .datetime-picker-popup, + .datetime-range-picker-popup { + margin: 0.5rem 1rem 1rem 1rem; + box-shadow: 0 8px 24px 0 rgba(14, 30, 37, 0.15); + } + .datetime-picker-popup:before, + .datetime-range-picker-popup:before { + content: ''; + position: absolute; + background: ${({ theme }) => theme.color.surface}; + border-top: 1px solid #dbdbdb; + border-right: 1px solid #dbdbdb; + width: 10px; + height: 10px; + z-index: -1; + right: 10px; + top: -6px; + transform: rotate(315deg); + } + + .datetime-picker .calendar .calendar-nav button i { + position: relative; + color: ${theme.color.mediumEmphasis}; + } + .datetime-picker .calendar .calendar-nav button.prev-month i:after { + content: ''; + display: block; + position: absolute; + top: -0.25rem; + left: 0.75rem; + width: 0.5rem; + height: 0.5rem; + border-left: 1.5px solid; + border-bottom: 1.5px solid; + transform: rotate(45deg); + } + .datetime-picker .calendar .calendar-nav button.next-month i:after { + content: ''; + display: block; + position: absolute; + top: -0.25rem; + left: 0.5rem; + width: 0.5rem; + height: 0.5rem; + border-right: 1.5px solid; + border-top: 1.5px solid; + transform: rotate(45deg); + } + .datetime-picker .time .sliders .slider { + background-image: linear-gradient(90deg, ${theme.color.disabled}, ${theme.color.disabled}); + } + .datetime-picker .time .sliders .slider .handle { + background-color: ${theme.color.primary['900']}; + border: none; + border-radius: 32px; + &:active { + background-color: ${theme.color.primary['1400']}; + } + } + .datetime-picker .time .sliders .slider .handle:hover { + border-color: #b8b8b8; + } + .datetime-picker .time .sliders .slider .handle:active, + .datetime-picker .time .sliders .slider .handle:focus { + background-color: #5cc4ef; + border-color: #5cc4ef; + } +`; + +const DatepickerStyles = ({ theme }) => ; + +export default withTheme(DatepickerStyles); diff --git a/packages/decap-cms-ui-4/src/inputs/DateInput/index.js b/packages/decap-cms-ui-4/src/inputs/DateInput/index.js new file mode 100644 index 000000000000..a8bd7bc4cf59 --- /dev/null +++ b/packages/decap-cms-ui-4/src/inputs/DateInput/index.js @@ -0,0 +1 @@ +export { default } from './DateInput'; diff --git a/packages/decap-cms-ui-4/src/inputs/DateInput/story.jsx b/packages/decap-cms-ui-4/src/inputs/DateInput/story.jsx new file mode 100644 index 000000000000..cde53c349ebf --- /dev/null +++ b/packages/decap-cms-ui-4/src/inputs/DateInput/story.jsx @@ -0,0 +1,28 @@ +import React from 'react'; +import styled from '@emotion/styled'; +import { withKnobs, boolean } from '@storybook/addon-knobs'; + +import DateInput from '.'; + +const StyledDateInput = styled(DateInput)` + width: 100%; +`; + +export default { + title: 'Inputs/DateInput', + decorators: [withKnobs], +}; + +export const _DateInput = () => { + return ( + + ); +}; + +_DateInput.story = { + name: 'DateInput', +}; diff --git a/packages/decap-cms-ui-4/src/inputs/ListInput/ListInput.jsx b/packages/decap-cms-ui-4/src/inputs/ListInput/ListInput.jsx new file mode 100644 index 000000000000..5e2486c9bf75 --- /dev/null +++ b/packages/decap-cms-ui-4/src/inputs/ListInput/ListInput.jsx @@ -0,0 +1,183 @@ +import React from 'react'; +import styled from '@emotion/styled'; +import { sortableContainer, sortableElement } from 'react-sortable-hoc'; +import arrayMove from 'array-move'; +import Field from '../../Field'; +import { Button } from '../../Button'; +import ListInputItem from './ListInputItem'; +import { TreeContentWrap } from '../../Tree/Tree'; + +const Draggable = sortableElement(({ children }) => <>{children}); + +const Container = sortableContainer(({ children }) => { + return
{children}
; +}); + +const StyledButton = styled(Button)` + width: 100%; + margin-top: 1rem; +`; +const ActionWrap = styled.div` + position: absolute; + top: -1rem; + right: 0; + z-index: 1; +`; +const StyledListInputItem = styled(ListInputItem)` + &.dragging { + background: ${({ theme }) => theme.color.elevatedSurface}; + box-shadow: 0 0 4px 1px + ${({ theme }) => (theme.darkMode ? 'rgba(0, 0, 0, 0.1)' : 'rgba(14, 30, 37, 0.06)')}, + 0 ${({ isMobile }) => (isMobile ? '-' : '')}8px 16px 0 + ${({ theme }) => (theme.darkMode ? 'rgba(0, 0, 0, 0.4)' : 'rgba(14, 30, 37, 0.2)')}; + border-radius: ${({ isMobile }) => (isMobile ? 0 : '6px')}; + & ${TreeContentWrap} { + box-shadow: 0 0 0 0 transparent; + } + } +`; + +class ListInput extends React.Component { + state = { focus: false, items: [], expandedItems: [] }; + + addListItem = (index, data = {}) => { + const items = [...this.state.items]; + items.splice(index, 0, data); + const expandedItems = this.state.expandedItems.map(item => (item >= index ? item + 1 : item)); + + this.setState({ items, expandedItems }, () => { + this.props.onChange(this.state.items); + setTimeout(() => this.toggleExpand(index)); + }); + }; + + handleAdd = () => this.addListItem(this.state.items.length); + + handleDelete = index => { + let items = [...this.state.items]; + const isExpanded = this.state.expandedItems.indexOf(index) !== -1; + + if (isExpanded) { + this.toggleExpand(index); + } + + delete items[index]; + items = items.filter(item => item !== null); + + this.setState({ items }, () => this.props.onChange(this.state.items)); + }; + + handleChange = (data, index) => { + const items = [...this.state.items]; + items[index] = { + ...items[index], + ...data, + }; + + this.setState({ items }, () => this.props.onChange(this.state.items)); + }; + + toggleExpand = index => { + let expandedItems = [...this.state.expandedItems]; + const expandedItemsIndex = expandedItems.indexOf(index); + const isExpanded = expandedItemsIndex !== -1; + + if (isExpanded) { + delete expandedItems[expandedItemsIndex]; + expandedItems = expandedItems.filter(id => id !== null); + } else { + expandedItems.push(index); + } + this.setState({ expandedItems }); + }; + + moveListItem = (oldIndex, newIndex) => { + const items = arrayMove([...this.state.items], oldIndex, newIndex); + const expandedItems = this.state.expandedItems.map(item => { + if (item === oldIndex) return newIndex; + if ( + item === newIndex || + (item > oldIndex && item < newIndex) || + (item < oldIndex && item > newIndex) + ) { + if (oldIndex < newIndex) return item - 1; + if (oldIndex > newIndex) return item + 1; + } + return item; + }); + + // Setting items to empty array intentionally to trigger unmount and remount so no transition animation occurs + this.setState({ items: [], expandedItems }, () => + this.setState({ items }, () => this.props.onChange(this.state.items)), + ); + }; + + handleDrop = ({ oldIndex, newIndex }) => { + this.moveListItem(oldIndex, newIndex); + }; + + render() { + const { name, label, labelSingular, fields, className, inline } = this.props; + const { focus, items, expandedItems } = this.state; + + return ( + + + {items && items.length > 1 && ( + + )} + + + {items.map((item, index) => { + const itemExpanded = expandedItems.indexOf(index) !== -1; + + return ( + + + + ); + })} + + + + Add New {labelSingular} + + + ); + } +} + +export default ListInput; diff --git a/packages/decap-cms-ui-4/src/inputs/ListInput/ListInputItem.jsx b/packages/decap-cms-ui-4/src/inputs/ListInput/ListInputItem.jsx new file mode 100644 index 000000000000..44d6a88512c5 --- /dev/null +++ b/packages/decap-cms-ui-4/src/inputs/ListInput/ListInputItem.jsx @@ -0,0 +1,176 @@ +import React, { useState } from 'react'; +import styled from '@emotion/styled'; +import { Button } from '../../Button'; +import Tree from '../../Tree'; +import { FieldContext } from '../../Field'; +import { Menu, MenuItem } from '../../Menu'; + +const ListItem = styled.div` + margin-left: -1rem; + margin-right: -1rem; + padding-left: 1rem; + padding-right: 1rem; + position: relative; +`; +const ListIconActions = styled.div` + margin-right: -0.5rem; +`; +const AddNewHoverZone = styled.div` + position: absolute; + left: 0; + right: 0; + bottom: -0.625rem; + display: flex; + justify-content: center; + align-items: center; + z-index: 1; +`; +const AddNewIconButton = styled(Button)` + line-height: 1; + padding: 0 0.75rem; + &, + &:hover, + &:focus, + &:active:hover, + &:focus:hover { + background-color: ${({ theme }) => theme.color.surface}; + } + transition: 200ms; + transform: scale(0); + + ${AddNewHoverZone}:hover & { + transform: scale(1); + } +`; + +const ListInputItem = ({ + itemExpanded, + labelSingular, + index, + item, + items, + fields, + onDelete, + addListItem, + moveListItem, + handleChange, + toggleExpand, + last, + className, +}) => { + const [anchorEl, setAnchorEl] = useState(null); + const [treeType, setTreeType] = useState(null); + + function handleOpenMenu(event) { + setAnchorEl(event.currentTarget); + } + + function handleClose() { + setAnchorEl(null); + } + + return ( + + toggleExpand(index)} + expanded={itemExpanded} + label={labelSingular} + description={!!Object.keys(item).length && item[Object.keys(item)[0]]} + type={treeType} + onHeaderMouseEnter={() => setTreeType('success')} + onHeaderMouseLeave={() => setTreeType(null)} + actions={() => ( + +