@nocobase/app-template-default 1.0.0-beta.58 → 1.0.0-beta.59
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/AGENTS.md +26 -23
- package/README.MD +7 -57
- package/client/agent-annotations-host.ts +8 -2
- package/client/app.ts +0 -2
- package/client/components/brand-loading-mark.tsx +46 -0
- package/client/components/brand-mark-geometry.ts +7 -0
- package/client/components/brand-spinner.tsx +22 -0
- package/client/components/brand-watermark.tsx +32 -0
- package/client/components/loading.tsx +4 -3
- package/client/components/route-overlay.tsx +15 -4
- package/client/components/status-badge.tsx +29 -0
- package/client/components/ui/alert-dialog.tsx +186 -0
- package/client/components/ui/card.tsx +102 -0
- package/client/components/ui/dialog.tsx +2 -2
- package/client/components/ui/radio-group.tsx +35 -0
- package/client/components/ui/spinner.tsx +2 -20
- package/client/components/ui/toast.tsx +3 -2
- package/client/extensions/nocobase-file-component-ui/components/file-upload-field.tsx +3 -3
- package/client/extensions/nocobase-inbox/inbox-item.tsx +4 -3
- package/client/layouts/app-layout.tsx +8 -28
- package/client/layouts/components/app-sidebar.tsx +4 -4
- package/client/layouts/components/header-actions.tsx +3 -36
- package/client/layouts/index.ts +0 -1
- package/client/lib/toaster.ts +1 -1
- package/client/locales/en-US.ts +31 -17
- package/client/locales/zh-CN.ts +24 -14
- package/client/pages/auth/login.tsx +1 -0
- package/client/pages/auth/shared.tsx +9 -9
- package/client/pages/home.tsx +80 -7
- package/client/plugins.ts +2 -2
- package/client/routes.ts +4 -10
- package/client/routing/app-router.tsx +5 -55
- package/client/routing/route-navigation.ts +1 -1
- package/client/styles.css +244 -0
- package/client/theme/theme-settings.tsx +22 -20
- package/client/theme/themes/compact.css +34 -16
- package/client/theme/themes/default.css +34 -16
- package/eslint.config.js +0 -13
- package/index.html +48 -16
- package/package.json +22 -26
- package/tests/components/app-layout-routes.test.tsx +1 -1
- package/tests/components/inbox.test.tsx +0 -19
- package/tests/components/layout-i18n.test.tsx +5 -92
- package/tests/components/primitive-labels.test.tsx +61 -1
- package/tests/fixtures/theme-tokens.html +1 -1
- package/tests/fixtures/theme-tokens.tsx +7 -0
- package/tests/logic/app-server.test.ts +0 -15
- package/tests/logic/client-plugin-registry.test.ts +0 -21
- package/tests/logic/client-routes.test.ts +2 -6
- package/tests/logic/client-shell.test.tsx +11 -107
- package/tests/logic/client-theme.test.tsx +17 -1
- package/tests/logic/config.test.ts +0 -1
- package/tests/logic/plugin-commands.test.ts +1 -1
- package/tsconfig.json +1 -4
- package/tsconfig.server.json +1 -6
- package/vitest.config.ts +1 -8
- package/client/components/inbox-badge.ts +0 -49
- package/client/components/inbox-button.tsx +0 -132
- package/client/components/inbox-header-button.tsx +0 -25
- package/client/layouts/components/surface-empty.tsx +0 -35
- package/client/layouts/settings-layout.tsx +0 -172
- package/server/config/workflow.ts +0 -24
- package/tests/logic/client-settings.test.tsx +0 -841
- package/tests/logic/dev-routes-production-build.test.ts +0 -168
- 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-
|
|
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-
|
|
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
|
-
|
|
2
|
-
|
|
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 = <
|
|
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 {
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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
|
-
</
|
|
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 {
|
|
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={
|
|
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
|
-
//
|
|
52
|
-
//
|
|
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) *
|
|
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 {
|
|
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>
|
package/client/layouts/index.ts
CHANGED
package/client/lib/toaster.ts
CHANGED
|
@@ -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 —
|
|
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 {
|