diff --git a/Gemfile b/Gemfile index b17c88e726e..7c23ddc2fde 100644 --- a/Gemfile +++ b/Gemfile @@ -33,6 +33,7 @@ gem 'jsbundling-rails', '~> 1.0.0' gem 'jwe' gem 'jwt' gem 'lograge', '>= 0.11.2' +gem 'lookbook', '~> 1.2.1', require: false gem 'lru_redux' gem 'maxminddb' gem 'multiset' diff --git a/Gemfile.lock b/Gemfile.lock index 4c2a7d65776..8ce1a825b53 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -345,6 +345,7 @@ GEM thor highline (2.0.3) html_tokenizer (0.0.7) + htmlbeautifier (1.4.2) htmlentities (4.3.4) http_accept_language (2.1.1) i18n (1.12.0) @@ -384,6 +385,19 @@ GEM loofah (2.19.0) crass (~> 1.0.2) nokogiri (>= 1.5.9) + lookbook (1.2.1) + actioncable + activemodel + css_parser + htmlbeautifier (~> 1.3) + htmlentities (~> 4.3.4) + listen (~> 3.0) + railties (>= 5.0) + redcarpet (~> 3.5) + rouge (>= 3.26, < 5.0) + view_component (~> 2.0) + yard (~> 0.9.25) + zeitwerk (~> 2.5) lru_redux (1.1.0) lumberjack (1.2.8) macaddr (1.7.2) @@ -531,6 +545,7 @@ GEM rb-inotify (0.10.1) ffi (~> 1.0) redacted_struct (1.1.0) + redcarpet (3.5.1) redis (4.7.1) redis-namespace (1.8.1) redis (>= 3.0.4) @@ -548,6 +563,7 @@ GEM retries (0.0.5) rexml (3.2.5) rotp (6.2.0) + rouge (4.0.0) rqrcode (2.1.0) chunky_png (~> 1.0) rqrcode_core (~> 1.0) @@ -767,6 +783,7 @@ DEPENDENCIES jwt knapsack lograge (>= 0.11.2) + lookbook (~> 1.2.1) lru_redux maxminddb multiset diff --git a/app/components/block_link_component.html.erb b/app/components/block_link_component.html.erb index 33b72139deb..738bbbcbd76 100644 --- a/app/components/block_link_component.html.erb +++ b/app/components/block_link_component.html.erb @@ -13,6 +13,6 @@ + > <% end %> diff --git a/app/components/download_button_component.rb b/app/components/download_button_component.rb index f4e09f17a2f..579575e89be 100644 --- a/app/components/download_button_component.rb +++ b/app/components/download_button_component.rb @@ -6,7 +6,7 @@ def initialize(file_data:, file_name:, **tag_options) icon: :file_download, action: ->(**tag_options, &block) do link_to( - "data:text/plain;charset=utf-8,#{CGI.escape(file_data)}", + "data:text/plain;charset=utf-8,#{ERB::Util.url_encode(file_data)}", download: file_name, **tag_options, &block diff --git a/app/components/memorable_date_component.rb b/app/components/memorable_date_component.rb index 8587e860702..3139de1925a 100644 --- a/app/components/memorable_date_component.rb +++ b/app/components/memorable_date_component.rb @@ -56,6 +56,10 @@ def initialize( @range_errors = range_errors end + def self.scripts + super + ValidatedFieldComponent.scripts + end + # Get error messages to be provided to the component. # Includes both a hash lookup for general error messages # and an array lookup for custom range error messages. diff --git a/app/components/validated_field_component.rb b/app/components/validated_field_component.rb index 097e215575e..a6abb1a9795 100644 --- a/app/components/validated_field_component.rb +++ b/app/components/validated_field_component.rb @@ -38,6 +38,12 @@ def type_mismatch_error_message end def inferred_input_type - form.send(:default_input_type, name, form.send(:find_attribute_column, name), tag_options) + if form.respond_to?(:default_input_type) + form.send(:default_input_type, name, form.send(:find_attribute_column, name), tag_options) + elsif tag_options.key?(:as) + tag_options[:as] + else + :text + end end end diff --git a/app/controllers/component_preview_controller.rb b/app/controllers/component_preview_controller.rb new file mode 100644 index 00000000000..bf0fc611508 --- /dev/null +++ b/app/controllers/component_preview_controller.rb @@ -0,0 +1,9 @@ +class ComponentPreviewController < ViewComponentsController + include ActionView::Helpers::AssetTagHelper + helper Lookbook::PreviewHelper + include Lookbook::PreviewController + include ScriptHelper + + helper_method :enqueue_component_scripts + alias_method :enqueue_component_scripts, :render_javascript_pack_once_tags +end diff --git a/app/views/layouts/component_preview.html.erb b/app/views/layouts/component_preview.html.erb new file mode 100644 index 00000000000..c94ab975617 --- /dev/null +++ b/app/views/layouts/component_preview.html.erb @@ -0,0 +1,21 @@ + + + + Component Preview + <%= stylesheet_link_tag 'application', media: 'all' %> + + + <% if params[:lookbook][:display][:form] == true %> +
+ <%= yield %> + <%= render SubmitButtonComponent.new(class: 'margin-top-5') do %> + <%= t('forms.buttons.submit.default') %> + <% end %> +
+ <% else %> + <%= yield %> + <% end %> + <%= javascript_packs_tag_once('application', prepend: true) %> + <%= render_javascript_pack_once_tags %> + + diff --git a/config/application.rb b/config/application.rb index d37be6d1246..f0a129044e2 100644 --- a/config/application.rb +++ b/config/application.rb @@ -159,5 +159,20 @@ class Application < Rails::Application # explicitly remove it when we want to disable it config.middleware.delete Rack::Attack end + + config.view_component.show_previews = IdentityConfig.store.component_previews_enabled + if IdentityConfig.store.component_previews_enabled + require 'lookbook' + + config.view_component.preview_controller = 'ComponentPreviewController' + config.view_component.preview_paths = [Rails.root.join('spec', 'components', 'previews')] + config.view_component.default_preview_layout = 'component_preview' + config.lookbook.auto_refresh = false + config.lookbook.project_name = "#{APP_NAME} Component Previews" + config.lookbook.ui_theme = 'blue' + + require 'component_preview_csp' + config.middleware.insert_after ActionDispatch::Static, ComponentPreviewCsp + end end end diff --git a/config/application.yml.default b/config/application.yml.default index 32d91fe3713..4422f7a30f3 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -54,6 +54,7 @@ aws_kms_multi_region_enabled: false backup_code_cost: '2000$8$1$' broken_personal_key_window_start: '2021-07-29T00:00:00Z' broken_personal_key_window_finish: '2021-09-22T00:00:00Z' +component_previews_enabled: false country_phone_number_overrides: '{}' doc_auth_error_dpi_threshold: 290 doc_auth_error_sharpness_threshold: 40 @@ -313,6 +314,7 @@ development: attribute_encryption_key_queue: '[{ "key": "11111111111111111111111111111111" }, { "key": "22222222222222222222222222222222" }]' aws_logo_bucket: '' aws_kms_regions: '["us-west-2", "us-east-1"]' + component_previews_enabled: true dashboard_api_token: test_token dashboard_url: http://localhost:3001/api/service_providers database_host: '' diff --git a/config/initializers/secure_headers.rb b/config/initializers/secure_headers.rb index 8f9769790f4..f4d9078d342 100644 --- a/config/initializers/secure_headers.rb +++ b/config/initializers/secure_headers.rb @@ -4,8 +4,11 @@ hsts: { preload: true, expires: 1.year, subdomains: true }, } + previews_enabled = IdentityConfig.store.rails_mailer_previews_enabled || + IdentityConfig.store.component_previews_enabled + config.action_dispatch.default_headers.merge!( - 'X-Frame-Options' => IdentityConfig.store.rails_mailer_previews_enabled ? 'SAMEORIGIN' : 'DENY', + 'X-Frame-Options' => previews_enabled ? 'SAMEORIGIN' : 'DENY', 'X-XSS-Protection' => '1; mode=block', 'X-Download-Options' => 'noopen', ) diff --git a/config/routes.rb b/config/routes.rb index 77b821f0f33..96939007fc3 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -154,6 +154,11 @@ end end + if IdentityConfig.store.component_previews_enabled + require 'lookbook' + mount Lookbook::Engine, at: '/components' + end + if IdentityConfig.store.lexisnexis_threatmetrix_mock_enabled get '/test/device_profiling' => 'test/device_profiling#index', as: :test_device_profiling_iframe diff --git a/lib/component_preview_csp.rb b/lib/component_preview_csp.rb new file mode 100644 index 00000000000..61b7bd6d39e --- /dev/null +++ b/lib/component_preview_csp.rb @@ -0,0 +1,26 @@ +class ComponentPreviewCsp + COMPONENT_REQUEST_PATH = /^\/components(\/|$)/ + + def initialize(app) + @app = app + end + + def call(env) + status, headers, body = @app.call(env) + request = Rack::Request.new(env) + + if headers['Content-Security-Policy'].present? && request.path.match?(COMPONENT_REQUEST_PATH) + headers['Content-Security-Policy'] = headers['Content-Security-Policy']. + split(';'). + map(&:strip). + map do |directive| + directive. + sub(/^script-src .+/, "script-src * 'unsafe-eval' 'unsafe-inline'"). + sub(/^style-src .+/, "style-src * 'unsafe-inline'") + end. + join(';') + end + + [status, headers, body] + end +end diff --git a/lib/identity_config.rb b/lib/identity_config.rb index 7ea1c6eff01..bb17877e656 100644 --- a/lib/identity_config.rb +++ b/lib/identity_config.rb @@ -116,6 +116,7 @@ def self.build_store(config_map) config.add(:backup_code_cost, type: :string) config.add(:broken_personal_key_window_start, type: :timestamp) config.add(:broken_personal_key_window_finish, type: :timestamp) + config.add(:component_previews_enabled, type: :boolean) config.add(:country_phone_number_overrides, type: :json) config.add(:dashboard_api_token, type: :string) config.add(:dashboard_url, type: :string) diff --git a/spec/components/download_button_component_spec.rb b/spec/components/download_button_component_spec.rb index 1d2768680b6..25ebf2cd99c 100644 --- a/spec/components/download_button_component_spec.rb +++ b/spec/components/download_button_component_spec.rb @@ -15,8 +15,9 @@ subject(:rendered) { render_inline instance } it 'renders link with data and file name' do + expect(rendered).to have_css('lg-download-button') expect(rendered).to have_css( - "lg-download-button a[href*='#{CGI.escape(file_data)}'][download='#{file_name}']", + "a[href='data:text/plain;charset=utf-8,Downloaded%20Text'][download='#{file_name}']", text: t('components.download_button.label'), ) end diff --git a/spec/components/previews/accordion_component_preview.rb b/spec/components/previews/accordion_component_preview.rb new file mode 100644 index 00000000000..8121cfcb833 --- /dev/null +++ b/spec/components/previews/accordion_component_preview.rb @@ -0,0 +1,27 @@ +class AccordionComponentPreview < BaseComponentPreview + # @!group Preview + def default + render(AccordionComponent.new) do |c| + c.header { 'Header' } + 'Content' + end + end + + def unbordered + render(AccordionComponent.new(bordered: false)) do |c| + c.header { 'Header' } + 'Content' + end + end + # @!endgroup + + # @param header text + # @param content text + # @param bordered toggle + def workbench(header: 'Header', content: 'Content', bordered: true) + render(AccordionComponent.new(bordered: bordered)) do |c| + c.header { header } + content + end + end +end diff --git a/spec/components/previews/alert_component_preview.rb b/spec/components/previews/alert_component_preview.rb new file mode 100644 index 00000000000..fe6c520358b --- /dev/null +++ b/spec/components/previews/alert_component_preview.rb @@ -0,0 +1,41 @@ +class AlertComponentPreview < BaseComponentPreview + # @!group Preview + def default + render(AlertComponent.new(message: 'A default message')) + end + + def info + render(AlertComponent.new(message: 'An info message', type: :info)) + end + + def success + render(AlertComponent.new(message: 'A success message', type: :success)) + end + + def warning + render(AlertComponent.new(message: 'A warning message', type: :warning)) + end + + def error + render(AlertComponent.new(message: 'An error message', type: :error)) + end + + def emergency + render(AlertComponent.new(message: 'An emergency message', type: :emergency)) + end + + def other + render(AlertComponent.new(message: 'An other message', type: :other)) + end + + def with_custom_text_tag + render(AlertComponent.new(type: :success, message: 'A custom message', text_tag: 'div')) + end + # @!endgroup + + # @param message text + # @param type select [info, success, warning, error, emergency, other] + def workbench(message: 'An important message', type: :info) + render(AlertComponent.new(message: message, type: type)) + end +end diff --git a/spec/components/previews/barcode_component_preview.rb b/spec/components/previews/barcode_component_preview.rb new file mode 100644 index 00000000000..6c088addf68 --- /dev/null +++ b/spec/components/previews/barcode_component_preview.rb @@ -0,0 +1,13 @@ +class BarcodeComponentPreview < BaseComponentPreview + # @!group Preview + def default + render(BarcodeComponent.new(barcode_data: '1234567812345678', label: 'Barcode')) + end + # @!endgroup + + # @param barcode_data text + # @param label text + def workbench(barcode_data: '1234567812345678', label: 'Barcode') + render(BarcodeComponent.new(barcode_data: barcode_data, label: label)) + end +end diff --git a/spec/components/previews/base_component_preview.rb b/spec/components/previews/base_component_preview.rb new file mode 100644 index 00000000000..ac576c699ed --- /dev/null +++ b/spec/components/previews/base_component_preview.rb @@ -0,0 +1,26 @@ +class BaseComponentPreview < ViewComponent::Preview + private + + def form_builder + @form_builder ||= SimpleForm::FormBuilder.new( + '', + form_instance, + ActionView::Base.new( + ActionView::LookupContext.new(ActionController::Base.view_paths), + {}, + nil, + ), + {}, + ) + end + + def form_instance + nil + end + + # rubocop:disable Layout/LineLength + def example_long_content + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc et tincidunt libero, quis eleifend dui. Quisque dui velit, euismod ac arcu in, vehicula suscipit dui. Vivamus sed justo justo. Nunc a feugiat libero. Nulla dapibus blandit nisl, ac ultrices sapien dapibus ut. Vivamus convallis elementum mi pulvinar elementum. Quisque at aliquet nibh. Donec sed magna ut ipsum auctor dapibus. Proin leo metus, placerat eu finibus sed, consequat eu urna. Nunc tristique purus sollicitudin, luctus nisi eu, commodo tortor. Praesent mattis dictum diam ac sodales.' + end + # rubocop:enable Layout/LineLength +end diff --git a/spec/components/previews/block_link_component_preview.rb b/spec/components/previews/block_link_component_preview.rb new file mode 100644 index 00000000000..5d3df5bcadc --- /dev/null +++ b/spec/components/previews/block_link_component_preview.rb @@ -0,0 +1,18 @@ +class BlockLinkComponentPreview < BaseComponentPreview + # @!group Preview + def default + render(BlockLinkComponent.new(url: '', new_tab: false).with_content('Link text')) + end + + def new_tab + render(BlockLinkComponent.new(url: '', new_tab: true).with_content('Link text')) + end + # @!endgroup + + # @param content text + # @param url text + # @param new_tab toggle + def workbench(content: 'Link text', url: '', new_tab: false) + render(BlockLinkComponent.new(url: url, new_tab: new_tab).with_content(content)) + end +end diff --git a/spec/components/previews/button_component_preview.rb b/spec/components/previews/button_component_preview.rb new file mode 100644 index 00000000000..5d50e988426 --- /dev/null +++ b/spec/components/previews/button_component_preview.rb @@ -0,0 +1,81 @@ +class ButtonComponentPreview < BaseComponentPreview + include ActionView::Context + include ActionView::Helpers::TagHelper + + # @!group Preview + def default + render(ButtonComponent.new.with_content('Button')) + end + + def with_icon + render(ButtonComponent.new(icon: :content_copy).with_content('Button')) + end + + def big + render(ButtonComponent.new(big: true).with_content('Button')) + end + + def wide + render(ButtonComponent.new(wide: true).with_content('Button')) + end + + def full_width + render(ButtonComponent.new(full_width: true).with_content('Button')) + end + + def outline + render(ButtonComponent.new(outline: true).with_content('Button')) + end + + def unstyled + render(ButtonComponent.new(unstyled: true).with_content('Button')) + end + + def danger + render(ButtonComponent.new(danger: true).with_content('Button')) + end + + def with_custom_action + render( + ButtonComponent.new( + action: ->(**tag_options, &block) do + content_tag(:'lg-custom-button', **tag_options, &block) + end, + ).with_content('Button'), + ) + end + # @!endgroup + + # rubocop:disable Layout/LineLength + # @param content text + # @param icon select [~,accessibility_new,accessible_forward,account_balance,account_box,account_circle,add,add_circle,add_circle_outline,alarm,alternate_email,announcement,api,arrow_back,arrow_downward,arrow_drop_down,arrow_drop_up,arrow_forward,arrow_upward,assessment,attach_file,attach_money,autorenew,backpack,bathtub,bedding,bookmark,bug_report,build,calendar_today,campaign,camping,cancel,chat,check,check_box_outline_blank,check_circle,check_circle_outline,checkroom,chevron_left,chevron_right,clean_hands,close,closed_caption,clothes,cloud,code,comment,connect_without_contact,construction,construction_worker,contact_page,content_copy,coronavirus,credit_card,deck,delete,device_thermostat,directions,directions_bike,directions_bus,directions_car,directions_walk,do_not_disturb,do_not_touch,drag_handle,eco,edit,electrical_services,emoji_events,error,error_outline,event,expand_less,expand_more,facebook,fast_forward,fast_rewind,favorite,favorite_border,file_download,file_present,file_upload,filter_alt,filter_list,fingerprint,first_page,flag,flickr,flight,flooding,folder,folder_open,format_quote,format_size,forum,github,grid_view,group_add,groups,hearing,help,help_outline,highlight_off,history,home,hospital,hotel,hourglass_empty,hurricane,identification,image,info,info_outline,insights,instagram,keyboard,label,language,last_page,launch,lightbulb,lightbulb_outline,link,link_off,list,local_cafe,local_fire_department,local_gas_station,local_grocery_store,local_hospital,local_laundry_service,local_library,local_offer,local_parking,local_pharmacy,local_police,local_taxi,location_city,location_on,lock,lock_open,lock_outline,login,logout,loop,mail,mail_outline,map,masks,medical_services,menu,military_tech,more_horiz,more_vert,my_location,navigate_before,navigate_far_before,navigate_far_next,navigate_next,near_me,notifications,notifications_active,notifications_none,notifications_off,park,people,person,pets,phone,photo_camera,print,priority_high,public,push_pin,radio_button_unchecked,rain,reduce_capacity,remove,report,restaurant,rss_feed,safety_divider,sanitizer,save_alt,schedule,school,science,search,security,send,sentiment_dissatisfied,sentiment_neutral,sentiment_satisfied,sentiment_satisfied_alt,sentiment_very_dissatisfied,settings,severe_weather,share,shield,shopping_basket,snow,soap,social_distance,sort_arrow,spellcheck,star,star_half,star_outline,store,support,support_agent,text_fields,thumb_down_alt,thumb_up_alt,timer,toggle_off,toggle_on,topic,tornado,translate,trending_down,trending_up,twitter,undo,unfold_less,unfold_more,update,upload_file,verified,verified_user,visibility,visibility_off,volume_off,warning,wash,wifi,work,youtube,zoom_in,zoom_out,zoom_out_map] + # @param big toggle + # @param wide toggle + # @param full_width toggle + # @param outline toggle + # @param unstyled toggle + # @param danger toggle + def workbench( + content: 'Button', + icon: nil, + big: false, + wide: false, + full_width: false, + outline: false, + unstyled: false, + danger: false + ) + render( + ButtonComponent.new( + icon: icon&.to_sym, + big: big, + wide: wide, + full_width: full_width, + outline: outline, + unstyled: unstyled, + danger: danger, + ).with_content(content), + ) + end + # rubocop:enable Layout/LineLength +end diff --git a/spec/components/previews/clipboard_button_component_preview.rb b/spec/components/previews/clipboard_button_component_preview.rb new file mode 100644 index 00000000000..41196462c97 --- /dev/null +++ b/spec/components/previews/clipboard_button_component_preview.rb @@ -0,0 +1,12 @@ +class ClipboardButtonComponentPreview < BaseComponentPreview + # @!group Preview + def default + render(ClipboardButtonComponent.new(clipboard_text: 'Copied Text')) + end + # @!endgroup + + # @param clipboard_text text + def workbench(clipboard_text: 'Copied Text') + render(ClipboardButtonComponent.new(clipboard_text: clipboard_text)) + end +end diff --git a/spec/components/previews/countdown_component_preview.rb b/spec/components/previews/countdown_component_preview.rb new file mode 100644 index 00000000000..0cbb9be4024 --- /dev/null +++ b/spec/components/previews/countdown_component_preview.rb @@ -0,0 +1,24 @@ +class CountdownComponentPreview < BaseComponentPreview + # @!group Preview + def default + render(CountdownComponent.new(expiration: Time.zone.now + 1.5.minutes)) + end + # @!endgroup + + # @param expiration datetime-local + # @param update_interval number + # @param start_immediately toggle + def workbench( + expiration: Time.zone.now + 1.5.minutes, + update_interval: 1, + start_immediately: true + ) + render( + CountdownComponent.new( + expiration: expiration, + update_interval: update_interval.seconds, + start_immediately: start_immediately, + ), + ) + end +end diff --git a/spec/components/previews/download_button_component_preview.rb b/spec/components/previews/download_button_component_preview.rb new file mode 100644 index 00000000000..e4f4a2787bf --- /dev/null +++ b/spec/components/previews/download_button_component_preview.rb @@ -0,0 +1,13 @@ +class DownloadButtonComponentPreview < BaseComponentPreview + # @!group Preview + def default + render(DownloadButtonComponent.new(file_data: 'File Data', file_name: 'file_name.txt')) + end + # @!endgroup + + # @param file_data text + # @param file_name text + def workbench(file_data: 'File Data', file_name: 'file_name.txt') + render(DownloadButtonComponent.new(file_data: file_data, file_name: file_name)) + end +end diff --git a/spec/components/previews/icon_component_preview.rb b/spec/components/previews/icon_component_preview.rb new file mode 100644 index 00000000000..852bbdc34fc --- /dev/null +++ b/spec/components/previews/icon_component_preview.rb @@ -0,0 +1,14 @@ +class IconComponentPreview < BaseComponentPreview + # @!group Preview + def default + render(IconComponent.new(icon: :content_copy)) + end + # @!endgroup + + # rubocop:disable Layout/LineLength + # @param icon select [~,accessibility_new,accessible_forward,account_balance,account_box,account_circle,add,add_circle,add_circle_outline,alarm,alternate_email,announcement,api,arrow_back,arrow_downward,arrow_drop_down,arrow_drop_up,arrow_forward,arrow_upward,assessment,attach_file,attach_money,autorenew,backpack,bathtub,bedding,bookmark,bug_report,build,calendar_today,campaign,camping,cancel,chat,check,check_box_outline_blank,check_circle,check_circle_outline,checkroom,chevron_left,chevron_right,clean_hands,close,closed_caption,clothes,cloud,code,comment,connect_without_contact,construction,construction_worker,contact_page,content_copy,coronavirus,credit_card,deck,delete,device_thermostat,directions,directions_bike,directions_bus,directions_car,directions_walk,do_not_disturb,do_not_touch,drag_handle,eco,edit,electrical_services,emoji_events,error,error_outline,event,expand_less,expand_more,facebook,fast_forward,fast_rewind,favorite,favorite_border,file_download,file_present,file_upload,filter_alt,filter_list,fingerprint,first_page,flag,flickr,flight,flooding,folder,folder_open,format_quote,format_size,forum,github,grid_view,group_add,groups,hearing,help,help_outline,highlight_off,history,home,hospital,hotel,hourglass_empty,hurricane,identification,image,info,info_outline,insights,instagram,keyboard,label,language,last_page,launch,lightbulb,lightbulb_outline,link,link_off,list,local_cafe,local_fire_department,local_gas_station,local_grocery_store,local_hospital,local_laundry_service,local_library,local_offer,local_parking,local_pharmacy,local_police,local_taxi,location_city,location_on,lock,lock_open,lock_outline,login,logout,loop,mail,mail_outline,map,masks,medical_services,menu,military_tech,more_horiz,more_vert,my_location,navigate_before,navigate_far_before,navigate_far_next,navigate_next,near_me,notifications,notifications_active,notifications_none,notifications_off,park,people,person,pets,phone,photo_camera,print,priority_high,public,push_pin,radio_button_unchecked,rain,reduce_capacity,remove,report,restaurant,rss_feed,safety_divider,sanitizer,save_alt,schedule,school,science,search,security,send,sentiment_dissatisfied,sentiment_neutral,sentiment_satisfied,sentiment_satisfied_alt,sentiment_very_dissatisfied,settings,severe_weather,share,shield,shopping_basket,snow,soap,social_distance,sort_arrow,spellcheck,star,star_half,star_outline,store,support,support_agent,text_fields,thumb_down_alt,thumb_up_alt,timer,toggle_off,toggle_on,topic,tornado,translate,trending_down,trending_up,twitter,undo,unfold_less,unfold_more,update,upload_file,verified,verified_user,visibility,visibility_off,volume_off,warning,wash,wifi,work,youtube,zoom_in,zoom_out,zoom_out_map] + def workbench(icon: :content_copy) + render(IconComponent.new(icon: icon&.to_sym)) + end + # rubocop:enable Layout/LineLength +end diff --git a/spec/components/previews/memorable_date_component_preview.rb b/spec/components/previews/memorable_date_component_preview.rb new file mode 100644 index 00000000000..ecb6dd7777c --- /dev/null +++ b/spec/components/previews/memorable_date_component_preview.rb @@ -0,0 +1,33 @@ +class MemorableDateComponentPreview < BaseComponentPreview + # @!group Preview + # @display form true + def default + render( + MemorableDateComponent.new( + form: form_builder, + name: :date, + label: 'Date', + hint: 'Example: 4 28 1986', + ), + ) + end + # @!endgroup + + # @display form true + # @param label text + # @param hint text + # @param min datetime-local + # @param max datetime-local + def workbench(label: 'Date', min: nil, max: nil, hint: 'Example: 4 28 1986') + render( + MemorableDateComponent.new( + form: form_builder, + name: :date, + label: label, + hint: hint, + min: min, + max: max, + ), + ) + end +end diff --git a/spec/components/previews/page_footer_component_preview.rb b/spec/components/previews/page_footer_component_preview.rb new file mode 100644 index 00000000000..4fb64b173ad --- /dev/null +++ b/spec/components/previews/page_footer_component_preview.rb @@ -0,0 +1,12 @@ +class PageFooterComponentPreview < BaseComponentPreview + # @!group Preview + def default + render(PageFooterComponent.new.with_content('Example Content')) + end + # @!endgroup + + # @param content text + def workbench(content: 'Example Content') + render(PageFooterComponent.new.with_content(content)) + end +end diff --git a/spec/components/previews/password_toggle_component_preview.rb b/spec/components/previews/password_toggle_component_preview.rb new file mode 100644 index 00000000000..8dca3099461 --- /dev/null +++ b/spec/components/previews/password_toggle_component_preview.rb @@ -0,0 +1,20 @@ +class PasswordToggleComponentPreview < BaseComponentPreview + # @!group Preview + def default + render(PasswordToggleComponent.new(form: form_builder)) + end + # @!endgroup + + # @param label text + # @param toggle_label text + # @param toggle_position select [~,top,bottom] + def workbench(label: nil, toggle_label: nil, toggle_position: 'top') + render( + PasswordToggleComponent.new( + form: form_builder, + **{ label: label, toggle_label: toggle_label }.compact, + toggle_position: toggle_position.to_sym, + ), + ) + end +end diff --git a/spec/components/previews/phone_input_component_preview.rb b/spec/components/previews/phone_input_component_preview.rb new file mode 100644 index 00000000000..e93c0c47c27 --- /dev/null +++ b/spec/components/previews/phone_input_component_preview.rb @@ -0,0 +1,37 @@ +class PhoneInputComponentPreview < BaseComponentPreview + # @!group Preview + # @display form true + def default + render(PhoneInputComponent.new(form: form_builder)) + end + + def limited_country_selection + render(PhoneInputComponent.new(form: form_builder, allowed_countries: ['US', 'CA', 'FR'])) + end + + def single_country_selection + render(PhoneInputComponent.new(form: form_builder, allowed_countries: ['US'])) + end + # @!endgroup + + # @display form true + # @param allowed_countries text + def workbench(allowed_countries: 'US,CA,FR') + render( + PhoneInputComponent.new( + form: form_builder, + allowed_countries: allowed_countries.split(','), + ), + ) + end + + private + + def form_instance + Class.new do + def international_code; end + + def phone; end + end.new + end +end diff --git a/spec/components/previews/process_list_component_preview.rb b/spec/components/previews/process_list_component_preview.rb new file mode 100644 index 00000000000..27b1a43d7a8 --- /dev/null +++ b/spec/components/previews/process_list_component_preview.rb @@ -0,0 +1,40 @@ +class ProcessListComponentPreview < BaseComponentPreview + # @!group Preview + def default + render(ProcessListComponent.new) do |c| + c.item(heading: 'Item 1') { 'Item 1 Content' } + c.item(heading: 'Item 2') { 'Item 2 Content' } + end + end + + def connected + render(ProcessListComponent.new(connected: true)) do |c| + c.item(heading: 'Item 1') { 'Item 1 Content' } + c.item(heading: 'Item 2') { 'Item 2 Content' } + end + end + + def big + render(ProcessListComponent.new(big: true)) do |c| + c.item(heading: 'Item 1') { 'Item 1 Content' } + c.item(heading: 'Item 2') { 'Item 2 Content' } + end + end + + def big_and_connected + render(ProcessListComponent.new(big: true, connected: true)) do |c| + c.item(heading: 'Item 1') { 'Item 1 Content' } + c.item(heading: 'Item 2') { 'Item 2 Content' } + end + end + # @!endgroup + + # @param connected toggle + # @param big toggle + def workbench(big: false, connected: false) + render(ProcessListComponent.new(big: big, connected: connected)) do |c| + c.item(heading: 'Item 1') { 'Item 1 Content' } + c.item(heading: 'Item 2') { 'Item 2 Content' } + end + end +end diff --git a/spec/components/previews/spinner_button_component_preview.rb b/spec/components/previews/spinner_button_component_preview.rb new file mode 100644 index 00000000000..94e1da7cbd5 --- /dev/null +++ b/spec/components/previews/spinner_button_component_preview.rb @@ -0,0 +1,28 @@ +class SpinnerButtonComponentPreview < BaseComponentPreview + # @!group Preview + def default + render(SpinnerButtonComponent.new(big: true).with_content('Submit')) + end + + def action_message + render( + SpinnerButtonComponent.new( + big: true, + action_message: 'Verifying…', + ).with_content('Submit'), + ) + end + # @!endgroup + + # @display form true + # @param action_message text + def workbench(action_message: nil) + render( + SpinnerButtonComponent.new( + form: form_builder, + big: true, + **{ action_message: action_message }.compact, + ).with_content('Submit'), + ) + end +end diff --git a/spec/components/previews/status_page_component_preview.rb b/spec/components/previews/status_page_component_preview.rb new file mode 100644 index 00000000000..c821f4873c0 --- /dev/null +++ b/spec/components/previews/status_page_component_preview.rb @@ -0,0 +1,32 @@ +class StatusPageComponentPreview < BaseComponentPreview + # @!group Preview + def error + render(StatusPageComponent.new(status: :error).with_content(example_long_content)) + end + + def error_with_lock_icon + render(StatusPageComponent.new(status: :error, icon: :lock).with_content(example_long_content)) + end + + def warning + render(StatusPageComponent.new(status: :warning).with_content(example_long_content)) + end + + def info_with_question_icon + render( + StatusPageComponent.new(status: :info, icon: :question).with_content(example_long_content), + ) + end + # @!endgroup + + # @param status select [~,info,warning,error] + # @param icon select [~,question,lock] + def workbench(status: 'error', icon: nil) + render( + StatusPageComponent.new( + status: status.to_sym, + **{ icon: icon&.to_sym }.compact, + ).with_content(example_long_content), + ) + end +end diff --git a/spec/components/previews/step_indicator_component_preview.rb b/spec/components/previews/step_indicator_component_preview.rb new file mode 100644 index 00000000000..6064e079bca --- /dev/null +++ b/spec/components/previews/step_indicator_component_preview.rb @@ -0,0 +1,28 @@ +class StepIndicatorComponentPreview < BaseComponentPreview + # @!group Preview + def default + render StepIndicatorComponent.new( + steps: [ + { name: :first_step, title: 'First Step' }, + { name: :second_step, title: 'Second Step' }, + { name: :third_step, title: 'Third Step' }, + { name: :fourth_step, title: 'Fourth Step' }, + ], + current_step: :second, + ) + end + # @!endgroup + + # @param current_step select [~,First Step,Second Step,Third Step,Fourth Step] + def workbench(current_step: 'Second Step') + render StepIndicatorComponent.new( + steps: [ + { name: :first_step, title: 'First Step' }, + { name: :second_step, title: 'Second Step' }, + { name: :third_step, title: 'Third Step' }, + { name: :fourth_step, title: 'Fourth Step' }, + ], + current_step: current_step.underscore.tr(' ', '_').to_sym, + ) + end +end diff --git a/spec/components/previews/time_component_preview.rb b/spec/components/previews/time_component_preview.rb new file mode 100644 index 00000000000..f744e9ba39c --- /dev/null +++ b/spec/components/previews/time_component_preview.rb @@ -0,0 +1,12 @@ +class TimeComponentPreview < BaseComponentPreview + # @!group Preview + def default + render TimeComponent.new(time: Time.zone.now + 5.hours) + end + # @!endgroup + + # @param time datetime-local + def workbench(time: Time.zone.now + 5.hours) + render TimeComponent.new(time: time) + end +end diff --git a/spec/components/previews/troubleshooting_options_component_preview.rb b/spec/components/previews/troubleshooting_options_component_preview.rb new file mode 100644 index 00000000000..b56a4ebf1fd --- /dev/null +++ b/spec/components/previews/troubleshooting_options_component_preview.rb @@ -0,0 +1,22 @@ +class TroubleshootingOptionsComponentPreview < BaseComponentPreview + # @!group Preview + def default + render(TroubleshootingOptionsComponent.new) do |c| + c.header { 'Header' } + c.option(url: '') { 'Option 1' } + c.option(url: '') { 'Option 2' } + c.option(url: '', new_tab: true) { 'Option 3 (New Tab)' } + end + end + # @!endgroup + + # @param header text + def workbench(header: 'Header') + render(TroubleshootingOptionsComponent.new) do |c| + c.header { header } + c.option(url: '') { 'Option 1' } + c.option(url: '') { 'Option 2' } + c.option(url: '', new_tab: true) { 'Option 3 (New Tab)' } + end + end +end diff --git a/spec/components/previews/validated_field_component_preview.rb b/spec/components/previews/validated_field_component_preview.rb new file mode 100644 index 00000000000..73f7b2a1e5d --- /dev/null +++ b/spec/components/previews/validated_field_component_preview.rb @@ -0,0 +1,65 @@ +class ValidatedFieldComponentPreview < BaseComponentPreview + # @!group Preview + # @display form true + def text_field + render( + ValidatedFieldComponent.new( + form: form_builder, + name: :text_field, + label: 'Text Field', + required: false, + ), + ) + end + + def required_text_field + render( + ValidatedFieldComponent.new( + form: form_builder, + name: :required_text_field, + label: 'Required Text Field', + required: true, + ), + ) + end + + def email_address + render( + ValidatedFieldComponent.new( + form: form_builder, + name: :email_address, + label: 'Email Address', + as: :email, + ), + ) + end + + def required_checkbox + render( + ValidatedFieldComponent.new( + form: form_builder, + name: :required_checkbox, + label: 'Required Checkbox', + as: :boolean, + required: true, + ), + ) + end + # @!endgroup + + # @display form true + # @param label text + # @param required toggle + # @param input_type select [~,Text,Email Address,Boolean] + def workbench(label: 'Input', required: true, input_type: 'Text') + render( + ValidatedFieldComponent.new( + form: form_builder, + name: :input, + label: label, + required: required, + as: input_type.underscore.tr(' ', '_').to_sym, + ), + ) + end +end