rails_map 1.1.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 (39) hide show
  1. checksums.yaml +7 -0
  2. data/.idea/.gitignore +8 -0
  3. data/.idea/material_theme_project_new.xml +12 -0
  4. data/AUTHENTICATION.md +221 -0
  5. data/CHANGELOG.md +75 -0
  6. data/Gemfile +10 -0
  7. data/LICENSE.txt +21 -0
  8. data/QUICKSTART.md +156 -0
  9. data/README.md +178 -0
  10. data/Rakefile +8 -0
  11. data/app/controllers/rails_map/docs_controller.rb +73 -0
  12. data/app/models/rails_map/user.rb +12 -0
  13. data/app/views/rails_map/docs/_styles.html.erb +489 -0
  14. data/app/views/rails_map/docs/controller.html.erb +137 -0
  15. data/app/views/rails_map/docs/index.html.erb +212 -0
  16. data/app/views/rails_map/docs/model.html.erb +214 -0
  17. data/app/views/rails_map/docs/routes.html.erb +139 -0
  18. data/config/initializers/rails_map.example.rb +44 -0
  19. data/config/routes.rb +9 -0
  20. data/docs/index.html +1354 -0
  21. data/lib/generators/rails_map/install_generator.rb +49 -0
  22. data/lib/generators/rails_map/templates/README +47 -0
  23. data/lib/generators/rails_map/templates/initializer.rb +38 -0
  24. data/lib/generators/rails_map/templates/migration.rb +14 -0
  25. data/lib/rails_map/auth.rb +15 -0
  26. data/lib/rails_map/configuration.rb +35 -0
  27. data/lib/rails_map/engine.rb +11 -0
  28. data/lib/rails_map/generators/html_generator.rb +120 -0
  29. data/lib/rails_map/parsers/model_parser.rb +257 -0
  30. data/lib/rails_map/parsers/route_parser.rb +356 -0
  31. data/lib/rails_map/railtie.rb +46 -0
  32. data/lib/rails_map/version.rb +5 -0
  33. data/lib/rails_map.rb +66 -0
  34. data/templates/controller.html.erb +74 -0
  35. data/templates/index.html.erb +64 -0
  36. data/templates/layout.html.erb +289 -0
  37. data/templates/model.html.erb +219 -0
  38. data/templates/routes.html.erb +86 -0
  39. metadata +144 -0
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsMap
4
+ class DocsController < ActionController::Base
5
+ layout false
6
+ before_action :load_configuration
7
+ before_action :authenticate_access!
8
+
9
+ def index
10
+ @routes = Parsers::RouteParser.new.parse
11
+ @models = Parsers::ModelParser.new.parse
12
+
13
+ # Debug logging
14
+ Rails.logger.info "RailsMap: Found #{@routes&.size || 0} controllers"
15
+ Rails.logger.info "RailsMap: Found #{@models&.size || 0} models"
16
+
17
+ # Ensure we always have a hash, never nil
18
+ @routes ||= {}
19
+ @models ||= {}
20
+ rescue => e
21
+ @routes = {}
22
+ @models = {}
23
+ Rails.logger.error "RailsMap Error: #{e.message}"
24
+ Rails.logger.error e.backtrace.join("\n")
25
+ end
26
+
27
+ def routes
28
+ @routes = Parsers::RouteParser.new.parse
29
+ @routes ||= {}
30
+ rescue => e
31
+ @routes = {}
32
+ Rails.logger.error "RailsMap Error: #{e.message}"
33
+ Rails.logger.error e.backtrace.join("\n")
34
+ end
35
+
36
+ def controller
37
+ all_routes = Parsers::RouteParser.new.parse || {}
38
+ @controller_name = params[:name]
39
+ @data = all_routes[@controller_name]
40
+
41
+ if @data.nil?
42
+ render plain: "Controller not found", status: :not_found
43
+ end
44
+ rescue => e
45
+ render plain: "Error loading controller: #{e.message}", status: :internal_server_error
46
+ end
47
+
48
+ def model
49
+ all_models = Parsers::ModelParser.new.parse || {}
50
+ @model_name = params[:name]
51
+ @model = all_models[@model_name]
52
+
53
+ if @model.nil?
54
+ render plain: "Model not found", status: :not_found
55
+ end
56
+ rescue => e
57
+ render plain: "Error loading model: #{e.message}", status: :internal_server_error
58
+ end
59
+
60
+ private
61
+
62
+ def load_configuration
63
+ @config = RailsMap.configuration
64
+ end
65
+
66
+ def authenticate_access!
67
+ # Allow host app to define authentication logic
68
+ if @config.authenticate_with.present?
69
+ instance_eval(&@config.authenticate_with)
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsMap
4
+ class User < ApplicationRecord
5
+ self.table_name = 'rails_map_users'
6
+
7
+ has_secure_password
8
+
9
+ validates :username, presence: true, uniqueness: true
10
+ validates :password, length: { minimum: 6 }, if: -> { password.present? }
11
+ end
12
+ end
@@ -0,0 +1,489 @@
1
+ <style>
2
+ :root {
3
+ --primary-color: <%= @config.theme_color %>;
4
+ --primary-dark: color-mix(in srgb, <%= @config.theme_color %> 85%, black);
5
+ --primary-light: color-mix(in srgb, <%= @config.theme_color %> 15%, white);
6
+ --bg-color: #0f0f10;
7
+ --bg-secondary: #18181b;
8
+ --card-bg: #1c1c21;
9
+ --card-bg-hover: #232329;
10
+ --text-color: #fafafa;
11
+ --text-muted: #a1a1aa;
12
+ --text-dim: #71717a;
13
+ --border-color: #27272a;
14
+ --border-hover: #3f3f46;
15
+ --success-color: #22c55e;
16
+ --warning-color: #f59e0b;
17
+ --danger-color: #ef4444;
18
+ --gradient-start: <%= @config.theme_color %>;
19
+ --gradient-end: color-mix(in srgb, <%= @config.theme_color %> 60%, #8b5cf6);
20
+ }
21
+
22
+ * { box-sizing: border-box; margin: 0; padding: 0; }
23
+
24
+ body {
25
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
26
+ background-color: var(--bg-color);
27
+ color: var(--text-color);
28
+ line-height: 1.6;
29
+ min-height: 100vh;
30
+ }
31
+
32
+ .container { max-width: 1400px; margin: 0 auto; padding: 2rem; }
33
+
34
+ /* Header */
35
+ .header {
36
+ background: var(--bg-secondary);
37
+ border-bottom: 1px solid var(--border-color);
38
+ padding: 3rem 0;
39
+ position: relative;
40
+ overflow: hidden;
41
+ }
42
+ .header::before {
43
+ content: '';
44
+ position: absolute;
45
+ top: 0;
46
+ left: 0;
47
+ right: 0;
48
+ height: 3px;
49
+ background: linear-gradient(90deg, var(--gradient-start), var(--gradient-end));
50
+ }
51
+ .header-content {
52
+ display: flex;
53
+ justify-content: space-between;
54
+ align-items: center;
55
+ }
56
+ .header h1 {
57
+ font-size: 1.75rem;
58
+ font-weight: 700;
59
+ letter-spacing: -0.025em;
60
+ background: linear-gradient(135deg, var(--text-color), var(--text-muted));
61
+ -webkit-background-clip: text;
62
+ -webkit-text-fill-color: transparent;
63
+ background-clip: text;
64
+ }
65
+ .header .subtitle {
66
+ color: var(--text-muted);
67
+ margin-top: 0.375rem;
68
+ font-size: 0.9375rem;
69
+ }
70
+ .header-badge {
71
+ display: inline-flex;
72
+ align-items: center;
73
+ gap: 0.5rem;
74
+ padding: 0.5rem 1rem;
75
+ background: var(--primary-light);
76
+ border: 1px solid color-mix(in srgb, var(--primary-color) 30%, transparent);
77
+ border-radius: 2rem;
78
+ font-size: 0.8125rem;
79
+ font-weight: 500;
80
+ color: var(--primary-color);
81
+ }
82
+
83
+ /* Search */
84
+ .search-wrapper {
85
+ position: relative;
86
+ margin-bottom: 2rem;
87
+ }
88
+ .search-icon {
89
+ position: absolute;
90
+ left: 1rem;
91
+ top: 50%;
92
+ transform: translateY(-50%);
93
+ color: var(--text-dim);
94
+ pointer-events: none;
95
+ }
96
+ .search-input {
97
+ width: 100%;
98
+ padding: 0.875rem 1rem 0.875rem 2.75rem;
99
+ font-size: 0.9375rem;
100
+ border: 1px solid var(--border-color);
101
+ border-radius: 0.75rem;
102
+ background: var(--card-bg);
103
+ color: var(--text-color);
104
+ transition: all 0.2s ease;
105
+ }
106
+ .search-input::placeholder { color: var(--text-dim); }
107
+ .search-input:focus {
108
+ outline: none;
109
+ border-color: var(--primary-color);
110
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary-color) 20%, transparent);
111
+ }
112
+
113
+ /* Stats */
114
+ .stats-grid {
115
+ display: grid;
116
+ grid-template-columns: repeat(3, 1fr);
117
+ gap: 1rem;
118
+ margin-bottom: 2.5rem;
119
+ }
120
+ .stat-card {
121
+ background: var(--card-bg);
122
+ border: 1px solid var(--border-color);
123
+ border-radius: 1rem;
124
+ padding: 1.5rem;
125
+ text-align: center;
126
+ transition: all 0.2s ease;
127
+ }
128
+ .stat-card:hover {
129
+ border-color: var(--border-hover);
130
+ background: var(--card-bg-hover);
131
+ }
132
+ .stat-value {
133
+ font-size: 2.5rem;
134
+ font-weight: 700;
135
+ letter-spacing: -0.025em;
136
+ background: linear-gradient(135deg, var(--primary-color), var(--gradient-end));
137
+ -webkit-background-clip: text;
138
+ -webkit-text-fill-color: transparent;
139
+ background-clip: text;
140
+ }
141
+ .stat-label {
142
+ font-size: 0.8125rem;
143
+ font-weight: 500;
144
+ color: var(--text-muted);
145
+ text-transform: uppercase;
146
+ letter-spacing: 0.05em;
147
+ margin-top: 0.25rem;
148
+ }
149
+
150
+ /* Navigation */
151
+ .nav {
152
+ display: flex;
153
+ gap: 0.75rem;
154
+ margin-bottom: 2rem;
155
+ flex-wrap: wrap;
156
+ }
157
+ .nav a {
158
+ display: inline-flex;
159
+ align-items: center;
160
+ gap: 0.5rem;
161
+ padding: 0.625rem 1.25rem;
162
+ background: var(--card-bg);
163
+ border: 1px solid var(--border-color);
164
+ border-radius: 0.5rem;
165
+ color: var(--text-muted);
166
+ text-decoration: none;
167
+ font-size: 0.875rem;
168
+ font-weight: 500;
169
+ transition: all 0.2s ease;
170
+ }
171
+ .nav a:hover {
172
+ background: var(--primary-color);
173
+ color: white;
174
+ border-color: var(--primary-color);
175
+ transform: translateY(-1px);
176
+ }
177
+
178
+ /* Section Headers */
179
+ .section {
180
+ margin-bottom: 3rem;
181
+ }
182
+ .section-header {
183
+ display: flex;
184
+ align-items: center;
185
+ gap: 0.75rem;
186
+ font-size: 1.125rem;
187
+ font-weight: 600;
188
+ margin-bottom: 1.25rem;
189
+ color: var(--text-color);
190
+ }
191
+ .section-header::before {
192
+ content: '';
193
+ width: 4px;
194
+ height: 1.25rem;
195
+ background: linear-gradient(180deg, var(--primary-color), var(--gradient-end));
196
+ border-radius: 2px;
197
+ }
198
+ .section-count {
199
+ font-size: 0.8125rem;
200
+ font-weight: 500;
201
+ color: var(--text-dim);
202
+ background: var(--bg-secondary);
203
+ padding: 0.25rem 0.625rem;
204
+ border-radius: 1rem;
205
+ margin-left: 0.5rem;
206
+ }
207
+
208
+ /* Cards Grid */
209
+ .grid {
210
+ display: grid;
211
+ grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
212
+ gap: 1rem;
213
+ }
214
+
215
+ /* Card */
216
+ .card {
217
+ background: var(--card-bg);
218
+ border: 1px solid var(--border-color);
219
+ border-radius: 1rem;
220
+ padding: 1.25rem;
221
+ transition: all 0.25s ease;
222
+ display: flex;
223
+ flex-direction: column;
224
+ position: relative;
225
+ overflow: hidden;
226
+ }
227
+ .card::before {
228
+ content: '';
229
+ position: absolute;
230
+ top: 0;
231
+ left: 0;
232
+ right: 0;
233
+ height: 2px;
234
+ background: linear-gradient(90deg, var(--primary-color), var(--gradient-end));
235
+ opacity: 0;
236
+ transition: opacity 0.25s ease;
237
+ }
238
+ .card:hover {
239
+ border-color: var(--border-hover);
240
+ background: var(--card-bg-hover);
241
+ transform: translateY(-2px);
242
+ }
243
+ .card:hover::before { opacity: 1; }
244
+
245
+ .card-header {
246
+ display: flex;
247
+ justify-content: space-between;
248
+ align-items: flex-start;
249
+ margin-bottom: 1rem;
250
+ gap: 0.75rem;
251
+ }
252
+ .card-title {
253
+ font-size: 1rem;
254
+ font-weight: 600;
255
+ color: var(--text-color);
256
+ overflow: hidden;
257
+ text-overflow: ellipsis;
258
+ white-space: nowrap;
259
+ flex: 1;
260
+ }
261
+
262
+ .card-meta {
263
+ display: flex;
264
+ flex-direction: column;
265
+ gap: 0.75rem;
266
+ flex: 1;
267
+ }
268
+ .card-meta-item {
269
+ display: flex;
270
+ align-items: center;
271
+ gap: 0.5rem;
272
+ }
273
+ .card-meta-label {
274
+ font-size: 0.6875rem;
275
+ font-weight: 600;
276
+ color: var(--text-dim);
277
+ text-transform: uppercase;
278
+ letter-spacing: 0.05em;
279
+ }
280
+ .card-meta-value {
281
+ font-family: 'JetBrains Mono', 'Fira Code', monospace;
282
+ font-size: 0.8125rem;
283
+ color: var(--text-muted);
284
+ background: var(--bg-secondary);
285
+ padding: 0.25rem 0.5rem;
286
+ border-radius: 0.375rem;
287
+ }
288
+
289
+ .card-stats {
290
+ display: flex;
291
+ gap: 1.5rem;
292
+ padding: 0.875rem 0;
293
+ border-top: 1px solid var(--border-color);
294
+ margin-top: auto;
295
+ }
296
+ .card-stat {
297
+ display: flex;
298
+ flex-direction: column;
299
+ }
300
+ .card-stat-value {
301
+ font-size: 1.25rem;
302
+ font-weight: 700;
303
+ color: var(--text-color);
304
+ }
305
+ .card-stat-label {
306
+ font-size: 0.6875rem;
307
+ color: var(--text-dim);
308
+ text-transform: uppercase;
309
+ letter-spacing: 0.05em;
310
+ }
311
+
312
+ .card-footer {
313
+ display: flex;
314
+ justify-content: space-between;
315
+ align-items: center;
316
+ padding-top: 0.875rem;
317
+ border-top: 1px solid var(--border-color);
318
+ margin-top: 0.875rem;
319
+ }
320
+ .card-actions {
321
+ font-size: 0.75rem;
322
+ color: var(--text-dim);
323
+ }
324
+ .card-link {
325
+ display: inline-flex;
326
+ align-items: center;
327
+ gap: 0.375rem;
328
+ font-size: 0.8125rem;
329
+ font-weight: 500;
330
+ color: var(--primary-color);
331
+ text-decoration: none;
332
+ transition: all 0.2s ease;
333
+ }
334
+ .card-link:hover {
335
+ gap: 0.5rem;
336
+ }
337
+ .card-link svg {
338
+ transition: transform 0.2s ease;
339
+ }
340
+ .card-link:hover svg {
341
+ transform: translateX(2px);
342
+ }
343
+
344
+ /* Badges */
345
+ .badge {
346
+ display: inline-flex;
347
+ align-items: center;
348
+ padding: 0.25rem 0.625rem;
349
+ border-radius: 0.375rem;
350
+ font-size: 0.6875rem;
351
+ font-weight: 600;
352
+ text-transform: uppercase;
353
+ letter-spacing: 0.025em;
354
+ }
355
+ .badge-count {
356
+ background: var(--bg-secondary);
357
+ color: var(--text-muted);
358
+ border: 1px solid var(--border-color);
359
+ }
360
+ .badge-get { background: rgba(34, 197, 94, 0.15); color: #4ade80; }
361
+ .badge-post { background: rgba(59, 130, 246, 0.15); color: #60a5fa; }
362
+ .badge-put, .badge-patch { background: rgba(245, 158, 11, 0.15); color: #fbbf24; }
363
+ .badge-delete { background: rgba(239, 68, 68, 0.15); color: #f87171; }
364
+ .badge-any { background: var(--bg-secondary); color: var(--text-muted); }
365
+ .badge-belongs-to { background: rgba(139, 92, 246, 0.15); color: #a78bfa; }
366
+ .badge-has-many { background: rgba(6, 182, 212, 0.15); color: #22d3ee; }
367
+ .badge-has-one { background: rgba(236, 72, 153, 0.15); color: #f472b6; }
368
+
369
+ /* Table */
370
+ .table-container {
371
+ overflow-x: auto;
372
+ background: var(--card-bg);
373
+ border: 1px solid var(--border-color);
374
+ border-radius: 1rem;
375
+ }
376
+ table { width: 100%; border-collapse: collapse; }
377
+ th, td {
378
+ padding: 0.875rem 1.25rem;
379
+ text-align: left;
380
+ border-bottom: 1px solid var(--border-color);
381
+ }
382
+ th {
383
+ background: var(--bg-secondary);
384
+ font-weight: 600;
385
+ font-size: 0.75rem;
386
+ text-transform: uppercase;
387
+ letter-spacing: 0.05em;
388
+ color: var(--text-dim);
389
+ }
390
+ tr:last-child td { border-bottom: none; }
391
+ tr:hover td { background: var(--card-bg-hover); }
392
+
393
+ .type-badge {
394
+ font-family: 'JetBrains Mono', 'Fira Code', monospace;
395
+ font-size: 0.75rem;
396
+ padding: 0.1875rem 0.5rem;
397
+ background: var(--bg-secondary);
398
+ border-radius: 0.25rem;
399
+ color: var(--text-muted);
400
+ }
401
+
402
+ code {
403
+ font-family: 'JetBrains Mono', 'Fira Code', monospace;
404
+ font-size: 0.8125rem;
405
+ background: var(--bg-secondary);
406
+ padding: 0.1875rem 0.5rem;
407
+ border-radius: 0.375rem;
408
+ color: var(--primary-color);
409
+ }
410
+
411
+ /* Breadcrumb */
412
+ .breadcrumb {
413
+ display: flex;
414
+ align-items: center;
415
+ gap: 0.5rem;
416
+ margin-bottom: 1.5rem;
417
+ font-size: 0.875rem;
418
+ }
419
+ .breadcrumb a {
420
+ color: var(--text-muted);
421
+ text-decoration: none;
422
+ transition: color 0.2s ease;
423
+ }
424
+ .breadcrumb a:hover { color: var(--primary-color); }
425
+ .breadcrumb-separator { color: var(--text-dim); }
426
+ .breadcrumb-current { color: var(--text-color); font-weight: 500; }
427
+
428
+ /* Links */
429
+ a.link {
430
+ color: var(--primary-color);
431
+ text-decoration: none;
432
+ transition: all 0.2s ease;
433
+ }
434
+ a.link:hover { text-decoration: underline; }
435
+
436
+ /* Empty State */
437
+ .empty-state {
438
+ text-align: center;
439
+ padding: 4rem 2rem;
440
+ color: var(--text-dim);
441
+ background: var(--card-bg);
442
+ border: 1px dashed var(--border-color);
443
+ border-radius: 1rem;
444
+ }
445
+ .empty-state-icon {
446
+ font-size: 3rem;
447
+ margin-bottom: 1rem;
448
+ opacity: 0.5;
449
+ }
450
+
451
+ /* Footer */
452
+ .footer {
453
+ text-align: center;
454
+ padding: 2.5rem 2rem;
455
+ color: var(--text-dim);
456
+ font-size: 0.8125rem;
457
+ border-top: 1px solid var(--border-color);
458
+ margin-top: 3rem;
459
+ }
460
+ .footer a {
461
+ color: var(--primary-color);
462
+ text-decoration: none;
463
+ }
464
+ .footer a:hover { text-decoration: underline; }
465
+
466
+ /* Responsive */
467
+ @media (max-width: 768px) {
468
+ .container { padding: 1rem; }
469
+ .header { padding: 2rem 0; }
470
+ .header-content { flex-direction: column; align-items: flex-start; gap: 1rem; }
471
+ .grid { grid-template-columns: 1fr; }
472
+ .stats-grid { grid-template-columns: 1fr; }
473
+ .stat-card { padding: 1.25rem; }
474
+ .stat-value { font-size: 2rem; }
475
+ }
476
+
477
+ /* Animations */
478
+ @keyframes fadeIn {
479
+ from { opacity: 0; transform: translateY(10px); }
480
+ to { opacity: 1; transform: translateY(0); }
481
+ }
482
+ .card { animation: fadeIn 0.3s ease forwards; }
483
+ .grid .card:nth-child(1) { animation-delay: 0.05s; }
484
+ .grid .card:nth-child(2) { animation-delay: 0.1s; }
485
+ .grid .card:nth-child(3) { animation-delay: 0.15s; }
486
+ .grid .card:nth-child(4) { animation-delay: 0.2s; }
487
+ .grid .card:nth-child(5) { animation-delay: 0.25s; }
488
+ .grid .card:nth-child(6) { animation-delay: 0.3s; }
489
+ </style>
@@ -0,0 +1,137 @@
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.0">
6
+ <title><%= @controller_name.camelize %>Controller - <%= @config.app_name %></title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
10
+ <%= render partial: 'rails_map/docs/styles' %>
11
+ </head>
12
+ <body>
13
+ <header class="header">
14
+ <div class="container">
15
+ <div class="header-content">
16
+ <div>
17
+ <h1><%= @config.app_name %></h1>
18
+ <p class="subtitle">API Documentation</p>
19
+ </div>
20
+ </div>
21
+ </div>
22
+ </header>
23
+
24
+ <main class="container">
25
+ <div class="breadcrumb">
26
+ <%= link_to "Home", rails_map.root_path %>
27
+ <span class="breadcrumb-separator">/</span>
28
+ <%= link_to "Routes", rails_map.routes_path %>
29
+ <span class="breadcrumb-separator">/</span>
30
+ <span class="breadcrumb-current"><%= @controller_name.camelize %></span>
31
+ </div>
32
+
33
+ <div class="stats-grid" style="grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); margin-bottom: 2rem;">
34
+ <div class="stat-card">
35
+ <div class="stat-value"><%= @data[:routes].size %></div>
36
+ <div class="stat-label">Routes</div>
37
+ </div>
38
+ <div class="stat-card">
39
+ <div class="stat-value"><%= @data[:actions].size %></div>
40
+ <div class="stat-label">Actions</div>
41
+ </div>
42
+ <div class="stat-card" style="text-align: left; padding: 1.25rem;">
43
+ <div class="stat-label" style="margin-bottom: 0.5rem;">Base Path</div>
44
+ <code style="font-size: 1rem;"><%= @data[:base_path] || '/' %></code>
45
+ </div>
46
+ </div>
47
+
48
+ <section class="section">
49
+ <h2 class="section-header">Actions</h2>
50
+ <div style="display: flex; flex-wrap: wrap; gap: 0.5rem; margin-bottom: 2rem;">
51
+ <% @data[:actions].each do |action| %>
52
+ <span class="badge badge-count" style="font-size: 0.8125rem; padding: 0.375rem 0.75rem;"><%= action %></span>
53
+ <% end %>
54
+ </div>
55
+ </section>
56
+
57
+ <section class="section">
58
+ <h2 class="section-header">Routes</h2>
59
+ <% if @data[:routes].any? %>
60
+ <div class="table-container">
61
+ <table>
62
+ <thead>
63
+ <tr>
64
+ <th style="width: 100px;">Method</th>
65
+ <th>Path</th>
66
+ <th>Action</th>
67
+ <th>Parameters</th>
68
+ </tr>
69
+ </thead>
70
+ <tbody>
71
+ <% @data[:routes].each do |route| %>
72
+ <tr>
73
+ <td>
74
+ <span class="badge badge-<%= route.verb.to_s.downcase.split('|').first %>">
75
+ <%= route.verb %>
76
+ </span>
77
+ </td>
78
+ <td><code><%= route.path %></code></td>
79
+ <td><code><%= route.action %></code></td>
80
+ <td>
81
+ <% all_params = [] %>
82
+ <% all_params += route.path_params if route.path_params&.any? %>
83
+ <% verb = route.verb.to_s.upcase %>
84
+ <% if %w[GET DELETE].include?(verb) %>
85
+ <% all_params += route.query_params if route.query_params&.any? %>
86
+ <% elsif %w[POST PUT PATCH].include?(verb) %>
87
+ <% all_params += route.request_body_params if route.request_body_params&.any? %>
88
+ <% end %>
89
+
90
+ <% if all_params.any? %>
91
+ <details style="cursor: pointer;">
92
+ <summary style="color: var(--primary); font-weight: 500;">
93
+ <%= all_params.size %> parameter<%= all_params.size > 1 ? 's' : '' %>
94
+ </summary>
95
+ <div style="margin-top: 0.5rem; padding: 0.75rem; background: var(--bg-secondary); border-radius: 6px; font-size: 0.875rem;">
96
+ <% all_params.each do |param| %>
97
+ <div style="margin-bottom: 0.5rem; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); last-child:border-bottom: none;">
98
+ <div style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.25rem;">
99
+ <code style="font-weight: 600; color: var(--primary);"><%= param[:name] %></code>
100
+ <span class="badge" style="font-size: 0.75rem; padding: 0.125rem 0.375rem;"><%= param[:type] %></span>
101
+ <% if param[:required] %>
102
+ <span style="color: var(--danger); font-size: 0.75rem; font-weight: 600;">required</span>
103
+ <% else %>
104
+ <span style="color: var(--text-dim); font-size: 0.75rem;">optional</span>
105
+ <% end %>
106
+ </div>
107
+ <div style="color: var(--text-dim); font-size: 0.75rem;">
108
+ Location: <%= param[:location] %>
109
+ </div>
110
+ </div>
111
+ <% end %>
112
+ </div>
113
+ </details>
114
+ <% else %>
115
+ <span style="color: var(--text-dim);">No parameters</span>
116
+ <% end %>
117
+ </td>
118
+ </tr>
119
+ <% end %>
120
+ </tbody>
121
+ </table>
122
+ </div>
123
+ <% else %>
124
+ <div class="empty-state">
125
+ <div class="empty-state-icon">🛣️</div>
126
+ <p>No routes found</p>
127
+ </div>
128
+ <% end %>
129
+ </section>
130
+ </main>
131
+
132
+ <footer class="footer">
133
+ <div>Generated by <a href="https://github.com/ArshdeepGrover/rails-map" target="_blank">RailsMap</a></div>
134
+ <div style="margin-top: 0.5rem;">Developed & maintained with ❤️ by <a href="https://www.arshdeepsingh.info?utm_source=rails_map&utm_medium=footer&utm_campaign=developer_credit&utm_content=docs_controller" target="_blank">Arshdeep Singh</a></div>
135
+ </footer>
136
+ </body>
137
+ </html>