proscenium 0.15.0.beta.4-x86_64-darwin → 0.15.0.beta.5-x86_64-darwin

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c8fe10f018fd22079605fce2d978031b9f923edb789804a2b80845eee8e78d0e
4
- data.tar.gz: a0b5e9633788960be763f301e15f9d9a7a54949e8deac70e3f0abcace630d0e9
3
+ metadata.gz: 1e01519ca50eae22b78bd8f55b6f55ad4c24327a06dc7ee13667a2d5b3baae5f
4
+ data.tar.gz: e767cff16534ad0092093a5654f6b4af5fdad769203fe4ae25b3855cd1d380b6
5
5
  SHA512:
6
- metadata.gz: 4d8bd75a53cd2315ae735942091c60928d70ac46a5ba96a1afc5ea19be20da53cf37debc500a53ae497f3d9afdd67bc170f19fe8d5b1279784b9846cf59ed8ee
7
- data.tar.gz: f8e8ea0d947606710ae48fa012b45f0ab95349e6f7b58bc9e2b7b47b2cec413fae8d30d0845fb6495357f4f721d907589a7f3a0d9077ae2b6ba302fd72bb7767
6
+ metadata.gz: 7aeedbf4df14e6f770e1666dd64ff66f0a4c38187ea1cd4bf1688a57a52709f76da67daa12dd296e36bf0107e05fb86092e7d1668714c60daf6da6ec0b8f7ed9
7
+ data.tar.gz: 499918efe5c3f447160bc49f5ae3287526cf5e5b1cb4cdff6bd1211bd95df7001068ea8285acddceb725425b8a236115732ad75a78780a7c3524736ec8f05867
data/README.md CHANGED
@@ -681,7 +681,7 @@ console.log(version);
681
681
 
682
682
  ```ruby
683
683
  class MyView < Proscenium::Phlex
684
- def template
684
+ def view_template
685
685
  h1 { 'Hello World' }
686
686
  end
687
687
  end
@@ -693,7 +693,7 @@ In your layouts, include `Proscenium::Phlex::AssetInclusions`, and call the `inc
693
693
  class ApplicationLayout < Proscenium::Phlex
694
694
  include Proscenium::Phlex::AssetInclusions # <--
695
695
 
696
- def template(&)
696
+ def view_template(&)
697
697
  doctype
698
698
  html do
699
699
  head do
@@ -721,7 +721,7 @@ Within your Phlex classes, any class names that begin with `@` will be treated a
721
721
  ```ruby
722
722
  # /app/views/users/show_view.rb
723
723
  class Users::ShowView < Proscenium::Phlex
724
- def template
724
+ def view_template
725
725
  h1 class: :@user_name do
726
726
  @user.name
727
727
  end
@@ -750,7 +750,7 @@ You can of course continue to reference regular class names in your view, and th
750
750
  ```ruby
751
751
  # /app/views/users/show_view.rb
752
752
  class Users::ShowView < Proscenium::Phlex
753
- def template
753
+ def view_template
754
754
  h1 class: :[@user_name, :title] do
755
755
  @user.name
756
756
  end
