From f19155ed44a0dabcc163fc55ee8e5b1b83b819bf Mon Sep 17 00:00:00 2001 From: mawkone Date: Sat, 13 Jun 2026 11:31:44 -0700 Subject: [PATCH] fix(dashboard): add missing dashboard-ui component kit --- .../components/project/dashboard-ui.tsx | 690 ++++++++++++++++++ 1 file changed, 690 insertions(+) create mode 100644 vibn-frontend/components/project/dashboard-ui.tsx diff --git a/vibn-frontend/components/project/dashboard-ui.tsx b/vibn-frontend/components/project/dashboard-ui.tsx new file mode 100644 index 0000000..d3e8bf9 --- /dev/null +++ b/vibn-frontend/components/project/dashboard-ui.tsx @@ -0,0 +1,690 @@ +"use client"; + +/** + * Shared dashboard design system. + * + * Single source of truth for the project dashboard look-and-feel. Replaces the + * `INK` palette object that was copy-pasted into every page. The aesthetic is + * the house "ink & parchment" brand (warm neutrals, NO blue/purple chrome) but + * the LAYOUT patterns — settings-as-cards, label-left/action-right rows, a + * single primary action per area, generous spacing — are borrowed from the + * Base44 dashboard reference. + * + * Everything is inline-styled (matching the existing pages) so it drops into any + * page with zero CSS wiring. Swap the brand accent in ONE place: THEME.accent. + */ + +import React, { useState } from "react"; + +export const THEME = { + // Flowbite / Tailwind cool-grey ramp + ink: "#111827", // gray-900 (headings / primary text) + mid: "#4b5563", // gray-600 (body / secondary) + muted: "#9ca3af", // gray-400 (tertiary / meta / icons) + // Surfaces + canvas: "#f9fafb", // gray-50 page background (flat fallback) + // Subtle, color-free depth: a soft light at top-center easing down to gray-100. + // Barely-there — reads as "premium", not a colored gradient. + canvasGradient: + "radial-gradient(120% 80% at 50% 0%, #ffffff 0%, #f9fafb 52%, #f3f4f6 100%)", + cardBg: "#ffffff", + subtleBg: "#f3f4f6", // gray-100 hover / active nav pill / row stripe + // Lines + border: "#e5e7eb", // gray-200 + borderSoft: "#f3f4f6", // gray-100 + // Primary action — neutral graphite (gray-900) to match the grayscale chrome + // in the reference. For Flowbite's signature blue CTA instead, set + // accent/accentHover to "#1c64f2" / "#1a56db" — single-variable swap. + accent: "#111827", + accentHover: "#1f2937", // gray-800 + accentText: "#ffffff", + // Destructive — Flowbite red + danger: "#e02424", // red-600 + dangerBg: "#fdf2f2", // red-50 + dangerBorder: "#f8b4b4", // red-300 + // Shape, depth & type — Flowbite rounded-lg + soft shadow-sm, Inter type. + radius: 10, + radiusSm: 8, + shadow: "0 1px 3px rgba(0,0,0,0.06), 0 1px 2px rgba(0,0,0,0.04)", + font: '"Inter", ui-sans-serif, system-ui, -apple-system, sans-serif', +} as const; + +// ── Page header ─────────────────────────────────────────────────────────── +export function PageHeader({ + title, + subtitle, + actions, +}: { + title: string; + subtitle?: string; + actions?: React.ReactNode; +}) { + return ( +
+
+

+ {title} +

+ {subtitle && ( +

+ {subtitle} +

+ )} +
+ {actions && ( +
{actions}
+ )} +
+ ); +} + +// ── Card ──────────────────────────────────────────────────────────────────── +export function Card({ + children, + style, + padding = 24, +}: { + children: React.ReactNode; + style?: React.CSSProperties; + padding?: number; +}) { + return ( +
+ {children} +
+ ); +} + +// ── SettingCard — title + description left, action right (Base44 row pattern) ─ +export function SettingCard({ + title, + description, + action, + danger, + children, +}: { + title: string; + description?: string; + action?: React.ReactNode; + danger?: boolean; + children?: React.ReactNode; +}) { + return ( + +
+
+

+ {title} +

+ {description && ( +

+ {description} +

+ )} +
+ {action &&
{action}
} +
+ {children &&
{children}
} +
+ ); +} + +// ── Section header (for grouping inside a page) ────────────────────────────── +export function SectionHeader({ + title, + count, +}: { + title: string; + count?: number; +}) { + return ( +
+

+ {title} +

