@nocobase/app-template-default 0.0.0-stage → 1.0.0-beta.58
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/.cursor/mcp.json +8 -0
- package/.mcp.json +8 -0
- package/.prettierignore +20 -0
- package/.vscode/mcp.json +8 -0
- package/AGENTS.md +434 -0
- package/CLAUDE.md +9 -0
- package/Dockerfile +132 -0
- package/Dockerfile.dockerignore +45 -0
- package/README.MD +485 -0
- package/cli/plugins.ts +8 -0
- package/client/agent-annotations-host.ts +19 -0
- package/client/app.ts +38 -0
- package/client/components/back-button.tsx +49 -0
- package/client/components/breadcrumbs.tsx +167 -0
- package/client/components/inbox-badge.ts +49 -0
- package/client/components/inbox-button.tsx +132 -0
- package/client/components/inbox-header-button.tsx +25 -0
- package/client/components/loading.tsx +35 -0
- package/client/components/page-container.tsx +24 -0
- package/client/components/page-header.tsx +33 -0
- package/client/components/route-child-page.tsx +121 -0
- package/client/components/route-dialog.tsx +10 -0
- package/client/components/route-drawer.tsx +10 -0
- package/client/components/route-overlay.tsx +190 -0
- package/client/components/ui/alert.tsx +75 -0
- package/client/components/ui/badge.tsx +51 -0
- package/client/components/ui/breadcrumb.tsx +121 -0
- package/client/components/ui/button.tsx +58 -0
- package/client/components/ui/collapsible.tsx +19 -0
- package/client/components/ui/context-menu.tsx +271 -0
- package/client/components/ui/dialog.tsx +160 -0
- package/client/components/ui/dropdown-menu.tsx +270 -0
- package/client/components/ui/empty.tsx +103 -0
- package/client/components/ui/field.tsx +236 -0
- package/client/components/ui/input-group.tsx +158 -0
- package/client/components/ui/input.tsx +20 -0
- package/client/components/ui/kbd.tsx +26 -0
- package/client/components/ui/label.tsx +18 -0
- package/client/components/ui/popover.tsx +89 -0
- package/client/components/ui/select.tsx +200 -0
- package/client/components/ui/separator.tsx +22 -0
- package/client/components/ui/sheet.tsx +137 -0
- package/client/components/ui/sidebar.tsx +723 -0
- package/client/components/ui/skeleton.tsx +13 -0
- package/client/components/ui/spinner.tsx +20 -0
- package/client/components/ui/tabs.tsx +79 -0
- package/client/components/ui/textarea.tsx +17 -0
- package/client/components/ui/toast.tsx +231 -0
- package/client/components/ui/tooltip.tsx +63 -0
- package/client/components/use-route-overlay.ts +26 -0
- package/client/config/api.ts +13 -0
- package/client/config/app.ts +8 -0
- package/client/config/auth.ts +18 -0
- package/client/config/index.ts +17 -0
- package/client/extensions/nocobase-auth-forms/field-props.ts +13 -0
- package/client/extensions/nocobase-auth-forms/form-parts.tsx +101 -0
- package/client/extensions/nocobase-auth-forms/password-input.tsx +41 -0
- package/client/extensions/nocobase-auth-forms/password-login-form.tsx +123 -0
- package/client/extensions/nocobase-auth-forms/password-registration-form.tsx +193 -0
- package/client/extensions/nocobase-auth-forms/password-reset-form.tsx +132 -0
- package/client/extensions/nocobase-auth-forms/password-reset-request-form.tsx +99 -0
- package/client/extensions/nocobase-auth-methods/auth-methods.tsx +170 -0
- package/client/extensions/nocobase-auth-split-layout/auth-split-layout.tsx +126 -0
- package/client/extensions/nocobase-device-approval/device-approval.tsx +277 -0
- package/client/extensions/nocobase-device-approval/locales/en-US.ts +55 -0
- package/client/extensions/nocobase-device-approval/locales/zh-CN.ts +27 -0
- package/client/extensions/nocobase-device-approval/use-device-approval.ts +176 -0
- package/client/extensions/nocobase-file-component-ui/README.md +13 -0
- package/client/extensions/nocobase-file-component-ui/components/file-list.tsx +147 -0
- package/client/extensions/nocobase-file-component-ui/components/file-preview-dialog.tsx +263 -0
- package/client/extensions/nocobase-file-component-ui/components/file-preview-field.tsx +68 -0
- package/client/extensions/nocobase-file-component-ui/components/file-thumbnail.tsx +73 -0
- package/client/extensions/nocobase-file-component-ui/components/file-upload-field.tsx +386 -0
- package/client/extensions/nocobase-file-component-ui/components/previewers/file-preview-content.tsx +255 -0
- package/client/extensions/nocobase-file-component-ui/components/previewers/office-open-xml-preview.tsx +226 -0
- package/client/extensions/nocobase-file-component-ui/index.ts +8 -0
- package/client/extensions/nocobase-file-component-ui/lib/file-preview.ts +194 -0
- package/client/extensions/nocobase-file-component-ui/lib/file-url.ts +22 -0
- package/client/extensions/nocobase-file-component-ui/lib/message-key.ts +18 -0
- package/client/extensions/nocobase-file-component-ui/types.ts +69 -0
- package/client/extensions/nocobase-inbox/decision-actions-bar.tsx +140 -0
- package/client/extensions/nocobase-inbox/frame.ts +31 -0
- package/client/extensions/nocobase-inbox/inbox-actions.ts +21 -0
- package/client/extensions/nocobase-inbox/inbox-detail.tsx +386 -0
- package/client/extensions/nocobase-inbox/inbox-item.tsx +274 -0
- package/client/extensions/nocobase-inbox/inbox-list.tsx +433 -0
- package/client/extensions/nocobase-inbox/inbox-page.tsx +545 -0
- package/client/extensions/nocobase-inbox/locales/en-US.ts +111 -0
- package/client/extensions/nocobase-inbox/locales/zh-CN.ts +57 -0
- package/client/extensions/nocobase-inbox/model.ts +428 -0
- package/client/extensions/nocobase-inbox/registry-scope.tsx +49 -0
- package/client/extensions/nocobase-inbox/registry.ts +303 -0
- package/client/extensions/nocobase-inbox/source.ts +117 -0
- package/client/hooks/use-mobile.ts +21 -0
- package/client/index.tsx +60 -0
- package/client/layouts/app-layout.tsx +105 -0
- package/client/layouts/components/app-brand.tsx +49 -0
- package/client/layouts/components/app-sidebar.tsx +196 -0
- package/client/layouts/components/header-actions.tsx +50 -0
- package/client/layouts/components/language-switcher.tsx +95 -0
- package/client/layouts/components/layout-header.tsx +17 -0
- package/client/layouts/components/navigation-menu.tsx +411 -0
- package/client/layouts/components/sidebar-footer.tsx +107 -0
- package/client/layouts/components/surface-empty.tsx +35 -0
- package/client/layouts/components/user-menu.tsx +127 -0
- package/client/layouts/index.ts +2 -0
- package/client/layouts/settings-layout.tsx +172 -0
- package/client/layouts/use-sidebar-preference.ts +57 -0
- package/client/lib/constants.ts +5 -0
- package/client/lib/toaster.ts +56 -0
- package/client/lib/utils.ts +1 -0
- package/client/locales/en-US.ts +178 -0
- package/client/locales/index.ts +9 -0
- package/client/locales/zh-CN.ts +161 -0
- package/client/pages/auth/device.tsx +41 -0
- package/client/pages/auth/forgot-password.tsx +39 -0
- package/client/pages/auth/login.tsx +66 -0
- package/client/pages/auth/register.tsx +46 -0
- package/client/pages/auth/reset-password.tsx +38 -0
- package/client/pages/auth/shared.tsx +111 -0
- package/client/pages/home.tsx +16 -0
- package/client/pages/inbox.tsx +12 -0
- package/client/plugins.ts +37 -0
- package/client/react-providers.ts +27 -0
- package/client/route-overrides.ts +9 -0
- package/client/routes.ts +75 -0
- package/client/routing/app-router.tsx +127 -0
- package/client/routing/client-route.tsx +129 -0
- package/client/routing/route-context.tsx +85 -0
- package/client/routing/route-navigation.ts +153 -0
- package/client/routing/route-tree.tsx +42 -0
- package/client/routing/standalone-page-layout.tsx +15 -0
- package/client/runtime.ts +26 -0
- package/client/service-provider.ts +40 -0
- package/client/source-extensions.ts +19 -0
- package/client/startup.tsx +22 -0
- package/client/styles.css +122 -0
- package/client/theme/index.ts +4 -0
- package/client/theme/theme-context.ts +12 -0
- package/client/theme/theme-preferences.ts +84 -0
- package/client/theme/theme-presets.ts +7 -0
- package/client/theme/theme-provider.tsx +106 -0
- package/client/theme/theme-settings.tsx +176 -0
- package/client/theme/themes/compact.css +140 -0
- package/client/theme/themes/default.css +117 -0
- package/client/vite-env.d.ts +5 -0
- package/components.json +27 -0
- package/config.example.yml +156 -0
- package/database/main/migrations/.gitkeep +0 -0
- package/database/main/migrations/202610060001_create_device_code.ts +37 -0
- package/database/main/seeds/.gitkeep +0 -0
- package/database/tsconfig.json +4 -0
- package/ecosystem.config.js +35 -0
- package/eslint.config.js +24 -0
- package/index.html +100 -0
- package/package.json +198 -3
- package/playwright.config.ts +10 -0
- package/public/assets/favicon.ico +0 -0
- package/public/assets/logo-dark.png +0 -0
- package/public/assets/logo-mark-dark.png +0 -0
- package/public/assets/logo-mark.png +0 -0
- package/public/assets/logo.png +0 -0
- package/server/app.ts +50 -0
- package/server/config/api.ts +8 -0
- package/server/config/app.ts +40 -0
- package/server/config/auth.ts +36 -0
- package/server/config/authorization.ts +18 -0
- package/server/config/caching.ts +21 -0
- package/server/config/database.ts +22 -0
- package/server/config/drive.ts +30 -0
- package/server/config/i18n.ts +17 -0
- package/server/config/index.ts +67 -0
- package/server/config/jobs.ts +27 -0
- package/server/config/logging.ts +23 -0
- package/server/config/notification.ts +11 -0
- package/server/config/queue.ts +28 -0
- package/server/config/scheduler.ts +23 -0
- package/server/config/secrets.ts +10 -0
- package/server/config/server.ts +26 -0
- package/server/config/session.ts +60 -0
- package/server/config/snowflake.ts +21 -0
- package/server/config/spa.ts +24 -0
- package/server/config/users.ts +11 -0
- package/server/config/workflow.ts +24 -0
- package/server/config.ts +24 -0
- package/server/embedded.ts +23 -0
- package/server/locales/en-US.ts +10 -0
- package/server/locales/index.ts +10 -0
- package/server/locales/zh-CN.ts +5 -0
- package/server/plugins.ts +37 -0
- package/server/providers/index.ts +5 -0
- package/server/routes/index.ts +6 -0
- package/server/runtime.ts +24 -0
- package/server/standalone.ts +29 -0
- package/server/tsconfig.json +4 -0
- package/tailwind.config.mjs +59 -0
- package/tests/components/app-layout-routes.test.tsx +132 -0
- package/tests/components/app-title.test.tsx +57 -0
- package/tests/components/auth-i18n.test.tsx +87 -0
- package/tests/components/back-button.test.tsx +80 -0
- package/tests/components/file-preview.test.tsx +101 -0
- package/tests/components/header-hover.test.tsx +87 -0
- package/tests/components/inbox.test.tsx +139 -0
- package/tests/components/language-switcher.test.tsx +298 -0
- package/tests/components/layout-containers.test.tsx +15 -0
- package/tests/components/layout-i18n.test.tsx +187 -0
- package/tests/components/loading.test.tsx +25 -0
- package/tests/components/navigation-menu.test.tsx +355 -0
- package/tests/components/page-harness.test.tsx +318 -0
- package/tests/components/primitive-labels.test.tsx +80 -0
- package/tests/components/sidebar-permissions.test.tsx +110 -0
- package/tests/components/sidebar-preference.test.tsx +42 -0
- package/tests/fixtures/theme-tokens.html +12 -0
- package/tests/fixtures/theme-tokens.tsx +111 -0
- package/tests/logic/account-permissions.test.tsx +204 -0
- package/tests/logic/app-locale-coverage.test.ts +136 -0
- package/tests/logic/app-server.test.ts +1589 -0
- package/tests/logic/asset-url-build.test.ts +106 -0
- package/tests/logic/client-auth.test.tsx +122 -0
- package/tests/logic/client-plugin-registry.test.ts +63 -0
- package/tests/logic/client-route-recovery.test.tsx +80 -0
- package/tests/logic/client-routes.test.ts +126 -0
- package/tests/logic/client-runtime.test.ts +112 -0
- package/tests/logic/client-settings.test.tsx +841 -0
- package/tests/logic/client-shell.test.tsx +466 -0
- package/tests/logic/client-theme.test.tsx +441 -0
- package/tests/logic/config.test.ts +197 -0
- package/tests/logic/dev-client-watch.test.ts +108 -0
- package/tests/logic/dev-routes-production-build.test.ts +168 -0
- package/tests/logic/device-code-migration.test.ts +70 -0
- package/tests/logic/lifecycle.test.ts +221 -0
- package/tests/logic/plugin-commands.test.ts +69 -0
- package/tests/logic/plugin-page-authz.test.ts +53 -0
- package/tests/logic/route-navigation.test.ts +66 -0
- package/tests/logic/route-overlay.test.tsx +353 -0
- package/tests/logic/server-imports.test.ts +88 -0
- package/tests/logic/tailwind-sources.test.ts +151 -0
- package/tests/logic/template-package-name.test.ts +88 -0
- package/tests/logic/theme-preferences.test.ts +86 -0
- package/tests/logic/theme-tokens.test.ts +248 -0
- package/tests/logic/toaster.test.tsx +108 -0
- package/tests/logic/user-menu-sign-out.test.tsx +98 -0
- package/tests/setup/client-config.ts +27 -0
- package/tsconfig.json +11 -0
- package/tsconfig.node.json +12 -0
- package/tsconfig.server.json +19 -0
- package/vite.config.ts +92 -0
- package/vitest.config.ts +44 -0
- package/README.md +0 -3
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { cn } from 'cn';
|
|
2
|
+
|
|
3
|
+
function Skeleton({ className, ...props }: React.ComponentProps<'div'>) {
|
|
4
|
+
return (
|
|
5
|
+
<div
|
|
6
|
+
data-slot='skeleton'
|
|
7
|
+
className={cn('animate-pulse rounded-md bg-muted', className)}
|
|
8
|
+
{...props}
|
|
9
|
+
/>
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export { Skeleton };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { useTranslation } from '@nocobase/i18n/client';
|
|
2
|
+
import { cn } from 'cn';
|
|
3
|
+
import { Loader2Icon } from 'lucide-react';
|
|
4
|
+
|
|
5
|
+
function Spinner(inputProps: React.ComponentProps<'svg'>) {
|
|
6
|
+
const { t } = useTranslation();
|
|
7
|
+
const { className, ...props } = inputProps;
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<Loader2Icon
|
|
11
|
+
data-slot='spinner'
|
|
12
|
+
role='status'
|
|
13
|
+
aria-label={t('status.loading', { defaultValue: 'Loading' })}
|
|
14
|
+
className={cn('size-4 animate-spin', className)}
|
|
15
|
+
{...props}
|
|
16
|
+
/>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export { Spinner };
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { Tabs as TabsPrimitive } from '@base-ui/react/tabs';
|
|
2
|
+
import { cva, type VariantProps } from 'class-variance-authority';
|
|
3
|
+
import { cn } from 'cn';
|
|
4
|
+
|
|
5
|
+
function Tabs({
|
|
6
|
+
className,
|
|
7
|
+
orientation = 'horizontal',
|
|
8
|
+
...props
|
|
9
|
+
}: TabsPrimitive.Root.Props) {
|
|
10
|
+
return (
|
|
11
|
+
<TabsPrimitive.Root
|
|
12
|
+
data-slot='tabs'
|
|
13
|
+
data-orientation={orientation}
|
|
14
|
+
className={cn(
|
|
15
|
+
'group/tabs flex gap-2 data-horizontal:flex-col',
|
|
16
|
+
className,
|
|
17
|
+
)}
|
|
18
|
+
{...props}
|
|
19
|
+
/>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const tabsListVariants = cva(
|
|
24
|
+
'group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground group-data-horizontal/tabs:h-8 group-data-vertical/tabs:h-fit group-data-vertical/tabs:flex-col data-[variant=line]:rounded-none',
|
|
25
|
+
{
|
|
26
|
+
variants: {
|
|
27
|
+
variant: {
|
|
28
|
+
default: 'bg-muted',
|
|
29
|
+
line: 'gap-1 bg-transparent',
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
defaultVariants: {
|
|
33
|
+
variant: 'default',
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
function TabsList({
|
|
39
|
+
className,
|
|
40
|
+
variant = 'default',
|
|
41
|
+
...props
|
|
42
|
+
}: TabsPrimitive.List.Props & VariantProps<typeof tabsListVariants>) {
|
|
43
|
+
return (
|
|
44
|
+
<TabsPrimitive.List
|
|
45
|
+
data-slot='tabs-list'
|
|
46
|
+
data-variant={variant}
|
|
47
|
+
className={cn(tabsListVariants({ variant }), className)}
|
|
48
|
+
{...props}
|
|
49
|
+
/>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function TabsTrigger({ className, ...props }: TabsPrimitive.Tab.Props) {
|
|
54
|
+
return (
|
|
55
|
+
<TabsPrimitive.Tab
|
|
56
|
+
data-slot='tabs-trigger'
|
|
57
|
+
className={cn(
|
|
58
|
+
"relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-1.5 py-0.5 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-vertical/tabs:w-full group-data-vertical/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 has-data-[icon=inline-end]:pr-1 has-data-[icon=inline-start]:pl-1 aria-disabled:pointer-events-none aria-disabled:opacity-50 dark:text-muted-foreground dark:hover:text-foreground group-data-[variant=default]/tabs-list:data-active:shadow-sm group-data-[variant=line]/tabs-list:data-active:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
59
|
+
'group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent',
|
|
60
|
+
'data-active:bg-background data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 dark:data-active:text-foreground',
|
|
61
|
+
'after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-horizontal/tabs:after:inset-x-0 group-data-horizontal/tabs:after:bottom-[-5px] group-data-horizontal/tabs:after:h-0.5 group-data-vertical/tabs:after:inset-y-0 group-data-vertical/tabs:after:-right-1 group-data-vertical/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100',
|
|
62
|
+
className,
|
|
63
|
+
)}
|
|
64
|
+
{...props}
|
|
65
|
+
/>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function TabsContent({ className, ...props }: TabsPrimitive.Panel.Props) {
|
|
70
|
+
return (
|
|
71
|
+
<TabsPrimitive.Panel
|
|
72
|
+
data-slot='tabs-content'
|
|
73
|
+
className={cn('flex-1 text-sm outline-none', className)}
|
|
74
|
+
{...props}
|
|
75
|
+
/>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { cn } from 'cn';
|
|
3
|
+
|
|
4
|
+
function Textarea({ className, ...props }: React.ComponentProps<'textarea'>) {
|
|
5
|
+
return (
|
|
6
|
+
<textarea
|
|
7
|
+
data-slot='textarea'
|
|
8
|
+
className={cn(
|
|
9
|
+
'flex field-sizing-content min-h-16 w-full rounded-lg border border-input bg-transparent px-2.5 py-2 text-base transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40',
|
|
10
|
+
className,
|
|
11
|
+
)}
|
|
12
|
+
{...props}
|
|
13
|
+
/>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export { Textarea };
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useTranslation } from '@nocobase/i18n/client';
|
|
4
|
+
import * as React from 'react';
|
|
5
|
+
import { Toast as ToastPrimitive } from '@base-ui/react/toast';
|
|
6
|
+
import { cn } from 'cn';
|
|
7
|
+
|
|
8
|
+
import { Button } from '#components/ui/button';
|
|
9
|
+
import {
|
|
10
|
+
XIcon,
|
|
11
|
+
CircleCheckIcon,
|
|
12
|
+
InfoIcon,
|
|
13
|
+
TriangleAlertIcon,
|
|
14
|
+
OctagonXIcon,
|
|
15
|
+
Loader2Icon,
|
|
16
|
+
} from 'lucide-react';
|
|
17
|
+
|
|
18
|
+
const toast = ToastPrimitive.createToastManager();
|
|
19
|
+
|
|
20
|
+
function ToastProvider({ ...props }: ToastPrimitive.Provider.Props) {
|
|
21
|
+
return <ToastPrimitive.Provider {...props} />;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function ToastPortal({ ...props }: ToastPrimitive.Portal.Props) {
|
|
25
|
+
return <ToastPrimitive.Portal data-slot='toast-portal' {...props} />;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function ToastViewport({ className, ...props }: ToastPrimitive.Viewport.Props) {
|
|
29
|
+
return (
|
|
30
|
+
<ToastPrimitive.Viewport
|
|
31
|
+
data-slot='toast-viewport'
|
|
32
|
+
className={cn(
|
|
33
|
+
'pointer-events-none fixed inset-x-4 bottom-4 z-50 mx-auto w-auto max-w-sm outline-none sm:right-4 sm:left-auto sm:mx-0 sm:w-full',
|
|
34
|
+
className,
|
|
35
|
+
)}
|
|
36
|
+
{...props}
|
|
37
|
+
/>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function Toast({ className, ...props }: ToastPrimitive.Root.Props) {
|
|
42
|
+
return (
|
|
43
|
+
<ToastPrimitive.Root
|
|
44
|
+
data-slot='toast'
|
|
45
|
+
className={cn(
|
|
46
|
+
'group/toast pointer-events-auto absolute right-0 bottom-0 z-[calc(1000-var(--toast-index))] w-full origin-bottom rounded-2xl border bg-popover text-popover-foreground shadow-lg will-change-transform outline-none select-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50',
|
|
47
|
+
'[--gap:0.75rem] [--height:var(--toast-frontmost-height,var(--toast-height))] [--offset-y:calc(var(--toast-offset-y)*-1+calc(var(--toast-index)*var(--gap)*-1)+var(--toast-swipe-movement-y))] [--peek:0.75rem] [--scale:calc(max(0,1-(var(--toast-index)*0.1)))] [--shrink:calc(1-var(--scale))]',
|
|
48
|
+
'h-(--height) [transform:translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)-(var(--toast-index)*var(--peek))-(var(--shrink)*var(--height))))_scale(var(--scale))] [transition:transform_500ms_cubic-bezier(0.22,1,0.36,1),opacity_500ms,height_150ms]',
|
|
49
|
+
"after:absolute after:top-full after:left-0 after:h-[calc(var(--gap)+1px)] after:w-full after:content-['']",
|
|
50
|
+
'data-expanded:h-(--toast-height) data-expanded:[transform:translateX(var(--toast-swipe-movement-x))_translateY(var(--offset-y))]',
|
|
51
|
+
'data-limited:opacity-0 data-starting-style:[transform:translateY(150%)]',
|
|
52
|
+
'[&[data-ending-style]:not([data-limited]):not([data-swipe-direction])]:[transform:translateY(150%)]',
|
|
53
|
+
'data-ending-style:data-[swipe-direction=down]:[transform:translateY(calc(var(--toast-swipe-movement-y)+150%))]',
|
|
54
|
+
'data-ending-style:data-[swipe-direction=left]:[transform:translateX(calc(var(--toast-swipe-movement-x)-150%))_translateY(var(--offset-y))]',
|
|
55
|
+
'data-ending-style:data-[swipe-direction=right]:[transform:translateX(calc(var(--toast-swipe-movement-x)+150%))_translateY(var(--offset-y))]',
|
|
56
|
+
'data-ending-style:data-[swipe-direction=up]:[transform:translateY(calc(var(--toast-swipe-movement-y)-150%))]',
|
|
57
|
+
'data-expanded:data-ending-style:data-[swipe-direction=down]:[transform:translateY(calc(var(--toast-swipe-movement-y)+150%))]',
|
|
58
|
+
'data-expanded:data-ending-style:data-[swipe-direction=left]:[transform:translateX(calc(var(--toast-swipe-movement-x)-150%))_translateY(var(--offset-y))]',
|
|
59
|
+
'data-expanded:data-ending-style:data-[swipe-direction=right]:[transform:translateX(calc(var(--toast-swipe-movement-x)+150%))_translateY(var(--offset-y))]',
|
|
60
|
+
'data-expanded:data-ending-style:data-[swipe-direction=up]:[transform:translateY(calc(var(--toast-swipe-movement-y)-150%))]',
|
|
61
|
+
className,
|
|
62
|
+
)}
|
|
63
|
+
{...props}
|
|
64
|
+
/>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function ToastContent({ className, ...props }: ToastPrimitive.Content.Props) {
|
|
69
|
+
return (
|
|
70
|
+
<ToastPrimitive.Content
|
|
71
|
+
data-slot='toast-content'
|
|
72
|
+
className={cn(
|
|
73
|
+
'flex h-full items-center gap-3 overflow-hidden p-4 transition-opacity duration-250 ease-[cubic-bezier(0.22,1,0.36,1)] data-behind:opacity-0 data-expanded:opacity-100',
|
|
74
|
+
className,
|
|
75
|
+
)}
|
|
76
|
+
{...props}
|
|
77
|
+
/>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function ToastTitle({ className, ...props }: ToastPrimitive.Title.Props) {
|
|
82
|
+
return (
|
|
83
|
+
<ToastPrimitive.Title
|
|
84
|
+
data-slot='toast-title'
|
|
85
|
+
className={cn('text-sm font-medium', className)}
|
|
86
|
+
{...props}
|
|
87
|
+
/>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function ToastDescription({
|
|
92
|
+
className,
|
|
93
|
+
...props
|
|
94
|
+
}: ToastPrimitive.Description.Props) {
|
|
95
|
+
return (
|
|
96
|
+
<ToastPrimitive.Description
|
|
97
|
+
data-slot='toast-description'
|
|
98
|
+
className={cn('text-sm text-muted-foreground', className)}
|
|
99
|
+
{...props}
|
|
100
|
+
/>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function ToastAction({
|
|
105
|
+
className,
|
|
106
|
+
render = <Button variant='outline' size='sm' />,
|
|
107
|
+
...props
|
|
108
|
+
}: ToastPrimitive.Action.Props) {
|
|
109
|
+
return (
|
|
110
|
+
<ToastPrimitive.Action
|
|
111
|
+
data-slot='toast-action'
|
|
112
|
+
render={render}
|
|
113
|
+
className={cn('shrink-0', className)}
|
|
114
|
+
{...props}
|
|
115
|
+
/>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function ToastClose({
|
|
120
|
+
className,
|
|
121
|
+
children,
|
|
122
|
+
render = <Button variant='ghost' size='icon-sm' />,
|
|
123
|
+
...props
|
|
124
|
+
}: ToastPrimitive.Close.Props) {
|
|
125
|
+
const { t } = useTranslation();
|
|
126
|
+
return (
|
|
127
|
+
<ToastPrimitive.Close
|
|
128
|
+
data-slot='toast-close'
|
|
129
|
+
aria-label={t('actions.close')}
|
|
130
|
+
render={render}
|
|
131
|
+
className={cn(
|
|
132
|
+
"relative shrink-0 text-muted-foreground after:absolute after:-inset-2 after:content-[''] hover:text-foreground",
|
|
133
|
+
className,
|
|
134
|
+
)}
|
|
135
|
+
{...props}
|
|
136
|
+
>
|
|
137
|
+
{children ?? <XIcon aria-hidden='true' />}
|
|
138
|
+
</ToastPrimitive.Close>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function ToastIcon({ type }: { type: string | undefined }) {
|
|
143
|
+
let icon: React.ReactNode = null;
|
|
144
|
+
|
|
145
|
+
if (type === 'success') {
|
|
146
|
+
icon = <CircleCheckIcon aria-hidden='true' />;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (type === 'info') {
|
|
150
|
+
icon = <InfoIcon aria-hidden='true' />;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (type === 'warning') {
|
|
154
|
+
icon = <TriangleAlertIcon aria-hidden='true' />;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (type === 'error') {
|
|
158
|
+
icon = <OctagonXIcon className='text-destructive' aria-hidden='true' />;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (type === 'loading') {
|
|
162
|
+
icon = <Loader2Icon className='animate-spin' aria-hidden='true' />;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (!icon) {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<span
|
|
171
|
+
data-slot='toast-icon'
|
|
172
|
+
className="shrink-0 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4"
|
|
173
|
+
>
|
|
174
|
+
{icon}
|
|
175
|
+
</span>
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function ToastList() {
|
|
180
|
+
const { toasts } = ToastPrimitive.useToastManager();
|
|
181
|
+
|
|
182
|
+
return toasts.map((toastItem) => (
|
|
183
|
+
<Toast key={toastItem.id} toast={toastItem}>
|
|
184
|
+
<ToastContent>
|
|
185
|
+
<ToastIcon type={toastItem.type} />
|
|
186
|
+
<div className='flex min-w-0 flex-1 flex-col gap-1'>
|
|
187
|
+
<ToastTitle />
|
|
188
|
+
<ToastDescription />
|
|
189
|
+
</div>
|
|
190
|
+
<ToastAction />
|
|
191
|
+
<ToastClose />
|
|
192
|
+
</ToastContent>
|
|
193
|
+
</Toast>
|
|
194
|
+
));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function Toaster({
|
|
198
|
+
children,
|
|
199
|
+
toastManager = toast,
|
|
200
|
+
...props
|
|
201
|
+
}: ToastPrimitive.Provider.Props) {
|
|
202
|
+
return (
|
|
203
|
+
<ToastProvider toastManager={toastManager} {...props}>
|
|
204
|
+
{children}
|
|
205
|
+
<ToastPortal>
|
|
206
|
+
<ToastViewport>
|
|
207
|
+
<ToastList />
|
|
208
|
+
</ToastViewport>
|
|
209
|
+
</ToastPortal>
|
|
210
|
+
</ToastProvider>
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const createToastManager = ToastPrimitive.createToastManager;
|
|
215
|
+
const useToastManager = ToastPrimitive.useToastManager;
|
|
216
|
+
|
|
217
|
+
export {
|
|
218
|
+
Toaster,
|
|
219
|
+
Toast,
|
|
220
|
+
ToastAction,
|
|
221
|
+
ToastClose,
|
|
222
|
+
ToastContent,
|
|
223
|
+
ToastDescription,
|
|
224
|
+
ToastPortal,
|
|
225
|
+
ToastProvider,
|
|
226
|
+
ToastTitle,
|
|
227
|
+
ToastViewport,
|
|
228
|
+
createToastManager,
|
|
229
|
+
toast,
|
|
230
|
+
useToastManager,
|
|
231
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Tooltip as TooltipPrimitive } from '@base-ui/react/tooltip';
|
|
2
|
+
import { cn } from 'cn';
|
|
3
|
+
|
|
4
|
+
function TooltipProvider({
|
|
5
|
+
delay = 0,
|
|
6
|
+
...props
|
|
7
|
+
}: TooltipPrimitive.Provider.Props) {
|
|
8
|
+
return (
|
|
9
|
+
<TooltipPrimitive.Provider
|
|
10
|
+
data-slot='tooltip-provider'
|
|
11
|
+
delay={delay}
|
|
12
|
+
{...props}
|
|
13
|
+
/>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function Tooltip({ ...props }: TooltipPrimitive.Root.Props) {
|
|
18
|
+
return <TooltipPrimitive.Root data-slot='tooltip' {...props} />;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function TooltipTrigger({ ...props }: TooltipPrimitive.Trigger.Props) {
|
|
22
|
+
return <TooltipPrimitive.Trigger data-slot='tooltip-trigger' {...props} />;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function TooltipContent({
|
|
26
|
+
className,
|
|
27
|
+
side = 'top',
|
|
28
|
+
sideOffset = 4,
|
|
29
|
+
align = 'center',
|
|
30
|
+
alignOffset = 0,
|
|
31
|
+
children,
|
|
32
|
+
...props
|
|
33
|
+
}: TooltipPrimitive.Popup.Props &
|
|
34
|
+
Pick<
|
|
35
|
+
TooltipPrimitive.Positioner.Props,
|
|
36
|
+
'align' | 'alignOffset' | 'side' | 'sideOffset'
|
|
37
|
+
>) {
|
|
38
|
+
return (
|
|
39
|
+
<TooltipPrimitive.Portal>
|
|
40
|
+
<TooltipPrimitive.Positioner
|
|
41
|
+
align={align}
|
|
42
|
+
alignOffset={alignOffset}
|
|
43
|
+
side={side}
|
|
44
|
+
sideOffset={sideOffset}
|
|
45
|
+
className='isolate z-50'
|
|
46
|
+
>
|
|
47
|
+
<TooltipPrimitive.Popup
|
|
48
|
+
data-slot='tooltip-content'
|
|
49
|
+
className={cn(
|
|
50
|
+
'z-50 inline-flex w-fit max-w-xs origin-(--transform-origin) items-center gap-1.5 rounded-md bg-foreground px-3 py-1.5 text-xs text-background has-data-[slot=kbd]:pr-1.5 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-sm data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95',
|
|
51
|
+
className,
|
|
52
|
+
)}
|
|
53
|
+
{...props}
|
|
54
|
+
>
|
|
55
|
+
{children}
|
|
56
|
+
<TooltipPrimitive.Arrow className='z-50 size-2.5 translate-y-[calc(-50%-2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground data-[side=bottom]:top-1 data-[side=inline-end]:top-1/2! data-[side=inline-end]:-left-1 data-[side=inline-end]:-translate-y-1/2 data-[side=inline-start]:top-1/2! data-[side=inline-start]:-right-1 data-[side=inline-start]:-translate-y-1/2 data-[side=left]:top-1/2! data-[side=left]:-right-1 data-[side=left]:-translate-y-1/2 data-[side=right]:top-1/2! data-[side=right]:-left-1 data-[side=right]:-translate-y-1/2 data-[side=top]:-bottom-2.5' />
|
|
57
|
+
</TooltipPrimitive.Popup>
|
|
58
|
+
</TooltipPrimitive.Positioner>
|
|
59
|
+
</TooltipPrimitive.Portal>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { createContext, useContext, type Context } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface RouteOverlayContextValue {
|
|
4
|
+
/** Runs `beforeClose`, then navigates to the parent route unless it declined. Concurrent calls share one request. */
|
|
5
|
+
readonly close: () => Promise<void>;
|
|
6
|
+
/** True while a close request for the current location is pending. */
|
|
7
|
+
readonly isClosing: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Internal context shared by both route overlay components. */
|
|
11
|
+
export const RouteOverlayContext: Context<RouteOverlayContextValue | null> =
|
|
12
|
+
createContext<RouteOverlayContextValue | null>(null);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Closes the enclosing `RouteDialog` or `RouteDrawer`. Call it from a component rendered inside the overlay — its
|
|
16
|
+
* body or its footer — never from the page that returns the overlay, which sits outside the provider.
|
|
17
|
+
*/
|
|
18
|
+
export function useRouteOverlay(): RouteOverlayContextValue {
|
|
19
|
+
const value = useContext(RouteOverlayContext);
|
|
20
|
+
if (!value) {
|
|
21
|
+
throw new Error(
|
|
22
|
+
'useRouteOverlay must be used inside RouteDialog or RouteDrawer',
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defineAppConfig,
|
|
3
|
+
resolveAppUrl,
|
|
4
|
+
type AppConfigFactory,
|
|
5
|
+
} from '@nocobase/app-client';
|
|
6
|
+
|
|
7
|
+
const api: AppConfigFactory<{ baseURL: string }> = defineAppConfig(
|
|
8
|
+
(_runtime) => ({
|
|
9
|
+
baseURL: resolveAppUrl('/api'),
|
|
10
|
+
}),
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
export default api;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { defineAppConfig, type AppConfigFactory } from '@nocobase/app-client';
|
|
2
|
+
import { apiKeyClient } from '@nocobase/app-plugin-api-keys/client';
|
|
3
|
+
import type { AuthConfig } from '@nocobase/app-plugin-authentication/client';
|
|
4
|
+
import {
|
|
5
|
+
deviceAuthorizationClient,
|
|
6
|
+
usernameClient,
|
|
7
|
+
} from 'better-auth/client/plugins';
|
|
8
|
+
|
|
9
|
+
const auth: AppConfigFactory<AuthConfig> = defineAppConfig((_runtime) => ({
|
|
10
|
+
// `deviceAuthorizationClient()` serves the `/device` page (`pages/auth/device.tsx`), where people approve a CLI's sign-in.
|
|
11
|
+
plugins: [
|
|
12
|
+
usernameClient({ displayUsername: false }),
|
|
13
|
+
apiKeyClient(),
|
|
14
|
+
deviceAuthorizationClient(),
|
|
15
|
+
],
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
export default auth;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import app from './app.js';
|
|
2
|
+
import api from './api.js';
|
|
3
|
+
import { defaultAppConfigs, type AppConfigFactory } from '@nocobase/app-client';
|
|
4
|
+
import type { AuthConfig } from '@nocobase/app-plugin-authentication/client';
|
|
5
|
+
import auth from './auth.js';
|
|
6
|
+
|
|
7
|
+
const defaultConfigs: AppConfigFactory<{
|
|
8
|
+
auth: AuthConfig;
|
|
9
|
+
app: ReturnType<typeof app>;
|
|
10
|
+
api: ReturnType<typeof api>;
|
|
11
|
+
}> = defaultAppConfigs({
|
|
12
|
+
auth,
|
|
13
|
+
app,
|
|
14
|
+
api,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export default defaultConfigs;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
export function errorId(id: string): string {
|
|
4
|
+
return `${id}-error`;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/** The props that mark a control invalid and point it at its error message. */
|
|
8
|
+
export function invalidProps(
|
|
9
|
+
id: string,
|
|
10
|
+
error: ReactNode,
|
|
11
|
+
): { 'aria-invalid'?: true; 'aria-describedby'?: string } {
|
|
12
|
+
return error ? { 'aria-describedby': errorId(id), 'aria-invalid': true } : {};
|
|
13
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { CircleAlert, CircleCheck } from 'lucide-react';
|
|
2
|
+
import type { ReactElement, ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
import { Alert, AlertDescription } from '#components/ui/alert';
|
|
5
|
+
import {
|
|
6
|
+
Field,
|
|
7
|
+
FieldDescription,
|
|
8
|
+
FieldError,
|
|
9
|
+
FieldLabel,
|
|
10
|
+
} from '#components/ui/field';
|
|
11
|
+
|
|
12
|
+
import { errorId } from './field-props.js';
|
|
13
|
+
|
|
14
|
+
export interface AuthFieldProps {
|
|
15
|
+
readonly id: string;
|
|
16
|
+
readonly label: ReactNode;
|
|
17
|
+
/** Shown at the end of the label row, such as a "Forgot password?" link. */
|
|
18
|
+
readonly labelAside?: ReactNode;
|
|
19
|
+
/** Shown under the control and its error, at the end of the row, such as a "Forgot password?" link. It follows the control in tab order. */
|
|
20
|
+
readonly below?: ReactNode;
|
|
21
|
+
readonly error?: ReactNode;
|
|
22
|
+
readonly children: ReactNode;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** A label, its control and the control's error message. The control takes `invalidProps(id, error)`. */
|
|
26
|
+
export function AuthField({
|
|
27
|
+
below,
|
|
28
|
+
children,
|
|
29
|
+
error,
|
|
30
|
+
id,
|
|
31
|
+
label,
|
|
32
|
+
labelAside,
|
|
33
|
+
}: AuthFieldProps): ReactElement {
|
|
34
|
+
return (
|
|
35
|
+
<Field data-invalid={error ? true : undefined}>
|
|
36
|
+
<div className='flex items-center gap-2'>
|
|
37
|
+
<FieldLabel htmlFor={id}>{label}</FieldLabel>
|
|
38
|
+
{labelAside ? (
|
|
39
|
+
<span className='ml-auto text-sm [&_a]:underline-offset-4 [&_a:hover]:underline'>
|
|
40
|
+
{labelAside}
|
|
41
|
+
</span>
|
|
42
|
+
) : null}
|
|
43
|
+
</div>
|
|
44
|
+
{children}
|
|
45
|
+
{error ? <FieldError id={errorId(id)}>{error}</FieldError> : null}
|
|
46
|
+
{below ? (
|
|
47
|
+
<div className='flex justify-end text-sm text-muted-foreground [&_a]:underline-offset-4 [&_a:hover]:text-foreground [&_a:hover]:underline'>
|
|
48
|
+
{below}
|
|
49
|
+
</div>
|
|
50
|
+
) : null}
|
|
51
|
+
</Field>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface AuthFormStatusProps {
|
|
56
|
+
readonly children: ReactNode;
|
|
57
|
+
readonly type: 'error' | 'success';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** A message about the whole form: an alert for an error, a status for a success. */
|
|
61
|
+
export function AuthFormStatus({
|
|
62
|
+
children,
|
|
63
|
+
type,
|
|
64
|
+
}: AuthFormStatusProps): ReactElement {
|
|
65
|
+
const error = type === 'error';
|
|
66
|
+
return (
|
|
67
|
+
<Alert
|
|
68
|
+
role={error ? 'alert' : 'status'}
|
|
69
|
+
variant={error ? 'destructive' : 'default'}
|
|
70
|
+
>
|
|
71
|
+
{error ? (
|
|
72
|
+
<CircleAlert aria-hidden='true' />
|
|
73
|
+
) : (
|
|
74
|
+
<CircleCheck aria-hidden='true' />
|
|
75
|
+
)}
|
|
76
|
+
<AlertDescription>{children}</AlertDescription>
|
|
77
|
+
</Alert>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface AuthFormSubmitProps {
|
|
82
|
+
/** The submit button. */
|
|
83
|
+
readonly children: ReactNode;
|
|
84
|
+
/** A centred line of links under the button, such as "Don't have an account? Sign up". */
|
|
85
|
+
readonly footer?: ReactNode;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** The submit button and the footer line beneath it. */
|
|
89
|
+
export function AuthFormSubmit({
|
|
90
|
+
children,
|
|
91
|
+
footer,
|
|
92
|
+
}: AuthFormSubmitProps): ReactElement {
|
|
93
|
+
return (
|
|
94
|
+
<Field>
|
|
95
|
+
{children}
|
|
96
|
+
{footer ? (
|
|
97
|
+
<FieldDescription className='text-center'>{footer}</FieldDescription>
|
|
98
|
+
) : null}
|
|
99
|
+
</Field>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Eye, EyeOff } from 'lucide-react';
|
|
2
|
+
import { useState, type ComponentProps, type ReactElement } from 'react';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
InputGroup,
|
|
6
|
+
InputGroupAddon,
|
|
7
|
+
InputGroupButton,
|
|
8
|
+
InputGroupInput,
|
|
9
|
+
} from '#components/ui/input-group';
|
|
10
|
+
|
|
11
|
+
export interface PasswordInputProps extends Omit<
|
|
12
|
+
ComponentProps<'input'>,
|
|
13
|
+
'type'
|
|
14
|
+
> {
|
|
15
|
+
readonly showLabel?: string;
|
|
16
|
+
readonly hideLabel?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** A password input with a button that shows or hides what was typed. */
|
|
20
|
+
export function PasswordInput({
|
|
21
|
+
hideLabel = 'Hide password',
|
|
22
|
+
showLabel = 'Show password',
|
|
23
|
+
...props
|
|
24
|
+
}: PasswordInputProps): ReactElement {
|
|
25
|
+
const [visible, setVisible] = useState(false);
|
|
26
|
+
return (
|
|
27
|
+
<InputGroup>
|
|
28
|
+
<InputGroupInput {...props} type={visible ? 'text' : 'password'} />
|
|
29
|
+
<InputGroupAddon align='inline-end'>
|
|
30
|
+
<InputGroupButton
|
|
31
|
+
aria-label={visible ? hideLabel : showLabel}
|
|
32
|
+
aria-pressed={visible}
|
|
33
|
+
onClick={() => setVisible((value) => !value)}
|
|
34
|
+
size='icon-xs'
|
|
35
|
+
>
|
|
36
|
+
{visible ? <EyeOff aria-hidden='true' /> : <Eye aria-hidden='true' />}
|
|
37
|
+
</InputGroupButton>
|
|
38
|
+
</InputGroupAddon>
|
|
39
|
+
</InputGroup>
|
|
40
|
+
);
|
|
41
|
+
}
|