opal-vite 0.2.19 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6a6fe0d4b79fe6f2f19296adc07c985911e700798992b4db56ee884b6fd43d72
4
- data.tar.gz: 3d0543a0e7bd1d719a85f68c0429a2b8bbc8c7b67f84b85c115a88283684f28d
3
+ metadata.gz: 8736c0cccd990adbc7f614480718ba0bab767f94ad281f508b0a01b9898deb3d
4
+ data.tar.gz: 93ab376b58edb586aed577d0d4a19011f7f84660c62eebffe418eccb4b9b1e73
5
5
  SHA512:
6
- metadata.gz: f576e967b72b0b4df43f54e7be1d02ad7e10a4623230cc4a57ef17fb64ede2763e11eea0954756a36b53e64a7c297347c8b48ec9019b144964e8df3cea0944eb
7
- data.tar.gz: 7e8b1dae798ceea304b01df2ee82b0943f59f2b93657084f79ba886c419bbba3b1cebfede142df91172abebc20c5a609ee4563b9e926fe4483fff00b8bbb28fb
6
+ metadata.gz: 1c0d8a02384a2e1f958d2a0ce62f66fe8f63db8c87cbe709089e04ca9441e2d37a9a41b860170cdb2ca8f24467a79ce12b2587d1312e0d59dd22e75020429911
7
+ data.tar.gz: 7aae2b96e735993cfd583cdc98ea0ad6bbd49110ef63209b17f6f49e614bf7edc0d1948612f40522ed9aa6e1839fa6e02731a7f6764ef269b3b61c17cee5c2b3
@@ -1,5 +1,5 @@
1
1
  module Opal
2
2
  module Vite
3
- VERSION = "0.2.19"
3
+ VERSION = "0.3.1"
4
4
  end
5
5
  end
