Refactors of Dialogs, Popover styles, button sizes and Text (#10199)

In this PR:

1. Now button has proper sized icons, size of the button is also alighned with the grid
2. Migrate ResizableTextContentEditableInput to Text component
3. Migrate Dialog to variants API
4. Did another attemt to make text more reusable. Now instead of using paddings, we use ::before and ::after to make text better aligned inside a grid
This commit is contained in:
Sergei Garin 2024-06-07 11:10:11 +03:00 committed by GitHub
parent b0589d267d
commit 7a20bdc82f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 233 additions and 89 deletions

View File

@ -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<aria.ButtonProps, 'onPress' | 'type'> & PropsWithoutHref)
| (BaseButtonProps & Omit<aria.LinkProps, 'onPress' | 'type'> & PropsWithHref)
| (BaseButtonProps & Omit<aria.ButtonProps, 'children' | 'onPress' | 'type'> & PropsWithoutHref)
| (BaseButtonProps & Omit<aria.LinkProps, 'children' | 'onPress' | 'type'> & PropsWithHref)
/**
* Props for a button with an href.
@ -54,7 +56,7 @@ export interface BaseButtonProps extends Omit<twv.VariantProps<typeof BUTTON_STY
* If the handler returns a promise, the button will be in a loading state until the promise resolves.
*/
readonly onPress?: (event: aria.PressEvent) => Promise<void> | 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}</>
<span className={textClasses()}>{children}</span>
</>
)
}

View File

