view_component-storybook 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (30) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +21 -0
  4. data/docs/CHANGELOG.md +69 -0
  5. data/lib/view_component/storybook/collections/controls_collection.rb +80 -0
  6. data/lib/view_component/storybook/collections/layout_collection.rb +37 -0
  7. data/lib/view_component/storybook/collections/parameters_collection.rb +40 -0
  8. data/lib/view_component/storybook/collections/stories_collection.rb +31 -0
  9. data/lib/view_component/storybook/collections/valid_for_story_concern.rb +15 -0
  10. data/lib/view_component/storybook/collections.rb +17 -0
  11. data/lib/view_component/storybook/controls/base_options.rb +30 -0
  12. data/lib/view_component/storybook/controls/boolean.rb +30 -0
  13. data/lib/view_component/storybook/controls/color.rb +26 -0
  14. data/lib/view_component/storybook/controls/control.rb +34 -0
  15. data/lib/view_component/storybook/controls/date.rb +32 -0
  16. data/lib/view_component/storybook/controls/multi_options.rb +44 -0
  17. data/lib/view_component/storybook/controls/number.rb +38 -0
  18. data/lib/view_component/storybook/controls/object.rb +28 -0
  19. data/lib/view_component/storybook/controls/options.rb +36 -0
  20. data/lib/view_component/storybook/controls/simple_control.rb +47 -0
  21. data/lib/view_component/storybook/controls/text.rb +13 -0
  22. data/lib/view_component/storybook/controls.rb +23 -0
  23. data/lib/view_component/storybook/engine.rb +76 -0
  24. data/lib/view_component/storybook/stories.rb +120 -0
  25. data/lib/view_component/storybook/stories_parser.rb +48 -0
  26. data/lib/view_component/storybook/story.rb +25 -0
  27. data/lib/view_component/storybook/tasks/view_component_storybook.rake +35 -0
  28. data/lib/view_component/storybook/version.rb +7 -0
  29. data/lib/view_component/storybook.rb +63 -0
  30. metadata +285 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: dd438eab6c92e5e41e9df2b06d53c5702cb9f3121e7af553a743512058c1ef2c
