lookbook 0.2.0

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 (48) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +257 -0
  4. data/app/assets/lookbook/css/app.css +37 -0
  5. data/app/assets/lookbook/css/code_theme.css +214 -0
  6. data/app/assets/lookbook/css/tooltip_theme.css +16 -0
  7. data/app/assets/lookbook/js/app.js +47 -0
  8. data/app/assets/lookbook/js/preview.js +76 -0
  9. data/app/assets/lookbook/js/reloader.js +28 -0
  10. data/app/assets/lookbook/js/size_observer.js +16 -0
  11. data/app/assets/lookbook/js/split.js +26 -0
  12. data/app/channels/lookbook/connection.rb +11 -0
  13. data/app/channels/lookbook/reload_channel.rb +7 -0
  14. data/app/controllers/lookbook/browser_controller.rb +141 -0
  15. data/app/helpers/lookbook/application_helper.rb +24 -0
  16. data/app/views/lookbook/browser/error.html.erb +1 -0
  17. data/app/views/lookbook/browser/index.html.erb +8 -0
  18. data/app/views/lookbook/browser/not_found.html.erb +25 -0
  19. data/app/views/lookbook/browser/show.html.erb +33 -0
  20. data/app/views/lookbook/layouts/app.html.erb +49 -0
  21. data/app/views/lookbook/partials/_icon_sprite.html.erb +1 -0
  22. data/app/views/lookbook/partials/_preview.html.erb +18 -0
  23. data/app/views/lookbook/partials/_sidebar.html.erb +21 -0
  24. data/app/views/lookbook/partials/inspector/_code.html.erb +1 -0
  25. data/app/views/lookbook/partials/inspector/_inspector.html.erb +43 -0
  26. data/app/views/lookbook/partials/inspector/_plain.html.erb +3 -0
  27. data/app/views/lookbook/partials/inspector/_prose.html.erb +3 -0
  28. data/app/views/lookbook/partials/nav/_collection.html.erb +17 -0
  29. data/app/views/lookbook/partials/nav/_label.html.erb +13 -0
  30. data/app/views/lookbook/partials/nav/_nav.html.erb +27 -0
  31. data/app/views/lookbook/partials/nav/_preview.html.erb +48 -0
  32. data/config/lookbook_cable.yml +8 -0
  33. data/config/routes.rb +8 -0
  34. data/lib/lookbook.rb +14 -0
  35. data/lib/lookbook/collection.rb +51 -0
  36. data/lib/lookbook/engine.rb +96 -0
  37. data/lib/lookbook/lang.rb +42 -0
  38. data/lib/lookbook/navigation.rb +68 -0
  39. data/lib/lookbook/parser.rb +33 -0
  40. data/lib/lookbook/preview.rb +86 -0
  41. data/lib/lookbook/preview_controller.rb +17 -0
  42. data/lib/lookbook/preview_example.rb +68 -0
  43. data/lib/lookbook/taggable.rb +23 -0
  44. data/lib/lookbook/version.rb +3 -0
  45. data/lib/tasks/lookbook_tasks.rake +15 -0
  46. data/public/lookbook-assets/app.css +2361 -0
  47. data/public/lookbook-assets/app.js +7692 -0
  48. metadata +242 -0
