okonomi_ui_kit 0.1.5 → 0.1.7

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 (28) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/builds/okonomi_ui_kit/application.tailwind.css +313 -4
  3. data/app/helpers/okonomi_ui_kit/component.rb +80 -0
  4. data/app/helpers/okonomi_ui_kit/components/alert.rb +9 -0
  5. data/app/helpers/okonomi_ui_kit/components/badge.rb +31 -0
  6. data/app/helpers/okonomi_ui_kit/components/button_to.rb +34 -0
  7. data/app/helpers/okonomi_ui_kit/components/code.rb +73 -0
  8. data/app/helpers/okonomi_ui_kit/components/link_to.rb +34 -0
  9. data/app/helpers/okonomi_ui_kit/components/page.rb +247 -0
  10. data/app/helpers/okonomi_ui_kit/components/table.rb +207 -0
  11. data/app/helpers/okonomi_ui_kit/components/typography.rb +68 -0
  12. data/app/helpers/okonomi_ui_kit/config.rb +16 -0
  13. data/app/helpers/okonomi_ui_kit/theme.rb +55 -3
  14. data/app/helpers/okonomi_ui_kit/ui_helper.rb +35 -63
  15. data/app/javascript/okonomi_ui_kit/controllers/modal_controller.js +94 -0
  16. data/app/views/okonomi/components/alert/_alert.html.erb +3 -0
  17. data/app/views/okonomi/components/code/_code.html.erb +1 -0
  18. data/app/views/okonomi/components/page/_page.html.erb +5 -0
  19. data/app/views/okonomi/components/table/_table.html.erb +3 -0
  20. data/app/views/okonomi/components/typography/_typography.html.erb +7 -0
  21. data/app/views/okonomi/modals/_confirmation_modal.html.erb +77 -0
  22. data/lib/okonomi_ui_kit/engine.rb +0 -3
  23. data/lib/okonomi_ui_kit/version.rb +1 -1
  24. metadata +23 -7
  25. data/app/helpers/okonomi_ui_kit/badge_helper.rb +0 -23
  26. data/app/helpers/okonomi_ui_kit/page_builder_helper.rb +0 -217
  27. data/app/helpers/okonomi_ui_kit/table_helper.rb +0 -158
  28. data/app/views/okonomi/components/_typography.html.erb +0 -7
