lookbook 1.5.5 → 2.0.0.beta.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 (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