@@ -0,0 +1,16 @@
1
+ .tippy-box[data-theme~="lookbook"] {
2
+ @apply bg-indigo-500 text-white text-xs opacity-90;
3
+
4
+ &[data-placement^="top"] > .tippy-arrow::before {
5
+ border-top-color: theme("colors.indigo.500");
6
+ }
7
+ &[data-placement^="bottom"] > .tippy-arrow::before {
8
+ border-bottom-color: theme("colors.indigo.500");
9
+ }
10
+ &[data-placement^="left"] > .tippy-arrow::before {
11
+ border-left-color: theme("colors.indigo.500");
12
+ }
13
+ &[data-placement^="right"] > .tippy-arrow::before {
14
+ border-right-color: theme("colors.indigo.500");
15
+ }
16
+ }
@@ -0,0 +1,47 @@
1
+ import Alpine from "alpinejs";
2
+ import Fern from "@ryangjchandler/fern";
3
+ import Tooltip from "@ryangjchandler/alpine-tooltip";
4
+ import Clipboard from "@ryangjchandler/alpine-clipboard";
5
+ import split from "./split";
6
+ import preview from "./preview";
7
+ import observeSize from "./size_observer";
8
+ import reloader from "./reloader";
9
+
10
+ // Plugins
11
+
12
+ Alpine.plugin(Fern);
13
+ Alpine.plugin(Tooltip);
14
+ Alpine.plugin(Clipboard);
15
+
16
+ // Data
17
+
18
+ Alpine.data("preview", preview);
19
+ Alpine.data("sizeObserver", observeSize);
20
+ Alpine.data("split", split);
21
+
22
+ // Stores
23
+
24
+ Alpine.store("app", { reflowing: false });
25
+ Alpine.persistedStore("nav", {
26
+ width: 280,
27
+ filter: "",
28
+ open: {},
29
+ scrollTop: 0,
30
+ shouldDisplay(previewName) {
31
+ const cleanFilter = this.filter.replace(/\s/g, "");
32
+ return (
33
+ cleanFilter === "" || previewName.includes(cleanFilter.toLowerCase())
34
+ );
35
+ },
36
+ });
37
+ Alpine.persistedStore("preview", {});
38
+ Alpine.persistedStore("inspector", {
39
+ height: 200,
40
+ active: "source",
41
+ });
42
+
43
+ // Init
44
+
45
+ window.Alpine = Alpine;
46
+ reloader(window.SOCKET_PATH).start();
47
+ Alpine.start();
@@ -0,0 +1,76 @@
1
+ export default function preview() {
2
+ const app = Alpine.store("app");
3
+ const preview = Alpine.store("preview");
4
+ return {
5
+ init() {
6
+ this.root = this.$el;
7
+ },
8
+ onResize(e) {
9
+ const size =
10
+ this.resizeStartSize - (this.resizeStartPosition - e.pageX) * 2;
11
+ const parentSize = this.root.parentElement.clientWidth;
12
+ const percentSize = (Math.round(size) / parentSize) * 100;
13
+ const minWidth = (300 / parentSize) * 100;
14
+ preview.width = `${Math.min(Math.max(percentSize, minWidth), 100)}%`;
15
+ },
16
+ onResizeStart(e) {
17
+ app.reflowing = true;
18
+ this.onResize = this.onResize.bind(this);
19
+ this.onResizeEnd = this.onResizeEnd.bind(this);
20
+ this.resizeStartPosition = e.pageX;
21
+ this.resizeStartSize = this.root.clientWidth;
22
+ window.addEventListener("pointermove", this.onResize);
23
+ window.addEventListener("pointerup", this.onResizeEnd);
24
+ },
25
+ onResizeEnd(e) {
26
+ window.removeEventListener("pointermove", this.onResize);
27
+ window.removeEventListener("pointerup", this.onResizeEnd);
28
+ app.reflowing = false;
29
+ },
30
+ handle: {
31
+ ["@pointerdown"]: "onResizeStart",
32
+ ["@dblclick"]() {
33
+ if (preview.width === "100%" && preview.lastWidth) {
34
+ preview.width = preview.lastWidth;
35
+ } else {
36
+ preview.lastWidth = preview.width;
37
+ preview.width = "100%";
38
+ }
39
+ },
40
+ },
41
+ };
42
+ }
43
+
44
+ // export default function (dimension, store, { shrink = false, centered = false } = {}) {
45
+ // const position = (e) => (dimension == "height" ? e.pageY : e.pageX);
46
+ // const pane = {
47
+ // onResize(e) {
48
+ // let size =
49
+ // this.resizeStartSize -
50
+ // (shrink
51
+ // ? (this.resizeStartPosition - position(e)) * (centered ? 2 : 1)
52
+ // : (position(e) - this.resizeStartPosition) * (centered ? 2 : 1));
53
+ // const parentSize =
54
+ // dimension == "height"
55
+ // ? this.$el.parentElement.clientHeight
56
+ // : this.$el.parentElement.clientWidth;
57
+ // const percentSize = (Math.round(size) / parentSize) * 100;
58
+ // store[dimension] = `${Math.min(Math.max(percentSize, 0), 100)}%`;
59
+ // },
60
+ // onResizeStart(e) {
61
+ // Spruce.store("app").reflowing = true;
62
+ // this.resizeStartPosition = position(e);
63
+ // this.resizeStartSize = dimension == "height" ? this.$el.clientHeight : this.$el.clientWidth;
64
+ // this.onResize = this.onResize.bind(this);
65
+ // this.onResizeEnd = this.onResizeEnd.bind(this);
66
+ // window.addEventListener("pointermove", this.onResize);
67
+ // window.addEventListener("pointerup", this.onResizeEnd);
68
+ // },
69
+ // onResizeEnd() {
70
+ // Spruce.store("app").reflowing = false;
71
+ // window.removeEventListener("pointermove", this.onResize);
72
+ // window.removeEventListener("pointerup", this.onResizeEnd);
73
+ // },
74
+ // };
75
+ // return pane;
76
+ // };
@@ -0,0 +1,28 @@
1
+ import { createConsumer } from "@rails/actioncable";
2
+ import debounce from "debounce";
3
+
4
+ export default function (endpoint) {
5
+ const uid = (Date.now() + ((Math.random() * 100) | 0)).toString();
6
+ const consumer = createConsumer(`${endpoint}?uid=${uid}`);
7
+
8
+ return {
9
+ uid,
10
+ consumer,
11
+ start() {
12
+ const received = debounce(() => {
13
+ console.log("Lookbook files changed");
14
+ document.dispatchEvent(new CustomEvent("refresh"));
15
+ }, 300);
16
+
17
+ consumer.subscriptions.create("Lookbook::ReloadChannel", {
18
+ received,
19
+ connected() {
20
+ console.log("Lookbook websocket connected");
21
+ },
22
+ disconnected() {
23
+ console.log("Lookbook websocket disconnected");
24
+ },
25
+ });
26
+ },
27
+ };
28
+ }
@@ -0,0 +1,16 @@
1
+ export default function () {
2
+ return {
3
+ observedWidth: 0,
4
+ observedHeight: 0,
5
+ init() {
6
+ const ro = new ResizeObserver((entries) => {
7
+ const rect = entries[0].contentRect;
8
+ this.observedWidth = Math.round(rect.width);
9
+ this.observedHeight = Math.round(rect.height);
10
+ });
11
+ ro.observe(this.$el);
12
+ this.observedWidth = Math.round(this.$el.clientWidth);
13
+ this.observedHeight = Math.round(this.$el.clientHeight);
14
+ },
15
+ };
16
+ }
@@ -0,0 +1,26 @@
1
+ import Split from "split-grid";
2
+
3
+ export default function (props) {
4
+ const app = Alpine.store("app");
5
+ return {
6
+ init() {
7
+ Split({
8
+ [`${props.direction === "vertical" ? "row" : "column"}Gutters`]: [
9
+ { track: 1, element: this.$el },
10
+ ],
11
+ minSize: props.minSize,
12
+ writeStyle() {},
13
+ onDrag(dir, track, style) {
14
+ splits = style.split(" ").map((num) => parseInt(num));
15
+ props.onDrag(splits);
16
+ },
17
+ onDragStart() {
18
+ app.reflowing = true;
19
+ },
20
+ onDragEnd() {
21
+ app.reflowing = false;
22
+ },
23
+ });
24
+ },
25
+ };
26
+ }
@@ -0,0 +1,11 @@
1
+ module Lookbook
2
+ class Connection < ActionCable::Connection::Base
3
+ identified_by :uid
4
+
5
+ def connect
6
+ self.uid = request.params[:uid]
7
+ logger.add_tags(uid)
8
+ logger.info "connected to Lookbook"
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,7 @@
1
+ module Lookbook
2
+ class ReloadChannel < ActionCable::Channel::Base
3
+ def subscribed
4
+ stream_from "reload"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,141 @@
1
+ module Lookbook
2
+ class BrowserController < ActionController::Base
3
+ EXCEPTIONS = [ViewComponent::PreviewTemplateError, ViewComponent::ComponentError, ViewComponent::TemplateError, ActionView::Template::Error]
4
+
5
+ protect_from_forgery with: :exception
6
+ prepend_view_path File.expand_path("../../views/lookbook", __dir__)
7
+
8
+ layout "layouts/app"
9
+ helper Lookbook::Engine.helpers
10
+
11
+ before_action :find_preview, only: [:preview, :show]
12
+ before_action :find_example, only: [:preview, :show]
13
+ before_action :assign_nav, only: [:index, :show]
14
+
15
+ def index
16
+ end
17
+
18
+ def preview
19
+ if @example
20
+ render html: preview_output
21
+ else
22
+ render "browser/not_found"
23
+ end
24
+ end
25
+
26
+ def show
27
+ if @example
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])
39
+ end
40
+ assign_inspector
41
+ rescue *EXCEPTIONS
42
+ render "browser/error"
43
+ end
44
+ else
45
+ render "browser/not_found"
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ def find_preview
52
+ candidates = []
53
+ params[:path].to_s.scan(%r{/|$}) { candidates << $` }
54
+ match = candidates.detect { |candidate| Lookbook::Preview.exists?(candidate) }
55
+ @preview = match ? Lookbook::Preview.find(match) : nil
56
+ end
57
+
58
+ def find_example
59
+ @example = if @preview
60
+ if params[:path] == @preview.lookbook_path
61
+ redirect_to show_path "#{params[:path]}/#{@preview.lookbook_examples.first.name}"
62
+ else
63
+ @example_name = File.basename(params[:path])
64
+ @preview.lookbook_example(@example_name)
65
+ end
66
+ end
67
+ end
68
+
69
+ def using_preview_template?
70
+ @render_args[:template] == "view_components/preview"
71
+ end
72
+
73
+ def preview_output
74
+ @preview_output ||= if @preview
75
+ preview_controller.request.params[:path] = "#{@preview.preview_name}/#{@example.name}".chomp("/")
76
+ preview_controller.process(:previews)
77
+ end
78
+ end
79
+
80
+ def assign_inspector
81
+ @inspector = {
82
+ panes: {
83
+ source: {
84
+ label: "Source",
85
+ content: @source || "",
86
+ template: "code",
87
+ lang: @source_lang,
88
+ clipboard: @source
89
+ },
90
+ output: {
91
+ label: "Output",
92
+ content: @render_output || "",
93
+ template: "code",
94
+ lang: @render_output_lang,
95
+ clipboard: @render_output
96
+ },
97
+ notes: {
98
+ label: "Notes",
99
+ content: @example.notes.presence || "<em class='opacity-50'>No notes provided.</em>",
100
+ template: "prose",
101
+ disabled: @example.notes.blank?
102
+ }
103
+ }
104
+ }
105
+ end
106
+
107
+ def assign_nav
108
+ @nav = Collection.new
109
+ previews.reject { |p| p.hidden? }.each do |preview|
110
+ current = @nav
111
+ if preview.hierarchy_depth == 1
112
+ current.add(preview)
113
+ else
114
+ preview.lookbook_parent_collections.each.with_index(1) do |name, i|
115
+ target = current.get_or_create(name)
116
+ if preview.hierarchy_depth == i + 1
117
+ target.add(preview)
118
+ else
119
+ current = target
120
+ end
121
+ end
122
+ end
123
+ end
124
+ @nav
125
+ end
126
+
127
+ def previews
128
+ Lookbook::Preview.all
129
+ end
130
+
131
+ def preview_controller
132
+ return @preview_controller if @preview_controller.present?
133
+ controller_class = Lookbook.config.preview_controller.constantize
134
+ controller_class.class_eval { include Lookbook::PreviewController }
135
+ controller = controller_class.new
136
+ controller.request = request
137
+ controller.response = response
138
+ @preview_controller ||= controller
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,24 @@
1
+ require "redcarpet"
2
+ require "rouge"
3
+
4
+ module Lookbook
5
+ module ApplicationHelper
6
+ def config
7
+ Lookbook::Engine.config.lookbook
8
+ end
9
+
10
+ def markdown(text)
11
+ Markdown.new(text).to_html.html_safe
12
+ end
13
+
14
+ def highlight(source, language)
15
+ formatter = Rouge::Formatters::HTML.new(css_class: "highlight")
16
+ lexer = Rouge::Lexer.find(language)
17
+ formatter.format(lexer.lex(source)).html_safe
18
+ end
19
+
20
+ def nav_padding_style(depth)
21
+ "padding-left: calc((#{depth} * 12px) + 0.5rem);"
22
+ end
23
+ end
24
+ end
@@ -0,0 +1 @@
1
+ <iframe src="<%= url_for lookbook.preview_path %>" frameborder="0" class=" h-screen w-full" seamless></iframe>
@@ -0,0 +1,8 @@
1
+ <div class="flex flex-col items-center justify-center h-screen w-full">
2
+ <div class="p-4 text-center">
3
+ <svg class="feather w-10 h-10 text-gray-300 mx-auto">
4
+ <use xlink:href="#layers" />
5
+ </svg>
6
+ <h5 class="mt-4 text-gray-400 text-base">Select a preview to get started</h5>
7
+ </div>
8
+ </div>
@@ -0,0 +1,25 @@
1
+ <div class="bg-white flex flex-col items-center justify-center h-screen w-full">
2
+ <div class="p-4 text-center">
3
+ <svg class="feather w-10 h-10 text-red-300 mx-auto">
4
+ <use xlink:href="#alert-triangle" />
5
+ </svg>
6
+ <div class="mt-3 text-gray-700 max-w-xs">
7
+ <% if @preview %>
8
+ <div data-role="example-not-found">
9
+ <h5 class="text-base">Not found</h5>
10
+ <p class="mt-2 text-gray-400 text-sm">
11
+ The "<span class="text-gray-500"><%= @preview.lookbook_label %></span>" preview does not have an example named "<span class="text-gray-500"><%= @example_name %></span>".
12
+ </p>
13
+ </div>
14
+ <% else %>
15
+ <div data-role="preview-not-found">
16
+ <h5 class="text-base">Preview not found</h5>
17
+ <p class="mt-2 text-gray-400 text-sm">
18
+ Looked for "<span class="text-gray-500"><%= params[:path] %></span>".<br>
19
+ The preview may have been renamed or deleted.
20
+ </p>
21
+ </div>
22
+ <% end %>
23
+ </div>
24
+ </div>
25
+ </div>