view_component_storybook 0.5.0 → 0.10.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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +7 -128
  3. data/app/controllers/view_component/storybook/stories_controller.rb +10 -11
  4. data/app/views/view_component/storybook/stories/show.html.erb +8 -1
  5. data/config/locales/en.yml +32 -0
  6. data/lib/view_component/storybook.rb +14 -0
  7. data/lib/view_component/storybook/content_concern.rb +42 -0
  8. data/lib/view_component/storybook/controls.rb +5 -1
  9. data/lib/view_component/storybook/controls/base_options_config.rb +41 -0
  10. data/lib/view_component/storybook/controls/boolean_config.rb +7 -6
  11. data/lib/view_component/storybook/controls/color_config.rb +4 -6
  12. data/lib/view_component/storybook/controls/control_config.rb +25 -25
  13. data/lib/view_component/storybook/controls/controls_helpers.rb +76 -0
  14. data/lib/view_component/storybook/controls/custom_config.rb +52 -0
  15. data/lib/view_component/storybook/controls/date_config.rb +14 -13
  16. data/lib/view_component/storybook/controls/multi_options_config.rb +46 -0
  17. data/lib/view_component/storybook/controls/number_config.rb +13 -10
  18. data/lib/view_component/storybook/controls/object_config.rb +11 -7
  19. data/lib/view_component/storybook/controls/options_config.rb +18 -25
  20. data/lib/view_component/storybook/controls/simple_control_config.rb +48 -0
  21. data/lib/view_component/storybook/controls/text_config.rb +1 -3
  22. data/lib/view_component/storybook/dsl.rb +1 -2
  23. data/lib/view_component/storybook/dsl/{controls_dsl.rb → legacy_controls_dsl.rb} +19 -22
  24. data/lib/view_component/storybook/engine.rb +2 -1
  25. data/lib/view_component/storybook/method_args.rb +16 -0
  26. data/lib/view_component/storybook/method_args/control_method_args.rb +91 -0
  27. data/lib/view_component/storybook/method_args/method_args.rb +52 -0
  28. data/lib/view_component/storybook/method_args/method_parameters_names.rb +97 -0
  29. data/lib/view_component/storybook/slots.rb +14 -0
  30. data/lib/view_component/storybook/slots/slot.rb +24 -0
  31. data/lib/view_component/storybook/slots/slot_config.rb +44 -0
  32. data/lib/view_component/storybook/stories.rb +60 -14
  33. data/lib/view_component/storybook/story.rb +18 -0
  34. data/lib/view_component/storybook/story_config.rb +140 -15
  35. data/lib/view_component/storybook/tasks/write_stories_json.rake +6 -0
  36. data/lib/view_component/storybook/version.rb +1 -1
  37. metadata +64 -23
  38. data/lib/view_component/storybook/controls/array_config.rb +0 -36
  39. data/lib/view_component/storybook/dsl/story_dsl.rb +0 -39
@@ -4,19 +4,31 @@ module ViewComponent
4
4
  module Storybook
5
5
  class Stories
6
6
  extend ActiveSupport::DescendantsTracker
7
+ include ActiveModel::Validations
7
8
 
8
9
  class_attribute :story_configs, default: []
9
- class_attribute :parameters, :title, :stories_layout
10
+ class_attribute :stories_parameters, :stories_title, :stories_layout
11
+
12
+ validate :validate_story_configs
10
13
 
11
14
  class << self
15
+ def title(title = nil)
16
+ # if no argument is passed act like a getter
17
+ self.stories_title = title unless title.nil?
18
+ stories_title
19
+ end
20
+
12
21
  def story(name, component = default_component, &block)
13
- story_config = StoryConfig.configure(story_id(name), name, component, layout, &block)
22
+ story_config = StoryConfig.new(story_id(name), name, component, layout, &block)
23
+ story_config.instance_eval(&block)
14
24
  story_configs << story_config
15
25
  story_config
