create-modern-react 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +270 -72
  2. package/bin/index.js +13 -13
  3. package/lib/install.js +103 -32
  4. package/lib/prompts.js +152 -179
  5. package/lib/setup.js +267 -159
  6. package/package.json +17 -8
  7. package/templates/base/.env.example +9 -0
  8. package/templates/base/.eslintrc.cjs +37 -0
  9. package/templates/base/.prettierrc +11 -0
  10. package/templates/base/components.json +17 -0
  11. package/templates/base/index.html +2 -1
  12. package/templates/base/package.json +33 -14
  13. package/templates/base/postcss.config.js +6 -0
  14. package/templates/base/src/App.tsx +5 -18
  15. package/templates/base/src/components/layout/error-boundary.tsx +60 -0
  16. package/templates/base/src/components/layout/index.ts +2 -0
  17. package/templates/base/src/components/layout/root-layout.tsx +36 -0
  18. package/templates/base/src/components/ui/button.tsx +55 -0
  19. package/templates/base/src/components/ui/card.tsx +85 -0
  20. package/templates/base/src/components/ui/index.ts +12 -0
  21. package/templates/base/src/components/ui/input.tsx +24 -0
  22. package/templates/base/src/components/ui/separator.tsx +29 -0
  23. package/templates/base/src/components/ui/skeleton.tsx +15 -0
  24. package/templates/base/src/hooks/index.ts +3 -0
  25. package/templates/base/src/hooks/use-cancel-token.ts +63 -0
  26. package/templates/base/src/hooks/use-debounce.ts +29 -0
  27. package/templates/base/src/hooks/use-loader.ts +39 -0
  28. package/templates/base/src/index.css +73 -60
  29. package/templates/base/src/lib/utils.ts +14 -0
  30. package/templates/base/src/main.tsx +6 -6
  31. package/templates/base/src/providers/index.tsx +27 -0
  32. package/templates/base/src/providers/theme-provider.tsx +92 -0
  33. package/templates/base/src/routes/index.tsx +40 -0
  34. package/templates/base/src/routes/routes.ts +36 -0
  35. package/templates/base/src/screens/home/index.tsx +132 -0
  36. package/templates/base/src/screens/not-found/index.tsx +29 -0
  37. package/templates/base/src/services/alertify-services.ts +133 -0
  38. package/templates/base/src/services/api/api-helpers.ts +130 -0
  39. package/templates/base/src/services/api/axios-instance.ts +77 -0
  40. package/templates/base/src/services/api/index.ts +9 -0
  41. package/templates/base/src/services/index.ts +2 -0
  42. package/templates/base/src/types/index.ts +55 -0
  43. package/templates/base/src/vite-env.d.ts +31 -0
  44. package/templates/base/tailwind.config.js +77 -0
  45. package/templates/base/tsconfig.json +4 -3
  46. package/templates/base/tsconfig.node.json +22 -0
  47. package/templates/base/vite.config.ts +65 -4
  48. package/templates/optional/antd/config-provider.tsx +33 -0
  49. package/templates/optional/antd/index.ts +2 -0
  50. package/templates/optional/antd/styles/antd-overrides.css +104 -0
  51. package/templates/optional/antd/theme.ts +75 -0
  52. package/templates/optional/husky/.husky/pre-commit +1 -0
  53. package/templates/optional/husky/.lintstagedrc.json +6 -0
  54. package/templates/optional/redux/hooks.ts +17 -0
  55. package/templates/optional/redux/index.ts +13 -0
  56. package/templates/optional/redux/provider.tsx +33 -0
  57. package/templates/optional/redux/store/index.ts +45 -0
  58. package/templates/optional/redux/store/slices/app-slice.ts +62 -0
  59. package/templates/base/src/App.css +0 -14
