feat: initial implementation of responsive app panes (#1198)
This commit is contained in:
@@ -25,6 +25,7 @@ import PermissionsModalWrapper from '@/Components/PermissionsModal/PermissionsMo
|
||||
import { PanelResizedData } from '@/Types/PanelResizedData'
|
||||
import TagContextMenuWrapper from '@/Components/Tags/TagContextMenuWrapper'
|
||||
import FileDragNDropProvider from '../FileDragNDropProvider/FileDragNDropProvider'
|
||||
import ResponsivePaneProvider from '../ResponsivePane/ResponsivePaneProvider'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
@@ -182,19 +183,21 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
|
||||
featuresController={viewControllerManager.featuresController}
|
||||
filesController={viewControllerManager.filesController}
|
||||
>
|
||||
<Navigation application={application} />
|
||||
<ContentListView
|
||||
application={application}
|
||||
accountMenuController={viewControllerManager.accountMenuController}
|
||||
filesController={viewControllerManager.filesController}
|
||||
itemListController={viewControllerManager.itemListController}
|
||||
navigationController={viewControllerManager.navigationController}
|
||||
noAccountWarningController={viewControllerManager.noAccountWarningController}
|
||||
noteTagsController={viewControllerManager.noteTagsController}
|
||||
notesController={viewControllerManager.notesController}
|
||||
selectionController={viewControllerManager.selectionController}
|
||||
/>
|
||||
<NoteGroupView application={application} />
|
||||
<ResponsivePaneProvider>
|
||||
<Navigation application={application} />
|
||||
<ContentListView
|
||||
application={application}
|
||||
accountMenuController={viewControllerManager.accountMenuController}
|
||||
filesController={viewControllerManager.filesController}
|
||||
itemListController={viewControllerManager.itemListController}
|
||||
navigationController={viewControllerManager.navigationController}
|
||||
noAccountWarningController={viewControllerManager.noAccountWarningController}
|
||||
noteTagsController={viewControllerManager.noteTagsController}
|
||||
notesController={viewControllerManager.notesController}
|
||||
selectionController={viewControllerManager.selectionController}
|
||||
/>
|
||||
<NoteGroupView application={application} />
|
||||
</ResponsivePaneProvider>
|
||||
</FileDragNDropProvider>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@ import { NotesController } from '@/Controllers/NotesController'
|
||||
import { AccountMenuController } from '@/Controllers/AccountMenu/AccountMenuController'
|
||||
import { ElementIds } from '@/Constants/ElementIDs'
|
||||
import ContentListHeader from './Header/ContentListHeader'
|
||||
import ResponsivePaneContent from '../ResponsivePane/ResponsivePaneContent'
|
||||
import { AppPaneId } from '../ResponsivePane/AppPaneMetadata'
|
||||
|
||||
type Props = {
|
||||
accountMenuController: AccountMenuController
|
||||
@@ -168,11 +170,11 @@ const ContentListView: FunctionComponent<Props> = ({
|
||||
return (
|
||||
<div
|
||||
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'}
|
||||
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-container">
|
||||
<ContentListHeader
|
||||
@@ -204,7 +206,7 @@ const ContentListView: FunctionComponent<Props> = ({
|
||||
selectionController={selectionController}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</ResponsivePaneContent>
|
||||
{itemsViewPanelRef.current && (
|
||||
<PanelResizer
|
||||
collapsable={true}
|
||||
|
||||
@@ -7,6 +7,8 @@ import ListItemFlagIcons from './ListItemFlagIcons'
|
||||
import ListItemTags from './ListItemTags'
|
||||
import ListItemMetadata from './ListItemMetadata'
|
||||
import { DisplayableListItemProps } from './Types/DisplayableListItemProps'
|
||||
import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider'
|
||||
import { AppPaneId } from '../ResponsivePane/AppPaneMetadata'
|
||||
|
||||
const FileListItem: FunctionComponent<DisplayableListItemProps> = ({
|
||||
application,
|
||||
@@ -20,6 +22,8 @@ const FileListItem: FunctionComponent<DisplayableListItemProps> = ({
|
||||
sortBy,
|
||||
tags,
|
||||
}) => {
|
||||
const { toggleAppPane } = useResponsiveAppPane()
|
||||
|
||||
const openFileContextMenu = useCallback(
|
||||
(posX: number, posY: number) => {
|
||||
filesController.setFileContextMenuLocation({
|
||||
@@ -41,9 +45,12 @@ const FileListItem: FunctionComponent<DisplayableListItemProps> = ({
|
||||
[selectionController, item.uuid, openFileContextMenu],
|
||||
)
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
void selectionController.selectItem(item.uuid, true)
|
||||
}, [item.uuid, selectionController])
|
||||
const onClick = useCallback(async () => {
|
||||
const { didSelect } = await selectionController.selectItem(item.uuid, true)
|
||||
if (didSelect) {
|
||||
toggleAppPane(AppPaneId.Editor)
|
||||
}
|
||||
}, [item.uuid, selectionController, toggleAppPane])
|
||||
|
||||
const IconComponent = () =>
|
||||
getFileIconComponent(
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { PLAIN_EDITOR_NAME } from '@/Constants/Constants'
|
||||
import { sanitizeHtmlString, SNNote } from '@standardnotes/snjs'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent } from 'react'
|
||||
import { FunctionComponent, useCallback } from 'react'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import ListItemConflictIndicator from './ListItemConflictIndicator'
|
||||
import ListItemFlagIcons from './ListItemFlagIcons'
|
||||
import ListItemTags from './ListItemTags'
|
||||
import ListItemMetadata from './ListItemMetadata'
|
||||
import { DisplayableListItemProps } from './Types/DisplayableListItemProps'
|
||||
import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider'
|
||||
import { AppPaneId } from '../ResponsivePane/AppPaneMetadata'
|
||||
|
||||
const NoteListItem: FunctionComponent<DisplayableListItemProps> = ({
|
||||
application,
|
||||
@@ -22,6 +24,8 @@ const NoteListItem: FunctionComponent<DisplayableListItemProps> = ({
|
||||
sortBy,
|
||||
tags,
|
||||
}) => {
|
||||
const { toggleAppPane } = useResponsiveAppPane()
|
||||
|
||||
const editorForNote = application.componentManager.editorForNote(item as SNNote)
|
||||
const editorName = editorForNote?.name ?? PLAIN_EDITOR_NAME
|
||||
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 (
|
||||
<div
|
||||
className={`content-list-item flex w-full cursor-pointer items-stretch text-text ${
|
||||
selected && 'selected border-l-2 border-solid border-info'
|
||||
}`}
|
||||
id={item.uuid}
|
||||
onClick={() => {
|
||||
void selectionController.selectItem(item.uuid, true)
|
||||
}}
|
||||
onClick={onClick}
|
||||
onContextMenu={(event) => {
|
||||
event.preventDefault()
|
||||
void openContextMenu(event.clientX, event.clientY)
|
||||
|
||||
@@ -4,9 +4,11 @@ import { WebApplication } from '@/Application/Application'
|
||||
import { PANEL_NAME_NAVIGATION } from '@/Constants/Constants'
|
||||
import { ApplicationEvent, PrefKey } from '@standardnotes/snjs'
|
||||
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 SearchBar from '@/Components/SearchBar/SearchBar'
|
||||
import ResponsivePaneContent from '@/Components/ResponsivePane/ResponsivePaneContent'
|
||||
import { AppPaneId } from '@/Components/ResponsivePane/AppPaneMetadata'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
@@ -14,7 +16,7 @@ type Props = {
|
||||
|
||||
const Navigation: FunctionComponent<Props> = ({ application }) => {
|
||||
const viewControllerManager = useMemo(() => application.getViewControllerManager(), [application])
|
||||
const [ref, setRef] = useState<HTMLDivElement | null>()
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
const [panelWidth, setPanelWidth] = useState<number>(0)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -44,13 +46,8 @@ const Navigation: FunctionComponent<Props> = ({ application }) => {
|
||||
}, [viewControllerManager])
|
||||
|
||||
return (
|
||||
<div
|
||||
id="navigation"
|
||||
className="sn-component section app-column app-column-first"
|
||||
data-aria-label="Navigation"
|
||||
ref={setRef}
|
||||
>
|
||||
<div id="navigation-content" className="content">
|
||||
<div id="navigation" className="sn-component section app-column app-column-first" ref={ref}>
|
||||
<ResponsivePaneContent paneId={AppPaneId.Navigation} contentElementId="navigation-content">
|
||||
<SearchBar
|
||||
itemListController={viewControllerManager.itemListController}
|
||||
searchOptionsController={viewControllerManager.searchOptionsController}
|
||||
@@ -66,12 +63,12 @@ const Navigation: FunctionComponent<Props> = ({ application }) => {
|
||||
<SmartViewsSection viewControllerManager={viewControllerManager} />
|
||||
<TagsSection viewControllerManager={viewControllerManager} />
|
||||
</div>
|
||||
</div>
|
||||
{ref && (
|
||||
</ResponsivePaneContent>
|
||||
{ref.current && (
|
||||
<PanelResizer
|
||||
collapsable={true}
|
||||
defaultWidth={150}
|
||||
panel={ref}
|
||||
panel={ref.current}
|
||||
hoverable={true}
|
||||
side={PanelSide.Right}
|
||||
type={PanelResizeType.WidthOnly}
|
||||
|
||||
@@ -7,6 +7,8 @@ import MultipleSelectedFiles from '../MultipleSelectedFiles/MultipleSelectedFile
|
||||
import { ElementIds } from '@/Constants/ElementIDs'
|
||||
import FileView from '@/Components/FileView/FileView'
|
||||
import { FileDnDContext } from '@/Components/FileDragNDropProvider/FileDragNDropProvider'
|
||||
import { AppPaneId } from '../ResponsivePane/AppPaneMetadata'
|
||||
import ResponsivePaneContent from '../ResponsivePane/ResponsivePaneContent'
|
||||
|
||||
type State = {
|
||||
showMultipleSelectedNotes: boolean
|
||||
@@ -88,49 +90,48 @@ class NoteGroupView extends PureComponent<Props, State> {
|
||||
|
||||
return (
|
||||
<div id={ElementIds.EditorColumn} className="app-column app-column-third h-full">
|
||||
{this.state.showMultipleSelectedNotes && (
|
||||
<MultipleSelectedNotes
|
||||
application={this.application}
|
||||
filesController={this.viewControllerManager.filesController}
|
||||
selectionController={this.viewControllerManager.selectionController}
|
||||
featuresController={this.viewControllerManager.featuresController}
|
||||
filePreviewModalController={this.viewControllerManager.filePreviewModalController}
|
||||
navigationController={this.viewControllerManager.navigationController}
|
||||
notesController={this.viewControllerManager.notesController}
|
||||
noteTagsController={this.viewControllerManager.noteTagsController}
|
||||
historyModalController={this.viewControllerManager.historyModalController}
|
||||
/>
|
||||
)}
|
||||
|
||||
{this.state.showMultipleSelectedFiles && (
|
||||
<MultipleSelectedFiles
|
||||
filesController={this.viewControllerManager.filesController}
|
||||
selectionController={this.viewControllerManager.selectionController}
|
||||
/>
|
||||
)}
|
||||
|
||||
{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">
|
||||
Drop your files to upload them
|
||||
</div>
|
||||
)}
|
||||
|
||||
{shouldNotShowMultipleSelectedItems && this.state.controllers.length > 0 && (
|
||||
<>
|
||||
{this.state.controllers.map((controller) => {
|
||||
return controller instanceof NoteViewController ? (
|
||||
<NoteView key={controller.item.uuid} application={this.application} controller={controller} />
|
||||
) : (
|
||||
<FileView
|
||||
key={controller.item.uuid}
|
||||
application={this.application}
|
||||
viewControllerManager={this.viewControllerManager}
|
||||
file={controller.item}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
<ResponsivePaneContent paneId={AppPaneId.Editor}>
|
||||
{this.state.showMultipleSelectedNotes && (
|
||||
<MultipleSelectedNotes
|
||||
application={this.application}
|
||||
filesController={this.viewControllerManager.filesController}
|
||||
selectionController={this.viewControllerManager.selectionController}
|
||||
featuresController={this.viewControllerManager.featuresController}
|
||||
filePreviewModalController={this.viewControllerManager.filePreviewModalController}
|
||||
navigationController={this.viewControllerManager.navigationController}
|
||||
notesController={this.viewControllerManager.notesController}
|
||||
noteTagsController={this.viewControllerManager.noteTagsController}
|
||||
historyModalController={this.viewControllerManager.historyModalController}
|
||||
/>
|
||||
)}
|
||||
{this.state.showMultipleSelectedFiles && (
|
||||
<MultipleSelectedFiles
|
||||
filesController={this.viewControllerManager.filesController}
|
||||
selectionController={this.viewControllerManager.selectionController}
|
||||
/>
|
||||
)}
|
||||
{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">
|
||||
Drop your files to upload them
|
||||
</div>
|
||||
)}
|
||||
{shouldNotShowMultipleSelectedItems && this.state.controllers.length > 0 && (
|
||||
<>
|
||||
{this.state.controllers.map((controller) => {
|
||||
return controller instanceof NoteViewController ? (
|
||||
<NoteView key={controller.item.uuid} application={this.application} controller={controller} />
|
||||
) : (
|
||||
<FileView
|
||||
key={controller.item.uuid}
|
||||
application={this.application}
|
||||
viewControllerManager={this.viewControllerManager}
|
||||
file={controller.item}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</ResponsivePaneContent>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ const NoteTagsContainer = ({ viewControllerManager }: Props) => {
|
||||
|
||||
return (
|
||||
<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={{
|
||||
maxWidth: tagsContainerMaxWidth,
|
||||
}}
|
||||
|
||||
@@ -908,11 +908,8 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
||||
)}
|
||||
|
||||
{this.note && (
|
||||
<div
|
||||
id="editor-title-bar"
|
||||
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 id="editor-title-bar" className="content-title-bar section-title-bar z-editor-title-bar w-full">
|
||||
<div className="mb-2 flex flex-wrap items-center justify-between gap-2 md:mb-0 md:flex-nowrap md:gap-0">
|
||||
<div className={(this.state.noteLocked ? 'locked' : '') + ' flex-grow'}>
|
||||
<div className="title overflow-auto">
|
||||
<input
|
||||
@@ -930,22 +927,26 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<div id="save-status-container">
|
||||
<div id="save-status">
|
||||
<div
|
||||
className={
|
||||
(this.state.syncTakingTooLong ? 'font-bold text-warning ' : '') +
|
||||
(this.state.saveError ? 'font-bold text-danger ' : '') +
|
||||
'message text-xs'
|
||||
}
|
||||
>
|
||||
{this.state.noteStatus?.message}
|
||||
<div className="flex flex-col flex-wrap items-start gap-3 md:flex-row md:flex-nowrap md:items-center">
|
||||
{this.state.noteStatus?.message?.length && (
|
||||
<div id="save-status-container">
|
||||
<div id="save-status">
|
||||
<div
|
||||
className={
|
||||
(this.state.syncTakingTooLong ? 'font-bold text-warning ' : '') +
|
||||
(this.state.saveError ? 'font-bold text-danger ' : '') +
|
||||
'message text-xs'
|
||||
}
|
||||
>
|
||||
{this.state.noteStatus?.message}
|
||||
</div>
|
||||
{this.state.noteStatus?.desc && (
|
||||
<div className="desc text-xs">{this.state.noteStatus.desc}</div>
|
||||
)}
|
||||
</div>
|
||||
{this.state.noteStatus?.desc && <div className="desc text-xs">{this.state.noteStatus.desc}</div>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mr-3">
|
||||
)}
|
||||
<div className="flex items-center gap-3">
|
||||
<AttachedFilesButton
|
||||
application={this.application}
|
||||
onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction}
|
||||
@@ -956,28 +957,24 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
||||
notesController={this.viewControllerManager.notesController}
|
||||
selectionController={this.viewControllerManager.selectionController}
|
||||
/>
|
||||
</div>
|
||||
<div className="mr-3">
|
||||
<ChangeEditorButton
|
||||
application={this.application}
|
||||
viewControllerManager={this.viewControllerManager}
|
||||
onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction}
|
||||
/>
|
||||
</div>
|
||||
<div className="mr-3">
|
||||
<PinNoteButton
|
||||
notesController={this.viewControllerManager.notesController}
|
||||
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>
|
||||
<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>
|
||||
<NoteTagsContainer viewControllerManager={this.viewControllerManager} />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Component, createRef, MouseEventHandler } from 'react'
|
||||
import { debounce } from '@/Utils'
|
||||
import styled from 'styled-components'
|
||||
import { classNames } from '@/Utils/ConcatenateClassNames'
|
||||
|
||||
export type ResizeFinishCallback = (
|
||||
lastWidth: number,
|
||||
@@ -39,50 +39,6 @@ type State = {
|
||||
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> {
|
||||
private overlay?: HTMLDivElement
|
||||
private resizerElementRef = createRef<HTMLDivElement>()
|
||||
@@ -351,13 +307,14 @@ class PanelResizer extends Component<Props, State> {
|
||||
|
||||
override render() {
|
||||
return (
|
||||
<StyledPanelResizer
|
||||
hoverable={this.props.hoverable}
|
||||
alwaysVisible={this.props.alwaysVisible}
|
||||
pressed={this.state.pressed}
|
||||
collapsed={this.state.collapsed}
|
||||
className={`panel-resizer ${this.props.side}`}
|
||||
onMouseDown={this.onMouseDown}
|
||||
<div
|
||||
className={classNames(
|
||||
'absolute right-0 top-0 z-panel-resizer',
|
||||
'hidden h-full w-[4px] cursor-col-resize border-y-0 bg-[color:var(--panel-resizer-background-color)] md:block',
|
||||
this.props.alwaysVisible || this.state.collapsed || this.state.pressed ? ' opacity-100' : 'opacity-0',
|
||||
this.props.hoverable && 'hover:opacity-100',
|
||||
this.props.side === PanelSide.Left && 'left-0 right-auto',
|
||||
)}
|
||||
ref={this.resizerElementRef}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -13,6 +13,8 @@ import {
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { AppPaneId } from '../ResponsivePane/AppPaneMetadata'
|
||||
import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider'
|
||||
|
||||
type Props = {
|
||||
view: SmartView
|
||||
@@ -44,6 +46,8 @@ const smartViewIconType = (view: SmartView, isSelected: boolean): IconType => {
|
||||
}
|
||||
|
||||
const SmartViewsListItem: FunctionComponent<Props> = ({ view, tagsState }) => {
|
||||
const { toggleAppPane } = useResponsiveAppPane()
|
||||
|
||||
const [title, setTitle] = useState(view.title || '')
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
@@ -55,9 +59,10 @@ const SmartViewsListItem: FunctionComponent<Props> = ({ view, tagsState }) => {
|
||||
setTitle(view.title || '')
|
||||
}, [setTitle, view])
|
||||
|
||||
const selectCurrentTag = useCallback(() => {
|
||||
void tagsState.setSelectedTag(view)
|
||||
}, [tagsState, view])
|
||||
const selectCurrentTag = useCallback(async () => {
|
||||
await tagsState.setSelectedTag(view)
|
||||
toggleAppPane(AppPaneId.Items)
|
||||
}, [tagsState, toggleAppPane, view])
|
||||
|
||||
const onBlur = useCallback(() => {
|
||||
tagsState.save(view, title).catch(console.error)
|
||||
|
||||
@@ -20,6 +20,8 @@ import {
|
||||
} from 'react'
|
||||
import { useDrag, useDrop } from 'react-dnd'
|
||||
import { DropItem, DropProps, ItemTypes } from './DragNDrop'
|
||||
import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider'
|
||||
import { AppPaneId } from '../ResponsivePane/AppPaneMetadata'
|
||||
|
||||
type Props = {
|
||||
tag: SNTag
|
||||
@@ -33,6 +35,8 @@ const PADDING_BASE_PX = 14
|
||||
const PADDING_PER_LEVEL_PX = 21
|
||||
|
||||
export const TagsListItem: FunctionComponent<Props> = observer(({ tag, features, tagsState, level, onContextMenu }) => {
|
||||
const { toggleAppPane } = useResponsiveAppPane()
|
||||
|
||||
const [title, setTitle] = useState(tag.title || '')
|
||||
const [subtagTitle, setSubtagTitle] = useState('')
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
@@ -77,9 +81,10 @@ export const TagsListItem: FunctionComponent<Props> = observer(({ tag, features,
|
||||
[setShowChildren, tag, tagsState],
|
||||
)
|
||||
|
||||
const selectCurrentTag = useCallback(() => {
|
||||
void tagsState.setSelectedTag(tag)
|
||||
}, [tagsState, tag])
|
||||
const selectCurrentTag = useCallback(async () => {
|
||||
await tagsState.setSelectedTag(tag)
|
||||
toggleAppPane(AppPaneId.Items)
|
||||
}, [tagsState, tag, toggleAppPane])
|
||||
|
||||
const onBlur = useCallback(() => {
|
||||
tagsState.save(tag, title).catch(console.error)
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
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',
|
||||
NoteTitleEditor: 'note-title-editor',
|
||||
FileTitleEditor: 'file-title-editor',
|
||||
FileTextPreview: 'file-text-preview',
|
||||
EditorContent: 'editor-content',
|
||||
EditorColumn: 'editor-column',
|
||||
ContentList: 'notes-scrollable',
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export const classNames = (...values: (string | boolean | undefined)[]): string => {
|
||||
return values.map((value) => (typeof value === 'string' ? value : null)).join(' ')
|
||||
}
|
||||
@@ -1,16 +1,50 @@
|
||||
.app-column-container {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto 2fr;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
display: grid;
|
||||
grid-template-rows: auto;
|
||||
grid-template-columns: auto auto 2fr;
|
||||
}
|
||||
}
|
||||
|
||||
.app-column-first {
|
||||
width: 220px;
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
.app-column-second {
|
||||
width: 350px;
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
.app-column {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,10 +74,13 @@ $heading-height: 75px;
|
||||
font-size: calc(var(--sn-stylekit-base-font-size) - 2px);
|
||||
text-transform: none;
|
||||
font-weight: normal;
|
||||
text-align: right;
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.desc,
|
||||
.message:not(.warning):not(.danger) {
|
||||
.message:not(.warning):not(.text-danger) {
|
||||
opacity: 0.35;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,14 @@
|
||||
|
||||
#items-column {
|
||||
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);
|
||||
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;
|
||||
-khtml-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
@@ -20,11 +23,6 @@
|
||||
font-size: var(--sn-stylekit-font-size-h3);
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#items-title-bar-container {
|
||||
padding: 0.8125rem;
|
||||
}
|
||||
|
||||
@@ -12,11 +12,11 @@ $content-horizontal-padding: 16px;
|
||||
-moz-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&,
|
||||
#navigation-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--navigation-column-background-color);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user