441 lines
19 KiB
JavaScript
441 lines
19 KiB
JavaScript
// ============================================================
|
|
// app-chrome.jsx — three reusable in-product nav shells.
|
|
// Each exposes `<children>` as the main content slot so page
|
|
// bodies (Customer/Dashboard/Admin) can be dropped into any
|
|
// nav style.
|
|
//
|
|
// All three share the invented brand "Lattice" + same workspace
|
|
// name + same user, so swapping the chrome reads as one product
|
|
// in three nav layouts.
|
|
// ============================================================
|
|
|
|
// ── Tiny stroke-icon helper ─────────────────────────────────
|
|
const Icon = ({ d, size = 16, sw = 1.6 }) => (
|
|
<svg width={size} height={size} viewBox="0 0 24 24" fill="none"
|
|
stroke="currentColor" strokeWidth={sw}
|
|
strokeLinecap="round" strokeLinejoin="round">{d}</svg>
|
|
);
|
|
|
|
// Common Tabler-style paths
|
|
const P = {
|
|
search: <><circle cx="11" cy="11" r="7"/><path d="m20 20-3-3"/></>,
|
|
bell: <><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10 21a2 2 0 0 0 4 0"/></>,
|
|
home: <><path d="m3 12 9-9 9 9"/><path d="M5 10v10h14V10"/></>,
|
|
inbox: <><path d="M22 12h-6l-2 3h-4l-2-3H2"/><path d="M5 5h14l3 7v7H2v-7z"/></>,
|
|
building: <><path d="M3 21h18M5 21V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16"/><path d="M9 7h2M13 7h2M9 11h2M13 11h2M9 15h2M13 15h2"/></>,
|
|
people: <><circle cx="9" cy="8" r="4"/><path d="M3 21a6 6 0 0 1 12 0"/><circle cx="17" cy="6" r="3"/><path d="M21 17a4 4 0 0 0-6-3.5"/></>,
|
|
target: <><circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="5"/><circle cx="12" cy="12" r="1"/></>,
|
|
check: <><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></>,
|
|
bar: <><path d="M3 3v18h18"/><rect x="7" y="11" width="3" height="7"/><rect x="13" y="7" width="3" height="11"/></>,
|
|
workflow: <><rect x="3" y="3" width="6" height="6" rx="1"/><rect x="15" y="15" width="6" height="6" rx="1"/><path d="M9 6h6a3 3 0 0 1 3 3v6"/></>,
|
|
settings: <><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.5-1 1.7 1.7 0 0 0-.3-1.8L4.2 7a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.8.3 1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8v0a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1z"/><circle cx="12" cy="12" r="3"/></>,
|
|
plus: <path d="M12 5v14M5 12h14"/>,
|
|
chevron: <path d="m6 9 6 6 6-6"/>,
|
|
chevR: <path d="m9 6 6 6-6 6"/>,
|
|
doc: <><path d="M14 3H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/><path d="M14 3v6h6"/></>,
|
|
hash: <path d="M9 3l-2 18M17 3l-2 18M3 9h18M2 15h18"/>,
|
|
star: <path d="m12 3 2.6 6.2 6.7.5-5.1 4.4 1.6 6.6L12 17.3 6.2 20.7l1.6-6.6L2.7 9.7l6.7-.5z"/>,
|
|
spark: <path d="M12 3v4M12 17v4M3 12h4M17 12h4M6 6l3 3M15 15l3 3M6 18l3-3M15 9l3-3"/>,
|
|
more: <><circle cx="5" cy="12" r="1"/><circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/></>,
|
|
dot: <circle cx="12" cy="12" r="3"/>,
|
|
};
|
|
|
|
const SANS = "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif";
|
|
|
|
// ── Brand mark, shared ───────────────────────────────────────
|
|
const LatticeMark = ({ size = 18 }) => (
|
|
<svg width={size} height={size} viewBox="0 0 24 24" fill="none">
|
|
<defs>
|
|
<linearGradient id={`lg${size}`} x1="0" y1="0" x2="1" y2="1">
|
|
<stop offset="0%" stopColor="#6e6cff"/>
|
|
<stop offset="100%" stopColor="#b15bff"/>
|
|
</linearGradient>
|
|
</defs>
|
|
<path d="M3 20 L12 4 L21 20 Z" fill={`url(#lg${size})`}/>
|
|
</svg>
|
|
);
|
|
|
|
// ============================================================
|
|
// SHELL 1 — Sidebar (Linear/Notion/Twenty school)
|
|
// ============================================================
|
|
const navItems = [
|
|
{ id: "home", label: "Home", icon: P.home },
|
|
{ id: "inbox", label: "Inbox", icon: P.inbox, count: "12" },
|
|
{ id: "tasks", label: "My tasks", icon: P.check, count: "3" },
|
|
{ id: "_views", section: "Views" },
|
|
{ id: "companies", label: "Companies", icon: P.building },
|
|
{ id: "people", label: "People", icon: P.people },
|
|
{ id: "deals", label: "Opportunities", icon: P.target },
|
|
{ id: "_tools", section: "Tools" },
|
|
{ id: "insights", label: "Insights", icon: P.bar },
|
|
{ id: "flows", label: "Automations", icon: P.workflow },
|
|
{ id: "docs", label: "Docs", icon: P.doc },
|
|
{ id: "_admin", section: "Admin" },
|
|
{ id: "settings", label: "Settings", icon: P.settings },
|
|
];
|
|
|
|
const SidebarChrome = ({ active = "home", children }) => {
|
|
const SideItem = ({ id, label, icon, count }) => {
|
|
const isActive = id === active;
|
|
return (
|
|
<div style={{
|
|
display: "flex", alignItems: "center", gap: 10,
|
|
padding: "6px 10px", borderRadius: 6, fontSize: 13,
|
|
color: isActive ? "#111" : "#5a5a5e",
|
|
background: isActive ? "#ffffff" : "transparent",
|
|
boxShadow: isActive ? "0 1px 0 #00000008, 0 0 0 1px #00000008" : "none",
|
|
fontWeight: isActive ? 500 : 400, cursor: "pointer",
|
|
}}>
|
|
<span style={{ color: isActive ? "#5e5cff" : "#8a8a90", display: "flex" }}>
|
|
<Icon d={icon} size={15} />
|
|
</span>
|
|
<span style={{ flex: 1 }}>{label}</span>
|
|
{count && <span style={{
|
|
fontSize: 11, color: "#8a8a90", fontVariantNumeric: "tabular-nums",
|
|
}}>{count}</span>}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<div style={{
|
|
width: "100%", height: "100%", display: "grid",
|
|
gridTemplateColumns: "248px 1fr",
|
|
background: "#fcfcfb", fontFamily: SANS, color: "#111",
|
|
overflow: "hidden",
|
|
}}>
|
|
<aside style={{
|
|
background: "#f5f5f2", borderRight: "1px solid #e8e8e3",
|
|
display: "flex", flexDirection: "column",
|
|
}}>
|
|
<div style={{
|
|
padding: "12px 12px", display: "flex", alignItems: "center", gap: 10,
|
|
borderBottom: "1px solid #e8e8e3",
|
|
}}>
|
|
<div style={{
|
|
width: 26, height: 26, borderRadius: 6,
|
|
background: "linear-gradient(135deg, #6e6cff 0%, #b15bff 100%)",
|
|
display: "flex", alignItems: "center", justifyContent: "center",
|
|
color: "#fff", fontWeight: 700, fontSize: 13,
|
|
}}>L</div>
|
|
<div style={{ flex: 1, lineHeight: 1.2 }}>
|
|
<div style={{ fontSize: 13, fontWeight: 600 }}>Lattice Studio</div>
|
|
<div style={{ fontSize: 11, color: "#8a8a90" }}>Free · 4 members</div>
|
|
</div>
|
|
<span style={{ color: "#8a8a90", display: "flex" }}>
|
|
<Icon d={P.chevron} size={14} />
|
|
</span>
|
|
</div>
|
|
|
|
<div style={{ padding: "10px 12px" }}>
|
|
<div style={{
|
|
display: "flex", alignItems: "center", gap: 8, padding: "6px 10px",
|
|
background: "#fff", border: "1px solid #e8e8e3", borderRadius: 6,
|
|
fontSize: 12, color: "#8a8a90",
|
|
}}>
|
|
<Icon d={P.search} size={14} />
|
|
<span style={{ flex: 1 }}>Search…</span>
|
|
<span style={{
|
|
fontSize: 10, padding: "1px 5px", border: "1px solid #e0e0d8",
|
|
borderRadius: 3, fontFamily: "monospace",
|
|
}}>⌘K</span>
|
|
</div>
|
|
</div>
|
|
|
|
<nav style={{ padding: "4px 8px", flex: 1, overflowY: "auto" }}>
|
|
{navItems.map(item => item.section ? (
|
|
<div key={item.id} style={{
|
|
fontSize: 11, color: "#8a8a90", letterSpacing: "0.04em",
|
|
padding: "14px 10px 6px", textTransform: "uppercase",
|
|
fontWeight: 500,
|
|
}}>{item.section}</div>
|
|
) : (
|
|
<SideItem key={item.id} {...item} />
|
|
))}
|
|
</nav>
|
|
|
|
<div style={{
|
|
padding: "10px 12px", borderTop: "1px solid #e8e8e3",
|
|
display: "flex", alignItems: "center", gap: 10,
|
|
}}>
|
|
<div style={{
|
|
width: 24, height: 24, borderRadius: "50%", background: "#d4b8a8",
|
|
display: "flex", alignItems: "center", justifyContent: "center",
|
|
fontSize: 11, fontWeight: 600, color: "#5a3e34",
|
|
}}>MR</div>
|
|
<div style={{ flex: 1, fontSize: 12 }}>
|
|
<div style={{ fontWeight: 500 }}>Mira Reyes</div>
|
|
<div style={{ color: "#8a8a90", fontSize: 11 }}>mira@lattice.co</div>
|
|
</div>
|
|
<span style={{ color: "#8a8a90", display: "flex" }}>
|
|
<Icon d={P.chevron} size={14} />
|
|
</span>
|
|
</div>
|
|
</aside>
|
|
|
|
<main style={{ overflow: "hidden", display: "flex", flexDirection: "column" }}>
|
|
{children}
|
|
</main>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// ============================================================
|
|
// SHELL 2 — Icon rail + secondary panel (Slack/Discord/mail school)
|
|
// ============================================================
|
|
const railItems = [
|
|
{ id: "home", icon: P.home, label: "Home" },
|
|
{ id: "inbox", icon: P.inbox, label: "Inbox", badge: "9" },
|
|
{ id: "companies", icon: P.building, label: "Companies" },
|
|
{ id: "people", icon: P.people, label: "People" },
|
|
{ id: "deals", icon: P.target, label: "Opportunities", badge: "2" },
|
|
{ id: "insights", icon: P.bar, label: "Insights" },
|
|
{ id: "settings", icon: P.settings, label: "Settings" },
|
|
];
|
|
|
|
// Secondary panel content per active rail item — wrapper passes
|
|
// in `secondary` so each page can ship its own.
|
|
const RailChrome = ({ active = "home", secondary, children }) => {
|
|
const activeItem = railItems.find(r => r.id === active) || railItems[0];
|
|
|
|
const RailIcon = ({ icon, isActive, badge }) => (
|
|
<div style={{
|
|
width: 40, height: 40, borderRadius: 10,
|
|
background: isActive ? "#5e5cff" : "transparent",
|
|
color: isActive ? "#fff" : "#9a9aa6",
|
|
display: "flex", alignItems: "center", justifyContent: "center",
|
|
cursor: "pointer", position: "relative",
|
|
}}>
|
|
<Icon d={icon} size={18} sw={2} />
|
|
{badge && (
|
|
<span style={{
|
|
position: "absolute", top: -2, right: -2, minWidth: 16, height: 16,
|
|
padding: "0 4px", background: "#ff4d5e", color: "#fff",
|
|
borderRadius: 8, fontSize: 10, fontWeight: 600,
|
|
display: "flex", alignItems: "center", justifyContent: "center",
|
|
border: "2px solid #08080c",
|
|
}}>{badge}</span>
|
|
)}
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<div style={{
|
|
width: "100%", height: "100%", display: "grid",
|
|
gridTemplateColumns: "72px 260px 1fr",
|
|
background: "#0f0f14", color: "#e8e8ee", fontFamily: SANS,
|
|
overflow: "hidden",
|
|
}}>
|
|
{/* Icon rail */}
|
|
<div style={{
|
|
background: "#08080c", borderRight: "1px solid #ffffff08",
|
|
display: "flex", flexDirection: "column", alignItems: "center",
|
|
padding: "12px 0", gap: 6,
|
|
}}>
|
|
<div style={{
|
|
width: 40, height: 40, borderRadius: 10,
|
|
background: "linear-gradient(135deg, #5e5cff 0%, #b15bff 100%)",
|
|
display: "flex", alignItems: "center", justifyContent: "center",
|
|
color: "#fff", fontWeight: 800, fontSize: 16, marginBottom: 6,
|
|
}}>L</div>
|
|
<div style={{ width: 24, height: 1, background: "#ffffff10", margin: "4px 0" }}></div>
|
|
{railItems.map(r => (
|
|
<RailIcon key={r.id} icon={r.icon} isActive={r.id === active} badge={r.badge} />
|
|
))}
|
|
<div style={{ flex: 1 }}></div>
|
|
<RailIcon icon={P.spark} />
|
|
<div style={{
|
|
width: 32, height: 32, borderRadius: "50%", marginTop: 4,
|
|
background: "#d4b8a8", display: "flex", alignItems: "center",
|
|
justifyContent: "center", fontSize: 12, fontWeight: 600, color: "#5a3e34",
|
|
border: "2px solid #08080c", boxShadow: "0 0 0 2px #5e5cff",
|
|
position: "relative",
|
|
}}>MR
|
|
<span style={{
|
|
position: "absolute", bottom: -2, right: -2, width: 11, height: 11,
|
|
background: "#22c55e", borderRadius: "50%", border: "2px solid #08080c",
|
|
}}></span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Secondary panel */}
|
|
<div style={{
|
|
background: "#13131a", borderRight: "1px solid #ffffff08",
|
|
display: "flex", flexDirection: "column", overflow: "hidden",
|
|
}}>
|
|
<div style={{
|
|
padding: "16px 16px 12px", borderBottom: "1px solid #ffffff08",
|
|
}}>
|
|
<div style={{
|
|
display: "flex", justifyContent: "space-between", alignItems: "center",
|
|
marginBottom: 12,
|
|
}}>
|
|
<span style={{ fontSize: 15, fontWeight: 600 }}>{activeItem.label}</span>
|
|
<span style={{ color: "#9a9aa6", display: "flex" }}>
|
|
<Icon d={P.more} size={16} />
|
|
</span>
|
|
</div>
|
|
<div style={{
|
|
display: "flex", alignItems: "center", gap: 8,
|
|
padding: "7px 10px", background: "#08080c",
|
|
borderRadius: 7, fontSize: 12, color: "#9a9aa6",
|
|
}}>
|
|
<Icon d={P.search} size={13} />
|
|
<span style={{ flex: 1 }}>Jump to…</span>
|
|
<span style={{
|
|
fontSize: 10, padding: "1px 5px",
|
|
background: "#ffffff08", borderRadius: 3, fontFamily: "monospace",
|
|
}}>⌘K</span>
|
|
</div>
|
|
</div>
|
|
<div style={{ padding: "10px 8px", flex: 1, overflowY: "auto" }}>
|
|
{secondary}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<main style={{ overflow: "hidden", display: "flex", flexDirection: "column" }}>
|
|
{children}
|
|
</main>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// Convenience list item for the rail's secondary panel (dark theme)
|
|
const RailItem = ({ leading, label, sub, trailing, active }) => (
|
|
<div style={{
|
|
display: "flex", alignItems: "center", gap: 10,
|
|
padding: "8px 10px", borderRadius: 6, fontSize: 13,
|
|
color: active ? "#fff" : "#dcdce4",
|
|
background: active ? "#ffffff14" : "transparent",
|
|
cursor: "pointer",
|
|
}}>
|
|
{leading}
|
|
<div style={{ flex: 1, lineHeight: 1.2, minWidth: 0 }}>
|
|
<div style={{
|
|
fontSize: 13, fontWeight: active ? 500 : 400,
|
|
whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis",
|
|
}}>{label}</div>
|
|
{sub && <div style={{ fontSize: 11, color: "#7a7a85", marginTop: 1 }}>{sub}</div>}
|
|
</div>
|
|
{trailing}
|
|
</div>
|
|
);
|
|
|
|
const RailSectionHeader = ({ children, action }) => (
|
|
<div style={{
|
|
fontSize: 11, color: "#6a6a78", padding: "12px 10px 4px",
|
|
textTransform: "uppercase", letterSpacing: "0.04em", fontWeight: 500,
|
|
display: "flex", justifyContent: "space-between", alignItems: "center",
|
|
}}>
|
|
<span>{children}</span>
|
|
{action}
|
|
</div>
|
|
);
|
|
|
|
// ============================================================
|
|
// SHELL 3 — Top horizontal + ⌘K (Vercel/Stripe school)
|
|
// ============================================================
|
|
const TopbarChrome = ({ tabs, activeTab, breadcrumb, children }) => {
|
|
const TabItem = ({ label, isActive }) => (
|
|
<div style={{
|
|
padding: "16px 2px", margin: "0 12px", fontSize: 13, fontWeight: 500,
|
|
color: isActive ? "#fff" : "#9a9aa6", whiteSpace: "nowrap",
|
|
borderBottom: isActive ? "2px solid #fff" : "2px solid transparent",
|
|
cursor: "pointer", position: "relative", top: 1,
|
|
}}>{label}</div>
|
|
);
|
|
|
|
return (
|
|
<div style={{
|
|
width: "100%", height: "100%", background: "#fafaf9",
|
|
color: "#111", fontFamily: SANS, display: "flex", flexDirection: "column",
|
|
overflow: "hidden",
|
|
}}>
|
|
<header style={{ background: "#0a0a0a", color: "#fff" }}>
|
|
<div style={{
|
|
display: "flex", alignItems: "center", gap: 14,
|
|
padding: "12px 24px",
|
|
}}>
|
|
<div style={{
|
|
display: "flex", alignItems: "center", gap: 8, fontWeight: 600, fontSize: 14,
|
|
}}>
|
|
<LatticeMark size={20} />
|
|
Lattice
|
|
</div>
|
|
{breadcrumb && (
|
|
<>
|
|
<span style={{ color: "#3a3a3a" }}>/</span>
|
|
<div style={{ display: "flex", alignItems: "center", gap: 8, fontSize: 13 }}>
|
|
<div style={{
|
|
width: 18, height: 18, borderRadius: "50%", background: "#e8a87c",
|
|
fontSize: 9, fontWeight: 700, color: "#5a3e34",
|
|
display: "flex", alignItems: "center", justifyContent: "center",
|
|
}}>MR</div>
|
|
<span>mira-reyes</span>
|
|
<span style={{ color: "#5a5a5e", display: "flex" }}><Icon d={P.chevron} size={12}/></span>
|
|
</div>
|
|
<span style={{ color: "#3a3a3a" }}>/</span>
|
|
<div style={{ fontSize: 13, display: "flex", alignItems: "center", gap: 6 }}>
|
|
<span style={{ whiteSpace: "nowrap" }}>{breadcrumb}</span>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
<div style={{ flex: 1 }}></div>
|
|
|
|
<div style={{
|
|
display: "flex", alignItems: "center", gap: 10,
|
|
padding: "6px 12px", borderRadius: 8,
|
|
background: "#1a1a1a", border: "1px solid #2a2a2a",
|
|
color: "#9a9aa6", fontSize: 12, minWidth: 280,
|
|
}}>
|
|
<Icon d={P.search} size={13} />
|
|
<span style={{ flex: 1 }}>Find or jump to anything…</span>
|
|
<span style={{
|
|
fontSize: 10, padding: "1px 5px", background: "#2a2a2a",
|
|
borderRadius: 3, fontFamily: "monospace",
|
|
}}>⌘K</span>
|
|
</div>
|
|
|
|
<button style={{
|
|
background: "transparent", border: "1px solid #2a2a2a",
|
|
color: "#fff", padding: "5px 12px", borderRadius: 6,
|
|
fontSize: 12, fontFamily: SANS, cursor: "pointer", whiteSpace: "nowrap",
|
|
}}>Feedback</button>
|
|
<span style={{ color: "#9a9aa6", display: "flex", cursor: "pointer", position: "relative" }}>
|
|
<Icon d={P.bell} size={16}/>
|
|
<span style={{
|
|
position: "absolute", top: -2, right: -2, width: 7, height: 7,
|
|
background: "#5e5cff", borderRadius: "50%",
|
|
}}></span>
|
|
</span>
|
|
<div style={{
|
|
width: 26, height: 26, borderRadius: "50%", background: "#d4b8a8",
|
|
fontSize: 11, fontWeight: 600, color: "#5a3e34",
|
|
display: "flex", alignItems: "center", justifyContent: "center",
|
|
cursor: "pointer",
|
|
}}>MR</div>
|
|
</div>
|
|
|
|
<div style={{
|
|
display: "flex", alignItems: "center",
|
|
padding: "0 16px", borderBottom: "1px solid #1a1a1a",
|
|
overflowX: "auto",
|
|
}}>
|
|
{(tabs || []).map(t => (
|
|
<TabItem key={t} label={t} isActive={t === activeTab} />
|
|
))}
|
|
</div>
|
|
</header>
|
|
|
|
<div style={{ flex: 1, overflow: "hidden" }}>{children}</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
Object.assign(window, {
|
|
Icon, P, SANS, LatticeMark,
|
|
SidebarChrome, RailChrome, RailItem, RailSectionHeader, TopbarChrome,
|
|
});
|