{
+ readonly hideInfoBar?: true
+}
+
+/** A page. */
+export default function Page(props: PageProps) {
+ const { hideInfoBar = false, children } = props
+ const [isHelpChatOpen, setIsHelpChatOpen] = React.useState(false)
+ const { unsetModal } = modalProvider.useSetModal()
+ const session = authProvider.useUserSession()
+
+ const doCloseChat = () => {
+ setIsHelpChatOpen(false)
+ }
+
+ React.useEffect(() => {
+ const onClick = () => {
+ if (getSelection()?.type !== 'Range') {
+ unsetModal()
+ }
+ }
+ document.addEventListener('click', onClick)
+ return () => {
+ document.removeEventListener('click', onClick)
+ }
+ }, [/* should never change */ unsetModal])
+
+ return (
+ <>
+ {children}
+ {!hideInfoBar && (
+
+
+
+ )}
+ {/* `session.accessToken` MUST be present in order for the `Chat` component to work. */}
+ {!hideInfoBar && session?.accessToken != null && process.env.ENSO_CLOUD_CHAT_URL != null ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+ >
+ )
+}
diff --git a/app/ide-desktop/lib/dashboard/src/components/Root.tsx b/app/ide-desktop/lib/dashboard/src/components/Root.tsx
index 528eb0ad2627..c1991921fc37 100644
--- a/app/ide-desktop/lib/dashboard/src/components/Root.tsx
+++ b/app/ide-desktop/lib/dashboard/src/components/Root.tsx
@@ -4,6 +4,10 @@ import * as React from 'react'
import * as aria from '#/components/aria'
import * as portal from '#/components/Portal'
+// ============
+// === Root ===
+// ============
+
/** Props for {@link Root}. */
export interface RootProps extends React.PropsWithChildren {
readonly rootRef: React.RefObject
diff --git a/app/ide-desktop/lib/dashboard/src/components/dashboard/Label.tsx b/app/ide-desktop/lib/dashboard/src/components/dashboard/Label.tsx
index 9ec6bdf53b57..bf4264e4f18f 100644
--- a/app/ide-desktop/lib/dashboard/src/components/dashboard/Label.tsx
+++ b/app/ide-desktop/lib/dashboard/src/components/dashboard/Label.tsx
@@ -16,8 +16,6 @@ import * as backend from '#/services/Backend'
/** Props for a {@link Label}. */
interface InternalLabelProps extends Readonly {
- // This matches the capitalization of `data-` attributes in React.
- // eslint-disable-next-line @typescript-eslint/naming-convention
readonly 'data-testid'?: string
/** When true, the button is not faded out even when not hovered. */
readonly active?: boolean
diff --git a/app/ide-desktop/lib/dashboard/src/configurations/inputBindings.ts b/app/ide-desktop/lib/dashboard/src/configurations/inputBindings.ts
index 3ed743a344fe..b2e0e8fa977d 100644
--- a/app/ide-desktop/lib/dashboard/src/configurations/inputBindings.ts
+++ b/app/ide-desktop/lib/dashboard/src/configurations/inputBindings.ts
@@ -14,6 +14,7 @@ import CopyIcon from 'enso-assets/copy.svg'
import DataDownloadIcon from 'enso-assets/data_download.svg'
import DataUploadIcon from 'enso-assets/data_upload.svg'
import DuplicateIcon from 'enso-assets/duplicate.svg'
+import LogoIcon from 'enso-assets/enso_logo.svg'
import OpenIcon from 'enso-assets/open.svg'
import PasteIcon from 'enso-assets/paste.svg'
import PenIcon from 'enso-assets/pen.svg'
@@ -120,4 +121,10 @@ export const BINDINGS = inputBindings.defineBindings({
rebindable: true,
icon: ArrowRightIcon,
},
+ aboutThisApp: {
+ name: 'About Enso',
+ bindings: ['Mod+/'],
+ rebindable: true,
+ icon: LogoIcon,
+ },
})
diff --git a/app/ide-desktop/lib/dashboard/src/layouts/ChatPlaceholder.tsx b/app/ide-desktop/lib/dashboard/src/layouts/ChatPlaceholder.tsx
index b9ef552ea7cf..d4598e192f97 100644
--- a/app/ide-desktop/lib/dashboard/src/layouts/ChatPlaceholder.tsx
+++ b/app/ide-desktop/lib/dashboard/src/layouts/ChatPlaceholder.tsx
@@ -18,14 +18,16 @@ import UnstyledButton from '#/components/UnstyledButton'
/** Props for a {@link ChatPlaceholder}. */
export interface ChatPlaceholderProps {
- /** This should only be false when the panel is closing. */
+ /** This should only be `true` when in the auth flow. */
+ readonly hideLoginButtons?: true
+ /** This should only be `false` when the panel is closing. */
readonly isOpen: boolean
readonly doClose: () => void
}
/** A placeholder component replacing `Chat` when a user is not logged in. */
export default function ChatPlaceholder(props: ChatPlaceholderProps) {
- const { isOpen, doClose } = props
+ const { hideLoginButtons = false, isOpen, doClose } = props
const { getText } = textProvider.useText()
const logger = loggerProvider.useLogger()
const navigate = navigateHooks.useNavigate()
@@ -51,22 +53,26 @@ export default function ChatPlaceholder(props: ChatPlaceholderProps) {
{getText('placeholderChatPrompt')}
- {
- navigate(appUtils.LOGIN_PATH)
- }}
- >
- {getText('login')}
-
- {
- navigate(appUtils.REGISTRATION_PATH)
- }}
- >
- {getText('register')}
-
+ {!hideLoginButtons && (
+ {
+ navigate(appUtils.LOGIN_PATH)
+ }}
+ >
+ {getText('login')}
+
+ )}
+ {!hideLoginButtons && (
+ {
+ navigate(appUtils.REGISTRATION_PATH)
+ }}
+ >
+ {getText('register')}
+
+ )}
,
diff --git a/app/ide-desktop/lib/dashboard/src/layouts/InfoBar.tsx b/app/ide-desktop/lib/dashboard/src/layouts/InfoBar.tsx
new file mode 100644
index 000000000000..d56175b81314
--- /dev/null
+++ b/app/ide-desktop/lib/dashboard/src/layouts/InfoBar.tsx
@@ -0,0 +1,73 @@
+/** @file A toolbar containing chat and the user menu. */
+import * as React from 'react'
+
+import ChatIcon from 'enso-assets/chat.svg'
+import LogoIcon from 'enso-assets/enso_logo.svg'
+
+import * as modalProvider from '#/providers/ModalProvider'
+import * as textProvider from '#/providers/TextProvider'
+
+import InfoMenu from '#/layouts/InfoMenu'
+
+import Button from '#/components/styled/Button'
+import FocusArea from '#/components/styled/FocusArea'
+import SvgMask from '#/components/SvgMask'
+import UnstyledButton from '#/components/UnstyledButton'
+
+// ===============
+// === InfoBar ===
+// ===============
+
+/** Props for a {@link InfoBar}. */
+export interface InfoBarProps {
+ readonly isHelpChatOpen: boolean
+ readonly setIsHelpChatOpen: (isHelpChatOpen: boolean) => void
+}
+
+/** A toolbar containing chat and the user menu. */
+export default function InfoBar(props: InfoBarProps) {
+ const { isHelpChatOpen, setIsHelpChatOpen } = props
+ const { updateModal } = modalProvider.useSetModal()
+ const { getText } = textProvider.useText()
+
+ return (
+