Skip to content

Commit

Permalink
💄 Improve small topbar nav issues (#540)
Browse files Browse the repository at this point in the history
* wip: give topbar love, add layout docs

initial effort towards #488

* small misc improvements
  • Loading branch information
aaronleopold authored Dec 26, 2024
1 parent 821c1bc commit acd8a24
Show file tree
Hide file tree
Showing 23 changed files with 233 additions and 54 deletions.
24 changes: 24 additions & 0 deletions docs/components/InlineIconButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import clsx from 'clsx'
import React from 'react'

interface InlineIconButtonProps {
children: React.ReactNode
text?: string
size?: number
}

export default function InlineIconButton(props: InlineIconButtonProps) {
const size = props.size ? props.size : 16

return (
<span
className={clsx(
'inline-flex items-center rounded-md bg-[#D3D5D7] px-1 text-black dark:bg-[#161719] dark:text-[#D3D5D7]',
{ 'p-1': !props.text },
)}
>
{React.cloneElement(props.children as React.ReactElement, { size })}
{props.text && <span className="ml-1">{props.text}</span>}
</span>
)
}
1 change: 1 addition & 0 deletions docs/pages/guides/configuration/_meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ import { Meta } from 'nextra'
export default {
'server-options': 'Server',
theming: 'Theming',
layout: 'Layout',
} satisfies Meta
101 changes: 101 additions & 0 deletions docs/pages/guides/configuration/layout.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { Callout, Steps } from 'nextra/components'
import { Brush, Lock, LockOpen, Eye, EyeOff, Bolt } from 'lucide-react'
import InlineIconButton from '../../../components/InlineIconButton'

# Layout

Stump has a flexible layout system that allows you to customize the layout of the app to your liking. There are more options planned, but for now the two main options are navigation placement

## Navigation Placement

The navigation has two main placement options:

1. Sidebar
2. Topbar

They cannot be used together, and while they offer the same navigtion options there are some differences in functionality

## Sidebar

The sidebar is the default navigation placement. It is a vertical navigation, left-aligned and fixed to the viewport height. Since the sidebar is fixed and always visible, there is more wiggle room for squeezing in additional functionality and sub-menus. Therefore, when using the sidebar you have access to shortcuts that otherwise aren't found in the topbar. For example, the library submenu allows for:

- Scanning a library
- Accessing the file explorer
- Directly accessing the settings page for a library
- Quick deletion of a library (with a confirmation dialog)

Each first-class item (e.g., library, smart list, etc.) has a submenu of its own, with options like outlined above.

### Secondary Sidebars

Throughout the app, you will find secondary sidebars that are used to display nested navigation options when needed. Some of these are collapsible, and/or can be toggled on / off. These are generally used for navigation that is specific to the current context, e.g., the library settings is organized into many pages which otherwise wouldn't fit in the main sidebar.

#### Enable / Disable

You can enable or disable the settings-specific secondary sidebars by toggling the switch in the appearance settings labeled `Settings sidebar`. This setting will apply to all settings pages with a secondary sidebar.

#### Collapse Primary Sidebar

You can set the primary sidebar to be collapsed when a secondary sidebar is present. This setting is found in the appearance settings labeled `Replace primary sidebar`.

## Topbar

The topbar is a horizontal navigation, top-aligned and fixed to the viewport width. It is more compact than the sidebar, and is better suited for users who prefer a more minimalistic interface. The topbar has a more limited set of options, but still allows for the same core navigation-specific functionality as the sidebar.

### Last Visited Library

The topbar has a special feature that allows you to quickly access the last library you visited. This is a convenience feature with no real equivalent in the sidebar, and might be useful for users who have many libraries but frequently visit the same one.

<Callout emoji="🚀">
Stump tracks the timestamp for each library you visit, but only exposes the most recent one in the
UI. If you feel like this feature could be improved to utilize a history of visited libraries,
please let us know!
</Callout>

### Adjusted Width

The topbar has a special feature that allows you to adjust the width of the margins around the main content area. This allows you to better optimize a layout that suits your needs, and is especially useful for users who prefer a more spacious layout on larger monitors. This option is unlocked when the topbar is enabled, and can be found in the appearance settings labeled `Adjusted width`

## Arrangement

The arrangement of the navigation items is the same between the sidebar and topbar, and you are able to customize the order of the items to your liking. You can also hide / show items as you see fit.

### Customization

To customize the layout of the app:

<Steps>

#### Navigate to appearance settings

You can access the settings page by clicking on the cog icon in the sidebar or topbar, depending on your layout choice, then clicking on the <InlineIconButton text="Appearance"><Brush/></InlineIconButton> tab

<Callout emoji="ℹ️">
If you have disabled the secondary, settings sidebar, you will either need to access the select
box towards the top labeled `Section` and select `Appearance` or use the topbar settings menu to
navigate to the appearance settings
</Callout>

#### Scroll down to the arrangement section

Scroll down until you see a section titled `Navigation arrangement`. Here you can drag and drop the items to rearrange them, and toggle the visibility of items by clicking on the eye icon

#### Customize to your liking

To make any changes, you need to click the <InlineIconButton><Lock/></InlineIconButton> icon to unlock the arrangement. Once you are done, click the <InlineIconButton><LockOpen/></InlineIconButton> icon again to lock the arrangement. Changes are saved automatically as you make them, so you don't need to worry about losing your changes if you forget to lock the arrangement. The following is a list of icons and their meanings:

| Icon | Description |
| ------------------------------------------------ | -------------------------------------- |
| <InlineIconButton><Lock/></InlineIconButton> | Arrangement is locked, click to unlock |
| <InlineIconButton><LockOpen/></InlineIconButton> | Arrangement is unlocked, click to lock |
| <InlineIconButton><Eye/></InlineIconButton> | Item is visible, click to hide |
| <InlineIconButton><EyeOff/></InlineIconButton> | Item is hidden, click to show |
| <InlineIconButton><Bolt/></InlineIconButton> | Show / hide additional options |

</Steps>

## Future Plans

In the future, I would love to have the time to implement more options for the layout system. Some ideas include:

- Spacing options for the entire app (e.g., compact vs spacious)
10 changes: 9 additions & 1 deletion packages/browser/src/components/filters/FilterHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { cn, useSticky } from '@stump/components'
import { useMediaMatch } from 'rooks'

import { usePreferences } from '@/hooks/usePreferences'

import { useFilterContext } from './context'
import Search from './Search'

Expand Down Expand Up @@ -43,7 +45,13 @@ export default function FilterHeader({
navOffset,
}: Props) {
const isMobile = useMediaMatch('(max-width: 768px)')
const { ref, isSticky } = useSticky({ extraOffset: isMobile ? 56 : 0 })
const {
preferences: { primary_navigation_mode },
} = usePreferences()
const { ref, isSticky } = useSticky<HTMLDivElement>({
extraOffset: isMobile || primary_navigation_mode === 'TOPBAR' ? 56 : 0,
})

const { filters, setFilter, removeFilter } = useFilterContext()

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ type Props = {

export default function LastVisitedLibrary({ container }: Props) {
const { sdk } = useSDK()
const { data: library } = useQuery([sdk.library.keys.getLastVisited], sdk.library.getLastVisited)
const { data: library } = useQuery([sdk.library.keys.getLastVisited], () =>
sdk.library.getLastVisited(),
)

if (!library) {
return null
Expand Down
12 changes: 9 additions & 3 deletions packages/browser/src/components/navigation/topbar/TopBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { NavigationItem } from '@stump/sdk'
import { Book, Home } from 'lucide-react'
import { useCallback, useMemo } from 'react'
import { useLocation } from 'react-router'
import { useDimensionsRef } from 'rooks'
import { match } from 'ts-pattern'

import { useAppContext } from '@/context'
Expand All @@ -19,6 +20,7 @@ import TopBarNavLink from './TopBarNavLink'
export default function TopNavigation() {
const location = useLocation()

const [ref, size] = useDimensionsRef()
const { t } = useLocaleContext()
const { checkPermission } = useAppContext()
const {
Expand Down Expand Up @@ -76,6 +78,7 @@ export default function TopNavigation() {
key="libraries-topbar-navlink"
showCreate={ctx.show_create_action}
showLinkToAll={ctx.show_link_to_all}
width={size?.width}
/>
))
// .with('SmartLists', () => <SmartListSideBarSection />)
Expand All @@ -84,12 +87,13 @@ export default function TopNavigation() {
key="book-clubs-topbar-navlink"
showCreate={ctx.show_create_action}
showLinkToAll={ctx.show_link_to_all}
width={size?.width}
/>
))
.otherwise(() => null),
)
.filter(Boolean),
[arrangement, checkSectionPermission, location, t],
[arrangement, checkSectionPermission, location, t, size?.width],
)

return (
Expand All @@ -101,10 +105,12 @@ export default function TopNavigation() {
}}
>
<NavigationMenu className="z-[100] h-full">
<NavigationMenu.List className="w-full pl-4">{sections}</NavigationMenu.List>
<div ref={ref}>
<NavigationMenu.List className="w-full pl-4">{sections}</NavigationMenu.List>
</div>
</NavigationMenu>

<div className="flex h-full items-center gap-x-2">
<div className="flex h-full shrink-0 items-center gap-x-2">
<NavigationMenu className="z-[100] h-full pr-4" viewPortProps={{ align: 'right' }}>
<NavigationMenu.List className="w-full">
<SettingsNavigationItem />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ type Props = {
isActive?: boolean
className?: string
}

export default function TopBarNavLink({
to,
isActive,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,15 +63,6 @@ export default function UserMenu() {
<span className="ml-1 line-clamp-1 font-medium">Notifications</span>
</TopBarLinkListItem>

<TopBarLinkListItem
className="rounded-none py-3"
to={paths.settings('app/appearance')}
isActive={location.pathname.startsWith(paths.settings('app/appearance'))}
>
<Bell className="mr-2 h-4 w-4 shrink-0" />
<span className="ml-1 line-clamp-1 font-medium">Preferences</span>
</TopBarLinkListItem>

<TopBarButtonItem className="rounded-none py-3" onClick={logout}>
<LogOut className="mr-2 h-4 w-4 shrink-0" />
Logout
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { NavigationMenu } from '@stump/components'
import { cn, NavigationMenu } from '@stump/components'

import { EntityOptionProps } from '@/components/navigation/types'

Expand All @@ -7,8 +7,8 @@ const IS_DEVELOPMENT = import.meta.env.DEV
// TODO: implement me

type Props = EntityOptionProps
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export default function BookClubNavigationItem(_: Props) {

export default function BookClubNavigationItem({ width }: Props) {
if (!IS_DEVELOPMENT) {
return null
}
Expand All @@ -19,7 +19,14 @@ export default function BookClubNavigationItem(_: Props) {
Book clubs
</NavigationMenu.Trigger>
<NavigationMenu.Content>
<div className="p-4 md:w-[400px]">TODO make me</div>
<div
style={{ width }}
className={cn('flex min-h-[150px] gap-3 p-2', {
'md:w-[400px] lg:w-[500px]': !width,
})}
>
TODO make me
</div>
</NavigationMenu.Content>
</NavigationMenu.Item>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ import TopBarLinkListItem from '../../TopBarLinkListItem'

type Props = EntityOptionProps

export default function LibraryNavigationItem({ showCreate = true, showLinkToAll = false }: Props) {
export default function LibraryNavigationItem({
showCreate = true,
showLinkToAll = false,
width,
}: Props) {
const { libraries } = useLibraries()

const location = useLocation()
Expand Down Expand Up @@ -54,6 +58,7 @@ export default function LibraryNavigationItem({ showCreate = true, showLinkToAll
<TopBarLinkListItem
to={paths.librarySeries(library.id)}
isActive={location.pathname.startsWith(paths.librarySeries(library.id))}
className="h-9"
>
{library.emoji ? (
<span className="mr-2 h-4 w-4 shrink-0">{library.emoji}</span>
Expand All @@ -78,12 +83,12 @@ export default function LibraryNavigationItem({ showCreate = true, showLinkToAll
</NavigationMenu.Trigger>
<NavigationMenu.Content>
<div
className={cn('flex min-h-[150px] gap-3 p-4 md:w-[400px] lg:w-[500px]', {
'md:w-[300px] lg:w-[350px]': !libraries?.length,
style={{ width }}
className={cn('flex min-h-[150px] min-w-[300px] gap-3 p-2', {
'md:w-[400px] lg:w-[500px]': !width,
'md:w-[300px] lg:w-[350px]': !width && !libraries?.length,
})}
>
<LastVisitedLibrary container={(children) => <div className="w-1/3">{children}</div>} />

<div
className={cn('flex w-2/3 shrink-0 flex-col gap-y-2', {
'w-full': !libraries?.length,
Expand All @@ -97,24 +102,26 @@ export default function LibraryNavigationItem({ showCreate = true, showLinkToAll
<TopBarLinkListItem
to={paths.libraryCreate()}
isActive={location.pathname.startsWith(paths.libraryCreate())}
className="shrink-0 justify-center self-end border border-dashed border-edge-subtle py-2.5"
className="justify-center self-end border border-dashed border-edge-subtle p-1"
>
<span className="line-clamp-1 font-medium">Create library</span>
<span className="line-clamp-1 text-sm font-medium">Create library</span>
</TopBarLinkListItem>
)}

{showLinkToAll && (
<TopBarLinkListItem
to={paths.libraries()}
isActive={location.pathname.startsWith(paths.libraries())}
className="shrink-0 justify-center self-end border border-dashed border-edge-subtle py-2.5"
className="justify-center self-end border border-dashed border-edge-subtle p-1"
>
<span className="line-clamp-1 font-medium">See all</span>
<span className="line-clamp-1 text-sm font-medium">See all</span>
</TopBarLinkListItem>
)}
</div>
</div>
</div>

<LastVisitedLibrary container={(children) => <div className="w-1/3">{children}</div>} />
</div>
</NavigationMenu.Content>
</NavigationMenu.Item>
Expand Down
1 change: 1 addition & 0 deletions packages/browser/src/components/navigation/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export type EntityOptionProps = {
showCreate?: boolean
showLinkToAll?: boolean
width?: number
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export default function BookClubSettingsSideBar() {
className={cn(
'relative flex h-full w-48 shrink-0 flex-col border-edge bg-background px-2 py-4 text-foreground-subtle',
primary_navigation_mode === 'TOPBAR'
? 'fixed top-12 z-50 h-screen border-x'
? 'fixed top-12 z-50 h-screen border-r'
: 'fixed top-0 z-50 h-screen border-r',
)}
>
Expand Down
13 changes: 11 additions & 2 deletions packages/browser/src/scenes/library/LibraryLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useLibraryByID, useLibraryStats } from '@stump/client'
import { useLibraryByID, useLibraryStats, useVisitLibrary } from '@stump/client'
import { cn } from '@stump/components'
import { useMemo } from 'react'
import { useMemo, useRef } from 'react'
import { Suspense, useEffect } from 'react'
import { Outlet, useLocation, useNavigate, useParams } from 'react-router'
import { useMediaMatch } from 'rooks'
Expand Down Expand Up @@ -45,6 +45,15 @@ export default function LibraryLayout() {
}
}, [isLoading, library, navigate])

const { visitLibrary } = useVisitLibrary()
const alreadyVisited = useRef(false)
useEffect(() => {
if (library?.id && !alreadyVisited.current) {
alreadyVisited.current = true
visitLibrary(library.id)
}
}, [library?.id, visitLibrary])

const renderHeader = () => (isSettings ? <LibrarySettingsHeader /> : <LibraryHeader />)

if (isLoading || !library) return null
Expand Down
5 changes: 4 additions & 1 deletion packages/browser/src/scenes/library/LibraryNavigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ export default function LibraryNavigation() {
fetchConfig: checkPermission('file:upload'),
})
const { prefetch: prefetchSeries } = usePrefetchLibrarySeries({ id })
const { ref, isSticky } = useSticky<HTMLDivElement>({ extraOffset: isMobile ? 56 : 0 })

const { ref, isSticky } = useSticky<HTMLDivElement>({
extraOffset: isMobile || primary_navigation_mode === 'TOPBAR' ? 56 : 0,
})

const canAccessFiles = checkPermission('file:explorer')
const tabs = useMemo(
Expand Down
Loading

0 comments on commit acd8a24

Please sign in to comment.