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,43 @@
1
+ <div class="<%= wrapper_classes %>" data-controller="date-field" data-date-field="true">
2
+ <% if label.present? %>
3
+ <label class="field__label" for="<%= display_input_options[:id] %>"><%= label %></label>
4
+ <% end %>
5
+ <div class="date-field__input">
6
+ <%= tag.input(**display_input_options) %>
7
+ <%= tag.input(**native_input_options) %>
8
+ <button type="button"
9
+ class="date-field__toggle"
10
+ data-date-field-toggle="true"
11
+ data-date-field-target="toggle"
12
+ aria-label="<%= toggle_label %>"
13
+ title="<%= toggle_label %>"
14
+ <%= "disabled" if native_input_options[:disabled] %>>
15
+ <%= ui_icon(icon_name, class_name: "h-[18px] w-[18px]") %>
16
+ </button>
17
+ </div>
18
+ <%
19
+ support_fragments = []
20
+ support_fragments << supporting_text if supporting_text.present?
21
+ support_output = safe_join(support_fragments, " ")
22
+ support_hidden = support_output.blank?
23
+ %>
24
+ <% default_support = supporting_text.to_s %>
25
+ <% if supporting_text.present? %>
26
+ <p class="field__supporting"
27
+ id="<%= support_id %>"
28
+ data-field-support="true"
29
+ data-date-field-support="true"
30
+ data-default-support="<%= default_support %>"
31
+ aria-live="polite">
32
+ <%= support_output %>
33
+ </p>
34
+ <% else %>
35
+ <p class="field__supporting"
36
+ id="<%= support_id %>"
37
+ data-field-support="true"
38
+ data-date-field-support="true"
39
+ data-default-support="<%= default_support %>"
40
+ aria-live="polite"
41
+ hidden></p>
42
+ <% end %>
43
+ </div>
@@ -0,0 +1 @@
1
+ <%= render partial: "baldur/optional/google_sign_in_button", locals: local_assigns %>
@@ -0,0 +1,36 @@
1
+ <%
2
+ items = Array(local_assigns[:items])
3
+ menu_class = local_assigns[:menu_class].to_s.strip
4
+ summary_aria_label = local_assigns[:summary_aria_label].presence || "More actions"
5
+ empty_text = local_assigns[:empty_text].presence || "No quick actions yet"
6
+ menu_classes = [ "menu", menu_class.presence ].compact.join(" ")
7
+ %>
8
+
9
+ <details class="menu-wrapper inline-block" data-controller="details-menu" data-action="click@window->details-menu#closeIfClickOutside keydown@window->details-menu#closeIfEscape click->details-menu#closeIfLink">
10
+ <summary class="icon-button" aria-label="<%= summary_aria_label %>">
11
+ <%= ui_icon("ellipsis", class_name: "h-5 w-5") %>
12
+ </summary>
13
+ <div class="<%= menu_classes %>">
14
+ <% if items.present? %>
15
+ <% items.each do |item| %>
16
+ <%
17
+ label = item[:label] || item["label"]
18
+ href = item[:href] || item["href"] || "#"
19
+ open_modal = item[:open_modal] || item["open_modal"]
20
+ action = item[:action] || item["action"]
21
+ item_data = (item[:data] || item["data"] || {}).to_h
22
+ label_class = item[:label_class] || item["label_class"]
23
+ data_attributes = {}
24
+ data_attributes[:open_modal] = open_modal if open_modal.present?
25
+ data_attributes[:action] = action if action.present?
26
+ item_data.each { |key, value| data_attributes[key] = value }
27
+ %>
28
+ <%= link_to href, class: "menu__option", data: data_attributes do %>
29
+ <span class="<%= ["menu__option-label", label_class].compact.join(" ") %>"><%= label %></span>
30
+ <% end %>
31
+ <% end %>
32
+ <% else %>
33
+ <span class="px-3 py-2 text-xs text-muted block"><%= empty_text %></span>
34
+ <% end %>
35
+ </div>
36
+ </details>
@@ -0,0 +1,45 @@
1
+ <%
2
+ variant_classes = {
3
+ default: "card",
4
+ accent: "card card--primary"
5
+ }
6
+ card_classes = variant_classes[variant || :default]
7
+ trend_classes = if trend.to_s.start_with?("-")
8
+ "text-[color:var(--color-error)]"
9
+ else
10
+ "text-[color:var(--color-tertiary)]"
11
+ end
12
+ caption_icon_tone = caption_icon&.fetch(:tone, :info)&.to_sym
13
+ caption_icon_classes = {
14
+ warn: "text-[color:var(--color-error)]",
15
+ success: "text-[color:var(--color-secondary)]",
16
+ info: "text-[color:var(--color-outline)]"
17
+ }
18
+ caption_icon_class = caption_icon_classes[caption_icon_tone] || caption_icon_classes[:info]
19
+ caption_icon_name = caption_icon&.fetch(:name, nil)
20
+ action_options = action.present? ? { variant: :text }.merge(action) : nil
21
+ %>
22
+ <article class="<%= card_classes %>">
23
+ <p class="text-sm font-semibold text-[color:var(--color-on-surface-variant)]"><%= label %></p>
24
+ <% if value.present? %>
25
+ <div class="mt-3 flex items-baseline gap-2">
26
+ <p class="text-3xl font-semibold text-[color:var(--color-on-surface)]"><%= value %></p>
27
+ <% if trend.present? %>
28
+ <span class="text-xs font-semibold <%= trend_classes %>"><%= trend %></span>
29
+ <% end %>
30
+ </div>
31
+ <% end %>
32
+ <% if caption.present? %>
33
+ <p class="mt-2 flex items-center gap-1 text-xs text-[color:color-mix(in srgb,var(--color-on-surface) 70%,transparent)]">
34
+ <% if caption_icon_name.present? %>
35
+ <%= ui_icon(caption_icon_name, class_name: "h-4 w-4 #{caption_icon_class}") %>
36
+ <% end %>
37
+ <span><%= caption %></span>
38
+ </p>
39
+ <% end %>
40
+ <% if action_options.present? %>
41
+ <div class="mt-3">
42
+ <%= ui_button(**action_options) %>
43
+ </div>
44
+ <% end %>
45
+ </article>
@@ -0,0 +1,78 @@
1
+ <%= content_tag :div, class: wrapper_classes, data: wrapper_data do %>
2
+ <% if label.present? %>
3
+ <label class="field__label" for="<%= field_id %>"><%= label %></label>
4
+ <% end %>
5
+
6
+ <div class="menu-select"
7
+ data-controller="menu-select"
8
+ data-menu-select="true"
9
+ data-menu-placement="bottom"
10
+ <%= %(data-menu-select-disabled="true") if disabled %>>
11
+ <% hidden_input_data = input_data.present? ? input_data.dup : {} %>
12
+ <% hidden_input_data[:"menu-select-input"] = true %>
13
+ <% hidden_input_data[:menu_select_target] ||= "input" %>
14
+ <%= hidden_field_tag name, selected_value, data: hidden_input_data %>
15
+
16
+ <button type="button"
17
+ class="menu-select__trigger<%= disabled ? " is-disabled" : "" %>"
18
+ id="<%= field_id %>"
19
+ aria-haspopup="menu"
20
+ aria-expanded="false"
21
+ aria-disabled="<%= disabled ? "true" : "false" %>"
22
+ <%= "disabled" if disabled %>
23
+ data-menu-select-trigger
24
+ data-menu-select-target="trigger">
25
+ <span class="menu-select__label"
26
+ data-menu-select-label
27
+ data-menu-select-target="label"><%= button_label %></span>
28
+ <%= ui_icon("chevron-down", class_name: "menu-select__icon") %>
29
+ </button>
30
+
31
+ <div class="menu menu--select"
32
+ role="menu"
33
+ aria-labelledby="<%= field_id %>"
34
+ data-menu-select-list
35
+ data-menu-select-target="list">
36
+ <% options.each do |option| %>
37
+ <% option_classes = ["menu__option"]
38
+ option_classes << "is-selected" if option[:value] == selected_value
39
+ option_classes << "is-disabled" if option[:disabled]
40
+ %>
41
+ <% is_selected = option[:value] == selected_value %>
42
+ <button type="button"
43
+ class="<%= option_classes.join(' ') %>"
44
+ role="menuitemradio"
45
+ aria-checked="<%= is_selected ? 'true' : 'false' %>"
46
+ aria-disabled="<%= option[:disabled] ? 'true' : 'false' %>"
47
+ data-menu-select-option="true"
48
+ data-value="<%= option[:value] %>"
49
+ data-label="<%= option[:label] %>"
50
+ data-disabled="<%= option[:disabled] ? 'true' : 'false' %>">
51
+ <span class="menu__option-content">
52
+ <span class="menu__option-head">
53
+ <span class="menu__option-label" data-menu-select-option-label><%= option[:label] %></span>
54
+ <% if option[:badge].present? %>
55
+ <span class="menu__option-badge">
56
+ <%= ui_badge(
57
+ text: option[:badge],
58
+ variant: option[:badge_variant] || :disabled,
59
+ size: option[:badge_size] || :xs
60
+ ) %>
61
+ </span>
62
+ <% end %>
63
+ </span>
64
+ <% support_text = option[:support].presence || option[:description] %>
65
+ <% if support_text.present? %>
66
+ <span class="menu__option-support"><%= support_text %></span>
67
+ <% end %>
68
+ </span>
69
+ <%= ui_icon("check", class_name: "menu__option-icon") %>
70
+ </button>
71
+ <% end %>
72
+ </div>
73
+ </div>
74
+
75
+ <% if supporting_text.present? %>
76
+ <p class="field__supporting"><%= supporting_text %></p>
77
+ <% end %>
78
+ <% end %>
@@ -0,0 +1,54 @@
1
+ <% title = local_assigns.fetch(:title, "") unless defined?(title) %>
2
+ <% description = local_assigns.fetch(:description, nil) unless defined?(description) %>
3
+ <% close_label = local_assigns.fetch(:close_label, "Cancel") unless defined?(close_label) %>
4
+ <% submit_label = local_assigns.fetch(:submit_label, "Continue") unless defined?(submit_label) %>
5
+ <% body = local_assigns.fetch(:body, nil) %>
6
+ <% tertiary_button_options = local_assigns[:tertiary_button_options] unless defined?(tertiary_button_options) %>
7
+ <% show_footer = local_assigns.key?(:show_footer) ? local_assigns[:show_footer] : true %>
8
+ <% dialog_class = local_assigns[:dialog_class] %>
9
+ <% dialog_classes = ["dialog", "motion-fade-scale", *dialog_class.to_s.split].uniq.join(" ") %>
10
+ <div id="<%= id %>" class="<%= dialog_classes %>">
11
+ <div class="flex items-start justify-between gap-4">
12
+ <div>
13
+ <h3 class="text-xl font-semibold text-[color:var(--color-on-surface)]"><%= title %></h3>
14
+ <% if description.present? %>
15
+ <p class="text-sm text-[color:color-mix(in srgb,var(--color-on-surface) 75%,transparent)]"><%= description %></p>
16
+ <% end %>
17
+ </div>
18
+ <% if local_assigns[:close_button] != false %>
19
+ <button type="button" class="icon-button" aria-label="Close modal" data-modal-close="true">
20
+ <%= ui_icon("x", class_name: "h-5 w-5") %>
21
+ </button>
22
+ <% end %>
23
+ </div>
24
+ <% if body.present? %>
25
+ <div class="mt-4 space-y-3 text-sm text-[color:var(--color-on-surface)]">
26
+ <%= body %>
27
+ </div>
28
+ <% end %>
29
+ <% if show_footer %>
30
+ <% close_button_options ||= {} %>
31
+ <% submit_button_options ||= {} %>
32
+ <% tertiary_button_options ||= nil %>
33
+ <% close_render_options = { label: close_label, variant: :ghost }.merge(close_button_options.except(:data) || {}) %>
34
+ <% close_render_options[:data] = { modal_close: true }.merge(close_button_options[:data] || {}) %>
35
+ <% submit_render_options = { label: submit_label, variant: :primary, type: :submit }.merge(submit_button_options.except(:data) || {}) %>
36
+ <% submit_render_options[:data] = submit_button_options[:data] if submit_button_options[:data].present? %>
37
+ <% tertiary_render_options = nil %>
38
+ <% if tertiary_button_options.present? %>
39
+ <% tertiary_render_options = { variant: :text }.merge(tertiary_button_options.except(:data) || {}) %>
40
+ <% tertiary_render_options[:data] = tertiary_button_options[:data] if tertiary_button_options[:data].present? %>
41
+ <% end %>
42
+ <div class="mt-6 flex flex-wrap items-center gap-3">
43
+ <% if tertiary_render_options.present? %>
44
+ <div class="flex flex-wrap gap-3">
45
+ <%= render "baldur/components/button", **tertiary_render_options %>
46
+ </div>
47
+ <% end %>
48
+ <div class="ml-auto flex flex-wrap gap-3 justify-end">
49
+ <%= render "baldur/components/button", **close_render_options %>
50
+ <%= render "baldur/components/button", **submit_render_options %>
51
+ </div>
52
+ </div>
53
+ <% end %>
54
+ </div>
@@ -0,0 +1,61 @@
1
+ <% return if total_pages <= 1 %>
2
+ <%
3
+ first_page = current_page > 1 ? 1 : nil
4
+ previous_page = current_page > 1 ? current_page - 1 : nil
5
+ next_page = current_page < total_pages ? current_page + 1 : nil
6
+ last_page = current_page < total_pages ? total_pages : nil
7
+ %>
8
+
9
+ <nav class="pagination" aria-label="Pagination">
10
+ <div class="pagination__list">
11
+ <% if first_page %>
12
+ <%= link_to path_builder.call(first_page), class: "pagination__item", aria: { label: "Go to first page" } do %>
13
+ <%= ui_icon("chevrons-left", class_name: "pagination__icon") %>
14
+ <% end %>
15
+ <% else %>
16
+ <span class="pagination__item is-disabled" aria-disabled="true">
17
+ <%= ui_icon("chevrons-left", class_name: "pagination__icon") %>
18
+ </span>
19
+ <% end %>
20
+
21
+ <% if previous_page %>
22
+ <%= link_to path_builder.call(previous_page), class: "pagination__item", aria: { label: "Go to previous page" } do %>
23
+ <%= ui_icon("chevron-left", class_name: "pagination__icon") %>
24
+ <% end %>
25
+ <% else %>
26
+ <span class="pagination__item is-disabled" aria-disabled="true">
27
+ <%= ui_icon("chevron-left", class_name: "pagination__icon") %>
28
+ </span>
29
+ <% end %>
30
+
31
+ <% pages.each do |page| %>
32
+ <% if page == :gap %>
33
+ <span class="pagination__gap" aria-hidden="true">...</span>
34
+ <% elsif page == current_page %>
35
+ <span class="pagination__item is-current" aria-current="page"><%= page %></span>
36
+ <% else %>
37
+ <%= link_to page, path_builder.call(page), class: "pagination__item", aria: { label: "Go to page #{page}" } %>
38
+ <% end %>
39
+ <% end %>
40
+
41
+ <% if next_page %>
42
+ <%= link_to path_builder.call(next_page), class: "pagination__item", aria: { label: "Go to next page" } do %>
43
+ <%= ui_icon("chevron-right", class_name: "pagination__icon") %>
44
+ <% end %>
45
+ <% else %>
46
+ <span class="pagination__item is-disabled" aria-disabled="true">
47
+ <%= ui_icon("chevron-right", class_name: "pagination__icon") %>
48
+ </span>
49
+ <% end %>
50
+
51
+ <% if last_page %>
52
+ <%= link_to path_builder.call(last_page), class: "pagination__item", aria: { label: "Go to last page" } do %>
53
+ <%= ui_icon("chevrons-right", class_name: "pagination__icon") %>
54
+ <% end %>
55
+ <% else %>
56
+ <span class="pagination__item is-disabled" aria-disabled="true">
57
+ <%= ui_icon("chevrons-right", class_name: "pagination__icon") %>
58
+ </span>
59
+ <% end %>
60
+ </div>
61
+ </nav>
@@ -0,0 +1,51 @@
1
+ <%
2
+ segments = Array(items)
3
+ return if segments.empty?
4
+ %>
5
+ <div class="segmented <%= classes %>" role="tablist" aria-label="<%= aria_label %>">
6
+ <% segments.each do |segment| %>
7
+ <% label = segment[:label].to_s %>
8
+ <% badge_label = segment[:badge_label].to_s.presence %>
9
+ <% icon = segment[:icon].presence %>
10
+ <% selected = !!segment[:selected] %>
11
+ <% disabled = !!segment[:disabled] %>
12
+ <% value = segment[:value].presence || label.parameterize %>
13
+ <% icon_only = icon.present? && label.blank? %>
14
+ <% data_attributes = (segment[:data] || {}).merge(value: value) %>
15
+ <% button_classes = [
16
+ "segmented__button",
17
+ ("is-selected" if selected),
18
+ ("is-disabled" if disabled),
19
+ ("is-icon-only" if icon_only),
20
+ (segment[:class] if segment[:class].present?)
21
+ ].compact.join(" ") %>
22
+ <% button_options = {
23
+ type: "button",
24
+ class: button_classes,
25
+ role: "tab",
26
+ aria: {
27
+ selected: selected,
28
+ disabled: disabled
29
+ },
30
+ tabindex: selected ? 0 : -1,
31
+ data: data_attributes
32
+ } %>
33
+ <%= tag.button(**button_options) do %>
34
+ <% if icon.present? %>
35
+ <%= ui_icon(icon, class_name: "segmented__icon") %>
36
+ <% end %>
37
+ <% if label.present? || badge_label.present? %>
38
+ <span class="segmented__content">
39
+ <% if label.present? %>
40
+ <span class="segmented__label"><%= label %></span>
41
+ <% end %>
42
+ <% if badge_label.present? %>
43
+ <span class="segmented__badge"><%= badge_label %></span>
44
+ <% end %>
45
+ </span>
46
+ <% else %>
47
+ <span class="sr-only"><%= segment[:sr_label].presence || value.to_s.humanize %></span>
48
+ <% end %>
49
+ <% end %>
50
+ <% end %>
51
+ </div>
@@ -0,0 +1,41 @@
1
+ <%
2
+ items = Array(local_assigns[:items]).compact
3
+ nav_class = [ "settings-nav", local_assigns[:class] ].compact.join(" ")
4
+ %>
5
+
6
+ <nav class="<%= nav_class %>"
7
+ aria-label="<%= local_assigns[:aria_label].presence || "Settings navigation" %>"
8
+ data-controller="accordion"
9
+ data-accordion-allow-multiple-value="true">
10
+ <% items.each do |item| %>
11
+ <% children = Array(item[:children]).compact %>
12
+ <% if children.any? %>
13
+ <% accordion_id = "settings-nav-#{SecureRandom.hex(4)}" %>
14
+ <div class="settings-nav__group settings-nav__disclosure" data-accordion-target="item" data-open="<%= item[:expanded] %>">
15
+ <button type="button"
16
+ class="settings-nav__summary"
17
+ data-action="click->accordion#toggle"
18
+ aria-expanded="<%= item[:expanded] ? "true" : "false" %>"
19
+ aria-controls="<%= accordion_id %>">
20
+ <span class="settings-nav__summary-label"><%= item[:label] %></span>
21
+ <span aria-hidden="true" data-accordion-target="icon"><%= ui_icon("chevron-right", class_name: "settings-nav__summary-icon") %></span>
22
+ </button>
23
+ <div id="<%= accordion_id %>" class="settings-nav__children-shell" data-accordion-target="content" aria-hidden="<%= item[:expanded] ? "false" : "true" %>">
24
+ <div class="settings-nav__children">
25
+ <% children.each do |child| %>
26
+ <%= link_to child[:label],
27
+ child[:href],
28
+ class: "settings-nav__link",
29
+ aria: { current: child[:active] ? "page" : nil } %>
30
+ <% end %>
31
+ </div>
32
+ </div>
33
+ </div>
34
+ <% else %>
35
+ <%= link_to item[:label],
36
+ item[:href],
37
+ class: "settings-nav__item settings-nav__link",
38
+ aria: { current: item[:active] ? "page" : nil } %>
39
+ <% end %>
40
+ <% end %>
41
+ </nav>
@@ -0,0 +1,42 @@
1
+ <%
2
+ template = !!local_assigns[:template]
3
+ variant = (local_assigns[:variant] || :notice).to_sym
4
+ icon_map = {
5
+ notice: "info",
6
+ success: "check-circle",
7
+ warning: "triangle-alert",
8
+ error: "circle-alert"
9
+ }
10
+ title = local_assigns[:title].presence || case variant
11
+ when :success then "Success"
12
+ when :notice then "Notice"
13
+ when :warning then "Warning"
14
+ when :error then "Error"
15
+ else "Notice"
16
+ end
17
+ icon_content = local_assigns[:icon].presence || icon_map[variant] || "info"
18
+ dismiss_label = local_assigns[:dismiss_label] || "Dismiss"
19
+ %>
20
+ <div class="snackbar snackbar--<%= variant %>" data-controller="snackbar" data-snackbar data-snackbar-variant="<%= variant %>">
21
+ <div class="snackbar__icon" data-snackbar-icon>
22
+ <%= ui_icon(icon_content, class_name: "h-5 w-5") unless template %>
23
+ </div>
24
+ <div class="snackbar__body">
25
+ <p class="snackbar__title" data-snackbar-label><%= title unless template %></p>
26
+ <p class="snackbar__message" data-snackbar-message><%= message unless template %></p>
27
+ </div>
28
+ <button type="button"
29
+ class="snackbar__dismiss"
30
+ data-action="snackbar#handleDismiss"
31
+ data-snackbar-dismiss
32
+ aria-label="<%= dismiss_label %>"
33
+ data-snackbar-dismiss-label="<%= dismiss_label %>">
34
+ <span class="snackbar__dismiss-ring" aria-hidden="true">
35
+ <svg viewBox="0 0 20 20" focusable="false">
36
+ <circle class="snackbar__dismiss-track" cx="10" cy="10" r="8" pathLength="100" />
37
+ <circle class="snackbar__dismiss-indicator" cx="10" cy="10" r="8" pathLength="100" data-snackbar-target="progressIndicator" />
38
+ </svg>
39
+ </span>
40
+ <span class="snackbar__dismiss-icon" aria-hidden="true"><%= ui_icon("x", class_name: "h-4 w-4") %></span>
41
+ </button>
42
+ </div>
@@ -0,0 +1,13 @@
1
+ <div class="snackbar-stack"
2
+ data-baldur-snackbar-stack
3
+ role="status"
4
+ aria-live="polite"
5
+ aria-atomic="true">
6
+ <% Array(snackbars).each do |snackbar| %>
7
+ <%= render "baldur/components/snackbar", **snackbar %>
8
+ <% end %>
9
+
10
+ <template data-baldur-snackbar-template>
11
+ <%= render "baldur/components/snackbar", template: true %>
12
+ </template>
13
+ </div>
@@ -0,0 +1,39 @@
1
+ <ul class="stepper" role="list">
2
+ <% steps.each_with_index do |step, index| %>
3
+ <% status = step[:status] %>
4
+ <% status_class =
5
+ case status
6
+ when :completed then "stepper__item--completed"
7
+ when :current then "stepper__item--current"
8
+ else "stepper__item--pending"
9
+ end %>
10
+ <% left_classes = [ "stepper__line" ] %>
11
+ <% if index.zero? %>
12
+ <% left_classes << "stepper__line--hidden" %>
13
+ <% elsif steps[index - 1][:status] == :completed %>
14
+ <% left_classes << "stepper__line--active" %>
15
+ <% end %>
16
+ <% right_classes = [ "stepper__line" ] %>
17
+ <% if index == steps.length - 1 %>
18
+ <% right_classes << "stepper__line--hidden" %>
19
+ <% elsif status == :completed %>
20
+ <% right_classes << "stepper__line--active" %>
21
+ <% end %>
22
+ <li
23
+ class="stepper__item <%= status_class %>"
24
+ role="listitem"
25
+ <%= 'aria-current="step"' if status == :current %>>
26
+ <div class="stepper__indicator-row">
27
+ <div class="<%= left_classes.join(" ") %>" aria-hidden="true"></div>
28
+ <div class="stepper__indicator" aria-hidden="true">
29
+ <span><%= status == :completed ? "✓" : index + 1 %></span>
30
+ </div>
31
+ <div class="<%= right_classes.join(" ") %>" aria-hidden="true"></div>
32
+ </div>
33
+ <p class="stepper__label"><%= step[:title] %></p>
34
+ <% if step[:description].present? %>
35
+ <p class="stepper__description"><%= step[:description] %></p>
36
+ <% end %>
37
+ </li>
38
+ <% end %>
39
+ </ul>
@@ -0,0 +1,117 @@
1
+ <%
2
+ normalized_columns = Array(columns).map do |column|
3
+ if column.is_a?(Hash)
4
+ column.symbolize_keys
5
+ else
6
+ key = column.is_a?(Symbol) ? column : column.to_s.parameterize.underscore.to_sym
7
+ { label: column, key: key }
8
+ end
9
+ end
10
+ row_classes = local_assigns[:row_class] || nil
11
+ row_attributes_config = local_assigns[:row_attributes]
12
+ sort_config = local_assigns[:sort]
13
+ sort_config = sort_config.respond_to?(:symbolize_keys) ? sort_config.symbolize_keys : {}
14
+ sort_key = sort_config[:key].to_s
15
+ sort_direction = sort_config[:direction].to_s.downcase == "asc" ? "asc" : "desc"
16
+ sort_path_builder = local_assigns[:sort_path_builder]
17
+ header_classes = "px-6 py-4 text-left text-xs font-semibold uppercase tracking-wide text-[color:var(--color-on-surface-variant)]"
18
+ cell_classes = "px-6 py-4 text-sm text-[color:var(--color-on-surface)]"
19
+ table_classes = ["table", local_assigns[:table_class], local_assigns[:class]].compact.join(" ")
20
+ %>
21
+ <div class="table-card__scroll">
22
+ <table class="<%= table_classes %>">
23
+ <thead>
24
+ <tr>
25
+ <% normalized_columns.each do |column| %>
26
+ <% column_header_class = [header_classes, column[:header_class]].compact.join(" ") %>
27
+ <% column_sort_key = (column[:sort_key] || column[:key]).to_s %>
28
+ <% sortable = !!column[:sortable] && sort_path_builder.respond_to?(:call) && column_sort_key.present? %>
29
+ <% sorted = sortable && sort_key == column_sort_key %>
30
+ <% next_direction = sorted && sort_direction == "asc" ? "desc" : "asc" %>
31
+ <% sort_path = sortable ? (sort_path_builder.call(column_sort_key, next_direction) rescue nil) : nil %>
32
+ <% sort_icon = if sorted
33
+ sort_direction == "asc" ? "arrow-up" : "arrow-down"
34
+ else
35
+ "chevrons-up-down"
36
+ end %>
37
+ <% aria_sort = if sorted
38
+ sort_direction == "asc" ? "ascending" : "descending"
39
+ else
40
+ "none"
41
+ end %>
42
+ <% tooltip_options = case column[:header_tooltip]
43
+ when String
44
+ { content: column[:header_tooltip] }
45
+ when Hash
46
+ column[:header_tooltip].symbolize_keys
47
+ end %>
48
+ <% th_attributes = { scope: "col", class: column_header_class } %>
49
+ <% th_attributes[:"aria-sort"] = aria_sort if sortable %>
50
+ <%= tag.th(**th_attributes) do %>
51
+ <% header_content_class = [ "inline-flex items-center gap-1", (column_header_class.include?("text-right") ? "justify-end" : "justify-start") ].join(" ") %>
52
+ <% tooltip_bubble_class = [ "w-80 text-left normal-case tracking-normal", tooltip_options&.dig(:bubble_class) ].compact.join(" ") %>
53
+ <div class="<%= header_content_class %>">
54
+ <% if sortable && sort_path.present? %>
55
+ <%= link_to sort_path, class: "inline-flex items-center gap-1 hover:text-[color:var(--color-on-surface)]" do %>
56
+ <span><%= column[:label] %></span>
57
+ <%= ui_icon(sort_icon, class_name: "h-4 w-4") %>
58
+ <% end %>
59
+ <% else %>
60
+ <span><%= column[:label] %></span>
61
+ <% end %>
62
+ <% if tooltip_options&.dig(:content).present? %>
63
+ <%= ui_tooltip(
64
+ text: "#{column[:label]} definition",
65
+ content: tooltip_options[:content],
66
+ variant: tooltip_options.fetch(:variant, :icon),
67
+ icon: tooltip_options.fetch(:icon, "circle-help"),
68
+ trigger_class: tooltip_options[:trigger_class],
69
+ bubble_class: tooltip_bubble_class
70
+ ) %>
71
+ <% end %>
72
+ </div>
73
+ <% if sortable && sort_path.blank? %>
74
+ <span class="sr-only">Sort unavailable</span>
75
+ <% end %>
76
+ <% end %>
77
+ <% end %>
78
+ </tr>
79
+ </thead>
80
+ <tbody>
81
+ <% if rows.blank? %>
82
+ <tr>
83
+ <td colspan="<%= normalized_columns.length %>" class="px-6 py-6 text-center text-sm text-[color:color-mix(in srgb,var(--color-on-surface)_70%,transparent)]">
84
+ <%= empty_state %>
85
+ </td>
86
+ </tr>
87
+ <% else %>
88
+ <% rows.each do |row|
89
+ cells = if row.is_a?(Hash)
90
+ normalized_columns.map { |column| row[column[:key]] }
91
+ else
92
+ Array(row)
93
+ end
94
+ dynamic_attributes =
95
+ case row_attributes_config
96
+ when Proc
97
+ row_attributes_config.call(row) || {}
98
+ when Hash
99
+ row_attributes_config
100
+ else
101
+ {}
102
+ end
103
+ attributes = dynamic_attributes.deep_dup
104
+ classes = [row_classes, attributes.delete(:class)].compact.join(" ").presence
105
+ attributes[:class] = classes if classes.present?
106
+ %>
107
+ <%= tag.tr(**attributes) do %>
108
+ <% cells.each_with_index do |cell, index| %>
109
+ <% column_cell_class = [cell_classes, normalized_columns[index]&.fetch(:cell_class, nil)].compact.join(" ") %>
110
+ <td class="<%= column_cell_class %>"><%= cell %></td>
111
+ <% end %>
112
+ <% end %>
113
+ <% end %>
114
+ <% end %>
115
+ </tbody>
116
+ </table>
117
+ </div>