lookbook 0.2.4 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +81 -0
  3. data/app/assets/lookbook/css/app.css +28 -0
  4. data/app/assets/lookbook/js/app.js +49 -24
  5. data/app/assets/lookbook/js/nav/leaf.js +20 -0
  6. data/app/assets/lookbook/js/nav/node.js +31 -0
  7. data/app/assets/lookbook/js/nav.js +39 -0
  8. data/app/assets/lookbook/js/page.js +33 -0
  9. data/app/assets/lookbook/js/utils/clipboard.js +13 -0
  10. data/app/assets/lookbook/js/utils/morph.js +16 -0
  11. data/app/assets/lookbook/js/{reloader.js → utils/reloader.js} +0 -0
  12. data/app/assets/lookbook/js/utils/screen.js +44 -0
  13. data/app/assets/lookbook/js/{size_observer.js → utils/size_observer.js} +1 -1
  14. data/app/assets/lookbook/js/{split.js → utils/split.js} +4 -4
  15. data/app/assets/lookbook/js/workbench/inspector.js +11 -0
  16. data/app/assets/lookbook/js/workbench/preview.js +39 -0
  17. data/app/assets/lookbook/js/workbench.js +14 -0
  18. data/app/controllers/lookbook/{browser_controller.rb → app_controller.rb} +58 -31
  19. data/app/helpers/lookbook/application_helper.rb +1 -1
  20. data/app/views/lookbook/_sidebar.html.erb +45 -0
  21. data/app/views/lookbook/_workbench.html.erb +12 -0
  22. data/app/views/lookbook/{browser → app}/error.html.erb +0 -0
  23. data/app/views/lookbook/app/index.html.erb +11 -0
  24. data/app/views/lookbook/{browser → app}/not_found.html.erb +1 -1
  25. data/app/views/lookbook/app/show.html.erb +1 -0
  26. data/app/views/lookbook/layouts/app.html.erb +16 -26
  27. data/app/views/lookbook/nav/_collection.html.erb +5 -0
  28. data/app/views/lookbook/nav/_leaf.html.erb +22 -0
  29. data/app/views/lookbook/nav/_node.html.erb +19 -0
  30. data/app/views/lookbook/nav/_preview.html.erb +11 -0
  31. data/app/views/lookbook/preview_group.html.erb +8 -0
  32. data/app/views/lookbook/shared/_clipboard.html.erb +11 -0
  33. data/app/views/lookbook/shared/_header.html.erb +8 -0
  34. data/app/views/lookbook/workbench/_header.html.erb +39 -0
  35. data/app/views/lookbook/workbench/_inspector.html.erb +32 -0
  36. data/app/views/lookbook/workbench/_preview.html.erb +24 -0
  37. data/app/views/lookbook/workbench/inspector/_code.html.erb +3 -0
  38. data/app/views/lookbook/workbench/inspector/_notes.html.erb +24 -0
  39. data/app/views/lookbook/{partials → workbench}/inspector/_plain.html.erb +0 -0
  40. data/config/routes.rb +3 -3
  41. data/lib/lookbook/preview.rb +26 -3
  42. data/lib/lookbook/preview_controller.rb +6 -1
  43. data/lib/lookbook/preview_example.rb +3 -2
  44. data/lib/lookbook/preview_group.rb +37 -0
  45. data/lib/lookbook/taggable.rb +5 -1
  46. data/lib/lookbook/version.rb +1 -1
  47. data/lib/lookbook.rb +1 -0
  48. data/lib/tasks/lookbook_tasks.rake +1 -1
  49. data/public/lookbook-assets/app.css +229 -99
  50. data/public/lookbook-assets/app.js +882 -56
  51. data/{app/views/lookbook/partials/_icon_sprite.html.erb → public/lookbook-assets/feather-sprite.svg} +1 -1
  52. metadata +51 -22
  53. data/app/assets/lookbook/js/preview.js +0 -76
  54. data/app/views/lookbook/browser/index.html.erb +0 -8
  55. data/app/views/lookbook/browser/show.html.erb +0 -33
  56. data/app/views/lookbook/partials/_preview.html.erb +0 -18
  57. data/app/views/lookbook/partials/_sidebar.html.erb +0 -21
  58. data/app/views/lookbook/partials/inspector/_code.html.erb +0 -1
  59. data/app/views/lookbook/partials/inspector/_inspector.html.erb +0 -43
  60. data/app/views/lookbook/partials/inspector/_prose.html.erb +0 -3
  61. data/app/views/lookbook/partials/nav/_collection.html.erb +0 -17
  62. data/app/views/lookbook/partials/nav/_label.html.erb +0 -13
  63. data/app/views/lookbook/partials/nav/_nav.html.erb +0 -27
  64. data/app/views/lookbook/partials/nav/_preview.html.erb +0 -48
