├── .gitignore
├── .husky
└── pre-commit
├── .prettierignore
├── .prettierrc
├── LICENSE
├── README.md
├── app
├── globals.css
├── icon.svg
├── layout.tsx
└── page.tsx
├── components
├── sections
│ ├── footer.tsx
│ ├── header.tsx
│ ├── showcase.tsx
│ └── sticky-header.tsx
└── ui
│ ├── component-container.tsx
│ ├── family-style-otp.tsx
│ ├── image-preview.tsx
│ ├── index.ts
│ └── multi-step.tsx
├── eslint.config.mjs
├── hooks
├── use-measure.tsx
├── use-mounted.ts
└── use-sticky.ts
├── lib
└── utils.ts
├── next.config.ts
├── package-lock.json
├── package.json
├── postcss.config.mjs
├── public
├── dark-icon.svg
├── gyoza-shop.jpg
├── light-icon.svg
└── profile.png
└── tsconfig.json
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.*
7 | .yarn/*
8 | !.yarn/patches
9 | !.yarn/plugins
10 | !.yarn/releases
11 | !.yarn/versions
12 |
13 | # testing
14 | /coverage
15 |
16 | # next.js
17 | /.next/
18 | /out/
19 |
20 | # production
21 | /build
22 |
23 | # misc
24 | .DS_Store
25 | *.pem
26 |
27 | # debug
28 | npm-debug.log*
29 | yarn-debug.log*
30 | yarn-error.log*
31 | .pnpm-debug.log*
32 |
33 | # env files (can opt-in for committing if needed)
34 | .env*
35 |
36 | # vercel
37 | .vercel
38 |
39 | # typescript
40 | *.tsbuildinfo
41 | next-env.d.ts
42 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | npm run lint-staged
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | # Ignore artifacts:
2 | .next
3 | node_modules
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 100,
3 | "useTabs": false,
4 | "tabWidth": 2,
5 | "semi": true,
6 | "singleQuote": true,
7 | "plugins": ["prettier-plugin-sort-imports", "prettier-plugin-tailwindcss"]
8 | }
9 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Smintfy
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | # Snippet
9 |
10 | Collections of ui components built by smintfy. Feel free to play with them.
11 |
12 | ## Tech Stack
13 |
14 | Snippet is primarily built with Next.js, React, TypeScript, TailwindCSS, and Motion.
15 |
16 | This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
17 |
18 | ## Motivation
19 |
20 | Snippet is built as a playground for learning and experiment. By open sourcing the code, I hope other people can learn from it as well.
21 |
22 | ### Inspiration
23 |
24 | These are some of the people that has been inspiring me to improve my design engineering skills.
25 |
26 | - [Emil Kowalski](https://x.com/emilkowalski_)
27 | - [Mariana Castilho](https://x.com/mrncst)
28 | - [Jakub Krehel](https://x.com/jakubkrehel)
29 | - More soon...
30 |
31 | And here are some apps that inspire me.
32 |
33 | - [Family](https://family.co/)
34 | - More soon...
35 |
36 | ## Contributing
37 |
38 | Contributions are welcomed, If you find a bug or have a feature request, feel free to create an [issue](https://github.com/Smintfy/ui-snippet/issues) or submit a [PR](https://github.com/Smintfy/ui-snippet/pulls).
39 |
40 | ### Getting started with development
41 |
42 | You can fork and clone the repository to your local machine if you want to contribute or you can just clone the repository if you want to play around.
43 |
44 | ```bash
45 | # clone your forked repo
46 | git clone https://github.com/your-username/ui-snippet.git
47 |
48 | # or just clone it without fork
49 | git clone https://github.com/smintfy/ui-snippet.git
50 | ```
51 |
52 | Navigate to the project directory
53 |
54 | ```bash
55 | cd ui-snippet
56 | ```
57 |
58 | Install dependencies
59 |
60 | ```bash
61 | npm install
62 | ```
63 |
64 | Run the development server
65 |
66 | ```bash
67 | npm run dev
68 | ```
69 |
70 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
--------------------------------------------------------------------------------
/app/globals.css:
--------------------------------------------------------------------------------
1 | @import 'tailwindcss';
2 |
3 | /*
4 | The default border color has changed to `currentColor` in Tailwind CSS v4,
5 | so we've added these compatibility styles to make sure everything still
6 | looks the same as it did with Tailwind CSS v3.
7 |
8 | If we ever want to remove these styles, we need to add an explicit border
9 | color utility to any element that depends on these defaults.
10 | */
11 | @layer base {
12 | *,
13 | ::after,
14 | ::before,
15 | ::backdrop,
16 | ::file-selector-button {
17 | border-color: var(--color-border, currentColor);
18 | }
19 | }
20 |
21 | @theme inline {
22 | --color-primary: var(--primary);
23 | --color-secondary: var(--secondary);
24 | --color-tertiary: var(--tertiary);
25 | --color-border: var(--border);
26 | --color-accent: var(--accent);
27 | }
28 |
29 | :root {
30 | --background: oklch(1 0 0);
31 | --primary: oklch(0.209 0 0);
32 | --secondary: oklch(0.442 0 0);
33 | --tertiary: oklch(0.556 0 0);
34 | --border: oklch(0.922 0 0);
35 | --accent: oklch(0.976 0 0);
36 | }
37 |
38 | body {
39 | background: var(--background);
40 | color: var(--primary);
41 | font-family: Arial, Helvetica, sans-serif;
42 | }
43 |
44 | @keyframes fastPulse {
45 | 0%, 100% { opacity: 1 }
46 | 50% { opacity: 0.4 }
47 | }
48 |
49 | .fast-pulse {
50 | animation: fastPulse 0.6s ease-in-out infinite;
51 | }
--------------------------------------------------------------------------------
/app/icon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import { Inter } from 'next/font/google';
2 | import type { Metadata } from 'next';
3 | import { cn } from '@/lib/utils';
4 | import './globals.css';
5 |
6 | const inter = Inter({
7 | variable: '--font-inter',
8 | subsets: ['latin'],
9 | });
10 |
11 | export const metadata: Metadata = {
12 | title: 'uisnippet',
13 | description: 'Collections of ui components made by smintfy.',
14 | };
15 |
16 | export default function RootLayout({
17 | children,
18 | }: Readonly<{
19 | children: React.ReactNode;
20 | }>) {
21 | return (
22 |
23 | {children}
24 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/app/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import Showcase from '@/components/sections/showcase';
4 | import Header from '@/components/sections/header';
5 | import Footer from '@/components/sections/footer';
6 | import { cn } from '@/lib/utils';
7 | import { Toaster } from 'sonner';
8 |
9 | export default function Home() {
10 | return (
11 |
16 |
17 |
18 |
19 |
20 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/components/sections/footer.tsx:
--------------------------------------------------------------------------------
1 | import Image from 'next/image';
2 |
3 | export default function Footer() {
4 | return (
5 |
46 | );
47 | }
48 |
--------------------------------------------------------------------------------
/components/sections/header.tsx:
--------------------------------------------------------------------------------
1 | import StickyHeader from './sticky-header';
2 |
3 | export default function Header() {
4 | return (
5 | <>
6 |
7 |
8 | Collections of ui components built by smintfy.
9 |
10 | >
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/components/sections/showcase.tsx:
--------------------------------------------------------------------------------
1 | import { ComponentContainer, FamilyStyleOTP, ImagePreview, MultiStep } from '@/components/ui';
2 |
3 | interface ComponentContainerProps {
4 | name: string;
5 | description: React.JSX.Element;
6 | tags: string[];
7 | source: string;
8 | children: React.ReactNode;
9 | }
10 |
11 | const components: ComponentContainerProps[] = [
12 | {
13 | name: 'Image preview',
14 | description: <>Interaction built using shared layout animations and Radix dialog primitive.>,
15 | tags: ['react', 'radix', 'motion', 'tailwind'],
16 | source: 'https://github.com/Smintfy/ui-snippet/blob/main/components/ui/image-preview.tsx',
17 | children: ,
18 | },
19 | {
20 | name: 'Multi-step flow',
21 | description: (
22 | <>This is a multi-step component guiding users through a process or for an onboarding.>
23 | ),
24 | tags: ['react', 'motion', 'tailwind'],
25 | source: 'https://github.com/Smintfy/ui-snippet/blob/main/components/ui/multi-step.tsx',
26 | children: ,
27 | },
28 | {
29 | name: 'Family style OTP',
30 | description: (
31 | <>
32 | Recreating OTP component from{' '}
33 |
38 | Family
39 | {' '}
40 | built on top of{' '}
41 |
42 | otp-input
43 | {' '}
44 | by guilhermerodz. And by the way, the correct code is {'"'}123456{'"'}.
45 | >
46 | ),
47 | tags: ['react', 'motion', 'tailwind'],
48 | source: 'https://github.com/Smintfy/ui-snippet/blob/main/components/ui/family-style-otp.tsx',
49 | children: ,
50 | },
51 | ];
52 |
53 | export default function Showcase() {
54 | return (
55 |
56 | {components.map((component) => (
57 |
64 | {component.children}
65 |
66 | ))}
67 |
68 | );
69 | }
70 |
--------------------------------------------------------------------------------
/components/sections/sticky-header.tsx:
--------------------------------------------------------------------------------
1 | // Credit to https://github.com/marcbouchenoire for the sticky header implementation
2 |
3 | 'use client';
4 |
5 | import { useIsMounted } from '@/hooks/use-mounted';
6 | import { useIsSticky } from '@/hooks/use-sticky';
7 | import { cn } from '@/lib/utils';
8 | import { useRef } from 'react';
9 |
10 | export default function StickyHeader() {
11 | const stickyRef = useRef(null!);
12 | const isSticky = useIsSticky(stickyRef);
13 | const isMounted = useIsMounted();
14 |
15 | return (
16 | <>
17 |
32 |
72 | >
73 | );
74 | }
75 |
--------------------------------------------------------------------------------
/components/ui/component-container.tsx:
--------------------------------------------------------------------------------
1 | interface ComponentContainerProps {
2 | name: string;
3 | description: React.JSX.Element;
4 | tags: string[];
5 | source: string;
6 | children: React.ReactNode;
7 | }
8 |
9 | export default function ComponentContainer({
10 | name,
11 | description,
12 | tags,
13 | source,
14 | children,
15 | }: ComponentContainerProps) {
16 | return (
17 |
18 |
27 |
28 | {children}
29 |
30 |
31 | {tags.map((tag) => (
32 |
36 | {tag}
37 |
38 | ))}
39 |
40 |
41 | );
42 | }
43 |
--------------------------------------------------------------------------------
/components/ui/family-style-otp.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { motion, AnimatePresence } from 'motion/react';
4 | import { useEffect, useRef, useState } from 'react';
5 | import { OTPInput, SlotProps } from 'input-otp';
6 | import { cn } from '@/lib/utils';
7 | import { toast } from 'sonner';
8 |
9 | interface AnimatedNumberProps {
10 | value: string | null;
11 | placeholder: string;
12 | }
13 |
14 | function Separator() {
15 | return ;
16 | }
17 |
18 | function AnimatedNumber({ value, placeholder }: AnimatedNumberProps) {
19 | return (
20 |
21 |
22 |
30 | {value ?? placeholder}
31 |
32 |
33 |
34 | );
35 | }
36 |
37 | function Slot(props: SlotProps & { isShaking?: boolean; isVerifying: boolean; delay: number; }) {
38 | const placeholderChar = '0';
39 |
40 | return (
41 |
51 |
52 | {props.isActive ? (
53 |
62 | ) : null}
63 |
64 | );
65 | }
66 |
67 | export default function FamilyStyleOTP() {
68 | const CORRECT_OTP = '123456';
69 | const [value, setValue] = useState('');
70 | const [disableSubmitButton, setDisableSubmitButton] = useState(true);
71 | const [isVerifying, setIsVerifying] = useState(false);
72 | const [isShaking, setIsShaking] = useState(false);
73 | const [errorMessage, setErrorMessage] = useState('');
74 |
75 | const otpRef = useRef(null);
76 |
77 | useEffect(() => {
78 | setDisableSubmitButton(value.length !== 6);
79 | }, [value]);
80 |
81 | const handleSubmit = () => {
82 | // Fix for an issue where user can briefly click submit again after submitting,
83 | // prevent submission if one is already in progress.
84 | if (isVerifying) return;
85 |
86 | setIsVerifying(true);
87 | setDisableSubmitButton(true);
88 | setErrorMessage('');
89 |
90 | setTimeout(() => {
91 | if (value === CORRECT_OTP) {
92 | toast.message('Successfully verified', {
93 | description: 'Your OTP has been verified.',
94 | });
95 | } else {
96 | setIsShaking(true);
97 | setErrorMessage('Invalid validation code');
98 | }
99 |
100 | setValue('');
101 | setIsVerifying(false);
102 |
103 | // Focus back to the first slot after submitting.
104 | if (otpRef.current) {
105 | otpRef.current.focus();
106 | otpRef.current.setSelectionRange(0, 0);
107 | }
108 | }, 2000);
109 | };
110 |
111 | return (
112 |
113 |
114 |
Verification Code
115 |
We{"'"}ve sent you a verification code.
116 |
117 |
setIsShaking(false)}
121 | >
122 | {
128 | // Error if value doesn't contain number
129 | if (!/^\d*$/.test(newValue)) {
130 | setIsShaking(true);
131 | return;
132 | }
133 | setValue(newValue);
134 | if (errorMessage) setErrorMessage('');
135 | }}
136 | onKeyDown={(e) => {
137 | if (e.key === 'Enter') {
138 | e.preventDefault();
139 | if (value.length < 6) return;
140 | handleSubmit();
141 | }
142 | }}
143 | render={({ slots }) => (
144 | <>
145 |
146 | {slots.slice(0, 3).map((slot, idx) => (
147 |
154 | ))}
155 |
156 |
157 |
158 | {slots.slice(3).map((slot, idx) => (
159 |
166 | ))}
167 |
168 | >
169 | )}
170 | />
171 |
172 |
173 | Didn{"'"}t receive a code?{' '}
174 |
184 |
185 |
186 |
187 | {errorMessage && (
188 |
196 | {errorMessage}
197 |
198 | )}
199 |
200 |
201 |
252 |
253 | );
254 | }
255 |
--------------------------------------------------------------------------------
/components/ui/image-preview.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { motion, AnimatePresence, MotionConfig } from 'framer-motion';
4 | import { VisuallyHidden } from '@radix-ui/react-visually-hidden';
5 | import { Dialog } from 'radix-ui';
6 | import { X } from 'lucide-react';
7 | import { useState } from 'react';
8 | import Image from 'next/image';
9 |
10 | const MotionImage = motion.create(Image);
11 |
12 | export default function ImagePreview() {
13 | const [isOpen, setIsOpen] = useState(false);
14 |
15 | return (
16 |
17 |
20 |
21 |
22 |
27 |
36 |
37 |
38 |
39 |
40 | {isOpen && (
41 | <>
42 |
43 |
49 |
50 |
51 |
52 |
53 | Image Preview
54 |
55 | Interaction built using shared layout animations and Radix dialog
56 | primitive.
57 |
58 |
59 |
63 |
71 |
72 |
80 |
81 |
82 |
83 |
84 | >
85 | )}
86 |
87 |
88 |
89 |
90 |
91 | );
92 | }
93 |
--------------------------------------------------------------------------------
/components/ui/index.ts:
--------------------------------------------------------------------------------
1 | export { default as ComponentContainer } from './component-container';
2 | export { default as FamilyStyleOTP } from './family-style-otp';
3 | export { default as ImagePreview } from './image-preview';
4 | export { default as MultiStep } from './multi-step';
5 |
--------------------------------------------------------------------------------
/components/ui/multi-step.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { motion, MotionConfig, AnimatePresence } from 'motion/react';
4 | import { useMemo, useRef, useState } from 'react';
5 | import useMeasure from '@/hooks/use-measure';
6 | import { cn } from '@/lib/utils';
7 | import { toast } from 'sonner';
8 |
9 | const PrimaryButton: React.FC> = ({
10 | children,
11 | className,
12 | ...props
13 | }) => {
14 | return (
15 |
26 | );
27 | };
28 |
29 | const SecondaryButton: React.FC> = ({
30 | children,
31 | className,
32 | ...props
33 | }) => {
34 | return (
35 |
45 | );
46 | };
47 |
48 | const StepOne = () => {
49 | return (
50 | <>
51 |
52 |
This is step one
53 |
54 | This is a multi-step component. It{"'"}s a great way to guide users through a process.
55 |
56 |
57 |
63 | >
64 | );
65 | };
66 |
67 | const StepTwo = () => {
68 | return (
69 | <>
70 |
71 |
This is step two
72 |
73 | This is a multi-step component. It{"'"}s a great way to guide users through a process.
74 |
75 |
76 |
81 | >
82 | );
83 | };
84 |
85 | const StepThree = () => {
86 | return (
87 | <>
88 |
89 |
This is step three
90 |
91 | This is a multi-step component. It{"'"}s a great way to guide users through a process.
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 | >
102 | );
103 | };
104 |
105 | export default function MultiStep() {
106 | const [currentStep, setCurrentStep] = useState(0);
107 | const [clickDirection, setClickDirection] = useState(1);
108 | const ref = useRef(null);
109 | const { height } = useMeasure({ ref });
110 |
111 | const steps = useMemo(() => {
112 | switch (currentStep) {
113 | case 0:
114 | return ;
115 | case 1:
116 | return ;
117 | case 2:
118 | return ;
119 | }
120 | }, [currentStep]);
121 |
122 | const variants = {
123 | initial: (custom: number) => ({
124 | x: `${100 * custom}%`,
125 | filter: 'blur(2.5px)',
126 | opacity: 0,
127 | }),
128 | animate: () => ({
129 | x: 0,
130 | filter: 'blur(0)',
131 | opacity: 1,
132 | }),
133 | exit: (custom: number) => ({
134 | x: `${-100 * custom}%`,
135 | filter: 'blur(2.5px)',
136 | opacity: 0,
137 | }),
138 | };
139 |
140 | return (
141 |
142 |
143 |
150 |
151 |
152 |
156 |
157 |
166 | {steps}
167 |
168 |
169 |
170 |
171 | {
176 | if (currentStep === 0) {
177 | return;
178 | }
179 | setCurrentStep((prev) => prev - 1);
180 | setClickDirection(-1);
181 | }}
182 | >
183 | Back
184 |
185 | {
188 | if (currentStep === 2) {
189 | toast.message('Onboarding completed', {
190 | description: 'Now you know what to do.',
191 | });
192 | setCurrentStep(0);
193 | return;
194 | }
195 | setCurrentStep((prev) => prev + 1);
196 | setClickDirection(1);
197 | }}
198 | >
199 | {currentStep === 2 ? 'Complete' : 'Continue'}
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 | );
208 | }
209 |
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import { FlatCompat } from '@eslint/eslintrc';
2 | import { fileURLToPath } from 'url';
3 | import { dirname } from 'path';
4 |
5 | const __filename = fileURLToPath(import.meta.url);
6 | const __dirname = dirname(__filename);
7 |
8 | const compat = new FlatCompat({
9 | baseDirectory: __dirname,
10 | });
11 |
12 | const eslintConfig = [...compat.extends('next/core-web-vitals', 'next/typescript')];
13 |
14 | export default eslintConfig;
15 |
--------------------------------------------------------------------------------
/hooks/use-measure.tsx:
--------------------------------------------------------------------------------
1 | import { RefObject, useEffect, useState } from 'react';
2 |
3 | const useMeasure = ({ ref }: { ref: RefObject }) => {
4 | const [height, setHeight] = useState(0);
5 |
6 | useEffect(() => {
7 | const observer = new ResizeObserver((entries) => {
8 | for (const entry of entries) {
9 | const rect = entry.target.getBoundingClientRect();
10 |
11 | setHeight(rect.height);
12 | }
13 | });
14 |
15 | if (ref.current) {
16 | observer.observe(ref.current);
17 | }
18 |
19 | return () => observer.disconnect();
20 | }, [ref]);
21 |
22 | return { height };
23 | };
24 |
25 | export default useMeasure;
26 |
--------------------------------------------------------------------------------
/hooks/use-mounted.ts:
--------------------------------------------------------------------------------
1 | import { useSyncExternalStore } from 'react';
2 |
3 | const subscribe = () => () => {};
4 | const getSnapshot = () => true;
5 | const getServerSnapshot = () => false;
6 |
7 | export function useIsMounted() {
8 | const isMounted = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
9 |
10 | return isMounted;
11 | }
12 |
--------------------------------------------------------------------------------
/hooks/use-sticky.ts:
--------------------------------------------------------------------------------
1 | import { type RefObject, useEffect, useState } from 'react';
2 |
3 | export function useIsSticky(ref: RefObject) {
4 | const [isSticky, setIsSticky] = useState(false);
5 |
6 | useEffect(() => {
7 | const current = ref.current;
8 | if (!current) return;
9 |
10 | const observer = new IntersectionObserver(
11 | ([entry]) => setIsSticky((entry?.intersectionRatio ?? 1) < 1),
12 | {
13 | threshold: [1],
14 | },
15 | );
16 |
17 | observer.observe(current as T);
18 |
19 | return () => {
20 | observer.unobserve(current as T);
21 | };
22 | }, []); // eslint-disable-line react-hooks/exhaustive-deps
23 |
24 | return isSticky;
25 | }
26 |
--------------------------------------------------------------------------------
/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { twMerge } from 'tailwind-merge';
2 | import { ClassValue, clsx } from 'clsx';
3 |
4 | export const cn = (...inputs: ClassValue[]) => {
5 | return twMerge(clsx(inputs));
6 | };
7 |
--------------------------------------------------------------------------------
/next.config.ts:
--------------------------------------------------------------------------------
1 | import type { NextConfig } from 'next';
2 |
3 | const nextConfig: NextConfig = {
4 | /* config options here */
5 | };
6 |
7 | export default nextConfig;
8 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ui-snippet",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint",
10 | "format": "prettier --write \"**/*.{ts,tsx}\"",
11 | "lint-staged": "prettier --write --ignore-unknown",
12 | "prepare": "npx husky install"
13 | },
14 | "dependencies": {
15 | "@number-flow/react": "^0.5.8",
16 | "@radix-ui/react-visually-hidden": "^1.1.2",
17 | "clsx": "^2.1.1",
18 | "input-otp": "^1.4.2",
19 | "lucide-react": "^0.479.0",
20 | "motion": "^12.4.10",
21 | "next": "^15.2.4",
22 | "radix-ui": "^1.1.3",
23 | "react": "^19.0.0",
24 | "react-dom": "^19.0.0",
25 | "sonner": "^2.0.2",
26 | "tailwind-merge": "^3.0.2"
27 | },
28 | "devDependencies": {
29 | "@eslint/eslintrc": "^3",
30 | "@tailwindcss/postcss": "^4",
31 | "@types/node": "^20",
32 | "@types/react": "^19",
33 | "@types/react-dom": "^19",
34 | "eslint": "^9",
35 | "eslint-config-next": "15.2.1",
36 | "lint-staged": "^15.5.0",
37 | "prettier": "^3.5.3",
38 | "prettier-plugin-sort-imports": "^1.8.6",
39 | "prettier-plugin-tailwindcss": "^0.6.11",
40 | "tailwindcss": "^4",
41 | "typescript": "^5"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | const config = {
2 | plugins: ['@tailwindcss/postcss'],
3 | };
4 |
5 | export default config;
6 |
--------------------------------------------------------------------------------
/public/dark-icon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/gyoza-shop.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Smintfy/ui-snippet/eb6a867cebe3a7ae9fc490f4c49b1e6722d248ba/public/gyoza-shop.jpg
--------------------------------------------------------------------------------
/public/light-icon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/profile.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Smintfy/ui-snippet/eb6a867cebe3a7ae9fc490f4c49b1e6722d248ba/public/profile.png
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2017",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "noEmit": true,
9 | "esModuleInterop": true,
10 | "module": "esnext",
11 | "moduleResolution": "bundler",
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "jsx": "preserve",
15 | "incremental": true,
16 | "plugins": [
17 | {
18 | "name": "next"
19 | }
20 | ],
21 | "paths": {
22 | "@/*": ["./*"]
23 | }
24 | },
25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26 | "exclude": ["node_modules"]
27 | }
28 |
--------------------------------------------------------------------------------