okonomi_ui_kit 0.1.7 → 0.1.9

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 (77) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +165 -7
  3. data/app/assets/builds/okonomi_ui_kit/application.tailwind.css +664 -187
  4. data/app/helpers/okonomi_ui_kit/application_helper.rb +8 -0
  5. data/app/helpers/okonomi_ui_kit/attribute_section_helper.rb +5 -5
  6. data/app/helpers/okonomi_ui_kit/component.rb +44 -21
  7. data/app/helpers/okonomi_ui_kit/components/alert.rb +1 -1
  8. data/app/helpers/okonomi_ui_kit/components/badge.rb +5 -5
  9. data/app/helpers/okonomi_ui_kit/components/breadcrumbs.rb +69 -0
  10. data/app/helpers/okonomi_ui_kit/components/button_base.rb +56 -0
  11. data/app/helpers/okonomi_ui_kit/components/button_tag.rb +23 -0
  12. data/app/helpers/okonomi_ui_kit/components/button_to.rb +4 -15
  13. data/app/helpers/okonomi_ui_kit/components/code.rb +41 -37
  14. data/app/helpers/okonomi_ui_kit/components/confirmation_modal.rb +130 -0
  15. data/app/helpers/okonomi_ui_kit/components/forms/check_box_with_label.rb +38 -0
  16. data/app/helpers/okonomi_ui_kit/components/forms/collection_select.rb +57 -0
  17. data/app/helpers/okonomi_ui_kit/components/forms/date_field.rb +9 -0
  18. data/app/helpers/okonomi_ui_kit/components/forms/datetime_local_field.rb +9 -0
  19. data/app/helpers/okonomi_ui_kit/components/forms/email_field.rb +9 -0
  20. data/app/helpers/okonomi_ui_kit/components/forms/field.rb +24 -0
  21. data/app/helpers/okonomi_ui_kit/components/forms/field_set.rb +17 -0
  22. data/app/helpers/okonomi_ui_kit/components/forms/input_base.rb +57 -0
  23. data/app/helpers/okonomi_ui_kit/components/forms/label.rb +27 -0
  24. data/app/helpers/okonomi_ui_kit/components/forms/multi_select.rb +18 -0
  25. data/app/helpers/okonomi_ui_kit/components/forms/number_field.rb +9 -0
  26. data/app/helpers/okonomi_ui_kit/components/forms/password_field.rb +9 -0
  27. data/app/helpers/okonomi_ui_kit/components/forms/search_field.rb +9 -0
  28. data/app/helpers/okonomi_ui_kit/components/forms/select.rb +57 -0
  29. data/app/helpers/okonomi_ui_kit/components/forms/show_if.rb +28 -0
  30. data/app/helpers/okonomi_ui_kit/components/forms/telephone_field.rb +9 -0
  31. data/app/helpers/okonomi_ui_kit/components/forms/text_area.rb +9 -0
  32. data/app/helpers/okonomi_ui_kit/components/forms/text_field.rb +9 -0
  33. data/app/helpers/okonomi_ui_kit/components/forms/time_field.rb +9 -0
  34. data/app/helpers/okonomi_ui_kit/components/forms/upload_field.rb +25 -0
  35. data/app/helpers/okonomi_ui_kit/components/forms/url_field.rb +9 -0
  36. data/app/helpers/okonomi_ui_kit/components/forms.rb +6 -0
  37. data/app/helpers/okonomi_ui_kit/components/icon.rb +36 -0
  38. data/app/helpers/okonomi_ui_kit/components/link_to.rb +8 -19
  39. data/app/helpers/okonomi_ui_kit/components/navigation.rb +98 -0
  40. data/app/helpers/okonomi_ui_kit/components/page.rb +8 -8
  41. data/app/helpers/okonomi_ui_kit/components/table.rb +9 -10
  42. data/app/helpers/okonomi_ui_kit/components/typography.rb +16 -16
  43. data/app/helpers/okonomi_ui_kit/components.rb +4 -0
  44. data/app/helpers/okonomi_ui_kit/config.rb +5 -1
  45. data/app/helpers/okonomi_ui_kit/configs.rb +4 -0
  46. data/app/helpers/okonomi_ui_kit/form_builder.rb +39 -130
  47. data/app/helpers/okonomi_ui_kit/form_component.rb +7 -0
  48. data/app/helpers/okonomi_ui_kit/svg_icons.rb +5 -5
  49. data/app/helpers/okonomi_ui_kit/t_w_merge.rb +114 -0
  50. data/app/helpers/okonomi_ui_kit/ui_helper.rb +17 -58
  51. data/app/views/okonomi/components/breadcrumbs/_breadcrumbs.html.erb +46 -0
  52. data/app/views/okonomi/components/confirmation_modal/_confirmation_modal.html.erb +76 -0
  53. data/app/views/okonomi/components/forms/check_box_with_label/_check_box_with_label.html.erb +6 -0
  54. data/app/views/okonomi/components/forms/field/_field.html.erb +3 -0
  55. data/app/views/okonomi/components/forms/field_set/_field_set.html.erb +3 -0
  56. data/app/views/okonomi/components/forms/upload_field/_upload_field.html.erb +1 -0
  57. data/app/views/okonomi/components/icon/_icon.html.erb +38 -0
  58. data/app/views/okonomi/components/navigation/_link.html.erb +18 -0
  59. data/app/views/okonomi/components/navigation/_navigation.html.erb +4 -0
  60. data/app/views/okonomi/forms/tailwind/_checkbox_label.html.erb +2 -2
  61. data/app/views/okonomi/forms/tailwind/_field.html.erb +6 -6
  62. data/app/views/okonomi/forms/tailwind/_multi_select.html.erb +2 -4
  63. data/app/views/okonomi/forms/tailwind/_upload_field.html.erb +10 -10
  64. data/config/importmap.rb +1 -1
  65. data/lib/okonomi_ui_kit/engine.rb +0 -3
  66. data/lib/okonomi_ui_kit/version.rb +1 -1
  67. metadata +43 -12
  68. data/app/helpers/okonomi_ui_kit/breadcrumbs_helper.rb +0 -60
  69. data/app/helpers/okonomi_ui_kit/icon_helper.rb +0 -39
  70. data/app/helpers/okonomi_ui_kit/navigation_helper.rb +0 -72
  71. data/app/helpers/okonomi_ui_kit/theme.rb +0 -159
  72. data/app/helpers/okonomi_ui_kit/theme_helper.rb +0 -17
  73. data/app/views/okonomi/forms/tailwind/_field_set.html.erb +0 -3
  74. data/app/views/okonomi/modals/_confirmation_modal.html.erb +0 -77
  75. data/app/views/okonomi/navigation/_link.html.erb +0 -15
  76. data/app/views/okonomi/navigation/_menu.html.erb +0 -3
  77. data/app/views/okonomi/navigation/_navbar.html.erb +0 -105
