create-ortha-app 0.4.0
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/LICENSE +21 -0
- package/README.md +7 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +277 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +32 -0
- package/dist/lib/conditionals.d.ts +37 -0
- package/dist/lib/conditionals.d.ts.map +1 -0
- package/dist/lib/conditionals.js +115 -0
- package/dist/lib/features.d.ts +185 -0
- package/dist/lib/features.d.ts.map +1 -0
- package/dist/lib/features.js +328 -0
- package/dist/lib/template.d.ts +46 -0
- package/dist/lib/template.d.ts.map +1 -0
- package/dist/lib/template.js +115 -0
- package/dist/lib/ui.d.ts +76 -0
- package/dist/lib/ui.d.ts.map +1 -0
- package/dist/lib/ui.js +310 -0
- package/dist/lib/validate.d.ts +13 -0
- package/dist/lib/validate.d.ts.map +1 -0
- package/dist/lib/validate.js +51 -0
- package/package.json +37 -0
- package/templates/default/README.md.tmpl +199 -0
- package/templates/default/_gitignore +8 -0
- package/templates/default/apps/admin/index.html +38 -0
- package/templates/default/apps/admin/src/main.tsx +5 -0
- package/templates/default/apps/admin/src/plugins.spec.ts +58 -0
- package/templates/default/apps/admin/src/plugins.ts +57 -0
- package/templates/default/apps/admin/src/styles.css +254 -0
- package/templates/default/apps/admin/tsconfig.json +29 -0
- package/templates/default/apps/admin/vite.config.mts +67 -0
- package/templates/default/apps/admin-e2e/playwright.config.ts +51 -0
- package/templates/default/apps/admin-e2e/src/auth.spec.ts +55 -0
- package/templates/default/apps/admin-e2e/src/support/seed.ts +62 -0
- package/templates/default/apps/admin-e2e/tsconfig.json +23 -0
- package/templates/default/apps/server/jest.config.js +38 -0
- package/templates/default/apps/server/jest.setup.js +20 -0
- package/templates/default/apps/server/ortha.config.ts +505 -0
- package/templates/default/apps/server/src/main.ts +26 -0
- package/templates/default/apps/server/src/plugins.spec.ts +71 -0
- package/templates/default/apps/server/src/plugins.ts +229 -0
- package/templates/default/apps/server/tsconfig.json +31 -0
- package/templates/default/apps/server-e2e/jest.config.js +47 -0
- package/templates/default/apps/server-e2e/src/api.spec.ts +107 -0
- package/templates/default/apps/server-e2e/src/global-setup.ts +41 -0
- package/templates/default/apps/server-e2e/src/jest.setup.ts +28 -0
- package/templates/default/apps/server-e2e/src/support/db.ts +143 -0
- package/templates/default/apps/server-e2e/src/support/test-app.ts +64 -0
- package/templates/default/apps/server-e2e/tsconfig.json +26 -0
- package/templates/default/docker-compose.yml +21 -0
- package/templates/default/env.tmpl +140 -0
- package/templates/default/package.json.tmpl +51 -0
- package/templates/default/tsconfig.json +18 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# __APP_TITLE__
|
|
2
|
+
|
|
3
|
+
An [Ortha CMS](https://github.com/ortha-source/ortha-cms) app.
|
|
4
|
+
|
|
5
|
+
## Getting started
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
docker compose up -d # Postgres
|
|
9
|
+
npm run migrate # create the schema
|
|
10
|
+
npm run dev # API on :3000, admin on :4200
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Sign in at <http://localhost:4200> with the admin credentials in `.env`
|
|
14
|
+
(`ORTHA_ROOT_ADMIN_EMAIL` / `ORTHA_ROOT_ADMIN_PASSWORD`). That account is
|
|
15
|
+
provisioned on boot, idempotently — clear those two variables once you have it.
|
|
16
|
+
|
|
17
|
+
## Commands
|
|
18
|
+
|
|
19
|
+
| Command | What it does |
|
|
20
|
+
| --- | --- |
|
|
21
|
+
| `npm run dev` | `tsc --watch`, `node --watch`, and the Vite dev server together |
|
|
22
|
+
| `npm run build` | Compiles the server and builds the admin bundle |
|
|
23
|
+
| `npm start` | Runs the built server — API **and** admin, one process, one origin |
|
|
24
|
+
| `npm run migrate` | Applies every plugin's pending migrations |
|
|
25
|
+
| `npm run generate -- --name=<name>` | Generates a migration for your own content tables |
|
|
26
|
+
| `npm run studio` | Opens Drizzle Studio on this app's database |
|
|
27
|
+
| `npm test` | Unit tests — both halves |
|
|
28
|
+
| `npm run e2e` | Both end-to-end suites |
|
|
29
|
+
| `npm run e2e:server` | API tests against a real database |
|
|
30
|
+
| `npm run e2e:admin` | Browser tests against a mocked API |
|
|
31
|
+
| `npm run typecheck` | Both TypeScript projects |
|
|
32
|
+
|
|
33
|
+
## Tests
|
|
34
|
+
|
|
35
|
+
**Unit tests** run under two runners, one per half, each matching its own
|
|
36
|
+
toolchain:
|
|
37
|
+
|
|
38
|
+
- **`apps/server/**/*.spec.ts`** → Jest with `@swc/jest` (`apps/server/jest.config.js`).
|
|
39
|
+
Jest rather than Vitest because NestJS DI reads `emitDecoratorMetadata`, and
|
|
40
|
+
Vitest's esbuild transform does not emit it — providers resolve as
|
|
41
|
+
`undefined`, with no error naming the cause.
|
|
42
|
+
- **`apps/admin/src/**/*.spec.tsx`** → Vitest with jsdom, configured inside
|
|
43
|
+
`apps/admin/vite.config.mts` so tests resolve modules exactly the way the app does.
|
|
44
|
+
|
|
45
|
+
Neither touches a database: `jest.setup.js` supplies placeholder secrets so
|
|
46
|
+
`npm test` runs anywhere, including CI with no `.env`.
|
|
47
|
+
|
|
48
|
+
The two shipped specs assert the things that break quietly — the plugin lists
|
|
49
|
+
and their order. Server-side that is migration order (a plugin whose tables
|
|
50
|
+
reference another's must come after it, which fails only on a *fresh*
|
|
51
|
+
database); admin-side it is that exactly one plugin contributes a `layout`, and
|
|
52
|
+
that it is the shell's, because the host mounts the first one it finds and the
|
|
53
|
+
shell's is what composes the auth gate.
|
|
54
|
+
|
|
55
|
+
**End-to-end tests** come in two suites, split the way the app is:
|
|
56
|
+
|
|
57
|
+
### `apps/server-e2e` — the API, for real
|
|
58
|
+
|
|
59
|
+
Jest + supertest, booting **this app** through `createServer` with the real
|
|
60
|
+
plugin list against a real Postgres. `apps/server-e2e/src/support/test-app.ts` gives you
|
|
61
|
+
`createTestApp()` / `closeTestApp()`.
|
|
62
|
+
|
|
63
|
+
It manages its own database. `<your database>_e2e` is created and migrated on
|
|
64
|
+
first run, using the same `applyPluginMigrations` that `ortha migrate` calls —
|
|
65
|
+
so the schema under test is the real one. It never touches the database
|
|
66
|
+
`DATABASE_URL` names, because it **truncates every table** between suites;
|
|
67
|
+
`E2E_DATABASE_URL` overrides the derivation, and pointing it at your
|
|
68
|
+
development database is refused rather than obeyed.
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
docker compose up -d
|
|
72
|
+
npm run e2e:server
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### `apps/admin-e2e` — the UI, in a browser
|
|
76
|
+
|
|
77
|
+
Playwright against the Vite dev server with `/api` mocked at the network layer
|
|
78
|
+
(`apps/admin-e2e/src/support/seed.ts`). No backend, no database, no migration — a run is
|
|
79
|
+
fast and hermetic, and when it fails it is the UI that is wrong. API behaviour
|
|
80
|
+
belongs in `apps/server-e2e`, where a failure names the endpoint instead of blaming
|
|
81
|
+
a page.
|
|
82
|
+
|
|
83
|
+
```sh
|
|
84
|
+
npx playwright install chromium # once
|
|
85
|
+
npm run e2e:admin
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Anything the seed does not mock returns `404` on purpose: a silent empty `200`
|
|
89
|
+
makes a page render its empty state and pass, which is the false green this
|
|
90
|
+
exists to avoid.
|
|
91
|
+
|
|
92
|
+
## What you own
|
|
93
|
+
|
|
94
|
+
Almost nothing, on purpose — the CMS is the `@orthacms/*` packages, and this app
|
|
95
|
+
is the composition of them.
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
apps/
|
|
99
|
+
├── server/ the API — ortha.config.ts, src/{main,plugins}.ts
|
|
100
|
+
├── admin/ the UI — index.html, vite.config.mts, src/
|
|
101
|
+
├── server-e2e/ API tests against a real database
|
|
102
|
+
└── admin-e2e/ browser tests against a mocked API
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
The same four apps Ortha CMS itself is built from, so anything you read in its
|
|
106
|
+
source lands in the same place here.
|
|
107
|
+
|
|
108
|
+
| File | Why you would open it |
|
|
109
|
+
| --- | --- |
|
|
110
|
+
| `apps/server/src/plugins.ts` | Which plugins the API runs, and **in what order** |
|
|
111
|
+
| `apps/admin/src/plugins.ts` | Which plugins the admin UI runs |
|
|
112
|
+
| `apps/server/ortha.config.ts` | Every setting, typed — the only reader of `process.env` |
|
|
113
|
+
| `apps/admin/src/styles.css` | The theme tokens: this app's whole visual identity |
|
|
114
|
+
|
|
115
|
+
Upgrading is a version bump. Keep the `@orthacms/*` versions **in step with each
|
|
116
|
+
other** — they are released together, and a partial upgrade can leave two copies
|
|
117
|
+
of a shared package in `node_modules`, which means two React context instances
|
|
118
|
+
and UI that silently stops talking to itself.
|
|
119
|
+
|
|
120
|
+
### Writing your own plugin
|
|
121
|
+
|
|
122
|
+
A server plugin is a `ServerPlugin` from `@orthacms/bootstrap-server` — a name,
|
|
123
|
+
a NestJS module, and optionally its own migrations — added to the array in
|
|
124
|
+
`src/server/plugins.ts`. `@InjectDatabase()` from `@orthacms/database` gives it
|
|
125
|
+
the shared connection. An admin plugin is an `AdminPlugin` from
|
|
126
|
+
`@orthacms/bootstrap-admin`, added to `src/admin/plugins.ts`.
|
|
127
|
+
|
|
128
|
+
`@orthacms/utils-server` and `@orthacms/utils-admin` hold the shared toolkit
|
|
129
|
+
(filter parsing, `apiClient`, slots, table URL state), and
|
|
130
|
+
`@orthacms/design-system` the UI primitives. All three are already declared here.
|
|
131
|
+
|
|
132
|
+
The extension points are declared too, so you can import them directly:
|
|
133
|
+
`@orthacms/content-domain` (the content kernel's types), `@orthacms/tools-server`
|
|
134
|
+
(register an agent tool), `@orthacms/copilot-domain`,
|
|
135
|
+
`@orthacms/query-builder-admin`.
|
|
136
|
+
|
|
137
|
+
If you add an `@orthacms/*` package this app does not already list — a copilot
|
|
138
|
+
backend, the GraphQL adapter — install it at **the same version as the rest**.
|
|
139
|
+
Keeping them in step is what stops npm nesting a second copy of a shared
|
|
140
|
+
package, which means two React context instances and UI that silently stops
|
|
141
|
+
talking to itself.
|
|
142
|
+
|
|
143
|
+
## What this app was scaffolded with
|
|
144
|
+
|
|
145
|
+
### AI copilot
|
|
146
|
+
|
|
147
|
+
Installed, and **off** until you turn it on: set `COPILOT_ENABLED=true` in
|
|
148
|
+
`.env`. Enabling a hosted backend sends workspace content to a third party, so
|
|
149
|
+
it is an explicit decision rather than a default.
|
|
150
|
+
|
|
151
|
+
Backends are registered in `src/server/plugins.ts`, and **the order is the
|
|
152
|
+
setting** — there is no `defaultProvider`. The first registered backend serves a
|
|
153
|
+
run that names none, and it is what the admin's model picker opens on. A backend
|
|
154
|
+
is only registered when its connection settings are present, so an unconfigured
|
|
155
|
+
one never appears as an option that fails on the first message.
|
|
156
|
+
|
|
157
|
+
The bundled `fake` provider needs no key and no network, and is registered last
|
|
158
|
+
— so with nothing configured the chat still works, and it is the default only
|
|
159
|
+
because it is the only one.
|
|
160
|
+
<!-- ortha:if graphql -->
|
|
161
|
+
### GraphQL
|
|
162
|
+
|
|
163
|
+
`POST /api/v1/graphql`, alongside REST. It is a protocol adapter over the same
|
|
164
|
+
services, not a second API: same bearer tokens, same scopes, same visibility
|
|
165
|
+
rules. The schema is built per workspace content-grant set, so introspection
|
|
166
|
+
cannot enumerate types a workspace was not granted.
|
|
167
|
+
|
|
168
|
+
GraphiQL rides the same switch as the API reference (`API_DOCS`).
|
|
169
|
+
<!-- ortha:end -->
|
|
170
|
+
<!-- ortha:if mcp -->
|
|
171
|
+
### MCP
|
|
172
|
+
|
|
173
|
+
`POST /api/v1/mcp`, off until `MCP_ENABLED=true`. It lets an external agent do
|
|
174
|
+
content CRUD with an API token — mint one on the API Tokens page, `read` scope
|
|
175
|
+
for a retrieval-only agent, `full` for one that authors.
|
|
176
|
+
<!-- ortha:end -->
|
|
177
|
+
|
|
178
|
+
## Adding content types
|
|
179
|
+
|
|
180
|
+
This app starts with none. To add some:
|
|
181
|
+
|
|
182
|
+
1. Write them under `apps/server/src/content/` and export a `contentTypes` array.
|
|
183
|
+
2. Pass them to `ContentPlugin({ types: contentTypes, migrations: … })` in
|
|
184
|
+
`apps/server/src/plugins.ts` — the comment there has the exact shape.
|
|
185
|
+
3. Add `apps/server/drizzle.config.ts` pointing `schema` at your
|
|
186
|
+
`src/content/index.ts` and `out` at `../../migrations`.
|
|
187
|
+
4. `npm run generate -- --name=add_content_types && npm run migrate`
|
|
188
|
+
|
|
189
|
+
## Deploying
|
|
190
|
+
|
|
191
|
+
`npm run build && npm start` is the whole thing: one process serving the API and
|
|
192
|
+
the admin bundle on one origin. Set in the environment:
|
|
193
|
+
|
|
194
|
+
- `DATABASE_URL` — required
|
|
195
|
+
- `NODE_ENV=production` — spelled exactly; anything else drops `Secure` from the
|
|
196
|
+
session cookie and publishes the API reference
|
|
197
|
+
- `TRUST_PROXY` — required behind a load balancer, or every client shares one
|
|
198
|
+
rate-limit bucket
|
|
199
|
+
- `MEDIA_LOCAL_ROOT` — a persistent volume, or uploads vanish on each deploy
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<title>__APP_TITLE__</title>
|
|
6
|
+
<base href="/" />
|
|
7
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
8
|
+
<script>
|
|
9
|
+
/* Apply the persisted theme before first paint, so a dark-mode user
|
|
10
|
+
never sees a flash of the light canvas. Mirrors the design-system
|
|
11
|
+
AppearanceProvider, which takes over on mount. */
|
|
12
|
+
(function () {
|
|
13
|
+
try {
|
|
14
|
+
var stored = localStorage.getItem('ortha.theme');
|
|
15
|
+
var theme =
|
|
16
|
+
stored === 'light' ||
|
|
17
|
+
stored === 'dark' ||
|
|
18
|
+
stored === 'system'
|
|
19
|
+
? stored
|
|
20
|
+
: 'system';
|
|
21
|
+
var dark =
|
|
22
|
+
theme === 'dark' ||
|
|
23
|
+
(theme === 'system' &&
|
|
24
|
+
window.matchMedia('(prefers-color-scheme: dark)')
|
|
25
|
+
.matches);
|
|
26
|
+
document.documentElement.classList.toggle('dark', dark);
|
|
27
|
+
document.documentElement.style.colorScheme = dark
|
|
28
|
+
? 'dark'
|
|
29
|
+
: 'light';
|
|
30
|
+
} catch (e) {}
|
|
31
|
+
})();
|
|
32
|
+
</script>
|
|
33
|
+
</head>
|
|
34
|
+
<body>
|
|
35
|
+
<div id="root"></div>
|
|
36
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
37
|
+
</body>
|
|
38
|
+
</html>
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { buildPlugins } from './plugins';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The admin composition, asserted.
|
|
6
|
+
*
|
|
7
|
+
* A browser test proves the happy path assembles, and can say nothing about the
|
|
8
|
+
* two ways this list goes wrong while still rendering something plausible:
|
|
9
|
+
*
|
|
10
|
+
* 1. **A layout registered ahead of the shell's.** The host mounts the *first*
|
|
11
|
+
* `layout` it finds, and the shell's is what composes identity's auth gate —
|
|
12
|
+
* so a second contributor placed earlier renders every private route
|
|
13
|
+
* **ungated** while signed out. It looks like a styling accident.
|
|
14
|
+
* 2. **A plugin quietly dropped.** Its routes and slot contributions stop
|
|
15
|
+
* existing with no error; the sidebar is simply shorter, and every test that
|
|
16
|
+
* does not open that page stays green.
|
|
17
|
+
*
|
|
18
|
+
* Both are decisions visible in `plugins.ts`, so they are pinned here rather
|
|
19
|
+
* than through a browser. Relative order between slot fillers is *not* asserted
|
|
20
|
+
* — slots are module-level singletons registered before the first render, so
|
|
21
|
+
* that order only decides the order of items within a slot.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** Every plugin the admin registers, in order. */
|
|
25
|
+
const EXPECTED_PLUGINS = [
|
|
26
|
+
'identity',
|
|
27
|
+
'shell',
|
|
28
|
+
'workspaces',
|
|
29
|
+
'insights',
|
|
30
|
+
'content',
|
|
31
|
+
'i18n',
|
|
32
|
+
'wysiwyg',
|
|
33
|
+
'media',
|
|
34
|
+
'copilot',
|
|
35
|
+
'users',
|
|
36
|
+
'activity',
|
|
37
|
+
'api-tokens'
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
describe('buildPlugins()', () => {
|
|
41
|
+
it('registers exactly the plugins this admin ships', () => {
|
|
42
|
+
expect(buildPlugins().map((plugin) => plugin.name)).toEqual(
|
|
43
|
+
EXPECTED_PLUGINS
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('contributes exactly one layout, and it is the shell’s', () => {
|
|
48
|
+
const layouts = buildPlugins().filter((plugin) => plugin.layout);
|
|
49
|
+
|
|
50
|
+
expect(layouts.map((plugin) => plugin.name)).toEqual(['shell']);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('puts identity first, so its public routes render outside the gate', () => {
|
|
54
|
+
// Sign-in and accept-invite must not be wrapped in the layout that
|
|
55
|
+
// requires a session — otherwise there is no way to get one.
|
|
56
|
+
expect(buildPlugins()[0]?.name).toBe('identity');
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { AdminPlugin } from '@orthacms/bootstrap-admin';
|
|
2
|
+
import { ActivityPlugin } from '@orthacms/activity-admin';
|
|
3
|
+
import { ApiTokensPlugin } from '@orthacms/api-tokens-admin';
|
|
4
|
+
import { ContentPlugin } from '@orthacms/content-admin';
|
|
5
|
+
import { I18nPlugin } from '@orthacms/i18n-admin';
|
|
6
|
+
import { IdentityPlugin } from '@orthacms/identity-admin';
|
|
7
|
+
import { InsightsPlugin } from '@orthacms/insights-admin';
|
|
8
|
+
import { MediaPlugin } from '@orthacms/media-admin';
|
|
9
|
+
import { ShellPlugin } from '@orthacms/shell-admin';
|
|
10
|
+
import { UsersPlugin } from '@orthacms/users-admin';
|
|
11
|
+
import { WorkspacesPlugin } from '@orthacms/workspaces-admin';
|
|
12
|
+
import { WysiwygPlugin } from '@orthacms/wysiwyg-admin';
|
|
13
|
+
import { CopilotPlugin } from '@orthacms/copilot-admin';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The admin's composition, mirroring `src/server/plugins.ts` on the UI side.
|
|
17
|
+
*
|
|
18
|
+
* **Two positions matter; the rest is legibility.**
|
|
19
|
+
*
|
|
20
|
+
* `IdentityPlugin` is first because it contributes the only *public* routes —
|
|
21
|
+
* sign-in and accept-invite — which must render outside the gated layout.
|
|
22
|
+
*
|
|
23
|
+
* `ShellPlugin` is the one plugin contributing a `layout`, and the host mounts
|
|
24
|
+
* the **first** layout it finds. The shell's layout is what composes
|
|
25
|
+
* identity's auth gate, so a plugin registering a layout ahead of it would
|
|
26
|
+
* render every private route *ungated* — losing the sidebar and the `<main>`
|
|
27
|
+
* landmark with it, which makes an authorization bug look like a styling
|
|
28
|
+
* accident. Keep it second.
|
|
29
|
+
*
|
|
30
|
+
* Everything else is order-independent: slots are module-level singletons and
|
|
31
|
+
* every plugin's contributions are registered before the first render, so a
|
|
32
|
+
* filler registered ahead of the plugin defining its slot still lands. Only
|
|
33
|
+
* two things follow from position — the order of items within a slot, and
|
|
34
|
+
* which plugin wins an id collision in a last-writer-wins merge.
|
|
35
|
+
*/
|
|
36
|
+
export function buildPlugins(): AdminPlugin[] {
|
|
37
|
+
return [
|
|
38
|
+
IdentityPlugin(),
|
|
39
|
+
ShellPlugin(),
|
|
40
|
+
WorkspacesPlugin(),
|
|
41
|
+
// Registers the dashboard's default sections, which merge by id with
|
|
42
|
+
// the last writer winning — so anything renaming a band comes after.
|
|
43
|
+
InsightsPlugin(),
|
|
44
|
+
ContentPlugin(),
|
|
45
|
+
// Content Library slot fillers, hence after ContentPlugin().
|
|
46
|
+
I18nPlugin(),
|
|
47
|
+
WysiwygPlugin(),
|
|
48
|
+
MediaPlugin(),
|
|
49
|
+
// The docked chat panel plus the full-page Agents view. Belongs with
|
|
50
|
+
// the workspace-interior features: the panel mounts into the workspace
|
|
51
|
+
// shell's sidebar footer.
|
|
52
|
+
CopilotPlugin(),
|
|
53
|
+
UsersPlugin(),
|
|
54
|
+
ActivityPlugin(),
|
|
55
|
+
ApiTokensPlugin()
|
|
56
|
+
];
|
|
57
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
@import 'tailwindcss';
|
|
2
|
+
@import '@orthacms/design-system/styles.css';
|
|
3
|
+
/* The rich-text scope (`.ortha-wysiwyg`) shared by the WYSIWYG editor and its
|
|
4
|
+
previews. Reads the `--color-*` tokens below at runtime, so it follows
|
|
5
|
+
light/dark with the rest of the admin. */
|
|
6
|
+
@import '@orthacms/wysiwyg-admin/styles.css';
|
|
7
|
+
|
|
8
|
+
/* Tailwind excludes `node_modules` from its automatic content detection, so
|
|
9
|
+
without this line none of the classes inside the Ortha packages are seen and
|
|
10
|
+
the whole admin renders unstyled. The published packages ship compiled JS
|
|
11
|
+
with the class strings intact, which is what this scans. */
|
|
12
|
+
@source "../../../node_modules/@orthacms/*/dist/**/*.js";
|
|
13
|
+
|
|
14
|
+
@custom-variant dark (&:where(.dark, .dark *));
|
|
15
|
+
|
|
16
|
+
/* ---------------------------------------------------------------------------
|
|
17
|
+
Theme. This file is yours — these tokens are the app's whole visual
|
|
18
|
+
identity, and every component in the design system resolves its colours
|
|
19
|
+
through them at runtime.
|
|
20
|
+
|
|
21
|
+
The contract to keep: every `*-foreground` must clear WCAG AA (4.5:1) on its
|
|
22
|
+
own surface. `@theme` rather than `@theme inline` is deliberate — it makes
|
|
23
|
+
utilities resolve to `var(--color-*)`, which is what lets the `.dark` block
|
|
24
|
+
below re-declare them and have the change take effect without rebuilding.
|
|
25
|
+
--------------------------------------------------------------------------- */
|
|
26
|
+
@theme {
|
|
27
|
+
--radius: 0.625rem;
|
|
28
|
+
|
|
29
|
+
--color-background: oklch(1 0 0);
|
|
30
|
+
--color-foreground: oklch(0.21 0.012 285);
|
|
31
|
+
--color-card: oklch(1 0 0);
|
|
32
|
+
--color-card-foreground: oklch(0.21 0.012 285);
|
|
33
|
+
--color-popover: oklch(1 0 0);
|
|
34
|
+
--color-popover-foreground: oklch(0.21 0.012 285);
|
|
35
|
+
|
|
36
|
+
--color-primary: oklch(0.25 0.015 285);
|
|
37
|
+
--color-primary-foreground: oklch(0.985 0 0);
|
|
38
|
+
|
|
39
|
+
/* The brand accent. `brand` is for icons and large accents (it clears 3:1,
|
|
40
|
+
not 4.5:1 — never put white text on it); `brand-text` is the link-grade
|
|
41
|
+
version that does clear AA as text. */
|
|
42
|
+
--color-brand: oklch(0.66 0.2 35);
|
|
43
|
+
--color-brand-foreground: oklch(0.21 0.012 285);
|
|
44
|
+
--color-brand-text: oklch(0.55 0.18 35);
|
|
45
|
+
--color-brand-soft: oklch(0.955 0.025 40);
|
|
46
|
+
--color-brand-soft-foreground: oklch(0.47 0.16 35);
|
|
47
|
+
|
|
48
|
+
--color-secondary: oklch(0.945 0.003 285);
|
|
49
|
+
--color-secondary-foreground: oklch(0.25 0.012 285);
|
|
50
|
+
--color-muted: oklch(0.975 0.003 285);
|
|
51
|
+
--color-muted-foreground: oklch(0.49 0.015 285);
|
|
52
|
+
--color-accent: oklch(0.975 0.003 285);
|
|
53
|
+
--color-accent-foreground: oklch(0.21 0.012 285);
|
|
54
|
+
|
|
55
|
+
--color-destructive: oklch(0.55 0.22 27);
|
|
56
|
+
--color-destructive-foreground: oklch(0.985 0 0);
|
|
57
|
+
--color-destructive-soft: oklch(0.95 0.025 20);
|
|
58
|
+
--color-destructive-soft-foreground: oklch(0.44 0.17 27);
|
|
59
|
+
|
|
60
|
+
--color-success: oklch(0.52 0.14 150);
|
|
61
|
+
--color-success-foreground: oklch(0.985 0 0);
|
|
62
|
+
--color-success-soft: oklch(0.95 0.045 150);
|
|
63
|
+
--color-success-soft-foreground: oklch(0.42 0.11 150);
|
|
64
|
+
|
|
65
|
+
--color-warning: oklch(0.55 0.12 66);
|
|
66
|
+
--color-warning-foreground: oklch(0.985 0 0);
|
|
67
|
+
--color-warning-soft: oklch(0.95 0.045 80);
|
|
68
|
+
--color-warning-soft-foreground: oklch(0.44 0.1 60);
|
|
69
|
+
|
|
70
|
+
--color-info: oklch(0.52 0.17 255);
|
|
71
|
+
--color-info-foreground: oklch(0.985 0 0);
|
|
72
|
+
--color-info-soft: oklch(0.945 0.03 255);
|
|
73
|
+
--color-info-soft-foreground: oklch(0.42 0.14 260);
|
|
74
|
+
|
|
75
|
+
--color-teal-soft: oklch(0.94 0.035 190);
|
|
76
|
+
--color-teal-soft-foreground: oklch(0.4 0.08 190);
|
|
77
|
+
--color-violet-soft: oklch(0.945 0.035 300);
|
|
78
|
+
--color-violet-soft-foreground: oklch(0.42 0.14 300);
|
|
79
|
+
|
|
80
|
+
--color-border: oklch(0.9 0.008 285);
|
|
81
|
+
/* Darker than `border` so form controls read as editable. */
|
|
82
|
+
--color-input: oklch(0.84 0.01 285);
|
|
83
|
+
--color-ring: oklch(0.55 0.01 285);
|
|
84
|
+
|
|
85
|
+
--color-status-active: oklch(0.63 0.17 149);
|
|
86
|
+
--color-status-invited: oklch(0.67 0.14 70);
|
|
87
|
+
|
|
88
|
+
/* Sidebar chrome. Authored dark in both themes; everything rendered inside
|
|
89
|
+
the sidebar must use these tokens rather than the light-surface ones. */
|
|
90
|
+
--color-sidebar: oklch(0.21 0.012 285);
|
|
91
|
+
--color-sidebar-foreground: oklch(0.93 0.005 285);
|
|
92
|
+
--color-sidebar-primary: oklch(0.66 0.2 35);
|
|
93
|
+
--color-sidebar-primary-foreground: oklch(0.21 0.012 285);
|
|
94
|
+
--color-sidebar-accent: oklch(0.3 0.018 285);
|
|
95
|
+
--color-sidebar-accent-foreground: oklch(0.985 0.002 285);
|
|
96
|
+
--color-sidebar-border: oklch(0.31 0.012 285);
|
|
97
|
+
--color-sidebar-ring: oklch(0.75 0.01 285);
|
|
98
|
+
|
|
99
|
+
/* Nav-icon accents, lightened so each clears 3:1 on the dark sidebar. */
|
|
100
|
+
--color-nav-orange: oklch(0.72 0.17 40);
|
|
101
|
+
--color-nav-blue: oklch(0.74 0.11 250);
|
|
102
|
+
--color-nav-green: oklch(0.74 0.13 150);
|
|
103
|
+
--color-nav-violet: oklch(0.76 0.11 300);
|
|
104
|
+
--color-nav-amber: oklch(0.78 0.13 80);
|
|
105
|
+
--color-nav-teal: oklch(0.75 0.1 190);
|
|
106
|
+
|
|
107
|
+
/* Data-visualisation palette (dashboard widgets). Hex rather than oklch
|
|
108
|
+
because these values are the output of a contrast validator — treat them
|
|
109
|
+
as fixed data. `chart-1`/`chart-2` are the categorical pair, the only one
|
|
110
|
+
that stays distinguishable under colourblind simulation on both the light
|
|
111
|
+
and the dark card; `chart-q0`..`q5` are the sequential ramp, low to high. */
|
|
112
|
+
--color-chart-1: #2a78d6;
|
|
113
|
+
--color-chart-2: #1baf7a;
|
|
114
|
+
--color-chart-q0: #eef2f7;
|
|
115
|
+
--color-chart-q1: #cde2fb;
|
|
116
|
+
--color-chart-q2: #9ec5f4;
|
|
117
|
+
--color-chart-q3: #5598e7;
|
|
118
|
+
--color-chart-q4: #2a78d6;
|
|
119
|
+
--color-chart-q5: #184f95;
|
|
120
|
+
--color-chart-on-deep: #ffffff;
|
|
121
|
+
--color-chart-grid: oklch(0.93 0.004 285);
|
|
122
|
+
--color-chart-axis: oklch(0.86 0.006 285);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/* Dark theme, toggled by the `.dark` class the design system's
|
|
126
|
+
AppearanceProvider puts on <html>. Only the light-surface tokens flip; the
|
|
127
|
+
sidebar chrome was already authored dark and carries over.
|
|
128
|
+
|
|
129
|
+
Note the semantic solids invert their *foreground* too: lightened enough to
|
|
130
|
+
read on a near-black canvas, they can no longer carry white text at AA. */
|
|
131
|
+
.dark {
|
|
132
|
+
--color-background: oklch(0.175 0.012 285);
|
|
133
|
+
--color-foreground: oklch(0.95 0.004 285);
|
|
134
|
+
--color-card: oklch(0.215 0.012 285);
|
|
135
|
+
--color-card-foreground: oklch(0.95 0.004 285);
|
|
136
|
+
--color-popover: oklch(0.215 0.012 285);
|
|
137
|
+
--color-popover-foreground: oklch(0.95 0.004 285);
|
|
138
|
+
|
|
139
|
+
--color-primary: oklch(0.92 0.004 285);
|
|
140
|
+
--color-primary-foreground: oklch(0.21 0.012 285);
|
|
141
|
+
|
|
142
|
+
--color-brand: oklch(0.7 0.19 40);
|
|
143
|
+
--color-brand-foreground: oklch(0.21 0.012 285);
|
|
144
|
+
--color-brand-text: oklch(0.8 0.14 45);
|
|
145
|
+
--color-brand-soft: oklch(0.29 0.05 40);
|
|
146
|
+
--color-brand-soft-foreground: oklch(0.83 0.11 45);
|
|
147
|
+
|
|
148
|
+
--color-secondary: oklch(0.28 0.008 285);
|
|
149
|
+
--color-secondary-foreground: oklch(0.95 0.004 285);
|
|
150
|
+
--color-muted: oklch(0.25 0.006 285);
|
|
151
|
+
--color-muted-foreground: oklch(0.72 0.012 285);
|
|
152
|
+
--color-accent: oklch(0.27 0.008 285);
|
|
153
|
+
--color-accent-foreground: oklch(0.95 0.004 285);
|
|
154
|
+
|
|
155
|
+
--color-destructive: oklch(0.65 0.2 25);
|
|
156
|
+
--color-destructive-foreground: oklch(0.21 0.012 285);
|
|
157
|
+
--color-destructive-soft: oklch(0.3 0.08 20);
|
|
158
|
+
--color-destructive-soft-foreground: oklch(0.83 0.11 25);
|
|
159
|
+
|
|
160
|
+
--color-success: oklch(0.62 0.14 150);
|
|
161
|
+
--color-success-foreground: oklch(0.21 0.012 285);
|
|
162
|
+
--color-success-soft: oklch(0.3 0.06 150);
|
|
163
|
+
--color-success-soft-foreground: oklch(0.83 0.12 150);
|
|
164
|
+
|
|
165
|
+
--color-warning: oklch(0.7 0.13 70);
|
|
166
|
+
--color-warning-foreground: oklch(0.21 0.012 285);
|
|
167
|
+
--color-warning-soft: oklch(0.32 0.06 70);
|
|
168
|
+
--color-warning-soft-foreground: oklch(0.86 0.1 82);
|
|
169
|
+
|
|
170
|
+
--color-info: oklch(0.62 0.15 255);
|
|
171
|
+
--color-info-foreground: oklch(0.21 0.012 285);
|
|
172
|
+
--color-info-soft: oklch(0.3 0.07 255);
|
|
173
|
+
--color-info-soft-foreground: oklch(0.83 0.1 258);
|
|
174
|
+
|
|
175
|
+
--color-teal-soft: oklch(0.3 0.05 190);
|
|
176
|
+
--color-teal-soft-foreground: oklch(0.83 0.08 190);
|
|
177
|
+
--color-violet-soft: oklch(0.31 0.06 300);
|
|
178
|
+
--color-violet-soft-foreground: oklch(0.85 0.1 300);
|
|
179
|
+
|
|
180
|
+
--color-border: oklch(0.3 0.008 285);
|
|
181
|
+
--color-input: oklch(0.4 0.01 285);
|
|
182
|
+
--color-ring: oklch(0.62 0.01 285);
|
|
183
|
+
|
|
184
|
+
--color-status-active: oklch(0.7 0.16 149);
|
|
185
|
+
--color-status-invited: oklch(0.74 0.13 70);
|
|
186
|
+
|
|
187
|
+
--color-chart-1: #3987e5;
|
|
188
|
+
--color-chart-2: #199e70;
|
|
189
|
+
--color-chart-q0: #1b1b22;
|
|
190
|
+
--color-chart-q1: #1f3350;
|
|
191
|
+
--color-chart-q2: #184f95;
|
|
192
|
+
--color-chart-q3: #256abf;
|
|
193
|
+
--color-chart-q4: #3987e5;
|
|
194
|
+
--color-chart-q5: #86b6ef;
|
|
195
|
+
--color-chart-on-deep: #101016;
|
|
196
|
+
--color-chart-grid: oklch(0.27 0.006 285);
|
|
197
|
+
--color-chart-axis: oklch(0.35 0.008 285);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/* Avatar accents. Plain `:root` custom properties rather than `@theme`
|
|
201
|
+
because they are applied via inline `var(--color-avatar-…)` styles, and
|
|
202
|
+
Tailwind tree-shakes `@theme` tokens no generated utility references. */
|
|
203
|
+
:root {
|
|
204
|
+
--color-avatar-slate: oklch(0.5 0.05 250);
|
|
205
|
+
--color-avatar-green: oklch(0.5 0.11 150);
|
|
206
|
+
--color-avatar-amber: oklch(0.5 0.1 70);
|
|
207
|
+
--color-avatar-violet: oklch(0.5 0.16 300);
|
|
208
|
+
--color-avatar-rose: oklch(0.5 0.15 15);
|
|
209
|
+
--color-avatar-teal: oklch(0.5 0.08 190);
|
|
210
|
+
--color-avatar-indigo: oklch(0.5 0.16 275);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
@layer base {
|
|
214
|
+
* {
|
|
215
|
+
@apply border-border;
|
|
216
|
+
}
|
|
217
|
+
body {
|
|
218
|
+
@apply bg-background text-foreground;
|
|
219
|
+
}
|
|
220
|
+
#root {
|
|
221
|
+
/* `overflow-x-clip`, not `-hidden`: `hidden` would make #root a scroll
|
|
222
|
+
container and break the sticky chrome. Note `html` deliberately
|
|
223
|
+
carries no overflow of its own — a non-visible overflow there stops
|
|
224
|
+
`body`'s from reaching the viewport, which breaks the scrollbar
|
|
225
|
+
compensation Radix applies when a menu opens, and the whole layout
|
|
226
|
+
jumps sideways every time one does. */
|
|
227
|
+
@apply min-h-svh overflow-x-clip;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/* Thin, theme-following scrollbars. The admin is a stack of scrollports and
|
|
231
|
+
the platform default paints a heavy grey column in every one. */
|
|
232
|
+
* {
|
|
233
|
+
scrollbar-width: thin;
|
|
234
|
+
scrollbar-color: var(--color-border) transparent;
|
|
235
|
+
}
|
|
236
|
+
::-webkit-scrollbar {
|
|
237
|
+
width: 8px;
|
|
238
|
+
height: 8px;
|
|
239
|
+
}
|
|
240
|
+
::-webkit-scrollbar-track,
|
|
241
|
+
::-webkit-scrollbar-corner {
|
|
242
|
+
background: transparent;
|
|
243
|
+
}
|
|
244
|
+
::-webkit-scrollbar-thumb {
|
|
245
|
+
border-radius: 9999px;
|
|
246
|
+
background-color: var(--color-border);
|
|
247
|
+
}
|
|
248
|
+
::-webkit-scrollbar-thumb:hover {
|
|
249
|
+
background-color: var(--color-muted-foreground);
|
|
250
|
+
}
|
|
251
|
+
::selection {
|
|
252
|
+
@apply bg-foreground/15;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "es2022",
|
|
4
|
+
"lib": [
|
|
5
|
+
"es2023",
|
|
6
|
+
"dom",
|
|
7
|
+
"dom.iterable"
|
|
8
|
+
],
|
|
9
|
+
"module": "esnext",
|
|
10
|
+
"moduleResolution": "bundler",
|
|
11
|
+
"jsx": "react-jsx",
|
|
12
|
+
"types": [
|
|
13
|
+
"node",
|
|
14
|
+
"vite/client"
|
|
15
|
+
],
|
|
16
|
+
"strict": true,
|
|
17
|
+
"skipLibCheck": true,
|
|
18
|
+
"esModuleInterop": true,
|
|
19
|
+
"forceConsistentCasingInFileNames": true,
|
|
20
|
+
"resolveJsonModule": true,
|
|
21
|
+
"allowImportingTsExtensions": true,
|
|
22
|
+
"noEmit": true
|
|
23
|
+
},
|
|
24
|
+
"include": [
|
|
25
|
+
"src/**/*.ts",
|
|
26
|
+
"src/**/*.tsx",
|
|
27
|
+
"vite.config.mts"
|
|
28
|
+
]
|
|
29
|
+
}
|