feat(navigation): Add "Command palette" button (#20018)
* Clarify search/actions bar copy * Add command palette to the navbar * Remove `keyboardShortcut` from `NavbarButton` * Remove `systemStatusHealthy` * Fix side action styling * Enforce top-right placement of navbar side action tooltips * Move command palette info to tooltip * Restore pnpm-lock.yaml * Revert "System issue!" removal * Rename "Switch theme" to "Change theme" * Update UI snapshots for `chromium` (2) * Update UI snapshots for `chromium` (2) * Update UI snapshots for `chromium` (2) * Fix tooltip * Update UI snapshots for `chromium` (1) * Update UI snapshots for `chromium` (2) --------- Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 118 KiB |
@ -127,10 +127,6 @@
|
||||
.NavbarButton {
|
||||
position: relative;
|
||||
|
||||
.LemonButton__icon {
|
||||
transition: color 100ms ease, transform 100ms ease;
|
||||
}
|
||||
|
||||
&.NavbarButton--here {
|
||||
&::after {
|
||||
position: absolute;
|
||||
|
@ -1,8 +1,4 @@
|
||||
.KeyboardShortcut {
|
||||
margin: 0 0.125rem;
|
||||
}
|
||||
|
||||
.KeyboardShortcut__key {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import './KeyboardShortcut.scss'
|
||||
|
||||
import clsx from 'clsx'
|
||||
import { isMac } from 'lib/utils'
|
||||
import { isMac, isMobile } from 'lib/utils'
|
||||
|
||||
import { HotKeyOrModifier } from '~/types'
|
||||
|
||||
@ -26,19 +26,24 @@ const MODIFIER_PRIORITY: HotKeyOrModifier[] = ['shift', 'command', 'option']
|
||||
export interface KeyboardShortcutProps extends Partial<Record<HotKeyOrModifier, true>> {
|
||||
/** Whether this shortcut should be shown with muted opacity. */
|
||||
muted?: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function KeyboardShortcut({ muted, ...keys }: KeyboardShortcutProps): JSX.Element {
|
||||
export function KeyboardShortcut({ muted, className, ...keys }: KeyboardShortcutProps): JSX.Element | null {
|
||||
const sortedKeys = Object.keys(keys).sort(
|
||||
(a, b) =>
|
||||
(-MODIFIER_PRIORITY.indexOf(a as HotKeyOrModifier) || 0) -
|
||||
(-MODIFIER_PRIORITY.indexOf(b as HotKeyOrModifier) || 0)
|
||||
) as HotKeyOrModifier[]
|
||||
|
||||
if (isMobile()) {
|
||||
// If the user agent says we're on mobile, then it's unlikely - though of course not impossible -
|
||||
// that there's a physical keyboard. Hence in that case we don't show the keyboard shortcut
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<span
|
||||
className={clsx('KeyboardShortcut KeyboardShortcut__key space-x-0.5', muted && 'KeyboardShortcut--muted')}
|
||||
>
|
||||
<span className={clsx('KeyboardShortcut space-x-0.5', muted && 'KeyboardShortcut--muted', className)}>
|
||||
{sortedKeys.map((key) => (
|
||||
<span key={key}>{KEY_TO_SYMBOL[key] || key}</span>
|
||||
))}
|
||||
|
@ -17,6 +17,7 @@ import { navigationLogic } from '~/layout/navigation/navigationLogic'
|
||||
import { AccountPopoverOverlay } from '~/layout/navigation/TopBar/AccountPopover'
|
||||
|
||||
import { navigation3000Logic } from '../navigationLogic'
|
||||
import { KeyboardShortcut } from './KeyboardShortcut'
|
||||
import { NavbarButton } from './NavbarButton'
|
||||
|
||||
export function Navbar(): JSX.Element {
|
||||
@ -74,9 +75,20 @@ export function Navbar(): JSX.Element {
|
||||
<NavbarButton
|
||||
identifier="search-button"
|
||||
icon={<IconSearch />}
|
||||
title="Search"
|
||||
shortTitle="Search"
|
||||
title={
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<span>
|
||||
For search, press <KeyboardShortcut command k />
|
||||
</span>
|
||||
<span>
|
||||
For commands, press <KeyboardShortcut command shift k />
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
forceTooltipOnHover
|
||||
sideIcon={<KeyboardShortcut command k />}
|
||||
onClick={toggleSearchBar}
|
||||
keyboardShortcut={{ command: true, k: true }}
|
||||
/>
|
||||
<NavbarButton
|
||||
icon={<IconToolbar />}
|
||||
|
@ -4,7 +4,6 @@ import { useActions, useValues } from 'kea'
|
||||
import { useFeatureFlag } from 'lib/hooks/useFeatureFlag'
|
||||
import { LemonButton, LemonButtonProps } from 'lib/lemon-ui/LemonButton'
|
||||
import { Tooltip } from 'lib/lemon-ui/Tooltip'
|
||||
import { isMobile } from 'lib/utils'
|
||||
import React, { FunctionComponent, ReactElement, useState } from 'react'
|
||||
import { sceneLogic } from 'scenes/sceneLogic'
|
||||
|
||||
@ -13,7 +12,6 @@ import { SidebarChangeNoticeContent, useSidebarChangeNotices } from '~/layout/na
|
||||
|
||||
import { navigation3000Logic } from '../navigationLogic'
|
||||
import { NavbarItem } from '../types'
|
||||
import { KeyboardShortcut, KeyboardShortcutProps } from './KeyboardShortcut'
|
||||
|
||||
export interface NavbarButtonProps extends Pick<LemonButtonProps, 'onClick' | 'icon' | 'sideIcon' | 'to' | 'active'> {
|
||||
identifier: string
|
||||
@ -22,136 +20,110 @@ export interface NavbarButtonProps extends Pick<LemonButtonProps, 'onClick' | 'i
|
||||
shortTitle?: string
|
||||
forceTooltipOnHover?: boolean
|
||||
tag?: 'alpha' | 'beta' | 'new'
|
||||
keyboardShortcut?: KeyboardShortcutProps
|
||||
sideAction?: NavbarItem['sideAction']
|
||||
}
|
||||
|
||||
export const NavbarButton: FunctionComponent<NavbarButtonProps> = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
NavbarButtonProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
identifier,
|
||||
shortTitle,
|
||||
title,
|
||||
forceTooltipOnHover,
|
||||
tag,
|
||||
onClick,
|
||||
keyboardShortcut,
|
||||
sideAction,
|
||||
sideIcon,
|
||||
...rest
|
||||
},
|
||||
ref
|
||||
): JSX.Element => {
|
||||
const { activeScene } = useValues(sceneLogic)
|
||||
const { sceneBreadcrumbKeys } = useValues(breadcrumbsLogic)
|
||||
const { hideNavOnMobile } = useActions(navigation3000Logic)
|
||||
const { isNavCollapsed } = useValues(navigation3000Logic)
|
||||
const isUsingNewNav = useFeatureFlag('POSTHOG_3000_NAV')
|
||||
>(({ identifier, shortTitle, title, forceTooltipOnHover, tag, onClick, sideAction, ...rest }, ref): JSX.Element => {
|
||||
const { activeScene } = useValues(sceneLogic)
|
||||
const { sceneBreadcrumbKeys } = useValues(breadcrumbsLogic)
|
||||
const { hideNavOnMobile } = useActions(navigation3000Logic)
|
||||
const { isNavCollapsed } = useValues(navigation3000Logic)
|
||||
const isUsingNewNav = useFeatureFlag('POSTHOG_3000_NAV')
|
||||
|
||||
const [hasBeenClicked, setHasBeenClicked] = useState(false)
|
||||
const [hasBeenClicked, setHasBeenClicked] = useState(false)
|
||||
|
||||
const here = activeScene === identifier || sceneBreadcrumbKeys.includes(identifier)
|
||||
const isNavCollapsedActually = isNavCollapsed || isUsingNewNav
|
||||
const here = activeScene === identifier || sceneBreadcrumbKeys.includes(identifier)
|
||||
const isNavCollapsedActually = isNavCollapsed || isUsingNewNav
|
||||
|
||||
const buttonProps: LemonButtonProps = rest
|
||||
if (!isUsingNewNav) {
|
||||
buttonProps.active = here
|
||||
}
|
||||
if (!isNavCollapsedActually) {
|
||||
if (sideAction) {
|
||||
// @ts-expect-error - in this case we are perfectly okay with assigning a sideAction
|
||||
buttonProps.sideAction = {
|
||||
...sideAction,
|
||||
divider: true,
|
||||
'data-attr': `menu-item-${sideAction.identifier.toLowerCase()}`,
|
||||
}
|
||||
buttonProps.sideIcon = null
|
||||
} else if (keyboardShortcut && !isMobile()) {
|
||||
// If the user agent says we're on mobile, then it's unlikely - but not impossible -
|
||||
// that there's a physical keyboard. Hence in that case we don't show the keyboard shortcut
|
||||
buttonProps.sideIcon = (
|
||||
<span className="text-xs">
|
||||
<KeyboardShortcut {...keyboardShortcut} />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
let content: JSX.Element | string | undefined
|
||||
if (!isNavCollapsedActually) {
|
||||
content = shortTitle || title
|
||||
if (tag) {
|
||||
content = (
|
||||
<>
|
||||
<span className="grow">{content}</span>
|
||||
<LemonTag
|
||||
type={tag === 'alpha' ? 'completion' : tag === 'beta' ? 'warning' : 'success'}
|
||||
size="small"
|
||||
className="ml-2"
|
||||
>
|
||||
{tag.toUpperCase()}
|
||||
</LemonTag>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const buttonContent = (
|
||||
<LemonButton
|
||||
ref={ref}
|
||||
data-attr={`menu-item-${identifier.toString().toLowerCase()}`}
|
||||
onMouseEnter={() => setHasBeenClicked(false)}
|
||||
onClick={(e) => {
|
||||
if (buttonProps.to) {
|
||||
hideNavOnMobile()
|
||||
}
|
||||
setHasBeenClicked(true)
|
||||
onClick?.(e)
|
||||
}}
|
||||
className={clsx('NavbarButton', isUsingNewNav && here && 'NavbarButton--here')}
|
||||
fullWidth
|
||||
type="secondary"
|
||||
status="alt"
|
||||
{...buttonProps}
|
||||
>
|
||||
{content}
|
||||
</LemonButton>
|
||||
)
|
||||
|
||||
const [notices, onAcknowledged] = useSidebarChangeNotices({ identifier })
|
||||
|
||||
return (
|
||||
<li className="w-full">
|
||||
{notices.length ? (
|
||||
<Tooltip
|
||||
title={<SidebarChangeNoticeContent notices={notices} onAcknowledged={onAcknowledged} />}
|
||||
placement={notices[0].placement ?? 'right'}
|
||||
delayMs={0}
|
||||
visible={true}
|
||||
>
|
||||
{buttonContent}
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Tooltip
|
||||
title={
|
||||
forceTooltipOnHover || isNavCollapsedActually
|
||||
? here
|
||||
? `${title} (you are here)`
|
||||
: title
|
||||
: null
|
||||
}
|
||||
placement="right"
|
||||
delayMs={0}
|
||||
visible={hasBeenClicked ? false : undefined} // Force-hide tooltip after button click
|
||||
>
|
||||
{buttonContent}
|
||||
</Tooltip>
|
||||
)}
|
||||
</li>
|
||||
)
|
||||
const buttonProps: LemonButtonProps = rest
|
||||
if (!isUsingNewNav) {
|
||||
buttonProps.active = here
|
||||
}
|
||||
)
|
||||
let content: JSX.Element | string | undefined
|
||||
if (!isNavCollapsedActually) {
|
||||
content = shortTitle || title
|
||||
if (tag) {
|
||||
content = (
|
||||
<>
|
||||
<span className="grow">{content}</span>
|
||||
<LemonTag
|
||||
type={tag === 'alpha' ? 'completion' : tag === 'beta' ? 'warning' : 'success'}
|
||||
size="small"
|
||||
className="ml-2"
|
||||
>
|
||||
{tag.toUpperCase()}
|
||||
</LemonTag>
|
||||
</>
|
||||
)
|
||||
}
|
||||
if (sideAction) {
|
||||
// @ts-expect-error - in this case we are perfectly okay with assigning a sideAction
|
||||
buttonProps.sideAction = {
|
||||
...sideAction,
|
||||
divider: true,
|
||||
'data-attr': `menu-item-${sideAction.identifier.toLowerCase()}`,
|
||||
}
|
||||
buttonProps.sideIcon = null
|
||||
}
|
||||
} else {
|
||||
buttonProps.sideIcon = null
|
||||
}
|
||||
|
||||
const buttonContent = (
|
||||
<LemonButton
|
||||
ref={ref}
|
||||
data-attr={`menu-item-${identifier.toString().toLowerCase()}`}
|
||||
onMouseEnter={() => setHasBeenClicked(false)}
|
||||
onClick={(e) => {
|
||||
if (buttonProps.to) {
|
||||
hideNavOnMobile()
|
||||
}
|
||||
setHasBeenClicked(true)
|
||||
onClick?.(e)
|
||||
}}
|
||||
className={clsx('NavbarButton', isUsingNewNav && here && 'NavbarButton--here')}
|
||||
fullWidth
|
||||
type="secondary"
|
||||
status="alt"
|
||||
{...buttonProps}
|
||||
>
|
||||
{content}
|
||||
</LemonButton>
|
||||
)
|
||||
|
||||
const [notices, onAcknowledged] = useSidebarChangeNotices({ identifier })
|
||||
|
||||
return (
|
||||
<li className="w-full">
|
||||
{notices.length ? (
|
||||
<Tooltip
|
||||
title={<SidebarChangeNoticeContent notices={notices} onAcknowledged={onAcknowledged} />}
|
||||
placement={notices[0].placement ?? 'right'}
|
||||
delayMs={0}
|
||||
visible={true}
|
||||
>
|
||||
{buttonContent}
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Tooltip
|
||||
title={
|
||||
forceTooltipOnHover || isNavCollapsedActually
|
||||
? here
|
||||
? `${title} (you are here)`
|
||||
: title
|
||||
: null
|
||||
}
|
||||
placement="right"
|
||||
delayMs={0}
|
||||
visible={hasBeenClicked ? false : undefined} // Force-hide tooltip after button click
|
||||
>
|
||||
{buttonContent}
|
||||
</Tooltip>
|
||||
)}
|
||||
</li>
|
||||
)
|
||||
})
|
||||
NavbarButton.displayName = 'NavbarButton'
|
||||
|
@ -30,7 +30,7 @@ interface NavbarItemBase {
|
||||
icon: JSX.Element
|
||||
featureFlag?: (typeof FEATURE_FLAGS)[keyof typeof FEATURE_FLAGS]
|
||||
tag?: 'alpha' | 'beta' | 'new'
|
||||
sideAction?: Pick<SideAction, 'icon' | 'to' | 'onClick' | 'tooltip' | 'dropdown'> & { identifier: string }
|
||||
sideAction?: Omit<SideAction, 'divider' | 'data-attr' | 'tooltipPlacement'> & { identifier: string }
|
||||
}
|
||||
export interface SceneNavbarItem extends NavbarItemBase {
|
||||
to: string
|
||||
|
@ -3,6 +3,7 @@ import { LemonInput } from '@posthog/lemon-ui'
|
||||
import { useActions, useValues } from 'kea'
|
||||
import { CommandFlow } from 'lib/components/CommandPalette/commandPaletteLogic'
|
||||
import { IconChevronRight } from 'lib/lemon-ui/icons'
|
||||
import { isMac } from 'lib/utils'
|
||||
import React from 'react'
|
||||
|
||||
import { KeyboardShortcut } from '~/layout/navigation-3000/components/KeyboardShortcut'
|
||||
@ -33,7 +34,9 @@ export const ActionInput = (): JSX.Element => {
|
||||
fullWidth
|
||||
prefix={<PrefixIcon activeFlow={activeFlow} />}
|
||||
suffix={<KeyboardShortcut escape />}
|
||||
placeholder={activeFlow?.instruction ?? 'Run a command…'}
|
||||
placeholder={
|
||||
activeFlow?.instruction ?? `Run a command… or press ${isMac() ? 'Delete' : 'Backspace'} for search`
|
||||
}
|
||||
autoFocus
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { LemonButton, LemonInput } from '@posthog/lemon-ui'
|
||||
import { LemonInput } from '@posthog/lemon-ui'
|
||||
import { useActions, useValues } from 'kea'
|
||||
import { isMac } from 'lib/utils'
|
||||
import { forwardRef, Ref } from 'react'
|
||||
import { teamLogic } from 'scenes/teamLogic'
|
||||
|
||||
@ -8,15 +7,12 @@ import { KeyboardShortcut } from '~/layout/navigation-3000/components/KeyboardSh
|
||||
|
||||
import { searchBarLogic } from './searchBarLogic'
|
||||
|
||||
export const SearchInput = forwardRef(function _SearchInput(_, ref: Ref<HTMLInputElement>): JSX.Element {
|
||||
export const SearchInput = forwardRef(function SearchInput(_, ref: Ref<HTMLInputElement>): JSX.Element {
|
||||
const { currentTeam } = useValues(teamLogic)
|
||||
const { searchQuery } = useValues(searchBarLogic)
|
||||
const { setSearchQuery, hideCommandBar } = useActions(searchBarLogic)
|
||||
const { setSearchQuery } = useActions(searchBarLogic)
|
||||
|
||||
const modifierKey = isMac() ? '⌘' : '^'
|
||||
const placeholder = currentTeam
|
||||
? `Search the ${currentTeam.name} project or press ${modifierKey}⇧K to go to commands…`
|
||||
: `Search or press ${modifierKey}⇧K to go to commands…`
|
||||
const placeholder = `Search ${currentTeam ? 'the ' + currentTeam.name : 'this'} project… or enter > for commands`
|
||||
|
||||
return (
|
||||
<div className="border-b">
|
||||
@ -26,11 +22,7 @@ export const SearchInput = forwardRef(function _SearchInput(_, ref: Ref<HTMLInpu
|
||||
type="search"
|
||||
className="CommandBar__input"
|
||||
fullWidth
|
||||
suffix={
|
||||
<LemonButton onClick={() => hideCommandBar()} noPadding>
|
||||
<KeyboardShortcut escape />
|
||||
</LemonButton>
|
||||
}
|
||||
suffix={<KeyboardShortcut escape />}
|
||||
placeholder={placeholder}
|
||||
autoFocus
|
||||
value={searchQuery}
|
||||
|
@ -839,10 +839,10 @@ export const commandPaletteLogic = kea<commandPaletteLogicType>([
|
||||
scope: GLOBAL_COMMAND_SCOPE,
|
||||
resolver: {
|
||||
icon: IconEye,
|
||||
display: 'Switch theme',
|
||||
display: 'Change theme',
|
||||
synonyms: ['toggle theme', 'dark mode', 'light mode'],
|
||||
executor: () => ({
|
||||
scope: 'Switch theme',
|
||||
scope: 'Change theme',
|
||||
resolver: [
|
||||
{
|
||||
icon: IconDay,
|
||||
|
@ -379,7 +379,7 @@
|
||||
var(--lemon-button-side-action-width) -
|
||||
var(--lemon-button-padding-right, var(--lemon-button-padding-horizontal))
|
||||
);
|
||||
height: 1.25rem;
|
||||
height: calc(var(--lemon-button-height) - 1.0625rem);
|
||||
color: var(--muted);
|
||||
|
||||
&--divider {
|
||||
@ -403,7 +403,6 @@
|
||||
|
||||
.LemonButton {
|
||||
--lemon-button-depth: 0px;
|
||||
--lemon-button-icon-opacity: 0.5;
|
||||
|
||||
width: var(--lemon-button-side-action-width);
|
||||
height: 100%;
|
||||
|
@ -259,12 +259,12 @@ export const LemonButton: React.FunctionComponent<LemonButtonProps & React.RefAt
|
||||
{workingButton}
|
||||
<div className="LemonButtonWithSideAction__side-button">
|
||||
<SideComponent
|
||||
// We don't want secondary style as it creates double borders
|
||||
type={type !== 'secondary' ? type : undefined}
|
||||
type={type}
|
||||
size={size}
|
||||
status={status}
|
||||
dropdown={sideDropdown as LemonButtonDropdown}
|
||||
noPadding
|
||||
active={active}
|
||||
{...sideActionRest}
|
||||
/>
|
||||
</div>
|
||||
|