@nocobase/app-template-default 1.0.0-beta.58 → 1.0.0-beta.60

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 (72) hide show
  1. package/AGENTS.md +28 -25
  2. package/README.MD +7 -57
  3. package/client/agent-annotations-host.ts +8 -2
  4. package/client/app.ts +0 -2
  5. package/client/components/brand-loading-mark.tsx +46 -0
  6. package/client/components/brand-mark-geometry.ts +7 -0
  7. package/client/components/brand-spinner.tsx +22 -0
  8. package/client/components/brand-watermark.tsx +32 -0
  9. package/client/components/loading.tsx +4 -3
  10. package/client/components/route-overlay.tsx +15 -4
  11. package/client/components/status-badge.tsx +29 -0
  12. package/client/components/ui/alert-dialog.tsx +186 -0
  13. package/client/components/ui/card.tsx +102 -0
  14. package/client/components/ui/dialog.tsx +2 -2
  15. package/client/components/ui/radio-group.tsx +35 -0
  16. package/client/components/ui/spinner.tsx +2 -20
  17. package/client/components/ui/toast.tsx +3 -2
  18. package/client/extensions/nocobase-file-component-ui/components/file-upload-field.tsx +3 -3
  19. package/client/extensions/nocobase-inbox/inbox-item.tsx +4 -3
  20. package/client/layouts/app-layout.tsx +8 -28
  21. package/client/layouts/components/app-sidebar.tsx +4 -4
  22. package/client/layouts/components/header-actions.tsx +3 -36
  23. package/client/layouts/index.ts +0 -1
  24. package/client/lib/toaster.ts +1 -1
  25. package/client/locales/en-US.ts +8 -164
  26. package/client/locales/system/en-US.ts +344 -0
  27. package/client/locales/system/zh-CN.ts +320 -0
  28. package/client/locales/zh-CN.ts +3 -155
  29. package/client/pages/auth/login.tsx +1 -0
  30. package/client/pages/auth/shared.tsx +9 -9
  31. package/client/pages/home/capabilities.ts +52 -0
  32. package/client/pages/home/index.tsx +190 -0
  33. package/client/pages/home/prompt-dialog.tsx +122 -0
  34. package/client/pages/home/use-copy.ts +38 -0
  35. package/client/plugins.ts +2 -2
  36. package/client/routes.ts +5 -11
  37. package/client/routing/app-router.tsx +5 -55
  38. package/client/routing/route-navigation.ts +1 -1
  39. package/client/styles.css +244 -0
  40. package/client/theme/theme-settings.tsx +22 -20
  41. package/client/theme/themes/compact.css +34 -16
  42. package/client/theme/themes/default.css +34 -16
  43. package/eslint.config.js +0 -13
  44. package/index.html +48 -16
  45. package/package.json +23 -27
  46. package/tests/components/app-layout-routes.test.tsx +1 -1
  47. package/tests/components/home.test.tsx +174 -0
  48. package/tests/components/inbox.test.tsx +0 -19
  49. package/tests/components/layout-i18n.test.tsx +5 -92
  50. package/tests/components/primitive-labels.test.tsx +61 -1
  51. package/tests/fixtures/theme-tokens.html +1 -1
  52. package/tests/fixtures/theme-tokens.tsx +7 -0
  53. package/tests/logic/app-server.test.ts +0 -15
  54. package/tests/logic/client-plugin-registry.test.ts +0 -21
  55. package/tests/logic/client-routes.test.ts +2 -6
  56. package/tests/logic/client-shell.test.tsx +11 -107
  57. package/tests/logic/client-theme.test.tsx +17 -1
  58. package/tests/logic/config.test.ts +0 -1
  59. package/tests/logic/plugin-commands.test.ts +1 -1
  60. package/tsconfig.json +1 -4
  61. package/tsconfig.server.json +1 -6
  62. package/vitest.config.ts +1 -8
  63. package/client/components/inbox-badge.ts +0 -49
  64. package/client/components/inbox-button.tsx +0 -132
  65. package/client/components/inbox-header-button.tsx +0 -25
  66. package/client/layouts/components/surface-empty.tsx +0 -35
  67. package/client/layouts/settings-layout.tsx +0 -172
  68. package/client/pages/home.tsx +0 -16
  69. package/server/config/workflow.ts +0 -24
  70. package/tests/logic/client-settings.test.tsx +0 -841
  71. package/tests/logic/dev-routes-production-build.test.ts +0 -168
  72. package/tests/logic/plugin-page-authz.test.ts +0 -53
