diff --git a/client/scss/core.scss b/client/scss/core.scss index 55465ce8f7..a126607855 100644 --- a/client/scss/core.scss +++ b/client/scss/core.scss @@ -148,6 +148,7 @@ These are classes that provide overrides. // VENDOR: overrides of vendor styles. @import 'overrides/vendor.datetimepicker'; @import 'overrides/vendor.tagit'; +@import 'overrides/vendor.tippy'; // UTILITIES: classes that do one simple thing. @import 'overrides/utilities.hidden'; diff --git a/client/scss/overrides/_vendor.tippy.scss b/client/scss/overrides/_vendor.tippy.scss new file mode 100644 index 0000000000..2cb5f6ca05 --- /dev/null +++ b/client/scss/overrides/_vendor.tippy.scss @@ -0,0 +1,21 @@ +@import '../../../node_modules/tippy.js/dist/tippy'; + +.tippy-box { + @apply w-bg-primary w-text-white w-text-14; +} + +.tippy-box[data-placement^='top'] > .tippy-arrow::before { + @apply w-border-t-primary; +} + +.tippy-box[data-placement^='bottom'] > .tippy-arrow::before { + @apply w-border-b-primary; +} + +.tippy-box[data-placement^='left'] > .tippy-arrow::before { + @apply w-border-l-primary; +} + +.tippy-box[data-placement^='right'] > .tippy-arrow::before { + @apply w-border-r-primary; +} diff --git a/client/scss/settings/_variables.scss b/client/scss/settings/_variables.scss index be632f8471..5f2ff6eb86 100644 --- a/client/scss/settings/_variables.scss +++ b/client/scss/settings/_variables.scss @@ -127,7 +127,7 @@ $font-wagtail-icons: wagtail; // misc sizing $thumbnail-width: 130px; $menu-width: 200px; -$menu-width-slim: 60px; +$menu-width-slim: 65px; $menu-width-max: 320px; diff --git a/client/src/components/PageExplorer/PageExplorerItem.scss b/client/src/components/PageExplorer/PageExplorerItem.scss index 55c1cd6e36..b97659bcc2 100644 --- a/client/src/components/PageExplorer/PageExplorerItem.scss +++ b/client/src/components/PageExplorer/PageExplorerItem.scss @@ -3,7 +3,7 @@ } .c-page-explorer__item__link { - @apply w-inline-flex w-items-start sm:w-items-center w-flex-wrap w-grow w-cursor-pointer w-gap-1; + @apply w-inline-flex w-items-start sm:w-items-center w-flex-wrap w-grow w-cursor-pointer w-gap-1 w-transition; padding: 1.45em 1em; &:focus, @@ -35,7 +35,7 @@ } .c-page-explorer__item__action { - @apply w-text-white/85; + @apply w-text-white/85 w-transition; display: inline-flex; align-items: center; justify-content: center; diff --git a/client/src/components/Sidebar/Sidebar.scss b/client/src/components/Sidebar/Sidebar.scss index 8a0e7e4cc9..ccefd4c70f 100644 --- a/client/src/components/Sidebar/Sidebar.scss +++ b/client/src/components/Sidebar/Sidebar.scss @@ -23,6 +23,7 @@ } @include media-breakpoint-up(sm) { + position: static; inset-inline-end: $sidebar-toggle-spacing; // Remove once we drop support for Safari 13. @@ -79,23 +80,13 @@ &__collapse-toggle { @include sidebar-toggle; - display: grid; + // All other styling is done with utility classes on this element } // When in mobile mode, hide the collapse-toggle and show the nav-toggle (which is defined in the .sidebar-nav-toggle class below) &--mobile &__collapse-toggle { display: none; } - - // This element should cover all the area beneath the collapse toggle - // It's only used to attach mouse enter/exit event handlers to control peeking - &__peek-hover-area { - margin-top: $sidebar-toggle-size; - display: grid; - grid-template-columns: 1fr; - overflow-y: auto; - overflow-x: hidden; - } } // This is a separate component as it needs to display in the header @@ -118,5 +109,4 @@ @import 'menu/MenuItem'; @import 'menu/SubMenuItem'; @import 'modules/MainMenu'; -@import 'modules/Search'; @import 'modules/WagtailBranding'; diff --git a/client/src/components/Sidebar/Sidebar.tsx b/client/src/components/Sidebar/Sidebar.tsx index 81ee1d741b..7e526694e5 100644 --- a/client/src/components/Sidebar/Sidebar.tsx +++ b/client/src/components/Sidebar/Sidebar.tsx @@ -17,6 +17,8 @@ export interface ModuleRenderContext { key: number; slim: boolean; expandingOrCollapsing: boolean; + onAccountExpand: () => void; + onSearchClick: () => void; currentPath: string; strings: Strings; navigate(url: string): Promise; @@ -46,7 +48,6 @@ export const Sidebar: React.FunctionComponent = ({ // 'collapsed' is a persistent state that is controlled by the arrow icon at the top // It records the user's general preference for a collapsed/uncollapsed menu // This is just a hint though, and we may still collapse the menu if the screen is too small - // Also, we may display the full menu temporarily in collapsed mode (see 'peeking' below) const [collapsed, setCollapsed] = React.useState(collapsedOnLoad); // Call onExpandCollapse(true) if menu is initialised in collapsed state @@ -56,11 +57,6 @@ export const Sidebar: React.FunctionComponent = ({ } }, []); - // 'peeking' is a temporary state to allow the user to peek in the menu while it is collapsed, or hidden. - // When peeking is true, the menu renders as if it's not collapsed, but as an overlay instead of occupying - // space next to the content - const [peeking, setPeeking] = React.useState(false); - // 'visibleOnMobile' indicates whether the sidebar is currently visible on mobile // On mobile, the sidebar is completely hidden by default and must be opened manually const [visibleOnMobile, setVisibleOnMobile] = React.useState(false); @@ -80,6 +76,7 @@ export const Sidebar: React.FunctionComponent = ({ setVisibleOnMobile(false); } } + window.addEventListener('resize', handleResize); handleResize(); return () => window.removeEventListener('resize', handleResize); @@ -88,7 +85,7 @@ export const Sidebar: React.FunctionComponent = ({ // Whether or not to display the menu with slim layout. // Separate from 'collapsed' as the menu can still be displayed with an expanded // layout while in 'collapsed' mode if the user is 'peeking' into it (see above) - const slim = collapsed && !peeking && !isMobile; + const slim = collapsed && !isMobile; // 'expandingOrCollapsing' is set to true whilst the the menu is transitioning between slim and expanded layouts const [expandingOrCollapsing, setExpandingOrCollapsing] = @@ -124,40 +121,33 @@ export const Sidebar: React.FunctionComponent = ({ }; }; - // Switch peeking on/off when the mouse cursor hovers the sidebar or focus is on the sidebar - const [mouseHover, setMouseHover] = React.useState(false); const [focused, setFocused] = React.useState(false); - const onMouseEnterHandler = () => { - setMouseHover(true); - }; - - const onMouseLeaveHandler = () => { - setMouseHover(false); + const onBlurHandler = () => { + if (focused) { + setFocused(false); + setCollapsed(true); + } }; const onFocusHandler = () => { - setFocused(true); - }; - - const onBlurHandler = () => { - setFocused(false); - }; - - // We need a stop peeking timeout to stop the sidebar moving as someone tab's though the menu - const stopPeekingTimeout = React.useRef(null); - - React.useEffect(() => { - if (mouseHover || focused) { - clearTimeout(stopPeekingTimeout.current); - setPeeking(true); - } else { - clearTimeout(stopPeekingTimeout.current); - stopPeekingTimeout.current = setTimeout(() => { - setPeeking(false); - }, SIDEBAR_TRANSITION_DURATION); + if (focused) { + setCollapsed(false); + setFocused(true); } - }, [mouseHover, focused]); + }; + + const onSearchClick = () => { + if (slim) { + onClickCollapseToggle(); + } + }; + + const onAccountExpand = () => { + if (slim) { + onClickCollapseToggle(); + } + }; // Render modules const renderedModules = modules.map((module, index) => @@ -165,6 +155,8 @@ export const Sidebar: React.FunctionComponent = ({ key: index, slim, expandingOrCollapsing, + onAccountExpand, + onSearchClick, currentPath, strings, navigate, @@ -181,31 +173,43 @@ export const Sidebar: React.FunctionComponent = ({ (isMobile && !visibleOnMobile ? ' sidebar--hidden' : '') } > -
- - -
- {renderedModules} + /> +
+ + {renderedModules}
-
+ name="expand-right" + /> + +
+ + +
h2 { // w-min-h-[160px] and w-mt-[35px] classes are to vertically align the title and icon combination to the search input on the left - @apply w-min-h-[160px] w-mt-[35px] w-text-white w-mb-0 w-inline-flex w-flex-col w-justify-center w-items-center; + @apply w-min-h-[160px] w-mt-[45px] w-px-4 w-box-border w-text-center w-text-white w-mb-0 w-inline-flex w-flex-col w-justify-center w-items-center; &:before { font-size: 4em; @@ -50,6 +55,10 @@ width: 100%; opacity: 0.15; } + + @at-root .sidebar--slim & { + @apply w-mt-3; + } } ul > li { @@ -81,9 +90,6 @@ box-shadow: 2px 0 2px rgba(0, 0, 0, 0.35); } - @at-root .sidebar--slim #{&} { - transform: translate3d($menu-width-slim - $menu-width, 0, 0); - } // Don't apply this to nested submenus though @at-root .sidebar--slim .sidebar-sub-menu-panel #{&} { transform: translate3d(0, 0, 0); diff --git a/client/src/components/Sidebar/menu/SubMenuItem.tsx b/client/src/components/Sidebar/menu/SubMenuItem.tsx index d455881a77..c1cbc9905f 100644 --- a/client/src/components/Sidebar/menu/SubMenuItem.tsx +++ b/client/src/components/Sidebar/menu/SubMenuItem.tsx @@ -6,6 +6,7 @@ import { renderMenu } from '../modules/MainMenu'; import { SidebarPanel } from '../SidebarPanel'; import { SIDEBAR_TRANSITION_DURATION } from '../Sidebar'; import { MenuItemDefinition, MenuItemProps } from './MenuItem'; +import Tippy from '@tippyjs/react'; interface SubMenuItemProps extends MenuItemProps { slim: boolean; @@ -65,19 +66,21 @@ export const SubMenuItem: React.FunctionComponent = ({ return (
  • - + + +

    - + +
    - + + { const strings = {}; const user = { avatarUrl: 'https://gravatar/profile' }; + const onAccountExpand = jest.fn(); it('should render with the minimum required props', () => { const wrapper = shallow( @@ -13,6 +14,7 @@ describe('Menu', () => { menuItems={[]} strings={strings} user={user} + onAccountExpand={onAccountExpand} />, ); @@ -26,6 +28,7 @@ describe('Menu', () => { menuItems={[]} strings={strings} user={user} + onAccountExpand={onAccountExpand} />, ); diff --git a/client/src/components/Sidebar/modules/MainMenu.tsx b/client/src/components/Sidebar/modules/MainMenu.tsx index 9f2638f7ae..265d23280a 100644 --- a/client/src/components/Sidebar/modules/MainMenu.tsx +++ b/client/src/components/Sidebar/modules/MainMenu.tsx @@ -5,6 +5,7 @@ import { LinkMenuItemDefinition } from '../menu/LinkMenuItem'; import { MenuItemDefinition } from '../menu/MenuItem'; import { SubMenuItemDefinition } from '../menu/SubMenuItem'; import { ModuleDefinition, Strings } from '../Sidebar'; +import Tippy from '@tippyjs/react'; export function renderMenu( path: string, @@ -64,6 +65,7 @@ interface MenuProps { user: MainMenuModuleDefinition['user']; slim: boolean; expandingOrCollapsing: boolean; + onAccountExpand: () => void; currentPath: string; strings: Strings; @@ -75,6 +77,7 @@ export const Menu: React.FunctionComponent = ({ accountMenuItems, user, expandingOrCollapsing, + onAccountExpand, slim, currentPath, strings, @@ -90,6 +93,7 @@ export const Menu: React.FunctionComponent = ({ activePath: '', }); const accountSettingsOpen = state.navigationPath.startsWith('.account'); + const isVisible = !slim || expandingOrCollapsing; // Whenever currentPath or menu changes, work out new activePath React.useEffect(() => { @@ -161,17 +165,30 @@ export const Menu: React.FunctionComponent = ({ }; }, []); + // Determine if the sidebar is expanded from account button click + const [expandedFromAccountClick, setExpandedFromAccountClick] = + React.useState(false); + // Whenever the parent Sidebar component collapses or expands, close any open menus React.useEffect(() => { - if (expandingOrCollapsing) { + if (expandingOrCollapsing && !expandedFromAccountClick) { dispatch({ type: 'set-navigation-path', path: '', }); } + if (expandedFromAccountClick) { + setExpandedFromAccountClick(false); + } }, [expandingOrCollapsing]); const onClickAccountSettings = () => { + // Pass account expand information to Sidebar component + onAccountExpand(); + if (slim) { + setExpandedFromAccountClick(true); + } + if (accountSettingsOpen) { dispatch({ type: 'set-navigation-path', @@ -199,47 +216,53 @@ export const Menu: React.FunctionComponent = ({
    -
    - +
    +
    + {user.name} +
    + +
    + +
      {renderMenu('', accountMenuItems, slim, state, dispatch, navigate)} @@ -267,7 +290,15 @@ export class MainMenuModuleDefinition implements ModuleDefinition { this.user = user; } - render({ slim, expandingOrCollapsing, key, currentPath, strings, navigate }) { + render({ + slim, + expandingOrCollapsing, + onAccountExpand, + key, + currentPath, + strings, + navigate, + }) { return ( void; searchUrl: string; strings: Strings; @@ -15,11 +22,13 @@ interface SearchInputProps { export const SearchInput: React.FunctionComponent = ({ slim, expandingOrCollapsing, + onSearchClick, searchUrl, strings, navigate, }) => { const isVisible = !slim || expandingOrCollapsing; + const searchInput = React.useRef(null); const onSubmitForm = (e: React.FormEvent) => { if (e.target instanceof HTMLFormElement) { @@ -36,36 +45,84 @@ export const SearchInput: React.FunctionComponent = ({ } }; - const className = - 'sidebar-search' + - (slim ? ' sidebar-search--slim' : '') + - (isVisible ? ' sidebar-search--visible' : ''); - return (
      - - - +
      + + {/* Use padding left 23px to align icon in slim mode and padding right 18px to ensure focus is full width */} + + + + + + {/* Classes marked important to trump the base input styling set in _forms.scss */} + +
      ); }; @@ -77,13 +134,21 @@ export class SearchModuleDefinition implements ModuleDefinition { this.searchUrl = searchUrl; } - render({ slim, key, expandingOrCollapsing, strings, navigate }) { + render({ + slim, + key, + expandingOrCollapsing, + onSearchClick, + strings, + navigate, + }) { return ( diff --git a/client/src/components/Sidebar/modules/WagtailBranding.scss b/client/src/components/Sidebar/modules/WagtailBranding.scss index df1eab9860..d85efb697c 100644 --- a/client/src/components/Sidebar/modules/WagtailBranding.scss +++ b/client/src/components/Sidebar/modules/WagtailBranding.scss @@ -18,7 +18,7 @@ align-items: center; color: #aaa; -webkit-font-smoothing: auto; - margin: 1.8em auto 2.5em; + margin: 4em auto 1.8em; text-align: center; width: 100px; height: 100px; @@ -28,10 +28,15 @@ box-sizing: border-box; border-radius: 100%; + @include media-breakpoint-up(sm) { + margin: 1.8em auto 2.5em; + } + // Reduce overall size when in slim mode .sidebar--slim & { + @include show-focus-outline-inside(); width: 60px; - transform: none; + height: 60px; } // Remove background on 404 page @@ -63,30 +68,13 @@ // Bird wrapper &__icon-wrapper { - @apply w-bg-white/15 w-overflow-hidden hover:w-overflow-visible; + @apply w-bg-white/15 w-relative w-overflow-hidden hover:w-overflow-visible; margin: auto; - position: absolute; - // Remove once we drop support for Safari 13. - // stylelint-disable-next-line property-disallowed-list - left: 0; - inset-inline-start: 0; - top: 0; width: 100px; height: 100px; border-radius: 50%; - // Remove once we drop support for Safari 13. - // stylelint-disable-next-line property-disallowed-list - transition: left $menu-transition-duration ease, - inset-inline-start $menu-transition-duration ease, - top $menu-transition-duration ease, width $menu-transition-duration ease, - height $menu-transition-duration ease; .sidebar--slim & { - // Remove once we drop support for Safari 13. - // stylelint-disable-next-line property-disallowed-list - left: 10px; - inset-inline-start: 10px; - top: 10px; width: 40px; height: 40px; } @@ -97,28 +85,6 @@ position: static; } } - - // Bird icons - &__icon { - .sidebar--slim & { - width: 42px; - height: 51px; - top: 10px; - // Remove once we drop support for Safari 13. - // stylelint-disable-next-line property-disallowed-list - left: -9px; - inset-inline-start: -9px; - } - - // TODO: Fix legacy specificity issues - &[data-part='eye--open'] { - display: inline !important; - } - - &[data-part='eye--closed'] { - display: none !important; - } - } } .sidebar-custom-branding { diff --git a/client/src/components/Sidebar/modules/WagtailBranding.tsx b/client/src/components/Sidebar/modules/WagtailBranding.tsx index 762840ce7c..a302ff0cad 100644 --- a/client/src/components/Sidebar/modules/WagtailBranding.tsx +++ b/client/src/components/Sidebar/modules/WagtailBranding.tsx @@ -5,6 +5,7 @@ import WagtailLogo from './WagtailLogo'; interface WagtailBrandingProps { homeUrl: string; strings: Strings; + slim: boolean; currentPath: string; navigate(url: string): void; } @@ -12,6 +13,7 @@ interface WagtailBrandingProps { const WagtailBranding: React.FunctionComponent = ({ homeUrl, strings, + slim, currentPath, navigate, }) => { @@ -79,7 +81,7 @@ const WagtailBranding: React.FunctionComponent = ({ }; const desktopClassName = - 'sidebar-wagtail-branding' + + 'sidebar-wagtail-branding w-transition-all w-duration-150' + (isWagging ? ' sidebar-wagtail-branding--wagging' : ''); return ( @@ -92,8 +94,8 @@ const WagtailBranding: React.FunctionComponent = ({ onMouseMove={onMouseMove} onMouseLeave={onMouseLeave} > -
      - +
      +
      ); @@ -106,11 +108,12 @@ export class WagtailBrandingModuleDefinition implements ModuleDefinition { this.homeUrl = homeUrl; } - render({ strings, key, navigate, currentPath }) { + render({ strings, slim, key, navigate, currentPath }) { return ( { - const feathersClasses = 'group-hover:w-text-black'; +const WagtailLogo = ({ className, slim }: WagtailLogoProps) => { + const feathersClasses = + 'group-hover:w-text-black w-transition-all w-duration-150'; return (
      - + className="avatar avatar-on-dark w-flex-shrink-0 !w-w-[28px] !w-h-[28px]" + > + +
      +
      +
      + +
      + +
      diff --git a/client/tailwind.config.js b/client/tailwind.config.js index e80340454a..de563f0ee6 100644 --- a/client/tailwind.config.js +++ b/client/tailwind.config.js @@ -73,6 +73,9 @@ module.exports = { 15: '0.15', 85: '0.85', }, + outlineOffset: { + inside: '-3px', + }, }, }, plugins: [ diff --git a/package-lock.json b/package-lock.json index ff6bb46fc6..b13adb0f6d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "wagtail", "version": "1.0.0", "dependencies": { + "@tippyjs/react": "^4.2.6", "draft-js": "^0.10.5", "draftail": "^1.4.1", "draftjs-filters": "^2.5.0", @@ -23,6 +24,7 @@ "redux-thunk": "^2.3.0", "reselect": "^4.0.0", "telepath-unpack": "^0.0.3", + "tippy.js": "^6.3.7", "uuid": "^8.3.2" }, "devDependencies": { @@ -3112,7 +3114,6 @@ "version": "2.11.2", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.2.tgz", "integrity": "sha512-92FRmppjjqz29VMJ2dn+xdyXZBrMlE42AV6Kq6BwjWV7CNUW1hs2FtxSNLQE+gJhaZ6AAmYuO9y8dshhcBl7vA==", - "dev": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/popperjs" @@ -9436,6 +9437,18 @@ "react-dom": "^16.8.0 || ^17.0.0" } }, + "node_modules/@tippyjs/react": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/@tippyjs/react/-/react-4.2.6.tgz", + "integrity": "sha512-91RicDR+H7oDSyPycI13q3b7o4O60wa2oRbjlz2fyRLmHImc4vyDwuUP8NtZaN0VARJY5hybvDYrFzhY9+Lbyw==", + "dependencies": { + "tippy.js": "^6.3.1" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", @@ -27477,6 +27490,14 @@ "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=", "dev": true }, + "node_modules/tippy.js": { + "version": "6.3.7", + "resolved": "https://registry.npmjs.org/tippy.js/-/tippy.js-6.3.7.tgz", + "integrity": "sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==", + "dependencies": { + "@popperjs/core": "^2.9.0" + } + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -31768,8 +31789,7 @@ "@popperjs/core": { "version": "2.11.2", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.2.tgz", - "integrity": "sha512-92FRmppjjqz29VMJ2dn+xdyXZBrMlE42AV6Kq6BwjWV7CNUW1hs2FtxSNLQE+gJhaZ6AAmYuO9y8dshhcBl7vA==", - "dev": true + "integrity": "sha512-92FRmppjjqz29VMJ2dn+xdyXZBrMlE42AV6Kq6BwjWV7CNUW1hs2FtxSNLQE+gJhaZ6AAmYuO9y8dshhcBl7vA==" }, "@sinonjs/commons": { "version": "1.8.3", @@ -36771,6 +36791,14 @@ "store2": "^2.12.0" } }, + "@tippyjs/react": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/@tippyjs/react/-/react-4.2.6.tgz", + "integrity": "sha512-91RicDR+H7oDSyPycI13q3b7o4O60wa2oRbjlz2fyRLmHImc4vyDwuUP8NtZaN0VARJY5hybvDYrFzhY9+Lbyw==", + "requires": { + "tippy.js": "^6.3.1" + } + }, "@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", @@ -50810,6 +50838,14 @@ "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=", "dev": true }, + "tippy.js": { + "version": "6.3.7", + "resolved": "https://registry.npmjs.org/tippy.js/-/tippy.js-6.3.7.tgz", + "integrity": "sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==", + "requires": { + "@popperjs/core": "^2.9.0" + } + }, "tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", diff --git a/package.json b/package.json index 96e0a6f085..006b4527bc 100644 --- a/package.json +++ b/package.json @@ -95,6 +95,7 @@ "webpack-cli": "^4.9.1" }, "dependencies": { + "@tippyjs/react": "^4.2.6", "draft-js": "^0.10.5", "draftail": "^1.4.1", "draftjs-filters": "^2.5.0", @@ -110,6 +111,7 @@ "redux-thunk": "^2.3.0", "reselect": "^4.0.0", "telepath-unpack": "^0.0.3", + "tippy.js": "^6.3.7", "uuid": "^8.3.2" }, "scripts": {