@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,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
|
+
}
|