@@ -35,7 +35,7 @@ module OkonomiUiKit
35
35
  def render_content
36
36
  @template.safe_join(@content_parts)
37
37
  end
38
-
38
+
39
39
  def to_s
40
40
  render_content
41
41
  end
@@ -62,7 +62,7 @@ module OkonomiUiKit
62
62
  end
63
63
 
64
64
  def breadcrumbs(&block)
65
- @breadcrumbs_content = @template.breadcrumbs(&block)
65
+ @breadcrumbs_content = @template.ui.breadcrumbs(&block)
66
66
  end
67
67
 
68
68
  def row(&block)
@@ -163,7 +163,7 @@ module OkonomiUiKit
163
163
  if block_given?
164
164
  # Capture the content first to see if attributes were used
165
165
  content = capture { yield(self) }
166
-
166
+
167
167
  @body_content = if @attributes.any?
168
168
  # If attributes were added, wrap them in dl
169
169
  tag.div do
@@ -192,10 +192,10 @@ module OkonomiUiKit
192
192
  attribute_html = tag.div(class: "py-6 sm:grid sm:grid-cols-3 sm:gap-4") do
193
193
  dt_content = tag.dt(label, class: "text-sm font-medium text-gray-900")
194
194
  dd_content = tag.dd(content, class: "mt-1 text-sm/6 text-gray-700 sm:col-span-2 sm:mt-0")
