mirror of
https://github.com/moumen-soliman/uitripled
synced 2026-04-21 13:37:20 +00:00
Add author credits; register marquee & dashboard
Expose an author.username on Component and render a GitHub credit link in AnimationDetailPage, AnimationCard and the components landing cards. Add a new NativeMarquee component and demo (react-baseui) and register a Dashboard block and Native Marquee component in the registry (packages/registry public files). Fix registry paths for the flip-text resource and update native-user-card implementation content (motion/hover tweaks). Also update public llms timestamps. These changes wire up author attribution and add/register new UI pieces in the registry.
This commit is contained in:
parent
4831a011e2
commit
86378f1a26
20 changed files with 567 additions and 33 deletions
|
|
@ -30,6 +30,7 @@ import {
|
|||
ChevronDown,
|
||||
Copy,
|
||||
FileText,
|
||||
Github,
|
||||
Info,
|
||||
RefreshCw,
|
||||
} from "lucide-react";
|
||||
|
|
@ -621,6 +622,23 @@ export default function AnimationDetailPageClient({
|
|||
<p className="mb-4 text-sm text-muted-foreground sm:text-base">
|
||||
{component.description}
|
||||
</p>
|
||||
{component.author?.username && (
|
||||
<a
|
||||
href={`https://github.com/${component.author.username}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="mb-4 inline-flex items-center gap-1.5 text-sm text-muted-foreground transition-colors hover:text-foreground"
|
||||
aria-label={`View ${component.author.username} on GitHub`}
|
||||
>
|
||||
<span>
|
||||
By{" "}
|
||||
<span className="font-medium text-foreground">
|
||||
@{component.author.username}
|
||||
</span>
|
||||
</span>
|
||||
<Github className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import { Component, categoryNames } from "@/types";
|
||||
import { motion } from "framer-motion";
|
||||
import { Github } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
|
||||
|
|
@ -39,7 +40,7 @@ export function AnimationCard({ animation }: AnimationCardProps) {
|
|||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-4">
|
||||
<div className="p-4 pb-3">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<span className="rounded border border-border bg-card px-2 py-0.5 text-xs font-medium text-muted-foreground">
|
||||
{categoryNames[animation.category]}
|
||||
|
|
@ -73,6 +74,25 @@ export function AnimationCard({ animation }: AnimationCardProps) {
|
|||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
{animation.author?.username && (
|
||||
<div className="flex items-center justify-end border-t border-border/60 px-4 py-2">
|
||||
<a
|
||||
href={`https://github.com/${animation.author.username}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1.5 text-xs text-muted-foreground transition-colors hover:text-foreground"
|
||||
aria-label={`View ${animation.author.username} on GitHub`}
|
||||
>
|
||||
<span>
|
||||
By{" "}
|
||||
<span className="font-medium text-foreground">
|
||||
@{animation.author.username}
|
||||
</span>
|
||||
</span>
|
||||
<Github className="h-3 w-3" aria-hidden="true" />
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { Component, ComponentCategory, categoryNames } from "@/types";
|
|||
import { Input } from "@uitripled/react-shadcn/ui/input";
|
||||
import { ScrollArea } from "@uitripled/react-shadcn/ui/scroll-area";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { ArrowRight, Search } from "lucide-react";
|
||||
import { ArrowRight, Github, Search } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
|
|
@ -18,10 +18,11 @@ const ComponentCard = ({ component }: { component: Component }) => {
|
|||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.9 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="group relative flex h-full flex-col overflow-hidden rounded-xl border border-border bg-card/50 transition-all hover:-translate-y-1 hover:border-primary/50 hover:bg-card hover:shadow-md"
|
||||
>
|
||||
<Link
|
||||
href={`/components/${component.id}`}
|
||||
className="group relative flex h-full flex-col overflow-hidden rounded-xl border border-border bg-card/50 p-4 transition-all hover:-translate-y-1 hover:border-primary/50 hover:bg-card hover:shadow-md"
|
||||
className="flex flex-1 flex-col p-4"
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
@ -56,6 +57,25 @@ const ComponentCard = ({ component }: { component: Component }) => {
|
|||
</div>
|
||||
)}
|
||||
</Link>
|
||||
{component.author?.username && (
|
||||
<div className="flex items-center justify-end border-t border-border/60 px-4 py-2">
|
||||
<a
|
||||
href={`https://github.com/${component.author.username}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1.5 text-[11px] text-muted-foreground transition-colors hover:text-foreground"
|
||||
aria-label={`View ${component.author.username} on GitHub`}
|
||||
>
|
||||
<span>
|
||||
By{" "}
|
||||
<span className="font-medium text-foreground">
|
||||
@{component.author.username}
|
||||
</span>
|
||||
</span>
|
||||
<Github className="h-3 w-3" aria-hidden="true" />
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
> Production-ready UI components, blocks, and full pages available in shadcn/ui and Base UI, powered by Framer Motion. Copy-paste components — not an installable npm package.
|
||||
|
||||
**Last Updated:** 2026-03-13T00:55:24.849Z
|
||||
**Last Updated:** 2026-04-14T15:39:21.158Z
|
||||
|
||||
- Components come in **shadcn/ui**, **Base UI**, and **Carbon** variants (the suffix in the name indicates the style system, e.g. `-shadcnui`, `-baseui`, `-carbon`)
|
||||
- **Copy-paste model** — browse the docs, preview a component, then copy its code directly into your project
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
> Production-ready UI components, blocks, and full pages available in shadcn/ui and Base UI, powered by Framer Motion. Copy-paste components — not an installable npm package.
|
||||
|
||||
**Last Updated:** 2026-03-13T00:55:24.833Z
|
||||
**Last Updated:** 2026-04-14T15:39:21.142Z
|
||||
|
||||
- Components come in **shadcn/ui**, **Base UI**, and **Carbon** variants (the suffix in the name indicates the style system, e.g. `-shadcnui`, `-baseui`, `-carbon`)
|
||||
- **Copy-paste model** — browse the docs, preview a component, then copy its code directly into your project
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@
|
|||
],
|
||||
"files": [
|
||||
{
|
||||
"path": "@uitripled/react-baseui/src/components/native/native-flip-text-baseui.tsx",
|
||||
"content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { AnimatePresence, motion } from \"framer-motion\";\nimport { useEffect, useState } from \"react\";\n\ninterface NativeFlipTextProps {\n /**\n * Array of words to flip through.\n */\n words: string[];\n /**\n * Duration of each word display in ms.\n * Default: 2000\n */\n duration?: number;\n className?: string;\n}\n\nexport function NativeFlipText({\n words,\n duration = 2000,\n className,\n}: NativeFlipTextProps) {\n const [index, setIndex] = useState(0);\n\n useEffect(() => {\n const interval = setInterval(() => {\n setIndex((prevIndex) => (prevIndex + 1) % words.length);\n }, duration);\n\n return () => clearInterval(interval);\n }, [words.length, duration]);\n\n return (\n <div\n className={cn(\n \"inline-flex items-center justify-center overflow-hidden\",\n className\n )}\n >\n <AnimatePresence mode=\"wait\">\n <motion.div\n key={words[index]}\n initial={{ rotateX: -90, opacity: 0, filter: \"blur(6px)\" }}\n animate={{ rotateX: 0, opacity: 1, filter: \"blur(0px)\" }}\n exit={{ rotateX: 90, opacity: 0, filter: \"blur(6px)\" }}\n transition={{\n type: \"spring\",\n stiffness: 100,\n damping: 20,\n opacity: { duration: 0.3 },\n filter: { duration: 0.3 },\n rotateX: { duration: 0.4 },\n }}\n className=\"text-center\"\n >\n {words[index]}\n </motion.div>\n </AnimatePresence>\n </div>\n );\n}\n",
|
||||
"path": "@uitripled/react-carbon/src/components/native/native-flip-text-carbon-baseui.tsx",
|
||||
"content": "// Error: Could not read file @uitripled/react-carbon/src/components/native/native-flip-text-carbon-baseui.tsx\n// File not found: @uitripled/react-carbon/src/components/native/native-flip-text-carbon-baseui.tsx",
|
||||
"type": "registry:component",
|
||||
"target": "components/uitripled/native-flip-text-baseui.tsx"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
"files": [
|
||||
{
|
||||
"path": "@uitripled/react-baseui/src/components/native/native-user-card-baseui.tsx",
|
||||
"content": "\"use client\";\n\nimport { Avatar } from \"@base-ui/react/avatar\";\nimport { Button } from \"@base-ui/react/button\";\nimport { cn } from \"@/lib/utils\";\nimport { motion, MotionConfig } from \"framer-motion\";\nimport { ArrowRight } from \"lucide-react\";\n\ninterface NativeUserCardProps {\n imageSrc: string;\n name: string;\n handle: string;\n href?: string;\n onClick?: () => void;\n className?: string;\n}\n\nconst transition = {\n type: \"spring\" as const,\n stiffness: 400,\n damping: 30,\n};\n\nexport function NativeUserCard({\n imageSrc,\n name,\n handle,\n href = \"#\",\n onClick,\n className,\n}: NativeUserCardProps) {\n const CardContent = (\n <motion.div\n initial={{ opacity: 0, scale: 0.98, y: 5 }}\n animate={{ opacity: 1, scale: 1, y: 0 }}\n transition={transition}\n className={cn(\n \"group relative flex w-full max-w-full items-center justify-between gap-4 rounded-lg border border-border bg-card p-1 transition-all duration-300\",\n className\n )}\n >\n <div className=\"flex min-w-0 flex-1 items-center gap-2\">\n <div className=\"relative h-10 w-10 shrink-0 overflow-hidden rounded-lg sm:h-12 sm:w-12\">\n <Avatar.Root className=\"h-full w-full rounded-lg\">\n <Avatar.Image src={imageSrc} alt={name} className=\"h-full w-full object-cover\" />\n <Avatar.Fallback className=\"flex h-full w-full items-center justify-center bg-muted text-muted-foreground font-semibold\">\n {name.charAt(0)}\n </Avatar.Fallback>\n </Avatar.Root>\n </div>\n\n <div className=\"flex min-w-0 flex-col\">\n <h3 className=\"truncate text-sm font-semibold leading-none tracking-tight text-foreground\">\n {name}\n </h3>\n <p className=\"truncate text-xs font-medium text-muted-foreground\">\n {handle}\n </p>\n </div>\n </div>\n\n <div className=\"relative shrink-0 pl-2\">\n <Button\n className={cn(\n \"flex h-10 w-10 shrink-0 cursor-pointer items-center justify-center rounded-lg bg-primary text-primary-foreground opacity-100 transition-transform duration-300 group-hover:scale-105 sm:h-12 sm:w-12\",\n \"outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\"\n )}\n >\n <motion.div whileTap={{ scale: 0.95 }}>\n <ArrowRight className=\"h-3 w-3\" />\n </motion.div>\n </Button>\n </div>\n </motion.div>\n );\n\n return (\n <MotionConfig transition={transition}>\n {onClick ? (\n <Button\n onClick={onClick}\n className=\"block w-full max-w-full text-left outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded-lg h-auto p-0 hover:bg-transparent\"\n aria-label={`View profile of ${name}`}\n >\n {CardContent}\n </Button>\n ) : (\n <a\n href={href}\n className=\"block w-full max-w-full outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded-lg\"\n aria-label={`View profile of ${name}`}\n >\n {CardContent}\n </a>\n )}\n </MotionConfig>\n );\n}\n",
|
||||
"content": "\"use client\";\n\nimport { Avatar } from \"@base-ui/react/avatar\";\nimport { Button } from \"@base-ui/react/button\";\nimport { cn } from \"@/lib/utils\";\nimport { motion, MotionConfig } from \"framer-motion\";\nimport { ArrowRight } from \"lucide-react\";\n\ninterface NativeUserCardProps {\n imageSrc: string;\n name: string;\n handle: string;\n href?: string;\n onClick?: () => void;\n className?: string;\n}\n\nconst transition = {\n type: \"spring\" as const,\n stiffness: 400,\n damping: 30,\n};\n\nexport function NativeUserCard({\n imageSrc,\n name,\n handle,\n href = \"#\",\n onClick,\n className,\n}: NativeUserCardProps) {\n const CardContent = (\n <motion.div\n initial=\"initial\"\n animate=\"animate\"\n whileHover=\"hover\"\n variants={{\n initial: { opacity: 0, scale: 0.98, y: 5 },\n animate: { opacity: 1, scale: 1, y: 0 },\n }}\n transition={transition}\n className={cn(\n \"group relative flex w-full max-w-full items-center justify-between gap-4 rounded-lg border border-border bg-card p-1 transition-all duration-300\",\n className\n )}\n >\n <div className=\"flex min-w-0 flex-1 items-center gap-2\">\n <div className=\"relative h-10 w-10 shrink-0 overflow-hidden rounded-lg sm:h-12 sm:w-12\">\n <Avatar.Root className=\"h-full w-full rounded-lg\">\n <Avatar.Image src={imageSrc} alt={name} className=\"h-full w-full object-cover\" />\n <Avatar.Fallback className=\"flex h-full w-full items-center justify-center bg-muted text-muted-foreground font-semibold\">\n {name.charAt(0)}\n </Avatar.Fallback>\n </Avatar.Root>\n </div>\n\n <div className=\"flex min-w-0 flex-col\">\n <h3 className=\"truncate text-sm font-semibold leading-none tracking-tight text-foreground\">\n {name}\n </h3>\n <p className=\"truncate text-xs font-medium text-muted-foreground\">\n {handle}\n </p>\n </div>\n </div>\n\n <div className=\"relative shrink-0 pl-2\">\n <Button\n className={cn(\n \"flex h-10 w-10 shrink-0 cursor-pointer items-center justify-center rounded-lg bg-primary text-primary-foreground opacity-100 transition-transform duration-300 sm:h-12 sm:w-12\",\n \"outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\"\n )}\n >\n <motion.div whileTap={{ scale: 0.95 }}>\n <motion.span\n className=\"inline-block\"\n variants={{\n hover: { x: 3 },\n }}\n transition={{ type: \"spring\", stiffness: 400, damping: 25 }}\n >\n <ArrowRight className=\"h-3 w-3\" />\n </motion.span>\n </motion.div>\n </Button>\n </div>\n </motion.div>\n );\n\n return (\n <MotionConfig transition={transition}>\n {onClick ? (\n <Button\n onClick={onClick}\n className=\"block w-full max-w-full text-left outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded-lg h-auto p-0 hover:bg-transparent\"\n aria-label={`View profile of ${name}`}\n >\n {CardContent}\n </Button>\n ) : (\n <a\n href={href}\n className=\"block w-full max-w-full outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded-lg\"\n aria-label={`View profile of ${name}`}\n >\n {CardContent}\n </a>\n )}\n </MotionConfig>\n );\n}\n",
|
||||
"type": "registry:component",
|
||||
"target": "components/uitripled/native-user-card-baseui.tsx"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3692,7 +3692,7 @@
|
|||
],
|
||||
"files": [
|
||||
{
|
||||
"path": "@uitripled/react-baseui/src/components/native/native-flip-text-baseui.tsx",
|
||||
"path": "@uitripled/react-carbon/src/components/native/native-flip-text-carbon-baseui.tsx",
|
||||
"type": "registry:component",
|
||||
"target": "components/uitripled/native-flip-text-baseui.tsx"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,6 +45,9 @@ export type Component = {
|
|||
isFree?: boolean;
|
||||
display?: boolean;
|
||||
availableIn?: UILibrary[]; // Which UI libraries have this component implemented
|
||||
author?: {
|
||||
username: string; // GitHub username — rendered as a credit link on component cards
|
||||
};
|
||||
};
|
||||
|
||||
export const categoryNames: Record<ComponentCategory, string> = {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,66 @@
|
|||
"use client";
|
||||
|
||||
import { NativeMarquee, SideFadeGradients } from "../native-marquee-baseui";
|
||||
import { Cpu, GitBranch, Globe, Layers, Shield, Zap } from "lucide-react";
|
||||
|
||||
const ITEMS = [
|
||||
{ id: "marquee-fast", icon: <Zap aria-hidden="true" />, label: "Fast" },
|
||||
{
|
||||
id: "marquee-secure",
|
||||
icon: <Shield aria-hidden="true" />,
|
||||
label: "Secure",
|
||||
},
|
||||
{
|
||||
id: "marquee-modular",
|
||||
icon: <Layers aria-hidden="true" />,
|
||||
label: "Modular",
|
||||
},
|
||||
{ id: "marquee-global", icon: <Globe aria-hidden="true" />, label: "Global" },
|
||||
{
|
||||
id: "marquee-versioned",
|
||||
icon: <GitBranch aria-hidden="true" />,
|
||||
label: "Versioned",
|
||||
},
|
||||
{
|
||||
id: "marquee-performant",
|
||||
icon: <Cpu aria-hidden="true" />,
|
||||
label: "Performant",
|
||||
},
|
||||
];
|
||||
|
||||
export function NativeMarqueeDemo() {
|
||||
return (
|
||||
<div className="flex overflow-hidden gap-2 relative flex-col">
|
||||
<SideFadeGradients />
|
||||
<NativeMarquee
|
||||
gap={8}
|
||||
pauseOnTouch={true}
|
||||
pauseOnHover={true}
|
||||
items={ITEMS}
|
||||
/>
|
||||
<NativeMarquee gap={8} reverse={true} speed={5} items={ITEMS} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function NativeMarqueeVertical() {
|
||||
return (
|
||||
<div className="flex overflow-hidden gap-2 relative h-96">
|
||||
<SideFadeGradients isVertical={true} />
|
||||
<NativeMarquee
|
||||
gap={8}
|
||||
pauseOnTouch={true}
|
||||
pauseOnHover={true}
|
||||
items={ITEMS}
|
||||
isVertical={true}
|
||||
/>
|
||||
<NativeMarquee
|
||||
gap={8}
|
||||
reverse={true}
|
||||
speed={5}
|
||||
items={ITEMS}
|
||||
isVertical={true}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,249 @@
|
|||
"use client";
|
||||
|
||||
import { cn } from "@uitripled/utils";
|
||||
import { motion, useAnimate } from "framer-motion";
|
||||
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
|
||||
interface NativeMarqueeItem {
|
||||
id: string;
|
||||
icon?: React.ReactNode;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface NativeMarqueeProps {
|
||||
className?: string;
|
||||
speed?: number;
|
||||
items: NativeMarqueeItem[];
|
||||
gap?: number;
|
||||
isVertical?: boolean;
|
||||
reverse?: boolean;
|
||||
pauseOnHover?: boolean;
|
||||
pauseOnTouch?: boolean;
|
||||
}
|
||||
|
||||
function NativeMarquee({
|
||||
speed = 10,
|
||||
gap = 2,
|
||||
isVertical = false,
|
||||
reverse = false,
|
||||
pauseOnHover = false,
|
||||
pauseOnTouch = false,
|
||||
items,
|
||||
}: NativeMarqueeProps) {
|
||||
const { containerRef, marqueeRef, multiplier, isMounted } = useMarquee({
|
||||
isVertical,
|
||||
});
|
||||
|
||||
const [scope1, animate1] = useAnimate();
|
||||
const [scope2, animate2] = useAnimate();
|
||||
const anim1Ref = useRef<any>(null);
|
||||
const anim2Ref = useRef<any>(null);
|
||||
|
||||
const duration = 100 / speed;
|
||||
|
||||
useEffect(() => {
|
||||
if (!isMounted || !scope1.current || !scope2.current) return;
|
||||
|
||||
const axis = isVertical ? "y" : "x";
|
||||
const from = reverse ? -100 : 0;
|
||||
const to = reverse ? 0 : -100;
|
||||
|
||||
const start = () => {
|
||||
anim1Ref.current = animate1(
|
||||
scope1.current,
|
||||
{ [axis]: [from + "%", to + "%"] },
|
||||
{ duration, ease: "linear", repeat: Infinity }
|
||||
);
|
||||
anim2Ref.current = animate2(
|
||||
scope2.current,
|
||||
{ [axis]: [from + "%", to + "%"] },
|
||||
{ duration, ease: "linear", repeat: Infinity }
|
||||
);
|
||||
};
|
||||
|
||||
const animationFrame = requestAnimationFrame(start);
|
||||
|
||||
return () => cancelAnimationFrame(animationFrame);
|
||||
}, [isMounted, isVertical, speed, reverse]);
|
||||
|
||||
const content = useMemo(
|
||||
() => (
|
||||
<ul className={`flex items-center gap-2 ${isVertical ? "flex-col" : ""}`}>
|
||||
{items.map((item) => (
|
||||
<li
|
||||
key={item.id}
|
||||
className={cn(
|
||||
"flex items-center gap-2 ",
|
||||
"border border-border bg-background px-4 py-2 rounded-full text-sm text-muted-foreground whitespace-nowrap",
|
||||
"hover:bg-muted hover:border-foreground/20",
|
||||
isVertical ? "w-full justify-center" : ""
|
||||
)}
|
||||
>
|
||||
{item.icon}
|
||||
{item.label}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
),
|
||||
[items]
|
||||
);
|
||||
|
||||
const copies = useCallback(
|
||||
(multiplier: number) => {
|
||||
const arraySize = multiplier >= 0 ? multiplier : 0;
|
||||
return [...Array(arraySize)].map((_, i) => (
|
||||
<span key={i} aria-hidden="true" className="shrink-0">
|
||||
{content}
|
||||
</span>
|
||||
));
|
||||
},
|
||||
[items]
|
||||
);
|
||||
|
||||
const pause = () => {
|
||||
anim1Ref.current?.pause();
|
||||
anim2Ref.current?.pause();
|
||||
};
|
||||
|
||||
const resume = () => {
|
||||
anim1Ref.current?.play();
|
||||
anim2Ref.current?.play();
|
||||
};
|
||||
|
||||
const hoverProps = pauseOnHover
|
||||
? { onMouseEnter: pause, onMouseLeave: resume }
|
||||
: {};
|
||||
const touchProps = pauseOnTouch
|
||||
? { onTouchStart: pause, onTouchEnd: resume }
|
||||
: {};
|
||||
|
||||
if (!isMounted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={cn(
|
||||
"flex shrink-0 grow-0 basis-auto overflow-hidden",
|
||||
isVertical ? "flex-col h-full" : "w-full "
|
||||
)}
|
||||
{...hoverProps}
|
||||
{...touchProps}
|
||||
>
|
||||
<motion.div ref={scope1}>
|
||||
<div
|
||||
className={cn(
|
||||
"flex grow-0 shrink-0 basis-auto min-w-min",
|
||||
isVertical ? "flex-col" : ""
|
||||
)}
|
||||
style={{
|
||||
marginRight: isVertical ? 0 : `${gap}px`,
|
||||
marginBottom: isVertical ? `${gap}px` : 0,
|
||||
gap: `${gap}px`,
|
||||
}}
|
||||
>
|
||||
<span ref={marqueeRef} className="shrink-0">
|
||||
{content}
|
||||
</span>
|
||||
{copies(multiplier - 1)}
|
||||
</div>
|
||||
</motion.div>
|
||||
<motion.div ref={scope2}>
|
||||
<div
|
||||
className={cn(
|
||||
"flex grow-0 shrink-0 basis-auto min-w-min",
|
||||
isVertical ? "flex-col" : ""
|
||||
)}
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
marginRight: isVertical ? 0 : `${gap}px`,
|
||||
marginBottom: isVertical ? `${gap}px` : 0,
|
||||
gap: `${gap}px`,
|
||||
}}
|
||||
>
|
||||
{copies(multiplier)}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function useMarquee(
|
||||
{ isVertical = false }: { isVertical?: boolean } = { isVertical: false }
|
||||
) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const marqueeRef = useRef<HTMLDivElement>(null);
|
||||
const [multiplier, setMultiplier] = useState(1);
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
|
||||
const calculateMultiplier = useCallback(() => {
|
||||
if (!containerRef.current || !marqueeRef.current) return;
|
||||
const containerRect = containerRef.current.getBoundingClientRect();
|
||||
const marqueeRect = marqueeRef.current.getBoundingClientRect();
|
||||
|
||||
let marqueeSize: number;
|
||||
let containerSize: number;
|
||||
let scale: number;
|
||||
if (isVertical) {
|
||||
containerSize = containerRect.height;
|
||||
marqueeSize = marqueeRect.height;
|
||||
scale = Math.ceil(containerSize / marqueeSize);
|
||||
} else {
|
||||
containerSize = containerRect.width;
|
||||
marqueeSize = marqueeRect.width;
|
||||
scale = Math.min(Math.ceil(containerSize / marqueeSize), 20);
|
||||
}
|
||||
setMultiplier(marqueeSize < containerSize ? scale : 1);
|
||||
}, [isVertical]);
|
||||
|
||||
useEffect(() => {
|
||||
setIsMounted(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isMounted) return;
|
||||
calculateMultiplier();
|
||||
if (marqueeRef.current && containerRef.current) {
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
calculateMultiplier();
|
||||
});
|
||||
resizeObserver.observe(marqueeRef.current);
|
||||
return () => resizeObserver.disconnect();
|
||||
}
|
||||
}, [calculateMultiplier, isMounted]);
|
||||
|
||||
return { containerRef, marqueeRef, multiplier, isMounted };
|
||||
}
|
||||
|
||||
function SideFadeGradients({ isVertical = false }: { isVertical?: boolean }) {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={cn(
|
||||
"absolute z-10 pointer-events-none",
|
||||
isVertical
|
||||
? "inset-x-0 top-0 h-24 bg-gradient-to-b from-background to-transparent"
|
||||
: "inset-y-0 left-0 w-24 bg-gradient-to-r from-background to-transparent"
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
"absolute z-10 pointer-events-none",
|
||||
isVertical
|
||||
? "inset-x-0 bottom-0 h-24 bg-gradient-to-t from-background to-transparent"
|
||||
: "inset-y-0 right-0 w-24 bg-gradient-to-l from-background to-transparent"
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export { NativeMarquee, useMarquee, SideFadeGradients };
|
||||
22
packages/registry/public/r/dashboard-baseui.json
Normal file
22
packages/registry/public/r/dashboard-baseui.json
Normal file
File diff suppressed because one or more lines are too long
22
packages/registry/public/r/native-marquee-baseui.json
Normal file
22
packages/registry/public/r/native-marquee-baseui.json
Normal file
File diff suppressed because one or more lines are too long
19
packages/registry/public/r/native-marquee-shadcnui.json
Normal file
19
packages/registry/public/r/native-marquee-shadcnui.json
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -14,7 +14,7 @@
|
|||
"files": [
|
||||
{
|
||||
"path": "@uitripled/react-baseui/src/components/native/native-user-card-baseui.tsx",
|
||||
"content": "\"use client\";\n\nimport { Avatar } from \"@base-ui/react/avatar\";\nimport { Button } from \"@base-ui/react/button\";\nimport { cn } from \"@/lib/utils\";\nimport { motion, MotionConfig } from \"framer-motion\";\nimport { ArrowRight } from \"lucide-react\";\n\ninterface NativeUserCardProps {\n imageSrc: string;\n name: string;\n handle: string;\n href?: string;\n onClick?: () => void;\n className?: string;\n}\n\nconst transition = {\n type: \"spring\" as const,\n stiffness: 400,\n damping: 30,\n};\n\nexport function NativeUserCard({\n imageSrc,\n name,\n handle,\n href = \"#\",\n onClick,\n className,\n}: NativeUserCardProps) {\n const CardContent = (\n <motion.div\n initial={{ opacity: 0, scale: 0.98, y: 5 }}\n animate={{ opacity: 1, scale: 1, y: 0 }}\n transition={transition}\n className={cn(\n \"group relative flex w-full max-w-full items-center justify-between gap-4 rounded-lg border border-border bg-card p-1 transition-all duration-300\",\n className\n )}\n >\n <div className=\"flex min-w-0 flex-1 items-center gap-2\">\n <div className=\"relative h-10 w-10 shrink-0 overflow-hidden rounded-lg sm:h-12 sm:w-12\">\n <Avatar.Root className=\"h-full w-full rounded-lg\">\n <Avatar.Image src={imageSrc} alt={name} className=\"h-full w-full object-cover\" />\n <Avatar.Fallback className=\"flex h-full w-full items-center justify-center bg-muted text-muted-foreground font-semibold\">\n {name.charAt(0)}\n </Avatar.Fallback>\n </Avatar.Root>\n </div>\n\n <div className=\"flex min-w-0 flex-col\">\n <h3 className=\"truncate text-sm font-semibold leading-none tracking-tight text-foreground\">\n {name}\n </h3>\n <p className=\"truncate text-xs font-medium text-muted-foreground\">\n {handle}\n </p>\n </div>\n </div>\n\n <div className=\"relative shrink-0 pl-2\">\n <Button\n className={cn(\n \"flex h-10 w-10 shrink-0 cursor-pointer items-center justify-center rounded-lg bg-primary text-primary-foreground opacity-100 transition-transform duration-300 group-hover:scale-105 sm:h-12 sm:w-12\",\n \"outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\"\n )}\n >\n <motion.div whileTap={{ scale: 0.95 }}>\n <ArrowRight className=\"h-3 w-3\" />\n </motion.div>\n </Button>\n </div>\n </motion.div>\n );\n\n return (\n <MotionConfig transition={transition}>\n {onClick ? (\n <Button\n onClick={onClick}\n className=\"block w-full max-w-full text-left outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded-lg h-auto p-0 hover:bg-transparent\"\n aria-label={`View profile of ${name}`}\n >\n {CardContent}\n </Button>\n ) : (\n <a\n href={href}\n className=\"block w-full max-w-full outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded-lg\"\n aria-label={`View profile of ${name}`}\n >\n {CardContent}\n </a>\n )}\n </MotionConfig>\n );\n}\n",
|
||||
"content": "\"use client\";\n\nimport { Avatar } from \"@base-ui/react/avatar\";\nimport { Button } from \"@base-ui/react/button\";\nimport { cn } from \"@/lib/utils\";\nimport { motion, MotionConfig } from \"framer-motion\";\nimport { ArrowRight } from \"lucide-react\";\n\ninterface NativeUserCardProps {\n imageSrc: string;\n name: string;\n handle: string;\n href?: string;\n onClick?: () => void;\n className?: string;\n}\n\nconst transition = {\n type: \"spring\" as const,\n stiffness: 400,\n damping: 30,\n};\n\nexport function NativeUserCard({\n imageSrc,\n name,\n handle,\n href = \"#\",\n onClick,\n className,\n}: NativeUserCardProps) {\n const CardContent = (\n <motion.div\n initial=\"initial\"\n animate=\"animate\"\n whileHover=\"hover\"\n variants={{\n initial: { opacity: 0, scale: 0.98, y: 5 },\n animate: { opacity: 1, scale: 1, y: 0 },\n }}\n transition={transition}\n className={cn(\n \"group relative flex w-full max-w-full items-center justify-between gap-4 rounded-lg border border-border bg-card p-1 transition-all duration-300\",\n className\n )}\n >\n <div className=\"flex min-w-0 flex-1 items-center gap-2\">\n <div className=\"relative h-10 w-10 shrink-0 overflow-hidden rounded-lg sm:h-12 sm:w-12\">\n <Avatar.Root className=\"h-full w-full rounded-lg\">\n <Avatar.Image src={imageSrc} alt={name} className=\"h-full w-full object-cover\" />\n <Avatar.Fallback className=\"flex h-full w-full items-center justify-center bg-muted text-muted-foreground font-semibold\">\n {name.charAt(0)}\n </Avatar.Fallback>\n </Avatar.Root>\n </div>\n\n <div className=\"flex min-w-0 flex-col\">\n <h3 className=\"truncate text-sm font-semibold leading-none tracking-tight text-foreground\">\n {name}\n </h3>\n <p className=\"truncate text-xs font-medium text-muted-foreground\">\n {handle}\n </p>\n </div>\n </div>\n\n <div className=\"relative shrink-0 pl-2\">\n <Button\n className={cn(\n \"flex h-10 w-10 shrink-0 cursor-pointer items-center justify-center rounded-lg bg-primary text-primary-foreground opacity-100 transition-transform duration-300 sm:h-12 sm:w-12\",\n \"outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\"\n )}\n >\n <motion.div whileTap={{ scale: 0.95 }}>\n <motion.span\n className=\"inline-block\"\n variants={{\n hover: { x: 3 },\n }}\n transition={{ type: \"spring\", stiffness: 400, damping: 25 }}\n >\n <ArrowRight className=\"h-3 w-3\" />\n </motion.span>\n </motion.div>\n </Button>\n </div>\n </motion.div>\n );\n\n return (\n <MotionConfig transition={transition}>\n {onClick ? (\n <Button\n onClick={onClick}\n className=\"block w-full max-w-full text-left outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded-lg h-auto p-0 hover:bg-transparent\"\n aria-label={`View profile of ${name}`}\n >\n {CardContent}\n </Button>\n ) : (\n <a\n href={href}\n className=\"block w-full max-w-full outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded-lg\"\n aria-label={`View profile of ${name}`}\n >\n {CardContent}\n </a>\n )}\n </MotionConfig>\n );\n}\n",
|
||||
"type": "registry:component",
|
||||
"target": "components/uitripled/native-user-card-baseui.tsx"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1315,7 +1315,7 @@
|
|||
],
|
||||
"files": [
|
||||
{
|
||||
"path": "@uitripled/react-baseui/src/components/stocks-dashboard/dashboard-baseui.tsx",
|
||||
"path": "@uitripled/react-baseui/src/components/components/stocks-dashboard/dashboard.tsx",
|
||||
"type": "registry:block",
|
||||
"target": "components/uitripled/dashboard-baseui.tsx"
|
||||
}
|
||||
|
|
@ -4050,6 +4050,48 @@
|
|||
"category": "native",
|
||||
"subcategory": null
|
||||
},
|
||||
{
|
||||
"name": "native-marquee-baseui",
|
||||
"type": "registry:component",
|
||||
"title": "Native Marquee",
|
||||
"description": "A smooth, infinite scrolling marquee for logos or text. (Base UI)",
|
||||
"registryDependencies": [
|
||||
"button"
|
||||
],
|
||||
"dependencies": [
|
||||
"framer-motion",
|
||||
"react"
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"path": "@uitripled/react-baseui/src/components/native/native-marquee-baseui.tsx",
|
||||
"type": "registry:component",
|
||||
"target": "components/uitripled/native-marquee-baseui.tsx"
|
||||
}
|
||||
],
|
||||
"category": "native",
|
||||
"subcategory": null
|
||||
},
|
||||
{
|
||||
"name": "native-marquee-shadcnui",
|
||||
"type": "registry:component",
|
||||
"title": "Native Marquee",
|
||||
"description": "A smooth, infinite scrolling marquee for logos or text.",
|
||||
"registryDependencies": [],
|
||||
"dependencies": [
|
||||
"framer-motion",
|
||||
"react"
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"path": "@uitripled/react-shadcn/src/components/native/native-marquee-shadcnui.tsx",
|
||||
"type": "registry:component",
|
||||
"target": "components/uitripled/native-marquee-shadcnui.tsx"
|
||||
}
|
||||
],
|
||||
"category": "native",
|
||||
"subcategory": null
|
||||
},
|
||||
{
|
||||
"name": "native-morphing-button-baseui",
|
||||
"type": "registry:component",
|
||||
|
|
|
|||
|
|
@ -1315,7 +1315,7 @@
|
|||
],
|
||||
"files": [
|
||||
{
|
||||
"path": "@uitripled/react-baseui/src/components/stocks-dashboard/dashboard-baseui.tsx",
|
||||
"path": "@uitripled/react-baseui/src/components/components/stocks-dashboard/dashboard.tsx",
|
||||
"type": "registry:block",
|
||||
"target": "components/uitripled/dashboard-baseui.tsx"
|
||||
}
|
||||
|
|
@ -3692,7 +3692,7 @@
|
|||
],
|
||||
"files": [
|
||||
{
|
||||
"path": "@uitripled/react-baseui/src/components/native/native-flip-text-baseui.tsx",
|
||||
"path": "@uitripled/react-carbon/src/components/native/native-flip-text-carbon-baseui.tsx",
|
||||
"type": "registry:component",
|
||||
"target": "components/uitripled/native-flip-text-baseui.tsx"
|
||||
}
|
||||
|
|
@ -4050,6 +4050,48 @@
|
|||
"category": "native",
|
||||
"subcategory": null
|
||||
},
|
||||
{
|
||||
"name": "native-marquee-baseui",
|
||||
"type": "registry:component",
|
||||
"title": "Native Marquee",
|
||||
"description": "A smooth, infinite scrolling marquee for logos or text. (Base UI)",
|
||||
"registryDependencies": [
|
||||
"button"
|
||||
],
|
||||
"dependencies": [
|
||||
"framer-motion",
|
||||
"react"
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"path": "@uitripled/react-baseui/src/components/native/native-marquee-baseui.tsx",
|
||||
"type": "registry:component",
|
||||
"target": "components/uitripled/native-marquee-baseui.tsx"
|
||||
}
|
||||
],
|
||||
"category": "native",
|
||||
"subcategory": null
|
||||
},
|
||||
{
|
||||
"name": "native-marquee-shadcnui",
|
||||
"type": "registry:component",
|
||||
"title": "Native Marquee",
|
||||
"description": "A smooth, infinite scrolling marquee for logos or text.",
|
||||
"registryDependencies": [],
|
||||
"dependencies": [
|
||||
"framer-motion",
|
||||
"react"
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"path": "@uitripled/react-shadcn/src/components/native/native-marquee-shadcnui.tsx",
|
||||
"type": "registry:component",
|
||||
"target": "components/uitripled/native-marquee-shadcnui.tsx"
|
||||
}
|
||||
],
|
||||
"category": "native",
|
||||
"subcategory": null
|
||||
},
|
||||
{
|
||||
"name": "native-morphing-button-baseui",
|
||||
"type": "registry:component",
|
||||
|
|
|
|||
|
|
@ -823,13 +823,13 @@ export const nativeComponents: Component[] = [
|
|||
availableIn: ["shadcnui", "carbon", "baseui"],
|
||||
},
|
||||
{
|
||||
id: "native-marquee",
|
||||
name: "Native Marquee",
|
||||
description: "A smooth, infinite scrolling marquee for logos or text.",
|
||||
category: "native",
|
||||
tags: ["marquee", "scroll", "animation", "infinite", "interactive", "native"],
|
||||
component: NativeMarquee,
|
||||
variants: [
|
||||
id: "native-marquee",
|
||||
name: "Native Marquee",
|
||||
description: "A smooth, infinite scrolling marquee for logos or text.",
|
||||
category: "native",
|
||||
tags: ["marquee", "scroll", "animation", "infinite", "interactive", "native"],
|
||||
component: NativeMarquee,
|
||||
variants: [
|
||||
{
|
||||
id: "default",
|
||||
name: "Default",
|
||||
|
|
@ -845,18 +845,18 @@ export const nativeComponents: Component[] = [
|
|||
<SideFadeGradients isVertical={true} />
|
||||
<NativeMarquee gap={8} pauseOnTouch={true} pauseOnHover={true} items={ITEMS} isVertical={true}/>
|
||||
<NativeMarquee gap={8} reverse={true} speed={5} items={ITEMS} isVertical={true}/>
|
||||
`
|
||||
}
|
||||
`,
|
||||
},
|
||||
],
|
||||
codePath:"@uitripled/react-shadcn/src/components/native/native-marquee-shadcnui.tsx",
|
||||
duration: "1000ms",
|
||||
easing: "linear",
|
||||
display: true,
|
||||
availableIn: ["shadcnui"],
|
||||
author: {
|
||||
username: "M4sayev"
|
||||
}
|
||||
codePath: "@uitripled/react-shadcn/src/components/native/native-marquee-shadcnui.tsx",
|
||||
duration: "1000ms",
|
||||
easing: "linear",
|
||||
display: true,
|
||||
availableIn: ["shadcnui", "baseui"],
|
||||
author: {
|
||||
username: "M4sayev",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "native-notification-bell",
|
||||
name: "Native Notification Bell",
|
||||
|
|
|
|||
|
|
@ -18,4 +18,7 @@ export type Component = {
|
|||
easing?: string;
|
||||
baseuiComponent?: React.ComponentType<any>;
|
||||
baseuiCodePath?: string;
|
||||
author?: {
|
||||
username: string;
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -207,6 +207,12 @@ function parseRegistryFile(content: string, fileName: string) {
|
|||
}
|
||||
}
|
||||
|
||||
// Extract optional baseuiCodePath override used when generating
|
||||
// the baseui variant. Without this, generateVariantEntries falls back
|
||||
// to convertToBaseuiPath which can't handle non-shadcn codePaths.
|
||||
const baseuiCodePathMatch = entryText.match(/baseuiCodePath:\s*["'](@uitripled\/[^"']+)["']/);
|
||||
const baseuiCodePath = baseuiCodePathMatch ? baseuiCodePathMatch[1] : null;
|
||||
|
||||
if (componentPath) {
|
||||
entries.push({
|
||||
id,
|
||||
|
|
@ -215,6 +221,7 @@ function parseRegistryFile(content: string, fileName: string) {
|
|||
category,
|
||||
componentName,
|
||||
componentPath,
|
||||
baseuiCodePath,
|
||||
availableIn,
|
||||
});
|
||||
} else {
|
||||
|
|
@ -422,7 +429,7 @@ function getSubcategory(componentPath: string, category: string) {
|
|||
* @param {string|null} uiLibrary - The UI library variant (shadcnui, baseui, or null for pure)
|
||||
*/
|
||||
function createRegistryEntry(componentData: any, uiLibrary: string | null = null) {
|
||||
const { id, name, description, category, componentPath } = componentData;
|
||||
const { id, name, description, category, componentPath, baseuiCodePath } = componentData;
|
||||
const registryCategory = CATEGORY_MAPPING[category] || category;
|
||||
const registryType = CATEGORY_TO_TYPE[category] || "registry:component";
|
||||
|
||||
|
|
@ -443,8 +450,9 @@ function createRegistryEntry(componentData: any, uiLibrary: string | null = null
|
|||
entryName = id.endsWith("-baseui") ? id : `${id}-baseui`;
|
||||
entryTitle = name;
|
||||
entryDescription = description ? `${description} (Base UI)` : description;
|
||||
// Convert to baseui path
|
||||
entryPath = convertToBaseuiPath(componentPath, id);
|
||||
// Prefer an explicit baseuiCodePath override when the entry declares one,
|
||||
// since convertToBaseuiPath only knows how to rewrite react-shadcn paths.
|
||||
entryPath = baseuiCodePath || convertToBaseuiPath(componentPath, id);
|
||||
}
|
||||
|
||||
const entry: any = {
|
||||
|
|
|
|||
Loading…
Reference in a new issue