okonomi_ui_kit 0.1.8 → 0.1.10

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 (81) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +50 -6
  3. data/app/assets/builds/okonomi_ui_kit/application.tailwind.css +508 -225
  4. data/app/helpers/okonomi_ui_kit/CLAUDE.md +619 -0
  5. data/app/helpers/okonomi_ui_kit/application_helper.rb +8 -0
  6. data/app/helpers/okonomi_ui_kit/attribute_section_helper.rb +5 -5
  7. data/app/helpers/okonomi_ui_kit/component.rb +14 -6
  8. data/app/helpers/okonomi_ui_kit/components/alert.rb +1 -1
  9. data/app/helpers/okonomi_ui_kit/components/badge.rb +4 -4
  10. data/app/helpers/okonomi_ui_kit/components/breadcrumbs.rb +4 -4
  11. data/app/helpers/okonomi_ui_kit/components/button_base.rb +94 -22
  12. data/app/helpers/okonomi_ui_kit/components/button_tag.rb +14 -8
  13. data/app/helpers/okonomi_ui_kit/components/button_to.rb +8 -7
  14. data/app/helpers/okonomi_ui_kit/components/code.rb +41 -37
  15. data/app/helpers/okonomi_ui_kit/components/confirmation_modal.rb +130 -0
  16. data/app/helpers/okonomi_ui_kit/components/dropdown_button.rb +147 -0
  17. data/app/helpers/okonomi_ui_kit/components/forms/check_box_with_label.rb +38 -0
  18. data/app/helpers/okonomi_ui_kit/components/forms/collection_select.rb +57 -0
  19. data/app/helpers/okonomi_ui_kit/components/forms/date_field.rb +9 -0
  20. data/app/helpers/okonomi_ui_kit/components/forms/datetime_local_field.rb +9 -0
  21. data/app/helpers/okonomi_ui_kit/components/forms/email_field.rb +9 -0
  22. data/app/helpers/okonomi_ui_kit/components/forms/field.rb +24 -0
  23. data/app/helpers/okonomi_ui_kit/components/forms/field_set.rb +17 -0
  24. data/app/helpers/okonomi_ui_kit/components/forms/input_base.rb +57 -0
  25. data/app/helpers/okonomi_ui_kit/components/forms/label.rb +27 -0
  26. data/app/helpers/okonomi_ui_kit/components/forms/multi_select.rb +18 -0
  27. data/app/helpers/okonomi_ui_kit/components/forms/number_field.rb +9 -0
  28. data/app/helpers/okonomi_ui_kit/components/forms/password_field.rb +9 -0
  29. data/app/helpers/okonomi_ui_kit/components/forms/search_field.rb +9 -0
  30. data/app/helpers/okonomi_ui_kit/components/forms/select.rb +57 -0
  31. data/app/helpers/okonomi_ui_kit/components/forms/show_if.rb +28 -0
  32. data/app/helpers/okonomi_ui_kit/components/forms/telephone_field.rb +9 -0
  33. data/app/helpers/okonomi_ui_kit/components/forms/text_area.rb +9 -0
  34. data/app/helpers/okonomi_ui_kit/components/forms/text_field.rb +9 -0
  35. data/app/helpers/okonomi_ui_kit/components/forms/time_field.rb +9 -0
  36. data/app/helpers/okonomi_ui_kit/components/forms/upload_field.rb +25 -0
  37. data/app/helpers/okonomi_ui_kit/components/forms/url_field.rb +9 -0
  38. data/app/helpers/okonomi_ui_kit/components/forms.rb +6 -0
  39. data/app/helpers/okonomi_ui_kit/components/icon.rb +6 -6
  40. data/app/helpers/okonomi_ui_kit/components/link_to.rb +11 -10
  41. data/app/helpers/okonomi_ui_kit/components/navigation.rb +98 -0
  42. data/app/helpers/okonomi_ui_kit/components/page.rb +18 -203
  43. data/app/helpers/okonomi_ui_kit/components/page_header.rb +111 -0
  44. data/app/helpers/okonomi_ui_kit/components/page_section.rb +145 -0
  45. data/app/helpers/okonomi_ui_kit/components/table.rb +7 -8
  46. data/app/helpers/okonomi_ui_kit/components/typography.rb +16 -16
  47. data/app/helpers/okonomi_ui_kit/components.rb +4 -0
  48. data/app/helpers/okonomi_ui_kit/configs.rb +4 -0
  49. data/app/helpers/okonomi_ui_kit/form_builder.rb +39 -130
  50. data/app/helpers/okonomi_ui_kit/form_component.rb +7 -0
  51. data/app/helpers/okonomi_ui_kit/svg_icons.rb +5 -5
  52. data/app/helpers/okonomi_ui_kit/t_w_merge.rb +33 -27
  53. data/app/helpers/okonomi_ui_kit/ui_helper.rb +17 -58
  54. data/app/javascript/okonomi_ui_kit/controllers/dropdown_controller.js +6 -0
  55. data/app/views/okonomi/components/confirmation_modal/_confirmation_modal.html.erb +76 -0
  56. data/app/views/okonomi/components/dropdown_button/_dropdown_button.html.erb +282 -0
  57. data/app/views/okonomi/components/forms/check_box_with_label/_check_box_with_label.html.erb +6 -0
  58. data/app/views/okonomi/{forms/tailwind → components/forms/field}/_field.html.erb +7 -7
  59. data/app/views/okonomi/components/forms/field_set/_field_set.html.erb +3 -0
  60. data/app/views/okonomi/components/forms/upload_field/_upload_field.html.erb +1 -0
  61. data/app/views/okonomi/components/navigation/_link.html.erb +18 -0
  62. data/app/views/okonomi/components/navigation/_navigation.html.erb +4 -0
  63. data/app/views/okonomi/components/page/_page.html.erb +1 -1
  64. data/app/views/okonomi/components/page_header/_page_header.html.erb +4 -0
  65. data/app/views/okonomi/components/page_section/_page_section.html.erb +4 -0
  66. data/app/views/okonomi/forms/tailwind/_checkbox_label.html.erb +2 -2
  67. data/app/views/okonomi/forms/tailwind/_multi_select.html.erb +2 -4
  68. data/app/views/okonomi/forms/tailwind/_upload_field.html.erb +10 -10
  69. data/config/importmap.rb +1 -1
  70. data/lib/okonomi_ui_kit/engine.rb +0 -1
  71. data/lib/okonomi_ui_kit/version.rb +1 -1
  72. metadata +47 -16
  73. data/app/helpers/okonomi_ui_kit/navigation_helper.rb +0 -72
  74. data/app/helpers/okonomi_ui_kit/theme.rb +0 -136
  75. data/app/helpers/okonomi_ui_kit/theme_helper.rb +0 -17
  76. data/app/views/okonomi/forms/tailwind/_field_set.html.erb +0 -3
  77. data/app/views/okonomi/modals/_confirmation_modal.html.erb +0 -77
  78. data/app/views/okonomi/navigation/_link.html.erb +0 -15
  79. data/app/views/okonomi/navigation/_menu.html.erb +0 -3
  80. data/app/views/okonomi/navigation/_navbar.html.erb +0 -105
  81. data/app/views/okonomi/page_builder/_page.html.erb +0 -3
