agent-notes 2.0.4__py3-none-any.whl
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.
- agent_notes/VERSION +1 -0
- agent_notes/__init__.py +1 -0
- agent_notes/__main__.py +4 -0
- agent_notes/cli.py +348 -0
- agent_notes/commands/__init__.py +27 -0
- agent_notes/commands/_install_helpers.py +262 -0
- agent_notes/commands/build.py +170 -0
- agent_notes/commands/doctor.py +112 -0
- agent_notes/commands/info.py +95 -0
- agent_notes/commands/install.py +99 -0
- agent_notes/commands/list.py +169 -0
- agent_notes/commands/memory.py +430 -0
- agent_notes/commands/regenerate.py +152 -0
- agent_notes/commands/set_role.py +143 -0
- agent_notes/commands/uninstall.py +26 -0
- agent_notes/commands/update.py +169 -0
- agent_notes/commands/validate.py +199 -0
- agent_notes/commands/wizard.py +720 -0
- agent_notes/config.py +154 -0
- agent_notes/data/agents/agents.yaml +352 -0
- agent_notes/data/agents/analyst.md +45 -0
- agent_notes/data/agents/api-reviewer.md +47 -0
- agent_notes/data/agents/architect.md +46 -0
- agent_notes/data/agents/coder.md +28 -0
- agent_notes/data/agents/database-specialist.md +45 -0
- agent_notes/data/agents/debugger.md +47 -0
- agent_notes/data/agents/devil.md +47 -0
- agent_notes/data/agents/devops.md +38 -0
- agent_notes/data/agents/explorer.md +23 -0
- agent_notes/data/agents/integrations.md +44 -0
- agent_notes/data/agents/lead.md +216 -0
- agent_notes/data/agents/performance-profiler.md +44 -0
- agent_notes/data/agents/refactorer.md +48 -0
- agent_notes/data/agents/reviewer.md +44 -0
- agent_notes/data/agents/security-auditor.md +44 -0
- agent_notes/data/agents/system-auditor.md +38 -0
- agent_notes/data/agents/tech-writer.md +32 -0
- agent_notes/data/agents/test-runner.md +36 -0
- agent_notes/data/agents/test-writer.md +39 -0
- agent_notes/data/cli/claude.yaml +25 -0
- agent_notes/data/cli/copilot.yaml +18 -0
- agent_notes/data/cli/opencode.yaml +22 -0
- agent_notes/data/commands/brainstorm.md +8 -0
- agent_notes/data/commands/debug.md +9 -0
- agent_notes/data/commands/review.md +10 -0
- agent_notes/data/global-claude.md +290 -0
- agent_notes/data/global-copilot.md +27 -0
- agent_notes/data/global-opencode.md +40 -0
- agent_notes/data/hooks/session-context.md.tpl +19 -0
- agent_notes/data/models/claude-haiku-4-5.yaml +15 -0
- agent_notes/data/models/claude-opus-4-1.yaml +16 -0
- agent_notes/data/models/claude-opus-4-5.yaml +16 -0
- agent_notes/data/models/claude-opus-4-6.yaml +16 -0
- agent_notes/data/models/claude-opus-4-7.yaml +15 -0
- agent_notes/data/models/claude-sonnet-4-5.yaml +16 -0
- agent_notes/data/models/claude-sonnet-4-6.yaml +15 -0
- agent_notes/data/models/claude-sonnet-4.yaml +16 -0
- agent_notes/data/pricing.yaml +33 -0
- agent_notes/data/roles/orchestrator.yaml +5 -0
- agent_notes/data/roles/reasoner.yaml +5 -0
- agent_notes/data/roles/scout.yaml +5 -0
- agent_notes/data/roles/worker.yaml +5 -0
- agent_notes/data/rules/code-quality.md +9 -0
- agent_notes/data/rules/safety.md +10 -0
- agent_notes/data/scripts/cost-report +211 -0
- agent_notes/data/skills/brainstorming/SKILL.md +57 -0
- agent_notes/data/skills/code-review/SKILL.md +64 -0
- agent_notes/data/skills/debugging-protocol/SKILL.md +51 -0
- agent_notes/data/skills/docker-compose/SKILL.md +318 -0
- agent_notes/data/skills/docker-compose-advanced/SKILL.md +575 -0
- agent_notes/data/skills/docker-dockerfile/SKILL.md +385 -0
- agent_notes/data/skills/docker-dockerfile-languages/SKILL.md +293 -0
- agent_notes/data/skills/git/SKILL.md +87 -0
- agent_notes/data/skills/rails-active-storage/SKILL.md +321 -0
- agent_notes/data/skills/rails-broadcasting/SKILL.md +374 -0
- agent_notes/data/skills/rails-concerns/SKILL.md +806 -0
- agent_notes/data/skills/rails-controllers/SKILL.md +510 -0
- agent_notes/data/skills/rails-controllers-advanced/SKILL.md +441 -0
- agent_notes/data/skills/rails-helpers/SKILL.md +677 -0
- agent_notes/data/skills/rails-initializers/SKILL.md +79 -0
- agent_notes/data/skills/rails-javascript/SKILL.md +567 -0
- agent_notes/data/skills/rails-jobs/SKILL.md +700 -0
- agent_notes/data/skills/rails-kamal/SKILL.md +483 -0
- agent_notes/data/skills/rails-lib/SKILL.md +101 -0
- agent_notes/data/skills/rails-mailers/SKILL.md +321 -0
- agent_notes/data/skills/rails-migrations/SKILL.md +268 -0
- agent_notes/data/skills/rails-models/SKILL.md +459 -0
- agent_notes/data/skills/rails-models-advanced/SKILL.md +398 -0
- agent_notes/data/skills/rails-routes/SKILL.md +804 -0
- agent_notes/data/skills/rails-style/SKILL.md +538 -0
- agent_notes/data/skills/rails-testing-controllers/SKILL.md +343 -0
- agent_notes/data/skills/rails-testing-models/SKILL.md +296 -0
- agent_notes/data/skills/rails-testing-system/SKILL.md +375 -0
- agent_notes/data/skills/rails-validations/SKILL.md +108 -0
- agent_notes/data/skills/rails-view-components/SKILL.md +511 -0
- agent_notes/data/skills/rails-view-components-advanced/SKILL.md +376 -0
- agent_notes/data/skills/rails-views/SKILL.md +413 -0
- agent_notes/data/skills/rails-views-advanced/SKILL.md +450 -0
- agent_notes/data/skills/refactoring-protocol/SKILL.md +64 -0
- agent_notes/data/skills/tdd/SKILL.md +57 -0
- agent_notes/data/templates/__init__.py +1 -0
- agent_notes/data/templates/__pycache__/__init__.cpython-314.pyc +0 -0
- agent_notes/data/templates/frontmatter/__init__.py +1 -0
- agent_notes/data/templates/frontmatter/__pycache__/__init__.cpython-314.pyc +0 -0
- agent_notes/data/templates/frontmatter/__pycache__/claude.cpython-314.pyc +0 -0
- agent_notes/data/templates/frontmatter/__pycache__/cursor.cpython-314.pyc +0 -0
- agent_notes/data/templates/frontmatter/__pycache__/opencode.cpython-314.pyc +0 -0
- agent_notes/data/templates/frontmatter/claude.py +44 -0
- agent_notes/data/templates/frontmatter/opencode.py +104 -0
- agent_notes/doctor_checks.py +189 -0
- agent_notes/domain/__init__.py +17 -0
- agent_notes/domain/agent.py +34 -0
- agent_notes/domain/cli_backend.py +40 -0
- agent_notes/domain/diagnostics.py +29 -0
- agent_notes/domain/diff.py +44 -0
- agent_notes/domain/model.py +27 -0
- agent_notes/domain/role.py +13 -0
- agent_notes/domain/rule.py +13 -0
- agent_notes/domain/skill.py +15 -0
- agent_notes/domain/state.py +46 -0
- agent_notes/install_state.py +11 -0
- agent_notes/registries/__init__.py +16 -0
- agent_notes/registries/_base.py +46 -0
- agent_notes/registries/agent_registry.py +107 -0
- agent_notes/registries/cli_registry.py +89 -0
- agent_notes/registries/model_registry.py +85 -0
- agent_notes/registries/role_registry.py +64 -0
- agent_notes/registries/rule_registry.py +80 -0
- agent_notes/registries/skill_registry.py +141 -0
- agent_notes/services/__init__.py +8 -0
- agent_notes/services/diagnostics/__init__.py +47 -0
- agent_notes/services/diagnostics/_checks.py +272 -0
- agent_notes/services/diagnostics/_display.py +346 -0
- agent_notes/services/diagnostics/_fix.py +169 -0
- agent_notes/services/diff.py +349 -0
- agent_notes/services/fs.py +195 -0
- agent_notes/services/install_state_builder.py +210 -0
- agent_notes/services/installer.py +293 -0
- agent_notes/services/memory_backend.py +155 -0
- agent_notes/services/rendering.py +329 -0
- agent_notes/services/session_context.py +23 -0
- agent_notes/services/settings_writer.py +79 -0
- agent_notes/services/state_store.py +249 -0
- agent_notes/services/ui.py +419 -0
- agent_notes/services/user_config.py +62 -0
- agent_notes/services/validation.py +67 -0
- agent_notes/state.py +21 -0
- agent_notes-2.0.4.dist-info/METADATA +14 -0
- agent_notes-2.0.4.dist-info/RECORD +162 -0
- agent_notes-2.0.4.dist-info/WHEEL +5 -0
- agent_notes-2.0.4.dist-info/entry_points.txt +2 -0
- agent_notes-2.0.4.dist-info/licenses/LICENSE +21 -0
- agent_notes-2.0.4.dist-info/top_level.txt +2 -0
- tests/conftest.py +20 -0
- tests/functional/__init__.py +0 -0
- tests/functional/test_build_commands.py +88 -0
- tests/functional/test_registries.py +128 -0
- tests/integration/__init__.py +0 -0
- tests/integration/test_build_output.py +129 -0
- tests/plugins/__init__.py +0 -0
- tests/plugins/test_agents.py +93 -0
- tests/plugins/test_skills.py +77 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rails-initializers
|
|
3
|
+
description: "Rails configuration initializers: extensions, CORS, filtering, and custom config"
|
|
4
|
+
group: rails
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Initializers Guide
|
|
8
|
+
|
|
9
|
+
Guide for Rails configuration and boot-time setup.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## File Structure
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
config/initializers/
|
|
17
|
+
├── extensions.rb # Load lib/ extensions
|
|
18
|
+
├── assets.rb # Asset pipeline config
|
|
19
|
+
├── filter_parameter_logging.rb
|
|
20
|
+
├── inflections.rb
|
|
21
|
+
└── cors.rb
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Common Initializers
|
|
27
|
+
|
|
28
|
+
### Load Extensions
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
# config/initializers/extensions.rb
|
|
32
|
+
Dir["#{Rails.root}/lib/rails_ext/*"].each { |path|
|
|
33
|
+
require "rails_ext/#{File.basename(path)}"
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Filter Parameters
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
# config/initializers/filter_parameter_logging.rb
|
|
41
|
+
Rails.application.config.filter_parameters += [
|
|
42
|
+
:passw, :secret, :token, :_key, :crypt, :salt, :certificate
|
|
43
|
+
]
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### CORS
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
# config/initializers/cors.rb
|
|
50
|
+
Rails.application.config.middleware.insert_before 0, Rack::Cors do
|
|
51
|
+
allow do
|
|
52
|
+
origins "example.com"
|
|
53
|
+
resource "*", headers: :any, methods: [:get, :post, :put, :delete, :options]
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Custom Configuration
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
# config/initializers/app_config.rb
|
|
62
|
+
Rails.application.config.x.api_key = ENV["API_KEY"]
|
|
63
|
+
Rails.application.config.x.feature_flags = {
|
|
64
|
+
new_dashboard: true,
|
|
65
|
+
experimental_feature: false
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
# Usage
|
|
69
|
+
Rails.configuration.x.api_key
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Summary
|
|
75
|
+
|
|
76
|
+
- **Boot Time**: Code runs when app starts
|
|
77
|
+
- **Configuration**: Set up libraries, features
|
|
78
|
+
- **Load Order**: Matters for dependencies
|
|
79
|
+
- **Environment Specific**: Use ENV for secrets
|
|
@@ -0,0 +1,567 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rails-javascript
|
|
3
|
+
description: "Rails frontend: Stimulus controllers, Turbo patterns, Importmap, and view transitions"
|
|
4
|
+
group: rails
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# JavaScript & Stimulus
|
|
8
|
+
|
|
9
|
+
Frontend patterns with Stimulus, Turbo, and Importmap (Rails 8 defaults).
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Philosophy
|
|
14
|
+
|
|
15
|
+
1. **HTML over the wire** - Server renders HTML, not JSON
|
|
16
|
+
2. **Progressive enhancement** - Works without JavaScript
|
|
17
|
+
3. **Sprinkles of interactivity** - Stimulus for behavior
|
|
18
|
+
4. **One controller per behavior** - Focused, reusable
|
|
19
|
+
5. **Turbo for speed** - Fast navigation without SPAs
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## File Structure
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
app/javascript/
|
|
27
|
+
├── application.js
|
|
28
|
+
└── controllers/
|
|
29
|
+
├── index.js
|
|
30
|
+
├── dialog_controller.js
|
|
31
|
+
├── auto_submit_controller.js
|
|
32
|
+
├── navigable_list_controller.js
|
|
33
|
+
└── drag_drop_controller.js
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Stimulus Controller Template
|
|
39
|
+
|
|
40
|
+
```javascript
|
|
41
|
+
// app/javascript/controllers/dialog_controller.js
|
|
42
|
+
import { Controller } from "@hotwired/stimulus"
|
|
43
|
+
|
|
44
|
+
export default class extends Controller {
|
|
45
|
+
static targets = [ "dialog", "content" ]
|
|
46
|
+
static values = { open: Boolean, dismissable: { type: Boolean, default: true } }
|
|
47
|
+
static classes = [ "open", "closing" ]
|
|
48
|
+
|
|
49
|
+
connect() {
|
|
50
|
+
this.boundHandleEscape = this.handleEscape.bind(this)
|
|
51
|
+
document.addEventListener("keydown", this.boundHandleEscape)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
disconnect() {
|
|
55
|
+
document.removeEventListener("keydown", this.boundHandleEscape)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
open() {
|
|
59
|
+
this.openValue = true
|
|
60
|
+
this.dialogTarget.showModal()
|
|
61
|
+
this.dialogTarget.classList.add(this.openClass)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
close() {
|
|
65
|
+
if (!this.dismissableValue) return
|
|
66
|
+
|
|
67
|
+
this.openValue = false
|
|
68
|
+
this.dialogTarget.classList.add(this.closingClass)
|
|
69
|
+
|
|
70
|
+
setTimeout(() => {
|
|
71
|
+
this.dialogTarget.close()
|
|
72
|
+
this.dialogTarget.classList.remove(this.openClass, this.closingClass)
|
|
73
|
+
}, 200)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
handleEscape(event) {
|
|
77
|
+
if (event.key === "Escape" && this.openValue) {
|
|
78
|
+
this.close()
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Private methods with #
|
|
83
|
+
#cleanup() {
|
|
84
|
+
// Implementation
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Stimulus Patterns
|
|
92
|
+
|
|
93
|
+
### Auto-Submit Form
|
|
94
|
+
|
|
95
|
+
```javascript
|
|
96
|
+
import { Controller } from "@hotwired/stimulus"
|
|
97
|
+
|
|
98
|
+
export default class extends Controller {
|
|
99
|
+
static targets = [ "form" ]
|
|
100
|
+
static values = { delay: { type: Number, default: 300 } }
|
|
101
|
+
|
|
102
|
+
submit() {
|
|
103
|
+
clearTimeout(this.timeout)
|
|
104
|
+
|
|
105
|
+
this.timeout = setTimeout(() => {
|
|
106
|
+
this.formTarget.requestSubmit()
|
|
107
|
+
}, this.delayValue)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
```erb
|
|
113
|
+
<%= form_with model: @filter, data: { controller: "auto-submit", action: "input->auto-submit#submit" } do |f| %>
|
|
114
|
+
<%= f.text_field :query, data: { auto_submit_target: "form" } %>
|
|
115
|
+
<% end %>
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Keyboard Navigation
|
|
119
|
+
|
|
120
|
+
```javascript
|
|
121
|
+
import { Controller } from "@hotwired/stimulus"
|
|
122
|
+
|
|
123
|
+
export default class extends Controller {
|
|
124
|
+
static targets = [ "item" ]
|
|
125
|
+
|
|
126
|
+
connect() {
|
|
127
|
+
this.index = 0
|
|
128
|
+
this.highlight(0)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
navigate(event) {
|
|
132
|
+
switch(event.key) {
|
|
133
|
+
case "ArrowDown":
|
|
134
|
+
event.preventDefault()
|
|
135
|
+
this.next()
|
|
136
|
+
break
|
|
137
|
+
case "ArrowUp":
|
|
138
|
+
event.preventDefault()
|
|
139
|
+
this.previous()
|
|
140
|
+
break
|
|
141
|
+
case "Enter":
|
|
142
|
+
event.preventDefault()
|
|
143
|
+
this.select()
|
|
144
|
+
break
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
next() {
|
|
149
|
+
this.index = Math.min(this.index + 1, this.itemTargets.length - 1)
|
|
150
|
+
this.highlight(this.index)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
previous() {
|
|
154
|
+
this.index = Math.max(this.index - 1, 0)
|
|
155
|
+
this.highlight(this.index)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
select() {
|
|
159
|
+
this.itemTargets[this.index]?.click()
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
highlight(index) {
|
|
163
|
+
this.itemTargets.forEach((item, i) => {
|
|
164
|
+
item.classList.toggle("highlighted", i === index)
|
|
165
|
+
})
|
|
166
|
+
this.itemTargets[index]?.scrollIntoView({ block: "nearest" })
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## Turbo Patterns
|
|
174
|
+
|
|
175
|
+
### Turbo Frames
|
|
176
|
+
|
|
177
|
+
```erb
|
|
178
|
+
<%# Lazy loading %>
|
|
179
|
+
<turbo-frame id="stats" src="<%= stats_path %>" loading="lazy">
|
|
180
|
+
<p>Loading stats...</p>
|
|
181
|
+
</turbo-frame>
|
|
182
|
+
|
|
183
|
+
<%# Navigation within frame %>
|
|
184
|
+
<turbo-frame id="modal">
|
|
185
|
+
<%= link_to "Edit", edit_card_path(@card) %>
|
|
186
|
+
</turbo-frame>
|
|
187
|
+
|
|
188
|
+
<%# Break out of frame %>
|
|
189
|
+
<%= link_to "View All", cards_path, data: { turbo_frame: "_top" } %>
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Turbo Streams
|
|
193
|
+
|
|
194
|
+
```ruby
|
|
195
|
+
# Controller
|
|
196
|
+
def create
|
|
197
|
+
@card = Card.create!(card_params)
|
|
198
|
+
|
|
199
|
+
respond_to do |format|
|
|
200
|
+
format.turbo_stream
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
```erb
|
|
206
|
+
<%# create.turbo_stream.erb %>
|
|
207
|
+
<%= turbo_stream.prepend "cards", @card %>
|
|
208
|
+
<%= turbo_stream.update "flash", partial: "shared/flash" %>
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Turbo Drive
|
|
212
|
+
|
|
213
|
+
```erb
|
|
214
|
+
<%# Disable Turbo for link %>
|
|
215
|
+
<%= link_to "External", external_path, data: { turbo: false } %>
|
|
216
|
+
|
|
217
|
+
<%# Confirm before navigation %>
|
|
218
|
+
<%= link_to "Delete", @card, method: :delete, data: { turbo_confirm: "Are you sure?" } %>
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## Testing JavaScript
|
|
224
|
+
|
|
225
|
+
### System Tests with JavaScript
|
|
226
|
+
|
|
227
|
+
```ruby
|
|
228
|
+
class DialogTest < ApplicationSystemTestCase
|
|
229
|
+
test "opening and closing dialog", js: true do
|
|
230
|
+
visit cards_path
|
|
231
|
+
|
|
232
|
+
click_on "New Card"
|
|
233
|
+
assert_selector "dialog[open]"
|
|
234
|
+
|
|
235
|
+
click_on "Cancel"
|
|
236
|
+
assert_no_selector "dialog[open]"
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## Importmap (Rails 8 Default)
|
|
244
|
+
|
|
245
|
+
Rails 8 defaults to importmap for JavaScript management - no build step required.
|
|
246
|
+
|
|
247
|
+
### Configuration
|
|
248
|
+
|
|
249
|
+
```ruby
|
|
250
|
+
# config/importmap.rb
|
|
251
|
+
pin "application"
|
|
252
|
+
pin "@hotwired/turbo-rails", to: "turbo.min.js"
|
|
253
|
+
pin "@hotwired/stimulus", to: "stimulus.min.js"
|
|
254
|
+
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"
|
|
255
|
+
|
|
256
|
+
# Pin your controllers
|
|
257
|
+
pin_all_from "app/javascript/controllers", under: "controllers"
|
|
258
|
+
|
|
259
|
+
# Add third-party libraries
|
|
260
|
+
pin "local-time" # from jspm.io
|
|
261
|
+
pin "sortablejs" # from unpkg.com
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### Adding JavaScript Libraries
|
|
265
|
+
|
|
266
|
+
```bash
|
|
267
|
+
# Import from CDN
|
|
268
|
+
bin/importmap pin local-time
|
|
269
|
+
|
|
270
|
+
# Pin specific version
|
|
271
|
+
bin/importmap pin sortablejs@1.15.0
|
|
272
|
+
|
|
273
|
+
# Check current pins
|
|
274
|
+
bin/importmap json
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### Application Setup
|
|
278
|
+
|
|
279
|
+
```javascript
|
|
280
|
+
// app/javascript/application.js
|
|
281
|
+
import "@hotwired/turbo-rails"
|
|
282
|
+
import "./controllers"
|
|
283
|
+
|
|
284
|
+
// Optional: Import libraries
|
|
285
|
+
import LocalTime from "local-time"
|
|
286
|
+
LocalTime.start()
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
**When to use alternatives:**
|
|
290
|
+
- **esbuild/webpack**: Need npm packages not available via importmap
|
|
291
|
+
- **propshaft**: Simpler asset pipeline (no import maps)
|
|
292
|
+
- **vite**: Modern bundler with HMR
|
|
293
|
+
|
|
294
|
+
---
|
|
295
|
+
|
|
296
|
+
## Turbo 8 Features
|
|
297
|
+
|
|
298
|
+
### Page Refresh (Morphing)
|
|
299
|
+
|
|
300
|
+
Turbo 8 can morph entire pages while preserving scroll position and form state.
|
|
301
|
+
|
|
302
|
+
```erb
|
|
303
|
+
<%# Enable for entire app %>
|
|
304
|
+
<%# app/views/layouts/application.html.erb %>
|
|
305
|
+
<meta name="turbo-refresh-method" content="morph">
|
|
306
|
+
<meta name="turbo-refresh-scroll" content="preserve">
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
```ruby
|
|
310
|
+
# Enable for specific actions
|
|
311
|
+
class CardsController < ApplicationController
|
|
312
|
+
def index
|
|
313
|
+
# Will use morphing for page updates
|
|
314
|
+
@cards = Card.all
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# Force refresh after form submit
|
|
319
|
+
def create
|
|
320
|
+
@card = Card.create!(card_params)
|
|
321
|
+
redirect_to cards_path, status: :see_other # Triggers refresh
|
|
322
|
+
end
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
### Turbo Permanent Elements
|
|
326
|
+
|
|
327
|
+
Preserve elements across page navigations:
|
|
328
|
+
|
|
329
|
+
```erb
|
|
330
|
+
<div id="player" data-turbo-permanent>
|
|
331
|
+
<%# Audio player persists across navigation %>
|
|
332
|
+
<audio controls src="<%= @podcast.audio_url %>"></audio>
|
|
333
|
+
</div>
|
|
334
|
+
|
|
335
|
+
<div id="flash" data-turbo-permanent>
|
|
336
|
+
<%# Flash messages persist during loading %>
|
|
337
|
+
<%= render "shared/flash" %>
|
|
338
|
+
</div>
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
### Prefetching
|
|
342
|
+
|
|
343
|
+
Speed up navigation by prefetching links:
|
|
344
|
+
|
|
345
|
+
```erb
|
|
346
|
+
<%# Prefetch on hover %>
|
|
347
|
+
<%= link_to "View Card", @card, data: { turbo_prefetch: true } %>
|
|
348
|
+
|
|
349
|
+
<%# Instant navigation - prefetch on mousedown %>
|
|
350
|
+
<%= link_to "Dashboard", dashboard_path, data: { turbo_preload: "instant" } %>
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
---
|
|
354
|
+
|
|
355
|
+
## View Transitions (CSS)
|
|
356
|
+
|
|
357
|
+
Smooth page transitions using CSS View Transitions API with Turbo.
|
|
358
|
+
|
|
359
|
+
### Enable View Transitions
|
|
360
|
+
|
|
361
|
+
```erb
|
|
362
|
+
<%# app/views/layouts/application.html.erb %>
|
|
363
|
+
<meta name="view-transition" content="same-origin">
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
### CSS Animations
|
|
367
|
+
|
|
368
|
+
```css
|
|
369
|
+
/* app/assets/stylesheets/application.css */
|
|
370
|
+
|
|
371
|
+
/* Fade transition */
|
|
372
|
+
::view-transition-old(root),
|
|
373
|
+
::view-transition-new(root) {
|
|
374
|
+
animation-duration: 0.3s;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/* Slide transition */
|
|
378
|
+
::view-transition-old(root) {
|
|
379
|
+
animation-name: slide-out;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
::view-transition-new(root) {
|
|
383
|
+
animation-name: slide-in;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
@keyframes slide-out {
|
|
387
|
+
from { transform: translateX(0); }
|
|
388
|
+
to { transform: translateX(-100%); }
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
@keyframes slide-in {
|
|
392
|
+
from { transform: translateX(100%); }
|
|
393
|
+
to { transform: translateX(0); }
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/* Named transitions for specific elements */
|
|
397
|
+
.card-detail {
|
|
398
|
+
view-transition-name: card-detail;
|
|
399
|
+
}
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
**Browser Support:** Chrome/Edge 111+, Safari 18+. Gracefully degrades in older browsers.
|
|
403
|
+
|
|
404
|
+
---
|
|
405
|
+
|
|
406
|
+
## Production-Ready Stimulus Controllers
|
|
407
|
+
|
|
408
|
+
### Clipboard Controller
|
|
409
|
+
|
|
410
|
+
```javascript
|
|
411
|
+
// controllers/clipboard_controller.js
|
|
412
|
+
import { Controller } from "@hotwired/stimulus"
|
|
413
|
+
|
|
414
|
+
export default class extends Controller {
|
|
415
|
+
static targets = [ "source", "button" ]
|
|
416
|
+
static values = {
|
|
417
|
+
successMessage: { type: String, default: "Copied!" },
|
|
418
|
+
successDuration: { type: Number, default: 2000 }
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
copy(event) {
|
|
422
|
+
event.preventDefault()
|
|
423
|
+
|
|
424
|
+
navigator.clipboard.writeText(this.sourceTarget.value || this.sourceTarget.textContent)
|
|
425
|
+
.then(() => this.showSuccess())
|
|
426
|
+
.catch(() => this.showError())
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
showSuccess() {
|
|
430
|
+
const originalText = this.buttonTarget.textContent
|
|
431
|
+
this.buttonTarget.textContent = this.successMessageValue
|
|
432
|
+
|
|
433
|
+
setTimeout(() => {
|
|
434
|
+
this.buttonTarget.textContent = originalText
|
|
435
|
+
}, this.successDurationValue)
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
showError() {
|
|
439
|
+
this.buttonTarget.textContent = "Failed to copy"
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
```erb
|
|
445
|
+
<div data-controller="clipboard">
|
|
446
|
+
<input data-clipboard-target="source" value="<%= @share_url %>" readonly>
|
|
447
|
+
<button data-action="click->clipboard#copy" data-clipboard-target="button">
|
|
448
|
+
Copy URL
|
|
449
|
+
</button>
|
|
450
|
+
</div>
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
### Dropdown Controller
|
|
454
|
+
|
|
455
|
+
```javascript
|
|
456
|
+
// controllers/dropdown_controller.js
|
|
457
|
+
import { Controller } from "@hotwired/stimulus"
|
|
458
|
+
|
|
459
|
+
export default class extends Controller {
|
|
460
|
+
static targets = [ "menu" ]
|
|
461
|
+
static classes = [ "open" ]
|
|
462
|
+
|
|
463
|
+
connect() {
|
|
464
|
+
this.boundHandleClickOutside = this.handleClickOutside.bind(this)
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
toggle(event) {
|
|
468
|
+
event.stopPropagation()
|
|
469
|
+
|
|
470
|
+
if (this.menuTarget.classList.contains(this.openClass)) {
|
|
471
|
+
this.close()
|
|
472
|
+
} else {
|
|
473
|
+
this.open()
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
open() {
|
|
478
|
+
this.menuTarget.classList.add(this.openClass)
|
|
479
|
+
document.addEventListener("click", this.boundHandleClickOutside)
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
close() {
|
|
483
|
+
this.menuTarget.classList.remove(this.openClass)
|
|
484
|
+
document.removeEventListener("click", this.boundHandleClickOutside)
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
handleClickOutside(event) {
|
|
488
|
+
if (!this.element.contains(event.target)) {
|
|
489
|
+
this.close()
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
disconnect() {
|
|
494
|
+
document.removeEventListener("click", this.boundHandleClickOutside)
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
```erb
|
|
500
|
+
<div data-controller="dropdown" class="dropdown">
|
|
501
|
+
<button data-action="click->dropdown#toggle">
|
|
502
|
+
Options ▾
|
|
503
|
+
</button>
|
|
504
|
+
|
|
505
|
+
<div data-dropdown-target="menu" data-dropdown-open-class="open" class="dropdown-menu">
|
|
506
|
+
<%= link_to "Edit", edit_card_path(@card) %>
|
|
507
|
+
<%= link_to "Delete", @card, method: :delete %>
|
|
508
|
+
</div>
|
|
509
|
+
</div>
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
### Toggle Controller
|
|
513
|
+
|
|
514
|
+
```javascript
|
|
515
|
+
// controllers/toggle_controller.js
|
|
516
|
+
import { Controller } from "@hotwired/stimulus"
|
|
517
|
+
|
|
518
|
+
export default class extends Controller {
|
|
519
|
+
static targets = [ "toggleable" ]
|
|
520
|
+
static classes = [ "hidden" ]
|
|
521
|
+
|
|
522
|
+
toggle() {
|
|
523
|
+
this.toggleableTargets.forEach(target => {
|
|
524
|
+
target.classList.toggle(this.hiddenClass)
|
|
525
|
+
})
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
show() {
|
|
529
|
+
this.toggleableTargets.forEach(target => {
|
|
530
|
+
target.classList.remove(this.hiddenClass)
|
|
531
|
+
})
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
hide() {
|
|
535
|
+
this.toggleableTargets.forEach(target => {
|
|
536
|
+
target.classList.add(this.hiddenClass)
|
|
537
|
+
})
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
```erb
|
|
543
|
+
<div data-controller="toggle">
|
|
544
|
+
<button data-action="click->toggle#toggle">
|
|
545
|
+
Show/Hide Details
|
|
546
|
+
</button>
|
|
547
|
+
|
|
548
|
+
<div data-toggle-target="toggleable" data-toggle-hidden-class="hidden" class="hidden">
|
|
549
|
+
<p>Additional details...</p>
|
|
550
|
+
</div>
|
|
551
|
+
</div>
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
---
|
|
555
|
+
|
|
556
|
+
## Summary
|
|
557
|
+
|
|
558
|
+
- **Importmap**: Rails 8 default, no build step required
|
|
559
|
+
- **Stimulus**: One controller per behavior, focused and reusable
|
|
560
|
+
- **Targets**: Reference DOM elements
|
|
561
|
+
- **Values**: Pass data from HTML to JavaScript
|
|
562
|
+
- **Actions**: Respond to events
|
|
563
|
+
- **Turbo 8**: Page refresh with morphing, prefetching
|
|
564
|
+
- **View Transitions**: Smooth CSS animations between pages
|
|
565
|
+
- **Turbo Frames**: Lazy loading, scoped navigation
|
|
566
|
+
- **Turbo Streams**: Real-time updates
|
|
567
|
+
- **Progressive Enhancement**: Works without JavaScript
|