proscenium 0.19.0.beta16-x86_64-linux → 0.19.0.test1-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.
Files changed (91) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +44 -0
  3. data/lib/proscenium/builder.rb +1 -1
  4. data/lib/proscenium/bundled_gems.rb +1 -1
  5. data/lib/proscenium/css_module.rb +5 -0
  6. data/lib/proscenium/ext/proscenium +0 -0
  7. data/lib/proscenium/middleware.rb +6 -0
  8. data/lib/proscenium/monkey.rb +9 -2
  9. data/lib/proscenium/phlex.rb +6 -0
  10. data/lib/proscenium/railtie.rb +22 -2
  11. data/lib/proscenium/react_componentable.rb +1 -1
  12. data/lib/proscenium/registry/bundled_package.rb +29 -0
  13. data/lib/proscenium/registry/package.rb +95 -0
  14. data/lib/proscenium/registry/ruby_gem_package.rb +30 -0
  15. data/lib/proscenium/registry.rb +29 -0
  16. data/lib/proscenium/resolver.rb +9 -0
  17. data/lib/proscenium/ruby_gems.rb +67 -0
  18. data/lib/proscenium/ui/breadcrumbs/component.module.css +14 -0
  19. data/lib/proscenium/ui/breadcrumbs/component.rb +71 -0
  20. data/lib/proscenium/ui/breadcrumbs/computed_element.rb +69 -0
  21. data/lib/proscenium/ui/breadcrumbs/control.rb +95 -0
  22. data/lib/proscenium/ui/breadcrumbs/mixins.css +83 -0
  23. data/lib/proscenium/ui/breadcrumbs.rb +72 -0
  24. data/lib/proscenium/ui/component.rb +7 -0
  25. data/lib/proscenium/ui/custom_element.js +54 -0
  26. data/lib/proscenium/ui/flash/bun.lock +19 -0
  27. data/lib/proscenium/ui/flash/index.css +1 -0
  28. data/lib/proscenium/ui/flash/index.js +77 -0
  29. data/lib/proscenium/ui/flash/node_modules/dom-mutations/index.d.ts +33 -0
  30. data/lib/proscenium/ui/flash/node_modules/dom-mutations/index.js +44 -0
  31. data/lib/proscenium/ui/flash/node_modules/dom-mutations/license +9 -0
  32. data/lib/proscenium/ui/flash/node_modules/dom-mutations/package.json +59 -0
  33. data/lib/proscenium/ui/flash/node_modules/dom-mutations/readme.md +125 -0
  34. data/lib/proscenium/ui/flash/node_modules/sourdough-toast/LICENSE +20 -0
  35. data/lib/proscenium/ui/flash/node_modules/sourdough-toast/README.md +11 -0
  36. data/lib/proscenium/ui/flash/node_modules/sourdough-toast/package.json +44 -0
  37. data/lib/proscenium/ui/flash/node_modules/sourdough-toast/src/sourdough-toast.css +697 -0
  38. data/lib/proscenium/ui/flash/node_modules/sourdough-toast/src/sourdough-toast.js +537 -0
  39. data/lib/proscenium/ui/flash/package.json +11 -0
  40. data/lib/proscenium/ui/flash.rb +15 -0
  41. data/lib/proscenium/ui/form/field_methods.rb +88 -0
  42. data/lib/proscenium/ui/form/fields/base.rb +188 -0
  43. data/lib/proscenium/ui/form/fields/checkbox/index.jsx +48 -0
  44. data/lib/proscenium/ui/form/fields/checkbox/index.module.css +9 -0
  45. data/lib/proscenium/ui/form/fields/checkbox/previews/basic.jsx +8 -0
  46. data/lib/proscenium/ui/form/fields/checkbox.rb +32 -0
  47. data/lib/proscenium/ui/form/fields/date.module.css +27 -0
  48. data/lib/proscenium/ui/form/fields/datetime.rb +15 -0
  49. data/lib/proscenium/ui/form/fields/hidden.rb +9 -0
  50. data/lib/proscenium/ui/form/fields/input/index.jsx +71 -0
  51. data/lib/proscenium/ui/form/fields/input/index.module.css +13 -0
  52. data/lib/proscenium/ui/form/fields/input/previews/basic.jsx +8 -0
  53. data/lib/proscenium/ui/form/fields/input.rb +14 -0
  54. data/lib/proscenium/ui/form/fields/radio_group.rb +173 -0
  55. data/lib/proscenium/ui/form/fields/radio_input/index.jsx +44 -0
  56. data/lib/proscenium/ui/form/fields/radio_input/index.module.css +13 -0
  57. data/lib/proscenium/ui/form/fields/radio_input/previews/basic.jsx +8 -0
  58. data/lib/proscenium/ui/form/fields/radio_input.rb +17 -0
  59. data/lib/proscenium/ui/form/fields/rich_textarea.css +23 -0
  60. data/lib/proscenium/ui/form/fields/rich_textarea.js +6 -0
  61. data/lib/proscenium/ui/form/fields/rich_textarea.rb +18 -0
  62. data/lib/proscenium/ui/form/fields/select.jsx +47 -0
  63. data/lib/proscenium/ui/form/fields/select.module.css +46 -0
  64. data/lib/proscenium/ui/form/fields/select.rb +300 -0
  65. data/lib/proscenium/ui/form/fields/tel.css +297 -0
  66. data/lib/proscenium/ui/form/fields/tel.js +83 -0
  67. data/lib/proscenium/ui/form/fields/tel.rb +54 -0
  68. data/lib/proscenium/ui/form/fields/textarea/index.jsx +50 -0
  69. data/lib/proscenium/ui/form/fields/textarea/index.module.css +13 -0
  70. data/lib/proscenium/ui/form/fields/textarea/previews/basic.jsx +8 -0
  71. data/lib/proscenium/ui/form/fields/textarea.rb +18 -0
  72. data/lib/proscenium/ui/form/translation.rb +71 -0
  73. data/lib/proscenium/ui/form.css +52 -0
  74. data/lib/proscenium/ui/form.rb +211 -0
  75. data/lib/proscenium/ui/props.css +7 -0
  76. data/lib/proscenium/ui/stimulus-loading.js +65 -0
  77. data/lib/proscenium/ui/test.js +1 -0
  78. data/lib/proscenium/ui/ujs/class.js +15 -0
  79. data/lib/proscenium/ui/ujs/data_confirm.js +23 -0
  80. data/lib/proscenium/ui/ujs/data_disable_with.js +68 -0
  81. data/lib/proscenium/ui/ujs/index.js +9 -0
  82. data/lib/proscenium/ui.rb +11 -0
  83. data/lib/proscenium/version.rb +1 -1
  84. data/lib/proscenium/view_component/css_modules.rb +11 -0
  85. data/lib/proscenium/view_component/react_component.rb +22 -0
  86. data/lib/proscenium/view_component/sideload.rb +4 -0
  87. data/lib/proscenium/view_component.rb +36 -0
  88. data/lib/proscenium.rb +34 -6
  89. metadata +136 -24
  90. /data/lib/proscenium/{react-manager → ui/react-manager}/index.jsx +0 -0
  91. /data/lib/proscenium/{react-manager → ui/react-manager}/react.js +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5918439e20e4d5d6369efe0024039308ed3883fbb62a9d9c8592ce5fbeffe739
