Home page touch-ups (#2135)

* Save part images when navigating home

* Load part images in project cards if available

* Polish home page

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* Merge branch 'main' into franknoirot/project-images

* Mostly restored link + form functionality

* Working cards with images

* Comment out project image stuff

* Little style tweaks

* Remove unused imports

* More minor styling tweaks

* Merge branch 'main' into franknoirot/project-images

* Was using the wrong imported `Project` type

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* Revert any docs changes

* Revert wasm-lib divergences

* Move ProjectCard into its component folder

* Remove unused hook useSaveVideoFrame

* Remove "hideOnLevel" config from theme setting

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Frank Noirot
2024-05-23 11:47:02 -04:00
committed by GitHub
parent 5b7d707b26
commit 023ed1a687
11 changed files with 399 additions and 314 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 40 KiB

View File

@ -395,6 +395,14 @@ const CustomIconMap = {
/>
</svg>
),
trash: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M8.5 6H5V8H6M8.5 6V4H11.5V6M8.5 6H11.5M11.5 6H15V8H14M6 8V15.5H8M6 8H14M14 8V15.5H12M8 15.5V10M8 15.5H10M12 15.5V10M12 15.5H10M10 15.5V12"
stroke="currentColor"
/>
</svg>
),
vertical: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path

View File

