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,230 @@
1
+ module Baldur
2
+ module UiHelperForms
3
+ def ui_text_field_tag(name, value = nil, label: nil, supporting_text: nil, placeholder: nil, type: :text, required: false, disabled: false, wrapper_class: nil, input_class: nil, input_options: {}, multiline: false, prefix: nil, suffix: nil, &block)
4
+ options = (input_options || {}).deep_dup
5
+ input_id = options[:id].presence || "ui-text-field-#{SecureRandom.hex(3)}"
6
+ field_classes = [ "field", "text-field", ("is-disabled" if disabled), wrapper_class ].compact.join(" ")
7
+ control_classes = [ "text-field__control", input_class, options[:class] ].compact.join(" ")
8
+ support_id = "#{input_id}-support"
9
+
10
+ options[:id] = input_id
11
+ options[:class] = control_classes
12
+ options[:placeholder] = placeholder if placeholder.present?
13
+ options[:required] = true if required
14
+ options[:disabled] = true if disabled
15
+ options[:type] = type unless multiline
16
+ options[:name] = name
17
+ options[:value] = value unless value.nil? || multiline
18
+ aria = options[:aria].presence || {}
19
+ aria[:describedby] = [ aria[:describedby], support_id ].compact.join(" ").presence
20
+ options[:aria] = aria if aria.present?
21
+
22
+ baldur_render "baldur/components/text_field",
23
+ wrapper_classes: field_classes,
24
+ label: label,
25
+ supporting_text: supporting_text,
26
+ input_options: options,
27
+ input_value: multiline ? value.to_s : nil,
28
+ multiline: multiline,
29
+ prefix: prefix,
30
+ suffix: suffix,
31
+ support_id: support_id,
32
+ supporting_slot: block_given? ? capture(&block) : nil
33
+ end
34
+
35
+ def ui_date_field_tag(name, value = nil, label: nil, supporting_text: nil, placeholder: "YYYY-MM-DD", required: false, disabled: false, wrapper_class: nil, input_class: nil, input_options: {}, native_input_options: {}, toggle_label: "Open date picker", icon_name: "calendar", min_date: nil, max_date: nil)
36
+ display_options = (input_options || {}).deep_dup
37
+ native_options = (native_input_options || {}).deep_dup
38
+ input_id = display_options[:id].presence || "ui-date-field-#{SecureRandom.hex(3)}"
39
+ support_id = "#{input_id}-support"
40
+ wrapper_classes = [ "field", "date-field", wrapper_class ].compact.join(" ")
41
+
42
+ parsed_date = case value
43
+ when Date
44
+ value
45
+ when Time, DateTime, ActiveSupport::TimeWithZone
46
+ value.to_date
47
+ when String
48
+ begin
49
+ Date.iso8601(value)
50
+ rescue ArgumentError
51
+ begin
52
+ Date.parse(value)
53
+ rescue ArgumentError
54
+ nil
55
+ end
56
+ end
57
+ else
58
+ value.respond_to?(:to_date) ? value.to_date : nil
59
+ end
60
+ iso_value = parsed_date&.strftime("%Y-%m-%d")
61
+ native_id = native_options[:id].presence || "#{input_id}-native"
62
+
63
+ display_value = if display_options.key?(:value)
64
+ display_options[:value]
65
+ else
66
+ iso_value.presence || value.to_s.presence
67
+ end
68
+
69
+ display_options[:id] = input_id
70
+ display_options[:class] = [ "text-field__control", "date-field__display", input_class, display_options[:class] ].compact.join(" ")
71
+ display_options[:type] ||= "text"
72
+ display_options[:name] ||= name
73
+ display_options[:placeholder] ||= placeholder if placeholder.present?
74
+ display_options[:required] = true if required
75
+ display_options[:disabled] = true if disabled
76
+ display_options[:autocomplete] ||= "off"
77
+ display_options[:value] = display_value if display_value.present? && !display_options.key?(:value)
78
+
79
+ display_data = (display_options[:data] || {}).dup
80
+ display_data[:date_field_display] = true
81
+ display_data[:date_field_target] ||= "display"
82
+ display_data[:date_field_native_target] ||= native_id
83
+ display_data[:field_label] ||= label if label.present?
84
+ display_options[:data] = display_data
85
+
86
+ aria = display_options[:aria].presence || {}
87
+ describedby = [ aria[:describedby], support_id ].compact.join(" ").presence
88
+ aria[:describedby] = describedby if describedby.present?
89
+ display_options[:aria] = aria if aria.present?
90
+
91
+ native_options[:id] = native_id
92
+ native_options[:class] = [ "date-field__native", native_options[:class] ].compact.join(" ")
93
+ native_options[:type] ||= "date"
94
+ native_options.delete(:name) if native_options[:name].blank?
95
+ native_options[:value] = iso_value if iso_value.present? && !native_options.key?(:value)
96
+ native_options[:required] = true if required
97
+ native_options[:disabled] = true if disabled
98
+ native_options[:tabindex] ||= "-1"
99
+ native_options[:autocomplete] ||= "off"
100
+ native_options[:style] ||= "position:absolute; inset:auto; width:1px; height:1px; opacity:0; pointer-events:none;"
101
+
102
+ if min_date.present?
103
+ min_date_value = case min_date
104
+ when Date, Time, DateTime, ActiveSupport::TimeWithZone
105
+ min_date.strftime("%Y-%m-%d")
106
+ when String
107
+ min_date
108
+ end
109
+ native_options[:min] = min_date_value if min_date_value.present?
110
+ end
111
+
112
+ if max_date.present?
113
+ max_date_value = case max_date
114
+ when Date, Time, DateTime, ActiveSupport::TimeWithZone
115
+ max_date.strftime("%Y-%m-%d")
116
+ when String
117
+ max_date
118
+ end
119
+ native_options[:max] = max_date_value if max_date_value.present?
120
+ end
121
+
122
+ native_data = (native_options[:data] || {}).dup
123
+ native_data[:date_field_native] = true
124
+ native_data[:date_field_target] ||= "native"
125
+ native_data[:date_field_name] ||= name
126
+ native_data[:date_field_display_target] ||= input_id
127
+ native_options[:data] = native_data
128
+
129
+ native_aria = native_options[:aria].presence || {}
130
+ native_aria[:hidden] = "true"
131
+ native_aria[:label] ||= label if label.present?
132
+ native_options[:aria] = native_aria if native_aria.present?
133
+
134
+ baldur_render "baldur/components/date_field",
135
+ wrapper_classes: wrapper_classes,
136
+ label: label,
137
+ supporting_text: supporting_text,
138
+ display_input_options: display_options,
139
+ native_input_options: native_options,
140
+ toggle_label: toggle_label,
141
+ icon_name: icon_name,
142
+ support_id: support_id
143
+ end
144
+
145
+ def ui_mobile_field_tag(name, value = nil, label: "Mobile", placeholder: "10-digit mobile number", country_code: "+91", required: false, disabled: false, wrapper_class: nil, input_class: nil, input_options: {})
146
+ normalized_value = value.to_s.gsub(/\D+/, "").slice(0, 10)
147
+ merged_input_options = (input_options || {}).deep_dup
148
+ merged_input_options[:data] = (merged_input_options[:data] || {}).merge(
149
+ mobile_input: true,
150
+ mobile_country_code: country_code
151
+ )
152
+ merged_input_options[:inputmode] ||= "numeric"
153
+ merged_input_options[:pattern] ||= "\\d*"
154
+ merged_input_options[:autocomplete] ||= "tel"
155
+ merged_wrapper_class = [ wrapper_class, "field--mobile" ].compact.join(" ")
156
+
157
+ ui_text_field_tag(
158
+ name,
159
+ normalized_value,
160
+ label: label,
161
+ placeholder: placeholder,
162
+ type: :tel,
163
+ required: required,
164
+ disabled: disabled,
165
+ wrapper_class: merged_wrapper_class,
166
+ input_class: input_class,
167
+ input_options: merged_input_options,
168
+ prefix: country_code
169
+ )
170
+ end
171
+
172
+ def ui_menu_select_tag(name, options:, selected: nil, placeholder: "Select an option", label: nil, supporting_text: nil, wrapper_class: nil, data: nil, input_data: nil, disabled: false)
173
+ normalized = options.map do |option|
174
+ case option
175
+ when Hash
176
+ option.symbolize_keys
177
+ when Array
178
+ value, label_text, meta = option
179
+ details = meta.is_a?(Hash) ? meta.symbolize_keys : {}
180
+ { value: value, label: label_text }.merge(details)
181
+ else
182
+ { value: option, label: option }
183
+ end
184
+ end
185
+
186
+ normalized.each do |option|
187
+ option[:value] = option[:value].to_s
188
+ option[:label] = option[:label].to_s
189
+ option[:disabled] = !!option[:disabled] if option.key?(:disabled)
190
+ option[:disabled] ||= option[:available] == false if option.key?(:available)
191
+ option[:badge] = option[:badge].to_s if option[:badge]
192
+ option[:badge_variant] = option[:badge_variant].to_sym if option[:badge_variant]
193
+ option[:badge_size] = option[:badge_size].to_sym if option[:badge_size]
194
+ option[:support] = option[:support].to_s if option[:support]
195
+ option[:description] = option[:description].to_s if option[:description]
196
+ end
197
+
198
+ selected_value = selected.present? ? selected.to_s : nil
199
+ selected_option = normalized.find { |opt| opt[:value] == selected_value } if selected_value
200
+ unless selected_option || placeholder.present?
201
+ selected_option = normalized.find { |opt| !opt[:disabled] }
202
+ selected_value = selected_option&.dig(:value)
203
+ end
204
+ button_label = selected_option&.dig(:label) || placeholder
205
+
206
+ field_id = "ui-menu-select-#{SecureRandom.hex(4)}"
207
+ auto_width_class = menu_select_explicit_width?(wrapper_class) ? nil : "field--select-auto"
208
+ wrapper_classes = [ "field", "field--select", auto_width_class, wrapper_class ].compact.join(" ")
209
+
210
+ baldur_render "baldur/components/menu_select",
211
+ name: name,
212
+ field_id: field_id,
213
+ wrapper_classes: wrapper_classes,
214
+ wrapper_data: data,
215
+ label: label,
216
+ supporting_text: supporting_text,
217
+ selected_value: selected_value,
218
+ button_label: button_label,
219
+ options: normalized,
220
+ input_data: input_data,
221
+ disabled: disabled
222
+ end
223
+
224
+ private
225
+
226
+ def menu_select_explicit_width?(wrapper_class)
227
+ wrapper_class.to_s.match?(/(^|\s)(?:[a-z0-9_-]+:)*(?:w|min-w|max-w)-\S+/)
228
+ end
229
+ end
230
+ end
@@ -0,0 +1,98 @@
1
+ module Baldur
2
+ module UiHelperUnavailable
3
+ def ui_unavailable_metric(reason: nil, dependencies: nil, warning_keys: nil, text: "Not available", text_class: "text-xs text-muted")
4
+ content_tag(:span, class: "inline-flex items-center gap-1 whitespace-nowrap align-middle") do
5
+ safe_join(
6
+ [
7
+ content_tag(:span, text, class: text_class),
8
+ ui_tooltip(
9
+ text: "Why unavailable",
10
+ content: ui_unavailable_explanation_content(reason: reason, dependencies: dependencies, warning_keys: warning_keys),
11
+ icon: "info",
12
+ variant: :icon,
13
+ inline: true
14
+ )
15
+ ]
16
+ )
17
+ end
18
+ end
19
+
20
+ def ui_unavailable_explanation_content(reason:, dependencies:, warning_keys: nil)
21
+ resolved_dependencies = ui_unavailable_dependencies(dependencies: dependencies, warning_keys: warning_keys)
22
+ groups = ui_unavailable_dependency_groups(resolved_dependencies)
23
+ return ui_unavailable_explanation_sections(reason: reason, groups: groups) if reason.present? || groups.present?
24
+
25
+ Baldur.config.unavailable_fallback_message
26
+ end
27
+
28
+ def ui_unavailable_dependencies(dependencies:, warning_keys: nil)
29
+ explicit_dependencies = Array(dependencies).compact
30
+ return explicit_dependencies if explicit_dependencies.present?
31
+
32
+ Array(Baldur.config.warning_dependency_resolver.call(warning_keys))
33
+ end
34
+
35
+ def ui_unavailable_dependency_groups(dependencies)
36
+ grouped = {}
37
+ order = []
38
+
39
+ Array(dependencies).each do |dependency|
40
+ dep = dependency.respond_to?(:with_indifferent_access) ? dependency.with_indifferent_access : dependency
41
+ dataset_key = dep[:dataset_key]
42
+ field_key = dep[:field_key]
43
+ snapshot_metric = dep[:snapshot_metric]
44
+ next if dataset_key.blank? && field_key.blank? && snapshot_metric.blank?
45
+
46
+ group_key = dataset_key.presence || "__no_dataset__"
47
+ unless grouped.key?(group_key)
48
+ grouped[group_key] = {
49
+ dataset_key: dataset_key.presence,
50
+ fields: [],
51
+ metrics: []
52
+ }
53
+ order << group_key
54
+ end
55
+
56
+ group = grouped[group_key]
57
+ group[:fields] << field_key if field_key.present? && !group[:fields].include?(field_key)
58
+ group[:metrics] << snapshot_metric if snapshot_metric.present? && !group[:metrics].include?(snapshot_metric)
59
+ end
60
+
61
+ order.filter_map do |group_key|
62
+ group = grouped[group_key]
63
+ details = []
64
+ details << group[:fields].join(", ") if group[:fields].present?
65
+ details << "snapshot metric: #{group[:metrics].join(', ')}" if group[:metrics].present?
66
+ next if group[:dataset_key].blank? && details.blank?
67
+
68
+ {
69
+ dataset_label: (ui_dependency_dataset_name(group[:dataset_key]) if group[:dataset_key].present?),
70
+ details: details.join(" | ")
71
+ }
72
+ end
73
+ end
74
+
75
+ def ui_unavailable_dependency_group_content(groups)
76
+ lines = groups.map do |group|
77
+ line = if group[:dataset_label].present? && group[:details].present?
78
+ "• #{group[:dataset_label]} › #{group[:details]}"
79
+ else
80
+ "• #{group[:dataset_label].presence || group[:details]}"
81
+ end
82
+ content_tag(:span, line, class: "block text-left")
83
+ end
84
+ safe_join([ content_tag(:span, "Upload/refresh:", class: "block text-left"), *lines ])
85
+ end
86
+
87
+ def ui_unavailable_explanation_sections(reason:, groups:)
88
+ parts = []
89
+ parts << content_tag(:span, reason, class: "block text-left") if reason.present?
90
+ parts << ui_unavailable_dependency_group_content(groups) if groups.present?
91
+ safe_join(parts)
92
+ end
93
+
94
+ def ui_dependency_dataset_name(dataset_key)
95
+ Baldur.config.dependency_dataset_name_resolver.call(dataset_key)
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,30 @@
1
+ <%
2
+ open = !!local_assigns[:open]
3
+ accordion_id = local_assigns[:id] || "accordion-#{SecureRandom.hex(4)}"
4
+ summary_class = local_assigns[:summary_class] || "cursor-pointer text-base font-semibold text-on-surface flex items-center gap-2"
5
+ body_class = ["pt-4 pb-4 space-y-4", local_assigns[:body_class]].compact.join(" ")
6
+ wrapper_class = [local_assigns[:class]].compact.join(" ")
7
+ %>
8
+ <div class="<%= wrapper_class %>" data-controller="accordion">
9
+ <div data-accordion-target="item" data-open="<%= open %>">
10
+ <button
11
+ type="button"
12
+ class="<%= [summary_class, "w-full text-left"].compact.join(" ") %>"
13
+ data-action="click->accordion#toggle"
14
+ aria-expanded="<%= open ? "true" : "false" %>"
15
+ aria-controls="<%= accordion_id %>"
16
+ >
17
+ <span aria-hidden="true" data-accordion-target="icon"><%= ui_icon("chevron-right", class_name: "h-[18px] w-[18px]") %></span>
18
+ <span><%= local_assigns[:title] %></span>
19
+ </button>
20
+ <div
21
+ id="<%= accordion_id %>"
22
+ data-accordion-target="content"
23
+ aria-hidden="<%= open ? "false" : "true" %>"
24
+ >
25
+ <div class="<%= body_class %>">
26
+ <%= local_assigns[:body].presence || yield %>
27
+ </div>
28
+ </div>
29
+ </div>
30
+ </div>
@@ -0,0 +1,6 @@
1
+ <%
2
+ row_classes = ["flex flex-col gap-2 sm:flex-row sm:justify-end", local_assigns[:class]].compact.join(" ")
3
+ %>
4
+ <div class="<%= row_classes %>">
5
+ <%= safe_join(Array(buttons).map { |button_options| ui_button(**button_options.symbolize_keys) }) %>
6
+ </div>
@@ -0,0 +1,61 @@
1
+ <%
2
+ variant = (variant || :notice).to_sym
3
+ collapsible = !!local_assigns[:collapsible]
4
+ collapsed = !!local_assigns[:collapsed]
5
+ icon_map = {
6
+ notice: "info",
7
+ success: "check-circle",
8
+ warning: "triangle-alert",
9
+ error: "circle-alert"
10
+ }
11
+ icon_content = icon.presence || icon_map[variant] || "info"
12
+ classes = ["alert", "alert--#{variant}", ("alert--collapsed" if collapsed), class_name].compact.join(" ")
13
+ data_attributes = {}
14
+ if collapsible
15
+ data_attributes[:controller] = "alert"
16
+ data_attributes[:alert_collapsed_value] = collapsed
17
+ data_attributes[:alert_storage_key_value] = local_assigns[:collapse_storage_key] if local_assigns[:collapse_storage_key].present?
18
+ end
19
+ %>
20
+ <%= content_tag :div, class: classes, data: data_attributes do %>
21
+ <div class="alert__icon">
22
+ <%= ui_icon(icon_content, class_name: "h-6 w-6") %>
23
+ </div>
24
+ <div class="alert__body">
25
+ <% if title.present? %>
26
+ <p class="alert__title"><%= title %></p>
27
+ <% end %>
28
+ <% if body.present? %>
29
+ <div
30
+ class="<%= ["alert__details", ("alert__details--collapsed" if collapsed)].compact.join(" ") %>"
31
+ data-alert-target="details"
32
+ aria-hidden="<%= collapsed %>">
33
+ <div class="alert__content">
34
+ <%= body %>
35
+ </div>
36
+ </div>
37
+ <% end %>
38
+ </div>
39
+ <% if actions.present? || collapsible %>
40
+ <div class="alert__rail">
41
+ <% if actions.present? %>
42
+ <div class="alert__actions" data-alert-target="actions" <%= "hidden" if collapsed %>>
43
+ <%= actions %>
44
+ </div>
45
+ <% end %>
46
+ <% if collapsible %>
47
+ <div class="alert__summary-action" data-alert-target="expandButton" <%= "hidden" unless collapsed %>>
48
+ <%= ui_button(
49
+ label: local_assigns[:collapsed_summary_action_label].presence || "More",
50
+ variant: :text,
51
+ size: :sm,
52
+ type: :button,
53
+ class: "alert__toggle",
54
+ data: { action: "alert#expand" },
55
+ aria: { expanded: (!collapsed).to_s }
56
+ ) %>
57
+ </div>
58
+ <% end %>
59
+ </div>
60
+ <% end %>
61
+ <% end %>
@@ -0,0 +1,25 @@
1
+ <%
2
+ sizes = {
3
+ xs: "chip--xs",
4
+ sm: "text-xs",
5
+ md: "text-sm",
6
+ lg: "text-base"
7
+ }
8
+ variants = {
9
+ default: nil,
10
+ success: "chip--success",
11
+ warn: "chip--warn",
12
+ danger: "chip--danger",
13
+ disabled: "chip--disabled",
14
+ outline: nil,
15
+ accent: "chip--primary"
16
+ }
17
+ badge_classes = [
18
+ "chip",
19
+ sizes[size || :sm],
20
+ variants[variant || :default]
21
+ ].compact.join(" ")
22
+ html_options = local_assigns[:html_options] ? local_assigns[:html_options].dup : {}
23
+ html_options[:class] = [ badge_classes, html_options[:class] ].compact.join(" ")
24
+ %>
25
+ <%= content_tag(:span, text, html_options) %>
@@ -0,0 +1,81 @@
1
+ <%
2
+ label = local_assigns[:label]
3
+ variant = (local_assigns[:variant] || :primary).to_sym
4
+ size = (local_assigns[:size] || :md).to_sym
5
+ href = local_assigns[:href]
6
+ method = local_assigns[:method].presence&.to_s&.downcase
7
+ non_get_method = method.present? && method != "get"
8
+ icon = local_assigns[:icon]
9
+ variant_classes = {
10
+ primary: "button button--filled",
11
+ filled: "button button--filled",
12
+ tonal: "button button--tonal",
13
+ secondary: "button button--tonal",
14
+ danger_tonal: "button button--tonal-error",
15
+ accent: "button button--tertiary",
16
+ outline: "button button--outlined",
17
+ text: "button button--text",
18
+ ghost: "button button--text",
19
+ danger: "button button--error",
20
+ elevated: "button button--elevated"
21
+ }
22
+ size_classes = {
23
+ xs: "button--sm",
24
+ sm: "button--sm",
25
+ md: nil,
26
+ lg: "button--lg"
27
+ }
28
+ width_class = local_assigns[:block] ? "button--block" : nil
29
+ classes = [
30
+ variant_classes[variant] || variant_classes[:primary],
31
+ size_classes[size],
32
+ width_class,
33
+ local_assigns[:class]
34
+ ].compact.join(" ")
35
+ content = capture do
36
+ if icon.present?
37
+ icon_markup = icon.to_s.include?("<") ? icon : ui_icon(icon, class_name: "h-4 w-4")
38
+ concat content_tag(:span, icon_markup, class: "inline-flex items-center", aria: { hidden: true })
39
+ end
40
+ concat content_tag(:span, label || "Action")
41
+ end
42
+ %>
43
+ <% disabled = !!local_assigns[:disabled] %>
44
+
45
+ <% if href.present? %>
46
+ <% if non_get_method %>
47
+ <%= button_to href,
48
+ method: method.to_sym,
49
+ class: classes,
50
+ data: local_assigns[:data],
51
+ disabled: disabled,
52
+ params: local_assigns[:params],
53
+ form_class: ["inline-block", local_assigns[:form_class]].compact.join(" ") do %>
54
+ <%= content %>
55
+ <% end %>
56
+ <% else %>
57
+ <%
58
+ safe_rel = local_assigns[:rel]
59
+ if local_assigns[:target] == "_blank"
60
+ safe_rel = [safe_rel, "noopener noreferrer"].compact.join(" ").strip
61
+ end
62
+ %>
63
+ <%= link_to href,
64
+ class: classes,
65
+ data: local_assigns[:data],
66
+ aria: local_assigns[:aria],
67
+ target: local_assigns[:target],
68
+ rel: safe_rel.presence do %>
69
+ <%= content %>
70
+ <% end %>
71
+ <% end %>
72
+ <% else %>
73
+ <%= button_tag type: local_assigns[:type] || "button",
74
+ class: classes,
75
+ data: local_assigns[:data],
76
+ aria: local_assigns[:aria],
77
+ form: local_assigns[:form],
78
+ disabled: disabled do %>
79
+ <%= content %>
80
+ <% end %>
81
+ <% end %>
@@ -0,0 +1,40 @@
1
+ <%
2
+ variant_name = (variant || :default).to_sym
3
+ base_classes = "card"
4
+ variant_classes = {
5
+ default: nil,
6
+ accent: "card--primary",
7
+ surface: "card--surface",
8
+ danger: "card--attention"
9
+ }
10
+ card_classes = [base_classes, variant_classes[variant_name] || variant_classes[:default], local_assigns[:class]].compact.join(" ")
11
+ actions_content = if actions.respond_to?(:call)
12
+ capture(&actions)
13
+ else
14
+ actions
15
+ end
16
+ body_classes = ["mt-4", body_class].compact.join(" ")
17
+ %>
18
+ <article class="<%= card_classes %>">
19
+ <div class="flex flex-col gap-3">
20
+ <div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
21
+ <div>
22
+ <% if badge.present? %>
23
+ <p class="text-xs font-semibold uppercase tracking-wide text-[color:var(--color-primary)]"><%= badge %></p>
24
+ <% end %>
25
+ <% if title.present? %>
26
+ <h2 class="text-lg font-semibold text-[color:var(--color-on-surface)]"><%= title %></h2>
27
+ <% end %>
28
+ <% if description.present? %>
29
+ <p class="text-sm text-[color:color-mix(in srgb,var(--color-on-surface) 75%,transparent)]"><%= description %></p>
30
+ <% end %>
31
+ </div>
32
+ <% if actions_content.present? %>
33
+ <div class="flex flex-wrap gap-2"><%= actions_content %></div>
34
+ <% end %>
35
+ </div>
36
+ <% if body.present? %>
37
+ <div class="<%= body_classes %>"><%= body %></div>
38
+ <% end %>
39
+ </div>
40
+ </article>
@@ -0,0 +1,42 @@
1
+ <%
2
+ actions_content = if actions.respond_to?(:call)
3
+ capture(&actions)
4
+ else
5
+ actions
6
+ end
7
+ footer_content = if footer.respond_to?(:call)
8
+ capture(&footer)
9
+ else
10
+ footer
11
+ end
12
+ card_classes = ["chart-card", local_assigns[:class]].compact.join(" ")
13
+ %>
14
+ <section class="<%= card_classes %>">
15
+ <% if title.present? || description.present? || actions_content.present? %>
16
+ <div class="chart-card__header">
17
+ <div>
18
+ <% if title.present? %>
19
+ <h3 class="text-lg font-semibold text-[color:var(--color-on-surface)]"><%= title %></h3>
20
+ <% end %>
21
+ <% if description.present? %>
22
+ <p class="text-sm text-muted"><%= description %></p>
23
+ <% end %>
24
+ </div>
25
+ <% if actions_content.present? %>
26
+ <div class="flex flex-wrap items-center gap-2">
27
+ <%= actions_content %>
28
+ </div>
29
+ <% end %>
30
+ </div>
31
+ <% end %>
32
+
33
+ <div class="chart-card__body">
34
+ <%= body %>
35
+ </div>
36
+
37
+ <% if footer_content.present? %>
38
+ <div class="chart-card__footer">
39
+ <%= footer_content %>
40
+ </div>
41
+ <% end %>
42
+ </section>
@@ -0,0 +1,27 @@
1
+ <%
2
+ input_id = id.presence || [name, value].compact.join("_").to_s.parameterize(separator: "_")
3
+ wrapper_classes = ["checkbox", ("is-disabled" if disabled), wrapper_class].compact.join(" ")
4
+ native_classes = ["checkbox__native", input_class].compact.join(" ")
5
+
6
+ body_fragments = []
7
+ if label.present?
8
+ body_fragments << content_tag(:span, label, class: "checkbox__label")
9
+ end
10
+ if description.present?
11
+ body_fragments << content_tag(:span, description, class: "checkbox__description")
12
+ end
13
+ body_fragments << body if body.present?
14
+ body_content = safe_join(body_fragments) if body_fragments.any?
15
+ %>
16
+
17
+ <label class="<%= wrapper_classes %>">
18
+ <span class="checkbox__control">
19
+ <%= check_box_tag name, value, checked, id: input_id, required: required, disabled: disabled, data: data, aria: aria, form: form, class: native_classes %>
20
+ <span class="checkbox__box" aria-hidden="true">
21
+ <span class="checkbox__check"><%= ui_icon("check", class_name: "h-4 w-4") %></span>
22
+ </span>
23
+ </span>
24
+ <% if body_content.present? %>
25
+ <span class="checkbox__body"><%= body_content %></span>
26
+ <% end %>
27
+ </label>