okonomi_ui_kit 0.1.9 → 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.
@@ -0,0 +1,619 @@
1
+ # OkonomiUiKit Component Implementation Guide
2
+
3
+ This guide explains how to implement new components in OkonomiUiKit using the plugin-based architecture.
4
+
5
+ ## Component Architecture Overview
6
+
7
+ OkonomiUiKit uses a plugin-based system where components are dynamically loaded through the `method_missing` mechanism in the `UiBuilder` class. This provides a clean, extensible way to add new components without modifying the core helper.
8
+
9
+ ## Steps to Create a New Component
10
+
11
+ ### 1. Create the Component Class
12
+
13
+ Create a new file in `app/helpers/okonomi_ui_kit/components/[component_name].rb`:
14
+
15
+ ```ruby
16
+ module OkonomiUiKit
17
+ module Components
18
+ class YourComponent < OkonomiUiKit::Component
19
+ def render(*args, &block)
20
+ # Extract options and process arguments
21
+ # Call view.render with the template path and variables
22
+ end
23
+ end
24
+ end
25
+ end
26
+ ```
27
+
28
+ ### 2. Create the Template Structure
29
+
30
+ Create the template directory and file:
31
+ ```
32
+ app/views/okonomi/components/[component_name]/_[component_name].html.erb
33
+ ```
34
+
35
+ The template path follows the convention: `okonomi/components/[name]/[name]`
36
+
37
+ ### 3. Base Component Class
38
+
39
+ All components inherit from `OkonomiUiKit::Component` which provides:
40
+
41
+ - `view`: Reference to the template/view context
42
+ - `style`: Access to registered component styles
43
+ - `template_path`: Automatically generates the path to the component's template
44
+ - `name`: Returns the underscored component name
45
+
46
+ ## Example: Alert Component
47
+
48
+ Here's how the Alert component is implemented:
49
+
50
+ ### Component Class (`app/helpers/okonomi_ui_kit/components/alert.rb`):
51
+ ```ruby
52
+ module OkonomiUiKit
53
+ module Components
54
+ class Alert < OkonomiUiKit::Component
55
+ def render(title, options = {}, &block)
56
+ view.render(template_path, title:, options: options.with_indifferent_access, &block)
57
+ end
58
+ end
59
+ end
60
+ end
61
+ ```
62
+
63
+ ### Template (`app/views/okonomi/components/alert/_alert.html.erb`):
64
+ ```erb
65
+ <div class="hover:bg-blue-700">
66
+ <%= title %>
67
+ </div>
68
+ ```
69
+
70
+ ## Example: Typography Component
71
+
72
+ A more complex example showing variant handling and theme integration:
73
+
74
+ ### Component Class (`app/helpers/okonomi_ui_kit/components/typography.rb`):
75
+ ```ruby
76
+ module OkonomiUiKit
77
+ module Components
78
+ class Typography < OkonomiUiKit::Component
79
+ TYPOGRAPHY_COMPONENTS = {
80
+ body1: 'p',
81
+ body2: 'p',
82
+ h1: 'h1',
83
+ h2: 'h2',
84
+ h3: 'h3',
85
+ h4: 'h4',
86
+ h5: 'h5',
87
+ h6: 'h6',
88
+ }.freeze
89
+
90
+ def render(text = nil, options = {}, &block)
91
+ options, text = text, nil if block_given?
92
+ options ||= {}
93
+
94
+ variant = (options.delete(:variant) || 'body1').to_sym
95
+ component = (TYPOGRAPHY_COMPONENTS[variant] || 'span').to_s
96
+ color = (options.delete(:color) || 'default').to_sym
97
+
98
+ classes = [
99
+ style(:variants, variant) || '',
100
+ style(:colors, color) || '',
101
+ options.delete(:class) || ''
102
+ ].reject(&:blank?).join(' ')
103
+
104
+ view.render(
105
+ template_path,
106
+ text: text,
107
+ options: options,
108
+ variant: variant,
109
+ component: component,
110
+ classes: classes,
111
+ &block
112
+ )
113
+ end
114
+ end
115
+ end
116
+ end
117
+ ```
118
+
119
+ ### Template (`app/views/okonomi/components/typography/_typography.html.erb`):
120
+ ```erb
121
+ <%= content_tag component, class: classes do %>
122
+ <% if defined?(text)%>
123
+ <%= text %>
124
+ <% else %>
125
+ <%= yield %>
126
+ <% end %>
127
+ <% end %>
128
+ ```
129
+
130
+ ## How the Plugin System Works
131
+
132
+ 1. When `ui.your_component(...)` is called, the `method_missing` in `UiBuilder` intercepts it
133
+ 2. It converts `your_component` to `YourComponent` and checks if `OkonomiUiKit::Components::YourComponent` exists
134
+ 3. If found, it instantiates the component with the template and theme, then calls `render`
135
+ 4. If not found, it calls `super` to raise the standard NoMethodError
136
+
137
+ ```ruby
138
+ def method_missing(method_name, *args, &block)
139
+ component_name = "OkonomiUiKit::Components::#{method_name.to_s.camelize}"
140
+ if Object.const_defined?(component_name)
141
+ return component_name.constantize.new(@template, get_theme).render(*args, &block)
142
+ else
143
+ super
144
+ end
145
+ end
146
+ ```
147
+
148
+ ## Best Practices
149
+
150
+ 1. **Style Integration**: Always use the `style` method to get styling classes
151
+ 2. **Flexible Arguments**: Support both text/content as first argument and block form
152
+ 3. **Options Processing**: Use `with_indifferent_access` for options hashes
153
+ 4. **Class Composition**: Build classes arrays and join them, filtering out blanks
154
+ 5. **Template Variables**: Pass all necessary variables to the template explicitly
155
+
156
+ ## Defining and Using Styles in Components
157
+
158
+ ### Style Registration
159
+
160
+ Components can define their default styles using the `register_styles` class method. This approach provides:
161
+ - Clean separation of styling from logic
162
+ - Easy style overrides via theme system
163
+ - Consistent style access patterns
164
+
165
+ #### Basic Style Registration
166
+
167
+ ```ruby
168
+ module OkonomiUiKit
169
+ module Components
170
+ class Button < OkonomiUiKit::Component
171
+ register_styles :default do
172
+ {
173
+ base: "inline-flex items-center justify-center rounded-md font-medium",
174
+ sizes: {
175
+ sm: "px-3 py-1.5 text-sm",
176
+ md: "px-4 py-2 text-base",
177
+ lg: "px-6 py-3 text-lg"
178
+ },
179
+ variants: {
180
+ primary: "bg-primary-600 text-white hover:bg-primary-700",
181
+ secondary: "bg-secondary-600 text-white hover:bg-secondary-700",
182
+ outlined: "border border-gray-300 bg-white text-gray-700 hover:bg-gray-50"
183
+ }
184
+ }
185
+ end
186
+ end
187
+ end
188
+ end
189
+ ```
190
+
191
+ ### Accessing Styles in Components
192
+
193
+ The `Component` base class provides a `style` method to access registered styles:
194
+
195
+ ```ruby
196
+ def render(text, options = {})
197
+ size = (options.delete(:size) || :md).to_sym
198
+ variant = (options.delete(:variant) || :primary).to_sym
199
+
200
+ classes = [
201
+ style(:base), # Access base styles
202
+ style(:sizes, size), # Access nested styles
203
+ style(:variants, variant), # Access variant styles
204
+ options.delete(:class) # Include custom classes
205
+ ].compact.join(' ')
206
+
207
+ view.tag.button(text, class: classes, **options)
208
+ end
209
+ ```
210
+
211
+ ### Using Config Classes for Customization
212
+
213
+ Users can customize component styles by creating config classes instead of modifying theme configuration:
214
+
215
+ ```ruby
216
+ # app/helpers/okonomi_ui_kit/configs/button.rb
217
+ module OkonomiUiKit
218
+ module Configs
219
+ class Button < OkonomiUiKit::Config
220
+ register_styles :default do
221
+ {
222
+ base: "custom-button-base-classes",
223
+ variants: {
224
+ primary: "bg-brand-500 hover:bg-brand-600"
225
+ }
226
+ }
227
+ end
228
+ end
229
+ end
230
+ end
231
+ ```
232
+
233
+ For detailed information, see the [Style Override Guide](../guides/style-overrides-guide.md).
234
+
235
+ ### Style Registration Patterns
236
+
237
+ #### 1. Simple Components
238
+ For components with basic styling needs:
239
+
240
+ ```ruby
241
+ register_styles :default do
242
+ {
243
+ base: "inline-block rounded px-2 py-1 text-sm",
244
+ colors: {
245
+ default: "bg-gray-100 text-gray-800",
246
+ primary: "bg-blue-100 text-blue-800",
247
+ success: "bg-green-100 text-green-800"
248
+ }
249
+ }
250
+ end
251
+ ```
252
+
253
+ #### 2. Complex Components
254
+ For components with multiple style dimensions:
255
+
256
+ ```ruby
257
+ register_styles :default do
258
+ {
259
+ base: "relative inline-flex items-center",
260
+ variants: {
261
+ solid: "shadow-sm",
262
+ ghost: "shadow-none",
263
+ raised: "shadow-lg"
264
+ },
265
+ sizes: {
266
+ sm: "h-8 text-sm",
267
+ md: "h-10 text-base",
268
+ lg: "h-12 text-lg"
269
+ },
270
+ states: {
271
+ disabled: "opacity-50 cursor-not-allowed",
272
+ loading: "cursor-wait",
273
+ active: "ring-2 ring-offset-2"
274
+ }
275
+ }
276
+ end
277
+ ```
278
+
279
+ #### 3. Conditional Styles
280
+ When styles depend on multiple conditions:
281
+
282
+ ```ruby
283
+ def render(content, options = {})
284
+ variant = options.delete(:variant) || :solid
285
+ size = options.delete(:size) || :md
286
+ disabled = options.delete(:disabled)
287
+ loading = options.delete(:loading)
288
+
289
+ classes = [
290
+ style(:base),
291
+ style(:variants, variant),
292
+ style(:sizes, size),
293
+ disabled ? style(:states, :disabled) : nil,
294
+ loading ? style(:states, :loading) : nil
295
+ ].compact.join(' ')
296
+
297
+ # ...
298
+ end
299
+ ```
300
+
301
+ ### Style Override System
302
+
303
+ Components use a two-layer style system:
304
+ 1. **Internal Styles**: Default styles defined with `register_styles` in the component
305
+ 2. **Config Styles**: Override styles defined in config classes
306
+
307
+ Styles are merged intelligently using TWMerge, with config styles taking precedence over internal styles.
308
+
309
+ ### Style Method Reference
310
+
311
+ The `style` method supports multiple access patterns:
312
+
313
+ ```ruby
314
+ # Access base styles
315
+ style(:base) # => "inline-flex items-center..."
316
+
317
+ # Access nested styles
318
+ style(:variants, :primary) # => "bg-primary-600 text-white..."
319
+
320
+ # Access deeply nested styles
321
+ style(:states, :hover, :primary) # => "hover:bg-primary-700"
322
+
323
+ # Returns nil for non-existent keys (safe access)
324
+ style(:variants, :unknown) # => nil
325
+ ```
326
+
327
+ ### Best Practices for Component Styles
328
+
329
+ 1. **Use Semantic Keys**: Name style groups based on their purpose (variants, sizes, states)
330
+ 2. **Provide Defaults**: Always include sensible defaults for optional style parameters
331
+ 3. **Keep Base Minimal**: Base styles should only include essential, always-applied classes
332
+ 4. **Avoid Conflicts**: Design style groups to be composable without conflicts
333
+ 5. **Use Tailwind Utilities**: Leverage Tailwind's utility classes for consistency
334
+ 6. **Document Style Options**: Include comments describing available style options
335
+
336
+ ### Complete Example: Badge Component
337
+
338
+ ```ruby
339
+ module OkonomiUiKit
340
+ module Components
341
+ class Badge < OkonomiUiKit::Component
342
+ def render(text, options = {})
343
+ options = options.with_indifferent_access
344
+ severity = (options.delete(:severity) || :default).to_sym
345
+
346
+ classes = [
347
+ style(:base),
348
+ style(:severities, severity) || '',
349
+ options.delete(:class) || ''
350
+ ].reject(&:blank?).join(' ')
351
+
352
+ view.tag.span(text, class: classes, **options)
353
+ end
354
+
355
+ register_styles :default do
356
+ {
357
+ base: "inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium",
358
+ severities: {
359
+ default: "bg-gray-100 text-gray-800",
360
+ success: "bg-green-100 text-green-800",
361
+ danger: "bg-red-100 text-red-800",
362
+ info: "bg-blue-100 text-blue-800",
363
+ warning: "bg-yellow-100 text-yellow-800"
364
+ }
365
+ }
366
+ end
367
+ end
368
+ end
369
+ end
370
+ ```
371
+
372
+ ## Component Style System
373
+
374
+ All components use the `register_styles` method to define their default styles and access them via the `style` helper:
375
+
376
+ ```ruby
377
+ class MyComponent < OkonomiUiKit::Component
378
+ register_styles :default do
379
+ {
380
+ base: "...",
381
+ variants: { ... }
382
+ }
383
+ end
384
+
385
+ def render(...)
386
+ style(:base) # Access registered styles
387
+ end
388
+ end
389
+ ```
390
+
391
+ The `style` method provides:
392
+ - Clean syntax for accessing styles
393
+ - Automatic merging with config class overrides
394
+ - Safe access (returns nil for missing keys)
395
+ - Deep nesting support
396
+
397
+ ## Testing Your Component
398
+
399
+ After creating a component, test it in your views:
400
+
401
+ ```erb
402
+ <%= ui.your_component("Content", variant: :primary, color: :success) %>
403
+
404
+ <%= ui.your_component(variant: :secondary) do %>
405
+ <span>Block content</span>
406
+ <% end %>
407
+ ```
408
+
409
+ ## Component Naming Conventions
410
+
411
+ - Component class: `PascalCase` (e.g., `ButtonGroup`)
412
+ - Method name: `snake_case` (e.g., `button_group`)
413
+ - Template directory: `snake_case` (e.g., `button_group`)
414
+ - Template file: `_snake_case.html.erb` (e.g., `_button_group.html.erb`)
415
+
416
+ This architecture ensures components are:
417
+ - Easy to add without modifying core code
418
+ - Automatically available through the `ui` helper
419
+ - Consistent in structure and behavior
420
+ - Fully integrated with the theme system
421
+
422
+ ## Testing Components
423
+
424
+ Components should be thoroughly tested to ensure they work correctly and integrate properly with the theme system. Here's how to test components:
425
+
426
+ ### Test File Location
427
+
428
+ Create test files in `test/helpers/okonomi_ui_kit/components/[component_name]_test.rb`
429
+
430
+ ### Basic Test Structure
431
+
432
+ ```ruby
433
+ require "test_helper"
434
+
435
+ module OkonomiUiKit
436
+ module Components
437
+ class YourComponentTest < ActionView::TestCase
438
+ include OkonomiUiKit::UiHelper
439
+
440
+ test "component renders with default options" do
441
+ html = ui.your_component("Content")
442
+
443
+ assert_includes html, "expected content"
444
+ assert_includes html, "<expected_tag"
445
+ end
446
+ end
447
+ end
448
+ end
449
+ ```
450
+
451
+ ### Key Testing Areas
452
+
453
+ 1. **Default Rendering**
454
+ ```ruby
455
+ test "renders with minimal arguments" do
456
+ html = ui.typography("Hello World")
457
+
458
+ assert_includes html, "<p"
459
+ assert_includes html, "Hello World"
460
+ assert_includes html, "</p>"
461
+ end
462
+ ```
463
+
464
+ 2. **Variant Support**
465
+ ```ruby
466
+ test "renders all variants correctly" do
467
+ %i[primary secondary outlined].each do |variant|
468
+ html = ui.button("Click", variant: variant)
469
+
470
+ # Assert variant-specific rendering
471
+ end
472
+ end
473
+ ```
474
+
475
+ 3. **Block Content**
476
+ ```ruby
477
+ test "accepts block content" do
478
+ html = ui.card do
479
+ "<strong>Custom content</strong>".html_safe
480
+ end
481
+
482
+ assert_includes html, "<strong>Custom content</strong>"
483
+ end
484
+ ```
485
+
486
+ 4. **Theme Integration**
487
+ ```ruby
488
+ test "applies registered styles" do
489
+ html = ui.alert("Message", variant: :success)
490
+
491
+ # Check that registered styles are applied
492
+ assert_match /class="[^"]*"/, html
493
+ end
494
+ ```
495
+
496
+ 5. **HTML Options**
497
+ ```ruby
498
+ test "accepts html options" do
499
+ html = ui.badge("New", id: "my-badge", data: { value: "1" })
500
+
501
+ assert_includes html, 'id="my-badge"'
502
+ assert_includes html, 'data-value="1"'
503
+ end
504
+ ```
505
+
506
+ 6. **Style Registration**
507
+ ```ruby
508
+ test "applies registered styles correctly" do
509
+ html = ui.button("Test", variant: :primary)
510
+
511
+ # Verify the component uses its registered styles
512
+ assert_match /bg-primary/, html
513
+ end
514
+ ```
515
+
516
+ 7. **Edge Cases**
517
+ ```ruby
518
+ test "handles nil content gracefully" do
519
+ html = ui.typography(nil)
520
+
521
+ assert_includes html, "<p"
522
+ refute_includes html, "nil"
523
+ end
524
+ ```
525
+
526
+ 8. **Plugin System**
527
+ ```ruby
528
+ test "component loads via plugin system" do
529
+ assert_nothing_raised do
530
+ ui.your_component("Test")
531
+ end
532
+ end
533
+ ```
534
+
535
+ ### Complete Typography Test Example
536
+
537
+ Here's the complete test suite for the Typography component as a reference:
538
+
539
+ ```ruby
540
+ require "test_helper"
541
+
542
+ module OkonomiUiKit
543
+ module Components
544
+ class TypographyTest < ActionView::TestCase
545
+ include OkonomiUiKit::UiHelper
546
+
547
+ test "typography renders with default variant and text" do
548
+ html = ui.typography("Hello World")
549
+
550
+ assert_includes html, "<p"
551
+ assert_includes html, "Hello World"
552
+ assert_includes html, "</p>"
553
+ end
554
+
555
+ test "typography renders all heading variants correctly" do
556
+ %i[h1 h2 h3 h4 h5 h6].each do |variant|
557
+ html = ui.typography("Heading #{variant}", variant: variant)
558
+
559
+ assert_includes html, "<#{variant}"
560
+ assert_includes html, "Heading #{variant}"
561
+ assert_includes html, "</#{variant}>"
562
+ end
563
+ end
564
+
565
+ test "typography accepts block content" do
566
+ html = ui.typography(variant: :h2) do
567
+ "<strong>Bold content</strong>".html_safe
568
+ end
569
+
570
+ assert_includes html, "<h2"
571
+ assert_includes html, "<strong>Bold content</strong>"
572
+ assert_includes html, "</h2>"
573
+ end
574
+
575
+ test "typography applies color classes" do
576
+ html = ui.typography("Colored text", color: :primary)
577
+
578
+ assert_match /class="[^"]*"/, html
579
+ end
580
+
581
+ test "typography merges custom classes" do
582
+ html = ui.typography("Custom styled", class: "custom-class")
583
+
584
+ assert_includes html, "custom-class"
585
+ end
586
+
587
+ test "typography accepts html options" do
588
+ html = ui.typography("Text with ID", id: "my-typography", data: { testid: "typography-element" })
589
+
590
+ assert_includes html, 'id="my-typography"'
591
+ assert_includes html, 'data-testid="typography-element"'
592
+ end
593
+ end
594
+ end
595
+ end
596
+ ```
597
+
598
+ ### Running Component Tests
599
+
600
+ Run tests for a specific component:
601
+ ```bash
602
+ bin/rails test test/helpers/okonomi_ui_kit/components/typography_test.rb
603
+ ```
604
+
605
+ Run all component tests:
606
+ ```bash
607
+ bin/rails test test/helpers/okonomi_ui_kit/components/
608
+ ```
609
+
610
+ ### Testing Best Practices
611
+
612
+ 1. **Test Public Interface** - Focus on testing what users of the component will use
613
+ 2. **Avoid Implementation Details** - Don't test internal methods or exact HTML structure unless critical
614
+ 3. **Test Edge Cases** - Include tests for nil values, empty strings, missing options
615
+ 4. **Test Integration** - Ensure components work with the style system and view helpers
616
+ 5. **Keep Tests Fast** - Use ActionView::TestCase for unit tests rather than integration tests
617
+ 6. **Use Descriptive Names** - Test names should clearly indicate what behavior they verify
618
+
619
+ This testing approach ensures your components are reliable, maintainable, and properly integrated with the OkonomiUiKit system.
@@ -99,5 +99,9 @@ module OkonomiUiKit
99
99
  OkonomiUiKit::TWMerge.deep_merge_all(*hashes)
100
100
  end
101
101
  delegate :deep_merge, to: :class
102
+
103
+ def tw_merge(*classes)
104
+ OkonomiUiKit::TWMerge.merge_all(*classes)
105
+ end
102
106
  end
103
107
  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