lookbook 1.5.5 → 2.0.0.beta.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (198) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +13 -31
  3. data/app/assets/lookbook/css/lookbook.css +9 -0
  4. data/app/assets/lookbook/css/themes/blue.css +7 -0
  5. data/app/assets/lookbook/css/themes/green.css +7 -0
  6. data/app/assets/lookbook/css/themes/indigo.css +7 -0
  7. data/app/assets/lookbook/css/themes/rose.css +7 -0
  8. data/app/assets/lookbook/css/themes/zinc.css +7 -0
  9. data/app/assets/lookbook/css/tooltip.css +9 -6
  10. data/app/assets/lookbook/img/lucide-sprite.svg +4960 -0
  11. data/app/assets/lookbook/js/app.js +22 -4
  12. data/app/assets/lookbook/js/helpers/dom.js +4 -7
  13. data/app/assets/lookbook/js/helpers/string.js +4 -11
  14. data/app/assets/lookbook/js/index.js +61 -0
  15. data/app/assets/lookbook/js/lib/lookbook.js +113 -0
  16. data/app/assets/lookbook/js/lib/tippy.js +1 -0
  17. data/app/assets/lookbook/js/lookbook-core.js +1 -0
  18. data/app/assets/lookbook/js/lookbook.js +2 -61
  19. data/app/components/lookbook/base_component.rb +3 -1
  20. data/app/components/lookbook/button/component.html.erb +13 -24
  21. data/app/components/lookbook/button/component.js +13 -3
  22. data/app/components/lookbook/button/component.rb +16 -25
  23. data/app/components/lookbook/code/component.rb +0 -2
  24. data/app/components/lookbook/copy_button/component.html.erb +5 -5
  25. data/app/components/lookbook/copy_button/component.rb +6 -3
  26. data/app/components/lookbook/debug_menu/component.html.erb +1 -0
  27. data/app/components/lookbook/debug_menu/component.rb +12 -1
  28. data/app/components/lookbook/display_options/editor/component.html.erb +1 -1
  29. data/app/components/lookbook/display_options/field/component.css +0 -26
  30. data/app/components/lookbook/display_options/field/component.html.erb +1 -1
  31. data/app/components/lookbook/embed/component.html.erb +6 -51
  32. data/app/components/lookbook/embed/component.rb +17 -16
  33. data/app/components/lookbook/embed/inspector/component.html.erb +102 -0
  34. data/app/components/lookbook/embed/inspector/component.js +46 -0
  35. data/app/components/lookbook/embed/inspector/component.rb +64 -0
  36. data/app/components/lookbook/embed_code_dropdown/component.css +12 -0
  37. data/app/components/lookbook/embed_code_dropdown/component.html.erb +19 -0
  38. data/app/components/lookbook/embed_code_dropdown/component.js +26 -0
  39. data/app/components/lookbook/embed_code_dropdown/component.rb +41 -0
  40. data/app/components/lookbook/header/component.html.erb +7 -6
  41. data/app/components/lookbook/header/component.rb +5 -1
  42. data/app/components/lookbook/icon/component.html.erb +1 -1
  43. data/app/components/lookbook/icon_button/component.html.erb +20 -0
  44. data/app/components/lookbook/icon_button/component.rb +46 -0
  45. data/app/components/lookbook/nav/component.html.erb +0 -1
  46. data/app/components/lookbook/nav/entity/component.rb +2 -2
  47. data/app/components/lookbook/nav/item/component.rb +1 -1
  48. data/app/components/lookbook/page_tabs/component.html.erb +2 -2
  49. data/app/components/lookbook/params/editor/component.rb +1 -0
  50. data/app/components/lookbook/prose/component.rb +1 -3
  51. data/app/components/lookbook/tabs/component.html.erb +2 -2
  52. data/app/components/lookbook/tabs/component.js +1 -1
  53. data/app/components/lookbook/tag_component.rb +2 -1
  54. data/app/components/lookbook/text_button/component.html.erb +26 -0
  55. data/app/components/lookbook/text_button/component.rb +42 -0
  56. data/app/components/lookbook/toolbar/component.html.erb +1 -1
  57. data/app/components/lookbook/viewport/component.rb +0 -4
  58. data/app/controllers/concerns/lookbook/targetable_concern.rb +26 -22
  59. data/app/controllers/concerns/lookbook/with_panels_concern.rb +30 -0
  60. data/app/controllers/concerns/lookbook/with_preview_controller_concern.rb +4 -3
  61. data/app/controllers/lookbook/application_controller.rb +9 -12
  62. data/app/controllers/lookbook/embeds_controller.rb +148 -0
  63. data/app/controllers/lookbook/inspector_controller.rb +3 -22
  64. data/app/controllers/lookbook/page_controller.rb +7 -6
  65. data/app/controllers/lookbook/pages_controller.rb +3 -4
  66. data/app/controllers/lookbook/preview_controller.rb +17 -0
  67. data/app/controllers/lookbook/previews_controller.rb +10 -30
  68. data/app/helpers/lookbook/application_helper.rb +3 -19
  69. data/app/views/layouts/lookbook/application.html.erb +85 -60
  70. data/app/views/layouts/lookbook/embed.html.erb +16 -0
  71. data/app/views/layouts/lookbook/shell.html.erb +1 -1
  72. data/app/views/layouts/lookbook/skeleton.html.erb +13 -8
  73. data/app/views/lookbook/404.html.erb +2 -2
  74. data/app/views/lookbook/embeds/show.html.erb +12 -0
  75. data/app/views/lookbook/inspector/panels/_notes.html.erb +1 -1
  76. data/app/views/lookbook/inspector/panels/_output.html.erb +3 -3
  77. data/app/views/lookbook/inspector/panels/_source.html.erb +6 -6
  78. data/app/views/lookbook/inspector/show.html.erb +130 -123
  79. data/app/views/lookbook/pages/show.html.erb +81 -34
  80. data/app/views/lookbook/partials/_iframe_content_scripts.html.erb +1 -0
  81. data/app/views/lookbook/partials/_user_styles.html.erb +5 -0
  82. data/app/views/lookbook/previews/group.html.erb +14 -0
  83. data/app/views/lookbook/previews/preview.html.erb +5 -0
  84. data/app/views/lookbook/previews/show.html.erb +1 -0
  85. data/config/app.yml +31 -16
  86. data/config/panels.yml +23 -25
  87. data/config/routes.rb +3 -1
  88. data/config/tags.yml +6 -2
  89. data/lib/lookbook/cable/cable.rb +53 -0
  90. data/lib/lookbook/engine.rb +105 -93
  91. data/lib/lookbook/entities/collections/entity_collection.rb +11 -6
  92. data/lib/lookbook/entities/collections/page_collection.rb +33 -8
  93. data/lib/lookbook/entities/collections/preview_collection.rb +42 -17
  94. data/lib/lookbook/entities/collections/render_target_collection.rb +4 -0
  95. data/lib/lookbook/entities/collections/scenario_collection.rb +4 -0
  96. data/lib/lookbook/entities/concerns/{annotatable.rb → annotatable_entity.rb} +7 -6
  97. data/lib/lookbook/entities/concerns/{inspectable.rb → inspectable_entity.rb} +2 -1
  98. data/lib/lookbook/entities/concerns/{locatable.rb → locatable_entity.rb} +8 -14
  99. data/lib/lookbook/entities/concerns/navigable_entity.rb +44 -0
  100. data/lib/lookbook/entities/entity.rb +7 -2
  101. data/lib/lookbook/entities/{page.rb → page_entity.rb} +10 -6
  102. data/lib/lookbook/entities/{page_section.rb → page_section_entity.rb} +1 -1
  103. data/lib/lookbook/entities/preview_entity.rb +99 -0
  104. data/lib/lookbook/entities/renderable_entity.rb +50 -0
  105. data/lib/lookbook/entities/rendered_scenario_entity.rb +53 -0
  106. data/lib/lookbook/entities/scenario_entity.rb +112 -0
  107. data/lib/lookbook/entities/scenario_group_entity.rb +53 -0
  108. data/lib/lookbook/error.rb +5 -5
  109. data/lib/lookbook/file_watcher.rb +19 -35
  110. data/lib/lookbook/helpers/class_names_helper.rb +28 -0
  111. data/lib/lookbook/helpers/page_helper.rb +18 -0
  112. data/{app/helpers/lookbook → lib/lookbook/helpers}/preview_helper.rb +3 -0
  113. data/lib/lookbook/helpers/ui_elements_helper.rb +115 -0
  114. data/lib/lookbook/preview.rb +79 -0
  115. data/lib/lookbook/preview_controller_actions.rb +50 -0
  116. data/lib/lookbook/preview_parser.rb +4 -2
  117. data/lib/lookbook/reloaders.rb +71 -0
  118. data/lib/lookbook/runtime_context.rb +49 -0
  119. data/lib/lookbook/services/data/resolvers/data_resolver.rb +4 -6
  120. data/lib/lookbook/services/entities/entity_tree_builder.rb +6 -6
  121. data/lib/lookbook/services/list_resolver.rb +35 -0
  122. data/lib/lookbook/services/markdown_renderer.rb +12 -2
  123. data/lib/lookbook/services/priority_prefix_parser.rb +16 -0
  124. data/lib/lookbook/services/urls/search_param_encoder.rb +16 -0
  125. data/lib/lookbook/services/urls/search_param_parser.rb +7 -6
  126. data/lib/lookbook/stores/config_store.rb +16 -16
  127. data/lib/lookbook/stores/input_store.rb +1 -3
  128. data/lib/lookbook/stores/panel_store.rb +28 -50
  129. data/lib/lookbook/support/deprecation.rb +5 -0
  130. data/lib/lookbook/support/errors/preview_template_error.rb +7 -0
  131. data/lib/lookbook/support/evented_file_update_checker.rb +69 -0
  132. data/lib/lookbook/support/null_websocket.rb +9 -0
  133. data/lib/lookbook/support/store.rb +9 -0
  134. data/lib/lookbook/support/tree_node.rb +7 -7
  135. data/lib/lookbook/support/utils/path_utils.rb +7 -1
  136. data/lib/lookbook/support/utils/utils.rb +8 -0
  137. data/lib/lookbook/tags/{position_tag.rb → priority_tag.rb} +4 -4
  138. data/lib/lookbook/tags/renders_tag.rb +4 -0
  139. data/lib/lookbook/tags/tag_provider.rb +3 -0
  140. data/lib/lookbook/tags/type_tag.rb +7 -0
  141. data/lib/lookbook/tags/yard_tag.rb +1 -2
  142. data/lib/lookbook/version.rb +1 -1
  143. data/lib/lookbook/websocket.rb +6 -53
  144. data/lib/lookbook.rb +179 -53
  145. data/public/lookbook-assets/css/lookbook.css +432 -376
  146. data/public/lookbook-assets/css/lookbook.css.map +1 -1
  147. data/public/lookbook-assets/css/themes/blue.css +7 -0
  148. data/public/lookbook-assets/css/themes/blue.css.map +1 -1
  149. data/public/lookbook-assets/css/themes/green.css +7 -0
  150. data/public/lookbook-assets/css/themes/green.css.map +1 -1
  151. data/public/lookbook-assets/css/themes/indigo.css +7 -0
  152. data/public/lookbook-assets/css/themes/indigo.css.map +1 -1
  153. data/public/lookbook-assets/css/themes/rose.css +7 -0
  154. data/public/lookbook-assets/css/themes/rose.css.map +1 -1
  155. data/public/lookbook-assets/css/themes/zinc.css +7 -0
  156. data/public/lookbook-assets/css/themes/zinc.css.map +1 -1
  157. data/public/lookbook-assets/img/lucide-sprite.svg +4960 -0
  158. data/public/lookbook-assets/js/embed.js +1363 -843
  159. data/public/lookbook-assets/js/embed.js.map +1 -1
  160. data/public/lookbook-assets/js/iframe.js +906 -0
  161. data/public/lookbook-assets/js/iframe.js.map +1 -0
  162. data/public/lookbook-assets/js/index.js +13567 -0
  163. data/public/lookbook-assets/js/index.js.map +1 -0
  164. data/public/lookbook-assets/js/lookbook-core.js +85 -0
  165. data/public/lookbook-assets/js/lookbook-core.js.map +1 -0
  166. data/public/lookbook-assets/js/lookbook.js +151 -12726
  167. data/public/lookbook-assets/js/lookbook.js.map +1 -1
  168. data/public/lookbook-assets/lookbook-esm.js +1427 -0
  169. data/public/lookbook-assets/lookbook-esm.js.map +1 -0
  170. data/public/lookbook-assets/lookbook-global.js +1427 -0
  171. data/public/lookbook-assets/lookbook-global.js.map +1 -0
  172. data/public/lookbook-assets/lookbook.js +1427 -0
  173. data/public/lookbook-assets/lookbook.js.map +1 -0
  174. metadata +85 -77
  175. data/app/components/lookbook/embed/component.js +0 -39
  176. data/app/helpers/lookbook/component_helper.rb +0 -84
  177. data/app/helpers/lookbook/output_helper.rb +0 -19
  178. data/app/helpers/lookbook/page_helper.rb +0 -34
  179. data/app/views/layouts/lookbook/inspector.html.erb +0 -7
  180. data/app/views/layouts/lookbook/page.html.erb +0 -53
  181. data/app/views/layouts/lookbook/standalone.html.erb +0 -5
  182. data/app/views/lookbook/preview.html.erb +0 -14
  183. data/lib/lookbook/entities/collections/component_collection.rb +0 -4
  184. data/lib/lookbook/entities/collections/preview_example_collection.rb +0 -4
  185. data/lib/lookbook/entities/component.rb +0 -31
  186. data/lib/lookbook/entities/concerns/navigable.rb +0 -43
  187. data/lib/lookbook/entities/preview.rb +0 -87
  188. data/lib/lookbook/entities/preview_example.rb +0 -104
  189. data/lib/lookbook/entities/preview_group.rb +0 -52
  190. data/lib/lookbook/preview_actions.rb +0 -43
  191. data/lib/lookbook/process.rb +0 -21
  192. data/lib/lookbook/rendered_example.rb +0 -37
  193. data/lib/lookbook/services/position_prefix_parser.rb +0 -16
  194. data/lib/lookbook/services/urls/search_param_builder.rb +0 -13
  195. data/lib/lookbook/tags/component_tag.rb +0 -13
  196. /data/app/assets/lookbook/js/{embed.js → iframe.js} +0 -0
  197. /data/{app/channels/lookbook → lib/lookbook/cable}/connection.rb +0 -0
  198. /data/{app/channels/lookbook → lib/lookbook/cable}/reload_channel.rb +0 -0
