@nocobase/app-template-default 1.0.0-beta.58 → 1.0.0-beta.59
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +26 -23
- package/README.MD +7 -57
- package/client/agent-annotations-host.ts +8 -2
- package/client/app.ts +0 -2
- package/client/components/brand-loading-mark.tsx +46 -0
- package/client/components/brand-mark-geometry.ts +7 -0
- package/client/components/brand-spinner.tsx +22 -0
- package/client/components/brand-watermark.tsx +32 -0
- package/client/components/loading.tsx +4 -3
- package/client/components/route-overlay.tsx +15 -4
- package/client/components/status-badge.tsx +29 -0
- package/client/components/ui/alert-dialog.tsx +186 -0
- package/client/components/ui/card.tsx +102 -0
- package/client/components/ui/dialog.tsx +2 -2
- package/client/components/ui/radio-group.tsx +35 -0
- package/client/components/ui/spinner.tsx +2 -20
- package/client/components/ui/toast.tsx +3 -2
- package/client/extensions/nocobase-file-component-ui/components/file-upload-field.tsx +3 -3
- package/client/extensions/nocobase-inbox/inbox-item.tsx +4 -3
- package/client/layouts/app-layout.tsx +8 -28
- package/client/layouts/components/app-sidebar.tsx +4 -4
- package/client/layouts/components/header-actions.tsx +3 -36
- package/client/layouts/index.ts +0 -1
- package/client/lib/toaster.ts +1 -1
- package/client/locales/en-US.ts +31 -17
- package/client/locales/zh-CN.ts +24 -14
- package/client/pages/auth/login.tsx +1 -0
- package/client/pages/auth/shared.tsx +9 -9
- package/client/pages/home.tsx +80 -7
- package/client/plugins.ts +2 -2
- package/client/routes.ts +4 -10
- package/client/routing/app-router.tsx +5 -55
- package/client/routing/route-navigation.ts +1 -1
- package/client/styles.css +244 -0
- package/client/theme/theme-settings.tsx +22 -20
- package/client/theme/themes/compact.css +34 -16
- package/client/theme/themes/default.css +34 -16
- package/eslint.config.js +0 -13
- package/index.html +48 -16
- package/package.json +22 -26
- package/tests/components/app-layout-routes.test.tsx +1 -1
- package/tests/components/inbox.test.tsx +0 -19
- package/tests/components/layout-i18n.test.tsx +5 -92
- package/tests/components/primitive-labels.test.tsx +61 -1
- package/tests/fixtures/theme-tokens.html +1 -1
- package/tests/fixtures/theme-tokens.tsx +7 -0
- package/tests/logic/app-server.test.ts +0 -15
- package/tests/logic/client-plugin-registry.test.ts +0 -21
- package/tests/logic/client-routes.test.ts +2 -6
- package/tests/logic/client-shell.test.tsx +11 -107
- package/tests/logic/client-theme.test.tsx +17 -1
- package/tests/logic/config.test.ts +0 -1
- package/tests/logic/plugin-commands.test.ts +1 -1
- package/tsconfig.json +1 -4
- package/tsconfig.server.json +1 -6
- package/vitest.config.ts +1 -8
- package/client/components/inbox-badge.ts +0 -49
- package/client/components/inbox-button.tsx +0 -132
- package/client/components/inbox-header-button.tsx +0 -25
- package/client/layouts/components/surface-empty.tsx +0 -35
- package/client/layouts/settings-layout.tsx +0 -172
- package/server/config/workflow.ts +0 -24
- package/tests/logic/client-settings.test.tsx +0 -841
- package/tests/logic/dev-routes-production-build.test.ts +0 -168
- package/tests/logic/plugin-page-authz.test.ts +0 -53
package/AGENTS.md
CHANGED
|
@@ -6,8 +6,6 @@ Do not create a plugin to add a feature. Plugins are separately published packag
|
|
|
6
6
|
|
|
7
7
|
When users refer to annotations, marks, or selections in the current application, first run `pnpm exec agent-annotations list --json` from the application root to find Agent Annotations records, ask for clarification only if none match or the target remains unclear, and do not implement annotation requests when the user only asks where they are.
|
|
8
8
|
|
|
9
|
-
The Workflow plugin is retained as a dependency but is disabled by default in Client, Server, and CLI. Its configuration and example sources are retained for explicit opt-in use.
|
|
10
|
-
|
|
11
9
|
## Default template scope
|
|
12
10
|
|
|
13
11
|
Default is the clean application starting point. It registers product capabilities but no `app-plugin-*-example` plugins, example pages, application sample services, or sample APIs. Keep runnable demonstrations in `app-template-examples`. Application-owned server routes start empty; the only built-in application provider exposes Authorization Permission Sets as direct roles in the Users page. The only application page is a localized homepage.
|
|
@@ -36,7 +34,7 @@ Read the one page your task needs, not the whole directory.
|
|
|
36
34
|
|
|
37
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.
|
|
38
36
|
|
|
39
|
-
`.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.
|
|
40
38
|
|
|
41
39
|
## Where things go
|
|
42
40
|
|
|
@@ -71,13 +69,13 @@ A feature with a page and an API touches five places: a migration for the table,
|
|
|
71
69
|
|
|
72
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.
|
|
73
71
|
|
|
74
|
-
`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.
|
|
75
73
|
|
|
76
|
-
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.
|
|
77
75
|
|
|
78
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.
|
|
79
77
|
|
|
80
|
-
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.
|
|
81
79
|
|
|
82
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:
|
|
83
81
|
|
|
@@ -106,13 +104,13 @@ Route paths are application-internal. Never write the deployment base path such
|
|
|
106
104
|
|
|
107
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.
|
|
108
106
|
|
|
109
|
-
|
|
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.
|
|
110
108
|
|
|
111
|
-
**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.
|
|
112
110
|
|
|
113
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.
|
|
114
112
|
|
|
115
|
-
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.
|
|
116
114
|
|
|
117
115
|
### Components and styling
|
|
118
116
|
|
|
@@ -127,7 +125,7 @@ pnpm exec prettier --write client/components/ui/card.tsx
|
|
|
127
125
|
|
|
128
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.
|
|
129
127
|
|
|
130
|
-
`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.
|
|
131
129
|
|
|
132
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.
|
|
133
131
|
|
|
@@ -135,6 +133,8 @@ Style with the semantic Tailwind tokens — `bg-background`, `text-foreground`,
|
|
|
135
133
|
|
|
136
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.
|
|
137
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
|
+
|
|
138
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.
|
|
139
139
|
|
|
140
140
|
### Server routes
|
|
@@ -187,7 +187,7 @@ Bind services to their existing tokens in a provider's `register()`; calling `cr
|
|
|
187
187
|
|
|
188
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`.
|
|
189
189
|
|
|
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 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`.
|
|
191
191
|
|
|
192
192
|
```bash
|
|
193
193
|
curl -H "x-api-key: <key>" http://127.0.0.1:13000/main/api/swagger
|
|
@@ -296,15 +296,14 @@ Route overrides must stay lazy, declare a `componentEntry`, and load a default-e
|
|
|
296
296
|
|
|
297
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:
|
|
298
298
|
|
|
299
|
-
| The requirement sounds like
|
|
300
|
-
|
|
|
301
|
-
|
|
|
302
|
-
|
|
|
303
|
-
|
|
|
304
|
-
|
|
|
305
|
-
|
|
|
306
|
-
|
|
|
307
|
-
| 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` |
|
|
308
307
|
|
|
309
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.
|
|
310
309
|
|
|
@@ -316,7 +315,7 @@ Building a permission system, a notification sender, or a job scheduler by hand
|
|
|
316
315
|
|
|
317
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.
|
|
318
317
|
|
|
319
|
-
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.
|
|
320
319
|
|
|
321
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.
|
|
322
321
|
|
|
@@ -358,7 +357,7 @@ Declare such a package in `dependencies` when you write the code; nothing will r
|
|
|
358
357
|
|
|
359
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.
|
|
360
359
|
|
|
361
|
-
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.
|
|
362
361
|
|
|
363
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.
|
|
364
363
|
|
|
@@ -421,7 +420,7 @@ The account menu checks Better Auth sign-out results before refreshing the sessi
|
|
|
421
420
|
|
|
422
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.
|
|
423
422
|
|
|
424
|
-
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.
|
|
425
424
|
|
|
426
425
|
## Development logging
|
|
427
426
|
|
|
@@ -432,3 +431,7 @@ Navigation groups retain their expanded or collapsed state while the navigation
|
|
|
432
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.
|
|
433
432
|
|
|
434
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
|
|
@@ -422,10 +416,6 @@ curl -H "x-api-key: <key>" http://127.0.0.1:13000/main/api/swagger
|
|
|
422
416
|
|
|
423
417
|
Replace `/main` with the application's `APP_BASE_PATH`. An application without the authentication plugin registers no way to tell who is asking, so it does not serve the documentation at all and both URLs answer `404`. There is no setting that makes it public.
|
|
424
418
|
|
|
425
|
-
## Optional Workflow plugin
|
|
426
|
-
|
|
427
|
-
The Workflow plugin is disabled by default in Client, Server, and CLI. Its package dependency and `server/config/workflow.ts` are retained. To opt in, register the plugin with `pnpm nocobase plugin register @nocobase/app-plugin-workflow` and restore the `workflow` import, type, and entry in `server/config/index.ts`.
|
|
428
|
-
|
|
429
419
|
## Working with an AI agent
|
|
430
420
|
|
|
431
421
|
`AGENTS.md` describes how to build features here, and `.agents/skills/` contains package-owned guidance generated by `pnpm nocobase skills sync`. Give an agent an API key when it needs to call the application: it learns the available endpoints from the API document above rather than from route sources. If you use Claude Code, `CLAUDE.md` points at the same guidance. Keep application-specific rules in committed `AGENTS.md` files because synchronization replaces package-owned Skill directories.
|
|
@@ -434,46 +424,6 @@ The Workflow plugin is disabled by default in Client, Server, and CLI. Its packa
|
|
|
434
424
|
|
|
435
425
|
MIT
|
|
436
426
|
|
|
437
|
-
## Publish to an application Hub
|
|
438
|
-
|
|
439
|
-
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.
|
|
440
|
-
|
|
441
|
-
### Add the Hub as a remote
|
|
442
|
-
|
|
443
|
-
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:
|
|
444
|
-
|
|
445
|
-
```bash
|
|
446
|
-
pnpm nocobase hub remote add origin http://127.0.0.1:13000/main/apps/crm
|
|
447
|
-
pnpm nocobase hub remote list
|
|
448
|
-
```
|
|
449
|
-
|
|
450
|
-
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.
|
|
451
|
-
|
|
452
|
-
### Create and save a publishing key
|
|
453
|
-
|
|
454
|
-
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`.
|
|
455
|
-
|
|
456
|
-
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/nocobase/blob/v3-develop/docs/docs/en/deployment/hub.md) for the console walkthrough.
|
|
457
|
-
|
|
458
|
-
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:
|
|
459
|
-
|
|
460
|
-
```bash
|
|
461
|
-
echo "$HUB_KEY" | pnpm nocobase hub auth login --remote origin --with-token
|
|
462
|
-
pnpm nocobase hub deploy --remote origin --json
|
|
463
|
-
```
|
|
464
|
-
|
|
465
|
-
### Deploy
|
|
466
|
-
|
|
467
|
-
```bash
|
|
468
|
-
pnpm nocobase hub deploy --json
|
|
469
|
-
pnpm nocobase hub upload --json
|
|
470
|
-
pnpm nocobase hub deploy --release-id <releaseId> --json
|
|
471
|
-
```
|
|
472
|
-
|
|
473
|
-
`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.
|
|
474
|
-
|
|
475
|
-
`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.
|
|
476
|
-
|
|
477
427
|
## Runtime paths and application creation
|
|
478
428
|
|
|
479
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.
|
|
@@ -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
|
+
}
|