\r\n\r\nexport function TooltipContent({\r\n children,\r\n className,\r\n sideOffset = 6,\r\n ...props\r\n}: TooltipContentProps) {\r\n return (\r\n \r\n \r\n {children}\r\n \r\n \r\n )\r\n}\r\n"
10 | }
11 | }
--------------------------------------------------------------------------------
/apps/www/public/videos/v2.0/cli.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guhrodrrigues/luxe/35f351c6514246a73cb0fdaa28484226f1644208/apps/www/public/videos/v2.0/cli.mp4
--------------------------------------------------------------------------------
/apps/www/public/videos/v2.0/new-components.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guhrodrrigues/luxe/35f351c6514246a73cb0fdaa28484226f1644208/apps/www/public/videos/v2.0/new-components.mp4
--------------------------------------------------------------------------------
/apps/www/public/videos/v2.0/variant-props.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guhrodrrigues/luxe/35f351c6514246a73cb0fdaa28484226f1644208/apps/www/public/videos/v2.0/variant-props.mp4
--------------------------------------------------------------------------------
/apps/www/src/@types/docs.ts:
--------------------------------------------------------------------------------
1 | export type Docs = {
2 | title: string;
3 | description: string;
4 | slug: string;
5 | date?: string;
6 | content: string;
7 | externalDocs?: string;
8 | externalApi?: string;
9 | author_image?: string;
10 | author_twitter?: string;
11 | banner?: string;
12 | author?: string;
13 | };
14 |
--------------------------------------------------------------------------------
/apps/www/src/@types/images.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.jpg'
2 | declare module '*.svg'
3 |
--------------------------------------------------------------------------------
/apps/www/src/app/(home)/_components/AnimateEnter.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { motion } from 'motion/react'
4 |
5 | import { cn } from '@/utils/cn'
6 |
7 | type AnimateEnterProps = {
8 | className?: string
9 | delay?: number
10 | children: React.ReactNode
11 | duration?: number
12 | }
13 |
14 | export function AnimateEnter({
15 | className,
16 | delay,
17 | children,
18 | duration = 0.4,
19 | }: AnimateEnterProps) {
20 | return (
21 |
28 | {children}
29 |
30 | )
31 | }
32 |
--------------------------------------------------------------------------------
/apps/www/src/app/(home)/_components/FloatToggleTheme.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { useEffect, useState } from 'react'
4 |
5 | import { useTheme } from 'next-themes'
6 |
7 | import { Icons } from '@/app/_components/Icons'
8 | import { TextMorph } from '@/app/_components/TextMorph'
9 | import { AnimatePresence, motion, useScroll } from 'motion/react'
10 |
11 | export function FloatToggleTheme() {
12 | const [visible, setVisible] = useState(false)
13 |
14 | const { resolvedTheme, setTheme } = useTheme()
15 | const { scrollY } = useScroll()
16 |
17 | useEffect(() => {
18 | const unsubscribe = scrollY.on('change', value => {
19 | setVisible(value > 100)
20 | })
21 |
22 | return () => unsubscribe()
23 | }, [scrollY])
24 |
25 | return (
26 |
27 | {visible && (
28 | setTheme(resolvedTheme === 'dark' ? 'light' : 'dark')}
30 | initial={{ width: 0, y: -100, opacity: 0 }}
31 | animate={{ width: 112, y: 0, opacity: 1 }}
32 | exit={{ width: 0, y: -100, opacity: 0 }}
33 | className="fixed left-0 right-0 top-4 z-[999] mx-auto flex h-8 items-center justify-between rounded-2xl border border-border bg-background px-2 text-primary shadow dark:border-transparent dark:bg-[#161616] dark:shadow-inner dark:shadow-neutral-800/80"
34 | >
35 |
36 | {resolvedTheme === 'dark' ? (
37 |
47 |
48 |
49 | ) : (
50 |
60 |
61 |
62 | )}
63 |
64 |
74 |
75 |
76 | {resolvedTheme === 'dark' ? 'Dark' : 'Light'}
77 |
78 | mode
79 |
80 |
81 |
82 | )}
83 |
84 | )
85 | }
86 |
--------------------------------------------------------------------------------
/apps/www/src/app/(home)/_components/GridBackground.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/utils/cn";
2 |
3 | export function GridBackground() {
4 | return (
5 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/apps/www/src/app/(home)/_components/components-section/ComponentsExample.tsx:
--------------------------------------------------------------------------------
1 | import { MultiStepModal } from "@/app/_components/OldMultiStepModal";
2 | import { AnimatedTabs } from "@/app/_components/ui/animated-tabs";
3 | import { TooltipExample } from "@/app/ui/_components/examples/TooltipExample";
4 | import { ComponentView } from "@/app/ui/_components/ComponentView";
5 |
6 | import { AnimateEnter } from "../AnimateEnter";
7 |
8 | import { cn } from "@/utils/cn";
9 | import { AccordionExample } from "@/app/ui/_components/examples/AccordionExample";
10 | import { Button } from "@/app/_components/ui/button";
11 | import { Text } from "@/app/_components/ui/text";
12 | import { DropdownMenuExample } from "@/app/ui/_components/examples/DropdownMenuExample";
13 |
14 | const COMPONENTS_EXAMPLE = [
15 | {
16 | component: (
17 | This is a text generation effect.
18 | ),
19 | isReloadAnimation: true,
20 | },
21 | { component: Button },
22 | { component: },
23 | {
24 | component: ,
25 | className: "md:col-span-1",
26 | componentViewClassName: "min-h-[350px]",
27 | },
28 | {
29 | component: ,
30 | className: "md:col-span-2",
31 | componentViewClassName: "min-h-[350px]",
32 | },
33 | {
34 | component: ,
35 | className: "md:col-span-2",
36 | componentViewClassName: "min-h-[300px]",
37 | },
38 | {
39 | component: Generating code... ,
40 | componentViewClassName: "min-h-[300px]",
41 | },
42 | {
43 | component: Button ,
44 | componentViewClassName: "min-h-[300px]",
45 | },
46 | {
47 | component: (
48 |
49 | ),
50 | className: "md:col-span-2",
51 | componentViewClassName: "min-h-[300px]",
52 | },
53 | ];
54 |
55 | export function ComponentsExample() {
56 | return (
57 |
58 | {COMPONENTS_EXAMPLE.map(
59 | (
60 | { isReloadAnimation, component, className, componentViewClassName },
61 | idx,
62 | ) => (
63 |
64 |
71 | {component}
72 |
73 |
74 | ),
75 | )}
76 |
77 | );
78 | }
79 |
--------------------------------------------------------------------------------
/apps/www/src/app/(home)/_components/components-section/ComponentsSection.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from "next-view-transitions";
2 |
3 | import { ChevronRightIcon } from "lucide-react";
4 |
5 | import { AnimateEnter } from "../AnimateEnter";
6 | import { ComponentsExample } from "./ComponentsExample";
7 |
8 | export function ComponentsSection() {
9 | return (
10 |
11 |
12 |
13 | Elevate your web apps with sophisticated interfaces
14 |
15 |
16 | Choose a component, copy the code, and instantly elevate your
17 | interface. With just a few clicks, and your app shines.
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | );
26 | }
27 |
28 | function Button() {
29 | return (
30 |
34 |
35 | Explore All Components
36 |
37 |
38 |
41 |
42 | );
43 | }
44 |
45 | function ChevronIconGlitch() {
46 | return (
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 | );
59 | }
60 |
--------------------------------------------------------------------------------
/apps/www/src/app/(home)/_components/components-section/index.ts:
--------------------------------------------------------------------------------
1 | export * from './ComponentsSection'
2 |
--------------------------------------------------------------------------------
/apps/www/src/app/(home)/_components/feedbacks-section/FeedbacksCard.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 |
3 | export function FeedbacksCard() {
4 | return (
5 |
6 |
7 | “Luxe is an ultra-aesthetic user interface library. I think it's
8 | promising when built as an abstract layer on top of your existing design
9 | system.”
10 |
11 |
33 |
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/apps/www/src/app/(home)/_components/feedbacks-section/FeedbacksSection.tsx:
--------------------------------------------------------------------------------
1 | import { AnimateEnter } from "../AnimateEnter";
2 | import { FeedbacksCard } from "./FeedbacksCard";
3 |
4 | export function FeedbacksSection() {
5 | return (
6 |
7 |
8 |
9 |
10 |
14 |
15 |
16 |
17 |
18 | );
19 | }
20 |
21 | function Blur() {
22 | return (
23 |
31 | );
32 | }
33 |
34 | function Line() {
35 | return (
36 |
40 | );
41 | }
42 |
--------------------------------------------------------------------------------
/apps/www/src/app/(home)/_components/feedbacks-section/index.ts:
--------------------------------------------------------------------------------
1 | export * from './FeedbacksSection'
2 |
--------------------------------------------------------------------------------
/apps/www/src/app/(home)/_components/hero-section/AnimatedBadge.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { cn } from "@/utils/cn";
4 |
5 | import { motion } from "motion/react";
6 | import { useState } from "react";
7 |
8 | export function AnimatedBadge() {
9 | const [isHover, setIsHover] = useState(false);
10 |
11 | return (
12 | setIsHover(true)}
16 | onMouseLeave={() => setIsHover(false)}
17 | >
18 |
24 |
28 |
47 |
48 |
49 |
50 |
56 | New
57 |
58 |
59 | Luxe 2.0
60 |
61 |
62 |
69 |
78 |
79 | );
80 | }
81 |
--------------------------------------------------------------------------------
/apps/www/src/app/(home)/_components/hero-section/AnimatedNumber.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useEffect, useState } from "react";
4 |
5 | import {
6 | type SpringOptions,
7 | motion,
8 | useSpring,
9 | useTransform,
10 | } from "motion/react";
11 |
12 | import { cn } from "@/utils/cn";
13 |
14 | export function AnimatedNumber({ value }: { value: number }) {
15 | const [newValue, setNewValue] = useState(0);
16 |
17 | useEffect(() => {
18 | setNewValue(value);
19 | }, [value]);
20 |
21 | return (
22 |
29 | );
30 | }
31 |
32 | type AnimationProps = {
33 | value: number;
34 | springOptions?: SpringOptions;
35 | } & React.ComponentProps<"span">;
36 |
37 | function Animation({ value, className, springOptions }: AnimationProps) {
38 | const spring = useSpring(value, springOptions);
39 | const view = useTransform(spring, (current) =>
40 | Math.round(current).toLocaleString(),
41 | );
42 |
43 | useEffect(() => {
44 | spring.set(value);
45 | }, [spring, value]);
46 |
47 | return (
48 | {view}
49 | );
50 | }
51 |
--------------------------------------------------------------------------------
/apps/www/src/app/(home)/_components/hero-section/HeroContent.tsx:
--------------------------------------------------------------------------------
1 | import { ButtonGlitchBrightness } from "@/app/_components/ButtonGlitchBrightness";
2 | import { AnimateEnter } from "../AnimateEnter";
3 | import { GridBackground } from "../GridBackground";
4 | import { Techs } from "../techs-section/Techs";
5 | import { GetStartedButton } from "../slogan-section/GetStartedButton";
6 | import { AnimatedBadge } from "./AnimatedBadge";
7 |
8 | export async function HeroContent() {
9 | return (
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | Illuminate your apps
19 |
20 |
21 |
22 |
23 |
24 | Library of copy and paste components to illuminate your applications
25 | with elegance and sophistication.
26 |
27 |
28 |
33 |
39 |
40 |
41 |
42 |
43 |
44 | Using
45 |
46 |
47 |
48 |
49 |
50 | );
51 | }
52 |
--------------------------------------------------------------------------------
/apps/www/src/app/(home)/_components/hero-section/HeroSection.tsx:
--------------------------------------------------------------------------------
1 | import { HeroContent } from "./HeroContent";
2 | import { Spotlight } from "./Spotlight";
3 |
4 | export function HeroSection() {
5 | return (
6 |
10 | );
11 | }
12 |
--------------------------------------------------------------------------------
/apps/www/src/app/(home)/_components/hero-section/Spotlight.tsx:
--------------------------------------------------------------------------------
1 | export function Spotlight() {
2 | return (
3 |
9 |
10 |
19 |
20 |
21 |
30 |
31 |
37 |
41 |
42 |
43 |
44 | );
45 | }
46 |
--------------------------------------------------------------------------------
/apps/www/src/app/(home)/_components/hero-section/index.ts:
--------------------------------------------------------------------------------
1 | export * from './HeroContent'
2 | export * from './HeroSection'
3 |
--------------------------------------------------------------------------------
/apps/www/src/app/(home)/_components/slogan-section/GalleryButton.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from "next-view-transitions";
2 |
3 | export function GalleryButton() {
4 | return (
5 |
9 |
10 |
11 |
12 | );
13 | }
14 |
15 | function TextGlitch({ text }: { text: string }) {
16 | return (
17 |
18 | {text}
19 |
20 | {text}
21 |
22 |
23 | {text}
24 |
25 |
26 | );
27 | }
28 |
29 | function Brightness() {
30 | return (
31 |
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/apps/www/src/app/(home)/_components/slogan-section/GetStartedButton.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from "next-view-transitions";
2 |
3 | import { ChevronRightIcon } from "lucide-react";
4 |
5 | export function GetStartedButton() {
6 | return (
7 |
11 | Get Started
12 |
13 |
14 | );
15 | }
16 |
17 | function ChevronIconGlitch() {
18 | return (
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/apps/www/src/app/(home)/_components/slogan-section/SloganSection.tsx:
--------------------------------------------------------------------------------
1 | import { AnimateEnter } from "../AnimateEnter";
2 | import { GridBackground } from "../GridBackground";
3 | import { GalleryButton } from "./GalleryButton";
4 | import { GetStartedButton } from "./GetStartedButton";
5 |
6 | export function SloganSection() {
7 | return (
8 |
9 |
10 |
11 |
12 | Build fast. Ship with style.
13 |
14 |
15 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/apps/www/src/app/(home)/_components/slogan-section/TypewriterTitle.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { useEffect } from 'react'
4 |
5 | import { motion, stagger, useAnimate, useInView } from 'motion/react'
6 |
7 | const words = [
8 | {
9 | text: 'Illuminate',
10 | },
11 | {
12 | text: 'your',
13 | },
14 | {
15 | text: 'applications.',
16 | },
17 | ]
18 |
19 | export function TypewriterTitle() {
20 | const [scope, animate] = useAnimate()
21 | const isInView = useInView(scope)
22 |
23 | useEffect(() => {
24 | if (isInView) {
25 | animate(
26 | 'span',
27 | {
28 | display: 'inline-block',
29 | opacity: 1,
30 | },
31 | {
32 | duration: 0.3,
33 | delay: stagger(0.1),
34 | ease: 'easeInOut',
35 | },
36 | )
37 | }
38 | }, [isInView, animate])
39 |
40 | const wordsArray = words.map(word => {
41 | return {
42 | ...word,
43 | text: word.text.split(''),
44 | }
45 | })
46 |
47 | function renderWords() {
48 | return (
49 | <>
50 | {wordsArray.map((word, idx) => {
51 | return (
52 |
53 | {word.text.map((char, index) => (
54 |
55 | {char}
56 |
57 | ))}
58 |
59 |
60 | )
61 | })}
62 | >
63 | )
64 | }
65 |
66 | return (
67 |
68 |
83 |
84 | {renderWords()}
85 |
86 |
87 |
88 |
89 | )
90 | }
91 |
92 | function Cursor() {
93 | return (
94 |
108 | )
109 | }
110 |
--------------------------------------------------------------------------------
/apps/www/src/app/(home)/_components/slogan-section/index.ts:
--------------------------------------------------------------------------------
1 | export * from './SloganSection'
2 |
--------------------------------------------------------------------------------
/apps/www/src/app/(home)/_components/techs-section/Techs.tsx:
--------------------------------------------------------------------------------
1 | import { TECHS } from "@/data/techs";
2 |
3 | export function Techs() {
4 | return (
5 |
6 |
7 | {TECHS.slice(0, 2).map(({ icon, name }) => (
8 |
12 | {icon}
13 |
14 | {name}
15 |
16 |
17 | ))}
18 |
19 |
20 | {TECHS.slice(2, 4).map(({ icon, name }) => (
21 |
25 | {icon}
26 |
27 | {name}
28 |
29 |
30 | ))}
31 |
32 |
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/apps/www/src/app/(home)/_components/techs-section/TechsSection.tsx:
--------------------------------------------------------------------------------
1 | import { AnimateEnter } from '../AnimateEnter'
2 | import { Techs } from './Techs'
3 |
4 | export function TechsSection() {
5 | return (
6 |
7 |
8 |
12 |
20 |
21 |
22 |
23 | Various technologies used to build beautiful interfaces, fluid
24 | animations and easy access.
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | )
34 | }
35 |
--------------------------------------------------------------------------------
/apps/www/src/app/(home)/_components/techs-section/index.ts:
--------------------------------------------------------------------------------
1 | export * from './TechsSection'
2 |
--------------------------------------------------------------------------------
/apps/www/src/app/(home)/page.tsx:
--------------------------------------------------------------------------------
1 | import { BlurBackground } from "./_components/BlurBackground";
2 | import { ComponentsSection } from "./_components/components-section";
3 | import { FeedbacksSection } from "./_components/feedbacks-section/FeedbacksSection";
4 | import { HeroSection } from "./_components/hero-section";
5 | import { SloganSection } from "./_components/slogan-section";
6 | import { Footer } from "../_components/Footer";
7 |
8 | export default function Home() {
9 | return (
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/apps/www/src/app/_components/BlurImage.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import NextImage from "next/image";
4 |
5 | import { useState } from "react";
6 |
7 | import { cn } from "@/utils/cn";
8 |
9 | type BlurImageProps = {
10 | className?: string;
11 | lazy?: boolean;
12 | } & React.ComponentPropsWithoutRef;
13 |
14 | export function BlurImage({
15 | alt,
16 | src,
17 | className,
18 | lazy = true,
19 | ...props
20 | }: BlurImageProps) {
21 | const [isLoading, setIsLoading] = useState(true);
22 |
23 | return (
24 | setIsLoading(false)}
34 | {...props}
35 | />
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/apps/www/src/app/_components/ButtonGlitchBrightness.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/utils/cn";
2 | import { Link } from "next-view-transitions";
3 | import React from "react";
4 |
5 | type ButtonGlitchBrightnessProps = {
6 | href: string;
7 | text: string;
8 | shine?: boolean;
9 | } & React.ComponentProps;
10 |
11 | export function ButtonGlitchBrightness({
12 | href,
13 | text,
14 | className,
15 | shine = true,
16 | }: ButtonGlitchBrightnessProps) {
17 | return (
18 |
25 |
26 | {shine && (
27 |
30 | )}
31 |
32 | );
33 | }
34 |
35 | function TextGlitch({ text }: { text: string }) {
36 | return (
37 |
38 | {text}
39 |
40 | {text}
41 |
42 |
43 | {text}
44 |
45 |
46 | );
47 | }
48 |
--------------------------------------------------------------------------------
/apps/www/src/app/_components/Divider.tsx:
--------------------------------------------------------------------------------
1 | export function Divider() {
2 | return
;
3 | }
4 |
--------------------------------------------------------------------------------
/apps/www/src/app/_components/GradientLine.tsx:
--------------------------------------------------------------------------------
1 | export function GradientLine() {
2 | return (
3 |
4 | )
5 | }
6 |
--------------------------------------------------------------------------------
/apps/www/src/app/_components/RequestComponentButton.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/utils/cn'
2 |
3 | export function RequestComponentButton() {
4 | return (
5 |
15 | Request a component
16 |
17 | )
18 | }
19 |
--------------------------------------------------------------------------------
/apps/www/src/app/_components/TextAnimateEnter.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { motion } from 'motion/react'
4 |
5 | import { cn } from '@/utils/cn'
6 |
7 | type TextAnimateEnterProps = {
8 | text: string
9 | duration?: number
10 | containerClassName?: string
11 | initialDelay?: number
12 | } & React.ComponentProps<'span'>
13 |
14 | export function TextAnimateEnter({
15 | text,
16 | duration = 0.5,
17 | containerClassName,
18 | className,
19 | initialDelay = 0,
20 | }: TextAnimateEnterProps) {
21 | return (
22 |
25 | {text.split(' ').map((char, index) => (
26 |
44 | {char}{' '}
45 |
46 | ))}
47 |
48 | )
49 | }
50 |
--------------------------------------------------------------------------------
/apps/www/src/app/_components/TextMorph.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { AnimatePresence, motion } from 'motion/react'
4 | import { useId, useMemo } from 'react'
5 |
6 | import { cn } from '@/utils/cn'
7 |
8 | type TextMorphProps = {
9 | children: string
10 | } & React.ComponentProps<'p'>
11 |
12 | export function TextMorph({ children, className }: TextMorphProps) {
13 | const id = useId()
14 |
15 | const characters = useMemo(() => {
16 | const charCounts: Record = {}
17 |
18 | return children.split('').map((char, index) => {
19 | const lowerChar = char.toLowerCase()
20 | charCounts[lowerChar] = (charCounts[lowerChar] || 0) + 1
21 |
22 | return {
23 | id: `${id}-${lowerChar}${charCounts[lowerChar]}`,
24 | label: index === 0 ? char.toUpperCase() : lowerChar,
25 | }
26 | })
27 | }, [children, id])
28 |
29 | return (
30 |
31 |
32 | {characters.map(character => (
33 |
47 | {character.label}
48 |
49 | ))}
50 |
51 |
52 | )
53 | }
54 |
--------------------------------------------------------------------------------
/apps/www/src/app/_components/mdx/index.ts:
--------------------------------------------------------------------------------
1 | export { MDX } from "./MDXComponents";
2 |
--------------------------------------------------------------------------------
/apps/www/src/app/_components/ui/accordion.tsx:
--------------------------------------------------------------------------------
1 | import * as RadixAccordion from '@radix-ui/react-accordion'
2 |
3 | import { cn } from '@/utils/cn'
4 |
5 | export const Accordion = RadixAccordion.Root
6 |
7 | type AccordionItemProps = React.ComponentProps
8 |
9 | export function AccordionItem({
10 | children,
11 | value,
12 | className,
13 | ...props
14 | }: AccordionItemProps) {
15 | return (
16 |
24 | {children}
25 |
26 | )
27 | }
28 |
29 | type AccordionTriggerProps = React.ComponentProps
30 |
31 | export function AccordionTrigger({
32 | children,
33 | className,
34 | ...props
35 | }: AccordionTriggerProps) {
36 | return (
37 |
38 | svg]:rotate-45',
42 | className,
43 | )}
44 | {...props}
45 | >
46 | {children}
47 |
58 | Trigger
59 |
60 |
61 |
62 |
63 |
64 | )
65 | }
66 |
67 | type AccordionContentProps = React.ComponentProps
68 |
69 | export function AccordionContent({
70 | children,
71 | className,
72 | ...props
73 | }: AccordionContentProps) {
74 | return (
75 |
82 | {children}
83 |
84 | )
85 | }
86 |
--------------------------------------------------------------------------------
/apps/www/src/app/_components/ui/animated-tabs.tsx:
--------------------------------------------------------------------------------
1 | "use client"; // @NOTE: Add in case you are using Next.js
2 |
3 | import { useEffect, useRef, useState } from "react";
4 |
5 | import { cn } from "@/utils/cn";
6 |
7 | type AnimatedTabsProps = {
8 | tabs: Array;
9 | };
10 |
11 | export function AnimatedTabs({ tabs }: AnimatedTabsProps) {
12 | const [activeTab, setActiveTab] = useState(tabs[0]);
13 |
14 | const containerRef = useRef(null);
15 | const activeTabRef = useRef(null);
16 |
17 | useEffect(() => {
18 | const container = containerRef.current;
19 |
20 | if (container && activeTab) {
21 | const activeTabElement = activeTabRef.current;
22 |
23 | if (activeTabElement) {
24 | const { offsetLeft, offsetWidth } = activeTabElement;
25 |
26 | const clipLeft = offsetLeft;
27 | const clipRight = offsetLeft + offsetWidth;
28 |
29 | container.style.clipPath = `inset(0 ${Number(100 - (clipRight / container.offsetWidth) * 100).toFixed()}% 0 ${Number((clipLeft / container.offsetWidth) * 100).toFixed()}% round 17px)`;
30 | }
31 | }
32 | }, [activeTab]);
33 |
34 | return (
35 |
36 |
40 |
41 | {tabs.map((tab, index) => (
42 | setActiveTab(tab)}
46 | className={cn(
47 | "flex h-8 items-center rounded-full p-3 font-medium text-primary-invert text-sm/5.5 max-sm:last:hidden",
48 | )}
49 | tabIndex={-1}
50 | >
51 | {tab}
52 |
53 | ))}
54 |
55 |
56 |
57 | {tabs.map((tab, index) => {
58 | const isActive = activeTab === tab;
59 |
60 | return (
61 | setActiveTab(tab)}
66 | className="flex h-8 items-center rounded-full p-3 font-medium text-primary-muted text-sm/5.5 max-sm:last:hidden"
67 | >
68 | {tab}
69 |
70 | );
71 | })}
72 |
73 |
74 | );
75 | }
76 |
--------------------------------------------------------------------------------
/apps/www/src/app/_components/ui/checkbox.tsx:
--------------------------------------------------------------------------------
1 | "use client"; // @NOTE: Add in case you are using Next.js
2 |
3 | import * as RadixCheckbox from "@radix-ui/react-checkbox";
4 |
5 | import { AnimatePresence, motion } from "motion/react";
6 |
7 | type CheckboxProps = React.CustomComponentPropsWithRef<
8 | typeof RadixCheckbox.Root
9 | >;
10 |
11 | export function Checkbox(props: CheckboxProps) {
12 | const { checked } = props;
13 |
14 | return (
15 |
19 |
20 |
24 |
38 |
39 |
40 |
41 |
42 |
43 | );
44 | }
45 |
46 | type CheckIconProps = {
47 | checkedState: CheckboxProps["checked"];
48 | };
49 |
50 | function CheckIcon({ checkedState }: CheckIconProps) {
51 | const CHECK_PATH = "M5 13 L10 18 L20 6";
52 | const INDETERMINATE_PATH = "M6 12 H18";
53 |
54 | return (
55 |
62 | Check
63 |
64 |
77 |
78 | );
79 | }
80 |
--------------------------------------------------------------------------------
/apps/www/src/app/_components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | import * as RadixDialog from '@radix-ui/react-dialog'
2 |
3 | import { cn } from '@/utils/cn'
4 |
5 | export const Dialog = RadixDialog.Root
6 |
7 | export const DialogTrigger = RadixDialog.Trigger
8 |
9 | export const DialogClose = RadixDialog.Close
10 |
11 | function DialogOverlay() {
12 | return (
13 |
14 |
21 |
22 | )
23 | }
24 |
25 | type DialogContentProps = React.ComponentProps
26 |
27 | export function DialogContent({
28 | children,
29 | className,
30 | ...props
31 | }: DialogContentProps) {
32 | return (
33 |
34 |
35 |
45 | {children}
46 |
47 |
48 | )
49 | }
50 |
51 | type DialogTitleProps = React.ComponentProps
52 |
53 | export function DialogTitle({
54 | children,
55 | className,
56 | ...props
57 | }: DialogTitleProps) {
58 | return (
59 |
63 | {children}
64 |
65 | )
66 | }
67 |
68 | type DialogDescriptionProps = React.ComponentProps<
69 | typeof RadixDialog.Description
70 | >
71 |
72 | export function DialogDescription({
73 | children,
74 | className,
75 | ...props
76 | }: DialogDescriptionProps) {
77 | return (
78 |
85 | {children}
86 |
87 | )
88 | }
89 |
90 | type DialogFooterProps = React.ComponentProps<'div'>
91 |
92 | export function DialogFooter({
93 | children,
94 | className,
95 | ...props
96 | }: DialogFooterProps) {
97 | return (
98 |
105 | {children}
106 |
107 | )
108 | }
109 |
--------------------------------------------------------------------------------
/apps/www/src/app/_components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | "use client"; // @NOTE: Add in case you are using Next.js
2 |
3 | import { useState } from "react";
4 |
5 | import { AnimatePresence, motion, type Variants } from "motion/react";
6 |
7 | import { cn } from "@/utils/cn";
8 |
9 | export type InputProps = React.ComponentPropsWithRef<"input">;
10 | type FieldState = "idle" | "filled";
11 |
12 | export function Input({
13 | placeholder,
14 | onChange,
15 | className,
16 | ...props
17 | }: InputProps) {
18 | const [fieldState, setFieldState] = useState("idle");
19 |
20 | const animatedPlaceholderVariants: Variants = {
21 | show: {
22 | x: 0,
23 | opacity: 1,
24 | filter: "blur(var(--blur-none))",
25 | },
26 | hidden: {
27 | x: 28,
28 | opacity: 0,
29 | filter: "blur(var(--blur-xs))",
30 | },
31 | };
32 |
33 | return (
34 |
42 |
{
50 | setFieldState(event.target.value.length > 0 ? "filled" : "idle");
51 | onChange?.(event);
52 | }}
53 | />
54 |
55 |
56 | {fieldState !== "filled" && (
57 |
72 | {placeholder}
73 |
74 | )}
75 |
76 |
77 | );
78 | }
79 |
--------------------------------------------------------------------------------
/apps/www/src/app/_components/ui/spinner.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/utils/cn'
2 |
3 | type SpinnerProps = {
4 | size?: string
5 | } & React.ComponentProps<'div'>
6 |
7 | export function Spinner({
8 | size = 'size-6',
9 | className,
10 | ...props
11 | }: SpinnerProps) {
12 | const bars = Array(12).fill(0)
13 |
14 | return (
15 |
16 |
17 | {bars.map((_, i) => (
18 |
32 | ))}
33 |
34 |
35 | )
36 | }
37 |
--------------------------------------------------------------------------------
/apps/www/src/app/_components/ui/switch.tsx:
--------------------------------------------------------------------------------
1 | import * as RadixSwitch from '@radix-ui/react-switch'
2 |
3 | import { cn } from '@/utils/cn'
4 |
5 | export type SwitchProps = React.ComponentProps
6 |
7 | export function Switch({ className, ...props }: SwitchProps) {
8 | return (
9 |
17 |
24 |
25 | )
26 | }
27 |
--------------------------------------------------------------------------------
/apps/www/src/app/_components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | import * as RadixTooltip from '@radix-ui/react-tooltip'
2 |
3 | import { cn } from '@/utils/cn'
4 |
5 | export const Tooltip = RadixTooltip.Root
6 |
7 | export const TooltipTrigger = RadixTooltip.Trigger
8 |
9 | type TooltipProviderProps = React.ComponentProps
10 |
11 | export function TooltipProvider({ children, ...props }: TooltipProviderProps) {
12 | return (
13 |
14 | {children}
15 |
16 | )
17 | }
18 |
19 | type TooltipContentProps = React.ComponentProps
20 |
21 | export function TooltipContent({
22 | children,
23 | className,
24 | sideOffset = 6,
25 | ...props
26 | }: TooltipContentProps) {
27 | return (
28 |
29 |
40 | {children}
41 |
42 |
43 | )
44 | }
45 |
--------------------------------------------------------------------------------
/apps/www/src/app/_docs/get-started/cli.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: "CLI"
3 | description: "Add components instantly in your app with the Luxe CLI."
4 | slug: "cli"
5 | ---
6 |
7 |
8 |
9 |
10 |
11 | ## init
12 |
13 |
14 |
15 |
16 |
17 | Use the init command to create initial setup in your project.
18 |
19 | This command installs required dependencies, configures CSS variables, and adds the cn util.
20 |
21 |
28 |
29 |
30 |
31 |
32 |
33 | ### Options
34 |
35 |
36 |
37 | ```md
38 | Usage: @luxeui/ui init
39 |
40 | initialize your project and install dependencies.
41 | ```
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | ## add
54 |
55 |
56 |
57 |
58 |
59 | Use the add command to add components and your dependencies in your app.
60 |
61 |
67 |
68 |
69 |
70 |
71 |
72 | ### Options
73 |
74 |
75 |
76 | ```md
77 | Usage: @luxeui/ui add [components...]
78 |
79 | select and add the components you need.
80 |
81 | Arguments:
82 | components enter the component name(s)
83 | ```
84 |
85 |
86 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/apps/www/src/app/_docs/get-started/next.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Next"
3 | description: "Install and configure Next."
4 | slug: "next"
5 | ---
6 |
7 |
8 |
9 |
10 |
11 |
12 | 1
13 |
14 |
15 |
16 |
17 |
18 | Create a new project
19 |
20 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | 2
34 |
35 |
36 |
37 |
38 |
After the above command, it is mandatory to select these values in the prompts
39 |
40 |
41 |
42 | ```shell
43 | Would you like to use TypeScript? Yes
44 | Would you like to use Tailwind CSS? Yes
45 | Would you like to customize the default import alias (@/*)? Yes
46 | What import alias would you like configured? @/*
47 | ```
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 | 3
60 |
61 |
62 |
63 |
64 |
65 |
Start the app
66 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/apps/www/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 | import { fontSans, fontMono } from "@/utils/fonts";
3 |
4 | import { ViewTransitions } from "next-view-transitions";
5 |
6 | import "@/styles/globals.css";
7 |
8 | import { cn } from "@/utils/cn";
9 |
10 | import { ThemeProvider } from "./theme-provider";
11 | import { Header } from "./ui/_components/Header";
12 |
13 | export const metadata: Metadata = {
14 | authors: [{ name: "Gustavo Rodrigues", url: "https://guhrodrigues.com" }],
15 | category: "developer",
16 | creator: "Gustavo Rodrigues",
17 | title: {
18 | default: "Luxe: Illuminate your apps.",
19 | template: "Luxe: %s",
20 | },
21 | description:
22 | "Library of copy and paste components to illuminate your apps with elegance and sophistication.",
23 | icons: ["/favicon.svg"],
24 | keywords: [
25 | "Gustavo Rodrigues",
26 | "guhrodrigues.com",
27 | "luxeui.com",
28 | "luxe.guhrodrigues.com",
29 | "Motion",
30 | "UI Design",
31 | "Luxe",
32 | "UI Library",
33 | "Design Engineer",
34 | "Frontend Developer",
35 | "Component library",
36 | "Frontend",
37 | "Copy and Paste",
38 | "CLI",
39 | "Command Line Interface",
40 | "Dark Mode",
41 | "Light Mode",
42 | "UX Design",
43 | "Developer",
44 | "Software",
45 | "Copy and paste components ready to use. Practical. Customizable.",
46 | "Design",
47 | "Vercel",
48 | "Next.js",
49 | "React",
50 | "TypeScript",
51 | "TailwindCSS",
52 | "Framer Motion",
53 | "Server Components",
54 | "Client Components",
55 | ],
56 | openGraph: {
57 | images: [
58 | {
59 | width: 1920,
60 | height: 1080,
61 | url: "https://luxeui.com/open-graphs/og-website.png",
62 | alt: "Luxe website cover",
63 | },
64 | ],
65 | locale: "en",
66 | siteName: "Gustavo Rodrigues",
67 | title: "Luxe",
68 | description:
69 | "Copy and paste components ready to use. Practical. Customizable.",
70 | type: "website",
71 | url: "https://luxeui.com",
72 | },
73 | publisher: "Gustavo Rodrigues",
74 | twitter: {
75 | images: [
76 | {
77 | width: 1920,
78 | height: 1080,
79 | url: "https://luxeui.com/open-graphs/og-website.png",
80 | alt: "Luxe website cover",
81 | },
82 | ],
83 | card: "summary_large_image",
84 | title: "Luxe: Illuminate your apps.",
85 | description:
86 | "Copy and paste components ready to use. Practical. Customizable.",
87 | site: "@guhrodrrigues",
88 | creator: "Gustavo Rodrigues",
89 | },
90 | };
91 |
92 | export default function RootLayout({
93 | children,
94 | }: Readonly<{
95 | children: React.ReactNode;
96 | }>) {
97 | return (
98 |
99 |
106 |
107 |
113 |
114 | {children}
115 |
116 |
117 |
118 |
119 | );
120 | }
121 |
--------------------------------------------------------------------------------
/apps/www/src/app/not-found.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 | import Link from "next/link";
3 |
4 | import { MoveLeftIcon } from "lucide-react";
5 |
6 | export const metadata: Metadata = {
7 | title: "Not Found",
8 | description: "The route you're trying to access doesn't exist.",
9 | };
10 |
11 | export default function NotFoundPage() {
12 | return (
13 |
14 |
15 |
29 |
30 |
Not Found
31 |
32 | The route you're trying to access doesn't exist.
33 |
34 |
38 |
39 |
Back to home
40 |
41 |
42 |
43 |
44 | );
45 | }
46 |
--------------------------------------------------------------------------------
/apps/www/src/app/theme-provider.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import {
4 | type ThemeProviderProps as NextThemeProviderProps,
5 | ThemeProvider as NextThemesProvider,
6 | } from 'next-themes'
7 |
8 | type ThemeProviderProps = Omit & {
9 | children: React.ReactNode
10 | }
11 |
12 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
13 | return {children}
14 | }
15 |
--------------------------------------------------------------------------------
/apps/www/src/app/ui/_components/Breadcrumbs.tsx:
--------------------------------------------------------------------------------
1 | import { ChevronRight } from 'lucide-react'
2 | import Link from 'next/link'
3 |
4 | type Breadcrumbs = {
5 | category?: string
6 | groupName: string
7 | backLink?: string
8 | currentPage: string
9 | }
10 |
11 | export function Breadcrumbs({
12 | category,
13 | groupName,
14 | backLink,
15 | currentPage,
16 | }: Breadcrumbs) {
17 | return (
18 |
19 | {category && (
20 | <>
21 |
22 | {category}
23 |
24 |
25 | >
26 | )}
27 | {backLink ? (
28 |
32 | {groupName}
33 |
34 | ) : (
35 |
36 | {groupName}
37 |
38 | )}
39 |
40 | {currentPage}
41 |
42 | )
43 | }
44 |
--------------------------------------------------------------------------------
/apps/www/src/app/ui/_components/CodeBlock.tsx:
--------------------------------------------------------------------------------
1 | import { CodeIcon, TerminalIcon } from "lucide-react";
2 |
3 | import { cn } from "@/utils/cn";
4 |
5 | import { CopyCode } from "./CopyCode";
6 |
7 | import { getFileContent } from "@/utils/get-file-content";
8 |
9 | type CodeBlockProps = {
10 | fileName?: string;
11 | copyCode?: boolean;
12 | contentClassName?: string;
13 | customFilePath?: string;
14 | simpleCode?: string;
15 | } & React.ComponentProps<"div">;
16 |
17 | export function CodeBlock({
18 | fileName,
19 | className,
20 | children,
21 | contentClassName,
22 | simpleCode,
23 | customFilePath,
24 | copyCode = true,
25 | }: CodeBlockProps) {
26 | return (
27 |
33 | {fileName && copyCode && (
34 |
35 |
36 | {fileName === "Terminal" ? (
37 |
41 | ) : (
42 |
46 | )}
47 |
48 | {fileName}
49 |
50 |
51 |
60 |
61 | )}
62 |
65 |
66 | );
67 | }
68 |
--------------------------------------------------------------------------------
/apps/www/src/app/ui/_components/CommandBlock.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useMemo, useState } from "react";
4 |
5 | import { cn } from "@/utils/cn";
6 |
7 | import { CopyCode } from "./CopyCode";
8 | import { Tabs, TabsList, TabsTrigger, TabsContent } from "./Tabs";
9 |
10 | type CommandBlockProps = {
11 | npmCommand: string;
12 | yarnCommand: string;
13 | pnpmCommand: string;
14 | bunCommand: string;
15 | } & React.ComponentProps<"div">;
16 |
17 | export function CommandBlock({
18 | npmCommand,
19 | yarnCommand,
20 | pnpmCommand,
21 | bunCommand,
22 | className,
23 | }: CommandBlockProps) {
24 | const tabs = useMemo(() => {
25 | return {
26 | npm: npmCommand,
27 | pnpm: pnpmCommand,
28 | yarn: yarnCommand,
29 | bun: bunCommand,
30 | };
31 | }, [npmCommand, pnpmCommand, yarnCommand, bunCommand]);
32 |
33 | const [selectedPackage, setSelectedPackage] =
34 | useState("npm");
35 |
36 | return (
37 |
43 |
47 | setSelectedPackage(value as keyof typeof tabs)
48 | }
49 | >
50 |
51 |
52 | {Object.entries(tabs).map(([key, _]) => (
53 |
58 | {key}
59 |
60 | ))}
61 |
62 |
63 |
64 |
65 | {Object.entries(tabs).map(([key, value]) => (
66 |
67 |
68 |
72 | {value}
73 |
74 |
75 |
76 | ))}
77 |
78 |
79 |
80 | );
81 | }
82 |
--------------------------------------------------------------------------------
/apps/www/src/app/ui/_components/ComponentView.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useState } from "react";
4 |
5 | import { motion } from "motion/react";
6 |
7 | import { RotateCwIcon } from "lucide-react";
8 |
9 | import { cn } from "@/utils/cn";
10 |
11 | type ComponentViewProps = {
12 | isReloadAnimation?: boolean;
13 | } & React.ComponentProps<"div">;
14 |
15 | export function ComponentView({
16 | isReloadAnimation,
17 | className,
18 | children,
19 | }: ComponentViewProps) {
20 | const [reloadKey, setReloadKey] = useState(0);
21 |
22 | function handleReload() {
23 | setReloadKey((prevKey) => prevKey + 1);
24 | }
25 |
26 | return (
27 |
33 | {isReloadAnimation ?
{children}
: children}
34 | {isReloadAnimation && (
35 |
36 |
42 |
43 |
44 |
45 | )}
46 |
47 | );
48 | }
49 |
--------------------------------------------------------------------------------
/apps/www/src/app/ui/_components/CopyCode.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useState } from "react";
4 |
5 | import { AnimatePresence, motion } from "motion/react";
6 |
7 | import { CheckIcon } from "lucide-react";
8 |
9 | import { TextMorph } from "@/app/_components/TextMorph";
10 |
11 | type CopyCode = {
12 | code: string;
13 | example?: boolean;
14 | mode?: "text" | "icon";
15 | };
16 |
17 | export function CopyCode({ code, example = false, mode = "icon" }: CopyCode) {
18 | const [copied, setCopied] = useState(false);
19 |
20 | function handleCopy() {
21 | navigator.clipboard.writeText(code);
22 |
23 | setCopied(true);
24 |
25 | setTimeout(() => setCopied(false), 1500);
26 | }
27 |
28 | return mode === "icon" ? (
29 |
33 |
34 | {copied ? (
35 |
46 |
47 |
48 | ) : (
49 |
60 |
61 |
62 | )}
63 |
64 |
65 | ) : (
66 |
70 | {copied ? "Copied" : "Copy"} Code
71 |
72 | );
73 | }
74 |
75 | function Icon() {
76 | return (
77 |
88 |
89 |
90 |
91 |
92 | );
93 | }
94 |
--------------------------------------------------------------------------------
/apps/www/src/app/ui/_components/ToggleTheme.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useTheme } from "next-themes";
4 |
5 | import { Icons } from "@/app/_components/Icons";
6 |
7 | import { cn } from "@/utils/cn";
8 |
9 | export function ToggleTheme() {
10 | const { setTheme, resolvedTheme: theme } = useTheme();
11 |
12 | return (
13 | setTheme(theme === "dark" ? "light" : "dark")}
15 | className={cn(
16 | "relative flex size-8 rounded-full items-center justify-center outline-none",
17 | "focus-visible:ring-1 focus-visible:ring-neutral-300/80 dark:focus-visible:ring-neutral-800 bg-background",
18 | "border border-border/60 dark:border-border/50 dark:hover:bg-main-foreground/20 dark:hover:border-white/10",
19 | "focus-visible:ring-1 focus-visible:ring-neutral-300/80 dark:focus-visible:ring-neutral-800 ease-linear duration-150",
20 | "dark:shadow-[0px_32px_64px_-16px_transparent,0px_16px_32px_-8px_transparent,0px_8px_16px_-4px_transparent,0px_4px_8px_-2px_transparent,0px_-8px_16px_-1px_transparent,0px_2px_4px_-1px_transparent,0px_0px_0px_1px_transparent,inset_0px_0px_0px_1px_transparent,inset_0px_1px_0px_rgb(255,255,255,0.2)]",
21 | )}
22 | >
23 |
24 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/apps/www/src/app/ui/_components/cmdk/index.ts:
--------------------------------------------------------------------------------
1 | export { CommandMenu } from "./CommandMenu";
2 |
--------------------------------------------------------------------------------
/apps/www/src/app/ui/_components/examples/AccordionExample.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Accordion,
3 | AccordionItem,
4 | AccordionTrigger,
5 | AccordionContent,
6 | } from "@/app/_components/ui/accordion";
7 |
8 | export function AccordionExample() {
9 | return (
10 |
15 |
16 | Is it accessible?
17 |
18 | Yes. It adheres to the WAI-ARIA design pattern.
19 |
20 |
21 |
22 | Is it unstyled?
23 |
24 | Yes. It's unstyled by default, giving you freedom over the look and
25 | feel.
26 |
27 |
28 |
29 | Can it be animated?
30 |
31 | Yes! You can animate the Accordion with CSS or JavaScript.
32 |
33 |
34 |
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/apps/www/src/app/ui/_components/examples/CheckboxExample.tsx:
--------------------------------------------------------------------------------
1 | import { Checkbox } from "@/app/_components/ui/checkbox";
2 |
3 | export function CheckboxExample() {
4 | return (
5 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/apps/www/src/app/ui/_components/examples/DialogExample.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Dialog,
3 | DialogTrigger,
4 | DialogContent,
5 | DialogTitle,
6 | DialogDescription,
7 | DialogFooter,
8 | DialogClose,
9 | } from "@/app/_components/ui/dialog";
10 |
11 | import { Button } from "@/app/_components/ui/button";
12 | import { Input } from "@/app/_components/ui/input";
13 |
14 | export function DialogExample() {
15 | return (
16 |
17 |
18 | Open
19 |
20 |
21 |
22 | Change Username
23 |
24 |
25 | Make changes to your username here.
26 |
27 |
28 |
29 |
30 |
31 |
32 | Cancel
33 |
34 |
35 | Save Changes
36 |
37 |
38 |
39 |
40 | );
41 | }
42 |
--------------------------------------------------------------------------------
/apps/www/src/app/ui/_components/examples/DropdownMenuExample.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | DrodpownMenuTrigger,
3 | DropdownMenu,
4 | DropdownMenuContent,
5 | DropdownMenuItem,
6 | } from "@/app/_components/ui/dropdown-menu";
7 |
8 | import {
9 | LayoutGridIcon,
10 | TrashIcon,
11 | Building2,
12 | UserCircleIcon,
13 | ChevronRightIcon,
14 | BellIcon,
15 | } from "lucide-react";
16 |
17 | import { cn } from "@/utils/cn";
18 |
19 | const ITEMS = [
20 | { icon: , name: "Profile" },
21 | { icon: , name: "Your applications" },
22 | { icon: , name: "Teams" },
23 | { icon: , name: "Notifications" },
24 | {
25 | icon: ,
26 | name: "Remove account",
27 | customStyle:
28 | "!text-red-500 duration-150 hover:!bg-red-600/10 focus-visible:text-red-500 focus-visible:!bg-red-500/10 focus-visible:!border-red-500/10",
29 | },
30 | ];
31 |
32 | export function DropdownMenuExample() {
33 | return (
34 |
35 |
36 |
37 | Settings
38 |
39 |
40 |
41 | {ITEMS.map(({ icon, name, customStyle }, index) => (
42 |
43 | {icon}
44 |
45 | {name}
46 |
50 |
51 |
52 | ))}
53 |
54 |
55 | );
56 | }
57 |
--------------------------------------------------------------------------------
/apps/www/src/app/ui/_components/examples/MultiStepModalExample.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | MultiStepModal,
3 | MultiStepModalContent,
4 | MultiStepModalTrigger,
5 | } from "@/app/_components/ui/multi-step-modal";
6 |
7 | import { Button } from "@/app/_components/ui/button";
8 |
9 | export function MultiStepModalExample() {
10 | const steps = [
11 | {
12 | title: "Luxe",
13 | description:
14 | "A library of components ready for you to copy and paste, designed to illuminate your apps with elegance, sophistication and a unique touch of style.",
15 | },
16 | {
17 | title: "How to use?",
18 | description:
19 | "Simply click on a component, copy the code and paste it into your project.",
20 | },
21 | {
22 | title: "Results",
23 | description:
24 | "Luxe will add extra shine to your application, with smooth components.",
25 | },
26 | {
27 | title: "Copy now",
28 | description:
29 | "Elevate your project with sophisticated, ready to use components. Illuminate up your app quickly, easily and effortlessly!",
30 | },
31 | ];
32 |
33 | return (
34 |
35 |
36 | Open
37 |
38 |
39 |
40 | );
41 | }
42 |
--------------------------------------------------------------------------------
/apps/www/src/app/ui/_components/examples/SwitchExample.tsx:
--------------------------------------------------------------------------------
1 | import { Switch } from "@/app/_components/ui/switch";
2 |
3 | export function SwitchExample() {
4 | return (
5 |
6 |
7 |
11 | Airplane Mode
12 |
13 |
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/apps/www/src/app/ui/_components/examples/TooltipExample.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | TooltipProvider,
3 | Tooltip,
4 | TooltipContent,
5 | TooltipTrigger,
6 | } from "@/app/_components/ui/tooltip";
7 |
8 | import { Button } from "@/app/_components/ui/button";
9 |
10 | export function TooltipExample() {
11 | return (
12 |
13 |
14 |
15 | Hover
16 |
17 |
18 | Add to library
19 |
20 |
21 |
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/apps/www/src/app/ui/_components/sidebar/Sidebar.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/utils/cn";
2 | import { SidebarButton } from "./SidebarButton";
3 |
4 | import { GET_STARTED } from "@/data/get-started";
5 | import { getComponents } from "@/utils/get-components";
6 |
7 | export function Sidebar() {
8 | return (
9 |
10 |
11 |
12 |
13 | Get Started
14 |
15 |
16 | {GET_STARTED.map((component) => (
17 |
23 | ))}
24 |
25 |
26 |
27 |
28 |
29 | Components
30 |
31 |
37 | New
38 |
39 |
40 |
41 | {getComponents.map((component) => (
42 |
47 | ))}
48 |
49 |
50 |
51 |
52 | );
53 | }
54 |
--------------------------------------------------------------------------------
/apps/www/src/app/ui/_components/sidebar/SidebarButton.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Link from "next/link";
4 | import { usePathname } from "next/navigation";
5 |
6 | import { cn } from "@/utils/cn";
7 |
8 | type SidebarButton = {
9 | slug: string;
10 | name: string;
11 | isNew?: boolean;
12 | isBeta?: boolean;
13 | isUpdated?: boolean;
14 | onClick?: () => void;
15 | };
16 |
17 | export function SidebarButton({
18 | name,
19 | slug,
20 | isNew = false,
21 | isBeta = false,
22 | isUpdated = false,
23 | onClick,
24 | ...props
25 | }: SidebarButton) {
26 | const pathname = usePathname();
27 |
28 | const isActive = pathname === slug;
29 |
30 | return (
31 |
43 | {isNew && (
44 |
45 |
{name}
46 |
52 | New
53 |
54 |
55 | )}
56 | {isBeta && (
57 |
58 |
{name}
59 |
65 | Beta
66 |
67 |
68 | )}
69 | {isUpdated && (
70 |
71 | {name}
72 |
73 | Updated
74 |
75 |
76 | )}
77 | {!isUpdated && !isNew && !isBeta && (
78 | {name}
79 | )}
80 |
81 | );
82 | }
83 |
--------------------------------------------------------------------------------
/apps/www/src/app/ui/cli/page.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 |
3 | import { Breadcrumbs } from "../_components/Breadcrumbs";
4 | import { Pagination } from "../_components/Pagination";
5 |
6 | import { getDocs } from "@/lib/mdx";
7 | import { notFound } from "next/navigation";
8 | import { MDX } from "@/app/_components/mdx";
9 |
10 | export const metadata: Metadata = {
11 | title: "CLI",
12 | description:
13 | "This code is widely used in Luxe, it is responsible for merging classes when they have conditionals.",
14 | openGraph: {
15 | images: [
16 | {
17 | width: 1920,
18 | height: 1080,
19 | url: "https://luxeui.com/open-graphs/og-cli.png",
20 | alt: "Luxe's website cover",
21 | },
22 | ],
23 | locale: "en",
24 | siteName: "Gustavo Rodrigues",
25 | title: "Luxe — Add Utilities",
26 | description:
27 | "This code is widely used in Luxe, it is responsible for merging classes when they have conditionals.",
28 | type: "website",
29 | url: "https://luxeui.com/ui/cli",
30 | },
31 | twitter: {
32 | images: [
33 | {
34 | width: 1920,
35 | height: 1080,
36 | url: "https://luxeui.com/open-graphs/og-cli.png",
37 | alt: "Luxe's website cover",
38 | },
39 | ],
40 | card: "summary_large_image",
41 | title: "Luxe — Add Utilities",
42 | description:
43 | "This code is widely used in Luxe, it is responsible for merging classes when they have conditionals.",
44 | site: "@guhrodrrigues",
45 | creator: "Gustavo Rodrigues",
46 | },
47 | };
48 |
49 | const Docs = getDocs("get-started");
50 |
51 | export default async function CLIPage() {
52 | const docs = Docs.find((docs) => docs.slug === "cli");
53 |
54 | if (!docs) return notFound();
55 |
56 | const { content, title, description } = docs;
57 |
58 | return (
59 |
60 |
61 |
62 |
63 |
64 | {title}
65 |
66 |
67 | {description}
68 |
69 |
70 |
71 |
72 |
78 |
79 | );
80 | }
81 |
--------------------------------------------------------------------------------
/apps/www/src/app/ui/installation/[slug]/page.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 | import { notFound } from "next/navigation";
3 |
4 | import { Breadcrumbs } from "../../_components/Breadcrumbs";
5 | import { Pagination } from "../../_components/Pagination";
6 | import { getDocs } from "@/lib/mdx";
7 | import { MDX } from "@/app/_components/mdx";
8 |
9 | const Docs = getDocs("get-started").filter((docs) => docs.slug !== "cli");
10 |
11 | export async function generateMetadata({
12 | params,
13 | }: {
14 | params: Promise<{ slug: string }>;
15 | }): Promise {
16 | const { slug } = await params;
17 |
18 | const docs = Docs.find((docs) => docs.slug === slug);
19 |
20 | if (!docs) {
21 | return;
22 | }
23 |
24 | const { title, slug: slugDocs } = docs;
25 |
26 | return {
27 | title: `${title} Installation`,
28 | description: `How to install dependencies and structure your application with ${title}`,
29 | openGraph: {
30 | title: `Luxe — ${title} Installation`,
31 | description: `How to install dependencies and structure your application with ${title}`,
32 | type: "website",
33 | url: `https://luxeui.com/ui/installation/${slugDocs}`,
34 | images: [
35 | {
36 | width: 1920,
37 | height: 1080,
38 | url: "https://luxeui.com/open-graphs/og-installation.png",
39 | alt: "Luxe's website cover",
40 | },
41 | ],
42 | },
43 | twitter: {
44 | title: `Luxe — ${title} Installation`,
45 | description: `How to install dependencies and structure your application with ${title}`,
46 | card: "summary_large_image",
47 | images: [
48 | {
49 | width: 1920,
50 | height: 1080,
51 | url: "https://luxeui.com/open-graphs/og-installation.png",
52 | alt: "Luxe's website cover",
53 | },
54 | ],
55 | },
56 | };
57 | }
58 |
59 | export default async function InstallationSlugPage({
60 | params,
61 | }: {
62 | params: Promise<{ slug: string }>;
63 | }) {
64 | const { slug } = await params;
65 |
66 | const docs = Docs.find((docs) => docs.slug === slug);
67 |
68 | if (!docs) notFound();
69 |
70 | const { title, description, content } = docs;
71 |
72 | const currentPage = Docs.indexOf(docs);
73 | const nextPage = Docs[currentPage + 1];
74 | const previousPage = Docs[currentPage - 1];
75 |
76 | return (
77 |
78 |
79 |
85 |
86 |
87 | {title}
88 |
89 |
90 | {description}
91 |
92 |
93 |
94 |
95 |
105 |
106 | );
107 | }
108 |
--------------------------------------------------------------------------------
/apps/www/src/app/ui/installation/_data/installation.tsx:
--------------------------------------------------------------------------------
1 | import { Icons } from '@/app/_components/Icons'
2 |
3 | export const INSTALLATION = [
4 | {
5 | slug: 'next',
6 | icon: ,
7 | name: 'Next.js',
8 | },
9 | {
10 | slug: 'vite',
11 | icon: ,
12 | name: 'Vite',
13 | },
14 | ]
15 |
--------------------------------------------------------------------------------
/apps/www/src/app/ui/installation/page.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 |
3 | import { Breadcrumbs } from "../_components/Breadcrumbs";
4 | import { Pagination } from "../_components/Pagination";
5 | import { Card } from "./_components/Card";
6 | import { INSTALLATION } from "./_data/installation";
7 |
8 | export const metadata: Metadata = {
9 | title: "Installation",
10 | description: "How to install dependencies and structure your application",
11 | openGraph: {
12 | images: [
13 | {
14 | width: 1920,
15 | height: 1080,
16 | url: "https://luxeui.com/open-graphs/og-installation.png",
17 | alt: "Luxe's website cover",
18 | },
19 | ],
20 | locale: "en",
21 | siteName: "Gustavo Rodrigues",
22 | title: "Luxe — Installation",
23 | description: "How to install dependencies and structure your application",
24 | type: "website",
25 | url: "https://luxeui.com/ui/installation",
26 | },
27 | twitter: {
28 | images: [
29 | {
30 | width: 1920,
31 | height: 1080,
32 | url: "https://luxeui.com/open-graphs/og-installation.png",
33 | alt: "Luxe's website cover",
34 | },
35 | ],
36 | card: "summary_large_image",
37 | title: "Luxe — Installation",
38 | description: "How to install dependencies and structure your application",
39 | site: "@guhrodrrigues",
40 | creator: "Gustavo Rodrigues",
41 | },
42 | };
43 |
44 | export default function InstallationPage() {
45 | return (
46 |
47 |
48 |
49 |
50 |
51 | Installation
52 |
53 |
54 | How to install dependencies and structure your application.
55 |
56 |
57 |
58 |
59 | {INSTALLATION.map(({ slug, icon, name }) => (
60 |
61 | ))}
62 |
63 |
69 |
70 | );
71 | }
72 |
--------------------------------------------------------------------------------
/apps/www/src/app/ui/layout.tsx:
--------------------------------------------------------------------------------
1 | import { Metadata } from "next";
2 |
3 | import { Sidebar } from "@/app/ui/_components/sidebar/Sidebar";
4 | import { OnThisPage } from "./_components/OnThisPage";
5 |
6 | export const metadata: Metadata = {
7 | title: {
8 | default: "Browse Components",
9 | template: "Luxe: %s",
10 | },
11 | description:
12 | "Navigate to all the components that will make your application sophisticated and luxurious.",
13 | openGraph: {
14 | images: [
15 | {
16 | width: 1920,
17 | height: 1080,
18 | url: "https://luxeui.com/open-graphs/og-browse-components.png",
19 | alt: "Luxe's website cover",
20 | },
21 | ],
22 | locale: "en",
23 | siteName: "Gustavo Rodrigues",
24 | title: "Luxe — Browse Components",
25 | description:
26 | "Navigate to all the components that will make your application sophisticated and luxurious.",
27 | type: "website",
28 | url: "https://luxeui.com/ui",
29 | },
30 | twitter: {
31 | images: [
32 | {
33 | width: 1920,
34 | height: 1080,
35 | url: "https://luxeui.com/open-graphs/og-browse-components.png",
36 | alt: "Luxe's website cover",
37 | },
38 | ],
39 | card: "summary_large_image",
40 | title: "Luxe — Browse Components",
41 | description:
42 | "Navigate to all the components that will make your application sophisticated and luxurious.",
43 | site: "@guhrodrrigues",
44 | creator: "Gustavo Rodrigues",
45 | },
46 | };
47 |
48 | type ComponentPageLayout = {
49 | children: React.ReactNode;
50 | };
51 |
52 | export default function ComponentPageLayout({ children }: ComponentPageLayout) {
53 | return (
54 |
55 |
56 |
57 |
58 | {children}
59 |
60 |
61 |
62 |
63 | );
64 | }
65 |
--------------------------------------------------------------------------------
/apps/www/src/app/updates/layout.tsx:
--------------------------------------------------------------------------------
1 | import { Footer } from "../_components/Footer";
2 |
3 | export default function UpdatesLayout({
4 | children,
5 | }: {
6 | children: React.ReactNode;
7 | }) {
8 | return (
9 | <>
10 | {children}
11 |
12 | >
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/apps/www/src/data/get-started.ts:
--------------------------------------------------------------------------------
1 | export const GET_STARTED = [
2 | {
3 | name: "Installation",
4 | slug: "/ui/installation",
5 | },
6 | {
7 | name: "CLI",
8 | slug: "/ui/cli",
9 | isBeta: true,
10 | },
11 | ];
12 |
--------------------------------------------------------------------------------
/apps/www/src/data/techs.tsx:
--------------------------------------------------------------------------------
1 | import { Icons } from '@/app/_components/Icons'
2 |
3 | export const TECHS = [
4 | {
5 | icon: ,
6 | name: 'React',
7 | },
8 | {
9 | icon: ,
10 | name: 'tailwindcss',
11 | },
12 | {
13 | icon: ,
14 | name: 'Motion',
15 | },
16 | {
17 | icon: ,
18 | name: 'Radix UI',
19 | },
20 | ]
21 |
--------------------------------------------------------------------------------
/apps/www/src/data/updates.ts:
--------------------------------------------------------------------------------
1 | type Update = {
2 | href: string;
3 | title: string;
4 | description: string;
5 | author: string;
6 | banner: string;
7 | author_image: string;
8 | date: string;
9 | };
10 |
11 | export const UPDATES: Update[] = [
12 | {
13 | href: "v2.0",
14 | title: "Introducing Luxe 2.0",
15 | description:
16 | "One of Luxe's biggest launches yet is here. This new version introduces powerful components with variant props, a new CLI, and a complete redesign.",
17 | author: "Gustavo Rodrigues",
18 | banner: "/open-graphs/updates/v2.0.webp",
19 | author_image: "https://github.com/guhrodrrigues.png",
20 | date: "June 2, 2025",
21 | },
22 | ];
23 |
--------------------------------------------------------------------------------
/apps/www/src/lib/mdx.ts:
--------------------------------------------------------------------------------
1 | import type { Docs } from "@/@types/docs";
2 |
3 | import fs from "fs";
4 | import path from "path";
5 |
6 | import matter from "gray-matter";
7 |
8 | function readFile(filePath: string): Docs | null {
9 | try {
10 | const rawContent = fs.readFileSync(filePath, "utf-8");
11 | const { data, content } = matter(rawContent);
12 |
13 | const slug = path.basename(filePath, path.extname(filePath));
14 |
15 | return {
16 | ...data,
17 | slug,
18 | content,
19 | } as Docs;
20 | } catch (error) {
21 | console.error(`Failed to read or parse the file at ${filePath}:`, error);
22 | return null;
23 | }
24 | }
25 |
26 | function getFiles(dir: string): string[] {
27 | try {
28 | return fs.readdirSync(dir).filter((file) => path.extname(file) === ".mdx");
29 | } catch (error) {
30 | console.error(`Failed to read directory at ${dir}:`, error);
31 | return [];
32 | }
33 | }
34 |
35 | export function getDocs(directory?: string): Docs[] {
36 | const files = getFiles(
37 | path.join(
38 | process.cwd(),
39 | "src/app",
40 | "_docs",
41 | ...(directory ? [directory] : []),
42 | ),
43 | );
44 |
45 | return files
46 | .map((file) =>
47 | readFile(
48 | path.join(
49 | process.cwd(),
50 | "src/app",
51 | "_docs",
52 | ...(directory ? [directory] : []),
53 | file,
54 | ),
55 | ),
56 | )
57 | .filter((docs): docs is Docs => docs !== null);
58 | }
59 |
--------------------------------------------------------------------------------
/apps/www/src/styles/luxe.css:
--------------------------------------------------------------------------------
1 | @import "tailwindcss";
2 | @import "tw-animate-css";
3 |
4 | :root {
5 | --main: oklch(0.97 0 0);
6 | --main-secondary: oklch(97% 0 0);
7 | --main-foreground: oklch(0.925 0 0);
8 | --main-muted: oklch(0.96 0 0);
9 | --main-background: oklch(0.97 0 0);
10 | --main-invert: oklch(0.205 0 0);
11 |
12 | --primary: oklch(0 0 0);
13 | --primary-invert: oklch(1 0 0);
14 | --primary-foreground: oklch(37.1% 0 0);
15 | --primary-muted: oklch(0.556 0 0);
16 |
17 | --border: oklch(0.885 0 0);
18 | }
19 |
20 | .dark {
21 | --main: oklch(0.178 0 0);
22 | --main-secondary: oklch(0.205 0 0);
23 | --main-foreground: oklch(0.269 0 0);
24 | --main-muted: oklch(0.168 0 0);
25 | --main-background: oklch(0.145 0 0);
26 | --main-invert: oklch(0.8 0 0);
27 |
28 | --primary: oklch(1 0 0);
29 | --primary-invert: oklch(0 0 0);
30 | --primary-foreground: oklch(0.97 0 0);
31 | --primary-muted: oklch(0.708 0 0);
32 |
33 | --border: oklch(0.26 0 0);
34 | }
35 |
36 | @theme inline {
37 | --color-main: var(--main);
38 | --color-main-secondary: var(--main-secondary);
39 | --color-main-foreground: var(--main-foreground);
40 | --color-main-muted: var(--main-muted);
41 | --color-main-background: var(--main-background);
42 | --color-main-invert: var(--main-invert);
43 |
44 | --color-primary: var(--primary);
45 | --color-primary-foreground: var(--primary-foreground);
46 | --color-primary-muted: var(--primary-muted);
47 | --color-primary-invert: var(--primary-invert);
48 |
49 | --color-border: var(--border);
50 |
51 | --animate-shine: shine 6s linear infinite;
52 | --animate-text-gradient: text-gradient 2s linear infinite;
53 | --animate-text-shake: text-shake 1s ease 1;
54 | --animate-brightness: brightness 2.2s linear infinite;
55 | --animate-spinner: spinner 1.2s linear infinite;
56 | --animate-accordion-open: accordion-open 0.2s ease-out;
57 | --animate-accordion-close: accordion-close 0.2s ease-out;
58 |
59 | @keyframes accordion-open {
60 | from {
61 | height: 0;
62 | }
63 | to {
64 | height: var(--radix-accordion-content-height);
65 | }
66 | }
67 |
68 | @keyframes accordion-close {
69 | from {
70 | height: var(--radix-accordion-content-height);
71 | }
72 | to {
73 | height: 0;
74 | }
75 | }
76 |
77 | @keyframes shine {
78 | from {
79 | background-position: 0 0;
80 | }
81 | to {
82 | background-position: -400% 0;
83 | }
84 | }
85 |
86 | @keyframes text-gradient {
87 | to {
88 | background-position: 200% center;
89 | }
90 | }
91 |
92 | @keyframes text-shake {
93 | 15% {
94 | transform: translateX(5px);
95 | }
96 | 30% {
97 | transform: translateX(-5px);
98 | }
99 | 50% {
100 | transform: translateX(3px);
101 | }
102 | 80% {
103 | transform: translateX(2px);
104 | }
105 | 100% {
106 | transform: translateX(0);
107 | }
108 | }
109 |
110 | @keyframes brightness {
111 | 0% {
112 | transform: skew(-13deg) translateX(-100%);
113 | }
114 | 100% {
115 | transform: skew(-13deg) translateX(100%);
116 | }
117 | }
118 |
119 | @keyframes spinner {
120 | 0% {
121 | opacity: 1;
122 | }
123 | 100% {
124 | opacity: 0.15;
125 | }
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/apps/www/src/utils/cn.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from 'clsx'
2 | import { twMerge } from 'tailwind-merge'
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
--------------------------------------------------------------------------------
/apps/www/src/utils/detect-os.ts:
--------------------------------------------------------------------------------
1 | const MAC_OS_PLATFORMS = ["Macintosh", "MacIntel", "MacPPC", "Mac68K"];
2 | const WINDOWS_PLATFORMS = ["Win32", "Win64", "Windows", "WinCE"];
3 | const IOS_PLATFORMS = ["iPhone", "iPad", "iPod"];
4 |
5 | function isPlatform(platform: string, platformsArray: string[]) {
6 | return platformsArray.includes(platform);
7 | }
8 |
9 | export function detectOS() {
10 | if (typeof window === "undefined") {
11 | return "Unknown OS - possibly server-side";
12 | }
13 |
14 | const userAgent = window?.navigator.userAgent;
15 | const platform = window?.navigator.platform;
16 |
17 | if (isPlatform(platform, MAC_OS_PLATFORMS)) {
18 | return "Mac OS";
19 | }
20 |
21 | if (isPlatform(platform, IOS_PLATFORMS)) {
22 | return "iOS";
23 | }
24 |
25 | if (isPlatform(platform, WINDOWS_PLATFORMS)) {
26 | return "Windows";
27 | }
28 |
29 | if (/Android/.test(userAgent)) {
30 | return "Android";
31 | }
32 |
33 | if (/Linux/.test(platform)) {
34 | return "Linux";
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/apps/www/src/utils/fonts.ts:
--------------------------------------------------------------------------------
1 | import { GeistMono } from 'geist/font/mono'
2 | import { GeistSans } from 'geist/font/sans'
3 |
4 | export const fontSans = GeistSans
5 | export const fontMono = GeistMono
6 |
--------------------------------------------------------------------------------
/apps/www/src/utils/get-components.ts:
--------------------------------------------------------------------------------
1 | import { getDocs } from "@/lib/mdx";
2 |
3 | export const getComponents = getDocs().sort((a, b) =>
4 | a.title.localeCompare(b.title),
5 | );
6 |
--------------------------------------------------------------------------------
/apps/www/src/utils/get-file-content.ts:
--------------------------------------------------------------------------------
1 | import fs from "fs";
2 | import path from "path";
3 |
4 | export function getFileContent(dir: string, fileName: string) {
5 | const filePath = path.join(process.cwd(), "src", dir, fileName);
6 | const fileContent = fs.readFileSync(filePath, "utf-8");
7 |
8 | return fileContent;
9 | }
10 |
--------------------------------------------------------------------------------
/apps/www/src/utils/shiki.ts:
--------------------------------------------------------------------------------
1 | import { createHighlighter } from 'shiki'
2 |
3 | type CodeProps = {
4 | code: string
5 | lang?: string
6 | }
7 |
8 | export async function codeToHtml({ code, lang = 'tsx' }: CodeProps) {
9 | const highlighter = await createHighlighter({
10 | themes: ['github-dark', 'github-light'],
11 | langs: [lang],
12 | })
13 |
14 | return highlighter.codeToHtml(code, {
15 | lang: lang,
16 | themes: {
17 | dark: 'github-dark',
18 | light: 'github-light',
19 | },
20 | defaultColor: false,
21 | cssVariablePrefix: '--_s-',
22 | })
23 | }
24 |
--------------------------------------------------------------------------------
/apps/www/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": [
4 | "dom",
5 | "dom.iterable",
6 | "esnext"
7 | ],
8 | "allowJs": true,
9 | "skipLibCheck": true,
10 | "strict": true,
11 | "noEmit": true,
12 | "esModuleInterop": true,
13 | "module": "esnext",
14 | "moduleResolution": "bundler",
15 | "resolveJsonModule": true,
16 | "isolatedModules": true,
17 | "jsx": "preserve",
18 | "incremental": true,
19 | "plugins": [
20 | {
21 | "name": "next"
22 | }
23 | ],
24 | "paths": {
25 | "@/*": [
26 | "./src/*"
27 | ]
28 | },
29 | "target": "ES2017"
30 | },
31 | "include": [
32 | "next-env.d.ts",
33 | "**/*.ts",
34 | "**/*.tsx",
35 | ".next/types/**/*.ts"
36 | ],
37 | "exclude": [
38 | "node_modules"
39 | ]
40 | }
41 |
--------------------------------------------------------------------------------
/biome.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
3 | "vcs": {
4 | "enabled": true,
5 | "clientKind": "git",
6 | "useIgnoreFile": false,
7 | "defaultBranch": "main"
8 | },
9 | "files": {
10 | "ignoreUnknown": false,
11 | "ignore": ["node_modules/", "build/", ".next/", "dist/"]
12 | },
13 | "formatter": {
14 | "enabled": true,
15 | "indentStyle": "tab",
16 | "useEditorconfig": true
17 | },
18 | "organizeImports": {
19 | "enabled": true
20 | },
21 | "linter": {
22 | "enabled": true,
23 | "rules": {
24 | "recommended": true,
25 | "nursery": {
26 | "useSortedClasses": {
27 | "level": "warn",
28 | "fix": "unsafe",
29 | "options": {
30 | "functions": ["cn"],
31 | "attributes": ["className", "class"]
32 | }
33 | }
34 | },
35 | "style": {
36 | "noNonNullAssertion": "off"
37 | }
38 | }
39 | },
40 | "javascript": {
41 | "formatter": {
42 | "quoteStyle": "single",
43 | "indentStyle": "space",
44 | "semicolons": "asNeeded",
45 | "arrowParentheses": "asNeeded"
46 | }
47 | },
48 | "json": {
49 | "parser": {
50 | "allowComments": true
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/commitlint.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | extends: ['@commitlint/config-conventional'],
3 | }
4 |
--------------------------------------------------------------------------------
/config/ts-config/base.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "compilerOptions": {
4 | "target": "ES2022",
5 | "module": "ESNext",
6 | "lib": ["ES2022", "DOM"],
7 | "strict": true,
8 | "esModuleInterop": true,
9 | "skipLibCheck": true,
10 | "forceConsistentCasingInFileNames": true,
11 | "resolveJsonModule": true,
12 | "allowSyntheticDefaultImports": true,
13 | "moduleResolution": "bundler",
14 | "declaration": true,
15 | "declarationMap": true,
16 | "sourceMap": true,
17 | "noUnusedLocals": true,
18 | "noUnusedParameters": true,
19 | "noFallthroughCasesInSwitch": true,
20 | "noImplicitReturns": false
21 | },
22 | "exclude": ["node_modules/", "dist/"]
23 | }
24 |
--------------------------------------------------------------------------------
/config/ts-config/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@luxe/ts-config",
3 | "version": "1.0.0",
4 | "exports": {
5 | "./*.json": "./*.json"
6 | },
7 | "license": "MIT",
8 | "private": true
9 | }
10 |
--------------------------------------------------------------------------------
/config/ts-config/react.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "extends": "./base.json",
4 | "compilerOptions": {
5 | "moduleResolution": "bundler",
6 | "jsx": "react-jsx",
7 | "lib": ["ES2022", "DOM"],
8 | "types": ["react", "react-dom"],
9 | "allowJs": false,
10 | "noEmit": true
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "luxe",
3 | "type": "module",
4 | "private": true,
5 | "license": "MIT",
6 | "packageManager": "pnpm@10.3.0",
7 | "engines": {
8 | "node": ">=20"
9 | },
10 | "dependencies": {
11 | "@luxe/react": "workspace:*",
12 | "@radix-ui/react-slot": "^1.2.3",
13 | "@tailwindcss/postcss": "^4.0.14",
14 | "postcss": "^8"
15 | },
16 | "devDependencies": {
17 | "@biomejs/biome": "1.9.4",
18 | "@chromatic-com/storybook": "^3.2.4",
19 | "@commitlint/cli": "18.6.1",
20 | "@commitlint/config-conventional": "18.6.2",
21 | "@luxe/ts-config": "workspace:*",
22 | "@storybook/addon-a11y": "^8.5.5",
23 | "@storybook/addon-essentials": "^8.5.5",
24 | "@storybook/addon-interactions": "^8.5.5",
25 | "@storybook/addon-onboarding": "^8.5.5",
26 | "@storybook/addon-themes": "^8.5.5",
27 | "@storybook/blocks": "^8.5.5",
28 | "@storybook/manager-api": "^8.5.5",
29 | "@storybook/react": "^8.5.5",
30 | "@storybook/react-vite": "^8.5.5",
31 | "@storybook/test": "^8.5.5",
32 | "@storybook/theming": "^8.5.5",
33 | "@tailwindcss/cli": "^4.1.5",
34 | "@tailwindcss/vite": "^4.1.5",
35 | "@types/node": "^20",
36 | "@vitejs/plugin-react": "^4.3.4",
37 | "glob": "^11.0.1",
38 | "prop-types": "^15.8.1",
39 | "storybook": "^8.5.5",
40 | "tailwindcss": "4.0.14",
41 | "tailwindcss-animate": "^1.0.7",
42 | "ts-morph": "^25.0.1",
43 | "tsx": "^4.19.3",
44 | "turbo": "^2.4.1",
45 | "tw-animate-css": "^1.2.8",
46 | "typescript": "^5",
47 | "vite": "^6.1.0"
48 | },
49 | "scripts": {
50 | "dev": "turbo dev --filter=www",
51 | "build": "turbo build",
52 | "lint": "turbo lint",
53 | "storybook": "storybook dev -p 6006",
54 | "storybook:build": "storybook build",
55 | "build:registry": "tsx scripts/build-registry.ts"
56 | },
57 | "pnpm": {
58 | "overrides": {}
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/packages/cli/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@luxeui/ui",
3 | "description": "Add components instantly in your app with the Luxe CLI.",
4 | "version": "1.0.0-beta.1",
5 | "type": "module",
6 | "bin": "./dist/index.js",
7 | "files": ["dist"],
8 | "types": "./dist/index.d.ts",
9 | "scripts": {
10 | "dev": "tsup && NODE_ENV=development node dist/index.js",
11 | "build": "tsup",
12 | "lint": "biome check --write ."
13 | },
14 | "dependencies": {
15 | "@clack/prompts": "0.9.1",
16 | "chalk": "^5.4.1",
17 | "commander": "^13.1.0",
18 | "eta": "^3.5.0",
19 | "lilconfig": "^3.1.3",
20 | "package-manager-detector": "^1.3.0",
21 | "postcss": "^8",
22 | "postcss-merge-rules": "^7.0.5",
23 | "prettier": "^3.5.3",
24 | "prettier-plugin-tailwindcss": "^0.6.11",
25 | "scule": "^1.3.0",
26 | "tsconfig-paths": "^4.2.0",
27 | "zod": "^3.24.2"
28 | },
29 | "devDependencies": {
30 | "@types/node": "^20",
31 | "tsup": "^8.3.6"
32 | },
33 | "license": "MIT"
34 | }
35 |
--------------------------------------------------------------------------------
/packages/cli/src/commands/add/index.ts:
--------------------------------------------------------------------------------
1 | import chalk from 'chalk'
2 | import { Command } from 'commander'
3 |
4 | import { handler } from './handler'
5 | import { AddCommandErrors, preFlight } from './preflight'
6 |
7 | import { CLIError } from '@/utils/cli-error'
8 | import { logger } from '@/utils/logger'
9 |
10 | import { apiConfig } from '@/services/api-config'
11 |
12 | export const add = new Command()
13 | .name('add')
14 | .description('select and add the components you need.')
15 | .argument('[components...]', 'enter the component name(s)')
16 | .action(async args => {
17 | const { errorsFound } = preFlight()
18 |
19 | if (errorsFound[AddCommandErrors.MANIFEST_FILE_NOT_FOUND]) {
20 | logger.error(
21 | `Project not initialized. Run ${chalk.yellow('@luxeui/ui init')} first`,
22 | )
23 | }
24 |
25 | try {
26 | const response = await fetch(`${apiConfig.luxeRegistry}/index.json`, {
27 | method: 'GET',
28 | headers: {
29 | 'Content-Type': 'application/json',
30 | },
31 | })
32 |
33 | const { components } = await response.json()
34 | const availableComponents = components as string[]
35 |
36 | handler(availableComponents, args)
37 | } catch (err) {
38 | if (err instanceof CLIError) {
39 | logger.error(err.message)
40 | }
41 | }
42 | })
43 |
--------------------------------------------------------------------------------
/packages/cli/src/commands/add/preflight.ts:
--------------------------------------------------------------------------------
1 | import { manifestManager } from '@/utils/manifest-manager'
2 |
3 | export enum AddCommandErrors {
4 | MANIFEST_FILE_NOT_FOUND = '1',
5 | }
6 |
7 | export function preFlight() {
8 | const errorsFound = {} as Record
9 |
10 | try {
11 | manifestManager.readManifest
12 | } catch {
13 | errorsFound[AddCommandErrors.MANIFEST_FILE_NOT_FOUND] = true
14 | }
15 |
16 | return {
17 | errorsFound,
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/packages/cli/src/commands/add/utils.ts:
--------------------------------------------------------------------------------
1 | import { promises as fs, existsSync } from 'node:fs'
2 | import path from 'node:path'
3 |
4 | import type { Eta } from 'eta'
5 | import prettier from 'prettier'
6 | import chalk from 'chalk'
7 | import { pascalCase } from 'scule'
8 |
9 | import { type Registry, RegistrySchema } from '@/schemas/registry'
10 | import { apiConfig } from '@/services/api-config'
11 |
12 | import { CLIError } from '@/utils/cli-error'
13 | import { logger } from '@/utils/logger'
14 | import { manifestManager } from '@/utils/manifest-manager'
15 | import { resolveAliasToAbsolutePath } from '@/utils/resolve-alias-to-absolute-path'
16 |
17 | export async function fetchComponentRegistry(componentName: string) {
18 | try {
19 | const response = await fetch(
20 | `${apiConfig.luxeRegistry}/${componentName}.json`,
21 | {
22 | method: 'GET',
23 | headers: {
24 | 'Content-Type': 'application/json',
25 | },
26 | },
27 | )
28 |
29 | if (!response.ok) {
30 | logger.warning(
31 | `Component ${chalk.green(componentName)} not found in the registry. Please check the name.`,
32 | )
33 | return null
34 | }
35 |
36 | const parsed = RegistrySchema.safeParse(await response.json())
37 |
38 | if (!parsed.success) {
39 | logger.warning(
40 | `Component ${componentName} failed schema validation and will be skipped.`,
41 | )
42 | return null
43 | }
44 |
45 | return parsed.data
46 | } catch (error) {
47 | throw new CLIError(
48 | `Failed to fetch registry for ${componentName}: ${(error as Error).message}`,
49 | )
50 | }
51 | }
52 |
53 | export async function writeComponentFileFromTemplate(
54 | componentRegistryEntry: Registry,
55 | eta: Eta,
56 | ) {
57 | const { file } = componentRegistryEntry
58 | const getManifest = manifestManager.readManifest
59 |
60 | try {
61 | const componentCode = eta.renderString(file.content, {
62 | aliases: getManifest.aliases,
63 | })
64 |
65 | const formattedComponentCode = await prettier.format(componentCode, {
66 | parser: 'typescript',
67 | })
68 |
69 | const componentsDirPath = resolveAliasToAbsolutePath(
70 | getManifest.aliases.components,
71 | )
72 |
73 | if (!existsSync(componentsDirPath)) {
74 | await fs.mkdir(componentsDirPath, {
75 | recursive: true,
76 | })
77 | }
78 |
79 | const outFilePath = path.join(componentsDirPath, file.name)
80 |
81 | await fs.writeFile(outFilePath, formattedComponentCode, 'utf-8')
82 | } catch {
83 | throw new CLIError(`Failed to write component file: ${file.name}`)
84 | }
85 | }
86 |
87 | export function logComponentSummary(installedComponents: string[]) {
88 | if (!installedComponents.length) return
89 |
90 | const getManifest = manifestManager.readManifest
91 |
92 | const componentsPath = resolveAliasToAbsolutePath(
93 | getManifest.aliases.components,
94 | )
95 |
96 | const formattedList = installedComponents
97 | .map(
98 | component => ` ${chalk.gray('•')} ${chalk.green(pascalCase(component))}`,
99 | )
100 | .join('\n')
101 |
102 | logger.step(
103 | [
104 | chalk.bold('✔ Components added\n'),
105 | formattedList,
106 | '',
107 | `${chalk.dim('Location →')} ${chalk.yellow(componentsPath)}`,
108 | ].join('\n'),
109 | )
110 | }
111 |
--------------------------------------------------------------------------------
/packages/cli/src/commands/init/index.ts:
--------------------------------------------------------------------------------
1 | import * as p from '@clack/prompts'
2 | import { Command } from 'commander'
3 |
4 | import { CLIError } from '@/utils/cli-error'
5 | import { logger } from '@/utils/logger'
6 |
7 | import { handler } from './handler'
8 | import { InitCommandErrors, preFlight } from './preflight'
9 |
10 | export const init = new Command()
11 | .name('init')
12 | .description('initialize your project and install dependencies.')
13 | .action(async () => {
14 | try {
15 | const { errorsFound } = await preFlight()
16 |
17 | if (errorsFound[InitCommandErrors.UNIDENTIFIED_NODE_PROJECT]) {
18 | logger.error(
19 | `No Node.js project detected. Ensure 'package.json' exists in the current directory.`,
20 | )
21 | }
22 |
23 | if (errorsFound[InitCommandErrors.TAILWIND_NOT_INSTALLED]) {
24 | logger.warning(
25 | 'TailwindCSS is not installed. Continuing may cause issues during initialization.',
26 | )
27 |
28 | const shouldProceedWithoutTailwind = await p.confirm({
29 | message: 'Proceed without TailwindCSS?',
30 | active: 'Yes, proceed anyway.',
31 | inactive: 'No, abort setup.',
32 | initialValue: false,
33 | })
34 |
35 | if (!shouldProceedWithoutTailwind) {
36 | logger.info(
37 | 'Setup aborted. Install TailwindCSS first:\n→ https://tailwindcss.com/docs/installation',
38 | )
39 | process.exit(0)
40 | }
41 | }
42 |
43 | if (errorsFound[InitCommandErrors.INCOMPATIBLE_VERSION_TAILWIND]) {
44 | logger.error(
45 | 'Incompatible TailwindCSS version detected. Please use version 4.x.x or higher.',
46 | )
47 | }
48 |
49 | await handler()
50 | } catch (err) {
51 | if (err instanceof CLIError) {
52 | logger.error(err.message)
53 | }
54 | }
55 | })
56 |
--------------------------------------------------------------------------------
/packages/cli/src/commands/init/preflight.ts:
--------------------------------------------------------------------------------
1 | import { promises as fs, existsSync } from 'node:fs'
2 | import path from 'node:path'
3 |
4 | import { PROCESS_CWD, TAILWIND_PACKAGE, TAILWIND_V4_REGEX } from '@/utils/const'
5 |
6 | export enum InitCommandErrors {
7 | UNIDENTIFIED_NODE_PROJECT = '1',
8 | TAILWIND_NOT_INSTALLED = '2',
9 | INCOMPATIBLE_VERSION_TAILWIND = '3',
10 | }
11 |
12 | export async function preFlight() {
13 | const errorsFound = {} as Record
14 |
15 | const packageJsonFilePath = path.resolve(PROCESS_CWD, 'package.json')
16 |
17 | const isPackageJsonFileExists = existsSync(packageJsonFilePath)
18 |
19 | if (!isPackageJsonFileExists) {
20 | errorsFound[InitCommandErrors.UNIDENTIFIED_NODE_PROJECT] = true
21 | }
22 |
23 | const packageJsonFileContent = await fs.readFile(packageJsonFilePath, 'utf8')
24 |
25 | const { dependencies, devDependencies } = JSON.parse(
26 | packageJsonFileContent,
27 | ) as {
28 | dependencies: Record
29 | devDependencies: Record
30 | }
31 |
32 | const isTailwindInDeps =
33 | TAILWIND_PACKAGE in dependencies || TAILWIND_PACKAGE in devDependencies
34 |
35 | if (!isTailwindInDeps) {
36 | errorsFound[InitCommandErrors.TAILWIND_NOT_INSTALLED] = true
37 | } else {
38 | const version =
39 | dependencies[TAILWIND_PACKAGE] || devDependencies[TAILWIND_PACKAGE]
40 |
41 | const isTailwindV4 = TAILWIND_V4_REGEX.test(version)
42 |
43 | if (!isTailwindV4) {
44 | errorsFound[InitCommandErrors.INCOMPATIBLE_VERSION_TAILWIND] = true
45 | }
46 | }
47 |
48 | return {
49 | errorsFound,
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/packages/cli/src/index.ts:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env node
2 |
3 | import { program } from 'commander'
4 |
5 | import { description, name, version } from '../package.json'
6 |
7 | import { add } from '@/commands/add'
8 | import { init } from '@/commands/init'
9 |
10 | function main() {
11 | program
12 | .version(version)
13 | .name(name)
14 | .description(description)
15 | .addCommand(init)
16 | .addCommand(add)
17 |
18 | program.parse()
19 | }
20 |
21 | main()
22 |
--------------------------------------------------------------------------------
/packages/cli/src/lib/postcss/index.ts:
--------------------------------------------------------------------------------
1 | import postcss from 'postcss'
2 | export { postcss }
3 |
--------------------------------------------------------------------------------
/packages/cli/src/lib/postcss/plugins.ts:
--------------------------------------------------------------------------------
1 | import type { AtRule, Plugin, Rule } from 'postcss'
2 | import postcssMergeRules from 'postcss-merge-rules'
3 |
4 | const postcssMergeRootBlocks: Plugin = {
5 | postcssPlugin: 'postcss-merge-root-blocks',
6 | Once(root) {
7 | let firstRootBlock: Rule | null = null
8 | let lastRootBlock: Rule | null = null
9 |
10 | const importAtRules: AtRule[] = []
11 |
12 | root.walkAtRules('import', atRule => {
13 | importAtRules.push(atRule)
14 | })
15 |
16 | root.walkRules((rule: Rule) => {
17 | if (rule.selector === ':root') {
18 | if (!firstRootBlock) {
19 | firstRootBlock = rule
20 | }
21 | lastRootBlock = rule
22 | }
23 | })
24 |
25 | if (firstRootBlock && lastRootBlock && firstRootBlock !== lastRootBlock) {
26 | // @ts-expect-error
27 | firstRootBlock.each(node => {
28 | lastRootBlock!.append(node)
29 | })
30 |
31 | // @ts-expect-error
32 | firstRootBlock.remove()
33 | }
34 |
35 | if (importAtRules.length > 0) {
36 | for (const atRule of importAtRules) {
37 | atRule.remove()
38 | root.prepend(atRule)
39 | }
40 | }
41 | },
42 | }
43 |
44 | // @TODO: Create a plugin in the future that prevents code duplication
45 | // const postcssRemoveDuplicateBlocks: Plugin = {
46 | // postcssPlugin: 'postcss-remove-duplicate-blocks',
47 | // Once(root) {},
48 | // }
49 |
50 | export { postcssMergeRules, postcssMergeRootBlocks }
51 |
--------------------------------------------------------------------------------
/packages/cli/src/schemas/manifest.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod'
2 |
3 | export const ManifestSchema = z.object({
4 | tailwind: z.object({
5 | css: z.string(),
6 | }),
7 | aliases: z.object({
8 | components: z.string(),
9 | utils: z.string(),
10 | }),
11 | })
12 |
13 | export type Manifest = z.infer
14 |
--------------------------------------------------------------------------------
/packages/cli/src/schemas/registry.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod'
2 |
3 | export const RegistrySchema = z.object({
4 | name: z.string(),
5 | externalDependencies: z.array(z.string()),
6 | internalDependencies: z.array(z.string()),
7 | file: z.object({
8 | name: z.string(),
9 | content: z.string(),
10 | }),
11 | })
12 |
13 | export type Registry = z.infer
14 |
--------------------------------------------------------------------------------
/packages/cli/src/services/api-config.ts:
--------------------------------------------------------------------------------
1 | const baseURL = 'https://luxeui.com'
2 | // const baseURL = 'http://localhost:3000'
3 |
4 | export const apiConfig = {
5 | baseURL,
6 | luxeManifestUrl: `${baseURL}/schemas/luxe.json`,
7 | luxeRegistry: `${baseURL}/registry/components`,
8 | }
9 |
--------------------------------------------------------------------------------
/packages/cli/src/utils/cli-error.ts:
--------------------------------------------------------------------------------
1 | const DEFAULT_ERROR_MESSAGE =
2 | 'An internal error occurred in the Hynix CLI. Please report this issue if it persists.'
3 |
4 | export class CLIError extends Error {
5 | constructor(
6 | message: string = DEFAULT_ERROR_MESSAGE,
7 | public readonly cause?: unknown,
8 | ) {
9 | super(message)
10 | this.name = 'CLIError'
11 |
12 | if (Error.captureStackTrace) {
13 | Error.captureStackTrace(this, CLIError)
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/packages/cli/src/utils/logger.ts:
--------------------------------------------------------------------------------
1 | import { log } from '@clack/prompts'
2 | import chalk from 'chalk'
3 |
4 | import type { ForegroundColorName } from 'chalk'
5 |
6 | export const logger = {
7 | info(message: string) {
8 | log.info(chalk.blue(message))
9 | },
10 | success(message: string) {
11 | log.success(chalk.green(message))
12 | },
13 | error(message: string) {
14 | log.error(chalk.red(message))
15 | process.exit(0)
16 | },
17 | warning(message: string) {
18 | log.warning(chalk.yellow(message))
19 | },
20 | custom(
21 | message: string,
22 | options: {
23 | color?: ForegroundColorName
24 | },
25 | ) {
26 | const { color } = options
27 | log.message(chalk[color ?? 'grey'](message))
28 | },
29 | step: log.step,
30 |
31 | // Used only in dev environment
32 | debug(message: string) {
33 | log.message(chalk.magenta(`[DEV] ${message}`))
34 | },
35 | }
36 |
--------------------------------------------------------------------------------
/packages/cli/src/utils/manifest-manager.ts:
--------------------------------------------------------------------------------
1 | import { promises as fs } from 'node:fs'
2 | import path from 'node:path'
3 |
4 | import { lilconfigSync } from 'lilconfig'
5 |
6 | import { type Manifest, ManifestSchema } from '@/schemas/manifest'
7 |
8 | import { CLIError } from './cli-error'
9 | import { FS_ERROR_CODES, MANIFEST_FILE, PROCESS_CWD } from './const'
10 |
11 | class ManifestManager {
12 | public async saveManifest(content: Manifest) {
13 | try {
14 | const result = ManifestSchema.safeParse(content)
15 |
16 | if (!result.success) {
17 | throw new Error(
18 | 'Invalid manifest data format received. Ensure it complies with the expected schema.',
19 | )
20 | }
21 |
22 | const structuredFileContent = JSON.stringify(result.data, null, 2)
23 |
24 | await fs.writeFile(
25 | path.resolve(PROCESS_CWD, MANIFEST_FILE),
26 | structuredFileContent,
27 | 'utf8',
28 | )
29 | } catch (err) {
30 | if (err.code === FS_ERROR_CODES.PERMISSION_DENIED) {
31 | throw new CLIError(
32 | `Unable to write to the manifest file. Please ensure you have the necessary permissions\nto write to the directory: ${PROCESS_CWD}.`,
33 | )
34 | }
35 | }
36 | }
37 |
38 | public get readManifest(): Manifest {
39 | const manifestFile = lilconfigSync('luxe', {
40 | searchPlaces: [MANIFEST_FILE],
41 | }).search()
42 |
43 | if (!manifestFile) {
44 | throw new CLIError('Could not read your manifest file.')
45 | }
46 |
47 | return manifestFile.config
48 | }
49 | }
50 |
51 | export const manifestManager = new ManifestManager()
52 |
--------------------------------------------------------------------------------
/packages/cli/src/utils/resolve-alias-to-absolute-path.ts:
--------------------------------------------------------------------------------
1 | import path from 'node:path'
2 | import * as tsConfigPaths from 'tsconfig-paths'
3 |
4 | import { PROCESS_CWD } from './const'
5 |
6 | export function resolveAliasToAbsolutePath(aliasImport: string) {
7 | const configResult = tsConfigPaths.loadConfig(PROCESS_CWD)
8 |
9 | if (configResult.resultType === 'failed') {
10 | throw new Error(`Failed to load tsconfig: ${configResult.message}`)
11 | }
12 |
13 | const { absoluteBaseUrl, paths } = configResult
14 |
15 | if (!paths) {
16 | throw new Error('No paths found in tsconfig configuration.')
17 | }
18 |
19 | for (const aliasPattern in paths) {
20 | const aliasPrefix = aliasPattern.replace(/\*$/, '')
21 | const targetPatterns = paths[aliasPattern]
22 |
23 | if (aliasImport.startsWith(aliasPrefix)) {
24 | const remainingPath = aliasImport.slice(aliasPrefix.length)
25 |
26 | const targetPattern = targetPatterns[0]
27 | const targetPrefix = targetPattern.replace(/\*$/, '')
28 |
29 | const resolvedPath = path.resolve(
30 | absoluteBaseUrl,
31 | targetPrefix,
32 | remainingPath,
33 | )
34 |
35 | return resolvedPath
36 | }
37 | }
38 |
39 | throw new Error(
40 | `Failed to resolve the alias '${aliasImport}' to an absolute path. Please verify that the alias is correctly configured in tsconfig.json.`,
41 | )
42 | }
43 |
--------------------------------------------------------------------------------
/packages/cli/src/utils/resolve-package-manager-command.ts:
--------------------------------------------------------------------------------
1 | import type { Command, ResolvedCommand } from 'package-manager-detector'
2 |
3 | import { resolveCommand } from 'package-manager-detector/commands'
4 | import { detect } from 'package-manager-detector/detect'
5 |
6 | export async function resolvePackageManagerCommand(
7 | command: Command,
8 | args: string[],
9 | ) {
10 | const pm = await detect({
11 | strategies: [
12 | 'lockfile',
13 | 'packageManager-field',
14 | 'install-metadata',
15 | 'devEngines-field',
16 | ],
17 | })
18 |
19 | if (!pm) {
20 | throw new Error('Could not detect package manager.')
21 | }
22 |
23 | return resolveCommand(pm.agent, command, args) ?? ({} as ResolvedCommand)
24 | }
25 |
--------------------------------------------------------------------------------
/packages/cli/src/utils/run-shell-command.ts:
--------------------------------------------------------------------------------
1 | import { exec } from 'node:child_process'
2 | import { promisify } from 'node:util'
3 |
4 | export async function runShellCommand(command: string) {
5 | try {
6 | const execAsync = promisify(exec)
7 | await execAsync(command)
8 | } catch (err) {
9 | throw new Error(`Command failed: ${command}`)
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/packages/cli/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@luxe/ts-config/base.json",
3 | "compilerOptions": {
4 | "baseUrl": ".",
5 | "paths": {
6 | "@/*": ["src/*"]
7 | },
8 | "useUnknownInCatchVariables": false
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/packages/cli/tsup.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'tsup'
2 |
3 | export default defineConfig(() => {
4 | return {
5 | entry: ['./src/index.ts'],
6 | format: ['esm', 'cjs'],
7 | dts: true,
8 | minify: true,
9 | target: 'esnext',
10 | }
11 | })
12 |
--------------------------------------------------------------------------------
/packages/react/__stories__/components/accordion.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react'
2 |
3 | import {
4 | Accordion,
5 | AccordionContent,
6 | AccordionItem,
7 | AccordionTrigger,
8 | } from '@/components/accordion'
9 |
10 | const meta: Meta = {
11 | title: 'components/Accordion',
12 | component: () => (
13 |
19 |
20 | Is it accessible?
21 |
22 | Yes. It adheres to the WAI-ARIA design pattern.
23 |
24 |
25 |
26 | Is it unstyled?
27 |
28 | Yes. It's unstyled by default, giving you freedom over the look and
29 | feel.
30 |
31 |
32 |
33 | Can it be animated?
34 |
35 | Yes! You can animate the Accordion with CSS or JavaScript.
36 |
37 |
38 |
39 | ),
40 | parameters: {
41 | layout: 'centered',
42 | },
43 | }
44 |
45 | export default meta
46 |
47 | export const Basic: StoryObj = {}
48 |
--------------------------------------------------------------------------------
/packages/react/__stories__/components/animated-tabs.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react'
2 |
3 | import { AnimatedTabs } from '@/components/animated-tabs'
4 |
5 | const meta: Meta = {
6 | title: 'components/AnimatedTabs',
7 | component: AnimatedTabs,
8 | args: {
9 | tabs: ['All Posts', 'Interactions', 'Resources', 'Docs'],
10 | },
11 | parameters: {
12 | layout: 'centered',
13 | },
14 | }
15 |
16 | export default meta
17 |
18 | export const Basic: StoryObj = {}
19 |
--------------------------------------------------------------------------------
/packages/react/__stories__/components/badge.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react'
2 |
3 | import { Badge, type BadgeProps } from '@/components/badge'
4 |
5 | const meta: Meta = {
6 | title: 'components/Badge',
7 | component: Badge,
8 | args: {
9 | children: 'Badge',
10 | },
11 | argTypes: {
12 | variant: {
13 | options: [
14 | 'shine',
15 | 'outline',
16 | 'animated-border',
17 | 'rotate-border',
18 | 'success',
19 | 'destructive',
20 | 'default',
21 | ],
22 | control: 'radio',
23 | },
24 | },
25 | parameters: {
26 | layout: 'centered',
27 | },
28 | }
29 |
30 | export default meta
31 |
32 | export const Basic: StoryObj = {}
33 |
--------------------------------------------------------------------------------
/packages/react/__stories__/components/button.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react'
2 |
3 | import { Button, type ButtonProps } from '@/components/button'
4 |
5 | const meta: Meta = {
6 | title: 'components/Button',
7 | component: Button,
8 | args: {
9 | children: 'Button',
10 | },
11 | argTypes: {
12 | variant: {
13 | options: [
14 | 'shine',
15 | 'outline',
16 | 'animated-border',
17 | 'rotate-border',
18 | 'success',
19 | 'destructive',
20 | 'glitch-brightness',
21 | 'default',
22 | ],
23 | control: 'radio',
24 | },
25 | magnetic: {
26 | control: 'boolean',
27 | },
28 | },
29 | parameters: {
30 | layout: 'centered',
31 | },
32 | }
33 |
34 | export default meta
35 |
36 | export const Basic: StoryObj = {}
37 |
--------------------------------------------------------------------------------
/packages/react/__stories__/components/card.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react'
2 |
3 | import { Card, type CardProps } from '@/components/card'
4 |
5 | const meta: Meta = {
6 | title: 'components/Card',
7 | component: Card,
8 | args: {
9 | children: (
10 |
11 |
12 | Luxe
13 |
14 |
15 | Explore the new website that simplifies the creation of sophisticated
16 | dark mode components.
17 |
18 |
19 | ),
20 | },
21 | argTypes: {
22 | variant: {
23 | options: ['default', 'animated-border', 'shine', 'revealed-pointer'],
24 | control: 'radio',
25 | },
26 | },
27 | parameters: {
28 | layout: 'centered',
29 | },
30 | }
31 |
32 | export default meta
33 |
34 | export const Basic: StoryObj = {}
35 |
--------------------------------------------------------------------------------
/packages/react/__stories__/components/checkbox.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react'
2 |
3 | import { Checkbox } from '@/components/checkbox'
4 |
5 | const meta: Meta = {
6 | title: 'components/Checkbox',
7 | component: Checkbox,
8 | parameters: {
9 | layout: 'centered',
10 | },
11 | }
12 |
13 | export default meta
14 |
15 | export const Basic: StoryObj = {}
16 |
--------------------------------------------------------------------------------
/packages/react/__stories__/components/dialog.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react'
2 |
3 | import {
4 | Dialog,
5 | DialogClose,
6 | DialogContent,
7 | DialogDescription,
8 | DialogFooter,
9 | DialogTitle,
10 | DialogTrigger,
11 | } from '@/components/dialog'
12 | import { Button } from '@/components/button'
13 | import { Input } from '@/components/input'
14 |
15 | const meta: Meta = {
16 | title: 'components/Dialog',
17 | component: Dialog,
18 | args: {
19 | children: (
20 |
21 |
22 | Open
23 |
24 |
25 |
26 | Change Username
27 |
28 |
29 | Make changes to your username here.
30 |
31 |
32 |
33 |
34 |
35 |
36 | Cancel
37 |
38 |
39 | Save Changes
40 |
41 |
42 |
43 |
44 | ),
45 | },
46 | parameters: {
47 | layout: 'centered',
48 | },
49 | }
50 |
51 | export default meta
52 |
53 | export const Basic: StoryObj = {}
54 |
--------------------------------------------------------------------------------
/packages/react/__stories__/components/dropdown-menu.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react'
2 |
3 | import { cn } from '@/utils/cn'
4 |
5 | import {
6 | DropdownMenu,
7 | DrodpownMenuTrigger,
8 | DropdownMenuItem,
9 | DropdownMenuContent,
10 | } from '@/components/dropdown-menu'
11 |
12 | import {
13 | LayoutGridIcon,
14 | TrashIcon,
15 | Building2,
16 | UserCircleIcon,
17 | ChevronRightIcon,
18 | BellIcon,
19 | } from 'lucide-react'
20 |
21 | const ITEMS = [
22 | { icon: , name: 'Profile' },
23 | { icon: , name: 'Your applications' },
24 | { icon: , name: 'Teams' },
25 | { icon: , name: 'Notifications' },
26 | {
27 | icon: ,
28 | name: 'Remove account',
29 | customStyle:
30 | '!text-red-500 duration-150 hover:!bg-red-600/10 focus-visible:text-red-500 focus-visible:!bg-red-500/10 focus-visible:!border-red-500/10',
31 | },
32 | ]
33 |
34 | const meta: Meta = {
35 | title: 'components/Dropdown',
36 | component: () => (
37 |
38 |
39 |
40 | Settings
41 |
42 |
43 |
44 | {ITEMS.map(({ icon, name, customStyle }, index) => (
45 |
46 | {icon}
47 |
48 | {name}
49 |
53 |
54 |
55 | ))}
56 |
57 |
58 | ),
59 | parameters: {
60 | layout: 'centered',
61 | },
62 | }
63 |
64 | export default meta
65 |
66 | export const Basic: StoryObj = {}
67 |
--------------------------------------------------------------------------------
/packages/react/__stories__/components/input.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react'
2 |
3 | import { Input, type InputProps } from '@/components/input'
4 |
5 | const meta: Meta = {
6 | title: 'components/Input',
7 | component: Input,
8 | args: {
9 | placeholder: 'Placeholder',
10 | },
11 | parameters: {
12 | layout: 'centered',
13 | },
14 | }
15 |
16 | export default meta
17 |
18 | export const Basic: StoryObj = {}
19 |
--------------------------------------------------------------------------------
/packages/react/__stories__/components/multi-step-modal.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react'
2 |
3 | import {
4 | MultiStepModal,
5 | MultiStepModalContent,
6 | MultiStepModalTrigger,
7 | } from '@/components/multi-step-modal'
8 |
9 | import { Button } from '@/components/button'
10 |
11 | const meta: Meta = {
12 | title: 'components/MultiStepModal',
13 | component: () => {
14 | const steps = [
15 | {
16 | title: 'Luxe',
17 | description:
18 | 'A library of components ready for you to copy and paste, designed to illuminate your apps with elegance, sophistication and a unique touch of style.',
19 | },
20 | {
21 | title: 'How to use?',
22 | description:
23 | 'Simply click on a component, copy the code and paste it into your project. This will give your app an extra shine.',
24 | },
25 | {
26 | title: 'Results',
27 | description:
28 | 'Luxe will add extra shine to your application, with smooth components.',
29 | },
30 | {
31 | title: 'Copy now',
32 | description:
33 | 'Elevate your project with sophisticated, ready to use components. Illuminate up your app quickly, easily and effortlessly!',
34 | },
35 | ]
36 |
37 | return (
38 |
39 |
40 | Open
41 |
42 |
43 |
44 |
45 | )
46 | },
47 | parameters: {
48 | layout: 'centered',
49 | },
50 | }
51 |
52 | export default meta
53 |
54 | export const Basic: StoryObj = {}
55 |
--------------------------------------------------------------------------------
/packages/react/__stories__/components/spinner.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react'
2 |
3 | import { Spinner } from '@/components/spinner'
4 |
5 | const meta: Meta = {
6 | title: 'components/Spinner',
7 | component: Spinner,
8 | parameters: {
9 | layout: 'centered',
10 | },
11 | }
12 |
13 | export default meta
14 |
15 | export const Basic: StoryObj = {}
16 |
--------------------------------------------------------------------------------
/packages/react/__stories__/components/switch.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react'
2 |
3 | import { Switch } from '@/components/switch'
4 |
5 | const meta: Meta = {
6 | title: 'components/Switch',
7 | component: Switch,
8 | parameters: {
9 | layout: 'centered',
10 | },
11 | }
12 |
13 | export default meta
14 |
15 | export const Basic: StoryObj = {}
16 |
--------------------------------------------------------------------------------
/packages/react/__stories__/components/text.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react'
2 |
3 | import { Text } from '@/components/text'
4 |
5 | const meta: Meta = {
6 | title: 'components/Text',
7 | component: Text,
8 | args: {
9 | children: 'This is a text',
10 | },
11 | argTypes: {
12 | variant: {
13 | options: ['shine', 'generate-effect', 'glitch', 'hover-enter', 'shake'],
14 | control: 'radio',
15 | },
16 | },
17 | parameters: {
18 | layout: 'centered',
19 | },
20 | }
21 |
22 | export default meta
23 |
24 | export const Basic: StoryObj = {}
25 |
--------------------------------------------------------------------------------
/packages/react/__stories__/components/tooltip.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react'
2 |
3 | import {
4 | TooltipProvider,
5 | Tooltip,
6 | TooltipTrigger,
7 | TooltipContent,
8 | } from '@/components/tooltip'
9 | import { Button } from '@/components/button'
10 |
11 | const meta: Meta = {
12 | title: 'components/Tooltip',
13 | component: () => (
14 |
15 |
16 |
17 | Hover
18 |
19 |
20 | Add to library
21 |
22 |
23 |
24 | ),
25 | parameters: {
26 | layout: 'centered',
27 | },
28 | }
29 |
30 | export default meta
31 |
32 | export const Basic: StoryObj = {}
33 |
--------------------------------------------------------------------------------
/packages/react/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@luxe/react",
3 | "version": "1.0.0",
4 | "type": "module",
5 | "exports": {
6 | "./styles.css": "./src/styles/globals.css"
7 | },
8 | "license": "MIT",
9 | "dependencies": {
10 | "@radix-ui/react-accordion": "^1.2.8",
11 | "@radix-ui/react-checkbox": "^1.1.2",
12 | "@radix-ui/react-dialog": "^1.1.6",
13 | "@radix-ui/react-switch": "^1.2.3",
14 | "@radix-ui/react-tooltip": "^1.2.4",
15 | "clsx": "^2.1.0",
16 | "lucide-react": "^0.330.0",
17 | "motion": "^11.15.0",
18 | "react": "^19.0.0",
19 | "react-dom": "^19.0.0",
20 | "react-use-measure": "^2.1.1",
21 | "tailwind-merge": "^2.2.1",
22 | "tailwind-variants": "0.2.0"
23 | },
24 | "devDependencies": {
25 | "@types/react": "^18",
26 | "@types/react-dom": "^18"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/packages/react/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | const config = {
2 | plugins: {
3 | "@tailwindcss/postcss": {},
4 | },
5 | };
6 |
7 | export default config;
8 |
--------------------------------------------------------------------------------
/packages/react/src/components/accordion.tsx:
--------------------------------------------------------------------------------
1 | import * as RadixAccordion from '@radix-ui/react-accordion'
2 |
3 | import { cn } from '@/utils/cn'
4 |
5 | export const Accordion = RadixAccordion.Root
6 |
7 | type AccordionItemProps = React.ComponentProps
8 |
9 | export function AccordionItem({
10 | children,
11 | value,
12 | className,
13 | ...props
14 | }: AccordionItemProps) {
15 | return (
16 |
24 | {children}
25 |
26 | )
27 | }
28 |
29 | type AccordionTriggerProps = React.ComponentProps
30 |
31 | export function AccordionTrigger({
32 | children,
33 | className,
34 | ...props
35 | }: AccordionTriggerProps) {
36 | return (
37 |
38 | svg]:rotate-45',
42 | className,
43 | )}
44 | {...props}
45 | >
46 | {children}
47 |
58 | Trigger
59 |
60 |
61 |
62 |
63 |
64 | )
65 | }
66 |
67 | type AccordionContentProps = React.ComponentProps
68 |
69 | export function AccordionContent({
70 | children,
71 | className,
72 | ...props
73 | }: AccordionContentProps) {
74 | return (
75 |
82 | {children}
83 |
84 | )
85 | }
86 |
--------------------------------------------------------------------------------
/packages/react/src/components/animated-tabs.tsx:
--------------------------------------------------------------------------------
1 | 'use client' // @NOTE: Add in case you are using Next.js
2 |
3 | import { useEffect, useRef, useState } from 'react'
4 |
5 | import { cn } from '@/utils/cn'
6 |
7 | type AnimatedTabsProps = {
8 | tabs: Array
9 | }
10 |
11 | export function AnimatedTabs({ tabs }: AnimatedTabsProps) {
12 | const [activeTab, setActiveTab] = useState(tabs[0])
13 |
14 | const containerRef = useRef(null)
15 | const activeTabRef = useRef(null)
16 |
17 | useEffect(() => {
18 | const container = containerRef.current
19 |
20 | if (container && activeTab) {
21 | const activeTabElement = activeTabRef.current
22 |
23 | if (activeTabElement) {
24 | const { offsetLeft, offsetWidth } = activeTabElement
25 |
26 | const clipLeft = offsetLeft
27 | const clipRight = offsetLeft + offsetWidth
28 |
29 | container.style.clipPath = `inset(0 ${Number(100 - (clipRight / container.offsetWidth) * 100).toFixed()}% 0 ${Number((clipLeft / container.offsetWidth) * 100).toFixed()}% round 17px)`
30 | }
31 | }
32 | }, [activeTab])
33 |
34 | return (
35 |
36 |
40 |
42 | {tabs.map((tab, index) => (
43 | setActiveTab(tab)}
47 | className={cn(
48 | 'flex h-8 items-center rounded-full p-3 font-medium text-primary-invert text-sm/5.5',
49 | )}
50 | tabIndex={-1}
51 | >
52 | {tab}
53 |
54 | ))}
55 |
56 |
57 |
58 | {tabs.map((tab, index) => {
59 | const isActive = activeTab === tab
60 |
61 | return (
62 | setActiveTab(tab)}
67 | className='flex h-8 items-center rounded-full p-3 font-medium text-primary-muted text-sm/5.5'
68 | >
69 | {tab}
70 |
71 | )
72 | })}
73 |
74 |
75 | )
76 | }
77 |
--------------------------------------------------------------------------------
/packages/react/src/components/checkbox.tsx:
--------------------------------------------------------------------------------
1 | "use client"; // @NOTE: Add in case you are using Next.js
2 |
3 | import * as RadixCheckbox from "@radix-ui/react-checkbox";
4 |
5 | import { AnimatePresence, motion } from "motion/react";
6 |
7 | type CheckboxProps = React.CustomComponentPropsWithRef<
8 | typeof RadixCheckbox.Root
9 | >;
10 |
11 | export function Checkbox(props: CheckboxProps) {
12 | const { checked } = props;
13 |
14 | return (
15 |
19 |
20 |
24 |
38 |
39 |
40 |
41 |
42 |
43 | );
44 | }
45 |
46 | type CheckIconProps = {
47 | checkedState: CheckboxProps["checked"];
48 | };
49 |
50 | function CheckIcon({ checkedState }: CheckIconProps) {
51 | const CHECK_PATH = "M5 13 L10 18 L20 6";
52 | const INDETERMINATE_PATH = "M6 12 H18";
53 |
54 | return (
55 |
62 | Check
63 |
64 |
77 |
78 | );
79 | }
80 |
--------------------------------------------------------------------------------
/packages/react/src/components/dialog.tsx:
--------------------------------------------------------------------------------
1 | import * as RadixDialog from '@radix-ui/react-dialog'
2 |
3 | import { cn } from '@/utils/cn'
4 |
5 | export const Dialog = RadixDialog.Root
6 |
7 | export const DialogTrigger = RadixDialog.Trigger
8 |
9 | export const DialogClose = RadixDialog.Close
10 |
11 | function DialogOverlay() {
12 | return (
13 |
14 |
21 |
22 | )
23 | }
24 |
25 | type DialogContentProps = React.ComponentProps
26 |
27 | export function DialogContent({
28 | children,
29 | className,
30 | ...props
31 | }: DialogContentProps) {
32 | return (
33 |
34 |
35 |
45 | {children}
46 |
47 |
48 | )
49 | }
50 |
51 | type DialogTitleProps = React.ComponentProps
52 |
53 | export function DialogTitle({
54 | children,
55 | className,
56 | ...props
57 | }: DialogTitleProps) {
58 | return (
59 |
63 | {children}
64 |
65 | )
66 | }
67 |
68 | type DialogDescriptionProps = React.ComponentProps<
69 | typeof RadixDialog.Description
70 | >
71 |
72 | export function DialogDescription({
73 | children,
74 | className,
75 | ...props
76 | }: DialogDescriptionProps) {
77 | return (
78 |
85 | {children}
86 |
87 | )
88 | }
89 |
90 | type DialogFooterProps = React.ComponentProps<'div'>
91 |
92 | export function DialogFooter({
93 | children,
94 | className,
95 | ...props
96 | }: DialogFooterProps) {
97 | return (
98 |
105 | {children}
106 |
107 | )
108 | }
109 |
--------------------------------------------------------------------------------
/packages/react/src/components/input.tsx:
--------------------------------------------------------------------------------
1 | "use client"; // @NOTE: Add in case you are using Next.js
2 |
3 | import { useState } from "react";
4 |
5 | import { AnimatePresence, motion, type Variants } from "motion/react";
6 |
7 | import { cn } from "@/utils/cn";
8 |
9 | export type InputProps = React.ComponentPropsWithRef<"input">;
10 | type FieldState = "idle" | "filled";
11 |
12 | export function Input({
13 | placeholder,
14 | onChange,
15 | className,
16 | ...props
17 | }: InputProps) {
18 | const [fieldState, setFieldState] = useState("idle");
19 |
20 | const animatedPlaceholderVariants: Variants = {
21 | show: {
22 | x: 0,
23 | opacity: 1,
24 | filter: "blur(var(--blur-none))",
25 | },
26 | hidden: {
27 | x: 28,
28 | opacity: 0,
29 | filter: "blur(var(--blur-xs))",
30 | },
31 | };
32 |
33 | return (
34 |
42 |
{
50 | setFieldState(event.target.value.length > 0 ? "filled" : "idle");
51 | onChange?.(event);
52 | }}
53 | />
54 |
55 |
56 | {fieldState !== "filled" && (
57 |
72 | {placeholder}
73 |
74 | )}
75 |
76 |
77 | );
78 | }
79 |
--------------------------------------------------------------------------------
/packages/react/src/components/spinner.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/utils/cn'
2 |
3 | type SpinnerProps = {
4 | size?: string
5 | } & React.ComponentProps<'div'>
6 |
7 | export function Spinner({
8 | size = 'size-6',
9 | className,
10 | ...props
11 | }: SpinnerProps) {
12 | const bars = Array(12).fill(0)
13 |
14 | return (
15 |
16 |
17 | {bars.map((_, i) => (
18 |
32 | ))}
33 |
34 |
35 | )
36 | }
37 |
--------------------------------------------------------------------------------
/packages/react/src/components/switch.tsx:
--------------------------------------------------------------------------------
1 | import * as RadixSwitch from '@radix-ui/react-switch'
2 |
3 | import { cn } from '@/utils/cn'
4 |
5 | export type SwitchProps = React.ComponentProps
6 |
7 | export function Switch({ className, ...props }: SwitchProps) {
8 | return (
9 |
17 |
24 |
25 | )
26 | }
27 |
--------------------------------------------------------------------------------
/packages/react/src/components/tooltip.tsx:
--------------------------------------------------------------------------------
1 | import * as RadixTooltip from '@radix-ui/react-tooltip'
2 |
3 | import { cn } from '@/utils/cn'
4 |
5 | export const Tooltip = RadixTooltip.Root
6 |
7 | export const TooltipTrigger = RadixTooltip.Trigger
8 |
9 | type TooltipProviderProps = React.ComponentProps
10 |
11 | export function TooltipProvider({ children, ...props }: TooltipProviderProps) {
12 | return (
13 |
14 | {children}
15 |
16 | )
17 | }
18 |
19 | type TooltipContentProps = React.ComponentProps
20 |
21 | export function TooltipContent({
22 | children,
23 | className,
24 | sideOffset = 6,
25 | ...props
26 | }: TooltipContentProps) {
27 | return (
28 |
29 |
40 | {children}
41 |
42 |
43 | )
44 | }
45 |
--------------------------------------------------------------------------------
/packages/react/src/styles/globals.css:
--------------------------------------------------------------------------------
1 | @import "tailwindcss";
2 | @import "./luxe.css";
3 |
4 | @custom-variant dark (&:is(.dark *));
5 |
6 | :root {
7 | --background: #f5f5f5;
8 | --main: #f3f3f3;
9 | --dotted: #b8b8b8;
10 | --primary: #000000;
11 | --foreground: #4a4c52;
12 | --shiki-theme: var(--shiki--light);
13 | --shiki--light: var(--shiki-theme,);
14 | --shiki--dark: var(--shiki-theme,);
15 | --scrollbar: #a7a7a7;
16 | --shiki-theme: var(--shiki--light);
17 | }
18 |
19 | .dark {
20 | --background: #0a0a0a;
21 | --main: #0c0c0c;
22 | --dotted: #292929;
23 | --primary: #ffffff;
24 | --foreground: #b5b3ad;
25 | --shiki-theme: var(--shiki--dark);
26 | --shiki--light: var(--shiki-theme,);
27 | --shiki--dark: var(--shiki-theme,);
28 | --scrollbar: #262626;
29 | --shiki-theme: var(--shiki--dark);
30 | }
31 |
32 | @theme inline {
33 | --color-background: var(--background);
34 | --color-background-muted: #0d0d0d;
35 | --color-main: var(--main);
36 | --color-secondary: #c2c2c2;
37 | --color-primary: var(--primary);
38 | --color-foreground: var(--foreground);
39 | --color-muted: #0a0a0a;
40 |
41 | --animate-spotlight: spotlight 2s ease 1 forwards;
42 |
43 | @keyframes spotlight {
44 | 0% {
45 | opacity: 0;
46 | transform: translate(-72%, -62%) scale(0.5);
47 | }
48 | 100% {
49 | opacity: 1;
50 | transform: translate(-50%, -40%) scale(1);
51 | }
52 | }
53 | }
54 |
55 | @layer base {
56 | img,
57 | button {
58 | @apply select-none;
59 | }
60 |
61 | ::selection {
62 | @apply bg-neutral-400/20;
63 | }
64 |
65 | .top-dotted {
66 | @apply [background-image:linear-gradient(90deg,var(--dotted)_25%,transparent_25%)] [background-repeat:repeat-x] [background-size:4px_1px];
67 | }
68 |
69 | .bottom-dotted {
70 | @apply [background-image:linear-gradient(90deg,var(--dotted)_25%,transparent_25%)] [background-repeat:repeat-x] [background-size:4px_1px] [background-position:bottom];
71 | }
72 |
73 | .right-dotted {
74 | @apply [background-image:linear-gradient(0deg,var(--dotted)_25%,transparent_25%)] [background-position:100%] [background-repeat:repeat-y] [background-size:1px_4px];
75 | }
76 | }
77 |
78 | * {
79 | font-variant-ligatures: none;
80 | -webkit-tap-highlight-color: transparent;
81 | scrollbar-width: thin;
82 | scrollbar-color: var(--scrollbar) transparent;
83 | }
84 |
85 | html {
86 | -webkit-text-size-adjust: 100%;
87 | -moz-tab-size: 4;
88 | -o-tab-size: 4;
89 | tab-size: 4;
90 | font-feature-settings: normal;
91 | font-variation-settings: normal;
92 | }
93 |
94 | .shiki {
95 | background-color: transparent !important;
96 | font-size: 14px;
97 | border: 1px solid transparent;
98 | font-family: var(--font-mono);
99 |
100 | &:focus-visible {
101 | border-color: var(--border);
102 | outline: none;
103 | }
104 |
105 | & span {
106 | color: var(--shiki--light, var(--_s-light))
107 | var(--shiki--dark, var(--_s-dark));
108 | }
109 | }
110 |
111 | button {
112 | cursor: pointer;
113 | }
114 |
115 | .text-gradient {
116 | background: linear-gradient(
117 | to right bottom,
118 | var(--primary) 30%,
119 | color-mix(in srgb, var(--primary) 50%, transparent)
120 | );
121 | -webkit-background-clip: text;
122 | -webkit-text-fill-color: transparent;
123 | background-clip: text;
124 | color: unset;
125 | }
126 |
127 | body {
128 | @apply bg-background;
129 | }
130 |
--------------------------------------------------------------------------------
/packages/react/src/styles/luxe.css:
--------------------------------------------------------------------------------
1 | @import "tailwindcss";
2 | @import "tw-animate-css";
3 |
4 | :root {
5 | --main: oklch(0.97 0 0);
6 | --main-secondary: oklch(97% 0 0);
7 | --main-foreground: oklch(0.925 0 0);
8 | --main-muted: oklch(0.96 0 0);
9 | --main-background: oklch(0.97 0 0);
10 | --main-invert: oklch(0.205 0 0);
11 |
12 | --primary: oklch(0 0 0);
13 | --primary-invert: oklch(1 0 0);
14 | --primary-foreground: oklch(37.1% 0 0);
15 | --primary-muted: oklch(0.556 0 0);
16 |
17 | --border: oklch(0.885 0 0);
18 | }
19 |
20 | .dark {
21 | --main: oklch(0.178 0 0);
22 | --main-secondary: oklch(0.205 0 0);
23 | --main-foreground: oklch(0.269 0 0);
24 | --main-muted: oklch(0.168 0 0);
25 | --main-background: oklch(0.145 0 0);
26 | --main-invert: oklch(0.8 0 0);
27 |
28 | --primary: oklch(1 0 0);
29 | --primary-invert: oklch(0 0 0);
30 | --primary-foreground: oklch(0.97 0 0);
31 | --primary-muted: oklch(0.708 0 0);
32 |
33 | --border: oklch(0.26 0 0);
34 | }
35 |
36 | @theme inline {
37 | --color-main: var(--main);
38 | --color-main-secondary: var(--main-secondary);
39 | --color-main-foreground: var(--main-foreground);
40 | --color-main-muted: var(--main-muted);
41 | --color-main-background: var(--main-background);
42 | --color-main-invert: var(--main-invert);
43 |
44 | --color-primary: var(--primary);
45 | --color-primary-foreground: var(--primary-foreground);
46 | --color-primary-muted: var(--primary-muted);
47 | --color-primary-invert: var(--primary-invert);
48 |
49 | --color-border: var(--border);
50 |
51 | --animate-shine: shine 6s linear infinite;
52 | --animate-text-gradient: text-gradient 2s linear infinite;
53 | --animate-text-shake: text-shake 1s ease 1;
54 | --animate-brightness: brightness 2.2s linear infinite;
55 | --animate-spinner: spinner 1.2s linear infinite;
56 | --animate-accordion-open: accordion-open 0.2s ease-out;
57 | --animate-accordion-close: accordion-close 0.2s ease-out;
58 |
59 | @keyframes accordion-open {
60 | from {
61 | height: 0;
62 | }
63 | to {
64 | height: var(--radix-accordion-content-height);
65 | }
66 | }
67 |
68 | @keyframes accordion-close {
69 | from {
70 | height: var(--radix-accordion-content-height);
71 | }
72 | to {
73 | height: 0;
74 | }
75 | }
76 |
77 | @keyframes shine {
78 | from {
79 | background-position: 0 0;
80 | }
81 | to {
82 | background-position: -400% 0;
83 | }
84 | }
85 |
86 | @keyframes text-gradient {
87 | to {
88 | background-position: 200% center;
89 | }
90 | }
91 |
92 | @keyframes text-shake {
93 | 15% {
94 | transform: translateX(5px);
95 | }
96 | 30% {
97 | transform: translateX(-5px);
98 | }
99 | 50% {
100 | transform: translateX(3px);
101 | }
102 | 80% {
103 | transform: translateX(2px);
104 | }
105 | 100% {
106 | transform: translateX(0);
107 | }
108 | }
109 |
110 | @keyframes brightness {
111 | 0% {
112 | transform: skew(-13deg) translateX(-100%);
113 | }
114 | 100% {
115 | transform: skew(-13deg) translateX(100%);
116 | }
117 | }
118 |
119 | @keyframes spinner {
120 | 0% {
121 | opacity: 1;
122 | }
123 | 100% {
124 | opacity: 0.15;
125 | }
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/packages/react/src/utils/cn.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from 'clsx'
2 | import { twMerge } from 'tailwind-merge'
3 |
4 | export const cn = (...inputs: ClassValue[]) => twMerge(clsx(...inputs))
5 |
--------------------------------------------------------------------------------
/packages/react/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@luxe/ts-config/react.json",
3 | "compilerOptions": {
4 | "baseUrl": ".",
5 | "paths": {
6 | "@/*": ["src/*"],
7 | "@/registry/*": ["src/components/*"]
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - packages/*
3 | - apps/*
4 | - config/*
5 |
--------------------------------------------------------------------------------
/turbo.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://turbo.build/schema.json",
3 | "tasks": {
4 | "build": {
5 | "dependsOn": ["^build"],
6 | "outputs": [".next/**", "!.next/cache/**", "dist/*"]
7 | },
8 | "dev": {
9 | "persistent": true,
10 | "cache": false
11 | },
12 | "lint": {
13 | "cache": true
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import path from 'node:path'
2 |
3 | import { defineConfig } from 'vite'
4 | import react from '@vitejs/plugin-react'
5 |
6 | import tailwindcss from '@tailwindcss/vite'
7 |
8 | export default defineConfig(() => {
9 | return {
10 | plugins: [react(), tailwindcss()],
11 | resolve: {
12 | alias: [
13 | {
14 | find: '@',
15 | replacement: path.resolve(__dirname, 'packages/react/src'),
16 | },
17 | {
18 | find: '@/registry',
19 | replacement: path.resolve(__dirname, 'packages/react/src/components'),
20 | },
21 | ],
22 | },
23 | }
24 | })
25 |
--------------------------------------------------------------------------------