@@ -0,0 +1,130 @@
1
+ import { AxiosRequestConfig, CancelToken, AxiosError } from 'axios';
2
+ import axiosInstance from './axios-instance';
3
+
4
+ type Headers = Record<string, string>;
5
+
6
+ interface ApiResponse<T> {
7
+ data: T;
8
+ status: number;
9
+ message?: string;
10
+ }
11
+
12
+ /**
13
+ * POST request helper
14
+ */
15
+ export async function postApi<T>(
16
+ path: string,
17
+ data?: unknown,
18
+ headers?: Headers,
19
+ cancelToken?: CancelToken,
20
+ config?: AxiosRequestConfig
21
+ ): Promise<ApiResponse<T>> {
22
+ const response = await axiosInstance.post<T>(path, data, {
23
+ headers,
24
+ cancelToken,
25
+ ...config,
26
+ });
27
+ return {
28
+ data: response.data,
29
+ status: response.status,
30
+ };
31
+ }
32
+
33
+ /**
34
+ * GET request helper
35
+ */
36
+ export async function getApi<T>(
37
+ path: string,
38
+ headers?: Headers,
39
+ cancelToken?: CancelToken,
40
+ config?: AxiosRequestConfig
41
+ ): Promise<ApiResponse<T>> {
42
+ const response = await axiosInstance.get<T>(path, {
43
+ headers,
44
+ cancelToken,
45
+ ...config,
46
+ });
47
+ return {
48
+ data: response.data,
49
+ status: response.status,
50
+ };
51
+ }
52
+
53
+ /**
54
+ * PATCH request helper
55
+ */
56
+ export async function patchApi<T>(
57
+ path: string,
58
+ data?: unknown,
59
+ headers?: Headers,
60
+ config?: AxiosRequestConfig
61
+ ): Promise<ApiResponse<T>> {
62
+ const response = await axiosInstance.patch<T>(path, data, {
63
+ headers,
64
+ ...config,
65
+ });
66
+ return {
67
+ data: response.data,
68
+ status: response.status,
69
+ };
70
+ }
71
+
72
+ /**
73
+ * PUT request helper
74
+ */
75
+ export async function putApi<T>(
76
+ path: string,
77
+ data?: unknown,
78
+ headers?: Headers,
79
+ config?: AxiosRequestConfig
80
+ ): Promise<ApiResponse<T>> {
81
+ const response = await axiosInstance.put<T>(path, data, {
82
+ headers,
83
+ ...config,
84
+ });
85
+ return {
86
+ data: response.data,
87
+ status: response.status,
88
+ };
89
+ }
90
+
91
+ /**
92
+ * DELETE request helper
93
+ */
94
+ export async function deleteApi<T>(
95
+ path: string,
96
+ data?: unknown,
97
+ headers?: Headers,
98
+ cancelToken?: CancelToken,
99
+ config?: AxiosRequestConfig
100
+ ): Promise<ApiResponse<T>> {
101
+ const response = await axiosInstance.delete<T>(path, {
102
+ headers,
103
+ data,
104
+ cancelToken,
105
+ ...config,
106
+ });
107
+ return {
108
+ data: response.data,
109
+ status: response.status,
110
+ };
111
+ }
112
+
113
+ /**
114
+ * Error handler for Redux async thunks
115
+ * Extracts error message and returns it via rejectWithValue
116
+ */
117
+ export function handleApiError(
118
+ error: unknown,
119
+ rejectWithValue: (value: string) => unknown
120
+ ) {
121
+ if (error instanceof AxiosError) {
122
+ const message =
123
+ error.response?.data?.message ||
124
+ error.response?.data?.error ||
125
+ error.message ||
126
+ 'An unexpected error occurred';
127
+ return rejectWithValue(message);
128
+ }
129
+ return rejectWithValue('An unexpected error occurred');
130
+ }
@@ -0,0 +1,77 @@
1
+ import axios, { AxiosError, InternalAxiosRequestConfig } from 'axios';
2
+
3
+ const BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000/api';
4
+
5
+ /**
6
+ * Configured Axios instance with interceptors
7
+ * - Automatically adds auth token from localStorage
8
+ * - Handles 401 errors (token refresh/logout)
9
+ * - Supports request cancellation
10
+ */
11
+ export const axiosInstance = axios.create({
12
+ baseURL: BASE_URL,
13
+ timeout: 30000,
14
+ headers: {
15
+ 'Content-Type': 'application/json',
16
+ },
17
+ });
18
+
19
+ // Request interceptor - Add auth token
20
+ axiosInstance.interceptors.request.use(
21
+ (config: InternalAxiosRequestConfig) => {
22
+ const token = localStorage.getItem('accessToken');
23
+ if (token && config.headers) {
24
+ config.headers.Authorization = `Bearer ${token}`;
25
+ }
26
+ return config;
27
+ },
28
+ (error: AxiosError) => {
29
+ return Promise.reject(error);
30
+ }
31
+ );
32
+
33
+ // Response interceptor - Handle errors
34
+ axiosInstance.interceptors.response.use(
35
+ (response) => response,
36
+ async (error: AxiosError) => {
37
+ const originalRequest = error.config as InternalAxiosRequestConfig & {
38
+ _retry?: boolean;
39
+ };
40
+
41
+ // Handle 401 Unauthorized
42
+ if (error.response?.status === 401 && !originalRequest._retry) {
43
+ originalRequest._retry = true;
44
+
45
+ // Option 1: Try to refresh token
46
+ const refreshToken = localStorage.getItem('refreshToken');
47
+ if (refreshToken) {
48
+ try {
49
+ const response = await axios.post(`${BASE_URL}/auth/refresh`, {
50
+ refreshToken,
51
+ });
52
+
53
+ const { accessToken } = response.data;
54
+ localStorage.setItem('accessToken', accessToken);
55
+
56
+ if (originalRequest.headers) {
57
+ originalRequest.headers.Authorization = `Bearer ${accessToken}`;
58
+ }
59
+ return axiosInstance(originalRequest);
60
+ } catch {
61
+ // Refresh failed - clear tokens and redirect
62
+ localStorage.removeItem('accessToken');
63
+ localStorage.removeItem('refreshToken');
64
+ window.location.href = '/login';
65
+ }
66
+ } else {
67
+ // No refresh token - redirect to login
68
+ localStorage.removeItem('accessToken');
69
+ window.location.href = '/login';
70
+ }
71
+ }
72
+
73
+ return Promise.reject(error);
74
+ }
75
+ );
76
+
77
+ export default axiosInstance;
@@ -0,0 +1,9 @@
1
+ export { axiosInstance, default as axios } from './axios-instance';
2
+ export {
3
+ getApi,
4
+ postApi,
5
+ patchApi,
6
+ putApi,
7
+ deleteApi,
8
+ handleApiError,
9
+ } from './api-helpers';
@@ -0,0 +1,2 @@
1
+ export * from './api';
2
+ export { Alertify, default as alertify } from './alertify-services';
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Common TypeScript type definitions
3
+ * Add your application-wide types here
4
+ */
5
+
6
+ // API Response types
7
+ export interface ApiResponse<T> {
8
+ data: T;
9
+ status: number;
10
+ message?: string;
11
+ }
12
+
13
+ export interface PaginatedResponse<T> {
14
+ data: T[];
15
+ total: number;
16
+ page: number;
17
+ pageSize: number;
18
+ totalPages: number;
19
+ }
20
+
21
+ // Error types
22
+ export interface ApiError {
23
+ message: string;
24
+ code?: string;
25
+ details?: Record<string, unknown>;
26
+ }
27
+
28
+ // User types (example)
29
+ export interface User {
30
+ id: string;
31
+ email: string;
32
+ name: string;
33
+ avatar?: string;
34
+ createdAt: string;
35
+ updatedAt: string;
36
+ }
37
+
38
+ // Auth types
39
+ export interface AuthTokens {
40
+ accessToken: string;
41
+ refreshToken: string;
42
+ }
43
+
44
+ // Utility types
45
+ export type Nullable<T> = T | null;
46
+ export type Optional<T> = T | undefined;
47
+ export type ValueOf<T> = T[keyof T];
48
+
49
+ // Make all properties optional recursively
50
+ export type DeepPartial<T> = {
51
+ [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
52
+ };
53
+
54
+ // Make specific keys required
55
+ export type RequireKeys<T, K extends keyof T> = T & Required<Pick<T, K>>;
@@ -0,0 +1,31 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ // SVG as React Component
4
+ declare module '*.svg?react' {
5
+ import * as React from 'react';
6
+ const ReactComponent: React.FunctionComponent<
7
+ React.SVGProps<SVGSVGElement> & { title?: string }
8
+ >;
9
+ export default ReactComponent;
10
+ }
11
+
12
+ declare module '*.svg' {
13
+ import * as React from 'react';
14
+ export const ReactComponent: React.FunctionComponent<
15
+ React.SVGProps<SVGSVGElement> & { title?: string }
16
+ >;
17
+ const src: string;
18
+ export default src;
19
+ }
20
+
21
+ // Environment variables
22
+ interface ImportMetaEnv {
23
+ readonly VITE_API_URL: string;
24
+ readonly VITE_APP_NAME: string;
25
+ readonly VITE_APP_VERSION: string;
26
+ readonly VITE_ENABLE_DEVTOOLS: string;
27
+ }
28
+
29
+ interface ImportMeta {
30
+ readonly env: ImportMetaEnv;
31
+ }
@@ -0,0 +1,77 @@
1
+ /** @type {import('tailwindcss').Config} */
2
+ export default {
3
+ darkMode: ['class'],
4
+ content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
5
+ theme: {
6
+ extend: {
7
+ colors: {
8
+ border: 'hsl(var(--border))',
9
+ input: 'hsl(var(--input))',
10
+ ring: 'hsl(var(--ring))',
11
+ background: 'hsl(var(--background))',
12
+ foreground: 'hsl(var(--foreground))',
13
+ primary: {
14
+ DEFAULT: 'hsl(var(--primary))',
15
+ foreground: 'hsl(var(--primary-foreground))',
16
+ },
17
+ secondary: {
18
+ DEFAULT: 'hsl(var(--secondary))',
19
+ foreground: 'hsl(var(--secondary-foreground))',
20
+ },
21
+ destructive: {
22
+ DEFAULT: 'hsl(var(--destructive))',
23
+ foreground: 'hsl(var(--destructive-foreground))',
24
+ },
25
+ muted: {
26
+ DEFAULT: 'hsl(var(--muted))',
27
+ foreground: 'hsl(var(--muted-foreground))',
28
+ },
29
+ accent: {
30
+ DEFAULT: 'hsl(var(--accent))',
31
+ foreground: 'hsl(var(--accent-foreground))',
32
+ },
33
+ popover: {
34
+ DEFAULT: 'hsl(var(--popover))',
35
+ foreground: 'hsl(var(--popover-foreground))',
36
+ },
37
+ card: {
38
+ DEFAULT: 'hsl(var(--card))',
39
+ foreground: 'hsl(var(--card-foreground))',
40
+ },
41
+ },
42
+ borderRadius: {
43
+ lg: 'var(--radius)',
44
+ md: 'calc(var(--radius) - 2px)',
45
+ sm: 'calc(var(--radius) - 4px)',
46
+ },
47
+ fontFamily: {
48
+ sans: [
49
+ 'Inter',
50
+ 'system-ui',
51
+ '-apple-system',
52
+ 'BlinkMacSystemFont',
53
+ 'Segoe UI',
54
+ 'Roboto',
55
+ 'Helvetica Neue',
56
+ 'Arial',
57
+ 'sans-serif',
58
+ ],
59
+ },
60
+ keyframes: {
61
+ 'accordion-down': {
62
+ from: { height: '0' },
63
+ to: { height: 'var(--radix-accordion-content-height)' },
64
+ },
65
+ 'accordion-up': {
66
+ from: { height: 'var(--radix-accordion-content-height)' },
67
+ to: { height: '0' },
68
+ },
69
+ },
70
+ animation: {
71
+ 'accordion-down': 'accordion-down 0.2s ease-out',
72
+ 'accordion-up': 'accordion-up 0.2s ease-out',
73
+ },
74
+ },
75
+ },
76
+ plugins: [],
77
+ };
@@ -9,8 +9,8 @@
9
9
  /* Bundler mode */
10
10
  "moduleResolution": "bundler",
11
11
  "allowImportingTsExtensions": true,
12
- "resolveJsonModule": true,
13
12
  "isolatedModules": true,
13
+ "moduleDetection": "force",
14
14
  "noEmit": true,
15
15
  "jsx": "react-jsx",
16
16
 
@@ -19,11 +19,12 @@
19
19
  "noUnusedLocals": true,
20
20
  "noUnusedParameters": true,
21
21
  "noFallthroughCasesInSwitch": true,
22
+ "forceConsistentCasingInFileNames": true,
22
23
 
23
- /* Path mapping */
24
+ /* Path aliases */
24
25
  "baseUrl": ".",
25
26
  "paths": {
26
- "~/*": ["src/*"]
27
+ "~/*": ["./src/*"]
27
28
  }
28
29
  },
