@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.
Files changed (249) 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 +434 -0
  6. package/CLAUDE.md +9 -0
  7. package/Dockerfile +132 -0
  8. package/Dockerfile.dockerignore +45 -0
  9. package/README.MD +485 -0
  10. package/cli/plugins.ts +8 -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 +37 -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 +156 -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 +198 -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/api.ts +8 -0
  165. package/server/config/app.ts +40 -0
  166. package/server/config/auth.ts +36 -0
  167. package/server/config/authorization.ts +18 -0
  168. package/server/config/caching.ts +21 -0
  169. package/server/config/database.ts +22 -0
  170. package/server/config/drive.ts +30 -0
  171. package/server/config/i18n.ts +17 -0
  172. package/server/config/index.ts +67 -0
  173. package/server/config/jobs.ts +27 -0
  174. package/server/config/logging.ts +23 -0
  175. package/server/config/notification.ts +11 -0
  176. package/server/config/queue.ts +28 -0
  177. package/server/config/scheduler.ts +23 -0
  178. package/server/config/secrets.ts +10 -0
  179. package/server/config/server.ts +26 -0
  180. package/server/config/session.ts +60 -0
  181. package/server/config/snowflake.ts +21 -0
  182. package/server/config/spa.ts +24 -0
  183. package/server/config/users.ts +11 -0
  184. package/server/config/workflow.ts +24 -0
  185. package/server/config.ts +24 -0
  186. package/server/embedded.ts +23 -0
  187. package/server/locales/en-US.ts +10 -0
  188. package/server/locales/index.ts +10 -0
  189. package/server/locales/zh-CN.ts +5 -0
  190. package/server/plugins.ts +37 -0
  191. package/server/providers/index.ts +5 -0
  192. package/server/routes/index.ts +6 -0
  193. package/server/runtime.ts +24 -0
  194. package/server/standalone.ts +29 -0
  195. package/server/tsconfig.json +4 -0
  196. package/tailwind.config.mjs +59 -0
  197. package/tests/components/app-layout-routes.test.tsx +132 -0
  198. package/tests/components/app-title.test.tsx +57 -0
  199. package/tests/components/auth-i18n.test.tsx +87 -0
  200. package/tests/components/back-button.test.tsx +80 -0
  201. package/tests/components/file-preview.test.tsx +101 -0
  202. package/tests/components/header-hover.test.tsx +87 -0
  203. package/tests/components/inbox.test.tsx +139 -0
  204. package/tests/components/language-switcher.test.tsx +298 -0
  205. package/tests/components/layout-containers.test.tsx +15 -0
  206. package/tests/components/layout-i18n.test.tsx +187 -0
  207. package/tests/components/loading.test.tsx +25 -0
  208. package/tests/components/navigation-menu.test.tsx +355 -0
  209. package/tests/components/page-harness.test.tsx +318 -0
  210. package/tests/components/primitive-labels.test.tsx +80 -0
  211. package/tests/components/sidebar-permissions.test.tsx +110 -0
  212. package/tests/components/sidebar-preference.test.tsx +42 -0
  213. package/tests/fixtures/theme-tokens.html +12 -0
  214. package/tests/fixtures/theme-tokens.tsx +111 -0
  215. package/tests/logic/account-permissions.test.tsx +204 -0
  216. package/tests/logic/app-locale-coverage.test.ts +136 -0
  217. package/tests/logic/app-server.test.ts +1589 -0
  218. package/tests/logic/asset-url-build.test.ts +106 -0
  219. package/tests/logic/client-auth.test.tsx +122 -0
  220. package/tests/logic/client-plugin-registry.test.ts +63 -0
  221. package/tests/logic/client-route-recovery.test.tsx +80 -0
  222. package/tests/logic/client-routes.test.ts +126 -0
  223. package/tests/logic/client-runtime.test.ts +112 -0
  224. package/tests/logic/client-settings.test.tsx +841 -0
  225. package/tests/logic/client-shell.test.tsx +466 -0
  226. package/tests/logic/client-theme.test.tsx +441 -0
  227. package/tests/logic/config.test.ts +197 -0
  228. package/tests/logic/dev-client-watch.test.ts +108 -0
  229. package/tests/logic/dev-routes-production-build.test.ts +168 -0
  230. package/tests/logic/device-code-migration.test.ts +70 -0
  231. package/tests/logic/lifecycle.test.ts +221 -0
  232. package/tests/logic/plugin-commands.test.ts +69 -0
  233. package/tests/logic/plugin-page-authz.test.ts +53 -0
  234. package/tests/logic/route-navigation.test.ts +66 -0
  235. package/tests/logic/route-overlay.test.tsx +353 -0
  236. package/tests/logic/server-imports.test.ts +88 -0
  237. package/tests/logic/tailwind-sources.test.ts +151 -0
  238. package/tests/logic/template-package-name.test.ts +88 -0
  239. package/tests/logic/theme-preferences.test.ts +86 -0
  240. package/tests/logic/theme-tokens.test.ts +248 -0
  241. package/tests/logic/toaster.test.tsx +108 -0
  242. package/tests/logic/user-menu-sign-out.test.tsx +98 -0
  243. package/tests/setup/client-config.ts +27 -0
  244. package/tsconfig.json +11 -0
  245. package/tsconfig.node.json +12 -0
  246. package/tsconfig.server.json +19 -0
  247. package/vite.config.ts +92 -0
  248. package/vitest.config.ts +44 -0
  249. package/README.md +0 -3
