ui enhancements

This commit is contained in:
gosangam
2026-05-31 00:26:05 +05:30
parent dcdd2383c6
commit ec20eb996b
16 changed files with 951 additions and 9317 deletions
+10 -6
View File
@@ -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>
);
+9 -3
View File
@@ -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 -5
View File
@@ -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>
+32 -8
View File
@@ -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}