lookbook 2.0.0 → 2.0.2

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