@nexa-stack/framework 1.0.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 (81) hide show
  1. package/.env.example +46 -0
  2. package/LICENSE +21 -0
  3. package/README.md +72 -0
  4. package/bin/nexa.mjs +41 -0
  5. package/bin/nexa.ts +334 -0
  6. package/docs/AI.md +69 -0
  7. package/docs/ARCHITECTURE.md +74 -0
  8. package/docs/EXAMPLES.md +114 -0
  9. package/docs/FRAMEWORK.md +226 -0
  10. package/docs/LANGUAGE.md +39 -0
  11. package/docs/README.md +7 -0
  12. package/docs/READY.md +51 -0
  13. package/docs/REFERENCE.md +255 -0
  14. package/docs/START.md +98 -0
  15. package/docs/advanced.md +97 -0
  16. package/docs/authentication.md +54 -0
  17. package/docs/cli.md +15 -0
  18. package/docs/compare.md +51 -0
  19. package/docs/configuration.md +65 -0
  20. package/docs/database.md +396 -0
  21. package/docs/installation.md +57 -0
  22. package/docs/localization.md +47 -0
  23. package/docs/resources.md +75 -0
  24. package/docs/routing.md +59 -0
  25. package/docs/seeding.md +33 -0
  26. package/docs/services.md +146 -0
  27. package/package.json +77 -0
  28. package/packages/auth/src/auth.test.ts +23 -0
  29. package/packages/auth/src/auth.ts +287 -0
  30. package/packages/auth/src/index.ts +17 -0
  31. package/packages/cache/src/index.ts +203 -0
  32. package/packages/client/src/index.ts +49 -0
  33. package/packages/core/src/app.ts +97 -0
  34. package/packages/core/src/config.ts +55 -0
  35. package/packages/core/src/dev.ts +104 -0
  36. package/packages/core/src/fields.test.ts +81 -0
  37. package/packages/core/src/fields.ts +309 -0
  38. package/packages/core/src/index.ts +60 -0
  39. package/packages/core/src/lang.ts +79 -0
  40. package/packages/core/src/loader.ts +42 -0
  41. package/packages/core/src/migrate.ts +91 -0
  42. package/packages/core/src/policy.ts +36 -0
  43. package/packages/core/src/registry.ts +17 -0
  44. package/packages/core/src/reload.ts +92 -0
  45. package/packages/core/src/resource.test.ts +32 -0
  46. package/packages/core/src/resource.ts +87 -0
  47. package/packages/core/src/routes.ts +22 -0
  48. package/packages/core/src/runtime.ts +11 -0
  49. package/packages/database/src/builder.ts +266 -0
  50. package/packages/database/src/database.ts +252 -0
  51. package/packages/database/src/dialect.ts +186 -0
  52. package/packages/database/src/index.ts +6 -0
  53. package/packages/database/src/mysql.ts +114 -0
  54. package/packages/database/src/postgres.ts +117 -0
  55. package/packages/database/src/query.ts +115 -0
  56. package/packages/database/src/sqlite.ts +216 -0
  57. package/packages/database/src/types.ts +104 -0
  58. package/packages/events/src/index.ts +17 -0
  59. package/packages/export/src/index.ts +36 -0
  60. package/packages/log/src/index.ts +38 -0
  61. package/packages/mail/src/index.ts +130 -0
  62. package/packages/notifications/src/index.ts +84 -0
  63. package/packages/plugins/src/index.ts +39 -0
  64. package/packages/queue/src/index.ts +185 -0
  65. package/packages/queue/src/jobs.ts +9 -0
  66. package/packages/schedule/src/index.ts +64 -0
  67. package/packages/server/src/index.ts +1 -0
  68. package/packages/server/src/middleware.ts +143 -0
  69. package/packages/server/src/query.ts +40 -0
  70. package/packages/server/src/router.ts +813 -0
  71. package/packages/sms/src/index.ts +33 -0
  72. package/packages/storage/src/upload.ts +36 -0
  73. package/packages/testing/src/index.ts +67 -0
  74. package/packages/validation/src/index.ts +1 -0
  75. package/packages/validation/src/validate.test.ts +35 -0
  76. package/packages/validation/src/validate.ts +112 -0
  77. package/public/admin.html +369 -0
  78. package/public/compare.html +66 -0
  79. package/public/dev-bar.js +213 -0
  80. package/public/docs.html +315 -0
  81. package/public/index.html +66 -0
