@nocobase/portal-template-default 1.0.3 → 1.0.5
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/README.MD +10 -5
- package/package.json +17 -17
- package/registry/nocobase-ai/components/chat/chat-message.tsx +8 -6
- package/registry/nocobase-ai/components/chat/chat-messages.tsx +8 -3
- package/registry/nocobase-ai/components/chat/markdown-message.tsx +26 -0
- package/registry/nocobase-ai/components/chat/sub-agent-conversation.tsx +3 -3
- package/registry/nocobase-ai/demo/configuration-gate.tsx +83 -35
- package/registry/nocobase-i18n/README.md +23 -32
- package/registry/nocobase-i18n/components/language-switcher.tsx +1 -1
- package/registry/nocobase-i18n/demo/index.tsx +2 -2
- package/registry/nocobase-i18n/demo/prompt-generator.tsx +3 -2
- package/registry/nocobase-i18n/extension.tsx +3 -4
- package/registry/nocobase-i18n/index.ts +4 -3
- package/registry/nocobase-i18n/locales/en-US.ts +2 -52
- package/registry/nocobase-i18n/locales/index.ts +8 -0
- package/registry/nocobase-i18n/locales/zh-CN.ts +2 -52
- package/registry/nocobase-i18n/provider.tsx +18 -6
- package/registry/nocobase-i18n/server-resources.ts +13 -31
- package/registry/nocobase-i18n/tests/i18n-regression.mjs +14 -5
- package/registry/nocobase-route-surfaces/README.md +21 -0
- package/registry/nocobase-route-surfaces/demo/index.tsx +266 -0
- package/{src/extensions/nocobase-acl → registry/nocobase-route-surfaces}/demo/lazy-route.tsx +2 -1
- package/registry/nocobase-route-surfaces/demo/prompt-generator.tsx +90 -0
- package/registry/nocobase-route-surfaces/demo/scenarios.ts +95 -0
- package/registry/nocobase-route-surfaces/extension.tsx +119 -0
- package/registry/nocobase-route-surfaces/index.ts +7 -0
- package/registry/nocobase-route-surfaces/route-dialog.tsx +145 -0
- package/registry/nocobase-route-surfaces/route-drawer.tsx +186 -0
- package/registry/nocobase-route-surfaces/route-page.tsx +29 -0
- package/registry/nocobase-route-surfaces/route-surface-context.ts +13 -0
- package/registry/nocobase-route-surfaces/use-refine-unsaved-changes.tsx +106 -0
- package/registry/nocobase-route-surfaces/use-route-surface-close.ts +15 -0
- package/registry/nocobase-route-surfaces/use-route-surface-state.ts +65 -0
- package/registry.config.json +30 -3
- package/scripts/registry.mjs +0 -2
- package/src/App.tsx +65 -35
- package/src/app/extension.ts +1 -2
- package/src/app/extensions.tsx +0 -14
- package/src/components/app-shell/breadcrumb.tsx +67 -2
- package/src/components/app-shell/sidebar.tsx +2 -2
- package/src/components/resources/views/list-view.tsx +3 -2
- package/src/components/ui/sheet.tsx +22 -21
- package/src/lib/nocobase/client.ts +6 -1
- package/src/locales/en-US.ts +81 -0
- package/src/locales/zh-CN.ts +77 -0
- package/src/pages/users/create.tsx +76 -53
- package/src/pages/users/edit.tsx +87 -56
- package/src/pages/users/index.ts +3 -0
- package/src/pages/users/layout.tsx +15 -0
- package/src/pages/users/list.tsx +19 -21
- package/src/pages/users/role-badges.tsx +43 -0
- package/src/pages/users/role-detail.tsx +272 -0
- package/src/pages/users/role-utils.ts +6 -0
- package/src/pages/users/routes.ts +24 -0
- package/src/pages/users/show.tsx +195 -104
- package/src/pages/users/types.ts +13 -0
- package/src/providers/i18n/index.ts +3 -0
- package/{registry/nocobase-i18n → src/providers/i18n}/runtime.ts +63 -16
- package/src/providers/system-settings/context.ts +26 -0
- package/src/providers/system-settings/index.ts +2 -0
- package/src/providers/system-settings/provider.tsx +94 -0
- package/src/extensions/nocobase-acl/README.md +0 -19
- package/src/extensions/nocobase-acl/components/acl-boundary.tsx +0 -106
- package/src/extensions/nocobase-acl/components/index.ts +0 -3
- package/src/extensions/nocobase-acl/components/role-options.ts +0 -36
- package/src/extensions/nocobase-acl/components/role-switcher.tsx +0 -135
- package/src/extensions/nocobase-acl/demo/boundary-api.tsx +0 -186
- package/src/extensions/nocobase-acl/demo/components.tsx +0 -231
- package/src/extensions/nocobase-acl/demo/index.tsx +0 -505
- package/src/extensions/nocobase-acl/demo/prompt-generator.tsx +0 -134
- package/src/extensions/nocobase-acl/demo/role-switcher-prompt-generator.tsx +0 -198
- package/src/extensions/nocobase-acl/demo/scenario-section.tsx +0 -37
- package/src/extensions/nocobase-acl/extension.tsx +0 -71
- package/src/extensions/nocobase-acl/index.ts +0 -1
- package/src/extensions/nocobase-ai/README.md +0 -70
- package/src/extensions/nocobase-ai/adapters/react-hook-form.ts +0 -20
- package/src/extensions/nocobase-ai/assets/nocobase-ai-chat.svg +0 -50
- package/src/extensions/nocobase-ai/components/ai-root-provider.tsx +0 -32
- package/src/extensions/nocobase-ai/components/chat/ai-employee-avatar.tsx +0 -31
- package/src/extensions/nocobase-ai/components/chat/chat-attachment.tsx +0 -92
- package/src/extensions/nocobase-ai/components/chat/chat-compact.tsx +0 -90
- package/src/extensions/nocobase-ai/components/chat/chat-composer.tsx +0 -429
- package/src/extensions/nocobase-ai/components/chat/chat-empty-state.tsx +0 -51
- package/src/extensions/nocobase-ai/components/chat/chat-header.tsx +0 -88
- package/src/extensions/nocobase-ai/components/chat/chat-history-dialog.tsx +0 -77
- package/src/extensions/nocobase-ai/components/chat/chat-message.tsx +0 -252
- package/src/extensions/nocobase-ai/components/chat/chat-messages.tsx +0 -173
- package/src/extensions/nocobase-ai/components/chat/chat-window.tsx +0 -118
- package/src/extensions/nocobase-ai/components/chat/conversation-list.tsx +0 -343
- package/src/extensions/nocobase-ai/components/chat/markdown-message.tsx +0 -165
- package/src/extensions/nocobase-ai/components/chat/model-select-options.tsx +0 -30
- package/src/extensions/nocobase-ai/components/chat/reasoning-panel.tsx +0 -29
- package/src/extensions/nocobase-ai/components/chat/sub-agent-conversation.tsx +0 -142
- package/src/extensions/nocobase-ai/components/chat/tool-call-card.tsx +0 -316
- package/src/extensions/nocobase-ai/components/chat/user-prompt-editor.tsx +0 -99
- package/src/extensions/nocobase-ai/components/chat/work-context-chip.tsx +0 -114
- package/src/extensions/nocobase-ai/components/index.ts +0 -59
- package/src/extensions/nocobase-ai/components/page-elements/ai-form.tsx +0 -44
- package/src/extensions/nocobase-ai/components/page-elements/page-element-provider.tsx +0 -469
- package/src/extensions/nocobase-ai/components/surfaces/chat-dialog.tsx +0 -31
- package/src/extensions/nocobase-ai/components/surfaces/chat-inline.tsx +0 -18
- package/src/extensions/nocobase-ai/components/surfaces/chat-page.tsx +0 -18
- package/src/extensions/nocobase-ai/components/surfaces/chat-side-panel-layout.tsx +0 -54
- package/src/extensions/nocobase-ai/components/surfaces/chat-side-panel.tsx +0 -74
- package/src/extensions/nocobase-ai/components/surfaces/chat-surface-actions.tsx +0 -54
- package/src/extensions/nocobase-ai/components/tools/builtin-tool-renderers.tsx +0 -31
- package/src/extensions/nocobase-ai/components/tools/business-report-dialog.tsx +0 -364
- package/src/extensions/nocobase-ai/components/tools/business-report-renderer.tsx +0 -122
- package/src/extensions/nocobase-ai/components/tools/business-report-utils.tsx +0 -282
- package/src/extensions/nocobase-ai/components/tools/chart-renderer.tsx +0 -99
- package/src/extensions/nocobase-ai/components/tools/echarts-preview.tsx +0 -59
- package/src/extensions/nocobase-ai/components/tools/echarts-runtime-advanced.ts +0 -18
- package/src/extensions/nocobase-ai/components/tools/echarts-runtime-common.ts +0 -20
- package/src/extensions/nocobase-ai/components/tools/echarts-runtime-components.ts +0 -34
- package/src/extensions/nocobase-ai/components/tools/echarts-runtime-hierarchy.ts +0 -24
- package/src/extensions/nocobase-ai/components/tools/echarts-runtime.ts +0 -138
- package/src/extensions/nocobase-ai/components/tools/sub-agent-renderer.tsx +0 -67
- package/src/extensions/nocobase-ai/components/tools/suggestions-renderer.tsx +0 -59
- package/src/extensions/nocobase-ai/components/tools/tool-renderer-provider.tsx +0 -55
- package/src/extensions/nocobase-ai/components/tools/tool-renderer-utils.ts +0 -18
- package/src/extensions/nocobase-ai/components/tools/workflow-renderer.tsx +0 -108
- package/src/extensions/nocobase-ai/components/triggers/ai-chat-floating-trigger.tsx +0 -73
- package/src/extensions/nocobase-ai/components/triggers/ai-employee-shortcut.tsx +0 -149
- package/src/extensions/nocobase-ai/demo/configuration-gate.tsx +0 -49
- package/src/extensions/nocobase-ai/demo/container-showcase.tsx +0 -210
- package/src/extensions/nocobase-ai/demo/floating.tsx +0 -134
- package/src/extensions/nocobase-ai/demo/index.tsx +0 -346
- package/src/extensions/nocobase-ai/demo/interaction-showcase.tsx +0 -151
- package/src/extensions/nocobase-ai/demo/page-context-prompt-generator.tsx +0 -355
- package/src/extensions/nocobase-ai/demo/page-context.tsx +0 -884
- package/src/extensions/nocobase-ai/demo/page-element-showcase.tsx +0 -197
- package/src/extensions/nocobase-ai/demo/prompt-card.tsx +0 -41
- package/src/extensions/nocobase-ai/demo/prompt-generator.tsx +0 -452
- package/src/extensions/nocobase-ai/demo/shortcut.tsx +0 -1207
- package/src/extensions/nocobase-ai/demo/tool-cards.tsx +0 -530
- package/src/extensions/nocobase-ai/extension.tsx +0 -158
- package/src/extensions/nocobase-ai/global-ai-chat.tsx +0 -213
- package/src/extensions/nocobase-ai/index.ts +0 -3
- package/src/extensions/nocobase-ai/providers/ai-provider.tsx +0 -323
- package/src/extensions/nocobase-ai/providers/avatars.ts +0 -817
- package/src/extensions/nocobase-ai/providers/chat-context.tsx +0 -109
- package/src/extensions/nocobase-ai/providers/chat-controller.ts +0 -84
- package/src/extensions/nocobase-ai/providers/chat-message-utils.ts +0 -198
- package/src/extensions/nocobase-ai/providers/chat-provider.tsx +0 -871
- package/src/extensions/nocobase-ai/providers/chat-reducer.ts +0 -147
- package/src/extensions/nocobase-ai/providers/chat-task-utils.ts +0 -105
- package/src/extensions/nocobase-ai/providers/chat-transport.ts +0 -184
- package/src/extensions/nocobase-ai/providers/form-registry.tsx +0 -271
- package/src/extensions/nocobase-ai/providers/frontend-tool-registry.tsx +0 -231
- package/src/extensions/nocobase-ai/providers/index.ts +0 -61
- package/src/extensions/nocobase-ai/providers/model.ts +0 -32
- package/src/extensions/nocobase-ai/providers/page-context.tsx +0 -136
- package/src/extensions/nocobase-ai/providers/stream-coalescer.ts +0 -60
- package/src/extensions/nocobase-ai/providers/stream-event-utils.ts +0 -82
- package/src/extensions/nocobase-ai/providers/stream-parser.ts +0 -61
- package/src/extensions/nocobase-ai/providers/sub-agent-stream.ts +0 -316
- package/src/extensions/nocobase-ai/providers/types.ts +0 -183
- package/src/extensions/nocobase-ai/providers/ui-message-stream.ts +0 -463
- package/src/extensions/nocobase-ai/providers/use-automatic-tool-approval.ts +0 -94
- package/src/extensions/nocobase-ai/providers/use-chat-attachments.ts +0 -206
- package/src/extensions/nocobase-ai/providers/use-chat-message-actions.ts +0 -554
- package/src/extensions/nocobase-ai/providers/use-chat-runtime.ts +0 -210
- package/src/extensions/nocobase-ai/providers/use-chat-work-context.ts +0 -93
- package/src/extensions/nocobase-ai/providers/use-conversation-catalog.ts +0 -108
- package/src/extensions/nocobase-ai/providers/use-conversation-history.ts +0 -178
- package/src/extensions/nocobase-ai/services/index.ts +0 -7
- package/src/extensions/nocobase-ai/services/nocobase-ai-service.ts +0 -454
- package/src/extensions/nocobase-ai/services/types.ts +0 -82
- /package/{registry/nocobase-i18n → src/providers/i18n}/i18n-provider.ts +0 -0
- /package/{registry/nocobase-i18n → src/providers/i18n}/locale-store.ts +0 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { AppExtension } from "@/app/extension";
|
|
2
|
+
import { Layers3 } from "lucide-react";
|
|
3
|
+
import { lazy } from "react";
|
|
4
|
+
import { Route } from "react-router";
|
|
5
|
+
import { LazyRouteSurfaceDemo } from "./demo/lazy-route";
|
|
6
|
+
|
|
7
|
+
const DemoHome = lazy(() =>
|
|
8
|
+
import("./demo").then((module) => ({
|
|
9
|
+
default: module.RouteSurfacesDemoHome,
|
|
10
|
+
}))
|
|
11
|
+
);
|
|
12
|
+
const DemoDrawer = lazy(() =>
|
|
13
|
+
import("./demo").then((module) => ({ default: module.DemoDrawerRoute }))
|
|
14
|
+
);
|
|
15
|
+
const DemoSecondDrawer = lazy(() =>
|
|
16
|
+
import("./demo").then((module) => ({
|
|
17
|
+
default: module.DemoSecondDrawerRoute,
|
|
18
|
+
}))
|
|
19
|
+
);
|
|
20
|
+
const DemoDialog = lazy(() =>
|
|
21
|
+
import("./demo").then((module) => ({ default: module.DemoDialogRoute }))
|
|
22
|
+
);
|
|
23
|
+
const DemoPage = lazy(() =>
|
|
24
|
+
import("./demo").then((module) => ({ default: module.DemoPageRoute }))
|
|
25
|
+
);
|
|
26
|
+
const DemoPageDrawer = lazy(() =>
|
|
27
|
+
import("./demo").then((module) => ({
|
|
28
|
+
default: module.DemoPageDrawerRoute,
|
|
29
|
+
}))
|
|
30
|
+
);
|
|
31
|
+
const DemoPageDrawerDialog = lazy(() =>
|
|
32
|
+
import("./demo").then((module) => ({
|
|
33
|
+
default: module.DemoPageDrawerDialogRoute,
|
|
34
|
+
}))
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const routeSurfacesExtension: AppExtension = {
|
|
38
|
+
id: "nocobase-route-surfaces",
|
|
39
|
+
resources: [
|
|
40
|
+
{
|
|
41
|
+
name: "route-surfaces",
|
|
42
|
+
list: "/route-surfaces",
|
|
43
|
+
meta: {
|
|
44
|
+
label: "Route surfaces",
|
|
45
|
+
icon: <Layers3 />,
|
|
46
|
+
description: "URL-backed drawer, dialog, page, and nested route patterns.",
|
|
47
|
+
acl: { type: "authenticated" },
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
routes: (
|
|
52
|
+
<>
|
|
53
|
+
<Route
|
|
54
|
+
key="route-surfaces-overlays"
|
|
55
|
+
path="/route-surfaces"
|
|
56
|
+
element={
|
|
57
|
+
<LazyRouteSurfaceDemo>
|
|
58
|
+
<DemoHome />
|
|
59
|
+
</LazyRouteSurfaceDemo>
|
|
60
|
+
}
|
|
61
|
+
>
|
|
62
|
+
<Route
|
|
63
|
+
path="drawer"
|
|
64
|
+
element={
|
|
65
|
+
<LazyRouteSurfaceDemo>
|
|
66
|
+
<DemoDrawer />
|
|
67
|
+
</LazyRouteSurfaceDemo>
|
|
68
|
+
}
|
|
69
|
+
>
|
|
70
|
+
<Route
|
|
71
|
+
path="second"
|
|
72
|
+
element={
|
|
73
|
+
<LazyRouteSurfaceDemo>
|
|
74
|
+
<DemoSecondDrawer />
|
|
75
|
+
</LazyRouteSurfaceDemo>
|
|
76
|
+
}
|
|
77
|
+
/>
|
|
78
|
+
</Route>
|
|
79
|
+
<Route
|
|
80
|
+
path="dialog"
|
|
81
|
+
element={
|
|
82
|
+
<LazyRouteSurfaceDemo>
|
|
83
|
+
<DemoDialog />
|
|
84
|
+
</LazyRouteSurfaceDemo>
|
|
85
|
+
}
|
|
86
|
+
/>
|
|
87
|
+
</Route>
|
|
88
|
+
<Route
|
|
89
|
+
key="route-surfaces-page"
|
|
90
|
+
path="/route-surfaces/page"
|
|
91
|
+
element={
|
|
92
|
+
<LazyRouteSurfaceDemo>
|
|
93
|
+
<DemoPage />
|
|
94
|
+
</LazyRouteSurfaceDemo>
|
|
95
|
+
}
|
|
96
|
+
>
|
|
97
|
+
<Route
|
|
98
|
+
path="drawer"
|
|
99
|
+
element={
|
|
100
|
+
<LazyRouteSurfaceDemo>
|
|
101
|
+
<DemoPageDrawer />
|
|
102
|
+
</LazyRouteSurfaceDemo>
|
|
103
|
+
}
|
|
104
|
+
>
|
|
105
|
+
<Route
|
|
106
|
+
path="dialog"
|
|
107
|
+
element={
|
|
108
|
+
<LazyRouteSurfaceDemo>
|
|
109
|
+
<DemoPageDrawerDialog />
|
|
110
|
+
</LazyRouteSurfaceDemo>
|
|
111
|
+
}
|
|
112
|
+
/>
|
|
113
|
+
</Route>
|
|
114
|
+
</Route>
|
|
115
|
+
</>
|
|
116
|
+
),
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
export default routeSurfacesExtension;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export * from "./route-dialog";
|
|
2
|
+
export * from "./route-drawer";
|
|
3
|
+
export * from "./route-page";
|
|
4
|
+
export * from "./route-surface-context";
|
|
5
|
+
export * from "./use-refine-unsaved-changes";
|
|
6
|
+
export * from "./use-route-surface-close";
|
|
7
|
+
export type { RouteSurfaceBeforeClose } from "./use-route-surface-state";
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { Dialog as DialogPrimitive } from "@base-ui/react/dialog";
|
|
2
|
+
import { X } from "lucide-react";
|
|
3
|
+
import { type ReactNode, useCallback, useContext } from "react";
|
|
4
|
+
|
|
5
|
+
import { Button } from "@/components/ui/button";
|
|
6
|
+
import { cn } from "@/lib/utils";
|
|
7
|
+
import {
|
|
8
|
+
RouteOverlayDepthContext,
|
|
9
|
+
RouteSurfaceContext,
|
|
10
|
+
} from "./route-surface-context";
|
|
11
|
+
import {
|
|
12
|
+
type RouteSurfaceBeforeClose,
|
|
13
|
+
useRouteSurfaceState,
|
|
14
|
+
} from "./use-route-surface-state";
|
|
15
|
+
|
|
16
|
+
export function RouteDialog({
|
|
17
|
+
title,
|
|
18
|
+
description,
|
|
19
|
+
actions,
|
|
20
|
+
children,
|
|
21
|
+
closeLabel,
|
|
22
|
+
closeTo,
|
|
23
|
+
beforeClose,
|
|
24
|
+
className,
|
|
25
|
+
nested,
|
|
26
|
+
}: {
|
|
27
|
+
title: ReactNode;
|
|
28
|
+
description?: ReactNode;
|
|
29
|
+
actions?: ReactNode;
|
|
30
|
+
children: ReactNode;
|
|
31
|
+
closeLabel: string;
|
|
32
|
+
closeTo: string;
|
|
33
|
+
beforeClose?: RouteSurfaceBeforeClose;
|
|
34
|
+
className?: string;
|
|
35
|
+
nested?: ReactNode;
|
|
36
|
+
}) {
|
|
37
|
+
const parentOverlayDepth = useContext(RouteOverlayDepthContext);
|
|
38
|
+
const isNestedOverlay = parentOverlayDepth > 0;
|
|
39
|
+
const { open, setOpen, close, navigateAfterClose } = useRouteSurfaceState({
|
|
40
|
+
closeTo,
|
|
41
|
+
beforeClose,
|
|
42
|
+
animated: true,
|
|
43
|
+
});
|
|
44
|
+
const handleOpenChange = useCallback(
|
|
45
|
+
(nextOpen: boolean, eventDetails: DialogPrimitive.Root.ChangeEventDetails) => {
|
|
46
|
+
if (nextOpen) {
|
|
47
|
+
setOpen(true);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
eventDetails.cancel();
|
|
52
|
+
void close();
|
|
53
|
+
},
|
|
54
|
+
[close, setOpen]
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<RouteSurfaceContext.Provider value={close}>
|
|
59
|
+
<RouteOverlayDepthContext.Provider value={parentOverlayDepth + 1}>
|
|
60
|
+
<DialogPrimitive.Root
|
|
61
|
+
open={open}
|
|
62
|
+
onOpenChange={handleOpenChange}
|
|
63
|
+
onOpenChangeComplete={(nextOpen) => {
|
|
64
|
+
if (!nextOpen) navigateAfterClose();
|
|
65
|
+
}}
|
|
66
|
+
>
|
|
67
|
+
<DialogPrimitive.Portal>
|
|
68
|
+
{isNestedOverlay ? (
|
|
69
|
+
<RouteDialogBackdrop open={open} onClose={close} />
|
|
70
|
+
) : (
|
|
71
|
+
<DialogPrimitive.Backdrop
|
|
72
|
+
className="fixed inset-0 isolate z-50 bg-black/10 duration-150 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0"
|
|
73
|
+
onClick={(event) => {
|
|
74
|
+
event.stopPropagation();
|
|
75
|
+
void close();
|
|
76
|
+
}}
|
|
77
|
+
/>
|
|
78
|
+
)}
|
|
79
|
+
<DialogPrimitive.Popup
|
|
80
|
+
className={cn(
|
|
81
|
+
"fixed top-1/2 left-1/2 z-50 flex max-h-[calc(100dvh-2rem)] w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 flex-col overflow-hidden rounded-xl bg-popover text-sm text-popover-foreground ring-1 ring-foreground/10 duration-150 outline-none sm:max-w-2xl 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",
|
|
82
|
+
className
|
|
83
|
+
)}
|
|
84
|
+
>
|
|
85
|
+
<header className="relative shrink-0 border-b px-5 py-4 pr-14 text-left">
|
|
86
|
+
<div className="flex items-start justify-between gap-4">
|
|
87
|
+
<div className="min-w-0 space-y-1">
|
|
88
|
+
<DialogPrimitive.Title className="font-heading truncate text-lg font-medium">
|
|
89
|
+
{title}
|
|
90
|
+
</DialogPrimitive.Title>
|
|
91
|
+
{description ? (
|
|
92
|
+
<DialogPrimitive.Description className="text-sm text-muted-foreground">
|
|
93
|
+
{description}
|
|
94
|
+
</DialogPrimitive.Description>
|
|
95
|
+
) : null}
|
|
96
|
+
</div>
|
|
97
|
+
{actions ? (
|
|
98
|
+
<div className="flex shrink-0 items-center gap-2">
|
|
99
|
+
{actions}
|
|
100
|
+
</div>
|
|
101
|
+
) : null}
|
|
102
|
+
</div>
|
|
103
|
+
<Button
|
|
104
|
+
type="button"
|
|
105
|
+
variant="ghost"
|
|
106
|
+
size="icon-sm"
|
|
107
|
+
className="absolute top-3 right-3"
|
|
108
|
+
onClick={() => void close()}
|
|
109
|
+
>
|
|
110
|
+
<X />
|
|
111
|
+
<span className="sr-only">{closeLabel}</span>
|
|
112
|
+
</Button>
|
|
113
|
+
</header>
|
|
114
|
+
{children}
|
|
115
|
+
</DialogPrimitive.Popup>
|
|
116
|
+
</DialogPrimitive.Portal>
|
|
117
|
+
{nested}
|
|
118
|
+
</DialogPrimitive.Root>
|
|
119
|
+
</RouteOverlayDepthContext.Provider>
|
|
120
|
+
</RouteSurfaceContext.Provider>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function RouteDialogBackdrop({
|
|
125
|
+
open,
|
|
126
|
+
onClose,
|
|
127
|
+
}: {
|
|
128
|
+
open: boolean;
|
|
129
|
+
onClose: () => Promise<boolean>;
|
|
130
|
+
}) {
|
|
131
|
+
return (
|
|
132
|
+
<div
|
|
133
|
+
role="presentation"
|
|
134
|
+
aria-hidden="true"
|
|
135
|
+
className={cn(
|
|
136
|
+
"fixed inset-0 isolate z-50 bg-black/10 transition-opacity duration-150 supports-backdrop-filter:backdrop-blur-xs",
|
|
137
|
+
open ? "opacity-100" : "pointer-events-none opacity-0"
|
|
138
|
+
)}
|
|
139
|
+
onClick={(event) => {
|
|
140
|
+
event.stopPropagation();
|
|
141
|
+
void onClose();
|
|
142
|
+
}}
|
|
143
|
+
/>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { Drawer as DrawerPrimitive } from "@base-ui/react/drawer";
|
|
2
|
+
import { X } from "lucide-react";
|
|
3
|
+
import {
|
|
4
|
+
type CSSProperties,
|
|
5
|
+
type ReactNode,
|
|
6
|
+
useCallback,
|
|
7
|
+
useContext,
|
|
8
|
+
} from "react";
|
|
9
|
+
|
|
10
|
+
import { Button } from "@/components/ui/button";
|
|
11
|
+
import { cn } from "@/lib/utils";
|
|
12
|
+
import {
|
|
13
|
+
RouteOverlayDepthContext,
|
|
14
|
+
RouteSurfaceContext,
|
|
15
|
+
} from "./route-surface-context";
|
|
16
|
+
import {
|
|
17
|
+
type RouteSurfaceBeforeClose,
|
|
18
|
+
useRouteSurfaceState,
|
|
19
|
+
} from "./use-route-surface-state";
|
|
20
|
+
|
|
21
|
+
export function RouteDrawer({
|
|
22
|
+
title,
|
|
23
|
+
description,
|
|
24
|
+
actions,
|
|
25
|
+
children,
|
|
26
|
+
closeLabel,
|
|
27
|
+
closeTo,
|
|
28
|
+
beforeClose,
|
|
29
|
+
className,
|
|
30
|
+
nested,
|
|
31
|
+
stackOffset = "4rem",
|
|
32
|
+
}: {
|
|
33
|
+
title: ReactNode;
|
|
34
|
+
description?: ReactNode;
|
|
35
|
+
actions?: ReactNode;
|
|
36
|
+
children: ReactNode;
|
|
37
|
+
closeLabel: string;
|
|
38
|
+
closeTo: string;
|
|
39
|
+
beforeClose?: RouteSurfaceBeforeClose;
|
|
40
|
+
className?: string;
|
|
41
|
+
nested?: ReactNode;
|
|
42
|
+
stackOffset?: string;
|
|
43
|
+
}) {
|
|
44
|
+
const parentOverlayDepth = useContext(RouteOverlayDepthContext);
|
|
45
|
+
const isNestedOverlay = parentOverlayDepth > 0;
|
|
46
|
+
const { open, setOpen, close, navigateAfterClose } = useRouteSurfaceState({
|
|
47
|
+
closeTo,
|
|
48
|
+
beforeClose,
|
|
49
|
+
animated: true,
|
|
50
|
+
});
|
|
51
|
+
const handleOpenChange = useCallback(
|
|
52
|
+
(nextOpen: boolean, eventDetails: DrawerPrimitive.Root.ChangeEventDetails) => {
|
|
53
|
+
if (nextOpen) {
|
|
54
|
+
setOpen(true);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
eventDetails.cancel();
|
|
59
|
+
void close();
|
|
60
|
+
},
|
|
61
|
+
[close, setOpen]
|
|
62
|
+
);
|
|
63
|
+
const stackStyle = {
|
|
64
|
+
"--peek": stackOffset,
|
|
65
|
+
"--stack-step": 0,
|
|
66
|
+
"--bleed": "0px",
|
|
67
|
+
} as CSSProperties;
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<RouteSurfaceContext.Provider value={close}>
|
|
71
|
+
<RouteOverlayDepthContext.Provider value={parentOverlayDepth + 1}>
|
|
72
|
+
<DrawerPrimitive.Root
|
|
73
|
+
open={open}
|
|
74
|
+
swipeDirection="right"
|
|
75
|
+
onOpenChange={handleOpenChange}
|
|
76
|
+
onOpenChangeComplete={(nextOpen) => {
|
|
77
|
+
if (!nextOpen) navigateAfterClose();
|
|
78
|
+
}}
|
|
79
|
+
>
|
|
80
|
+
<DrawerPrimitive.Portal>
|
|
81
|
+
{isNestedOverlay ? (
|
|
82
|
+
<RouteDrawerBackdrop open={open} onClose={close} />
|
|
83
|
+
) : (
|
|
84
|
+
<DrawerPrimitive.Backdrop
|
|
85
|
+
className="fixed inset-0 z-50 min-h-dvh bg-black/10 opacity-[max(var(--drawer-overlay-min-opacity,0),calc(1-var(--drawer-swipe-progress)))] transition-opacity duration-450 ease-[cubic-bezier(0.32,0.72,0,1)] select-none data-ending-style:pointer-events-none data-ending-style:opacity-0 data-starting-style:opacity-0 data-swiping:duration-0 supports-backdrop-filter:backdrop-blur-xs"
|
|
86
|
+
onClick={(event) => {
|
|
87
|
+
event.stopPropagation();
|
|
88
|
+
void close();
|
|
89
|
+
}}
|
|
90
|
+
/>
|
|
91
|
+
)}
|
|
92
|
+
<DrawerPrimitive.Viewport className="pointer-events-none fixed inset-0 z-50 select-none">
|
|
93
|
+
<DrawerPrimitive.Popup
|
|
94
|
+
style={stackStyle}
|
|
95
|
+
className={cn(
|
|
96
|
+
"group/route-drawer pointer-events-auto fixed inset-y-0 right-0 z-50 flex w-full transform-[translate3d(var(--translate-x,0px),0,0)] flex-row bg-popover text-sm text-popover-foreground shadow-xl transition-transform duration-450 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-transform outline-none sm:w-full lg:w-[42vw] lg:min-w-[40rem]",
|
|
97
|
+
"[--stack-peek-offset:max(0px,calc((var(--nested-drawers)-var(--stack-progress))*var(--peek)))] [--stack-progress:clamp(0,var(--drawer-swipe-progress),1)]",
|
|
98
|
+
"[--closed-transform:translate3d(calc(100%+2px),0,0)] [--translate-x:calc(var(--drawer-swipe-movement-x)-var(--stack-peek-offset))]",
|
|
99
|
+
"data-ending-style:transform-(--closed-transform) data-ending-style:opacity-[0.9999] data-starting-style:transform-(--closed-transform) data-swiping:duration-0 data-nested-drawer-swiping:duration-0",
|
|
100
|
+
"data-nested-drawer-open:overflow-hidden",
|
|
101
|
+
className
|
|
102
|
+
)}
|
|
103
|
+
>
|
|
104
|
+
<div
|
|
105
|
+
aria-hidden="true"
|
|
106
|
+
className="pointer-events-none absolute inset-0 z-20 bg-black/10 opacity-0 transition-opacity duration-450 ease-[cubic-bezier(0.22,1,0.36,1)] group-data-nested-drawer-open/route-drawer:opacity-100 supports-backdrop-filter:backdrop-blur-xs"
|
|
107
|
+
/>
|
|
108
|
+
<DrawerPrimitive.Content className="flex min-h-0 flex-1 flex-col overflow-hidden overscroll-contain select-text group-data-swiping/route-drawer:select-none">
|
|
109
|
+
<header className="relative shrink-0 border-b px-5 py-4 pr-14 text-left">
|
|
110
|
+
<div className="flex items-start justify-between gap-4">
|
|
111
|
+
<div className="min-w-0 space-y-1">
|
|
112
|
+
<DrawerPrimitive.Title className="font-heading truncate text-lg font-medium text-foreground">
|
|
113
|
+
{title}
|
|
114
|
+
</DrawerPrimitive.Title>
|
|
115
|
+
{description ? (
|
|
116
|
+
<DrawerPrimitive.Description className="text-sm text-balance text-muted-foreground">
|
|
117
|
+
{description}
|
|
118
|
+
</DrawerPrimitive.Description>
|
|
119
|
+
) : null}
|
|
120
|
+
</div>
|
|
121
|
+
{actions ? (
|
|
122
|
+
<div className="flex shrink-0 items-center gap-2">
|
|
123
|
+
{actions}
|
|
124
|
+
</div>
|
|
125
|
+
) : null}
|
|
126
|
+
</div>
|
|
127
|
+
<Button
|
|
128
|
+
type="button"
|
|
129
|
+
variant="ghost"
|
|
130
|
+
size="icon-sm"
|
|
131
|
+
className="absolute top-3 right-3"
|
|
132
|
+
onClick={() => void close()}
|
|
133
|
+
>
|
|
134
|
+
<X />
|
|
135
|
+
<span className="sr-only">{closeLabel}</span>
|
|
136
|
+
</Button>
|
|
137
|
+
</header>
|
|
138
|
+
{children}
|
|
139
|
+
</DrawerPrimitive.Content>
|
|
140
|
+
</DrawerPrimitive.Popup>
|
|
141
|
+
</DrawerPrimitive.Viewport>
|
|
142
|
+
</DrawerPrimitive.Portal>
|
|
143
|
+
{nested}
|
|
144
|
+
</DrawerPrimitive.Root>
|
|
145
|
+
</RouteOverlayDepthContext.Provider>
|
|
146
|
+
</RouteSurfaceContext.Provider>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function RouteDrawerBackdrop({
|
|
151
|
+
open,
|
|
152
|
+
onClose,
|
|
153
|
+
}: {
|
|
154
|
+
open: boolean;
|
|
155
|
+
onClose: () => Promise<boolean>;
|
|
156
|
+
}) {
|
|
157
|
+
return (
|
|
158
|
+
<div
|
|
159
|
+
role="presentation"
|
|
160
|
+
aria-hidden="true"
|
|
161
|
+
className={cn(
|
|
162
|
+
"fixed inset-0 z-50 min-h-dvh bg-black/10 transition-opacity duration-450 ease-[cubic-bezier(0.32,0.72,0,1)] select-none supports-backdrop-filter:backdrop-blur-xs",
|
|
163
|
+
open ? "opacity-100" : "pointer-events-none opacity-0"
|
|
164
|
+
)}
|
|
165
|
+
onClick={(event) => {
|
|
166
|
+
event.stopPropagation();
|
|
167
|
+
void onClose();
|
|
168
|
+
}}
|
|
169
|
+
/>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function RouteDrawerFooter({
|
|
174
|
+
className,
|
|
175
|
+
...props
|
|
176
|
+
}: React.ComponentProps<"div">) {
|
|
177
|
+
return (
|
|
178
|
+
<div
|
|
179
|
+
className={cn(
|
|
180
|
+
"mt-auto flex shrink-0 flex-col gap-2 border-t px-5 py-4",
|
|
181
|
+
className
|
|
182
|
+
)}
|
|
183
|
+
{...props}
|
|
184
|
+
/>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
import { RouteSurfaceContext } from "./route-surface-context";
|
|
4
|
+
import {
|
|
5
|
+
type RouteSurfaceBeforeClose,
|
|
6
|
+
useRouteSurfaceState,
|
|
7
|
+
} from "./use-route-surface-state";
|
|
8
|
+
|
|
9
|
+
export function RoutePage({
|
|
10
|
+
children,
|
|
11
|
+
closeTo,
|
|
12
|
+
beforeClose,
|
|
13
|
+
}: {
|
|
14
|
+
children: ReactNode;
|
|
15
|
+
closeTo: string;
|
|
16
|
+
beforeClose?: RouteSurfaceBeforeClose;
|
|
17
|
+
}) {
|
|
18
|
+
const { close } = useRouteSurfaceState({
|
|
19
|
+
closeTo,
|
|
20
|
+
beforeClose,
|
|
21
|
+
animated: false,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<RouteSurfaceContext.Provider value={close}>
|
|
26
|
+
{children}
|
|
27
|
+
</RouteSurfaceContext.Provider>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { createContext } from "react";
|
|
2
|
+
|
|
3
|
+
export type RouteSurfaceCloseOptions = {
|
|
4
|
+
skipBeforeClose?: boolean;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export type RouteSurfaceClose = (
|
|
8
|
+
options?: RouteSurfaceCloseOptions
|
|
9
|
+
) => Promise<boolean>;
|
|
10
|
+
|
|
11
|
+
export const RouteSurfaceContext = createContext<RouteSurfaceClose | null>(null);
|
|
12
|
+
|
|
13
|
+
export const RouteOverlayDepthContext = createContext(0);
|