@nexa-stack/framework 1.0.0 → 1.1.1
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/README.md +13 -3
- package/bin/nexa.ts +25 -11
- package/docs/UI_RULES.md +19 -0
- package/package.json +11 -5
- package/packages/core/src/app.ts +1 -1
- package/packages/server/src/router.ts +36 -12
- package/public/admin-assets/nexa.css +2 -0
- package/public/admin-assets/nexa.umd.js +80 -0
- package/public/admin.html +6 -4
- package/public/app.html +19 -0
- package/public/index.html +22 -66
- package/public/compare.html +0 -66
- package/public/docs.html +0 -315
package/public/admin.html
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
-
<title>Nexa</title>
|
|
6
|
+
<title>Nexa Admin</title>
|
|
7
|
+
<link rel="stylesheet" href="/admin-assets/nexa.css" />
|
|
7
8
|
<style>
|
|
8
9
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
9
10
|
body { font-family: system-ui, sans-serif; background: #f1f5f9; min-height: 100vh; color: #1e293b; }
|
|
@@ -61,7 +62,7 @@
|
|
|
61
62
|
.error-msg { background: #fee2e2; color: #dc2626; padding: 12px; border-radius: 8px; margin-bottom: 16px; }
|
|
62
63
|
</style>
|
|
63
64
|
</head>
|
|
64
|
-
<body>
|
|
65
|
+
<body nexa="luxury light azure">
|
|
65
66
|
|
|
66
67
|
<div id="login">
|
|
67
68
|
<div class="login-card">
|
|
@@ -81,7 +82,7 @@
|
|
|
81
82
|
<div id="app">
|
|
82
83
|
<nav>
|
|
83
84
|
<div class="logo">Nexa</div>
|
|
84
|
-
<a class="docs-link" href="/
|
|
85
|
+
<a class="docs-link" href="/">Nexa Stack</a>
|
|
85
86
|
<div class="lang-switch">
|
|
86
87
|
<button id="nav-lang-ar" onclick="setLang('ar')">عربي</button>
|
|
87
88
|
<button id="nav-lang-en" onclick="setLang('en')">EN</button>
|
|
@@ -107,7 +108,8 @@
|
|
|
107
108
|
</main>
|
|
108
109
|
</div>
|
|
109
110
|
|
|
110
|
-
<script>
|
|
111
|
+
<script src="/admin-assets/nexa.umd.js"></script>
|
|
112
|
+
<script>
|
|
111
113
|
const API = location.origin;
|
|
112
114
|
let token = localStorage.getItem("token");
|
|
113
115
|
let schemas = [];
|
package/public/app.html
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Nexa UI starter</title>
|
|
7
|
+
<link rel="stylesheet" href="/admin-assets/nexa.css">
|
|
8
|
+
</head>
|
|
9
|
+
<body nexa="luxury light azure">
|
|
10
|
+
<header nexa="header glass fixed"></header>
|
|
11
|
+
<main>
|
|
12
|
+
<section nexa="hero split" title="Build with Nexa" eyebrow="Nexa UI"></section>
|
|
13
|
+
<section nexa="features cards-3"></section>
|
|
14
|
+
<section nexa="cta bold"></section>
|
|
15
|
+
</main>
|
|
16
|
+
<footer nexa="footer simple"></footer>
|
|
17
|
+
<script src="/admin-assets/nexa.umd.js"></script>
|
|
18
|
+
</body>
|
|
19
|
+
</html>
|
package/public/index.html
CHANGED
|
@@ -1,66 +1,22 @@
|
|
|
1
|
-
<!
|
|
2
|
-
<html lang="en"
|
|
3
|
-
<head>
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
<
|
|
10
|
-
:
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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>
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Nexa Stack</title>
|
|
7
|
+
<link rel="stylesheet" href="/admin-assets/nexa.css">
|
|
8
|
+
</head>
|
|
9
|
+
<body nexa="luxury light azure">
|
|
10
|
+
<main nexa="container" style="padding-block:10vh;max-width:760px">
|
|
11
|
+
<div nexa="badge primary" label="Nexa Stack"></div>
|
|
12
|
+
<h1>Nexa is running.</h1>
|
|
13
|
+
<p>Schema-driven REST API, JWT authentication and a Nexa UI-powered admin are ready for your resources.</p>
|
|
14
|
+
<div nexa="button-group">
|
|
15
|
+
<a class="nexa-button nexa-primary" href="/admin">Open admin</a>
|
|
16
|
+
<a class="nexa-button nexa-outline" href="/api">View API</a>
|
|
17
|
+
</div>
|
|
18
|
+
<p><code>admin@nexa.dev</code> / <code>123456</code></p>
|
|
19
|
+
</main>
|
|
20
|
+
<script src="/admin-assets/nexa.umd.js"></script>
|
|
21
|
+
</body>
|
|
22
|
+
</html>
|
package/public/compare.html
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
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>
|
package/public/docs.html
DELETED
|
@@ -1,315 +0,0 @@
|
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
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>
|