funapi 0.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 (87) hide show
  1. checksums.yaml +7 -0
  2. data/.claude/25-09-01-OPENAPI_IMPLEMENTATION.md +233 -0
  3. data/.claude/25-09-05-RESPONSE_SCHEMA.md +383 -0
  4. data/.claude/25-09-10-OPENAPI_PLAN.md +219 -0
  5. data/.claude/25-10-26-MIDDLEWARE_IMPLEMENTATION.md +230 -0
  6. data/.claude/25-10-26-MIDDLEWARE_PLAN.md +353 -0
  7. data/.claude/25-10-27-BACKGROUND_TASKS_ANALYSIS.md +325 -0
  8. data/.claude/25-10-27-DEPENDENCY_IMPLEMENTATION_SUMMARY.md +325 -0
  9. data/.claude/25-10-27-DEPENDENCY_INJECTION_PLAN.md +753 -0
  10. data/.claude/25-12-24-LIFECYCLE_HOOKS_PLAN.md +421 -0
  11. data/.claude/25-12-24-PUBLISHING_AND_DOGFOODING_PLAN.md +327 -0
  12. data/.claude/25-12-24-TEMPLATE_RENDERING_PLAN.md +704 -0
  13. data/.claude/DECISIONS.md +397 -0
  14. data/.claude/PROJECT_PLAN.md +80 -0
  15. data/.claude/TESTING_PLAN.md +285 -0
  16. data/.claude/TESTING_STATUS.md +157 -0
  17. data/.tool-versions +1 -0
  18. data/AGENTS.md +416 -0
  19. data/CHANGELOG.md +5 -0
  20. data/CODE_OF_CONDUCT.md +132 -0
  21. data/LICENSE.txt +21 -0
  22. data/README.md +660 -0
  23. data/Rakefile +10 -0
  24. data/docs +8 -0
  25. data/docs-site/.gitignore +3 -0
  26. data/docs-site/Gemfile +9 -0
  27. data/docs-site/app.rb +138 -0
  28. data/docs-site/content/essential/handler.md +156 -0
  29. data/docs-site/content/essential/lifecycle.md +161 -0
  30. data/docs-site/content/essential/middleware.md +201 -0
  31. data/docs-site/content/essential/openapi.md +155 -0
  32. data/docs-site/content/essential/routing.md +123 -0
  33. data/docs-site/content/essential/validation.md +166 -0
  34. data/docs-site/content/getting-started/at-glance.md +82 -0
  35. data/docs-site/content/getting-started/key-concepts.md +150 -0
  36. data/docs-site/content/getting-started/quick-start.md +127 -0
  37. data/docs-site/content/index.md +81 -0
  38. data/docs-site/content/patterns/async-operations.md +137 -0
  39. data/docs-site/content/patterns/background-tasks.md +143 -0
  40. data/docs-site/content/patterns/database.md +175 -0
  41. data/docs-site/content/patterns/dependencies.md +141 -0
  42. data/docs-site/content/patterns/deployment.md +212 -0
  43. data/docs-site/content/patterns/error-handling.md +184 -0
  44. data/docs-site/content/patterns/response-schema.md +159 -0
  45. data/docs-site/content/patterns/templates.md +193 -0
  46. data/docs-site/content/patterns/testing.md +218 -0
  47. data/docs-site/mise.toml +2 -0
  48. data/docs-site/public/css/style.css +234 -0
  49. data/docs-site/templates/layouts/docs.html.erb +28 -0
  50. data/docs-site/templates/page.html.erb +3 -0
  51. data/docs-site/templates/partials/_nav.html.erb +19 -0
  52. data/examples/background_tasks_demo.rb +159 -0
  53. data/examples/demo_middleware.rb +55 -0
  54. data/examples/demo_openapi.rb +63 -0
  55. data/examples/dependency_block_demo.rb +150 -0
  56. data/examples/dependency_cleanup_demo.rb +146 -0
  57. data/examples/dependency_injection_demo.rb +200 -0
  58. data/examples/lifecycle_demo.rb +57 -0
  59. data/examples/middleware_demo.rb +74 -0
  60. data/examples/templates/layouts/application.html.erb +66 -0
  61. data/examples/templates/todos/_todo.html.erb +15 -0
  62. data/examples/templates/todos/index.html.erb +12 -0
  63. data/examples/templates_demo.rb +87 -0
  64. data/lib/funapi/application.rb +521 -0
  65. data/lib/funapi/async.rb +57 -0
  66. data/lib/funapi/background_tasks.rb +52 -0
  67. data/lib/funapi/config.rb +23 -0
  68. data/lib/funapi/database/sequel/fibered_connection_pool.rb +87 -0
  69. data/lib/funapi/dependency_wrapper.rb +66 -0
  70. data/lib/funapi/depends.rb +138 -0
  71. data/lib/funapi/exceptions.rb +72 -0
  72. data/lib/funapi/middleware/base.rb +13 -0
  73. data/lib/funapi/middleware/cors.rb +23 -0
  74. data/lib/funapi/middleware/request_logger.rb +32 -0
  75. data/lib/funapi/middleware/trusted_host.rb +34 -0
  76. data/lib/funapi/middleware.rb +4 -0
  77. data/lib/funapi/openapi/schema_converter.rb +85 -0
  78. data/lib/funapi/openapi/spec_generator.rb +179 -0
  79. data/lib/funapi/router.rb +43 -0
  80. data/lib/funapi/schema.rb +65 -0
  81. data/lib/funapi/server/falcon.rb +38 -0
  82. data/lib/funapi/template_response.rb +17 -0
  83. data/lib/funapi/templates.rb +111 -0
  84. data/lib/funapi/version.rb +5 -0
  85. data/lib/funapi.rb +14 -0
  86. data/sig/fun_api.rbs +499 -0
  87. metadata +220 -0
