wabi 0.4.0 → 0.7.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: 1975dde8069f308dad1c022923250b6ede092df708ba5cf5fdee333554dccbbe
4
+ data.tar.gz: 8e90d286204bb1ab579116ea8ab3ea2eec74262d55d20b758846b70bda85b755
5
5
  SHA512:
6
- metadata.gz: 2a4eaf8d667a84e467238e9ba0b4f9cd14d9a5b685698ea82ed3c7ed443bac56b5ddc3c35bf68299947ec38ba7ed49b7d2a6fc0799a5311fc98e3aa9a443b5fe
7
- data.tar.gz: 1f03f62dbcf1907505f19dd570d2ca7e06b15cbb9ad3cacb579885deba29b947057da789296981cf6ccafbca2c502989d400bb8e2011e84e0550d20dd49b3419
6
+ metadata.gz: fa61e143320502d057ee2f9bd4e4b6618726035c491917c9af0b0767b4464068f9c94376211853eba983cb0e131e2ddc82a361711fae495bba31fc1d53e0a206
7
+ data.tar.gz: dc50cb00e2cabfa8c992fbd3410f6947a76208c3b955c651771cc95f81f180ca151ab061afecfa32d743e79d5abc87b7cea5906f6b511f3b6082421a638cdf3f
data/CHANGELOG.md CHANGED
@@ -2,6 +2,181 @@
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.7.0 - 2026-05-30
6
+
7
+ Quality + finish: 10 items closing v0.6 deferrals and v0.5 long-tail. No new
8
+ components; one breaking change.
9
+
10
+ ### Breaking
11
+
12
+ - **Slider range hidden inputs** now use Rails-native bracket params
13
+ (`name[min]` / `name[max]`) instead of v0.6's flat `name_min` /
14
+ `name_max`. Apps consuming range params must update their strong params
15
+ from `params.permit(:price_min, :price_max)` to
16
+ `params.require(:price).permit(:min, :max)`. Single-thumb sliders are
17
+ unchanged (`name=<value>`).
18
+
19
+ ### Features
20
+
21
+ - **Combobox `ComboboxItemIndicator`** now wires through `getItemIndicatorProps`
22
+ and toggles `hidden` based on the item's selected state (was unwired in v0.6).
23
+ - **Combobox per-item `disabled`** flag now works end-to-end: an item can carry
24
+ `{ value, label, disabled: true }` and the controller passes `isItemDisabled`
25
+ to the Zag collection so Zag stamps `data-disabled` on the matching `<li>` for
26
+ the `data-[disabled]:*` Tailwind variants.
27
+ - **Command palette item selection closes the dialog.** The bridge controller
28
+ listens at `document` level for `wabi--combobox:change` and filters by id
29
+ linkage (`data-wabi--command-id`, read via `getAttribute` — the double-dash
30
+ attribute does not round-trip through `dataset`) to survive the dialog portal
31
+ move. The controllers are ordered `wabi--command wabi--dialog` so the bridge
32
+ stamps the dialog content before the dialog portals it. The "Preview in v0.6"
33
+ callout is removed.
34
+ - **Command auto-opens the combobox when the dialog opens** so item clicks work
35
+ immediately, without typing first. The combobox builds its collection from the
36
+ rendered items when no `items` value is supplied.
37
+ - **Toast Sonner-style animations.** Slides in from the right on enter and slides
38
+ back out on exit — pure CSS transitions on `data-state`, no plugin dependency.
39
+ - **`window.wabiToasters[id]`** keyed registry (via a new `wabi--toaster`
40
+ controller on the `<ol>`) for pages with multiple Toaster instances.
41
+ `turbo_stream.wabi_toast(toaster_id: "alerts", ...)` targets a specific one.
42
+ `window.wabiToaster` remains as a deprecated alias to the most-recently-
43
+ connected toaster for back-compat.
44
+ - **DropdownMenu N-level submenus.** Sub-inside-sub nesting now works to
45
+ arbitrary depth (v0.6 was single-level only). Each sub links to its closest
46
+ ancestor sub or the root menu.
47
+
48
+ ### Cleanup / Perf
49
+
50
+ - **Vestigial `*-target="portal"` wrappers removed** from the portal-using
51
+ overlays (Dialog, Drawer, Command dialog). The wrapper was a v0.4 placeholder
52
+ for `portal: false` mode and added no behavior in v0.5+. (Popover, Tooltip,
53
+ DropdownMenu, and Select had already been cleaned in a prior refactor.)
54
+ - **`WabiPortalRegistry.applyInert`** caches the body-siblings list. The cache is
55
+ invalidated only when register/unregister changes the portal node set;
56
+ `onOpenChange` reuses the cache and just toggles the inert attribute.
57
+
58
+ ### Deferred to v0.8
59
+
60
+ - Combobox async items (server-side fetching).
61
+ - Toast `@zag-js/toast` group machine retry.
62
+ - `wabi:update` three-way merge on conflict.
63
+ - Slider marks/ticks.
64
+ - Overlay controller boilerplate refactor (the overlay controllers share
65
+ attach/restore boilerplate; the DropdownMenu ancestor-lookup is duplicated
66
+ between `connect()` and `render()`).
67
+ - `motion-reduce:transition-none` on Toast (and overlays) for reduced-motion.
68
+ - `WabiPortalRegistry` unregister/restoreFromBody ordering tidy.
69
+
70
+ ## 0.6.0 - 2026-05-28
71
+
72
+ ### Features
73
+
74
+ - **Forms wave** — 7 new components bring Wabi to 27 total:
75
+ - `Toggle` — pressable toggle button (Bold/Italic style). Distinct from
76
+ Switch, which is the sliding control. Variants: appearance
77
+ (default/outline) and size (default/sm/lg). Powered by `@zag-js/toggle`.
78
+ - `RadioGroup` + `RadioGroupItem` + `RadioGroupIndicator` —
79
+ single-select radio group with keyboard navigation via
80
+ `@zag-js/radio-group`. Hidden `<input type="radio">` per item for
81
+ form submission.
82
+ - `ToggleGroup` + `ToggleGroupItem` — group of toggles. `type: :single`
83
+ enforces single selection (radio-like with button styling);
84
+ `type: :multiple` allows multiple simultaneous selections. Hidden
85
+ inputs emitted by the controller (`name` for single, `name[]` for
86
+ multiple).
87
+ - `Slider` + `SliderLabel` + `SliderTrack` + `SliderRange` +
88
+ `SliderThumb` — value picker. Accepts Integer (single thumb) or
89
+ Array (range mode). Vertical orientation via
90
+ `orientation: :vertical`. Range inputs submit as `name_min`/`name_max`.
91
+ - `Combobox` + 8 sub-components — input with autocomplete dropdown.
92
+ Static items only in v0.6 (async deferred to v0.7). Uses the
93
+ Sprint 9 portal pattern as a non-modal anchored overlay. A sibling
94
+ `<input type="hidden">` mirrors the selected value so form
95
+ submission posts the value, not the typed label.
96
+ - `Form` + `FormField` + `FormLabel` + `FormDescription` +
97
+ `FormMessage` — Phlex wrapper over Rails' `form_with` helper.
98
+ FormMessage auto-extracts ActiveModel errors via `model:` + `field:`
99
+ kwargs, with `text:` override for explicit messages. FormLabel uses
100
+ `for_:` (matching the standalone `Label` convention).
101
+ - `Command` + 7 sub-components — Cmd+K palette. Composition of Dialog
102
+ (modal) and Combobox (filterable list). The combobox controller is
103
+ mounted on an inner wrapper so it can't overwrite the dialog's
104
+ role / aria-modal / data-state attributes (a trap discovered during
105
+ smoke testing — `spreadProps` from `@zag-js/vanilla` strips existing
106
+ attrs). Selection-closes-palette is **deferred to v0.7** (the
107
+ `wabi--command` bridge can't reach across the dialog portal yet).
108
+
109
+ ### Docs
110
+
111
+ - Form docs page demonstrates a multi-field example (name + email + bio
112
+ + newsletter checkbox + submit) with client-side validation and
113
+ inline success / per-field error messages — no page reload.
114
+ - Sidebar preserves its scroll position across Turbo navigations and
115
+ highlights the active component with `bg-accent + font-semibold +
116
+ shadow-sm` via Tailwind's `aria-[current=page]:` arbitrary variant.
117
+ - Docs `Pagefind` indexer now derives `ROUTES_TO_INDEX` from
118
+ `ComponentsController::ALL` instead of a hand-maintained list, so
119
+ future component additions are crawled automatically.
120
+
121
+ ### Deferred to v0.7
122
+
123
+ - Combobox async items (server-side fetching via Turbo Frame or
124
+ callback).
125
+ - Combobox `ComboboxItemIndicator` wiring (exported but not consumed by
126
+ the controller render loop).
127
+ - Combobox `disabled:` on individual items (data attribute is set but
128
+ the collection doesn't pass `isItemDisabled`).
129
+ - Slider marks/ticks at specific track positions.
130
+ - Command palette item selection auto-closing the dialog (the
131
+ `wabi--command` bridge listener doesn't cross the dialog portal —
132
+ needs Stimulus Outlets or a document-level listener).
133
+ - Toast `@zag-js/toast` group machine retry + Sonner-style animations
134
+ (carried over from v0.5).
135
+ - Overlay controller boilerplate refactor (the now-six overlay
136
+ controllers share ~50 LOC of attach/restore + ref capture).
137
+ - Vestigial `wabi--<name>-target="portal"` wrappers cleanup.
138
+ - DropdownMenu multi-level submenu nesting.
139
+ - `wabi:update` three-way merge.
140
+
141
+ ## 0.5.0 - 2026-05-27
142
+
143
+ ### Breaking
144
+
145
+ - **Overlays portal to `document.body` by default**. Dialog, Drawer,
146
+ Tooltip, Popover, DropdownMenu, and Select all move their content (and
147
+ backdrop/positioner where applicable) to `<body>` on connect. Pass
148
+ `portal: false` to keep v0.4 in-tree behavior. The shared
149
+ `WabiPortalRegistry` JS module is now part of every overlay's wabi:add
150
+ install — it lands at
151
+ `app/javascript/controllers/wabi/_shared/portal_registry.js`.
152
+
153
+ ### Features
154
+
155
+ - `wabi:update` generator. Per-component diff-aware updates with
156
+ per-file conflict detection. Flags: `--force`, `--dry-run`. Reads
157
+ and writes per-file SHA256 hashes in `wabi.lock.json` (additive
158
+ schema; legacy lockfiles fall back to prompting on every file).
159
+ - Real portal pattern for the 6 overlays. Resolves the v0.1 carryover
160
+ documented in `docs/V01-CARRYOVER.md` (#3). Overlays inside
161
+ transformed/scrollable ancestors now position relative to the
162
+ viewport.
163
+
164
+ ### Docs
165
+
166
+ - `+esm` jsdelivr footnote added to 5 component pages
167
+ (checkbox/select/switch/dialog/tabs) for parity with the other 5
168
+ Zag-backed components.
169
+
170
+ ### Deferred to v0.6
171
+
172
+ - 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.
173
+ - Forms wave (RadioGroup/Toggle/ToggleGroup/Slider/Combobox/Command/Form).
174
+ - Nav wave (Sheet/ContextMenu/Pagination/NavigationMenu).
175
+ - Data wave (Calendar/DatePicker/DataTable).
176
+ - Three-way merge in `wabi:update`.
177
+ - Multi-level DropdownMenu nesting (sub-inside-sub).
178
+ - Phlex 2.4 Ruby 4 warnings (upstream).
179
+
5
180
  ## [0.4.0] — 2026-05-27
6
181
 
7
182
  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.7.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.7.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