@vyckr/tachyon 1.1.10 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/.env.example +7 -4
  2. package/LICENSE +21 -0
  3. package/README.md +210 -90
  4. package/package.json +50 -33
  5. package/src/cli/bundle.ts +37 -0
  6. package/src/cli/serve.ts +100 -0
  7. package/src/{client/template.js → compiler/render-template.js} +10 -17
  8. package/src/compiler/template-compiler.ts +419 -0
  9. package/src/runtime/hot-reload-client.ts +15 -0
  10. package/src/{client/dev.html → runtime/shells/development.html} +2 -2
  11. package/src/runtime/shells/not-found.html +73 -0
  12. package/src/{client/prod.html → runtime/shells/production.html} +1 -1
  13. package/src/runtime/spa-renderer.ts +439 -0
  14. package/src/server/console-logger.ts +39 -0
  15. package/src/server/process-executor.ts +287 -0
  16. package/src/server/process-pool.ts +80 -0
  17. package/src/server/route-handler.ts +229 -0
  18. package/src/server/schema-validator.ts +161 -0
  19. package/bun.lock +0 -127
  20. package/components/clicker.html +0 -30
  21. package/deno.lock +0 -19
  22. package/go.mod +0 -3
  23. package/lib/gson-2.3.jar +0 -0
  24. package/main.js +0 -13
  25. package/routes/DELETE +0 -18
  26. package/routes/GET +0 -17
  27. package/routes/HTML +0 -131
  28. package/routes/POST +0 -32
  29. package/routes/SOCKET +0 -26
  30. package/routes/api/:version/DELETE +0 -10
  31. package/routes/api/:version/GET +0 -29
  32. package/routes/api/:version/PATCH +0 -24
  33. package/routes/api/GET +0 -29
  34. package/routes/api/POST +0 -16
  35. package/routes/api/PUT +0 -21
  36. package/src/client/404.html +0 -7
  37. package/src/client/dist.ts +0 -20
  38. package/src/client/hmr.ts +0 -12
  39. package/src/client/render.ts +0 -417
  40. package/src/client/routes.json +0 -1
  41. package/src/client/yon.ts +0 -360
  42. package/src/router.ts +0 -186
  43. package/src/serve.ts +0 -147
  44. package/src/server/logger.ts +0 -31
  45. package/src/server/tach.ts +0 -238
  46. package/tests/index.test.ts +0 -110
  47. package/tests/stream.ts +0 -24
  48. package/tests/worker.ts +0 -7
  49. package/tsconfig.json +0 -17
