lookbook 3.0.0.alpha.1 → 3.0.0.alpha.2

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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +8 -261
  3. data/app/components/lookbook/ui/app/router/router.html.erb +1 -0
  4. data/app/components/lookbook/ui/app/router/router.js +29 -3
  5. data/app/components/lookbook/ui/elements/nav/nav_item/nav_item.js +2 -4
  6. data/app/components/lookbook/ui/elements/viewport/viewport.css +1 -0
  7. data/app/components/lookbook/ui/elements/viewport/viewport.html.erb +0 -1
  8. data/app/components/lookbook/ui/elements/viewport/viewport.rb +1 -6
  9. data/app/components/lookbook/ui/previews/preview_embed/preview_embed.html.erb +1 -2
  10. data/app/components/lookbook/ui/previews/preview_embed/preview_embed.rb +5 -4
  11. data/app/components/lookbook/ui/previews/preview_inspector/preview_inspector.html.erb +2 -2
  12. data/app/components/lookbook/ui/previews/preview_inspector/preview_inspector.rb +3 -2
  13. data/app/controllers/lookbook/events_controller.rb +4 -0
  14. data/app/controllers/lookbook/previews_controller.rb +1 -1
  15. data/app/views/layouts/lookbook/embed.html.erb +1 -1
  16. data/app/views/lookbook/inspector/panels/_preview.html.erb +1 -2
  17. data/app/views/lookbook/previews/embed.html.erb +1 -1
  18. data/app/views/lookbook/previews/inspect.html.erb +1 -0
  19. data/app/views/lookbook/previews/scenario.html.erb +5 -1
  20. data/config/initializers/05_autoload_previews.rb +2 -2
  21. data/config/routes.rb +2 -1
  22. data/lib/lookbook/concerns/feature_checks.rb +17 -0
  23. data/lib/lookbook/config.rb +2 -3
  24. data/lib/lookbook/directory_entity.rb +72 -0
  25. data/lib/lookbook/engine.rb +1 -1
  26. data/lib/lookbook/entity.rb +4 -2
  27. data/lib/lookbook/entity_tree.rb +17 -2
  28. data/lib/lookbook/pages/page_directories.rb +20 -0
  29. data/lib/lookbook/pages/page_directory_entity.rb +9 -40
  30. data/lib/lookbook/pages/page_entity.rb +4 -2
  31. data/lib/lookbook/pages/pages.rb +5 -14
  32. data/lib/lookbook/previews/preview_directories.rb +20 -0
  33. data/lib/lookbook/previews/preview_directory_entity.rb +8 -35
  34. data/lib/lookbook/previews/preview_entity.rb +7 -8
  35. data/lib/lookbook/previews/previews.rb +14 -15
  36. data/lib/lookbook/previews/scenario_entity.rb +1 -1
  37. data/lib/lookbook/reloader.rb +3 -6
  38. data/lib/lookbook/services/list_resolver.rb +13 -2
  39. data/lib/lookbook/services/styles_extractor.rb +1 -1
  40. data/lib/lookbook/utils.rb +4 -0
  41. data/lib/lookbook/version.rb +1 -1
  42. data/lib/lookbook.rb +0 -1
  43. data/public/lookbook-assets/app.css +2 -0
  44. data/public/lookbook-assets/app.js +48 -8
  45. metadata +6 -17
  46. data/lib/lookbook/previews/tags/priority_tag.rb +0 -11
