feat: tablet responsiveness (#1369)

* feat: "Notes" column responsiveness for tablets

* feat: ability to expand/collapse the navigation column, move some styles to tailwind

* feat: ability to expand/collapse the navigation column, move some styles to tailwind

* feat: icon for collapsing/expanding navigation column, style improvements

* fix: restore vertical panels resizability on large screens

* feat: tags/folders section for collapsed state

* feat: notes subsection styling in navigation section

* feat: hide rename & delete links of smart views when in collapsed mode

* fix: show notes count in bold when in collapsed mode

* fix: better padding for folders in collapsed state

* fix: smaller left padding for collapsed case

* fix: better view of expand/collapse control

* fix: hide search bar when collapsed

* fix: adjust position of "expand" control when in collapsed state

* chore: remove comments

* refactor: use `classNames` util when applying conditional css classes

* chore: shorter class names syntax

* chore: shorter syntax for conditional classes

* fix: better way of setting background color with opacity

* fix: use variable colors according to SN themes

* chore: move "colors" from "extend" section and thus override Tailwind's colors (so only SN colors will exist)

* fix: correct hover style in collapsed mode

* fix: correct upper boundary for extra small screens
This commit is contained in:
Vardan Hakobyan
2022-08-08 11:07:15 +04:00
committed by GitHub
parent 8332a70384
commit f555fbcaaa
13 changed files with 369 additions and 288 deletions

View File

@@ -172,7 +172,7 @@ const ContentListView: FunctionComponent<Props> = ({
return ( return (
<div <div
id="items-column" id="items-column"
className="sn-component section app-column app-column-second flex flex-col border-b border-solid border-border" className="sn-component section app-column flex flex-col border-b border-solid border-border xl:w-87.5 xsm-only:!w-full sm-only:!w-full md-only:!w-52 lg-only:!w-52"
aria-label={'Notes & Files'} aria-label={'Notes & Files'}
ref={itemsViewPanelRef} ref={itemsViewPanelRef}
> >

View File

@@ -10,6 +10,7 @@ import SearchBar from '@/Components/SearchBar/SearchBar'
import ResponsivePaneContent from '@/Components/ResponsivePane/ResponsivePaneContent' import ResponsivePaneContent from '@/Components/ResponsivePane/ResponsivePaneContent'
import { AppPaneId } from '@/Components/ResponsivePane/AppPaneMetadata' import { AppPaneId } from '@/Components/ResponsivePane/AppPaneMetadata'
import { classNames } from '@/Utils/ConcatenateClassNames' import { classNames } from '@/Utils/ConcatenateClassNames'
import Icon from '@/Components/Icon/Icon'
type Props = { type Props = {
application: WebApplication application: WebApplication
@@ -19,6 +20,7 @@ const Navigation: FunctionComponent<Props> = ({ application }) => {
const viewControllerManager = useMemo(() => application.getViewControllerManager(), [application]) const viewControllerManager = useMemo(() => application.getViewControllerManager(), [application])
const ref = useRef<HTMLDivElement>(null) const ref = useRef<HTMLDivElement>(null)
const [panelWidth, setPanelWidth] = useState<number>(0) const [panelWidth, setPanelWidth] = useState<number>(0)
const [isPanelExpanded, setIsPanelExpanded] = useState(true)
useEffect(() => { useEffect(() => {
const removeObserver = application.addEventObserver(async () => { const removeObserver = application.addEventObserver(async () => {
@@ -47,19 +49,39 @@ const Navigation: FunctionComponent<Props> = ({ application }) => {
}, [viewControllerManager]) }, [viewControllerManager])
return ( return (
<div id="navigation" className="sn-component section app-column app-column-first" ref={ref}> <div
id="navigation"
className={classNames(
'sn-component section app-column xl:w-[220px] xsm-only:!w-full sm-only:!w-full md-only:transition-width lg-only:transition-width',
isPanelExpanded ? 'md-only:!w-[220px] lg-only:!w-[220px]' : 'md-only:!w-18 lg-only:!w-18',
)}
ref={ref}
>
<ResponsivePaneContent paneId={AppPaneId.Navigation} contentElementId="navigation-content"> <ResponsivePaneContent paneId={AppPaneId.Navigation} contentElementId="navigation-content">
<SearchBar {isPanelExpanded && (
itemListController={viewControllerManager.itemListController} <SearchBar
searchOptionsController={viewControllerManager.searchOptionsController} itemListController={viewControllerManager.itemListController}
selectedViewTitle={viewControllerManager.navigationController.selected?.title} searchOptionsController={viewControllerManager.searchOptionsController}
/> selectedViewTitle={viewControllerManager.navigationController.selected?.title}
<div className="section-title-bar"> />
<div className="section-title-bar-header"> )}
<div className="title text-sm"> <div className={'flex justify-end'}>
<span className="font-bold">Views</span> <div className={classNames('section-title-bar block w-full xl:block', !isPanelExpanded && 'hidden')}>
<div className="section-title-bar-header">
<div className="title text-sm">
<span className="font-bold">Views</span>
</div>
</div> </div>
</div> </div>
<div
className={classNames(
'hidden items-end self-end rounded-full rounded-tr-none rounded-br-none bg-default p-1 text-foreground md:flex xl:hidden',
isPanelExpanded ? 'mb-1 w-fit' : 'mt-4.5 mb-3 ml-3 w-full',
)}
onClick={() => setIsPanelExpanded(!isPanelExpanded)}
>
<Icon type="chevron-down" className={isPanelExpanded ? 'rotate-90' : '-rotate-90'} />
</div>
</div> </div>
<div <div
className={classNames( className={classNames(
@@ -68,8 +90,8 @@ const Navigation: FunctionComponent<Props> = ({ application }) => {
'md:hover:[overflow-y:_overlay]', 'md:hover:[overflow-y:_overlay]',
)} )}
> >
<SmartViewsSection viewControllerManager={viewControllerManager} /> <SmartViewsSection viewControllerManager={viewControllerManager} isCollapsed={!isPanelExpanded} />
<TagsSection viewControllerManager={viewControllerManager} /> <TagsSection viewControllerManager={viewControllerManager} isCollapsed={!isPanelExpanded} />
</div> </div>
</ResponsivePaneContent> </ResponsivePaneContent>
{ref.current && ( {ref.current && (

View File

@@ -5,9 +5,10 @@ import SmartViewsListItem from './SmartViewsListItem'
type Props = { type Props = {
viewControllerManager: ViewControllerManager viewControllerManager: ViewControllerManager
isCollapsed: boolean
} }
const SmartViewsList: FunctionComponent<Props> = ({ viewControllerManager }: Props) => { const SmartViewsList: FunctionComponent<Props> = ({ viewControllerManager, isCollapsed }: Props) => {
const allViews = viewControllerManager.navigationController.smartViews const allViews = viewControllerManager.navigationController.smartViews
return ( return (
@@ -19,6 +20,7 @@ const SmartViewsList: FunctionComponent<Props> = ({ viewControllerManager }: Pro
view={view} view={view}
tagsState={viewControllerManager.navigationController} tagsState={viewControllerManager.navigationController}
features={viewControllerManager.featuresController} features={viewControllerManager.featuresController}
isCollapsed={isCollapsed}
/> />
) )
})} })}

View File

@@ -15,11 +15,13 @@ import {
} from 'react' } from 'react'
import { AppPaneId } from '../ResponsivePane/AppPaneMetadata' import { AppPaneId } from '../ResponsivePane/AppPaneMetadata'
import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider' import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider'
import { classNames } from '@/Utils/ConcatenateClassNames'
type Props = { type Props = {
view: SmartView view: SmartView
tagsState: NavigationController tagsState: NavigationController
features: FeaturesController features: FeaturesController
isCollapsed: boolean
} }
const PADDING_BASE_PX = 14 const PADDING_BASE_PX = 14
@@ -45,7 +47,7 @@ const smartViewIconType = (view: SmartView, isSelected: boolean): IconType => {
return 'hashtag' return 'hashtag'
} }
const SmartViewsListItem: FunctionComponent<Props> = ({ view, tagsState }) => { const SmartViewsListItem: FunctionComponent<Props> = ({ view, tagsState, isCollapsed }) => {
const { toggleAppPane } = useResponsiveAppPane() const { toggleAppPane } = useResponsiveAppPane()
const [title, setTitle] = useState(view.title || '') const [title, setTitle] = useState(view.title || '')
@@ -111,18 +113,30 @@ const SmartViewsListItem: FunctionComponent<Props> = ({ view, tagsState }) => {
return ( return (
<> <>
<div <div
className={`tag ${isSelected ? 'selected' : ''} ${isFaded ? 'opacity-50' : ''}`} className={classNames(
'tag',
isSelected && 'selected',
isFaded && 'opacity-50',
isCollapsed && '!bg-transparent',
)}
onClick={selectCurrentTag} onClick={selectCurrentTag}
style={{ style={{
paddingLeft: `${level * PADDING_PER_LEVEL_PX + PADDING_BASE_PX}px`, paddingLeft: `${level * PADDING_PER_LEVEL_PX + PADDING_BASE_PX}px`,
}} }}
> >
<div className="tag-info"> <div className="tag-info relative">
<div className={'tag-icon mr-2'}> <div
<Icon type={iconType} className={`${isSelected ? 'text-info' : 'text-neutral'}`} /> className={classNames(
isCollapsed
? `relative flex h-[40px] w-[40px] items-center justify-center
${isSelected ? 'transparent-info-color-background' : 'transparent-info-color-background-hover'}`
: 'tag-icon mr-2',
)}
>
<Icon type={iconType} className={isSelected ? 'text-info' : 'text-neutral'} />
</div> </div>
<input <input
className={`title ${isEditing ? 'editing' : ''}`} className={classNames('title', isEditing ? 'editing' : '', isCollapsed ? 'hidden' : 'block')}
disabled={!isEditing} disabled={!isEditing}
id={`react-tag-${view.uuid}`} id={`react-tag-${view.uuid}`}
onBlur={onBlur} onBlur={onBlur}
@@ -132,10 +146,12 @@ const SmartViewsListItem: FunctionComponent<Props> = ({ view, tagsState }) => {
spellCheck={false} spellCheck={false}
ref={inputRef} ref={inputRef}
/> />
<div className="count">{view.uuid === SystemViewId.AllNotes && tagsState.allNotesCount}</div> <div className={classNames('count', isCollapsed ? 'absolute top-0 right-0' : '')}>
{view.uuid === SystemViewId.AllNotes && tagsState.allNotesCount}
</div>
</div> </div>
{!isSystemView(view) && ( {!isSystemView(view) && !isCollapsed && (
<div className="meta"> <div className="meta">
{view.conflictOf && ( {view.conflictOf && (
<div className="danger text-[0.625rem] font-bold">Conflicted Copy {view.conflictOf}</div> <div className="danger text-[0.625rem] font-bold">Conflicted Copy {view.conflictOf}</div>

View File

@@ -5,12 +5,13 @@ import SmartViewsList from './SmartViewsList'
type Props = { type Props = {
viewControllerManager: ViewControllerManager viewControllerManager: ViewControllerManager
isCollapsed: boolean
} }
const SmartViewsSection: FunctionComponent<Props> = ({ viewControllerManager }) => { const SmartViewsSection: FunctionComponent<Props> = ({ viewControllerManager, isCollapsed }) => {
return ( return (
<section> <section>
<SmartViewsList viewControllerManager={viewControllerManager} /> <SmartViewsList viewControllerManager={viewControllerManager} isCollapsed={isCollapsed} />
</section> </section>
) )
} }

View File

@@ -9,9 +9,10 @@ import { TagsListItem } from './TagsListItem'
type Props = { type Props = {
viewControllerManager: ViewControllerManager viewControllerManager: ViewControllerManager
isCollapsed: boolean
} }
const TagsList: FunctionComponent<Props> = ({ viewControllerManager }: Props) => { const TagsList: FunctionComponent<Props> = ({ viewControllerManager, isCollapsed }: Props) => {
const tagsState = viewControllerManager.navigationController const tagsState = viewControllerManager.navigationController
const allTags = tagsState.allLocalRootTags const allTags = tagsState.allLocalRootTags
@@ -52,6 +53,7 @@ const TagsList: FunctionComponent<Props> = ({ viewControllerManager }: Props) =>
tagsState={tagsState} tagsState={tagsState}
features={viewControllerManager.featuresController} features={viewControllerManager.featuresController}
onContextMenu={onContextMenu} onContextMenu={onContextMenu}
isCollapsed={isCollapsed}
/> />
) )
})} })}

View File

@@ -22,6 +22,7 @@ import { useDrag, useDrop } from 'react-dnd'
import { DropItem, DropProps, ItemTypes } from './DragNDrop' import { DropItem, DropProps, ItemTypes } from './DragNDrop'
import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider' import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider'
import { AppPaneId } from '../ResponsivePane/AppPaneMetadata' import { AppPaneId } from '../ResponsivePane/AppPaneMetadata'
import { classNames } from '@/Utils/ConcatenateClassNames'
type Props = { type Props = {
tag: SNTag tag: SNTag
@@ -29,273 +30,290 @@ type Props = {
features: FeaturesController features: FeaturesController
level: number level: number
onContextMenu: (tag: SNTag, posX: number, posY: number) => void onContextMenu: (tag: SNTag, posX: number, posY: number) => void
isCollapsed: boolean
} }
const PADDING_BASE_PX = 14 const PADDING_BASE_PX = 14
const PADDING_BASE_WHEN_COLLAPSED_PX = 6
const PADDING_PER_LEVEL_PX = 21 const PADDING_PER_LEVEL_PX = 21
const PADDING_PER_LEVEL_WHEN_COLLAPSED_PX = 10
export const TagsListItem: FunctionComponent<Props> = observer(({ tag, features, tagsState, level, onContextMenu }) => { export const TagsListItem: FunctionComponent<Props> = observer(
const { toggleAppPane } = useResponsiveAppPane() ({ tag, features, tagsState, level, onContextMenu, isCollapsed }) => {
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)
const subtagInputRef = useRef<HTMLInputElement>(null) const subtagInputRef = useRef<HTMLInputElement>(null)
const menuButtonRef = useRef<HTMLAnchorElement>(null) const menuButtonRef = useRef<HTMLAnchorElement>(null)
const isSelected = tagsState.selected === tag const isSelected = tagsState.selected === tag
const isEditing = tagsState.editingTag === tag const isEditing = tagsState.editingTag === tag
const isAddingSubtag = tagsState.addingSubtagTo === tag const isAddingSubtag = tagsState.addingSubtagTo === tag
const noteCounts = computed(() => tagsState.getNotesCount(tag)) const noteCounts = computed(() => tagsState.getNotesCount(tag))
const childrenTags = computed(() => tagsState.getChildren(tag)).get() const childrenTags = computed(() => tagsState.getChildren(tag)).get()
const hasChildren = childrenTags.length > 0 const hasChildren = childrenTags.length > 0
const hasFolders = features.hasFolders const hasFolders = features.hasFolders
const hasAtLeastOneFolder = tagsState.hasAtLeastOneFolder const hasAtLeastOneFolder = tagsState.hasAtLeastOneFolder
const premiumModal = usePremiumModal() const premiumModal = usePremiumModal()
const [showChildren, setShowChildren] = useState(tag.expanded) const [showChildren, setShowChildren] = useState(tag.expanded)
const [hadChildren, setHadChildren] = useState(hasChildren) const [hadChildren, setHadChildren] = useState(hasChildren)
useEffect(() => { useEffect(() => {
if (!hadChildren && hasChildren) { if (!hadChildren && hasChildren) {
setShowChildren(true) setShowChildren(true)
}
setHadChildren(hasChildren)
}, [hadChildren, hasChildren])
useEffect(() => {
setTitle(tag.title || '')
}, [setTitle, tag])
const toggleChildren: MouseEventHandler = useCallback(
(e) => {
e.stopPropagation()
setShowChildren((x) => {
tagsState.setExpanded(tag, !x)
return !x
})
},
[setShowChildren, tag, tagsState],
)
const selectCurrentTag = useCallback(async () => {
await tagsState.setSelectedTag(tag)
toggleAppPane(AppPaneId.Items)
}, [tagsState, tag, toggleAppPane])
const onBlur = useCallback(() => {
tagsState.save(tag, title).catch(console.error)
setTitle(tag.title)
}, [tagsState, tag, title, setTitle])
const onInput: FormEventHandler = useCallback(
(e) => {
const value = (e.target as HTMLInputElement).value
setTitle(value)
},
[setTitle],
)
const onKeyDown: KeyboardEventHandler = useCallback(
(e) => {
if (e.key === KeyboardKey.Enter) {
inputRef.current?.blur()
e.preventDefault()
} }
}, setHadChildren(hasChildren)
[inputRef], }, [hadChildren, hasChildren])
)
useEffect(() => { useEffect(() => {
if (isEditing) { setTitle(tag.title || '')
inputRef.current?.focus() }, [setTitle, tag])
}
}, [inputRef, isEditing])
const onSubtagInput = useCallback((e) => { const toggleChildren: MouseEventHandler = useCallback(
const value = (e.target as HTMLInputElement).value (e) => {
setSubtagTitle(value) e.stopPropagation()
}, []) setShowChildren((x) => {
tagsState.setExpanded(tag, !x)
const onSubtagInputBlur = useCallback(() => { return !x
tagsState.createSubtagAndAssignParent(tag, subtagTitle).catch(console.error) })
setSubtagTitle('')
}, [subtagTitle, tag, tagsState])
const onSubtagKeyDown: KeyboardEventHandler = useCallback(
(e) => {
if (e.key === KeyboardKey.Enter) {
e.preventDefault()
subtagInputRef.current?.blur()
}
},
[subtagInputRef],
)
useEffect(() => {
if (isAddingSubtag) {
subtagInputRef.current?.focus()
}
}, [subtagInputRef, isAddingSubtag])
const [, dragRef] = useDrag(
() => ({
type: ItemTypes.TAG,
item: { uuid: tag.uuid },
canDrag: () => {
return true
}, },
collect: (monitor) => ({ [setShowChildren, tag, tagsState],
isDragging: !!monitor.isDragging(), )
}),
}),
[tag],
)
const [{ isOver, canDrop }, dropRef] = useDrop<DropItem, void, DropProps>( const selectCurrentTag = useCallback(async () => {
() => ({ await tagsState.setSelectedTag(tag)
accept: ItemTypes.TAG, toggleAppPane(AppPaneId.Items)
canDrop: (item) => { }, [tagsState, tag, toggleAppPane])
return tagsState.isValidTagParent(tag, item as SNTag)
const onBlur = useCallback(() => {
tagsState.save(tag, title).catch(console.error)
setTitle(tag.title)
}, [tagsState, tag, title, setTitle])
const onInput: FormEventHandler = useCallback(
(e) => {
const value = (e.target as HTMLInputElement).value
setTitle(value)
}, },
drop: (item) => { [setTitle],
if (!hasFolders) { )
premiumModal.activate(TAG_FOLDERS_FEATURE_NAME)
return
}
tagsState.assignParent(item.uuid, tag.uuid).catch(console.error)
},
collect: (monitor) => ({
isOver: !!monitor.isOver(),
canDrop: !!monitor.canDrop(),
}),
}),
[tag, tagsState, hasFolders, premiumModal],
)
const readyToDrop = isOver && canDrop const onKeyDown: KeyboardEventHandler = useCallback(
(e) => {
const toggleContextMenu = useCallback(() => { if (e.key === KeyboardKey.Enter) {
if (!menuButtonRef.current) { inputRef.current?.blur()
return
}
const contextMenuOpen = tagsState.contextMenuOpen
const menuButtonRect = menuButtonRef.current?.getBoundingClientRect()
if (contextMenuOpen) {
tagsState.setContextMenuOpen(false)
} else {
onContextMenu(tag, menuButtonRect.right, menuButtonRect.top)
}
}, [onContextMenu, tagsState, tag])
return (
<>
<button
className={`tag focus:shadow-inner ${isSelected ? 'selected' : ''} ${readyToDrop ? 'is-drag-over' : ''}`}
onClick={selectCurrentTag}
ref={dragRef}
style={{
paddingLeft: `${level * PADDING_PER_LEVEL_PX + PADDING_BASE_PX}px`,
}}
onContextMenu={(e) => {
e.preventDefault() e.preventDefault()
onContextMenu(tag, e.clientX, e.clientY) }
}} },
> [inputRef],
<div className="tag-info" title={title} ref={dropRef}> )
{hasAtLeastOneFolder && (
<div className="tag-fold-container">
<a
role="button"
className={`tag-fold focus:shadow-inner ${showChildren ? 'opened' : 'closed'} ${
!hasChildren ? 'invisible' : ''
}`}
onClick={hasChildren ? toggleChildren : undefined}
>
<Icon className={'text-neutral'} type={showChildren ? 'menu-arrow-down-alt' : 'menu-arrow-right'} />
</a>
</div>
)}
<div className={'tag-icon draggable mr-2'} ref={dragRef}>
<Icon type="hashtag" className={`${isSelected ? 'text-info' : 'text-neutral'}`} />
</div>
<input
className={`title focus:shadow-none focus:outline-none ${isEditing ? 'editing' : ''}`}
id={`react-tag-${tag.uuid}`}
disabled={!isEditing}
onBlur={onBlur}
onInput={onInput}
value={title}
onKeyDown={onKeyDown}
spellCheck={false}
ref={inputRef}
/>
<div className="flex items-center">
<a
role="button"
className={`mr-2 cursor-pointer border-0 bg-transparent hover:bg-contrast focus:shadow-inner ${
isSelected ? 'visible' : 'invisible'
}`}
onClick={toggleContextMenu}
ref={menuButtonRef}
>
<Icon type="more" className="text-neutral" />
</a>
<div className="count">{noteCounts.get()}</div>
</div>
</div>
<div className={`meta ${hasAtLeastOneFolder ? 'with-folders' : ''}`}> useEffect(() => {
{tag.conflictOf && <div className="danger text-[0.625rem] font-bold">Conflicted Copy {tag.conflictOf}</div>} if (isEditing) {
</div> inputRef.current?.focus()
</button> }
{isAddingSubtag && ( }, [inputRef, isEditing])
<div
className="tag overflow-hidden" const onSubtagInput = useCallback((e) => {
const value = (e.target as HTMLInputElement).value
setSubtagTitle(value)
}, [])
const onSubtagInputBlur = useCallback(() => {
tagsState.createSubtagAndAssignParent(tag, subtagTitle).catch(console.error)
setSubtagTitle('')
}, [subtagTitle, tag, tagsState])
const onSubtagKeyDown: KeyboardEventHandler = useCallback(
(e) => {
if (e.key === KeyboardKey.Enter) {
e.preventDefault()
subtagInputRef.current?.blur()
}
},
[subtagInputRef],
)
useEffect(() => {
if (isAddingSubtag) {
subtagInputRef.current?.focus()
}
}, [subtagInputRef, isAddingSubtag])
const [, dragRef] = useDrag(
() => ({
type: ItemTypes.TAG,
item: { uuid: tag.uuid },
canDrag: () => {
return true
},
collect: (monitor) => ({
isDragging: !!monitor.isDragging(),
}),
}),
[tag],
)
const [{ isOver, canDrop }, dropRef] = useDrop<DropItem, void, DropProps>(
() => ({
accept: ItemTypes.TAG,
canDrop: (item) => {
return tagsState.isValidTagParent(tag, item as SNTag)
},
drop: (item) => {
if (!hasFolders) {
premiumModal.activate(TAG_FOLDERS_FEATURE_NAME)
return
}
tagsState.assignParent(item.uuid, tag.uuid).catch(console.error)
},
collect: (monitor) => ({
isOver: !!monitor.isOver(),
canDrop: !!monitor.canDrop(),
}),
}),
[tag, tagsState, hasFolders, premiumModal],
)
const readyToDrop = isOver && canDrop
const toggleContextMenu = useCallback(() => {
if (!menuButtonRef.current) {
return
}
const contextMenuOpen = tagsState.contextMenuOpen
const menuButtonRect = menuButtonRef.current?.getBoundingClientRect()
if (contextMenuOpen) {
tagsState.setContextMenuOpen(false)
} else {
onContextMenu(tag, menuButtonRect.right, menuButtonRect.top)
}
}, [onContextMenu, tagsState, tag])
return (
<>
<button
className={`tag focus:shadow-inner ${isSelected ? 'selected' : ''} ${readyToDrop ? 'is-drag-over' : ''}`}
onClick={selectCurrentTag}
ref={dragRef}
style={{ style={{
paddingLeft: `${(level + 1) * PADDING_PER_LEVEL_PX + PADDING_BASE_PX}px`, paddingLeft: `${
isCollapsed
? level * PADDING_PER_LEVEL_WHEN_COLLAPSED_PX + PADDING_BASE_WHEN_COLLAPSED_PX
: level * PADDING_PER_LEVEL_PX + PADDING_BASE_PX
}px`,
}}
onContextMenu={(e) => {
e.preventDefault()
onContextMenu(tag, e.clientX, e.clientY)
}} }}
> >
<div className="tag-info"> <div className="tag-info" title={title} ref={dropRef}>
<div className="flex h-full min-w-[22px] items-center border-0 bg-transparent p-0" /> {hasAtLeastOneFolder && (
<div className="tag-icon mr-1"> <div className="tag-fold-container">
<Icon type="hashtag" className="mr-1 text-neutral" /> <a
role="button"
className={`tag-fold focus:shadow-inner ${showChildren ? 'opened' : 'closed'} ${
!hasChildren ? 'invisible' : ''
}`}
onClick={hasChildren ? toggleChildren : undefined}
>
<Icon className={'text-neutral'} type={showChildren ? 'menu-arrow-down-alt' : 'menu-arrow-right'} />
</a>
</div>
)}
<div
className={classNames('tag-icon draggable mr-2', isCollapsed ? 'md-only:!hidden lg-only:!hidden' : '')}
ref={dragRef}
>
<Icon type="hashtag" className={`${isSelected ? 'text-info' : 'text-neutral'}`} />
</div> </div>
<input <input
className="title w-full focus:shadow-none focus:outline-none" className={classNames(
type="text" 'title focus:shadow-none focus:outline-none',
ref={subtagInputRef} isEditing ? 'editing' : '',
onBlur={onSubtagInputBlur} isCollapsed ? 'md-only:!w-min lg-only:!w-min' : '',
onKeyDown={onSubtagKeyDown} )}
value={subtagTitle} id={`react-tag-${tag.uuid}`}
onInput={onSubtagInput} disabled={!isEditing}
onBlur={onBlur}
onInput={onInput}
value={title}
onKeyDown={onKeyDown}
spellCheck={false}
ref={inputRef}
/> />
<div className="flex items-center">
<a
role="button"
className={`mr-2 cursor-pointer border-0 bg-transparent hover:bg-contrast focus:shadow-inner ${
isSelected ? 'visible' : 'invisible'
}`}
onClick={toggleContextMenu}
ref={menuButtonRef}
>
<Icon type="more" className="text-neutral" />
</a>
<div className="count">{noteCounts.get()}</div>
</div>
</div> </div>
</div>
)} <div className={`meta ${hasAtLeastOneFolder ? 'with-folders' : ''}`}>
{showChildren && ( {tag.conflictOf && <div className="danger text-[0.625rem] font-bold">Conflicted Copy {tag.conflictOf}</div>}
<> </div>
{childrenTags.map((tag) => { </button>
return ( {isAddingSubtag && (
<TagsListItem <div
level={level + 1} className="tag overflow-hidden"
key={tag.uuid} style={{
tag={tag} paddingLeft: `${(level + 1) * PADDING_PER_LEVEL_PX + PADDING_BASE_PX}px`,
tagsState={tagsState} }}
features={features} >
onContextMenu={onContextMenu} <div className="tag-info">
<div className="flex h-full min-w-[22px] items-center border-0 bg-transparent p-0" />
<div className="tag-icon mr-1">
<Icon type="hashtag" className="mr-1 text-neutral" />
</div>
<input
className="title w-full focus:shadow-none focus:outline-none"
type="text"
ref={subtagInputRef}
onBlur={onSubtagInputBlur}
onKeyDown={onSubtagKeyDown}
value={subtagTitle}
onInput={onSubtagInput}
/> />
) </div>
})} </div>
</> )}
)} {showChildren && (
</> <>
) {childrenTags.map((tag) => {
}) return (
<TagsListItem
level={level + 1}
key={tag.uuid}
tag={tag}
tagsState={tagsState}
features={features}
onContextMenu={onContextMenu}
isCollapsed={isCollapsed}
/>
)
})}
</>
)}
</>
)
},
)
TagsListItem.displayName = 'TagsListItem' TagsListItem.displayName = 'TagsListItem'

View File

@@ -5,12 +5,14 @@ import { observer } from 'mobx-react-lite'
import { FunctionComponent, useCallback, useEffect, useState } from 'react' import { FunctionComponent, useCallback, useEffect, useState } from 'react'
import TagsSectionAddButton from './TagsSectionAddButton' import TagsSectionAddButton from './TagsSectionAddButton'
import TagsSectionTitle from './TagsSectionTitle' import TagsSectionTitle from './TagsSectionTitle'
import { classNames } from '@/Utils/ConcatenateClassNames'
type Props = { type Props = {
viewControllerManager: ViewControllerManager viewControllerManager: ViewControllerManager
isCollapsed: boolean
} }
const TagsSection: FunctionComponent<Props> = ({ viewControllerManager }) => { const TagsSection: FunctionComponent<Props> = ({ viewControllerManager, isCollapsed }) => {
const [hasMigration, setHasMigration] = useState<boolean>(false) const [hasMigration, setHasMigration] = useState<boolean>(false)
const checkIfMigrationNeeded = useCallback(() => { const checkIfMigrationNeeded = useCallback(() => {
@@ -53,7 +55,7 @@ const TagsSection: FunctionComponent<Props> = ({ viewControllerManager }) => {
return ( return (
<section> <section>
<div className="section-title-bar"> <div className={classNames('section-title-bar', isCollapsed ? 'md-only:hidden lg-only:hidden' : '')}>
<div className="section-title-bar-header"> <div className="section-title-bar-header">
<TagsSectionTitle <TagsSectionTitle
features={viewControllerManager.featuresController} features={viewControllerManager.featuresController}
@@ -66,7 +68,13 @@ const TagsSection: FunctionComponent<Props> = ({ viewControllerManager }) => {
/> />
</div> </div>
</div> </div>
<TagsList viewControllerManager={viewControllerManager} /> <div
className={classNames(
'hidden',
isCollapsed ? 'mt-6 mb-7 border border-border md-only:block lg-only:block' : '',
)}
/>
<TagsList viewControllerManager={viewControllerManager} isCollapsed={isCollapsed} />
</section> </section>
) )
} }

View File

@@ -7,15 +7,16 @@ import { FunctionComponent } from 'react'
type Props = { type Props = {
tags: NavigationController tags: NavigationController
features: FeaturesController features: FeaturesController
className?: string
} }
const TagsSectionAddButton: FunctionComponent<Props> = ({ tags }) => { const TagsSectionAddButton: FunctionComponent<Props> = ({ tags, className = '' }) => {
return ( return (
<IconButton <IconButton
focusable={true} focusable={true}
icon="add" icon="add"
title="Create a new tag" title="Create a new tag"
className="p-0 text-neutral" className={`p-0 text-neutral ${className}`}
onClick={() => tags.createNewTemplate()} onClick={() => tags.createNewTemplate()}
/> />
) )

View File

@@ -9,9 +9,10 @@ type Props = {
features: FeaturesController features: FeaturesController
hasMigration: boolean hasMigration: boolean
onClickMigration: () => void onClickMigration: () => void
className?: string
} }
const TagsSectionTitle: FunctionComponent<Props> = ({ features, hasMigration, onClickMigration }) => { const TagsSectionTitle: FunctionComponent<Props> = ({ features, hasMigration, onClickMigration, className = '' }) => {
const entitledToFolders = features.hasFolders const entitledToFolders = features.hasFolders
const modal = usePremiumModal() const modal = usePremiumModal()
@@ -22,7 +23,7 @@ const TagsSectionTitle: FunctionComponent<Props> = ({ features, hasMigration, on
if (entitledToFolders) { if (entitledToFolders) {
return ( return (
<> <>
<div className="title text-sm"> <div className={`title text-sm ${className}`}>
<span className="font-bold">Folders</span> <span className="font-bold">Folders</span>
{hasMigration && ( {hasMigration && (
<label className="ml-1 cursor-pointer font-bold text-info" onClick={onClickMigration}> <label className="ml-1 cursor-pointer font-bold text-info" onClick={onClickMigration}>
@@ -36,7 +37,7 @@ const TagsSectionTitle: FunctionComponent<Props> = ({ features, hasMigration, on
return ( return (
<> <>
<div className="title text-sm"> <div className={`title text-sm ${className}`}>
<span className="font-bold">Tags</span> <span className="font-bold">Tags</span>
<Tooltip label={TAG_FOLDERS_FEATURE_TOOLTIP}> <Tooltip label={TAG_FOLDERS_FEATURE_TOOLTIP}>
<label className="ml-1 cursor-pointer font-bold text-passive-2" onClick={showPremiumAlert}> <label className="ml-1 cursor-pointer font-bold text-passive-2" onClick={showPremiumAlert}>

View File

@@ -9,22 +9,6 @@
} }
} }
.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 { .app-column {
overflow: hidden; overflow: hidden;

View File

@@ -132,6 +132,21 @@
@include DimmedBackground(var(--sn-stylekit-info-color), 0.08); @include DimmedBackground(var(--sn-stylekit-info-color), 0.08);
} }
.transparent-info-color-background {
&::after {
@include DimmedBackground(var(--sn-stylekit-info-color), .12);
border-radius: 100px;
}
}
.transparent-info-color-background-hover {
&:hover {
&::after {
@extend .transparent-info-color-background
}
}
}
svg.sk-circular-progress { svg.sk-circular-progress {
$pi: 3.14159265358979; $pi: 3.14159265358979;

View File

@@ -1,4 +1,5 @@
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
module.exports = { module.exports = {
content: ['./src/javascripts/**/*.tsx', '../toast/src/**/*.tsx'], content: ['./src/javascripts/**/*.tsx', '../toast/src/**/*.tsx'],
theme: { theme: {
@@ -9,6 +10,7 @@ module.exports = {
26: '6.5rem', 26: '6.5rem',
30: '7.5rem', 30: '7.5rem',
70: '17.5rem', 70: '17.5rem',
87.5: '21.875rem',
125: '31.25rem', 125: '31.25rem',
160: '40rem', 160: '40rem',
}, },
@@ -70,6 +72,15 @@ module.exports = {
fontSize: { fontSize: {
'menu-item': '0.813rem', 'menu-item': '0.813rem',
}, },
screens: {
'xsm-only': { min: '320px', max: '639px' },
'sm-only': { min: '640px', max: '767px' },
'md-only': { min: '768px', max: '1023px' },
'lg-only': { min: '1024px', max: '1279px' },
},
transitionProperty: {
width: 'width',
},
}, },
colors: { colors: {
transparent: 'transparent', transparent: 'transparent',