first commit

This commit is contained in:
2026-07-17 21:48:37 +05:30
commit f85599dac5
124 changed files with 10775 additions and 0 deletions
+100
View File
@@ -0,0 +1,100 @@
"use client";
import { useEffect, useState } from "react";
import { useFeatureFlags, useSdk, useTheme, useNotifications } from "@lynkd/messaging-inbox-sdk";
import { useUi } from "@/lib/ui-state";
import { Avatar, IconButton } from "@/components/ui/primitives";
import { Search, Sun, Moon, Bell, ChevronDown, PanelLeft } from "lucide-react";
export function Header({
title,
subtitle,
eyebrow,
children,
}: {
title: React.ReactNode;
subtitle?: React.ReactNode;
eyebrow?: string;
children?: React.ReactNode;
}) {
const flags = useFeatureFlags();
const { me } = useSdk();
const { scheme, toggleScheme } = useTheme();
const { setPaletteOpen, setNotifOpen, openProfile, setSidebarOpen, sidebarCollapsed, toggleSidebarCollapsed } = useUi();
const { unread } = useNotifications();
const [mac, setMac] = useState(true);
useEffect(() => {
setMac(/mac|iphone|ipad/i.test(navigator.platform || navigator.userAgent));
}, []);
return (
<header className="flex h-[60px] shrink-0 items-center gap-3 border-b border-border bg-surface/80 px-4 backdrop-blur">
<button
className="focus-ring grid h-9 w-9 place-items-center rounded-control text-muted hover:bg-hover md:hidden"
onClick={() => setSidebarOpen(true)}
aria-label="Open sidebar"
>
<PanelLeft size={18} />
</button>
{sidebarCollapsed && (
<button
className="focus-ring hidden h-9 w-9 place-items-center rounded-control text-muted hover:bg-hover hover:text-ink md:grid"
onClick={toggleSidebarCollapsed}
aria-label="Expand sidebar"
title="Expand sidebar"
>
<PanelLeft size={18} />
</button>
)}
<div className="min-w-0 flex-1">
{eyebrow && <div className="text-[11px] font-semibold uppercase tracking-wide text-dim">{eyebrow}</div>}
<div className="flex items-center gap-2">
<h1 className="truncate text-[17px] font-bold leading-tight">{title}</h1>
</div>
{subtitle && <p className="truncate text-[12.5px] text-muted">{subtitle}</p>}
</div>
<div className="flex items-center gap-1.5">
{children}
{flags.search && (
<button
onClick={() => setPaletteOpen(true)}
className="focus-ring hidden items-center gap-2 rounded-control border border-border bg-panel px-3 py-2 text-[13px] text-muted transition-colors hover:bg-hover hover:text-ink sm:flex"
>
<Search size={15} />
<span>Search</span>
<kbd suppressHydrationWarning className="rounded bg-hover px-1.5 py-0.5 font-mono text-[10px] text-dim">{mac ? "⌘K" : "Ctrl K"}</kbd>
</button>
)}
{flags.themeToggle && (
<IconButton label="Toggle theme" onClick={toggleScheme}>
{scheme === "dark" ? <Sun size={18} /> : <Moon size={18} />}
</IconButton>
)}
{flags.notifications && (
<IconButton label="Notifications" onClick={() => setNotifOpen(true)} badge={unread}>
<Bell size={18} />
</IconButton>
)}
{me && (
<button
onClick={() => openProfile(me.id)}
className="focus-ring ml-1 flex items-center gap-2 rounded-control p-1 pr-2 hover:bg-hover"
>
<Avatar user={me} size={32} showPresence={false} />
<span className="hidden text-left lg:block">
<span className="block text-[13px] font-semibold leading-tight">{me.name}</span>
<span className="block text-[11px] text-muted">{me.title}</span>
</span>
<ChevronDown size={15} className="hidden text-muted lg:block" />
</button>
)}
</div>
</header>
);
}
+273
View File
@@ -0,0 +1,273 @@
"use client";
import { useState } from "react";
import { usePathname, useRouter } from "next/navigation";
import clsx from "clsx";
import {
useChannels,
useFeatureFlags,
useInbox,
useSdk,
type Channel,
} from "@lynkd/messaging-inbox-sdk";
import { useUi } from "@/lib/ui-state";
import { Avatar, CountBadge } from "@/components/ui/primitives";
import {
Hash,
Lock,
ChevronDown,
ChevronRight,
Plus,
MessageSquarePlus,
Send,
Bookmark,
MessagesSquare,
Inbox as InboxIcon,
Sliders,
Circle,
PanelLeftClose,
} from "lucide-react";
export function Sidebar() {
const { starred, channels, dms } = useChannels();
const { me, brandName, channelById } = useSdk();
const flags = useFeatureFlags();
const router = useRouter();
const pathname = usePathname();
const { openProfile, setThemingOpen, openModal, toggleSidebarCollapsed } = useUi();
const { items: openInbox } = useInbox("OPEN");
const quick = [
flags.threads && { icon: MessagesSquare, label: "Threads", href: "/threads" },
flags.inbox && { icon: InboxIcon, label: "Inbox", href: "/inbox", badge: openInbox.length },
flags.drafts && { icon: Send, label: "Drafts & sent", href: "/drafts" },
flags.savedItems && { icon: Bookmark, label: "Saved", href: "/saved" },
].filter(Boolean) as { icon: typeof Hash; label: string; href: string; badge?: number }[];
return (
<aside className="flex h-full w-[264px] shrink-0 flex-col border-r border-border bg-surface">
{/* workspace header */}
<div className="flex items-center gap-2 px-3.5 py-3">
<div className="brand-mark grid h-8 w-8 shrink-0 place-items-center rounded-[10px] text-sm font-black text-black/80">
{brandName.slice(0, 1)}
</div>
<div className="min-w-0 flex-1">
<div className="truncate text-[15px] font-bold leading-tight">{brandName}</div>
<div className="flex items-center gap-1 text-[11px] text-muted">
<Circle size={7} className="fill-success text-success" /> Business+
</div>
</div>
<button
title="New message"
onClick={() => openModal("new-message")}
className="focus-ring grid h-8 w-8 shrink-0 place-items-center rounded-control border border-border bg-panel text-ink transition-colors hover:bg-hover"
>
<MessageSquarePlus size={16} />
</button>
<button
title="Collapse sidebar"
onClick={toggleSidebarCollapsed}
className="focus-ring hidden h-8 w-8 shrink-0 place-items-center rounded-control text-muted transition-colors hover:bg-hover hover:text-ink md:grid"
>
<PanelLeftClose size={16} />
</button>
</div>
{/* quick nav */}
<div className="px-2 pb-1">
{quick.map((q) => {
const active = pathname === q.href;
return (
<button
key={q.href}
onClick={() => router.push(q.href)}
className={clsx(
"focus-ring flex w-full items-center gap-2.5 rounded-control px-2.5 py-[7px] text-[14px] transition-colors",
active ? "bg-accent-soft font-semibold text-accent" : "text-muted hover:bg-hover hover:text-ink",
)}
>
<q.icon size={17} />
<span className="flex-1 text-left">{q.label}</span>
{q.badge ? <CountBadge count={q.badge} /> : null}
</button>
);
})}
</div>
<div className="mx-3 my-1.5 h-px bg-border" />
{/* channel lists */}
<div className="min-h-0 flex-1 overflow-y-auto px-2 pb-3">
{flags.messaging && starred.length > 0 && (
<ChannelGroup title="Starred" defaultOpen items={starred} activePath={pathname} onOpen={(c) => router.push(`/c/${c.id}`)} me={me?.id} channelById={channelById} />
)}
{flags.messaging && (
<ChannelGroup
title="Channels"
defaultOpen
items={channels}
activePath={pathname}
onOpen={(c) => router.push(`/c/${c.id}`)}
footer={
flags.channelBrowser ? (
<SidebarAction icon={Plus} label="Add channels" onClick={() => openModal("create-channel")} />
) : null
}
me={me?.id}
channelById={channelById}
/>
)}
{flags.directMessages && (
<ChannelGroup
title="Direct messages"
defaultOpen
items={dms}
activePath={pathname}
onOpen={(c) => router.push(`/c/${c.id}`)}
footer={<SidebarAction icon={Plus} label="Invite people" onClick={() => openModal("invite")} />}
me={me?.id}
channelById={channelById}
/>
)}
</div>
{/* user card footer */}
{me && (
<div className="flex items-center gap-2.5 border-t border-border px-3 py-2.5">
<button
className="focus-ring flex min-w-0 flex-1 items-center gap-2.5 rounded-control p-1 text-left hover:bg-hover"
onClick={() => openProfile(me.id)}
>
<Avatar user={me} size={34} />
<div className="min-w-0 flex-1">
<div className="truncate text-[13px] font-semibold">{me.name}</div>
<div className="truncate text-[11px] text-muted">
{me.statusEmoji} {me.statusText ?? "Active"}
</div>
</div>
</button>
{flags.themingPanel && (
<button
title="Theme & appearance"
onClick={() => setThemingOpen(true)}
className="focus-ring grid h-8 w-8 place-items-center rounded-control text-muted hover:bg-hover hover:text-ink"
>
<Sliders size={16} />
</button>
)}
</div>
)}
</aside>
);
}
function ChannelGroup({
title,
items,
activePath,
onOpen,
footer,
defaultOpen,
me,
channelById,
}: {
title: string;
items: Channel[];
activePath: string;
onOpen: (c: Channel) => void;
footer?: React.ReactNode;
defaultOpen?: boolean;
me?: string;
channelById: (id: string) => Channel | undefined;
}) {
const [open, setOpen] = useState(defaultOpen ?? true);
return (
<div className="mb-1.5">
<button
onClick={() => setOpen((o) => !o)}
className="focus-ring flex w-full items-center gap-1 rounded px-2 py-1 text-[12px] font-bold uppercase tracking-wide text-dim hover:text-muted"
>
{open ? <ChevronDown size={13} /> : <ChevronRight size={13} />}
{title}
</button>
{open && (
<div className="mt-0.5">
{items.map((c) => (
<ChannelRow key={c.id} channel={c} active={activePath === `/c/${c.id}`} onOpen={() => onOpen(c)} meId={me} />
))}
{footer}
</div>
)}
</div>
);
}
function ChannelRow({
channel,
active,
onOpen,
meId,
}: {
channel: Channel;
active: boolean;
onOpen: () => void;
meId?: string;
}) {
const { userById } = useSdk();
const unread = channel.unreadCount ?? 0;
const isDm = channel.kind === "dm";
const other = isDm ? channel.memberIds.find((id) => id !== meId) : undefined;
const otherUser = other ? userById(other) : undefined;
return (
<button
onClick={onOpen}
className={clsx(
"focus-ring group flex w-full items-center gap-2 rounded-control px-2.5 py-[6px] text-[14px] transition-colors",
active
? "bg-accent-soft font-semibold text-accent"
: unread
? "font-semibold text-ink hover:bg-hover"
: "text-muted hover:bg-hover hover:text-ink",
)}
>
<span className="grid w-4 shrink-0 place-items-center">
{channel.kind === "channel" && <Hash size={15} />}
{channel.kind === "private" && <Lock size={13} />}
{channel.kind === "group_dm" && <MessagesSquare size={14} />}
{isDm && otherUser && <Avatar user={otherUser} size={18} showPresence rounded="6px" />}
</span>
<span className="flex-1 truncate text-left">
{channel.name}
{channel.external && <span className="ml-1 text-[10px] text-dim"></span>}
</span>
{channel.mentionCount ? (
<CountBadge count={channel.mentionCount} tone="danger" />
) : unread ? (
<CountBadge count={unread} />
) : null}
</button>
);
}
function SidebarAction({
icon: Icon,
label,
onClick,
}: {
icon: typeof Hash;
label: string;
onClick: () => void;
}) {
return (
<button
onClick={onClick}
className="focus-ring flex w-full items-center gap-2 rounded-control px-2.5 py-[6px] text-[13px] text-muted transition-colors hover:bg-hover hover:text-ink"
>
<span className="grid h-[18px] w-[18px] place-items-center rounded bg-hover">
<Icon size={13} />
</span>
{label}
</button>
);
}
+92
View File
@@ -0,0 +1,92 @@
"use client";
import { useState } from "react";
import { usePathname, useRouter } from "next/navigation";
import { useSdk, useFeatureFlags } from "@lynkd/messaging-inbox-sdk";
import { useUi } from "@/lib/ui-state";
import { IconButton } from "@/components/ui/primitives";
import { Home, MessageSquare, Inbox as InboxIcon, Bell, LifeBuoy, MoreHorizontal, Plus } from "lucide-react";
import clsx from "clsx";
/** Slack-style far-left rail: workspace switcher + primary destinations. */
export function WorkspaceRail() {
const { bootstrap, brandName } = useSdk();
const flags = useFeatureFlags();
const router = useRouter();
const pathname = usePathname();
const { setNotifOpen, openModal, toast } = useUi();
const workspaces = bootstrap?.workspaces ?? [];
const [activeWs, setActiveWs] = useState<string | undefined>(bootstrap?.workspace?.id);
const nav = [
{ icon: Home, label: "Home", href: "/", match: (p: string) => p === "/" || p.startsWith("/c/") },
flags.messaging && { icon: MessageSquare, label: "DMs", href: "/dms", match: (p: string) => p.startsWith("/dms") },
flags.inbox && { icon: InboxIcon, label: "Inbox", href: "/inbox", match: (p: string) => p.startsWith("/inbox") },
flags.support && { icon: LifeBuoy, label: "Support", href: "/support", match: (p: string) => p.startsWith("/support") },
].filter(Boolean) as { icon: typeof Home; label: string; href: string; match: (p: string) => boolean }[];
const active = activeWs ?? bootstrap?.workspace?.id;
return (
<nav className="hidden w-[68px] shrink-0 flex-col items-center gap-1 border-r border-border bg-bg py-3 md:flex">
{flags.workspaceSwitcher && (
<div className="flex flex-col items-center gap-2 pb-2">
{workspaces.map((w) => (
<button
key={w.id}
title={w.name}
onClick={() => {
setActiveWs(w.id);
if (w.id !== bootstrap?.workspace?.id) toast(`Switched to ${w.name} (demo)`, "success");
}}
className={clsx(
"focus-ring grid h-11 w-11 place-items-center rounded-[14px] text-sm font-bold transition-all",
w.id === active ? "text-white ring-2 ring-accent" : "text-ink/80 hover:rounded-[12px]",
)}
style={{ background: w.id === active ? "var(--brand-gradient)" : "var(--c-elevated)" }}
>
{w.initials}
</button>
))}
<IconButton label="Add workspace" onClick={() => openModal("create-workspace")}>
<Plus size={18} />
</IconButton>
<div className="my-1 h-px w-8 bg-border" />
</div>
)}
<div className="flex min-h-0 flex-1 flex-col items-center gap-1 overflow-y-auto no-scrollbar">
{nav.map((n) => {
const isActive = n.match(pathname);
return (
<button
key={n.href}
title={n.label}
onClick={() => router.push(n.href)}
className={clsx(
"focus-ring flex h-12 w-12 flex-col items-center justify-center gap-0.5 rounded-control text-[10px] font-medium transition-colors",
isActive ? "bg-accent-soft text-accent" : "text-muted hover:bg-hover hover:text-ink",
)}
>
<n.icon size={20} />
<span>{n.label}</span>
</button>
);
})}
</div>
<div className="flex flex-col items-center gap-1">
{flags.notifications && (
<IconButton label="Notifications" onClick={() => setNotifOpen(true)}>
<Bell size={20} />
</IconButton>
)}
<IconButton label="Help & more" onClick={() => toast("Help center — coming soon", "default")}>
<MoreHorizontal size={20} />
</IconButton>
</div>
<span className="sr-only">{brandName}</span>
</nav>
);
}