create-nextly-app 0.0.2-alpha.6 → 0.0.2-alpha.60

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 (54) hide show
  1. package/dist/{chunk-6NVRVAZA.mjs → chunk-SEU6FVET.mjs} +2701 -1726
  2. package/dist/chunk-SEU6FVET.mjs.map +1 -0
  3. package/dist/cli.cjs +2803 -1831
  4. package/dist/cli.cjs.map +1 -1
  5. package/dist/cli.mjs +31 -35
  6. package/dist/cli.mjs.map +1 -1
  7. package/dist/index.cjs +2620 -1643
  8. package/dist/index.cjs.map +1 -1
  9. package/dist/index.d.cts +2 -1
  10. package/dist/index.d.ts +2 -1
  11. package/dist/index.mjs +1 -1
  12. package/package.json +11 -12
  13. package/templates/base/.env.example +11 -0
  14. package/templates/base/AGENTS.md.template +158 -0
  15. package/templates/base/CLAUDE.md.template +1 -0
  16. package/templates/base/gitignore +42 -0
  17. package/templates/base/next.config.ts +11 -17
  18. package/templates/base/src/app/admin/api/media/[[...path]]/route.ts +26 -0
  19. package/templates/base/src/app/globals.css +15 -2
  20. package/templates/base/src/app/layout.tsx +30 -15
  21. package/templates/base/tsconfig.json +8 -1
  22. package/templates/blank/src/app/globals.css +37 -1
  23. package/templates/blank/src/app/layout.tsx +26 -15
  24. package/templates/plugin/AGENTS.md.template +96 -0
  25. package/templates/plugin/CLAUDE.md.template +1 -0
  26. package/templates/plugin/README.md +44 -0
  27. package/templates/plugin/dev/.env.example +8 -0
  28. package/templates/plugin/dev/instrumentation.ts +10 -0
  29. package/templates/plugin/dev/next.config.ts +30 -0
  30. package/templates/plugin/dev/nextly.config.ts +17 -0
  31. package/templates/plugin/dev/src/app/admin/[[...params]]/page.tsx +9 -0
  32. package/templates/plugin/dev/src/app/admin/api/[[...params]]/route.ts +19 -0
  33. package/templates/plugin/dev/src/app/layout.tsx +13 -0
  34. package/templates/plugin/dev/tsconfig.json +22 -0
  35. package/templates/plugin/eslint.config.mjs +8 -0
  36. package/templates/plugin/gitignore +10 -0
  37. package/templates/plugin/src/admin/SettingsPage.tsx +57 -0
  38. package/templates/plugin/src/admin/index.ts +24 -0
  39. package/templates/plugin/src/collections/example.ts +13 -0
  40. package/templates/plugin/src/index.ts +6 -0
  41. package/templates/plugin/src/plugin.test.ts +28 -0
  42. package/templates/plugin/src/plugin.ts +48 -0
  43. package/templates/plugin/src/types.ts +17 -0
  44. package/templates/plugin/template.json +14 -0
  45. package/templates/plugin/tsconfig.json +18 -0
  46. package/templates/plugin/tsup.config.ts +24 -0
  47. package/templates/plugin/vitest.config.ts +8 -0
  48. package/dist/chunk-6NVRVAZA.mjs.map +0 -1
  49. package/templates/blank/src/access/README.md +0 -22
  50. package/templates/blank/src/collections/README.md +0 -22
  51. package/templates/blank/src/lib/README.md +0 -15
  52. package/templates/blank/src/singles/README.md +0 -19
  53. /package/templates/blank/src/{components → app/(frontend)/components}/README.md +0 -0
  54. /package/templates/blank/src/{components → app/(frontend)/components}/ThemeToggle.tsx +0 -0
package/dist/index.d.cts CHANGED
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * Project type / template selection.
3
3
  * "blog" is the first content template added for alpha.
4
+ * "plugin" scaffolds a publishable Nextly plugin package + embedded /dev playground (D44/D45).
4
5
  */
5
- type ProjectType = "blank" | "blog";
6
+ type ProjectType = "blank" | "blog" | "plugin";
6
7
  /**
7
8
  * Schema approach selection for content templates.
8
9
  * - code-first: Full schema definitions in nextly.config.ts (like Payload CMS)
package/dist/index.d.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * Project type / template selection.
3
3
  * "blog" is the first content template added for alpha.
4
+ * "plugin" scaffolds a publishable Nextly plugin package + embedded /dev playground (D44/D45).
4
5
  */