29
30
  "include": ["src"],
@@ -0,0 +1,22 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["ES2023"],
5
+ "module": "ESNext",
6
+ "skipLibCheck": true,
7
+
8
+ /* Bundler mode */
9
+ "moduleResolution": "bundler",
10
+ "allowImportingTsExtensions": true,
11
+ "isolatedModules": true,
12
+ "moduleDetection": "force",
13
+ "noEmit": true,
14
+
15
+ /* Linting */
16
+ "strict": true,
17
+ "noUnusedLocals": true,
18
+ "noUnusedParameters": true,
19
+ "noFallthroughCasesInSwitch": true
20
+ },
21
+ "include": ["vite.config.ts"]
22
+ }
@@ -1,12 +1,73 @@
1
- import { defineConfig } from "vite";
2
- import react from "@vitejs/plugin-react";
1
+ import { defineConfig } from 'vite';
2
+ import react from '@vitejs/plugin-react-swc';
3
+ import svgr from 'vite-plugin-svgr';
4
+ import compression from 'vite-plugin-compression';
5
+ import path from 'path';
3
6
 
4
7
  // https://vitejs.dev/config/
5
8
  export default defineConfig({
6
- plugins: [react()],
9
+ plugins: [
10
+ react(),
11
+ // Import SVGs as React components
12
+ // Usage: import { ReactComponent as Logo } from './logo.svg'
13
+ // Or: import Logo from './logo.svg?react'
14
+ svgr({
15
+ svgrOptions: {
16
+ // SVGR options
17
+ icon: true, // Replace width/height with 1em to make SVG scale with font-size
18
+ svgo: true, // Optimize SVGs
19
+ svgoConfig: {
20
+ plugins: [
21
+ {
22
+ name: 'preset-default',
23
+ params: {
24
+ overrides: {
25
+ removeViewBox: false, // Keep viewBox for proper scaling
26
+ },
27
+ },
28
+ },
29
+ ],
30
+ },
31
+ },
32
+ include: '**/*.svg',
33
+ }),
34
+ // Gzip compression for production builds
35
+ // Files larger than 1KB will be compressed
36
+ compression({
37
+ algorithm: 'gzip',
38
+ ext: '.gz',
39
+ threshold: 1024, // Only compress files larger than 1KB
40
+ deleteOriginFile: false, // Keep original files
41
+ verbose: true, // Log compression results
42
+ filter: /\.(js|css|html|json|svg)$/i, // File types to compress
43
+ }),
44
+ ],
7
45
  resolve: {
8
46
  alias: {
9
- "~": "/src",
47
+ '~': path.resolve(__dirname, './src'),
10
48
  },
11
49
  },
50
+ server: {
51
+ port: 3000,
52
+ open: true,
53
+ host: true,
54
+ },
55
+ build: {
56
+ minify: 'esbuild',
57
+ target: 'esnext',
58
+ sourcemap: false,
59
+ // Chunk splitting for better caching
60
+ rollupOptions: {
61
+ output: {
62
+ manualChunks: {
63
+ vendor: ['react', 'react-dom'],
64
+ router: ['wouter'],
65
+ },
66
+ },
67
+ },
68
+ },
69
+ esbuild: {
70
+ // Remove console.log and debugger in production
71
+ drop: process.env.NODE_ENV === 'production' ? ['console', 'debugger'] : [],
72
+ },
12
73
  });
