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.
- 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>
|