@ -1,235 +0,0 @@
import { FormEvent, useEffect, useRef, useState } from 'react'
import { paths } from 'lib/paths'
import { Link } from 'react-router-dom'
import { ActionButton } from './ActionButton'
import {
faCheck,
faPenAlt,
faTrashAlt,
faX,
} from '@fortawesome/free-solid-svg-icons'
import { FILE_EXT } from 'lib/constants'
import { Dialog } from '@headlessui/react'
import { useHotkeys } from 'react-hotkeys-hook'
import Tooltip from './Tooltip'
import { Project } from 'wasm-lib/kcl/bindings/Project'
function ProjectCard({
project,
handleRenameProject,
handleDeleteProject,
...props
}: {
project: Project
handleRenameProject: (
e: FormEvent<HTMLFormElement>,
f: Project
) => Promise<void>
handleDeleteProject: (f: Project) => Promise<void>
}) {
useHotkeys('esc', () => setIsEditing(false))
const [isEditing, setIsEditing] = useState(false)
const [isConfirmingDelete, setIsConfirmingDelete] = useState(false)
const [numberOfFiles, setNumberOfFiles] = useState(1)
const [numberOfFolders, setNumberOfFolders] = useState(0)
let inputRef = useRef<HTMLInputElement>(null)
function handleSave(e: FormEvent<HTMLFormElement>) {
e.preventDefault()
void handleRenameProject(e, project).then(() => setIsEditing(false))
}
function getDisplayedTime(dateStr: string) {
const date = new Date(dateStr)
const startOfToday = new Date()
startOfToday.setHours(0, 0, 0, 0)
return date.getTime() < startOfToday.getTime()
? date.toLocaleDateString()
: date.toLocaleTimeString()
}
useEffect(() => {
async function getNumberOfFiles() {
setNumberOfFiles(project.kcl_file_count)
setNumberOfFolders(project.directory_count)
}
void getNumberOfFiles()
}, [project.kcl_file_count, project.directory_count])
useEffect(() => {
if (inputRef.current) {
inputRef.current.focus()
inputRef.current.select()
}
}, [inputRef.current])
return (
<li
{...props}
className="group relative min-h-[5em] p-1 rounded-sm border border-chalkboard-20 dark:border-chalkboard-80 hover:!border-primary"
>
{isEditing ? (
<form onSubmit={handleSave} className="flex items-center gap-2">
<input
className="min-w-0 p-1 dark:bg-chalkboard-80 dark:border-chalkboard-40 focus:outline-none"
type="text"
id="newProjectName"
name="newProjectName"
autoCorrect="off"
autoCapitalize="off"
defaultValue={project.name}
ref={inputRef}
/>
<div className="flex items-center gap-1">
<ActionButton
Element="button"
type="submit"
iconStart={{
icon: faCheck,
size: 'sm',
className: 'p-1',
bgClassName: '!bg-transparent',
}}
className="!p-0"
>
<Tooltip position="left" delay={1000}>
Rename project
</Tooltip>
</ActionButton>
<ActionButton
Element="button"
iconStart={{
icon: faX,
size: 'sm',
iconClassName: 'dark:!text-chalkboard-20',
bgClassName: '!bg-transparent',
className: 'p-1',
}}
className="!p-0"
onClick={() => setIsEditing(false)}
>
<Tooltip position="left" delay={1000}>
Cancel
</Tooltip>
</ActionButton>
</div>
</form>
) : (
<>
<Link
className="relative z-0 flex flex-col h-full gap-2 p-1 !no-underline !text-chalkboard-110 dark:!text-chalkboard-10"
to={`${paths.FILE}/${encodeURIComponent(project.default_file)}`}
data-testid="project-link"
>
<div className="flex-1">{project.name?.replace(FILE_EXT, '')}</div>
<span className="text-xs text-chalkboard-60">
{numberOfFiles} file{numberOfFiles === 1 ? '' : 's'}{' '}
{numberOfFolders > 0 &&
`/ ${numberOfFolders} folder${
numberOfFolders === 1 ? '' : 's'
}`}
</span>
<span className="text-xs text-chalkboard-60">
Edited{' '}
{project.metadata && project.metadata?.modified
? getDisplayedTime(project.metadata.modified)
: 'never'}
</span>
</Link>
<div className="absolute z-10 flex items-center gap-1 opacity-0 bottom-2 right-2 group-hover:opacity-100 group-focus-within:opacity-100">
<ActionButton
Element="button"
iconStart={{
icon: faPenAlt,
className: 'p-1',
iconClassName: 'dark:!text-chalkboard-20',
bgClassName: '!bg-transparent',
size: 'xs',
}}
onClick={(e) => {
e.stopPropagation()
e.nativeEvent.stopPropagation()
setIsEditing(true)
}}
className="!p-0"
>
<Tooltip position="left" delay={1000}>
Rename project
</Tooltip>
</ActionButton>
<ActionButton
Element="button"
iconStart={{
icon: faTrashAlt,
className: 'p-1',
size: 'xs',
bgClassName: '!bg-transparent',
iconClassName: '!text-destroy-70',
}}
className="!p-0 hover:border-destroy-40 dark:hover:border-destroy-40"
onClick={(e) => {
e.stopPropagation()
e.nativeEvent.stopPropagation()
setIsConfirmingDelete(true)
}}
>
<Tooltip position="left" delay={1000}>
Delete project
</Tooltip>
</ActionButton>
</div>
<Dialog
open={isConfirmingDelete}
onClose={() => setIsConfirmingDelete(false)}
className="relative z-50"
>
<div className="fixed inset-0 grid bg-chalkboard-110/80 place-content-center">
<Dialog.Panel className="max-w-2xl p-4 border rounded bg-chalkboard-10 dark:bg-chalkboard-100 border-destroy-80">
<Dialog.Title as="h2" className="mb-4 text-2xl font-bold">
Delete File
</Dialog.Title>
<Dialog.Description>
This will permanently delete "{project.name || 'this file'}".
</Dialog.Description>
<p className="my-4">
Are you sure you want to delete "{project.name || 'this file'}
"? This action cannot be undone.
</p>
<div className="flex justify-between">
<ActionButton
Element="button"
onClick={async () => {
await handleDeleteProject(project)
setIsConfirmingDelete(false)
}}
iconStart={{
icon: faTrashAlt,
bgClassName: 'bg-destroy-80',
className: 'p-1',
size: 'sm',
iconClassName: '!text-destroy-70 dark:!text-destroy-40',
}}
className="hover:border-destroy-40 dark:hover:border-destroy-40"
>
Delete
</ActionButton>
<ActionButton
Element="button"
onClick={() => setIsConfirmingDelete(false)}
>
Cancel
</ActionButton>
</div>
</Dialog.Panel>
</div>
</Dialog>
</>
)}
</li>
)
}
export default ProjectCard

View File

