nextworks 0.1.0-alpha.11 → 0.1.0-alpha.14
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/README.md +20 -9
- package/dist/kits/auth-core/.nextworks/docs/AUTH_CORE_README.md +3 -3
- package/dist/kits/auth-core/.nextworks/docs/AUTH_QUICKSTART.md +264 -244
- package/dist/kits/auth-core/app/(protected)/settings/profile/profile-form.tsx +120 -114
- package/dist/kits/auth-core/app/api/auth/forgot-password/route.ts +116 -114
- package/dist/kits/auth-core/app/api/auth/reset-password/route.ts +66 -63
- package/dist/kits/auth-core/app/api/auth/send-verify-email/route.ts +1 -1
- package/dist/kits/auth-core/app/api/users/[id]/route.ts +134 -127
- package/dist/kits/auth-core/app/auth/reset-password/page.tsx +186 -187
- package/dist/kits/auth-core/components/auth/dashboard.tsx +25 -2
- package/dist/kits/auth-core/components/auth/forgot-password-form.tsx +90 -90
- package/dist/kits/auth-core/components/auth/login-form.tsx +492 -467
- package/dist/kits/auth-core/components/auth/signup-form.tsx +28 -29
- package/dist/kits/auth-core/lib/auth.ts +46 -15
- package/dist/kits/auth-core/lib/forms/map-errors.ts +37 -11
- package/dist/kits/auth-core/lib/server/result.ts +45 -45
- package/dist/kits/auth-core/lib/validation/forms.ts +1 -2
- package/dist/kits/auth-core/package-deps.json +4 -2
- package/dist/kits/auth-core/types/next-auth.d.ts +1 -1
- package/dist/kits/blocks/.nextworks/docs/BLOCKS_QUICKSTART.md +2 -8
- package/dist/kits/blocks/.nextworks/docs/THEME_GUIDE.md +18 -1
- package/dist/kits/blocks/app/templates/productlaunch/page.tsx +0 -2
- package/dist/kits/blocks/components/sections/FAQ.tsx +0 -1
- package/dist/kits/blocks/components/sections/Newsletter.tsx +2 -2
- package/dist/kits/blocks/components/ui/switch.tsx +78 -78
- package/dist/kits/blocks/components/ui/theme-selector.tsx +1 -1
- package/dist/kits/blocks/lib/themes.ts +1 -0
- package/dist/kits/blocks/package-deps.json +4 -4
- package/dist/kits/data/.nextworks/docs/DATA_QUICKSTART.md +128 -112
- package/dist/kits/data/.nextworks/docs/DATA_README.md +2 -1
- package/dist/kits/data/app/api/posts/[id]/route.ts +83 -83
- package/dist/kits/data/app/api/posts/route.ts +136 -138
- package/dist/kits/data/app/api/seed-demo/route.ts +1 -2
- package/dist/kits/data/app/api/users/[id]/route.ts +29 -17
- package/dist/kits/data/app/api/users/check-email/route.ts +1 -1
- package/dist/kits/data/app/api/users/check-unique/route.ts +30 -27
- package/dist/kits/data/app/api/users/route.ts +0 -2
- package/dist/kits/data/app/examples/demo/create-post-form.tsx +108 -106
- package/dist/kits/data/app/examples/demo/page.tsx +2 -1
- package/dist/kits/data/app/examples/demo/seed-demo-button.tsx +1 -1
- package/dist/kits/data/components/admin/posts-manager.tsx +727 -719
- package/dist/kits/data/components/admin/users-manager.tsx +435 -432
- package/dist/kits/data/lib/server/result.ts +5 -2
- package/dist/kits/data/package-deps.json +1 -1
- package/dist/kits/data/scripts/seed-demo.mjs +1 -2
- package/dist/kits/forms/app/api/wizard/route.ts +76 -71
- package/dist/kits/forms/app/examples/forms/server-action/page.tsx +78 -71
- package/dist/kits/forms/components/hooks/useCheckUnique.ts +85 -79
- package/dist/kits/forms/components/ui/form/form-control.tsx +28 -28
- package/dist/kits/forms/components/ui/form/form-description.tsx +23 -22
- package/dist/kits/forms/components/ui/form/form-item.tsx +21 -21
- package/dist/kits/forms/components/ui/form/form-label.tsx +24 -24
- package/dist/kits/forms/components/ui/form/form-message.tsx +28 -29
- package/dist/kits/forms/components/ui/switch.tsx +78 -78
- package/dist/kits/forms/lib/forms/map-errors.ts +1 -1
- package/dist/kits/forms/lib/validation/forms.ts +1 -2
- package/package.json +1 -1
|
@@ -23,7 +23,6 @@ import { FormItem } from "@/components/ui/form/form-item";
|
|
|
23
23
|
import { FormLabel } from "@/components/ui/form/form-label";
|
|
24
24
|
import { FormMessage } from "@/components/ui/form/form-message";
|
|
25
25
|
import { FormControl } from "@/components/ui/form/form-control";
|
|
26
|
-
import { toast } from "sonner";
|
|
27
26
|
import { mapApiErrorsToForm } from "@/lib/forms/map-errors";
|
|
28
27
|
import useCheckUnique from "@/components/hooks/useCheckUnique";
|
|
29
28
|
|
|
@@ -163,7 +162,6 @@ export default function SignupForm({
|
|
|
163
162
|
const { data: session, status } = useSession();
|
|
164
163
|
const redirectTo = searchParams.get("callbackUrl") || defaultRedirect;
|
|
165
164
|
const [formError, setFormError] = useState<string | null>(null);
|
|
166
|
-
const [success, setSuccess] = useState(false);
|
|
167
165
|
const [isGithubLoading, setIsGithubLoading] = useState(false);
|
|
168
166
|
const [githubAvailable, setGithubAvailable] = useState(false);
|
|
169
167
|
|
|
@@ -178,24 +176,23 @@ export default function SignupForm({
|
|
|
178
176
|
const {
|
|
179
177
|
control,
|
|
180
178
|
handleSubmit,
|
|
181
|
-
setError: setFieldError,
|
|
182
179
|
formState: { isSubmitting },
|
|
183
180
|
} = formMethods;
|
|
184
181
|
|
|
185
182
|
const emailValue = formMethods.watch("email");
|
|
186
|
-
const {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
183
|
+
const { loading: checkingEmail, unique: emailUnique } = useCheckUnique(
|
|
184
|
+
"email",
|
|
185
|
+
emailValue,
|
|
186
|
+
500,
|
|
187
|
+
);
|
|
191
188
|
|
|
192
|
-
const useDebouncedValidator = (
|
|
193
|
-
fn: (v:
|
|
189
|
+
const useDebouncedValidator = <T,>(
|
|
190
|
+
fn: (v: T) => Promise<boolean | string | undefined>,
|
|
194
191
|
delay = 500,
|
|
195
192
|
) => {
|
|
196
193
|
const timer = useRef<number | undefined>(undefined);
|
|
197
194
|
return useCallback(
|
|
198
|
-
(value:
|
|
195
|
+
(value: T): Promise<boolean | string | undefined> => {
|
|
199
196
|
if (timer.current) window.clearTimeout(timer.current);
|
|
200
197
|
return new Promise((resolve) => {
|
|
201
198
|
timer.current = window.setTimeout(async () => {
|
|
@@ -250,7 +247,6 @@ export default function SignupForm({
|
|
|
250
247
|
|
|
251
248
|
const onSubmit = async (data: SignupFormValues) => {
|
|
252
249
|
setFormError(null);
|
|
253
|
-
setSuccess(false);
|
|
254
250
|
try {
|
|
255
251
|
const res = await fetch("/api/signup", {
|
|
256
252
|
method: "POST",
|
|
@@ -258,12 +254,25 @@ export default function SignupForm({
|
|
|
258
254
|
body: JSON.stringify(data),
|
|
259
255
|
});
|
|
260
256
|
|
|
261
|
-
const payload = await res.json().catch(() => null);
|
|
262
|
-
if (
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
257
|
+
const payload: unknown = await res.json().catch(() => null);
|
|
258
|
+
if (
|
|
259
|
+
!res.ok ||
|
|
260
|
+
!(
|
|
261
|
+
payload &&
|
|
262
|
+
typeof payload === "object" &&
|
|
263
|
+
"success" in payload &&
|
|
264
|
+
payload.success === true
|
|
265
|
+
)
|
|
266
|
+
) {
|
|
267
|
+
const msg = payload ? mapApiErrorsToForm(formMethods, payload as never) : undefined;
|
|
268
|
+
const payloadMessage =
|
|
269
|
+
payload &&
|
|
270
|
+
typeof payload === "object" &&
|
|
271
|
+
"message" in payload &&
|
|
272
|
+
typeof payload.message === "string"
|
|
273
|
+
? payload.message
|
|
274
|
+
: null;
|
|
275
|
+
setFormError(msg || payloadMessage || "Signup failed");
|
|
267
276
|
return;
|
|
268
277
|
}
|
|
269
278
|
|
|
@@ -271,8 +280,7 @@ export default function SignupForm({
|
|
|
271
280
|
`/auth/login?signup=1&callbackUrl=${encodeURIComponent(redirectTo)}`,
|
|
272
281
|
);
|
|
273
282
|
} catch (e: unknown) {
|
|
274
|
-
|
|
275
|
-
setFormError(err.message || "Unknown error");
|
|
283
|
+
setFormError(e instanceof Error ? e.message : "Unknown error");
|
|
276
284
|
}
|
|
277
285
|
};
|
|
278
286
|
|
|
@@ -319,15 +327,6 @@ export default function SignupForm({
|
|
|
319
327
|
{formError}
|
|
320
328
|
</div>
|
|
321
329
|
)}
|
|
322
|
-
{success && (
|
|
323
|
-
<div
|
|
324
|
-
className={cn(alerts.successClassName)}
|
|
325
|
-
role="status"
|
|
326
|
-
aria-live="polite"
|
|
327
|
-
>
|
|
328
|
-
{labels.success}
|
|
329
|
-
</div>
|
|
330
|
-
)}
|
|
331
330
|
|
|
332
331
|
{showGithub && githubAvailable && (
|
|
333
332
|
<>
|
|
@@ -8,6 +8,12 @@ import { compare } from "bcryptjs";
|
|
|
8
8
|
import { z } from "zod";
|
|
9
9
|
import { zodErrorToFieldErrors } from "@/lib/api/errors";
|
|
10
10
|
|
|
11
|
+
type JwtTokenWithAppClaims = {
|
|
12
|
+
id?: string;
|
|
13
|
+
role?: string;
|
|
14
|
+
image?: string | null;
|
|
15
|
+
};
|
|
16
|
+
|
|
11
17
|
export const authOptions: NextAuthOptions = {
|
|
12
18
|
adapter: PrismaAdapter(prisma),
|
|
13
19
|
providers: [
|
|
@@ -66,8 +72,8 @@ export const authOptions: NextAuthOptions = {
|
|
|
66
72
|
name: user.name,
|
|
67
73
|
email: user.email,
|
|
68
74
|
role: user.role,
|
|
69
|
-
} as
|
|
70
|
-
} catch (err:
|
|
75
|
+
} as unknown as import("next-auth").User;
|
|
76
|
+
} catch (err: unknown) {
|
|
71
77
|
if (err instanceof z.ZodError) {
|
|
72
78
|
throw new Error(
|
|
73
79
|
JSON.stringify({
|
|
@@ -77,8 +83,10 @@ export const authOptions: NextAuthOptions = {
|
|
|
77
83
|
);
|
|
78
84
|
}
|
|
79
85
|
try {
|
|
80
|
-
|
|
81
|
-
|
|
86
|
+
if (err instanceof Error) {
|
|
87
|
+
JSON.parse(err.message);
|
|
88
|
+
throw err; // already structured
|
|
89
|
+
}
|
|
82
90
|
} catch {
|
|
83
91
|
throw new Error(
|
|
84
92
|
JSON.stringify({
|
|
@@ -88,6 +96,8 @@ export const authOptions: NextAuthOptions = {
|
|
|
88
96
|
);
|
|
89
97
|
}
|
|
90
98
|
}
|
|
99
|
+
|
|
100
|
+
return null;
|
|
91
101
|
},
|
|
92
102
|
}),
|
|
93
103
|
...(process.env.GITHUB_ID && process.env.GITHUB_SECRET
|
|
@@ -102,29 +112,50 @@ export const authOptions: NextAuthOptions = {
|
|
|
102
112
|
session: { strategy: "jwt" },
|
|
103
113
|
callbacks: {
|
|
104
114
|
async jwt({ token, user, trigger, session }) {
|
|
115
|
+
const t = token as typeof token & JwtTokenWithAppClaims;
|
|
116
|
+
|
|
105
117
|
if (user) {
|
|
106
|
-
|
|
118
|
+
// NextAuth's `user` type doesn't include custom properties like `role`.
|
|
119
|
+
// Narrow via runtime checks instead of `any`.
|
|
120
|
+
const u = user as unknown as Record<string, unknown>;
|
|
121
|
+
|
|
122
|
+
if (typeof u.id === "string") t.id = u.id;
|
|
123
|
+
|
|
107
124
|
token.name = user.name ?? "";
|
|
108
125
|
token.email = user.email ?? "";
|
|
109
|
-
|
|
110
|
-
|
|
126
|
+
|
|
127
|
+
if (typeof u.role === "string") {
|
|
128
|
+
t.role = u.role;
|
|
129
|
+
} else {
|
|
130
|
+
t.role = t.role ?? "user";
|
|
131
|
+
}
|
|
111
132
|
}
|
|
133
|
+
|
|
112
134
|
if (trigger === "update" && session) {
|
|
113
|
-
|
|
114
|
-
if (typeof
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
if (
|
|
118
|
-
|
|
135
|
+
if (typeof session.name === "string") token.name = session.name;
|
|
136
|
+
if (typeof session.email === "string") token.email = session.email;
|
|
137
|
+
|
|
138
|
+
const s = session as unknown as Record<string, unknown>;
|
|
139
|
+
if (typeof s.role === "string") t.role = s.role;
|
|
140
|
+
|
|
141
|
+
if (s.image === null || typeof s.image === "string") {
|
|
142
|
+
t.image = (s.image as string | null) ?? null;
|
|
143
|
+
}
|
|
119
144
|
}
|
|
145
|
+
|
|
120
146
|
return token;
|
|
121
147
|
},
|
|
122
148
|
async session({ session, token }) {
|
|
149
|
+
const t = token as typeof token & JwtTokenWithAppClaims;
|
|
150
|
+
|
|
123
151
|
if (session.user) {
|
|
124
|
-
(
|
|
152
|
+
if (typeof t.id === "string") session.user.id = t.id;
|
|
125
153
|
session.user.name = token.name ?? null;
|
|
126
154
|
session.user.email = token.email ?? "";
|
|
127
|
-
|
|
155
|
+
|
|
156
|
+
if (typeof t.role === "string") {
|
|
157
|
+
(session.user as unknown as Record<string, unknown>).role = t.role;
|
|
158
|
+
}
|
|
128
159
|
}
|
|
129
160
|
return session;
|
|
130
161
|
},
|
|
@@ -1,11 +1,37 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
import type { FieldValues, Path, UseFormReturn } from "react-hook-form";
|
|
2
|
+
|
|
3
|
+
type ApiErrorPayload = {
|
|
4
|
+
message?: unknown;
|
|
5
|
+
errors?: unknown;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
9
|
+
return typeof value === "object" && value !== null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function mapApiErrorsToForm<TFieldValues extends FieldValues>(
|
|
13
|
+
methods: UseFormReturn<TFieldValues>,
|
|
14
|
+
payload: unknown,
|
|
15
|
+
): string | null {
|
|
16
|
+
if (!isRecord(payload)) return null;
|
|
17
|
+
|
|
18
|
+
const messageRaw = (payload as ApiErrorPayload).message;
|
|
19
|
+
const errorsRaw = (payload as ApiErrorPayload).errors;
|
|
20
|
+
|
|
21
|
+
const message = typeof messageRaw === "string" ? messageRaw : null;
|
|
22
|
+
|
|
23
|
+
if (!isRecord(errorsRaw)) return message;
|
|
24
|
+
|
|
25
|
+
for (const [field, msg] of Object.entries(errorsRaw)) {
|
|
26
|
+
// Runtime safety: only set errors for fields that exist on the form
|
|
27
|
+
if (field in methods.getValues()) {
|
|
28
|
+
methods.setError(field as Path<TFieldValues>, {
|
|
29
|
+
type: "server",
|
|
30
|
+
message: typeof msg === "string" ? msg : String(msg),
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return message;
|
|
36
|
+
}
|
|
37
|
+
|
|
@@ -1,45 +1,45 @@
|
|
|
1
|
-
export function jsonOk(data?: any, opts?: { status?: number; message?: string }) {
|
|
2
|
-
return Response.json({ success: true, data: data ?? null, message: opts?.message ?? null }, { status: opts?.status ?? 200 });
|
|
3
|
-
}
|
|
4
|
-
|
|
5
|
-
export function jsonFail(
|
|
6
|
-
message: string,
|
|
7
|
-
opts?: { status?: number; code?: string | number; errors?: Record<string, string> | null },
|
|
8
|
-
) {
|
|
9
|
-
return Response.json(
|
|
10
|
-
{ success: false, data: null, message, code: opts?.code ?? null, errors: opts?.errors ?? null },
|
|
11
|
-
{ status: opts?.status ?? 400 },
|
|
12
|
-
);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function jsonFromZod(err: any, opts?: { status?: number; message?: string }) {
|
|
16
|
-
const fieldErrors: Record<string, string> = {};
|
|
17
|
-
if (err?.issues && Array.isArray(err.issues)) {
|
|
18
|
-
for (const issue of err.issues) {
|
|
19
|
-
if (issue.path && issue.path.length > 0) {
|
|
20
|
-
fieldErrors[String(issue.path[0])] = issue.message;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
return jsonFail(opts?.message || "Validation failed", {
|
|
25
|
-
status: opts?.status ?? 400,
|
|
26
|
-
errors: Object.keys(fieldErrors).length > 0 ? fieldErrors : null,
|
|
27
|
-
code: "VALIDATION_ERROR",
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function jsonFromPrisma(err: any) {
|
|
32
|
-
// Basic unique violation mapping (P2002)
|
|
33
|
-
const code = (err && err.code) || "PRISMA_ERROR";
|
|
34
|
-
if (code === "P2002") {
|
|
35
|
-
const meta = err.meta || {};
|
|
36
|
-
const target = Array.isArray(meta.target) ? meta.target[0] : meta.target;
|
|
37
|
-
const field = typeof target === "string" ? target : "field";
|
|
38
|
-
return jsonFail("Unique constraint violation", {
|
|
39
|
-
status: 409,
|
|
40
|
-
errors: { [field]: `${field} already in use` },
|
|
41
|
-
code: "UNIQUE_CONSTRAINT",
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
return jsonFail("Database error", { status: 500, code });
|
|
45
|
-
}
|
|
1
|
+
export function jsonOk(data?: any, opts?: { status?: number; message?: string }) {
|
|
2
|
+
return Response.json({ success: true, data: data ?? null, message: opts?.message ?? null }, { status: opts?.status ?? 200 });
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function jsonFail(
|
|
6
|
+
message: string,
|
|
7
|
+
opts?: { status?: number; code?: string | number; errors?: Record<string, string> | null },
|
|
8
|
+
) {
|
|
9
|
+
return Response.json(
|
|
10
|
+
{ success: false, data: null, message, code: opts?.code ?? null, errors: opts?.errors ?? null },
|
|
11
|
+
{ status: opts?.status ?? 400 },
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function jsonFromZod(err: any, opts?: { status?: number; message?: string }) {
|
|
16
|
+
const fieldErrors: Record<string, string> = {};
|
|
17
|
+
if (err?.issues && Array.isArray(err.issues)) {
|
|
18
|
+
for (const issue of err.issues) {
|
|
19
|
+
if (issue.path && issue.path.length > 0) {
|
|
20
|
+
fieldErrors[String(issue.path[0])] = issue.message;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return jsonFail(opts?.message || "Validation failed", {
|
|
25
|
+
status: opts?.status ?? 400,
|
|
26
|
+
errors: Object.keys(fieldErrors).length > 0 ? fieldErrors : null,
|
|
27
|
+
code: "VALIDATION_ERROR",
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function jsonFromPrisma(err: any) {
|
|
32
|
+
// Basic unique violation mapping (P2002)
|
|
33
|
+
const code = (err && err.code) || "PRISMA_ERROR";
|
|
34
|
+
if (code === "P2002") {
|
|
35
|
+
const meta = err.meta || {};
|
|
36
|
+
const target = Array.isArray(meta.target) ? meta.target[0] : meta.target;
|
|
37
|
+
const field = typeof target === "string" ? target : "field";
|
|
38
|
+
return jsonFail("Unique constraint violation", {
|
|
39
|
+
status: 409,
|
|
40
|
+
errors: { [field]: `${field} already in use` },
|
|
41
|
+
code: "UNIQUE_CONSTRAINT",
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return jsonFail("Database error", { status: 500, code });
|
|
45
|
+
}
|
|
@@ -60,8 +60,7 @@ export const userUpdateSchema = z.object({
|
|
|
60
60
|
password: z
|
|
61
61
|
.string()
|
|
62
62
|
.min(6, "Password must be at least 6 characters")
|
|
63
|
-
.optional()
|
|
64
|
-
.or(z.literal("")),
|
|
63
|
+
.optional(),
|
|
65
64
|
emailVerified: z
|
|
66
65
|
.union([z.date(), z.string().transform((s) => new Date(s)), z.null()])
|
|
67
66
|
.optional(),
|
|
@@ -11,9 +11,11 @@
|
|
|
11
11
|
"next-auth": "^4.24.13",
|
|
12
12
|
"tailwind-merge": "^3.3.1",
|
|
13
13
|
"nodemailer": "^7.0.7",
|
|
14
|
-
"@nextworks/blocks-core": "
|
|
14
|
+
"@nextworks/blocks-core": "0.1.0-alpha.14"
|
|
15
15
|
},
|
|
16
16
|
"devDependencies": {
|
|
17
|
-
"prisma": "^6.16.1"
|
|
17
|
+
"prisma": "^6.16.1",
|
|
18
|
+
"@types/nodemailer": "^7.0.4"
|
|
19
|
+
|
|
18
20
|
}
|
|
19
21
|
}
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
This document explains the Blocks kit: prebuilt UI sections, templates and core UI primitives included in this repository. The Blocks kit is intended to be a non-invasive copyable kit (shadCN-style) you can install into any Next.js App Router + TypeScript + Tailwind project.
|
|
4
4
|
|
|
5
5
|
> **Alpha note**
|
|
6
|
-
> Other kits (Auth Core, Forms, Data) are currently tested and supported on top of a default Blocks install.
|
|
6
|
+
> Other kits (Auth Core, Forms, Data) are currently tested and supported on top of a default Blocks install.
|
|
7
|
+
> For the smoothest experience, install **Blocks first** in your app before adding other kits.
|
|
7
8
|
|
|
8
9
|
> If you are using the `nextworks` CLI in your own app, you can install this Blocks kit by running:
|
|
9
10
|
>
|
|
@@ -12,13 +13,6 @@ This document explains the Blocks kit: prebuilt UI sections, templates and core
|
|
|
12
13
|
> ```
|
|
13
14
|
>
|
|
14
15
|
> This installs **core UI primitives, sections, and page templates**, so the example templates work out of the box. The CLI will copy files into your project under `components/`, `app/templates/`, `lib/`, and `public/` as described below.
|
|
15
|
-
>
|
|
16
|
-
> Advanced:
|
|
17
|
-
>
|
|
18
|
-
> - `npx nextworks add blocks --ui-only` → install core UI primitives only (no sections/templates).
|
|
19
|
-
> - `npx nextworks add blocks --sections` → install core + sections only.
|
|
20
|
-
> - `npx nextworks add blocks --templates` → install core + templates only.
|
|
21
|
-
> - `npx nextworks add blocks --sections --templates` → install core + sections + templates.
|
|
22
16
|
|
|
23
17
|
What’s included
|
|
24
18
|
|
|
@@ -14,7 +14,24 @@ This project now includes a comprehensive theming system that allows you to easi
|
|
|
14
14
|
|
|
15
15
|
### 1. Basic Setup
|
|
16
16
|
|
|
17
|
-
Your app is
|
|
17
|
+
Your app is configured with the enhanced theme provider via `AppProviders` in `app/layout.tsx`.
|
|
18
|
+
|
|
19
|
+
**Turbopack / Next 16 note (fonts + AppProviders)**
|
|
20
|
+
|
|
21
|
+
As of the current alpha, `@nextworks/blocks-core/server` intentionally **does not** import `next/font/*`.
|
|
22
|
+
Fonts are instead configured directly in your app’s `app/layout.tsx` (the CLI patches this for you).
|
|
23
|
+
This avoids Turbopack dev issues related to internal Next font modules.
|
|
24
|
+
|
|
25
|
+
If you ever see a font-related Turbopack error after upgrades or manual edits, re-run:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npx nextworks add blocks --sections --templates
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
to re-apply the layout patch, and ensure `app/layout.tsx` contains a valid
|
|
32
|
+
`import { ... } from "next/font/google";` plus the corresponding `const geistSans = ...` etc.
|
|
33
|
+
|
|
34
|
+
The default theme variant is set to "monochrome".
|
|
18
35
|
|
|
19
36
|
### 2. Available Themes
|
|
20
37
|
|
|
@@ -11,8 +11,6 @@ import { Testimonials } from "./components/Testimonials";
|
|
|
11
11
|
import { TrustBadges } from "./components/TrustBadges";
|
|
12
12
|
import { Features } from "./components/Features";
|
|
13
13
|
|
|
14
|
-
import { PresetThemeVars } from "./PresetThemeVars";
|
|
15
|
-
|
|
16
14
|
export default function ProductLaunchPage() {
|
|
17
15
|
return (
|
|
18
16
|
// <PresetThemeVars>
|
|
@@ -33,8 +33,8 @@ export interface NewsletterProps {
|
|
|
33
33
|
input?: { className?: string };
|
|
34
34
|
button?: {
|
|
35
35
|
className?: string;
|
|
36
|
-
variant?:
|
|
37
|
-
size?:
|
|
36
|
+
variant?: React.ComponentProps<typeof Button>["variant"];
|
|
37
|
+
size?: React.ComponentProps<typeof Button>["size"];
|
|
38
38
|
unstyled?: boolean;
|
|
39
39
|
};
|
|
40
40
|
|
|
@@ -1,78 +1,78 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
|
-
import { cn } from "@/lib/utils";
|
|
3
|
-
|
|
4
|
-
export type SwitchProps = React.InputHTMLAttributes<HTMLInputElement> & {
|
|
5
|
-
isLoading?: boolean;
|
|
6
|
-
};
|
|
7
|
-
|
|
8
|
-
const Switch = React.forwardRef<HTMLInputElement, SwitchProps>(
|
|
9
|
-
({ className, isLoading, disabled, ...props }, ref) => {
|
|
10
|
-
const checked = !!(props as any).checked;
|
|
11
|
-
const isDisabled = !!disabled || !!isLoading;
|
|
12
|
-
return (
|
|
13
|
-
<label
|
|
14
|
-
className={cn(
|
|
15
|
-
"focus-within:ring-offset-background inline-flex items-center rounded-full focus-within:ring-2 focus-within:ring-[var(--ring)] focus-within:ring-offset-2",
|
|
16
|
-
isDisabled ? "cursor-not-allowed opacity-80" : "cursor-pointer",
|
|
17
|
-
className,
|
|
18
|
-
)}
|
|
19
|
-
>
|
|
20
|
-
<input
|
|
21
|
-
type="checkbox"
|
|
22
|
-
role="switch"
|
|
23
|
-
ref={ref}
|
|
24
|
-
className="sr-only"
|
|
25
|
-
disabled={isDisabled}
|
|
26
|
-
aria-busy={isLoading ? "true" : undefined}
|
|
27
|
-
{...(props as any)}
|
|
28
|
-
/>
|
|
29
|
-
<span
|
|
30
|
-
aria-hidden
|
|
31
|
-
className={cn(
|
|
32
|
-
"relative inline-flex h-6 w-11 flex-shrink-0 rounded-full transition-colors duration-200",
|
|
33
|
-
checked ? "bg-[var(--primary)]" : "bg-[var(--primary)]/80",
|
|
34
|
-
)}
|
|
35
|
-
>
|
|
36
|
-
<span
|
|
37
|
-
className={cn(
|
|
38
|
-
// Thumb should adjust for theme to guarantee contrast
|
|
39
|
-
"absolute top-0.5 left-0.5 h-5 w-5 transform rounded-full bg-[var(--switch-thumb)] shadow-md transition-transform duration-200 ease-in-out",
|
|
40
|
-
checked ? "translate-x-5" : "translate-x-0",
|
|
41
|
-
)}
|
|
42
|
-
/>
|
|
43
|
-
|
|
44
|
-
{/* Loading indicator centered inside the switch when isLoading */}
|
|
45
|
-
{isLoading && (
|
|
46
|
-
<span className="absolute inset-0 flex items-center justify-center">
|
|
47
|
-
<svg
|
|
48
|
-
className="h-4 w-4 animate-spin text-white"
|
|
49
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
50
|
-
fill="none"
|
|
51
|
-
viewBox="0 0 24 24"
|
|
52
|
-
aria-hidden
|
|
53
|
-
>
|
|
54
|
-
<circle
|
|
55
|
-
className="opacity-25"
|
|
56
|
-
cx="12"
|
|
57
|
-
cy="12"
|
|
58
|
-
r="10"
|
|
59
|
-
stroke="currentColor"
|
|
60
|
-
strokeWidth="4"
|
|
61
|
-
/>
|
|
62
|
-
<path
|
|
63
|
-
className="opacity-75"
|
|
64
|
-
fill="currentColor"
|
|
65
|
-
d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"
|
|
66
|
-
/>
|
|
67
|
-
</svg>
|
|
68
|
-
</span>
|
|
69
|
-
)}
|
|
70
|
-
</span>
|
|
71
|
-
</label>
|
|
72
|
-
);
|
|
73
|
-
},
|
|
74
|
-
);
|
|
75
|
-
|
|
76
|
-
Switch.displayName = "Switch";
|
|
77
|
-
|
|
78
|
-
export { Switch };
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "@/lib/utils";
|
|
3
|
+
|
|
4
|
+
export type SwitchProps = React.InputHTMLAttributes<HTMLInputElement> & {
|
|
5
|
+
isLoading?: boolean;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const Switch = React.forwardRef<HTMLInputElement, SwitchProps>(
|
|
9
|
+
({ className, isLoading, disabled, ...props }, ref) => {
|
|
10
|
+
const checked = !!(props as any).checked;
|
|
11
|
+
const isDisabled = !!disabled || !!isLoading;
|
|
12
|
+
return (
|
|
13
|
+
<label
|
|
14
|
+
className={cn(
|
|
15
|
+
"focus-within:ring-offset-background inline-flex items-center rounded-full focus-within:ring-2 focus-within:ring-[var(--ring)] focus-within:ring-offset-2",
|
|
16
|
+
isDisabled ? "cursor-not-allowed opacity-80" : "cursor-pointer",
|
|
17
|
+
className,
|
|
18
|
+
)}
|
|
19
|
+
>
|
|
20
|
+
<input
|
|
21
|
+
type="checkbox"
|
|
22
|
+
role="switch"
|
|
23
|
+
ref={ref}
|
|
24
|
+
className="sr-only"
|
|
25
|
+
disabled={isDisabled}
|
|
26
|
+
aria-busy={isLoading ? "true" : undefined}
|
|
27
|
+
{...(props as any)}
|
|
28
|
+
/>
|
|
29
|
+
<span
|
|
30
|
+
aria-hidden
|
|
31
|
+
className={cn(
|
|
32
|
+
"relative inline-flex h-6 w-11 flex-shrink-0 rounded-full transition-colors duration-200",
|
|
33
|
+
checked ? "bg-[var(--primary)]" : "bg-[var(--primary)]/80",
|
|
34
|
+
)}
|
|
35
|
+
>
|
|
36
|
+
<span
|
|
37
|
+
className={cn(
|
|
38
|
+
// Thumb should adjust for theme to guarantee contrast
|
|
39
|
+
"absolute top-0.5 left-0.5 h-5 w-5 transform rounded-full bg-[var(--switch-thumb)] shadow-md transition-transform duration-200 ease-in-out",
|
|
40
|
+
checked ? "translate-x-5" : "translate-x-0",
|
|
41
|
+
)}
|
|
42
|
+
/>
|
|
43
|
+
|
|
44
|
+
{/* Loading indicator centered inside the switch when isLoading */}
|
|
45
|
+
{isLoading && (
|
|
46
|
+
<span className="absolute inset-0 flex items-center justify-center">
|
|
47
|
+
<svg
|
|
48
|
+
className="h-4 w-4 animate-spin text-white"
|
|
49
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
50
|
+
fill="none"
|
|
51
|
+
viewBox="0 0 24 24"
|
|
52
|
+
aria-hidden
|
|
53
|
+
>
|
|
54
|
+
<circle
|
|
55
|
+
className="opacity-25"
|
|
56
|
+
cx="12"
|
|
57
|
+
cy="12"
|
|
58
|
+
r="10"
|
|
59
|
+
stroke="currentColor"
|
|
60
|
+
strokeWidth="4"
|
|
61
|
+
/>
|
|
62
|
+
<path
|
|
63
|
+
className="opacity-75"
|
|
64
|
+
fill="currentColor"
|
|
65
|
+
d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"
|
|
66
|
+
/>
|
|
67
|
+
</svg>
|
|
68
|
+
</span>
|
|
69
|
+
)}
|
|
70
|
+
</span>
|
|
71
|
+
</label>
|
|
72
|
+
);
|
|
73
|
+
},
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
Switch.displayName = "Switch";
|
|
77
|
+
|
|
78
|
+
export { Switch };
|