@@ -0,0 +1,57 @@
1
+ module OkonomiUiKit
2
+ module Components
3
+ module Forms
4
+ class CollectionSelect < OkonomiUiKit::FormComponent
5
+ def render(form, method, collection, value_method, text_method, options = {}, html_options = {})
6
+ html_options = html_options.with_indifferent_access
7
+ css = build_select_classes(form, method, html_options)
8
+
9
+ select_html = form.collection_select(method, collection, value_method, text_method, options, html_options.merge(class: css))
10
+ icon_html = view.ui.icon(
11
+ style(:icon, :file),
12
+ class: style(:icon, :class)
13
+ )
14
+
15
+ view.content_tag(:div, class: style(:wrapper)) do
16
+ view.concat(select_html)
17
+ view.concat(icon_html)
18
+ end
19
+ end
20
+
21
+ register_styles :default do
22
+ {
23
+ wrapper: "relative",
24
+ root: "w-full appearance-none border-0 pl-3 pr-10 py-2 rounded-md ring-1 focus:outline-none focus-within:ring-1",
25
+ error: "bg-danger-100 text-danger-400 ring-danger-400 focus:ring-danger-600",
26
+ valid: "text-default-700 ring-gray-300 focus-within:ring-gray-400",
27
+ icon: {
28
+ file: "heroicons/mini/chevron-down",
29
+ class: "absolute right-2 top-1/2 -translate-y-1/2 size-5 text-gray-400 pointer-events-none"
30
+ }
31
+ }
32
+ end
33
+
34
+ private
35
+
36
+ def build_select_classes(form, method, html_options)
37
+ css = [
38
+ style(:root),
39
+ when_errors(form, method, style(:error), style(:valid)),
40
+ html_options[:class]
41
+ ].compact.join(" ").split(" ").uniq
42
+
43
+ TWMerge.merge(css)
44
+ end
45
+
46
+ def when_errors(form, method, value, default_value = nil)
47
+ key = method.to_s.gsub("_id", "").to_sym
48
+ if form.object.errors.include?(key) || form.object.errors.include?(method)
49
+ value
50
+ else
51
+ default_value
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,9 @@
1
+ module OkonomiUiKit
2
+ module Components
3
+ module Forms
4
+ class DateField < OkonomiUiKit::Components::Forms::InputBase
5
+ type :date
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module OkonomiUiKit
2
+ module Components
3
+ module Forms
4
+ class DatetimeLocalField < OkonomiUiKit::Components::Forms::InputBase
5
+ type :datetime_local
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module OkonomiUiKit
2
+ module Components
3
+ module Forms
4
+ class EmailField < OkonomiUiKit::Components::Forms::InputBase
5
+ type :email
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,24 @@
1
+ module OkonomiUiKit
2
+ module Components
3
+ module Forms
4
+ class Field < OkonomiUiKit::FormComponent
5
+ def render(form, field_id = nil, options = {}, &block)
6
+ view.render(template_path, component: self, form: form, field_id: field_id, options: options, &block)
7
+ end
8
+
9
+ register_styles :default do
10
+ {
11
+ wrapper: "w-full flex flex-col gap-2",
12
+ header: "flex justify-between items-center",
13
+ hint: {
14
+ trigger: "text-primary-600 text-sm hover:cursor-help",
15
+ content: "text-xs absolute border rounded-md bg-gray-100 border-gray-600 text-gray-600 p-1 z-10"
16
+ },
17
+ content: "block",
18
+ error: "mt-1 text-danger-600 text-sm"
19
+ }
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,17 @@
1
+ module OkonomiUiKit
2
+ module Components
3
+ module Forms
4
+ class FieldSet < OkonomiUiKit::FormComponent
5
+ def render(form, options = {}, &block)
6
+ view.render(template_path, component: self, options: options, form: form, &block)
7
+ end
8
+
9
+ register_styles :default do
10
+ {
11
+ root: "w-full flex flex-col gap-4 col-span-1 sm:col-span-3 md:col-span-5"
12
+ }
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,57 @@
1
+ module OkonomiUiKit
2
+ module Components
3
+ module Forms
4
+ class InputBase < OkonomiUiKit::FormComponent
5
+ def render_arguments(object, method, options = {})
6
+ css = input_field_classes(object, method, self.class.type_value, options)
7
+ [ method, { autocomplete: "off" }.merge(options).merge(class: css) ]
8
+ end
9
+
10
+ register_styles :default do
11
+ {
12
+ root: "w-full border-0 px-3 py-2 rounded-md ring-1 focus:outline-none focus-within:ring-1",
13
+ error: "bg-danger-100 text-danger-400 ring-danger-400 focus:ring-danger-600",
14
+ valid: "text-default-700 ring-gray-300 focus-within:ring-gray-400",
15
+ disabled: "disabled:bg-gray-50 disabled:cursor-not-allowed"
16
+ }
17
+ end
18
+
19
+ def self.type(type)
20
+ @type = type
21
+ end
22
+
23
+ def self.type_value
24
+ @type || :text
25
+ end
26
+
27
+ def input_field_classes(object, method, type, options, include_disabled: true)
28
+ css_classes = OkonomiUiKit::TWMerge.merge_all(
29
+ style(:root),
30
+ when_errors(
31
+ object,
32
+ method,
33
+ style(:error),
34
+ style(:valid)
35
+ ),
36
+ options[:class]
37
+ )
38
+
39
+ if include_disabled
40
+ css_classes = OkonomiUiKit::TWMerge.merge_all(css_classes, style(:disabled))
41
+ end
42
+
43
+ css_classes
44
+ end
45
+
46
+ def when_errors(object, method, value, default_value = nil)
47
+ key = method.to_s.gsub("_id", "").to_sym
48
+ if object.errors.include?(key) || object.errors.include?(method)
49
+ value
50
+ else
51
+ default_value
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,27 @@
1
+ module OkonomiUiKit
2
+ module Components
3
+ module Forms
4
+ class Label < OkonomiUiKit::FormComponent
5
+ def render(form, method, text = nil, options = {}, &block)
6
+ options = options.with_indifferent_access
7
+ base_classes = style(:root)
8
+ # Call the parent class label method directly to avoid infinite loop
9
+ ActionView::Helpers::FormBuilder.instance_method(:label).bind(form).call(method, text, merge_class(options, base_classes), &block)
10
+ end
11
+
12
+ register_styles :default do
13
+ {
14
+ root: "block text-sm/6 font-medium text-gray-900"
15
+ }
16
+ end
17
+
18
+ private
19
+
20
+ def merge_class(options, new_class)
21
+ options[:class] = TWMerge.merge(options[:class] || "", new_class)
22
+ options
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,18 @@
1
+ module OkonomiUiKit
2
+ module Components
3
+ module Forms
4
+ class MultiSelect < OkonomiUiKit::FormComponent
5
+ def render(form, method, options = {})
6
+ view.render(template_path, component: self, form: form, method: method, options: options)
7
+ end
8
+
9
+ register_styles :default do
10
+ {
11
+ wrapper: "grid grid-cols-2 gap-2",
12
+ item: "flex items-center"
13
+ }
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,9 @@
1
+ module OkonomiUiKit
2
+ module Components
3
+ module Forms
4
+ class NumberField < OkonomiUiKit::Components::Forms::InputBase
5
+ type :number
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module OkonomiUiKit
2
+ module Components
3
+ module Forms
4
+ class PasswordField < OkonomiUiKit::Components::Forms::InputBase
5
+ type :password
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module OkonomiUiKit
2
+ module Components
3
+ module Forms
4
+ class SearchField < OkonomiUiKit::Components::Forms::InputBase
5
+ type :search
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,57 @@
1
+ module OkonomiUiKit
2
+ module Components
3
+ module Forms
4
+ class Select < OkonomiUiKit::FormComponent
5
+ def render(form, method, choices = nil, options = {}, html_options = {}, &block)
6
+ html_options = html_options.with_indifferent_access
7
+ css = build_select_classes(form, method, html_options)
8
+
9
+ select_html = ActionView::Helpers::FormBuilder.instance_method(:select).bind(form).call(method, choices, options, html_options.merge(class: css), &block)
10
+ icon_html = view.ui.icon(
11
+ style(:icon, :file),
12
+ class: style(:icon, :class)
13
+ )
14
+
15
+ view.content_tag(:div, class: style(:wrapper)) do
16
+ view.concat(select_html)
17
+ view.concat(icon_html)
18
+ end
19
+ end
20
+
21
+ register_styles :default do
22
+ {
23
+ wrapper: "relative",
24
+ root: "w-full appearance-none border-0 pl-3 pr-10 py-2 rounded-md ring-1 focus:outline-none focus-within:ring-1",
25
+ error: "bg-danger-100 text-danger-400 ring-danger-400 focus:ring-danger-600",
26
+ valid: "text-default-700 ring-gray-300 focus-within:ring-gray-400",
27
+ icon: {
28
+ file: "heroicons/mini/chevron-down",
29
+ class: "absolute right-2 top-1/2 -translate-y-1/2 size-5 text-gray-400 pointer-events-none"
30
+ }
31
+ }
32
+ end
33
+
34
+ private
35
+
36
+ def build_select_classes(form, method, html_options)
37
+ css = [
38
+ style(:root),
39
+ when_errors(form, method, style(:error), style(:valid)),
40
+ html_options[:class]
41
+ ].compact.join(" ").split(" ").uniq
42
+
43
+ css.join(" ")
44
+ end
45
+
46
+ def when_errors(form, method, value, default_value = nil)
47
+ key = method.to_s.gsub("_id", "").to_sym
48
+ if form.object.errors.include?(key) || form.object.errors.include?(method)
49
+ value
50
+ else
51
+ default_value
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,28 @@
1
+ module OkonomiUiKit
2
+ module Components
3
+ module Forms
4
+ class ShowIf < OkonomiUiKit::FormComponent
5
+ def render(form, options = {}, &block)
6
+ field = options[:field]
7
+ equals = options[:equals]
8
+ field_id = "#{form.object_name}_#{field}"
9
+ view.tag.div(
10
+ class: style(:root),
11
+ data: {
12
+ controller: "form-field-visibility",
13
+ "form-field-visibility-field-id-value": field_id,
14
+ "form-field-visibility-equals-value": equals
15
+ },
16
+ &block
17
+ )
18
+ end
19
+
20
+ register_styles :default do
21
+ {
22
+ root: "hidden"
23
+ }
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,9 @@
1
+ module OkonomiUiKit
2
+ module Components
3
+ module Forms
4
+ class TelephoneField < OkonomiUiKit::Components::Forms::InputBase
5
+ type :telephone_field
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module OkonomiUiKit
2
+ module Components
3
+ module Forms
4
+ class TextArea < OkonomiUiKit::Components::Forms::InputBase
5
+ type :textarea
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module OkonomiUiKit
2
+ module Components
3
+ module Forms
4
+ class TextField < OkonomiUiKit::Components::Forms::InputBase
5
+ type :text
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module OkonomiUiKit
2
+ module Components
3
+ module Forms
4
+ class TimeField < OkonomiUiKit::Components::Forms::InputBase
5
+ type :time
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,25 @@
1
+ module OkonomiUiKit
2
+ module Components
3
+ module Forms
4
+ class UploadField < OkonomiUiKit::FormComponent
5
+ def render(form, method, options = {})
6
+ view.render(template_path, component: self, form: form, method: method, options: options)
7
+ end
8
+
9
+ register_styles :default do
10
+ {
11
+ dropzone: "border-2 border-dashed border-gray-300 rounded-md p-6 text-center cursor-pointer hover:bg-gray-50 relative",
12
+ label: "block cursor-pointer",
13
+ content: "flex flex-col items-center text-gray-600 space-y-2",
14
+ preview: "w-full flex justify-center items-center",
15
+ preview_image: "max-h-32",
16
+ icon: "size-12 text-gray-400",
17
+ filename: "text-sm text-gray-500",
18
+ file_input: "hidden",
19
+ clear_button: "absolute top-2 right-2 bg-red-100 text-red-700 px-2 py-1 text-xs rounded hover:bg-red-200"
20
+ }
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,9 @@
1
+ module OkonomiUiKit
2
+ module Components
3
+ module Forms
4
+ class UrlField < OkonomiUiKit::Components::Forms::InputBase
5
+ type :url
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,6 @@
1
+ module OkonomiUiKit
2
+ module Components
3
+ module Forms
4
+ end
5
+ end
6
+ end
@@ -3,18 +3,18 @@ module OkonomiUiKit
3
3
  class Icon < OkonomiUiKit::Component