@@ -0,0 +1,193 @@
1
+ ---
2
+ title: Templates
3
+ ---
4
+
5
+ # Templates
6
+
7
+ Render ERB templates for HTML responses. Perfect for HTMX-powered applications.
8
+
9
+ ## Setup
10
+
11
+ ```ruby
12
+ require 'funapi/templates'
13
+
14
+ templates = FunApi::Templates.new(
15
+ directory: 'templates',
16
+ layout: 'layouts/application.html.erb' # Optional default layout
17
+ )
18
+ ```
19
+
20
+ ## Basic Rendering
21
+
22
+ Return a `TemplateResponse` from your handler:
23
+
24
+ ```ruby
25
+ api.get '/' do |input, req, task|
26
+ templates.response('home.html.erb', title: 'Home', user: current_user)
27
+ end
28
+ ```
29
+
30
+ Template (`templates/home.html.erb`):
31
+
32
+ ```erb
33
+ <h1>Welcome, <%= user[:name] %>!</h1>
34
+ <p>This is the home page.</p>
35
+ ```
36
+
37
+ ## Layouts
38
+
39
+ ### Default Layout
40
+
41
+ Set a default layout in the constructor:
42
+
43
+ ```ruby
44
+ templates = FunApi::Templates.new(
45
+ directory: 'templates',
46
+ layout: 'layouts/application.html.erb'
47
+ )
48
+ ```
49
+
50
+ Layout (`templates/layouts/application.html.erb`):
51
+
52
+ ```erb
53
+ <!DOCTYPE html>
54
+ <html>
55
+ <head>
56
+ <title><%= title %></title>
57
+ </head>
58
+ <body>
59
+ <nav>...</nav>
60
+
61
+ <main>
62
+ <%= yield_content %>
63
+ </main>
64
+
65
+ <footer>...</footer>
66
+ </body>
67
+ </html>
68
+ ```
69
+
70
+ Use `yield_content` to insert the template content.
71
+
72
+ ### Disabling Layout
73
+
74
+ For partials or HTMX responses, disable the layout:
75
+
76
+ ```ruby
77
+ api.post '/items' do |input, req, task|
78
+ item = create_item(input[:body])
79
+ templates.response('items/_item.html.erb', layout: false, item: item, status: 201)
80
+ end
81
+ ```
82
+
83
+ ### Different Layouts
84
+
85
+ Use `with_layout` for route groups:
86
+
87
+ ```ruby
88
+ templates = FunApi::Templates.new(directory: 'templates')
89
+
90
+ public_templates = templates.with_layout('layouts/public.html.erb')
91
+ admin_templates = templates.with_layout('layouts/admin.html.erb')
92
+
93
+ api.get '/' do |input, req, task|
94
+ public_templates.response('home.html.erb', title: 'Home')
95
+ end
96
+
97
+ api.get '/admin' do |input, req, task|
98
+ admin_templates.response('admin/dashboard.html.erb', title: 'Dashboard')
99
+ end
100
+ ```
101
+
102
+ ## Partials
103
+
104
+ Render partials within templates:
105
+
106
+ ```erb
107
+ <!-- templates/items/index.html.erb -->
108
+ <ul id="items">
109
+ <% items.each do |item| %>
110
+ <%= render_partial('items/_item.html.erb', item: item) %>
111
+ <% end %>
112
+ </ul>
113
+ ```
114
+
115
+ ```erb
116
+ <!-- templates/items/_item.html.erb -->
117
+ <li id="item-<%= item[:id] %>">
118
+ <%= item[:name] %>
119
+ </li>
120
+ ```
121
+
122
+ ## With HTMX
123
+
124
+ FunApi templates work great with HTMX:
125
+
126
+ ```ruby
127
+ # Full page with layout
128
+ api.get '/items' do |input, req, task|
129
+ items = fetch_items
130
+ templates.response('items/index.html.erb', items: items)
131
+ end
132
+
133
+ # Partial for HTMX insertion
134
+ api.post '/items', body: ItemSchema do |input, req, task|
135
+ item = create_item(input[:body])
136
+ templates.response('items/_item.html.erb', layout: false, item: item, status: 201)
137
+ end
138
+
139
+ # Empty response for HTMX delete
140
+ api.delete '/items/:id' do |input, req, task|
141
+ delete_item(input[:path]['id'])
142
+ FunApi::TemplateResponse.new('')
143
+ end
144
+ ```
145
+
146
+ Template with HTMX:
147
+
148
+ ```erb
149
+ <form hx-post="/items" hx-target="#items" hx-swap="beforeend">
150
+ <input type="text" name="name" placeholder="New item">
151
+ <button type="submit">Add</button>
152
+ </form>
153
+
154
+ <ul id="items">
155
+ <% items.each do |item| %>
156
+ <%= render_partial('items/_item.html.erb', item: item) %>
157
+ <% end %>
158
+ </ul>
159
+ ```
160
+
161
+ ## Template Variables
162
+
163
+ Pass any variables as keyword arguments:
164
+
165
+ ```ruby
166
+ templates.response('page.html.erb',
167
+ title: 'My Page',
168
+ user: current_user,
169
+ items: items,
170
+ flash: { notice: 'Success!' }
171
+ )
172
+ ```
173
+
174
+ Access them directly in templates:
175
+
176
+ ```erb
177
+ <h1><%= title %></h1>
178
+ <p>Hello, <%= user[:name] %></p>
179
+
180
+ <% if flash[:notice] %>
181
+ <div class="notice"><%= flash[:notice] %></div>
182
+ <% end %>
183
+ ```
184
+
185
+ ## Custom Status and Headers
186
+
187
+ ```ruby
188
+ templates.response('error.html.erb',
189
+ status: 404,
190
+ headers: { 'X-Custom' => 'value' },
191
+ message: 'Not found'
192
+ )
193
+ ```
@@ -0,0 +1,218 @@
1
+ ---
2
+ title: Testing
3
+ ---
4
+
5
+ # Testing
6
+
7
+ Test FunApi applications with any Ruby testing framework.
8
+
9
+ ## Basic Setup with Minitest
10
+
11
+ ```ruby
12
+ require 'minitest/autorun'
13
+ require 'rack/test'
14
+ require 'async'
15
+
16
+ class TestMyApi < Minitest::Test
17
+ include Rack::Test::Methods
18
+
19
+ def app
20
+ @app ||= FunApi::App.new do |api|
21
+ api.get '/hello' do |input, req, task|
22
+ [{ message: 'Hello!' }, 200]
23
+ end
24
+ end
25
+ end
26
+
27
+ def async_request(method, path, **options)
28
+ Async do
29
+ send(method, path, **options)
30
+ last_response
31
+ end.wait
32
+ end
33
+
34
+ def test_hello
35
+ response = async_request(:get, '/hello')
36
+ assert_equal 200, response.status
37
+ assert_equal({ 'message' => 'Hello!' }, JSON.parse(response.body))
38
+ end
39
+ end
40
+ ```
41
+
42
+ ## Testing with RSpec
43
+
44
+ ```ruby
45
+ require 'rack/test'
46
+ require 'async'
47
+
48
+ RSpec.describe 'My API' do
49
+ include Rack::Test::Methods
50
+
51
+ let(:app) do
52
+ FunApi::App.new do |api|
53
+ api.get '/hello' do |input, req, task|
54
+ [{ message: 'Hello!' }, 200]
55
+ end
56
+ end
57
+ end
58
+
59
+ def async_request(method, path, **options)
60
+ Async do
61
+ send(method, path, **options)
62
+ last_response
63
+ end.wait
64
+ end
65
+
66
+ it 'returns hello' do
67
+ response = async_request(:get, '/hello')
68
+ expect(response.status).to eq(200)
69
+ expect(JSON.parse(response.body)).to eq({ 'message' => 'Hello!' })
70
+ end
71
+ end
72
+ ```
73
+
74
+ ## Testing POST Requests
75
+
76
+ ```ruby
77
+ def test_create_user
78
+ response = async_request(:post, '/users',
79
+ input: JSON.dump({ name: 'Alice', email: 'alice@example.com' }),
80
+ 'CONTENT_TYPE' => 'application/json'
81
+ )
82
+
83
+ assert_equal 201, response.status
84
+ body = JSON.parse(response.body)
85
+ assert_equal 'Alice', body['created']['name']
86
+ end
87
+ ```
88
+
89
+ ## Testing Validation Errors
90
+
91
+ ```ruby
92
+ def test_validation_error
93
+ response = async_request(:post, '/users',
94
+ input: JSON.dump({ name: 'Alice' }), # Missing email
95
+ 'CONTENT_TYPE' => 'application/json'
96
+ )
97
+
98
+ assert_equal 422, response.status
99
+ body = JSON.parse(response.body)
100
+ assert body['detail'].any? { |e| e['loc'].include?('email') }
101
+ end
102
+ ```
103
+
104
+ ## Testing with Dependencies
105
+
106
+ Mock dependencies for testing:
107
+
108
+ ```ruby
109
+ def app
110
+ @app ||= FunApi::App.new do |api|
111
+ api.register(:db) { MockDatabase.new }
112
+
113
+ api.get '/users', depends: [:db] do |input, req, task, db:|
114
+ [{ users: db.all_users }, 200]
115
+ end
116
+ end
117
+ end
118
+
119
+ class MockDatabase
120
+ def all_users
121
+ [{ id: 1, name: 'Test User' }]
122
+ end
123
+ end
124
+
125
+ def test_users_with_mock_db
126
+ response = async_request(:get, '/users')
127
+ assert_equal 200, response.status
128
+ assert_equal 1, JSON.parse(response.body)['users'].length
129
+ end
130
+ ```
131
+
132
+ ## Testing Path Parameters
133
+
134
+ ```ruby
135
+ def test_get_user_by_id
136
+ response = async_request(:get, '/users/123')
137
+ assert_equal 200, response.status
138
+ assert_equal '123', JSON.parse(response.body)['id']
139
+ end
140
+ ```
141
+
142
+ ## Testing Query Parameters
143
+
144
+ ```ruby
145
+ def test_search
146
+ response = async_request(:get, '/search?q=ruby&limit=10')
147
+ assert_equal 200, response.status
148
+ end
149
+ ```
150
+
151
+ ## Testing Headers
152
+
153
+ ```ruby
154
+ def test_auth_header
155
+ response = async_request(:get, '/protected',
156
+ 'HTTP_AUTHORIZATION' => 'Bearer token123'
157
+ )
158
+ assert_equal 200, response.status
159
+ end
160
+ ```
161
+
162
+ ## Integration Testing with Real Database
163
+
164
+ ```ruby
165
+ class TestWithDatabase < Minitest::Test
166
+ def setup
167
+ @db = PG.connect(ENV['TEST_DATABASE_URL'])
168
+ @db.exec("TRUNCATE users")
169
+ end
170
+
171
+ def teardown
172
+ @db.close
173
+ end
174
+
175
+ def app
176
+ FunApi::App.new do |api|
177
+ api.register(:db) { @db }
178
+ # routes...
179
+ end
180
+ end
181
+
182
+ def test_creates_user_in_database
183
+ async_request(:post, '/users',
184
+ input: JSON.dump({ name: 'Alice', email: 'alice@test.com' }),
185
+ 'CONTENT_TYPE' => 'application/json'
186
+ )
187
+
188
+ result = @db.exec("SELECT * FROM users WHERE email = 'alice@test.com'")
189
+ assert_equal 1, result.ntuples
190
+ end
191
+ end
192
+ ```
193
+
194
+ ## Helper Module
195
+
196
+ Extract common test helpers:
197
+
198
+ ```ruby
199
+ module FunApiTestHelpers
200
+ def async_request(method, path, **options)
201
+ Async do
202
+ send(method, path, **options)
203
+ last_response
204
+ end.wait
205
+ end
206
+
207
+ def json_body
208
+ JSON.parse(last_response.body)
209
+ end
210
+
211
+ def post_json(path, body)
212
+ async_request(:post, path,
213
+ input: JSON.dump(body),
214
+ 'CONTENT_TYPE' => 'application/json'
215
+ )
216
+ end
217
+ end
218
+ ```
@@ -0,0 +1,2 @@
1
+ [tools]
2
+ ruby = "3.4.5"
@@ -0,0 +1,234 @@
1
+ /* Reset and base */
2
+ *, *::before, *::after {
3
+ box-sizing: border-box;
4
+ }
5
+
6
+ :root {
7
+ --color-bg: #0a0a0a;
8
+ --color-bg-secondary: #141414;
9
+ --color-text: #e5e5e5;
10
+ --color-text-muted: #a3a3a3;
11
+ --color-primary: #3b82f6;
12
+ --color-primary-hover: #60a5fa;
13
+ --color-border: #262626;
14
+ --color-code-bg: #1a1a1a;
15
+ --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, sans-serif;
16
+ --font-mono: "SF Mono", SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace;
17
+ }
18
+
19
+ body {
20
+ margin: 0;
21
+ font-family: var(--font-sans);
22
+ font-size: 16px;
23
+ line-height: 1.6;
24
+ color: var(--color-text);
25
+ background-color: var(--color-bg);
26
+ }
27
+
28
+ a {
29
+ color: var(--color-primary);
30
+ text-decoration: none;
31
+ }
32
+
33
+ a:hover {
34
+ color: var(--color-primary-hover);
35
+ }
36
+
37
+ /* Layout */
38
+ .layout {
39
+ display: grid;
40
+ grid-template-columns: 260px 1fr;
41
+ grid-template-rows: 60px 1fr;
42
+ min-height: 100vh;
43
+ }
44
+
45
+ .header {
46
+ grid-column: 1 / -1;
47
+ display: flex;
48
+ align-items: center;
49
+ justify-content: space-between;
50
+ padding: 0 24px;
51
+ border-bottom: 1px solid var(--color-border);
52
+ background-color: var(--color-bg);
53
+ position: sticky;
54
+ top: 0;
55
+ z-index: 100;
56
+ }
57
+
58
+ .logo {
59
+ font-size: 1.25rem;
60
+ font-weight: 700;
61
+ color: var(--color-text);
62
+ }
63
+
64
+ .header-nav {
65
+ display: flex;
66
+ gap: 24px;
67
+ }
68
+
69
+ .header-nav a {
70
+ color: var(--color-text-muted);
71
+ }
72
+
73
+ .header-nav a:hover {
74
+ color: var(--color-text);
75
+ }
76
+
77
+ .sidebar {
78
+ padding: 24px;
79
+ border-right: 1px solid var(--color-border);
80
+ background-color: var(--color-bg);
81
+ overflow-y: auto;
82
+ position: sticky;
83
+ top: 60px;
84
+ height: calc(100vh - 60px);
85
+ }
86
+
87
+ .content {
88
+ padding: 48px;
89
+ max-width: 800px;
90
+ }
91
+
92
+ /* Navigation */
93
+ .nav-section {
94
+ margin-bottom: 24px;
95
+ }
96
+
97
+ .nav-section-title {
98
+ font-size: 0.75rem;
99
+ font-weight: 600;
100
+ text-transform: uppercase;
101
+ letter-spacing: 0.05em;
102
+ color: var(--color-text-muted);
103
+ margin: 0 0 12px 0;
104
+ }
105
+
106
+ .nav-list {
107
+ list-style: none;
108
+ margin: 0;
109
+ padding: 0;
110
+ }
111
+
112
+ .nav-item {
113
+ margin: 0;
114
+ }
115
+
116
+ .nav-link {
117
+ display: block;
118
+ padding: 6px 12px;
119
+ margin: 2px 0;
120
+ border-radius: 6px;
121
+ color: var(--color-text-muted);
122
+ font-size: 0.9rem;
123
+ }
124
+
125
+ .nav-link:hover {
126
+ color: var(--color-text);
127
+ background-color: var(--color-bg-secondary);
128
+ }
129
+
130
+ .nav-link.active {
131
+ color: var(--color-primary);
132
+ background-color: rgba(59, 130, 246, 0.1);
133
+ }
134
+
135
+ /* Article content */
136
+ .article {
137
+ color: var(--color-text);
138
+ }
139
+
140
+ .article h1 {
141
+ font-size: 2.5rem;
142
+ font-weight: 700;
143
+ margin: 0 0 16px 0;
144
+ line-height: 1.2;
145
+ }
146
+
147
+ .article h2 {
148
+ font-size: 1.5rem;
149
+ font-weight: 600;
150
+ margin: 48px 0 16px 0;
151
+ padding-top: 24px;
152
+ border-top: 1px solid var(--color-border);
153
+ }
154
+
155
+ .article h3 {
156
+ font-size: 1.25rem;
157
+ font-weight: 600;
158
+ margin: 32px 0 12px 0;
159
+ }
160
+
161
+ .article p {
162
+ margin: 0 0 16px 0;
163
+ color: var(--color-text-muted);
164
+ }
165
+
166
+ .article ul, .article ol {
167
+ margin: 0 0 16px 0;
168
+ padding-left: 24px;
169
+ color: var(--color-text-muted);
170
+ }
171
+
172
+ .article li {
173
+ margin: 8px 0;
174
+ }
175
+
176
+ .article code {
177
+ font-family: var(--font-mono);
178
+ font-size: 0.9em;
179
+ background-color: var(--color-code-bg);
180
+ padding: 2px 6px;
181
+ border-radius: 4px;
182
+ }
183
+
184
+ .article pre {
185
+ background-color: var(--color-code-bg);
186
+ border-radius: 8px;
187
+ padding: 16px;
188
+ overflow-x: auto;
189
+ margin: 0 0 24px 0;
190
+ }
191
+
192
+ .article pre code {
193
+ background: none;
194
+ padding: 0;
195
+ font-size: 0.875rem;
196
+ line-height: 1.7;
197
+ }
198
+
199
+ .article blockquote {
200
+ margin: 0 0 16px 0;
201
+ padding: 12px 16px;
202
+ border-left: 3px solid var(--color-primary);
203
+ background-color: var(--color-bg-secondary);
204
+ border-radius: 0 8px 8px 0;
205
+ }
206
+
207
+ .article blockquote p {
208
+ margin: 0;
209
+ }
210
+
211
+ /* Code highlighting (Rouge) */
212
+ .highlight .k, .highlight .kd, .highlight .kn, .highlight .kp, .highlight .kr, .highlight .kt { color: #c678dd; }
213
+ .highlight .s, .highlight .s1, .highlight .s2, .highlight .sr { color: #98c379; }
214
+ .highlight .c, .highlight .c1, .highlight .cm { color: #5c6370; font-style: italic; }
215
+ .highlight .n, .highlight .nf, .highlight .nb { color: #61afef; }
216
+ .highlight .nc, .highlight .no { color: #e5c07b; }
217
+ .highlight .o, .highlight .p { color: #abb2bf; }
218
+ .highlight .mi, .highlight .mf { color: #d19a66; }
219
+ .highlight .ss { color: #56b6c2; }
220
+
221
+ /* Responsive */
222
+ @media (max-width: 768px) {
223
+ .layout {
224
+ grid-template-columns: 1fr;
225
+ }
226
+
227
+ .sidebar {
228
+ display: none;
229
+ }
230
+
231
+ .content {
232
+ padding: 24px;
233
+ }
234
+ }
@@ -0,0 +1,28 @@
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><%= title %> - FunApi</title>
7
+ <link rel="stylesheet" href="/css/style.css">
8
+ </head>
9
+ <body>
10
+ <div class="layout">
11
+ <header class="header">
12
+ <a href="/" class="logo">FunApi</a>
13
+ <nav class="header-nav">
14
+ <a href="/docs/getting-started/quick-start">Docs</a>
15
+ <a href="https://github.com/gafemoyano/fun_api" target="_blank">GitHub</a>
16
+ </nav>
17
+ </header>
18
+
19
+ <aside class="sidebar">
20
+ <%= render_partial("partials/_nav.html.erb", nav: nav, current_path: current_path) %>
21
+ </aside>
22
+
23
+ <main class="content">
24
+ <%= yield_content %>
25
+ </main>
26
+ </div>
27
+ </body>
28
+ </html>
@@ -0,0 +1,3 @@
1
+ <article class="article">
2
+ <%= content %>
3
+ </article>
@@ -0,0 +1,19 @@
1
+ <nav class="nav">
2
+ <% nav.each do |section| %>
3
+ <div class="nav-section">
4
+ <h3 class="nav-section-title"><%= section[:title] %></h3>
5
+ <ul class="nav-list">
6
+ <% section[:items].each do |item| %>
7
+ <li class="nav-item">
8
+ <a
9
+ href="/docs/<%= item[:path] %>"
10
+ class="nav-link<%= ' active' if current_path == item[:path] %>"
11
+ >
12
+ <%= item[:title] %>
13
+ </a>
14
+ </li>
15
+ <% end %>
16
+ </ul>
17
+ </div>
18
+ <% end %>
19
+ </nav>