@@ -0,0 +1,186 @@
1
+ import * as React from 'react';
2
+ import { AlertDialog as AlertDialogPrimitive } from '@base-ui/react/alert-dialog';
3
+ import { cn } from 'cn';
4
+
5
+ import { Button } from '#components/ui/button';
6
+
7
+ function AlertDialog({ ...props }: AlertDialogPrimitive.Root.Props) {
8
+ return <AlertDialogPrimitive.Root data-slot='alert-dialog' {...props} />;
9
+ }
10
+
11
+ function AlertDialogTrigger({ ...props }: AlertDialogPrimitive.Trigger.Props) {
12
+ return (
13
+ <AlertDialogPrimitive.Trigger data-slot='alert-dialog-trigger' {...props} />
14
+ );
15
+ }
16
+
17
+ function AlertDialogPortal({ ...props }: AlertDialogPrimitive.Portal.Props) {
18
+ return (
19
+ <AlertDialogPrimitive.Portal data-slot='alert-dialog-portal' {...props} />
20
+ );
21
+ }
22
+
23
+ function AlertDialogOverlay({
24
+ className,
25
+ ...props
26
+ }: AlertDialogPrimitive.Backdrop.Props) {
27
+ return (
28
+ <AlertDialogPrimitive.Backdrop
29
+ data-slot='alert-dialog-overlay'
30
+ className={cn(
31
+ 'fixed inset-0 isolate z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0',
32
+ className,
33
+ )}
34
+ {...props}
35
+ />
36
+ );
37
+ }
38
+
39
+ function AlertDialogContent({
40
+ className,
41
+ size = 'default',
42
+ ...props
43
+ }: AlertDialogPrimitive.Popup.Props & {
44
+ size?: 'default' | 'sm';
45
+ }) {
46
+ return (
47
+ <AlertDialogPortal>
48
+ <AlertDialogOverlay />
49
+ <AlertDialogPrimitive.Popup
50
+ data-slot='alert-dialog-content'
51
+ data-size={size}
52
+ className={cn(
53
+ 'group/alert-dialog-content fixed top-1/2 left-1/2 z-50 grid w-full -translate-x-1/2 -translate-y-1/2 max-h-[calc(100dvh-2rem)] gap-5 overflow-y-auto rounded-xl bg-popover p-6 text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none max-w-[calc(100%-2rem)] 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',
54
+ size === 'sm' ? 'sm:max-w-sm' : 'sm:max-w-lg',
55
+ className,
56
+ )}
57
+ {...props}
58
+ />
59
+ </AlertDialogPortal>
60
+ );
61
+ }
62
+
63
+ function AlertDialogHeader({
64
+ className,
65
+ ...props
66
+ }: React.ComponentProps<'div'>) {
67
+ return (
68
+ <div
69
+ data-slot='alert-dialog-header'
70
+ className={cn(
71
+ 'grid grid-rows-[auto_1fr] place-items-center gap-1.5 text-center has-data-[slot=alert-dialog-media]:grid-rows-[auto_auto_1fr] has-data-[slot=alert-dialog-media]:gap-x-4 sm:group-data-[size=default]/alert-dialog-content:place-items-start sm:group-data-[size=default]/alert-dialog-content:text-left sm:group-data-[size=default]/alert-dialog-content:has-data-[slot=alert-dialog-media]:grid-rows-[auto_1fr]',
72
+ className,
73
+ )}
74
+ {...props}
75
+ />
76
+ );
77
+ }
78
+
79
+ function AlertDialogFooter({
80
+ className,
81
+ ...props
82
+ }: React.ComponentProps<'div'>) {
83
+ return (
84
+ <div
85
+ data-slot='alert-dialog-footer'
86
+ className={cn(
87
+ '-mx-6 -mb-6 flex flex-col-reverse gap-3 rounded-b-xl border-t bg-muted/30 px-6 py-4 group-data-[size=sm]/alert-dialog-content:grid group-data-[size=sm]/alert-dialog-content:grid-cols-2 sm:flex-row sm:justify-end',
88
+ className,
89
+ )}
90
+ {...props}
91
+ />
92
+ );
93
+ }
94
+
95
+ function AlertDialogMedia({
96
+ className,
97
+ ...props
98
+ }: React.ComponentProps<'div'>) {
99
+ return (
100
+ <div
101
+ data-slot='alert-dialog-media'
102
+ className={cn(
103
+ "mb-2 inline-flex size-10 items-center justify-center rounded-md bg-muted sm:group-data-[size=default]/alert-dialog-content:row-span-2 *:[svg:not([class*='size-'])]:size-6",
104
+ className,
105
+ )}
106
+ {...props}
107
+ />
108
+ );
109
+ }
110
+
111
+ function AlertDialogTitle({
112
+ className,
113
+ ...props
114
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
115
+ return (
116
+ <AlertDialogPrimitive.Title
117
+ data-slot='alert-dialog-title'
118
+ className={cn(
119
+ 'font-heading text-base font-medium sm:group-data-[size=default]/alert-dialog-content:group-has-data-[slot=alert-dialog-media]/alert-dialog-content:col-start-2',
120
+ className,
121
+ )}
122
+ {...props}
123
+ />
124
+ );
125
+ }
126
+
127
+ function AlertDialogDescription({
128
+ className,
129
+ ...props
130
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
131
+ return (
132
+ <AlertDialogPrimitive.Description
133
+ data-slot='alert-dialog-description'
134
+ className={cn(
135
+ 'text-sm text-balance text-muted-foreground md:text-pretty *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground',
136
+ className,
137
+ )}
138
+ {...props}
139
+ />
140
+ );
141
+ }
142
+
143
+ function AlertDialogAction({
144
+ className,
145
+ ...props
146
+ }: React.ComponentProps<typeof Button>) {
147
+ return (
148
+ <Button
149
+ data-slot='alert-dialog-action'
150
+ className={cn(className)}
151
+ {...props}
152
+ />
153
+ );
154
+ }
155
+
156
+ function AlertDialogCancel({
157
+ className,
158
+ variant = 'outline',
159
+ size = 'default',
160
+ ...props
161
+ }: AlertDialogPrimitive.Close.Props &
162
+ Pick<React.ComponentProps<typeof Button>, 'variant' | 'size'>) {
163
+ return (
164
+ <AlertDialogPrimitive.Close
165
+ data-slot='alert-dialog-cancel'
166
+ className={cn(className)}
167
+ render={<Button variant={variant} size={size} />}
168
+ {...props}
169
+ />
170
+ );
171
+ }
172
+
173
+ export {
174
+ AlertDialog,
175
+ AlertDialogAction,
176
+ AlertDialogCancel,
177
+ AlertDialogContent,
178
+ AlertDialogDescription,
179
+ AlertDialogFooter,
180
+ AlertDialogHeader,
181
+ AlertDialogMedia,
182
+ AlertDialogOverlay,
183
+ AlertDialogPortal,
184
+ AlertDialogTitle,
185
+ AlertDialogTrigger,
186
+ };
@@ -0,0 +1,102 @@
1
+ import * as React from 'react';
2
+ import { cn } from 'cn';
3
+
4
+ function Card({
5
+ className,
6
+ size = 'default',
7
+ ...props
8
+ }: React.ComponentProps<'div'> & { size?: 'default' | 'sm' }) {
9
+ return (
10
+ <div
11
+ data-slot='card'
12
+ data-size={size}
13
+ className={cn(
14
+ 'group/card flex flex-col gap-(--card-spacing) overflow-hidden rounded-xl bg-card py-(--card-spacing) text-sm text-card-foreground ring-1 ring-foreground/10 [--card-spacing:--spacing(4)] has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(3)] data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl',
15
+ className,
16
+ )}
17
+ {...props}
18
+ />
19
+ );
20
+ }
21
+
22
+ function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
23
+ return (
24
+ <div
25
+ data-slot='card-header'
26
+ className={cn(
27
+ 'group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-(--card-spacing) has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-(--card-spacing)',
28
+ className,
29
+ )}
30
+ {...props}
31
+ />
32
+ );
33
+ }
34
+
35
+ function CardTitle({ className, ...props }: React.ComponentProps<'div'>) {
36
+ return (
37
+ <div
38
+ data-slot='card-title'
39
+ className={cn(
40
+ 'font-heading text-base leading-snug font-medium group-data-[size=sm]/card:text-sm',
41
+ className,
42
+ )}
43
+ {...props}
44
+ />
45
+ );
46
+ }
47
+
48
+ function CardDescription({ className, ...props }: React.ComponentProps<'div'>) {
49
+ return (
50
+ <div
51
+ data-slot='card-description'
52
+ className={cn('text-sm text-muted-foreground', className)}
53
+ {...props}
54
+ />
55
+ );
56
+ }
57
+
58
+ function CardAction({ className, ...props }: React.ComponentProps<'div'>) {
59
+ return (
60
+ <div
61
+ data-slot='card-action'
62
+ className={cn(
63
+ 'col-start-2 row-span-2 row-start-1 self-start justify-self-end',
64
+ className,
65
+ )}
66
+ {...props}
67
+ />
68
+ );
69
+ }
70
+
71
+ function CardContent({ className, ...props }: React.ComponentProps<'div'>) {
72
+ return (
73
+ <div
74
+ data-slot='card-content'
75
+ className={cn('px-(--card-spacing)', className)}
76
+ {...props}
77
+ />
78
+ );
79
+ }
80
+
81
+ function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
82
+ return (
83
+ <div
84
+ data-slot='card-footer'
85
+ className={cn(
86
+ 'flex items-center rounded-b-xl border-t bg-muted/50 p-(--card-spacing)',
87
+ className,
88
+ )}
89
+ {...props}
90
+ />
91
+ );
92
+ }
93
+
94
+ export {
95
+ Card,
96
+ CardHeader,
97
+ CardFooter,
98
+ CardTitle,
99
+ CardAction,
100
+ CardDescription,
101
+ CardContent,
102
+ };
@@ -53,7 +53,7 @@ function DialogContent({
53
53
  <DialogPrimitive.Popup
54
54
  data-slot='dialog-content'
55
55
  className={cn(
56
- 'fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-popover p-4 text-sm text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-sm 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',
56
+ 'fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 max-h-[calc(100dvh-2rem)] gap-5 overflow-y-auto rounded-xl bg-popover p-6 text-sm text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-lg 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',
57
57
  className,
58
58
  )}
59
59
  {...props}
@@ -102,7 +102,7 @@ function DialogFooter({
102
102
  <div
103
103
  data-slot='dialog-footer'
104
104
  className={cn(
105
- '-mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t bg-muted/50 p-4 sm:flex-row sm:justify-end',
105
+ '-mx-6 -mb-6 flex flex-col-reverse gap-3 rounded-b-xl border-t bg-muted/30 px-6 py-4 sm:flex-row sm:justify-end',
106
106
  className,
107
107
  )}
108
108
  {...props}
@@ -0,0 +1,35 @@
1
+ import { Radio as RadioPrimitive } from '@base-ui/react/radio';
2
+ import { RadioGroup as RadioGroupPrimitive } from '@base-ui/react/radio-group';
3
+ import { cn } from 'cn';
4
+
5
+ function RadioGroup({ className, ...props }: RadioGroupPrimitive.Props) {
6
+ return (
7
+ <RadioGroupPrimitive
8
+ data-slot='radio-group'
9
+ className={cn('grid w-full gap-2', className)}
10
+ {...props}
11
+ />
12
+ );
13
+ }
14
+
15
+ function RadioGroupItem({ className, ...props }: RadioPrimitive.Root.Props) {
16
+ return (
17
+ <RadioPrimitive.Root
18
+ data-slot='radio-group-item'
19
+ className={cn(
20
+ 'group/radio-group-item peer relative flex aspect-square size-4 shrink-0 rounded-full border border-input outline-none group-has-[:focus-visible]/field-label:ring-0 group-has-[:focus-visible]/field-label:not-data-checked:border-input after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 aria-invalid:aria-checked:border-primary dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground group-has-[:focus-visible]/field-label:data-checked:border-primary dark:data-checked:bg-primary',
21
+ className,
22
+ )}
23
+ {...props}
24
+ >
25
+ <RadioPrimitive.Indicator
26
+ data-slot='radio-group-indicator'
27
+ className='flex size-4 items-center justify-center'
28
+ >
29
+ <span className='absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2 rounded-full bg-primary-foreground' />
30
+ </RadioPrimitive.Indicator>
31
+ </RadioPrimitive.Root>
32
+ );
33
+ }
34
+
35
+ export { RadioGroup, RadioGroupItem };
@@ -1,20 +1,2 @@
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 };
1
+ // Preserve the shadcn Spinner API while sharing the application's brand indicator.
2
+ export { BrandSpinner as Spinner } from '#components/brand-spinner';
@@ -1,5 +1,7 @@
1
1
  'use client';
2
2
 
3
+ import { BrandSpinner } from '#components/brand-spinner';
4
+
3
5
  import { useTranslation } from '@nocobase/i18n/client';
4
6
  import * as React from 'react';
5
7
  import { Toast as ToastPrimitive } from '@base-ui/react/toast';
@@ -12,7 +14,6 @@ import {
12
14
  InfoIcon,
13
15
  TriangleAlertIcon,
14
16
  OctagonXIcon,
15
- Loader2Icon,
16
17
  } from 'lucide-react';
17
18
 
18
19
  const toast = ToastPrimitive.createToastManager();
@@ -159,7 +160,7 @@ function ToastIcon({ type }: { type: string | undefined }) {
159
160
  }
160
161
 
161
162
  if (type === 'loading') {
162
- icon = <Loader2Icon className='animate-spin' aria-hidden='true' />;
163
+ icon = <BrandSpinner aria-hidden='true' />;
163
164
  }
164
165
 
165
166
  if (!icon) {
@@ -1,6 +1,6 @@
1
1
  import { messageKey } from '../lib/message-key.js';
2
2
  import { useTranslation } from '@nocobase/i18n/client';
3
- import { LoaderCircle, RotateCcw, Trash2, UploadCloud, X } from 'lucide-react';
3
+ import { RotateCcw, Trash2, UploadCloud, X } from 'lucide-react';
4
4
  import {
5
5
  useEffect,
6
6
  useRef,
@@ -12,6 +12,7 @@ import {
12
12
 
13
13
  import type { FileRecord, FileUploadFieldProps } from '../types';
14
14
  import { Button } from '#components/ui/button';
15
+ import { Spinner } from '#components/ui/spinner';
15
16
  import { FileThumbnail } from './file-thumbnail';
16
17
 
17
18
  type UploadItem = {
@@ -289,8 +290,7 @@ export function FileUploadField(
289
290
  <div key={item.key} className='w-36 rounded-md border p-2'>
290
291
  <div className='flex h-20 items-center justify-center overflow-hidden rounded-sm bg-muted/30'>
291
292
  {item.status === 'uploading' ? (
292
- <LoaderCircle
293
- className='animate-spin'
293
+ <Spinner
294
294
  aria-label={t('files.uploading', {
295
295
  defaultValue: 'Uploading',
296
296
  })}
@@ -179,11 +179,12 @@ function ItemCard({
179
179
  <span className='mt-0.5 flex shrink-0'>
180
180
  <InboxTypeIcon entry={entry} renderer={renderer} />
181
181
  </span>
182
- <button
182
+ <Button
183
+ variant='ghost'
183
184
  type='button'
184
185
  aria-current={selected ? 'true' : undefined}
185
186
  // Stretched over the whole card, so a click anywhere on it selects the item.
186
- className="block min-w-0 flex-1 cursor-pointer space-y-1 text-left after:absolute after:inset-0 after:rounded-lg after:content-[''] focus-visible:outline-none"
187
+ className="block h-auto min-w-0 flex-1 cursor-pointer space-y-1 rounded-none border-0 bg-transparent p-0 text-left font-normal whitespace-normal hover:bg-transparent active:translate-y-0 after:absolute after:inset-0 after:rounded-lg after:content-[''] focus-visible:outline-none"
187
188
  onClick={() => onSelect(entry)}
188
189
  onKeyDown={onKeyDown}
189
190
  >
@@ -234,7 +235,7 @@ function ItemCard({
234
235
  {sentence}
235
236
  </span>
236
237
  ) : null}
237
- </button>
238
+ </Button>
238
239
  <DropdownMenu>
239
240
  <DropdownMenuTrigger
240
241
  render={
@@ -1,17 +1,14 @@
1
1
  import { useSyncServerLocale } from '@nocobase/app-plugin-i18n/client';
2
- import { useMemo, type ReactElement } from 'react';
2
+ import type { ReactElement } from 'react';
3
3
  import type { AppClientRegisteredRoute } from '@nocobase/app-client/plugins';
4
4
  import { Outlet, useLocation } from 'react-router';
5
5
 
6
+ import { BrandWatermark } from '#components/brand-watermark';
6
7
  import { Breadcrumbs } from '#components/breadcrumbs';
7
- import { EMPTY_ARRAY } from '#lib/constants';
8
8
 
9
9
  import { RouteTreeProvider } from '../routing/route-context.js';
10
10
 
11
- import {
12
- PageBreadcrumbProvider,
13
- useClientApplication,
14
- } from '@nocobase/app-client';
11
+ import { PageBreadcrumbProvider } from '@nocobase/app-client';
15
12
  import { useTranslation } from '@nocobase/i18n/client';
16
13
  import { LayoutHeader } from './components/layout-header.js';
17
14
  import {
@@ -26,19 +23,13 @@ import { AppSidebarFooter } from './components/sidebar-footer.js';
26
23
  import {
27
24
  useRouteNavigation,
28
25
  selectedNavigationId,
29
- navigationPages,
30
26
  } from '../routing/route-navigation.js';
31
27
 
32
28
  export interface AppLayoutProps {
33
29
  readonly routes: readonly AppClientRegisteredRoute[];
34
- /** Pages plugins declare with `defineDevRoutes()`: rendered in this shell at `/dev/...`, never in its navigation. */
35
- readonly devRoutes?: readonly AppClientRegisteredRoute[];
36
30
  }
37
31
 
38
- export function AppLayout({
39
- routes,
40
- devRoutes = EMPTY_ARRAY,
41
- }: AppLayoutProps): ReactElement {
32
+ export function AppLayout({ routes }: AppLayoutProps): ReactElement {
42
33
  // The browser decides what it renders; this tells the server the same language so its messages match.
43
34
  useSyncServerLocale();
44
35
 
@@ -49,21 +40,13 @@ export function AppLayout({
49
40
  useLocation().pathname,
50
41
  denied,
51
42
  );
52
- const settingsNavigation = useRouteNavigation(
53
- useClientApplication().runtime.settingsRouteTree,
54
- );
55
- // Pages read their trail from this tree, so it keeps its identity between renders.
56
- const routeTree = useMemo(
57
- () => (devRoutes.length ? [...routes, ...devRoutes] : routes),
58
- [routes, devRoutes],
59
- );
60
43
  const navigationLabel = t('navigation.label', {
61
44
  defaultValue: 'Application navigation',
62
45
  });
63
46
 
64
47
  return (
65
48
  // The shell owns the business route tree used by its pages and navigation.
66
- <RouteTreeProvider routes={routeTree}>
49
+ <RouteTreeProvider routes={routes}>
67
50
  <PageBreadcrumbProvider>
68
51
  <AppSidebarProvider>
69
52
  <AppSidebar label={navigationLabel} footer={<AppSidebarFooter />}>
@@ -84,13 +67,10 @@ export function AppLayout({
84
67
  {/* The current page's trail: the route tree's, or the one the page declares (`usePageBreadcrumb`). */}
85
68
  <Breadcrumbs denied={denied} />
86
69
  </div>
87
- <HeaderActions
88
- showSettings={
89
- navigationPages(settingsNavigation.items).length > 0
90
- }
91
- />
70
+ <HeaderActions />
92
71
  </LayoutHeader>
93
- <main className='relative min-w-0 flex-1 overflow-hidden'>
72
+ <main className='app-brand-surface relative isolate min-w-0 flex-1 overflow-hidden'>
73
+ <BrandWatermark className='-z-10' />
94
74
  {/* main only positions; the page scrolls in here, so a child page layer laid over main is neither
95
75
  moved by the page's scrolling nor stretched by its height. */}
96
76
  <div className='h-full overflow-y-auto'>
@@ -48,11 +48,11 @@ export function AppSidebarProvider({
48
48
  open={!collapsed}
49
49
  onOpenChange={(open) => setCollapsed(!open)}
50
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).
51
+ // Give expanded navigation more breathing room while retaining density-aware sizing
52
+ // and the existing compact icon rail (the provider's documented width override).
53
53
  style={
54
54
  {
55
- '--sidebar-width': 'calc(var(--spacing) * 64)',
55
+ '--sidebar-width': 'calc(var(--spacing) * 75)',
56
56
  '--sidebar-width-icon': 'calc(var(--spacing) * 12)',
57
57
  } as CSSProperties
58
58
  }
@@ -123,7 +123,7 @@ export function AppSidebar({
123
123
  <>
124
124
  {header}
125
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'>
126
+ <SidebarContent className='app-navigation group-data-[collapsible=icon]:overflow-y-auto [&_[data-slot=sidebar-menu-button]]:h-10 [&_[data-slot=sidebar-menu-button]]:px-3'>
127
127
  {children}
128
128
  </SidebarContent>
129
129
  {footer}
@@ -1,47 +1,14 @@
1
- import { useTranslation } from '@nocobase/i18n/client';
2
- import { Settings } from 'lucide-react';
3
1
  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';
2
+ import { TooltipProvider } from '#components/ui/tooltip';
11
3
 
12
4
  import { ThemeSettings } from '../../theme/index.js';
13
5
  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
6
 
7
+ /** The header's right-hand actions, shared by every authenticated page. */
8
+ export function HeaderActions(): ReactElement {
27
9
  return (
28
10
  <TooltipProvider>
29
11
  <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
12
  <ThemeSettings />
46
13
  <UserMenu />
47
14
  </div>
@@ -1,2 +1 @@
1
1
  export { AppLayout, type AppLayoutProps } from './app-layout.js';
2
- export { SettingsLayout, type SettingsLayoutProps } from './settings-layout.js';
@@ -39,7 +39,7 @@ export function createToaster(manager: typeof toast = toast): Toaster {
39
39
  * Base UI announces a high-priority toast at once, but hides the toast itself from assistive technology until the
40
40
  * toast viewport is focused, and repeats its title and description in a visually hidden alert. That suits an error
41
41
  * written as plain text. Every other error keeps the default priority: one with an action, or whose title or
42
- * description is an element rather than text, may hold a control — Hub's technical details toggle is one — which a
42
+ * description is an element rather than text, may hold a control — a technical details toggle, for one — which a
43
43
  * high priority would hide and duplicate.
44
44
  */
45
45
  function isUrgent({ type, title, description, action }: ToastOptions): boolean {