@@ -0,0 +1,275 @@
1
+ # backtick_javascript: true
2
+
3
+ module OpalVite
4
+ module Concerns
5
+ module V1
6
+ # Base64Helpers - provides Base64 encoding/decoding utilities
7
+ #
8
+ # This module wraps JavaScript's btoa/atob and provides additional
9
+ # utilities for Base64 operations commonly needed in web applications.
10
+ #
11
+ # @example Basic usage
12
+ # class MyController < StimulusController
13
+ # include OpalVite::Concerns::V1::Base64Helpers
14
+ #
15
+ # def connect
16
+ # encoded = base64_encode("Hello, World!")
17
+ # puts encoded # => "SGVsbG8sIFdvcmxkIQ=="
18
+ #
19
+ # decoded = base64_decode(encoded)
20
+ # puts decoded # => "Hello, World!"
21
+ # end
22
+ # end
23
+ #
24
+ module Base64Helpers
25
+ # ===== Basic Encoding/Decoding =====
26
+
27
+ # Encode a string to Base64
28
+ # @param str [String] String to encode
29
+ # @return [String] Base64 encoded string
30
+ def base64_encode(str)
31
+ `btoa(#{str})`
32
+ rescue
33
+ nil
34
+ end
35
+
36
+ # Decode a Base64 string
37
+ # @param str [String] Base64 string to decode
38
+ # @return [String] Decoded string
39
+ def base64_decode(str)
40
+ `atob(#{str})`
41
+ rescue
42
+ nil
43
+ end
44
+
45
+ # ===== URL-Safe Base64 =====
46
+
47
+ # Encode a string to URL-safe Base64
48
+ # @param str [String] String to encode
49
+ # @return [String] URL-safe Base64 encoded string
50
+ def base64_encode_urlsafe(str)
51
+ encoded = base64_encode(str)
52
+ return nil unless encoded
53
+
54
+ # Replace + with -, / with _, and remove =
55
+ encoded.gsub('+', '-').gsub('/', '_').gsub('=', '')
56
+ end
57
+
58
+ # Decode a URL-safe Base64 string
59
+ # @param str [String] URL-safe Base64 string to decode
60
+ # @return [String] Decoded string
61
+ def base64_decode_urlsafe(str)
62
+ # Restore standard Base64 characters
63
+ standard = str.gsub('-', '+').gsub('_', '/')
64
+
65
+ # Add padding if needed
66
+ case standard.length % 4
67
+ when 2
68
+ standard += '=='
69
+ when 3
70
+ standard += '='
71
+ end
72
+
73
+ base64_decode(standard)
74
+ end
75
+
76
+ # ===== Unicode Support =====
77
+
78
+ # Encode a Unicode string to Base64
79
+ # @param str [String] Unicode string to encode
80
+ # @return [String] Base64 encoded string
81
+ def base64_encode_unicode(str)
82
+ # Convert to UTF-8 bytes, then encode
83
+ `btoa(unescape(encodeURIComponent(#{str})))`
84
+ rescue
85
+ nil
86
+ end
87
+
88
+ # Decode a Base64 string to Unicode
89
+ # @param str [String] Base64 string to decode
90
+ # @return [String] Decoded Unicode string
91
+ def base64_decode_unicode(str)
92
+ `decodeURIComponent(escape(atob(#{str})))`
93
+ rescue
94
+ nil
95
+ end
96
+
97
+ # ===== Binary Data (ArrayBuffer/Uint8Array) =====
98
+
99
+ # Encode an ArrayBuffer or Uint8Array to Base64
100
+ # @param buffer [Native] ArrayBuffer or Uint8Array
101
+ # @return [String] Base64 encoded string
102
+ def base64_encode_buffer(buffer)
103
+ `
104
+ var bytes = new Uint8Array(#{buffer});
105
+ var binary = '';
106
+ for (var i = 0; i < bytes.byteLength; i++) {
107
+ binary += String.fromCharCode(bytes[i]);
108
+ }
109
+ return btoa(binary);
110
+ `
111
+ rescue
112
+ nil
113
+ end
114
+
115
+ # Decode a Base64 string to Uint8Array
116
+ # @param str [String] Base64 string to decode
117
+ # @return [Native] Uint8Array
118
+ def base64_decode_to_buffer(str)
119
+ `
120
+ var binary = atob(#{str});
121
+ var bytes = new Uint8Array(binary.length);
122
+ for (var i = 0; i < binary.length; i++) {
123
+ bytes[i] = binary.charCodeAt(i);
124
+ }
125
+ return bytes;
126
+ `
127
+ rescue
128
+ nil
129
+ end
130
+
131
+ # ===== Data URL Support =====
132
+
133
+ # Create a data URL from content
134
+ # @param content [String] Content to encode
135
+ # @param mime_type [String] MIME type (default: "text/plain")
136
+ # @return [String] Data URL
137
+ def to_data_url(content, mime_type = 'text/plain')
138
+ encoded = base64_encode_unicode(content)
139
+ return nil unless encoded
140
+
141
+ "data:#{mime_type};base64,#{encoded}"
142
+ end
143
+
144
+ # Parse a data URL
145
+ # @param data_url [String] Data URL
146
+ # @return [Hash, nil] Hash with :mime_type and :data keys, or nil
147
+ def parse_data_url(data_url)
148
+ return nil unless data_url.to_s.start_with?('data:')
149
+
150
+ match = `#{data_url}.match(/^data:([^;]+);base64,(.+)$/)`
151
+ return nil if `#{match} === null`
152
+
153
+ {
154
+ mime_type: `#{match}[1]`,
155
+ data: base64_decode_unicode(`#{match}[2]`)
156
+ }
157
+ end
158
+
159
+ # ===== Authentication Helpers =====
160
+
161
+ # Create a Basic Auth header value
162
+ # @param username [String] Username
163
+ # @param password [String] Password
164
+ # @return [String] Basic auth header value
165
+ def basic_auth_header(username, password)
166
+ credentials = "#{username}:#{password}"
167
+ encoded = base64_encode(credentials)
168
+ "Basic #{encoded}"
169
+ end
170
+
171
+ # Parse a Basic Auth header value
172
+ # @param header [String] Authorization header value
173
+ # @return [Hash, nil] Hash with :username and :password keys, or nil
174
+ def parse_basic_auth(header)
175
+ return nil unless header.to_s.start_with?('Basic ')
176
+
177
+ encoded = header[6..-1]
178
+ decoded = base64_decode(encoded)
179
+ return nil unless decoded
180
+
181
+ parts = decoded.split(':', 2)
182
+ return nil if parts.length != 2
183
+
184
+ {
185
+ username: parts[0],
186
+ password: parts[1]
187
+ }
188
+ end
189
+
190
+ # ===== JWT Helpers =====
191
+
192
+ # Decode a JWT payload (without verification)
193
+ # @param token [String] JWT token
194
+ # @return [Hash, nil] Decoded payload or nil
195
+ # @note This does NOT verify the signature - use only for reading claims
196
+ def decode_jwt_payload(token)
197
+ parts = token.to_s.split('.')
198
+ return nil if parts.length != 3
199
+
200
+ payload_base64 = parts[1]
201
+ payload_json = base64_decode_urlsafe(payload_base64)
202
+ return nil unless payload_json
203
+
204
+ `JSON.parse(#{payload_json})`
205
+ rescue
206
+ nil
207
+ end
208
+
209
+ # Check if a JWT is expired
210
+ # @param token [String] JWT token
211
+ # @return [Boolean] True if expired
212
+ def jwt_expired?(token)
213
+ payload = decode_jwt_payload(token)
214
+ return true unless payload
215
+
216
+ exp = `#{payload}.exp`
217
+ return false if `#{exp} === undefined || #{exp} === null`
218
+
219
+ now = `Math.floor(Date.now() / 1000)`
220
+ `#{exp} < #{now}`
221
+ end
222
+
223
+ # Get JWT expiration time
224
+ # @param token [String] JWT token
225
+ # @return [Time, nil] Expiration time or nil
226
+ def jwt_expires_at(token)
227
+ payload = decode_jwt_payload(token)
228
+ return nil unless payload
229
+
230
+ exp = `#{payload}.exp`
231
+ return nil if `#{exp} === undefined || #{exp} === null`
232
+
233
+ # Convert Unix timestamp to Ruby-like time representation
234
+ `new Date(#{exp} * 1000)`
235
+ end
236
+
237
+ # ===== Utility Methods =====
238
+
239
+ # Check if a string is valid Base64
240
+ # @param str [String] String to check
241
+ # @return [Boolean] True if valid Base64
242
+ def valid_base64?(str)
243
+ return false if str.nil? || str.empty?
244
+
245
+ # Check for valid Base64 characters
246
+ `
247
+ var regex = /^[A-Za-z0-9+/]*={0,2}$/;
248
+ if (!regex.test(#{str})) return false;
249
+ if (#{str}.length % 4 !== 0) return false;
250
+ try {
251
+ atob(#{str});
252
+ return true;
253
+ } catch (e) {
254
+ return false;
255
+ }
256
+ `
257
+ end
258
+
259
+ # Get the decoded length of a Base64 string
260
+ # @param str [String] Base64 string
261
+ # @return [Integer] Decoded length in bytes
262
+ def base64_decoded_length(str)
263
+ return 0 if str.nil? || str.empty?
264
+
265
+ len = str.length
266
+ padding = str.end_with?('==') ? 2 : (str.end_with?('=') ? 1 : 0)
267
+ (len * 3 / 4) - padding
268
+ end
269
+ end
270
+ end
271
+ end
272
+ end
273
+
274
+ # Alias for convenience
275
+ Base64Helpers = OpalVite::Concerns::V1::Base64Helpers
@@ -450,12 +450,7 @@ module OpalVite
450
450
  # @param name [String] Event name
451
451
  # @yield Block to execute when event fires
452
452
  def on_controller_event(name, &block)
453
- `
454
- const handler = #{block};
455
- this.element.addEventListener(#{name}, function(e) {
456
- handler(e);
457
- });
458
- `
453
+ `this.element.addEventListener(#{name}, #{block})`
459
454
  end
460
455
 
461
456
  # Get the current event's target element
@@ -1307,6 +1302,643 @@ module OpalVite
1307
1302
  `#{promise}.finally(#{block})`
1308
1303
  end
1309
1304
 