@@ -0,0 +1,80 @@
1
+ module OkonomiUiKit
2
+ class Component
3
+ attr_reader :view, :theme
4
+
5
+ def initialize(view, theme)
6
+ @view = view
7
+ @theme = theme || OkonomiUiKit::Theme::DEFAULT_THEME
8
+ end
9
+
10
+ def template_path
11
+ "okonomi/components/#{name}/#{name}"
12
+ end
13
+
14
+ def name
15
+ self.class.name.demodulize.underscore
16
+ end
17
+
18
+ def style(*args)
19
+ styles.dig(*args)
20
+ end
21
+
22
+ def styles
23
+ @combined_styles ||= combined_styles
24
+ end
25
+
26
+ def combined_styles
27
+ internal_name = internal_styles_registry.has_key?(theme_name) ? theme_name : :default
28
+ config_name = config_styles_registry.has_key?(theme_name) ? theme_name : :default
29
+
30
+ internal_styles = internal_styles_registry[internal_name] || {}
31
+ config_styles = config_styles_registry[config_name] || {}
32
+
33
+ {}.deep_merge(internal_styles).deep_merge(config_styles)
34
+ end
35
+
36
+ def internal_styles_registry
37
+ self.class.internal_styles_registry
38
+ end
39
+
40
+ def config_styles_registry
41
+ self.class.config_styles_registry
42
+ end
43
+
44
+ def theme_name
45
+ :default
46
+ end
47
+
48
+ def self.config_styles_registry
49
+ return Hash.new({}) unless config_class?
50
+
51
+ config_class.styles_registry
52
+ end
53
+
54
+ def self.config_class
55
+ return nil unless config_class?
56
+
57
+ Object.const_get(config_class_name)
58
+ end
59
+
60
+ def self.config_class_name
61
+ "OkonomiUiKit::Configs::#{name.demodulize}"
62
+ end
63
+
64
+ def self.config_class?
65
+ Object.const_defined?(config_class_name)
66
+ end
67
+
68
+ def self.register_styles(theme = :default, &block)
69
+ styles = block.call if block_given?
70
+
71
+ raise ArgumentError, "Styles must be a Hash" unless styles.is_a?(Hash)
72
+
73
+ internal_styles_registry[theme] = styles if styles.is_a?(Hash)
74
+ end
75
+
76
+ def self.internal_styles_registry
77
+ @internal_styles_registry ||= {}
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,9 @@
1
+ module OkonomiUiKit
2
+ module Components
3
+ class Alert < OkonomiUiKit::Component
4
+ def render(title, options = {}, &block)
5
+ view.render(template_path, title:, options: options.with_indifferent_access, &block)
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,31 @@
1
+ module OkonomiUiKit
2
+ module Components
3
+ class Badge < OkonomiUiKit::Component
4
+ def render(text, options = {})
5
+ options = options.with_indifferent_access
6
+ severity = (options.delete(:severity) || :default).to_sym
7
+
8
+ classes = [
9
+ style(:base),
10
+ style(:severities, severity) || '',
11
+ options.delete(:class) || ''
12
+ ].reject(&:blank?).join(' ')
13
+
14
+ view.tag.span(text, class: classes, **options)
15
+ end
16
+
17
+ register_styles :default do
18
+ {
19
+ base: "inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium",
20
+ severities: {
21
+ default: "bg-gray-100 text-gray-800",
22
+ success: "bg-green-100 text-green-800",
23
+ danger: "bg-red-100 text-red-800",
24
+ info: "bg-blue-100 text-blue-800",
25
+ warning: "bg-yellow-100 text-yellow-800"
26
+ }
27
+ }
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,34 @@
1
+ module OkonomiUiKit
2
+ module Components
3
+ class ButtonTo < OkonomiUiKit::Component
4
+ def render(name = nil, options = nil, html_options = nil, &block)
5
+ html_options, options, name = options, name, block if block_given?
6
+
7
+ html_options ||= {}
8
+ html_options = html_options.with_indifferent_access
9
+
10
+ variant = (html_options.delete(:variant) || 'contained').to_sym
11
+ color = (html_options.delete(:color) || 'default').to_sym
12
+
13
+ html_options[:class] = build_button_class(variant: variant, color: color, classes: html_options[:class])
14
+
15
+ if block_given?
16
+ view.button_to(options, html_options, &block)
17
+ else
18
+ view.button_to(name, options, html_options)
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def build_button_class(variant:, color:, classes: '')
25
+ [
26
+ theme.dig(:components, :link, :root) || '',
27
+ theme.dig(:components, :link, variant.to_sym, :root) || '',
28
+ theme.dig(:components, :link, variant.to_sym, :colors, color.to_sym) || '',
29
+ classes,
30
+ ].reject(&:blank?).join(' ')
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,73 @@
1
+ module OkonomiUiKit
2
+ module Components
3
+ class Code < OkonomiUiKit::Component
4
+ def render(content = nil, options = {}, &block)
5
+ options, content = content, nil if block_given?
6
+ options ||= {}
7
+ options = options.with_indifferent_access
8
+
9
+ # Extract component-specific options
10
+ language = options.delete(:language) || options.delete(:lang)
11
+ variant = (options.delete(:variant) || 'default').to_sym
12
+ size = (options.delete(:size) || 'default').to_sym
13
+ wrap = options.delete(:wrap) != false # Default to true
14
+
15
+ # Build classes
16
+ classes = build_classes(variant: variant, size: size, wrap: wrap, custom_class: options.delete(:class))
17
+
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
26
+
27
+ view.render(
28
+ template_path,
29
+ content: escaped_content.strip.html_safe,
30
+ options: options,
31
+ classes: classes,
32
+ language: language
33
+ )
34
+ end
35
+
36
+ private
37
+
38
+ 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"
64
+
65
+ [base_classes, variant_classes, size_classes, wrap_classes, custom_class].compact.join(' ')
66
+ end
67
+
68
+ def html_escape(content)
69
+ ERB::Util.html_escape(content.to_s.strip)
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,34 @@
1
+ module OkonomiUiKit
2
+ module Components
3
+ class LinkTo < OkonomiUiKit::Component
4
+ def render(name = nil, options = nil, html_options = nil, &block)
5
+ html_options, options, name = options, name, block if block_given?
6
+
7
+ html_options ||= {}
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
12
+
13
+ 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)
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def build_button_class(variant:, color:, classes: '')
25
+ [
26
+ theme.dig(:components, :link, :root) || '',
27
+ theme.dig(:components, :link, variant.to_sym, :root) || '',
28
+ theme.dig(:components, :link, variant.to_sym, :colors, color.to_sym) || '',
29
+ classes,
30
+ ].reject(&:blank?).join(' ')
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,247 @@
1
+ module OkonomiUiKit
2
+ module Components
3
+ class Page < OkonomiUiKit::Component
4
+ def render(options = {}, &block)
5
+ builder = PageBuilder.new(view)
6
+
7
+ view.render(template_path, builder: builder, options: options, &block)
8
+ end
9
+ end
10
+
11
+ class PageBuilder
12
+ include ActionView::Helpers::TagHelper
13
+ include ActionView::Helpers::CaptureHelper
14
+
15
+ def initialize(template)
16
+ @template = template
17
+ @content_parts = []
18
+ end
19
+
20
+ def page_header(**options, &block)
21
+ header_builder = PageHeaderBuilder.new(@template)
22
+ yield(header_builder) if block_given?
23
+ @content_parts << header_builder.render
24
+ nil
25
+ end
26
+
27
+ 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
32
+ nil
33
+ end
34
+
35
+ def render_content
36
+ @template.safe_join(@content_parts)
37
+ end
38
+
39
+ def to_s
40
+ render_content
41
+ end
42
+
43
+ private
44
+
45
+ def tag
46
+ @template.tag
47
+ end
48
+
49
+ def capture(*args, &block)
50
+ @template.capture(*args, &block)
51
+ end
52
+ 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.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
+ end
247
+ end