baldur 0.1.1

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 (164) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +3 -0
  3. data/LICENSE +21 -0
  4. data/README.md +318 -0
  5. data/TODO.md +6 -0
  6. data/app/assets/javascripts/baldur/controllers/accordion_controller.js +148 -0
  7. data/app/assets/javascripts/baldur/controllers/alert_controller.js +209 -0
  8. data/app/assets/javascripts/baldur/controllers/date_field_controller.js +558 -0
  9. data/app/assets/javascripts/baldur/controllers/details_menu_controller.js +30 -0
  10. data/app/assets/javascripts/baldur/controllers/form_submit_controller.js +7 -0
  11. data/app/assets/javascripts/baldur/controllers/marketing_pricing_controller.js +47 -0
  12. data/app/assets/javascripts/baldur/controllers/marketing_tabs_controller.js +118 -0
  13. data/app/assets/javascripts/baldur/controllers/menu_select_controller.js +401 -0
  14. data/app/assets/javascripts/baldur/controllers/mobile_sidebar_controller.js +13 -0
  15. data/app/assets/javascripts/baldur/controllers/modal_controller.js +149 -0
  16. data/app/assets/javascripts/baldur/controllers/panel_right_controller.js +1 -0
  17. data/app/assets/javascripts/baldur/controllers/panel_secondary_controller.js +129 -0
  18. data/app/assets/javascripts/baldur/controllers/segmented_tabs_controller.js +38 -0
  19. data/app/assets/javascripts/baldur/controllers/sidebar_controller.js +77 -0
  20. data/app/assets/javascripts/baldur/controllers/smooth_scroll_controller.js +29 -0
  21. data/app/assets/javascripts/baldur/controllers/snackbar_controller.js +158 -0
  22. data/app/assets/javascripts/baldur/controllers/table_disclosure_controller.js +46 -0
  23. data/app/assets/javascripts/baldur/controllers/theme_controller.js +90 -0
  24. data/app/assets/javascripts/baldur/controllers/tooltip_controller.js +136 -0
  25. data/app/assets/javascripts/baldur/lib/animation-helpers.js +56 -0
  26. data/app/assets/javascripts/baldur/lib/dom-helpers.js +80 -0
  27. data/app/assets/javascripts/baldur/lib/field-validation-helpers.js +36 -0
  28. data/app/assets/javascripts/baldur/lib/focus-management.js +89 -0
  29. data/app/assets/javascripts/baldur/lib/formatting-helpers.js +100 -0
  30. data/app/assets/javascripts/baldur/lib/lucide.js +20 -0
  31. data/app/assets/javascripts/baldur/lib/snackbar.js +50 -0
  32. data/app/assets/javascripts/baldur/lib/storage-helpers.js +50 -0
  33. data/app/assets/stylesheets/baldur/application/components/alert.css +226 -0
  34. data/app/assets/stylesheets/baldur/application/components/app_bar.css +41 -0
  35. data/app/assets/stylesheets/baldur/application/components/button.css +173 -0
  36. data/app/assets/stylesheets/baldur/application/components/card.css +63 -0
  37. data/app/assets/stylesheets/baldur/application/components/chart.css +40 -0
  38. data/app/assets/stylesheets/baldur/application/components/chip.css +51 -0
  39. data/app/assets/stylesheets/baldur/application/components/dialog.css +81 -0
  40. data/app/assets/stylesheets/baldur/application/components/forms.css +624 -0
  41. data/app/assets/stylesheets/baldur/application/components/layout.css +2 -0
  42. data/app/assets/stylesheets/baldur/application/components/list.css +15 -0
  43. data/app/assets/stylesheets/baldur/application/components/menu.css +300 -0
  44. data/app/assets/stylesheets/baldur/application/components/panel-right.css +1 -0
  45. data/app/assets/stylesheets/baldur/application/components/panel-secondary.css +71 -0
  46. data/app/assets/stylesheets/baldur/application/components/progress.css +84 -0
  47. data/app/assets/stylesheets/baldur/application/components/segmented-buttons.css +117 -0
  48. data/app/assets/stylesheets/baldur/application/components/settings-nav.css +84 -0
  49. data/app/assets/stylesheets/baldur/application/components/sidebar.css +123 -0
  50. data/app/assets/stylesheets/baldur/application/components/snackbar.css +179 -0
  51. data/app/assets/stylesheets/baldur/application/components/stepper.css +124 -0
  52. data/app/assets/stylesheets/baldur/application/components/switch.css +105 -0
  53. data/app/assets/stylesheets/baldur/application/components/table.css +331 -0
  54. data/app/assets/stylesheets/baldur/application/components/timeline.css +184 -0
  55. data/app/assets/stylesheets/baldur/application/components/utilities.css +180 -0
  56. data/app/assets/stylesheets/baldur/application/global.css +125 -0
  57. data/app/assets/stylesheets/baldur/application/marketing/layout.css +36 -0
  58. data/app/assets/stylesheets/baldur/application/motion.css +125 -0
  59. data/app/assets/stylesheets/baldur/application/theme.css +329 -0
  60. data/app/assets/stylesheets/baldur/theme/dark.css +90 -0
  61. data/app/assets/stylesheets/baldur/theme/light.css +82 -0
  62. data/app/assets/stylesheets/baldur.css +27 -0
  63. data/app/assets/stylesheets/baldur_panel_right.css +1 -0
  64. data/app/assets/stylesheets/baldur_panel_secondary.css +1 -0
  65. data/app/assets/tailwind/baldur/engine.css +5 -0
  66. data/app/helpers/baldur/compatibility/ui_aliases.rb +7 -0
  67. data/app/helpers/baldur/marketing_helper.rb +121 -0
  68. data/app/helpers/baldur/optional/auth_page_helper.rb +17 -0
  69. data/app/helpers/baldur/optional/google_auth_helper.rb +16 -0
  70. data/app/helpers/baldur/optional/panel_right_helper.rb +7 -0
  71. data/app/helpers/baldur/optional/panel_secondary_helper.rb +26 -0
  72. data/app/helpers/baldur/render_helper.rb +13 -0
  73. data/app/helpers/baldur/ui_helper.rb +217 -0
  74. data/app/helpers/baldur/ui_helper_feedback.rb +93 -0
  75. data/app/helpers/baldur/ui_helper_forms.rb +230 -0
  76. data/app/helpers/baldur/ui_helper_unavailable.rb +98 -0
  77. data/app/views/baldur/components/_accordion.html.erb +30 -0
  78. data/app/views/baldur/components/_action_row.html.erb +6 -0
  79. data/app/views/baldur/components/_alert.html.erb +61 -0
  80. data/app/views/baldur/components/_badge.html.erb +25 -0
  81. data/app/views/baldur/components/_button.html.erb +81 -0
  82. data/app/views/baldur/components/_card.html.erb +40 -0
  83. data/app/views/baldur/components/_chart_card.html.erb +42 -0
  84. data/app/views/baldur/components/_checkbox.html.erb +27 -0
  85. data/app/views/baldur/components/_date_field.html.erb +43 -0
  86. data/app/views/baldur/components/_google_sign_in_button.html.erb +1 -0
  87. data/app/views/baldur/components/_kebab_menu.html.erb +36 -0
  88. data/app/views/baldur/components/_kpi.html.erb +45 -0
  89. data/app/views/baldur/components/_menu_select.html.erb +78 -0
  90. data/app/views/baldur/components/_modal.html.erb +54 -0
  91. data/app/views/baldur/components/_pagination.html.erb +61 -0
  92. data/app/views/baldur/components/_segmented_buttons.html.erb +51 -0
  93. data/app/views/baldur/components/_settings_nav.html.erb +41 -0
  94. data/app/views/baldur/components/_snackbar.html.erb +42 -0
  95. data/app/views/baldur/components/_snackbar_stack.html.erb +13 -0
  96. data/app/views/baldur/components/_stepper.html.erb +39 -0
  97. data/app/views/baldur/components/_table.html.erb +117 -0
  98. data/app/views/baldur/components/_table_card.html.erb +86 -0
  99. data/app/views/baldur/components/_table_footer.html.erb +68 -0
  100. data/app/views/baldur/components/_text_field.html.erb +33 -0
  101. data/app/views/baldur/components/_tooltip.html.erb +73 -0
  102. data/app/views/baldur/marketing/_cta_banner.html.erb +20 -0
  103. data/app/views/baldur/marketing/_faq_section.html.erb +37 -0
  104. data/app/views/baldur/marketing/_features_section.html.erb +67 -0
  105. data/app/views/baldur/marketing/_footer.html.erb +38 -0
  106. data/app/views/baldur/marketing/_hero_section.html.erb +259 -0
  107. data/app/views/baldur/marketing/_pricing_tables.html.erb +99 -0
  108. data/app/views/baldur/marketing/_testimonials_section.html.erb +80 -0
  109. data/app/views/baldur/marketing/_top_nav.html.erb +28 -0
  110. data/app/views/baldur/optional/_auth_page.html.erb +21 -0
  111. data/app/views/baldur/optional/_google_sign_in_button.html.erb +19 -0
  112. data/app/views/baldur/optional/_panel_right.html.erb +1 -0
  113. data/app/views/baldur/optional/_panel_secondary.html.erb +34 -0
  114. data/baldur.gemspec +30 -0
  115. data/config/importmap.rb +2 -0
  116. data/lib/baldur/configuration.rb +24 -0
  117. data/lib/baldur/engine.rb +10 -0
  118. data/lib/baldur/version.rb +3 -0
  119. data/lib/baldur.rb +17 -0
  120. data/lib/generators/baldur/install/install_generator.rb +113 -0
  121. data/lib/generators/baldur/install/templates/baldur_initializer.rb +19 -0
  122. data/lib/generators/baldur/install/templates/fonts.css +14 -0
  123. data/lib/generators/baldur/install/templates/theme.css +27 -0
  124. data/lib/generators/baldur/install/templates/ui_helper.rb +4 -0
  125. data/lib/generators/baldur/install_google_auth/install_google_auth_generator.rb +15 -0
  126. data/lib/generators/baldur/install_panel_right/install_panel_right_generator.rb +9 -0
  127. data/lib/generators/baldur/install_panel_secondary/install_panel_secondary_generator.rb +21 -0
  128. data/script/verify_host_install +111 -0
  129. data/test/gemspec_test.rb +11 -0
  130. data/test/install_generator_test.rb +35 -0
  131. data/test/install_panel_secondary_generator_test.rb +21 -0
  132. data/test/marketing_helper_test.rb +38 -0
  133. data/test/run_all.rb +3 -0
  134. data/test/test_helper.rb +9 -0
  135. data/test/tmp/install_generator/app/assets/stylesheets/fonts.css +14 -0
  136. data/test/tmp/install_generator/app/assets/stylesheets/theme.css +27 -0
  137. data/test/tmp/install_generator/app/assets/tailwind/application.css +4 -0
  138. data/test/tmp/install_generator/app/helpers/ui_helper.rb +4 -0
  139. data/test/tmp/install_generator/app/javascript/controllers/accordion_controller.js +1 -0
  140. data/test/tmp/install_generator/app/javascript/controllers/date_field_controller.js +1 -0
  141. data/test/tmp/install_generator/app/javascript/controllers/details_menu_controller.js +1 -0
  142. data/test/tmp/install_generator/app/javascript/controllers/form_submit_controller.js +1 -0
  143. data/test/tmp/install_generator/app/javascript/controllers/marketing_pricing_controller.js +1 -0
  144. data/test/tmp/install_generator/app/javascript/controllers/marketing_tabs_controller.js +1 -0
  145. data/test/tmp/install_generator/app/javascript/controllers/menu_select_controller.js +1 -0
  146. data/test/tmp/install_generator/app/javascript/controllers/modal_controller.js +1 -0
  147. data/test/tmp/install_generator/app/javascript/controllers/segmented_tabs_controller.js +1 -0
  148. data/test/tmp/install_generator/app/javascript/controllers/sidebar_controller.js +1 -0
  149. data/test/tmp/install_generator/app/javascript/controllers/smooth_scroll_controller.js +1 -0
  150. data/test/tmp/install_generator/app/javascript/controllers/snackbar_controller.js +1 -0
  151. data/test/tmp/install_generator/app/javascript/controllers/theme_controller.js +1 -0
  152. data/test/tmp/install_generator/app/javascript/controllers/tooltip_controller.js +1 -0
  153. data/test/tmp/install_generator/app/javascript/lib/animation-helpers.js +1 -0
  154. data/test/tmp/install_generator/app/javascript/lib/dom-helpers.js +1 -0
  155. data/test/tmp/install_generator/app/javascript/lib/field-validation-helpers.js +1 -0
  156. data/test/tmp/install_generator/app/javascript/lib/focus-management.js +1 -0
  157. data/test/tmp/install_generator/app/javascript/lib/formatting-helpers.js +1 -0
  158. data/test/tmp/install_generator/app/javascript/lib/snackbar.js +1 -0
  159. data/test/tmp/install_generator/app/javascript/lib/storage-helpers.js +1 -0
  160. data/test/tmp/install_generator/config/initializers/baldur.rb +19 -0
  161. data/test/tmp/install_panel_secondary_generator/app/assets/tailwind/application.css +2 -0
  162. data/test/tmp/install_panel_secondary_generator/app/helpers/panel_secondary_helper.rb +3 -0
  163. data/test/tmp/install_panel_secondary_generator/app/javascript/controllers/panel_secondary_controller.js +1 -0
  164. metadata +259 -0
