proscenium 0.16.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/CODE_OF_CONDUCT.md +84 -0
- data/LICENSE.txt +21 -0
- data/README.md +908 -0
- data/lib/proscenium/builder.rb +189 -0
- data/lib/proscenium/core_ext/object/css_module_ivars.rb +19 -0
- data/lib/proscenium/css_module/path.rb +31 -0
- data/lib/proscenium/css_module/rewriter.rb +44 -0
- data/lib/proscenium/css_module/transformer.rb +84 -0
- data/lib/proscenium/css_module.rb +57 -0
- data/lib/proscenium/ensure_loaded.rb +27 -0
- data/lib/proscenium/ext/proscenium +0 -0
- data/lib/proscenium/ext/proscenium.h +131 -0
- data/lib/proscenium/helper.rb +70 -0
- data/lib/proscenium/importer.rb +134 -0
- data/lib/proscenium/libs/custom_element.js +54 -0
- data/lib/proscenium/libs/react-manager/index.jsx +121 -0
- data/lib/proscenium/libs/react-manager/react.js +2 -0
- data/lib/proscenium/libs/stimulus-loading.js +65 -0
- data/lib/proscenium/libs/test.js +1 -0
- data/lib/proscenium/libs/ujs/class.js +15 -0
- data/lib/proscenium/libs/ujs/data_confirm.js +23 -0
- data/lib/proscenium/libs/ujs/data_disable_with.js +68 -0
- data/lib/proscenium/libs/ujs/index.js +9 -0
- data/lib/proscenium/log_subscriber.rb +37 -0
- data/lib/proscenium/middleware/base.rb +103 -0
- data/lib/proscenium/middleware/engines.rb +45 -0
- data/lib/proscenium/middleware/esbuild.rb +30 -0
- data/lib/proscenium/middleware/runtime.rb +18 -0
- data/lib/proscenium/middleware/url.rb +16 -0
- data/lib/proscenium/middleware.rb +76 -0
- data/lib/proscenium/monkey.rb +95 -0
- data/lib/proscenium/phlex/asset_inclusions.rb +17 -0
- data/lib/proscenium/phlex/css_modules.rb +79 -0
- data/lib/proscenium/phlex/react_component.rb +32 -0
- data/lib/proscenium/phlex.rb +42 -0
- data/lib/proscenium/railtie.rb +106 -0
- data/lib/proscenium/react_componentable.rb +95 -0
- data/lib/proscenium/resolver.rb +39 -0
- data/lib/proscenium/side_load.rb +155 -0
- data/lib/proscenium/source_path.rb +15 -0
- data/lib/proscenium/templates/rescues/build_error.html.erb +30 -0
- data/lib/proscenium/ui/breadcrumbs/component.module.css +14 -0
- data/lib/proscenium/ui/breadcrumbs/component.rb +79 -0
- data/lib/proscenium/ui/breadcrumbs/computed_element.rb +69 -0
- data/lib/proscenium/ui/breadcrumbs/control.rb +95 -0
- data/lib/proscenium/ui/breadcrumbs/mixins.css +83 -0
- data/lib/proscenium/ui/breadcrumbs.rb +72 -0
- data/lib/proscenium/ui/component.rb +11 -0
- data/lib/proscenium/ui/test.js +1 -0
- data/lib/proscenium/ui.rb +14 -0
- data/lib/proscenium/utils.rb +13 -0
- data/lib/proscenium/version.rb +5 -0
- data/lib/proscenium/view_component/css_modules.rb +11 -0
- data/lib/proscenium/view_component/react_component.rb +22 -0
- data/lib/proscenium/view_component/sideload.rb +4 -0
- data/lib/proscenium/view_component.rb +38 -0
- data/lib/proscenium.rb +70 -0
- metadata +228 -0
@@ -0,0 +1,155 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Proscenium
|
4
|
+
class SideLoad
|
5
|
+
module Controller
|
6
|
+
def self.included(child)
|
7
|
+
child.class_eval do
|
8
|
+
class_attribute :sideload_assets_options
|
9
|
+
child.extend ClassMethods
|
10
|
+
|
11
|
+
append_after_action :capture_and_replace_proscenium_stylesheets,
|
12
|
+
:capture_and_replace_proscenium_javascripts,
|
13
|
+
if: -> { response.content_type&.include?('html') }
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
module ClassMethods
|
18
|
+
def sideload_assets(value)
|
19
|
+
self.sideload_assets_options = value
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def capture_and_replace_proscenium_stylesheets
|
24
|
+
return if response_body.nil?
|
25
|
+
return if response_body.first.blank? || !Proscenium::Importer.css_imported?
|
26
|
+
return unless response_body.first.include? '<!-- [PROSCENIUM_STYLESHEETS] -->'
|
27
|
+
|
28
|
+
imports = Proscenium::Importer.imported.dup
|
29
|
+
paths_to_build = []
|
30
|
+
Proscenium::Importer.each_stylesheet(delete: true) do |x, _|
|
31
|
+
paths_to_build << x.delete_prefix('/')
|
32
|
+
end
|
33
|
+
|
34
|
+
result = Proscenium::Builder.build_to_path(paths_to_build.join(';'),
|
35
|
+
base_url: helpers.request.base_url)
|
36
|
+
|
37
|
+
out = []
|
38
|
+
result.split(';').each do |x|
|
39
|
+
inpath, outpath = x.split('::')
|
40
|
+
inpath.prepend '/'
|
41
|
+
outpath.delete_prefix! 'public'
|
42
|
+
|
43
|
+
next unless imports.key?(inpath)
|
44
|
+
|
45
|
+
import = imports[inpath]
|
46
|
+
opts = import[:css].is_a?(Hash) ? import[:css] : {}
|
47
|
+
opts[:data] ||= {}
|
48
|
+
opts[:data][:original_href] = inpath
|
49
|
+
out << helpers.stylesheet_link_tag(outpath, extname: false, **opts)
|
50
|
+
end
|
51
|
+
|
52
|
+
response_body.first.gsub! '<!-- [PROSCENIUM_STYLESHEETS] -->', out.join.html_safe
|
53
|
+
end
|
54
|
+
|
55
|
+
def capture_and_replace_proscenium_javascripts
|
56
|
+
return if response_body.nil?
|
57
|
+
return if response_body.first.blank? || !Proscenium::Importer.js_imported?
|
58
|
+
|
59
|
+
imports = Proscenium::Importer.imported.dup
|
60
|
+
paths_to_build = []
|
61
|
+
Proscenium::Importer.each_javascript(delete: true) do |x, _|
|
62
|
+
paths_to_build << x.delete_prefix('/')
|
63
|
+
end
|
64
|
+
|
65
|
+
result = Proscenium::Builder.build_to_path(paths_to_build.join(';'),
|
66
|
+
base_url: helpers.request.base_url)
|
67
|
+
|
68
|
+
if response_body.first.include? '<!-- [PROSCENIUM_JAVASCRIPTS] -->'
|
69
|
+
out = []
|
70
|
+
scripts = {}
|
71
|
+
result.split(';').each do |x|
|
72
|
+
inpath, outpath = x.split('::')
|
73
|
+
inpath.prepend '/'
|
74
|
+
outpath.delete_prefix! 'public'
|
75
|
+
|
76
|
+
next unless imports.key?(inpath)
|
77
|
+
|
78
|
+
if (import = imports[inpath]).delete(:lazy)
|
79
|
+
scripts[inpath] = import.merge(outpath:)
|
80
|
+
else
|
81
|
+
opts = import[:js].is_a?(Hash) ? import[:js] : {}
|
82
|
+
out << helpers.javascript_include_tag(outpath, extname: false, **opts)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
response_body.first.gsub! '<!-- [PROSCENIUM_JAVASCRIPTS] -->', out.join.html_safe
|
87
|
+
end
|
88
|
+
|
89
|
+
return unless response_body.first.include? '<!-- [PROSCENIUM_LAZY_SCRIPTS] -->'
|
90
|
+
|
91
|
+
lazy_script = ''
|
92
|
+
if scripts.present?
|
93
|
+
lazy_script = helpers.content_tag 'script', type: 'application/json',
|
94
|
+
id: 'prosceniumLazyScripts' do
|
95
|
+
scripts.to_json.html_safe
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
response_body.first.gsub! '<!-- [PROSCENIUM_LAZY_SCRIPTS] -->', lazy_script
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
class << self
|
104
|
+
# Side loads the class, and its super classes that respond to `.source_path`.
|
105
|
+
#
|
106
|
+
# Set the `abstract_class` class variable to true in any class, and it will not be side
|
107
|
+
# loaded.
|
108
|
+
#
|
109
|
+
# If the class responds to `.sideload`, it will be called instead of the regular side loading.
|
110
|
+
# You can use this to customise what is side loaded.
|
111
|
+
def sideload_inheritance_chain(obj, options)
|
112
|
+
return unless Proscenium.config.side_load
|
113
|
+
|
114
|
+
options = {} if options.nil?
|
115
|
+
options = { js: options, css: options } unless options.is_a?(Hash)
|
116
|
+
|
117
|
+
unless obj.sideload_assets_options.nil?
|
118
|
+
tpl_options = obj.sideload_assets_options
|
119
|
+
options = if tpl_options.is_a?(Hash)
|
120
|
+
options.deep_merge tpl_options
|
121
|
+
else
|
122
|
+
{ js: tpl_options, css: tpl_options }
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
%i[css js].each do |k|
|
127
|
+
options[k] = obj.instance_eval(&options[k]) if options[k].is_a?(Proc)
|
128
|
+
end
|
129
|
+
|
130
|
+
css_imports = []
|
131
|
+
|
132
|
+
klass = obj.class
|
133
|
+
while klass.respond_to?(:source_path) && klass.source_path && !klass.abstract_class
|
134
|
+
if klass.respond_to?(:sideload)
|
135
|
+
klass.sideload options
|
136
|
+
elsif options[:css] == false
|
137
|
+
Importer.sideload klass.source_path, **options
|
138
|
+
else
|
139
|
+
Importer.sideload_js klass.source_path, **options
|
140
|
+
css_imports << klass.source_path
|
141
|
+
end
|
142
|
+
|
143
|
+
klass = klass.superclass
|
144
|
+
end
|
145
|
+
|
146
|
+
# The reason why we sideload CSS after JS is because the order of CSS is important.
|
147
|
+
# Basically, the layout should be loaded before the view so that CSS cascading works i9n the
|
148
|
+
# right direction.
|
149
|
+
css_imports.reverse_each do |it|
|
150
|
+
Importer.sideload_css it, **options
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Include this into any class to expose a `source_path` class and instance method, which will return
|
4
|
+
# the absolute file system path to the current object.
|
5
|
+
module Proscenium::SourcePath
|
6
|
+
def self.included(base)
|
7
|
+
base.extend ClassMethods
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
def source_path
|
12
|
+
@source_path ||= name.nil? ? nil : Pathname.new(const_source_location(name).first)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
<header>
|
2
|
+
<h1>
|
3
|
+
<%= Rails.gem_version >= Gem::Version.new('7.1.0') ? @exception_wrapper.exception_class_name : @exception.class.to_s %>
|
4
|
+
<% if params_valid? && @request.parameters['controller'] %>
|
5
|
+
in <%= @request.parameters['controller'].camelize %>Controller<% if @request.parameters['action'] %>#<%= @request.parameters['action'] %><% end %>
|
6
|
+
<% end %>
|
7
|
+
</h1>
|
8
|
+
</header>
|
9
|
+
|
10
|
+
<main role="main" id="container">
|
11
|
+
<%= render "rescues/message_and_suggestions", exception: @exception, exception_wrapper: Rails.gem_version >= Gem::Version.new('7.1.0') ? @exception_wrapper : nil %>
|
12
|
+
|
13
|
+
<% if @exception.error['location'] %>
|
14
|
+
<div class="source">
|
15
|
+
<div class="data">
|
16
|
+
<pre>
|
17
|
+
|
18
|
+
<%= @exception.error['location']['file'] %>:<%= @exception.error['location']['line'] %>:<%= @exception.error['location']['column'] %>
|
19
|
+
|
20
|
+
<%= @exception.error['location']['line'].to_s.rjust 5 %> │ <%= @exception.error['location']['line_text'] %>
|
21
|
+
│ <%= (@exception.error['location']['length'] > 1 ? "~" * @exception.error['location']['length'] : "^").rjust(@exception.error['location']['column'] + @exception.error['location']['length']) %>
|
22
|
+
<%- if @exception.error['location']['suggestion'].present? -%> + │ <%= @exception.error['location']['suggestion'].rjust(@exception.error['location']['column'] + 1) %>
|
23
|
+
<% else %> <%- end -%>
|
24
|
+
</pre>
|
25
|
+
</div>
|
26
|
+
</div>
|
27
|
+
<% end %>
|
28
|
+
|
29
|
+
<%= render template: "rescues/_request_and_response" %>
|
30
|
+
</main>
|
@@ -0,0 +1,14 @@
|
|
1
|
+
@layer proscenium-ui-component {
|
2
|
+
/*
|
3
|
+
* Custom properties:
|
4
|
+
*
|
5
|
+
* --puiBreadcrumbs--link-color: LinkText;
|
6
|
+
* --puiBreadcrumbs--link-hover-color: HighlightText;
|
7
|
+
* --puiBreadcrumbs--separator-color: GrayText;
|
8
|
+
* --puiBreadcrumbs--separator: url("/proscenium/icons/angle-right-regular.svg");
|
9
|
+
*/
|
10
|
+
|
11
|
+
.base {
|
12
|
+
@mixin breadcrumbs from url("./mixins.css");
|
13
|
+
}
|
14
|
+
}
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Proscenium::UI
|
4
|
+
class Breadcrumbs::Component < Component
|
5
|
+
include Phlex::Rails::Helpers::URLFor
|
6
|
+
|
7
|
+
# The path (route) to use as the HREF for the home segment. Defaults to `:root`.
|
8
|
+
option :home_path, Types::String | Types::Symbol, default: -> { :root }
|
9
|
+
|
10
|
+
# Assign false to hide the home segment.
|
11
|
+
option :with_home, Types::Bool, default: -> { true }
|
12
|
+
|
13
|
+
# One or more class name(s) for the base div element which will be appended to the default.
|
14
|
+
option :class, Types::Coercible::String | Types::Array.of(Types::Coercible::String),
|
15
|
+
as: :class_name, default: -> { [] }
|
16
|
+
|
17
|
+
# One or more class name(s) for the base div element which will replace the default. If both
|
18
|
+
# `class` and `class!` are provided, all values will be merged. Defaults to `:@base`.
|
19
|
+
option :class!, Types::Coercible::String | Types::Array.of(Types::Coercible::String),
|
20
|
+
as: :class_name_override, default: -> { :@base }
|
21
|
+
|
22
|
+
def view_template
|
23
|
+
div class: [*class_name_override, *class_name] do
|
24
|
+
ol do
|
25
|
+
if with_home
|
26
|
+
li do
|
27
|
+
home_template
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
breadcrumbs.each do |ce|
|
32
|
+
li do
|
33
|
+
path = ce.path
|
34
|
+
path.nil? ? ce.name : a(href: url_for(path)) { ce.name }
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
# Override this to customise the home breadcrumb. You can call super with a block to use the
|
44
|
+
# default template, but with custom content.
|
45
|
+
#
|
46
|
+
# @example
|
47
|
+
# def home_template
|
48
|
+
# super { 'hello' }
|
49
|
+
# end
|
50
|
+
def home_template(&block)
|
51
|
+
a(href: url_for(home_path)) do
|
52
|
+
if block
|
53
|
+
yield
|
54
|
+
else
|
55
|
+
svg role: 'img', xmlns: 'http://www.w3.org/2000/svg', viewBox: '0 0 576 512' do |s|
|
56
|
+
s.path fill: 'currentColor',
|
57
|
+
d: 'M488 312.7V456c0 13.3-10.7 24-24 24H348c-6.6 0-12-5.4-12-12V356c0-6.6-5.4-' \
|
58
|
+
'12-12-12h-72c-6.6 0-12 5.4-12 12v112c0 6.6-5.4 12-12 12H112c-13.3 0-24-10.' \
|
59
|
+
'7-24-24V312.7c0-3.6 1.6-7 4.4-9.3l188-154.8c4.4-3.6 10.8-3.6 15.3 0l188 15' \
|
60
|
+
'4.8c2.7 2.3 4.3 5.7 4.3 9.3zm83.6-60.9L488 182.9V44.4c0-6.6-5.4-12-12-12h-' \
|
61
|
+
'56c-6.6 0-12 5.4-12 12V117l-89.5-73.7c-17.7-14.6-43.3-14.6-61 0L4.4 251.8c' \
|
62
|
+
'-5.1 4.2-5.8 11.8-1.6 16.9l25.5 31c4.2 5.1 11.8 5.8 16.9 1.6l235.2-193.7c4' \
|
63
|
+
'.4-3.6 10.8-3.6 15.3 0l235.2 193.7c5.1 4.2 12.7 3.5 16.9-1.6l25.5-31c4.2-5' \
|
64
|
+
'.2 3.4-12.7-1.7-16.9z'
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Don't render if @hide_breadcrumbs is true.
|
71
|
+
def render?
|
72
|
+
helpers.assigns['hide_breadcrumbs'] != true
|
73
|
+
end
|
74
|
+
|
75
|
+
def breadcrumbs
|
76
|
+
helpers.controller.breadcrumbs.map { |e| Breadcrumbs::ComputedElement.new e, helpers }
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Proscenium::UI::Breadcrumbs
|
4
|
+
class ComputedElement
|
5
|
+
def initialize(element, context)
|
6
|
+
@element = element
|
7
|
+
@context = context
|
8
|
+
end
|
9
|
+
|
10
|
+
# If name is a Symbol of a controller method, that method is called.
|
11
|
+
# If name is a Symbol of a controller instance variable, that variable is returned.
|
12
|
+
# If name is a Proc, it is executed in the context of the controller instance.
|
13
|
+
#
|
14
|
+
# @return [String] the content of the breadcrumb element.
|
15
|
+
def name
|
16
|
+
@name ||= case name = @element.name
|
17
|
+
when Symbol
|
18
|
+
if name.to_s.starts_with?('@')
|
19
|
+
name = get_instance_variable(name)
|
20
|
+
name.respond_to?(:for_breadcrumb) ? name.for_breadcrumb : name.to_s
|
21
|
+
else
|
22
|
+
res = @context.controller.send(name)
|
23
|
+
res.try(:for_breadcrumb) || res.to_s
|
24
|
+
end
|
25
|
+
when Proc
|
26
|
+
@context.controller.instance_exec(&name)
|
27
|
+
else
|
28
|
+
name.respond_to?(:for_breadcrumb) ? name.for_breadcrumb : name.to_s
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# If path is a Symbol of a controller method, that method is called.
|
33
|
+
# If path is a Symbol of a controller instance variable, that variable is returned.
|
34
|
+
# If path is an Array, each element is processed as above.
|
35
|
+
# If path is a Proc, it is executed in the context of the controller instance.
|
36
|
+
#
|
37
|
+
# No matter what, the result is always passed to `url_for` before being returned.
|
38
|
+
#
|
39
|
+
# @return [String] the URL for the element
|
40
|
+
def path
|
41
|
+
@path ||= unless @element.path.nil?
|
42
|
+
case path = @element.path
|
43
|
+
when Array
|
44
|
+
path.map! { |x| x.to_s.starts_with?('@') ? get_instance_variable(x) : x }
|
45
|
+
when Symbol
|
46
|
+
if path.to_s.starts_with?('@')
|
47
|
+
path = get_instance_variable(path)
|
48
|
+
elsif @context.controller.respond_to?(path, true)
|
49
|
+
path = @context.controller.send(path)
|
50
|
+
end
|
51
|
+
when Proc
|
52
|
+
path = @context.controller.instance_exec(&path)
|
53
|
+
end
|
54
|
+
|
55
|
+
@context.url_for path
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def get_instance_variable(element)
|
62
|
+
unless @context.instance_variable_defined?(element)
|
63
|
+
raise NameError, "undefined instance variable `#{element}' for breadcrumb", caller
|
64
|
+
end
|
65
|
+
|
66
|
+
@context.instance_variable_get element
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Proscenium::UI::Breadcrumbs
|
4
|
+
# Include this module in your controller to add support for adding breadcrumb elements. You can
|
5
|
+
# then use the `add_breadcrumb` and `prepend_breadcrumb` class methods to append and/or prepend
|
6
|
+
# breadcrumb elements.
|
7
|
+
module Control
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
include ActionView::Helpers::SanitizeHelper
|
10
|
+
|
11
|
+
included do
|
12
|
+
helper_method :breadcrumbs_as_json, :breadcrumbs_for_title if respond_to?(:helper_method)
|
13
|
+
end
|
14
|
+
|
15
|
+
module ClassMethods
|
16
|
+
# Appends a new breadcrumb element into the collection.
|
17
|
+
#
|
18
|
+
# @param name [String, Symbol, Proc, #for_breadcrumb] The name or content of the breadcrumb.
|
19
|
+
# @param path [String, Symbol, Array, Proc, nil] The path (route) to use as the HREF for the
|
20
|
+
# breadcrumb.
|
21
|
+
# @param filter_options [Hash] Options to pass through to the before_action filter.
|
22
|
+
def add_breadcrumb(name, path = nil, **filter_options)
|
23
|
+
element_options = filter_options.delete(:options) || {}
|
24
|
+
|
25
|
+
before_action(filter_options) do |controller|
|
26
|
+
controller.send :add_breadcrumb, name, path, element_options
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Prepend a new breadcrumb element into the collection.
|
31
|
+
#
|
32
|
+
# @param name [String, Symbol, Proc, #for_breadcrumb] The name or content of the breadcrumb.
|
33
|
+
# @param path [String, Symbol, Array, Proc, nil] The path (route) to use as the HREF for the
|
34
|
+
# breadcrumb.
|
35
|
+
# @param filter_options [Hash] Options to pass through to the before_action filter.
|
36
|
+
def prepend_breadcrumb(name, path = nil, **filter_options)
|
37
|
+
element_options = filter_options.delete(:options) || {}
|
38
|
+
|
39
|
+
before_action(filter_options) do |controller|
|
40
|
+
controller.send :prepend_breadcrumb, name, path, element_options
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Pushes a new breadcrumb element into the collection.
|
46
|
+
#
|
47
|
+
# @param name [String, Symbol, Proc, #for_breadcrumb] The name or content of the breadcrumb.
|
48
|
+
# @param path [String, Symbol, Array, Proc, nil] The path (route) to use as the HREF for the
|
49
|
+
# breadcrumb.
|
50
|
+
# @param options [Hash]
|
51
|
+
def add_breadcrumb(name, path = nil, options = {})
|
52
|
+
breadcrumbs << Element.new(name, path, options)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Prepend a new breadcrumb element into the collection.
|
56
|
+
#
|
57
|
+
# @param name [String, Symbol, Proc, #for_breadcrumb] The name or content of the breadcrumb.
|
58
|
+
# @param path [String, Symbol, Array, Proc, nil] The path (route) to use as the HREF for the
|
59
|
+
# breadcrumb.
|
60
|
+
# @param options [Hash]
|
61
|
+
def prepend_breadcrumb(name, path = nil, options = {})
|
62
|
+
breadcrumbs.prepend Element.new(name, path, options)
|
63
|
+
end
|
64
|
+
|
65
|
+
def breadcrumbs
|
66
|
+
@breadcrumbs ||= []
|
67
|
+
end
|
68
|
+
|
69
|
+
def breadcrumbs_as_json
|
70
|
+
computed_breadcrumbs.map do |ele|
|
71
|
+
path = ele.path
|
72
|
+
|
73
|
+
{ name: ele.name, path: ele.path.nil? || helpers.current_page?(path) ? nil : path }
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# @param primary [Boolean] whether to return only the primary breadcrumb.
|
78
|
+
def breadcrumbs_for_title(primary: false)
|
79
|
+
names = computed_breadcrumbs.map(&:name)
|
80
|
+
return names.pop if primary
|
81
|
+
|
82
|
+
out = [names.pop]
|
83
|
+
out << names.join(': ') unless names.empty?
|
84
|
+
strip_tags out.join(' - ')
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
def computed_breadcrumbs
|
90
|
+
@computed_breadcrumbs ||= breadcrumbs.map do |ele|
|
91
|
+
ComputedElement.new ele, helpers
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
@define-mixin breadcrumbs {
|
2
|
+
/* Default properties */
|
3
|
+
--_puiBreadcrumbs--separator-color: GrayText;
|
4
|
+
--_puiBreadcrumbs--separator: url("/proscenium/icons/angle-right-regular.svg");
|
5
|
+
|
6
|
+
margin: 10px;
|
7
|
+
|
8
|
+
ol {
|
9
|
+
list-style: none;
|
10
|
+
padding: 0;
|
11
|
+
margin: 0;
|
12
|
+
display: flex;
|
13
|
+
align-items: baseline;
|
14
|
+
|
15
|
+
li {
|
16
|
+
text-transform: uppercase;
|
17
|
+
display: flex;
|
18
|
+
align-items: center;
|
19
|
+
|
20
|
+
@media (max-width: 426px) {
|
21
|
+
&:not(:nth-last-child(2)) {
|
22
|
+
display: none;
|
23
|
+
}
|
24
|
+
|
25
|
+
&:nth-last-child(2)::before {
|
26
|
+
@mixin _separator;
|
27
|
+
margin: 0 0.5rem 0 0;
|
28
|
+
transform: rotate(180deg);
|
29
|
+
}
|
30
|
+
}
|
31
|
+
|
32
|
+
@media (min-width: 427px) {
|
33
|
+
&:not(:last-child)::after {
|
34
|
+
@mixin _separator;
|
35
|
+
margin: 0 0.5rem;
|
36
|
+
}
|
37
|
+
}
|
38
|
+
|
39
|
+
&:last-child {
|
40
|
+
font-weight: 500;
|
41
|
+
text-transform: none;
|
42
|
+
}
|
43
|
+
|
44
|
+
&:last-child > a {
|
45
|
+
font-weight: 500;
|
46
|
+
text-transform: none;
|
47
|
+
}
|
48
|
+
|
49
|
+
a {
|
50
|
+
color: var(--puiBreadcrumbs--link-color, revert);
|
51
|
+
display: flex;
|
52
|
+
|
53
|
+
&:hover {
|
54
|
+
color: var(--puiBreadcrumbs--link-hover-color, revert);
|
55
|
+
}
|
56
|
+
}
|
57
|
+
|
58
|
+
svg {
|
59
|
+
height: 1em;
|
60
|
+
width: 1em;
|
61
|
+
}
|
62
|
+
}
|
63
|
+
}
|
64
|
+
}
|
65
|
+
|
66
|
+
@define-mixin _separator {
|
67
|
+
display: inline-block;
|
68
|
+
content: "";
|
69
|
+
height: 1rem;
|
70
|
+
width: 1rem;
|
71
|
+
-webkit-mask: var(
|
72
|
+
--puiBreadcrumbs--separator,
|
73
|
+
var(--_puiBreadcrumbs--separator)
|
74
|
+
)
|
75
|
+
no-repeat center center;
|
76
|
+
mask: var(--puiBreadcrumbs--separator, var(--_puiBreadcrumbs--separator))
|
77
|
+
no-repeat center center;
|
78
|
+
vertical-align: sub;
|
79
|
+
background-color: var(
|
80
|
+
--puiBreadcrumbs--separator-color,
|
81
|
+
var(--_puiBreadcrumbs--separator-color)
|
82
|
+
);
|
83
|
+
}
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Proscenium::UI
|
4
|
+
# Provides breadcrumb functionality for controllers and views. Breadcrumbs are a type of
|
5
|
+
# navigation that show the user where they are in the application's hierarchy.
|
6
|
+
# The `Proscenium::UI::Breadcrumbs::Control` module provides the `add_breadcrumb` and
|
7
|
+
# `prepend_breadcrumb` class methods for adding breadcrumb elements, and is intended to be
|
8
|
+
# included in your controllers.
|
9
|
+
#
|
10
|
+
# The `add_breadcrumb` method adds a new breadcrumb element to the end of the collection, while
|
11
|
+
# the `prepend_breadcrumb` method adds a new breadcrumb element to the beginning of the
|
12
|
+
# collection. Both methods take a name, and path as arguments. The name argument is the name or
|
13
|
+
# content of the breadcrumb, while the path argument is the path (route) to use as the HREF for
|
14
|
+
# the breadcrumb.
|
15
|
+
#
|
16
|
+
# class UsersController < ApplicationController
|
17
|
+
# include Proscenium::UI::Breadcrumbs::Control
|
18
|
+
# add_breadcrumb 'Users', :users_path
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# Display the breadcrumbs in your views with the breadcrumbs component.
|
22
|
+
# @see `Proscenium::UI::Breadcrumbs::Component`.
|
23
|
+
#
|
24
|
+
# At it's simplest, you can add a breadcrumb with a name of "User", and a path of "/users" like
|
25
|
+
# this:
|
26
|
+
#
|
27
|
+
# add_breadcrumb 'Foo', '/foo'
|
28
|
+
#
|
29
|
+
# The value of the path is always passed to `url_for` before being rendered. It is also optional,
|
30
|
+
# and if omitted, the breadcrumb will be rendered as plain text.
|
31
|
+
#
|
32
|
+
# Both name and path can be given a Symbol, which can be used to call a method of the same name on
|
33
|
+
# the controller. If a Symbol is given as the path, and no method of the same name exists, then
|
34
|
+
# `url_for` will be called with the Symbol as the argument. Likewise, if an Array is given as the
|
35
|
+
# path, then `url_for` will be called with the Array as the argument.
|
36
|
+
#
|
37
|
+
# If a Symbol is given as the path or name, and it begins with `@` (eg. `:@foo`), then the
|
38
|
+
# instance variable of the same name will be called.
|
39
|
+
#
|
40
|
+
# add_breadcrumb :@foo, :@bar
|
41
|
+
#
|
42
|
+
# A Proc can also be given as the name and/or path. The Proc will be called within the context of
|
43
|
+
# the controller.
|
44
|
+
#
|
45
|
+
# add_breadcrumb -> { @foo }, -> { @bar }
|
46
|
+
#
|
47
|
+
# Passing an object that responds to `#for_breadcrumb` as the name will call that method on the
|
48
|
+
# object to get the breadcrumb name.
|
49
|
+
#
|
50
|
+
module Breadcrumbs
|
51
|
+
extend ActiveSupport::Autoload
|
52
|
+
|
53
|
+
autoload :Control
|
54
|
+
autoload :ComputedElement
|
55
|
+
autoload :Component
|
56
|
+
|
57
|
+
# Represents a navigation element in the breadcrumb collection.
|
58
|
+
class Element
|
59
|
+
attr_accessor :name, :path, :options
|
60
|
+
|
61
|
+
# @param name [String] the element/link name
|
62
|
+
# @param path [String] the element/link URL
|
63
|
+
# @param options [Hash] the element/link options
|
64
|
+
# @return [Element]
|
65
|
+
def initialize(name, path = nil, options = {})
|
66
|
+
self.name = name
|
67
|
+
self.path = path
|
68
|
+
self.options = options
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
console.log("/proscenium/ui/test.js");
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Proscenium
|
4
|
+
module Utils
|
5
|
+
module_function
|
6
|
+
|
7
|
+
# @param value [#to_s] The value to create the digest from. This will usually be a `Pathname`.
|
8
|
+
# @return [String] digest of the given value.
|
9
|
+
def digest(value)
|
10
|
+
Digest::SHA1.hexdigest(value.to_s)[..7]
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|