appos 0.4.3-0 → 0.5.1-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/exports/api/index.d.mts +3 -3
- package/dist/exports/api/index.mjs +34 -9
- package/dist/exports/api/workflows/index.d.mts +2 -2
- package/dist/exports/api/workflows/index.mjs +2 -2
- package/dist/exports/{auth-By0xx0MI.d.mts → auth-BCOIpGDO.d.mts} +325 -214
- package/dist/exports/cli/index.d.mts +105 -106
- package/dist/exports/cli/index.mjs +87 -13
- package/dist/exports/{constants-BePPc_yF.mjs → constants-BicCnEiJ.mjs} +7 -1
- package/dist/exports/{index-Bpo5QE7k.d.mts → index-CiuTp085.d.mts} +580 -614
- package/dist/exports/{openapi-crG3j4xx.mjs → openapi-uisUTLq7.mjs} +45 -102
- package/dist/exports/{send-email-Bgcdjy-e.mjs → send-email-CXxlkfFL.mjs} +1 -1
- package/dist/exports/{server-BlNxgHUc.mjs → server-CA4aI0U6.mjs} +6 -2
- package/dist/exports/storybook/index.d.ts +40 -0
- package/dist/exports/storybook/index.js +43 -0
- package/dist/exports/tests/api.d.mts +2 -2
- package/dist/exports/tests/api.mjs +3 -3
- package/dist/exports/vite/index.d.mts +19 -3
- package/dist/exports/vite/index.mjs +82 -6
- package/dist/exports/web/index.d.ts +2247 -700
- package/dist/exports/web/index.js +2445 -754
- package/dist/exports/web/routes.d.mts +41 -43
- package/dist/exports/web/ssr.d.mts +2 -2
- package/package.json +48 -31
- package/src/storybook/.storybook/main.ts +119 -0
- package/src/storybook/.storybook/manager.ts +75 -0
- package/src/storybook/.storybook/preview.tsx +76 -0
- package/src/storybook/components/ColorGrid.tsx +324 -0
- package/src/storybook/components/IconGallery.tsx +268 -0
- package/src/storybook/components/SpacingScale.tsx +236 -0
- package/src/storybook/components/TypographyScale.tsx +277 -0
- package/src/storybook/components/index.ts +3 -0
- package/src/storybook/docs/Introduction.mdx +29 -0
- package/src/storybook/docs/foundations/Colors.mdx +27 -0
- package/src/storybook/docs/foundations/Icons.mdx +26 -0
- package/src/storybook/docs/foundations/Spacing.mdx +23 -0
- package/src/storybook/docs/foundations/Typography.mdx +39 -0
- package/src/storybook/index.ts +147 -0
- package/src/storybook/shims/void-elements.js +18 -0
- package/src/web/client/auth-paths.ts +148 -0
- package/src/web/client/auth.ts +218 -0
- package/src/web/client/i18n.d.ts +9 -0
- package/src/web/client/i18n.ts +127 -0
- package/src/web/client/react-router.ts +84 -0
- package/src/web/hooks/index.ts +1 -0
- package/src/web/hooks/use-mobile.ts +19 -0
- package/src/web/index.ts +42 -0
- package/src/web/lib/colors.ts +220 -0
- package/src/web/lib/utils.ts +52 -0
- package/src/web/routes/auth/_layout.tsx +5 -11
- package/src/web/routes.ts +101 -0
- package/src/web/server/context.ts +14 -0
- package/src/web/server/render.ts +11 -0
- package/src/web/ssr.ts +17 -0
- package/src/web/ui/accordion.stories.tsx +183 -0
- package/src/web/ui/accordion.tsx +134 -0
- package/src/web/ui/alert-dialog.stories.tsx +215 -0
- package/src/web/ui/alert-dialog.tsx +328 -0
- package/src/web/ui/alert.stories.tsx +224 -0
- package/src/web/ui/alert.tsx +136 -0
- package/src/web/ui/animated-check.stories.tsx +200 -0
- package/src/web/ui/animated-check.tsx +98 -0
- package/src/web/ui/aspect-ratio.stories.tsx +204 -0
- package/src/web/ui/aspect-ratio.tsx +36 -0
- package/src/web/ui/auth/__mocks__/auth-context.tsx +273 -0
- package/src/web/ui/auth/auth-card.stories.tsx +295 -0
- package/src/web/ui/auth/auth-card.tsx +124 -0
- package/src/web/ui/auth/auth-layout.stories.tsx +330 -0
- package/src/web/ui/auth/auth-layout.tsx +269 -0
- package/src/web/ui/auth/captcha-widget.stories.tsx +200 -0
- package/src/web/ui/auth/captcha-widget.tsx +197 -0
- package/src/web/ui/auth/context.tsx +87 -0
- package/src/web/ui/auth/email-input.stories.tsx +167 -0
- package/src/web/ui/auth/email-input.tsx +84 -0
- package/src/web/ui/auth/forms/email-otp-form.tsx +377 -0
- package/src/web/ui/auth/forms/forgot-password-form.tsx +240 -0
- package/src/web/ui/auth/forms/index.ts +40 -0
- package/src/web/ui/auth/forms/login-form.tsx +486 -0
- package/src/web/ui/auth/forms/phone-otp-form.tsx +388 -0
- package/src/web/ui/auth/forms/reset-password-form.tsx +331 -0
- package/src/web/ui/auth/forms/signup-form.tsx +499 -0
- package/src/web/ui/auth/forms/two-factor-form.tsx +410 -0
- package/src/web/ui/auth/forms/verify-email-form.tsx +373 -0
- package/src/web/ui/auth/hooks.ts +266 -0
- package/src/web/ui/auth/index.ts +31 -0
- package/src/web/ui/auth/magic-link-form.tsx +191 -0
- package/src/web/ui/auth/otp-input.stories.tsx +199 -0
- package/src/web/ui/auth/otp-input.tsx +157 -0
- package/src/web/ui/auth/passkey-button.stories.tsx +115 -0
- package/src/web/ui/auth/passkey-button.tsx +113 -0
- package/src/web/ui/auth/passkey-register-button.stories.tsx +109 -0
- package/src/web/ui/auth/passkey-register-button.tsx +106 -0
- package/src/web/ui/auth/password-input.stories.tsx +287 -0
- package/src/web/ui/auth/password-input.tsx +306 -0
- package/src/web/ui/auth/phone-input.stories.tsx +160 -0
- package/src/web/ui/auth/phone-input.tsx +89 -0
- package/src/web/ui/auth/social-buttons.stories.tsx +217 -0
- package/src/web/ui/auth/social-buttons.tsx +196 -0
- package/src/web/ui/auth/sso-button.stories.tsx +160 -0
- package/src/web/ui/auth/sso-button.tsx +108 -0
- package/src/web/ui/auth/trust-signals.stories.tsx +208 -0
- package/src/web/ui/auth/trust-signals.tsx +260 -0
- package/src/web/ui/avatar.stories.tsx +302 -0
- package/src/web/ui/avatar.tsx +189 -0
- package/src/web/ui/badge.stories.tsx +225 -0
- package/src/web/ui/badge.tsx +66 -0
- package/src/web/ui/brand.stories.tsx +266 -0
- package/src/web/ui/brand.tsx +162 -0
- package/src/web/ui/breadcrumb.stories.tsx +271 -0
- package/src/web/ui/breadcrumb.tsx +214 -0
- package/src/web/ui/button-group.stories.tsx +251 -0
- package/src/web/ui/button-group.tsx +87 -0
- package/src/web/ui/button.stories.tsx +264 -0
- package/src/web/ui/button.tsx +120 -0
- package/src/web/ui/calendar.stories.tsx +235 -0
- package/src/web/ui/calendar.tsx +221 -0
- package/src/web/ui/card.stories.tsx +262 -0
- package/src/web/ui/card.tsx +199 -0
- package/src/web/ui/carousel.stories.tsx +244 -0
- package/src/web/ui/carousel.tsx +241 -0
- package/src/web/ui/chart.stories.tsx +833 -0
- package/src/web/ui/chart.tsx +390 -0
- package/src/web/ui/checkbox.stories.tsx +208 -0
- package/src/web/ui/checkbox.tsx +39 -0
- package/src/web/ui/collapsible.stories.tsx +239 -0
- package/src/web/ui/collapsible.tsx +21 -0
- package/src/web/ui/command.stories.tsx +291 -0
- package/src/web/ui/command.tsx +189 -0
- package/src/web/ui/context-menu.stories.tsx +255 -0
- package/src/web/ui/context-menu.tsx +263 -0
- package/src/web/ui/dialog.stories.tsx +263 -0
- package/src/web/ui/dialog.tsx +273 -0
- package/src/web/ui/drawer.stories.tsx +299 -0
- package/src/web/ui/drawer.tsx +130 -0
- package/src/web/ui/dropdown-menu.stories.tsx +320 -0
- package/src/web/ui/dropdown-menu.tsx +263 -0
- package/src/web/ui/empty.stories.tsx +204 -0
- package/src/web/ui/empty.tsx +101 -0
- package/src/web/ui/error-page.stories.tsx +181 -0
- package/src/web/ui/error-page.tsx +136 -0
- package/src/web/ui/field.stories.tsx +364 -0
- package/src/web/ui/field.tsx +252 -0
- package/src/web/ui/globals.css +3 -0
- package/src/web/ui/hover-card.stories.tsx +261 -0
- package/src/web/ui/hover-card.tsx +50 -0
- package/src/web/ui/index.ts +55 -0
- package/src/web/ui/input-group.stories.tsx +290 -0
- package/src/web/ui/input-group.tsx +157 -0
- package/src/web/ui/input-otp.stories.tsx +235 -0
- package/src/web/ui/input-otp.tsx +86 -0
- package/src/web/ui/input.stories.tsx +205 -0
- package/src/web/ui/input.tsx +30 -0
- package/src/web/ui/item.stories.tsx +348 -0
- package/src/web/ui/item.tsx +200 -0
- package/src/web/ui/kbd.stories.tsx +236 -0
- package/src/web/ui/kbd.tsx +27 -0
- package/src/web/ui/label.stories.tsx +180 -0
- package/src/web/ui/label.tsx +32 -0
- package/src/web/ui/locales/en/ui.json +13 -30
- package/src/web/ui/locales/zh-CN/ui.json +17 -34
- package/src/web/ui/locales/zh-TW/ui.json +17 -34
- package/src/web/ui/menubar.stories.tsx +385 -0
- package/src/web/ui/menubar.tsx +273 -0
- package/src/web/ui/navigation-menu.stories.tsx +361 -0
- package/src/web/ui/navigation-menu.tsx +168 -0
- package/src/web/ui/pagination.stories.tsx +312 -0
- package/src/web/ui/pagination.tsx +221 -0
- package/src/web/ui/popover.stories.tsx +315 -0
- package/src/web/ui/popover.tsx +89 -0
- package/src/web/ui/progress.stories.tsx +239 -0
- package/src/web/ui/progress.tsx +128 -0
- package/src/web/ui/radio-group.stories.tsx +315 -0
- package/src/web/ui/radio-group.tsx +38 -0
- package/src/web/ui/resizable.stories.tsx +304 -0
- package/src/web/ui/resizable.tsx +53 -0
- package/src/web/ui/scroll-area.stories.tsx +276 -0
- package/src/web/ui/scroll-area.tsx +53 -0
- package/src/web/ui/select.stories.tsx +314 -0
- package/src/web/ui/select.tsx +312 -0
- package/src/web/ui/separator.stories.tsx +246 -0
- package/src/web/ui/separator.tsx +36 -0
- package/src/web/ui/sheet.stories.tsx +417 -0
- package/src/web/ui/sheet.tsx +133 -0
- package/src/web/ui/sidebar.stories.tsx +402 -0
- package/src/web/ui/sidebar.tsx +723 -0
- package/src/web/ui/skeleton.stories.tsx +235 -0
- package/src/web/ui/skeleton.tsx +26 -0
- package/src/web/ui/slider.stories.tsx +294 -0
- package/src/web/ui/slider.tsx +75 -0
- package/src/web/ui/sonner.stories.tsx +279 -0
- package/src/web/ui/sonner.tsx +46 -0
- package/src/web/ui/spinner.stories.tsx +204 -0
- package/src/web/ui/spinner.tsx +28 -0
- package/src/web/ui/switch.stories.tsx +285 -0
- package/src/web/ui/switch.tsx +43 -0
- package/src/web/ui/table.stories.tsx +362 -0
- package/src/web/ui/table.tsx +115 -0
- package/src/web/ui/tabs.stories.tsx +401 -0
- package/src/web/ui/tabs.tsx +136 -0
- package/src/web/ui/textarea.stories.tsx +272 -0
- package/src/web/ui/textarea.tsx +28 -0
- package/src/web/ui/toggle-group.stories.tsx +334 -0
- package/src/web/ui/toggle-group.tsx +88 -0
- package/src/web/ui/toggle.stories.tsx +264 -0
- package/src/web/ui/toggle.tsx +57 -0
- package/src/web/ui/tooltip.stories.tsx +342 -0
- package/src/web/ui/tooltip.tsx +116 -0
|
@@ -3,7 +3,7 @@ import i18n, { changeLanguage, createInstance, getFixedT, init, loadLanguages, l
|
|
|
3
3
|
import { I18nextProvider, Trans, initReactI18next, initReactI18next as initReactI18next$1, useTranslation, useTranslation as useTranslation$1 } from "react-i18next";
|
|
4
4
|
import { passkeyClient } from "@better-auth/passkey/client";
|
|
5
5
|
import { ssoClient } from "@better-auth/sso/client";
|
|
6
|
-
import { adminClient, anonymousClient, apiKeyClient, emailOTPClient, magicLinkClient, multiSessionClient, phoneNumberClient, twoFactorClient, usernameClient } from "better-auth/client/plugins";
|
|
6
|
+
import { adminClient, anonymousClient, apiKeyClient, emailOTPClient, lastLoginMethodClient, magicLinkClient, multiSessionClient, phoneNumberClient, twoFactorClient, usernameClient } from "better-auth/client/plugins";
|
|
7
7
|
import { createAuthClient } from "better-auth/react";
|
|
8
8
|
import { Form, IDLE_FETCHER, Link, Link as Link$1, Links, Meta, NavLink, Outlet, Route, Scripts, ScrollRestoration, ServerRouter, href, isRouteErrorResponse, matchPath, matchRoutes, parsePath, redirect, redirect as redirect$1, redirectDocument, renderMatches, replace, resolvePath, useActionData, useAsyncError, useAsyncValue, useBeforeUnload, useBlocker, useFetcher, useFetchers, useFormAction, useHref, useInRouterContext, useLinkClickHandler, useLoaderData, useLocation, useMatch, useMatches, useNavigate, useNavigation, useNavigationType, useOutlet, useOutletContext, useParams, useResolvedPath, useRevalidator, useRouteError, useRouteError as useRouteError$1, useRouteLoaderData, useRoutes, useSearchParams, useSubmit, useViewTransitionState } from "react-router";
|
|
9
9
|
import LanguageDetector from "i18next-browser-languagedetector";
|
|
@@ -13,22 +13,26 @@ import * as React from "react";
|
|
|
13
13
|
import { createContext, useCallback, useContext, useEffect, useId, useMemo, useRef, useState } from "react";
|
|
14
14
|
import { clsx } from "clsx";
|
|
15
15
|
import { twMerge } from "tailwind-merge";
|
|
16
|
+
import { z } from "zod";
|
|
16
17
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
17
18
|
import { cva } from "class-variance-authority";
|
|
18
|
-
import {
|
|
19
|
+
import { CaptchaFox } from "@captchafox/react";
|
|
20
|
+
import HCaptcha from "@hcaptcha/react-hcaptcha";
|
|
21
|
+
import { Turnstile } from "@marsidev/react-turnstile";
|
|
22
|
+
import { AlertCircleIcon, AlertTriangleIcon, ArrowLeftIcon, AtSignIcon, Building2Icon, CheckCircleIcon, CheckIcon, ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon, ChevronUpIcon, CircleCheckIcon, CircleIcon, EyeIcon, EyeOffIcon, FingerprintIcon, HomeIcon, InfoIcon, KeyIcon, Loader2Icon, LoaderCircleIcon, LockIcon, MailIcon, MinusIcon, MoreHorizontalIcon, OctagonXIcon, PanelLeftIcon, PhoneIcon, PlusIcon, RefreshCwIcon, SearchIcon, SettingsIcon, ShieldCheckIcon, TriangleAlertIcon, UserIcon, XIcon } from "lucide-react";
|
|
23
|
+
import ReCAPTCHA from "react-google-recaptcha";
|
|
19
24
|
import { Controller, FormProvider, useForm, useFormContext } from "react-hook-form";
|
|
20
25
|
import { Separator as Separator$1 } from "@base-ui/react/separator";
|
|
21
26
|
import { Input as Input$1 } from "@base-ui/react/input";
|
|
22
27
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
23
|
-
import { z } from "zod";
|
|
24
28
|
import { Button as Button$1 } from "@base-ui/react/button";
|
|
25
29
|
import { OTPInput, OTPInputContext } from "input-otp";
|
|
26
30
|
import { Checkbox as Checkbox$1 } from "@base-ui/react/checkbox";
|
|
31
|
+
import { mergeProps } from "@base-ui/react/merge-props";
|
|
32
|
+
import { useRender } from "@base-ui/react/use-render";
|
|
27
33
|
import { Accordion as Accordion$1 } from "@base-ui/react/accordion";
|
|
28
34
|
import { AlertDialog as AlertDialog$1 } from "@base-ui/react/alert-dialog";
|
|
29
35
|
import { Avatar as Avatar$1 } from "@base-ui/react/avatar";
|
|
30
|
-
import { mergeProps } from "@base-ui/react/merge-props";
|
|
31
|
-
import { useRender } from "@base-ui/react/use-render";
|
|
32
36
|
import { DayPicker, getDefaultClassNames } from "react-day-picker";
|
|
33
37
|
import useEmblaCarousel from "embla-carousel-react";
|
|
34
38
|
import * as RechartsPrimitive from "recharts";
|
|
@@ -77,10 +81,10 @@ function defineAuthFeatures(config) {
|
|
|
77
81
|
phoneOtp: config.methods?.phoneOtp !== void 0,
|
|
78
82
|
emailOtp: config.methods?.emailOtp !== void 0,
|
|
79
83
|
oauth: {
|
|
80
|
-
google: config.oauth?.google === true,
|
|
81
|
-
github: config.oauth?.github === true,
|
|
82
|
-
apple: config.oauth?.apple === true,
|
|
83
|
-
facebook: config.oauth?.facebook === true
|
|
84
|
+
google: config.methods?.oauth?.google === true,
|
|
85
|
+
github: config.methods?.oauth?.github === true,
|
|
86
|
+
apple: config.methods?.oauth?.apple === true,
|
|
87
|
+
facebook: config.methods?.oauth?.facebook === true
|
|
84
88
|
},
|
|
85
89
|
twoFactor: {
|
|
86
90
|
enabled: config.plugins?.twoFactor !== void 0,
|
|
@@ -91,7 +95,13 @@ function defineAuthFeatures(config) {
|
|
|
91
95
|
multiSession: config.plugins?.multiSession !== void 0,
|
|
92
96
|
username: config.plugins?.username !== void 0,
|
|
93
97
|
anonymous: config.plugins?.anonymous !== void 0,
|
|
94
|
-
sso: config.plugins?.sso !== void 0
|
|
98
|
+
sso: config.plugins?.sso !== void 0,
|
|
99
|
+
lastUsedMethod: config.plugins?.lastUsedMethod !== void 0,
|
|
100
|
+
captcha: {
|
|
101
|
+
enabled: config.plugins?.captcha !== void 0,
|
|
102
|
+
provider: config.plugins?.captcha?.provider ?? null,
|
|
103
|
+
siteKey: config.plugins?.captcha?.siteKey ?? null
|
|
104
|
+
}
|
|
95
105
|
};
|
|
96
106
|
}
|
|
97
107
|
/**
|
|
@@ -115,6 +125,7 @@ function defineAuthClient(opts) {
|
|
|
115
125
|
if (config.plugins?.anonymous) plugins.push(anonymousClient());
|
|
116
126
|
if (config.plugins?.multiSession) plugins.push(multiSessionClient());
|
|
117
127
|
if (config.plugins?.sso) plugins.push(ssoClient({ domainVerification: config.plugins.sso.domainVerification ? { enabled: true } : void 0 }));
|
|
128
|
+
if (config.plugins?.lastUsedMethod) plugins.push(lastLoginMethodClient());
|
|
118
129
|
return {
|
|
119
130
|
client: createAuthClient({
|
|
120
131
|
baseURL: (baseURL ?? config.baseURL ?? AUTH_BASE_URL) || void 0,
|
|
@@ -277,9 +288,70 @@ function useIsMobile(mobileBreakpoint = 768) {
|
|
|
277
288
|
function cn(...inputs) {
|
|
278
289
|
return twMerge(clsx(inputs));
|
|
279
290
|
}
|
|
291
|
+
/**
|
|
292
|
+
* Standard size variants used across UI components.
|
|
293
|
+
*
|
|
294
|
+
* @example
|
|
295
|
+
* ```tsx
|
|
296
|
+
* import { type Size, sizes } from "#src/web/lib/utils.ts";
|
|
297
|
+
*
|
|
298
|
+
* interface Props {
|
|
299
|
+
* size?: Size;
|
|
300
|
+
* }
|
|
301
|
+
*
|
|
302
|
+
* // In stories
|
|
303
|
+
* argTypes: {
|
|
304
|
+
* size: { control: "select", options: sizes },
|
|
305
|
+
* }
|
|
306
|
+
* ```
|
|
307
|
+
*/
|
|
308
|
+
const sizeSchema = z.enum([
|
|
309
|
+
"xs",
|
|
310
|
+
"sm",
|
|
311
|
+
"default",
|
|
312
|
+
"lg",
|
|
313
|
+
"xl"
|
|
314
|
+
]);
|
|
315
|
+
/** Available sizes for storybook controls. */
|
|
316
|
+
const sizes = sizeSchema.options;
|
|
317
|
+
/**
|
|
318
|
+
* Standard orientation variants for layout components.
|
|
319
|
+
*
|
|
320
|
+
* @example
|
|
321
|
+
* ```tsx
|
|
322
|
+
* import { type Orientation, orientations } from "#src/web/lib/utils.ts";
|
|
323
|
+
*
|
|
324
|
+
* interface Props {
|
|
325
|
+
* orientation?: Orientation;
|
|
326
|
+
* }
|
|
327
|
+
* ```
|
|
328
|
+
*/
|
|
329
|
+
const orientationSchema = z.enum(["horizontal", "vertical"]);
|
|
330
|
+
/** Available orientation keys for storybook controls. */
|
|
331
|
+
const orientations = orientationSchema.options;
|
|
280
332
|
|
|
281
333
|
//#endregion
|
|
282
334
|
//#region src/web/ui/card.tsx
|
|
335
|
+
/**
|
|
336
|
+
* A container component for grouping related content and actions.
|
|
337
|
+
*
|
|
338
|
+
* Provides a visual boundary with optional header, content, and footer
|
|
339
|
+
* sections. Supports multiple sizes for different layout needs.
|
|
340
|
+
*
|
|
341
|
+
* @example
|
|
342
|
+
* ```tsx
|
|
343
|
+
* <Card>
|
|
344
|
+
* <CardHeader>
|
|
345
|
+
* <CardTitle>Card Title</CardTitle>
|
|
346
|
+
* <CardDescription>Card description text.</CardDescription>
|
|
347
|
+
* </CardHeader>
|
|
348
|
+
* <CardContent>Main content goes here.</CardContent>
|
|
349
|
+
* <CardFooter>
|
|
350
|
+
* <Button>Action</Button>
|
|
351
|
+
* </CardFooter>
|
|
352
|
+
* </Card>
|
|
353
|
+
* ```
|
|
354
|
+
*/
|
|
283
355
|
function Card({ className, size = "default", ...props }) {
|
|
284
356
|
return /* @__PURE__ */ jsx("div", {
|
|
285
357
|
"data-slot": "card",
|
|
@@ -288,6 +360,20 @@ function Card({ className, size = "default", ...props }) {
|
|
|
288
360
|
...props
|
|
289
361
|
});
|
|
290
362
|
}
|
|
363
|
+
/**
|
|
364
|
+
* The header section of a Card containing title and description.
|
|
365
|
+
*
|
|
366
|
+
* Supports an optional CardAction slot for buttons or menus positioned
|
|
367
|
+
* in the top-right corner.
|
|
368
|
+
*
|
|
369
|
+
* @example
|
|
370
|
+
* ```tsx
|
|
371
|
+
* <CardHeader>
|
|
372
|
+
* <CardTitle>Settings</CardTitle>
|
|
373
|
+
* <CardDescription>Manage your preferences.</CardDescription>
|
|
374
|
+
* </CardHeader>
|
|
375
|
+
* ```
|
|
376
|
+
*/
|
|
291
377
|
function CardHeader({ className, ...props }) {
|
|
292
378
|
return /* @__PURE__ */ jsx("div", {
|
|
293
379
|
"data-slot": "card-header",
|
|
@@ -295,6 +381,17 @@ function CardHeader({ className, ...props }) {
|
|
|
295
381
|
...props
|
|
296
382
|
});
|
|
297
383
|
}
|
|
384
|
+
/**
|
|
385
|
+
* The title text displayed in a CardHeader.
|
|
386
|
+
*
|
|
387
|
+
* Renders with medium font weight and appropriate sizing based on the
|
|
388
|
+
* parent Card's size prop.
|
|
389
|
+
*
|
|
390
|
+
* @example
|
|
391
|
+
* ```tsx
|
|
392
|
+
* <CardTitle>Account Settings</CardTitle>
|
|
393
|
+
* ```
|
|
394
|
+
*/
|
|
298
395
|
function CardTitle({ className, ...props }) {
|
|
299
396
|
return /* @__PURE__ */ jsx("div", {
|
|
300
397
|
"data-slot": "card-title",
|
|
@@ -302,6 +399,16 @@ function CardTitle({ className, ...props }) {
|
|
|
302
399
|
...props
|
|
303
400
|
});
|
|
304
401
|
}
|
|
402
|
+
/**
|
|
403
|
+
* A secondary text element providing context for a Card.
|
|
404
|
+
*
|
|
405
|
+
* Displays in muted foreground color below the CardTitle.
|
|
406
|
+
*
|
|
407
|
+
* @example
|
|
408
|
+
* ```tsx
|
|
409
|
+
* <CardDescription>Manage your account settings and preferences.</CardDescription>
|
|
410
|
+
* ```
|
|
411
|
+
*/
|
|
305
412
|
function CardDescription({ className, ...props }) {
|
|
306
413
|
return /* @__PURE__ */ jsx("div", {
|
|
307
414
|
"data-slot": "card-description",
|
|
@@ -309,6 +416,21 @@ function CardDescription({ className, ...props }) {
|
|
|
309
416
|
...props
|
|
310
417
|
});
|
|
311
418
|
}
|
|
419
|
+
/**
|
|
420
|
+
* A slot for action buttons positioned in the CardHeader.
|
|
421
|
+
*
|
|
422
|
+
* Automatically positioned in the top-right corner of the header.
|
|
423
|
+
* Use for dropdown menus, edit buttons, or other quick actions.
|
|
424
|
+
*
|
|
425
|
+
* @example
|
|
426
|
+
* ```tsx
|
|
427
|
+
* <CardAction>
|
|
428
|
+
* <Button variant="ghost" size="icon-sm">
|
|
429
|
+
* <MoreHorizontalIcon />
|
|
430
|
+
* </Button>
|
|
431
|
+
* </CardAction>
|
|
432
|
+
* ```
|
|
433
|
+
*/
|
|
312
434
|
function CardAction({ className, ...props }) {
|
|
313
435
|
return /* @__PURE__ */ jsx("div", {
|
|
314
436
|
"data-slot": "card-action",
|
|
@@ -316,6 +438,19 @@ function CardAction({ className, ...props }) {
|
|
|
316
438
|
...props
|
|
317
439
|
});
|
|
318
440
|
}
|
|
441
|
+
/**
|
|
442
|
+
* The main content area of a Card.
|
|
443
|
+
*
|
|
444
|
+
* Provides consistent horizontal padding. Use for the primary content
|
|
445
|
+
* of the card such as forms, lists, or text.
|
|
446
|
+
*
|
|
447
|
+
* @example
|
|
448
|
+
* ```tsx
|
|
449
|
+
* <CardContent>
|
|
450
|
+
* <p>Your content goes here.</p>
|
|
451
|
+
* </CardContent>
|
|
452
|
+
* ```
|
|
453
|
+
*/
|
|
319
454
|
function CardContent({ className, ...props }) {
|
|
320
455
|
return /* @__PURE__ */ jsx("div", {
|
|
321
456
|
"data-slot": "card-content",
|
|
@@ -323,6 +458,20 @@ function CardContent({ className, ...props }) {
|
|
|
323
458
|
...props
|
|
324
459
|
});
|
|
325
460
|
}
|
|
461
|
+
/**
|
|
462
|
+
* The footer section of a Card for actions or supplementary content.
|
|
463
|
+
*
|
|
464
|
+
* Typically contains action buttons aligned horizontally. Provides
|
|
465
|
+
* consistent padding that adapts to the Card's size.
|
|
466
|
+
*
|
|
467
|
+
* @example
|
|
468
|
+
* ```tsx
|
|
469
|
+
* <CardFooter>
|
|
470
|
+
* <Button variant="outline">Cancel</Button>
|
|
471
|
+
* <Button>Save</Button>
|
|
472
|
+
* </CardFooter>
|
|
473
|
+
* ```
|
|
474
|
+
*/
|
|
326
475
|
function CardFooter({ className, ...props }) {
|
|
327
476
|
return /* @__PURE__ */ jsx("div", {
|
|
328
477
|
"data-slot": "card-footer",
|
|
@@ -403,7 +552,7 @@ const brandVariants = cva("inline-flex items-center select-none shrink-0", {
|
|
|
403
552
|
size: {
|
|
404
553
|
xs: "gap-1",
|
|
405
554
|
sm: "gap-1",
|
|
406
|
-
|
|
555
|
+
default: "gap-1.5",
|
|
407
556
|
lg: "gap-2",
|
|
408
557
|
xl: "gap-2"
|
|
409
558
|
},
|
|
@@ -413,7 +562,7 @@ const brandVariants = cva("inline-flex items-center select-none shrink-0", {
|
|
|
413
562
|
}
|
|
414
563
|
},
|
|
415
564
|
defaultVariants: {
|
|
416
|
-
size: "
|
|
565
|
+
size: "default",
|
|
417
566
|
orientation: "horizontal"
|
|
418
567
|
}
|
|
419
568
|
});
|
|
@@ -421,21 +570,21 @@ const logoVariants = cva("shrink-0 flex items-center justify-center", {
|
|
|
421
570
|
variants: { size: {
|
|
422
571
|
xs: "size-6 [&>svg]:size-6",
|
|
423
572
|
sm: "size-7 [&>svg]:size-7",
|
|
424
|
-
|
|
573
|
+
default: "size-8 [&>svg]:size-8",
|
|
425
574
|
lg: "size-10 [&>svg]:size-10",
|
|
426
575
|
xl: "size-12 [&>svg]:size-12"
|
|
427
576
|
} },
|
|
428
|
-
defaultVariants: { size: "
|
|
577
|
+
defaultVariants: { size: "default" }
|
|
429
578
|
});
|
|
430
579
|
const nameVariants = cva("font-semibold tracking-tight text-foreground", {
|
|
431
580
|
variants: { size: {
|
|
432
581
|
xs: "text-sm",
|
|
433
582
|
sm: "text-base",
|
|
434
|
-
|
|
583
|
+
default: "text-lg",
|
|
435
584
|
lg: "text-xl",
|
|
436
585
|
xl: "text-2xl"
|
|
437
586
|
} },
|
|
438
|
-
defaultVariants: { size: "
|
|
587
|
+
defaultVariants: { size: "default" }
|
|
439
588
|
});
|
|
440
589
|
/**
|
|
441
590
|
* Brand component for displaying app/company logo and name with proportionate sizing.
|
|
@@ -473,7 +622,7 @@ const nameVariants = cva("font-semibold tracking-tight text-foreground", {
|
|
|
473
622
|
* </Link>
|
|
474
623
|
* ```
|
|
475
624
|
*/
|
|
476
|
-
function Brand({ className, logo, logoAlt, name, orientation = "horizontal", size = "
|
|
625
|
+
function Brand({ className, logo, logoAlt, name, orientation = "horizontal", size = "default", to = "/" }) {
|
|
477
626
|
const renderLogo = () => {
|
|
478
627
|
if (!logo) return null;
|
|
479
628
|
if (typeof logo === "string") return /* @__PURE__ */ jsx("div", {
|
|
@@ -520,7 +669,7 @@ function Brand({ className, logo, logoAlt, name, orientation = "horizontal", siz
|
|
|
520
669
|
* />
|
|
521
670
|
* ```
|
|
522
671
|
*/
|
|
523
|
-
function TrustSignals({ showSSL, showPrivacyLink, privacyUrl = "/privacy", showTermsLink, termsUrl = "/terms", complianceBadges, customBadges, securityMessage, className }) {
|
|
672
|
+
function TrustSignals({ showSSL, showPrivacyLink, privacyUrl = "/legal/privacy-policy", showTermsLink, termsUrl = "/legal/terms", complianceBadges, customBadges, securityMessage, className }) {
|
|
524
673
|
const { t: t$1 } = useTranslation$1(["ui"]);
|
|
525
674
|
const hasLinks = showPrivacyLink || showTermsLink;
|
|
526
675
|
const hasBadges = complianceBadges && complianceBadges.length > 0 || customBadges && customBadges.length > 0;
|
|
@@ -695,7 +844,7 @@ function AuthLayout({ variant = "centered", children, brand, brandContent, trust
|
|
|
695
844
|
className: "flex justify-center",
|
|
696
845
|
children: /* @__PURE__ */ jsx(Brand, {
|
|
697
846
|
...brand,
|
|
698
|
-
size: brand.size ?? "
|
|
847
|
+
size: brand.size ?? "default"
|
|
699
848
|
})
|
|
700
849
|
}), children]
|
|
701
850
|
}), trustSignals && /* @__PURE__ */ jsx("div", {
|
|
@@ -869,8 +1018,330 @@ function useAuthContext() {
|
|
|
869
1018
|
return context;
|
|
870
1019
|
}
|
|
871
1020
|
|
|
1021
|
+
//#endregion
|
|
1022
|
+
//#region src/web/ui/auth/hooks.ts
|
|
1023
|
+
/**
|
|
1024
|
+
* Hook to access the Better Auth client, features, and config.
|
|
1025
|
+
*
|
|
1026
|
+
* Provides unified access to all auth-related context values:
|
|
1027
|
+
* - `client`: The Better Auth client for making auth API calls.
|
|
1028
|
+
* - `features`: Derived feature flags for conditional rendering based on enabled auth methods.
|
|
1029
|
+
* - `config`: The auth configuration values for forms and validation.
|
|
1030
|
+
*
|
|
1031
|
+
* @returns An object containing the auth client, features, and config.
|
|
1032
|
+
*
|
|
1033
|
+
* @example
|
|
1034
|
+
* ```tsx
|
|
1035
|
+
* // Access client for auth operations
|
|
1036
|
+
* const { client } = useAuth();
|
|
1037
|
+
* await client.signIn.email({ email, password });
|
|
1038
|
+
*
|
|
1039
|
+
* // Access features for conditional rendering
|
|
1040
|
+
* const { features } = useAuth();
|
|
1041
|
+
* if (features.oauth.google) {
|
|
1042
|
+
* // Show Google login button
|
|
1043
|
+
* }
|
|
1044
|
+
*
|
|
1045
|
+
* // Access config for form validation
|
|
1046
|
+
* const { config } = useAuth();
|
|
1047
|
+
* const minPassword = config.methods?.emailPassword?.minPasswordLength ?? 8;
|
|
1048
|
+
* ```
|
|
1049
|
+
*/
|
|
1050
|
+
function useAuth() {
|
|
1051
|
+
const { client, features, config } = useAuthContext();
|
|
1052
|
+
return {
|
|
1053
|
+
client,
|
|
1054
|
+
features,
|
|
1055
|
+
config
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
/**
|
|
1059
|
+
* Hook for managing auth form state.
|
|
1060
|
+
* Provides loading, error, and success state management.
|
|
1061
|
+
*
|
|
1062
|
+
* @example
|
|
1063
|
+
* ```tsx
|
|
1064
|
+
* const { state, execute, reset } = useAuthForm();
|
|
1065
|
+
*
|
|
1066
|
+
* const onSubmit = async (data) => {
|
|
1067
|
+
* await execute(async () => {
|
|
1068
|
+
* await client.signIn.email(data);
|
|
1069
|
+
* });
|
|
1070
|
+
* };
|
|
1071
|
+
*
|
|
1072
|
+
* if (state.error) {
|
|
1073
|
+
* // Show error message
|
|
1074
|
+
* }
|
|
1075
|
+
* ```
|
|
1076
|
+
*/
|
|
1077
|
+
function useAuthForm() {
|
|
1078
|
+
const [state, setState] = useState({
|
|
1079
|
+
isLoading: false,
|
|
1080
|
+
error: null,
|
|
1081
|
+
isSuccess: false
|
|
1082
|
+
});
|
|
1083
|
+
return {
|
|
1084
|
+
state,
|
|
1085
|
+
execute: useCallback(async (fn) => {
|
|
1086
|
+
setState({
|
|
1087
|
+
isLoading: true,
|
|
1088
|
+
error: null,
|
|
1089
|
+
isSuccess: false
|
|
1090
|
+
});
|
|
1091
|
+
try {
|
|
1092
|
+
const result = await fn();
|
|
1093
|
+
setState({
|
|
1094
|
+
isLoading: false,
|
|
1095
|
+
error: null,
|
|
1096
|
+
isSuccess: true
|
|
1097
|
+
});
|
|
1098
|
+
return result;
|
|
1099
|
+
} catch (err) {
|
|
1100
|
+
setState({
|
|
1101
|
+
isLoading: false,
|
|
1102
|
+
error: err instanceof Error ? err.message : "An error occurred",
|
|
1103
|
+
isSuccess: false
|
|
1104
|
+
});
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
}, []),
|
|
1108
|
+
reset: useCallback(() => {
|
|
1109
|
+
setState({
|
|
1110
|
+
isLoading: false,
|
|
1111
|
+
error: null,
|
|
1112
|
+
isSuccess: false
|
|
1113
|
+
});
|
|
1114
|
+
}, []),
|
|
1115
|
+
setError: useCallback((error) => {
|
|
1116
|
+
setState((prev) => ({
|
|
1117
|
+
...prev,
|
|
1118
|
+
error,
|
|
1119
|
+
isSuccess: false
|
|
1120
|
+
}));
|
|
1121
|
+
}, []),
|
|
1122
|
+
isLoading: state.isLoading,
|
|
1123
|
+
error: state.error,
|
|
1124
|
+
isSuccess: state.isSuccess
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
/**
|
|
1128
|
+
* Hook to get and manage the last used login method.
|
|
1129
|
+
* Returns null values if the lastUsedMethod feature is not enabled.
|
|
1130
|
+
*
|
|
1131
|
+
* @returns An object with lastMethod, isLastMethod helper, and clearLastMethod function.
|
|
1132
|
+
*
|
|
1133
|
+
* @example
|
|
1134
|
+
* ```tsx
|
|
1135
|
+
* const { lastMethod, isLastMethod } = useLastUsedMethod();
|
|
1136
|
+
*
|
|
1137
|
+
* // Check if a specific method was last used
|
|
1138
|
+
* if (isLastMethod("google")) {
|
|
1139
|
+
* // Highlight Google button
|
|
1140
|
+
* }
|
|
1141
|
+
*
|
|
1142
|
+
* // Display the last method
|
|
1143
|
+
* {lastMethod && <Badge>Last used: {lastMethod}</Badge>}
|
|
1144
|
+
* ```
|
|
1145
|
+
*/
|
|
1146
|
+
function useLastUsedMethod() {
|
|
1147
|
+
const { client, features } = useAuthContext();
|
|
1148
|
+
return useMemo(() => {
|
|
1149
|
+
if (!features.lastUsedMethod) return {
|
|
1150
|
+
lastMethod: null,
|
|
1151
|
+
isLastMethod: () => false,
|
|
1152
|
+
clearLastMethod: () => {}
|
|
1153
|
+
};
|
|
1154
|
+
const lastLoginMethodPlugin = client.lastLoginMethod;
|
|
1155
|
+
if (!lastLoginMethodPlugin) return {
|
|
1156
|
+
lastMethod: null,
|
|
1157
|
+
isLastMethod: () => false,
|
|
1158
|
+
clearLastMethod: () => {}
|
|
1159
|
+
};
|
|
1160
|
+
return {
|
|
1161
|
+
lastMethod: lastLoginMethodPlugin.getLastUsedLoginMethod() ?? null,
|
|
1162
|
+
isLastMethod: (method) => lastLoginMethodPlugin.isLastUsedLoginMethod(method),
|
|
1163
|
+
clearLastMethod: () => lastLoginMethodPlugin.clearLastUsedLoginMethod()
|
|
1164
|
+
};
|
|
1165
|
+
}, [client, features.lastUsedMethod]);
|
|
1166
|
+
}
|
|
1167
|
+
/**
|
|
1168
|
+
* Hook to get captcha configuration for rendering widgets.
|
|
1169
|
+
*
|
|
1170
|
+
* Apps need to install and render the appropriate captcha widget based on the provider:
|
|
1171
|
+
* - `cloudflare-turnstile`: Use `@marsidev/react-turnstile`
|
|
1172
|
+
* - `google-recaptcha`: Use `react-google-recaptcha` (v2 or v3 based on server minScore)
|
|
1173
|
+
* - `hcaptcha`: Use `@hcaptcha/react-hcaptcha`
|
|
1174
|
+
* - `captchafox`: Use `@captchafox/react`
|
|
1175
|
+
*
|
|
1176
|
+
* Pass the captcha token to auth forms via the `captchaToken` prop.
|
|
1177
|
+
*
|
|
1178
|
+
* @returns Captcha configuration including provider and siteKey.
|
|
1179
|
+
*
|
|
1180
|
+
* @example
|
|
1181
|
+
* ```tsx
|
|
1182
|
+
* import { Turnstile } from "@marsidev/react-turnstile";
|
|
1183
|
+
*
|
|
1184
|
+
* function LoginPage() {
|
|
1185
|
+
* const { enabled, provider, siteKey } = useCaptcha();
|
|
1186
|
+
* const [captchaToken, setCaptchaToken] = useState<string | null>(null);
|
|
1187
|
+
*
|
|
1188
|
+
* return (
|
|
1189
|
+
* <>
|
|
1190
|
+
* {enabled && provider === "cloudflare-turnstile" && siteKey && (
|
|
1191
|
+
* <Turnstile
|
|
1192
|
+
* siteKey={siteKey}
|
|
1193
|
+
* onSuccess={setCaptchaToken}
|
|
1194
|
+
* />
|
|
1195
|
+
* )}
|
|
1196
|
+
* <LoginForm captchaToken={captchaToken} />
|
|
1197
|
+
* </>
|
|
1198
|
+
* );
|
|
1199
|
+
* }
|
|
1200
|
+
* ```
|
|
1201
|
+
*/
|
|
1202
|
+
function useCaptcha() {
|
|
1203
|
+
const { features } = useAuthContext();
|
|
1204
|
+
return features.captcha;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
//#endregion
|
|
1208
|
+
//#region src/web/ui/auth/captcha-widget.tsx
|
|
1209
|
+
/**
|
|
1210
|
+
* Unified captcha widget that renders the appropriate provider based on config.
|
|
1211
|
+
* Supports Cloudflare Turnstile, Google reCAPTCHA, hCaptcha, and CaptchaFox.
|
|
1212
|
+
*
|
|
1213
|
+
* @example
|
|
1214
|
+
* ```tsx
|
|
1215
|
+
* const [captchaToken, setCaptchaToken] = useState<string | null>(null);
|
|
1216
|
+
* const captchaResetRef = useRef<(() => void) | null>(null);
|
|
1217
|
+
*
|
|
1218
|
+
* <CaptchaWidget
|
|
1219
|
+
* onSuccess={setCaptchaToken}
|
|
1220
|
+
* onExpire={() => setCaptchaToken(null)}
|
|
1221
|
+
* resetRef={captchaResetRef}
|
|
1222
|
+
* />
|
|
1223
|
+
*
|
|
1224
|
+
* // Reset on form error:
|
|
1225
|
+
* captchaResetRef.current?.();
|
|
1226
|
+
* ```
|
|
1227
|
+
*/
|
|
1228
|
+
function CaptchaWidget({ onSuccess, onError, onExpire, resetRef, className }) {
|
|
1229
|
+
const { t: t$1 } = useTranslation$1(["ui"]);
|
|
1230
|
+
const captcha = useCaptcha();
|
|
1231
|
+
const turnstileRef = useRef(null);
|
|
1232
|
+
const recaptchaRef = useRef(null);
|
|
1233
|
+
const hcaptchaRef = useRef(null);
|
|
1234
|
+
const captchafoxRef = useRef(null);
|
|
1235
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
1236
|
+
const handleReset = useCallback(() => {
|
|
1237
|
+
turnstileRef.current?.reset();
|
|
1238
|
+
recaptchaRef.current?.reset();
|
|
1239
|
+
hcaptchaRef.current?.resetCaptcha();
|
|
1240
|
+
captchafoxRef.current?.reset();
|
|
1241
|
+
}, []);
|
|
1242
|
+
const handleWidgetLoad = useCallback(() => {
|
|
1243
|
+
setIsLoading(false);
|
|
1244
|
+
}, []);
|
|
1245
|
+
if (resetRef) resetRef.current = handleReset;
|
|
1246
|
+
const handleSuccess = useCallback((token) => {
|
|
1247
|
+
onSuccess(token);
|
|
1248
|
+
}, [onSuccess]);
|
|
1249
|
+
const handleError = useCallback(() => {
|
|
1250
|
+
onError?.();
|
|
1251
|
+
}, [onError]);
|
|
1252
|
+
const handleExpire = useCallback(() => {
|
|
1253
|
+
onExpire?.();
|
|
1254
|
+
}, [onExpire]);
|
|
1255
|
+
if (!captcha.enabled || !captcha.siteKey) return null;
|
|
1256
|
+
const loadingIndicator = isLoading && /* @__PURE__ */ jsxs("div", {
|
|
1257
|
+
className: "flex h-16.25 w-full items-center justify-center rounded-md border border-border bg-muted/30",
|
|
1258
|
+
children: [/* @__PURE__ */ jsx(LoaderCircleIcon, { className: "size-5 animate-spin text-muted-foreground" }), /* @__PURE__ */ jsx("span", {
|
|
1259
|
+
className: "ml-2 text-sm text-muted-foreground",
|
|
1260
|
+
children: t$1("ui:captcha.loading")
|
|
1261
|
+
})]
|
|
1262
|
+
});
|
|
1263
|
+
switch (captcha.provider) {
|
|
1264
|
+
case "cloudflare-turnstile": return /* @__PURE__ */ jsxs("div", {
|
|
1265
|
+
className: cn("flex flex-col gap-2", className),
|
|
1266
|
+
children: [loadingIndicator, /* @__PURE__ */ jsx("div", {
|
|
1267
|
+
className: cn(isLoading && "sr-only"),
|
|
1268
|
+
children: /* @__PURE__ */ jsx(Turnstile, {
|
|
1269
|
+
ref: turnstileRef,
|
|
1270
|
+
siteKey: captcha.siteKey,
|
|
1271
|
+
onSuccess: handleSuccess,
|
|
1272
|
+
onError: handleError,
|
|
1273
|
+
onExpire: handleExpire,
|
|
1274
|
+
onWidgetLoad: handleWidgetLoad,
|
|
1275
|
+
options: {
|
|
1276
|
+
theme: "auto",
|
|
1277
|
+
size: "flexible"
|
|
1278
|
+
}
|
|
1279
|
+
})
|
|
1280
|
+
})]
|
|
1281
|
+
});
|
|
1282
|
+
case "google-recaptcha": return /* @__PURE__ */ jsxs("div", {
|
|
1283
|
+
className: cn("flex flex-col gap-2", className),
|
|
1284
|
+
children: [loadingIndicator, /* @__PURE__ */ jsx("div", {
|
|
1285
|
+
className: cn(isLoading && "sr-only"),
|
|
1286
|
+
children: /* @__PURE__ */ jsx(ReCAPTCHA, {
|
|
1287
|
+
ref: recaptchaRef,
|
|
1288
|
+
sitekey: captcha.siteKey,
|
|
1289
|
+
onChange: (token) => {
|
|
1290
|
+
if (token) handleSuccess(token);
|
|
1291
|
+
else handleExpire();
|
|
1292
|
+
},
|
|
1293
|
+
onErrored: handleError,
|
|
1294
|
+
onExpired: handleExpire,
|
|
1295
|
+
asyncScriptOnLoad: handleWidgetLoad
|
|
1296
|
+
})
|
|
1297
|
+
})]
|
|
1298
|
+
});
|
|
1299
|
+
case "hcaptcha": return /* @__PURE__ */ jsxs("div", {
|
|
1300
|
+
className: cn("flex flex-col gap-2", className),
|
|
1301
|
+
children: [loadingIndicator, /* @__PURE__ */ jsx("div", {
|
|
1302
|
+
className: cn(isLoading && "sr-only"),
|
|
1303
|
+
children: /* @__PURE__ */ jsx(HCaptcha, {
|
|
1304
|
+
ref: hcaptchaRef,
|
|
1305
|
+
sitekey: captcha.siteKey,
|
|
1306
|
+
onVerify: handleSuccess,
|
|
1307
|
+
onError: handleError,
|
|
1308
|
+
onExpire: handleExpire,
|
|
1309
|
+
onLoad: handleWidgetLoad
|
|
1310
|
+
})
|
|
1311
|
+
})]
|
|
1312
|
+
});
|
|
1313
|
+
case "captchafox": return /* @__PURE__ */ jsxs("div", {
|
|
1314
|
+
className: cn("flex flex-col gap-2", className),
|
|
1315
|
+
children: [loadingIndicator, /* @__PURE__ */ jsx("div", {
|
|
1316
|
+
className: cn(isLoading && "sr-only"),
|
|
1317
|
+
children: /* @__PURE__ */ jsx(CaptchaFox, {
|
|
1318
|
+
ref: captchafoxRef,
|
|
1319
|
+
sitekey: captcha.siteKey,
|
|
1320
|
+
onVerify: handleSuccess,
|
|
1321
|
+
onError: handleError,
|
|
1322
|
+
onExpire: handleExpire,
|
|
1323
|
+
onLoad: handleWidgetLoad
|
|
1324
|
+
})
|
|
1325
|
+
})]
|
|
1326
|
+
});
|
|
1327
|
+
default: return null;
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
|
|
872
1331
|
//#endregion
|
|
873
1332
|
//#region src/web/ui/label.tsx
|
|
1333
|
+
/**
|
|
1334
|
+
* A text label associated with a form control.
|
|
1335
|
+
*
|
|
1336
|
+
* Provides accessible labeling for inputs. Automatically handles disabled
|
|
1337
|
+
* states when used with peer or group patterns.
|
|
1338
|
+
*
|
|
1339
|
+
* @example
|
|
1340
|
+
* ```tsx
|
|
1341
|
+
* <Label htmlFor="email">Email Address</Label>
|
|
1342
|
+
* <Input id="email" type="email" />
|
|
1343
|
+
* ```
|
|
1344
|
+
*/
|
|
874
1345
|
function Label({ className, ...props }) {
|
|
875
1346
|
return /* @__PURE__ */ jsx("label", {
|
|
876
1347
|
"data-slot": "label",
|
|
@@ -881,6 +1352,18 @@ function Label({ className, ...props }) {
|
|
|
881
1352
|
|
|
882
1353
|
//#endregion
|
|
883
1354
|
//#region src/web/ui/separator.tsx
|
|
1355
|
+
/**
|
|
1356
|
+
* A visual divider to separate content sections.
|
|
1357
|
+
*
|
|
1358
|
+
* Renders as a horizontal or vertical line depending on orientation.
|
|
1359
|
+
* Use to create visual boundaries between related content groups.
|
|
1360
|
+
*
|
|
1361
|
+
* @example
|
|
1362
|
+
* ```tsx
|
|
1363
|
+
* <Separator />
|
|
1364
|
+
* <Separator orientation="vertical" className="h-4" />
|
|
1365
|
+
* ```
|
|
1366
|
+
*/
|
|
884
1367
|
function Separator({ className, orientation = "horizontal", ...props }) {
|
|
885
1368
|
return /* @__PURE__ */ jsx(Separator$1, {
|
|
886
1369
|
"data-slot": "separator",
|
|
@@ -910,7 +1393,7 @@ function FieldLegend({ className, variant = "legend", ...props }) {
|
|
|
910
1393
|
function FieldGroup({ className, ...props }) {
|
|
911
1394
|
return /* @__PURE__ */ jsx("div", {
|
|
912
1395
|
"data-slot": "field-group",
|
|
913
|
-
className: cn("gap-7 data-[slot=checkbox-group]:gap-3
|
|
1396
|
+
className: cn("gap-7 data-[slot=checkbox-group]:gap-3 *:data-[slot=field-group]:gap-4 group/field-group @container/field-group flex w-full flex-col", className),
|
|
914
1397
|
...props
|
|
915
1398
|
});
|
|
916
1399
|
}
|
|
@@ -941,7 +1424,7 @@ function FieldContent({ className, ...props }) {
|
|
|
941
1424
|
function FieldLabel({ className, ...props }) {
|
|
942
1425
|
return /* @__PURE__ */ jsx(Label, {
|
|
943
1426
|
"data-slot": "field-label",
|
|
944
|
-
className: cn("has-data-checked:bg-primary/5 has-data-checked:border-primary dark:has-data-checked:bg-primary/10 gap-2 group-data-[disabled=true]/field:opacity-50 has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border
|
|
1427
|
+
className: cn("has-data-checked:bg-primary/5 has-data-checked:border-primary dark:has-data-checked:bg-primary/10 gap-2 group-data-[disabled=true]/field:opacity-50 has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border *:data-[slot=field]:p-3 group/field-label peer/field-label flex w-fit leading-snug", "has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col", className),
|
|
945
1428
|
...props
|
|
946
1429
|
});
|
|
947
1430
|
}
|
|
@@ -955,7 +1438,7 @@ function FieldTitle({ className, ...props }) {
|
|
|
955
1438
|
function FieldDescription({ className, ...props }) {
|
|
956
1439
|
return /* @__PURE__ */ jsx("p", {
|
|
957
1440
|
"data-slot": "field-description",
|
|
958
|
-
className: cn("text-muted-foreground text-left text-sm [[data-variant=legend]+&]:-mt-1.5 leading-normal font-normal group-has-
|
|
1441
|
+
className: cn("text-muted-foreground text-left text-sm [[data-variant=legend]+&]:-mt-1.5 leading-normal font-normal group-has-data-[orientation=horizontal]/field:text-balance", "last:mt-0 nth-last-2:-mt-1", "[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4", className),
|
|
959
1442
|
...props
|
|
960
1443
|
});
|
|
961
1444
|
}
|
|
@@ -998,8 +1481,19 @@ function FieldError({ className, children, errors, showIcon = true, ...props })
|
|
|
998
1481
|
|
|
999
1482
|
//#endregion
|
|
1000
1483
|
//#region src/web/ui/input.tsx
|
|
1001
|
-
|
|
1002
|
-
|
|
1484
|
+
/**
|
|
1485
|
+
* A text input field for user data entry.
|
|
1486
|
+
*
|
|
1487
|
+
* Supports all native input types including text, email, password, and file.
|
|
1488
|
+
* Includes focus and validation states with appropriate visual feedback.
|
|
1489
|
+
*
|
|
1490
|
+
* @example
|
|
1491
|
+
* ```tsx
|
|
1492
|
+
* <Input type="email" placeholder="Enter your email" />
|
|
1493
|
+
* ```
|
|
1494
|
+
*/
|
|
1495
|
+
function Input({ className, type, ...props }) {
|
|
1496
|
+
return /* @__PURE__ */ jsx(Input$1, {
|
|
1003
1497
|
type,
|
|
1004
1498
|
"data-slot": "input",
|
|
1005
1499
|
className: cn("dark:bg-input/30 border-input hover:border-ring/50 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 h-9 rounded-md border bg-transparent px-2.5 py-1 text-base shadow-xs transition-[color,box-shadow] file:h-7 file:text-sm file:font-medium focus-visible:ring-[3px] focus-visible:shadow-[0_0_8px_rgba(0,0,0,0.04)] aria-invalid:ring-[3px] md:text-sm file:text-foreground placeholder:text-muted-foreground w-full min-w-0 outline-none file:inline-flex file:border-0 file:bg-transparent disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50", className),
|
|
@@ -1061,6 +1555,7 @@ function EmailInput({ name = "email", label, hideLabel = false, showIcon = false
|
|
|
1061
1555
|
|
|
1062
1556
|
//#endregion
|
|
1063
1557
|
//#region src/web/ui/alert.tsx
|
|
1558
|
+
/** Alert variant styles for different semantic meanings. */
|
|
1064
1559
|
const alertVariants = cva("grid gap-0.5 rounded-lg border px-4 py-3 text-left text-sm has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-18 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2.5 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-4 w-full relative group/alert", {
|
|
1065
1560
|
variants: { variant: {
|
|
1066
1561
|
default: "bg-card text-card-foreground border-border",
|
|
@@ -1071,6 +1566,22 @@ const alertVariants = cva("grid gap-0.5 rounded-lg border px-4 py-3 text-left te
|
|
|
1071
1566
|
} },
|
|
1072
1567
|
defaultVariants: { variant: "default" }
|
|
1073
1568
|
});
|
|
1569
|
+
/**
|
|
1570
|
+
* A contextual feedback message for user actions or system status.
|
|
1571
|
+
*
|
|
1572
|
+
* Supports semantic variants (info, success, warning, destructive) with
|
|
1573
|
+
* appropriate color coding. Use with AlertTitle and AlertDescription for
|
|
1574
|
+
* structured content, and AlertAction for dismissal or action buttons.
|
|
1575
|
+
*
|
|
1576
|
+
* @example
|
|
1577
|
+
* ```tsx
|
|
1578
|
+
* <Alert variant="info">
|
|
1579
|
+
* <InfoIcon />
|
|
1580
|
+
* <AlertTitle>Heads up!</AlertTitle>
|
|
1581
|
+
* <AlertDescription>This is an informational alert.</AlertDescription>
|
|
1582
|
+
* </Alert>
|
|
1583
|
+
* ```
|
|
1584
|
+
*/
|
|
1074
1585
|
function Alert({ className, variant, ...props }) {
|
|
1075
1586
|
return /* @__PURE__ */ jsx("div", {
|
|
1076
1587
|
"data-slot": "alert",
|
|
@@ -1079,6 +1590,17 @@ function Alert({ className, variant, ...props }) {
|
|
|
1079
1590
|
...props
|
|
1080
1591
|
});
|
|
1081
1592
|
}
|
|
1593
|
+
/**
|
|
1594
|
+
* The title heading for an Alert component.
|
|
1595
|
+
*
|
|
1596
|
+
* Displays prominently with medium font weight. Automatically positioned
|
|
1597
|
+
* after any icon in the alert.
|
|
1598
|
+
*
|
|
1599
|
+
* @example
|
|
1600
|
+
* ```tsx
|
|
1601
|
+
* <AlertTitle>Success!</AlertTitle>
|
|
1602
|
+
* ```
|
|
1603
|
+
*/
|
|
1082
1604
|
function AlertTitle({ className, ...props }) {
|
|
1083
1605
|
return /* @__PURE__ */ jsx("div", {
|
|
1084
1606
|
"data-slot": "alert-title",
|
|
@@ -1086,6 +1608,19 @@ function AlertTitle({ className, ...props }) {
|
|
|
1086
1608
|
...props
|
|
1087
1609
|
});
|
|
1088
1610
|
}
|
|
1611
|
+
/**
|
|
1612
|
+
* The descriptive text content for an Alert component.
|
|
1613
|
+
*
|
|
1614
|
+
* Displays in muted foreground color with proper text wrapping. Supports
|
|
1615
|
+
* multiple paragraphs with automatic spacing.
|
|
1616
|
+
*
|
|
1617
|
+
* @example
|
|
1618
|
+
* ```tsx
|
|
1619
|
+
* <AlertDescription>
|
|
1620
|
+
* Your changes have been saved successfully.
|
|
1621
|
+
* </AlertDescription>
|
|
1622
|
+
* ```
|
|
1623
|
+
*/
|
|
1089
1624
|
function AlertDescription({ className, ...props }) {
|
|
1090
1625
|
return /* @__PURE__ */ jsx("div", {
|
|
1091
1626
|
"data-slot": "alert-description",
|
|
@@ -1093,6 +1628,21 @@ function AlertDescription({ className, ...props }) {
|
|
|
1093
1628
|
...props
|
|
1094
1629
|
});
|
|
1095
1630
|
}
|
|
1631
|
+
/**
|
|
1632
|
+
* An action slot positioned in the top-right corner of an Alert.
|
|
1633
|
+
*
|
|
1634
|
+
* Use for dismiss buttons or quick actions. Absolutely positioned to not
|
|
1635
|
+
* interfere with the alert content flow.
|
|
1636
|
+
*
|
|
1637
|
+
* @example
|
|
1638
|
+
* ```tsx
|
|
1639
|
+
* <AlertAction>
|
|
1640
|
+
* <Button variant="ghost" size="icon-sm" aria-label="Dismiss">
|
|
1641
|
+
* <XIcon />
|
|
1642
|
+
* </Button>
|
|
1643
|
+
* </AlertAction>
|
|
1644
|
+
* ```
|
|
1645
|
+
*/
|
|
1096
1646
|
function AlertAction({ className, ...props }) {
|
|
1097
1647
|
return /* @__PURE__ */ jsx("div", {
|
|
1098
1648
|
"data-slot": "alert-action",
|
|
@@ -1103,32 +1653,67 @@ function AlertAction({ className, ...props }) {
|
|
|
1103
1653
|
|
|
1104
1654
|
//#endregion
|
|
1105
1655
|
//#region src/web/ui/button.tsx
|
|
1656
|
+
/** Button variant styles. */
|
|
1657
|
+
const variants = {
|
|
1658
|
+
default: "bg-primary text-primary-foreground hover:bg-primary/80",
|
|
1659
|
+
outline: "border-border bg-background hover:bg-muted hover:text-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 aria-expanded:bg-muted aria-expanded:text-foreground shadow-xs",
|
|
1660
|
+
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
|
|
1661
|
+
ghost: "hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground",
|
|
1662
|
+
destructive: "bg-destructive/10 hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/20 text-destructive focus-visible:border-destructive/40 dark:hover:bg-destructive/30",
|
|
1663
|
+
link: "text-primary underline-offset-4 hover:underline"
|
|
1664
|
+
};
|
|
1665
|
+
/** Button size styles. */
|
|
1666
|
+
const sizes$1 = {
|
|
1667
|
+
default: "h-9 gap-1.5 px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
|
1668
|
+
xs: "h-6 gap-1 rounded-[min(var(--radius-md),8px)] px-2 text-xs in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
|
|
1669
|
+
sm: "h-8 gap-1 rounded-[min(var(--radius-md),10px)] px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5",
|
|
1670
|
+
lg: "h-10 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3",
|
|
1671
|
+
icon: "size-9",
|
|
1672
|
+
"icon-xs": "size-6 rounded-[min(var(--radius-md),8px)] in-data-[slot=button-group]:rounded-md [&_svg:not([class*='size-'])]:size-3",
|
|
1673
|
+
"icon-sm": "size-8 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-md",
|
|
1674
|
+
"icon-lg": "size-10"
|
|
1675
|
+
};
|
|
1106
1676
|
const buttonVariants = cva("focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-md border border-transparent bg-clip-padding text-sm font-medium focus-visible:ring-[3px] aria-invalid:ring-[3px] [&_svg:not([class*='size-'])]:size-4 inline-flex items-center justify-center whitespace-nowrap transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none shrink-0 [&_svg]:shrink-0 outline-none group/button select-none", {
|
|
1107
1677
|
variants: {
|
|
1108
|
-
variant:
|
|
1109
|
-
|
|
1110
|
-
outline: "border-border bg-background hover:bg-muted hover:text-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 aria-expanded:bg-muted aria-expanded:text-foreground shadow-xs",
|
|
1111
|
-
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
|
|
1112
|
-
ghost: "hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground",
|
|
1113
|
-
destructive: "bg-destructive/10 hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/20 text-destructive focus-visible:border-destructive/40 dark:hover:bg-destructive/30",
|
|
1114
|
-
link: "text-primary underline-offset-4 hover:underline"
|
|
1115
|
-
},
|
|
1116
|
-
size: {
|
|
1117
|
-
default: "h-9 gap-1.5 px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
|
1118
|
-
xs: "h-6 gap-1 rounded-[min(var(--radius-md),8px)] px-2 text-xs in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
|
|
1119
|
-
sm: "h-8 gap-1 rounded-[min(var(--radius-md),10px)] px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5",
|
|
1120
|
-
lg: "h-10 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3",
|
|
1121
|
-
icon: "size-9",
|
|
1122
|
-
"icon-xs": "size-6 rounded-[min(var(--radius-md),8px)] in-data-[slot=button-group]:rounded-md [&_svg:not([class*='size-'])]:size-3",
|
|
1123
|
-
"icon-sm": "size-8 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-md",
|
|
1124
|
-
"icon-lg": "size-10"
|
|
1125
|
-
}
|
|
1678
|
+
variant: variants,
|
|
1679
|
+
size: sizes$1
|
|
1126
1680
|
},
|
|
1127
1681
|
defaultVariants: {
|
|
1128
1682
|
variant: "default",
|
|
1129
1683
|
size: "default"
|
|
1130
1684
|
}
|
|
1131
1685
|
});
|
|
1686
|
+
/** Available button variant keys. */
|
|
1687
|
+
const buttonVariantKeys = Object.keys(variants);
|
|
1688
|
+
/** Available button size keys. */
|
|
1689
|
+
const buttonSizeKeys = Object.keys(sizes$1);
|
|
1690
|
+
/**
|
|
1691
|
+
* A versatile button component with multiple visual variants and sizes.
|
|
1692
|
+
*
|
|
1693
|
+
* Supports different visual styles through the `variant` prop (default, secondary,
|
|
1694
|
+
* outline, ghost, destructive, link) and various sizes through the `size` prop.
|
|
1695
|
+
* Icon-only buttons should use `size="icon"` variants with an `aria-label` for accessibility.
|
|
1696
|
+
*
|
|
1697
|
+
* @example
|
|
1698
|
+
* ```tsx
|
|
1699
|
+
* // Default button
|
|
1700
|
+
* <Button>Click me</Button>
|
|
1701
|
+
*
|
|
1702
|
+
* // With variant and size
|
|
1703
|
+
* <Button variant="destructive" size="sm">Delete</Button>
|
|
1704
|
+
*
|
|
1705
|
+
* // With inline icon
|
|
1706
|
+
* <Button>
|
|
1707
|
+
* <IconPlus data-icon="inline-start" />
|
|
1708
|
+
* Create
|
|
1709
|
+
* </Button>
|
|
1710
|
+
*
|
|
1711
|
+
* // Icon-only button (requires aria-label)
|
|
1712
|
+
* <Button size="icon" variant="ghost" aria-label="Settings">
|
|
1713
|
+
* <IconSettings />
|
|
1714
|
+
* </Button>
|
|
1715
|
+
* ```
|
|
1716
|
+
*/
|
|
1132
1717
|
function Button({ className, variant = "default", size = "default", ...props }) {
|
|
1133
1718
|
return /* @__PURE__ */ jsx(Button$1, {
|
|
1134
1719
|
"data-slot": "button",
|
|
@@ -1143,120 +1728,25 @@ function Button({ className, variant = "default", size = "default", ...props })
|
|
|
1143
1728
|
|
|
1144
1729
|
//#endregion
|
|
1145
1730
|
//#region src/web/ui/spinner.tsx
|
|
1146
|
-
function Spinner({ className, ...props }) {
|
|
1147
|
-
return /* @__PURE__ */ jsx(Loader2Icon, {
|
|
1148
|
-
role: "status",
|
|
1149
|
-
"aria-label": "Loading",
|
|
1150
|
-
className: cn("size-4 animate-spin", className),
|
|
1151
|
-
...props
|
|
1152
|
-
});
|
|
1153
|
-
}
|
|
1154
|
-
|
|
1155
|
-
//#endregion
|
|
1156
|
-
//#region src/web/ui/auth/hooks.ts
|
|
1157
1731
|
/**
|
|
1158
|
-
*
|
|
1159
|
-
*
|
|
1160
|
-
* Provides unified access to all auth-related context values:
|
|
1161
|
-
* - `client`: The Better Auth client for making auth API calls.
|
|
1162
|
-
* - `features`: Derived feature flags for conditional rendering based on enabled auth methods.
|
|
1163
|
-
* - `config`: The auth configuration values for forms and validation.
|
|
1164
|
-
*
|
|
1165
|
-
* @returns An object containing the auth client, features, and config.
|
|
1166
|
-
*
|
|
1167
|
-
* @example
|
|
1168
|
-
* ```tsx
|
|
1169
|
-
* // Access client for auth operations
|
|
1170
|
-
* const { client } = useAuth();
|
|
1171
|
-
* await client.signIn.email({ email, password });
|
|
1172
|
-
*
|
|
1173
|
-
* // Access features for conditional rendering
|
|
1174
|
-
* const { features } = useAuth();
|
|
1175
|
-
* if (features.oauth.google) {
|
|
1176
|
-
* // Show Google login button
|
|
1177
|
-
* }
|
|
1732
|
+
* A spinning loading indicator.
|
|
1178
1733
|
*
|
|
1179
|
-
*
|
|
1180
|
-
*
|
|
1181
|
-
* const minPassword = config.methods?.emailPassword?.minPasswordLength ?? 8;
|
|
1182
|
-
* ```
|
|
1183
|
-
*/
|
|
1184
|
-
function useAuth() {
|
|
1185
|
-
const { client, features, config } = useAuthContext();
|
|
1186
|
-
return {
|
|
1187
|
-
client,
|
|
1188
|
-
features,
|
|
1189
|
-
config
|
|
1190
|
-
};
|
|
1191
|
-
}
|
|
1192
|
-
/**
|
|
1193
|
-
* Hook for managing auth form state.
|
|
1194
|
-
* Provides loading, error, and success state management.
|
|
1734
|
+
* Use to indicate async operations in progress. Includes accessible
|
|
1735
|
+
* status role and label for screen readers.
|
|
1195
1736
|
*
|
|
1196
1737
|
* @example
|
|
1197
1738
|
* ```tsx
|
|
1198
|
-
*
|
|
1199
|
-
*
|
|
1200
|
-
* const onSubmit = async (data) => {
|
|
1201
|
-
* await execute(async () => {
|
|
1202
|
-
* await client.signIn.email(data);
|
|
1203
|
-
* });
|
|
1204
|
-
* };
|
|
1205
|
-
*
|
|
1206
|
-
* if (state.error) {
|
|
1207
|
-
* // Show error message
|
|
1208
|
-
* }
|
|
1739
|
+
* <Spinner />
|
|
1740
|
+
* <Button disabled><Spinner className="mr-2" />Loading...</Button>
|
|
1209
1741
|
* ```
|
|
1210
1742
|
*/
|
|
1211
|
-
function
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1743
|
+
function Spinner({ className, ...props }) {
|
|
1744
|
+
return /* @__PURE__ */ jsx(Loader2Icon, {
|
|
1745
|
+
role: "status",
|
|
1746
|
+
"aria-label": "Loading",
|
|
1747
|
+
className: cn("size-4 animate-spin", className),
|
|
1748
|
+
...props
|
|
1216
1749
|
});
|
|
1217
|
-
return {
|
|
1218
|
-
state,
|
|
1219
|
-
execute: useCallback(async (fn) => {
|
|
1220
|
-
setState({
|
|
1221
|
-
isLoading: true,
|
|
1222
|
-
error: null,
|
|
1223
|
-
isSuccess: false
|
|
1224
|
-
});
|
|
1225
|
-
try {
|
|
1226
|
-
const result = await fn();
|
|
1227
|
-
setState({
|
|
1228
|
-
isLoading: false,
|
|
1229
|
-
error: null,
|
|
1230
|
-
isSuccess: true
|
|
1231
|
-
});
|
|
1232
|
-
return result;
|
|
1233
|
-
} catch (err) {
|
|
1234
|
-
setState({
|
|
1235
|
-
isLoading: false,
|
|
1236
|
-
error: err instanceof Error ? err.message : "An error occurred",
|
|
1237
|
-
isSuccess: false
|
|
1238
|
-
});
|
|
1239
|
-
return;
|
|
1240
|
-
}
|
|
1241
|
-
}, []),
|
|
1242
|
-
reset: useCallback(() => {
|
|
1243
|
-
setState({
|
|
1244
|
-
isLoading: false,
|
|
1245
|
-
error: null,
|
|
1246
|
-
isSuccess: false
|
|
1247
|
-
});
|
|
1248
|
-
}, []),
|
|
1249
|
-
setError: useCallback((error) => {
|
|
1250
|
-
setState((prev) => ({
|
|
1251
|
-
...prev,
|
|
1252
|
-
error,
|
|
1253
|
-
isSuccess: false
|
|
1254
|
-
}));
|
|
1255
|
-
}, []),
|
|
1256
|
-
isLoading: state.isLoading,
|
|
1257
|
-
error: state.error,
|
|
1258
|
-
isSuccess: state.isSuccess
|
|
1259
|
-
};
|
|
1260
1750
|
}
|
|
1261
1751
|
|
|
1262
1752
|
//#endregion
|
|
@@ -1418,6 +1908,9 @@ function defineEmailOtpSchema() {
|
|
|
1418
1908
|
function EmailOTPForm({ callbackURL, className, title, description, logo, onSuccess }) {
|
|
1419
1909
|
const { t: t$1 } = useTranslation$1(["ui"]);
|
|
1420
1910
|
const { client, config, features } = useAuth();
|
|
1911
|
+
const captcha = useCaptcha();
|
|
1912
|
+
const [captchaToken, setCaptchaToken] = useState(null);
|
|
1913
|
+
const captchaResetRef = useRef(null);
|
|
1421
1914
|
const { isLoading, error, execute, setError } = useAuthForm();
|
|
1422
1915
|
const [stage, setStage] = useState("email");
|
|
1423
1916
|
const [pendingEmail, setPendingEmail] = useState(null);
|
|
@@ -1458,16 +1951,21 @@ function EmailOTPForm({ callbackURL, className, title, description, logo, onSucc
|
|
|
1458
1951
|
}, 1e3);
|
|
1459
1952
|
};
|
|
1460
1953
|
const handleEmailSubmit = async (data) => {
|
|
1461
|
-
await execute(async () => {
|
|
1462
|
-
const
|
|
1954
|
+
if (!await execute(async () => {
|
|
1955
|
+
const response = await client.emailOtp.sendVerificationOtp({
|
|
1463
1956
|
email: data.email,
|
|
1464
|
-
type: "sign-in"
|
|
1957
|
+
type: "sign-in",
|
|
1958
|
+
fetchOptions: captchaToken ? { headers: { "x-captcha-response": captchaToken } } : void 0
|
|
1465
1959
|
});
|
|
1466
|
-
if (
|
|
1960
|
+
if (response.error) throw new Error(response.error.message || t$1("ui:auth.errors.generic"));
|
|
1467
1961
|
setPendingEmail(data.email);
|
|
1468
1962
|
setStage("verify");
|
|
1469
1963
|
startCountdown();
|
|
1470
|
-
|
|
1964
|
+
return response;
|
|
1965
|
+
})) {
|
|
1966
|
+
setCaptchaToken(null);
|
|
1967
|
+
captchaResetRef.current?.();
|
|
1968
|
+
}
|
|
1471
1969
|
};
|
|
1472
1970
|
const handleOtpSubmit = async (data) => {
|
|
1473
1971
|
if (!pendingEmail) return;
|
|
@@ -1482,14 +1980,20 @@ function EmailOTPForm({ callbackURL, className, title, description, logo, onSucc
|
|
|
1482
1980
|
};
|
|
1483
1981
|
const handleResend = async () => {
|
|
1484
1982
|
if (!pendingEmail) return;
|
|
1485
|
-
|
|
1983
|
+
const result = await execute(async () => {
|
|
1486
1984
|
const response = await client.emailOtp.sendVerificationOtp({
|
|
1487
1985
|
email: pendingEmail,
|
|
1488
|
-
type: "sign-in"
|
|
1986
|
+
type: "sign-in",
|
|
1987
|
+
fetchOptions: captchaToken ? { headers: { "x-captcha-response": captchaToken } } : void 0
|
|
1489
1988
|
});
|
|
1490
1989
|
if (response.error) throw new Error(response.error.message || t$1("ui:auth.errors.generic"));
|
|
1491
1990
|
return response;
|
|
1492
|
-
})
|
|
1991
|
+
});
|
|
1992
|
+
if (!result) {
|
|
1993
|
+
setCaptchaToken(null);
|
|
1994
|
+
captchaResetRef.current?.();
|
|
1995
|
+
}
|
|
1996
|
+
if (result) startCountdown();
|
|
1493
1997
|
};
|
|
1494
1998
|
const handleBack = () => {
|
|
1495
1999
|
setError(null);
|
|
@@ -1566,10 +2070,16 @@ function EmailOTPForm({ callbackURL, className, title, description, logo, onSucc
|
|
|
1566
2070
|
label: t$1("ui:auth.login.email"),
|
|
1567
2071
|
showIcon: true
|
|
1568
2072
|
}),
|
|
2073
|
+
captcha.enabled && /* @__PURE__ */ jsx(CaptchaWidget, {
|
|
2074
|
+
onSuccess: setCaptchaToken,
|
|
2075
|
+
onExpire: () => setCaptchaToken(null),
|
|
2076
|
+
onError: () => setCaptchaToken(null),
|
|
2077
|
+
resetRef: captchaResetRef
|
|
2078
|
+
}),
|
|
1569
2079
|
/* @__PURE__ */ jsx(Button, {
|
|
1570
2080
|
type: "submit",
|
|
1571
2081
|
className: "h-10 w-full font-medium",
|
|
1572
|
-
disabled: isLoading,
|
|
2082
|
+
disabled: isLoading || captcha.enabled && !captchaToken,
|
|
1573
2083
|
children: isLoading ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Spinner, { className: "mr-2 size-4" }), t$1("ui:auth.emailOtp.sending")] }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(MailIcon, { className: "mr-2 size-4" }), t$1("ui:auth.emailOtp.send")] })
|
|
1574
2084
|
})
|
|
1575
2085
|
]
|
|
@@ -1590,10 +2100,10 @@ function EmailOTPForm({ callbackURL, className, title, description, logo, onSucc
|
|
|
1590
2100
|
* <AnimatedCheck size="lg" color="green" />
|
|
1591
2101
|
*
|
|
1592
2102
|
* // Primary color variant
|
|
1593
|
-
* <AnimatedCheck size="
|
|
2103
|
+
* <AnimatedCheck size="default" color="primary" />
|
|
1594
2104
|
* ```
|
|
1595
2105
|
*/
|
|
1596
|
-
function AnimatedCheck({ className, size = "
|
|
2106
|
+
function AnimatedCheck({ className, size = "default", color = "green" }) {
|
|
1597
2107
|
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("style", { children: `
|
|
1598
2108
|
@keyframes ac-scale-in {
|
|
1599
2109
|
from { transform: scale(0); opacity: 0; }
|
|
@@ -1604,9 +2114,11 @@ function AnimatedCheck({ className, size = "md", color = "green" }) {
|
|
|
1604
2114
|
}
|
|
1605
2115
|
` }), /* @__PURE__ */ jsxs("svg", {
|
|
1606
2116
|
className: cn({
|
|
2117
|
+
xs: "size-6",
|
|
1607
2118
|
sm: "size-8",
|
|
1608
|
-
|
|
1609
|
-
lg: "size-16"
|
|
2119
|
+
default: "size-12",
|
|
2120
|
+
lg: "size-16",
|
|
2121
|
+
xl: "size-20"
|
|
1610
2122
|
}[size], {
|
|
1611
2123
|
green: "text-green-600 dark:text-green-400",
|
|
1612
2124
|
primary: "text-primary"
|
|
@@ -1667,6 +2179,9 @@ function defineForgotPasswordSchema() {
|
|
|
1667
2179
|
function ForgotPasswordForm({ callbackURL, className, loginLink = "/login", title, description, logo, onSuccess }) {
|
|
1668
2180
|
const { t: t$1 } = useTranslation$1(["ui"]);
|
|
1669
2181
|
const { client } = useAuth();
|
|
2182
|
+
const captcha = useCaptcha();
|
|
2183
|
+
const [captchaToken, setCaptchaToken] = useState(null);
|
|
2184
|
+
const captchaResetRef = useRef(null);
|
|
1670
2185
|
const { isLoading, error, isSuccess, execute, reset: resetForm } = useAuthForm();
|
|
1671
2186
|
const form = useForm({
|
|
1672
2187
|
resolver: zodResolver(defineForgotPasswordSchema()),
|
|
@@ -1675,14 +2190,20 @@ function ForgotPasswordForm({ callbackURL, className, loginLink = "/login", titl
|
|
|
1675
2190
|
});
|
|
1676
2191
|
const submittedEmail = form.getValues("email");
|
|
1677
2192
|
const onSubmit = async (data) => {
|
|
1678
|
-
|
|
2193
|
+
const result = await execute(async () => {
|
|
1679
2194
|
const response = await client.requestPasswordReset({
|
|
1680
2195
|
email: data.email,
|
|
1681
|
-
redirectTo: callbackURL
|
|
2196
|
+
redirectTo: callbackURL,
|
|
2197
|
+
fetchOptions: captchaToken ? { headers: { "x-captcha-response": captchaToken } } : void 0
|
|
1682
2198
|
});
|
|
1683
2199
|
if (response.error) throw new Error(response.error.message || t$1("ui:auth.errors.generic"));
|
|
1684
2200
|
return response;
|
|
1685
|
-
})
|
|
2201
|
+
});
|
|
2202
|
+
if (!result) {
|
|
2203
|
+
setCaptchaToken(null);
|
|
2204
|
+
captchaResetRef.current?.();
|
|
2205
|
+
}
|
|
2206
|
+
if (result) onSuccess?.();
|
|
1686
2207
|
};
|
|
1687
2208
|
const handleTryAgain = () => {
|
|
1688
2209
|
resetForm();
|
|
@@ -1750,10 +2271,16 @@ function ForgotPasswordForm({ callbackURL, className, loginLink = "/login", titl
|
|
|
1750
2271
|
label: t$1("ui:auth.forgotPassword.email"),
|
|
1751
2272
|
showIcon: true
|
|
1752
2273
|
}),
|
|
2274
|
+
captcha.enabled && /* @__PURE__ */ jsx(CaptchaWidget, {
|
|
2275
|
+
onSuccess: setCaptchaToken,
|
|
2276
|
+
onExpire: () => setCaptchaToken(null),
|
|
2277
|
+
onError: () => setCaptchaToken(null),
|
|
2278
|
+
resetRef: captchaResetRef
|
|
2279
|
+
}),
|
|
1753
2280
|
/* @__PURE__ */ jsx(Button, {
|
|
1754
2281
|
type: "submit",
|
|
1755
2282
|
className: "h-10 w-full font-medium",
|
|
1756
|
-
disabled: isLoading,
|
|
2283
|
+
disabled: isLoading || captcha.enabled && !captchaToken,
|
|
1757
2284
|
children: isLoading ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Spinner, { className: "mr-2 size-4" }), t$1("ui:auth.forgotPassword.submitting")] }) : t$1("ui:auth.forgotPassword.submit")
|
|
1758
2285
|
})
|
|
1759
2286
|
]
|
|
@@ -1764,6 +2291,18 @@ function ForgotPasswordForm({ callbackURL, className, loginLink = "/login", titl
|
|
|
1764
2291
|
|
|
1765
2292
|
//#endregion
|
|
1766
2293
|
//#region src/web/ui/checkbox.tsx
|
|
2294
|
+
/**
|
|
2295
|
+
* A control for toggling a boolean value on or off.
|
|
2296
|
+
*
|
|
2297
|
+
* Use for binary choices in forms like agreeing to terms, enabling features,
|
|
2298
|
+
* or selecting items. Supports validation states and disabled mode.
|
|
2299
|
+
*
|
|
2300
|
+
* @example
|
|
2301
|
+
* ```tsx
|
|
2302
|
+
* <Checkbox id="terms" />
|
|
2303
|
+
* <Label htmlFor="terms">Accept terms and conditions</Label>
|
|
2304
|
+
* ```
|
|
2305
|
+
*/
|
|
1767
2306
|
function Checkbox({ className, ...props }) {
|
|
1768
2307
|
return /* @__PURE__ */ jsx(Checkbox$1.Root, {
|
|
1769
2308
|
"data-slot": "checkbox",
|
|
@@ -1857,7 +2396,10 @@ function defineMagicLinkSchema() {
|
|
|
1857
2396
|
function MagicLinkForm({ callbackURL, className, successMessage, onSuccess }) {
|
|
1858
2397
|
const { t: t$1 } = useTranslation$1(["ui"]);
|
|
1859
2398
|
const { client, features } = useAuth();
|
|
1860
|
-
const
|
|
2399
|
+
const captcha = useCaptcha();
|
|
2400
|
+
const [captchaToken, setCaptchaToken] = useState(null);
|
|
2401
|
+
const captchaResetRef = useRef(null);
|
|
2402
|
+
const { isLoading, error, isSuccess, execute, reset } = useAuthForm();
|
|
1861
2403
|
const [sentEmail, setSentEmail] = useState(null);
|
|
1862
2404
|
const resolvedSuccessMessage = successMessage ?? t$1("ui:auth.magicLink.successDescription", { email: sentEmail });
|
|
1863
2405
|
const form = useForm({
|
|
@@ -1866,21 +2408,40 @@ function MagicLinkForm({ callbackURL, className, successMessage, onSuccess }) {
|
|
|
1866
2408
|
mode: "onBlur"
|
|
1867
2409
|
});
|
|
1868
2410
|
if (!features.magicLink) return null;
|
|
2411
|
+
const handleTryDifferentEmail = () => {
|
|
2412
|
+
reset();
|
|
2413
|
+
form.reset();
|
|
2414
|
+
setSentEmail(null);
|
|
2415
|
+
};
|
|
1869
2416
|
const onSubmit = async (data) => {
|
|
1870
|
-
|
|
2417
|
+
const result = await execute(async () => {
|
|
1871
2418
|
const response = await client.signIn.magicLink({
|
|
1872
2419
|
email: data.email,
|
|
1873
|
-
callbackURL
|
|
2420
|
+
callbackURL,
|
|
2421
|
+
fetchOptions: captchaToken ? { headers: { "x-captcha-response": captchaToken } } : void 0
|
|
1874
2422
|
});
|
|
1875
2423
|
if (response.error) throw new Error(response.error.message || t$1("ui:auth.errors.generic"));
|
|
1876
2424
|
setSentEmail(data.email);
|
|
1877
2425
|
return response;
|
|
1878
|
-
})
|
|
2426
|
+
});
|
|
2427
|
+
if (!result) {
|
|
2428
|
+
setCaptchaToken(null);
|
|
2429
|
+
captchaResetRef.current?.();
|
|
2430
|
+
}
|
|
2431
|
+
if (result) onSuccess?.();
|
|
1879
2432
|
};
|
|
1880
|
-
if (isSuccess && sentEmail) return /* @__PURE__ */ jsxs(
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
2433
|
+
if (isSuccess && sentEmail) return /* @__PURE__ */ jsxs("div", {
|
|
2434
|
+
className: cn("space-y-4", className),
|
|
2435
|
+
children: [/* @__PURE__ */ jsxs(Alert, {
|
|
2436
|
+
variant: "success",
|
|
2437
|
+
children: [/* @__PURE__ */ jsx(CheckCircleIcon, { className: "size-4" }), /* @__PURE__ */ jsx(AlertDescription, { children: resolvedSuccessMessage })]
|
|
2438
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
2439
|
+
type: "button",
|
|
2440
|
+
variant: "outline",
|
|
2441
|
+
className: "h-10 w-full font-medium",
|
|
2442
|
+
onClick: handleTryDifferentEmail,
|
|
2443
|
+
children: t$1("ui:auth.magicLink.tryDifferentEmail")
|
|
2444
|
+
})]
|
|
1884
2445
|
});
|
|
1885
2446
|
return /* @__PURE__ */ jsx(FormProvider, {
|
|
1886
2447
|
...form,
|
|
@@ -1896,10 +2457,16 @@ function MagicLinkForm({ callbackURL, className, successMessage, onSuccess }) {
|
|
|
1896
2457
|
name: "email",
|
|
1897
2458
|
label: t$1("ui:auth.login.email")
|
|
1898
2459
|
}),
|
|
2460
|
+
captcha.enabled && /* @__PURE__ */ jsx(CaptchaWidget, {
|
|
2461
|
+
onSuccess: setCaptchaToken,
|
|
2462
|
+
onExpire: () => setCaptchaToken(null),
|
|
2463
|
+
onError: () => setCaptchaToken(null),
|
|
2464
|
+
resetRef: captchaResetRef
|
|
2465
|
+
}),
|
|
1899
2466
|
/* @__PURE__ */ jsx(Button, {
|
|
1900
2467
|
type: "submit",
|
|
1901
2468
|
className: "h-10 w-full font-medium",
|
|
1902
|
-
disabled: isLoading,
|
|
2469
|
+
disabled: isLoading || captcha.enabled && !captchaToken,
|
|
1903
2470
|
children: isLoading ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Spinner, { className: "mr-2 size-4" }), t$1("ui:auth.magicLink.submitting")] }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(MailIcon, { className: "mr-2 size-4" }), t$1("ui:auth.magicLink.submit")] })
|
|
1904
2471
|
})
|
|
1905
2472
|
]
|
|
@@ -1907,6 +2474,49 @@ function MagicLinkForm({ callbackURL, className, successMessage, onSuccess }) {
|
|
|
1907
2474
|
});
|
|
1908
2475
|
}
|
|
1909
2476
|
|
|
2477
|
+
//#endregion
|
|
2478
|
+
//#region src/web/ui/badge.tsx
|
|
2479
|
+
/** Badge variant styles for different visual appearances. */
|
|
2480
|
+
const badgeVariants = cva("h-5 gap-1 rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium transition-all has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&>svg]:size-3! inline-flex items-center justify-center w-fit whitespace-nowrap shrink-0 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-colors overflow-hidden group/badge", {
|
|
2481
|
+
variants: { variant: {
|
|
2482
|
+
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
|
2483
|
+
secondary: "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
|
|
2484
|
+
destructive: "bg-destructive/10 [a]:hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 text-destructive dark:bg-destructive/20",
|
|
2485
|
+
outline: "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
|
|
2486
|
+
ghost: "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
|
|
2487
|
+
link: "text-primary underline-offset-4 hover:underline"
|
|
2488
|
+
} },
|
|
2489
|
+
defaultVariants: { variant: "default" }
|
|
2490
|
+
});
|
|
2491
|
+
/**
|
|
2492
|
+
* A small label component for highlighting status, categories, or counts.
|
|
2493
|
+
*
|
|
2494
|
+
* Supports multiple visual variants and can render as different elements
|
|
2495
|
+
* using the `render` prop. Commonly used for tags, status indicators,
|
|
2496
|
+
* and notification counts.
|
|
2497
|
+
*
|
|
2498
|
+
* @example
|
|
2499
|
+
* ```tsx
|
|
2500
|
+
* <Badge>New</Badge>
|
|
2501
|
+
* <Badge variant="secondary">Draft</Badge>
|
|
2502
|
+
* <Badge variant="destructive">Error</Badge>
|
|
2503
|
+
* ```
|
|
2504
|
+
*/
|
|
2505
|
+
function Badge({ className, variant = "default", render, ...props }) {
|
|
2506
|
+
return useRender({
|
|
2507
|
+
defaultTagName: "span",
|
|
2508
|
+
props: mergeProps({ className: cn(badgeVariants({
|
|
2509
|
+
className,
|
|
2510
|
+
variant
|
|
2511
|
+
})) }, props),
|
|
2512
|
+
render,
|
|
2513
|
+
state: {
|
|
2514
|
+
slot: "badge",
|
|
2515
|
+
variant
|
|
2516
|
+
}
|
|
2517
|
+
});
|
|
2518
|
+
}
|
|
2519
|
+
|
|
1910
2520
|
//#endregion
|
|
1911
2521
|
//#region src/web/ui/auth/passkey-button.tsx
|
|
1912
2522
|
/**
|
|
@@ -1923,8 +2533,10 @@ function MagicLinkForm({ callbackURL, className, successMessage, onSuccess }) {
|
|
|
1923
2533
|
function PasskeyButton({ autoFill = true, className, disabled, label, onSuccess, onError }) {
|
|
1924
2534
|
const { t: t$1 } = useTranslation$1(["ui"]);
|
|
1925
2535
|
const { client, features } = useAuth();
|
|
2536
|
+
const { isLastMethod } = useLastUsedMethod();
|
|
1926
2537
|
const [isLoading, setIsLoading] = useState(false);
|
|
1927
2538
|
if (!features.passkey) return null;
|
|
2539
|
+
const isLast = isLastMethod("passkey");
|
|
1928
2540
|
const handleSignIn = async () => {
|
|
1929
2541
|
setIsLoading(true);
|
|
1930
2542
|
try {
|
|
@@ -1937,30 +2549,58 @@ function PasskeyButton({ autoFill = true, className, disabled, label, onSuccess,
|
|
|
1937
2549
|
}
|
|
1938
2550
|
};
|
|
1939
2551
|
return /* @__PURE__ */ jsxs(Button, {
|
|
1940
|
-
variant: "outline",
|
|
2552
|
+
variant: isLast ? "default" : "outline",
|
|
1941
2553
|
type: "button",
|
|
1942
2554
|
disabled: disabled || isLoading,
|
|
1943
2555
|
onClick: handleSignIn,
|
|
1944
2556
|
className: cn("h-10 w-full gap-3 font-medium transition-all hover:shadow-sm", className),
|
|
1945
|
-
children: [
|
|
2557
|
+
children: [
|
|
2558
|
+
isLoading ? /* @__PURE__ */ jsx(Spinner, { className: "size-5" }) : /* @__PURE__ */ jsx(FingerprintIcon, { className: cn("size-5", !isLast && "text-primary") }),
|
|
2559
|
+
/* @__PURE__ */ jsx("span", {
|
|
2560
|
+
className: "flex-1 text-left",
|
|
2561
|
+
children: label || t$1("ui:auth.passkey.signIn")
|
|
2562
|
+
}),
|
|
2563
|
+
isLast && /* @__PURE__ */ jsx(Badge, {
|
|
2564
|
+
variant: "secondary",
|
|
2565
|
+
className: "ml-1 text-xs",
|
|
2566
|
+
children: t$1("ui:auth.common.lastUsed")
|
|
2567
|
+
})
|
|
2568
|
+
]
|
|
1946
2569
|
});
|
|
1947
2570
|
}
|
|
1948
2571
|
|
|
1949
2572
|
//#endregion
|
|
1950
2573
|
//#region src/web/ui/auth/password-input.tsx
|
|
1951
2574
|
/**
|
|
1952
|
-
* Calculates password strength based on
|
|
1953
|
-
*
|
|
2575
|
+
* Calculates password strength based on length thresholds relative to minLength
|
|
2576
|
+
* and character variety criteria. Uses proportional thresholds so strength
|
|
2577
|
+
* calculation scales with the configured minimum password length.
|
|
2578
|
+
*
|
|
2579
|
+
* @param password - The password to evaluate.
|
|
2580
|
+
* @param minLength - The minimum required password length from config.
|
|
2581
|
+
* @returns Object with strength level (weak/fair/good/strong) and numeric score (0-100).
|
|
2582
|
+
*
|
|
2583
|
+
* @example
|
|
2584
|
+
* ```typescript
|
|
2585
|
+
* // With default minLength of 8
|
|
2586
|
+
* calculatePasswordStrength("short", 8); // { strength: "weak", score: 25 }
|
|
2587
|
+
* calculatePasswordStrength("LongP@ss123", 8); // { strength: "strong", score: 100 }
|
|
2588
|
+
*
|
|
2589
|
+
* // With custom minLength of 12
|
|
2590
|
+
* calculatePasswordStrength("Password123!", 12); // { strength: "fair", score: 50 }
|
|
2591
|
+
* ```
|
|
1954
2592
|
*/
|
|
1955
|
-
function calculatePasswordStrength(password) {
|
|
2593
|
+
function calculatePasswordStrength(password, minLength) {
|
|
1956
2594
|
if (!password) return {
|
|
1957
2595
|
strength: "weak",
|
|
1958
2596
|
score: 0
|
|
1959
2597
|
};
|
|
1960
2598
|
let score = 0;
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
if (password.length >=
|
|
2599
|
+
const mediumLength = Math.floor(minLength * 1.5);
|
|
2600
|
+
const longLength = minLength * 2;
|
|
2601
|
+
if (password.length >= minLength) score += 1;
|
|
2602
|
+
if (password.length >= mediumLength) score += 1;
|
|
2603
|
+
if (password.length >= longLength) score += 1;
|
|
1964
2604
|
if (/[a-z]/.test(password)) score += 1;
|
|
1965
2605
|
if (/[A-Z]/.test(password)) score += 1;
|
|
1966
2606
|
if (/[0-9]/.test(password)) score += 1;
|
|
@@ -2011,7 +2651,7 @@ function PasswordInput({ name = "password", label, hideLabel = false, showToggle
|
|
|
2011
2651
|
const error = form?.formState?.errors?.[name];
|
|
2012
2652
|
const hasError = !!error;
|
|
2013
2653
|
const passwordValue = form?.watch(name) || "";
|
|
2014
|
-
const { strength, score } = calculatePasswordStrength(passwordValue);
|
|
2654
|
+
const { strength, score } = calculatePasswordStrength(passwordValue, minLength);
|
|
2015
2655
|
const strengthConfig = {
|
|
2016
2656
|
weak: {
|
|
2017
2657
|
label: t$1("ui:auth.password.weak"),
|
|
@@ -2194,9 +2834,16 @@ const PROVIDER_CONFIG = {
|
|
|
2194
2834
|
function SocialButtons({ callbackURL, className, disabled, layout = "stack", onSuccess }) {
|
|
2195
2835
|
const { t: t$1 } = useTranslation$1(["ui"]);
|
|
2196
2836
|
const { client, features } = useAuth();
|
|
2837
|
+
const { isLastMethod } = useLastUsedMethod();
|
|
2197
2838
|
const [loadingProvider, setLoadingProvider] = useState(null);
|
|
2198
|
-
const
|
|
2199
|
-
|
|
2839
|
+
const sortedProviders = useMemo(() => {
|
|
2840
|
+
return [...Object.keys(PROVIDER_CONFIG).filter((provider) => features.oauth[provider])].sort((a, b) => {
|
|
2841
|
+
if (isLastMethod(a)) return -1;
|
|
2842
|
+
if (isLastMethod(b)) return 1;
|
|
2843
|
+
return 0;
|
|
2844
|
+
});
|
|
2845
|
+
}, [features.oauth, isLastMethod]);
|
|
2846
|
+
if (sortedProviders.length === 0) return null;
|
|
2200
2847
|
const handleSignIn = async (provider) => {
|
|
2201
2848
|
setLoadingProvider(provider);
|
|
2202
2849
|
try {
|
|
@@ -2210,17 +2857,29 @@ function SocialButtons({ callbackURL, className, disabled, layout = "stack", onS
|
|
|
2210
2857
|
}
|
|
2211
2858
|
};
|
|
2212
2859
|
return /* @__PURE__ */ jsx("div", {
|
|
2213
|
-
className: cn("grid gap-3", layout === "grid" &&
|
|
2214
|
-
children:
|
|
2860
|
+
className: cn("grid gap-3", layout === "grid" && sortedProviders.length > 1 ? "grid-cols-2" : "grid-cols-1", className),
|
|
2861
|
+
children: sortedProviders.map((provider) => {
|
|
2215
2862
|
const config = PROVIDER_CONFIG[provider];
|
|
2216
2863
|
const isLoading = loadingProvider === provider;
|
|
2864
|
+
const isLast = isLastMethod(provider);
|
|
2217
2865
|
return /* @__PURE__ */ jsxs(Button, {
|
|
2218
|
-
variant: "outline",
|
|
2866
|
+
variant: isLast ? "default" : "outline",
|
|
2219
2867
|
type: "button",
|
|
2220
2868
|
disabled: disabled || loadingProvider !== null,
|
|
2221
2869
|
onClick: () => handleSignIn(provider),
|
|
2222
2870
|
className: cn("h-10 w-full gap-3 font-medium transition-all hover:shadow-sm", config.hoverClass),
|
|
2223
|
-
children: [
|
|
2871
|
+
children: [
|
|
2872
|
+
isLoading ? /* @__PURE__ */ jsx(Spinner, { className: "size-5" }) : config.icon,
|
|
2873
|
+
/* @__PURE__ */ jsx("span", {
|
|
2874
|
+
className: "flex-1 text-left",
|
|
2875
|
+
children: layout === "grid" && sortedProviders.length > 1 ? config.label : t$1("ui:auth.common.continueWith", { provider: config.label })
|
|
2876
|
+
}),
|
|
2877
|
+
isLast && /* @__PURE__ */ jsx(Badge, {
|
|
2878
|
+
variant: "secondary",
|
|
2879
|
+
className: "ml-1 text-xs",
|
|
2880
|
+
children: t$1("ui:auth.common.lastUsed")
|
|
2881
|
+
})
|
|
2882
|
+
]
|
|
2224
2883
|
}, provider);
|
|
2225
2884
|
})
|
|
2226
2885
|
});
|
|
@@ -2242,7 +2901,9 @@ function SocialButtons({ callbackURL, className, disabled, layout = "stack", onS
|
|
|
2242
2901
|
function SSOButton({ email, domain, callbackURL, className, disabled, label }) {
|
|
2243
2902
|
const { t: t$1 } = useTranslation$1(["ui"]);
|
|
2244
2903
|
const { client, features } = useAuth();
|
|
2904
|
+
const { isLastMethod } = useLastUsedMethod();
|
|
2245
2905
|
const [isLoading, setIsLoading] = useState(false);
|
|
2906
|
+
const isLast = isLastMethod("sso");
|
|
2246
2907
|
if (!features.sso) return null;
|
|
2247
2908
|
const handleSignIn = async () => {
|
|
2248
2909
|
setIsLoading(true);
|
|
@@ -2257,84 +2918,72 @@ function SSOButton({ email, domain, callbackURL, className, disabled, label }) {
|
|
|
2257
2918
|
}
|
|
2258
2919
|
};
|
|
2259
2920
|
return /* @__PURE__ */ jsxs(Button, {
|
|
2260
|
-
variant: "outline",
|
|
2921
|
+
variant: isLast ? "default" : "outline",
|
|
2261
2922
|
type: "button",
|
|
2262
2923
|
disabled: disabled || isLoading,
|
|
2263
2924
|
onClick: handleSignIn,
|
|
2264
2925
|
className: cn("h-10 w-full gap-3 font-medium transition-all hover:shadow-sm", className),
|
|
2265
|
-
children: [
|
|
2926
|
+
children: [
|
|
2927
|
+
isLoading ? /* @__PURE__ */ jsx(Spinner, { className: "size-5" }) : /* @__PURE__ */ jsx(Building2Icon, { className: cn("size-5", !isLast && "text-primary") }),
|
|
2928
|
+
/* @__PURE__ */ jsx("span", {
|
|
2929
|
+
className: "flex-1 text-left",
|
|
2930
|
+
children: label || t$1("ui:auth.sso.submit")
|
|
2931
|
+
}),
|
|
2932
|
+
isLast && /* @__PURE__ */ jsx(Badge, {
|
|
2933
|
+
variant: "secondary",
|
|
2934
|
+
className: "ml-1 text-xs",
|
|
2935
|
+
children: t$1("ui:auth.common.lastUsed")
|
|
2936
|
+
})
|
|
2937
|
+
]
|
|
2266
2938
|
});
|
|
2267
2939
|
}
|
|
2268
2940
|
|
|
2269
2941
|
//#endregion
|
|
2270
|
-
//#region src/web/ui/auth/
|
|
2271
|
-
/**
|
|
2272
|
-
* Creates the two-factor TOTP verification schema with i18n-aware error messages.
|
|
2273
|
-
* Accepts optional TOTP digits config for customizing validation rules.
|
|
2274
|
-
*
|
|
2275
|
-
* @param digits - The expected TOTP code length (default: 6).
|
|
2276
|
-
* @returns A Zod schema for validating TOTP verification form data.
|
|
2277
|
-
*
|
|
2278
|
-
* @example
|
|
2279
|
-
* ```typescript
|
|
2280
|
-
* const config = useAuthConfig();
|
|
2281
|
-
* const digits = config.plugins?.twoFactor?.totp?.digits ?? 6;
|
|
2282
|
-
* const schema = defineTwoFactorTotpSchema(digits);
|
|
2283
|
-
* ```
|
|
2284
|
-
*/
|
|
2285
|
-
function defineTwoFactorTotpSchema(digits = 6) {
|
|
2286
|
-
return z.object({
|
|
2287
|
-
code: z.string().min(digits, i18n.t("ui:validation.codeLength", { length: digits })).max(digits, i18n.t("ui:validation.codeLength", { length: digits })).regex(/^\d+$/, i18n.t("ui:validation.codeDigitsOnly")),
|
|
2288
|
-
trustDevice: z.boolean().optional()
|
|
2289
|
-
});
|
|
2290
|
-
}
|
|
2942
|
+
//#region src/web/ui/auth/phone-input.tsx
|
|
2291
2943
|
/**
|
|
2292
|
-
*
|
|
2293
|
-
*
|
|
2294
|
-
*
|
|
2295
|
-
* @param codeLength - The expected backup code length (default: 10).
|
|
2296
|
-
* @returns A Zod schema for validating backup code verification form data.
|
|
2944
|
+
* Phone number input field with react-hook-form integration.
|
|
2945
|
+
* Automatically handles validation errors from form context.
|
|
2946
|
+
* Accepts E.164 format phone numbers.
|
|
2297
2947
|
*
|
|
2298
2948
|
* @example
|
|
2299
|
-
* ```
|
|
2300
|
-
*
|
|
2301
|
-
*
|
|
2302
|
-
*
|
|
2949
|
+
* ```tsx
|
|
2950
|
+
* <FormProvider {...form}>
|
|
2951
|
+
* <PhoneInput name="phoneNumber" label="Phone number" showIcon />
|
|
2952
|
+
* </FormProvider>
|
|
2303
2953
|
* ```
|
|
2304
2954
|
*/
|
|
2305
|
-
function
|
|
2306
|
-
return z.object({
|
|
2307
|
-
code: z.string().min(1, i18n.t("ui:validation.backupCodeRequired")).max(codeLength * 2, i18n.t("ui:validation.backupCodeInvalid")),
|
|
2308
|
-
trustDevice: z.boolean().optional()
|
|
2309
|
-
});
|
|
2310
|
-
}
|
|
2311
|
-
function BackupCodeInput() {
|
|
2955
|
+
function PhoneInput({ name = "phoneNumber", label, hideLabel = false, showIcon = false, className, placeholder, ...props }) {
|
|
2312
2956
|
const id = useId();
|
|
2313
2957
|
const { t: t$1 } = useTranslation$1(["ui"]);
|
|
2314
2958
|
const form = useFormContext();
|
|
2315
|
-
const error = form?.formState?.errors?.
|
|
2959
|
+
const error = form?.formState?.errors?.[name];
|
|
2316
2960
|
const hasError = !!error;
|
|
2961
|
+
const resolvedLabel = label ?? t$1("ui:auth.phoneOtp.number");
|
|
2962
|
+
const resolvedPlaceholder = placeholder ?? t$1("ui:auth.phoneOtp.placeholder");
|
|
2317
2963
|
return /* @__PURE__ */ jsxs(Field, {
|
|
2318
2964
|
"data-invalid": hasError || void 0,
|
|
2319
2965
|
children: [
|
|
2320
2966
|
/* @__PURE__ */ jsx(FieldLabel, {
|
|
2321
2967
|
htmlFor: id,
|
|
2322
|
-
|
|
2968
|
+
className: cn(hideLabel && "sr-only"),
|
|
2969
|
+
children: resolvedLabel
|
|
2323
2970
|
}),
|
|
2324
2971
|
/* @__PURE__ */ jsxs("div", {
|
|
2325
2972
|
className: "relative",
|
|
2326
|
-
children: [/* @__PURE__ */ jsx("div", {
|
|
2973
|
+
children: [showIcon && /* @__PURE__ */ jsx("div", {
|
|
2327
2974
|
className: "pointer-events-none absolute left-3 top-1/2 -translate-y-1/2",
|
|
2328
|
-
children: /* @__PURE__ */ jsx(
|
|
2975
|
+
children: /* @__PURE__ */ jsx(PhoneIcon, { className: "size-4 text-muted-foreground" })
|
|
2329
2976
|
}), /* @__PURE__ */ jsx(Input, {
|
|
2330
2977
|
id,
|
|
2331
|
-
type: "
|
|
2332
|
-
|
|
2333
|
-
|
|
2978
|
+
type: "tel",
|
|
2979
|
+
inputMode: "tel",
|
|
2980
|
+
autoComplete: "tel",
|
|
2981
|
+
placeholder: resolvedPlaceholder,
|
|
2334
2982
|
"aria-invalid": hasError,
|
|
2335
2983
|
"aria-describedby": hasError ? `${id}-error` : void 0,
|
|
2336
|
-
className: "
|
|
2337
|
-
...form?.register(
|
|
2984
|
+
className: cn(showIcon && "pl-10", "h-10", className),
|
|
2985
|
+
...form?.register(name),
|
|
2986
|
+
...props
|
|
2338
2987
|
})]
|
|
2339
2988
|
}),
|
|
2340
2989
|
error?.message && /* @__PURE__ */ jsx(FieldError, {
|
|
@@ -2344,123 +2993,425 @@ function BackupCodeInput() {
|
|
|
2344
2993
|
]
|
|
2345
2994
|
});
|
|
2346
2995
|
}
|
|
2996
|
+
|
|
2997
|
+
//#endregion
|
|
2998
|
+
//#region src/web/ui/auth/forms/phone-otp-form.tsx
|
|
2347
2999
|
/**
|
|
2348
|
-
*
|
|
2349
|
-
*
|
|
3000
|
+
* Creates the OTP verification schema with i18n-aware error messages.
|
|
3001
|
+
* Accepts optional OTP length config for customizing validation rules.
|
|
3002
|
+
*
|
|
3003
|
+
* @param otpLength - The expected OTP code length (default: 6).
|
|
3004
|
+
* @returns A Zod schema for validating OTP verification form data.
|
|
3005
|
+
*
|
|
3006
|
+
* @example
|
|
3007
|
+
* ```typescript
|
|
3008
|
+
* const config = useAuthConfig();
|
|
3009
|
+
* const otpLength = config.methods?.phoneOtp?.otpLength ?? 6;
|
|
3010
|
+
* const schema = defineOtpSchema(otpLength);
|
|
3011
|
+
* ```
|
|
3012
|
+
*/
|
|
3013
|
+
function defineOtpSchema(otpLength = 6) {
|
|
3014
|
+
return z.object({ code: z.string().min(otpLength, i18n.t("ui:validation.codeLength", { length: otpLength })).max(otpLength, i18n.t("ui:validation.codeLength", { length: otpLength })).regex(/^\d+$/, i18n.t("ui:validation.codeDigitsOnly")) });
|
|
3015
|
+
}
|
|
3016
|
+
/**
|
|
3017
|
+
* Creates the phone number schema with i18n-aware error messages.
|
|
3018
|
+
*
|
|
3019
|
+
* @returns A Zod schema for validating phone number form data.
|
|
3020
|
+
*/
|
|
3021
|
+
function definePhoneNumberSchema() {
|
|
3022
|
+
return z.object({ phoneNumber: z.string().min(10, i18n.t("ui:validation.phoneTooShort")).regex(/^\+?[1-9]\d{1,14}$/, i18n.t("ui:validation.phoneInvalid")) });
|
|
3023
|
+
}
|
|
3024
|
+
/**
|
|
3025
|
+
* Phone OTP authentication form with two-stage flow.
|
|
3026
|
+
* Stage 1: Enter phone number to request OTP.
|
|
3027
|
+
* Stage 2: Enter the OTP code to verify.
|
|
2350
3028
|
*
|
|
2351
3029
|
* @example
|
|
2352
3030
|
* ```tsx
|
|
2353
3031
|
* <AuthProvider auth={auth}>
|
|
2354
|
-
* <
|
|
2355
|
-
*
|
|
3032
|
+
* <PhoneOTPForm
|
|
3033
|
+
* callbackURL="/dashboard"
|
|
2356
3034
|
* onSuccess={() => navigate("/dashboard")}
|
|
2357
3035
|
* />
|
|
2358
3036
|
* </AuthProvider>
|
|
2359
3037
|
* ```
|
|
2360
3038
|
*/
|
|
2361
|
-
function
|
|
3039
|
+
function PhoneOTPForm({ callbackURL, className, title, description, logo, onSuccess }) {
|
|
2362
3040
|
const { t: t$1 } = useTranslation$1(["ui"]);
|
|
2363
|
-
const { client, config } = useAuth();
|
|
3041
|
+
const { client, config, features } = useAuth();
|
|
3042
|
+
const captcha = useCaptcha();
|
|
3043
|
+
const [captchaToken, setCaptchaToken] = useState(null);
|
|
3044
|
+
const captchaResetRef = useRef(null);
|
|
2364
3045
|
const { isLoading, error, execute, setError } = useAuthForm();
|
|
2365
|
-
const [
|
|
2366
|
-
const
|
|
2367
|
-
const
|
|
2368
|
-
const
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
}
|
|
3046
|
+
const [stage, setStage] = useState("phone");
|
|
3047
|
+
const [pendingPhone, setPendingPhone] = useState(null);
|
|
3048
|
+
const [countdown, setCountdown] = useState(0);
|
|
3049
|
+
const countdownRef = useRef(null);
|
|
3050
|
+
const otpLength = config.methods?.phoneOtp?.otpLength ?? 6;
|
|
3051
|
+
useEffect(() => {
|
|
3052
|
+
return () => {
|
|
3053
|
+
if (countdownRef.current) clearInterval(countdownRef.current);
|
|
3054
|
+
};
|
|
3055
|
+
}, []);
|
|
3056
|
+
const phoneForm = useForm({
|
|
3057
|
+
resolver: zodResolver(definePhoneNumberSchema()),
|
|
3058
|
+
defaultValues: { phoneNumber: "" },
|
|
2374
3059
|
mode: "onBlur"
|
|
2375
3060
|
});
|
|
2376
|
-
const
|
|
2377
|
-
resolver: zodResolver(
|
|
2378
|
-
defaultValues: {
|
|
2379
|
-
code: "",
|
|
2380
|
-
trustDevice: false
|
|
2381
|
-
},
|
|
3061
|
+
const otpForm = useForm({
|
|
3062
|
+
resolver: zodResolver(defineOtpSchema(otpLength)),
|
|
3063
|
+
defaultValues: { code: "" },
|
|
2382
3064
|
mode: "onBlur"
|
|
2383
3065
|
});
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
3066
|
+
if (!features.phoneOtp) return null;
|
|
3067
|
+
/**
|
|
3068
|
+
* Starts a 30-second countdown timer for the resend button.
|
|
3069
|
+
* Clears any existing timer before starting a new one.
|
|
3070
|
+
*/
|
|
3071
|
+
const startCountdown = () => {
|
|
3072
|
+
if (countdownRef.current) clearInterval(countdownRef.current);
|
|
3073
|
+
setCountdown(30);
|
|
3074
|
+
countdownRef.current = setInterval(() => {
|
|
3075
|
+
setCountdown((c) => {
|
|
3076
|
+
if (c <= 1) {
|
|
3077
|
+
if (countdownRef.current) clearInterval(countdownRef.current);
|
|
3078
|
+
return 0;
|
|
3079
|
+
}
|
|
3080
|
+
return c - 1;
|
|
2389
3081
|
});
|
|
2390
|
-
|
|
3082
|
+
}, 1e3);
|
|
3083
|
+
};
|
|
3084
|
+
const handlePhoneSubmit = async (data) => {
|
|
3085
|
+
if (!await execute(async () => {
|
|
3086
|
+
const response = await client.phoneNumber.sendOtp({
|
|
3087
|
+
phoneNumber: data.phoneNumber,
|
|
3088
|
+
fetchOptions: captchaToken ? { headers: { "x-captcha-response": captchaToken } } : void 0
|
|
3089
|
+
});
|
|
3090
|
+
if (response.error) throw new Error(response.error.message || t$1("ui:auth.errors.generic"));
|
|
3091
|
+
setPendingPhone(data.phoneNumber);
|
|
3092
|
+
setStage("verify");
|
|
3093
|
+
startCountdown();
|
|
2391
3094
|
return response;
|
|
2392
|
-
}))
|
|
3095
|
+
})) {
|
|
3096
|
+
setCaptchaToken(null);
|
|
3097
|
+
captchaResetRef.current?.();
|
|
3098
|
+
}
|
|
2393
3099
|
};
|
|
2394
|
-
const
|
|
3100
|
+
const handleOtpSubmit = async (data) => {
|
|
3101
|
+
if (!pendingPhone) return;
|
|
2395
3102
|
if (await execute(async () => {
|
|
2396
|
-
const response = await client.
|
|
2397
|
-
|
|
2398
|
-
|
|
3103
|
+
const response = await client.phoneNumber.verify({
|
|
3104
|
+
phoneNumber: pendingPhone,
|
|
3105
|
+
code: data.code
|
|
2399
3106
|
});
|
|
2400
|
-
if (response.error) throw new Error(response.error.message || t$1("ui:auth.
|
|
3107
|
+
if (response.error) throw new Error(response.error.message || t$1("ui:auth.otp.invalid"));
|
|
2401
3108
|
return response;
|
|
2402
3109
|
})) onSuccess?.();
|
|
2403
3110
|
};
|
|
2404
|
-
const
|
|
3111
|
+
const handleResend = async () => {
|
|
3112
|
+
if (!pendingPhone) return;
|
|
3113
|
+
const result = await execute(async () => {
|
|
3114
|
+
const response = await client.phoneNumber.sendOtp({
|
|
3115
|
+
phoneNumber: pendingPhone,
|
|
3116
|
+
fetchOptions: captchaToken ? { headers: { "x-captcha-response": captchaToken } } : void 0
|
|
3117
|
+
});
|
|
3118
|
+
if (response.error) throw new Error(response.error.message || t$1("ui:auth.errors.generic"));
|
|
3119
|
+
return response;
|
|
3120
|
+
});
|
|
3121
|
+
if (!result) {
|
|
3122
|
+
setCaptchaToken(null);
|
|
3123
|
+
captchaResetRef.current?.();
|
|
3124
|
+
}
|
|
3125
|
+
if (result) startCountdown();
|
|
3126
|
+
};
|
|
3127
|
+
const handleBack = () => {
|
|
2405
3128
|
setError(null);
|
|
2406
|
-
|
|
3129
|
+
setStage("phone");
|
|
3130
|
+
otpForm.reset();
|
|
2407
3131
|
};
|
|
2408
|
-
if (
|
|
2409
|
-
title: t$1("ui:auth.
|
|
2410
|
-
description: t$1("ui:auth.
|
|
3132
|
+
if (stage === "verify" && pendingPhone) return /* @__PURE__ */ jsx(AuthCard, {
|
|
3133
|
+
title: t$1("ui:auth.otp.code"),
|
|
3134
|
+
description: t$1("ui:auth.phoneOtp.codeSent", { phone: pendingPhone }),
|
|
2411
3135
|
logo,
|
|
2412
3136
|
className,
|
|
2413
3137
|
children: /* @__PURE__ */ jsx(FormProvider, {
|
|
2414
|
-
...
|
|
3138
|
+
...otpForm,
|
|
2415
3139
|
children: /* @__PURE__ */ jsxs("form", {
|
|
2416
|
-
onSubmit:
|
|
3140
|
+
onSubmit: otpForm.handleSubmit(handleOtpSubmit),
|
|
2417
3141
|
className: "space-y-4",
|
|
2418
3142
|
children: [
|
|
2419
3143
|
error && /* @__PURE__ */ jsxs(Alert, {
|
|
2420
3144
|
variant: "destructive",
|
|
2421
3145
|
children: [/* @__PURE__ */ jsx(AlertCircleIcon, { className: "size-4" }), /* @__PURE__ */ jsx(AlertDescription, { children: error })]
|
|
2422
3146
|
}),
|
|
2423
|
-
/* @__PURE__ */ jsx(
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
}), /* @__PURE__ */ jsx(FieldLabel, {
|
|
2431
|
-
htmlFor: "trustDevice-backup",
|
|
2432
|
-
className: "text-sm font-normal",
|
|
2433
|
-
children: t$1("ui:auth.twoFactor.trustDevice")
|
|
2434
|
-
})]
|
|
3147
|
+
/* @__PURE__ */ jsx(OtpInput, {
|
|
3148
|
+
name: "code",
|
|
3149
|
+
label: t$1("ui:auth.otp.code"),
|
|
3150
|
+
length: otpLength,
|
|
3151
|
+
hideLabel: true,
|
|
3152
|
+
showSeparator: true,
|
|
3153
|
+
onComplete: () => otpForm.handleSubmit(handleOtpSubmit)()
|
|
2435
3154
|
}),
|
|
2436
3155
|
/* @__PURE__ */ jsx(Button, {
|
|
2437
3156
|
type: "submit",
|
|
2438
3157
|
className: "h-10 w-full font-medium",
|
|
2439
3158
|
disabled: isLoading,
|
|
2440
|
-
children: isLoading ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Spinner, { className: "mr-2 size-4" }), t$1("ui:auth.
|
|
2441
|
-
}),
|
|
2442
|
-
/* @__PURE__ */ jsx(Button, {
|
|
2443
|
-
type: "button",
|
|
2444
|
-
variant: "ghost",
|
|
2445
|
-
className: "w-full text-muted-foreground",
|
|
2446
|
-
onClick: switchMode,
|
|
2447
|
-
children: t$1("ui:auth.twoFactor.useAuthenticator")
|
|
3159
|
+
children: isLoading ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Spinner, { className: "mr-2 size-4" }), t$1("ui:auth.otp.verifying")] }) : t$1("ui:auth.otp.verify")
|
|
2448
3160
|
}),
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
3161
|
+
/* @__PURE__ */ jsxs("div", {
|
|
3162
|
+
className: "flex gap-2",
|
|
3163
|
+
children: [/* @__PURE__ */ jsxs(Button, {
|
|
3164
|
+
type: "button",
|
|
3165
|
+
variant: "ghost",
|
|
3166
|
+
className: "flex-1 text-muted-foreground",
|
|
3167
|
+
onClick: handleBack,
|
|
3168
|
+
children: [/* @__PURE__ */ jsx(ArrowLeftIcon, { className: "mr-2 size-4" }), t$1("ui:auth.common.backButton")]
|
|
3169
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
3170
|
+
type: "button",
|
|
3171
|
+
variant: "ghost",
|
|
3172
|
+
className: "flex-1 text-muted-foreground",
|
|
3173
|
+
onClick: handleResend,
|
|
3174
|
+
disabled: isLoading || countdown > 0,
|
|
3175
|
+
children: countdown > 0 ? t$1("ui:auth.otp.resendIn", { seconds: countdown }) : t$1("ui:auth.phoneOtp.resend")
|
|
3176
|
+
})]
|
|
3177
|
+
})
|
|
3178
|
+
]
|
|
3179
|
+
})
|
|
3180
|
+
})
|
|
3181
|
+
});
|
|
3182
|
+
return /* @__PURE__ */ jsx(AuthCard, {
|
|
3183
|
+
title: title || t$1("ui:auth.phoneOtp.title"),
|
|
3184
|
+
description,
|
|
3185
|
+
logo,
|
|
3186
|
+
className,
|
|
3187
|
+
children: /* @__PURE__ */ jsx(FormProvider, {
|
|
3188
|
+
...phoneForm,
|
|
3189
|
+
children: /* @__PURE__ */ jsxs("form", {
|
|
3190
|
+
onSubmit: phoneForm.handleSubmit(handlePhoneSubmit),
|
|
3191
|
+
className: "space-y-4",
|
|
3192
|
+
children: [
|
|
3193
|
+
error && /* @__PURE__ */ jsxs(Alert, {
|
|
3194
|
+
variant: "destructive",
|
|
3195
|
+
children: [/* @__PURE__ */ jsx(AlertCircleIcon, { className: "size-4" }), /* @__PURE__ */ jsx(AlertDescription, { children: error })]
|
|
3196
|
+
}),
|
|
3197
|
+
/* @__PURE__ */ jsx(PhoneInput, {
|
|
3198
|
+
name: "phoneNumber",
|
|
3199
|
+
label: t$1("ui:auth.phoneOtp.number"),
|
|
3200
|
+
showIcon: true
|
|
3201
|
+
}),
|
|
3202
|
+
captcha.enabled && /* @__PURE__ */ jsx(CaptchaWidget, {
|
|
3203
|
+
onSuccess: setCaptchaToken,
|
|
3204
|
+
onExpire: () => setCaptchaToken(null),
|
|
3205
|
+
onError: () => setCaptchaToken(null),
|
|
3206
|
+
resetRef: captchaResetRef
|
|
3207
|
+
}),
|
|
3208
|
+
/* @__PURE__ */ jsx(Button, {
|
|
3209
|
+
type: "submit",
|
|
3210
|
+
className: "h-10 w-full font-medium",
|
|
3211
|
+
disabled: isLoading || captcha.enabled && !captchaToken,
|
|
3212
|
+
children: isLoading ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Spinner, { className: "mr-2 size-4" }), t$1("ui:auth.phoneOtp.sending")] }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(PhoneIcon, { className: "mr-2 size-4" }), t$1("ui:auth.phoneOtp.send")] })
|
|
3213
|
+
})
|
|
3214
|
+
]
|
|
3215
|
+
})
|
|
3216
|
+
})
|
|
3217
|
+
});
|
|
3218
|
+
}
|
|
3219
|
+
|
|
3220
|
+
//#endregion
|
|
3221
|
+
//#region src/web/ui/auth/forms/two-factor-form.tsx
|
|
3222
|
+
/**
|
|
3223
|
+
* Creates the two-factor TOTP verification schema with i18n-aware error messages.
|
|
3224
|
+
* Accepts optional TOTP digits config for customizing validation rules.
|
|
3225
|
+
*
|
|
3226
|
+
* @param digits - The expected TOTP code length (default: 6).
|
|
3227
|
+
* @returns A Zod schema for validating TOTP verification form data.
|
|
3228
|
+
*
|
|
3229
|
+
* @example
|
|
3230
|
+
* ```typescript
|
|
3231
|
+
* const config = useAuthConfig();
|
|
3232
|
+
* const digits = config.plugins?.twoFactor?.totp?.digits ?? 6;
|
|
3233
|
+
* const schema = defineTwoFactorTotpSchema(digits);
|
|
3234
|
+
* ```
|
|
3235
|
+
*/
|
|
3236
|
+
function defineTwoFactorTotpSchema(digits = 6) {
|
|
3237
|
+
return z.object({
|
|
3238
|
+
code: z.string().min(digits, i18n.t("ui:validation.codeLength", { length: digits })).max(digits, i18n.t("ui:validation.codeLength", { length: digits })).regex(/^\d+$/, i18n.t("ui:validation.codeDigitsOnly")),
|
|
3239
|
+
trustDevice: z.boolean().optional()
|
|
3240
|
+
});
|
|
3241
|
+
}
|
|
3242
|
+
/**
|
|
3243
|
+
* Creates the two-factor backup code verification schema with i18n-aware error messages.
|
|
3244
|
+
* Accepts optional backup code length config for customizing validation rules.
|
|
3245
|
+
*
|
|
3246
|
+
* @param codeLength - The expected backup code length (default: 10).
|
|
3247
|
+
* @returns A Zod schema for validating backup code verification form data.
|
|
3248
|
+
*
|
|
3249
|
+
* @example
|
|
3250
|
+
* ```typescript
|
|
3251
|
+
* const config = useAuthConfig();
|
|
3252
|
+
* const codeLength = config.plugins?.twoFactor?.backupCodes?.length ?? 10;
|
|
3253
|
+
* const schema = defineTwoFactorBackupSchema(codeLength);
|
|
3254
|
+
* ```
|
|
3255
|
+
*/
|
|
3256
|
+
function defineTwoFactorBackupSchema(codeLength = 10) {
|
|
3257
|
+
return z.object({
|
|
3258
|
+
code: z.string().min(1, i18n.t("ui:validation.backupCodeRequired")).max(codeLength * 2, i18n.t("ui:validation.backupCodeInvalid")),
|
|
3259
|
+
trustDevice: z.boolean().optional()
|
|
3260
|
+
});
|
|
3261
|
+
}
|
|
3262
|
+
function BackupCodeInput() {
|
|
3263
|
+
const id = useId();
|
|
3264
|
+
const { t: t$1 } = useTranslation$1(["ui"]);
|
|
3265
|
+
const form = useFormContext();
|
|
3266
|
+
const error = form?.formState?.errors?.code;
|
|
3267
|
+
const hasError = !!error;
|
|
3268
|
+
return /* @__PURE__ */ jsxs(Field, {
|
|
3269
|
+
"data-invalid": hasError || void 0,
|
|
3270
|
+
children: [
|
|
3271
|
+
/* @__PURE__ */ jsx(FieldLabel, {
|
|
3272
|
+
htmlFor: id,
|
|
3273
|
+
children: t$1("ui:auth.twoFactor.backupCode")
|
|
3274
|
+
}),
|
|
3275
|
+
/* @__PURE__ */ jsxs("div", {
|
|
3276
|
+
className: "relative",
|
|
3277
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
3278
|
+
className: "pointer-events-none absolute left-3 top-1/2 -translate-y-1/2",
|
|
3279
|
+
children: /* @__PURE__ */ jsx(KeyIcon, { className: "size-4 text-muted-foreground" })
|
|
3280
|
+
}), /* @__PURE__ */ jsx(Input, {
|
|
3281
|
+
id,
|
|
3282
|
+
type: "text",
|
|
3283
|
+
autoComplete: "one-time-code",
|
|
3284
|
+
placeholder: t$1("ui:auth.twoFactor.backupPlaceholder"),
|
|
3285
|
+
"aria-invalid": hasError,
|
|
3286
|
+
"aria-describedby": hasError ? `${id}-error` : void 0,
|
|
3287
|
+
className: "h-10 pl-10 font-mono",
|
|
3288
|
+
...form?.register("code")
|
|
3289
|
+
})]
|
|
3290
|
+
}),
|
|
3291
|
+
error?.message && /* @__PURE__ */ jsx(FieldError, {
|
|
3292
|
+
id: `${id}-error`,
|
|
3293
|
+
children: String(error.message)
|
|
3294
|
+
})
|
|
3295
|
+
]
|
|
3296
|
+
});
|
|
3297
|
+
}
|
|
3298
|
+
/**
|
|
3299
|
+
* Two-factor authentication form with improved UX.
|
|
3300
|
+
* Supports TOTP authenticator apps and backup codes.
|
|
3301
|
+
*
|
|
3302
|
+
* @example
|
|
3303
|
+
* ```tsx
|
|
3304
|
+
* <AuthProvider auth={auth}>
|
|
3305
|
+
* <TwoFactorForm
|
|
3306
|
+
* showTrustDevice
|
|
3307
|
+
* onSuccess={() => navigate("/dashboard")}
|
|
3308
|
+
* />
|
|
3309
|
+
* </AuthProvider>
|
|
3310
|
+
* ```
|
|
3311
|
+
*/
|
|
3312
|
+
function TwoFactorForm({ className, title, description, logo, showTrustDevice = true, onSuccess, onBack }) {
|
|
3313
|
+
const { t: t$1 } = useTranslation$1(["ui"]);
|
|
3314
|
+
const { client, config } = useAuth();
|
|
3315
|
+
const { isLoading, error, execute, setError } = useAuthForm();
|
|
3316
|
+
const [mode, setMode] = useState("totp");
|
|
3317
|
+
const totpDigits = config.plugins?.twoFactor?.totp?.digits ?? 6;
|
|
3318
|
+
const backupCodeLength = config.plugins?.twoFactor?.backupCodes?.length ?? 10;
|
|
3319
|
+
const totpForm = useForm({
|
|
3320
|
+
resolver: zodResolver(defineTwoFactorTotpSchema(totpDigits)),
|
|
3321
|
+
defaultValues: {
|
|
3322
|
+
code: "",
|
|
3323
|
+
trustDevice: false
|
|
3324
|
+
},
|
|
3325
|
+
mode: "onBlur"
|
|
3326
|
+
});
|
|
3327
|
+
const backupForm = useForm({
|
|
3328
|
+
resolver: zodResolver(defineTwoFactorBackupSchema(backupCodeLength)),
|
|
3329
|
+
defaultValues: {
|
|
3330
|
+
code: "",
|
|
3331
|
+
trustDevice: false
|
|
3332
|
+
},
|
|
3333
|
+
mode: "onBlur"
|
|
3334
|
+
});
|
|
3335
|
+
const handleTotpSubmit = async (data) => {
|
|
3336
|
+
if (await execute(async () => {
|
|
3337
|
+
const response = await client.twoFactor.verifyTotp({
|
|
3338
|
+
code: data.code,
|
|
3339
|
+
trustDevice: data.trustDevice
|
|
3340
|
+
});
|
|
3341
|
+
if (response.error) throw new Error(response.error.message || t$1("ui:auth.errors.codeInvalid"));
|
|
3342
|
+
return response;
|
|
3343
|
+
})) onSuccess?.();
|
|
3344
|
+
};
|
|
3345
|
+
const handleBackupSubmit = async (data) => {
|
|
3346
|
+
if (await execute(async () => {
|
|
3347
|
+
const response = await client.twoFactor.verifyBackupCode({
|
|
3348
|
+
code: data.code,
|
|
3349
|
+
trustDevice: data.trustDevice
|
|
3350
|
+
});
|
|
3351
|
+
if (response.error) throw new Error(response.error.message || t$1("ui:auth.errors.backupCodeInvalid"));
|
|
3352
|
+
return response;
|
|
3353
|
+
})) onSuccess?.();
|
|
3354
|
+
};
|
|
3355
|
+
const switchMode = () => {
|
|
3356
|
+
setError(null);
|
|
3357
|
+
setMode(mode === "totp" ? "backup" : "totp");
|
|
3358
|
+
};
|
|
3359
|
+
if (mode === "backup") return /* @__PURE__ */ jsx(AuthCard, {
|
|
3360
|
+
title: t$1("ui:auth.twoFactor.backupTitle"),
|
|
3361
|
+
description: t$1("ui:auth.twoFactor.backupDescription"),
|
|
3362
|
+
logo,
|
|
3363
|
+
className,
|
|
3364
|
+
children: /* @__PURE__ */ jsx(FormProvider, {
|
|
3365
|
+
...backupForm,
|
|
3366
|
+
children: /* @__PURE__ */ jsxs("form", {
|
|
3367
|
+
onSubmit: backupForm.handleSubmit(handleBackupSubmit),
|
|
3368
|
+
className: "space-y-4",
|
|
3369
|
+
children: [
|
|
3370
|
+
error && /* @__PURE__ */ jsxs(Alert, {
|
|
3371
|
+
variant: "destructive",
|
|
3372
|
+
children: [/* @__PURE__ */ jsx(AlertCircleIcon, { className: "size-4" }), /* @__PURE__ */ jsx(AlertDescription, { children: error })]
|
|
3373
|
+
}),
|
|
3374
|
+
/* @__PURE__ */ jsx(BackupCodeInput, {}),
|
|
3375
|
+
showTrustDevice && /* @__PURE__ */ jsxs(Field, {
|
|
3376
|
+
orientation: "horizontal",
|
|
3377
|
+
className: "w-auto",
|
|
3378
|
+
children: [/* @__PURE__ */ jsx(Checkbox, {
|
|
3379
|
+
id: "trustDevice-backup",
|
|
3380
|
+
...backupForm.register("trustDevice")
|
|
3381
|
+
}), /* @__PURE__ */ jsx(FieldLabel, {
|
|
3382
|
+
htmlFor: "trustDevice-backup",
|
|
3383
|
+
className: "text-sm font-normal",
|
|
3384
|
+
children: t$1("ui:auth.twoFactor.trustDevice")
|
|
3385
|
+
})]
|
|
3386
|
+
}),
|
|
3387
|
+
/* @__PURE__ */ jsx(Button, {
|
|
3388
|
+
type: "submit",
|
|
3389
|
+
className: "h-10 w-full font-medium",
|
|
3390
|
+
disabled: isLoading,
|
|
3391
|
+
children: isLoading ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Spinner, { className: "mr-2 size-4" }), t$1("ui:auth.twoFactor.submitting")] }) : t$1("ui:auth.twoFactor.submit")
|
|
3392
|
+
}),
|
|
3393
|
+
/* @__PURE__ */ jsx(Button, {
|
|
3394
|
+
type: "button",
|
|
3395
|
+
variant: "ghost",
|
|
3396
|
+
className: "w-full text-muted-foreground",
|
|
3397
|
+
onClick: switchMode,
|
|
3398
|
+
children: t$1("ui:auth.twoFactor.useAuthenticator")
|
|
3399
|
+
}),
|
|
3400
|
+
onBack && /* @__PURE__ */ jsxs(Button, {
|
|
3401
|
+
type: "button",
|
|
3402
|
+
variant: "ghost",
|
|
3403
|
+
className: "w-full text-muted-foreground",
|
|
3404
|
+
onClick: onBack,
|
|
3405
|
+
children: [/* @__PURE__ */ jsx(ArrowLeftIcon, { className: "mr-2 size-4" }), t$1("ui:auth.common.backButton")]
|
|
3406
|
+
})
|
|
3407
|
+
]
|
|
3408
|
+
})
|
|
3409
|
+
})
|
|
3410
|
+
});
|
|
3411
|
+
return /* @__PURE__ */ jsx(AuthCard, {
|
|
3412
|
+
title: title || t$1("ui:auth.twoFactor.title"),
|
|
3413
|
+
description: description || t$1("ui:auth.twoFactor.description"),
|
|
3414
|
+
logo,
|
|
2464
3415
|
className,
|
|
2465
3416
|
children: /* @__PURE__ */ jsx(FormProvider, {
|
|
2466
3417
|
...totpForm,
|
|
@@ -2566,6 +3517,9 @@ function LoginForm({ callbackURL, className, forgotPasswordLink = "/forgot-passw
|
|
|
2566
3517
|
const { t: t$1 } = useTranslation$1(["ui"]);
|
|
2567
3518
|
const { client, features } = useAuth();
|
|
2568
3519
|
const { isLoading, error, execute } = useAuthForm();
|
|
3520
|
+
const captcha = useCaptcha();
|
|
3521
|
+
const [captchaToken, setCaptchaToken] = useState(null);
|
|
3522
|
+
const captchaResetRef = useRef(null);
|
|
2569
3523
|
const [twoFactorPending, setTwoFactorPending] = useState(false);
|
|
2570
3524
|
const [emailVerificationRequired, setEmailVerificationRequired] = useState(false);
|
|
2571
3525
|
const [resendSuccess, setResendSuccess] = useState(false);
|
|
@@ -2579,7 +3533,7 @@ function LoginForm({ callbackURL, className, forgotPasswordLink = "/forgot-passw
|
|
|
2579
3533
|
mode: "onBlur"
|
|
2580
3534
|
});
|
|
2581
3535
|
const email = form.watch("email");
|
|
2582
|
-
const hasAlternativeAuth = features.magicLink || features.passkey || features.sso || Object.values(features.oauth).some(Boolean);
|
|
3536
|
+
const hasAlternativeAuth = features.magicLink || features.passkey || features.sso || features.phoneOtp || features.emailOtp || Object.values(features.oauth).some(Boolean);
|
|
2583
3537
|
const hasPasswordlessOnly = !features.emailPassword && hasAlternativeAuth;
|
|
2584
3538
|
const hasAnyAuthMethod = features.emailPassword || hasAlternativeAuth;
|
|
2585
3539
|
const handleResendVerification = async () => {
|
|
@@ -2603,7 +3557,8 @@ function LoginForm({ callbackURL, className, forgotPasswordLink = "/forgot-passw
|
|
|
2603
3557
|
email: data.email,
|
|
2604
3558
|
password: data.password,
|
|
2605
3559
|
rememberMe: data.rememberMe,
|
|
2606
|
-
callbackURL
|
|
3560
|
+
callbackURL,
|
|
3561
|
+
fetchOptions: captchaToken ? { headers: { "x-captcha-response": captchaToken } } : void 0
|
|
2607
3562
|
});
|
|
2608
3563
|
if (response.error?.status === 403) {
|
|
2609
3564
|
setEmailVerificationRequired(true);
|
|
@@ -2614,410 +3569,173 @@ function LoginForm({ callbackURL, className, forgotPasswordLink = "/forgot-passw
|
|
|
2614
3569
|
return response;
|
|
2615
3570
|
}
|
|
2616
3571
|
if (response.error) throw new Error(response.error.message || t$1("ui:auth.errors.invalidCredentials"));
|
|
2617
|
-
return response;
|
|
2618
|
-
});
|
|
2619
|
-
if (result) {
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
/* @__PURE__ */ jsx(Link$1, {
|
|
2627
|
-
to: signupLink,
|
|
2628
|
-
className: "font-medium text-primary transition-colors hover:text-primary/80 hover:underline",
|
|
2629
|
-
children: t$1("ui:auth.login.signUp")
|
|
2630
|
-
})
|
|
2631
|
-
] });
|
|
2632
|
-
if (twoFactorPending) return /* @__PURE__ */ jsx(TwoFactorForm, {
|
|
2633
|
-
className,
|
|
2634
|
-
logo,
|
|
2635
|
-
onSuccess,
|
|
2636
|
-
onBack: () => setTwoFactorPending(false)
|
|
2637
|
-
});
|
|
2638
|
-
if (!hasAnyAuthMethod) return /* @__PURE__ */ jsx(AuthCard, {
|
|
2639
|
-
title: title || t$1("ui:auth.login.title"),
|
|
2640
|
-
description,
|
|
2641
|
-
footer,
|
|
2642
|
-
logo,
|
|
2643
|
-
showSecurityIcon,
|
|
2644
|
-
className,
|
|
2645
|
-
children: /* @__PURE__ */ jsx(Empty, {
|
|
2646
|
-
className: "border-none p-6",
|
|
2647
|
-
children: /* @__PURE__ */ jsxs(EmptyHeader, { children: [
|
|
2648
|
-
/* @__PURE__ */ jsx(EmptyMedia, {
|
|
2649
|
-
variant: "icon",
|
|
2650
|
-
children: /* @__PURE__ */ jsx(SettingsIcon, {})
|
|
2651
|
-
}),
|
|
2652
|
-
/* @__PURE__ */ jsx(EmptyTitle, { children: t$1("ui:auth.common.noMethodsConfigured") }),
|
|
2653
|
-
/* @__PURE__ */ jsx(EmptyDescription, { children: t$1("ui:auth.common.noMethodsDescription") })
|
|
2654
|
-
] })
|
|
2655
|
-
})
|
|
2656
|
-
});
|
|
2657
|
-
return /* @__PURE__ */ jsx(AuthCard, {
|
|
2658
|
-
title: title || t$1("ui:auth.login.title"),
|
|
2659
|
-
description: description || t$1("ui:auth.login.description"),
|
|
2660
|
-
footer,
|
|
2661
|
-
logo,
|
|
2662
|
-
showSecurityIcon,
|
|
2663
|
-
className,
|
|
2664
|
-
children: /* @__PURE__ */ jsx(FormProvider, {
|
|
2665
|
-
...form,
|
|
2666
|
-
children: /* @__PURE__ */ jsxs("form", {
|
|
2667
|
-
onSubmit: form.handleSubmit(onSubmit),
|
|
2668
|
-
className: "space-y-4",
|
|
2669
|
-
children: [
|
|
2670
|
-
error && !emailVerificationRequired && /* @__PURE__ */ jsxs(Alert, {
|
|
2671
|
-
variant: "destructive",
|
|
2672
|
-
children: [/* @__PURE__ */ jsx(AlertCircleIcon, { className: "size-4" }), /* @__PURE__ */ jsx(AlertDescription, { children: error })]
|
|
2673
|
-
}),
|
|
2674
|
-
emailVerificationRequired && (resendSuccess ? /* @__PURE__ */ jsxs(Alert, {
|
|
2675
|
-
variant: "success",
|
|
2676
|
-
children: [/* @__PURE__ */ jsx(CheckCircleIcon, { className: "size-4" }), /* @__PURE__ */ jsx(AlertDescription, { children: t$1("ui:auth.verifyEmail.resendSuccess") })]
|
|
2677
|
-
}) : /* @__PURE__ */ jsxs(Alert, {
|
|
2678
|
-
variant: "warning",
|
|
2679
|
-
children: [
|
|
2680
|
-
/* @__PURE__ */ jsx(MailIcon, { className: "size-4" }),
|
|
2681
|
-
/* @__PURE__ */ jsx(AlertDescription, { children: t$1("ui:auth.login.emailNotVerifiedDescription") }),
|
|
2682
|
-
/* @__PURE__ */ jsx("div", {
|
|
2683
|
-
className: "col-start-2 flex justify-end pt-2",
|
|
2684
|
-
children: /* @__PURE__ */ jsxs(Button, {
|
|
2685
|
-
type: "button",
|
|
2686
|
-
size: "sm",
|
|
2687
|
-
className: "h-7 bg-amber-900 text-white hover:bg-amber-800 dark:bg-amber-100 dark:text-amber-900 dark:hover:bg-amber-200",
|
|
2688
|
-
onClick: handleResendVerification,
|
|
2689
|
-
disabled: isLoading,
|
|
2690
|
-
children: [isLoading && /* @__PURE__ */ jsx(Spinner, { className: "size-3" }), t$1("ui:auth.verifyEmail.resend")]
|
|
2691
|
-
})
|
|
2692
|
-
})
|
|
2693
|
-
]
|
|
2694
|
-
})),
|
|
2695
|
-
hasPasswordlessOnly && /* @__PURE__ */ jsxs("div", {
|
|
2696
|
-
className: "space-y-3",
|
|
2697
|
-
children: [
|
|
2698
|
-
/* @__PURE__ */ jsx(PasskeyButton, {
|
|
2699
|
-
disabled: isLoading,
|
|
2700
|
-
onSuccess
|
|
2701
|
-
}),
|
|
2702
|
-
/* @__PURE__ */ jsx(SocialButtons, {
|
|
2703
|
-
callbackURL,
|
|
2704
|
-
disabled: isLoading
|
|
2705
|
-
}),
|
|
2706
|
-
/* @__PURE__ */ jsx(SSOButton, {
|
|
2707
|
-
email,
|
|
2708
|
-
callbackURL,
|
|
2709
|
-
disabled: isLoading
|
|
2710
|
-
})
|
|
2711
|
-
]
|
|
2712
|
-
}),
|
|
2713
|
-
features.emailPassword && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2714
|
-
(features.passkey || Object.values(features.oauth).some(Boolean)) && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
|
|
2715
|
-
className: "space-y-3",
|
|
2716
|
-
children: [/* @__PURE__ */ jsx(PasskeyButton, {
|
|
2717
|
-
disabled: isLoading,
|
|
2718
|
-
onSuccess
|
|
2719
|
-
}), /* @__PURE__ */ jsx(SocialButtons, {
|
|
2720
|
-
callbackURL,
|
|
2721
|
-
disabled: isLoading
|
|
2722
|
-
})]
|
|
2723
|
-
}), /* @__PURE__ */ jsx(FieldSeparator, { children: t$1("ui:auth.login.orContinueWith") })] }),
|
|
2724
|
-
/* @__PURE__ */ jsx(EmailInput, {
|
|
2725
|
-
name: "email",
|
|
2726
|
-
label: t$1("ui:auth.login.email"),
|
|
2727
|
-
showIcon: true
|
|
2728
|
-
}),
|
|
2729
|
-
/* @__PURE__ */ jsx(PasswordInput, {
|
|
2730
|
-
name: "password",
|
|
2731
|
-
label: t$1("ui:auth.login.password"),
|
|
2732
|
-
showIcon: true
|
|
2733
|
-
}),
|
|
2734
|
-
/* @__PURE__ */ jsxs("div", {
|
|
2735
|
-
className: "flex items-center justify-between",
|
|
2736
|
-
children: [showRememberMe && /* @__PURE__ */ jsxs(Field, {
|
|
2737
|
-
orientation: "horizontal",
|
|
2738
|
-
className: "w-auto",
|
|
2739
|
-
children: [/* @__PURE__ */ jsx(Checkbox, {
|
|
2740
|
-
id: "rememberMe",
|
|
2741
|
-
...form.register("rememberMe")
|
|
2742
|
-
}), /* @__PURE__ */ jsx(FieldLabel, {
|
|
2743
|
-
htmlFor: "rememberMe",
|
|
2744
|
-
className: "text-sm font-normal",
|
|
2745
|
-
children: t$1("ui:auth.login.rememberMe")
|
|
2746
|
-
})]
|
|
2747
|
-
}), forgotPasswordLink && /* @__PURE__ */ jsx(Link$1, {
|
|
2748
|
-
to: forgotPasswordLink,
|
|
2749
|
-
className: "text-sm text-muted-foreground transition-colors hover:text-primary",
|
|
2750
|
-
children: t$1("ui:auth.login.forgotPassword")
|
|
2751
|
-
})]
|
|
2752
|
-
}),
|
|
2753
|
-
/* @__PURE__ */ jsx(Button, {
|
|
2754
|
-
type: "submit",
|
|
2755
|
-
className: "h-10 w-full font-medium",
|
|
2756
|
-
disabled: isLoading,
|
|
2757
|
-
children: isLoading ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Spinner, { className: "mr-2 size-4" }), t$1("ui:auth.login.submitting")] }) : t$1("ui:auth.login.submit")
|
|
2758
|
-
}),
|
|
2759
|
-
features.sso && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(FieldSeparator, { children: t$1("ui:auth.common.or") }), /* @__PURE__ */ jsx(SSOButton, {
|
|
2760
|
-
email,
|
|
2761
|
-
callbackURL,
|
|
2762
|
-
disabled: isLoading
|
|
2763
|
-
})] })
|
|
2764
|
-
] }),
|
|
2765
|
-
features.magicLink && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(FieldSeparator, { children: t$1("ui:auth.login.orSignInWith") }), /* @__PURE__ */ jsx(MagicLinkForm, { callbackURL })] })
|
|
2766
|
-
]
|
|
2767
|
-
})
|
|
2768
|
-
})
|
|
2769
|
-
});
|
|
2770
|
-
}
|
|
2771
|
-
|
|
2772
|
-
//#endregion
|
|
2773
|
-
//#region src/web/ui/auth/phone-input.tsx
|
|
2774
|
-
/**
|
|
2775
|
-
* Phone number input field with react-hook-form integration.
|
|
2776
|
-
* Automatically handles validation errors from form context.
|
|
2777
|
-
* Accepts E.164 format phone numbers.
|
|
2778
|
-
*
|
|
2779
|
-
* @example
|
|
2780
|
-
* ```tsx
|
|
2781
|
-
* <FormProvider {...form}>
|
|
2782
|
-
* <PhoneInput name="phoneNumber" label="Phone number" showIcon />
|
|
2783
|
-
* </FormProvider>
|
|
2784
|
-
* ```
|
|
2785
|
-
*/
|
|
2786
|
-
function PhoneInput({ name = "phoneNumber", label, hideLabel = false, showIcon = false, className, placeholder, ...props }) {
|
|
2787
|
-
const id = useId();
|
|
2788
|
-
const { t: t$1 } = useTranslation$1(["ui"]);
|
|
2789
|
-
const form = useFormContext();
|
|
2790
|
-
const error = form?.formState?.errors?.[name];
|
|
2791
|
-
const hasError = !!error;
|
|
2792
|
-
const resolvedLabel = label ?? t$1("ui:auth.phoneOtp.number");
|
|
2793
|
-
const resolvedPlaceholder = placeholder ?? t$1("ui:auth.phoneOtp.placeholder");
|
|
2794
|
-
return /* @__PURE__ */ jsxs(Field, {
|
|
2795
|
-
"data-invalid": hasError || void 0,
|
|
2796
|
-
children: [
|
|
2797
|
-
/* @__PURE__ */ jsx(FieldLabel, {
|
|
2798
|
-
htmlFor: id,
|
|
2799
|
-
className: cn(hideLabel && "sr-only"),
|
|
2800
|
-
children: resolvedLabel
|
|
2801
|
-
}),
|
|
2802
|
-
/* @__PURE__ */ jsxs("div", {
|
|
2803
|
-
className: "relative",
|
|
2804
|
-
children: [showIcon && /* @__PURE__ */ jsx("div", {
|
|
2805
|
-
className: "pointer-events-none absolute left-3 top-1/2 -translate-y-1/2",
|
|
2806
|
-
children: /* @__PURE__ */ jsx(PhoneIcon, { className: "size-4 text-muted-foreground" })
|
|
2807
|
-
}), /* @__PURE__ */ jsx(Input, {
|
|
2808
|
-
id,
|
|
2809
|
-
type: "tel",
|
|
2810
|
-
inputMode: "tel",
|
|
2811
|
-
autoComplete: "tel",
|
|
2812
|
-
placeholder: resolvedPlaceholder,
|
|
2813
|
-
"aria-invalid": hasError,
|
|
2814
|
-
"aria-describedby": hasError ? `${id}-error` : void 0,
|
|
2815
|
-
className: cn(showIcon && "pl-10", "h-10", className),
|
|
2816
|
-
...form?.register(name),
|
|
2817
|
-
...props
|
|
2818
|
-
})]
|
|
2819
|
-
}),
|
|
2820
|
-
error?.message && /* @__PURE__ */ jsx(FieldError, {
|
|
2821
|
-
id: `${id}-error`,
|
|
2822
|
-
children: String(error.message)
|
|
2823
|
-
})
|
|
2824
|
-
]
|
|
2825
|
-
});
|
|
2826
|
-
}
|
|
2827
|
-
|
|
2828
|
-
//#endregion
|
|
2829
|
-
//#region src/web/ui/auth/forms/phone-otp-form.tsx
|
|
2830
|
-
/**
|
|
2831
|
-
* Creates the OTP verification schema with i18n-aware error messages.
|
|
2832
|
-
* Accepts optional OTP length config for customizing validation rules.
|
|
2833
|
-
*
|
|
2834
|
-
* @param otpLength - The expected OTP code length (default: 6).
|
|
2835
|
-
* @returns A Zod schema for validating OTP verification form data.
|
|
2836
|
-
*
|
|
2837
|
-
* @example
|
|
2838
|
-
* ```typescript
|
|
2839
|
-
* const config = useAuthConfig();
|
|
2840
|
-
* const otpLength = config.methods?.phoneOtp?.otpLength ?? 6;
|
|
2841
|
-
* const schema = defineOtpSchema(otpLength);
|
|
2842
|
-
* ```
|
|
2843
|
-
*/
|
|
2844
|
-
function defineOtpSchema(otpLength = 6) {
|
|
2845
|
-
return z.object({ code: z.string().min(otpLength, i18n.t("ui:validation.codeLength", { length: otpLength })).max(otpLength, i18n.t("ui:validation.codeLength", { length: otpLength })).regex(/^\d+$/, i18n.t("ui:validation.codeDigitsOnly")) });
|
|
2846
|
-
}
|
|
2847
|
-
/**
|
|
2848
|
-
* Creates the phone number schema with i18n-aware error messages.
|
|
2849
|
-
*
|
|
2850
|
-
* @returns A Zod schema for validating phone number form data.
|
|
2851
|
-
*/
|
|
2852
|
-
function definePhoneNumberSchema() {
|
|
2853
|
-
return z.object({ phoneNumber: z.string().min(10, i18n.t("ui:validation.phoneTooShort")).regex(/^\+?[1-9]\d{1,14}$/, i18n.t("ui:validation.phoneInvalid")) });
|
|
2854
|
-
}
|
|
2855
|
-
/**
|
|
2856
|
-
* Phone OTP authentication form with two-stage flow.
|
|
2857
|
-
* Stage 1: Enter phone number to request OTP.
|
|
2858
|
-
* Stage 2: Enter the OTP code to verify.
|
|
2859
|
-
*
|
|
2860
|
-
* @example
|
|
2861
|
-
* ```tsx
|
|
2862
|
-
* <AuthProvider auth={auth}>
|
|
2863
|
-
* <PhoneOTPForm
|
|
2864
|
-
* callbackURL="/dashboard"
|
|
2865
|
-
* onSuccess={() => navigate("/dashboard")}
|
|
2866
|
-
* />
|
|
2867
|
-
* </AuthProvider>
|
|
2868
|
-
* ```
|
|
2869
|
-
*/
|
|
2870
|
-
function PhoneOTPForm({ callbackURL, className, title, description, logo, onSuccess }) {
|
|
2871
|
-
const { t: t$1 } = useTranslation$1(["ui"]);
|
|
2872
|
-
const { client, config, features } = useAuth();
|
|
2873
|
-
const { isLoading, error, execute, setError } = useAuthForm();
|
|
2874
|
-
const [stage, setStage] = useState("phone");
|
|
2875
|
-
const [pendingPhone, setPendingPhone] = useState(null);
|
|
2876
|
-
const [countdown, setCountdown] = useState(0);
|
|
2877
|
-
const countdownRef = useRef(null);
|
|
2878
|
-
const otpLength = config.methods?.phoneOtp?.otpLength ?? 6;
|
|
2879
|
-
useEffect(() => {
|
|
2880
|
-
return () => {
|
|
2881
|
-
if (countdownRef.current) clearInterval(countdownRef.current);
|
|
2882
|
-
};
|
|
2883
|
-
}, []);
|
|
2884
|
-
const phoneForm = useForm({
|
|
2885
|
-
resolver: zodResolver(definePhoneNumberSchema()),
|
|
2886
|
-
defaultValues: { phoneNumber: "" },
|
|
2887
|
-
mode: "onBlur"
|
|
2888
|
-
});
|
|
2889
|
-
const otpForm = useForm({
|
|
2890
|
-
resolver: zodResolver(defineOtpSchema(otpLength)),
|
|
2891
|
-
defaultValues: { code: "" },
|
|
2892
|
-
mode: "onBlur"
|
|
2893
|
-
});
|
|
2894
|
-
if (!features.phoneOtp) return null;
|
|
2895
|
-
/**
|
|
2896
|
-
* Starts a 30-second countdown timer for the resend button.
|
|
2897
|
-
* Clears any existing timer before starting a new one.
|
|
2898
|
-
*/
|
|
2899
|
-
const startCountdown = () => {
|
|
2900
|
-
if (countdownRef.current) clearInterval(countdownRef.current);
|
|
2901
|
-
setCountdown(30);
|
|
2902
|
-
countdownRef.current = setInterval(() => {
|
|
2903
|
-
setCountdown((c) => {
|
|
2904
|
-
if (c <= 1) {
|
|
2905
|
-
if (countdownRef.current) clearInterval(countdownRef.current);
|
|
2906
|
-
return 0;
|
|
2907
|
-
}
|
|
2908
|
-
return c - 1;
|
|
2909
|
-
});
|
|
2910
|
-
}, 1e3);
|
|
2911
|
-
};
|
|
2912
|
-
const handlePhoneSubmit = async (data) => {
|
|
2913
|
-
await execute(async () => {
|
|
2914
|
-
const result = await client.phoneNumber.sendOtp({ phoneNumber: data.phoneNumber });
|
|
2915
|
-
if (result.error) throw new Error(result.error.message || t$1("ui:auth.errors.generic"));
|
|
2916
|
-
setPendingPhone(data.phoneNumber);
|
|
2917
|
-
setStage("verify");
|
|
2918
|
-
startCountdown();
|
|
2919
|
-
});
|
|
2920
|
-
};
|
|
2921
|
-
const handleOtpSubmit = async (data) => {
|
|
2922
|
-
if (!pendingPhone) return;
|
|
2923
|
-
if (await execute(async () => {
|
|
2924
|
-
const response = await client.phoneNumber.verify({
|
|
2925
|
-
phoneNumber: pendingPhone,
|
|
2926
|
-
code: data.code
|
|
2927
|
-
});
|
|
2928
|
-
if (response.error) throw new Error(response.error.message || t$1("ui:auth.otp.invalid"));
|
|
2929
|
-
return response;
|
|
2930
|
-
})) onSuccess?.();
|
|
2931
|
-
};
|
|
2932
|
-
const handleResend = async () => {
|
|
2933
|
-
if (!pendingPhone) return;
|
|
2934
|
-
if (await execute(async () => {
|
|
2935
|
-
const response = await client.phoneNumber.sendOtp({ phoneNumber: pendingPhone });
|
|
2936
|
-
if (response.error) throw new Error(response.error.message || t$1("ui:auth.errors.generic"));
|
|
2937
|
-
return response;
|
|
2938
|
-
})) startCountdown();
|
|
2939
|
-
};
|
|
2940
|
-
const handleBack = () => {
|
|
2941
|
-
setError(null);
|
|
2942
|
-
setStage("phone");
|
|
2943
|
-
otpForm.reset();
|
|
3572
|
+
return response;
|
|
3573
|
+
});
|
|
3574
|
+
if (!result) {
|
|
3575
|
+
setCaptchaToken(null);
|
|
3576
|
+
captchaResetRef.current?.();
|
|
3577
|
+
}
|
|
3578
|
+
if (result) {
|
|
3579
|
+
if (!result.data?.twoFactorRedirect) onSuccess?.();
|
|
3580
|
+
}
|
|
2944
3581
|
};
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
3582
|
+
const footer = signupLink && /* @__PURE__ */ jsxs("span", { children: [
|
|
3583
|
+
t$1("ui:auth.login.noAccount"),
|
|
3584
|
+
" ",
|
|
3585
|
+
/* @__PURE__ */ jsx(Link$1, {
|
|
3586
|
+
to: signupLink,
|
|
3587
|
+
className: "font-medium text-primary transition-colors hover:text-primary/80 hover:underline",
|
|
3588
|
+
children: t$1("ui:auth.login.signUp")
|
|
3589
|
+
})
|
|
3590
|
+
] });
|
|
3591
|
+
if (twoFactorPending) return /* @__PURE__ */ jsx(TwoFactorForm, {
|
|
3592
|
+
className,
|
|
3593
|
+
logo,
|
|
3594
|
+
onSuccess,
|
|
3595
|
+
onBack: () => setTwoFactorPending(false)
|
|
3596
|
+
});
|
|
3597
|
+
if (!hasAnyAuthMethod) return /* @__PURE__ */ jsx(AuthCard, {
|
|
3598
|
+
title: title || t$1("ui:auth.login.title"),
|
|
3599
|
+
description,
|
|
3600
|
+
footer,
|
|
2948
3601
|
logo,
|
|
3602
|
+
showSecurityIcon,
|
|
2949
3603
|
className,
|
|
2950
|
-
children: /* @__PURE__ */ jsx(
|
|
2951
|
-
|
|
2952
|
-
children: /* @__PURE__ */ jsxs(
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
/* @__PURE__ */ jsx(OtpInput, {
|
|
2961
|
-
name: "code",
|
|
2962
|
-
label: t$1("ui:auth.otp.code"),
|
|
2963
|
-
length: otpLength,
|
|
2964
|
-
hideLabel: true,
|
|
2965
|
-
showSeparator: true,
|
|
2966
|
-
onComplete: () => otpForm.handleSubmit(handleOtpSubmit)()
|
|
2967
|
-
}),
|
|
2968
|
-
/* @__PURE__ */ jsx(Button, {
|
|
2969
|
-
type: "submit",
|
|
2970
|
-
className: "h-10 w-full font-medium",
|
|
2971
|
-
disabled: isLoading,
|
|
2972
|
-
children: isLoading ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Spinner, { className: "mr-2 size-4" }), t$1("ui:auth.otp.verifying")] }) : t$1("ui:auth.otp.verify")
|
|
2973
|
-
}),
|
|
2974
|
-
/* @__PURE__ */ jsxs("div", {
|
|
2975
|
-
className: "flex gap-2",
|
|
2976
|
-
children: [/* @__PURE__ */ jsxs(Button, {
|
|
2977
|
-
type: "button",
|
|
2978
|
-
variant: "ghost",
|
|
2979
|
-
className: "flex-1 text-muted-foreground",
|
|
2980
|
-
onClick: handleBack,
|
|
2981
|
-
children: [/* @__PURE__ */ jsx(ArrowLeftIcon, { className: "mr-2 size-4" }), t$1("ui:auth.common.backButton")]
|
|
2982
|
-
}), /* @__PURE__ */ jsx(Button, {
|
|
2983
|
-
type: "button",
|
|
2984
|
-
variant: "ghost",
|
|
2985
|
-
className: "flex-1 text-muted-foreground",
|
|
2986
|
-
onClick: handleResend,
|
|
2987
|
-
disabled: isLoading || countdown > 0,
|
|
2988
|
-
children: countdown > 0 ? t$1("ui:auth.otp.resendIn", { seconds: countdown }) : t$1("ui:auth.phoneOtp.resend")
|
|
2989
|
-
})]
|
|
2990
|
-
})
|
|
2991
|
-
]
|
|
2992
|
-
})
|
|
3604
|
+
children: /* @__PURE__ */ jsx(Empty, {
|
|
3605
|
+
className: "border-none p-6",
|
|
3606
|
+
children: /* @__PURE__ */ jsxs(EmptyHeader, { children: [
|
|
3607
|
+
/* @__PURE__ */ jsx(EmptyMedia, {
|
|
3608
|
+
variant: "icon",
|
|
3609
|
+
children: /* @__PURE__ */ jsx(SettingsIcon, {})
|
|
3610
|
+
}),
|
|
3611
|
+
/* @__PURE__ */ jsx(EmptyTitle, { children: t$1("ui:auth.common.noMethodsConfigured") }),
|
|
3612
|
+
/* @__PURE__ */ jsx(EmptyDescription, { children: t$1("ui:auth.common.noMethodsDescription") })
|
|
3613
|
+
] })
|
|
2993
3614
|
})
|
|
2994
3615
|
});
|
|
2995
3616
|
return /* @__PURE__ */ jsx(AuthCard, {
|
|
2996
|
-
title: title || t$1("ui:auth.
|
|
2997
|
-
description,
|
|
3617
|
+
title: title || t$1("ui:auth.login.title"),
|
|
3618
|
+
description: description || t$1("ui:auth.login.description"),
|
|
3619
|
+
footer,
|
|
2998
3620
|
logo,
|
|
3621
|
+
showSecurityIcon,
|
|
2999
3622
|
className,
|
|
3000
3623
|
children: /* @__PURE__ */ jsx(FormProvider, {
|
|
3001
|
-
...
|
|
3624
|
+
...form,
|
|
3002
3625
|
children: /* @__PURE__ */ jsxs("form", {
|
|
3003
|
-
onSubmit:
|
|
3626
|
+
onSubmit: form.handleSubmit(onSubmit),
|
|
3004
3627
|
className: "space-y-4",
|
|
3005
3628
|
children: [
|
|
3006
|
-
error && /* @__PURE__ */ jsxs(Alert, {
|
|
3629
|
+
error && !emailVerificationRequired && /* @__PURE__ */ jsxs(Alert, {
|
|
3007
3630
|
variant: "destructive",
|
|
3008
3631
|
children: [/* @__PURE__ */ jsx(AlertCircleIcon, { className: "size-4" }), /* @__PURE__ */ jsx(AlertDescription, { children: error })]
|
|
3009
3632
|
}),
|
|
3010
|
-
/* @__PURE__ */
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3633
|
+
emailVerificationRequired && (resendSuccess ? /* @__PURE__ */ jsxs(Alert, {
|
|
3634
|
+
variant: "success",
|
|
3635
|
+
children: [/* @__PURE__ */ jsx(CheckCircleIcon, { className: "size-4" }), /* @__PURE__ */ jsx(AlertDescription, { children: t$1("ui:auth.verifyEmail.resendSuccess") })]
|
|
3636
|
+
}) : /* @__PURE__ */ jsxs(Alert, {
|
|
3637
|
+
variant: "warning",
|
|
3638
|
+
children: [
|
|
3639
|
+
/* @__PURE__ */ jsx(MailIcon, { className: "size-4" }),
|
|
3640
|
+
/* @__PURE__ */ jsx(AlertDescription, { children: t$1("ui:auth.login.emailNotVerifiedDescription") }),
|
|
3641
|
+
/* @__PURE__ */ jsx("div", {
|
|
3642
|
+
className: "col-start-2 flex justify-end pt-2",
|
|
3643
|
+
children: /* @__PURE__ */ jsxs(Button, {
|
|
3644
|
+
type: "button",
|
|
3645
|
+
size: "sm",
|
|
3646
|
+
className: "h-7 bg-amber-900 text-white hover:bg-amber-800 dark:bg-amber-100 dark:text-amber-900 dark:hover:bg-amber-200",
|
|
3647
|
+
onClick: handleResendVerification,
|
|
3648
|
+
disabled: isLoading,
|
|
3649
|
+
children: [isLoading && /* @__PURE__ */ jsx(Spinner, { className: "size-3" }), t$1("ui:auth.verifyEmail.resend")]
|
|
3650
|
+
})
|
|
3651
|
+
})
|
|
3652
|
+
]
|
|
3653
|
+
})),
|
|
3654
|
+
hasPasswordlessOnly && /* @__PURE__ */ jsxs("div", {
|
|
3655
|
+
className: "space-y-3",
|
|
3656
|
+
children: [
|
|
3657
|
+
/* @__PURE__ */ jsx(PasskeyButton, {
|
|
3658
|
+
disabled: isLoading,
|
|
3659
|
+
onSuccess
|
|
3660
|
+
}),
|
|
3661
|
+
/* @__PURE__ */ jsx(SocialButtons, {
|
|
3662
|
+
callbackURL,
|
|
3663
|
+
disabled: isLoading
|
|
3664
|
+
}),
|
|
3665
|
+
/* @__PURE__ */ jsx(SSOButton, {
|
|
3666
|
+
email,
|
|
3667
|
+
callbackURL,
|
|
3668
|
+
disabled: isLoading
|
|
3669
|
+
}),
|
|
3670
|
+
features.phoneOtp && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(FieldSeparator, { children: t$1("ui:auth.login.orSignInWith") }), /* @__PURE__ */ jsx(PhoneOTPForm, {
|
|
3671
|
+
callbackURL,
|
|
3672
|
+
onSuccess
|
|
3673
|
+
})] }),
|
|
3674
|
+
features.emailOtp && /* @__PURE__ */ jsxs(Fragment, { children: [!features.phoneOtp && /* @__PURE__ */ jsx(FieldSeparator, { children: t$1("ui:auth.login.orSignInWith") }), /* @__PURE__ */ jsx(EmailOTPForm, {
|
|
3675
|
+
callbackURL,
|
|
3676
|
+
onSuccess
|
|
3677
|
+
})] })
|
|
3678
|
+
]
|
|
3014
3679
|
}),
|
|
3015
|
-
/* @__PURE__ */
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3680
|
+
features.emailPassword && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
3681
|
+
(features.passkey || Object.values(features.oauth).some(Boolean)) && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
|
|
3682
|
+
className: "space-y-3",
|
|
3683
|
+
children: [/* @__PURE__ */ jsx(PasskeyButton, {
|
|
3684
|
+
disabled: isLoading,
|
|
3685
|
+
onSuccess
|
|
3686
|
+
}), /* @__PURE__ */ jsx(SocialButtons, {
|
|
3687
|
+
callbackURL,
|
|
3688
|
+
disabled: isLoading
|
|
3689
|
+
})]
|
|
3690
|
+
}), /* @__PURE__ */ jsx(FieldSeparator, { children: t$1("ui:auth.login.orContinueWith") })] }),
|
|
3691
|
+
/* @__PURE__ */ jsx(EmailInput, {
|
|
3692
|
+
name: "email",
|
|
3693
|
+
label: t$1("ui:auth.login.email"),
|
|
3694
|
+
showIcon: true
|
|
3695
|
+
}),
|
|
3696
|
+
/* @__PURE__ */ jsx(PasswordInput, {
|
|
3697
|
+
name: "password",
|
|
3698
|
+
label: t$1("ui:auth.login.password"),
|
|
3699
|
+
showIcon: true
|
|
3700
|
+
}),
|
|
3701
|
+
/* @__PURE__ */ jsxs("div", {
|
|
3702
|
+
className: "flex items-center justify-between",
|
|
3703
|
+
children: [showRememberMe && /* @__PURE__ */ jsxs(Field, {
|
|
3704
|
+
orientation: "horizontal",
|
|
3705
|
+
className: "w-auto",
|
|
3706
|
+
children: [/* @__PURE__ */ jsx(Checkbox, {
|
|
3707
|
+
id: "rememberMe",
|
|
3708
|
+
...form.register("rememberMe")
|
|
3709
|
+
}), /* @__PURE__ */ jsx(FieldLabel, {
|
|
3710
|
+
htmlFor: "rememberMe",
|
|
3711
|
+
className: "text-sm font-normal",
|
|
3712
|
+
children: t$1("ui:auth.login.rememberMe")
|
|
3713
|
+
})]
|
|
3714
|
+
}), forgotPasswordLink && /* @__PURE__ */ jsx(Link$1, {
|
|
3715
|
+
to: forgotPasswordLink,
|
|
3716
|
+
className: "text-sm text-muted-foreground transition-colors hover:text-primary",
|
|
3717
|
+
children: t$1("ui:auth.login.forgotPassword")
|
|
3718
|
+
})]
|
|
3719
|
+
}),
|
|
3720
|
+
captcha.enabled && /* @__PURE__ */ jsx(CaptchaWidget, {
|
|
3721
|
+
onSuccess: setCaptchaToken,
|
|
3722
|
+
onExpire: () => setCaptchaToken(null),
|
|
3723
|
+
onError: () => setCaptchaToken(null),
|
|
3724
|
+
resetRef: captchaResetRef
|
|
3725
|
+
}),
|
|
3726
|
+
/* @__PURE__ */ jsx(Button, {
|
|
3727
|
+
type: "submit",
|
|
3728
|
+
className: "h-10 w-full font-medium",
|
|
3729
|
+
disabled: isLoading || captcha.enabled && !captchaToken,
|
|
3730
|
+
children: isLoading ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Spinner, { className: "mr-2 size-4" }), t$1("ui:auth.login.submitting")] }) : t$1("ui:auth.login.submit")
|
|
3731
|
+
}),
|
|
3732
|
+
features.sso && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(FieldSeparator, { children: t$1("ui:auth.common.or") }), /* @__PURE__ */ jsx(SSOButton, {
|
|
3733
|
+
email,
|
|
3734
|
+
callbackURL,
|
|
3735
|
+
disabled: isLoading
|
|
3736
|
+
})] })
|
|
3737
|
+
] }),
|
|
3738
|
+
features.magicLink && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(FieldSeparator, { children: t$1("ui:auth.login.orSignInWith") }), /* @__PURE__ */ jsx(MagicLinkForm, { callbackURL })] })
|
|
3021
3739
|
]
|
|
3022
3740
|
})
|
|
3023
3741
|
})
|
|
@@ -3232,25 +3950,33 @@ function ResetPasswordForm({ token, className, loginLink = "/login", forgotPassw
|
|
|
3232
3950
|
//#region src/web/ui/auth/forms/signup-form.tsx
|
|
3233
3951
|
/**
|
|
3234
3952
|
* Creates the signup form schema with i18n-aware error messages.
|
|
3235
|
-
* Accepts optional password config for customizing validation rules.
|
|
3953
|
+
* Accepts optional password and username config for customizing validation rules.
|
|
3236
3954
|
*
|
|
3237
|
-
* @param
|
|
3955
|
+
* @param options - Schema configuration options.
|
|
3238
3956
|
* @returns A Zod schema for validating signup form data.
|
|
3239
3957
|
*
|
|
3240
3958
|
* @example
|
|
3241
3959
|
* ```typescript
|
|
3242
3960
|
* const config = useAuthConfig();
|
|
3243
|
-
* const schema = defineSignupSchema(
|
|
3961
|
+
* const schema = defineSignupSchema({
|
|
3962
|
+
* passwordConfig: config.methods?.emailPassword,
|
|
3963
|
+
* usernameConfig: config.plugins?.username,
|
|
3964
|
+
* usernameEnabled: features.username,
|
|
3965
|
+
* });
|
|
3244
3966
|
* ```
|
|
3245
3967
|
*/
|
|
3246
|
-
function defineSignupSchema(
|
|
3247
|
-
const minPassword = passwordConfig?.minPasswordLength ?? 8;
|
|
3248
|
-
const maxPassword = passwordConfig?.maxPasswordLength ?? 128;
|
|
3249
|
-
|
|
3968
|
+
function defineSignupSchema(options) {
|
|
3969
|
+
const minPassword = options?.passwordConfig?.minPasswordLength ?? 8;
|
|
3970
|
+
const maxPassword = options?.passwordConfig?.maxPasswordLength ?? 128;
|
|
3971
|
+
const minUsername = options?.usernameConfig?.minUsernameLength ?? 3;
|
|
3972
|
+
const maxUsername = options?.usernameConfig?.maxUsernameLength ?? 32;
|
|
3973
|
+
const baseSchema = z.object({
|
|
3250
3974
|
name: z.string().min(1, i18n.t("ui:validation.nameRequired")).max(100, i18n.t("ui:validation.nameMax", { max: 100 })),
|
|
3251
3975
|
email: z.email(i18n.t("ui:validation.emailInvalid")).min(1, i18n.t("ui:validation.emailRequired")),
|
|
3252
3976
|
password: z.string().min(minPassword, i18n.t("ui:validation.passwordMin", { min: minPassword })).max(maxPassword, i18n.t("ui:validation.passwordMax", { max: maxPassword }))
|
|
3253
3977
|
});
|
|
3978
|
+
if (options?.usernameEnabled) return baseSchema.extend({ username: z.string().min(minUsername, i18n.t("ui:validation.usernameMin", { min: minUsername })).max(maxUsername, i18n.t("ui:validation.usernameMax", { max: maxUsername })).regex(/^[a-zA-Z0-9_-]+$/, i18n.t("ui:validation.usernameFormat")) });
|
|
3979
|
+
return baseSchema;
|
|
3254
3980
|
}
|
|
3255
3981
|
function NameInput({ showIcon = false }) {
|
|
3256
3982
|
const id = useId();
|
|
@@ -3287,6 +4013,41 @@ function NameInput({ showIcon = false }) {
|
|
|
3287
4013
|
]
|
|
3288
4014
|
});
|
|
3289
4015
|
}
|
|
4016
|
+
function UsernameInput({ showIcon = false }) {
|
|
4017
|
+
const id = useId();
|
|
4018
|
+
const { t: t$1 } = useTranslation$1(["ui"]);
|
|
4019
|
+
const form = useFormContext();
|
|
4020
|
+
const error = form?.formState?.errors?.username;
|
|
4021
|
+
const hasError = !!error;
|
|
4022
|
+
return /* @__PURE__ */ jsxs(Field, {
|
|
4023
|
+
"data-invalid": hasError || void 0,
|
|
4024
|
+
children: [
|
|
4025
|
+
/* @__PURE__ */ jsx(FieldLabel, {
|
|
4026
|
+
htmlFor: id,
|
|
4027
|
+
children: t$1("ui:auth.signup.username")
|
|
4028
|
+
}),
|
|
4029
|
+
/* @__PURE__ */ jsxs("div", {
|
|
4030
|
+
className: "relative",
|
|
4031
|
+
children: [showIcon && /* @__PURE__ */ jsx("div", {
|
|
4032
|
+
className: "pointer-events-none absolute left-3 top-1/2 -translate-y-1/2",
|
|
4033
|
+
children: /* @__PURE__ */ jsx(AtSignIcon, { className: "size-4 text-muted-foreground" })
|
|
4034
|
+
}), /* @__PURE__ */ jsx(Input, {
|
|
4035
|
+
id,
|
|
4036
|
+
type: "text",
|
|
4037
|
+
autoComplete: "username",
|
|
4038
|
+
"aria-invalid": hasError,
|
|
4039
|
+
"aria-describedby": hasError ? `${id}-error` : void 0,
|
|
4040
|
+
className: cn(showIcon && "pl-10", "h-10"),
|
|
4041
|
+
...form?.register("username")
|
|
4042
|
+
})]
|
|
4043
|
+
}),
|
|
4044
|
+
error?.message && /* @__PURE__ */ jsx(FieldError, {
|
|
4045
|
+
id: `${id}-error`,
|
|
4046
|
+
children: String(error.message)
|
|
4047
|
+
})
|
|
4048
|
+
]
|
|
4049
|
+
});
|
|
4050
|
+
}
|
|
3290
4051
|
/**
|
|
3291
4052
|
* Complete signup form with password strength indicator.
|
|
3292
4053
|
* Renders based on enabled features in AuthFeatures.
|
|
@@ -3302,32 +4063,48 @@ function NameInput({ showIcon = false }) {
|
|
|
3302
4063
|
* </AuthProvider>
|
|
3303
4064
|
* ```
|
|
3304
4065
|
*/
|
|
3305
|
-
function SignupForm({ callbackURL, className, loginLink = "/login", title, description, logo, showSecurityIcon, showTermsCheckbox = false, termsUrl = "/terms", privacyUrl = "/privacy", onSuccess }) {
|
|
4066
|
+
function SignupForm({ callbackURL, className, loginLink = "/login", title, description, logo, showSecurityIcon, showTermsCheckbox = false, termsUrl = "/legal/terms", privacyUrl = "/legal/privacy-policy", onSuccess }) {
|
|
3306
4067
|
const { t: t$1 } = useTranslation$1(["ui"]);
|
|
3307
4068
|
const { client, config, features } = useAuth();
|
|
3308
4069
|
const { isLoading, error, execute } = useAuthForm();
|
|
4070
|
+
const captcha = useCaptcha();
|
|
4071
|
+
const [captchaToken, setCaptchaToken] = useState(null);
|
|
4072
|
+
const captchaResetRef = useRef(null);
|
|
3309
4073
|
const form = useForm({
|
|
3310
|
-
resolver: zodResolver(defineSignupSchema(
|
|
4074
|
+
resolver: zodResolver(defineSignupSchema({
|
|
4075
|
+
passwordConfig: config.methods?.emailPassword,
|
|
4076
|
+
usernameConfig: config.plugins?.username,
|
|
4077
|
+
usernameEnabled: features.username
|
|
4078
|
+
})),
|
|
3311
4079
|
defaultValues: {
|
|
3312
4080
|
name: "",
|
|
3313
4081
|
email: "",
|
|
3314
|
-
password: ""
|
|
4082
|
+
password: "",
|
|
4083
|
+
...features.username && { username: "" }
|
|
3315
4084
|
},
|
|
3316
4085
|
mode: "onBlur"
|
|
3317
4086
|
});
|
|
3318
4087
|
const hasOAuth = Object.values(features.oauth).some(Boolean);
|
|
3319
4088
|
const hasAnyAuthMethod = features.emailPassword || hasOAuth;
|
|
3320
4089
|
const onSubmit = async (data) => {
|
|
3321
|
-
|
|
3322
|
-
const
|
|
4090
|
+
const result = await execute(async () => {
|
|
4091
|
+
const signUpData = {
|
|
3323
4092
|
name: data.name,
|
|
3324
4093
|
email: data.email,
|
|
3325
4094
|
password: data.password,
|
|
3326
|
-
callbackURL
|
|
3327
|
-
|
|
4095
|
+
callbackURL,
|
|
4096
|
+
fetchOptions: captchaToken ? { headers: { "x-captcha-response": captchaToken } } : void 0
|
|
4097
|
+
};
|
|
4098
|
+
if (features.username && "username" in data) signUpData.username = data.username;
|
|
4099
|
+
const response = await client.signUp.email(signUpData);
|
|
3328
4100
|
if (response.error) throw new Error(response.error.message || t$1("ui:auth.common.signupFailed"));
|
|
3329
4101
|
return response;
|
|
3330
|
-
})
|
|
4102
|
+
});
|
|
4103
|
+
if (!result) {
|
|
4104
|
+
setCaptchaToken(null);
|
|
4105
|
+
captchaResetRef.current?.();
|
|
4106
|
+
}
|
|
4107
|
+
if (result) onSuccess?.({ email: data.email });
|
|
3331
4108
|
};
|
|
3332
4109
|
const footer = loginLink && /* @__PURE__ */ jsxs("span", { children: [
|
|
3333
4110
|
t$1("ui:auth.signup.hasAccount"),
|
|
@@ -3380,6 +4157,7 @@ function SignupForm({ callbackURL, className, loginLink = "/login", title, descr
|
|
|
3380
4157
|
}), features.emailPassword && /* @__PURE__ */ jsx(FieldSeparator, { children: t$1("ui:auth.signup.orContinueWith") })] }),
|
|
3381
4158
|
features.emailPassword && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
3382
4159
|
/* @__PURE__ */ jsx(NameInput, { showIcon: true }),
|
|
4160
|
+
features.username && /* @__PURE__ */ jsx(UsernameInput, { showIcon: true }),
|
|
3383
4161
|
/* @__PURE__ */ jsx(EmailInput, {
|
|
3384
4162
|
name: "email",
|
|
3385
4163
|
label: t$1("ui:auth.signup.email"),
|
|
@@ -3423,10 +4201,16 @@ function SignupForm({ callbackURL, className, loginLink = "/login", title, descr
|
|
|
3423
4201
|
]
|
|
3424
4202
|
})]
|
|
3425
4203
|
}),
|
|
4204
|
+
captcha.enabled && /* @__PURE__ */ jsx(CaptchaWidget, {
|
|
4205
|
+
onSuccess: setCaptchaToken,
|
|
4206
|
+
onExpire: () => setCaptchaToken(null),
|
|
4207
|
+
onError: () => setCaptchaToken(null),
|
|
4208
|
+
resetRef: captchaResetRef
|
|
4209
|
+
}),
|
|
3426
4210
|
/* @__PURE__ */ jsx(Button, {
|
|
3427
4211
|
type: "submit",
|
|
3428
4212
|
className: "h-10 w-full font-medium",
|
|
3429
|
-
disabled: isLoading,
|
|
4213
|
+
disabled: isLoading || captcha.enabled && !captchaToken,
|
|
3430
4214
|
children: isLoading ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Spinner, { className: "mr-2 size-4" }), t$1("ui:auth.signup.submitting")] }) : t$1("ui:auth.signup.submit")
|
|
3431
4215
|
})
|
|
3432
4216
|
] })
|
|
@@ -3457,6 +4241,9 @@ function SignupForm({ callbackURL, className, loginLink = "/login", title, descr
|
|
|
3457
4241
|
function VerifyEmailForm({ token, email, callbackURL, className, loginLink = "/login", title, description, logo, onSuccess }) {
|
|
3458
4242
|
const { t: t$1 } = useTranslation$1(["ui"]);
|
|
3459
4243
|
const { client } = useAuth();
|
|
4244
|
+
const captcha = useCaptcha();
|
|
4245
|
+
const [captchaToken, setCaptchaToken] = useState(null);
|
|
4246
|
+
const captchaResetRef = useRef(null);
|
|
3460
4247
|
const { isLoading, error, isSuccess, execute, setError } = useAuthForm();
|
|
3461
4248
|
const [resendSuccess, setResendSuccess] = useState(false);
|
|
3462
4249
|
const [countdown, setCountdown] = useState(0);
|
|
@@ -3507,14 +4294,20 @@ function VerifyEmailForm({ token, email, callbackURL, className, loginLink = "/l
|
|
|
3507
4294
|
return;
|
|
3508
4295
|
}
|
|
3509
4296
|
setResendSuccess(false);
|
|
3510
|
-
|
|
4297
|
+
const result = await execute(async () => {
|
|
3511
4298
|
const response = await client.sendVerificationEmail({
|
|
3512
4299
|
email,
|
|
3513
|
-
callbackURL
|
|
4300
|
+
callbackURL,
|
|
4301
|
+
fetchOptions: captchaToken ? { headers: { "x-captcha-response": captchaToken } } : void 0
|
|
3514
4302
|
});
|
|
3515
4303
|
if (response.error) throw new Error(response.error.message || t$1("ui:auth.errors.generic"));
|
|
3516
4304
|
return response;
|
|
3517
|
-
})
|
|
4305
|
+
});
|
|
4306
|
+
if (!result) {
|
|
4307
|
+
setCaptchaToken(null);
|
|
4308
|
+
captchaResetRef.current?.();
|
|
4309
|
+
}
|
|
4310
|
+
if (result) {
|
|
3518
4311
|
setResendSuccess(true);
|
|
3519
4312
|
startCountdown();
|
|
3520
4313
|
}
|
|
@@ -3588,10 +4381,16 @@ function VerifyEmailForm({ token, email, callbackURL, className, loginLink = "/l
|
|
|
3588
4381
|
variant: "destructive",
|
|
3589
4382
|
children: [/* @__PURE__ */ jsx(AlertCircleIcon, { className: "size-4" }), /* @__PURE__ */ jsx(AlertDescription, { children: error })]
|
|
3590
4383
|
}),
|
|
4384
|
+
email && captcha.enabled && /* @__PURE__ */ jsx(CaptchaWidget, {
|
|
4385
|
+
onSuccess: setCaptchaToken,
|
|
4386
|
+
onExpire: () => setCaptchaToken(null),
|
|
4387
|
+
onError: () => setCaptchaToken(null),
|
|
4388
|
+
resetRef: captchaResetRef
|
|
4389
|
+
}),
|
|
3591
4390
|
email && /* @__PURE__ */ jsxs(Button, {
|
|
3592
4391
|
className: "h-10 w-full font-medium",
|
|
3593
4392
|
onClick: handleResend,
|
|
3594
|
-
disabled: isLoading || countdown > 0,
|
|
4393
|
+
disabled: isLoading || countdown > 0 || captcha.enabled && !captchaToken,
|
|
3595
4394
|
children: [isLoading ? /* @__PURE__ */ jsx(Spinner, { className: "mr-2 size-4" }) : /* @__PURE__ */ jsx(RefreshCwIcon, { className: "mr-2 size-4" }), countdown > 0 ? t$1("ui:auth.otp.resendIn", { seconds: countdown }) : t$1("ui:auth.verifyEmail.resend")]
|
|
3596
4395
|
})
|
|
3597
4396
|
]
|
|
@@ -3629,10 +4428,16 @@ function VerifyEmailForm({ token, email, callbackURL, className, loginLink = "/l
|
|
|
3629
4428
|
variant: "success",
|
|
3630
4429
|
children: [/* @__PURE__ */ jsx(CheckCircleIcon, { className: "size-4" }), /* @__PURE__ */ jsx(AlertDescription, { children: t$1("ui:auth.verifyEmail.resendSuccess") })]
|
|
3631
4430
|
}),
|
|
4431
|
+
email && captcha.enabled && /* @__PURE__ */ jsx(CaptchaWidget, {
|
|
4432
|
+
onSuccess: setCaptchaToken,
|
|
4433
|
+
onExpire: () => setCaptchaToken(null),
|
|
4434
|
+
onError: () => setCaptchaToken(null),
|
|
4435
|
+
resetRef: captchaResetRef
|
|
4436
|
+
}),
|
|
3632
4437
|
email && /* @__PURE__ */ jsxs(Button, {
|
|
3633
4438
|
className: "h-10 w-full font-medium",
|
|
3634
4439
|
onClick: handleResend,
|
|
3635
|
-
disabled: isLoading || countdown > 0,
|
|
4440
|
+
disabled: isLoading || countdown > 0 || captcha.enabled && !captchaToken,
|
|
3636
4441
|
children: [isLoading ? /* @__PURE__ */ jsx(Spinner, { className: "mr-2 size-4" }) : /* @__PURE__ */ jsx(RefreshCwIcon, { className: "mr-2 size-4" }), countdown > 0 ? t$1("ui:auth.otp.resendIn", { seconds: countdown }) : t$1("ui:auth.verifyEmail.resend")]
|
|
3637
4442
|
})
|
|
3638
4443
|
]
|
|
@@ -3688,6 +4493,23 @@ function PasskeyRegisterButton({ className, disabled, label, onSuccess, onError
|
|
|
3688
4493
|
|
|
3689
4494
|
//#endregion
|
|
3690
4495
|
//#region src/web/ui/accordion.tsx
|
|
4496
|
+
/**
|
|
4497
|
+
* A vertically stacked set of interactive headings that reveal content sections.
|
|
4498
|
+
*
|
|
4499
|
+
* Use accordions to organize content into collapsible sections, reducing visual
|
|
4500
|
+
* clutter while keeping information accessible. Supports single or multiple
|
|
4501
|
+
* expanded items.
|
|
4502
|
+
*
|
|
4503
|
+
* @example
|
|
4504
|
+
* ```tsx
|
|
4505
|
+
* <Accordion>
|
|
4506
|
+
* <AccordionItem value="item-1">
|
|
4507
|
+
* <AccordionTrigger>Section Title</AccordionTrigger>
|
|
4508
|
+
* <AccordionContent>Section content goes here.</AccordionContent>
|
|
4509
|
+
* </AccordionItem>
|
|
4510
|
+
* </Accordion>
|
|
4511
|
+
* ```
|
|
4512
|
+
*/
|
|
3691
4513
|
function Accordion({ className, ...props }) {
|
|
3692
4514
|
return /* @__PURE__ */ jsx(Accordion$1.Root, {
|
|
3693
4515
|
"data-slot": "accordion",
|
|
@@ -3695,6 +4517,20 @@ function Accordion({ className, ...props }) {
|
|
|
3695
4517
|
...props
|
|
3696
4518
|
});
|
|
3697
4519
|
}
|
|
4520
|
+
/**
|
|
4521
|
+
* A single collapsible section within an Accordion.
|
|
4522
|
+
*
|
|
4523
|
+
* Each item contains a trigger and content panel. The `value` prop uniquely
|
|
4524
|
+
* identifies this item for controlled state management.
|
|
4525
|
+
*
|
|
4526
|
+
* @example
|
|
4527
|
+
* ```tsx
|
|
4528
|
+
* <AccordionItem value="faq-1">
|
|
4529
|
+
* <AccordionTrigger>What is this?</AccordionTrigger>
|
|
4530
|
+
* <AccordionContent>This is an accordion item.</AccordionContent>
|
|
4531
|
+
* </AccordionItem>
|
|
4532
|
+
* ```
|
|
4533
|
+
*/
|
|
3698
4534
|
function AccordionItem({ className, ...props }) {
|
|
3699
4535
|
return /* @__PURE__ */ jsx(Accordion$1.Item, {
|
|
3700
4536
|
"data-slot": "accordion-item",
|
|
@@ -3702,6 +4538,17 @@ function AccordionItem({ className, ...props }) {
|
|
|
3702
4538
|
...props
|
|
3703
4539
|
});
|
|
3704
4540
|
}
|
|
4541
|
+
/**
|
|
4542
|
+
* The clickable header that toggles the accordion content visibility.
|
|
4543
|
+
*
|
|
4544
|
+
* Includes an animated chevron icon that rotates based on the expanded state.
|
|
4545
|
+
* Supports keyboard navigation and focus management.
|
|
4546
|
+
*
|
|
4547
|
+
* @example
|
|
4548
|
+
* ```tsx
|
|
4549
|
+
* <AccordionTrigger>Click to expand</AccordionTrigger>
|
|
4550
|
+
* ```
|
|
4551
|
+
*/
|
|
3705
4552
|
function AccordionTrigger({ className, children, ...props }) {
|
|
3706
4553
|
return /* @__PURE__ */ jsx(Accordion$1.Header, {
|
|
3707
4554
|
className: "flex",
|
|
@@ -3723,6 +4570,19 @@ function AccordionTrigger({ className, children, ...props }) {
|
|
|
3723
4570
|
})
|
|
3724
4571
|
});
|
|
3725
4572
|
}
|
|
4573
|
+
/**
|
|
4574
|
+
* The collapsible content panel of an accordion item.
|
|
4575
|
+
*
|
|
4576
|
+
* Animates smoothly when expanding and collapsing. Content is hidden from
|
|
4577
|
+
* screen readers when collapsed.
|
|
4578
|
+
*
|
|
4579
|
+
* @example
|
|
4580
|
+
* ```tsx
|
|
4581
|
+
* <AccordionContent>
|
|
4582
|
+
* <p>This content is revealed when the accordion is expanded.</p>
|
|
4583
|
+
* </AccordionContent>
|
|
4584
|
+
* ```
|
|
4585
|
+
*/
|
|
3726
4586
|
function AccordionContent({ className, children, ...props }) {
|
|
3727
4587
|
return /* @__PURE__ */ jsx(Accordion$1.Panel, {
|
|
3728
4588
|
"data-slot": "accordion-content",
|
|
@@ -3737,24 +4597,72 @@ function AccordionContent({ className, children, ...props }) {
|
|
|
3737
4597
|
|
|
3738
4598
|
//#endregion
|
|
3739
4599
|
//#region src/web/ui/alert-dialog.tsx
|
|
4600
|
+
/**
|
|
4601
|
+
* A modal dialog that interrupts the user with important content and expects a response.
|
|
4602
|
+
*
|
|
4603
|
+
* Use for destructive actions or critical decisions that require explicit user
|
|
4604
|
+
* confirmation. Prevents interaction with the rest of the page until dismissed.
|
|
4605
|
+
*
|
|
4606
|
+
* @example
|
|
4607
|
+
* ```tsx
|
|
4608
|
+
* <AlertDialog>
|
|
4609
|
+
* <AlertDialogTrigger render={<Button variant="destructive" />}>
|
|
4610
|
+
* Delete Account
|
|
4611
|
+
* </AlertDialogTrigger>
|
|
4612
|
+
* <AlertDialogContent>
|
|
4613
|
+
* <AlertDialogHeader>
|
|
4614
|
+
* <AlertDialogTitle>Are you sure?</AlertDialogTitle>
|
|
4615
|
+
* <AlertDialogDescription>This action cannot be undone.</AlertDialogDescription>
|
|
4616
|
+
* </AlertDialogHeader>
|
|
4617
|
+
* <AlertDialogFooter>
|
|
4618
|
+
* <AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
4619
|
+
* <AlertDialogAction>Continue</AlertDialogAction>
|
|
4620
|
+
* </AlertDialogFooter>
|
|
4621
|
+
* </AlertDialogContent>
|
|
4622
|
+
* </AlertDialog>
|
|
4623
|
+
* ```
|
|
4624
|
+
*/
|
|
3740
4625
|
function AlertDialog({ ...props }) {
|
|
3741
4626
|
return /* @__PURE__ */ jsx(AlertDialog$1.Root, {
|
|
3742
4627
|
"data-slot": "alert-dialog",
|
|
3743
4628
|
...props
|
|
3744
4629
|
});
|
|
3745
4630
|
}
|
|
4631
|
+
/**
|
|
4632
|
+
* The button that opens the alert dialog.
|
|
4633
|
+
*
|
|
4634
|
+
* Use the `render` prop to customize the trigger element while maintaining
|
|
4635
|
+
* proper accessibility attributes.
|
|
4636
|
+
*
|
|
4637
|
+
* @example
|
|
4638
|
+
* ```tsx
|
|
4639
|
+
* <AlertDialogTrigger render={<Button />}>Open Dialog</AlertDialogTrigger>
|
|
4640
|
+
* ```
|
|
4641
|
+
*/
|
|
3746
4642
|
function AlertDialogTrigger({ ...props }) {
|
|
3747
4643
|
return /* @__PURE__ */ jsx(AlertDialog$1.Trigger, {
|
|
3748
4644
|
"data-slot": "alert-dialog-trigger",
|
|
3749
4645
|
...props
|
|
3750
4646
|
});
|
|
3751
4647
|
}
|
|
4648
|
+
/**
|
|
4649
|
+
* Portals the dialog content to the end of the document body.
|
|
4650
|
+
*
|
|
4651
|
+
* Used internally by AlertDialogContent. Ensures the dialog renders above
|
|
4652
|
+
* other content regardless of DOM position.
|
|
4653
|
+
*/
|
|
3752
4654
|
function AlertDialogPortal({ ...props }) {
|
|
3753
4655
|
return /* @__PURE__ */ jsx(AlertDialog$1.Portal, {
|
|
3754
4656
|
"data-slot": "alert-dialog-portal",
|
|
3755
4657
|
...props
|
|
3756
4658
|
});
|
|
3757
4659
|
}
|
|
4660
|
+
/**
|
|
4661
|
+
* The backdrop overlay behind the alert dialog.
|
|
4662
|
+
*
|
|
4663
|
+
* Dims the background and prevents interaction with the page. Includes
|
|
4664
|
+
* blur effect on supported browsers. Used internally by AlertDialogContent.
|
|
4665
|
+
*/
|
|
3758
4666
|
function AlertDialogOverlay({ className, ...props }) {
|
|
3759
4667
|
return /* @__PURE__ */ jsx(AlertDialog$1.Backdrop, {
|
|
3760
4668
|
"data-slot": "alert-dialog-overlay",
|
|
@@ -3762,6 +4670,20 @@ function AlertDialogOverlay({ className, ...props }) {
|
|
|
3762
4670
|
...props
|
|
3763
4671
|
});
|
|
3764
4672
|
}
|
|
4673
|
+
/**
|
|
4674
|
+
* The container for the alert dialog content.
|
|
4675
|
+
*
|
|
4676
|
+
* Centers the dialog on screen with smooth open/close animations. Includes
|
|
4677
|
+
* the portal and overlay automatically.
|
|
4678
|
+
*
|
|
4679
|
+
* @example
|
|
4680
|
+
* ```tsx
|
|
4681
|
+
* <AlertDialogContent size="sm">
|
|
4682
|
+
* <AlertDialogHeader>...</AlertDialogHeader>
|
|
4683
|
+
* <AlertDialogFooter>...</AlertDialogFooter>
|
|
4684
|
+
* </AlertDialogContent>
|
|
4685
|
+
* ```
|
|
4686
|
+
*/
|
|
3765
4687
|
function AlertDialogContent({ className, size = "default", ...props }) {
|
|
3766
4688
|
return /* @__PURE__ */ jsxs(AlertDialogPortal, { children: [/* @__PURE__ */ jsx(AlertDialogOverlay, {}), /* @__PURE__ */ jsx(AlertDialog$1.Popup, {
|
|
3767
4689
|
"data-slot": "alert-dialog-content",
|
|
@@ -3770,6 +4692,19 @@ function AlertDialogContent({ className, size = "default", ...props }) {
|
|
|
3770
4692
|
...props
|
|
3771
4693
|
})] });
|
|
3772
4694
|
}
|
|
4695
|
+
/**
|
|
4696
|
+
* The header section containing the dialog title and description.
|
|
4697
|
+
*
|
|
4698
|
+
* Handles layout for optional media icon and adapts to different dialog sizes.
|
|
4699
|
+
*
|
|
4700
|
+
* @example
|
|
4701
|
+
* ```tsx
|
|
4702
|
+
* <AlertDialogHeader>
|
|
4703
|
+
* <AlertDialogTitle>Confirm Action</AlertDialogTitle>
|
|
4704
|
+
* <AlertDialogDescription>This will affect your data.</AlertDialogDescription>
|
|
4705
|
+
* </AlertDialogHeader>
|
|
4706
|
+
* ```
|
|
4707
|
+
*/
|
|
3773
4708
|
function AlertDialogHeader({ className, ...props }) {
|
|
3774
4709
|
return /* @__PURE__ */ jsx("div", {
|
|
3775
4710
|
"data-slot": "alert-dialog-header",
|
|
@@ -3777,6 +4712,19 @@ function AlertDialogHeader({ className, ...props }) {
|
|
|
3777
4712
|
...props
|
|
3778
4713
|
});
|
|
3779
4714
|
}
|
|
4715
|
+
/**
|
|
4716
|
+
* The footer section containing dialog action buttons.
|
|
4717
|
+
*
|
|
4718
|
+
* Arranges buttons in a responsive layout with proper spacing.
|
|
4719
|
+
*
|
|
4720
|
+
* @example
|
|
4721
|
+
* ```tsx
|
|
4722
|
+
* <AlertDialogFooter>
|
|
4723
|
+
* <AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
4724
|
+
* <AlertDialogAction>Confirm</AlertDialogAction>
|
|
4725
|
+
* </AlertDialogFooter>
|
|
4726
|
+
* ```
|
|
4727
|
+
*/
|
|
3780
4728
|
function AlertDialogFooter({ className, ...props }) {
|
|
3781
4729
|
return /* @__PURE__ */ jsx("div", {
|
|
3782
4730
|
"data-slot": "alert-dialog-footer",
|
|
@@ -3784,6 +4732,18 @@ function AlertDialogFooter({ className, ...props }) {
|
|
|
3784
4732
|
...props
|
|
3785
4733
|
});
|
|
3786
4734
|
}
|
|
4735
|
+
/**
|
|
4736
|
+
* An optional media container for icons or images in the dialog header.
|
|
4737
|
+
*
|
|
4738
|
+
* Displays prominently above or beside the title depending on dialog size.
|
|
4739
|
+
*
|
|
4740
|
+
* @example
|
|
4741
|
+
* ```tsx
|
|
4742
|
+
* <AlertDialogMedia>
|
|
4743
|
+
* <AlertTriangleIcon />
|
|
4744
|
+
* </AlertDialogMedia>
|
|
4745
|
+
* ```
|
|
4746
|
+
*/
|
|
3787
4747
|
function AlertDialogMedia({ className, ...props }) {
|
|
3788
4748
|
return /* @__PURE__ */ jsx("div", {
|
|
3789
4749
|
"data-slot": "alert-dialog-media",
|
|
@@ -3791,6 +4751,16 @@ function AlertDialogMedia({ className, ...props }) {
|
|
|
3791
4751
|
...props
|
|
3792
4752
|
});
|
|
3793
4753
|
}
|
|
4754
|
+
/**
|
|
4755
|
+
* The title text for the alert dialog.
|
|
4756
|
+
*
|
|
4757
|
+
* Displayed prominently and announced by screen readers when the dialog opens.
|
|
4758
|
+
*
|
|
4759
|
+
* @example
|
|
4760
|
+
* ```tsx
|
|
4761
|
+
* <AlertDialogTitle>Delete Item</AlertDialogTitle>
|
|
4762
|
+
* ```
|
|
4763
|
+
*/
|
|
3794
4764
|
function AlertDialogTitle({ className, ...props }) {
|
|
3795
4765
|
return /* @__PURE__ */ jsx(AlertDialog$1.Title, {
|
|
3796
4766
|
"data-slot": "alert-dialog-title",
|
|
@@ -3798,13 +4768,35 @@ function AlertDialogTitle({ className, ...props }) {
|
|
|
3798
4768
|
...props
|
|
3799
4769
|
});
|
|
3800
4770
|
}
|
|
4771
|
+
/**
|
|
4772
|
+
* The descriptive text explaining the alert dialog purpose.
|
|
4773
|
+
*
|
|
4774
|
+
* Provides context about the action and its consequences.
|
|
4775
|
+
*
|
|
4776
|
+
* @example
|
|
4777
|
+
* ```tsx
|
|
4778
|
+
* <AlertDialogDescription>
|
|
4779
|
+
* This action cannot be undone. Your data will be permanently removed.
|
|
4780
|
+
* </AlertDialogDescription>
|
|
4781
|
+
* ```
|
|
4782
|
+
*/
|
|
3801
4783
|
function AlertDialogDescription({ className, ...props }) {
|
|
3802
4784
|
return /* @__PURE__ */ jsx(AlertDialog$1.Description, {
|
|
3803
4785
|
"data-slot": "alert-dialog-description",
|
|
3804
|
-
className: cn("text-
|
|
4786
|
+
className: cn("text-foreground/70 *:[a]:hover:text-foreground text-sm text-balance md:text-pretty *:[a]:underline *:[a]:underline-offset-3", className),
|
|
3805
4787
|
...props
|
|
3806
4788
|
});
|
|
3807
4789
|
}
|
|
4790
|
+
/**
|
|
4791
|
+
* The primary action button that confirms the dialog action.
|
|
4792
|
+
*
|
|
4793
|
+
* Styled as the default button variant. Does not automatically close the dialog.
|
|
4794
|
+
*
|
|
4795
|
+
* @example
|
|
4796
|
+
* ```tsx
|
|
4797
|
+
* <AlertDialogAction onClick={handleDelete}>Delete</AlertDialogAction>
|
|
4798
|
+
* ```
|
|
4799
|
+
*/
|
|
3808
4800
|
function AlertDialogAction({ className, ...props }) {
|
|
3809
4801
|
return /* @__PURE__ */ jsx(Button, {
|
|
3810
4802
|
"data-slot": "alert-dialog-action",
|
|
@@ -3812,6 +4804,16 @@ function AlertDialogAction({ className, ...props }) {
|
|
|
3812
4804
|
...props
|
|
3813
4805
|
});
|
|
3814
4806
|
}
|
|
4807
|
+
/**
|
|
4808
|
+
* The cancel button that dismisses the dialog without taking action.
|
|
4809
|
+
*
|
|
4810
|
+
* Automatically closes the dialog when clicked. Styled as outline variant by default.
|
|
4811
|
+
*
|
|
4812
|
+
* @example
|
|
4813
|
+
* ```tsx
|
|
4814
|
+
* <AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
4815
|
+
* ```
|
|
4816
|
+
*/
|
|
3815
4817
|
function AlertDialogCancel({ className, variant = "outline", size = "default", ...props }) {
|
|
3816
4818
|
return /* @__PURE__ */ jsx(AlertDialog$1.Close, {
|
|
3817
4819
|
"data-slot": "alert-dialog-cancel",
|
|
@@ -3826,6 +4828,19 @@ function AlertDialogCancel({ className, variant = "outline", size = "default", .
|
|
|
3826
4828
|
|
|
3827
4829
|
//#endregion
|
|
3828
4830
|
//#region src/web/ui/aspect-ratio.tsx
|
|
4831
|
+
/**
|
|
4832
|
+
* A container that maintains a specific aspect ratio for its content.
|
|
4833
|
+
*
|
|
4834
|
+
* Useful for images, videos, and embedded content that need to maintain
|
|
4835
|
+
* consistent proportions across different viewport sizes.
|
|
4836
|
+
*
|
|
4837
|
+
* @example
|
|
4838
|
+
* ```tsx
|
|
4839
|
+
* <AspectRatio ratio={16 / 9}>
|
|
4840
|
+
* <img src="/image.jpg" alt="Landscape" className="object-cover" />
|
|
4841
|
+
* </AspectRatio>
|
|
4842
|
+
* ```
|
|
4843
|
+
*/
|
|
3829
4844
|
function AspectRatio({ ratio, className, ...props }) {
|
|
3830
4845
|
return /* @__PURE__ */ jsx("div", {
|
|
3831
4846
|
"data-slot": "aspect-ratio",
|
|
@@ -3837,6 +4852,21 @@ function AspectRatio({ ratio, className, ...props }) {
|
|
|
3837
4852
|
|
|
3838
4853
|
//#endregion
|
|
3839
4854
|
//#region src/web/ui/avatar.tsx
|
|
4855
|
+
/**
|
|
4856
|
+
* A circular image element for representing users or entities.
|
|
4857
|
+
*
|
|
4858
|
+
* Supports multiple sizes and integrates with AvatarImage for the photo,
|
|
4859
|
+
* AvatarFallback for initials when no image is available, and AvatarBadge
|
|
4860
|
+
* for status indicators.
|
|
4861
|
+
*
|
|
4862
|
+
* @example
|
|
4863
|
+
* ```tsx
|
|
4864
|
+
* <Avatar>
|
|
4865
|
+
* <AvatarImage src="/user.jpg" alt="John Doe" />
|
|
4866
|
+
* <AvatarFallback>JD</AvatarFallback>
|
|
4867
|
+
* </Avatar>
|
|
4868
|
+
* ```
|
|
4869
|
+
*/
|
|
3840
4870
|
function Avatar({ className, size = "default", ...props }) {
|
|
3841
4871
|
return /* @__PURE__ */ jsx(Avatar$1.Root, {
|
|
3842
4872
|
"data-slot": "avatar",
|
|
@@ -3845,6 +4875,17 @@ function Avatar({ className, size = "default", ...props }) {
|
|
|
3845
4875
|
...props
|
|
3846
4876
|
});
|
|
3847
4877
|
}
|
|
4878
|
+
/**
|
|
4879
|
+
* The image element displayed within an Avatar.
|
|
4880
|
+
*
|
|
4881
|
+
* Automatically scales to fill the avatar container and handles image loading.
|
|
4882
|
+
* Falls back to AvatarFallback when the image fails to load.
|
|
4883
|
+
*
|
|
4884
|
+
* @example
|
|
4885
|
+
* ```tsx
|
|
4886
|
+
* <AvatarImage src="/profile.jpg" alt="User profile" />
|
|
4887
|
+
* ```
|
|
4888
|
+
*/
|
|
3848
4889
|
function AvatarImage({ className, ...props }) {
|
|
3849
4890
|
return /* @__PURE__ */ jsx(Avatar$1.Image, {
|
|
3850
4891
|
"data-slot": "avatar-image",
|
|
@@ -3852,6 +4893,17 @@ function AvatarImage({ className, ...props }) {
|
|
|
3852
4893
|
...props
|
|
3853
4894
|
});
|
|
3854
4895
|
}
|
|
4896
|
+
/**
|
|
4897
|
+
* A fallback element displayed when the Avatar image is unavailable.
|
|
4898
|
+
*
|
|
4899
|
+
* Typically contains user initials or a placeholder icon. Automatically shown
|
|
4900
|
+
* when AvatarImage fails to load or is not provided.
|
|
4901
|
+
*
|
|
4902
|
+
* @example
|
|
4903
|
+
* ```tsx
|
|
4904
|
+
* <AvatarFallback>JD</AvatarFallback>
|
|
4905
|
+
* ```
|
|
4906
|
+
*/
|
|
3855
4907
|
function AvatarFallback({ className, ...props }) {
|
|
3856
4908
|
return /* @__PURE__ */ jsx(Avatar$1.Fallback, {
|
|
3857
4909
|
"data-slot": "avatar-fallback",
|
|
@@ -3859,6 +4911,20 @@ function AvatarFallback({ className, ...props }) {
|
|
|
3859
4911
|
...props
|
|
3860
4912
|
});
|
|
3861
4913
|
}
|
|
4914
|
+
/**
|
|
4915
|
+
* A status indicator badge positioned at the corner of an Avatar.
|
|
4916
|
+
*
|
|
4917
|
+
* Use for online/offline status, notification counts, or verification badges.
|
|
4918
|
+
* Automatically sizes based on the parent Avatar size.
|
|
4919
|
+
*
|
|
4920
|
+
* @example
|
|
4921
|
+
* ```tsx
|
|
4922
|
+
* <Avatar>
|
|
4923
|
+
* <AvatarImage src="/user.jpg" alt="John Doe" />
|
|
4924
|
+
* <AvatarBadge />
|
|
4925
|
+
* </Avatar>
|
|
4926
|
+
* ```
|
|
4927
|
+
*/
|
|
3862
4928
|
function AvatarBadge({ className, ...props }) {
|
|
3863
4929
|
return /* @__PURE__ */ jsx("span", {
|
|
3864
4930
|
"data-slot": "avatar-badge",
|
|
@@ -3866,6 +4932,25 @@ function AvatarBadge({ className, ...props }) {
|
|
|
3866
4932
|
...props
|
|
3867
4933
|
});
|
|
3868
4934
|
}
|
|
4935
|
+
/**
|
|
4936
|
+
* A container for displaying multiple avatars in an overlapping stack.
|
|
4937
|
+
*
|
|
4938
|
+
* Automatically handles negative spacing and ring styles for visual separation.
|
|
4939
|
+
* Commonly used for displaying team members or participants.
|
|
4940
|
+
*
|
|
4941
|
+
* @example
|
|
4942
|
+
* ```tsx
|
|
4943
|
+
* <AvatarGroup>
|
|
4944
|
+
* <Avatar>
|
|
4945
|
+
* <AvatarImage src="/user1.jpg" alt="User 1" />
|
|
4946
|
+
* </Avatar>
|
|
4947
|
+
* <Avatar>
|
|
4948
|
+
* <AvatarImage src="/user2.jpg" alt="User 2" />
|
|
4949
|
+
* </Avatar>
|
|
4950
|
+
* <AvatarGroupCount>+5</AvatarGroupCount>
|
|
4951
|
+
* </AvatarGroup>
|
|
4952
|
+
* ```
|
|
4953
|
+
*/
|
|
3869
4954
|
function AvatarGroup({ className, ...props }) {
|
|
3870
4955
|
return /* @__PURE__ */ jsx("div", {
|
|
3871
4956
|
"data-slot": "avatar-group",
|
|
@@ -3873,6 +4958,17 @@ function AvatarGroup({ className, ...props }) {
|
|
|
3873
4958
|
...props
|
|
3874
4959
|
});
|
|
3875
4960
|
}
|
|
4961
|
+
/**
|
|
4962
|
+
* A count indicator for an AvatarGroup showing additional members.
|
|
4963
|
+
*
|
|
4964
|
+
* Displays remaining count when not all avatars are shown. Styled to match
|
|
4965
|
+
* the avatar sizes within the group.
|
|
4966
|
+
*
|
|
4967
|
+
* @example
|
|
4968
|
+
* ```tsx
|
|
4969
|
+
* <AvatarGroupCount>+12</AvatarGroupCount>
|
|
4970
|
+
* ```
|
|
4971
|
+
*/
|
|
3876
4972
|
function AvatarGroupCount({ className, ...props }) {
|
|
3877
4973
|
return /* @__PURE__ */ jsx("div", {
|
|
3878
4974
|
"data-slot": "avatar-group-count",
|
|
@@ -3881,36 +4977,29 @@ function AvatarGroupCount({ className, ...props }) {
|
|
|
3881
4977
|
});
|
|
3882
4978
|
}
|
|
3883
4979
|
|
|
3884
|
-
//#endregion
|
|
3885
|
-
//#region src/web/ui/badge.tsx
|
|
3886
|
-
const badgeVariants = cva("h-5 gap-1 rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium transition-all has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&>svg]:size-3! inline-flex items-center justify-center w-fit whitespace-nowrap shrink-0 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-colors overflow-hidden group/badge", {
|
|
3887
|
-
variants: { variant: {
|
|
3888
|
-
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
|
3889
|
-
secondary: "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
|
|
3890
|
-
destructive: "bg-destructive/10 [a]:hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 text-destructive dark:bg-destructive/20",
|
|
3891
|
-
outline: "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
|
|
3892
|
-
ghost: "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
|
|
3893
|
-
link: "text-primary underline-offset-4 hover:underline"
|
|
3894
|
-
} },
|
|
3895
|
-
defaultVariants: { variant: "default" }
|
|
3896
|
-
});
|
|
3897
|
-
function Badge({ className, variant = "default", render, ...props }) {
|
|
3898
|
-
return useRender({
|
|
3899
|
-
defaultTagName: "span",
|
|
3900
|
-
props: mergeProps({ className: cn(badgeVariants({
|
|
3901
|
-
className,
|
|
3902
|
-
variant
|
|
3903
|
-
})) }, props),
|
|
3904
|
-
render,
|
|
3905
|
-
state: {
|
|
3906
|
-
slot: "badge",
|
|
3907
|
-
variant
|
|
3908
|
-
}
|
|
3909
|
-
});
|
|
3910
|
-
}
|
|
3911
|
-
|
|
3912
4980
|
//#endregion
|
|
3913
4981
|
//#region src/web/ui/breadcrumb.tsx
|
|
4982
|
+
/**
|
|
4983
|
+
* A navigation component that shows the user's current location in a hierarchy.
|
|
4984
|
+
*
|
|
4985
|
+
* Helps users understand their position within a site structure and navigate
|
|
4986
|
+
* back to parent pages. Includes proper accessibility attributes.
|
|
4987
|
+
*
|
|
4988
|
+
* @example
|
|
4989
|
+
* ```tsx
|
|
4990
|
+
* <Breadcrumb>
|
|
4991
|
+
* <BreadcrumbList>
|
|
4992
|
+
* <BreadcrumbItem>
|
|
4993
|
+
* <BreadcrumbLink href="/">Home</BreadcrumbLink>
|
|
4994
|
+
* </BreadcrumbItem>
|
|
4995
|
+
* <BreadcrumbSeparator />
|
|
4996
|
+
* <BreadcrumbItem>
|
|
4997
|
+
* <BreadcrumbPage>Current Page</BreadcrumbPage>
|
|
4998
|
+
* </BreadcrumbItem>
|
|
4999
|
+
* </BreadcrumbList>
|
|
5000
|
+
* </Breadcrumb>
|
|
5001
|
+
* ```
|
|
5002
|
+
*/
|
|
3914
5003
|
function Breadcrumb({ className, ...props }) {
|
|
3915
5004
|
return /* @__PURE__ */ jsx("nav", {
|
|
3916
5005
|
"aria-label": "breadcrumb",
|
|
@@ -3919,6 +5008,18 @@ function Breadcrumb({ className, ...props }) {
|
|
|
3919
5008
|
...props
|
|
3920
5009
|
});
|
|
3921
5010
|
}
|
|
5011
|
+
/**
|
|
5012
|
+
* The ordered list container for breadcrumb items.
|
|
5013
|
+
*
|
|
5014
|
+
* Wraps BreadcrumbItem components and handles flex layout with proper spacing.
|
|
5015
|
+
*
|
|
5016
|
+
* @example
|
|
5017
|
+
* ```tsx
|
|
5018
|
+
* <BreadcrumbList>
|
|
5019
|
+
* <BreadcrumbItem>...</BreadcrumbItem>
|
|
5020
|
+
* </BreadcrumbList>
|
|
5021
|
+
* ```
|
|
5022
|
+
*/
|
|
3922
5023
|
function BreadcrumbList({ className, ...props }) {
|
|
3923
5024
|
return /* @__PURE__ */ jsx("ol", {
|
|
3924
5025
|
"data-slot": "breadcrumb-list",
|
|
@@ -3926,6 +5027,19 @@ function BreadcrumbList({ className, ...props }) {
|
|
|
3926
5027
|
...props
|
|
3927
5028
|
});
|
|
3928
5029
|
}
|
|
5030
|
+
/**
|
|
5031
|
+
* A single item in the breadcrumb navigation.
|
|
5032
|
+
*
|
|
5033
|
+
* Contains either a BreadcrumbLink for navigable items or BreadcrumbPage
|
|
5034
|
+
* for the current page indicator.
|
|
5035
|
+
*
|
|
5036
|
+
* @example
|
|
5037
|
+
* ```tsx
|
|
5038
|
+
* <BreadcrumbItem>
|
|
5039
|
+
* <BreadcrumbLink href="/products">Products</BreadcrumbLink>
|
|
5040
|
+
* </BreadcrumbItem>
|
|
5041
|
+
* ```
|
|
5042
|
+
*/
|
|
3929
5043
|
function BreadcrumbItem({ className, ...props }) {
|
|
3930
5044
|
return /* @__PURE__ */ jsx("li", {
|
|
3931
5045
|
"data-slot": "breadcrumb-item",
|
|
@@ -3933,6 +5047,17 @@ function BreadcrumbItem({ className, ...props }) {
|
|
|
3933
5047
|
...props
|
|
3934
5048
|
});
|
|
3935
5049
|
}
|
|
5050
|
+
/**
|
|
5051
|
+
* A navigable link within a breadcrumb item.
|
|
5052
|
+
*
|
|
5053
|
+
* Use for all breadcrumb levels except the current page. Supports custom
|
|
5054
|
+
* rendering via the `render` prop for integration with routing libraries.
|
|
5055
|
+
*
|
|
5056
|
+
* @example
|
|
5057
|
+
* ```tsx
|
|
5058
|
+
* <BreadcrumbLink href="/dashboard">Dashboard</BreadcrumbLink>
|
|
5059
|
+
* ```
|
|
5060
|
+
*/
|
|
3936
5061
|
function BreadcrumbLink({ className, render, ...props }) {
|
|
3937
5062
|
return useRender({
|
|
3938
5063
|
defaultTagName: "a",
|
|
@@ -3941,6 +5066,17 @@ function BreadcrumbLink({ className, render, ...props }) {
|
|
|
3941
5066
|
state: { slot: "breadcrumb-link" }
|
|
3942
5067
|
});
|
|
3943
5068
|
}
|
|
5069
|
+
/**
|
|
5070
|
+
* The current page indicator in a breadcrumb trail.
|
|
5071
|
+
*
|
|
5072
|
+
* Non-interactive element that represents the user's current location.
|
|
5073
|
+
* Includes proper ARIA attributes for accessibility.
|
|
5074
|
+
*
|
|
5075
|
+
* @example
|
|
5076
|
+
* ```tsx
|
|
5077
|
+
* <BreadcrumbPage>Settings</BreadcrumbPage>
|
|
5078
|
+
* ```
|
|
5079
|
+
*/
|
|
3944
5080
|
function BreadcrumbPage({ className, ...props }) {
|
|
3945
5081
|
return /* @__PURE__ */ jsx("span", {
|
|
3946
5082
|
"data-slot": "breadcrumb-page",
|
|
@@ -3951,6 +5087,18 @@ function BreadcrumbPage({ className, ...props }) {
|
|
|
3951
5087
|
...props
|
|
3952
5088
|
});
|
|
3953
5089
|
}
|
|
5090
|
+
/**
|
|
5091
|
+
* A visual separator between breadcrumb items.
|
|
5092
|
+
*
|
|
5093
|
+
* Displays a chevron icon by default but accepts custom children.
|
|
5094
|
+
* Hidden from screen readers as it's purely decorative.
|
|
5095
|
+
*
|
|
5096
|
+
* @example
|
|
5097
|
+
* ```tsx
|
|
5098
|
+
* <BreadcrumbSeparator />
|
|
5099
|
+
* <BreadcrumbSeparator>/</BreadcrumbSeparator>
|
|
5100
|
+
* ```
|
|
5101
|
+
*/
|
|
3954
5102
|
function BreadcrumbSeparator({ children, className, ...props }) {
|
|
3955
5103
|
return /* @__PURE__ */ jsx("li", {
|
|
3956
5104
|
"data-slot": "breadcrumb-separator",
|
|
@@ -3961,6 +5109,19 @@ function BreadcrumbSeparator({ children, className, ...props }) {
|
|
|
3961
5109
|
children: children ?? /* @__PURE__ */ jsx(ChevronRightIcon, {})
|
|
3962
5110
|
});
|
|
3963
5111
|
}
|
|
5112
|
+
/**
|
|
5113
|
+
* An ellipsis indicator for truncated breadcrumb trails.
|
|
5114
|
+
*
|
|
5115
|
+
* Use when there are too many levels to display. Often combined with a
|
|
5116
|
+
* dropdown menu to show hidden items.
|
|
5117
|
+
*
|
|
5118
|
+
* @example
|
|
5119
|
+
* ```tsx
|
|
5120
|
+
* <BreadcrumbItem>
|
|
5121
|
+
* <BreadcrumbEllipsis />
|
|
5122
|
+
* </BreadcrumbItem>
|
|
5123
|
+
* ```
|
|
5124
|
+
*/
|
|
3964
5125
|
function BreadcrumbEllipsis({ className, ...props }) {
|
|
3965
5126
|
return /* @__PURE__ */ jsxs("span", {
|
|
3966
5127
|
"data-slot": "breadcrumb-ellipsis",
|
|
@@ -4016,7 +5177,7 @@ function Calendar({ className, classNames, showOutsideDays = true, captionLayout
|
|
|
4016
5177
|
const defaultClassNames = getDefaultClassNames();
|
|
4017
5178
|
return /* @__PURE__ */ jsx(DayPicker, {
|
|
4018
5179
|
showOutsideDays,
|
|
4019
|
-
className: cn("p-3 [--cell-radius:var(--radius-md)] [--cell-size:--spacing(8)] bg-background group/calendar
|
|
5180
|
+
className: cn("p-3 [--cell-radius:var(--radius-md)] [--cell-size:--spacing(8)] bg-background group/calendar in-data-[slot=card-content]:bg-transparent in-data-[slot=popover-content]:bg-transparent", String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`, String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`, className),
|
|
4020
5181
|
captionLayout,
|
|
4021
5182
|
formatters: {
|
|
4022
5183
|
formatMonthDropdown: (date) => date.toLocaleString("default", { month: "short" }),
|
|
@@ -4389,30 +5550,85 @@ function CollapsibleContent({ ...props }) {
|
|
|
4389
5550
|
|
|
4390
5551
|
//#endregion
|
|
4391
5552
|
//#region src/web/ui/dialog.tsx
|
|
5553
|
+
/**
|
|
5554
|
+
* A modal window that appears above the page content.
|
|
5555
|
+
*
|
|
5556
|
+
* Use for focused tasks like forms, confirmations, or displaying detailed
|
|
5557
|
+
* information. Traps focus and prevents interaction with the background.
|
|
5558
|
+
*
|
|
5559
|
+
* @example
|
|
5560
|
+
* ```tsx
|
|
5561
|
+
* <Dialog>
|
|
5562
|
+
* <DialogTrigger render={<Button />}>Open Dialog</DialogTrigger>
|
|
5563
|
+
* <DialogContent>
|
|
5564
|
+
* <DialogHeader>
|
|
5565
|
+
* <DialogTitle>Edit Profile</DialogTitle>
|
|
5566
|
+
* <DialogDescription>Make changes to your profile.</DialogDescription>
|
|
5567
|
+
* </DialogHeader>
|
|
5568
|
+
* <DialogFooter>
|
|
5569
|
+
* <Button>Save changes</Button>
|
|
5570
|
+
* </DialogFooter>
|
|
5571
|
+
* </DialogContent>
|
|
5572
|
+
* </Dialog>
|
|
5573
|
+
* ```
|
|
5574
|
+
*/
|
|
4392
5575
|
function Dialog({ ...props }) {
|
|
4393
5576
|
return /* @__PURE__ */ jsx(Dialog$1.Root, {
|
|
4394
5577
|
"data-slot": "dialog",
|
|
4395
5578
|
...props
|
|
4396
5579
|
});
|
|
4397
5580
|
}
|
|
5581
|
+
/**
|
|
5582
|
+
* The button that opens the dialog.
|
|
5583
|
+
*
|
|
5584
|
+
* Use the `render` prop to customize the trigger element while maintaining
|
|
5585
|
+
* proper accessibility attributes.
|
|
5586
|
+
*
|
|
5587
|
+
* @example
|
|
5588
|
+
* ```tsx
|
|
5589
|
+
* <DialogTrigger render={<Button />}>Open</DialogTrigger>
|
|
5590
|
+
* ```
|
|
5591
|
+
*/
|
|
4398
5592
|
function DialogTrigger({ ...props }) {
|
|
4399
5593
|
return /* @__PURE__ */ jsx(Dialog$1.Trigger, {
|
|
4400
5594
|
"data-slot": "dialog-trigger",
|
|
4401
5595
|
...props
|
|
4402
5596
|
});
|
|
4403
5597
|
}
|
|
5598
|
+
/**
|
|
5599
|
+
* Portals the dialog content to the end of the document body.
|
|
5600
|
+
*
|
|
5601
|
+
* Used internally by DialogContent. Ensures the dialog renders above
|
|
5602
|
+
* other content regardless of DOM position.
|
|
5603
|
+
*/
|
|
4404
5604
|
function DialogPortal({ ...props }) {
|
|
4405
5605
|
return /* @__PURE__ */ jsx(Dialog$1.Portal, {
|
|
4406
5606
|
"data-slot": "dialog-portal",
|
|
4407
5607
|
...props
|
|
4408
5608
|
});
|
|
4409
5609
|
}
|
|
5610
|
+
/**
|
|
5611
|
+
* A button that closes the dialog when clicked.
|
|
5612
|
+
*
|
|
5613
|
+
* Can be placed anywhere within the dialog content.
|
|
5614
|
+
*
|
|
5615
|
+
* @example
|
|
5616
|
+
* ```tsx
|
|
5617
|
+
* <DialogClose render={<Button variant="outline" />}>Cancel</DialogClose>
|
|
5618
|
+
* ```
|
|
5619
|
+
*/
|
|
4410
5620
|
function DialogClose({ ...props }) {
|
|
4411
5621
|
return /* @__PURE__ */ jsx(Dialog$1.Close, {
|
|
4412
5622
|
"data-slot": "dialog-close",
|
|
4413
5623
|
...props
|
|
4414
5624
|
});
|
|
4415
5625
|
}
|
|
5626
|
+
/**
|
|
5627
|
+
* The backdrop overlay behind the dialog.
|
|
5628
|
+
*
|
|
5629
|
+
* Dims the background and prevents interaction with the page. Includes
|
|
5630
|
+
* blur effect on supported browsers. Used internally by DialogContent.
|
|
5631
|
+
*/
|
|
4416
5632
|
function DialogOverlay({ className, ...props }) {
|
|
4417
5633
|
return /* @__PURE__ */ jsx(Dialog$1.Backdrop, {
|
|
4418
5634
|
"data-slot": "dialog-overlay",
|
|
@@ -4420,6 +5636,20 @@ function DialogOverlay({ className, ...props }) {
|
|
|
4420
5636
|
...props
|
|
4421
5637
|
});
|
|
4422
5638
|
}
|
|
5639
|
+
/**
|
|
5640
|
+
* The container for the dialog content.
|
|
5641
|
+
*
|
|
5642
|
+
* Centers the dialog on screen with smooth open/close animations. Includes
|
|
5643
|
+
* the portal and overlay automatically. Optionally shows a close button.
|
|
5644
|
+
*
|
|
5645
|
+
* @example
|
|
5646
|
+
* ```tsx
|
|
5647
|
+
* <DialogContent>
|
|
5648
|
+
* <DialogHeader>...</DialogHeader>
|
|
5649
|
+
* <DialogFooter>...</DialogFooter>
|
|
5650
|
+
* </DialogContent>
|
|
5651
|
+
* ```
|
|
5652
|
+
*/
|
|
4423
5653
|
function DialogContent({ className, children, showCloseButton = true, ...props }) {
|
|
4424
5654
|
return /* @__PURE__ */ jsxs(DialogPortal, { children: [/* @__PURE__ */ jsx(DialogOverlay, {}), /* @__PURE__ */ jsxs(Dialog$1.Popup, {
|
|
4425
5655
|
"data-slot": "dialog-content",
|
|
@@ -4439,6 +5669,19 @@ function DialogContent({ className, children, showCloseButton = true, ...props }
|
|
|
4439
5669
|
})]
|
|
4440
5670
|
})] });
|
|
4441
5671
|
}
|
|
5672
|
+
/**
|
|
5673
|
+
* The header section containing the dialog title and description.
|
|
5674
|
+
*
|
|
5675
|
+
* Provides consistent spacing and layout for dialog headers.
|
|
5676
|
+
*
|
|
5677
|
+
* @example
|
|
5678
|
+
* ```tsx
|
|
5679
|
+
* <DialogHeader>
|
|
5680
|
+
* <DialogTitle>Edit Settings</DialogTitle>
|
|
5681
|
+
* <DialogDescription>Configure your preferences.</DialogDescription>
|
|
5682
|
+
* </DialogHeader>
|
|
5683
|
+
* ```
|
|
5684
|
+
*/
|
|
4442
5685
|
function DialogHeader({ className, ...props }) {
|
|
4443
5686
|
return /* @__PURE__ */ jsx("div", {
|
|
4444
5687
|
"data-slot": "dialog-header",
|
|
@@ -4446,6 +5689,20 @@ function DialogHeader({ className, ...props }) {
|
|
|
4446
5689
|
...props
|
|
4447
5690
|
});
|
|
4448
5691
|
}
|
|
5692
|
+
/**
|
|
5693
|
+
* The footer section containing dialog action buttons.
|
|
5694
|
+
*
|
|
5695
|
+
* Arranges buttons in a responsive layout with proper spacing.
|
|
5696
|
+
* Optionally includes a close button.
|
|
5697
|
+
*
|
|
5698
|
+
* @example
|
|
5699
|
+
* ```tsx
|
|
5700
|
+
* <DialogFooter>
|
|
5701
|
+
* <Button variant="outline">Cancel</Button>
|
|
5702
|
+
* <Button>Save</Button>
|
|
5703
|
+
* </DialogFooter>
|
|
5704
|
+
* ```
|
|
5705
|
+
*/
|
|
4449
5706
|
function DialogFooter({ className, showCloseButton = false, children, ...props }) {
|
|
4450
5707
|
return /* @__PURE__ */ jsxs("div", {
|
|
4451
5708
|
"data-slot": "dialog-footer",
|
|
@@ -4457,6 +5714,16 @@ function DialogFooter({ className, showCloseButton = false, children, ...props }
|
|
|
4457
5714
|
})]
|
|
4458
5715
|
});
|
|
4459
5716
|
}
|
|
5717
|
+
/**
|
|
5718
|
+
* The title text for the dialog.
|
|
5719
|
+
*
|
|
5720
|
+
* Displayed prominently and announced by screen readers when the dialog opens.
|
|
5721
|
+
*
|
|
5722
|
+
* @example
|
|
5723
|
+
* ```tsx
|
|
5724
|
+
* <DialogTitle>Confirm Action</DialogTitle>
|
|
5725
|
+
* ```
|
|
5726
|
+
*/
|
|
4460
5727
|
function DialogTitle({ className, ...props }) {
|
|
4461
5728
|
return /* @__PURE__ */ jsx(Dialog$1.Title, {
|
|
4462
5729
|
"data-slot": "dialog-title",
|
|
@@ -4464,6 +5731,18 @@ function DialogTitle({ className, ...props }) {
|
|
|
4464
5731
|
...props
|
|
4465
5732
|
});
|
|
4466
5733
|
}
|
|
5734
|
+
/**
|
|
5735
|
+
* The descriptive text explaining the dialog purpose.
|
|
5736
|
+
*
|
|
5737
|
+
* Provides context about the action or content within the dialog.
|
|
5738
|
+
*
|
|
5739
|
+
* @example
|
|
5740
|
+
* ```tsx
|
|
5741
|
+
* <DialogDescription>
|
|
5742
|
+
* This action cannot be undone. Please confirm your choice.
|
|
5743
|
+
* </DialogDescription>
|
|
5744
|
+
* ```
|
|
5745
|
+
*/
|
|
4467
5746
|
function DialogDescription({ className, ...props }) {
|
|
4468
5747
|
return /* @__PURE__ */ jsx(Dialog$1.Description, {
|
|
4469
5748
|
"data-slot": "dialog-description",
|
|
@@ -4474,6 +5753,17 @@ function DialogDescription({ className, ...props }) {
|
|
|
4474
5753
|
|
|
4475
5754
|
//#endregion
|
|
4476
5755
|
//#region src/web/ui/textarea.tsx
|
|
5756
|
+
/**
|
|
5757
|
+
* A multi-line text input field.
|
|
5758
|
+
*
|
|
5759
|
+
* Use for longer text content like comments, descriptions, or messages.
|
|
5760
|
+
* Automatically expands based on content with a minimum height.
|
|
5761
|
+
*
|
|
5762
|
+
* @example
|
|
5763
|
+
* ```tsx
|
|
5764
|
+
* <Textarea placeholder="Enter your message..." />
|
|
5765
|
+
* ```
|
|
5766
|
+
*/
|
|
4477
5767
|
function Textarea({ className, ...props }) {
|
|
4478
5768
|
return /* @__PURE__ */ jsx("textarea", {
|
|
4479
5769
|
"data-slot": "textarea",
|
|
@@ -5405,6 +6695,29 @@ function NavigationMenuIndicator({ className, ...props }) {
|
|
|
5405
6695
|
|
|
5406
6696
|
//#endregion
|
|
5407
6697
|
//#region src/web/ui/pagination.tsx
|
|
6698
|
+
/**
|
|
6699
|
+
* A navigation component for moving between pages of content.
|
|
6700
|
+
*
|
|
6701
|
+
* Provides a consistent interface for paginated data. Includes proper
|
|
6702
|
+
* ARIA attributes for accessibility.
|
|
6703
|
+
*
|
|
6704
|
+
* @example
|
|
6705
|
+
* ```tsx
|
|
6706
|
+
* <Pagination>
|
|
6707
|
+
* <PaginationContent>
|
|
6708
|
+
* <PaginationItem>
|
|
6709
|
+
* <PaginationPrevious href="/page/1" />
|
|
6710
|
+
* </PaginationItem>
|
|
6711
|
+
* <PaginationItem>
|
|
6712
|
+
* <PaginationLink href="/page/2">2</PaginationLink>
|
|
6713
|
+
* </PaginationItem>
|
|
6714
|
+
* <PaginationItem>
|
|
6715
|
+
* <PaginationNext href="/page/3" />
|
|
6716
|
+
* </PaginationItem>
|
|
6717
|
+
* </PaginationContent>
|
|
6718
|
+
* </Pagination>
|
|
6719
|
+
* ```
|
|
6720
|
+
*/
|
|
5408
6721
|
function Pagination({ className, ...props }) {
|
|
5409
6722
|
return /* @__PURE__ */ jsx("nav", {
|
|
5410
6723
|
"aria-label": "pagination",
|
|
@@ -5413,6 +6726,18 @@ function Pagination({ className, ...props }) {
|
|
|
5413
6726
|
...props
|
|
5414
6727
|
});
|
|
5415
6728
|
}
|
|
6729
|
+
/**
|
|
6730
|
+
* The container for pagination items.
|
|
6731
|
+
*
|
|
6732
|
+
* Wraps PaginationItem components in a horizontal flex layout.
|
|
6733
|
+
*
|
|
6734
|
+
* @example
|
|
6735
|
+
* ```tsx
|
|
6736
|
+
* <PaginationContent>
|
|
6737
|
+
* <PaginationItem>...</PaginationItem>
|
|
6738
|
+
* </PaginationContent>
|
|
6739
|
+
* ```
|
|
6740
|
+
*/
|
|
5416
6741
|
function PaginationContent({ className, ...props }) {
|
|
5417
6742
|
return /* @__PURE__ */ jsx("ul", {
|
|
5418
6743
|
"data-slot": "pagination-content",
|
|
@@ -5420,12 +6745,35 @@ function PaginationContent({ className, ...props }) {
|
|
|
5420
6745
|
...props
|
|
5421
6746
|
});
|
|
5422
6747
|
}
|
|
6748
|
+
/**
|
|
6749
|
+
* A single item wrapper in the pagination list.
|
|
6750
|
+
*
|
|
6751
|
+
* Contains a PaginationLink, PaginationPrevious, PaginationNext, or PaginationEllipsis.
|
|
6752
|
+
*
|
|
6753
|
+
* @example
|
|
6754
|
+
* ```tsx
|
|
6755
|
+
* <PaginationItem>
|
|
6756
|
+
* <PaginationLink href="/page/1">1</PaginationLink>
|
|
6757
|
+
* </PaginationItem>
|
|
6758
|
+
* ```
|
|
6759
|
+
*/
|
|
5423
6760
|
function PaginationItem({ ...props }) {
|
|
5424
6761
|
return /* @__PURE__ */ jsx("li", {
|
|
5425
6762
|
"data-slot": "pagination-item",
|
|
5426
6763
|
...props
|
|
5427
6764
|
});
|
|
5428
6765
|
}
|
|
6766
|
+
/**
|
|
6767
|
+
* A clickable link to a specific page number.
|
|
6768
|
+
*
|
|
6769
|
+
* Displays as active when representing the current page. Renders as a
|
|
6770
|
+
* button styled anchor element.
|
|
6771
|
+
*
|
|
6772
|
+
* @example
|
|
6773
|
+
* ```tsx
|
|
6774
|
+
* <PaginationLink href="/page/1" isActive>1</PaginationLink>
|
|
6775
|
+
* ```
|
|
6776
|
+
*/
|
|
5429
6777
|
function PaginationLink({ className, isActive, size = "icon", ...props }) {
|
|
5430
6778
|
return /* @__PURE__ */ jsx(Button, {
|
|
5431
6779
|
variant: isActive ? "outline" : "ghost",
|
|
@@ -5440,6 +6788,16 @@ function PaginationLink({ className, isActive, size = "icon", ...props }) {
|
|
|
5440
6788
|
})
|
|
5441
6789
|
});
|
|
5442
6790
|
}
|
|
6791
|
+
/**
|
|
6792
|
+
* A link to the previous page with chevron icon.
|
|
6793
|
+
*
|
|
6794
|
+
* Shows "Previous" text on larger screens. Includes accessible label.
|
|
6795
|
+
*
|
|
6796
|
+
* @example
|
|
6797
|
+
* ```tsx
|
|
6798
|
+
* <PaginationPrevious href="/page/1" />
|
|
6799
|
+
* ```
|
|
6800
|
+
*/
|
|
5443
6801
|
function PaginationPrevious({ className, ...props }) {
|
|
5444
6802
|
return /* @__PURE__ */ jsxs(PaginationLink, {
|
|
5445
6803
|
"aria-label": "Go to previous page",
|
|
@@ -5452,6 +6810,16 @@ function PaginationPrevious({ className, ...props }) {
|
|
|
5452
6810
|
})]
|
|
5453
6811
|
});
|
|
5454
6812
|
}
|
|
6813
|
+
/**
|
|
6814
|
+
* A link to the next page with chevron icon.
|
|
6815
|
+
*
|
|
6816
|
+
* Shows "Next" text on larger screens. Includes accessible label.
|
|
6817
|
+
*
|
|
6818
|
+
* @example
|
|
6819
|
+
* ```tsx
|
|
6820
|
+
* <PaginationNext href="/page/3" />
|
|
6821
|
+
* ```
|
|
6822
|
+
*/
|
|
5455
6823
|
function PaginationNext({ className, ...props }) {
|
|
5456
6824
|
return /* @__PURE__ */ jsxs(PaginationLink, {
|
|
5457
6825
|
"aria-label": "Go to next page",
|
|
@@ -5464,6 +6832,19 @@ function PaginationNext({ className, ...props }) {
|
|
|
5464
6832
|
}), /* @__PURE__ */ jsx(ChevronRightIcon, { "data-icon": "inline-end" })]
|
|
5465
6833
|
});
|
|
5466
6834
|
}
|
|
6835
|
+
/**
|
|
6836
|
+
* An ellipsis indicator for skipped page numbers.
|
|
6837
|
+
*
|
|
6838
|
+
* Use when there are too many pages to display all links. Hidden from
|
|
6839
|
+
* screen readers with accessible fallback text.
|
|
6840
|
+
*
|
|
6841
|
+
* @example
|
|
6842
|
+
* ```tsx
|
|
6843
|
+
* <PaginationItem>
|
|
6844
|
+
* <PaginationEllipsis />
|
|
6845
|
+
* </PaginationItem>
|
|
6846
|
+
* ```
|
|
6847
|
+
*/
|
|
5467
6848
|
function PaginationEllipsis({ className, ...props }) {
|
|
5468
6849
|
return /* @__PURE__ */ jsxs("span", {
|
|
5469
6850
|
"aria-hidden": true,
|
|
@@ -5529,6 +6910,20 @@ function PopoverDescription({ className, ...props }) {
|
|
|
5529
6910
|
|
|
5530
6911
|
//#endregion
|
|
5531
6912
|
//#region src/web/ui/progress.tsx
|
|
6913
|
+
/**
|
|
6914
|
+
* A visual indicator showing the completion progress of a task.
|
|
6915
|
+
*
|
|
6916
|
+
* Displays a horizontal bar that fills based on the value prop.
|
|
6917
|
+
* Use for upload progress, multi-step forms, or loading states.
|
|
6918
|
+
*
|
|
6919
|
+
* @example
|
|
6920
|
+
* ```tsx
|
|
6921
|
+
* <Progress value={60}>
|
|
6922
|
+
* <ProgressLabel>Uploading...</ProgressLabel>
|
|
6923
|
+
* <ProgressValue />
|
|
6924
|
+
* </Progress>
|
|
6925
|
+
* ```
|
|
6926
|
+
*/
|
|
5532
6927
|
function Progress({ className, children, value, ...props }) {
|
|
5533
6928
|
return /* @__PURE__ */ jsxs(Progress$1.Root, {
|
|
5534
6929
|
value,
|
|
@@ -5538,6 +6933,12 @@ function Progress({ className, children, value, ...props }) {
|
|
|
5538
6933
|
children: [children, /* @__PURE__ */ jsx(ProgressTrack, { children: /* @__PURE__ */ jsx(ProgressIndicator, {}) })]
|
|
5539
6934
|
});
|
|
5540
6935
|
}
|
|
6936
|
+
/**
|
|
6937
|
+
* The background track of the progress bar.
|
|
6938
|
+
*
|
|
6939
|
+
* Contains the ProgressIndicator that shows the filled portion.
|
|
6940
|
+
* Used internally by Progress.
|
|
6941
|
+
*/
|
|
5541
6942
|
function ProgressTrack({ className, ...props }) {
|
|
5542
6943
|
return /* @__PURE__ */ jsx(Progress$1.Track, {
|
|
5543
6944
|
className: cn("bg-muted h-1.5 rounded-full relative flex w-full items-center overflow-x-hidden", className),
|
|
@@ -5545,6 +6946,12 @@ function ProgressTrack({ className, ...props }) {
|
|
|
5545
6946
|
...props
|
|
5546
6947
|
});
|
|
5547
6948
|
}
|
|
6949
|
+
/**
|
|
6950
|
+
* The filled portion of the progress bar.
|
|
6951
|
+
*
|
|
6952
|
+
* Animates width based on the parent Progress value.
|
|
6953
|
+
* Used internally by Progress.
|
|
6954
|
+
*/
|
|
5548
6955
|
function ProgressIndicator({ className, ...props }) {
|
|
5549
6956
|
return /* @__PURE__ */ jsx(Progress$1.Indicator, {
|
|
5550
6957
|
"data-slot": "progress-indicator",
|
|
@@ -5552,6 +6959,16 @@ function ProgressIndicator({ className, ...props }) {
|
|
|
5552
6959
|
...props
|
|
5553
6960
|
});
|
|
5554
6961
|
}
|
|
6962
|
+
/**
|
|
6963
|
+
* A descriptive label for the progress bar.
|
|
6964
|
+
*
|
|
6965
|
+
* Displays above the progress track to describe what is being measured.
|
|
6966
|
+
*
|
|
6967
|
+
* @example
|
|
6968
|
+
* ```tsx
|
|
6969
|
+
* <ProgressLabel>Uploading files...</ProgressLabel>
|
|
6970
|
+
* ```
|
|
6971
|
+
*/
|
|
5555
6972
|
function ProgressLabel({ className, ...props }) {
|
|
5556
6973
|
return /* @__PURE__ */ jsx(Progress$1.Label, {
|
|
5557
6974
|
className: cn("text-sm font-medium", className),
|
|
@@ -5559,6 +6976,16 @@ function ProgressLabel({ className, ...props }) {
|
|
|
5559
6976
|
...props
|
|
5560
6977
|
});
|
|
5561
6978
|
}
|
|
6979
|
+
/**
|
|
6980
|
+
* A numeric display of the progress percentage.
|
|
6981
|
+
*
|
|
6982
|
+
* Automatically shows the current progress value as a percentage.
|
|
6983
|
+
*
|
|
6984
|
+
* @example
|
|
6985
|
+
* ```tsx
|
|
6986
|
+
* <ProgressValue />
|
|
6987
|
+
* ```
|
|
6988
|
+
*/
|
|
5562
6989
|
function ProgressValue({ className, ...props }) {
|
|
5563
6990
|
return /* @__PURE__ */ jsx(Progress$1.Value, {
|
|
5564
6991
|
className: cn("text-muted-foreground ml-auto text-sm tabular-nums", className),
|
|
@@ -5647,7 +7074,39 @@ function ScrollBar({ className, orientation = "vertical", ...props }) {
|
|
|
5647
7074
|
|
|
5648
7075
|
//#endregion
|
|
5649
7076
|
//#region src/web/ui/select.tsx
|
|
7077
|
+
/**
|
|
7078
|
+
* A dropdown for selecting a single value from a list of options.
|
|
7079
|
+
*
|
|
7080
|
+
* Provides a native-like select experience with custom styling. Supports
|
|
7081
|
+
* keyboard navigation, search, and grouping of options.
|
|
7082
|
+
*
|
|
7083
|
+
* @example
|
|
7084
|
+
* ```tsx
|
|
7085
|
+
* <Select>
|
|
7086
|
+
* <SelectTrigger>
|
|
7087
|
+
* <SelectValue placeholder="Select an option" />
|
|
7088
|
+
* </SelectTrigger>
|
|
7089
|
+
* <SelectContent>
|
|
7090
|
+
* <SelectItem value="apple">Apple</SelectItem>
|
|
7091
|
+
* <SelectItem value="banana">Banana</SelectItem>
|
|
7092
|
+
* </SelectContent>
|
|
7093
|
+
* </Select>
|
|
7094
|
+
* ```
|
|
7095
|
+
*/
|
|
5650
7096
|
const Select = Select$1.Root;
|
|
7097
|
+
/**
|
|
7098
|
+
* A container for grouping related SelectItem components.
|
|
7099
|
+
*
|
|
7100
|
+
* Use with SelectLabel to create labeled groups of options.
|
|
7101
|
+
*
|
|
7102
|
+
* @example
|
|
7103
|
+
* ```tsx
|
|
7104
|
+
* <SelectGroup>
|
|
7105
|
+
* <SelectLabel>Fruits</SelectLabel>
|
|
7106
|
+
* <SelectItem value="apple">Apple</SelectItem>
|
|
7107
|
+
* </SelectGroup>
|
|
7108
|
+
* ```
|
|
7109
|
+
*/
|
|
5651
7110
|
function SelectGroup({ className, ...props }) {
|
|
5652
7111
|
return /* @__PURE__ */ jsx(Select$1.Group, {
|
|
5653
7112
|
"data-slot": "select-group",
|
|
@@ -5655,6 +7114,16 @@ function SelectGroup({ className, ...props }) {
|
|
|
5655
7114
|
...props
|
|
5656
7115
|
});
|
|
5657
7116
|
}
|
|
7117
|
+
/**
|
|
7118
|
+
* Displays the currently selected value in the trigger.
|
|
7119
|
+
*
|
|
7120
|
+
* Shows a placeholder when no value is selected.
|
|
7121
|
+
*
|
|
7122
|
+
* @example
|
|
7123
|
+
* ```tsx
|
|
7124
|
+
* <SelectValue placeholder="Select a fruit" />
|
|
7125
|
+
* ```
|
|
7126
|
+
*/
|
|
5658
7127
|
function SelectValue({ className, ...props }) {
|
|
5659
7128
|
return /* @__PURE__ */ jsx(Select$1.Value, {
|
|
5660
7129
|
"data-slot": "select-value",
|
|
@@ -5662,6 +7131,19 @@ function SelectValue({ className, ...props }) {
|
|
|
5662
7131
|
...props
|
|
5663
7132
|
});
|
|
5664
7133
|
}
|
|
7134
|
+
/**
|
|
7135
|
+
* The button that opens the select dropdown.
|
|
7136
|
+
*
|
|
7137
|
+
* Displays the SelectValue and a dropdown indicator icon.
|
|
7138
|
+
* Supports multiple sizes.
|
|
7139
|
+
*
|
|
7140
|
+
* @example
|
|
7141
|
+
* ```tsx
|
|
7142
|
+
* <SelectTrigger>
|
|
7143
|
+
* <SelectValue placeholder="Select..." />
|
|
7144
|
+
* </SelectTrigger>
|
|
7145
|
+
* ```
|
|
7146
|
+
*/
|
|
5665
7147
|
function SelectTrigger({ className, size = "default", children, ...props }) {
|
|
5666
7148
|
return /* @__PURE__ */ jsxs(Select$1.Trigger, {
|
|
5667
7149
|
"data-slot": "select-trigger",
|
|
@@ -5671,6 +7153,20 @@ function SelectTrigger({ className, size = "default", children, ...props }) {
|
|
|
5671
7153
|
children: [children, /* @__PURE__ */ jsx(Select$1.Icon, { render: /* @__PURE__ */ jsx(ChevronDownIcon, { className: "text-muted-foreground size-4 pointer-events-none" }) })]
|
|
5672
7154
|
});
|
|
5673
7155
|
}
|
|
7156
|
+
/**
|
|
7157
|
+
* The dropdown popup containing the select options.
|
|
7158
|
+
*
|
|
7159
|
+
* Positions relative to the trigger with scroll buttons for long lists.
|
|
7160
|
+
* Supports alignment and side positioning options.
|
|
7161
|
+
*
|
|
7162
|
+
* @example
|
|
7163
|
+
* ```tsx
|
|
7164
|
+
* <SelectContent>
|
|
7165
|
+
* <SelectItem value="1">Option 1</SelectItem>
|
|
7166
|
+
* <SelectItem value="2">Option 2</SelectItem>
|
|
7167
|
+
* </SelectContent>
|
|
7168
|
+
* ```
|
|
7169
|
+
*/
|
|
5674
7170
|
function SelectContent({ className, children, side = "bottom", sideOffset = 4, align = "center", alignOffset = 0, alignItemWithTrigger = true, ...props }) {
|
|
5675
7171
|
return /* @__PURE__ */ jsx(Select$1.Portal, { children: /* @__PURE__ */ jsx(Select$1.Positioner, {
|
|
5676
7172
|
side,
|
|
@@ -5691,6 +7187,16 @@ function SelectContent({ className, children, side = "bottom", sideOffset = 4, a
|
|
|
5691
7187
|
})
|
|
5692
7188
|
}) });
|
|
5693
7189
|
}
|
|
7190
|
+
/**
|
|
7191
|
+
* A label for a SelectGroup.
|
|
7192
|
+
*
|
|
7193
|
+
* Displays in muted style above the group's items.
|
|
7194
|
+
*
|
|
7195
|
+
* @example
|
|
7196
|
+
* ```tsx
|
|
7197
|
+
* <SelectLabel>Category</SelectLabel>
|
|
7198
|
+
* ```
|
|
7199
|
+
*/
|
|
5694
7200
|
function SelectLabel({ className, ...props }) {
|
|
5695
7201
|
return /* @__PURE__ */ jsx(Select$1.GroupLabel, {
|
|
5696
7202
|
"data-slot": "select-label",
|
|
@@ -5698,6 +7204,17 @@ function SelectLabel({ className, ...props }) {
|
|
|
5698
7204
|
...props
|
|
5699
7205
|
});
|
|
5700
7206
|
}
|
|
7207
|
+
/**
|
|
7208
|
+
* A single selectable option in the dropdown.
|
|
7209
|
+
*
|
|
7210
|
+
* Displays a checkmark when selected. Supports keyboard navigation
|
|
7211
|
+
* and focus states.
|
|
7212
|
+
*
|
|
7213
|
+
* @example
|
|
7214
|
+
* ```tsx
|
|
7215
|
+
* <SelectItem value="apple">Apple</SelectItem>
|
|
7216
|
+
* ```
|
|
7217
|
+
*/
|
|
5701
7218
|
function SelectItem({ className, children, ...props }) {
|
|
5702
7219
|
return /* @__PURE__ */ jsxs(Select$1.Item, {
|
|
5703
7220
|
"data-slot": "select-item",
|
|
@@ -5712,6 +7229,16 @@ function SelectItem({ className, children, ...props }) {
|
|
|
5712
7229
|
})]
|
|
5713
7230
|
});
|
|
5714
7231
|
}
|
|
7232
|
+
/**
|
|
7233
|
+
* A horizontal line separating groups of select items.
|
|
7234
|
+
*
|
|
7235
|
+
* Use to visually divide different sections of options.
|
|
7236
|
+
*
|
|
7237
|
+
* @example
|
|
7238
|
+
* ```tsx
|
|
7239
|
+
* <SelectSeparator />
|
|
7240
|
+
* ```
|
|
7241
|
+
*/
|
|
5715
7242
|
function SelectSeparator({ className, ...props }) {
|
|
5716
7243
|
return /* @__PURE__ */ jsx(Select$1.Separator, {
|
|
5717
7244
|
"data-slot": "select-separator",
|
|
@@ -5719,6 +7246,12 @@ function SelectSeparator({ className, ...props }) {
|
|
|
5719
7246
|
...props
|
|
5720
7247
|
});
|
|
5721
7248
|
}
|
|
7249
|
+
/**
|
|
7250
|
+
* A scroll indicator button at the top of the select dropdown.
|
|
7251
|
+
*
|
|
7252
|
+
* Appears when content is scrollable and there are items above the viewport.
|
|
7253
|
+
* Used internally by SelectContent.
|
|
7254
|
+
*/
|
|
5722
7255
|
function SelectScrollUpButton({ className, ...props }) {
|
|
5723
7256
|
return /* @__PURE__ */ jsx(Select$1.ScrollUpArrow, {
|
|
5724
7257
|
"data-slot": "select-scroll-up-button",
|
|
@@ -5727,6 +7260,12 @@ function SelectScrollUpButton({ className, ...props }) {
|
|
|
5727
7260
|
children: /* @__PURE__ */ jsx(ChevronUpIcon, {})
|
|
5728
7261
|
});
|
|
5729
7262
|
}
|
|
7263
|
+
/**
|
|
7264
|
+
* A scroll indicator button at the bottom of the select dropdown.
|
|
7265
|
+
*
|
|
7266
|
+
* Appears when content is scrollable and there are items below the viewport.
|
|
7267
|
+
* Used internally by SelectContent.
|
|
7268
|
+
*/
|
|
5730
7269
|
function SelectScrollDownButton({ className, ...props }) {
|
|
5731
7270
|
return /* @__PURE__ */ jsx(Select$1.ScrollDownArrow, {
|
|
5732
7271
|
"data-slot": "select-scroll-down-button",
|
|
@@ -5820,6 +7359,18 @@ function SheetDescription({ className, ...props }) {
|
|
|
5820
7359
|
|
|
5821
7360
|
//#endregion
|
|
5822
7361
|
//#region src/web/ui/skeleton.tsx
|
|
7362
|
+
/**
|
|
7363
|
+
* A placeholder component showing a pulsing animation while content loads.
|
|
7364
|
+
*
|
|
7365
|
+
* Use to indicate loading states and reduce layout shift. Apply appropriate
|
|
7366
|
+
* width and height classes to match the expected content dimensions.
|
|
7367
|
+
*
|
|
7368
|
+
* @example
|
|
7369
|
+
* ```tsx
|
|
7370
|
+
* <Skeleton className="h-4 w-32" />
|
|
7371
|
+
* <Skeleton className="h-12 w-12 rounded-full" />
|
|
7372
|
+
* ```
|
|
7373
|
+
*/
|
|
5823
7374
|
function Skeleton({ className, ...props }) {
|
|
5824
7375
|
return /* @__PURE__ */ jsx("div", {
|
|
5825
7376
|
"data-slot": "skeleton",
|
|
@@ -5830,6 +7381,12 @@ function Skeleton({ className, ...props }) {
|
|
|
5830
7381
|
|
|
5831
7382
|
//#endregion
|
|
5832
7383
|
//#region src/web/ui/tooltip.tsx
|
|
7384
|
+
/**
|
|
7385
|
+
* Provides shared delay configuration for child tooltips.
|
|
7386
|
+
*
|
|
7387
|
+
* Used internally by Tooltip. Can be used explicitly to share delay
|
|
7388
|
+
* settings across multiple tooltips.
|
|
7389
|
+
*/
|
|
5833
7390
|
function TooltipProvider({ delay = 0, ...props }) {
|
|
5834
7391
|
return /* @__PURE__ */ jsx(Tooltip$1.Provider, {
|
|
5835
7392
|
"data-slot": "tooltip-provider",
|
|
@@ -5837,18 +7394,59 @@ function TooltipProvider({ delay = 0, ...props }) {
|
|
|
5837
7394
|
...props
|
|
5838
7395
|
});
|
|
5839
7396
|
}
|
|
7397
|
+
/**
|
|
7398
|
+
* A popup that displays information on hover or focus.
|
|
7399
|
+
*
|
|
7400
|
+
* Use for additional context about UI elements. Shows after a brief
|
|
7401
|
+
* delay when hovering or focusing the trigger.
|
|
7402
|
+
*
|
|
7403
|
+
* @example
|
|
7404
|
+
* ```tsx
|
|
7405
|
+
* <Tooltip>
|
|
7406
|
+
* <TooltipTrigger>
|
|
7407
|
+
* <Button variant="ghost" size="icon">?</Button>
|
|
7408
|
+
* </TooltipTrigger>
|
|
7409
|
+
* <TooltipContent>Help information</TooltipContent>
|
|
7410
|
+
* </Tooltip>
|
|
7411
|
+
* ```
|
|
7412
|
+
*/
|
|
5840
7413
|
function Tooltip({ ...props }) {
|
|
5841
7414
|
return /* @__PURE__ */ jsx(TooltipProvider, { children: /* @__PURE__ */ jsx(Tooltip$1.Root, {
|
|
5842
7415
|
"data-slot": "tooltip",
|
|
5843
7416
|
...props
|
|
5844
7417
|
}) });
|
|
5845
7418
|
}
|
|
7419
|
+
/**
|
|
7420
|
+
* The element that triggers the tooltip on hover or focus.
|
|
7421
|
+
*
|
|
7422
|
+
* Wraps any interactive element to enable tooltip behavior.
|
|
7423
|
+
*
|
|
7424
|
+
* @example
|
|
7425
|
+
* ```tsx
|
|
7426
|
+
* <TooltipTrigger>
|
|
7427
|
+
* <Button>Hover me</Button>
|
|
7428
|
+
* </TooltipTrigger>
|
|
7429
|
+
* ```
|
|
7430
|
+
*/
|
|
5846
7431
|
function TooltipTrigger({ ...props }) {
|
|
5847
7432
|
return /* @__PURE__ */ jsx(Tooltip$1.Trigger, {
|
|
5848
7433
|
"data-slot": "tooltip-trigger",
|
|
5849
7434
|
...props
|
|
5850
7435
|
});
|
|
5851
7436
|
}
|
|
7437
|
+
/**
|
|
7438
|
+
* The popup content displayed when the tooltip is visible.
|
|
7439
|
+
*
|
|
7440
|
+
* Supports positioning relative to the trigger with smooth animations.
|
|
7441
|
+
* Includes an arrow pointing to the trigger.
|
|
7442
|
+
*
|
|
7443
|
+
* @example
|
|
7444
|
+
* ```tsx
|
|
7445
|
+
* <TooltipContent side="bottom">
|
|
7446
|
+
* This is tooltip content
|
|
7447
|
+
* </TooltipContent>
|
|
7448
|
+
* ```
|
|
7449
|
+
*/
|
|
5852
7450
|
function TooltipContent({ className, side = "top", sideOffset = 4, align = "center", alignOffset = 0, children, ...props }) {
|
|
5853
7451
|
return /* @__PURE__ */ jsx(Tooltip$1.Portal, { children: /* @__PURE__ */ jsx(Tooltip$1.Positioner, {
|
|
5854
7452
|
align,
|
|
@@ -6231,6 +7829,18 @@ function SidebarMenuSubButton({ render, size = "md", isActive = false, className
|
|
|
6231
7829
|
|
|
6232
7830
|
//#endregion
|
|
6233
7831
|
//#region src/web/ui/slider.tsx
|
|
7832
|
+
/**
|
|
7833
|
+
* A control for selecting a numeric value within a range.
|
|
7834
|
+
*
|
|
7835
|
+
* Supports single value or range selection with draggable thumbs.
|
|
7836
|
+
* Works with both horizontal and vertical orientations.
|
|
7837
|
+
*
|
|
7838
|
+
* @example
|
|
7839
|
+
* ```tsx
|
|
7840
|
+
* <Slider defaultValue={[50]} max={100} step={1} />
|
|
7841
|
+
* <Slider defaultValue={[25, 75]} max={100} />
|
|
7842
|
+
* ```
|
|
7843
|
+
*/
|
|
6234
7844
|
function Slider({ className, defaultValue, value, min = 0, max = 100, ...props }) {
|
|
6235
7845
|
const _values = React.useMemo(() => Array.isArray(value) ? value : Array.isArray(defaultValue) ? defaultValue : [min, max], [
|
|
6236
7846
|
value,
|
|
@@ -6291,6 +7901,18 @@ const Toaster = ({ ...props }) => {
|
|
|
6291
7901
|
|
|
6292
7902
|
//#endregion
|
|
6293
7903
|
//#region src/web/ui/switch.tsx
|
|
7904
|
+
/**
|
|
7905
|
+
* A toggle control for switching between on and off states.
|
|
7906
|
+
*
|
|
7907
|
+
* Use for binary settings that take effect immediately. Supports
|
|
7908
|
+
* multiple sizes and disabled states.
|
|
7909
|
+
*
|
|
7910
|
+
* @example
|
|
7911
|
+
* ```tsx
|
|
7912
|
+
* <Switch id="airplane" />
|
|
7913
|
+
* <Label htmlFor="airplane">Airplane Mode</Label>
|
|
7914
|
+
* ```
|
|
7915
|
+
*/
|
|
6294
7916
|
function Switch({ className, size = "default", ...props }) {
|
|
6295
7917
|
return /* @__PURE__ */ jsx(Switch$1.Root, {
|
|
6296
7918
|
"data-slot": "switch",
|
|
@@ -6369,6 +7991,24 @@ function TableCaption({ className, ...props }) {
|
|
|
6369
7991
|
|
|
6370
7992
|
//#endregion
|
|
6371
7993
|
//#region src/web/ui/tabs.tsx
|
|
7994
|
+
/**
|
|
7995
|
+
* A set of layered content panels displayed one at a time.
|
|
7996
|
+
*
|
|
7997
|
+
* Organizes content into distinct views that users can switch between.
|
|
7998
|
+
* Supports horizontal and vertical orientations.
|
|
7999
|
+
*
|
|
8000
|
+
* @example
|
|
8001
|
+
* ```tsx
|
|
8002
|
+
* <Tabs defaultValue="account">
|
|
8003
|
+
* <TabsList>
|
|
8004
|
+
* <TabsTrigger value="account">Account</TabsTrigger>
|
|
8005
|
+
* <TabsTrigger value="settings">Settings</TabsTrigger>
|
|
8006
|
+
* </TabsList>
|
|
8007
|
+
* <TabsContent value="account">Account content</TabsContent>
|
|
8008
|
+
* <TabsContent value="settings">Settings content</TabsContent>
|
|
8009
|
+
* </Tabs>
|
|
8010
|
+
* ```
|
|
8011
|
+
*/
|
|
6372
8012
|
function Tabs({ className, orientation = "horizontal", ...props }) {
|
|
6373
8013
|
return /* @__PURE__ */ jsx(Tabs$1.Root, {
|
|
6374
8014
|
"data-slot": "tabs",
|
|
@@ -6377,6 +8017,7 @@ function Tabs({ className, orientation = "horizontal", ...props }) {
|
|
|
6377
8017
|
...props
|
|
6378
8018
|
});
|
|
6379
8019
|
}
|
|
8020
|
+
/** TabsList variant styles for different visual appearances. */
|
|
6380
8021
|
const tabsListVariants = cva("rounded-lg p-[3px] group-data-horizontal/tabs:h-9 data-[variant=line]:rounded-none group/tabs-list text-muted-foreground inline-flex w-fit items-center justify-center group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col", {
|
|
6381
8022
|
variants: { variant: {
|
|
6382
8023
|
default: "bg-muted",
|
|
@@ -6384,6 +8025,19 @@ const tabsListVariants = cva("rounded-lg p-[3px] group-data-horizontal/tabs:h-9
|
|
|
6384
8025
|
} },
|
|
6385
8026
|
defaultVariants: { variant: "default" }
|
|
6386
8027
|
});
|
|
8028
|
+
/**
|
|
8029
|
+
* The container for tab triggers.
|
|
8030
|
+
*
|
|
8031
|
+
* Supports default (pill-style) and line variants. Arranges triggers
|
|
8032
|
+
* horizontally or vertically based on parent orientation.
|
|
8033
|
+
*
|
|
8034
|
+
* @example
|
|
8035
|
+
* ```tsx
|
|
8036
|
+
* <TabsList variant="line">
|
|
8037
|
+
* <TabsTrigger value="tab1">Tab 1</TabsTrigger>
|
|
8038
|
+
* </TabsList>
|
|
8039
|
+
* ```
|
|
8040
|
+
*/
|
|
6387
8041
|
function TabsList({ className, variant = "default", ...props }) {
|
|
6388
8042
|
return /* @__PURE__ */ jsx(Tabs$1.List, {
|
|
6389
8043
|
"data-slot": "tabs-list",
|
|
@@ -6392,6 +8046,16 @@ function TabsList({ className, variant = "default", ...props }) {
|
|
|
6392
8046
|
...props
|
|
6393
8047
|
});
|
|
6394
8048
|
}
|
|
8049
|
+
/**
|
|
8050
|
+
* A button that activates its associated tab content panel.
|
|
8051
|
+
*
|
|
8052
|
+
* Displays active state styling and supports keyboard navigation.
|
|
8053
|
+
*
|
|
8054
|
+
* @example
|
|
8055
|
+
* ```tsx
|
|
8056
|
+
* <TabsTrigger value="settings">Settings</TabsTrigger>
|
|
8057
|
+
* ```
|
|
8058
|
+
*/
|
|
6395
8059
|
function TabsTrigger({ className, ...props }) {
|
|
6396
8060
|
return /* @__PURE__ */ jsx(Tabs$1.Tab, {
|
|
6397
8061
|
"data-slot": "tabs-trigger",
|
|
@@ -6399,6 +8063,19 @@ function TabsTrigger({ className, ...props }) {
|
|
|
6399
8063
|
...props
|
|
6400
8064
|
});
|
|
6401
8065
|
}
|
|
8066
|
+
/**
|
|
8067
|
+
* The content panel associated with a tab trigger.
|
|
8068
|
+
*
|
|
8069
|
+
* Only visible when its corresponding trigger is active. Hidden panels
|
|
8070
|
+
* are removed from the accessibility tree.
|
|
8071
|
+
*
|
|
8072
|
+
* @example
|
|
8073
|
+
* ```tsx
|
|
8074
|
+
* <TabsContent value="settings">
|
|
8075
|
+
* <p>Settings panel content.</p>
|
|
8076
|
+
* </TabsContent>
|
|
8077
|
+
* ```
|
|
8078
|
+
*/
|
|
6402
8079
|
function TabsContent({ className, ...props }) {
|
|
6403
8080
|
return /* @__PURE__ */ jsx(Tabs$1.Panel, {
|
|
6404
8081
|
"data-slot": "tabs-content",
|
|
@@ -6409,6 +8086,7 @@ function TabsContent({ className, ...props }) {
|
|
|
6409
8086
|
|
|
6410
8087
|
//#endregion
|
|
6411
8088
|
//#region src/web/ui/toggle.tsx
|
|
8089
|
+
/** Toggle variant and size styles. */
|
|
6412
8090
|
const toggleVariants = cva("hover:text-foreground aria-pressed:bg-muted focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive gap-1 rounded-md text-sm font-medium transition-[color,box-shadow] [&_svg:not([class*='size-'])]:size-4 group/toggle hover:bg-muted inline-flex items-center justify-center whitespace-nowrap outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0", {
|
|
6413
8091
|
variants: {
|
|
6414
8092
|
variant: {
|
|
@@ -6426,6 +8104,19 @@ const toggleVariants = cva("hover:text-foreground aria-pressed:bg-muted focus-vi
|
|
|
6426
8104
|
size: "default"
|
|
6427
8105
|
}
|
|
6428
8106
|
});
|
|
8107
|
+
/**
|
|
8108
|
+
* A button that can be toggled on or off.
|
|
8109
|
+
*
|
|
8110
|
+
* Use for toolbar buttons, formatting options, or any binary state toggle.
|
|
8111
|
+
* Shows a pressed state when active.
|
|
8112
|
+
*
|
|
8113
|
+
* @example
|
|
8114
|
+
* ```tsx
|
|
8115
|
+
* <Toggle aria-label="Toggle bold">
|
|
8116
|
+
* <BoldIcon />
|
|
8117
|
+
* </Toggle>
|
|
8118
|
+
* ```
|
|
8119
|
+
*/
|
|
6429
8120
|
function Toggle({ className, variant = "default", size = "default", ...props }) {
|
|
6430
8121
|
return /* @__PURE__ */ jsx(Toggle$1, {
|
|
6431
8122
|
"data-slot": "toggle",
|
|
@@ -6484,4 +8175,4 @@ function ToggleGroupItem({ className, children, variant = "default", size = "def
|
|
|
6484
8175
|
}
|
|
6485
8176
|
|
|
6486
8177
|
//#endregion
|
|
6487
|
-
export { AUTH_BASE_PATH, AUTH_BASE_URL, Accordion, AccordionContent, AccordionItem, AccordionTrigger, Alert, AlertAction, AlertDescription, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogMedia, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger, AlertTitle, AnimatedCheck, AspectRatio, AuthCard, AuthLayout, AuthProvider, Avatar, AvatarBadge, AvatarFallback, AvatarGroup, AvatarGroupCount, AvatarImage, Badge, Brand, Breadcrumb, BreadcrumbEllipsis, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, Button, ButtonGroup, ButtonGroupSeparator, ButtonGroupText, Calendar, CalendarDayButton, Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious, ChartContainer, ChartLegend, ChartLegendContent, ChartStyle, ChartTooltip, ChartTooltipContent, Checkbox, Collapsible, CollapsibleContent, CollapsibleTrigger, Command, CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, CommandShortcut, ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuGroup, ContextMenuItem, ContextMenuLabel, ContextMenuPortal, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuShortcut, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger, DEFAULT_AUTH_PATHS, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerOverlay, DrawerPortal, DrawerTitle, DrawerTrigger, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, EmailInput, EmailOTPForm, Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle, ErrorPage, Field, FieldContent, FieldDescription, FieldError, FieldGroup, FieldLabel, FieldLegend, FieldSeparator, FieldSet, FieldTitle, ForgotPasswordForm, Form, HoverCard, HoverCardContent, HoverCardTrigger, HydratedRouter, I18nextProvider, IDLE_FETCHER, Input, InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput, InputGroupText, InputGroupTextarea, InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot, Item, ItemActions, ItemContent, ItemDescription, ItemFooter, ItemGroup, ItemHeader, ItemMedia, ItemSeparator, ItemTitle, Kbd, KbdGroup, Label, Link, Links, LoginForm, MagicLinkForm, Menubar, MenubarCheckboxItem, MenubarContent, MenubarGroup, MenubarItem, MenubarLabel, MenubarMenu, MenubarPortal, MenubarRadioGroup, MenubarRadioItem, MenubarSeparator, MenubarShortcut, MenubarSub, MenubarSubContent, MenubarSubTrigger, MenubarTrigger, Meta, NavLink, NavigationMenu, NavigationMenuContent, NavigationMenuIndicator, NavigationMenuItem, NavigationMenuLink, NavigationMenuList, NavigationMenuPositioner, NavigationMenuTrigger, OtpInput, Outlet, Pagination, PaginationContent, PaginationEllipsis, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious, PasskeyButton, PasskeyRegisterButton, PasswordInput, PhoneInput, PhoneOTPForm, Popover, PopoverContent, PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger, Progress, ProgressIndicator, ProgressLabel, ProgressTrack, ProgressValue, QueryClient, QueryClientProvider, RadioGroup, RadioGroupItem, ResetPasswordForm, ResizableHandle, ResizablePanel, ResizablePanelGroup, Route, SSOButton, Scripts, ScrollArea, ScrollBar, ScrollRestoration, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, Separator, ServerRouter, Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, SheetTrigger, Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupAction, SidebarGroupContent, SidebarGroupLabel, SidebarHeader, SidebarInput, SidebarInset, SidebarMenu, SidebarMenuAction, SidebarMenuBadge, SidebarMenuButton, SidebarMenuItem, SidebarMenuSkeleton, SidebarMenuSub, SidebarMenuSubButton, SidebarMenuSubItem, SidebarProvider, SidebarRail, SidebarSeparator, SidebarTrigger, SignupForm, Skeleton, Slider, SocialButtons, Spinner, Switch, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow, Tabs, TabsContent, TabsList, TabsTrigger, Textarea, Toaster, Toggle, ToggleGroup, ToggleGroupItem, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, Trans, TrustSignals, TwoFactorForm, VerifyEmailForm, badgeVariants, buttonGroupVariants, buttonVariants, changeLanguage, cn, createInstance, defaultI18nConfig, defineAuthClient, defineAuthFeatures, defineEmailOtpSchema, defineForgotPasswordSchema, defineI18nClient, defineLoginRedirect, defineLoginSchema, defineMagicLinkSchema, defineOtpSchema, definePhoneNumberSchema, definePostAuthRedirect, defineResetPasswordSchema, defineSignupSchema, defineTwoFactorBackupSchema, defineTwoFactorTotpSchema, getFixedT, href, init, initReactI18next, loadLanguages, loadNamespaces, loadResources, matchPath, matchRoutes, navigationMenuTriggerStyle, parsePath, redirect, redirectDocument, reloadResources, renderMatches, replace, resolvePath, setDefaultNamespace, t, tabsListVariants, toggleVariants, use, useActionData, useAsyncError, useAsyncValue, useAuth, useAuthContext, useAuthForm, useBeforeUnload, useBlocker, useCarousel, useFetcher, useFetchers, useFormAction, useHref, useInRouterContext, useIsMobile, useLinkClickHandler, useLoaderData, useLocation, useMatch, useMatches, useMutation, useNavigate, useNavigation, useNavigationType, useOutlet, useOutletContext, useParams, useQuery, useQueryClient, useResolvedPath, useRevalidator, useRouteError, useRouteLoaderData, useRoutes, useSearchParams, useSidebar, useSubmit, useTranslation, useViewTransitionState };
|
|
8178
|
+
export { AUTH_BASE_PATH, AUTH_BASE_URL, Accordion, AccordionContent, AccordionItem, AccordionTrigger, Alert, AlertAction, AlertDescription, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogMedia, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger, AlertTitle, AnimatedCheck, AspectRatio, AuthCard, AuthLayout, AuthProvider, Avatar, AvatarBadge, AvatarFallback, AvatarGroup, AvatarGroupCount, AvatarImage, Badge, Brand, Breadcrumb, BreadcrumbEllipsis, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, Button, ButtonGroup, ButtonGroupSeparator, ButtonGroupText, Calendar, CalendarDayButton, CaptchaWidget, Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious, ChartContainer, ChartLegend, ChartLegendContent, ChartStyle, ChartTooltip, ChartTooltipContent, Checkbox, Collapsible, CollapsibleContent, CollapsibleTrigger, Command, CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, CommandShortcut, ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuGroup, ContextMenuItem, ContextMenuLabel, ContextMenuPortal, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuShortcut, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger, DEFAULT_AUTH_PATHS, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerOverlay, DrawerPortal, DrawerTitle, DrawerTrigger, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, EmailInput, EmailOTPForm, Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle, ErrorPage, Field, FieldContent, FieldDescription, FieldError, FieldGroup, FieldLabel, FieldLegend, FieldSeparator, FieldSet, FieldTitle, ForgotPasswordForm, Form, HoverCard, HoverCardContent, HoverCardTrigger, HydratedRouter, I18nextProvider, IDLE_FETCHER, Input, InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput, InputGroupText, InputGroupTextarea, InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot, Item, ItemActions, ItemContent, ItemDescription, ItemFooter, ItemGroup, ItemHeader, ItemMedia, ItemSeparator, ItemTitle, Kbd, KbdGroup, Label, Link, Links, LoginForm, MagicLinkForm, Menubar, MenubarCheckboxItem, MenubarContent, MenubarGroup, MenubarItem, MenubarLabel, MenubarMenu, MenubarPortal, MenubarRadioGroup, MenubarRadioItem, MenubarSeparator, MenubarShortcut, MenubarSub, MenubarSubContent, MenubarSubTrigger, MenubarTrigger, Meta, NavLink, NavigationMenu, NavigationMenuContent, NavigationMenuIndicator, NavigationMenuItem, NavigationMenuLink, NavigationMenuList, NavigationMenuPositioner, NavigationMenuTrigger, OtpInput, Outlet, Pagination, PaginationContent, PaginationEllipsis, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious, PasskeyButton, PasskeyRegisterButton, PasswordInput, PhoneInput, PhoneOTPForm, Popover, PopoverContent, PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger, Progress, ProgressIndicator, ProgressLabel, ProgressTrack, ProgressValue, QueryClient, QueryClientProvider, RadioGroup, RadioGroupItem, ResetPasswordForm, ResizableHandle, ResizablePanel, ResizablePanelGroup, Route, SSOButton, Scripts, ScrollArea, ScrollBar, ScrollRestoration, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, Separator, ServerRouter, Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, SheetTrigger, Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupAction, SidebarGroupContent, SidebarGroupLabel, SidebarHeader, SidebarInput, SidebarInset, SidebarMenu, SidebarMenuAction, SidebarMenuBadge, SidebarMenuButton, SidebarMenuItem, SidebarMenuSkeleton, SidebarMenuSub, SidebarMenuSubButton, SidebarMenuSubItem, SidebarProvider, SidebarRail, SidebarSeparator, SidebarTrigger, SignupForm, Skeleton, Slider, SocialButtons, Spinner, Switch, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow, Tabs, TabsContent, TabsList, TabsTrigger, Textarea, Toaster, Toggle, ToggleGroup, ToggleGroupItem, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, Trans, TrustSignals, TwoFactorForm, VerifyEmailForm, badgeVariants, buttonGroupVariants, buttonSizeKeys, buttonVariantKeys, buttonVariants, changeLanguage, cn, createInstance, defaultI18nConfig, defineAuthClient, defineAuthFeatures, defineEmailOtpSchema, defineForgotPasswordSchema, defineI18nClient, defineLoginRedirect, defineLoginSchema, defineMagicLinkSchema, defineOtpSchema, definePhoneNumberSchema, definePostAuthRedirect, defineResetPasswordSchema, defineSignupSchema, defineTwoFactorBackupSchema, defineTwoFactorTotpSchema, getFixedT, href, init, initReactI18next, loadLanguages, loadNamespaces, loadResources, matchPath, matchRoutes, navigationMenuTriggerStyle, orientationSchema, orientations, parsePath, redirect, redirectDocument, reloadResources, renderMatches, replace, resolvePath, setDefaultNamespace, sizeSchema, sizes, t, tabsListVariants, toggleVariants, use, useActionData, useAsyncError, useAsyncValue, useAuth, useAuthContext, useAuthForm, useBeforeUnload, useBlocker, useCaptcha, useCarousel, useFetcher, useFetchers, useFormAction, useHref, useInRouterContext, useIsMobile, useLastUsedMethod, useLinkClickHandler, useLoaderData, useLocation, useMatch, useMatches, useMutation, useNavigate, useNavigation, useNavigationType, useOutlet, useOutletContext, useParams, useQuery, useQueryClient, useResolvedPath, useRevalidator, useRouteError, useRouteLoaderData, useRoutes, useSearchParams, useSidebar, useSubmit, useTranslation, useViewTransitionState };
|