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.
Files changed (57) hide show
  1. package/README.md +20 -9
  2. package/dist/kits/auth-core/.nextworks/docs/AUTH_CORE_README.md +3 -3
  3. package/dist/kits/auth-core/.nextworks/docs/AUTH_QUICKSTART.md +264 -244
  4. package/dist/kits/auth-core/app/(protected)/settings/profile/profile-form.tsx +120 -114
  5. package/dist/kits/auth-core/app/api/auth/forgot-password/route.ts +116 -114
  6. package/dist/kits/auth-core/app/api/auth/reset-password/route.ts +66 -63
  7. package/dist/kits/auth-core/app/api/auth/send-verify-email/route.ts +1 -1
  8. package/dist/kits/auth-core/app/api/users/[id]/route.ts +134 -127
  9. package/dist/kits/auth-core/app/auth/reset-password/page.tsx +186 -187
  10. package/dist/kits/auth-core/components/auth/dashboard.tsx +25 -2
  11. package/dist/kits/auth-core/components/auth/forgot-password-form.tsx +90 -90
  12. package/dist/kits/auth-core/components/auth/login-form.tsx +492 -467
  13. package/dist/kits/auth-core/components/auth/signup-form.tsx +28 -29
  14. package/dist/kits/auth-core/lib/auth.ts +46 -15
  15. package/dist/kits/auth-core/lib/forms/map-errors.ts +37 -11
  16. package/dist/kits/auth-core/lib/server/result.ts +45 -45
  17. package/dist/kits/auth-core/lib/validation/forms.ts +1 -2
  18. package/dist/kits/auth-core/package-deps.json +4 -2
  19. package/dist/kits/auth-core/types/next-auth.d.ts +1 -1
  20. package/dist/kits/blocks/.nextworks/docs/BLOCKS_QUICKSTART.md +2 -8
  21. package/dist/kits/blocks/.nextworks/docs/THEME_GUIDE.md +18 -1
  22. package/dist/kits/blocks/app/templates/productlaunch/page.tsx +0 -2
  23. package/dist/kits/blocks/components/sections/FAQ.tsx +0 -1
  24. package/dist/kits/blocks/components/sections/Newsletter.tsx +2 -2
  25. package/dist/kits/blocks/components/ui/switch.tsx +78 -78
  26. package/dist/kits/blocks/components/ui/theme-selector.tsx +1 -1
  27. package/dist/kits/blocks/lib/themes.ts +1 -0
  28. package/dist/kits/blocks/package-deps.json +4 -4
  29. package/dist/kits/data/.nextworks/docs/DATA_QUICKSTART.md +128 -112
  30. package/dist/kits/data/.nextworks/docs/DATA_README.md +2 -1
  31. package/dist/kits/data/app/api/posts/[id]/route.ts +83 -83
  32. package/dist/kits/data/app/api/posts/route.ts +136 -138
  33. package/dist/kits/data/app/api/seed-demo/route.ts +1 -2
  34. package/dist/kits/data/app/api/users/[id]/route.ts +29 -17
  35. package/dist/kits/data/app/api/users/check-email/route.ts +1 -1
  36. package/dist/kits/data/app/api/users/check-unique/route.ts +30 -27
  37. package/dist/kits/data/app/api/users/route.ts +0 -2
  38. package/dist/kits/data/app/examples/demo/create-post-form.tsx +108 -106
  39. package/dist/kits/data/app/examples/demo/page.tsx +2 -1
  40. package/dist/kits/data/app/examples/demo/seed-demo-button.tsx +1 -1
  41. package/dist/kits/data/components/admin/posts-manager.tsx +727 -719
  42. package/dist/kits/data/components/admin/users-manager.tsx +435 -432
  43. package/dist/kits/data/lib/server/result.ts +5 -2
  44. package/dist/kits/data/package-deps.json +1 -1
  45. package/dist/kits/data/scripts/seed-demo.mjs +1 -2
  46. package/dist/kits/forms/app/api/wizard/route.ts +76 -71
  47. package/dist/kits/forms/app/examples/forms/server-action/page.tsx +78 -71
  48. package/dist/kits/forms/components/hooks/useCheckUnique.ts +85 -79
  49. package/dist/kits/forms/components/ui/form/form-control.tsx +28 -28
  50. package/dist/kits/forms/components/ui/form/form-description.tsx +23 -22
  51. package/dist/kits/forms/components/ui/form/form-item.tsx +21 -21
  52. package/dist/kits/forms/components/ui/form/form-label.tsx +24 -24
  53. package/dist/kits/forms/components/ui/form/form-message.tsx +28 -29
  54. package/dist/kits/forms/components/ui/switch.tsx +78 -78
  55. package/dist/kits/forms/lib/forms/map-errors.ts +1 -1
  56. package/dist/kits/forms/lib/validation/forms.ts +1 -2
  57. 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
