claude-agent-framework 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +128 -0
- package/bin/claude-framework +3 -0
- package/framework/agents/design-lead.md +240 -0
- package/framework/agents/product-owner.md +179 -0
- package/framework/agents/tech-lead.md +226 -0
- package/framework/commands/ayuda.md +127 -0
- package/framework/commands/a/303/261adir.md +98 -0
- package/framework/commands/backup.md +397 -0
- package/framework/commands/cambiar.md +110 -0
- package/framework/commands/cloud.md +457 -0
- package/framework/commands/code.md +142 -0
- package/framework/commands/debug.md +334 -0
- package/framework/commands/deploy.md +383 -0
- package/framework/commands/deshacer.md +120 -0
- package/framework/commands/estado.md +218 -0
- package/framework/commands/explica.md +227 -0
- package/framework/commands/feature.md +120 -0
- package/framework/commands/git.md +427 -0
- package/framework/commands/historial.md +202 -0
- package/framework/commands/learn.md +408 -0
- package/framework/commands/movil.md +245 -0
- package/framework/commands/nuevo.md +118 -0
- package/framework/commands/plan.md +134 -0
- package/framework/commands/prd.md +113 -0
- package/framework/commands/probar.md +148 -0
- package/framework/commands/revisar.md +208 -0
- package/framework/commands/seeds.md +230 -0
- package/framework/commands/seguridad.md +226 -0
- package/framework/commands/tasks.md +157 -0
- package/framework/skills/architecture/algorithms.md +970 -0
- package/framework/skills/architecture/clean-code.md +1080 -0
- package/framework/skills/architecture/design-patterns.md +1984 -0
- package/framework/skills/architecture/functional-programming.md +972 -0
- package/framework/skills/architecture/solid.md +991 -0
- package/framework/skills/cloud/cloud-aws.md +848 -0
- package/framework/skills/cloud/cloud-azure.md +931 -0
- package/framework/skills/cloud/cloud-gcp.md +848 -0
- package/framework/skills/cloud/message-queues.md +1229 -0
- package/framework/skills/core/accessibility.md +401 -0
- package/framework/skills/core/api.md +474 -0
- package/framework/skills/core/authentication.md +306 -0
- package/framework/skills/core/authorization.md +388 -0
- package/framework/skills/core/background-jobs.md +341 -0
- package/framework/skills/core/caching.md +473 -0
- package/framework/skills/core/code-review.md +341 -0
- package/framework/skills/core/controllers.md +290 -0
- package/framework/skills/core/cua.md +285 -0
- package/framework/skills/core/documentation.md +472 -0
- package/framework/skills/core/file-uploads.md +351 -0
- package/framework/skills/core/hotwire-native.md +296 -0
- package/framework/skills/core/hotwire.md +278 -0
- package/framework/skills/core/i18n.md +334 -0
- package/framework/skills/core/imports-exports.md +750 -0
- package/framework/skills/core/infrastructure.md +337 -0
- package/framework/skills/core/models.md +228 -0
- package/framework/skills/core/notifications.md +672 -0
- package/framework/skills/core/payments.md +581 -0
- package/framework/skills/core/performance.md +361 -0
- package/framework/skills/core/rails-scaffold.md +131 -0
- package/framework/skills/core/search.md +518 -0
- package/framework/skills/core/security.md +565 -0
- package/framework/skills/core/seeds.md +307 -0
- package/framework/skills/core/seo.md +542 -0
- package/framework/skills/core/testing.md +393 -0
- package/framework/skills/core/views.md +260 -0
- package/framework/skills/core/websockets.md +564 -0
- package/framework/skills/data/advanced-sql.md +1204 -0
- package/framework/skills/data/nosql.md +1141 -0
- package/framework/skills/devops/containers-advanced.md +1237 -0
- package/framework/skills/devops/debugging.md +834 -0
- package/framework/skills/devops/git-workflow.md +752 -0
- package/framework/skills/devops/networking.md +932 -0
- package/framework/skills/devops/shell-scripting.md +1132 -0
- package/framework/sub-agents/architecture-patterns-agent.md +1450 -0
- package/framework/sub-agents/cloud-agent.md +677 -0
- package/framework/sub-agents/data.md +504 -0
- package/framework/sub-agents/debugging-agent.md +554 -0
- package/framework/sub-agents/devops.md +483 -0
- package/framework/sub-agents/docs.md +176 -0
- package/framework/sub-agents/frontend-dev.md +349 -0
- package/framework/sub-agents/git-workflow-agent.md +697 -0
- package/framework/sub-agents/integrations.md +630 -0
- package/framework/sub-agents/native-dev.md +434 -0
- package/framework/sub-agents/qa.md +138 -0
- package/framework/sub-agents/rails-dev.md +375 -0
- package/framework/sub-agents/security.md +526 -0
- package/framework/sub-agents/ui.md +437 -0
- package/framework/sub-agents/ux.md +284 -0
- package/framework/templates/api-spec.md +500 -0
- package/framework/templates/component-spec.md +248 -0
- package/framework/templates/feature.json +13 -0
- package/framework/templates/model-spec.md +318 -0
- package/framework/templates/prd-template.md +80 -0
- package/framework/templates/task-plan.md +122 -0
- package/framework/templates/task-user-story.md +52 -0
- package/framework/templates/technical-spec.md +260 -0
- package/framework/templates/user-story.md +95 -0
- package/package.json +42 -0
- package/project-templates/CLAUDE.md +42 -0
- package/project-templates/contexts/architecture.md +25 -0
- package/project-templates/contexts/conventions.md +46 -0
- package/project-templates/contexts/design-system.md +47 -0
- package/project-templates/contexts/requirements.md +38 -0
- package/project-templates/contexts/stack.md +30 -0
- package/project-templates/history/active/models.md +11 -0
- package/project-templates/history/changelog.md +15 -0
- package/project-templates/workspace/.gitkeep +0 -0
- package/src/cli.js +52 -0
- package/src/init.js +104 -0
- package/src/status.js +75 -0
- package/src/update.js +88 -0
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
# Skill: Testing
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
Write comprehensive tests using RSpec, FactoryBot, Faker, and Shoulda Matchers.
|
|
5
|
+
|
|
6
|
+
## Setup
|
|
7
|
+
|
|
8
|
+
### Gemfile
|
|
9
|
+
```ruby
|
|
10
|
+
group :development, :test do
|
|
11
|
+
gem "rspec-rails"
|
|
12
|
+
gem "factory_bot_rails"
|
|
13
|
+
gem "faker"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
group :test do
|
|
17
|
+
gem "shoulda-matchers"
|
|
18
|
+
gem "capybara"
|
|
19
|
+
gem "selenium-webdriver"
|
|
20
|
+
end
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Install RSpec
|
|
24
|
+
```bash
|
|
25
|
+
rails generate rspec:install
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Configure RSpec
|
|
29
|
+
```ruby
|
|
30
|
+
# spec/rails_helper.rb
|
|
31
|
+
require "spec_helper"
|
|
32
|
+
ENV["RAILS_ENV"] ||= "test"
|
|
33
|
+
require_relative "../config/environment"
|
|
34
|
+
|
|
35
|
+
abort("Running in production!") if Rails.env.production?
|
|
36
|
+
require "rspec/rails"
|
|
37
|
+
|
|
38
|
+
Dir[Rails.root.join("spec/support/**/*.rb")].sort.each { |f| require f }
|
|
39
|
+
|
|
40
|
+
RSpec.configure do |config|
|
|
41
|
+
config.fixture_paths = [Rails.root.join("spec/fixtures")]
|
|
42
|
+
config.use_transactional_fixtures = true
|
|
43
|
+
config.infer_spec_type_from_file_location!
|
|
44
|
+
config.filter_rails_from_backtrace!
|
|
45
|
+
|
|
46
|
+
config.include FactoryBot::Syntax::Methods
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
Shoulda::Matchers.configure do |config|
|
|
50
|
+
config.integrate do |with|
|
|
51
|
+
with.test_framework :rspec
|
|
52
|
+
with.library :rails
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Factories
|
|
58
|
+
|
|
59
|
+
### Basic Factory
|
|
60
|
+
```ruby
|
|
61
|
+
# spec/factories/users.rb
|
|
62
|
+
FactoryBot.define do
|
|
63
|
+
factory :user do
|
|
64
|
+
email_address { Faker::Internet.unique.email }
|
|
65
|
+
password { "password123" }
|
|
66
|
+
password_confirmation { "password123" }
|
|
67
|
+
|
|
68
|
+
trait :admin do
|
|
69
|
+
role { "admin" }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
trait :with_articles do
|
|
73
|
+
transient do
|
|
74
|
+
articles_count { 3 }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
after(:create) do |user, evaluator|
|
|
78
|
+
create_list(:article, evaluator.articles_count, user: user)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Associated Factory
|
|
86
|
+
```ruby
|
|
87
|
+
# spec/factories/articles.rb
|
|
88
|
+
FactoryBot.define do
|
|
89
|
+
factory :article do
|
|
90
|
+
title { Faker::Lorem.sentence }
|
|
91
|
+
body { Faker::Lorem.paragraphs(number: 3).join("\n\n") }
|
|
92
|
+
published { false }
|
|
93
|
+
user
|
|
94
|
+
|
|
95
|
+
trait :published do
|
|
96
|
+
published { true }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
trait :with_comments do
|
|
100
|
+
transient do
|
|
101
|
+
comments_count { 5 }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
after(:create) do |article, evaluator|
|
|
105
|
+
create_list(:comment, evaluator.comments_count, article: article)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Model Specs
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
# spec/models/article_spec.rb
|
|
116
|
+
require "rails_helper"
|
|
117
|
+
|
|
118
|
+
RSpec.describe Article, type: :model do
|
|
119
|
+
describe "validations" do
|
|
120
|
+
it { should validate_presence_of(:title) }
|
|
121
|
+
it { should validate_length_of(:title).is_at_most(255) }
|
|
122
|
+
it { should validate_presence_of(:body) }
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
describe "associations" do
|
|
126
|
+
it { should belong_to(:user) }
|
|
127
|
+
it { should have_many(:comments).dependent(:destroy) }
|
|
128
|
+
it { should have_many(:taggings).dependent(:destroy) }
|
|
129
|
+
it { should have_many(:tags).through(:taggings) }
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
describe "scopes" do
|
|
133
|
+
describe ".published" do
|
|
134
|
+
it "returns only published articles" do
|
|
135
|
+
published = create(:article, :published)
|
|
136
|
+
draft = create(:article, published: false)
|
|
137
|
+
|
|
138
|
+
expect(Article.published).to include(published)
|
|
139
|
+
expect(Article.published).not_to include(draft)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
describe ".recent" do
|
|
144
|
+
it "orders by created_at descending" do
|
|
145
|
+
old = create(:article, created_at: 1.week.ago)
|
|
146
|
+
new = create(:article, created_at: 1.day.ago)
|
|
147
|
+
|
|
148
|
+
expect(Article.recent).to eq([new, old])
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
describe "#publish!" do
|
|
154
|
+
it "marks article as published" do
|
|
155
|
+
article = create(:article, published: false)
|
|
156
|
+
|
|
157
|
+
article.publish!
|
|
158
|
+
|
|
159
|
+
expect(article.reload).to be_published
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
describe "callbacks" do
|
|
164
|
+
it "generates slug before save" do
|
|
165
|
+
article = build(:article, title: "Hello World")
|
|
166
|
+
|
|
167
|
+
article.save
|
|
168
|
+
|
|
169
|
+
expect(article.slug).to eq("hello-world")
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Request Specs
|
|
176
|
+
|
|
177
|
+
```ruby
|
|
178
|
+
# spec/requests/articles_spec.rb
|
|
179
|
+
require "rails_helper"
|
|
180
|
+
|
|
181
|
+
RSpec.describe "Articles", type: :request do
|
|
182
|
+
let(:user) { create(:user) }
|
|
183
|
+
let(:article) { create(:article, user: user) }
|
|
184
|
+
|
|
185
|
+
describe "GET /articles" do
|
|
186
|
+
it "returns success" do
|
|
187
|
+
get articles_path
|
|
188
|
+
expect(response).to have_http_status(:success)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
it "displays articles" do
|
|
192
|
+
article = create(:article, :published)
|
|
193
|
+
get articles_path
|
|
194
|
+
expect(response.body).to include(article.title)
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
describe "GET /articles/:id" do
|
|
199
|
+
context "when article is published" do
|
|
200
|
+
it "returns success" do
|
|
201
|
+
article = create(:article, :published)
|
|
202
|
+
get article_path(article)
|
|
203
|
+
expect(response).to have_http_status(:success)
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
context "when article is draft" do
|
|
208
|
+
it "redirects unauthorized users" do
|
|
209
|
+
article = create(:article, published: false)
|
|
210
|
+
get article_path(article)
|
|
211
|
+
expect(response).to redirect_to(new_session_path)
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
describe "POST /articles" do
|
|
217
|
+
context "when authenticated" do
|
|
218
|
+
before { sign_in(user) }
|
|
219
|
+
|
|
220
|
+
it "creates article with valid params" do
|
|
221
|
+
expect {
|
|
222
|
+
post articles_path, params: { article: { title: "Test", body: "Content" } }
|
|
223
|
+
}.to change(Article, :count).by(1)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
it "does not create with invalid params" do
|
|
227
|
+
expect {
|
|
228
|
+
post articles_path, params: { article: { title: "", body: "" } }
|
|
229
|
+
}.not_to change(Article, :count)
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
context "when not authenticated" do
|
|
234
|
+
it "redirects to login" do
|
|
235
|
+
post articles_path, params: { article: { title: "Test", body: "Content" } }
|
|
236
|
+
expect(response).to redirect_to(new_session_path)
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
describe "PATCH /articles/:id" do
|
|
242
|
+
before { sign_in(user) }
|
|
243
|
+
|
|
244
|
+
it "updates article" do
|
|
245
|
+
patch article_path(article), params: { article: { title: "Updated" } }
|
|
246
|
+
expect(article.reload.title).to eq("Updated")
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
describe "DELETE /articles/:id" do
|
|
251
|
+
before { sign_in(user) }
|
|
252
|
+
|
|
253
|
+
it "destroys article" do
|
|
254
|
+
article # create it
|
|
255
|
+
expect {
|
|
256
|
+
delete article_path(article)
|
|
257
|
+
}.to change(Article, :count).by(-1)
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
## System Specs
|
|
264
|
+
|
|
265
|
+
```ruby
|
|
266
|
+
# spec/system/articles_spec.rb
|
|
267
|
+
require "rails_helper"
|
|
268
|
+
|
|
269
|
+
RSpec.describe "Articles", type: :system do
|
|
270
|
+
let(:user) { create(:user) }
|
|
271
|
+
|
|
272
|
+
before do
|
|
273
|
+
driven_by(:selenium_chrome_headless)
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
describe "creating an article" do
|
|
277
|
+
before { sign_in(user) }
|
|
278
|
+
|
|
279
|
+
it "allows user to create article" do
|
|
280
|
+
visit new_article_path
|
|
281
|
+
|
|
282
|
+
fill_in "Title", with: "My Article"
|
|
283
|
+
fill_in "Body", with: "Article content here"
|
|
284
|
+
click_button "Create Article"
|
|
285
|
+
|
|
286
|
+
expect(page).to have_content("Article created successfully")
|
|
287
|
+
expect(page).to have_content("My Article")
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
it "shows validation errors" do
|
|
291
|
+
visit new_article_path
|
|
292
|
+
|
|
293
|
+
click_button "Create Article"
|
|
294
|
+
|
|
295
|
+
expect(page).to have_content("can't be blank")
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
describe "editing an article" do
|
|
300
|
+
let!(:article) { create(:article, user: user, title: "Original") }
|
|
301
|
+
|
|
302
|
+
before { sign_in(user) }
|
|
303
|
+
|
|
304
|
+
it "allows owner to edit" do
|
|
305
|
+
visit article_path(article)
|
|
306
|
+
click_link "Edit"
|
|
307
|
+
|
|
308
|
+
fill_in "Title", with: "Updated Title"
|
|
309
|
+
click_button "Update Article"
|
|
310
|
+
|
|
311
|
+
expect(page).to have_content("Updated Title")
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
describe "with Turbo interactions" do
|
|
316
|
+
before { sign_in(user) }
|
|
317
|
+
|
|
318
|
+
it "adds comment without page reload", js: true do
|
|
319
|
+
article = create(:article, :published)
|
|
320
|
+
visit article_path(article)
|
|
321
|
+
|
|
322
|
+
fill_in "Comment", with: "Great article!"
|
|
323
|
+
click_button "Post Comment"
|
|
324
|
+
|
|
325
|
+
within("#comments") do
|
|
326
|
+
expect(page).to have_content("Great article!")
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
## Test Helpers
|
|
334
|
+
|
|
335
|
+
```ruby
|
|
336
|
+
# spec/support/authentication_helper.rb
|
|
337
|
+
module AuthenticationHelper
|
|
338
|
+
def sign_in(user)
|
|
339
|
+
post session_path, params: {
|
|
340
|
+
email_address: user.email_address,
|
|
341
|
+
password: "password123"
|
|
342
|
+
}
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
RSpec.configure do |config|
|
|
347
|
+
config.include AuthenticationHelper, type: :request
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
# spec/support/system_helper.rb
|
|
351
|
+
module SystemHelper
|
|
352
|
+
def sign_in(user)
|
|
353
|
+
visit new_session_path
|
|
354
|
+
fill_in "Email", with: user.email_address
|
|
355
|
+
fill_in "Password", with: "password123"
|
|
356
|
+
click_button "Sign In"
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
RSpec.configure do |config|
|
|
361
|
+
config.include SystemHelper, type: :system
|
|
362
|
+
end
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
## Running Tests
|
|
366
|
+
|
|
367
|
+
```bash
|
|
368
|
+
# All tests
|
|
369
|
+
bundle exec rspec
|
|
370
|
+
|
|
371
|
+
# Specific file
|
|
372
|
+
bundle exec rspec spec/models/article_spec.rb
|
|
373
|
+
|
|
374
|
+
# Specific test
|
|
375
|
+
bundle exec rspec spec/models/article_spec.rb:25
|
|
376
|
+
|
|
377
|
+
# By type
|
|
378
|
+
bundle exec rspec spec/models
|
|
379
|
+
bundle exec rspec spec/requests
|
|
380
|
+
bundle exec rspec spec/system
|
|
381
|
+
|
|
382
|
+
# With format
|
|
383
|
+
bundle exec rspec --format documentation
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
## Best Practices
|
|
387
|
+
|
|
388
|
+
1. **One assertion per example** when possible
|
|
389
|
+
2. **Use let/let!** for setup, not before blocks
|
|
390
|
+
3. **Use traits** for variations
|
|
391
|
+
4. **Test behavior, not implementation**
|
|
392
|
+
5. **Use factories, not fixtures**
|
|
393
|
+
6. **Keep tests fast** - mock external services
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
# Skill: Views
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
Create Rails views with ERB, Tailwind CSS, and proper structure following mobile-first design principles.
|
|
5
|
+
|
|
6
|
+
## View Structure
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
app/views/
|
|
10
|
+
├── layouts/
|
|
11
|
+
│ ├── application.html.erb
|
|
12
|
+
│ └── _navigation.html.erb
|
|
13
|
+
├── shared/
|
|
14
|
+
│ ├── _flash.html.erb
|
|
15
|
+
│ ├── _footer.html.erb
|
|
16
|
+
│ └── _pagination.html.erb
|
|
17
|
+
└── articles/
|
|
18
|
+
├── index.html.erb
|
|
19
|
+
├── show.html.erb
|
|
20
|
+
├── new.html.erb
|
|
21
|
+
├── edit.html.erb
|
|
22
|
+
├── _article.html.erb
|
|
23
|
+
└── _form.html.erb
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Layout Template
|
|
27
|
+
|
|
28
|
+
```erb
|
|
29
|
+
<!DOCTYPE html>
|
|
30
|
+
<html lang="<%= I18n.locale %>" class="h-full">
|
|
31
|
+
<head>
|
|
32
|
+
<meta charset="UTF-8">
|
|
33
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
34
|
+
<title><%= content_for(:title) || "App Name" %></title>
|
|
35
|
+
<%= csrf_meta_tags %>
|
|
36
|
+
<%= csp_meta_tag %>
|
|
37
|
+
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
|
|
38
|
+
<%= javascript_importmap_tags %>
|
|
39
|
+
</head>
|
|
40
|
+
<body class="h-full bg-gray-50">
|
|
41
|
+
<%= render "shared/navigation" %>
|
|
42
|
+
|
|
43
|
+
<main class="container mx-auto px-4 py-8">
|
|
44
|
+
<%= render "shared/flash" %>
|
|
45
|
+
<%= yield %>
|
|
46
|
+
</main>
|
|
47
|
+
|
|
48
|
+
<%= render "shared/footer" %>
|
|
49
|
+
</body>
|
|
50
|
+
</html>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Flash Messages
|
|
54
|
+
|
|
55
|
+
```erb
|
|
56
|
+
<%# app/views/shared/_flash.html.erb %>
|
|
57
|
+
<div id="flash" class="fixed top-4 right-4 z-50 space-y-2">
|
|
58
|
+
<% flash.each do |type, message| %>
|
|
59
|
+
<div class="<%= flash_class(type) %> px-4 py-3 rounded-lg shadow-lg flex items-center gap-2"
|
|
60
|
+
data-controller="flash"
|
|
61
|
+
data-flash-target="message">
|
|
62
|
+
<span><%= message %></span>
|
|
63
|
+
<button type="button" data-action="flash#dismiss" class="ml-auto">
|
|
64
|
+
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
|
65
|
+
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"/>
|
|
66
|
+
</svg>
|
|
67
|
+
</button>
|
|
68
|
+
</div>
|
|
69
|
+
<% end %>
|
|
70
|
+
</div>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Index View
|
|
74
|
+
|
|
75
|
+
```erb
|
|
76
|
+
<%# app/views/articles/index.html.erb %>
|
|
77
|
+
<% content_for :title, "Articles" %>
|
|
78
|
+
|
|
79
|
+
<div class="flex justify-between items-center mb-6">
|
|
80
|
+
<h1 class="text-2xl font-bold text-gray-900">Articles</h1>
|
|
81
|
+
<%= link_to "New Article", new_article_path,
|
|
82
|
+
class: "bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg" %>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<%= turbo_frame_tag "articles" do %>
|
|
86
|
+
<div class="space-y-4">
|
|
87
|
+
<%= render @articles %>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<% if @articles.empty? %>
|
|
91
|
+
<div class="text-center py-12 text-gray-500">
|
|
92
|
+
<p>No articles yet.</p>
|
|
93
|
+
<%= link_to "Create your first article", new_article_path, class: "text-blue-600 hover:underline" %>
|
|
94
|
+
</div>
|
|
95
|
+
<% end %>
|
|
96
|
+
<% end %>
|
|
97
|
+
|
|
98
|
+
<%= render "shared/pagination", pagy: @pagy %>
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Show View
|
|
102
|
+
|
|
103
|
+
```erb
|
|
104
|
+
<%# app/views/articles/show.html.erb %>
|
|
105
|
+
<% content_for :title, @article.title %>
|
|
106
|
+
|
|
107
|
+
<article class="max-w-3xl mx-auto">
|
|
108
|
+
<header class="mb-8">
|
|
109
|
+
<h1 class="text-3xl font-bold text-gray-900 mb-2"><%= @article.title %></h1>
|
|
110
|
+
<div class="flex items-center gap-4 text-sm text-gray-500">
|
|
111
|
+
<span>By <%= @article.user.name %></span>
|
|
112
|
+
<time datetime="<%= @article.created_at.iso8601 %>">
|
|
113
|
+
<%= @article.created_at.strftime("%B %d, %Y") %>
|
|
114
|
+
</time>
|
|
115
|
+
</div>
|
|
116
|
+
</header>
|
|
117
|
+
|
|
118
|
+
<div class="prose prose-lg max-w-none">
|
|
119
|
+
<%= simple_format(@article.body) %>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
<footer class="mt-8 pt-8 border-t flex gap-4">
|
|
123
|
+
<%= link_to "Edit", edit_article_path(@article), class: "text-blue-600 hover:underline" %>
|
|
124
|
+
<%= button_to "Delete", @article, method: :delete,
|
|
125
|
+
data: { turbo_confirm: "Are you sure?" },
|
|
126
|
+
class: "text-red-600 hover:underline" %>
|
|
127
|
+
<%= link_to "Back", articles_path, class: "text-gray-600 hover:underline" %>
|
|
128
|
+
</footer>
|
|
129
|
+
</article>
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Form Partial
|
|
133
|
+
|
|
134
|
+
```erb
|
|
135
|
+
<%# app/views/articles/_form.html.erb %>
|
|
136
|
+
<%= form_with model: article, class: "space-y-6" do |f| %>
|
|
137
|
+
<% if article.errors.any? %>
|
|
138
|
+
<div class="bg-red-50 border border-red-200 rounded-lg p-4">
|
|
139
|
+
<h3 class="text-red-800 font-medium mb-2">Please fix the following errors:</h3>
|
|
140
|
+
<ul class="list-disc list-inside text-red-700 text-sm">
|
|
141
|
+
<% article.errors.full_messages.each do |error| %>
|
|
142
|
+
<li><%= error %></li>
|
|
143
|
+
<% end %>
|
|
144
|
+
</ul>
|
|
145
|
+
</div>
|
|
146
|
+
<% end %>
|
|
147
|
+
|
|
148
|
+
<div>
|
|
149
|
+
<%= f.label :title, class: "block text-sm font-medium text-gray-700 mb-1" %>
|
|
150
|
+
<%= f.text_field :title,
|
|
151
|
+
class: "w-full rounded-lg border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500",
|
|
152
|
+
required: true %>
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
<div>
|
|
156
|
+
<%= f.label :body, class: "block text-sm font-medium text-gray-700 mb-1" %>
|
|
157
|
+
<%= f.text_area :body,
|
|
158
|
+
rows: 10,
|
|
159
|
+
class: "w-full rounded-lg border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500" %>
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
<div class="flex items-center gap-2">
|
|
163
|
+
<%= f.check_box :published, class: "rounded border-gray-300 text-blue-600 focus:ring-blue-500" %>
|
|
164
|
+
<%= f.label :published, "Publish immediately", class: "text-sm text-gray-700" %>
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
<div class="flex gap-4">
|
|
168
|
+
<%= f.submit class: "bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-lg cursor-pointer" %>
|
|
169
|
+
<%= link_to "Cancel", articles_path, class: "text-gray-600 hover:underline py-2" %>
|
|
170
|
+
</div>
|
|
171
|
+
<% end %>
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Card Partial
|
|
175
|
+
|
|
176
|
+
```erb
|
|
177
|
+
<%# app/views/articles/_article.html.erb %>
|
|
178
|
+
<%= turbo_frame_tag dom_id(article) do %>
|
|
179
|
+
<article class="bg-white rounded-lg shadow p-6 hover:shadow-md transition-shadow">
|
|
180
|
+
<h2 class="text-xl font-semibold mb-2">
|
|
181
|
+
<%= link_to article.title, article, class: "text-gray-900 hover:text-blue-600" %>
|
|
182
|
+
</h2>
|
|
183
|
+
|
|
184
|
+
<p class="text-gray-600 mb-4 line-clamp-2"><%= truncate(article.body, length: 150) %></p>
|
|
185
|
+
|
|
186
|
+
<div class="flex items-center justify-between text-sm text-gray-500">
|
|
187
|
+
<span><%= article.user.name %></span>
|
|
188
|
+
<time><%= time_ago_in_words(article.created_at) %> ago</time>
|
|
189
|
+
</div>
|
|
190
|
+
</article>
|
|
191
|
+
<% end %>
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Helper Methods
|
|
195
|
+
|
|
196
|
+
```ruby
|
|
197
|
+
# app/helpers/application_helper.rb
|
|
198
|
+
module ApplicationHelper
|
|
199
|
+
def flash_class(type)
|
|
200
|
+
case type.to_sym
|
|
201
|
+
when :notice, :success
|
|
202
|
+
"bg-green-100 text-green-800 border border-green-200"
|
|
203
|
+
when :alert, :error
|
|
204
|
+
"bg-red-100 text-red-800 border border-red-200"
|
|
205
|
+
when :warning
|
|
206
|
+
"bg-yellow-100 text-yellow-800 border border-yellow-200"
|
|
207
|
+
else
|
|
208
|
+
"bg-blue-100 text-blue-800 border border-blue-200"
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def active_link_class(path)
|
|
213
|
+
current_page?(path) ? "text-blue-600 font-medium" : "text-gray-600 hover:text-gray-900"
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## Responsive Design
|
|
219
|
+
|
|
220
|
+
```erb
|
|
221
|
+
<%# Mobile-first grid %>
|
|
222
|
+
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
223
|
+
<%= render @articles %>
|
|
224
|
+
</div>
|
|
225
|
+
|
|
226
|
+
<%# Responsive navigation %>
|
|
227
|
+
<nav class="flex flex-col md:flex-row md:items-center gap-4">
|
|
228
|
+
<%= link_to "Home", root_path %>
|
|
229
|
+
<%= link_to "Articles", articles_path %>
|
|
230
|
+
</nav>
|
|
231
|
+
|
|
232
|
+
<%# Hidden on mobile %>
|
|
233
|
+
<div class="hidden md:block">Desktop only</div>
|
|
234
|
+
|
|
235
|
+
<%# Visible on mobile %>
|
|
236
|
+
<div class="md:hidden">Mobile only</div>
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## Turbo Frames
|
|
240
|
+
|
|
241
|
+
```erb
|
|
242
|
+
<%# Lazy loading %>
|
|
243
|
+
<%= turbo_frame_tag "comments", src: article_comments_path(@article), loading: :lazy do %>
|
|
244
|
+
<p class="text-gray-500">Loading comments...</p>
|
|
245
|
+
<% end %>
|
|
246
|
+
|
|
247
|
+
<%# Modal frame %>
|
|
248
|
+
<%= turbo_frame_tag "modal" %>
|
|
249
|
+
|
|
250
|
+
<%# Form that updates frame %>
|
|
251
|
+
<%= turbo_frame_tag "search_results" do %>
|
|
252
|
+
<%= form_with url: search_path, method: :get, data: { turbo_frame: "search_results" } do |f| %>
|
|
253
|
+
<%= f.search_field :q, placeholder: "Search..." %>
|
|
254
|
+
<% end %>
|
|
255
|
+
|
|
256
|
+
<div id="results">
|
|
257
|
+
<%= render @results %>
|
|
258
|
+
</div>
|
|
259
|
+
<% end %>
|
|
260
|
+
```
|