@nexus_js/cli 0.7.1 → 0.7.4
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/dist/bin.js +220 -21
- package/dist/bin.js.map +1 -1
- package/dist/config.d.ts +56 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/create.d.ts +5 -3
- package/dist/create.d.ts.map +1 -1
- package/dist/create.js +1573 -107
- package/dist/create.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/load-app-config.d.ts +3 -0
- package/dist/load-app-config.d.ts.map +1 -0
- package/dist/load-app-config.js +24 -0
- package/dist/load-app-config.js.map +1 -0
- package/dist/security-report.d.ts +16 -0
- package/dist/security-report.d.ts.map +1 -0
- package/dist/security-report.js +114 -0
- package/dist/security-report.js.map +1 -0
- package/dist/studio.d.ts +39 -2
- package/dist/studio.d.ts.map +1 -1
- package/dist/studio.js +270 -3
- package/dist/studio.js.map +1 -1
- package/package.json +7 -5
package/dist/create.js
CHANGED
|
@@ -2,12 +2,15 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* create-nexus — scaffolding CLI for new Nexus projects.
|
|
4
4
|
* Usage: npm create @nexus_js/nexus@latest my-app
|
|
5
|
-
* npx @nexus_js/create-nexus my-app
|
|
6
|
-
* npm exec --package=@nexus_js/cli -- create-nexus my-app
|
|
5
|
+
* npx @nexus_js/create-nexus my-app [--template minimal|full]
|
|
6
|
+
* npm exec --package=@nexus_js/cli -- create-nexus my-app -t minimal
|
|
7
|
+
* create-nexus --yes (defaults, no prompts)
|
|
7
8
|
*/
|
|
8
|
-
import { readFileSync } from 'node:fs';
|
|
9
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
9
10
|
import { mkdir, writeFile } from 'node:fs/promises';
|
|
10
11
|
import { dirname, join, resolve } from 'node:path';
|
|
12
|
+
import { stdin as input, stdout as output } from 'node:process';
|
|
13
|
+
import { createInterface } from 'node:readline/promises';
|
|
11
14
|
import { fileURLToPath } from 'node:url';
|
|
12
15
|
/** Same version as the published @nexus_js/cli (peer @nexus_js/* packages stay in sync). */
|
|
13
16
|
function getPublishedCliVersion() {
|
|
@@ -17,6 +20,7 @@ function getPublishedCliVersion() {
|
|
|
17
20
|
}
|
|
18
21
|
const CYAN = '\x1b[36m';
|
|
19
22
|
const GREEN = '\x1b[32m';
|
|
23
|
+
const RED = '\x1b[31m';
|
|
20
24
|
const YELLOW = '\x1b[33m';
|
|
21
25
|
const RESET = '\x1b[0m';
|
|
22
26
|
const BOLD = '\x1b[1m';
|
|
@@ -27,47 +31,214 @@ const BANNER = `
|
|
|
27
31
|
The Definitive Full-Stack Framework
|
|
28
32
|
Islands × Runes × Server Actions
|
|
29
33
|
`;
|
|
34
|
+
const TEMPLATE_HINT = {
|
|
35
|
+
minimal: 'Minimal — one landing page, no i18n (build almost from scratch)',
|
|
36
|
+
full: 'Full — i18n (en/es/pt), islands + blog examples',
|
|
37
|
+
};
|
|
38
|
+
/** Official Nexus mark — keep in sync with docs/assets/nexus-logo.svg */
|
|
39
|
+
const NEXUS_LOGO_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 256 256" fill="none" aria-hidden="true">
|
|
40
|
+
<defs>
|
|
41
|
+
<linearGradient id="nxOrbitGrad" x1="40" y1="36" x2="216" y2="220" gradientUnits="userSpaceOnUse">
|
|
42
|
+
<stop offset="0%" stop-color="#2DD4BF"/>
|
|
43
|
+
<stop offset="50%" stop-color="#7C3AED"/>
|
|
44
|
+
<stop offset="100%" stop-color="#F472B6"/>
|
|
45
|
+
</linearGradient>
|
|
46
|
+
<radialGradient id="nxMarkBg" cx="32%" cy="28%" r="85%">
|
|
47
|
+
<stop offset="0%" stop-color="#1e293b"/>
|
|
48
|
+
<stop offset="100%" stop-color="#020617"/>
|
|
49
|
+
</radialGradient>
|
|
50
|
+
<radialGradient id="nxNucleusFill" cx="50%" cy="50%" r="50%">
|
|
51
|
+
<stop offset="0%" stop-color="#020617"/>
|
|
52
|
+
<stop offset="72%" stop-color="#0f172a"/>
|
|
53
|
+
<stop offset="100%" stop-color="#1e1b4a"/>
|
|
54
|
+
</radialGradient>
|
|
55
|
+
</defs>
|
|
56
|
+
<rect x="20" y="20" width="216" height="216" rx="60" ry="60" fill="url(#nxMarkBg)"/>
|
|
57
|
+
<rect x="20" y="20" width="216" height="216" rx="60" ry="60" stroke="#475569" stroke-width="1" fill="none" opacity="0.45"/>
|
|
58
|
+
<circle cx="128" cy="86" r="46" fill="url(#nxOrbitGrad)" opacity="0.88"/>
|
|
59
|
+
<circle cx="172" cy="152" r="40" fill="url(#nxOrbitGrad)" opacity="0.78"/>
|
|
60
|
+
<circle cx="84" cy="152" r="34" fill="url(#nxOrbitGrad)" opacity="0.72"/>
|
|
61
|
+
<circle cx="128" cy="128" r="54" fill="url(#nxNucleusFill)"/>
|
|
62
|
+
<circle cx="128" cy="128" r="54" fill="none" stroke="url(#nxOrbitGrad)" stroke-width="2" opacity="0.55"/>
|
|
63
|
+
<circle cx="128" cy="128" r="48" fill="none" stroke="#ffffff" stroke-opacity="0.08" stroke-width="1"/>
|
|
64
|
+
<path d="M 108 90 L 108 166 M 108 90 L 148 166 M 148 90 L 148 166" stroke="#ffffff" stroke-width="12" stroke-linecap="round" stroke-linejoin="round"/>
|
|
65
|
+
</svg>`;
|
|
66
|
+
const TEMPLATE_DETAIL = {
|
|
67
|
+
minimal: 'Single +page.nx + layout, no i18n, no /islands or /blog examples',
|
|
68
|
+
full: 'i18n (en/es/pt), islands guide, blog — like the my-nexus-app reference',
|
|
69
|
+
};
|
|
70
|
+
function parseCreateArgs(argv) {
|
|
71
|
+
let projectNamePositional;
|
|
72
|
+
let template;
|
|
73
|
+
let help = false;
|
|
74
|
+
let useDefaults = false;
|
|
75
|
+
for (let i = 2; i < argv.length; i++) {
|
|
76
|
+
const a = argv[i];
|
|
77
|
+
if (a === undefined)
|
|
78
|
+
continue;
|
|
79
|
+
if (a === '--help' || a === '-h') {
|
|
80
|
+
help = true;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (a === '--yes' || a === '-y' || a === '--defaults') {
|
|
84
|
+
useDefaults = true;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (a === '--template' || a === '-t') {
|
|
88
|
+
const v = argv[++i];
|
|
89
|
+
if (v === 'minimal' || v === 'full')
|
|
90
|
+
template = v;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (a.startsWith('-'))
|
|
94
|
+
continue;
|
|
95
|
+
if (!projectNamePositional)
|
|
96
|
+
projectNamePositional = a;
|
|
97
|
+
}
|
|
98
|
+
return { projectNamePositional, template, help, useDefaults };
|
|
99
|
+
}
|
|
100
|
+
/** Safe folder name for ./name (no path segments). */
|
|
101
|
+
function normalizeProjectName(raw, fallback) {
|
|
102
|
+
const t = raw.trim().replace(/[/\\]+/g, '').replace(/^\.+/, '');
|
|
103
|
+
if (!t || t.includes('..'))
|
|
104
|
+
return fallback;
|
|
105
|
+
const safe = t.replace(/[^a-zA-Z0-9._-]/g, '-').replace(/^-+|-+$/g, '');
|
|
106
|
+
return safe.length > 0 ? safe.slice(0, 214) : fallback;
|
|
107
|
+
}
|
|
108
|
+
async function runInteractiveWizard(opts) {
|
|
109
|
+
const rl = createInterface({ input, output });
|
|
110
|
+
try {
|
|
111
|
+
const defaultName = normalizeProjectName(opts.nameHint ?? '', 'my-nexus-app');
|
|
112
|
+
console.log(`\n ${BOLD}◇ Configure your Nexus project${RESET}\n`);
|
|
113
|
+
const nameAns = await rl.question(` ${CYAN}?${RESET} Project directory ${DIM}./${defaultName}${RESET} ${DIM}(press Enter for default)${RESET}\n` +
|
|
114
|
+
` ${DIM}│${RESET}\n ${DIM}└${RESET} `);
|
|
115
|
+
const projectName = normalizeProjectName(nameAns.length > 0 ? nameAns : defaultName, defaultName);
|
|
116
|
+
let template;
|
|
117
|
+
if (opts.templateFromFlag) {
|
|
118
|
+
template = opts.templateFromFlag;
|
|
119
|
+
console.log(`\n ${DIM}Starter:${RESET} ${BOLD}${template}${RESET} ${DIM}(--template)${RESET}`);
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
console.log(`\n ${BOLD}?${RESET} Which starter do you want?\n`);
|
|
123
|
+
console.log(` ${DIM}❯${RESET} ${DIM}1${RESET} ${TEMPLATE_HINT.minimal}`);
|
|
124
|
+
console.log(` ${DIM}2${RESET} ${TEMPLATE_HINT.full}\n`);
|
|
125
|
+
console.log(` ${DIM}${TEMPLATE_DETAIL.minimal}${RESET}`);
|
|
126
|
+
console.log(` ${DIM}${TEMPLATE_DETAIL.full}${RESET}\n`);
|
|
127
|
+
const tAns = (await rl.question(` ${CYAN}?${RESET} Pick ${DIM}[1-2, default 2]${RESET} `)).trim().toLowerCase();
|
|
128
|
+
if (tAns === '1' || tAns === 'minimal')
|
|
129
|
+
template = 'minimal';
|
|
130
|
+
else
|
|
131
|
+
template = 'full';
|
|
132
|
+
}
|
|
133
|
+
console.log(`\n ${BOLD}◇ Summary${RESET}\n`);
|
|
134
|
+
console.log(` ${DIM}·${RESET} ${DIM}Location${RESET} ${BOLD}./${projectName}${RESET}`);
|
|
135
|
+
console.log(` ${DIM}·${RESET} ${DIM}Starter${RESET} ${BOLD}${template}${RESET} — ${TEMPLATE_DETAIL[template]}\n`);
|
|
136
|
+
const confirm = (await rl.question(` ${CYAN}?${RESET} Create project? ${DIM}[Y/n]${RESET} `)).trim().toLowerCase();
|
|
137
|
+
if (confirm === 'n' || confirm === 'no') {
|
|
138
|
+
console.log(`\n ${DIM}Aborted.${RESET}\n`);
|
|
139
|
+
process.exit(0);
|
|
140
|
+
}
|
|
141
|
+
return { projectName, template };
|
|
142
|
+
}
|
|
143
|
+
finally {
|
|
144
|
+
rl.close();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
30
147
|
async function main() {
|
|
148
|
+
const { projectNamePositional, template: templateFlag, help, useDefaults } = parseCreateArgs(process.argv);
|
|
149
|
+
if (help) {
|
|
150
|
+
console.log(BANNER);
|
|
151
|
+
console.log(` ${BOLD}Usage:${RESET} create-nexus [directory] [options]\n`);
|
|
152
|
+
console.log(` ${BOLD}Options:${RESET}`);
|
|
153
|
+
console.log(` ${DIM}--template, -t${RESET} ${DIM}minimal${RESET} | ${DIM}full${RESET} ${DIM}(starter; skips template step in the wizard)${RESET}`);
|
|
154
|
+
console.log(` ${DIM}--yes, -y${RESET} ${DIM}Skip prompts${RESET} — use defaults (full starter, name from argv or ${DIM}my-nexus-app${RESET})`);
|
|
155
|
+
console.log(` ${DIM}--defaults${RESET} ${DIM}Same as --yes${RESET}`);
|
|
156
|
+
console.log(` ${DIM}--help, -h${RESET} ${DIM}Show this help${RESET}\n`);
|
|
157
|
+
console.log(` ${BOLD}Interactive mode${RESET} ${DIM}(terminal):${RESET} asks for directory name, starter, and confirmation.`);
|
|
158
|
+
console.log(` ${BOLD}Non-interactive${RESET} ${DIM}(CI, pipe):${RESET} uses ${DIM}--yes${RESET} or defaults ${DIM}(full, my-nexus-app)${RESET}.\n`);
|
|
159
|
+
console.log(` ${BOLD}Templates:${RESET}`);
|
|
160
|
+
console.log(` ${DIM}minimal${RESET} One ${CYAN}+page.nx${RESET} + simple layout, no i18n, no example routes.`);
|
|
161
|
+
console.log(` ${DIM}full${RESET} i18n, islands presentation, blog — same as the reference app.\n`);
|
|
162
|
+
process.exit(0);
|
|
163
|
+
}
|
|
31
164
|
console.log(BANNER);
|
|
32
|
-
|
|
165
|
+
let projectName;
|
|
166
|
+
let template;
|
|
167
|
+
if (useDefaults) {
|
|
168
|
+
projectName = normalizeProjectName(projectNamePositional ?? '', 'my-nexus-app');
|
|
169
|
+
template = templateFlag ?? 'full';
|
|
170
|
+
}
|
|
171
|
+
else if (input.isTTY) {
|
|
172
|
+
const w = await runInteractiveWizard({
|
|
173
|
+
nameHint: projectNamePositional,
|
|
174
|
+
templateFromFlag: templateFlag,
|
|
175
|
+
});
|
|
176
|
+
projectName = w.projectName;
|
|
177
|
+
template = w.template;
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
projectName = normalizeProjectName(projectNamePositional ?? '', 'my-nexus-app');
|
|
181
|
+
template = templateFlag ?? 'full';
|
|
182
|
+
}
|
|
33
183
|
const targetDir = resolve(process.cwd(), projectName);
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
184
|
+
if (existsSync(targetDir)) {
|
|
185
|
+
console.error(`\n ${RED}✖${RESET} Directory already exists: ${BOLD}${projectName}${RESET}\n` +
|
|
186
|
+
` ${DIM}Choose another name, remove the folder, or run from a different directory.${RESET}\n`);
|
|
187
|
+
process.exit(1);
|
|
188
|
+
}
|
|
189
|
+
console.log(`\n Creating ${BOLD}${projectName}${RESET} ${DIM}(${template})${RESET}\n`);
|
|
190
|
+
const dirs = template === 'full'
|
|
191
|
+
? [
|
|
192
|
+
'src/routes',
|
|
193
|
+
'src/routes/islands',
|
|
194
|
+
'src/routes/blog/[slug]',
|
|
195
|
+
'src/components',
|
|
196
|
+
'src/islands',
|
|
197
|
+
'src/lib',
|
|
198
|
+
'public',
|
|
199
|
+
'scripts',
|
|
200
|
+
]
|
|
201
|
+
: ['src/routes', 'src/components', 'src/islands', 'src/lib', 'public', 'scripts'];
|
|
45
202
|
for (const dir of dirs) {
|
|
46
203
|
await mkdir(join(targetDir, dir), { recursive: true });
|
|
47
204
|
}
|
|
48
|
-
|
|
49
|
-
await writeProjectFiles(targetDir, projectName);
|
|
205
|
+
await writeProjectFiles(targetDir, projectName, template);
|
|
50
206
|
console.log(` ${GREEN}✓${RESET} Project created at ${BOLD}${projectName}/${RESET}\n`);
|
|
51
207
|
console.log(` Next steps:\n`);
|
|
52
|
-
console.log(` ${DIM}cd${RESET} ${projectName}`);
|
|
53
|
-
console.log(` ${DIM}npm install${RESET} ${DIM}(or pnpm
|
|
54
|
-
console.log(` ${DIM}npm run dev${RESET} ${DIM}(or pnpm dev)${RESET}\n`);
|
|
208
|
+
console.log(` ${DIM}1.${RESET} ${DIM}cd${RESET} ${projectName}`);
|
|
209
|
+
console.log(` ${DIM}2.${RESET} ${DIM}npm install${RESET} ${DIM}(required — or pnpm / yarn / bun install)${RESET}`);
|
|
210
|
+
console.log(` ${DIM}3.${RESET} ${DIM}npm run dev${RESET} ${DIM}(or pnpm dev, yarn dev, bun run dev)${RESET}\n`);
|
|
55
211
|
console.log(` ${CYAN}◆${RESET} Docs: ${BOLD}https://nexusjs.dev${RESET}\n`);
|
|
56
212
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
213
|
+
function buildMinimalScaffoldFiles(name, range, nexusCli, ensureDeps) {
|
|
214
|
+
return {
|
|
215
|
+
'scripts/check-node-modules.mjs': `import { access } from 'node:fs/promises';
|
|
216
|
+
import { join } from 'node:path';
|
|
217
|
+
|
|
218
|
+
const marker = join(process.cwd(), 'node_modules/@nexus_js/cli/package.json');
|
|
219
|
+
try {
|
|
220
|
+
await access(marker);
|
|
221
|
+
} catch {
|
|
222
|
+
console.error(
|
|
223
|
+
'\\n Dependencies are missing. Run npm install (or pnpm / yarn / bun install), then try again.\\n',
|
|
224
|
+
);
|
|
225
|
+
process.exit(1);
|
|
226
|
+
}
|
|
227
|
+
`,
|
|
61
228
|
'package.json': JSON.stringify({
|
|
62
229
|
name,
|
|
63
230
|
version: '0.1.0',
|
|
64
231
|
private: true,
|
|
65
232
|
type: 'module',
|
|
66
233
|
scripts: {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
234
|
+
predev: ensureDeps,
|
|
235
|
+
dev: `${nexusCli} dev`,
|
|
236
|
+
prebuild: ensureDeps,
|
|
237
|
+
build: `${nexusCli} build`,
|
|
238
|
+
prestart: ensureDeps,
|
|
239
|
+
start: `${nexusCli} start`,
|
|
240
|
+
precheck: ensureDeps,
|
|
241
|
+
check: `${nexusCli} check`,
|
|
71
242
|
},
|
|
72
243
|
dependencies: {
|
|
73
244
|
'@nexus_js/runtime': range,
|
|
@@ -93,6 +264,581 @@ async function writeProjectFiles(dir, name) {
|
|
|
93
264
|
'nexus.config.ts': `import type { NexusConfig } from '@nexus_js/cli';
|
|
94
265
|
|
|
95
266
|
export default {
|
|
267
|
+
defaultHydration: 'client:visible',
|
|
268
|
+
|
|
269
|
+
images: {
|
|
270
|
+
formats: ['avif', 'webp'],
|
|
271
|
+
sizes: [640, 1280, 1920],
|
|
272
|
+
},
|
|
273
|
+
|
|
274
|
+
server: {
|
|
275
|
+
port: 3000,
|
|
276
|
+
},
|
|
277
|
+
|
|
278
|
+
build: {
|
|
279
|
+
outDir: '.nexus/output',
|
|
280
|
+
sourcemap: false,
|
|
281
|
+
},
|
|
282
|
+
} satisfies NexusConfig;
|
|
283
|
+
`,
|
|
284
|
+
'src/routes/+layout.nx': `---
|
|
285
|
+
const appName = "My Nexus App";
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
<html lang="en">
|
|
289
|
+
<head>
|
|
290
|
+
<meta charset="UTF-8">
|
|
291
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
292
|
+
<meta name="description" content="Built with Nexus — add your own copy in +layout.nx.">
|
|
293
|
+
<title>{appName}</title>
|
|
294
|
+
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
|
295
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
296
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
297
|
+
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,400..700;1,9..40,400&family=Outfit:wght@500;600;700&display=swap" rel="stylesheet">
|
|
298
|
+
</head>
|
|
299
|
+
<body class="nx-body">
|
|
300
|
+
<div class="nx-bg" aria-hidden="true"></div>
|
|
301
|
+
<header class="nx-header">
|
|
302
|
+
<a class="nx-brand" href="/">
|
|
303
|
+
<span class="nx-brand-mark" aria-hidden="true">◆</span>
|
|
304
|
+
<span>{appName}</span>
|
|
305
|
+
</a>
|
|
306
|
+
<nav class="nx-nav" aria-label="Main">
|
|
307
|
+
<a href="https://nexusjs.dev" target="_blank" rel="noopener noreferrer">Docs</a>
|
|
308
|
+
</nav>
|
|
309
|
+
</header>
|
|
310
|
+
<main class="nx-main">
|
|
311
|
+
<!--nexus:slot-->
|
|
312
|
+
</main>
|
|
313
|
+
<footer class="nx-footer">
|
|
314
|
+
<p>Built with <a href="https://nexusjs.dev" target="_blank" rel="noopener noreferrer">Nexus</a></p>
|
|
315
|
+
</footer>
|
|
316
|
+
</body>
|
|
317
|
+
</html>
|
|
318
|
+
|
|
319
|
+
<style>
|
|
320
|
+
:global(:root) {
|
|
321
|
+
--nx-bg0: #07080c;
|
|
322
|
+
--nx-surface: rgba(255, 255, 255, 0.04);
|
|
323
|
+
--nx-border: rgba(255, 255, 255, 0.08);
|
|
324
|
+
--nx-text: #f1f3f7;
|
|
325
|
+
--nx-muted: #8b93a7;
|
|
326
|
+
--nx-accent: #8b7cf8;
|
|
327
|
+
--nx-radius: 14px;
|
|
328
|
+
--nx-font: "DM Sans", system-ui, -apple-system, sans-serif;
|
|
329
|
+
--nx-display: "Outfit", var(--nx-font);
|
|
330
|
+
}
|
|
331
|
+
:global(.nx-body) {
|
|
332
|
+
margin: 0;
|
|
333
|
+
min-height: 100vh;
|
|
334
|
+
font-family: var(--nx-font);
|
|
335
|
+
color: var(--nx-text);
|
|
336
|
+
background: var(--nx-bg0);
|
|
337
|
+
line-height: 1.5;
|
|
338
|
+
-webkit-font-smoothing: antialiased;
|
|
339
|
+
}
|
|
340
|
+
:global(.nx-bg) {
|
|
341
|
+
position: fixed;
|
|
342
|
+
inset: 0;
|
|
343
|
+
z-index: -1;
|
|
344
|
+
background:
|
|
345
|
+
radial-gradient(ellipse 80% 50% at 50% -20%, rgba(139, 124, 248, 0.22), transparent),
|
|
346
|
+
var(--nx-bg0);
|
|
347
|
+
}
|
|
348
|
+
:global(.nx-header) {
|
|
349
|
+
display: flex;
|
|
350
|
+
align-items: center;
|
|
351
|
+
justify-content: space-between;
|
|
352
|
+
gap: 1rem;
|
|
353
|
+
padding: 1rem 1.5rem;
|
|
354
|
+
max-width: 1100px;
|
|
355
|
+
margin: 0 auto;
|
|
356
|
+
border-bottom: 1px solid var(--nx-border);
|
|
357
|
+
backdrop-filter: blur(12px);
|
|
358
|
+
background: rgba(7, 8, 12, 0.75);
|
|
359
|
+
position: sticky;
|
|
360
|
+
top: 0;
|
|
361
|
+
z-index: 10;
|
|
362
|
+
}
|
|
363
|
+
:global(.nx-brand) {
|
|
364
|
+
display: inline-flex;
|
|
365
|
+
align-items: center;
|
|
366
|
+
gap: 0.5rem;
|
|
367
|
+
font-family: var(--nx-display);
|
|
368
|
+
font-weight: 700;
|
|
369
|
+
font-size: 1.1rem;
|
|
370
|
+
color: var(--nx-text);
|
|
371
|
+
text-decoration: none;
|
|
372
|
+
}
|
|
373
|
+
:global(.nx-brand-mark) { color: var(--nx-accent); font-size: 1.25rem; line-height: 1; }
|
|
374
|
+
:global(.nx-nav a) {
|
|
375
|
+
color: var(--nx-muted);
|
|
376
|
+
text-decoration: none;
|
|
377
|
+
font-size: 0.925rem;
|
|
378
|
+
font-weight: 500;
|
|
379
|
+
}
|
|
380
|
+
:global(.nx-nav a:hover) { color: var(--nx-text); }
|
|
381
|
+
:global(.nx-main) {
|
|
382
|
+
max-width: 1100px;
|
|
383
|
+
margin: 0 auto;
|
|
384
|
+
padding: 2rem 1.5rem 4rem;
|
|
385
|
+
}
|
|
386
|
+
:global(.nx-footer) {
|
|
387
|
+
max-width: 1100px;
|
|
388
|
+
margin: 0 auto;
|
|
389
|
+
padding: 2rem 1.5rem 3rem;
|
|
390
|
+
border-top: 1px solid var(--nx-border);
|
|
391
|
+
text-align: center;
|
|
392
|
+
}
|
|
393
|
+
:global(.nx-footer p) {
|
|
394
|
+
margin: 0;
|
|
395
|
+
font-size: 0.875rem;
|
|
396
|
+
color: var(--nx-muted);
|
|
397
|
+
}
|
|
398
|
+
:global(.nx-footer a) { color: var(--nx-accent); text-decoration: none; }
|
|
399
|
+
:global(.nx-footer a:hover) { text-decoration: underline; }
|
|
400
|
+
</style>
|
|
401
|
+
`,
|
|
402
|
+
'src/routes/+page.nx': `---
|
|
403
|
+
|
|
404
|
+
<section class="landing">
|
|
405
|
+
<p class="landing-kicker">Nexus</p>
|
|
406
|
+
<h1 class="landing-title">Start here</h1>
|
|
407
|
+
<p class="landing-lead">
|
|
408
|
+
This is the <strong>minimal</strong> template: one presentation page, no i18n, no example blog or islands route.
|
|
409
|
+
Edit <code class="landing-code">src/routes/+page.nx</code> and add routes under <code class="landing-code">src/routes/</code>.
|
|
410
|
+
</p>
|
|
411
|
+
<p class="landing-hint">
|
|
412
|
+
Want i18n, demos, and blog stubs? Create a new project with <code class="landing-code">--template full</code>.
|
|
413
|
+
</p>
|
|
414
|
+
<a class="landing-btn" href="https://nexusjs.dev" target="_blank" rel="noopener noreferrer">Documentation</a>
|
|
415
|
+
</section>
|
|
416
|
+
|
|
417
|
+
<style>
|
|
418
|
+
.landing {
|
|
419
|
+
max-width: 36rem;
|
|
420
|
+
padding: 2rem 0 3rem;
|
|
421
|
+
}
|
|
422
|
+
.landing-kicker {
|
|
423
|
+
margin: 0 0 0.75rem;
|
|
424
|
+
font-size: 0.75rem;
|
|
425
|
+
font-weight: 600;
|
|
426
|
+
letter-spacing: 0.14em;
|
|
427
|
+
text-transform: uppercase;
|
|
428
|
+
color: var(--nx-accent);
|
|
429
|
+
}
|
|
430
|
+
.landing-title {
|
|
431
|
+
margin: 0 0 1rem;
|
|
432
|
+
font-family: var(--nx-display);
|
|
433
|
+
font-size: clamp(1.75rem, 4vw, 2.5rem);
|
|
434
|
+
font-weight: 700;
|
|
435
|
+
letter-spacing: -0.02em;
|
|
436
|
+
}
|
|
437
|
+
.landing-lead {
|
|
438
|
+
margin: 0 0 1rem;
|
|
439
|
+
color: var(--nx-muted);
|
|
440
|
+
line-height: 1.65;
|
|
441
|
+
font-size: 1.05rem;
|
|
442
|
+
}
|
|
443
|
+
.landing-hint {
|
|
444
|
+
margin: 0 0 1.75rem;
|
|
445
|
+
font-size: 0.9rem;
|
|
446
|
+
color: var(--nx-muted);
|
|
447
|
+
line-height: 1.55;
|
|
448
|
+
}
|
|
449
|
+
.landing-code {
|
|
450
|
+
font-family: ui-monospace, monospace;
|
|
451
|
+
font-size: 0.88em;
|
|
452
|
+
padding: 0.1em 0.35em;
|
|
453
|
+
border-radius: 6px;
|
|
454
|
+
background: var(--nx-surface);
|
|
455
|
+
border: 1px solid var(--nx-border);
|
|
456
|
+
color: var(--nx-accent);
|
|
457
|
+
}
|
|
458
|
+
.landing-btn {
|
|
459
|
+
display: inline-flex;
|
|
460
|
+
align-items: center;
|
|
461
|
+
padding: 0.65rem 1.25rem;
|
|
462
|
+
border-radius: 10px;
|
|
463
|
+
font-size: 0.9375rem;
|
|
464
|
+
font-weight: 600;
|
|
465
|
+
text-decoration: none;
|
|
466
|
+
background: linear-gradient(135deg, var(--nx-accent), #6366f1);
|
|
467
|
+
color: #fff;
|
|
468
|
+
box-shadow: 0 4px 24px rgba(139, 124, 248, 0.15);
|
|
469
|
+
}
|
|
470
|
+
.landing-btn:hover { filter: brightness(1.06); }
|
|
471
|
+
</style>
|
|
472
|
+
`,
|
|
473
|
+
'src/lib/db.ts': `// Database client placeholder
|
|
474
|
+
// Replace with your preferred ORM (Prisma, Drizzle, etc.)
|
|
475
|
+
|
|
476
|
+
export const db = {
|
|
477
|
+
user: {
|
|
478
|
+
async findFirst() {
|
|
479
|
+
return { id: 1, name: 'Demo User', email: 'demo@nexusjs.dev' };
|
|
480
|
+
},
|
|
481
|
+
async findMany() {
|
|
482
|
+
return [{ id: 1, name: 'Demo User', email: 'demo@nexusjs.dev' }];
|
|
483
|
+
},
|
|
484
|
+
async update(args: { where?: unknown; data: unknown }) {
|
|
485
|
+
return { ...(args.data as object) };
|
|
486
|
+
},
|
|
487
|
+
async create(args: { data: unknown }) {
|
|
488
|
+
return args.data;
|
|
489
|
+
},
|
|
490
|
+
},
|
|
491
|
+
};
|
|
492
|
+
`,
|
|
493
|
+
'public/favicon.svg': NEXUS_LOGO_SVG,
|
|
494
|
+
'.gitignore': `node_modules/
|
|
495
|
+
.nexus/
|
|
496
|
+
dist/
|
|
497
|
+
*.js.map
|
|
498
|
+
`,
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
function buildFullScaffoldFiles(name, range, nexusCli, ensureDeps) {
|
|
502
|
+
return {
|
|
503
|
+
'scripts/check-node-modules.mjs': `import { access } from 'node:fs/promises';
|
|
504
|
+
import { join } from 'node:path';
|
|
505
|
+
|
|
506
|
+
const marker = join(process.cwd(), 'node_modules/@nexus_js/cli/package.json');
|
|
507
|
+
try {
|
|
508
|
+
await access(marker);
|
|
509
|
+
} catch {
|
|
510
|
+
console.error(
|
|
511
|
+
'\\n Dependencies are missing. Run npm install (or pnpm / yarn / bun install), then try again.\\n',
|
|
512
|
+
);
|
|
513
|
+
process.exit(1);
|
|
514
|
+
}
|
|
515
|
+
`,
|
|
516
|
+
'package.json': JSON.stringify({
|
|
517
|
+
name,
|
|
518
|
+
version: '0.1.0',
|
|
519
|
+
private: true,
|
|
520
|
+
type: 'module',
|
|
521
|
+
scripts: {
|
|
522
|
+
predev: ensureDeps,
|
|
523
|
+
dev: `${nexusCli} dev`,
|
|
524
|
+
prebuild: ensureDeps,
|
|
525
|
+
build: `${nexusCli} build`,
|
|
526
|
+
prestart: ensureDeps,
|
|
527
|
+
start: `${nexusCli} start`,
|
|
528
|
+
precheck: ensureDeps,
|
|
529
|
+
check: `${nexusCli} check`,
|
|
530
|
+
},
|
|
531
|
+
dependencies: {
|
|
532
|
+
'@nexus_js/runtime': range,
|
|
533
|
+
},
|
|
534
|
+
devDependencies: {
|
|
535
|
+
'@nexus_js/cli': range,
|
|
536
|
+
'@nexus_js/compiler': range,
|
|
537
|
+
typescript: '^5.5.0',
|
|
538
|
+
},
|
|
539
|
+
}, null, 2),
|
|
540
|
+
'tsconfig.json': JSON.stringify({
|
|
541
|
+
compilerOptions: {
|
|
542
|
+
target: 'ES2022',
|
|
543
|
+
module: 'NodeNext',
|
|
544
|
+
moduleResolution: 'NodeNext',
|
|
545
|
+
strict: true,
|
|
546
|
+
skipLibCheck: true,
|
|
547
|
+
noEmit: true,
|
|
548
|
+
paths: { '$lib/*': ['./src/lib/*'] },
|
|
549
|
+
},
|
|
550
|
+
include: ['nexus.config.ts', 'src/**/*.ts'],
|
|
551
|
+
}, null, 2),
|
|
552
|
+
'src/lib/i18n.ts': `/**
|
|
553
|
+
* i18n — aligned with nexus.config.ts \`i18n.locales\`.
|
|
554
|
+
* Resolve locale per request: ?lang= → cookie nx-lang → Accept-Language → default.
|
|
555
|
+
*/
|
|
556
|
+
|
|
557
|
+
export type Locale = 'en' | 'es' | 'pt';
|
|
558
|
+
|
|
559
|
+
export const LOCALES: Locale[] = ['en', 'es', 'pt'];
|
|
560
|
+
export const DEFAULT_LOCALE: Locale = 'en';
|
|
561
|
+
|
|
562
|
+
type CtxLike = {
|
|
563
|
+
url: URL;
|
|
564
|
+
getCookie: (name: string) => string | undefined;
|
|
565
|
+
request: Request;
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
function isLocale(s: string | undefined | null): s is Locale {
|
|
569
|
+
return s === 'en' || s === 'es' || s === 'pt';
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/** Active locale for this request (use in templates: getLocale(ctx)). */
|
|
573
|
+
export function getLocale(ctx: CtxLike): Locale {
|
|
574
|
+
const q = ctx.url.searchParams.get('lang') ?? ctx.url.searchParams.get('locale');
|
|
575
|
+
if (isLocale(q)) return q;
|
|
576
|
+
const ck = ctx.getCookie('nx-lang');
|
|
577
|
+
if (isLocale(ck)) return ck;
|
|
578
|
+
const al = ctx.request.headers.get('accept-language');
|
|
579
|
+
if (al) {
|
|
580
|
+
const first = al.split(',')[0]?.trim().split('-')[0]?.toLowerCase();
|
|
581
|
+
if (first === 'es' || first === 'pt') return first;
|
|
582
|
+
}
|
|
583
|
+
return DEFAULT_LOCALE;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/** Same path + query, with \`lang\` set (preserves other search params). */
|
|
587
|
+
export function langHref(ctx: CtxLike, locale: Locale): string {
|
|
588
|
+
const u = new URL(ctx.url.href);
|
|
589
|
+
u.searchParams.set('lang', locale);
|
|
590
|
+
return u.pathname + u.search;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/** Internal link with current locale in \`lang\` (e.g. \`/islands?lang=es\`). */
|
|
594
|
+
export function pathWithLang(ctx: CtxLike, pathname: string): string {
|
|
595
|
+
const u = new URL(ctx.url.href);
|
|
596
|
+
u.pathname = pathname;
|
|
597
|
+
u.searchParams.set('lang', getLocale(ctx));
|
|
598
|
+
return u.pathname + u.search;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
export function layoutCopy(locale: Locale) {
|
|
602
|
+
const t = {
|
|
603
|
+
en: {
|
|
604
|
+
appName: 'My Nexus App',
|
|
605
|
+
metaDescription:
|
|
606
|
+
'Built with Nexus — full-stack framework with islands, Svelte 5 runes, and server actions.',
|
|
607
|
+
navHome: 'Home',
|
|
608
|
+
navIslands: 'Islands',
|
|
609
|
+
navBlog: 'Blog',
|
|
610
|
+
navDocs: 'Docs',
|
|
611
|
+
navAria: 'Main',
|
|
612
|
+
langAria: 'Language',
|
|
613
|
+
footerTagline: 'Islands · Runes · Server Actions',
|
|
614
|
+
footerMade: 'Built with',
|
|
615
|
+
},
|
|
616
|
+
es: {
|
|
617
|
+
appName: 'Mi app Nexus',
|
|
618
|
+
metaDescription:
|
|
619
|
+
'Hecho con Nexus — framework full stack con islas, runes de Svelte 5 y server actions.',
|
|
620
|
+
navHome: 'Inicio',
|
|
621
|
+
navIslands: 'Islas',
|
|
622
|
+
navBlog: 'Blog',
|
|
623
|
+
navDocs: 'Docs',
|
|
624
|
+
navAria: 'Principal',
|
|
625
|
+
langAria: 'Idioma',
|
|
626
|
+
footerTagline: 'Islas · Runes · Server Actions',
|
|
627
|
+
footerMade: 'Hecho con',
|
|
628
|
+
},
|
|
629
|
+
pt: {
|
|
630
|
+
appName: 'Meu app Nexus',
|
|
631
|
+
metaDescription:
|
|
632
|
+
'Feito com Nexus — framework full stack com ilhas, runes Svelte 5 e server actions.',
|
|
633
|
+
navHome: 'Início',
|
|
634
|
+
navIslands: 'Ilhas',
|
|
635
|
+
navBlog: 'Blog',
|
|
636
|
+
navDocs: 'Docs',
|
|
637
|
+
navAria: 'Principal',
|
|
638
|
+
langAria: 'Idioma',
|
|
639
|
+
footerTagline: 'Ilhas · Runes · Server Actions',
|
|
640
|
+
footerMade: 'Feito com',
|
|
641
|
+
},
|
|
642
|
+
};
|
|
643
|
+
return t[locale];
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
export function langActiveClass(ctx: CtxLike, locale: Locale): string {
|
|
647
|
+
return getLocale(ctx) === locale ? 'nx-lang-btn--on' : '';
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
export function homeCopy(locale: Locale) {
|
|
651
|
+
const t = {
|
|
652
|
+
en: {
|
|
653
|
+
kicker: "You're running Nexus",
|
|
654
|
+
greeting: 'Ship less JavaScript.',
|
|
655
|
+
sub: 'Nexus combines islands architecture, Svelte 5 runes, and server actions — so your default page weight stays tiny.',
|
|
656
|
+
ctaDocs: 'Documentation',
|
|
657
|
+
ctaIslands: 'Islands guide',
|
|
658
|
+
ctaBlog: 'Example blog',
|
|
659
|
+
featTitle: 'Why Nexus',
|
|
660
|
+
features: [
|
|
661
|
+
{ icon: '◇', title: 'Islands', desc: 'HTML-first pages; JS only where you opt in with client:visible and friends.' },
|
|
662
|
+
{ icon: '⚡', title: 'Runes', desc: 'Fine-grained reactivity with Svelte 5 — no legacy stores required.' },
|
|
663
|
+
{ icon: '↯', title: 'Server actions', desc: 'Mutations colocated with routes; type-safe, SSR-friendly.' },
|
|
664
|
+
],
|
|
665
|
+
demoTitle: 'Interactive island',
|
|
666
|
+
demoHint:
|
|
667
|
+
'The counter below is a small client island — the rest of this page can stay server-rendered.',
|
|
668
|
+
demoLabel: 'Hydrated in the browser',
|
|
669
|
+
counterAria: 'Increment counter',
|
|
670
|
+
},
|
|
671
|
+
es: {
|
|
672
|
+
kicker: 'Estás ejecutando Nexus',
|
|
673
|
+
greeting: 'Envía menos JavaScript.',
|
|
674
|
+
sub: 'Nexus combina arquitectura de islas, runes de Svelte 5 y server actions — el peso por defecto de la página se mantiene bajo.',
|
|
675
|
+
ctaDocs: 'Documentación',
|
|
676
|
+
ctaIslands: 'Guía de islas',
|
|
677
|
+
ctaBlog: 'Blog de ejemplo',
|
|
678
|
+
featTitle: 'Por qué Nexus',
|
|
679
|
+
features: [
|
|
680
|
+
{ icon: '◇', title: 'Islas', desc: 'Páginas HTML primero; JS solo donde eliges con client:visible y similares.' },
|
|
681
|
+
{ icon: '⚡', title: 'Runes', desc: 'Reactividad fina con Svelte 5 — sin stores legacy.' },
|
|
682
|
+
{ icon: '↯', title: 'Server actions', desc: 'Mutaciones junto a las rutas; tipadas y amigables con SSR.' },
|
|
683
|
+
],
|
|
684
|
+
demoTitle: 'Isla interactiva',
|
|
685
|
+
demoHint:
|
|
686
|
+
'El contador es una isla pequeña — el resto de la página puede seguir renderizado en el servidor.',
|
|
687
|
+
demoLabel: 'Hidratado en el navegador',
|
|
688
|
+
counterAria: 'Incrementar contador',
|
|
689
|
+
},
|
|
690
|
+
pt: {
|
|
691
|
+
kicker: 'Você está rodando Nexus',
|
|
692
|
+
greeting: 'Envie menos JavaScript.',
|
|
693
|
+
sub: 'Nexus combina ilhas, runes Svelte 5 e server actions — o peso padrão da página permanece baixo.',
|
|
694
|
+
ctaDocs: 'Documentação',
|
|
695
|
+
ctaIslands: 'Guia de ilhas',
|
|
696
|
+
ctaBlog: 'Blog de exemplo',
|
|
697
|
+
featTitle: 'Por que Nexus',
|
|
698
|
+
features: [
|
|
699
|
+
{ icon: '◇', title: 'Ilhas', desc: 'HTML primeiro; JS só onde você marca com client:visible e afins.' },
|
|
700
|
+
{ icon: '⚡', title: 'Runes', desc: 'Reatividade fina com Svelte 5 — sem stores legados.' },
|
|
701
|
+
{ icon: '↯', title: 'Server actions', desc: 'Mutações junto às rotas; tipadas e SSR-friendly.' },
|
|
702
|
+
],
|
|
703
|
+
demoTitle: 'Ilha interativa',
|
|
704
|
+
demoHint: 'O contador abaixo é uma ilha pequena — o restante pode ficar no servidor.',
|
|
705
|
+
demoLabel: 'Hidratado no navegador',
|
|
706
|
+
counterAria: 'Incrementar contador',
|
|
707
|
+
},
|
|
708
|
+
};
|
|
709
|
+
return t[locale];
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
export function islandsCopy(locale: Locale) {
|
|
713
|
+
const t = {
|
|
714
|
+
en: {
|
|
715
|
+
pageTitle: 'Islands & components',
|
|
716
|
+
lead: 'How Nexus sends HTML first and adds JavaScript only where you mark it with client:*.',
|
|
717
|
+
presKicker: 'Mini brief',
|
|
718
|
+
s1h: '1 · What is an island?',
|
|
719
|
+
s1p:
|
|
720
|
+
'The server paints the full page in HTML. Only the block wrapped with a directive like client:load or client:visible downloads a small bundle that the browser hydrates (runes, clicks, state).',
|
|
721
|
+
diagramAria: 'Flow: server sends HTML; only the island hydrates',
|
|
722
|
+
flowSrv: 'SSR',
|
|
723
|
+
flowHtml: 'HTML + marked island',
|
|
724
|
+
flowJs: 'Island JS',
|
|
725
|
+
flowOk: 'Interactivity',
|
|
726
|
+
s2h: '2 · Hydration directives',
|
|
727
|
+
thDirective: 'Directive',
|
|
728
|
+
thWhen: 'When',
|
|
729
|
+
thUse: 'Typical use',
|
|
730
|
+
r1: ['client:load', 'On page load', 'Critical UI — nav, modal'],
|
|
731
|
+
r2: ['client:idle', 'When the browser is idle', 'Secondary widgets'],
|
|
732
|
+
r3: ['client:visible', 'When entering the viewport', 'Below-the-fold content'],
|
|
733
|
+
r4: ['client:media="…"', 'When the media query matches', 'Mobile-only or desktop-only'],
|
|
734
|
+
r5: ['server:only', 'Never hydrates', 'Heavy tables, admin without JS'],
|
|
735
|
+
s3h: '3 · Structure of a .nx file',
|
|
736
|
+
l1: 'Frontmatter — server only: data, import, await.',
|
|
737
|
+
l2: 'script — runes ($state, $derived, $effect) for the client.',
|
|
738
|
+
l3: 'HTML template — interpolations and client:* on the interactive root.',
|
|
739
|
+
l4: 'style — scoped CSS for this file.',
|
|
740
|
+
s4h: '4 · Componentization',
|
|
741
|
+
s4p:
|
|
742
|
+
'Split reusable pieces into src/components/MyName.nx (same blocks). In a route, import in the frontmatter and use <MyName /> in the template when the compiler resolves it.',
|
|
743
|
+
s4muted: 'Import resolution and preloads follow the compiler conventions.',
|
|
744
|
+
s5h: '5 · Live demo (client:visible)',
|
|
745
|
+
s5p: 'Scroll down to hydrate the island; the button updates reactive state.',
|
|
746
|
+
demoBtnAria: 'Add one',
|
|
747
|
+
refh: 'References',
|
|
748
|
+
refp: 'In the repo: docs/ISLANDS.md · Official site:',
|
|
749
|
+
},
|
|
750
|
+
es: {
|
|
751
|
+
pageTitle: 'Islas y componentes',
|
|
752
|
+
lead: 'Cómo Nexus envía HTML primero y añade JavaScript solo donde lo marcas con client:*.',
|
|
753
|
+
presKicker: 'Mini presentación',
|
|
754
|
+
s1h: '1 · ¿Qué es una isla?',
|
|
755
|
+
s1p:
|
|
756
|
+
'El servidor pinta toda la página en HTML. Solo el bloque con client:load, client:visible, etc. genera un bundle pequeño que el navegador hidrata (runes, clics, estado).',
|
|
757
|
+
diagramAria: 'Flujo: servidor entrega HTML; el navegador hidrata solo la isla',
|
|
758
|
+
flowSrv: 'SSR',
|
|
759
|
+
flowHtml: 'HTML + isla marcada',
|
|
760
|
+
flowJs: 'JS de la isla',
|
|
761
|
+
flowOk: 'Interactividad',
|
|
762
|
+
s2h: '2 · Directivas de hidratación',
|
|
763
|
+
thDirective: 'Directiva',
|
|
764
|
+
thWhen: 'Cuándo',
|
|
765
|
+
thUse: 'Uso típico',
|
|
766
|
+
r1: ['client:load', 'Al cargar la página', 'UI crítica — nav, modal'],
|
|
767
|
+
r2: ['client:idle', 'Cuando el navegador está libre', 'Widgets secundarios'],
|
|
768
|
+
r3: ['client:visible', 'Al entrar en viewport', 'Contenido bajo el fold'],
|
|
769
|
+
r4: ['client:media="…"', 'Si coincide la media query', 'Solo móvil o solo desktop'],
|
|
770
|
+
r5: ['server:only', 'Nunca hidrata', 'Tablas pesadas, admin sin JS'],
|
|
771
|
+
s3h: '3 · Estructura de un archivo .nx',
|
|
772
|
+
l1: 'Frontmatter — solo servidor: datos, import, await.',
|
|
773
|
+
l2: 'script — runes ($state, $derived, $effect) para la parte cliente.',
|
|
774
|
+
l3: 'Plantilla HTML — interpolaciones y client:* en el contenedor interactivo.',
|
|
775
|
+
l4: 'style — CSS con alcance al archivo.',
|
|
776
|
+
s4h: '4 · Componentizar',
|
|
777
|
+
s4p:
|
|
778
|
+
'Separa piezas en src/components/MiNombre.nx (mismo formato). En una ruta, importa en el frontmatter y usa <MiNombre /> en la plantilla cuando el compilador lo resuelva.',
|
|
779
|
+
s4muted: 'La resolución de imports y preloads sigue las convenciones del compilador.',
|
|
780
|
+
s5h: '5 · Demo en vivo (client:visible)',
|
|
781
|
+
s5p: 'Al hacer scroll hasta aquí, la isla se hidrata; el botón incrementa el estado reactivo.',
|
|
782
|
+
demoBtnAria: 'Sumar uno',
|
|
783
|
+
refh: 'Referencias',
|
|
784
|
+
refp: 'En el repo: docs/ISLANDS.md · Sitio oficial:',
|
|
785
|
+
},
|
|
786
|
+
pt: {
|
|
787
|
+
pageTitle: 'Ilhas e componentes',
|
|
788
|
+
lead: 'Como o Nexus envia HTML primeiro e só adiciona JS onde você marca com client:*.',
|
|
789
|
+
presKicker: 'Mini apresentação',
|
|
790
|
+
s1h: '1 · O que é uma ilha?',
|
|
791
|
+
s1p:
|
|
792
|
+
'O servidor renderiza a página inteira em HTML. Só o bloco com client:load, client:visible, etc. baixa um bundle pequeno que o navegador hidrata.',
|
|
793
|
+
diagramAria: 'Fluxo: servidor entrega HTML; só a ilha hidrata',
|
|
794
|
+
flowSrv: 'SSR',
|
|
795
|
+
flowHtml: 'HTML + ilha marcada',
|
|
796
|
+
flowJs: 'JS da ilha',
|
|
797
|
+
flowOk: 'Interatividade',
|
|
798
|
+
s2h: '2 · Diretivas de hidratação',
|
|
799
|
+
thDirective: 'Diretiva',
|
|
800
|
+
thWhen: 'Quando',
|
|
801
|
+
thUse: 'Uso típico',
|
|
802
|
+
r1: ['client:load', 'Ao carregar a página', 'UI crítica — nav, modal'],
|
|
803
|
+
r2: ['client:idle', 'Quando o navegador está ocioso', 'Widgets secundários'],
|
|
804
|
+
r3: ['client:visible', 'Ao entrar na viewport', 'Abaixo da dobra'],
|
|
805
|
+
r4: ['client:media="…"', 'Se a media query casar', 'Só mobile ou só desktop'],
|
|
806
|
+
r5: ['server:only', 'Nunca hidrata', 'Tabelas pesadas, admin sem JS'],
|
|
807
|
+
s3h: '3 · Estrutura de um arquivo .nx',
|
|
808
|
+
l1: 'Frontmatter — só servidor: dados, import, await.',
|
|
809
|
+
l2: 'script — runes ($state, $derived, $effect) para o cliente.',
|
|
810
|
+
l3: 'Template HTML — interpolações e client:* na raiz interativa.',
|
|
811
|
+
l4: 'style — CSS escopado ao arquivo.',
|
|
812
|
+
s4h: '4 · Componentizar',
|
|
813
|
+
s4p:
|
|
814
|
+
'Separe em src/components/Nome.nx. Na rota, importe no frontmatter e use <Nome /> no template quando o compilador resolver.',
|
|
815
|
+
s4muted: 'Imports e preloads seguem as convenções do compilador.',
|
|
816
|
+
s5h: '5 · Demo ao vivo (client:visible)',
|
|
817
|
+
s5p: 'Role até aqui para hidratar a ilha; o botão atualiza o estado.',
|
|
818
|
+
demoBtnAria: 'Mais um',
|
|
819
|
+
refh: 'Referências',
|
|
820
|
+
refp: 'No repositório: docs/ISLANDS.md · Site oficial:',
|
|
821
|
+
},
|
|
822
|
+
};
|
|
823
|
+
return t[locale];
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
export function pageHomeCopy(ctx: CtxLike) {
|
|
827
|
+
return homeCopy(getLocale(ctx));
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
export function pageIslandsCopy(ctx: CtxLike) {
|
|
831
|
+
return islandsCopy(getLocale(ctx));
|
|
832
|
+
}
|
|
833
|
+
`,
|
|
834
|
+
'nexus.config.ts': `import type { NexusConfig } from '@nexus_js/cli';
|
|
835
|
+
|
|
836
|
+
export default {
|
|
837
|
+
i18n: {
|
|
838
|
+
defaultLocale: 'en',
|
|
839
|
+
locales: ['en', 'es', 'pt'],
|
|
840
|
+
},
|
|
841
|
+
|
|
96
842
|
// Islands hydration strategy defaults
|
|
97
843
|
defaultHydration: 'client:visible',
|
|
98
844
|
|
|
@@ -115,123 +861,836 @@ export default {
|
|
|
115
861
|
} satisfies NexusConfig;
|
|
116
862
|
`,
|
|
117
863
|
'src/routes/+layout.nx': `---
|
|
118
|
-
|
|
119
|
-
|
|
864
|
+
import {
|
|
865
|
+
getLocale,
|
|
866
|
+
langHref,
|
|
867
|
+
langActiveClass,
|
|
868
|
+
layoutCopy,
|
|
869
|
+
pathWithLang,
|
|
870
|
+
} from '$lib/i18n';
|
|
120
871
|
---
|
|
121
872
|
|
|
122
|
-
<html lang="
|
|
873
|
+
<html lang="{getLocale(ctx)}">
|
|
123
874
|
<head>
|
|
124
875
|
<meta charset="UTF-8">
|
|
125
876
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
126
|
-
<
|
|
877
|
+
<meta name="description" content="{layoutCopy(getLocale(ctx)).metaDescription}">
|
|
878
|
+
<title>{layoutCopy(getLocale(ctx)).appName}</title>
|
|
879
|
+
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
|
880
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
881
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
882
|
+
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,400..700;1,9..40,400&family=Outfit:wght@500;600;700&display=swap" rel="stylesheet">
|
|
127
883
|
</head>
|
|
128
|
-
<body>
|
|
129
|
-
<
|
|
130
|
-
|
|
131
|
-
<a href="/
|
|
132
|
-
|
|
133
|
-
|
|
884
|
+
<body class="nx-body">
|
|
885
|
+
<div class="nx-bg" aria-hidden="true"></div>
|
|
886
|
+
<header class="nx-header">
|
|
887
|
+
<a class="nx-brand" href="{pathWithLang(ctx, '/')}">
|
|
888
|
+
<span class="nx-brand-mark" aria-hidden="true">◆</span>
|
|
889
|
+
<span>{layoutCopy(getLocale(ctx)).appName}</span>
|
|
890
|
+
</a>
|
|
891
|
+
<div class="nx-header-actions">
|
|
892
|
+
<nav class="nx-nav" aria-label="{layoutCopy(getLocale(ctx)).navAria}">
|
|
893
|
+
<a href="{pathWithLang(ctx, '/')}">{layoutCopy(getLocale(ctx)).navHome}</a>
|
|
894
|
+
<a href="{pathWithLang(ctx, '/islands')}">{layoutCopy(getLocale(ctx)).navIslands}</a>
|
|
895
|
+
<a href="{pathWithLang(ctx, '/blog')}">{layoutCopy(getLocale(ctx)).navBlog}</a>
|
|
896
|
+
<a href="https://nexusjs.dev" target="_blank" rel="noopener noreferrer">{layoutCopy(getLocale(ctx)).navDocs}</a>
|
|
897
|
+
</nav>
|
|
898
|
+
<div class="nx-lang" role="group" aria-label="{layoutCopy(getLocale(ctx)).langAria}">
|
|
899
|
+
<a class="nx-lang-btn {langActiveClass(ctx, 'en')}" href="{langHref(ctx, 'en')}">EN</a>
|
|
900
|
+
<a class="nx-lang-btn {langActiveClass(ctx, 'es')}" href="{langHref(ctx, 'es')}">ES</a>
|
|
901
|
+
<a class="nx-lang-btn {langActiveClass(ctx, 'pt')}" href="{langHref(ctx, 'pt')}">PT</a>
|
|
902
|
+
</div>
|
|
903
|
+
</div>
|
|
904
|
+
</header>
|
|
905
|
+
<main class="nx-main">
|
|
134
906
|
<!--nexus:slot-->
|
|
135
907
|
</main>
|
|
908
|
+
<footer class="nx-footer">
|
|
909
|
+
<p>
|
|
910
|
+
{layoutCopy(getLocale(ctx)).footerMade}
|
|
911
|
+
<a href="https://nexusjs.dev" target="_blank" rel="noopener noreferrer">Nexus</a>
|
|
912
|
+
· {layoutCopy(getLocale(ctx)).footerTagline}
|
|
913
|
+
</p>
|
|
914
|
+
</footer>
|
|
136
915
|
</body>
|
|
137
916
|
</html>
|
|
917
|
+
|
|
918
|
+
<style>
|
|
919
|
+
:global(:root) {
|
|
920
|
+
--nx-bg0: #07080c;
|
|
921
|
+
--nx-bg1: #0e1118;
|
|
922
|
+
--nx-surface: rgba(255, 255, 255, 0.04);
|
|
923
|
+
--nx-border: rgba(255, 255, 255, 0.08);
|
|
924
|
+
--nx-text: #f1f3f7;
|
|
925
|
+
--nx-muted: #8b93a7;
|
|
926
|
+
--nx-accent: #8b7cf8;
|
|
927
|
+
--nx-accent-dim: rgba(139, 124, 248, 0.15);
|
|
928
|
+
--nx-radius: 14px;
|
|
929
|
+
--nx-font: "DM Sans", system-ui, -apple-system, sans-serif;
|
|
930
|
+
--nx-display: "Outfit", var(--nx-font);
|
|
931
|
+
}
|
|
932
|
+
:global(.nx-body) {
|
|
933
|
+
margin: 0;
|
|
934
|
+
min-height: 100vh;
|
|
935
|
+
font-family: var(--nx-font);
|
|
936
|
+
color: var(--nx-text);
|
|
937
|
+
background: var(--nx-bg0);
|
|
938
|
+
line-height: 1.5;
|
|
939
|
+
-webkit-font-smoothing: antialiased;
|
|
940
|
+
}
|
|
941
|
+
:global(.nx-bg) {
|
|
942
|
+
position: fixed;
|
|
943
|
+
inset: 0;
|
|
944
|
+
z-index: -1;
|
|
945
|
+
background:
|
|
946
|
+
radial-gradient(ellipse 80% 50% at 50% -20%, rgba(139, 124, 248, 0.22), transparent),
|
|
947
|
+
radial-gradient(ellipse 60% 40% at 100% 0%, rgba(56, 189, 248, 0.08), transparent),
|
|
948
|
+
var(--nx-bg0);
|
|
949
|
+
}
|
|
950
|
+
:global(.nx-header) {
|
|
951
|
+
display: flex;
|
|
952
|
+
align-items: center;
|
|
953
|
+
justify-content: space-between;
|
|
954
|
+
gap: 1rem;
|
|
955
|
+
padding: 1rem 1.5rem;
|
|
956
|
+
max-width: 1100px;
|
|
957
|
+
margin: 0 auto;
|
|
958
|
+
border-bottom: 1px solid var(--nx-border);
|
|
959
|
+
backdrop-filter: blur(12px);
|
|
960
|
+
background: rgba(7, 8, 12, 0.75);
|
|
961
|
+
position: sticky;
|
|
962
|
+
top: 0;
|
|
963
|
+
z-index: 10;
|
|
964
|
+
}
|
|
965
|
+
:global(.nx-header-actions) {
|
|
966
|
+
display: flex;
|
|
967
|
+
flex-wrap: wrap;
|
|
968
|
+
align-items: center;
|
|
969
|
+
gap: 0.75rem 1.25rem;
|
|
970
|
+
justify-content: flex-end;
|
|
971
|
+
}
|
|
972
|
+
:global(.nx-brand) {
|
|
973
|
+
display: inline-flex;
|
|
974
|
+
align-items: center;
|
|
975
|
+
gap: 0.5rem;
|
|
976
|
+
font-family: var(--nx-display);
|
|
977
|
+
font-weight: 700;
|
|
978
|
+
font-size: 1.1rem;
|
|
979
|
+
color: var(--nx-text);
|
|
980
|
+
text-decoration: none;
|
|
981
|
+
}
|
|
982
|
+
:global(.nx-brand-mark) {
|
|
983
|
+
color: var(--nx-accent);
|
|
984
|
+
font-size: 1.25rem;
|
|
985
|
+
line-height: 1;
|
|
986
|
+
}
|
|
987
|
+
:global(.nx-nav) {
|
|
988
|
+
display: flex;
|
|
989
|
+
flex-wrap: wrap;
|
|
990
|
+
gap: 0.25rem 1.25rem;
|
|
991
|
+
align-items: center;
|
|
992
|
+
}
|
|
993
|
+
:global(.nx-nav a) {
|
|
994
|
+
color: var(--nx-muted);
|
|
995
|
+
text-decoration: none;
|
|
996
|
+
font-size: 0.925rem;
|
|
997
|
+
font-weight: 500;
|
|
998
|
+
transition: color 0.15s ease;
|
|
999
|
+
}
|
|
1000
|
+
:global(.nx-nav a:hover) {
|
|
1001
|
+
color: var(--nx-text);
|
|
1002
|
+
}
|
|
1003
|
+
:global(.nx-lang) {
|
|
1004
|
+
display: inline-flex;
|
|
1005
|
+
gap: 0.2rem;
|
|
1006
|
+
padding: 0.2rem;
|
|
1007
|
+
border-radius: 10px;
|
|
1008
|
+
border: 1px solid var(--nx-border);
|
|
1009
|
+
background: rgba(0, 0, 0, 0.2);
|
|
1010
|
+
}
|
|
1011
|
+
:global(.nx-lang-btn) {
|
|
1012
|
+
padding: 0.25rem 0.5rem;
|
|
1013
|
+
border-radius: 7px;
|
|
1014
|
+
font-size: 0.7rem;
|
|
1015
|
+
font-weight: 700;
|
|
1016
|
+
letter-spacing: 0.04em;
|
|
1017
|
+
color: var(--nx-muted);
|
|
1018
|
+
text-decoration: none;
|
|
1019
|
+
transition: background 0.12s ease, color 0.12s ease;
|
|
1020
|
+
}
|
|
1021
|
+
:global(.nx-lang-btn:hover) {
|
|
1022
|
+
color: var(--nx-text);
|
|
1023
|
+
}
|
|
1024
|
+
:global(.nx-lang-btn--on) {
|
|
1025
|
+
background: rgba(139, 124, 248, 0.25);
|
|
1026
|
+
color: var(--nx-text);
|
|
1027
|
+
}
|
|
1028
|
+
:global(.nx-main) {
|
|
1029
|
+
max-width: 1100px;
|
|
1030
|
+
margin: 0 auto;
|
|
1031
|
+
padding: 2rem 1.5rem 4rem;
|
|
1032
|
+
}
|
|
1033
|
+
:global(.nx-footer) {
|
|
1034
|
+
max-width: 1100px;
|
|
1035
|
+
margin: 0 auto;
|
|
1036
|
+
padding: 2rem 1.5rem 3rem;
|
|
1037
|
+
border-top: 1px solid var(--nx-border);
|
|
1038
|
+
text-align: center;
|
|
1039
|
+
}
|
|
1040
|
+
:global(.nx-footer p) {
|
|
1041
|
+
margin: 0;
|
|
1042
|
+
font-size: 0.875rem;
|
|
1043
|
+
color: var(--nx-muted);
|
|
1044
|
+
}
|
|
1045
|
+
:global(.nx-footer a) {
|
|
1046
|
+
color: var(--nx-accent);
|
|
1047
|
+
text-decoration: none;
|
|
1048
|
+
}
|
|
1049
|
+
:global(.nx-footer a:hover) {
|
|
1050
|
+
text-decoration: underline;
|
|
1051
|
+
}
|
|
1052
|
+
</style>
|
|
138
1053
|
`,
|
|
139
1054
|
'src/routes/+page.nx': `---
|
|
140
|
-
|
|
141
|
-
const greeting = "Welcome to Nexus";
|
|
142
|
-
const features = [
|
|
143
|
-
{ icon: "🏝️", title: "Islands Architecture", desc: "Zero JS by default" },
|
|
144
|
-
{ icon: "⚡", title: "Svelte 5 Runes", desc: "Fine-grained reactivity" },
|
|
145
|
-
{ icon: "🔧", title: "Server Actions", desc: "Type-safe mutations" },
|
|
146
|
-
];
|
|
1055
|
+
import { pageHomeCopy, pathWithLang } from '$lib/i18n';
|
|
147
1056
|
---
|
|
148
1057
|
|
|
149
1058
|
<script>
|
|
150
|
-
// Client island — only this code reaches the browser
|
|
151
1059
|
let count = $state(0);
|
|
152
1060
|
let doubled = $derived(count * 2);
|
|
153
1061
|
</script>
|
|
154
1062
|
|
|
155
1063
|
<section class="hero">
|
|
156
|
-
<
|
|
157
|
-
<
|
|
1064
|
+
<p class="hero-kicker">{pageHomeCopy(ctx).kicker}</p>
|
|
1065
|
+
<h1 class="hero-title">{pageHomeCopy(ctx).greeting}</h1>
|
|
1066
|
+
<p class="hero-lead">{pageHomeCopy(ctx).sub}</p>
|
|
1067
|
+
<div class="hero-actions">
|
|
1068
|
+
<a class="btn btn-primary" href="https://nexusjs.dev" target="_blank" rel="noopener noreferrer">{pageHomeCopy(ctx).ctaDocs}</a>
|
|
1069
|
+
<a class="btn btn-ghost" href="{pathWithLang(ctx, '/islands')}">{pageHomeCopy(ctx).ctaIslands}</a>
|
|
1070
|
+
<a class="btn btn-ghost" href="{pathWithLang(ctx, '/blog')}">{pageHomeCopy(ctx).ctaBlog}</a>
|
|
1071
|
+
</div>
|
|
1072
|
+
</section>
|
|
1073
|
+
|
|
1074
|
+
<section class="features" aria-labelledby="feat-heading">
|
|
1075
|
+
<h2 id="feat-heading" class="section-title">{pageHomeCopy(ctx).featTitle}</h2>
|
|
1076
|
+
<ul class="feature-grid">
|
|
1077
|
+
{#each pageHomeCopy(ctx).features as f}
|
|
1078
|
+
<li class="card">
|
|
1079
|
+
<span class="card-icon" aria-hidden="true">{f.icon}</span>
|
|
1080
|
+
<h3 class="card-title">{f.title}</h3>
|
|
1081
|
+
<p class="card-desc">{f.desc}</p>
|
|
1082
|
+
</li>
|
|
1083
|
+
{/each}
|
|
1084
|
+
</ul>
|
|
1085
|
+
</section>
|
|
1086
|
+
|
|
1087
|
+
<section class="demo" aria-labelledby="demo-heading">
|
|
1088
|
+
<h2 id="demo-heading" class="section-title">{pageHomeCopy(ctx).demoTitle}</h2>
|
|
1089
|
+
<p class="demo-hint">{pageHomeCopy(ctx).demoHint}</p>
|
|
1090
|
+
<div class="counter-wrap" client:visible>
|
|
1091
|
+
<div class="counter">
|
|
1092
|
+
<p class="counter-label">{pageHomeCopy(ctx).demoLabel}</p>
|
|
1093
|
+
<div class="counter-row">
|
|
1094
|
+
<button id="counter-btn" type="button" class="btn-counter" onclick={() => count++} aria-label="{pageHomeCopy(ctx).counterAria}">
|
|
1095
|
+
+1
|
|
1096
|
+
</button>
|
|
1097
|
+
<output class="counter-out" for="counter-btn">
|
|
1098
|
+
<span class="counter-val">{count}</span>
|
|
1099
|
+
<span class="counter-meta">×2 = {doubled}</span>
|
|
1100
|
+
</output>
|
|
1101
|
+
</div>
|
|
1102
|
+
</div>
|
|
1103
|
+
</div>
|
|
158
1104
|
</section>
|
|
159
1105
|
|
|
160
|
-
<
|
|
161
|
-
{
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
1106
|
+
<style>
|
|
1107
|
+
.hero {
|
|
1108
|
+
text-align: center;
|
|
1109
|
+
padding: 2.5rem 0 3rem;
|
|
1110
|
+
max-width: 40rem;
|
|
1111
|
+
margin: 0 auto;
|
|
1112
|
+
}
|
|
1113
|
+
.hero-kicker {
|
|
1114
|
+
margin: 0 0 0.75rem;
|
|
1115
|
+
font-size: 0.8125rem;
|
|
1116
|
+
font-weight: 600;
|
|
1117
|
+
letter-spacing: 0.12em;
|
|
1118
|
+
text-transform: uppercase;
|
|
1119
|
+
color: var(--nx-accent);
|
|
1120
|
+
}
|
|
1121
|
+
.hero-title {
|
|
1122
|
+
margin: 0 0 1rem;
|
|
1123
|
+
font-family: var(--nx-display);
|
|
1124
|
+
font-size: clamp(2rem, 5vw, 2.75rem);
|
|
1125
|
+
font-weight: 700;
|
|
1126
|
+
line-height: 1.15;
|
|
1127
|
+
letter-spacing: -0.02em;
|
|
1128
|
+
}
|
|
1129
|
+
.hero-lead {
|
|
1130
|
+
margin: 0 0 2rem;
|
|
1131
|
+
font-size: 1.0625rem;
|
|
1132
|
+
color: var(--nx-muted);
|
|
1133
|
+
line-height: 1.65;
|
|
1134
|
+
}
|
|
1135
|
+
.hero-actions {
|
|
1136
|
+
display: flex;
|
|
1137
|
+
flex-wrap: wrap;
|
|
1138
|
+
gap: 0.75rem;
|
|
1139
|
+
justify-content: center;
|
|
1140
|
+
}
|
|
1141
|
+
.btn {
|
|
1142
|
+
display: inline-flex;
|
|
1143
|
+
align-items: center;
|
|
1144
|
+
justify-content: center;
|
|
1145
|
+
padding: 0.65rem 1.25rem;
|
|
1146
|
+
border-radius: 10px;
|
|
1147
|
+
font-size: 0.9375rem;
|
|
1148
|
+
font-weight: 600;
|
|
1149
|
+
text-decoration: none;
|
|
1150
|
+
transition: transform 0.12s ease, background 0.15s ease, border-color 0.15s ease;
|
|
1151
|
+
}
|
|
1152
|
+
.btn:active {
|
|
1153
|
+
transform: scale(0.98);
|
|
1154
|
+
}
|
|
1155
|
+
.btn-primary {
|
|
1156
|
+
background: linear-gradient(135deg, var(--nx-accent), #6366f1);
|
|
1157
|
+
color: #fff;
|
|
1158
|
+
border: none;
|
|
1159
|
+
box-shadow: 0 4px 24px var(--nx-accent-dim);
|
|
1160
|
+
}
|
|
1161
|
+
.btn-primary:hover {
|
|
1162
|
+
filter: brightness(1.06);
|
|
1163
|
+
}
|
|
1164
|
+
.btn-ghost {
|
|
1165
|
+
background: var(--nx-surface);
|
|
1166
|
+
color: var(--nx-text);
|
|
1167
|
+
border: 1px solid var(--nx-border);
|
|
1168
|
+
}
|
|
1169
|
+
.btn-ghost:hover {
|
|
1170
|
+
border-color: rgba(255, 255, 255, 0.18);
|
|
1171
|
+
background: rgba(255, 255, 255, 0.06);
|
|
1172
|
+
}
|
|
1173
|
+
.section-title {
|
|
1174
|
+
margin: 0 0 1.25rem;
|
|
1175
|
+
font-family: var(--nx-display);
|
|
1176
|
+
font-size: 1.35rem;
|
|
1177
|
+
font-weight: 600;
|
|
1178
|
+
letter-spacing: -0.02em;
|
|
1179
|
+
}
|
|
1180
|
+
.features {
|
|
1181
|
+
padding: 2rem 0 1rem;
|
|
1182
|
+
}
|
|
1183
|
+
.feature-grid {
|
|
1184
|
+
list-style: none;
|
|
1185
|
+
margin: 0;
|
|
1186
|
+
padding: 0;
|
|
1187
|
+
display: grid;
|
|
1188
|
+
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
|
1189
|
+
gap: 1rem;
|
|
1190
|
+
}
|
|
1191
|
+
.card {
|
|
1192
|
+
margin: 0;
|
|
1193
|
+
padding: 1.35rem 1.25rem;
|
|
1194
|
+
border-radius: var(--nx-radius);
|
|
1195
|
+
background: var(--nx-surface);
|
|
1196
|
+
border: 1px solid var(--nx-border);
|
|
1197
|
+
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
|
1198
|
+
}
|
|
1199
|
+
.card:hover {
|
|
1200
|
+
border-color: rgba(139, 124, 248, 0.35);
|
|
1201
|
+
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.25);
|
|
1202
|
+
}
|
|
1203
|
+
.card-icon {
|
|
1204
|
+
display: block;
|
|
1205
|
+
font-size: 1.5rem;
|
|
1206
|
+
line-height: 1;
|
|
1207
|
+
margin-bottom: 0.75rem;
|
|
1208
|
+
color: var(--nx-accent);
|
|
1209
|
+
}
|
|
1210
|
+
.card-title {
|
|
1211
|
+
margin: 0 0 0.5rem;
|
|
1212
|
+
font-family: var(--nx-display);
|
|
1213
|
+
font-size: 1.05rem;
|
|
1214
|
+
font-weight: 600;
|
|
1215
|
+
}
|
|
1216
|
+
.card-desc {
|
|
1217
|
+
margin: 0;
|
|
1218
|
+
font-size: 0.9rem;
|
|
1219
|
+
color: var(--nx-muted);
|
|
1220
|
+
line-height: 1.55;
|
|
1221
|
+
}
|
|
1222
|
+
.demo {
|
|
1223
|
+
padding: 2.5rem 0 1rem;
|
|
1224
|
+
}
|
|
1225
|
+
.demo-hint {
|
|
1226
|
+
margin: 0 0 1.25rem;
|
|
1227
|
+
font-size: 0.9rem;
|
|
1228
|
+
color: var(--nx-muted);
|
|
1229
|
+
max-width: 36rem;
|
|
1230
|
+
}
|
|
1231
|
+
.counter-wrap {
|
|
1232
|
+
display: flex;
|
|
1233
|
+
justify-content: center;
|
|
1234
|
+
}
|
|
1235
|
+
.counter {
|
|
1236
|
+
width: 100%;
|
|
1237
|
+
max-width: 420px;
|
|
1238
|
+
padding: 1.5rem;
|
|
1239
|
+
border-radius: var(--nx-radius);
|
|
1240
|
+
background: linear-gradient(145deg, rgba(139, 124, 248, 0.08), var(--nx-surface));
|
|
1241
|
+
border: 1px solid var(--nx-border);
|
|
1242
|
+
text-align: center;
|
|
1243
|
+
}
|
|
1244
|
+
.counter-label {
|
|
1245
|
+
margin: 0 0 1rem;
|
|
1246
|
+
font-size: 0.75rem;
|
|
1247
|
+
font-weight: 600;
|
|
1248
|
+
letter-spacing: 0.08em;
|
|
1249
|
+
text-transform: uppercase;
|
|
1250
|
+
color: var(--nx-muted);
|
|
1251
|
+
}
|
|
1252
|
+
.counter-row {
|
|
1253
|
+
display: flex;
|
|
1254
|
+
align-items: center;
|
|
1255
|
+
justify-content: center;
|
|
1256
|
+
gap: 1.25rem;
|
|
1257
|
+
flex-wrap: wrap;
|
|
1258
|
+
}
|
|
1259
|
+
.btn-counter {
|
|
1260
|
+
min-width: 3.5rem;
|
|
1261
|
+
min-height: 3rem;
|
|
1262
|
+
padding: 0 1.25rem;
|
|
1263
|
+
border-radius: 10px;
|
|
1264
|
+
border: 1px solid var(--nx-border);
|
|
1265
|
+
background: rgba(255, 255, 255, 0.06);
|
|
1266
|
+
color: var(--nx-text);
|
|
1267
|
+
font-size: 1rem;
|
|
1268
|
+
font-weight: 700;
|
|
1269
|
+
font-family: inherit;
|
|
1270
|
+
cursor: pointer;
|
|
1271
|
+
transition: background 0.15s ease, border-color 0.15s ease, transform 0.1s ease;
|
|
1272
|
+
}
|
|
1273
|
+
.btn-counter:hover {
|
|
1274
|
+
background: rgba(139, 124, 248, 0.2);
|
|
1275
|
+
border-color: rgba(139, 124, 248, 0.45);
|
|
1276
|
+
}
|
|
1277
|
+
.btn-counter:focus-visible {
|
|
1278
|
+
outline: 2px solid var(--nx-accent);
|
|
1279
|
+
outline-offset: 2px;
|
|
1280
|
+
}
|
|
1281
|
+
.btn-counter:active {
|
|
1282
|
+
transform: scale(0.97);
|
|
1283
|
+
}
|
|
1284
|
+
.counter-out {
|
|
1285
|
+
display: flex;
|
|
1286
|
+
flex-direction: column;
|
|
1287
|
+
align-items: flex-start;
|
|
1288
|
+
gap: 0.15rem;
|
|
1289
|
+
font-size: 1.5rem;
|
|
1290
|
+
font-weight: 700;
|
|
1291
|
+
font-variant-numeric: tabular-nums;
|
|
1292
|
+
font-family: var(--nx-display);
|
|
1293
|
+
}
|
|
1294
|
+
.counter-meta {
|
|
1295
|
+
font-size: 0.875rem;
|
|
1296
|
+
font-weight: 500;
|
|
1297
|
+
color: var(--nx-muted);
|
|
1298
|
+
}
|
|
1299
|
+
</style>
|
|
1300
|
+
`,
|
|
1301
|
+
'src/routes/islands/+page.nx': `---
|
|
1302
|
+
import { pageIslandsCopy } from '$lib/i18n';
|
|
1303
|
+
---
|
|
1304
|
+
|
|
1305
|
+
<script>
|
|
1306
|
+
let demo = $state(0);
|
|
1307
|
+
let doubled = $derived(demo * 2);
|
|
1308
|
+
</script>
|
|
1309
|
+
|
|
1310
|
+
<section class="pres-hero">
|
|
1311
|
+
<p class="pres-kicker">{pageIslandsCopy(ctx).presKicker}</p>
|
|
1312
|
+
<h1 class="pres-title">{pageIslandsCopy(ctx).pageTitle}</h1>
|
|
1313
|
+
<p class="pres-lead">{pageIslandsCopy(ctx).lead}</p>
|
|
1314
|
+
</section>
|
|
1315
|
+
|
|
1316
|
+
<section class="pres-section" aria-labelledby="s1">
|
|
1317
|
+
<h2 id="s1" class="pres-h2">{pageIslandsCopy(ctx).s1h}</h2>
|
|
1318
|
+
<p class="pres-p">
|
|
1319
|
+
{pageIslandsCopy(ctx).s1p}
|
|
1320
|
+
</p>
|
|
1321
|
+
<div class="pres-diagram" role="img" aria-label="{pageIslandsCopy(ctx).diagramAria}">
|
|
1322
|
+
<div class="pres-flow">
|
|
1323
|
+
<span class="pres-box pres-box--srv">{pageIslandsCopy(ctx).flowSrv}</span>
|
|
1324
|
+
<span class="pres-arrow" aria-hidden="true">→</span>
|
|
1325
|
+
<span class="pres-box">{pageIslandsCopy(ctx).flowHtml}</span>
|
|
1326
|
+
<span class="pres-arrow" aria-hidden="true">→</span>
|
|
1327
|
+
<span class="pres-box pres-box--js">{pageIslandsCopy(ctx).flowJs}</span>
|
|
1328
|
+
<span class="pres-arrow" aria-hidden="true">→</span>
|
|
1329
|
+
<span class="pres-box pres-box--ok">{pageIslandsCopy(ctx).flowOk}</span>
|
|
166
1330
|
</div>
|
|
167
|
-
|
|
1331
|
+
</div>
|
|
168
1332
|
</section>
|
|
169
1333
|
|
|
170
|
-
<
|
|
171
|
-
<
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
1334
|
+
<section class="pres-section" aria-labelledby="s2">
|
|
1335
|
+
<h2 id="s2" class="pres-h2">{pageIslandsCopy(ctx).s2h}</h2>
|
|
1336
|
+
<div class="pres-table-wrap">
|
|
1337
|
+
<table class="pres-table">
|
|
1338
|
+
<thead>
|
|
1339
|
+
<tr>
|
|
1340
|
+
<th>{pageIslandsCopy(ctx).thDirective}</th>
|
|
1341
|
+
<th>{pageIslandsCopy(ctx).thWhen}</th>
|
|
1342
|
+
<th>{pageIslandsCopy(ctx).thUse}</th>
|
|
1343
|
+
</tr>
|
|
1344
|
+
</thead>
|
|
1345
|
+
<tbody>
|
|
1346
|
+
<tr>
|
|
1347
|
+
<td><code>client:load</code></td>
|
|
1348
|
+
<td>{pageIslandsCopy(ctx).r1[1]}</td>
|
|
1349
|
+
<td>{pageIslandsCopy(ctx).r1[2]}</td>
|
|
1350
|
+
</tr>
|
|
1351
|
+
<tr>
|
|
1352
|
+
<td><code>client:idle</code></td>
|
|
1353
|
+
<td>{pageIslandsCopy(ctx).r2[1]}</td>
|
|
1354
|
+
<td>{pageIslandsCopy(ctx).r2[2]}</td>
|
|
1355
|
+
</tr>
|
|
1356
|
+
<tr>
|
|
1357
|
+
<td><code>client:visible</code></td>
|
|
1358
|
+
<td>{pageIslandsCopy(ctx).r3[1]}</td>
|
|
1359
|
+
<td>{pageIslandsCopy(ctx).r3[2]}</td>
|
|
1360
|
+
</tr>
|
|
1361
|
+
<tr>
|
|
1362
|
+
<td><code>client:media="…"</code></td>
|
|
1363
|
+
<td>{pageIslandsCopy(ctx).r4[1]}</td>
|
|
1364
|
+
<td>{pageIslandsCopy(ctx).r4[2]}</td>
|
|
1365
|
+
</tr>
|
|
1366
|
+
<tr>
|
|
1367
|
+
<td><code>server:only</code></td>
|
|
1368
|
+
<td>{pageIslandsCopy(ctx).r5[1]}</td>
|
|
1369
|
+
<td>{pageIslandsCopy(ctx).r5[2]}</td>
|
|
1370
|
+
</tr>
|
|
1371
|
+
</tbody>
|
|
1372
|
+
</table>
|
|
1373
|
+
</div>
|
|
1374
|
+
</section>
|
|
1375
|
+
|
|
1376
|
+
<section class="pres-section" aria-labelledby="s3">
|
|
1377
|
+
<h2 id="s3" class="pres-h2">{pageIslandsCopy(ctx).s3h}</h2>
|
|
1378
|
+
<ol class="pres-list">
|
|
1379
|
+
<li>{pageIslandsCopy(ctx).l1}</li>
|
|
1380
|
+
<li>{pageIslandsCopy(ctx).l2}</li>
|
|
1381
|
+
<li>{pageIslandsCopy(ctx).l3}</li>
|
|
1382
|
+
<li>{pageIslandsCopy(ctx).l4}</li>
|
|
1383
|
+
</ol>
|
|
1384
|
+
</section>
|
|
1385
|
+
|
|
1386
|
+
<section class="pres-section" aria-labelledby="s4">
|
|
1387
|
+
<h2 id="s4" class="pres-h2">{pageIslandsCopy(ctx).s4h}</h2>
|
|
1388
|
+
<p class="pres-p">
|
|
1389
|
+
{pageIslandsCopy(ctx).s4p}
|
|
1390
|
+
</p>
|
|
1391
|
+
<p class="pres-p pres-muted">
|
|
1392
|
+
{pageIslandsCopy(ctx).s4muted}
|
|
1393
|
+
</p>
|
|
1394
|
+
</section>
|
|
1395
|
+
|
|
1396
|
+
<section class="pres-section" aria-labelledby="s5">
|
|
1397
|
+
<h2 id="s5" class="pres-h2">{pageIslandsCopy(ctx).s5h}</h2>
|
|
1398
|
+
<p class="pres-p">{pageIslandsCopy(ctx).s5p}</p>
|
|
1399
|
+
<div class="pres-demo" client:visible>
|
|
1400
|
+
<div class="pres-demo-inner">
|
|
1401
|
+
<button type="button" class="pres-demo-btn" id="pres-demo-btn" onclick={() => demo++} aria-label="{pageIslandsCopy(ctx).demoBtnAria}">
|
|
1402
|
+
+1
|
|
1403
|
+
</button>
|
|
1404
|
+
<div class="pres-demo-out">
|
|
1405
|
+
<span class="pres-demo-val">{demo}</span>
|
|
1406
|
+
<span class="pres-demo-meta">×2 = {doubled}</span>
|
|
1407
|
+
</div>
|
|
1408
|
+
</div>
|
|
1409
|
+
</div>
|
|
1410
|
+
</section>
|
|
1411
|
+
|
|
1412
|
+
<section class="pres-section pres-outro">
|
|
1413
|
+
<h2 class="pres-h2">{pageIslandsCopy(ctx).refh}</h2>
|
|
1414
|
+
<p class="pres-p">
|
|
1415
|
+
{pageIslandsCopy(ctx).refp}
|
|
1416
|
+
<a href="https://nexusjs.dev" target="_blank" rel="noopener noreferrer">nexusjs.dev</a>
|
|
1417
|
+
</p>
|
|
1418
|
+
</section>
|
|
175
1419
|
|
|
176
1420
|
<style>
|
|
177
|
-
.hero {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
1421
|
+
.pres-hero {
|
|
1422
|
+
padding: 1rem 0 2rem;
|
|
1423
|
+
border-bottom: 1px solid var(--nx-border);
|
|
1424
|
+
margin-bottom: 2rem;
|
|
1425
|
+
}
|
|
1426
|
+
.pres-kicker {
|
|
1427
|
+
margin: 0 0 0.5rem;
|
|
1428
|
+
font-size: 0.75rem;
|
|
1429
|
+
font-weight: 600;
|
|
1430
|
+
letter-spacing: 0.1em;
|
|
1431
|
+
text-transform: uppercase;
|
|
1432
|
+
color: var(--nx-accent);
|
|
1433
|
+
}
|
|
1434
|
+
.pres-title {
|
|
1435
|
+
margin: 0 0 0.75rem;
|
|
1436
|
+
font-family: var(--nx-display);
|
|
1437
|
+
font-size: clamp(1.75rem, 4vw, 2.25rem);
|
|
1438
|
+
font-weight: 700;
|
|
1439
|
+
letter-spacing: -0.02em;
|
|
1440
|
+
}
|
|
1441
|
+
.pres-lead {
|
|
1442
|
+
margin: 0;
|
|
1443
|
+
max-width: 40rem;
|
|
1444
|
+
color: var(--nx-muted);
|
|
1445
|
+
line-height: 1.6;
|
|
1446
|
+
font-size: 1.05rem;
|
|
1447
|
+
}
|
|
1448
|
+
.pres-section {
|
|
1449
|
+
margin-bottom: 2.5rem;
|
|
1450
|
+
}
|
|
1451
|
+
.pres-h2 {
|
|
1452
|
+
margin: 0 0 1rem;
|
|
1453
|
+
font-family: var(--nx-display);
|
|
1454
|
+
font-size: 1.2rem;
|
|
1455
|
+
font-weight: 600;
|
|
1456
|
+
}
|
|
1457
|
+
.pres-p {
|
|
1458
|
+
margin: 0 0 1rem;
|
|
1459
|
+
color: var(--nx-text);
|
|
1460
|
+
line-height: 1.65;
|
|
1461
|
+
max-width: 46rem;
|
|
1462
|
+
}
|
|
1463
|
+
.pres-muted {
|
|
1464
|
+
color: var(--nx-muted);
|
|
1465
|
+
font-size: 0.9rem;
|
|
1466
|
+
}
|
|
1467
|
+
.pres-code {
|
|
1468
|
+
font-family: ui-monospace, monospace;
|
|
1469
|
+
font-size: 0.88em;
|
|
1470
|
+
background: var(--nx-surface);
|
|
1471
|
+
padding: 0.12em 0.35em;
|
|
1472
|
+
border-radius: 6px;
|
|
1473
|
+
border: 1px solid var(--nx-border);
|
|
1474
|
+
}
|
|
1475
|
+
.pres-diagram {
|
|
1476
|
+
margin-top: 1.25rem;
|
|
1477
|
+
padding: 1.25rem;
|
|
1478
|
+
border-radius: var(--nx-radius);
|
|
1479
|
+
background: var(--nx-surface);
|
|
1480
|
+
border: 1px solid var(--nx-border);
|
|
1481
|
+
overflow-x: auto;
|
|
1482
|
+
}
|
|
1483
|
+
.pres-flow {
|
|
1484
|
+
display: flex;
|
|
1485
|
+
flex-wrap: wrap;
|
|
1486
|
+
align-items: center;
|
|
1487
|
+
gap: 0.5rem 0.75rem;
|
|
1488
|
+
font-size: 0.9rem;
|
|
1489
|
+
}
|
|
1490
|
+
.pres-box {
|
|
1491
|
+
padding: 0.4rem 0.75rem;
|
|
1492
|
+
border-radius: 8px;
|
|
1493
|
+
background: rgba(255, 255, 255, 0.06);
|
|
1494
|
+
border: 1px solid var(--nx-border);
|
|
1495
|
+
}
|
|
1496
|
+
.pres-box--srv {
|
|
1497
|
+
border-color: rgba(56, 189, 248, 0.35);
|
|
1498
|
+
}
|
|
1499
|
+
.pres-box--js {
|
|
1500
|
+
border-color: rgba(139, 124, 248, 0.45);
|
|
1501
|
+
}
|
|
1502
|
+
.pres-box--ok {
|
|
1503
|
+
border-color: rgba(52, 211, 153, 0.4);
|
|
1504
|
+
}
|
|
1505
|
+
.pres-arrow {
|
|
1506
|
+
color: var(--nx-muted);
|
|
1507
|
+
}
|
|
1508
|
+
.pres-table-wrap {
|
|
1509
|
+
overflow-x: auto;
|
|
1510
|
+
border-radius: var(--nx-radius);
|
|
1511
|
+
border: 1px solid var(--nx-border);
|
|
1512
|
+
}
|
|
1513
|
+
.pres-table {
|
|
1514
|
+
width: 100%;
|
|
1515
|
+
border-collapse: collapse;
|
|
1516
|
+
font-size: 0.9rem;
|
|
1517
|
+
}
|
|
1518
|
+
.pres-table th,
|
|
1519
|
+
.pres-table td {
|
|
1520
|
+
padding: 0.65rem 1rem;
|
|
1521
|
+
text-align: left;
|
|
1522
|
+
border-bottom: 1px solid var(--nx-border);
|
|
1523
|
+
}
|
|
1524
|
+
.pres-table th {
|
|
1525
|
+
background: rgba(0, 0, 0, 0.2);
|
|
1526
|
+
font-weight: 600;
|
|
1527
|
+
font-family: var(--nx-display);
|
|
1528
|
+
}
|
|
1529
|
+
.pres-table tr:last-child td {
|
|
1530
|
+
border-bottom: none;
|
|
1531
|
+
}
|
|
1532
|
+
.pres-table code {
|
|
1533
|
+
font-family: ui-monospace, monospace;
|
|
1534
|
+
font-size: 0.85em;
|
|
1535
|
+
color: var(--nx-accent);
|
|
1536
|
+
}
|
|
1537
|
+
.pres-list {
|
|
1538
|
+
margin: 0;
|
|
1539
|
+
padding-left: 1.25rem;
|
|
1540
|
+
max-width: 46rem;
|
|
1541
|
+
line-height: 1.75;
|
|
1542
|
+
color: var(--nx-text);
|
|
1543
|
+
}
|
|
1544
|
+
.pres-list li {
|
|
1545
|
+
margin-bottom: 0.5rem;
|
|
1546
|
+
}
|
|
1547
|
+
.pres-demo {
|
|
1548
|
+
display: flex;
|
|
1549
|
+
justify-content: flex-start;
|
|
1550
|
+
margin-top: 1rem;
|
|
1551
|
+
}
|
|
1552
|
+
.pres-demo-inner {
|
|
1553
|
+
display: flex;
|
|
1554
|
+
align-items: center;
|
|
1555
|
+
gap: 1.25rem;
|
|
1556
|
+
flex-wrap: wrap;
|
|
1557
|
+
padding: 1.25rem 1.5rem;
|
|
1558
|
+
border-radius: var(--nx-radius);
|
|
1559
|
+
background: linear-gradient(145deg, rgba(139, 124, 248, 0.1), var(--nx-surface));
|
|
1560
|
+
border: 1px solid var(--nx-border);
|
|
1561
|
+
}
|
|
1562
|
+
.pres-demo-btn {
|
|
1563
|
+
min-width: 3rem;
|
|
1564
|
+
min-height: 2.75rem;
|
|
1565
|
+
padding: 0 1rem;
|
|
1566
|
+
border-radius: 10px;
|
|
1567
|
+
border: 1px solid var(--nx-border);
|
|
1568
|
+
background: rgba(255, 255, 255, 0.08);
|
|
1569
|
+
color: var(--nx-text);
|
|
1570
|
+
font-weight: 700;
|
|
1571
|
+
font-family: inherit;
|
|
1572
|
+
cursor: pointer;
|
|
1573
|
+
}
|
|
1574
|
+
.pres-demo-btn:hover {
|
|
1575
|
+
background: rgba(139, 124, 248, 0.2);
|
|
1576
|
+
border-color: rgba(139, 124, 248, 0.45);
|
|
1577
|
+
}
|
|
1578
|
+
.pres-demo-out {
|
|
1579
|
+
display: flex;
|
|
1580
|
+
flex-direction: column;
|
|
1581
|
+
gap: 0.2rem;
|
|
1582
|
+
font-family: var(--nx-display);
|
|
1583
|
+
font-size: 1.35rem;
|
|
1584
|
+
font-weight: 700;
|
|
1585
|
+
font-variant-numeric: tabular-nums;
|
|
1586
|
+
}
|
|
1587
|
+
.pres-demo-meta {
|
|
1588
|
+
font-size: 0.85rem;
|
|
1589
|
+
font-weight: 500;
|
|
1590
|
+
color: var(--nx-muted);
|
|
1591
|
+
}
|
|
1592
|
+
.pres-outro {
|
|
1593
|
+
padding-top: 1rem;
|
|
1594
|
+
border-top: 1px solid var(--nx-border);
|
|
1595
|
+
}
|
|
1596
|
+
.pres-outro a {
|
|
1597
|
+
color: var(--nx-accent);
|
|
1598
|
+
text-decoration: none;
|
|
1599
|
+
}
|
|
1600
|
+
.pres-outro a:hover {
|
|
1601
|
+
text-decoration: underline;
|
|
1602
|
+
}
|
|
182
1603
|
</style>
|
|
183
1604
|
`,
|
|
184
1605
|
'src/routes/blog/+page.nx': `---
|
|
185
|
-
|
|
1606
|
+
import { pathWithLang } from '$lib/i18n';
|
|
1607
|
+
|
|
186
1608
|
const posts = [
|
|
187
1609
|
{ slug: "hello-nexus", title: "Hello Nexus", date: "2026-04-03" },
|
|
188
1610
|
{ slug: "islands-arch", title: "Islands Architecture Deep Dive", date: "2026-04-01" },
|
|
189
1611
|
];
|
|
190
1612
|
---
|
|
191
1613
|
|
|
192
|
-
<
|
|
193
|
-
<
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
<
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
1614
|
+
<article class="blog-index">
|
|
1615
|
+
<h1 class="blog-title">Blog</h1>
|
|
1616
|
+
<ul class="blog-list">
|
|
1617
|
+
{#each posts as post}
|
|
1618
|
+
<li class="blog-item">
|
|
1619
|
+
<a class="blog-link" href="{pathWithLang(ctx, '/blog/' + post.slug)}">{post.title}</a>
|
|
1620
|
+
<time class="blog-date" datetime={post.date}>{post.date}</time>
|
|
1621
|
+
</li>
|
|
1622
|
+
{/each}
|
|
1623
|
+
</ul>
|
|
1624
|
+
</article>
|
|
1625
|
+
|
|
1626
|
+
<style>
|
|
1627
|
+
.blog-index { max-width: 36rem; }
|
|
1628
|
+
.blog-title {
|
|
1629
|
+
margin: 0 0 1.5rem;
|
|
1630
|
+
font-family: var(--nx-display);
|
|
1631
|
+
font-size: 1.75rem;
|
|
1632
|
+
font-weight: 700;
|
|
1633
|
+
}
|
|
1634
|
+
.blog-list { list-style: none; margin: 0; padding: 0; }
|
|
1635
|
+
.blog-item {
|
|
1636
|
+
display: flex;
|
|
1637
|
+
flex-wrap: wrap;
|
|
1638
|
+
align-items: baseline;
|
|
1639
|
+
justify-content: space-between;
|
|
1640
|
+
gap: 0.5rem 1rem;
|
|
1641
|
+
padding: 1rem 0;
|
|
1642
|
+
border-bottom: 1px solid var(--nx-border);
|
|
1643
|
+
}
|
|
1644
|
+
.blog-link {
|
|
1645
|
+
color: var(--nx-text);
|
|
1646
|
+
font-weight: 600;
|
|
1647
|
+
text-decoration: none;
|
|
1648
|
+
}
|
|
1649
|
+
.blog-link:hover { color: var(--nx-accent); }
|
|
1650
|
+
.blog-date { font-size: 0.875rem; color: var(--nx-muted); }
|
|
1651
|
+
</style>
|
|
201
1652
|
`,
|
|
202
1653
|
'src/routes/blog/[slug]/+page.nx': `---
|
|
203
|
-
|
|
204
|
-
//
|
|
205
|
-
const { slug } = ctx.params;
|
|
206
|
-
const post = { title: \`Post: \${slug}\`, content: "Post content here..." };
|
|
1654
|
+
import { pathWithLang } from '$lib/i18n';
|
|
1655
|
+
// Use ctx in the template only — frontmatter runs at module load before ctx exists.
|
|
207
1656
|
---
|
|
208
1657
|
|
|
209
|
-
<article>
|
|
210
|
-
<
|
|
211
|
-
<
|
|
212
|
-
<
|
|
1658
|
+
<article class="blog-post">
|
|
1659
|
+
<a class="blog-back" href="{pathWithLang(ctx, '/blog')}">← Blog</a>
|
|
1660
|
+
<h1 class="blog-post-title">Post: {ctx.params.slug}</h1>
|
|
1661
|
+
<p class="blog-post-body">Placeholder article for <strong>{ctx.params.slug}</strong>. Wire this to your CMS or <code class="inline-code">load()</code> data.</p>
|
|
213
1662
|
</article>
|
|
214
|
-
`,
|
|
215
|
-
'src/routes/api/users/+server.nx': `---
|
|
216
|
-
// API route — returns JSON
|
|
217
|
-
// GET /api/users
|
|
218
|
-
const users = [
|
|
219
|
-
{ id: 1, name: "Alice" },
|
|
220
|
-
{ id: 2, name: "Bob" },
|
|
221
|
-
];
|
|
222
|
-
---
|
|
223
|
-
|
|
224
|
-
// Export GET handler
|
|
225
|
-
export async function GET(ctx) {
|
|
226
|
-
return Response.json({ users });
|
|
227
|
-
}
|
|
228
1663
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
1664
|
+
<style>
|
|
1665
|
+
.blog-post { max-width: 40rem; }
|
|
1666
|
+
.blog-back {
|
|
1667
|
+
display: inline-block;
|
|
1668
|
+
margin-bottom: 1.25rem;
|
|
1669
|
+
font-size: 0.9rem;
|
|
1670
|
+
color: var(--nx-muted);
|
|
1671
|
+
text-decoration: none;
|
|
1672
|
+
}
|
|
1673
|
+
.blog-back:hover { color: var(--nx-accent); }
|
|
1674
|
+
.blog-post-title {
|
|
1675
|
+
margin: 0 0 1rem;
|
|
1676
|
+
font-family: var(--nx-display);
|
|
1677
|
+
font-size: 1.75rem;
|
|
1678
|
+
font-weight: 700;
|
|
1679
|
+
}
|
|
1680
|
+
.blog-post-body {
|
|
1681
|
+
margin: 0 0 1.5rem;
|
|
1682
|
+
color: var(--nx-muted);
|
|
1683
|
+
line-height: 1.65;
|
|
1684
|
+
}
|
|
1685
|
+
.inline-code {
|
|
1686
|
+
font-size: 0.9em;
|
|
1687
|
+
padding: 0.15em 0.4em;
|
|
1688
|
+
border-radius: 6px;
|
|
1689
|
+
background: var(--nx-surface);
|
|
1690
|
+
border: 1px solid var(--nx-border);
|
|
1691
|
+
color: var(--nx-accent);
|
|
1692
|
+
}
|
|
1693
|
+
</style>
|
|
235
1694
|
`,
|
|
236
1695
|
'src/lib/db.ts': `// Database client placeholder
|
|
237
1696
|
// Replace with your preferred ORM (Prisma, Drizzle, etc.)
|
|
@@ -245,7 +1704,7 @@ export const db = {
|
|
|
245
1704
|
return [{ id: 1, name: 'Demo User', email: 'demo@nexusjs.dev' }];
|
|
246
1705
|
},
|
|
247
1706
|
async update(args: { where?: unknown; data: unknown }) {
|
|
248
|
-
return { ...args.data };
|
|
1707
|
+
return { ...(args.data as object) };
|
|
249
1708
|
},
|
|
250
1709
|
async create(args: { data: unknown }) {
|
|
251
1710
|
return args.data;
|
|
@@ -253,15 +1712,22 @@ export const db = {
|
|
|
253
1712
|
},
|
|
254
1713
|
};
|
|
255
1714
|
`,
|
|
256
|
-
'public/favicon.svg':
|
|
257
|
-
<text y="28" font-size="28">◆</text>
|
|
258
|
-
</svg>`,
|
|
1715
|
+
'public/favicon.svg': NEXUS_LOGO_SVG,
|
|
259
1716
|
'.gitignore': `node_modules/
|
|
260
1717
|
.nexus/
|
|
261
1718
|
dist/
|
|
262
1719
|
*.js.map
|
|
263
1720
|
`,
|
|
264
1721
|
};
|
|
1722
|
+
}
|
|
1723
|
+
async function writeProjectFiles(dir, name, template) {
|
|
1724
|
+
const v = getPublishedCliVersion();
|
|
1725
|
+
const range = `^${v}`;
|
|
1726
|
+
const nexusCli = 'node ./node_modules/@nexus_js/cli/dist/bin.js';
|
|
1727
|
+
const ensureDeps = 'node ./scripts/check-node-modules.mjs';
|
|
1728
|
+
const files = template === 'full'
|
|
1729
|
+
? buildFullScaffoldFiles(name, range, nexusCli, ensureDeps)
|
|
1730
|
+
: buildMinimalScaffoldFiles(name, range, nexusCli, ensureDeps);
|
|
265
1731
|
for (const [filepath, content] of Object.entries(files)) {
|
|
266
1732
|
const fullPath = join(dir, filepath);
|
|
267
1733
|
await writeFile(fullPath, content, 'utf-8');
|