Mark cloud as beta (#10992)

This commit is contained in:
Sergei Garin 2024-09-06 16:17:57 +03:00 committed by GitHub
parent 6617dd9d86
commit d242ed18bb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 292 additions and 193 deletions

View File

@ -48,8 +48,12 @@ export const TEXT_STYLE = twv.tv({
variant: { variant: {
custom: '', custom: '',
body: 'text-xs leading-[20px] before:h-[1px] after:h-[3px] font-medium', body: 'text-xs leading-[20px] before:h-[1px] after:h-[3px] font-medium',
// eslint-disable-next-line @typescript-eslint/naming-convention
'body-sm': 'text-[10.5px] leading-[16px] before:h-[0.5px] after:h-[2.5px] font-medium',
h1: 'text-xl leading-[29px] before:h-0.5 after:h-[5px] font-bold', h1: 'text-xl leading-[29px] before:h-0.5 after:h-[5px] font-bold',
subtitle: 'text-[13.5px] leading-[19px] before:h-[1px] after:h-[3px] font-bold', subtitle: 'text-[13.5px] leading-[19px] before:h-[1px] after:h-[3px] font-bold',
caption: 'text-[8.5px] leading-[12px] before:h-[0.5px] after:h-[1.5px]',
overline: 'text-[8.5px] leading-[16px] before:h-[0.5px] after:h-[1.5px] uppercase',
}, },
weight: { weight: {
custom: '', custom: '',

View File

@ -0,0 +1,77 @@
/**
* @file
*
* Badges are used to highlight an item's status for quick recognition.
*/
import type { VariantProps } from '#/utilities/tailwindVariants'
import { tv } from '#/utilities/tailwindVariants'
import type { ReactNode } from 'react'
import { TEXT_STYLE } from '../AriaComponents'
/**
* Props for the {@link Badge} component.
*/
export interface BadgeProps extends VariantProps<typeof BADGE_STYLES> {
readonly children?: ReactNode
readonly className?: string
}
export const BADGE_STYLES = tv({
base: 'flex items-center justify-center px-[5px] border-[0.5px]',
variants: {
variant: {
solid: 'border-transparent bg-[var(--badge-bg-color)] text-[var(--badge-text-color)]',
outline: 'border-[var(--badge-border-color)] bg-transparent text-[var(--badge-text-color)]',
},
color: {
primary:
'[--badge-border-color:var(--color-primary)] [--badge-bg-color:var(--color-primary)] [--badge-text-color:var(--color-invert)]',
accent:
'[--badge-border-color:var(--color-accent)] [--badge-bg-color:var(--color-accent)] [--badge-text-color:var(--color-invert)]',
error:
'[--badge-border-color:var(--color-danger)] [--badge-bg-color:var(--color-danger)] [--badge-text-color:var(--color-invert)]',
},
rounded: {
true: 'rounded-full',
false: 'rounded-none',
none: 'rounded-none',
small: 'rounded-sm',
medium: 'rounded-md',
large: 'rounded-lg',
xlarge: 'rounded-xl',
xxlarge: 'rounded-2xl',
xxxlarge: 'rounded-3xl',
xxxxlarge: 'rounded-4xl',
full: 'rounded-full',
},
},
slots: {
icon: 'flex items-center justify-center',
content: TEXT_STYLE({
variant: 'body-sm',
color: 'current',
className: '',
}),
},
defaultVariants: {
variant: 'solid',
color: 'primary',
rounded: 'xxxxlarge',
iconPosition: 'start',
},
})
/**
* Badges are used to highlight an item's status for quick recognition.
*/
export function Badge(props: BadgeProps) {
const { children, color, rounded, className, variant } = props
const classes = BADGE_STYLES({ color, rounded, variant })
return (
<div className={classes.base({ class: className })}>
<div className={classes.content()}>{children}</div>
</div>
)
}

View File

@ -0,0 +1,6 @@
/**
* @file
*
* Barrel export file for Badge
*/
export * from './Badge'

View File

@ -16,7 +16,7 @@ import SettingsIcon from '#/assets/settings.svg'
import Trash2Icon from '#/assets/trash2.svg' import Trash2Icon from '#/assets/trash2.svg'
import * as aria from '#/components/aria' import * as aria from '#/components/aria'
import * as ariaComponents from '#/components/AriaComponents' import * as ariaComponents from '#/components/AriaComponents'
import FocusArea from '#/components/styled/FocusArea' import { Badge } from '#/components/Badge'
import SvgMask from '#/components/SvgMask' import SvgMask from '#/components/SvgMask'
import * as mimeTypes from '#/data/mimeTypes' import * as mimeTypes from '#/data/mimeTypes'
import AssetEventType from '#/events/AssetEventType' import AssetEventType from '#/events/AssetEventType'
@ -77,11 +77,12 @@ interface CategoryMetadata {
interface InternalCategorySwitcherItemProps extends CategoryMetadata { interface InternalCategorySwitcherItemProps extends CategoryMetadata {
readonly currentCategory: Category readonly currentCategory: Category
readonly setCategory: (category: Category) => void readonly setCategory: (category: Category) => void
readonly badgeContent?: React.ReactNode
} }
/** An entry in a {@link CategorySwitcher}. */ /** An entry in a {@link CategorySwitcher}. */
function CategorySwitcherItem(props: InternalCategorySwitcherItemProps) { function CategorySwitcherItem(props: InternalCategorySwitcherItemProps) {
const { currentCategory, setCategory } = props const { currentCategory, setCategory, badgeContent } = props
const { isNested = false, category, icon, label, buttonLabel, dropZoneLabel } = props const { isNested = false, category, icon, label, buttonLabel, dropZoneLabel } = props
const { iconClassName } = props const { iconClassName } = props
const { user } = authProvider.useFullUserSession() const { user } = authProvider.useFullUserSession()
@ -258,6 +259,11 @@ function CategorySwitcherItem(props: InternalCategorySwitcherItemProps) {
<ariaComponents.Text slot="description" truncate="1" className="flex-auto"> <ariaComponents.Text slot="description" truncate="1" className="flex-auto">
{label} {label}
</ariaComponents.Text> </ariaComponents.Text>
{badgeContent != null && (
<Badge color="accent" variant="solid">
{badgeContent}
</Badge>
)}
</div> </div>
</ariaComponents.Button> </ariaComponents.Button>
<div className="absolute left-full ml-2 hidden group-focus-visible:block"> <div className="absolute left-full ml-2 hidden group-focus-visible:block">
@ -354,199 +360,196 @@ export default function CategorySwitcher(props: CategorySwitcherProps) {
) )
return ( return (
<FocusArea direction="vertical"> <div className="flex flex-col gap-2 py-1">
{(innerProps) => ( <ariaComponents.Text variant="subtitle" className="px-2 font-bold">
<div className="flex flex-col gap-2 py-1" {...innerProps}> {getText('category')}
<ariaComponents.Text variant="subtitle" className="px-2 font-bold"> </ariaComponents.Text>
{getText('category')}
</ariaComponents.Text>
<div <div
aria-label={getText('categorySwitcherMenuLabel')} aria-label={getText('categorySwitcherMenuLabel')}
role="grid" role="grid"
className="flex flex-col items-start" className="flex flex-col items-start"
> >
<CategorySwitcherItem
{...itemProps}
category={{ type: 'cloud' }}
icon={CloudIcon}
label={getText('cloudCategory')}
buttonLabel={getText('cloudCategoryButtonLabel')}
dropZoneLabel={getText('cloudCategoryDropZoneLabel')}
badgeContent={getText('cloudCategoryBadgeContent')}
/>
{(user.plan === backend.Plan.team || user.plan === backend.Plan.enterprise) && (
<CategorySwitcherItem
{...itemProps}
isNested
category={{
type: 'user',
rootPath: backend.Path(`enso://Users/${user.name}`),
homeDirectoryId: selfDirectoryId,
}}
icon={PersonIcon}
label={getText('myFilesCategory')}
buttonLabel={getText('myFilesCategoryButtonLabel')}
dropZoneLabel={getText('myFilesCategoryDropZoneLabel')}
/>
)}
<CategorySwitcherItem
{...itemProps}
isNested
category={{ type: 'recent' }}
icon={RecentIcon}
label={getText('recentCategory')}
buttonLabel={getText('recentCategoryButtonLabel')}
dropZoneLabel={getText('recentCategoryDropZoneLabel')}
iconClassName="-ml-0.5"
/>
<CategorySwitcherItem
{...itemProps}
isNested
category={{ type: 'trash' }}
icon={Trash2Icon}
label={getText('trashCategory')}
buttonLabel={getText('trashCategoryButtonLabel')}
dropZoneLabel={getText('trashCategoryDropZoneLabel')}
/>
{usersDirectoryQuery.data?.map((userDirectory) => {
if (userDirectory.type !== backend.AssetType.directory) {
return null
} else {
const otherUser = usersById.get(userDirectory.id)
return !otherUser || otherUser.userId === user.userId ?
null
: <CategorySwitcherItem
key={otherUser.userId}
{...itemProps}
isNested
category={{
type: 'user',
rootPath: backend.Path(`enso://Users/${otherUser.name}`),
homeDirectoryId: userDirectory.id,
}}
icon={PersonIcon}
label={getText('userCategory', otherUser.name)}
buttonLabel={getText('userCategoryButtonLabel', otherUser.name)}
dropZoneLabel={getText('userCategoryDropZoneLabel', otherUser.name)}
/>
}
})}
{teamsDirectoryQuery.data?.map((teamDirectory) => {
if (teamDirectory.type !== backend.AssetType.directory) {
return null
} else {
const team = teamsById.get(teamDirectory.id)
return !team ? null : (
<CategorySwitcherItem
key={team.id}
{...itemProps}
isNested
category={{
type: 'team',
team,
rootPath: backend.Path(`enso://Teams/${team.groupName}`),
homeDirectoryId: teamDirectory.id,
}}
icon={PeopleIcon}
label={getText('teamCategory', team.groupName)}
buttonLabel={getText('teamCategoryButtonLabel', team.groupName)}
dropZoneLabel={getText('teamCategoryDropZoneLabel', team.groupName)}
/>
)
}
})}
{localBackend && (
<div className="group flex items-center justify-between self-stretch">
<CategorySwitcherItem <CategorySwitcherItem
{...itemProps} {...itemProps}
category={{ type: 'cloud' }} category={{ type: 'local' }}
icon={CloudIcon} icon={ComputerIcon}
label={getText('cloudCategory')} label={getText('localCategory')}
buttonLabel={getText('cloudCategoryButtonLabel')} buttonLabel={getText('localCategoryButtonLabel')}
dropZoneLabel={getText('cloudCategoryDropZoneLabel')} dropZoneLabel={getText('localCategoryDropZoneLabel')}
/> />
{(user.plan === backend.Plan.team || user.plan === backend.Plan.enterprise) && ( <ariaComponents.Button
size="medium"
variant="icon"
icon={SettingsIcon}
aria-label={getText('changeLocalRootDirectoryInSettings')}
className="opacity-0 transition-opacity group-hover:opacity-100"
onPress={() => {
// eslint-disable-next-line @typescript-eslint/naming-convention
setSearchParams({ 'cloud-ide_SettingsTab': '"local"' })
setPage(TabType.settings)
}}
/>
</div>
)}
{localBackend &&
localRootDirectories?.map((directory) => (
<div key={directory} className="group flex items-center self-stretch">
<CategorySwitcherItem <CategorySwitcherItem
{...itemProps} {...itemProps}
isNested isNested
category={{ category={{
type: 'user', type: 'local-directory',
rootPath: backend.Path(`enso://Users/${user.name}`), rootPath: backend.Path(directory),
homeDirectoryId: selfDirectoryId, homeDirectoryId: newDirectoryId(backend.Path(directory)),
}} }}
icon={PersonIcon} icon={FolderIcon}
label={getText('myFilesCategory')} label={getFileName(directory)}
buttonLabel={getText('myFilesCategoryButtonLabel')} buttonLabel={getText('localCategoryButtonLabel')}
dropZoneLabel={getText('myFilesCategoryDropZoneLabel')} dropZoneLabel={getText('localCategoryDropZoneLabel')}
/> />
)} <div className="grow" />
<CategorySwitcherItem <ariaComponents.Button
{...itemProps} size="medium"
isNested variant="icon"
category={{ type: 'recent' }} icon={Minus2Icon}
icon={RecentIcon} aria-label={getText('removeDirectoryFromFavorites')}
label={getText('recentCategory')} className="hidden group-hover:block"
buttonLabel={getText('recentCategoryButtonLabel')} onPress={() => {
dropZoneLabel={getText('recentCategoryDropZoneLabel')} setModal(
iconClassName="-ml-0.5" <ConfirmDeleteModal
/> actionText={getText(
<CategorySwitcherItem 'removeTheLocalDirectoryXFromFavorites',
{...itemProps} getFileName(directory),
isNested )}
category={{ type: 'trash' }} actionButtonLabel={getText('remove')}
icon={Trash2Icon} doDelete={() => {
label={getText('trashCategory')} setLocalRootDirectories(
buttonLabel={getText('trashCategoryButtonLabel')} localRootDirectories.filter(
dropZoneLabel={getText('trashCategoryDropZoneLabel')} (otherDirectory) => otherDirectory !== directory,
/> ),
{usersDirectoryQuery.data?.map((userDirectory) => { )
if (userDirectory.type !== backend.AssetType.directory) {
return null
} else {
const otherUser = usersById.get(userDirectory.id)
return !otherUser || otherUser.userId === user.userId ?
null
: <CategorySwitcherItem
key={otherUser.userId}
{...itemProps}
isNested
category={{
type: 'user',
rootPath: backend.Path(`enso://Users/${otherUser.name}`),
homeDirectoryId: userDirectory.id,
}} }}
icon={PersonIcon} />,
label={getText('userCategory', otherUser.name)}
buttonLabel={getText('userCategoryButtonLabel', otherUser.name)}
dropZoneLabel={getText('userCategoryDropZoneLabel', otherUser.name)}
/>
}
})}
{teamsDirectoryQuery.data?.map((teamDirectory) => {
if (teamDirectory.type !== backend.AssetType.directory) {
return null
} else {
const team = teamsById.get(teamDirectory.id)
return !team ? null : (
<CategorySwitcherItem
key={team.id}
{...itemProps}
isNested
category={{
type: 'team',
team,
rootPath: backend.Path(`enso://Teams/${team.groupName}`),
homeDirectoryId: teamDirectory.id,
}}
icon={PeopleIcon}
label={getText('teamCategory', team.groupName)}
buttonLabel={getText('teamCategoryButtonLabel', team.groupName)}
dropZoneLabel={getText('teamCategoryDropZoneLabel', team.groupName)}
/>
) )
} }}
})} />
{localBackend && ( </div>
<div className="group flex items-center justify-between self-stretch"> ))}
<CategorySwitcherItem {localBackend && window.fileBrowserApi && (
{...itemProps} <div className="flex">
category={{ type: 'local' }} <div className="ml-[15px] mr-1 border-r border-primary/20" />
icon={ComputerIcon} <ariaComponents.Button
label={getText('localCategory')} size="xsmall"
buttonLabel={getText('localCategoryButtonLabel')} variant="outline"
dropZoneLabel={getText('localCategoryDropZoneLabel')} icon={PlusIcon}
/> loaderPosition="icon"
<ariaComponents.Button className="ml-0.5 rounded-full px-2 selectable"
size="medium" onPress={async () => {
variant="icon" const [newDirectory] =
icon={SettingsIcon} (await window.fileBrowserApi?.openFileBrowser('directory')) ?? []
aria-label={getText('changeLocalRootDirectoryInSettings')} if (newDirectory != null) {
className="opacity-0 transition-opacity group-hover:opacity-100" setLocalRootDirectories([...(localRootDirectories ?? []), newDirectory])
onPress={() => { }
// eslint-disable-next-line @typescript-eslint/naming-convention }}
setSearchParams({ 'cloud-ide_SettingsTab': '"local"' }) >
setPage(TabType.settings) <div className="ml-1.5">{getText('addLocalDirectory')}</div>
}} </ariaComponents.Button>
/>
</div>
)}
{localBackend &&
localRootDirectories?.map((directory) => (
<div key={directory} className="group flex items-center self-stretch">
<CategorySwitcherItem
{...itemProps}
isNested
category={{
type: 'local-directory',
rootPath: backend.Path(directory),
homeDirectoryId: newDirectoryId(backend.Path(directory)),
}}
icon={FolderIcon}
label={getFileName(directory)}
buttonLabel={getText('localCategoryButtonLabel')}
dropZoneLabel={getText('localCategoryDropZoneLabel')}
/>
<div className="grow" />
<ariaComponents.Button
size="medium"
variant="icon"
icon={Minus2Icon}
aria-label={getText('removeDirectoryFromFavorites')}
className="hidden group-hover:block"
onPress={() => {
setModal(
<ConfirmDeleteModal
actionText={getText(
'removeTheLocalDirectoryXFromFavorites',
getFileName(directory),
)}
actionButtonLabel={getText('remove')}
doDelete={() => {
setLocalRootDirectories(
localRootDirectories.filter(
(otherDirectory) => otherDirectory !== directory,
),
)
}}
/>,
)
}}
/>
</div>
))}
{localBackend && window.fileBrowserApi && (
<div className="flex">
<div className="ml-[15px] mr-1 border-r border-primary/20" />
<ariaComponents.Button
size="xsmall"
variant="outline"
icon={PlusIcon}
loaderPosition="icon"
className="ml-0.5 rounded-full px-2 selectable"
onPress={async () => {
const [newDirectory] =
(await window.fileBrowserApi?.openFileBrowser('directory')) ?? []
if (newDirectory != null) {
setLocalRootDirectories([...(localRootDirectories ?? []), newDirectory])
}
}}
>
<div className="ml-1.5">{getText('addLocalDirectory')}</div>
</ariaComponents.Button>
</div>
)}
</div> </div>
</div> )}
)} </div>
</FocusArea> </div>
) )
} }

View File

@ -121,12 +121,7 @@ function DashboardInner(props: DashboardProps) {
const [category, setCategory] = searchParamsState.useSearchParamsState<categoryModule.Category>( const [category, setCategory] = searchParamsState.useSearchParamsState<categoryModule.Category>(
'driveCategory', 'driveCategory',
() => { () => (localBackend != null ? { type: 'local' } : { type: 'cloud' }),
const shouldDefaultToCloud =
initialLocalProjectId == null && (user.isEnabled || localBackend == null)
const type = shouldDefaultToCloud ? 'cloud' : 'local'
return { type }
},
(value): value is categoryModule.Category => (value): value is categoryModule.Category =>
categoryModule.CATEGORY_SCHEMA.safeParse(value).success, categoryModule.CATEGORY_SCHEMA.safeParse(value).success,
) )

View File

@ -7,6 +7,19 @@
@tailwind utilities; @tailwind utilities;
:root { :root {
--color-primary-rgb: 0 0 0;
--color-primary-opacity: 60%;
--color-primary: rgb(var(--color-primary-rgb) / var(--color-primary-opacity));
--color-accent-rgb: 73 159 75;
--color-accent-opacity: 100%;
--color-accent: rgb(var(--color-accent-rgb) / var(--color-accent-opacity));
--color-danger-rgb: 211 59 11;
--color-danger-opacity: 100%;
--color-danger: rgb(var(--color-danger-rgb) / var(--color-danger-opacity));
--color-invert-rgb: 255 255 255;
--color-invert-opacity: 100%;
--color-invert: rgb(var(--color-invert-rgb) / var(--color-invert-opacity));
--top-bar-height: 3rem; --top-bar-height: 3rem;
--row-height: 2rem; --row-height: 2rem;
--table-row-height: 2.3125rem; --table-row-height: 2.3125rem;

View File

@ -17,9 +17,10 @@ export default /** @satisfies {import('tailwindcss').Config} */ ({
// modified. // modified.
/** The default color of all text. */ /** The default color of all text. */
// This should be named "regular". // This should be named "regular".
primary: 'rgb(0 0 0 / 60%)', primary: 'rgb(var(--color-primary-rgb) / var(--color-primary-opacity))',
invert: 'rgb(255 255 255 / 80%)', invert: 'rgb(var(--color-invert-rgb) / var(--color-invert-opacity))',
accent: '#499f4b', accent: 'rgb(var(--color-accent-rgb) / 100%)',
danger: 'rgb(var(--color-danger-rgb) / 100%)',
'accent-dark': '#3e9152', 'accent-dark': '#3e9152',
'hover-bg': 'rgb(0 0 0 / 10%)', 'hover-bg': 'rgb(0 0 0 / 10%)',
frame: 'rgb(255 255 255 / 40%)', frame: 'rgb(255 255 255 / 40%)',
@ -38,7 +39,6 @@ export default /** @satisfies {import('tailwindcss').Config} */ ({
v3: '#252423', v3: '#252423',
youtube: '#c62421', youtube: '#c62421',
discord: '#404796', discord: '#404796',
danger: '#d33b0b',
'selection-brush': 'lch(70% 0 0 / 50%)', 'selection-brush': 'lch(70% 0 0 / 50%)',
dim: 'rgb(0 0 0 / 25%)', dim: 'rgb(0 0 0 / 25%)',
'dim-darker': 'rgb(0 0 0 / 40%)', 'dim-darker': 'rgb(0 0 0 / 40%)',

View File

@ -296,6 +296,7 @@
"teamCategoryButtonLabel": "$0 (Team)", "teamCategoryButtonLabel": "$0 (Team)",
"localCategoryButtonLabel": "Local", "localCategoryButtonLabel": "Local",
"cloudCategoryDropZoneLabel": "Move to your organization's home directory", "cloudCategoryDropZoneLabel": "Move to your organization's home directory",
"cloudCategoryBadgeContent": "Beta",
"myFilesCategoryDropZoneLabel": "Move to your home directory", "myFilesCategoryDropZoneLabel": "Move to your home directory",
"recentCategoryDropZoneLabel": "Move to Recent category", "recentCategoryDropZoneLabel": "Move to Recent category",
"trashCategoryDropZoneLabel": "Move to Trash category", "trashCategoryDropZoneLabel": "Move to Trash category",