1305
+ # ===== Stimulus Values API =====
1306
+ # Access Stimulus values defined with `static values = { name: Type }`
1307
+ # Note: These methods use "stimulus_value" prefix to avoid conflict with
1308
+ # get_value(element) which gets an element's value attribute.
1309
+
1310
+ # Get a Stimulus value
1311
+ # @param name [Symbol, String] Value name (e.g., :count, :url)
1312
+ # @return [Object] The value (auto-converted by Stimulus based on type)
1313
+ # @example
1314
+ # # With self.values = { count: :number, url: :string }
1315
+ # stimulus_value(:count) # => 0
1316
+ # stimulus_value(:url) # => "/api/items"
1317
+ def stimulus_value(name)
1318
+ prop_name = "#{camelize(name, false)}Value"
1319
+ `this[#{prop_name}]`
1320
+ end
1321
+
1322
+ # Set a Stimulus value
1323
+ # @param name [Symbol, String] Value name
1324
+ # @param value [Object] The value to set
1325
+ # @example
1326
+ # set_stimulus_value(:count, 5)
1327
+ # set_stimulus_value(:items, [1, 2, 3])
1328
+ def set_stimulus_value(name, value)
1329
+ prop_name = "#{camelize(name, false)}Value"
1330
+ native_value = value.respond_to?(:to_n) ? value.to_n : value
1331
+ `this[#{prop_name}] = #{native_value}`
1332
+ end
1333
+
1334
+ # Check if a Stimulus value exists (has data attribute)
1335
+ # @param name [Symbol, String] Value name
1336
+ # @return [Boolean] true if value's data attribute exists
1337
+ # @example
1338
+ # if has_stimulus_value?(:api_url)
1339
+ # fetch_json(stimulus_value(:api_url)) { |data| ... }
1340
+ # end
1341
+ def has_stimulus_value?(name)
1342
+ prop_name = "has#{camelize(name)}Value"
1343
+ `this[#{prop_name}]`
1344
+ end
1345
+
1346
+ # Increment a numeric Stimulus value
1347
+ # @param name [Symbol, String] Value name
1348
+ # @param amount [Number] Amount to increment (default: 1)
1349
+ def increment_stimulus_value(name, amount = 1)
1350
+ set_stimulus_value(name, stimulus_value(name) + amount)
1351
+ end
1352
+
1353
+ # Decrement a numeric Stimulus value
1354
+ # @param name [Symbol, String] Value name
1355
+ # @param amount [Number] Amount to decrement (default: 1)
1356
+ def decrement_stimulus_value(name, amount = 1)
1357
+ set_stimulus_value(name, stimulus_value(name) - amount)
1358
+ end
1359
+
1360
+ # Toggle a boolean Stimulus value
1361
+ # @param name [Symbol, String] Value name
1362
+ def toggle_stimulus_value(name)
1363
+ set_stimulus_value(name, !stimulus_value(name))
1364
+ end
1365
+
1366
+ # ===== Stimulus CSS Classes API =====
1367
+ # Access CSS classes defined with `static classes = [ "loading", "active" ]`
1368
+
1369
+ # Get a Stimulus CSS class (singular)
1370
+ # @param name [Symbol, String] Class logical name
1371
+ # @return [String] The CSS class name
1372
+ # @example
1373
+ # # With static classes = [ "loading" ] and data-controller-loading-class="spinner"
1374
+ # get_class(:loading) # => "spinner"
1375
+ def get_class(name)
1376
+ prop_name = "#{camelize(name, false)}Class"
1377
+ `this[#{prop_name}]`
1378
+ end
1379
+
1380
+ # Get all Stimulus CSS classes (plural, for space-separated values)
1381
+ # @param name [Symbol, String] Class logical name
1382
+ # @return [Array] Array of CSS class names
1383
+ # @example
1384
+ # # With data-controller-loading-class="spinner bg-gray-500"
1385
+ # get_classes(:loading) # => ["spinner", "bg-gray-500"]
1386
+ def get_classes(name)
1387
+ prop_name = "#{camelize(name, false)}Classes"
1388
+ `Array.from(this[#{prop_name}] || [])`
1389
+ end
1390
+
1391
+ # Check if a Stimulus CSS class is defined
1392
+ # @param name [Symbol, String] Class logical name
1393
+ # @return [Boolean] true if class data attribute exists
1394
+ def has_class_definition?(name)
1395
+ prop_name = "has#{camelize(name)}Class"
1396
+ `this[#{prop_name}]`
1397
+ end
1398
+
1399
+ # Apply a Stimulus CSS class to an element
1400
+ # @param element [Native] DOM element
1401
+ # @param name [Symbol, String] Class logical name
1402
+ def apply_class(element, name)
1403
+ el = to_native_element(element)
1404
+ class_name = get_class(name)
1405
+ `#{el}.classList.add(#{class_name})` if class_name
1406
+ end
1407
+
1408
+ # Apply all Stimulus CSS classes to an element
1409
+ # @param element [Native] DOM element
1410
+ # @param name [Symbol, String] Class logical name
1411
+ def apply_classes(element, name)
1412
+ el = to_native_element(element)
1413
+ classes = get_classes(name)
1414
+ `#{el}.classList.add(...#{classes})` if classes
1415
+ end
1416
+
1417
+ # Remove a Stimulus CSS class from an element
1418
+ # @param element [Native] DOM element
1419
+ # @param name [Symbol, String] Class logical name
1420
+ def remove_applied_class(element, name)
1421
+ el = to_native_element(element)
1422
+ class_name = get_class(name)
1423
+ `#{el}.classList.remove(#{class_name})` if class_name
1424
+ end
1425
+
1426
+ # Remove all Stimulus CSS classes from an element
1427
+ # @param element [Native] DOM element
1428
+ # @param name [Symbol, String] Class logical name
1429
+ def remove_applied_classes(element, name)
1430
+ el = to_native_element(element)
1431
+ classes = get_classes(name)
1432
+ `#{el}.classList.remove(...#{classes})` if classes
1433
+ end
1434
+
1435
+ # ===== Stimulus Outlets API =====
1436
+ # Access outlets defined with `static outlets = [ "result" ]`
1437
+
1438
+ # Check if an outlet exists
1439
+ # @param name [Symbol, String] Outlet identifier
1440
+ # @return [Boolean] true if outlet is connected
1441
+ # @example
1442
+ # if has_outlet?(:modal)
1443
+ # get_outlet(:modal).open
1444
+ # end
1445
+ def has_outlet?(name)
1446
+ prop_name = "has#{camelize(name)}Outlet"
1447
+ `this[#{prop_name}]`
1448
+ end
1449
+
1450
+ # Get a single outlet controller instance
1451
+ # @param name [Symbol, String] Outlet identifier
1452
+ # @return [Native] The outlet controller instance
1453
+ # @note Throws error if outlet doesn't exist - use has_outlet? first
1454
+ def get_outlet(name)
1455
+ prop_name = "#{camelize(name, false)}Outlet"
1456
+ `this[#{prop_name}]`
1457
+ end
1458
+
1459
+ # Get all outlet controller instances
1460
+ # @param name [Symbol, String] Outlet identifier
1461
+ # @return [Array] Array of outlet controller instances
1462
+ def get_outlets(name)
1463
+ prop_name = "#{camelize(name, false)}Outlets"
1464
+ `Array.from(this[#{prop_name}] || [])`
1465
+ end
1466
+
1467
+ # Get a single outlet's element
1468
+ # @param name [Symbol, String] Outlet identifier
1469
+ # @return [Native] The outlet's controller element
1470
+ # @note Throws error if outlet doesn't exist - use has_outlet? first
1471
+ def get_outlet_element(name)
1472
+ prop_name = "#{camelize(name, false)}OutletElement"
1473
+ `this[#{prop_name}]`
1474
+ end
1475
+
1476
+ # Get all outlet elements
1477
+ # @param name [Symbol, String] Outlet identifier
1478
+ # @return [Array] Array of outlet controller elements
1479
+ def get_outlet_elements(name)
1480
+ prop_name = "#{camelize(name, false)}OutletElements"
1481
+ `Array.from(this[#{prop_name}] || [])`
1482
+ end
1483
+
1484
+ # Call a method on an outlet controller
1485
+ # @param name [Symbol, String] Outlet identifier
1486
+ # @param method [Symbol, String] Method name to call
1487
+ # @param args [Array] Arguments to pass
1488
+ # @return [Object] Method return value
1489
+ def call_outlet(name, method, *args)
1490
+ return nil unless has_outlet?(name)
1491
+ outlet = get_outlet(name)
1492
+ js_call_on(outlet, method, *args)
1493
+ end
1494
+
1495
+ # Call a method on all outlet controllers
1496
+ # @param name [Symbol, String] Outlet identifier
1497
+ # @param method [Symbol, String] Method name to call
1498
+ # @param args [Array] Arguments to pass
1499
+ def call_all_outlets(name, method, *args)
1500
+ get_outlets(name).each do |outlet|
1501
+ js_call_on(outlet, method, *args)
1502
+ end
1503
+ end
1504
+
1505
+ # ===== Stimulus dispatch() API =====
1506
+ # Emit custom events with controller identifier prefix
1507
+
1508
+ # Dispatch a Stimulus-style custom event
1509
+ # @param name [String] Event name (will be prefixed with controller identifier)
1510
+ # @param detail [Hash] Event detail data
1511
+ # @param target [Native, nil] Target element (default: controller element)
1512
+ # @param prefix [String, nil] Custom prefix (default: controller identifier)
1513
+ # @param bubbles [Boolean] Whether event bubbles (default: true)
1514
+ # @param cancelable [Boolean] Whether event is cancelable (default: true)
1515
+ # @return [Native] The dispatched event
1516
+ # @example
1517
+ # # In a "clipboard" controller:
1518
+ # stimulus_dispatch("copied", detail: { content: text })
1519
+ # # Dispatches "clipboard:copied" event
1520
+ def stimulus_dispatch(name, detail: {}, target: nil, prefix: nil, bubbles: true, cancelable: true)
1521
+ native_detail = detail.respond_to?(:to_n) ? detail.to_n : detail
1522
+ # Build options object, only including target/prefix if explicitly provided
1523
+ # Note: Ruby nil becomes Opal.nil in JS (truthy), so we must check explicitly
1524
+ options = `{ detail: #{native_detail}, bubbles: #{bubbles}, cancelable: #{cancelable} }`
1525
+ `#{options}.target = #{target}` unless target.nil?
1526
+ `#{options}.prefix = #{prefix}` unless prefix.nil?
1527
+ `this.dispatch(#{name}, #{options})`
1528
+ end
1529
+
1530
+ # Dispatch event and check if it was cancelled
1531
+ # @param name [String] Event name
1532
+ # @param detail [Hash] Event detail data
1533
+ # @return [Boolean] true if event was NOT cancelled
1534
+ def stimulus_dispatch_confirm(name, detail: {})
1535
+ event = stimulus_dispatch(name, detail: detail, cancelable: true)
1536
+ `!#{event}.defaultPrevented`
1537
+ end
1538
+
1539
+ # ===== Stimulus Action Parameters API =====
1540
+ # Access action parameters from data-[identifier]-[name]-param attributes
1541
+
1542
+ # Get all action parameters from the current event
1543
+ # @return [Native] JavaScript object with all parameters
1544
+ # @example
1545
+ # # <button data-action="item#delete" data-item-id-param="123">
1546
+ # def delete(event)
1547
+ # params = action_params # => { id: 123 }
1548
+ # end
1549
+ def action_params
1550
+ `event.params || {}`
1551
+ end
1552
+
1553
+ # Get a specific action parameter
1554
+ # @param name [Symbol, String] Parameter name
1555
+ # @return [Object] Parameter value (auto-typecast by Stimulus)
1556
+ # @example
1557
+ # action_param(:id) # => 123 (Number)
1558
+ # action_param(:url) # => "/api/item" (String)
1559
+ def action_param(name)
1560
+ `(event.params || {})[#{name.to_s}]`
1561
+ end
1562
+
1563
+ # Get action parameter as integer with default
1564
+ # @param name [Symbol, String] Parameter name
1565
+ # @param default [Integer] Default value if missing or NaN
1566
+ # @return [Integer] Parameter value
1567
+ def action_param_int(name, default = 0)
1568
+ value = action_param(name)
1569
+ result = parse_int(value)
1570
+ is_nan?(result) ? default : result
1571
+ end
1572
+
1573
+ # Get action parameter as boolean
1574
+ # @param name [Symbol, String] Parameter name
1575
+ # @return [Boolean] Parameter value
1576
+ def action_param_bool(name)
1577
+ value = action_param(name)
1578
+ `!!#{value} && #{value} !== "false" && #{value} !== "0"`
1579
+ end
1580
+
1581
+ # Check if an action parameter exists
1582
+ # @param name [Symbol, String] Parameter name
1583
+ # @return [Boolean] true if parameter exists
1584
+ def has_action_param?(name)
1585
+ `(event.params || {}).hasOwnProperty(#{name.to_s})`
1586
+ end
1587
+
1588
+ # ===== Stimulus Controller Access =====
1589
+ # Access controller properties and other controllers
1590
+
1591
+ # Get the controller's application instance
1592
+ # @return [Native] Stimulus Application
1593
+ def this_application
1594
+ `this.application`
1595
+ end
1596
+
1597
+ # Get the controller's identifier
1598
+ # @return [String] Controller identifier (e.g., "modal", "tabs")
1599
+ def this_identifier
1600
+ `this.identifier`
1601
+ end
1602
+
1603
+ # Get the controller's element
1604
+ # @return [Native] Controller's root element
1605
+ def this_element
1606
+ `this.element`
1607
+ end
1608
+
1609
+ # Get another controller instance by element and identifier
1610
+ # @param element [Native] DOM element with the controller
1611
+ # @param identifier [String] Controller identifier
1612
+ # @return [Native, nil] Controller instance or nil
1613
+ # @example
1614
+ # modal = get_controller(modal_element, "modal")
1615
+ # modal.open if modal
1616
+ def get_controller(element, identifier)
1617
+ el = to_native_element(element)
1618
+ `this.application.getControllerForElementAndIdentifier(#{el}, #{identifier})`
1619
+ end
1620
+
1621
+ # Get all controllers of a type within an element's scope
1622
+ # @param element [Native] Container element
1623
+ # @param identifier [String] Controller identifier
1624
+ # @return [Array] Array of controller instances
1625
+ def get_controllers(element, identifier)
1626
+ el = to_native_element(element)
1627
+ `Array.from(#{el}.querySelectorAll('[data-controller~="' + #{identifier} + '"]'))
1628
+ .map(el => this.application.getControllerForElementAndIdentifier(el, #{identifier}))
1629
+ .filter(c => c !== null)`
1630
+ end
1631
+
1632
+ # ===== Stimulus Scope Helpers =====
1633
+
1634
+ # Get the controller's scope (element and descendants, excluding nested controllers)
1635
+ # @return [Native] Controller's scope element
1636
+ def this_scope
1637
+ `this.scope.element`
1638
+ end
1639
+
1640
+ # Check if an element is within this controller's scope
1641
+ # @param element [Native] Element to check
1642
+ # @return [Boolean] true if element is in scope
1643
+ def in_scope?(element)
1644
+ el = to_native_element(element)
1645
+ `this.element.contains(#{el})`
1646
+ end
1647
+
1648
+ # ===== Debounce/Throttle Utilities =====
1649
+
1650
+ # Create a debounced version of a block
1651
+ # @param wait [Integer] Milliseconds to wait
1652
+ # @yield Block to debounce
1653
+ # @return [Native] Debounced function
1654
+ def debounce(wait, &block)
1655
+ `
1656
+ var timeout;
1657
+ return function(...args) {
1658
+ clearTimeout(timeout);
1659
+ timeout = setTimeout(() => #{block.call}, wait);
1660
+ };
1661
+ `
1662
+ end
1663
+
1664
+ # Create a throttled version of a block
1665
+ # @param wait [Integer] Milliseconds between calls
1666
+ # @yield Block to throttle
1667
+ # @return [Native] Throttled function
1668
+ def throttle(wait, &block)
1669
+ `
1670
+ var lastTime = 0;
1671
+ return function(...args) {
1672
+ var now = Date.now();
1673
+ if (now - lastTime >= #{wait}) {
1674
+ lastTime = now;
1675
+ #{block.call};
1676
+ }
1677
+ };
1678
+ `
1679
+ end
1680
+
1681
+ # Debounce a method call
1682
+ # @param wait [Integer] Milliseconds to wait
1683
+ # @param key [String] Unique key for this debounce
1684
+ # @yield Block to execute after debounce
1685
+ def debounced(wait, key = 'default', &block)
1686
+ @_debounce_timers ||= {}
1687
+ timer_key = "_debounce_#{key}"
1688
+
1689
+ `clearTimeout(#{@_debounce_timers[timer_key]})`
1690
+ @_debounce_timers[timer_key] = `setTimeout(function() { #{block.call} }, #{wait})`
1691
+ end
1692
+
1693
+ # Throttle a method call
1694
+ # @param wait [Integer] Milliseconds between calls
1695
+ # @param key [String] Unique key for this throttle
1696
+ # @yield Block to execute if not throttled
1697
+ def throttled(wait, key = 'default', &block)
1698
+ @_throttle_times ||= {}
1699
+ time_key = "_throttle_#{key}"
1700
+
1701
+ now = `Date.now()`
1702
+ last_time = @_throttle_times[time_key] || 0
1703
+
1704
+ if `#{now} - #{last_time} >= #{wait}`
1705
+ @_throttle_times[time_key] = now
1706
+ block.call
1707
+ end
1708
+ end
1709
+
1710
+ # ===== Clipboard Utilities =====
1711
+
1712
+ # Copy text to clipboard
1713
+ # @param text [String] Text to copy
1714
+ # @yield Optional callback on success
1715
+ def copy_to_clipboard(text, &on_success)
1716
+ `navigator.clipboard.writeText(#{text}).then(function() {
1717
+ #{on_success.call if on_success}
1718
+ })`
1719
+ end
1720
+
1721
+ # Read text from clipboard
1722
+ # @yield Block receiving clipboard text
1723
+ def read_from_clipboard(&block)
1724
+ `navigator.clipboard.readText().then(function(text) {
1725
+ #{block.call(`text`)}
1726
+ })`
1727
+ end
1728
+
1729
+ # ===== Object Utilities =====
1730
+
1731
+ # Deep clone an object
1732
+ # @param obj [Object] Object to clone
1733
+ # @return [Object] Cloned object
1734
+ def deep_clone(obj)
1735
+ `JSON.parse(JSON.stringify(#{obj.to_n}))`
1736
+ end
1737
+
1738
+ # Deep merge two objects
1739
+ # @param target [Hash] Target object
1740
+ # @param source [Hash] Source object
1741
+ # @return [Hash] Merged object
1742
+ def deep_merge(target, source)
1743
+ `
1744
+ function deepMerge(target, source) {
1745
+ var result = Object.assign({}, target);
1746
+ for (var key in source) {
1747
+ if (source.hasOwnProperty(key)) {
1748
+ if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
1749
+ result[key] = deepMerge(result[key] || {}, source[key]);
1750
+ } else {
1751
+ result[key] = source[key];
1752
+ }
1753
+ }
1754
+ }
1755
+ return result;
1756
+ }
1757
+ return deepMerge(#{target.to_n}, #{source.to_n});
1758
+ `
1759
+ end
1760
+
1761
+ # Pick specific keys from an object
1762
+ # @param obj [Hash] Source object
1763
+ # @param keys [Array<String, Symbol>] Keys to pick
1764
+ # @return [Hash] Object with only specified keys
1765
+ def pick(obj, *keys)
1766
+ result = {}
1767
+ keys.flatten.each do |key|
1768
+ key_s = key.to_s
1769
+ native_obj = obj.to_n
1770
+ if `#{native_obj}.hasOwnProperty(#{key_s})`
1771
+ result[key_s] = `#{native_obj}[#{key_s}]`
1772
+ end
1773
+ end
1774
+ result
1775
+ end
1776
+
1777
+ # Omit specific keys from an object
1778
+ # @param obj [Hash] Source object
1779
+ # @param keys [Array<String, Symbol>] Keys to omit
1780
+ # @return [Hash] Object without specified keys
1781
+ def omit(obj, *keys)
1782
+ keys_to_omit = keys.flatten.map(&:to_s)
1783
+ result = {}
1784
+ native_obj = obj.to_n
1785
+ `Object.keys(#{native_obj}).forEach(function(key) {
1786
+ if (!#{keys_to_omit}.includes(key)) {
1787
+ #{result[`key`] = `#{native_obj}[key]`}
1788
+ }
1789
+ })`
1790
+ result
1791
+ end
1792
+
1793
+ # ===== Set Utilities =====
1794
+
1795
+ # Create a unique array (like Set)
1796
+ # @param arr [Array] Array with possible duplicates
1797
+ # @return [Array] Array with unique values
1798
+ def unique(arr)
1799
+ `[...new Set(#{arr.to_n})]`
1800
+ end
1801
+
1802
+ # Get intersection of two arrays
1803
+ # @param arr1 [Array] First array
1804
+ # @param arr2 [Array] Second array
1805
+ # @return [Array] Intersection
1806
+ def intersection(arr1, arr2)
1807
+ set2 = `new Set(#{arr2.to_n})`
1808
+ `#{arr1.to_n}.filter(x => #{set2}.has(x))`
1809
+ end
1810
+
1811
+ # Get difference of two arrays (arr1 - arr2)
1812
+ # @param arr1 [Array] First array
1813
+ # @param arr2 [Array] Second array
1814
+ # @return [Array] Difference
1815
+ def difference(arr1, arr2)
1816
+ set2 = `new Set(#{arr2.to_n})`
1817
+ `#{arr1.to_n}.filter(x => !#{set2}.has(x))`
1818
+ end
1819
+
1820
+ # Get union of two arrays
1821
+ # @param arr1 [Array] First array
1822
+ # @param arr2 [Array] Second array
1823
+ # @return [Array] Union (unique values)
1824
+ def union(arr1, arr2)
1825
+ `[...new Set([...#{arr1.to_n}, ...#{arr2.to_n}])]`
1826
+ end
1827
+
1828
+ # ===== Validation Utilities =====
1829
+
1830
+ # Validate an email address
1831
+ # @param email [String] Email to validate
1832
+ # @return [Boolean] True if valid
1833
+ def valid_email?(email)
1834
+ `/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(#{email})`
1835
+ end
1836
+
1837
+ # Validate a URL
1838
+ # @param url [String] URL to validate
1839
+ # @return [Boolean] True if valid
1840
+ def valid_url?(url)
1841
+ `
1842
+ try {
1843
+ new URL(#{url});
1844
+ return true;
1845
+ } catch (e) {
1846
+ return false;
1847
+ }
1848
+ `
1849
+ end
1850
+
1851
+ # Validate a phone number (basic)
1852
+ # @param phone [String] Phone number to validate
1853
+ # @return [Boolean] True if valid
1854
+ def valid_phone?(phone)
1855
+ # Remove common separators and check for digits
1856
+ cleaned = `#{phone}.replace(/[\s\-\(\)\.]/g, '')`
1857
+ `
1858
+ var pattern = /^[\+]?[0-9]{10,15}$/;
1859
+ return pattern.test(#{cleaned});
1860
+ `
1861
+ end
1862
+
1863
+ # Check if a value is blank (nil, empty string, or whitespace only)
1864
+ # @param value [Object] Value to check
1865
+ # @return [Boolean] True if blank
1866
+ def blank?(value)
1867
+ return true if value.nil?
1868
+ return `#{value}.trim() === ''` if `typeof #{value} === 'string'`
1869
+ return `#{value}.length === 0` if `Array.isArray(#{value})`
1870
+ false
1871
+ end
1872
+
1873
+ # Check if a value is present (not blank)
1874
+ # @param value [Object] Value to check
1875
+ # @return [Boolean] True if present
1876
+ def present?(value)
1877
+ !blank?(value)
1878
+ end
1879
+
1880
+ # Validate minimum length
1881
+ # @param value [String] Value to check
1882
+ # @param min [Integer] Minimum length
1883
+ # @return [Boolean] True if valid
1884
+ def min_length?(value, min)
1885
+ return false if value.nil?
1886
+ `#{value}.length >= #{min}`
1887
+ end
1888
+
1889
+ # Validate maximum length
1890
+ # @param value [String] Value to check
1891
+ # @param max [Integer] Maximum length
1892
+ # @return [Boolean] True if valid
1893
+ def max_length?(value, max)
1894
+ return false if value.nil?
1895
+ `#{value}.length <= #{max}`
1896
+ end
1897
+
1898
+ # Validate a value matches a pattern
1899
+ # @param value [String] Value to check
1900
+ # @param pattern [String] Regular expression pattern
1901
+ # @return [Boolean] True if matches
1902
+ def matches_pattern?(value, pattern)
1903
+ return false if value.nil?
1904
+ `new RegExp(#{pattern}).test(#{value})`
1905
+ end
1906
+
1907
+ # ===== Console Helpers =====
1908
+
1909
+ # Log a styled message to console
1910
+ # @param message [String] Message to log
1911
+ # @param style [String] CSS style string
1912
+ def console_styled(message, style = 'color: blue; font-weight: bold;')
1913
+ `console.log('%c' + #{message}, #{style})`
1914
+ end
1915
+
1916
+ # Log a grouped set of messages
1917
+ # @param label [String] Group label
1918
+ # @yield Block that logs messages
1919
+ def console_group(label, collapsed: false, &block)
1920
+ if collapsed
1921
+ `console.groupCollapsed(#{label})`
1922
+ else
1923
+ `console.group(#{label})`
1924
+ end
1925
+ block.call
1926
+ `console.groupEnd()`
1927
+ end
1928
+
1929
+ # Log a table
1930
+ # @param data [Array, Hash] Data to display as table
1931
+ def console_table(data)
1932
+ `console.table(#{data.to_n})`
1933
+ end
1934
+
1935
+ # Log with timestamp
1936
+ # @param message [String] Message to log
1937
+ def console_time(message)
1938
+ timestamp = `new Date().toISOString()`
1939
+ `console.log('[' + #{timestamp} + '] ' + #{message})`
1940
+ end
1941
+
1310
1942
  private
1311
1943
 
1312
1944
  # Convert snake_case to camelCase, preserving existing camelCase
@@ -0,0 +1,316 @@
1
+ # backtick_javascript: true
2
+
3
+ module OpalVite
4
+ module Concerns
5
+ module V1
6
+ # URIHelpers - provides URL parsing and manipulation utilities
7
+ #
8
+ # This module wraps JavaScript's URL and URLSearchParams APIs
9
+ # to provide Ruby-friendly methods for URL operations.
10
+ #
11
+ # @example Basic usage
12
+ # class MyController < StimulusController
13
+ # include OpalVite::Concerns::V1::URIHelpers
14
+ #
15
+ # def connect
16
+ # url = parse_url("https://example.com/path?foo=bar")
17
+ # puts url_hostname(url) # => "example.com"
18
+ # puts url_param(url, "foo") # => "bar"
19
+ # end
20
+ # end
21
+ #
22
+ module URIHelpers
23
+ # ===== URL Parsing =====
24
+
25
+ # Parse a URL string into a URL object
26
+ # @param url_string [String] URL string to parse
27
+ # @return [Native] JavaScript URL object
28
+ def parse_url(url_string)
29
+ `new URL(#{url_string})`
30
+ rescue
31
+ nil
32
+ end
33
+
34
+ # Parse a URL with a base URL
35
+ # @param url_string [String] Relative or absolute URL
36
+ # @param base [String] Base URL
37
+ # @return [Native] JavaScript URL object
38
+ def parse_url_with_base(url_string, base)
39
+ `new URL(#{url_string}, #{base})`
40
+ rescue
41
+ nil
42
+ end
43
+
44
+ # Get the current page URL
45
+ # @return [Native] JavaScript URL object
46
+ def current_url
47
+ `new URL(window.location.href)`
48
+ end
49
+
50
+ # ===== URL Components =====
51
+
52
+ # Get the protocol (scheme) of a URL
53
+ # @param url [Native] URL object
54
+ # @return [String] Protocol (e.g., "https:")
55
+ def url_protocol(url)
56
+ `#{url}.protocol`
57
+ end
58
+
59
+ # Get the hostname of a URL
60
+ # @param url [Native] URL object
61
+ # @return [String] Hostname (e.g., "example.com")
62
+ def url_hostname(url)
63
+ `#{url}.hostname`
64
+ end
65
+
66
+ # Get the host (hostname + port) of a URL
67
+ # @param url [Native] URL object
68
+ # @return [String] Host (e.g., "example.com:8080")
69
+ def url_host(url)
70
+ `#{url}.host`
71
+ end
72
+
73
+ # Get the port of a URL
74
+ # @param url [Native] URL object
75
+ # @return [String] Port number or empty string
76
+ def url_port(url)
77
+ `#{url}.port`
78
+ end
79
+
80
+ # Get the pathname of a URL
81
+ # @param url [Native] URL object
82
+ # @return [String] Pathname (e.g., "/path/to/page")
83
+ def url_pathname(url)
84
+ `#{url}.pathname`
85
+ end
86
+
87
+ # Get the search string (query string with ?)
88
+ # @param url [Native] URL object
89
+ # @return [String] Search string (e.g., "?foo=bar")
90
+ def url_search(url)
91
+ `#{url}.search`
92
+ end
93
+
94
+ # Get the hash (fragment) of a URL
95
+ # @param url [Native] URL object
96
+ # @return [String] Hash (e.g., "#section")
97
+ def url_hash(url)
98
+ `#{url}.hash`
99
+ end
100
+
101
+ # Get the origin of a URL
102
+ # @param url [Native] URL object
103
+ # @return [String] Origin (e.g., "https://example.com")
104
+ def url_origin(url)
105
+ `#{url}.origin`
106
+ end
107
+
108
+ # Get the full URL as a string
109
+ # @param url [Native] URL object
110
+ # @return [String] Full URL string
111
+ def url_to_string(url)
112
+ `#{url}.href`
113
+ end
114
+
115
+ # ===== Query Parameters =====
116
+
117
+ # Get a query parameter value
118
+ # @param url [Native] URL object
119
+ # @param name [String] Parameter name
120
+ # @return [String, nil] Parameter value or nil
121
+ def url_param(url, name)
122
+ result = `#{url}.searchParams.get(#{name})`
123
+ `#{result} === null` ? nil : result
124
+ end
125
+
126
+ # Get all values for a query parameter
127
+ # @param url [Native] URL object
128
+ # @param name [String] Parameter name
129
+ # @return [Array<String>] Array of values
130
+ def url_params(url, name)
131
+ `Array.from(#{url}.searchParams.getAll(#{name}))`
132
+ end
133
+
134
+ # Check if a query parameter exists
135
+ # @param url [Native] URL object
136
+ # @param name [String] Parameter name
137
+ # @return [Boolean] True if parameter exists
138
+ def url_has_param?(url, name)
139
+ `#{url}.searchParams.has(#{name})`
140
+ end
141
+
142
+ # Get all query parameters as a Hash
143
+ # @param url [Native] URL object
144
+ # @return [Hash] Hash of parameter names to values
145
+ def url_all_params(url)
146
+ result = {}
147
+ `#{url}.searchParams.forEach((value, key) => {
148
+ #{result[`key`] = `value`}
149
+ })`
150
+ result
151
+ end
152
+
153
+ # Set a query parameter (mutates the URL object)
154
+ # @param url [Native] URL object
155
+ # @param name [String] Parameter name
156
+ # @param value [String] Parameter value
157
+ def url_set_param(url, name, value)
158
+ `#{url}.searchParams.set(#{name}, #{value})`
159
+ end
160
+
161
+ # Append a query parameter (allows duplicates)
162
+ # @param url [Native] URL object
163
+ # @param name [String] Parameter name
164
+ # @param value [String] Parameter value
165
+ def url_append_param(url, name, value)
166
+ `#{url}.searchParams.append(#{name}, #{value})`
167
+ end
168
+
169
+ # Delete a query parameter
170
+ # @param url [Native] URL object
171
+ # @param name [String] Parameter name
172
+ def url_delete_param(url, name)
173
+ `#{url}.searchParams.delete(#{name})`
174
+ end
175
+
176
+ # ===== URL Building =====
177
+
178
+ # Build a URL from components
179
+ # @param options [Hash] URL components
180
+ # @option options [String] :protocol Protocol (default: "https:")
181
+ # @option options [String] :hostname Hostname (required)
182
+ # @option options [String] :port Port number
183
+ # @option options [String] :pathname Path
184
+ # @option options [Hash] :params Query parameters
185
+ # @option options [String] :hash Fragment
186
+ # @return [String] Built URL string
187
+ def build_url(options = {})
188
+ protocol = options[:protocol] || 'https:'
189
+ hostname = options[:hostname] || 'localhost'
190
+ port = options[:port]
191
+ pathname = options[:pathname] || '/'
192
+ params = options[:params] || {}
193
+ hash = options[:hash]
194
+
195
+ # Build base URL
196
+ base = "#{protocol}//#{hostname}"
197
+ base += ":#{port}" if port && !port.to_s.empty?
198
+ base += pathname
199
+
200
+ url = parse_url(base)
201
+ return nil unless url
202
+
203
+ # Add query parameters
204
+ params.each do |key, value|
205
+ url_set_param(url, key.to_s, value.to_s)
206
+ end
207
+
208
+ # Add hash
209
+ `#{url}.hash = #{hash}` if hash
210
+
211
+ url_to_string(url)
212
+ end
213
+
214
+ # ===== URL Encoding =====
215
+
216
+ # Encode a URI component
217
+ # @param str [String] String to encode
218
+ # @return [String] Encoded string
219
+ def encode_uri_component(str)
220
+ `encodeURIComponent(#{str})`
221
+ end
222
+
223
+ # Decode a URI component
224
+ # @param str [String] String to decode
225
+ # @return [String] Decoded string
226
+ def decode_uri_component(str)
227
+ `decodeURIComponent(#{str})`
228
+ rescue
229
+ str
230
+ end
231
+
232
+ # Encode a full URI
233
+ # @param str [String] URI to encode
234
+ # @return [String] Encoded URI
235
+ def encode_uri(str)
236
+ `encodeURI(#{str})`
237
+ end
238
+
239
+ # Decode a full URI
240
+ # @param str [String] URI to decode
241
+ # @return [String] Decoded URI
242
+ def decode_uri(str)
243
+ `decodeURI(#{str})`
244
+ rescue
245
+ str
246
+ end
247
+
248
+ # ===== Query String Utilities =====
249
+
250
+ # Parse a query string into a Hash
251
+ # @param query_string [String] Query string (with or without leading ?)
252
+ # @return [Hash] Parsed parameters
253
+ def parse_query_string(query_string)
254
+ # Remove leading ? if present
255
+ qs = query_string.to_s
256
+ qs = qs[1..-1] if qs.start_with?('?')
257
+
258
+ result = {}
259
+ params = `new URLSearchParams(#{qs})`
260
+ `#{params}.forEach((value, key) => {
261
+ #{result[`key`] = `value`}
262
+ })`
263
+ result
264
+ end
265
+
266
+ # Build a query string from a Hash
267
+ # @param params [Hash] Parameters
268
+ # @return [String] Query string (without leading ?)
269
+ def build_query_string(params)
270
+ search_params = `new URLSearchParams()`
271
+ params.each do |key, value|
272
+ `#{search_params}.append(#{key.to_s}, #{value.to_s})`
273
+ end
274
+ `#{search_params}.toString()`
275
+ end
276
+
277
+ # ===== Path Utilities =====
278
+
279
+ # Join path segments
280
+ # @param segments [Array<String>] Path segments
281
+ # @return [String] Joined path
282
+ def join_path(*segments)
283
+ segments.map { |s| s.to_s.gsub(%r{^/|/$}, '') }.reject(&:empty?).join('/')
284
+ end
285
+
286
+ # Get the filename from a path
287
+ # @param path [String] Path
288
+ # @return [String] Filename
289
+ def path_basename(path)
290
+ path.to_s.split('/').last || ''
291
+ end
292
+
293
+ # Get the directory from a path
294
+ # @param path [String] Path
295
+ # @return [String] Directory path
296
+ def path_dirname(path)
297
+ parts = path.to_s.split('/')
298
+ parts.pop
299
+ parts.join('/') || '/'
300
+ end
301
+
302
+ # Get the file extension from a path
303
+ # @param path [String] Path
304
+ # @return [String] Extension (with dot) or empty string
305
+ def path_extname(path)
306
+ basename = path_basename(path)
307
+ idx = basename.rindex('.')
308
+ idx ? basename[idx..-1] : ''
309
+ end
310
+ end
311
+ end
312
+ end
313
+ end
314
+
315
+ # Alias for convenience
316
+ URIHelpers = OpalVite::Concerns::V1::URIHelpers
@@ -6,3 +6,5 @@ require 'opal_vite/concerns/v1/storable'
6
6
  require 'opal_vite/concerns/v1/stimulus_helpers'
7
7
  require 'opal_vite/concerns/v1/vue_helpers'
8
8
  require 'opal_vite/concerns/v1/react_helpers'
9
+ require 'opal_vite/concerns/v1/uri_helpers'
10
+ require 'opal_vite/concerns/v1/base64_helpers'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: opal-vite
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.19
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - stofu1234
@@ -120,12 +120,14 @@ files:
120
120
  - opal/opal_vite/concerns/storable.rb
121
121
  - opal/opal_vite/concerns/toastable.rb
122
122
  - opal/opal_vite/concerns/v1.rb
123
+ - opal/opal_vite/concerns/v1/base64_helpers.rb
123
124
  - opal/opal_vite/concerns/v1/dom_helpers.rb
124
125
  - opal/opal_vite/concerns/v1/js_proxy_ex.rb
125
126
  - opal/opal_vite/concerns/v1/react_helpers.rb
126
127
  - opal/opal_vite/concerns/v1/stimulus_helpers.rb
127
128
  - opal/opal_vite/concerns/v1/storable.rb
128
129
  - opal/opal_vite/concerns/v1/toastable.rb
130
+ - opal/opal_vite/concerns/v1/uri_helpers.rb
129
131
  - opal/opal_vite/concerns/v1/vue_helpers.rb
130
132
  - opal/opal_vite/concerns/vue_helpers.rb
131
133
  homepage: https://stofu1234.github.io/opal-vite/