@@ -1,5 +1,7 @@
1
+ require "htmlbeautifier"
2
+
1
3
  module Lookbook
2
- class BrowserController < ActionController::Base
4
+ class AppController < ActionController::Base
3
5
  EXCEPTIONS = [ViewComponent::PreviewTemplateError, ViewComponent::ComponentError, ViewComponent::TemplateError, ActionView::Template::Error]
4
6
 
5
7
  protect_from_forgery with: :exception
@@ -11,43 +13,40 @@ module Lookbook
11
13
  before_action :find_preview, only: [:preview, :show]
12
14
  before_action :find_example, only: [:preview, :show]
13
15
  before_action :assign_nav, only: [:index, :show]
14
-
15
- def index
16
- end
16
+ before_action :initialize_inspector, only: [:show]
17
17
 
18
18
  def preview
19
19
  if @example
20
- render html: preview_output
20
+ render html: rendered_example
21
21
  else
22
- render "browser/not_found"
22
+ render "app/not_found"
23
23
  end
24
24
  end
25
25
 
26
26
  def show
27
27
  if @example
28
28
  begin
29
- @preview_srcdoc = preview_output.gsub("\"", "&quot;")
30
- @render_args = @preview.render_args(@example.name, params: preview_controller.params.permit!)
31
- @render_output = preview_controller.render_component_to_string(@preview, @example_name)
32
- @render_output_lang = Lookbook::Lang.find(:html)
33
- if using_preview_template?
34
- @source = @example.method_source
35
- @source_lang = @example.source_lang
36
- else
37
- @source = @example.template_source(@render_args[:template])
38
- @source_lang = @example.template_lang(@render_args[:template])
29
+ @rendered_example = rendered_example.gsub("\"", "&quot;")
30
+ (@example.type == :group ? @example.examples : [@example]).each do |example|
31
+ include_example_data(example)
39
32
  end
40
33
  assign_inspector
41
34
  rescue *EXCEPTIONS
42
- render "browser/error"
35
+ render "app/error"
43
36
  end
44
37
  else
45
- render "browser/not_found"
38
+ render "app/not_found"
46
39
  end
47
40
  end
48
41
 
49
42
  private
50
43
 
44
+ def initialize_inspector
45
+ @source = []
46
+ @output = []
47
+ @notes = []
48
+ end
49
+
51
50
  def find_preview
52
51
  candidates = []
