Ninna UI - Full Component API Source Reference This document contains the unminified TypeScript Implementation and Styling layer of all Ninna UI components. It is explicitly designed for LLMs to read and instantly understand the current API boundaries, DOM structures, and prop-types of the design system. ====================================================== View Category: blocks ====================================================== ### File: app/views/blocks/BlockCard.tsx ```tsx import { Link } from "react-router"; import { ArrowRight } from "lucide-react"; import { Skeleton } from "@ninna-ui/feedback"; import type { BlockMeta } from "./registry"; // ─── Per-slug realistic skeleton previews ─────────────────────────────────── function DashboardSkeleton() { return (
{/* Sidebar */}
{[48, 36, 48, 36, 55].map((w, i) => ( ))}
{/* Main */}
{["primary/20", "success/20", "warning/20"].map((c, i) => (
))}
{[1, 2, 3].map((r) => (
))}
); } function AuthLoginSkeleton() { return (
); } function SettingsPageSkeleton() { return (
{/* Tabs */}
{[80, 70, 90, 65, 75].map((w, i) => ( ))}
{/* Content */}
{[1, 2, 3].map((i) => (
))}
); } function UserProfileSkeleton() { return (
{["All", "Repos", "Activity"].map((t, i) => ( ))}
{[1, 2].map((i) => (
))}
); } function PricingTableSkeleton() { return (
{[false, true, false].map((featured, i) => (
{[1, 2, 3].map((r) => (
))}
))}
); } function HeroSkeleton() { return (
{[50, 40, 45].map((w, i) => ( ))}
); } function FeaturesSkeleton() { return (
{[...Array(6)].map((_, i) => (
))}
); } function FaqSkeleton() { return (
{[1, 2, 3, 4].map((i) => (
{i === 2 && (
)}
))}
); } function ContactFormSkeleton() { return (
); } function Error404Skeleton() { return (
404
); } function Error500Skeleton() { return (
500
); } function Error403Skeleton() { return (
403
); } function MaintenanceSkeleton() { return (
{[1, 2, 3].map((r) => (
))}
); } function ArticleCardsSkeleton() { return (
{[...Array(4)].map((_, i) => (
))}
); } function ArticlePageSkeleton() { return (
{/* TOC */}
{[70, 90, 60, 80, 65].map((w, i) => ( ))}
{/* Article */}
{[95, 85, 95, 70].map((w, i) => ( ))}
{[90, 75, 90].map((w, i) => ( ))}
); } function NewsletterSkeleton() { return (
); } function BlogCategoryFilterSkeleton() { return (
{[40, 55, 35, 45].map((w, i) => ( ))}
{[0, 1, 2, 3].map((i) => (
))}
); } function CommentsSkeleton() { return (
{[1, 2].map((i) => (
{i === 1 && }
))}
); } function ChangelogSkeleton() { return (
{[1, 2].map((i) => (
{i === 1 &&
}
{i === 1 && }
{[50, 60, 45].map((w, ci) => (
))}
))}
); } // ─── Wave 1 skeleton previews ───────────────────────────────────────────── function AuthSignupSkeleton() { return (
); } function AuthResetPasswordSkeleton() { return (
{[1, 2, 3].map((s) => (
))}
); } function AuthOtpVerifySkeleton() { return (
{[1, 2, 3, 4, 5, 6].map((d) => ( ))}
); } function ProductCardGridSkeleton() { return (
{[1, 2, 3, 4].map((i) => (
))}
); } function ShoppingCartSkeleton() { return (
{[1, 2, 3].map((i) => (
))}
); } function CheckoutFormSkeleton() { return (
{[1, 2, 3].map((s) => (
))}
); } function OrderConfirmationSkeleton() { return (
{[1, 2].map((i) => (
))}
); } function CommandPaletteSkeleton() { return (
{[65, 55, 70].map((w, i) => (
))} {[60, 50].map((w, i) => (
))}
); } function DataTableFiltersSkeleton() { return (
{[30, 35, 18, 18, 22].map((w, i) => ( ))}
{[1, 2, 3, 4].map((r) => (
))}
); } function FileUploadZoneSkeleton() { return (
{[1, 2].map((i) => (
{i === 2 && }
))}
); } function NotificationsPanelSkeleton() { return (
{[1, 2, 3].map((i) => (
))}
); } function OnboardingWizardSkeleton() { return (
{[1, 2, 3, 4].map((i) => ( ))}
); } function CalendarEventsSkeleton() { return (
{[...Array(28)].map((_, i) => (
{i === 12 &&
} {i === 18 &&
}
))}
); } function TimelineActivitySkeleton() { return (
{[1, 2, 3].map((i) => (
{i < 3 &&
}
))}
); } function EmptyStateShowcaseSkeleton() { return (
); } function TestimonialsCarouselSkeleton() { return (
{[1, 2, 3, 4, 5].map((i) => ( ))}
); } function StatsCounterSkeleton() { return (
{[1, 2, 3, 4].map((i) => (
))}
); } function TeamGridSkeleton() { return (
{[1, 2, 3, 4].map((i) => (
))}
); } function CtaBannerSkeleton() { return (
); } function LandingHeroSplitSkeleton() { return (
); } function FeatureComparisonTableSkeleton() { return (
{[1, 2, 3, 4].map((i) => ( ))}
{[1, 2, 3, 4].map((i) => (
))}
); } function SocialProofBannerSkeleton() { return (
{[1, 2, 3, 4].map((i) => (
))}
{[1, 2, 3].map((i) => (
))}
); } function CookieConsentSkeleton() { return (
); } function SocialProfileLinksSkeleton() { return (
{[1, 2, 3].map((i) => (
))}
{[1, 2, 3, 4].map((i) => ( ))}
); } // ─── Skeleton map ──────────────────────────────────────────────────────────── const SLUG_SKELETON: Record = { dashboard: DashboardSkeleton, "authentication-login": AuthLoginSkeleton, "settings-page": SettingsPageSkeleton, "user-profile": UserProfileSkeleton, "pricing-table": PricingTableSkeleton, "hero-header": HeroSkeleton, "features-grid": FeaturesSkeleton, "faq-accordion": FaqSkeleton, "contact-form": ContactFormSkeleton, "error-404": Error404Skeleton, "error-500": Error500Skeleton, "error-403": Error403Skeleton, "maintenance": MaintenanceSkeleton, "article-cards": ArticleCardsSkeleton, "article-page": ArticlePageSkeleton, "newsletter-section": NewsletterSkeleton, "blog-category-filter": BlogCategoryFilterSkeleton, "comments-section": CommentsSkeleton, "changelog": ChangelogSkeleton, "authentication-signup": AuthSignupSkeleton, "authentication-reset-password": AuthResetPasswordSkeleton, "authentication-otp-verify": AuthOtpVerifySkeleton, "product-card-grid": ProductCardGridSkeleton, "shopping-cart": ShoppingCartSkeleton, "checkout-form": CheckoutFormSkeleton, "order-confirmation": OrderConfirmationSkeleton, "command-palette": CommandPaletteSkeleton, "data-table-filters": DataTableFiltersSkeleton, "file-upload-zone": FileUploadZoneSkeleton, "notifications-panel": NotificationsPanelSkeleton, "onboarding-wizard": OnboardingWizardSkeleton, "calendar-events": CalendarEventsSkeleton, "timeline-activity": TimelineActivitySkeleton, "empty-state-showcase": EmptyStateShowcaseSkeleton, "testimonials-carousel": TestimonialsCarouselSkeleton, "stats-counter": StatsCounterSkeleton, "team-grid": TeamGridSkeleton, "cta-banner": CtaBannerSkeleton, "landing-hero-split": LandingHeroSplitSkeleton, "feature-comparison-table": FeatureComparisonTableSkeleton, "social-proof-banner": SocialProofBannerSkeleton, "cookie-consent": CookieConsentSkeleton, "social-profile-links": SocialProfileLinksSkeleton, }; // ─── Gradient per category ─────────────────────────────────────────────────── const CATEGORY_GRADIENT: Record = { "authentication": "from-primary/8 via-transparent to-info/8", "application-ui": "from-primary/8 via-transparent to-secondary/8", "page-sections": "from-accent/8 via-transparent to-warning/8", "error-pages": "from-danger/8 via-transparent to-warning/8", "blog-ui": "from-info/8 via-transparent to-success/8", "ecommerce": "from-warning/8 via-transparent to-primary/8", "marketing": "from-secondary/8 via-transparent to-accent/8", "data-display": "from-info/8 via-transparent to-primary/8", }; // ─── BlockCard ─────────────────────────────────────────────────────────────── export function BlockCard({ block }: { block: BlockMeta }) { const SkeletonPreview = SLUG_SKELETON[block.slug]; const gradient = CATEGORY_GRADIENT[block.category]; return ( {/* Preview thumbnail */}
{SkeletonPreview ? ( ) : ( /* Fallback generic skeleton */
)}
{/* Coming soon overlay */} {block.comingSoon && (
Coming soon
)}
{/* Card body */}