@@ -0,0 +1,112 @@
1
+ module Lookbook
2
+ # Represents a preview scenario method within a preview class
3
+ #
4
+ # @ignore methods
5
+ # @api public
6
+ class ScenarioEntity < Entity
7
+ include InspectableEntity
8
+ include AnnotatableEntity
9
+ include NavigableEntity
10
+
11
+ delegate :group, to: :code_object
12
+
13
+ attr_reader :preview
14
+
15
+ def initialize(code_object, preview, priority: nil)
16
+ @code_object = code_object
17
+ @preview = preview
18
+ @default_priority = priority
19
+ @lookup_path = "#{parent.lookup_path}/#{name}"
20
+ end
21
+
22
+ def id
23
+ @_id ||= Utils.id(fetch_config(:id) { "#{parent.id}-#{code_object.name}" })
24
+ end
25
+
26
+ def name
27
+ @_name ||= Utils.name(code_object.name)
28
+ end
29
+
30
+ def display_options
31
+ parent.display_options.merge(fetch_config(:display_options, {}))
32
+ end
33
+
34
+ def render_targets
35
+ @_render_targets ||= RenderTargetCollection.new(load_render_targets)
36
+ end
37
+
38
+ def render_target
39
+ render_targets.first
40
+ end
41
+
42
+ def scenarios
43
+ [self]
44
+ end
45
+
46
+ def template_source(template_path)
47
+ source_path = template_file_path(template_path)
48
+ source_path ? File.read(source_path) : nil
49
+ end
50
+
51
+ def template_lang(template_path)
52
+ path = template_file_path(template_path)
53
+ Lookbook::Lang.guess(path) || Lookbook::Lang.find(:html)
54
+ end
55
+
56
+ def search_terms
57
+ [parent.label, label]
58
+ end
59
+
60
+ def url_path
61
+ lookbook_inspect_path(lookup_path)
62
+ end
63
+
64
+ def type
65
+ :scenario
66
+ end
67
+
68
+ alias_method :parent, :preview
69
+ alias_method :components, :render_targets
70
+ alias_method :component, :render_target
71
+ alias_method :lang, :source_lang
72
+ alias_method :examples, :scenarios
73
+
74
+ deprecate lang: :source_lang, deprecator: Deprecation
75
+ deprecate examples: :scenarios, deprecator: Deprecation
76
+
77
+ protected
78
+
79
+ def sort_handler(other_entity)
80
+ if Lookbook.config.preview_sort_scenarios
81
+ label <=> other_entity.label
82
+ else
83
+ [priority, label] <=> [other_entity.priority, other_entity.label]
84
+ end
85
+ end
86
+
87
+ def format_source(source)
88
+ source.sub(/^def \w+\s?(\([^)]+\))?/m, "").split("\n")[0..-2].join("\n")
89
+ end
90
+
91
+ def template_file_path(template_path)
92
+ return full_template_path(template_path) if respond_to?(:full_template_path, true)
93
+
94
+ search_dirs = [*Engine.preview_paths, *Engine.view_paths]
95
+ template_path = "#{template_path.to_s.sub(/\..*$/, "")}.html.*"
96
+ PathUtils.determine_full_path(template_path, search_dirs)
97
+ end
98
+
99
+ def load_render_targets
100
+ render_target_identifiers = [*fetch_config(:renders, []), *preview.send(:fetch_config, :renders, [])]
101
+ render_target_identifiers = preview.guess_render_targets if render_target_identifiers.empty?
102
+
103
+ render_targets = render_target_identifiers.map do |render_target|
104
+ RenderableEntity.new(render_target)
105
+ rescue NameError
106
+ Lookbook.logger.warn "#{render_target} was not found"
107
+ nil
108
+ end
109
+ render_targets.compact.uniq(&:lookup_path)
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,53 @@
1
+ module Lookbook
2
+ # Represents a group of preview scenarios within a preview class
3
+ #
4
+ # @ignore methods
5
+ # @api public
6
+ class ScenarioGroupEntity < Entity
7
+ include NavigableEntity
8
+
9
+ attr_reader :scenarios, :preview
10
+
11
+ def initialize(name, scenarios, preview)
12
+ @name = Utils.name(name)
13
+ @scenarios = ScenarioCollection.new(scenarios)
14
+ @preview = preview
15
+ @lookup_path = "#{parent.lookup_path}/#{@name}"
16
+ end
17
+
18
+ def display_options
19
+ merged = {}
20
+ scenarios.to_a.reverse.map do |scenario|
21
+ merged.merge!(scenario.display_options)
22
+ end
23
+ merged
24
+ end
25
+
26
+ def render_targets
27
+ @_render_targets ||= RenderTargetCollection.new(scenarios.flat_map(&:render_targets).uniq(&:path))
28
+ end
29
+
30
+ def search_terms
31
+ [parent.label, label]
32
+ end
33
+
34
+ def tags(tag_name = nil)
35
+ scenarios.flat_map { |scenario| scenario.tags(tag_name) }
36
+ end
37
+
38
+ def tag(tag_name = nil)
39
+ tags(tag_name).first
40
+ end
41
+
42
+ def url_path
43
+ lookbook_inspect_path(lookup_path)
44
+ end
45
+
46
+ def type
47
+ :group
48
+ end
49
+
50
+ alias_method :parent, :preview
51
+ alias_method :components, :render_targets
52
+ end
53
+ end
@@ -1,6 +1,6 @@
1
1
  module Lookbook