16
26
  end
17
27
 
18
- def parameters(**params)
19
- self.parameters = params
28
+ def parameters(params = nil)
29
+ # if no argument is passed act like a getter
30
+ self.stories_parameters = params unless params.nil?
31
+ stories_parameters
20
32
  end
21
33
 
22
34
  def layout(layout = nil)
@@ -26,6 +38,7 @@ module ViewComponent
26
38
  end
27
39
 
28
40
  def to_csf_params
41
+ validate!
29
42
  csf_params = { title: title }
30
43
  csf_params[:parameters] = parameters if parameters.present?
31
44
  csf_params[:stories] = story_configs.map(&:to_csf_params)
@@ -33,7 +46,7 @@ module ViewComponent
33
46
  end
34
47
 
35
48
  def write_csf_json
36
- json_path = File.join(ViewComponent::Storybook.stories_path, "#{stories_name}.stories.json")
49
+ json_path = File.join(stories_path, "#{stories_name}.stories.json")
37
50
  File.open(json_path, "w") do |f|
38
51
  f.write(JSON.pretty_generate(to_csf_params))
39
52
  end
@@ -56,7 +69,7 @@ module ViewComponent
56
69
  end
57
70
 
58
71
  # Find a component stories by its underscored class name.
59
- def find_stories(stories_name)
72
+ def find_story_configs(stories_name)
60
73
  all.find { |stories| stories.stories_name == stories_name }
61
74
  end
62
75
 
@@ -66,23 +79,34 @@ module ViewComponent
66
79
  end
67
80
 
68
81
  # find the story by name
69
- def find_story(name)
82
+ def find_story_config(name)
70
83
  story_configs.find { |config| config.name == name.to_sym }
71
84
  end
72
85
 
86
+ # validation - ActiveModel::Validations like but on the class vs the instance
87
+ def valid?
88
+ # use an instance so we can enjoy the benefits of ActiveModel::Validations
89
+ @validation_instance = new
90
+ @validation_instance.valid?
91
+ end
92
+
93
+ delegate :errors, to: :@validation_instance
94
+
95
+ def validate!
96
+ valid? || raise(ValidationError, @validation_instance)
97
+ end
98
+
73
99
  private
74
100
 
75
101
  def inherited(other)
76
102
  super(other)
77
103
  # setup class defaults
78
- other.title = other.stories_name.humanize.titlecase
104
+ other.stories_title = other.stories_name.humanize.titlecase
79
105
  other.story_configs = []
80
106
  end
81
107
 
82
108
  def default_component
83
109
  name.chomp("Stories").constantize
84
- rescue StandardError
85
- nil
86
110
  end
87
111
 
88
112
  def load_stories
@@ -93,14 +117,36 @@ module ViewComponent
93
117
  Storybook.stories_path
94
118
  end
95
119
 
96
- def show_stories
97
- Storybook.show_stories
98
- end
99
-
100
120
  def story_id(name)
101
121
  "#{stories_name}/#{name.to_s.parameterize}".underscore
102
122
  end
103
123
  end
124
+
125
+ protected
126
+
127
+ def validate_story_configs
128
+ story_configs.reject(&:valid?).each do |story_config|
129
+ story_errors = story_config.errors.full_messages.join(', ')
130
+ errors.add(:story_configs, :invalid_story, story_name: story_config.name, story_errors: story_errors)
131
+ end
132
+
133
+ story_names = story_configs.map(&:name)
134
+ duplicate_names = story_names.group_by(&:itself).map { |k, v| k if v.length > 1 }.compact
135
+ return if duplicate_names.empty?
136
+
137
+ duplicate_name_sentence = duplicate_names.map { |name| "'#{name}'" }.to_sentence
138
+ errors.add(:story_configs, :duplicate_stories, count: duplicate_names.count, duplicate_names: duplicate_name_sentence)
139
+ end
140
+
141
+ class ValidationError < StandardError
142
+ attr_reader :stories
143
+
144
+ def initialize(stories)
145
+ @stories = stories
146
+
147
+ super("#{@stories.class.name} invalid: (#{@stories.errors.full_messages.join(', ')})")
148
+ end
149
+ end
104
150
  end
