feat: improve mobile ui

This commit is contained in:
0xzio
2025-05-20 21:48:58 +08:00
parent dba7915737
commit f98d0ae763
18 changed files with 132 additions and 111 deletions

View File

@@ -1,5 +1,3 @@
import { DndProvider } from 'react-dnd'
import { HTML5Backend } from 'react-dnd-html5-backend'
import { Redirect, Route } from 'react-router-dom'
import { DarkMode } from '@aparajita/capacitor-dark-mode'
import { SafeArea } from '@capacitor-community/safe-area'
@@ -30,6 +28,11 @@ import '@ionic/react/css/text-alignment.css'
import '@ionic/react/css/text-transformation.css'
import '@ionic/react/css/flex-utils.css'
import '@ionic/react/css/display.css'
//
import 'react-grid-layout/css/styles.css'
import 'react-resizable/css/styles.css'
import 'react-datepicker/dist/react-datepicker.css'
import '@glideapps/glide-data-grid/dist/index.css'
/**
* Ionic Dark Mode
* -----------------------------------------------------
@@ -142,26 +145,25 @@ const App: React.FC = () => {
return (
<IonApp>
<div id="portal" className="fixed left-0 top-0 z-[100000000]" />
<LinguiClientProvider initialLocale={'en'} initialMessages={{}}>
<DndProvider backend={HTML5Backend}>
<DashboardProviders>
<IonReactRouter>
<IonSplitPane contentId="main">
<Menu />
<IonRouterOutlet id="main">
<Route path="/" exact={true}>
<Redirect to="/folder/area" />
</Route>
<Route path="/folder/:name" exact={true}>
<NavProvider nav={nav.current!}>
<IonNav ref={nav} root={() => <PageHome />}></IonNav>
</NavProvider>
</Route>
</IonRouterOutlet>
</IonSplitPane>
</IonReactRouter>
</DashboardProviders>
</DndProvider>
<DashboardProviders>
<IonReactRouter>
<IonSplitPane contentId="main">
<Menu />
<IonRouterOutlet id="main">
<Route path="/" exact={true}>
<Redirect to="/folder/area" />
</Route>
<Route path="/folder/:name" exact={true}>
<NavProvider nav={nav.current!}>
<IonNav ref={nav} root={() => <PageHome />}></IonNav>
</NavProvider>
</Route>
</IonRouterOutlet>
</IonSplitPane>
</IonReactRouter>
</DashboardProviders>
</LinguiClientProvider>
</IonApp>
)

View File

@@ -51,7 +51,21 @@ export const AreaList: React.FC = () => {
return (
<div className="">
<div className="mb-2 mt-2 px-3 text-xl font-bold">My areas</div>
<div className="flex items-center justify-between">
<div className="mb-2 mt-2 px-3 text-xl font-bold">My areas</div>
<IonMenuToggle>
<MenuItem
className="flex cursor-pointer items-center gap-2"
onClick={async () => {
setIsOpen(true)
// menuController.close()
menuController.close('main')
}}
>
<PlusIcon size={24} />
</MenuItem>
</IonMenuToggle>
</div>
<div className="flex flex-col gap-1">
{areas.map((item) => (
@@ -81,22 +95,6 @@ export const AreaList: React.FC = () => {
</MenuItem>
</IonMenuToggle>
))}
<IonMenuToggle>
<MenuItem
className="flex cursor-pointer items-center gap-2"
onClick={async () => {
setIsOpen(true)
// menuController.close()
// menuController.close('main')
}}
>
<PlusIcon size={24} />
<div>
<Trans id="Create area"></Trans>
</div>
</MenuItem>
</IonMenuToggle>
</div>
</div>
)

View File

@@ -3,7 +3,6 @@ import { useHomeTab } from '@/hooks/useHomeTab'
import { IonFooter, IonToolbar } from '@ionic/react'
import { PlusIcon } from 'lucide-react'
import { Button } from '@penx/uikit/button'
import { cn } from '@penx/utils'
interface Props {
onAdd: () => void
@@ -13,63 +12,27 @@ export const Footer = ({ onAdd }: Props) => {
const { setType } = useHomeTab()
return (
<IonFooter>
<IonFooter
style={{
boxShadow: '0 0 0 rgba(0, 0, 0, 0)',
}}
>
<IonToolbar
className="toolbar px-3"
style={{
'--border-width': 0,
}}
>
<div className="flex items-center justify-between gap-3 rounded-full px-3">
<div className="flex items-center justify-center gap-3 rounded-full px-3 pb-4">
<Button
size="icon"
variant="ghost"
className={cn('size-8 rounded-full')}
onClick={async () => {
setType('HOME')
}}
>
<span className="icon-[solar--home-2-linear] size-6"></span>
</Button>
<Button
size="icon"
variant="ghost"
className={cn('size-8 rounded-full')}
onClick={async () => {
setType('NOTE')
}}
>
<span className="icon-[solar--notes-linear] size-6"></span>
</Button>
<Button
size="icon"
variant="ghost"
className="text-background bg-foreground size-9 rounded-full"
className="text-background shadow-popover size-14 rounded-full"
onClick={async () => {
onAdd()
}}
>
<PlusIcon size={24} />
</Button>
<Button
size="icon"
variant="ghost"
className={cn('size-8 rounded-full')}
onClick={async () => {
setType('TASK')
}}
>
<span className="icon-[solar--check-square-linear] size-6"></span>
</Button>
<Button
size="icon"
variant="ghost"
className={cn('size-8 rounded-full')}
onClick={async () => {
setType('PROFILE')
}}
>
<span className="icon-[solar--user-linear] size-6"></span>
<PlusIcon size={28} className="text-foreground" />
</Button>
</div>
</IonToolbar>

View File

@@ -1,6 +1,10 @@
import { useEffect, useRef } from 'react'
import { Capacitor } from '@capacitor/core'
import { IonContent, IonMenu } from '@ionic/react'
import { AreaWidgets } from '@penx/components/area-widgets/AreaWidgets'
import { useMobileMenu } from '@penx/hooks/useMobileMenu'
import { useSession } from '@penx/session'
import { store } from '@penx/store'
import { cn } from '@penx/utils'
import { AreaList } from './AreaList'
import { MobileModeToggle } from './MobileModeToggle'
@@ -8,12 +12,22 @@ import { MobileModeToggle } from './MobileModeToggle'
const platform = Capacitor.getPlatform()
const Menu: React.FC = () => {
const { setMenu } = useMobileMenu()
const { isLoading } = useSession()
const menu = useRef<HTMLIonMenuElement>(null)
if (isLoading) return null
useEffect(() => {
setMenu(menu)
}, [menu])
return (
<IonMenu contentId="main" type="overlay">
<IonMenu
ref={menu}
id="menu"
menuId="myMenu"
contentId="main"
type="overlay"
>
<IonContent className="ion-padding safe-area drawer-menu h-full">
<div
className={cn(
@@ -23,6 +37,7 @@ const Menu: React.FC = () => {
>
<div className="flex-1">
<AreaList />
<AreaWidgets />
</div>
<div className="flex items-center justify-between">
<MobileModeToggle />

View File

@@ -1,4 +1,6 @@
import React, { useEffect, useRef, useState } from 'react'
import { DndProvider } from 'react-dnd'
import { HTML5Backend } from 'react-dnd-html5-backend'
import { CreationMenu } from '@/components/CreationMenu'
import { MobileCreation } from '@/components/MobileCreation'
import { Capacitor } from '@capacitor/core'
@@ -59,7 +61,9 @@ export const PageCreation = ({
</IonToolbar>
</IonHeader>
<IonContent className="ion-padding">
<MobileCreation creationId={creationId} />
<DndProvider backend={HTML5Backend}>
<MobileCreation creationId={creationId} />
</DndProvider>
</IonContent>
</>
)

View File

@@ -36,6 +36,7 @@ import {
} from 'motion/react'
import { EditWidgetButton } from '@penx/components/area-widgets/EditWidget/EditWidgetButton'
import { AreaDialog } from '@penx/components/AreaDialog'
import { PanelList } from '@penx/components/DashboardLayout/PanelList'
import { QuickInput } from '@penx/components/QuickInput'
import { appEmitter } from '@penx/emitter'
import { useArea } from '@penx/hooks/useArea'
@@ -44,6 +45,7 @@ import { ICreationNode } from '@penx/model-type'
import { useSession } from '@penx/session'
import { Button } from '@penx/uikit/button'
import { Separator } from '@penx/uikit/separator'
import { SidebarProvider } from '@penx/uikit/ui/sidebar'
import { cn } from '@penx/utils'
import { PageCreation } from './PageCreation'
@@ -188,7 +190,7 @@ const PageHome: React.FC = ({ nav }: any) => {
<IonButtons slot="end" className="">
<SearchButton />
<EditWidgetButton />
{/* <EditWidgetButton /> */}
</IonButtons>
</IonToolbar>
</IonHeader>
@@ -201,17 +203,20 @@ const PageHome: React.FC = ({ nav }: any) => {
</IonHeader> */}
<div
className="text-foreground relative flex min-h-full flex-col px-1"
className="text-foreground z-1 relative flex min-h-full flex-col px-1"
style={
{
'--background': 'oklch(1 0 0)',
} as any
}
>
{type === 'HOME' && <MobileHome />}
{type === 'TASK' && <MobileTask />}
{type === 'PROFILE' &&
(session ? <ProfileContent /> : <LoginContent />)}
<SidebarProvider>
<PanelList />
</SidebarProvider>
{/* {type === 'HOME' && <MobileHome />} */}
{/* {type === 'TASK' && <MobileTask />} */}
{/* {type === 'PROFILE' &&
(session ? <ProfileContent /> : <LoginContent />)} */}
</div>
</IonContent>

