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
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Practical::Views::FormBuilders::Base < ActionView::Helpers::FormBuilder
4
+ include Practical::Views::ElementHelper
5
+ attr_reader :template, :object
6
+
7
+ def fieldset(options = {}, &block)
8
+ finalized_options = mix({class: 'wa-stack'}, options)
9
+ template.tag.fieldset(**finalized_options, &block)
10
+ end
11
+
12
+ def legend(options = {}, &block)
13
+ finalized_options = mix({}, options)
14
+ template.tag.legend(**finalized_options, &block)
15
+ end
16
+
17
+ def fieldset_title(icon:, title:)
18
+ template.render(Practical::Views::Form::FieldsetTitleComponent.new) do |component|
19
+ component.with_icon {
20
+ template.render icon
21
+ }
22
+
23
+ title
24
+ end
25
+ end
26
+
27
+ def field_title(icon:, title:)
28
+ template.render(Practical::Views::Form::FieldTitleComponent.new) do |component|
29
+ component.with_icon {
30
+ template.render icon
31
+ }
32
+
33
+ title
34
+ end
35
+ end
36
+
37
+ def input_component(object_method, label_options: {}, &block)
38
+ template.render(Practical::Views::Form::InputComponent.new(
39
+ f: self,
40
+ object_method: object_method,
41
+ label_options: label_options
42
+ ), &block)
43
+ end
44
+
45
+ def practical_editor_field(object_method:, direct_upload_url:, hidden_input_options: {}, options: {})
46
+ aria_describedby_id = field_errors_id(object_method)
47
+ input_id = field_id(object_method)
48
+
49
+ finalized_hidden_input_options = mix({
50
+ "aria-describedby": aria_describedby_id,
51
+ "aria-invalid": errors_for(object_method).present?,
52
+ "id": input_id,
53
+ }, hidden_input_options)
54
+
55
+ template.safe_join([
56
+ self.hidden_field(object_method, **finalized_hidden_input_options),
57
+ template.render(Practical::Views::Form::PracticalEditorComponent.new(
58
+ input_id: input_id,
59
+ aria_describedby_id: aria_describedby_id,
60
+ direct_upload_url: direct_upload_url,
61
+ options: options
62
+ ))
63
+ ])
64
+ end
65
+
66
+ def button_component(options = {}, &block)
67
+ template.render(Practical::Views::ButtonComponent.new(**options), &block)
68
+ end
69
+
70
+ def check_box_collection(field_method:, options:, collection_check_boxes_options: {})
71
+ collection_check_boxes(field_method,
72
+ options,
73
+ :value,
74
+ :title,
75
+ collection_check_boxes_options
76
+ ) do |collection_builder|
77
+ collection_builder.label(class: "wa-flank") do
78
+ template.safe_join([
79
+ collection_builder.checkbox,
80
+ template.render(Practical::Views::Form::OptionLabelComponent.new) do |component|
81
+ component.with_title do
82
+ template.icon_text(
83
+ icon: collection_builder.object.icon,
84
+ text: collection_builder.object.title
85
+ )
86
+ end
87
+
88
+ component.with_description{ collection_builder.object.description }
89
+ end
90
+ ])
91
+ end
92
+ end
93
+ end
94
+
95
+ def radio_collection(field_method:, options:, collection_check_boxes_options: {})
96
+ collection_radio_buttons(field_method,
97
+ options,
98
+ :value,
99
+ :title,
100
+ collection_check_boxes_options
101
+ ) do |collection_builder|
102
+ collection_builder.label(class: "wa-flank") do
103
+ template.safe_join([
104
+ collection_builder.radio_button,
105
+ template.render(Practical::Views::Form::OptionLabelComponent.new) do |component|
106
+ component.with_title do
107
+ template.icon_text(
108
+ icon: collection_builder.object.icon,
109
+ text: collection_builder.object.title
110
+ )
111
+ end
112
+
113
+ component.with_description{ collection_builder.object.description }
114
+ end
115
+ ])
116
+ end
117
+ end
118
+ end
119
+
120
+ def required_radio_collection_wrapper(object_method, options = {}, &block)
121
+ template.render(Practical::Views::Form::RequiredRadioCollectionWrapperComponent.new(
122
+ f: self,
123
+ object_method: object_method,
124
+ options: options
125
+ ), &block)
126
+ end
127
+
128
+ def field_errors(object_method, options = {})
129
+ template.render Practical::Views::Form::FieldErrorsComponent.new(
130
+ f: self,
131
+ object_method: object_method,
132
+ options: options
133
+ )
134
+ end
135
+
136
+ def fallback_error_section(blurb_key: :"practical_framework.forms.generic_error_blurb", id:, options: {})
137
+ template.render Practical::Views::Form::FallbackErrorsSectionComponent.new(
138
+ f: self,
139
+ blurb: template.translate(blurb_key, raise: true),
140
+ options: options
141
+ )
142
+ end
143
+
144
+ def errors_for(object_method)
145
+ return unless object.present? && object_method.present?
146
+ object.errors.group_by_attribute[object_method]
147
+ end
148
+
149
+ def field_errors_id(object_method)
150
+ field_id(object_method, :errors)
151
+ end
152
+ end
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Practical::Views::IconSet
4
+ IconDefinition = Data.define(:method_name, :icon_name, :preset)
5
+ SpritesheetIconDefinition = Data.define(:method_name, :icon_name, :library)
6
+
7
+ Preset = Data.define(:family, :variant)
8
+
9
+ PRESETS = {
10
+ brand: Preset.new(family: :brands, variant: nil),
11
+ duotone: Preset.new(family: :duotone, variant: :solid),
12
+ regular: Preset.new(family: :classic, variant: :regular),
13
+ solid: Preset.new(family: :solid, variant: nil)
14
+ }.freeze
15
+
16
+ def self.presets
17
+ PRESETS
18
+ end
19
+
20
+ def self.icon(**options)
21
+ Practical::Views::IconComponent.new(**options)
22
+ end
23
+
24
+ def self.spritesheet_icon(library:, **options)
25
+ Practical::Views::IconComponent.new(**options.with_defaults(
26
+ family: :default,
27
+ options: {library: library}
28
+ ))
29
+ end
30
+
31
+ presets.keys.each do |label|
32
+ define_singleton_method(:"#{label}_icon") do |**options|
33
+ preset = presets.fetch(label)
34
+ return icon(**options.merge(family: preset.family, variant: preset.variant))
35
+ end
36
+ end
37
+
38
+ def self.define_icons(icon_definitions:)
39
+ icon_definitions.each do |icon_definition|
40
+ define_singleton_method(icon_definition.method_name) do
41
+ preset = presets.fetch(icon_definition.preset.to_sym)
42
+ return icon(
43
+ family: preset.family,
44
+ variant: preset.variant,
45
+ name: icon_definition.icon_name
46
+ )
47
+ end
48
+ end
49
+ end
50
+
51
+ def self.define_spritesheet_icons(spritesheet_icon_definitions:)
52
+ spritesheet_icon_definitions.each do |icon_definition|
53
+ define_singleton_method(icon_definition.method_name) do
54
+ return spritesheet_icon(
55
+ library: icon_definition.library,
56
+ name: icon_definition.icon_name
57
+ )
58
+ end
59
+ end
60
+ end
61
+
62
+ define_icons(icon_definitions: [
63
+ IconDefinition.new(method_name: :user_icon, icon_name: :user, preset: :duotone),
64
+ IconDefinition.new(method_name: :save_icon, icon_name: :"floppy-disk", preset: :duotone),
65
+ IconDefinition.new(method_name: :filters_icon, icon_name: :filters, preset: :duotone),
66
+ IconDefinition.new(method_name: :filters_icon, icon_name: :filters, preset: :duotone),
67
+ IconDefinition.new(method_name: :ascending_icon, icon_name: :"sort-up", preset: :duotone),
68
+ IconDefinition.new(method_name: :descending_icon, icon_name: :"sort-down", preset: :duotone),
69
+
70
+ IconDefinition.new(method_name: :apply_filters_icon, icon_name: :filter, preset: :solid),
71
+ IconDefinition.new(method_name: :generic_add_icon, icon_name: :"circle-plus", preset: :solid),
72
+ IconDefinition.new(method_name: :error_list_icon, icon_name: :"circle-exclamation", preset: :solid),
73
+ IconDefinition.new(method_name: :notes_icon, icon_name: :note, preset: :solid),
74
+ IconDefinition.new(method_name: :dialog_close_icon, icon_name: :xmark, preset: :solid),
75
+
76
+ IconDefinition.new(method_name: :sort_icon, icon_name: :sort, preset: :regular),
77
+
78
+ IconDefinition.new(method_name: :info_icon, icon_name: :"circle-info", preset: :duotone),
79
+ IconDefinition.new(method_name: :signup_icon, icon_name: :"user-plus", preset: :duotone),
80
+ IconDefinition.new(method_name: :send_email_icon, icon_name: :"paper-plane", preset: :duotone),
81
+ IconDefinition.new(method_name: :sent_email_icon, icon_name: :"envelope-dot", preset: :duotone),
82
+ IconDefinition.new(method_name: :link_icon, icon_name: :link, preset: :duotone),
83
+ IconDefinition.new(method_name: :remove_link_icon, icon_name: :"link-slash", preset: :duotone),
84
+ IconDefinition.new(method_name: :theme_icon, icon_name: :swatchbook, preset: :duotone),
85
+ IconDefinition.new(method_name: :sign_out_icon, icon_name: :"person-from-portal", preset: :duotone),
86
+ IconDefinition.new(method_name: :theming_icon, icon_name: :"spray-can-sparkles", preset: :duotone),
87
+ IconDefinition.new(method_name: :share_icon, icon_name: :"share-from-square", preset: :duotone),
88
+ IconDefinition.new(method_name: :caution_diamond_icon, icon_name: :"diamond-exclamation", preset: :duotone),
89
+ IconDefinition.new(method_name: :alert_icon, icon_name: :"triangle-exclamation", preset: :duotone),
90
+ IconDefinition.new(method_name: :swap_icon, icon_name: :swap, preset: :duotone),
91
+ IconDefinition.new(method_name: :waiting_icon, icon_name: :"hourglass-half", preset: :duotone),
92
+ IconDefinition.new(method_name: :accept_icon, icon_name: :"circle-check", preset: :duotone),
93
+ IconDefinition.new(method_name: :deny_icon, icon_name: :"circle-xmark", preset: :duotone),
94
+ IconDefinition.new(method_name: :destroy_user_icon, icon_name: :"user-slash", preset: :duotone),
95
+ IconDefinition.new(method_name: :organization_icon, icon_name: :warehouse, preset: :duotone),
96
+
97
+ IconDefinition.new(method_name: :user_name_icon, icon_name: :"hand-wave", preset: :solid),
98
+ IconDefinition.new(method_name: :email_address_icon, icon_name: :at, preset: :solid),
99
+ IconDefinition.new(method_name: :light_theme_icon, icon_name: :sun, preset: :solid),
100
+ IconDefinition.new(method_name: :dark_theme_icon, icon_name: :moon, preset: :solid),
101
+ IconDefinition.new(method_name: :match_system_theme_icon, icon_name: :"circle-half-stroke", preset: :solid),
102
+ IconDefinition.new(method_name: :previous_arrow, icon_name: :"arrow-left", preset: :solid),
103
+ IconDefinition.new(method_name: :next_arrow, icon_name: :"arrow-right", preset: :solid),
104
+ IconDefinition.new(method_name: :checkbox_check_icon, icon_name: :check, preset: :solid),
105
+ IconDefinition.new(method_name: :passkey_label_icon, icon_name: :tag, preset: :solid),
106
+ IconDefinition.new(method_name: :success_icon, icon_name: :"circle-check", preset: :solid),
107
+ IconDefinition.new(method_name: :memberships_icon, icon_name: :hexagon, preset: :solid),
108
+ IconDefinition.new(method_name: :badge_icon, icon_name: :"id-card", preset: :solid),
109
+ IconDefinition.new(method_name: :close_toast_icon, icon_name: :"circle-xmark", preset: :solid),
110
+ IconDefinition.new(method_name: :home_icon, icon_name: :home, preset: :solid),
111
+
112
+ IconDefinition.new(method_name: :revoke_membership_invitation_icon, icon_name: :"link-slash", preset: :duotone),
113
+
114
+ IconDefinition.new(method_name: :organization_manager_icon, icon_name: "users-gear", preset: :duotone),
115
+ ])
116
+
117
+ define_icons(icon_definitions: [
118
+ IconDefinition.new(method_name: :csv_icon, icon_name: "file-csv", preset: :duotone),
119
+ IconDefinition.new(method_name: :xls_icon, icon_name: "file-xls", preset: :duotone),
120
+ IconDefinition.new(method_name: :doc_icon, icon_name: "file-doc", preset: :duotone),
121
+ IconDefinition.new(method_name: :pdf_icon, icon_name: "file-pdf", preset: :duotone),
122
+ IconDefinition.new(method_name: :heic_icon, icon_name: "file-image", preset: :duotone),
123
+ IconDefinition.new(method_name: :missing_file_icon, icon_name: "file-slash", preset: :solid),
124
+ IconDefinition.new(method_name: :txt_icon, icon_name: "file-txt", preset: :duotone),
125
+ ])
126
+
127
+ define_spritesheet_icons(spritesheet_icon_definitions: [
128
+ SpritesheetIconDefinition.new(method_name: :passkey_icon, icon_name: :passkey, library: :kit),
129
+ SpritesheetIconDefinition.new(method_name: :add_note_icon, icon_name: :"solid-note-circle-plus", library: :kit),
130
+ SpritesheetIconDefinition.new(method_name: :edit_note_icon, icon_name: :"solid-note-pen", library: :kit),
131
+ SpritesheetIconDefinition.new(method_name: :delete_note_icon, icon_name: :"solid-note-slash", library: :kit),
132
+ SpritesheetIconDefinition.new(method_name: :archive_membership_icon, icon_name: :"solid-id-badge-slash", library: :kit),
133
+ SpritesheetIconDefinition.new(method_name: :accepted_membership_invitation_icon, icon_name: :"solid-warehouse-circle-check", library: :kit),
134
+ SpritesheetIconDefinition.new(method_name: :destroy_membership_invitation_icon, icon_name: :"solid-warehouse-slash", library: :kit),
135
+ SpritesheetIconDefinition.new(method_name: :delete_note_icon, icon_name: :"solid-note-slash", library: :kit),
136
+
137
+ ])
138
+
139
+ def self.profile_icon = badge_icon
140
+
141
+ def self.checkbox_indeterminate_icon
142
+ solid_icon(name: :minus, options: {class: "indeterminate-icon"})
143
+ end
144
+
145
+ def self.emergency_passkey_registration_icon
146
+ duotone_icon(name: :"light-emergency-on", options: {style: "--secondary-color: var(--wa-color-danger-fill-loud); --secondary-opacity: 0.7;"})
147
+ end
148
+
149
+ def self.signin_icon
150
+ duotone_icon(name: :"person-to-portal", options: {style: "--secondary-color: var(--wa-color-brand-fill-normal); --secondary-opacity: 0.9;"})
151
+ end
152
+
153
+ def self.signout_icon
154
+ duotone_icon(name: :"person-from-portal", options: {style: "--secondary-color: var(--wa-color-brand-fill-normal); --secondary-opacity: 0.9;"})
155
+ end
156
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Practical::Views::WebAwesome::StyleUtility::AppearanceVariant < Practical::Views::WebAwesome::StyleUtility::Base
4
+ attr_accessor :variants
5
+
6
+ def initialize(variants:)
7
+ if variants.kind_of?(Array)
8
+ variants = variants.map(&:to_s)
9
+ else
10
+ variants = variants.to_s.split(" ")
11
+ end
12
+
13
+ self.variants = Array.wrap(variants)
14
+ end
15
+
16
+ def to_css
17
+ ApplicationController.helpers.class_names(variants.map{|variant| self.class.apply_css_prefix(variant) })
18
+ end
19
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Practical::Views::WebAwesome::StyleUtility::Base
4
+ include Practical::Views::ElementHelper
5
+ def to_web_awesome
6
+ raise NotImplementedError
7
+ end
8
+
9
+ def to_css
10
+ raise NotImplementedError
11
+ end
12
+
13
+ def self.css(**options)
14
+ self.new(**options).to_css
15
+ end
16
+
17
+ def self.apply_css_prefix(value)
18
+ return value if value.start_with?("wa-")
19
+ "wa-#{value}"
20
+ end
21
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Practical::Views::WebAwesome::StyleUtility::ColorVariant < Practical::Views::WebAwesome::StyleUtility::Base
4
+ attr_accessor :variant
5
+
6
+ def initialize(variant:)
7
+ self.variant = variant.to_s
8
+ end
9
+
10
+ def to_web_awesome
11
+ variant
12
+ end
13
+
14
+ def to_css
15
+ self.class.apply_css_prefix(variant)
16
+ end
17
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Practical::Views::WebAwesome::StyleUtility::Size < Practical::Views::WebAwesome::StyleUtility::Base
4
+ attr_accessor :size
5
+
6
+ def initialize(size:)
7
+ self.size = size.to_s
8
+ end
9
+
10
+ def to_css
11
+ self.class.apply_css_prefix(self.class.from_alias(size: size))
12
+ end
13
+
14
+ def self.apply_css_prefix(value)
15
+ return value if value.start_with?("wa-size")
16
+ "wa-size-#{value}"
17
+ end
18
+
19
+ def self.from_alias(size:)
20
+ case size.to_sym
21
+ when :small
22
+ return :s
23
+ when :medium
24
+ return :m
25
+ when :large
26
+ return :l
27
+ else
28
+ return size
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,38 @@
1
+ en:
2
+ emergency_passkey_registrations:
3
+ request_form:
4
+ title:
5
+ text: "Can’t log in?"
6
+
7
+ description:
8
+ html: "<p>Not to worry! You can register a new passkey and get back into your account.</p>"
9
+
10
+ registration_form:
11
+ title:
12
+ text: "Register your new passkey"
13
+
14
+ description:
15
+ html: "<p>Once you register your new passkey, you'll be able to log back into your account.</p>"
16
+
17
+ sent_message: "Sending the instructions to your email!"
18
+ registered_message: "Your passkey has been registered; you can login now!"
19
+ prompt: "Can’t log in?"
20
+ registration:
21
+ prompt: "Create an account"
22
+ signin:
23
+ prompt: "Sign in"
24
+ form:
25
+ title: "Sign in"
26
+ submit_button: "Sign in"
27
+
28
+ user_settings:
29
+ registration:
30
+ form:
31
+ title: Create an account
32
+ submit_button_title: "Sign up"
33
+
34
+ app_navigation:
35
+ user_settings:
36
+ home_title: "Home"
37
+ profile_title: "My Account"
38
+ signout_title: "Sign out"
@@ -0,0 +1,18 @@
1
+ en:
2
+ devise:
3
+ emergency_passkey_registrations:
4
+ credential_missing_or_could_not_be_parsed: "We weren't able to register this passkey."
5
+ webauthn_origin_verification_error: "There was an error setting up this passkey."
6
+ webauthn_challenge_verification_error: "We weren't able to verify this passkey; please try again."
7
+ failure:
8
+ credential_missing_or_could_not_be_parsed: "We weren't able to register this passkey."
9
+ webauthn_origin_verification_error: "There was an error setting up this passkey."
10
+ webauthn_challenge_verification_error: "We weren't able to verify this passkey; please try again."
11
+ stored_credential_not_found: "We weren't able to find this passkey."
12
+ passkeys:
13
+ must_be_at_least_one_passkey: "You need at least 1 passkey to be able to login."
14
+ not_reauthenticated: "You need to verify your identity with an existing passkey first."
15
+ registrations:
16
+ email_missing: "Please provide your email address."
17
+ passkey_label_missing: "Please label your passkey."
18
+ not_reauthenticated: "You need to verify your identity with an existing passkey first."
@@ -0,0 +1,9 @@
1
+ en:
2
+ practical_framework:
3
+ forms:
4
+ generic_error_blurb: "Please fix the following errors:"
5
+ placeholders:
6
+ email: "grace@hopper.me"
7
+ name: "Grace Hopper"
8
+ passkey_label: "iCloud Keychain"
9
+ unlock_title: "Unlock"
data/config/routes.rb ADDED
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ Rails.application.routes.draw do
4
+ end
data/db/seeds/setup.rb ADDED
@@ -0,0 +1,13 @@
1
+ users.defaults(name: -> { Faker::Name.name },
2
+ email: -> { Faker::Internet.email },
3
+ webauthn_id: -> { SecureRandom.uuid }
4
+ )
5
+
6
+
7
+ def users.create_labeled(label, email: labeled_email(label), **)
8
+ create(label, unique_by: :email, email: email, **)
9
+ end
10
+
11
+ def users.labeled_email(label)
12
+ "#{label}@example.com"
13
+ end
@@ -0,0 +1,34 @@
1
+ user_1 = users.create_labeled(:user_1)
2
+
3
+ passkeys.create(:user_1_passkey,
4
+ unique_by: :user,
5
+ user: user_1,
6
+ label: "Passkey 1",
7
+ external_id: SecureRandom.hex,
8
+ public_key: SecureRandom.hex
9
+ )
10
+
11
+ user_1.emergency_passkey_registrations.create!
12
+
13
+ user_2 = users.create_labeled(:user_2)
14
+
15
+ passkeys.create(:user_2_passkey,
16
+ unique_by: :user,
17
+ user: user_2,
18
+ label: "Passkey 2",
19
+ external_id: SecureRandom.hex,
20
+ public_key: SecureRandom.hex
21
+ )
22
+
23
+ passkeys.create(:user_1_passkey_2,
24
+ unique_by: :user,
25
+ user: user_1,
26
+ label: "Passkey 3",
27
+ external_id: SecureRandom.hex,
28
+ public_key: SecureRandom.hex
29
+ )
30
+
31
+ user_1.emergency_passkey_registrations.create!(
32
+ used_at: 1.week.ago,
33
+ passkey: passkeys.user_1_passkey_2
34
+ )
@@ -0,0 +1,8 @@
1
+ Description:
2
+ Creates a new test helper for the Practical Framework, under the namespace Practical::Test::Helpers
3
+
4
+ Example:
5
+ bin/rails generate practical:test:helper assertions
6
+
7
+ This will create:
8
+ app/lib/practical/test/helpers/assertions.rb
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Practical::Test::HelperGenerator < Rails::Generators::NamedBase
4
+ source_root File.expand_path("templates", __dir__)
5
+
6
+ def create_helper
7
+ template "helper.rb", File.join("app/lib/practical/test/helpers", "#{file_path}.rb")
8
+ end
9
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Practical::Test::Helpers::<%= class_name %>
4
+ end
@@ -0,0 +1,9 @@
1
+ Description:
2
+ Creates a new module for tests that should be shared across all apps that use the framework; which can be included
3
+ in an app's test files as needed, in `Practical::Test::Shared`
4
+
5
+ Example:
6
+ bin/rails generate practical:test:shared_test auth/models/user/base
7
+
8
+ This will create:
9
+ app/lib/practical/test/shared/auth/models/user/base.rb
@@ -0,0 +1,7 @@
1
+ class Practical::Test::SharedTestGenerator < Rails::Generators::NamedBase
2
+ source_root File.expand_path("templates", __dir__)
3
+
4
+ def create_shared_test
5
+ template "shared_test.rb", File.join("app/lib/practical/test/shared/", "#{file_path}.rb")
6
+ end
7
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Practical::Test::Shared::<%= class_name %>
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ test "a test shared across apps using the framework"
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ Description:
2
+ Wrapper around the `component` generator, to prefix the given name with `practical/views/` and set the parent to
3
+ `Practical::Views::BaseComponent`
4
+
5
+ Example:
6
+ bin/rails practical:views:component icon name
7
+
8
+ This will create:
9
+ app/components/practical/views/icon_component.rb
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'view_component'
4
+
5
+ class Practical::Views::ComponentGenerator < Rails::Generators::NamedBase
6
+ source_root File.expand_path("templates", __dir__)
7
+
8
+ class_option :parent, default: "Practical::Views::BaseComponent"
9
+ class_option :component, type: :boolean, default: true
10
+
11
+ def prepare_name
12
+ unless class_name.start_with? "Practical::Views"
13
+ self.name = "practical/views/#{name}"
14
+ end
15
+ end
16
+
17
+ hook_for :component, in: :rails, type: :boolean do |instance, generator_klass|
18
+ instance.invoke generator_klass, [ instance.name, *instance.args], instance.options
19
+ end
20
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Practical
4
+ module Framework
5
+ class Engine < ::Rails::Engine
6
+ config.app_generators do |g|
7
+ g.apply_rubocop_autocorrect_after_generate!
8
+ g.test_framework :test_unit, fixture: false, fixture_replacement: :oaken
9
+ end
10
+
11
+ # Hopefully can be removed in the future: https://github.com/rails/rails/issues/39522
12
+ module PracticalFrameworkFormBuilderActiveSupportExtension
13
+ def practical_form_with(**args, &block)
14
+ original_field_error_proc = ::ActionView::Base.field_error_proc
15
+ ::ActionView::Base.field_error_proc = ->(html_tag, instance) { html_tag }
16
+ content_tag(:"practical-framework-error-handling") do
17
+ form_with(**args, &block)
18
+ end
19
+ ensure
20
+ ::ActionView::Base.field_error_proc = original_field_error_proc
21
+ end
22
+ end
23
+
24
+ initializer 'practical-framework.view_helpers' do
25
+ ActiveSupport.on_load(:action_view) do
26
+ include Practical::Framework::Engine::PracticalFrameworkFormBuilderActiveSupportExtension
27
+ include Practical::Helpers::FormWithHelper
28
+ include Practical::Helpers::IconHelper
29
+ include Practical::Helpers::TextHelper
30
+ include Practical::Helpers::TranslationHelper
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Practical::Helpers::FormWithHelper
4
+ def application_form_with(**options, &block)
5
+ finalized_options = options.with_defaults(
6
+ builder: ApplicationFormBuilder
7
+ )
8
+ practical_form_with(**finalized_options, &block)
9
+ end
10
+ end