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,351 @@
|
|
|
1
|
+
# Skill: File Uploads
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
Handle file uploads using Active Storage with local disk storage.
|
|
5
|
+
|
|
6
|
+
## Setup
|
|
7
|
+
|
|
8
|
+
### Install Active Storage
|
|
9
|
+
```bash
|
|
10
|
+
rails active_storage:install
|
|
11
|
+
rails db:migrate
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
### Configure Storage
|
|
15
|
+
```yaml
|
|
16
|
+
# config/storage.yml
|
|
17
|
+
local:
|
|
18
|
+
service: Disk
|
|
19
|
+
root: <%= Rails.root.join("storage") %>
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
# config/environments/development.rb
|
|
24
|
+
config.active_storage.service = :local
|
|
25
|
+
|
|
26
|
+
# config/environments/production.rb
|
|
27
|
+
config.active_storage.service = :local # Or cloud service
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Model Attachments
|
|
31
|
+
|
|
32
|
+
### Single Attachment
|
|
33
|
+
```ruby
|
|
34
|
+
class User < ApplicationRecord
|
|
35
|
+
has_one_attached :avatar
|
|
36
|
+
|
|
37
|
+
# With validation
|
|
38
|
+
validates :avatar, content_type: ['image/png', 'image/jpeg'],
|
|
39
|
+
size: { less_than: 5.megabytes }
|
|
40
|
+
end
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Multiple Attachments
|
|
44
|
+
```ruby
|
|
45
|
+
class Article < ApplicationRecord
|
|
46
|
+
has_many_attached :images
|
|
47
|
+
|
|
48
|
+
validates :images, content_type: ['image/png', 'image/jpeg', 'image/webp'],
|
|
49
|
+
size: { less_than: 10.megabytes },
|
|
50
|
+
limit: { max: 10 }
|
|
51
|
+
end
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### With Variants (Image Processing)
|
|
55
|
+
```ruby
|
|
56
|
+
# Gemfile
|
|
57
|
+
gem "image_processing", "~> 1.2"
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
class User < ApplicationRecord
|
|
62
|
+
has_one_attached :avatar do |attachable|
|
|
63
|
+
attachable.variant :thumb, resize_to_limit: [100, 100]
|
|
64
|
+
attachable.variant :medium, resize_to_limit: [300, 300]
|
|
65
|
+
attachable.variant :large, resize_to_limit: [800, 800]
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Controller Handling
|
|
71
|
+
|
|
72
|
+
### Single File Upload
|
|
73
|
+
```ruby
|
|
74
|
+
class UsersController < ApplicationController
|
|
75
|
+
def update
|
|
76
|
+
if current_user.update(user_params)
|
|
77
|
+
redirect_to current_user, notice: "Profile updated."
|
|
78
|
+
else
|
|
79
|
+
render :edit, status: :unprocessable_entity
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
def user_params
|
|
86
|
+
params.require(:user).permit(:name, :email, :avatar)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Multiple File Upload
|
|
92
|
+
```ruby
|
|
93
|
+
class ArticlesController < ApplicationController
|
|
94
|
+
def create
|
|
95
|
+
@article = current_user.articles.build(article_params)
|
|
96
|
+
|
|
97
|
+
if @article.save
|
|
98
|
+
redirect_to @article
|
|
99
|
+
else
|
|
100
|
+
render :new, status: :unprocessable_entity
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def update
|
|
105
|
+
@article = Article.find(params[:id])
|
|
106
|
+
|
|
107
|
+
if @article.update(article_params)
|
|
108
|
+
redirect_to @article
|
|
109
|
+
else
|
|
110
|
+
render :edit, status: :unprocessable_entity
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
private
|
|
115
|
+
|
|
116
|
+
def article_params
|
|
117
|
+
params.require(:article).permit(:title, :body, images: [])
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Removing Attachments
|
|
123
|
+
```ruby
|
|
124
|
+
class UsersController < ApplicationController
|
|
125
|
+
def remove_avatar
|
|
126
|
+
current_user.avatar.purge
|
|
127
|
+
redirect_to edit_user_path(current_user), notice: "Avatar removed."
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Routes
|
|
132
|
+
resources :users do
|
|
133
|
+
member do
|
|
134
|
+
delete :remove_avatar
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Form Helpers
|
|
140
|
+
|
|
141
|
+
### Single File
|
|
142
|
+
```erb
|
|
143
|
+
<%= form_with model: @user do |f| %>
|
|
144
|
+
<div>
|
|
145
|
+
<%= f.label :avatar %>
|
|
146
|
+
<%= f.file_field :avatar,
|
|
147
|
+
accept: "image/png,image/jpeg",
|
|
148
|
+
class: "block w-full text-sm text-gray-500
|
|
149
|
+
file:mr-4 file:py-2 file:px-4
|
|
150
|
+
file:rounded file:border-0
|
|
151
|
+
file:text-sm file:font-semibold
|
|
152
|
+
file:bg-blue-50 file:text-blue-700
|
|
153
|
+
hover:file:bg-blue-100" %>
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
<% if @user.avatar.attached? %>
|
|
157
|
+
<div class="mt-2">
|
|
158
|
+
<%= image_tag @user.avatar.variant(:thumb), class: "rounded" %>
|
|
159
|
+
<%= link_to "Remove", remove_avatar_user_path(@user),
|
|
160
|
+
method: :delete,
|
|
161
|
+
data: { turbo_confirm: "Remove avatar?" },
|
|
162
|
+
class: "text-red-600 text-sm" %>
|
|
163
|
+
</div>
|
|
164
|
+
<% end %>
|
|
165
|
+
|
|
166
|
+
<%= f.submit "Save" %>
|
|
167
|
+
<% end %>
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Multiple Files
|
|
171
|
+
```erb
|
|
172
|
+
<%= form_with model: @article do |f| %>
|
|
173
|
+
<div>
|
|
174
|
+
<%= f.label :images %>
|
|
175
|
+
<%= f.file_field :images,
|
|
176
|
+
multiple: true,
|
|
177
|
+
accept: "image/*",
|
|
178
|
+
class: "block w-full" %>
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
<% if @article.images.attached? %>
|
|
182
|
+
<div class="grid grid-cols-3 gap-4 mt-4">
|
|
183
|
+
<% @article.images.each do |image| %>
|
|
184
|
+
<div class="relative">
|
|
185
|
+
<%= image_tag image.variant(resize_to_limit: [200, 200]), class: "rounded" %>
|
|
186
|
+
<%= button_to "×",
|
|
187
|
+
purge_image_article_path(@article, image_id: image.id),
|
|
188
|
+
method: :delete,
|
|
189
|
+
class: "absolute top-1 right-1 bg-red-500 text-white rounded-full w-6 h-6" %>
|
|
190
|
+
</div>
|
|
191
|
+
<% end %>
|
|
192
|
+
</div>
|
|
193
|
+
<% end %>
|
|
194
|
+
|
|
195
|
+
<%= f.submit "Save" %>
|
|
196
|
+
<% end %>
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Direct Upload (JavaScript)
|
|
200
|
+
```erb
|
|
201
|
+
<%= form_with model: @article, data: { controller: "upload" } do |f| %>
|
|
202
|
+
<%= f.file_field :images,
|
|
203
|
+
multiple: true,
|
|
204
|
+
direct_upload: true,
|
|
205
|
+
data: {
|
|
206
|
+
upload_target: "input",
|
|
207
|
+
action: "change->upload#upload"
|
|
208
|
+
} %>
|
|
209
|
+
|
|
210
|
+
<div data-upload-target="progress" class="hidden">
|
|
211
|
+
<div class="bg-blue-600 h-2 rounded" data-upload-target="bar" style="width: 0%"></div>
|
|
212
|
+
</div>
|
|
213
|
+
<% end %>
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
```javascript
|
|
217
|
+
// app/javascript/controllers/upload_controller.js
|
|
218
|
+
import { Controller } from "@hotwired/stimulus"
|
|
219
|
+
import { DirectUpload } from "@rails/activestorage"
|
|
220
|
+
|
|
221
|
+
export default class extends Controller {
|
|
222
|
+
static targets = ["input", "progress", "bar"]
|
|
223
|
+
|
|
224
|
+
upload() {
|
|
225
|
+
Array.from(this.inputTarget.files).forEach(file => {
|
|
226
|
+
const upload = new DirectUpload(file, this.inputTarget.dataset.directUploadUrl, this)
|
|
227
|
+
|
|
228
|
+
upload.create((error, blob) => {
|
|
229
|
+
if (error) {
|
|
230
|
+
console.error(error)
|
|
231
|
+
} else {
|
|
232
|
+
// Append hidden field with signed_id
|
|
233
|
+
const hiddenField = document.createElement("input")
|
|
234
|
+
hiddenField.type = "hidden"
|
|
235
|
+
hiddenField.name = this.inputTarget.name
|
|
236
|
+
hiddenField.value = blob.signed_id
|
|
237
|
+
this.element.appendChild(hiddenField)
|
|
238
|
+
}
|
|
239
|
+
})
|
|
240
|
+
})
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
directUploadWillStoreFileWithXHR(request) {
|
|
244
|
+
this.progressTarget.classList.remove("hidden")
|
|
245
|
+
request.upload.addEventListener("progress", event => {
|
|
246
|
+
const progress = (event.loaded / event.total) * 100
|
|
247
|
+
this.barTarget.style.width = `${progress}%`
|
|
248
|
+
})
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
## Displaying Attachments
|
|
254
|
+
|
|
255
|
+
### Images
|
|
256
|
+
```erb
|
|
257
|
+
<%# Original %>
|
|
258
|
+
<%= image_tag @user.avatar %>
|
|
259
|
+
|
|
260
|
+
<%# With variant %>
|
|
261
|
+
<%= image_tag @user.avatar.variant(:thumb) %>
|
|
262
|
+
|
|
263
|
+
<%# With fallback %>
|
|
264
|
+
<% if @user.avatar.attached? %>
|
|
265
|
+
<%= image_tag @user.avatar.variant(:medium), class: "rounded-full" %>
|
|
266
|
+
<% else %>
|
|
267
|
+
<%= image_tag "default-avatar.png", class: "rounded-full" %>
|
|
268
|
+
<% end %>
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Files/Documents
|
|
272
|
+
```erb
|
|
273
|
+
<% if @document.file.attached? %>
|
|
274
|
+
<div class="flex items-center gap-2">
|
|
275
|
+
<span><%= @document.file.filename %></span>
|
|
276
|
+
<span class="text-gray-500">(<%= number_to_human_size(@document.file.byte_size) %>)</span>
|
|
277
|
+
<%= link_to "Download", rails_blob_path(@document.file, disposition: "attachment"),
|
|
278
|
+
class: "text-blue-600" %>
|
|
279
|
+
</div>
|
|
280
|
+
<% end %>
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### PDF Preview
|
|
284
|
+
```erb
|
|
285
|
+
<% if @document.file.content_type == "application/pdf" %>
|
|
286
|
+
<iframe src="<%= rails_blob_path(@document.file) %>"
|
|
287
|
+
class="w-full h-96 border rounded"></iframe>
|
|
288
|
+
<% end %>
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
## Validations
|
|
292
|
+
|
|
293
|
+
### Custom Validator
|
|
294
|
+
```ruby
|
|
295
|
+
# app/validators/content_type_validator.rb
|
|
296
|
+
class ContentTypeValidator < ActiveModel::EachValidator
|
|
297
|
+
def validate_each(record, attribute, value)
|
|
298
|
+
return unless value.attached?
|
|
299
|
+
|
|
300
|
+
allowed = Array(options[:in] || options[:with])
|
|
301
|
+
|
|
302
|
+
files = value.is_a?(ActiveStorage::Attached::Many) ? value : [value]
|
|
303
|
+
|
|
304
|
+
files.each do |file|
|
|
305
|
+
unless allowed.include?(file.content_type)
|
|
306
|
+
record.errors.add(attribute, :invalid_content_type)
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
### Usage
|
|
314
|
+
```ruby
|
|
315
|
+
class Article < ApplicationRecord
|
|
316
|
+
has_many_attached :images
|
|
317
|
+
|
|
318
|
+
validates :images, content_type: { in: %w[image/png image/jpeg image/webp] }
|
|
319
|
+
validates :images, size: { less_than: 5.megabytes }
|
|
320
|
+
end
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
## Background Processing
|
|
324
|
+
|
|
325
|
+
### Analyze on Upload
|
|
326
|
+
```ruby
|
|
327
|
+
# Active Storage analyzes files automatically in background
|
|
328
|
+
# Configure in config/application.rb
|
|
329
|
+
config.active_storage.queues.analysis = :default
|
|
330
|
+
config.active_storage.queues.purge = :default
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
### Custom Processing Job
|
|
334
|
+
```ruby
|
|
335
|
+
class ProcessImageJob < ApplicationJob
|
|
336
|
+
queue_as :default
|
|
337
|
+
|
|
338
|
+
def perform(image_id)
|
|
339
|
+
image = ActiveStorage::Blob.find(image_id)
|
|
340
|
+
# Custom processing
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
## Best Practices
|
|
346
|
+
|
|
347
|
+
1. **Validate content types** - Prevent malicious uploads
|
|
348
|
+
2. **Set size limits** - Protect storage and bandwidth
|
|
349
|
+
3. **Use variants** - Don't serve original large images
|
|
350
|
+
4. **Enable direct uploads** - Better UX for large files
|
|
351
|
+
5. **Purge unused files** - Clean up orphaned attachments
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
# Skill: Hotwire Native
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
Build native iOS and Android apps using the existing Rails views with Hotwire Native (formerly Turbo Native).
|
|
5
|
+
|
|
6
|
+
## Overview
|
|
7
|
+
|
|
8
|
+
Hotwire Native wraps your Rails web app in a native shell, providing:
|
|
9
|
+
- Native navigation (push/pop)
|
|
10
|
+
- Native UI elements (tab bars, toolbars)
|
|
11
|
+
- Bridge components for native features
|
|
12
|
+
- Offline support
|
|
13
|
+
- Push notifications
|
|
14
|
+
|
|
15
|
+
## iOS Setup
|
|
16
|
+
|
|
17
|
+
### Project Structure
|
|
18
|
+
```
|
|
19
|
+
ios/
|
|
20
|
+
├── App/
|
|
21
|
+
│ ├── AppDelegate.swift
|
|
22
|
+
│ ├── SceneDelegate.swift
|
|
23
|
+
│ └── Configuration.swift
|
|
24
|
+
├── Navigation/
|
|
25
|
+
│ ├── Navigator.swift
|
|
26
|
+
│ └── PathConfiguration.swift
|
|
27
|
+
└── Bridge/
|
|
28
|
+
└── Components/
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Basic Configuration
|
|
32
|
+
```swift
|
|
33
|
+
// Configuration.swift
|
|
34
|
+
import HotwireNative
|
|
35
|
+
|
|
36
|
+
struct Configuration {
|
|
37
|
+
static let baseURL = URL(string: "https://yourapp.com")!
|
|
38
|
+
|
|
39
|
+
static var pathConfiguration: PathConfiguration {
|
|
40
|
+
PathConfiguration(sources: [
|
|
41
|
+
.server(baseURL.appendingPathComponent("/configurations/ios_v1.json"))
|
|
42
|
+
])
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Scene Delegate
|
|
48
|
+
```swift
|
|
49
|
+
// SceneDelegate.swift
|
|
50
|
+
import UIKit
|
|
51
|
+
import HotwireNative
|
|
52
|
+
|
|
53
|
+
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|
54
|
+
var window: UIWindow?
|
|
55
|
+
private let navigator = Navigator()
|
|
56
|
+
|
|
57
|
+
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options: UIScene.ConnectionOptions) {
|
|
58
|
+
guard let windowScene = scene as? UIWindowScene else { return }
|
|
59
|
+
|
|
60
|
+
window = UIWindow(windowScene: windowScene)
|
|
61
|
+
window?.rootViewController = navigator.rootViewController
|
|
62
|
+
window?.makeKeyAndVisible()
|
|
63
|
+
|
|
64
|
+
navigator.route(Configuration.baseURL)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Android Setup
|
|
70
|
+
|
|
71
|
+
### Project Structure
|
|
72
|
+
```
|
|
73
|
+
android/
|
|
74
|
+
├── app/src/main/
|
|
75
|
+
│ ├── java/com/yourapp/
|
|
76
|
+
│ │ ├── MainActivity.kt
|
|
77
|
+
│ │ ├── MainSessionNavHostFragment.kt
|
|
78
|
+
│ │ └── bridge/
|
|
79
|
+
│ └── res/
|
|
80
|
+
│ └── navigation/
|
|
81
|
+
│ └── main.xml
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Main Activity
|
|
85
|
+
```kotlin
|
|
86
|
+
// MainActivity.kt
|
|
87
|
+
import android.os.Bundle
|
|
88
|
+
import dev.hotwire.navigation.activities.HotwireActivity
|
|
89
|
+
import dev.hotwire.navigation.navigator.NavigatorConfiguration
|
|
90
|
+
|
|
91
|
+
class MainActivity : HotwireActivity() {
|
|
92
|
+
override fun onCreate(savedInstanceState: Bundle?) {
|
|
93
|
+
super.onCreate(savedInstanceState)
|
|
94
|
+
setContentView(R.layout.activity_main)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
override fun navigatorConfigurations() = listOf(
|
|
98
|
+
NavigatorConfiguration(
|
|
99
|
+
name = "main",
|
|
100
|
+
startLocation = "https://yourapp.com",
|
|
101
|
+
navigatorHostId = R.id.main_nav_host
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Path Configuration
|
|
108
|
+
|
|
109
|
+
### Server-Side JSON
|
|
110
|
+
```json
|
|
111
|
+
// public/configurations/ios_v1.json
|
|
112
|
+
{
|
|
113
|
+
"settings": {
|
|
114
|
+
"screenshots_enabled": true
|
|
115
|
+
},
|
|
116
|
+
"rules": [
|
|
117
|
+
{
|
|
118
|
+
"patterns": ["/"],
|
|
119
|
+
"properties": {
|
|
120
|
+
"presentation": "default"
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
"patterns": ["/new$", "/edit$"],
|
|
125
|
+
"properties": {
|
|
126
|
+
"presentation": "modal"
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
"patterns": ["/articles/\\d+"],
|
|
131
|
+
"properties": {
|
|
132
|
+
"presentation": "push"
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
]
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Presentation Types
|
|
140
|
+
- `default` - Standard push navigation
|
|
141
|
+
- `modal` - Present as modal sheet
|
|
142
|
+
- `replace` - Replace current screen
|
|
143
|
+
- `pop` - Pop back
|
|
144
|
+
- `refresh` - Refresh current
|
|
145
|
+
- `none` - Handle in native code
|
|
146
|
+
|
|
147
|
+
## Bridge Components
|
|
148
|
+
|
|
149
|
+
### Rails Helper
|
|
150
|
+
```ruby
|
|
151
|
+
# app/helpers/hotwire_native_helper.rb
|
|
152
|
+
module HotwireNativeHelper
|
|
153
|
+
def hotwire_native_app?
|
|
154
|
+
request.user_agent&.include?("Hotwire Native")
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def hotwire_native_ios?
|
|
158
|
+
request.user_agent&.include?("Hotwire Native iOS")
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def hotwire_native_android?
|
|
162
|
+
request.user_agent&.include?("Hotwire Native Android")
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Conditional Rendering
|
|
168
|
+
```erb
|
|
169
|
+
<% if hotwire_native_app? %>
|
|
170
|
+
<%# Native-specific UI %>
|
|
171
|
+
<div data-bridge-component="navbar" data-bridge-title="<%= @article.title %>"></div>
|
|
172
|
+
<% else %>
|
|
173
|
+
<%# Web navigation %>
|
|
174
|
+
<%= render "shared/navigation" %>
|
|
175
|
+
<% end %>
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Bridge Component (JavaScript)
|
|
179
|
+
```javascript
|
|
180
|
+
// app/javascript/controllers/bridge/button_controller.js
|
|
181
|
+
import { BridgeComponent } from "@hotwired/hotwire-native-bridge"
|
|
182
|
+
|
|
183
|
+
export default class extends BridgeComponent {
|
|
184
|
+
static component = "button"
|
|
185
|
+
static targets = ["button"]
|
|
186
|
+
|
|
187
|
+
connect() {
|
|
188
|
+
super.connect()
|
|
189
|
+
this.send("connect", { title: this.buttonTarget.textContent })
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Called from native
|
|
193
|
+
clicked() {
|
|
194
|
+
this.buttonTarget.click()
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### iOS Bridge Handler
|
|
200
|
+
```swift
|
|
201
|
+
// ButtonComponent.swift
|
|
202
|
+
import HotwireNative
|
|
203
|
+
|
|
204
|
+
final class ButtonComponent: BridgeComponent {
|
|
205
|
+
override class var name: String { "button" }
|
|
206
|
+
|
|
207
|
+
override func onReceive(message: Message) {
|
|
208
|
+
guard let event = Event(rawValue: message.event) else { return }
|
|
209
|
+
|
|
210
|
+
switch event {
|
|
211
|
+
case .connect:
|
|
212
|
+
handleConnect(message: message)
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private func handleConnect(message: Message) {
|
|
217
|
+
guard let data: ConnectData = message.data() else { return }
|
|
218
|
+
// Add native button to toolbar
|
|
219
|
+
addButton(title: data.title)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private func addButton(title: String) {
|
|
223
|
+
let button = UIBarButtonItem(title: title, style: .plain, target: self, action: #selector(buttonTapped))
|
|
224
|
+
delegate?.webView?.viewController?.navigationItem.rightBarButtonItem = button
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
@objc private func buttonTapped() {
|
|
228
|
+
reply(to: "connect", with: MessageData(action: "clicked"))
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
private extension ButtonComponent {
|
|
233
|
+
enum Event: String {
|
|
234
|
+
case connect
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
struct ConnectData: Decodable {
|
|
238
|
+
let title: String
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
## Native Features
|
|
244
|
+
|
|
245
|
+
### Pull to Refresh
|
|
246
|
+
```erb
|
|
247
|
+
<%# Enable in layout %>
|
|
248
|
+
<body data-turbo-refresh-method="morph" data-turbo-refresh-scroll="preserve">
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Native Form Inputs
|
|
252
|
+
```erb
|
|
253
|
+
<% if hotwire_native_app? %>
|
|
254
|
+
<%= f.date_field :date, data: { bridge_component: "date-picker" } %>
|
|
255
|
+
<% else %>
|
|
256
|
+
<%= f.date_field :date %>
|
|
257
|
+
<% end %>
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### Camera/Photos
|
|
261
|
+
```erb
|
|
262
|
+
<div data-controller="bridge--photo" data-bridge-component="photo">
|
|
263
|
+
<button data-action="bridge--photo#capture">Take Photo</button>
|
|
264
|
+
<input type="hidden" name="photo_data" data-bridge--photo-target="input">
|
|
265
|
+
</div>
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
## Styling for Native
|
|
269
|
+
|
|
270
|
+
```erb
|
|
271
|
+
<%# app/views/layouts/application.html.erb %>
|
|
272
|
+
<html class="<%= 'hotwire-native' if hotwire_native_app? %>">
|
|
273
|
+
|
|
274
|
+
<%# In CSS %>
|
|
275
|
+
<style>
|
|
276
|
+
/* Hide web-only elements in native */
|
|
277
|
+
.hotwire-native .web-only {
|
|
278
|
+
display: none;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/* Safe area insets */
|
|
282
|
+
.hotwire-native body {
|
|
283
|
+
padding-top: env(safe-area-inset-top);
|
|
284
|
+
padding-bottom: env(safe-area-inset-bottom);
|
|
285
|
+
}
|
|
286
|
+
</style>
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
## Best Practices
|
|
290
|
+
|
|
291
|
+
1. **Mobile-first design** - Views should work well on small screens
|
|
292
|
+
2. **Use semantic HTML** - Native can style based on elements
|
|
293
|
+
3. **Minimize JavaScript** - Let native handle interactions
|
|
294
|
+
4. **Test on devices** - Simulator ≠ real device
|
|
295
|
+
5. **Handle offline** - Show appropriate messages
|
|
296
|
+
6. **Fast responses** - Native users expect instant feedback
|