wabi 0.15.0 → 0.16.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: 5da6047adf08752f3a94d9e9d10828e5eb68f6e597f0193608a7bcabd3125a6e
4
- data.tar.gz: 2874c23f200ecc7b714e21a27a85ca6808192e6bf82e3a59f3baa21c1c5f530c
3
+ metadata.gz: d4495a8631cd5ad14cac92e522f0bdd4f1409ec525d9ca7772e806c6a2556947
4
+ data.tar.gz: 5434b6d05819812507dd79b35b4e8d5de17b18e0f3972be980cab3ee3073c6a3
5
5
  SHA512:
6
- metadata.gz: a36925fa1db4d383e3b2435331a3f5c1f8ec639a702ef43483cd165b4f4abb33482cd188459ca9f4fcf7bc6928953caaa27ef2a9c08c1682a1399e21bc302199
7
- data.tar.gz: e3cf19e0882ad6bcb472bc46650ba69b91feafc2fae65e46f383a16dfc8a01abc66c94bb0b8fb4b06278fc0da7f3b6bc5c42f63ff6ab3b2f53d0fe3117aaa576
6
+ metadata.gz: c2fa7aa63c365c998c8b1c842e042d6c52de53ee28f9ec9ca23a4ee252cbb8716a9c48500cc1659d518ed78fed2b8b0a02b98a528e5f3eea8227bb610cebd62d
7
+ data.tar.gz: 8e555b3a09a17aed90cff323d97480199ff85b1f236170e90441a6b20551f908a5e24d16a35c3c8e0a8d19641614fd8f3fd9b778606c4cb50dc6497fa08482ff
data/CHANGELOG.md CHANGED
@@ -2,6 +2,28 @@
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.16.0 - 2026-06-03
6
+
7
+ Local Zag vendoring for offline / strict-CSP installs, plus a `:pill` style
8
+ variant for Tabs.
9
+
10
+ ### Added
11
+
12
+ - **`wabi:vendor` generator — opt-in local vendoring of Zag (offline / strict-CSP).**
13
+ Components pin Zag from the jsDelivr `+esm` CDN by default, which fails for
14
+ offline, air-gapped, or strict-CSP apps. `bin/rails g wabi:vendor` reads the
15
+ jsDelivr `+esm` pins from `config/importmap.rb`, downloads the full transitive
16
+ `+esm` graph into `vendor/javascript/`, rewrites every cross-package CDN
17
+ reference (including subpath exports like `@floating-ui/utils/dom`) to a bare
18
+ specifier, and repins each package at its local copy — so no controller loads
19
+ from the CDN at runtime. Pass package names to vendor a subset. `wabi:add` now
20
+ hints at it when a component needs Zag. CDN remains the default.
21
+ - **Tabs `:pill` variant.** `Tabs.new(variant: :pill)` renders a rounded
22
+ container with a solid-primary active pill, as an alternative to the default
23
+ `:standard` segmented look. The variant is set once on the root and propagates
24
+ to `TabsList`/`TabsTrigger` via a `group-data-[variant=pill]` marker — no
25
+ changes to how triggers are written, and the Stimulus controller is unchanged.
26
+
5
27
  ## 0.15.0 - 2026-06-03
6
28
 
7
29
  Adds the **NumberInput** component.
data/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  Wabi is an open-source UI component library for **Ruby on Rails 8**, built on **Phlex + Tailwind 4 + Stimulus + Hotwire**. Inspired by shadcn/ui, components are *copied* into your app — you own the code, customize freely, no upstream API to drift away from.
6
6
 
