Browse Source

feat(toolbar): Iterate on the toolbar color/style and close button (#74924)

The main floating panel has some new colors now, but it also fades away
after 3seconds so it's not fulling in people's face

This is to address the feedback that:
a) it's too blended in when the app has a white background
b) it stands out too much when there's a red badge on a button

We can style it better with some more effort, i was trying to invert the
background when it's faded but had trouble because of the gradient and
transitions not playing together or something. We can get fancier too
and make it like slide away and be replaced with only a sentry logo or
something idk.

| With an open panel | Without a panel open, after 3s |
| --- | --- |
|
![SCR-20240724-ouox](https://github.com/user-attachments/assets/4880d6ec-de71-4f3d-b096-cfc524ce3603)
|
![SCR-20240724-owiz](https://github.com/user-attachments/assets/5350a4ef-78a3-4281-998f-122d4291ead4)
Ryan Albrecht 7 months ago
parent
commit
74b3b5b626

+ 1 - 1
static/app/components/devtoolbar/components/alerts/alertCountBadge.tsx

@@ -8,5 +8,5 @@ export default function AlertCountBadge() {
   if (count === undefined) {
     return null;
   }
-  return <CountBadge value={count} />;
+  return <CountBadge value={count} variant="red" />;
 }

+ 19 - 3
static/app/components/devtoolbar/components/countBadge.tsx

@@ -2,14 +2,30 @@ import {css} from '@emotion/react';
 
 import {smallCss} from '../styles/typography';
 
-export default function CountBadge({value}: {value: number}) {
-  return <div css={[smallCss, counterCss]}>{value}</div>;
+/**
+ * If you want more variants/colors then add to this record:
+ */
+const variants = {
+  red: css`
+    background: var(--red400);
+    color: var(--gray100);
+  `,
+};
+
+interface Props {
+  value: number;
+  variant: keyof typeof variants;
+}
+
+export default function CountBadge({value, variant}: Props) {
+  return <div css={[smallCss, counterCss, variants[variant]]}>{value}</div>;
 }
 
 const counterCss = css`
-  background: red;
   background: var(--red400);
   border-radius: 50%;
+  border: 1px solid transparent;
+  box-sizing: content-box;
   color: var(--gray100);
   height: 1rem;
   line-height: 1rem;

+ 24 - 55
static/app/components/devtoolbar/components/navigation.tsx

@@ -1,7 +1,5 @@
 import type {ReactNode} from 'react';
-import {css} from '@emotion/react';
 
-import InteractionStateLayer from 'sentry/components/interactionStateLayer';
 import {
   IconClose,
   IconFlag,
@@ -14,11 +12,11 @@ import {
 import useConfiguration from '../hooks/useConfiguration';
 import usePlacementCss from '../hooks/usePlacementCss';
 import useToolbarRoute from '../hooks/useToolbarRoute';
-import {navigationButtonCss, navigationCss} from '../styles/navigation';
-import {resetButtonCss, resetDialogCss} from '../styles/reset';
-import {buttonCss} from '../styles/typography';
+import {navigationCss} from '../styles/navigation';
+import {resetDialogCss} from '../styles/reset';
 
 import AlertCountBadge from './alerts/alertCountBadge';
+import IconButton from './navigation/iconButton';
 
 export default function Navigation({
   setIsDisabled,
@@ -28,23 +26,15 @@ export default function Navigation({
   const {trackAnalytics} = useConfiguration();
   const placement = usePlacementCss();
 
+  const {state: route} = useToolbarRoute();
+  const isRouteActive = !!route.activePanel;
+
   return (
     <dialog
-      css={[
-        resetDialogCss,
-        navigationCss,
-        hideButtonContainerCss,
-        placement.navigation.css,
-      ]}
+      css={[resetDialogCss, navigationCss, placement.navigation.css]}
+      data-has-active={isRouteActive}
     >
-      <NavButton panelName="issues" label="Issues" icon={<IconIssues />} />
-      <NavButton panelName="feedback" label="User Feedback" icon={<IconMegaphone />} />
-      <NavButton panelName="alerts" label="Active Alerts" icon={<IconSiren />}>
-        <AlertCountBadge />
-      </NavButton>
-      <NavButton panelName="featureFlags" label="Feature Flags" icon={<IconFlag />} />
-      <NavButton panelName="releases" label="Releases" icon={<IconReleases />} />
-      <HideButton
+      <IconButton
         onClick={() => {
           setIsDisabled(true);
           trackAnalytics?.({
@@ -52,7 +42,19 @@ export default function Navigation({
             eventName: `devtoolbar: Hide devtoolbar`,
           });
         }}
+        title="Hide for this session"
+        icon={<IconClose />}
       />
+
+      <hr style={{margin: 0, width: '100%'}} />
+
+      <NavButton panelName="issues" label="Issues" icon={<IconIssues />} />
+      <NavButton panelName="feedback" label="User Feedback" icon={<IconMegaphone />} />
+      <NavButton panelName="alerts" label="Active Alerts" icon={<IconSiren />}>
+        <AlertCountBadge />
+      </NavButton>
+      <NavButton panelName="featureFlags" label="Feature Flags" icon={<IconFlag />} />
+      <NavButton panelName="releases" label="Releases" icon={<IconReleases />} />
     </dialog>
   );
 }
@@ -74,10 +76,9 @@ function NavButton({
   const isActive = state.activePanel === panelName;
 
   return (
-    <button
-      aria-label={label}
-      css={[resetButtonCss, navigationButtonCss]}
+    <IconButton
       data-active-route={isActive}
+      icon={icon}
       onClick={() => {
         setActivePanel(isActive ? null : panelName);
         trackAnalytics?.({
@@ -87,39 +88,7 @@ function NavButton({
       }}
       title={label}
     >
-      <InteractionStateLayer />
-      {icon}
       {children}
-    </button>
-  );
-}
-
-const hideButtonContainerCss = css`
-  :hover button {
-    visibility: visible;
-  }
-`;
-const hideButtonCss = css`
-  border-radius: 50%;
-  color: var(--gray300);
-  height: 1.6rem;
-  left: -10px;
-  position: absolute;
-  top: -10px;
-  visibility: hidden;
-  width: 1.6rem;
-  z-index: 1;
-`;
-
-function HideButton({onClick}: {onClick: () => void}) {
-  return (
-    <button
-      aria-label="Hide for this session"
-      css={[resetButtonCss, buttonCss, hideButtonCss]}
-      onClick={onClick}
-      title="Hide for this session"
-    >
-      <IconClose isCircled />
-    </button>
+    </IconButton>
   );
 }

+ 58 - 0
static/app/components/devtoolbar/components/navigation/iconButton.tsx

@@ -0,0 +1,58 @@
+import type {ButtonHTMLAttributes, ReactNode} from 'react';
+import {css, type Interpolation} from '@emotion/react';
+
+import InteractionStateLayer from 'sentry/components/interactionStateLayer';
+
+import {resetButtonCss} from '../../styles/reset';
+import {buttonCss} from '../../styles/typography';
+
+const iconButtonCss = css`
+  border: 1px solid transparent;
+  background: none;
+  color: white;
+  padding: var(--space100) var(--space100);
+  border-radius: var(--space100);
+  gap: var(--space50);
+
+  &:hover:not(:disabled) {
+    border-color: white;
+  }
+
+  &[data-active-route='true'] {
+    background: white;
+    color: var(--gray400);
+  }
+`;
+
+type BaseButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
+  css?: Interpolation<any>;
+};
+
+interface IconButtonProps extends BaseButtonProps {
+  icon: ReactNode;
+  title: string;
+  children?: ReactNode;
+  onClick?: () => void;
+}
+
+export default function IconButton({
+  children,
+  icon,
+  onClick,
+  title,
+  ...props
+}: IconButtonProps) {
+  return (
+    <button
+      aria-label={title}
+      css={[resetButtonCss, buttonCss, iconButtonCss]}
+      onClick={onClick}
+      title={title}
+      {...props}
+    >
+      <InteractionStateLayer />
+      {icon}
+      {children}
+    </button>
+  );
+}

+ 17 - 18
static/app/components/devtoolbar/styles/navigation.ts

@@ -2,12 +2,26 @@ import {css} from '@emotion/react';
 
 export const navigationCss = css`
   align-items: center;
-  background: white;
-  border: 1px solid var(--gray200);
-  border-radius: var(--space150);
   display: flex;
   gap: var(--space50);
   padding: var(--space50);
+
+  background: rgb(58, 46, 93);
+  background: linear-gradient(41deg, rgba(58, 46, 93, 1) 61%, rgba(136, 81, 145, 1) 100%);
+  border-radius: var(--space150);
+  border: 1px solid var(--gray200);
+  color: white;
+
+  transition: opacity 2s ease-out 3s;
+
+  &[data-has-active='false'] {
+    opacity: 0.15;
+  }
+
+  &:hover {
+    transition: opacity 0.1s ease-in 0s;
+    opacity: 1;
+  }
 `;
 
 export const navigationRightEdgeCss = css`
@@ -19,18 +33,3 @@ export const navigationRightEdgeCss = css`
 export const navigationBottomRightCornerCss = css`
   flex-direction: row;
 `;
-
-export const navigationButtonCss = css`
-  border: 1px solid transparent;
-  background: none;
-  color: var(--gray400);
-  padding: var(--space100) var(--space150);
-  border-radius: var(--space100);
-  gap: var(--space50);
-
-  &[data-active-route='true'] {
-    color: var(--purple300);
-    background: var(--purple100);
-    border-color: var(--purple100);
-  }
-`;