okonomi_ui_kit 0.1.9 → 0.1.11

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.
@@ -10,31 +10,99 @@ module OkonomiUiKit
10
10
  ].reject(&:blank?).join(" ")
11
11
  end
12
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
79
+ end
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,20 +2,26 @@ 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
15
  variant = (options.delete(:variant) || "contained").to_sym
11
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
@@ -9,13 +9,14 @@ module OkonomiUiKit
9
9
 
10
10
  variant = (html_options.delete(:variant) || "contained").to_sym
11
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
@@ -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
@@ -9,13 +9,14 @@ module OkonomiUiKit
9
9
 
10
10
  variant = (html_options.delete(:variant) || "text").to_sym
11
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.link_to(options, html_options, &block)
17
- else
18
- view.link_to(name, options, html_options)
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
@@ -2,9 +2,22 @@ module OkonomiUiKit
2
2
  module Components
3
3
  class Page < OkonomiUiKit::Component
4
4
  def render(options = {}, &block)
5
+ options = options.with_indifferent_access
6
+
7
+ classes = tw_merge(
8
+ style(:root),
9
+ options.delete(:class)
10
+ )
11
+
5
12
  builder = PageBuilder.new(view)
6
13
 
7
- view.render(template_path, builder: builder, options: options, &block)
14
+ view.render(template_path, builder: builder, options: options.merge(class: classes), &block)
15
+ end
16
+
17
+ register_styles :default do
18
+ {
19
+ root: "flex flex-col gap-8 p-8"
20
+ }
8
21
  end
9
22
  end
10
23
 
@@ -18,17 +31,12 @@ module OkonomiUiKit
18
31
  end
19
32
 
20
33
  def page_header(**options, &block)
21
- header_builder = PageHeaderBuilder.new(@template)
22
- yield(header_builder) if block_given?
23
- @content_parts << header_builder.render
34
+ @content_parts << @template.ui.page_header(options, &block)
24
35
  nil
25
36
  end
26
37
 
27
38
  def section(**options, &block)
28
- section_builder = SectionBuilder.new(@template)
29
- section_builder.title(options[:title]) if options[:title]
30
- yield(section_builder) if block_given?
31
- @content_parts << section_builder.render
39
+ @content_parts << @template.ui.page_section(options, &block)
32
40
  nil
33
41
  end
34
42
 
@@ -50,198 +58,5 @@ module OkonomiUiKit
50
58
  @template.capture(*args, &block)
51
59
  end
52
60
  end
53
-
54
- class PageHeaderBuilder
55
- include ActionView::Helpers::TagHelper
56
- include ActionView::Helpers::CaptureHelper
57
-
58
- def initialize(template)
59
- @template = template
60
- @breadcrumbs_content = nil
61
- @row_content = nil
62
- end
63
-
64
- def breadcrumbs(&block)
65
- @breadcrumbs_content = @template.ui.breadcrumbs(&block)
66
- end
67
-
68
- def row(&block)
69
- row_builder = PageHeaderRowBuilder.new(@template)
70
- yield(row_builder) if block_given?
71
- @row_content = row_builder.render
72
- end
73
-
74
- def render
75
- content = []
76
- content << @breadcrumbs_content if @breadcrumbs_content
77
- content << @row_content if @row_content
78
-
79
- tag.div(class: "flex flex-col gap-2") do
80
- @template.safe_join(content.compact)
81
- end
82
- end
83
-
84
- private
85
-
86
- def tag
87
- @template.tag
88
- end
89
-
90
- def capture(*args, &block)
91
- @template.capture(*args, &block)
92
- end
93
- end
94
-
95
- class PageHeaderRowBuilder
96
- include ActionView::Helpers::TagHelper
97
- include ActionView::Helpers::CaptureHelper
98
-
99
- def initialize(template)
100
- @template = template
101
- @title_content = nil
102
- @actions_content = nil
103
- end
104
-
105
- def title(text, **options)
106
- @title_content = tag.h1(text, class: "text-2xl font-bold leading-7 text-gray-900 truncate sm:text-3xl sm:tracking-tight")
107
- end
108
-
109
- def actions(&block)
110
- @actions_content = tag.div(class: "mt-4 flex md:ml-4 md:mt-0 gap-2") do
111
- capture(&block) if block_given?
112
- end
113
- end
114
-
115
- def render
116
- tag.div(class: "flex w-full justify-between items-center") do
117
- content = []
118
- content << @title_content if @title_content
119
- content << @actions_content if @actions_content
120
- @template.safe_join(content.compact)
121
- end
122
- end
123
-
124
- private
125
-
126
- def tag
127
- @template.tag
128
- end
129
-
130
- def capture(*args, &block)
131
- @template.capture(*args, &block)
132
- end
133
- end
134
-
135
- class SectionBuilder
136
- include ActionView::Helpers::TagHelper
137
- include ActionView::Helpers::CaptureHelper
138
-
139
- def initialize(template)
140
- @template = template
141
- @title_content = nil
142
- @subtitle_content = nil
143
- @actions_content = nil
144
- @body_content = nil
145
- @attributes = []
146
- end
147
-
148
- def title(text, **options)
149
- @title_content = tag.h3(text, class: "text-base/7 font-semibold text-gray-900")
150
- end
151
-
152
- def subtitle(text, **options)
153
- @subtitle_content = tag.p(text, class: "mt-1 max-w-2xl text-sm/6 text-gray-500")
154
- end
155
-
156
- def actions(&block)
157
- @actions_content = tag.div(class: "mt-4 flex md:ml-4 md:mt-0") do
158
- capture(&block) if block_given?
159
- end
160
- end
161
-
162
- def body(&block)
163
- if block_given?
164
- # Capture the content first to see if attributes were used
165
- content = capture { yield(self) }
166
-
167
- @body_content = if @attributes.any?
168
- # If attributes were added, wrap them in dl
169
- tag.div do
170
- tag.dl(class: "divide-y divide-gray-100") do
171
- @template.safe_join(@attributes)
172
- end
173
- end
174
- else
175
- # Otherwise, just return the captured content
176
- tag.div do
177
- content
178
- end
179
- end
180
- end
181
- end
182
-
183
- def attribute(label, value = nil, **options, &block)
184
- content = if block_given?
185
- capture(&block)
186
- elsif value.respond_to?(:call)
187
- value.call
188
- else
189
- value
190
- end
191
-
192
- attribute_html = tag.div(class: "py-6 sm:grid sm:grid-cols-3 sm:gap-4") do
193
- dt_content = tag.dt(label, class: "text-sm font-medium text-gray-900")
194
- dd_content = tag.dd(content, class: "mt-1 text-sm/6 text-gray-700 sm:col-span-2 sm:mt-0")
195
-
196
- dt_content + dd_content
197
- end
198
-
199
- @attributes << attribute_html
200
- end
201
-
202
- def render
203
- tag.div(class: "overflow-hidden bg-white") do
204
- header_content = build_header
205
- content_parts = []
206
- content_parts << header_content if header_content.present?
207
- content_parts << @body_content if @body_content
208
- @template.safe_join(content_parts.compact)
209
- end
210
- end
211
-
212
- private
213
-
214
- def build_header
215
- return nil unless @title_content || @subtitle_content || @actions_content
216
-
217
- tag.div(class: "py-6") do
218
- if @actions_content
219
- tag.div(class: "flex w-full justify-between items-start") do
220
- title_section = tag.div do
221
- content_parts = []
222
- content_parts << @title_content if @title_content
223
- content_parts << @subtitle_content if @subtitle_content
224
- @template.safe_join(content_parts.compact)
225
- end
226
-
227
- title_section + @actions_content
228
- end
229
- else
230
- content_parts = []
231
- content_parts << @title_content if @title_content
232
- content_parts << @subtitle_content if @subtitle_content
233
- @template.safe_join(content_parts.compact)
234
- end
235
- end
236
- end
237
-
238
- def tag
239
- @template.tag
240
- end
241
-
242
- def capture(*args, &block)
243
- @template.capture(*args, &block)
244
- end
245
- end
246
61
  end
