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.
Files changed (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +7 -0
  3. package/dist/cli.d.ts +3 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +277 -0
  6. package/dist/index.d.ts +11 -0
  7. package/dist/index.d.ts.map +1 -0
  8. package/dist/index.js +32 -0
  9. package/dist/lib/conditionals.d.ts +37 -0
  10. package/dist/lib/conditionals.d.ts.map +1 -0
  11. package/dist/lib/conditionals.js +115 -0
  12. package/dist/lib/features.d.ts +185 -0
  13. package/dist/lib/features.d.ts.map +1 -0
  14. package/dist/lib/features.js +328 -0
  15. package/dist/lib/template.d.ts +46 -0
  16. package/dist/lib/template.d.ts.map +1 -0
  17. package/dist/lib/template.js +115 -0
  18. package/dist/lib/ui.d.ts +76 -0
  19. package/dist/lib/ui.d.ts.map +1 -0
  20. package/dist/lib/ui.js +310 -0
  21. package/dist/lib/validate.d.ts +13 -0
  22. package/dist/lib/validate.d.ts.map +1 -0
  23. package/dist/lib/validate.js +51 -0
  24. package/package.json +37 -0
  25. package/templates/default/README.md.tmpl +199 -0
  26. package/templates/default/_gitignore +8 -0
  27. package/templates/default/apps/admin/index.html +38 -0
  28. package/templates/default/apps/admin/src/main.tsx +5 -0
  29. package/templates/default/apps/admin/src/plugins.spec.ts +58 -0
  30. package/templates/default/apps/admin/src/plugins.ts +57 -0
  31. package/templates/default/apps/admin/src/styles.css +254 -0
  32. package/templates/default/apps/admin/tsconfig.json +29 -0
  33. package/templates/default/apps/admin/vite.config.mts +67 -0
  34. package/templates/default/apps/admin-e2e/playwright.config.ts +51 -0
  35. package/templates/default/apps/admin-e2e/src/auth.spec.ts +55 -0
  36. package/templates/default/apps/admin-e2e/src/support/seed.ts +62 -0
  37. package/templates/default/apps/admin-e2e/tsconfig.json +23 -0
  38. package/templates/default/apps/server/jest.config.js +38 -0
  39. package/templates/default/apps/server/jest.setup.js +20 -0
  40. package/templates/default/apps/server/ortha.config.ts +505 -0
  41. package/templates/default/apps/server/src/main.ts +26 -0
  42. package/templates/default/apps/server/src/plugins.spec.ts +71 -0
  43. package/templates/default/apps/server/src/plugins.ts +229 -0
  44. package/templates/default/apps/server/tsconfig.json +31 -0
  45. package/templates/default/apps/server-e2e/jest.config.js +47 -0
  46. package/templates/default/apps/server-e2e/src/api.spec.ts +107 -0
  47. package/templates/default/apps/server-e2e/src/global-setup.ts +41 -0
  48. package/templates/default/apps/server-e2e/src/jest.setup.ts +28 -0
  49. package/templates/default/apps/server-e2e/src/support/db.ts +143 -0
  50. package/templates/default/apps/server-e2e/src/support/test-app.ts +64 -0
  51. package/templates/default/apps/server-e2e/tsconfig.json +26 -0
  52. package/templates/default/docker-compose.yml +21 -0
  53. package/templates/default/env.tmpl +140 -0
  54. package/templates/default/package.json.tmpl +51 -0
  55. 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,8 @@
1
+ node_modules
2
+ dist
3
+ .env
4
+ .storage
5
+ *.tsbuildinfo
6
+ test-output
7
+ playwright-report
8
+ test-results
@@ -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,5 @@
1
+ import { createAdmin } from '@orthacms/bootstrap-admin';
2
+ import { buildPlugins } from './plugins';
3
+ import './styles.css';
4
+
5
+ createAdmin({ plugins: buildPlugins() });
@@ -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
+ }