paper_view 0.1.0 → 0.2.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/README.md +34 -2
- data/app/controllers/paper_view/stats_controller.rb +12 -0
- data/app/helpers/paper_view/application_helper.rb +26 -0
- data/app/models/paper_view/metadata.rb +73 -0
- data/app/models/paper_view/payload_sizes.rb +107 -0
- data/app/models/paper_view/table_stats.rb +62 -0
- data/app/models/paper_view/timeline.rb +2 -2
- data/app/models/paper_view/version_row.rb +27 -5
- data/app/models/paper_view/versions_table.rb +60 -4
- data/app/views/layouts/paper_view/application.html.erb +12 -4
- data/app/views/paper_view/shared/_script.html.erb +174 -70
- data/app/views/paper_view/shared/_styles.html.erb +113 -19
- data/app/views/paper_view/stats/_stats.html.erb +84 -0
- data/app/views/paper_view/stats/show.html.erb +21 -0
- data/app/views/paper_view/versions/_filters.html.erb +1 -1
- data/app/views/paper_view/versions/_footer.html.erb +1 -1
- data/app/views/paper_view/versions/_panel.html.erb +91 -44
- data/app/views/paper_view/versions/_timeline_row.html.erb +6 -6
- data/app/views/paper_view/versions/index.html.erb +4 -4
- data/config/routes.rb +3 -0
- data/lib/paper_view/assets/turbo.js +7256 -0
- data/lib/paper_view/configuration.rb +4 -1
- data/lib/paper_view/turbo_asset.rb +21 -0
- data/lib/paper_view/version.rb +1 -1
- data/lib/paper_view.rb +1 -0
- metadata +9 -1
|
@@ -1,94 +1,198 @@
|
|
|
1
1
|
<%= javascript_tag nonce: true do %>
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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;
|
|
48
|
+
|
|
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"));
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
timeline.addEventListener("keydown", function (event) {
|
|
79
|
+
var row = event.target.closest("[data-pv-version]");
|
|
80
|
+
if (!row) return;
|
|
44
81
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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"));
|
|
50
91
|
});
|
|
51
92
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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;
|
|
97
|
+
|
|
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";
|
|
55
116
|
});
|
|
56
117
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
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");
|
|
60
129
|
}
|
|
61
130
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
+
}
|
|
150
|
+
|
|
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;
|
|
67
168
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
+
}
|
|
74
177
|
});
|
|
75
178
|
|
|
76
|
-
|
|
77
|
-
var
|
|
78
|
-
if (
|
|
79
|
-
|
|
80
|
-
|
|
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();
|
|
81
187
|
}
|
|
188
|
+
});
|
|
82
189
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
navigator.clipboard.writeText(document.getElementById(copy.getAttribute("data-pv-copy")).textContent);
|
|
86
|
-
copy.textContent = "Copied";
|
|
190
|
+
document.addEventListener("turbo:before-fetch-request", function (event) {
|
|
191
|
+
event.detail.fetchOptions.headers["X-PaperView-Turbo"] = "1";
|
|
87
192
|
});
|
|
88
193
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
194
|
+
window.addEventListener("pageshow", clearBusyButtons);
|
|
195
|
+
|
|
196
|
+
init();
|
|
93
197
|
})();
|
|
94
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,16 +90,27 @@
|
|
|
86
90
|
top: 0;
|
|
87
91
|
z-index: 10;
|
|
88
92
|
display: flex;
|
|
89
|
-
align-items:
|
|
90
|
-
gap:
|
|
91
|
-
|
|
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:
|
|
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-
|
|
98
|
-
.pv-
|
|
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); }
|
|
99
114
|
|
|
100
115
|
.pv-main { max-width: 1560px; margin: 0 auto; padding: 24px; }
|
|
101
116
|
.pv-stack { display: flex; flex-direction: column; gap: 16px; }
|
|
@@ -173,6 +188,8 @@
|
|
|
173
188
|
@keyframes pv-spin { to { transform: rotate(360deg); } }
|
|
174
189
|
@media (prefers-reduced-motion: reduce) { .pv-spinner { animation-duration: 2.4s; } }
|
|
175
190
|
|
|
191
|
+
.turbo-progress-bar { height: 2px; background: var(--pv-accent); }
|
|
192
|
+
|
|
176
193
|
.pv-panel--hero { width: 100%; max-width: 880px; margin: 32px auto 0; }
|
|
177
194
|
.pv-hero { padding: 48px 40px 40px; text-align: center; }
|
|
178
195
|
.pv-hero__title { font-size: 22px; }
|
|
@@ -221,8 +238,9 @@
|
|
|
221
238
|
padding: 12px 18px;
|
|
222
239
|
border-bottom: 1px solid var(--pv-border-subtle);
|
|
223
240
|
cursor: pointer;
|
|
241
|
+
scroll-margin: calc(var(--pv-header-h) + 30px) 0 16px;
|
|
224
242
|
}
|
|
225
|
-
.pv-version--
|
|
243
|
+
.pv-version--multi-item-view { grid-template-columns: 64px 22px 168px 1fr auto; }
|
|
226
244
|
.pv-version:hover { background: var(--pv-row-hover); }
|
|
227
245
|
.pv-version--selected, .pv-version--selected:hover { background: var(--pv-row-selected); }
|
|
228
246
|
.pv-version:focus-visible { outline: 2px solid var(--pv-accent); outline-offset: -2px; }
|
|
@@ -242,7 +260,7 @@
|
|
|
242
260
|
.pv-version__content { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
|
|
243
261
|
.pv-version__meta { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
|
244
262
|
.pv-version__actor { font: 600 13px/20px var(--pv-sans); color: var(--pv-ink); }
|
|
245
|
-
.pv-version--
|
|
263
|
+
.pv-version--multi-item-view .pv-version__actor { font-weight: 400; color: var(--pv-ink-secondary); }
|
|
246
264
|
.pv-version__note { font: 400 12px/18px var(--pv-sans); color: var(--pv-ink-soft); }
|
|
247
265
|
|
|
248
266
|
.pv-pill { font: 500 11px/16px var(--pv-sans); border-radius: 4px; padding: 1px 6px; white-space: nowrap; }
|
|
@@ -262,26 +280,38 @@
|
|
|
262
280
|
text-overflow: ellipsis;
|
|
263
281
|
white-space: nowrap;
|
|
264
282
|
}
|
|
265
|
-
.pv-version--
|
|
283
|
+
.pv-version--multi-item-view .pv-value { max-width: 240px; }
|
|
266
284
|
.pv-value--old { color: var(--pv-del); background: var(--pv-del-bg); }
|
|
267
285
|
.pv-value--new { color: var(--pv-add); background: var(--pv-add-bg); }
|
|
268
286
|
|
|
269
|
-
.pv-side {
|
|
270
|
-
|
|
287
|
+
.pv-side {
|
|
288
|
+
position: sticky;
|
|
289
|
+
top: calc(var(--pv-header-h) + 16px);
|
|
290
|
+
width: clamp(330px, 26vw, 420px);
|
|
291
|
+
flex: none;
|
|
292
|
+
max-height: calc(100dvh - var(--pv-layout-top, 190px) - 16px);
|
|
293
|
+
overflow-y: auto;
|
|
294
|
+
overscroll-behavior: contain;
|
|
295
|
+
}
|
|
296
|
+
.pv-detail__head { display: flex; align-items: baseline; justify-content: space-between; gap: 4px 12px; flex-wrap: wrap; padding: 14px 16px; border-bottom: 1px solid var(--pv-border); }
|
|
271
297
|
.pv-detail__id { font: 600 13px/1.2 var(--pv-mono); color: var(--pv-ink); }
|
|
272
298
|
.pv-detail__stamp { font: 400 12px/1.2 var(--pv-sans); color: var(--pv-ink-soft); }
|
|
273
299
|
.pv-detail__meta { display: grid; gap: 12px; padding: 14px 16px; border-bottom: 1px solid var(--pv-border); }
|
|
274
|
-
.pv-detail__meta--split { grid-template-columns:
|
|
300
|
+
.pv-detail__meta--split { grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); }
|
|
275
301
|
.pv-detail__field { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
|
|
276
302
|
.pv-detail__item { font: 400 13px/1.4 var(--pv-mono); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
277
|
-
.pv-detail__who { font: 400 13px/1.4 var(--pv-sans); color: var(--pv-ink); }
|
|
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; }
|
|
278
308
|
.pv-detail__body { display: flex; flex-direction: column; gap: 14px; padding: 14px 16px; }
|
|
279
309
|
.pv-detail__note { margin: 0; font: 400 12px/1.6 var(--pv-sans); color: var(--pv-ink-soft); }
|
|
280
310
|
.pv-label { font: 600 11px/1.2 var(--pv-sans); letter-spacing: .06em; text-transform: uppercase; color: var(--pv-ink-faint); }
|
|
281
311
|
|
|
282
312
|
.pv-attribute { display: flex; flex-direction: column; gap: 6px; }
|
|
283
313
|
.pv-attribute__name { font: 600 12px/1.3 var(--pv-mono); color: var(--pv-ink); word-break: break-all; }
|
|
284
|
-
.pv-attribute__unchanged {
|
|
314
|
+
.pv-attribute__unchanged, .pv-meta__more {
|
|
285
315
|
align-self: flex-start;
|
|
286
316
|
padding: 0;
|
|
287
317
|
border: 0;
|
|
@@ -339,6 +369,65 @@
|
|
|
339
369
|
.pv-snippet__copy:hover { background: var(--pv-pill-bg); }
|
|
340
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; }
|
|
341
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
|
+
|
|
342
431
|
.pv-empty { margin: 0; padding: 48px 16px; text-align: center; color: var(--pv-ink-soft); }
|
|
343
432
|
.pv-footer {
|
|
344
433
|
display: grid;
|
|
@@ -369,20 +458,25 @@
|
|
|
369
458
|
|
|
370
459
|
@media (max-width: 900px) {
|
|
371
460
|
.pv-layout { flex-direction: column; }
|
|
372
|
-
.pv-side { position: static; width: 100%; }
|
|
461
|
+
.pv-side { position: static; width: 100%; max-height: none; overflow: visible; }
|
|
373
462
|
}
|
|
374
463
|
|
|
375
464
|
@media (max-width: 720px) {
|
|
376
465
|
.pv-main { padding: 16px; }
|
|
377
466
|
.pv-footer { grid-template-columns: 1fr; justify-items: center; }
|
|
378
467
|
.pv-footer__end, .pv-pagination { grid-column: 1; justify-content: center; }
|
|
379
|
-
.pv-header { padding:
|
|
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; }
|
|
380
474
|
}
|
|
381
475
|
|
|
382
476
|
@media (max-width: 640px) {
|
|
383
|
-
.pv-version, .pv-version--
|
|
477
|
+
.pv-version, .pv-version--multi-item-view { grid-template-columns: 52px 16px 1fr; padding: 12px 14px; }
|
|
384
478
|
.pv-version__rail { grid-row: 1 / -1; }
|
|
385
479
|
.pv-version__item, .pv-version__content, .pv-version__id { grid-column: 3; }
|
|
386
|
-
.pv-value, .pv-version--
|
|
480
|
+
.pv-value, .pv-version--multi-item-view .pv-value { max-width: 100%; }
|
|
387
481
|
}
|
|
388
482
|
</style>
|
|
@@ -0,0 +1,84 @@
|
|
|
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-card__head">
|
|
34
|
+
<span class="pv-card__title">Weight by item type</span>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<div class="pv-stats">
|
|
38
|
+
<div class="pv-stat pv-stat--head">
|
|
39
|
+
<span>Item type</span>
|
|
40
|
+
<span class="pv-stat__figure">Versions</span>
|
|
41
|
+
<span class="pv-stat__figure">Estimated payload</span>
|
|
42
|
+
<span>Share</span>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<% if stats %>
|
|
46
|
+
<% stats.rows.each do |row| %>
|
|
47
|
+
<% share = row.share(stats.estimated_total_bytes) %>
|
|
48
|
+
|
|
49
|
+
<div class="pv-stat">
|
|
50
|
+
<span class="pv-stat__type">
|
|
51
|
+
<% if row.item_type %>
|
|
52
|
+
<%= link_to row.item_type, versions_path(item_type: row.item_type) %>
|
|
53
|
+
<% else %>
|
|
54
|
+
<span class="pv-muted"><%= PaperView::ApplicationHelper::EMPTY %></span>
|
|
55
|
+
<% end %>
|
|
56
|
+
</span>
|
|
57
|
+
|
|
58
|
+
<span class="pv-stat__figure"><%= number_with_delimiter(row.count) %></span>
|
|
59
|
+
<span class="pv-stat__figure"><%= paper_view_bytes(row.estimated_bytes) %></span>
|
|
60
|
+
|
|
61
|
+
<span class="pv-stat__share">
|
|
62
|
+
<svg class="pv-share" viewBox="0 0 100 8" preserveAspectRatio="none" aria-hidden="true">
|
|
63
|
+
<rect class="pv-share__fill" x="0" y="0" width="<%= (share * 100).round(2) %>" height="8" rx="1.5"></rect>
|
|
64
|
+
</svg>
|
|
65
|
+
<span class="pv-stat__percent"><%= paper_view_percentage(share) %></span>
|
|
66
|
+
</span>
|
|
67
|
+
</div>
|
|
68
|
+
<% end %>
|
|
69
|
+
<% else %>
|
|
70
|
+
<% 6.times do %>
|
|
71
|
+
<div class="pv-stat pv-stat--pending">
|
|
72
|
+
<span class="pv-stat__type"><%= tag.span(class: "pv-skeleton pv-skeleton--type") %></span>
|
|
73
|
+
<span class="pv-stat__figure"><%= tag.span(class: "pv-skeleton pv-skeleton--count") %></span>
|
|
74
|
+
<span class="pv-stat__figure"><%= tag.span(class: "pv-skeleton pv-skeleton--bytes") %></span>
|
|
75
|
+
<span class="pv-stat__share">
|
|
76
|
+
<%= tag.span(class: "pv-skeleton pv-skeleton--bar") %>
|
|
77
|
+
<%= tag.span(class: "pv-skeleton pv-skeleton--percent") %>
|
|
78
|
+
</span>
|
|
79
|
+
</div>
|
|
80
|
+
<% end %>
|
|
81
|
+
<% end %>
|
|
82
|
+
</div>
|
|
83
|
+
</section>
|
|
84
|
+
<% 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,
|
|
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,
|
|
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
|
|