247
62
  end
@@ -0,0 +1,111 @@
1
+ module OkonomiUiKit
2
+ module Components
3
+ class PageHeader < OkonomiUiKit::Component
4
+ def render(options = {}, &block)
5
+ options = options.with_indifferent_access
6
+
7
+ classes = tw_merge(
8
+ style(:root),
9
+ options.delete(:class)
10
+ )
11
+
12
+ builder = PageHeaderBuilder.new(view, self)
13
+
14
+ view.render(template_path, builder: builder, options: options.merge(class: classes), &block)
15
+ end
16
+
17
+ register_styles :default do
18
+ {
19
+ root: "flex flex-col gap-2",
20
+ row: "flex w-full justify-between items-center",
21
+ actions: "mt-4 flex md:ml-4 md:mt-0 gap-2"
22
+ }
23
+ end
24
+ end
25
+
26
+ class PageHeaderBuilder
27
+ include ActionView::Helpers::TagHelper
28
+ include ActionView::Helpers::CaptureHelper
29
+
30
+ def initialize(template, component)
31
+ @template = template
32
+ @component = component
33
+ @breadcrumbs_content = nil
34
+ @row_content = nil
35
+ end
36
+
37
+ def breadcrumbs(&block)
38
+ @breadcrumbs_content = @template.ui.breadcrumbs(&block)
39
+ end
40
+
41
+ def row(&block)
42
+ row_builder = PageHeaderRowBuilder.new(@template, @component)
43
+ yield(row_builder) if block_given?
44
+ @row_content = row_builder.render
45
+ end
46
+
47
+ def render
48
+ content = []
49
+ content << @breadcrumbs_content if @breadcrumbs_content
50
+ content << @row_content if @row_content
51
+
52
+ @template.safe_join(content.compact)
53
+ end
54
+
55
+ private
56
+
57
+ def tag
58
+ @template.tag
59
+ end
60
+
61
+ def capture(*args, &block)
62
+ @template.capture(*args, &block)
63
+ end
64
+ end
65
+
66
+ class PageHeaderRowBuilder
67
+ include ActionView::Helpers::TagHelper
68
+ include ActionView::Helpers::CaptureHelper
69
+
70
+ attr_reader :template
71
+
72
+ delegate :ui, to: :template
73
+
74
+ def initialize(template, component)
75
+ @template = template
76
+ @component = component
77
+ @title_content = nil
78
+ @actions_content = nil
79
+ end
80
+
81
+ def title(text, **options)
82
+ @title_content = ui.typography(text, variant: "h1", **options)
83
+ end
84
+
85
+ def actions(&block)
86
+ @actions_content = tag.div(class: @component.style(:actions)) do
87
+ capture(&block) if block_given?
88
+ end
89
+ end
90
+
91
+ def render
92
+ tag.div(class: @component.style(:row)) do
93
+ content = []
94
+ content << @title_content if @title_content
95
+ content << @actions_content if @actions_content
96
+ @template.safe_join(content.compact)
97
+ end
98
+ end
99
+
100
+ private
101
+
102
+ def tag
103
+ @template.tag
104
+ end
105
+
106
+ def capture(*args, &block)
107
+ @template.capture(*args, &block)
108
+ end
109
+ end
110
+ end
111
+ end