gemstar 1.0.2 → 1.1

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.
@@ -52,9 +52,9 @@
52
52
  align-items: center;
53
53
  justify-content: space-between;
54
54
  gap: 0.6rem;
55
- padding: 0.4rem 0.65rem;
56
- border-bottom: 1px solid #ece8df;
57
- background: #fff;
55
+ padding: 0.55rem 0.75rem;
56
+ border-bottom: 1px solid #e6d5c2;
57
+ background: #fbf1e4;
58
58
  position: sticky;
59
59
  top: 0;
60
60
  z-index: 2;
@@ -210,6 +210,18 @@
210
210
  margin: 0.3rem 0 0;
211
211
  color: var(--muted);
212
212
  }
213
+ .detail-subtitle > :first-child {
214
+ margin-top: 0;
215
+ }
216
+ .detail-subtitle > :last-child {
217
+ margin-bottom: 0;
218
+ }
219
+ .detail-subtitle a[href] {
220
+ color: #2563c9;
221
+ }
222
+ .detail-subtitle a[href]:hover {
223
+ text-decoration: underline;
224
+ }
213
225
  .detail-origin {
214
226
  margin: 0;
215
227
  color: var(--ink);
@@ -236,6 +248,9 @@
236
248
  gap: 0.28rem;
237
249
  flex-wrap: wrap;
238
250
  }
251
+ .list-filters-secondary {
252
+ padding-top: 0.05rem;
253
+ }
239
254
  .list-filter-button {
240
255
  border: 1px solid var(--line);
241
256
  border-radius: 999px;
@@ -296,6 +311,12 @@
296
311
  justify-content: space-between;
297
312
  gap: 0.5rem;
298
313
  }
314
+ .gem-name-lockup {
315
+ display: inline-flex;
316
+ align-items: center;
317
+ gap: 0.38rem;
318
+ min-width: 0;
319
+ }
299
320
  .gem-updated-dot {
300
321
  width: 0.45rem;
301
322
  height: 0.45rem;
@@ -311,12 +332,26 @@
311
332
  color: var(--muted);
312
333
  font-size: 0.76rem;
313
334
  }
335
+ .package-type-tag {
336
+ display: inline-flex;
337
+ align-items: center;
338
+ border: 1px solid #ded6c8;
339
+ border-radius: 999px;
340
+ padding: 0.02rem 0.33rem;
341
+ background: #fbf7ef;
342
+ color: #786d61;
343
+ font-size: 0.62rem;
344
+ font-weight: 600;
345
+ letter-spacing: 0.01em;
346
+ line-height: 1.25;
347
+ flex: 0 0 auto;
348
+ }
314
349
  .gem-row.status-added .gem-version {
315
350
  color: var(--green);
316
351
  font-weight: 600;
317
352
  }
318
353
  .detail {
319
- padding: 0.5rem 0.8rem 0.5rem 0.5rem;
354
+ padding: 0.62rem 0.8rem 0.5rem 0.5rem;
320
355
  display: grid;
321
356
  gap: 0.45rem;
322
357
  align-content: start;
@@ -367,7 +402,7 @@
367
402
  flex: 0 0 auto;
368
403
  }