@@ -0,0 +1,72 @@
1
+ module Lookbook
2
+ class DirectoryEntity < Entity
3
+ include EntityTreeNode
4
+
5
+ CONFIG_FILE_NAME = "_config.yml"
6
+
7
+ attr_reader :lookup_path
8
+
9
+ def initialize(lookup_path, path: nil)
10
+ @lookup_path = lookup_path
11
+ @path = path
12
+ @type = :directory
13
+ end
14
+
15
+ def id
16
+ @id ||= Utils.id(lookup_path)
17
+ end
18
+
19
+ def name
20
+ @name ||= lookup_path.split("/").pop
21
+ end
22
+
23
+ def label
24
+ config.fetch(:label, super)
25
+ end
26
+
27
+ def hidden?
28
+ config.fetch(:hidden, false) || children.select(&:visible?).none?
29
+ end
30
+
31
+ def path
32
+ Pathname(@path) if @path
33
+ end
34
+
35
+ def exists?
36
+ path ? File.exist?(path) : false
37
+ end
38
+
39
+ def children
40
+ raise Lookbook::Error, "DirectoryEntity subclasses must define a #children method"
41
+ end
42
+
43
+ def parent
44
+ raise Lookbook::Error, "DirectoryEntity subclasses must define a #parent method"
45
+ end
46
+
47
+ def to_h
48
+ {
49
+ entity: "directory",
50
+ name: name,
51
+ label: label,
52
+ lookup_path: lookup_path,
53
+ path: path.to_s,
54
+ children: children.map(&:to_h)
55
+ }
56
+ end
57
+
58
+ protected
59
+
60
+ def config
61
+ @config ||= begin
62
+ opts = if exists?
63
+ config_file_path = File.join(path, CONFIG_FILE_NAME)
64
+ if File.exist?(config_file_path)
65
+ YAML.safe_load_file(config_file_path)
66
+ end
67
+ end
68
+ DataObject.new(opts || {})
69
+ end
70
+ end
71
+ end
72
+ end
@@ -13,7 +13,7 @@ module Lookbook
13
13
  end
14
14
 
15
15
  config.to_prepare do
16
- ViewComponentConfigSync.call if Gem.loaded_specs.has_key?("view_component")
16
+ ViewComponentConfigSync.call if Utils.gem_installed?("view_component")
17
17
 
18
18
  preview_controller = Lookbook.config.preview_controller.constantize
19
19
  unless preview_controller.include?(Lookbook::PreviewControllerActions)
@@ -1,5 +1,7 @@
1
1
  module Lookbook
2
2
  class Entity
3
+ DEFAULT_PRIORITY = 1
4
+
3
5
  send(:include, Lookbook::Engine.routes.url_helpers) # YARD parsing workaround: https://github.com/lsegal/yard/issues/546
4
6
 
5
7
  def id
@@ -29,7 +31,7 @@ module Lookbook
29
31
  end
30
32
 
31
33
  def priority
32
- default_priority || 0
34
+ default_priority || DEFAULT_PRIORITY
33
35
  end
34
36
 
35
37
  attr_reader :default_priority
@@ -39,7 +41,7 @@ module Lookbook
39
41
  end
40
42
 
41
43
  def <=>(other)
42
- [priority || Float::INFINITY, label] <=> [other.priority || Float::INFINITY, other.label]
44
+ [priority || DEFAULT_PRIORITY, label.downcase] <=> [other.priority || DEFAULT_PRIORITY, other.label.downcase]
43
45
  end
44
46
 
45
47
  def parent_lookup_path = nil
@@ -4,13 +4,19 @@ module Lookbook
4
4
 
5
5
  attr_reader :lookup_path
6
6
 
7
- def initialize(leaf_nodes = [])
7
+ def initialize(leaf_nodes = [], config_path: nil)
8
8
  @leaf_nodes = leaf_nodes
9
+ @config_path = config_path
9
10
  @lookup_path = ""
10
11
  end
11
12
 
12
13
  def children
13
- @children ||= nodes.select { _1.depth == 1 }.sort
14
+ @children ||= begin
15
+ child_nodes = nodes.select { _1.depth == 1 }.sort
16
+ ListResolver.call(config.fetch(:children, "*"), child_nodes.map(&:name)) do |name|
17
+ child_nodes.find { _1.name == name }
18
+ end
19
+ end
14
20
  end
15
21
 
16
22
  def children_of(parent)
@@ -61,5 +67,14 @@ module Lookbook
61
67
  [node, child_nodes]
62
68
  end
63
69
  end
70
+
71
+ def config
72
+ @config ||= begin
73
+ opts = if @config_path && File.exist?(@config_path)
74
+ YAML.safe_load_file(@config_path)
75
+ end
76
+ DataObject.new(opts || {})
77
+ end
78
+ end
64
79
  end
65
80
  end
