wabi 0.8.0 → 0.10.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: 917f6232c33f67a4dfd39c41f80f22f04c238dce585f27ee262806804905611e
4
+ data.tar.gz: ee3d4df53acf6fa939dc3332ebd4963d07d9c007b75957e7dafd8350ea22f8c2
5
5
  SHA512:
6
- metadata.gz: 79428d950c54fed32896a55b83e9e4a5281c4df8a31be5f370d57e0a111ecbe130ac4f7dd657e6baf27e0de3aad893fc89fa8eaf6b61c60357f4c38c940b47eb
7
- data.tar.gz: 75cc44242f00676ff1d06bbc87cf39f426fb1594079046fbc25b0cf47acd355e3a3a9a5e2caf44778f235e0a5158bcb5aa3be100087f296db8dac07fe2c55832
6
+ metadata.gz: '097aef93667322fd86b77f1eead744acd4bb55d33eb1812143e5a5c690262f67484cc9ed00b055e8fd748b9d7a1ce3dfa8101afbd1c1501fa10134209161b8e1'
7
+ data.tar.gz: b49022683f977aec56c959b97e206b27b892a34c606b2a44432a77d68e43836c9795e34010538af6d9bb01151c31d8a4efb52c1d02be30bbd4b7dabcfc09b437
data/CHANGELOG.md CHANGED
@@ -2,6 +2,75 @@
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.10.0 - 2026-06-01
6
+
7
+ Toast group coordination — the last long-deferred carryover.
8
+
9
+ ### Features
10
+
11
+ - **Toast Sonner-style stacking.** Toasts now collapse into a peek stack
12
+ (`visible_count`, default 3) and expand to a full spaced list on group
13
+ hover/focus. Overflow toasts are kept and surface as front toasts dismiss
14
+ (their timers hold until visible). Hovering the group pauses every timer.
15
+ `Toaster.new(visible_count:, gap:)` configure the stack. Built on a custom
16
+ two-controller coordinator (`wabi--toaster` + `wabi--toast` via Stimulus
17
+ outlets) — **not** `@zag-js/toast`, whose imperative DOM-creation model looped
18
+ against Wabi's SSR + Turbo Stream append in v0.5.
19
+ - **Swipe-to-dismiss.** Drag a toast horizontally past a threshold to dismiss it.
20
+
21
+ ### Breaking
22
+
23
+ - **Toast no longer bakes in a `translate-x` enter/exit transform.** The
24
+ slide/scale is now JS-driven inline styles; the Tailwind base fades via
25
+ opacity only. Apps that overrode the toast transform via `class:` should
26
+ remove `translate-x-*` overrides.
27
+
28
+ ## 0.9.0 - 2026-05-31
29
+
30
+ DX + polish, plus a full fix of the vertical Slider.
31
+
32
+ ### Breaking
33
+
34
+ - **Slider `marks:` moved from `Slider` to `SliderControl`.** Marks now render
35
+ inside the control (the track's positioning context) so they align with the
36
+ track in both orientations. Update `Slider.new(marks: […])` →
37
+ `SliderControl.new(marks: […])` (and pass `orientation:` to `SliderControl`
38
+ for vertical). Marks were introduced in v0.8.
39
+
40
+ ### Features
41
+
42
+ - **`wabi:update` three-way merge.** Locally-edited component files are now
43
+ 3-way merged (base + local + new) via `git merge-file` instead of the
44
+ all-or-nothing prompt: non-conflicting registry changes auto-apply while your
45
+ edits are preserved; true conflicts are written with `<<<<<<<` markers. Falls
46
+ back to the y/n/d/q prompt when git is unavailable or the lockfile predates
47
+ this release. The lockfile now records `{ hash, content }` per file
48
+ (back-compat: legacy string-hash entries still load and use the fallback). A
49
+ data-safety guard never writes a file when `git merge-file` errors.
50
+ - **Combobox async error state.** New optional `ComboboxError` slot (hidden,
51
+ `aria-live`) — shown on async fetch failure (prior results kept), hidden on
52
+ the next successful fetch.
53
+
54
+ ### Fixes
55
+
56
+ - **Vertical Slider now works end-to-end.** The v0.8 slider was effectively
57
+ horizontal-only; the vertical orientation didn't render usably. Fixed across
58
+ the board, each gated by orientation so horizontal is unchanged:
59
+ - `SliderControl` fills the column height in vertical (was collapsing to 0,
60
+ leaving the track 0px tall).
61
+ - Thumb centers on the cross-axis correctly per orientation (horizontal:
62
+ vertical-center; vertical: horizontal-center on the rail).
63
+ - `SliderRange` fill is `w-full` in vertical (was zero-width / invisible) with
64
+ Zag driving the height from the value.
65
+ - Marks render inside `SliderControl`, aligned with the track (below it for
66
+ horizontal, beside it spanning its height for vertical), with the tick +
67
+ label offset oriented accordingly.
68
+
69
+ ### Deferred to v0.10
70
+
71
+ - Toast `@zag-js/toast` group machine (high-risk; failed twice).
72
+ - Phlex 2.4 Ruby 4 warnings (upstream).
73
+
5
74
  ## 0.8.0 - 2026-05-31
6
75
 
7
76
  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.10.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.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Oscar Ortega