369
404
  .detail-disclosure {
370
- margin-top: 0.2rem;
405
+ margin-top: 0.38rem;
371
406
  border: 1px solid #ece8df;
372
407
  border-radius: 0.3rem;
373
408
  background: #fff;
@@ -4,6 +4,7 @@
4
4
  const toSelect = document.querySelector("[data-to-select]");
5
5
  const sidebarPanel = document.querySelector("[data-sidebar-panel]");
6
6
  const filterButtons = Array.from(document.querySelectorAll("[data-filter-button]"));
7
+ const ecosystemButtons = Array.from(document.querySelectorAll("[data-ecosystem-button]"));
7
8
  const gemSearch = document.querySelector("[data-gem-search]");
8
9
  const emptyGemList = document.querySelector("[data-gem-list-empty]");
9
10
  let detailPanel = document.querySelector("[data-detail-panel]");
@@ -13,8 +14,11 @@
13
14
  let detailRequestToken = 0;
14
15
  let activeDetailUrl = detailPanel ? detailPanel.dataset.detailUrl : null;
15
16
  let currentFilter = <%= selected_filter_json %>;
17
+ let currentPackageScope = <%= selected_package_scope_json %>;
16
18
  let currentSearch = "";
17
19
  const emptyDetailHtml = <%= empty_detail_html_json %>;
20
+ const packageCollectionLabel = <%= (@selected_project&.package_collection_label || "Packages").downcase.dump %>;
21
+ const detailDisclosureStorageKey = "gemstar.detailDisclosureOpen";
18
22
 
19
23
  const visibleGemLinks = () => gemLinks.filter((link) => !link.hidden);
20
24
  const currentSelectedIndex = () => visibleGemLinks().findIndex((link) => link.classList.contains("is-selected"));
@@ -23,7 +27,18 @@
23
27
  link.classList.remove("is-selected");
24
28
  });
25
29
  };
26
- const requestedGemName = () => new URL(window.location.href).searchParams.get("gem");
30
+ const packageNameParam = (url) => url.searchParams.get("package") || url.searchParams.get("gem");
31
+ const requestedGemName = () => packageNameParam(new URL(window.location.href));
32
+ const currentDetailGemName = () => {
33
+ if (!detailPanel || !detailPanel.dataset.detailUrl) return null;
34
+
35
+ try {
36
+ return packageNameParam(new URL(detailPanel.dataset.detailUrl, window.location.origin));
37
+ } catch (_error) {
38
+ return null;
39
+ }
40
+ };
41
+ const detailNeedsInitialFetch = () => detailPanel && detailPanel.dataset.detailDeferred === "true" && !!detailPanel.dataset.detailUrl;
27
42
  const isSidebarFocused = () => document.activeElement === sidebarPanel;
28
43
  const isDetailFocused = () => detailPanel && document.activeElement === detailPanel;
29
44
  const focusSidebar = () => {
@@ -32,6 +47,31 @@
32
47
  const focusDetail = () => {
33
48
  if (detailPanel) detailPanel.focus({ preventScroll: true });
34
49
  };
50
+ const storedDetailDisclosureOpen = () => {
51
+ try {
52
+ return window.localStorage.getItem(detailDisclosureStorageKey) === "true";
53
+ } catch (_error) {
54
+ return false;
55
+ }
56
+ };
57
+ const persistDetailDisclosureOpen = (open) => {
58
+ try {
59
+ window.localStorage.setItem(detailDisclosureStorageKey, open ? "true" : "false");
60
+ } catch (_error) {
61
+ // ignore storage failures
62
+ }
63
+ };
64
+ const bindDetailDisclosure = () => {
65
+ if (!detailPanel) return;
66
+
67
+ const disclosure = detailPanel.querySelector(".detail-disclosure");
68
+ if (!disclosure) return;
69
+
70
+ disclosure.open = storedDetailDisclosureOpen();
71
+ disclosure.addEventListener("toggle", () => {
72
+ persistDetailDisclosureOpen(disclosure.open);
73
+ });
74
+ };
35
75
 
36
76
  const applyGemFilter = (filter) => {
37
77
  currentFilter = filter;
@@ -42,15 +82,20 @@
42
82
  button.classList.toggle("is-active", button.dataset.filterButton === filter);
43
83
  });
44
84
 
