@nocobase/app-template-default 1.0.0-beta.57 → 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 -21
- package/README.MD +7 -53
- package/cli/plugins.ts +1 -7
- 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 -6
- 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/config.example.yml +0 -16
- package/eslint.config.js +0 -13
- package/index.html +48 -16
- package/package.json +24 -30
- package/server/config/index.ts +0 -6
- package/server/plugins.ts +0 -4
- 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/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/ai.ts +0 -27
- 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 -76
package/AGENTS.md
CHANGED
|
@@ -34,7 +34,7 @@ Read the one page your task needs, not the whole directory.
|
|
|
34
34
|
|
|
35
35
|
`.agents/skills/nocobase-app-upgrade/` is a separate Skill for a separate job: merging a newer release of the template this application was generated from. Read it when the task is upgrading the template rather than building a feature, and read it before touching anything — an upgrade done by copying the newest template over this application destroys the work that made it this application.
|
|
36
36
|
|
|
37
|
-
`.agents/skills/nocobase-deployment/` covers moving this application from source to a production server
|
|
37
|
+
`.agents/skills/nocobase-deployment/` covers moving this application from source to a production server: building for the target platform, what the archive does and does not carry, migrations and business data, production configuration, and what to verify afterwards. Read it before building for deployment, and when an application starts in production but does not work.
|
|
38
38
|
|
|
39
39
|
## Where things go
|
|
40
40
|
|
|
@@ -69,13 +69,13 @@ A feature with a page and an API touches five places: a migration for the table,
|
|
|
69
69
|
|
|
70
70
|
Layouts own breadcrumb route context and render the current page's trail in the header after the sidebar toggle (`client/components/breadcrumbs.tsx`): the route trail by default, or the trail a page declares with `usePageBreadcrumb` from `@nocobase/app-client` when it names records. Pages do not render breadcrumbs themselves. `AppRouter` selects routes and layouts. See [pages and routes](.agents/skills/nocobase-app-development/references/frontend/references/page.md) for each layout's scope.
|
|
71
71
|
|
|
72
|
-
`client/layouts/components/layout-header.tsx` is a presentation container accepting children. The sidebar is shadcn's Sidebar: `components/app-sidebar.tsx` wraps its provider, desktop icon mode and phone sheet, and `components/navigation-menu.tsx` draws the permission-filtered route tree with `SidebarMenu*` and controlled `Collapsible` sub-menus. Leave `client/components/ui/sidebar.tsx` as the shadcn CLI writes it and customize from outside: widths go through the provider's `style` in spacing units, the phone sheet is rendered in `app-sidebar.tsx` with a translated title, and the provider's Ctrl/Cmd+B shortcut is stopped at `document`. App
|
|
72
|
+
`client/layouts/components/layout-header.tsx` is a presentation container accepting children. The sidebar is shadcn's Sidebar: `components/app-sidebar.tsx` wraps its provider, desktop icon mode and phone sheet, and `components/navigation-menu.tsx` draws the permission-filtered route tree with `SidebarMenu*` and controlled `Collapsible` sub-menus. Leave `client/components/ui/sidebar.tsx` as the shadcn CLI writes it and customize from outside: widths go through the provider's `style` in spacing units, the phone sheet is rendered in `app-sidebar.tsx` with a translated title, and the provider's Ctrl/Cmd+B shortcut is stopped at `document`. The App layout owns menus, branding, permissions and redirects. Desktop icon mode shows leaf labels as zero-delay tooltips and opens groups in hover and keyboard-focus popovers, preserving filtered navigation, parent-page links, nested groups and the selected group's highlight. Keep this behavior aligned across layouts. Desktop collapse state is shared through `useSidebarPreference` at `nocobase:sidebar:collapsed` across applications on the same origin; whether the phone sheet is open stays local to each layout.
|
|
73
73
|
|
|
74
|
-
The Settings
|
|
74
|
+
The header carries no Settings or Inbox entry. An application has no separate settings area: a page that configures something is an ordinary route in `client/routes.ts` with `navigation`, placed in the sidebar like any other page, for example under a Settings navigation group. The `/inbox` page declares no navigation; give it an entry where the application wants one.
|
|
75
75
|
|
|
76
76
|
`client/routing/`, `client/layouts/`, `client/theme/`, the server entry points, the `package.json` scripts that call `nocobase`, and the tsconfigs are the scaffolding the template provides. It is still this application's own source — it shipped to the user and they may change it — but it is the part the template evolves, so an edit there is what a future upgrade has to reconcile.
|
|
77
77
|
|
|
78
|
-
Prefer the mechanism the system already provides. Declare a page in `client/routes.ts` and add `navigation` when it needs a menu entry. Refine resources add no menu entries and, with no data provider registered, load nothing; pages load data through `useApiClient`. A
|
|
78
|
+
Prefer the mechanism the system already provides. Declare a page in `client/routes.ts` and add `navigation` when it needs a menu entry. Refine resources add no menu entries and, with no data provider registered, load nothing; pages load data through `useApiClient`. A page uses `authz` to restrict access; a plugin page is customized through an option or an override. Before editing the shell to add a menu, check the route and its `navigation` declaration.
|
|
79
79
|
|
|
80
80
|
When the built-in mechanism genuinely cannot express what is being asked, changing this structure is a legitimate answer — not a last resort to apologize for. Do it deliberately, and leave the next agent enough to work with:
|
|
81
81
|
|
|
@@ -104,13 +104,13 @@ Route paths are application-internal. Never write the deployment base path such
|
|
|
104
104
|
|
|
105
105
|
`auth` controls browser navigation only: `required` for signed-in pages, `guest` for sign-in and registration, `optional` for pages that work either way. It is not server security. An endpoint the page calls enforces its own authentication independently.
|
|
106
106
|
|
|
107
|
-
|
|
107
|
+
Declare every page with `defineAppRoutes()`; there is no `/settings` or `/dev` surface. An administrative page is an ordinary route with `navigation` and `authz`, enforced by the server, and plugins contribute no settings pages for the application to host.
|
|
108
108
|
|
|
109
|
-
**Declare navigation on the route.**
|
|
109
|
+
**Declare navigation on the route.** Menus read `navigation: { title: 'navigation.orders' }`; titles resolve in the owning locale namespace. Add the translation in `client/locales/`. Refine resources do not add menu entries.
|
|
110
110
|
|
|
111
111
|
Use recursive groups to organize menus; their path is optional. Pages may also have children, but must manually render `Outlet`. For URL-addressable dialogs and drawers, declare the child in `defineAppRoutes()` in `client/routes.ts`, place the owning page's `Outlet`, then render `RouteDialog` or `RouteDrawer`. A child page of its own — a record's page, a form too long for a dialog — returns `RouteChildPage` around its `PageContainer`; only tab content renders inline, and a child route that returns a bare `PageContainer` renders below the parent's content instead of covering it. Call `useRouteOverlay()` only from a descendant rendered inside the overlay, including its footer, never from the page returning the wrapper. A record opens over the page the user is on: a dashboard or a board that shows records declares their drawer under its own route instead of linking to the list's, and a row menu's Edit opens its own dialog rather than the one stacked on the drawer. On a page with tabs the user is on the tab being shown: an overlay the page's header opens is declared under every tab and linked through the current tab (`` `${tab}/edit` ``, never a bare `edit`, which resolves beside the tabs), so it stacks on that tab and closing returns to it. Read `.agents/skills/nocobase-app-development/references/frontend/references/overlay.md` before implementing overlays or close guards.
|
|
112
112
|
|
|
113
|
-
Declare `authz` on the first page of every path: `{ resource: { type: 'page', id: 'orders' }, action: 'access' }` for a product page,
|
|
113
|
+
Declare `authz` on the first page of every path: `{ resource: { type: 'page', id: 'orders' }, action: 'access' }` for a product page, `'unrestricted'` for a page only root may open, or `'skip'`. A nested page that omits it inherits its nearest ancestor page's value, and a child's own value overrides it. A first page that omits it still registers, with a development warning: protected pages become unrestricted-only, which hides them from everyone but root, while guest and optional pages skip the check. Always declare it rather than relying on that default. Parent guards still apply when a child skips. Menus and loaders read the declared rule; endpoints enforce authorization independently. The page id identifies stored page grants, so changing it requires migrating the grants that reference it.
|
|
114
114
|
|
|
115
115
|
### Components and styling
|
|
116
116
|
|
|
@@ -125,7 +125,7 @@ pnpm exec prettier --write client/components/ui/card.tsx
|
|
|
125
125
|
|
|
126
126
|
Build your own components by composing these primitives, and put them in `client/components/`. A page below another one — a covering child page, a record's own page — leaves by the header's breadcrumb when its route declares `breadcrumb` or the page declares its trail with `usePageBreadcrumb`, and otherwise by `BackButton` (`client/components/back-button.tsx`) above its heading; never both.
|
|
127
127
|
|
|
128
|
-
`PageContainer`, `PageHeader`, `RouteDialog`, `RouteDrawer`, `RouteChildPage` and `useRouteOverlay` in `client/components/` come from the NocoBase UI Library, as do the authentication blocks in `client/extensions/nocobase-auth-forms/`, `nocobase-auth-methods/`, `nocobase-auth-split-layout/` and `nocobase-device-approval/` that `client/pages/auth/` composes, the inbox block in `client/extensions/nocobase-inbox/` that `client/pages/inbox.tsx` renders on `/inbox
|
|
128
|
+
`PageContainer`, `PageHeader`, `RouteDialog`, `RouteDrawer`, `RouteChildPage` and `useRouteOverlay` in `client/components/` come from the NocoBase UI Library, as do the authentication blocks in `client/extensions/nocobase-auth-forms/`, `nocobase-auth-methods/`, `nocobase-auth-split-layout/` and `nocobase-device-approval/` that `client/pages/auth/` composes, and the inbox block in `client/extensions/nocobase-inbox/` that `client/pages/inbox.tsx` renders on `/inbox`; like the rest of the source, they belong to the application. `yes n | pnpm exec shadcn add @nocobase/<item>` adds another item from the library: a single component lands in `client/components/`, a complete feature in `client/extensions/nocobase-<item>/`. A date field and a list's `DataTable` are the application's own components: compose `DatePicker` in `client/components/date-picker.tsx` from the `calendar` and `popover` primitives, following shadcn's Date Picker guide, and build `DataTable` in `client/components/` from the `table` primitive and `@tanstack/react-table`, following shadcn's Data Table guide. The CLI adds the packages an item or primitive needs to `dependencies`: move the new ones, such as `react-day-picker` for `calendar`, to `devDependencies`, as "Adding a dependency" below explains for everything client code imports, but leave `@nocobase/i18n` in `dependencies`, where the server needs it, and put back the `^` of its range, which the CLI pins.
|
|
129
129
|
|
|
130
130
|
**Look components up in the shadcn skill.** The development Skill carries the shadcn/ui skill, unchanged from shadcn 4.21.0, in `.agents/skills/nocobase-app-development/references/frontend/shadcn/`. Read `references/frontend/references/shadcn.md` first: it routes to the skill's rules and states where this application departs from them, among others `pnpm exec shadcn` instead of `npx shadcn@latest`, no `shadcn apply` or `--preset`, and create, edit and detail as `RouteDialog` or `RouteDrawer` child routes rather than a `Sheet`. `pnpm exec shadcn docs <name>` gives one primitive's documentation and examples for the Base UI version. For a whole page, start from the worked example that `references/frontend/references/example.md` indexes.
|
|
131
131
|
|
|
@@ -133,6 +133,8 @@ Style with the semantic Tailwind tokens — `bg-background`, `text-foreground`,
|
|
|
133
133
|
|
|
134
134
|
**Visual consistency is a whole-application property.** Match the surrounding code's spacing, typography, and component choices. If a change genuinely calls for a different look, change the application's design tokens in `client/theme/themes/*.css` so every page moves together. Never restyle only the part you are working on — a page that looks different from the rest is a defect, not a customization.
|
|
135
135
|
|
|
136
|
+
The default brand palette and density live in `client/theme/themes/*.css`; shared hover/cursor and Badge geometry live in `client/styles.css`, outside generated shadcn primitives. For semantic status labels, use `StatusBadge` from `client/components/status-badge.tsx` with `neutral`, `info`, `warning`, or `success`; choose the tone by meaning and keep the visible status text. Reuse these shared components and tokens in new pages so template upgrades can carry the design consistently.
|
|
137
|
+
|
|
136
138
|
**Follow the UI guidelines.** `.agents/skills/nocobase-app-development/references/frontend/ui-guidelines.md` sets the conventions every page follows, among them the page header and the single placement of the primary action, empty states, row actions behind one "…" menu, dates picked from a `Calendar` in a `Popover` rather than a native date input, and medium-width drawers; the frontend workflow says how much of it to read for a change.
|
|
137
139
|
|
|
138
140
|
### Server routes
|
|
@@ -185,7 +187,7 @@ Bind services to their existing tokens in a provider's `register()`; calling `cr
|
|
|
185
187
|
|
|
186
188
|
The running application serves an OpenAPI 3.1 document of every `/api` route it has — its own, every registered plugin's, the data endpoints and Better Auth's `/api/auth/...` — at `<origin><APP_BASE_PATH>/api/swagger` (JSON), with Swagger UI at `<origin><APP_BASE_PATH>/api/swagger/docs`. Locally that is `http://127.0.0.1:13000/main/api/swagger/docs`, with `/main` replaced by the actual `APP_BASE_PATH`.
|
|
187
189
|
|
|
188
|
-
Reading either needs a signed-in session or an API key: open the Swagger UI in a browser where you are signed in, or send a key in `x-api-key`. A user creates keys
|
|
190
|
+
Reading either needs a signed-in session or an API key: open the Swagger UI in a browser where you are signed in, or send a key in `x-api-key`. A signed-in user creates keys with `POST <APP_BASE_PATH>/api/apiKeys`; the `nocobase-app-plugin-api-keys` Skill covers the plugin. Without a valid credential the routes answer `401` with reason `API_DOCS_UNAUTHENTICATED`, and an application with no access check registered, such as one without the authentication plugin, answers `404`.
|
|
189
191
|
|
|
190
192
|
```bash
|
|
191
193
|
curl -H "x-api-key: <key>" http://127.0.0.1:13000/main/api/swagger
|
|
@@ -294,15 +296,14 @@ Route overrides must stay lazy, declare a `componentEntry`, and load a default-e
|
|
|
294
296
|
|
|
295
297
|
**You are not starting from scratch.** This application ships with NocoBase packages that already solve whole categories of requirement, and packages may publish a Skill explaining how to use them. `pnpm nocobase skills sync` copies Skills from direct `@nocobase/*` dependencies and registered plugins into `.agents/skills/`. Before implementing a feature, check whether an installed and registered plugin already covers it:
|
|
296
298
|
|
|
297
|
-
| The requirement sounds like
|
|
298
|
-
|
|
|
299
|
-
|
|
|
300
|
-
|
|
|
301
|
-
|
|
|
302
|
-
|
|
|
303
|
-
|
|
|
304
|
-
|
|
|
305
|
-
| Translated text and language switching | `@nocobase/app-plugin-i18n` |
|
|
299
|
+
| The requirement sounds like | Read the Skill for |
|
|
300
|
+
| -------------------------------------------------------------------------------------------- | ------------------------------------- |
|
|
301
|
+
| Email, IM, or in-app messages; notifying someone that something happened | `@nocobase/app-plugin-notification` |
|
|
302
|
+
| Roles, permissions, "user A may only see their own records", field-level or row-level access | `@nocobase/app-plugin-authorization` |
|
|
303
|
+
| Sign-in, registration, sessions, password reset | `@nocobase/app-plugin-authentication` |
|
|
304
|
+
| User listing, account state, password reset, and application-owned role assignment | `@nocobase/app-plugin-users` |
|
|
305
|
+
| File upload and metadata through Repository | `@nocobase/app-plugin-file` |
|
|
306
|
+
| Translated text and language switching | `@nocobase/app-plugin-i18n` |
|
|
306
307
|
|
|
307
308
|
Run `pnpm nocobase skills sync` if `.agents/skills/` is missing or looks out of date, then read the Skill for the plugin you need. It documents that plugin's public entries, the ownership boundary, and how to verify the result — which is faster and more correct than inferring an API from its source.
|
|
308
309
|
|
|
@@ -314,7 +315,7 @@ Building a permission system, a notification sender, or a job scheduler by hand
|
|
|
314
315
|
|
|
315
316
|
Before removing a direct `@nocobase/*` dependency, search application imports, Client/Server/CLI plugin registrations, routes, services, configuration, tests, and build scripts for its package name and public contracts. Migrate or delete those references first. The command below changes dependency metadata and generated Skills; it does not edit application source or configuration, so never use it to remove a capability the application still needs.
|
|
316
317
|
|
|
317
|
-
Use `pnpm nocobase package remove @nocobase/example`. It invokes the application's package manager so `package.json` and the lockfile stay consistent, then deletes only synchronized Skills recorded as belonging to that package. For an `@nocobase/app-plugin-*` package it delegates to the plugin unregister
|
|
318
|
+
Use `pnpm nocobase package remove @nocobase/example`. It invokes the application's package manager so `package.json` and the lockfile stay consistent, then deletes only synchronized Skills recorded as belonging to that package. For an `@nocobase/app-plugin-*` package it delegates to the plugin unregister command, removing its Client, Server, and CLI registrations together; `pnpm nocobase plugin unregister <name>` remains a supported plugin-specific entry point. Preview with `--dry-run`, and use `--json` when another tool needs structured output.
|
|
318
319
|
|
|
319
320
|
After an interrupted or manual removal, first confirm the manifest no longer declares the package, then run a full `pnpm nocobase skills sync` to reconcile stale package-owned output. `package remove` may also be passed a package already absent from the manifest to clean recorded historical Skill ownership, and it must not uninstall or clean Skills owned by another package.
|
|
320
321
|
|
|
@@ -356,7 +357,7 @@ Declare such a package in `dependencies` when you write the code; nothing will r
|
|
|
356
357
|
|
|
357
358
|
`CDN_BASE_URL` sets Vite's `base` only during a production build: `CDN_BASE_URL=https://cdn.example.com/my-app/v1/ pnpm build`. Use a full HTTPS URL ending in `/`; unset, empty or whitespace-only values keep the default `./`. `pnpm build` reads it from the process environment, then `.env.local`, then `.env`; development ignores it and continues using `APP_BASE_PATH`. It is a build parameter, absent from `config variables` and `dist/variables.json`, with no `config.yml` setting. Changing it requires rebuilding.
|
|
358
359
|
|
|
359
|
-
Upload the static files from `dist/client/` to that CDN prefix with their directory structure preserved, and enable CORS for the application's origin. Keep `index.html` and the complete client tree on the application server so it can inject runtime configuration into HTML and serve files still addressed there. Use `resolveAssetUrl('/assets/logo.png')` from `@nocobase/app-client` for a file shipped as `dist/client/assets/logo.png`; template logos use this helper. It reads Vite's build-time `BASE_URL`, uses an absolute HTTP(S) CDN base when configured, and otherwise falls back to the runtime mount path. Keep API paths, page links and runtime-generated files
|
|
360
|
+
Upload the static files from `dist/client/` to that CDN prefix with their directory structure preserved, and enable CORS for the application's origin. Keep `index.html` and the complete client tree on the application server so it can inject runtime configuration into HTML and serve files still addressed there. Use `resolveAssetUrl('/assets/logo.png')` from `@nocobase/app-client` for a file shipped as `dist/client/assets/logo.png`; template logos use this helper. It reads Vite's build-time `BASE_URL`, uses an absolute HTTP(S) CDN base when configured, and otherwise falls back to the runtime mount path. Keep API paths, page links and runtime-generated files on `resolveAppUrl`. Vite does not rewrite arbitrary URL strings. The README's CDN section describes deployment and Docker usage.
|
|
360
361
|
|
|
361
362
|
For a Docker source build, pass `--build-arg CDN_BASE_URL=https://cdn.example.com/my-app/v1/`. With `DIST=prebuilt`, set `CDN_BASE_URL` when building `dist/` beforehand. Neither a prebuilt image's build argument nor `docker run -e CDN_BASE_URL=...` can change compiled asset URLs.
|
|
362
363
|
|
|
@@ -419,7 +420,7 @@ The account menu checks Better Auth sign-out results before refreshing the sessi
|
|
|
419
420
|
|
|
420
421
|
The authorization provider clears the permission snapshot before rendering a new session. Route navigation and page guards subscribe to the authorization revision; preserve these checks when customizing the shell so account changes and permission updates take effect without a reload. Pending checks hide protected content, and failed checks deny access.
|
|
421
422
|
|
|
422
|
-
Navigation groups retain their expanded or collapsed state while the navigation tree stays mounted. Selecting a new page expands its ancestor groups without collapsing other groups; users can still collapse the active group manually.
|
|
423
|
+
Navigation groups retain their expanded or collapsed state while the navigation tree stays mounted. Selecting a new page expands its ancestor groups without collapsing other groups; users can still collapse the active group manually.
|
|
423
424
|
|
|
424
425
|
## Development logging
|
|
425
426
|
|
|
@@ -430,3 +431,7 @@ Navigation groups retain their expanded or collapsed state while the navigation
|
|
|
430
431
|
`runtime.paths`, configuration context `paths`, and `app.paths` share one resolved `AppPaths` object. Use `paths.storage('...')`, `paths.database('...')`, or the corresponding directory fields. `AppPathOptions` is input only; application path policies run before the final object is created and configuration is loaded. Standalone entries declare the deployment root in `server/runtime.ts` so the server and CLI share persistent storage outside the compiled code directory.
|
|
431
432
|
|
|
432
433
|
`server/app.ts` calls `createAppFromRuntime(runtime)` to transfer configuration, paths, mode and Host logging policy and bind `runtime.app`. Keep Provider, middleware and route registration explicit and ordered; `startApplicationInScope` owns startup and shutdown binding.
|
|
434
|
+
|
|
435
|
+
## Branded loading
|
|
436
|
+
|
|
437
|
+
Use `Loading` for page or panel waits and `Spinner` (the shadcn-compatible adapter to `BrandSpinner`) for inline pending actions, uploads, and refresh indicators. Both share the stable NocoBase mark and orbit animation; do not introduce independent Lucide spinners for application loading. Keep skeletons for content placeholders and progress bars for determinate progress. `index.html` has the matching pre-JavaScript mark and must stay visually consistent. All animations respect reduced motion.
|
package/README.MD
CHANGED
|
@@ -87,7 +87,7 @@ client/ Browser application
|
|
|
87
87
|
├── pages/ Your page components
|
|
88
88
|
├── components/ Your components
|
|
89
89
|
│ └── ui/ shadcn/ui primitives
|
|
90
|
-
├── layouts/ App
|
|
90
|
+
├── layouts/ App layout
|
|
91
91
|
│ └── components/ Layout containers, navigation, branding and account controls
|
|
92
92
|
├── routing/ Route rendering, access checks, loading and error UI
|
|
93
93
|
├── locales/ Your translated strings
|
|
@@ -203,7 +203,7 @@ pnpm nocobase package remove @nocobase/example --dry-run
|
|
|
203
203
|
pnpm nocobase package remove @nocobase/example --json
|
|
204
204
|
```
|
|
205
205
|
|
|
206
|
-
For an ordinary package, the command uses the application's package manager to remove the dependency and update both `package.json` and the lockfile, then removes only the Skill directories recorded as owned by that package. For an `@nocobase/app-plugin-*` package, it reuses the `plugin unregister`
|
|
206
|
+
For an ordinary package, the command uses the application's package manager to remove the dependency and update both `package.json` and the lockfile, then removes only the Skill directories recorded as owned by that package. For an `@nocobase/app-plugin-*` package, it reuses the `plugin unregister` steps so Client, Server, and CLI registrations are removed together with the dependency and its Skills. `pnpm nocobase plugin unregister <name>` remains available when the task is specifically expressed as unregistering a plugin.
|
|
207
207
|
|
|
208
208
|
After an interrupted or manual removal, make sure the manifest no longer declares the package and run a full `pnpm nocobase skills sync` to reconcile stale package-owned output. Passing a package already absent from the manifest to `package remove` can clean its recorded historical Skill ownership without uninstalling any other package.
|
|
209
209
|
|
|
@@ -253,7 +253,7 @@ For example, `pnpm build --target linux-x64 --node-version 24` produces:
|
|
|
253
253
|
}
|
|
254
254
|
```
|
|
255
255
|
|
|
256
|
-
`nodeMajor` describes the target runtime, not the Node version used to run the build. No target patch version is implied. These fields provide platform and Node ABI checks for
|
|
256
|
+
`nodeMajor` describes the target runtime, not the Node version used to run the build. No target patch version is implied. These fields provide platform and Node ABI checks for deployment; they do not describe system libraries, CPU instruction requirements, or other runtime prerequisites.
|
|
257
257
|
|
|
258
258
|
If your application has no native modules — `pg`, `mysql2`, and `tedious` are all plain JavaScript — no platform-specific binaries need retargeting. The build still records the selected target, and the deployment runtime must satisfy `engines.node`.
|
|
259
259
|
|
|
@@ -277,11 +277,11 @@ import { resolveAssetUrl } from '@nocobase/app-client';
|
|
|
277
277
|
<img src={resolveAssetUrl('/assets/logo-mark.png')} alt='Logo' />;
|
|
278
278
|
```
|
|
279
279
|
|
|
280
|
-
Pass the file's path relative to `dist/client/`, with or without a leading slash. `resolveAssetUrl` reads `import.meta.env.BASE_URL`, which Vite replaces with the configured `base` during the application build; the browser does not read `CDN_BASE_URL` from the server environment. With an absolute HTTP(S) CDN base it returns the CDN URL, otherwise it falls back to `resolveAppUrl` and the runtime `APP_BASE_PATH`. Already resolved URLs are preserved. Use `resolveAppUrl` for API URLs, page links and runtime-generated resources
|
|
280
|
+
Pass the file's path relative to `dist/client/`, with or without a leading slash. `resolveAssetUrl` reads `import.meta.env.BASE_URL`, which Vite replaces with the configured `base` during the application build; the browser does not read `CDN_BASE_URL` from the server environment. With an absolute HTTP(S) CDN base it returns the CDN URL, otherwise it falls back to `resolveAppUrl` and the runtime `APP_BASE_PATH`. Already resolved URLs are preserved. Use `resolveAppUrl` for API URLs, page links and runtime-generated resources; the CDN must actually contain a file before it can be served there.
|
|
281
281
|
|
|
282
282
|
#### Packing the build
|
|
283
283
|
|
|
284
|
-
`pnpm build --tar` writes `storage/exports/dist.tar.gz`, the artifact
|
|
284
|
+
`pnpm build --tar` writes `storage/exports/dist.tar.gz`, the artifact `@nocobase/app-installer` installs on a server.
|
|
285
285
|
|
|
286
286
|
The archive has two levels: `config.example.yml` and `dist/` sit at the root, and the build is inside `dist/`. What `dist/` holds varies with the application; this is the shape:
|
|
287
287
|
|
|
@@ -399,13 +399,7 @@ Tests live in `tests/`, never beside the source they cover. Put logic and integr
|
|
|
399
399
|
|
|
400
400
|
### Notification testing
|
|
401
401
|
|
|
402
|
-
|
|
403
|
-
notification logs settings page to send a real message through the durable
|
|
404
|
-
Notification Manager. The form is generated from safe Channel test metadata;
|
|
405
|
-
Email tests require an address and IM tests use the configured logical target.
|
|
406
|
-
The button remains visible so configuration problems are discoverable. The
|
|
407
|
-
server enforces the `notification:test` `send` permission only when the message
|
|
408
|
-
is submitted.
|
|
402
|
+
With the `notification:test` `send` permission, send a real message through the durable Notification Manager with `POST <APP_BASE_PATH>/api/notifications/testSends`. `GET <APP_BASE_PATH>/api/notifications/testTargets` lists each configured channel and the fields its test takes: Email tests require an address and IM tests use the configured logical target. The server enforces the permission when the message is submitted.
|
|
409
403
|
|
|
410
404
|
## API documentation
|
|
411
405
|
|
|
@@ -414,7 +408,7 @@ The application documents every `/api` endpoint it serves — its own routes, ev
|
|
|
414
408
|
- Swagger UI: `<origin><APP_BASE_PATH>/api/swagger/docs`, such as `http://127.0.0.1:13000/main/api/swagger/docs`
|
|
415
409
|
- JSON: `<origin><APP_BASE_PATH>/api/swagger`
|
|
416
410
|
|
|
417
|
-
Only a signed-in user or an API key may read them; anyone else gets `401`. Open the Swagger UI in a browser where you are signed in, or, from a script, send an API key created
|
|
411
|
+
Only a signed-in user or an API key may read them; anyone else gets `401`. Open the Swagger UI in a browser where you are signed in, or, from a script, send an API key created with `POST <APP_BASE_PATH>/api/apiKeys` in the `x-api-key` header. Swagger UI's **Authorize** button takes a key too and keeps it across reloads.
|
|
418
412
|
|
|
419
413
|
```bash
|
|
420
414
|
curl -H "x-api-key: <key>" http://127.0.0.1:13000/main/api/swagger
|
|
@@ -430,46 +424,6 @@ Replace `/main` with the application's `APP_BASE_PATH`. An application without t
|
|
|
430
424
|
|
|
431
425
|
MIT
|
|
432
426
|
|
|
433
|
-
## Publish to an application Hub
|
|
434
|
-
|
|
435
|
-
This template depends on `@nocobase/hub-cli`, which gives the application the `pnpm nocobase hub` commands; removing the dependency removes them. Run them in the source checkout or in CI: a built `dist/` does not register them, because they build and send the application from its sources.
|
|
436
|
-
|
|
437
|
-
### Add the Hub as a remote
|
|
438
|
-
|
|
439
|
-
A remote is one App on one Hub, addressed as `<Hub URL>/apps/<App ID>`, where the Hub URL includes the Hub's mount path:
|
|
440
|
-
|
|
441
|
-
```bash
|
|
442
|
-
pnpm nocobase hub remote add origin http://127.0.0.1:13000/main/apps/crm
|
|
443
|
-
pnpm nocobase hub remote list
|
|
444
|
-
```
|
|
445
|
-
|
|
446
|
-
Remotes are saved in `.nocobase/hub.json`, which holds addresses only and belongs in version control. The first remote added is the default; every other `hub` command takes `--remote <name>` to pick another, and `hub remote remove <name>` deletes one.
|
|
447
|
-
|
|
448
|
-
### Create and save a publishing key
|
|
449
|
-
|
|
450
|
-
The `hub` commands authenticate with a Hub publishing key, created in Hub rather than in this application. Open **API Keys** in the Hub navigation (`<Hub URL>/api-keys`); reaching that page requires `hub.app / manage-api-keys`, granted to `hub-administrator` and `hub-operator` by default. In **Create API Key**, select this application under **Applications**, or **All applications (including future apps)**, then grant the permissions the commands below need: **Upload release** for `pnpm nocobase hub upload`, and both **Upload release** and **Deploy release** for `pnpm nocobase hub deploy`.
|
|
451
|
-
|
|
452
|
-
The plaintext key is shown once at creation; while the key stays active its creator can copy it again from the same page. Bound applications and granted permissions cannot be changed afterwards, so a key created without `Deploy release` has to be deleted and recreated rather than edited. A key never exceeds its creator's current permissions, so uploads fail if the creator later loses access to the bound application. See [Publish applications with Hub](https://github.com/nocobase/nocobase3/blob/develop/docs/docs/en/deployment/hub.md) for the console walkthrough.
|
|
453
|
-
|
|
454
|
-
Save the key with `pnpm nocobase hub auth login`, which asks for it without echoing it and checks with the Hub that it opens the App. It is stored outside the project in `$XDG_CONFIG_HOME/nocobase/hub-credentials.json` (`~/.config` when that is unset, `%APPDATA%\nocobase` on Windows), readable by the owner only, one key per remote URL. The commands read no key or Hub address from `.env`, the environment or command-line flags. `hub auth status` reports whether each remote has a key the Hub accepts, and `hub auth logout` removes a saved key. In CI, pipe the key in from a secret:
|
|
455
|
-
|
|
456
|
-
```bash
|
|
457
|
-
echo "$HUB_KEY" | pnpm nocobase hub auth login --remote origin --with-token
|
|
458
|
-
pnpm nocobase hub deploy --remote origin --json
|
|
459
|
-
```
|
|
460
|
-
|
|
461
|
-
### Deploy
|
|
462
|
-
|
|
463
|
-
```bash
|
|
464
|
-
pnpm nocobase hub deploy --json
|
|
465
|
-
pnpm nocobase hub upload --json
|
|
466
|
-
pnpm nocobase hub deploy --release-id <releaseId> --json
|
|
467
|
-
```
|
|
468
|
-
|
|
469
|
-
`hub deploy` asks the Hub which platform it runs Apps on, builds the application for it with `nocobase build --target … --node-version … --tar`, uploads the archive as a Release and deploys it, waiting for the result unless `--no-wait` is given. Do not run `pnpm build --tar` first. When the Hub already has the archive, its Release is deployed. `hub upload` builds and uploads without deploying, and `hub deploy --release-id` deploys a Release already on the Hub, which is also how to roll back. `--no-build` sends the existing `storage/exports/dist.tar.gz` and `--file` another archive; both are checked against the Hub's platform and fail with `BUILD_TARGET_MISMATCH` when they were built for another. Hub has no automatic or manual deployment setting; scripts decide which commands to run.
|
|
470
|
-
|
|
471
|
-
`node_modules/@nocobase/hub-cli/README.md` is the full reference: every flag, `--config` for the runtime configuration, retry identity, the `--json` document and the exit codes. Exit 3 does not mean the deployment failed or was cancelled; inspect the deployment in Hub and retry with the same `--idempotency-key` if necessary.
|
|
472
|
-
|
|
473
427
|
## Runtime paths and application creation
|
|
474
428
|
|
|
475
429
|
`runtime.paths`, configuration context `paths`, and `app.paths` share one resolved `AppPaths` object. Use `paths.storage('...')`, `paths.database('...')`, or the corresponding directory fields. `AppPathOptions` is input only; application path policies run before the final object is created and configuration is loaded. Standalone entries declare the deployment root in `server/runtime.ts` so the server and CLI share persistent storage outside the compiled code directory. That storage is `storage/` under the deployment root unless `APP_STORAGE_DIR` names another directory, absolute or relative to the deployment root; set it when the deployment root itself is replaced on every release, as an installer that keeps one directory per release does.
|
package/cli/plugins.ts
CHANGED
|
@@ -1,14 +1,8 @@
|
|
|
1
1
|
import { defineCliPlugins, type AppCliPlugins } from '@nocobase/app-cli';
|
|
2
|
-
import aiEmployee from '@nocobase/app-plugin-ai-employee/cli';
|
|
3
2
|
import scheduler from '@nocobase/app-plugin-scheduler/cli';
|
|
4
|
-
import workflow from '@nocobase/app-plugin-workflow/cli';
|
|
5
3
|
|
|
6
4
|
// Array order is command registration order. A plugin contributes its commands
|
|
7
5
|
// by appearing in this list; removing its entry and its import removes them.
|
|
8
|
-
const cliPlugins: AppCliPlugins = defineCliPlugins([
|
|
9
|
-
workflow,
|
|
10
|
-
scheduler,
|
|
11
|
-
aiEmployee,
|
|
12
|
-
]);
|
|
6
|
+
const cliPlugins: AppCliPlugins = defineCliPlugins([scheduler]);
|
|
13
7
|
|
|
14
8
|
export default cliPlugins;
|
|
@@ -6,11 +6,17 @@ export default defineClientExtension({
|
|
|
6
6
|
host: {
|
|
7
7
|
theme: () =>
|
|
8
8
|
document.documentElement.classList.contains('dark') ? 'dark' : 'light',
|
|
9
|
-
brandColor: () =>
|
|
9
|
+
brandColor: () => {
|
|
10
|
+
const primary = getComputedStyle(document.documentElement)
|
|
11
|
+
.getPropertyValue('--primary')
|
|
12
|
+
.trim();
|
|
13
|
+
// The annotation host accepts only #RRGGBB; custom themes may use other CSS color formats.
|
|
14
|
+
return /^#[\da-f]{6}$/i.test(primary) ? primary : '#002ad1';
|
|
15
|
+
},
|
|
10
16
|
subscribe(listener) {
|
|
11
17
|
const observer = new MutationObserver(listener);
|
|
12
18
|
observer.observe(document.documentElement, {
|
|
13
|
-
attributeFilter: ['class'],
|
|
19
|
+
attributeFilter: ['class', 'data-theme'],
|
|
14
20
|
attributes: true,
|
|
15
21
|
});
|
|
16
22
|
return () => observer.disconnect();
|
package/client/app.ts
CHANGED
|
@@ -28,8 +28,6 @@ export function createApp(runtime: AppRuntimeContext): ClientApplication {
|
|
|
28
28
|
],
|
|
29
29
|
routes: createElement(AppRouter, {
|
|
30
30
|
clientRoutes: runtime.routes,
|
|
31
|
-
settingsRouteTree: runtime.settingsRouteTree,
|
|
32
|
-
devRouteTree: runtime.devRouteTree,
|
|
33
31
|
}),
|
|
34
32
|
}),
|
|
35
33
|
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { ComponentPropsWithoutRef, ReactElement } from 'react';
|
|
2
|
+
import { cn } from 'cn';
|
|
3
|
+
|
|
4
|
+
import { BRAND_MARK_PATH, BRAND_MARK_TRANSLATE } from './brand-mark-geometry';
|
|
5
|
+
|
|
6
|
+
/** Keep the brand recognizable; the orbit, rather than the bricks, conveys progress. */
|
|
7
|
+
export function BrandLoadingMark({
|
|
8
|
+
className,
|
|
9
|
+
...props
|
|
10
|
+
}: ComponentPropsWithoutRef<'svg'>): ReactElement {
|
|
11
|
+
return (
|
|
12
|
+
<svg
|
|
13
|
+
aria-hidden='true'
|
|
14
|
+
focusable='false'
|
|
15
|
+
viewBox='0 0 64 64'
|
|
16
|
+
className={cn('size-12 shrink-0 text-primary', className)}
|
|
17
|
+
fill='none'
|
|
18
|
+
{...props}
|
|
19
|
+
>
|
|
20
|
+
<circle
|
|
21
|
+
cx='32'
|
|
22
|
+
cy='32'
|
|
23
|
+
r='28'
|
|
24
|
+
stroke='currentColor'
|
|
25
|
+
strokeWidth='2'
|
|
26
|
+
opacity='.12'
|
|
27
|
+
/>
|
|
28
|
+
<g className='nocobase-loading-orbit'>
|
|
29
|
+
<path
|
|
30
|
+
d='M32 4a28 28 0 0 1 28 28'
|
|
31
|
+
stroke='currentColor'
|
|
32
|
+
strokeWidth='3'
|
|
33
|
+
strokeLinecap='round'
|
|
34
|
+
/>
|
|
35
|
+
</g>
|
|
36
|
+
<g
|
|
37
|
+
transform='translate(15 17.176475219726562) scale(.34)'
|
|
38
|
+
fill='currentColor'
|
|
39
|
+
>
|
|
40
|
+
<g transform={BRAND_MARK_TRANSLATE}>
|
|
41
|
+
<path d={BRAND_MARK_PATH} />
|
|
42
|
+
</g>
|
|
43
|
+
</g>
|
|
44
|
+
</svg>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Official Logomark vector from the supplied logos/Transparent/Vectors asset.
|
|
2
|
+
* Keep geometry unchanged; color, size and decorative animation belong to consumers.
|
|
3
|
+
*/
|
|
4
|
+
export const BRAND_MARK_VIEW_BOX = '0 0 100 87.19720458984375';
|
|
5
|
+
export const BRAND_MARK_TRANSLATE = 'translate(0 -6.401886463165283)';
|
|
6
|
+
export const BRAND_MARK_PATH =
|
|
7
|
+
'M99.876 55.369c-.025-.045-.076-.072-.109-.111-.066-.078-.125-.168-.217-.221l-.014-.008-.001-.002h-.001L80.368 43.648v-24.67a.92.92 0 0 0-.124-.459c-.026-.045-.075-.072-.108-.111-.066-.08-.126-.168-.218-.221l-.014-.008-.001-.002h-.002L60.286 6.531a.92.92 0 0 0-.94 0L20.099 29.834h-.002l-.001.002-.014.006c-.092.055-.151.143-.218.221-.033.041-.082.068-.108.113a.907.907 0 0 0-.124.457v1.361L.466 43.373H.465l-.001.002c-.098.055-.162.143-.23.227-.034.041-.084.068-.109.113a.898.898 0 0 0-.125.457v25.195c0 .324.171.625.45.791l39.264 23.311c.002.002.006 0 .008.002l.007.008a.924.924 0 0 0 .911 0c.003-.002.004-.006.008-.008.001-.002.005 0 .007-.002l19.161-11.377 19.162 11.377c.002.002.006 0 .008.002.004.002.004.006.007.008a.924.924 0 0 0 .911 0c.003-.002.004-.006.008-.008.002-.002.005 0 .007-.002L99.55 81.812a.916.916 0 0 0 .45-.791V55.828a.92.92 0 0 0-.124-.459zM41.104 65.867V42.812l37.424-22.219v23.055L41.104 65.867zM59.815 8.391l17.83 10.588L59.82 29.561 40.185 41.219l-17.83-10.586 37.46-22.242zM39.264 42.812V65.866L21.472 55.305V32.248l17.792 10.564z';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { useTranslation } from '@nocobase/i18n/client';
|
|
2
|
+
import type { ComponentPropsWithoutRef, ReactElement } from 'react';
|
|
3
|
+
import { cn } from 'cn';
|
|
4
|
+
import { BrandLoadingMark } from './brand-loading-mark';
|
|
5
|
+
|
|
6
|
+
/** Inline loading uses the same brand animation as page and startup loading. */
|
|
7
|
+
export function BrandSpinner({
|
|
8
|
+
className,
|
|
9
|
+
...props
|
|
10
|
+
}: ComponentPropsWithoutRef<'svg'>): ReactElement {
|
|
11
|
+
const { t } = useTranslation();
|
|
12
|
+
return (
|
|
13
|
+
<BrandLoadingMark
|
|
14
|
+
data-slot='spinner'
|
|
15
|
+
role='status'
|
|
16
|
+
aria-hidden={undefined}
|
|
17
|
+
aria-label={t('status.loading', { defaultValue: 'Loading' })}
|
|
18
|
+
className={cn('size-5 text-current', className)}
|
|
19
|
+
{...props}
|
|
20
|
+
/>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ReactElement } from 'react';
|
|
2
|
+
import { cn } from 'cn';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
BRAND_MARK_PATH,
|
|
6
|
+
BRAND_MARK_TRANSLATE,
|
|
7
|
+
BRAND_MARK_VIEW_BOX,
|
|
8
|
+
} from './brand-mark-geometry';
|
|
9
|
+
|
|
10
|
+
/** Decorative brand geometry; kept outside page content and hidden from assistive technology. */
|
|
11
|
+
export function BrandWatermark({
|
|
12
|
+
className,
|
|
13
|
+
}: {
|
|
14
|
+
readonly className?: string;
|
|
15
|
+
}): ReactElement {
|
|
16
|
+
return (
|
|
17
|
+
<svg
|
|
18
|
+
aria-hidden='true'
|
|
19
|
+
focusable='false'
|
|
20
|
+
viewBox={BRAND_MARK_VIEW_BOX}
|
|
21
|
+
className={cn(
|
|
22
|
+
'pointer-events-none absolute top-0 right-4 w-48 text-primary opacity-[0.045] sm:right-8 sm:w-64',
|
|
23
|
+
className,
|
|
24
|
+
)}
|
|
25
|
+
fill='currentColor'
|
|
26
|
+
>
|
|
27
|
+
<g transform={BRAND_MARK_TRANSLATE}>
|
|
28
|
+
<path d={BRAND_MARK_PATH} />
|
|
29
|
+
</g>
|
|
30
|
+
</svg>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useTranslation } from '@nocobase/i18n/client';
|
|
2
2
|
import type { ComponentPropsWithoutRef, ReactElement } from 'react';
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { BrandLoadingMark } from '#components/brand-loading-mark';
|
|
5
5
|
import { cn } from 'cn';
|
|
6
6
|
|
|
7
7
|
export interface LoadingProps extends ComponentPropsWithoutRef<'div'> {
|
|
@@ -22,14 +22,15 @@ export function Loading(inputProps: LoadingProps): ReactElement {
|
|
|
22
22
|
<div
|
|
23
23
|
aria-label={label}
|
|
24
24
|
className={cn(
|
|
25
|
-
'flex items-center justify-center',
|
|
25
|
+
'flex flex-col items-center justify-center gap-3',
|
|
26
26
|
fullscreen && 'min-h-svh w-full bg-background',
|
|
27
27
|
className,
|
|
28
28
|
)}
|
|
29
29
|
role='status'
|
|
30
30
|
{...props}
|
|
31
31
|
>
|
|
32
|
-
<
|
|
32
|
+
<BrandLoadingMark />
|
|
33
|
+
<span className='text-sm text-muted-foreground'>{label}</span>
|
|
33
34
|
</div>
|
|
34
35
|
);
|
|
35
36
|
}
|
|
@@ -134,6 +134,8 @@ export function RouteOverlay({
|
|
|
134
134
|
<DialogOverlay forceRender />
|
|
135
135
|
<DialogPrimitive.Popup
|
|
136
136
|
ref={popupRef}
|
|
137
|
+
data-slot='route-overlay-content'
|
|
138
|
+
data-presentation={drawer ? 'drawer' : 'dialog'}
|
|
137
139
|
finalFocus={() => {
|
|
138
140
|
const previous = previousFocusRef.current;
|
|
139
141
|
if (parentPopup?.current) {
|
|
@@ -153,17 +155,26 @@ export function RouteOverlay({
|
|
|
153
155
|
className,
|
|
154
156
|
)}
|
|
155
157
|
>
|
|
156
|
-
<header
|
|
158
|
+
<header
|
|
159
|
+
data-slot='route-overlay-header'
|
|
160
|
+
className='flex shrink-0 flex-col gap-2 border-b p-6 pr-14'
|
|
161
|
+
>
|
|
157
162
|
<DialogTitle>{title}</DialogTitle>
|
|
158
163
|
{description != null && (
|
|
159
164
|
<DialogDescription>{description}</DialogDescription>
|
|
160
165
|
)}
|
|
161
166
|
</header>
|
|
162
|
-
<div
|
|
167
|
+
<div
|
|
168
|
+
data-slot='route-overlay-body'
|
|
169
|
+
className='min-h-0 flex-1 overflow-y-auto p-6'
|
|
170
|
+
>
|
|
163
171
|
{children}
|
|
164
172
|
</div>
|
|
165
173
|
{footer != null && (
|
|
166
|
-
<footer
|
|
174
|
+
<footer
|
|
175
|
+
data-slot='route-overlay-footer'
|
|
176
|
+
className='flex shrink-0 flex-wrap justify-end gap-2 border-t bg-muted/30 p-6'
|
|
177
|
+
>
|
|
167
178
|
{footer}
|
|
168
179
|
</footer>
|
|
169
180
|
)}
|
|
@@ -172,7 +183,7 @@ export function RouteOverlay({
|
|
|
172
183
|
<Button
|
|
173
184
|
variant='ghost'
|
|
174
185
|
size='icon-sm'
|
|
175
|
-
className='absolute top-
|
|
186
|
+
className='absolute top-4 right-4'
|
|
176
187
|
/>
|
|
177
188
|
}
|
|
178
189
|
>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ComponentProps, ReactElement } from 'react';
|
|
2
|
+
import { cn } from 'cn';
|
|
3
|
+
import { Badge } from '#components/ui/badge';
|
|
4
|
+
|
|
5
|
+
const tones = {
|
|
6
|
+
neutral: 'border-border bg-muted text-muted-foreground',
|
|
7
|
+
info: 'border-primary/15 bg-primary/8 text-primary',
|
|
8
|
+
warning:
|
|
9
|
+
'border-(--status-warning)/20 bg-(--status-warning-background) text-(--status-warning)',
|
|
10
|
+
success:
|
|
11
|
+
'border-(--status-success)/20 bg-(--status-success-background) text-(--status-success)',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** Quiet semantic status labels; reserve solid primary fills for actions. */
|
|
15
|
+
export function StatusBadge({
|
|
16
|
+
tone = 'neutral',
|
|
17
|
+
className,
|
|
18
|
+
...props
|
|
19
|
+
}: Omit<ComponentProps<typeof Badge>, 'variant'> & {
|
|
20
|
+
readonly tone?: keyof typeof tones;
|
|
21
|
+
}): ReactElement {
|
|
22
|
+
return (
|
|
23
|
+
<Badge
|
|
24
|
+
{...props}
|
|
25
|
+
variant='outline'
|
|
26
|
+
className={cn(tones[tone], className)}
|
|
27
|
+
/>
|
|
28
|
+
);
|
|
29
|
+
}
|