lookbook 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +257 -0
- data/app/assets/lookbook/css/app.css +37 -0
- data/app/assets/lookbook/css/code_theme.css +214 -0
- data/app/assets/lookbook/css/tooltip_theme.css +16 -0
- data/app/assets/lookbook/js/app.js +47 -0
- data/app/assets/lookbook/js/preview.js +76 -0
- data/app/assets/lookbook/js/reloader.js +28 -0
- data/app/assets/lookbook/js/size_observer.js +16 -0
- data/app/assets/lookbook/js/split.js +26 -0
- data/app/channels/lookbook/connection.rb +11 -0
- data/app/channels/lookbook/reload_channel.rb +7 -0
- data/app/controllers/lookbook/browser_controller.rb +141 -0
- data/app/helpers/lookbook/application_helper.rb +24 -0
- data/app/views/lookbook/browser/error.html.erb +1 -0
- data/app/views/lookbook/browser/index.html.erb +8 -0
- data/app/views/lookbook/browser/not_found.html.erb +25 -0
- data/app/views/lookbook/browser/show.html.erb +33 -0
- data/app/views/lookbook/layouts/app.html.erb +49 -0
- data/app/views/lookbook/partials/_icon_sprite.html.erb +1 -0
- data/app/views/lookbook/partials/_preview.html.erb +18 -0
- data/app/views/lookbook/partials/_sidebar.html.erb +21 -0
- data/app/views/lookbook/partials/inspector/_code.html.erb +1 -0
- data/app/views/lookbook/partials/inspector/_inspector.html.erb +43 -0
- data/app/views/lookbook/partials/inspector/_plain.html.erb +3 -0
- data/app/views/lookbook/partials/inspector/_prose.html.erb +3 -0
- data/app/views/lookbook/partials/nav/_collection.html.erb +17 -0
- data/app/views/lookbook/partials/nav/_label.html.erb +13 -0
- data/app/views/lookbook/partials/nav/_nav.html.erb +27 -0
- data/app/views/lookbook/partials/nav/_preview.html.erb +48 -0
- data/config/lookbook_cable.yml +8 -0
- data/config/routes.rb +8 -0
- data/lib/lookbook.rb +14 -0
- data/lib/lookbook/collection.rb +51 -0
- data/lib/lookbook/engine.rb +96 -0
- data/lib/lookbook/lang.rb +42 -0
- data/lib/lookbook/navigation.rb +68 -0
- data/lib/lookbook/parser.rb +33 -0
- data/lib/lookbook/preview.rb +86 -0
- data/lib/lookbook/preview_controller.rb +17 -0
- data/lib/lookbook/preview_example.rb +68 -0
- data/lib/lookbook/taggable.rb +23 -0
- data/lib/lookbook/version.rb +3 -0
- data/lib/tasks/lookbook_tasks.rake +15 -0
- data/public/lookbook-assets/app.css +2361 -0
- data/public/lookbook-assets/app.js +7692 -0
- 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,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("\"", """)
|
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>
|