53
52
  params[:path].to_s.scan(%r{/|$}) { candidates << $` }
@@ -66,12 +65,39 @@ module Lookbook
66
65
  end
67
66
  end
68
67
 
69
- def using_preview_template?
70
- @render_args[:template] == "view_components/preview"
68
+ def include_example_data(example)
69
+ content = HtmlBeautifier.beautify(preview_controller.render_example_to_string(@preview, example.name))
70
+ @output << {
71
+ label: "<!-- #{example.label} -->",
72
+ content: content,
73
+ lang: Lookbook::Lang.find(:html)
74
+ }
75
+ render_args = @preview.render_args(example.name, params: preview_controller.params.permit!)
76
+ has_template = render_args[:template] != "view_components/preview"
77
+ @source << {
78
+ label: has_template ? "<!-- #{example.label} -->" : "\# #{example.label}",
79
+ content: has_template ? example.template_source(render_args[:template]) : example.method_source,
80
+ lang: has_template ? example.template_lang(render_args[:template]) : example.source_lang
81
+ }
82
+ if example.notes.present?
83
+ @notes << {
84
+ label: example.label,
85
+ content: example.notes
86
+ }
87
+ end
71
88
  end
72
89
 
73
- def preview_output
74
- @preview_output ||= if @preview
90
+ def rendered_example
91
+ if @example.type == :group
92
+ examples = @example.examples.map do |example|
93
+ {
94
+ label: example.label,
95
+ html: preview_controller.render_example_to_string(@preview, example.name)
96
+ }
97
+ end
98
+ joined = render_to_string "./preview_group", locals: {examples: examples}, layout: nil
99
+ preview_controller.render_in_layout_to_string(joined, @preview.lookbook_layout)
100
+ else
75
101
  preview_controller.request.params[:path] = "#{@preview.preview_name}/#{@example.name}".chomp("/")
76
102
  preview_controller.process(:previews)
77
103
  end
@@ -82,23 +108,24 @@ module Lookbook
82
108
  panes: {
83
109
  source: {
84
110
  label: "Source",
85
- content: @source || "",
86
111
  template: "code",
87
- lang: @source_lang,
88
- clipboard: @source
112
+ hotkey: "s",
113
+ items: @source,
114
+ clipboard: @source.map { |s| @source.many? ? "#{s[:label]}\n#{s[:content]}" : s[:content] }.join("\n\n")
89
115
  },
90
116
  output: {
91
117
  label: "Output",
92
- content: @render_output || "",
93
118
  template: "code",
94
- lang: @render_output_lang,
95
- clipboard: @render_output
119
+ hotkey: "o",
120
+ items: @output,
121
+ clipboard: @output.map { |o| @output.many? ? "#{o[:label]}\n#{o[:content]}" : o[:content] }.join("\n\n")
96
122
  },
97
123
  notes: {
98
124
  label: "Notes",
99
- content: @example.notes.presence || "<em class='opacity-50'>No notes provided.</em>",
100
- template: "prose",
101
- disabled: @example.notes.blank?
125
+ template: "notes",
126
+ hotkey: "n",
127
+ items: @notes,
128
+ disabled: @notes.none?
102
129
  }
103
130
  }
104
131
  }
@@ -18,7 +18,7 @@ module Lookbook
18
18
  end
19
19
 
20
20
  def nav_padding_style(depth)
21
- "padding-left: calc((#{depth} * 12px) + 0.5rem);"
21
+ "padding-left: calc((#{depth - 1} * 12px) + 0.5rem);"
22
22
  end
23
23
  end
24
24
  end
@@ -0,0 +1,45 @@
1
+ <div id="sidebar" class="h-full" x-data="nav" @document:updated.window="updateNav" @navigate.window="navigate">
2
+ <div class="bg-white h-10 border-b border-gray-300 flex items-center relative">
3
+ <button class="flex-none ml-4 " x-show="!$screen('md')" @click="$dispatch('sidebar:toggle')" x-cloak>
4
+ <svg class="feather w-5 h-5 hover:text-indigo-500 transition">
5
+ <use xlink:href="/lookbook-assets/feather-sprite.svg#x" />
6
+ </svg>
7
+ </button>
8
+ <input
9
+ type="text"
10
+ class="text-sm px-4 h-10 w-full border-0 bg-transparent focus:outline-none outline-none focus:ring-0"
11
+ placeholder="Filter previews&hellip;"
12
+ x-ref="filter"
13
+ x-model="$store.nav.filter"
14
+ x-effect="if (sidebarOpenMobile) focusFilter()"
15
+ @keyup.stop="if ($event.key === 'Escape') $store.nav.filtering ? clearFilter() : unfocusFilter()"
16
+ @keyup.f.document="focusFilter"
17
+ >
18
+ <button class="text-gray-400 hover:text-indigo-500 focus:ring-0 focus:outline-none absolute top-1/2 right-2 transform -translate-y-1/2" @click="clearFilter">
19
+ <svg class="feather w-3 h-3">
20
+ <use xlink:href="/lookbook-assets/feather-sprite.svg#x" />
21
+ </svg>
22
+ </button>
23
+ </div>
24
+ <div class="relative overflow-y-auto w-full" style="height: calc(100% - 40px)">
25
+ <div x-ref="shim">
26
+ <nav id="nav" class="select-none" x-data="navNode" @nav:updated.window="filter" x-init="$watch('$store.nav.filterText', () => filter()); $nextTick(() => filter());">
27
+ <% if @nav.items.any? %>
28
+ <ul x-ref="items">
29
+ <% @nav.items.each do |item| %>
30
+ <%= render "./nav/#{item.type.to_s}", node: item %>
31
+ <% end %>
32
+ </ul>
33
+ <div class="p-4 text-center" x-show="hidden" x-cloak>
34
+ <em class="text-gray-400">No matching previews found.</em>
35
+ </div>
36
+ <% else %>
37
+ <div class="p-4">
38
+ <h4 class="text-gray-500 mb-1">No previews found.</h4>
39
+ <p class="text-gray-400 text-xs">Have you set your <a class="underline" href="https://viewcomponent.org/api.html#preview_paths">preview paths</a> config correctly?</p>
40
+ </div>
41
+ <% end %>
42
+ </nav>
43
+ </div>
44
+ </div>
45
+ </div>
@@ -0,0 +1,12 @@
1
+ <div id="workbench" class="bg-gray-50 h-screen flex flex-col" x-data="workbench">
2
+ <%= render "./workbench/header" %>
3
+ <div class="md:grid flex-grow" :style="`grid-template-rows: 1fr 1px ${$store.inspector.height}px`">
4
+ <%= render "./workbench/preview" %>
5
+ <div class="w-full gutter border-t border-gray-300 relative" x-data="split(splitProps)" x-show="$screen('md')">
6
+ <div class="h-[11px] w-full bg-transparent hover:bg-indigo-100 hover:bg-opacity-20 transition absolute left-0 right-0 transform -translate-y-1/2 cursor-[row-resize]"></div>
7
+ </div>
8
+ <% if @inspector %>
9
+ <%= render "./workbench/inspector", **@inspector %>
10
+ <% end %>
11
+ </div>
12
+ </div>
File without changes
@@ -0,0 +1,11 @@
1
+ <div class="flex flex-col h-full min-h-fill w-full">
2
+ <%= render "./shared/header" %>
3
+ <div class="flex flex-col items-center justify-center h-full min-h-fill">
4
+ <div class="p-4 text-center">
5
+ <svg class="feather w-10 h-10 text-gray-300 mx-auto">
6
+ <use xlink:href="/lookbook-assets/feather-sprite.svg#layers" />
7
+ </svg>
8
+ <h5 class="mt-4 text-gray-400 text-base">Select a preview to get started</h5>
9
+ </div>
10
+ </div>
11
+ </div>
@@ -1,7 +1,7 @@
1
1
  <div class="bg-white flex flex-col items-center justify-center h-screen w-full">
2
2
  <div class="p-4 text-center">
3
3
  <svg class="feather w-10 h-10 text-red-300 mx-auto">
4
- <use xlink:href="#alert-triangle" />
4
+ <use xlink:href="/lookbook-assets/feather-sprite.svg#alert-triangle" />
5
5
  </svg>
6
6
  <div class="mt-3 text-gray-700 max-w-xs">
7
7
  <% if @preview %>
@@ -0,0 +1 @@
1
+ <%= render "./workbench" %>
@@ -1,9 +1,9 @@
1
1
  <!DOCTYPE html>
2
- <html lang="en">
2
+ <html lang="en" class="h-screen">
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
5
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
6
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
7
7
 
8
8
  <link href="/lookbook-assets/app.css" rel="stylesheet">
9
9
  <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>👀</text></svg>">
@@ -17,35 +17,25 @@
17
17
 
18
18
  <title><%= [@example&.label, @preview&.label, "Lookbook"].compact.join(" :: ") %></title>
19
19
  </head>
20
- <body class="text-gray-800 font-sans text-sm antialiased">
20
+ <body class="text-gray-800 font-sans text-sm antialiased h-screen overflow-hidden">
21
21
  <div
22
- class="grid h-screen w-screen max-w-full overflow-hidden"
22
+ x-data="page"
23
+ x-effect="updateTitle"
24
+ @refresh.document="fetchHTML().then(doc => $dispatch('document:updated', {doc}))"
25
+ @popstate.window="fetchHTML().then(doc => { $dispatch('document:loaded', {doc}); sidebarOpenMobile = false})"
26
+ @sidebar:toggle.window="sidebarOpenMobile = !sidebarOpenMobile"
23
27
  :style="`grid-template-columns: ${$store.nav.width}px 1px 1fr;`"
24
- x-data="{
25
- doc: document,
26
- location: window.location.pathname,
27
- update(eventName){
28
- return fetch(document.location).then(async response => {
29
- if (!response.ok) return location.reload();
30
- const html = await response.text();
31
- this.doc = (new DOMParser()).parseFromString(html, 'text/html');
32
- $dispatch(eventName, {doc: this.doc})
33
- });
34
- }
35
- }"
36
- x-effect="document.title = doc.title; location = window.location.pathname"
37
- @refresh.document="update('document:updated')"
38
- @popstate.window="update('document:loaded')">
39
- <%= render "partials/sidebar" %>
40
- <div class="h-screen gutter border-r border-gray-300 relative" x-data="split({minSize: 200, onDrag: (splits) => { $store.nav.width = Math.min(splits[0], 500) }})">
28
+ class="md:grid w-screen h-screen"
29
+ >
30
+ <div class="h-full bg-gray-100 overflow-hidden" x-show="$screen('md') || sidebarOpenMobile" x-cloak>
31
+ <%= render "./sidebar" %>
32
+ </div>
33
+ <div x-data="split(splitProps)" class="h-full gutter border-r border-gray-300 relative" x-show="$screen('md')" x-cloak>
41
34
  <div class="w-[9px] h-full bg-transparent hover:bg-indigo-100 hover:bg-opacity-20 transition absolute top-0 bottom-0 transform -translate-x-1/2 cursor-[col-resize] z-10"></div>
42
35
  </div>
43
- <div id="main" class="h-screen overflow-hidden w-full" x-effect="$el.innerHTML = doc.getElementById('main').innerHTML">
36
+ <main id="main" x-effect="render" class="h-full overflow-hidden w-full" x-show="!$screen('md') || !sidebarOpenMobile" x-cloak>
44
37
  <%= yield %>
45
- </div>
46
- </div>
47
- <div class="hidden">
48
- <%= render "partials/icon_sprite" %>
38
+ </main>
49
39
  </div>
50
40
  </body>
51
41
  </html>
@@ -0,0 +1,5 @@
1
+ <%= render "./nav/node", node: node do %>
2
+ <% node.items.each do |item| %>
3
+ <%= render "./nav/#{item.type.to_s}", node: item %>
4
+ <% end %>
5
+ <% end %>
@@ -0,0 +1,22 @@
1
+ <%
2
+ path = show_path leaf.path
3
+ display ||= :leaf
4
+ label ||= leaf.label
5
+ %>
6
+ <li x-data="navLeaf" :class="{hidden}" x-init="matchers = <%= leaf.matchers.to_json %>; path = '<%= path %>'; setActive()" @popstate.window="setActive">
7
+ <a href="<%= path %>"
8
+ class="pr-3 py-[5px] flex items-center w-full group transition hover:bg-gray-200 hover:bg-opacity-50"
9
+ style="<%= nav_padding_style(depth) %>"
10
+ :class="{'!bg-indigo-100':active}"
11
+ @click.stop.prevent="navigate"
12
+ >
13
+ <div class="relative w-3.5 h-3.5 mr-1.5 <%= "ml-[3px]" if display == :node %> " :class="active ? 'text-gray-900' : 'text-indigo-500'">
14
+ <svg class="feather flex-none group-hover:text-indigo-800 transition w-3.5 h-3.5">
15
+ <use xlink:href="/lookbook-assets/feather-sprite.svg#<%= display == :node ? "layers" : "eye" %>" />
16
+ </svg>
17
+ </div>
18
+ <div class="truncate whitespace-nowrap <%= "font-bold" if display == :node %>">
19
+ <%= label %>
20
+ </div>
21
+ </a>
22
+ </li>
@@ -0,0 +1,19 @@
1
+ <li id="<%= node.id %>" x-data="navNode" <% if node.hierarchy_depth == 1 %> class="py-1 border-b border-gray-300 cursor-pointer"<% end %> :class="{hidden}" x-cloak>
2
+ <div @click="toggle(); if (open()) { <%= "navigate('#{path}')" if defined?(path) %>}" style="<%= nav_padding_style(node.hierarchy_depth) %>" class="hover:bg-gray-200 hover:bg-opacity-50">
3
+ <div class="flex items-center cursor-pointer pr-3 py-[5px]">
4
+ <svg class="feather w-3 h-3 mr-1 text-gray-500 flex-none">
5
+ <use xlink:href="/lookbook-assets/feather-sprite.svg#chevron-down" x-show="open" x-cloak />
6
+ <use xlink:href="/lookbook-assets/feather-sprite.svg#chevron-right" x-show="!open()" x-cloak />
7
+ </svg>
8
+ <svg class="feather h-3.5 w-3.5 mr-1.5 flex-none text-indigo-500">
9
+ <use xlink:href="/lookbook-assets/feather-sprite.svg#<%= node.type == :preview ? 'layers' : 'folder' %>" />
10
+ </svg>
11
+ <div class="truncate whitespace-nowrap text-left <%= "font-bold" if node.type == :preview %>">
12
+ <%= node.label %>
13
+ </div>
14
+ </div>
15
+ </div>
16
+ <ul x-ref="items" x-show="open" x-cloak>
17
+ <%= yield %>
18
+ </ul>
19
+ </li>
@@ -0,0 +1,11 @@
1
+ <% examples = node.get_examples %>
2
+ <% if examples.many? %>
3
+ <%= render "./nav/node", node: node, path: show_path(examples.first.path) do %>
4
+ <% examples.each do |example| %>
5
+ <%= render "./nav/leaf", leaf: example, depth: example.hierarchy_depth + 1 %>
6
+ <% end %>
7
+ <% end %>
8
+ <% else %>
9
+ <% example = examples.first %>
10
+ <%= render "./nav/leaf", leaf: example, depth: example.hierarchy_depth, label: node.label, display: :node %>
11
+ <% end %>
@@ -0,0 +1,8 @@
1
+ <% examples.each do |example| %>
2
+ <div style="margin-bottom: 30px;">
3
+ <h6 style="color: #999; font-family: sans-serif; font-size: 14px; margin-top: 0; margin-bottom: 10px;">
4
+ <%= example[:label] %>
5
+ </h6>
6
+ <%= example[:html] %>
7
+ </div>
8
+ <% end %>
@@ -0,0 +1,11 @@
1
+ <button
2
+ class="p-1.5 border-b border-l border-gray-200 hover:border-gray-300 rounded-bl-md bg-white absolute top-0 right-0 text-gray-400 hover:text-indigo-500 transition"
3
+ x-data="clipboard"
4
+ x-tooltip.theme.lookbook="done ? 'copied!' : 'copy to clipboard'"
5
+ @click="save"
6
+ data-tippy-placement="left">
7
+ <svg class="feather h-4 w-4 ">
8
+ <use x-bind:href="`/lookbook-assets/feather-sprite.svg#${done ? 'check' : 'clipboard'}`" />
9
+ </svg>
10
+ <div class="hidden" x-init="content = $el.innerText"><%== yield %></div>
11
+ </button>
@@ -0,0 +1,8 @@
1
+ <header class="py-2 px-4 w-full flex-none bg-white border-b border-gray-300 flex items-center h-10">
2
+ <button class="flex-none mr-3" x-show="!$screen('md')" @click="$dispatch('sidebar:toggle')">
3
+ <svg class="feather w-5 h-5 hover:text-indigo-500 transition">
4
+ <use xlink:href="/lookbook-assets/feather-sprite.svg#menu" />
5
+ </svg>
6
+ </button>
7
+ <%= yield %>
8
+ </header>
@@ -0,0 +1,39 @@
1
+ <%= render "./shared/header" do %>
2
+ <div class="flex items-center">
3
+ <div class="flex items-center space-x-1">
4
+ <strong class="whitespace-nowrap truncate"><%= @preview.label %></strong>
5
+ <% if @preview.get_examples.many? %>
6
+ <span>/</span>
7
+ <span class="whitespace-nowrap truncate"><%= @example.label %></span>
8
+ <% end %>
9
+ </div>
10
+ </div>
11
+ <div class="flex text-xs ml-auto font-monospace text-gray-700 space-x-1" x-show="$screen('md')">
12
+ <span x-text="`${previewViewportWidth}px`"></span>
13
+ <span class="text-gray-500">x</span>
14
+ <span x-text="`${previewViewportHeight}px`"></span>
15
+ </div>
16
+ <div class="flex items-center ml-auto md:ml-3 md:pl-3 space-x-3 md:border-l border-gray-300 text-gray-400">
17
+ <a
18
+ href="<%= url_for %>"
19
+ class="block transition hover:text-indigo-800"
20
+ x-tooltip.theme.lookbook="`Refresh preview`"
21
+ @click.prevent.stop="$dispatch('navigate')"
22
+ data-hotkey="r"
23
+ >
24
+ <svg class="feather w-4 h-4">
25
+ <use xlink:href="/lookbook-assets/feather-sprite.svg#refresh-cw" />
26
+ </svg>
27
+ </a>
28
+ <a
29
+ href="<%= preview_path %>"
30
+ class="block transition hover:text-indigo-800" target="_blank"
31
+ x-tooltip.theme.lookbook="`Open in new window`"
32
+ data-hotkey="w"
33
+ >
34
+ <svg class="feather w-4 h-4">
35
+ <use xlink:href="/lookbook-assets/feather-sprite.svg#external-link" />
36
+ </svg>
37
+ </a>
38
+ </div>
39
+ <% end %>
@@ -0,0 +1,32 @@
1
+ <div id="inspector" class="bg-white w-full overflow-hidden flex flex-col" x-data="inspector" x-show="$screen('md')">
2
+ <div class="px-4 border-b border-gray-200 flex items-center flex-none select-none">
3
+ <nav class="-mb-px flex space-x-8 cursor-auto">
4
+ <% panes.each do |key, props| %>
5
+ <a
6
+ href="#<%= key %>"
7
+ class="whitespace-nowrap py-2 px-1 border-b-2 cursor-pointer <%= "!text-gray-300" if props[:disabled] %>"
8
+ :class="{
9
+ 'border-indigo-400': active('<%= key %>'),
10
+ 'border-transparent text-gray-500 hover:text-gray-700': !active('<%= key %>')
11
+ }"
12
+ @click.stop.prevent="switchTo('<%= key %>')"
13
+ <% if props[:hotkey] %>data-hotkey="<%= props[:hotkey] %>"<% end %>
14
+ >
15
+ <%== props[:label] %>
16
+ </a>
17
+ <% end %>
18
+ </nav>
19
+ </div>
20
+ <div class="flex-auto overflow-auto bg-gray-50">
21
+ <% panes.each do |key, props| %>
22
+ <div class="flex flex-col h-full relative" x-show="active('<%= key %>')" x-cloak>
23
+ <% if props[:clipboard].present? %>
24
+ <%= render "./shared/clipboard" do %><%= h props[:clipboard].strip %><% end %>
25
+ <% end %>
26
+ <div class="flex flex-col h-full overflow-auto">
27
+ <%= render "./workbench/inspector/#{props[:template]}", key: key, **props %>
28
+ </div>
29
+ </div>
30
+ <% end %>
31
+ </div>
32
+ </div>
@@ -0,0 +1,24 @@
1
+ <div id="preview" class="h-full md:h-auto md:min-h-0 flex w-full bg-gray-50">
2
+ <div class="relative mx-auto bg-white" x-data="preview" :style="`width: ${$screen('md') ? $store.preview.width : '100%'}`">
3
+ <iframe seamless
4
+ class="absolute h-full inset-0 w-full border-l border-gray-300 md:pr-4 md:-mx-px"
5
+ src="<%= url_for lookbook.preview_path %>"
6
+ <% if config.preview_srcdoc %>srcdoc="<%== @rendered_example %>"<% end %>
7
+ frameborder="0"
8
+ x-data="sizeObserver"
9
+ x-effect="previewViewportWidth = observedWidth; previewViewportHeight = observedHeight;"
10
+ ></iframe>
11
+ <div class="absolute opacity-0 inset-0 pointer-events-none" :class="{ 'pointer-events-none': !$store.page.reflowing }"></div>
12
+ <div
13
+ class="border-l border-r border-gray-300 bg-white hover:bg-indigo-100 hover:bg-opacity-20 transition absolute right-0 inset-y-0 flex items-center w-4 cursor-[col-resize] select-none"
14
+ style="touch-action: none"
15
+ @pointerdown="onResizeStart"
16
+ @dblclick="toggleFullWidth"
17
+ x-show="$screen('md')"
18
+ >
19
+ <svg class="h-4 w-4 text-gray-600 pointer-events-none" fill="currentColor" viewBox="0 0 24 24">
20
+ <path d="M8 5h2v14H8zM14 5h2v14h-2z"></path>
21
+ </svg>
22
+ </div>
23
+ </div>
24
+ </div>
@@ -0,0 +1,3 @@
1
+ <pre class="h-full"><code class="h-full p-4 block highlight font-monospace" style="background-image: none"><% items.each do |item| %><%== highlight( "#{items.many? ? item[:label] + "\n" : ""}#{item[:content].strip}", item[:lang][:name]) %>
2
+
3
+ <% end %></code></pre>
@@ -0,0 +1,24 @@
1
+ <div class="text-gray-600">
2
+ <% if items.many? %>
3
+ <div class="divide-y divide-gray-300">
4
+ <% items.each do |item| %>
5
+ <div class="px-4 pt-10 pb-8 relative">
6
+ <div class="prose prose-sm">
7
+ <%= markdown(item[:content]) %>
8
+ </div>
9
+ <h6 class="text-[11px] text-gray-600 inline-block px-2 py-0.25 bg-gray-100 border border-t-0 border-gray-300 absolute top-0 left-4 rounded-b">
10
+ <%= item[:label] %>
11
+ </h6>
12
+ </div>
13
+ <% end %>
14
+ </div>
15
+ <% else %>
16
+ <div class="p-4 prose prose-sm">
17
+ <% if items.any? %>
18
+ <%= markdown(items[0][:content]) %>
19
+ <% else %>
20
+ <em class='opacity-50'>No notes provided.</em>
21
+ <% end %>
22
+ </div>
23
+ <% end %>
24
+ </div>
data/config/routes.rb CHANGED
@@ -3,8 +3,8 @@ Lookbook::Engine.routes.draw do
3
3
  mount Lookbook::Engine.websocket => Lookbook::Engine.cable.mount_path
