outro_rails 0.1.5 → 0.1.7

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 873a2a004ee523b79f100a4aa815e4986dbede341e9ca65740faef497821d8ad
4
- data.tar.gz: a053185e62246437b3fd50381ffbacac6afc60ac3e498bcdb06a2a314683b9cd
3
+ metadata.gz: 3985ee600ea177ac1e876651e79887b853620e683a0debfc4bdd24dc234961d6
4
+ data.tar.gz: 858928155568c2b92979dfeae2d84f7f679373a1d0010e16b92e5676a06b20dd
5
5
  SHA512:
6
- metadata.gz: 0d52f9319f9948d5b0912341bf59bc73ca165ad7d618cd6c9eeb3cdb2068bf8e3a949512d4d66e336fea4ec0da2725a02439537e76fb974ab3fd3ee5ed3f4788
7
- data.tar.gz: 60b284f1df23ebc1233f3c1a083e5b3ff263e3af45a4750dfea2d9b2ea112ae1ddb1cb9f402ba1e5a7b0be29c568db7624247a25c36b8c59bf20d71e8b4bf1ff
6
+ metadata.gz: 343d939032844096a9b8440f52936f79d4214a1c55ad132124b3f87e830d27bdbe730d851a3244eba6bbe50e0a34be31dc3ceedfd9d1b01dac1d92eb9b41dd89
7
+ data.tar.gz: 40380f523dc037dde65c1e20d7acc41769a79727cf219c2fbe6e8e8df5b076654450eca28733e32e377db19ce8568b1886fb309357ebba45c1b4bb590bddc5fd
data/README.md CHANGED
@@ -50,7 +50,8 @@ quickest version is to add two tags to your layout:
50
50
  ```
51
51
 
52
52
  If you'd rather fold the engine's assets into your existing bundles, see
53
- [Assets](#assets) below.
53
+ [Assets](#assets) below. If you'd rather style the form controls with your
54
+ own CSS framework, see [Control classes](#control-classes).
54
55
 
55
56
  ## Assets
56
57
 
@@ -161,6 +162,61 @@ yarn build
161
162
  yarn build:css
162
163
  ```
163
164
 
165
+ ### Form Control Classes
166
+
167
+ Out of the box the engine's text fields, textareas, selects, and labels
168
+ carry its own classes, styled by the stylesheet above:
169
+
170
+ | Element | Setting | Default |
171
+ | ---------- | -------------------------- | ---------------- |
172
+ | `button` | `OutroRails.button_class` | `outro-button` |
173
+ | `input` | `OutroRails.input_class` | `outro-input` |
174
+ | `label` | `OutroRails.label_class` | `outro-label` |
175
+ | `select` | `OutroRails.select_class` | `outro-select` |
176
+ | `textarea` | `OutroRails.textarea_class` | `outro-textarea` |
177
+
178
+ If your app already has a form style (Bootstrap, Tailwind, your own classes)
179
+ point these at it in an initializer and the engine will render your classes
180
+ instead:
181
+
182
+ ```ruby
183
+ # config/initializers/outro_rails.rb
184
+ OutroRails.configure do |config|
185
+ config.button_class = "btn btn-sm"
186
+ config.input_class = "form-control"
187
+ config.label_class = "form-label"
188
+ config.select_class = "form-select"
189
+ config.textarea_class = "form-control"
190
+ end
191
+ ```
192
+
193
+ Each setting is a plain string and can also be assigned on its own
194
+ (`OutroRails.select_class = "form-select"`).
195
+
196
+ These apply everywhere the engine renders a form control.
197
+
198
+ #### Per render
199
+
200
+ The same four names are accepted as locals on the chart partial and as
201
+ options on `chord_chart_area` for a page that needs something other than
202
+ the application-wide setting:
203
+
204
+ ```erb
205
+ <%= form.chord_chart_area :chord_sheet,
206
+ button_class: "btn-primary",
207
+ input_class: "form-control",
208
+ label_class: "form-label",
209
+ select_class: "form-select",
210
+ textarea_class: "form-control font-monospace" %>
211
+ ```
212
+
213
+ ```erb
214
+ <%= render "outro_rails/chord_charts/chart",
215
+ chord_chart: @song.chord_sheet,
216
+ select_class: "form-select",
217
+ label_class: "form-label" %>
218
+ ```
219
+
164
220
  ## Usage
165
221
  To view the chord library and music theory tools, mount the engine in your
166
222
  routes.rb:
@@ -258,6 +314,12 @@ When the model declares the chart with `required: false`, the fieldset is
258
314
  labeled "Optional" and tells the user to leave the fields blank if there's
259
315
  no chord chart.
260
316
 