@@ -785,7 +785,7 @@ Any ViewComponent class that inherits `Proscenium::ViewComponent` will automatic
785
785
  ```ruby
786
786
  # /app/components/user_component.rb
787
787
  class UserComponent < Proscenium::ViewComponent
788
- def template
788
+ def view_template
789
789
  div.h1 @user.name, class: css_module(:user_name)
790
790
  end
791
791
  end
@@ -71,7 +71,7 @@ module Proscenium
71
71
  msg << " at #{location['file']}:#{location['line']}:#{location['column']}"
72
72
  end
73
73
 
74
- super msg
74
+ super(msg)
75
75
  end
76
76
  end
77
77
 
@@ -79,7 +79,7 @@ module Proscenium
79
79
  attr_reader :error_msg, :path
80
80
 
81
81
  def initialize(path, error_msg)
82
- super "Failed to resolve '#{path}' -- #{error_msg}"
82
+ super("Failed to resolve '#{path}' -- #{error_msg}")
83
83
  end
84
84
  end
85
85
 
@@ -100,7 +100,7 @@ module Proscenium
100
100
  @base_url = base_url
101
101
  end
102
102
 
103
- def build_to_path(path) # rubocop:disable Metrics/AbcSize
103
+ def build_to_path(path)
104
104
  ActiveSupport::Notifications.instrument('build_to_path.proscenium',
105
105
  identifier: path,
106
106
  cached: Proscenium.cache.exist?(path)) do
@@ -163,7 +163,9 @@ module Proscenium
163
163
  end
164
164
 
165
165
  def engines
166
- Proscenium.config.engines.to_h { |e| [e.engine_name, e.root.to_s] }
166
+ Proscenium.config.engines.to_h { |e| [e.engine_name, e.root.to_s] }.tap do |x|
167
+ x['proscenium/ui'] = Proscenium.ui_path.to_s
168
+ end
167
169
  end
168
170
 
169
171
  def import_map
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Base class for custom elements, providing support for event delegation, and idempotent
3
+ * customElement registration.
4
+ *
5
+ * The `handleEvent` method is called any time an event defined in `delegatedEvents` is triggered.
6
+ * It's a central handler to handle events for this custom element.
7
+ *
8
+ * @example
9
+ * class MyComponent extends CustomElement {
10
+ * static componentName = 'my-component'
11
+ * static delegatedEvents = ['click']
12
+ *
13
+ * handleEvent(event) {
14
+ * console.log('Hello, world!')
15
+ * }
16
+ * }
17
+ * MyComponent.register()
18
+ */
19
+ export default class CustomElement extends HTMLElement {
20
+ /**
21
+ * Register the component as a custom element, inferring the component name from the kebab-cased
22
+ * class name. You can override the component name by setting a static `componentName` property.
23
+ *
24
+ * This method is idempotent.
25
+ */
26
+ static register() {
27
+ if (this.componentName === undefined) {
28
+ this.componentName = this.name
29
+ .replaceAll(/(.)([A-Z])/g, "$1-$2")
30
+ .toLowerCase();
31
+ }
32
+
33
+ if (!customElements.get(this.componentName)) {
34
+ customElements.define(this.componentName, this);
35
+ }
36
+ }
37
+
38
+ /**
39
+ * A list of event types to be delegated for the lifetime of the custom element.
40
+ *
41
+ * @type {Array}
42
+ */
43
+ static delegatedEvents = [];
44
+
45
+ constructor() {
46
+ super();
47
+
48
+ if (typeof this.handleEvent !== "undefined") {
49
+ this.constructor.delegatedEvents?.forEach((event) => {
50
+ this.addEventListener(event, this);
51
+ });
52
+ }
53
+ }
54
+ }
@@ -1,22 +1,9 @@
1
- const controllerAttribute = "data-controller";
2
- const controllerFilenameExtension = ".js";
1
+ export function lazyLoadControllersFrom(under, app, element = document) {
2
+ const { controllerAttribute } = app.schema;
3
3
 
4
- export function lazyLoadControllersFrom(
5
- under,
6
- application,
7
- element = document
8
- ) {
9
- lazyLoadExistingControllers(under, application, element);
10
- lazyLoadNewControllers(under, application, element);
11
- }
12
-
13
- function lazyLoadExistingControllers(under, application, element) {
14
- queryControllerNamesWithin(element).forEach((controllerName) =>
15
- loadController(controllerName, under, application)
16
- );
17
- }
4
+ lazyLoadExistingControllers(element);
18
5
 
19
- function lazyLoadNewControllers(under, application, element) {
6
+ // Lazy load new controllers.
20
7
  new MutationObserver((mutationsList) => {
21
8
  for (const { attributeName, target, type } of mutationsList) {
22
9
  switch (type) {
@@ -26,13 +13,13 @@ function lazyLoadNewControllers(under, application, element) {
26
13
  target.getAttribute(controllerAttribute)
27
14
  ) {
28
15
  extractControllerNamesFrom(target).forEach((controllerName) =>
29
- loadController(controllerName, under, application)
16
+ loadController(controllerName)
30
17
  );
31
18
  }
32
19
  }
33
20
 
34
21
  case "childList": {
35
- lazyLoadExistingControllers(under, application, target);
22
+ lazyLoadExistingControllers(target);
36
23
  }
37
24
  }
38
25
  }
@@ -41,43 +28,38 @@ function lazyLoadNewControllers(under, application, element) {
41
28
  subtree: true,
42
29
  childList: true,
43
30
  });
44
- }
45
31
 
46
- function queryControllerNamesWithin(element) {
47
- return Array.from(element.querySelectorAll(`[${controllerAttribute}]`))
48
- .map(extractControllerNamesFrom)
49
- .flat();
50
- }
51
-
52
- function extractControllerNamesFrom(element) {
53
- return element
54
- .getAttribute(controllerAttribute)
55
- .split(/\s+/)
56
- .filter((content) => content.length);
57
- }
32
+ function lazyLoadExistingControllers(element) {
33
+ Array.from(element.querySelectorAll(`[${controllerAttribute}]`))
34
+ .map(extractControllerNamesFrom)
35
+ .flat()
36
+ .forEach(loadController);
37
+ }
58
38
 
59
- function loadController(name, under, application) {
60
- if (canRegisterController(name, application)) {
61
- import(controllerFilename(name, under))
62
- .then((module) => registerController(name, module, application))
63
- .catch((error) =>
64
- console.error(`Failed to autoload controller: ${name}`, error)
65
- );
39
+ function extractControllerNamesFrom(element) {
40
+ return element
41
+ .getAttribute(controllerAttribute)
42
+ .split(/\s+/)
43
+ .filter((content) => content.length);
66
44
  }
67
- }
68
45
 
69
- function controllerFilename(name, under) {
70
- return `${under}/${name
71
- .replace(/--/g, "/")
72
- .replace(/-/g, "_")}_controller${controllerFilenameExtension}`;
73
- }
46
+ function loadController(name) {
47
+ if (canRegisterController(name)) {
48
+ const fileToImport = `${under}/${name
49
+ .replace(/--/g, "/")
50
+ .replace(/-/g, "_")}_controller.js`;
74
51
 
75
- function registerController(name, module, application) {
76
- if (canRegisterController(name, application)) {
77
- application.register(name, module.default);
52
+ import(fileToImport)
53
+ .then((module) => {
54
+ canRegisterController(name) && app.register(name, module.default);
55
+ })
56
+ .catch((error) =>
57
+ console.error(`Failed to autoload controller: ${name}`, error)
58
+ );
59
+ }
78
60
  }
79
- }
80
61
 
81
- function canRegisterController(name, application) {
82
- return !application.router.modulesByIdentifier.has(name);
62
+ function canRegisterController(name) {
63
+ return !app.router.modulesByIdentifier.has(name);
64
+ }
83
65
  }
@@ -3,15 +3,13 @@ import DataDisableWith from "./data_disable_with";
3
3
 
4
4
  export default class UJS {
5
5
  constructor() {
6
- const dc = new DataConfirm();
7
- const ddw = new DataDisableWith();
6
+ this.dc = new DataConfirm();
7
+ this.ddw = new DataDisableWith();
8
8
 
9
- document.body.addEventListener(
10
- "submit",
11
- (event) => {
12
- dc.onSubmit(event) && ddw.onSubmit(event);
13
- },
14
- { capture: true }
15
- );
9
+ document.addEventListener("submit", this, { capture: true });
10
+ }
11
+
12
+ handleEvent(event) {
13
+ this.dc.onSubmit(event) && this.ddw.onSubmit(event);
16
14
  }
17
15
  }
@@ -15,7 +15,7 @@ module Proscenium
15
15
  def initialize(args)
16
16
  @detail = args[:detail]
17
17
  @file = args[:file]
18
- super "Failed to build '#{args[:file]}' -- #{detail}"
18
+ super("Failed to build '#{args[:file]}' -- #{detail}")
19
19
  end
20
20
  end
21
21
 
@@ -20,18 +20,26 @@ module Proscenium
20
20
  #
21
21
  class Engines < Esbuild
22
22
  def real_path
23
- @real_path ||= Pathname.new(@request.path.delete_prefix("/#{engine.engine_name}")).to_s
23
+ @real_path ||= Pathname.new(@request.path.delete_prefix("/#{engine_name}")).to_s
24
24
  end
25
25
 
26
26
  def root_for_readable
27
- engine.root
27
+ ui? ? Proscenium.ui_path : engine.root
28
28
  end
29
29
 
30
30
  def engine
31
- @engine ||= Proscenium.config.engines.find do |engine|
32
- @request.path.start_with?("/#{engine.engine_name}")
31
+ @engine ||= Proscenium.config.engines.find do |x|
32
+ @request.path.start_with?("/#{x.engine_name}")
33
33
  end
34
34
  end
35
+
36
+ def engine_name
37
+ ui? ? 'proscenium/ui' : engine.engine_name
38
+ end
39
+
40
+ def ui?
41
+ @request.path.start_with?('/proscenium/ui/')
42
+ end
35
43
  end
36
44
  end
37
45
  end
@@ -15,7 +15,7 @@ module Proscenium
15
15
  detail[:text]
16
16
  end
17
17
 
18
- super args
18
+ super(args)
19
19
  end
20
20
  end
21
21
 
@@ -45,17 +45,26 @@ module Proscenium
45
45
  return Runtime if request.path.match?(%r{^/@proscenium/})
46
46
  return Esbuild if Pathname.new(request.path).fnmatch?(app_path_glob, File::FNM_EXTGLOB)
47
47
 
48
- Engines if Pathname.new(request.path).fnmatch?(engines_path_glob, File::FNM_EXTGLOB)
48
+ pathname = Pathname.new(request.path)
49
+ Engines if pathname.fnmatch?(ui_path_glob, File::FNM_EXTGLOB) ||
50
+ pathname.fnmatch?(engines_path_glob, File::FNM_EXTGLOB)
49
51
  end
50
52
 
51
53
  def app_path_glob
52
- "/{#{Proscenium::ALLOWED_DIRECTORIES}}/**.{#{FILE_EXTENSIONS.join(',')}}"
54
+ "/{#{Proscenium::ALLOWED_DIRECTORIES}}/**.{#{file_extensions}}"
53
55
  end
54
56
 
55
57
  def engines_path_glob
56
58
  names = Proscenium.config.engines.map(&:engine_name)
59
+ "/{#{names.join(',')}}/{#{Proscenium::ALLOWED_DIRECTORIES}}/**.{#{file_extensions}}"
60
+ end
61
+
62
+ def ui_path_glob
63
+ "/proscenium/ui/**.{#{file_extensions}}"
64
+ end
57
65
 
58
- "/{#{names.join(',')}}/{#{Proscenium::ALLOWED_DIRECTORIES}}/**.{#{FILE_EXTENSIONS.join(',')}}"
66
+ def file_extensions
67
+ @file_extensions ||= FILE_EXTENSIONS.join(',')
59
68
  end
60
69
 
61
70
  # TODO: handle precompiled assets
@@ -40,7 +40,7 @@ module Proscenium
40
40
  #
41
41
  # # app/components/user/component.rb
42
42
  # class User::Component < Proscenium::Phlex
43
- # def template
43
+ # def view_template
44
44
  # div class: :@user_name do
45
45
  # 'Joel Moss'
46
46
  # end
@@ -54,7 +54,7 @@ module Proscenium
54
54
  # add the CSS Module name `name` to the <div>.
55
55
  #
56
56
  # class User::Component < Proscenium::Phlex
57
- # def template
57
+ # def view_template
58
58
  # div class: '/lib/users@name' do
59
59
  # 'Joel Moss'
60
60
  # end
@@ -18,14 +18,14 @@ module Proscenium
18
18
  # Override this to provide your own loading UI.
19
19
  #
20
20
  # @example
21
- # def template(**attributes, &block)
21
+ # def view_template(**attributes, &block)
22
22
  # super do
23
23
  # 'Look at me! I am loading now...'
24
24
  # end
25
25
  # end
26
26
  #
27
27
  # @yield the given block to a `div` within the top level component div.
28
- def template(**attributes, &block)
28
+ def view_template(**attributes, &block)
29
29
  send root_tag, **{ data: data_attributes }.deep_merge(attributes), &block
30
30
  end
31
31
  end
@@ -49,6 +49,12 @@ module Proscenium
49
49
  end
50
50
  end
51
51
 
52
+ initializer 'proscenium.ui' do
53
+ ActiveSupport::Inflector.inflections(:en) do |inflect|
54
+ inflect.acronym 'UI'
55
+ end
56
+ end
57
+
52
58
  initializer 'proscenium.debugging' do
53
59
  if Rails.gem_version >= Gem::Version.new('7.1.0')
54
60
  tpl_path = root.join('lib', 'proscenium', 'templates').to_s
@@ -75,6 +81,16 @@ module Proscenium
75
81
  ActionView::PartialRenderer.prepend Monkey::PartialRenderer
76
82
  end
77
83
  end
84
+
85
+ initializer 'proscenium.public_path' do |app|
86
+ if app.config.public_file_server.enabled
87
+ headers = app.config.public_file_server.headers || {}
88
+ index = app.config.public_file_server.index_name || 'index'
89
+
90
+ app.middleware.insert_after(ActionDispatch::Static, ActionDispatch::Static,
91
+ root.join('public').to_s, index: index, headers: headers)
92
+ end
93
+ end
78
94
  end
79
95
  end
80
96
 
@@ -23,6 +23,8 @@ module Proscenium
23
23
 
24
24
  if path.start_with?('@proscenium/')
25
25
  "/#{path}"
26
+ elsif path.start_with?(Proscenium.ui_path.to_s)
27
+ path.delete_prefix Proscenium.root.join('lib').to_s
26
28
  elsif (engine = Proscenium.config.engines.find { |e| path.start_with? "#{e.root}/" })
27
29
  path.sub(/^#{engine.root}/, "/#{engine.engine_name}")
28
30
  elsif path.start_with?("#{Rails.root}/")
@@ -10,7 +10,7 @@ module Proscenium
10
10
 
11
11
  append_after_action :capture_and_replace_proscenium_stylesheets,
12
12
  :capture_and_replace_proscenium_javascripts,
13
- if: -> { request.format.html? && !response.redirect? }
13
+ if: -> { response.content_type&.include?('html') }
14
14
  end
15
15
  end
16
16
 
@@ -20,7 +20,7 @@ module Proscenium
20
20
  end
21
21
  end
22
22
 
23
- def capture_and_replace_proscenium_stylesheets # rubocop:disable Metrics/*
23
+ def capture_and_replace_proscenium_stylesheets
24
24
  return if response_body.nil?
25
25
  return if response_body.first.blank? || !Proscenium::Importer.css_imported?
26
26
  return unless response_body.first.include? '<!-- [PROSCENIUM_STYLESHEETS] -->'
@@ -52,7 +52,7 @@ module Proscenium
52
52
  response_body.first.gsub! '<!-- [PROSCENIUM_STYLESHEETS] -->', out.join.html_safe
53
53
  end
54
54
 
55
- def capture_and_replace_proscenium_javascripts # rubocop:disable Metrics/*
55
+ def capture_and_replace_proscenium_javascripts
56
56
  return if response_body.nil?
57
57
  return if response_body.first.blank? || !Proscenium::Importer.js_imported?
58
58
 
@@ -108,7 +108,7 @@ module Proscenium
108
108
  #
109
109
  # If the class responds to `.sideload`, it will be called instead of the regular side loading.
110
110
  # You can use this to customise what is side loaded.
111
- def sideload_inheritance_chain(obj, options) # rubocop:disable Metrics/*
111
+ def sideload_inheritance_chain(obj, options)
112
112
  return unless Proscenium.config.side_load
113
113
 
114
114
  options = {} if options.nil?
@@ -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,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry-initializer'
4
+
5
+ module Proscenium::UI
6
+ class Component < Proscenium::Phlex
7
+ self.abstract_class = true
8
+
9
+ extend Dry::Initializer
10
+ end
11
+ end
@@ -0,0 +1 @@
1
+ console.log("/proscenium/ui/test.js");
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry-types'
4
+
5
+ module Proscenium::UI
6
+ extend ActiveSupport::Autoload
7
+
8
+ autoload :Component
9
+ autoload :Breadcrumbs
10
+
11
+ module Types
12
+ include Dry.Types()
13
+ end
14
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Proscenium
4
- VERSION = '0.15.0.beta.4'
4
+ VERSION = '0.15.0.beta.5'
5
5
  end
data/lib/proscenium.rb CHANGED
@@ -27,6 +27,7 @@ module Proscenium
27
27
  autoload :Builder
28
28
  autoload :Importer
29
29
  autoload :Resolver
30
+ autoload :UI
30
31
 
31
32
  class Deprecator
32
33
  def deprecation_warning(name, message, _caller_backtrace = nil)
@@ -55,6 +56,14 @@ module Proscenium
55
56
  def cache
56
57
  @cache ||= config.cache || ActiveSupport::Cache::NullStore.new
57
58
  end
59
+
60
+ def ui_path
61
+ Railtie.root.join('lib', 'proscenium', 'ui')
62
+ end
63
+
64
+ def root
65
+ Railtie.root
66
+ end
58
67
  end
59
68
  end
60
69
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: proscenium
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.15.0.beta.4
4
+ version: 0.15.0.beta.5
5
5
  platform: x86_64-darwin
6
6
  authors:
7
7
  - Joel Moss
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-03-21 00:00:00.000000000 Z
11
+ date: 2024-04-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -30,6 +30,34 @@ dependencies:
30
30
  - - "<"
31
31
  - !ruby/object:Gem::Version
32
32
  version: '8.0'
33
+ - !ruby/object:Gem::Dependency
34
+ name: dry-initializer
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '3.1'
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '3.1'
47
+ - !ruby/object:Gem::Dependency
48
+ name: dry-types
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '1.7'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '1.7'
33
61
  - !ruby/object:Gem::Dependency
34
62
  name: ffi
35
63
  requirement: !ruby/object:Gem::Requirement
@@ -58,6 +86,20 @@ dependencies:
58
86
  - - "~>"
59
87
  - !ruby/object:Gem::Version
60
88
  version: '3.13'
89
+ - !ruby/object:Gem::Dependency
90
+ name: phlex-rails
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: 1.2.1
96
+ type: :runtime
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: 1.2.1
61
103
  - !ruby/object:Gem::Dependency
62
104
  name: railties
63
105
  requirement: !ruby/object:Gem::Requirement
@@ -114,6 +156,7 @@ files:
114
156
  - lib/proscenium/ext/proscenium.h
115
157
  - lib/proscenium/helper.rb
116
158
  - lib/proscenium/importer.rb
159
+ - lib/proscenium/libs/custom_element.js
117
160
  - lib/proscenium/libs/react-manager/index.jsx
118
161
  - lib/proscenium/libs/react-manager/react.js
119
162
  - lib/proscenium/libs/stimulus-loading.js
@@ -140,6 +183,15 @@ files:
140
183
  - lib/proscenium/side_load.rb
141
184
  - lib/proscenium/source_path.rb
142
185
  - lib/proscenium/templates/rescues/build_error.html.erb
186
+ - lib/proscenium/ui.rb
187
+ - lib/proscenium/ui/breadcrumbs.rb
188
+ - lib/proscenium/ui/breadcrumbs/component.module.css
189
+ - lib/proscenium/ui/breadcrumbs/component.rb
190
+ - lib/proscenium/ui/breadcrumbs/computed_element.rb
191
+ - lib/proscenium/ui/breadcrumbs/control.rb
192
+ - lib/proscenium/ui/breadcrumbs/mixins.css
193
+ - lib/proscenium/ui/component.rb
194
+ - lib/proscenium/ui/test.js
143
195
  - lib/proscenium/utils.rb
144
196
  - lib/proscenium/version.rb
145
197
  - lib/proscenium/view_component.rb
@@ -169,7 +221,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
169
221
  - !ruby/object:Gem::Version
170
222
  version: '0'
171
223
  requirements: []
172
- rubygems_version: 3.5.5
224
+ rubygems_version: 3.5.7
173
225
  signing_key:
174
226
  specification_version: 4
175
227
  summary: The engine powering your Rails frontend