{block.title}

{block.description}

{block.components.slice(0, 3).map((pkg) => ( {pkg.replace("@ninna-ui/", "")} ))} {block.components.length > 3 && ( +{block.components.length - 3} )}
); } ``` ### File: app/views/blocks/BlockViewer.tsx ```tsx import { useState, useEffect, useRef, useCallback } from "react"; import { Link } from "react-router"; import { ArrowLeft, Code2, Eye, Copy, Check, ChevronRight, Monitor, Tablet, Smartphone, RefreshCw, Maximize2 } from "lucide-react"; import { useTheme } from "~/context/ThemeContext"; import { cn } from "~/utils/cn"; import type { BlockMeta } from "./registry"; import { CATEGORY_LABELS } from "./registry"; import { loadBlockSource } from "~/utils/sourceRegistry"; interface BlockViewerProps { meta: BlockMeta; children: React.ReactNode; sourceCode?: string; } function CopyButton({ text, size = "md" }: { text: string; size?: "sm" | "md" }) { const [copied, setCopied] = useState(false); const handleCopy = async () => { await navigator.clipboard.writeText(text); setCopied(true); setTimeout(() => setCopied(false), 2000); }; return ( ); } export function BlockViewer({ meta, sourceCode: initialSource }: Omit) { const { resolvedTheme, preset } = useTheme(); const [view, setView] = useState<"preview" | "code">("preview"); const [sourceCode, setSourceCode] = useState(initialSource); const [isLoading, setIsLoading] = useState(false); // Resizing state const [width, setWidth] = useState("100%"); const [isResizing, setIsResizing] = useState(false); const containerRef = useRef(null); useEffect(() => { if (view === "code" && !sourceCode) { setIsLoading(true); loadBlockSource(meta.slug) .then((src) => { if (src) setSourceCode(src); }) .finally(() => { setIsLoading(false); }); } }, [view, sourceCode, meta.slug]); // Resizing logic const startResizing = useCallback((e: React.MouseEvent) => { e.preventDefault(); setIsResizing(true); }, []); const stopResizing = useCallback(() => { setIsResizing(false); }, []); const resize = useCallback((e: MouseEvent) => { if (isResizing && containerRef.current) { const containerRect = containerRef.current.getBoundingClientRect(); // Keep it within reasonable bounds const minWidth = 320; const maxWidth = containerRect.width; // Since it's centered, we calculate distance from center const centerX = containerRect.left + containerRect.width / 2; const centeredWidth = Math.abs(e.clientX - centerX) * 2; if (centeredWidth >= minWidth && centeredWidth <= maxWidth) { setWidth(centeredWidth); } else if (centeredWidth < minWidth) { setWidth(minWidth); } else { setWidth("100%"); } } }, [isResizing]); useEffect(() => { if (isResizing) { window.addEventListener("mousemove", resize); window.addEventListener("mouseup", stopResizing); } else { window.removeEventListener("mousemove", resize); window.removeEventListener("mouseup", stopResizing); } return () => { window.removeEventListener("mousemove", resize); window.removeEventListener("mouseup", stopResizing); }; }, [isResizing, resize, stopResizing]); return (
{/* ── Sticky Toolbar ── */}
{/* Breadcrumbs */} {/* Actions */}
{/* Viewport controls (only in preview) */} {view === "preview" && (
)}
{/* Preview / Code toggle */}
setView("preview")} label="Preview" > setView("code")} label="Source code" >
{/* Copy when in code view */} {sourceCode && view === "code" && ( )} {/* Divider */}
{/* ── Content ── */} {view === "code" ? (
Note on Icons: The source code below uses lucide-react for iconography. We recommend installing it via npm i lucide-react, or you can easily swap them out for your own SVGs.
{/* Code toolbar */}
) : (
{/* Viewport feedback (visible when width is not 100%) */} {width !== "100%" && (
{Math.round(Number(width))}px
)}
{/* Mini browser chrome */}