its-showtime 0.1.1

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 (36) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +179 -0
  4. data/bin/showtime +47 -0
  5. data/lib/showtime/app.rb +399 -0
  6. data/lib/showtime/charts.rb +229 -0
  7. data/lib/showtime/component_registry.rb +38 -0
  8. data/lib/showtime/components/Components.md +309 -0
  9. data/lib/showtime/components/alerts.rb +83 -0
  10. data/lib/showtime/components/base.rb +63 -0
  11. data/lib/showtime/components/charts.rb +119 -0
  12. data/lib/showtime/components/data.rb +328 -0
  13. data/lib/showtime/components/inputs.rb +390 -0
  14. data/lib/showtime/components/layout.rb +135 -0
  15. data/lib/showtime/components/media.rb +73 -0
  16. data/lib/showtime/components/sidebar.rb +130 -0
  17. data/lib/showtime/components/text.rb +156 -0
  18. data/lib/showtime/components.rb +18 -0
  19. data/lib/showtime/compute_tracker.rb +21 -0
  20. data/lib/showtime/helpers.rb +53 -0
  21. data/lib/showtime/logger.rb +143 -0
  22. data/lib/showtime/public/.vite/manifest.json +34 -0
  23. data/lib/showtime/public/assets/antd-3aDVoXqG.js +447 -0
  24. data/lib/showtime/public/assets/charts-iowb_sWQ.js +3858 -0
  25. data/lib/showtime/public/assets/index-B2b3lWS5.js +43 -0
  26. data/lib/showtime/public/assets/index-M6NVamDM.css +1 -0
  27. data/lib/showtime/public/assets/react-BE6xecJX.js +32 -0
  28. data/lib/showtime/public/index.html +19 -0
  29. data/lib/showtime/public/letter.png +0 -0
  30. data/lib/showtime/public/logo.png +0 -0
  31. data/lib/showtime/release.rb +108 -0
  32. data/lib/showtime/session.rb +131 -0
  33. data/lib/showtime/version.rb +3 -0
  34. data/lib/showtime/views/index.erb +32 -0
  35. data/lib/showtime.rb +157 -0
  36. metadata +300 -0