@ -0,0 +1,53 @@
import { Dialog } from '@headlessui/react'
import { ActionButton } from 'components/ActionButton'
interface DeleteProjectDialogProps {
projectName: string
onConfirm: () => void
onDismiss: () => void
}
export function DeleteProjectDialog({
projectName,
onConfirm,
onDismiss,
}: DeleteProjectDialogProps) {
return (
<Dialog open={true} onClose={onDismiss} className="relative z-50">
<div className="fixed inset-0 grid bg-chalkboard-110/80 place-content-center">
<Dialog.Panel className="max-w-2xl p-4 border rounded bg-chalkboard-10 dark:bg-chalkboard-100 border-destroy-80">
<Dialog.Title as="h2" className="mb-4 text-2xl font-bold">
Delete File
</Dialog.Title>
<Dialog.Description>
This will permanently delete "{projectName || 'this file'}
".
</Dialog.Description>
<p className="my-4">
Are you sure you want to delete "{projectName || 'this file'}
"? This action cannot be undone.
</p>
<div className="flex justify-between">
<ActionButton
Element="button"
onClick={onConfirm}
iconStart={{
icon: 'trash',
bgClassName: 'bg-destroy-10 dark:bg-destroy-80',
iconClassName: '!text-destroy-80 dark:!text-destroy-20',
}}
className="hover:border-destroy-40 dark:hover:border-destroy-40 hover:bg-destroy-10/20 dark:hover:bg-destroy-80/20"
>
Delete
</ActionButton>
<ActionButton Element="button" onClick={onDismiss}>
Cancel
</ActionButton>
</div>
</Dialog.Panel>
</div>
</Dialog>
)
}

View File