105
151
  end
106
152
  end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponent
4
+ module Storybook
5
+ class Story
6
+ include ActiveModel::Validations
7
+
8
+ attr_reader :component, :content_block, :slots, :layout
9
+
10
+ def initialize(component, content_block, slots, layout)
11
+ @component = component
12
+ @content_block = content_block
13
+ @slots = slots
14
+ @layout = layout
15
+ end
16
+ end
17
+ end
18
+ end
@@ -4,19 +4,73 @@ module ViewComponent
4
4
  module Storybook
5
5
  class StoryConfig
6
6
  include ActiveModel::Validations
7
+ include ContentConcern
8
+ include Controls::ControlsHelpers
7
9
 
8
- attr_reader :id, :name, :component
9
- attr_accessor :controls, :parameters, :layout, :content_block
10
+ attr_reader :id, :name, :component_class
10
11
 
11
- def initialize(id, name, component, layout)
12
+ validate :validate_constructor_args
13
+
14
+ def initialize(id, name, component_class, layout)
12
15
  @id = id
13
16
  @name = name
14
- @component = component
17
+ @component_class = component_class
15
18
  @layout = layout
16
- @controls = []
19
+ @slots ||= {}
20
+ end
21
+
22
+ def constructor(*args, **kwargs, &block)
23
+ @constructor_args = MethodArgs::ControlMethodArgs.new(
24
+ component_constructor,
25
+ *args,
26
+ **kwargs
27
+ )
28
+ content(nil, &block)
29
+
30
+ self
31
+ end
32
+
33
+ # Once deprecated block version is removed make this a private getter
34
+ def controls(&block)
35
+ if block_given?
36
+ ActiveSupport::Deprecation.warn("`controls` will be removed in v1.0.0. Use `#constructor` instead.")
37
+ controls_dsl = Dsl::LegacyControlsDsl.new
38
+ controls_dsl.instance_eval(&block)
39
+
40
+ controls_hash = controls_dsl.controls.index_by(&:param)
41
+ constructor(**controls_hash)
42
+ else
43
+ list = constructor_args.controls.dup
44
+ list << content_control if content_control
45
+ list += slots.flat_map(&:controls) if slots
46
+ list
47
+ end
48
+ end
49
+
50
+ def layout(layout = nil)
51
+ @layout = layout unless layout.nil?
52
+ @layout
53
+ end
54
+
55
+ def parameters(parameters = nil)
56
+ @parameters = parameters unless parameters.nil?
57
+ @parameters
58
+ end
59
+
60
+ def method_missing(method_name, *args, **kwargs, &block)
61
+ if component_class.slot_type(method_name)
62
+ slot(method_name, *args, **kwargs, &block)
63
+ else
64
+ super
65
+ end
66
+ end
67
+
68
+ def respond_to_missing?(method_name, _include_private = false)
69
+ component_class.slot_type(method_name).present?
17
70
  end
18
71
 
19
72
  def to_csf_params
73
+ validate!
20
74
  csf_params = { name: name, parameters: { server: { id: id } } }
21
75
  csf_params.deep_merge!(parameters: parameters) if parameters.present?
22
76
  controls.each do |control|
@@ -25,18 +79,89 @@ module ViewComponent
25
79
  csf_params
26
80
  end
27
81
 