View File

@@ -21,7 +21,7 @@ import { useStructs } from '@penx/hooks/useStructs'
import { ICreationNode } from '@penx/model-type'
import { store } from '@penx/store'
import { trpc } from '@penx/trpc-client'
import { StructType, Panel } from '@penx/types'
import { Panel, StructType } from '@penx/types'
import { Checkbox } from '@penx/uikit/checkbox'
import { Separator } from '@penx/uikit/separator'
import { cn } from '@penx/utils'
@@ -111,7 +111,7 @@ export function Creation({ panel, className, ref }: Props) {
<div
className={cn(
'creation-container relative z-0 min-h-[100vh] flex-1 flex-col overflow-y-auto overflow-x-hidden px-0 pb-40 md:px-8',
isMobileApp && 'pt-0',
isMobileApp && 'px-3 pt-0',
className,
)}
onClick={(e: any) => {
@@ -187,7 +187,7 @@ export function Creation({ panel, className, ref }: Props) {
</div>
)}
<div className="flex items-center justify-between">
<div className="flex items-center justify-between bg-">
<div className="flex items-center gap-1">
<ChangeType creation={creation} />
<div className="text-foreground/60 text-lg"></div>

View File

@@ -11,7 +11,7 @@ export const PropList = ({ onUpdateProps }: Props) => {
const { structs } = useStructs()
const struct = structs.find((m) => m.id === creation.structId)!
if (!struct.columns.length) return null
if (struct.columns.length < 2) return null
return (
<div className="mt-4 flex flex-col gap-1">
{struct.columns.map((column, i) => {

View File

@@ -1,6 +1,7 @@
'use client'
import { ReactNode } from 'react'
import { isMobileApp } from '@penx/constants'
import { SidebarTrigger } from '@penx/uikit/sidebar'
export function PanelHeaderWrapper({
@@ -10,6 +11,7 @@ export function PanelHeaderWrapper({
children: ReactNode
index: number
}) {
if (isMobileApp!) return null
if (index === 0)
return (
<div className="flex h-10 shrink-0 items-center gap-1 pl-2 pr-1">

View File

@@ -1,5 +1,6 @@
'use client'
import { isMobileApp } from '@penx/constants'
import { Panel, PanelType } from '@penx/types'
import { ResizableHandle, ResizablePanel } from '@penx/uikit/resizable'
import { cn } from '@penx/utils'
@@ -60,7 +61,7 @@ export function PanelItem({
{panel.type === PanelType.WIDGET && (
<>
<PanelWidgetHeader index={index} panel={panel} />
{!isMobileApp && <PanelWidgetHeader index={index} panel={panel} />}
<PanelWidget index={index} panel={panel} />
</>
)}

View File

@@ -7,7 +7,7 @@ import {
usePanelCreationContext,
} from '@penx/components/PanelCreationProvider'
import { PublishDialog } from '@penx/components/PublishDialog'
import { BUILTIN_PAGE_SLUGS, ROOT_DOMAIN } from '@penx/constants'
import { BUILTIN_PAGE_SLUGS, isMobileApp, ROOT_DOMAIN } from '@penx/constants'
import { CreationStatus } from '@penx/db/client'
import { getSiteDomain } from '@penx/libs/getSiteDomain'
import { Panel } from '@penx/types'
@@ -45,6 +45,7 @@ export function Content({ panel, index }: Props) {
<ClosePanelButton panel={panel} />
</div>
</PanelHeaderWrapper>
<Creation panel={panel} className="" />
</>
)

View File

@@ -21,7 +21,7 @@ import { toast } from 'sonner'
import { useLocalStorage, useWindowSize } from 'usehooks-ts'
import { useAddCreation } from '@penx/hooks/useAddCreation'
import { store } from '@penx/store'
import { StructType, PanelType } from '@penx/types'
import { PanelType, StructType } from '@penx/types'
import { Button } from '@penx/uikit/button'
import { Checkbox } from '@penx/uikit/checkbox'
import { Textarea } from '@penx/uikit/textarea'
@@ -81,6 +81,7 @@ export function QuickInput({
addCreation({
type: StructType.NOTE,
content: JSON.stringify(slateValue),
isAddPanel: false,
})
afterSubmit?.()
setInput('')

View File

@@ -31,17 +31,15 @@ import {
TooltipTrigger,
} from '@penx/uikit/tooltip'
import { useLoginDialog } from '@penx/widgets/LoginDialog/useLoginDialog'
import { AddCreationButton } from '../AddCreationButton'
import { AreaWidgets } from '../area-widgets'
import { AreasPopover } from '../AreasPopover/AreasPopover'
import { ProfileButton } from '../ProfileButton'
import { ImportPostEntry } from './ImportPostEntry'
import { QuickSearchTrigger } from './QuickSearchTrigger'
import { VisitSiteButton } from './VisitSiteButton'
import { AddCreationButton } from '../AddCreationButton'
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
const { session } = useSession()
const { setIsOpen } = useLoginDialog()
return (
<Sidebar variant="inset" {...props}>
<SidebarHeader className="m-0 p-0 pb-1">
@@ -50,7 +48,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
<AreasPopover />
</SidebarMenuItem>
</SidebarMenu>
<div className='flex items-center justify-between mb-1 gap-1'>
<div className="mb-1 flex items-center justify-between gap-1">
<QuickSearchTrigger />
{!isMobileApp && <AddCreationButton></AddCreationButton>}
</div>

View File

@@ -15,7 +15,8 @@ export function AreaWidgets({}: Props) {
<div className="space-y-2">
{isMobileApp && <MobileWidgetList />}
{!isMobileApp && <WidgetList />}
{!isMobileApp && <AddWidgetButton />}
{/* {!isMobileApp && } */}
<AddWidgetButton />
</div>
</>
)

View File

@@ -17,10 +17,11 @@ import { useArea } from '@penx/hooks/useArea'
import { updateCreationProps } from '@penx/hooks/useCreation'
import { creationIdAtom, useCreationId } from '@penx/hooks/useCreationId'
import { useCreationStruct } from '@penx/hooks/useCreationStruct'
import { mobileMenuAtom, useMobileMenu } from '@penx/hooks/useMobileMenu'
import { usePanels } from '@penx/hooks/usePanels'
import { ICreationNode } from '@penx/model-type'
import { store } from '@penx/store'
import { StructType, PanelType, SiteCreation } from '@penx/types'
import { PanelType, SiteCreation, StructType } from '@penx/types'
import { Checkbox } from '@penx/uikit/checkbox'
import {
ContextMenu,
@@ -41,6 +42,7 @@ export function CreationItem({ creation, className }: CreationItemProps) {
const { isCreationInPanels } = usePanels()
const { isAll, setVisible } = useIsAllContext()
const struct = useCreationStruct(creation)
const { close } = useMobileMenu()
const getTitleFromContent = () => {
try {
@@ -63,10 +65,11 @@ export function CreationItem({ creation, className }: CreationItemProps) {
)}
onClick={() => {
if (isMobileApp) {
appEmitter.emit('ROUTE_TO_CREATION', creation)
store.set(creationIdAtom, creation.id)
// appEmitter.emit('ROUTE_TO_CREATION', creation)
// store.set(creationIdAtom, creation.id)
close()
setVisible?.(false)
return
// return
}
store.panels.updateMainPanel({

View File

@@ -7,11 +7,13 @@ import { AnimatePresence, motion } from 'motion/react'
import { Drawer } from 'vaul'
import { isMobileApp, WidgetType } from '@penx/constants'
import { useArea } from '@penx/hooks/useArea'
import { useMobileMenu } from '@penx/hooks/useMobileMenu'
import { useStructs } from '@penx/hooks/useStructs'
import { store } from '@penx/store'
import { Widget } from '@penx/types'
import { PanelType, Widget } from '@penx/types'
import { ContextMenu, ContextMenuTrigger } from '@penx/uikit/context-menu'
import { DialogDescription, DialogTitle } from '@penx/uikit/dialog'
import { uniqueId } from '@penx/unique-id'
import { cn } from '@penx/utils'
import { WidgetIcon } from '@penx/widgets/WidgetIcon'
import { WidgetName } from '@penx/widgets/WidgetName'
@@ -65,6 +67,7 @@ export const WidgetItem = forwardRef<HTMLDivElement, Props>(
const { area } = useArea()
const [visible, setVisible] = useState(false)
const [struct, setStruct] = useState(null as any)
const { close } = useMobileMenu()
// useEffect(() => {
// if (!dragOverlay) {
@@ -176,7 +179,16 @@ export const WidgetItem = forwardRef<HTMLDivElement, Props>(
{widget.type === WidgetType.ALL_STRUCTS ? (
<StructList
onSelect={(struct) => {
setVisible(!visible)
if (isMobileApp) {
close()
store.panels.openWidgetPanel({
id: uniqueId(),
type: PanelType.WIDGET,
structId: struct.id,
})
} else {
setVisible(!visible)
}
setStruct(struct)
}}
/>

View File

@@ -0,0 +1,15 @@
import { atom, useAtom, useAtomValue } from 'jotai'
export const mobileMenuAtom = atom<any>({} as any)
export function useMobileMenu() {
const [menu, setMenu] = useAtom(mobileMenuAtom)
return {
close: () => {
return menu.current?.close()
},
open: () => menu.current?.open(),
menu,
setMenu,
}
}

View File

@@ -41,7 +41,7 @@ function DropdownMenuContent({
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
className={cn(
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 max-h-(--radix-dropdown-menu-content-available-height) origin-(--radix-dropdown-menu-content-transform-origin) border-foreground/10 z-50 min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border p-1 shadow-md',
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 max-h-(--radix-dropdown-menu-content-available-height) origin-(--radix-dropdown-menu-content-transform-origin) shadow-popover z-50 min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md p-1',
className,
)}
{...props}