@ -0,0 +1,176 @@
import { FormEvent, useEffect, useRef, useState } from 'react'
import { paths } from 'lib/paths'
import { Link } from 'react-router-dom'
import { ActionButton } from '../ActionButton'
import { FILE_EXT } from 'lib/constants'
import { useHotkeys } from 'react-hotkeys-hook'
import Tooltip from '../Tooltip'
import { DeleteProjectDialog } from './DeleteProjectDialog'
import { ProjectCardRenameForm } from './ProjectCardRenameForm'
import { Project } from 'wasm-lib/kcl/bindings/Project'
function ProjectCard({
project,
handleRenameProject,
handleDeleteProject,
...props
}: {
project: Project
handleRenameProject: (
e: FormEvent<HTMLFormElement>,
f: Project
) => Promise<void>
handleDeleteProject: (f: Project) => Promise<void>
}) {
useHotkeys('esc', () => setIsEditing(false))
const [isEditing, setIsEditing] = useState(false)
const [isConfirmingDelete, setIsConfirmingDelete] = useState(false)
const [numberOfFiles, setNumberOfFiles] = useState(1)
const [numberOfFolders, setNumberOfFolders] = useState(0)
// const [imageUrl, setImageUrl] = useState('')
let inputRef = useRef<HTMLInputElement>(null)
function handleSave(e: FormEvent<HTMLFormElement>) {
e.preventDefault()
void handleRenameProject(e, project).then(() => setIsEditing(false))
}
function getDisplayedTime(dateStr: string) {
const date = new Date(dateStr)
const startOfToday = new Date()
startOfToday.setHours(0, 0, 0, 0)
return date.getTime() < startOfToday.getTime()
? date.toLocaleDateString()
: date.toLocaleTimeString()
}
useEffect(() => {
async function getNumberOfFiles() {
setNumberOfFiles(project.kcl_file_count)
setNumberOfFolders(project.directory_count)
}
// async function setupImageUrl() {
// const projectImagePath = await join(project.path, PROJECT_IMAGE_NAME)
// if (await exists(projectImagePath)) {
// const imageData = await readFile(projectImagePath)
// const blob = new Blob([imageData], { type: 'image/jpg' })
// const imageUrl = URL.createObjectURL(blob)
// setImageUrl(imageUrl)
// }
// }
void getNumberOfFiles()
// void setupImageUrl()
}, [project.kcl_file_count, project.directory_count])
useEffect(() => {
if (inputRef.current && isEditing) {
inputRef.current.focus()
inputRef.current.select()
}
}, [isEditing, inputRef.current])
return (
<li
{...props}
className="group relative flex flex-col rounded-sm border border-primary/40 dark:border-chalkboard-80 hover:!border-primary"
>
<Link
data-testid="project-link"
to={`${paths.FILE}/${encodeURIComponent(project.default_file)}`}
className="flex flex-col flex-1 !no-underline !text-chalkboard-110 dark:!text-chalkboard-10 group-hover:!hue-rotate-0 min-h-[5em] divide-y divide-primary/40 dark:divide-chalkboard-80 group-hover:!divide-primary"
>
{/* <div className="h-36 relative overflow-hidden bg-gradient-to-b from-transparent to-primary/10 rounded-t-sm">
{imageUrl && (
<img
src={imageUrl}
alt=""
className="h-full w-full transition-transform group-hover:scale-105 object-cover"
/>
)}
</div> */}
<div className="pb-2 flex flex-col flex-grow flex-auto gap-2 rounded-b-sm">
{isEditing ? (
<ProjectCardRenameForm
onSubmit={handleSave}
className="flex items-center gap-2 p-2"
onClick={(e) => e.stopPropagation()}
project={project}
onDismiss={() => setIsEditing(false)}
ref={inputRef}
/>
) : (
<h3 className="font-sans relative z-0 p-2">
{project.name?.replace(FILE_EXT, '')}
</h3>
)}
<span className="px-2 text-chalkboard-60 text-xs">
{numberOfFiles} file{numberOfFiles === 1 ? '' : 's'}{' '}
{numberOfFolders > 0 &&
`/ ${numberOfFolders} folder${numberOfFolders === 1 ? '' : 's'}`}
</span>
<span className="px-2 text-chalkboard-60 text-xs">
Edited{' '}
{project.metadata && project.metadata?.modified
? getDisplayedTime(project.metadata.modified)
: 'never'}
</span>
</div>
</Link>
{!isEditing && (
<div className="absolute z-10 flex items-center gap-1 opacity-0 bottom-2 right-2 group-hover:opacity-100 group-focus-within:opacity-100">
<ActionButton
Element="button"
iconStart={{
icon: 'sketch',
iconClassName: 'dark:!text-chalkboard-20',
bgClassName: '!bg-transparent',
}}
onClick={(e) => {
e.stopPropagation()
e.nativeEvent.stopPropagation()
setIsEditing(true)
}}
className="!p-0"
>
<Tooltip position="top-right" delay={1000}>
Rename project
</Tooltip>
</ActionButton>
<ActionButton
Element="button"
iconStart={{
icon: 'trash',
iconClassName: 'dark:!text-chalkboard-30',
bgClassName: '!bg-transparent',
}}
className="!p-0"
onClick={(e) => {
e.stopPropagation()
e.nativeEvent.stopPropagation()
setIsConfirmingDelete(true)
}}
>
<Tooltip position="top-right" delay={1000}>
Delete project
</Tooltip>
</ActionButton>
</div>
)}
{isConfirmingDelete && (
<DeleteProjectDialog
projectName={project.name}
onConfirm={async () => {
await handleDeleteProject(project)
setIsConfirmingDelete(false)
}}
onDismiss={() => setIsConfirmingDelete(false)}
/>
)}
</li>
)
}
export default ProjectCard

View File

@ -0,0 +1,67 @@
import { ActionButton } from 'components/ActionButton'
import Tooltip from 'components/Tooltip'
import { HTMLProps, forwardRef } from 'react'
import { Project } from 'wasm-lib/kcl/bindings/Project'
interface ProjectCardRenameFormProps extends HTMLProps<HTMLFormElement> {
project: Project
onDismiss: () => void
}
export const ProjectCardRenameForm = forwardRef(
(
{ project, onDismiss, ...props }: ProjectCardRenameFormProps,
ref: React.Ref<HTMLInputElement>
) => {
return (
<form {...props}>
<input
className="min-w-0 dark:bg-chalkboard-80 dark:border-chalkboard-40 focus:outline-none"
type="text"
id="newProjectName"
onClickCapture={(e) => e.preventDefault()}
name="newProjectName"
required
autoCorrect="off"
autoCapitalize="off"
defaultValue={project.name}
ref={ref}
onKeyDown={(e) => {
if (e.key === 'Escape') {
onDismiss()
}
}}
/>
<div className="flex items-center gap-1">
<ActionButton
Element="button"
type="submit"
iconStart={{
icon: 'checkmark',
bgClassName: '!bg-transparent',
}}
className="!p-0"
>
<Tooltip position="left" delay={1000}>
Rename project
</Tooltip>
</ActionButton>
<ActionButton
Element="button"
iconStart={{
icon: 'close',
iconClassName: 'dark:!text-chalkboard-20',
bgClassName: '!bg-transparent',
}}
className="!p-0"
onClick={onDismiss}
>
<Tooltip position="left" delay={1000}>
Cancel
</Tooltip>
</ActionButton>
</div>
</form>
)
}
)