@@ -0,0 +1,20 @@
1
+ module Lookbook
2
+ class PageDirectories
3
+ def initialize
4
+ @directories = []
5
+ end
6
+
7
+ def find_or_add(lookup_path, path = nil)
8
+ find(lookup_path) || add(lookup_path, path)
9
+ end
10
+
11
+ def find(lookup_path)
12
+ @directories.find { _1.lookup_path == lookup_path }
13
+ end
14
+
15
+ def add(lookup_path, path = nil)
16
+ @directories << PageDirectoryEntity.new(lookup_path, path: path)
17
+ @directories.last
18
+ end
19
+ end
20
+ end
@@ -1,55 +1,24 @@
1
1
  module Lookbook
2
- class PageDirectoryEntity < Entity
3
- include EntityTreeNode
4
-
5
- attr_reader :path
6
-
7
- def initialize(path, default_priority: nil)
8
- @path = Pathname(path)
9
- @default_priority = default_priority
10
- @type = :directory
11
- end
12
-
13
- def lookup_path
14
- @lookup_path ||= PathPriorityPrefixesStripper.call(path)
15
- end
16
-
17
- def id
18
- @id ||= Utils.id(lookup_path)
19
- end
20
-
21
- def name
22
- @name ||= lookup_path.split("/").pop
23
- end
24
-
2
+ class PageDirectoryEntity < DirectoryEntity
25
3
  def children
26
- @children ||= Pages.tree.children_of(self).sort
27
- end
28
-
29
- def hidden?
30
- children.select(&:visible?).none?
4
+ @children ||= begin
5
+ child_nodes = Pages.tree.children_of(self).sort
6
+ ListResolver.call(config.fetch(:children, "*"), child_nodes.map(&:name)) do |name|
7
+ child_nodes.find { _1.name == name }
8
+ end
9
+ end
31
10
  end
32
11
 
33
12
  def parent
34
13
  parent_lookup_path = File.dirname(lookup_path).delete_prefix(".")
35
- Pages.directories.find { _1.lookup_path == parent_lookup_path }
14
+ Pages.directories.find_or_add(parent_lookup_path, File.dirname(path)) if parent_lookup_path.present?
36
15
  end
37
16
 
38
17
  def priority
39
18
  @priority = begin
40
- pos = PriorityPrefixParser.call(File.basename(path)).first || @default_priority
19
+ pos = PriorityPrefixParser.call(File.basename(path)).first || Entity::DEFAULT_PRIORITY
41
20
  pos.to_i
42
21
  end
43
22
  end
44
-
45
- def to_h
46
- {
47
- entity: "directory",
48
- name: name,
49
- label: label,
50
- lookup_path: lookup_path,
51
- children: children.map(&:to_h)
52
- }
53
- end
54
23
  end
55
24
  end
@@ -56,7 +56,9 @@ module Lookbook
56
56
  end
57
57
 
58
58
  def parent
59
- Pages.directories.find { _1.lookup_path == parent_lookup_path }
59
+ if parent_lookup_path.present?
60
+ Pages.directories.find_or_add(parent_lookup_path, File.dirname(file_path))
61
+ end
60
62
  end
61
63
 
62
64
  def next
@@ -69,7 +71,7 @@ module Lookbook
69
71
 
70
72
  def priority
71
73
  @priority = begin
72
- pos = PriorityPrefixParser.call(file_name).first || metadata.fetch(:priority, 0)
74
+ pos = PriorityPrefixParser.call(file_name).first || metadata.fetch(:priority, Entity::DEFAULT_PRIORITY)
73
75
  pos.to_i
74
76
  end
75
77
  end
@@ -30,7 +30,10 @@ module Lookbook
30
30
  def tree
31
31
  @tree ||= begin
32
32
  debug("pages: building tree")
33
- EntityTree.new(store.all)
33
+
34
+ config_dir = page_paths.detect { Dir["#{_1}/#{DirectoryEntity::CONFIG_FILE_NAME}"].first }
35
+ config_path = File.join(config_dir, DirectoryEntity::CONFIG_FILE_NAME) if config_dir
36
+ EntityTree.new(store.all, config_path: config_path)
34
37
  end
35
38
  end
36
39
 
