@nocobase/portal-template-default 1.0.3 → 1.0.4

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 (170) hide show
  1. package/README.MD +10 -5
  2. package/package.json +1 -1
  3. package/registry/nocobase-ai/components/chat/chat-message.tsx +8 -6
  4. package/registry/nocobase-ai/components/chat/chat-messages.tsx +8 -3
  5. package/registry/nocobase-ai/components/chat/markdown-message.tsx +26 -0
  6. package/registry/nocobase-ai/components/chat/sub-agent-conversation.tsx +3 -3
  7. package/registry/nocobase-ai/demo/configuration-gate.tsx +83 -35
  8. package/registry/nocobase-i18n/README.md +23 -32
  9. package/registry/nocobase-i18n/components/language-switcher.tsx +1 -1
  10. package/registry/nocobase-i18n/demo/index.tsx +2 -2
  11. package/registry/nocobase-i18n/demo/prompt-generator.tsx +3 -2
  12. package/registry/nocobase-i18n/extension.tsx +3 -4
  13. package/registry/nocobase-i18n/index.ts +4 -3
  14. package/registry/nocobase-i18n/locales/en-US.ts +2 -52
  15. package/registry/nocobase-i18n/locales/index.ts +8 -0
  16. package/registry/nocobase-i18n/locales/zh-CN.ts +2 -52
  17. package/registry/nocobase-i18n/provider.tsx +18 -6
  18. package/registry/nocobase-i18n/server-resources.ts +13 -31
  19. package/registry/nocobase-i18n/tests/i18n-regression.mjs +14 -5
  20. package/registry/nocobase-route-surfaces/README.md +21 -0
  21. package/registry/nocobase-route-surfaces/demo/index.tsx +266 -0
  22. package/{src/extensions/nocobase-acl → registry/nocobase-route-surfaces}/demo/lazy-route.tsx +2 -1
  23. package/registry/nocobase-route-surfaces/demo/prompt-generator.tsx +90 -0
  24. package/registry/nocobase-route-surfaces/demo/scenarios.ts +95 -0
  25. package/registry/nocobase-route-surfaces/extension.tsx +119 -0
  26. package/registry/nocobase-route-surfaces/index.ts +7 -0
  27. package/registry/nocobase-route-surfaces/route-dialog.tsx +145 -0
  28. package/registry/nocobase-route-surfaces/route-drawer.tsx +186 -0
  29. package/registry/nocobase-route-surfaces/route-page.tsx +29 -0
  30. package/registry/nocobase-route-surfaces/route-surface-context.ts +13 -0
  31. package/registry/nocobase-route-surfaces/use-refine-unsaved-changes.tsx +106 -0
  32. package/registry/nocobase-route-surfaces/use-route-surface-close.ts +15 -0
  33. package/registry/nocobase-route-surfaces/use-route-surface-state.ts +65 -0
  34. package/registry.config.json +30 -3
  35. package/scripts/registry.mjs +0 -2
  36. package/src/App.tsx +65 -35
  37. package/src/app/extension.ts +1 -2
  38. package/src/app/extensions.tsx +0 -14
  39. package/src/components/app-shell/breadcrumb.tsx +67 -2
  40. package/src/components/app-shell/sidebar.tsx +2 -2
  41. package/src/components/resources/views/list-view.tsx +3 -2
  42. package/src/components/ui/sheet.tsx +22 -21
  43. package/src/lib/nocobase/client.ts +6 -1
  44. package/src/locales/en-US.ts +81 -0
  45. package/src/locales/zh-CN.ts +77 -0
  46. package/src/pages/users/create.tsx +76 -53
  47. package/src/pages/users/edit.tsx +87 -56
  48. package/src/pages/users/index.ts +3 -0
  49. package/src/pages/users/layout.tsx +15 -0
  50. package/src/pages/users/list.tsx +19 -21
  51. package/src/pages/users/role-badges.tsx +43 -0
  52. package/src/pages/users/role-detail.tsx +272 -0
  53. package/src/pages/users/role-utils.ts +6 -0
  54. package/src/pages/users/routes.ts +24 -0
  55. package/src/pages/users/show.tsx +195 -104
  56. package/src/pages/users/types.ts +13 -0
  57. package/src/providers/i18n/index.ts +3 -0
  58. package/{registry/nocobase-i18n → src/providers/i18n}/runtime.ts +63 -16
  59. package/src/providers/system-settings/context.ts +26 -0
  60. package/src/providers/system-settings/index.ts +2 -0
  61. package/src/providers/system-settings/provider.tsx +94 -0
  62. package/src/extensions/nocobase-acl/README.md +0 -19
  63. package/src/extensions/nocobase-acl/components/acl-boundary.tsx +0 -106
  64. package/src/extensions/nocobase-acl/components/index.ts +0 -3
  65. package/src/extensions/nocobase-acl/components/role-options.ts +0 -36
  66. package/src/extensions/nocobase-acl/components/role-switcher.tsx +0 -135
  67. package/src/extensions/nocobase-acl/demo/boundary-api.tsx +0 -186
  68. package/src/extensions/nocobase-acl/demo/components.tsx +0 -231
  69. package/src/extensions/nocobase-acl/demo/index.tsx +0 -505
  70. package/src/extensions/nocobase-acl/demo/prompt-generator.tsx +0 -134
  71. package/src/extensions/nocobase-acl/demo/role-switcher-prompt-generator.tsx +0 -198
  72. package/src/extensions/nocobase-acl/demo/scenario-section.tsx +0 -37
  73. package/src/extensions/nocobase-acl/extension.tsx +0 -71
  74. package/src/extensions/nocobase-acl/index.ts +0 -1
  75. package/src/extensions/nocobase-ai/README.md +0 -70
  76. package/src/extensions/nocobase-ai/adapters/react-hook-form.ts +0 -20
  77. package/src/extensions/nocobase-ai/assets/nocobase-ai-chat.svg +0 -50
  78. package/src/extensions/nocobase-ai/components/ai-root-provider.tsx +0 -32
  79. package/src/extensions/nocobase-ai/components/chat/ai-employee-avatar.tsx +0 -31
  80. package/src/extensions/nocobase-ai/components/chat/chat-attachment.tsx +0 -92
  81. package/src/extensions/nocobase-ai/components/chat/chat-compact.tsx +0 -90
  82. package/src/extensions/nocobase-ai/components/chat/chat-composer.tsx +0 -429
  83. package/src/extensions/nocobase-ai/components/chat/chat-empty-state.tsx +0 -51
  84. package/src/extensions/nocobase-ai/components/chat/chat-header.tsx +0 -88
  85. package/src/extensions/nocobase-ai/components/chat/chat-history-dialog.tsx +0 -77
  86. package/src/extensions/nocobase-ai/components/chat/chat-message.tsx +0 -252
  87. package/src/extensions/nocobase-ai/components/chat/chat-messages.tsx +0 -173
  88. package/src/extensions/nocobase-ai/components/chat/chat-window.tsx +0 -118
  89. package/src/extensions/nocobase-ai/components/chat/conversation-list.tsx +0 -343
  90. package/src/extensions/nocobase-ai/components/chat/markdown-message.tsx +0 -165
  91. package/src/extensions/nocobase-ai/components/chat/model-select-options.tsx +0 -30
  92. package/src/extensions/nocobase-ai/components/chat/reasoning-panel.tsx +0 -29
  93. package/src/extensions/nocobase-ai/components/chat/sub-agent-conversation.tsx +0 -142
  94. package/src/extensions/nocobase-ai/components/chat/tool-call-card.tsx +0 -316
  95. package/src/extensions/nocobase-ai/components/chat/user-prompt-editor.tsx +0 -99
  96. package/src/extensions/nocobase-ai/components/chat/work-context-chip.tsx +0 -114
  97. package/src/extensions/nocobase-ai/components/index.ts +0 -59
  98. package/src/extensions/nocobase-ai/components/page-elements/ai-form.tsx +0 -44
  99. package/src/extensions/nocobase-ai/components/page-elements/page-element-provider.tsx +0 -469
  100. package/src/extensions/nocobase-ai/components/surfaces/chat-dialog.tsx +0 -31
  101. package/src/extensions/nocobase-ai/components/surfaces/chat-inline.tsx +0 -18
  102. package/src/extensions/nocobase-ai/components/surfaces/chat-page.tsx +0 -18
  103. package/src/extensions/nocobase-ai/components/surfaces/chat-side-panel-layout.tsx +0 -54
  104. package/src/extensions/nocobase-ai/components/surfaces/chat-side-panel.tsx +0 -74
  105. package/src/extensions/nocobase-ai/components/surfaces/chat-surface-actions.tsx +0 -54
  106. package/src/extensions/nocobase-ai/components/tools/builtin-tool-renderers.tsx +0 -31
  107. package/src/extensions/nocobase-ai/components/tools/business-report-dialog.tsx +0 -364
  108. package/src/extensions/nocobase-ai/components/tools/business-report-renderer.tsx +0 -122
  109. package/src/extensions/nocobase-ai/components/tools/business-report-utils.tsx +0 -282
  110. package/src/extensions/nocobase-ai/components/tools/chart-renderer.tsx +0 -99
  111. package/src/extensions/nocobase-ai/components/tools/echarts-preview.tsx +0 -59
  112. package/src/extensions/nocobase-ai/components/tools/echarts-runtime-advanced.ts +0 -18
  113. package/src/extensions/nocobase-ai/components/tools/echarts-runtime-common.ts +0 -20
  114. package/src/extensions/nocobase-ai/components/tools/echarts-runtime-components.ts +0 -34
  115. package/src/extensions/nocobase-ai/components/tools/echarts-runtime-hierarchy.ts +0 -24
  116. package/src/extensions/nocobase-ai/components/tools/echarts-runtime.ts +0 -138
  117. package/src/extensions/nocobase-ai/components/tools/sub-agent-renderer.tsx +0 -67
  118. package/src/extensions/nocobase-ai/components/tools/suggestions-renderer.tsx +0 -59
  119. package/src/extensions/nocobase-ai/components/tools/tool-renderer-provider.tsx +0 -55
  120. package/src/extensions/nocobase-ai/components/tools/tool-renderer-utils.ts +0 -18
  121. package/src/extensions/nocobase-ai/components/tools/workflow-renderer.tsx +0 -108
  122. package/src/extensions/nocobase-ai/components/triggers/ai-chat-floating-trigger.tsx +0 -73
  123. package/src/extensions/nocobase-ai/components/triggers/ai-employee-shortcut.tsx +0 -149
  124. package/src/extensions/nocobase-ai/demo/configuration-gate.tsx +0 -49
  125. package/src/extensions/nocobase-ai/demo/container-showcase.tsx +0 -210
  126. package/src/extensions/nocobase-ai/demo/floating.tsx +0 -134
  127. package/src/extensions/nocobase-ai/demo/index.tsx +0 -346
  128. package/src/extensions/nocobase-ai/demo/interaction-showcase.tsx +0 -151
  129. package/src/extensions/nocobase-ai/demo/page-context-prompt-generator.tsx +0 -355
  130. package/src/extensions/nocobase-ai/demo/page-context.tsx +0 -884
  131. package/src/extensions/nocobase-ai/demo/page-element-showcase.tsx +0 -197
  132. package/src/extensions/nocobase-ai/demo/prompt-card.tsx +0 -41
  133. package/src/extensions/nocobase-ai/demo/prompt-generator.tsx +0 -452
  134. package/src/extensions/nocobase-ai/demo/shortcut.tsx +0 -1207
  135. package/src/extensions/nocobase-ai/demo/tool-cards.tsx +0 -530
  136. package/src/extensions/nocobase-ai/extension.tsx +0 -158
  137. package/src/extensions/nocobase-ai/global-ai-chat.tsx +0 -213
  138. package/src/extensions/nocobase-ai/index.ts +0 -3
  139. package/src/extensions/nocobase-ai/providers/ai-provider.tsx +0 -323
  140. package/src/extensions/nocobase-ai/providers/avatars.ts +0 -817
  141. package/src/extensions/nocobase-ai/providers/chat-context.tsx +0 -109
  142. package/src/extensions/nocobase-ai/providers/chat-controller.ts +0 -84
  143. package/src/extensions/nocobase-ai/providers/chat-message-utils.ts +0 -198
  144. package/src/extensions/nocobase-ai/providers/chat-provider.tsx +0 -871
  145. package/src/extensions/nocobase-ai/providers/chat-reducer.ts +0 -147
  146. package/src/extensions/nocobase-ai/providers/chat-task-utils.ts +0 -105
  147. package/src/extensions/nocobase-ai/providers/chat-transport.ts +0 -184
  148. package/src/extensions/nocobase-ai/providers/form-registry.tsx +0 -271
  149. package/src/extensions/nocobase-ai/providers/frontend-tool-registry.tsx +0 -231
  150. package/src/extensions/nocobase-ai/providers/index.ts +0 -61
  151. package/src/extensions/nocobase-ai/providers/model.ts +0 -32
  152. package/src/extensions/nocobase-ai/providers/page-context.tsx +0 -136
  153. package/src/extensions/nocobase-ai/providers/stream-coalescer.ts +0 -60
  154. package/src/extensions/nocobase-ai/providers/stream-event-utils.ts +0 -82
  155. package/src/extensions/nocobase-ai/providers/stream-parser.ts +0 -61
  156. package/src/extensions/nocobase-ai/providers/sub-agent-stream.ts +0 -316
  157. package/src/extensions/nocobase-ai/providers/types.ts +0 -183
  158. package/src/extensions/nocobase-ai/providers/ui-message-stream.ts +0 -463
  159. package/src/extensions/nocobase-ai/providers/use-automatic-tool-approval.ts +0 -94
  160. package/src/extensions/nocobase-ai/providers/use-chat-attachments.ts +0 -206
  161. package/src/extensions/nocobase-ai/providers/use-chat-message-actions.ts +0 -554
  162. package/src/extensions/nocobase-ai/providers/use-chat-runtime.ts +0 -210
  163. package/src/extensions/nocobase-ai/providers/use-chat-work-context.ts +0 -93
  164. package/src/extensions/nocobase-ai/providers/use-conversation-catalog.ts +0 -108
  165. package/src/extensions/nocobase-ai/providers/use-conversation-history.ts +0 -178
  166. package/src/extensions/nocobase-ai/services/index.ts +0 -7
  167. package/src/extensions/nocobase-ai/services/nocobase-ai-service.ts +0 -454
  168. package/src/extensions/nocobase-ai/services/types.ts +0 -82
  169. /package/{registry/nocobase-i18n → src/providers/i18n}/i18n-provider.ts +0 -0
  170. /package/{registry/nocobase-i18n → src/providers/i18n}/locale-store.ts +0 -0