28
- def values_from_params(params)
29
- controls.map do |control|
30
- value = control.value_from_param(params[control.param])
31
- value = control.value if value.nil? # nil only not falsey
32
- [control.param, value]
33
- end.to_h
82
+ def validate!
83
+ valid? || raise(ValidationError, self)
84
+ end
85
+
86
+ ##
87
+ # Build a Story from this config
88
+ # * Resolves the values of the constructor args from the params
89
+ # * constructs the component
90
+ # * resolve the content_control and content_block to a single block
91
+ # * builds a list of Slots by resolving their args from the params
92
+ def story(params)
93
+ # constructor_args.target_method is UnboundMethod so can't call it directly
94
+ component = constructor_args.call(params) do |*args, **kwargs|
95
+ component_class.new(*args, **kwargs)
96
+ end
97
+
98
+ story_content_block = resolve_content_block(params)
99
+
100
+ story_slots = slots.map do |slot_config|
101
+ slot_config.slot(component, params)
102
+ end
103
+
104
+ Storybook::Story.new(component, story_content_block, story_slots, layout)
105
+ end
106
+
107
+ class ValidationError < StandardError
108
+ attr_reader :story_config
109
+
110
+ def initialize(story_config)
111
+ @story_config = story_config
112
+
113
+ super("'#{@story_config.name}' invalid: (#{@story_config.errors.full_messages.join(', ')})")
114
+ end
115
+ end
116
+
117
+ private
118
+
119
+ def constructor_args
120
+ @constructor_args ||= MethodArgs::ControlMethodArgs.new(component_constructor)
121
+ end
122
+
123
+ def component_constructor
124
+ component_class.instance_method(:initialize)
125
+ end
126
+
127
+ def slots
128
+ @slots.values.flatten
34
129
  end
35
130
 
36
- def self.configure(id, name, component, layout, &configuration)
37
- config = new(id, name, component, layout)
38
- ViewComponent::Storybook::Dsl::StoryDsl.evaluate!(config, &configuration)
39
- config
131
+ def validate_constructor_args
132
+ return if constructor_args.valid?
133
+
134
+ constructor_args_errors = constructor_args.errors.full_messages.join(', ')
135
+ errors.add(:constructor_args, :invalid, errors: constructor_args_errors)
136
+ end
137
+
138
+ def slot(slot_name, *args, **kwargs, &block)
139
+ # if the name is a slot then build a SlotConfig with slot_name and param the same
140
+ if component_class.slot_type(slot_name) == :collection_item
141
+ # generate a unique param generated by the count of slots with this name already added
142
+ @slots[slot_name] ||= []
143
+ slot_index = @slots[slot_name].count + 1
144
+ slot_config = Slots::SlotConfig.from_component(
145
+ component_class,
146
+ slot_name,
147
+ "#{slot_name}#{slot_index}".to_sym,
148
+ *args,
149
+ **kwargs,
150
+ &block
151
+ )
152
+ @slots[slot_name] << slot_config
153
+ else
154
+ slot_config = Slots::SlotConfig.from_component(
155
+ component_class,
156
+ slot_name,
157
+ slot_name,
158
+ *args,
159
+ **kwargs,
160
+ &block
161
+ )
162
+ @slots[slot_name] = slot_config
163
+ end
164
+ slot_config
40
165
  end
41
166
  end
42
167
  end
@@ -4,10 +4,16 @@ namespace :view_component_storybook do
4
4
  desc "Write CSF JSON stories for all Stories"
5
5
  task write_stories_json: :environment do
6
6
  puts "Writing Stories JSON"
7
+ exceptions = []
7
8
  ViewComponent::Storybook::Stories.all.each do |stories|
8
9
  json_path = stories.write_csf_json
9
10
  puts "#{stories.name} => #{json_path}"
11
+ rescue StandardError => e
12
+ exceptions << e
10
13
  end
14
+
15
+ raise StandardError, exceptions.map(&:message).join(", ") if exceptions.present?
16
+
11
17
  puts "Done"
12
18
  end
13
19
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module ViewComponent
4
4
  module Storybook
