diff --git a/pkg/webui/components/breadcrumbs/breadcrumb/breadcrumb.styl b/pkg/webui/components/breadcrumbs/breadcrumb/breadcrumb.styl index c4ce46675cf..c9e81b013cd 100644 --- a/pkg/webui/components/breadcrumbs/breadcrumb/breadcrumb.styl +++ b/pkg/webui/components/breadcrumbs/breadcrumb/breadcrumb.styl @@ -14,29 +14,30 @@ .container + display: flex + &:not(:last-child):after material-icon() + nudge('down', 1px) margin: 0 $cs.xxs content: 'keyboard_arrow_right' color: $tc-subtle-gray - .link one-liner() text-decoration: none color: $tc-subtle-gray - min-height: 18px + display: flex + align-items: center +focus-visible() text-decoration: underline color: $tc-deep-gray - .last one-liner() display: flex align-items: center color: $tc-deep-gray - min-height: 18px font-weight: $fw.bold +media-query($bp.s) &:last-child diff --git a/pkg/webui/components/breadcrumbs/breadcrumb/index.js b/pkg/webui/components/breadcrumbs/breadcrumb/index.js index a25a39d8ba8..dfe3a7ca856 100644 --- a/pkg/webui/components/breadcrumbs/breadcrumb/index.js +++ b/pkg/webui/components/breadcrumbs/breadcrumb/index.js @@ -30,17 +30,17 @@ const Breadcrumb = ({ className, path, content, isLast }) => { if (!isLast) { Component = Link componentProps = { - className: classnames(className, style.container, style.link), + className: style.link, to: path, secondary: true, } } else { Component = 'span' - componentProps = { className: classnames(className, style.container, style.last) } + componentProps = { className: classnames(className, style.last) } } return ( - + {isRawText ? content : } diff --git a/pkg/webui/components/breadcrumbs/breadcrumbs.js b/pkg/webui/components/breadcrumbs/breadcrumbs.js index 7c8e6a37a31..b7a492c86f4 100644 --- a/pkg/webui/components/breadcrumbs/breadcrumbs.js +++ b/pkg/webui/components/breadcrumbs/breadcrumbs.js @@ -15,7 +15,6 @@ import React from 'react' import ReactDom from 'react-dom' import classnames from 'classnames' -import { Container } from 'react-grid-system' import PropTypes from '@ttn-lw/lib/prop-types' @@ -52,9 +51,7 @@ const PortalledBreadcrumbs = ({ className, ...rest }) => { nodes.push( ReactDom.createPortal(
- - - +
, element, ), diff --git a/pkg/webui/components/breadcrumbs/breadcrumbs.styl b/pkg/webui/components/breadcrumbs/breadcrumbs.styl index 8c23a52e432..c1ab67f57ac 100644 --- a/pkg/webui/components/breadcrumbs/breadcrumbs.styl +++ b/pkg/webui/components/breadcrumbs/breadcrumbs.styl @@ -14,10 +14,10 @@ .breadcrumbs color: $tc-subtle-gray - height: $breadcrumbs-bar-height display: flex align-items: center overflow: hidden + height: 1rem +media-query($bp.s) overflow: auto @@ -30,3 +30,5 @@ &-container box-sizing: border-box height: $breadcrumbs-bar-height + display: flex + align-items: center diff --git a/pkg/webui/components/button/button.styl b/pkg/webui/components/button/button.styl index cd755958fe9..0f904770d0d 100644 --- a/pkg/webui/components/button/button.styl +++ b/pkg/webui/components/button/button.styl @@ -22,7 +22,7 @@ &.primary, &.secondary, &.naked position: relative display: inline-flex - transition: 80ms all ease-in-out + transition: 80ms background ease-in-out, 80ms color ease-in-out, 80ms border-color ease-in-out, 80ms box-shadow ease-in-out border-radius: $br.m outline: 0 cursor: pointer @@ -35,23 +35,20 @@ white-space: nowrap box-sizing: border-box - .arrow-icon - transform: rotate(0deg) - transition: transform .2s ease-in-out - margin-right: - $cs.xs - - .arrow-icon-expanded - transform: rotate(180deg) - transition: transform .2s ease-in-out - .icon margin-left: - $cs.xxs &:only-child margin-right: - $cs.xxs + .expand-icon + color: $c.grey-500 + font-size: 1.285rem + margin-right: - $cs.xxs + &.only-icon gap: 0 + padding: 0 $cs.s &.primary color: white diff --git a/pkg/webui/components/button/index.js b/pkg/webui/components/button/index.js index abcafb59a97..9d853df0253 100644 --- a/pkg/webui/components/button/index.js +++ b/pkg/webui/components/button/index.js @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import React, { useCallback, forwardRef, useMemo, useState, useRef } from 'react' +import React, { useCallback, forwardRef, useMemo, useRef } from 'react' import classnames from 'classnames' import { useIntl } from 'react-intl' @@ -51,7 +51,8 @@ const assembleClassnames = ({ className, error, }) => - classnames(style.button, className, { + classnames(style.button, { + [className]: !Boolean(dropdownItems), // If there are dropdown items, the button is wrapped in a div with the className. [style.danger]: danger, [style.warning]: warning, [style.primary]: primary, @@ -67,7 +68,7 @@ const assembleClassnames = ({ }) const buttonChildren = props => { - const { dropdownItems, icon, busy, message, expanded, noDropdownIcon, children } = props + const { dropdownItems, icon, busy, message, noDropdownIcon, children } = props const content = ( <> @@ -75,16 +76,7 @@ const buttonChildren = props => { {message && } {children} {dropdownItems && ( - <> - {!noDropdownIcon && ( - - )} - + <>{!noDropdownIcon && } )} ) @@ -103,6 +95,7 @@ const Button = forwardRef((props, ref) => { disabled, dropdownItems, dropdownClassName, + dropdownPosition, name, type, value, @@ -111,48 +104,26 @@ const Button = forwardRef((props, ref) => { onBlur, onClick, form, + className, ...rest } = props - const [expanded, setExpanded] = useState(false) const innerRef = useRef() const combinedRef = combineRefs([ref, innerRef]) const dataProps = useMemo(() => filterDataProps(rest), [rest]) - const handleClickOutside = useCallback( - e => { - if (innerRef.current && !innerRef.current.contains(e.target)) { - setExpanded(false) - } - }, - [innerRef], - ) - - const toggleDropdown = useCallback(() => { - setExpanded(oldExpanded => { - const newState = !oldExpanded - if (newState) document.addEventListener('mousedown', handleClickOutside) - else document.removeEventListener('mousedown', handleClickOutside) - return newState - }) - }, [handleClickOutside]) - const handleClick = useCallback( evt => { if (busy || disabled) { return } - if (dropdownItems) { - toggleDropdown() - return - } // Passing a value to the onClick handler is useful for components that // are rendered multiple times, e.g. in a list. The value can be used to // identify the component that was clicked. onClick(evt, value) }, - [busy, disabled, dropdownItems, onClick, toggleDropdown, value], + [busy, disabled, onClick, value], ) const intl = useIntl() @@ -169,7 +140,7 @@ const Button = forwardRef((props, ref) => { ) : ( - + {Boolean(iconElement) ? iconElement : null} - - ) - - const handleMouseEnter = useCallback(() => { - if (leaveDelayTimer) { - clearTimeout(leaveDelayTimer) - setLeaveDelayTimer(null) - } - setExpandedSubmenu(true) - }, [leaveDelayTimer]) - - const handleMouseLeave = useCallback(() => { - // Set a timer when mouse leaves, to only close after a delay. - // This prevents the menu from closing when the mouse moves over the submenu - // and also makes the UI more forgiving when the mouse accidentally leaves the menu. - setLeaveDelayTimer( - setTimeout(() => { - setExpandedSubmenu(false) - }, 250), - ) - }, []) - - useEffect( - () => () => { - if (leaveDelayTimer) { - clearTimeout(leaveDelayTimer) - } - }, - [leaveDelayTimer], + ) - const withSubmenu = ( - + {submenuItems} - - + + ) return ( -
  • - {Boolean(submenuItems) ? withSubmenu : ItemElement} +
  • + {submenu || ItemElement}
  • ) } @@ -216,5 +192,6 @@ DropdownHeaderItem.propTypes = { Dropdown.Item = DropdownItem Dropdown.HeaderItem = DropdownHeaderItem +Dropdown.Attached = AttachedDropdown export default Dropdown diff --git a/pkg/webui/components/header/header.styl b/pkg/webui/components/header/header.styl index 4f555e5b5b7..ab46e66d957 100644 --- a/pkg/webui/components/header/header.styl +++ b/pkg/webui/components/header/header.styl @@ -21,6 +21,10 @@ justify-content: space-between align-items: center height: 4rem + +media-query($bp.xs) + padding: 0 $cs.xs .logo height: $cs.l + +media-query($bp.xs) + width: 3rem diff --git a/pkg/webui/components/header/index.js b/pkg/webui/components/header/index.js index 4e9bd79180a..175b08f41dc 100644 --- a/pkg/webui/components/header/index.js +++ b/pkg/webui/components/header/index.js @@ -17,7 +17,6 @@ import classnames from 'classnames' import Button from '@ttn-lw/components/button' import ProfileDropdown from '@ttn-lw/components/profile-dropdown' -import Link from '@ttn-lw/components/link' import PropTypes from '@ttn-lw/lib/prop-types' @@ -37,16 +36,27 @@ const Header = ({ // Const isGuest = !Boolean(user)
    -
    -
    +
    +
    - ) } diff --git a/pkg/webui/components/sidebar/search-button/search-button.styl b/pkg/webui/components/sidebar/search-button/search-button.styl index dbc26d26213..eb37023e814 100644 --- a/pkg/webui/components/sidebar/search-button/search-button.styl +++ b/pkg/webui/components/sidebar/search-button/search-button.styl @@ -19,23 +19,31 @@ position: relative display: flex justify-content: space-between + align-items: center padding: $cs.xxs $cs.xxs gap: $cs.xxs box-shadow: 0px 2px 7px 1px rgba(0, 0, 0, 0.06) background: white - border: 1px solid $c.grey-100 + border: 1px solid $c.grey-200 border-radius: $br.l transition: border-color 0.08s linear, box-shadow 0.08s linear, color 0.08s linear + &:hover border: 1px solid $c.grey-200 color: $c.grey-700 box-shadow: 0px 2px 7px 0px rgba(0, 0, 0, 0.08) - &-minimized + &.is-minimized justify-content: center padding: $cs.xs $cs.xxs + & > div p + display: none + + .backslash + display: none + p line-height: 1.4 @@ -44,7 +52,8 @@ position: relative top: 1px - .backslash-container + .backslash + margin: 0 height: 1.65rem // 24px width: 1.65rem // 24px text-align: center @@ -54,3 +63,22 @@ display: flex justify-content: center align-items: center + padding-bottom: 1px + + .fly-out-list + margin: 0 + top: - $cs.xs + left: calc(4rem - 4px) // Button width plus 2 borders plus 2px. + position: absolute + z-index: $zi.dropdown + color: $c.grey-900 + + // Pseudo element to extend the clickable area + // to connect with the parent button. + &:before + content: "" + position: absolute + top: 0 + left: -1rem + width: 1rem + height: 100% diff --git a/pkg/webui/components/sidebar/side-header/index.js b/pkg/webui/components/sidebar/side-header/index.js index 74c675aeec2..b3c225e11f0 100644 --- a/pkg/webui/components/sidebar/side-header/index.js +++ b/pkg/webui/components/sidebar/side-header/index.js @@ -16,8 +16,6 @@ import React, { useContext } from 'react' import classnames from 'classnames' import { Link } from 'react-router-dom' -import LAYOUT from '@ttn-lw/constants/layout' - import Button from '@ttn-lw/components/button' import SidebarContext from '@console/containers/side-bar/context' @@ -26,14 +24,8 @@ import PropTypes from '@ttn-lw/lib/prop-types' import style from './side-header.styl' -const getViewportWidth = () => - Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0) - const SideHeader = ({ logo, miniLogo }) => { - const { onMinimizeToggle, isMinimized } = useContext(SidebarContext) - - const viewportWidth = getViewportWidth() - const isMobile = viewportWidth <= LAYOUT.BREAKPOINTS.M + const { onMinimizeToggle, isMinimized, onDrawerCloseClick } = useContext(SidebarContext) return (
    { > {/* Render two logos to prevent layout flashes when switching between minimized and maximized states. */} - - + + - {!isMobile && ( -
    ) } diff --git a/pkg/webui/components/sidebar/side-header/side-header.styl b/pkg/webui/components/sidebar/side-header/side-header.styl index d3274238fed..0903bd5ab39 100644 --- a/pkg/webui/components/sidebar/side-header/side-header.styl +++ b/pkg/webui/components/sidebar/side-header/side-header.styl @@ -19,8 +19,15 @@ gap: $cs.m &.is-minimized - flex-direction: column - gap: $cs.xs + +media-query-min($bp.m) + flex-direction: column + gap: $cs.xs + + .logo + display: none + + .mini-logo + display: flex .minimize-button transition: none @@ -34,4 +41,6 @@ width: 9.57rem .mini-logo + display: none width: 2.8rem + padding: $cs.s 0 diff --git a/pkg/webui/components/sidebar/side-menu/item/link.js b/pkg/webui/components/sidebar/side-menu/item/link.js index b81b6256c5e..12956e442fb 100644 --- a/pkg/webui/components/sidebar/side-menu/item/link.js +++ b/pkg/webui/components/sidebar/side-menu/item/link.js @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import React, { useCallback, useContext } from 'react' +import React, { useCallback, useContext, useRef } from 'react' import { NavLink } from 'react-router-dom' import classnames from 'classnames' @@ -28,6 +28,7 @@ import PropTypes from '@ttn-lw/lib/prop-types' import style from './item.styl' const MenuLink = ({ icon, title, path, onClick, exact, disabled }) => { + const ref = useRef() const { isMinimized } = useContext(SidebarContext) const className = useCallback( @@ -35,21 +36,19 @@ const MenuLink = ({ icon, title, path, onClick, exact, disabled }) => { classnames(style.link, { [style.active]: isActive, [style.disabled]: disabled, - 'j-center': isMinimized, + [style.isMinimized]: isMinimized, }), [disabled, isMinimized], ) return ( - + {icon && }{' '} - {!isMinimized && } + {isMinimized && ( -
    - - - -
    + + + )}
    ) diff --git a/pkg/webui/components/sidebar/switcher/switcher.styl b/pkg/webui/components/sidebar/switcher/switcher.styl index d3039557daa..e0160f6662e 100644 --- a/pkg/webui/components/sidebar/switcher/switcher.styl +++ b/pkg/webui/components/sidebar/switcher/switcher.styl @@ -14,7 +14,7 @@ .switcher-container border-radius: $br.l - border: 1px solid $c.grey-100 + border: 1px solid $c.grey-200 background: white box-shadow: 0px 2px 7px 1px rgba(0, 0, 0, 0.06) display: flex @@ -29,7 +29,9 @@ display: flex justify-content: center width: 100% - transition: background 200ms ease-in-out + transition: background 80ms ease-in-out + position: relative + padding: $cs.s $cs.xxs &.active background: $c.grey-900 @@ -46,6 +48,37 @@ &:not(.active) background: $c.grey-100 + &.is-minimized + flex-direction: column + + .link + padding: $cs.xs 0 + + .caption + display: none + + .icon + display: block + .icon + display: none font-size: $fs.xl font-weight: 300 + + .fly-out-list + margin: 0 + top: - $cs.xs + left: calc(3.5rem - 1px) + position: absolute + z-index: $zi.dropdown + color: $c.grey-900 + + // Pseudo element to extend the clickable area + // to connect with the parent button. + &:before + content: "" + position: absolute + top: 0 + left: -1rem + width: 1rem + height: 100% diff --git a/pkg/webui/console/containers/header/index.js b/pkg/webui/console/containers/header/index.js index 6ca6c5291ea..52354d33118 100644 --- a/pkg/webui/console/containers/header/index.js +++ b/pkg/webui/console/containers/header/index.js @@ -20,6 +20,7 @@ import Dropdown from '@ttn-lw/components/dropdown' import sharedMessages from '@ttn-lw/lib/shared-messages' import { selectAssetsRootPath, selectBrandingRootPath } from '@ttn-lw/lib/selectors/env' +import PropTypes from '@ttn-lw/lib/prop-types' import selectAccountUrl from '@console/lib/selectors/app-config' import { @@ -37,7 +38,7 @@ import Logo from '../logo' const accountUrl = selectAccountUrl() -const Header = () => { +const Header = ({ onMenuClick }) => { const dispatch = useDispatch() const handleLogout = useCallback(() => dispatch(logout()), [dispatch]) @@ -122,9 +123,13 @@ const Header = () => { starDropdownItems={[]} brandLogo={brandLogo} Logo={Logo} - onMenuClick={() => null} + onMenuClick={onMenuClick} /> ) } +Header.propTypes = { + onMenuClick: PropTypes.func.isRequired, +} + export default Header diff --git a/pkg/webui/console/containers/logo/index.js b/pkg/webui/console/containers/logo/index.js index 00aed4c3a72..dd79fc2f80c 100644 --- a/pkg/webui/console/containers/logo/index.js +++ b/pkg/webui/console/containers/logo/index.js @@ -16,24 +16,16 @@ import React from 'react' import LogoComponent from '@ttn-lw/components/logo' -import { - selectAssetsRootPath, - selectBrandingRootPath, - selectApplicationSiteName, -} from '@ttn-lw/lib/selectors/env' +import { selectAssetsRootPath, selectApplicationSiteName } from '@ttn-lw/lib/selectors/env' const logo = { - src: `${selectAssetsRootPath()}/logo.svg`, + src: `${selectAssetsRootPath()}/tts-logo.svg`, alt: `${selectApplicationSiteName()} Logo`, } -const hasCustomBranding = selectBrandingRootPath() !== selectAssetsRootPath() -const brandLogo = hasCustomBranding - ? { - src: `${selectBrandingRootPath()}/logo.svg`, - alt: 'Logo', - } - : undefined - -const Logo = props => +const miniLogo = { + src: `${selectAssetsRootPath()}/tts-logo-icon.svg`, + alt: `${selectApplicationSiteName()} Logo`, +} +const Logo = props => export default Logo diff --git a/pkg/webui/console/containers/side-bar/index.js b/pkg/webui/console/containers/side-bar/index.js index 429c5422d7f..82ac94aa54b 100644 --- a/pkg/webui/console/containers/side-bar/index.js +++ b/pkg/webui/console/containers/side-bar/index.js @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import React, { useCallback, useState } from 'react' +import React, { useCallback, useEffect, useState } from 'react' import { useLocation } from 'react-router-dom' import classnames from 'classnames' @@ -31,15 +31,28 @@ import SwitcherContainer from './switcher' import style from './side-bar.styl' -const getViewportWidth = () => - Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0) - -const Sidebar = ({ isDrawerOpen }) => { - const viewportWidth = getViewportWidth() - const isMobile = viewportWidth <= LAYOUT.BREAKPOINTS.M +const Sidebar = ({ isDrawerOpen, onDrawerCloseClick }) => { const { pathname } = useLocation() const [isMinimized, setIsMinimized] = useState(false) + // Reset minimized state when screen size changes to mobile. + useEffect(() => { + const handleResize = () => { + if (window.innerWidth < LAYOUT.BREAKPOINTS.M) { + setIsMinimized(false) + } + } + + window.addEventListener('resize', handleResize) + + return () => window.removeEventListener('resize', handleResize) + }, []) + + // Close the drawer on navigation changes. + useEffect(() => { + onDrawerCloseClick() + }, [pathname, onDrawerCloseClick]) + const onMinimizeToggle = useCallback(async () => { setIsMinimized(prev => !prev) }, []) @@ -58,36 +71,38 @@ const Sidebar = ({ isDrawerOpen }) => { 'd-flex direction-column j-between gap-cs-m bg-tts-primary-050', { [style.sidebarMinimized]: isMinimized, - [style.sidebarOpen]: isMobile && isDrawerOpen, + [style.sidebarOpen]: isDrawerOpen, 'p-cs-m': !isMinimized, - 'p-vert-cs-l': isMinimized, + 'p-vert-cs-s': isMinimized, 'p-sides-cs-xs': isMinimized, }, ) return ( -