85
+ ecosystemButtons.forEach((button) => {
86
+ button.classList.toggle("is-active", button.dataset.ecosystemButton === currentPackageScope);
87
+ });
88
+
45
89
  gemLinks.forEach((link) => {
46
90
  const updated = link.dataset.gemUpdated === "true";
47
91
  const pinned = pinnedGemName && link.dataset.gemName === pinnedGemName;
92
+ const matchesScope = currentPackageScope === "all" || link.dataset.packageScope === currentPackageScope;
48
93
  const matchesSearch = searchTerm === "" || link.dataset.gemName.toLowerCase().includes(searchTerm);
49
- link.hidden = ((filter === "updated" && !updated && !pinned) || !matchesSearch);
94
+ link.hidden = (!matchesScope || (filter === "updated" && !updated && !pinned) || !matchesSearch);
50
95
  });
51
96
 
52
97
  if (emptyGemList) {
53
- const emptyMessage = searchTerm === "" ? "No updated gems in this revision range." : "No gems match this filter.";
98
+ const emptyMessage = searchTerm === "" ? `No updated ${packageCollectionLabel} in this revision range.` : `No ${packageCollectionLabel} match this filter.`;
54
99
  emptyGemList.hidden = visibleGemLinks().length !== 0;
55
100
  const text = emptyGemList.querySelector("p");
56
101
  if (text) text.textContent = emptyMessage;
@@ -58,12 +103,14 @@
58
103
 
59
104
  if (currentSelectedIndex() === -1) {
60
105
  clearSidebarSelection();
61
- replaceDetail(emptyDetailHtml);
106
+ if (visibleGemLinks().length === 0) {
107
+ replaceDetail(emptyDetailHtml);
108
+ }
62
109
  }
63
110
  };
64
111
 