5
- type ProjectType = "blank" | "blog";
6
+ type ProjectType = "blank" | "blog" | "plugin";
6
7
  /**
7
8
  * Schema approach selection for content templates.
8
9
  * - code-first: Full schema definitions in nextly.config.ts (like Payload CMS)
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createRequire } from 'module';
2
- export { createNextly } from './chunk-6NVRVAZA.mjs';
2
+ export { createNextly } from './chunk-SEU6FVET.mjs';
3
3
 
4
4
  createRequire(import.meta.url);
5
5
  //# sourceMappingURL=index.mjs.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-nextly-app",
3
- "version": "0.0.2-alpha.6",
3
+ "version": "0.0.2-alpha.60",
4
4
  "description": "CLI to scaffold Nextly in your Next.js project",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -27,27 +27,26 @@
27
27
  "commander": "^14.0.2",
28
28
  "execa": "^9.6.0",
29
29
  "fs-extra": "^11.3.0",
30
- "ora": "^9.0.0",
31
30
  "picocolors": "^1.1.1",
32
- "tar": "^7.4.0"
31
+ "tar": "^7.5.19"
33
32
  },
34
33
  "devDependencies": {
35
34
  "@types/fs-extra": "^11.0.4",
36
- "@types/tar": "^6.1.13",
37
35
  "@types/node": "^20.19.17",
38
- "@vitest/coverage-v8": "^4.0.8",
39
- "@vitest/ui": "^4.0.8",
40
- "eslint": "^9.34.0",
36
+ "@types/tar": "^6.1.13",
37
+ "@vitest/coverage-v8": "^4.1.0",
38
+ "@vitest/ui": "^4.1.0",
39
+ "eslint": "^9.39.1",
41
40
  "tsup": "^8.5.0",
42
41
  "typescript": "^5.9.3",
43
42
  "vite-tsconfig-paths": "^5.1.4",
44
- "vitest": "^4.0.8",
45
- "@nextlyhq/eslint-config": "0.0.2-alpha.5",
46
- "@nextlyhq/telemetry": "0.0.2-alpha.5",
47
- "@nextlyhq/tsconfig": "0.0.2-alpha.5"
43
+ "vitest": "^4.1.0",
44
+ "@nextlyhq/eslint-config": "0.0.2-alpha.60",
45
+ "@nextlyhq/telemetry": "0.0.2-alpha.60",
46
+ "@nextlyhq/tsconfig": "0.0.2-alpha.60"
48
47
  },
49
48
  "engines": {
50
- "node": ">=20.0.0"
49
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
51
50
  },
52
51
  "keywords": [
53
52
  "nextly",
@@ -12,5 +12,16 @@ NEXTLY_SECRET=change-me-generate-a-secure-secret
12
12
  # Application URL
13
13
  NEXT_PUBLIC_APP_URL=http://localhost:3000
14
14
 
15
+ # Development diagnostics (opt-in)
16
+ # Uncomment to add a _devDiagnostics field to error responses, carrying the
17
+ # error's log context and the underlying cause, so a failure names itself while
18
+ # you build instead of only in the server log.
19
+ #
20
+ # Left commented on purpose. It is the SECOND of two independent signals — the
21
+ # first is NODE_ENV — and the second exists precisely because NODE_ENV is a
22
+ # runtime value a deployment can carry by mistake. A default that ships in this
23
+ # file would be true in exactly that case, which is the one it guards against.
24
+ # NEXTLY_DEV_DIAGNOSTICS=1
25
+
15
26
  # Storage Configuration
16
27
  {{storageEnvVars}}
@@ -0,0 +1,158 @@
1
+ <!-- nextly:managed:start -->
2
+ <!--
3
+ Everything between these markers is generated by create-nextly-app and may be
4
+ replaced when the project is upgraded. Write your own notes OUTSIDE the block:
5
+ anything above or below it is yours and is preserved.
6
+ -->
7
+
8
+ # Agent guide for this Nextly project
9
+
10
+ A Next.js app with [Nextly](https://nextlyhq.com) for content. The CMS runs
11
+ inside this app rather than beside it: same server, same database, no separate
12
+ service to start.
13
+
14
+ ## Where things are
15
+
16
+ | path | what it is |
17
+ | ---------------------- | ------------------------------------------------------------------ |
18
+ | `nextly.config.ts` | the config the whole app boots from — collections, database, admin |
19
+ | `src/collections/` | code-first collection definitions, when this project uses them |
20
+ | `src/app/(frontend)/` | the public site |
21
+ | `src/app/admin/` | the admin panel, served at `/admin` |
22
+ | `migrations/` | SQL migrations, in this project's dialect |
23
+ | `src/types/generated/` | generated types — never edit by hand |
24
+
25
+ ## Commands
26
+
27
+ ```bash
28
+ {{runCommand}} dev # start Next.js; schema changes apply in-process
29
+ {{runCommand}} build # migrate, then build
30
+ {{runCommand}} db:setup # first run: create the schema
31
+ {{runCommand}} db:migrate # apply pending migrations
32
+ {{runCommand}} db:migrate:status # what has and has not been applied
33
+ {{runCommand}} db:migrate:fresh # drop everything and re-run (destructive)
34
+ {{runCommand}} types:generate # regenerate types after a schema change
35
+ ```
36
+
37
+ This project uses **{{databaseDialect}}**.
38
+
39
+ ## Reading content
40
+
41
+ Content is read through the Direct API, on the server:
42
+
43
+ ```ts
44
+ import { getNextly } from "nextly";
45
+ import nextlyConfig from "@nextly-config";
46
+
47
+ const nextly = await getNextly({ config: nextlyConfig });
48
+ const posts = await nextly.find({ collection: "posts", limit: 10 });
49
+ ```
50
+
51
+ **`find()` returns loosely-typed documents until types are generated.** Without
52
+ `{{runCommand}} types:generate`, `posts.items` is `Record<string, unknown>[]` and a
53
+ field access is `unknown`. Generating types makes the slug and the return type
54
+ infer, so `posts.items` becomes `Post[]`.
55
+
56
+ Until then, narrow with a runtime check rather than an assertion — a document
57
+ missing a field yields `undefined`, and asserting the type only moves the
58
+ failure to where the value is used:
59
+
60
+ ```ts
61
+ const slug = typeof doc.slug === "string" ? doc.slug : undefined;
62
+ ```
63
+
64
+ ## Things that surprise people
65
+
66
+ - **Users are not a normal collection.** Read them through `nextly.users`, not
67
+ `find({ collection: "users" })`. The users namespace supports pagination and
68
+ `search`; it does not support a `where` filter.
69
+ - **A migration you generate is one file, in THIS project's dialect.**
70
+ `{{execCommand}} nextly migrate:create <name>` (no package.json script exists for it) writes a
71
+ single `migrations/<timestamp>_<slug>.sql` containing **{{databaseDialect}}**
72
+ SQL — no `.mysql.sql` or `.sqlite.sql` beside it. Edit that file.
73
+ - **A suffixed set means the migration was shipped, not generated.** A template
74
+ that supports every dialect ships `0001_x.sql` (Postgres) alongside
75
+ `0001_x.mysql.sql` and `0001_x.sqlite.sql`, and the CLI runs the variant for
76
+ the configured dialect. Those are prebuilt. Adding companion variants to a
77
+ migration you generated is not part of the normal workflow, and nothing will
78
+ run them unless this project is itself meant to support several dialects.
79
+ - **The admin needs a first user.** Visit `/admin` after `{{runCommand}} dev` and
80
+ complete the setup screen.
81
+ - **Cache tags come from `nextly/runtime`.** Wrap PUBLIC reads in `cachedFind`
82
+ and tag them with `nextlyTags(collection)` so a write invalidates the right
83
+ pages.
84
+
85
+ **`keyParts` is a security boundary, not a performance detail.** Two callers
86
+ whose `keyParts` match share one cache entry. So if the read applies any
87
+ per-caller rule — owner-only scoping, role-based visibility, an API key's
88
+ narrowed scope — the caller's identity has to be IN the key, or one user's
89
+ rows get served to another:
90
+
91
+ **A shared cache entry is only safe if the read is public for EVERY reader.**
92
+ Two independent things can make it not so, and each needs its own answer:
93
+
94
+ - **drafts** — `find()` defaults to `overrideAccess: true`, a trusted server
95
+ read, so a slug filter alone can return an unpublished row. `status:
96
+ "published"` is enforced even on a trusted read and fixes this one.
97
+ - **per-row access rules** — owner-only scoping, role-based visibility, an API
98
+ key's narrowed scope. `status` does NOT evaluate these; it is a
99
+ draft/published lifecycle filter and nothing more. A published row that only
100
+ its owner may read is still returned by the query above.
101
+
102
+ So the decision is: **does this collection carry read-access rules?**
103
+
104
+ ```ts
105
+ // NO access rules — the result is identical for every reader, so one shared
106
+ // entry is correct and gets the full ISR benefit.
107
+ // `find()` always returns the list envelope `{ items, meta }`, so a detail route takes the
108
+ // first item — and caches THAT, not the envelope around it.
109
+ cachedFind(
110
+ async () =>
111
+ (
112
+ await nextly.find({
113
+ collection: "posts",
114
+ where: { slug: { equals: slug } },
115
+ status: "published",
116
+ limit: 1,
117
+ })
118
+ ).items[0] ?? null,
119
+ { tags: nextlyTags("posts"), keyParts: ["posts", slug] }
120
+ );
121
+
122
+ // ACCESS RULES — evaluate them as the caller, and key by EVERY dimension the
123
+ // rules read, not just who the caller is.
124
+ //
125
+ // Both role fields are optional and Nextly reads BOTH: the many-to-many
126
+ // `roles` unioned with the singular `role`. Deduped and sorted, so one role
127
+ // set spelled two ways is one cache entry rather than two.
128
+ const roleKey = (u: { role?: string; roles?: string[] }) =>
129
+ [...new Set([...(u.roles ?? []), u.role ?? []].flat())].sort().join(",");
130
+
131
+ cachedFind(
132
+ () => nextly.find({ collection: "orders", user, overrideAccess: false }),
133
+ {
134
+ tags: nextlyTags("orders"),
135
+ keyParts: ["orders", "list", user.id, roleKey(user)],
136
+ }
137
+ );
138
+ ```
139
+
140
+ **`user.id` alone is not the authorization context.** Roles change, custom
141
+ claims change, an API key's scope is narrower than its owner's. A key built
142
+ from identity alone survives all of those, so the same person can fill the
143
+ entry while privileged and read it back after being downgraded — the tags bust
144
+ on CONTENT changes, and a permission change is not one. Include whatever the
145
+ rules actually read: the role set, the claim, the key's scope.
146
+
147
+ If you are unsure whether a collection has read rules, do not cache it. A
148
+ missing cache is slow; a shared one leaks.
149
+
150
+ ## Conventions worth keeping
151
+
152
+ - Server components read content directly; do not build an API route to fetch
153
+ your own content.
154
+ - Regenerate types in the same change as the schema edit that needs them.
155
+ - Migrations are append-only once shared. Add a new one rather than editing an
156
+ applied file.
157
+
158
+ <!-- nextly:managed:end -->
@@ -0,0 +1 @@
1
+ @AGENTS.md
@@ -0,0 +1,42 @@
1
+ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
+
3
+ # dependencies
4
+ /node_modules
5
+ /.pnp
6
+ .pnp.*
7
+ .yarn/*
8
+ !.yarn/patches
9
+ !.yarn/plugins
10
+ !.yarn/releases
11
+ !.yarn/versions
12
+
13
+ # testing
14
+ /coverage
15
+
16
+ # next.js
17
+ /.next/
18
+ /out/
19
+
20
+ # production
21
+ /build
22
+
23
+ # misc
24
+ .DS_Store
25
+ *.pem
26
+
27
+ # debug
28
+ npm-debug.log*
29
+ yarn-debug.log*
30
+ yarn-error.log*
31
+ .pnpm-debug.log*
32
+
33
+ # env files (can opt-in for committing if needed)
34
+ .env*
35
+ !.env.example
36
+
37
+ # vercel
38
+ .vercel
39
+
40
+ # typescript
41
+ *.tsbuildinfo
42
+ next-env.d.ts
@@ -1,21 +1,15 @@
1
1
  import type { NextConfig } from "next";
2
2
 
3
- const nextConfig: NextConfig = {
4
- serverExternalPackages: [
5
- "nextly",
6
- "@nextlyhq/adapter-drizzle",
7
- "@nextlyhq/adapter-postgres",
8
- "@nextlyhq/adapter-mysql",
9
- "@nextlyhq/adapter-sqlite",
10
- "drizzle-orm",
11
- "drizzle-kit",
12
- "pg",
13
- "mysql2",
14
- "better-sqlite3",
15
- "bcryptjs",
16
- "sharp",
17
- "esbuild",
18
- ],
19
- };
3
+ /**
4
+ * Minimal base `next.config.ts` for bundled templates.
5
+ *
6
+ * This file is intentionally left minimal (no adapter whitelist). The
7
+ * scaffolder will generate or patch a database-specific `next.config.ts`
8
+ * that includes only the selected adapter and driver in
9
+ * `serverExternalPackages`. Keeping the base file minimal avoids
10
+ * advertising adapters the user didn't choose and prevents misleading
11
+ * generated projects.
12
+ */
13
+ const nextConfig: NextConfig = {};
20
14
 
