ui enhancements
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
/**
|
||||
* SiteBottomNav — mobile-only bottom tab bar.
|
||||
*
|
||||
* Four items: Home (Feed) · Knowledgebase · Search · Account.
|
||||
* Four items: Home · Search · Kautilya · Account.
|
||||
* Mirrors the Instagram pattern — fixed to the viewport bottom, full
|
||||
* width, equal-width tabs, the active tab uses the primary color.
|
||||
*
|
||||
@@ -14,11 +14,13 @@ import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { Home, Search, User as UserIcon } from "lucide-react";
|
||||
import { useAuth } from "@/components/auth/auth-provider";
|
||||
import { KautilyaIcon } from "@/components/selection-toolbar/shared";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type Item = {
|
||||
href: string;
|
||||
/** Tab label, used for the aria-label. Bottom nav is icon-only. */
|
||||
label: string;
|
||||
href: string;
|
||||
icon: React.ReactNode;
|
||||
matchPrefixes?: string[];
|
||||
};
|
||||
@@ -48,6 +50,11 @@ export function SiteBottomNav() {
|
||||
"/disciples",
|
||||
],
|
||||
},
|
||||
{
|
||||
href: "/chat",
|
||||
label: "Kautilya",
|
||||
icon: <KautilyaIcon className="size-5" />,
|
||||
},
|
||||
{
|
||||
href: user ? "/u/me" : "/login",
|
||||
label: user ? "You" : "Sign in",
|
||||
@@ -79,7 +86,7 @@ export function SiteBottomNav() {
|
||||
{items.map((item) => {
|
||||
const active = isActive(item);
|
||||
return (
|
||||
<li key={item.href} className="flex-1">
|
||||
<li key={item.label} className="flex-1">
|
||||
<Link
|
||||
href={item.href}
|
||||
aria-label={item.label}
|
||||
@@ -92,9 +99,6 @@ export function SiteBottomNav() {
|
||||
)}
|
||||
>
|
||||
{item.icon}
|
||||
<span className="text-[0.65rem] font-medium">
|
||||
{item.label}
|
||||
</span>
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
|
||||
@@ -6,12 +6,12 @@
|
||||
* Layout (collapsed, w-16):
|
||||
*
|
||||
* ┌──┐
|
||||
* │🏠│ ← top group (Home / Knowledgebase / Collections /
|
||||
* │📚│ Bookmarks / Mentions / Search)
|
||||
* │🏠│ ← top group (Home / Search / Kautilya /
|
||||
* │🔍│ Collections / Bookmarks / Mentions)
|
||||
* │✦ │
|
||||
* │📁│
|
||||
* │🔖│
|
||||
* │@ │
|
||||
* │🔍│
|
||||
* │ │
|
||||
* │⋮ │ ← bottom group (Reading settings, Profile)
|
||||
* │👤│
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
} from "lucide-react";
|
||||
import { useAuth } from "@/components/auth/auth-provider";
|
||||
import { ReadingSettings } from "@/components/reading-settings";
|
||||
import { KautilyaIcon } from "@/components/selection-toolbar/shared";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type NavItem = {
|
||||
@@ -73,6 +74,11 @@ const TOP_ITEMS: NavItem[] = [
|
||||
"/disciples",
|
||||
],
|
||||
},
|
||||
{
|
||||
href: "/chat",
|
||||
label: "Kautilya",
|
||||
icon: <KautilyaIcon className="size-5" />,
|
||||
},
|
||||
{
|
||||
href: "/u/me/collections",
|
||||
label: "Collections",
|
||||
|
||||
@@ -16,13 +16,24 @@
|
||||
*/
|
||||
|
||||
import { useState } from "react";
|
||||
import { FolderPlus, Heart, MessageCircle, Repeat2 } from "lucide-react";
|
||||
import {
|
||||
HiOutlineArrowPath,
|
||||
HiOutlineChatBubbleOvalLeft,
|
||||
HiOutlineFolderPlus,
|
||||
HiOutlineHeart,
|
||||
} from "react-icons/hi2";
|
||||
import { toast } from "sonner";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useAuthGate } from "@/components/auth/auth-gate";
|
||||
import { useMediaQuery } from "@/components/selection-toolbar/shared";
|
||||
import { CollectionsDrawer } from "@/components/selection-toolbar/collections-drawer";
|
||||
|
||||
// All four icons come from the Heroicons v2 outline family (react-icons
|
||||
// `hi2` namespace), so they share one design language — uniform stroke
|
||||
// weight, matching corner radius, matching optical sizing. No more
|
||||
// Lucide cross-icon visual drift.
|
||||
const ICON_CLASS = "size-[1.375rem]";
|
||||
|
||||
export function PostActionsFooter({ postID }: { postID: string }) {
|
||||
const { requireAuth } = useAuthGate();
|
||||
const isMobile = useMediaQuery("(max-width: 767px)");
|
||||
@@ -44,7 +55,7 @@ export function PostActionsFooter({ postID }: { postID: string }) {
|
||||
toast("Likes are coming soon", { duration: 2000 });
|
||||
})
|
||||
}
|
||||
icon={<Heart className="size-5" />}
|
||||
icon={<HiOutlineHeart className={ICON_CLASS} />}
|
||||
/>
|
||||
<ActionButton
|
||||
label="Comment"
|
||||
@@ -53,7 +64,7 @@ export function PostActionsFooter({ postID }: { postID: string }) {
|
||||
toast("Comments are coming soon", { duration: 2000 });
|
||||
})
|
||||
}
|
||||
icon={<MessageCircle className="size-5" />}
|
||||
icon={<HiOutlineChatBubbleOvalLeft className={ICON_CLASS} />}
|
||||
/>
|
||||
<ActionButton
|
||||
label="Reshare"
|
||||
@@ -62,13 +73,13 @@ export function PostActionsFooter({ postID }: { postID: string }) {
|
||||
toast("Resharing is coming soon", { duration: 2000 });
|
||||
})
|
||||
}
|
||||
icon={<Repeat2 className="size-5" />}
|
||||
icon={<HiOutlineArrowPath className={ICON_CLASS} />}
|
||||
/>
|
||||
</div>
|
||||
<ActionButton
|
||||
label="Save to collection"
|
||||
onClick={() => withAuth(() => setCollectionsOpen(true))}
|
||||
icon={<FolderPlus className="size-5" />}
|
||||
icon={<HiOutlineFolderPlus className={ICON_CLASS} />}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
* always visible on touch (mobile).
|
||||
*/
|
||||
|
||||
import { useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { ImageIcon, Trash2 } from "lucide-react";
|
||||
@@ -248,22 +248,46 @@ function Caption({
|
||||
expanded: boolean;
|
||||
onToggle: () => void;
|
||||
}) {
|
||||
// Show 3 lines collapsed. The "more" toggle appears only when the text
|
||||
// is long enough to warrant it (a cheap char-length check beats a
|
||||
// measure-and-compare pass for our purposes — captions over ~180 chars
|
||||
// will always overflow 3 lines at the rendered font size).
|
||||
const longEnough = text.length > 180;
|
||||
// We clamp to 3 lines collapsed and expose a "More" toggle only when
|
||||
// the clamp is actually hiding something. A char-length heuristic gets
|
||||
// this wrong: a 200-char caption can fit cleanly in 3 lines on a wide
|
||||
// card (no clipping → "More" would be a false promise). Instead measure
|
||||
// scrollHeight vs clientHeight on the clamped <p>. Once we've observed
|
||||
// overflow we latch it on so "Show less" stays visible after expansion
|
||||
// (where scrollHeight would equal clientHeight and we'd otherwise lose
|
||||
// the way back).
|
||||
const pRef = useRef<HTMLParagraphElement | null>(null);
|
||||
const [overflowed, setOverflowed] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (expanded) return;
|
||||
const el = pRef.current;
|
||||
if (!el) return;
|
||||
const measure = () => {
|
||||
// Sub-pixel rounding can leave a 1px gap even when content fits;
|
||||
// require a real difference before flagging overflow.
|
||||
setOverflowed(el.scrollHeight - el.clientHeight > 1);
|
||||
};
|
||||
measure();
|
||||
const ro = new ResizeObserver(measure);
|
||||
ro.observe(el);
|
||||
return () => ro.disconnect();
|
||||
}, [text, expanded]);
|
||||
|
||||
const showToggle = overflowed || expanded;
|
||||
|
||||
return (
|
||||
<div className="text-sm text-foreground leading-relaxed">
|
||||
<p
|
||||
ref={pRef}
|
||||
className={cn(
|
||||
"font-[family-name:var(--font-title)] break-words",
|
||||
!expanded && longEnough && "line-clamp-3",
|
||||
!expanded && "line-clamp-3",
|
||||
)}
|
||||
>
|
||||
“{text}”
|
||||
</p>
|
||||
{longEnough ? (
|
||||
{showToggle ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onToggle}
|
||||
|
||||
Reference in New Issue
Block a user