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

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 93000fde1226789c26de17f4d0d075412e9be6e7733be78580f16474776c5dd7
4
- data.tar.gz: d76651fcd12c177882a28bf5652dd137727020a9d3f4d71c022510d67c09565e
3
+ metadata.gz: b32b5cbadeca029ce04a376125a924b059d388b60e90835b9a8e6d7a152a9c72
4
+ data.tar.gz: ff4a5b0ca38e8b028313f1a4bfc6fc214a11f2bf3cd6a49f89125d15ba26190f
5
5
  SHA512:
6
- metadata.gz: 196e4a7b647e0a7e0c61ee3c359661287ead2fa709c7e844fac398a812e988d593a094ba0b58219ac474cdfa9e83961e99cb494183656ec022804ccd4663b8bd
7
- data.tar.gz: 316e36fe6fda602134a282d738003d953459252f959ab4d875ca6adcf4d87c39f93e085b495fab880faffd873c52a592ad7b6950593c21d944c21a3ca4d30a6f
6
+ metadata.gz: 3b900744ef723e3cb4d6d7c83a242d8fd8635d72fdcd7b297df131db407def41f934641125c230bc4feeab3a8694710d6300804558812bd8374dc1e4fb93618a
7
+ data.tar.gz: b24280e853b5d57051af8f681d3b4b7923902dcb45226451fa8e01663c3025dc94987c534ddc740a08936a46a6a3ac14935ebbf08fefe2c732431c08e37cffce
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
Binary file
@@ -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-linux
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