@nocobase/app-template-default 0.0.0-stage → 1.0.0-beta.57

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (250) hide show
  1. package/.cursor/mcp.json +8 -0
  2. package/.mcp.json +8 -0
  3. package/.prettierignore +20 -0
  4. package/.vscode/mcp.json +8 -0
  5. package/AGENTS.md +432 -0
  6. package/CLAUDE.md +9 -0
  7. package/Dockerfile +132 -0
  8. package/Dockerfile.dockerignore +45 -0
  9. package/README.MD +481 -0
  10. package/cli/plugins.ts +14 -0
  11. package/client/agent-annotations-host.ts +19 -0
  12. package/client/app.ts +38 -0
  13. package/client/components/back-button.tsx +49 -0
  14. package/client/components/breadcrumbs.tsx +167 -0
  15. package/client/components/inbox-badge.ts +49 -0
  16. package/client/components/inbox-button.tsx +132 -0
  17. package/client/components/inbox-header-button.tsx +25 -0
  18. package/client/components/loading.tsx +35 -0
  19. package/client/components/page-container.tsx +24 -0
  20. package/client/components/page-header.tsx +33 -0
  21. package/client/components/route-child-page.tsx +121 -0
  22. package/client/components/route-dialog.tsx +10 -0
  23. package/client/components/route-drawer.tsx +10 -0
  24. package/client/components/route-overlay.tsx +190 -0
  25. package/client/components/ui/alert.tsx +75 -0
  26. package/client/components/ui/badge.tsx +51 -0
  27. package/client/components/ui/breadcrumb.tsx +121 -0
  28. package/client/components/ui/button.tsx +58 -0
  29. package/client/components/ui/collapsible.tsx +19 -0
  30. package/client/components/ui/context-menu.tsx +271 -0
  31. package/client/components/ui/dialog.tsx +160 -0
  32. package/client/components/ui/dropdown-menu.tsx +270 -0
  33. package/client/components/ui/empty.tsx +103 -0
  34. package/client/components/ui/field.tsx +236 -0
  35. package/client/components/ui/input-group.tsx +158 -0
  36. package/client/components/ui/input.tsx +20 -0
  37. package/client/components/ui/kbd.tsx +26 -0
  38. package/client/components/ui/label.tsx +18 -0
  39. package/client/components/ui/popover.tsx +89 -0
  40. package/client/components/ui/select.tsx +200 -0
  41. package/client/components/ui/separator.tsx +22 -0
  42. package/client/components/ui/sheet.tsx +137 -0
  43. package/client/components/ui/sidebar.tsx +723 -0
  44. package/client/components/ui/skeleton.tsx +13 -0
  45. package/client/components/ui/spinner.tsx +20 -0
  46. package/client/components/ui/tabs.tsx +79 -0
  47. package/client/components/ui/textarea.tsx +17 -0
  48. package/client/components/ui/toast.tsx +231 -0
  49. package/client/components/ui/tooltip.tsx +63 -0
  50. package/client/components/use-route-overlay.ts +26 -0
  51. package/client/config/api.ts +13 -0
  52. package/client/config/app.ts +8 -0
  53. package/client/config/auth.ts +18 -0
  54. package/client/config/index.ts +17 -0
  55. package/client/extensions/nocobase-auth-forms/field-props.ts +13 -0
  56. package/client/extensions/nocobase-auth-forms/form-parts.tsx +101 -0
  57. package/client/extensions/nocobase-auth-forms/password-input.tsx +41 -0
  58. package/client/extensions/nocobase-auth-forms/password-login-form.tsx +123 -0
  59. package/client/extensions/nocobase-auth-forms/password-registration-form.tsx +193 -0
  60. package/client/extensions/nocobase-auth-forms/password-reset-form.tsx +132 -0
  61. package/client/extensions/nocobase-auth-forms/password-reset-request-form.tsx +99 -0
  62. package/client/extensions/nocobase-auth-methods/auth-methods.tsx +170 -0
  63. package/client/extensions/nocobase-auth-split-layout/auth-split-layout.tsx +126 -0
  64. package/client/extensions/nocobase-device-approval/device-approval.tsx +277 -0
  65. package/client/extensions/nocobase-device-approval/locales/en-US.ts +55 -0
  66. package/client/extensions/nocobase-device-approval/locales/zh-CN.ts +27 -0
  67. package/client/extensions/nocobase-device-approval/use-device-approval.ts +176 -0
  68. package/client/extensions/nocobase-file-component-ui/README.md +13 -0
  69. package/client/extensions/nocobase-file-component-ui/components/file-list.tsx +147 -0
  70. package/client/extensions/nocobase-file-component-ui/components/file-preview-dialog.tsx +263 -0
  71. package/client/extensions/nocobase-file-component-ui/components/file-preview-field.tsx +68 -0
  72. package/client/extensions/nocobase-file-component-ui/components/file-thumbnail.tsx +73 -0
  73. package/client/extensions/nocobase-file-component-ui/components/file-upload-field.tsx +386 -0
  74. package/client/extensions/nocobase-file-component-ui/components/previewers/file-preview-content.tsx +255 -0
  75. package/client/extensions/nocobase-file-component-ui/components/previewers/office-open-xml-preview.tsx +226 -0
  76. package/client/extensions/nocobase-file-component-ui/index.ts +8 -0
  77. package/client/extensions/nocobase-file-component-ui/lib/file-preview.ts +194 -0
  78. package/client/extensions/nocobase-file-component-ui/lib/file-url.ts +22 -0
  79. package/client/extensions/nocobase-file-component-ui/lib/message-key.ts +18 -0
  80. package/client/extensions/nocobase-file-component-ui/types.ts +69 -0
  81. package/client/extensions/nocobase-inbox/decision-actions-bar.tsx +140 -0
  82. package/client/extensions/nocobase-inbox/frame.ts +31 -0
  83. package/client/extensions/nocobase-inbox/inbox-actions.ts +21 -0
  84. package/client/extensions/nocobase-inbox/inbox-detail.tsx +386 -0
  85. package/client/extensions/nocobase-inbox/inbox-item.tsx +274 -0
  86. package/client/extensions/nocobase-inbox/inbox-list.tsx +433 -0
  87. package/client/extensions/nocobase-inbox/inbox-page.tsx +545 -0
  88. package/client/extensions/nocobase-inbox/locales/en-US.ts +111 -0
  89. package/client/extensions/nocobase-inbox/locales/zh-CN.ts +57 -0
  90. package/client/extensions/nocobase-inbox/model.ts +428 -0
  91. package/client/extensions/nocobase-inbox/registry-scope.tsx +49 -0
  92. package/client/extensions/nocobase-inbox/registry.ts +303 -0
  93. package/client/extensions/nocobase-inbox/source.ts +117 -0
  94. package/client/hooks/use-mobile.ts +21 -0
  95. package/client/index.tsx +60 -0
  96. package/client/layouts/app-layout.tsx +105 -0
  97. package/client/layouts/components/app-brand.tsx +49 -0
  98. package/client/layouts/components/app-sidebar.tsx +196 -0
  99. package/client/layouts/components/header-actions.tsx +50 -0
  100. package/client/layouts/components/language-switcher.tsx +95 -0
  101. package/client/layouts/components/layout-header.tsx +17 -0
  102. package/client/layouts/components/navigation-menu.tsx +411 -0
  103. package/client/layouts/components/sidebar-footer.tsx +107 -0
  104. package/client/layouts/components/surface-empty.tsx +35 -0
  105. package/client/layouts/components/user-menu.tsx +127 -0
  106. package/client/layouts/index.ts +2 -0
  107. package/client/layouts/settings-layout.tsx +172 -0
  108. package/client/layouts/use-sidebar-preference.ts +57 -0
  109. package/client/lib/constants.ts +5 -0
  110. package/client/lib/toaster.ts +56 -0
  111. package/client/lib/utils.ts +1 -0
  112. package/client/locales/en-US.ts +178 -0
  113. package/client/locales/index.ts +9 -0
  114. package/client/locales/zh-CN.ts +161 -0
  115. package/client/pages/auth/device.tsx +41 -0
  116. package/client/pages/auth/forgot-password.tsx +39 -0
  117. package/client/pages/auth/login.tsx +66 -0
  118. package/client/pages/auth/register.tsx +46 -0
  119. package/client/pages/auth/reset-password.tsx +38 -0
  120. package/client/pages/auth/shared.tsx +111 -0
  121. package/client/pages/home.tsx +16 -0
  122. package/client/pages/inbox.tsx +12 -0
  123. package/client/plugins.ts +41 -0
  124. package/client/react-providers.ts +27 -0
  125. package/client/route-overrides.ts +9 -0
  126. package/client/routes.ts +75 -0
  127. package/client/routing/app-router.tsx +127 -0
  128. package/client/routing/client-route.tsx +129 -0
  129. package/client/routing/route-context.tsx +85 -0
  130. package/client/routing/route-navigation.ts +153 -0
  131. package/client/routing/route-tree.tsx +42 -0
  132. package/client/routing/standalone-page-layout.tsx +15 -0
  133. package/client/runtime.ts +26 -0
  134. package/client/service-provider.ts +40 -0
  135. package/client/source-extensions.ts +19 -0
  136. package/client/startup.tsx +22 -0
  137. package/client/styles.css +122 -0
  138. package/client/theme/index.ts +4 -0
  139. package/client/theme/theme-context.ts +12 -0
  140. package/client/theme/theme-preferences.ts +84 -0
  141. package/client/theme/theme-presets.ts +7 -0
  142. package/client/theme/theme-provider.tsx +106 -0
  143. package/client/theme/theme-settings.tsx +176 -0
  144. package/client/theme/themes/compact.css +140 -0
  145. package/client/theme/themes/default.css +117 -0
  146. package/client/vite-env.d.ts +5 -0
  147. package/components.json +27 -0
  148. package/config.example.yml +172 -0
  149. package/database/main/migrations/.gitkeep +0 -0
  150. package/database/main/migrations/202610060001_create_device_code.ts +37 -0
  151. package/database/main/seeds/.gitkeep +0 -0
  152. package/database/tsconfig.json +4 -0
  153. package/ecosystem.config.js +35 -0
  154. package/eslint.config.js +24 -0
  155. package/index.html +100 -0
  156. package/package.json +200 -3
  157. package/playwright.config.ts +10 -0
  158. package/public/assets/favicon.ico +0 -0
  159. package/public/assets/logo-dark.png +0 -0
  160. package/public/assets/logo-mark-dark.png +0 -0
  161. package/public/assets/logo-mark.png +0 -0
  162. package/public/assets/logo.png +0 -0
  163. package/server/app.ts +50 -0
  164. package/server/config/ai.ts +27 -0
  165. package/server/config/api.ts +8 -0
  166. package/server/config/app.ts +40 -0
  167. package/server/config/auth.ts +36 -0
  168. package/server/config/authorization.ts +18 -0
  169. package/server/config/caching.ts +21 -0
  170. package/server/config/database.ts +22 -0
  171. package/server/config/drive.ts +30 -0
  172. package/server/config/i18n.ts +17 -0
  173. package/server/config/index.ts +73 -0
  174. package/server/config/jobs.ts +27 -0
  175. package/server/config/logging.ts +23 -0
  176. package/server/config/notification.ts +11 -0
  177. package/server/config/queue.ts +28 -0
  178. package/server/config/scheduler.ts +23 -0
  179. package/server/config/secrets.ts +10 -0
  180. package/server/config/server.ts +26 -0
  181. package/server/config/session.ts +60 -0
  182. package/server/config/snowflake.ts +21 -0
  183. package/server/config/spa.ts +24 -0
  184. package/server/config/users.ts +11 -0
  185. package/server/config/workflow.ts +24 -0
  186. package/server/config.ts +24 -0
  187. package/server/embedded.ts +23 -0
  188. package/server/locales/en-US.ts +10 -0
  189. package/server/locales/index.ts +10 -0
  190. package/server/locales/zh-CN.ts +5 -0
  191. package/server/plugins.ts +41 -0
  192. package/server/providers/index.ts +5 -0
  193. package/server/routes/index.ts +6 -0
  194. package/server/runtime.ts +24 -0
  195. package/server/standalone.ts +29 -0
  196. package/server/tsconfig.json +4 -0
  197. package/tailwind.config.mjs +59 -0
  198. package/tests/components/app-layout-routes.test.tsx +132 -0
  199. package/tests/components/app-title.test.tsx +57 -0
  200. package/tests/components/auth-i18n.test.tsx +87 -0
  201. package/tests/components/back-button.test.tsx +80 -0
  202. package/tests/components/file-preview.test.tsx +101 -0
  203. package/tests/components/header-hover.test.tsx +87 -0
  204. package/tests/components/inbox.test.tsx +139 -0
  205. package/tests/components/language-switcher.test.tsx +298 -0
  206. package/tests/components/layout-containers.test.tsx +15 -0
  207. package/tests/components/layout-i18n.test.tsx +187 -0
  208. package/tests/components/loading.test.tsx +25 -0
  209. package/tests/components/navigation-menu.test.tsx +355 -0
  210. package/tests/components/page-harness.test.tsx +318 -0
  211. package/tests/components/primitive-labels.test.tsx +80 -0
  212. package/tests/components/sidebar-permissions.test.tsx +110 -0
  213. package/tests/components/sidebar-preference.test.tsx +42 -0
  214. package/tests/fixtures/theme-tokens.html +12 -0
  215. package/tests/fixtures/theme-tokens.tsx +111 -0
  216. package/tests/logic/account-permissions.test.tsx +204 -0
  217. package/tests/logic/app-locale-coverage.test.ts +136 -0
  218. package/tests/logic/app-server.test.ts +1589 -0
  219. package/tests/logic/asset-url-build.test.ts +106 -0
  220. package/tests/logic/client-auth.test.tsx +122 -0
  221. package/tests/logic/client-plugin-registry.test.ts +63 -0
  222. package/tests/logic/client-route-recovery.test.tsx +80 -0
  223. package/tests/logic/client-routes.test.ts +126 -0
  224. package/tests/logic/client-runtime.test.ts +112 -0
  225. package/tests/logic/client-settings.test.tsx +841 -0
  226. package/tests/logic/client-shell.test.tsx +466 -0
  227. package/tests/logic/client-theme.test.tsx +441 -0
  228. package/tests/logic/config.test.ts +197 -0
  229. package/tests/logic/dev-client-watch.test.ts +108 -0
  230. package/tests/logic/dev-routes-production-build.test.ts +168 -0
  231. package/tests/logic/device-code-migration.test.ts +70 -0
  232. package/tests/logic/lifecycle.test.ts +221 -0
  233. package/tests/logic/plugin-commands.test.ts +69 -0
  234. package/tests/logic/plugin-page-authz.test.ts +76 -0
  235. package/tests/logic/route-navigation.test.ts +66 -0
  236. package/tests/logic/route-overlay.test.tsx +353 -0
  237. package/tests/logic/server-imports.test.ts +88 -0
  238. package/tests/logic/tailwind-sources.test.ts +151 -0
  239. package/tests/logic/template-package-name.test.ts +88 -0
  240. package/tests/logic/theme-preferences.test.ts +86 -0
  241. package/tests/logic/theme-tokens.test.ts +248 -0
  242. package/tests/logic/toaster.test.tsx +108 -0
  243. package/tests/logic/user-menu-sign-out.test.tsx +98 -0
  244. package/tests/setup/client-config.ts +27 -0
  245. package/tsconfig.json +11 -0
  246. package/tsconfig.node.json +12 -0
  247. package/tsconfig.server.json +19 -0
  248. package/vite.config.ts +92 -0
  249. package/vitest.config.ts +37 -0
  250. 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,8 @@
1
+ import { defineAppConfig, type AppConfigFactory } from '@nocobase/app-client';
2
+
3
+ const app: AppConfigFactory<{ title: string }> = defineAppConfig(
4
+ (_runtime) => ({
5
+ title: 'NocoBase',
6
+ }),
7
+ );
8
+ export default app;
@@ -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
+ }