195
-
195
+
196
196
  dt_content + dd_content
197
197
  end
198
-
198
+
199
199
  @attributes << attribute_html
200
200
  end
201
201
 
@@ -213,7 +213,7 @@ module OkonomiUiKit
213
213
 
214
214
  def build_header
215
215
  return nil unless @title_content || @subtitle_content || @actions_content
216
-
216
+
217
217
  tag.div(class: "py-6") do
218
218
  if @actions_content
219
219
  tag.div(class: "flex w-full justify-between items-start") do
@@ -223,7 +223,7 @@ module OkonomiUiKit
223
223
  content_parts << @subtitle_content if @subtitle_content
224
224
  @template.safe_join(content_parts.compact)
225
225
  end
226
-
226
+
227
227
  title_section + @actions_content
228
228
  end
229
229
  else
@@ -244,4 +244,4 @@ module OkonomiUiKit
244
244
  end
245
245
  end
246
246
  end
247
- end
247
+ end
@@ -4,8 +4,8 @@ module OkonomiUiKit
4
4
  def render(options = {}, &block)
5
5
  options = options.with_indifferent_access
6
6
  variant = (options.delete(:variant) || :default).to_sym
7
-
8
- builder = TableBuilder.new(view, theme, self, variant)
7
+
8
+ builder = TableBuilder.new(view, self, variant)
9
9
  view.render(template_path, builder: builder, options: options, &block)
10
10
  end
11
11
 
@@ -48,9 +48,8 @@ module OkonomiUiKit
48
48
  include ActionView::Helpers::TagHelper
49
49
  include ActionView::Helpers::CaptureHelper
50
50
 
51
- def initialize(template, theme, style_provider, variant = :default)
51
+ def initialize(template, style_provider, variant = :default)
52
52
  @template = template
53
- @theme = theme
54
53
  @style_provider = style_provider
55
54
  @variant = variant
56
55
  @current_row_cells = []
@@ -127,8 +126,8 @@ module OkonomiUiKit
127
126
  capture(&block)
128
127
  else
129
128
  tag.div(class: style(:empty_state, :wrapper)) do
130
- icon_content = if @template.respond_to?(:svg_icon)
131
- @template.svg_icon(icon, class: style(:empty_state, :icon))
129
+ icon_content = if @template.respond_to?(:ui)
130
+ @template.ui.icon(icon, class: style(:empty_state, :icon))
132
131
  else
133
132
  tag.div(class: style(:empty_state, :icon))
134
133
  end
@@ -157,7 +156,7 @@ module OkonomiUiKit
157
156
 
158
157
  def render_th(cell, is_first, is_last)
159
158
  align_class = style(:alignment, cell[:align]) || style(:alignment, :left)
160
-
159
+
161
160
  position_class = if is_first
162
161
  style(:th, :first)
163
162
  elsif is_last
@@ -171,7 +170,7 @@ module OkonomiUiKit
171
170
  position_class,
172
171
  align_class,
173
172
  cell[:options][:class]
174
- ].compact.join(' ')
173
+ ].compact.join(" ")
175
174
 
176
175
  options = cell[:options].except(:class)
177
176
  tag.th(cell[:content], scope: cell[:scope], class: classes, **options)
@@ -179,7 +178,7 @@ module OkonomiUiKit
179
178
 
180
179
  def render_td(cell, is_first, is_last)
181
180
  align_class = style(:alignment, cell[:align]) || style(:alignment, :left)
182
-
181
+
183
182
  position_class = if is_first
184
183
  style(:td, :first)
185
184
  elsif is_last
@@ -193,7 +192,7 @@ module OkonomiUiKit
193
192
  position_class,
