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

Sign up to get free protection for your applications and to get access to all the features.
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;