5
- VERSION = "0.5.0"
5
+ VERSION = "0.10.0"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: view_component_storybook
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jon Palmer
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-09-19 00:00:00.000000000 Z
11
+ date: 2021-08-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: view_component
@@ -16,28 +16,42 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '2.2'
19
+ version: '2.36'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '2.2'
26
+ version: '2.36'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: bundler
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '1.14'
33
+ version: '2.2'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.2'
41
+ - !ruby/object:Gem::Dependency
42
+ name: capybara
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3'
34
48
  type: :development
35
49
  prerelease: false
36
50
  version_requirements: !ruby/object:Gem::Requirement
37
51
  requirements:
38
52
  - - "~>"
39
53
  - !ruby/object:Gem::Version
40
- version: '1.14'
54
+ version: '3'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: rake
43
57
  requirement: !ruby/object:Gem::Requirement
@@ -72,84 +86,98 @@ dependencies:
72
86
  requirements:
73
87
  - - "~>"
74
88
  - !ruby/object:Gem::Version
75
- version: '3.9'
89
+ version: '3.10'
76
90
  type: :development
77
91
  prerelease: false
78
92
  version_requirements: !ruby/object:Gem::Requirement
79
93
  requirements:
80
94
  - - "~>"
81
95
  - !ruby/object:Gem::Version
82
- version: '3.9'
96
+ version: '3.10'
83
97
  - !ruby/object:Gem::Dependency
84
98
  name: rspec-rails
85
99
  requirement: !ruby/object:Gem::Requirement
86
100
  requirements:
87
101
  - - "~>"
88
102
  - !ruby/object:Gem::Version
89
- version: '3.9'
103
+ version: '5.0'
90
104
  type: :development
91
105
  prerelease: false
92
106
  version_requirements: !ruby/object:Gem::Requirement
93
107
  requirements:
94
108
  - - "~>"
95
109
  - !ruby/object:Gem::Version
96
- version: '3.9'
110
+ version: '5.0'
97
111
  - !ruby/object:Gem::Dependency
98
112
  name: rubocop
99
113
  requirement: !ruby/object:Gem::Requirement
100
114
  requirements:
101
115
  - - "~>"
102
116
  - !ruby/object:Gem::Version
103
- version: '0.81'
117
+ version: '1.18'
104
118
  type: :development
105
119
  prerelease: false
106
120
  version_requirements: !ruby/object:Gem::Requirement
107
121
  requirements:
108
122
  - - "~>"
109
123
  - !ruby/object:Gem::Version
110
- version: '0.81'
124
+ version: '1.18'
111
125
  - !ruby/object:Gem::Dependency
112
126
  name: rubocop-rails
113
127
  requirement: !ruby/object:Gem::Requirement
114
128
  requirements:
115
129
  - - "~>"
116
130
  - !ruby/object:Gem::Version
117
- version: 2.4.2
131
+ version: '2.11'
118
132
  type: :development
119
133
  prerelease: false
120
134
  version_requirements: !ruby/object:Gem::Requirement
121
135
  requirements:
122
136
  - - "~>"
123
137
  - !ruby/object:Gem::Version
124
- version: 2.4.2
138
+ version: '2.11'
125
139
  - !ruby/object:Gem::Dependency
126
140
  name: rubocop-rspec
127
141
  requirement: !ruby/object:Gem::Requirement
128
142
  requirements:
129
143
  - - "~>"
130
144
  - !ruby/object:Gem::Version
131
- version: '1.38'
145
+ version: '2.1'
132
146
  type: :development
133
147
  prerelease: false
134
148
  version_requirements: !ruby/object:Gem::Requirement
135
149
  requirements:
136
150
  - - "~>"
137
151
  - !ruby/object:Gem::Version
138
- version: '1.38'
152
+ version: '2.1'
139
153
  - !ruby/object:Gem::Dependency
140
154
  name: simplecov
141
155
  requirement: !ruby/object:Gem::Requirement
142
156
  requirements:
143
157
  - - "~>"
144
158
  - !ruby/object:Gem::Version
