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
@@ -2,7 +2,7 @@ module OkonomiUiKit
2
2
  module Components
3
3
  class Alert < OkonomiUiKit::Component
4
4
  def render(title, options = {}, &block)
5
- view.render(template_path, title:, options: options.with_indifferent_access, &block)
5
+ view.render(template_path, title: title, options: options.with_indifferent_access, &block)
6
6
  end
7
7
  end
8
8
  end
@@ -5,11 +5,11 @@ module OkonomiUiKit
5
5
  options = options.with_indifferent_access
6
6
  severity = (options.delete(:severity) || options.delete(:variant) || :default).to_sym
7
7
 
8
- classes = [
8
+ classes = tw_merge(
9
9
  style(:base),
10
- style(:severities, severity) || '',
11
- options.delete(:class) || ''
12
- ].reject(&:blank?).join(' ')
10
+ style(:severities, severity),
11
+ options.delete(:class)
12
+ )
13
13
 
14
14
  view.tag.span(text, class: classes, **options)
15
15
  end
@@ -28,7 +28,7 @@ module OkonomiUiKit
28
28
  }
29
29
  end
30
30
 
31
- def initialize(template, options = {})
31
+ def initialize(template)
32
32
  super
33
33
  @items = []
34
34
  @builder = BreadcrumbBuilder.new(self)
@@ -36,9 +36,9 @@ module OkonomiUiKit
36
36
 
37
37
  def render(options = {}, &block)
38
38
  return "" if block.nil?
39
-
39
+
40
40
  block.call(@builder)