@@ -44,19 +47,7 @@ module Lookbook
44
47
  Utils.deep_camelize_keys(to_data(...))
45
48
  end
46
49
 
47
- def directories
48
- @directories ||= begin
49
- dirnames = store.all.map { _1.relative_file_path.dirname.to_s.delete_prefix(".") }.compact_blank.uniq
50
- directory_paths = dirnames.flat_map do |path|
51
- current_path = ""
52
- path.split("/").map do |segment|
53
- current_path = "#{current_path}/#{segment}".delete_prefix("/")
54
- end
55
- end
56
- sorted_paths = directory_paths.uniq.sort
57
- sorted_paths.map.with_index(1) { PageDirectoryEntity.new(_1, default_priority: _2) }
58
- end
59
- end
50
+ def directories = @directories ||= PageDirectories.new
60
51
 
61
52
  def reloader
62
53
  Reloader.new(:pages, watch_paths, watch_extensions) do |changes|
@@ -0,0 +1,20 @@
1
+ module Lookbook
2
+ class PreviewDirectories
3
+ def initialize
4
+ @directories = []
5
+ end
6
+
7
+ def find_or_add(lookup_path, path = nil)
8
+ find(lookup_path) || add(lookup_path, path)
9
+ end
10
+
11
+ def find(lookup_path)
12
+ @directories.find { _1.lookup_path == lookup_path }
13
+ end
14
+
15
+ def add(lookup_path, path = nil)
16
+ @directories << PreviewDirectoryEntity.new(lookup_path, path: path)
17
+ @directories.last
18
+ end
19
+ end
20
+ end
@@ -1,44 +1,17 @@
1
1
  module Lookbook
2
- class PreviewDirectoryEntity < Entity
3
- include EntityTreeNode
4
-
5
- attr_reader :lookup_path
6
-
7
- def initialize(lookup_path, default_priority: nil)
8
- @lookup_path = lookup_path
9
- @default_priority = default_priority
10
- @type = :directory
11
- end
12
-
13
- def id
14
- @id ||= Utils.id(lookup_path)
15
- end
16
-
17
- def name
18
- @name ||= lookup_path.split("/").pop
19
- end
20
-
2
+ class PreviewDirectoryEntity < DirectoryEntity
21
3
  def children
22
- @children ||= Previews.tree.children_of(self).sort
23
- end
24
-
25
- def hidden?
26
- children.select(&:visible?).none?
4
+ @children ||= begin
5
+ child_nodes = Previews.tree.children_of(self).sort
6
+ ListResolver.call(config.fetch(:children, "*"), child_nodes.map(&:name)) do |name|
7
+ child_nodes.find { _1.name == name }
8
+ end
9
+ end
27
10
  end
28
11
 
29
12
  def parent
30
13
  parent_lookup_path = File.dirname(lookup_path).delete_prefix(".")
31
- Previews.directories.find { _1.lookup_path == parent_lookup_path }
32
- end
33
-
34
- def to_h
35
- {
36
- entity: "directory",
37
- name: name,
38
- label: label,
39
- lookup_path: lookup_path,
40
- children: children.map(&:to_h)
41
- }
14
+ Previews.directories.find_or_add(parent_lookup_path, File.dirname(path)) if parent_lookup_path.present?
42
15
  end
43
16
  end
44
17
  end
@@ -1,6 +1,7 @@
1
1
  module Lookbook
2
2
  class PreviewEntity < Entity
3
3
  include EntityTreeNode
4
+ include FeatureChecks
4
5
 
5
6
  attr_reader :preview_class, :preview_class_name, :preview_methods, :file_path, :metadata
6
7
 
@@ -35,10 +36,6 @@ module Lookbook
35
36
  metadata.status(default: Lookbook.config.preview_default_status)
36
37
  end
37
38
 
38
- def priority
39
- metadata.fetch(:priority, super)
40
- end
41
-
42
39
  def url_param
43
40
  case Lookbook.config.preview_url_param.to_sym
44
41
  when :uuid
@@ -152,11 +149,13 @@ module Lookbook
152
149
  end
153
150
 
154
151
  def mailer_preview?