317
+ The fields inside the fieldset use the engine's own control classes. Pass
318
+ `input_class:`, `textarea_class:`, `select_class:`, or `label_class:` to
319
+ render them with your app's classes for this form, or set them once
320
+ application-wide — see [Control classes](#control-classes). Any other
321
+ option is passed through to `fields_for`, as before.
322
+
261
323
  ### Controller: Permitting the Parameters
262
324
 
263
325
  `chord_chart_area` submits the chart as nested attributes, so your
@@ -471,7 +533,6 @@ It seeds the chord library and four example songs (`spec/dummy/db/seeds.rb`).
471
533
  The example songs are listed on the home page. To illustrate permissions,
472
534
  the dummy app sets even record ids as editable and odd ones as view-only.
473
535
 
474
-
475
536
  ## Publishing a New Version
476
537
 
477
538
  **1. Bump the version.** Edit `lib/outro_rails/version.rb`
@@ -11,8 +11,7 @@
11
11
  from `currentColor`, so they adapt to light and dark host themes.
12
12
  ========================================================================== */
13
13
 
14
- .outro,
15
- .outro-page {
14
+ :root {
16
15
  --outro-container-width: 66rem;
17
16
  --outro-radius: 14px;
18
17
  --outro-radius-sm: 6px;
@@ -37,6 +36,7 @@
37
36
  --outro-focus-ring: #7ab0ff;
38
37
  --outro-instrument-bg: #222222;
39
38
  --outro-instrument-radius: 12px;
39
+ --outro-field-gap: 0.25rem;
40
40
  --outro-fretboard: linear-gradient(180deg, #4a3222, #3a2718);
41
41
  --outro-fretboard-edge: #2c1f14;
42
42
  --outro-fretboard-ink: #d8c9b8;
@@ -54,8 +54,7 @@
54
54
 
55
55
  @supports not (color: color-mix(in srgb, currentColor 50%, transparent)) {
56
56
 
57
- .outro,
58
- .outro-page {
57
+ :root {
59
58
  --outro-muted: rgba(128, 128, 128, 0.9);
60
59
  --outro-border: rgba(128, 128, 128, 0.3);
61
60
  --outro-border-strong: rgba(128, 128, 128, 0.5);
@@ -129,8 +128,6 @@
129
128
  align-items: center;
130
129
  gap: 0.4rem;
131
130
  min-width: 0;
132
- align-items: center;
133
- min-width: 0;
134
131
  margin: 0;
135
132
  padding: 0.3rem clamp(0.6rem, 2vw, 0.95rem);
136
133
  border: 1px solid var(--outro-border);
@@ -307,21 +304,6 @@
307
304
  font: inherit;
308
305
  }
309
306
 
310
- .outro-button {
311
- display: inline-flex;
312
- align-items: center;
313
- gap: 0.45rem;
314
- padding: 0.4rem 0.9rem;
315
- border: 1px solid var(--outro-border-strong);
316
- border-radius: var(--outro-radius-sm);
317
- background-color: var(--outro-surface, transparent);
318
- color: inherit;
319
- font: inherit;
320
- line-height: 1.2;
321
- text-decoration: none;
322
- cursor: pointer;
323
- }
324
-
325
307
  .outro-card--link {
326
308
  transition: transform 0.16s ease, box-shadow 0.16s ease,
327
309
  border-color 0.16s ease;
@@ -415,13 +397,6 @@
415
397
  }
416
398
  }
417
399
 
418
- .outro-panel {
419
- padding: var(--outro-card-padding);
420
- border: 1px solid var(--outro-border);
421
- border-radius: var(--outro-radius);
422
- background-color: var(--outro-surface, transparent);
423
- }
424
-
425
400
  .outro-empty {
426
401
  padding: clamp(1.25rem, 4vw, 2rem);
427
402
  border: 1px dashed var(--outro-border-strong);
@@ -438,17 +413,6 @@
438
413
  margin-bottom: 0;
439
414
  }
440
415
 
441
- .outro-select,
442
- .outro-input,
443
- .outro-textarea {
444
- padding: var(--outro-control-padding);
445
- border: 1px solid var(--outro-border-strong);
446
- border-radius: var(--outro-radius-sm);
447
- background-color: var(--outro-surface, transparent);
448
- color: inherit;
449
- font: inherit;
450
- }
451
-
452
416
  .outro-button {
453
417
  display: inline-flex;
454
418
  align-items: center;
@@ -494,7 +458,7 @@
494
458
  .outro-picker__field {
495
459
  display: flex;
496
460
  flex-direction: column;
497
- gap: 0.25rem;
461
+ gap: var(--outro-field-gap);
498
462
  }
499
463
 
500
464
  .outro-picker__field .outro-select {
@@ -727,7 +691,6 @@
727
691
  .outro-guitar {
728
692
  display: flex;
729
693
  flex-direction: column;
730
- align-items: center;
731
694
  gap: 14px;
732
695
  width: 100%;
733
696
  max-width: 100%;
@@ -735,7 +698,6 @@
735
698
  border-radius: var(--outro-instrument-radius);
736
699
  background-color: var(--outro-instrument-bg);
737
700
  overflow-x: auto;
738
- align-items: center;
739
701
  align-items: safe center;
740
702
  }
741
703
 
@@ -910,7 +872,6 @@
910
872
  .outro-piano {
911
873
  display: flex;
912
874
  flex-direction: column;
913
- align-items: center;
914
875
  gap: 14px;
915
876
  width: 100%;
916
877
  max-width: 100%;
@@ -918,7 +879,6 @@
918
879
  border-radius: var(--outro-instrument-radius);
919
880
  background-color: var(--outro-instrument-bg);
920
881
  overflow-x: auto;
921
- align-items: center;
922
882
  align-items: safe center;
923
883
  }
924
884
 
@@ -1207,6 +1167,15 @@
1207
1167
  position: relative;
1208
1168
  }
1209
1169
 
1170
+ .outro-chord-chart-fields__legend {
1171
+ float: none;
1172
+ }
1173
+
1174
+ legend.outro-chord-chart-fields__legend {
1175
+ float: none;
1176
+ margin-bottom: 0;
1177
+ }
1178
+
1210
1179
  .outro-chord-chart__body {
1211
1180
  margin: 0;
1212
1181
  padding: 0.85rem;
@@ -3,6 +3,7 @@
3
3
  module OutroRails
4
4
  module ChordChartsHelper
5
5
  include CapoHelper
6
+ include ClassNamesHelper
6
7
  include InstrumentsHelper
7
8
  include TransposerHelper
8
9
 
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OutroRails
4
+ module ClassNamesHelper
5
+ def outro_button_class(*modifiers)
6
+ outro_element_class(:button, *modifiers)
7
+ end
8
+
9
+ def outro_input_class(*modifiers)
10
+ outro_element_class(:input, *modifiers)
11
+ end
12
+
13
+ def outro_label_class(*modifiers)
14
+ outro_element_class(:label, *modifiers)
15
+ end
16
+
17
+ def outro_select_class(*modifiers)
18
+ outro_element_class(:select, *modifiers)
19
+ end
20
+
21
+ def outro_textarea_class(*modifiers)
22
+ outro_element_class(:textarea, *modifiers)
23
+ end
24
+
25
+ def outro_element_class(element, *modifiers)
26
+ tokens = [ OutroRails.class_for(element), *modifiers ]
27
+ .flat_map { |token| token.to_s.split(/\s+/) }
28
+ .reject(&:empty?)
29
+ .uniq
30
+
31
+ tokens.join(" ").presence
32
+ end
33
+ end
34
+ end
@@ -16,27 +16,27 @@
16
16
  <%= form_with url: caged_picker_path, method: :get,
17
17
  class: "outro-picker" do %>
18
18
  <label class="outro-picker__field">
19
- <span class="outro-label">Root</span>
19
+ <%= tag.span "Root", class: outro_label_class %>
20
20
  <%= select_tag :root,
21
21
  options_for_select(scale_root_options, @root.path_slug),
22
- class: "outro-select",
22
+ class: outro_select_class,
23
23
  onchange: "this.form.requestSubmit()" %>
24
24
  </label>
25
25
 
26
26
  <label class="outro-picker__field">
27
- <span class="outro-label">Shape</span>
27
+ <%= tag.span "Shape", class: outro_label_class %>
28
28
  <%= select_tag :shape,
29
29
  options_for_select(
30
30
  OutroRails::Instruments::Guitar::ChordShapes::CAGED_LETTERS
31
31
  .map { |l| [ "#{l} shape", l.downcase ] },
32
32
  @letter.downcase
33
33
  ),
34
- class: "outro-select",
34
+ class: outro_select_class,
35
35
  onchange: "this.form.requestSubmit()" %>
36
36
  </label>
37
37
 
38
38
  <%= button_tag type: :submit, name: nil,
39
- class: "outro-button",
39
+ class: outro_button_class,
40
40
  "aria-label": "Show Shapes" do %>
41
41
  <%= image_tag "outro_rails/shapes.svg", class: 'outro-button__icon' %>
42
42
  Show Shapes
@@ -14,7 +14,8 @@
14
14
  ] %>
15
15
 
16
16
  <div class="outro-panel outro-filters">
17
- <span class="outro-label" id="capo-position-label">Capo position</span>
17
+ <%= tag.span "Capo position", id: "capo-position-label",
18
+ class: outro_label_class %>
18
19
  <nav class="outro-chip-row outro-capo-frets"
19
20
  aria-labelledby="capo-position-label">
20
21
  <% OutroRails::Instruments::Guitar::Capo::FRETS.each do |fret| %>
@@ -8,10 +8,7 @@
8
8
  render "outro_rails/chord_charts/chart", chord_chart: @song.chord_sheet
9
9
 
10
10
  Locals:
11
- chord_chart - required, an OutroRails::ChordChart. Pass nil (what an
12
- optional `has_chord_chart ..., required: false` reader
13
- returns before anyone fills it in) to render a short
14
- "no chord chart" placeholder instead.
11
+ chord_chart - required, an OutroRails::ChordChart
15
12
  instrument - "guitar" or "piano", default "guitar"
16
13
  to_key - preview the chart transposed into this key, default none
17
14
  editable - let the viewer change which voicing shows for each
@@ -22,277 +19,271 @@
22
19
 
23
20
  %>
24
21
 
25
- <% chord_chart = local_assigns.fetch(:chord_chart) %>
22
+ <%
23
+ chord_chart = local_assigns.fetch(:chord_chart)
24
+ valid_instruments = chord_chart_instrument_options.map(&:last)
25
+ requested = local_assigns[:instrument]
26
+ instrument = valid_instruments.include?(requested) ? requested : "guitar"
27
+ to_key = local_assigns[:to_key].presence
28
+ editable = local_assigns[:editable] || false
29
+ transposed = to_key.present? && to_key != chord_chart.key
30
+ chords = chord_chart.display_chords(to_key: to_key)
26
31
 
27
- <% if chord_chart.nil? %>
28
- <div class="outro-chord-chart outro-chord-chart--empty">
29
- <div class="outro-empty">
30
- <p>No chord chart.</p>
31
- </div>
32
- </div>
33
- <% else %>
34
-
35
- <%
36
- valid_instruments = chord_chart_instrument_options.map(&:last)
37
- requested = local_assigns[:instrument]
38
- instrument = valid_instruments.include?(requested) ? requested : "guitar"
39
- to_key = local_assigns[:to_key].presence
40
- editable = local_assigns[:editable] || false
41
- transposed = to_key.present? && to_key != chord_chart.key
42
- chords = chord_chart.display_chords(to_key: to_key)
32
+ voicings = chords.to_h do |symbol, chord|
33
+ bass = OutroRails::Theory::Transposer.split_bass(symbol).last
34
+ [ symbol,
35
+ chord_chart.voicing_for(chord, instrument,
36
+ transposed: transposed, bass: bass) ]
37
+ end
43
38
 
44
- voicings = chords.to_h do |symbol, chord|
45
- bass = OutroRails::Theory::Transposer.split_bass(symbol).last
46
- [ symbol,
47
- chord_chart.voicing_for(chord, instrument,
48
- transposed: transposed, bass: bass) ]
39
+ editing = editable && !transposed
40
+ selections =
41
+ if editing
42
+ chord_chart.voicings.where(instrument: instrument)
43
+ .index_by { |s| [ s.chord_id, s.bass ] }
44
+ else
45
+ {}
49
46
  end
47
+ %>
50
48
 
51
- editing = editable && !transposed
52
- selections =
53
- if editing
54
- chord_chart.voicings.where(instrument: instrument)
55
- .index_by { |s| [ s.chord_id, s.bass ] }
56
- else
57
- {}
58
- end
59
- %>
49
+ <%# The container is keyed by chart id so the voicing cycler's
50
+ fetch-based refresh (see outro_rails.js) can swap in the updated
51
+ chart from the redirected response without reloading the page. %>
52
+ <div class="outro-chord-chart" data-outro-chord-chart="<%= chord_chart.id %>">
53
+ <section class="outro-card">
54
+ <h2 class="outro-section-heading">
55
+ <%= chord_chart.song %>
56
+ <span class="outro-symbol-list__detail"><%= chord_chart.artist %></span>
57
+ </h2>
60
58
 
61
- <div class="outro-chord-chart" data-outro-chord-chart="<%= chord_chart.id %>">
62
- <section class="outro-card">
63
- <h2 class="outro-section-heading">
64
- <%= chord_chart.song %>
65
- <span class="outro-symbol-list__detail"><%= chord_chart.artist %></span>
66
- </h2>
59
+ <dl class="outro-spec">
60
+ <dt class="outro-spec__term">Tuning</dt>
61
+ <dd class="outro-spec__detail"><%= chord_chart.to_tuning.label %></dd>
67
62
 
68
- <dl class="outro-spec">
69
- <dt class="outro-spec__term">Tuning</dt>
70
- <dd class="outro-spec__detail"><%= chord_chart.to_tuning.label %></dd>
63
+ <dt class="outro-spec__term">Capo</dt>
64
+ <dd class="outro-spec__detail">
65
+ <% if chord_chart.capo.present? %>
66
+ <%= capo_fret_label(chord_chart.capo) %>
67
+ <% else %>
68
+ None
69
+ <% end %>
70
+ </dd>
71
71
 
72
- <dt class="outro-spec__term">Capo</dt>
73
- <dd class="outro-spec__detail">
74
- <% if chord_chart.capo.present? %>
75
- <%= capo_fret_label(chord_chart.capo) %>
76
- <% else %>
77
- None
78
- <% end %>
79
- </dd>
72
+ <dt class="outro-spec__term">Key</dt>
73
+ <dd class="outro-spec__detail">
74
+ <% if transposed %>
75
+ <%= to_key %>
76
+ <span class="outro-symbol-list__detail">
77
+ originally <%= chord_chart.key %>
78
+ </span>
79
+ <% else %>
80
+ <%= chord_chart.key %>
81
+ <% end %>
82
+ </dd>
83
+ </dl>
80
84
 
81
- <dt class="outro-spec__term">Key</dt>
82
- <dd class="outro-spec__detail">
83
- <% if transposed %>
84
- <%= to_key %>
85
- <span class="outro-symbol-list__detail">
86
- originally <%= chord_chart.key %>
87
- </span>
88
- <% else %>
89
- <%= chord_chart.key %>
90
- <% end %>
91
- </dd>
92
- </dl>
85
+ <div class="outro-picker outro-picker--flush">
86
+ <%= form_with url: request.path, method: :get,
87
+ class: "outro-picker__field" do %>
88
+ <span class="outro-label">Instrument</span>
89
+ <%= select_tag :instrument,
90
+ options_for_select(chord_chart_instrument_options, instrument),
91
+ class: outro_select_class,
92
+ onchange: "this.form.requestSubmit()" %>
93
+ <%= hidden_field_tag :to_key, to_key if to_key %>
94
+ <% end %>
93
95
 
94
- <% if chord_chart&.body.present? %>
95
- <div class="outro-picker outro-picker--flush">
96
- <%= form_with url: request.path, method: :get,
97
- class: "outro-picker__field" do %>
98
- <span class="outro-label">Instrument</span>
99
- <%= select_tag :instrument,
100
- options_for_select(chord_chart_instrument_options, instrument),
101
- class: "outro-select",
102
- onchange: "this.form.requestSubmit()" %>
103
- <%= hidden_field_tag :to_key, to_key if to_key %>
104
- <% end %>
96
+ <%= form_with url: request.path, method: :get,
97
+ class: "outro-picker__field" do %>
98
+ <span class="outro-label">Transpose to</span>
99
+ <%= select_tag :to_key,
100
+ grouped_options_for_select(chord_chart_key_options,
101
+ to_key || chord_chart.key),
102
+ class: outro_select_class,
103
+ onchange: "this.form.requestSubmit()" %>
104
+ <%= hidden_field_tag :instrument, instrument %>
105
+ <% end %>
105
106
 
106
- <%= form_with url: request.path, method: :get,
107
- class: "outro-picker__field" do %>
108
- <span class="outro-label">Transpose to</span>
109
- <%= select_tag :to_key,
110
- grouped_options_for_select(chord_chart_key_options,
111
- to_key || chord_chart.key),
112
- class: "outro-select",
113
- onchange: "this.form.requestSubmit()" %>
114
- <%= hidden_field_tag :instrument, instrument %>
115
- <% end %>
107
+ <% if transposed %>
108
+ <%= link_to url_for(request.query_parameters.except("to_key")),
109
+ class: outro_button_class do %>
110
+ <%= image_tag "outro_rails/undo.svg",
111
+ class: "outro-button__icon" %>
112
+ <%= "Reset to #{chord_chart.key}" %>
113
+ <% end %>
114
+ <% end %>
115
+ </div>
116
+ </section>
117
+
118
+ <section class="outro-card">
119
+ <h2 class="outro-section-heading">Chart</h2>
120
+ <pre class="outro-chord-chart__body"><%=
121
+ chord_chart_body(chord_chart, to_key: to_key)
122
+ %></pre>
116
123
 
117
- <% if transposed %>
118
- <%= link_to "Reset to #{chord_chart.key}",
119
- url_for(request.query_parameters.except("to_key")),
120
- class: "outro-chip outro-chip--link" %>
121
- <% end %>
122
- </div>
123
- <%end %>
124
- </section>
124
+ <%# Hover-popup figures, one per distinct chord in the body. <template>
125
+ contents are inert; outro_rails.js clones the matching one into a
126
+ floating popup when a chord in the chart text is hovered. %>
127
+ <% voicings.each do |symbol, voicing| %>
128
+ <% next unless voicing %>
129
+ <template data-outro-chord-popup="<%= symbol %>">
130
+ <p class="outro-chord-chart__popup-title"><%= voicing.symbol %></p>
131
+ <%= render "outro_rails/chord_voicings/figure", voicing: voicing %>
132
+ </template>
133
+ <% end %>
134
+ </section>
125
135
 
126
- <% if chord_chart&.body.present? %>
127
- <section class="outro-card">
128
- <h2 class="outro-section-heading">Chart</h2>
129
- <pre class="outro-chord-chart__body">
130
- <%= chord_chart_body(chord_chart, to_key: to_key) %>
131
- </pre>
136
+ <section class="outro-card">
137
+ <h2 class="outro-section-heading">
138
+ Chords
139
+ <% if editing %>
140
+ <span class="outro-symbol-list__detail">
141
+ Use the arrows to browse each chord's voicings and pick
142
+ which one shows.
143
+ </span>
144
+ <% end %>
145
+ </h2>
132
146
 
133
- <% voicings.each do |symbol, voicing| %>
134
- <% next unless voicing %>
135
- <template data-outro-chord-popup="<%= symbol %>">
136
- <p class="outro-chord-chart__popup-title"><%= voicing.symbol %></p>
137
- <%= render "outro_rails/chord_voicings/figure", voicing: voicing %>
138
- </template>
139
- <% end %>
140
- </section>
147
+ <% if chords.any? %>
148
+ <ul class="outro-grid outro-grid--figures">
149
+ <% chords.each do |symbol, chord| %>
150
+ <% voicing = voicings[symbol] %>
151
+ <li class="outro-grid__item">
152
+ <div class="outro-card">
153
+ <h3 class="outro-section-heading">
154
+ <%= link_to symbol, outro_rails.chord_path(chord) %>
155
+ <% if voicing && voicing.symbol != symbol %>
156
+ <span class="outro-symbol-list__detail">
157
+ <%= voicing.symbol %> voicing
158
+ </span>
159
+ <% end %>
160
+ </h3>
141
161
 
142
- <section class="outro-card">
143
- <h2 class="outro-section-heading">
144
- Chords
145
- <% if editing %>
146
- <span class="outro-symbol-list__detail">
147
- Use the arrows to browse each chord's voicings and pick
148
- which one shows.
149
- </span>
150
- <% end %>
151
- </h2>
162
+ <% if !voicing %>
163
+ <div class="outro-empty">
164
+ <p>No <%= instrument %> voicing yet.</p>
165
+ </div>
166
+ <% elsif editing %>
167
+ <%
168
+ bass = OutroRails::Theory::Transposer
169
+ .split_bass(symbol).last
170
+ all_voicings = chord.voicings
171
+ .where(instrument: instrument)
172
+ .order(:position).to_a
173
+ normalized = OutroRails::ChordChart::Voicing
174
+ .normalize_bass(bass)
175
+ selection = selections[[ chord.id, normalized ]]
176
+ default_voicing =
177
+ chord_chart.default_voicing_for(chord, instrument,
178
+ bass: bass)
152
179
 
153
- <% if chords.any? %>
154
- <ul class="outro-grid outro-grid--figures">
155
- <% chords.each do |symbol, chord| %>
156
- <% voicing = voicings[symbol] %>
157
- <li class="outro-grid__item">
158
- <div class="outro-card">
159
- <h3 class="outro-section-heading">
160
- <%= link_to symbol, outro_rails.chord_path(chord) %>
161
- <% if voicing && voicing.symbol != symbol %>
162
- <span class="outro-symbol-list__detail">
163
- <%= voicing.symbol %> voicing
164
- </span>
165
- <% end %>
166
- </h3>
180
+ form_params = {
181
+ chord_id: chord.id,
182
+ instrument: instrument,
183
+ bass: bass,
184
+ return_to: request.fullpath
185
+ }
186
+ %>
167
187
 
168
- <% if !voicing %>
169
- <div class="outro-empty">
170
- <p>No <%= instrument %> voicing yet.</p>
188
+ <div class="outro-voicing-cycler" data-outro-voicing-cycler>
189
+ <% if all_voicings.many? %>
190
+ <div class="outro-voicing-cycler__nav">
191
+ <button type="button" data-voicing-prev
192
+ class="<%= outro_button_class %>
193
+ outro-voicing-cycler__arrow"
194
+ aria-label=
195
+ "Previous voicing for <%= symbol %>">
196
+ &#9664;
197
+ </button>
198
+ <span class="outro-voicing-cycler__counter"
199
+ data-voicing-counter aria-live="polite"></span>
200
+ <button type="button" data-voicing-next
201
+ class="<%= outro_button_class %>
202
+ outro-voicing-cycler__arrow"
203
+ aria-label=
204
+ "Next voicing for <%= symbol %>">
205
+ &#9654;
206
+ </button>
171
207
  </div>
172
- <% elsif editing %>
173
- <%
174
- bass = OutroRails::Theory::Transposer
175
- .split_bass(symbol).last
176
- all_voicings = chord.voicings
177
- .where(instrument: instrument)
178
- .order(:position).to_a
179
- normalized = OutroRails::ChordChart::Voicing
180
- .normalize_bass(bass)
181
- selection = selections[[ chord.id, normalized ]]
182
- default_voicing =
183
- chord_chart.default_voicing_for(chord, instrument,
184
- bass: bass)
208
+ <% end %>
185
209
 
186
- form_params = {
187
- chord_id: chord.id,
188
- instrument: instrument,
189
- bass: bass,
190
- return_to: request.fullpath
191
- }
210
+ <% all_voicings.each do |candidate| %>
211
+ <%
212
+ showing = candidate.id == voicing.id
213
+ is_pick = selection&.chord_voicing_id == candidate.id
214
+ is_default = default_voicing&.id == candidate.id
215
+ caption = "#{candidate.symbol} - " \
216
+ "position #{candidate.position}"
192
217
  %>
218
+ <div class="outro-voicing-cycler__slide"
219
+ data-voicing-slide
220
+ <%= "hidden" unless showing %>>
221
+ <p class="outro-spec__detail">
222
+ <%= render "outro_rails/chord_voicings/figure",
223
+ voicing: candidate,
224
+ caption: caption %>
193
225
 
194
- <div class="outro-voicing-cycler" data-outro-voicing-cycler>
195
- <% if all_voicings.many? %>
196
- <div class="outro-voicing-cycler__nav">
197
- <button type="button" data-voicing-prev
198
- class="outro-button
199
- outro-voicing-cycler__arrow"
200
- aria-label=
201
- "Previous voicing for <%= symbol %>">
202
- &#9664;
203
- </button>
204
- <span class="outro-voicing-cycler__counter"
205
- data-voicing-counter aria-live="polite"></span>
206
- <button type="button" data-voicing-next
207
- class="outro-button
208
- outro-voicing-cycler__arrow"
209
- aria-label=
210
- "Next voicing for <%= symbol %>">
211
- &#9654;
212
- </button>
213
- </div>
214
- <% end %>
226
+ </p>
215
227
 
216
- <% all_voicings.each do |candidate| %>
228
+ <% if is_default && selection %>
229
+ <%# Not currently on the default: navigating here
230
+ offers the revert, same as the pick's slide. %>
217
231
  <%
218
- showing = candidate.id == voicing.id
219
- is_pick = selection&.chord_voicing_id == candidate.id
220
- is_default = default_voicing&.id == candidate.id
221
- caption = "#{candidate.symbol} - " \
222
- "position #{candidate.position}"
232
+ revert_path = outro_rails.chord_chart_voicing_path(
233
+ chord_chart, selection, **form_params
234
+ )
223
235
  %>
224
- <div class="outro-voicing-cycler__slide"
225
- data-voicing-slide
226
- <%= "hidden" unless showing %>>
227
- <p class="outro-spec__detail">
228
- <%= render "outro_rails/chord_voicings/figure",
229
- voicing: candidate,
230
- caption: caption %>
231
-
232
- </p>
233
-
234
- <% if is_default && selection %>
235
- <%# Not currently on the default: navigating here
236
- offers the revert, same as the pick's slide. %>
237
- <%
238
- revert_path = outro_rails.chord_chart_voicing_path(
239
- chord_chart, selection, **form_params
240
- )
241
- %>
242
- <%= button_to revert_path,
243
- method: :delete, class: "outro-button",
244
- form_class: "outro-inline-form" do %>
245
- <%= image_tag "outro_rails/undo.svg",
246
- class: "outro-button__icon" %>
247
- Revert to default
248
- <% end %>
249
- <% elsif !showing %>
250
- <%
251
- pick_path = outro_rails.chord_chart_voicings_path(
252
- chord_chart, **form_params
253
- )
254
- %>
255
- <%= button_to pick_path,
256
- method: :post,
257
- params: { chord_voicing_id: candidate.id },
258
- class: "outro-button",
259
- form_class: "outro-inline-form" do %>
260
- <%= image_tag "outro_rails/check.svg",
261
- class: "outro-button__icon" %>
262
- Select Voicing
263
- <% end %>
264
- <% elsif is_pick %>
265
- <%
266
- revert_path = outro_rails.chord_chart_voicing_path(
267
- chord_chart, selection, **form_params
268
- )
269
- %>
270
- <%= button_to revert_path,
271
- method: :delete, class: "outro-button",
272
- form_class: "outro-inline-form" do %>
273
- <%= image_tag "outro_rails/undo.svg",
274
- class: "outro-button__icon" %>
275
- Revert to default
276
- <% end %>
277
- <% end %>
278
- </div>
236
+ <%= button_to revert_path,
237
+ method: :delete, class: outro_button_class,
238
+ form_class: "outro-inline-form" do %>
239
+ <%= image_tag "outro_rails/undo.svg",
240
+ class: "outro-button__icon" %>
241
+ Revert to default
242
+ <% end %>
243
+ <% elsif !showing %>
244
+ <%
245
+ pick_path = outro_rails.chord_chart_voicings_path(
246
+ chord_chart, **form_params
247
+ )
248
+ %>
249
+ <%= button_to pick_path,
250
+ method: :post,
251
+ params: { chord_voicing_id: candidate.id },
252
+ class: "outro-button",
253
+ form_class: "outro-inline-form" do %>
254
+ <%= image_tag "outro_rails/check.svg",
255
+ class: "outro-button__icon" %>
256
+ Select Voicing
257
+ <% end %>
258
+ <% elsif is_pick %>
259
+ <%
260
+ revert_path = outro_rails.chord_chart_voicing_path(
261
+ chord_chart, selection, **form_params
262
+ )
263
+ %>
264
+ <%= button_to revert_path,
265
+ method: :delete, class: outro_button_class,
266
+ form_class: "outro-inline-form" do %>
267
+ <%= image_tag "outro_rails/undo.svg",
268
+ class: "outro-button__icon" %>
269
+ Revert to default
270
+ <% end %>
279
271
  <% end %>
280
272
  </div>
281
- <% else %>
282
- <%= render "outro_rails/chord_voicings/figure",
283
- voicing: voicing %>
284
273
  <% end %>
285
274
  </div>
286
- </li>
287
- <% end %>
288
- </ul>
289
- <% else %>
290
- <div class="outro-empty">
291
- <p>No recognized chords.</p>
292
- </div>
275
+ <% else %>
276
+ <%= render "outro_rails/chord_voicings/figure",
277
+ voicing: voicing %>
278
+ <% end %>
279
+ </div>
280
+ </li>
293
281
  <% end %>
294
- </section>
282
+ </ul>
283
+ <% else %>
284
+ <div class="outro-empty">
285
+ <p>No recognized chords yet.</p>
286
+ </div>
295
287
  <% end %>
296
- </div>
297
-
298
- <% end %>
288
+ </section>
289
+ </div>
@@ -44,40 +44,41 @@
44
44
 
45
45
  <div class="outro-picker outro-picker--grid outro-picker--cols-2">
46
46
  <label class="outro-picker__field">
47
- <span class="outro-label">Artist</span>
48
- <%= f.text_field :artist, class: "outro-input" %>
47
+ <%= tag.span "Artist", class: outro_label_class %>
48
+ <%= f.text_field :artist, class: outro_input_class %>
49
49
  </label>
50
50
 
51
51
  <label class="outro-picker__field">
52
- <span class="outro-label">Song</span>
53
- <%= f.text_field :song, class: "outro-input" %>
52
+ <%= tag.span "Song", class: outro_label_class %>
53
+ <%= f.text_field :song, class: outro_input_class %>
54
54
  </label>
55
55
  </div>
56
56
 
57
57
  <div class="outro-picker outro-picker--grid outro-picker--cols-3">
58
58
  <label class="outro-picker__field">
59
- <span class="outro-label">Tuning</span>
59
+ <%= tag.span "Tuning", class: outro_label_class %>
60
60
  <%= f.select :tuning, chord_chart_tuning_options, {},
61
- class: "outro-select" %>
61
+ class: outro_select_class %>
62
62
  </label>
63
63
 
64
64
  <label class="outro-picker__field">
65
- <span class="outro-label">Capo</span>
65
+ <%= tag.span "Capo", class: outro_label_class %>
66
66
  <%= f.select :capo, chord_chart_capo_options,
67
- { include_blank: "None" }, class: "outro-select" %>
67
+ { include_blank: "None" }, class: outro_select_class %>
68
68
  </label>
69
69
 
70
70
  <label class="outro-picker__field">
71
- <span class="outro-label">Key</span>
72
- <%= f.select :key, chord_chart_key_options, {}, class: "outro-select" %>
71
+ <%= tag.span "Key", class: outro_label_class %>
72
+ <%= f.select :key, chord_chart_key_options, {},
73
+ class: outro_select_class %>
73
74
  </label>
74
75
  </div>
75
76
 
76
77
  <label class="outro-transposer__field">
77
- <span class="outro-label">Chords &amp; lyrics (Optional)</span>
78
+ <%= tag.span "Chords & lyrics (Optional)", class: outro_label_class %>
78
79
  <%= f.text_area :body,
79
80
  rows: 16,
80
81
  placeholder: "Paste or type a chord chart here.",
81
- class: "outro-textarea outro-transposer__textarea" %>
82
+ class: outro_textarea_class("outro-transposer__textarea") %>
82
83
  </label>
83
84
  </fieldset>
@@ -5,9 +5,9 @@
5
5
  <%= search_field_tag :q, params[:missing],
6
6
  placeholder: "F#m7b5 or fsharpm7flat5",
7
7
  "aria-label": "Go to chord by symbol or slug",
8
- class: "outro-input outro-filters__search-input" %>
8
+ class: outro_input_class("outro-filters__search-input") %>
9
9
  <%= button_tag type: :submit, name: nil,
10
- class: "outro-button",
10
+ class: outro_button_class,
11
11
  "aria-label": "Search" do %>
12
12
  <%= image_tag "outro_rails/search.svg", class: 'outro-button__icon' %>
13
13
  Search
@@ -18,52 +18,52 @@
18
18
  <%= form_with url: chords_path, method: :get,
19
19
  class: "outro-picker outro-filters__facets" do %>
20
20
  <label class="outro-picker__field">
21
- <span class="outro-label">Root</span>
21
+ <%= tag.span "Root", class: outro_label_class %>
22
22
  <%= select_tag :root,
23
23
  options_for_select(chord_root_options, @filters[:root]),
24
24
  include_blank: "Any",
25
- class: "outro-select",
25
+ class: outro_select_class,
26
26
  onchange: "this.form.requestSubmit()" %>
27
27
  </label>
28
28
 
29
29
  <label class="outro-picker__field">
30
- <span class="outro-label">Quality</span>
30
+ <%= tag.span "Quality", class: outro_label_class %>
31
31
  <%= select_tag :quality,
32
32
  options_for_select(chord_quality_options, @filters[:quality]),
33
33
  include_blank: "Any",
34
- class: "outro-select",
34
+ class: outro_select_class,
35
35
  onchange: "this.form.requestSubmit()" %>
36
36
  </label>
37
37
 
38
38
  <label class="outro-picker__field">
39
- <span class="outro-label">Extension</span>
39
+ <%= tag.span "Extension", class: outro_label_class %>
40
40
  <%= select_tag :extension,
41
41
  options_for_select(chord_extension_options, @filters[:extension]),
42
42
  include_blank: "Any",
43
- class: "outro-select",
43
+ class: outro_select_class,
44
44
  onchange: "this.form.requestSubmit()" %>
45
45
  </label>
46
46
 
47
47
  <label class="outro-picker__field">
48
- <span class="outro-label">Alteration</span>
48
+ <%= tag.span "Alteration", class: outro_label_class %>
49
49
  <%= select_tag :alteration,
50
50
  options_for_select(chord_alteration_options, @filters[:alteration]),
51
51
  include_blank: "Any",
52
- class: "outro-select",
52
+ class: outro_select_class,
53
53
  onchange: "this.form.requestSubmit()" %>
54
54
  </label>
55
55
 
56
56
  <label class="outro-picker__field">
57
- <span class="outro-label">Added tone</span>
57
+ <%= tag.span "Added tone", class: outro_label_class %>
58
58
  <%= select_tag :added_tone,
59
59
  options_for_select(chord_added_tone_options, @filters[:added_tone]),
60
60
  include_blank: "Any",
61
- class: "outro-select",
61
+ class: outro_select_class,
62
62
  onchange: "this.form.requestSubmit()" %>
63
63
  </label>
64
64
 
65
65
  <%= button_tag type: :submit, name: nil,
66
- class: "outro-button",
66
+ class: outro_button_class,
67
67
  "aria-label": "Filter" do %>
68
68
  <%= image_tag "outro_rails/filter.svg", class: 'outro-button__icon' %>
69
69
  Filter
@@ -71,7 +71,7 @@
71
71
 
72
72
  <% if filters_active? %>
73
73
  <%= link_to chords_path,
74
- class: "outro-button",
74
+ class: outro_button_class,
75
75
  "aria-label": "Clear" do %>
76
76
  <%= image_tag "outro_rails/clear.svg", class: 'outro-button__icon' %>
77
77
  Clear
@@ -19,7 +19,7 @@
19
19
  <div class="outro-empty">
20
20
  <p>No chords match those filters.</p>
21
21
  <%= link_to chords_path,
22
- class: "outro-button",
22
+ class: outro_button_class,
23
23
  "aria-label": "Clear Filters" do %>
24
24
  <%= image_tag "outro_rails/clear.svg", class: 'outro-button__icon' %>
25
25
  Clear Filters
@@ -63,9 +63,8 @@
63
63
  </dl>
64
64
 
65
65
  <div class="outro-card__footer">
66
- <span class="outro-label" id="scale-instruments-label">
67
- See this scale on
68
- </span>
66
+ <%= tag.span "See this scale on", id: "scale-instruments-label",
67
+ class: outro_label_class %>
69
68
  <div class="outro-chip-row" role="group"
70
69
  aria-labelledby="scale-instruments-label">
71
70
  <% OutroRails::ScalesController::INSTRUMENTS.each do |instrument| %>
@@ -12,17 +12,17 @@
12
12
  <%= hidden_field_tag :instrument, @instrument %>
13
13
 
14
14
  <label class="outro-picker__field">
15
- <span class="outro-label">Tuning</span>
15
+ <%= tag.span "Tuning", class: outro_label_class %>
16
16
  <%= select_tag :tuning,
17
17
  options_for_select(guitar_tuning_options, tuning.slug),
18
- class: "outro-select",
18
+ class: outro_select_class,
19
19
  onchange: "this.form.requestSubmit()" %>
20
20
  </label>
21
21
 
22
22
  <%# Fallback for keyboard users / no JS; the select auto-submits
23
23
  on change. %>
24
24
  <%= button_tag type: :submit, name: nil,
25
- class: "outro-button",
25
+ class: outro_button_class,
26
26
  "aria-label": "Show Tuning" do %>
27
27
  <%= image_tag "outro_rails/guitar.svg", class: 'outro-button__icon' %>
28
28
  Show Tuning
@@ -3,33 +3,33 @@
3
3
  <%= hidden_field_tag :instrument, @instrument %>
4
4
 
5
5
  <label class="outro-picker__field">
6
- <span class="outro-label">Root</span>
6
+ <%= tag.span "Root", class: outro_label_class %>
7
7
  <%= select_tag :root,
8
8
  options_for_select(scale_root_options, @scale.root.path_slug),
9
- class: "outro-select",
9
+ class: outro_select_class,
10
10
  onchange: "this.form.requestSubmit()" %>
11
11
  </label>
12
12
 
13
13
  <label class="outro-picker__field">
14
- <span class="outro-label">Scale</span>
14
+ <%= tag.span "Scale", class: outro_label_class %>
15
15
  <%= select_tag :type,
16
16
  options_for_select(scale_type_options, @scale.slug),
17
- class: "outro-select",
17
+ class: outro_select_class,
18
18
  onchange: "this.form.requestSubmit()" %>
19
19
  </label>
20
20
 
21
21
  <% if @instrument == "guitar" %>
22
22
  <label class="outro-picker__field">
23
- <span class="outro-label">Tuning</span>
23
+ <%= tag.span "Tuning", class: outro_label_class %>
24
24
  <%= select_tag :tuning,
25
25
  options_for_select(guitar_tuning_options, @tuning&.slug),
26
- class: "outro-select",
26
+ class: outro_select_class,
27
27
  onchange: "this.form.requestSubmit()" %>
28
28
  </label>
29
29
  <% end %>
30
30
 
31
31
  <%= button_tag type: :submit, name: nil,
32
- class: "outro-button",
32
+ class: outro_button_class,
33
33
  "aria-label": "Show Scale" do %>
34
34
  <%= image_tag "outro_rails/scales.svg", class: 'outro-button__icon' %>
35
35
  Show Scale
@@ -77,9 +77,8 @@
77
77
  <% others = OutroRails::ScalesController::INSTRUMENTS - [ @instrument ] %>
78
78
  <% if others.any? %>
79
79
  <div class="outro-switch">
80
- <span class="outro-label" id="scale-switch-label">
81
- View this scale on
82
- </span>
80
+ <%= tag.span "View this scale on", id: "scale-switch-label",
81
+ class: outro_label_class %>
83
82
  <div class="outro-chip-row" role="group"
84
83
  aria-labelledby="scale-switch-label">
85
84
  <% others.each do |other| %>
@@ -21,39 +21,39 @@
21
21
  class: "outro-transposer" do %>
22
22
  <div class="outro-picker outro-picker--flush">
23
23
  <label class="outro-picker__field">
24
- <span class="outro-label">Convert</span>
24
+ <%= tag.span "Convert", class: outro_label_class %>
25
25
  <%= select_tag :mode,
26
26
  options_for_select(transposer_mode_options, @mode),
27
- class: "outro-select" %>
27
+ class: outro_select_class %>
28
28
  </label>
29
29
 
30
30
  <label class="outro-picker__field">
31
- <span class="outro-label">Original key</span>
31
+ <%= tag.span "Original key", class: outro_label_class %>
32
32
  <%= select_tag :from_key,
33
33
  grouped_options_for_select(transposer_key_options,
34
34
  @from_key.to_s),
35
- class: "outro-select" %>
35
+ class: outro_select_class %>
36
36
  </label>
37
37
 
38
38
  <label class="outro-picker__field">
39
- <span class="outro-label">New key</span>
39
+ <%= tag.span "New key", class: outro_label_class %>
40
40
  <%= select_tag :to_key,
41
41
  grouped_options_for_select(transposer_key_options,
42
42
  @to_key.to_s),
43
- class: "outro-select" %>
43
+ class: outro_select_class %>
44
44
  </label>
45
45
  </div>
46
46
 
47
47
  <label class="outro-transposer__field">
48
- <span class="outro-label">Chords and lyrics</span>
48
+ <%= tag.span "Chords and lyrics", class: outro_label_class %>
49
49
  <%= text_area_tag :text, @text,
50
50
  rows: 8,
51
51
  placeholder: "Paste a chord chart here.",
52
- class: "outro-textarea outro-transposer__textarea" %>
52
+ class: outro_textarea_class("outro-transposer__textarea") %>
53
53
  </label>
54
54
 
55
55
  <%= button_tag type: :submit, name: nil,
56
- class: "outro-button",
56
+ class: outro_button_class,
57
57
  "aria-label": "Transpose" do %>
58
58
  <%= image_tag "outro_rails/transposer.svg",
59
59
  class: 'outro-button__icon' %>
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OutroRails
4
- VERSION = "0.1.5"
4
+ VERSION = "0.1.7"
5
5
  end
data/lib/outro_rails.rb CHANGED
@@ -18,4 +18,35 @@ module OutroRails
18
18
  !!chord_chart_voicing_authorization.call(chord_chart: chord_chart,
19
19
  controller: controller)
20
20
  end
21
+
22
+ ELEMENT_CLASS_DEFAULTS = {
23
+ button: "outro-button",
24
+ input: "outro-input",
25
+ label: "outro-label",
26
+ select: "outro-select",
27
+ textarea: "outro-textarea"
28
+ }.freeze
29
+
30
+ mattr_accessor :button_class, default: ELEMENT_CLASS_DEFAULTS[:button]
31
+ mattr_accessor :input_class, default: ELEMENT_CLASS_DEFAULTS[:input]
32
+ mattr_accessor :label_class, default: ELEMENT_CLASS_DEFAULTS[:label]
33
+ mattr_accessor :select_class, default: ELEMENT_CLASS_DEFAULTS[:select]
34
+ mattr_accessor :textarea_class, default: ELEMENT_CLASS_DEFAULTS[:textarea]
35
+
36
+ def self.configure
37
+ yield self
38
+ self
39
+ end
40
+
41
+ def self.class_for(element)
42
+ key = element.to_sym
43
+
44
+ unless ELEMENT_CLASS_DEFAULTS.key?(key)
45
+ raise ArgumentError,
46
+ "unknown element #{element.inspect}; expected one of " \
47
+ "#{ELEMENT_CLASS_DEFAULTS.keys.map(&:inspect).join(', ')}"
48
+ end
49
+
50
+ public_send(:"#{key}_class")
51
+ end
21
52
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: outro_rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.5
4
+ version: 0.1.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - endtoendpaper
@@ -79,6 +79,7 @@ files:
79
79
  - app/helpers/outro_rails/chord_charts_helper.rb
80
80
  - app/helpers/outro_rails/chords_helper.rb
81
81
  - app/helpers/outro_rails/circle_of_fifths_helper.rb
82
+ - app/helpers/outro_rails/class_names_helper.rb
82
83
  - app/helpers/outro_rails/instruments_helper.rb
83
84
  - app/helpers/outro_rails/scales_helper.rb
84
85
  - app/helpers/outro_rails/transposer_helper.rb