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,390 @@
1
+ require_relative 'base'
2
+ require 'tempfile'
3
+ require 'base64'
4
+ require 'ostruct'
5
+
6
+ module Showtime
7
+ module Components
8
+ class Button < BaseComponent
9
+ attr_reader :label, :value
10
+
11
+ def initialize(label, key: nil, help: nil)
12
+ super(key: key, help: help)
13
+ @label = label
14
+
15
+ # Initialize button state
16
+ @value = St.get(@key, false)
17
+ end
18
+
19
+ def clicked?
20
+ St.get(@key) == true
21
+ end
22
+
23
+ def value
24
+ St.get(@key) == true
25
+ end
26
+
27
+ def to_h
28
+ # Get the latest value from the session
29
+ @value = St.get(@key) == true
30
+
31
+ super.merge({
32
+ label: @label,
33
+ value: @value
34
+ })
35
+ end
36
+ end
37
+
38
+ class Checkbox < BaseComponent
39
+ attr_reader :label, :value
40
+
41
+ def initialize(label, value: false, key: nil, help: nil)
42
+ super(key: key, help: help)
43
+ @label = label
44
+ @value = value
45
+ end
46
+
47
+ def to_h
48
+ super.merge({
49
+ label: @label,
50
+ value: @value
51
+ })
52
+ end
53
+ end
54
+
55
+ class TextInput < BaseComponent
56
+ attr_reader :label
57
+
58
+ def initialize(label, value: "", key: nil, help: nil)
59
+ super(key: key, help: help)
60
+ @label = label
61
+ @value = value
62
+ end
63
+
64
+ def to_h
65
+ super.merge({
66
+ label: @label,
67
+ value: value
68
+ })
69
+ end
70
+ end
71
+
72
+ class NumberInput < BaseComponent
73
+ attr_reader :label, :min_value, :max_value, :step
74
+
75
+ def initialize(label, min_value: nil, max_value: nil, value: nil, step: 1, key: nil, help: nil)
76
+ super(key: key, help: help)
77
+ @label = label
78
+ @min_value = min_value
79
+ @max_value = max_value
80
+ @value = value || min_value || 0
81
+ @step = step
82
+ end
83
+
84
+ def to_h
85
+ super.merge({
86
+ label: @label,
87
+ min_value: @min_value,
88
+ max_value: @max_value,
89
+ value: value,
90
+ step: @step
91
+ })
92
+ end
93
+ end
94
+
95
+ class TextArea < BaseComponent
96
+ attr_reader :label, :height
97
+
98
+ def initialize(label, value: "", height: nil, key: nil, help: nil, **kwargs)
99
+ super(key: key, help: help)
100
+ @label = label
101
+ # Ensure @value is initialized even if not using session or no initial value in session
102
+ @value = St.get(@key) || value
103
+ @height = height
104
+ @kwargs = kwargs
105
+ end
106
+
107
+ def to_h
108
+ super.merge({
109
+ label: @label,
110
+ value: value, # Use the new value getter
111
+ height: @height
112
+ }).merge(@kwargs)
113
+ end
114
+ end
115
+
116
+ class DateInput < BaseComponent
117
+ attr_reader :label, :value, :min_value, :max_value, :format
118
+
119
+ def initialize(label, value: nil, min_value: nil, max_value: nil, format: 'YYYY-MM-DD', key: nil, help: nil)
120
+ super(key: key, help: help)
121
+ @label = label
122
+ @min_value = min_value
123
+ @max_value = max_value
124
+ @format = format
125
+
126
+ # Initialize value in session
127
+ initial_value = value || Date.today
128
+ St.set(@key, initial_value) if St.get(@key).nil?
129
+ end
130
+
131
+ def value
132
+ stored_value = St.get(@key)
133
+ return Date.today if stored_value.nil?
134
+
135
+ # Convert ISO8601 string back to Date if needed
136
+ stored_value.is_a?(String) ? Date.parse(stored_value) : stored_value
137
+ end
138
+
139
+ def to_h
140
+ current_value = value
141
+ super.merge({
142
+ label: @label,
143
+ value: current_value.is_a?(Date) ? current_value.iso8601 : current_value,
144
+ min_value: @min_value&.is_a?(Date) ? @min_value.iso8601 : @min_value,
145
+ max_value: @max_value&.is_a?(Date) ? @max_value.iso8601 : @max_value,
146
+ format: @format
147
+ })
148
+ end
149
+ end
150
+
151
+ class FileUploader < BaseComponent
152
+ attr_reader :label, :type, :accept_multiple_files, :max_size, :wide
153
+
154
+ DEFAULT_MAX_SIZE = 200 * 1024 * 1024 # 200 MB
155
+
156
+ def initialize(label, type: nil, accept_multiple_files: false, max_size: DEFAULT_MAX_SIZE, key: nil, help: nil, wide: false)
157
+ super(key: key, help: help)
158
+ @label = label
159
+ @type = type
160
+ @accept_multiple_files = accept_multiple_files
161
+ @max_size = max_size
162
+ @key ||= "file_uploader_#{label.downcase.gsub(/\W+/, '_')}"
163
+ @wide = wide
164
+ end
165
+
166
+ def value
167
+ file_data = St.get(@key)
168
+ return nil if file_data.nil?
169
+
170
+ size = file_data['size']
171
+ name = file_data['name']
172
+ content_type = file_data['type']
173
+ file_path = file_data['path']
174
+
175
+ return nil if size > @max_size
176
+
177
+ # Create a file struct that reads from the server-side file path
178
+ OpenStruct.new(
179
+ name: name,
180
+ size: size,
181
+ content_type: content_type,
182
+ path: file_path,
183
+ read: -> {
184
+ # Read the file directly from the server's filesystem
185
+ # The path is now a real file path on the server
186
+ File.binread(file_path)
187
+ },
188
+ temp_path: file_path
189
+ )
190
+ end
191
+
192
+ def self.finalizer(path)
193
+ proc { File.delete(path) if File.exist?(path) }
194
+ end
195
+
196
+ # Convenience method to read content regardless of storage type
197
+ def read
198
+ uploaded_file = value
199
+ uploaded_file&.read&.call # Call the read lambda on the OpenStruct
200
+ end
201
+
202
+ # Convenience method to read content as a Base64 encoded string
203
+ # Accepts the result of self.value as an argument to avoid re-fetching from session
204
+ def read_as_base64(uploaded_file_details = nil)
205
+ # If no details passed, try to get them via self.value (maintaining old behavior if called directly)
206
+ # However, the preferred way will be to pass the result of self.value
207
+ current_uploaded_file = uploaded_file_details || value
208
+ return nil unless current_uploaded_file && current_uploaded_file.path && current_uploaded_file.content_type
209
+
210
+ # Ensure the temporary file exists before trying to read it
211
+ return nil unless File.exist?(current_uploaded_file.path)
212
+
213
+ binary_data = File.binread(current_uploaded_file.path)
214
+ "data:#{current_uploaded_file.content_type};base64,#{Base64.strict_encode64(binary_data)}"
215
+ end
216
+
217
+ def to_h
218
+ mime_types = if @type.is_a?(Array)
219
+ @type
220
+ elsif @type.is_a?(String)
221
+ [@type]
222
+ elsif @type.nil?
223
+ []
224
+ end
225
+
226
+ super.merge({
227
+ label: @label,
228
+ accept: mime_types,
229
+ multiple: @accept_multiple_files,
230
+ max_size: @max_size,
231
+ value: St.get(@key),
232
+ wide: @wide
233
+ })
234
+ end
235
+ end
236
+
237
+ class Select < BaseComponent
238
+ attr_reader :label, :options
239
+
240
+ def initialize(label, options, value: nil, key: nil, help: nil)
241
+ super(key: key, help: help)
242
+ @label = label
243
+ @options = map_options(options)
244
+ # Store initial value in session if key is provided and no value exists
245
+ if @key && St.get(@key).nil?
246
+ St.set(@key, value)
247
+ end
248
+ # Ensure @value is initialized even if not using session or no initial value in session
249
+ @value = St.get(@key) || value
250
+ end
251
+
252
+ def value
253
+ # Retrieve value from session if key is provided
254
+ @key ? St.get(@key) : @value
255
+ end
256
+
257
+ def map_options(options)
258
+ # Convert options to a hash format if they are not already
259
+ # This assumes options are either strings or hashes
260
+ if !options.is_a?(Array)
261
+ @errors << "Options must be an array"
262
+ end
263
+ options.map do |option|
264
+ if option.is_a?(Hash)
265
+ option.transform_keys(&:to_s)
266
+ else
267
+ { value: option, label: option }
268
+ end
269
+ end
270
+
271
+ end
272
+
273
+ def to_h
274
+ super.merge({
275
+ label: @label,
276
+ options: @options,
277
+ value: value
278
+ })
279
+ end
280
+ end
281
+
282
+ class Multiselect < BaseComponent
283
+ attr_reader :label, :options, :value
284
+
285
+ def initialize(label, options, default: [], key: nil, help: nil)
286
+ super(key: key, help: help)
287
+ @label = label
288
+ @options = options
289
+ @value = default & options # Intersection to ensure only valid options
290
+ end
291
+
292
+ def to_h
293
+ super.merge({
294
+ label: @label,
295
+ options: @options,
296
+ value: @value
297
+ })
298
+ end
299
+ end
300
+
301
+ class Slider < BaseComponent
302
+ attr_reader :label, :min_value, :max_value, :value, :step
303
+
304
+ def initialize(label, min_value: 0, max_value: 100, value: nil, step: 1, key: nil, help: nil)
305
+ super(key: key, help: help)
306
+ @label = label
307
+ @min_value = min_value
308
+ @max_value = max_value
309
+ @value = value || min_value
310
+ @step = step
311
+ end
312
+
313
+ def to_h
314
+ super.merge({
315
+ label: @label,
316
+ min_value: @min_value,
317
+ max_value: @max_value,
318
+ value: @value,
319
+ step: @step
320
+ })
321
+ end
322
+ end
323
+
324
+ class Radio < BaseComponent
325
+ attr_reader :label, :options, :index, :value
326
+
327
+ def initialize(label, options, index: 0, key: nil, help: nil)
328
+ super(key: key, help: help)
329
+ @label = label
330
+ @options = options
331
+ @index = [index, options.length - 1].min
332
+ @value = options[@index] if options.any?
333
+ end
334
+
335
+ def to_h
336
+ super.merge({
337
+ label: @label,
338
+ options: @options,
339
+ value: @value
340
+ })
341
+ end
342
+ end
343
+ end
344
+
345
+ module St
346
+ def self.button(label, key: nil, help: nil)
347
+ Showtime::session.add_element(Showtime::Components::Button.new(label, key: key, help: help))
348
+ end
349
+
350
+ def self.checkbox(label, value: false, key: nil, help: nil)
351
+ Showtime::session.add_element(Showtime::Components::Checkbox.new(label, value: value, key: key, help: help))
352
+ end
353
+
354
+ def self.text_input(label, value: "", key: nil, help: nil)
355
+ Showtime::session.add_element(Showtime::Components::TextInput.new(label, value: value, key: key, help: help))
356
+ end
357
+
358
+ def self.number_input(label, min_value: nil, max_value: nil, value: nil, step: 1, key: nil, help: nil)
359
+ Showtime::session.add_element(Showtime::Components::NumberInput.new(label, min_value: min_value, max_value: max_value, value: value, step: step, key: key, help: help))
360
+ end
361
+
362
+ def self.text_area(label, value: "", height: nil, key: nil, help: nil, **kwargs)
363
+ Showtime::session.add_element(Showtime::Components::TextArea.new(label, value: value, height: height, key: key, help: help, **kwargs))
364
+ end
365
+
366
+ def self.date_input(label, value: nil, min_value: nil, max_value: nil, format: 'YYYY-MM-DD', key: nil, help: nil)
367
+ Showtime::session.add_element(Showtime::Components::DateInput.new(label, value: value, min_value: min_value, max_value: max_value, format: format, key: key, help: help))
368
+ end
369
+
370
+ def self.file_uploader(label, type: nil, accept_multiple_files: false, max_size: Showtime::Components::FileUploader::DEFAULT_MAX_SIZE, key: nil, help: nil, wide: false)
371
+ Showtime::session.add_element(Showtime::Components::FileUploader.new(label, type: type, accept_multiple_files: accept_multiple_files, max_size: max_size, key: key, help: help, wide: wide))
372
+ end
373
+
374
+ def self.select(label, options, value: nil, key: nil, help: nil)
375
+ Showtime::session.add_element(Showtime::Components::Select.new(label, options, value: value, key: key, help: help))
376
+ end
377
+
378
+ def self.multiselect(label, options, default: [], key: nil, help: nil)
379
+ Showtime::session.add_element(Showtime::Components::Multiselect.new(label, options, default: default, key: key, help: help))
380
+ end
381
+
382
+ def self.slider(label, min_value: 0, max_value: 100, value: nil, step: 1, key: nil, help: nil)
383
+ Showtime::session.add_element(Showtime::Components::Slider.new(label, min_value: min_value, max_value: max_value, value: value, step: step, key: key, help: help))
384
+ end
385
+
386
+ def self.radio(label, options, index: 0, key: nil, help: nil)
387
+ Showtime::session.add_element(Showtime::Components::Radio.new(label, options, index: index, key: key, help: help))
388
+ end
389
+ end
390
+ end
@@ -0,0 +1,135 @@
1
+ require_relative 'base'
2
+
3
+ module Showtime
4
+ module Components
5
+ class ContainerComponent < BaseComponent
6
+ attr_reader :children
7
+
8
+ def initialize(key: nil, help: nil)
9
+ super(key: key, help: help)
10
+ @children = []
11
+ end
12
+
13
+ def to_h
14
+ super.merge({
15
+ children: @children.map(&:to_h)
16
+ })
17
+ end
18
+ end
19
+
20
+ class Container < ContainerComponent
21
+ attr_reader :title, :border, :grow
22
+
23
+ def initialize(title: nil, border: true, grow: false, key: nil, help: nil)
24
+ super(key: key, help: help)
25
+ @title = title
26
+ @border = border
27
+ @grow = grow
28
+ end
29
+
30
+ def to_h
31
+ super.merge({
32
+ title: @title,
33
+ border: @border,
34
+ grow: @grow
35
+ })
36
+ end
37
+ end
38
+
39
+ class SplitLayout < ContainerComponent
40
+ attr_reader :direction, :sizes, :height
41
+
42
+ def initialize(direction: 'horizontal', sizes: nil, height: nil, ttype: 'card', key: nil, help: nil)
43
+ super(key: key, help: help)
44
+ @direction = direction
45
+ @sizes = sizes
46
+ @height = height
47
+ @ttype = ttype
48
+ end
49
+
50
+ def to_h
51
+ super.merge({
52
+ direction: @direction,
53
+ sizes: @sizes,
54
+ height: @height,
55
+ ttype: @ttype
56
+ })
57
+ end
58
+ end
59
+
60
+ class Collapse < ContainerComponent
61
+ attr_reader :label, :expanded
62
+
63
+ def initialize(label, expanded: false, key: nil, help: nil)
64
+ super(key: key, help: help)
65
+ @label = label
66
+ @expanded = expanded
67
+ St.set(@key, expanded) if St.get(@key).nil?
68
+ end
69
+
70
+ def value
71
+ St.get(@key)
72
+ end
73
+
74
+ def to_h
75
+ super.merge({
76
+ label: @label,
77
+ expanded: value
78
+ })
79
+ end
80
+ end
81
+ end
82
+
83
+ module St
84
+ def self.container(title: nil, border: true, grow: false, key: nil, help: nil, &block)
85
+ container = Showtime::Components::Container.new(title: title, border: border, grow: grow, key: key, help: help)
86
+ element = Showtime::session.add_element(container)
87
+
88
+ if block_given?
89
+ Showtime::session.with_container(container) do
90
+ yield
91
+ end
92
+ end
93
+
94
+ element
95
+ end
96
+
97
+ # Build a responsive split layout container.
98
+ #
99
+ # @param direction [String]
100
+ # 'horizontal' (default) or 'vertical'
101
+ # @param sizes [Array<Integer>, nil]
102
+ # column sizes for each child. When provided in horizontal mode,
103
+ # they are interpreted on a 24-column grid (Ant Design) and should sum to 24
104
+ # so sibling panels align; when omitted, equal distribution across 24 columns is used.
105
+ # @param height [Integer, String, nil] optional height for vertical layouts (number => px, string => CSS value)
106
+ # @param type [String] visual style for the container (card/ghost/success/info/warning/error)
107
+ # @param key [String, nil] component key
108
+ # @param help [String, nil] optional tooltip/help text
109
+ def self.split_layout(direction: 'horizontal', sizes: nil, height: nil, type: 'card', key: nil, help: nil, &block)
110
+ container = Showtime::Components::SplitLayout.new(direction: direction, sizes: sizes, height: height, ttype: type, key: key, help: help)
111
+ element = Showtime::session.add_element(container)
112
+
113
+ if block_given?
114
+ Showtime::session.with_container(container) do
115
+ yield
116
+ end
117
+ end
118
+
119
+ element
120
+ end
121
+
122
+ def self.collapse(label, expanded: false, key: nil, help: nil, &block)
123
+ container = Showtime::Components::Collapse.new(label, expanded: expanded, key: key, help: help)
124
+ element = Showtime::session.add_element(container)
125
+
126
+ if block_given?
127
+ Showtime::session.with_container(container) do
128
+ yield
129
+ end
130
+ end
131
+
132
+ element
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,73 @@
1
+ require 'pathname'
2
+ require 'base64'
3
+ require_relative 'base'
4
+ require_relative '../helpers'
5
+
6
+ module Showtime
7
+ module Components
8
+ class Image < BaseComponent
9
+ attr_reader :src, :caption, :width, :use_column_width
10
+
11
+ def initialize(src, caption: nil, width: nil, use_column_width: true, key: nil, help: nil)
12
+ super(key: key, help: help)
13
+ @src = resolve_src(src)
14
+ @caption = caption
15
+ @width = width
16
+ @use_column_width = use_column_width
17
+ end
18
+
19
+ def resolve_src(src)
20
+ # Only process if it's a string, not a URL, not a data URI, and not already absolute
21
+ if src.is_a?(String) &&
22
+ !src.start_with?('http://', 'https://') &&
23
+ !src.include?(';base64,') &&
24
+ !(defined?(Pathname) && Pathname.new(src).absolute?) # Check Pathname.new(src).absolute? only if Pathname is defined
25
+ # The '1' offset is because St.image calls Image.new, which then calls resolve_src.
26
+ # We want the path relative to the script calling St.image.
27
+ return Showtime::Helpers.absolute_path(src)
28
+ end
29
+ src # Return original src if it's a URL, data URI, absolute, or not a string
30
+ end
31
+
32
+ def to_h
33
+ Showtime::Logger.debug("Image src: #{@src}")
34
+ image_data = if @src.is_a?(String) && File.exist?(@src)
35
+ # It's a file path
36
+ mime_type = case File.extname(@src).downcase
37
+ when '.png' then 'image/png'
38
+ when '.jpg', '.jpeg' then 'image/jpeg'
39
+ when '.gif' then 'image/gif'
40
+ when '.svg' then 'image/svg+xml'
41
+ else 'application/octet-stream'
42
+ end
43
+ "data:#{mime_type};base64,#{Base64.strict_encode64(File.read(@src))}"
44
+ elsif @src.is_a?(String) && @src.start_with?('http')
45
+ # It's a URL
46
+ @src
47
+ elsif @src.is_a?(String) && @src.include?(';base64,')
48
+ # It's already a data URI
49
+ @src
50
+ else
51
+ # Assume it's binary data (or needs default handling if not a recognized string type)
52
+ # This branch might need further refinement if @src can be non-string binary
53
+ Showtime::Logger.warn("Image src is not a file path, URL, or data URI. Attempting to treat as binary. Src: #{@src.class}")
54
+ "data:image/png;base64,#{Base64.strict_encode64(@src.to_s)}" # Ensure @src is stringified if it's raw binary
55
+ end
56
+
57
+ super.merge({
58
+ src: image_data,
59
+ caption: @caption,
60
+ width: @width,
61
+ use_column_width: @use_column_width
62
+ })
63
+ end
64
+ end
65
+ end
66
+
67
+ module St
68
+ def self.image(src, caption: nil, width: nil, use_column_width: false, key: nil, help: nil)
69
+ component = Components::Image.new(src, caption: caption, width: width, use_column_width: use_column_width, key: key, help: help)
70
+ Showtime.session.add_element(component)
71
+ end
72
+ end
73
+ end