@bizbasics/ui 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,43 @@
1
+ import type { CSSProperties } from "react";
2
+
3
+ interface Props {
4
+ size?: number;
5
+ color?: string;
6
+ style?: CSSProperties;
7
+ }
8
+
9
+ export function Spinner({ size = 20, color = "var(--bb-brand)", style }: Props) {
10
+ return (
11
+ <svg
12
+ width={size}
13
+ height={size}
14
+ viewBox="0 0 24 24"
15
+ fill="none"
16
+ style={{ animation: "bb-spin 0.65s linear infinite", flexShrink: 0, ...style }}
17
+ >
18
+ <style>{`@keyframes bb-spin { to { transform: rotate(360deg); } }`}</style>
19
+ <circle
20
+ cx="12" cy="12" r="10"
21
+ stroke={color}
22
+ strokeWidth="2.5"
23
+ strokeDasharray="56"
24
+ strokeDashoffset="14"
25
+ strokeLinecap="round"
26
+ />
27
+ </svg>
28
+ );
29
+ }
30
+
31
+ export function PageSpinner() {
32
+ return (
33
+ <div style={{
34
+ display: "flex",
35
+ alignItems: "center",
36
+ justifyContent: "center",
37
+ height: "100%",
38
+ minHeight: "200px",
39
+ }}>
40
+ <Spinner size={28} />
41
+ </div>
42
+ );
43
+ }
@@ -0,0 +1,131 @@
1
+ "use client";
2
+
3
+ import { createContext, useContext, useState, useCallback, type ReactNode, type CSSProperties } from "react";
4
+
5
+ // ── Types ────────────────────────────────────────────────────────────────────
6
+
7
+ type Variant = "success" | "error" | "warning" | "info";
8
+
9
+ interface ToastItem {
10
+ id: string;
11
+ message: string;
12
+ variant: Variant;
13
+ duration: number;
14
+ }
15
+
16
+ interface ToastContextValue {
17
+ toast: (message: string, variant?: Variant, duration?: number) => void;
18
+ }
19
+
20
+ // ── Context ───────────────────────────────────────────────────────────────────
21
+
22
+ const ToastContext = createContext<ToastContextValue | null>(null);
23
+
24
+ export function useToast(): ToastContextValue {
25
+ const ctx = useContext(ToastContext);
26
+ if (!ctx) throw new Error("useToast must be used inside <ToastProvider>");
27
+ return ctx;
28
+ }
29
+
30
+ // ── Provider ──────────────────────────────────────────────────────────────────
31
+
32
+ export function ToastProvider({ children }: { children: ReactNode }) {
33
+ const [items, setItems] = useState<ToastItem[]>([]);
34
+
35
+ const toast = useCallback((message: string, variant: Variant = "info", duration = 4000) => {
36
+ const id = `${Date.now()}-${Math.random()}`;
37
+ setItems((prev) => [...prev, { id, message, variant, duration }]);
38
+ setTimeout(() => {
39
+ setItems((prev) => prev.filter((t) => t.id !== id));
40
+ }, duration);
41
+ }, []);
42
+
43
+ const dismiss = useCallback((id: string) => {
44
+ setItems((prev) => prev.filter((t) => t.id !== id));
45
+ }, []);
46
+
47
+ return (
48
+ <ToastContext.Provider value={{ toast }}>
49
+ {children}
50
+ <ToastList items={items} onDismiss={dismiss} />
51
+ </ToastContext.Provider>
52
+ );
53
+ }
54
+
55
+ // ── Display ───────────────────────────────────────────────────────────────────
56
+
57
+ const variantStyles: Record<Variant, CSSProperties> = {
58
+ success: { borderLeftColor: "#16a34a", background: "#1a2a1e" },
59
+ error: { borderLeftColor: "#dc2626", background: "#2a1a1a" },
60
+ warning: { borderLeftColor: "#d97706", background: "#2a2218" },
61
+ info: { borderLeftColor: "#6366f1", background: "#1e1e2a" },
62
+ };
63
+
64
+ const variantIcons: Record<Variant, string> = {
65
+ success: "✓",
66
+ error: "✕",
67
+ warning: "!",
68
+ info: "i",
69
+ };
70
+
71
+ const variantIconColors: Record<Variant, string> = {
72
+ success: "#16a34a",
73
+ error: "#dc2626",
74
+ warning: "#d97706",
75
+ info: "#818cf8",
76
+ };
77
+
78
+ function ToastList({ items, onDismiss }: { items: ToastItem[]; onDismiss: (id: string) => void }) {
79
+ if (items.length === 0) return null;
80
+ return (
81
+ <div style={{
82
+ position: "fixed", bottom: "24px", right: "24px",
83
+ display: "flex", flexDirection: "column", gap: "8px",
84
+ zIndex: "var(--bb-z-toast, 400)" as unknown as number,
85
+ maxWidth: "360px", width: "100%",
86
+ pointerEvents: "none",
87
+ }}>
88
+ {items.map((item) => (
89
+ <div
90
+ key={item.id}
91
+ style={{
92
+ display: "flex", alignItems: "flex-start", gap: "10px",
93
+ padding: "12px 14px",
94
+ borderRadius: "var(--bb-radius)",
95
+ border: "1px solid rgba(255,255,255,0.08)",
96
+ borderLeft: `3px solid ${variantStyles[item.variant].borderLeftColor}`,
97
+ background: variantStyles[item.variant].background as string,
98
+ boxShadow: "0 4px 20px rgba(0,0,0,0.3)",
99
+ fontSize: "var(--bb-text-sm)",
100
+ color: "#e2e8f0",
101
+ fontFamily: "var(--bb-font-sans)",
102
+ pointerEvents: "all",
103
+ animation: "bb-toast-in 0.2s ease",
104
+ }}
105
+ >
106
+ <style>{`@keyframes bb-toast-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }`}</style>
107
+ <span style={{
108
+ width: "16px", height: "16px", borderRadius: "9999px",
109
+ background: variantIconColors[item.variant],
110
+ color: "#fff", fontSize: "10px", fontWeight: 700,
111
+ display: "flex", alignItems: "center", justifyContent: "center",
112
+ flexShrink: 0, marginTop: "1px",
113
+ }}>
114
+ {variantIcons[item.variant]}
115
+ </span>
116
+ <span style={{ flex: 1, lineHeight: 1.5 }}>{item.message}</span>
117
+ <button
118
+ onClick={() => onDismiss(item.id)}
119
+ style={{
120
+ background: "none", border: "none", color: "#64748b",
121
+ cursor: "pointer", fontSize: "16px", lineHeight: 1,
122
+ padding: "0", flexShrink: 0,
123
+ }}
124
+ >
125
+ ×
126
+ </button>
127
+ </div>
128
+ ))}
129
+ </div>
130
+ );
131
+ }
@@ -0,0 +1,148 @@
1
+ "use client";
2
+
3
+ import { type CSSProperties } from "react";
4
+ import { Avatar } from "./Avatar";
5
+ import type { ProfileCalloutUser } from "./ProfileCallout";
6
+
7
+ interface Props {
8
+ user: ProfileCalloutUser;
9
+ plan?: string;
10
+ onClose: () => void;
11
+ }
12
+
13
+ export function UserProfilePanel({ user, plan, onClose }: Props) {
14
+ return (
15
+ <>
16
+ {/* Backdrop */}
17
+ <div
18
+ onClick={onClose}
19
+ style={{
20
+ position: "fixed", inset: 0, zIndex: 290,
21
+ background: "rgba(0,0,0,0.45)",
22
+ }}
23
+ />
24
+
25
+ {/* Panel */}
26
+ <div style={{
27
+ position: "fixed", top: 0, right: 0, bottom: 0,
28
+ width: "320px", zIndex: 300,
29
+ background: "#1a1d24",
30
+ borderLeft: "1px solid rgba(255,255,255,0.08)",
31
+ boxShadow: "-20px 0 60px rgba(0,0,0,0.4)",
32
+ display: "flex", flexDirection: "column",
33
+ fontFamily: "var(--bb-font-sans)",
34
+ }}>
35
+ {/* Header */}
36
+ <div style={{
37
+ display: "flex", alignItems: "center", justifyContent: "space-between",
38
+ padding: "16px 20px",
39
+ borderBottom: "1px solid rgba(255,255,255,0.07)",
40
+ }}>
41
+ <span style={{ fontSize: "14px", fontWeight: 600, color: "#f1f5f9" }}>Profile</span>
42
+ <button
43
+ onClick={onClose}
44
+ style={{ background: "none", border: "none", color: "#64748b", cursor: "pointer", fontSize: "20px", lineHeight: 1, padding: "2px" }}
45
+ >
46
+ ×
47
+ </button>
48
+ </div>
49
+
50
+ {/* Body */}
51
+ <div style={{ flex: 1, overflowY: "auto", padding: "24px 20px" }}>
52
+ {/* Avatar + name */}
53
+ <div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: "12px", marginBottom: "32px" }}>
54
+ <Avatar name={user.name} size={72} />
55
+ <div style={{ textAlign: "center" }}>
56
+ <div style={{ fontSize: "16px", fontWeight: 700, color: "#f1f5f9" }}>{user.name}</div>
57
+ <div style={{ fontSize: "13px", color: "#64748b", marginTop: "4px" }}>{user.email}</div>
58
+ {user.role && (
59
+ <div style={roleBadgeStyle}>{user.role.replace(/_/g, " ")}</div>
60
+ )}
61
+ </div>
62
+ </div>
63
+
64
+ {/* Info rows */}
65
+ <div style={{ display: "flex", flexDirection: "column", gap: "1px" }}>
66
+ {plan && (
67
+ <InfoRow label="Plan" value={<span style={{ textTransform: "capitalize" }}>{plan}</span>} />
68
+ )}
69
+ </div>
70
+
71
+ {/* Links */}
72
+ <div style={{ marginTop: "28px", display: "flex", flexDirection: "column", gap: "4px" }}>
73
+ <PanelLink href="https://app.bizbasics.ai/settings/profile">Edit profile</PanelLink>
74
+ <PanelLink href="https://app.bizbasics.ai/settings/security">Change password</PanelLink>
75
+ <PanelLink href="https://app.bizbasics.ai/settings/notifications">Notification preferences</PanelLink>
76
+ </div>
77
+ </div>
78
+
79
+ {/* Footer */}
80
+ <div style={{
81
+ padding: "16px 20px",
82
+ borderTop: "1px solid rgba(255,255,255,0.07)",
83
+ }}>
84
+ <a
85
+ href="https://app.bizbasics.ai/dashboard"
86
+ style={{
87
+ display: "block", textAlign: "center",
88
+ padding: "9px",
89
+ background: "rgba(255,255,255,0.06)",
90
+ border: "1px solid rgba(255,255,255,0.08)",
91
+ borderRadius: "8px",
92
+ fontSize: "13px", fontWeight: 500,
93
+ color: "#94a3b8", textDecoration: "none",
94
+ }}
95
+ >
96
+ Go to platform dashboard
97
+ </a>
98
+ </div>
99
+ </div>
100
+ </>
101
+ );
102
+ }
103
+
104
+ function InfoRow({ label, value }: { label: string; value: React.ReactNode }) {
105
+ return (
106
+ <div style={{
107
+ display: "flex", justifyContent: "space-between", alignItems: "center",
108
+ padding: "10px 12px", background: "rgba(255,255,255,0.03)",
109
+ borderRadius: "6px",
110
+ }}>
111
+ <span style={{ fontSize: "12px", color: "#64748b" }}>{label}</span>
112
+ <span style={{ fontSize: "13px", color: "#e2e8f0" }}>{value}</span>
113
+ </div>
114
+ );
115
+ }
116
+
117
+ function PanelLink({ href, children }: { href: string; children: string }) {
118
+ return (
119
+ <a
120
+ href={href}
121
+ style={{
122
+ display: "flex", alignItems: "center", justifyContent: "space-between",
123
+ padding: "9px 12px", borderRadius: "7px",
124
+ fontSize: "13px", color: "#c4c5c9", textDecoration: "none",
125
+ transition: "background 120ms",
126
+ }}
127
+ onMouseEnter={(e) => (e.currentTarget.style.background = "rgba(255,255,255,0.05)")}
128
+ onMouseLeave={(e) => (e.currentTarget.style.background = "transparent")}
129
+ >
130
+ {children}
131
+ <svg width={14} height={14} fill="none" stroke="currentColor" viewBox="0 0 24 24" style={{ color: "#475569" }}>
132
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
133
+ </svg>
134
+ </a>
135
+ );
136
+ }
137
+
138
+ const roleBadgeStyle: CSSProperties = {
139
+ display: "inline-block",
140
+ marginTop: "6px",
141
+ padding: "2px 10px",
142
+ background: "rgba(255,255,255,0.07)",
143
+ border: "1px solid rgba(255,255,255,0.10)",
144
+ borderRadius: "9999px",
145
+ fontSize: "11px",
146
+ color: "#94a3b8",
147
+ textTransform: "capitalize",
148
+ };
package/src/index.ts ADDED
@@ -0,0 +1,32 @@
1
+ // Layout
2
+ export { AppShell } from "./components/AppShell";
3
+ export { AppSidebar } from "./components/AppSidebar";
4
+ export type { AppSidebarProps } from "./components/AppSidebar";
5
+ export { AppTopBar, TopBarIconButton } from "./components/AppTopBar";
6
+ export { SidebarBrand, OrgSwitcher } from "./components/SidebarBrand";
7
+ export { NavItem, type NavItemDef } from "./components/NavItem";
8
+ export { NavSection, NavDivider } from "./components/NavSection";
9
+ export { ProfileCallout, type ProfileCalloutUser } from "./components/ProfileCallout";
10
+
11
+ // Cross-product navigation (for external products)
12
+ export { PlatformNav } from "./components/PlatformNav";
13
+ export type { PlatformNavApp, PlatformNavUser, PlatformNavProps } from "./components/PlatformNav";
14
+
15
+ // UI primitives
16
+ export { Button } from "./components/Button";
17
+ export { Input, Textarea, Select } from "./components/Input";
18
+ export { Modal } from "./components/Modal";
19
+ export { Avatar } from "./components/Avatar";
20
+ export { Badge } from "./components/Badge";
21
+ export { Spinner, PageSpinner } from "./components/Spinner";
22
+ export { EmptyState } from "./components/EmptyState";
23
+ export { Card, CardHeader, CardDivider } from "./components/Card";
24
+
25
+ // Auth layout primitives
26
+ export { AuthLayout, AuthCard, AuthInput, AuthButton } from "./components/Auth";
27
+
28
+ // Toast / Snackbar
29
+ export { ToastProvider, useToast } from "./components/Toast";
30
+
31
+ // User profile panel
32
+ export { UserProfilePanel } from "./components/UserProfilePanel";
package/src/tokens.css ADDED
@@ -0,0 +1,158 @@
1
+ /*
2
+ * bizbasics design tokens — single source of truth
3
+ * Import this file in every app's globals.css:
4
+ * @import "@bizbasics/ui/tokens.css";
5
+ *
6
+ * All apps share the same sidebar, topbar, and component token values.
7
+ * Content-area background varies by app (--bb-app-bg).
8
+ */
9
+
10
+ :root {
11
+ /* ─── Brand ─────────────────────────────────────────────── */
12
+ --bb-brand: #2563eb; /* blue-600 — primary accent */
13
+ --bb-brand-hover: #1d4ed8; /* blue-700 */
14
+ --bb-brand-light: #93c5fd; /* blue-300 — links, active text */
15
+ --bb-brand-muted: rgba(37, 99, 235, 0.14); /* active item bg */
16
+ --bb-brand-ring: rgba(37, 99, 235, 0.30); /* focus ring */
17
+
18
+ /* ─── Sidebar (always dark, consistent across all apps) ─── */
19
+ --bb-sidebar-bg: #0f172a;
20
+ --bb-sidebar-header-bg: #0f172a;
21
+ --bb-sidebar-border: rgba(255, 255, 255, 0.06);
22
+ --bb-sidebar-text: #c4c5c9;
23
+ --bb-sidebar-text-muted: #5f6370;
24
+ --bb-sidebar-hover-bg: rgba(255, 255, 255, 0.055);
25
+ --bb-sidebar-active-bg: rgba(37, 99, 235, 0.16);
26
+ --bb-sidebar-active-text: #93c5fd; /* blue-300 */
27
+ --bb-sidebar-width: 240px;
28
+ --bb-sidebar-section-label: #4b4f5a;
29
+
30
+ /* ─── TopBar (always dark) ───────────────────────────────── */
31
+ --bb-topbar-bg: #111827;
32
+ --bb-topbar-border: rgba(255, 255, 255, 0.07);
33
+ --bb-topbar-text: #e5e7eb;
34
+ --bb-topbar-muted: #6b7280;
35
+ --bb-topbar-height: 48px;
36
+
37
+ /* ─── Surface / Content ──────────────────────────────────── */
38
+ /* Override --bb-app-bg per-app in their own globals.css */
39
+ --bb-app-bg: #f4f5f7; /* default: light (portal, admin) */
40
+
41
+ --bb-surface: #ffffff;
42
+ --bb-surface-muted: #f9fafb;
43
+ --bb-surface-overlay: #ffffff;
44
+
45
+ --bb-border: #e5e7eb;
46
+ --bb-border-muted: #f3f4f6;
47
+ --bb-border-strong: #d1d5db;
48
+
49
+ /* ─── Text (light content area) ─────────────────────────── */
50
+ --bb-text-primary: #111827;
51
+ --bb-text-secondary: #374151;
52
+ --bb-text-muted: #6b7280;
53
+ --bb-text-disabled: #9ca3af;
54
+ --bb-text-inverse: #f9fafb;
55
+
56
+ /* ─── Status colors ─────────────────────────────────────── */
57
+ --bb-success: #16a34a;
58
+ --bb-success-bg: #f0fdf4;
59
+ --bb-success-border: #bbf7d0;
60
+
61
+ --bb-warning: #d97706;
62
+ --bb-warning-bg: #fffbeb;
63
+ --bb-warning-border: #fde68a;
64
+
65
+ --bb-error: #dc2626;
66
+ --bb-error-bg: #fef2f2;
67
+ --bb-error-border: #fecaca;
68
+
69
+ --bb-info: #2563eb;
70
+ --bb-info-bg: #eff6ff;
71
+ --bb-info-border: #bfdbfe;
72
+
73
+ /* ─── Slate scale (convenience aliases used by components) ── */
74
+ --bb-slate-50: #f8fafc;
75
+ --bb-slate-100: #f1f5f9;
76
+ --bb-slate-200: #e2e8f0;
77
+ --bb-slate-300: #cbd5e1;
78
+ --bb-slate-400: #94a3b8;
79
+ --bb-slate-500: #64748b;
80
+ --bb-slate-600: #475569;
81
+ --bb-slate-700: #334155;
82
+ --bb-slate-800: #1e293b;
83
+ --bb-slate-900: #0f172a;
84
+
85
+ /* ─── Typography ─────────────────────────────────────────── */
86
+ --bb-font-sans: -apple-system, BlinkMacSystemFont, "Inter", "Segoe UI",
87
+ Roboto, Helvetica, Arial, sans-serif;
88
+ --bb-font-mono: "JetBrains Mono", "Fira Code", "Cascadia Code",
89
+ "Source Code Pro", Menlo, monospace;
90
+
91
+ --bb-text-xs: 11px;
92
+ --bb-text-sm: 13px;
93
+ --bb-text-base: 14px;
94
+ --bb-text-md: 15px;
95
+ --bb-text-lg: 17px;
96
+ --bb-text-xl: 20px;
97
+ --bb-text-2xl: 24px;
98
+
99
+ /* ─── Radius ─────────────────────────────────────────────── */
100
+ --bb-radius-sm: 4px;
101
+ --bb-radius: 8px;
102
+ --bb-radius-md: 10px;
103
+ --bb-radius-lg: 12px;
104
+ --bb-radius-xl: 16px;
105
+ --bb-radius-full: 9999px;
106
+
107
+ /* ─── Shadow ─────────────────────────────────────────────── */
108
+ --bb-shadow-sm: 0 1px 2px rgba(0,0,0,0.06);
109
+ --bb-shadow: 0 1px 4px rgba(0,0,0,0.08), 0 4px 12px rgba(0,0,0,0.06);
110
+ --bb-shadow-md: 0 4px 16px rgba(0,0,0,0.10), 0 1px 3px rgba(0,0,0,0.08);
111
+ --bb-shadow-lg: 0 8px 32px rgba(0,0,0,0.14), 0 2px 8px rgba(0,0,0,0.08);
112
+ --bb-shadow-xl: 0 20px 60px rgba(0,0,0,0.20), 0 4px 12px rgba(0,0,0,0.10);
113
+
114
+ /* ─── Transition ─────────────────────────────────────────── */
115
+ --bb-transition: 120ms ease;
116
+ --bb-transition-slow: 200ms ease;
117
+
118
+ /* ─── Z-index ────────────────────────────────────────────── */
119
+ --bb-z-dropdown: 100;
120
+ --bb-z-sticky: 200;
121
+ --bb-z-modal: 300;
122
+ --bb-z-toast: 400;
123
+ }
124
+
125
+ /* Dark content variant — used by relay, monk, and future dark apps */
126
+ .bb-dark-content {
127
+ --bb-app-bg: #1a1d21;
128
+ --bb-surface: #22252c;
129
+ --bb-surface-muted: #1e2128;
130
+ --bb-surface-overlay: #2a2d36;
131
+ --bb-border: rgba(255, 255, 255, 0.07);
132
+ --bb-border-muted: rgba(255, 255, 255, 0.04);
133
+ --bb-border-strong: rgba(255, 255, 255, 0.12);
134
+ --bb-text-primary: #e5e7eb;
135
+ --bb-text-secondary: #c4c5c9;
136
+ --bb-text-muted: #6b7280;
137
+ --bb-text-disabled: #4b5563;
138
+ }
139
+
140
+ /* ─── Global base styles injected by every app ─────────────── */
141
+ *, *::before, *::after { box-sizing: border-box; }
142
+
143
+ body {
144
+ margin: 0;
145
+ font-family: var(--bb-font-sans);
146
+ font-size: var(--bb-text-base);
147
+ -webkit-font-smoothing: antialiased;
148
+ -moz-osx-font-smoothing: grayscale;
149
+ background: var(--bb-app-bg);
150
+ color: var(--bb-text-primary);
151
+ }
152
+
153
+ /* Scrollbar — consistent thin dark scrollbar */
154
+ * { scrollbar-width: thin; scrollbar-color: rgba(255,255,255,0.12) transparent; }
155
+ ::-webkit-scrollbar { width: 5px; height: 5px; }
156
+ ::-webkit-scrollbar-track { background: transparent; }
157
+ ::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.12); border-radius: var(--bb-radius-full); }
158
+ ::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.22); }
package/tsconfig.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2017",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "allowJs": true,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "esModuleInterop": true,
9
+ "module": "esnext",
10
+ "moduleResolution": "bundler",
11
+ "resolveJsonModule": true,
12
+ "isolatedModules": true,
13
+ "jsx": "react-jsx",
14
+ "declaration": true,
15
+ "declarationMap": true
16
+ },
17
+ "include": ["src"]
18
+ }