155
- preview_class.ancestors.include?(::ActionMailer::Preview)
152
+ action_mailer_available? && preview_class.ancestors.include?(::ActionMailer::Preview)
156
153
  end
157
154
 
158
155
  def parent
159
- Previews.directories.find { _1.lookup_path == parent_lookup_path }
156
+ if parent_lookup_path.present?
157
+ Previews.directories.find_or_add(parent_lookup_path, File.dirname(file_path))
158
+ end
160
159
  end
161
160
 
162
161
  def children
@@ -181,9 +180,9 @@ module Lookbook
181
180
  private
182
181
 
183
182
  def default_readme_path
184
- preview_base_path = file_path.to_s.delete_suffix("_preview.rb")
183
+ preview_basename = File.basename(file_path)
185
184
  page_extensions_glob = "{#{Lookbook.config.page_extensions.join(",")}}"
186
- readme_path = Dir["#{preview_base_path}*.#{page_extensions_glob}"].first
185
+ readme_path = Dir["#{preview_basename}.#{page_extensions_glob}"].first
187
186
  Pathname(readme_path) if readme_path
188
187
  end
189
188
 
@@ -2,6 +2,7 @@ module Lookbook
2
2
  module Previews
3
3
  class << self
4
4
  include Loggable
5
+ include FeatureChecks
5
6
 
6
7
  delegate_missing_to :store
7
8
 
@@ -31,7 +32,10 @@ module Lookbook
31
32
  def tree
32
33
  @tree ||= begin
33
34
  debug("previews: building tree")
34
- EntityTree.new(inspector_targets)
35
+
36
+ config_dir = preview_paths.detect { Dir["#{_1}/#{DirectoryEntity::CONFIG_FILE_NAME}"].first }
37
+ config_path = File.join(config_dir, DirectoryEntity::CONFIG_FILE_NAME) if config_dir
38
+ EntityTree.new(inspector_targets, config_path: config_path)
35
39
  end
36
40
  end
37
41
 
@@ -54,13 +58,19 @@ module Lookbook
54
58
  def preview_class?(klass)
55
59
  if (defined?(ViewComponent) && klass.ancestors.include?(ViewComponent::Preview)) ||
56
60
  klass.ancestors.include?(Lookbook::Preview) ||
57
- klass.ancestors.include?(::ActionMailer::Preview)
61
+ (action_mailer_available? && klass.ancestors.include?(::ActionMailer::Preview))
58
62
  !klass.respond_to?(:abstract_class) || klass.abstract_class != true
59
63
  end
60
64
  end
61
65
 
62
66
  def preview_paths
63
- @preview_paths ||= Utils.normalize_paths(Lookbook.config.preview_paths)
67
+ @preview_paths ||= begin
68
+ action_mailer_paths = if Rails.application.config.respond_to?(:action_mailer)
69
+ Rails.application.config.action_mailer.preview_paths
70
+ end
71
+ paths = [Lookbook.config.preview_paths, action_mailer_paths].compact.flatten
72
+ Utils.normalize_paths(paths)
73
+ end
64
74
  end
65
75
 
66
76
  def component_paths
@@ -104,18 +114,7 @@ module Lookbook
104
114
  @inspector_targets ||= Previews.all.flat_map { _1.inspector_targets }
105
115
  end
106
116
 
107
- def directories
108
- @directories ||= begin
109
- directory_paths = store.all.map(&:parent_lookup_path).compact_blank.uniq.flat_map do |path|
110
- current_path = ""
111
- path.split("/").map do |segment|
112
- current_path = "#{current_path}/#{segment}".delete_prefix("/")
113
- end
114
- end
115
- sorted_paths = directory_paths.uniq.sort
116
- sorted_paths.map.with_index(1) { PreviewDirectoryEntity.new(_1, default_priority: _2) }
117
- end
118
- end
117
+ def directories = @directories ||= PreviewDirectories.new
119
118
 
120
119
  def statuses
121
120
  @statuses ||= Lookbook.config.preview_statuses.map do |name, props|
@@ -86,7 +86,7 @@ module Lookbook
86
86
  def render_args(request_params: {})
87
87
  resolved_params = resolve_request_params(request_params)
88
88
  result = call_method(**resolved_params)
