wabi 0.4.0 → 0.5.0

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: 5c5512565440dd12e42434dceb8522dbd9b0757e41b63981617e8a645f69bb37
4
- data.tar.gz: 2f0c77f0a635bcd6bb2673fb403db9d9247bc916a4adf77ef9398a6816e3fbff
3
+ metadata.gz: 0660623d8f6e54a685e9793a3e1ca16e9ac56fcf7150e061c0e80835b8fbe9bd
4
+ data.tar.gz: ecbbce636fe38adf58c7931c208dbbe7000cc6b737ce516c286396c483f8e2e9
5
5
  SHA512:
6
- metadata.gz: 2a4eaf8d667a84e467238e9ba0b4f9cd14d9a5b685698ea82ed3c7ed443bac56b5ddc3c35bf68299947ec38ba7ed49b7d2a6fc0799a5311fc98e3aa9a443b5fe
7
- data.tar.gz: 1f03f62dbcf1907505f19dd570d2ca7e06b15cbb9ad3cacb579885deba29b947057da789296981cf6ccafbca2c502989d400bb8e2011e84e0550d20dd49b3419
6
+ metadata.gz: 7c197e08b3f8cdea3f5587fc265495af390b12a185a030daf7add62fcf17dafd156d7d0466c39f0c943fff8400b486b46a09ce20a85c34fa89bd463056897ff3
7
+ data.tar.gz: f8f0d96bad57dc8889221364e06e594bec7f8762ad1373715a308f5a312751e3335347d9b54895dae4c6c29da441900220e318e9d389c65bbd65295efd4eb924
data/CHANGELOG.md CHANGED
@@ -2,6 +2,45 @@
2
2
 
3
3
  All notable changes to Wabi land here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
4
4
 