@@ -0,0 +1,196 @@
1
+ import { useTranslation } from '@nocobase/i18n/client';
2
+ import { PanelLeft, X } from 'lucide-react';
3
+ import {
4
+ useEffect,
5
+ type CSSProperties,
6
+ type ReactElement,
7
+ type ReactNode,
8
+ } from 'react';
9
+
10
+ import { Button } from '#components/ui/button';
11
+ import {
12
+ Sheet,
13
+ SheetContent,
14
+ SheetDescription,
15
+ SheetHeader,
16
+ SheetTitle,
17
+ } from '#components/ui/sheet';
18
+ import {
19
+ Sidebar,
20
+ SidebarContent,
21
+ SidebarHeader,
22
+ SidebarProvider,
23
+ SidebarRail,
24
+ useSidebar,
25
+ } from '#components/ui/sidebar';
26
+ import { TooltipProvider } from '#components/ui/tooltip';
27
+
28
+ import { useSidebarPreference } from '../use-sidebar-preference.js';
29
+ import { AppBrand } from './app-brand.js';
30
+
31
+ // The layouts compose shadcn's Sidebar without editing `components/ui/sidebar.tsx`, so the primitive stays as the
32
+ // shadcn CLI writes it and can be updated from the registry. Everything the template needs differently is done here.
33
+
34
+ /**
35
+ * The shell's sidebar state: shadcn's provider, with the desktop icon mode kept in the shared sidebar preference
36
+ * (`use-sidebar-preference.ts`) so every layout on this origin opens it the same way. Whether the phone's sheet is
37
+ * open stays local to the layout.
38
+ */
39
+ export function AppSidebarProvider({
40
+ children,
41
+ }: {
42
+ readonly children: ReactNode;
43
+ }): ReactElement {
44
+ const [collapsed, setCollapsed] = useSidebarPreference();
45
+ useSuppressSidebarShortcut();
46
+ return (
47
+ <SidebarProvider
48
+ open={!collapsed}
49
+ onOpenChange={(open) => setCollapsed(!open)}
50
+ className='h-svh bg-background'
51
+ // shadcn's 16rem and 3rem, in spacing units: a density preset that changes the unit scales the sidebar together
52
+ // with the items inside it (the provider's documented way to set the widths).
53
+ style={
54
+ {
55
+ '--sidebar-width': 'calc(var(--spacing) * 64)',
56
+ '--sidebar-width-icon': 'calc(var(--spacing) * 12)',
57
+ } as CSSProperties
58
+ }
59
+ >
60
+ {/* Icon-mode labels appear as soon as the pointer reaches an entry, like the header's hints. */}
61
+ <TooltipProvider delay={0}>{children}</TooltipProvider>
62
+ </SidebarProvider>
63
+ );
64
+ }
65
+
66
+ /**
67
+ * shadcn's provider toggles the sidebar on Ctrl/Cmd+B from a `window` keydown listener. The template has no such
68
+ * shortcut (it is bold in every rich-text editor), so a `document` listener in the bubble phase stops the event
69
+ * before it reaches `window`. Whatever is focused, an editor included, still receives the key first.
70
+ */
71
+ function useSuppressSidebarShortcut(): void {
72
+ useEffect(() => {
73
+ const stop = (event: KeyboardEvent) => {
74
+ if (event.key === 'b' && (event.metaKey || event.ctrlKey))
75
+ event.stopPropagation();
76
+ };
77
+ document.addEventListener('keydown', stop);
78
+ return () => document.removeEventListener('keydown', stop);
79
+ }, []);
80
+ }
81
+
82
+ /**
83
+ * The sidebar: the brand, the given navigation and an optional footer. On a desktop it is shadcn's `Sidebar` in icon
84
+ * mode; on a phone it is a Sheet rendered here with the same slots, because the primitive's own mobile sheet carries
85
+ * an untranslated title.
86
+ */
87
+ export function AppSidebar({
88
+ children,
89
+ footer,
90
+ label,
91
+ }: {
92
+ readonly children: ReactNode;
93
+ readonly footer?: ReactNode;
94
+ /** The accessible name of the sidebar, and the title of the phone's sheet. */
95
+ readonly label: string;
96
+ }): ReactElement {
97
+ const { t } = useTranslation();
98
+ const { isMobile, openMobile, setOpenMobile, state } = useSidebar();
99
+ // A sheet left open on a phone must not reappear when the window narrows again after widening.
100
+ useEffect(() => {
101
+ if (!isMobile && openMobile) setOpenMobile(false);
102
+ }, [isMobile, openMobile, setOpenMobile]);
103
+
104
+ const header = (
105
+ <SidebarHeader className='h-16 shrink-0 flex-row items-center justify-between border-b border-sidebar-border/70 px-5 group-data-[collapsible=icon]:justify-center group-data-[collapsible=icon]:px-0'>
106
+ <AppBrand compact={state === 'collapsed' && !isMobile} />
107
+ {isMobile ? (
108
+ <Button
109
+ aria-label={t('navigation.close', {
110
+ defaultValue: 'Close navigation',
111
+ })}
112
+ className='hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:border-sidebar-ring focus-visible:ring-sidebar-ring dark:hover:bg-sidebar-accent'
113
+ onClick={() => setOpenMobile(false)}
114
+ size='icon'
115
+ variant='ghost'
116
+ >
117
+ <X />
118
+ </Button>
119
+ ) : null}
120
+ </SidebarHeader>
121
+ );
122
+ const content = (
123
+ <>
124
+ {header}
125
+ {/* The primitive hides the icon mode's overflow; a long menu still scrolls there. */}
126
+ <SidebarContent className='group-data-[collapsible=icon]:overflow-y-auto'>
127
+ {children}
128
+ </SidebarContent>
129
+ {footer}
130
+ </>
131
+ );
132
+
133
+ const railLabel = t('navigation.toggle', {
134
+ defaultValue: 'Expand or collapse navigation',
135
+ });
136
+
137
+ if (isMobile)
138
+ return (
139
+ <Sheet open={openMobile} onOpenChange={setOpenMobile}>
140
+ {/* The attributes and classes of the primitive's own mobile branch, so `sidebar-*` styles apply alike. */}
141
+ <SheetContent
142
+ data-sidebar='sidebar'
143
+ data-slot='sidebar'
144
+ data-mobile='true'
145
+ className='w-(--sidebar-width) gap-0 bg-sidebar p-0 text-sidebar-foreground'
146
+ showCloseButton={false}
147
+ side='left'
148
+ style={
149
+ { '--sidebar-width': 'calc(var(--spacing) * 72)' } as CSSProperties
150
+ }
151
+ >
152
+ <SheetHeader className='sr-only'>
153
+ <SheetTitle>{label}</SheetTitle>
154
+ <SheetDescription>
155
+ {t('navigation.description', {
156
+ defaultValue: 'Go to a page of this application.',
157
+ })}
158
+ </SheetDescription>
159
+ </SheetHeader>
160
+ <div className='flex h-full w-full flex-col'>{content}</div>
161
+ </SheetContent>
162
+ </Sheet>
163
+ );
164
+
165
+ return (
166
+ <Sidebar collapsible='icon' role='complementary' aria-label={label}>
167
+ {content}
168
+ {/* The primitive's edge handle; its built-in label is untranslated, and the props given here replace it. */}
169
+ <SidebarRail aria-label={railLabel} title={railLabel} />
170
+ </Sidebar>
171
+ );
172
+ }
173
+
174
+ /** The header's sidebar button: opens the sheet on a phone, and switches the icon mode on a desktop. */
175
+ export function AppSidebarToggle(): ReactElement {
176
+ const { t } = useTranslation();
177
+ const { isMobile, state, toggleSidebar } = useSidebar();
178
+ const collapsed = state === 'collapsed';
179
+ const label = isMobile
180
+ ? t('navigation.open', { defaultValue: 'Open navigation' })
181
+ : collapsed
182
+ ? t('navigation.expand', { defaultValue: 'Expand navigation' })
183
+ : t('navigation.collapse', { defaultValue: 'Collapse navigation' });
184
+ return (
185
+ <Button
186
+ aria-label={label}
187
+ aria-pressed={isMobile ? undefined : collapsed}
188
+ className='size-9 rounded-xl text-muted-foreground hover:text-foreground'
189
+ onClick={toggleSidebar}
190
+ size='icon'
191
+ variant='ghost'
192
+ >
193
+ <PanelLeft />
194
+ </Button>
195
+ );
196
+ }
@@ -0,0 +1,50 @@
1
+ import { useTranslation } from '@nocobase/i18n/client';
2
+ import { Settings } from 'lucide-react';
3
+ import type { ReactElement } from 'react';
4
+ import { Link } from 'react-router';
5
+ import {
6
+ Tooltip,
7
+ TooltipTrigger,
8
+ TooltipContent,
9
+ TooltipProvider,
10
+ } from '#components/ui/tooltip';
11
+
12
+ import { ThemeSettings } from '../../theme/index.js';
13
+ import { UserMenu } from './user-menu.js';
14
+ import { InboxHeaderButton } from '#components/inbox-header-button';
15
+
16
+ const ACTION_LINK_CLASS =
17
+ 'inline-flex size-10 items-center justify-center rounded-xl border border-border/70 bg-background/60 text-foreground transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-3 focus-visible:ring-ring/50';
18
+
19
+ /** Keep header entries visible on their destination pages so navigation stays consistent across surfaces. */
20
+ export function HeaderActions({
21
+ showSettings,
22
+ }: {
23
+ readonly showSettings: boolean;
24
+ }): ReactElement {
25
+ const { t } = useTranslation();
26
+
27
+ return (
28
+ <TooltipProvider>
29
+ <div className='flex shrink-0 items-center gap-2'>
30
+ {/* The inbox's entry, from the UI Library; keep its unread shortcut on every authenticated surface. */}
31
+ <InboxHeaderButton />
32
+ {showSettings ? (
33
+ <Tooltip>
34
+ <TooltipTrigger
35
+ render={<Link to='/settings' className={ACTION_LINK_CLASS} />}
36
+ aria-label={t('settings.title', { defaultValue: 'Settings' })}
37
+ >
38
+ <Settings className='size-5' />
39
+ </TooltipTrigger>
40
+ <TooltipContent side='bottom'>
41
+ {t('settings.title', { defaultValue: 'Settings' })}
42
+ </TooltipContent>
43
+ </Tooltip>
44
+ ) : null}
45
+ <ThemeSettings />
46
+ <UserMenu />
47
+ </div>
48
+ </TooltipProvider>
49
+ );
50
+ }
@@ -0,0 +1,95 @@
1
+ import { useToaster } from '@nocobase/app-client';
2
+ import { useAppLocale } from '@nocobase/app-plugin-i18n/client';
3
+ import { useTranslation } from '@nocobase/i18n/client';
4
+ import { Languages } from 'lucide-react';
5
+ import type { ReactElement } from 'react';
6
+
7
+ import {
8
+ DropdownMenuRadioGroup,
9
+ DropdownMenuRadioItem,
10
+ DropdownMenuSub,
11
+ DropdownMenuSubContent,
12
+ DropdownMenuSubTrigger,
13
+ } from '#components/ui/dropdown-menu';
14
+ import { cn } from 'cn';
15
+
16
+ export interface LanguageSwitcherProps {
17
+ readonly className?: string;
18
+ }
19
+
20
+ /** Account-menu language choices; the plugin owns persistence and resource loading. */
21
+ export function LanguageSwitcher({
22
+ className,
23
+ }: LanguageSwitcherProps): ReactElement | null {
24
+ const { locale, locales, setLocale, switching } = useAppLocale();
25
+ const { t } = useTranslation();
26
+ const toaster = useToaster();
27
+
28
+ async function applyLocaleChange(value: string): Promise<void> {
29
+ const result = await setLocale(value);
30
+ if (result.fallback) {
31
+ toaster.show({
32
+ type: 'info',
33
+ title: t('notices.serverLocaleFallback', {
34
+ lng: value,
35
+ defaultValue:
36
+ 'The server does not support this language, so server messages will use English.',
37
+ }),
38
+ });
39
+ }
40
+ }
41
+
42
+ function handleLocaleChange(value: unknown): void {
43
+ if (typeof value !== 'string' || value === locale) return;
44
+
45
+ const localeChange = applyLocaleChange(value);
46
+ localeChange.catch(() => {
47
+ toaster.show({
48
+ type: 'error',
49
+ title: t('notices.languageChangeFailed', {
50
+ lng: value,
51
+ defaultValue:
52
+ 'Unable to complete the language change. Please try again.',
53
+ }),
54
+ });
55
+ });
56
+ }
57
+
58
+ // With one language there is nothing to choose.
59
+ if (locales.length < 2) return null;
60
+
61
+ return (
62
+ <DropdownMenuSub>
63
+ <DropdownMenuSubTrigger
64
+ className={cn('gap-2', className)}
65
+ disabled={switching}
66
+ >
67
+ <Languages aria-hidden='true' className='size-4' />
68
+ <span className='flex-1'>
69
+ {t('actions.language', { defaultValue: 'Language' })}
70
+ </span>
71
+ <span className='text-muted-foreground'>
72
+ {locales.find((definition) => definition.locale === locale)?.label ??
73
+ locale}
74
+ </span>
75
+ </DropdownMenuSubTrigger>
76
+ <DropdownMenuSubContent>
77
+ <DropdownMenuRadioGroup
78
+ value={locale}
79
+ onValueChange={handleLocaleChange}
80
+ >
81
+ {locales.map((definition) => (
82
+ <DropdownMenuRadioItem
83
+ closeOnClick
84
+ key={definition.locale}
85
+ value={definition.locale}
86
+ disabled={switching}
87
+ >
88
+ {definition.label}
89
+ </DropdownMenuRadioItem>
90
+ ))}
91
+ </DropdownMenuRadioGroup>
92
+ </DropdownMenuSubContent>
93
+ </DropdownMenuSub>
94
+ );
95
+ }
@@ -0,0 +1,17 @@
1
+ import type { ComponentProps } from 'react';
2
+ import { cn } from 'cn';
3
+
4
+ export function LayoutHeader({
5
+ className,
6
+ ...props
7
+ }: ComponentProps<'header'>) {
8
+ return (
9
+ <header
10
+ className={cn(
11
+ 'flex h-16 shrink-0 items-center border-b border-border/70 bg-background/85 px-3 backdrop-blur-xl md:px-4',
12
+ className,
13
+ )}
14
+ {...props}
15
+ />
16
+ );
17
+ }