89
- if result.is_a?(ActionMailer::Parameterized::MessageDelivery)
89
+ if mailer_preview?
90
90
  {
91
91
  email: result,
92
92
  template: Previews.scenario_template
@@ -1,6 +1,7 @@
1
1
  module Lookbook
2
2
  class Reloader
3
3
  include Loggable
4
+ include FeatureChecks
4
5
 
5
6
  delegate :execute, :execute_if_updated, :updated?, to: :file_watcher
6
7
 
@@ -39,7 +40,7 @@ module Lookbook
39
40
  @last_changeset = nil
40
41
  end
41
42
 
42
- if evented?
43
+ if listen_available?
43
44
  file_watcher.on_change do |changeset|
44
45
  if watching?(changeset.all)
45
46
  debug("#{name}: file changes detected")
@@ -54,7 +55,7 @@ module Lookbook
54
55
  end
55
56
 
56
57
  def file_watcher_class
57
- if evented?
58
+ if listen_available?
58
59
  require_relative "evented_file_update_checker"
59
60
 
60
61
  Lookbook::EventedFileUpdateChecker
@@ -62,9 +63,5 @@ module Lookbook
62
63
  ActiveSupport::FileUpdateChecker
63
64
  end
64
65
  end
65
-
66
- def evented?
67
- Gem.loaded_specs.has_key?("listen")
68
- end
69
66
  end
70
67
  end
@@ -8,15 +8,26 @@ module Lookbook
8
8
  end
9
9
 
10
10
  def call(&resolver)
11
- included = to_include.inject([]) do |result, name|
11
+ included = to_include.each_with_object([]) do |name, result|
12
12
  if name.to_s == "*"
13
- result += item_set.select { |item| !result.include?(item) }
13
+ result << "*"
14
14
  elsif item_set.include?(name)
15
15
  result << name
16
16
  end
17
17
  result
18
18
  end
19
19
 
20
+ remaining_items = item_set.difference(included)
21
+ included = included.flat_map do |name|
22
+ if name == "*"
23
+ rest = remaining_items
24
+ remaining_items = []
25
+ rest
26
+ else
27
+ name
28
+ end
29
+ end
30
+
20
31
  resolved = resolver ? included.map { |item| resolver.call(item) } : included
21
32
  resolved.compact.uniq
22
33
  end
@@ -33,7 +33,7 @@ module Lookbook
33
33
  end
34
34
 
35
35
  def strip_styles(text)
36
- iframes = text.scan(IFRAME_REGEX).flatten
36
+ iframes = text.scan(IFRAME_REGEX).flatten.map(&:strip).compact_blank
37
37
  iframes.each.with_index(1) { text.gsub!(_1, "IFRAME_#{_2}") }
38
38
  text = text.gsub(STYLE_TAGS_REGEX, "").strip
39
39
  iframes.each.with_index(1) { text.gsub!("IFRAME_#{_2}", _1) }
@@ -75,6 +75,10 @@ module Lookbook
75
75
  obj
76
76
  end
77
77
  end
78
+
79
+ def gem_installed?(name)
80
+ Gem.loaded_specs.has_key?(name)
81
+ end
78
82
  end
79
83
  end
80
84
  end
@@ -1,3 +1,3 @@
1
1
  module Lookbook
2
- VERSION = "3.0.0.alpha.1"
2
+ VERSION = "3.0.0.alpha.2"
3
3
  end
data/lib/lookbook.rb CHANGED
@@ -1,5 +1,4 @@
1
1
  require "zeitwerk"
2
- require "action_mailer"
3
2
  require "yard"
4
3
  require_relative "lookbook/version"
5
4
  require_relative "lookbook/logger"
@@ -2402,6 +2402,8 @@ tr:not(:last-child) {
2402
2402
  grid-template-rows: 1fr;
2403
2403
  outline: 1px solid #d1d5db;
2404
2404
  outline: 1px solid var(--lb-color-surface-divider);
2405
+ background-color: #fff;
2406
+ background-color: var(--lb-viewport-background-color);
2405
2407
  }
2406
2408
  .resize-x[data-component="viewport"] .viewport-dimensions {
2407
2409
  right: 1rem;