initial
This commit is contained in:
parent
50c873678e
commit
40e1d6ca21
6
.gitignore
vendored
6
.gitignore
vendored
@ -39,3 +39,9 @@ yarn-error.log*
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
*storybook.log
|
||||
storybook-static
|
||||
|
||||
.vscode
|
||||
tailwind.config.js
|
||||
22
.storybook/main.ts
Normal file
22
.storybook/main.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import type { StorybookConfig } from "@storybook/nextjs-vite";
|
||||
|
||||
const config: StorybookConfig = {
|
||||
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
|
||||
addons: [
|
||||
"@chromatic-com/storybook",
|
||||
"@storybook/addon-docs",
|
||||
"@storybook/addon-onboarding",
|
||||
"@storybook/addon-a11y",
|
||||
"@storybook/addon-vitest",
|
||||
"@storybook/addon-styling-webpack"
|
||||
],
|
||||
framework: {
|
||||
name: "@storybook/nextjs-vite",
|
||||
options: {},
|
||||
},
|
||||
staticDirs: ["../public"],
|
||||
core: {
|
||||
disableTelemetry: true, // 👈 Disables telemetry
|
||||
},
|
||||
};
|
||||
export default config;
|
||||
64
.storybook/preview.tsx
Normal file
64
.storybook/preview.tsx
Normal file
@ -0,0 +1,64 @@
|
||||
import type { Preview } from "@storybook/nextjs-vite";
|
||||
import { Geist, Geist_Mono, Inter, Manrope } from "next/font/google";
|
||||
import "../src/app/globals.css";
|
||||
import React from "react";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const manrope = Manrope({
|
||||
variable: "--font-manrope",
|
||||
subsets: ["latin", "cyrillic"],
|
||||
weight: ["200", "300", "400", "500", "600", "700", "800"],
|
||||
});
|
||||
|
||||
const inter = Inter({
|
||||
variable: "--font-inter",
|
||||
subsets: ["latin", "cyrillic"],
|
||||
weight: ["100", "200", "300", "400", "500", "600", "700", "800", "900"],
|
||||
});
|
||||
|
||||
const preview: Preview = {
|
||||
parameters: {
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/i,
|
||||
},
|
||||
},
|
||||
|
||||
a11y: {
|
||||
// 'todo' - show a11y violations in the test UI only
|
||||
// 'error' - fail CI on a11y violations
|
||||
// 'off' - skip a11y checks entirely
|
||||
test: "todo",
|
||||
},
|
||||
|
||||
layout: "padded",
|
||||
|
||||
backgrounds: {
|
||||
options: {
|
||||
light: { name: "Light", value: "#fff" },
|
||||
dark: { name: "Dark", value: "#333" },
|
||||
},
|
||||
},
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div
|
||||
className={`${geistSans.variable} ${geistMono.variable} ${manrope.variable} ${inter.variable} flex items-center justify-center size-full max-w-[560px] min-w-xs mx-auto antialiased`}
|
||||
>
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default preview;
|
||||
7
.storybook/vitest.setup.ts
Normal file
7
.storybook/vitest.setup.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import * as a11yAddonAnnotations from "@storybook/addon-a11y/preview";
|
||||
import { setProjectAnnotations } from '@storybook/nextjs-vite';
|
||||
import * as projectAnnotations from './preview';
|
||||
|
||||
// This is an important step to apply the right configuration when testing your stories.
|
||||
// More info at: https://storybook.js.org/docs/api/portable-stories/portable-stories-vitest#setprojectannotations
|
||||
setProjectAnnotations([a11yAddonAnnotations, projectAnnotations]);
|
||||
22
components.json
Normal file
22
components.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/app/globals.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"iconLibrary": "lucide",
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"registries": {}
|
||||
}
|
||||
@ -1,3 +1,6 @@
|
||||
// For more info, see https://github.com/storybookjs/eslint-plugin-storybook#configuration-flat-config-format
|
||||
import storybook from "eslint-plugin-storybook";
|
||||
|
||||
import { dirname } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { FlatCompat } from "@eslint/eslintrc";
|
||||
@ -9,17 +12,14 @@ const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
});
|
||||
|
||||
const eslintConfig = [
|
||||
...compat.extends("next/core-web-vitals", "next/typescript"),
|
||||
{
|
||||
ignores: [
|
||||
"node_modules/**",
|
||||
".next/**",
|
||||
"out/**",
|
||||
"build/**",
|
||||
"next-env.d.ts",
|
||||
],
|
||||
},
|
||||
];
|
||||
const eslintConfig = [...compat.extends("next/core-web-vitals", "next/typescript"), {
|
||||
ignores: [
|
||||
"node_modules/**",
|
||||
".next/**",
|
||||
"out/**",
|
||||
"build/**",
|
||||
"next-env.d.ts",
|
||||
],
|
||||
}, ...storybook.configs["flat/recommended"]];
|
||||
|
||||
export default eslintConfig;
|
||||
|
||||
5708
package-lock.json
generated
5708
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
35
package.json
35
package.json
@ -6,20 +6,47 @@
|
||||
"dev": "next dev --turbopack",
|
||||
"build": "next build --turbopack",
|
||||
"start": "next start",
|
||||
"lint": "eslint"
|
||||
"lint": "eslint",
|
||||
"storybook": "storybook dev -p 6006 --ci",
|
||||
"build-storybook": "storybook build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-progress": "^1.1.7",
|
||||
"@radix-ui/react-separator": "^1.1.7",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.544.0",
|
||||
"next": "15.5.3",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"next": "15.5.3"
|
||||
"tailwind-merge": "^3.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5",
|
||||
"@chromatic-com/storybook": "^4.1.1",
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@storybook/addon-a11y": "^9.1.6",
|
||||
"@storybook/addon-docs": "^9.1.6",
|
||||
"@storybook/addon-onboarding": "^9.1.6",
|
||||
"@storybook/addon-styling-webpack": "^2.0.0",
|
||||
"@storybook/addon-vitest": "^9.1.6",
|
||||
"@storybook/nextjs-vite": "^9.1.6",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"@vitest/browser": "^3.2.4",
|
||||
"@vitest/coverage-v8": "^3.2.4",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.5.3",
|
||||
"@eslint/eslintrc": "^3"
|
||||
"eslint-plugin-storybook": "^9.1.6",
|
||||
"playwright": "^1.55.0",
|
||||
"storybook": "^9.1.6",
|
||||
"tailwindcss": "^4",
|
||||
"tw-animate-css": "^1.3.8",
|
||||
"typescript": "^5",
|
||||
"vitest": "^3.2.4"
|
||||
}
|
||||
}
|
||||
|
||||
5
postcss.config.mjs
Normal file
5
postcss.config.mjs
Normal file
@ -0,0 +1,5 @@
|
||||
const config = {
|
||||
plugins: { "@tailwindcss/postcss": {} },
|
||||
};
|
||||
|
||||
export default config;
|
||||
@ -1,42 +1,164 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
--font-manrope: var(--font-manrope);
|
||||
--font-inter: var(--font-inter);
|
||||
--font-geist-sans: var(--font-geist-sans);
|
||||
--font-geist-mono: var(--font-geist-mono);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-ring: var(--ring);
|
||||
--color-input: var(--input);
|
||||
--color-border: var(--border);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-card: var(--card);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
|
||||
/* Тени */
|
||||
--shadow-blue-glow: 0px 5px 14px 0px rgba(59, 130, 246, 0.4),
|
||||
0px 4px 6px 0px rgba(59, 130, 246, 0.1);
|
||||
--shadow-blue-glow-2: 0px 0px 19px 0px rgba(59, 130, 246, 0.3),
|
||||
0px 0px 0px 0px rgba(59, 130, 246, 0.2);
|
||||
--shadow-black-glow: 0px 8px 15px 0px #00000026, 0px 4px 6px 0px #00000014;
|
||||
--shadow-coupon: 0px 20px 40px 0px #0000004d, 0px 8px 16px 0px #00000033;
|
||||
}
|
||||
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
--radius: 0.625rem;
|
||||
|
||||
/* === ПЕРЕОПРЕДЕЛЯЕМ SHADCN ПЕРЕМЕННЫЕ ПОД НАШИ НУЖДЫ === */
|
||||
|
||||
/* Основные цвета */
|
||||
--background: oklch(1 0 0); /* Белый фон */
|
||||
--foreground: oklch(0 0 0); /* Черный текст */
|
||||
--card: oklch(1 0 0); /* Белый фон карточек */
|
||||
--card-foreground: oklch(
|
||||
0.2781 0.0296 256.85
|
||||
); /* #1f2937 - Темно-синий текст карточек */
|
||||
--popover: oklch(1 0 0); /* Белый фон попапов */
|
||||
--popover-foreground: oklch(0 0 0); /* Черный текст попапов */
|
||||
|
||||
/* Primary - наш синий */
|
||||
--primary: oklch(0.6231 0.188 259.81); /* #3B82F6 */
|
||||
--primary-foreground: oklch(1 0 0); /* Белый текст на синем */
|
||||
|
||||
/* Secondary - светло-серый */
|
||||
--secondary: oklch(0.97 0 0); /* Светло-серый фон */
|
||||
--secondary-foreground: oklch(0.2795 0.0368 260.03); /* Темно-серый текст */
|
||||
|
||||
/* Muted - для второстепенного контента */
|
||||
--muted: oklch(0.97 0 0); /* Светло-серый фон */
|
||||
--muted-foreground: oklch(0.59 0.02 260.8); /* #64748B - серый текст */
|
||||
|
||||
/* Accent - для акцентов */
|
||||
--accent: oklch(0.97 0 0); /* Светло-серый фон */
|
||||
--accent-foreground: oklch(0.2795 0.0368 260.03); /* Темно-серый текст */
|
||||
|
||||
/* Destructive - красный для ошибок */
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
|
||||
/* Success - зеленый для успешных действий */
|
||||
--success: oklch(0.36 0.245 27.325);
|
||||
|
||||
/* Border и Input */
|
||||
--border: oklch(0.9288 0.0126 255.51); /* Светло-серая граница */
|
||||
--input: oklch(0.922 0 0); /* Светло-серый фон инпутов */
|
||||
--ring: oklch(0.6231 0.188 259.81); /* Синий фокус */
|
||||
|
||||
/* Chart цвета - можно оставить как есть или переопределить */
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
|
||||
/* Sidebar - можно оставить как есть */
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
|
||||
/* === ДОПОЛНИТЕЛЬНЫЕ ПЕРЕМЕННЫЕ ТОЛЬКО ДЛЯ ГРАДИЕНТОВ === */
|
||||
--primary-light: oklch(0.954 0.025 259.8); /* #EBF5FF - для градиента */
|
||||
--primary-lighter: oklch(0.909 0.045 259.8); /* #DBEAFE - для градиента */
|
||||
--primary-dark: oklch(0.5461 0.2152 262.88); /* #2563EB - для градиента */
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #ededed;
|
||||
}
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
max-width: 100vw;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
color: var(--foreground);
|
||||
background: var(--background);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
color-scheme: dark;
|
||||
.dark {
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.205 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.205 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.922 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.269 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.556 0 0);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.556 0 0);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import { Geist, Geist_Mono, Inter, Manrope } from "next/font/google";
|
||||
import "./globals.css";
|
||||
|
||||
const geistSans = Geist({
|
||||
@ -12,6 +12,18 @@ const geistMono = Geist_Mono({
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const manrope = Manrope({
|
||||
variable: "--font-manrope",
|
||||
subsets: ["latin", "cyrillic"],
|
||||
weight: ["200", "300", "400", "500", "600", "700", "800"],
|
||||
});
|
||||
|
||||
const inter = Inter({
|
||||
variable: "--font-inter",
|
||||
subsets: ["latin"],
|
||||
weight: ["100", "200", "300", "400", "500", "600", "700", "800", "900"],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
@ -24,7 +36,9 @@ export default function RootLayout({
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={`${geistSans.variable} ${geistMono.variable}`}>
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} ${manrope.variable} ${inter.variable} antialiased`}
|
||||
>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -1,167 +0,0 @@
|
||||
.page {
|
||||
--gray-rgb: 0, 0, 0;
|
||||
--gray-alpha-200: rgba(var(--gray-rgb), 0.08);
|
||||
--gray-alpha-100: rgba(var(--gray-rgb), 0.05);
|
||||
|
||||
--button-primary-hover: #383838;
|
||||
--button-secondary-hover: #f2f2f2;
|
||||
|
||||
display: grid;
|
||||
grid-template-rows: 20px 1fr 20px;
|
||||
align-items: center;
|
||||
justify-items: center;
|
||||
min-height: 100svh;
|
||||
padding: 80px;
|
||||
gap: 64px;
|
||||
font-family: var(--font-geist-sans);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.page {
|
||||
--gray-rgb: 255, 255, 255;
|
||||
--gray-alpha-200: rgba(var(--gray-rgb), 0.145);
|
||||
--gray-alpha-100: rgba(var(--gray-rgb), 0.06);
|
||||
|
||||
--button-primary-hover: #ccc;
|
||||
--button-secondary-hover: #1a1a1a;
|
||||
}
|
||||
}
|
||||
|
||||
.main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32px;
|
||||
grid-row-start: 2;
|
||||
}
|
||||
|
||||
.main ol {
|
||||
font-family: var(--font-geist-mono);
|
||||
padding-left: 0;
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
letter-spacing: -0.01em;
|
||||
list-style-position: inside;
|
||||
}
|
||||
|
||||
.main li:not(:last-of-type) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.main code {
|
||||
font-family: inherit;
|
||||
background: var(--gray-alpha-100);
|
||||
padding: 2px 4px;
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ctas {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.ctas a {
|
||||
appearance: none;
|
||||
border-radius: 128px;
|
||||
height: 48px;
|
||||
padding: 0 20px;
|
||||
border: 1px solid transparent;
|
||||
transition:
|
||||
background 0.2s,
|
||||
color 0.2s,
|
||||
border-color 0.2s;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
a.primary {
|
||||
background: var(--foreground);
|
||||
color: var(--background);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
a.secondary {
|
||||
border-color: var(--gray-alpha-200);
|
||||
min-width: 158px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
grid-row-start: 3;
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.footer a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.footer img {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Enable hover only on non-touch devices */
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
a.primary:hover {
|
||||
background: var(--button-primary-hover);
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
a.secondary:hover {
|
||||
background: var(--button-secondary-hover);
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.footer a:hover {
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.page {
|
||||
padding: 32px;
|
||||
padding-bottom: 80px;
|
||||
}
|
||||
|
||||
.main {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.main ol {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ctas {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.ctas a {
|
||||
font-size: 14px;
|
||||
height: 40px;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
a.secondary {
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
.footer {
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.logo {
|
||||
filter: invert();
|
||||
}
|
||||
}
|
||||
@ -1,34 +1,39 @@
|
||||
import Image from "next/image";
|
||||
import styles from "./page.module.css";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className={styles.page}>
|
||||
<main className={styles.main}>
|
||||
<div className="font-sans grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20">
|
||||
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
|
||||
<Image
|
||||
className={styles.logo}
|
||||
className="dark:invert"
|
||||
src="/next.svg"
|
||||
alt="Next.js logo"
|
||||
width={180}
|
||||
height={38}
|
||||
priority
|
||||
/>
|
||||
<ol>
|
||||
<li>
|
||||
Get started by editing <code>src/app/page.tsx</code>.
|
||||
<ol className="font-mono list-inside list-decimal text-sm/6 text-center sm:text-left">
|
||||
<li className="mb-2 tracking-[-.01em]">
|
||||
Get started by editing{" "}
|
||||
<code className="bg-black/[.05] dark:bg-white/[.06] font-mono font-semibold px-1 py-0.5 rounded">
|
||||
src/app/page.tsx
|
||||
</code>
|
||||
.
|
||||
</li>
|
||||
<li className="tracking-[-.01em]">
|
||||
Save and see your changes instantly.
|
||||
</li>
|
||||
<li>Save and see your changes instantly.</li>
|
||||
</ol>
|
||||
|
||||
<div className={styles.ctas}>
|
||||
<div className="flex gap-4 items-center flex-col sm:flex-row">
|
||||
<a
|
||||
className={styles.primary}
|
||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
||||
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
|
||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
className={styles.logo}
|
||||
className="dark:invert"
|
||||
src="/vercel.svg"
|
||||
alt="Vercel logomark"
|
||||
width={20}
|
||||
@ -37,18 +42,19 @@ export default function Home() {
|
||||
Deploy now
|
||||
</a>
|
||||
<a
|
||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
||||
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]"
|
||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={styles.secondary}
|
||||
>
|
||||
Read our docs
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
<footer className={styles.footer}>
|
||||
<footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
|
||||
<a
|
||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
@ -62,7 +68,8 @@ export default function Home() {
|
||||
Learn
|
||||
</a>
|
||||
<a
|
||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
@ -76,7 +83,8 @@ export default function Home() {
|
||||
Examples
|
||||
</a>
|
||||
<a
|
||||
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
|
||||
24
src/components/layout/Header/Header.stories.tsx
Normal file
24
src/components/layout/Header/Header.stories.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import { Meta, StoryObj } from "@storybook/nextjs-vite";
|
||||
import { Header } from "./Header";
|
||||
import { fn } from "storybook/test";
|
||||
|
||||
/** Reusable Header Component */
|
||||
const meta: Meta<typeof Header> = {
|
||||
title: "Layout/Header",
|
||||
component: Header,
|
||||
tags: ["autodocs"],
|
||||
args: {
|
||||
progressProps: {
|
||||
value: (5 / 15) * 100,
|
||||
label: "5 of 15",
|
||||
className: "max-w-[198px]",
|
||||
},
|
||||
onBack: fn(),
|
||||
},
|
||||
argTypes: {},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default = {} satisfies Story;
|
||||
32
src/components/layout/Header/Header.tsx
Normal file
32
src/components/layout/Header/Header.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ChevronLeft } from "lucide-react";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
interface HeaderProps extends React.ComponentProps<"header"> {
|
||||
progressProps?: React.ComponentProps<typeof Progress>;
|
||||
onBack?: () => void;
|
||||
}
|
||||
|
||||
function Header({ className, progressProps, onBack, ...props }: HeaderProps) {
|
||||
return (
|
||||
<header className={cn("w-full p-6 pb-3", className)} {...props}>
|
||||
<div className="w-full flex justify-left items-center">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="hover:bg-transparent rounded-full p-0! ml-[-13px] mb-[-9px]"
|
||||
onClick={onBack}
|
||||
>
|
||||
<ChevronLeft size={36} />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="w-full flex justify-center items-center">
|
||||
<Progress {...progressProps} />
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
export { Header };
|
||||
@ -0,0 +1,45 @@
|
||||
import { Meta, StoryObj } from "@storybook/nextjs-vite";
|
||||
import { LayoutQuestion } from "./LayoutQuestion";
|
||||
import { fn } from "storybook/test";
|
||||
|
||||
/** Reusable LayoutQuestion page Component */
|
||||
const meta: Meta<typeof LayoutQuestion> = {
|
||||
title: "Layout/LayoutQuestion",
|
||||
component: LayoutQuestion,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
layout: "fullscreen",
|
||||
},
|
||||
args: {
|
||||
headerProps: {
|
||||
progressProps: {
|
||||
value: (5 / 15) * 100,
|
||||
label: "5 of 15",
|
||||
className: "max-w-[198px]",
|
||||
},
|
||||
onBack: fn(),
|
||||
},
|
||||
title: {
|
||||
children: "Which best represents your hair loss and goals?",
|
||||
},
|
||||
subtitle: {
|
||||
children: "Let's personalize your hair care journey",
|
||||
},
|
||||
children: (
|
||||
<div className="w-full mt-[30px] text-center p-8 bg-secondary">
|
||||
Children
|
||||
</div>
|
||||
),
|
||||
bottomActionButtonProps: {
|
||||
actionButtonProps: {
|
||||
children: "Continue",
|
||||
},
|
||||
},
|
||||
},
|
||||
argTypes: {},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default = {} satisfies Story;
|
||||
90
src/components/layout/LayoutQuestion/LayoutQuestion.tsx
Normal file
90
src/components/layout/LayoutQuestion/LayoutQuestion.tsx
Normal file
@ -0,0 +1,90 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Header } from "@/components/layout/Header/Header";
|
||||
import Typography, {
|
||||
TypographyProps,
|
||||
} from "@/components/ui/Typography/Typography";
|
||||
import {
|
||||
BottomActionButton,
|
||||
BottomActionButtonProps,
|
||||
} from "@/components/widgets/BottomActionButton/BottomActionButton";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
export interface LayoutQuestionProps
|
||||
extends Omit<React.ComponentProps<"section">, "title" | "content"> {
|
||||
headerProps?: React.ComponentProps<typeof Header>;
|
||||
title: TypographyProps<"h2">;
|
||||
subtitle: TypographyProps<"p">;
|
||||
children: React.ReactNode;
|
||||
bottomActionButtonProps?: BottomActionButtonProps;
|
||||
}
|
||||
|
||||
function LayoutQuestion({
|
||||
className,
|
||||
headerProps,
|
||||
title,
|
||||
subtitle,
|
||||
children,
|
||||
bottomActionButtonProps,
|
||||
...props
|
||||
}: LayoutQuestionProps) {
|
||||
const bottomActionButtonRef = useRef<HTMLDivElement | null>(null);
|
||||
const [bottomActionButtonHeight, setBottomActionButtonHeight] =
|
||||
useState<number>(132);
|
||||
|
||||
useEffect(() => {
|
||||
if (bottomActionButtonRef.current) {
|
||||
console.log(bottomActionButtonRef.current.clientHeight);
|
||||
|
||||
setBottomActionButtonHeight(bottomActionButtonRef.current.clientHeight);
|
||||
}
|
||||
}, [bottomActionButtonProps]);
|
||||
|
||||
return (
|
||||
<section
|
||||
className={cn(`block min-h-dvh w-full`, className)}
|
||||
{...props}
|
||||
style={{
|
||||
paddingBottom: `${bottomActionButtonHeight}px`,
|
||||
...props.style,
|
||||
}}
|
||||
>
|
||||
<Header {...headerProps} />
|
||||
<div className="w-full flex flex-col justify-center items-center p-6 pt-[30px]">
|
||||
{title && (
|
||||
<Typography
|
||||
as="h2"
|
||||
font="manrope"
|
||||
weight="bold"
|
||||
align="left"
|
||||
{...title}
|
||||
className={cn(title.className, "text-[25px] leading-[38px]")}
|
||||
/>
|
||||
)}
|
||||
{subtitle && (
|
||||
<Typography
|
||||
as="p"
|
||||
weight="medium"
|
||||
align="left"
|
||||
{...subtitle}
|
||||
className={cn(
|
||||
subtitle.className,
|
||||
"w-full mt-2.5 text-[17px] leading-[26px]"
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{children}
|
||||
{bottomActionButtonProps && (
|
||||
<BottomActionButton
|
||||
{...bottomActionButtonProps}
|
||||
className="max-w-[560px]"
|
||||
ref={bottomActionButtonRef}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export { LayoutQuestion };
|
||||
174
src/components/templates/Question/Question.stories.tsx
Normal file
174
src/components/templates/Question/Question.stories.tsx
Normal file
@ -0,0 +1,174 @@
|
||||
import { Meta, StoryObj } from "@storybook/nextjs-vite";
|
||||
import { Question } from "./Question";
|
||||
import { fn } from "storybook/test";
|
||||
import { useState } from "react";
|
||||
import { MainButtonProps } from "@/components/ui/MainButton/MainButton";
|
||||
import { SelectAnswersListProps } from "@/components/widgets/SelectAnswersList/SelectAnswersList";
|
||||
|
||||
/** Reusable Question page Component */
|
||||
const meta: Meta<typeof Question> = {
|
||||
title: "Templates/Question",
|
||||
component: Question,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
layout: "fullscreen",
|
||||
},
|
||||
args: {
|
||||
layoutQuestionProps: {
|
||||
headerProps: {
|
||||
progressProps: {
|
||||
value: (5 / 15) * 100,
|
||||
label: "5 of 15",
|
||||
className: "max-w-[198px]",
|
||||
},
|
||||
onBack: fn(),
|
||||
},
|
||||
title: {
|
||||
children: "Which best represents your hair loss and goals?",
|
||||
},
|
||||
subtitle: {
|
||||
children: "Let's personalize your hair care journey",
|
||||
},
|
||||
},
|
||||
contentType: "radio-answers-list",
|
||||
},
|
||||
argTypes: {
|
||||
contentType: {
|
||||
control: { type: "select" },
|
||||
options: ["radio-answers-list", "select-answers-list"],
|
||||
},
|
||||
content: {
|
||||
control: { type: "object" },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default = {} satisfies Story;
|
||||
|
||||
export const RadioAnswers = {
|
||||
args: {
|
||||
contentType: "radio-answers-list",
|
||||
content: {
|
||||
answers: [
|
||||
{
|
||||
children: "FEMALE",
|
||||
emoji: "👩",
|
||||
id: "female",
|
||||
},
|
||||
{
|
||||
children: "MALE",
|
||||
emoji: "👨",
|
||||
isCheckbox: true,
|
||||
id: "male",
|
||||
},
|
||||
{
|
||||
children: "Receding hairline, want to slow its progress",
|
||||
id: "without-emoji",
|
||||
},
|
||||
],
|
||||
activeAnswer: {
|
||||
children: "MALE",
|
||||
emoji: "👨",
|
||||
isCheckbox: true,
|
||||
id: "male",
|
||||
},
|
||||
onAnswerClick: fn(),
|
||||
onChangeSelectedAnswer: fn(),
|
||||
},
|
||||
},
|
||||
} satisfies Story;
|
||||
|
||||
export const SelectAnswers = {
|
||||
args: {
|
||||
contentType: "select-answers-list",
|
||||
content: {
|
||||
answers: [
|
||||
{
|
||||
children: "Receding hairline, want to slow its progress",
|
||||
isCheckbox: true,
|
||||
id: "hairline",
|
||||
},
|
||||
{
|
||||
children: "Experiencing hair loss, exploring",
|
||||
isCheckbox: true,
|
||||
id: "exploring",
|
||||
},
|
||||
{
|
||||
children: "Experiencing hair loss, ready to start",
|
||||
isCheckbox: true,
|
||||
id: "ready-to-start",
|
||||
},
|
||||
{
|
||||
children: "Experiencing hair loss, ready to start",
|
||||
id: "ready-to-start-text",
|
||||
},
|
||||
{
|
||||
children: "Experiencing hair loss, ready to start",
|
||||
emoji: "👩🏼",
|
||||
id: "ready-to-start-emoji",
|
||||
},
|
||||
{
|
||||
children: "Experiencing hair loss, ready to start",
|
||||
emoji: "👩🏼",
|
||||
isCheckbox: true,
|
||||
id: "ready-to-start-emoji-checkbox",
|
||||
},
|
||||
],
|
||||
activeAnswers: [
|
||||
{
|
||||
children: "Experiencing hair loss, ready to start",
|
||||
isCheckbox: true,
|
||||
id: "ready-to-start",
|
||||
},
|
||||
{
|
||||
children: "Experiencing hair loss, ready to start",
|
||||
emoji: "👩🏼",
|
||||
id: "ready-to-start-emoji",
|
||||
},
|
||||
],
|
||||
onChangeSelectedAnswers: fn(),
|
||||
onAnswerClick: fn(),
|
||||
},
|
||||
},
|
||||
render: (args) => {
|
||||
const { layoutQuestionProps, content, ...rest } = args;
|
||||
const [selectedAnswers, setSelectedAnswers] = useState<
|
||||
MainButtonProps[] | null
|
||||
>((content as SelectAnswersListProps).activeAnswers);
|
||||
|
||||
const onActionButtonClick = () => {
|
||||
fn()(selectedAnswers);
|
||||
};
|
||||
|
||||
const layoutQuestionArgs = {
|
||||
...layoutQuestionProps,
|
||||
bottomActionButtonProps: {
|
||||
actionButtonProps: {
|
||||
children: "Continue",
|
||||
onClick: onActionButtonClick,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const onChangeSelectedAnswers = (answers: MainButtonProps[] | null) => {
|
||||
setSelectedAnswers(answers);
|
||||
fn()(answers);
|
||||
};
|
||||
|
||||
const contentArgs = {
|
||||
...content,
|
||||
onChangeSelectedAnswers,
|
||||
};
|
||||
|
||||
return (
|
||||
<Question
|
||||
{...rest}
|
||||
layoutQuestionProps={layoutQuestionArgs}
|
||||
content={contentArgs}
|
||||
/>
|
||||
);
|
||||
},
|
||||
} satisfies Story;
|
||||
45
src/components/templates/Question/Question.tsx
Normal file
45
src/components/templates/Question/Question.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
RadioAnswersList,
|
||||
RadioAnswersListProps,
|
||||
} from "@/components/widgets/RadioAnswersList/RadioAnswersList";
|
||||
import {
|
||||
LayoutQuestion,
|
||||
LayoutQuestionProps,
|
||||
} from "@/components/layout/LayoutQuestion/LayoutQuestion";
|
||||
import {
|
||||
SelectAnswersList,
|
||||
SelectAnswersListProps,
|
||||
} from "@/components/widgets/SelectAnswersList/SelectAnswersList";
|
||||
|
||||
interface QuestionProps
|
||||
extends Omit<React.ComponentProps<"div">, "title" | "content"> {
|
||||
layoutQuestionProps: Omit<LayoutQuestionProps, "children">;
|
||||
content: RadioAnswersListProps | SelectAnswersListProps;
|
||||
contentType: "radio-answers-list" | "select-answers-list";
|
||||
}
|
||||
|
||||
function Question({
|
||||
layoutQuestionProps,
|
||||
content,
|
||||
contentType,
|
||||
...props
|
||||
}: QuestionProps) {
|
||||
return (
|
||||
<LayoutQuestion {...layoutQuestionProps}>
|
||||
{content && (
|
||||
<div className="w-full mt-[30px]" {...props}>
|
||||
{contentType === "radio-answers-list" && (
|
||||
<RadioAnswersList {...(content as RadioAnswersListProps)} />
|
||||
)}
|
||||
{contentType === "select-answers-list" && (
|
||||
<SelectAnswersList {...(content as SelectAnswersListProps)} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</LayoutQuestion>
|
||||
);
|
||||
}
|
||||
|
||||
export { Question };
|
||||
60
src/components/ui/ActionButton/ActionButton.stories.tsx
Normal file
60
src/components/ui/ActionButton/ActionButton.stories.tsx
Normal file
@ -0,0 +1,60 @@
|
||||
import { Meta, StoryObj } from "@storybook/nextjs-vite";
|
||||
import { ActionButton } from "./ActionButton";
|
||||
import Link from "next/link";
|
||||
import { fn } from "storybook/test";
|
||||
|
||||
/** Reusable ActionButton Component */
|
||||
const meta: Meta<typeof ActionButton> = {
|
||||
title: "UI/ActionButton",
|
||||
component: ActionButton,
|
||||
tags: ["autodocs"],
|
||||
args: {
|
||||
disabled: false,
|
||||
asChild: false,
|
||||
cornerRadius: "3xl",
|
||||
children: "Continue",
|
||||
onClick: fn(),
|
||||
},
|
||||
argTypes: {
|
||||
cornerRadius: {
|
||||
control: { type: "select" },
|
||||
options: ["3xl", "full"],
|
||||
},
|
||||
asChild: {
|
||||
control: { type: "boolean" },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default = {} satisfies Story;
|
||||
|
||||
export const Disabled = {
|
||||
args: {
|
||||
children: "Disabled",
|
||||
disabled: true,
|
||||
},
|
||||
} satisfies Story;
|
||||
|
||||
export const AsChild = {
|
||||
args: {
|
||||
asChild: true,
|
||||
children: <Link href="#test">Link ActionButton</Link>,
|
||||
},
|
||||
} satisfies Story;
|
||||
|
||||
export const CornerRadiusFull = {
|
||||
args: {
|
||||
cornerRadius: "full",
|
||||
children: "Full Corner Radius",
|
||||
},
|
||||
} satisfies Story;
|
||||
|
||||
export const CornerRadius3xl = {
|
||||
args: {
|
||||
cornerRadius: "3xl",
|
||||
children: "3xl Corner Radius",
|
||||
},
|
||||
} satisfies Story;
|
||||
46
src/components/ui/ActionButton/ActionButton.tsx
Normal file
46
src/components/ui/ActionButton/ActionButton.tsx
Normal file
@ -0,0 +1,46 @@
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "../button";
|
||||
|
||||
const buttonVariants = cva(
|
||||
cn(
|
||||
"w-full",
|
||||
"bg-gradient-to-r from-[#3B82F6] to-[#2563EB]",
|
||||
"cursor-pointer",
|
||||
"inline-flex items-center justify-center gap-2",
|
||||
"font-inter text-xl/[24px] font-bold text-primary-foreground",
|
||||
"px-[27px] py-5",
|
||||
"transition-all",
|
||||
"disabled:opacity-50",
|
||||
"shadow-blue-glow"
|
||||
),
|
||||
{
|
||||
variants: {
|
||||
cornerRadius: {
|
||||
"3xl": "rounded-3xl",
|
||||
full: "rounded-full",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
cornerRadius: "3xl",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
function ActionButton({
|
||||
className,
|
||||
cornerRadius,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Button> &
|
||||
VariantProps<typeof buttonVariants> & {}) {
|
||||
return (
|
||||
<Button
|
||||
data-slot="action-button"
|
||||
className={cn(buttonVariants({ cornerRadius, className }))}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { ActionButton, buttonVariants };
|
||||
61
src/components/ui/MainButton/MainButton.stories.tsx
Normal file
61
src/components/ui/MainButton/MainButton.stories.tsx
Normal file
@ -0,0 +1,61 @@
|
||||
import { Meta, StoryObj } from "@storybook/nextjs-vite";
|
||||
import { MainButton } from "./MainButton";
|
||||
import { fn } from "storybook/test";
|
||||
import { useState } from "react";
|
||||
|
||||
/** Reusable MainButton Component */
|
||||
const meta: Meta<typeof MainButton> = {
|
||||
title: "UI/MainButton",
|
||||
component: MainButton,
|
||||
tags: ["autodocs"],
|
||||
args: {
|
||||
disabled: false,
|
||||
children: "Title",
|
||||
onClick: fn(),
|
||||
},
|
||||
argTypes: {
|
||||
active: {
|
||||
control: { type: "boolean" },
|
||||
},
|
||||
},
|
||||
render: (args) => {
|
||||
const [active, setActive] = useState(args.active);
|
||||
return (
|
||||
<MainButton
|
||||
{...args}
|
||||
onClick={() => setActive((prev) => !prev)}
|
||||
active={active}
|
||||
/>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default = {} satisfies Story;
|
||||
|
||||
export const WithEmoji = {
|
||||
args: {
|
||||
emoji: "🩷️",
|
||||
},
|
||||
} satisfies Story;
|
||||
|
||||
export const WithCheckbox = {
|
||||
args: {
|
||||
isCheckbox: true,
|
||||
checkboxProps: {
|
||||
onCheckedChange: fn(),
|
||||
},
|
||||
},
|
||||
} satisfies Story;
|
||||
|
||||
export const WithCheckboxAndEmoji = {
|
||||
args: {
|
||||
isCheckbox: true,
|
||||
emoji: "🩷️",
|
||||
checkboxProps: {
|
||||
onCheckedChange: fn(),
|
||||
},
|
||||
},
|
||||
} satisfies Story;
|
||||
89
src/components/ui/MainButton/MainButton.tsx
Normal file
89
src/components/ui/MainButton/MainButton.tsx
Normal file
@ -0,0 +1,89 @@
|
||||
"use client";
|
||||
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "../button";
|
||||
import { Checkbox } from "../checkbox";
|
||||
import { Label } from "../label";
|
||||
|
||||
const buttonVariants = cva(
|
||||
cn(
|
||||
"w-full",
|
||||
"cursor-pointer",
|
||||
"inline-flex items-center justify-center gap-4",
|
||||
"font-manrope text-[18px]/[18px] font-medium",
|
||||
"pl-[26px] pr-[18px] py-[18px]",
|
||||
"transition-all",
|
||||
"disabled:opacity-50",
|
||||
"border-2"
|
||||
),
|
||||
{
|
||||
variants: {
|
||||
cornerRadius: {
|
||||
"3xl": "rounded-3xl",
|
||||
full: "rounded-full",
|
||||
},
|
||||
active: {
|
||||
true: "bg-gradient-to-r from-[#EBF5FF] to-[#DBEAFE] border-primary shadow-blue-glow-2 text-primary",
|
||||
false:
|
||||
"bg-background border-border shadow-black-glow text-secondary-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
cornerRadius: "3xl",
|
||||
active: false,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export interface MainButtonProps
|
||||
extends React.ComponentProps<typeof Button>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
emoji?: string;
|
||||
isCheckbox?: boolean;
|
||||
checkboxProps?: React.ComponentProps<typeof Checkbox>;
|
||||
}
|
||||
|
||||
function MainButton({
|
||||
className,
|
||||
cornerRadius,
|
||||
active,
|
||||
children,
|
||||
emoji,
|
||||
isCheckbox,
|
||||
checkboxProps,
|
||||
disabled,
|
||||
...props
|
||||
}: MainButtonProps) {
|
||||
return (
|
||||
<Button
|
||||
data-slot="main-button"
|
||||
className={cn(buttonVariants({ cornerRadius, active, className }))}
|
||||
{...props}
|
||||
asChild
|
||||
>
|
||||
<Label
|
||||
data-disabled={disabled}
|
||||
className={cn(
|
||||
disabled && "pointer-events-none opacity-50 cursor-not-allowed"
|
||||
)}
|
||||
>
|
||||
{emoji && <span className="text-[40px]">{Array.from(emoji)[0]}</span>}
|
||||
<p className="w-full text-left min-h-[38px] flex items-center">
|
||||
{children}
|
||||
</p>
|
||||
{isCheckbox && (
|
||||
<Checkbox
|
||||
{...checkboxProps}
|
||||
checked={active ?? false}
|
||||
disabled={disabled}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
)}
|
||||
</Label>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export { MainButton, buttonVariants };
|
||||
69
src/components/ui/TextInput/TextInput.stories.tsx
Normal file
69
src/components/ui/TextInput/TextInput.stories.tsx
Normal file
@ -0,0 +1,69 @@
|
||||
import { Meta, StoryObj } from "@storybook/nextjs-vite";
|
||||
import { TextInput } from "./TextInput";
|
||||
import { fn } from "storybook/test";
|
||||
|
||||
/** Reusable TextInput Component */
|
||||
const meta: Meta<typeof TextInput> = {
|
||||
title: "UI/TextInput",
|
||||
component: TextInput,
|
||||
tags: ["autodocs"],
|
||||
args: {
|
||||
disabled: false,
|
||||
placeholder: "Placeholder",
|
||||
"aria-invalid": false,
|
||||
"aria-errormessage": "",
|
||||
label: "",
|
||||
type: "text",
|
||||
onChange: fn(),
|
||||
onFocus: fn(),
|
||||
onBlur: fn(),
|
||||
onError: fn(),
|
||||
},
|
||||
argTypes: {
|
||||
"aria-invalid": {
|
||||
control: { type: "boolean" },
|
||||
},
|
||||
"aria-errormessage": {
|
||||
control: { type: "text" },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default = {} satisfies Story;
|
||||
|
||||
export const Disabled = {
|
||||
args: {
|
||||
disabled: true,
|
||||
},
|
||||
} satisfies Story;
|
||||
|
||||
export const WithLabel = {
|
||||
args: {
|
||||
label: "Label",
|
||||
},
|
||||
} satisfies Story;
|
||||
|
||||
export const Email = {
|
||||
args: {
|
||||
type: "email",
|
||||
label: "Email",
|
||||
placeholder: "example@email.com",
|
||||
},
|
||||
render: (args) => (
|
||||
<div className="flex flex-col gap-2">
|
||||
<TextInput {...args} />
|
||||
<TextInput {...args} value={"Email"} />
|
||||
</div>
|
||||
),
|
||||
} satisfies Story;
|
||||
|
||||
export const Invalid = {
|
||||
args: {
|
||||
label: "Email",
|
||||
"aria-invalid": true,
|
||||
"aria-errormessage": "Invalid email",
|
||||
},
|
||||
} satisfies Story;
|
||||
45
src/components/ui/TextInput/TextInput.tsx
Normal file
45
src/components/ui/TextInput/TextInput.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Input } from "../input";
|
||||
import { Label } from "../label";
|
||||
import { useId } from "react";
|
||||
|
||||
interface TextInputProps extends React.ComponentProps<typeof Input> {
|
||||
label?: string;
|
||||
}
|
||||
|
||||
function TextInput({ className, label, ...props }: TextInputProps) {
|
||||
const id = useId();
|
||||
const inputId = props.id || id;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
{label && (
|
||||
<Label
|
||||
htmlFor={inputId}
|
||||
className="text-muted-foreground font-inter font-medium text-base"
|
||||
>
|
||||
{label}
|
||||
</Label>
|
||||
)}
|
||||
<Input
|
||||
data-slot="text-input"
|
||||
className={cn(
|
||||
"py-3.5 px-4",
|
||||
"font-inter text-[18px]/[28px] font-semibold text-foreground",
|
||||
"placeholder:text-muted-foreground placeholder:text-[18px]/[28px] font-medium",
|
||||
"border-2 border-primary/30 rounded-2xl",
|
||||
className
|
||||
)}
|
||||
id={inputId}
|
||||
{...props}
|
||||
/>
|
||||
{props["aria-invalid"] && (
|
||||
<p className="text-destructive font-inter font-medium text-xs">
|
||||
{props["aria-errormessage"]}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { TextInput };
|
||||
205
src/components/ui/Typography/Typography.stories.tsx
Normal file
205
src/components/ui/Typography/Typography.stories.tsx
Normal file
@ -0,0 +1,205 @@
|
||||
import { Meta, StoryObj } from "@storybook/nextjs-vite";
|
||||
import Typography from "./Typography";
|
||||
|
||||
/** Reusable Typography Component */
|
||||
const meta: Meta<typeof Typography> = {
|
||||
title: "UI/Typography",
|
||||
component: Typography,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
args: {
|
||||
children: "Typography Text",
|
||||
size: "md",
|
||||
weight: "regular",
|
||||
color: "default",
|
||||
align: "center",
|
||||
as: "span",
|
||||
font: "inter",
|
||||
},
|
||||
argTypes: {
|
||||
as: {
|
||||
control: { type: "select" },
|
||||
options: ["span", "p", "h1", "h2", "h3", "h4", "h5", "h6", "div"],
|
||||
description: "HTML element to render",
|
||||
},
|
||||
size: {
|
||||
control: { type: "select" },
|
||||
options: ["xs", "sm", "md", "lg", "xl", "2xl"],
|
||||
description: "Text size variant",
|
||||
},
|
||||
weight: {
|
||||
control: { type: "select" },
|
||||
options: ["regular", "medium", "semiBold", "bold", "extraBold", "black"],
|
||||
description: "Font weight variant",
|
||||
},
|
||||
color: {
|
||||
control: { type: "select" },
|
||||
options: [
|
||||
"default",
|
||||
"primary",
|
||||
"secondary",
|
||||
"destructive",
|
||||
"success",
|
||||
"muted",
|
||||
"card",
|
||||
"accent",
|
||||
],
|
||||
description: "Text color variant",
|
||||
},
|
||||
align: {
|
||||
control: { type: "select" },
|
||||
options: ["center", "left", "right"],
|
||||
description: "Text alignment",
|
||||
},
|
||||
children: {
|
||||
control: { type: "text" },
|
||||
description: "Text content",
|
||||
},
|
||||
font: {
|
||||
control: { type: "select" },
|
||||
options: ["manrope", "inter", "geistSans", "geistMono"],
|
||||
description: "Font variant",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default = {
|
||||
args: {
|
||||
children: "Default Typography",
|
||||
},
|
||||
} satisfies Story;
|
||||
|
||||
export const Fonts = {
|
||||
decorators: [
|
||||
() => (
|
||||
<div className="space-y-4">
|
||||
<Typography font="manrope">Font: manrope</Typography>
|
||||
<Typography font="inter">Font: inter</Typography>
|
||||
<Typography font="geistSans">Font: geist Sans</Typography>
|
||||
<Typography font="geistMono">Font: geist Mono</Typography>
|
||||
</div>
|
||||
),
|
||||
],
|
||||
} satisfies Story;
|
||||
|
||||
export const Sizes = {
|
||||
decorators: [
|
||||
() => (
|
||||
<div className="space-y-4">
|
||||
<Typography size="xs">Extra Small Text (xs)</Typography>
|
||||
<Typography size="sm">Small Text (sm)</Typography>
|
||||
<Typography size="md">Medium Text (md)</Typography>
|
||||
<Typography size="lg">Large Text (lg)</Typography>
|
||||
<Typography size="xl">Extra Large Text (xl)</Typography>
|
||||
<Typography size="2xl">2X Large Text (2xl)</Typography>
|
||||
</div>
|
||||
),
|
||||
],
|
||||
} satisfies Story;
|
||||
|
||||
export const Weights = {
|
||||
decorators: [
|
||||
() => (
|
||||
<div className="space-y-4">
|
||||
<Typography weight="regular">Regular Weight</Typography>
|
||||
<Typography weight="medium">Medium Weight</Typography>
|
||||
<Typography weight="semiBold">Semi Bold Weight</Typography>
|
||||
<Typography weight="bold">Bold Weight</Typography>
|
||||
<Typography weight="extraBold">Extra Bold Weight</Typography>
|
||||
<Typography weight="black">Black Weight</Typography>
|
||||
</div>
|
||||
),
|
||||
],
|
||||
} satisfies Story;
|
||||
|
||||
export const Colors = {
|
||||
decorators: [
|
||||
() => (
|
||||
<div className="space-y-4 flex flex-col gap-4">
|
||||
<Typography color="default">Default Color</Typography>
|
||||
<Typography color="primary" className="bg-primary rounded-sm">
|
||||
Primary Color
|
||||
</Typography>
|
||||
<Typography color="secondary">Secondary Color</Typography>
|
||||
<Typography color="destructive">Destructive Color</Typography>
|
||||
<Typography color="success">Success Color</Typography>
|
||||
<Typography color="muted">Muted Color</Typography>
|
||||
<Typography color="card">Card Color</Typography>
|
||||
<Typography color="accent">Accent Color</Typography>
|
||||
</div>
|
||||
),
|
||||
],
|
||||
} satisfies Story;
|
||||
|
||||
export const Alignments = {
|
||||
decorators: [
|
||||
() => (
|
||||
<div className="space-y-4 w-full">
|
||||
<Typography align="left">Left Aligned Text</Typography>
|
||||
<Typography align="center">Center Aligned Text</Typography>
|
||||
<Typography align="right">Right Aligned Text</Typography>
|
||||
</div>
|
||||
),
|
||||
],
|
||||
} satisfies Story;
|
||||
|
||||
export const AsElements = {
|
||||
decorators: [
|
||||
() => (
|
||||
<div className="space-y-4">
|
||||
<Typography as="h1" size="2xl" weight="bold">
|
||||
Heading 1
|
||||
</Typography>
|
||||
<Typography as="h2" size="xl" weight="semiBold">
|
||||
Heading 2
|
||||
</Typography>
|
||||
<Typography as="h3" size="lg" weight="medium">
|
||||
Heading 3
|
||||
</Typography>
|
||||
<Typography as="p" size="md">
|
||||
Paragraph text with regular styling
|
||||
</Typography>
|
||||
<Typography as="span" size="sm" color="muted">
|
||||
Small span text
|
||||
</Typography>
|
||||
</div>
|
||||
),
|
||||
],
|
||||
} satisfies Story;
|
||||
|
||||
export const Headings = {
|
||||
decorators: [
|
||||
() => (
|
||||
<div className="space-y-6">
|
||||
<Typography as="h1" size="2xl" weight="bold" color="default">
|
||||
Main Heading
|
||||
</Typography>
|
||||
<Typography as="h2" size="xl" weight="semiBold" color="secondary">
|
||||
Section Heading
|
||||
</Typography>
|
||||
<Typography as="h3" size="lg" weight="medium" color="default">
|
||||
Subsection Heading
|
||||
</Typography>
|
||||
<Typography as="p" size="md" weight="regular" color="muted">
|
||||
This is a paragraph of text that demonstrates how the typography
|
||||
component can be used for body content with proper spacing and
|
||||
readability.
|
||||
</Typography>
|
||||
</div>
|
||||
),
|
||||
],
|
||||
} satisfies Story;
|
||||
|
||||
export const CustomStyles = {
|
||||
args: {
|
||||
children: "Custom Styled Text",
|
||||
className: "bg-blue-100 text-blue-800 px-4 py-2 rounded-lg",
|
||||
size: "lg",
|
||||
weight: "bold",
|
||||
},
|
||||
} satisfies Story;
|
||||
92
src/components/ui/Typography/Typography.tsx
Normal file
92
src/components/ui/Typography/Typography.tsx
Normal file
@ -0,0 +1,92 @@
|
||||
import { createElement, JSX, ReactNode } from "react";
|
||||
import { cva, VariantProps } from "class-variance-authority";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const typographyVariants = cva(cn("text-center text-foreground block"), {
|
||||
variants: {
|
||||
size: {
|
||||
xs: "text-xs",
|
||||
sm: "text-sm",
|
||||
md: "text-base",
|
||||
lg: "text-lg",
|
||||
xl: "text-xl",
|
||||
"2xl": "text-2xl",
|
||||
"3xl": "text-3xl",
|
||||
"4xl": "text-4xl",
|
||||
},
|
||||
weight: {
|
||||
regular: "font-normal",
|
||||
medium: "font-medium",
|
||||
semiBold: "font-semibold",
|
||||
bold: "font-bold",
|
||||
extraBold: "font-extrabold",
|
||||
black: "font-black",
|
||||
},
|
||||
color: {
|
||||
default: "text-foreground",
|
||||
primary: "text-primary-foreground",
|
||||
secondary: "text-secondary-foreground",
|
||||
destructive: "text-destructive",
|
||||
success: "text-success",
|
||||
card: "text-card-foreground",
|
||||
accent: "text-accent-foreground",
|
||||
muted: "text-muted-foreground",
|
||||
},
|
||||
align: {
|
||||
center: "text-center",
|
||||
left: "text-left",
|
||||
right: "text-right",
|
||||
},
|
||||
font: {
|
||||
manrope: "font-manrope",
|
||||
inter: "font-inter",
|
||||
geistSans: "font-geist-sans",
|
||||
geistMono: "font-geist-mono",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: "md",
|
||||
weight: "regular",
|
||||
color: "default",
|
||||
align: "center",
|
||||
font: "inter",
|
||||
},
|
||||
});
|
||||
|
||||
type TypographyElements = Pick<
|
||||
JSX.IntrinsicElements,
|
||||
"span" | "p" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "div"
|
||||
>;
|
||||
|
||||
export type TypographyProps<T extends keyof TypographyElements = "span"> =
|
||||
VariantProps<typeof typographyVariants> &
|
||||
Omit<TypographyElements[T], "color"> & {
|
||||
as?: T;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export default function Typography<
|
||||
T extends keyof TypographyElements = "span"
|
||||
>({
|
||||
as: Component = "span" as T,
|
||||
children,
|
||||
className,
|
||||
weight,
|
||||
size,
|
||||
color,
|
||||
align,
|
||||
font,
|
||||
...props
|
||||
}: TypographyProps<T>) {
|
||||
return createElement(
|
||||
Component,
|
||||
{
|
||||
className: cn(
|
||||
typographyVariants({ size, weight, color, align, font }),
|
||||
className
|
||||
),
|
||||
...props,
|
||||
},
|
||||
children
|
||||
);
|
||||
}
|
||||
55
src/components/ui/button.stories.tsx
Normal file
55
src/components/ui/button.stories.tsx
Normal file
@ -0,0 +1,55 @@
|
||||
import { Meta, StoryObj } from "@storybook/nextjs-vite";
|
||||
import { Button } from "./button";
|
||||
import Link from "next/link";
|
||||
import { fn } from "storybook/test";
|
||||
|
||||
/** Reusable Button Component */
|
||||
const meta: Meta<typeof Button> = {
|
||||
title: "Shadcn UI/Button",
|
||||
component: Button,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
args: {
|
||||
disabled: false,
|
||||
asChild: false,
|
||||
onClick: fn(),
|
||||
},
|
||||
argTypes: {
|
||||
asChild: {
|
||||
control: { type: "boolean" },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default = {
|
||||
args: {
|
||||
children: "Button",
|
||||
},
|
||||
} satisfies Story;
|
||||
|
||||
export const Disabled = {
|
||||
args: {
|
||||
children: "Disabled",
|
||||
disabled: true,
|
||||
},
|
||||
} satisfies Story;
|
||||
|
||||
export const AsChild = {
|
||||
args: {
|
||||
asChild: true,
|
||||
children: <Link href="#test">Link Button</Link>,
|
||||
},
|
||||
} satisfies Story;
|
||||
|
||||
export const CustomStyles = {
|
||||
args: {
|
||||
children: "Custom Styled",
|
||||
className:
|
||||
"bg-gradient-to-r from-purple-500 to-pink-500 text-white hover:from-purple-600 hover:to-pink-600 shadow-lg",
|
||||
},
|
||||
} satisfies Story;
|
||||
57
src/components/ui/button.tsx
Normal file
57
src/components/ui/button.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
const buttonVariants = cva(
|
||||
cn(
|
||||
"cursor-pointer",
|
||||
"px-4 py-2",
|
||||
"disabled:cursor-not-allowed disabled:opacity-70",
|
||||
"outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive"
|
||||
),
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground",
|
||||
destructive:
|
||||
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost:
|
||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
interface ButtonProps
|
||||
extends React.ComponentProps<"button">,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean;
|
||||
}
|
||||
|
||||
function Button({
|
||||
className,
|
||||
asChild = false,
|
||||
variant,
|
||||
...props
|
||||
}: ButtonProps) {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
className={buttonVariants({ variant, className })}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Button };
|
||||
28
src/components/ui/checkbox.stories.tsx
Normal file
28
src/components/ui/checkbox.stories.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
import { Meta, StoryObj } from "@storybook/nextjs-vite";
|
||||
import { Checkbox } from "./checkbox";
|
||||
import { fn } from "storybook/test";
|
||||
|
||||
/** Reusable Checkbox Component */
|
||||
const meta: Meta<typeof Checkbox> = {
|
||||
title: "Shadcn UI/Checkbox",
|
||||
component: Checkbox,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
args: {
|
||||
disabled: false,
|
||||
onCheckedChange: fn(),
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default = {} satisfies Story;
|
||||
|
||||
export const Disabled = {
|
||||
args: {
|
||||
disabled: true,
|
||||
},
|
||||
} satisfies Story;
|
||||
31
src/components/ui/checkbox.tsx
Normal file
31
src/components/ui/checkbox.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
"use client";
|
||||
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
|
||||
import { CheckIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Checkbox({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
|
||||
return (
|
||||
<CheckboxPrimitive.Root
|
||||
data-slot="checkbox"
|
||||
className={cn(
|
||||
"cursor-pointer peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-5 shrink-0 rounded-full border-2 shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
data-slot="checkbox-indicator"
|
||||
className="flex items-center justify-center text-current transition-none"
|
||||
>
|
||||
<CheckIcon className="size-3.5" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
);
|
||||
}
|
||||
|
||||
export { Checkbox };
|
||||
78
src/components/ui/input.stories.tsx
Normal file
78
src/components/ui/input.stories.tsx
Normal file
@ -0,0 +1,78 @@
|
||||
import { Meta, StoryObj } from "@storybook/nextjs-vite";
|
||||
import { Input } from "./input";
|
||||
import { fn } from "storybook/test";
|
||||
|
||||
/** Reusable Input Component */
|
||||
const meta: Meta<typeof Input> = {
|
||||
title: "Shadcn UI/Input",
|
||||
component: Input,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
args: {
|
||||
disabled: false,
|
||||
placeholder: "Placeholder",
|
||||
onChange: fn(),
|
||||
onFocus: fn(),
|
||||
onBlur: fn(),
|
||||
},
|
||||
argTypes: {
|
||||
type: {
|
||||
control: { type: "select" },
|
||||
options: ["text", "email", "password", "number"],
|
||||
},
|
||||
disabled: {
|
||||
control: { type: "boolean" },
|
||||
},
|
||||
placeholder: {
|
||||
control: { type: "text" },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default = {} satisfies Story;
|
||||
|
||||
export const WithValue = {
|
||||
args: {
|
||||
defaultValue: "Value",
|
||||
},
|
||||
} satisfies Story;
|
||||
|
||||
export const Email = {
|
||||
args: {
|
||||
type: "email",
|
||||
placeholder: "example@email.com",
|
||||
},
|
||||
} satisfies Story;
|
||||
|
||||
export const Password = {
|
||||
args: {
|
||||
type: "password",
|
||||
placeholder: "Password",
|
||||
},
|
||||
} satisfies Story;
|
||||
|
||||
export const Number = {
|
||||
args: {
|
||||
type: "number",
|
||||
placeholder: "Number",
|
||||
},
|
||||
} satisfies Story;
|
||||
|
||||
export const Disabled = {
|
||||
args: {
|
||||
placeholder: "Disabled",
|
||||
disabled: true,
|
||||
},
|
||||
} satisfies Story;
|
||||
|
||||
export const WithError = {
|
||||
args: {
|
||||
placeholder: "With Error",
|
||||
"aria-invalid": true,
|
||||
},
|
||||
} satisfies Story;
|
||||
19
src/components/ui/input.tsx
Normal file
19
src/components/ui/input.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Input };
|
||||
23
src/components/ui/label.tsx
Normal file
23
src/components/ui/label.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
"use client";
|
||||
|
||||
import * as LabelPrimitive from "@radix-ui/react-label";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Label({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||
return (
|
||||
<LabelPrimitive.Root
|
||||
data-slot="label"
|
||||
className={cn(
|
||||
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Label };
|
||||
103
src/components/ui/progress.stories.tsx
Normal file
103
src/components/ui/progress.stories.tsx
Normal file
@ -0,0 +1,103 @@
|
||||
import { Meta, StoryObj } from "@storybook/nextjs-vite";
|
||||
import { Progress } from "./progress";
|
||||
import { ActionButton } from "./ActionButton/ActionButton";
|
||||
import { useState } from "react";
|
||||
|
||||
/** Reusable Progress Component */
|
||||
const meta: Meta<typeof Progress> = {
|
||||
title: "Shadcn UI/Progress",
|
||||
component: Progress,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
args: {
|
||||
value: 50,
|
||||
},
|
||||
argTypes: {
|
||||
value: {
|
||||
control: { type: "range", min: 0, max: 100, step: 1 },
|
||||
description: "Progress value from 0 to 100",
|
||||
},
|
||||
label: {
|
||||
control: { type: "text" },
|
||||
description: "Progress label",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default = {
|
||||
args: {
|
||||
value: 50,
|
||||
},
|
||||
} satisfies Story;
|
||||
|
||||
export const Empty = {
|
||||
args: {
|
||||
value: 0,
|
||||
},
|
||||
} satisfies Story;
|
||||
|
||||
export const Quarter = {
|
||||
args: {
|
||||
value: 25,
|
||||
},
|
||||
} satisfies Story;
|
||||
|
||||
export const Half = {
|
||||
args: {
|
||||
value: 50,
|
||||
},
|
||||
} satisfies Story;
|
||||
|
||||
export const ThreeQuarters = {
|
||||
args: {
|
||||
value: 75,
|
||||
},
|
||||
} satisfies Story;
|
||||
|
||||
export const Complete = {
|
||||
args: {
|
||||
value: 100,
|
||||
},
|
||||
} satisfies Story;
|
||||
|
||||
export const CustomColors = {
|
||||
args: {
|
||||
value: 80,
|
||||
classNameProgress: "bg-green-200 [&>div]:bg-green-600",
|
||||
},
|
||||
} satisfies Story;
|
||||
|
||||
export const Animated = {
|
||||
args: {
|
||||
value: 0,
|
||||
},
|
||||
decorators: [
|
||||
(Story) => {
|
||||
const [progress, setProgress] = useState(0);
|
||||
|
||||
const handleSetRandomValue = () => {
|
||||
setProgress(Math.floor(Math.random() * 100));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full flex flex-col gap-8">
|
||||
<Story args={{ value: progress }} />
|
||||
<ActionButton onClick={handleSetRandomValue}>
|
||||
Set random value
|
||||
</ActionButton>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
],
|
||||
} satisfies Story;
|
||||
|
||||
export const WithLabel = {
|
||||
args: {
|
||||
label: "1 of 15",
|
||||
},
|
||||
} satisfies Story;
|
||||
46
src/components/ui/progress.tsx
Normal file
46
src/components/ui/progress.tsx
Normal file
@ -0,0 +1,46 @@
|
||||
"use client";
|
||||
|
||||
import * as ProgressPrimitive from "@radix-ui/react-progress";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Label } from "./label";
|
||||
|
||||
interface ProgressProps
|
||||
extends React.ComponentProps<typeof ProgressPrimitive.Root> {
|
||||
label?: string;
|
||||
classNameProgress?: string;
|
||||
}
|
||||
|
||||
function Progress({
|
||||
className,
|
||||
classNameProgress,
|
||||
value,
|
||||
label,
|
||||
...props
|
||||
}: ProgressProps) {
|
||||
return (
|
||||
<div className={cn("w-full flex flex-col gap-[3px]", className)}>
|
||||
{label && (
|
||||
<Label className="flex-row justify-end w-full px-2 text-right text-muted-foreground font-inter font-medium text-xs">
|
||||
{label}
|
||||
</Label>
|
||||
)}
|
||||
<ProgressPrimitive.Root
|
||||
data-slot="progress"
|
||||
className={cn(
|
||||
"bg-border relative h-1.5 w-full overflow-hidden rounded-full",
|
||||
classNameProgress
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ProgressPrimitive.Indicator
|
||||
data-slot="progress-indicator"
|
||||
className="bg-primary h-full w-full flex-1 transition-all rounded-full"
|
||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||
/>
|
||||
</ProgressPrimitive.Root>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { Progress };
|
||||
27
src/components/ui/separator.tsx
Normal file
27
src/components/ui/separator.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
"use client";
|
||||
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Separator({
|
||||
className,
|
||||
orientation = "horizontal",
|
||||
decorative = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
||||
return (
|
||||
<SeparatorPrimitive.Root
|
||||
data-slot="separator"
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Separator };
|
||||
37
src/components/widgets/AnswersList/AnswersList.stories.tsx
Normal file
37
src/components/widgets/AnswersList/AnswersList.stories.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import { Meta, StoryObj } from "@storybook/nextjs-vite";
|
||||
import { AnswersList } from "./AnswersList";
|
||||
|
||||
/** Reusable AnswersList Component */
|
||||
const meta: Meta<typeof AnswersList> = {
|
||||
title: "Widgets/AnswersList",
|
||||
component: AnswersList,
|
||||
tags: ["autodocs"],
|
||||
args: {
|
||||
answers: [
|
||||
{
|
||||
children: "FEMALE",
|
||||
emoji: "👩",
|
||||
isCheckbox: true,
|
||||
id: "female",
|
||||
},
|
||||
{
|
||||
children: "MALE",
|
||||
emoji: "👨",
|
||||
id: "male",
|
||||
},
|
||||
{
|
||||
children: "Receding hairline, want to slow its progress",
|
||||
id: "without-emoji",
|
||||
},
|
||||
],
|
||||
},
|
||||
argTypes: {},
|
||||
render: (args) => {
|
||||
return <AnswersList {...args} activeAnswers={[args.answers[0]]} />;
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default = {} satisfies Story;
|
||||
36
src/components/widgets/AnswersList/AnswersList.tsx
Normal file
36
src/components/widgets/AnswersList/AnswersList.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
MainButton,
|
||||
MainButtonProps,
|
||||
} from "@/components/ui/MainButton/MainButton";
|
||||
|
||||
export interface AnswersListProps extends React.ComponentProps<"div"> {
|
||||
answers: MainButtonProps[];
|
||||
activeAnswers: MainButtonProps[] | null;
|
||||
onAnswerClick: (answer: MainButtonProps) => void;
|
||||
}
|
||||
|
||||
function AnswersList({
|
||||
className,
|
||||
answers,
|
||||
activeAnswers,
|
||||
onAnswerClick,
|
||||
...props
|
||||
}: AnswersListProps) {
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-3 w-full", className)} {...props}>
|
||||
{answers.map((answer) => (
|
||||
<MainButton
|
||||
key={answer.id}
|
||||
{...answer}
|
||||
onClick={() => onAnswerClick(answer)}
|
||||
active={activeAnswers?.includes(answer)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { AnswersList };
|
||||
@ -0,0 +1,53 @@
|
||||
import { Meta, StoryObj } from "@storybook/nextjs-vite";
|
||||
import { BottomActionButton } from "./BottomActionButton";
|
||||
import Typography from "@/components/ui/Typography/Typography";
|
||||
|
||||
/** Reusable BottomActionButton Component */
|
||||
const meta: Meta<typeof BottomActionButton> = {
|
||||
title: "Widgets/BottomActionButton",
|
||||
component: BottomActionButton,
|
||||
tags: ["autodocs"],
|
||||
args: {
|
||||
actionButtonProps: {
|
||||
children: "Continue",
|
||||
},
|
||||
},
|
||||
argTypes: {},
|
||||
render: (args) => (
|
||||
<div className="relative w-full h-full">
|
||||
<Typography as="p" size="lg">
|
||||
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Voluptates
|
||||
ipsam ea sit libero minus cum consequatur. Atque reprehenderit
|
||||
perspiciatis dolores accusamus hic, sapiente veniam ex dolorem ullam.
|
||||
Aspernatur, vitae debitis.
|
||||
</Typography>
|
||||
<br />
|
||||
<Typography as="p" size="lg">
|
||||
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Voluptates
|
||||
ipsam ea sit libero minus cum consequatur. Atque reprehenderit
|
||||
perspiciatis dolores accusamus hic, sapiente veniam ex dolorem ullam.
|
||||
Aspernatur, vitae debitis.
|
||||
</Typography>
|
||||
<br />
|
||||
<Typography as="p" size="lg">
|
||||
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Voluptates
|
||||
ipsam ea sit libero minus cum consequatur. Atque reprehenderit
|
||||
perspiciatis dolores accusamus hic, sapiente veniam ex dolorem ullam.
|
||||
Aspernatur, vitae debitis.
|
||||
</Typography>
|
||||
<br />
|
||||
<Typography as="p" size="lg">
|
||||
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Voluptates
|
||||
ipsam ea sit libero minus cum consequatur. Atque reprehenderit
|
||||
perspiciatis dolores accusamus hic, sapiente veniam ex dolorem ullam.
|
||||
Aspernatur, vitae debitis.
|
||||
</Typography>
|
||||
<BottomActionButton {...args} />
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default = {} satisfies Story;
|
||||
@ -0,0 +1,31 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { GradientBlur } from "../GradientBlur/GradientBlur";
|
||||
import { ActionButton } from "@/components/ui/ActionButton/ActionButton";
|
||||
|
||||
export interface BottomActionButtonProps extends React.ComponentProps<"div"> {
|
||||
actionButtonProps?: React.ComponentProps<typeof ActionButton>;
|
||||
}
|
||||
|
||||
function BottomActionButton({
|
||||
actionButtonProps,
|
||||
className,
|
||||
...props
|
||||
}: BottomActionButtonProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"fixed bottom-0 left-[50%] translate-x-[-50%] w-full",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<GradientBlur className="p-6 pt-11">
|
||||
<ActionButton {...actionButtonProps} />
|
||||
</GradientBlur>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { BottomActionButton };
|
||||
44
src/components/widgets/Coupon/Coupon.stories.tsx
Normal file
44
src/components/widgets/Coupon/Coupon.stories.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
import { Meta, StoryObj } from "@storybook/nextjs-vite";
|
||||
import { Coupon } from "./Coupon";
|
||||
import { fn } from "storybook/test";
|
||||
import Typography from "@/components/ui/Typography/Typography";
|
||||
|
||||
/** Reusable Coupon Component */
|
||||
const meta: Meta<typeof Coupon> = {
|
||||
title: "Widgets/Coupon",
|
||||
component: Coupon,
|
||||
tags: ["autodocs"],
|
||||
args: {
|
||||
title: {
|
||||
children: "Special Offer",
|
||||
},
|
||||
offer: {
|
||||
title: {
|
||||
children: "94% OFF",
|
||||
},
|
||||
description: {
|
||||
children: "Одноразовая эксклюзивная скидка",
|
||||
},
|
||||
},
|
||||
promoCode: {
|
||||
children: "HAIR50",
|
||||
},
|
||||
footer: {
|
||||
children: (
|
||||
<>
|
||||
Скопируйте или нажмите{" "}
|
||||
<Typography size="sm" weight="bold" className="inline text-[#9CA3AF]">
|
||||
Continue
|
||||
</Typography>
|
||||
</>
|
||||
),
|
||||
},
|
||||
onCopyPromoCode: fn(),
|
||||
},
|
||||
argTypes: {},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default = {} satisfies Story;
|
||||
137
src/components/widgets/Coupon/Coupon.tsx
Normal file
137
src/components/widgets/Coupon/Coupon.tsx
Normal file
@ -0,0 +1,137 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import Typography, {
|
||||
TypographyProps,
|
||||
} from "@/components/ui/Typography/Typography";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Files } from "lucide-react";
|
||||
|
||||
interface CouponProps extends Omit<React.ComponentProps<"div">, "title"> {
|
||||
title: TypographyProps<"h3">;
|
||||
offer: {
|
||||
title: TypographyProps<"h3">;
|
||||
description: TypographyProps<"p">;
|
||||
};
|
||||
promoCode: TypographyProps<"span">;
|
||||
footer: TypographyProps<"p">;
|
||||
onCopyPromoCode: (code: string) => void;
|
||||
}
|
||||
|
||||
function Coupon({
|
||||
className,
|
||||
title,
|
||||
offer,
|
||||
promoCode,
|
||||
footer,
|
||||
onCopyPromoCode,
|
||||
...props
|
||||
}: CouponProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"relative",
|
||||
"overflow-hidden",
|
||||
"w-full max-w-[342px]",
|
||||
"px-[32px] pb-3 pt-[34px]",
|
||||
"flex flex-col items-center",
|
||||
"bg-gradient-to-r from-[#1F2937] to-[#374151]",
|
||||
"rounded-3xl",
|
||||
"shadow-coupon",
|
||||
"**:z-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<svg
|
||||
width="30"
|
||||
height="31"
|
||||
viewBox="0 0 30 31"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M11.1621 4.78125L13.2012 8.25H13.125H8.90625C7.61133 8.25 6.5625 7.20117 6.5625 5.90625C6.5625 4.61133 7.61133 3.5625 8.90625 3.5625H9.03516C9.9082 3.5625 10.7227 4.02539 11.1621 4.78125ZM3.75 5.90625C3.75 6.75 3.95508 7.54688 4.3125 8.25H1.875C0.837891 8.25 0 9.08789 0 10.125V13.875C0 14.9121 0.837891 15.75 1.875 15.75H28.125C29.1621 15.75 30 14.9121 30 13.875V10.125C30 9.08789 29.1621 8.25 28.125 8.25H25.6875C26.0449 7.54688 26.25 6.75 26.25 5.90625C26.25 3.05859 23.9414 0.75 21.0938 0.75H20.9648C19.0957 0.75 17.3613 1.74023 16.4121 3.35156L15 5.75977L13.5879 3.35742C12.6387 1.74023 10.9043 0.75 9.03516 0.75H8.90625C6.05859 0.75 3.75 3.05859 3.75 5.90625ZM23.4375 5.90625C23.4375 7.20117 22.3887 8.25 21.0938 8.25H16.875H16.7988L18.8379 4.78125C19.2832 4.02539 20.0918 3.5625 20.9648 3.5625H21.0938C22.3887 3.5625 23.4375 4.61133 23.4375 5.90625ZM1.875 17.625V27.9375C1.875 29.4902 3.13477 30.75 4.6875 30.75H13.125V17.625H1.875ZM16.875 30.75H25.3125C26.8652 30.75 28.125 29.4902 28.125 27.9375V17.625H16.875V30.75Z"
|
||||
fill="#FCD34D"
|
||||
/>
|
||||
</svg>
|
||||
<Typography
|
||||
as="h3"
|
||||
size="xl"
|
||||
weight="bold"
|
||||
color="primary"
|
||||
{...title}
|
||||
className={cn(title.className, "leading-[140%]")}
|
||||
/>
|
||||
{offer && (
|
||||
<div
|
||||
className={cn(
|
||||
"w-full",
|
||||
"bg-gradient-to-r from-[#FCD34D] to-[#F59E0B]",
|
||||
"rounded-2xl",
|
||||
"px-5 py-4 mt-3.5"
|
||||
)}
|
||||
>
|
||||
<Typography
|
||||
as="h3"
|
||||
size="4xl"
|
||||
weight="black"
|
||||
color="card"
|
||||
{...offer.title}
|
||||
/>
|
||||
<Typography
|
||||
as="p"
|
||||
weight="semiBold"
|
||||
color="card"
|
||||
{...offer.description}
|
||||
className={cn(
|
||||
"text-[17px] leading-[100%]",
|
||||
"mt-2",
|
||||
offer.description.className
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
"relative overflow-hidden",
|
||||
"h-5",
|
||||
"mt-[34px] py-2.5 px-8",
|
||||
"before:content-[''] before:absolute before:top-[50%] before:left-0 before:size-5 before:bg-[#f8fafc] before:rounded-full before:translate-[-50%]",
|
||||
"after:content-[''] after:absolute after:top-[50%] after:right-0 after:size-5 after:bg-[#f8fafc] after:rounded-full after:translate-x-[50%] after:translate-y-[-50%]"
|
||||
)}
|
||||
style={{ width: "calc(100% + 32px * 2)" }}
|
||||
>
|
||||
<Separator className="bg-[#4B5563]" decorative />
|
||||
</div>
|
||||
{promoCode && (
|
||||
<div
|
||||
className="w-full flex items-center justify-center gap-2 mt2 cursor-pointer"
|
||||
onClick={() => onCopyPromoCode(promoCode.children as string)}
|
||||
>
|
||||
<Typography
|
||||
size="lg"
|
||||
weight="semiBold"
|
||||
{...promoCode}
|
||||
className={cn(promoCode.className, "text-[#FCD34D]")}
|
||||
/>
|
||||
<Files color="#FCD34D" size={25} />
|
||||
</div>
|
||||
)}
|
||||
{footer && (
|
||||
<Typography
|
||||
as="p"
|
||||
size="sm"
|
||||
color="muted"
|
||||
{...footer}
|
||||
className={cn(footer.className, "w-full mt-1 mb-5 text-[#9CA3AF]")}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="absolute right-[-16px] top-[-16px] size-16 rounded-full bg-[#5c594a] z-0!" />
|
||||
<div className="absolute left-[-24px] bottom-[-26px] size-20 rounded-full bg-[#24344c] z-0!" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { Coupon };
|
||||
179
src/components/widgets/GradientBlur/GradientBlur.module.css
Normal file
179
src/components/widgets/GradientBlur/GradientBlur.module.css
Normal file
@ -0,0 +1,179 @@
|
||||
.gradientContainer {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
text-align: -webkit-center;
|
||||
}
|
||||
|
||||
.gradientContainer > * {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.gradientContainer .gradientBlur {
|
||||
position: absolute;
|
||||
z-index: 5;
|
||||
inset: auto 0 0 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: -16px;
|
||||
top: -48px;
|
||||
height: auto;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.gradientContainer .gradientBlur > div,
|
||||
.gradientContainer .gradientBlur::before,
|
||||
.gradientContainer .gradientBlur::after {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
.gradientContainer .gradientBlur::before {
|
||||
content: "";
|
||||
z-index: 1;
|
||||
-webkit-backdrop-filter: blur(0.5px);
|
||||
backdrop-filter: blur(0.5px);
|
||||
-webkit-mask: linear-gradient(
|
||||
to bottom,
|
||||
rgba(0, 0, 0, 0) 0%,
|
||||
black 12.5%,
|
||||
black 25%,
|
||||
rgba(0, 0, 0, 0) 37.5%
|
||||
);
|
||||
mask: linear-gradient(
|
||||
to bottom,
|
||||
rgba(0, 0, 0, 0) 0%,
|
||||
black 12.5%,
|
||||
black 25%,
|
||||
rgba(0, 0, 0, 0) 37.5%
|
||||
);
|
||||
}
|
||||
|
||||
.gradientContainer .gradientBlur > div:nth-of-type(1) {
|
||||
z-index: 2;
|
||||
-webkit-backdrop-filter: blur(1px);
|
||||
backdrop-filter: blur(1px);
|
||||
-webkit-mask: linear-gradient(
|
||||
to bottom,
|
||||
rgba(0, 0, 0, 0) 12.5%,
|
||||
black 25%,
|
||||
black 37.5%,
|
||||
rgba(0, 0, 0, 0) 50%
|
||||
);
|
||||
mask: linear-gradient(
|
||||
to bottom,
|
||||
rgba(0, 0, 0, 0) 12.5%,
|
||||
black 25%,
|
||||
black 37.5%,
|
||||
rgba(0, 0, 0, 0) 50%
|
||||
);
|
||||
}
|
||||
|
||||
.gradientContainer .gradientBlur > div:nth-of-type(2) {
|
||||
z-index: 3;
|
||||
-webkit-backdrop-filter: blur(2px);
|
||||
backdrop-filter: blur(2px);
|
||||
-webkit-mask: linear-gradient(
|
||||
to bottom,
|
||||
rgba(0, 0, 0, 0) 25%,
|
||||
black 37.5%,
|
||||
black 50%,
|
||||
rgba(0, 0, 0, 0) 62.5%
|
||||
);
|
||||
mask: linear-gradient(
|
||||
to bottom,
|
||||
rgba(0, 0, 0, 0) 25%,
|
||||
black 37.5%,
|
||||
black 50%,
|
||||
rgba(0, 0, 0, 0) 62.5%
|
||||
);
|
||||
}
|
||||
|
||||
.gradientContainer .gradientBlur > div:nth-of-type(3) {
|
||||
z-index: 4;
|
||||
-webkit-backdrop-filter: blur(4px);
|
||||
backdrop-filter: blur(4px);
|
||||
-webkit-mask: linear-gradient(
|
||||
to bottom,
|
||||
rgba(0, 0, 0, 0) 37.5%,
|
||||
black 50%,
|
||||
black 62.5%,
|
||||
rgba(0, 0, 0, 0) 75%
|
||||
);
|
||||
mask: linear-gradient(
|
||||
to bottom,
|
||||
rgba(0, 0, 0, 0) 37.5%,
|
||||
black 50%,
|
||||
black 62.5%,
|
||||
rgba(0, 0, 0, 0) 75%
|
||||
);
|
||||
}
|
||||
|
||||
.gradientContainer .gradientBlur > div:nth-of-type(4) {
|
||||
z-index: 5;
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-mask: linear-gradient(
|
||||
to bottom,
|
||||
rgba(0, 0, 0, 0) 50%,
|
||||
black 62.5%,
|
||||
black 75%,
|
||||
rgba(0, 0, 0, 0) 87.5%
|
||||
);
|
||||
mask: linear-gradient(
|
||||
to bottom,
|
||||
rgba(0, 0, 0, 0) 50%,
|
||||
black 62.5%,
|
||||
black 75%,
|
||||
rgba(0, 0, 0, 0) 87.5%
|
||||
);
|
||||
}
|
||||
|
||||
.gradientContainer .gradientBlur > div:nth-of-type(5) {
|
||||
z-index: 6;
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
backdrop-filter: blur(16px);
|
||||
-webkit-mask: linear-gradient(
|
||||
to bottom,
|
||||
rgba(0, 0, 0, 0) 62.5%,
|
||||
black 75%,
|
||||
black 87.5%,
|
||||
rgba(0, 0, 0, 0) 100%
|
||||
);
|
||||
mask: linear-gradient(
|
||||
to bottom,
|
||||
rgba(0, 0, 0, 0) 62.5%,
|
||||
black 75%,
|
||||
black 87.5%,
|
||||
rgba(0, 0, 0, 0) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.gradientContainer .gradientBlur > div:nth-of-type(6) {
|
||||
z-index: 7;
|
||||
-webkit-backdrop-filter: blur(32px);
|
||||
backdrop-filter: blur(32px);
|
||||
-webkit-mask: linear-gradient(
|
||||
to bottom,
|
||||
rgba(0, 0, 0, 0) 75%,
|
||||
black 87.5%,
|
||||
black 100%
|
||||
);
|
||||
mask: linear-gradient(
|
||||
to bottom,
|
||||
rgba(0, 0, 0, 0) 75%,
|
||||
black 87.5%,
|
||||
black 100%
|
||||
);
|
||||
}
|
||||
|
||||
.gradientContainer .gradientBlur::after {
|
||||
content: "";
|
||||
z-index: 8;
|
||||
-webkit-backdrop-filter: blur(64px);
|
||||
backdrop-filter: blur(64px);
|
||||
-webkit-mask: linear-gradient(to bottom, rgba(0, 0, 0, 0) 87.5%, black 100%);
|
||||
mask: linear-gradient(to bottom, rgba(0, 0, 0, 0) 87.5%, black 100%);
|
||||
}
|
||||
52
src/components/widgets/GradientBlur/GradientBlur.stories.tsx
Normal file
52
src/components/widgets/GradientBlur/GradientBlur.stories.tsx
Normal file
@ -0,0 +1,52 @@
|
||||
import { Meta, StoryObj } from "@storybook/nextjs-vite";
|
||||
import { GradientBlur } from "./GradientBlur";
|
||||
import Typography from "@/components/ui/Typography/Typography";
|
||||
|
||||
/** Reusable GradientBlur Component */
|
||||
const meta: Meta<typeof GradientBlur> = {
|
||||
title: "Widgets/GradientBlur",
|
||||
component: GradientBlur,
|
||||
tags: ["autodocs"],
|
||||
args: {
|
||||
children: (
|
||||
<div className="w-[200px] mt-[30px] text-center p-8 bg-secondary h-[100px]">
|
||||
Children
|
||||
</div>
|
||||
),
|
||||
},
|
||||
argTypes: {},
|
||||
render: (args) => (
|
||||
<div className="relative w-full h-full">
|
||||
<Typography as="p" size="lg">
|
||||
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Voluptates
|
||||
ipsam ea sit libero minus cum consequatur. Atque reprehenderit
|
||||
perspiciatis dolores accusamus hic, sapiente veniam ex dolorem ullam.
|
||||
Aspernatur, vitae debitis.
|
||||
</Typography>
|
||||
<br />
|
||||
<Typography as="p" size="lg">
|
||||
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Voluptates
|
||||
ipsam ea sit libero minus cum consequatur. Atque reprehenderit
|
||||
perspiciatis dolores accusamus hic, sapiente veniam ex dolorem ullam.
|
||||
Aspernatur, vitae debitis.
|
||||
</Typography>
|
||||
<Typography as="p" size="lg">
|
||||
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Voluptates
|
||||
ipsam ea sit libero minus cum consequatur. Atque reprehenderit
|
||||
perspiciatis dolores accusamus hic, sapiente veniam ex dolorem ullam.
|
||||
Aspernatur, vitae debitis.
|
||||
</Typography>
|
||||
<div className="fixed bottom-0 left-0 right-0">
|
||||
<GradientBlur
|
||||
{...args}
|
||||
className="py-5"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default = {} satisfies Story;
|
||||
35
src/components/widgets/GradientBlur/GradientBlur.tsx
Normal file
35
src/components/widgets/GradientBlur/GradientBlur.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import styles from "./GradientBlur.module.css";
|
||||
|
||||
interface GradientBlurProps extends React.ComponentProps<"div"> {
|
||||
gradientClassName?: string;
|
||||
isActiveBlur?: boolean;
|
||||
}
|
||||
|
||||
function GradientBlur({
|
||||
className,
|
||||
children,
|
||||
gradientClassName,
|
||||
isActiveBlur = true,
|
||||
...props
|
||||
}: GradientBlurProps) {
|
||||
return (
|
||||
<div className={cn(styles.gradientContainer, className)} {...props}>
|
||||
{children}
|
||||
{isActiveBlur && (
|
||||
<div className={cn(styles.gradientBlur, gradientClassName)}>
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { GradientBlur };
|
||||
@ -0,0 +1,43 @@
|
||||
import { Meta, StoryObj } from "@storybook/nextjs-vite";
|
||||
import { RadioAnswersList } from "./RadioAnswersList";
|
||||
import { fn } from "storybook/test";
|
||||
|
||||
/** Reusable RadioAnswersList Component */
|
||||
const meta: Meta<typeof RadioAnswersList> = {
|
||||
title: "Widgets/RadioAnswersList",
|
||||
component: RadioAnswersList,
|
||||
tags: ["autodocs"],
|
||||
args: {
|
||||
answers: [
|
||||
{
|
||||
children: "FEMALE",
|
||||
emoji: "👩",
|
||||
id: "female",
|
||||
},
|
||||
{
|
||||
children: "MALE",
|
||||
emoji: "👨",
|
||||
isCheckbox: true,
|
||||
id: "male",
|
||||
},
|
||||
{
|
||||
children: "Receding hairline, want to slow its progress",
|
||||
id: "without-emoji",
|
||||
},
|
||||
],
|
||||
activeAnswer: {
|
||||
children: "MALE",
|
||||
emoji: "👨",
|
||||
isCheckbox: true,
|
||||
id: "male",
|
||||
},
|
||||
onChangeSelectedAnswer: fn(),
|
||||
onAnswerClick: fn(),
|
||||
},
|
||||
argTypes: {},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default = {} satisfies Story;
|
||||
53
src/components/widgets/RadioAnswersList/RadioAnswersList.tsx
Normal file
53
src/components/widgets/RadioAnswersList/RadioAnswersList.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
MainButton,
|
||||
MainButtonProps,
|
||||
} from "@/components/ui/MainButton/MainButton";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export interface RadioAnswersListProps extends React.ComponentProps<"div"> {
|
||||
answers: MainButtonProps[];
|
||||
activeAnswer: MainButtonProps | null;
|
||||
onAnswerClick?: (answer: MainButtonProps | null) => void;
|
||||
onChangeSelectedAnswer?: (answer: MainButtonProps | null) => void;
|
||||
}
|
||||
|
||||
function RadioAnswersList({
|
||||
className,
|
||||
answers,
|
||||
activeAnswer,
|
||||
onAnswerClick,
|
||||
onChangeSelectedAnswer,
|
||||
...props
|
||||
}: RadioAnswersListProps) {
|
||||
const [selectedAnswer, setSelectedAnswer] = useState<MainButtonProps | null>(
|
||||
activeAnswer
|
||||
);
|
||||
|
||||
const handleAnswerClick = (answer: MainButtonProps) => {
|
||||
setSelectedAnswer(answer);
|
||||
onAnswerClick?.(answer);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
onChangeSelectedAnswer?.(selectedAnswer);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedAnswer]);
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-3 w-full", className)} {...props}>
|
||||
{answers.map((answer) => (
|
||||
<MainButton
|
||||
key={answer.id}
|
||||
{...answer}
|
||||
onClick={() => handleAnswerClick(answer)}
|
||||
active={selectedAnswer?.id === answer.id}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { RadioAnswersList };
|
||||
@ -0,0 +1,58 @@
|
||||
import { Meta, StoryObj } from "@storybook/nextjs-vite";
|
||||
import { SelectAnswersList } from "./SelectAnswersList";
|
||||
import { fn } from "storybook/test";
|
||||
|
||||
/** Reusable SelectAnswersList Component */
|
||||
const meta: Meta<typeof SelectAnswersList> = {
|
||||
title: "Widgets/SelectAnswersList",
|
||||
component: SelectAnswersList,
|
||||
tags: ["autodocs"],
|
||||
args: {
|
||||
answers: [
|
||||
{
|
||||
children: "Receding hairline, want to slow its progress",
|
||||
isCheckbox: true,
|
||||
id: "hairline",
|
||||
},
|
||||
{
|
||||
children: "Experiencing hair loss, exploring",
|
||||
isCheckbox: true,
|
||||
id: "exploring",
|
||||
},
|
||||
{
|
||||
children: "Experiencing hair loss, ready to start",
|
||||
isCheckbox: true,
|
||||
id: "ready-to-start",
|
||||
},
|
||||
{
|
||||
children: "Experiencing hair loss, ready to start",
|
||||
id: "ready-to-start-text",
|
||||
},
|
||||
{
|
||||
children: "Experiencing hair loss, ready to start",
|
||||
emoji: "👩🏼",
|
||||
id: "ready-to-start-emoji",
|
||||
},
|
||||
{
|
||||
children: "Experiencing hair loss, ready to start",
|
||||
emoji: "👩🏼",
|
||||
isCheckbox: true,
|
||||
id: "ready-to-start-emoji-checkbox",
|
||||
},
|
||||
],
|
||||
activeAnswers: [
|
||||
{
|
||||
children: "Experiencing hair loss, ready to start",
|
||||
isCheckbox: true,
|
||||
id: "ready-to-start",
|
||||
},
|
||||
],
|
||||
onChangeSelectedAnswers: fn(),
|
||||
},
|
||||
argTypes: {},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default = {} satisfies Story;
|
||||
@ -0,0 +1,59 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
MainButton,
|
||||
MainButtonProps,
|
||||
} from "@/components/ui/MainButton/MainButton";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export interface SelectAnswersListProps extends React.ComponentProps<"div"> {
|
||||
answers: MainButtonProps[];
|
||||
activeAnswers: MainButtonProps[] | null;
|
||||
onChangeSelectedAnswers?: (answers: MainButtonProps[] | null) => void;
|
||||
onAnswerClick?: (answer: MainButtonProps | null) => void;
|
||||
}
|
||||
|
||||
function SelectAnswersList({
|
||||
className,
|
||||
answers,
|
||||
activeAnswers,
|
||||
onChangeSelectedAnswers,
|
||||
onAnswerClick,
|
||||
...props
|
||||
}: SelectAnswersListProps) {
|
||||
const [selectedAnswers, setSelectedAnswers] = useState<
|
||||
MainButtonProps[] | null
|
||||
>(activeAnswers);
|
||||
|
||||
const handleAnswerClick = (answer: MainButtonProps) => {
|
||||
if (selectedAnswers?.some((a) => a.id === answer.id)) {
|
||||
setSelectedAnswers(
|
||||
(prev) => prev?.filter((a) => a.id !== answer.id) || null
|
||||
);
|
||||
} else {
|
||||
setSelectedAnswers((prev) => [...(prev || []), answer]);
|
||||
}
|
||||
onAnswerClick?.(answer);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
onChangeSelectedAnswers?.(selectedAnswers);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedAnswers]);
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-3 w-full", className)} {...props}>
|
||||
{answers.map((answer) => (
|
||||
<MainButton
|
||||
key={answer.id}
|
||||
{...answer}
|
||||
onClick={() => handleAnswerClick(answer)}
|
||||
active={selectedAnswers?.some((a) => a.id === answer.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { SelectAnswersList };
|
||||
6
src/lib/utils.ts
Normal file
6
src/lib/utils.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
35
vitest.config.ts
Normal file
35
vitest.config.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
import { storybookTest } from '@storybook/addon-vitest/vitest-plugin';
|
||||
|
||||
const dirname =
|
||||
typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
// More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon
|
||||
export default defineConfig({
|
||||
test: {
|
||||
projects: [
|
||||
{
|
||||
extends: true,
|
||||
plugins: [
|
||||
// The plugin will run tests for the stories defined in your Storybook config
|
||||
// See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest
|
||||
storybookTest({ configDir: path.join(dirname, '.storybook') }),
|
||||
],
|
||||
test: {
|
||||
name: 'storybook',
|
||||
browser: {
|
||||
enabled: true,
|
||||
headless: true,
|
||||
provider: 'playwright',
|
||||
instances: [{ browser: 'chromium' }]
|
||||
},
|
||||
setupFiles: ['.storybook/vitest.setup.ts'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
1
vitest.shims.d.ts
vendored
Normal file
1
vitest.shims.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="@vitest/browser/providers/playwright" />
|
||||
Loading…
Reference in New Issue
Block a user