View File

@ -24,6 +24,8 @@ export const PROJECT_FOLDER = 'zoo-modeling-app-projects'
export const FILE_EXT = '.kcl'
/** Default file to open when a project is opened */
export const PROJECT_ENTRYPOINT = `main${FILE_EXT}` as const
/** Thumbnail file name */
export const PROJECT_IMAGE_NAME = `main.jpg` as const
/** The localStorage key for last-opened projects */
export const FILE_PERSIST_KEY = `${PROJECT_FOLDER}-last-opened` as const
/** The default name given to new kcl files in a project */

View File

@ -1,19 +1,18 @@
import {
faArrowDown,
faArrowUp,
faCircle,
} from '@fortawesome/free-solid-svg-icons'
import { CustomIconName } from 'components/CustomIcon'
import { Project } from 'wasm-lib/kcl/bindings/Project'
const DESC = ':desc'
export function getSortIcon(currentSort: string, newSort: string) {
export function getSortIcon(
currentSort: string,
newSort: string
): CustomIconName {
if (currentSort === newSort) {
return faArrowUp
return 'arrowUp'
} else if (currentSort === newSort + DESC) {
return faArrowDown
return 'arrowDown'
}
return faCircle
return 'horizontalDash'
}
export function getNextSearchParams(currentSort: string, newSort: string) {

View File

@ -4,16 +4,15 @@ import {
getNextProjectIndex,
interpolateProjectNameWithIndex,
doesProjectNameNeedInterpolated,
} from '../lib/tauriFS'
import { ActionButton } from '../components/ActionButton'
import { faArrowDown, faPlus } from '@fortawesome/free-solid-svg-icons'
} from 'lib/tauriFS'
import { ActionButton } from 'components/ActionButton'
import { toast } from 'react-hot-toast'
import { AppHeader } from '../components/AppHeader'
import ProjectCard from '../components/ProjectCard'
import { AppHeader } from 'components/AppHeader'
import ProjectCard from 'components/ProjectCard/ProjectCard'
import { useLoaderData, useNavigate, useSearchParams } from 'react-router-dom'
import { Link } from 'react-router-dom'
import { type HomeLoaderData } from 'lib/types'
import Loading from '../components/Loading'
import Loading from 'components/Loading'
import { useMachine } from '@xstate/react'
import { homeMachine } from '../machines/homeMachine'
import { ContextFrom, EventFrom } from 'xstate'
@ -187,9 +186,11 @@ const Home = () => {
new FormData(e.target as HTMLFormElement)
)
send('Rename project', {
data: { oldName: project.name, newName: newProjectName },
})
if (newProjectName !== project.name) {
send('Rename project', {
data: { oldName: project.name, newName: newProjectName },
})
}
}
async function handleDeleteProject(project: Project) {
@ -199,71 +200,93 @@ const Home = () => {
return (
<div className="relative flex flex-col h-screen overflow-hidden">
<AppHeader showToolbar={false} />
<div className="w-full max-w-5xl px-4 mx-auto my-24 overflow-y-auto lg:px-0">
<section className="flex justify-between">
<h1 className="text-3xl font-bold">Your Projects</h1>
<div className="flex gap-2 items-center">
<small>Sort by</small>
<ActionButton
Element="button"
className={
'text-sm ' +
(!sort.includes('name')
? 'text-chalkboard-80 dark:text-chalkboard-40'
: '')
}
onClick={() => setSearchParams(getNextSearchParams(sort, 'name'))}
iconStart={{
icon: getSortIcon(sort, 'name'),
className: 'p-1.5',
iconClassName: !sort.includes('name')
? '!text-chalkboard-40'
: '',
size: 'sm',
}}
>
Name
</ActionButton>
<ActionButton
Element="button"
className={
'text-sm ' +
(!isSortByModified
? 'text-chalkboard-80 dark:text-chalkboard-40'
: '')
}
onClick={() =>
setSearchParams(getNextSearchParams(sort, 'modified'))
}
iconStart={{
icon: sort ? getSortIcon(sort, 'modified') : faArrowDown,
className: 'p-1.5',
iconClassName: !isSortByModified ? '!text-chalkboard-40' : '',
size: 'sm',
}}
>
Last Modified
</ActionButton>
<div className="w-full flex flex-col overflow-hidden max-w-5xl px-4 mx-auto mt-24 lg:px-2">
<section>
<div className="flex justify-between items-baseline select-none">
<div className="flex gap-8 items-baseline">
<h1 className="text-3xl font-bold">Your Projects</h1>
<ActionButton
Element="button"
onClick={() => send('Create project')}
className="group !bg-primary !text-chalkboard-10 !border-primary hover:shadow-inner hover:hue-rotate-15"
iconStart={{
icon: 'plus',
bgClassName: '!bg-transparent rounded-sm',
iconClassName:
'!text-chalkboard-10 transition-transform group-active:rotate-90',
}}
data-testid="home-new-file"
>
New project
</ActionButton>
</div>
<div className="flex gap-2 items-center">
<small>Sort by</small>
<ActionButton
Element="button"
className={
'text-xs border-primary/10 ' +
(!sort.includes('name')
? 'text-chalkboard-80 dark:text-chalkboard-40'
: '')
}
onClick={() =>
setSearchParams(getNextSearchParams(sort, 'name'))
}
iconStart={{
icon: getSortIcon(sort, 'name'),
bgClassName: 'bg-transparent',
iconClassName: !sort.includes('name')
? '!text-chalkboard-90 dark:!text-chalkboard-30'
: '',
}}
>
Name
</ActionButton>
<ActionButton
Element="button"
className={
'text-xs border-primary/10 ' +
(!isSortByModified
? 'text-chalkboard-80 dark:text-chalkboard-40'
: '')
}
onClick={() =>
setSearchParams(getNextSearchParams(sort, 'modified'))
}
iconStart={{
icon: sort ? getSortIcon(sort, 'modified') : 'arrowDown',
bgClassName: 'bg-transparent',
iconClassName: !isSortByModified
? '!text-chalkboard-90 dark:!text-chalkboard-30'
: '',
}}
>
Last Modified
</ActionButton>
</div>
</div>
</section>
<section data-testid="home-section">
<p className="my-4 text-sm text-chalkboard-80 dark:text-chalkboard-30">
Loaded from{' '}
<span className="text-chalkboard-90 dark:text-chalkboard-20">
<Link
to="settings?tab=user#projectDirectory"
className="text-chalkboard-90 dark:text-chalkboard-20 underline underline-offset-2"
>
{settings.app.projectDirectory.current}
</span>
.{' '}
<Link to="settings" className="underline underline-offset-2">
Edit in settings
</Link>
.
</p>
</section>
<section
data-testid="home-section"
className="flex-1 overflow-y-auto pr-2 pb-24"
>
{state.matches('Reading projects') ? (
<Loading>Loading your Projects...</Loading>
) : (
<>
{projects.length > 0 ? (
<ul className="grid w-full grid-cols-4 gap-4 my-8">
<ul className="grid w-full grid-cols-4 gap-4">
{projects.sort(getSortFunction(sort)).map((project) => (
<ProjectCard
key={project.name}
@ -278,14 +301,6 @@ const Home = () => {
No Projects found, ready to make your first one?
</p>
)}
<ActionButton
Element="button"
onClick={() => send('Create project')}
iconStart={{ icon: faPlus, iconClassName: 'p-1 w-4' }}
data-testid="home-new-file"
>
New project
</ActionButton>
</>
)}
</section>