5
+ ## 0.5.0 - 2026-05-27
6
+
7
+ ### Breaking
8
+
9
+ - **Overlays portal to `document.body` by default**. Dialog, Drawer,
10
+ Tooltip, Popover, DropdownMenu, and Select all move their content (and
11
+ backdrop/positioner where applicable) to `<body>` on connect. Pass
12
+ `portal: false` to keep v0.4 in-tree behavior. The shared
13
+ `WabiPortalRegistry` JS module is now part of every overlay's wabi:add
14
+ install — it lands at
15
+ `app/javascript/controllers/wabi/_shared/portal_registry.js`.
16
+
17
+ ### Features
18
+
19
+ - `wabi:update` generator. Per-component diff-aware updates with
20
+ per-file conflict detection. Flags: `--force`, `--dry-run`. Reads
21
+ and writes per-file SHA256 hashes in `wabi.lock.json` (additive
22
+ schema; legacy lockfiles fall back to prompting on every file).
23
+ - Real portal pattern for the 6 overlays. Resolves the v0.1 carryover
24
+ documented in `docs/V01-CARRYOVER.md` (#3). Overlays inside
25
+ transformed/scrollable ancestors now position relative to the
26
+ viewport.
27
+
28
+ ### Docs
29
+
30
+ - `+esm` jsdelivr footnote added to 5 component pages
31
+ (checkbox/select/switch/dialog/tabs) for parity with the other 5
32
+ Zag-backed components.
33
+
34
+ ### Deferred to v0.6
35
+
36
+ - Toast `@zag-js/toast` group machine. Attempted in v0.5 but reverted — the group machine's interaction with Phlex+Stimulus+Turbo caused an infinite render loop. v0.4 vanilla setTimeout controller restored; group features (max/gap/pause-on-group-hover/swipe) targeted for v0.6 with a different implementation strategy.
37
+ - Forms wave (RadioGroup/Toggle/ToggleGroup/Slider/Combobox/Command/Form).
38
+ - Nav wave (Sheet/ContextMenu/Pagination/NavigationMenu).
39
+ - Data wave (Calendar/DatePicker/DataTable).
40
+ - Three-way merge in `wabi:update`.
41
+ - Multi-level DropdownMenu nesting (sub-inside-sub).
42
+ - Phlex 2.4 Ruby 4 warnings (upstream).
43
+
5
44
  ## [0.4.0] — 2026-05-27
6
45
 
7
46
  Sprint 8 — docs completeness + theme polish. The docs site gains the
@@ -37,17 +37,20 @@ module Wabi
37
37
 
38
38
  Array(data["registry_dependencies"]).each { |dep| install_component(dep) }
39
39
 
40
+ files_map = {}
40
41
  data["files"].each do |file|
41
42
  target = File.join(destination_root, file["path"])
42
43
  FileUtils.mkdir_p(File.dirname(target))
43
44
  File.write(target, file["content"])
45
+ files_map[file["path"]] = Digest::SHA256.hexdigest(file["content"])
44
46
  say " create #{file["path"]}", :green
45
47
  end
46
48
 
47
49
  (data["js_dependencies"] || {}).each { |pkg, ver| @js_deps_to_pin[pkg] = ver }
48
50
 
49
51
  hash = Digest::SHA256.hexdigest(JSON.generate(data["files"]))
50
- lockfile.record(name, version: data["version"], hash: hash)
52
+ lockfile.record(name, version: data["version"], hash: hash, files: files_map,
53
+ js_dependencies: data["js_dependencies"])
51
54
  end
52
55
 
53
56
  def print_js_pin_instructions
@@ -0,0 +1,184 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "digest"
5
+ require "json"
6
+ require "wabi/registry_client"
7
+ require "wabi/lockfile"
8
+
9
+ module Wabi
10
+ module Generators
11
+ class UpdateGenerator < Rails::Generators::Base
12
+ argument :components, type: :array, default: [],
13
+ banner: "[component_name ...]"
14
+
15
+ class_option :force, type: :boolean, default: false,
16
+ desc: "Overwrite locally-edited files without prompting."
17
+ class_option :dry_run, type: :boolean, default: false,
18
+ desc: "Report planned changes, write nothing."
19
+
20
+ desc "Update installed Wabi components to the latest registry versions."
21
+
22
+ class UpdateAborted < StandardError; end
23
+
24
+ def update_components
25
+ names = target_names
26
+ if names.empty?
27
+ say " no components to update", :yellow
28
+ return
29
+ end
30
+ names.each { |name| update_component(name) }
31
+ lockfile.save unless options[:dry_run]
32
+ rescue UpdateAborted => e
33
+ say " aborted: #{e.message}", :red
34
+ end
35
+
36
+ private
37
+
38
+ def target_names
39
+ components.any? ? components : lockfile.components.keys
40
+ end
41
+
42
+ def lockfile
43
+ @lockfile ||= Wabi::Lockfile.load(File.join(destination_root, "config/wabi.lock.json"))
44
+ end
45
+
46
+ def client
47
+ @client ||= Wabi::RegistryClient.new(base_url: lockfile.registry)
48
+ end
49
+
50
+ def update_component(name)
51
+ unless lockfile.components.key?(name)
52
+ say " not installed: #{name} (use `wabi:add #{name}`)", :yellow
53
+ return
54
+ end
55
+
56
+ installed_version = lockfile.components[name]["version"]
57
+ data = client.fetch(name)
58
+ remote_version = data["version"]
59
+
60
+ if Gem::Version.new(remote_version) <= Gem::Version.new(installed_version)
61
+ say " up-to-date #{name} (#{installed_version})", :cyan
62
+ return
63
+ end
64
+
65
+ say " updating #{name} (#{installed_version} -> #{remote_version})", :green
66
+
67
+ files_map = {}
68
+ installed_map = lockfile.components[name]["files"] || {}
69
+
70
+ data["files"].each do |file|
71
+ path = file["path"]
72
+ new_hash = Digest::SHA256.hexdigest(file["content"])
73
+ target = File.join(destination_root, path)
74
+
75
+ files_map[path] = new_hash
76
+
77
+ if !File.exist?(target)
78
+ write_file(target, file["content"], reason: "create")
79
+ next
80
+ end
81
+
82
+ on_disk_hash = Digest::SHA256.hexdigest(File.read(target))
83
+ installed_hash = installed_map[path]
84
+
85
+ if installed_hash.nil?
86
+ handle_conflict(path, target, file["content"], reason: "legacy lockfile has no per-file hash")
87
+ next
88
+ end
89
+
90
+ if on_disk_hash == installed_hash
91
+ write_file(target, file["content"], reason: "update")
92
+ else
93
+ handle_conflict(path, target, file["content"], reason: "edited locally")
94
+ end
95
+ end
96
+
97
+ print_js_deps_diff(name, data["js_dependencies"])
98
+
99
+ lockfile.record(
100
+ name,
101
+ version: remote_version,
102
+ hash: Digest::SHA256.hexdigest(JSON.generate(data["files"])),
103
+ files: files_map,
104
+ js_dependencies: data["js_dependencies"],
105
+ )
106
+ end
107
+
108
+ def write_file(target, content, reason:)
109
+ if options[:dry_run]
110
+ say " would write #{target} (#{reason})", :cyan
111
+ return
112
+ end
113
+ FileUtils.mkdir_p(File.dirname(target))
114
+ File.write(target, content)
115
+ say " #{reason.ljust(10)} #{target}", :green
116
+ end
117
+
118
+ def handle_conflict(path, target, new_content, reason:)
119
+ if options[:force]
120
+ write_file(target, new_content, reason: "force-overwrite")
121
+ return
122
+ end
123
+ if options[:dry_run]
124
+ say " CONFLICT #{path} (#{reason}) - would prompt", :yellow
125
+ return
126
+ end
127
+
128
+ loop do
129
+ say ""
130
+ say " conflict #{path}", :yellow
131
+ say " #{reason}. Overwrite with new version?", :yellow
132
+ answer = prompt_conflict(path)
133
+ case answer
134
+ when "y"
135
+ write_file(target, new_content, reason: "overwrite")
136
+ return
137
+ when "n"
138
+ say " skipped #{path}", :cyan
139
+ return
140
+ when "d"
141
+ print_diff(target, new_content)
142
+ next
143
+ when "q"
144
+ raise UpdateAborted, "user aborted update"
145
+ end
146
+ end
147
+ end
148
+
149
+ def prompt_conflict(_path)
150
+ ask(" (y)es / (n)o / (d)iff / (q)uit?", limited_to: %w[y n d q])
151
+ end
152
+
153
+ def print_diff(target, new_content)
154
+ on_disk = File.exist?(target) ? File.read(target) : ""
155
+ on_disk_lines = on_disk.split("\n", -1)
156
+ new_lines = new_content.split("\n", -1)
157
+ say ""
158
+ say " --- on-disk", :red
159
+ on_disk_lines.each { |l| say " - #{l}", :red }
160
+ say " +++ new", :green
161
+ new_lines.each { |l| say " + #{l}", :green }
162
+ say ""
163
+ end
164
+
165
+ def print_js_deps_diff(name, new_deps)
166
+ new_deps ||= {}
167
+ old_deps = lockfile.components.dig(name, "js_dependencies") || {}
168
+
169
+ added = new_deps.reject { |pkg, _| old_deps.key?(pkg) }
170
+ changed = new_deps.select { |pkg, ver| old_deps.key?(pkg) && old_deps[pkg] != ver }
171
+
172
+ return if added.empty? && changed.empty?
173
+
174
+ say ""
175
+ say " JS pin changes for #{name}:", :yellow
176
+ (added.merge(changed)).each do |pkg, version|
177
+ v = version.to_s.sub(/\A[~^]/, "")
178
+ v = "1.0.0" if v.empty?
179
+ say %( pin "#{pkg}", to: "https://cdn.jsdelivr.net/npm/#{pkg}@#{v}/+esm")
180
+ end
181
+ end
182
+ end
183
+ end
184
+ end
data/lib/wabi/lockfile.rb CHANGED
@@ -27,8 +27,11 @@ module Wabi
27
27
  @components = data["components"] || {}
28
28
  end
29
29
 
30
- def record(name, version:, hash:)
31
- @components[name] = { "version" => version, "hash" => hash }
30
+ def record(name, version:, hash:, files: nil, js_dependencies: nil)
31
+ entry = { "version" => version, "hash" => hash }
32
+ entry["files"] = files if files
33
+ entry["js_dependencies"] = js_dependencies if js_dependencies
34
+ @components[name] = entry
32
35
  end
33
36
 
34
37
  def save
data/lib/wabi/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Wabi
4
- VERSION = "0.4.0"
4
+ VERSION = "0.5.0"
5
5
  end
data/lib/wabi.rb CHANGED
@@ -16,6 +16,7 @@ end
16
16
  if defined?(Rails::Generators)
17
17
  require_relative "wabi/generators/install_generator"
18
18
  require_relative "wabi/generators/add_generator"
19
+ require_relative "wabi/generators/update_generator"
19
20
  require_relative "wabi/generators/list_generator"
20
21
  require_relative "wabi/generators/registry_generator"
21
22
  require_relative "wabi/generators/theme_generator"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: wabi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Oscar Ortega
@@ -126,6 +126,7 @@ files:
126
126
  - lib/wabi/generators/list_generator.rb
127
127
  - lib/wabi/generators/registry_generator.rb
128
128
  - lib/wabi/generators/theme_generator.rb
129
+ - lib/wabi/generators/update_generator.rb
129
130
  - lib/wabi/lockfile.rb
130
131
  - lib/wabi/registry_client.rb
131
132
  - lib/wabi/turbo_stream_extensions.rb