alpine: vendor the resize plugin beside the core
x-resize, which the birthday row's month select will read its width
through. alpine:vendor now refreshes every package it pins, the
official plugins being released in lockstep with the core.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019fHCYdysADrnpPkHaAHJpp
diff --git a/lib/pro_tacts/admin/layout.rb b/lib/pro_tacts/admin/layout.rb
index a620c58..d51abee 100644
--- a/lib/pro_tacts/admin/layout.rb
+++ b/lib/pro_tacts/admin/layout.rb
@@ -74,7 +74,10 @@ module ProTacts
# need anything else reachable to render. `defer` is
# Alpine's own requirement — it initializes on
# DOMContentLoaded and must not run before the markup it
- # reads exists.
+ # reads exists. A plugin goes before the core: it
+ # registers on the alpine:init the core fires on starting,
+ # and deferred scripts run in document order.
+ script(defer: true, src: "/vendor/alpine/resize.min.js")
script(defer: true, src: "/vendor/alpine/alpine.min.js")
end
body do
diff --git a/public/vendor/alpine/resize.min.js b/public/vendor/alpine/resize.min.js
new file mode 100644
index 0000000..6d2febd
--- /dev/null
+++ b/public/vendor/alpine/resize.min.js
@@ -0,0 +1,2 @@
+/* @alpinejs/resize 3.17.1, vendored from https://cdn.jsdelivr.net/npm/@alpinejs/resize@3.17.1/dist/cdn.min.js on 2026-10-20. Refresh with `rake alpine:vendor`; bump ALPINE_VERSION in tasks/alpine.rake to move the pin. */
+(()=>{function u(e){e.directive("resize",e.skipDuringClone((t,{value:i,expression:n,modifiers:o},{evaluateLater:r,cleanup:h})=>{let f=r(n),s=(z,m)=>{f(()=>{},{scope:{$width:z,$height:m}})},v=o.includes("document")?b(s):a(t,s);h(()=>v())}))}function a(e,t){let i=new ResizeObserver(n=>{let[o,r]=c(n);t(o,r)});return i.observe(e),()=>i.disconnect()}var d,l=new Set;function b(e){return l.add(e),d||(d=new ResizeObserver(t=>{let[i,n]=c(t);l.forEach(o=>o(i,n))}),d.observe(document.documentElement)),()=>{l.delete(e)}}function c(e){let t,i;for(let n of e)t=n.borderBoxSize[0].inlineSize,i=n.borderBoxSize[0].blockSize;return[t,i]}document.addEventListener("alpine:init",()=>{window.Alpine.plugin(u)});})();
diff --git a/tasks/alpine.rake b/tasks/alpine.rake
index 463458f..0d6ee37 100644
--- a/tasks/alpine.rake
+++ b/tasks/alpine.rake
@@ -3,31 +3,40 @@
# only through `tailscale serve`, and a page that needs a CDN reachable
# to work is a page that stops working for a reason nothing here
# controls. The version below is the pin — this task is how it moves.
+# The official plugins it vendors beside the core are released with it,
+# so one version pins them all.
ALPINE_VERSION = "3.17.1"
+# Each file under public/vendor/alpine, and the npm package it is.
+# Resize is x-resize, which the birthday row's month select reads its
+# width through (Admin::ContactsEdit#birthday_controls).
+ALPINE_PACKAGES = {"alpine" => "alpinejs", "resize" => "@alpinejs/resize"}.freeze
+
namespace :alpine do
- desc "Refresh the vendored Alpine from npm (ALPINE=version, default #{ALPINE_VERSION})"
+ desc "Refresh the vendored Alpine and its plugins from npm (ALPINE=version, default #{ALPINE_VERSION})"
task :vendor do
require "fileutils"
require "pathname"
version = ENV.fetch("ALPINE", ALPINE_VERSION)
- url = "https://cdn.jsdelivr.net/npm/alpinejs@#{version}/dist/cdn.min.js"
-
- body = `curl -fsS --max-time 60 #{url}`
- abort "could not fetch #{url}" unless $?.success?
-
out = Pathname.new(__dir__).parent / "public" / "vendor" / "alpine"
FileUtils.mkdir_p(out)
- # The banner is the only record of which build this is: the file
- # itself is minified and says nothing about its own version.
- header = "/* Alpine #{version}, vendored from #{url} on " \
- "#{Time.now.utc.strftime("%Y-%m-%d")}. Refresh with `rake alpine:vendor`; " \
- "bump ALPINE_VERSION in tasks/alpine.rake to move the pin. */\n"
+ ALPINE_PACKAGES.each do |file, package|
+ url = "https://cdn.jsdelivr.net/npm/#{package}@#{version}/dist/cdn.min.js"
+
+ body = `curl -fsS --max-time 60 #{url}`
+ abort "could not fetch #{url}" unless $?.success?
+
+ # The banner is the only record of which build this is: the file
+ # itself is minified and says nothing about its own version.
+ header = "/* #{package} #{version}, vendored from #{url} on " \
+ "#{Time.now.utc.strftime("%Y-%m-%d")}. Refresh with `rake alpine:vendor`; " \
+ "bump ALPINE_VERSION in tasks/alpine.rake to move the pin. */\n"
- (out / "alpine.min.js").write(header + body)
- puts "wrote #{out / "alpine.min.js"} (#{body.bytesize} bytes)"
+ (out / "#{file}.min.js").write(header + body)
+ puts "wrote #{out / "#{file}.min.js"} (#{body.bytesize} bytes)"
+ end
end
end
diff --git a/test/pro_tacts/admin/test_contacts_pages.rb b/test/pro_tacts/admin/test_contacts_pages.rb
index 494ab9c..953826d 100644
--- a/test/pro_tacts/admin/test_contacts_pages.rb
+++ b/test/pro_tacts/admin/test_contacts_pages.rb
@@ -332,7 +332,9 @@ class AdminContactsPagesTest < Minitest::Test
refute_includes head, "fonts.googleapis.com"
refute_includes head, "fonts.gstatic.com"
refute_includes head, "ibm-plex-mono"
- assert_includes head, '<script defer src="/vendor/alpine/alpine.min.js"></script>'
+ assert_includes head,
+ '<script defer src="/vendor/alpine/resize.min.js"></script>' \
+ '<script defer src="/vendor/alpine/alpine.min.js"></script>'
assert_empty head.scan(%r{(?:href|src)="(https?:)?//[^"]*"}),
"the layout links something off this host"
end