4
4
  end
5
5
 
6
- root to: "browser#index", as: :home
6
+ root to: "app#index", as: :home
7
7
 
8
- get "/preview/*path", to: "browser#preview", as: :preview
9
- get "/*path", to: "browser#show", as: :show
8
+ get "/preview/*path", to: "app#preview", as: :preview
9
+ get "/*path", to: "app#show", as: :show
10
10
  end
@@ -2,6 +2,10 @@ module Lookbook
2
2
  module Preview
3
3
  include Taggable
4
4
 
5
+ def id
6
+ lookbook_path.tr("_", "-")
7
+ end
8
+
5
9
  # Examples::FooBarComponent::Preview -> "Foo Bar"
6
10
  def lookbook_label
7
11
  super.presence || lookbook_path.split("/").last.titleize
@@ -19,9 +23,22 @@ module Lookbook
19
23
  return @lookbook_examples if @lookbook_examples.present?
20
24
  public_methods = public_instance_methods(false)
21
25
  public_method_objects = code_object.meths.filter { |m| public_methods.include?(m.name) }
22
- examples = public_method_objects.map { |m| PreviewExample.new(m.name.to_s, self) }
23
- examples.reject!(&:hidden?)
24
- @lookbook_examples ||= Lookbook.config.sort_examples ? examples.sort_by(&:label) : examples
26
+ visible = public_method_objects.map { |m| PreviewExample.new(m.name.to_s, self) }.reject(&:hidden?)
27
+ sorted = Lookbook.config.sort_examples ? visible.sort_by(&:label) : visible
28
+ @lookbook_examples = []
29
+ if code_object.groups.any?
30
+ sorted.group_by { |m| m.group }.each do |name, examples|
31
+ if name.nil?
32
+ @lookbook_examples += examples
33
+ else
34
+ name = lookbook_label if name.strip == ""
35
+ @lookbook_examples << PreviewGroup.new(name.underscore, self, examples)
36
+ end
37
+ end
38
+ else
39
+ @lookbook_examples = sorted
40
+ end
41
+ @lookbook_examples
25
42
  end
