("");
+ const { resolvedTheme } = useTheme();
const [mounted, setMounted] = useState(false);
- // Prevent hydration mismatch
+ // Calculate line count and create line numbers array
+ const lines = code.split('\n');
+ const lineCount = lines.length;
+ const maxLineNumberWidth = lineCount.toString().length;
+
useEffect(() => {
setMounted(true);
}, []);
- const currentTheme = mounted ? resolvedTheme || theme : "dark";
- const syntaxTheme = currentTheme === "dark" ? vscDarkPlus : vs;
+ useEffect(() => {
+ const generateHighlight = async () => {
+ try {
+ const theme = mounted && resolvedTheme === "dark" ? "github-dark" : "github-light";
+ const html = await codeToHtml(code, {
+ lang: language,
+ theme,
+ });
+
+ // If line numbers are enabled, process the HTML to add line number structure
+ if (showLineNumbers) {
+ const processedHtml = addLineNumbersToHtml(html, lineCount);
+ setHighlightedCode(processedHtml);
+ } else {
+ setHighlightedCode(html);
+ }
+ } catch (error) {
+ console.error("Highlighting error:", error);
+ setHighlightedCode(`${code} `);
+ }
+ };
+
+ generateHighlight();
+ }, [code, language, mounted, resolvedTheme, showLineNumbers, lineCount]);
+
+ // Function to add line numbers to the highlighted HTML
+ const addLineNumbersToHtml = (html: string, totalLines: number) => {
+ // Split HTML by newlines and add line number structure
+ const htmlLines = html.split('\n');
+ const preMatch = html.match(/]*>/);
+ const codeMatch = html.match(/]*>/);
+ const preOpenTag = preMatch ? preMatch[0] : '';
+ const codeOpenTag = codeMatch ? codeMatch[0] : '';
+
+ // Extract the content between and
+ const codeContent = html.match(/]*>([\s\S]*?)<\/code>/)?.[1] || '';
+ const lines = codeContent.split('\n');
+
+ // Wrap each line with line number data
+ const numberedLines = lines.map((line, index) => {
+ const lineNumber = index + 1;
+ return `${line} `;
+ }).join('\n');
+
+ return `${preOpenTag}${codeOpenTag}${numberedLines} `;
+ };
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(code);
setCopied(true);
- toast.success("Code copied to clipboard!", {
- duration: 2000,
- });
+ toast.success("Code copied to clipboard!");
setTimeout(() => setCopied(false), 2000);
} catch (err) {
toast.error("Failed to copy code");
@@ -48,78 +91,99 @@ export const CodeBlock = ({
};
return (
-
- {/* macOS-style Header with Traffic Lights */}
-
+
+ {/* Header with primary color accents */}
+
- {/* macOS Traffic Light Dots */}
-
-
-
-
+ {/* VS Code style dots with primary color */}
+
- {/* Language Label */}
-
+
{language}
- {/* Copy Button */}
{copied ? (
<>
-
- Copied!
+
+ Copied!
>
) : (
<>
-
- Copy
+
+ Copy
>
)}
- {/* Code Content */}
-
-
- {code}
-
+ {/* Code content with Shiki highlighting and logical line numbers */}
+
+ {showLineNumbers && (
+
+ {Array.from({ length: lineCount }, (_, i) => (
+
+ {i + 1}
+
+ ))}
+
+ )}
+
+ {highlightedCode ? (
+
+ ) : (
+
+ {code}
+
+ )}
+
+ {/* Subtle primary color accent at bottom */}
+
);
};
\ No newline at end of file
diff --git a/src/components/ComponentCard.tsx b/src/components/ComponentCard.tsx
index b90698d..387e851 100644
--- a/src/components/ComponentCard.tsx
+++ b/src/components/ComponentCard.tsx
@@ -137,7 +137,7 @@ export const ComponentCard = ({
{/* Tabs Section - Enhanced */}
-
+
-
+
{preview}
diff --git a/src/components/Header.tsx b/src/components/Header.tsx
index aa3a781..50b645a 100644
--- a/src/components/Header.tsx
+++ b/src/components/Header.tsx
@@ -1,4 +1,4 @@
-"use client"
+"use client";
import { useState, useEffect } from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
@@ -22,6 +22,10 @@ const Header = () => {
const { theme, setTheme } = useTheme();
const pathname = usePathname();
+ // Prevent hydration mismatch
+ const [mounted, setMounted] = useState(false);
+ useEffect(() => setMounted(true), []);
+
const navigation = [
{ name: "Home", href: "/", icon: Home },
{ name: "Components", href: "#components", icon: Code2 },
@@ -47,7 +51,6 @@ const Header = () => {
return (
<>
- {/* Skip to main content - Accessibility */}
Skip to main content
@@ -56,18 +59,23 @@ const Header = () => {
{/* Logo */}
-
+
-
- DevUI
-
+
DevUI
{/* Desktop Navigation */}
-
+
{navigation.map((item) => {
const isActive = pathname === item.href;
return (
@@ -75,8 +83,11 @@ const Header = () => {
{item.name}
@@ -89,19 +100,29 @@ const Header = () => {
{/* Actions */}
{/* Theme Toggle */}
-
setTheme(theme === "dark" ? "light" : "dark")}
- className="hover:bg-primary/10 transition-all duration-200 focus-ring"
- aria-label={`Switch to ${theme === "dark" ? "light" : "dark"} mode`}
- >
-
-
-
+ {mounted && (
+
+ setTheme(theme === "dark" ? "light" : "dark")
+ }
+ className="relative hover:bg-primary/10 transition-all duration-200 focus-ring"
+ aria-label={`Switch to ${
+ theme === "dark" ? "light" : "dark"
+ } mode`}
+ >
+
+
+
+ )}
{/* GitHub Link */}
-
+
{
{/* Star Button */}
-
+
{
href={item.href}
onClick={() => setIsMenuOpen(false)}
aria-current={isActive ? "page" : undefined}
- className={`flex items-center px-3 py-2 rounded-md text-base font-medium transition-all duration-200 focus-ring ${isActive
+ className={`flex items-center px-3 py-2 rounded-md text-base font-medium transition-all duration-200 focus-ring ${
+ isActive
? "bg-primary/10 text-foreground"
: "text-muted-foreground hover:text-foreground hover:bg-primary/10"
- }`}
+ }`}
>
{item.name}
@@ -189,4 +215,4 @@ const Header = () => {
);
};
-export default Header;
\ No newline at end of file
+export default Header;
diff --git a/src/components/ui/ThemeColorPicker.tsx b/src/components/ui/ThemeColorPicker.tsx
index aaafcda..2c76ba7 100644
--- a/src/components/ui/ThemeColorPicker.tsx
+++ b/src/components/ui/ThemeColorPicker.tsx
@@ -185,7 +185,7 @@ export default function ThemeColorPicker() {
}
`}
-
+
Date: Fri, 3 Oct 2025 20:17:08 +0600
Subject: [PATCH 3/4] release: new update (#154)
* fix: sync code with prod (#142)
* adding sidebar (#110) (#112)
Co-authored-by: vansh kabra <134841334+VANSH3104@users.noreply.github.com>
* V1.0.3 (#130)
* Add light/dark theme toggle in header (#121)
* t rebase --continue
Enhanced:redesign the component
* DarkMode Toggle
* Refactored Code Snippet UI (#122)
* Made Header & Cards more visually appealing
* Improved Search & Filter Design
* Formatted Code
* Made theme button bigger
* Changed animation settings for Header Text
* Minor UI Adjustments
* Redesigned the UI for code snippets
* Used shiki to get more modern code snippets with highlighting
* Refactored Code Snippet UI
---------
Co-authored-by: Deepanshu <144600350+Deepanshu1230@users.noreply.github.com>
Co-authored-by: Aneesh S
---------
Co-authored-by: vansh kabra <134841334+VANSH3104@users.noreply.github.com>
Co-authored-by: Deepanshu <144600350+Deepanshu1230@users.noreply.github.com>
Co-authored-by: Aneesh S
* docs(readme): add contributors section with contrib.rocks auto-generated image
* feat:NEW COMPONENT (#143)
* Create SECURITY.md
* feat: Feature/tooltip and fab components (#144)
* NEW COMPONENT
* add @radix-ui/react-tooltip and related components`
* feat: Add Versatile File Upload Component with Drag-and-Drop & Progress (#147)
* t rebase --continue
Enhanced:redesign the component
* DarkMode Toggle
* File Upload Drag And Drop
* feat: add skeleton loading support to ComponentCard (#134)
* Feature/fix slider invert direction, issue #56 (#150)
* fix-hydration issue for calendar
* fix/slider invert direction and visibility issue
* Fixed Dark Mode UI Issue (#151)
---------
Co-authored-by: vansh kabra <134841334+VANSH3104@users.noreply.github.com>
Co-authored-by: Deepanshu <144600350+Deepanshu1230@users.noreply.github.com>
Co-authored-by: Aneesh S
Co-authored-by: Durva Kadam
Co-authored-by: Mjtbvs <159575377+Nishat30@users.noreply.github.com>
Co-authored-by: dhruvil-1207
---
README.md | 12 +
SECURITY.md | 21 ++
package-lock.json | 37 ++-
package.json | 3 +-
src/app/globals.css | 42 ++-
src/components/ComponentCard.tsx | 146 ++++++----
src/components/ui/file-upload.tsx | 267 +++++++++++++++++++
src/components/ui/floating-action-button.tsx | 66 +++++
src/components/ui/sidebar.tsx | 109 ++++----
src/components/ui/skeleton.tsx | 27 ++
src/components/ui/sliderDemo.tsx | 23 ++
src/components/ui/tooltip.tsx | 30 +++
src/data/components.tsx | 113 +++++++-
13 files changed, 787 insertions(+), 109 deletions(-)
create mode 100644 SECURITY.md
create mode 100644 src/components/ui/file-upload.tsx
create mode 100644 src/components/ui/floating-action-button.tsx
create mode 100644 src/components/ui/skeleton.tsx
create mode 100644 src/components/ui/sliderDemo.tsx
create mode 100644 src/components/ui/tooltip.tsx
diff --git a/README.md b/README.md
index d1d8141..a4d2a44 100644
--- a/README.md
+++ b/README.md
@@ -92,3 +92,15 @@ npm run dev
**Hacktoberfest 2025:** Contribute, get PRs merged, and help us make **DevUI Components** the ultimate open-source component showcase!
+
+## 👥 Contributors
+
+Thanks to all the amazing contributors who make this project better! 💜
+
+
+
+
+
+
+
+Contributions of any kind are welcome! 🎉
\ No newline at end of file
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..034e848
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,21 @@
+# Security Policy
+
+## Supported Versions
+
+Use this section to tell people about which versions of your project are
+currently being supported with security updates.
+
+| Version | Supported |
+| ------- | ------------------ |
+| 5.1.x | :white_check_mark: |
+| 5.0.x | :x: |
+| 4.0.x | :white_check_mark: |
+| < 4.0 | :x: |
+
+## Reporting a Vulnerability
+
+Use this section to tell people how to report a vulnerability.
+
+Tell them where to go, how often they can expect to get an update on a
+reported vulnerability, what to expect if the vulnerability is accepted or
+declined, etc.
diff --git a/package-lock.json b/package-lock.json
index d1871f9..dbd400e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -18,6 +18,7 @@
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.13",
+ "@radix-ui/react-tooltip": "^1.2.8",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
@@ -40,7 +41,7 @@
"@types/react": "^19",
"@types/react-dom": "^19",
"@types/react-syntax-highlighter": "^15.5.13",
- "tailwindcss": "^4",
+ "tailwindcss": "^4.1.14",
"tw-animate-css": "^1.4.0",
"typescript": "^5"
}
@@ -1577,6 +1578,40 @@
}
}
},
+ "node_modules/@radix-ui/react-tooltip": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz",
+ "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-popper": "1.2.8",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-visually-hidden": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
diff --git a/package.json b/package.json
index 6a95796..4a0c13f 100644
--- a/package.json
+++ b/package.json
@@ -20,6 +20,7 @@
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.13",
+ "@radix-ui/react-tooltip": "^1.2.8",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
@@ -42,7 +43,7 @@
"@types/react": "^19",
"@types/react-dom": "^19",
"@types/react-syntax-highlighter": "^15.5.13",
- "tailwindcss": "^4",
+ "tailwindcss": "^4.1.14",
"tw-animate-css": "^1.4.0",
"typescript": "^5"
}
diff --git a/src/app/globals.css b/src/app/globals.css
index 46fcb8f..f3d5b0c 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -112,6 +112,45 @@
--sidebar-ring: oklch(0.556 0 0);
}
+/* --- Skeleton Shimmer Animation --- */
+
+/* Keyframes for the visual wave effect */
+@keyframes shimmer {
+ 0% {
+ background-position: -400% 0;
+ }
+ 100% {
+ background-position: 400% 0;
+ }
+}
+
+/* CSS Variables for Theme-Aware Colors */
+:root {
+ /* Light Theme Colors (e.g., matching a background of gray-50/100) */
+ --skeleton-bg-start: #e5e7eb; /* gray-200 */
+ --skeleton-bg-shimmer: #f3f4f6; /* gray-100 */
+}
+
+.dark {
+ /* Dark Theme Colors (e.g., matching a background of gray-900/800) */
+ --skeleton-bg-start: #374151; /* gray-700 */
+ --skeleton-bg-shimmer: #4b5563; /* gray-600 */
+}
+
+/* Base class applied to the skeleton element */
+.skeleton-shimmer {
+ /* Adjust color stops to be slightly wider for a smoother gradient */
+ background: linear-gradient(
+ 90deg,
+ var(--skeleton-bg-start) 0%,
+ var(--skeleton-bg-shimmer) 20%,
+ var(--skeleton-bg-start) 40%,
+ var(--skeleton-bg-start) 100%
+ );
+ background-size: 200% 100%; /* Important for the movement effect */
+ animation: shimmer 1.5s infinite cubic-bezier(0.4, 0, 0.2, 1);
+}
+
@layer base {
* {
@apply border-border outline-ring/50;
@@ -166,6 +205,7 @@
@apply gap-4 sm:gap-6 lg:gap-8;
}
+ /* Responsive gap utilities */
.gap-responsive-sm {
@apply gap-2 sm:gap-3 lg:gap-4;
}
@@ -410,4 +450,4 @@ input[type="search"] {
input[type="text"]:focus,
input[type="search"]:focus {
@apply shadow-sm shadow-primary/20;
-}
\ No newline at end of file
+}
diff --git a/src/components/ComponentCard.tsx b/src/components/ComponentCard.tsx
index b236c0f..29847da 100644
--- a/src/components/ComponentCard.tsx
+++ b/src/components/ComponentCard.tsx
@@ -1,9 +1,14 @@
-import { useState } from "react";
+import { useState, useEffect } from "react";
import { Card } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
-import { CodeBlock } from "./CodeBlock";
+// NOTE: CodeBlock was missing, so we define a basic version here to ensure the file compiles.
+// In a real project, this would be its own file with syntax highlighting (e.g., PrismJS or Shiki).
import { Eye, Code2, Info, ChevronDown, ChevronUp } from "lucide-react";
import { Button } from "@/components/ui/button";
+import { Skeleton} from "@/components/ui/skeleton";
+import { useTheme } from "next-themes";
+import { CodeBlock } from "@/components/CodeBlock";
+
interface PropData {
name: string;
@@ -22,6 +27,7 @@ interface ComponentCardProps {
propsData?: PropData[];
usageNotes?: string;
installCommand?: string;
+ loading?: boolean;
highlightQuery?: string;
}
@@ -34,51 +40,72 @@ export const ComponentCard = ({
propsData,
usageNotes,
installCommand,
+ loading = false, // Retaining default value from the loading branch
highlightQuery
}: ComponentCardProps) => {
const [activeTab, setActiveTab] = useState("preview");
const [showDetails, setShowDetails] = useState(false);
+ const { resolvedTheme } = useTheme();
+ const [mounted, setMounted] = useState(false);
+
+ useEffect(() => setMounted(true), []);
+ const isDark = mounted && resolvedTheme === 'dark';
+
+ // Conditional function to render the highlighted title
+ const renderTitle = () => {
+ if (!highlightQuery) {
+ return title;
+ }
+
+ // Split the title using a RegExp to capture the query for marking
+ return title.split(new RegExp(`(${highlightQuery})`, "ig")).map((part, idx) => (
+ part.toLowerCase() === highlightQuery.toLowerCase() ? (
+
+ {part}
+
+ ) : (
+ {part}
+ )
+ ));
+ };
return (
- {/* Header Section - Enhanced */}
-
- {highlightQuery ? (
- <>
- {title.split(new RegExp(`(${highlightQuery})`, "ig")).map((part, idx) => (
- part.toLowerCase() === highlightQuery.toLowerCase() ? (
-
- {part}
-
- ) : (
- {part}
- )
- ))}
- >
- ) : (
- title
- )}
-
- {category && (
+ {/* Title Rendering - Loading check takes precedence */}
+ {loading ? (
+
+ ) : (
+
+ {renderTitle()}
+
+ )}
+
+ {/* Category Badge */}
+ {!loading && category && (
{category}
)}
-
- {description}
-
+ {/* Description Rendering */}
+ {loading ? (
+ <>
+
+
+ >
+ ) : (
+
{description}
+ )}
- {/* Quick Actions Row */}
{(propsData || installCommand) && (
- {propsData && (
+ {!loading && propsData && (
)}
- {installCommand && (
+ {loading && }
+ {!loading && installCommand && (
{installCommand}
@@ -103,8 +131,7 @@ export const ComponentCard = ({
)}
- {/* Expandable Props Documentation */}
- {showDetails && propsData && (
+ {showDetails && propsData && !loading && (
@@ -117,26 +144,20 @@ export const ComponentCard = ({
className="p-3 rounded-lg bg-card border border-border hover:border-primary/50 transition-colors"
>
-
- {prop.name}
-
+ {prop.name}
{prop.required && (
required
)}
-
- {prop.type}
-
+ {prop.type}
{prop.default && (
default: {prop.default}
)}
-
- {prop.description}
-
+
{prop.description}
))}
@@ -150,23 +171,40 @@ export const ComponentCard = ({
)}
- {/* Tabs Section - Enhanced */}
-
+
-
- Preview
+ {loading ? (
+
+
+
+
+ ) : (
+ <>
+
+ Preview
+ >
+ )}
-
- Code
+ {loading ? (
+
+
+
+
+ ) : (
+ <>
+
+ Code
+ >
+ )}
@@ -175,17 +213,21 @@ export const ComponentCard = ({
value="preview"
className="p-4 sm:p-6 lg:p-8 min-h-[200px] sm:min-h-[240px]"
>
-
-
- {preview}
+ {loading ? (
+
+ ) : (
+
-
+ )}
-
+ {loading ? : }
);
-};
\ No newline at end of file
+};
diff --git a/src/components/ui/file-upload.tsx b/src/components/ui/file-upload.tsx
new file mode 100644
index 0000000..073572e
--- /dev/null
+++ b/src/components/ui/file-upload.tsx
@@ -0,0 +1,267 @@
+"use client"
+
+import * as React from "react"
+import { cn } from "@/lib/utils"
+import { Button } from "./button"
+import { Progress } from "./progress"
+
+interface FileUploadProps extends React.HTMLAttributes
{
+ onFileSelect?: (files: File[]) => void
+ onFileUpload?: (files: File[]) => Promise
+ accept?: string
+ multiple?: boolean
+ maxSize?: number // bytes
+ maxFiles?: number
+ disabled?: boolean
+ showProgress?: boolean
+ variant?: "default" | "compact" | "dropzone"
+}
+
+interface FileItem {
+ file: File
+ id: string
+ progress: number
+ status: "pending" | "uploading" | "success" | "error"
+ error?: string | null
+}
+
+// --- Helper Components ---
+const StatusIcon = ({ status }: { status: FileItem["status"] }) => {
+ switch (status) {
+ case "success":
+ return ✔️
+ case "error":
+ return ❌
+ case "uploading":
+ return ⏳
+ default:
+ return 📄
+ }
+}
+
+const FileItemRow = ({
+ fileItem,
+ removeFile,
+ showProgress,
+}: {
+ fileItem: FileItem
+ removeFile: (id: string) => void
+ showProgress?: boolean
+}) => (
+
+
+
+
+
+
{fileItem.file.name}
+
+ {(fileItem.file.size / 1024).toFixed(2)} KB
+ {fileItem.error && {fileItem.error} }
+
+
+
+
removeFile(fileItem.id)}
+ disabled={fileItem.status === "uploading"}
+ className="h-8 w-8 p-0"
+ >
+ ✖️
+
+
+ {showProgress && fileItem.status === "uploading" && (
+
+ )}
+
+)
+
+// --- Main Component ---
+const FileUpload = React.forwardRef(
+ (
+ {
+ className,
+ onFileSelect,
+ onFileUpload,
+ accept = "*/*",
+ multiple = false,
+ maxSize = 10 * 1024 * 1024,
+ maxFiles = multiple ? 5 : 1,
+ disabled = false,
+ showProgress = true,
+ variant = "default",
+ children,
+ ...props
+ },
+ ref
+ ) => {
+ const [files, setFiles] = React.useState([])
+ const [isDragOver, setIsDragOver] = React.useState(false)
+ const [isUploading, setIsUploading] = React.useState(false)
+ const fileInputRef = React.useRef(null)
+
+ const generateId = () => Math.random().toString(36).substr(2, 9)
+ const formatFileSize = (bytes: number) => {
+ if (bytes === 0) return "0 Bytes"
+ const k = 1024
+ const sizes = ["Bytes", "KB", "MB", "GB"]
+ const i = Math.floor(Math.log(bytes) / Math.log(k))
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]
+ }
+
+ const validateFile = (file: File): string | null => {
+ if (file.size > maxSize) return `File size exceeds ${formatFileSize(maxSize)}`
+ if (accept !== "*/*" && !accept.split(",").includes(`.${file.name.split(".").pop()}`))
+ return "Invalid file type"
+ return null
+ }
+
+ const handleFileSelect = (selectedFiles: FileList | null) => {
+ if (!selectedFiles || disabled) return
+ const newFiles: FileItem[] = Array.from(selectedFiles).map((file) => ({
+ file,
+ id: generateId(),
+ progress: 0,
+ status: validateFile(file) ? "error" : "pending",
+ error: validateFile(file),
+ }))
+
+ if (files.length + newFiles.length > maxFiles) {
+ alert(`Maximum ${maxFiles} files allowed`)
+ return
+ }
+
+ const updatedFiles = multiple ? [...files, ...newFiles] : newFiles
+ setFiles(updatedFiles)
+ onFileSelect?.(updatedFiles.map((f) => f.file))
+ }
+
+ const handleUpload = async () => {
+ if (!onFileUpload || isUploading) return
+ setIsUploading(true)
+ const pendingFiles = files.filter((f) => f.status === "pending")
+
+ try {
+ for (const fileItem of pendingFiles) {
+ setFiles((prev) =>
+ prev.map((f) =>
+ f.id === fileItem.id ? { ...f, status: "uploading", progress: 0 } : f
+ )
+ )
+ for (let progress = 0; progress <= 100; progress += 20) {
+ await new Promise((r) => setTimeout(r, 100))
+ setFiles((prev) =>
+ prev.map((f) => (f.id === fileItem.id ? { ...f, progress } : f))
+ )
+ }
+ setFiles((prev) =>
+ prev.map((f) =>
+ f.id === fileItem.id ? { ...f, status: "success", progress: 100 } : f
+ )
+ )
+ }
+ await onFileUpload(pendingFiles.map((f) => f.file))
+ } catch {
+ setFiles((prev) =>
+ prev.map((f) =>
+ pendingFiles.some((p) => p.id === f.id)
+ ? { ...f, status: "error", error: "Upload failed" }
+ : f
+ )
+ )
+ } finally {
+ setIsUploading(false)
+ }
+ }
+
+ const removeFile = (id: string) => setFiles((prev) => prev.filter((f) => f.id !== id))
+ const clearAll = () => {
+ setFiles([])
+ if (fileInputRef.current) fileInputRef.current.value = ""
+ }
+
+ const handleDragOver = (e: React.DragEvent) => {
+ e.preventDefault()
+ if (!disabled) setIsDragOver(true)
+ }
+ const handleDragLeave = (e: React.DragEvent) => {
+ e.preventDefault()
+ setIsDragOver(false)
+ }
+ const handleDrop = (e: React.DragEvent) => {
+ e.preventDefault()
+ setIsDragOver(false)
+ if (!disabled) handleFileSelect(e.dataTransfer.files)
+ }
+ const handleClick = () => !disabled && fileInputRef.current?.click()
+
+ const dropzoneClasses = cn(
+ "relative w-full rounded-lg border-2 border-dashed transition-all duration-200",
+ isDragOver && "border-primary bg-primary/5",
+ disabled && "opacity-50 cursor-not-allowed",
+ variant === "compact" && "p-4",
+ variant === "default" && "p-8",
+ variant === "dropzone" && "p-12 min-h-[200px] flex flex-col items-center justify-center",
+ "hover:border-primary/50 focus-within:border-primary focus-within:ring-2 focus-within:ring-primary/20",
+ className
+ )
+
+ return (
+
+
+
handleFileSelect(e.target.files)}
+ className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
+ disabled={disabled}
+ />
+
+
{isDragOver ? "Drop files here" : "Click or drag files to upload"}
+
+ {accept === "*/*" ? "Any file type" : accept} up to {formatFileSize(maxSize)}
+ {multiple && ` (max ${maxFiles} files)`}
+
+ {children &&
{children}
}
+
+
+
+ {files.length > 0 && (
+
+
+
Selected Files ({files.length})
+
+ {onFileUpload && files.some((f) => f.status === "pending") && (
+
+ Upload
+
+ )}
+
+ Clear
+
+
+
+
+
+ {files.map((fileItem) => (
+
+ ))}
+
+
+ )}
+
+ )
+ }
+)
+
+FileUpload.displayName = "FileUpload"
+export { FileUpload, type FileUploadProps }
diff --git a/src/components/ui/floating-action-button.tsx b/src/components/ui/floating-action-button.tsx
new file mode 100644
index 0000000..86e2d05
--- /dev/null
+++ b/src/components/ui/floating-action-button.tsx
@@ -0,0 +1,66 @@
+"use client"
+
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+import { cn } from "@/lib/utils"
+
+const fabVariants = cva(
+ "fixed inline-flex items-center justify-center whitespace-nowrap rounded-full text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 shadow-lg hover:shadow-xl transform hover:scale-105 active:scale-95",
+ {
+ variants: {
+ variant: {
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
+ destructive:
+ "bg-destructive text-destructive-foreground hover:bg-destructive/90",
+ outline:
+ "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
+ secondary:
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ ghost: "hover:bg-accent hover:text-accent-foreground",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ default: "h-14 w-14",
+ sm: "h-12 w-12",
+ lg: "h-16 w-16",
+ icon: "h-10 w-10",
+ },
+ position: {
+ "bottom-right": "bottom-6 right-6",
+ "bottom-left": "bottom-6 left-6",
+ "top-right": "top-6 right-6",
+ "top-left": "top-6 left-6",
+ "bottom-center": "bottom-6 left-1/2 transform -translate-x-1/2",
+ "top-center": "top-6 left-1/2 transform -translate-x-1/2",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ position: "bottom-right",
+ },
+ }
+)
+
+export interface FloatingActionButtonProps
+ extends React.ButtonHTMLAttributes,
+ VariantProps {
+ asChild?: boolean
+}
+
+const FloatingActionButton = React.forwardRef(
+ ({ className, variant, size, position, asChild = false, children, ...props }, ref) => {
+ return (
+
+ {children}
+
+ )
+ }
+)
+FloatingActionButton.displayName = "FloatingActionButton"
+
+export { FloatingActionButton, fabVariants }
\ No newline at end of file
diff --git a/src/components/ui/sidebar.tsx b/src/components/ui/sidebar.tsx
index b8227c6..1811267 100644
--- a/src/components/ui/sidebar.tsx
+++ b/src/components/ui/sidebar.tsx
@@ -1,9 +1,9 @@
"use client";
-import React, { useState, useEffect } from 'react';
-import { ChevronRight, Menu, X } from 'lucide-react';
-import { cn } from '@/lib/utils';
-import { Button } from './button';
+import React, { useState, useEffect } from "react";
+import { ChevronRight, Menu, X } from "lucide-react";
+import { cn } from "@/lib/utils";
+import { Button } from "./button";
export interface MenuItem {
id: string;
@@ -27,12 +27,14 @@ interface SidebarContextType {
closeSidebar: () => void;
}
-const SidebarContext = React.createContext(undefined);
+const SidebarContext = React.createContext(
+ undefined
+);
export const useSidebar = () => {
const context = React.useContext(SidebarContext);
if (!context) {
- throw new Error('useSidebar must be used within a SidebarProvider');
+ throw new Error("useSidebar must be used within a SidebarProvider");
}
return context;
};
@@ -41,7 +43,9 @@ interface SidebarProviderProps {
children: React.ReactNode;
}
-export const SidebarProvider: React.FC = ({ children }) => {
+export const SidebarProvider: React.FC = ({
+ children,
+}) => {
const [isOpen, setIsOpen] = useState(false);
const [isMobile, setIsMobile] = useState(false);
@@ -55,22 +59,26 @@ export const SidebarProvider: React.FC = ({ children }) =>
};
checkMobile();
- window.addEventListener('resize', checkMobile);
- return () => window.removeEventListener('resize', checkMobile);
+ window.addEventListener("resize", checkMobile);
+ return () => window.removeEventListener("resize", checkMobile);
}, []);
const toggleSidebar = () => setIsOpen(!isOpen);
const closeSidebar = () => setIsOpen(false);
return (
-
+
{children}
);
};
// Hamburger Menu Button Component
-export const SidebarTrigger: React.FC<{ className?: string }> = ({ className }) => {
+export const SidebarTrigger: React.FC<{ className?: string }> = ({
+ className,
+}) => {
const { isOpen, toggleSidebar } = useSidebar();
return (
@@ -78,7 +86,10 @@ export const SidebarTrigger: React.FC<{ className?: string }> = ({ className })
variant="ghost"
size="icon"
onClick={toggleSidebar}
- className={cn("md:hidden hover:scale-105 active:scale-95 transition-transform", className)}
+ className={cn(
+ "md:hidden hover:scale-105 active:scale-95 transition-transform",
+ className
+ )}
aria-label={isOpen ? "Close sidebar" : "Open sidebar"}
>
{isOpen ? : }
@@ -93,10 +104,10 @@ interface MenuItemComponentProps {
onItemClick?: (item: MenuItem) => void;
}
-const MenuItemComponent: React.FC = ({
- item,
- level = 0,
- onItemClick
+const MenuItemComponent: React.FC = ({
+ item,
+ level = 0,
+ onItemClick,
}) => {
const [isExpanded, setIsExpanded] = useState(false);
const { closeSidebar, isMobile } = useSidebar();
@@ -124,36 +135,41 @@ const MenuItemComponent: React.FC = ({
"active:scale-[0.98]",
"py-2.5 my-0.5"
)}
- style={{
- paddingLeft: `${16 + (level * 16)}px`,
- paddingRight: '12px'
+ style={{
+ paddingLeft: `${16 + level * 16}px`,
+ paddingRight: "12px",
}}
aria-expanded={hasChildren ? isExpanded : undefined}
aria-label={item.label}
>
{/* Icon with subtle scale animation */}
- {item.icon || (level > 0 && )}
+ {item.icon ||
+ (level > 0 && (
+
+ ))}
-
+
{/* Label with color transition */}
{item.label}
-
+
{/* Badge */}
{item.badge && (
{item.badge}
)}
-
+
{/* Chevron with smooth rotation */}
{hasChildren && (
-
+
)}
@@ -184,18 +200,18 @@ const MenuItemComponent: React.FC = ({
};
// Main Sidebar Component
-export const Sidebar: React.FC = ({
- items,
- className,
- onItemClick
+export const Sidebar: React.FC = ({
+ items,
+ className,
+ onItemClick,
}) => {
const { isOpen, isMobile, closeSidebar } = useSidebar();
useEffect(() => {
if (isMobile && isOpen) {
- document.body.style.overflow = 'hidden';
+ document.body.style.overflow = "hidden";
return () => {
- document.body.style.overflow = 'unset';
+ document.body.style.overflow = "unset";
};
}
}, [isMobile, isOpen]);
@@ -218,23 +234,22 @@ export const Sidebar: React.FC = ({
"transition-all duration-300 ease-out shadow-2xl",
"w-72",
"md:relative md:translate-x-0 md:z-auto md:shadow-none",
- isMobile
- ? isOpen
- ? "translate-x-0"
- : "-translate-x-full"
- : "",
- !isMobile && (isOpen ? "translate-x-0" : "-translate-x-full md:translate-x-0"),
+ isMobile ? (isOpen ? "translate-x-0" : "-translate-x-full") : "",
+ !isMobile &&
+ (isOpen ? "translate-x-0" : "-translate-x-full md:translate-x-0"),
className
)}
aria-label="Sidebar navigation"
- style={{ width: '288px' }}
+ style={{ width: "288px" }}
>
{/* Enhanced Sidebar Header with gradient accent */}
@@ -274,7 +289,9 @@ export const Sidebar: React.FC
= ({
John Doe
- john@example.com
+
+ john@example.com
+
@@ -290,9 +307,9 @@ interface SidebarContentProps {
className?: string;
}
-export const SidebarContent: React.FC = ({
- children,
- className
+export const SidebarContent: React.FC = ({
+ children,
+ className,
}) => {
const { isOpen, isMobile } = useSidebar();
@@ -307,4 +324,4 @@ export const SidebarContent: React.FC = ({
{children}
);
-};
\ No newline at end of file
+};
diff --git a/src/components/ui/skeleton.tsx b/src/components/ui/skeleton.tsx
new file mode 100644
index 0000000..630e6ab
--- /dev/null
+++ b/src/components/ui/skeleton.tsx
@@ -0,0 +1,27 @@
+import React from "react";
+import clsx from "clsx";
+
+interface SkeletonProps {
+ width?: string | number;
+ height?: string | number;
+ rounded?: boolean | string;
+ className?: string;
+}
+
+export const Skeleton: React.FC = ({
+ width = "100%",
+ height = "1rem",
+ rounded = true,
+ className
+}) => {
+ return (
+
+ );
+};
\ No newline at end of file
diff --git a/src/components/ui/sliderDemo.tsx b/src/components/ui/sliderDemo.tsx
new file mode 100644
index 0000000..6841489
--- /dev/null
+++ b/src/components/ui/sliderDemo.tsx
@@ -0,0 +1,23 @@
+"use client";
+
+import React, { useState } from "react";
+// Import the component being demonstrated from the same directory
+import { Slider } from "./slider";
+
+export function SliderDemo() {
+ const [sliderValue, setSliderValue] = useState([50]);
+
+ return (
+
+
+
+ Current Value: {sliderValue[0]}
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/ui/tooltip.tsx b/src/components/ui/tooltip.tsx
new file mode 100644
index 0000000..5a1a0d8
--- /dev/null
+++ b/src/components/ui/tooltip.tsx
@@ -0,0 +1,30 @@
+"use client"
+
+import * as React from "react"
+import * as TooltipPrimitive from "@radix-ui/react-tooltip"
+
+import { cn } from "@/lib/utils"
+
+const TooltipProvider = TooltipPrimitive.Provider
+
+const Tooltip = TooltipPrimitive.Root
+
+const TooltipTrigger = TooltipPrimitive.Trigger
+
+const TooltipContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, sideOffset = 4, ...props }, ref) => (
+
+))
+TooltipContent.displayName = TooltipPrimitive.Content.displayName
+
+export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
\ No newline at end of file
diff --git a/src/data/components.tsx b/src/data/components.tsx
index 89ae177..3702ad2 100644
--- a/src/data/components.tsx
+++ b/src/data/components.tsx
@@ -1,3 +1,5 @@
+// src/data/components.tsx
+
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
@@ -57,6 +59,19 @@ import {
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer";
+import { FileUpload } from "@/components/ui/file-upload";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from "@/components/ui/tooltip";
+import { FloatingActionButton } from "@/components/ui/floating-action-button";
+import { PlusIcon, HeartIcon, MessageCircleIcon } from "lucide-react";
+// REMOVED: import React, { useState } from "react";
+// ADDED: Import the component that now correctly encapsulates useState:
+import { SliderDemo } from "@/components/ui/sliderDemo";
+
export const componentsData = [
{
@@ -165,16 +180,22 @@ export function SwitchDemo() {
description:
"An input where the user selects a value from within a given range.",
category: "Form",
- preview: (
-
-
-
- ),
+ // FIX: Using the separate functional component
+ preview: ,
+
+ // Code snippet reflecting the correct controlled usage for users
code: `import { Slider } from "@/components/ui/slider"
+import { useState } from "react"
export function SliderDemo() {
+ const [value, setValue] = useState([50]);
return (
-
+
)
}`,
},
@@ -704,7 +725,7 @@ export function ToastDemo() {
-}`,
+ )`,
},
{
id: "sidebar",
@@ -930,4 +951,80 @@ export function DrawerDemo() {
)
}`,
},
-];
+ {
+ id: "file-upload",
+ title: "File Upload",
+ description: "A versatile file upload component with drag & drop, progress tracking, and multiple variants.",
+ category: "Form",
+ preview: (
+
+ console.log("Selected files:", files)}
+ onFileUpload={async (files) => {
+ // Simulate upload
+ console.log("Uploading files:", files);
+ await new Promise((resolve) => setTimeout(resolve, 2000));
+ }}
+ showProgress={true}
+ />
+
+ ),
+ code: `import { FileUpload } from "@/components/ui/file-upload"
+
+export function FileUploadDemo() {
+ const handleFileSelect = (files: File[]) => {
+ console.log("Selected files:", files)
+ }
+
+ const handleFileUpload = async (files: File[]) => {
+ // Simulate upload process
+ console.log("Uploading files:", files)
+ await new Promise((resolve) => setTimeout(resolve, 2000))
+ }
+
+ return (
+
+ {/* Compact Variant */}
+
+
+ {/* Default Dropzone Variant */}
+
+
+ {/* Large Dropzone Variant */}
+
+
+ Drag and drop files here or click to browse
+
+
+
+ )
+}`,
+ },
+];
\ No newline at end of file
From b827e4f17b7c5ca841b68c391cf2f4d2af7a3c2e Mon Sep 17 00:00:00 2001
From: Fahim Ahammed Firoz
Date: Fri, 3 Oct 2025 22:34:50 +0600
Subject: [PATCH 4/4] chore: update ci
---
.github/workflows/release-please.yml | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml
index 96c1dab..b79ba65 100644
--- a/.github/workflows/release-please.yml
+++ b/.github/workflows/release-please.yml
@@ -1,7 +1,7 @@
on:
push:
branches:
- - main
+ - stage
permissions:
contents: write
@@ -18,3 +18,5 @@ jobs:
token: ${{ secrets.GH_TOKEN }}
config-file: release-please-config.json
manifest-file: .release-please-manifest.json
+ default-branch: main
+ target-branch: main