source_monitor 0.2.0 → 0.3.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.
- checksums.yaml +4 -4
- data/.claude/agents/rails-concern.md +464 -0
- data/.claude/agents/rails-controller.md +424 -0
- data/.claude/agents/rails-hotwire.md +446 -0
- data/.claude/agents/rails-implement.md +374 -0
- data/.claude/agents/rails-job.md +334 -0
- data/.claude/agents/rails-lint.md +294 -0
- data/.claude/agents/rails-mailer.md +371 -0
- data/.claude/agents/rails-migration.md +449 -0
- data/.claude/agents/rails-model.md +420 -0
- data/.claude/agents/rails-policy.md +443 -0
- data/.claude/agents/rails-presenter.md +427 -0
- data/.claude/agents/rails-query.md +412 -0
- data/.claude/agents/rails-review.md +490 -0
- data/.claude/agents/rails-service.md +458 -0
- data/.claude/agents/rails-state-records.md +465 -0
- data/.claude/agents/rails-tdd.md +314 -0
- data/.claude/agents/rails-test.md +441 -0
- data/.claude/agents/rails-view-component.md +418 -0
- data/.claude/hooks/block-secrets.sh +52 -0
- data/.claude/settings.json +85 -0
- data/.claude/skills/action-cable-patterns/SKILL.md +296 -0
- data/.claude/skills/action-mailer-patterns/SKILL.md +295 -0
- data/.claude/skills/active-storage-setup/SKILL.md +311 -0
- data/.claude/skills/api-versioning/SKILL.md +294 -0
- data/.claude/skills/authentication-flow/SKILL.md +335 -0
- data/.claude/skills/authentication-flow/reference/current.md +248 -0
- data/.claude/skills/authentication-flow/reference/passwordless.md +253 -0
- data/.claude/skills/authentication-flow/reference/sessions.md +201 -0
- data/.claude/skills/authorization-pundit/SKILL.md +462 -0
- data/.claude/skills/caching-strategies/SKILL.md +350 -0
- data/.claude/skills/database-migrations/SKILL.md +354 -0
- data/.claude/skills/form-object-patterns/SKILL.md +399 -0
- data/.claude/skills/hotwire-patterns/SKILL.md +247 -0
- data/.claude/skills/hotwire-patterns/reference/stimulus.md +307 -0
- data/.claude/skills/hotwire-patterns/reference/tailwind-integration.md +112 -0
- data/.claude/skills/hotwire-patterns/reference/turbo-frames.md +158 -0
- data/.claude/skills/hotwire-patterns/reference/turbo-streams.md +218 -0
- data/.claude/skills/i18n-patterns/SKILL.md +320 -0
- data/.claude/skills/install/SKILL.md +367 -0
- data/.claude/skills/performance-optimization/SKILL.md +311 -0
- data/.claude/skills/rails-architecture/SKILL.md +259 -0
- data/.claude/skills/rails-architecture/reference/error-handling.md +333 -0
- data/.claude/skills/rails-architecture/reference/event-tracking.md +142 -0
- data/.claude/skills/rails-architecture/reference/layer-interactions.md +417 -0
- data/.claude/skills/rails-architecture/reference/multi-tenancy.md +152 -0
- data/.claude/skills/rails-architecture/reference/query-patterns.md +342 -0
- data/.claude/skills/rails-architecture/reference/service-patterns.md +286 -0
- data/.claude/skills/rails-architecture/reference/state-records.md +250 -0
- data/.claude/skills/rails-architecture/reference/testing-strategy.md +326 -0
- data/.claude/skills/rails-concern/SKILL.md +399 -0
- data/.claude/skills/rails-controller/SKILL.md +336 -0
- data/.claude/skills/rails-model-generator/SKILL.md +321 -0
- data/.claude/skills/rails-model-generator/reference/validations.md +298 -0
- data/.claude/skills/rails-presenter/SKILL.md +274 -0
- data/.claude/skills/rails-query-object/SKILL.md +289 -0
- data/.claude/skills/rails-service-object/SKILL.md +349 -0
- data/.claude/skills/solid-queue-setup/SKILL.md +307 -0
- data/.claude/skills/tdd-cycle/SKILL.md +359 -0
- data/.claude/skills/viewcomponent-patterns/SKILL.md +333 -0
- data/.gitignore +1 -0
- data/.rubocop.yml +2 -0
- data/.ruby-version +1 -1
- data/.vbw-planning/.notification-log.jsonl +192 -0
- data/.vbw-planning/.session-log.jsonl +871 -0
- data/.vbw-planning/PROJECT.md +51 -0
- data/.vbw-planning/REQUIREMENTS.md +50 -0
- data/.vbw-planning/SHIPPED.md +28 -0
- data/.vbw-planning/codebase/ARCHITECTURE.md +147 -0
- data/.vbw-planning/codebase/CONCERNS.md +99 -0
- data/.vbw-planning/codebase/CONVENTIONS.md +97 -0
- data/.vbw-planning/codebase/DEPENDENCIES.md +100 -0
- data/.vbw-planning/codebase/INDEX.md +86 -0
- data/.vbw-planning/codebase/META.md +42 -0
- data/.vbw-planning/codebase/PATTERNS.md +262 -0
- data/.vbw-planning/codebase/STACK.md +101 -0
- data/.vbw-planning/codebase/STRUCTURE.md +324 -0
- data/.vbw-planning/codebase/TESTING.md +154 -0
- data/.vbw-planning/config.json +12 -0
- data/.vbw-planning/discovery.json +24 -0
- data/.vbw-planning/milestones/default/ROADMAP.md +115 -0
- data/.vbw-planning/milestones/default/STATE.md +83 -0
- data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-01-SUMMARY.md +56 -0
- data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-01.md +187 -0
- data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-02-SUMMARY.md +64 -0
- data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-02.md +137 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-01-SUMMARY.md +67 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-01.md +142 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-02-SUMMARY.md +64 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-02.md +138 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-03-SUMMARY.md +85 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-03.md +147 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-04-SUMMARY.md +63 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-04.md +129 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-05-SUMMARY.md +74 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-05.md +154 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/03-VERIFICATION-wave1.md +303 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/03-VERIFICATION.md +510 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-01-SUMMARY.md +61 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-01.md +161 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-02-SUMMARY.md +66 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-02.md +132 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-03-SUMMARY.md +59 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-03.md +171 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-04-SUMMARY.md +56 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-04.md +152 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/04-CONTEXT.md +33 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-01-SUMMARY.md +42 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-01.md +119 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-02-SUMMARY.md +52 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-02.md +195 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-03-SUMMARY.md +79 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-03.md +130 -0
- data/CHANGELOG.md +28 -0
- data/CLAUDE.md +179 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +114 -101
- data/Rakefile +2 -0
- data/app/assets/builds/source_monitor/application.css +2076 -0
- data/app/assets/builds/source_monitor/application.js +2758 -0
- data/app/assets/builds/source_monitor/application.js.map +7 -0
- data/app/controllers/source_monitor/application_controller.rb +2 -0
- data/app/controllers/source_monitor/health_controller.rb +2 -0
- data/app/controllers/source_monitor/import_sessions/bulk_configuration.rb +106 -0
- data/app/controllers/source_monitor/import_sessions/entry_annotation.rb +187 -0
- data/app/controllers/source_monitor/import_sessions/health_check_management.rb +112 -0
- data/app/controllers/source_monitor/import_sessions/opml_parser.rb +130 -0
- data/app/controllers/source_monitor/import_sessions_controller.rb +6 -507
- data/app/controllers/source_monitor/items_controller.rb +2 -0
- data/app/controllers/source_monitor/sources_controller.rb +0 -14
- data/app/helpers/source_monitor/application_helper.rb +4 -112
- data/app/helpers/source_monitor/health_badge_helper.rb +69 -0
- data/app/helpers/source_monitor/table_sort_helper.rb +53 -0
- data/app/jobs/source_monitor/application_job.rb +2 -0
- data/app/models/source_monitor/application_record.rb +2 -0
- data/app/models/source_monitor/log_entry.rb +0 -2
- data/config/coverage_baseline.json +217 -1862
- data/config/routes.rb +2 -0
- data/db/migrate/20251009103000_add_feed_content_readability_to_sources.rb +2 -0
- data/db/migrate/20251014171659_add_performance_indexes.rb +2 -0
- data/db/migrate/20251014172525_add_fetch_status_check_constraint.rb +2 -0
- data/db/migrate/20251108120116_refresh_fetch_status_constraint.rb +2 -0
- data/db/migrate/20260210204022_add_composite_index_to_log_entries.rb +17 -0
- data/lib/source_monitor/assets/bundler.rb +2 -0
- data/lib/source_monitor/assets.rb +2 -0
- data/lib/source_monitor/configuration/authentication_settings.rb +62 -0
- data/lib/source_monitor/configuration/events.rb +60 -0
- data/lib/source_monitor/configuration/fetching_settings.rb +27 -0
- data/lib/source_monitor/configuration/health_settings.rb +27 -0
- data/lib/source_monitor/configuration/http_settings.rb +43 -0
- data/lib/source_monitor/configuration/model_definition.rb +108 -0
- data/lib/source_monitor/configuration/models.rb +36 -0
- data/lib/source_monitor/configuration/realtime_settings.rb +95 -0
- data/lib/source_monitor/configuration/retention_settings.rb +45 -0
- data/lib/source_monitor/configuration/scraper_registry.rb +67 -0
- data/lib/source_monitor/configuration/scraping_settings.rb +39 -0
- data/lib/source_monitor/configuration/validation_definition.rb +32 -0
- data/lib/source_monitor/configuration.rb +12 -579
- data/lib/source_monitor/dashboard/queries/recent_activity_query.rb +138 -0
- data/lib/source_monitor/dashboard/queries/stats_query.rb +71 -0
- data/lib/source_monitor/dashboard/queries.rb +2 -195
- data/lib/source_monitor/engine.rb +2 -0
- data/lib/source_monitor/fetching/feed_fetcher/adaptive_interval.rb +141 -0
- data/lib/source_monitor/fetching/feed_fetcher/entry_processor.rb +89 -0
- data/lib/source_monitor/fetching/feed_fetcher/source_updater.rb +200 -0
- data/lib/source_monitor/fetching/feed_fetcher.rb +37 -379
- data/lib/source_monitor/items/item_creator/content_extractor.rb +113 -0
- data/lib/source_monitor/items/item_creator/entry_parser/media_extraction.rb +96 -0
- data/lib/source_monitor/items/item_creator/entry_parser.rb +294 -0
- data/lib/source_monitor/items/item_creator.rb +28 -455
- data/lib/source_monitor/setup/bundle_installer.rb +2 -0
- data/lib/source_monitor/setup/cli.rb +2 -0
- data/lib/source_monitor/setup/dependency_checker.rb +2 -0
- data/lib/source_monitor/setup/detectors.rb +2 -0
- data/lib/source_monitor/setup/gemfile_editor.rb +2 -0
- data/lib/source_monitor/setup/initializer_patcher.rb +2 -0
- data/lib/source_monitor/setup/install_generator.rb +2 -0
- data/lib/source_monitor/setup/migration_installer.rb +2 -0
- data/lib/source_monitor/setup/node_installer.rb +2 -0
- data/lib/source_monitor/setup/prompter.rb +2 -0
- data/lib/source_monitor/setup/requirements.rb +2 -0
- data/lib/source_monitor/setup/shell_runner.rb +2 -0
- data/lib/source_monitor/setup/verification/action_cable_verifier.rb +2 -0
- data/lib/source_monitor/setup/verification/printer.rb +2 -0
- data/lib/source_monitor/setup/verification/result.rb +2 -0
- data/lib/source_monitor/setup/verification/runner.rb +2 -0
- data/lib/source_monitor/setup/verification/solid_queue_verifier.rb +2 -0
- data/lib/source_monitor/setup/verification/telemetry_logger.rb +2 -0
- data/lib/source_monitor/setup/workflow.rb +2 -0
- data/lib/source_monitor/version.rb +3 -1
- data/lib/source_monitor.rb +140 -58
- data/lib/tasks/source_monitor_assets.rake +2 -0
- data/lib/tasks/source_monitor_setup.rake +2 -0
- data/lib/tasks/source_monitor_tasks.rake +2 -0
- data/source_monitor.gemspec +3 -1
- metadata +144 -4
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
# Stimulus Reference
|
|
2
|
+
|
|
3
|
+
## Concept
|
|
4
|
+
|
|
5
|
+
Stimulus is a modest JavaScript framework for adding behavior to HTML. It connects JavaScript objects (controllers) to DOM elements using data attributes.
|
|
6
|
+
|
|
7
|
+
## Core Concepts
|
|
8
|
+
|
|
9
|
+
| Concept | Purpose | Attribute |
|
|
10
|
+
|---------|---------|-----------|
|
|
11
|
+
| Controller | JavaScript class | `data-controller="name"` |
|
|
12
|
+
| Action | Event handler | `data-action="event->controller#method"` |
|
|
13
|
+
| Target | DOM reference | `data-controller-target="name"` |
|
|
14
|
+
| Value | Reactive data | `data-controller-name-value="x"` |
|
|
15
|
+
| Class | CSS class reference | `data-controller-name-class="x"` |
|
|
16
|
+
| Outlet | Cross-controller reference | `data-controller-name-outlet=".selector"` |
|
|
17
|
+
|
|
18
|
+
## Basic Controller
|
|
19
|
+
|
|
20
|
+
```javascript
|
|
21
|
+
// app/javascript/controllers/hello_controller.js
|
|
22
|
+
import { Controller } from "@hotwired/stimulus"
|
|
23
|
+
|
|
24
|
+
export default class extends Controller {
|
|
25
|
+
static targets = ["output"]
|
|
26
|
+
|
|
27
|
+
greet() {
|
|
28
|
+
this.outputTarget.textContent = "Hello, Stimulus!"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```erb
|
|
34
|
+
<div data-controller="hello">
|
|
35
|
+
<button data-action="click->hello#greet">Greet</button>
|
|
36
|
+
<span data-hello-target="output"></span>
|
|
37
|
+
</div>
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Controller Lifecycle
|
|
41
|
+
|
|
42
|
+
```javascript
|
|
43
|
+
import { Controller } from "@hotwired/stimulus"
|
|
44
|
+
|
|
45
|
+
export default class extends Controller {
|
|
46
|
+
// Called when controller connects to DOM
|
|
47
|
+
connect() {
|
|
48
|
+
console.log("Connected!", this.element)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Called when controller disconnects
|
|
52
|
+
disconnect() {
|
|
53
|
+
console.log("Disconnected!")
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Called when target is added
|
|
57
|
+
outputTargetConnected(element) {
|
|
58
|
+
console.log("Target connected:", element)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Called when target is removed
|
|
62
|
+
outputTargetDisconnected(element) {
|
|
63
|
+
console.log("Target disconnected:", element)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Targets
|
|
69
|
+
|
|
70
|
+
```javascript
|
|
71
|
+
import { Controller } from "@hotwired/stimulus"
|
|
72
|
+
|
|
73
|
+
export default class extends Controller {
|
|
74
|
+
static targets = ["input", "output", "item"]
|
|
75
|
+
|
|
76
|
+
// Single target (first match)
|
|
77
|
+
copy() {
|
|
78
|
+
this.outputTarget.textContent = this.inputTarget.value
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Multiple targets (all matches)
|
|
82
|
+
clearAll() {
|
|
83
|
+
this.itemTargets.forEach(el => el.remove())
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Check if target exists
|
|
87
|
+
validate() {
|
|
88
|
+
if (this.hasOutputTarget) {
|
|
89
|
+
this.outputTarget.classList.add("validated")
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
```erb
|
|
96
|
+
<div data-controller="form">
|
|
97
|
+
<input data-form-target="input" type="text">
|
|
98
|
+
<div data-form-target="output"></div>
|
|
99
|
+
<div data-form-target="item">Item 1</div>
|
|
100
|
+
<div data-form-target="item">Item 2</div>
|
|
101
|
+
</div>
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Values
|
|
105
|
+
|
|
106
|
+
```javascript
|
|
107
|
+
import { Controller } from "@hotwired/stimulus"
|
|
108
|
+
|
|
109
|
+
export default class extends Controller {
|
|
110
|
+
static values = {
|
|
111
|
+
url: String,
|
|
112
|
+
count: { type: Number, default: 0 },
|
|
113
|
+
enabled: Boolean,
|
|
114
|
+
config: Object,
|
|
115
|
+
items: Array
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
connect() {
|
|
119
|
+
console.log(this.urlValue) // String
|
|
120
|
+
console.log(this.countValue) // Number
|
|
121
|
+
console.log(this.enabledValue) // Boolean
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Called when value changes
|
|
125
|
+
countValueChanged(value, previousValue) {
|
|
126
|
+
console.log(`Count changed from ${previousValue} to ${value}`)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
increment() {
|
|
130
|
+
this.countValue++ // Triggers countValueChanged
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
```erb
|
|
136
|
+
<div data-controller="counter"
|
|
137
|
+
data-counter-url-value="/api/count"
|
|
138
|
+
data-counter-count-value="5"
|
|
139
|
+
data-counter-enabled-value="true"
|
|
140
|
+
data-counter-config-value='{"max": 100}'>
|
|
141
|
+
</div>
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Actions
|
|
145
|
+
|
|
146
|
+
```javascript
|
|
147
|
+
import { Controller } from "@hotwired/stimulus"
|
|
148
|
+
|
|
149
|
+
export default class extends Controller {
|
|
150
|
+
// Basic action
|
|
151
|
+
submit() {
|
|
152
|
+
console.log("Submitted!")
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// With event parameter
|
|
156
|
+
handleClick(event) {
|
|
157
|
+
event.preventDefault()
|
|
158
|
+
console.log("Clicked:", event.target)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// With params
|
|
162
|
+
delete(event) {
|
|
163
|
+
const id = event.params.id
|
|
164
|
+
console.log("Delete item:", id)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
```erb
|
|
170
|
+
<div data-controller="items">
|
|
171
|
+
<%# Basic action %>
|
|
172
|
+
<button data-action="click->items#submit">Submit</button>
|
|
173
|
+
|
|
174
|
+
<%# Multiple events %>
|
|
175
|
+
<input data-action="input->items#validate focus->items#highlight">
|
|
176
|
+
|
|
177
|
+
<%# Shorthand (click is default for buttons) %>
|
|
178
|
+
<button data-action="items#submit">Submit</button>
|
|
179
|
+
|
|
180
|
+
<%# With params %>
|
|
181
|
+
<button data-action="items#delete" data-items-id-param="123">Delete</button>
|
|
182
|
+
|
|
183
|
+
<%# Prevent default %>
|
|
184
|
+
<form data-action="submit->items#handleSubmit:prevent">
|
|
185
|
+
|
|
186
|
+
<%# Stop propagation %>
|
|
187
|
+
<button data-action="click->items#handle:stop">Click</button>
|
|
188
|
+
</div>
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Common Patterns
|
|
192
|
+
|
|
193
|
+
### Toggle Visibility
|
|
194
|
+
|
|
195
|
+
```javascript
|
|
196
|
+
// toggle_controller.js
|
|
197
|
+
import { Controller } from "@hotwired/stimulus"
|
|
198
|
+
|
|
199
|
+
export default class extends Controller {
|
|
200
|
+
static targets = ["content"]
|
|
201
|
+
static classes = ["hidden"]
|
|
202
|
+
|
|
203
|
+
toggle() {
|
|
204
|
+
this.contentTarget.classList.toggle(this.hiddenClass)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
show() {
|
|
208
|
+
this.contentTarget.classList.remove(this.hiddenClass)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
hide() {
|
|
212
|
+
this.contentTarget.classList.add(this.hiddenClass)
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
```erb
|
|
218
|
+
<div data-controller="toggle" data-toggle-hidden-class="hidden">
|
|
219
|
+
<button data-action="toggle#toggle">Toggle</button>
|
|
220
|
+
<div data-toggle-target="content">Content here</div>
|
|
221
|
+
</div>
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Form Validation
|
|
225
|
+
|
|
226
|
+
```javascript
|
|
227
|
+
// validation_controller.js
|
|
228
|
+
import { Controller } from "@hotwired/stimulus"
|
|
229
|
+
|
|
230
|
+
export default class extends Controller {
|
|
231
|
+
static targets = ["input", "error", "submit"]
|
|
232
|
+
|
|
233
|
+
validate() {
|
|
234
|
+
const isValid = this.inputTarget.value.length >= 3
|
|
235
|
+
|
|
236
|
+
this.errorTarget.textContent = isValid ? "" : "Minimum 3 characters"
|
|
237
|
+
this.submitTarget.disabled = !isValid
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### Debounced Search
|
|
243
|
+
|
|
244
|
+
```javascript
|
|
245
|
+
// search_controller.js
|
|
246
|
+
import { Controller } from "@hotwired/stimulus"
|
|
247
|
+
|
|
248
|
+
export default class extends Controller {
|
|
249
|
+
static targets = ["input", "results"]
|
|
250
|
+
static values = { url: String }
|
|
251
|
+
|
|
252
|
+
search() {
|
|
253
|
+
clearTimeout(this.timeout)
|
|
254
|
+
this.timeout = setTimeout(() => {
|
|
255
|
+
this.performSearch()
|
|
256
|
+
}, 300)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async performSearch() {
|
|
260
|
+
const query = this.inputTarget.value
|
|
261
|
+
const response = await fetch(`${this.urlValue}?q=${query}`)
|
|
262
|
+
this.resultsTarget.innerHTML = await response.text()
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### Clipboard
|
|
268
|
+
|
|
269
|
+
```javascript
|
|
270
|
+
// clipboard_controller.js
|
|
271
|
+
import { Controller } from "@hotwired/stimulus"
|
|
272
|
+
|
|
273
|
+
export default class extends Controller {
|
|
274
|
+
static targets = ["source"]
|
|
275
|
+
static values = { successMessage: { type: String, default: "Copied!" } }
|
|
276
|
+
|
|
277
|
+
copy() {
|
|
278
|
+
navigator.clipboard.writeText(this.sourceTarget.value)
|
|
279
|
+
this.showNotification()
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
showNotification() {
|
|
283
|
+
// Show temporary feedback
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
## File Naming
|
|
289
|
+
|
|
290
|
+
```
|
|
291
|
+
app/javascript/controllers/
|
|
292
|
+
├── application.js # Auto-generated
|
|
293
|
+
├── index.js # Auto-generated
|
|
294
|
+
├── hello_controller.js # data-controller="hello"
|
|
295
|
+
├── clipboard_controller.js # data-controller="clipboard"
|
|
296
|
+
└── nested/
|
|
297
|
+
└── form_controller.js # data-controller="nested--form"
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
## Debugging
|
|
301
|
+
|
|
302
|
+
```javascript
|
|
303
|
+
// Enable debug mode
|
|
304
|
+
import { Application } from "@hotwired/stimulus"
|
|
305
|
+
const application = Application.start()
|
|
306
|
+
application.debug = true // Logs controller lifecycle
|
|
307
|
+
```
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# Tailwind CSS Integration with Hotwire
|
|
2
|
+
|
|
3
|
+
## Principles
|
|
4
|
+
|
|
5
|
+
- Mobile-first responsive design
|
|
6
|
+
- Semantic HTML with accessibility
|
|
7
|
+
- Consistent color palette and spacing
|
|
8
|
+
- Focus states on all interactive elements
|
|
9
|
+
|
|
10
|
+
## Responsive Breakpoints
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
sm: 640px+ (small tablets)
|
|
14
|
+
md: 768px+ (tablets)
|
|
15
|
+
lg: 1024px+ (desktops)
|
|
16
|
+
xl: 1280px+ (large desktops)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Common Patterns
|
|
20
|
+
|
|
21
|
+
### Responsive Grid
|
|
22
|
+
|
|
23
|
+
```erb
|
|
24
|
+
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
25
|
+
<%= render @items %>
|
|
26
|
+
</div>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Button Variants
|
|
30
|
+
|
|
31
|
+
```erb
|
|
32
|
+
<%# Primary %>
|
|
33
|
+
<%= link_to "Save", path, class: "bg-blue-600 hover:bg-blue-700 text-white font-semibold py-2 px-4 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors" %>
|
|
34
|
+
|
|
35
|
+
<%# Secondary %>
|
|
36
|
+
<%= link_to "Cancel", path, class: "bg-gray-100 hover:bg-gray-200 text-gray-700 font-semibold py-2 px-4 rounded-md border border-gray-300 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 transition-colors" %>
|
|
37
|
+
|
|
38
|
+
<%# Danger %>
|
|
39
|
+
<%= button_to "Delete", path, method: :delete, data: { turbo_confirm: "Are you sure?" }, class: "bg-red-600 hover:bg-red-700 text-white font-semibold py-2 px-4 rounded-md focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 transition-colors" %>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Form Fields
|
|
43
|
+
|
|
44
|
+
```erb
|
|
45
|
+
<div class="space-y-1">
|
|
46
|
+
<%= f.label :name, class: "block text-sm font-medium text-gray-700" %>
|
|
47
|
+
<%= f.text_field :name, class: "w-full px-3 py-2 rounded-md border border-gray-300 focus:border-blue-500 focus:ring-2 focus:ring-blue-500 transition-colors", placeholder: "Enter name..." %>
|
|
48
|
+
</div>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Cards
|
|
52
|
+
|
|
53
|
+
```erb
|
|
54
|
+
<div class="bg-white rounded-lg shadow-md p-6">
|
|
55
|
+
<h3 class="text-xl font-semibold text-gray-800 mb-2">Title</h3>
|
|
56
|
+
<p class="text-gray-600">Content</p>
|
|
57
|
+
</div>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Badges
|
|
61
|
+
|
|
62
|
+
```erb
|
|
63
|
+
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">Active</span>
|
|
64
|
+
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">Inactive</span>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Alerts
|
|
68
|
+
|
|
69
|
+
```erb
|
|
70
|
+
<div class="bg-green-50 border border-green-200 text-green-800 px-4 py-3 rounded-md" role="alert">
|
|
71
|
+
<p class="font-medium">Success!</p>
|
|
72
|
+
</div>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Turbo-Specific Styling
|
|
76
|
+
|
|
77
|
+
### Turbo Frame Loading State
|
|
78
|
+
|
|
79
|
+
```erb
|
|
80
|
+
<turbo-frame id="comments" src="<%= comments_path %>" loading="lazy" class="space-y-4">
|
|
81
|
+
<div class="flex items-center justify-center p-8">
|
|
82
|
+
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
|
83
|
+
</div>
|
|
84
|
+
</turbo-frame>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Skeleton Loader
|
|
88
|
+
|
|
89
|
+
```erb
|
|
90
|
+
<div class="animate-pulse space-y-4">
|
|
91
|
+
<div class="h-4 bg-gray-200 rounded w-3/4"></div>
|
|
92
|
+
<div class="h-4 bg-gray-200 rounded w-1/2"></div>
|
|
93
|
+
</div>
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Accessibility
|
|
97
|
+
|
|
98
|
+
- Use semantic HTML (`<nav>`, `<main>`, `<article>`, `<button>`)
|
|
99
|
+
- Include `aria-label` for icon-only buttons
|
|
100
|
+
- Ensure focus states with `focus:ring-` classes
|
|
101
|
+
- Add `sr-only` class for screen-reader-only text
|
|
102
|
+
- Minimum contrast ratio WCAG AA: 4.5:1
|
|
103
|
+
|
|
104
|
+
## Color Usage
|
|
105
|
+
|
|
106
|
+
| Color | Purpose |
|
|
107
|
+
|-------|---------|
|
|
108
|
+
| `blue-*` | Primary actions, links |
|
|
109
|
+
| `green-*` | Success, confirmations |
|
|
110
|
+
| `red-*` | Errors, destructive actions |
|
|
111
|
+
| `yellow-*` | Warnings |
|
|
112
|
+
| `gray-*` | Neutral, borders, disabled |
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# Turbo Frames Reference
|
|
2
|
+
|
|
3
|
+
## Concept
|
|
4
|
+
|
|
5
|
+
Turbo Frames scope navigation to a portion of the page. When a link or form inside a frame is activated, only that frame's content is replaced.
|
|
6
|
+
|
|
7
|
+
## Basic Usage
|
|
8
|
+
|
|
9
|
+
### Define a Frame
|
|
10
|
+
|
|
11
|
+
```erb
|
|
12
|
+
<%= turbo_frame_tag "user_profile" do %>
|
|
13
|
+
<h2><%= @user.name %></h2>
|
|
14
|
+
<%= link_to "Edit", edit_user_path(@user) %>
|
|
15
|
+
<% end %>
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
### Match Frame in Response
|
|
19
|
+
|
|
20
|
+
```erb
|
|
21
|
+
<%# edit.html.erb - must have matching frame %>
|
|
22
|
+
<%= turbo_frame_tag "user_profile" do %>
|
|
23
|
+
<%= form_with model: @user do |f| %>
|
|
24
|
+
<%= f.text_field :name %>
|
|
25
|
+
<%= f.submit %>
|
|
26
|
+
<% end %>
|
|
27
|
+
<% end %>
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Frame Attributes
|
|
31
|
+
|
|
32
|
+
| Attribute | Purpose | Example |
|
|
33
|
+
|-----------|---------|---------|
|
|
34
|
+
| `id` | Frame identifier | `turbo_frame_tag "posts"` |
|
|
35
|
+
| `src` | Lazy load URL | `src: posts_path` |
|
|
36
|
+
| `loading` | Load timing | `loading: :lazy` |
|
|
37
|
+
| `target` | Navigation target | `target: "_top"` |
|
|
38
|
+
| `disabled` | Disable frame | `disabled: true` |
|
|
39
|
+
|
|
40
|
+
## Common Patterns
|
|
41
|
+
|
|
42
|
+
### Lazy Loading
|
|
43
|
+
|
|
44
|
+
```erb
|
|
45
|
+
<%# Load content when frame enters viewport %>
|
|
46
|
+
<%= turbo_frame_tag "comments",
|
|
47
|
+
src: post_comments_path(@post),
|
|
48
|
+
loading: :lazy do %>
|
|
49
|
+
<p>Loading comments...</p>
|
|
50
|
+
<% end %>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Breaking Out of Frame
|
|
54
|
+
|
|
55
|
+
```erb
|
|
56
|
+
<%# Link navigates full page, not just frame %>
|
|
57
|
+
<%= link_to "View All", posts_path, data: { turbo_frame: "_top" } %>
|
|
58
|
+
|
|
59
|
+
<%# Or in the frame tag %>
|
|
60
|
+
<%= turbo_frame_tag "modal", target: "_top" do %>
|
|
61
|
+
...
|
|
62
|
+
<% end %>
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Targeting Different Frame
|
|
66
|
+
|
|
67
|
+
```erb
|
|
68
|
+
<%# Link updates a different frame %>
|
|
69
|
+
<%= link_to "Details", post_path(@post), data: { turbo_frame: "post_details" } %>
|
|
70
|
+
|
|
71
|
+
<%# This frame will be updated %>
|
|
72
|
+
<%= turbo_frame_tag "post_details" do %>
|
|
73
|
+
<p>Select a post to see details</p>
|
|
74
|
+
<% end %>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Inline Editing Pattern
|
|
78
|
+
|
|
79
|
+
```erb
|
|
80
|
+
<%# Show mode %>
|
|
81
|
+
<%= turbo_frame_tag dom_id(post) do %>
|
|
82
|
+
<div class="post">
|
|
83
|
+
<h3><%= post.title %></h3>
|
|
84
|
+
<p><%= post.body %></p>
|
|
85
|
+
<%= link_to "Edit", edit_post_path(post) %>
|
|
86
|
+
</div>
|
|
87
|
+
<% end %>
|
|
88
|
+
|
|
89
|
+
<%# Edit mode (edit.html.erb) %>
|
|
90
|
+
<%= turbo_frame_tag dom_id(@post) do %>
|
|
91
|
+
<%= form_with model: @post, data: { turbo_frame: dom_id(@post) } do |f| %>
|
|
92
|
+
<%= f.text_field :title %>
|
|
93
|
+
<%= f.text_area :body %>
|
|
94
|
+
<%= f.submit "Save" %>
|
|
95
|
+
<%= link_to "Cancel", @post %>
|
|
96
|
+
<% end %>
|
|
97
|
+
<% end %>
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Modal Pattern
|
|
101
|
+
|
|
102
|
+
```erb
|
|
103
|
+
<%# Trigger link %>
|
|
104
|
+
<%= link_to "New Post", new_post_path, data: { turbo_frame: "modal" } %>
|
|
105
|
+
|
|
106
|
+
<%# Modal frame (in layout) %>
|
|
107
|
+
<%= turbo_frame_tag "modal" %>
|
|
108
|
+
|
|
109
|
+
<%# new.html.erb %>
|
|
110
|
+
<%= turbo_frame_tag "modal" do %>
|
|
111
|
+
<div class="modal-backdrop">
|
|
112
|
+
<div class="modal-content">
|
|
113
|
+
<h2>New Post</h2>
|
|
114
|
+
<%= form_with model: @post do |f| %>
|
|
115
|
+
...
|
|
116
|
+
<% end %>
|
|
117
|
+
<%= link_to "Close", root_path, data: { turbo_frame: "modal" } %>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
<% end %>
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Tab Navigation
|
|
124
|
+
|
|
125
|
+
```erb
|
|
126
|
+
<nav>
|
|
127
|
+
<%= link_to "Details", post_details_path(@post), data: { turbo_frame: "tab_content" } %>
|
|
128
|
+
<%= link_to "Comments", post_comments_path(@post), data: { turbo_frame: "tab_content" } %>
|
|
129
|
+
<%= link_to "History", post_history_path(@post), data: { turbo_frame: "tab_content" } %>
|
|
130
|
+
</nav>
|
|
131
|
+
|
|
132
|
+
<%= turbo_frame_tag "tab_content" do %>
|
|
133
|
+
<%= render "details" %>
|
|
134
|
+
<% end %>
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Frame Events
|
|
138
|
+
|
|
139
|
+
```javascript
|
|
140
|
+
// Listen for frame events
|
|
141
|
+
document.addEventListener("turbo:frame-load", (event) => {
|
|
142
|
+
console.log("Frame loaded:", event.target.id)
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
document.addEventListener("turbo:frame-missing", (event) => {
|
|
146
|
+
console.log("Frame not found in response:", event.target.id)
|
|
147
|
+
event.preventDefault() // Handle gracefully
|
|
148
|
+
})
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Troubleshooting
|
|
152
|
+
|
|
153
|
+
| Issue | Cause | Solution |
|
|
154
|
+
|-------|-------|----------|
|
|
155
|
+
| Frame not updating | ID mismatch | Ensure frame IDs match exactly |
|
|
156
|
+
| Full page reload | Missing frame in response | Add matching frame tag |
|
|
157
|
+
| Content disappears | Empty frame returned | Check controller response |
|
|
158
|
+
| Wrong frame updates | Multiple frames with same ID | Use unique IDs |
|