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.
- checksums.yaml +7 -0
- data/README.md +37 -0
- data/Rakefile +10 -0
- data/app/components/practical/views/base_component.rb +6 -0
- data/app/components/practical/views/button_component.rb +27 -0
- data/app/components/practical/views/datatable/filter_applied.rb +25 -0
- data/app/components/practical/views/datatable/filter_section_component.html.erb +9 -0
- data/app/components/practical/views/datatable/filter_section_component.rb +19 -0
- data/app/components/practical/views/datatable/sort_link_component.rb +48 -0
- data/app/components/practical/views/datatable.rb +36 -0
- data/app/components/practical/views/flash_messages_component.rb +65 -0
- data/app/components/practical/views/form/error_list_component.rb +15 -0
- data/app/components/practical/views/form/error_list_item_component.rb +20 -0
- data/app/components/practical/views/form/error_list_item_template_component.rb +9 -0
- data/app/components/practical/views/form/fallback_errors_section_component.html.erb +7 -0
- data/app/components/practical/views/form/fallback_errors_section_component.rb +21 -0
- data/app/components/practical/views/form/field_errors_component.rb +28 -0
- data/app/components/practical/views/form/field_title_component.rb +23 -0
- data/app/components/practical/views/form/fieldset_title_component.rb +20 -0
- data/app/components/practical/views/form/input_component.html.erb +7 -0
- data/app/components/practical/views/form/input_component.rb +22 -0
- data/app/components/practical/views/form/option_label_component.rb +21 -0
- data/app/components/practical/views/form/practical_editor_component.rb +26 -0
- data/app/components/practical/views/form/required_radio_collection_wrapper_component.rb +23 -0
- data/app/components/practical/views/form_wrapper.rb +21 -0
- data/app/components/practical/views/icon_component.rb +36 -0
- data/app/components/practical/views/icon_for_file_extension_component.rb +53 -0
- data/app/components/practical/views/modal_dialog_component.html.erb +10 -0
- data/app/components/practical/views/modal_dialog_component.rb +16 -0
- data/app/components/practical/views/navigation/breadcrumb_item_component.rb +20 -0
- data/app/components/practical/views/navigation/breadcrumbs_component.html.erb +31 -0
- data/app/components/practical/views/navigation/breadcrumbs_component.rb +41 -0
- data/app/components/practical/views/navigation/navigation_link_component.rb +39 -0
- data/app/components/practical/views/navigation/pagination/goto_form_component.html.erb +31 -0
- data/app/components/practical/views/navigation/pagination/goto_form_component.rb +34 -0
- data/app/components/practical/views/navigation/pagination_component.html.erb +11 -0
- data/app/components/practical/views/navigation/pagination_component.rb +98 -0
- data/app/components/practical/views/open_dialog_button_component.rb +16 -0
- data/app/components/practical/views/page_component.html.erb +53 -0
- data/app/components/practical/views/page_component.rb +12 -0
- data/app/components/practical/views/relative_time_component.rb +13 -0
- data/app/components/practical/views/tiptap_document_component.rb +311 -0
- data/app/components/practical/views/toast_component.html.erb +26 -0
- data/app/components/practical/views/toast_component.rb +19 -0
- data/app/controllers/concerns/practical/auth/passkeys/emergency_registrations.rb +57 -0
- data/app/controllers/concerns/practical/auth/passkeys/web_authn_debug_context.rb +13 -0
- data/app/controllers/concerns/practical/views/flash_helpers.rb +37 -0
- data/app/controllers/concerns/practical/views/json_redirection.rb +7 -0
- data/app/lib/practical/defaults/shrine.rb +48 -0
- data/app/lib/practical/test/helpers/administrator/test_helpers.rb +7 -0
- data/app/lib/practical/test/helpers/extra_assertions.rb +7 -0
- data/app/lib/practical/test/helpers/flash_assertions.rb +8 -0
- data/app/lib/practical/test/helpers/integration/assertions.rb +23 -0
- data/app/lib/practical/test/helpers/passkey/system/base.rb +52 -0
- data/app/lib/practical/test/helpers/passkey/system/rack_test.rb +45 -0
- data/app/lib/practical/test/helpers/passkey/system/selenium.rb +107 -0
- data/app/lib/practical/test/helpers/passkey/test_helper.rb +128 -0
- data/app/lib/practical/test/helpers/postmark.rb +11 -0
- data/app/lib/practical/test/helpers/relation_builder_assertions.rb +18 -0
- data/app/lib/practical/test/helpers/setup/debug.rb +8 -0
- data/app/lib/practical/test/helpers/setup/faker_seed_pinning.rb +8 -0
- data/app/lib/practical/test/helpers/setup/simplecov.rb +17 -0
- data/app/lib/practical/test/helpers/shrine/test_data.rb +101 -0
- data/app/lib/practical/test/helpers/spy_assertions.rb +7 -0
- data/app/lib/practical/test/helpers/system/assertions.rb +33 -0
- data/app/lib/practical/test/helpers/system/capybara_prep.rb +10 -0
- data/app/lib/practical/test/shared/auth/passkeys/controllers/emergency_registration/base.rb +372 -0
- data/app/lib/practical/test/shared/auth/passkeys/controllers/emergency_registration/self_service.rb +66 -0
- data/app/lib/practical/test/shared/auth/passkeys/controllers/reauthentication/base.rb +119 -0
- data/app/lib/practical/test/shared/auth/passkeys/controllers/registrations/no_self_destroy.rb +13 -0
- data/app/lib/practical/test/shared/auth/passkeys/controllers/registrations/no_self_signup.rb +22 -0
- data/app/lib/practical/test/shared/auth/passkeys/controllers/registrations/self_destroy.rb +134 -0
- data/app/lib/practical/test/shared/auth/passkeys/controllers/registrations/self_signup.rb +221 -0
- data/app/lib/practical/test/shared/auth/passkeys/controllers/registrations/update.rb +220 -0
- data/app/lib/practical/test/shared/auth/passkeys/controllers/sessions/base.rb +108 -0
- data/app/lib/practical/test/shared/auth/passkeys/forms/emergency_registration.rb +82 -0
- data/app/lib/practical/test/shared/auth/passkeys/models/emergency_registration/base.rb +89 -0
- data/app/lib/practical/test/shared/auth/passkeys/models/emergency_registration/use_for_and_notify.rb +48 -0
- data/app/lib/practical/test/shared/auth/passkeys/models/passkey.rb +101 -0
- data/app/lib/practical/test/shared/auth/passkeys/models/resource_with_passkeys.rb +57 -0
- data/app/lib/practical/test/shared/auth/passkeys/policies/passkey.rb +18 -0
- data/app/lib/practical/test/shared/auth/passkeys/services/send_emergency_registration.rb +41 -0
- data/app/lib/practical/test/shared/models/normalized_email.rb +23 -0
- data/app/lib/practical/test/shared/models/user.rb +27 -0
- data/app/lib/practical/test/shared/models/utility/ip_address.rb +42 -0
- data/app/lib/practical/test/shared/models/utility/user_agent.rb +43 -0
- data/app/lib/practical/views/button/styling.rb +23 -0
- data/app/lib/practical/views/error_handling.rb +33 -0
- data/app/lib/practical/views/form_builders/base.rb +152 -0
- data/app/lib/practical/views/icon_set.rb +156 -0
- data/app/lib/practical/views/web_awesome/style_utility/appearance_variant.rb +19 -0
- data/app/lib/practical/views/web_awesome/style_utility/base.rb +21 -0
- data/app/lib/practical/views/web_awesome/style_utility/color_variant.rb +17 -0
- data/app/lib/practical/views/web_awesome/style_utility/size.rb +31 -0
- data/config/locales/auth.en.yml +38 -0
- data/config/locales/devise.passkeys.en.yml +18 -0
- data/config/locales/practical_framework.en.yml +9 -0
- data/config/routes.rb +4 -0
- data/db/seeds/setup.rb +13 -0
- data/db/seeds/users/default.rb +34 -0
- data/lib/generators/practical/test/helper/USAGE +8 -0
- data/lib/generators/practical/test/helper/helper_generator.rb +9 -0
- data/lib/generators/practical/test/helper/templates/helper.rb.tt +4 -0
- data/lib/generators/practical/test/shared_test/USAGE +9 -0
- data/lib/generators/practical/test/shared_test/shared_test_generator.rb +7 -0
- data/lib/generators/practical/test/shared_test/templates/shared_test.rb.tt +9 -0
- data/lib/generators/practical/views/component/USAGE +9 -0
- data/lib/generators/practical/views/component/component_generator.rb +20 -0
- data/lib/practical/framework/engine.rb +35 -0
- data/lib/practical/helpers/form_with_helper.rb +10 -0
- data/lib/practical/helpers/icon_helper.rb +18 -0
- data/lib/practical/helpers/text_helper.rb +20 -0
- data/lib/practical/helpers/translation_helper.rb +25 -0
- data/lib/practical/version.rb +5 -0
- data/lib/practical/views/element_helper.rb +48 -0
- data/lib/practical.rb +21 -0
- data/lib/tasks/practical/coverage.rake +19 -0
- data/lib/tasks/practical/framework_tasks.rake +6 -0
- 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."
|
data/config/routes.rb
ADDED
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,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,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,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
|