2
2
  class Error < StandardError
3
- delegate :full_message, :backtrace, :to_s, to: :target
3
+ delegate :full_message, :backtrace, :to_s, to: :original
4
4
 
5
5
  LINES_AROUND = 3
6
6
 
@@ -59,11 +59,11 @@ module Lookbook
59
59
  end
60
60
 
61
61
  def title
62
- @title || target.class.to_s
62
+ @title || original.class.to_s
63
63
  end
64
64
 
65
65
  def message
66
- (@message || target.message).gsub("(<unknown>):", "").strip.upcase_first
66
+ (@message || original.message).gsub("(<unknown>):", "").strip.upcase_first
67
67
  end
68
68
 
69
69
  def file_name
@@ -80,7 +80,7 @@ module Lookbook
80
80
  else
81
81
  @file_path.presence || nil
82
82
  end
83
- path&.to_s&.delete_prefix("#{Rails.root}/")
83
+ path.nil? ? nil : path.to_s.delete_prefix("#{Rails.root}/")
84
84
  end
85
85
 
86
86
  def line_number
@@ -101,7 +101,7 @@ module Lookbook
101
101
 
102
102
  protected
103
103
 
104
- def target
104
+ def original
105
105
  @original.presence || self
106
106
  end
107
107
 
@@ -1,46 +1,30 @@
1
1
  module Lookbook
