ruflet_core 0.0.16 → 0.0.17

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: 888ec7addc07079a2ef3b649047b8747d031b446489d3015fb96cb5c2101b339
4
- data.tar.gz: 342aac065292f59865cf01ace0347531803b41692eac2d3453fe5751c773fe0a
3
+ metadata.gz: 82d5d751f2b92b2b740414e315e1b1f9e88389eb0423ec0ce7a040710e191d59
4
+ data.tar.gz: 728109060a0cff6ca564e2bfa5d7620e5f8182dc05cf82b571ae3280e7e0a015
5
5
  SHA512:
6
- metadata.gz: 2ef5114b3cc6389dafae6ab528990b7be92c0bc2367c62fc2eb0920feaec0dccc17400df7961df5b162bfbc539c2489818f77ac1dd91a040ebb87e1964016906
7
- data.tar.gz: 9ad45dc326587bb2a601733b3c1efef6199c9d38424e8a82979d7b9f357fa44ffb579f19de30a3e2c8b2dd78681baf867b7555e9c21883f1f60dd44b7f4f50e0
6
+ metadata.gz: bf40cdafd48b625d189f9625945350873b4936c0668916ce8556e9d07f47368584892b29e57e4fe2e4583230d7ce5c87297848b90c5d01ab1d7ddac0b60df4d2
7
+ data.tar.gz: 2d229699ad359fcbc5dc5640dd176227bd0dad3f01d641b9f44ea569b26ccea61926b16c465284bda7e2fb786b514215922b8fdda5aa0a6b5f6baca36c0303fe
data/README.md CHANGED
@@ -11,9 +11,21 @@ require "ruflet"
11
11
 
12
12
  Ruflet.run do |page|
13
13
  page.title = "Hello"
14
- page.add(text("Hello Ruflet"))
14
+ page.add(
15
+ container(
16
+ bgcolor: :surface_container_high,
17
+ padding: 24,
18
+ content: text("Hello Ruflet", color: "DeepOrange500")
19
+ )
20
+ )
15
21
  end