145
- version: 0.18.5
159
+ version: 0.21.2
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - "~>"
165
+ - !ruby/object:Gem::Version
166
+ version: 0.21.2
167
+ - !ruby/object:Gem::Dependency
168
+ name: simplecov-console
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - "~>"
172
+ - !ruby/object:Gem::Version
173
+ version: '0.9'
146
174
  type: :development
147
175
  prerelease: false
148
176
  version_requirements: !ruby/object:Gem::Requirement
149
177
  requirements:
150
178
  - - "~>"
151
179
  - !ruby/object:Gem::Version
152
- version: 0.18.5
180
+ version: '0.9'
153
181
  description: Generate Storybook CSF JSON for rendering Rails View Components in Storybook
154
182
  email:
155
183
  - 328224+jonspalmer@users.noreply.github.com
@@ -161,22 +189,35 @@ files:
161
189
  - README.md
162
190
  - app/controllers/view_component/storybook/stories_controller.rb
163
191
  - app/views/view_component/storybook/stories/show.html.erb
192
+ - config/locales/en.yml
164
193
  - lib/view_component/storybook.rb
194
+ - lib/view_component/storybook/content_concern.rb
165
195
  - lib/view_component/storybook/controls.rb
166
- - lib/view_component/storybook/controls/array_config.rb
196
+ - lib/view_component/storybook/controls/base_options_config.rb
167
197
  - lib/view_component/storybook/controls/boolean_config.rb
168
198
  - lib/view_component/storybook/controls/color_config.rb
169
199
  - lib/view_component/storybook/controls/control_config.rb
200
+ - lib/view_component/storybook/controls/controls_helpers.rb
201
+ - lib/view_component/storybook/controls/custom_config.rb
170
202
  - lib/view_component/storybook/controls/date_config.rb
203
+ - lib/view_component/storybook/controls/multi_options_config.rb
171
204
  - lib/view_component/storybook/controls/number_config.rb
172
205
  - lib/view_component/storybook/controls/object_config.rb
173
206
  - lib/view_component/storybook/controls/options_config.rb
207
+ - lib/view_component/storybook/controls/simple_control_config.rb
174
208
  - lib/view_component/storybook/controls/text_config.rb
175
209
  - lib/view_component/storybook/dsl.rb
176
- - lib/view_component/storybook/dsl/controls_dsl.rb
177
- - lib/view_component/storybook/dsl/story_dsl.rb
210
+ - lib/view_component/storybook/dsl/legacy_controls_dsl.rb
178
211
  - lib/view_component/storybook/engine.rb
212
+ - lib/view_component/storybook/method_args.rb
213
+ - lib/view_component/storybook/method_args/control_method_args.rb
214
+ - lib/view_component/storybook/method_args/method_args.rb
215
+ - lib/view_component/storybook/method_args/method_parameters_names.rb
216
+ - lib/view_component/storybook/slots.rb
217
+ - lib/view_component/storybook/slots/slot.rb
218
+ - lib/view_component/storybook/slots/slot_config.rb
179
219
  - lib/view_component/storybook/stories.rb
220
+ - lib/view_component/storybook/story.rb
180
221
  - lib/view_component/storybook/story_config.rb
181
222
  - lib/view_component/storybook/tasks/write_stories_json.rake
182
223
  - lib/view_component/storybook/version.rb
@@ -195,14 +236,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
195
236
  requirements:
196
237
  - - ">="
197
238
  - !ruby/object:Gem::Version
198
- version: 2.3.0
239
+ version: 2.5.0
199
240
  required_rubygems_version: !ruby/object:Gem::Requirement
200
241
  requirements:
201
242
  - - ">="
202
243
  - !ruby/object:Gem::Version
203
244
  version: '0'
204
245
  requirements: []
205
- rubygems_version: 3.0.6
246
+ rubygems_version: 3.1.6
206
247
  signing_key:
207
248
  specification_version: 4
208
249
  summary: Storybook for Rails View Components