7
- 🎉 **Status:** v0.15.0 alpha — [available on RubyGems](https://rubygems.org/gems/wabi). 35 components, 8 theme palettes, WCAG-AA targeted, live docs + registry at [wabi-docs.onrender.com](https://wabi-docs.onrender.com).
7
+ 🎉 **Status:** v0.16.0 alpha — [available on RubyGems](https://rubygems.org/gems/wabi). 35 components, 8 theme palettes, WCAG-AA targeted, live docs + registry at [wabi-docs.onrender.com](https://wabi-docs.onrender.com).
8
8
 
9
9
  ---
10
10
 
@@ -108,6 +108,7 @@ That's a fully-accessible modal with focus trap, scroll lock, backdrop click, Es
108
108
  | `wabi:list` | Lists all available components in the configured registry. |
109
109
  | `wabi:registry <url>` | Switches the active registry origin (default: `https://wabi-docs.onrender.com/r`). |
110
110
  | `wabi:theme <slug>` | Swaps `tokens.css` for the requested palette. Run `bin/rails tailwindcss:build` after. |
111
+ | `wabi:vendor [pkg…]` | **Offline / strict-CSP.** Downloads the Zag `+esm` dependency graph for your jsDelivr-pinned packages into `vendor/javascript/` and repins `config/importmap.rb` at the local copies, so no controller loads from the CDN at runtime. Default: every jsDelivr `+esm` pin in the importmap. |
111
112
 
112
113
  ---
113
114
 
@@ -68,6 +68,8 @@ module Wabi
68
68
  say " imports (like @zag-js/*) — it only downloads the entry file, leaving relative"
69
69
  say " imports unresolved. The `+esm` endpoint above ships a single bundle with all"
70
70
  say " transitive deps resolved to absolute URLs.", :yellow
71
+ say "\n Offline / strict-CSP app? Run `bin/rails g wabi:vendor` to download Zag", :yellow
72
+ say " into vendor/javascript/ and pin the local copies instead of the CDN.", :yellow
71
73
  end
72
74
  end
73
75
  end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+ require "rails/generators"
3
+ require "net/http"
4
+ require "uri"
5
+ require "fileutils"
6
+ require "wabi/zag_vendor"
7
+
8
+ module Wabi
9
+ module Generators
10
+ class VendorGenerator < Rails::Generators::Base
11
+ argument :packages, type: :array, default: [],
12
+ banner: "[@scope/pkg ...] (default: all jsDelivr +esm pins in config/importmap.rb)"
13
+
14
+ desc "Download Zag's +esm graph into vendor/javascript and pin it locally (offline / strict-CSP)."
15
+
16
+ def vendor
17
+ roots = resolve_roots
18
+ if roots.empty?
19
+ say " No jsDelivr +esm pins found in config/importmap.rb (nothing to vendor).", :yellow
20
+ return
21
+ end
22
+
23
+ result = Wabi::ZagVendor.call(roots, fetcher: method(:http_get))
24
+
25
+ result.files.each do |pkg, content|
26
+ target = File.join(destination_root, "vendor/javascript", "#{pkg}.js")
27
+ FileUtils.mkdir_p(File.dirname(target))
28
+ File.write(target, content)
29
+ say " vendor vendor/javascript/#{pkg}.js", :green
30
+ end
31
+
32
+ rewrite_importmap(result.files.keys)
33
+ say "\n Vendored #{result.files.size} package(s). config/importmap.rb now points at local copies.", :green
34
+ end
35
+
36
+ private
37
+
38
+ IMPORTMAP = "config/importmap.rb"
39
+ # pin "<name>", to: "<jsDelivr +esm url>"
40
+ CDN_PIN = /^\s*pin\s+"(?<name>[^"]+)",\s*to:\s*"(?<url>https:\/\/cdn\.jsdelivr\.net\/npm\/[^"]+\/\+esm)"/
41
+
42
+ def importmap_path
43
+ File.join(destination_root, IMPORTMAP)
44
+ end
45
+
46
+ def resolve_roots
47
+ return [] unless File.exist?(importmap_path)
48
+ content = File.read(importmap_path)
49
+ pins = content.each_line.filter_map do |line|
50
+ next unless (m = line.match(CDN_PIN))
51
+ ref = m[:url].match(Wabi::ZagVendor::REF)
52
+ next unless ref
53
+ { name: m[:name], pkg: ref[1], ver: ref[2] }
54
+ end
55
+ return pins.map { |p| { pkg: p[:pkg], ver: p[:ver] } } if packages.empty?
56
+ packages.map do |name|
57
+ found = pins.find { |p| p[:pkg] == name }
58
+ { pkg: name, ver: found ? found[:ver] : "1.41.0" }
59
+ end
60
+ end
61
+
62
+ # For every vendored package, ensure `pin "<pkg>", to: "<pkg>.js" # vendored by wabi`.
63
+ # Replace an existing pin for that pkg (CDN or vendored); insert if absent.
64
+ def rewrite_importmap(pkgs)
65
+ content = File.exist?(importmap_path) ? File.read(importmap_path) : ""
66
+ pkgs.each do |pkg|
67
+ new_line = %(pin "#{pkg}", to: "#{pkg}.js" # vendored by wabi)
68
+ existing = /^\s*pin\s+"#{Regexp.escape(pkg)}",.*$/
69
+ if content =~ existing
70
+ content = content.sub(existing, new_line)
71
+ else
72
+ content = content.rstrip + "\n" + new_line + "\n"
73
+ end
74
+ end
75
+ File.write(importmap_path, content)
76
+ end
77
+
78
+ # Net::HTTP.get_response does NOT follow redirects; jsDelivr may 3xx.
79
+ def http_get(url, limit = 5)
80
+ raise Wabi::Error, "vendor: too many redirects for #{url}" if limit <= 0
81
+ res = Net::HTTP.get_response(URI.parse(url))
82
+ case res
83
+ when Net::HTTPSuccess then res.body.dup.force_encoding("UTF-8")
84
+ when Net::HTTPRedirection then http_get(URI.join(url, res["location"]).to_s, limit - 1)
85
+ else raise Wabi::Error, "vendor: failed to fetch #{url}: HTTP #{res.code}"
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
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.15.0"
4
+ VERSION = "0.16.0"
5
5
  end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+ require "set"
3
+ require "cgi"
4
+ require "wabi" # for Wabi::Error (defined in wabi.rb, not version.rb)
5
+
6
+ module Wabi
7
+ # Walks the jsDelivr `+esm` import graph for one or more root packages and
8
+ # rewrites cross-package CDN references (`/npm/<pkg>@<ver>/+esm`) to bare
9
+ # specifiers (`<pkg>`), so every file can be served locally and resolved
10
+ # through the importmap (Propshaft-digest-safe). Pure: HTTP is injected via
11
+ # `fetcher`, a callable taking a URL string and returning the body string.
12
+ class ZagVendor
13
+ CDN = "https://cdn.jsdelivr.net"
14
+ # /npm/<pkg>@<ver>[/<subpath>]/+esm — <pkg> may be scoped (@scope/name).
15
+ # jsDelivr references both the package root (`@ver/+esm`) and subpath exports
16
+ # (`@ver/dom/+esm`, e.g. @floating-ui/utils/dom); group 3 captures the
17
+ # optional subpath so those resolve to a distinct local module too.
18
+ REF = %r{/npm/((?:@[^/]+/)?[^/@]+)@([^/]+?)((?:/[^/]+)*?)/\+esm}
19
+
20
+ Result = Struct.new(:files, :versions) # files: {pkg=>content}, versions: {pkg=>ver}
21
+
22
+ def self.call(roots, fetcher:)
23
+ new(fetcher).call(roots)
24
+ end
25
+
26
+ def initialize(fetcher)
27
+ @fetcher = fetcher
28
+ end
29
+
30
+ # roots: [{ pkg: "@zag-js/dialog", ver: "1.41.0" }, ...]
31
+ def call(roots)
32
+ files = {}
33
+ versions = {} # specifier => normalized version (first seen)
34
+ queue = roots.map { |r| { spec: r[:pkg], sub: "", ver: normalize_version(r[:ver]) } }
35
+
36
+ until queue.empty?
37
+ item = queue.shift
38
+ spec = item[:spec] # bare specifier incl. any subpath, e.g. "@floating-ui/utils/dom"
39
+ pkg = spec.sub(item[:sub], "") # package name without the subpath, for the fetch URL
40
+ ver = item[:ver]
41
+
42
+ # Dedupe by SPECIFIER (package + subpath), not by URL: jsDelivr's real
43
+ # graph references the same module both as a resolved pin (`@0.2.11`) and
44
+ # as a range (`@%5E0.2.11`, i.e. URL-encoded `^0.2.11`). Those normalize
45
+ # to the same version and the rewrite drops the version anyway (bare
46
+ # specifier). Only genuinely-different versions (after normalizing the
47
+ # range/encoding) are a real conflict a single bare importmap pin can't
48
+ # carry.
49
+ if versions.key?(spec)
50
+ if versions[spec] != ver
51
+ raise Wabi::Error,
52
+ "vendor: #{spec} appears at two versions (#{versions[spec]} and #{ver}); " \
53
+ "a bare importmap pin can't carry two versions"
54
+ end
55
+ next
56
+ end
57
+ versions[spec] = ver
58
+
59
+ content = @fetcher.call("#{CDN}/npm/#{pkg}@#{ver}#{item[:sub]}/+esm")
60
+ files[spec] = content.gsub(REF) do
61
+ dep_pkg = Regexp.last_match(1)
62
+ dep_ver = normalize_version(Regexp.last_match(2))
63
+ dep_sub = Regexp.last_match(3).to_s # "" or "/dom"
64
+ dep_spec = "#{dep_pkg}#{dep_sub}"
65
+ queue << { spec: dep_spec, sub: dep_sub, ver: dep_ver }
66
+ dep_spec
67
+ end
68
+ end
69
+
70
+ Result.new(files, versions)
71
+ end
72
+
73
+ private
74
+
75
+ # jsDelivr emits versions URL-encoded and sometimes as semver ranges
76
+ # (`%5E0.2.11` → `^0.2.11`). Decode and strip leading range operators so a
77
+ # range ref and a pin ref to the same resolved version compare equal. The
78
+ # stripped exact version is also what we fetch (`@0.2.11/+esm`).
79
+ def normalize_version(ver)
80
+ CGI.unescape(ver.to_s).sub(/\A[\^~><= ]+/, "").strip
81
+ end
82
+ end
83
+ end
data/lib/wabi.rb CHANGED
@@ -20,4 +20,5 @@ if defined?(Rails::Generators)
20
20
  require_relative "wabi/generators/list_generator"
21
21
  require_relative "wabi/generators/registry_generator"
22
22
  require_relative "wabi/generators/theme_generator"
23
+ require_relative "wabi/generators/vendor_generator"
23
24
  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.15.0
4
+ version: 0.16.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Oscar Ortega
@@ -141,11 +141,13 @@ files:
141
141
  - lib/wabi/generators/registry_generator.rb
142
142
  - lib/wabi/generators/theme_generator.rb
143
143
  - lib/wabi/generators/update_generator.rb
144
+ - lib/wabi/generators/vendor_generator.rb
144
145
  - lib/wabi/lockfile.rb
145
146
  - lib/wabi/registry_client.rb
146
147
  - lib/wabi/turbo_stream_extensions.rb
147
148
  - lib/wabi/variants.rb
148
149
  - lib/wabi/version.rb
150
+ - lib/wabi/zag_vendor.rb
149
151
  - templates/controllers/wabi/theme_controller.js
150
152
  - templates/tokens.css
151
153
  homepage: https://wabikit.dev