@@ -1,18 +1,30 @@
1
1
  import { useEffect, useState, type PropsWithChildren } from "react";
2
2
 
3
3
  import { LoadingState } from "@/components/app-shell/loading-state";
4
- import { setTranslationResolver } from "@/lib/i18n";
4
+ import { nocobaseClient } from "@/lib/nocobase/client";
5
+ import { setLocalePersistence } from "@/providers/i18n";
6
+ import { useSystemSettings } from "@/providers/system-settings";
5
7
  import { loadServerLocaleResources } from "./server-resources";
6
- import { translate } from "./runtime";
7
8
 
8
- export function LocalePreferenceProvider({ children }: PropsWithChildren) {
9
+ export function NocoBaseI18nBootstrap({ children }: PropsWithChildren) {
10
+ const { settings } = useSystemSettings();
9
11
  const [ready, setReady] = useState(false);
10
12
 
11
- useEffect(() => setTranslationResolver(translate), []);
13
+ useEffect(
14
+ () =>
15
+ setLocalePersistence(async (locale) => {
16
+ if (!nocobaseClient.getToken()) return;
17
+ await nocobaseClient.action("users", "updateLang", {
18
+ method: "POST",
19
+ body: { appLang: locale },
20
+ });
21
+ }),
22
+ []
23
+ );
12
24
 
13
25
  useEffect(() => {
14
26
  let active = true;
15
- void loadServerLocaleResources()
27
+ void loadServerLocaleResources(settings)
16
28
  .catch((error) => {
17
29
  console.warn("Unable to load NocoBase locale resources", error);
18
30
  })
@@ -22,7 +34,7 @@ export function LocalePreferenceProvider({ children }: PropsWithChildren) {
22
34
  return () => {
23
35
  active = false;
24
36
  };
25
- }, []);
37
+ }, [settings]);
26
38
 
27
39
  if (!ready) {
28
40
  return <LoadingState className="min-h-80" />;
@@ -1,20 +1,18 @@
1
1
  import { nocobaseClient } from "@/lib/nocobase/client";
2
2
  import {
3
- getLocaleDefinitions,
3
+ type LocaleSystemSettings,
4
+ applyDocumentLocale,
5
+ getCurrentLocale,
6
+ i18n,
4
7
  registerLocale,
5
8
  setEnabledLocales,
6
- } from "./locale-store";
7
- import { applyDocumentLocale, getCurrentLocale, i18n } from "./runtime";
9
+ } from "@/providers/i18n";
8
10
 
9
11
  type ServerLanguagePayload = {
10
12
  lang?: string;
11
13
  resources?: Record<string, Record<string, string>>;
12
14
  };
13
15
 
14
- type SystemSettingsPayload = {
15
- enabledLanguages?: string[];
16
- };
17
-
18
16
  const serverResourceNamespaces = new Set(["lm-collections"]);
19
17
  const loadedServerResourceNamespaces = new Set<string>();
20
18
  const namespaceLoadPromises = new Map<string, Promise<void>>();
@@ -119,37 +117,23 @@ export function getServerResourceNamespaces() {
119
117
  return [...serverResourceNamespaces];
120
118
  }
121
119
 
122
- export function loadServerLocaleResources() {
120
+ export function loadServerLocaleResources(settings?: LocaleSystemSettings) {
123
121
  if (serverResourcesPromise) return serverResourcesPromise;
124
122
 
125
123
  bootstrapStarted = true;
126
124
  const requestedLocale = nocobaseClient.getStoredLocale();
127
125
  const initialNamespaces = [...serverResourceNamespaces];
128
- const systemSettingsPromise = nocobaseClient
129
- .action<SystemSettingsPayload>("systemSettings", "get", {
130
- method: "GET",
131
- includeRole: false,
132
- withAclMeta: false,
133
- })
134
- .catch((error) => {
135
- console.warn("Unable to load enabled NocoBase languages", error);
136
- return undefined;
137
- })
138
- .then((systemSettings) => {
139
- const configuredLocales = Array.isArray(systemSettings?.enabledLanguages)
140
- ? systemSettings.enabledLanguages
141
- : [];
142
- if (configuredLocales.length) setEnabledLocales(configuredLocales);
143
- return systemSettings;
144
- });
126
+ const configuredLocales = Array.isArray(settings?.enabledLanguages)
127
+ ? settings.enabledLanguages
128
+ : [];
129
+ if (configuredLocales.length) setEnabledLocales(configuredLocales);
145
130
  const languagePromise = requestServerResources(
146
131
  initialNamespaces,
147
132
  requestedLocale
148
133
  );
149
134
 
150
- serverResourcesPromise = Promise.all([systemSettingsPromise, languagePromise])
151
- .then(async ([systemSettings, payload]) => {
152
- const configuredLocales = systemSettings?.enabledLanguages ?? [];
135
+ serverResourcesPromise = languagePromise
136
+ .then(async (payload) => {
153
137
  const serverLocale = payload.lang;
154
138
 
155
139
  if (!configuredLocales.length && serverLocale) {
@@ -173,9 +157,7 @@ export function loadServerLocaleResources() {
173
157
  })
174
158
  .catch((error) => {
175
159
  bootstrapComplete = true;
176
- if (!getLocaleDefinitions().length) {
177
- setEnabledLocales([getCurrentLocale()]);
178
- }
160
+ if (!configuredLocales.length) setEnabledLocales([getCurrentLocale()]);
179
161
  serverResourcesPromise = undefined;
180
162
  throw error;
181
163
  });
@@ -9,13 +9,16 @@ const server = await createServer({
9
9
 
10
10
  try {
11
11
  await server.ssrLoadModule("/src/locales/index.ts");
12
- const { i18n, registerLocaleResources, translate } =
13
- await server.ssrLoadModule("/registry/nocobase-i18n/runtime.ts");
14
- const { resolveTranslatableText, setTranslationResolver } =
12
+ const {
13
+ applySystemLocale,
14
+ getCurrentLocale,
15
+ i18n,
16
+ registerLocaleResources,
17
+ translate,
18
+ } = await server.ssrLoadModule("/src/providers/i18n/runtime.ts");
19
+ const { resolveTranslatableText } =
15
20
  await server.ssrLoadModule("/src/lib/i18n.ts");
16
21
 
17
- setTranslationResolver(translate);
18
-
19
22
  await i18n.changeLanguage("en-US");
20
23
  assert.equal(translate("resources.users", { ns: "app" }, "Users"), "Users");
21
24
 
@@ -36,6 +39,12 @@ try {
36
39
  "示例"
37
40
  );
38
41
 
42
+ await applySystemLocale({
43
+ appLang: "en-US",
44
+ enabledLanguages: ["en-US", "zh-CN"],
45
+ });
46
+ assert.equal(getCurrentLocale(), "en-US");
47
+
39
48
  console.log("NocoBase i18n regression tests passed");
40
49
  } finally {
41
50
  await server.close();
@@ -0,0 +1,21 @@
1
+ # Route surfaces
2
+
3
+ Route surfaces keep URL navigation separate from visual presentation. The same
4
+ business content can be hosted by a routed drawer, dialog, or full page.
5
+
6
+ The Registry also installs a lazy-loaded Demo route at `/route-surfaces` with
7
+ live drawer, dialog, child-page, nested-drawer, and mixed page/drawer/dialog
8
+ scenarios plus a Prompt generator.
9
+
10
+ ## Components
11
+
12
+ - `RouteDrawer` supports URL-backed nested drawers and push-style stacking.
13
+ - `RouteDialog` provides the same close contract for modal routes.
14
+ - `RoutePage` provides the close context without an overlay.
15
+ - `useRouteSurfaceClose` lets content request a close without knowing how it is
16
+ presented.
17
+ - `useRefineUnsavedChangesGuard` adapts Refine's unsaved-change state to the
18
+ route surface close lifecycle and renders a shadcn Alert Dialog confirmation.
19
+
20
+ Route definitions, ACL guards, resource data fetching, and application-specific
21
+ paths remain application concerns.
@@ -0,0 +1,266 @@
1
+ import { ArrowLeft, ArrowRight, Layers3 } from "lucide-react";
2
+ import { useOutlet, useNavigate } from "react-router";
3
+
4
+ import { Badge } from "@/components/ui/badge";
5
+ import { Button } from "@/components/ui/button";
6
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
7
+ import {
8
+ RouteDialog,
9
+ RouteDrawer,
10
+ RoutePage,
11
+ useRouteSurfaceClose,
12
+ } from "../index";
13
+ import { RouteSurfacePromptGenerator } from "./prompt-generator";
14
+ import { routeSurfaceScenarios } from "./scenarios";
15
+
16
+ const demoBase = "/route-surfaces";
17
+
18
+ export function RouteSurfacesDemoHome() {
19
+ const navigate = useNavigate();
20
+ const overlay = useOutlet();
21
+
22
+ return (
23
+ <>
24
+ <div className="space-y-6">
25
+ <div className="space-y-2">
26
+ <Badge variant="secondary">Route surfaces</Badge>
27
+ <h1 className="font-heading text-3xl font-semibold tracking-tight">
28
+ URL-backed pages and overlays
29
+ </h1>
30
+ <p className="max-w-3xl text-muted-foreground">
31
+ Keep business content independent from whether it appears as a page,
32
+ drawer, dialog, or nested combination. Every preview below supports a
33
+ direct URL and browser history.
34
+ </p>
35
+ </div>
36
+
37
+ <div className="grid gap-4 lg:grid-cols-2 xl:grid-cols-3">
38
+ {routeSurfaceScenarios.map((scenario) => (
39
+ <Card key={scenario.id} className="flex flex-col">
40
+ <CardHeader>
41
+ <div className="flex items-center justify-between gap-3">
42
+ <Badge variant="outline">Scenario {scenario.number}</Badge>
43
+ <span className="text-xs text-muted-foreground">
44
+ {scenario.routeShape}
45
+ </span>
46
+ </div>
47
+ <CardTitle>{scenario.title}</CardTitle>
48
+ <CardDescription>{scenario.description}</CardDescription>
49
+ </CardHeader>
50
+ <CardContent className="mt-auto">
51
+ <Button
52
+ type="button"
53
+ variant="outline"
54
+ className="w-full"
55
+ onClick={() => navigate(scenario.path)}
56
+ >
57
+ Open preview
58
+ <ArrowRight />
59
+ </Button>
60
+ </CardContent>
61
+ </Card>
62
+ ))}
63
+ </div>
64
+
65
+ <RouteSurfacePromptGenerator />
66
+ </div>
67
+ {overlay}
68
+ </>
69
+ );
70
+ }
71
+
72
+ export function DemoDrawerRoute() {
73
+ const navigate = useNavigate();
74
+ const nested = useOutlet();
75
+
76
+ return (
77
+ <RouteDrawer
78
+ title="Customer details"
79
+ description="A routed drawer keeps the scenario page mounted behind it."
80
+ closeLabel="Close"
81
+ closeTo={demoBase}
82
+ nested={nested}
83
+ >
84
+ <DemoSurfaceBody
85
+ label="Drawer"
86
+ title="Northwind renewal"
87
+ description="Business content does not know that it is rendered inside a drawer."
88
+ >
89
+ <Button onClick={() => navigate("second")}>
90
+ Open second-level drawer
91
+ <ArrowRight />
92
+ </Button>
93
+ </DemoSurfaceBody>
94
+ </RouteDrawer>
95
+ );
96
+ }
97
+
98
+ export function DemoSecondDrawerRoute() {
99
+ return (
100
+ <RouteDrawer
101
+ title="Renewal activity"
102
+ description="The lower drawer is pushed outward and remains behind the layer mask."
103
+ closeLabel="Close"
104
+ closeTo={`${demoBase}/drawer`}
105
+ >
106
+ <DemoSurfaceBody
107
+ label="Second-level drawer"
108
+ title="Latest review"
109
+ description="Clicking this layer's backdrop closes only the top drawer."
110
+ />
111
+ </RouteDrawer>
112
+ );
113
+ }
114
+
115
+ export function DemoDialogRoute() {
116
+ return (
117
+ <RouteDialog
118
+ title="Approve renewal"
119
+ description="A modal route for a focused decision."
120
+ closeLabel="Close"
121
+ closeTo={demoBase}
122
+ >
123
+ <DemoSurfaceBody
124
+ label="Dialog"
125
+ title="Approval required"
126
+ description="The page stays mounted while the URL represents the active dialog."
127
+ />
128
+ </RouteDialog>
129
+ );
130
+ }
131
+
132
+ export function DemoPageRoute() {
133
+ const nested = useOutlet();
134
+
135
+ return (
136
+ <RoutePage closeTo={demoBase}>
137
+ <DemoPageContent nested={nested} />
138
+ </RoutePage>
139
+ );
140
+ }
141
+
142
+ function DemoPageContent({ nested }: { nested: React.ReactNode }) {
143
+ const close = useRouteSurfaceClose();
144
+ const navigate = useNavigate();
145
+
146
+ return (
147
+ <>
148
+ <div className="space-y-6">
149
+ <div className="flex flex-wrap items-start justify-between gap-4">
150
+ <div className="space-y-2">
151
+ <Badge variant="secondary">Child page</Badge>
152
+ <h1 className="font-heading text-3xl font-semibold tracking-tight">
153
+ Customer workspace
154
+ </h1>
155
+ <p className="text-muted-foreground">
156
+ This route replaces the Demo home instead of rendering inside its Outlet.
157
+ </p>
158
+ </div>
159
+ <Button variant="outline" onClick={() => void close()}>
160
+ <ArrowLeft />
161
+ Back to scenarios
162
+ </Button>
163
+ </div>
164
+
165
+ <Card>
166
+ <CardHeader>
167
+ <div className="flex items-center gap-2">
168
+ <Layers3 className="size-5 text-primary" />
169
+ <CardTitle>Page-owned workflow</CardTitle>
170
+ </div>
171
+ <CardDescription>
172
+ A page can still host routed overlays without becoming coupled to their content.
173
+ </CardDescription>
174
+ </CardHeader>
175
+ <CardContent>
176
+ <Button onClick={() => navigate("drawer")}>Open page drawer</Button>
177
+ </CardContent>
178
+ </Card>
179
+ </div>
180
+ {nested}
181
+ </>
182
+ );
183
+ }
184
+
185
+ export function DemoPageDrawerRoute() {
186
+ const navigate = useNavigate();
187
+ const nested = useOutlet();
188
+
189
+ return (
190
+ <RouteDrawer
191
+ title="Customer activity"
192
+ description="This drawer belongs to the child page route."
193
+ closeLabel="Close"
194
+ closeTo={`${demoBase}/page`}
195
+ nested={nested}
196
+ >
197
+ <DemoSurfaceBody
198
+ label="Page drawer"
199
+ title="Renewal conversation"
200
+ description="Open a dialog above this drawer to test mixed presentation types."
201
+ >
202
+ <Button onClick={() => navigate("dialog")}>
203
+ Open confirmation dialog
204
+ <ArrowRight />
205
+ </Button>
206
+ </DemoSurfaceBody>
207
+ </RouteDrawer>
208
+ );
209
+ }
210
+
211
+ export function DemoPageDrawerDialogRoute() {
212
+ return (
213
+ <RouteDialog
214
+ title="Confirm follow-up"
215
+ description="The dialog is nested in the drawer route subtree."
216
+ closeLabel="Close"
217
+ closeTo={`${demoBase}/page/drawer`}
218
+ >
219
+ <DemoSurfaceBody
220
+ label="Nested dialog"
221
+ title="Schedule a follow-up?"
222
+ description="Closing returns to the drawer; closing the drawer then returns to the page."
223
+ />
224
+ </RouteDialog>
225
+ );
226
+ }
227
+
228
+ function DemoSurfaceBody({
229
+ label,
230
+ title,
231
+ description,
232
+ children,
233
+ }: {
234
+ label: string;
235
+ title: string;
236
+ description: string;
237
+ children?: React.ReactNode;
238
+ }) {
239
+ return (
240
+ <div className="min-h-0 flex-1 overflow-y-auto px-5 py-5">
241
+ <div className="space-y-5">
242
+ <Badge variant="outline">{label}</Badge>
243
+ <div className="space-y-2">
244
+ <h2 className="text-xl font-semibold">{title}</h2>
245
+ <p className="text-muted-foreground">{description}</p>
246
+ </div>
247
+ <div className="grid gap-3 rounded-lg border bg-muted/30 p-4 sm:grid-cols-2">
248
+ <DemoField label="Owner" value="Ada Lovelace" />
249
+ <DemoField label="Status" value="In review" />
250
+ <DemoField label="Value" value="$48,000" />
251
+ <DemoField label="Renewal" value="September 30" />
252
+ </div>
253
+ {children ? <div>{children}</div> : null}
254
+ </div>
255
+ </div>
256
+ );
257
+ }
258
+
259
+ function DemoField({ label, value }: { label: string; value: string }) {
260
+ return (
261
+ <div className="space-y-1">
262
+ <div className="text-xs text-muted-foreground">{label}</div>
263
+ <div className="font-medium">{value}</div>
264
+ </div>
265
+ );
266
+ }
@@ -1,7 +1,8 @@
1
1
  import { Suspense, type ReactNode } from "react";
2
+
2
3
  import { LoadingState } from "@/components/app-shell/loading-state";
3
4
 
4
- export function LazyAclRoute({ children }: { children: ReactNode }) {
5
+ export function LazyRouteSurfaceDemo({ children }: { children: ReactNode }) {
5
6
  return (
6
7
  <Suspense fallback={<LoadingState className="min-h-80" />}>
7
8
  {children}
@@ -0,0 +1,90 @@
1
+ import { Check, Copy } from "lucide-react";
2
+ import { useMemo, useState } from "react";
3
+
4
+ import { Button } from "@/components/ui/button";
5
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
6
+ import { Input } from "@/components/ui/input";
7
+ import { Label } from "@/components/ui/label";
8
+ import {
9
+ Select,
10
+ SelectContent,
11
+ SelectItem,
12
+ SelectTrigger,
13
+ SelectValue,
14
+ } from "@/components/ui/select";
15
+ import { Textarea } from "@/components/ui/textarea";
16
+ import {
17
+ getRouteSurfacePrompt,
18
+ routeSurfaceScenarios,
19
+ type RouteSurfaceScenarioId,
20
+ } from "./scenarios";
21
+
22
+ export function RouteSurfacePromptGenerator() {
23
+ const [scenarioId, setScenarioId] =
24
+ useState<RouteSurfaceScenarioId>("drawer");
25
+ const [target, setTarget] = useState("a customer detail workflow");
26
+ const [copied, setCopied] = useState(false);
27
+ const scenario =
28
+ routeSurfaceScenarios.find((item) => item.id === scenarioId) ??
29
+ routeSurfaceScenarios[0];
30
+ const prompt = useMemo(
31
+ () => getRouteSurfacePrompt(scenario, target),
32
+ [scenario, target]
33
+ );
34
+
35
+ const copyPrompt = async () => {
36
+ await navigator.clipboard.writeText(prompt);
37
+ setCopied(true);
38
+ window.setTimeout(() => setCopied(false), 1500);
39
+ };
40
+
41
+ return (
42
+ <Card>
43
+ <CardHeader>
44
+ <CardTitle>Prompt generator</CardTitle>
45
+ <CardDescription>
46
+ Generate a complete routing scenario, not just an isolated overlay component.
47
+ </CardDescription>
48
+ </CardHeader>
49
+ <CardContent className="space-y-4">
50
+ <div className="grid gap-4 md:grid-cols-2">
51
+ <div className="space-y-2">
52
+ <Label htmlFor="route-surface-scenario">Scenario</Label>
53
+ <Select
54
+ value={scenarioId}
55
+ onValueChange={(value) =>
56
+ setScenarioId(value as RouteSurfaceScenarioId)
57
+ }
58
+ >
59
+ <SelectTrigger id="route-surface-scenario" className="w-full">
60
+ <SelectValue />
61
+ </SelectTrigger>
62
+ <SelectContent>
63
+ {routeSurfaceScenarios.map((item) => (
64
+ <SelectItem key={item.id} value={item.id}>
65
+ {item.number}. {item.title}
66
+ </SelectItem>
67
+ ))}
68
+ </SelectContent>
69
+ </Select>
70
+ </div>
71
+ <div className="space-y-2">
72
+ <Label htmlFor="route-surface-target">Business target</Label>
73
+ <Input
74
+ id="route-surface-target"
75
+ value={target}
76
+ onChange={(event) => setTarget(event.target.value)}
77
+ />
78
+ </div>
79
+ </div>
80
+ <Textarea value={prompt} readOnly className="min-h-80 font-mono text-xs" />
81
+ <div className="flex justify-end">
82
+ <Button type="button" onClick={() => void copyPrompt()}>
83
+ {copied ? <Check /> : <Copy />}
84
+ {copied ? "Copied" : "Copy prompt"}
85
+ </Button>
86
+ </div>
87
+ </CardContent>
88
+ </Card>
89
+ );
90
+ }
@@ -0,0 +1,95 @@
1
+ export type RouteSurfaceScenarioId =
2
+ | "drawer"
3
+ | "dialog"
4
+ | "page"
5
+ | "nested-drawers"
6
+ | "page-drawer-dialog";
7
+
8
+ export type RouteSurfaceScenario = {
9
+ id: RouteSurfaceScenarioId;
10
+ number: string;
11
+ title: string;
12
+ description: string;
13
+ path: string;
14
+ routeShape: string;
15
+ };
16
+
17
+ export const routeSurfaceScenarios: RouteSurfaceScenario[] = [
18
+ {
19
+ id: "drawer",
20
+ number: "1",
21
+ title: "Drawer route",
22
+ description: "Keep the current page mounted and open one URL-backed drawer.",
23
+ path: "/route-surfaces/drawer",
24
+ routeShape: "Page → Drawer",
25
+ },
26
+ {
27
+ id: "dialog",
28
+ number: "2",
29
+ title: "Dialog route",
30
+ description: "Open focused content in a modal while preserving the page URL history.",
31
+ path: "/route-surfaces/dialog",
32
+ routeShape: "Page → Dialog",
33
+ },
34
+ {
35
+ id: "page",
36
+ number: "3",
37
+ title: "Child page",
38
+ description: "Replace the current page instead of rendering the child inside an overlay.",
39
+ path: "/route-surfaces/page",
40
+ routeShape: "Page → Child page",
41
+ },
42
+ {
43
+ id: "nested-drawers",
44
+ number: "4",
45
+ title: "Second-level drawer",
46
+ description: "Push the lower drawer outward and keep the top drawer at a stable size.",
47
+ path: "/route-surfaces/drawer/second",
48
+ routeShape: "Page → Drawer → Drawer",
49
+ },
50
+ {
51
+ id: "page-drawer-dialog",
52
+ number: "5",
53
+ title: "Page, drawer, and dialog",
54
+ description: "Compose different presentation types without coupling business content to them.",
55
+ path: "/route-surfaces/page/drawer/dialog",
56
+ routeShape: "Child page → Drawer → Dialog",
57
+ },
58
+ ];
59
+
60
+ export function getRouteSurfacePrompt(
61
+ scenario: RouteSurfaceScenario,
62
+ target: string
63
+ ) {
64
+ const presentation = {
65
+ drawer:
66
+ "Keep the current page mounted and open the target content with RouteDrawer. Closing the drawer returns to the page route.",
67
+ dialog:
68
+ "Keep the current page mounted and open the target content with RouteDialog. The dialog route must be directly addressable and dismissible through the shared close lifecycle.",
69
+ page:
70
+ "Render the target content as a real child page with RoutePage. Register it as a page route branch rather than placing it inside a drawer outlet.",
71
+ "nested-drawers":
72
+ "Create a parent RouteDrawer with an Outlet and pass the rendered child outlet through its nested prop. The second RouteDrawer must push the lower drawer outward, keep equal widths, and close back to the parent route.",
73
+ "page-drawer-dialog":
74
+ "Render a RoutePage first, place a RouteDrawer route inside that page, then render a RouteDialog as the drawer's nested route. Closing each layer must return to the immediately lower URL.",
75
+ }[scenario.id];
76
+
77
+ return `Build ${target || "the requested workflow"} with the installed NocoBase route surfaces.
78
+
79
+ Scenario:
80
+ - ${scenario.routeShape}
81
+ - ${presentation}
82
+
83
+ Implementation requirements:
84
+ - Import RouteDrawer, RouteDialog, RoutePage, and useRouteSurfaceClose from @/extensions/nocobase-route-surfaces as needed.
85
+ - Keep data loading, forms, ACL guards, route paths, and business copy in application-owned files.
86
+ - Keep business content independent from its presentation container so the same content can move between a drawer, dialog, or page.
87
+ - Every surface must have an explicit closeTo URL.
88
+ - Use nested React Router routes and Outlet only when the visual layer is genuinely nested.
89
+ - A full child page must be a page route branch, not an Outlet rendered inside a drawer.
90
+ - For editable forms, use useRefineUnsavedChangesGuard, pass its beforeClose callback to the surface, render its confirmation node, and use useRouteSurfaceClose for Cancel and successful submission.
91
+ - Direct URL entry, refresh, browser back/forward, Escape, the close button, and backdrop dismissal must all restore the correct layer stack.
92
+ - Apply ResourceAccessGuard to each business resource/action independently; do not rely on a parent list permission for unrelated child pages.
93
+
94
+ Deliver the complete route definitions and React components using the application's existing design system.`;
95
+ }