@@ -10,36 +10,29 @@ export default async function(props) {
10
10
 
11
11
  return async function(elemId, event, compId) {
12
12
 
13
- const elemIds = new Map()
14
-
15
- elemIds.set('@', new Map())
16
- elemIds.set('id', new Map())
17
- elemIds.set('bind', new Map())
13
+ const counters = { id: {}, ev: {}, bind: {} }
18
14
 
19
15
  const ty_generateId = (hash, source) => {
20
16
 
21
- hash = compId ? `${hash}-${compId}` : hash
22
-
23
- if(elemIds.get(source).has(hash)) {
24
-
25
- const degree = elemIds.get(source).get(hash)
17
+ const key = compId ? hash + '-' + compId : hash
26
18
 
27
- elemIds.get(source).set(hash, degree + 1)
19
+ const map = counters[source]
28
20
 
29
- return "ty-" + hash + "-" + degree
21
+ if(key in map) {
22
+ return 'ty-' + key + '-' + map[key]++
30
23
  }
31
24
 
32
- elemIds.get(source).set(hash, 1)
25
+ map[key] = 1
33
26
 
34
- return "ty-" + hash + "-0"
27
+ return 'ty-' + key + '-0'
35
28
  }
36
29
 
37
30
  const ty_invokeEvent = (hash, action) => {
38
31
 
39
- if(elemId === ty_generateId(hash, '@')) {
32
+ if(elemId === ty_generateId(hash, 'ev')) {
40
33
 
41
34
  if(event && !action.endsWith(')')) {
42
- return `${action}('${event}')`
35
+ return action + "('" + event + "')"
43
36
  }
44
37
  return action
45
38
  }
@@ -49,7 +42,7 @@ export default async function(props) {
49
42
  const ty_assignValue = (hash, variable) => {
50
43
 
51
44
  if(elemId === ty_generateId(hash, 'bind') && event) {
52
- return `${variable} = '${event.value}'`
45
+ return variable + " = '" + event.value + "'"
53
46
  }
54
47
 
55
48
  return variable
@@ -0,0 +1,419 @@
1
+ import Router from "../server/route-handler.js";
2
+ import { BunRequest } from 'bun';
3
+ import { exists } from 'node:fs/promises';
4
+
5
+ interface ParsedElement {
6
+ static?: string;
7
+ element?: string;
8
+ }
9
+
10
+ interface ComponentData {
11
+ html: string;
12
+ script?: string;
13
+ scriptLang?: string;
14
+ }
15
+
16
+ const TEMPLATE_PATH = `${import.meta.dir}/render-template.js`;
17
+ const ROUTES_JSON_PATH = `${import.meta.dir}/../runtime/route-manifest.json`;
18
+ const LAYOUTS_JSON_PATH = `${import.meta.dir}/../runtime/layout-manifest.json`;
19
+ const NOT_FOUND_PATH = `${import.meta.dir}/../runtime/shells/not-found.html`;
20
+
21
+ const jsResponse = (body: BodyInit) =>
22
+ new Response(body, { headers: { 'Content-Type': 'application/javascript' } });
23
+
24
+ const jsonResponse = (path: string) =>
25
+ async () => new Response(await Bun.file(path).bytes(), { headers: { 'Content-Type': 'application/json' } });
26
+
27
+ export default class Yon {
28
+
29
+ private static readonly htmlMethod = 'HTML'
30
+ private static readonly layoutMethod = 'LAYOUT'
31
+ private static readonly compMapping = new Map<string, string>()
32
+ private static layoutMapping: Record<string, string> = {}
33
+
34
+ static getParams(request: BunRequest, route: string) {
35
+ const url = new URL(request.url)
36
+ const params = url.pathname.split('/').slice(route.split('/').length)
37
+ return { params: Router.parseParams(params) }
38
+ }
39
+
40
+ static async createStaticRoutes() {
41
+ Yon.compMapping.clear()
42
+ Yon.layoutMapping = {}
43
+
44
+ // Build client-side render + HMR scripts
45
+ const result = await Bun.build({
46
+ entrypoints: [`${import.meta.dir}/../runtime/spa-renderer.ts`, `${import.meta.dir}/../runtime/hot-reload-client.ts`],
47
+ minify: true
48
+ })
49
+
50
+ for (const output of result.outputs) {
51
+ Router.reqRoutes[output.path.replace('./', '/')] = {
52
+ GET: async () => jsResponse(output)
53
+ }
54
+ }
55
+
56
+ // JSON manifests
57
+ Router.reqRoutes["/routes.json"] = { GET: jsonResponse(ROUTES_JSON_PATH) }
58
+ Router.reqRoutes["/layouts.json"] = { GET: jsonResponse(LAYOUTS_JSON_PATH) }
59
+
60
+ // Optional user main.js
61
+ const main = Bun.file(`${process.cwd()}/main.js`)
62
+ if (await main.exists()) {
63
+ Router.reqRoutes["/main.js"] = {
64
+ GET: async () => new Response(await main.bytes(), { headers: { 'Content-Type': 'application/javascript' } })
65
+ }
66
+ }
67
+
68
+ // Bundle all client assets in parallel
69
+ await Promise.all([
70
+ Yon.bundleDependencies(),
71
+ Yon.bundleComponents(),
72
+ Yon.bundleLayouts(),
73
+ Yon.bundlePages(),
74
+ Yon.bundleAssets()
75
+ ])
76
+
77
+ // Write manifests after all routes are registered
78
+ await Promise.all([
79
+ Bun.write(Bun.file(ROUTES_JSON_PATH), JSON.stringify(Router.routeSlugs)),
80
+ Bun.write(Bun.file(LAYOUTS_JSON_PATH), JSON.stringify(Yon.layoutMapping))
81
+ ])
82
+ }
83
+
84
+ // ── Template extraction ────────────────────────────────────────────────────
85
+ private static async extractComponents(data: string): Promise<ComponentData> {
86
+ let scriptContent = '';
87
+ let scriptLang = 'js';
88
+
89
+ const rewriter = new HTMLRewriter()
90
+ .on('script', {
91
+ element(element) {
92
+ const lang = element.getAttribute('lang');
93
+ if (lang) scriptLang = lang;
94
+ },
95
+ text(text) { scriptContent += text.text; }
96
+ })
97
+
98
+ const htmlContent = await rewriter.transform(new Response(data)).text();
99
+
100
+ return {
101
+ html: htmlContent,
102
+ script: scriptContent || undefined,
103
+ scriptLang
104
+ };
105
+ }
106
+
107
+ // ── HTML → AST parsing ─────────────────────────────────────────────────────
108
+ private static parseHTML(
109
+ htmlContent: string,
110
+ imports: Map<string, Set<string>> = new Map()
111
+ ): Promise<ParsedElement[]> {
112
+ return new Promise((resolve) => {
113
+ const parsed: ParsedElement[] = [];
114
+ const tagStack: string[] = [];
115
+ let insideScript = false;
116
+ let insideStyle = false;
117
+
118
+ const genHash = () => Bun.randomUUIDv7().replace(/-/g, '').slice(-8);
119
+
120
+ const formatAttr = (name: string, value: string, hash: string): string => {
121
+ if (name.startsWith('@'))
122
+ return `${name}="\${eval(ty_invokeEvent('${hash}', '${value}'))}"`;
123
+ if (name === ':value')
124
+ return `value="\${eval(ty_assignValue('${hash}', '${value}'))}"`;
125
+ return `${name}="${value}"`;
126
+ };
127
+
128
+ const interpolate = (text: string) =>
129
+ text.replace(/\{([^{}]+)\}/g, '${$1}').replace(/\{\{([^{}]+)\}\}/g, '{${$1}}');
130
+
131
+ const rewriter = new HTMLRewriter()
132
+ .on('script', {
133
+ element(el) {
134
+ insideScript = true;
135
+ el.onEndTag(() => { insideScript = false; });
136
+ }
137
+ })
138
+ .on('style', {
139
+ element(el) {
140
+ insideStyle = true;
141
+ el.onEndTag(() => { insideStyle = false; });
142
+ },
143
+ text(text) {
144
+ parsed.push({ element: `\`<style>@scope { ${text.text} }</style>\`` });
145
+ }
146
+ })
147
+ .on('*', {
148
+ element(element) {
149
+ const tag = element.tagName.toUpperCase();
150
+
151
+ if (tag === 'SCRIPT' || tag === 'STYLE') return;
152
+
153
+ if (tag === 'SLOT') {
154
+ parsed.push({ element: '`<div id="ty-layout-slot"></div>`' });
155
+ element.remove();
156
+ return;
157
+ }
158
+
159
+ const hash = genHash();
160
+ const tagLower = element.tagName.toLowerCase();
161
+ const attrs: Record<string, string> = {};
162
+
163
+ for (const [name, value] of element.attributes) attrs[name] = value;
164
+
165
+ // Component import
166
+ if (tag.endsWith('_')) {
167
+ const compName = tagLower.slice(0, -1);
168
+ const filepath = Yon.compMapping.get(compName);
169
+ const isLazy = 'lazy' in attrs;
170
+
171
+ if (filepath && compName && !isLazy) {
172
+ const existing = imports.get(filepath);
173
+ if (!existing || !existing.has(compName)) {
174
+ const keyword = existing ? 'const' : 'const';
175
+ const awaitPrefix = existing ? '' : 'await ';
176
+ parsed.push({ static: `${keyword} { default: ${compName} } = ${awaitPrefix}import('/components/${filepath}')` });
177
+ if (existing) existing.add(compName);
178
+ else imports.set(filepath, new Set([compName]));
179
+ }
180
+ }
181
+ }
182
+
183
+ // Auto-generate id for non-control, non-component elements
184
+ if (!attrs.id && !tag.endsWith('_') && tag !== 'LOOP' && tag !== 'LOGIC') {
185
+ attrs[':id'] = `ty_generateId('${hash}', 'id')`;
186
+ }
187
+
188
+ const attrStr = Object.entries(attrs).map(([n, v]) => formatAttr(n, v, hash)).join(' ');
189
+ tagStack.push(tagLower);
190
+
191
+ if (element.selfClosing) {
192
+ parsed.push({ element: `\`<${tagLower} ${attrStr} />\`` });
193
+ tagStack.pop();
194
+ } else {
195
+ parsed.push({ element: `\`<${tagLower} ${attrStr}>\`` });
196
+ }
197
+ },
198
+ text(text) {
199
+ if (text.text.trim() && !insideScript && !insideStyle) {
200
+ parsed.push({ element: `\`${interpolate(text.text)}\`` });
201
+ }
202
+ }
203
+ })
204
+ .on('*', {
205
+ element(element) {
206
+ if (element.selfClosing) return;
207
+ const tag = element.tagName.toUpperCase();
208
+ if (tag === 'SCRIPT' || tag === 'STYLE') return;
209
+ element.onEndTag(() => {
210
+ const tagName = tagStack.pop();
211
+ if (tagName) parsed.push({ element: `\`</${tagName}>\`` });
212
+ });
213
+ }
214
+ });
215
+
216
+ rewriter.transform(new Response(htmlContent)).text().then(() => resolve(parsed));
217
+ });
218
+ }
219
+
220
+ // ── JS code generation ─────────────────────────────────────────────────────
221
+ private static async createJSData(elements: ParsedElement[], scriptContent?: string): Promise<string> {
222
+ const statics: string[] = [];
223
+ const body: string[] = [];
224
+
225
+ for (const el of elements) {
226
+ if (el.static) statics.push(el.static);
227
+ if (el.element) {
228
+ // Control flow and component tags are raw JS, not concatenated
229
+ if (el.element.includes('<loop') || el.element.includes('</loop') ||
230
+ el.element.includes('<logic') || el.element.includes('</logic') ||
231
+ /<([A-Za-z0-9-]+)_\s+([^/>]*)\/>/.test(el.element)) {
232
+ body.push(el.element);
233
+ } else {
234
+ body.push(`elements+=${el.element}`);
235
+ }
236
+ }
237
+ }
238
+
239
+ let code = await Bun.file(TEMPLATE_PATH).text();
240
+
241
+ code = code
242
+ .replaceAll('// imports', statics.join('\n'))
243
+ .replaceAll('// script', scriptContent ?? '')
244
+ .replaceAll('// inners', body.join('\n'));
245
+
246
+ // Transform control flow tags to JS
247
+ code = code
248
+ .replaceAll(/`<loop :for="(.*?)">`|`<\/loop>`/g, (_, expr) => expr ? `for(${expr}) {` : '}')
249
+ .replaceAll(/`<logic :if="(.*?)">`|`<\/logic>`/g, (_, expr) => expr ? `if(${expr}) {` : '}')
250
+ .replaceAll(/`<logic :else-if="(.*?)">`|`<\/logic>`/g, (_, expr) => expr ? `else if(${expr}) {` : '}')
251
+ .replaceAll(/`<logic else="">`|`<\/logic>`/g, (_, expr) => expr ? `else {` : '}');
252
+
253
+ // Bind dynamic attributes :attr="expr" → attr="${expr}"
254
+ code = code.replaceAll(/:(\w[\w-]*)="([^"]*)"/g, '$1="${$2}"');
255
+
256
+ // Transform component invocations
257
+ code = code.replaceAll(/`<([A-Za-z0-9-]+)_\s+([^/>]*)\/>`/g, (_, component, attrStr) => {
258
+ const matches = attrStr.matchAll(/([a-zA-Z0-9-@]+)="([^"]*)"/g);
259
+ const props: string[] = [];
260
+ const events: string[] = [];
261
+ const hash = genHash();
262
+ const isLazy = /\blazy\b/.test(attrStr);
263
+
264
+ for (const [, key, value] of matches) {
265
+ if (key === 'lazy') continue;
266
+ if (key.startsWith('@')) {
267
+ events.push(`${key}="${value.replace(/(ty_invokeEvent\(')([^"]+)(',[^)]+\))/g, `$1${hash}$3`)}"`);
268
+ } else {
269
+ props.push(`${key}=${value}`);
270
+ }
271
+ }
272
+
273
+ const genId = "${ty_generateId('" + hash + "', 'id')}";
274
+
275
+ if (isLazy) {
276
+ const filepath = Yon.compMapping.get(component);
277
+ const propsEncoded = props.length ? props.join(';') : '';
278
+ return `
279
+ elements += \`<div id="${genId}" data-lazy-component="${component}" data-lazy-path="/components/${filepath}" data-lazy-props="${propsEncoded}" ${events.join(' ')}></div>\`
280
+ `;
281
+ }
282
+
283
+ return `
284
+ elements += \`<div id="${genId}" ${events.join(' ')}>\`
285
+ if(!compRenders.has('${hash}')) {
286
+ render = await ${component}(\`${props.join(';')}\`)
287
+ elements += await render(elemId, event, '${hash}')
288
+ compRenders.set('${hash}', render)
289
+ } else {
290
+ render = compRenders.get('${hash}')
291
+ elements += await render(elemId, event, '${hash}')
292
+ }
293
+ elements += '</div>'
294
+ `;
295
+ });
296
+
297
+ return code;
298
+ }
299
+
300
+ // ── Build & register a single template module ──────────────────────────────
301
+ private static async registerModule(data: ComponentData, route: string, dir: 'pages' | 'components' | 'layouts') {
302
+ const parsed = await Yon.parseHTML(data.html);
303
+ const jsCode = await Yon.createJSData(parsed, data.script);
304
+
305
+ const srcRoute = route.replace('.html', `.${data.scriptLang || 'js'}`);
306
+ const tmpPath = `/tmp/${srcRoute}`;
307
+
308
+ await Bun.write(Bun.file(tmpPath), jsCode);
309
+
310
+ const result = await Bun.build({
311
+ entrypoints: [tmpPath],
312
+ external: ["*"],
313
+ minify: { whitespace: true, syntax: true }
314
+ });
315
+
316
+ const outRoute = srcRoute.replace('.ts', '.js');
317
+
318
+ Router.reqRoutes[`/${dir}/${outRoute}`] = {
319
+ GET: () => jsResponse(result.outputs[0])
320
+ };
321
+ }
322
+
323
+ // ── Asset bundlers ─────────────────────────────────────────────────────────
324
+ private static async bundleAssets() {
325
+ if (!await exists(Router.assetsPath)) return;
326
+
327
+ for (const route of new Bun.Glob('**/*').scanSync({ cwd: Router.assetsPath })) {
328
+ const file = Bun.file(`${Router.assetsPath}/${route}`);
329
+ Router.reqRoutes[`/assets/${route}`] = {
330
+ GET: async () => new Response(await file.bytes(), { headers: { 'Content-Type': file.type } })
331
+ };
332
+ }
333
+ }
334
+
335
+ private static async bundlePages() {
336
+ if (await exists(Router.routesPath)) {
337
+ for (const route of new Bun.Glob(`**/${Yon.htmlMethod}`).scanSync({ cwd: Router.routesPath })) {
338
+ await Router.validateRoute(route);
339
+ const data = await Yon.extractComponents(await Bun.file(`${Router.routesPath}/${route}`).text());
340
+ await Yon.registerModule(data, `${route}.${data.scriptLang || 'js'}`, 'pages');
341
+ }
342
+ }
343
+
344
+ // 404 page
345
+ const nfFile = Bun.file(`${process.cwd()}/404.html`);
346
+ const nfContent = await nfFile.exists()
347
+ ? await nfFile.text()
348
+ : await Bun.file(NOT_FOUND_PATH).text();
349
+ const nfData = await Yon.extractComponents(nfContent);
350
+ await Yon.registerModule(nfData, '404.html', 'pages');
351
+ }
352
+
353
+ private static async bundleLayouts() {
354
+ if (!await exists(Router.routesPath)) return;
355
+
356
+ for (const layout of new Bun.Glob(`**/${Yon.layoutMethod}`).scanSync({ cwd: Router.routesPath })) {
357
+ const prefix = layout === Yon.layoutMethod ? '/' : `/${layout.replace(`/${Yon.layoutMethod}`, '')}`;
358
+ const data = await Yon.extractComponents(await Bun.file(`${Router.routesPath}/${layout}`).text());
359
+ const layoutRoute = layout === Yon.layoutMethod ? Yon.layoutMethod : layout;
360
+ await Yon.registerModule(data, `${layoutRoute}.${data.scriptLang || 'js'}`, 'layouts');
361
+ Yon.layoutMapping[prefix] = `/layouts/${layoutRoute}.js`;
362
+ }
363
+ }
364
+
365
+ private static async bundleComponents() {
366
+ if (!await exists(Router.componentsPath)) return;
367
+
368
+ for (const comp of new Bun.Glob('**/*.html').scanSync({ cwd: Router.componentsPath })) {
369
+ const filename = comp.split('/').pop()!.replace('.html', '');
370
+ Yon.compMapping.set(filename, comp.replace('.html', '.js'));
371
+ const data = await Yon.extractComponents(await Bun.file(`${Router.componentsPath}/${comp}`).text());
372
+ await Yon.registerModule(data, comp, 'components');
373
+ }
374
+ }
375
+
376
+ private static async bundleDependencies() {
377
+ const packageFile = Bun.file(`${process.cwd()}/package.json`);
378
+ if (!await packageFile.exists()) return;
379
+
380
+ const packages = await packageFile.json();
381
+ const modules = Object.keys(packages.dependencies ?? {});
382
+ const fallbackEntries = ['index.js', 'index', 'index.node'];
383
+
384
+ for (const mod of modules) {
385
+ const modPackPath = `${process.cwd()}/node_modules/${mod}/package.json`;
386
+ const modPack = await Bun.file(modPackPath).json();
387
+
388
+ if (!modPack.main) {
389
+ for (const entry of fallbackEntries) {
390
+ if (await Bun.file(`${process.cwd()}/node_modules/${mod}/${entry}`).exists()) {
391
+ modPack.main = entry;
392
+ break;
393
+ }
394
+ }
395
+ }
396
+
397
+ if (!modPack.main) continue;
398
+
399
+ try {
400
+ const result = await Bun.build({
401
+ entrypoints: [`${process.cwd()}/node_modules/${mod}/${(modPack.main as string).replace('./', '')}`],
402
+ minify: true
403
+ });
404
+
405
+ for (const output of result.outputs) {
406
+ Router.reqRoutes[`/modules/${mod}.js`] = {
407
+ GET: () => jsResponse(output)
408
+ };
409
+ }
410
+ } catch (e) {
411
+ console.warn(`Failed to bundle module '${mod}': ${(e as Error).message}`, process.pid);
412
+ }
413
+ }
414
+ }
415
+ }
416
+
417
+ function genHash(): string {
418
+ return Bun.randomUUIDv7().replace(/-/g, '').slice(-8);
419
+ }
@@ -0,0 +1,15 @@
1
+ const HMR_RECONNECT_MS = 3000
2
+
3
+ function connectHMR() {
4
+ fetch('/hmr').then(async res => {
5
+
6
+ for await(const _ of res.body!) {
7
+ window.location.reload()
8
+ }
9
+
10
+ }).catch(() => {
11
+ setTimeout(connectHMR, HMR_RECONNECT_MS)
12
+ })
13
+ }
14
+
15
+ connectHMR()
@@ -6,8 +6,8 @@
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
7
  <title></title>
8
8
  <script src="/main.js"></script>
9
- <script src="/render.js"></script>
10
- <script src="/hmr.js"></script>
9
+ <script src="/spa-renderer.js"></script>
10
+ <script src="/hot-reload-client.js"></script>
11
11
  </head>
12
12
  <body></body>
13
13
  </html>
@@ -0,0 +1,73 @@
1
+ <script>
2
+ document.title = "404 — Page Not Found"
3
+ </script>
4
+
5
+ <style>
6
+ .nf-container {
7
+ display: flex;
8
+ flex-direction: column;
9
+ align-items: center;
10
+ justify-content: center;
11
+ min-height: 80vh;
12
+ text-align: center;
13
+ font-family: system-ui, -apple-system, sans-serif;
14
+ color: #334155;
15
+ }
16
+ .nf-code {
17
+ font-size: 8rem;
18
+ font-weight: 800;
19
+ letter-spacing: -0.04em;
20
+ background: linear-gradient(135deg, #6366f1, #8b5cf6, #a855f7);
21
+ -webkit-background-clip: text;
22
+ -webkit-text-fill-color: transparent;
23
+ background-clip: text;
24
+ line-height: 1;
25
+ margin: 0;
26
+ }
27
+ .nf-label {
28
+ font-size: 1.25rem;
29
+ font-weight: 500;
30
+ color: #64748b;
31
+ margin: 0.5rem 0 1.5rem;
32
+ }
33
+ .nf-message {
34
+ font-size: 0.95rem;
35
+ color: #94a3b8;
36
+ max-width: 360px;
37
+ margin-bottom: 2rem;
38
+ line-height: 1.5;
39
+ }
40
+ .nf-home {
41
+ display: inline-flex;
42
+ align-items: center;
43
+ gap: 0.5rem;
44
+ padding: 0.65rem 1.5rem;
45
+ background: linear-gradient(135deg, #6366f1, #8b5cf6);
46
+ color: #fff;
47
+ text-decoration: none;
48
+ border-radius: 9999px;
49
+ font-size: 0.9rem;
50
+ font-weight: 500;
51
+ transition: transform 0.15s, box-shadow 0.15s;
52
+ box-shadow: 0 4px 14px rgba(99, 102, 241, 0.35);
53
+ }
54
+ .nf-home:hover {
55
+ transform: translateY(-1px);
56
+ box-shadow: 0 6px 20px rgba(99, 102, 241, 0.45);
57
+ }
58
+ .nf-divider {
59
+ width: 64px;
60
+ height: 4px;
61
+ border-radius: 2px;
62
+ background: linear-gradient(90deg, #6366f1, #a855f7);
63
+ margin-bottom: 1.5rem;
64
+ }
65
+ </style>
66
+
67
+ <div class="nf-container">
68
+ <p class="nf-code">404</p>
69
+ <div class="nf-divider"></div>
70
+ <p class="nf-label">Page Not Found</p>
71
+ <p class="nf-message">The page you're looking for doesn't exist or has been moved.</p>
72
+ <a class="nf-home" href="/">&#8592; Back to Home</a>
73
+ </div>
@@ -6,7 +6,7 @@
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
7
  <title></title>
8
8
  <script src="/main.js"></script>
9
- <script src="/render.js"></script>
9
+ <script src="/spa-renderer.js"></script>
10
10
  </head>
11
11
  <body></body>
12
12
  </html>