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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +179 -0
- data/bin/showtime +47 -0
- data/lib/showtime/app.rb +399 -0
- data/lib/showtime/charts.rb +229 -0
- data/lib/showtime/component_registry.rb +38 -0
- data/lib/showtime/components/Components.md +309 -0
- data/lib/showtime/components/alerts.rb +83 -0
- data/lib/showtime/components/base.rb +63 -0
- data/lib/showtime/components/charts.rb +119 -0
- data/lib/showtime/components/data.rb +328 -0
- data/lib/showtime/components/inputs.rb +390 -0
- data/lib/showtime/components/layout.rb +135 -0
- data/lib/showtime/components/media.rb +73 -0
- data/lib/showtime/components/sidebar.rb +130 -0
- data/lib/showtime/components/text.rb +156 -0
- data/lib/showtime/components.rb +18 -0
- data/lib/showtime/compute_tracker.rb +21 -0
- data/lib/showtime/helpers.rb +53 -0
- data/lib/showtime/logger.rb +143 -0
- data/lib/showtime/public/.vite/manifest.json +34 -0
- data/lib/showtime/public/assets/antd-3aDVoXqG.js +447 -0
- data/lib/showtime/public/assets/charts-iowb_sWQ.js +3858 -0
- data/lib/showtime/public/assets/index-B2b3lWS5.js +43 -0
- data/lib/showtime/public/assets/index-M6NVamDM.css +1 -0
- data/lib/showtime/public/assets/react-BE6xecJX.js +32 -0
- data/lib/showtime/public/index.html +19 -0
- data/lib/showtime/public/letter.png +0 -0
- data/lib/showtime/public/logo.png +0 -0
- data/lib/showtime/release.rb +108 -0
- data/lib/showtime/session.rb +131 -0
- data/lib/showtime/version.rb +3 -0
- data/lib/showtime/views/index.erb +32 -0
- data/lib/showtime.rb +157 -0
- 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
|