4
4
  def render(name, options = {})
5
5
  options = options.with_indifferent_access
6
-
6
+
7
7
  # Extract specific icon options
8
8
  variant = options.delete(:variant) || :outlined
9
9
  width = options.delete(:width)
10
10
  height = options.delete(:height)
11
-
11
+
12
12
  # Build classes array
13
13
  classes = [
14
14
  style(:base),
15
15
  options.delete(:class)
16
- ].compact.join(' ')
17
-
16
+ ].compact.join(" ")
17
+
18
18
  view.render(
19
19
  template_path,
20
20
  name: name,
@@ -25,7 +25,7 @@ module OkonomiUiKit
25
25
  options: options
26
26
  )
27
27
  end
28
-
28
+
29
29
  register_styles :default do
30
30
  {
31
31
  base: "inline-block"
@@ -33,4 +33,4 @@ module OkonomiUiKit
33
33
  end
34
34
  end
35
35
  end
36
- end
36
+ end
@@ -3,21 +3,22 @@ module OkonomiUiKit
3
3
  class LinkTo < OkonomiUiKit::Components::ButtonBase
4
4
  def render(name = nil, options = nil, html_options = nil, &block)
5
5
  html_options, options, name = options, name, block if block_given?
6
-
6
+
7
7
  html_options ||= {}
8
8
  html_options = html_options.with_indifferent_access
9
+
10
+ variant = (html_options.delete(:variant) || "text").to_sym
11
+ color = (html_options.delete(:color) || "default").to_sym
9
12
 
10
- variant = (html_options.delete(:variant) || 'text').to_sym
11
- color = (html_options.delete(:color) || 'default').to_sym
12
-
13
+ # Extract icon configuration
14
+ icon_config, html_options = extract_icon_config(html_options)
15
+
13
16
  html_options[:class] = build_button_class(variant: variant, color: color, classes: html_options[:class])
14
-
15
- if block_given?
16
- view.link_to(options, html_options, &block)
17
- else
18
- view.link_to(name, options, html_options)
17
+
18
+ view.link_to(options, html_options) do
19
+ render_button_content(icon_config, name, &block)
19
20
  end
20
21
  end
21
22
  end
22
23
  end
23
- end
24
+ end
@@ -0,0 +1,98 @@
1
+ module OkonomiUiKit
2
+ module Components
3
+ class Navigation < OkonomiUiKit::Component
4
+ def render(options = {}, &block)
5
+ options = options.with_indifferent_access
6
+ builder = NavigationBuilder.new(view, self)
7
+
8
+ view.render(template_path, builder: builder, options: options, &block)
9
+ end
10
+
11
+ register_styles :default do
12
+ {
13
+ menu: {
14
+ base: "flex flex-1 flex-col gap-y-7"
15
+ },
16
+ group: {
17
+ title: "text-xs/6 font-semibold text-gray-400",
18
+ list: "-mx-2 mt-2 space-y-1"
19
+ },
20
+ link: {
21
+ base: "group flex gap-x-3 rounded-md p-2 text-sm/6 font-semibold hover:bg-gray-50 hover:text-primary-600 text-gray-700",
22
+ active: "group flex gap-x-3 rounded-md p-2 text-sm/6 font-semibold hover:bg-gray-50 hover:text-primary-600 bg-gray-50 text-primary-600",
23
+ icon: "size-6 text-gray-400 group-hover:text-primary-600",
24
+ initials: {
25
+ base: "flex size-6 shrink-0 items-center justify-center rounded-lg border border-gray-200 bg-white text-[0.625rem] font-medium text-gray-400 group-hover:border-primary-600 group-hover:text-primary-600"
26
+ }
27
+ },
28
+ profile_section: {
29
+ base: "-mx-6 mt-auto"
30
+ }
31
+ }
32
+ end
33
+ end
34
+
35
+ class NavigationBuilder
36
+ attr_reader :view, :navigation_component, :groups
37
+
38
+ def initialize(view, navigation_component)
39
+ @view = view
40
+ @navigation_component = navigation_component
41
+ @groups = []
42
+ end
43
+
44
+ def group(title, &block)
45
+ group_builder = NavigationGroupBuilder.new(view, navigation_component)
46
+ yield(group_builder)
47
+
48
+ @groups << view.tag.li do
49
+ view.tag.div(title, class: style(:group, :title)) +
50
+ view.tag.ul(group_builder.render_links, role: "list", class: style(:group, :list))
51
+ end
52
+ end
53
+
54
+ def profile_section(&block)
55
+ content = view.capture(&block)
56
+ @groups << view.tag.li(content, class: style(:profile_section, :base))
57
+ end
58
+
59
+ def style(*args)
60
+ navigation_component.style(*args)
61
+ end
62
+
63
+ def render_groups
64
+ view.safe_join(@groups)
65
+ end
66
+ end
67
+
68
+ class NavigationGroupBuilder
69
+ attr_reader :view, :navigation_component, :links
70
+
71
+ def initialize(view, navigation_component)
72
+ @view = view
73
+ @navigation_component = navigation_component
74
+ @links = []
75
+ end
76
+
77
+ def nav_link(title, path, icon: nil, initials: nil, exact: false)
78
+ @links << view.tag.li do
79
+ view.render "okonomi/components/navigation/link",
80
+ path: path,
81
+ title: title,
82
+ icon: icon,
83
+ initials: initials,
84
+ exact: exact,
85
+ style_helper: self
86
+ end
87
+ end
88
+
89
+ def style(*args)
90
+ navigation_component.style(*args)
91
+ end
92
+
93
+ def render_links
94
+ view.safe_join(@links)
95
+ end
96
+ end
97
+ end
98
+ end