lookbook 2.0.0 → 2.0.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a0dbc6b86d4b4514268cbcfc281ced7379199b45e2b53a3a6614ce3dbc171bff
4
- data.tar.gz: aad673566f48af3340f9df7e82e8189819dce778658245127137d4936d6312ac
3
+ metadata.gz: 367001bca8dd83ecf86085760aaa3befe93665913ada3602c893b5b768700b82
4
+ data.tar.gz: 733c573b3380072d0acecb9ce63e50f5a652d75ff649bda983c25935c938464b
5
5
  SHA512:
6
- metadata.gz: e87da3e694b8d2317e0bc040f685ceb1a623a7b3bc72740b60606a6750ccf55b6a8c6f2db56b9b574de584dac3d42b25d72e1781301afc126446c3a584510745
7
- data.tar.gz: ecc122d412ab626a1a2e5abfe33a96a2e261aeac40e6d26b302f9a0a44b7ee7854068f801f45080ca184c087638e27c1da91de250c0977c0f01b719eedeabfbb
6
+ metadata.gz: afe34ddf105269ad47a25d1f0b208eb7dec1acf7858cde29c1aec9f3a59fcafb99ee17f4f48e7ba3b1aef59ac20aa50e2e0382560862cef334c25605f43130b6
7
+ data.tar.gz: ced6d61a75a6e614dba752215f9ff7226c1c10e2c569048a605af35ed5c165129581dd9e8f34362a0d52863fae04143e23ec2615d33afdc027437b4f8ab96393
data/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  <p>A UI development environment for Ruby on Rails applications.</p>
6
6
 
7
- <p><strong><a href="https://lookbook.build">Documentation</a> &nbsp;|&nbsp; <a href="http://demo.lookbook.build/">Demo site</a></strong></p>
7
+ <p><strong><a href="https://lookbook.build">Documentation</a> &nbsp;|&nbsp; <a href="http://demo.lookbook.build/lookbook">Demo site</a></strong></p>
8
8
 
9
9
  <p><a href="https://rubygems.org/gems/lookbook"><img src="https://img.shields.io/gem/v/lookbook" alt="Gem version"></a>
10
10
  <a href="https://github.com/ViewComponent/lookbook/actions/workflows/ci.yml"><img src="https://github.com/ViewComponent/lookbook/actions/workflows/ci.yml/badge.svg" alt="CI status"></a></p>
@@ -13,6 +13,13 @@
13
13
 
14
14
  ---
15
15
 