4
- data.tar.gz: 9d6c0eb5da2a7ac7ca64116f9276b09378d2efbcc148ac7306eaf29ae9e6f6d8
3
+ metadata.gz: 6008df04f1f7b06748556e9d54761d2e39fa28ed3583e343086a6549d8efce09
4
+ data.tar.gz: b3e7ed15208d7923f94d3208a8d7d4968fd3438256f1868bdf67b08cb9060a00
5
5
  SHA512:
6
- metadata.gz: 29ab69f3f678c96c7a33613573d111058e70b8280c609c6267da2285af31b7edb9a2ed920bea08a61541d518e835db42da7a3d9f9d9d3c7d0775607dbfffae2a
7
- data.tar.gz: 97459db33bf3cd2534ce10839dbf33c0bd6b7f210e4140e9669345c0afb0fc308a6126e9149c747119374550511660147d841cf3d0bd19d4405c9dc70f8bf1fd
6
+ metadata.gz: 0c9b49b3fb6379c29873e5c128993053fa143dbfd9586d3ceab02ad40450922809051d1f390be8b9154f12629763d36a09116604976e79a1711f477a57b5b281
7
+ data.tar.gz: 32da1d543ae84ec296cc8045b4d018c825e4910df0b1c002d45ee3e1b18a3e39d82ace95ff872eabbec88d39009c0573249e28bebb90acf795776b8e1ceda444
data/README.md CHANGED
@@ -49,6 +49,7 @@
49
49
  - [JSX](#jsx)
50
50
  - [JSON](#json)
51
51
  - [Phlex Support](#phlex-support)
52
+ - [ViewComponent Support](#viewcomponent-support)
52
53
  - [Cache Busting](#cache-busting)
53
54
  - [rjs is back!](#rjs-is-back)
54
55
  - [Resolution](#resolution)
@@ -768,6 +769,49 @@ end
768
769
  <h1 class="user_name-ABCD1234 title">Joel Moss</h1>
769
770
  ```
770
771
 
772
+ ## ViewComponent Support
773
+
774
+ [ViewComponent](https://viewcomponent.org/) iA framework for creating reusable, testable & encapsulated view components, built to integrate seamlessly with Ruby on Rails. Proscenium works perfectly with ViewComponent, with support for side-loading, CSS modules, and more. Simply write your ViewComponent classes and inherit from `Proscenium::ViewComponent`.
775
+
776
+ ```ruby
777
+ class MyView < Proscenium::ViewComponent
778
+ def call
779
+ tag.h1 'Hello World'
780
+ end
781
+ end
782
+ ```
783
+
784
+ ### Side-loading
785
+
786
+ Any ViewComponent class that inherits `Proscenium::ViewComponent` will automatically be [side-loaded](#side-loading).
787
+
788
+ ### CSS Modules
789
+
790
+ [CSS Modules](#css-modules) are fully supported in ViewComponent classes, with access to the [`css_module` helper](#in-your-views) if you need it.
791
+
792
+ ```ruby
793
+ # /app/components/user_component.rb
794
+ class UserComponent < Proscenium::ViewComponent
795
+ def view_template
796
+ div.h1 @user.name, class: css_module(:user_name)
797
+ end
798
+ end
799
+ ```
800
+
801
+ ```css
802
+ /* # /app/components/user_component.module.css */
803
+ .userName {
804
+ color: red;
805
+ font-size: 50px;
806
+ }
807
+ ```
808
+
809
+ The view above will be rendered something like this:
810
+
811
+ ```html
812
+ <h1 class="user_name-ABCD1234">Joel Moss</h1>
813
+ ```
814
+
771
815
  ## Cache Busting
772
816
 
773
817
  > _COMING SOON_
@@ -16,7 +16,7 @@ module Proscenium
16
16
 
17
17
  module Request
18
18
  extend FFI::Library
19
- ffi_lib ENV.fetch('PROSCENIUM_BIN', Pathname.new(__dir__).join('ext/proscenium').to_s)
19
+ ffi_lib Pathname.new(__dir__).join('ext/proscenium').to_s
20
20
 
21
21
  enum :environment, [:development, 1, :test, :production]
22
22
 
@@ -13,7 +13,7 @@ module Proscenium
13
13
  bundle = {}
14
14
  specs.each do |s|
15
15
  bundle[s.name] = if s.name == 'proscenium'
16
- Pathname(s.full_gem_path).join('lib/proscenium').to_s
16
+ Pathname(s.full_gem_path).join('lib/proscenium/ui').to_s
17
17
  else
18
18
  s.full_gem_path
19
19
  end
@@ -1,6 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Proscenium::CssModule
4
+ extend ActiveSupport::Autoload
5
+
6
+ autoload :Path
7
+ autoload :Transformer
8
+
4
9
  class TransformError < StandardError
5
10
  def initialize(name, additional_msg = nil)
6
11
  msg = "Failed to transform CSS module `#{name}`"
Binary file
@@ -2,9 +2,15 @@
2
2
 
3
3
  module Proscenium
4
4
  class Middleware
5
+ extend ActiveSupport::Autoload
6
+
5
7
  # Error when the build command fails.
6
8
  class BuildError < StandardError; end
7
9
 
10
+ autoload :Base
11
+ autoload :Esbuild
12
+ autoload :RubyGems
13
+
8
14
  def initialize(app)
9
15
  @app = app
10
16
 
@@ -9,8 +9,15 @@ module Proscenium
9
9
  result = super
10
10
  return result if !view.controller || !Proscenium.config.side_load
11
11
 
12
- to_sideload = if template.respond_to?(:virtual_path) &&
13
- template.respond_to?(:type) && template.type == :html
12
+ renderable = template.instance_variable_get(:@renderable)
13
+
14
+ to_sideload = if Object.const_defined?(:ViewComponent) &&
15
+ template.is_a?(ActionView::Template::Renderable) &&
16
+ renderable.class < ::ViewComponent::Base &&
17
+ renderable.class.format == :html
18
+ renderable
19
+ elsif template.respond_to?(:virtual_path) &&
20
+ template.respond_to?(:type) && template.type == :html
14
21
  template
15
22
  end
16
23
  if to_sideload
@@ -4,6 +4,12 @@ require 'phlex-rails'
4
4
 
5
5
  module Proscenium
6
6
  class Phlex < ::Phlex::HTML
7
+ extend ActiveSupport::Autoload
8
+
9
+ autoload :CssModules
10
+ autoload :ReactComponent
11
+ autoload :AssetInclusions
12
+
7
13
  include Proscenium::SourcePath
8
14
  include CssModules
9
15
  include AssetInclusions
@@ -14,7 +14,9 @@ module Proscenium
14
14
  config.proscenium.bundle = true
15
15
  config.proscenium.side_load = true
16
16
  config.proscenium.code_splitting = true
17
+
17
18
  config.proscenium.ensure_loaded = :raise
19
+
18
20
  config.proscenium.cache_query_string = Rails.env.production? && ENV.fetch('REVISION', nil)
19
21
  config.proscenium.cache_max_age = 2_592_000 # 30 days
20
22
 
@@ -33,9 +35,17 @@ module Proscenium
33
35
  end
34
36
  end
35
37
 
38
+ initializer 'proscenium.ui' do
39
+ ActiveSupport::Inflector.inflections(:en) do |inflect|
40
+ inflect.acronym 'UI'
41
+ end
42
+ end
43
+
36
44
  initializer 'proscenium.debugging' do
37
- tpl_path = root.join('lib', 'proscenium', 'templates').to_s
38
- ActionDispatch::DebugView::RESCUES_TEMPLATE_PATHS << tpl_path
45
+ if Rails.gem_version >= Gem::Version.new('7.1.0')
46
+ tpl_path = root.join('lib', 'proscenium', 'templates').to_s
47
+ ActionDispatch::DebugView::RESCUES_TEMPLATE_PATHS << tpl_path
48
+ end
39
49
  end
40
50
 
41
51
  initializer 'proscenium.middleware' do |app|
@@ -55,5 +65,15 @@ module Proscenium
55
65
  ActionView::PartialRenderer.prepend Monkey::PartialRenderer
56
66
  end
57
67
  end
68
+
69
+ initializer 'proscenium.public_path' do |app|
70
+ if app.config.public_file_server.enabled
71
+ headers = app.config.public_file_server.headers || {}
72
+ index = app.config.public_file_server.index_name || 'index'
73
+
74
+ app.middleware.insert_after(ActionDispatch::Static, ActionDispatch::Static,
75
+ root.join('public').to_s, index:, headers:)
76
+ end
77
+ end
58
78
  end
59
79
  end
@@ -11,7 +11,7 @@ module Proscenium
11
11
  # The HTML tag to use as the wrapping element for the component. You can reassign this in your
12
12
  # component class to use a different tag:
13
13
  #
14
- # class MyComponent < Proscenium::Phlex::ReactComponent
14
+ # class MyComponent < Proscenium::ViewComponent::ReactComponent
15
15
  # self.root_tag = :span
16
16
  # end
17
17
  #
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubygems/package'
4
+
5
+ class Proscenium::Registry
6
+ class BundledPackage < Package
7
+ def version = @version ||= spec.version.to_s
8
+
9
+ private
10
+
11
+ def package_json
12
+ @package_json ||= begin
13
+ unless (gem_path = Proscenium::BundledGems.pathname_for(gem_name))
14
+ raise PackageNotInstalledError, name
15
+ end
16
+
17
+ if (package_path = gem_path.join('package.json')).exist?
18
+ JSON.parse(package_path.read)
19
+ else
20
+ default_package_json
21
+ end
22
+ end
23
+ end
24
+
25
+ def spec
26
+ @spec ||= Bundler.load.specs[gem_name].first
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Proscenium::Registry
4
+ # 1. Fetch the gem metadata from RubyGems API.
5
+ # 2. Extract any package.json from the gem, and populate the response with it.
6
+ # 3. Create a tarball containing the fetched package.json. This will be downloaded by the npm
7
+ # client, and unpacked into node_modules. Proscenium ignores this, as it will pull contents
8
+ # directly from location of the installed gem.
9
+ # 4. Return a valid npm response listing package details, tarball location, and its dependencies.
10
+ #
11
+ # See https://wiki.commonjs.org/wiki/Packages/Registry
12
+ class Package
13
+ extend Literal::Properties
14
+
15
+ prop :name, String, :positional, reader: :private
16
+ prop :version, _String?
17
+ prop :host, String
18
+
19
+ def as_json
20
+ {
21
+ name:,
22
+ 'dist-tags': {
23
+ latest: version
24
+ },
25
+ versions: {
26
+ version => {
27
+ name:,
28
+ version:,
29
+ dependencies: package_json['dependencies'] || {},
30
+ dist: {
31
+ tarball:,
32
+ integrity:,
33
+ shasum:
34
+ }
35
+ }
36
+ }
37
+ }
38
+ end
39
+
40
+ def validate!
41
+ return self if name.start_with?('@rubygems/')
42
+
43
+ raise PackageUnsupportedError, name
44
+ end
45
+
46
+ def gem_name = @gem_name ||= name.gsub('@rubygems/', '')
47
+ def version = @version # rubocop:disable Style/TrivialAccessors
48
+ def shasum = Digest::SHA1.file(tarball_path).hexdigest
49
+ def integrity = "sha512-#{Digest::SHA512.file(tarball_path).base64digest}"
50
+
51
+ private
52
+
53
+ def tarball
54
+ create_tarball unless tarball_path.exist?
55
+
56
+ "#{@host}/#{tarball_path.relative_path_from(Rails.public_path)}"
57
+ end
58
+
59
+ def tarball_name
60
+ @tarball_name ||= "#{gem_name}-#{version}"
61
+ end
62
+
63
+ def tarball_path
64
+ @tarball_path ||= Rails.public_path.join('proscenium_registry_tarballs')
65
+ .join("@rubygems/#{gem_name}/#{tarball_name}.tgz")
66
+ end
67
+
68
+ def create_tarball
69
+ FileUtils.mkdir_p(File.dirname(tarball_path))
70
+
71
+ File.open(tarball_path, 'wb') do |file|
72
+ Zlib::GzipWriter.wrap(file) do |gz|
73
+ Gem::Package::TarWriter.new(gz) do |tar|
74
+ contents = package_json.to_json
75
+ tar.add_file_simple('package/package.json', 0o444, contents.length) do |io|
76
+ io.write contents
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+
83
+ def package_json
84
+ @package_json ||= default_package_json
85
+ end
86
+
87
+ def default_package_json
88
+ {
89
+ name:,
90
+ version:,
91
+ dependencies: {}
92
+ }
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'gems'
4
+
5
+ class Proscenium::Registry
6
+ class RubyGemPackage < Package
7
+ def version = spec['version']
8
+
9
+ private
10
+
11
+ def package_json
12
+ @package_json ||= begin
13
+ package_path = Proscenium::RubyGems.path_for(gem_name, version).join('package.json')
14
+ if package_path.exist?
15
+ JSON.parse path.read
16
+ else
17
+ default_package_json
18
+ end
19
+ end
20
+ end
21
+
22
+ def spec
23
+ @spec ||= if @version.present?
24
+ Gems::V2.info gem_name, @version
25
+ else
26
+ Gems.info(gem_name)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Proscenium::Registry
4
+ extend ActiveSupport::Autoload
5
+
6
+ autoload :Package
7
+ autoload :BundledPackage
8
+ autoload :RubyGemPackage
9
+
10
+ class PackageUnsupportedError < StandardError
11
+ def initialize(name)
12
+ super("Package `#{name}` is not valid; only Ruby gems are supported via the @rubygems scope.")
13
+ end
14
+ end
15
+
16
+ class PackageNotInstalledError < StandardError
17
+ def initialize(name)
18
+ super("Package `#{name}` is not found in your bundle; have you installed the Ruby gem?")
19
+ end
20
+ end
21
+
22
+ def self.bundled_package(name, host:)
23
+ BundledPackage.new(name, host:).validate!
24
+ end
25
+
26
+ def self.ruby_gem_package(name, version, host:)
27
+ RubyGemPackage.new(name, version:, host:).validate!
28
+ end
29
+ end
@@ -19,7 +19,16 @@ module Proscenium
19
19
  end
20
20
 
21
21
  self.resolved[path] ||= if (gem = BundledGems.paths.find { |_, v| path.start_with? "#{v}/" })
22
+ # If the path is a rubygem, and it is installed with npm via the
23
+ # @rubygems scope, then resolve the path to the symlinked location
24
+ # in node_modules.
25
+ # npm_path = Rails.root.join("node_modules/@rubygems/#{gem.first}")
26
+ # if npm_path.symlink?
27
+ # npm_path.realpath.join(path.sub(/^#{gem.last}/, '.')).to_s
28
+ # .delete_prefix(Rails.root.to_s)
29
+ # else
22
30
  path.sub(/^#{gem.last}/, "/node_modules/@rubygems/#{gem.first}")
31
+ # end
23
32
  elsif path.start_with?("#{Rails.root}/")
24
33
  path.delete_prefix Rails.root.to_s
25
34
  else
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubygems/package'
4
+ require 'rubygems/remote_fetcher'
5
+
6
+ module Proscenium
7
+ class RubyGems
8
+ def self.path_for(name, version = nil)
9
+ Pathname new(name, version).path
10
+ end
11
+
12
+ def initialize(name, version = nil)
13
+ @name = name
14
+ @version = version
15
+ end
16
+
17
+ def path
18
+ dependency = Gem::Dependency.new @name, @version
19
+ path = gem_path dependency
20
+
21
+ raise "Gem '#{@name}' not installed nor fetchable." unless path
22
+
23
+ basename = File.basename path, '.gem'
24
+ target_dir = File.expand_path basename, Rails.root.join('tmp', 'unpacked_gems')
25
+
26
+ Gem::Package.new(path).extract_files target_dir
27
+
28
+ target_dir
29
+ end
30
+
31
+ # Find cached filename in Gem.path. Returns nil if the file cannot be found.
32
+ def find_in_cache(filename)
33
+ Gem.path.each do |path|
34
+ this_path = File.join(path, 'cache', filename)
35
+ return this_path if File.exist? this_path
36
+ end
37
+
38
+ nil
39
+ end
40
+
41
+ # Return the full path to the cached gem file matching the given
42
+ # name and version requirement. Returns 'nil' if no match.
43
+ #
44
+ # Example:
45
+ #
46
+ # get_path 'rake', '> 0.4' # "/usr/lib/ruby/gems/1.8/cache/rake-0.4.2.gem"
47
+ # get_path 'rake', '< 0.1' # nil
48
+ # get_path 'rak' # nil (exact name required)
49
+ def gem_path(dependency)
50
+ return dependency.name if /\.gem$/i.match?(dependency.name)
51
+
52
+ specs = dependency.matching_specs
53
+ selected = specs.max_by(&:version)
54
+
55
+ return Gem::RemoteFetcher.fetcher.download_to_cache(dependency) unless selected
56
+ return unless /^#{selected.name}$/i.match?(dependency.name)
57
+
58
+ # We expect to find (basename).gem in the 'cache' directory. Furthermore,
59
+ # the name match must be exact (ignoring case).
60
+ path = find_in_cache File.basename selected.cache_file
61
+
62
+ return Gem::RemoteFetcher.fetcher.download_to_cache(dependency) unless path
63
+
64
+ path
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,14 @@
1
+ @layer proscenium-ui-component {
2
+ /*
3
+ * 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,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Proscenium::UI
4
+ class Breadcrumbs::Component < Component
5
+ extend Literal::Properties
6
+ include Phlex::Rails::Helpers::URLFor
7
+
8
+ # The path (route) to use as the HREF for the home segment. Defaults to `:root`.
9
+ prop :home_path, _Union(String, Symbol), default: -> { :root }
10
+
11
+ # Assign false to hide the home segment.
12
+ prop :with_home, _Boolean, default: -> { true }
13
+
14
+ def view_template
15
+ div class: :@base do
16
+ ol do
17
+ if @with_home
18
+ li do
19
+ home_template
20
+ end
21
+ end
22
+
23
+ breadcrumbs.each do |ce|
24
+ li do
25
+ path = ce.path
26
+ path.nil? ? ce.name : a(href: url_for(path)) { ce.name }
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ # Override this to customise the home breadcrumb. You can call super with a block to use the
36
+ # default template, but with custom content.
37
+ #
38
+ # @example
39
+ # def home_template
40
+ # super { 'hello' }
41
+ # end
42
+ def home_template(&block)
43
+ a(href: url_for(@home_path)) do
44
+ if block
45
+ yield
46
+ else
47
+ svg role: 'img', xmlns: 'http://www.w3.org/2000/svg', viewBox: '0 0 576 512' do |s|
48
+ s.path fill: 'currentColor',
49
+ d: 'M488 312.7V456c0 13.3-10.7 24-24 24H348c-6.6 0-12-5.4-12-12V356c0-6.6-5.4-' \
50
+ '12-12-12h-72c-6.6 0-12 5.4-12 12v112c0 6.6-5.4 12-12 12H112c-13.3 0-24-10.' \
51
+ '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' \
52
+ '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-' \
53
+ '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' \
54
+ '-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' \
55
+ '.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' \
56
+ '.2 3.4-12.7-1.7-16.9z'
57
+ end
58
+ end
59
+ end
60
+ end
61
+
62
+ # Don't render if @hide_breadcrumbs is true.
63
+ def render?
64
+ helpers.assigns['hide_breadcrumbs'] != true
65
+ end
66
+
67
+ def breadcrumbs
68
+ helpers.controller.breadcrumbs.map { |e| Breadcrumbs::ComputedElement.new e, helpers }
69
+ end
70
+ end
71
+ 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