@@ -0,0 +1,66 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en" dir="ltr">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>Nexa vs Laravel</title>
7
+ <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;600;700&family=IBM+Plex+Mono:wght@400&display=swap" rel="stylesheet" />
8
+ <style>
9
+ :root { --bg:#f4f1eb; --ink:#161614; --muted:#5e5b54; --nexa:#0f5c4c; --laravel:#c2410c; --panel:#fffcf7; --line:#d9d3c6; }
10
+ * { box-sizing: border-box; }
11
+ body { margin:0; font-family:"IBM Plex Sans",system-ui,sans-serif; background:var(--bg); color:var(--ink); padding:2rem 1.25rem 4rem; }
12
+ .wrap { max-width:820px; margin:0 auto; }
13
+ a { color:var(--nexa); }
14
+ .nav { display:flex; gap:.6rem; margin-bottom:1.5rem; flex-wrap:wrap; }
15
+ .nav a { text-decoration:none; border:1px solid var(--line); background:var(--panel); padding:.35rem .8rem; border-radius:6px; font-size:.88rem; }
16
+ h1 { font-size:1.8rem; margin:0 0 .5rem; }
17
+ .lead { color:var(--muted); margin:0 0 2rem; }
18
+ h2 { font-size:1.1rem; margin:2rem 0 .75rem; }
19
+ table { width:100%; border-collapse:collapse; background:var(--panel); border:1px solid var(--line); border-radius:10px; overflow:hidden; font-size:.92rem; }
20
+ th,td { padding:.7rem .85rem; text-align:left; border-bottom:1px solid var(--line); }
21
+ th { background:#efeae1; }
22
+ tr:last-child td { border-bottom:none; }
23
+ code { font-family:"IBM Plex Mono",monospace; font-size:.82em; direction:ltr; display:inline-block; }
24
+ .n { color:var(--nexa); font-weight:600; }
25
+ .l { color:var(--laravel); font-weight:600; }
26
+ pre { background:#1c1c1a; color:#e8e4dc; padding:1rem; border-radius:8px; direction:ltr; text-align:left; overflow:auto; font-size:.85rem; font-family:"IBM Plex Mono",monospace; }
27
+ </style>
28
+ </head>
29
+ <body>
30
+ <div class="wrap">
31
+ <div class="nav">
32
+ <a href="/docs">Docs</a>
33
+ <a href="/compare">Compare</a>
34
+ <a href="/admin">Admin</a>
35
+ </div>
36
+ <h1>Nexa vs Laravel — same jobs, less code</h1>
37
+ <p class="lead">Ship a full backend in hours, not weeks.</p>
38
+
39
+ <h2>Commands</h2>
40
+ <table>
41
+ <tr><th>Task</th><th class="l">Laravel</th><th class="n">Nexa</th></tr>
42
+ <tr><td>Serve</td><td><code>php artisan serve</code></td><td><code>nexa serve</code></td></tr>
43
+ <tr><td>New table</td><td><code>make:model -mcr</code></td><td><code>resource(...)</code></td></tr>
44
+ <tr><td>New project</td><td><code>composer create-project</code></td><td><code>nexa new my-app</code></td></tr>
45
+ </table>
46
+
47
+ <h2>Data definition</h2>
48
+ <p class="l">Laravel = Migration + Model + Controller + Route</p>
49
+ <p class="n">Nexa = just this:</p>
50
+ <pre>resource("posts", {
51
+ title: string().required(),
52
+ }).admin();</pre>
53
+
54
+ <h2>Services (same idea)</h2>
55
+ <table>
56
+ <tr><th></th><th class="l">Laravel</th><th class="n">Nexa</th></tr>
57
+ <tr><td>Mail</td><td><code>Mail::</code></td><td><code>mail()</code></td></tr>
58
+ <tr><td>Cache</td><td><code>Cache::</code></td><td><code>cache</code></td></tr>
59
+ <tr><td>Log</td><td><code>Log::</code></td><td><code>log</code></td></tr>
60
+ <tr><td>Queue</td><td><code>dispatch()</code></td><td><code>dispatch()</code></td></tr>
61
+ </table>
62
+
63
+ <p style="margin-top:2rem;color:var(--muted)">Start at <a href="/docs#/START">/docs</a> — full backend in hours.<br/>Admin login: <code>admin@nexa.dev</code> / <code>123456</code></p>
64
+ </div>
65
+ </body>
66
+ </html>
@@ -0,0 +1,213 @@
1
+ (() => {
2
+ if (window.__NEXA_DEV_BAR__) return;
3
+ window.__NEXA_DEV_BAR__ = true;
4
+
5
+ const GROUP_COLORS = {
6
+ resource: "#38bdf8",
7
+ custom: "#a78bfa",
8
+ auth: "#fbbf24",
9
+ notifications: "#34d399",
10
+ probe: "#94a3b8",
11
+ dev: "#f472b6",
12
+ api: "#64748b",
13
+ };
14
+
15
+ const STATUS_COLORS = (s) =>
16
+ s >= 500 ? "#f87171" : s >= 400 ? "#fbbf24" : s >= 300 ? "#94a3b8" : "#4ade80";
17
+
18
+ const style = document.createElement("style");
19
+ style.textContent = `
20
+ body.nexa-dev-open { padding-bottom: 320px !important; }
21
+ body.nexa-dev-bar { padding-bottom: 40px !important; }
22
+ #nexa-dev-bar {
23
+ position: fixed; left: 0; right: 0; bottom: 0; z-index: 99999;
24
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
25
+ font-size: 12px; color: #e2e8f0; pointer-events: auto;
26
+ }
27
+ #nexa-dev-bar * { box-sizing: border-box; }
28
+ #nexa-dev-strip {
29
+ display: flex; align-items: center; gap: 12px; height: 36px;
30
+ padding: 0 14px; background: #0f172a; border-top: 1px solid #334155;
31
+ cursor: pointer; user-select: none;
32
+ }
33
+ #nexa-dev-strip:hover { background: #1e293b; }
34
+ #nexa-dev-badge {
35
+ background: linear-gradient(135deg, #0f5c4c, #2563eb);
36
+ color: #fff; font-weight: 700; padding: 2px 8px; border-radius: 4px;
37
+ letter-spacing: 0.04em;
38
+ }
39
+ #nexa-dev-meta { display: flex; gap: 14px; flex: 1; flex-wrap: wrap; align-items: center; }
40
+ #nexa-dev-meta span { color: #94a3b8; }
41
+ #nexa-dev-meta b { color: #f8fafc; font-weight: 600; }
42
+ #nexa-dev-last { color: #64748b; max-width: 40vw; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
43
+ #nexa-dev-chevron { color: #64748b; transition: transform .2s; }
44
+ #nexa-dev-bar.open #nexa-dev-chevron { transform: rotate(180deg); }
45
+ #nexa-dev-panel {
46
+ display: none; height: 280px; background: #1e293b; border-top: 1px solid #334155;
47
+ grid-template-columns: 1fr 1fr; gap: 0;
48
+ }
49
+ #nexa-dev-bar.open #nexa-dev-panel { display: grid; }
50
+ .nexa-dev-col { overflow: hidden; display: flex; flex-direction: column; border-left: 1px solid #334155; }
51
+ .nexa-dev-col:first-child { border-left: none; }
52
+ .nexa-dev-col h3 {
53
+ margin: 0; padding: 8px 12px; font-size: 11px; text-transform: uppercase;
54
+ letter-spacing: 0.08em; color: #64748b; background: #0f172a;
55
+ }
56
+ .nexa-dev-scroll { overflow: auto; flex: 1; padding: 6px 0; }
57
+ .nexa-dev-row {
58
+ display: flex; align-items: center; gap: 8px; padding: 4px 12px;
59
+ border-bottom: 1px solid #33415522;
60
+ }
61
+ .nexa-dev-row:hover { background: #33415544; }
62
+ .nexa-dev-method {
63
+ min-width: 44px; font-weight: 700; font-size: 10px;
64
+ }
65
+ .nexa-dev-path { flex: 1; color: #cbd5e1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
66
+ .nexa-dev-tag {
67
+ font-size: 9px; padding: 1px 5px; border-radius: 3px; text-transform: uppercase;
68
+ background: #334155; color: #94a3b8;
69
+ }
70
+ .nexa-dev-ms { color: #64748b; min-width: 36px; text-align: left; }
71
+ .nexa-dev-status { min-width: 28px; font-weight: 700; text-align: center; }
72
+ #nexa-dev-reload-ok { color: #4ade80; }
73
+ #nexa-dev-reload-err { color: #f87171; }
74
+ #nexa-dev-links a { color: #38bdf8; text-decoration: none; margin-left: 8px; }
75
+ #nexa-dev-links a:hover { text-decoration: underline; }
76
+ `;
77
+ document.head.appendChild(style);
78
+ document.body.classList.add("nexa-dev-bar");
79
+
80
+ const root = document.createElement("div");
81
+ root.id = "nexa-dev-bar";
82
+ root.innerHTML = `
83
+ <div id="nexa-dev-panel">
84
+ <div class="nexa-dev-col">
85
+ <h3>Routes</h3>
86
+ <div class="nexa-dev-scroll" id="nexa-dev-routes"></div>
87
+ </div>
88
+ <div class="nexa-dev-col">
89
+ <h3>Recent requests</h3>
90
+ <div class="nexa-dev-scroll" id="nexa-dev-requests"></div>
91
+ </div>
92
+ </div>
93
+ <div id="nexa-dev-strip">
94
+ <span id="nexa-dev-badge">NEXA</span>
95
+ <div id="nexa-dev-meta"></div>
96
+ <span id="nexa-dev-last"></span>
97
+ <span id="nexa-dev-links">
98
+ <a href="/api/dev/routes" target="_blank" rel="noopener" onclick="event.stopPropagation()">JSON</a>
99
+ <a href="/api" target="_blank" rel="noopener" onclick="event.stopPropagation()">/api</a>
100
+ </span>
101
+ <span id="nexa-dev-chevron">▲</span>
102
+ </div>
103
+ `;
104
+ document.body.appendChild(root);
105
+
106
+ const meta = document.getElementById("nexa-dev-meta");
107
+ const lastEl = document.getElementById("nexa-dev-last");
108
+ const routesListEl = document.getElementById("nexa-dev-routes");
109
+ const requestsEl = document.getElementById("nexa-dev-requests");
110
+
111
+ let routesLoaded = false;
112
+ let open = false;
113
+
114
+ root.querySelector("#nexa-dev-strip").addEventListener("click", () => {
115
+ open = !open;
116
+ root.classList.toggle("open", open);
117
+ document.body.classList.toggle("nexa-dev-open", open);
118
+ document.body.classList.toggle("nexa-dev-bar", !open);
119
+ if (open && !routesLoaded) loadRoutes();
120
+ });
121
+
122
+ function renderRoutes(routes) {
123
+ routesListEl.innerHTML = routes
124
+ .map((r) => {
125
+ const color = GROUP_COLORS[r.group] || GROUP_COLORS.api;
126
+ return `<div class="nexa-dev-row">
127
+ <span class="nexa-dev-method" style="color:${color}">${r.method}</span>
128
+ <span class="nexa-dev-path" title="${r.path}">${r.path}</span>
129
+ <span class="nexa-dev-tag" style="color:${color}">${r.group}</span>
130
+ </div>`;
131
+ })
132
+ .join("");
133
+ }
134
+
135
+ function renderRequests(requests) {
136
+ if (!requests?.length) {
137
+ requestsEl.innerHTML = `<div class="nexa-dev-row"><span class="nexa-dev-path">No requests yet — use the app.</span></div>`;
138
+ return;
139
+ }
140
+ requestsEl.innerHTML = requests
141
+ .map((r) => {
142
+ const sc = STATUS_COLORS(r.status);
143
+ return `<div class="nexa-dev-row">
144
+ <span class="nexa-dev-method" style="color:${sc}">${r.method}</span>
145
+ <span class="nexa-dev-path" title="${r.path}">${r.path}</span>
146
+ <span class="nexa-dev-status" style="color:${sc}">${r.status}</span>
147
+ <span class="nexa-dev-ms">${r.ms}ms</span>
148
+ </div>`;
149
+ })
150
+ .join("");
151
+ }
152
+
153
+ function renderStatus(d) {
154
+ const reload = d.last_reload_at
155
+ ? d.last_reload_error
156
+ ? `<span id="nexa-dev-reload-err">reload failed</span>`
157
+ : `<span id="nexa-dev-reload-ok">reload ${timeAgo(d.last_reload_at)}</span>`
158
+ : `<span>no reload yet</span>`;
159
+
160
+ meta.innerHTML = `
161
+ <span>port <b>${d.port}</b></span>
162
+ <span><b>${d.resources}</b> resources</span>
163
+ <span><b>${d.routes}</b> routes</span>
164
+ <span>hot reload <b>${d.hot_reload ? "ON" : "OFF"}</b></span>
165
+ <span>${reload}</span>
166
+ `;
167
+
168
+ const last = d.recent_requests?.[0];
169
+ lastEl.textContent = last
170
+ ? `${last.method} ${last.path} → ${last.status} (${last.ms}ms)`
171
+ : "waiting for requests…";
172
+
173
+ renderRequests(d.recent_requests);
174
+ }
175
+
176
+ function timeAgo(iso) {
177
+ const sec = Math.round((Date.now() - new Date(iso).getTime()) / 1000);
178
+ if (sec < 5) return "just now";
179
+ if (sec < 60) return `${sec}s ago`;
180
+ return `${Math.round(sec / 60)}m ago`;
181
+ }
182
+
183
+ async function loadRoutes() {
184
+ try {
185
+ const res = await fetch("/api/dev/routes");
186
+ if (!res.ok) return;
187
+ const { data } = await res.json();
188
+ renderRoutes(data.routes || []);
189
+ routesLoaded = true;
190
+ } catch {
191
+ routesListEl.innerHTML = `<div class="nexa-dev-row"><span class="nexa-dev-path">Could not load routes.</span></div>`;
192
+ }
193
+ }
194
+
195
+ async function poll() {
196
+ try {
197
+ const res = await fetch("/api/dev/status");
198
+ if (res.status === 404) {
199
+ root.remove();
200
+ document.body.classList.remove("nexa-dev-bar", "nexa-dev-open");
201
+ return;
202
+ }
203
+ if (!res.ok) return;
204
+ const { data } = await res.json();
205
+ renderStatus(data);
206
+ } catch {
207
+ /* server starting */
208
+ }
209
+ }
210
+
211
+ poll();
212
+ setInterval(poll, 2500);
213
+ })();
@@ -0,0 +1,315 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en" dir="ltr">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>Nexa Docs</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
8
+ <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet" />
9
+ <style>
10
+ :root {
11
+ --bg: #f6f3ee;
12
+ --ink: #1a1a18;
13
+ --muted: #5c5a55;
14
+ --line: #ddd8ce;
15
+ --panel: #fffdf9;
16
+ --accent: #0f5c4c;
17
+ --code-bg: #1e1e1c;
18
+ --code-fg: #e8e4dc;
19
+ }
20
+ * { box-sizing: border-box; }
21
+ body {
22
+ margin: 0;
23
+ font-family: "IBM Plex Sans", system-ui, sans-serif;
24
+ background:
25
+ radial-gradient(ellipse 80% 50% at 10% -10%, #dfece7 0%, transparent 55%),
26
+ var(--bg);
27
+ color: var(--ink);
28
+ min-height: 100vh;
29
+ }
30
+ .layout {
31
+ display: grid;
32
+ grid-template-columns: 260px 1fr;
33
+ min-height: 100vh;
34
+ }
35
+ aside {
36
+ border-right: 1px solid var(--line);
37
+ background: var(--panel);
38
+ padding: 1.5rem 1rem 2rem;
39
+ position: sticky;
40
+ top: 0;
41
+ height: 100vh;
42
+ overflow: auto;
43
+ }
44
+ .brand {
45
+ font-size: 1.35rem;
46
+ font-weight: 700;
47
+ color: var(--accent);
48
+ text-decoration: none;
49
+ display: block;
50
+ margin-bottom: 0.25rem;
51
+ }
52
+ .brand-sub {
53
+ font-size: 0.8rem;
54
+ color: var(--muted);
55
+ margin-bottom: 1.5rem;
56
+ }
57
+ nav a {
58
+ display: block;
59
+ padding: 0.45rem 0.7rem;
60
+ border-radius: 6px;
61
+ color: var(--ink);
62
+ text-decoration: none;
63
+ font-size: 0.92rem;
64
+ margin-bottom: 2px;
65
+ }
66
+ nav a:hover { background: #efeae2; }
67
+ nav a.active {
68
+ background: var(--accent);
69
+ color: #fff;
70
+ font-weight: 600;
71
+ }
72
+ nav .group {
73
+ font-size: 0.7rem;
74
+ text-transform: uppercase;
75
+ letter-spacing: 0.06em;
76
+ color: var(--muted);
77
+ margin: 1rem 0.7rem 0.35rem;
78
+ font-weight: 600;
79
+ }
80
+ main {
81
+ padding: 2rem 2.5rem 4rem;
82
+ max-width: 820px;
83
+ }
84
+ .top {
85
+ display: flex;
86
+ gap: 0.75rem;
87
+ align-items: center;
88
+ margin-bottom: 1.5rem;
89
+ flex-wrap: wrap;
90
+ }
91
+ .top a.btn {
92
+ font-size: 0.85rem;
93
+ color: var(--accent);
94
+ text-decoration: none;
95
+ border: 1px solid var(--line);
96
+ padding: 0.35rem 0.75rem;
97
+ border-radius: 6px;
98
+ background: var(--panel);
99
+ }
100
+ article h1 { font-size: 1.85rem; margin: 0 0 1rem; line-height: 1.3; }
101
+ article h2 { font-size: 1.25rem; margin: 2rem 0 0.75rem; border-bottom: 1px solid var(--line); padding-bottom: 0.35rem; }
102
+ article h3 { font-size: 1.05rem; margin: 1.4rem 0 0.5rem; }
103
+ article p, article li { line-height: 1.7; color: var(--ink); }
104
+ article p { margin: 0.65rem 0; }
105
+ article a { color: var(--accent); }
106
+ article code {
107
+ font-family: "IBM Plex Mono", ui-monospace, monospace;
108
+ font-size: 0.86em;
109
+ background: #efeae2;
110
+ padding: 0.1em 0.35em;
111
+ border-radius: 4px;
112
+ }
113
+ article pre {
114
+ background: var(--code-bg);
115
+ color: var(--code-fg);
116
+ padding: 1rem 1.1rem;
117
+ border-radius: 8px;
118
+ overflow: auto;
119
+ font-size: 0.84rem;
120
+ line-height: 1.55;
121
+ direction: ltr;
122
+ text-align: left;
123
+ }
124
+ article pre code { background: none; padding: 0; color: inherit; }
125
+ article table {
126
+ width: 100%;
127
+ border-collapse: collapse;
128
+ margin: 1rem 0;
129
+ font-size: 0.92rem;
130
+ }
131
+ article th, article td {
132
+ border: 1px solid var(--line);
133
+ padding: 0.5rem 0.65rem;
134
+ text-align: left;
135
+ }
136
+ article th { background: #efeae2; }
137
+ article blockquote {
138
+ margin: 1rem 0;
139
+ padding: 0.6rem 1rem;
140
+ border-left: 3px solid var(--accent);
141
+ background: #e8f2ef;
142
+ color: var(--muted);
143
+ }
144
+ .loading, .error { color: var(--muted); padding: 2rem 0; }
145
+ @media (max-width: 800px) {
146
+ .layout { grid-template-columns: 1fr; }
147
+ aside {
148
+ position: relative;
149
+ height: auto;
150
+ border-left: none;
151
+ border-bottom: 1px solid var(--line);
152
+ }
153
+ main { padding: 1.25rem; }
154
+ }
155
+ </style>
156
+ </head>
157
+ <body>
158
+ <div class="layout">
159
+ <aside>
160
+ <a class="brand" href="/docs">Nexa</a>
161
+ <div class="brand-sub">Docs · v1.0.0</div>
162
+ <nav id="nav"></nav>
163
+ </aside>
164
+ <main>
165
+ <div class="top">
166
+ <a class="btn" href="/admin">Admin</a>
167
+ <a class="btn" href="/compare">Why Nexa?</a>
168
+ <a class="btn" href="/api" target="_blank">API</a>
169
+ </div>
170
+ <article id="content" class="loading">Loading…</article>
171
+ </main>
172
+ </div>
173
+ <script>
174
+ const CHAPTERS = [
175
+ { group: "Start here", items: [
176
+ { id: "START", title: "★ Quick start" },
177
+ { id: "REFERENCE", title: "★ API reference" },
178
+ { id: "compare", title: "Why Nexa?" },
179
+ ]},
180
+ { group: "Guides", items: [
181
+ { id: "resources", title: "Resources" },
182
+ { id: "authentication", title: "Authentication" },
183
+ { id: "database", title: "Database" },
184
+ { id: "services", title: "Services" },
185
+ { id: "advanced", title: "Advanced" },
186
+ { id: "cli", title: "CLI" },
187
+ ]},
188
+ { group: "Reference", items: [
189
+ { id: "FRAMEWORK", title: "Framework reference" },
190
+ { id: "READY", title: "Ready checklist" },
191
+ ]},
192
+ ];
193
+
194
+ function slugFromHash() {
195
+ const h = location.hash.replace(/^#\/?/, "") || "START";
196
+ return h.replace(/\.md$/, "");
197
+ }
198
+
199
+ function renderNav(active) {
200
+ const nav = document.getElementById("nav");
201
+ nav.innerHTML = CHAPTERS.map((g) => {
202
+ const links = g.items.map((i) =>
203
+ `<a href="#/${i.id}" class="${i.id === active ? "active" : ""}">${i.title}</a>`
204
+ ).join("");
205
+ return `<div class="group">${g.group}</div>${links}`;
206
+ }).join("");
207
+ }
208
+
209
+ function escapeHtml(s) {
210
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
211
+ }
212
+
213
+ function mdToHtml(md) {
214
+ const lines = md.replace(/\r\n/g, "\n").split("\n");
215
+ let html = "";
216
+ let inCode = false;
217
+ let codeLang = "";
218
+ let codeBuf = [];
219
+ let inTable = false;
220
+ let tableRows = [];
221
+ let inList = false;
222
+
223
+ function flushList() {
224
+ if (inList) { html += "</ul>"; inList = false; }
225
+ }
226
+ function flushTable() {
227
+ if (!inTable) return;
228
+ const rows = tableRows.filter((r) => !/^\s*\|?\s*-+/.test(r));
229
+ html += "<table>";
230
+ rows.forEach((row, idx) => {
231
+ const cells = row.replace(/^\|/, "").replace(/\|$/, "").split("|").map((c) => c.trim());
232
+ const tag = idx === 0 ? "th" : "td";
233
+ html += "<tr>" + cells.map((c) => `<${tag}>${inline(c)}</${tag}>`).join("") + "</tr>";
234
+ });
235
+ html += "</table>";
236
+ inTable = false;
237
+ tableRows = [];
238
+ }
239
+
240
+ function inline(t) {
241
+ return escapeHtml(t)
242
+ .replace(/`([^`]+)`/g, "<code>$1</code>")
243
+ .replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
244
+ .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
245
+ }
246
+
247
+ for (const line of lines) {
248
+ if (line.startsWith("```")) {
249
+ flushList(); flushTable();
250
+ if (!inCode) {
251
+ inCode = true;
252
+ codeLang = line.slice(3).trim();
253
+ codeBuf = [];
254
+ } else {
255
+ html += `<pre><code>${escapeHtml(codeBuf.join("\n"))}</code></pre>`;
256
+ inCode = false;
257
+ }
258
+ continue;
259
+ }
260
+ if (inCode) { codeBuf.push(line); continue; }
261
+
262
+ if (line.trim().startsWith("|")) {
263
+ flushList();
264
+ inTable = true;
265
+ tableRows.push(line);
266
+ continue;
267
+ } else {
268
+ flushTable();
269
+ }
270
+
271
+ if (/^### /.test(line)) { flushList(); html += `<h3>${inline(line.slice(4))}</h3>`; continue; }
272
+ if (/^## /.test(line)) { flushList(); html += `<h2>${inline(line.slice(3))}</h2>`; continue; }
273
+ if (/^# /.test(line)) { flushList(); html += `<h1>${inline(line.slice(2))}</h1>`; continue; }
274
+ if (/^[-*] /.test(line)) {
275
+ if (!inList) { html += "<ul>"; inList = true; }
276
+ html += `<li>${inline(line.slice(2))}</li>`;
277
+ continue;
278
+ }
279
+ if (/^\d+\. /.test(line)) {
280
+ flushList();
281
+ html += `<p>${inline(line)}</p>`;
282
+ continue;
283
+ }
284
+ if (line.trim() === "") { flushList(); continue; }
285
+ flushList();
286
+ html += `<p>${inline(line)}</p>`;
287
+ }
288
+ flushList(); flushTable();
289
+ if (inCode) html += `<pre><code>${escapeHtml(codeBuf.join("\n"))}</code></pre>`;
290
+ return html;
291
+ }
292
+
293
+ async function load(id) {
294
+ renderNav(id);
295
+ const el = document.getElementById("content");
296
+ el.className = "loading";
297
+ el.textContent = "Loading…";
298
+ try {
299
+ const res = await fetch(`/api/docs/${id}`);
300
+ if (!res.ok) throw new Error("not found");
301
+ const data = await res.json();
302
+ el.className = "";
303
+ el.innerHTML = mdToHtml(data.content);
304
+ document.title = `${data.title || id} · Nexa Docs`;
305
+ } catch {
306
+ el.className = "error";
307
+ el.textContent = "Page not found.";
308
+ }
309
+ }
310
+
311
+ window.addEventListener("hashchange", () => load(slugFromHash()));
312
+ load(slugFromHash());
313
+ </script>
314
+ </body>
315
+ </html>
@@ -0,0 +1,66 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en" dir="ltr">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>Nexa</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
8
+ <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;600;700&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet" />
9
+ <style>
10
+ :root {
11
+ --ink: #121410;
12
+ --muted: #5a5f56;
13
+ --panel: rgba(255, 252, 247, 0.88);
14
+ --line: rgba(18, 20, 16, 0.12);
15
+ --accent: #0d5c4c;
16
+ }
17
+ * { box-sizing: border-box; margin: 0; padding: 0; }
18
+ body {
19
+ min-height: 100vh;
20
+ font-family: "IBM Plex Sans", system-ui, sans-serif;
21
+ color: var(--ink);
22
+ background:
23
+ radial-gradient(ellipse 80% 60% at 10% 20%, rgba(13, 92, 76, 0.18), transparent 55%),
24
+ linear-gradient(165deg, #f3efe6 0%, #e7efe9 45%, #f6f1ea 100%);
25
+ display: grid;
26
+ place-items: center;
27
+ padding: 2rem 1.25rem;
28
+ }
29
+ .shell {
30
+ width: min(720px, 100%);
31
+ background: var(--panel);
32
+ border: 1px solid var(--line);
33
+ border-radius: 20px;
34
+ padding: clamp(1.75rem, 4vw, 2.75rem);
35
+ }
36
+ .brand { font-size: clamp(2rem, 5vw, 2.75rem); font-weight: 700; color: var(--accent); }
37
+ h1 { font-size: 1.35rem; margin: 1rem 0 0.5rem; font-weight: 600; }
38
+ p { color: var(--muted); line-height: 1.6; margin-bottom: 1.25rem; }
39
+ .actions { display: flex; flex-wrap: wrap; gap: 0.75rem; }
40
+ a.btn {
41
+ display: inline-block; padding: 0.7rem 1.1rem; border-radius: 10px;
42
+ text-decoration: none; font-weight: 600; font-size: 0.95rem;
43
+ }
44
+ a.btn.primary { background: var(--accent); color: #fff; }
45
+ a.btn.ghost { border: 1px solid var(--line); color: var(--ink); }
46
+ code { font-family: "IBM Plex Mono", monospace; font-size: 0.85em; background: #efeae2; padding: 0.1em 0.35em; border-radius: 4px; }
47
+ </style>
48
+ </head>
49
+ <body>
50
+ <div class="shell">
51
+ <div class="brand">Nexa</div>
52
+ <h1>Backend framework for Node.js</h1>
53
+ <p>
54
+ Define a <code>resource</code> and get a database table, REST API, auth, and admin panel.
55
+ Default language is English — set <code>APP_LOCALE=ar</code> for Arabic UI.
56
+ </p>
57
+ <div class="actions">
58
+ <a class="btn primary" href="/admin">Admin</a>
59
+ <a class="btn ghost" href="/docs">Docs</a>
60
+ </div>
61
+ <p style="margin-top:1.5rem;font-size:0.85rem">
62
+ Default login: <code>admin@nexa.dev</code> / <code>123456</code>
63
+ </p>
64
+ </div>
65
+ </body>
66
+ </html>