paper_view 0.1.1 → 0.2.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.
@@ -1,96 +1,198 @@
1
1
  <%= javascript_tag nonce: true do %>
2
- document.addEventListener("submit", function (event) {
3
- var button = event.target.querySelector("button[type=submit]");
4
- if (button && !button.querySelector(".pv-spinner")) {
5
- button.classList.add("pv-btn--busy");
6
- button.insertAdjacentHTML("beforeend", '<span class="pv-spinner"></span>');
2
+ (function () {
3
+ function clearBusyButtons() {
4
+ document.querySelectorAll(".pv-btn--busy").forEach(function (button) {
5
+ button.classList.remove("pv-btn--busy");
6
+ button.querySelectorAll(".pv-spinner").forEach(function (spinner) { spinner.remove(); });
7
+ });
7
8
  }
8
- });
9
-
10
- document.addEventListener("change", function (event) {
11
- var select = event.target.closest("select[data-pv-autosubmit]");
12
- if (!select || !select.form) return;
13
9
 
14
- if (select.form.requestSubmit) {
15
- select.form.requestSubmit();
16
- } else {
17
- select.form.submit();
10
+ // Turbo never removes stylesheets it merged into the head, so a per request CSP nonce would pile them up.
11
+ function dropStaleStyles() {
12
+ var styles = document.head.querySelectorAll("style[data-pv-styles]");
13
+ for (var index = 0; index < styles.length - 1; index++) styles[index].remove();
18
14
  }
19
- });
20
15
 
21
- window.addEventListener("pageshow", function () {
22
- document.querySelectorAll(".pv-btn--busy").forEach(function (button) {
23
- button.classList.remove("pv-btn--busy");
24
- button.querySelectorAll(".pv-spinner").forEach(function (spinner) { spinner.remove(); });
25
- });
26
- });
16
+ function initTimeline() {
17
+ var timeline = document.querySelector("[data-pv-timeline]");
18
+ var panels = document.querySelector("[data-pv-panels]");
19
+ if (!timeline || !panels) return;
27
20
 
28
- (function () {
29
- var timeline = document.querySelector("[data-pv-timeline]");
30
- var panels = document.querySelector("[data-pv-panels]");
31
- if (!timeline || !panels) return;
21
+ var footer = timeline.parentNode.querySelector(".pv-footer");
32
22
 
33
- function collapse(panel) {
34
- panel.querySelectorAll("[data-pv-toggle]").forEach(function (toggle) { setExpanded(toggle, false); });
35
- panel.querySelectorAll("[data-pv-copy]").forEach(function (button) { button.textContent = "Copy"; });
36
- }
23
+ function collapse(panel) {
24
+ panel.querySelectorAll("[data-pv-toggle]").forEach(function (toggle) { setExpanded(toggle, false); });
25
+ panel.querySelectorAll("[data-pv-copy]").forEach(function (button) { button.textContent = "Copy"; });
26
+ }
37
27
 
38
- function setExpanded(toggle, expanded) {
39
- var target = document.getElementById(toggle.getAttribute("data-pv-toggle"));
40
- toggle.setAttribute("aria-expanded", expanded);
41
- toggle.querySelector(".pv-disclosure__arrow").textContent = expanded ? "▾" : "▸";
42
- if (target) target.hidden = !expanded;
43
- }
28
+ function setExpanded(toggle, expanded) {
29
+ var target = document.getElementById(toggle.getAttribute("data-pv-toggle"));
30
+ toggle.setAttribute("aria-expanded", expanded);
31
+ toggle.querySelector(".pv-disclosure__arrow").textContent = expanded ? "▾" : "▸";
32
+ if (target) target.hidden = !expanded;
33
+ }
34
+
35
+ function select(id) {
36
+ timeline.querySelectorAll("[data-pv-version]").forEach(function (row) {
37
+ var active = row.getAttribute("data-pv-version") === id;
38
+ row.classList.toggle("pv-version--selected", active);
39
+ row.setAttribute("aria-pressed", active);
40
+ });
41
+
42
+ panels.querySelectorAll("[data-pv-panel]").forEach(function (panel) {
43
+ panel.hidden = panel.getAttribute("data-pv-panel") !== id;
44
+ if (panel.hidden) collapse(panel);
45
+ });
46
+
47
+ panels.scrollTop = 0;
44
48
 
45
- function select(id) {
46
- timeline.querySelectorAll("[data-pv-version]").forEach(function (row) {
47
- var active = row.getAttribute("data-pv-version") === id;
48
- row.classList.toggle("pv-version--selected", active);
49
- row.setAttribute("aria-pressed", active);
49
+ var url = new URL(window.location.href);
50
+ url.searchParams.set("version", id);
51
+ window.history.replaceState(window.history.state, "", url);
52
+ }
53
+
54
+ function rows() {
55
+ return Array.prototype.slice.call(timeline.querySelectorAll("[data-pv-version]"));
56
+ }
57
+
58
+ function focusRow(row) {
59
+ select(row.getAttribute("data-pv-version"));
60
+ row.focus();
61
+ }
62
+
63
+ function move(row, step) {
64
+ var all = rows();
65
+ var next = all[all.indexOf(row) + step];
66
+ if (next) return focusRow(next);
67
+
68
+ var control = step > 0 && footer && footer.querySelector("a[href], select, button");
69
+ if (control) control.focus();
70
+ }
71
+
72
+ timeline.addEventListener("click", function (event) {
73
+ if (event.target.closest("a")) return;
74
+ var row = event.target.closest("[data-pv-version]");
75
+ if (row) select(row.getAttribute("data-pv-version"));
50
76
  });
51
77
 
52
- panels.querySelectorAll("[data-pv-panel]").forEach(function (panel) {
53
- panel.hidden = panel.getAttribute("data-pv-panel") !== id;
54
- if (panel.hidden) collapse(panel);
78
+ timeline.addEventListener("keydown", function (event) {
79
+ var row = event.target.closest("[data-pv-version]");
80
+ if (!row) return;
81
+
82
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
83
+ event.preventDefault();
84
+ move(row, event.key === "ArrowDown" ? 1 : -1);
85
+ return;
86
+ }
87
+
88
+ if (event.key !== "Enter" && event.key !== " ") return;
89
+ event.preventDefault();
90
+ select(row.getAttribute("data-pv-version"));
55
91
  });
56
92
 
57
- panels.scrollTop = 0;
93
+ if (footer) {
94
+ footer.addEventListener("keydown", function (event) {
95
+ // Inside a select or input the arrow keys belong to the control itself.
96
+ if (event.key !== "ArrowUp" || event.target.closest("select, input")) return;
58
97
 
59
- var url = new URL(window.location.href);
60
- url.searchParams.set("version", id);
61
- window.history.replaceState({}, "", url);
98
+ var all = rows();
99
+ if (!all.length) return;
100
+ event.preventDefault();
101
+ focusRow(all[all.length - 1]);
102
+ });
103
+ }
104
+
105
+ panels.addEventListener("click", function (event) {
106
+ var toggle = event.target.closest("[data-pv-toggle]");
107
+ if (toggle) {
108
+ setExpanded(toggle, toggle.getAttribute("aria-expanded") !== "true");
109
+ return;
110
+ }
111
+
112
+ var copy = event.target.closest("[data-pv-copy]");
113
+ if (!copy || !navigator.clipboard) return;
114
+ navigator.clipboard.writeText(document.getElementById(copy.getAttribute("data-pv-copy")).textContent);
115
+ copy.textContent = "Copied";
116
+ });
117
+
118
+ if (new URL(window.location.href).searchParams.get("version")) {
119
+ var selected = timeline.querySelector(".pv-version--selected");
120
+ if (selected) selected.scrollIntoView({block: "center"});
121
+ }
62
122
  }
63
123
 
64
- timeline.addEventListener("click", function (event) {
65
- if (event.target.closest("a")) return;
66
- var row = event.target.closest("[data-pv-version]");
67
- if (row) select(row.getAttribute("data-pv-version"));
68
- });
124
+ function measureLayoutTop() {
125
+ var layout = document.querySelector(".pv-layout");
126
+ if (!layout) return;
127
+ var top = layout.getBoundingClientRect().top + window.scrollY;
128
+ document.documentElement.style.setProperty("--pv-layout-top", Math.round(top) + "px");
129
+ }
130
+
131
+ function initStats() {
132
+ var pending = document.querySelector("[data-pv-stats]");
133
+ if (!pending || !window.fetch) return;
134
+
135
+ var source = pending.getAttribute("data-pv-stats");
136
+ var started = Date.now();
137
+ pending.removeAttribute("data-pv-stats");
138
+
139
+ function show(html) {
140
+ var elapsed = Date.now() - started;
141
+
142
+ // Nothing is on screen before 350ms, and once the skeleton is there it stays long enough to read as a load.
143
+ setTimeout(function () {
144
+ pending.innerHTML = html;
145
+ pending.removeAttribute("aria-busy");
146
+ pending.classList.remove("pv-pending");
147
+ pending.classList.add("pv-loaded");
148
+ }, elapsed < 350 ? 0 : Math.max(0, 900 - elapsed));
149
+ }
69
150
 
70
- timeline.addEventListener("keydown", function (event) {
71
- if (event.key !== "Enter" && event.key !== " ") return;
72
- var row = event.target.closest("[data-pv-version]");
73
- if (!row) return;
74
- event.preventDefault();
75
- select(row.getAttribute("data-pv-version"));
151
+ fetch(source, {credentials: "same-origin", headers: {"X-Requested-With": "XMLHttpRequest"}})
152
+ .then(function (response) { return response.ok ? response.text() : Promise.reject(response); })
153
+ .then(show)
154
+ .catch(function () { window.location.href = source; });
155
+ }
156
+
157
+ function init() {
158
+ dropStaleStyles();
159
+ clearBusyButtons();
160
+ measureLayoutTop();
161
+ initTimeline();
162
+ initStats();
163
+ }
164
+
165
+ // Turbo re-runs this script after every visit, the listeners below must only be registered once.
166
+ if (window.paperViewStarted) return init();
167
+ window.paperViewStarted = true;
168
+
169
+ window.addEventListener("resize", measureLayoutTop);
170
+
171
+ document.addEventListener("submit", function (event) {
172
+ var button = event.target.querySelector("button[type=submit]");
173
+ if (button && !button.querySelector(".pv-spinner")) {
174
+ button.classList.add("pv-btn--busy");
175
+ button.insertAdjacentHTML("beforeend", '<span class="pv-spinner"></span>');
176
+ }
76
177
  });
77
178
 
78
- panels.addEventListener("click", function (event) {
79
- var toggle = event.target.closest("[data-pv-toggle]");
80
- if (toggle) {
81
- setExpanded(toggle, toggle.getAttribute("aria-expanded") !== "true");
82
- return;
179
+ document.addEventListener("change", function (event) {
180
+ var select = event.target.closest("select[data-pv-autosubmit]");
181
+ if (!select || !select.form) return;
182
+
183
+ if (select.form.requestSubmit) {
184
+ select.form.requestSubmit();
185
+ } else {
186
+ select.form.submit();
83
187
  }
188
+ });
84
189
 
85
- var copy = event.target.closest("[data-pv-copy]");
86
- if (!copy || !navigator.clipboard) return;
87
- navigator.clipboard.writeText(document.getElementById(copy.getAttribute("data-pv-copy")).textContent);
88
- copy.textContent = "Copied";
190
+ document.addEventListener("turbo:before-fetch-request", function (event) {
191
+ event.detail.fetchOptions.headers["X-PaperView-Turbo"] = "1";
89
192
  });
90
193
 
91
- if (new URL(window.location.href).searchParams.get("version")) {
92
- var selected = timeline.querySelector(".pv-version--selected");
93
- if (selected) selected.scrollIntoView({block: "center"});
94
- }
194
+ window.addEventListener("pageshow", clearBusyButtons);
195
+
196
+ init();
95
197
  })();
96
198
  <% end %>
@@ -1,4 +1,4 @@
1
- <style <%= paper_view_nonce_attribute %>>
1
+ <style data-pv-styles <%= paper_view_nonce_attribute %>>
2
2
  *, *::before, *::after { box-sizing: border-box; }
3
3
  [hidden] { display: none !important; }
4
4
 
@@ -22,6 +22,8 @@
22
22
  --pv-accent-hover: #1d4ed8;
23
23
  --pv-on-accent: #ffffff;
24
24
  --pv-dot: #cbd5e1;
25
+ --pv-skeleton: #e4e7ec;
26
+ --pv-skeleton-sheen: #f2f4f7;
25
27
  --pv-del: #b91c1c;
26
28
  --pv-del-bg: #fef2f2;
27
29
  --pv-del-rule: #fca5a5;
@@ -55,6 +57,8 @@
55
57
  --pv-accent: #4a8bff;
56
58
  --pv-accent-hover: #79a9ff;
57
59
  --pv-dot: #39414d;
60
+ --pv-skeleton: #232a33;
61
+ --pv-skeleton-sheen: #2f3945;
58
62
  --pv-del: #ff7b72;
59
63
  --pv-del-bg: #2d1618;
60
64
  --pv-del-rule: #7d2a2a;
@@ -86,15 +90,27 @@
86
90
  top: 0;
87
91
  z-index: 10;
88
92
  display: flex;
89
- align-items: center;
90
- gap: 16px;
91
- padding: 12px 24px;
93
+ align-items: stretch;
94
+ gap: 20px;
95
+ height: var(--pv-header-h);
96
+ padding: 0 24px;
92
97
  background: var(--pv-surface);
93
98
  border-bottom: 1px solid var(--pv-border);
94
99
  }
95
- .pv-header__brand { display: flex; align-items: baseline; gap: 8px; color: var(--pv-ink); font-size: 15px; font-weight: 650; }
100
+ .pv-header__brand { display: flex; align-items: center; gap: 8px; color: var(--pv-ink); font-size: 15px; font-weight: 650; }
96
101
  .pv-header__brand:hover { color: var(--pv-ink); text-decoration: none; }
97
- .pv-header__meta { margin-left: auto; color: var(--pv-ink-soft); font-size: 12.5px; }
102
+ .pv-header__meta { display: flex; align-items: center; margin-left: auto; color: var(--pv-ink-soft); font-size: 12.5px; }
103
+ .pv-header__nav { display: flex; align-items: stretch; gap: 20px; }
104
+ .pv-header__link {
105
+ display: flex;
106
+ align-items: center;
107
+ height: 100%;
108
+ color: var(--pv-ink-soft);
109
+ font-size: 13px;
110
+ font-weight: 400;
111
+ }
112
+ .pv-header__link:hover { color: var(--pv-ink); text-decoration: none; }
113
+ .pv-header__link--active { color: var(--pv-ink); font-weight: 600; box-shadow: inset 0 -2px 0 var(--pv-ink); }
98
114
 
99
115
  .pv-main { max-width: 1560px; margin: 0 auto; padding: 24px; }
100
116
  .pv-stack { display: flex; flex-direction: column; gap: 16px; }
@@ -172,6 +188,8 @@
172
188
  @keyframes pv-spin { to { transform: rotate(360deg); } }
173
189
  @media (prefers-reduced-motion: reduce) { .pv-spinner { animation-duration: 2.4s; } }
174
190
 
191
+ .turbo-progress-bar { height: 2px; background: var(--pv-accent); }
192
+
175
193
  .pv-panel--hero { width: 100%; max-width: 880px; margin: 32px auto 0; }
176
194
  .pv-hero { padding: 48px 40px 40px; text-align: center; }
177
195
  .pv-hero__title { font-size: 22px; }
@@ -220,6 +238,7 @@
220
238
  padding: 12px 18px;
221
239
  border-bottom: 1px solid var(--pv-border-subtle);
222
240
  cursor: pointer;
241
+ scroll-margin: calc(var(--pv-header-h) + 30px) 0 16px;
223
242
  }
224
243
  .pv-version--multi-item-view { grid-template-columns: 64px 22px 168px 1fr auto; }
225
244
  .pv-version:hover { background: var(--pv-row-hover); }
@@ -270,7 +289,7 @@
270
289
  top: calc(var(--pv-header-h) + 16px);
271
290
  width: clamp(330px, 26vw, 420px);
272
291
  flex: none;
273
- max-height: calc(100vh - var(--pv-header-h) - 32px);
292
+ max-height: calc(100dvh - var(--pv-layout-top, 190px) - 16px);
274
293
  overflow-y: auto;
275
294
  overscroll-behavior: contain;
276
295
  }
@@ -282,13 +301,17 @@
282
301
  .pv-detail__field { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
283
302
  .pv-detail__item { font: 400 13px/1.4 var(--pv-mono); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
284
303
  .pv-detail__who { font: 400 13px/1.4 var(--pv-sans); color: var(--pv-ink); overflow-wrap: anywhere; }
304
+ .pv-detail__metadata { display: flex; flex-direction: column; gap: 8px; padding: 14px 16px; border-bottom: 1px solid var(--pv-border); }
305
+ .pv-meta { display: grid; grid-template-columns: minmax(0, 96px) minmax(0, 1fr); gap: 4px 12px; }
306
+ .pv-meta__key { font: 400 12px/18px var(--pv-mono); color: var(--pv-ink-soft); overflow-wrap: anywhere; }
307
+ .pv-meta__value { font: 400 12px/18px var(--pv-mono); color: var(--pv-ink); overflow-wrap: anywhere; }
285
308
  .pv-detail__body { display: flex; flex-direction: column; gap: 14px; padding: 14px 16px; }
286
309
  .pv-detail__note { margin: 0; font: 400 12px/1.6 var(--pv-sans); color: var(--pv-ink-soft); }
287
310
  .pv-label { font: 600 11px/1.2 var(--pv-sans); letter-spacing: .06em; text-transform: uppercase; color: var(--pv-ink-faint); }
288
311
 
289
312
  .pv-attribute { display: flex; flex-direction: column; gap: 6px; }
290
313
  .pv-attribute__name { font: 600 12px/1.3 var(--pv-mono); color: var(--pv-ink); word-break: break-all; }
291
- .pv-attribute__unchanged {
314
+ .pv-attribute__unchanged, .pv-meta__more {
292
315
  align-self: flex-start;
293
316
  padding: 0;
294
317
  border: 0;
@@ -346,6 +369,65 @@
346
369
  .pv-snippet__copy:hover { background: var(--pv-pill-bg); }
347
370
  .pv-snippet__code { margin: 0; padding: 10px 12px; font: 400 11px/1.7 var(--pv-mono); color: var(--pv-ink); white-space: pre-wrap; word-break: break-all; }
348
371
 
372
+ .pv-tiles {
373
+ display: grid;
374
+ grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
375
+ gap: 1px;
376
+ background: var(--pv-border);
377
+ }
378
+ .pv-tile { display: flex; flex-direction: column; gap: 5px; padding: 16px 18px; background: var(--pv-surface); }
379
+ .pv-tile__value { font: 600 20px/1.2 var(--pv-sans); letter-spacing: -.01em; }
380
+ .pv-tile__note { font: 400 11.5px/1.3 var(--pv-sans); color: var(--pv-ink-faint); }
381
+
382
+ .pv-stat {
383
+ display: grid;
384
+ grid-template-columns: minmax(0, 1fr) 90px 130px 200px;
385
+ align-items: center;
386
+ gap: 0 14px;
387
+ padding: 11px 18px;
388
+ border-bottom: 1px solid var(--pv-border-subtle);
389
+ }
390
+ .pv-stat:last-child { border-bottom: 0; }
391
+ .pv-stat:not(.pv-stat--head):hover { background: var(--pv-row-hover); }
392
+ .pv-stat--head { padding-top: 9px; padding-bottom: 9px; background: var(--pv-surface-muted); font: 600 11px/1.2 var(--pv-sans); letter-spacing: .06em; text-transform: uppercase; color: var(--pv-ink-soft); }
393
+ .pv-stat--head:hover { background: var(--pv-surface-muted); }
394
+ .pv-stat__type { font: 600 13px/1.4 var(--pv-mono); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
395
+ .pv-stat__figure { font: 400 12.5px/1.4 var(--pv-mono); text-align: right; }
396
+ .pv-stat--head .pv-stat__figure { font: inherit; }
397
+ .pv-stat__share { display: flex; align-items: center; gap: 10px; }
398
+ .pv-stat__percent { flex: none; width: 46px; font: 400 12px/1.4 var(--pv-mono); color: var(--pv-ink-soft); text-align: right; }
399
+ .pv-share { flex: 1; min-width: 0; height: 8px; border-radius: 2px; background: var(--pv-border-subtle); }
400
+ .pv-share__fill { fill: var(--pv-accent); }
401
+
402
+ .pv-skeleton {
403
+ display: inline-block;
404
+ width: 100%;
405
+ height: 11px;
406
+ border-radius: 4px;
407
+ background: linear-gradient(90deg, var(--pv-skeleton) 30%, var(--pv-skeleton-sheen) 50%, var(--pv-skeleton) 70%) 0 0 / 220% 100%;
408
+ animation: pv-shimmer 1.6s ease-in-out infinite;
409
+ }
410
+ .pv-skeleton--value { width: 76px; height: 14px; }
411
+ .pv-skeleton--note { width: 150px; }
412
+ .pv-skeleton--type { width: 58%; }
413
+ .pv-skeleton--count { width: 44px; }
414
+ .pv-skeleton--bytes { width: 66px; }
415
+ .pv-skeleton--bar { flex: 1; height: 8px; border-radius: 2px; }
416
+ .pv-skeleton--percent { flex: none; width: 34px; }
417
+ .pv-stat--pending:nth-child(3n) .pv-skeleton--type { width: 42%; }
418
+ .pv-stat--pending:nth-child(3n + 1) .pv-skeleton--type { width: 71%; }
419
+ @keyframes pv-shimmer { from { background-position: 120% 0; } to { background-position: -20% 0; } }
420
+
421
+ /* Held back long enough that a small versions table never flashes a skeleton on its way in. */
422
+ .pv-pending { animation: pv-fade-in .3s ease-out .35s both; }
423
+ .pv-loaded { animation: pv-fade-in .2s ease-out both; }
424
+ @keyframes pv-fade-in { from { opacity: 0; } to { opacity: 1; } }
425
+
426
+ @media (prefers-reduced-motion: reduce) {
427
+ .pv-skeleton { animation: none; }
428
+ .pv-pending, .pv-loaded { animation-duration: .01ms; }
429
+ }
430
+
349
431
  .pv-empty { margin: 0; padding: 48px 16px; text-align: center; color: var(--pv-ink-soft); }
350
432
  .pv-footer {
351
433
  display: grid;
@@ -383,7 +465,12 @@
383
465
  .pv-main { padding: 16px; }
384
466
  .pv-footer { grid-template-columns: 1fr; justify-items: center; }
385
467
  .pv-footer__end, .pv-pagination { grid-column: 1; justify-content: center; }
386
- .pv-header { padding: 12px 16px; }
468
+ .pv-header { padding: 0 16px; }
469
+ .pv-stat { grid-template-columns: minmax(0, 1fr) 100px; gap: 4px 12px; }
470
+ .pv-stat--head { display: none; }
471
+ .pv-stat__type { grid-column: 1 / -1; }
472
+ .pv-stat__figure { text-align: left; }
473
+ .pv-stat__share { grid-column: 1 / -1; }
387
474
  }
388
475
 
389
476
  @media (max-width: 640px) {
@@ -0,0 +1,80 @@
1
+ <section class="pv-panel">
2
+ <div class="pv-tiles">
3
+ <div class="pv-tile">
4
+ <span class="pv-label">Versions</span>
5
+ <span class="pv-tile__value"><%= stats ? number_with_delimiter(stats.total_count) : tag.span(class: "pv-skeleton pv-skeleton--value") %></span>
6
+ </div>
7
+
8
+ <div class="pv-tile">
9
+ <span class="pv-label">Item types</span>
10
+ <span class="pv-tile__value"><%= stats ? number_with_delimiter(stats.rows.size) : tag.span(class: "pv-skeleton pv-skeleton--value") %></span>
11
+ </div>
12
+
13
+ <div class="pv-tile">
14
+ <span class="pv-label">Table size</span>
15
+ <span class="pv-tile__value"><%= stats ? paper_view_bytes(stats.disk_size&.table) : tag.span(class: "pv-skeleton pv-skeleton--value") %></span>
16
+ <span class="pv-tile__note">
17
+ <% if stats %>
18
+ <%= paper_view_bytes(stats.estimated_total_bytes) %> payload<%= " plus #{paper_view_bytes(stats.disk_size.indexes)} of indexes" if stats.disk_size %>
19
+ <% else %>
20
+ <%= tag.span(class: "pv-skeleton pv-skeleton--note") %>
21
+ <% end %>
22
+ </span>
23
+ </div>
24
+ </div>
25
+ </section>
26
+
27
+ <% if stats&.empty? %>
28
+ <section class="pv-panel">
29
+ <p class="pv-empty">No versions have been recorded yet.</p>
30
+ </section>
31
+ <% else %>
32
+ <section class="pv-card">
33
+ <div class="pv-stats">
34
+ <div class="pv-stat pv-stat--head">
35
+ <span>Item type</span>
36
+ <span class="pv-stat__figure">Versions</span>
37
+ <span class="pv-stat__figure">Size</span>
38
+ <span>Share</span>
39
+ </div>
40
+
41
+ <% if stats %>
42
+ <% stats.rows.each do |row| %>
43
+ <% share = row.share(stats.estimated_total_bytes) %>
44
+
45
+ <div class="pv-stat">
46
+ <span class="pv-stat__type">
47
+ <% if row.item_type %>
48
+ <%= link_to row.item_type, versions_path(item_type: row.item_type) %>
49
+ <% else %>
50
+ <span class="pv-muted"><%= PaperView::ApplicationHelper::EMPTY %></span>
51
+ <% end %>
52
+ </span>
53
+
54
+ <span class="pv-stat__figure"><%= number_with_delimiter(row.count) %></span>
55
+ <span class="pv-stat__figure"><%= paper_view_bytes(row.estimated_bytes) %></span>
56
+
57
+ <span class="pv-stat__share">
58
+ <svg class="pv-share" viewBox="0 0 100 8" preserveAspectRatio="none" aria-hidden="true">
59
+ <rect class="pv-share__fill" x="0" y="0" width="<%= (share * 100).round(2) %>" height="8" rx="1.5"></rect>
60
+ </svg>
61
+ <span class="pv-stat__percent"><%= paper_view_percentage(share) %></span>
62
+ </span>
63
+ </div>
64
+ <% end %>
65
+ <% else %>
66
+ <% 6.times do %>
67
+ <div class="pv-stat pv-stat--pending">
68
+ <span class="pv-stat__type"><%= tag.span(class: "pv-skeleton pv-skeleton--type") %></span>
69
+ <span class="pv-stat__figure"><%= tag.span(class: "pv-skeleton pv-skeleton--count") %></span>
70
+ <span class="pv-stat__figure"><%= tag.span(class: "pv-skeleton pv-skeleton--bytes") %></span>
71
+ <span class="pv-stat__share">
72
+ <%= tag.span(class: "pv-skeleton pv-skeleton--bar") %>
73
+ <%= tag.span(class: "pv-skeleton pv-skeleton--percent") %>
74
+ </span>
75
+ </div>
76
+ <% end %>
77
+ <% end %>
78
+ </div>
79
+ </section>
80
+ <% end %>
@@ -0,0 +1,21 @@
1
+ <% if @stats %>
2
+ <%= render "stats", stats: @stats %>
3
+ <% else %>
4
+ <div
5
+ class="pv-stack pv-pending"
6
+ aria-busy="true"
7
+ data-pv-stats="<%= stats_path(load_content: 1) %>"
8
+ >
9
+ <%= render "stats", stats: nil %>
10
+ </div>
11
+
12
+ <noscript>
13
+ <style <%= paper_view_nonce_attribute %>>.pv-pending { display: none; }</style>
14
+
15
+ <section class="pv-panel">
16
+ <p class="pv-empty">
17
+ <%= link_to "Load the statistics", stats_path(load_content: 1) %>
18
+ </p>
19
+ </section>
20
+ </noscript>
21
+ <% end %>
@@ -1,4 +1,4 @@
1
- <%= form_with url: versions_path, method: :get, local: true, data: {turbo: false}, class: "pv-search" do %>
1
+ <%= form_with url: versions_path, method: :get, local: true, class: "pv-search" do %>
2
2
  <%= paper_view_hidden_filters(except: %i[item_type item_id event]) %>
3
3
 
4
4
  <div class="pv-field pv-field--grow">
@@ -25,7 +25,7 @@
25
25
  <% end %>
26
26
  </nav>
27
27
 
28
- <%= form_with url: versions_path, method: :get, local: true, data: {turbo: false}, class: "pv-footer__end" do %>
28
+ <%= form_with url: versions_path, method: :get, local: true, class: "pv-footer__end" do %>
29
29
  <%= paper_view_hidden_filters(except: :per_page) %>
30
30
  <label class="pv-muted" for="pv-per-page">Per page</label>
31
31
 
@@ -22,6 +22,41 @@
22
22
  </div>
23
23
  </div>
24
24
 
25
+ <% if row.metadata.any? %>
26
+ <div class="pv-detail__metadata">
27
+ <span class="pv-label">Metadata</span>
28
+
29
+ <div class="pv-meta">
30
+ <% row.metadata.visible.each do |entry| %>
31
+ <span class="pv-meta__key"><%= entry.name %></span>
32
+ <span class="pv-meta__value"><%= entry.text %></span>
33
+ <% end %>
34
+ </div>
35
+
36
+ <% if row.metadata.hidden.any? %>
37
+ <% rest_id = "pv-meta-#{row.id}" %>
38
+ <button
39
+ type="button"
40
+ class="pv-meta__more"
41
+ aria-expanded="false"
42
+ aria-controls="<%= rest_id %>"
43
+ data-pv-toggle="<%= rest_id %>"
44
+ >
45
+ <span class="pv-disclosure__arrow">▸</span>
46
+ <%= row.metadata.hidden.size %> more
47
+ <%= "key".pluralize(row.metadata.hidden.size) %>
48
+ </button>
49
+
50
+ <div class="pv-meta" id="<%= rest_id %>" hidden>
51
+ <% row.metadata.hidden.each do |entry| %>
52
+ <span class="pv-meta__key"><%= entry.name %></span>
53
+ <span class="pv-meta__value"><%= entry.text %></span>
54
+ <% end %>
55
+ </div>
56
+ <% end %>
57
+ </div>
58
+ <% end %>
59
+
25
60
  <div class="pv-detail__body">
26
61
  <% if row.change_set.unreadable? %>
27
62
  <p class="pv-detail__note">
@@ -27,16 +27,16 @@
27
27
  <div class="pv-layout">
28
28
  <section class="pv-card">
29
29
  <div class="pv-card__head">
30
- <span class="pv-card__title"><%= @timeline.title %></span>
30
+ <span class="pv-card__title"><%= @query.title %></span>
31
31
 
32
32
  <span class="pv-card__meta">
33
33
  <%= number_with_delimiter(@paginator.total_count) %>
34
- <%= "version".pluralize(@paginator.total_count) %><%= " · #{@timeline.items.size} items" if @timeline.multi_item_view? %>
34
+ <%= "version".pluralize(@paginator.total_count) %><%= " · #{@query.matched_item_types.size} item types" if @query.matched_item_types.size > 1 %>
35
35
  </span>
36
36
 
37
37
  <span class="pv-spacer"></span>
38
38
 
39
- <%= form_with url: versions_path, method: :get, local: true, data: {turbo: false}, class: "pv-head-form" do %>
39
+ <%= form_with url: versions_path, method: :get, local: true, class: "pv-head-form" do %>
40
40
  <%= paper_view_hidden_filters(except: :sort) %>
41
41
 
42
42
  <select
@@ -58,7 +58,7 @@
58
58
  <div class="pv-timeline__day"><%= group.label %></div>
59
59
 
60
60
  <% group.rows.each do |row| %>
61
- <%= render "paper_view/versions/timeline_row", row: row, multi_item_view: @timeline.multi_item_view?, selected: row == @timeline.selected %>
61
+ <%= render "paper_view/versions/timeline_row", row: row, multi_item_view: !@query.single_item?, selected: row == @timeline.selected %>
62
62
  <% end %>
63
63
  </div>
64
64
  <% end %>
@@ -69,7 +69,7 @@
69
69
 
70
70
  <aside class="pv-side" data-pv-panels>
71
71
  <% @timeline.rows.each do |row| %>
72
- <%= render "paper_view/versions/panel", row: row, multi_item_view: @timeline.multi_item_view?, active: row == @timeline.selected %>
72
+ <%= render "paper_view/versions/panel", row: row, multi_item_view: !@query.single_item?, active: row == @timeline.selected %>
73
73
  <% end %>
74
74
  </aside>
75
75
  </div>
data/config/routes.rb CHANGED
@@ -2,4 +2,7 @@ PaperView::Engine.routes.draw do
2
2
  root "versions#index"
3
3
 
4
4
  resources :versions, only: :index
5
+ resource :stats, only: :show
6
+
7
+ get "turbo-:digest.js", to: PaperView::TurboAsset, as: :turbo_asset
5
8
  end