21
15
  export default nextConfig;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Admin Media API — permission-gated management surface.
3
+ *
4
+ * The admin's session cookie is scoped to `/admin`, so it reaches here but not
5
+ * the public `/api/media`. Every operation is checked against a media
6
+ * permission (read/create/update/delete-media) and the acting identity is
7
+ * taken from the session/API key, never a request field. This is the only
8
+ * place media can be uploaded, edited, moved or deleted.
9
+ *
10
+ * Supported (all gated):
11
+ * - GET/POST/PATCH/DELETE /admin/api/media/* (files, folders, bulk delete)
12
+ */
13
+
14
+ import { createMediaHandlers } from "nextly/api/media-handlers";
15
+
16
+ import config from "@nextly-config";
17
+
18
+ const handlers = createMediaHandlers({
19
+ config,
20
+ requireAuth: true,
21
+ });
22
+
23
+ export const GET = handlers.GET;
24
+ export const POST = handlers.POST;
25
+ export const PATCH = handlers.PATCH;
26
+ export const DELETE = handlers.DELETE;
@@ -1,5 +1,15 @@
1
1
  @import "tailwindcss";
2
2
 
3
+ /* The families the imported font stylesheets declare, bound to the variables the rules
4
+ below read. Named here rather than by `next/font`, which used to inject them: the faces
5
+ now arrive as ordinary stylesheets, so the binding has to be written down somewhere, and
6
+ the name is load-bearing — the admin theme reads `--font-geist`. Quoted family names, so
7
+ the two-word values stay one identifier. */
8
+ :root {
9
+ --font-geist: "Geist Variable";
10
+ --font-geist-mono: "Geist Mono Variable";
11
+ }
12
+
3
13
  :root {
4
14
  --background: #ffffff;
5
15
  --foreground: #171717;
@@ -8,8 +18,11 @@
8
18
  @theme inline {
9
19
  --color-background: var(--background);
10
20
  --color-foreground: var(--foreground);
11
- --font-sans: var(--font-geist-sans);
12
- --font-mono: var(--font-geist-mono);
21
+ /* Fallback arguments, not a trailing family list: an unresolved var() with no
22
+ fallback invalidates the whole declaration rather than falling through to
23
+ what comes after the closing paren. */
24
+ --font-sans: var(--font-geist, ui-sans-serif), system-ui, sans-serif;
25
+ --font-mono: var(--font-geist-mono, ui-monospace), monospace;
13
26
  }
14
27
 
15
28
  @media (prefers-color-scheme: dark) {
@@ -1,16 +1,31 @@
1
1
  import type { Metadata } from "next";
2
- import { Geist, Geist_Mono } from "next/font/google";
2
+ import "@fontsource-variable/geist";
3
+ import "@fontsource-variable/geist-mono";
3
4
  import "./globals.css";
4
5
 
5
- const geistSans = Geist({
6
- variable: "--font-geist-sans",
7
- subsets: ["latin"],
8
- });
9
-
10
- const geistMono = Geist_Mono({
11
- variable: "--font-geist-mono",
12
- subsets: ["latin"],
13
- });
6
+ /**
7
+ * The faces are imported as STYLESHEETS from packages in `node_modules`, rather
8
+ * than fetched from fonts.googleapis.com by `next/font/google` while
9
+ * `next build` runs — which made every build depend on reaching a third party
10
+ * and fail behind a proxy, on a locked-down runner, or offline.
11
+ *
12
+ * A bare package import rather than `next/font/local` pointing INTO
13
+ * `node_modules`: a literal path asserts where the package physically lives,
14
+ * and that assertion is false under Yarn's Plug'n'Play linker (no
15
+ * `node_modules` at all), under npm/Yarn workspace hoisting (the package moves
16
+ * to the workspace root), and under pnpm's own symlinked store. An import asks
17
+ * the resolver instead, which is correct under every layout.
18
+ *
19
+ * The trade is `next/font`'s metric-adjusted fallback, so text can shift
20
+ * slightly as the face arrives. The families are bound to this app's `--font-*`
21
+ * variables in `globals.css`, which is what every rule downstream reads. *
22
+ * Importing the package root ships every subset the face offers — Latin, Latin
23
+ * Extended, Cyrillic, Vietnamese — but each `@font-face` carries a
24
+ * `unicode-range`, so a browser downloads only the subsets the page actually
25
+ * uses. The deployed bundle is larger than a single hand-picked file; what a
26
+ * reader fetches is not, and a page in Cyrillic now gets its face instead of a
27
+ * fallback.
28
+ */
14
29
 
15
30
  /**
16
31
  * `metadataBase` tells Next.js how to resolve relative URLs in OpenGraph
@@ -32,12 +47,12 @@ export default function RootLayout({
32
47
  children: React.ReactNode;
33
48
  }>) {
34
49
  return (
50
+ // The font variables belong on <html>: the admin theme declares --font-sans
51
+ // in a non-inline @theme, which emits it into :root and resolves the
52
+ // reference THERE. A variable exposed lower down, on <body>, is invisible to
53
+ // that declaration however it is spelled.
35
54
  <html lang="en" suppressHydrationWarning>
36
- <body
37
- className={`${geistSans.variable} ${geistMono.variable} antialiased`}
38
- >
39
- {children}
40
- </body>
55
+ <body className="antialiased">{children}</body>
41
56
  </html>
42
57
  );
43
58
  }
@@ -20,7 +20,14 @@
20
20
  ],
21
21
  "paths": {
22
22
  "@/*": ["./src/*"],
23
- "@nextly-config": ["./nextly.config.ts"]
23
+ "@/components/*": ["./src/app/(frontend)/components/*"],
24
+ "@nextly-config": ["./nextly.config.ts"],
25
+ // Pagefind's browser bundle is generated into public/ at build time, so it
26
+ // is imported by URL and cannot be resolved on disk while type-checking.
27
+ // A `declare module` cannot describe it either: TypeScript treats a rooted
28
+ // specifier as relative and rejects the declaration (TS2436). Templates
29
+ // without a search page simply never import it, leaving this mapping inert.
30
+ "/pagefind/pagefind.js": ["./src/types/pagefind.d.ts"]
24
31
  }
25
32
  },
26
33
  "include": [
@@ -1,5 +1,25 @@
1
1
  @import "tailwindcss";
2
2
 
3
+ /* The families the imported font stylesheets declare, bound to the variables the rules
4
+ below read. Named here rather than by `next/font`, which used to inject them: the faces
5
+ now arrive as ordinary stylesheets, so the binding has to be written down somewhere, and
6
+ the name is load-bearing — the admin theme reads `--font-geist`. Quoted family names, so
7
+ the two-word values stay one identifier. */
8
+ :root {
9
+ /* `--font-geist` and `--font-geist-mono` are the names the ADMIN theme reads
10
+ (`--font-sans: var(--font-geist, Geist), ...` in a non-inline `@theme`, so
11
+ it resolves at `:root`). A template that loads a different face still has to
12
+ expose it under these names or the admin renders in a system fallback while
13
+ the app's own pages render correctly. */
14
+ --font-geist: "Inter Variable";
15
+ --font-geist-mono: "JetBrains Mono Variable";
16
+ /* This template's own display face, read by `.font-display` below. NOT named
17
+ `--font-mono` for its mono face: that is the theme's OWN token, declared at
18
+ `:root`, and redefining it here replaces the theme's whole fallback chain
19
+ rather than feeding it. */
20
+ --font-display: "Inter Variable";
21
+ }
22
+
3
23
  /**
4
24
  * Blank-template global styles.
5
25
  *
@@ -28,6 +48,18 @@
28
48
  --color-foreground: #f9fafb;
29
49
  }
30
50
 
51
+ /* `@theme inline` on purpose, and it is the whole point of this block. An inline theme
52
+ SUBSTITUTES the value where the utility is used; a plain `@theme` emits the variable into
53
+ `:root`, which is where `@nextlyhq/admin` declares its own `--font-mono` — redefining it there
54
+ would replace the admin's entire fallback chain with this one family.
55
+
56
+ Needed because the frontend routes do not load the admin stylesheet, so nothing else binds the
57
+ `font-mono` utility this template uses on its badge and file-path text; without it they fall
58
+ back to Tailwind's default mono rather than the face the template loads. */
59
+ @theme inline {
60
+ --font-mono: var(--font-geist-mono, ui-monospace), monospace;
61
+ }
62
+
31
63
  @theme {
32
64
  --color-background: var(--color-background);
33
65
  --color-foreground: var(--color-foreground);
@@ -67,7 +99,11 @@ body {
67
99
  }
68
100
 
69
101
  .font-display {
70
- font-family: var(--font-display), ui-sans-serif, system-ui, sans-serif;
102
+ /* The generic families are the var()'s fallback argument rather than a list
103
+ after it: an unresolved var() with no fallback invalidates the whole
104
+ declaration, so the element would inherit its parent's face instead of
105
+ falling through to these. */
106
+ font-family: var(--font-display, ui-sans-serif), system-ui, sans-serif;
71
107
  }
72
108
 
73
109
  .btn-primary {
@@ -1,19 +1,32 @@
1
1
  import type { Metadata } from "next";
2
- import { Inter, JetBrains_Mono } from "next/font/google";
2
+ import "@fontsource-variable/inter";
3
+ import "@fontsource-variable/jetbrains-mono";
3
4
 
4
5
  import "./globals.css";
5
6
 
6
- const display = Inter({
7
- variable: "--font-display",
8
- subsets: ["latin"],
9
- display: "swap",
10
- });
11
-
12
- const mono = JetBrains_Mono({
13
- variable: "--font-mono",
14
- subsets: ["latin"],
15
- display: "swap",
16
- });
7
+ /**
8
+ * The faces are imported as STYLESHEETS from packages in `node_modules`, rather
9
+ * than fetched from fonts.googleapis.com by `next/font/google` while
10
+ * `next build` runs — which made every build depend on reaching a third party
11
+ * and fail behind a proxy, on a locked-down runner, or offline.
12
+ *
13
+ * A bare package import rather than `next/font/local` pointing INTO
14
+ * `node_modules`: a literal path asserts where the package physically lives,
15
+ * and that assertion is false under Yarn's Plug'n'Play linker (no
16
+ * `node_modules` at all), under npm/Yarn workspace hoisting (the package moves
17
+ * to the workspace root), and under pnpm's own symlinked store. An import asks
18
+ * the resolver instead, which is correct under every layout.
19
+ *
20
+ * The trade is `next/font`'s metric-adjusted fallback, so text can shift
21
+ * slightly as the face arrives. The families are bound to this app's `--font-*`
22
+ * variables in `globals.css`, which is what every rule downstream reads. *
23
+ * Importing the package root ships every subset the face offers — Latin, Latin
24
+ * Extended, Cyrillic, Vietnamese — but each `@font-face` carries a
25
+ * `unicode-range`, so a browser downloads only the subsets the page actually
26
+ * uses. The deployed bundle is larger than a single hand-picked file; what a
27
+ * reader fetches is not, and a page in Cyrillic now gets its face instead of a
28
+ * fallback.
29
+ */
17
30
 
18
31
  /**
19
32
  * Blank-template root layout.
@@ -38,9 +51,7 @@ export default function RootLayout({
38
51
  }>) {
39
52
  return (
40
53
  <html lang="en" suppressHydrationWarning>
41
- <body className={`${display.variable} ${mono.variable} antialiased`}>
42
- {children}
43
- </body>
54
+ <body className="antialiased">{children}</body>
44
55
  </html>
45
56
  );
46
57
  }