65
112
  const syncSidebarSelection = (gemName = null, keepVisible = false) => {
66
- const effectiveGemName = gemName || new URL(window.location.href).searchParams.get("gem");
113
+ const effectiveGemName = gemName || requestedGemName();
67
114
  if (!effectiveGemName) {
68
115
  clearSidebarSelection();
69
116
  return;
@@ -94,6 +141,7 @@
94
141
  detailPanel = document.querySelector("[data-detail-panel]");
95
142
  if (detailPanel) detailPanel.scrollTop = 0;
96
143
  activeDetailUrl = detailPanel ? detailPanel.dataset.detailUrl : null;
144
+ bindDetailDisclosure();
97
145
  if (shouldRestoreDetailFocus) {
98
146
  focusDetail();
99
147
  }
@@ -122,7 +170,7 @@
122
170
  </section>
123
171
  `;
124
172
 
125
- const fetchDetail = (url, pushHistory = true) => {
173
+ const fetchDetail = (url, historyMode = "push") => {
126
174
  const normalizedUrl = new URL(url, window.location.origin).toString();
127
175
  const requestToken = ++detailRequestToken;
128
176
  activeDetailUrl = normalizedUrl;
@@ -139,32 +187,64 @@
139
187
  if (requestToken !== detailRequestToken || normalizedUrl !== activeDetailUrl) return;
140
188
 
141
189
  replaceDetail(html);
142
- if (pushHistory) {
190
+ if (historyMode !== "none") {
143
191
  const pageUrl = new URL(window.location.href);
144
192
  const detailUrl = new URL(url, window.location.origin);
145
193
  pageUrl.search = detailUrl.search;
146
194
  pageUrl.searchParams.set("filter", currentFilter);
147
- window.history.pushState({}, "", pageUrl);
195
+ pageUrl.searchParams.set("scope", currentPackageScope);
196
+ const historyMethod = historyMode === "replace" ? "replaceState" : "pushState";
197
+ window.history[historyMethod]({}, "", pageUrl);
148
198
  }
149
199
  const detailUrl = new URL(url, window.location.origin);
150
- syncSidebarSelection(detailUrl.searchParams.get("gem"));
200
+ syncSidebarSelection(packageNameParam(detailUrl));
151
201
  })
152
202
  .catch(() => {
153
203
  stopDetailLoading();
154
204
  });
155
205
  };
156
206
 
157
- const activateGemLink = (link, pushHistory = true, keepVisible = false) => {
207
+ const activateGemLink = (link, historyMode = "push", keepVisible = false) => {
158
208
  if (!link) return;
159
209
 
160
210
  stopDetailPoll();
161
211
  stopDetailLoading();
162
212
  syncSidebarSelection(link.dataset.gemName, keepVisible);
163
- fetchDetail(link.dataset.detailUrl || link.href, pushHistory);
213
+ fetchDetail(link.dataset.detailUrl || link.href, historyMode);
164
214
 
165
215
  focusSidebar();
166
216
  };
167
217
 
218
+ const ensureInitialGemSelection = () => {
219
+ const requested = requestedGemName();
220
+
221
+ const detailGemName = currentDetailGemName();
222
+ const detailLink = detailGemName ? visibleGemLinks().find((link) => link.dataset.gemName === detailGemName) : null;
223
+
224
+ if (detailLink) {
225
+ syncSidebarSelection(detailGemName, true);
226
+ if (!requested) {
227
+ const pageUrl = new URL(window.location.href);
228
+ const detailUrl = new URL(detailLink.dataset.detailUrl || detailLink.href, window.location.origin);
229
+ pageUrl.search = detailUrl.search;
230
+ pageUrl.searchParams.set("filter", currentFilter);
231
+ pageUrl.searchParams.set("scope", currentPackageScope);
232
+ window.history.replaceState({}, "", pageUrl);
233
+ }
234
+ if (detailNeedsInitialFetch()) {
235
+ fetchDetail(detailLink.dataset.detailUrl || detailLink.href, requested ? "none" : "replace");
236
+ }
237
+ return;
238
+ }
239
+
240
+ if (requested) return;
241
+
242
+ const firstVisibleLink = visibleGemLinks()[0];
243
+ if (firstVisibleLink) {
244
+ activateGemLink(firstVisibleLink, "replace", true);
245
+ }
246
+ };
247
+
168
248
  const scheduleDetailPoll = () => {
169
249
  stopDetailPoll();
170
250
  if (!detailPanel || detailPanel.dataset.detailPending !== "true") return;
@@ -176,11 +256,13 @@
176
256
 
177
257
  if (detailPanel) {
178
258
  detailPanel.scrollTop = 0;
259
+ bindDetailDisclosure();
179
260
  scheduleDetailPoll();
180
261
  }
181
262
 
182
263
  syncSidebarSelection(null, true);
183
264
  applyGemFilter(currentFilter);
265
+ ensureInitialGemSelection();
184
266
 
185
267
  gemLinks.forEach((link) => {
186
268
  link.addEventListener("click", (event) => {
@@ -202,6 +284,20 @@
202
284
  });
203
285
  });
204
286
 
287
+ ecosystemButtons.forEach((button) => {
288
+ button.addEventListener("click", () => {
289
+ currentPackageScope = button.dataset.ecosystemButton;
290
+ applyGemFilter(currentFilter);
291
+ if (sidebarPanel) {
292
+ sidebarPanel.scrollTop = 0;
293
+ }
294
+ const url = new URL(window.location.href);
295
+ url.searchParams.set("scope", currentPackageScope);
296
+ window.history.replaceState({}, "", url);
297
+ syncSidebarSelection(null, true);
298
+ });
299
+ });
300
+
205
301
  if (gemSearch) {
206
302
  gemSearch.addEventListener("input", (event) => {
207
303
  currentSearch = event.target.value || "";
@@ -232,19 +328,19 @@
232
328
  event.target.value = "<%= selected_project_index %>";
233
329
  return;
234
330
  }
235
- navigate({ project: event.target.value, from: null, to: "worktree", filter: currentFilter, gem: null });
331
+ navigate({ project: event.target.value, from: null, to: "worktree", filter: currentFilter, scope: currentPackageScope, package: null, gem: null });
236
332
  });
237
333
  }
238
334
 
239
335
  if (fromSelect) {
240
336
  fromSelect.addEventListener("change", (event) => {
241
- navigate({ from: event.target.value, filter: currentFilter, gem: null });
337
+ navigate({ from: event.target.value, filter: currentFilter, scope: currentPackageScope, package: null, gem: null });
242
338
  });
243
339
  }
244
340
 
245
341
  if (toSelect) {
246
342
  toSelect.addEventListener("change", (event) => {
247
- navigate({ to: event.target.value, filter: currentFilter, gem: null });
343
+ navigate({ to: event.target.value, filter: currentFilter, scope: currentPackageScope, package: null, gem: null });
248
344
  });
249
345
  }
250
346
 
@@ -277,6 +373,14 @@
277
373
  }
278
374
 
279
375
  if (!isSidebarFocused()) {
376
+ if (event.key === "ArrowLeft") {
377
+ event.preventDefault();
378
+ focusSidebar();
379
+ }
380
+ if (event.key === "ArrowRight") {
381
+ event.preventDefault();
382
+ focusDetail();
383
+ }
280
384
  if (event.key === "ArrowDown" || event.key === "ArrowUp") {
281
385
  event.preventDefault();
282
386
  focusSidebar();
@@ -296,7 +400,7 @@
296
400
 
297
401
  if (nextIndex !== null && nextIndex !== currentIndex) {
298
402
  event.preventDefault();
299
- activateGemLink(links[nextIndex], true, true);
403
+ activateGemLink(links[nextIndex], "push", true);
300
404
  }
301
405
  });
302
406
 
@@ -4,7 +4,8 @@
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
6
  <title><%= title %></title>
7
- <link rel="icon" href="<%= favicon_data_uri %>">
7
+ <link rel="icon" href="/favicon.svg" type="image/svg+xml">
8
+ <link rel="shortcut icon" href="/favicon.ico">
8
9
  <style>
9
10
  <%= styles_css %>
10
11
  </style>
data/lib/gemstar.rb CHANGED
@@ -15,7 +15,10 @@ require "gemstar/outputs/markdown"
15
15
  require "gemstar/cache"
16
16
  require "gemstar/change_log"
17
17
  require "gemstar/git_hub"
18
+ require "gemstar/importmap_file"
18
19
  require "gemstar/lock_file"
20
+ require "gemstar/npm_metadata"
21
+ require "gemstar/package_lock_file"
19
22
  require "gemstar/project"
20
23
  require "gemstar/remote_repository"
21
24
  require "gemstar/utils"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gemstar
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.2
4
+ version: '1.1'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Florian Dejako
@@ -243,12 +243,17 @@ files:
243
243
  - lib/gemstar/commands/diff.rb
244
244
  - lib/gemstar/commands/server.rb
245
245
  - lib/gemstar/config.rb
246
+ - lib/gemstar/data/importmap_package_metadata.json
247
+ - lib/gemstar/data/ruby_gems_metadata.json
246
248
  - lib/gemstar/git_hub.rb
247
249
  - lib/gemstar/git_repo.rb
250
+ - lib/gemstar/importmap_file.rb
248
251
  - lib/gemstar/lock_file.rb
252
+ - lib/gemstar/npm_metadata.rb
249
253
  - lib/gemstar/outputs/basic.rb
250
254
  - lib/gemstar/outputs/html.rb
251
255
  - lib/gemstar/outputs/markdown.rb
256
+ - lib/gemstar/package_lock_file.rb
252
257
  - lib/gemstar/project.rb
253
258
  - lib/gemstar/remote_repository.rb
254
259
  - lib/gemstar/request_logger.rb
@@ -281,7 +286,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
281
286
  - !ruby/object:Gem::Version
282
287
  version: '0'
283
288
  requirements: []
284
- rubygems_version: 4.0.6
289
+ rubygems_version: 3.6.9
285
290
  specification_version: 4
286
291
  summary: Making sense of gems.
287
292
  test_files: []