- loading: checkingEmail,
188
- unique: emailUnique,
189
- error: emailUniqueError,
190
- } = useCheckUnique("email", emailValue, 500);
183
+ const { loading: checkingEmail, unique: emailUnique } = useCheckUnique(
184
+ "email",
185
+ emailValue,
186
+ 500,
187
+ );
191
188
 
192
- const useDebouncedValidator = (
193
- fn: (v: any) => Promise<boolean | string | undefined>,
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: any): Promise<boolean | string | undefined> => {
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 (!res.ok || !payload?.success) {
263
- const msg = payload
264
- ? mapApiErrorsToForm(formMethods, payload)
265
- : undefined;
266
- setFormError(msg || payload?.message || "Signup failed");
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
- const err = e as Error;
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 any;
70
- } catch (err: any) {
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
- JSON.parse(err?.message);
81
- throw err; // already structured
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
- (token as any).id = (user as any).id;
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
- (token as any).role =
110
- (user as any).role ?? (token as any).role ?? "user";
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
- const s = session as any;
114
- if (typeof s.name === "string") token.name = s.name;
115
- if (typeof s.email === "string") token.email = s.email;
116
- if (typeof s.role === "string") (token as any).role = s.role;
117
- if (s.image === null || typeof s.image === "string")
118
- (token as any).image = s.image ?? null;
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
- (session.user as any).id = (token as any).id as string;
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
- (session.user as any).role = (token as any).role ?? "user";
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
- export function mapApiErrorsToForm(methods: any, payload: any) {
2
- if (!payload || typeof payload !== "object") return null;
3
- const errors = payload.errors || null;
4
- const message = payload.message || null;
5
- if (errors && typeof errors === "object") {
6
- Object.entries(errors).forEach(([field, msg]) => {
7
- methods.setError(field as any, { type: "server", message: String(msg) });
8
- });
9
- }
10
- return message;
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
  }
@@ -1,7 +1,7 @@
1
1
  // Copyright (c) 2025 Jakob Bro Liebe Hansen
2
2
  // SPDX-License-Identifier: MIT
3
3
 
4
- import NextAuth, { DefaultSession } from "next-auth";
4
+ import type { DefaultSession } from "next-auth";
5
5
 
6
6
  declare module "next-auth" {
7
7
  interface Session {
@@ -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. For the smoothest experience, install Blocks first in your app before adding other kits.
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 already configured with the enhanced theme provider in `app/layout.tsx`. The default theme is set to "monochrome".
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>
@@ -3,7 +3,6 @@
3
3
  import React, { useMemo, useState } from "react";
4
4
  import { cn } from "@/lib/utils";
5
5
  import { ChevronDown, ChevronUp } from "lucide-react";
6
- import { Button } from "../ui/button";
7
6
 
8
7
  /**
9
8
  * Represents a single FAQ item.
@@ -33,8 +33,8 @@ export interface NewsletterProps {
33
33
  input?: { className?: string };
34
34
  button?: {
35
35
  className?: string;
36
- variant?: any;
37
- size?: any;
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 };
@@ -7,7 +7,7 @@ import { useThemeVariant } from "@/components/enhanced-theme-provider";
7
7
  import {
8
8
  themes,
9
9
  darkThemes,
10
- ThemeVariant,
10
+ type ThemeVariant,
11
11
  type ThemeConfig,
12
12
  } from "@/lib/themes";
13
13
 
@@ -1,3 +1,4 @@
1
+ export type { ThemeConfig, ThemeVariant } from "@nextworks/blocks-core";
1
2
  import { type ThemeConfig, type ThemeVariant } from "@nextworks/blocks-core";
2
3
 
3
4
  export const themes: Record<ThemeVariant, ThemeConfig> = {