194
193
  align_class,
195
194
  cell[:options][:class]
196
- ].compact.join(' ')
195
+ ].compact.join(" ")
197
196
 
198
197
  options = cell[:options].except(:class)
199
198
  tag.td(cell[:content], class: classes, **options)
@@ -2,14 +2,14 @@ module OkonomiUiKit
2
2
  module Components
3
3
  class Typography < OkonomiUiKit::Component
4
4
  TYPOGRAPHY_COMPONENTS = {
5
- body1: 'p',
6
- body2: 'p',
7
- h1: 'h1',
8
- h2: 'h2',
9
- h3: 'h3',
10
- h4: 'h4',
11
- h5: 'h5',
12
- h6: 'h6',
5
+ body1: "p",
6
+ body2: "p",
7
+ h1: "h1",
8
+ h2: "h2",
9
+ h3: "h3",
10
+ h4: "h4",
11
+ h5: "h5",
12
+ h6: "h6"
13
13
  }.freeze
14
14
 
15
15
  def render(text = nil, options = {}, &block)
@@ -17,15 +17,15 @@ module OkonomiUiKit
17
17
  options ||= {}
18
18
  options = options.with_indifferent_access
19
19
 
20
- variant = (options.delete(:variant) || 'body1').to_sym
21
- component = (TYPOGRAPHY_COMPONENTS[variant] || 'span').to_s
22
- color = (options.delete(:color) || 'default').to_sym
20
+ variant = (options.delete(:variant) || "body1").to_sym
21
+ component = (TYPOGRAPHY_COMPONENTS[variant] || "span").to_s
22
+ color = (options.delete(:color) || "default").to_sym
23
23
 
24
24
  classes = [
25
- style(:variants, variant) || '',
26
- style(:colors, color) || '',
27
- options.delete(:class) || ''
28
- ].reject(&:blank?).join(' ')
25
+ style(:variants, variant) || "",
26
+ style(:colors, color) || "",
27
+ options.delete(:class) || ""
28
+ ].reject(&:blank?).join(" ")
29
29
 