2
2
  class FileWatcher
3
- attr_reader :listeners, :force_polling
4
-
5
- def initialize(force_polling: false)
6
- @force_polling = force_polling
7
- @listeners = []
8
- end
9
-
10
- def watch(paths, extensions = ".*", opts = nil, &block)
11
- paths = PathUtils.normalize_paths(paths)
12
-
13
- if paths.any?
14
- opts = opts.to_h
15
- opts[:only] = /\.(#{Array(extensions).join("|")})$/
16
-
17
- listeners << init_listener(paths, opts, &block)
3
+ class << self
4
+ def new(...)
5
+ if evented?
6
+ Lookbook.logger.debug "Using `EventedFileUpdateChecker` for file watching"
7
+ else
8
+ Lookbook.logger.debug "The 'listen' gem was not found. Using `FileUpdateChecker` for file watching"
9
+ end
10
+
11
+ file_watcher.new(...)
18
12
  end
19
- end
20
13
 
21
- def start
22
- if listeners.any?
23
- Lookbook.logger.debug "Starting listeners"
24
- listeners.each { |l| l.start }
14
+ def evented?
15
+ !(file_watcher <= ActiveSupport::FileUpdateChecker)
25
16
  end
26
- end
27
17
 
28
- def stop
29
- if listeners.any?
30
- Lookbook.logger.debug "Stopping listeners"
31
- listeners.each { |l| l.stop }
32
- end
33
- end
18
+ protected
34
19
 
35
- protected
20
+ def file_watcher
21
+ @_file_watcher ||= begin
22
+ require_relative "./support/evented_file_update_checker"
36
23
 
37
- def init_listener(paths, opts, &block)
38
- Listen.to(
39
- *paths,
40
- **opts,
41
- force_polling: force_polling
42
- ) do |modified, added, removed|
43
- block.call({modified: modified, added: added, removed: removed})
24
+ EventedFileUpdateChecker
25
+ rescue LoadError
26
+ ActiveSupport::FileUpdateChecker
27
+ end
44
28
  end
45
29
  end
46
30
  end
@@ -0,0 +1,28 @@
1
+ module Lookbook
2
+ module ClassNamesHelper
3
+ def build_tag_values(*args)
4
+ tag_values = []
5
+
6
+ args.each do |tag_value|
7
+ case tag_value
8
+ when Hash
9
+ tag_value.each do |key, val|
10
+ tag_values << key.to_s if val && key.present?
11
+ end
12
+ when Array
13
+ tag_values.concat build_tag_values(*tag_value)
14
+ else
15
+ tag_values << tag_value.to_s if tag_value.present?
16
+ end
17
+ end
18
+
19
+ tag_values
20
+ end
21
+
22
+ def class_names(*args)
23
+ tokens = build_tag_values(*args).flat_map { |value| value.to_s.split(/\s+/) }.uniq
24
+
25
+ safe_join(tokens, " ")
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,18 @@
1
+ module Lookbook
2
+ # Helpers for documentation page templates
3
+ #
4
+ # @api public
5
+ module PageHelper
6
+ # Returns the URL path to a page.
7
+ #
8
+ # @param id [String, PageEntity] The id or PageEntity instance to generate a URL path for
9
+ def page_path(id)
10
+ page = id.is_a?(PageEntity) ? id : Engine.pages.find_by_id(id)
11
+ if page.present?
12
+ lookbook_page_path page.lookup_path
13
+ else
14
+ Lookbook.logger.warn "Could not find page with id ':#{id}'"
15
+ end
16
+ end
17
+ end
18
+ end
@@ -1,4 +1,7 @@
1
1
  module Lookbook
2
+ # Set of helpers for preview layouts
3
+ #
4
+ # @api public
2
5
  module PreviewHelper
3
6
  def lookbook_display(key, fallback = nil)
4
7
  params.dig(:lookbook, :display, key.to_sym) || fallback
@@ -0,0 +1,115 @@
1
+ module Lookbook
2
+ # Helpers for rendering UI elements.
3
+ #
4
+ # These are available for use in documentation page templates
5
+ # and custom preview inspector panel templates.
6
+ #
7
+ # @api public
8
+ module UiElementsHelper
9
+ # Render an icon.
10
+ #
11
+ # Lookbook uses icons from the [Lucide Icons](https://lucide.dev/) set and
12
+ # a full list of available icon names can be found on that site.
13
+ #
14
+ # @example
15
+ # <%= icon :trash %>
16
+ # <%= icon :camera, size: 6, style: "color: red;" %>
17
+ #
18
+ # @param name [Symbol, String] Name of the icon
19
+ # @param opts [Hash] Options hash
20
+ def icon(name, **opts)
21
+ lookbook_render :icon, name: name, **opts
22
+ end
23
+
24
+ # Display a syntax-highlighted block of code.
25
+ #
26
+ # An alternative to using markdown code blocks for templates that have
27
+ # markdown parsing disabled, or for when more control is required.
28
+ #
29
+ # @param language [Symbol] Which language the code is written in
30
+ # @param opts [Hash] Options hash
31
+ # @param block [Proc] Code block
32
+ def code(language = :html, **opts, &block)
33
+ opts[:language] ||= language
34
+ lookbook_render :code, **opts, &block
35
+ end
36
+
37
+ # Render a 'live' embed of a component preview.
38
+ #
39
+ # If no scenario name is provided then the default (first) preview
40
+ # scenario will be rendered in the embed.
41
+ #
42
+ # @param preview [String] Name of the preview class to embed
43
+ # @param scenario [String] Example method name
44
+ # @param opts [Hash] Options hash
45
+ def embed(preview, scenario = nil, **opts)
46
+ preview_entity = if preview.is_a?(Symbol)
47
+ Engine.previews.find_by_path(preview)
48
+ else
49
+ Engine.previews.find_by_preview_class(preview)
50
+ end
51
+ scenario_entity = scenario ? preview_entity&.scenario(scenario) : preview_entity&.default_scenario
52
+ opts[:actions] ||= ["inspect", "open"]
53
+
54
+ lookbook_render Embed::Component.new(
55
+ scenario: scenario_entity,
56
+ params: opts.fetch(:params, {}),
57
+ options: opts.except(:params)
58
+ )
59
+ end
60
+
61
+ # @api private
62
+ def prose(**opts, &block)
63
+ lookbook_render :prose, **opts, &block
64
+ end
65
+
66
+ # @api private
67
+ def lookbook_tag(tag = :div, **attrs, &block)
68
+ lookbook_render :tag, tag: tag, **attrs, &block
69
+ end
70
+
71
+ # @api private
72
+ def lookbook_render(ref, **attrs, &block)
73
+ comp = if ref.is_a? ViewComponent::Base
74
+ ref
75
+ else
76
+ klass = component_class(ref)
77
+ attrs.key?(:content) ? klass.new(**attrs.except(:content)).with_content(attrs[:content]) : klass.new(**attrs)
78
+ end
79
+ if block && !attrs.key?(:content)
80
+ public_send render_method_name, comp, &block
81
+ else
82
+ public_send render_method_name, comp
83
+ end
84
+ end
85
+
86
+ private
87
+
88
+ COMPONENT_CLASSES = {} # cache for constantized references
89
+
90
+ # @api private
91
+ def render_method_name
92
+ if Rails.application.config.view_component.render_monkey_patch_enabled || Rails.version.to_f >= 6.1
93
+ :render
94
+ else
95
+ :render_component
96
+ end
97
+ end
98
+
99
+ # @api private
100
+ def component_class(ref)
101
+ klass = COMPONENT_CLASSES[ref]
102
+ if klass.nil?
103
+ ref = ref.to_s.tr("-", "_")
104
+ class_namespace = ref.camelize
105
+ begin
106
+ klass = "Lookbook::#{class_namespace}::Component".constantize
107
+ rescue
108
+ klass = "Lookbook::#{class_namespace}Component".constantize
109
+ end
110
+ COMPONENT_CLASSES[ref] = klass
111
+ end
112
+ klass
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,79 @@
1
+ module Lookbook
2
+ class Preview
3
+ include ActionView::Helpers::TagHelper
4
+ include ActionView::Helpers::AssetTagHelper
5
+
6
+ def render(component = nil, **args, &block)
7
+ if component.nil?
8
+ {
9
+ type: :view,
10
+ template: args[:template] || Lookbook.config.preview_template,
11
+ args: args,
12
+ locals: args[:locals] || {},
13
+ block: block
14
+ }
15
+ else
16
+ {
17
+ type: component.is_a?(String) ? :view : :component,
18
+ args: args,
19
+ block: block,
20
+ component: component,
21
+ locals: {},
22
+ template: Lookbook.config.preview_template
23
+ }
24
+ end
25
+ end
26
+
27
+ def render_with_template(template: nil, locals: nil)
28
+ {
29
+ type: :template,
30
+ template: template,
31
+ locals: locals.to_h
32
+ }
33
+ end
34
+
35
+ alias_method :render_component, :render
36
+
37
+ class << self
38
+ # Returns the arguments for rendering of the component in its layout
39
+ def render_args(scenario, params: {})
40
+ scenario_params_names = instance_method(scenario).parameters.map(&:last)
41
+ provided_params = params.slice(*scenario_params_names).to_h.symbolize_keys
42
+ result = provided_params.empty? ? new.public_send(scenario) : new.public_send(scenario, **provided_params)
43
+ result ||= {}
44
+ result[:template] = scenario_template_path(scenario) if result[:template].nil?
45
+ @layout = nil unless defined?(@layout)
46
+ result.merge(layout: @layout)
47
+ end
48
+
49
+ # rubocop:disable Style/TrivialAccessors
50
+ def layout(layout_name)
51
+ @layout = layout_name
52
+ end
53
+ # rubocop:enable Style/TrivialAccessors
54
+
55
+ # Returns the relative path (from preview_path) to the scenario template if the template exists
56
+ def scenario_template_path(scenario)
57
+ preview_name = name.chomp("Preview").underscore
58
+ preview_path =
59
+ Engine.preview_paths.detect do |path|
60
+ Dir["#{path}/#{preview_name}_preview/#{scenario}.html.*"].first
61
+ end
62
+
63
+ if preview_path.nil?
64
+ raise(
65
+ PreviewTemplateError,
66
+ "A preview template for scenario #{scenario} doesn't exist.\n\n" \
67
+ "To fix this issue, create a template for the scenario."
68
+ )
69
+ end
70
+
71
+ path = Dir["#{preview_path}/#{preview_name}_preview/#{scenario}.html.*"].first
72
+ Pathname.new(path)
73
+ .relative_path_from(Pathname.new(preview_path))
74
+ .to_s
75
+ .sub(/\..*$/, "")
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,50 @@
1
+ module Lookbook
2
+ module PreviewControllerActions
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ helper PreviewHelper
7
+ prepend_view_path Engine.root.join("app/views")
8
+ before_action :permit_embeds
9
+
10
+ def render_scenario_to_string(preview, scenario_name)
11
+ prepend_application_view_paths
12
+ prepend_preview_examples_view_path
13
+
14
+ @preview = preview
15
+ @scenario_name = scenario_name
16
+ @render_args = @preview.render_args(@scenario_name, params: params.permit!)
17
+ template = @render_args[:template]
18
+ locals = @render_args[:locals]
19
+ opts = {}
20
+ opts[:layout] = nil
21
+ opts[:locals] = locals if locals.present?
22
+
23
+ with_optional_action_view_annotations do
24
+ render html: render_to_string(template, **opts)
25
+ end
26
+ end
27
+
28
+ def render_in_layout_to_string(template, locals, opts = {})
29
+ with_optional_action_view_annotations do
30
+ html = render_to_string(template, locals: locals, **determine_layout(opts[:layout]))
31
+ if opts[:append_html].present?
32
+ html += opts[:append_html]
33
+ end
34
+ render html: html
35
+ end
36
+ end
37
+
38
+ protected
39
+
40
+ def with_optional_action_view_annotations(&block)
41
+ disable = Lookbook.config.preview_disable_action_view_annotations
42
+ ActionViewAnnotationsHandler.call(disable_annotations: disable, &block)
43
+ end
44
+
45
+ def permit_embeds
46
+ headers["X-Frame-Options"] = Lookbook.config.preview_embeds.policy
47
+ end
48
+ end
49
+ end
50
+ end
@@ -12,12 +12,14 @@ module Lookbook
12
12
  YARD::Parser::SourceParser.after_parse_list { run_callbacks }
13
13
  end
14
14
 
15
- def parse(&block)
15
+ def parse(files = nil, &block)
16
16
  unless @parsing
17
17
  @parsing = true
18
18
  @after_parse_once_callbacks << block if block
19
+ files_list = files ? files.select { |file| file.to_s.end_with?(".rb") } : paths
20
+
19
21
  YARD::Registry.clear
20
- YARD.parse(paths)
22
+ YARD.parse(files_list)
21
23
  end
22
24
  end
23
25
 
@@ -0,0 +1,71 @@
1
+ require "active_support"
2
+
3
+ module Lookbook
4
+ class Reloaders
5
+ attr_reader :reloaders
6
+
7
+ def initialize
8
+ @reloaders = []
9
+ end
10
+
11
+ def add(name, directories, extensions, &callback)
12
+ reloader = Reloader.new(name, directories, extensions, &callback)
13
+ reloaders.push(reloader)
14
+
15
+ if Engine.reloading?
16
+ Rails.application.reloaders << reloader
17
+ Rails.application.reloader.to_run { reloader.execute_if_updated }
18
+ end
19
+ end
20
+
21
+ def execute
22
+ reloaders.each { |reloader| reloader.execute }
23
+ end
24
+
25
+ def register_changes(changes)
26
+ reloader = reloaders.find { |reloader| reloader.watching?(changes) }
27
+ reloader.last_changes = changes if reloader
28
+ end
29
+
30
+ class Reloader
31
+ delegate :execute, :execute_if_updated, :updated?, to: :file_watcher
32
+
33
+ attr_reader :name, :directories, :extensions, :callback
34
+ attr_accessor :last_changes
35
+
36
+ def initialize(name, directories, extensions, &callback)
37
+ @name = name.to_sym
38
+ @directories = directories
39
+ @extensions = extensions
40
+ @callback = callback
41
+ @last_changes = nil
42
+ end
43
+
44
+ def watching?(changes)
45
+ file_paths = changes.to_h.values.flatten
46
+ !!file_paths.find do |file_path|
47
+ file_path = Pathname(file_path).expand_path
48
+ directories.find do |dir_path|
49
+ matcher = File.expand_path(File.join(dir_path, "**"))
50
+ file_path.fnmatch?(matcher)
51
+ end
52
+ end
53
+ end
54
+
55
+ protected
56
+
57
+ def file_watcher
58
+ return @_file_watcher if @_file_watcher
59
+
60
+ to_watch = directories.each_with_object({}) do |directory, result|
61
+ result[directory] = extensions
62
+ end
63
+
64
+ @_file_watcher ||= FileWatcher.new([], to_watch) do
65
+ callback.call(@last_changes)
66
+ @last_changes = nil
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end