dinorex 1.0.0 → 1.0.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/public/index.html +356 -396
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dinorex",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "AI-powered API documentation generator — one command, full docs.",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -5,140 +5,134 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>Dinorex</title>
7
7
  <link rel="preconnect" href="https://fonts.googleapis.com" />
8
- <link href="https://fonts.googleapis.com/css2?family=Syne:wght@400;600;700;800&family=JetBrains+Mono:ital,wght@0,400;0,500;0,600;1,400&display=swap" rel="stylesheet" />
8
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
9
9
  <style>
10
10
  :root {
11
- --bg: #07070d;
12
- --panel: #0e0e18;
13
- --surface: #12121e;
14
- --surface2: #18182a;
15
- --border: #1f1f33;
16
- --border2: #2a2a44;
17
- --accent: #00e87a;
18
- --accent-dim: rgba(0,232,122,0.12);
19
- --accent2: #ff5c35;
20
- --accent3: #8b7cf8;
21
- --gold: #f5c842;
22
- --text: #e2e2f0;
23
- --text-mid: #9090b8;
24
- --text-muted: #4a4a6a;
25
- --GET: #00d4aa; --GET-bg: rgba(0,212,170,0.1);
26
- --POST: #ff6b35; --POST-bg: rgba(255,107,53,0.1);
27
- --PUT: #f7c948; --PUT-bg: rgba(247,201,72,0.1);
28
- --PATCH: #a78bfa; --PATCH-bg: rgba(167,139,250,0.1);
29
- --DELETE: #ff4d6d; --DELETE-bg: rgba(255,77,109,0.1);
30
- --sidebar-w: 300px;
31
- --header-h: 56px;
11
+ --bg: #0f1117;
12
+ --panel: #161b27;
13
+ --surface: #1c2333;
14
+ --surface2: #212840;
15
+ --border: #2a3149;
16
+ --border2: #323d5c;
17
+ --accent: #00c46a;
18
+ --accent-dim: rgba(0,196,106,0.1);
19
+ --accent2: #3b82f6;
20
+ --gold: #f59e0b;
21
+ --text: #e2e8f0;
22
+ --text-mid: #94a3b8;
23
+ --text-muted: #475569;
24
+ --GET: #10b981; --GET-bg: rgba(16,185,129,0.12);
25
+ --POST: #f97316; --POST-bg: rgba(249,115,22,0.12);
26
+ --PUT: #eab308; --PUT-bg: rgba(234,179,8,0.12);
27
+ --PATCH: #a78bfa; --PATCH-bg: rgba(167,139,250,0.12);
28
+ --DELETE: #ef4444; --DELETE-bg: rgba(239,68,68,0.12);
29
+ --sidebar-w: 260px;
30
+ --header-h: 52px;
32
31
  }
33
32
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
34
- html { scroll-behavior: smooth; }
35
- body { font-family: 'Syne', sans-serif; background: var(--bg); color: var(--text); height: 100vh; overflow: hidden; display: flex; flex-direction: column; }
36
-
37
- /* ── Noise overlay ── */
38
- body::after { content: ''; position: fixed; inset: 0; background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.03'/%3E%3C/svg%3E"); pointer-events: none; z-index: 9999; opacity: 0.4; }
33
+ body { font-family: 'Inter', sans-serif; background: var(--bg); color: var(--text); height: 100vh; overflow: hidden; display: flex; flex-direction: column; font-size: 13px; }
39
34
 
40
35
  /* ── Header ── */
41
36
  header {
42
37
  height: var(--header-h);
43
38
  background: var(--panel);
44
39
  border-bottom: 1px solid var(--border);
45
- display: flex;
46
- align-items: center;
47
- padding: 0 1.25rem;
48
- gap: 1rem;
49
- flex-shrink: 0;
50
- position: relative;
51
- z-index: 100;
40
+ display: flex; align-items: center;
41
+ padding: 0 1rem; gap: 0.75rem;
42
+ flex-shrink: 0; z-index: 100;
52
43
  }
53
- .logo { display: flex; align-items: center; gap: 9px; text-decoration: none; }
54
- .logo-dino { font-size: 1.3rem; line-height: 1; }
55
- .logo-name { font-size: 1.05rem; font-weight: 800; letter-spacing: -0.01em; background: linear-gradient(100deg, var(--accent) 0%, var(--accent3) 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
56
- .logo-ver { font-size: 0.65rem; font-family: 'JetBrains Mono', monospace; color: var(--text-muted); background: var(--surface2); border: 1px solid var(--border); border-radius: 4px; padding: 2px 6px; margin-left: 4px; -webkit-text-fill-color: var(--text-muted); }
57
-
58
- .header-divider { width: 1px; height: 24px; background: var(--border); margin: 0 0.25rem; }
44
+ .logo { display: flex; align-items: center; gap: 8px; cursor: pointer; text-decoration: none; }
45
+ .logo-dino { font-size: 1.2rem; }
46
+ .logo-name { font-size: 0.95rem; font-weight: 700; color: var(--text); letter-spacing: -0.01em; }
47
+ .logo-name span { color: var(--accent); }
59
48
 
60
- .project-name-badge { font-size: 0.82rem; color: var(--text-mid); font-weight: 600; }
61
-
62
- .header-spacer { flex: 1; }
49
+ .hdr-divider { width: 1px; height: 20px; background: var(--border); }
50
+ .hdr-project { font-size: 0.8rem; color: var(--text-mid); font-weight: 500; }
51
+ .hdr-spacer { flex: 1; }
63
52
 
64
53
  .status-pill {
65
- display: flex; align-items: center; gap: 6px;
66
- font-size: 0.72rem; font-family: 'JetBrains Mono', monospace;
54
+ display: flex; align-items: center; gap: 5px;
55
+ font-size: 0.72rem; font-weight: 500;
67
56
  color: var(--text-muted); padding: 3px 10px;
68
- background: var(--surface2); border: 1px solid var(--border); border-radius: 20px;
57
+ background: var(--surface); border: 1px solid var(--border); border-radius: 20px;
69
58
  }
70
- .status-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--text-muted); }
59
+ .status-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--text-muted); flex-shrink: 0; }
71
60
  .status-pill.ready .status-dot { background: var(--accent); box-shadow: 0 0 6px var(--accent); }
72
- .status-pill.analyzing .status-dot { background: var(--gold); animation: pulse 1s ease-in-out infinite; }
61
+ .status-pill.ready { color: var(--text-mid); }
62
+ .status-pill.analyzing .status-dot { background: var(--gold); animation: blink 1s ease-in-out infinite; }
73
63
  .status-pill.error .status-dot { background: var(--DELETE); }
74
- @keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.3; } }
64
+ @keyframes blink { 0%,100%{opacity:1} 50%{opacity:0.2} }
75
65
 
76
66
  .hdr-btn {
77
- display: flex; align-items: center; gap: 6px;
78
- background: var(--surface2); border: 1px solid var(--border);
79
- color: var(--text-mid); font-family: 'Syne', sans-serif; font-size: 0.75rem; font-weight: 600;
67
+ display: flex; align-items: center; gap: 5px;
68
+ background: var(--surface); border: 1px solid var(--border);
69
+ color: var(--text-mid); font-family: 'Inter', sans-serif;
70
+ font-size: 0.75rem; font-weight: 500;
80
71
  padding: 5px 12px; border-radius: 6px; cursor: pointer; transition: all 0.15s;
81
- white-space: nowrap;
82
72
  }
