feat: initial implementation of responsive app panes (#1198)

This commit is contained in:
Aman Harwara
2022-07-04 21:20:28 +05:30
committed by GitHub
parent 38725daeb9
commit 21ea2ec7a1
20 changed files with 336 additions and 186 deletions

View File

@@ -25,6 +25,7 @@ import PermissionsModalWrapper from '@/Components/PermissionsModal/PermissionsMo
import { PanelResizedData } from '@/Types/PanelResizedData' import { PanelResizedData } from '@/Types/PanelResizedData'
import TagContextMenuWrapper from '@/Components/Tags/TagContextMenuWrapper' import TagContextMenuWrapper from '@/Components/Tags/TagContextMenuWrapper'
import FileDragNDropProvider from '../FileDragNDropProvider/FileDragNDropProvider' import FileDragNDropProvider from '../FileDragNDropProvider/FileDragNDropProvider'
import ResponsivePaneProvider from '../ResponsivePane/ResponsivePaneProvider'
type Props = { type Props = {
application: WebApplication application: WebApplication
@@ -182,19 +183,21 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
featuresController={viewControllerManager.featuresController} featuresController={viewControllerManager.featuresController}
filesController={viewControllerManager.filesController} filesController={viewControllerManager.filesController}
> >
<Navigation application={application} /> <ResponsivePaneProvider>
<ContentListView <Navigation application={application} />
application={application} <ContentListView
accountMenuController={viewControllerManager.accountMenuController} application={application}
filesController={viewControllerManager.filesController} accountMenuController={viewControllerManager.accountMenuController}
itemListController={viewControllerManager.itemListController} filesController={viewControllerManager.filesController}
navigationController={viewControllerManager.navigationController} itemListController={viewControllerManager.itemListController}
noAccountWarningController={viewControllerManager.noAccountWarningController} navigationController={viewControllerManager.navigationController}
noteTagsController={viewControllerManager.noteTagsController} noAccountWarningController={viewControllerManager.noAccountWarningController}
notesController={viewControllerManager.notesController} noteTagsController={viewControllerManager.noteTagsController}
selectionController={viewControllerManager.selectionController} notesController={viewControllerManager.notesController}
/> selectionController={viewControllerManager.selectionController}
<NoteGroupView application={application} /> />
<NoteGroupView application={application} />
</ResponsivePaneProvider>
</FileDragNDropProvider> </FileDragNDropProvider>
</div> </div>

View File

@@ -17,6 +17,8 @@ import { NotesController } from '@/Controllers/NotesController'
import { AccountMenuController } from '@/Controllers/AccountMenu/AccountMenuController' import { AccountMenuController } from '@/Controllers/AccountMenu/AccountMenuController'
import { ElementIds } from '@/Constants/ElementIDs' import { ElementIds } from '@/Constants/ElementIDs'
import ContentListHeader from './Header/ContentListHeader' import ContentListHeader from './Header/ContentListHeader'
import ResponsivePaneContent from '../ResponsivePane/ResponsivePaneContent'
import { AppPaneId } from '../ResponsivePane/AppPaneMetadata'
type Props = { type Props = {
accountMenuController: AccountMenuController accountMenuController: AccountMenuController
@@ -168,11 +170,11 @@ const ContentListView: FunctionComponent<Props> = ({
return ( return (
<div <div
id="items-column" id="items-column"
className="sn-component section app-column app-column-second" className="sn-component section app-column app-column-second border-b border-solid border-border"
aria-label={'Notes & Files'} aria-label={'Notes & Files'}
ref={itemsViewPanelRef} ref={itemsViewPanelRef}
> >
<div className="content"> <ResponsivePaneContent paneId={AppPaneId.Items}>
<div id="items-title-bar" className="section-title-bar border-b border-solid border-border"> <div id="items-title-bar" className="section-title-bar border-b border-solid border-border">
<div id="items-title-bar-container"> <div id="items-title-bar-container">
<ContentListHeader <ContentListHeader
@@ -204,7 +206,7 @@ const ContentListView: FunctionComponent<Props> = ({
selectionController={selectionController} selectionController={selectionController}
/> />
) : null} ) : null}
</div> </ResponsivePaneContent>
{itemsViewPanelRef.current && ( {itemsViewPanelRef.current && (
<PanelResizer <PanelResizer
collapsable={true} collapsable={true}

View File

@@ -7,6 +7,8 @@ import ListItemFlagIcons from './ListItemFlagIcons'
import ListItemTags from './ListItemTags' import ListItemTags from './ListItemTags'
import ListItemMetadata from './ListItemMetadata' import ListItemMetadata from './ListItemMetadata'
import { DisplayableListItemProps } from './Types/DisplayableListItemProps' import { DisplayableListItemProps } from './Types/DisplayableListItemProps'
import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider'
import { AppPaneId } from '../ResponsivePane/AppPaneMetadata'
const FileListItem: FunctionComponent<DisplayableListItemProps> = ({ const FileListItem: FunctionComponent<DisplayableListItemProps> = ({
application, application,
@@ -20,6 +22,8 @@ const FileListItem: FunctionComponent<DisplayableListItemProps> = ({
sortBy, sortBy,
tags, tags,
}) => { }) => {
const { toggleAppPane } = useResponsiveAppPane()
const openFileContextMenu = useCallback( const openFileContextMenu = useCallback(
(posX: number, posY: number) => { (posX: number, posY: number) => {
filesController.setFileContextMenuLocation({ filesController.setFileContextMenuLocation({
@@ -41,9 +45,12 @@ const FileListItem: FunctionComponent<DisplayableListItemProps> = ({
[selectionController, item.uuid, openFileContextMenu], [selectionController, item.uuid, openFileContextMenu],
) )
const onClick = useCallback(() => { const onClick = useCallback(async () => {
void selectionController.selectItem(item.uuid, true) const { didSelect } = await selectionController.selectItem(item.uuid, true)
}, [item.uuid, selectionController]) if (didSelect) {
toggleAppPane(AppPaneId.Editor)
}
}, [item.uuid, selectionController, toggleAppPane])
const IconComponent = () => const IconComponent = () =>
getFileIconComponent( getFileIconComponent(

View File

@@ -1,13 +1,15 @@
import { PLAIN_EDITOR_NAME } from '@/Constants/Constants' import { PLAIN_EDITOR_NAME } from '@/Constants/Constants'
import { sanitizeHtmlString, SNNote } from '@standardnotes/snjs' import { sanitizeHtmlString, SNNote } from '@standardnotes/snjs'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import { FunctionComponent } from 'react' import { FunctionComponent, useCallback } from 'react'
import Icon from '@/Components/Icon/Icon' import Icon from '@/Components/Icon/Icon'
import ListItemConflictIndicator from './ListItemConflictIndicator' import ListItemConflictIndicator from './ListItemConflictIndicator'
import ListItemFlagIcons from './ListItemFlagIcons' import ListItemFlagIcons from './ListItemFlagIcons'
import ListItemTags from './ListItemTags' import ListItemTags from './ListItemTags'
import ListItemMetadata from './ListItemMetadata' import ListItemMetadata from './ListItemMetadata'
import { DisplayableListItemProps } from './Types/DisplayableListItemProps' import { DisplayableListItemProps } from './Types/DisplayableListItemProps'
import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider'
import { AppPaneId } from '../ResponsivePane/AppPaneMetadata'
const NoteListItem: FunctionComponent<DisplayableListItemProps> = ({ const NoteListItem: FunctionComponent<DisplayableListItemProps> = ({
application, application,
@@ -22,6 +24,8 @@ const NoteListItem: FunctionComponent<DisplayableListItemProps> = ({
sortBy, sortBy,
tags, tags,
}) => { }) => {
const { toggleAppPane } = useResponsiveAppPane()
const editorForNote = application.componentManager.editorForNote(item as SNNote) const editorForNote = application.componentManager.editorForNote(item as SNNote)
const editorName = editorForNote?.name ?? PLAIN_EDITOR_NAME const editorName = editorForNote?.name ?? PLAIN_EDITOR_NAME
const [icon, tint] = application.iconsController.getIconAndTintForNoteType(editorForNote?.package_info.note_type) const [icon, tint] = application.iconsController.getIconAndTintForNoteType(editorForNote?.package_info.note_type)
@@ -43,15 +47,20 @@ const NoteListItem: FunctionComponent<DisplayableListItemProps> = ({
} }
} }
const onClick = useCallback(async () => {
const { didSelect } = await selectionController.selectItem(item.uuid, true)
if (didSelect) {
toggleAppPane(AppPaneId.Editor)
}
}, [item.uuid, selectionController, toggleAppPane])
return ( return (
<div <div
className={`content-list-item flex w-full cursor-pointer items-stretch text-text ${ className={`content-list-item flex w-full cursor-pointer items-stretch text-text ${
selected && 'selected border-l-2 border-solid border-info' selected && 'selected border-l-2 border-solid border-info'
}`} }`}
id={item.uuid} id={item.uuid}
onClick={() => { onClick={onClick}
void selectionController.selectItem(item.uuid, true)
}}
onContextMenu={(event) => { onContextMenu={(event) => {
event.preventDefault() event.preventDefault()
void openContextMenu(event.clientX, event.clientY) void openContextMenu(event.clientX, event.clientY)

View File

@@ -4,9 +4,11 @@ import { WebApplication } from '@/Application/Application'
import { PANEL_NAME_NAVIGATION } from '@/Constants/Constants' import { PANEL_NAME_NAVIGATION } from '@/Constants/Constants'
import { ApplicationEvent, PrefKey } from '@standardnotes/snjs' import { ApplicationEvent, PrefKey } from '@standardnotes/snjs'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import { FunctionComponent, useCallback, useEffect, useMemo, useState } from 'react' import { FunctionComponent, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import PanelResizer, { PanelSide, ResizeFinishCallback, PanelResizeType } from '@/Components/PanelResizer/PanelResizer' import PanelResizer, { PanelSide, ResizeFinishCallback, PanelResizeType } from '@/Components/PanelResizer/PanelResizer'
import SearchBar from '@/Components/SearchBar/SearchBar' import SearchBar from '@/Components/SearchBar/SearchBar'
import ResponsivePaneContent from '@/Components/ResponsivePane/ResponsivePaneContent'
import { AppPaneId } from '@/Components/ResponsivePane/AppPaneMetadata'
type Props = { type Props = {
application: WebApplication application: WebApplication
@@ -14,7 +16,7 @@ type Props = {
const Navigation: FunctionComponent<Props> = ({ application }) => { const Navigation: FunctionComponent<Props> = ({ application }) => {
const viewControllerManager = useMemo(() => application.getViewControllerManager(), [application]) const viewControllerManager = useMemo(() => application.getViewControllerManager(), [application])
const [ref, setRef] = useState<HTMLDivElement | null>() const ref = useRef<HTMLDivElement>(null)
const [panelWidth, setPanelWidth] = useState<number>(0) const [panelWidth, setPanelWidth] = useState<number>(0)
useEffect(() => { useEffect(() => {
@@ -44,13 +46,8 @@ const Navigation: FunctionComponent<Props> = ({ application }) => {
}, [viewControllerManager]) }, [viewControllerManager])
return ( return (
<div <div id="navigation" className="sn-component section app-column app-column-first" ref={ref}>
id="navigation" <ResponsivePaneContent paneId={AppPaneId.Navigation} contentElementId="navigation-content">
className="sn-component section app-column app-column-first"
data-aria-label="Navigation"
ref={setRef}
>
<div id="navigation-content" className="content">
<SearchBar <SearchBar
itemListController={viewControllerManager.itemListController} itemListController={viewControllerManager.itemListController}
searchOptionsController={viewControllerManager.searchOptionsController} searchOptionsController={viewControllerManager.searchOptionsController}
@@ -66,12 +63,12 @@ const Navigation: FunctionComponent<Props> = ({ application }) => {
<SmartViewsSection viewControllerManager={viewControllerManager} /> <SmartViewsSection viewControllerManager={viewControllerManager} />
<TagsSection viewControllerManager={viewControllerManager} /> <TagsSection viewControllerManager={viewControllerManager} />
</div> </div>
</div> </ResponsivePaneContent>
{ref && ( {ref.current && (
<PanelResizer <PanelResizer
collapsable={true} collapsable={true}
defaultWidth={150} defaultWidth={150}
panel={ref} panel={ref.current}
hoverable={true} hoverable={true}
side={PanelSide.Right} side={PanelSide.Right}
type={PanelResizeType.WidthOnly} type={PanelResizeType.WidthOnly}

View File

@@ -7,6 +7,8 @@ import MultipleSelectedFiles from '../MultipleSelectedFiles/MultipleSelectedFile
import { ElementIds } from '@/Constants/ElementIDs' import { ElementIds } from '@/Constants/ElementIDs'
import FileView from '@/Components/FileView/FileView' import FileView from '@/Components/FileView/FileView'
import { FileDnDContext } from '@/Components/FileDragNDropProvider/FileDragNDropProvider' import { FileDnDContext } from '@/Components/FileDragNDropProvider/FileDragNDropProvider'
import { AppPaneId } from '../ResponsivePane/AppPaneMetadata'
import ResponsivePaneContent from '../ResponsivePane/ResponsivePaneContent'
type State = { type State = {
showMultipleSelectedNotes: boolean showMultipleSelectedNotes: boolean
@@ -88,49 +90,48 @@ class NoteGroupView extends PureComponent<Props, State> {
return ( return (
<div id={ElementIds.EditorColumn} className="app-column app-column-third h-full"> <div id={ElementIds.EditorColumn} className="app-column app-column-third h-full">
{this.state.showMultipleSelectedNotes && ( <ResponsivePaneContent paneId={AppPaneId.Editor}>
<MultipleSelectedNotes {this.state.showMultipleSelectedNotes && (
application={this.application} <MultipleSelectedNotes
filesController={this.viewControllerManager.filesController} application={this.application}
selectionController={this.viewControllerManager.selectionController} filesController={this.viewControllerManager.filesController}
featuresController={this.viewControllerManager.featuresController} selectionController={this.viewControllerManager.selectionController}
filePreviewModalController={this.viewControllerManager.filePreviewModalController} featuresController={this.viewControllerManager.featuresController}
navigationController={this.viewControllerManager.navigationController} filePreviewModalController={this.viewControllerManager.filePreviewModalController}
notesController={this.viewControllerManager.notesController} navigationController={this.viewControllerManager.navigationController}
noteTagsController={this.viewControllerManager.noteTagsController} notesController={this.viewControllerManager.notesController}
historyModalController={this.viewControllerManager.historyModalController} noteTagsController={this.viewControllerManager.noteTagsController}
/> historyModalController={this.viewControllerManager.historyModalController}
)} />
)}
{this.state.showMultipleSelectedFiles && ( {this.state.showMultipleSelectedFiles && (
<MultipleSelectedFiles <MultipleSelectedFiles
filesController={this.viewControllerManager.filesController} filesController={this.viewControllerManager.filesController}
selectionController={this.viewControllerManager.selectionController} selectionController={this.viewControllerManager.selectionController}
/> />
)} )}
{this.viewControllerManager.navigationController.isInFilesView && fileDragNDropContext?.isDraggingFiles && (
{this.viewControllerManager.navigationController.isInFilesView && fileDragNDropContext?.isDraggingFiles && ( <div className="absolute bottom-8 left-1/2 z-dropdown-menu -translate-x-1/2 rounded bg-info px-5 py-3 text-info-contrast shadow-main">
<div className="absolute bottom-8 left-1/2 z-dropdown-menu -translate-x-1/2 rounded bg-info px-5 py-3 text-info-contrast shadow-main"> Drop your files to upload them
Drop your files to upload them </div>
</div> )}
)} {shouldNotShowMultipleSelectedItems && this.state.controllers.length > 0 && (
<>
{shouldNotShowMultipleSelectedItems && this.state.controllers.length > 0 && ( {this.state.controllers.map((controller) => {
<> return controller instanceof NoteViewController ? (
{this.state.controllers.map((controller) => { <NoteView key={controller.item.uuid} application={this.application} controller={controller} />
return controller instanceof NoteViewController ? ( ) : (
<NoteView key={controller.item.uuid} application={this.application} controller={controller} /> <FileView
) : ( key={controller.item.uuid}
<FileView application={this.application}
key={controller.item.uuid} viewControllerManager={this.viewControllerManager}
application={this.application} file={controller.item}
viewControllerManager={this.viewControllerManager} />
file={controller.item} )
/> })}
) </>
})} )}
</> </ResponsivePaneContent>
)}
</div> </div>
) )
} }

View File

@@ -17,7 +17,7 @@ const NoteTagsContainer = ({ viewControllerManager }: Props) => {
return ( return (
<div <div
className="-mt-1 -mr-2 flex min-w-80 flex-wrap bg-transparent" className="flex min-w-80 flex-wrap bg-transparent md:-mr-2"
style={{ style={{
maxWidth: tagsContainerMaxWidth, maxWidth: tagsContainerMaxWidth,
}} }}

View File

@@ -908,11 +908,8 @@ class NoteView extends PureComponent<NoteViewProps, State> {
)} )}
{this.note && ( {this.note && (
<div <div id="editor-title-bar" className="content-title-bar section-title-bar z-editor-title-bar w-full">
id="editor-title-bar" <div className="mb-2 flex flex-wrap items-center justify-between gap-2 md:mb-0 md:flex-nowrap md:gap-0">
className="content-title-bar section-title-bar section-title-bar z-editor-title-bar w-full"
>
<div className="flex h-8 items-center justify-between">
<div className={(this.state.noteLocked ? 'locked' : '') + ' flex-grow'}> <div className={(this.state.noteLocked ? 'locked' : '') + ' flex-grow'}>
<div className="title overflow-auto"> <div className="title overflow-auto">
<input <input
@@ -930,22 +927,26 @@ class NoteView extends PureComponent<NoteViewProps, State> {
/> />
</div> </div>
</div> </div>
<div className="flex items-center"> <div className="flex flex-col flex-wrap items-start gap-3 md:flex-row md:flex-nowrap md:items-center">
<div id="save-status-container"> {this.state.noteStatus?.message?.length && (
<div id="save-status"> <div id="save-status-container">
<div <div id="save-status">
className={ <div
(this.state.syncTakingTooLong ? 'font-bold text-warning ' : '') + className={
(this.state.saveError ? 'font-bold text-danger ' : '') + (this.state.syncTakingTooLong ? 'font-bold text-warning ' : '') +
'message text-xs' (this.state.saveError ? 'font-bold text-danger ' : '') +
} 'message text-xs'
> }
{this.state.noteStatus?.message} >
{this.state.noteStatus?.message}
</div>
{this.state.noteStatus?.desc && (
<div className="desc text-xs">{this.state.noteStatus.desc}</div>
)}
</div> </div>
{this.state.noteStatus?.desc && <div className="desc text-xs">{this.state.noteStatus.desc}</div>}
</div> </div>
</div> )}
<div className="mr-3"> <div className="flex items-center gap-3">
<AttachedFilesButton <AttachedFilesButton
application={this.application} application={this.application}
onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction} onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction}
@@ -956,28 +957,24 @@ class NoteView extends PureComponent<NoteViewProps, State> {
notesController={this.viewControllerManager.notesController} notesController={this.viewControllerManager.notesController}
selectionController={this.viewControllerManager.selectionController} selectionController={this.viewControllerManager.selectionController}
/> />
</div>
<div className="mr-3">
<ChangeEditorButton <ChangeEditorButton
application={this.application} application={this.application}
viewControllerManager={this.viewControllerManager} viewControllerManager={this.viewControllerManager}
onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction} onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction}
/> />
</div>
<div className="mr-3">
<PinNoteButton <PinNoteButton
notesController={this.viewControllerManager.notesController} notesController={this.viewControllerManager.notesController}
onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction} onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction}
/> />
<NotesOptionsPanel
application={this.application}
navigationController={this.viewControllerManager.navigationController}
notesController={this.viewControllerManager.notesController}
noteTagsController={this.viewControllerManager.noteTagsController}
historyModalController={this.viewControllerManager.historyModalController}
onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction}
/>
</div> </div>
<NotesOptionsPanel
application={this.application}
navigationController={this.viewControllerManager.navigationController}
notesController={this.viewControllerManager.notesController}
noteTagsController={this.viewControllerManager.noteTagsController}
historyModalController={this.viewControllerManager.historyModalController}
onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction}
/>
</div> </div>
</div> </div>
<NoteTagsContainer viewControllerManager={this.viewControllerManager} /> <NoteTagsContainer viewControllerManager={this.viewControllerManager} />

View File

@@ -1,6 +1,6 @@
import { Component, createRef, MouseEventHandler } from 'react' import { Component, createRef, MouseEventHandler } from 'react'
import { debounce } from '@/Utils' import { debounce } from '@/Utils'
import styled from 'styled-components' import { classNames } from '@/Utils/ConcatenateClassNames'
export type ResizeFinishCallback = ( export type ResizeFinishCallback = (
lastWidth: number, lastWidth: number,
@@ -39,50 +39,6 @@ type State = {
pressed: boolean pressed: boolean
} }
const StyledPanelResizer = styled.div<{
hoverable?: boolean
alwaysVisible?: boolean
pressed: boolean
collapsed: boolean
}>`
background-color: var(--panel-resizer-background-color);
border-bottom: none;
border-top: none;
cursor: col-resize;
height: 100%;
opacity: 0;
position: absolute;
right: 0;
top: 0;
width: 4px;
z-index: var(--z-index-panel-resizer);
@keyframes fade {
0% {
opacity: 0;
}
50% {
opacity: 1;
}
100% {
opacity: 0;
}
}
&.left {
left: 0;
right: none;
}
${(props) => (props.alwaysVisible || props.collapsed || props.pressed) && 'opacity: 1;'}
&:hover {
${(props) => props.hoverable && 'opacity: 1;'}
}
`
class PanelResizer extends Component<Props, State> { class PanelResizer extends Component<Props, State> {
private overlay?: HTMLDivElement private overlay?: HTMLDivElement
private resizerElementRef = createRef<HTMLDivElement>() private resizerElementRef = createRef<HTMLDivElement>()
@@ -351,13 +307,14 @@ class PanelResizer extends Component<Props, State> {
override render() { override render() {
return ( return (
<StyledPanelResizer <div
hoverable={this.props.hoverable} className={classNames(
alwaysVisible={this.props.alwaysVisible} 'absolute right-0 top-0 z-panel-resizer',
pressed={this.state.pressed} 'hidden h-full w-[4px] cursor-col-resize border-y-0 bg-[color:var(--panel-resizer-background-color)] md:block',
collapsed={this.state.collapsed} this.props.alwaysVisible || this.state.collapsed || this.state.pressed ? ' opacity-100' : 'opacity-0',
className={`panel-resizer ${this.props.side}`} this.props.hoverable && 'hover:opacity-100',
onMouseDown={this.onMouseDown} this.props.side === PanelSide.Left && 'left-0 right-auto',
)}
ref={this.resizerElementRef} ref={this.resizerElementRef}
/> />
) )

View File

@@ -0,0 +1,19 @@
import { IconType } from '@standardnotes/snjs'
export enum AppPaneId {
Navigation = 'NavigationColumn',
Items = 'ItemsColumn',
Editor = 'EditorColumn',
}
export const AppPaneTitles = {
[AppPaneId.Navigation]: 'Navigation',
[AppPaneId.Items]: 'Notes & Files',
[AppPaneId.Editor]: 'Editor',
}
export const AppPaneIcons: Record<AppPaneId, IconType> = {
[AppPaneId.Navigation]: 'hashtag',
[AppPaneId.Items]: 'notes',
[AppPaneId.Editor]: 'plain-text',
}

View File

@@ -0,0 +1,44 @@
import { useMemo, ReactNode } from 'react'
import Icon from '@/Components/Icon/Icon'
import { AppPaneIcons, AppPaneId, AppPaneTitles } from './AppPaneMetadata'
import { classNames } from '@/Utils/ConcatenateClassNames'
import { useResponsiveAppPane } from './ResponsivePaneProvider'
type Props = {
children: ReactNode
contentClassName?: string
contentElementId?: string
paneId: AppPaneId
}
const ResponsivePaneContent = ({ children, contentClassName, contentElementId, paneId }: Props) => {
const { selectedPane, toggleAppPane: togglePane } = useResponsiveAppPane()
const isSelectedPane = useMemo(() => selectedPane === paneId, [paneId, selectedPane])
return (
<>
<button
className={classNames(
'flex w-full items-center justify-between border-b border-solid border-border px-4 py-2 focus:shadow-none focus:outline-none md:hidden',
isSelectedPane ? 'bg-contrast' : 'bg-default',
)}
onClick={() => togglePane(paneId)}
>
<div className="flex items-center gap-2 font-semibold">
<Icon type={AppPaneIcons[paneId]} />
<span>{AppPaneTitles[paneId]}</span>
</div>
<Icon type="chevron-down" />
</button>
<div
id={contentElementId}
className={classNames('content', isSelectedPane ? 'h-full' : 'hidden flex-col md:flex', contentClassName)}
>
{children}
</div>
</>
)
}
export default ResponsivePaneContent

View File

@@ -0,0 +1,64 @@
import { ElementIds } from '@/Constants/ElementIDs'
import { useEffect, ReactNode, useMemo, createContext, useCallback, useContext, useState } from 'react'
import { AppPaneId } from './AppPaneMetadata'
type ResponsivePaneData = {
selectedPane: AppPaneId
toggleAppPane: (paneId: AppPaneId) => void
}
const ResponsivePaneContext = createContext<ResponsivePaneData | undefined>(undefined)
export const useResponsiveAppPane = () => {
const value = useContext(ResponsivePaneContext)
if (!value) {
throw new Error('Component must be a child of <ResponsivePaneProvider />')
}
return value
}
type Props = {
children: ReactNode
}
const ResponsivePaneProvider = ({ children }: Props) => {
const [currentSelectedPane, setCurrentSelectedPane] = useState<AppPaneId>(AppPaneId.Editor)
const [previousSelectedPane, setPreviousSelectedPane] = useState<AppPaneId>(AppPaneId.Editor)
const toggleAppPane = useCallback(
(paneId: AppPaneId) => {
if (paneId === currentSelectedPane) {
setCurrentSelectedPane(previousSelectedPane ? previousSelectedPane : AppPaneId.Editor)
setPreviousSelectedPane(paneId)
} else {
setPreviousSelectedPane(currentSelectedPane)
setCurrentSelectedPane(paneId)
}
},
[currentSelectedPane, previousSelectedPane],
)
useEffect(() => {
if (previousSelectedPane) {
const previousPaneElement = document.getElementById(ElementIds[previousSelectedPane])
previousPaneElement?.classList.remove('selected')
}
const currentPaneElement = document.getElementById(ElementIds[currentSelectedPane])
currentPaneElement?.classList.add('selected')
}, [currentSelectedPane, previousSelectedPane])
const contextValue = useMemo(
() => ({
selectedPane: currentSelectedPane,
toggleAppPane,
}),
[currentSelectedPane, toggleAppPane],
)
return <ResponsivePaneContext.Provider value={contextValue}>{children}</ResponsivePaneContext.Provider>
}
export default ResponsivePaneProvider

View File

@@ -13,6 +13,8 @@ import {
useRef, useRef,
useState, useState,
} from 'react' } from 'react'
import { AppPaneId } from '../ResponsivePane/AppPaneMetadata'
import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider'
type Props = { type Props = {
view: SmartView view: SmartView
@@ -44,6 +46,8 @@ const smartViewIconType = (view: SmartView, isSelected: boolean): IconType => {
} }
const SmartViewsListItem: FunctionComponent<Props> = ({ view, tagsState }) => { const SmartViewsListItem: FunctionComponent<Props> = ({ view, tagsState }) => {
const { toggleAppPane } = useResponsiveAppPane()
const [title, setTitle] = useState(view.title || '') const [title, setTitle] = useState(view.title || '')
const inputRef = useRef<HTMLInputElement>(null) const inputRef = useRef<HTMLInputElement>(null)
@@ -55,9 +59,10 @@ const SmartViewsListItem: FunctionComponent<Props> = ({ view, tagsState }) => {
setTitle(view.title || '') setTitle(view.title || '')
}, [setTitle, view]) }, [setTitle, view])
const selectCurrentTag = useCallback(() => { const selectCurrentTag = useCallback(async () => {
void tagsState.setSelectedTag(view) await tagsState.setSelectedTag(view)
}, [tagsState, view]) toggleAppPane(AppPaneId.Items)
}, [tagsState, toggleAppPane, view])
const onBlur = useCallback(() => { const onBlur = useCallback(() => {
tagsState.save(view, title).catch(console.error) tagsState.save(view, title).catch(console.error)

View File

@@ -20,6 +20,8 @@ import {
} from 'react' } from 'react'
import { useDrag, useDrop } from 'react-dnd' import { useDrag, useDrop } from 'react-dnd'
import { DropItem, DropProps, ItemTypes } from './DragNDrop' import { DropItem, DropProps, ItemTypes } from './DragNDrop'
import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider'
import { AppPaneId } from '../ResponsivePane/AppPaneMetadata'
type Props = { type Props = {
tag: SNTag tag: SNTag
@@ -33,6 +35,8 @@ const PADDING_BASE_PX = 14
const PADDING_PER_LEVEL_PX = 21 const PADDING_PER_LEVEL_PX = 21
export const TagsListItem: FunctionComponent<Props> = observer(({ tag, features, tagsState, level, onContextMenu }) => { export const TagsListItem: FunctionComponent<Props> = observer(({ tag, features, tagsState, level, onContextMenu }) => {
const { toggleAppPane } = useResponsiveAppPane()
const [title, setTitle] = useState(tag.title || '') const [title, setTitle] = useState(tag.title || '')
const [subtagTitle, setSubtagTitle] = useState('') const [subtagTitle, setSubtagTitle] = useState('')
const inputRef = useRef<HTMLInputElement>(null) const inputRef = useRef<HTMLInputElement>(null)
@@ -77,9 +81,10 @@ export const TagsListItem: FunctionComponent<Props> = observer(({ tag, features,
[setShowChildren, tag, tagsState], [setShowChildren, tag, tagsState],
) )
const selectCurrentTag = useCallback(() => { const selectCurrentTag = useCallback(async () => {
void tagsState.setSelectedTag(tag) await tagsState.setSelectedTag(tag)
}, [tagsState, tag]) toggleAppPane(AppPaneId.Items)
}, [tagsState, tag, toggleAppPane])
const onBlur = useCallback(() => { const onBlur = useCallback(() => {
tagsState.save(tag, title).catch(console.error) tagsState.save(tag, title).catch(console.error)

View File

@@ -1,9 +1,11 @@
export const ElementIds = { export const ElementIds = {
ContentList: 'notes-scrollable',
EditorColumn: 'editor-column',
EditorContent: 'editor-content',
FileTextPreview: 'file-text-preview',
FileTitleEditor: 'file-title-editor',
ItemsColumn: 'items-column',
NavigationColumn: 'navigation',
NoteTextEditor: 'note-text-editor', NoteTextEditor: 'note-text-editor',
NoteTitleEditor: 'note-title-editor', NoteTitleEditor: 'note-title-editor',
FileTitleEditor: 'file-title-editor',
FileTextPreview: 'file-text-preview',
EditorContent: 'editor-content',
EditorColumn: 'editor-column',
ContentList: 'notes-scrollable',
} }

View File

@@ -0,0 +1,3 @@
export const classNames = (...values: (string | boolean | undefined)[]): string => {
return values.map((value) => (typeof value === 'string' ? value : null)).join(' ')
}

View File

@@ -1,16 +1,50 @@
.app-column-container { .app-column-container {
display: grid; display: flex;
grid-template-columns: auto auto 2fr; flex-direction: column;
@media screen and (min-width: 768px) {
display: grid;
grid-template-rows: auto;
grid-template-columns: auto auto 2fr;
}
} }
.app-column-first { .app-column-first {
width: 220px; width: 220px;
@media screen and (max-width: 768px) {
width: 100% !important;
}
} }
.app-column-second { .app-column-second {
width: 350px; width: 350px;
@media screen and (max-width: 768px) {
width: 100% !important;
}
} }
.app-column { .app-column {
overflow: hidden; overflow: hidden;
.content {
height: 100%;
}
@media screen and (max-width: 768px) {
&.selected {
flex-grow: 1;
.content {
height: 100%;
overflow-y: auto;
}
}
&:not(.selected) {
height: auto;
border-bottom: 1px solid var(--sn-stylekit-border-color);
}
}
} }

View File

@@ -74,10 +74,13 @@ $heading-height: 75px;
font-size: calc(var(--sn-stylekit-base-font-size) - 2px); font-size: calc(var(--sn-stylekit-base-font-size) - 2px);
text-transform: none; text-transform: none;
font-weight: normal; font-weight: normal;
text-align: right;
@media screen and (min-width: 768px) {
text-align: right;
}
.desc, .desc,
.message:not(.warning):not(.danger) { .message:not(.warning):not(.text-danger) {
opacity: 0.35; opacity: 0.35;
} }
} }

View File

@@ -2,11 +2,14 @@
#items-column { #items-column {
background-color: var(--items-column-background-color); background-color: var(--items-column-background-color);
border-left: 1px solid var(--items-column-border-left-color);
border-right: 1px solid var(--items-column-border-right-color);
font-size: var(--sn-stylekit-font-size-h2); font-size: var(--sn-stylekit-font-size-h2);
user-select: none; user-select: none;
@media screen and (min-width: 768px) {
border-left: 1px solid var(--items-column-border-left-color);
border-right: 1px solid var(--items-column-border-right-color);
}
-moz-user-select: none; -moz-user-select: none;
-khtml-user-select: none; -khtml-user-select: none;
-webkit-user-select: none; -webkit-user-select: none;
@@ -20,11 +23,6 @@
font-size: var(--sn-stylekit-font-size-h3); font-size: var(--sn-stylekit-font-size-h3);
} }
.content {
display: flex;
flex-direction: column;
}
#items-title-bar-container { #items-title-bar-container {
padding: 0.8125rem; padding: 0.8125rem;
} }

View File

@@ -12,11 +12,11 @@ $content-horizontal-padding: 16px;
-moz-user-select: none; -moz-user-select: none;
-khtml-user-select: none; -khtml-user-select: none;
-webkit-user-select: none; -webkit-user-select: none;
display: flex;
flex-direction: column;
&, &,
#navigation-content { #navigation-content {
display: flex;
flex-direction: column;
background-color: var(--navigation-column-background-color); background-color: var(--navigation-column-background-color);
} }