16
22
  ```
17
23
 
24
+ Color properties accept named colors and hex values. Use Ruby-style symbols
25
+ like `:deep_orange_500`, compact strings like `"DeepOrange500"`, spaced names
26
+ like `"Deep Orange 500"`, semantic theme names like `:primary_container`, or
27
+ hex strings like `"#ff6d00"`. Ruflet normalizes named colors before sending
28
+ them to the client.
29
+
18
30
  Use `ruflet_server` to run a standalone server-driven application. Use
19
31
  `ruflet_rails` when the UI is hosted by Rails.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ruflet
4
- VERSION = "0.0.16" unless const_defined?(:VERSION)
4
+ VERSION = "0.0.17" unless const_defined?(:VERSION)
5
5
  end
@@ -158,16 +158,48 @@ module Ruflet
158
158
  canonicalize(color.to_s)
159
159
  end
160
160
 
161
+ def self.normalize_property(key, value)
162
+ normalize_value(value, color_key?(key))
163
+ end
164
+
165
+ def self.color_key?(key)
166
+ name = key.to_s
167
+ name == "color" ||
168
+ name == "colors" ||
169
+ name.end_with?("bgcolor") ||
170
+ name.end_with?("_color") ||
171
+ name.end_with?("_colors") ||
172
+ name == "color_scheme_seed"
173
+ end
174
+
175
+ def self.normalize_value(value, color_context = false)
176
+ case value
177
+ when String
178
+ color_context ? canonicalize(value) : value
179
+ when Symbol
180
+ color_context ? canonicalize(value.to_s) : value
181
+ when Array
182
+ value.map { |nested| normalize_value(nested, color_context) }
183
+ when Hash
184
+ value.each_with_object({}) do |(nested_key, nested_value), result|
185
+ result[nested_key] = normalize_value(nested_value, color_context || color_key?(nested_key))
186
+ end
187
+ else
188
+ value
189
+ end
190
+ end
191
+
161
192
  # Canonicalizes a named color into flet's wire format. Flet color names are
162
193
  # lowercase with no separators ("bluegrey", "deeporange", "red500"), so we
163
- # strip underscores/whitespace and downcase. Hex values (#... / 0x...) and
164
- # the optional ",opacity" suffix are preserved untouched.
194
+ # strip underscores, hyphens, and whitespace before downcasing. Hex values
195
+ # (#... / 0x...) are downcased without separator stripping; the optional
196
+ # ",opacity" suffix is preserved untouched.
165
197
  def self.canonicalize(value)
166
198
  return value unless value.is_a?(String)
167
199
 
168
200
  color, separator, opacity = value.partition(",")
169
201
  color = color.strip.downcase
170
- color = color.delete("_ \t\n") unless color.start_with?("#") || color.start_with?("0x")
202
+ color = color.gsub(/[_\-\s]+/, "") unless color.start_with?("#") || color.start_with?("0x")
171
203
 
172
204
  "#{color}#{separator}#{opacity}"
173
205
  end
@@ -198,14 +198,11 @@ module Ruflet
198
198
  end
199
199
 
200
200
  def normalize_color_prop(key, value)
201
- return value unless value.is_a?(String)
202
- return Ruflet::Colors.canonicalize(value) if color_prop_key?(key)
203
-
204
- value
201
+ Ruflet::Colors.normalize_property(key, value)
205
202
  end
206
203
 
207
204
  def color_prop_key?(key)
208
- key == "color" || key.end_with?("bgcolor") || key.end_with?("_color")
205
+ Ruflet::Colors.color_key?(key)
209
206
  end
210
207
 
211
208
  def normalize_icon_prop(key, value)
@@ -183,6 +183,7 @@ module Ruflet
183
183
  id: "_dialogs",
184
184
  controls: []
185
185
  )
186
+ @mounted_services = []
186
187
  @invoke_waiters = {}
187
188
  @invoke_callbacks = {}
188
189
  @invoke_waiters_mutex = Mutex.new
@@ -540,6 +541,12 @@ module Ruflet
540
541
  def bottom_sheet=(value)
541
542
  @bottom_sheet = value
542
543
  refresh_dialogs_container!
544
+ # Mirror dialog= / snack_bar=: once the dialogs container is mounted, push
545
+ # the change as an in-place patch. Without this, opening a bottom sheet
546
+ # after the container is already mounted (e.g. a prior dialog/snackbar)
547
+ # never reaches the client, since send_view_patch skips _dialogs once
548
+ # mounted. This is what made NativeApp's modal: sheets fail to open.
549
+ push_dialogs_update! if @dialogs_container_mounted
543
550
  end
544
551
 
545
552
  def bottomsheet=(value)
@@ -1567,6 +1574,7 @@ module Ruflet
1567
1574
  raise ArgumentError, "page #{key} must use an icon name string, not #{value.inspect}"
1568
1575
  end
1569
1576
 
1577
+ value = Ruflet::Colors.normalize_property(key, value)
1570
1578
  return value.value if value.is_a?(Ruflet::IconData)
1571
1579
  value.is_a?(Symbol) ? value.to_s : value
1572
1580
  end
@@ -1592,8 +1600,14 @@ module Ruflet
1592
1600
  query_string = route_value.to_s.split("?", 2)[1].to_s
1593
1601
  return {} if query_string.empty?
1594
1602
 
1595
- CGI.parse(query_string).each_with_object({}) do |(key, values), result|
1596
- result[key] = values.size == 1 ? values.first : values
1603
+ query_string.split("&").each_with_object({}) do |pair, result|
1604
+ next if pair.empty?
1605
+
1606
+ key, value = pair.split("=", 2)
1607
+ key = CGI.unescape(key.to_s.tr("+", " "))
1608
+ value = CGI.unescape(value.to_s.tr("+", " "))
1609
+ previous = result[key]
1610
+ result[key] = previous.nil? ? value : Array(previous) << value
1597
1611
  end
1598
1612
  end
1599
1613
 
@@ -1701,14 +1715,61 @@ module Ruflet
1701
1715
  def push_services_update!
1702
1716
  refresh_control_indexes!
1703
1717
 
1704
- if @services_container.wire_id
1718
+ unless @services_container.wire_id
1719
+ # First mount: the whole list ships inside the page/view patch.
1720
+ send_view_patch
1721
+ @mounted_services = Array(@services_container.props["_services"]).dup
1722
+ return
1723
+ end
1724
+
1725
+ current = Array(@services_container.props["_services"])
1726
+ ops = services_patch_ops(@mounted_services, current)
1727
+ unless ops.empty?
1728
+ # Incrementally add/remove individual services. Re-sending the whole
1729
+ # `_services` list as full objects (a single replace op) makes the
1730
+ # client run Control.fromMap for EVERY service, replacing each Control
1731
+ # instance while ServiceRegistry only binds ids it hasn't seen — so an
1732
+ # already-mounted service (share, clipboard, haptic) keeps its old
1733
+ # binding while controlsIndex points at the fresh, listener-less
1734
+ # instance, and its next invoke hangs. That was the "after Copy, Share
1735
+ # stops working" bug. Add/remove ops leave untouched services intact.
1705
1736
  send_message(Protocol::ACTIONS[:patch_control], {
1706
1737
  "id" => @services_container.wire_id,
1707
- "patch" => [[0], [0, 0, "_services", serialize_patch_value(@services_container.props["_services"])]]
1738
+ "patch" => [[0, { "_services" => [1] }], *ops]
1708
1739
  })
1709
- else
1710
- send_view_patch
1711
1740
  end
1741
+ @mounted_services = current.dup
1742
+ end
1743
+
1744
+ # Diff the previously-mounted services list against the current one and emit
1745
+ # add (1) / remove (2) ops against the `_services` list (patch node 1).
1746
+ # Services are normally append-only; a moved entry is expressed as
1747
+ # remove + add. Existing entries are never re-serialized, so their client
1748
+ # Control instances (and invoke listeners) survive.
1749
+ def services_patch_ops(previous, current)
1750
+ ops = []
1751
+ working = previous.dup
1752
+
1753
+ (working.length - 1).downto(0) do |index|
1754
+ next if current.any? { |service| service.equal?(working[index]) }
1755
+
1756
+ ops << [2, 1, index]
1757
+ working.delete_at(index)
1758
+ end
1759
+
1760
+ current.each_with_index do |service, index|
1761
+ next if working[index]&.equal?(service)
1762
+
1763
+ existing = working.index { |candidate| candidate.equal?(service) }
1764
+ if existing
1765
+ ops << [2, 1, existing]
1766
+ working.delete_at(existing)
1767
+ end
1768
+ ops << [1, 1, index, serialize_patch_value(service)]
1769
+ working.insert(index, service)
1770
+ end
1771
+
1772
+ ops
1712
1773
  end
1713
1774
 
1714
1775
  def push_dialogs_update!
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruflet_core
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.16
4
+ version: 0.0.17
5
5
  platform: ruby
6
6
  authors:
7
7
  - AdamMusa
@@ -253,7 +253,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
253
253
  - !ruby/object:Gem::Version
254
254
  version: '0'
255
255
  requirements: []
256
- rubygems_version: 3.7.2
256
+ rubygems_version: 4.0.11
257
257
  specification_version: 4
258
258
  summary: Ruflet core runtime package.
259
259
  test_files: []