+ {typeof count === "number" && ( + + ({count}) + + )} +
+ ); +} + +// ── Empty state (dashed card, centered) ──────────────────────────── +export function EmptyState({ + icon, + title, + hint, + action, +}: { + icon?: React.ReactNode; + title: string; + hint?: string; + action?: React.ReactNode; +}) { + return ( +
+ {icon &&
{icon}
} +
+ {title} +
+ {hint && ( +
+ {hint} +
+ )} + {action &&
{action}
} +
+ ); +} + +// ── Buttons (icon + label; hover handled inline) ─────────────────────── +type BtnProps = { + children: React.ReactNode; + icon?: React.ReactNode; + onClick?: () => void; + type?: "button" | "submit"; + disabled?: boolean; + danger?: boolean; +}; + +const btnBase: React.CSSProperties = { + display: "inline-flex", + alignItems: "center", + gap: 8, + padding: "9px 16px", + borderRadius: THEME.radiusSm, + fontSize: "0.875rem", + fontWeight: 600, + fontFamily: THEME.font, + cursor: "pointer", + transition: "background 0.15s ease, border-color 0.15s ease", + whiteSpace: "nowrap", +}; + +export function PrimaryButton({ + children, + icon, + onClick, + type = "button", + disabled, +}: BtnProps) { + const [hover, setHover] = useState(false); + return ( + + ); +} + +export function SecondaryButton({ + children, + icon, + onClick, + type = "button", + disabled, + danger, +}: BtnProps) { + const [hover, setHover] = useState(false); + const color = danger ? THEME.danger : THEME.ink; + const borderColor = danger ? THEME.dangerBorder : THEME.border; + return ( + + ); +} + +// ── List & Key-Value Rows ────────────────────────────────────────────────── +export function ListCard({ + children, + style, +}: { + children: React.ReactNode; + style?: React.CSSProperties; +}) { + return ( +
+ {children} +
+ ); +} + +export function ListRow({ + label, + value, + action, + icon, +}: { + label: string | React.ReactNode; + value?: string | React.ReactNode; + action?: React.ReactNode; + icon?: React.ReactNode; +}) { + return ( +
+
+ {icon &&
{icon}
} + + {label} + +
+
+ {value && ( + + {value} + + )} + {action &&
{action}
} +
+
+ ); +} + +export function KvRow({ + label, + value, + mono, + dot, +}: { + label: string; + value: React.ReactNode; + mono?: boolean; + dot?: string; +}) { + return ( +
+ + {label} + + + {dot && ( +
+ )} + {value} + +
+ ); +} + +// ── Badges & Status ──────────────────────────────────────────────────────── +export function Badge({ + children, + color = "default", +}: { + children: React.ReactNode; + color?: "default" | "success" | "warning" | "danger" | "accent"; +}) { + const bg = + color === "success" + ? "#def7ec" + : color === "warning" + ? "#fdf6b2" + : color === "danger" + ? THEME.dangerBg + : color === "accent" + ? THEME.ink + : THEME.subtleBg; + const text = + color === "success" + ? "#03543f" + : color === "warning" + ? "#723b13" + : color === "danger" + ? THEME.danger + : color === "accent" + ? THEME.cardBg + : THEME.mid; + + return ( + + {children} + + ); +} + +export function StatusDot({ + status, +}: { + status: "success" | "warning" | "danger" | "neutral"; +}) { + const color = + status === "success" + ? "#31c48d" + : status === "warning" + ? "#faca15" + : status === "danger" + ? THEME.danger + : THEME.muted; + return ( +
+ ); +} + +// ── Stat Card ────────────────────────────────────────────────────────────── +export function StatCard({ + label, + value, + onClick, +}: { + label: string; + value: string | number; + onClick?: () => void; +}) { + return ( + + ); +} + +// ── Text field (label + input/textarea) ────────────────────────────────────── +export function TextField({ + label, + defaultValue, + value, + onChange, + placeholder, + multiline, + rows = 3, +}: { + label?: string; + defaultValue?: string; + value?: string; + onChange?: (v: string) => void; + placeholder?: string; + multiline?: boolean; + rows?: number; +}) { + const [focus, setFocus] = useState(false); + const fieldStyle: React.CSSProperties = { + padding: "10px 14px", + border: `1px solid ${focus ? THEME.muted : THEME.border}`, + borderRadius: THEME.radiusSm, + fontSize: "0.9rem", + fontFamily: THEME.font, + color: THEME.ink, + background: THEME.cardBg, + outline: "none", + width: "100%", + boxSizing: "border-box", + resize: multiline ? "vertical" : undefined, + transition: "border-color 0.15s ease", + }; + return ( +
+ {label && ( + + )} + {multiline ? ( +