Skip to content

Commit

Permalink
Conditionally show page title
Browse files Browse the repository at this point in the history
  • Loading branch information
colebemis committed Dec 13, 2024
1 parent c7817fc commit d66b3b9
Show file tree
Hide file tree
Showing 4 changed files with 62 additions and 78 deletions.
40 changes: 5 additions & 35 deletions src/components/app-layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { useAtomValue } from "jotai"
import { useEffect, useRef, useState } from "react"
import { LoadingIcon16 } from "../components/icons"
import { RepoForm } from "../components/repo-form"
import {
Expand All @@ -10,10 +9,11 @@ import {
isSignedOutAtom,
sidebarAtom,
} from "../global-state"
import { useIsScrolled } from "../hooks/is-scrolled"
import { cx } from "../utils/cx"
import { AppHeader, AppHeaderProps } from "./app-header"
import { Sidebar } from "./sidebar"
import { NavBar } from "./nav-bar"
import { Sidebar } from "./sidebar"

type AppLayoutProps = AppHeaderProps & {
className?: string
Expand All @@ -35,33 +35,7 @@ export function AppLayout({
const githubRepo = useAtomValue(githubRepoAtom)
const sidebar = useAtomValue(sidebarAtom)

const [isScrolled, setIsScrolled] = useState(false)
const scrollContainerRef = useRef<HTMLElement>(null)
// Reference to an invisible element at the top of the content
// Used to detect when content is scrolled
const topSentinelRef = useRef<HTMLDivElement>(null)

useEffect(() => {
const sentinel = topSentinelRef.current
if (!sentinel) return

// Create an IntersectionObserver that watches when our sentinel element
// enters or leaves the viewport
const observer = new IntersectionObserver(
([entry]) => {
// When sentinel is visible (intersecting), we're at the top (not scrolled)
// When it's not visible, we've scrolled down
setIsScrolled(!entry.isIntersecting)
},
{
// Only trigger when sentinel is fully visible/hidden
threshold: 1.0,
},
)

observer.observe(sentinel)
return () => observer.disconnect()
}, [])
const { isScrolled, topSentinelProps } = useIsScrolled()

return (
<div className="flex overflow-hidden">
Expand All @@ -76,12 +50,8 @@ export function AppLayout({
actions={isRepoCloned || isSignedOut || disableGuard ? actions : undefined}
className={cx("border-b", isScrolled ? "border-border-secondary" : "border-transparent")}
/>
<main ref={scrollContainerRef} className="relative overflow-auto [scrollbar-gutter:stable]">
{/* Invisible sentinel element that helps detect scroll position */}
<div
ref={topSentinelRef}
className="pointer-events-none absolute inset-x-0 top-0 h-[1px]"
/>
<main className="relative overflow-auto [scrollbar-gutter:stable]">
<div {...topSentinelProps} />
{isRepoNotCloned && !disableGuard ? (
<div className="flex h-full flex-col items-center">
<div className="mx-auto w-full max-w-lg p-4">
Expand Down
41 changes: 4 additions & 37 deletions src/components/sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useSetAtom } from "jotai"
import { useEffect, useRef, useState } from "react"
import { sidebarAtom } from "../global-state"
import { useIsScrolled } from "../hooks/is-scrolled"
import { cx } from "../utils/cx"
import { IconButton } from "./icon-button"
import { SidebarFillIcon16 } from "./icons"
Expand All @@ -9,34 +9,7 @@ import { NavItems } from "./nav-items"
export function Sidebar() {
const setSidebar = useSetAtom(sidebarAtom)

// const { isScrolled, scrollContainerRef, topSentinelRef } = useIsScrolled()
const [isScrolled, setIsScrolled] = useState(false)
const scrollContainerRef = useRef<HTMLDivElement>(null)
// Reference to an invisible element at the top of the content
// Used to detect when content is scrolled
const topSentinelRef = useRef<HTMLDivElement>(null)

useEffect(() => {
const sentinel = topSentinelRef.current
if (!sentinel) return

// Create an IntersectionObserver that watches when our sentinel element
// enters or leaves the viewport
const observer = new IntersectionObserver(
([entry]) => {
// When sentinel is visible (intersecting), we're at the top (not scrolled)
// When it's not visible, we've scrolled down
setIsScrolled(!entry.isIntersecting)
},
{
// Only trigger when sentinel is fully visible/hidden
threshold: 1.0,
},
)

observer.observe(sentinel)
return () => observer.disconnect()
}, [])
const { isScrolled, topSentinelProps } = useIsScrolled()

return (
<div className="grid w-56 flex-shrink-0 grid-rows-[auto_1fr] overflow-hidden border-r border-border-secondary">
Expand All @@ -55,14 +28,8 @@ export function Sidebar() {
<SidebarFillIcon16 />
</IconButton>
</div>
<div
ref={scrollContainerRef}
className="relative flex scroll-py-2 flex-col gap-2 overflow-auto p-2 pt-0"
>
<div
ref={topSentinelRef}
className="pointer-events-none absolute inset-x-0 top-0 h-[1px]"
/>
<div className="relative flex scroll-py-2 flex-col gap-2 overflow-auto p-2 pt-0">
<div {...topSentinelProps} />
<NavItems />
</div>
</div>
Expand Down
39 changes: 39 additions & 0 deletions src/hooks/is-scrolled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { useState, useRef, useEffect, useMemo } from "react"

export function useIsScrolled() {
const [isScrolled, setIsScrolled] = useState(false)
// Reference to an invisible element at the top of the content
// Used to detect when content is scrolled
const topSentinelRef = useRef<HTMLDivElement>(null)

useEffect(() => {
const sentinel = topSentinelRef.current
if (!sentinel) return

// Create an IntersectionObserver that watches when our sentinel element
// enters or leaves the viewport
const observer = new IntersectionObserver(
([entry]) => {
// When sentinel is visible (intersecting), we're at the top (not scrolled)
// When it's not visible, we've scrolled down
setIsScrolled(!entry.isIntersecting)
},
{
// Only trigger when sentinel is fully visible/hidden
threshold: 1.0,
},
)

observer.observe(sentinel)
return () => observer.disconnect()
}, [])

const topSentinelProps = useMemo(() => {
return {
ref: topSentinelRef,
className: "pointer-events-none absolute inset-x-0 top-0 h-[1px]",
}
}, [])

return { isScrolled, topSentinelProps }
}
20 changes: 14 additions & 6 deletions src/routes/notes_.$.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import {
FullwidthIcon16,
MoreIcon16,
NoteIcon16,
PinFillIcon12,
PinFillIcon16,
PinIcon16,
ShareIcon16,
Expand All @@ -49,6 +48,7 @@ import {
widthAtom,
} from "../global-state"
import { useEditorSettings } from "../hooks/editor-settings"
import { useIsScrolled } from "../hooks/is-scrolled"
import { useDeleteNote, useNoteById, useSaveNote } from "../hooks/note"
import { useSearchNotes } from "../hooks/search"
import { GitHubRepository, Note, NoteId, Template } from "../schema"
Expand Down Expand Up @@ -312,18 +312,25 @@ function NotePage() {
})
}, [getHandleSave, getEditorValue, getSwitchToReading, getIsDirty])

const { isScrolled, topSentinelProps } = useIsScrolled()

const shouldShowPageTitle =
parsedNote.displayName !== "Untitled note" && (isScrolled || !parsedNote.title)

return (
<AppLayout
title={
<span className="flex items-center gap-2">
{parsedNote.pinned ? <PinFillIcon12 className="text-[var(--orange-11)]" /> : null}
<span className={cx("truncate", !note ? "italic text-text-secondary" : "")}>
<PageTitle note={parsedNote} />
</span>
{/* {parsedNote.pinned ? <PinFillIcon12 className="text-[var(--orange-11)]" /> : null} */}
{shouldShowPageTitle ? (
<span className={cx("truncate", !note ? "italic text-text-secondary" : "")}>
<PageTitle note={parsedNote} />
</span>
) : null}
{isDirty ? <DotIcon8 className="text-[var(--amber-11)]" /> : null}
</span>
}
icon={<NoteFavicon note={parsedNote} />}
icon={shouldShowPageTitle ? <NoteFavicon note={parsedNote} /> : null}
actions={
<div className="flex items-center gap-2">
{!note || isDirty ? (
Expand Down Expand Up @@ -505,6 +512,7 @@ function NotePage() {
}
}}
>
<div {...topSentinelProps} />
<div className="p-4">
<div
className={cx(
Expand Down

0 comments on commit d66b3b9

Please sign in to comment.