stimulus-pdf-viewer-rails 0.1.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.
@@ -0,0 +1,4 @@
1
+ # Importmap pins for stimulus-pdf-viewer
2
+ # These are automatically added when the gem is loaded
3
+
4
+ pin "stimulus-pdf-viewer", to: "stimulus-pdf-viewer.esm.js"
@@ -0,0 +1,220 @@
1
+ <div class="pdf-toolbar">
2
+ <%# Left section: Sidebar toggle + Page navigation + Zoom %>
3
+ <div class="pdf-toolbar-section pdf-toolbar-left">
4
+ <button class="pdf-toolbar-btn" data-action="click->pdf-viewer#toggleSidebar" title="Toggle thumbnails">
5
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
6
+ <rect x="3" y="3" width="7" height="18" rx="1" />
7
+ <line x1="14" y1="6" x2="21" y2="6" />
8
+ <line x1="14" y1="12" x2="21" y2="12" />
9
+ <line x1="14" y1="18" x2="21" y2="18" />
10
+ </svg>
11
+ </button>
12
+
13
+ <div class="pdf-toolbar-separator"></div>
14
+
15
+ <div class="pdf-toolbar-group pdf-toolbar-nav">
16
+ <button class="pdf-toolbar-btn" data-action="click->pdf-viewer#previousPage" title="Previous page" data-pdf-viewer-target="prevBtn">
17
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
18
+ <path d="M8 12L3 7l5-5v10z" />
19
+ </svg>
20
+ </button>
21
+ <div class="pdf-page-input-container">
22
+ <input type="number" class="pdf-page-input" data-action="change->pdf-viewer#goToPage keydown->pdf-viewer#handlePageInputKey" data-pdf-viewer-target="pageInput" min="1" value="1" autocomplete="off">
23
+ <span class="pdf-page-separator">/</span>
24
+ <span class="pdf-page-count" data-pdf-viewer-target="pageCount">-</span>
25
+ </div>
26
+ <button class="pdf-toolbar-btn" data-action="click->pdf-viewer#nextPage" title="Next page" data-pdf-viewer-target="nextBtn">
27
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
28
+ <path d="M8 4l5 5-5 5V4z" />
29
+ </svg>
30
+ </button>
31
+ </div>
32
+
33
+ <div class="pdf-toolbar-separator"></div>
34
+
35
+ <div class="pdf-toolbar-group pdf-toolbar-zoom">
36
+ <button class="pdf-toolbar-btn" data-action="click->pdf-viewer#zoomOut" title="Zoom out">
37
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
38
+ <circle cx="11" cy="11" r="8" />
39
+ <line x1="21" y1="21" x2="16.65" y2="16.65" />
40
+ <line x1="8" y1="11" x2="14" y2="11" />
41
+ </svg>
42
+ </button>
43
+ <select class="pdf-zoom-select" data-action="change->pdf-viewer#setZoom" data-pdf-viewer-target="zoomSelect">
44
+ <option value="auto" selected>Auto</option>
45
+ <option value="page-width">Page Width</option>
46
+ <option value="page-fit">Page Fit</option>
47
+ <option value="page-actual">Actual Size</option>
48
+ <option disabled>-----</option>
49
+ <option value="0.5">50%</option>
50
+ <option value="0.75">75%</option>
51
+ <option value="1">100%</option>
52
+ <option value="1.25">125%</option>
53
+ <option value="1.5">150%</option>
54
+ <option value="2">200%</option>
55
+ <option value="3">300%</option>
56
+ </select>
57
+ <button class="pdf-toolbar-btn" data-action="click->pdf-viewer#zoomIn" title="Zoom in">
58
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
59
+ <circle cx="11" cy="11" r="8" />
60
+ <line x1="21" y1="21" x2="16.65" y2="16.65" />
61
+ <line x1="11" y1="8" x2="11" y2="14" />
62
+ <line x1="8" y1="11" x2="14" y2="11" />
63
+ </svg>
64
+ </button>
65
+ </div>
66
+ </div>
67
+
68
+ <%# Right section: Annotation tools %>
69
+ <div class="pdf-toolbar-section pdf-toolbar-right">
70
+ <div class="pdf-toolbar-group pdf-toolbar-tools">
71
+ <button class="pdf-tool-btn active" data-tool="select" data-action="click->pdf-viewer#selectTool" aria-label="Select tool">
72
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
73
+ <path d="M3 3l7.07 16.97 2.51-7.39 7.39-2.51L3 3z" />
74
+ <path d="M13 13l6 6" />
75
+ </svg>
76
+ <span>Select</span>
77
+ </button>
78
+ <button class="pdf-tool-btn" data-tool="highlight" data-action="click->pdf-viewer#selectTool" aria-label="Highlight tool">
79
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
80
+ <path d="M12 20h9" />
81
+ <path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z" />
82
+ <rect x="2" y="18" width="8" height="4" rx="1" fill="#FFEB3B" stroke="none" />
83
+ </svg>
84
+ <span>Highlight</span>
85
+ </button>
86
+ <button class="pdf-tool-btn" data-tool="underline" data-action="click->pdf-viewer#selectTool" aria-label="Underline tool">
87
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
88
+ <path d="M6 3v7a6 6 0 0 0 6 6 6 6 0 0 0 6-6V3" />
89
+ <line x1="4" y1="21" x2="20" y2="21" stroke-width="3" />
90
+ </svg>
91
+ <span>Underline</span>
92
+ </button>
93
+ <button class="pdf-tool-btn" data-tool="note" data-action="click->pdf-viewer#selectTool" aria-label="Add note">
94
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
95
+ <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" fill="#FFF9C4" />
96
+ <line x1="8" y1="9" x2="16" y2="9" />
97
+ <line x1="8" y1="13" x2="14" y2="13" />
98
+ </svg>
99
+ <span>Add Note</span>
100
+ </button>
101
+ <button class="pdf-tool-btn" data-tool="ink" data-action="click->pdf-viewer#selectTool" aria-label="Draw tool">
102
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
103
+ <path d="M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z" />
104
+ </svg>
105
+ <span>Draw</span>
106
+ </button>
107
+ </div>
108
+
109
+ <div class="pdf-toolbar-separator"></div>
110
+
111
+ <div class="pdf-toolbar-group pdf-toolbar-colors" data-pdf-viewer-target="colorPicker">
112
+ <%# Color picker will be rendered here by JavaScript %>
113
+ </div>
114
+
115
+ <div class="pdf-toolbar-separator"></div>
116
+
117
+ <div class="pdf-toolbar-group pdf-toolbar-actions">
118
+ <button class="pdf-toolbar-btn" data-action="click->pdf-viewer#toggleSearch" title="Search (Ctrl+F)">
119
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
120
+ <circle cx="11" cy="11" r="8" />
121
+ <line x1="21" y1="21" x2="16.65" y2="16.65" />
122
+ </svg>
123
+ </button>
124
+ <div class="pdf-toolbar-separator"></div>
125
+
126
+ <%# Overflow menu button (visible on mobile only) %>
127
+ <button class="pdf-toolbar-overflow-btn" data-action="click->pdf-viewer#toggleOverflowMenu" title="More tools" data-pdf-viewer-target="overflowBtn">
128
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
129
+ <circle cx="12" cy="12" r="1" fill="currentColor" />
130
+ <circle cx="12" cy="5" r="1" fill="currentColor" />
131
+ <circle cx="12" cy="19" r="1" fill="currentColor" />
132
+ </svg>
133
+ </button>
134
+
135
+ <button class="pdf-toolbar-btn" data-action="click->pdf-viewer#toggleAnnotationSidebar" title="Annotations">
136
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
137
+ <line x1="17" y1="6" x2="21" y2="6" />
138
+ <line x1="17" y1="12" x2="21" y2="12" />
139
+ <line x1="17" y1="18" x2="21" y2="18" />
140
+ <rect x="3" y="3" width="7" height="18" rx="1" />
141
+ </svg>
142
+ </button>
143
+ </div>
144
+
145
+ <%# Overflow menu dropdown (mobile - nav, zoom, actions) %>
146
+ <div class="pdf-toolbar-overflow-menu" data-pdf-viewer-target="overflowMenu">
147
+ <%# Page Navigation %>
148
+ <div class="pdf-overflow-section">
149
+ <div class="pdf-overflow-row">
150
+ <button class="pdf-overflow-btn" data-action="click->pdf-viewer#previousPage" title="Previous page">
151
+ <svg width="20" height="20" viewBox="0 0 16 16" fill="currentColor">
152
+ <path d="M8 12L3 7l5-5v10z" />
153
+ </svg>
154
+ </button>
155
+ <div class="pdf-overflow-page-display">
156
+ <span data-pdf-viewer-target="overflowPageNum">1</span>
157
+ <span class="pdf-page-separator">/</span>
158
+ <span data-pdf-viewer-target="overflowPageCount">-</span>
159
+ </div>
160
+ <button class="pdf-overflow-btn" data-action="click->pdf-viewer#nextPage" title="Next page">
161
+ <svg width="20" height="20" viewBox="0 0 16 16" fill="currentColor">
162
+ <path d="M8 4l5 5-5 5V4z" />
163
+ </svg>
164
+ </button>
165
+ </div>
166
+ </div>
167
+
168
+ <%# More Tools (underline, ink) %>
169
+ <div class="pdf-overflow-section">
170
+ <div class="pdf-overflow-section-title">More Tools</div>
171
+ <div class="pdf-overflow-tools">
172
+ <button class="pdf-overflow-tool-btn" data-tool="underline" data-action="click->pdf-viewer#selectToolFromOverflow" aria-label="Underline tool">
173
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
174
+ <path d="M6 3v7a6 6 0 0 0 6 6 6 6 0 0 0 6-6V3" />
175
+ <line x1="4" y1="21" x2="20" y2="21" stroke-width="3" />
176
+ </svg>
177
+ <span>Underline</span>
178
+ </button>
179
+ <button class="pdf-overflow-tool-btn" data-tool="ink" data-action="click->pdf-viewer#selectToolFromOverflow" aria-label="Draw tool">
180
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
181
+ <path d="M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z" />
182
+ </svg>
183
+ <span>Draw</span>
184
+ </button>
185
+ </div>
186
+ </div>
187
+
188
+ <%# Actions %>
189
+ <div class="pdf-overflow-section">
190
+ <div class="pdf-overflow-actions">
191
+ <button class="pdf-overflow-action-btn" data-action="click->pdf-viewer#toggleSearch">
192
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
193
+ <circle cx="11" cy="11" r="8" />
194
+ <line x1="21" y1="21" x2="16.65" y2="16.65" />
195
+ </svg>
196
+ <span>Search</span>
197
+ </button>
198
+ <button class="pdf-overflow-action-btn" data-action="click->pdf-viewer#toggleSidebar">
199
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
200
+ <rect x="3" y="3" width="7" height="18" rx="1" />
201
+ <line x1="14" y1="6" x2="21" y2="6" />
202
+ <line x1="14" y1="12" x2="21" y2="12" />
203
+ <line x1="14" y1="18" x2="21" y2="18" />
204
+ </svg>
205
+ <span>Thumbnails</span>
206
+ </button>
207
+ <button class="pdf-overflow-action-btn" data-action="click->pdf-viewer#toggleAnnotationSidebar">
208
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
209
+ <line x1="17" y1="6" x2="21" y2="6" />
210
+ <line x1="17" y1="12" x2="21" y2="12" />
211
+ <line x1="17" y1="18" x2="21" y2="18" />
212
+ <rect x="3" y="3" width="7" height="18" rx="1" />
213
+ </svg>
214
+ <span>Annotations</span>
215
+ </button>
216
+ </div>
217
+ </div>
218
+ </div>
219
+ </div>
220
+ </div>
@@ -0,0 +1,54 @@
1
+ <%#
2
+ PDF Viewer partial example for Rails
3
+
4
+ Required local variables:
5
+ - document: An object with `file` (ActiveStorage attachment) and `name` attributes
6
+ - annotations_url: Path to the annotations REST endpoint
7
+
8
+ Optional local variables:
9
+ - user_name: Current user's name (for watermarks)
10
+ - organization_name: Organization name (for watermarks)
11
+ - initial_page: Page number to open on load
12
+ - initial_annotation: Annotation ID to highlight on load
13
+ - tracking_url: Endpoint for time tracking (optional)
14
+ %>
15
+
16
+ <div class="pdf-viewer-container"
17
+ data-controller="pdf-viewer"
18
+ data-pdf-viewer-target="container"
19
+ data-pdf-viewer-document-url-value="<%= url_for(document.file) %>"
20
+ data-pdf-viewer-document-name-value="<%= document.name %>"
21
+ data-pdf-viewer-annotations-url-value="<%= annotations_url %>"
22
+ <% if local_assigns[:user_name] %>
23
+ data-pdf-viewer-user-name-value="<%= user_name %>"
24
+ <% end %>
25
+ <% if local_assigns[:organization_name] %>
26
+ data-pdf-viewer-organization-name-value="<%= organization_name %>"
27
+ <% end %>
28
+ <% if local_assigns[:initial_page] %>
29
+ data-pdf-viewer-initial-page-value="<%= initial_page %>"
30
+ <% end %>
31
+ <% if local_assigns[:initial_annotation] %>
32
+ data-pdf-viewer-initial-annotation-value="<%= initial_annotation %>"
33
+ <% end %>
34
+ <% if local_assigns[:tracking_url] %>
35
+ data-pdf-viewer-tracking-url-value="<%= tracking_url %>"
36
+ <% end %>>
37
+
38
+ <%# Toolbar - render the toolbar partial %>
39
+ <div class="pdf-viewer-toolbar">
40
+ <%= render "path/to/toolbar" %>
41
+ </div>
42
+
43
+ <%# Main viewer body %>
44
+ <div class="pdf-viewer-body">
45
+ <%# Loading overlay - shown while PDF loads %>
46
+ <div class="pdf-loading-overlay" data-pdf-viewer-target="loadingOverlay">
47
+ <div class="pdf-loading-spinner"></div>
48
+ <div class="pdf-loading-text">Loading document...</div>
49
+ </div>
50
+
51
+ <%# PDF pages container - pages are rendered here by JavaScript %>
52
+ <div class="pdf-pages-container"></div>
53
+ </div>
54
+ </div>
@@ -0,0 +1,95 @@
1
+ require "rails/generators"
2
+
3
+ module StimulusPdfViewer
4
+ module Generators
5
+ class InstallGenerator < ::Rails::Generators::Base
6
+ source_root File.expand_path("install/templates", __dir__)
7
+
8
+ desc "Install stimulus-pdf-viewer into your Rails application"
9
+
10
+ def register_stimulus_controller
11
+ say "Registering Stimulus controller..."
12
+
13
+ controller_registration = <<~JS
14
+
15
+ // PDF Viewer
16
+ import { PdfViewerController, PdfDownloadController } from "stimulus-pdf-viewer"
17
+ application.register("pdf-viewer", PdfViewerController)
18
+ application.register("pdf-download", PdfDownloadController)
19
+ JS
20
+
21
+ controllers_file = "app/javascript/controllers/index.js"
22
+
23
+ if File.exist?(controllers_file)
24
+ append_to_file controllers_file, controller_registration
25
+ say "Added controller registration to #{controllers_file}", :green
26
+ else
27
+ say "Could not find #{controllers_file}. Please manually register the controllers:", :yellow
28
+ say controller_registration
29
+ end
30
+ end
31
+
32
+ def add_stylesheet_import
33
+ say "Adding stylesheet import..."
34
+
35
+ stylesheet_import = '@import "stimulus-pdf-viewer";'
36
+
37
+ # Try common stylesheet locations
38
+ stylesheet_files = [
39
+ "app/assets/stylesheets/application.scss",
40
+ "app/assets/stylesheets/application.css.scss",
41
+ "app/assets/stylesheets/application.sass.scss"
42
+ ]
43
+
44
+ stylesheet_file = stylesheet_files.find { |f| File.exist?(f) }
45
+
46
+ if stylesheet_file
47
+ append_to_file stylesheet_file, "\n#{stylesheet_import}\n"
48
+ say "Added stylesheet import to #{stylesheet_file}", :green
49
+ else
50
+ say "Could not find a SCSS stylesheet. Please manually add:", :yellow
51
+ say stylesheet_import
52
+ end
53
+ end
54
+
55
+ def add_pdf_worker_meta_tag
56
+ say "Adding PDF.js worker meta tag..."
57
+
58
+ meta_tag = '<meta name="pdf-worker-src" content="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/4.0.379/pdf.worker.min.js">'
59
+
60
+ layout_file = "app/views/layouts/application.html.erb"
61
+
62
+ if File.exist?(layout_file)
63
+ inject_into_file layout_file, " #{meta_tag}\n", after: "<head>\n"
64
+ say "Added PDF worker meta tag to #{layout_file}", :green
65
+ else
66
+ say "Could not find #{layout_file}. Please manually add to your <head>:", :yellow
67
+ say meta_tag
68
+ end
69
+ end
70
+
71
+ def copy_example_partials
72
+ if yes?("Would you like to copy example view partials? (y/n)")
73
+ directory "views", "app/views/pdf_viewer"
74
+ say "Copied example partials to app/views/pdf_viewer/", :green
75
+ end
76
+ end
77
+
78
+ def show_next_steps
79
+ say ""
80
+ say "=" * 60, :green
81
+ say "stimulus-pdf-viewer installed successfully!", :green
82
+ say "=" * 60, :green
83
+ say ""
84
+ say "Next steps:"
85
+ say "1. Create an Annotation model and controller for your app"
86
+ say "2. Set up routes for annotations REST API"
87
+ say "3. Add the PDF viewer partial to your views"
88
+ say ""
89
+ say "See the README for detailed integration instructions:"
90
+ say "https://github.com/jhubert/stimulus-pdf-viewer-rails"
91
+ say ""
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,2 @@
1
+ # Convenience require for the gem
2
+ require "stimulus_pdf_viewer/rails"
@@ -0,0 +1,26 @@
1
+ module StimulusPdfViewer
2
+ module Rails
3
+ class Engine < ::Rails::Engine
4
+ isolate_namespace StimulusPdfViewer
5
+
6
+ initializer "stimulus-pdf-viewer.assets" do |app|
7
+ # Add our assets to the asset paths
8
+ app.config.assets.paths << root.join("app/assets/javascripts")
9
+ app.config.assets.paths << root.join("app/assets/stylesheets")
10
+ app.config.assets.paths << root.join("app/assets/images")
11
+
12
+ # Precompile the main JS file
13
+ app.config.assets.precompile += %w[
14
+ stimulus-pdf-viewer.esm.js
15
+ stimulus-pdf-viewer.js
16
+ ]
17
+ end
18
+
19
+ initializer "stimulus-pdf-viewer.importmap", before: "importmap" do |app|
20
+ if app.config.respond_to?(:importmap)
21
+ app.config.importmap.paths << root.join("config/importmap.rb")
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,7 @@
1
+ module StimulusPdfViewer
2
+ module Rails
3
+ VERSION = "0.1.0"
4
+ # This should match the npm package version
5
+ STIMULUS_PDF_VIEWER_VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ require "stimulus_pdf_viewer/rails/version"
2
+ require "stimulus_pdf_viewer/rails/engine"
3
+
4
+ module StimulusPdfViewer
5
+ module Rails
6
+ end
7
+ end
@@ -0,0 +1,196 @@
1
+ require "fileutils"
2
+ require "json"
3
+ require "open-uri"
4
+ require "tmpdir"
5
+
6
+ namespace :stimulus_pdf_viewer do
7
+ desc "Update vendored assets from stimulus-pdf-viewer npm package"
8
+ task :update, [:version] do |_t, args|
9
+ version = args[:version] || "latest"
10
+ updater = StimulusPdfViewerAssetUpdater.new(version)
11
+ updater.run
12
+ end
13
+
14
+ desc "Check for newer versions of stimulus-pdf-viewer"
15
+ task :check do
16
+ checker = StimulusPdfViewerVersionChecker.new
17
+ checker.run
18
+ end
19
+ end
20
+
21
+ class StimulusPdfViewerAssetUpdater
22
+ NPM_REGISTRY_URL = "https://registry.npmjs.org/stimulus-pdf-viewer"
23
+ GEM_ROOT = File.expand_path("../..", __dir__)
24
+
25
+ ASSET_MAPPINGS = {
26
+ "dist/stimulus-pdf-viewer.esm.js" => "app/assets/javascripts/stimulus-pdf-viewer.esm.js",
27
+ "dist/stimulus-pdf-viewer.js" => "app/assets/javascripts/stimulus-pdf-viewer.js",
28
+ "styles/pdf-viewer.scss" => "app/assets/stylesheets/stimulus-pdf-viewer.scss"
29
+ }.freeze
30
+
31
+ CURSOR_SOURCE_DIR = "assets/cursors"
32
+ CURSOR_DEST_DIR = "app/assets/images/stimulus-pdf-viewer"
33
+
34
+ def initialize(version)
35
+ @requested_version = version
36
+ end
37
+
38
+ def run
39
+ puts "Fetching package info from npm registry..."
40
+ package_info = fetch_package_info
41
+
42
+ @version = resolve_version(package_info)
43
+ puts "Target version: #{@version}"
44
+
45
+ current_version = read_current_version
46
+ if current_version == @version && @requested_version == "latest"
47
+ puts "Already at version #{@version}. Use rake stimulus_pdf_viewer:update[#{@version}] to force reinstall."
48
+ return
49
+ end
50
+
51
+ if current_version == @version
52
+ puts "Reinstalling version #{@version}..."
53
+ else
54
+ puts "Updating from #{current_version} to #{@version}..."
55
+ end
56
+
57
+ Dir.mktmpdir do |tmpdir|
58
+ tarball_path = download_package(package_info, tmpdir)
59
+ extract_package(tarball_path, tmpdir)
60
+
61
+ package_dir = File.join(tmpdir, "package")
62
+ copy_assets(package_dir)
63
+ update_version_file
64
+ end
65
+
66
+ puts "Successfully updated to version #{@version}"
67
+ end
68
+
69
+ private
70
+
71
+ def fetch_package_info
72
+ JSON.parse(URI.open(NPM_REGISTRY_URL).read)
73
+ rescue OpenURI::HTTPError => e
74
+ abort "Failed to fetch package info: #{e.message}"
75
+ end
76
+
77
+ def resolve_version(package_info)
78
+ if @requested_version == "latest"
79
+ package_info["dist-tags"]["latest"]
80
+ else
81
+ unless package_info["versions"].key?(@requested_version)
82
+ available = package_info["versions"].keys.last(10).join(", ")
83
+ abort "Version #{@requested_version} not found. Recent versions: #{available}"
84
+ end
85
+ @requested_version
86
+ end
87
+ end
88
+
89
+ def read_current_version
90
+ version_file = File.join(GEM_ROOT, "lib/stimulus_pdf_viewer/rails/version.rb")
91
+ content = File.read(version_file)
92
+ match = content.match(/STIMULUS_PDF_VIEWER_VERSION\s*=\s*["']([^"']+)["']/)
93
+ match ? match[1] : "unknown"
94
+ end
95
+
96
+ def download_package(package_info, tmpdir)
97
+ tarball_url = package_info["versions"][@version]["dist"]["tarball"]
98
+ tarball_path = File.join(tmpdir, "package.tgz")
99
+
100
+ puts "Downloading #{tarball_url}..."
101
+ File.open(tarball_path, "wb") do |file|
102
+ URI.open(tarball_url) { |remote| file.write(remote.read) }
103
+ end
104
+
105
+ tarball_path
106
+ end
107
+
108
+ def extract_package(tarball_path, tmpdir)
109
+ puts "Extracting package..."
110
+ system("tar", "-xzf", tarball_path, "-C", tmpdir, exception: true)
111
+ end
112
+
113
+ def copy_assets(package_dir)
114
+ puts "Copying assets..."
115
+
116
+ ASSET_MAPPINGS.each do |src, dest|
117
+ src_path = File.join(package_dir, src)
118
+ dest_path = File.join(GEM_ROOT, dest)
119
+
120
+ unless File.exist?(src_path)
121
+ warn " Warning: #{src} not found in package"
122
+ next
123
+ end
124
+
125
+ FileUtils.cp(src_path, dest_path)
126
+ puts " #{src} -> #{dest}"
127
+ end
128
+
129
+ copy_cursors(package_dir)
130
+ end
131
+
132
+ def copy_cursors(package_dir)
133
+ cursor_src = File.join(package_dir, CURSOR_SOURCE_DIR)
134
+ cursor_dest = File.join(GEM_ROOT, CURSOR_DEST_DIR)
135
+
136
+ unless Dir.exist?(cursor_src)
137
+ warn " Warning: #{CURSOR_SOURCE_DIR} not found in package"
138
+ return
139
+ end
140
+
141
+ FileUtils.mkdir_p(cursor_dest)
142
+
143
+ Dir.glob(File.join(cursor_src, "*.svg")).each do |svg|
144
+ FileUtils.cp(svg, cursor_dest)
145
+ puts " #{CURSOR_SOURCE_DIR}/#{File.basename(svg)} -> #{CURSOR_DEST_DIR}/"
146
+ end
147
+ end
148
+
149
+ def update_version_file
150
+ version_file = File.join(GEM_ROOT, "lib/stimulus_pdf_viewer/rails/version.rb")
151
+ content = File.read(version_file)
152
+
153
+ updated = content.gsub(
154
+ /STIMULUS_PDF_VIEWER_VERSION\s*=\s*["'][^"']+["']/,
155
+ "STIMULUS_PDF_VIEWER_VERSION = \"#{@version}\""
156
+ )
157
+
158
+ File.write(version_file, updated)
159
+ puts "Updated STIMULUS_PDF_VIEWER_VERSION to #{@version}"
160
+ end
161
+ end
162
+
163
+ class StimulusPdfViewerVersionChecker
164
+ NPM_REGISTRY_URL = "https://registry.npmjs.org/stimulus-pdf-viewer"
165
+ GEM_ROOT = File.expand_path("../..", __dir__)
166
+
167
+ def run
168
+ current = read_current_version
169
+ latest = fetch_latest_version
170
+
171
+ puts "Current vendored version: #{current}"
172
+ puts "Latest npm version: #{latest}"
173
+
174
+ if current == latest
175
+ puts "You are up to date!"
176
+ else
177
+ puts "Update available! Run: rake stimulus_pdf_viewer:update"
178
+ end
179
+ end
180
+
181
+ private
182
+
183
+ def read_current_version
184
+ version_file = File.join(GEM_ROOT, "lib/stimulus_pdf_viewer/rails/version.rb")
185
+ content = File.read(version_file)
186
+ match = content.match(/STIMULUS_PDF_VIEWER_VERSION\s*=\s*["']([^"']+)["']/)
187
+ match ? match[1] : "unknown"
188
+ end
189
+
190
+ def fetch_latest_version
191
+ response = JSON.parse(URI.open(NPM_REGISTRY_URL).read)
192
+ response["dist-tags"]["latest"]
193
+ rescue OpenURI::HTTPError => e
194
+ abort "Failed to fetch package info: #{e.message}"
195
+ end
196
+ end