create-brainerce-store 1.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.
- package/dist/index.d.ts +1 -0
- package/dist/index.js +502 -0
- package/package.json +44 -0
- package/templates/nextjs/base/.env.local.ejs +3 -0
- package/templates/nextjs/base/.eslintrc.json +3 -0
- package/templates/nextjs/base/gitignore +30 -0
- package/templates/nextjs/base/next.config.ts +9 -0
- package/templates/nextjs/base/package.json.ejs +30 -0
- package/templates/nextjs/base/postcss.config.mjs +9 -0
- package/templates/nextjs/base/src/app/account/page.tsx +105 -0
- package/templates/nextjs/base/src/app/auth/callback/page.tsx +99 -0
- package/templates/nextjs/base/src/app/cart/page.tsx +263 -0
- package/templates/nextjs/base/src/app/checkout/page.tsx +463 -0
- package/templates/nextjs/base/src/app/globals.css +30 -0
- package/templates/nextjs/base/src/app/layout.tsx.ejs +33 -0
- package/templates/nextjs/base/src/app/login/page.tsx +56 -0
- package/templates/nextjs/base/src/app/order-confirmation/page.tsx +191 -0
- package/templates/nextjs/base/src/app/page.tsx +95 -0
- package/templates/nextjs/base/src/app/products/[slug]/page.tsx +346 -0
- package/templates/nextjs/base/src/app/products/page.tsx +243 -0
- package/templates/nextjs/base/src/app/register/page.tsx +66 -0
- package/templates/nextjs/base/src/app/verify-email/page.tsx +291 -0
- package/templates/nextjs/base/src/components/account/order-history.tsx +184 -0
- package/templates/nextjs/base/src/components/account/profile-section.tsx +73 -0
- package/templates/nextjs/base/src/components/auth/login-form.tsx +92 -0
- package/templates/nextjs/base/src/components/auth/oauth-buttons.tsx +134 -0
- package/templates/nextjs/base/src/components/auth/register-form.tsx +177 -0
- package/templates/nextjs/base/src/components/cart/cart-item.tsx +150 -0
- package/templates/nextjs/base/src/components/cart/cart-nudges.tsx +39 -0
- package/templates/nextjs/base/src/components/cart/cart-summary.tsx +67 -0
- package/templates/nextjs/base/src/components/cart/coupon-input.tsx +131 -0
- package/templates/nextjs/base/src/components/cart/reservation-countdown.tsx +100 -0
- package/templates/nextjs/base/src/components/checkout/checkout-form.tsx +273 -0
- package/templates/nextjs/base/src/components/checkout/payment-step.tsx +124 -0
- package/templates/nextjs/base/src/components/checkout/shipping-step.tsx +111 -0
- package/templates/nextjs/base/src/components/checkout/tax-display.tsx +62 -0
- package/templates/nextjs/base/src/components/layout/footer.tsx +35 -0
- package/templates/nextjs/base/src/components/layout/header.tsx +329 -0
- package/templates/nextjs/base/src/components/products/discount-badge.tsx +36 -0
- package/templates/nextjs/base/src/components/products/product-card.tsx +94 -0
- package/templates/nextjs/base/src/components/products/product-grid.tsx +33 -0
- package/templates/nextjs/base/src/components/products/stock-badge.tsx +34 -0
- package/templates/nextjs/base/src/components/products/variant-selector.tsx +147 -0
- package/templates/nextjs/base/src/components/shared/loading-spinner.tsx +30 -0
- package/templates/nextjs/base/src/components/shared/price-display.tsx +62 -0
- package/templates/nextjs/base/src/hooks/use-search.ts +77 -0
- package/templates/nextjs/base/src/lib/brainerce.ts.ejs +59 -0
- package/templates/nextjs/base/src/lib/utils.ts +6 -0
- package/templates/nextjs/base/src/providers/store-provider.tsx.ejs +168 -0
- package/templates/nextjs/base/tailwind.config.ts +30 -0
- package/templates/nextjs/base/tsconfig.json +23 -0
- package/templates/nextjs/themes/minimal/globals.css +30 -0
- package/templates/nextjs/themes/minimal/theme.json +23 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { cn } from '@/lib/utils';
|
|
4
|
+
|
|
5
|
+
interface LoadingSpinnerProps {
|
|
6
|
+
size?: 'sm' | 'md' | 'lg';
|
|
7
|
+
className?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const sizeClasses = {
|
|
11
|
+
sm: 'h-4 w-4 border-2',
|
|
12
|
+
md: 'h-8 w-8 border-2',
|
|
13
|
+
lg: 'h-12 w-12 border-3',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function LoadingSpinner({ size = 'md', className }: LoadingSpinnerProps) {
|
|
17
|
+
return (
|
|
18
|
+
<div
|
|
19
|
+
className={cn(
|
|
20
|
+
'border-muted-foreground/30 border-t-primary animate-spin rounded-full',
|
|
21
|
+
sizeClasses[size],
|
|
22
|
+
className
|
|
23
|
+
)}
|
|
24
|
+
role="status"
|
|
25
|
+
aria-label="Loading"
|
|
26
|
+
>
|
|
27
|
+
<span className="sr-only">Loading...</span>
|
|
28
|
+
</div>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { formatPrice } from 'brainerce';
|
|
4
|
+
import { useStoreInfo } from '@/providers/store-provider';
|
|
5
|
+
import { cn } from '@/lib/utils';
|
|
6
|
+
|
|
7
|
+
interface PriceDisplayProps {
|
|
8
|
+
price: string | number;
|
|
9
|
+
salePrice?: string | number | null;
|
|
10
|
+
currency?: string;
|
|
11
|
+
className?: string;
|
|
12
|
+
size?: 'sm' | 'md' | 'lg';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const sizeClasses = {
|
|
16
|
+
sm: 'text-sm',
|
|
17
|
+
md: 'text-base',
|
|
18
|
+
lg: 'text-xl font-semibold',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export function PriceDisplay({
|
|
22
|
+
price,
|
|
23
|
+
salePrice,
|
|
24
|
+
currency,
|
|
25
|
+
className,
|
|
26
|
+
size = 'md',
|
|
27
|
+
}: PriceDisplayProps) {
|
|
28
|
+
const { storeInfo } = useStoreInfo();
|
|
29
|
+
const currencyCode = currency || storeInfo?.currency || 'USD';
|
|
30
|
+
|
|
31
|
+
const basePrice = typeof price === 'string' ? parseFloat(price) : price;
|
|
32
|
+
const sale =
|
|
33
|
+
salePrice != null ? (typeof salePrice === 'string' ? parseFloat(salePrice) : salePrice) : null;
|
|
34
|
+
const isOnSale = sale !== null && sale < basePrice;
|
|
35
|
+
|
|
36
|
+
const discountPercent =
|
|
37
|
+
isOnSale && basePrice > 0 ? Math.round(((basePrice - sale!) / basePrice) * 100) : 0;
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<span className={cn('inline-flex items-center gap-2', sizeClasses[size], className)}>
|
|
41
|
+
{isOnSale ? (
|
|
42
|
+
<>
|
|
43
|
+
<span className="text-destructive font-medium">
|
|
44
|
+
{formatPrice(sale!, { currency: currencyCode }) as string}
|
|
45
|
+
</span>
|
|
46
|
+
<span className="text-muted-foreground text-[0.85em] line-through">
|
|
47
|
+
{formatPrice(basePrice, { currency: currencyCode }) as string}
|
|
48
|
+
</span>
|
|
49
|
+
{discountPercent > 0 && (
|
|
50
|
+
<span className="bg-destructive text-destructive-foreground rounded px-1.5 py-0.5 text-xs font-medium">
|
|
51
|
+
-{discountPercent}%
|
|
52
|
+
</span>
|
|
53
|
+
)}
|
|
54
|
+
</>
|
|
55
|
+
) : (
|
|
56
|
+
<span className="text-foreground font-medium">
|
|
57
|
+
{formatPrice(basePrice, { currency: currencyCode }) as string}
|
|
58
|
+
</span>
|
|
59
|
+
)}
|
|
60
|
+
</span>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
4
|
+
import type { SearchSuggestions } from 'brainerce';
|
|
5
|
+
import { getClient } from '@/lib/brainerce';
|
|
6
|
+
|
|
7
|
+
const DEBOUNCE_MS = 300;
|
|
8
|
+
const MIN_QUERY_LENGTH = 2;
|
|
9
|
+
const SUGGESTION_LIMIT = 5;
|
|
10
|
+
|
|
11
|
+
interface UseSearchResult {
|
|
12
|
+
suggestions: SearchSuggestions | null;
|
|
13
|
+
loading: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function useSearch(query: string): UseSearchResult {
|
|
17
|
+
const [suggestions, setSuggestions] = useState<SearchSuggestions | null>(null);
|
|
18
|
+
const [loading, setLoading] = useState(false);
|
|
19
|
+
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
20
|
+
const abortRef = useRef<AbortController | null>(null);
|
|
21
|
+
|
|
22
|
+
const fetchSuggestions = useCallback(async (searchQuery: string) => {
|
|
23
|
+
// Cancel any in-flight request
|
|
24
|
+
abortRef.current?.abort();
|
|
25
|
+
abortRef.current = new AbortController();
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
setLoading(true);
|
|
29
|
+
const client = getClient();
|
|
30
|
+
const result = await client.getSearchSuggestions(searchQuery, SUGGESTION_LIMIT);
|
|
31
|
+
setSuggestions(result);
|
|
32
|
+
} catch {
|
|
33
|
+
// Silently ignore errors (likely aborted or network issue)
|
|
34
|
+
setSuggestions(null);
|
|
35
|
+
} finally {
|
|
36
|
+
setLoading(false);
|
|
37
|
+
}
|
|
38
|
+
}, []);
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
// Clear previous debounce
|
|
42
|
+
if (debounceRef.current) {
|
|
43
|
+
clearTimeout(debounceRef.current);
|
|
44
|
+
debounceRef.current = null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Clear suggestions if query is too short
|
|
48
|
+
if (!query || query.trim().length < MIN_QUERY_LENGTH) {
|
|
49
|
+
setSuggestions(null);
|
|
50
|
+
setLoading(false);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Debounce the search
|
|
55
|
+
debounceRef.current = setTimeout(() => {
|
|
56
|
+
fetchSuggestions(query.trim());
|
|
57
|
+
}, DEBOUNCE_MS);
|
|
58
|
+
|
|
59
|
+
return () => {
|
|
60
|
+
if (debounceRef.current) {
|
|
61
|
+
clearTimeout(debounceRef.current);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}, [query, fetchSuggestions]);
|
|
65
|
+
|
|
66
|
+
// Cleanup on unmount
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
return () => {
|
|
69
|
+
abortRef.current?.abort();
|
|
70
|
+
if (debounceRef.current) {
|
|
71
|
+
clearTimeout(debounceRef.current);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}, []);
|
|
75
|
+
|
|
76
|
+
return { suggestions, loading };
|
|
77
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { BrainerceClient } from 'brainerce';
|
|
2
|
+
|
|
3
|
+
const CONNECTION_ID = process.env.NEXT_PUBLIC_BRAINERCE_CONNECTION_ID || '<%= connectionId %>';
|
|
4
|
+
const API_URL = process.env.NEXT_PUBLIC_BRAINERCE_API_URL || '<%= apiBaseUrl %>';
|
|
5
|
+
|
|
6
|
+
// Singleton SDK client
|
|
7
|
+
let clientInstance: BrainerceClient | null = null;
|
|
8
|
+
|
|
9
|
+
export function getClient(): BrainerceClient {
|
|
10
|
+
if (!clientInstance) {
|
|
11
|
+
clientInstance = new BrainerceClient({
|
|
12
|
+
connectionId: CONNECTION_ID,
|
|
13
|
+
baseUrl: API_URL,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
return clientInstance;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Auth token helpers
|
|
20
|
+
const TOKEN_KEY = 'brainerce_customer_token';
|
|
21
|
+
const CART_ID_KEY = 'brainerce_cart_id';
|
|
22
|
+
|
|
23
|
+
export function getStoredToken(): string | null {
|
|
24
|
+
if (typeof window === 'undefined') return null;
|
|
25
|
+
return localStorage.getItem(TOKEN_KEY);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function setStoredToken(token: string | null): void {
|
|
29
|
+
if (typeof window === 'undefined') return;
|
|
30
|
+
if (token) {
|
|
31
|
+
localStorage.setItem(TOKEN_KEY, token);
|
|
32
|
+
} else {
|
|
33
|
+
localStorage.removeItem(TOKEN_KEY);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function getStoredCartId(): string | null {
|
|
38
|
+
if (typeof window === 'undefined') return null;
|
|
39
|
+
return localStorage.getItem(CART_ID_KEY);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function setStoredCartId(cartId: string | null): void {
|
|
43
|
+
if (typeof window === 'undefined') return;
|
|
44
|
+
if (cartId) {
|
|
45
|
+
localStorage.setItem(CART_ID_KEY, cartId);
|
|
46
|
+
} else {
|
|
47
|
+
localStorage.removeItem(CART_ID_KEY);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Initialize client with stored auth
|
|
52
|
+
export function initClient(): BrainerceClient {
|
|
53
|
+
const client = getClient();
|
|
54
|
+
const token = getStoredToken();
|
|
55
|
+
if (token) {
|
|
56
|
+
client.setCustomerToken(token);
|
|
57
|
+
}
|
|
58
|
+
return client;
|
|
59
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { createContext, useContext, useEffect, useState, useCallback } from 'react';
|
|
4
|
+
import type { StoreInfo, Cart, LocalCart } from 'brainerce';
|
|
5
|
+
import { getCartTotals } from 'brainerce';
|
|
6
|
+
import { getClient, initClient, getStoredToken, setStoredToken, setStoredCartId } from '@/lib/brainerce';
|
|
7
|
+
|
|
8
|
+
// ---- Store Info Context ----
|
|
9
|
+
interface StoreInfoContextValue {
|
|
10
|
+
storeInfo: StoreInfo | null;
|
|
11
|
+
loading: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const StoreInfoContext = createContext<StoreInfoContextValue>({
|
|
15
|
+
storeInfo: null,
|
|
16
|
+
loading: true,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export function useStoreInfo() {
|
|
20
|
+
return useContext(StoreInfoContext);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ---- Auth Context ----
|
|
24
|
+
interface AuthContextValue {
|
|
25
|
+
isLoggedIn: boolean;
|
|
26
|
+
token: string | null;
|
|
27
|
+
login: (token: string) => void;
|
|
28
|
+
logout: () => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const AuthContext = createContext<AuthContextValue>({
|
|
32
|
+
isLoggedIn: false,
|
|
33
|
+
token: null,
|
|
34
|
+
login: () => {},
|
|
35
|
+
logout: () => {},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export function useAuth() {
|
|
39
|
+
return useContext(AuthContext);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ---- Cart Context ----
|
|
43
|
+
interface CartContextValue {
|
|
44
|
+
cart: Cart | LocalCart | null;
|
|
45
|
+
cartLoading: boolean;
|
|
46
|
+
refreshCart: () => Promise<void>;
|
|
47
|
+
itemCount: number;
|
|
48
|
+
totals: { subtotal: number; discount: number; shipping: number; total: number };
|
|
49
|
+
isServerCart: (c: Cart | LocalCart | null) => c is Cart;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const CartContext = createContext<CartContextValue>({
|
|
53
|
+
cart: null,
|
|
54
|
+
cartLoading: true,
|
|
55
|
+
refreshCart: async () => {},
|
|
56
|
+
itemCount: 0,
|
|
57
|
+
totals: { subtotal: 0, discount: 0, shipping: 0, total: 0 },
|
|
58
|
+
isServerCart: (_c): _c is Cart => false,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
export function useCart() {
|
|
62
|
+
return useContext(CartContext);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ---- Provider Component ----
|
|
66
|
+
export function StoreProvider({ children }: { children: React.ReactNode }) {
|
|
67
|
+
const [storeInfo, setStoreInfo] = useState<StoreInfo | null>(null);
|
|
68
|
+
const [storeLoading, setStoreLoading] = useState(true);
|
|
69
|
+
const [token, setToken] = useState<string | null>(null);
|
|
70
|
+
const [cart, setCart] = useState<Cart | LocalCart | null>(null);
|
|
71
|
+
const [cartLoading, setCartLoading] = useState(true);
|
|
72
|
+
|
|
73
|
+
// Initialize client and auth
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
const client = initClient();
|
|
76
|
+
const stored = getStoredToken();
|
|
77
|
+
if (stored) {
|
|
78
|
+
setToken(stored);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
client
|
|
82
|
+
.getStoreInfo()
|
|
83
|
+
.then(setStoreInfo)
|
|
84
|
+
.catch(console.error)
|
|
85
|
+
.finally(() => setStoreLoading(false));
|
|
86
|
+
}, []);
|
|
87
|
+
|
|
88
|
+
// Cart management
|
|
89
|
+
const refreshCart = useCallback(async () => {
|
|
90
|
+
try {
|
|
91
|
+
setCartLoading(true);
|
|
92
|
+
const client = getClient();
|
|
93
|
+
const c = await client.smartGetCart();
|
|
94
|
+
setCart(c);
|
|
95
|
+
|
|
96
|
+
// Persist server cart ID
|
|
97
|
+
if (c && 'id' in c) {
|
|
98
|
+
setStoredCartId(c.id);
|
|
99
|
+
}
|
|
100
|
+
} catch (err) {
|
|
101
|
+
console.error('Failed to load cart:', err);
|
|
102
|
+
} finally {
|
|
103
|
+
setCartLoading(false);
|
|
104
|
+
}
|
|
105
|
+
}, []);
|
|
106
|
+
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
refreshCart();
|
|
109
|
+
}, [refreshCart, token]);
|
|
110
|
+
|
|
111
|
+
const login = useCallback((newToken: string) => {
|
|
112
|
+
const client = getClient();
|
|
113
|
+
client.setCustomerToken(newToken);
|
|
114
|
+
setStoredToken(newToken);
|
|
115
|
+
setToken(newToken);
|
|
116
|
+
|
|
117
|
+
// Sync local cart to server
|
|
118
|
+
client.syncCartOnLogin().catch(console.error);
|
|
119
|
+
}, []);
|
|
120
|
+
|
|
121
|
+
const logout = useCallback(() => {
|
|
122
|
+
const client = getClient();
|
|
123
|
+
client.clearCustomerToken();
|
|
124
|
+
setStoredToken(null);
|
|
125
|
+
setToken(null);
|
|
126
|
+
setCart(null);
|
|
127
|
+
refreshCart();
|
|
128
|
+
}, [refreshCart]);
|
|
129
|
+
|
|
130
|
+
const isServerCart = (c: Cart | LocalCart | null): c is Cart => {
|
|
131
|
+
return c !== null && 'id' in c;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const itemCount = cart
|
|
135
|
+
? cart.items.reduce((sum, item) => sum + item.quantity, 0)
|
|
136
|
+
: 0;
|
|
137
|
+
|
|
138
|
+
const totals = isServerCart(cart)
|
|
139
|
+
? getCartTotals(cart)
|
|
140
|
+
: {
|
|
141
|
+
subtotal: cart
|
|
142
|
+
? cart.items.reduce(
|
|
143
|
+
(sum, item) => sum + parseFloat(String(item.price || '0')) * item.quantity,
|
|
144
|
+
0
|
|
145
|
+
)
|
|
146
|
+
: 0,
|
|
147
|
+
discount: 0,
|
|
148
|
+
shipping: 0,
|
|
149
|
+
total: cart
|
|
150
|
+
? cart.items.reduce(
|
|
151
|
+
(sum, item) => sum + parseFloat(String(item.price || '0')) * item.quantity,
|
|
152
|
+
0
|
|
153
|
+
)
|
|
154
|
+
: 0,
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<StoreInfoContext.Provider value={{ storeInfo, loading: storeLoading }}>
|
|
159
|
+
<AuthContext.Provider value={{ isLoggedIn: !!token, token, login, logout }}>
|
|
160
|
+
<CartContext.Provider
|
|
161
|
+
value={{ cart, cartLoading, refreshCart, itemCount, totals, isServerCart }}
|
|
162
|
+
>
|
|
163
|
+
{children}
|
|
164
|
+
</CartContext.Provider>
|
|
165
|
+
</AuthContext.Provider>
|
|
166
|
+
</StoreInfoContext.Provider>
|
|
167
|
+
);
|
|
168
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Config } from 'tailwindcss';
|
|
2
|
+
|
|
3
|
+
const config: Config = {
|
|
4
|
+
content: ['./src/**/*.{js,ts,jsx,tsx,mdx}'],
|
|
5
|
+
theme: {
|
|
6
|
+
extend: {
|
|
7
|
+
colors: {
|
|
8
|
+
primary: 'hsl(var(--primary))',
|
|
9
|
+
'primary-foreground': 'hsl(var(--primary-foreground))',
|
|
10
|
+
secondary: 'hsl(var(--secondary))',
|
|
11
|
+
'secondary-foreground': 'hsl(var(--secondary-foreground))',
|
|
12
|
+
background: 'hsl(var(--background))',
|
|
13
|
+
foreground: 'hsl(var(--foreground))',
|
|
14
|
+
muted: 'hsl(var(--muted))',
|
|
15
|
+
'muted-foreground': 'hsl(var(--muted-foreground))',
|
|
16
|
+
border: 'hsl(var(--border))',
|
|
17
|
+
destructive: 'hsl(var(--destructive))',
|
|
18
|
+
'destructive-foreground': 'hsl(var(--destructive-foreground))',
|
|
19
|
+
accent: 'hsl(var(--accent))',
|
|
20
|
+
'accent-foreground': 'hsl(var(--accent-foreground))',
|
|
21
|
+
},
|
|
22
|
+
borderRadius: {
|
|
23
|
+
DEFAULT: 'var(--radius)',
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
plugins: [],
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export default config;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2017",
|
|
4
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
5
|
+
"allowJs": true,
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"module": "esnext",
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"jsx": "preserve",
|
|
15
|
+
"incremental": true,
|
|
16
|
+
"plugins": [{ "name": "next" }],
|
|
17
|
+
"paths": {
|
|
18
|
+
"@/*": ["./src/*"]
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
22
|
+
"exclude": ["node_modules"]
|
|
23
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
@tailwind base;
|
|
2
|
+
@tailwind components;
|
|
3
|
+
@tailwind utilities;
|
|
4
|
+
|
|
5
|
+
@layer base {
|
|
6
|
+
:root {
|
|
7
|
+
--background: 0 0% 100%;
|
|
8
|
+
--foreground: 0 0% 9%;
|
|
9
|
+
--primary: 0 0% 9%;
|
|
10
|
+
--primary-foreground: 0 0% 98%;
|
|
11
|
+
--secondary: 0 0% 96%;
|
|
12
|
+
--secondary-foreground: 0 0% 9%;
|
|
13
|
+
--muted: 0 0% 96%;
|
|
14
|
+
--muted-foreground: 0 0% 45%;
|
|
15
|
+
--accent: 0 0% 96%;
|
|
16
|
+
--accent-foreground: 0 0% 9%;
|
|
17
|
+
--destructive: 0 84% 60%;
|
|
18
|
+
--destructive-foreground: 0 0% 98%;
|
|
19
|
+
--border: 0 0% 90%;
|
|
20
|
+
--radius: 0.5rem;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
* {
|
|
24
|
+
@apply border-border;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
body {
|
|
28
|
+
@apply bg-background text-foreground antialiased;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Minimal",
|
|
3
|
+
"description": "Clean, neutral design with Inter font and medium radius",
|
|
4
|
+
"font": {
|
|
5
|
+
"family": "Inter",
|
|
6
|
+
"import": "next/font/google"
|
|
7
|
+
},
|
|
8
|
+
"colors": {
|
|
9
|
+
"background": "0 0% 100%",
|
|
10
|
+
"foreground": "0 0% 9%",
|
|
11
|
+
"primary": "0 0% 9%",
|
|
12
|
+
"primary-foreground": "0 0% 98%",
|
|
13
|
+
"secondary": "0 0% 96%",
|
|
14
|
+
"secondary-foreground": "0 0% 9%",
|
|
15
|
+
"muted": "0 0% 96%",
|
|
16
|
+
"muted-foreground": "0 0% 45%",
|
|
17
|
+
"accent": "0 0% 96%",
|
|
18
|
+
"accent-foreground": "0 0% 9%",
|
|
19
|
+
"destructive": "0 84% 60%",
|
|
20
|
+
"border": "0 0% 90%"
|
|
21
|
+
},
|
|
22
|
+
"radius": "0.5rem"
|
|
23
|
+
}
|