@@ -0,0 +1,33 @@
1
+ import { ConfigProvider, App as AntdApp, theme as antdThemeAlgorithm } from 'antd';
2
+ import { useTheme } from '~/providers/theme-provider';
3
+ import { antdTheme, antdDarkTheme } from './theme';
4
+
5
+ interface AntdConfigProviderProps {
6
+ children: React.ReactNode;
7
+ }
8
+
9
+ /**
10
+ * Ant Design ConfigProvider wrapper
11
+ * Provides theme configuration and integrates with the app's theme system
12
+ *
13
+ * Uses the AntdApp wrapper which provides:
14
+ * - Static methods for message, notification, modal
15
+ * - Consistent styling context
16
+ */
17
+ export function AntdConfigProvider({ children }: AntdConfigProviderProps) {
18
+ const { resolvedTheme } = useTheme();
19
+ const isDark = resolvedTheme === 'dark';
20
+
21
+ return (
22
+ <ConfigProvider
23
+ theme={{
24
+ ...(isDark ? antdDarkTheme : antdTheme),
25
+ algorithm: isDark
26
+ ? antdThemeAlgorithm.darkAlgorithm
27
+ : antdThemeAlgorithm.defaultAlgorithm,
28
+ }}
29
+ >
30
+ <AntdApp>{children}</AntdApp>
31
+ </ConfigProvider>
32
+ );
33
+ }
@@ -0,0 +1,2 @@
1
+ export { AntdConfigProvider } from './config-provider';
2
+ export { antdTheme, antdDarkTheme } from './theme';
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Ant Design style overrides
3
+ * Use this file to customize Ant Design components with CSS
4
+ * Prefer using the theme config when possible
5
+ */
6
+
7
+ /* Make Ant Design work better with Tailwind's reset */
8
+ .ant-btn {
9
+ font-weight: 500;
10
+ }
11
+
12
+ /* Improve form spacing */
13
+ .ant-form-item {
14
+ margin-bottom: 16px;
15
+ }
16
+
17
+ .ant-form-item:last-child {
18
+ margin-bottom: 0;
19
+ }
20
+
21
+ /* Better table styling */
22
+ .ant-table {
23
+ border-radius: 8px;
24
+ overflow: hidden;
25
+ }
26
+
27
+ .ant-table-thead > tr > th {
28
+ font-weight: 600;
29
+ }
30
+
31
+ /* Card improvements */
32
+ .ant-card {
33
+ border-radius: 8px;
34
+ }
35
+
36
+ .ant-card-head {
37
+ min-height: 48px;
38
+ }
39
+
40
+ /* Modal improvements */
41
+ .ant-modal-content {
42
+ border-radius: 12px;
43
+ overflow: hidden;
44
+ }
45
+
46
+ .ant-modal-header {
47
+ border-bottom: 1px solid var(--ant-color-border-secondary);
48
+ }
49
+
50
+ .ant-modal-footer {
51
+ border-top: 1px solid var(--ant-color-border-secondary);
52
+ }
53
+
54
+ /* Dropdown improvements */
55
+ .ant-dropdown-menu {
56
+ border-radius: 8px;
57
+ padding: 4px;
58
+ }
59
+
60
+ .ant-dropdown-menu-item {
61
+ border-radius: 4px;
62
+ }
63
+
64
+ /* Select improvements */
65
+ .ant-select-dropdown {
66
+ border-radius: 8px;
67
+ padding: 4px;
68
+ }
69
+
70
+ .ant-select-item {
71
+ border-radius: 4px;
72
+ }
73
+
74
+ /* Date picker improvements */
75
+ .ant-picker-dropdown {
76
+ border-radius: 8px;
77
+ }
78
+
79
+ /* Message improvements */
80
+ .ant-message .ant-message-notice-content {
81
+ border-radius: 8px;
82
+ }
83
+
84
+ /* Notification improvements */
85
+ .ant-notification-notice {
86
+ border-radius: 8px;
87
+ }
88
+
89
+ /* Tag improvements */
90
+ .ant-tag {
91
+ border-radius: 4px;
92
+ }
93
+
94
+ /* Skeleton improvements for dark mode */
95
+ .dark .ant-skeleton-content .ant-skeleton-title,
96
+ .dark .ant-skeleton-content .ant-skeleton-paragraph > li {
97
+ background: linear-gradient(
98
+ 90deg,
99
+ rgba(255, 255, 255, 0.06) 25%,
100
+ rgba(255, 255, 255, 0.12) 37%,
101
+ rgba(255, 255, 255, 0.06) 63%
102
+ );
103
+ background-size: 400% 100%;
104
+ }