cyperful 0.1.4 → 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.
- checksums.yaml +4 -4
- data/lib/cyperful/commands.rb +19 -0
- data/lib/cyperful/driver.rb +111 -46
- data/lib/cyperful/framework_injections.rb +38 -1
- data/lib/cyperful/test_parser.rb +1 -1
- data/lib/cyperful/ui_server.rb +12 -0
- data/lib/cyperful.rb +22 -7
- data/public/assets/index-CDElGKtz.css +1 -0
- data/public/assets/index-DMlaSnZ7.js +54 -0
- data/public/frame-agent.js +120 -0
- data/public/index.html +3 -5
- metadata +9 -9
- data/public/assets/index-25bd736c.css +0 -1
- data/public/assets/index-c11ede11.js +0 -40
- data/public/assets/logo-169f5e20.svg +0 -72
- data/watcher.js +0 -151
- /data/public/assets/{favicon-9f6bc28c.ico → favicon-DMdBZQlK.ico} +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 83254b2335dc0d99b443c2aad2260adc7c4f56bebaf32847a9e4ce151c03c6bc
|
4
|
+
data.tar.gz: 5a444d60e56c014569604e91162943aed5d6cb4f21655aaff8405be62d67b622
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 945bad502b63c646fd55f43a997fbb2442d27866600aaac1aeda1aabd2c218841f51a81a000ecb0ede704097bd0e53d4ad67d9b8aa9c5b501ce62dc445d1c3cf
|
7
|
+
data.tar.gz: 32ce9c41a512ebc499b1852d5cf584fdf9b86015906da4c154706f056a5c385cd341aa174388c2b3171b82c1a17f23bd012ccaa62f6382d86c070a7f91dbd467
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Cyperful
|
2
|
+
# @abstract
|
3
|
+
class AbstractCommand < StandardError
|
4
|
+
# don't print normal error/backtrace
|
5
|
+
def to_s
|
6
|
+
command_name =
|
7
|
+
self.class.name.split("::").last.chomp("Command").underscore
|
8
|
+
"(Captured cyperful command: #{command_name})"
|
9
|
+
end
|
10
|
+
def backtrace
|
11
|
+
[]
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class ResetCommand < AbstractCommand
|
16
|
+
end
|
17
|
+
class ExitCommand < AbstractCommand
|
18
|
+
end
|
19
|
+
end
|
data/lib/cyperful/driver.rb
CHANGED
@@ -18,7 +18,7 @@ class Cyperful::Driver
|
|
18
18
|
|
19
19
|
@source_filepath =
|
20
20
|
Object.const_source_location(test_class.name).first ||
|
21
|
-
(
|
21
|
+
raise("Could not find source file for #{test_class.name}")
|
22
22
|
|
23
23
|
reset_steps
|
24
24
|
|
@@ -36,11 +36,11 @@ class Cyperful::Driver
|
|
36
36
|
|
37
37
|
# Sanity check
|
38
38
|
unless @step_pausing_queue.empty?
|
39
|
-
raise "step_pausing_queue
|
39
|
+
raise "Unexpected: step_pausing_queue must be empty during setup"
|
40
40
|
end
|
41
41
|
|
42
42
|
# Wait for the user to click "Start"
|
43
|
-
step_pausing_dequeue
|
43
|
+
step_pausing_dequeue if @pause_at_step == true
|
44
44
|
end
|
45
45
|
|
46
46
|
def step_pausing_dequeue
|
@@ -81,6 +81,11 @@ class Cyperful::Driver
|
|
81
81
|
|
82
82
|
@pause_at_step = true
|
83
83
|
|
84
|
+
run_options = self.class.pop_run_options!
|
85
|
+
if run_options.key?(:pause_at_step)
|
86
|
+
@pause_at_step = run_options[:pause_at_step]
|
87
|
+
end
|
88
|
+
|
84
89
|
@test_result = nil
|
85
90
|
|
86
91
|
# reset SCREENSHOTS_DIR
|
@@ -88,10 +93,39 @@ class Cyperful::Driver
|
|
88
93
|
FileUtils.mkdir_p(SCREENSHOTS_DIR)
|
89
94
|
end
|
90
95
|
|
96
|
+
@next_run_options = {}
|
97
|
+
def self.next_run_options=(options)
|
98
|
+
@next_run_options = options
|
99
|
+
end
|
100
|
+
def self.pop_run_options!
|
101
|
+
opts = @next_run_options
|
102
|
+
@next_run_options = {}
|
103
|
+
opts
|
104
|
+
end
|
105
|
+
|
106
|
+
private def reload_const(class_name, source_path)
|
107
|
+
Object.send(:remove_const, class_name) if Object.const_defined?(class_name)
|
108
|
+
load(source_path) # reload the file
|
109
|
+
unless Object.const_defined?(class_name)
|
110
|
+
raise "Failed to reload test class: #{class_name}"
|
111
|
+
end
|
112
|
+
Object.const_get(class_name)
|
113
|
+
end
|
114
|
+
|
91
115
|
def queue_reset
|
92
|
-
|
93
|
-
|
94
|
-
|
116
|
+
at_exit do
|
117
|
+
# reload test-suite code on reset (for `setup_file_listener`)
|
118
|
+
# TODO: also reload dependent files
|
119
|
+
# NOTE: run_on_method will fail if test_name also changed
|
120
|
+
@test_class = reload_const(@test_class.name, @source_filepath)
|
121
|
+
|
122
|
+
# TODO
|
123
|
+
# if Cyperful.config.reload_source_files && defined?(Rails)
|
124
|
+
# Rails.application.reloader.reload!
|
125
|
+
# end
|
126
|
+
|
127
|
+
Minitest.run_one_method(@test_class, @test_name)
|
128
|
+
end
|
95
129
|
end
|
96
130
|
|
97
131
|
# subscribe to the execution of each line of code in the test.
|
@@ -111,28 +145,41 @@ class Cyperful::Driver
|
|
111
145
|
@tracepoint.enable
|
112
146
|
end
|
113
147
|
|
148
|
+
private def test_directory
|
149
|
+
@source_filepath.match(%r{^/.+/(?:test|spec)\b})&.[](0) ||
|
150
|
+
raise("Could not determine test directory for #{@source_filepath}")
|
151
|
+
end
|
152
|
+
|
114
153
|
# Every time a file changes the `test/` directory, reset this test
|
115
154
|
# TODO: add an option to auto-run on reload
|
116
155
|
def setup_file_listener
|
117
|
-
# TODO
|
118
|
-
|
119
|
-
#
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
156
|
+
# TODO
|
157
|
+
# if Cyperful.config.reload_source_files
|
158
|
+
# @source_file_listener = Listen.to(rails_directory) ...
|
159
|
+
# end
|
160
|
+
|
161
|
+
if Cyperful.config.reload_test_files
|
162
|
+
@file_listener&.stop
|
163
|
+
@file_listener =
|
164
|
+
Listen.to(test_directory) do |_modified, _added, _removed|
|
165
|
+
puts "Test files changed, resetting test..."
|
166
|
+
|
167
|
+
# keep the same pause state after the reload
|
168
|
+
self.class.next_run_options = { pause_at_step: @pause_at_step }
|
169
|
+
|
170
|
+
@pause_at_step = true # pause current test immediately
|
171
|
+
@step_pausing_queue.enq(:reset)
|
172
|
+
end
|
173
|
+
@file_listener.start
|
174
|
+
end
|
130
175
|
end
|
131
176
|
|
132
177
|
def print_steps
|
133
|
-
puts "#{@steps.length} steps:"
|
134
|
-
@steps.
|
135
|
-
puts "
|
178
|
+
puts "Found #{@steps.length} steps:"
|
179
|
+
@steps.each_with_index do |step, i|
|
180
|
+
puts " #{
|
181
|
+
(i + 1).to_s.rjust(2)
|
182
|
+
}. #{step[:method]}: #{step[:line]}:#{step[:column]}"
|
136
183
|
end
|
137
184
|
puts
|
138
185
|
end
|
@@ -190,11 +237,39 @@ class Cyperful::Driver
|
|
190
237
|
@ui_server.notify(steps_updated_data)
|
191
238
|
end
|
192
239
|
|
240
|
+
# called at the start of each step
|
241
|
+
def pause_on_step(step)
|
242
|
+
@current_step = step
|
243
|
+
|
244
|
+
# using `print` so we can append the step's status (see `finish_current_step`)
|
245
|
+
print("STEP #{(step[:index] + 1).to_s.rjust(2)}: #{step[:as_string]}")
|
246
|
+
|
247
|
+
if @pause_at_step == true || @pause_at_step == step[:index]
|
248
|
+
@current_step[:paused_at] = (Time.now.to_f * 1000.0).to_i
|
249
|
+
@current_step[:status] = "paused"
|
250
|
+
notify_updated_steps
|
251
|
+
|
252
|
+
# async wait for `continue_next_step`
|
253
|
+
step_pausing_dequeue
|
254
|
+
end
|
255
|
+
|
256
|
+
@current_step[:status] = "running"
|
257
|
+
@current_step[:start_at] = (Time.now.to_f * 1000.0).to_i
|
258
|
+
notify_updated_steps
|
259
|
+
end
|
260
|
+
|
261
|
+
# called at the end of each step
|
193
262
|
private def finish_current_step(error = nil)
|
194
263
|
if @current_step
|
195
264
|
@current_step[:end_at] = (Time.now.to_f * 1000.0).to_i
|
196
265
|
@current_step[:status] = !error ? "passed" : "failed"
|
197
266
|
|
267
|
+
puts(
|
268
|
+
" (#{@current_step[:end_at] - @current_step[:start_at]}ms)#{
|
269
|
+
error ? " FAILED" : ""
|
270
|
+
}",
|
271
|
+
)
|
272
|
+
|
198
273
|
# take screenshot after the step has finished
|
199
274
|
# path = File.join(SCREENSHOTS_DIR, "#{@current_step[:index]}.png")
|
200
275
|
|
@@ -210,25 +285,6 @@ class Cyperful::Driver
|
|
210
285
|
notify_updated_steps
|
211
286
|
end
|
212
287
|
|
213
|
-
def pause_on_step(step)
|
214
|
-
@current_step = step
|
215
|
-
|
216
|
-
puts "STEP: #{step[:as_string]}"
|
217
|
-
|
218
|
-
if @pause_at_step == true || @pause_at_step == step[:index]
|
219
|
-
@current_step[:paused_at] = (Time.now.to_f * 1000.0).to_i
|
220
|
-
@current_step[:status] = "paused"
|
221
|
-
notify_updated_steps
|
222
|
-
|
223
|
-
# async wait for `continue_next_step`
|
224
|
-
step_pausing_dequeue
|
225
|
-
end
|
226
|
-
|
227
|
-
@current_step[:status] = "running"
|
228
|
-
@current_step[:start_at] = (Time.now.to_f * 1000.0).to_i
|
229
|
-
notify_updated_steps
|
230
|
-
end
|
231
|
-
|
232
288
|
private def continue_next_step
|
233
289
|
@step_pausing_queue.enq(:next)
|
234
290
|
end
|
@@ -283,7 +339,15 @@ class Cyperful::Driver
|
|
283
339
|
[abs_url, display_url]
|
284
340
|
end
|
285
341
|
|
286
|
-
|
342
|
+
def self.load_frame_agent_js
|
343
|
+
return @frame_agent_js if defined?(@frame_agent_js)
|
344
|
+
|
345
|
+
@frame_agent_js =
|
346
|
+
File.read(File.join(Cyperful::ROOT_DIR, "public/frame-agent.js")).sub(
|
347
|
+
"__CYPERFUL_CONFIG__",
|
348
|
+
{ CYPERFUL_ORIGIN: "http://localhost:#{Cyperful.config.port}" }.to_json,
|
349
|
+
)
|
350
|
+
end
|
287
351
|
|
288
352
|
private def skip_multi_sessions
|
289
353
|
unless Capybara.current_session == @session
|
@@ -307,10 +371,10 @@ class Cyperful::Driver
|
|
307
371
|
|
308
372
|
@session.execute_script("window.location.href = #{abs_url.to_json}")
|
309
373
|
|
310
|
-
# inject the
|
374
|
+
# inject the frame-agent script into the page being tested.
|
311
375
|
# this script will notify the Cyperful UI for events like:
|
312
376
|
# console logs, network requests, client navigations, errors, etc.
|
313
|
-
@session.execute_script(
|
377
|
+
@session.execute_script(Cyperful::Driver.load_frame_agent_js) # ~9ms empirically
|
314
378
|
|
315
379
|
true
|
316
380
|
end
|
@@ -323,7 +387,7 @@ class Cyperful::Driver
|
|
323
387
|
end
|
324
388
|
|
325
389
|
def setup_api_server
|
326
|
-
@ui_server = Cyperful::UiServer.new(port:
|
390
|
+
@ui_server = Cyperful::UiServer.new(port: Cyperful.config.port)
|
327
391
|
|
328
392
|
@cyperful_origin = @ui_server.url_origin
|
329
393
|
|
@@ -363,7 +427,7 @@ class Cyperful::Driver
|
|
363
427
|
@tracepoint = nil
|
364
428
|
|
365
429
|
if error&.is_a?(Cyperful::ResetCommand)
|
366
|
-
puts "\
|
430
|
+
puts "\nResetting test (ignore any error logs)..."
|
367
431
|
|
368
432
|
@ui_server.notify(nil) # `break` out of the `loop` (see `UiServer#socket_open`)
|
369
433
|
|
@@ -395,6 +459,7 @@ class Cyperful::Driver
|
|
395
459
|
@ui_server.notify(nil) # `break` out of the `loop` (see `UiServer#socket_open`)
|
396
460
|
|
397
461
|
puts "Cyperful teardown complete. Waiting for command..."
|
462
|
+
# NOTE: this will raise an `Interrupt` if the user Ctrl+C's here
|
398
463
|
command = @step_pausing_queue.deq
|
399
464
|
queue_reset if command == :reset
|
400
465
|
ensure
|
@@ -1,3 +1,5 @@
|
|
1
|
+
require "action_dispatch/system_testing/driver"
|
2
|
+
|
1
3
|
# we need to override the some Capybara::Session methods because they
|
2
4
|
# control the top-level browser window, but we want them
|
3
5
|
# to control the iframe instead
|
@@ -51,13 +53,48 @@ module Cyperful::SystemTestHelper
|
|
51
53
|
Cyperful.teardown(error)
|
52
54
|
super
|
53
55
|
end
|
56
|
+
|
57
|
+
# Disable default screenshot on failure b/c we handle them ourselves.
|
58
|
+
# https://github.com/rails/rails/blob/main/actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb#L156
|
59
|
+
def take_failed_screenshot
|
60
|
+
nil
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
module PrependSystemTestingDriver
|
65
|
+
def initialize(...)
|
66
|
+
super(...)
|
67
|
+
|
68
|
+
prev_capabilities = @capabilities
|
69
|
+
@capabilities =
|
70
|
+
proc do |driver_opts|
|
71
|
+
prev_capabilities&.call(driver_opts)
|
72
|
+
|
73
|
+
next unless driver_opts.respond_to?(:add_argument)
|
74
|
+
|
75
|
+
# this assumes Selenium and Chrome:
|
76
|
+
|
77
|
+
# so user isn't prompted when we start recording video w/ MediaStream
|
78
|
+
driver_opts.add_argument("--auto-accept-this-tab-capture")
|
79
|
+
driver_opts.add_argument("--use-fake-ui-for-media-stream")
|
80
|
+
|
81
|
+
# make sure we're not in headless mode
|
82
|
+
driver_opts.args.delete("--headless")
|
83
|
+
driver_opts.args.delete("--headless=new")
|
84
|
+
end
|
85
|
+
end
|
54
86
|
end
|
87
|
+
ActionDispatch::SystemTesting::Driver.prepend(PrependSystemTestingDriver)
|
88
|
+
|
89
|
+
# if defined?(Minitest::Test)
|
90
|
+
# Minitest::Test::PASSTHROUGH_EXCEPTIONS << Cyperful::AbstractCommand
|
91
|
+
# end
|
55
92
|
|
56
93
|
# we need to allow the iframe to be embedded in the cyperful server
|
57
94
|
# TODO: use Rack middleware instead to support non-Rails apps
|
58
95
|
if defined?(Rails)
|
59
96
|
Rails.application.config.content_security_policy do |policy|
|
60
|
-
policy.frame_ancestors(:self, "localhost
|
97
|
+
policy.frame_ancestors(:self, "localhost:#{Cyperful.config.port}")
|
61
98
|
end
|
62
99
|
else
|
63
100
|
warn "Cyperful: Rails not detected, skipping content_security_policy fix.\nThe Cyperful UI may not work correctly."
|
data/lib/cyperful/test_parser.rb
CHANGED
@@ -74,7 +74,7 @@ class Cyperful::TestParser
|
|
74
74
|
test_string = node.children[0].children[2].children[0]
|
75
75
|
|
76
76
|
# https://github.com/rails/rails/blob/66676ce499a32e4c62220bd05f8ee2cdf0e15f0c/activesupport/lib/active_support/testing/declarative.rb#L14C23-L14C61
|
77
|
-
test_method = "test_#{test_string.gsub(/\s+/, "_")}"
|
77
|
+
test_method = :"test_#{test_string.gsub(/\s+/, "_")}"
|
78
78
|
|
79
79
|
block_node = node.children[2]
|
80
80
|
[test_method, block_node]
|
data/lib/cyperful/ui_server.rb
CHANGED
@@ -106,6 +106,18 @@ class Cyperful::UiServer
|
|
106
106
|
|
107
107
|
res.status = 204
|
108
108
|
end
|
109
|
+
|
110
|
+
@server.mount_proc("/api/config") do |req, res|
|
111
|
+
if req.request_method != "GET"
|
112
|
+
res.body = "Only POST allowed"
|
113
|
+
res.status = 405
|
114
|
+
next
|
115
|
+
end
|
116
|
+
|
117
|
+
res.body = Cyperful.config.to_h.to_json
|
118
|
+
res["Content-Type"] = "application/json"
|
119
|
+
res.status = 200
|
120
|
+
end
|
109
121
|
end
|
110
122
|
|
111
123
|
def start_async
|
data/lib/cyperful.rb
CHANGED
@@ -6,6 +6,27 @@ module Cyperful
|
|
6
6
|
|
7
7
|
@current = nil
|
8
8
|
|
9
|
+
class Config < Struct.new(
|
10
|
+
:port,
|
11
|
+
:auto_run_on_reload,
|
12
|
+
:reload_test_files,
|
13
|
+
# :reload_source_files, # not implemented yet
|
14
|
+
:history_recording, # EXPERIMENTAL
|
15
|
+
keyword_init: true,
|
16
|
+
)
|
17
|
+
def initialize
|
18
|
+
super(
|
19
|
+
port: 3004,
|
20
|
+
auto_run_on_reload: true,
|
21
|
+
reload_test_files: true,
|
22
|
+
history_recording: true,
|
23
|
+
)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
def self.config
|
27
|
+
@config ||= Config.new
|
28
|
+
end
|
29
|
+
|
9
30
|
def self.current
|
10
31
|
@current
|
11
32
|
end
|
@@ -34,13 +55,7 @@ module Cyperful
|
|
34
55
|
end
|
35
56
|
end
|
36
57
|
|
37
|
-
|
38
|
-
end
|
39
|
-
class Cyperful::ResetCommand < Cyperful::AbstractCommand
|
40
|
-
end
|
41
|
-
class Cyperful::ExitCommand < Cyperful::AbstractCommand
|
42
|
-
end
|
43
|
-
|
58
|
+
require "cyperful/commands"
|
44
59
|
require "cyperful/test_parser"
|
45
60
|
require "cyperful/ui_server"
|
46
61
|
require "cyperful/driver"
|
@@ -0,0 +1 @@
|
|
1
|
+
*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.pointer-events-none{pointer-events:none}.invisible{visibility:hidden}.static{position:static}.absolute{position:absolute}.relative{position:relative}.bottom-1{bottom:.25rem}.left-0{left:0}.left-1{left:.25rem}.right-0{right:0}.right-1{right:.25rem}.right-4{right:1rem}.top-0{top:0}.top-1{top:.25rem}.m-4{margin:1rem}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.ml-1{margin-left:.25rem}.mr-2{margin-right:.5rem}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.hidden{display:none}.h-10{height:2.5rem}.h-14{height:3.5rem}.h-2{height:.5rem}.h-2\.5{height:.625rem}.h-5{height:1.25rem}.h-7{height:1.75rem}.h-8{height:2rem}.h-full{height:100%}.h-screen{height:100vh}.max-h-16{max-height:4rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-full{width:100%}.flex-1{flex:1 1 0%}.basis-96{flex-basis:24rem}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes spin{to{transform:rotate(360deg)}}.animate-spin{animation:spin 1s linear infinite}.cursor-pointer{cursor:pointer}.resize{resize:both}.flex-col{flex-direction:column}.items-start{align-items:flex-start}.items-center{align-items:center}.items-stretch{align-items:stretch}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(.5rem * var(--tw-space-x-reverse));margin-left:calc(.5rem * calc(1 - var(--tw-space-x-reverse)))}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(1rem * var(--tw-space-x-reverse));margin-left:calc(1rem * calc(1 - var(--tw-space-x-reverse)))}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-md{border-radius:.375rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-t{border-top-width:1px}.border-blue-600{--tw-border-opacity: 1;border-color:rgb(37 99 235 / var(--tw-border-opacity))}.border-gray-200{--tw-border-opacity: 1;border-color:rgb(229 231 235 / var(--tw-border-opacity))}.border-gray-300{--tw-border-opacity: 1;border-color:rgb(209 213 219 / var(--tw-border-opacity))}.border-green-600{--tw-border-opacity: 1;border-color:rgb(22 163 74 / var(--tw-border-opacity))}.border-orange-500{--tw-border-opacity: 1;border-color:rgb(249 115 22 / var(--tw-border-opacity))}.border-red-600{--tw-border-opacity: 1;border-color:rgb(220 38 38 / var(--tw-border-opacity))}.border-slate-800{--tw-border-opacity: 1;border-color:rgb(30 41 59 / var(--tw-border-opacity))}.border-yellow-600{--tw-border-opacity: 1;border-color:rgb(202 138 4 / var(--tw-border-opacity))}.bg-\[\#121b2e\]{--tw-bg-opacity: 1;background-color:rgb(18 27 46 / var(--tw-bg-opacity))}.bg-blue-500{--tw-bg-opacity: 1;background-color:rgb(59 130 246 / var(--tw-bg-opacity))}.bg-gray-100{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity))}.bg-gray-200{--tw-bg-opacity: 1;background-color:rgb(229 231 235 / var(--tw-bg-opacity))}.bg-gray-50{--tw-bg-opacity: 1;background-color:rgb(249 250 251 / var(--tw-bg-opacity))}.bg-gray-500{--tw-bg-opacity: 1;background-color:rgb(107 114 128 / var(--tw-bg-opacity))}.bg-green-500{--tw-bg-opacity: 1;background-color:rgb(34 197 94 / var(--tw-bg-opacity))}.bg-orange-400{--tw-bg-opacity: 1;background-color:rgb(251 146 60 / var(--tw-bg-opacity))}.bg-red-500{--tw-bg-opacity: 1;background-color:rgb(239 68 68 / var(--tw-bg-opacity))}.bg-slate-950{--tw-bg-opacity: 1;background-color:rgb(2 6 23 / var(--tw-bg-opacity))}.bg-yellow-500{--tw-bg-opacity: 1;background-color:rgb(234 179 8 / var(--tw-bg-opacity))}.p-2{padding:.5rem}.p-4{padding:1rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.pt-0{padding-top:0}.text-center{text-align:center}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-\[1\.25em\]{font-size:1.25em}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-semibold{font-weight:600}.capitalize{text-transform:capitalize}.text-black{--tw-text-opacity: 1;color:rgb(0 0 0 / var(--tw-text-opacity))}.text-blue-500{--tw-text-opacity: 1;color:rgb(59 130 246 / var(--tw-text-opacity))}.text-gray-400{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity))}.text-gray-600{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity))}.text-green-100{--tw-text-opacity: 1;color:rgb(220 252 231 / var(--tw-text-opacity))}.text-green-500{--tw-text-opacity: 1;color:rgb(34 197 94 / var(--tw-text-opacity))}.text-red-100{--tw-text-opacity: 1;color:rgb(254 226 226 / var(--tw-text-opacity))}.text-red-400{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity))}.text-red-500{--tw-text-opacity: 1;color:rgb(239 68 68 / var(--tw-text-opacity))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.text-yellow-500{--tw-text-opacity: 1;color:rgb(234 179 8 / var(--tw-text-opacity))}.opacity-50{opacity:.5}.shadow{--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / .1), 0 1px 2px -1px rgb(0 0 0 / .1);--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-inner{--tw-shadow: inset 0 2px 4px 0 rgb(0 0 0 / .05);--tw-shadow-colored: inset 0 2px 4px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-md{--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / .1), 0 2px 4px -2px rgb(0 0 0 / .1);--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition-\[width\]{transition-property:width;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-100{transition-duration:.1s}@keyframes rotate{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.Logo g{transform-origin:50% 50%;animation:rotate 5s linear infinite;animation-play-state:paused}.Logo.Logo--animating g{animation-play-state:running}.Logo g:nth-child(1){animation-duration:1s}.Logo g:nth-child(2){animation-duration:1.5s}.Logo g:nth-child(3){animation-duration:3s}.Logo g:nth-child(4){animation-duration:4s}.hover\:bg-blue-600:hover{--tw-bg-opacity: 1;background-color:rgb(37 99 235 / var(--tw-bg-opacity))}.hover\:bg-gray-100:hover{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity))}.hover\:bg-gray-300:hover{--tw-bg-opacity: 1;background-color:rgb(209 213 219 / var(--tw-bg-opacity))}.hover\:bg-green-600:hover{--tw-bg-opacity: 1;background-color:rgb(22 163 74 / var(--tw-bg-opacity))}.hover\:bg-orange-500:hover{--tw-bg-opacity: 1;background-color:rgb(249 115 22 / var(--tw-bg-opacity))}.hover\:bg-red-600:hover{--tw-bg-opacity: 1;background-color:rgb(220 38 38 / var(--tw-bg-opacity))}.hover\:bg-yellow-600:hover{--tw-bg-opacity: 1;background-color:rgb(202 138 4 / var(--tw-bg-opacity))}.hover\:underline:hover{text-decoration-line:underline}.group:hover .group-hover\:block{display:block}
|