@@ -0,0 +1,86 @@
1
+ <%
2
+ actions_content = if actions.respond_to?(:call)
3
+ capture(&actions)
4
+ else
5
+ actions
6
+ end
7
+ title_meta_content = if title_meta.respond_to?(:call)
8
+ capture(&title_meta)
9
+ else
10
+ title_meta
11
+ end
12
+ controls_content = if controls.respond_to?(:call)
13
+ capture(&controls)
14
+ else
15
+ controls
16
+ end
17
+ footer_content = if footer.respond_to?(:call)
18
+ capture(&footer)
19
+ else
20
+ footer
21
+ end
22
+ controls_position = local_assigns[:controls_position].to_s == "header" ? "header" : "row"
23
+ header_controls_content = controls_position == "header" ? controls_content : nil
24
+ row_controls_content = controls_position == "row" ? controls_content : nil
25
+ header_present = title.present? || title_meta_content.present? || description.present? || actions_content.present? || header_controls_content.present?
26
+ controls_present = row_controls_content.present?
27
+ footer_present = footer_content.present?
28
+ card_classes = ["table-card", local_assigns[:class]].compact.join(" ")
29
+ %>
30
+ <div class="<%= card_classes %>">
31
+ <% if header_present %>
32
+ <div class="table-card__header table-card__section--top">
33
+ <div class="table-card__header-main">
34
+ <% if title.present? || title_meta_content.present? %>
35
+ <div class="table-card__header-title-row">
36
+ <% if title.present? %>
37
+ <h3 class="text-lg font-semibold text-[color:var(--color-on-surface)]"><%= title %></h3>
38
+ <% end %>
39
+ <% if title_meta_content.present? %>
40
+ <div class="table-card__title-meta"><%= title_meta_content %></div>
41
+ <% end %>
42
+ </div>
43
+ <% end %>
44
+ <% if description.present? %>
45
+ <p class="text-sm text-muted"><%= description %></p>
46
+ <% end %>
47
+ </div>
48
+ <% if actions_content.present? || header_controls_content.present? %>
49
+ <div class="table-card__header-side">
50
+ <% if actions_content.present? %>
51
+ <div class="table-card__header-actions">
52
+ <%= actions_content %>
53
+ </div>
54
+ <% end %>
55
+ <% if header_controls_content.present? %>
56
+ <div class="table-card__header-controls">
57
+ <%= header_controls_content %>
58
+ </div>
59
+ <% end %>
60
+ </div>
61
+ <% end %>
62
+ </div>
63
+ <% end %>
64
+
65
+ <% if controls_present %>
66
+ <% controls_classes = ["table-card__controls"] %>
67
+ <% controls_classes << "table-card__section--top" unless header_present %>
68
+ <% controls_classes << "table-card__section--bottom" unless footer_present %>
69
+ <div class="<%= controls_classes.join(" ") %>">
70
+ <%= row_controls_content %>
71
+ </div>
72
+ <% end %>
73
+
74
+ <% body_classes = ["table-card__body"] %>
75
+ <% body_classes << "table-card__section--top" unless header_present || controls_present %>
76
+ <% body_classes << "table-card__section--bottom" unless footer_present %>
77
+ <div class="<%= body_classes.join(" ") %>">
78
+ <%= body %>
79
+ </div>
80
+
81
+ <% if footer_present %>
82
+ <div class="table-card__footer table-card__section--bottom">
83
+ <%= footer_content %>
84
+ </div>
85
+ <% end %>
86
+ </div>
@@ -0,0 +1,68 @@
1
+ <% require "uri" %>
2
+ <%
3
+ rows_param = rows_per_page_param.to_s.presence
4
+ rows_options = Array(rows_per_page_options).map(&:to_i).select(&:positive?).uniq
5
+ rows_selected = rows_per_page_selected.to_i
6
+ rows_selected = per_page.to_i if rows_selected <= 0
7
+ rows_enabled = rows_param.present? && rows_options.present?
8
+ pagination_enabled = total_pages.to_i > 1
9
+ return unless rows_enabled || pagination_enabled
10
+ rows_selected = rows_options.first if rows_enabled && !rows_options.include?(rows_selected)
11
+ base_path = begin
12
+ path_builder.call(current_page)
13
+ rescue
14
+ nil
15
+ end
16
+ pagination_page_param = base_path.present? ? Rack::Utils.parse_query(URI.parse(base_path).query.to_s).keys.find { |key| key.to_s.include?("page") } : nil
17
+ pagination_page_param ||= "page"
18
+ rows_form_action = base_path.present? ? URI.parse(base_path).path.presence || request.path : request.path
19
+ rows_form_hidden_params = request.query_parameters.except(rows_param, pagination_page_param)
20
+ showing_from = if total_count.to_i.positive? && per_page.to_i.positive?
21
+ ((current_page - 1) * per_page) + 1
22
+ else
23
+ nil
24
+ end
25
+ showing_to = if total_count.to_i.positive? && per_page.to_i.positive?
26
+ [ current_page * per_page, total_count ].min
27
+ else
28
+ nil
29
+ end
30
+ %>
31
+
32
+ <div class="flex flex-wrap items-start justify-between gap-3">
33
+ <div class="flex min-w-0 flex-col gap-2">
34
+ <% if rows_enabled %>
35
+ <%= form_with url: rows_form_action, method: :get, local: true, class: "flex flex-wrap items-center gap-2 text-xs text-muted", data: { controller: "form-submit" }, html: { aria: { label: "Items per page" } } do %>
36
+ <% rows_form_hidden_params.each do |key, value| %>
37
+ <% Array(value).each do |entry| %>
38
+ <%= hidden_field_tag key, entry %>
39
+ <% end %>
40
+ <% end %>
41
+ <%= hidden_field_tag pagination_page_param, 1 %>
42
+ <span>Show</span>
43
+ <%= ui_menu_select_tag rows_param,
44
+ options: rows_options.map { |option| { value: option.to_s, label: option.to_s } },
45
+ selected: rows_selected.to_s,
46
+ wrapper_class: "w-24",
47
+ input_data: { action: "change->form-submit#submit" } %>
48
+ <span>items per page</span>
49
+ <% end %>
50
+ <% end %>
51
+
52
+ <div class="text-xs text-muted">
53
+ <% if showing_from && showing_to %>
54
+ Showing <%= showing_from %>-<%= showing_to %> of <%= total_count %>
55
+ <% else %>
56
+ Page <%= current_page %> of <%= total_pages %>
57
+ <% end %>
58
+ </div>
59
+ </div>
60
+
61
+ <% if pagination_enabled %>
62
+ <%= ui_pagination(
63
+ current_page: current_page,
64
+ total_pages: total_pages,
65
+ path_builder: path_builder
66
+ ) %>
67
+ <% end %>
68
+ </div>
@@ -0,0 +1,33 @@
1
+ <div class="<%= wrapper_classes %>">
2
+ <% if label.present? %>
3
+ <label class="field__label" for="<%= input_options[:id] %>"><%= label %></label>
4
+ <% end %>
5
+ <div class="text-field__input">
6
+ <% if prefix.present? %>
7
+ <span class="text-field__prefix"><%= prefix %></span>
8
+ <% end %>
9
+ <% if multiline %>
10
+ <%= tag.textarea(input_value.to_s, **input_options.except(:type, :value)) %>
11
+ <% else %>
12
+ <%= tag.input(**input_options) %>
13
+ <% end %>
14
+ <% if suffix.present? %>
15
+ <span class="text-field__suffix"><%= suffix %></span>
16
+ <% end %>
17
+ </div>
18
+ <%
19
+ support_fragments = []
20
+ support_fragments << supporting_text if supporting_text.present?
21
+ support_fragments << supporting_slot if supporting_slot.present?
22
+ support_output = safe_join(support_fragments, " ")
23
+ support_hidden = support_output.blank?
24
+ %>
25
+ <p class="field__supporting"
26
+ id="<%= support_id %>"
27
+ data-field-support="true"
28
+ data-default-support="<%= supporting_text.to_s %>"
29
+ aria-live="polite"
30
+ <%= "hidden" if support_hidden %>>
31
+ <%= support_output %>
32
+ </p>
33
+ </div>
@@ -0,0 +1,73 @@
1
+ <% tooltip_id = "tooltip-#{SecureRandom.hex(4)}" %>
2
+ <% inline = !!local_assigns[:inline] %>
3
+ <% wrapper_tag = inline ? :span : :div %>
4
+ <% bubble_tag = inline ? :span : :div %>
5
+ <% wrapper_classes = ["relative inline-flex items-center group", ("align-middle shrink-0" if inline), wrapper_class].compact.join(" ") %>
6
+ <% icon_variant = variant.to_s == "icon" %>
7
+ <% trigger_classes = if icon_variant
8
+ [
9
+ "inline-flex items-center justify-center rounded-full p-1 text-muted hover:text-base-content",
10
+ ("align-middle leading-none shrink-0" if inline),
11
+ "focus:outline-none focus:ring-2 focus:ring-primary/30",
12
+ trigger_class
13
+ ].compact.join(" ")
14
+ else
15
+ [
16
+ "inline-flex items-center gap-1 text-sm text-on-surface/70 cursor-pointer",
17
+ "border-b border-dashed border-current pb-0.5",
18
+ "transition-colors hover:text-on-surface",
19
+ trigger_class
20
+ ].compact.join(" ")
21
+ end %>
22
+ <% bubble_classes = [
23
+ "absolute left-0 top-full z-30 mt-2 hidden w-max max-w-[90vw] sm:max-w-[32rem] rounded-lg",
24
+ "border border-[color:var(--color-outline)]",
25
+ "bg-[color:var(--color-inverse-surface)]",
26
+ "text-[color:var(--color-inverse-on-surface)]",
27
+ "text-left whitespace-normal break-words",
28
+ "dark:bg-[color:var(--color-surface-lowest)]",
29
+ "dark:text-[color:var(--color-on-surface)]",
30
+ "p-3 text-sm font-normal",
31
+ "shadow-[var(--elev-1)]",
32
+ (inline ? "data-[state=open]:inline-block" : "data-[state=open]:block"),
33
+ bubble_class
34
+ ].compact.join(" ") %>
35
+
36
+ <%= content_tag wrapper_tag,
37
+ class: wrapper_classes,
38
+ data: {
39
+ controller: "tooltip",
40
+ action: "mouseenter->tooltip#open mouseleave->tooltip#close focusin->tooltip#open focusout->tooltip#close"
41
+ } do %>
42
+ <button
43
+ type="button"
44
+ class="<%= trigger_classes %>"
45
+ data-tooltip-target="trigger"
46
+ data-action="click->tooltip#toggle"
47
+ aria-describedby="<%= tooltip_id %>"
48
+ aria-expanded="false"
49
+ >
50
+ <% if icon_variant %>
51
+ <span class="sr-only"><%= text %></span>
52
+ <%= ui_icon(icon, class_name: "h-4 w-4") %>
53
+ <% else %>
54
+ <span><%= text %></span>
55
+ <% end %>
56
+ <% if show_icon && !icon_variant %>
57
+ <%= ui_icon(icon, class_name: "h-4 w-4") %>
58
+ <% end %>
59
+ </button>
60
+ <%= content_tag bubble_tag,
61
+ content,
62
+ id: tooltip_id,
63
+ role: "tooltip",
64
+ class: bubble_classes,
65
+ style: "text-align: left;",
66
+ data: {
67
+ tooltip_target: "bubble",
68
+ state: "closed"
69
+ },
70
+ aria: {
71
+ hidden: "true"
72
+ } %>
73
+ <% end %>
@@ -0,0 +1,20 @@
1
+ <%
2
+ payload = local_assigns.fetch(:banner)
3
+ section_attributes = {
4
+ class: ["marketing-cta-banner px-12 py-16 sm:px-12", local_assigns[:classes]].compact.join(" ")
5
+ }
6
+ section_attributes[:id] = payload[:id] if payload[:id].present?
7
+ %>
8
+ <section <%= tag.attributes(section_attributes) %>>
9
+ <div class="mx-auto flex max-w-6xl flex-col gap-6 md:flex-row md:items-center md:justify-between">
10
+ <div class="max-w-3xl">
11
+ <h2 class="text-neutral-50"><%= payload[:title] %></h2>
12
+ <p class="mt-4 text-neutral-50"><%= payload[:body] %></p>
13
+ </div>
14
+ <div class="flex flex-wrap gap-3">
15
+ <%= link_to payload[:cta_label],
16
+ payload[:cta_href],
17
+ class: "inline-flex h-11 items-center justify-center rounded-[1rem] bg-white px-5 text-base font-semibold text-[#1f1f27] shadow-[var(--elev-1)] transition-transform duration-150 hover:-translate-y-0.5" %>
18
+ </div>
19
+ </div>
20
+ </section>
@@ -0,0 +1,37 @@
1
+ <section id="<%= id %>" class="<%= ["marketing-faq-section bg-surface px-6 py-16 sm:px-12", local_assigns[:classes]].compact.join(" ") %>">
2
+ <div class="<%= ["mx-auto", (local_assigns[:container_class].presence || "max-w-4xl")].join(" ") %>">
3
+ <h2><%= title %></h2>
4
+ <% if description.present? %>
5
+ <p class="mt-3 text-soft"><%= description %></p>
6
+ <% end %>
7
+ <div class="mt-8 rounded-3xl border border-outline bg-surface-high px-6" data-controller="accordion">
8
+ <% Array(items).each_with_index do |item, index| %>
9
+ <% button_action = ui_marketing_action_string("click->accordion#toggle", item[:button_action], item[:button_actions]) %>
10
+ <div data-accordion-target="item" data-open="<%= item.key?(:open) ? !!item[:open] : index.zero? %>" class="<%= index.zero? ? "" : "border-t border-outline" %>">
11
+ <button
12
+ type="button"
13
+ class="flex w-full items-center gap-2 py-6 text-left text-lg font-semibold text-on-surface"
14
+ data-action="<%= button_action %>"
15
+ <% (item[:button_data] || {}).each do |key, value| %>
16
+ data-<%= key.to_s.dasherize %>="<%= value %>"
17
+ <% end %>
18
+ aria-expanded="<%= (item.key?(:open) ? !!item[:open] : index.zero?) ? "true" : "false" %>"
19
+ aria-controls="<%= item[:id].presence || "#{id}-item-#{index}" %>"
20
+ >
21
+ <span aria-hidden="true" data-accordion-target="icon"><%= ui_icon("chevron-right", class_name: "h-[18px] w-[18px]") %></span>
22
+ <span><%= item[:question] %></span>
23
+ </button>
24
+ <div
25
+ id="<%= item[:id].presence || "#{id}-item-#{index}" %>"
26
+ data-accordion-target="content"
27
+ aria-hidden="<%= (item.key?(:open) ? !!item[:open] : index.zero?) ? "false" : "true" %>"
28
+ >
29
+ <div class="pb-6 pl-7 pr-1">
30
+ <p class="text-soft"><%= item[:answer] %></p>
31
+ </div>
32
+ </div>
33
+ </div>
34
+ <% end %>
35
+ </div>
36
+ </div>
37
+ </section>
@@ -0,0 +1,67 @@
1
+ <%
2
+ resolved_tabs = Array(tabs)
3
+ selected_tab = resolved_tabs.find { |tab| tab[:selected] } || resolved_tabs.first
4
+ selected_value = selected_tab&.dig(:value)
5
+ tab_segments = resolved_tabs.map do |tab|
6
+ {
7
+ label: tab[:label],
8
+ value: tab[:value],
9
+ selected: tab[:value] == selected_value,
10
+ data: { action: "marketing-tabs#select", marketing_tabs_target: "option" }
11
+ }
12
+ end
13
+ %>
14
+ <section id="<%= id %>" class="<%= ["marketing-features-section bg-surface px-12 py-16 sm:px-12", local_assigns[:classes]].compact.join(" ") %>">
15
+ <div class="mx-auto max-w-6xl rounded-[40px] bg-accent/10 px-12 py-14 sm:px-12">
16
+ <div class="space-y-8" data-controller="marketing-tabs" data-marketing-tabs-tab-value="<%= selected_value %>">
17
+ <div class="text-center">
18
+ <h2><%= title %></h2>
19
+ <% if description.present? %>
20
+ <p class="mt-3 text-sm text-soft"><%= description %></p>
21
+ <% end %>
22
+ </div>
23
+ <div class="flex justify-center">
24
+ <%= ui_segmented_buttons(aria_label: "Use case tabs", items: tab_segments) %>
25
+ </div>
26
+
27
+ <div class="overflow-hidden transition-[height] duration-300 ease-out" data-marketing-tabs-target="panels">
28
+ <% resolved_tabs.each do |tab| %>
29
+ <% active = tab[:value] == selected_value %>
30
+ <% panel_classes = [("motion-slide-up" if active), ("hidden" unless active)].compact.join(" ") %>
31
+ <div data-marketing-tabs-target="panel"
32
+ data-tab="<%= tab[:value] %>"
33
+ role="tabpanel"
34
+ class="<%= panel_classes %>"
35
+ <%= 'hidden aria-hidden="true"' unless active %>>
36
+ <div class="grid items-start gap-8 md:grid-cols-[0.9fr_1.1fr]">
37
+ <div class="rounded-3xl border border-outline-variant bg-surface p-6">
38
+ <h3 class="text-2xl font-semibold text-on-surface"><%= tab[:panel_title].presence || tab[:label] %></h3>
39
+ <p class="mt-3 text-sm text-soft"><%= tab[:panel_body] %></p>
40
+ <% if tab[:visual].present? %>
41
+ <div class="mt-6"><%= ui_marketing_render_content(tab[:visual]) %></div>
42
+ <% else %>
43
+ <div class="mt-6 h-40 rounded-2xl border border-outline bg-surface-high"></div>
44
+ <% end %>
45
+ </div>
46
+ <div class="grid gap-5">
47
+ <p class="text-sm font-semibold uppercase tracking-wide text-muted"><%= question_label %></p>
48
+ <% Array(tab[:cards]).each do |card| %>
49
+ <article class="rounded-3xl border border-outline bg-surface-high p-6">
50
+ <h3 class="text-lg font-semibold"><%= card[:title] %></h3>
51
+ <p class="mt-3 text-sm text-soft"><%= card[:body] %></p>
52
+ </article>
53
+ <% end %>
54
+ </div>
55
+ </div>
56
+ </div>
57
+ <% end %>
58
+ </div>
59
+
60
+ <% if cta.present? %>
61
+ <div class="flex justify-center">
62
+ <%= ui_button(**cta) %>
63
+ </div>
64
+ <% end %>
65
+ </div>
66
+ </div>
67
+ </section>
@@ -0,0 +1,38 @@
1
+ <%
2
+ resolved_brand = local_assigns.fetch(:brand).symbolize_keys
3
+ resolved_brand[:logo_class] = resolved_brand[:logo_class].presence || "h-10 w-10 rounded-lg"
4
+ resolved_brand[:wordmark_class] = resolved_brand[:wordmark_class].presence || "text-2xl font-bold text-on-surface"
5
+ resolved_copyright = local_assigns[:copyright].presence || "© #{Time.current.year} #{resolved_brand[:name]}. All rights reserved."
6
+ %>
7
+ <footer class="<%= ["marketing-footer border-t border-outline-variant bg-surface px-12 py-12 text-sm text-soft", local_assigns[:classes]].compact.join(" ") %>">
8
+ <div class="mx-auto flex max-w-6xl flex-col gap-8 md:flex-row md:items-start md:justify-between">
9
+ <div>
10
+ <span class="brand-lockup">
11
+ <%= image_tag resolved_brand[:logo_src], alt: resolved_brand[:logo_alt], class: ["brand-lockup__logo", resolved_brand[:logo_class]].join(" ") %>
12
+ <span class="<%= ["brand-lockup__wordmark", resolved_brand[:wordmark_class]].join(" ") %>"><%= resolved_brand[:wordmark] %></span>
13
+ </span>
14
+ <p class="mt-2 max-w-md text-soft"><%= description %></p>
15
+ </div>
16
+ <div class="grid grid-cols-2 gap-8 sm:grid-cols-3">
17
+ <% Array(columns).each do |column| %>
18
+ <div>
19
+ <h4 class="font-semibold text-on-surface"><%= column[:title] %></h4>
20
+ <ul class="mt-3 space-y-2 text-soft">
21
+ <% Array(column[:links]).each do |link| %>
22
+ <li>
23
+ <% if link[:href].present? %>
24
+ <%= link_to link[:label], link[:href], class: link[:class].presence || "hover:text-primary" %>
25
+ <% else %>
26
+ <span><%= link[:label] %></span>
27
+ <% end %>
28
+ </li>
29
+ <% end %>
30
+ </ul>
31
+ </div>
32
+ <% end %>
33
+ </div>
34
+ </div>
35
+ <div class="mx-auto mt-8 max-w-6xl border-t border-outline-variant pt-6 text-xs text-soft">
36
+ <p><%= resolved_copyright %></p>
37
+ </div>
38
+ </footer>