41
- view.render("okonomi/components/breadcrumbs/breadcrumbs",
41
+ view.render("okonomi/components/breadcrumbs/breadcrumbs",
42
42
  component: self,
43
43
  items: @items,
44
44
  options: options
@@ -66,4 +66,4 @@ module OkonomiUiKit
66
66
  end
67
67
  end
68
68
  end
69
- end
69
+ end
@@ -1,40 +1,108 @@
1
1
  module OkonomiUiKit
2
2
  module Components
3
3
  class ButtonBase < OkonomiUiKit::Component
4
- def build_button_class(variant:, color:, classes: '')
4
+ def build_button_class(variant:, color:, classes: "")
5
5
  [
6
- style(:root) || '',
7
- style(variant.to_sym, :root) || '',
8
- style(variant.to_sym, :colors, color.to_sym) || '',
9
- classes,
10
- ].reject(&:blank?).join(' ')
6
+ style(:root) || "",
7
+ style(variant.to_sym, :root) || "",
8
+ style(variant.to_sym, :colors, color.to_sym) || "",
9
+ classes
10
+ ].reject(&:blank?).join(" ")
11
+ end
12
+
13
+ # Extracts and normalizes icon configuration from options
14
+ # Returns [icon_config, updated_options]
15
+ # icon_config will be nil or { path: "icon/path", position: :start/:end }
16
+ def extract_icon_config(options)
17
+ return [nil, options] unless options.is_a?(Hash)
18
+
19
+ icon_option = options.delete(:icon)
20
+ return [nil, options] unless icon_option
21
+
22
+ icon_config = case icon_option
23
+ when String
24
+ { path: icon_option, position: :start }
25
+ when Hash
26
+ if icon_option[:start]
27
+ { path: icon_option[:start], position: :start }
28
+ elsif icon_option[:end]
29
+ { path: icon_option[:end], position: :end }
30
+ else
31
+ # Invalid hash format, ignore
32
+ nil
33
+ end
34
+ else
35
+ nil
36
+ end
37
+
38
+ [icon_config, options]
39
+ end
40
+
41
+ # Renders button content with optional icon
42
+ # icon_config: { path: "icon/path", position: :start/:end }
43
+ # content: String or block content
44
+ # block: Optional block for content
45
+ def render_button_content(icon_config, content = nil, &block)
46
+ icon_html = if icon_config
47
+ view.ui.icon(icon_config[:path], class: style(:icon, icon_config[:position]))
48
+ end
49
+
50
+ content_html = if block_given?
51
+ view.capture(&block)
52
+ else
53
+ content
54
+ end
55
+
56
+ # Check if we have actual content (not empty/nil)
57
+ has_content = content_html.present?
58
+
59
+ if icon_config && has_content
60
+ # Both icon and content - wrap in flex container with gap
61
+ wrapper_class = "inline-flex items-center gap-1.5"
62
+
63
+ if icon_config[:position] == :end
64
+ view.content_tag(:span, class: wrapper_class) do
65
+ view.safe_join([content_html, icon_html].compact)
66
+ end
67
+ else
68
+ view.content_tag(:span, class: wrapper_class) do
69
+ view.safe_join([icon_html, content_html].compact)
70
+ end
71
+ end
72
+ elsif icon_config
73
+ # Icon only - no wrapper needed
74
+ icon_html
75
+ else
76
+ # Content only
77
+ content_html
78
+ end
11
79
  end
12
80
 
13
81
  register_styles :default do
14
82
  {
15
83
  root: "hover:cursor-pointer text-sm",
16
84
  outlined: {
17
- root: "inline-flex border items-center justify-center px-2 py-1 rounded-md font-medium focus:outline-none focus:ring-2 focus:ring-offset-2",
85
+ root: "inline-flex border items-center justify-center px-2 py-1 rounded-md font-medium focus:outline-none",
18
86
  colors: {
19
- default: "bg-white text-default-700 border-default-700 hover:bg-default-50",
20
- primary: "bg-white text-primary-600 border-primary-600 hover:bg-primary-50",
21
- secondary: "bg-white text-secondary-600 border-secondary-600 hover:bg-secondary-50",
22
- success: "bg-white text-success-600 border-success-600 hover:bg-success-50",
23
- danger: "bg-white text-danger-600 border-danger-600 hover:bg-danger-50",
24
- warning: "bg-white text-warning-600 border-warning-600 hover:bg-warning-50",
25
- info: "bg-white text-info-600 border-info-600 hover:bg-info-50"
87
+ default: "bg-white text-default-700 border-default-700 hover:bg-default-50 active:bg-default-100",
88
+ primary: "bg-white text-primary-600 border-primary-600 hover:bg-primary-50 active:bg-primary-100",
89
+ secondary: "bg-white text-secondary-600 border-secondary-600 hover:bg-secondary-50 active:bg-secondary-100",
90
+ success: "bg-white text-success-600 border-success-600 hover:bg-success-50 active:bg-success-100",
91
+ danger: "bg-white text-danger-600 border-danger-600 hover:bg-danger-50 active:bg-danger-100",
92
+ warning: "bg-white text-warning-600 border-warning-600 hover:bg-warning-50 active:bg-warning-100",
93
+ info: "bg-white text-info-600 border-info-600 hover:bg-info-50 active:bg-info-100"
26
94
  }
27
95
  },
28
96
  contained: {
29
- root: "inline-flex border items-center justify-center px-2 py-1 rounded-md font-medium focus:outline-none focus:ring-2 focus:ring-offset-2",
97
+ root: "inline-flex border items-center justify-center px-2 py-1 rounded-md font-medium focus:outline-none",
30
98
  colors: {
31
- default: "border-default-700 bg-default-600 text-white hover:bg-default-700",
32
- primary: "border-primary-700 bg-primary-600 text-white hover:bg-primary-700",
33
- secondary: "border-secondary-700 bg-secondary-600 text-white hover:bg-secondary-700",
34
- success: "border-success-700 bg-success-600 text-white hover:bg-success-700",
35
- danger: "border-danger-700 bg-danger-600 text-white hover:bg-danger-700",
36
- warning: "border-warning-700 bg-warning-600 text-white hover:bg-warning-700",
37
- info: "border-info-700 bg-info-600 text-white hover:bg-info-700"
99
+ default: "border-default-700 bg-default-600 text-white hover:bg-default-700 active:bg-default-500",
100
+ primary: "border-primary-700 bg-primary-600 text-white hover:bg-primary-700 active:bg-primary-500",
101
+ secondary: "border-secondary-700 bg-secondary-600 text-white hover:bg-secondary-700 active:bg-secondary-500",
102
+ success: "border-success-700 bg-success-600 text-white hover:bg-success-700 active:bg-success-500",
103
+ danger: "border-danger-700 bg-danger-600 text-white hover:bg-danger-700 active:bg-danger-500",
104
+ warning: "border-warning-700 bg-warning-600 text-white hover:bg-warning-700 active:bg-warning-500",
105
+ info: "border-info-700 bg-info-600 text-white hover:bg-info-700 active:bg-info-500"
38
106
  }
39
107
  },
40
108
  text: {
@@ -48,6 +116,10 @@ module OkonomiUiKit
48
116
  warning: "text-warning-600 hover:underline",
49
117
  info: "text-info-600 hover:underline"
50
118
  }
119
+ },
120
+ icon: {
121
+ start: "size-3.5",
122
+ end: "size-3.5"
51
123
  }
52
124
  }
