diff --git a/app/ide-desktop/lib/dashboard/src/components/AriaComponents/Button/Button.tsx b/app/ide-desktop/lib/dashboard/src/components/AriaComponents/Button/Button.tsx index 295b123dc9..6f7750d8f3 100644 --- a/app/ide-desktop/lib/dashboard/src/components/AriaComponents/Button/Button.tsx +++ b/app/ide-desktop/lib/dashboard/src/components/AriaComponents/Button/Button.tsx @@ -10,14 +10,16 @@ import * as ariaComponents from '#/components/AriaComponents' import Spinner, * as spinnerModule from '#/components/Spinner' import SvgMask from '#/components/SvgMask' +import * as text from '../Text' + // ============== // === Button === // ============== /** Props for a {@link Button}. */ export type ButtonProps = - | (BaseButtonProps & Omit & PropsWithoutHref) - | (BaseButtonProps & Omit & PropsWithHref) + | (BaseButtonProps & Omit & PropsWithoutHref) + | (BaseButtonProps & Omit & PropsWithHref) /** * Props for a button with an href. @@ -54,7 +56,7 @@ export interface BaseButtonProps extends Omit Promise | void - + readonly children?: React.ReactNode readonly testId?: string readonly formnovalidate?: boolean @@ -65,20 +67,62 @@ export const BUTTON_STYLES = twv.tv({ variants: { isDisabled: { true: 'disabled:opacity-50 disabled:cursor-not-allowed' }, isFocused: { - true: 'focus:outline-none focus-visible:outline focus-visible:outline-primary', + true: 'focus:outline-none focus-visible:outline focus-visible:outline-primary focus-visible:outline-offset-2', }, loading: { true: { base: 'cursor-wait' } }, fullWidth: { true: 'w-full' }, size: { - custom: '', - hero: 'px-8 py-4 text-lg font-bold', - large: 'px-6 py-3 text-base font-bold', - medium: 'px-4 py-2 text-sm font-bold', - small: 'px-3 pt-1 pb-[5px] text-xs font-medium', - xsmall: 'px-2 pt-1 pb-[5px] text-xs font-medium', - xxsmall: 'px-1.5 pt-1 pb-[5px] text-xs font-medium', + custom: { base: '', extraClickZone: 'after:inset-[-12px]' }, + hero: { base: 'px-8 py-4 text-lg font-bold', content: 'gap-[0.75em]' }, + large: { + base: 'px-[11px] py-[5px]', + content: 'gap-2', + text: text.TEXT_STYLE({ + variant: 'body', + color: 'custom', + weight: 'bold', + }), + extraClickZone: 'after:inset-[-6px]', + }, + medium: { + base: 'px-[9px] py-[3px]', + text: text.TEXT_STYLE({ + variant: 'body', + color: 'custom', + weight: 'bold', + }), + content: 'gap-2', + extraClickZone: 'after:inset-[-8px]', + }, + small: { + base: 'px-[7px] py-[1px]', + content: 'gap-1', + text: text.TEXT_STYLE({ + variant: 'body', + color: 'custom', + }), + extraClickZone: 'after:inset-[-10px]', + }, + xsmall: { + base: 'px-[5px] py-[1px]', + content: 'gap-1', + text: text.TEXT_STYLE({ + variant: 'body', + color: 'custom', + }), + extraClickZone: 'after:inset-[-12px]', + }, + xxsmall: { + base: 'px-[3px] py-[0px]', + content: 'gap-0.5', + text: text.TEXT_STYLE({ + variant: 'body', + color: 'custom', + }), + extraClickZone: 'after:inset-[-12px]', + }, }, - iconOnly: { true: '' }, + iconOnly: { true: { base: '', icon: 'w-full h-full' } }, rounded: { full: 'rounded-full', large: 'rounded-lg', @@ -91,17 +135,23 @@ export const BUTTON_STYLES = twv.tv({ }, variant: { custom: 'focus-visible:outline-offset-2', - link: 'inline-flex px-0 py-0 rounded-sm text-primary/50 underline hover:text-primary focus-visible:outline-offset-0', - primary: 'bg-primary text-white hover:bg-primary/70 focus-visible:outline-offset-2', - tertiary: 'bg-share text-white hover:bg-share/90 focus-visible:outline-offset-2', - cancel: 'bg-selected-frame opacity-80 hover:opacity-100 focus-visible:outline-offset-2', - delete: 'bg-delete text-white focus-visible:outline-offset-2', + link: { + base: 'inline-flex px-0 py-0 rounded-sm text-primary/50 underline hover:text-primary border-none', + icon: 'h-[1.25cap] mt-0.5', + }, + primary: 'bg-primary text-white hover:bg-primary/70', + tertiary: 'bg-share text-white hover:bg-share/90', + cancel: 'bg-white/50 hover:bg-white', + delete: + 'bg-danger/80 hover:bg-danger text-white focus-visible:outline-danger focus-visible:bg-danger', icon: { - base: 'opacity-70 hover:opacity-100 focus-visible:opacity-100 focus-visible:outline-offset-0', + base: 'opacity-80 hover:opacity-100 focus-visible:opacity-100', wrapper: 'w-full h-full', content: 'w-full h-full', - icon: 'w-fit h-fit', + extraClickZone: 'w-full h-full', }, + ghost: + 'opacity-80 hover:opacity-100 hover:bg-white focus-visible:opacity-100 focus-visible:bg-white', submit: 'bg-invite text-white opacity-80 hover:opacity-100 focus-visible:outline-offset-2', outline: 'border-primary/40 text-primary hover:border-primary focus-visible:outline-offset-2', }, @@ -114,11 +164,12 @@ export const BUTTON_STYLES = twv.tv({ }, }, slots: { - extraClickZone: 'flex relative after:inset-[-12px] after:absolute', + extraClickZone: 'flex relative after:absolute after:cursor-pointer', wrapper: 'relative block', loader: 'absolute inset-0 flex items-center justify-center', content: 'flex items-center gap-[0.5em]', - icon: 'h-[1.5em] flex-none', + text: '', + icon: 'h-[2cap] flex-none aspect-square', }, defaultVariants: { loading: false, @@ -130,12 +181,28 @@ export const BUTTON_STYLES = twv.tv({ showIconOnHover: false, }, compoundVariants: [ - { variant: 'icon', size: 'xxsmall', class: 'p-0.5 rounded-full', iconOnly: true }, - { variant: 'icon', size: 'xsmall', class: 'p-1 rounded-full', iconOnly: true }, - { variant: 'icon', size: 'small', class: 'p-1 rounded-full', iconOnly: true }, - { variant: 'icon', size: 'medium', class: 'p-2 rounded-full', iconOnly: true }, - { variant: 'icon', size: 'large', class: 'p-3 rounded-full', iconOnly: true }, - { variant: 'icon', size: 'hero', class: 'p-4 rounded-full', iconOnly: true }, + { variant: 'icon', isFocused: true, iconOnly: true, class: 'focus-visible:outline-offset-0' }, + { + variant: 'link', + isFocused: true, + class: 'focus-visible:outline-offset-1', + }, + { + variant: 'icon', + size: 'xxsmall', + class: 'aspect-square rounded-full w-4 p-0', + iconOnly: true, + }, + { + variant: 'icon', + size: 'xsmall', + class: 'aspect-square p-0 rounded-full w-5', + iconOnly: true, + }, + { size: 'small', class: 'aspect-square p-0 rounded-full w-6', iconOnly: true }, + { size: 'medium', class: 'aspect-square p-0 rounded-full w-8', iconOnly: true }, + { size: 'large', class: 'aspect-square p-0 rounded-full w-10', iconOnly: true }, + { size: 'hero', class: 'aspect-square p-0 rounded-full w-16', iconOnly: true }, { variant: 'link', size: 'xxsmall', class: 'font-medium' }, { variant: 'link', size: 'xsmall', class: 'font-medium' }, { variant: 'link', size: 'small', class: 'font-medium' }, @@ -230,6 +297,7 @@ export const Button = React.forwardRef(function Button( loader, extraClickZone, icon: iconClasses, + text: textClasses, } = BUTTON_STYLES({ isDisabled, loading: isLoading, @@ -260,7 +328,7 @@ export const Button = React.forwardRef(function Button( return ( <> {iconComponent} - <>{children} + {children} ) } diff --git a/app/ide-desktop/lib/dashboard/src/components/AriaComponents/Button/ButtonGroup.tsx b/app/ide-desktop/lib/dashboard/src/components/AriaComponents/Button/ButtonGroup.tsx index 4ab98c0651..4fa270b448 100644 --- a/app/ide-desktop/lib/dashboard/src/components/AriaComponents/Button/ButtonGroup.tsx +++ b/app/ide-desktop/lib/dashboard/src/components/AriaComponents/Button/ButtonGroup.tsx @@ -14,12 +14,14 @@ const STYLES = twv.tv({ base: 'flex w-full flex-1 shrink-0', variants: { wrap: { true: 'flex-wrap' }, - direction: { column: 'flex-col justify-center', row: 'flex-row items-center' }, + direction: { column: 'flex-col', row: 'flex-row' }, gap: { custom: '', large: 'gap-3.5', medium: 'gap-2', small: 'gap-1.5', + xsmall: 'gap-1', + xxsmall: 'gap-0.5', none: 'gap-0', }, align: { @@ -31,6 +33,11 @@ const STYLES = twv.tv({ evenly: 'justify-evenly', }, }, + compoundVariants: [ + { direction: 'column', align: 'start', class: 'items-start' }, + { direction: 'column', align: 'center', class: 'items-center' }, + { direction: 'column', align: 'end', class: 'items-end' }, + ], }) /** diff --git a/app/ide-desktop/lib/dashboard/src/components/AriaComponents/Dialog/Dialog.tsx b/app/ide-desktop/lib/dashboard/src/components/AriaComponents/Dialog/Dialog.tsx index 69306ef01f..088506984c 100644 --- a/app/ide-desktop/lib/dashboard/src/components/AriaComponents/Dialog/Dialog.tsx +++ b/app/ide-desktop/lib/dashboard/src/components/AriaComponents/Dialog/Dialog.tsx @@ -2,7 +2,6 @@ * Can be used to display alerts, confirmations, or other content. */ import * as React from 'react' -import clsx from 'clsx' import * as twv from 'tailwind-variants' import * as aria from '#/components/aria' @@ -48,10 +47,14 @@ const DIALOG_STYLES = twv.tv({ modal: 'w-full max-w-md min-h-[100px] max-h-[90vh]', fullscreen: 'w-full h-full max-w-full max-h-full bg-clip-border', }, + hideCloseButton: { true: { closeButton: 'hidden' } }, }, slots: { header: - 'sticky grid grid-cols-[1fr_auto_1fr] items-center border-b border-primary/10 px-3.5 py-2 text-primary', + 'sticky grid grid-cols-[1fr_auto_1fr] items-center border-b border-primary/10 px-3.5 pt-[3px] pb-0.5', + closeButton: 'col-start-1 col-end-1 mr-auto', + heading: 'col-start-2 col-end-2 my-0', + content: 'relative flex-auto overflow-y-auto p-3.5', }, }) @@ -83,7 +86,7 @@ export function Dialog(props: DialogProps) { const root = portal.useStrictPortalContext() const shouldRenderTitle = typeof title === 'string' - const dialogSlots = DIALOG_STYLES({ className, type, rounded }) + const dialogSlots = DIALOG_STYLES({ className, type, rounded, hideCloseButton }) utlities.useInteractOutside({ ref: dialogRef, @@ -151,23 +154,21 @@ export function Dialog(props: DialogProps) { {shouldRenderTitle && ( - {title} - + )} -
+
}> {typeof children === 'function' ? children(opts) : children} diff --git a/app/ide-desktop/lib/dashboard/src/components/AriaComponents/Dialog/Popover.tsx b/app/ide-desktop/lib/dashboard/src/components/AriaComponents/Dialog/Popover.tsx index 9e026784fd..2b335cc298 100644 --- a/app/ide-desktop/lib/dashboard/src/components/AriaComponents/Dialog/Popover.tsx +++ b/app/ide-desktop/lib/dashboard/src/components/AriaComponents/Dialog/Popover.tsx @@ -31,7 +31,7 @@ export interface PopoverProps export const POPOVER_STYLES = twv.tv({ extend: variants.DIALOG_BACKGROUND, - base: 'shadow-2xl w-full', + base: 'shadow-md w-full overflow-clip', variants: { isEntering: { true: 'animate-in fade-in placement-bottom:slide-in-from-top-1 placement-top:slide-in-from-bottom-1 placement-left:slide-in-from-right-1 placement-right:slide-in-from-left-1 ease-out duration-200', @@ -40,11 +40,11 @@ export const POPOVER_STYLES = twv.tv({ true: 'animate-out fade-out placement-bottom:slide-out-to-top-1 placement-top:slide-out-to-bottom-1 placement-left:slide-out-to-right-1 placement-right:slide-out-to-left-1 ease-in duration-150', }, size: { - xsmall: { base: 'max-w-xs', content: 'p-2.5' }, - small: { base: 'max-w-sm', content: 'p-3.5' }, - medium: { base: 'max-w-md', content: 'p-3.5' }, - large: { base: 'max-w-lg', content: 'px-4 py-4' }, - hero: { base: 'max-w-xl', content: 'px-6 py-5' }, + xsmall: { base: 'max-w-xs', dialog: 'p-2.5' }, + small: { base: 'max-w-sm', dialog: 'p-3.5' }, + medium: { base: 'max-w-md', dialog: 'p-3.5' }, + large: { base: 'max-w-lg', dialog: 'px-4 py-4' }, + hero: { base: 'max-w-xl', dialog: 'px-6 py-5' }, }, rounded: { none: '', @@ -57,7 +57,7 @@ export const POPOVER_STYLES = twv.tv({ }, }, slots: { - content: 'flex-auto overflow-y-auto', + content: 'flex-auto overflow-y-auto max-h-[inherit]', }, defaultVariants: { rounded: 'xxlarge', size: 'small' }, }) @@ -106,20 +106,22 @@ export function Popover(props: PopoverProps) { > {opts => ( - + {({ close }) => { closeRef.current = close return ( -
- - - }> - {typeof children === 'function' ? children({ ...opts, close }) : children} - - - -
+ + + }> + {typeof children === 'function' ? children({ ...opts, close }) : children} + + + ) }}
diff --git a/app/ide-desktop/lib/dashboard/src/components/AriaComponents/Dialog/types.ts b/app/ide-desktop/lib/dashboard/src/components/AriaComponents/Dialog/types.ts index f7bcbb63fd..7f367817b9 100644 --- a/app/ide-desktop/lib/dashboard/src/components/AriaComponents/Dialog/types.ts +++ b/app/ide-desktop/lib/dashboard/src/components/AriaComponents/Dialog/types.ts @@ -7,7 +7,6 @@ export interface DialogProps extends aria.DialogProps { * @default 'modal' */ readonly title?: string readonly isDismissable?: boolean - readonly hideCloseButton?: boolean readonly onOpenChange?: (isOpen: boolean) => void readonly isKeyboardDismissDisabled?: boolean readonly modalProps?: Pick diff --git a/app/ide-desktop/lib/dashboard/src/components/AriaComponents/Inputs/ResizableInput/ResizableContentEditableInput.tsx b/app/ide-desktop/lib/dashboard/src/components/AriaComponents/Inputs/ResizableInput/ResizableContentEditableInput.tsx index b256328f4d..f2c226e651 100644 --- a/app/ide-desktop/lib/dashboard/src/components/AriaComponents/Inputs/ResizableInput/ResizableContentEditableInput.tsx +++ b/app/ide-desktop/lib/dashboard/src/components/AriaComponents/Inputs/ResizableInput/ResizableContentEditableInput.tsx @@ -8,6 +8,7 @@ import * as twv from 'tailwind-variants' import * as eventCallbackHooks from '#/hooks/eventCallbackHooks' import * as aria from '#/components/aria' +import * as ariaComponents from '#/components/AriaComponents' import * as mergeRefs from '#/utilities/mergeRefs' @@ -16,7 +17,7 @@ import * as varants from './variants' const CONTENT_EDITABLE_STYLES = twv.tv({ extend: varants.INPUT_STYLES, base: '', - slots: { placeholder: 'text-primary/25 absolute inset-0 pointer-events-none' }, + slots: { placeholder: 'opacity-50 absolute inset-0 pointer-events-none' }, }) /** @@ -106,22 +107,22 @@ export const ResizableContentEditableInput = React.forwardRef( }} /> - + {placeholder} - +
{description != null && ( - + {description} - + )}
{errorMessage != null && ( - + {errorMessage} - + )} ) diff --git a/app/ide-desktop/lib/dashboard/src/components/AriaComponents/Inputs/ResizableInput/variants.ts b/app/ide-desktop/lib/dashboard/src/components/AriaComponents/Inputs/ResizableInput/variants.ts index f4360fd5d3..0291a5a2c3 100644 --- a/app/ide-desktop/lib/dashboard/src/components/AriaComponents/Inputs/ResizableInput/variants.ts +++ b/app/ide-desktop/lib/dashboard/src/components/AriaComponents/Inputs/ResizableInput/variants.ts @@ -6,15 +6,23 @@ import * as twv from 'tailwind-variants' +import * as text from '../../Text' + export const INPUT_STYLES = twv.tv({ - base: 'w-full overflow-hidden block cursor-text rounded-md border-2 border-primary/10 bg-transparent px-1.5 pb-1 pt-2 focus-within:border-primary/50 transition-colors duration-200', + base: 'w-full overflow-hidden block cursor-text rounded-md border-2 border-primary/10 bg-transparent px-1.5 pb-1 pt-1.5 focus-within:border-primary/50 transition-colors duration-200', variants: { isInvalid: { true: 'border-red-500/70 focus-within:border-red-500' } }, slots: { - inputContainer: 'block max-h-32 min-h-5 text-sm font-normal relative overflow-auto', - description: 'mt-1 block text-xs text-primary/40 select-none pointer-events-none', - error: 'block text-xs text-red-500', + inputContainer: text.TEXT_STYLE({ + className: 'block max-h-32 min-h-6 text-sm font-medium relative overflow-auto', + variant: 'body', + }), + description: 'block select-none pointer-events-none opacity-80', + error: 'block', textArea: 'block h-auto w-full max-h-full resize-none bg-transparent', - resizableSpan: - 'pointer-events-none invisible absolute block max-h-32 min-h-10 overflow-y-auto break-all text-sm', + resizableSpan: text.TEXT_STYLE({ + className: + 'pointer-events-none invisible absolute block max-h-32 min-h-10 overflow-y-auto break-all', + variant: 'body', + }), }, }) diff --git a/app/ide-desktop/lib/dashboard/src/components/AriaComponents/Text/Text.tsx b/app/ide-desktop/lib/dashboard/src/components/AriaComponents/Text/Text.tsx index 888f3bddc1..6e6c1ef008 100644 --- a/app/ide-desktop/lib/dashboard/src/components/AriaComponents/Text/Text.tsx +++ b/app/ide-desktop/lib/dashboard/src/components/AriaComponents/Text/Text.tsx @@ -24,7 +24,7 @@ export interface TextProps } export const TEXT_STYLE = twv.tv({ - base: 'inline-block', + base: 'inline-block flex-col before:block after:block before:flex-none after:flex-none before:w-full after:w-full', variants: { color: { custom: '', @@ -39,9 +39,9 @@ export const TEXT_STYLE = twv.tv({ // leading should always be after the text size to make sure it is not stripped by twMerge variant: { custom: '', - body: 'text-xs leading-[20px] pt-[1px] pb-[3px]', - h1: 'text-xl leading-[29px] pt-[2px] pb-[5px]', - subtitle: 'text-[13.5px] leading-[20px] pt-[1px] pb-[3px]', + body: 'text-xs leading-[20px] before:h-[1px] after:h-[3px]', + h1: 'text-xl leading-[29px] before:h-0.5 after:h-[5px]', + subtitle: 'text-[13.5px] leading-[20px] before:h-[1px] after:h-[3px]', }, weight: { custom: '', @@ -102,17 +102,17 @@ export const TEXT_STYLE = twv.tv({ { variant: 'h1', disableLineHeightCompensation: true, - class: 'pt-[unset] pb-[unset]', + class: 'before:h-[unset] after:h-[unset]', }, { variant: 'body', disableLineHeightCompensation: true, - class: 'pt-[unset] pb-[unset]', + class: 'before:h-[unset] after:h-[unset]', }, { variant: 'subtitle', disableLineHeightCompensation: true, - class: 'pt-[unset] pb-[unset]', + class: 'before:h-[unset] after:h-[unset]', }, ], }) diff --git a/app/ide-desktop/lib/dashboard/src/components/AriaComponents/Tooltip/Tooltip.tsx b/app/ide-desktop/lib/dashboard/src/components/AriaComponents/Tooltip/Tooltip.tsx index d6538a40b0..9ff06363bf 100644 --- a/app/ide-desktop/lib/dashboard/src/components/AriaComponents/Tooltip/Tooltip.tsx +++ b/app/ide-desktop/lib/dashboard/src/components/AriaComponents/Tooltip/Tooltip.tsx @@ -4,6 +4,8 @@ import * as twv from 'tailwind-variants' import * as aria from '#/components/aria' import * as portal from '#/components/Portal' +import * as text from '../Text' + // ================= // === Constants === // ================= @@ -18,7 +20,7 @@ export const TOOLTIP_STYLES = twv.tv({ }, size: { custom: '', - medium: 'text-xs leading-[25px] px-2 py-1', + medium: text.TEXT_STYLE({ className: 'px-2 py-1', color: 'custom', balance: true }), }, rounded: { custom: '', diff --git a/app/ide-desktop/lib/dashboard/src/components/ErrorBoundary.tsx b/app/ide-desktop/lib/dashboard/src/components/ErrorBoundary.tsx index b3bfc046fe..c157d766ef 100644 --- a/app/ide-desktop/lib/dashboard/src/components/ErrorBoundary.tsx +++ b/app/ide-desktop/lib/dashboard/src/components/ErrorBoundary.tsx @@ -9,11 +9,15 @@ import * as sentry from '@sentry/react' import * as reactQuery from '@tanstack/react-query' import * as errorBoundary from 'react-error-boundary' +import * as detect from 'enso-common/src/detect' + import * as textProvider from '#/providers/TextProvider' import * as ariaComponents from '#/components/AriaComponents' import * as result from '#/components/Result' +import * as errorUtils from '#/utilities/error' + /** * Props for the ErrorBoundary component */ @@ -58,14 +62,23 @@ export function ErrorBoundary(props: ErrorBoundaryProps) { ) } +/** + * Props for the DefaultFallbackComponent + */ +export interface FallBackProps extends errorBoundary.FallbackProps { + readonly error: unknown +} + /** * Default fallback component to show when there is an error */ -function DefaultFallbackComponent(props: errorBoundary.FallbackProps): React.JSX.Element { - const { resetErrorBoundary } = props +function DefaultFallbackComponent(props: FallBackProps): React.JSX.Element { + const { resetErrorBoundary, error } = props const { getText } = textProvider.useText() + const stack = errorUtils.tryGetStack(error) + return ( + {detect.IS_DEV_MODE && stack != null && ( + + + {stack} + + + )} + - + {getText('tryAgain')} diff --git a/app/ide-desktop/lib/dashboard/src/components/Page.tsx b/app/ide-desktop/lib/dashboard/src/components/Page.tsx index e393475720..808a7dea60 100644 --- a/app/ide-desktop/lib/dashboard/src/components/Page.tsx +++ b/app/ide-desktop/lib/dashboard/src/components/Page.tsx @@ -18,11 +18,12 @@ import Portal from '#/components/Portal' /** Props for a {@link Page}. */ export interface PageProps extends Readonly { readonly hideInfoBar?: true + readonly hideChat?: true } /** A page. */ export default function Page(props: PageProps) { - const { hideInfoBar = false, children } = props + const { hideInfoBar = false, children, hideChat = false } = props const [isHelpChatOpen, setIsHelpChatOpen] = React.useState(false) const { unsetModal } = modalProvider.useSetModal() const session = authProvider.useUserSession() @@ -52,7 +53,10 @@ export default function Page(props: PageProps) { )} {/* `session.accessToken` MUST be present in order for the `Chat` component to work. */} - {!hideInfoBar && session?.accessToken != null && process.env.ENSO_CLOUD_CHAT_URL != null ? ( + {!hideInfoBar && + !hideChat && + session?.accessToken != null && + process.env.ENSO_CLOUD_CHAT_URL != null ? ( + {title} ) : ( title )} - + {subtitle} diff --git a/app/ide-desktop/lib/dashboard/src/layouts/Chat.tsx b/app/ide-desktop/lib/dashboard/src/layouts/Chat.tsx index e24f79f483..1573dc0d0d 100644 --- a/app/ide-desktop/lib/dashboard/src/layouts/Chat.tsx +++ b/app/ide-desktop/lib/dashboard/src/layouts/Chat.tsx @@ -683,7 +683,7 @@ export default function Chat(props: ChatProps) { return reactDom.createPortal(
- + {getText('invite')} @@ -96,7 +96,7 @@ export default function UserBar(props: UserBarProps) { {getText('upgrade')} diff --git a/app/ide-desktop/lib/dashboard/src/pages/dashboard/Dashboard.tsx b/app/ide-desktop/lib/dashboard/src/pages/dashboard/Dashboard.tsx index 8535dde59a..8fd54f9cd4 100644 --- a/app/ide-desktop/lib/dashboard/src/pages/dashboard/Dashboard.tsx +++ b/app/ide-desktop/lib/dashboard/src/pages/dashboard/Dashboard.tsx @@ -474,7 +474,7 @@ export default function Dashboard(props: DashboardProps) { }, [page, setPage]) return ( - +
(error: MustNotBeKnown): string | null { : null } +/** + * Extracts the `stack` property of a value if it is a string. Intended to be used on {@link Error}s. + */ +export function tryGetStack( + error: MustNotBeKnown, + // eslint-disable-next-line no-restricted-syntax + defaultMessage: DefaultMessage = null as DefaultMessage +): DefaultMessage | string { + const unknownError: unknown = error + return unknownError != null && + typeof unknownError === 'object' && + 'stack' in unknownError && + typeof unknownError.stack === 'string' + ? unknownError.stack + : defaultMessage +} + /** Like {@link tryGetMessage} but return the string representation of the value if it is not an * {@link Error}. */ export function getMessageOrToString(error: MustNotBeKnown) {