wabi 0.8.0 → 0.9.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: 46c62f8e76667820ba8b7e4c68692e5de2ce77d7369dcabb2abbb3d226a1880e
4
- data.tar.gz: ac059f870c7c665c0e0dcca5e1540c6a554a4fb9f23e17cb98eaf723250aea65
3
+ metadata.gz: 4427b2a777bf298e102d813c0a5ccb2b7919e2a76ed7bd3d20e56127671e5041
4
+ data.tar.gz: ddd81feca27e709970b46be2704a58939d10a8a92405d27438cbf546bf30b878
5
5
  SHA512:
6
- metadata.gz: 79428d950c54fed32896a55b83e9e4a5281c4df8a31be5f370d57e0a111ecbe130ac4f7dd657e6baf27e0de3aad893fc89fa8eaf6b61c60357f4c38c940b47eb
7
- data.tar.gz: 75cc44242f00676ff1d06bbc87cf39f426fb1594079046fbc25b0cf47acd355e3a3a9a5e2caf44778f235e0a5158bcb5aa3be100087f296db8dac07fe2c55832
6
+ metadata.gz: e462360e2f326b745027ff02f126ca10a30c3456dfb5aec439b4f41e64548a39698c48274c145eb19ec3089cd654fb5139c426af7a0ec5f98d92e7dd2ff2ebfe
7
+ data.tar.gz: b2213c0ee4685e679df481349065e42a14c1112d9ea5be3866d7e2238e33c17cc35e310a7d76d44b0b7b49011b8a490316ded97ac4c8f921530e6d0a7de1f462
data/CHANGELOG.md CHANGED
@@ -2,6 +2,52 @@
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.9.0 - 2026-05-31
6
+
7
+ DX + polish, plus a full fix of the vertical Slider.
8
+
9
+ ### Breaking
10
+
11
+ - **Slider `marks:` moved from `Slider` to `SliderControl`.** Marks now render
12
+ inside the control (the track's positioning context) so they align with the
13
+ track in both orientations. Update `Slider.new(marks: […])` →
14
+ `SliderControl.new(marks: […])` (and pass `orientation:` to `SliderControl`
15
+ for vertical). Marks were introduced in v0.8.
16
+
17
+ ### Features
18
+
19
+ - **`wabi:update` three-way merge.** Locally-edited component files are now
20
+ 3-way merged (base + local + new) via `git merge-file` instead of the
21
+ all-or-nothing prompt: non-conflicting registry changes auto-apply while your
22
+ edits are preserved; true conflicts are written with `<<<<<<<` markers. Falls
23
+ back to the y/n/d/q prompt when git is unavailable or the lockfile predates
24
+ this release. The lockfile now records `{ hash, content }` per file
25
+ (back-compat: legacy string-hash entries still load and use the fallback). A
26
+ data-safety guard never writes a file when `git merge-file` errors.
27
+ - **Combobox async error state.** New optional `ComboboxError` slot (hidden,
28
+ `aria-live`) — shown on async fetch failure (prior results kept), hidden on
29
+ the next successful fetch.
30
+
31
+ ### Fixes
32
+
33
+ - **Vertical Slider now works end-to-end.** The v0.8 slider was effectively
34
+ horizontal-only; the vertical orientation didn't render usably. Fixed across
35
+ the board, each gated by orientation so horizontal is unchanged:
36
+ - `SliderControl` fills the column height in vertical (was collapsing to 0,
37
+ leaving the track 0px tall).
38
+ - Thumb centers on the cross-axis correctly per orientation (horizontal:
39
+ vertical-center; vertical: horizontal-center on the rail).
40
+ - `SliderRange` fill is `w-full` in vertical (was zero-width / invisible) with
41
+ Zag driving the height from the value.
42
+ - Marks render inside `SliderControl`, aligned with the track (below it for
43
+ horizontal, beside it spanning its height for vertical), with the tick +
44
+ label offset oriented accordingly.
45
+
46
+ ### Deferred to v0.10
47
+
48
+ - Toast `@zag-js/toast` group machine (high-risk; failed twice).
49
+ - Phlex 2.4 Ruby 4 warnings (upstream).
50
+
5
51
  ## 0.8.0 - 2026-05-31
6
52
 
7
53
  Focused high-value mix: one marquee feature, one self-contained feature, an
@@ -42,7 +42,10 @@ module Wabi
42
42
  target = File.join(destination_root, file["path"])
43
43
  FileUtils.mkdir_p(File.dirname(target))
44
44
  File.write(target, file["content"])
45
- files_map[file["path"]] = Digest::SHA256.hexdigest(file["content"])
45
+ files_map[file["path"]] = {
46
+ "hash" => Digest::SHA256.hexdigest(file["content"]),
47
+ "content" => file["content"],
48
+ }
46
49
  say " create #{file["path"]}", :green
47
50
  end
48
51
 
@@ -3,6 +3,8 @@
3
3
  require "rails/generators"
4
4
  require "digest"
5
5
  require "json"
6
+ require "open3"
7
+ require "tempfile"
6
8
  require "wabi/registry_client"
7
9
  require "wabi/lockfile"
8
10
 
@@ -72,7 +74,7 @@ module Wabi
72
74
  new_hash = Digest::SHA256.hexdigest(file["content"])
73
75
  target = File.join(destination_root, path)
74
76
 
75
- files_map[path] = new_hash
77
+ files_map[path] = { "hash" => new_hash, "content" => file["content"] }
76
78
 
77
79
  if !File.exist?(target)
78
80
  write_file(target, file["content"], reason: "create")
@@ -80,7 +82,7 @@ module Wabi
80
82
  end
81
83
 
82
84
  on_disk_hash = Digest::SHA256.hexdigest(File.read(target))
83
- installed_hash = installed_map[path]
85
+ installed_hash = Wabi::Lockfile.file_entry(installed_map[path])[:hash]
84
86
 
85
87
  if installed_hash.nil?
86
88
  handle_conflict(path, target, file["content"], reason: "legacy lockfile has no per-file hash")
@@ -90,7 +92,12 @@ module Wabi
90
92
  if on_disk_hash == installed_hash
91
93
  write_file(target, file["content"], reason: "update")
92
94
  else
93
- handle_conflict(path, target, file["content"], reason: "edited locally")
95
+ base_content = Wabi::Lockfile.file_entry(installed_map[path])[:content]
96
+ if base_content && git_available? && !options[:force]
97
+ merge_file(path, target, base_content, file["content"])
98
+ else
99
+ handle_conflict(path, target, file["content"], reason: "edited locally")
100
+ end
94
101
  end
95
102
  end
96
103
 
@@ -150,6 +157,63 @@ module Wabi
150
157
  ask(" (y)es / (n)o / (d)iff / (q)uit?", limited_to: %w[y n d q])
151
158
  end
152
159
 
160
+ def merge_file(path, target, base_content, new_content)
161
+ if options[:dry_run]
162
+ say " would 3-way merge #{path}", :cyan
163
+ return
164
+ end
165
+
166
+ begin
167
+ merged, conflicts, err = three_way_merge(target, base_content, new_content)
168
+ rescue Errno::ENOENT, StandardError => e
169
+ # git vanished mid-run, or any other spawn/IO failure.
170
+ merged, conflicts, err = nil, 255, e.message
171
+ end
172
+
173
+ # git merge-file returns 255 (an error, NOT a conflict count) on
174
+ # failure. Treat that — or empty output when we expected content — as a
175
+ # failure and DO NOT write: blanking the user's edited file would be
176
+ # irreversible data loss. Leave the file untouched; the user can re-run.
177
+ if conflicts == 255 || conflicts.negative? || (merged.to_s.empty? && !new_content.empty?)
178
+ detail = err.to_s.strip.empty? ? "" : ": #{err.to_s.strip.lines.first&.chomp}"
179
+ say " error #{path} (git merge-file failed, file unchanged#{detail})", :red
180
+ return
181
+ end
182
+
183
+ File.write(target, merged)
184
+ if conflicts.zero?
185
+ say " merged #{path}", :green
186
+ else
187
+ # git caps the exit code at 127, so report 127+ rather than lying.
188
+ count = conflicts >= 127 ? "127+ conflicts" : "#{conflicts} conflict#{'s' if conflicts != 1}"
189
+ say " merged #{path} (#{count} — resolve the <<<<<<< markers)", :yellow
190
+ end
191
+ end
192
+
193
+ def git_available?
194
+ return @git_available unless @git_available.nil?
195
+ @git_available = system("git", "--version", out: File::NULL, err: File::NULL) || false
196
+ end
197
+
198
+ # 3-way merge via `git merge-file`. Arg order is current/base/other =
199
+ # local/base/new (DO NOT reorder). Returns [merged_string, status, stderr]:
200
+ # status 0 = clean, 1..127 = number of conflicts (git caps at 127),
201
+ # 255 = error. stderr carries git's diagnostic on error.
202
+ def three_way_merge(local_path, base_content, new_content)
203
+ Tempfile.create("wabi-base") do |base_f|
204
+ Tempfile.create("wabi-new") do |new_f|
205
+ base_f.write(base_content); base_f.flush
206
+ new_f.write(new_content); new_f.flush
207
+ merged, err, status = Open3.capture3(
208
+ "git", "merge-file", "-p",
209
+ "-L", "local (your edits)", "-L", "base (original)", "-L", "new (registry)",
210
+ local_path, base_f.path, new_f.path
211
+ )
212
+ [merged, status.exitstatus, err]
213
+ end
214
+ end
215
+ end
216
+
153
217
  def print_diff(target, new_content)
154
218
  on_disk = File.exist?(target) ? File.read(target) : ""
155
219
  on_disk_lines = on_disk.split("\n", -1)
data/lib/wabi/lockfile.rb CHANGED
@@ -27,6 +27,17 @@ module Wabi
27
27
  @components = data["components"] || {}
28
28
  end
29
29
 
30
+ # Normalizes a per-file lockfile entry to { hash:, content: }, tolerating
31
+ # both the v0.9 object shape ({ "hash" =>, "content" => }) and the legacy
32
+ # string-hash shape (content: nil → caller falls back to the prompt).
33
+ def self.file_entry(raw)
34
+ case raw
35
+ when String then { hash: raw, content: nil }
36
+ when Hash then { hash: raw["hash"], content: raw["content"] }
37
+ else { hash: nil, content: nil }
38
+ end
39
+ end
40
+
30
41
  def record(name, version:, hash:, files: nil, js_dependencies: nil)
31
42
  entry = { "version" => version, "hash" => hash }
32
43
  entry["files"] = files if files
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.8.0"
4
+ VERSION = "0.9.0"
5
5
  end
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.8.0
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Oscar Ortega