83
- .hdr-btn:hover { border-color: var(--accent); color: var(--accent); }
84
- .hdr-btn.primary { background: var(--accent); border-color: var(--accent); color: #07070d; }
85
- .hdr-btn.primary:hover { opacity: 0.85; }
73
+ .hdr-btn:hover { background: var(--surface2); border-color: var(--border2); color: var(--text); }
74
+ .hdr-btn svg { width: 13px; height: 13px; }
86
75
 
87
- /* ── Body layout ── */
76
+ /* ── Layout ── */
88
77
  .app-body { display: flex; flex: 1; overflow: hidden; }
89
78
 
90
79
  /* ── Sidebar ── */
91
80
  nav {
92
- width: var(--sidebar-w);
93
- flex-shrink: 0;
94
- background: var(--panel);
95
- border-right: 1px solid var(--border);
96
- display: flex;
97
- flex-direction: column;
98
- overflow: hidden;
81
+ width: var(--sidebar-w); flex-shrink: 0;
82
+ background: var(--panel); border-right: 1px solid var(--border);
83
+ display: flex; flex-direction: column; overflow: hidden;
99
84
  }
100
- .nav-search {
101
- padding: 0.75rem;
85
+
86
+ .nav-header {
87
+ padding: 0.6rem 0.75rem;
102
88
  border-bottom: 1px solid var(--border);
103
89
  flex-shrink: 0;
104
90
  }
91
+
92
+ .search-wrap { position: relative; }
93
+ .search-icon { position: absolute; left: 8px; top: 50%; transform: translateY(-50%); color: var(--text-muted); pointer-events: none; }
105
94
  .search-input {
106
- width: 100%; background: var(--surface2); border: 1px solid var(--border);
107
- border-radius: 6px; padding: 6px 10px 6px 30px;
108
- color: var(--text); font-family: 'Syne', sans-serif; font-size: 0.8rem;
95
+ width: 100%; background: var(--surface); border: 1px solid var(--border);
96
+ border-radius: 6px; padding: 5px 8px 5px 28px;
97
+ color: var(--text); font-family: 'Inter', sans-serif; font-size: 0.78rem;
109
98
  outline: none; transition: border-color 0.15s;
110
- background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%234a4a6a' stroke-width='2'%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3Cpath d='m21 21-4.35-4.35'/%3E%3C/svg%3E");
111
- background-repeat: no-repeat; background-position: 9px center;
112
99
  }
113
- .search-input:focus { border-color: var(--accent3); }
100
+ .search-input:focus { border-color: var(--accent2); }
114
101
  .search-input::placeholder { color: var(--text-muted); }
115
102
 
116
103
  .nav-list { overflow-y: auto; flex: 1; padding: 0.5rem 0 2rem; }
117
104
  .nav-list::-webkit-scrollbar { width: 3px; }
118
105
  .nav-list::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 3px; }
119
106
 
120
- .nav-group-label {
121
- font-size: 0.65rem; font-weight: 700; letter-spacing: 0.12em;
122
- text-transform: uppercase; color: var(--text-muted);
123
- padding: 1rem 1rem 0.35rem;
107
+ .nav-group { margin-bottom: 0.25rem; }
108
+ .nav-group-header {
109
+ display: flex; align-items: center; gap: 6px;
110
+ padding: 0.4rem 0.75rem; cursor: pointer;
111
+ color: var(--text-mid); font-size: 0.78rem; font-weight: 600;
112
+ user-select: none;
124
113
  }
114
+ .nav-group-header:hover { color: var(--text); }
115
+ .nav-group-icon { color: var(--text-muted); font-size: 0.65rem; transition: transform 0.15s; }
116
+ .nav-group.open .nav-group-icon { transform: rotate(90deg); }
117
+ .nav-group-name { flex: 1; }
118
+ .nav-group-count { font-size: 0.65rem; color: var(--text-muted); background: var(--surface); padding: 1px 6px; border-radius: 10px; }
119
+
120
+ .nav-group-items { display: none; }
121
+ .nav-group.open .nav-group-items { display: block; }
125
122
 
126
123
  .nav-item {
127
- display: flex; align-items: center; gap: 8px;
128
- padding: 0.45rem 1rem; cursor: pointer;
124
+ display: flex; align-items: center; gap: 7px;
125
+ padding: 0.35rem 0.75rem 0.35rem 1.5rem;
126
+ cursor: pointer; transition: all 0.1s;
129
127
  border-left: 2px solid transparent;
130
- transition: all 0.12s;
131
- border-radius: 0 6px 6px 0;
132
- margin-right: 8px;
133
128
  }
134
- .nav-item:hover { background: var(--surface2); }
129
+ .nav-item:hover { background: var(--surface); }
135
130
  .nav-item.active { background: var(--accent-dim); border-left-color: var(--accent); }
136
131
  .nav-item.active .nav-path { color: var(--text); }
137
132
 
138
133
  .m-pill {
139
134
  font-family: 'JetBrains Mono', monospace; font-size: 0.58rem; font-weight: 600;
140
- padding: 2px 5px; border-radius: 3px; min-width: 44px; text-align: center;
141
- letter-spacing: 0.03em; flex-shrink: 0;
135
+ padding: 1px 5px; border-radius: 3px; min-width: 42px; text-align: center; flex-shrink: 0;
142
136
  }
143
137
  .m-GET { color: var(--GET); background: var(--GET-bg); }
144
138
  .m-POST { color: var(--POST); background: var(--POST-bg); }
@@ -148,160 +142,172 @@
148
142
 
149
143
  .nav-path { font-family: 'JetBrains Mono', monospace; font-size: 0.72rem; color: var(--text-mid); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
150
144
 
151
- /* ── Main panel ── */
152
- main { flex: 1; overflow-y: auto; display: flex; flex-direction: column; }
145
+ /* ── Main ── */
146
+ main { flex: 1; overflow-y: auto; display: flex; flex-direction: column; background: var(--bg); }
153
147
  main::-webkit-scrollbar { width: 4px; }
154
148
  main::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 4px; }
155
149
 
156
- /* Overview page */
157
- .overview {
158
- padding: 3rem;
159
- max-width: 860px;
160
- }
161
- .ov-eyebrow { font-size: 0.72rem; font-weight: 700; letter-spacing: 0.15em; text-transform: uppercase; color: var(--accent); margin-bottom: 0.75rem; }
162
- .ov-title { font-size: 2.8rem; font-weight: 800; letter-spacing: -0.04em; line-height: 1.05; margin-bottom: 0.75rem; }
163
- .ov-desc { color: var(--text-mid); font-size: 0.92rem; line-height: 1.65; max-width: 560px; margin-bottom: 2rem; }
164
- .ov-stats { display: flex; gap: 1px; background: var(--border); border: 1px solid var(--border); border-radius: 10px; overflow: hidden; margin-bottom: 2.5rem; max-width: 520px; }
165
- .ov-stat { flex: 1; background: var(--surface); padding: 1.25rem 1.5rem; }
166
- .ov-stat-n { font-size: 1.8rem; font-weight: 800; letter-spacing: -0.03em; background: linear-gradient(135deg, var(--accent), var(--accent3)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
167
- .ov-stat-l { font-size: 0.7rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.1em; margin-top: 2px; }
168
-
169
- .ov-collections { display: flex; flex-direction: column; gap: 0.5rem; }
170
- .ov-col-row {
171
- display: flex; align-items: center; gap: 1rem;
172
- background: var(--surface); border: 1px solid var(--border); border-radius: 8px;
173
- padding: 0.9rem 1.25rem; cursor: pointer; transition: all 0.15s;
174
- }
175
- .ov-col-row:hover { border-color: var(--border2); background: var(--surface2); }
176
- .ov-col-name { font-weight: 700; font-size: 0.9rem; flex: 1; }
177
- .ov-col-desc { font-size: 0.8rem; color: var(--text-mid); flex: 2; }
178
- .ov-col-count { font-family: 'JetBrains Mono', monospace; font-size: 0.75rem; color: var(--accent); background: var(--accent-dim); padding: 2px 9px; border-radius: 20px; }
179
-
180
- /* ── Endpoint detail view ── */
150
+ /* ── Overview ── */
151
+ .overview { padding: 2.5rem 3rem; max-width: 800px; }
152
+ .ov-eyebrow { font-size: 0.7rem; font-weight: 600; letter-spacing: 0.1em; text-transform: uppercase; color: var(--accent); margin-bottom: 0.5rem; }
153
+ .ov-title { font-size: 2rem; font-weight: 700; letter-spacing: -0.03em; margin-bottom: 0.5rem; }
154
+ .ov-desc { color: var(--text-mid); font-size: 0.85rem; line-height: 1.6; margin-bottom: 2rem; }
155
+ .ov-stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1px; background: var(--border); border: 1px solid var(--border); border-radius: 10px; overflow: hidden; margin-bottom: 2rem; }
156
+ .ov-stat { background: var(--panel); padding: 1rem 1.25rem; }
157
+ .ov-stat-n { font-size: 1.5rem; font-weight: 700; color: var(--accent); font-family: 'JetBrains Mono', monospace; }
158
+ .ov-stat-l { font-size: 0.68rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.08em; margin-top: 2px; }
159
+ .ov-base { font-family: 'JetBrains Mono', monospace; font-size: 0.8rem; color: var(--accent2); background: var(--surface); padding: 0.6rem 1rem; border-radius: 6px; border: 1px solid var(--border); display: inline-block; margin-bottom: 2rem; }
160
+
161
+ .ov-cols-title { font-size: 0.78rem; font-weight: 600; color: var(--text-mid); text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 0.75rem; }
162
+ .ov-col-row { display: flex; align-items: center; gap: 1rem; background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 0.85rem 1.1rem; cursor: pointer; transition: all 0.15s; margin-bottom: 0.5rem; }
163
+ .ov-col-row:hover { border-color: var(--border2); background: var(--surface); }
164
+ .ov-col-icon { width: 32px; height: 32px; background: var(--surface2); border-radius: 6px; display: flex; align-items: center; justify-content: center; font-size: 0.9rem; flex-shrink: 0; }
165
+ .ov-col-name { font-weight: 600; font-size: 0.85rem; }
166
+ .ov-col-desc { font-size: 0.78rem; color: var(--text-mid); flex: 1; }
167
+ .ov-col-count { font-family: 'JetBrains Mono', monospace; font-size: 0.7rem; color: var(--accent); background: var(--accent-dim); padding: 2px 8px; border-radius: 10px; flex-shrink: 0; }
168
+
169
+ /* ── Endpoint view ── */
181
170
  .ep-view { display: flex; flex-direction: column; height: 100%; }
182
171
 
183
- .ep-topbar {
184
- flex-shrink: 0;
185
- border-bottom: 1px solid var(--border);
186
- padding: 1.25rem 2rem;
187
- background: var(--panel);
188
- }
189
- .ep-topbar-row1 { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.4rem; }
190
- .ep-method-badge {
191
- font-family: 'JetBrains Mono', monospace; font-size: 0.75rem; font-weight: 700;
192
- padding: 4px 12px; border-radius: 5px; letter-spacing: 0.05em;
193
- }
194
- .ep-path-text { font-family: 'JetBrains Mono', monospace; font-size: 1rem; color: var(--text); }
195
- .ep-auth-tag { background: rgba(245,200,66,0.1); color: var(--gold); border: 1px solid rgba(245,200,66,0.2); font-size: 0.68rem; font-family: 'JetBrains Mono', monospace; padding: 2px 9px; border-radius: 20px; }
196
- .ep-summary { font-size: 0.85rem; color: var(--text-mid); }
172
+ .ep-topbar { flex-shrink: 0; padding: 1.5rem 2rem 0; background: var(--bg); }
173
+ .ep-topbar-row1 { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.5rem; flex-wrap: wrap; }
174
+ .ep-method-badge { font-family: 'JetBrains Mono', monospace; font-size: 0.72rem; font-weight: 700; padding: 4px 10px; border-radius: 5px; }
175
+ .ep-path-text { font-family: 'JetBrains Mono', monospace; font-size: 1rem; font-weight: 500; color: var(--text); }
176
+ .ep-auth-tag { background: rgba(245,158,11,0.1); color: var(--gold); border: 1px solid rgba(245,158,11,0.2); font-size: 0.68rem; padding: 2px 8px; border-radius: 20px; display: flex; align-items: center; gap: 4px; }
177
+ .ep-title { font-size: 1.2rem; font-weight: 700; margin-bottom: 0.25rem; }
178
+ .ep-desc { font-size: 0.83rem; color: var(--text-mid); line-height: 1.55; margin-bottom: 1rem; }
197
179
 
198
- .ep-tabs {
199
- flex-shrink: 0;
200
- display: flex; gap: 0; border-bottom: 1px solid var(--border);
201
- padding: 0 2rem; background: var(--panel);
202
- }
203
- .tab {
204
- font-family: 'Syne', sans-serif; font-size: 0.78rem; font-weight: 600;
205
- background: none; border: none; border-bottom: 2px solid transparent;
206
- color: var(--text-muted); padding: 0.7rem 1rem; cursor: pointer;
207
- transition: all 0.15s; margin-bottom: -1px;
208
- }
180
+ .ep-tabs { flex-shrink: 0; display: flex; border-bottom: 1px solid var(--border); padding: 0 2rem; background: var(--bg); }
181
+ .tab { font-size: 0.78rem; font-weight: 500; background: none; border: none; border-bottom: 2px solid transparent; color: var(--text-muted); padding: 0.65rem 1rem; cursor: pointer; transition: all 0.15s; margin-bottom: -1px; }
209
182
  .tab:hover { color: var(--text-mid); }
210
- .tab.on { color: var(--accent); border-bottom-color: var(--accent); }
183
+ .tab.on { color: var(--text); border-bottom-color: var(--accent2); }
211
184
 
212
- .ep-content { flex: 1; overflow-y: auto; padding: 2rem; }
185
+ .ep-content { flex: 1; overflow-y: auto; padding: 1.5rem 2rem; }
213
186
  .ep-content::-webkit-scrollbar { width: 4px; }
214
187
  .ep-content::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 4px; }
215
188
 
216
- .panel-section { margin-bottom: 1.75rem; }
217
- .section-label { font-size: 0.68rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.12em; color: var(--text-muted); margin-bottom: 0.6rem; }
189
+ /* ── Env bar (like the screenshot) ── */
190
+ .env-bar {
191
+ position: sticky; top: 0; z-index: 10;
192
+ background: var(--bg); border-bottom: 1px solid var(--border);
193
+ padding: 0.6rem 2rem; display: flex; align-items: center; gap: 0.75rem;
194
+ flex-shrink: 0;
195
+ }
196
+ .env-label { font-size: 0.72rem; color: var(--text-muted); font-weight: 500; }
197
+ .env-select {
198
+ background: var(--surface); border: 1px solid var(--border); color: var(--text);
199
+ font-family: 'Inter', sans-serif; font-size: 0.78rem; padding: 4px 8px;
200
+ border-radius: 5px; outline: none; cursor: pointer;
201
+ }
202
+ .env-spacer { flex: 1; }
203
+ .try-btn { background: var(--accent2); border: none; border-radius: 6px; color: white; font-family: 'Inter', sans-serif; font-weight: 600; font-size: 0.78rem; padding: 5px 14px; cursor: pointer; transition: opacity 0.15s; display: flex; align-items: center; gap: 5px; }
204
+ .try-btn:hover { opacity: 0.85; }
205
+ .curl-btn { background: var(--surface); border: 1px solid var(--border); border-radius: 6px; color: var(--text-mid); font-family: 'Inter', sans-serif; font-size: 0.78rem; padding: 5px 12px; cursor: pointer; transition: all 0.15s; }
206
+ .curl-btn:hover { border-color: var(--border2); color: var(--text); }
207
+
208
+ /* ── Panels ── */
209
+ .panel-section { margin-bottom: 1.5rem; }
210
+ .section-label { font-size: 0.7rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.1em; color: var(--text-muted); margin-bottom: 0.6rem; }
218
211
 
219
- /* Params table */
212
+ /* Table */
220
213
  .ptable { width: 100%; border-collapse: collapse; }
221
- .ptable th { text-align: left; font-size: 0.65rem; text-transform: uppercase; letter-spacing: 0.1em; color: var(--text-muted); padding: 0 0.75rem 0.5rem 0; border-bottom: 1px solid var(--border); }
222
- .ptable td { padding: 0.55rem 0.75rem 0.55rem 0; font-size: 0.83rem; border-bottom: 1px solid rgba(31,31,51,0.6); vertical-align: middle; }
223
- .pname { font-family: 'JetBrains Mono', monospace; color: var(--accent3); font-size: 0.8rem; }
224
- .ptype { font-family: 'JetBrains Mono', monospace; font-size: 0.72rem; color: var(--text-muted); }
225
- .pdesc { color: var(--text-mid); font-size: 0.8rem; }
226
- .req-tag { background: rgba(255,77,109,0.1); color: #ff4d6d; font-size: 0.62rem; padding: 1px 6px; border-radius: 3px; font-family: 'JetBrains Mono', monospace; }
214
+ .ptable th { text-align: left; font-size: 0.68rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em; color: var(--text-muted); padding: 0 1rem 0.5rem 0; border-bottom: 1px solid var(--border); }
215
+ .ptable td { padding: 0.55rem 1rem 0.55rem 0; font-size: 0.82rem; border-bottom: 1px solid rgba(42,49,73,0.5); vertical-align: middle; }
216
+ .pname { font-family: 'JetBrains Mono', monospace; color: var(--text); font-size: 0.8rem; font-weight: 500; }
217
+ .ptype { font-family: 'JetBrains Mono', monospace; font-size: 0.72rem; color: var(--accent2); background: rgba(59,130,246,0.08); padding: 1px 6px; border-radius: 3px; }
218
+ .pdesc { color: var(--text-mid); }
219
+ .pex { font-family: 'JetBrains Mono', monospace; font-size: 0.72rem; color: var(--accent); background: var(--surface); padding: 2px 7px; border-radius: 3px; }
220
+ .req-tag { background: rgba(239,68,68,0.1); color: #ef4444; font-size: 0.62rem; padding: 1px 6px; border-radius: 3px; font-family: 'JetBrains Mono', monospace; font-weight: 600; margin-left: 4px; }
227
221
 
228
222
  /* Code */
229
- pre, .code { background: #050509; border: 1px solid var(--border); border-radius: 8px; padding: 1rem 1.2rem; font-family: 'JetBrains Mono', monospace; font-size: 0.78rem; color: #9090b8; overflow-x: auto; white-space: pre; line-height: 1.65; }
223
+ pre { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 1rem 1.2rem; font-family: 'JetBrains Mono', monospace; font-size: 0.78rem; color: var(--text-mid); overflow-x: auto; white-space: pre; line-height: 1.65; }
224
+
225
+ /* Code tabs (Node.js / Python / Go) */
226
+ .code-lang-tabs { display: flex; gap: 0; margin-bottom: 0; border: 1px solid var(--border); border-bottom: none; border-radius: 8px 8px 0 0; overflow: hidden; }
227
+ .lang-tab { background: var(--surface); border: none; color: var(--text-muted); font-family: 'Inter', sans-serif; font-size: 0.75rem; font-weight: 500; padding: 6px 14px; cursor: pointer; transition: all 0.15s; }
228
+ .lang-tab:hover { color: var(--text-mid); }
229
+ .lang-tab.on { background: var(--panel); color: var(--text); }
230
+ .lang-pre { border-radius: 0 0 8px 8px !important; margin-top: 0 !important; }
230
231
 
231
232
  /* Response rows */
232
- .res-row { display: flex; gap: 1rem; align-items: flex-start; padding: 0.65rem 0; border-bottom: 1px solid rgba(31,31,51,0.5); }
233
- .sc { font-family: 'JetBrains Mono', monospace; font-size: 0.75rem; padding: 3px 9px; border-radius: 4px; min-width: 48px; text-align: center; }
234
- .s2 { background: rgba(0,212,170,0.1); color: var(--GET); }
235
- .s4 { background: rgba(247,201,72,0.1); color: var(--PUT); }
236
- .s5 { background: rgba(255,77,109,0.1); color: var(--DELETE); }
237
- .res-info { flex: 1; }
238
- .res-desc { font-size: 0.83rem; color: var(--text-mid); margin-bottom: 0.35rem; }
233
+ .res-row { display: flex; gap: 1rem; align-items: flex-start; padding: 0.6rem 0; border-bottom: 1px solid rgba(42,49,73,0.4); }
234
+ .sc { font-family: 'JetBrains Mono', monospace; font-size: 0.72rem; font-weight: 600; padding: 3px 8px; border-radius: 4px; min-width: 44px; text-align: center; flex-shrink: 0; }
235
+ .s2 { background: rgba(16,185,129,0.1); color: var(--GET); }
236
+ .s4 { background: rgba(234,179,8,0.1); color: var(--PUT); }
237
+ .s5 { background: rgba(239,68,68,0.1); color: var(--DELETE); }
238
+ .res-desc { font-size: 0.82rem; color: var(--text-mid); }
239
239
 
240
240
  /* Try it */
241
241
  .try-layout { display: flex; flex-direction: column; gap: 1rem; }
242
- .try-url-row {
243
- display: flex; align-items: center; gap: 0.75rem;
244
- background: #050509; border: 1px solid var(--border); border-radius: 8px;
245
- padding: 0.6rem 0.75rem;
246
- }
247
- .try-url-row input { background: none; border: none; color: var(--text); font-family: 'JetBrains Mono', monospace; font-size: 0.85rem; flex: 1; outline: none; }
248
- .send-btn { background: var(--accent); border: none; border-radius: 6px; color: #07070d; font-family: 'Syne', sans-serif; font-weight: 700; font-size: 0.8rem; padding: 6px 18px; cursor: pointer; transition: opacity 0.15s; white-space: nowrap; }
242
+ .try-url-row { display: flex; align-items: center; gap: 0.6rem; background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 0.5rem 0.75rem; }
243
+ .try-url-row input { background: none; border: none; color: var(--text); font-family: 'JetBrains Mono', monospace; font-size: 0.82rem; flex: 1; outline: none; }
244
+ .send-btn { background: var(--accent); border: none; border-radius: 5px; color: #0a0f0d; font-family: 'Inter', sans-serif; font-weight: 700; font-size: 0.78rem; padding: 5px 16px; cursor: pointer; transition: opacity 0.15s; white-space: nowrap; }
249
245
  .send-btn:hover { opacity: 0.85; }
250
- .send-btn:disabled { opacity: 0.35; cursor: not-allowed; }
251
- .try-textarea { width: 100%; background: #050509; border: 1px solid var(--border); border-radius: 8px; padding: 0.9rem 1rem; font-family: 'JetBrains Mono', monospace; font-size: 0.78rem; color: var(--text-mid); min-height: 120px; resize: vertical; outline: none; line-height: 1.65; }
252
- .try-textarea:focus { border-color: var(--accent3); }
253
- .try-token-row { display: flex; align-items: center; gap: 0.75rem; background: #050509; border: 1px solid var(--border); border-radius: 8px; padding: 0.6rem 0.75rem; }
254
- .try-token-row span { font-family: 'JetBrains Mono', monospace; font-size: 0.72rem; color: var(--text-muted); white-space: nowrap; }
255
- .try-token-row input { background: none; border: none; color: var(--text); font-family: 'JetBrains Mono', monospace; font-size: 0.82rem; flex: 1; outline: none; }
256
- .res-output { background: #050509; border: 1px solid var(--border); border-radius: 8px; padding: 1rem; font-family: 'JetBrains Mono', monospace; font-size: 0.78rem; color: var(--text-mid); min-height: 80px; white-space: pre-wrap; word-break: break-all; display: none; }
246
+ .try-textarea { width: 100%; background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 0.8rem 1rem; font-family: 'JetBrains Mono', monospace; font-size: 0.78rem; color: var(--text-mid); min-height: 110px; resize: vertical; outline: none; line-height: 1.65; }
247
+ .try-textarea:focus { border-color: var(--accent2); }
248
+ .try-token-row { display: flex; align-items: center; gap: 0.6rem; background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 0.5rem 0.75rem; }
249
+ .try-token-row span { font-family: 'JetBrains Mono', monospace; font-size: 0.7rem; color: var(--text-muted); white-space: nowrap; background: var(--surface2); padding: 2px 7px; border-radius: 3px; }
250
+ .try-token-row input { background: none; border: none; color: var(--text); font-family: 'JetBrains Mono', monospace; font-size: 0.8rem; flex: 1; outline: none; }
251
+ .res-output { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 1rem; font-family: 'JetBrains Mono', monospace; font-size: 0.78rem; color: var(--text-mid); min-height: 80px; white-space: pre-wrap; word-break: break-all; display: none; line-height: 1.6; }
257
252
  .res-output.show { display: block; }
258
- .res-stat { font-size: 0.7rem; margin-bottom: 0.5rem; }
259
- .ok { color: var(--accent); } .bad { color: var(--DELETE); }
260
-
261
- /* Loading / empty states */
262
- .state-center { display: flex; flex-direction: column; align-items: center; justify-content: center; flex: 1; gap: 1.25rem; padding: 4rem; text-align: center; }
263
- .dino-anim { font-size: 3.5rem; animation: walk 0.6s steps(2) infinite; }
264
- @keyframes walk { 0% { transform: scaleX(1); } 50% { transform: scaleX(-1); } }
265
- .state-title { font-size: 1.1rem; font-weight: 700; color: var(--text-mid); }
266
- .state-sub { font-size: 0.83rem; color: var(--text-muted); max-width: 340px; line-height: 1.5; }
267
- .progress-bar { width: 240px; height: 2px; background: var(--surface2); border-radius: 2px; overflow: hidden; }
268
- .progress-fill { height: 100%; background: linear-gradient(90deg, var(--accent), var(--accent3)); width: 30%; border-radius: 2px; animation: prog 1.4s ease-in-out infinite; }
269
- @keyframes prog { 0% { transform: translateX(-100%); } 100% { transform: translateX(400%); } }
253
+ .res-stat { font-size: 0.7rem; margin-bottom: 0.5rem; font-weight: 600; }
254
+ .ok { color: var(--GET); } .bad { color: var(--DELETE); }
255
+
256
+ /* Loading */
257
+ .state-center { display: flex; flex-direction: column; align-items: center; justify-content: center; flex: 1; gap: 1rem; padding: 4rem; text-align: center; }
258
+ .dino-anim { font-size: 3rem; animation: walk 0.6s steps(2) infinite; }
259
+ @keyframes walk { 0%{transform:scaleX(1)} 50%{transform:scaleX(-1)} }
260
+ .state-title { font-size: 1rem; font-weight: 600; color: var(--text-mid); }
261
+ .state-sub { font-size: 0.82rem; color: var(--text-muted); max-width: 320px; line-height: 1.5; }
262
+ .progress-bar { width: 200px; height: 2px; background: var(--surface2); border-radius: 2px; overflow: hidden; }
263
+ .progress-fill { height: 100%; background: linear-gradient(90deg, var(--accent), var(--accent2)); border-radius: 2px; animation: prog 1.4s ease-in-out infinite; }
264
+ @keyframes prog { 0%{transform:translateX(-100%)} 100%{transform:translateX(400%)} }
270
265
 
271
266
  /* Toast */
272
267
  .toasts { position: fixed; bottom: 1.5rem; right: 1.5rem; display: flex; flex-direction: column; gap: 0.5rem; z-index: 9000; }
273
- .toast { background: var(--surface2); border: 1px solid var(--border2); border-radius: 8px; padding: 0.65rem 1.1rem; font-size: 0.82rem; color: var(--text); animation: tin 0.25s ease; }
274
- @keyframes tin { from { transform: translateY(12px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
268
+ .toast { background: var(--surface); border: 1px solid var(--border2); border-radius: 8px; padding: 0.6rem 1rem; font-size: 0.8rem; color: var(--text); animation: tin 0.2s ease; box-shadow: 0 4px 20px rgba(0,0,0,0.4); }
269
+ @keyframes tin { from{transform:translateY(10px);opacity:0} to{transform:translateY(0);opacity:1} }
275
270
  </style>
276
271
  </head>
277
272
  <body>
278
273
  <header>
279
- <a class="logo" href="#" onclick="showOverview()">
274
+ <a class="logo" href="#" onclick="showOverview();return false;">
280
275
  <span class="logo-dino">🦕</span>
281
- <span class="logo-name">Dinorex</span>
276
+ <span class="logo-name">Dino<span>rex</span></span>
282
277
  </a>
283
- <div class="header-divider"></div>
284
- <span class="project-name-badge" id="hdrProject">—</span>
285
- <div class="header-spacer"></div>
286
- <div class="status-pill" id="statusPill"><span class="status-dot"></span><span id="statusText">Loading</span></div>
287
- <button class="hdr-btn" onclick="triggerRescan(false)" title="Check for new/changed endpoints">↻ Rescan</button>
288
- <button class="hdr-btn" onclick="triggerRescan(true)">⟳ Full Rescan</button>
289
- <button class="hdr-btn" onclick="exportPostman()">⬇ Postman</button>
290
- <button class="hdr-btn" onclick="exportSwagger()">⬇ Swagger</button>
278
+ <div class="hdr-divider"></div>
279
+ <span class="hdr-project" id="hdrProject">—</span>
280
+ <div class="hdr-spacer"></div>
281
+ <div class="status-pill" id="statusPill">
282
+ <span class="status-dot"></span>
283
+ <span id="statusText">Loading</span>
284
+ </div>
285
+ <button class="hdr-btn" onclick="triggerRescan(false)" title="Check for new/changed endpoints">
286
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>
287
+ Rescan
288
+ </button>
289
+ <button class="hdr-btn" onclick="triggerRescan(true)" title="Force full re-analysis">
290
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></svg>
291
+ Full Rescan
292
+ </button>
291
293
  </header>
292
294
 
293
295
  <div class="app-body">
294
296
  <nav>
295
- <div class="nav-search">
296
- <input class="search-input" type="text" placeholder="Search endpoints..." oninput="filterNav(this.value)" />
297
+ <div class="nav-header">
298
+ <div class="search-wrap">
299
+ <svg class="search-icon" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
300
+ <input class="search-input" type="text" placeholder="Search endpoints..." oninput="filterNav(this.value)" />
301
+ </div>
297
302
  </div>
298
303
  <div class="nav-list" id="navList"></div>
299
304
  </nav>
305
+
300
306
  <main id="mainPanel">
301
307
  <div class="state-center">
302
308
  <div class="dino-anim">🦕</div>
303
309
  <div class="state-title">Analyzing your API…</div>
304
- <div class="state-sub">Dinorex is scanning your project and generating docs. This takes about 15 seconds.</div>
310
+ <div class="state-sub">Scanning routes, controllers, services and models. This takes about 15 seconds.</div>
305
311
  <div class="progress-bar"><div class="progress-fill"></div></div>
306
312
  </div>
307
313
  </main>
@@ -314,83 +320,82 @@
314
320
  let activeId = null;
315
321
  let pollTimer = null;
316
322
 
317
- // ── Bootstrap ────────────────────────────────────────────────────────────
318
- async function init() {
319
- pollStatus();
320
- }
323
+ async function init() { pollStatus(); }
321
324
 
322
325
  async function pollStatus() {
323
326
  try {
324
- const [statusRes, specRes] = await Promise.all([fetch('/api/status'), fetch('/api/spec')]);
325
- const status = await statusRes.json();
326
- const specData = await specRes.json();
327
-
328
- updateStatusPill(status);
329
-
330
- if (!specData._loading && !specData.error) {
331
- spec = specData;
327
+ const [sr, dr] = await Promise.all([fetch('/api/status'), fetch('/api/spec')]);
328
+ const status = await sr.json();
329
+ const data = await dr.json();
330
+ updateStatus(status);
331
+ if (!data._loading && !data.error) {
332
+ spec = data;
332
333
  renderNav();
333
334
  if (!activeId) showOverview();
334
335
  else renderEndpoint(activeId);
335
- } else if (specData.error) {
336
- showError(specData.error);
336
+ } else if (data.error) {
337
+ showError(data.error);
337
338
  }
338
-
339
- if (status.state === 'analyzing' || status.state === 'pending') {
340
- pollTimer = setTimeout(pollStatus, 2000);
341
- } else if (status.state === 'ready' && specData._loading) {
342
- pollTimer = setTimeout(pollStatus, 1000);
339
+ if (status.state === 'analyzing' || status.state === 'pending' || (status.state === 'ready' && data._loading)) {
340
+ pollTimer = setTimeout(pollStatus, status.state === 'ready' ? 1000 : 2000);
343
341
  }
344
- } catch (e) {
345
- setTimeout(pollStatus, 3000);
346
- }
342
+ } catch { setTimeout(pollStatus, 3000); }
347
343
  }
348
344
 
349
- function updateStatusPill(s) {
345
+ function updateStatus(s) {
350
346
  const pill = document.getElementById('statusPill');
351
347
  const txt = document.getElementById('statusText');
352
348
  pill.className = 'status-pill ' + s.state;
353
- txt.textContent = s.state === 'ready' ? 'Ready' : s.state === 'analyzing' ? 'Analyzing…' : s.state === 'error' ? 'Error' : 'Loading';
349
+ txt.textContent = s.state === 'ready' ? 'System Ready' : s.state === 'analyzing' ? 'Analyzing…' : s.state === 'error' ? 'Error' : 'Loading';
354
350
  }
355
351
 
356
352
  // ── Nav ──────────────────────────────────────────────────────────────────
353
+ const colIcons = ['📁','📂','🗂️','📋','🔧','⚡','🛡️','👤','🏪','💳'];
354
+ function colIcon(i) { return colIcons[i % colIcons.length]; }
355
+
357
356
  function renderNav(filter = '') {
358
357
  if (!spec) return;
359
358
  const q = filter.toLowerCase();
360
359
  let html = '';
361
- for (const col of spec.collections) {
360
+ spec.collections.forEach((col, ci) => {
362
361
  const eps = col.endpoints.filter(e =>
363
362
  !q || e.path.toLowerCase().includes(q) || e.summary.toLowerCase().includes(q) || e.method.toLowerCase().includes(q)
364
363
  );
365
- if (!eps.length) continue;
366
- html += `<div class="nav-group-label">${col.name}</div>`;
367
- for (const ep of eps) {
368
- html += `<div class="nav-item ${ep.id === activeId ? 'active' : ''}" onclick="selectEndpoint('${ep.id}')">
369
- <span class="m-pill m-${ep.method}">${ep.method}</span>
370
- <span class="nav-path">${ep.path}</span>
371
- </div>`;
372
- }
373
- }
364
+ if (!eps.length) return;
365
+ const isOpen = !q ? true : true;
366
+ html += `<div class="nav-group ${isOpen?'open':''}" id="ng-${ci}">
367
+ <div class="nav-group-header" onclick="toggleGroup(${ci})">
368
+ <span class="nav-group-icon">▶</span>
369
+ <span style="font-size:.85rem;margin-right:2px">${colIcon(ci)}</span>
370
+ <span class="nav-group-name">${col.name}</span>
371
+ <span class="nav-group-count">${eps.length}</span>
372
+ </div>
373
+ <div class="nav-group-items">
374
+ ${eps.map(ep => `
375
+ <div class="nav-item ${ep.id === activeId ? 'active' : ''}" onclick="selectEndpoint('${ep.id}')">
376
+ <span class="m-pill m-${ep.method}">${ep.method}</span>
377
+ <span class="nav-path">${ep.path}</span>
378
+ </div>`).join('')}
379
+ </div>
380
+ </div>`;
381
+ });
374
382
  document.getElementById('navList').innerHTML = html || `<div style="padding:1rem;font-size:.8rem;color:var(--text-muted)">No results</div>`;
375
383
  }
376
384
 
377
- function filterNav(v) { renderNav(v); }
378
-
379
- function selectEndpoint(id) {
380
- activeId = id;
381
- renderNav(document.querySelector('.search-input')?.value || '');
382
- renderEndpoint(id);
385
+ function toggleGroup(ci) {
386
+ document.getElementById(`ng-${ci}`)?.classList.toggle('open');
383
387
  }
384
388
 
385
- // ── Overview ─────────────────────────────────────────────────────────────
389
+ function filterNav(v) { renderNav(v); }
390
+ function selectEndpoint(id) { activeId = id; renderNav(document.querySelector('.search-input')?.value||''); renderEndpoint(id); }
391
+
392
+ // ── Overview ──────────────────────────────────────────────────────────────
386
393
  function showOverview() {
387
- activeId = null;
388
- renderNav();
394
+ activeId = null; renderNav();
389
395
  if (!spec) return;
390
396
  document.getElementById('hdrProject').textContent = spec.projectName;
391
- const totalEps = spec.collections.reduce((a, c) => a + c.endpoints.length, 0);
392
- const main = document.getElementById('mainPanel');
393
- main.innerHTML = `
397
+ const totalEps = spec.collections.reduce((a,c) => a+c.endpoints.length, 0);
398
+ document.getElementById('mainPanel').innerHTML = `
394
399
  <div class="overview">
395
400
  <div class="ov-eyebrow">API Documentation</div>
396
401
  <div class="ov-title">${spec.projectName}</div>
@@ -399,22 +404,25 @@
399
404
  <div class="ov-stat"><div class="ov-stat-n">${totalEps}</div><div class="ov-stat-l">Endpoints</div></div>
400
405
  <div class="ov-stat"><div class="ov-stat-n">${spec.collections.length}</div><div class="ov-stat-l">Collections</div></div>
401
406
  <div class="ov-stat"><div class="ov-stat-n">${spec.version}</div><div class="ov-stat-l">Version</div></div>
402
- <div class="ov-stat"><div class="ov-stat-n" style="font-size:1rem;padding-top:4px">${spec.baseUrl}</div><div class="ov-stat-l">Base URL</div></div>
407
+ <div class="ov-stat"><div class="ov-stat-n">${spec.collections.reduce((a,c)=>a+c.endpoints.filter(e=>e.requiresAuth).length,0)}</div><div class="ov-stat-l">Auth Required</div></div>
403
408
  </div>
404
- <div class="ov-collections">
405
- ${spec.collections.map(c => `
406
- <div class="ov-col-row" onclick="selectEndpoint('${c.endpoints[0]?.id}')">
409
+ <div class="ov-base">${spec.baseUrl}</div>
410
+ <div class="ov-cols-title">Collections</div>
411
+ ${spec.collections.map((c,ci) => `
412
+ <div class="ov-col-row" onclick="selectEndpoint('${c.endpoints[0]?.id}')">
413
+ <div class="ov-col-icon">${colIcon(ci)}</div>
414
+ <div>
407
415
  <div class="ov-col-name">${c.name}</div>
408
- <div class="ov-col-desc">${c.description || ''}</div>
409
- <div class="ov-col-count">${c.endpoints.length} endpoints</div>
410
- </div>`).join('')}
411
- </div>
416
+ <div class="ov-col-desc" style="font-size:.75rem;margin-top:1px">${c.description||''}</div>
417
+ </div>
418
+ <div class="ov-col-count">${c.endpoints.length} endpoints</div>
419
+ </div>`).join('')}
412
420
  </div>`;
413
421
  }
414
422
 
415
- // ── Endpoint detail ───────────────────────────────────────────────────────
423
+ // ── Endpoint ──────────────────────────────────────────────────────────────
416
424
  function findEndpoint(id) {
417
- for (const col of (spec?.collections || [])) {
425
+ for (const col of (spec?.collections||[])) {
418
426
  const ep = col.endpoints.find(e => e.id === id);
419
427
  if (ep) return ep;
420
428
  }
@@ -426,20 +434,23 @@
426
434
  if (!ep || !spec) return;
427
435
  document.getElementById('hdrProject').textContent = spec.projectName;
428
436
 
429
- const hasPath = ep.pathParams?.length;
430
- const hasQuery = ep.queryParams?.length;
431
- const hasBody = ep.requestBody && Object.keys(ep.requestBody.schema || {}).length;
432
- const hasRes = ep.responses && Object.keys(ep.responses).length;
437
+ const hasPath = ep.pathParams?.length;
438
+ const hasQuery = ep.queryParams?.length;
439
+ const hasBody = ep.requestBody && Object.keys(ep.requestBody.schema||{}).length;
440
+ const hasRes = ep.responses && Object.keys(ep.responses).length;
433
441
 
434
442
  const tabs = [
435
- hasPath || hasQuery ? 'params' : null,
443
+ (hasPath||hasQuery) ? 'params' : null,
436
444
  hasBody ? 'body' : null,
437
445
  hasRes ? 'responses' : null,
446
+ 'examples',
438
447
  'try'
439
448
  ].filter(Boolean);
440
449
 
441
- const main = document.getElementById('mainPanel');
442
- main.innerHTML = `
450
+ const schema = ep.requestBody?.schema || {};
451
+ const bodyEx = JSON.stringify(Object.fromEntries(Object.entries(schema).map(([k,v])=>[k,v.example??''])), null, 2);
452
+
453
+ document.getElementById('mainPanel').innerHTML = `
443
454
  <div class="ep-view">
444
455
  <div class="ep-topbar">
445
456
  <div class="ep-topbar-row1">
@@ -447,128 +458,104 @@
447
458
  <span class="ep-path-text">${ep.path}</span>
448
459
  ${ep.requiresAuth ? `<span class="ep-auth-tag">🔑 Auth required</span>` : ''}
449
460
  </div>
450
- <div class="ep-summary">${ep.description || ep.summary}</div>
461
+ <div class="ep-title">${ep.summary}</div>
462
+ <div class="ep-desc">${ep.description||''}</div>
451
463
  </div>
452
464
  <div class="ep-tabs">
453
- ${tabs.map((t, i) => `<button class="tab ${i===0?'on':''}" onclick="switchTab(this,'${id}-${t}')">${{params:'Parameters',body:'Request Body',responses:'Responses',try:'Try It ▶'}[t]}</button>`).join('')}
465
+ ${tabs.map((t,i) => `<button class="tab ${i===0?'on':''}" onclick="switchTab(this,'${id}-${t}')">${{params:'Parameters',body:'Body',responses:'Responses',examples:'Examples',try:'Try It ▶'}[t]}</button>`).join('')}
454
466
  </div>
455
467
  <div class="ep-content">
456
- ${tabs.map((t, i) => `<div id="${id}-${t}" style="display:${i===0?'block':'none'}">${renderPanel(ep, t)}</div>`).join('')}
468
+ ${tabs.map((t,i) => `<div id="${id}-${t}" style="display:${i===0?'block':'none'}">${buildPanel(ep,t,bodyEx)}</div>`).join('')}
457
469
  </div>
458
470
  </div>`;
459
471
  }
460
472
 
461
473
  function switchTab(btn, panelId) {
462
- btn.closest('.ep-view').querySelectorAll('.tab').forEach(b => b.classList.remove('on'));
474
+ btn.closest('.ep-view').querySelectorAll('.tab').forEach(b=>b.classList.remove('on'));
463
475
  btn.classList.add('on');
464
- const content = btn.closest('.ep-view').querySelector('.ep-content');
465
- content.querySelectorAll(':scope > div').forEach(d => d.style.display = 'none');
466
- document.getElementById(panelId).style.display = 'block';
476
+ btn.closest('.ep-view').querySelector('.ep-content').querySelectorAll(':scope>div').forEach(d=>d.style.display='none');
477
+ document.getElementById(panelId).style.display='block';
467
478
  }
468
479
 
469
- function renderPanel(ep, tab) {
480
+ function buildPanel(ep, tab, bodyEx) {
470
481
  if (tab === 'params') {
471
- let html = '';
482
+ let h = '';
472
483
  if (ep.pathParams?.length) {
473
- html += `<div class="panel-section">
474
- <div class="section-label">Path Parameters</div>
475
- <table class="ptable">
476
- <thead><tr><th>Name</th><th>Type</th><th>Description</th><th>Example</th></tr></thead>
477
- <tbody>${ep.pathParams.map(p => `
478
- <tr>
479
- <td><span class="pname">${p.name}</span> <span class="req-tag">required</span></td>
480
- <td><span class="ptype">${p.type}</span></td>
481
- <td class="pdesc">${p.description}</td>
482
- <td><span class="ptype">${p.example}</span></td>
483
- </tr>`).join('')}
484
- </tbody>
485
- </table></div>`;
484
+ h += `<div class="panel-section"><div class="section-label">Path Parameters <span style="color:#ef4444;margin-left:4px">${ep.pathParams.length} Required</span></div>
485
+ <table class="ptable"><thead><tr><th>NAME</th><th>TYPE</th><th>DESCRIPTION</th><th>EXAMPLE</th></tr></thead><tbody>
486
+ ${ep.pathParams.map(p=>`<tr><td><span class="pname">${p.name}</span><span class="req-tag">REQUIRED</span></td><td><span class="ptype">${p.type}</span></td><td class="pdesc">${p.description}</td><td><span class="pex">${p.example}</span></td></tr>`).join('')}
487
+ </tbody></table></div>`;
486
488
  }
487
489
  if (ep.queryParams?.length) {
488
- html += `<div class="panel-section">
489
- <div class="section-label">Query Parameters</div>
490
- <table class="ptable">
491
- <thead><tr><th>Name</th><th>Type</th><th>Description</th><th>Example</th></tr></thead>
492
- <tbody>${ep.queryParams.map(q => `
493
- <tr>
494
- <td><span class="pname">${q.name}</span></td>
495
- <td><span class="ptype">${q.type}</span></td>
496
- <td class="pdesc">${q.description}</td>
497
- <td><span class="ptype">${q.example}</span></td>
498
- </tr>`).join('')}
499
- </tbody>
500
- </table></div>`;
490
+ h += `<div class="panel-section"><div class="section-label">Query Parameters</div>
491
+ <table class="ptable"><thead><tr><th>NAME</th><th>TYPE</th><th>DESCRIPTION</th><th>EXAMPLE</th></tr></thead><tbody>
492
+ ${ep.queryParams.map(q=>`<tr><td><span class="pname">${q.name}</span></td><td><span class="ptype">${q.type}</span></td><td class="pdesc">${q.description}</td><td><span class="pex">${q.example}</span></td></tr>`).join('')}
493
+ </tbody></table></div>`;
501
494
  }
502
- return html;
495
+ return h;
503
496
  }
504
497
 
505
498
  if (tab === 'body') {
506
- const schema = ep.requestBody?.schema || {};
507
- const example = JSON.stringify(
508
- Object.fromEntries(Object.entries(schema).map(([k, v]) => [k, v.example ?? ''])),
509
- null, 2
510
- );
511
- return `<div class="panel-section">
512
- <div class="section-label">Schema</div>
513
- <table class="ptable">
514
- <thead><tr><th>Field</th><th>Type</th><th>Required</th><th>Example</th></tr></thead>
515
- <tbody>${Object.entries(schema).map(([k, v]) => `
516
- <tr>
517
- <td><span class="pname">${k}</span></td>
518
- <td><span class="ptype">${v.type}</span></td>
519
- <td>${v.required ? '<span class="req-tag">required</span>' : '<span style="color:var(--text-muted)">—</span>'}</td>
520
- <td><span class="ptype">${v.example ?? ''}</span></td>
521
- </tr>`).join('')}
522
- </tbody>
523
- </table></div>
524
- <div class="panel-section">
525
- <div class="section-label">Example JSON</div>
526
- <pre>${example}</pre>
527
- </div>`;
499
+ const schema = ep.requestBody?.schema||{};
500
+ return `<div class="panel-section"><div class="section-label">Schema</div>
501
+ <table class="ptable"><thead><tr><th>FIELD</th><th>TYPE</th><th>REQUIRED</th><th>DESCRIPTION</th></tr></thead><tbody>
502
+ ${Object.entries(schema).map(([k,v])=>`<tr><td><span class="pname">${k}</span></td><td><span class="ptype">${v.type}</span></td><td>${v.required?'<span class="req-tag">YES</span>':'<span style="color:var(--text-muted)">—</span>'}</td><td class="pdesc">${v.description||''}</td></tr>`).join('')}
503
+ </tbody></table></div>
504
+ <div class="panel-section"><div class="section-label">Example Body</div><pre>${bodyEx}</pre></div>`;
528
505
  }
529
506
 
530
507
  if (tab === 'responses') {
531
- return `<div class="panel-section">${Object.entries(ep.responses || {}).map(([code, r]) => {
532
- const cls = code.startsWith('2') ? 's2' : code.startsWith('4') ? 's4' : 's5';
533
- return `<div class="res-row">
534
- <span class="sc ${cls}">${code}</span>
535
- <div class="res-info">
536
- <div class="res-desc">${r.description}</div>
537
- ${r.example ? `<pre style="margin-top:.5rem;font-size:.75rem">${JSON.stringify(r.example,null,2)}</pre>` : ''}
538
- </div>
539
- </div>`;
508
+ return `<div class="panel-section">${Object.entries(ep.responses||{}).map(([code,r])=>{
509
+ const cls = code.startsWith('2')?'s2':code.startsWith('4')?'s4':'s5';
510
+ return `<div class="res-row"><span class="sc ${cls}">${code}</span><div><div class="res-desc">${r.description}</div>${r.example?`<pre style="margin-top:.5rem;font-size:.73rem">${JSON.stringify(r.example,null,2)}</pre>`:''}</div></div>`;
540
511
  }).join('')}</div>`;
541
512
  }
542
513
 
514
+ if (tab === 'examples') {
515
+ const url = `${spec.baseUrl}${ep.path}`;
516
+ const hasBody = ['POST','PUT','PATCH'].includes(ep.method) && Object.keys(ep.requestBody?.schema||{}).length;
517
+ const authHeader = ep.requiresAuth ? `\n "Authorization": "Bearer {YOUR_KEY}",` : '';
518
+ const nodeEx = `import fetch from 'node-fetch';\n\nconst url = "${url}";\nconst headers = {${authHeader}\n "Accept": "application/json"${hasBody?',\n "Content-Type": "application/json"':''}\n};\n${hasBody?`\nconst body = ${bodyEx};\n`:''}
519
+ const response = await fetch(url, {\n method: "${ep.method}",\n headers,${hasBody?'\n body: JSON.stringify(body),':''}\n});\nconst data = await response.json();\nconsole.log(data);`;
520
+
521
+ const pyEx = `import requests\n\nurl = "${url}"\nheaders = {${ep.requiresAuth?`\n "Authorization": "Bearer {YOUR_KEY}",`:''}\n "Accept": "application/json"\n}${hasBody?`\n\nbody = ${bodyEx}`:''}
522
+ \nresponse = requests.${ep.method.toLowerCase()}(url, headers=headers${hasBody?', json=body':''})\nprint(response.json())`;
523
+
524
+ const curlEx = `curl -X ${ep.method} "${url}" \\${ep.requiresAuth?`\n -H "Authorization: Bearer {YOUR_KEY}" \\`:''}\n -H "Accept: application/json"${hasBody?` \\\n -H "Content-Type: application/json" \\\n -d '${bodyEx.replace(/\n/g,'').replace(/ /g,'')}'`:''}`;
525
+
526
+ return `<div class="panel-section">
527
+ <div class="section-label">Request Example</div>
528
+ <div class="code-lang-tabs">
529
+ <button class="lang-tab on" onclick="switchLang(this,'lang-node-${ep.id}')">Node.js</button>
530
+ <button class="lang-tab" onclick="switchLang(this,'lang-py-${ep.id}')">Python</button>
531
+ <button class="lang-tab" onclick="switchLang(this,'lang-curl-${ep.id}')">cURL</button>
532
+ </div>
533
+ <pre class="lang-pre" id="lang-node-${ep.id}">${nodeEx}</pre>
534
+ <pre class="lang-pre" id="lang-py-${ep.id}" style="display:none">${pyEx}</pre>
535
+ <pre class="lang-pre" id="lang-curl-${ep.id}" style="display:none">${curlEx}</pre>
536
+ </div>`;
537
+ }
538
+
543
539
  if (tab === 'try') {
544
- const schema = ep.requestBody?.schema || {};
545
- const hasBody = ['POST','PUT','PATCH'].includes(ep.method) && Object.keys(schema).length;
546
- const bodyEx = JSON.stringify(
547
- Object.fromEntries(Object.entries(schema).map(([k,v]) => [k, v.example ?? ''])),
548
- null, 2
549
- );
540
+ const hasBody = ['POST','PUT','PATCH'].includes(ep.method) && Object.keys(ep.requestBody?.schema||{}).length;
550
541
  return `<div class="try-layout">
551
542
  <div class="panel-section">
552
543
  <div class="section-label">URL</div>
553
544
  <div class="try-url-row">
554
- <span class="m-pill m-${ep.method}" style="font-size:.7rem">${ep.method}</span>
545
+ <span class="m-pill m-${ep.method}">${ep.method}</span>
555
546
  <input type="text" id="try-url-${ep.id}" value="${spec.baseUrl}${ep.path}" />
556
- <button class="send-btn" onclick="doSend('${ep.id}','${ep.method}',${!!hasBody})">Send</button>
547
+ <button class="send-btn" onclick="doSend('${ep.id}','${ep.method}',${!!hasBody})">▶ Send</button>
557
548
  </div>
558
549
  </div>
559
- ${ep.requiresAuth ? `<div class="panel-section">
560
- <div class="section-label">Bearer Token</div>
561
- <div class="try-token-row"><span>Bearer</span><input type="text" id="try-tok-${ep.id}" placeholder="Paste your token…" /></div>
562
- </div>` : ''}
563
- ${hasBody ? `<div class="panel-section">
564
- <div class="section-label">Request Body</div>
565
- <textarea class="try-textarea" id="try-body-${ep.id}">${bodyEx}</textarea>
566
- </div>` : ''}
550
+ ${ep.requiresAuth?`<div class="panel-section"><div class="section-label">Bearer Token</div>
551
+ <div class="try-token-row"><span>Bearer</span><input type="text" id="try-tok-${ep.id}" placeholder="Paste your token…"/></div></div>`:''}
552
+ ${hasBody?`<div class="panel-section"><div class="section-label">Request Body</div>
553
+ <textarea class="try-textarea" id="try-body-${ep.id}">${bodyEx}</textarea></div>`:''}
567
554
  <div class="panel-section">
568
555
  <div class="section-label">Response</div>
569
556
  <div class="res-output" id="try-res-${ep.id}">
570
557
  <div class="res-stat" id="try-stat-${ep.id}"></div>
571
- <div id="try-body-out-${ep.id}"></div>
558
+ <div id="try-out-${ep.id}"></div>
572
559
  </div>
573
560
  </div>
574
561
  </div>`;
@@ -576,76 +563,49 @@
576
563
  return '';
577
564
  }
578
565
 
566
+ function switchLang(btn, panelId) {
567
+ const section = btn.closest('.panel-section');
568
+ section.querySelectorAll('.lang-tab').forEach(b=>b.classList.remove('on'));
569
+ btn.classList.add('on');
570
+ section.querySelectorAll('.lang-pre').forEach(p=>p.style.display='none');
571
+ document.getElementById(panelId).style.display='block';
572
+ }
573
+
579
574
  async function doSend(id, method, hasBody) {
580
575
  const url = document.getElementById(`try-url-${id}`)?.value;
581
576
  const tok = document.getElementById(`try-tok-${id}`)?.value;
582
577
  const bodyEl = document.getElementById(`try-body-${id}`);
583
578
  const resBox = document.getElementById(`try-res-${id}`);
584
579
  const statEl = document.getElementById(`try-stat-${id}`);
585
- const bodyOut = document.getElementById(`try-body-out-${id}`);
586
-
587
- resBox.classList.add('show');
588
- bodyOut.textContent = 'Sending…';
589
- statEl.innerHTML = '';
590
-
591
- const headers = { 'Content-Type': 'application/json' };
592
- if (tok) headers['Authorization'] = `Bearer ${tok}`;
593
- const opts = { method, headers };
594
- if (hasBody && bodyEl) { try { opts.body = JSON.stringify(JSON.parse(bodyEl.value)); } catch { opts.body = bodyEl.value; } }
595
-
580
+ const outEl = document.getElementById(`try-out-${id}`);
581
+ resBox.classList.add('show'); outEl.textContent='Sending…'; statEl.innerHTML='';
582
+ const headers = {'Content-Type':'application/json'};
583
+ if (tok) headers['Authorization']=`Bearer ${tok}`;
584
+ const opts = {method,headers};
585
+ if (hasBody && bodyEl) { try{opts.body=JSON.stringify(JSON.parse(bodyEl.value));}catch{opts.body=bodyEl.value;} }
596
586
  try {
597
- const r = await fetch(url, opts);
587
+ const r = await fetch(url,opts);
598
588
  const txt = await r.text();
599
- let display; try { display = JSON.stringify(JSON.parse(txt), null, 2); } catch { display = txt; }
600
- statEl.innerHTML = `<span class="${r.ok?'ok':'bad'}">● ${r.status} ${r.statusText}</span>`;
601
- bodyOut.textContent = display;
602
- } catch(e) {
603
- statEl.innerHTML = `<span class="bad">● Network Error</span>`;
604
- bodyOut.textContent = e.message;
605
- }
589
+ let display; try{display=JSON.stringify(JSON.parse(txt),null,2);}catch{display=txt;}
590
+ statEl.innerHTML=`<span class="${r.ok?'ok':'bad'}">● ${r.status} ${r.statusText}</span>`;
591
+ outEl.textContent=display;
592
+ } catch(e) { statEl.innerHTML=`<span class="bad">● Network Error</span>`; outEl.textContent=e.message; }
606
593
  }
607
594
 
608
- // ── Rescan ───────────────────────────────────────────────────────────────
609
595
  async function triggerRescan(full) {
610
- const url = full ? '/api/rescan/full' : '/api/rescan';
611
- await fetch(url, { method: 'POST' });
612
- toast(full ? 'Full rescan started…' : 'Checking for new endpoints…');
613
- clearTimeout(pollTimer);
614
- pollStatus();
596
+ await fetch(full?'/api/rescan/full':'/api/rescan', {method:'POST'});
597
+ toast(full?'Full rescan started…':'Checking for new endpoints…');
598
+ clearTimeout(pollTimer); pollStatus();
615
599
  }
616
600
 
617
- // ── Exports ──────────────────────────────────────────────────────────────
618
- async function exportPostman() {
619
- if (!spec) return toast('Not ready yet');
620
- const r = await fetch('/api/export/postman');
621
- const blob = await r.blob();
622
- dl(blob, `${spec.projectName.replace(/\s+/g,'-')}-postman.json`);
623
- toast('Postman collection downloaded!');
624
- }
625
- async function exportSwagger() {
626
- if (!spec) return toast('Not ready yet');
627
- const r = await fetch('/api/export/swagger');
628
- const blob = await r.blob();
629
- dl(blob, `${spec.projectName.replace(/\s+/g,'-')}-openapi.yaml`);
630
- toast('Swagger YAML downloaded!');
601
+ function showError(msg) {
602
+ document.getElementById('mainPanel').innerHTML=`<div class="state-center"><div style="font-size:2rem">⚠️</div><div class="state-title" style="color:var(--DELETE)">Error</div><div class="state-sub">${msg}</div></div>`;
631
603
  }
632
- function dl(blob, name) { const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = name; a.click(); }
633
604
 
634
- // ── Toast ────────────────────────────────────────────────────────────────
635
605
  function toast(msg) {
636
- const t = document.createElement('div');
637
- t.className = 'toast'; t.textContent = msg;
606
+ const t=document.createElement('div'); t.className='toast'; t.textContent=msg;
638
607
  document.getElementById('toastContainer').appendChild(t);
639
- setTimeout(() => t.remove(), 3000);
640
- }
641
-
642
- function showError(msg) {
643
- document.getElementById('mainPanel').innerHTML = `
644
- <div class="state-center">
645
- <div style="font-size:2.5rem">⚠️</div>
646
- <div class="state-title" style="color:var(--DELETE)">Analysis Error</div>
647
- <div class="state-sub">${msg}</div>
648
- </div>`;
608
+ setTimeout(()=>t.remove(),3000);
649
609
  }
650
610
 
651
611
  init();