practical 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +37 -0
  3. data/Rakefile +10 -0
  4. data/app/components/practical/views/base_component.rb +6 -0
  5. data/app/components/practical/views/button_component.rb +27 -0
  6. data/app/components/practical/views/datatable/filter_applied.rb +25 -0
  7. data/app/components/practical/views/datatable/filter_section_component.html.erb +9 -0
  8. data/app/components/practical/views/datatable/filter_section_component.rb +19 -0
  9. data/app/components/practical/views/datatable/sort_link_component.rb +48 -0
  10. data/app/components/practical/views/datatable.rb +36 -0
  11. data/app/components/practical/views/flash_messages_component.rb +65 -0
  12. data/app/components/practical/views/form/error_list_component.rb +15 -0
  13. data/app/components/practical/views/form/error_list_item_component.rb +20 -0
  14. data/app/components/practical/views/form/error_list_item_template_component.rb +9 -0
  15. data/app/components/practical/views/form/fallback_errors_section_component.html.erb +7 -0
  16. data/app/components/practical/views/form/fallback_errors_section_component.rb +21 -0
  17. data/app/components/practical/views/form/field_errors_component.rb +28 -0
  18. data/app/components/practical/views/form/field_title_component.rb +23 -0
  19. data/app/components/practical/views/form/fieldset_title_component.rb +20 -0
  20. data/app/components/practical/views/form/input_component.html.erb +7 -0
  21. data/app/components/practical/views/form/input_component.rb +22 -0
  22. data/app/components/practical/views/form/option_label_component.rb +21 -0
  23. data/app/components/practical/views/form/practical_editor_component.rb +26 -0
  24. data/app/components/practical/views/form/required_radio_collection_wrapper_component.rb +23 -0
  25. data/app/components/practical/views/form_wrapper.rb +21 -0
  26. data/app/components/practical/views/icon_component.rb +36 -0
  27. data/app/components/practical/views/icon_for_file_extension_component.rb +53 -0
  28. data/app/components/practical/views/modal_dialog_component.html.erb +10 -0
  29. data/app/components/practical/views/modal_dialog_component.rb +16 -0
  30. data/app/components/practical/views/navigation/breadcrumb_item_component.rb +20 -0
  31. data/app/components/practical/views/navigation/breadcrumbs_component.html.erb +31 -0
  32. data/app/components/practical/views/navigation/breadcrumbs_component.rb +41 -0
  33. data/app/components/practical/views/navigation/navigation_link_component.rb +39 -0
  34. data/app/components/practical/views/navigation/pagination/goto_form_component.html.erb +31 -0
  35. data/app/components/practical/views/navigation/pagination/goto_form_component.rb +34 -0
  36. data/app/components/practical/views/navigation/pagination_component.html.erb +11 -0
  37. data/app/components/practical/views/navigation/pagination_component.rb +98 -0
  38. data/app/components/practical/views/open_dialog_button_component.rb +16 -0
  39. data/app/components/practical/views/page_component.html.erb +53 -0
  40. data/app/components/practical/views/page_component.rb +12 -0
  41. data/app/components/practical/views/relative_time_component.rb +13 -0
  42. data/app/components/practical/views/tiptap_document_component.rb +311 -0
  43. data/app/components/practical/views/toast_component.html.erb +26 -0
  44. data/app/components/practical/views/toast_component.rb +19 -0
  45. data/app/controllers/concerns/practical/auth/passkeys/emergency_registrations.rb +57 -0
  46. data/app/controllers/concerns/practical/auth/passkeys/web_authn_debug_context.rb +13 -0
  47. data/app/controllers/concerns/practical/views/flash_helpers.rb +37 -0
  48. data/app/controllers/concerns/practical/views/json_redirection.rb +7 -0
  49. data/app/lib/practical/defaults/shrine.rb +48 -0
  50. data/app/lib/practical/test/helpers/administrator/test_helpers.rb +7 -0
  51. data/app/lib/practical/test/helpers/extra_assertions.rb +7 -0
  52. data/app/lib/practical/test/helpers/flash_assertions.rb +8 -0
  53. data/app/lib/practical/test/helpers/integration/assertions.rb +23 -0
  54. data/app/lib/practical/test/helpers/passkey/system/base.rb +52 -0
  55. data/app/lib/practical/test/helpers/passkey/system/rack_test.rb +45 -0
  56. data/app/lib/practical/test/helpers/passkey/system/selenium.rb +107 -0
  57. data/app/lib/practical/test/helpers/passkey/test_helper.rb +128 -0
  58. data/app/lib/practical/test/helpers/postmark.rb +11 -0
  59. data/app/lib/practical/test/helpers/relation_builder_assertions.rb +18 -0
  60. data/app/lib/practical/test/helpers/setup/debug.rb +8 -0
  61. data/app/lib/practical/test/helpers/setup/faker_seed_pinning.rb +8 -0
  62. data/app/lib/practical/test/helpers/setup/simplecov.rb +17 -0
  63. data/app/lib/practical/test/helpers/shrine/test_data.rb +101 -0
  64. data/app/lib/practical/test/helpers/spy_assertions.rb +7 -0
  65. data/app/lib/practical/test/helpers/system/assertions.rb +33 -0
  66. data/app/lib/practical/test/helpers/system/capybara_prep.rb +10 -0
  67. data/app/lib/practical/test/shared/auth/passkeys/controllers/emergency_registration/base.rb +372 -0
  68. data/app/lib/practical/test/shared/auth/passkeys/controllers/emergency_registration/self_service.rb +66 -0
  69. data/app/lib/practical/test/shared/auth/passkeys/controllers/reauthentication/base.rb +119 -0
  70. data/app/lib/practical/test/shared/auth/passkeys/controllers/registrations/no_self_destroy.rb +13 -0
  71. data/app/lib/practical/test/shared/auth/passkeys/controllers/registrations/no_self_signup.rb +22 -0
  72. data/app/lib/practical/test/shared/auth/passkeys/controllers/registrations/self_destroy.rb +134 -0
  73. data/app/lib/practical/test/shared/auth/passkeys/controllers/registrations/self_signup.rb +221 -0
  74. data/app/lib/practical/test/shared/auth/passkeys/controllers/registrations/update.rb +220 -0
  75. data/app/lib/practical/test/shared/auth/passkeys/controllers/sessions/base.rb +108 -0
  76. data/app/lib/practical/test/shared/auth/passkeys/forms/emergency_registration.rb +82 -0
  77. data/app/lib/practical/test/shared/auth/passkeys/models/emergency_registration/base.rb +89 -0
  78. data/app/lib/practical/test/shared/auth/passkeys/models/emergency_registration/use_for_and_notify.rb +48 -0
  79. data/app/lib/practical/test/shared/auth/passkeys/models/passkey.rb +101 -0
  80. data/app/lib/practical/test/shared/auth/passkeys/models/resource_with_passkeys.rb +57 -0
  81. data/app/lib/practical/test/shared/auth/passkeys/policies/passkey.rb +18 -0
  82. data/app/lib/practical/test/shared/auth/passkeys/services/send_emergency_registration.rb +41 -0
  83. data/app/lib/practical/test/shared/models/normalized_email.rb +23 -0
  84. data/app/lib/practical/test/shared/models/user.rb +27 -0
  85. data/app/lib/practical/test/shared/models/utility/ip_address.rb +42 -0
  86. data/app/lib/practical/test/shared/models/utility/user_agent.rb +43 -0
  87. data/app/lib/practical/views/button/styling.rb +23 -0
  88. data/app/lib/practical/views/error_handling.rb +33 -0
  89. data/app/lib/practical/views/form_builders/base.rb +152 -0
  90. data/app/lib/practical/views/icon_set.rb +156 -0
  91. data/app/lib/practical/views/web_awesome/style_utility/appearance_variant.rb +19 -0
  92. data/app/lib/practical/views/web_awesome/style_utility/base.rb +21 -0
  93. data/app/lib/practical/views/web_awesome/style_utility/color_variant.rb +17 -0
  94. data/app/lib/practical/views/web_awesome/style_utility/size.rb +31 -0
  95. data/config/locales/auth.en.yml +38 -0
  96. data/config/locales/devise.passkeys.en.yml +18 -0
  97. data/config/locales/practical_framework.en.yml +9 -0
  98. data/config/routes.rb +4 -0
  99. data/db/seeds/setup.rb +13 -0
  100. data/db/seeds/users/default.rb +34 -0
  101. data/lib/generators/practical/test/helper/USAGE +8 -0
  102. data/lib/generators/practical/test/helper/helper_generator.rb +9 -0
  103. data/lib/generators/practical/test/helper/templates/helper.rb.tt +4 -0
  104. data/lib/generators/practical/test/shared_test/USAGE +9 -0
  105. data/lib/generators/practical/test/shared_test/shared_test_generator.rb +7 -0
  106. data/lib/generators/practical/test/shared_test/templates/shared_test.rb.tt +9 -0
  107. data/lib/generators/practical/views/component/USAGE +9 -0
  108. data/lib/generators/practical/views/component/component_generator.rb +20 -0
  109. data/lib/practical/framework/engine.rb +35 -0
  110. data/lib/practical/helpers/form_with_helper.rb +10 -0
  111. data/lib/practical/helpers/icon_helper.rb +18 -0
  112. data/lib/practical/helpers/text_helper.rb +20 -0
  113. data/lib/practical/helpers/translation_helper.rb +25 -0
  114. data/lib/practical/version.rb +5 -0
  115. data/lib/practical/views/element_helper.rb +48 -0
  116. data/lib/practical.rb +21 -0
  117. data/lib/tasks/practical/coverage.rake +19 -0
  118. data/lib/tasks/practical/framework_tasks.rake +6 -0
  119. metadata +303 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: da7aaa72d992ff3aaf4a43ed5c374c6ff1652843075713e08fa95ab1244d88bf