53
125
  end
@@ -2,22 +2,28 @@ module OkonomiUiKit
2
2
  module Components
3
3
  class ButtonTag < OkonomiUiKit::Components::ButtonBase
4
4
  def render(name = nil, options = {}, &block)
5
- options, name = options, block if block_given?
5
+ # Handle different parameter patterns
6
+ if name.is_a?(Hash) && options.empty?
7
+ # Called as button_tag(options) with block
8
+ options = name
9
+ name = nil
10
+ end
6
11
 
7
12
  options ||= {}
8
13
  options = options.with_indifferent_access
9
14
 
10
- variant = (options.delete(:variant) || 'contained').to_sym
11
- color = (options.delete(:color) || 'default').to_sym
15
+ variant = (options.delete(:variant) || "contained").to_sym
16
+ color = (options.delete(:color) || "default").to_sym
17
+
18
+ # Extract icon configuration
19
+ icon_config, options = extract_icon_config(options)
12
20
 
13
21
  options[:class] = build_button_class(variant: variant, color: color, classes: options[:class])
14
22
 
15
- if block_given?
16
- view.button_tag(options, &block)
17
- else
18
- view.button_tag(name, options)
23
+ view.button_tag(options) do
24
+ render_button_content(icon_config, name, &block)
19
25
  end
20
26
  end
21
27
  end
22
28
  end
23
- end
29
+ end
@@ -7,17 +7,18 @@ module OkonomiUiKit
7
7
  html_options ||= {}
8
8
  html_options = html_options.with_indifferent_access
9
9
 
10
- variant = (html_options.delete(:variant) || 'contained').to_sym
11
- color = (html_options.delete(:color) || 'default').to_sym
10
+ variant = (html_options.delete(:variant) || "contained").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)
12
15
 
13
16
  html_options[:class] = build_button_class(variant: variant, color: color, classes: html_options[:class])
14
17
 
15
- if block_given?
16
- view.button_to(options, html_options, &block)
17
- else
18
- view.button_to(name, options, html_options)
18
+ view.button_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
@@ -8,61 +8,65 @@ module OkonomiUiKit
8
8
 
9
9
  # Extract component-specific options
10
10
  language = options.delete(:language) || options.delete(:lang)
11
- variant = (options.delete(:variant) || 'default').to_sym
12
- size = (options.delete(:size) || 'default').to_sym
11
+ variant = (options.delete(:variant) || "default").to_sym
12
+ size = (options.delete(:size) || "default").to_sym
13
13
  wrap = options.delete(:wrap) != false # Default to true
14
14
 
15
15
  # Build classes
16
16
  classes = build_classes(variant: variant, size: size, wrap: wrap, custom_class: options.delete(:class))
17
17
 
18
18
  # Escape HTML entities in content
19
- escaped_content = if block_given?
20
- view.capture(&block)
21
- elsif content
22
- content
23
- else
24
- ""
25
- end
19
+ raw_content = if block_given?
20
+ view.capture(&block)
21
+ elsif content
22
+ content
23
+ else
24
+ ""
25
+ end
26
+
27
+ escaped_content = html_escape(raw_content)
26
28
 
27
29
  view.render(
28
30
  template_path,
29
- content: escaped_content.strip.html_safe,
31
+ content: escaped_content,
30
32
  options: options,
31
33
  classes: classes,
32
34
  language: language
33
35
  )
34
36
  end
35
37
 
