Files

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,
});