mirror of
https://github.com/enso-org/enso.git
synced 2024-11-23 08:08:34 +03:00
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:
parent
b0589d267d
commit
7a20bdc82f
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -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' },
|
||||
],
|
||||
})
|
||||
|
||||
/**
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
|
@ -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'>
|
||||
|
@ -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>
|
||||
)
|
||||
|
@ -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',
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
@ -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]',
|
||||
},
|
||||
],
|
||||
})
|
||||
|
@ -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: '',
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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
|
||||
|
@ -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')}
|
||||
|
@ -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' : ''
|
||||
|
@ -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>) {
|
||||
|
Loading…
Reference in New Issue
Block a user