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:
@@ -172,7 +172,7 @@ const ContentListView: FunctionComponent<Props> = ({
|
||||
return (
|
||||
<div
|
||||
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'}
|
||||
ref={itemsViewPanelRef}
|
||||
>
|
||||
|
||||
@@ -10,6 +10,7 @@ import SearchBar from '@/Components/SearchBar/SearchBar'
|
||||
import ResponsivePaneContent from '@/Components/ResponsivePane/ResponsivePaneContent'
|
||||
import { AppPaneId } from '@/Components/ResponsivePane/AppPaneMetadata'
|
||||
import { classNames } from '@/Utils/ConcatenateClassNames'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
@@ -19,6 +20,7 @@ const Navigation: FunctionComponent<Props> = ({ application }) => {
|
||||
const viewControllerManager = useMemo(() => application.getViewControllerManager(), [application])
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
const [panelWidth, setPanelWidth] = useState<number>(0)
|
||||
const [isPanelExpanded, setIsPanelExpanded] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
const removeObserver = application.addEventObserver(async () => {
|
||||
@@ -47,19 +49,39 @@ const Navigation: FunctionComponent<Props> = ({ application }) => {
|
||||
}, [viewControllerManager])
|
||||
|
||||
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">
|
||||
<SearchBar
|
||||
itemListController={viewControllerManager.itemListController}
|
||||
searchOptionsController={viewControllerManager.searchOptionsController}
|
||||
selectedViewTitle={viewControllerManager.navigationController.selected?.title}
|
||||
/>
|
||||
<div className="section-title-bar">
|
||||
<div className="section-title-bar-header">
|
||||
<div className="title text-sm">
|
||||
<span className="font-bold">Views</span>
|
||||
{isPanelExpanded && (
|
||||
<SearchBar
|
||||
itemListController={viewControllerManager.itemListController}
|
||||
searchOptionsController={viewControllerManager.searchOptionsController}
|
||||
selectedViewTitle={viewControllerManager.navigationController.selected?.title}
|
||||
/>
|
||||
)}
|
||||
<div className={'flex justify-end'}>
|
||||
<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
|
||||
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
|
||||
className={classNames(
|
||||
@@ -68,8 +90,8 @@ const Navigation: FunctionComponent<Props> = ({ application }) => {
|
||||
'md:hover:[overflow-y:_overlay]',
|
||||
)}
|
||||
>
|
||||
<SmartViewsSection viewControllerManager={viewControllerManager} />
|
||||
<TagsSection viewControllerManager={viewControllerManager} />
|
||||
<SmartViewsSection viewControllerManager={viewControllerManager} isCollapsed={!isPanelExpanded} />
|
||||
<TagsSection viewControllerManager={viewControllerManager} isCollapsed={!isPanelExpanded} />
|
||||
</div>
|
||||
</ResponsivePaneContent>
|
||||
{ref.current && (
|
||||
|
||||
@@ -5,9 +5,10 @@ import SmartViewsListItem from './SmartViewsListItem'
|
||||
|
||||
type Props = {
|
||||
viewControllerManager: ViewControllerManager
|
||||
isCollapsed: boolean
|
||||
}
|
||||
|
||||
const SmartViewsList: FunctionComponent<Props> = ({ viewControllerManager }: Props) => {
|
||||
const SmartViewsList: FunctionComponent<Props> = ({ viewControllerManager, isCollapsed }: Props) => {
|
||||
const allViews = viewControllerManager.navigationController.smartViews
|
||||
|
||||
return (
|
||||
@@ -19,6 +20,7 @@ const SmartViewsList: FunctionComponent<Props> = ({ viewControllerManager }: Pro
|
||||
view={view}
|
||||
tagsState={viewControllerManager.navigationController}
|
||||
features={viewControllerManager.featuresController}
|
||||
isCollapsed={isCollapsed}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -15,11 +15,13 @@ import {
|
||||
} from 'react'
|
||||
import { AppPaneId } from '../ResponsivePane/AppPaneMetadata'
|
||||
import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider'
|
||||
import { classNames } from '@/Utils/ConcatenateClassNames'
|
||||
|
||||
type Props = {
|
||||
view: SmartView
|
||||
tagsState: NavigationController
|
||||
features: FeaturesController
|
||||
isCollapsed: boolean
|
||||
}
|
||||
|
||||
const PADDING_BASE_PX = 14
|
||||
@@ -45,7 +47,7 @@ const smartViewIconType = (view: SmartView, isSelected: boolean): IconType => {
|
||||
return 'hashtag'
|
||||
}
|
||||
|
||||
const SmartViewsListItem: FunctionComponent<Props> = ({ view, tagsState }) => {
|
||||
const SmartViewsListItem: FunctionComponent<Props> = ({ view, tagsState, isCollapsed }) => {
|
||||
const { toggleAppPane } = useResponsiveAppPane()
|
||||
|
||||
const [title, setTitle] = useState(view.title || '')
|
||||
@@ -111,18 +113,30 @@ const SmartViewsListItem: FunctionComponent<Props> = ({ view, tagsState }) => {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={`tag ${isSelected ? 'selected' : ''} ${isFaded ? 'opacity-50' : ''}`}
|
||||
className={classNames(
|
||||
'tag',
|
||||
isSelected && 'selected',
|
||||
isFaded && 'opacity-50',
|
||||
isCollapsed && '!bg-transparent',
|
||||
)}
|
||||
onClick={selectCurrentTag}
|
||||
style={{
|
||||
paddingLeft: `${level * PADDING_PER_LEVEL_PX + PADDING_BASE_PX}px`,
|
||||
}}
|
||||
>
|
||||
<div className="tag-info">
|
||||
<div className={'tag-icon mr-2'}>
|
||||
<Icon type={iconType} className={`${isSelected ? 'text-info' : 'text-neutral'}`} />
|
||||
<div className="tag-info relative">
|
||||
<div
|
||||
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>
|
||||
<input
|
||||
className={`title ${isEditing ? 'editing' : ''}`}
|
||||
className={classNames('title', isEditing ? 'editing' : '', isCollapsed ? 'hidden' : 'block')}
|
||||
disabled={!isEditing}
|
||||
id={`react-tag-${view.uuid}`}
|
||||
onBlur={onBlur}
|
||||
@@ -132,10 +146,12 @@ const SmartViewsListItem: FunctionComponent<Props> = ({ view, tagsState }) => {
|
||||
spellCheck={false}
|
||||
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>
|
||||
|
||||
{!isSystemView(view) && (
|
||||
{!isSystemView(view) && !isCollapsed && (
|
||||
<div className="meta">
|
||||
{view.conflictOf && (
|
||||
<div className="danger text-[0.625rem] font-bold">Conflicted Copy {view.conflictOf}</div>
|
||||
|
||||
@@ -5,12 +5,13 @@ import SmartViewsList from './SmartViewsList'
|
||||
|
||||
type Props = {
|
||||
viewControllerManager: ViewControllerManager
|
||||
isCollapsed: boolean
|
||||
}
|
||||
|
||||
const SmartViewsSection: FunctionComponent<Props> = ({ viewControllerManager }) => {
|
||||
const SmartViewsSection: FunctionComponent<Props> = ({ viewControllerManager, isCollapsed }) => {
|
||||
return (
|
||||
<section>
|
||||
<SmartViewsList viewControllerManager={viewControllerManager} />
|
||||
<SmartViewsList viewControllerManager={viewControllerManager} isCollapsed={isCollapsed} />
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -9,9 +9,10 @@ import { TagsListItem } from './TagsListItem'
|
||||
|
||||
type Props = {
|
||||
viewControllerManager: ViewControllerManager
|
||||
isCollapsed: boolean
|
||||
}
|
||||
|
||||
const TagsList: FunctionComponent<Props> = ({ viewControllerManager }: Props) => {
|
||||
const TagsList: FunctionComponent<Props> = ({ viewControllerManager, isCollapsed }: Props) => {
|
||||
const tagsState = viewControllerManager.navigationController
|
||||
const allTags = tagsState.allLocalRootTags
|
||||
|
||||
@@ -52,6 +53,7 @@ const TagsList: FunctionComponent<Props> = ({ viewControllerManager }: Props) =>
|
||||
tagsState={tagsState}
|
||||
features={viewControllerManager.featuresController}
|
||||
onContextMenu={onContextMenu}
|
||||
isCollapsed={isCollapsed}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -22,6 +22,7 @@ import { useDrag, useDrop } from 'react-dnd'
|
||||
import { DropItem, DropProps, ItemTypes } from './DragNDrop'
|
||||
import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider'
|
||||
import { AppPaneId } from '../ResponsivePane/AppPaneMetadata'
|
||||
import { classNames } from '@/Utils/ConcatenateClassNames'
|
||||
|
||||
type Props = {
|
||||
tag: SNTag
|
||||
@@ -29,273 +30,290 @@ type Props = {
|
||||
features: FeaturesController
|
||||
level: number
|
||||
onContextMenu: (tag: SNTag, posX: number, posY: number) => void
|
||||
isCollapsed: boolean
|
||||
}
|
||||
|
||||
const PADDING_BASE_PX = 14
|
||||
const PADDING_BASE_WHEN_COLLAPSED_PX = 6
|
||||
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 }) => {
|
||||
const { toggleAppPane } = useResponsiveAppPane()
|
||||
export const TagsListItem: FunctionComponent<Props> = observer(
|
||||
({ tag, features, tagsState, level, onContextMenu, isCollapsed }) => {
|
||||
const { toggleAppPane } = useResponsiveAppPane()
|
||||
|
||||
const [title, setTitle] = useState(tag.title || '')
|
||||
const [subtagTitle, setSubtagTitle] = useState('')
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
const subtagInputRef = useRef<HTMLInputElement>(null)
|
||||
const menuButtonRef = useRef<HTMLAnchorElement>(null)
|
||||
const [title, setTitle] = useState(tag.title || '')
|
||||
const [subtagTitle, setSubtagTitle] = useState('')
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
const subtagInputRef = useRef<HTMLInputElement>(null)
|
||||
const menuButtonRef = useRef<HTMLAnchorElement>(null)
|
||||
|
||||
const isSelected = tagsState.selected === tag
|
||||
const isEditing = tagsState.editingTag === tag
|
||||
const isAddingSubtag = tagsState.addingSubtagTo === tag
|
||||
const noteCounts = computed(() => tagsState.getNotesCount(tag))
|
||||
const isSelected = tagsState.selected === tag
|
||||
const isEditing = tagsState.editingTag === tag
|
||||
const isAddingSubtag = tagsState.addingSubtagTo === tag
|
||||
const noteCounts = computed(() => tagsState.getNotesCount(tag))
|
||||
|
||||
const childrenTags = computed(() => tagsState.getChildren(tag)).get()
|
||||
const hasChildren = childrenTags.length > 0
|
||||
const childrenTags = computed(() => tagsState.getChildren(tag)).get()
|
||||
const hasChildren = childrenTags.length > 0
|
||||
|
||||
const hasFolders = features.hasFolders
|
||||
const hasAtLeastOneFolder = tagsState.hasAtLeastOneFolder
|
||||
const hasFolders = features.hasFolders
|
||||
const hasAtLeastOneFolder = tagsState.hasAtLeastOneFolder
|
||||
|
||||
const premiumModal = usePremiumModal()
|
||||
const premiumModal = usePremiumModal()
|
||||
|
||||
const [showChildren, setShowChildren] = useState(tag.expanded)
|
||||
const [hadChildren, setHadChildren] = useState(hasChildren)
|
||||
const [showChildren, setShowChildren] = useState(tag.expanded)
|
||||
const [hadChildren, setHadChildren] = useState(hasChildren)
|
||||
|
||||
useEffect(() => {
|
||||
if (!hadChildren && hasChildren) {
|
||||
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()
|
||||
useEffect(() => {
|
||||
if (!hadChildren && hasChildren) {
|
||||
setShowChildren(true)
|
||||
}
|
||||
},
|
||||
[inputRef],
|
||||
)
|
||||
setHadChildren(hasChildren)
|
||||
}, [hadChildren, hasChildren])
|
||||
|
||||
useEffect(() => {
|
||||
if (isEditing) {
|
||||
inputRef.current?.focus()
|
||||
}
|
||||
}, [inputRef, isEditing])
|
||||
useEffect(() => {
|
||||
setTitle(tag.title || '')
|
||||
}, [setTitle, tag])
|
||||
|
||||
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
|
||||
const toggleChildren: MouseEventHandler = useCallback(
|
||||
(e) => {
|
||||
e.stopPropagation()
|
||||
setShowChildren((x) => {
|
||||
tagsState.setExpanded(tag, !x)
|
||||
return !x
|
||||
})
|
||||
},
|
||||
collect: (monitor) => ({
|
||||
isDragging: !!monitor.isDragging(),
|
||||
}),
|
||||
}),
|
||||
[tag],
|
||||
)
|
||||
[setShowChildren, tag, tagsState],
|
||||
)
|
||||
|
||||
const [{ isOver, canDrop }, dropRef] = useDrop<DropItem, void, DropProps>(
|
||||
() => ({
|
||||
accept: ItemTypes.TAG,
|
||||
canDrop: (item) => {
|
||||
return tagsState.isValidTagParent(tag, item as SNTag)
|
||||
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)
|
||||
},
|
||||
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],
|
||||
)
|
||||
[setTitle],
|
||||
)
|
||||
|
||||
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={{
|
||||
paddingLeft: `${level * PADDING_PER_LEVEL_PX + PADDING_BASE_PX}px`,
|
||||
}}
|
||||
onContextMenu={(e) => {
|
||||
const onKeyDown: KeyboardEventHandler = useCallback(
|
||||
(e) => {
|
||||
if (e.key === KeyboardKey.Enter) {
|
||||
inputRef.current?.blur()
|
||||
e.preventDefault()
|
||||
onContextMenu(tag, e.clientX, e.clientY)
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
}
|
||||
},
|
||||
[inputRef],
|
||||
)
|
||||
|
||||
<div className={`meta ${hasAtLeastOneFolder ? 'with-folders' : ''}`}>
|
||||
{tag.conflictOf && <div className="danger text-[0.625rem] font-bold">Conflicted Copy {tag.conflictOf}</div>}
|
||||
</div>
|
||||
</button>
|
||||
{isAddingSubtag && (
|
||||
<div
|
||||
className="tag overflow-hidden"
|
||||
useEffect(() => {
|
||||
if (isEditing) {
|
||||
inputRef.current?.focus()
|
||||
}
|
||||
}, [inputRef, isEditing])
|
||||
|
||||
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={{
|
||||
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="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 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={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>
|
||||
<input
|
||||
className="title w-full focus:shadow-none focus:outline-none"
|
||||
type="text"
|
||||
ref={subtagInputRef}
|
||||
onBlur={onSubtagInputBlur}
|
||||
onKeyDown={onSubtagKeyDown}
|
||||
value={subtagTitle}
|
||||
onInput={onSubtagInput}
|
||||
className={classNames(
|
||||
'title focus:shadow-none focus:outline-none',
|
||||
isEditing ? 'editing' : '',
|
||||
isCollapsed ? 'md-only:!w-min lg-only:!w-min' : '',
|
||||
)}
|
||||
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>
|
||||
)}
|
||||
{showChildren && (
|
||||
<>
|
||||
{childrenTags.map((tag) => {
|
||||
return (
|
||||
<TagsListItem
|
||||
level={level + 1}
|
||||
key={tag.uuid}
|
||||
tag={tag}
|
||||
tagsState={tagsState}
|
||||
features={features}
|
||||
onContextMenu={onContextMenu}
|
||||
|
||||
<div className={`meta ${hasAtLeastOneFolder ? 'with-folders' : ''}`}>
|
||||
{tag.conflictOf && <div className="danger text-[0.625rem] font-bold">Conflicted Copy {tag.conflictOf}</div>}
|
||||
</div>
|
||||
</button>
|
||||
{isAddingSubtag && (
|
||||
<div
|
||||
className="tag overflow-hidden"
|
||||
style={{
|
||||
paddingLeft: `${(level + 1) * PADDING_PER_LEVEL_PX + PADDING_BASE_PX}px`,
|
||||
}}
|
||||
>
|
||||
<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'
|
||||
|
||||
@@ -5,12 +5,14 @@ import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent, useCallback, useEffect, useState } from 'react'
|
||||
import TagsSectionAddButton from './TagsSectionAddButton'
|
||||
import TagsSectionTitle from './TagsSectionTitle'
|
||||
import { classNames } from '@/Utils/ConcatenateClassNames'
|
||||
|
||||
type Props = {
|
||||
viewControllerManager: ViewControllerManager
|
||||
isCollapsed: boolean
|
||||
}
|
||||
|
||||
const TagsSection: FunctionComponent<Props> = ({ viewControllerManager }) => {
|
||||
const TagsSection: FunctionComponent<Props> = ({ viewControllerManager, isCollapsed }) => {
|
||||
const [hasMigration, setHasMigration] = useState<boolean>(false)
|
||||
|
||||
const checkIfMigrationNeeded = useCallback(() => {
|
||||
@@ -53,7 +55,7 @@ const TagsSection: FunctionComponent<Props> = ({ viewControllerManager }) => {
|
||||
|
||||
return (
|
||||
<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">
|
||||
<TagsSectionTitle
|
||||
features={viewControllerManager.featuresController}
|
||||
@@ -66,7 +68,13 @@ const TagsSection: FunctionComponent<Props> = ({ viewControllerManager }) => {
|
||||
/>
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,15 +7,16 @@ import { FunctionComponent } from 'react'
|
||||
type Props = {
|
||||
tags: NavigationController
|
||||
features: FeaturesController
|
||||
className?: string
|
||||
}
|
||||
|
||||
const TagsSectionAddButton: FunctionComponent<Props> = ({ tags }) => {
|
||||
const TagsSectionAddButton: FunctionComponent<Props> = ({ tags, className = '' }) => {
|
||||
return (
|
||||
<IconButton
|
||||
focusable={true}
|
||||
icon="add"
|
||||
title="Create a new tag"
|
||||
className="p-0 text-neutral"
|
||||
className={`p-0 text-neutral ${className}`}
|
||||
onClick={() => tags.createNewTemplate()}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -9,9 +9,10 @@ type Props = {
|
||||
features: FeaturesController
|
||||
hasMigration: boolean
|
||||
onClickMigration: () => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
const TagsSectionTitle: FunctionComponent<Props> = ({ features, hasMigration, onClickMigration }) => {
|
||||
const TagsSectionTitle: FunctionComponent<Props> = ({ features, hasMigration, onClickMigration, className = '' }) => {
|
||||
const entitledToFolders = features.hasFolders
|
||||
const modal = usePremiumModal()
|
||||
|
||||
@@ -22,7 +23,7 @@ const TagsSectionTitle: FunctionComponent<Props> = ({ features, hasMigration, on
|
||||
if (entitledToFolders) {
|
||||
return (
|
||||
<>
|
||||
<div className="title text-sm">
|
||||
<div className={`title text-sm ${className}`}>
|
||||
<span className="font-bold">Folders</span>
|
||||
{hasMigration && (
|
||||
<label className="ml-1 cursor-pointer font-bold text-info" onClick={onClickMigration}>
|
||||
@@ -36,7 +37,7 @@ const TagsSectionTitle: FunctionComponent<Props> = ({ features, hasMigration, on
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="title text-sm">
|
||||
<div className={`title text-sm ${className}`}>
|
||||
<span className="font-bold">Tags</span>
|
||||
<Tooltip label={TAG_FOLDERS_FEATURE_TOOLTIP}>
|
||||
<label className="ml-1 cursor-pointer font-bold text-passive-2" onClick={showPremiumAlert}>
|
||||
|
||||
@@ -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 {
|
||||
overflow: hidden;
|
||||
|
||||
|
||||
@@ -132,6 +132,21 @@
|
||||
@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 {
|
||||
$pi: 3.14159265358979;
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
|
||||
module.exports = {
|
||||
content: ['./src/javascripts/**/*.tsx', '../toast/src/**/*.tsx'],
|
||||
theme: {
|
||||
@@ -9,6 +10,7 @@ module.exports = {
|
||||
26: '6.5rem',
|
||||
30: '7.5rem',
|
||||
70: '17.5rem',
|
||||
87.5: '21.875rem',
|
||||
125: '31.25rem',
|
||||
160: '40rem',
|
||||
},
|
||||
@@ -70,6 +72,15 @@ module.exports = {
|
||||
fontSize: {
|
||||
'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: {
|
||||
transparent: 'transparent',
|
||||
|
||||
Reference in New Issue
Block a user