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 +4 -4
- data/CHANGELOG.md +22 -0
- data/README.md +2 -1
- data/lib/wabi/generators/add_generator.rb +2 -0
- data/lib/wabi/generators/vendor_generator.rb +90 -0
- data/lib/wabi/version.rb +1 -1
- data/lib/wabi/zag_vendor.rb +83 -0
- data/lib/wabi.rb +1 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d4495a8631cd5ad14cac92e522f0bdd4f1409ec525d9ca7772e806c6a2556947
|
|
4
|
+
data.tar.gz: 5434b6d05819812507dd79b35b4e8d5de17b18e0f3972be980cab3ee3073c6a3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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
|
@@ -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
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
|
+
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
|