38
+ register_styles :default do
39
+ {
40
+ base: "",
41
+ variants: {
42
+ default: "bg-gray-900 text-gray-100 p-4 rounded-lg",
43
+ inline: "bg-gray-100 text-gray-900 px-1 py-0.5 rounded text-sm font-mono",
44
+ minimal: "bg-gray-900 text-gray-100 p-3 rounded text-xs"
45
+ },
46
+ sizes: {
47
+ xs: "text-xs",
48
+ sm: "text-sm",
49
+ default: "text-sm",
50
+ lg: "text-base"
51
+ },
52
+ wrap: {
53
+ true: "overflow-x-auto",
54
+ false: "overflow-hidden"
55
+ }
56
+ }
57
+ end
58
+
36
59
  private
37
60
 
38
61
  def build_classes(variant:, size:, wrap:, custom_class: nil)
39
- base_classes = theme.dig(:components, :code, :base) || "bg-gray-900 text-gray-100 rounded-lg"
40
-
41
- variant_classes = case variant
42
- when :inline
43
- "bg-gray-100 text-gray-900 px-1 py-0.5 rounded text-sm font-mono"
44
- when :minimal
45
- "bg-gray-900 text-gray-100 p-3 rounded text-xs"
46
- else
47
- # :default
48
- "bg-gray-900 text-gray-100 p-4 rounded-lg"
49
- end
50
-
51
- size_classes = case size
52
- when :xs
53
- "text-xs"
54
- when :sm
55
- "text-sm"
56
- when :lg
57
- "text-base"
58
- else
59
- # :default
60
- "text-sm"
61
- end
62
-
63
- wrap_classes = wrap ? "overflow-x-auto" : "overflow-hidden"
62
+ base_classes = style(:base) || ""
63
+ variant_classes = style(:variants, variant) || ""
64
+ size_classes = style(:sizes, size) || ""
65
+ # Convert boolean wrap to symbol for hash access
66
+ wrap_key = wrap ? :true : :false
67
+ wrap_classes = style(:wrap, wrap_key) || ""
64
68
 
65
- [base_classes, variant_classes, size_classes, wrap_classes, custom_class].compact.join(' ')
69
+ [ base_classes, variant_classes, size_classes, wrap_classes, custom_class ].reject(&:blank?).join(" ")
66
70
  end
67
71
 
68
72
  def html_escape(content)
@@ -70,4 +74,4 @@ module OkonomiUiKit
70
74
  end
71
75
  end
72
76
  end