4
+ data.tar.gz: 66a3827dd5f7522da5fd429d79a2d1c590e26d142395ab757d57bb7eee391b76
5
+ SHA512:
6
+ metadata.gz: be57cabc078e464195eee2ae30b63dfd8629807eea2c15e2dd260ac80e545104bce41b2e51d5aa71b429fc7504cf41e2fc018292a2088a70ce29d0d24c59369a
7
+ data.tar.gz: 27d2af2dcb91f2acf6c22b083130650e440330bbf649974a746f634e22e9e135f933342fe1a84302d0640b2498f8669fe0c5e39150a18a9a85e6b6cb0d386f51
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2020 Jon Palmer
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,21 @@
1
+ # ViewComponent::Storybook
2
+
3
+ The ViewComponent::Storybook gem provides Ruby api for writing stories describing [View Components](https://github.com/github/view_component) and allowing them to be previewed and tested in [Storybook](https://github.com/storybookjs/storybook/) via its [Server](https://github.com/storybookjs/storybook/tree/next/app/server) support.
4
+
5
+ ## Features
6
+
7
+ - A Ruby DSL for writing Stories describing View Components
8
+ - A Rails controller backend for Storybook Server compatible with Storybook Controls Addon parameters
9
+ - Coming Soon: Rake tasks to watch View Components and Stories and trigger Storybook hot reloading
10
+
11
+ ## Documentation
12
+
13
+ See [jonspalmer.github.io/view_component-storybook](https://jonspalmer.github.io/view_component-storybook) for documentation.
14
+
15
+ ## Contributing
16
+
17
+ This project is intended to be a safe, welcoming space for collaboration. Contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct. We recommend reading the [contributing guide](./docs/CONTRIBUTING.md) as well.
18
+
19
+ ## License
20
+
21
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/docs/CHANGELOG.md ADDED
@@ -0,0 +1,69 @@
1
+ ---
2
+ layout: default
3
+ title: Changelog
4
+ ---
5
+
6
+ # Changelog
7
+
8
+ ## main
9
+
10
+ ## 0.12.1
11
+
12
+ Last 0.x release
13
+ * Prepare for renaming to `view_component-storybook` to align with Rubygems naming conventions and simpler installation.
14
+
15
+ ## 0.12.0
16
+
17
+ * Allow configuration of story titles via new `stories_title_generator` configuration lambda
18
+ * Added control descriptions
19
+ * Fixed bug with autoload paths
20
+ * Fixed Documentation typos
21
+
22
+ ## 0.11.1
23
+
24
+ * Fix for stories_route by when using deprecated `require "view_component/storybook/engine"`
25
+
26
+ ## 0.11.0
27
+
28
+ * Add support for dry-initializer
29
+ * Validate Slots
30
+
31
+ ## 0.10.1
32
+
33
+ * Allow Object Controls with arrays of simple values
34
+
35
+ ## 0.10.0
36
+
37
+ * New Stories API
38
+ * `title` - custom stories title
39
+ * `constructor` - component args
40
+ * `slot` - args and content for slots
41
+ * component and slot content accepts controls
42
+ * New Controls
43
+ * Custom Control
44
+ * Klazz Control
45
+ * Deprecated Controls DSL
46
+ * Add support for Object Controls with array values
47
+ * Remove ArrayConfig - array aliases Object
48
+ * Updated Options Control to match Storybook 6.2 syntax
49
+ * `options` as array
50
+ * add `labels` option
51
+ * deprecate `options` as Hash
52
+ * multi select types accept and return array values
53
+ * Improved validation errors with i18n support
54
+ * Add `stories_route` config to configure stories endpoint
55
+ * Jekyll docs
56
+
57
+ ## 0.9.0
58
+
59
+ * Allow view helpers in content blocks
60
+
61
+ ## 0.8.0
62
+
63
+ * Add support for components with keyword argument constructors
64
+
65
+ ## 0.7.0
66
+
67
+ * Add inclusion validation to Number type
68
+ * Support Objects with nested values
69
+ * Support nil control values
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponent
4
+ module Storybook
5
+ module Collections
6
+ class ControlsCollection
7
+ include Collections::ValidForStoryConcern
8
+
9
+ attr_reader :controls
10
+
11
+ attr_accessor :code_object
12
+
13
+ def initialize
14
+ @controls = []
15
+ end
16
+
17
+ def add(param, as:, only: nil, except: nil, **opts)
18
+ controls << { param: param, as: as, only: only, except: except, **opts }
19
+ end
20
+
21
+ def for_story(story_name)
22
+ # build the controls for the story_name
23
+ # pass through a hash to get the last valid control declared for each param
24
+ controls.map do |opts|
25
+ next unless valid_for_story?(story_name, **opts.slice(:only, :except))
26
+
27
+ param = opts[:param]
28
+ unless opts.key?(:default)
29
+ opts = opts.merge(default: parse_default(story_name, param))
30
+ end
31
+ [param, build_control(param, **opts.except(:param, :only, :except))]
32
+ end.compact.to_h.values
33
+ end
34
+
35
+ private
36
+
37
+ def parse_default(story_name, param)
38
+ code_method = code_object.meths.find { |m| m.name == story_name }
39
+ default_value_parts = code_method.parameters.find { |parts| parts[0].chomp(":") == param.to_s }
40
+ return unless default_value_parts
41
+
42
+ code_method.instance_eval(default_value_parts[1])
43
+ end
44
+
45
+ def build_control(param, as:, **opts)
46
+ case as
47
+ when :text
48
+ Controls::Text.new(param, **opts)
49
+ when :boolean
50
+ Controls::Boolean.new(param, **opts)
51
+ when :number
52
+ Controls::Number.new(param, type: :number, **opts)
53
+ when :range
54
+ Controls::Number.new(param, type: :range, **opts)
55
+ when :color
56
+ Controls::Color.new(param, **opts)
57
+ when :object, :array
58
+ Controls::Object.new(param, **opts)
59
+ when :select
60
+ Controls::Options.new(param, type: :select, **opts)
61
+ when :multi_select
62
+ Controls::MultiOptions.new(param, type: :'multi-select', **opts)
63
+ when :radio
64
+ Controls::Options.new(param, type: :radio, **opts)
65
+ when :inline_radio
66
+ Controls::Options.new(param, type: :'inline-radio', **opts)
67
+ when :check
68
+ Controls::MultiOptions.new(param, type: :check, **opts)
69
+ when :inline_check
70
+ Controls::MultiOptions.new(param, type: :'inline-check', **opts)
71
+ when :date
72
+ Controls::Date.new(param, **opts)
73
+ else
74
+ raise "Unknonwn control type '#{as}'"
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponent
4
+ module Storybook
5
+ module Collections
6
+ class LayoutCollection
7
+ include Collections::ValidForStoryConcern
8
+
9
+ def initialize
10
+ @default = nil
11
+ @layouts = []
12
+ end
13
+
14
+ def add(layout, only: nil, except: nil)
15
+ if only.nil? && except.nil?
16
+ @default = layout
17
+ else
18
+ layouts << { layout: layout, only: only, except: except }
19
+ end
20
+ end
21
+
22
+ # Parameters set for the story method
23
+ def for_story(story_name)
24
+ story_layout = default
25
+ layouts.each do |opts|
26
+ story_layout = opts[:layout] if valid_for_story?(story_name, **opts.slice(:only, :except))
27
+ end
28
+ story_layout
29
+ end
30
+
31
+ private
32
+
33
+ attr_reader :default, :layouts
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponent
4
+ module Storybook
5
+ module Collections
6
+ class ParametersCollection
7
+ include Collections::ValidForStoryConcern
8
+
9
+ def initialize
10
+ @all_paramsters = {}
11
+ @parameters = []
12
+ end
13
+
14
+ def add(params, only: nil, except: nil)
15
+ if only.nil? && except.nil?
16
+ all_paramsters.merge!(params)
17
+ else
18
+ parameters << { params: params, only: only, except: except }
19
+ end
20
+ end
21
+
22
+ # Parameters set for all stories
23
+ def for_all
24
+ all_paramsters
25
+ end
26
+
27
+ # Parameters set for the story method
28
+ def for_story(story_name)
29
+ parameters.each_with_object({}) do |opts, accum|
30
+ accum.merge!(opts[:params]) if valid_for_story?(story_name, **opts.slice(:only, :except))
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ attr_reader :all_paramsters, :parameters
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponent
4
+ module Storybook
5
+ module Collections
6
+ class StoriesCollection
7
+ include Enumerable
8
+
9
+ delegate_missing_to :stories
10
+
11
+ attr_reader :stories
12
+
13
+ def load(code_objects)
14
+ @stories = Array(code_objects).map { |obj| StoriesCollection.stories_from_code_object(obj) }.compact
15
+ end
16
+
17
+ def self.stories_from_code_object(code_object)
18
+ klass = code_object.path.constantize
19
+ klass.code_object = code_object
20
+ klass
21
+ end
22
+
23
+ def self.stories_class?(klass)
24
+ return unless klass.ancestors.include?(ViewComponent::Storybook::Stories)
25
+
26
+ !klass.respond_to?(:abstract_class) || klass.abstract_class != true
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponent
4
+ module Storybook
5
+ module Collections
6
+ module ValidForStoryConcern
7
+ extend ActiveSupport::Concern
8
+
9
+ def valid_for_story?(story_name, only:, except:)
10
+ (only.nil? || Array.wrap(only).include?(story_name)) && Array.wrap(except).exclude?(story_name)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/dependencies/autoload"
4
+
5
+ module ViewComponent
6
+ module Storybook
7
+ module Collections
8
+ extend ActiveSupport::Autoload
9
+
10
+ autoload :ValidForStoryConcern
11
+ autoload :StoriesCollection
12
+ autoload :ControlsCollection
13
+ autoload :ParametersCollection
14
+ autoload :LayoutCollection
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponent
4
+ module Storybook
5
+ module Controls
6
+ class BaseOptions < SimpleControl
7
+ attr_reader :type, :options, :labels
8
+
9
+ validates :type, :options, presence: true
10
+
11
+ def initialize(param, type:, options:, default: nil, labels: nil, name: nil, description: nil, **opts)
12
+ super(param, default: default, name: name, description: description, **opts)
13
+ @type = type
14
+ @options = options
15
+ @labels = labels
16
+ end
17
+
18
+ def to_csf_params
19
+ super.deep_merge(argTypes: { param => { options: options } })
20
+ end
21
+
22
+ private
23
+
24
+ def csf_control_params
25
+ labels.nil? ? super : super.merge(labels: labels)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponent
4
+ module Storybook
5
+ module Controls
6
+ class Boolean < SimpleControl
7
+ BOOLEAN_VALUES = [true, false].freeze
8
+
9
+ validates :default, inclusion: { in: BOOLEAN_VALUES }, unless: -> { default.nil? }
10
+
11
+ def type
12
+ :boolean
13
+ end
14
+
15
+ def parse_param_value(value)
16
+ if value.is_a?(String) && value.present?
17
+ case value
18
+ when "true"
19
+ true
20
+ when "false"
21
+ false
22
+ end
23
+ else
24
+ value
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponent
4
+ module Storybook
5
+ module Controls
6
+ class Color < SimpleControl
7
+ attr_reader :preset_colors
8
+
9
+ def initialize(param, default: nil, preset_colors: nil, name: nil, description: nil, **opts)
10
+ super(param, default: default, name: name, description: description, **opts)
11
+ @preset_colors = preset_colors
12
+ end
13
+
14
+ def type
15
+ :color
16
+ end
17
+
18
+ private
19
+
20
+ def csf_control_params
21
+ super.merge(presetColors: preset_colors).compact
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponent
4
+ module Storybook
5
+ module Controls
6
+ class Control
7
+ include ActiveModel::Validations
8
+
9
+ validates :param, presence: true
10
+
11
+ attr_reader :param, :name, :description, :default
12
+
13
+ def initialize(param, default:, name: nil, description: nil)
14
+ @param = param
15
+ @default = default
16
+ @name = name || param.to_s.humanize.titlecase
17
+ @description = description
18
+ end
19
+
20
+ def to_csf_params
21
+ # :nocov:
22
+ raise NotImplementedError
23
+ # :nocov:
24
+ end
25
+
26
+ def parse_param_value(value)
27
+ # :nocov:
28
+ raise NotImplementedError
29
+ # :nocov:
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponent
4
+ module Storybook
5
+ module Controls
6
+ class Date < SimpleControl
7
+ def type
8
+ :date
9
+ end
10
+
11
+ def parse_param_value(value)
12
+ if value.is_a?(String)
13
+ DateTime.iso8601(value)
14
+ else
15
+ value
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def csf_value
22
+ case default
23
+ when ::Date
24
+ default.in_time_zone
25
+ when Time
26
+ default.iso8601
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponent
4
+ module Storybook
5
+ module Controls
6
+ class MultiOptions < BaseOptions
7
+ TYPES = %i[multi-select check inline-check].freeze
8
+
9
+ validates :type, inclusion: { in: TYPES }, unless: -> { type.nil? }
10
+ validate :validate_default, unless: -> { options.nil? || default.nil? }
11
+
12
+ def initialize(param, type:, options:, default: nil, labels: nil, name: nil, description: nil, **opts)
13
+ super(param, type: type, options: options, default: Array.wrap(default), labels: labels, name: name, description: description, **opts)
14
+ end
15
+
16
+ def parse_param_value(value)
17
+ if value.is_a?(String)
18
+ value = value.split(',')
19
+ value = value.map(&:to_sym) if symbol_values
20
+ end
21
+ value
22
+ end
23
+
24
+ def to_csf_params
25
+ super.deep_merge(argTypes: { param => { options: options } })
26
+ end
27
+
28
+ private
29
+
30
+ def csf_control_params
31
+ labels.nil? ? super : super.merge(labels: labels)
32
+ end
33
+
34
+ def symbol_values
35
+ options.first.is_a?(Symbol)
36
+ end
37
+
38
+ def validate_default
39
+ errors.add(:default, :inclusion) unless default.to_set <= options.to_set
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponent
4
+ module Storybook
5
+ module Controls
6
+ class Number < SimpleControl
7
+ TYPES = %i[number range].freeze
8
+
9
+ attr_reader :type, :min, :max, :step
10
+
11
+ validates :type, presence: true
12
+ validates :type, inclusion: { in: TYPES }, unless: -> { type.nil? }
13
+
14
+ def initialize(param, type:, default: nil, min: nil, max: nil, step: nil, name: nil, description: nil, **opts)
15
+ super(param, default: default, name: name, description: description, **opts)
16
+ @type = type
17
+ @min = min
18
+ @max = max
19
+ @step = step
20
+ end
21
+
22
+ def parse_param_value(value)
23
+ if value.is_a?(String) && value.present?
24
+ (value.to_f % 1) > 0 ? value.to_f : value.to_i
25
+ else
26
+ value
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def csf_control_params
33
+ super.merge(min: min, max: max, step: step).compact
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponent
4
+ module Storybook
5
+ module Controls
6
+ class Object < SimpleControl
7
+ def type
8
+ :object
9
+ end
10
+
11
+ def parse_param_value(value)
12
+ if value.is_a?(String)
13
+ parsed_json = JSON.parse(value)
14
+ if parsed_json.is_a?(Array)
15
+ parsed_json.map do |item|
16
+ item.is_a?(Hash) ? item.deep_symbolize_keys : item
17
+ end
18
+ else
19
+ parsed_json.deep_symbolize_keys
20
+ end
21
+ else
22
+ value
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponent
4
+ module Storybook
5
+ module Controls
6
+ class Options < BaseOptions
7
+ TYPES = %i[select radio inline-radio].freeze
8
+
9
+ validates :type, inclusion: { in: TYPES }, unless: -> { type.nil? }
10
+ validates :default, inclusion: { in: ->(config) { config.options } }, unless: -> { options.nil? || default.nil? }
11
+
12
+ def parse_param_value(value)
13
+ if value.is_a?(String) && symbol_value
14
+ value.to_sym
15
+ else
16
+ value
17
+ end
18
+ end
19
+
20
+ def to_csf_params
21
+ super.deep_merge(argTypes: { param => { options: options } })
22
+ end
23
+
24
+ private
25
+
26
+ def csf_control_params
27
+ labels.nil? ? super : super.merge(labels: labels)
28
+ end
29
+
30
+ def symbol_value
31
+ @symbol_value ||= default.is_a?(Symbol)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end