4
+ data.tar.gz: 6c935a7c4cc3ac3538aa78c2f100b6b009b3113577b53bfba7b39efed5a3878d
5
+ SHA512:
6
+ metadata.gz: 88fd0246800371d155ccba2fc6337127a82e31e5ba41fb793bef3558149307914f095cb3e4fa620d86ca34a097f44fcf9278491667dc21bcd10856f04f453f2e
7
+ data.tar.gz: d7c98265992ff1ed00a0e9c6562bcebe239e749e4364345c5a364224c6835226909ae7e39802941867d6daa063e26739e02adb74556c5148927fa22ba682953c
data/README.md ADDED
@@ -0,0 +1,37 @@
1
+ # `practical`: The Practical Framework
2
+
3
+ _This gem is currently being open-sourced from a private repo, while also being singificantly reorganized. Its 3.0 release will be live soon!_
4
+
5
+ ## Installation
6
+
7
+ TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
8
+
9
+ Install the gem and add to the application's Gemfile by executing:
10
+
11
+ $ bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
12
+
13
+ If bundler is not being used to manage dependencies, install the gem by executing:
14
+
15
+ $ gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
16
+
17
+ ## Usage
18
+
19
+ TODO: Write usage instructions here
20
+
21
+ ## Development
22
+
23
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
24
+
25
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
26
+
27
+ ## Contributing
28
+
29
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/practical. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/practical/blob/main/CODE_OF_CONDUCT.md).
30
+
31
+ ## License
32
+
33
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
34
+
35
+ ## Code of Conduct
36
+
37
+ Everyone interacting in the Practical project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/practical/blob/main/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/setup"
4
+
5
+ APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
6
+ load "rails/tasks/engine.rake"
7
+
8
+ load "rails/tasks/statistics.rake"
9
+
10
+ require "bundler/gem_tasks"
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Practical::Views::BaseComponent < ViewComponent::Base
4
+ include Practical::Views::ElementHelper
5
+ delegate :safe_join, :icon_set, to: :helpers
6
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Practical::Views::ButtonComponent < Practical::Views::BaseComponent
4
+ include Practical::Views::Button::Styling
5
+ attr_accessor :type, :appearance, :color_variant, :size, :options
6
+
7
+ def initialize(type:, appearance: nil, color_variant: nil, size: nil, options: {})
8
+ @type = type
9
+ @options = options
10
+ initialize_style_utilities(appearance: appearance, color_variant: color_variant, size: size)
11
+ end
12
+
13
+ def call
14
+ defaults = {
15
+ type: type,
16
+ class: css_classes_from_style_utilities
17
+ }
18
+
19
+ if type.to_sym == :submit
20
+ defaults[:data] = { disable: true }
21
+ end
22
+
23
+ finalized_options = mix(defaults, options)
24
+
25
+ tag.button(content, **finalized_options)
26
+ end
27
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Practical::Views::Datatable::FilterApplied
4
+ extend ActiveSupport::Concern
5
+
6
+ def dialog_button_appearance
7
+ if custom_filters_applied?
8
+ return "filled outlined"
9
+ else
10
+ return "outlined"
11
+ end
12
+ end
13
+
14
+ def custom_filters_applied?
15
+ datatable_form.filters.to_h.compact != datatable_form.class.default_payload[:filters]
16
+ end
17
+
18
+ def filter_icon
19
+ if custom_filters_applied?
20
+ icon_set.filters_icon
21
+ else
22
+ icon_set.apply_filters_icon
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,9 @@
1
+ <section class="datatable-filters wa-cluster wa-callout wa-neutral wa-outlined wa-align-items-center wa-size-s">
2
+ <%= open_filters_button %>
3
+
4
+ <section class="wa-cluster">
5
+ <% applied_filters.each do |applied_filter| %>
6
+ <%= applied_filter %>
7
+ <% end %>
8
+ </section>
9
+ </section>
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Practical::Views::Datatable::FilterSectionComponent < Practical::Views::BaseComponent
4
+ renders_one :open_filters_button
5
+ renders_many :applied_filters, "AppliedFilterComponent"
6
+
7
+ class Practical::Views::Datatable::FilterSectionComponent::AppliedFilterComponent < Practical::Views::BaseComponent
8
+ renders_one :title
9
+
10
+ def call
11
+ tag.section(class: "wa-stack wa-gap-0") do
12
+ safe_join([
13
+ tag.strong(title),
14
+ content
15
+ ])
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Practical::Views::Datatable::SortLinkComponent < Practical::Views::BaseComponent
4
+ attr_accessor :url, :datatable_form, :sort_key, :options
5
+
6
+ def initialize(url:, datatable_form:, sort_key:, options: {})
7
+ self.url = url
8
+ self.datatable_form = datatable_form
9
+ self.sort_key = sort_key
10
+ self.options = options
11
+ end
12
+
13
+ def call
14
+ tag.a(**mix({href: url}, options)) {
15
+ safe_join([
16
+ render(icon),
17
+ content
18
+ ])
19
+ }
20
+ end
21
+
22
+ def icon
23
+ case sort_direction
24
+ when "desc"
25
+ return icon_set.descending_icon
26
+ when "asc"
27
+ return icon_set.ascending_icon
28
+ else
29
+ return icon_set.sort_icon
30
+ end
31
+ end
32
+
33
+ def sort_direction
34
+ datatable_form.sort_direction_for(key: sort_key)
35
+ end
36
+
37
+ def self.inverted_sort_direction(datatable_form:, sort_key:)
38
+ datatable_form.inverted_sort_direction_for(key: sort_key)
39
+ end
40
+
41
+ def self.merged_payload(datatable_form:, sort_key:)
42
+ datatable_form.merged_payload(sort_key: sort_key,
43
+ sort_direction: inverted_sort_direction(datatable_form: datatable_form,
44
+ sort_key: sort_key
45
+ )
46
+ )
47
+ end
48
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Practical::Views::Datatable
4
+ extend ActiveSupport::Concern
5
+
6
+ def sort_link_for(key:, &block)
7
+ component = Practical::Views::Datatable::SortLinkComponent.new(
8
+ url: sort_url_for(key: key),
9
+ datatable_form: datatable_form,
10
+ sort_key: key,
11
+ options: {class: "wa-flank wa-gap-2xs"}
12
+ )
13
+
14
+ render(component, &block)
15
+ end
16
+
17
+ def inverted_sort_direction(datatable_form:, sort_key:)
18
+ datatable_form.inverted_sort_direction_for(key: sort_key)
19
+ end
20
+
21
+ def merged_payload(datatable_form:, sort_key:)
22
+ datatable_form.merged_payload(sort_key: sort_key,
23
+ sort_direction: inverted_sort_direction(datatable_form: datatable_form,
24
+ sort_key: sort_key
25
+ )
26
+ )
27
+ end
28
+
29
+ def pagination_component
30
+ return Practical::Views::Navigation::PaginationComponent.new(
31
+ pagy: pagy,
32
+ request: request,
33
+ i18n_key: "pagy.item_name"
34
+ )
35
+ end
36
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Practical::Views::FlashMessagesComponent < Practical::Views::BaseComponent
4
+ def call
5
+ tag.aside(class: 'notification-messages wa-stack') do
6
+ safe_join([
7
+ alert_toast,
8
+ notice_toast,
9
+ success_toast,
10
+ ])
11
+ end
12
+ end
13
+
14
+ def success_toast
15
+ render_toast(
16
+ color_variant: :success,
17
+ data: helpers.flash[:success],
18
+ default_icon: icon_set.success_icon
19
+ )
20
+ end
21
+
22
+ def notice_toast
23
+ render_toast(
24
+ color_variant: :neutral,
25
+ data: helpers.flash[:notice],
26
+ default_icon: icon_set.info_icon
27
+ )
28
+ end
29
+
30
+ def alert_toast
31
+ render_toast(
32
+ color_variant: :warning,
33
+ data: helpers.flash[:alert],
34
+ default_icon: icon_set.alert_icon
35
+ )
36
+ end
37
+
38
+ def render_toast(color_variant:, data:, default_icon:)
39
+ return nil if data.nil?
40
+
41
+ case data
42
+ when String
43
+ message = data
44
+ icon = default_icon
45
+ when Hash
46
+ data = data.with_indifferent_access
47
+ message = data[:message]
48
+ icon = data[:icon]
49
+ end
50
+
51
+
52
+ component = Practical::Views::ToastComponent.new(color_variant: color_variant)
53
+
54
+ render component do |component|
55
+ if icon.present? && icon.is_a?(Hash)
56
+ icon = Practical::Views::IconComponent.new(**icon.to_h.symbolize_keys)
57
+ component.with_icon do
58
+ render icon
59
+ end
60
+ end
61
+
62
+ message
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Practical::Views::Form::ErrorListComponent < Practical::Views::BaseComponent
4
+ attr_reader :errors
5
+
6
+ def initialize(errors:)
7
+ @errors = errors
8
+ end
9
+
10
+ def call
11
+ tag.ul(class: 'error-list') {
12
+ safe_join(errors.map{|error| render Practical::Views::Form::ErrorListItemComponent.new(error: error) })
13
+ }
14
+ end
15
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Practical::Views::Form::ErrorListItemComponent < Practical::Views::BaseComponent
4
+ attr_reader :error
5
+
6
+ def initialize(error:)
7
+ @error = error
8
+ end
9
+
10
+ def before_render
11
+ error.options[:has_been_rendered] = true
12
+ end
13
+
14
+ def call
15
+ tag.li(class: 'wa-flank', data: {"error-type": error.type}) {
16
+ render(icon_set.error_list_icon) +
17
+ tag.span(error.message)
18
+ }
19
+ end
20
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Practical::Views::Form::ErrorListItemTemplateComponent < Practical::Views::BaseComponent
4
+ def call
5
+ tag.template(id: 'error-list-item-template') {
6
+ render Practical::Views::Form::ErrorListItemComponent.new(error: ActiveModel::Error.new(nil, nil, nil))
7
+ }
8
+ end
9
+ end
@@ -0,0 +1,7 @@
1
+ <%= tag.section(**finalized_options) do %>
2
+ <header>
3
+ <strong><%= blurb %></strong>
4
+ </header>
5
+
6
+ <%= render Practical::Views::Form::ErrorListComponent.new(errors: remaining_errors) %>
7
+ <% end %>
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Practical::Views::Form::FallbackErrorsSectionComponent < Practical::Views::BaseComponent
4
+ attr_reader :f, :blurb
5
+ def initialize(f:, blurb:, options:)
6
+ @f = f
7
+ @blurb = blurb
8
+ @options = options
9
+ end
10
+
11
+ def finalized_options
12
+ mix({
13
+ class: ["error-section", "fallback-error-section", "wa-callout", "wa-danger"]
14
+ }, @options)
15
+ end
16
+
17
+ def remaining_errors
18
+ return [] if f.object.try(:errors).blank?
19
+ return f.object.errors.reject{|error| error.options[:has_been_rendered] }
20
+ end
21
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Practical::Views::Form::FieldErrorsComponent < Practical::Views::BaseComponent
4
+ attr_reader :f, :object_method, :options
5
+
6
+ def initialize(f:, object_method:, options:)
7
+ @f = f
8
+ @object_method = object_method
9
+ @options = options
10
+ end
11
+
12
+ def call
13
+ id = f.field_errors_id(object_method)
14
+ classes = ["error-section", "wa-callout", "wa-danger"]
15
+ errors = f.errors_for(object_method)
16
+
17
+ if errors.blank?
18
+ classes << ["no-server-errors"]
19
+ errors = []
20
+ end
21
+
22
+ finalized_options = mix({id: id, class: classes}, options)
23
+
24
+ return label(object_method, nil, finalized_options) {
25
+ render Practical::Views::Form::ErrorListComponent.new(errors: errors)
26
+ }
27
+ end
28
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Practical::Views::Form::FieldTitleComponent < Practical::Views::BaseComponent
4
+ attr_accessor :options
5
+ renders_one :icon
6
+
7
+ def initialize(options: {})
8
+ self.options = options
9
+ end
10
+
11
+ def flank_class
12
+ return "wa-flank" if icon?
13
+ end
14
+
15
+ def call
16
+ tag.section(**mix({class: [flank_class, "field-title"]}, options)) {
17
+ safe_join([
18
+ (icon if icon?),
19
+ tag.div{ content }
20
+ ])
21
+ }
22
+ end
23
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Practical::Views::Form::FieldsetTitleComponent < Practical::Views::BaseComponent
4
+ attr_accessor :options
5
+ renders_one :icon
6
+
7
+ def initialize(options: {})
8
+ self.options = options
9
+ end
10
+
11
+ def call
12
+ tag.span(**mix({}, options)) {
13
+ safe_join([
14
+ (icon if icon?),
15
+ " ",
16
+ tag.span{ content }
17
+ ])
18
+ }
19
+ end
20
+ end
@@ -0,0 +1,7 @@
1
+ <section class='wa-stack wa-gap-3xs'>
2
+ <%= tag.label(**label_options) do %>
3
+ <%= label %>
4
+ <%= field %>
5
+ <% end %>
6
+ <%= f.field_errors(object_method) %>
7
+ </section>
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Practical::Views::Form::InputComponent < Practical::Views::BaseComponent
4
+ attr_accessor :f, :object_method, :label_options
5
+
6
+ renders_one :label
7
+ renders_one :field
8
+
9
+ def initialize(f:, object_method:, label_options: {})
10
+ self.f = f
11
+ self.object_method = object_method
12
+ self.label_options = label_options
13
+ end
14
+
15
+ def field_errors_id
16
+ f.field_errors_id(object_method)
17
+ end
18
+
19
+ def field_options(**options)
20
+ mix({"aria-describedby": field_errors_id}, options)
21
+ end
22
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Practical::Views::Form::OptionLabelComponent < Practical::Views::BaseComponent
4
+ renders_one :title
5
+ renders_one :description
6
+ attr_accessor :options
7
+
8
+ def initialize(options: {})
9
+ self.options = options
10
+ end
11
+
12
+
13
+ def call
14
+ tag.section(**mix({class: "wa-stack wa-size-s wa-gap-0"}, options)) {
15
+ safe_join([
16
+ tag.span(title),
17
+ tag.small(description, class: "wa-quiet"),
18
+ ])
19
+ }
20
+ end
21
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Practical::Views::Form::PracticalEditorComponent < Practical::Views::BaseComponent
4
+ attr_accessor :input_id, :aria_describedby_id, :direct_upload_url, :options
5
+
6
+ def initialize(input_id:, aria_describedby_id:, direct_upload_url:, options: {})
7
+ self.input_id = input_id
8
+ self.aria_describedby_id = aria_describedby_id
9
+ self.direct_upload_url = direct_upload_url
10
+ self.options = options
11
+ end
12
+
13
+ def finalized_options
14
+ mix({
15
+ input: input_id,
16
+ serializer: :json,
17
+ data: {direct_upload_url: direct_upload_url}
18
+ }, options)
19
+ end
20
+
21
+ def call
22
+ tag.practical_editor(**finalized_options) {
23
+ tag.div("slot": :editor, "aria-describedby": aria_describedby_id)
24
+ }
25
+ end
26
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Practical::Views::Form::RequiredRadioCollectionWrapperComponent < Practical::Views::BaseComponent
4
+ attr_reader :f, :object_method, :options
5
+ def initialize(f:, object_method:, options:)
6
+ @f = f
7
+ @object_method = object_method
8
+ @options = options
9
+ end
10
+
11
+ def finalized_options
12
+ mix({
13
+ "fieldset": f.field_id(object_method),
14
+ "field-name": f.field_name(object_method),
15
+ "errors-element": f.field_id(object_method, :errors),
16
+ "errors-element-aria": f.field_id(object_method, :errors_aria),
17
+ }, options)
18
+ end
19
+
20
+ def call
21
+ content_tag(:"required-radio-collection", content, **finalized_options)
22
+ end
23
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Practical::Views::FormWrapper
4
+ extend ActiveSupport::Concern
5
+
6
+ def generic_errors_id
7
+ dom_id(form, :generic_errors)
8
+ end
9
+
10
+ def wrapped_form_with(**options, &block)
11
+ finalized_options = mix({
12
+ html: {
13
+ "aria-describedby": generic_errors_id,
14
+ }
15
+ }, options)
16
+ helpers.application_form_with(
17
+ **finalized_options,
18
+ &block
19
+ )
20
+ end
21
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Practical::Views::IconComponent < Practical::Views::BaseComponent
4
+ include ActiveModel::Serializers::JSON
5
+
6
+ attr_accessor :name, :family, :variant, :fixed_width, :label, :options
7
+
8
+ def attributes=(hash)
9
+ hash.each do |key, value|
10
+ public_send("#{key}=", value)
11
+ end
12
+ end
13
+
14
+ def attributes
15
+ { "name" => nil, "family" => nil, "variant" => nil, "fixed_width" => nil, "label" => nil, "options" => nil }
16
+ end
17
+
18
+ def initialize(name:, family:, variant: nil, fixed_width: true, label: nil, options: {})
19
+ self.name = name
20
+ self.family = family
21
+ self.variant = variant
22
+ self.fixed_width = fixed_width
23
+ self.label = label
24
+ self.options = options
25
+ end
26
+
27
+ def call
28
+ tag.wa_icon(**mix({
29
+ "name": name,
30
+ "family": family,
31
+ "variant": variant,
32
+ "fixed-width": fixed_width,
33
+ "label": label
34
+ }, options))
35
+ end
36
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Practical::Views::IconForFileExtensionComponent < Practical::Views::BaseComponent
4
+ attr_reader :extension
5
+
6
+ def initialize(extension:)
7
+ raise ArgumentError unless self.class.supported_extension?(extension: extension)
8
+ @extension = extension.to_sym
9
+ end
10
+
11
+ def call
12
+ case extension
13
+ when :csv
14
+ render_icon(method_name: :csv_icon)
15
+ when :pdf
16
+ render_icon(method_name: :pdf_icon)
17
+ when :doc, :docx
18
+ render_icon(method_name: :doc_icon)
19
+ when :xls, :xlsx
20
+ render_icon(method_name: :xls_icon)
21
+ when :heic
22
+ render_icon(method_name: :heic_icon)
23
+ when :missing
24
+ render_icon(method_name: :missing_file_icon)
25
+ else
26
+ render_icon(method_name: :txt_icon)
27
+ end
28
+ end
29
+
30
+ def render_icon(method_name:)
31
+ render icon_set.public_send(method_name)
32
+ end
33
+
34
+ def self.supported_extension?(extension:)
35
+ supported_extensions.include?(extension.to_sym)
36
+ end
37
+
38
+ def self.supported_extensions
39
+ %i(
40
+ csv
41
+ pdf
42
+ docx
43
+ xlsx
44
+ doc
45
+ xls
46
+ heic
47
+ missing
48
+ txt
49
+ rtf
50
+ numbers
51
+ ).freeze
52
+ end
53
+ end