16
+ <div align="center">
17
+ Lookbook combines a powerful <strong>component browser</strong> and <strong>preview system</strong> with an <strong>integrated documentation engine</strong> to help teams build robust, modular, maintainable user interfaces. <a href="https://lookbook.build"><strong>Learn more &rarr;</strong></a>
18
+
19
+ </div>
20
+
21
+ ---
22
+
16
23
  [![Lookbook UI](.github/assets/lookbook_ui.png)](http://lookbook.build/)
17
24
 
18
25
  ## Development
@@ -36,6 +36,18 @@ export default function embedInspectorComponent(id, embedStore) {
36
36
  onResized({ height }) {
37
37
  if (height) {
38
38
  this.viewportHeight = height;
39
+
40
+ // Notify parent window of height resize so the parent window can implement
41
+ // its own iframe resize strategy if not using the Lookbook JS script.
42
+ // Uses Embedly-compatible postMessage format: https://docs.embed.ly/reference/provider-height-resizing
43
+ window.parent.postMessage(
44
+ JSON.stringify({
45
+ src: window.location.toString(),
46
+ context: "iframe.resize",
47
+ height,
48
+ }),
49
+ "*"
50
+ );
39
51
  }
40
52
  },
41
53
 
@@ -1,12 +1,19 @@
1
1
  @layer components {
2
2
  [data-component="embed-code-dropdown"] {
3
3
  & [data-component="code"] {
4
- @apply p-0;
4
+ @apply px-2 py-2 bg-lookbook-base-50 border border-dashed border-lookbook-divider;
5
5
  }
6
6
 
7
7
  & pre.code.highlight {
8
8
  @apply overflow-hidden whitespace-normal p-0;
9
9
  font-size: 11px;
10
10
  }
11
+
12
+ .line-clamp-2 {
13
+ overflow: hidden;
14
+ display: -webkit-box;
15
+ -webkit-box-orient: vertical;
16
+ -webkit-line-clamp: 2;
17
+ }
11
18
  }
12
19
  }
@@ -1,22 +1,64 @@
1
- <%= render_component_tag class:"p-3 w-[320px]" do %>
2
- <h4 class="text-[11px] uppercase tracking-wider mb-2 font-bold">Preview embed code</h4>
1
+ <%= render_component_tag class:"p-3 w-[360px] text-[11px] text-gray-600" do %>
3
2
 
4
- <p class="text-xs text-gray-600 mb-3">
5
- This code can be used to embed this preview in
6
- Lookbook pages<% unless policy == "SAMEORIGIN" %> or on external sites<% end %>.
7
- </p>
3
+ <!-- p class="mb-3">
4
+ Embed this preview in Lookbook pages or externally using the code or iframe URL below.
5
+ </!-->
8
6
 
9
- <div class="border-t border-lookbook-dropdown-divider pt-3 pb-3">
10
- <%= code :html do %><%= embed_code %><% end %>
7
+ <div class="mb-4 space-y-2">
8
+
9
+ <header class="flex items-center">
10
+ <h4 class=" uppercase tracking-wider font-bold">Embed Code</h4>
11
+ <span class="ml-auto" >
12
+ <a
13
+ href="https://lookbook.build/guide/sharing/embeds"
14
+ target="_blank"
15
+ title="Help"
16
+ class="text-lookbook-prose underline opacity-70 hover:opacity-100">
17
+ what's this?
18
+ </a>
19
+ </span>
20
+ </header>
21
+
22
+ <div class="relative group">
23
+ <%= code :html do %><%= embed_code %><% end %>
24
+
25
+ <span class="absolute top-px right-px bg-lookbook-base-50 rounded-md transition-opacity duration-300 opacity-0 group-hover:opacity-100">
26
+ <%= lookbook_render :copy_button, icon: :copy, tooltip: "Copy" do %>
27
+ <%= escape_once embed_code %>
28
+ <% end %>
29
+ </span>
30
+ </div>
31
+
11
32
  </div>
12
33
 
13
- <%= lookbook_render :text_button, "@click.stop.prevent": "copyEmbedCode", ":disabled": "copied" do |button| %>
14
- <% button.with_icon name: :copy, size: 3, "x-show": "!copied", "x-cloak": true %>
15
- <% button.with_icon name: :check, size: 3, "x-show": "copied", "x-cloak": true %>
16
- <span x-text="copied ? 'Copied!' : 'Copy embed code'"></span>
17
- <% end %>
34
+ <div class="space-y-2">
35
+
36
+ <header class="flex items-center">
37
+ <h4 class="text-[11px] uppercase tracking-wider font-bold">Embed URL</h4>
38
+ <span class="ml-auto" >
39
+ <a
40
+ href="https://lookbook.build/guide/sharing/embeds#other-services"
41
+ target="_blank"
42
+ title="Help"
43
+ class="text-lookbook-prose underline opacity-70 hover:opacity-100">
44
+ what's this?
45
+ </a>
46
+ </span>
47
+ </header>
48
+
49
+ <div class="relative group">
50
+ <div class="font-mono bg-lookbook-base-50 px-2 py-2 break-all border border-dashed border-lookbook-divider">
51
+ <span class="line-clamp-2">
52
+ <%= embed_url %>
53
+ </span>
54
+ </div>
55
+
56
+ <span class="absolute top-px right-px bg-lookbook-base-50 rounded-md transition-opacity duration-300 opacity-0 group-hover:opacity-100">
57
+ <%= lookbook_render :copy_button, icon: :copy, tooltip: "Copy" do %><%= embed_url %><% end %>
58
+ </span>
59
+ </div>
18
60
 
19
- <div class="hidden" x-ref="copyTarget">
20
- <%= escape_once embed_code %>
21
61
  </div>
22
- <% end %>
62
+
63
+
64
+ <% end %>
@@ -1,26 +1,3 @@
1
- import { decodeEntities } from "@helpers/string";
2
-
3
1
  export default function embedCodeDropdownComponent() {
4
- let copyTimeout = null;
5
-
6
- return {
7
- copied: false,
8
-
9
- copyEmbedCode() {
10
- this.$nextTick(async () => {
11
- const content = decodeEntities(this.$refs.copyTarget.innerHTML.trim());
12
-
13
- await window.navigator.clipboard.writeText(content);
14
- this.copied = true;
15
-
16
- if (copyTimeout) {
17
- clearTimeout(copyTimeout);
18
- }
19
-
20
- copyTimeout = setTimeout(() => {
21
- this.copied = false;
22
- }, 2000);
23
- });
24
- },
25
- };
2
+ return {};
26
3
  }
@@ -33,6 +33,15 @@ module Lookbook
33
33
  escape_once embed_tag
34
34
  end
35
35
 
36
+ def embed_url
37
+ props = {
38
+ preview: preview_name,
39
+ scenario: target.name,
40
+ **external_embed_params.transform_keys { |k| k.tr("-", "_") }
41
+ }.to_json
42
+ "#{app_path}embed?props=#{CGI.escape(props)}"
43
+ end
44
+
36
45
  private
37
46
 
38
47
  def alpine_component
@@ -8,8 +8,15 @@ module Lookbook
8
8
  # the request needs to look like it's coming from the host app,
9
9
  # not the Lookbook engine. So we try to get the controller and action
10
10
  # for the root path and use that as the 'fake' request context instead.
11
- request_path = main_app.respond_to?(:root_path) ? main_app.root_path : "/"
12
- path_parameters = Rails.application.routes.recognize_path(request_path)
11
+ path_parameters = begin
12
+ request_path = main_app.respond_to?(:root_path) ? main_app.root_path : "/"
13
+ Rails.application.routes.recognize_path(request_path)
14
+ rescue
15
+ # Fix for authenticated devise paths
16
+ if main_app.respond_to?(:new_user_session_path)
17
+ Rails.application.routes.recognize_path(main_app.new_user_session_path)
18
+ end
19
+ end
13
20
 
14
21
  preview_request = request.clone
15
22
  preview_request.path_parameters = path_parameters if path_parameters.present?
@@ -114,7 +114,9 @@ function guessBasePath() {
114
114
  const scriptSrc = script.src;
115
115
 
116
116
  if (scriptSrc && scriptSrc.includes("lookbook-assets")) {
117
- return scriptSrc.replace("lookbook-assets/js/lookbook.js", "lookbook");
117
+ return scriptSrc
118
+ .split("?")[0]
119
+ .replace("lookbook-assets/js/lookbook.js", "lookbook");
118
120
  }
119
121
 
120
122
  return `//${location.host}/lookbook`;
@@ -2,13 +2,16 @@ module Lookbook
2
2
  module HierarchicalCollection
3
3
  extend ActiveSupport::Concern
4
4
 
5
+ TREE_BUILDER = nil
6
+
5
7
  included do
6
8
  def entities
7
9
  @_cache[:entities] ||= collect_ordered_entities(to_tree(include_hidden: true))
8
10
  end
9
11
 
10
12
  def to_tree(include_hidden: false)
11
- @_cache[include_hidden ? :tree_with_hidden : :tree] ||= EntityTreeBuilder.call(@entities, include_hidden: include_hidden)
13
+ cache_key = include_hidden ? :tree_with_hidden : :tree
14
+ @_cache[cache_key] ||= self.class::TREE_BUILDER.call(@entities, include_hidden: include_hidden)
12
15
  end
13
16
 
14
17
  protected
@@ -2,6 +2,8 @@ module Lookbook
2
2
  class PageCollection < EntityCollection
3
3
  include HierarchicalCollection
4
4
 
5
+ TREE_BUILDER = PageTreeBuilder
6
+
5
7
  def load(page_paths, changes = nil)
6
8
  file_paths = PageCollection.file_paths(page_paths)
7
9
  reload_all(file_paths) # TODO: Fix incremental reloading
@@ -44,7 +46,7 @@ module Lookbook
44
46
 
45
47
  def self.file_paths(directories)
46
48
  directories.flat_map do |dir|
47
- PathUtils.normalize_paths(Dir["#{dir}/**/*.html.*", "#{dir}/**/*.md.*"].sort)
49
+ PathUtils.normalize_paths(Dir["#{dir}/**/*.html.*", "#{dir}/**/*.md.*", "#{dir}/**/*.md"].sort)
48
50
  end
49
51
  end
50
52
 
@@ -2,6 +2,8 @@ module Lookbook
2
2
  class PreviewCollection < EntityCollection
3
3
  include HierarchicalCollection
4
4
 
5
+ TREE_BUILDER = PreviewTreeBuilder
6
+
5
7
  def find_scenario_by_path(lookup_path)
6
8
  scenarios.find_by_path(lookup_path)
7
9
  end
@@ -1,36 +1,31 @@
1
1
  module Lookbook
2
- class EntityTreeBuilder < Service
2
+ class PageTreeBuilder < Service
3
3
  attr_reader :include_hidden
4
4
 
5
- def initialize(entities, include_hidden: false)
6
- @entities = entities.to_a
5
+ def initialize(pages, include_hidden: false)
6
+ @pages = pages.to_a
7
7
  @include_hidden = include_hidden
8
8
  end
9
9
 
10
10
  def call
11
11
  root_node = TreeNode.new
12
- entities.each do |entity|
12
+ pages.each do |page|
13
13
  current_node = root_node
14
- path_segments = parse_segments(entity.logical_path)
14
+
15
+ path_segments = parse_segments(page.relative_file_path)
15
16
  path_segments.each.with_index(1) do |segment, i|
16
17
  name, priority_prefix = segment
17
- content = entity if entity.depth == i # entities are always on the leaf nodes
18
+ content = page if page.depth == i # pages are always on the leaf nodes
18
19
 
19
20
  current_node.add_child(name, content, priority: priority_prefix) unless current_node.has_child?(name)
20
21
  current_node = current_node.get_child(name)
21
-
22
- if content && content.type == :preview
23
- content.visible_scenarios.each do |scenario|
24
- current_node.add_child(scenario.name, scenario)
25
- end
26
- end
27
22
  end
28
23
  end
29
24
  root_node
30
25
  end
31
26
 
32
27
  def parse_segments(path)
33
- path.split("/").map do |segment|
28
+ path.to_s.split("/").map do |segment|
34
29
  unless segment.start_with?(".")
35
30
  priority, name = PriorityPrefixParser.call(segment)
36
31
  [name, priority || 10000]
@@ -38,8 +33,8 @@ module Lookbook
38
33
  end.compact
39
34
  end
40
35
 
41
- def entities
42
- include_hidden ? @entities : @entities.select(&:visible?)
36
+ def pages
37
+ include_hidden ? @pages : @pages.select(&:visible?)
43
38
  end
44
39
  end
45
40
  end
@@ -0,0 +1,34 @@
1
+ module Lookbook
2
+ class PreviewTreeBuilder < Service
3
+ attr_reader :include_hidden
4
+
5
+ def initialize(previews, include_hidden: false)
6
+ @previews = previews.to_a
7
+ @include_hidden = include_hidden
8
+ end
9
+
10
+ def call
11
+ root_node = TreeNode.new
12
+ previews.each do |preview|
13
+ current_node = root_node
14
+
15
+ path_segments = preview.logical_path.split("/")
16
+ path_segments.each.with_index(1) do |name, i|
17
+ content = preview if preview.depth == i # entities are always on the leaf nodes
18
+
19
+ current_node.add_child(name, content) unless current_node.has_child?(name)
20
+ current_node = current_node.get_child(name)
21
+
22
+ content&.visible_scenarios&.each do |scenario|
23
+ current_node.add_child(scenario.name, scenario)
24
+ end
25
+ end
26
+ end
27
+ root_node
28
+ end
29
+
30
+ def previews
31
+ include_hidden ? @previews : @previews.select(&:visible?)
32
+ end
33
+ end
34
+ end
@@ -14,11 +14,11 @@ module Lookbook
14
14
  file_name = File.basename(path).split(".").first
15
15
 
16
16
  segments = [*directory_path&.split("/"), file_name].compact
17
- stripped_segments = segments.map! do |segment|
17
+ segments.map! do |segment|
18
18
  PriorityPrefixParser.call(segment).last.tr("-", "_")
19
19
  end
20
20
 
21
- to_path(stripped_segments)
21
+ to_path(segments)
22
22
  end
23
23
 
24
24
  def to_path(*args)
@@ -1,7 +1,7 @@
1
1
  module Lookbook
2
2
  class ParamTag < YardTag
3
3
  VALUE_TYPE_MATCHER = /^(\[\s?([A-Z]{1}\w+)\s?\])/
4
- DESCRIPTION_MATCHER = /(?<=\s|^)"(.*[^\\])"(?:\s|$)/
4
+ DESCRIPTION_MATCHER = /(?<=\s|^)"(.*?[^\\])"(?:\s|$)/
5
5
 
6
6
  supports_options
7
7
 
@@ -45,19 +45,26 @@ module Lookbook
45
45
  value_type = nil
46
46
  description = nil
47
47
 
48
+ # Parse out YARD-style value type definition - i.e. [Boolean]
48
49
  text.match(VALUE_TYPE_MATCHER) do |match_data|
49
50
  value_type = match_data[2]
50
51
  text.gsub!(VALUE_TYPE_MATCHER, "").strip!
51
52
  end
52
53
 
54
+ # Parse and remove any options from string
55
+ text_with_options = text
56
+ _, text = parse_options(text)
57
+ options_str = text_with_options.sub(text, "")
58
+
59
+ # Parse description, if provided
53
60
  text.match(DESCRIPTION_MATCHER) do |match_data|
54
61
  description = match_data[1]
55
- text.gsub!(DESCRIPTION_MATCHER, "").strip!
62
+ text.sub!(DESCRIPTION_MATCHER, "").strip!
56
63
  end
57
64
 
58
65
  input, rest = text.split(" ", 2)
59
66
 
60
- {input: input, value_type: value_type, description: description, rest: rest}
67
+ {input: input, value_type: value_type, description: description, rest: [rest, options_str].compact.join(" ")}
61
68
  end
62
69
  end
63
70
  end
@@ -40,15 +40,7 @@ module Lookbook
40
40
 
41
41
  def tag_parts
42
42
  if @tag_parts.nil?
43
- options, text = if self.class.supports_options?
44
- TagOptionsParser.call(@text, {
45
- file: host_file,
46
- base_dir: host_file&.dirname,
47
- eval_context: host_class_instance
48
- })
49
- else
50
- [{}, @text]
51
- end
43
+ options, text = parse_options(@text)
52
44
  end
53
45
  @tag_parts ||= {options: options, text: text}
54
46
  end
@@ -85,5 +77,17 @@ module Lookbook
85
77
  end
86
78
  host_code_object&.path&.constantize
87
79
  end
80
+
81
+ def parse_options(input)
82
+ if self.class.supports_options?
83
+ TagOptionsParser.call(input, {
84
+ file: host_file,
85
+ base_dir: host_file&.dirname,
86
+ eval_context: host_class_instance
87
+ })
88
+ else
89
+ [{}, @text]
90
+ end
91
+ end
88
92
  end
89
93
  end
@@ -1,3 +1,3 @@
1
1
  module Lookbook
2
- VERSION = "2.0.0"
2
+ VERSION = "2.0.2"
3
3
  end