@ -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' },
],
})
/**

View File

@ -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 && (
<aria.Header className={dialogSlots.header()}>
<ariaComponents.CloseButton
className={clsx('col-start-1 col-end-1 mr-auto mt-0.5', {
hidden: hideCloseButton,
})}
className={dialogSlots.closeButton()}
onPress={opts.close}
/>
<aria.Heading
<ariaComponents.Text.Heading
slot="title"
level={2}
className="col-start-2 col-end-2 my-0 text-base font-semibold leading-6"
className={dialogSlots.heading()}
>
{title}
</aria.Heading>
</ariaComponents.Text.Heading>
</aria.Header>
)}
<div className="relative flex-auto overflow-y-auto p-3.5">
<div className={dialogSlots.content()}>
<errorBoundary.ErrorBoundary>
<React.Suspense fallback={<loader.Loader minHeight="h32" />}>
{typeof children === 'function' ? children(opts) : children}

View File

@ -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 => (
<dialogStackProvider.DialogStackRegistrar id={dialogId} type="popover">
<aria.Dialog id={dialogId} ref={dialogRef}>
<aria.Dialog
id={dialogId}
ref={dialogRef}
className={POPOVER_STYLES({ ...opts, size, rounded }).content()}
>
{({ close }) => {
closeRef.current = close
return (
<div className={POPOVER_STYLES({ ...opts, size, rounded }).content()}>
<dialogProvider.DialogProvider value={{ close, dialogId }}>
<errorBoundary.ErrorBoundary>
<React.Suspense fallback={<loader.Loader minHeight="h16" />}>
{typeof children === 'function' ? children({ ...opts, close }) : children}
</React.Suspense>
</errorBoundary.ErrorBoundary>
</dialogProvider.DialogProvider>
</div>
<dialogProvider.DialogProvider value={{ close, dialogId }}>
<errorBoundary.ErrorBoundary>
<React.Suspense fallback={<loader.Loader minHeight="h16" />}>
{typeof children === 'function' ? children({ ...opts, close }) : children}
</React.Suspense>
</errorBoundary.ErrorBoundary>
</dialogProvider.DialogProvider>
)
}}
</aria.Dialog>

View File

@ -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<aria.ModalOverlayProps, 'className' | 'defaultOpen' | 'isOpen'>

View File

@ -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(
}}
/>
<span className={placeholderClass({ class: value ? 'hidden' : '' })}>
<ariaComponents.Text className={placeholderClass({ class: value ? 'hidden' : '' })}>
{placeholder}
</span>
</ariaComponents.Text>
</div>
{description != null && (
<aria.Text slot="description" className={descriptionClass()}>
<ariaComponents.Text slot="description" className={descriptionClass()}>
{description}
</aria.Text>
</ariaComponents.Text>
)}
</div>
{errorMessage != null && (
<aria.Text slot="errorMessage" className={error()}>
<ariaComponents.Text slot="errorMessage" color="danger" className={error()}>
{errorMessage}
</aria.Text>
</ariaComponents.Text>
)}
</aria.TextField>
)

View File

@ -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',
}),
},
})

View File

@ -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]',
},
],
})

View File

@ -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: '',

View File

@ -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 (
<result.Result
className="h-full"
@ -73,8 +86,27 @@ function DefaultFallbackComponent(props: errorBoundary.FallbackProps): React.JSX
title={getText('arbitraryErrorTitle')}
subtitle={getText('arbitraryErrorSubtitle')}
>
{detect.IS_DEV_MODE && stack != null && (
<ariaComponents.Alert className="mx-auto mb-4 max-w-screen-lg" variant="neutral">
<ariaComponents.Text
elementType="pre"
className="whitespace-pre-wrap text-left"
color="primary"
variant="subtitle"
>
{stack}
</ariaComponents.Text>
</ariaComponents.Alert>
)}
<ariaComponents.ButtonGroup align="center">
<ariaComponents.Button variant="submit" size="medium" onPress={resetErrorBoundary}>
<ariaComponents.Button
variant="submit"
size="large"
rounded="full"
className="w-24"
onPress={resetErrorBoundary}
>
{getText('tryAgain')}
</ariaComponents.Button>
</ariaComponents.ButtonGroup>

View File

@ -18,11 +18,12 @@ import Portal from '#/components/Portal'
/** Props for a {@link Page}. */
export interface PageProps extends Readonly<React.PropsWithChildren> {
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) {
</div>
)}
{/* `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 ? (
<Chat
isOpen={isHelpChatOpen}
doClose={doCloseChat}

View File

@ -82,14 +82,17 @@ export function Result(props: ResultProps) {
) : null}
{typeof title === 'string' ? (
<aria.Heading level={2} className="mb-2 text-2xl leading-10">
<aria.Heading level={2} className="mb-2 text-2xl leading-10 text-primary/60">
{title}
</aria.Heading>
) : (
title
)}
<aria.Text elementType="p" className="max-w-[750px] text-balance text-lg leading-6">
<aria.Text
elementType="p"
className="max-w-[750px] text-balance text-lg leading-6 text-primary/60"
>
{subtitle}
</aria.Text>

View File

@ -683,7 +683,7 @@ export default function Chat(props: ChatProps) {
return reactDom.createPortal(
<div
className={`fixed right top z-1 flex h-screen w-chat flex-col py-chat-y text-xs text-primary shadow-soft backdrop-blur-default transition-transform ${isOpen ? '' : 'translate-x-full'}`}
className={`fixed right top z-1 flex h-screen w-chat flex-col py-chat-y text-xs text-primary shadow-soft backdrop-blur-default transition-[transform,opacity] ${isOpen ? 'opacity-1' : 'translate-x-full opacity-0'}`}
{...focusWithinProps}
>
<ChatHeader

View File

@ -85,7 +85,7 @@ export default function UserBar(props: UserBarProps) {
{shouldShowInviteButton && (
<ariaComponents.DialogTrigger>
<ariaComponents.Button rounded="full" size="xsmall" variant="tertiary">
<ariaComponents.Button rounded="full" size="small" variant="tertiary">
{getText('invite')}
</ariaComponents.Button>
@ -96,7 +96,7 @@ export default function UserBar(props: UserBarProps) {
<ariaComponents.Button
variant="primary"
rounded="full"
size="xsmall"
size="small"
href={appUtils.SUBSCRIBE_PATH}
>
{getText('upgrade')}

View File

@ -474,7 +474,7 @@ export default function Dashboard(props: DashboardProps) {
}, [page, setPage])
return (
<Page hideInfoBar>
<Page hideInfoBar hideChat>
<div
className={`flex text-xs text-primary ${
page === pageSwitcher.Page.editor ? 'pointer-events-none cursor-none' : ''

View File

@ -46,6 +46,23 @@ export function tryGetError<T>(error: MustNotBeKnown<T>): 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<T, DefaultMessage extends string | null = null>(
error: MustNotBeKnown<T>,
// 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<T>(error: MustNotBeKnown<T>) {