wabi 0.7.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: 1975dde8069f308dad1c022923250b6ede092df708ba5cf5fdee333554dccbbe
4
- data.tar.gz: 8e90d286204bb1ab579116ea8ab3ea2eec74262d55d20b758846b70bda85b755
3
+ metadata.gz: 4427b2a777bf298e102d813c0a5ccb2b7919e2a76ed7bd3d20e56127671e5041
4
+ data.tar.gz: ddd81feca27e709970b46be2704a58939d10a8a92405d27438cbf546bf30b878
5
5
  SHA512:
6
- metadata.gz: fa61e143320502d057ee2f9bd4e4b6618726035c491917c9af0b0767b4464068f9c94376211853eba983cb0e131e2ddc82a361711fae495bba31fc1d53e0a206
7
- data.tar.gz: dc50cb00e2cabfa8c992fbd3410f6947a76208c3b955c651771cc95f81f180ca151ab061afecfa32d743e79d5abc87b7cea5906f6b511f3b6082421a638cdf3f
6
+ metadata.gz: e462360e2f326b745027ff02f126ca10a30c3456dfb5aec439b4f41e64548a39698c48274c145eb19ec3089cd654fb5139c426af7a0ec5f98d92e7dd2ff2ebfe
7
+ data.tar.gz: b2213c0ee4685e679df481349065e42a14c1112d9ea5be3866d7e2238e33c17cc35e310a7d76d44b0b7b49011b8a490316ded97ac4c8f921530e6d0a7de1f462
data/CHANGELOG.md CHANGED
@@ -2,6 +2,123 @@
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
+
51
+ ## 0.8.0 - 2026-05-31
52
+
53
+ Focused high-value mix: one marquee feature, one self-contained feature, an
54
+ accessibility win, and an overlay-controller hardening refactor. No breaking
55
+ changes.
56
+
57
+ ### Features
58
+
59
+ - **Combobox async items.** With `url:` set, the combobox debounces input and
60
+ fetches a server-rendered `ComboboxItem` fragment (AbortController-guarded so
61
+ stale in-flight responses are dropped), swaps it into the content, and
62
+ rebuilds the Zag collection from the new DOM via `machine.updateProps`. New
63
+ optional `ComboboxLoading` slot (shown during the fetch); empty state is
64
+ server-rendered; fetch errors keep the prior results + `console.warn`.
65
+ `param` (default `q`), `debounce` (default 250ms), and `min_length`
66
+ (default 1) are configurable. Sync mode is unchanged.
67
+ - **Slider marks/ticks.** `Slider(marks: [{value:, label:}])` (or bare
68
+ Integers) renders tick markers (short vertical lines) positioned via Zag
69
+ `getMarkerProps`, with optional labels. Works single + range.
70
+
71
+ ### Fixes
72
+
73
+ - **Slider is now fully functional + visible.** Several latent issues from the
74
+ v0.6 slider are fixed together:
75
+ - **Thumb + marks render.** `thumbAlignment: "center"` on the machine — the
76
+ previous `"contain"` default gated thumb/marker visibility on a thumb-size
77
+ measurement that never completed in the vanilla Stimulus setup (the machine
78
+ starts before `render()` decorates the DOM with Zag's part ids), leaving
79
+ the thumb knob and any marks `visibility: hidden`.
80
+ - **Pointer interaction.** New `SliderControl` element carries Zag's
81
+ `getControlProps` (`onPointerDown`), so click/drag on the track now sets the
82
+ value live. Previously the slider was keyboard-only. `SliderControl` also
83
+ provides the positioning context that vertically centers the thumb on the
84
+ track (the root is a flex column with the label/marks, so an absolutely
85
+ positioned thumb anchored to it floated above the bar).
86
+ - **Hidden-input dedup.** `syncHiddenInputs` cleanup selector
87
+ (`data-wabi--slider-hidden`) never matched the inputs `appendHidden` created
88
+ via `dataset` (which emitted single-dash `data-wabi-slider-hidden`), so a
89
+ new hidden input leaked on every render and the form value went stale.
90
+ `appendHidden` now uses `setAttribute` with the matching double-dash name.
91
+ - **Thumb styling.** Smaller (12px) thumb with a thinner border + subtle
92
+ shadow; `bg-foreground` fill (adapts to theme: dark knob in light mode,
93
+ light knob in dark mode).
94
+ - **Composition note:** the track + thumb(s) now nest inside `SliderControl`
95
+ (`Slider > SliderLabel + SliderControl(SliderTrack(SliderRange) + SliderThumb…)`).
96
+
97
+ ### Accessibility
98
+
99
+ - **`motion-reduce:transition-none`** on Toast and the animated overlays
100
+ (Dialog, Drawer, Popover, Tooltip, DropdownMenu, Select, Combobox content +
101
+ Dialog/Drawer backdrops) — `prefers-reduced-motion` users get instantaneous
102
+ enter/exit instead of slide/fade.
103
+
104
+ ### Refactor / internal
105
+
106
+ - **`_shared/overlay_portal.js`** — Dialog, Popover, Tooltip, Select, and
107
+ DropdownMenu now share `capturePortalRefs` / `attachToBody` / `restoreFromBody`
108
+ (composition, not inheritance) instead of duplicating that boilerplate per
109
+ controller. Behavior-preserving (each overlay browser re-smoked).
110
+ - DropdownMenu's closest-ancestor-sub lookup is deduped into `_parentMachineFor`
111
+ (was inline in both `connect()` and `render()`).
112
+ - Overlay `disconnect()` now restores from `<body>` before unregistering from
113
+ `WabiPortalRegistry`, so the registry recomputes its sibling cache on the
114
+ post-move DOM. `data-wabi-sub-index` is cleaned up on DropdownMenu disconnect.
115
+
116
+ ### Deferred to v0.9
117
+
118
+ - Toast `@zag-js/toast` group machine; `wabi:update` three-way merge; richer
119
+ Combobox async error UX; vertical-orientation Slider mark label offset; Phlex
120
+ 2.4 Ruby 4 warnings (upstream).
121
+
5
122
  ## 0.7.0 - 2026-05-30
6
123
 
7
124
  Quality + finish: 10 items closing v0.6 deferrals and v0.5 long-tail. No new
@@ -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.7.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.7.0
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Oscar Ortega