@@ -0,0 +1,229 @@
1
+ require "active_support"
2
+
3
+ module Showtime
4
+ module Charts
5
+ SUPPORTED_TYPES = %w[line bar column area pie scatter].freeze
6
+
7
+ class ValidationError < StandardError; end
8
+
9
+ def self.figure(chart_type:, data:, encoding:, colorway: nil, layout: {}, config: {})
10
+ Validator.validate_chart_type!(chart_type)
11
+ normalized_encoding = Validator.normalize_encoding(encoding, chart_type)
12
+ normalized_data = DataNormalizer.normalize(data, normalized_encoding, chart_type: chart_type)
13
+ Validator.validate_data!(chart_type, normalized_data, normalized_encoding)
14
+
15
+ {
16
+ chart_type: chart_type.to_s,
17
+ data: normalized_data,
18
+ encoding: normalized_encoding,
19
+ colorway: colorway,
20
+ layout: layout,
21
+ config: config,
22
+ }.compact
23
+ end
24
+
25
+ module Validator
26
+ module_function
27
+
28
+ def validate_chart_type!(chart_type)
29
+ return if SUPPORTED_TYPES.include?(chart_type.to_s)
30
+
31
+ raise ValidationError, "Unsupported chart_type #{chart_type}"
32
+ end
33
+
34
+ def normalize_encoding(encoding, chart_type)
35
+ if chart_type.to_s == "pie"
36
+ base = (encoding || {}).transform_keys(&:to_sym)
37
+ label_key = base[:label] || "label"
38
+ value_key = base[:value] || "value"
39
+
40
+ return {
41
+ label: label_key.to_s,
42
+ value: value_key.to_s
43
+ }
44
+ end
45
+
46
+ raise ValidationError, "encoding is required" if encoding.nil?
47
+
48
+ enc = encoding.transform_keys(&:to_sym)
49
+ raise ValidationError, "encoding.x is required" unless enc[:x]
50
+ raise ValidationError, "encoding.y is required" unless enc[:y]
51
+
52
+ {
53
+ x: enc[:x].to_s,
54
+ y: enc[:y].to_s,
55
+ series: enc[:series]&.to_s,
56
+ color: enc[:color]&.to_s,
57
+ stack: enc.key?(:stack) ? !!enc[:stack] : nil
58
+ }.compact
59
+ end
60
+
61
+ def validate_data!(chart_type, data, encoding)
62
+ if chart_type.to_s == "pie"
63
+ unless data.is_a?(Array) && data.all? { |slice| slice.key?("label") && slice.key?("value") }
64
+ raise ValidationError, "pie data requires label and value keys"
65
+ end
66
+ return
67
+ end
68
+
69
+ named_series = data.is_a?(Array) && data.first.is_a?(Hash) && data.first.key?(:name)
70
+ if named_series && encoding[:series].nil?
71
+ raise ValidationError, "encoding.series is required when data contains named series"
72
+ end
73
+ end
74
+ end
75
+
76
+ module DataNormalizer
77
+ module_function
78
+
79
+ def normalize(data, encoding, chart_type:)
80
+ return normalize_pie(data, encoding) if chart_type.to_s == "pie"
81
+
82
+ case data
83
+ when Hash
84
+ if data.values.all? { |v| v.is_a?(Array) }
85
+ normalize_hash_of_arrays(data, encoding)
86
+ else
87
+ normalize_grouped_hash(data, encoding)
88
+ end
89
+ when Array
90
+ return [] if data.empty?
91
+ if data.first.is_a?(Array)
92
+ normalize_array_of_arrays(data, encoding)
93
+ elsif data.first.is_a?(Hash)
94
+ normalize_array_of_hashes(data, encoding)
95
+ else
96
+ raise ValidationError, "Unsupported array contents #{data.first.class}"
97
+ end
98
+ when ->(d) { polars_dataframe?(d) }
99
+ normalize_polars_dataframe(data, encoding)
100
+ else
101
+ raise ValidationError, "Unsupported data type #{data.class}"
102
+ end
103
+ end
104
+
105
+ def polars_dataframe?(data)
106
+ data.respond_to?(:columns) && (data.respond_to?(:rows) || data.respond_to?(:to_a))
107
+ end
108
+
109
+ def normalize_grouped_hash(data, encoding)
110
+ data.sort_by { |k, _| k.to_s }.map do |key, value|
111
+ { "x" => key.is_a?(String) ? key : key.to_s, "y" => value }
112
+ end
113
+ end
114
+
115
+ def normalize_array_of_arrays(data, encoding)
116
+ data.sort_by { |row| row[0] }.map do |row|
117
+ { "x" => row[0], "y" => row[1] }
118
+ end
119
+ end
120
+
121
+ def normalize_hash_of_arrays(data, encoding)
122
+ raise ValidationError, "encoding.series is required for multi-series data" if encoding[:series].nil?
123
+
124
+ x_key = encoding.fetch(:x).to_s
125
+ y_key = encoding.fetch(:y).to_s
126
+
127
+ data.sort_by { |series_name, _| series_name.to_s }.map do |series_name, rows|
128
+ sorted_rows = rows.sort_by { |row| row[0] }
129
+ {
130
+ name: series_name.to_s,
131
+ data: sorted_rows.map { |row| { "x" => row[0], "y" => row[1] } },
132
+ }
133
+ end
134
+ end
135
+
136
+ def normalize_array_of_hashes(data, encoding)
137
+ x_key = encoding.fetch(:x).to_s
138
+ y_key = encoding.fetch(:y).to_s
139
+ series_key = encoding[:series]&.to_s
140
+
141
+ if series_key
142
+ grouped = data.group_by { |row| row[series_key] || row[series_key.to_sym] }
143
+ grouped.keys.sort_by(&:to_s).map do |series_name|
144
+ points = grouped[series_name].sort_by { |row| row[x_key] || row[x_key.to_sym] }
145
+ {
146
+ name: series_name.to_s,
147
+ data: points.map do |row|
148
+ {
149
+ "x" => coerce_value(row[x_key] || row[x_key.to_sym]),
150
+ "y" => coerce_value(row[y_key] || row[y_key.to_sym])
151
+ }
152
+ end,
153
+ }
154
+ end
155
+ else
156
+ data.sort_by { |row| row[x_key] || row[x_key.to_sym] }.map do |row|
157
+ {
158
+ "x" => coerce_value(row[x_key] || row[x_key.to_sym]),
159
+ "y" => coerce_value(row[y_key] || row[y_key.to_sym])
160
+ }
161
+ end
162
+ end
163
+ end
164
+
165
+ def coerce_value(value)
166
+ return value if value.is_a?(String) || value.is_a?(Numeric)
167
+ return value.to_f if value.respond_to?(:to_f)
168
+ value
169
+ end
170
+
171
+ def normalize_polars_dataframe(df, encoding)
172
+ rows =
173
+ if df.respond_to?(:rows)
174
+ df.rows(named: true)
175
+ else
176
+ # Fall back to to_a; if it returns arrays, zip with columns
177
+ raw = df.to_a
178
+ if raw.first.is_a?(Hash)
179
+ raw
180
+ else
181
+ cols = df.columns.map(&:to_s)
182
+ raw.map { |row| cols.zip(row).to_h }
183
+ end
184
+ end
185
+
186
+ normalize_array_of_hashes(rows, encoding)
187
+ end
188
+
189
+ def normalize_pie(data, encoding)
190
+ label_key = encoding.fetch(:label).to_s
191
+ value_key = encoding.fetch(:value).to_s
192
+
193
+ case data
194
+ when Hash
195
+ data.sort_by { |k, _| k.to_s }.map { |k, v| { "label" => k.to_s, "value" => v } }
196
+ when Array
197
+ return [] if data.empty?
198
+ if data.first.is_a?(Hash)
199
+ data.sort_by { |row| row[label_key] || row[label_key.to_sym] }.map do |row|
200
+ { "label" => row[label_key] || row[label_key.to_sym], "value" => row[value_key] || row[value_key.to_sym] }
201
+ end
202
+ elsif data.first.is_a?(Array)
203
+ data.sort_by { |row| row[0].to_s }.map { |row| { "label" => row[0].to_s, "value" => row[1] } }
204
+ else
205
+ raise ValidationError, "Unsupported array contents #{data.first.class}"
206
+ end
207
+ when ->(d) { polars_dataframe?(d) }
208
+ rows =
209
+ if data.respond_to?(:rows)
210
+ data.rows(named: true)
211
+ else
212
+ raw = data.to_a
213
+ if raw.first.is_a?(Hash)
214
+ raw
215
+ else
216
+ cols = data.columns.map(&:to_s)
217
+ raw.map { |row| cols.zip(row).to_h }
218
+ end
219
+ end
220
+ rows.sort_by { |row| row[label_key] || row[label_key.to_sym] }.map do |row|
221
+ { "label" => row[label_key] || row[label_key.to_sym], "value" => row[value_key] || row[value_key.to_sym] }
222
+ end
223
+ else
224
+ raise ValidationError, "Unsupported data type #{data.class}"
225
+ end
226
+ end
227
+ end
228
+ end
229
+ end
@@ -0,0 +1,38 @@
1
+ module Showtime
2
+ # Component registry for tracking dependencies and updates
3
+ class ComponentRegistry
4
+ def initialize
5
+ @dependencies = {} # key -> Set of dependent component keys
6
+ @dirty_components = Set.new
7
+ end
8
+
9
+ # Register a component's dependencies
10
+ def register_dependencies(key, dependencies)
11
+ dependencies.each do |dep|
12
+ @dependencies[dep] ||= Set.new
13
+ @dependencies[dep].add(key)
14
+ end
15
+ end
16
+
17
+ # Find all components that depend on a key
18
+ def find_dependents(key)
19
+ @dependencies[key] || Set.new
20
+ end
21
+
22
+ # Mark a component and its dependents as needing update
23
+ def mark_dirty(key)
24
+ @dirty_components.add(key)
25
+ find_dependents(key).each { |dep_key| mark_dirty(dep_key) }
26
+ end
27
+
28
+ # Get list of components that need updates
29
+ def dirty_components
30
+ @dirty_components.dup
31
+ end
32
+
33
+ # Clear the dirty components list
34
+ def clear_dirty
35
+ @dirty_components.clear
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,309 @@
1
+ # Creating a New Showtime Component
2
+
3
+ This guide walks through all the steps needed to create a new component in Showtime and make it available in the frontend.
4
+
5
+ ## Overview
6
+
7
+ Showtime components consist of two main parts:
8
+ 1. A Ruby component class in the backend
9
+ 2. A React component in the frontend
10
+
11
+ ## Metric Component (quick use)
12
+
13
+ Render a KPI-style number with an optional delta indicator:
14
+
15
+ ```ruby
16
+ St.metric("Revenue", value: 12_000, delta: "+5%", prefix: "$", suffix: "/wk", help: "vs last week")
17
+ ```
18
+
19
+ ## Backend Implementation
20
+
21
+ ### 1. Choose the Appropriate File
22
+
23
+ First, determine which Ruby file should contain your component based on its type:
24
+
25
+ - `alerts.rb`: Alert/notification components
26
+ - `charts.rb`: Data visualization components
27
+ - `data.rb`: Data display components
28
+ - `inputs.rb`: User input components
29
+ - `layout.rb`: Layout/structure components
30
+ - `media.rb`: Media (images, video) components
31
+ - `text.rb`: Text display components
32
+
33
+ ### 2. Create the Component Class
34
+
35
+ Create a new class that inherits from `BaseComponent`. Example:
36
+
37
+ ```ruby
38
+ module Showtime
39
+ module Components
40
+ class MyComponent < BaseComponent
41
+ attr_reader :label, :value
42
+
43
+ def initialize(label, value: nil, key: nil, help: nil)
44
+ super(key: key, help: help)
45
+ @label = label
46
+ @value = value
47
+ end
48
+
49
+ def to_h
50
+ super.merge({
51
+ label: @label,
52
+ value: @value
53
+ })
54
+ end
55
+ end
56
+ end
57
+ end
58
+ ```
59
+
60
+ Key points:
61
+ - Inherit from `BaseComponent`
62
+ - Define attributes using `attr_reader`
63
+ - Initialize with necessary parameters
64
+ - Implement `to_h` method to serialize component data
65
+
66
+ #### Optional Parameters
67
+
68
+ - **key**: A unique identifier for the component. It's used for:
69
+ - State management: Tracking component values across sessions
70
+ - WebSocket communication: Identifying which component triggered a change
71
+ - Component updates: Ensuring the correct component gets updated
72
+ - If not provided, some components like buttons will auto-generate a unique key
73
+ - Example format: `button_counter`, `data_table_1`, `color_picker_theme`
74
+
75
+ - **help**: A help text that appears below the component to provide additional information
76
+
77
+ ### 3. Add Method to St Module
78
+
79
+ Add a method to expose your component through the `St` module:
80
+
81
+ ```ruby
82
+ module St
83
+ def self.my_component(label, value: nil, key: nil, help: nil)
84
+ Showtime::session.add_element(
85
+ Showtime::Components::MyComponent.new(
86
+ label,
87
+ value: value,
88
+ key: key,
89
+ help: help
90
+ )
91
+ )
92
+ end
93
+ end
94
+ ```
95
+
96
+ ## Frontend Implementation
97
+
98
+ ### 1. Create the React Component
99
+
100
+ Create a new component file in `frontend/src/components/ui/` in the appropriate subdirectory:
101
+
102
+ ```typescript
103
+ // frontend/src/components/ui/my-component/MyComponent.tsx
104
+ import React from 'react';
105
+
106
+ interface MyComponentProps {
107
+ label: string;
108
+ value?: any;
109
+ help?: string;
110
+ onChange?: (value: any) => void;
111
+ }
112
+
113
+ export function MyComponent({
114
+ label,
115
+ value,
116
+ help,
117
+ onChange
118
+ }: MyComponentProps) {
119
+ return (
120
+ <div className="mb-4">
121
+ <label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
122
+ {label}
123
+ </label>
124
+ {/* Your component UI here */}
125
+ </div>
126
+ );
127
+ }
128
+ ```
129
+
130
+ ### 2. Export the Component
131
+
132
+ Add your component to `frontend/src/components/ui/index.ts`:
133
+
134
+ ```typescript
135
+ export { MyComponent } from './my-component/MyComponent';
136
+ ```
137
+
138
+ ### 3. Add to ComponentRenderer
139
+
140
+ Update `frontend/src/components/ComponentRenderer.tsx`:
141
+
142
+ 1. Import your component:
143
+ ```typescript
144
+ import { MyComponent } from './ui';
145
+ ```
146
+
147
+ 2. Add a case in the switch statement:
148
+ ```typescript
149
+ case 'my_component':
150
+ return (
151
+ <MyComponent
152
+ label={component.label || ''}
153
+ value={component.value}
154
+ onChange={handleChange}
155
+ help={component.help}
156
+ />
157
+ );
158
+ ```
159
+
160
+ ## Example: Creating a Color Picker Component
161
+
162
+ Here's a complete example of creating a color picker component:
163
+
164
+ ### Backend (lib/showtime/components/inputs.rb)
165
+
166
+ ```ruby
167
+ module Showtime
168
+ module Components
169
+ class ColorPicker < BaseComponent
170
+ attr_reader :label, :value
171
+
172
+ def initialize(label, value: "#000000", key: nil, help: nil)
173
+ super(key: key, help: help)
174
+ @label = label
175
+ @value = value
176
+ end
177
+
178
+ def to_h
179
+ super.merge({
180
+ label: @label,
181
+ value: @value
182
+ })
183
+ end
184
+ end
185
+ end
186
+
187
+ module St
188
+ def self.color_picker(label, value: "#000000", key: nil, help: nil)
189
+ Showtime::session.add_element(
190
+ Showtime::Components::ColorPicker.new(
191
+ label,
192
+ value: value,
193
+ key: key,
194
+ help: help
195
+ )
196
+ )
197
+ end
198
+ end
199
+ end
200
+ ```
201
+
202
+ ### Frontend
203
+
204
+ 1. Create the component (frontend/src/components/ui/input/ColorPicker.tsx):
205
+
206
+ ```typescript
207
+ import React from 'react';
208
+
209
+ interface ColorPickerProps {
210
+ label: string;
211
+ value: string;
212
+ help?: string;
213
+ onChange?: (value: string) => void;
214
+ }
215
+
216
+ export function ColorPicker({
217
+ label,
218
+ value = "#000000",
219
+ help,
220
+ onChange
221
+ }: ColorPickerProps) {
222
+ return (
223
+ <div className="mb-4">
224
+ <label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
225
+ {label}
226
+ </label>
227
+ <input
228
+ type="color"
229
+ value={value}
230
+ onChange={(e) => onChange?.(e.target.value)}
231
+ className="mt-1 block w-full"
232
+ />
233
+ {help && (
234
+ <p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
235
+ {help}
236
+ </p>
237
+ )}
238
+ </div>
239
+ );
240
+ }
241
+ ```
242
+
243
+ 2. Export the component in frontend/src/components/ui/index.ts:
244
+
245
+ ```typescript
246
+ export { ColorPicker } from './input/ColorPicker';
247
+ ```
248
+
249
+ 3. Add to ComponentRenderer.tsx:
250
+
251
+ ```typescript
252
+ // In the imports section:
253
+ import {
254
+ Alert,
255
+ Button,
256
+ ColorPicker, // Add this line
257
+ DateInput,
258
+ // ... other imports
259
+ } from './ui';
260
+
261
+ // In the switch statement:
262
+ case 'color_picker':
263
+ return (
264
+ <ColorPicker
265
+ label={component.label || ''}
266
+ value={component.value}
267
+ onChange={handleChange}
268
+ help={component.help}
269
+ />
270
+ );
271
+ ```
272
+
273
+ ### Usage Example
274
+
275
+ With everything set up, the component can be used in your Showtime applications:
276
+
277
+ ```ruby
278
+ St.color_picker("Choose a color", value: "#ff0000", help: "Select your favorite color")
279
+ ```
280
+
281
+ ## Best Practices
282
+
283
+ 1. **Component Naming**
284
+ - Use CamelCase for component class names
285
+ - Use snake_case for component types and method names
286
+
287
+ 2. **State Management**
288
+ - Use session state for persistent values
289
+ - Handle state updates through the WebSocket connection
290
+
291
+ 3. **Type Safety**
292
+ - Define clear interfaces in TypeScript
293
+ - Validate input parameters in Ruby
294
+
295
+ 4. **Styling**
296
+ - Use Tailwind CSS classes for styling
297
+ - Support both light and dark themes
298
+ - Follow existing component patterns
299
+
300
+ 5. **Error Handling**
301
+ - Provide meaningful error messages
302
+ - Handle edge cases gracefully
303
+
304
+ 6. **Documentation**
305
+ - Document all parameters
306
+ - Include usage examples
307
+ - Explain any special behaviors
308
+
309
+ Following this guide will ensure your new component integrates seamlessly with the Showtime framework and provides a consistent experience for users.
@@ -0,0 +1,83 @@
1
+ require_relative 'base'
2
+
3
+ module Showtime
4
+ module Components
5
+ class Info < BaseComponent
6
+ attr_reader :text
7
+
8
+ def initialize(text)
9
+ super()
10
+ @text = text
11
+ end
12
+
13
+ def to_h
14
+ super.merge({
15
+ text: @text
16
+ })
17
+ end
18
+ end
19
+
20
+ class Success < BaseComponent
21
+ attr_reader :text
22
+
23
+ def initialize(text)
24
+ super()
25
+ @text = text
26
+ end
27
+
28
+ def to_h
29
+ super.merge({
30
+ text: @text
31
+ })
32
+ end
33
+ end
34
+
35
+ class Warning < BaseComponent
36
+ attr_reader :text
37
+
38
+ def initialize(text)
39
+ super()
40
+ @text = text
41
+ end
42
+
43
+ def to_h
44
+ super.merge({
45
+ text: @text
46
+ })
47
+ end
48
+ end
49
+
50
+ class Error < BaseComponent
51
+ attr_reader :text
52
+
53
+ def initialize(text)
54
+ super()
55
+ @text = text
56
+ end
57
+
58
+ def to_h
59
+ super.merge({
60
+ text: @text
61
+ })
62
+ end
63
+ end
64
+ end
65
+
66
+ module St
67
+ def self.info(text)
68
+ Showtime::session.add_element(Showtime::Components::Info.new(text))
69
+ end
70
+
71
+ def self.success(text)
72
+ Showtime::session.add_element(Showtime::Components::Success.new(text))
73
+ end
74
+
75
+ def self.warning(text)
76
+ Showtime::session.add_element(Showtime::Components::Warning.new(text))
77
+ end
78
+
79
+ def self.error(text)
80
+ Showtime::session.add_element(Showtime::Components::Error.new(text))
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,63 @@
1
+ require 'json'
2
+ require 'base64'
3
+ require 'securerandom'
4
+ require 'active_support/all'
5
+
6
+ module Showtime
7
+ module Components
8
+ class BaseComponent
9
+ attr_reader :key, :help, :errors
10
+
11
+ def initialize(key: nil, help: nil)
12
+ @key = key || generate_unique_id
13
+ @help = help
14
+ @errors = []
15
+ end
16
+
17
+ def generate_unique_id
18
+ # Generate a stable key if none provided
19
+ if @key.nil?
20
+ # Add counter to ensure uniqueness
21
+ counter = Showtime::Helpers.update_component_counter(component_type)
22
+ @key = "#{component_type}_#{counter}"
23
+ elsif !@key.to_s.start_with?("#{component_type}_")
24
+ @key = "#{component_type}_#{@key}"
25
+ end
26
+ end
27
+
28
+ def validate
29
+ # Placeholder for validation logic
30
+ # For example, check if the component has a valid key
31
+ # if @key.nil? || @key.empty?
32
+ # @errors << "Key cannot be nil or empty"
33
+ # end
34
+ # returns errors if any
35
+ @errors
36
+ end
37
+
38
+ def to_h
39
+ {
40
+ type: component_type,
41
+ key: @key,
42
+ help: @help,
43
+ errors: validate,
44
+ }
45
+ end
46
+
47
+ def value
48
+ # Retrieve value from session if key is provided
49
+ @key ? St.get(@key) : @value
50
+ end
51
+
52
+ def component_type
53
+ self.class.name.demodulize.underscore
54
+ end
55
+
56
+ private
57
+
58
+ def self.component_type
59
+ name.demodulize.underscore
60
+ end
61
+ end
62
+ end
63
+ end