30
30
  view.render(
31
31
  template_path,
@@ -65,4 +65,4 @@ module OkonomiUiKit
65
65
  end
66
66
  end
67
67
  end
68
- end
68
+ end
@@ -0,0 +1,4 @@
1
+ module OkonomiUiKit
2
+ module Components
3
+ end
4
+ end
@@ -6,11 +6,15 @@ module OkonomiUiKit
6
6
  raise ArgumentError, "Styles must be a Hash" unless styles.is_a?(Hash)
7
7
 
8
8
  styles_registry[theme] ||= {}
9
- styles_registry[theme] = styles_registry[theme].deep_merge(styles)
9
+ styles_registry[theme] = deep_merge({}, styles_registry[theme], styles)
10
10
  end
11
11
 
12
12
  def self.styles_registry
13
13
  @styles_registry ||= {}
14
14
  end
15
+
16
+ def self.deep_merge(*hashes)
17
+ OkonomiUiKit::TWMerge.deep_merge_all(*hashes)
18
+ end
15
19
  end
16
20
  end
@@ -0,0 +1,4 @@
1
+ module OkonomiUiKit
2
+ module Configs
3
+ end
4
+ end
@@ -7,202 +7,111 @@ module OkonomiUiKit
7
7
  end
8
8
 
9
9
  def field_set(options = {}, &block)
10
- @template.render('okonomi/forms/tailwind/field_set', options:, form: self, &block)
10
+ ui.forms.field_set(self, options, &block)
11
11
  end
12
12
 
13
13
  def field(field_id = nil, options = {}, &block)
14
- @template.render('okonomi/forms/tailwind/field', field_id:, options:, form: self, &block)
14
+ ui.forms.field(self, field_id, options, &block)
15
15
  end
16
16
 
17
17
  def upload_field(method, options = {})
18
- @template.render('okonomi/forms/tailwind/upload_field', method:, options:, form: self)
18
+ ui.forms.upload_field(self, method, options)
19
19
  end
20
20
 
21
21
  def text_field(method, options = {})
22
- css = input_field_classes(method, :text, options)
23
- super(method, { autocomplete: "off" }.merge(options).merge(class: css))
22
+ super(*resolve_arguments(:text_field, method, options))
24
23
  end
25
24
 
26
25
  def email_field(method, options = {})
27
- css = input_field_classes(method, :email, options)
28
- super(method, options.merge(class: css))
26
+ super(*resolve_arguments(:email_field, method, options))
29
27
  end
30
28
 
31
29
  def url_field(method, options = {})
32
- css = input_field_classes(method, :url, options)
33
- super(method, options.merge(class: css))
30
+ super(*resolve_arguments(:url_field, method, options))
31
+ end
32
+
33
+ def resolve_arguments(component_name, *args)
34
+ component = ui.forms.resolve_component(component_name)
35
+
36
+ if component.nil?
37
+ args
38
+ else
39
+ component.render_arguments(object, *args)
40
+ end
34
41
  end
35
42
 
36
43
  def password_field(method, options = {})
37
- css = input_field_classes(method, :password, options)
38
- super(method, options.merge(class: css))
44
+ super(*resolve_arguments(:password_field, method, options))
39
45
  end
40
46
 
41
47
  def number_field(method, options = {})
42
- css = input_field_classes(method, :number, options)
43
- super(method, options.merge(class: css))
48
+ super(*resolve_arguments(:number_field, method, options))
44
49
  end
45
50
 
46
51
  def telephone_field(method, options = {})
47
- css = input_field_classes(method, :telephone_field, options)
48
- super(method, options.merge(class: css))
52
+ super(*resolve_arguments(:telephone_field, method, options))
49
53
  end
50
54
 
51
55
  def search_field(method, options = {})
52
- css = input_field_classes(method, :search, options)
53
- super(method, options.merge(class: css))
56
+ super(*resolve_arguments(:search_field, method, options))
54
57
  end
55
58
 
56
59
  def date_field(method, options = {})
57
- css = input_field_classes(method, :date, options)
58
- super(method, options.merge(class: css))
60
+ super(*resolve_arguments(:date_field, method, options))
59
61
  end
60
62
 
61
63
  def datetime_local_field(method, options = {})
62
- css = input_field_classes(method, :datetime_local, options)
63
- super(method, options.merge(class: css))
64
+ super(*resolve_arguments(:datetime_local_field, method, options))
64
65
  end
65
66
 
66
67
  def time_field(method, options = {})
67
- css = input_field_classes(method, :time, options)
68
- super(method, options.merge(class: css))
68
+ super(*resolve_arguments(:time_field, method, options))
69
69
  end
70
70
 
71
71
  def text_area(method, options = {})
72
- css = input_field_classes(method, :textarea, options, include_disabled: false)
73
- super(method, options.merge(class: css))
72
+ super(*resolve_arguments(:text_area, method, options))
74
73
  end
75
74
 
76
75
  def select(method, choices = nil, options = {}, html_options = {}, &block)
77
- css = [
78
- ui.get_theme.dig(:components, :select, :root),
79
- when_errors(
80
- method,
81
- ui.get_theme.dig(:components, :select, :error),
82
- ui.get_theme.dig(:components, :select, :valid)
83
- ),
84
- html_options[:class]
85
- ].compact.join(' ').split(' ').uniq
86
-
87
- select_html = super(method, choices, options, html_options.merge(class: css), &block)
88
- icon_html = @template.svg_icon(
89
- ui.get_theme.dig(:components, :select, :icon, :file),
90
- class: ui.get_theme.dig(:components, :select, :icon, :class)
91
- )
92
-
93
- @template.content_tag(:div, class: ui.get_theme.dig(:components, :select, :wrapper)) do
94
- @template.concat(select_html)
95
- @template.concat(icon_html)
96
- end
76
+ ui.forms.select(self, method, choices, options, html_options, &block)
97
77
  end
98
78
 
99
79
  def collection_select(method, collection, value_method, text_method, options = {}, html_options = {})
100
- css = [
101
- ui.get_theme.dig(:components, :select, :root),
102
- when_errors(
103
- method,
104
- ui.get_theme.dig(:components, :select, :error),
105
- ui.get_theme.dig(:components, :select, :valid)
106
- ),
107
- html_options[:class]
108
- ].compact.join(' ').split(' ').uniq
109
-
110
- select_html = super(method, collection, value_method, text_method, options, html_options.merge(class: css))
111
- icon_html = @template.svg_icon(
112
- ui.get_theme.dig(:components, :select, :icon, :file),
113
- class: ui.get_theme.dig(:components, :select, :icon, :class)
114
- )
115
-
116
- @template.content_tag(:div, class: ui.get_theme.dig(:components, :select, :wrapper)) do
117
- @template.concat(select_html)
118
- @template.concat(icon_html)
119
- end
80
+ ui.forms.collection_select(self, method, collection, value_method, text_method, options, html_options)
120
81
  end
121
82
 
122
83
  def multi_select(method, **options)
123
- @template.render(
124
- partial: 'okonomi/forms/tailwind/multi_select',
125
- locals: {
126
- form: self,
127
- method: method,
128
- options: options
129
- }
130
- )
84
+ ui.forms.multi_select(self, method, options)
131
85
  end
132
86
 
133
87
  def label(method, text = nil, options = {}, &block)
134
- base_classes = ui.get_theme.dig(:components, :label, :root)
135
- super(method, text, merge_class(options, base_classes), &block)
88
+ ui.forms.label(self, method, text, options, &block)
136
89
  end
137
90
 
138
91
  def submit(value = nil, options = {})
139
- variant = options.delete(:variant) || 'contained'
140
- color = options.delete(:color) || 'primary'
92
+ value, options = nil, value if value.is_a?(Hash)
93
+ value ||= submit_default_value
141
94
 
142
- base_classes = ui.button_class(variant:, color:)
143
- super(value, merge_class(options, base_classes))
95
+ options ||= {}
96
+ options[:type] = "submit"
97
+ options[:variant] ||= "contained"
98
+ options[:color] ||= "primary"
99
+
100
+ ui.button_tag(value, options)
144
101
  end
145
102
 
146
103
  def check_box_with_label(method, options = {}, checked_value = true, unchecked_value = false)
147
- @template.content_tag(:div, class: ui.get_theme.dig(:components, :checkbox, :wrapper)) do
148
- @template.concat check_box(
149
- method,
150
- {
151
- class: ui.get_theme.dig(:components, :checkbox, :input, :root)
152
- }.merge(options || {}),
153
- checked_value,
154
- unchecked_value
155
- )
156
- @template.concat @template.render('okonomi/forms/tailwind/checkbox_label', method:, options:, form: self)
157
- end
104
+ ui.forms.check_box_with_label(self, method, options, checked_value, unchecked_value)
158
105
  end
159
106
 
160
107
  def show_if(field:, equals:, &block)
161
- field_id = "#{object_name}_#{field}"
162
- @template.tag.div(
163
- class: "hidden",
164
- data: {
165
- controller: "form-field-visibility",
166
- "form-field-visibility-field-id-value": field_id,
167
- "form-field-visibility-equals-value": equals
168
- },
169
- &block
170
- )
108
+ ui.forms.show_if(self, field: field, equals: equals, &block)
171
109
  end
172
110
 
173
111
  private
174
112
 
175
- def input_field_classes(method, type, options, include_disabled: true)
176
- css_classes = [
177
- ui.get_theme.dig(:components, :input, :types, type, :root) || ui.get_theme.dig(:components, :input, :types, :text, :root),
178
- when_errors(
179
- method,
180
- ui.get_theme.dig(:components, :input, :types, type, :error) || ui.get_theme.dig(:components, :input, :types, :text, :error),
181
- ui.get_theme.dig(:components, :input, :types, type, :valid) || ui.get_theme.dig(:components, :input, :types, :text, :valid)
182
- ),
183
- options[:class]
184
- ]
185
-
186
- if include_disabled
187
- css_classes << (
188
- ui.get_theme.dig(:components, :input, :types, type, :disabled) || ui.get_theme.dig(:components, :input, :types, :text, :disabled)
189
- )
190
- end
191
-
192
- css_classes.compact.join(' ').split(' ').uniq
193
- end
194
-
195
- def when_errors(method, value, default_value = nil)
196
- key = method.to_s.gsub('_id', '').to_sym
197
- if object.errors.include?(key) || object.errors.include?(method)
198
- value
199
- else
200
- default_value
201
- end
202
- end
203
-
204
113
  def merge_class(options, new_class)
205
- options[:class] = [options[:class], new_class].compact.join(" ")
114
+ options[:class] = OkonomiUiKit::TWMerge.merge(options[:class] || "", new_class)
206
115
  options
207
116
  end
208
117
  end
@@ -0,0 +1,7 @@
1
+ module OkonomiUiKit
2
+ class FormComponent < Component
3
+ def self.namespace
4
+ OkonomiUiKit::Components::Forms
5
+ end
6
+ end
7
+ end
@@ -13,16 +13,16 @@ module OkonomiUiKit
13
13
 
14
14
  def self.file_path(name)
15
15
  # First check in the host application
16
- host_path = Rails.root.join('app', 'assets', 'images', "#{name}.svg")
16
+ host_path = Rails.root.join("app", "assets", "images", "#{name}.svg")
17
17
  return host_path if File.exist?(host_path)
18
-
18
+
19
19
  # Fall back to the engine's icons
20
- engine_path = OkonomiUiKit::Engine.root.join('app', 'assets', 'images', "#{name}.svg")
20
+ engine_path = OkonomiUiKit::Engine.root.join("app", "assets", "images", "#{name}.svg")
21
21
  return engine_path if File.exist?(engine_path)
22
-
22
+
23
23
  nil
24
24
  end
25
-
25
+
26
26
  # Deprecated, kept for backwards compatibility
27
27
  def self.file_name(name)
28
28
  file_path(name)
@@ -0,0 +1,114 @@
1
+ module OkonomiUiKit
2
+ class TWMerge
3
+ # ---- Public API -----------------------------------------------------------
4
+
5
+ # Merge two Tailwind class strings with conflict resolution.
6
+ def self.merge(a, b)
7
+ tokens = "#{a} #{b}".split(/\s+/).reject(&:empty?)
8
+ result = []
9
+ index_by_key = {}
10
+
11
+ tokens.each do |tok|
12
+ variants, base = split_variants(tok)
13
+ group = conflict_group_for(base)
14
+ key = [ variants, group || "literal:#{base}" ]
15
+
16
+ if index_by_key.key?(key)
17
+ pos = index_by_key[key]
18
+ result[pos] = tok
19
+ else
20
+ index_by_key[key] = result.length
21
+ result << tok
22
+ end
23
+ end
24
+
25
+ result.join(" ")
26
+ end
27
+
28
+ def self.merge_all(*args)
29
+ args.compact.reduce do |merged, arg|
30
+ merge(merged, arg)
31
+ end
32
+ end
33
+
34
+ # Deep-merge two hashes; when both values are strings, merge as Tailwind classes.
35
+ # For other types, the right-hand value wins unless it is nil.
36
+ def self.deep_merge(a, b)
37
+ if a.is_a?(Hash) && b.is_a?(Hash)
38
+ (a.keys | b.keys).each_with_object({}) do |k, h|
39
+ h[k] = deep_merge(a[k], b[k])
40
+ end
41
+ elsif a.is_a?(String) && b.is_a?(String)
42
+ merge(a, b)
43
+ else
44
+ b.nil? ? a : b
45
+ end
46
+ end
47
+
48
+ def self.deep_merge_all(*hashes)
49
+ hashes.reduce({}) do |result, hash|
50
+ if hash.is_a?(Hash)
51
+ deep_merge(result, hash)
52
+ else
53
+ raise ArgumentError, "All arguments must be Hashes"
54
+ end
55
+ end
56
+ end
57
+
58
+ # ---- Implementation details ----------------------------------------------
59
+
60
+ # Conflict groups (minimal, extensible). More specific patterns first.
61
+ CONFLICT_RULES = [
62
+ # Typography
63
+ [ /^text-(?:xs|sm|base|lg|xl|\d+xl|\[\S+\])$/, :text_size ],
64
+ [ /^text-(?:inherit|current|transparent|black|white|[a-z]+-\d{2,3}|[a-z]+-950|\[.+\])$/, :text_color ],
65
+ [ /^font-(?:thin|extralight|light|normal|medium|semibold|bold|extrabold|black)$/, :font_weight ],
66
+ [ /^leading-(?:none|tight|snug|normal|relaxed|loose|\d+|\[.+\])$/, :line_height ],
67
+ [ /^tracking-(?:tighter|tight|normal|wide|widest|\[.+\])$/, :letter_spacing ],
68
+
69
+ # Display & position
70
+ [ /^(?:hidden|block|inline|inline-block|flex|inline-flex|grid|inline-grid|table|inline-table|flow-root)$/, :display ],
71
+ [ /^(?:static|fixed|absolute|relative|sticky)$/, :position ],
72
+
73
+ # Flexbox
74
+ [ /^flex-(?:row|col|row-reverse|col-reverse)$/, :flex_direction ],
75
+ [ /^flex-(?:wrap|nowrap|wrap-reverse)$/, :flex_wrap ],
76
+ [ /^items-(?:start|end|center|baseline|stretch)$/, :align_items ],
77
+ [ /^justify-(?:start|end|center|between|around|evenly)$/, :justify_content ],
78
+
79
+ # Borders
80
+ [ /^(?:border|border-(?:\d+|\[\S+\]))$/, :border_width_overall ],
81
+ [ /^border-[trblxy](?:-\d+|\[\S+\])?$/, :border_width_side ],
82
+ [ /^border-(?:solid|dashed|dotted|double|none)$/, :border_style ],
83
+ [ /^border-(?:inherit|current|transparent|black|white|[a-z]+-\d{2,3}|[a-z]+-950|\[.+\])$/, :border_color ],
84
+
85
+ # Radius
86
+ [ /^rounded(?:-(?:none|sm|md|lg|xl|2xl|3xl|full|\[.+\]))?$/, :rounded_overall ],
87
+ [ /^rounded-(?:t|r|b|l|tl|tr|br|bl)-(?:none|sm|md|lg|xl|2xl|3xl|full|\[.+\])$/, :rounded_corner ],
88
+
89
+ # Background
90
+ [ /^bg-(?:inherit|current|transparent|black|white|[a-z]+-\d{2,3}|[a-z]+-950|\[.+\])$/, :bg_color ],
91
+
92
+ # Overflow & opacity
93
+ [ /^overflow-(?:auto|hidden|visible|scroll|clip)$/, :overflow ],
94
+ [ /^overflow-[xy]-(?:auto|hidden|visible|scroll|clip)$/, :overflow_axis ],
95
+ [ /^opacity-(?:\d{1,3}|\[.+\])$/, :opacity ]
96
+ ].freeze
97
+
98
+ class << self
99
+ private
100
+
101
+ # "sm:hover:text-lg" -> ["sm:hover", "text-lg"]
102
+ def split_variants(token)
103
+ parts = token.split(":")
104
+ return [ "", token ] if parts.size == 1
105
+ [ parts[0..-2].join(":"), parts[-1] ]
106
+ end
107
+
108
+ def conflict_group_for(base)
109
+ rule = CONFLICT_RULES.find { |(rx, _)| base.match?(rx) }
110
+ rule ? rule[1] : nil
111
+ end
112
+ end
113
+ end
114
+ end