From f21587ea047ecd961ee0d6e88a019a259896d114 Mon Sep 17 00:00:00 2001 From: Jesse Baker <jesse.baker@acquia.com> Date: Mon, 3 Feb 2025 15:56:47 +0000 Subject: [PATCH 01/10] refresh look/feel of topbar. Added new Extensions dropdown hooked up to dummy api and added a placeholder for CMS icon. --- .../ExperienceBuilderController.php | 23 ++++- ui/assets/icons/cms.svg | 6 +- ui/assets/icons/drop.svg | 5 +- ui/assets/icons/elements.svg | 4 + ui/assets/icons/extension.svg | 4 + ui/assets/icons/text.svg | 3 + ui/src/app/store.ts | 3 + ui/src/components/UndoRedo.tsx | 20 +++-- .../extensionsPopover/ExtensionButton.tsx | 32 +++++++ .../ExtensionPopover.module.css | 26 ++++++ .../extensionsPopover/ExtensionsPopover.tsx | 90 +++++++++++++++++++ .../panel/ContextualPanel.module.css | 2 +- ui/src/components/topbar/Topbar.module.css | 21 +++-- ui/src/components/topbar/Topbar.tsx | 73 +++++++++------ ui/src/features/canvas/Canvas.module.css | 2 +- ui/src/services/extensions.ts | 55 ++++++++++++ ui/src/styles/tokens/layout.css | 3 +- ui/src/types/Extensions.ts | 7 ++ 18 files changed, 328 insertions(+), 51 deletions(-) mode change 100755 => 100644 ui/assets/icons/drop.svg create mode 100644 ui/assets/icons/elements.svg create mode 100644 ui/assets/icons/extension.svg create mode 100644 ui/assets/icons/text.svg create mode 100644 ui/src/components/extensionsPopover/ExtensionButton.tsx create mode 100644 ui/src/components/extensionsPopover/ExtensionPopover.module.css create mode 100644 ui/src/components/extensionsPopover/ExtensionsPopover.tsx create mode 100644 ui/src/services/extensions.ts create mode 100644 ui/src/types/Extensions.ts diff --git a/src/Controller/ExperienceBuilderController.php b/src/Controller/ExperienceBuilderController.php index 9644691bb8..3335abc908 100644 --- a/src/Controller/ExperienceBuilderController.php +++ b/src/Controller/ExperienceBuilderController.php @@ -29,9 +29,30 @@ final class ExperienceBuilderController { <css-placeholder token="CSS-HERE-PLEASE"> <js-placeholder token="JS-HERE-PLEASE"> <title>Drupal Experience Builder</title> + <style> + .experience-builder-loading { + font-family: sans-serif; + opacity: 0.5; + display: flex; + justify-content: center; + align-items: center; + inset: 0; + position: fixed; + animation: pulseLoading 2s infinite; + } + + @keyframes pulseLoading { + 0%, 100% { + opacity: 1; + } + 50% { + opacity: 0.5; + } + } + </style> </head> <body> - <div id="experience-builder" class="experience-builder-container">Loading Experience Builder…</div> + <div id="experience-builder" class="experience-builder-container"><div class="experience-builder-loading">Loading Experience Builder…</div></div> </body> </html> HTML; diff --git a/ui/assets/icons/cms.svg b/ui/assets/icons/cms.svg index dc493ea478..c5d1135167 100644 --- a/ui/assets/icons/cms.svg +++ b/ui/assets/icons/cms.svg @@ -1,4 +1,4 @@ -<svg width="80" height="80" viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path fill-rule="evenodd" clip-rule="evenodd" d="M13.3327 16.6666C13.3327 16.6707 13.3333 16.6861 13.3429 16.7176C13.3542 16.7547 13.3821 16.8281 13.4496 16.938C13.5911 17.1685 13.8796 17.515 14.4201 17.941C15.5207 18.8084 17.3199 19.7353 19.8402 20.5754C24.8461 22.244 31.9736 23.3333 39.9993 23.3333C48.025 23.3333 55.1526 22.244 60.1585 20.5754C62.6788 19.7353 64.478 18.8084 65.5786 17.941C66.1191 17.515 66.4076 17.1685 66.5491 16.938C66.6166 16.8281 66.6445 16.7547 66.6558 16.7176C66.6656 16.6856 66.666 16.6712 66.666 16.6673C66.666 16.6671 66.666 16.6675 66.666 16.6673C66.666 16.6649 66.6663 16.6501 66.6558 16.6156C66.6445 16.5785 66.6166 16.5051 66.5491 16.3951C66.4076 16.1647 66.1191 15.8182 65.5786 15.3922C64.478 14.5248 62.6788 13.5979 60.1585 12.7578C55.1526 11.0892 48.025 9.99992 39.9993 9.99992C31.9736 9.99992 24.8461 11.0892 19.8402 12.7578C17.3199 13.5979 15.5207 14.5248 14.4201 15.3922C13.8796 15.8182 13.5911 16.1647 13.4496 16.3951C13.3821 16.5051 13.3542 16.5785 13.3429 16.6156C13.3326 16.6492 13.3327 16.6636 13.3327 16.6666ZM66.666 25.0886C65.3286 25.7725 63.8438 26.3742 62.2666 26.8999C56.4146 28.8506 48.5422 29.9999 39.9993 29.9999C31.4565 29.9999 23.5841 28.8506 17.7321 26.8999C16.1549 26.3742 14.6701 25.7725 13.3327 25.0886V39.9999L13.3332 40.0051C13.3339 40.0094 13.3357 40.0197 13.3408 40.0367C13.3508 40.0703 13.3764 40.1398 13.4398 40.2456C13.5724 40.4667 13.8474 40.807 14.3728 41.2299C15.443 42.0911 17.233 43.0396 19.8402 43.9087C25.0327 45.6395 32.2868 46.6666 39.9993 46.6666C47.7119 46.6666 54.966 45.6395 60.1585 43.9087C62.7657 43.0396 64.5557 42.0911 65.6259 41.2299C66.1513 40.807 66.4263 40.4667 66.5589 40.2456C66.6223 40.1398 66.6479 40.0703 66.6579 40.0367C66.6611 40.0261 66.663 40.0181 66.6641 40.0126C66.6648 40.0092 66.6652 40.0067 66.6655 40.0051L66.666 40.0008L66.666 25.0886ZM73.3327 16.6666C73.3327 13.8268 71.603 11.6519 69.705 10.1561C67.7695 8.63069 65.1752 7.40277 62.2666 6.43324C56.4146 4.48257 48.5422 3.33325 39.9993 3.33325C31.4565 3.33325 23.5841 4.48257 17.7321 6.43324C14.8235 7.40277 12.2292 8.63069 10.2937 10.1561C8.39566 11.6519 6.66602 13.8268 6.66602 16.6666V63.3332C6.66602 66.0841 8.30732 68.2394 10.1932 69.757C12.1098 71.2993 14.7132 72.5603 17.7321 73.5666C23.7918 75.5865 31.7989 76.6666 39.9993 76.6666C48.1998 76.6666 56.2069 75.5865 62.2666 73.5666C65.2855 72.5603 67.8889 71.2993 69.8055 69.757C71.6914 68.2394 73.3327 66.0841 73.3327 63.3332V16.6666ZM66.666 48.4199C65.3424 49.0981 63.8626 49.7013 62.2666 50.2333C56.2069 52.2532 48.1998 53.3332 39.9993 53.3332C31.7989 53.3332 23.7918 52.2532 17.7321 50.2333C16.1361 49.7013 14.6563 49.0981 13.3327 48.4199V63.3332L13.3332 63.3384C13.3339 63.3428 13.3357 63.353 13.3408 63.37C13.3508 63.4036 13.3764 63.4732 13.4398 63.579C13.5724 63.8 13.8474 64.1404 14.3728 64.5632C15.443 65.4245 17.233 66.373 19.8402 67.242C25.0327 68.9729 32.2868 69.9999 39.9993 69.9999C47.7119 69.9999 54.966 68.9729 60.1585 67.242C62.7657 66.373 64.5557 65.4245 65.6259 64.5632C66.1513 64.1404 66.4263 63.8 66.5589 63.579C66.6223 63.4732 66.6479 63.4036 66.6579 63.37C66.663 63.353 66.6648 63.3428 66.6655 63.3384L66.666 63.3341L66.666 48.4199Z" fill="currentColor"/> +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<rect width="24" height="24" fill="white" fill-opacity="0.01"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M5.6 6.4002C5.60001 6.40119 5.60014 6.40487 5.60244 6.41243C5.60516 6.42134 5.61186 6.43896 5.62806 6.46534C5.66202 6.52065 5.73126 6.60382 5.86099 6.70605C6.12513 6.91422 6.55693 7.13668 7.16181 7.3383C8.36322 7.73877 10.0738 8.0002 12 8.0002C13.9262 8.0002 15.6368 7.73877 16.8382 7.3383C17.4431 7.13668 17.8749 6.91422 18.139 6.70605C18.2687 6.60382 18.338 6.52065 18.3719 6.46534C18.3881 6.43896 18.3948 6.42134 18.3976 6.41243C18.3999 6.40477 18.4 6.40131 18.4 6.40037C18.4 6.40032 18.4 6.40041 18.4 6.40037C18.4 6.39979 18.4001 6.39624 18.3976 6.38796C18.3948 6.37905 18.3881 6.36143 18.3719 6.33505C18.338 6.27974 18.2687 6.19657 18.139 6.09434C17.8749 5.88617 17.4431 5.66371 16.8382 5.46209C15.6368 5.06162 13.9262 4.8002 12 4.8002C10.0738 4.8002 8.36322 5.06162 7.16181 5.46209C6.55693 5.66371 6.12513 5.88617 5.86099 6.09434C5.73126 6.19657 5.66202 6.27974 5.62806 6.33505C5.61186 6.36143 5.60516 6.37905 5.60244 6.38796C5.59999 6.39603 5.6 6.39948 5.6 6.4002ZM18.4 8.42147C18.079 8.58561 17.7227 8.73002 17.3442 8.8562C15.9397 9.32436 14.0503 9.60019 12 9.60019C9.94972 9.60019 8.06033 9.32436 6.65585 8.8562C6.27733 8.73002 5.92098 8.58561 5.6 8.42147V12.0002L5.60013 12.0014C5.60029 12.0025 5.60072 12.0049 5.60194 12.009C5.60434 12.0171 5.61049 12.0338 5.62572 12.0592C5.65753 12.1122 5.72352 12.1939 5.84962 12.2954C6.10648 12.5021 6.53608 12.7297 7.16181 12.9383C8.40801 13.3537 10.149 13.6002 12 13.6002C13.851 13.6002 15.592 13.3537 16.8382 12.9383C17.4639 12.7297 17.8935 12.5021 18.1504 12.2954C18.2765 12.1939 18.3425 12.1122 18.3743 12.0592C18.3895 12.0338 18.3957 12.0171 18.3981 12.009C18.3988 12.0065 18.3993 12.0046 18.3995 12.0032C18.3997 12.0024 18.3998 12.0018 18.3999 12.0014L18.4 12.0004L18.4 8.42147ZM20 6.4002C20 5.71864 19.5849 5.19667 19.1294 4.83768C18.6648 4.47158 18.0422 4.17688 17.3442 3.94419C15.9397 3.47603 14.0503 3.2002 12 3.2002C9.94972 3.2002 8.06033 3.47603 6.65585 3.94419C5.95779 4.17688 5.33517 4.47158 4.87063 4.83768C4.41511 5.19667 4 5.71864 4 6.4002V17.6002C4 18.2604 4.39391 18.7777 4.84651 19.1419C5.3065 19.5121 5.93132 19.8147 6.65585 20.0562C8.11018 20.541 10.0319 20.8002 12 20.8002C13.9681 20.8002 15.8898 20.541 17.3442 20.0562C18.0687 19.8147 18.6935 19.5121 19.1535 19.1419C19.6061 18.7777 20 18.2604 20 17.6002V6.4002ZM18.4 14.021C18.0823 14.1838 17.7272 14.3285 17.3442 14.4562C15.8898 14.941 13.9681 15.2002 12 15.2002C10.0319 15.2002 8.11018 14.941 6.65585 14.4562C6.27283 14.3285 5.91767 14.1838 5.6 14.021V17.6002L5.60013 17.6014C5.60029 17.6025 5.60072 17.6049 5.60194 17.609C5.60434 17.6171 5.61049 17.6338 5.62572 17.6592C5.65753 17.7122 5.72352 17.7939 5.84962 17.8954C6.10648 18.1021 6.53608 18.3297 7.16181 18.5383C8.40801 18.9537 10.149 19.2002 12 19.2002C13.851 19.2002 15.592 18.9537 16.8382 18.5383C17.4639 18.3297 17.8935 18.1021 18.1504 17.8954C18.2765 17.7939 18.3425 17.7122 18.3743 17.6592C18.3895 17.6338 18.3957 17.6171 18.3981 17.609C18.3993 17.6049 18.3997 17.6025 18.3999 17.6014L18.4 17.6004L18.4 14.021Z" fill="black"/> </svg> - diff --git a/ui/assets/icons/drop.svg b/ui/assets/icons/drop.svg old mode 100755 new mode 100644 index b81a75b3a4..56e2378875 --- a/ui/assets/icons/drop.svg +++ b/ui/assets/icons/drop.svg @@ -1,4 +1,3 @@ -<svg width="186.525" height="243.713" viewBox="0 0 186.525 243.713" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path fill-rule="evenodd" clip-rule="evenodd" d="M131.64 51.91C114.491 34.769 98.13 18.429 93.26 0c-4.87 18.429-21.234 34.769-38.38 51.91C29.16 77.613 0 106.743 0 150.434a93.263 93.263 0 1 0 186.525 0c0-43.688-29.158-72.821-54.885-98.524m-92 120.256c-5.719-.194-26.824-36.571 12.329-75.303l25.909 28.3a2.215 2.215 0 0 1-.173 3.306c-6.183 6.34-32.534 32.765-35.81 41.902-.675 1.886-1.663 1.815-2.256 1.795m53.624 47.943a32.075 32.075 0 0 1-32.076-32.075 33.423 33.423 0 0 1 7.995-21.187c5.784-7.072 24.077-26.963 24.077-26.963s18.012 20.183 24.033 26.896a31.368 31.368 0 0 1 8.046 21.254 32.076 32.076 0 0 1-32.075 32.075m61.392-52.015c-.691 1.512-2.26 4.036-4.376 4.113-3.773.138-4.176-1.796-6.965-5.923-6.122-9.06-59.551-64.9-69.545-75.699-8.79-9.498-1.238-16.195 2.266-19.704 4.395-4.403 17.224-17.225 17.224-17.225s38.255 36.296 54.19 61.096 10.444 46.26 7.206 53.342" fill="currentColor"/> +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M18.1477 9.48371C18.1365 9.46234 18.1254 9.45166 18.1031 9.43029C17.0096 8.25499 15.4586 6.58822 14.3428 5.40224C13.9969 5.04965 13.651 4.69706 13.3274 4.33379C13.2493 4.24832 13.1824 4.16284 13.1043 4.07736C13.0373 4.01326 13.0039 3.97052 13.0039 3.97052H13.015C12.6356 3.54314 12.3344 3.06234 12.1224 2.54949L12.0666 2.43196C12.0666 2.43196 12.0554 2.43196 12.0443 2.41059C12.0331 2.3999 12.0108 2.3999 11.9996 2.3999H11.9885C11.9662 2.3999 11.955 2.41059 11.9438 2.41059C11.9364 2.41771 11.929 2.42483 11.9215 2.43196L11.8657 2.54949C11.6426 3.06234 11.3413 3.54314 10.9731 3.97052H10.9843C10.9843 3.97052 10.9508 4.01326 10.8838 4.07736C10.8057 4.16284 10.7388 4.24832 10.6607 4.33379C10.3259 4.69706 9.99119 5.04965 9.64529 5.40224C8.54065 6.58822 6.97853 8.25499 5.88505 9.43029C5.88505 9.45166 5.86273 9.46234 5.84042 9.48371C1.71196 14.7512 6.19747 19.0463 6.19747 19.0463C7.52527 20.5208 9.41098 21.429 11.4306 21.5678C11.6091 21.5892 11.7988 21.5999 11.9996 21.5999H12.0108C12.2005 21.5999 12.3902 21.5892 12.5798 21.5678C14.6106 21.429 16.4851 20.5208 17.813 19.0463H17.8018C17.8018 19.0463 22.2873 14.7512 18.1588 9.48371H18.1477ZM8.54065 13.1912L8.44023 13.3408C7.68149 14.2276 7.23517 15.1678 7.09011 16.0974C7.0678 16.2149 6.95622 16.2897 6.84464 16.2684C6.76653 16.2577 6.71074 16.2042 6.67727 16.1401C6.47642 15.6914 6.34253 15.2213 6.27558 14.7405C6.0301 13.0416 6.56569 11.5565 7.7038 10.285C8.16128 9.78287 8.61876 9.2807 9.07624 8.77853C9.15434 8.69306 9.28824 8.68237 9.3775 8.75716C9.38494 8.75716 9.39238 8.76429 9.39982 8.77853C9.76803 9.18454 10.2478 9.70808 10.7834 10.3064C10.8504 10.3812 10.8504 10.4987 10.7834 10.5842C10.047 11.4283 9.26592 12.3258 8.56297 13.1805L8.54065 13.1912ZM14.8784 17.5932C14.4544 18.6617 13.6175 19.3027 12.4348 19.4844C10.7499 19.7301 9.17666 18.6296 8.92002 17.0163C8.92002 16.9949 8.92002 16.9842 8.90887 16.9628C8.77497 16.0547 9.06508 15.2533 9.67877 14.5695C10.3929 13.7682 11.9662 11.9091 11.9885 11.877C12.0219 11.9091 13.6956 13.9071 14.3205 14.6123C15.1239 15.4991 15.3024 16.5141 14.8784 17.5932ZM17.3778 15.9158C17.3666 15.9478 17.3443 15.9906 17.3332 16.0226C17.2885 16.1295 17.1658 16.1829 17.0542 16.1401C16.9873 16.1081 16.9315 16.0547 16.9203 15.9799C16.7641 15.0824 16.3178 14.1849 15.5925 13.3408H15.5814L15.5144 13.234L15.4698 13.1805C14.8896 12.4754 11.8211 9.03496 10.4821 7.53913C10.4152 7.46434 10.4152 7.34681 10.4821 7.26134C10.9285 6.79122 11.3748 6.3211 11.8099 5.8403C11.8881 5.75483 12.0219 5.75483 12.1112 5.8403H12.1224C12.3232 6.05399 12.5129 6.257 12.7026 6.47069C13.9188 7.77419 15.1462 9.05633 16.3289 10.3705C17.813 12.0159 18.1588 13.9178 17.3778 15.9264V15.9158Z" fill="#1C2024"/> </svg> - diff --git a/ui/assets/icons/elements.svg b/ui/assets/icons/elements.svg new file mode 100644 index 0000000000..c6fccd4539 --- /dev/null +++ b/ui/assets/icons/elements.svg @@ -0,0 +1,4 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<rect width="24" height="24" fill="white" fill-opacity="0.01"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M3.43805 6.39983C3.43805 4.76434 4.76386 3.43854 6.39934 3.43854C8.03482 3.43854 9.36064 4.76434 9.36064 6.39983C9.36064 8.0353 8.03482 9.36113 6.39934 9.36113C4.76386 9.36113 3.43805 8.0353 3.43805 6.39983ZM6.39934 1.99854C3.96858 1.99854 1.99805 3.96906 1.99805 6.39983C1.99805 8.8306 3.96858 10.8011 6.39934 10.8011C8.83011 10.8011 10.8006 8.8306 10.8006 6.39983C10.8006 3.96906 8.83011 1.99854 6.39934 1.99854ZM9.31186 17.6001L3.99928 20.5389V14.6612L9.31186 17.6001ZM4.22144 13.1384C3.47496 12.7255 2.55928 13.2654 2.55928 14.1185V21.0817C2.55928 21.9348 3.47496 22.4746 4.22144 22.0617L10.5151 18.5801C11.2855 18.1538 11.2855 17.0463 10.5151 16.6201L4.22144 13.1384ZM13.2793 14.3999C13.2793 13.7814 13.7808 13.2799 14.3993 13.2799H20.7993C21.4179 13.2799 21.9193 13.7814 21.9193 14.3999V20.7999C21.9193 21.4185 21.4179 21.9199 20.7993 21.9199H14.3993C13.7808 21.9199 13.2793 21.4185 13.2793 20.7999V14.3999ZM14.7193 14.7199V20.4799H20.4793V14.7199H14.7193ZM21.5084 3.50887C21.7896 3.22769 21.7896 2.77182 21.5084 2.49063C21.2273 2.20946 20.7715 2.20946 20.4902 2.49063L17.5993 5.38154L14.7084 2.49068C14.4273 2.2095 13.9714 2.2095 13.6902 2.49068C13.409 2.77185 13.409 3.22774 13.6902 3.5089L16.5811 6.39977L13.6902 9.29063C13.409 9.57182 13.409 10.0277 13.6902 10.3089C13.9714 10.5901 14.4273 10.5901 14.7084 10.3089L17.5993 7.41801L20.4902 10.3089C20.7715 10.5901 21.2273 10.5901 21.5084 10.3089C21.7896 10.0277 21.7896 9.57185 21.5084 9.29068L18.6176 6.39977L21.5084 3.50887Z" fill="#1C2024"/> +</svg> diff --git a/ui/assets/icons/extension.svg b/ui/assets/icons/extension.svg new file mode 100644 index 0000000000..e6f55fbb3c --- /dev/null +++ b/ui/assets/icons/extension.svg @@ -0,0 +1,4 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<rect width="24" height="24" fill="white" fill-opacity="0.01"/> +<path d="M13.6 5.6002C13.6 4.27471 12.5255 3.2002 11.2 3.2002C9.87452 3.2002 8.8 4.27471 8.8 5.6002V6.4002H4V11.2002H4.8C6.12548 11.2002 7.2 12.2747 7.2 13.6002C7.2 14.9257 6.12548 16.0002 4.8 16.0002H4V20.8002H8.8V20.0002C8.8 18.6747 9.87452 17.6002 11.2 17.6002C12.5255 17.6002 13.6 18.6747 13.6 20.0002V20.8002H18.4V16.0002H19.2C20.5255 16.0002 21.6 14.9257 21.6 13.6002C21.6 12.2747 20.5255 11.2002 19.2 11.2002H18.4V6.4002H13.6V5.6002Z" stroke="#272727" stroke-width="1.6" stroke-linejoin="round"/> +</svg> diff --git a/ui/assets/icons/text.svg b/ui/assets/icons/text.svg new file mode 100644 index 0000000000..6bc0483af8 --- /dev/null +++ b/ui/assets/icons/text.svg @@ -0,0 +1,3 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M6.31891 4.71979V7.19972C6.31891 7.59737 5.99654 7.91972 5.59891 7.91972C5.20126 7.91972 4.87891 7.59737 4.87891 7.19972V3.99982C4.87891 3.92369 4.89071 3.85033 4.9126 3.78147C5.00507 3.49055 5.27737 3.27979 5.59891 3.27979H18.3989C18.6475 3.27979 18.8665 3.40571 18.996 3.59723C19.0736 3.71214 19.1189 3.85067 19.1189 3.99979V7.19972C19.1189 7.59737 18.7966 7.91972 18.3989 7.91972C18.0013 7.91972 17.6789 7.59737 17.6789 7.19972V4.71979H12.8789V19.2798H14.8059C15.2035 19.2798 15.5259 19.6022 15.5259 19.9998C15.5259 20.3975 15.2035 20.7198 14.8059 20.7198H9.20587C8.80822 20.7198 8.48587 20.3975 8.48587 19.9998C8.48587 19.6022 8.80822 19.2798 9.20587 19.2798H11.1189V4.71979H6.31891Z" fill="#1C2024"/> +</svg> diff --git a/ui/src/app/store.ts b/ui/src/app/store.ts index 539a387b4c..a08e65cfbf 100644 --- a/ui/src/app/store.ts +++ b/ui/src/app/store.ts @@ -23,6 +23,7 @@ import { dummyPropsFormApi } from '@/services/dummyPropsForm'; import { pageDataFormApi } from '@/services/pageDataForm'; import { configurationSlice } from '@/features/configuration/configurationSlice'; import { sectionApi } from '@/services/sections'; +import { extensionsApi } from '@/services/extensions'; import { codeComponentApi } from '@/services/codeComponents'; import { formStateSlice } from '@/features/form/formStateSlice'; import type { UnknownAction } from 'redux'; @@ -87,6 +88,7 @@ const rootReducer = combineSlices( }, componentApi, sectionApi, + extensionsApi, codeComponentApi, layoutApi, previewApi, @@ -143,6 +145,7 @@ export const makeStore = (preloadedState?: Partial<RootState>) => { return getDefaultMiddleware().concat( componentApi.middleware, sectionApi.middleware, + extensionsApi.middleware, codeComponentApi.middleware, layoutApi.middleware, previewApi.middleware, diff --git a/ui/src/components/UndoRedo.tsx b/ui/src/components/UndoRedo.tsx index ac60b18479..313fbcec3a 100644 --- a/ui/src/components/UndoRedo.tsx +++ b/ui/src/components/UndoRedo.tsx @@ -8,6 +8,8 @@ import { useHotkeys } from 'react-hotkeys-hook'; import { useEffect } from 'react'; import { UndoRedoActionCreators } from '@/features/ui/uiSlice'; import { selectUndoType, selectRedoType } from '@/features/ui/uiSlice'; +import clsx from 'clsx'; +import styles from '@/components/topbar/Topbar.module.css'; const UndoRedo = () => { const dispatch = useAppDispatch(); @@ -50,24 +52,30 @@ const UndoRedo = () => { return ( <> <Button - variant="outline" + variant="ghost" color="gray" - highContrast + size="2" + className={clsx(styles.topBarButton)} onClick={() => dispatchUndo()} disabled={!isUndoable} aria-label="Undo" > - <ResetIcon /> Undo + <ResetIcon height="24" width="auto" /> </Button> <Button - variant="outline" + variant="ghost" color="gray" - highContrast + size="2" + className={clsx(styles.topBarButton)} onClick={() => dispatchRedo()} disabled={!isRedoable} aria-label="Redo" > - <ResetIcon style={{ transform: 'scaleX(-1)' }} /> Redo + <ResetIcon + height="24" + width="auto" + style={{ transform: 'scaleX(-1)' }} + /> </Button> </> ); diff --git a/ui/src/components/extensionsPopover/ExtensionButton.tsx b/ui/src/components/extensionsPopover/ExtensionButton.tsx new file mode 100644 index 0000000000..d09ef23679 --- /dev/null +++ b/ui/src/components/extensionsPopover/ExtensionButton.tsx @@ -0,0 +1,32 @@ +import clsx from 'clsx'; +import { Flex, Text } from '@radix-ui/themes'; +import styles from './ExtensionPopover.module.css'; +import type React from 'react'; +import { useCallback } from 'react'; +import { handleNonWorkingBtn } from '@/utils/function-utils'; +import type { Extension } from '@/types/Extensions'; + +interface ExtensionsPopoverProps { + extension: Extension; +} + +const ExtensionButton: React.FC<ExtensionsPopoverProps> = ({ extension }) => { + const { name, imgSrc } = extension; + const handleClick = useCallback((e: React.MouseEvent<HTMLButtonElement>) => { + e.preventDefault(); + handleNonWorkingBtn(); + }, []); + + return ( + <Flex justify="start" align="center" direction="column" asChild> + <button className={clsx(styles.extensionIcon)} onClick={handleClick}> + <img alt={name} src={imgSrc} height="42" width="42" /> + <Text align="center" size="1"> + {name} + </Text> + </button> + </Flex> + ); +}; + +export default ExtensionButton; diff --git a/ui/src/components/extensionsPopover/ExtensionPopover.module.css b/ui/src/components/extensionsPopover/ExtensionPopover.module.css new file mode 100644 index 0000000000..4edbc0c157 --- /dev/null +++ b/ui/src/components/extensionsPopover/ExtensionPopover.module.css @@ -0,0 +1,26 @@ +.content { + position: relative; + width: var(--extension-menu-width); + margin-top: var(--space-4); + background-color: var(--sand-1) !important; +} + +.extensionIcon { + margin: 0; + padding: var(--space-2); + background: none; + border-radius: 4px; + border: 1px solid transparent; + img { + border: 1px solid #ccc; + padding: var(--space-2); + margin-bottom: var(--space-2); + display: block; + border-radius: 4px; + background: var(--sand-1); + } + &:hover { + border: 1px solid var(--gray-6); + background: var(--gray-2); + } +} diff --git a/ui/src/components/extensionsPopover/ExtensionsPopover.tsx b/ui/src/components/extensionsPopover/ExtensionsPopover.tsx new file mode 100644 index 0000000000..3a29d911ae --- /dev/null +++ b/ui/src/components/extensionsPopover/ExtensionsPopover.tsx @@ -0,0 +1,90 @@ +import clsx from 'clsx'; +import topBarStyles from '@/components/topbar/Topbar.module.css'; +import { + Button, + Flex, + Heading, + Popover, + Link, + Grid, + Spinner, +} from '@radix-ui/themes'; +import ExtensionIcon from '@assets/icons/extension.svg?react'; +import Panel from '@/components/Panel'; +import styles from './ExtensionPopover.module.css'; +import { ExternalLinkIcon } from '@radix-ui/react-icons'; +import ExtensionButton from '@/components/extensionsPopover/ExtensionButton'; +import { handleNonWorkingBtn } from '@/utils/function-utils'; +import React, { useEffect } from 'react'; +import { useGetExtensionsQuery } from '@/services/extensions'; +import ErrorCard from '@/components/error/ErrorCard'; + +interface ExtensionsPopoverProps {} + +const ExtensionsPopover: React.FC<ExtensionsPopoverProps> = ({}) => { + const { + data: extensions, + isError, + error, + isLoading, + refetch, + } = useGetExtensionsQuery(); + + console.log(error); + + return ( + <Popover.Root> + <Popover.Trigger> + <Button + variant="ghost" + color="gray" + size="2" + className={clsx(topBarStyles.topBarButton)} + > + <ExtensionIcon height="24" width="auto" /> + </Button> + </Popover.Trigger> + <Popover.Content asChild> + <Panel className={clsx(styles.content, 'xb-app')}> + <Flex justify="between"> + <Heading as="h3" size="3" mb="4"> + Extensions + </Heading> + + <Flex justify="end" asChild> + <Link + size="1" + href="" + target="_blank" + onClick={(e: React.MouseEvent<HTMLAnchorElement>) => { + e.preventDefault(); + handleNonWorkingBtn(); + }} + > + Browse extensions <ExternalLinkIcon /> + </Link> + </Flex> + </Flex> + {isError && ( + <ErrorCard + error="Cannot display extensions, please try again" + resetErrorBoundary={refetch} + resetButtonText="Try again" + title="Error loading extensions" + /> + )} + {isLoading && <Spinner />} + {extensions && ( + <Grid columns="3" gap="3"> + {extensions.map((extension) => ( + <ExtensionButton extension={extension} key={extension.id} /> + ))} + </Grid> + )} + </Panel> + </Popover.Content> + </Popover.Root> + ); +}; + +export default ExtensionsPopover; diff --git a/ui/src/components/panel/ContextualPanel.module.css b/ui/src/components/panel/ContextualPanel.module.css index a040516051..a1e292694d 100644 --- a/ui/src/components/panel/ContextualPanel.module.css +++ b/ui/src/components/panel/ContextualPanel.module.css @@ -1,6 +1,6 @@ .contextualPanel { position: fixed; - top: calc(var(--topbar-height) + var(--topbar-space-above) + var(--topbar-space-below)); + top: calc(var(--topbar-height) + var(--topbar-space-below)); right: var(--topbar-space-side); width: var(--sidebar-right-width); height: var(--sidebar-height); diff --git a/ui/src/components/topbar/Topbar.module.css b/ui/src/components/topbar/Topbar.module.css index 6118874ed5..75f39583a6 100644 --- a/ui/src/components/topbar/Topbar.module.css +++ b/ui/src/components/topbar/Topbar.module.css @@ -1,8 +1,8 @@ .root { - --icon-height: 22px; + --icon-height: 24px; position: fixed; - inset: var(--topbar-space-above) var(--topbar-space-side) var(--topbar-space-below) var(--topbar-space-side); + inset: var(--topbar-space-above) 0 var(--topbar-space-below) 0; height: var(--topbar-height); transition: top 0.2s cubic-bezier(0.4, 0, 0.6, 1), @@ -10,8 +10,10 @@ right 0.2s cubic-bezier(0.4, 0, 0.6, 1), opacity 2s ease-out; transition-delay: 0.1s; - border-radius: var(--radius-6); box-shadow: var(--shadow-3); + &.root { + border-radius: 0; + } &.inPreview { top: 0; right: 0; @@ -31,11 +33,18 @@ } } -.drupalLogo { - width: auto; - height: var(--icon-height); +.topBarButton { + height: 24px !important; + width: 24px !important; + margin: 0 !important; } +.verticalDivider { + border-right: 1px solid var(--gray-a6); + height: 16px; +} + + .loading { padding-right: 10px; } diff --git a/ui/src/components/topbar/Topbar.tsx b/ui/src/components/topbar/Topbar.tsx index 1f4b8bea88..7615c044c1 100644 --- a/ui/src/components/topbar/Topbar.tsx +++ b/ui/src/components/topbar/Topbar.tsx @@ -1,19 +1,19 @@ import * as Menubar from '@radix-ui/react-menubar'; import styles from './Topbar.module.css'; -import { Button, Flex, SegmentedControl } from '@radix-ui/themes'; +import { Button, Flex, Grid, SegmentedControl } from '@radix-ui/themes'; import Panel from '@/components/Panel'; import UndoRedo from '@/components/UndoRedo'; import DropIcon from '@assets/icons/drop.svg?react'; -import { - ChevronLeftIcon, - EyeNoneIcon, - EyeOpenIcon, -} from '@radix-ui/react-icons'; +import CMSIcon from '@assets/icons/cms.svg?react'; +import { EyeNoneIcon, EyeOpenIcon } from '@radix-ui/react-icons'; import { useLocation, useNavigate } from 'react-router-dom'; import clsx from 'clsx'; import DemoPublishButton from '@/components/DemoPublishButton'; import UnpublishedChanges from '@/components/review/UnpublishedChanges'; import PageInfo from '../pageInfo/PageInfo'; +import ExtensionsPopover from '@/components/extensionsPopover/ExtensionsPopover'; +import type React from 'react'; +import { handleNonWorkingBtn } from '@/utils/function-utils'; const PREVIOUS_URL_STORAGE_KEY = 'XBPreviousURL'; @@ -43,38 +43,49 @@ const Topbar = () => { className={clsx(styles.root, { [styles.inPreview]: isPreview, })} - px="6" + px="3" > - <Flex height="100%" align="center" justify="between"> - <Button - asChild={true} - variant="ghost" - color="gray" - size="3" - data-testid="xb-back-button" - > - <a href={backHref} aria-labelledby="back-to-previous-label"> - <Flex gap="1" align="center" pr="1"> + <Grid columns="3" gap="3" width="auto" height="100%"> + <Flex align="center" justify="start" gap="2"> + <Button + asChild={true} + variant="ghost" + color="gray" + size="2" + className={clsx(styles.topBarButton)} + data-testid="xb-back-button" + > + <a href={backHref} aria-labelledby="back-to-previous-label"> <span className="visually-hidden" id="back-to-previous-label"> Exit Experience Builder </span> - <ChevronLeftIcon /> <DropIcon className={styles.drupalLogo} - height="22" + height="24" width="auto" /> - </Flex> - </a> - </Button> - {/* @todo: Keep the <AddMenu/> code to reuse for displaying module components.*/} - {/* https://www.drupal.org/project/experience_builder/issues/3482393 */} - {/*<AddMenu />*/} - <Flex gap="5" align="center" width="full" justify="center"> + </a> + </Button> + <div className={clsx(styles.verticalDivider)}></div> + + <ExtensionsPopover /> + <Button + variant="ghost" + color="gray" + size="2" + className={clsx(styles.topBarButton)} + onClick={(e: React.MouseEvent<HTMLButtonElement>) => { + e.preventDefault(); + handleNonWorkingBtn(); + }} + > + <CMSIcon height="24" width="auto" /> + </Button> + </Flex> + <Flex align="center" justify="center" gap="2"> <PageInfo /> </Flex> - - <Flex gap="4" align="center" justify="end"> + <Flex align="center" justify="end" gap="2"> {!isPreview && <UndoRedo />} {isPreview && ( <> @@ -113,7 +124,11 @@ const Topbar = () => { <DemoPublishButton /> <UnpublishedChanges /> </Flex> - </Flex> + </Grid> + + {/* /!* @todo: Keep the <AddMenu/> code to reuse for displaying module components.*!/*/} + {/* /!* https://www.drupal.org/project/experience_builder/issues/3482393 *!/*/} + {/* /!*<AddMenu />*!/*/} </Panel> </Menubar.Root> ); diff --git a/ui/src/features/canvas/Canvas.module.css b/ui/src/features/canvas/Canvas.module.css index daaddbce3c..b3e1663d5e 100644 --- a/ui/src/features/canvas/Canvas.module.css +++ b/ui/src/features/canvas/Canvas.module.css @@ -1,6 +1,6 @@ .canvasPane { position: fixed; - inset: 0; + inset: var(--topbar-height) 0 0 0; overflow: scroll; background: var(--gray-8); } diff --git a/ui/src/services/extensions.ts b/ui/src/services/extensions.ts new file mode 100644 index 0000000000..38f0d95815 --- /dev/null +++ b/ui/src/services/extensions.ts @@ -0,0 +1,55 @@ +// Need to use the React-specific entry point to import createApi +import { createApi } from '@reduxjs/toolkit/query/react'; +import { baseQuery } from '@/services/baseQuery'; +import type { ExtensionsList } from '@/types/Extensions'; + +const dummyExtensionsList = [ + { + name: 'Extension 1', + imgSrc: 'https://placekittens.com/24/24', + id: 'extension1', + }, + { + name: 'Extension with longer name 2', + imgSrc: 'https://placekittens.com/24/24', + id: 'extension2', + }, + { + name: 'Extension 3', + imgSrc: 'https://placekittens.com/24/24', + id: 'extension3', + }, + { + name: 'Extension 4', + imgSrc: 'https://placekittens.com/24/24', + id: 'extension4', + }, + { + name: 'Extension name 5', + imgSrc: 'https://placekittens.com/24/24', + id: 'extension5', + }, +]; +// Custom baseQuery function to return mock data during development +// @ts-ignore +const customBaseQuery = async (args, api, extraOptions) => { + if (args === 'xb-extensions') { + return { data: dummyExtensionsList }; + } + return baseQuery(args, api, extraOptions); +}; + +// Define a service using a base URL and expected endpoints +export const extensionsApi = createApi({ + reducerPath: 'extensionsApi', + baseQuery: customBaseQuery, + endpoints: (builder) => ({ + getExtensions: builder.query<ExtensionsList, void>({ + query: () => `xb-extensions`, + }), + }), +}); + +// Export hooks for usage in functional extensions, which are +// auto-generated based on the defined endpoints +export const { useGetExtensionsQuery } = extensionsApi; diff --git a/ui/src/styles/tokens/layout.css b/ui/src/styles/tokens/layout.css index 7c347bf5ff..c0136d0526 100644 --- a/ui/src/styles/tokens/layout.css +++ b/ui/src/styles/tokens/layout.css @@ -1,6 +1,6 @@ .xb-app { --topbar-height: 60px; - --topbar-space-above: 10px; + --topbar-space-above: 0px; --topbar-space-below: 10px; --topbar-space-side: 10px; --sidebar-height: calc(100vh - var(--topbar-height) - var(--topbar-space-above) - var(--topbar-space-below) * 2); @@ -8,6 +8,7 @@ --sidebar-right-width: 296px; --sidebar-space-side: var(--topbar-space-above); --component-insert-menu-width: 262px; + --extension-menu-width: 373px; --publish-review-width: 373px; --publish-review-max-width: 600px; --publish-review-max-height: 690px; diff --git a/ui/src/types/Extensions.ts b/ui/src/types/Extensions.ts new file mode 100644 index 0000000000..940b59bd4b --- /dev/null +++ b/ui/src/types/Extensions.ts @@ -0,0 +1,7 @@ +export interface Extension { + name: string; + imgSrc: string; + id: string; +} + +export type ExtensionsList = Extension[]; -- GitLab From bb35b68ac48dc8c314039a5c62ab95c0b0093f63 Mon Sep 17 00:00:00 2001 From: Jesse Baker <jesse.baker@acquia.com> Date: Mon, 3 Feb 2025 16:10:21 +0000 Subject: [PATCH 02/10] Added some tooltips --- .../extensionsPopover/ExtensionsPopover.tsx | 23 +++--- ui/src/components/topbar/Topbar.tsx | 74 +++++++++++-------- 2 files changed, 55 insertions(+), 42 deletions(-) diff --git a/ui/src/components/extensionsPopover/ExtensionsPopover.tsx b/ui/src/components/extensionsPopover/ExtensionsPopover.tsx index 3a29d911ae..f5b9dd8c3c 100644 --- a/ui/src/components/extensionsPopover/ExtensionsPopover.tsx +++ b/ui/src/components/extensionsPopover/ExtensionsPopover.tsx @@ -8,6 +8,7 @@ import { Link, Grid, Spinner, + Tooltip, } from '@radix-ui/themes'; import ExtensionIcon from '@assets/icons/extension.svg?react'; import Panel from '@/components/Panel'; @@ -34,16 +35,18 @@ const ExtensionsPopover: React.FC<ExtensionsPopoverProps> = ({}) => { return ( <Popover.Root> - <Popover.Trigger> - <Button - variant="ghost" - color="gray" - size="2" - className={clsx(topBarStyles.topBarButton)} - > - <ExtensionIcon height="24" width="auto" /> - </Button> - </Popover.Trigger> + <Tooltip content="Extensions"> + <Popover.Trigger> + <Button + variant="ghost" + color="gray" + size="2" + className={clsx(topBarStyles.topBarButton)} + > + <ExtensionIcon height="24" width="auto" /> + </Button> + </Popover.Trigger> + </Tooltip> <Popover.Content asChild> <Panel className={clsx(styles.content, 'xb-app')}> <Flex justify="between"> diff --git a/ui/src/components/topbar/Topbar.tsx b/ui/src/components/topbar/Topbar.tsx index 7615c044c1..a3ea23b6a0 100644 --- a/ui/src/components/topbar/Topbar.tsx +++ b/ui/src/components/topbar/Topbar.tsx @@ -1,6 +1,12 @@ import * as Menubar from '@radix-ui/react-menubar'; import styles from './Topbar.module.css'; -import { Button, Flex, Grid, SegmentedControl } from '@radix-ui/themes'; +import { + Button, + Flex, + Grid, + SegmentedControl, + Tooltip, +} from '@radix-ui/themes'; import Panel from '@/components/Panel'; import UndoRedo from '@/components/UndoRedo'; import DropIcon from '@assets/icons/drop.svg?react'; @@ -47,40 +53,44 @@ const Topbar = () => { > <Grid columns="3" gap="3" width="auto" height="100%"> <Flex align="center" justify="start" gap="2"> - <Button - asChild={true} - variant="ghost" - color="gray" - size="2" - className={clsx(styles.topBarButton)} - data-testid="xb-back-button" - > - <a href={backHref} aria-labelledby="back-to-previous-label"> - <span className="visually-hidden" id="back-to-previous-label"> - Exit Experience Builder - </span> - <DropIcon - className={styles.drupalLogo} - height="24" - width="auto" - /> - </a> - </Button> + <Tooltip content="Exit Experience Builder"> + <Button + asChild={true} + variant="ghost" + color="gray" + size="2" + className={clsx(styles.topBarButton)} + data-testid="xb-back-button" + > + <a href={backHref} aria-labelledby="back-to-previous-label"> + <span className="visually-hidden" id="back-to-previous-label"> + Exit Experience Builder + </span> + <DropIcon + className={styles.drupalLogo} + height="24" + width="auto" + /> + </a> + </Button> + </Tooltip> <div className={clsx(styles.verticalDivider)}></div> <ExtensionsPopover /> - <Button - variant="ghost" - color="gray" - size="2" - className={clsx(styles.topBarButton)} - onClick={(e: React.MouseEvent<HTMLButtonElement>) => { - e.preventDefault(); - handleNonWorkingBtn(); - }} - > - <CMSIcon height="24" width="auto" /> - </Button> + <Tooltip content="CMS"> + <Button + variant="ghost" + color="gray" + size="2" + className={clsx(styles.topBarButton)} + onClick={(e: React.MouseEvent<HTMLButtonElement>) => { + e.preventDefault(); + handleNonWorkingBtn(); + }} + > + <CMSIcon height="24" width="auto" /> + </Button> + </Tooltip> </Flex> <Flex align="center" justify="center" gap="2"> <PageInfo /> -- GitLab From 19a160a3f6307e3944f5612db696d04a33a2f0ef Mon Sep 17 00:00:00 2001 From: Jesse Baker <jesse.baker@acquia.com> Date: Mon, 3 Feb 2025 16:17:52 +0000 Subject: [PATCH 03/10] minor --- ui/src/components/extensionsPopover/ExtensionsPopover.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/ui/src/components/extensionsPopover/ExtensionsPopover.tsx b/ui/src/components/extensionsPopover/ExtensionsPopover.tsx index f5b9dd8c3c..3ab6c89c9b 100644 --- a/ui/src/components/extensionsPopover/ExtensionsPopover.tsx +++ b/ui/src/components/extensionsPopover/ExtensionsPopover.tsx @@ -16,7 +16,7 @@ import styles from './ExtensionPopover.module.css'; import { ExternalLinkIcon } from '@radix-ui/react-icons'; import ExtensionButton from '@/components/extensionsPopover/ExtensionButton'; import { handleNonWorkingBtn } from '@/utils/function-utils'; -import React, { useEffect } from 'react'; +import type React from 'react'; import { useGetExtensionsQuery } from '@/services/extensions'; import ErrorCard from '@/components/error/ErrorCard'; @@ -26,13 +26,10 @@ const ExtensionsPopover: React.FC<ExtensionsPopoverProps> = ({}) => { const { data: extensions, isError, - error, isLoading, refetch, } = useGetExtensionsQuery(); - console.log(error); - return ( <Popover.Root> <Tooltip content="Extensions"> @@ -70,7 +67,7 @@ const ExtensionsPopover: React.FC<ExtensionsPopoverProps> = ({}) => { </Flex> {isError && ( <ErrorCard - error="Cannot display extensions, please try again" + error="Cannot display extensions, please try again." resetErrorBoundary={refetch} resetButtonText="Try again" title="Error loading extensions" -- GitLab From c522d4ff0f8457e70a8f6cb8df90a1941947dc1d Mon Sep 17 00:00:00 2001 From: Jesse Baker <jesse.baker@acquia.com> Date: Tue, 4 Feb 2025 11:13:55 +0000 Subject: [PATCH 04/10] refactor, added storybook --- .../extensionsPopover/ExtensionButton.tsx | 2 +- ...r.module.css => ExtensionsList.module.css} | 7 -- .../extensionsPopover/ExtensionsList.tsx | 89 ++++++++++++++++++ .../ExtensionsListDisplay.stories.tsx | 91 +++++++++++++++++++ .../extensionsPopover/ExtensionsPopover.tsx | 90 ------------------ ui/src/components/topbar/Topbar.tsx | 56 ++++++++---- .../topbar/menu/TopbarPopover.module.css | 6 ++ .../components/topbar/menu/TopbarPopover.tsx | 31 +++++++ 8 files changed, 257 insertions(+), 115 deletions(-) rename ui/src/components/extensionsPopover/{ExtensionPopover.module.css => ExtensionsList.module.css} (72%) create mode 100644 ui/src/components/extensionsPopover/ExtensionsList.tsx create mode 100644 ui/src/components/extensionsPopover/ExtensionsListDisplay.stories.tsx delete mode 100644 ui/src/components/extensionsPopover/ExtensionsPopover.tsx create mode 100644 ui/src/components/topbar/menu/TopbarPopover.module.css create mode 100644 ui/src/components/topbar/menu/TopbarPopover.tsx diff --git a/ui/src/components/extensionsPopover/ExtensionButton.tsx b/ui/src/components/extensionsPopover/ExtensionButton.tsx index d09ef23679..f639da7f41 100644 --- a/ui/src/components/extensionsPopover/ExtensionButton.tsx +++ b/ui/src/components/extensionsPopover/ExtensionButton.tsx @@ -1,6 +1,6 @@ import clsx from 'clsx'; import { Flex, Text } from '@radix-ui/themes'; -import styles from './ExtensionPopover.module.css'; +import styles from './ExtensionsList.module.css'; import type React from 'react'; import { useCallback } from 'react'; import { handleNonWorkingBtn } from '@/utils/function-utils'; diff --git a/ui/src/components/extensionsPopover/ExtensionPopover.module.css b/ui/src/components/extensionsPopover/ExtensionsList.module.css similarity index 72% rename from ui/src/components/extensionsPopover/ExtensionPopover.module.css rename to ui/src/components/extensionsPopover/ExtensionsList.module.css index 4edbc0c157..fd8ccbbb05 100644 --- a/ui/src/components/extensionsPopover/ExtensionPopover.module.css +++ b/ui/src/components/extensionsPopover/ExtensionsList.module.css @@ -1,10 +1,3 @@ -.content { - position: relative; - width: var(--extension-menu-width); - margin-top: var(--space-4); - background-color: var(--sand-1) !important; -} - .extensionIcon { margin: 0; padding: var(--space-2); diff --git a/ui/src/components/extensionsPopover/ExtensionsList.tsx b/ui/src/components/extensionsPopover/ExtensionsList.tsx new file mode 100644 index 0000000000..aa1445f133 --- /dev/null +++ b/ui/src/components/extensionsPopover/ExtensionsList.tsx @@ -0,0 +1,89 @@ +import { Flex, Heading, Link, Grid, Spinner } from '@radix-ui/themes'; +import { ExternalLinkIcon } from '@radix-ui/react-icons'; +import ExtensionButton from '@/components/extensionsPopover/ExtensionButton'; +import { handleNonWorkingBtn } from '@/utils/function-utils'; +import type React from 'react'; +import { useGetExtensionsQuery } from '@/services/extensions'; +import ErrorCard from '@/components/error/ErrorCard'; + +interface ExtensionsPopoverProps {} + +const ExtensionsList: React.FC<ExtensionsPopoverProps> = ({}) => { + const { + data: extensions, + isError, + isLoading, + refetch, + } = useGetExtensionsQuery(); + + return ( + <ExtensionsListDisplay + extensions={extensions || []} + isLoading={isLoading} + isError={isError} + refetch={refetch} + /> + ); +}; + +interface ExtensionsListDisplayProps { + extensions: Array<any>; + isLoading: boolean; + isError: boolean; + refetch: () => void; +} + +const ExtensionsListDisplay: React.FC<ExtensionsListDisplayProps> = ({ + extensions, + isLoading, + isError, + refetch, +}) => { + return ( + <> + <Flex justify="between"> + <Heading as="h3" size="3" mb="4"> + Extensions + </Heading> + + <Flex justify="end" asChild> + <Link + size="1" + href="" + target="_blank" + onClick={(e: React.MouseEvent<HTMLAnchorElement>) => { + e.preventDefault(); + handleNonWorkingBtn(); + }} + > + Browse extensions <ExternalLinkIcon /> + </Link> + </Flex> + </Flex> + {isError && ( + <ErrorCard + error="Cannot display extensions, please try again." + resetErrorBoundary={refetch} + resetButtonText="Try again" + title="Error loading extensions" + /> + )} + {isLoading && ( + <Flex justify="center"> + <Spinner /> + </Flex> + )} + {extensions && ( + <Grid columns="3" gap="3"> + {extensions.map((extension) => ( + <ExtensionButton extension={extension} key={extension.id} /> + ))} + </Grid> + )} + </> + ); +}; + +export { ExtensionsListDisplay }; + +export default ExtensionsList; diff --git a/ui/src/components/extensionsPopover/ExtensionsListDisplay.stories.tsx b/ui/src/components/extensionsPopover/ExtensionsListDisplay.stories.tsx new file mode 100644 index 0000000000..c8ec0ff34f --- /dev/null +++ b/ui/src/components/extensionsPopover/ExtensionsListDisplay.stories.tsx @@ -0,0 +1,91 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { ExtensionsListDisplay } from '@/components/extensionsPopover/ExtensionsList'; + +const mockExtensions = [ + { + name: 'Extension 1', + imgSrc: 'https://placekittens.com/24/24', + id: 'extension1', + }, + { + name: 'Extension with longer name 2', + imgSrc: 'https://placekittens.com/24/24', + id: 'extension2', + }, + { + name: 'Extension 3', + imgSrc: 'https://placekittens.com/24/24', + id: 'extension3', + }, + { + name: 'Extension 4', + imgSrc: 'https://placekittens.com/24/24', + id: 'extension4', + }, + { + name: 'Extension name 5', + imgSrc: 'https://placekittens.com/24/24', + id: 'extension5', + }, +]; + +const meta: Meta<typeof ExtensionsListDisplay> = { + title: 'Components/ExtensionsList', + component: ExtensionsListDisplay, + args: { + extensions: mockExtensions, + isLoading: false, + isError: false, + refetch: () => {}, + }, + argTypes: { + extensions: { + control: { type: 'object' }, + }, + isLoading: { + control: { type: 'boolean' }, + }, + isError: { + control: { type: 'boolean' }, + }, + }, + decorators: [ + (Story) => ( + <div + style={{ + maxWidth: '375px', + background: '#fff', + padding: '1em', + }} + > + <Story /> + </div> + ), + ], +}; + +export default meta; + +type Story = StoryObj<typeof ExtensionsListDisplay>; + +export const Default: Story = {}; + +export const Loading: Story = { + args: { + extensions: [], + isLoading: true, + isError: false, + refetch: () => {}, + }, +}; + +export const Error: Story = { + args: { + extensions: [], + isLoading: false, + isError: true, + refetch: () => { + alert('Dummy refetch'); + }, + }, +}; diff --git a/ui/src/components/extensionsPopover/ExtensionsPopover.tsx b/ui/src/components/extensionsPopover/ExtensionsPopover.tsx deleted file mode 100644 index 3ab6c89c9b..0000000000 --- a/ui/src/components/extensionsPopover/ExtensionsPopover.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import clsx from 'clsx'; -import topBarStyles from '@/components/topbar/Topbar.module.css'; -import { - Button, - Flex, - Heading, - Popover, - Link, - Grid, - Spinner, - Tooltip, -} from '@radix-ui/themes'; -import ExtensionIcon from '@assets/icons/extension.svg?react'; -import Panel from '@/components/Panel'; -import styles from './ExtensionPopover.module.css'; -import { ExternalLinkIcon } from '@radix-ui/react-icons'; -import ExtensionButton from '@/components/extensionsPopover/ExtensionButton'; -import { handleNonWorkingBtn } from '@/utils/function-utils'; -import type React from 'react'; -import { useGetExtensionsQuery } from '@/services/extensions'; -import ErrorCard from '@/components/error/ErrorCard'; - -interface ExtensionsPopoverProps {} - -const ExtensionsPopover: React.FC<ExtensionsPopoverProps> = ({}) => { - const { - data: extensions, - isError, - isLoading, - refetch, - } = useGetExtensionsQuery(); - - return ( - <Popover.Root> - <Tooltip content="Extensions"> - <Popover.Trigger> - <Button - variant="ghost" - color="gray" - size="2" - className={clsx(topBarStyles.topBarButton)} - > - <ExtensionIcon height="24" width="auto" /> - </Button> - </Popover.Trigger> - </Tooltip> - <Popover.Content asChild> - <Panel className={clsx(styles.content, 'xb-app')}> - <Flex justify="between"> - <Heading as="h3" size="3" mb="4"> - Extensions - </Heading> - - <Flex justify="end" asChild> - <Link - size="1" - href="" - target="_blank" - onClick={(e: React.MouseEvent<HTMLAnchorElement>) => { - e.preventDefault(); - handleNonWorkingBtn(); - }} - > - Browse extensions <ExternalLinkIcon /> - </Link> - </Flex> - </Flex> - {isError && ( - <ErrorCard - error="Cannot display extensions, please try again." - resetErrorBoundary={refetch} - resetButtonText="Try again" - title="Error loading extensions" - /> - )} - {isLoading && <Spinner />} - {extensions && ( - <Grid columns="3" gap="3"> - {extensions.map((extension) => ( - <ExtensionButton extension={extension} key={extension.id} /> - ))} - </Grid> - )} - </Panel> - </Popover.Content> - </Popover.Root> - ); -}; - -export default ExtensionsPopover; diff --git a/ui/src/components/topbar/Topbar.tsx b/ui/src/components/topbar/Topbar.tsx index 8db7232c6d..72cb59bdc0 100644 --- a/ui/src/components/topbar/Topbar.tsx +++ b/ui/src/components/topbar/Topbar.tsx @@ -5,20 +5,24 @@ import { Flex, Grid, SegmentedControl, + Text, Tooltip, } from '@radix-ui/themes'; import Panel from '@/components/Panel'; import UndoRedo from '@/components/UndoRedo'; import DropIcon from '@assets/icons/drop.svg?react'; import CMSIcon from '@assets/icons/cms.svg?react'; +import ExtensionIcon from '@assets/icons/extension.svg?react'; import { EyeNoneIcon, EyeOpenIcon } from '@radix-ui/react-icons'; import { useLocation, useNavigate } from 'react-router-dom'; import clsx from 'clsx'; import UnpublishedChanges from '@/components/review/UnpublishedChanges'; import PageInfo from '../pageInfo/PageInfo'; -import ExtensionsPopover from '@/components/extensionsPopover/ExtensionsPopover'; +import ExtensionsList from '@/components/extensionsPopover/ExtensionsList'; import type React from 'react'; import { handleNonWorkingBtn } from '@/utils/function-utils'; +import TopbarPopover from '@/components/topbar/menu/TopbarPopover'; +import topBarStyles from '@/components/topbar/Topbar.module.css'; const PREVIOUS_URL_STORAGE_KEY = 'XBPreviousURL'; @@ -74,22 +78,40 @@ const Topbar = () => { </Button> </Tooltip> <div className={clsx(styles.verticalDivider)}></div> - - <ExtensionsPopover /> - <Tooltip content="CMS"> - <Button - variant="ghost" - color="gray" - size="2" - className={clsx(styles.topBarButton)} - onClick={(e: React.MouseEvent<HTMLButtonElement>) => { - e.preventDefault(); - handleNonWorkingBtn(); - }} - > - <CMSIcon height="24" width="auto" /> - </Button> - </Tooltip> + <TopbarPopover + tooltip="Extensions" + trigger={ + <Button + variant="ghost" + color="gray" + size="2" + className={clsx(topBarStyles.topBarButton)} + > + <ExtensionIcon height="24" width="auto" /> + </Button> + } + > + <ExtensionsList /> + </TopbarPopover> + <TopbarPopover + tooltip="CMS" + trigger={ + <Button + variant="ghost" + color="gray" + size="2" + className={clsx(styles.topBarButton)} + onClick={(e: React.MouseEvent<HTMLButtonElement>) => { + e.preventDefault(); + handleNonWorkingBtn(); + }} + > + <CMSIcon height="24" width="auto" /> + </Button> + } + > + <Text>Not yet supported</Text> + </TopbarPopover> </Flex> <Flex align="center" justify="center" gap="2"> <PageInfo /> diff --git a/ui/src/components/topbar/menu/TopbarPopover.module.css b/ui/src/components/topbar/menu/TopbarPopover.module.css new file mode 100644 index 0000000000..2eb22cbc5a --- /dev/null +++ b/ui/src/components/topbar/menu/TopbarPopover.module.css @@ -0,0 +1,6 @@ +.content { + position: relative; + width: var(--extension-menu-width); + margin-top: var(--space-4); + background-color: var(--sand-1) !important; +} diff --git a/ui/src/components/topbar/menu/TopbarPopover.tsx b/ui/src/components/topbar/menu/TopbarPopover.tsx new file mode 100644 index 0000000000..8cf06e9761 --- /dev/null +++ b/ui/src/components/topbar/menu/TopbarPopover.tsx @@ -0,0 +1,31 @@ +import clsx from 'clsx'; +import { Popover, Tooltip } from '@radix-ui/themes'; +import Panel from '@/components/Panel'; +import styles from './TopbarPopover.module.css'; +import type React from 'react'; +import type { ReactNode } from 'react'; + +interface TopbarPopoverProps { + trigger: ReactNode; + children: ReactNode; + tooltip: string; +} + +const TopbarPopover: React.FC<TopbarPopoverProps> = ({ + trigger, + children, + tooltip, +}) => { + return ( + <Popover.Root> + <Tooltip content={tooltip}> + <Popover.Trigger>{trigger}</Popover.Trigger> + </Tooltip> + <Popover.Content asChild> + <Panel className={clsx(styles.content, 'xb-app')}>{children}</Panel> + </Popover.Content> + </Popover.Root> + ); +}; + +export default TopbarPopover; -- GitLab From 01e418252214281f5d16ff140a9a91a17fe3f5a8 Mon Sep 17 00:00:00 2001 From: Jesse Baker <jesse.baker@acquia.com> Date: Tue, 4 Feb 2025 11:30:34 +0000 Subject: [PATCH 05/10] lint --- .../extensionsPopover/ExtensionsList.module.css | 12 ++++++------ .../components/extensionsPopover/ExtensionsList.tsx | 2 +- ui/src/components/topbar/Topbar.module.css | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/ui/src/components/extensionsPopover/ExtensionsList.module.css b/ui/src/components/extensionsPopover/ExtensionsList.module.css index fd8ccbbb05..90ca22e5f0 100644 --- a/ui/src/components/extensionsPopover/ExtensionsList.module.css +++ b/ui/src/components/extensionsPopover/ExtensionsList.module.css @@ -1,15 +1,15 @@ .extensionIcon { margin: 0; padding: var(--space-2); - background: none; - border-radius: 4px; border: 1px solid transparent; + border-radius: var(--radius-4); + background: none; img { - border: 1px solid #ccc; - padding: var(--space-2); - margin-bottom: var(--space-2); display: block; - border-radius: 4px; + margin-bottom: var(--space-2); + padding: var(--space-2); + border: 1px solid var(--gray-6); + border-radius: var(--radius-3); background: var(--sand-1); } &:hover { diff --git a/ui/src/components/extensionsPopover/ExtensionsList.tsx b/ui/src/components/extensionsPopover/ExtensionsList.tsx index aa1445f133..364e3ec842 100644 --- a/ui/src/components/extensionsPopover/ExtensionsList.tsx +++ b/ui/src/components/extensionsPopover/ExtensionsList.tsx @@ -8,7 +8,7 @@ import ErrorCard from '@/components/error/ErrorCard'; interface ExtensionsPopoverProps {} -const ExtensionsList: React.FC<ExtensionsPopoverProps> = ({}) => { +const ExtensionsList: React.FC<ExtensionsPopoverProps> = () => { const { data: extensions, isError, diff --git a/ui/src/components/topbar/Topbar.module.css b/ui/src/components/topbar/Topbar.module.css index d109d06cca..c52f358e12 100644 --- a/ui/src/components/topbar/Topbar.module.css +++ b/ui/src/components/topbar/Topbar.module.css @@ -34,13 +34,13 @@ } .topBarButton { - height: var(--icon-height) !important; width: var(--icon-height) !important; + height: var(--icon-height) !important; margin: 0 !important; } .verticalDivider { - border-right: 1px solid var(--gray-a6); height: 16px; + border-right: 1px solid var(--gray-a6); } -- GitLab From 3f951b2c252a4a6edc877686aec31c4edc417a05 Mon Sep 17 00:00:00 2001 From: Jesse Baker <jesse.baker@acquia.com> Date: Tue, 4 Feb 2025 15:01:10 +0000 Subject: [PATCH 06/10] Added a draggable non-modal dialog --- .../ExtensionButton.tsx | 16 ++- .../extensions/ExtensionDialog.module.css | 36 +++++ .../components/extensions/ExtensionDialog.tsx | 126 ++++++++++++++++++ .../ExtensionsList.module.css | 0 .../ExtensionsList.tsx | 2 +- .../ExtensionsListDisplay.stories.tsx | 2 +- ui/src/components/topbar/Topbar.tsx | 2 +- ui/src/features/editor/Editor.tsx | 2 + ui/src/features/ui/dialogSlice.ts | 4 +- 9 files changed, 181 insertions(+), 9 deletions(-) rename ui/src/components/{extensionsPopover => extensions}/ExtensionButton.tsx (70%) create mode 100644 ui/src/components/extensions/ExtensionDialog.module.css create mode 100644 ui/src/components/extensions/ExtensionDialog.tsx rename ui/src/components/{extensionsPopover => extensions}/ExtensionsList.module.css (100%) rename ui/src/components/{extensionsPopover => extensions}/ExtensionsList.tsx (96%) rename ui/src/components/{extensionsPopover => extensions}/ExtensionsListDisplay.stories.tsx (95%) diff --git a/ui/src/components/extensionsPopover/ExtensionButton.tsx b/ui/src/components/extensions/ExtensionButton.tsx similarity index 70% rename from ui/src/components/extensionsPopover/ExtensionButton.tsx rename to ui/src/components/extensions/ExtensionButton.tsx index f639da7f41..9674a37dc4 100644 --- a/ui/src/components/extensionsPopover/ExtensionButton.tsx +++ b/ui/src/components/extensions/ExtensionButton.tsx @@ -3,8 +3,9 @@ import { Flex, Text } from '@radix-ui/themes'; import styles from './ExtensionsList.module.css'; import type React from 'react'; import { useCallback } from 'react'; -import { handleNonWorkingBtn } from '@/utils/function-utils'; import type { Extension } from '@/types/Extensions'; +import { useAppDispatch } from '@/app/hooks'; +import { setDialogOpen } from '@/features/ui/dialogSlice'; interface ExtensionsPopoverProps { extension: Extension; @@ -12,10 +13,15 @@ interface ExtensionsPopoverProps { const ExtensionButton: React.FC<ExtensionsPopoverProps> = ({ extension }) => { const { name, imgSrc } = extension; - const handleClick = useCallback((e: React.MouseEvent<HTMLButtonElement>) => { - e.preventDefault(); - handleNonWorkingBtn(); - }, []); + const dispatch = useAppDispatch(); + + const handleClick = useCallback( + (e: React.MouseEvent<HTMLButtonElement>) => { + e.preventDefault(); + dispatch(setDialogOpen('extension')); + }, + [dispatch], + ); return ( <Flex justify="start" align="center" direction="column" asChild> diff --git a/ui/src/components/extensions/ExtensionDialog.module.css b/ui/src/components/extensions/ExtensionDialog.module.css new file mode 100644 index 0000000000..c3cf6d198e --- /dev/null +++ b/ui/src/components/extensions/ExtensionDialog.module.css @@ -0,0 +1,36 @@ +.DialogContent { + position: fixed; + top: 0; + left: 0; + width: 90vw; + max-width: 500px; + max-height: 85vh; + padding: 25px; + animation: content-show 150ms cubic-bezier(0.16, 1, 0.3, 1); + border-radius: 6px; + background-color: var(--gray-1); + box-shadow: var(--shadow-6); +} +.DialogContent:focus { + outline: none; +} + +@keyframes overlay-show { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes content-show { + from { + transform: translate(-50%, -48%) scale(0.96); + opacity: 0; + } + to { + transform: translate(-50%, -50%) scale(1); + opacity: 1; + } +} diff --git a/ui/src/components/extensions/ExtensionDialog.tsx b/ui/src/components/extensions/ExtensionDialog.tsx new file mode 100644 index 0000000000..2c04d263f0 --- /dev/null +++ b/ui/src/components/extensions/ExtensionDialog.tsx @@ -0,0 +1,126 @@ +import { Dialog } from 'radix-ui'; +import React, { useCallback, useRef, useState } from 'react'; + +import clsx from 'clsx'; +import styles from './ExtensionDialog.module.css'; +import { Box, Button, Flex, Heading, Theme } from '@radix-ui/themes'; +import { useAppDispatch, useAppSelector } from '@/app/hooks'; +import { + selectDialogOpen, + setDialogClosed, + setDialogOpen, +} from '@/features/ui/dialogSlice'; +import Panel from '@/components/Panel'; + +interface ExtensionDialogProps {} + +const ExtensionDialog: React.FC<ExtensionDialogProps> = () => { + const { extension } = useAppSelector(selectDialogOpen); + const dispatch = useAppDispatch(); + const handleOpenChange = useCallback( + (open: boolean) => { + open + ? dispatch(setDialogOpen('extension')) + : dispatch(setDialogClosed('extension')); + }, + [dispatch], + ); + + const dialogRef = useRef<HTMLDivElement | null>(null); + const windowWidth = window.visualViewport?.width || 100; + const [position, setPosition] = useState({ + x: windowWidth / 2 - 250, + y: 200, + }); + const [isDragging, setIsDragging] = useState(false); + + const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => { + setIsDragging(true); + }; + + const handleMouseMove = useCallback( + (e: MouseEvent) => { + if (isDragging && dialogRef.current) { + setPosition((prevPosition) => ({ + x: prevPosition.x + e.movementX, + y: prevPosition.y + e.movementY, + })); + } + }, + [isDragging], + ); + + const handleMouseUp = () => { + setIsDragging(false); + }; + + React.useEffect(() => { + if (isDragging) { + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseup', handleMouseUp); + } else { + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', handleMouseUp); + } + + return () => { + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', handleMouseUp); + }; + }, [handleMouseMove, isDragging]); + + return ( + <Dialog.Root modal={false} open={extension} onOpenChange={handleOpenChange}> + <Dialog.Portal> + <Theme> + <Dialog.Content + className={clsx(styles.DialogContent)} + asChild + style={{ + transform: `translate(${position.x}px, ${position.y}px)`, + position: 'absolute', + }} + onPointerDownOutside={(event) => { + event.preventDefault(); + }} + onInteractOutside={(event) => { + event.preventDefault(); + }} + ref={dialogRef} + > + <Panel> + <Box + mt="-5" + pt="5" + mx="-5" + px="5" + className={styles.DraggableArea} + onMouseDown={handleMouseDown} + style={{ + cursor: 'move', + }} + > + <Dialog.Title asChild> + <Heading as="h3" size="4"> + Extension + </Heading> + </Dialog.Title> + </Box> + <Dialog.Description className={clsx(styles.DialogDescription)}> + {/* @todo https://www.drupal.org/i/3485692 - render the proof of concept into this div */} + <div id="extensionPortalContainer">Not yet supported</div> + </Dialog.Description> + <Flex gap="2" justify="end"> + <Dialog.Close asChild> + <Button>Close</Button> + </Dialog.Close> + </Flex> + </Panel> + </Dialog.Content> + </Theme> + </Dialog.Portal> + </Dialog.Root> + ); +}; + +export default ExtensionDialog; diff --git a/ui/src/components/extensionsPopover/ExtensionsList.module.css b/ui/src/components/extensions/ExtensionsList.module.css similarity index 100% rename from ui/src/components/extensionsPopover/ExtensionsList.module.css rename to ui/src/components/extensions/ExtensionsList.module.css diff --git a/ui/src/components/extensionsPopover/ExtensionsList.tsx b/ui/src/components/extensions/ExtensionsList.tsx similarity index 96% rename from ui/src/components/extensionsPopover/ExtensionsList.tsx rename to ui/src/components/extensions/ExtensionsList.tsx index 364e3ec842..332da4ed39 100644 --- a/ui/src/components/extensionsPopover/ExtensionsList.tsx +++ b/ui/src/components/extensions/ExtensionsList.tsx @@ -1,6 +1,6 @@ import { Flex, Heading, Link, Grid, Spinner } from '@radix-ui/themes'; import { ExternalLinkIcon } from '@radix-ui/react-icons'; -import ExtensionButton from '@/components/extensionsPopover/ExtensionButton'; +import ExtensionButton from '@/components/extensions/ExtensionButton'; import { handleNonWorkingBtn } from '@/utils/function-utils'; import type React from 'react'; import { useGetExtensionsQuery } from '@/services/extensions'; diff --git a/ui/src/components/extensionsPopover/ExtensionsListDisplay.stories.tsx b/ui/src/components/extensions/ExtensionsListDisplay.stories.tsx similarity index 95% rename from ui/src/components/extensionsPopover/ExtensionsListDisplay.stories.tsx rename to ui/src/components/extensions/ExtensionsListDisplay.stories.tsx index c8ec0ff34f..9898925bad 100644 --- a/ui/src/components/extensionsPopover/ExtensionsListDisplay.stories.tsx +++ b/ui/src/components/extensions/ExtensionsListDisplay.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from '@storybook/react'; -import { ExtensionsListDisplay } from '@/components/extensionsPopover/ExtensionsList'; +import { ExtensionsListDisplay } from '@/components/extensions/ExtensionsList'; const mockExtensions = [ { diff --git a/ui/src/components/topbar/Topbar.tsx b/ui/src/components/topbar/Topbar.tsx index 72cb59bdc0..783d601dec 100644 --- a/ui/src/components/topbar/Topbar.tsx +++ b/ui/src/components/topbar/Topbar.tsx @@ -18,7 +18,7 @@ import { useLocation, useNavigate } from 'react-router-dom'; import clsx from 'clsx'; import UnpublishedChanges from '@/components/review/UnpublishedChanges'; import PageInfo from '../pageInfo/PageInfo'; -import ExtensionsList from '@/components/extensionsPopover/ExtensionsList'; +import ExtensionsList from '@/components/extensions/ExtensionsList'; import type React from 'react'; import { handleNonWorkingBtn } from '@/utils/function-utils'; import TopbarPopover from '@/components/topbar/menu/TopbarPopover'; diff --git a/ui/src/features/editor/Editor.tsx b/ui/src/features/editor/Editor.tsx index 3e7afb6e4e..54e7cbaeec 100644 --- a/ui/src/features/editor/Editor.tsx +++ b/ui/src/features/editor/Editor.tsx @@ -7,6 +7,7 @@ import ContextualPanel from '@/components/panel/ContextualPanel'; import { useEffect } from 'react'; import { setFirstLoadComplete } from '@/features/ui/uiSlice'; import { useAppDispatch } from '@/app/hooks'; +import ExtensionDialog from '@/components/extensions/ExtensionDialog'; const Editor = () => { const dispatch = useAppDispatch(); @@ -26,6 +27,7 @@ const Editor = () => { <div id="menuBarSubmenuContainer"></div> <SaveSectionDialog /> <CodeComponentDialogs /> + <ExtensionDialog /> </> ); }; diff --git a/ui/src/features/ui/dialogSlice.ts b/ui/src/features/ui/dialogSlice.ts index 1b89609e21..80dc1dc0fb 100644 --- a/ui/src/features/ui/dialogSlice.ts +++ b/ui/src/features/ui/dialogSlice.ts @@ -3,13 +3,15 @@ import type { PayloadAction } from '@reduxjs/toolkit'; export interface DialogSliceState { saveAsSection: boolean; + extension: boolean; } const initialState: DialogSliceState = { saveAsSection: false, + extension: false, }; -type UpdateDialogPayload = 'saveAsSection'; // only one dialog so far +type UpdateDialogPayload = keyof DialogSliceState; export const dialogSlice = createAppSlice({ name: 'dialog', -- GitLab From ade4c31862618a9c99fc1b61dabbd6db3aa7c951 Mon Sep 17 00:00:00 2001 From: Jesse Baker <jesse.baker@acquia.com> Date: Mon, 10 Feb 2025 10:30:27 +0000 Subject: [PATCH 07/10] minor --- .../extensions/ExtensionDialog.module.css | 13 +------- .../components/extensions/ExtensionDialog.tsx | 33 +++++++++++-------- 2 files changed, 21 insertions(+), 25 deletions(-) diff --git a/ui/src/components/extensions/ExtensionDialog.module.css b/ui/src/components/extensions/ExtensionDialog.module.css index c3cf6d198e..4f4f9a0d60 100644 --- a/ui/src/components/extensions/ExtensionDialog.module.css +++ b/ui/src/components/extensions/ExtensionDialog.module.css @@ -6,7 +6,7 @@ max-width: 500px; max-height: 85vh; padding: 25px; - animation: content-show 150ms cubic-bezier(0.16, 1, 0.3, 1); + animation: content-show 250ms cubic-bezier(0.16, 1, 0.3, 1); border-radius: 6px; background-color: var(--gray-1); box-shadow: var(--shadow-6); @@ -15,22 +15,11 @@ outline: none; } -@keyframes overlay-show { - from { - opacity: 0; - } - to { - opacity: 1; - } -} - @keyframes content-show { from { - transform: translate(-50%, -48%) scale(0.96); opacity: 0; } to { - transform: translate(-50%, -50%) scale(1); opacity: 1; } } diff --git a/ui/src/components/extensions/ExtensionDialog.tsx b/ui/src/components/extensions/ExtensionDialog.tsx index 2c04d263f0..0786d46997 100644 --- a/ui/src/components/extensions/ExtensionDialog.tsx +++ b/ui/src/components/extensions/ExtensionDialog.tsx @@ -3,7 +3,7 @@ import React, { useCallback, useRef, useState } from 'react'; import clsx from 'clsx'; import styles from './ExtensionDialog.module.css'; -import { Box, Button, Flex, Heading, Theme } from '@radix-ui/themes'; +import { Box, Button, Flex, Heading, Text, Theme } from '@radix-ui/themes'; import { useAppDispatch, useAppSelector } from '@/app/hooks'; import { selectDialogOpen, @@ -16,7 +16,15 @@ interface ExtensionDialogProps {} const ExtensionDialog: React.FC<ExtensionDialogProps> = () => { const { extension } = useAppSelector(selectDialogOpen); + const windowWidth = window.visualViewport?.width || 100; const dispatch = useAppDispatch(); + const [isDragging, setIsDragging] = useState(false); + const [position, setPosition] = useState({ + x: windowWidth / 2 - 250, + y: 200, + }); + const dialogRef = useRef<HTMLDivElement | null>(null); + const handleOpenChange = useCallback( (open: boolean) => { open @@ -25,15 +33,6 @@ const ExtensionDialog: React.FC<ExtensionDialogProps> = () => { }, [dispatch], ); - - const dialogRef = useRef<HTMLDivElement | null>(null); - const windowWidth = window.visualViewport?.width || 100; - const [position, setPosition] = useState({ - x: windowWidth / 2 - 250, - y: 200, - }); - const [isDragging, setIsDragging] = useState(false); - const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => { setIsDragging(true); }; @@ -69,6 +68,11 @@ const ExtensionDialog: React.FC<ExtensionDialogProps> = () => { }; }, [handleMouseMove, isDragging]); + if (!extension) { + return null; + } + console.log('Rendering', position); + return ( <Dialog.Root modal={false} open={extension} onOpenChange={handleOpenChange}> <Dialog.Portal> @@ -102,14 +106,17 @@ const ExtensionDialog: React.FC<ExtensionDialogProps> = () => { > <Dialog.Title asChild> <Heading as="h3" size="4"> - Extension + Extension proof of concept </Heading> </Dialog.Title> </Box> <Dialog.Description className={clsx(styles.DialogDescription)}> - {/* @todo https://www.drupal.org/i/3485692 - render the proof of concept into this div */} - <div id="extensionPortalContainer">Not yet supported</div> + Extension proof of concept </Dialog.Description> + {/* @todo https://www.drupal.org/i/3485692 - render the proof of concept into this div */} + <div id="extensionPortalContainer"> + <Text as="p">Not yet supported</Text> + </div> <Flex gap="2" justify="end"> <Dialog.Close asChild> <Button>Close</Button> -- GitLab From 5e7df82ac1784e6257c3c3b187722f8dc315342f Mon Sep 17 00:00:00 2001 From: Jesse Baker <jesse.baker@acquia.com> Date: Mon, 10 Feb 2025 11:18:12 +0000 Subject: [PATCH 08/10] Added a slice to store the "activeExtension" details to be passed to the dialog --- ui/src/app/store.ts | 2 + .../components/extensions/ExtensionButton.tsx | 36 +++++----- .../components/extensions/ExtensionDialog.tsx | 66 +++++++++++++------ ui/src/features/extensions/extensionsSlice.ts | 39 +++++++++++ ui/src/services/extensions.ts | 5 ++ ui/src/types/Extensions.ts | 8 ++- 6 files changed, 118 insertions(+), 38 deletions(-) create mode 100644 ui/src/features/extensions/extensionsSlice.ts diff --git a/ui/src/app/store.ts b/ui/src/app/store.ts index 29e1d247e0..4406587ee7 100644 --- a/ui/src/app/store.ts +++ b/ui/src/app/store.ts @@ -23,6 +23,7 @@ import { dummyPropsFormApi } from '@/services/dummyPropsForm'; import { pageDataFormApi } from '@/services/pageDataForm'; import { configurationSlice } from '@/features/configuration/configurationSlice'; import { sectionApi } from '@/services/sections'; +import { extensionsSlice } from '@/features/extensions/extensionsSlice'; import { extensionsApi } from '@/services/extensions'; import { codeComponentApi } from '@/services/codeComponents'; import { formStateSlice } from '@/features/form/formStateSlice'; @@ -101,6 +102,7 @@ const rootReducer = combineSlices( codeComponentDialogSlice, uiSlice, formStateSlice, + extensionsSlice, pendingChangesApi, publishReviewSlice, contentListApi, diff --git a/ui/src/components/extensions/ExtensionButton.tsx b/ui/src/components/extensions/ExtensionButton.tsx index 9674a37dc4..06f1b2deb8 100644 --- a/ui/src/components/extensions/ExtensionButton.tsx +++ b/ui/src/components/extensions/ExtensionButton.tsx @@ -1,38 +1,42 @@ import clsx from 'clsx'; -import { Flex, Text } from '@radix-ui/themes'; +import { Flex, Text, Tooltip } from '@radix-ui/themes'; import styles from './ExtensionsList.module.css'; import type React from 'react'; import { useCallback } from 'react'; -import type { Extension } from '@/types/Extensions'; +import type { ExtensionDefinition } from '@/types/Extensions'; import { useAppDispatch } from '@/app/hooks'; import { setDialogOpen } from '@/features/ui/dialogSlice'; - -interface ExtensionsPopoverProps { - extension: Extension; -} +import { setActiveExtension } from '@/features/extensions/extensionsSlice'; const ExtensionButton: React.FC<ExtensionsPopoverProps> = ({ extension }) => { - const { name, imgSrc } = extension; + const { name, imgSrc, description } = extension; const dispatch = useAppDispatch(); const handleClick = useCallback( (e: React.MouseEvent<HTMLButtonElement>) => { e.preventDefault(); dispatch(setDialogOpen('extension')); + dispatch(setActiveExtension(extension)); }, - [dispatch], + [dispatch, extension], ); return ( - <Flex justify="start" align="center" direction="column" asChild> - <button className={clsx(styles.extensionIcon)} onClick={handleClick}> - <img alt={name} src={imgSrc} height="42" width="42" /> - <Text align="center" size="1"> - {name} - </Text> - </button> - </Flex> + <Tooltip content={description}> + <Flex justify="start" align="center" direction="column" asChild> + <button className={clsx(styles.extensionIcon)} onClick={handleClick}> + <img alt={name} src={imgSrc} height="42" width="42" /> + <Text align="center" size="1"> + {name} + </Text> + </button> + </Flex> + </Tooltip> ); }; +interface ExtensionsPopoverProps { + extension: ExtensionDefinition; +} + export default ExtensionButton; diff --git a/ui/src/components/extensions/ExtensionDialog.tsx b/ui/src/components/extensions/ExtensionDialog.tsx index 0786d46997..cf3f2647f0 100644 --- a/ui/src/components/extensions/ExtensionDialog.tsx +++ b/ui/src/components/extensions/ExtensionDialog.tsx @@ -1,5 +1,5 @@ import { Dialog } from 'radix-ui'; -import React, { useCallback, useRef, useState } from 'react'; +import React, { useCallback, useRef, useState, useMemo } from 'react'; import clsx from 'clsx'; import styles from './ExtensionDialog.module.css'; @@ -10,28 +10,43 @@ import { setDialogClosed, setDialogOpen, } from '@/features/ui/dialogSlice'; +import { + selectActiveExtension, + unsetActiveExtension, +} from '@/features/extensions/extensionsSlice'; import Panel from '@/components/Panel'; interface ExtensionDialogProps {} const ExtensionDialog: React.FC<ExtensionDialogProps> = () => { const { extension } = useAppSelector(selectDialogOpen); + const activeExtension = useAppSelector(selectActiveExtension); + const dialogWidth = 500; const windowWidth = window.visualViewport?.width || 100; + const windowHeight = window.visualViewport?.height || 100; const dispatch = useAppDispatch(); const [isDragging, setIsDragging] = useState(false); - const [position, setPosition] = useState({ - x: windowWidth / 2 - 250, - y: 200, - }); + const initialPosition = useMemo(() => { + return { + x: windowWidth / 2 - dialogWidth / 2, + y: 200, + }; + }, [windowWidth, dialogWidth]); + const [position, setPosition] = useState(initialPosition); const dialogRef = useRef<HTMLDivElement | null>(null); const handleOpenChange = useCallback( (open: boolean) => { - open - ? dispatch(setDialogOpen('extension')) - : dispatch(setDialogClosed('extension')); + if (open) { + dispatch(setDialogOpen('extension')); + } else { + dispatch(setDialogClosed('extension')); + dispatch(unsetActiveExtension()); + // catch to reset dialog position in case it gets 'lost' off-screen - set it back to the middle on close/open + setPosition(initialPosition); + } }, - [dispatch], + [dispatch, initialPosition], ); const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => { setIsDragging(true); @@ -40,10 +55,21 @@ const ExtensionDialog: React.FC<ExtensionDialogProps> = () => { const handleMouseMove = useCallback( (e: MouseEvent) => { if (isDragging && dialogRef.current) { - setPosition((prevPosition) => ({ - x: prevPosition.x + e.movementX, - y: prevPosition.y + e.movementY, - })); + // Ensure the dialog cannot be dragged so far off the edge that it can't be dragged back on again. + const innerBound = 40; + const minX = 0 - dialogWidth + innerBound; + const maxX = windowWidth - innerBound; + const minY = 0 - innerBound / 2; + const maxY = windowHeight - innerBound; + setPosition((prevPosition) => { + const newX = prevPosition.x + e.movementX; + const newY = prevPosition.y + e.movementY; + + return { + x: Math.max(minX, Math.min(newX, maxX)), + y: Math.max(minY, Math.min(newY, maxY)), + }; + }); } }, [isDragging], @@ -68,13 +94,12 @@ const ExtensionDialog: React.FC<ExtensionDialogProps> = () => { }; }, [handleMouseMove, isDragging]); - if (!extension) { + if (!extension || activeExtension === null) { return null; } - console.log('Rendering', position); return ( - <Dialog.Root modal={false} open={extension} onOpenChange={handleOpenChange}> + <Dialog.Root modal={false} open={true} onOpenChange={handleOpenChange}> <Dialog.Portal> <Theme> <Dialog.Content @@ -106,15 +131,18 @@ const ExtensionDialog: React.FC<ExtensionDialogProps> = () => { > <Dialog.Title asChild> <Heading as="h3" size="4"> - Extension proof of concept + {activeExtension.name} </Heading> </Dialog.Title> </Box> <Dialog.Description className={clsx(styles.DialogDescription)}> - Extension proof of concept + {activeExtension.description} </Dialog.Description> {/* @todo https://www.drupal.org/i/3485692 - render the proof of concept into this div */} - <div id="extensionPortalContainer"> + <div + id="extensionPortalContainer" + className={`xb-extension-${activeExtension.id}`} + > <Text as="p">Not yet supported</Text> </div> <Flex gap="2" justify="end"> diff --git a/ui/src/features/extensions/extensionsSlice.ts b/ui/src/features/extensions/extensionsSlice.ts new file mode 100644 index 0000000000..ffac25fe44 --- /dev/null +++ b/ui/src/features/extensions/extensionsSlice.ts @@ -0,0 +1,39 @@ +import { createAppSlice } from '@/app/createAppSlice'; +import type { PayloadAction } from '@reduxjs/toolkit'; +import type { ExtensionDefinition, ActiveExtension } from '@/types/Extensions'; + +export interface ExtensionsSliceState { + activeExtension: ActiveExtension; +} + +const initialState: ExtensionsSliceState = { + activeExtension: null, +}; + +type UpdateExtensionsPayload = ExtensionDefinition; + +export const extensionsSlice = createAppSlice({ + name: 'extensions', + initialState, + reducers: (create) => ({ + setActiveExtension: create.reducer( + (state, action: PayloadAction<UpdateExtensionsPayload>) => { + state.activeExtension = action.payload; + }, + ), + unsetActiveExtension: create.reducer((state) => { + state.activeExtension = null; + }), + }), + selectors: { + selectActiveExtension: (state): ActiveExtension => { + return state.activeExtension; + }, + }, +}); + +export const extensionsReducer = extensionsSlice.reducer; + +export const { setActiveExtension, unsetActiveExtension } = + extensionsSlice.actions; +export const { selectActiveExtension } = extensionsSlice.selectors; diff --git a/ui/src/services/extensions.ts b/ui/src/services/extensions.ts index 1ac50e3cb2..8e22b0adb2 100644 --- a/ui/src/services/extensions.ts +++ b/ui/src/services/extensions.ts @@ -10,26 +10,31 @@ const kittenBase64 = const dummyExtensionsList = [ { name: 'Extension 1', + description: 'a description of extension 1', imgSrc: kittenBase64, id: 'extension1', }, { name: 'Extension with longer name 2', + description: 'a description of extension 3', imgSrc: kittenBase64, id: 'extension2', }, { name: 'Extension 3', + description: 'a description of extension 3', imgSrc: kittenBase64, id: 'extension3', }, { name: 'Extension 4', + description: 'a description of extension 4', imgSrc: kittenBase64, id: 'extension4', }, { name: 'Extension name 5', + description: 'a description of extension 5', imgSrc: kittenBase64, id: 'extension5', }, diff --git a/ui/src/types/Extensions.ts b/ui/src/types/Extensions.ts index 940b59bd4b..0e04cc61f2 100644 --- a/ui/src/types/Extensions.ts +++ b/ui/src/types/Extensions.ts @@ -1,7 +1,9 @@ -export interface Extension { +export interface ExtensionDefinition { name: string; - imgSrc: string; id: string; + description: string; + imgSrc: string; } -export type ExtensionsList = Extension[]; +export type ExtensionsList = ExtensionDefinition[]; +export type ActiveExtension = ExtensionDefinition | null; -- GitLab From 12f51344c108cdc66b98b4e45d52286338bfc493 Mon Sep 17 00:00:00 2001 From: Jesse Baker <jesse.baker@acquia.com> Date: Mon, 10 Feb 2025 16:19:00 +0000 Subject: [PATCH 09/10] Refactor draggable dialog to be invoked by using Balints Dialog component but with modal set to false --- ui/src/components/Dialog.tsx | 134 +++++++++------- .../DraggableDialogWrapper.module.css | 30 ++++ ui/src/components/DraggableDialogWrapper.tsx | 133 ++++++++++++++++ .../components/extensions/ExtensionDialog.tsx | 144 +++--------------- ui/src/services/extensions.ts | 2 +- 5 files changed, 264 insertions(+), 179 deletions(-) create mode 100644 ui/src/components/DraggableDialogWrapper.module.css create mode 100644 ui/src/components/DraggableDialogWrapper.tsx diff --git a/ui/src/components/Dialog.tsx b/ui/src/components/Dialog.tsx index e23ed59951..2852bcc8a9 100644 --- a/ui/src/components/Dialog.tsx +++ b/ui/src/components/Dialog.tsx @@ -1,14 +1,17 @@ -import { Button, Dialog as RadixDialog, Flex, Text } from '@radix-ui/themes'; +import { Button, Dialog as ThemedDialog, Flex, Text } from '@radix-ui/themes'; import ErrorCard from '@/components/error/ErrorCard'; import styles from './Dialog.module.css'; -import type { ReactNode } from 'react'; +import DraggableDialogWrapper from '@/components/DraggableDialogWrapper'; +import type React from 'react'; export interface DialogProps { open: boolean; onOpenChange: (open: boolean) => void; title: string; - description?: ReactNode; - children?: ReactNode; + // When modal is false, the dialog will also be draggable + modal?: boolean; + description?: React.ReactNode; + children?: React.ReactNode; error?: { title: string; message: string; @@ -32,6 +35,7 @@ const Dialog = ({ description, children, error, + modal = true, footer = { cancelText: 'Cancel', confirmText: 'Confirm', @@ -41,66 +45,86 @@ const Dialog = ({ onOpenChange(isOpen); }; - return ( - <RadixDialog.Root open={open} onOpenChange={handleOpenChange}> - <RadixDialog.Content - width="287px" - className={styles.dialogContent} - // aria-describedby={undefined} is needed when there is no description. - // @see https://www.radix-ui.com/primitives/docs/components/dialog#description - {...(!description && { 'aria-describedby': undefined })} + // The Dialog from Radix THEMES is hard coded to be modal. When we want modal to be false, + // we must use the primitive Radix Dialog (see DraggableDialogWrapper) + let DialogWrap: React.FC<{ children: React.ReactNode }>; + if (modal) { + DialogWrap = ({ children }) => ( + <ThemedDialog.Root open={open} onOpenChange={handleOpenChange}> + <ThemedDialog.Content + width="287px" + className={styles.dialogContent} + // aria-describedby={undefined} is needed when there is no description. + // @see https://www.radix-ui.com/primitives/docs/components/dialog#description + {...(!description && { 'aria-describedby': undefined })} + > + {children} + </ThemedDialog.Content> + </ThemedDialog.Root> + ); + } else { + DialogWrap = ({ children }) => ( + <DraggableDialogWrapper + onOpenChange={handleOpenChange} + description={description} > - <RadixDialog.Title className={styles.title}> - <Text size="1" weight="bold"> - {title} - </Text> - </RadixDialog.Title> + {children} + </DraggableDialogWrapper> + ); + } - {description && ( - <RadixDialog.Description size="2" mb="4"> - {description} - </RadixDialog.Description> - )} + return ( + <DialogWrap> + <ThemedDialog.Title className={styles.title}> + <Text size="1" weight="bold"> + {title} + </Text> + </ThemedDialog.Title> - <Flex direction="column" gap="2"> - <Flex direction="column" gap="1"> - {children} - </Flex> + {description && ( + <ThemedDialog.Description size="2" mb="4"> + {description} + </ThemedDialog.Description> + )} - {error && ( - <ErrorCard - title={error.title} - error={error.message} - resetButtonText={error.resetButtonText} - resetErrorBoundary={error.onReset} - /> - )} + <Flex direction="column" gap="2"> + <Flex direction="column" gap="1"> + {children} + </Flex> + + {error && ( + <ErrorCard + title={error.title} + error={error.message} + resetButtonText={error.resetButtonText} + resetErrorBoundary={error.onReset} + /> + )} - <Flex gap="2" justify="end"> - <RadixDialog.Close> - <Button variant="outline" size="1"> - {footer.cancelText} - </Button> - </RadixDialog.Close> - {footer.onConfirm && ( - <Button - onClick={footer.onConfirm} - disabled={footer.isConfirmDisabled} - loading={footer.isConfirmLoading} - size="1" - color={footer.isDanger ? 'red' : 'blue'} - > - {footer.confirmText} - </Button> - )} - </Flex> + <Flex gap="2" justify="end"> + <ThemedDialog.Close> + <Button variant="outline" size="1"> + {footer.cancelText} + </Button> + </ThemedDialog.Close> + {footer.onConfirm && ( + <Button + onClick={footer.onConfirm} + disabled={footer.isConfirmDisabled} + loading={footer.isConfirmLoading} + size="1" + color={footer.isDanger ? 'red' : 'blue'} + > + {footer.confirmText} + </Button> + )} </Flex> - </RadixDialog.Content> - </RadixDialog.Root> + </Flex> + </DialogWrap> ); }; -const DialogFieldLabel = ({ children }: { children: ReactNode }) => { +const DialogFieldLabel = ({ children }: { children: React.ReactNode }) => { return ( <Text as="label" size="1" weight="bold" className={styles.fieldLabel}> {children} diff --git a/ui/src/components/DraggableDialogWrapper.module.css b/ui/src/components/DraggableDialogWrapper.module.css new file mode 100644 index 0000000000..496fa85d33 --- /dev/null +++ b/ui/src/components/DraggableDialogWrapper.module.css @@ -0,0 +1,30 @@ +.DialogContent { + position: fixed; + top: 0; + left: 0; + width: 90vw; + max-width: 500px; + max-height: 85vh; + padding: 25px; + animation: content-show 250ms cubic-bezier(0.16, 1, 0.3, 1); + border-radius: 6px; + background-color: var(--gray-1); + box-shadow: var(--shadow-6); +} +.DialogContent:focus { + outline: none; +} +.DraggableArea { + position: absolute; + width: 100%; + height: var(--space-7); +} + +@keyframes content-show { + from { + opacity: 0; + } + to { + opacity: 1; + } +} diff --git a/ui/src/components/DraggableDialogWrapper.tsx b/ui/src/components/DraggableDialogWrapper.tsx new file mode 100644 index 0000000000..41e82731f9 --- /dev/null +++ b/ui/src/components/DraggableDialogWrapper.tsx @@ -0,0 +1,133 @@ +import { Dialog } from 'radix-ui'; +import type { ReactNode } from 'react'; +import React, { useCallback, useRef, useState, useMemo } from 'react'; + +import clsx from 'clsx'; +import styles from './DraggableDialogWrapper.module.css'; +import { Box, Theme } from '@radix-ui/themes'; +import Panel from '@/components/Panel'; + +interface DraggableDialogWrapperProps { + children: ReactNode; + onOpenChange: Function; + description: ReactNode; +} + +const PANEL_PADDING = '4'; + +const DraggableDialogWrapper: React.FC<DraggableDialogWrapperProps> = ({ + children, + onOpenChange, + description, +}) => { + const dialogWidth = 500; + const windowWidth = window.visualViewport?.width || 100; + const windowHeight = window.visualViewport?.height || 100; + const [isDragging, setIsDragging] = useState(false); + const initialPosition = useMemo(() => { + return { + x: windowWidth / 2 - dialogWidth / 2, + y: 200, + }; + }, [windowWidth, dialogWidth]); + const [position, setPosition] = useState(initialPosition); + const dialogRef = useRef<HTMLDivElement | null>(null); + + const handleOpenChange = useCallback( + (open: boolean) => { + onOpenChange(open); + setPosition(initialPosition); + }, + [initialPosition, onOpenChange], + ); + const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => { + setIsDragging(true); + }; + + const handleMouseMove = useCallback( + (e: MouseEvent) => { + if (isDragging && dialogRef.current) { + // Ensure the dialog cannot be dragged so far off the edge that it can't be dragged back on again. + const innerBound = 40; + const minX = 0 - dialogWidth + innerBound; + const maxX = windowWidth - innerBound; + const minY = 0 - innerBound / 2; + const maxY = windowHeight - innerBound; + setPosition((prevPosition) => { + const newX = prevPosition.x + e.movementX; + const newY = prevPosition.y + e.movementY; + + return { + x: Math.max(minX, Math.min(newX, maxX)), + y: Math.max(minY, Math.min(newY, maxY)), + }; + }); + } + }, + [isDragging, windowHeight, windowWidth], + ); + + const handleMouseUp = () => { + setIsDragging(false); + }; + + React.useEffect(() => { + if (isDragging) { + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseup', handleMouseUp); + } else { + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', handleMouseUp); + } + + return () => { + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', handleMouseUp); + }; + }, [handleMouseMove, isDragging]); + + return ( + <Dialog.Root modal={false} open={true} onOpenChange={handleOpenChange}> + <Dialog.Portal> + <Theme> + <Dialog.Content + className={clsx(styles.DialogContent)} + asChild + style={{ + transform: `translate(${position.x}px, ${position.y}px)`, + position: 'absolute', + }} + onPointerDownOutside={(event) => { + event.preventDefault(); + }} + onInteractOutside={(event) => { + event.preventDefault(); + }} + ref={dialogRef} + // aria-describedby={undefined} is needed when there is no description. + // @see https://www.radix-ui.com/primitives/docs/components/dialog#description + {...(!description && { 'aria-describedby': undefined })} + > + <Panel p={PANEL_PADDING}> + <Box + mt={`-${PANEL_PADDING}`} + pt={PANEL_PADDING} + mx={`-${PANEL_PADDING}`} + px={PANEL_PADDING} + className={styles.DraggableArea} + onMouseDown={handleMouseDown} + style={{ + cursor: 'move', + }} + /> + + {children} + </Panel> + </Dialog.Content> + </Theme> + </Dialog.Portal> + </Dialog.Root> + ); +}; + +export default DraggableDialogWrapper; diff --git a/ui/src/components/extensions/ExtensionDialog.tsx b/ui/src/components/extensions/ExtensionDialog.tsx index cf3f2647f0..cb9cb4a27d 100644 --- a/ui/src/components/extensions/ExtensionDialog.tsx +++ b/ui/src/components/extensions/ExtensionDialog.tsx @@ -1,9 +1,8 @@ -import { Dialog } from 'radix-ui'; -import React, { useCallback, useRef, useState, useMemo } from 'react'; +import Dialog from '@/components/Dialog'; +import type React from 'react'; +import { useCallback } from 'react'; -import clsx from 'clsx'; -import styles from './ExtensionDialog.module.css'; -import { Box, Button, Flex, Heading, Text, Theme } from '@radix-ui/themes'; +import { Text } from '@radix-ui/themes'; import { useAppDispatch, useAppSelector } from '@/app/hooks'; import { selectDialogOpen, @@ -14,26 +13,13 @@ import { selectActiveExtension, unsetActiveExtension, } from '@/features/extensions/extensionsSlice'; -import Panel from '@/components/Panel'; interface ExtensionDialogProps {} const ExtensionDialog: React.FC<ExtensionDialogProps> = () => { const { extension } = useAppSelector(selectDialogOpen); const activeExtension = useAppSelector(selectActiveExtension); - const dialogWidth = 500; - const windowWidth = window.visualViewport?.width || 100; - const windowHeight = window.visualViewport?.height || 100; const dispatch = useAppDispatch(); - const [isDragging, setIsDragging] = useState(false); - const initialPosition = useMemo(() => { - return { - x: windowWidth / 2 - dialogWidth / 2, - y: 200, - }; - }, [windowWidth, dialogWidth]); - const [position, setPosition] = useState(initialPosition); - const dialogRef = useRef<HTMLDivElement | null>(null); const handleOpenChange = useCallback( (open: boolean) => { @@ -42,119 +28,31 @@ const ExtensionDialog: React.FC<ExtensionDialogProps> = () => { } else { dispatch(setDialogClosed('extension')); dispatch(unsetActiveExtension()); - // catch to reset dialog position in case it gets 'lost' off-screen - set it back to the middle on close/open - setPosition(initialPosition); } }, - [dispatch, initialPosition], + [dispatch], ); - const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => { - setIsDragging(true); - }; - - const handleMouseMove = useCallback( - (e: MouseEvent) => { - if (isDragging && dialogRef.current) { - // Ensure the dialog cannot be dragged so far off the edge that it can't be dragged back on again. - const innerBound = 40; - const minX = 0 - dialogWidth + innerBound; - const maxX = windowWidth - innerBound; - const minY = 0 - innerBound / 2; - const maxY = windowHeight - innerBound; - setPosition((prevPosition) => { - const newX = prevPosition.x + e.movementX; - const newY = prevPosition.y + e.movementY; - - return { - x: Math.max(minX, Math.min(newX, maxX)), - y: Math.max(minY, Math.min(newY, maxY)), - }; - }); - } - }, - [isDragging], - ); - - const handleMouseUp = () => { - setIsDragging(false); - }; - - React.useEffect(() => { - if (isDragging) { - document.addEventListener('mousemove', handleMouseMove); - document.addEventListener('mouseup', handleMouseUp); - } else { - document.removeEventListener('mousemove', handleMouseMove); - document.removeEventListener('mouseup', handleMouseUp); - } - - return () => { - document.removeEventListener('mousemove', handleMouseMove); - document.removeEventListener('mouseup', handleMouseUp); - }; - }, [handleMouseMove, isDragging]); - if (!extension || activeExtension === null) { return null; } return ( - <Dialog.Root modal={false} open={true} onOpenChange={handleOpenChange}> - <Dialog.Portal> - <Theme> - <Dialog.Content - className={clsx(styles.DialogContent)} - asChild - style={{ - transform: `translate(${position.x}px, ${position.y}px)`, - position: 'absolute', - }} - onPointerDownOutside={(event) => { - event.preventDefault(); - }} - onInteractOutside={(event) => { - event.preventDefault(); - }} - ref={dialogRef} - > - <Panel> - <Box - mt="-5" - pt="5" - mx="-5" - px="5" - className={styles.DraggableArea} - onMouseDown={handleMouseDown} - style={{ - cursor: 'move', - }} - > - <Dialog.Title asChild> - <Heading as="h3" size="4"> - {activeExtension.name} - </Heading> - </Dialog.Title> - </Box> - <Dialog.Description className={clsx(styles.DialogDescription)}> - {activeExtension.description} - </Dialog.Description> - {/* @todo https://www.drupal.org/i/3485692 - render the proof of concept into this div */} - <div - id="extensionPortalContainer" - className={`xb-extension-${activeExtension.id}`} - > - <Text as="p">Not yet supported</Text> - </div> - <Flex gap="2" justify="end"> - <Dialog.Close asChild> - <Button>Close</Button> - </Dialog.Close> - </Flex> - </Panel> - </Dialog.Content> - </Theme> - </Dialog.Portal> - </Dialog.Root> + <Dialog + open={extension} + onOpenChange={handleOpenChange} + title={activeExtension.name} + modal={false} + footer={{ cancelText: 'Close' }} + description={activeExtension.description} + > + {/* @todo https://www.drupal.org/i/3485692 - render the proof of concept into this div */} + <div + id="extensionPortalContainer" + className={`xb-extension-${activeExtension.id}`} + > + <Text as="p">Not yet supported</Text> + </div> + </Dialog> ); }; diff --git a/ui/src/services/extensions.ts b/ui/src/services/extensions.ts index 8e22b0adb2..064db720ee 100644 --- a/ui/src/services/extensions.ts +++ b/ui/src/services/extensions.ts @@ -16,7 +16,7 @@ const dummyExtensionsList = [ }, { name: 'Extension with longer name 2', - description: 'a description of extension 3', + description: 'a description of extension 2', imgSrc: kittenBase64, id: 'extension2', }, -- GitLab From 5dd3751a47830f0b704800e6e1ddf4b6b4d4c07c Mon Sep 17 00:00:00 2001 From: Jesse Baker <jesse.baker@acquia.com> Date: Tue, 11 Feb 2025 10:20:22 +0000 Subject: [PATCH 10/10] Fixed massive performance issue with dialog wrapper components being recreated on every render --- ui/src/components/Dialog.tsx | 76 ++++++++++++-------- ui/src/components/DraggableDialogWrapper.tsx | 8 ++- 2 files changed, 51 insertions(+), 33 deletions(-) diff --git a/ui/src/components/Dialog.tsx b/ui/src/components/Dialog.tsx index 2852bcc8a9..6e6e394a00 100644 --- a/ui/src/components/Dialog.tsx +++ b/ui/src/components/Dialog.tsx @@ -8,7 +8,6 @@ export interface DialogProps { open: boolean; onOpenChange: (open: boolean) => void; title: string; - // When modal is false, the dialog will also be draggable modal?: boolean; description?: React.ReactNode; children?: React.ReactNode; @@ -28,6 +27,33 @@ export interface DialogProps { }; } +const DialogWrap = ({ open, handleOpenChange, children, description }: any) => ( + <ThemedDialog.Root open={open} onOpenChange={handleOpenChange}> + <ThemedDialog.Content + width="287px" + className={styles.dialogContent} + {...(!description && { 'aria-describedby': undefined })} + > + {children} + </ThemedDialog.Content> + </ThemedDialog.Root> +); + +const DraggableDialogWrap = ({ + handleOpenChange, + open, + description, + children, +}: any) => ( + <DraggableDialogWrapper + open={open} + onOpenChange={handleOpenChange} + description={description} + > + {children} + </DraggableDialogWrapper> +); + const Dialog = ({ open, onOpenChange, @@ -45,36 +71,14 @@ const Dialog = ({ onOpenChange(isOpen); }; - // The Dialog from Radix THEMES is hard coded to be modal. When we want modal to be false, - // we must use the primitive Radix Dialog (see DraggableDialogWrapper) - let DialogWrap: React.FC<{ children: React.ReactNode }>; - if (modal) { - DialogWrap = ({ children }) => ( - <ThemedDialog.Root open={open} onOpenChange={handleOpenChange}> - <ThemedDialog.Content - width="287px" - className={styles.dialogContent} - // aria-describedby={undefined} is needed when there is no description. - // @see https://www.radix-ui.com/primitives/docs/components/dialog#description - {...(!description && { 'aria-describedby': undefined })} - > - {children} - </ThemedDialog.Content> - </ThemedDialog.Root> - ); - } else { - DialogWrap = ({ children }) => ( - <DraggableDialogWrapper - onOpenChange={handleOpenChange} - description={description} - > - {children} - </DraggableDialogWrapper> - ); - } + const Wrapper = modal ? DialogWrap : DraggableDialogWrap; return ( - <DialogWrap> + <Wrapper + open={open} + handleOpenChange={handleOpenChange} + description={description} + > <ThemedDialog.Title className={styles.title}> <Text size="1" weight="bold"> {title} @@ -120,10 +124,22 @@ const Dialog = ({ )} </Flex> </Flex> - </DialogWrap> + </Wrapper> ); }; +// const DialogContent = ({ +// title, +// description, +// children, +// error, +// footer, +// }: any) => ( +// <> +// +// </> +// ); + const DialogFieldLabel = ({ children }: { children: React.ReactNode }) => { return ( <Text as="label" size="1" weight="bold" className={styles.fieldLabel}> diff --git a/ui/src/components/DraggableDialogWrapper.tsx b/ui/src/components/DraggableDialogWrapper.tsx index 41e82731f9..064b881202 100644 --- a/ui/src/components/DraggableDialogWrapper.tsx +++ b/ui/src/components/DraggableDialogWrapper.tsx @@ -8,17 +8,19 @@ import { Box, Theme } from '@radix-ui/themes'; import Panel from '@/components/Panel'; interface DraggableDialogWrapperProps { - children: ReactNode; onOpenChange: Function; + open: boolean; description: ReactNode; + children: ReactNode; } const PANEL_PADDING = '4'; const DraggableDialogWrapper: React.FC<DraggableDialogWrapperProps> = ({ - children, onOpenChange, + open, description, + children, }) => { const dialogWidth = 500; const windowWidth = window.visualViewport?.width || 100; @@ -87,7 +89,7 @@ const DraggableDialogWrapper: React.FC<DraggableDialogWrapperProps> = ({ }, [handleMouseMove, isDragging]); return ( - <Dialog.Root modal={false} open={true} onOpenChange={handleOpenChange}> + <Dialog.Root modal={false} open={open} onOpenChange={handleOpenChange}> <Dialog.Portal> <Theme> <Dialog.Content -- GitLab