73
- end
77
+ end
@@ -0,0 +1,130 @@
1
+ module OkonomiUiKit
2
+ module Components
3
+ class ConfirmationModal < OkonomiUiKit::Component
4
+ def render(options = {}, &block)
5
+ options = options.with_indifferent_access
6
+
7
+ # Extract and validate required options
8
+ title = options.fetch(:title) { raise ArgumentError, "title is required" }
9
+ message = options.fetch(:message) { raise ArgumentError, "message is required" }
10
+
11
+ # Extract optional parameters with defaults
12
+ confirm_text = options.delete(:confirm_text) || "Confirm"
13
+ cancel_text = options.delete(:cancel_text) || "Cancel"
14
+ variant = (options.delete(:variant) || :warning).to_sym
15
+ size = (options.delete(:size) || :md).to_sym
16
+ auto_open = options.delete(:auto_open) || false
17
+
18
+ # Build component options
19
+ modal_options = {
20
+ title: title,
21
+ message: message,
22
+ confirm_text: confirm_text,
23
+ cancel_text: cancel_text,
24
+ variant: variant,
25
+ size: size,
26
+ auto_open: auto_open,
27
+ has_custom_actions: block_given?,
28
+ data: options.delete(:data) || {}
29
+ }.merge(options)
30
+
31
+ view.render(template_path, component: self, options: modal_options, &block)
32
+ end
33
+
34
+ # Register default styles for the confirmation modal
35
+ register_styles :default do
36
+ {
37
+ # Modal container and backdrop
38
+ backdrop: "fixed inset-0 bg-gray-500/75 transition-opacity duration-300 ease-out opacity-0",
39
+ container: "fixed inset-0 z-10 w-screen overflow-y-auto",
40
+ wrapper: "flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0",
41
+
42
+ # Modal panel
43
+ panel: {
44
+ base: "relative transform overflow-hidden rounded-lg bg-white px-4 pt-5 pb-4 text-left shadow-xl transition-all duration-300 ease-out sm:my-8 sm:w-full sm:p-6 opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95",
45
+ sizes: {
46
+ sm: "sm:max-w-sm",
47
+ md: "sm:max-w-lg",
48
+ lg: "sm:max-w-2xl",
49
+ xl: "sm:max-w-4xl"
50
+ }
51
+ },
52
+
53
+ # Close button
54
+ close_button: {
55
+ wrapper: "absolute top-0 right-0 hidden pt-4 pr-4 sm:block",
56
+ button: "rounded-md bg-white text-gray-400 hover:text-gray-500 focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:outline-none",
57
+ icon: {
58
+ file: "heroicons/outline/x-mark",
59
+ class: "size-6"
60
+ }
61
+ },
62
+
63
+ # Icon configuration
64
+ icon: {
65
+ wrapper: "mx-auto flex size-12 shrink-0 items-center justify-center rounded-full sm:mx-0 sm:size-10",
66
+ class: "size-6",
67
+ variants: {
68
+ warning: {
69
+ wrapper: "bg-red-100",
70
+ icon: "text-red-600",
71
+ file: "heroicons/outline/exclamation-triangle"
72
+ },
73
+ info: {
74
+ wrapper: "bg-blue-100",
75
+ icon: "text-blue-600",
76
+ file: "heroicons/outline/information-circle"
77
+ },
78
+ success: {
79
+ wrapper: "bg-green-100",
80
+ icon: "text-green-600",
81
+ file: "heroicons/outline/check-circle"
82
+ }
83
+ }
84
+ },
85
+
86
+ # Content styling
87
+ content: {
88
+ wrapper: "sm:flex sm:items-start",
89
+ text_wrapper: "mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left",
90
+ title: "text-base font-semibold text-gray-900",
91
+ message: "mt-2 text-sm text-gray-500"
92
+ },
93
+
94
+ # Actions container
95
+ actions: {
96
+ wrapper: "mt-5 sm:mt-4 sm:flex sm:flex-row-reverse"
97
+ }
98
+ }
99
+ end
100
+
101
+ # Helper methods to build classes from styles
102
+ def modal_panel_class(size)
103
+ [
104
+ style(:panel, :base),
105
+ style(:panel, :sizes, size)
106
+ ].compact.join(" ")
107
+ end
108
+
109
+ def modal_icon_wrapper_class(variant)
110
+ [
111
+ style(:icon, :wrapper),
112
+ style(:icon, :variants, variant, :wrapper)
113
+ ].compact.join(" ")
114
+ end
115
+
116
+ def modal_icon_class(variant)
117
+ [
118
+ style(:icon, :class),
119
+ style(:icon, :variants, variant, :icon)
120
+ ].compact.join(" ")
121
+ end
122
+
123
+ def modal_data_attributes(options)
124
+ return "" unless options[:data]
125
+
126
+ options[:data].map { |k, v| "data-#{k.to_s.dasherize}=\"#{v}\"" }.join(" ").html_safe
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,147 @@
1
+ module OkonomiUiKit
2
+ module Components
3
+ class DropdownButton < ButtonBase
4
+ def render(options = {}, &block)
5
+ raise ArgumentError, "DropdownButton requires a block" unless block_given?
6
+
7
+ options = options.with_indifferent_access
8
+ variant = (options.delete(:variant) || "contained").to_sym
9
+ color = (options.delete(:color) || "default").to_sym
10
+
11
+ base_button_classes = build_button_class(
12
+ variant: variant,
13
+ color: color,
14
+ classes: options.delete(:class)
15
+ )
16
+
17
+ menu_classes = [
18
+ style(:menu, :root),
19
+ options.delete(:menu_class)
20
+ ].compact.join(" ")
21
+
22
+ dropdown_builder = DropdownBuilder.new(view)
23
+
24
+ view.render(
25
+ template_path,
26
+ base_button_classes: base_button_classes,
27
+ menu_classes: menu_classes,
28
+ dropdown_builder: dropdown_builder,
29
+ component: self,
30
+ options: options,
31
+ &block
32
+ )
33
+ end
34
+
35
+ register_styles :default do
36
+ {
37
+ primary: {
38
+ icon: "mr-1.5 size-3.5",
39
+ chevron: "size-3.5"
40
+ },
41
+ menu: {
42
+ root: "absolute right-0 z-10 mt-2 w-56 origin-top-right rounded-md bg-white shadow-lg ring-1 ring-gray-200 focus:outline-none",
43
+ divider: "h-0 my-1 border-t border-gray-200",
44
+ item: {
45
+ root: "hover:cursor-pointer block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 active:bg-gray-100 hover:text-gray-900",
46
+ icon: "mr-3 h-5 w-5 text-gray-400",
47
+ label: "flex items-center"
48
+ }
49
+ }
50
+ }
51
+ end
52
+
53
+ class DropdownBuilder
54
+ attr_reader :view, :items, :is_first
55
+
56
+ def initialize(view)
57
+ @view = view
58
+ @items = []
59
+ @is_first = true
60
+ end
61
+
62
+ def link_to(name = nil, options = nil, html_options = nil, &block)
63
+ # Handle icon extraction
64
+ if html_options.is_a?(Hash)
65
+ icon = html_options.delete(:icon)
66
+ else
67
+ icon = nil
68
+ end
69
+
70
+ item = {
71
+ type: :link,
72
+ name: name,
73
+ options: options,
74
+ html_options: html_options || {},
75
+ block: block,
76
+ is_first: @is_first,
77
+ icon: icon
78
+ }
79
+ @items << item
80
+ @is_first = false
81
+ end
82
+
83
+ def button_to(name = nil, options = {}, html_options = {}, &block)
84
+ # Handle icon extraction
85
+ if html_options.is_a?(Hash)
86
+ icon = html_options.delete(:icon)
87
+ else
88
+ icon = nil
89
+ end
90
+
91
+ item = {
92
+ type: :button,
93
+ name: name,
94
+ options: options,
95
+ html_options: html_options,
96
+ block: block,
97
+ is_first: @is_first,
98
+ icon: icon
99
+ }
100
+ @items << item
101
+ @is_first = false
102
+ end
103
+
104
+ def button_tag(content_or_options = nil, options = nil, &block)
105
+ # Handle the different argument patterns for button_tag
106
+ if content_or_options.is_a?(Hash)
107
+ options = content_or_options
108
+ content = nil
109
+ else
110
+ content = content_or_options
111
+ end
112
+
113
+ options ||= {}
114
+
115
+ # Handle icon extraction
116
+ icon = options.delete(:icon) if options.is_a?(Hash)
117
+
118
+ # Ensure type is button for button_tag
119
+ options[:type] ||= "button"
120
+
121
+ item = {
122
+ type: :button_tag,
123
+ name: content,
124
+ options: options,
125
+ block: block,
126
+ is_first: @is_first,
127
+ icon: icon
128
+ }
129
+ @items << item
130
+ @is_first = false
131
+ end
132
+
133
+ def divider
134
+ @items << { type: :divider }
135
+ end
136
+
137
+ def primary_item
138
+ @items.find { |item| item[:is_first] && item[:type] != :divider }
139
+ end
140
+
141
+ def menu_items
142
+ @items
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,38 @@
1
+ module OkonomiUiKit
2
+ module Components
3
+ module Forms
4
+ class CheckBoxWithLabel < OkonomiUiKit::FormComponent
5
+ def render(form, method, options = {}, checked_value = true, unchecked_value = false)
6
+ options = options.with_indifferent_access
7
+
8
+ view.content_tag(:div, class: style(:wrapper)) do
9
+ view.concat form.check_box(
10
+ method,
11
+ {
12
+ class: style(:input, :root)
13
+ }.merge(options || {}),
14
+ checked_value,
15
+ unchecked_value
16
+ )
17
+ view.concat view.render(template_path, component: self, method: method, options: options, form: form)
18
+ end
19
+ end
20
+
21
+ register_styles :default do
22
+ {
23
+ wrapper: "flex gap-x-3",
24
+ input: {
25
+ root: "size-4 rounded border-gray-300 text-primary-600 focus:ring-primary-600"
26
+ },
27
+ label: {
28
+ root: "text-sm/6 font-medium text-gray-900"
29
+ },
30
+ hint: {
31
+ root: "text-gray-500"
32
+ }
33
+ }
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end