1
mirror of https://github.com/jakejarvis/jarv.is.git synced 2026-06-05 19:15:30 -04:00

feat: enhance mobile navigation with simple dropdown menu

This commit is contained in:
2026-01-28 10:57:34 -05:00
parent 7743976db8
commit b80768cab6
8 changed files with 107 additions and 118 deletions
-87
View File
@@ -193,93 +193,6 @@
::selection {
@apply bg-selection text-selection-foreground;
}
::-webkit-scrollbar {
width: 5px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: color-mix(in oklab, var(--primary) 60%, transparent);
border-radius: 5px;
}
* {
scrollbar-width: thin;
scrollbar-color: color-mix(in oklab, var(--primary) 60%, transparent) transparent;
}
}
@layer components {
[data-slot="code-block"] {
@apply bg-code text-code-foreground overflow-x-auto overflow-y-hidden rounded-xl text-sm outline-none;
/* Override shiki inline styles */
& pre {
@apply m-0 rounded-xl !bg-transparent;
}
/* Dark mode token colors - override shiki inline color styles */
& span[style*="color"] {
@apply dark:![color:var(--shiki-dark)];
}
& code {
display: grid;
min-width: 100%;
white-space: pre;
padding: 0.875rem 1rem;
counter-reset: line;
}
& .line {
display: inline-block;
min-height: 1lh;
width: 100%;
padding-block: 0.125rem;
}
/* Highlighted lines */
& .line.highlighted {
position: relative;
background-color: var(--color-code-highlight);
&:after {
position: absolute;
top: 0;
left: 0;
width: 2px;
height: 100%;
content: "";
background-color: color-mix(in oklab, var(--muted-foreground) 50%, transparent);
}
}
/* Highlighted words */
& .highlighted-word {
position: relative;
background-color: var(--color-code-highlight);
border-radius: var(--radius-sm);
padding-inline: 0.3rem;
padding-block: 0.1rem;
}
}
/* Line numbers - only when data-line-numbers is set */
[data-slot="code-block"][data-line-numbers] .line::before {
font-size: var(--text-sm);
counter-increment: line;
content: counter(line);
display: inline-block;
width: 2rem;
margin-right: 1.5rem;
text-align: right;
color: var(--color-code-number);
}
[data-slot="code-block"][data-line-numbers] .line.highlighted::before {
background-color: var(--color-code-highlight);
}
}
/* View Transitions */
+13 -13
View File
@@ -96,25 +96,25 @@ const Page = async ({ params }: { params: Promise<{ slug: string }> }) => {
}}
/>
<div className="text-foreground/70 -mt-1 flex flex-wrap justify-items-start gap-x-4 text-[0.8rem] leading-9 tracking-wide md:text-[0.85rem]">
<div className="text-foreground/70 flex flex-wrap justify-items-start gap-4 text-[13px] tracking-wide">
<Link
href={`/${POSTS_DIR}/${frontmatter!.slug}`}
className={"text-foreground/70 flex flex-nowrap items-center gap-x-2 whitespace-nowrap hover:no-underline"}
className={"text-foreground/70 flex flex-nowrap items-center gap-1.5 whitespace-nowrap hover:no-underline"}
>
<CalendarDaysIcon className="inline size-4 shrink-0" aria-hidden="true" />
<CalendarDaysIcon className="inline size-3 shrink-0" aria-hidden="true" />
<time dateTime={formattedDates.dateISO} title={formattedDates.dateTitle}>
{formattedDates.dateDisplay}
</time>
</Link>
{frontmatter!.tags && (
<div className="flex flex-wrap items-center gap-x-2 whitespace-nowrap">
<TagIcon className="inline size-4 shrink-0" aria-hidden="true" />
<div className="flex flex-wrap items-center gap-1.5">
<TagIcon className="inline size-3 shrink-0" aria-hidden="true" />
{frontmatter!.tags.map((tag) => (
<span
key={tag}
title={tag}
className="before:text-foreground/40 lowercase before:pr-0.5 before:content-['\0023'] last-of-type:mr-0"
className="before:text-foreground/40 mx-px lowercase before:pr-0.5 before:content-['\0023'] first-of-type:ml-0 last-of-type:mr-0"
aria-label={`Tagged with ${tag}`}
>
{tag}
@@ -126,28 +126,28 @@ const Page = async ({ params }: { params: Promise<{ slug: string }> }) => {
<Link
href={`https://github.com/${env.NEXT_PUBLIC_GITHUB_REPO}/blob/main/${POSTS_DIR}/${frontmatter!.slug}/index.mdx`}
title={`Edit "${frontmatter!.title}" on GitHub`}
className={"text-foreground/70 flex flex-nowrap items-center gap-x-2 whitespace-nowrap hover:no-underline"}
className={"text-foreground/70 flex flex-nowrap items-center gap-1.5 whitespace-nowrap hover:no-underline"}
>
<SquarePenIcon className="inline size-4 shrink-0" aria-hidden="true" />
<SquarePenIcon className="inline size-3 shrink-0" aria-hidden="true" />
<span>Improve This Post</span>
</Link>
<Link
href={`/${POSTS_DIR}/${frontmatter!.slug}#comments`}
title={`${Intl.NumberFormat(env.NEXT_PUBLIC_SITE_LOCALE).format(commentCount || 0)} ${commentCount === 1 ? "comment" : "comments"}`}
className="text-foreground/70 flex flex-nowrap items-center gap-x-2 whitespace-nowrap hover:no-underline"
className="text-foreground/70 flex flex-nowrap items-center gap-1.5 whitespace-nowrap hover:no-underline"
>
<MessagesSquareIcon className="inline size-4 shrink-0" aria-hidden="true" />
<MessagesSquareIcon className="inline size-3 shrink-0" aria-hidden="true" />
<span>{Intl.NumberFormat(env.NEXT_PUBLIC_SITE_LOCALE).format(commentCount || 0)}</span>
</Link>
<div className="flex min-w-14 flex-nowrap items-center gap-x-2 whitespace-nowrap">
<EyeIcon className="inline size-4 shrink-0" aria-hidden="true" />
<div className="flex min-w-14 flex-nowrap items-center gap-1.5 whitespace-nowrap">
<EyeIcon className="inline size-3 shrink-0" aria-hidden="true" />
<ViewCounter slug={`${POSTS_DIR}/${frontmatter!.slug}`} />
</div>
</div>
<h1 className="mt-4 mb-5 text-4xl font-semibold tracking-tight sm:text-3xl">
<h1 className="my-5 text-4xl font-semibold tracking-tight sm:text-3xl">
<Link
href={`/${POSTS_DIR}/${frontmatter!.slug}`}
dangerouslySetInnerHTML={{ __html: frontmatter!.htmlTitle || frontmatter!.title }}
+1 -1
View File
@@ -114,7 +114,7 @@ const PostsList = async () => {
href={`/${POSTS_DIR}/${slug}`}
prefetch={false}
dangerouslySetInnerHTML={{ __html: htmlTitle || title }}
className="text-lg font-medium underline-offset-4 hover:underline md:text-base"
className="underline-offset-4 hover:underline"
/>
<PostStats slug={slug} views={views} comments={comments} />
+11 -2
View File
@@ -1,7 +1,8 @@
import { codeToHtml } from "shiki";
import { cacheLife } from "next/cache";
import CopyButton from "@/components/copy-button";
import reactToText from "react-to-text";
import CopyButton from "@/components/copy-button";
import { cn } from "@/lib/utils";
interface CodeBlockProps extends React.ComponentProps<"pre"> {
showLineNumbers?: boolean;
@@ -43,7 +44,15 @@ const CodeBlock = async ({ children, className, showLineNumbers = true, ...props
data-slot="code-block"
data-lang={lang}
data-line-numbers={showLineNumbers || undefined}
className={className}
className={cn(
"bg-code text-code-foreground overflow-x-auto overflow-y-hidden rounded-xl text-[13px] leading-normal outline-none",
"[&_span]:!bg-transparent [&_span[style*='color']]:dark:!text-(--shiki-dark)",
"[&_pre]:m-0 [&_pre]:rounded-xl [&_pre]:!bg-transparent",
"[&_code]:white-space-pre [&_code]:grid [&_code]:min-w-full [&_code]:px-4 [&_code]:py-3.5 [&_code]:[counter-reset:line]",
"[&_.line]:min-h-1lh [&_.line]:inline-block [&_.line]:w-full [&_.line]:py-0.5",
"data-[line-numbers]:[&_.line]:before:text-code-number data-[line-numbers]:[&_.line]:before:mr-6 data-[line-numbers]:[&_.line]:before:inline-block data-[line-numbers]:[&_.line]:before:w-5 data-[line-numbers]:[&_.line]:before:text-right data-[line-numbers]:[&_.line]:before:content-[counter(line)] data-[line-numbers]:[&_.line]:before:[counter-increment:line]",
className
)}
dangerouslySetInnerHTML={{ __html: html }}
/>
</div>
+72 -12
View File
@@ -1,8 +1,15 @@
"use client";
import { useSelectedLayoutSegment } from "next/navigation";
import { ChevronDownIcon } from "lucide-react";
import Button from "@/components/ui/button";
import Link from "@/components/link";
import {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
} from "@/components/ui/dropdown-menu";
const menuItems = [
{
@@ -22,20 +29,73 @@ const menuItems = [
const Menu = () => {
const segment = useSelectedLayoutSegment() || "";
return (
<div className="flex items-center gap-2">
{menuItems.map((item, index) => {
const isCurrent = item.href?.split("/")[1] === segment;
const currentItem = menuItems.find((item) => item.href?.split("/")[1] === segment);
const currentLabel = segment === "" ? "Home" : currentItem?.text || "Menu";
return (
<Button key={index} variant="ghost" size="sm" asChild>
<Link href={item.href} prefetch={false} aria-label={item.text} data-current={isCurrent}>
{item.text}
</Link>
return (
<nav data-slot="navigation-menu">
{/* Desktop: Show all buttons */}
<div className="hidden items-center gap-1.5 sm:flex">
{menuItems.map((item, index) => {
const isCurrent = item.href?.split("/")[1] === segment;
return (
<Button
asChild
key={index}
variant="ghost"
size="sm"
aria-label={item.text}
data-current={isCurrent || undefined}
className="data-current:bg-accent/60 data-current:text-accent-foreground"
>
<Link href={item.href} prefetch={false}>
{item.text}
</Link>
</Button>
);
})}
</div>
{/* Mobile: Show dropdown menu */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" className="flex sm:hidden">
{currentLabel}
<ChevronDownIcon className="size-3.5 opacity-60 transition-transform duration-200 data-[state=open]:rotate-180" />
</Button>
);
})}
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="min-w-[140px]">
<DropdownMenuItem asChild>
<Link
href="/"
prefetch={false}
data-current={segment === ""}
aria-current={segment === "" ? "page" : undefined}
>
Home
</Link>
</DropdownMenuItem>
{menuItems.map((item, index) => {
const isCurrent = item.href?.split("/")[1] === segment;
return (
<DropdownMenuItem
asChild
key={index}
className="data-current:bg-accent/40 data-current:text-accent-foreground data-current:font-medium"
data-current={isCurrent || undefined}
aria-current={isCurrent ? "page" : undefined}
>
<Link href={item.href} prefetch={false}>
{item.text}
</Link>
</DropdownMenuItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
</nav>
);
};
+1 -1
View File
@@ -16,7 +16,7 @@ const PageTitle = ({
>
<Link
href={canonical}
className="before:text-muted-foreground before:-mr-0.5 before:tracking-widest before:content-['\002E\002F'] hover:no-underline"
className="before:text-muted-foreground no-underline before:-mr-1 before:tracking-wider before:content-['\002E\002F']"
>
{children}
</Link>
+1 -1
View File
@@ -55,7 +55,7 @@ const DropdownMenuItem = ({
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...rest}
+8 -1
View File
@@ -160,7 +160,14 @@ const nextPlugins: Array<
[
"rehype-wrapper",
{
className: "prose prose-sm prose-neutral dark:prose-invert max-w-none",
className: [
"prose prose-sm prose-neutral dark:prose-invert",
"prose-headings:font-semibold prose-headings:tracking-tight",
"prose-h2:border-b prose-h2:pb-2",
"prose-a:underline-offset-4",
"prose-blockquote:**:before:content-none prose-blockquote:**:after:content-none prose-blockquote:text-(--tw-prose-body)",
"max-w-none",
].join(" "),
},
],
"rehype-mdx-code-props",