26
43
 
27
44
  # Examples::FooBarComponentPreview -> "Examples::FooBar"
@@ -57,6 +74,10 @@ module Lookbook
57
74
  lookbook_path.tr("_", "-")
58
75
  end
59
76
 
77
+ def lookbook_layout
78
+ defined?(@layout) ? @layout : nil
79
+ end
80
+
60
81
  class << self
61
82
  def all
62
83
  ViewComponent::Preview.all
@@ -69,6 +90,8 @@ module Lookbook
69
90
  def exists?(path)
70
91
  !!find(path)
71
92
  end
93
+
94
+
72
95
  end
73
96
 
74
97
  private
@@ -1,6 +1,6 @@
1
1
  module Lookbook
2
2
  module PreviewController
3
- def render_component_to_string(preview, example_name)
3
+ def render_example_to_string(preview, example_name)
4
4
  prepend_application_view_paths
5
5
  prepend_preview_examples_view_path
6
6
  @preview = preview
@@ -13,5 +13,10 @@ module Lookbook
13
13
  opts[:locals] = locals if locals.present?
14
14
  render_to_string template, opts
15
15
  end
16
+
17
+ def render_in_layout_to_string(content, layout_override)
18
+ layout = determine_layout(layout_override, prepend_views: false)[:layout]
19
+ render_to_string html: content, layout: layout
20
+ end
16
21
  end
17
22
  end
@@ -41,8 +41,8 @@ module Lookbook
41
41
  :example
42
42
  end
43
43
 
44
- def filter_match_string
45
- [*@preview.lookbook_parent_collections, @preview.label, label].join("/").gsub(/\s/, "").downcase
44
+ def matchers
45
+ [@preview.label, label].map { |m| m.gsub(/\s/, "").downcase }
46
46
  end
47
47
 
48
48
  def hierarchy_depth
@@ -62,6 +62,7 @@ module Lookbook
62
62
  Pathname.new(Dir["#{base_path}/#{template_path}.html.*"].first)
63
63
  end
64
64
 
65
+ alias_method :group, :lookbook_group
65
66
  alias_method :notes, :lookbook_notes
66
67
  alias_method :hidden?, :lookbook_hidden?
67
68
  end