studio-engine 0.6.1 → 0.6.2
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/CHANGELOG.md +8 -0
- data/app/assets/javascripts/studio/sticky_table_header.js +194 -0
- data/app/assets/stylesheets/studio/sticky_table_header.css +48 -0
- data/app/views/layouts/studio/_head.html.erb +6 -0
- data/lib/studio/engine.rb +7 -0
- data/lib/studio/version.rb +1 -1
- data/lib/studio.rb +1 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bc7a335b47486105708b2b7739c6a567e1486c6a68b2bf6d2e5e5abd8bc8d6a1
|
|
4
|
+
data.tar.gz: a0ab08402099d51e7fa059f6171cd83a7ad175c7a6ba2ae983e59dba9a36672f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f4b8203aa82fb630a33c0c12b3cc77f2f51cfe3cbbb8328354c6743a43643758403d570ecfb78569b386b614ca12cedeb29b81b1c122b1f1104fa7ab3ba83998
|
|
7
|
+
data.tar.gz: e568830a6432462d083ad833d75f999a9447f3547f97a83fbb7b9f9ea9fdfcdc2b05566e0f77159b72bb4a84d8395e5aaeef61e3a1ebbae7f931167bcf36df9b
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,14 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## v0.6.2 (2026-06-19)
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- **`Studio.sticky_table_headers` opt-in** for shared sticky table headers. The
|
|
11
|
+
flag defaults off and, when enabled by a consumer app, loads the engine-owned
|
|
12
|
+
`studio/sticky_table_header.css` and `studio/sticky_table_header.js` assets
|
|
13
|
+
from the shared Studio head partial.
|
|
14
|
+
|
|
7
15
|
## v0.6.1 (2026-06-18)
|
|
8
16
|
|
|
9
17
|
### Added
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
if (window.StudioStickyTableHeaders?.loaded) return;
|
|
3
|
+
|
|
4
|
+
const MANUAL_TABLE_SELECTOR = "table[data-sticky-table-header]";
|
|
5
|
+
const AUTO_TABLE_SELECTOR = "table";
|
|
6
|
+
const SKIP_CONTAINER_SELECTOR = "[data-sticky-table-skip]";
|
|
7
|
+
|
|
8
|
+
let stickyTables = [];
|
|
9
|
+
let scheduled = false;
|
|
10
|
+
let bootScheduled = false;
|
|
11
|
+
let navResizeObserver = null;
|
|
12
|
+
let tableMutationObserver = null;
|
|
13
|
+
|
|
14
|
+
window.StudioStickyTableHeaders = { loaded: true };
|
|
15
|
+
|
|
16
|
+
function navOffset() {
|
|
17
|
+
const raw = getComputedStyle(document.documentElement).getPropertyValue("--nav-h");
|
|
18
|
+
return Number.parseFloat(raw) || 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function scheduleUpdate() {
|
|
22
|
+
if (scheduled) return;
|
|
23
|
+
scheduled = true;
|
|
24
|
+
requestAnimationFrame(() => {
|
|
25
|
+
scheduled = false;
|
|
26
|
+
stickyTables.forEach((stickyTable) => stickyTable.update());
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function scheduleBoot() {
|
|
31
|
+
if (bootScheduled) return;
|
|
32
|
+
bootScheduled = true;
|
|
33
|
+
requestAnimationFrame(() => {
|
|
34
|
+
bootScheduled = false;
|
|
35
|
+
bootStickyTables();
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
class StickyTableHeader {
|
|
40
|
+
constructor(table) {
|
|
41
|
+
this.table = table;
|
|
42
|
+
this.prepareTable();
|
|
43
|
+
this.scroller = table.closest("[data-sticky-table-scroll]") || table.parentElement;
|
|
44
|
+
this.cloneShell = document.createElement("div");
|
|
45
|
+
this.cloneShell.className = "sticky-table-header-clone";
|
|
46
|
+
this.cloneShell.setAttribute("aria-hidden", "true");
|
|
47
|
+
|
|
48
|
+
this.cloneTable = document.createElement("table");
|
|
49
|
+
this.cloneTable.className = table.className;
|
|
50
|
+
this.cloneTable.innerHTML = table.tHead ? table.tHead.outerHTML : "";
|
|
51
|
+
this.cloneShell.appendChild(this.cloneTable);
|
|
52
|
+
document.body.appendChild(this.cloneShell);
|
|
53
|
+
|
|
54
|
+
this.scroller?.addEventListener("scroll", scheduleUpdate, { passive: true });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
prepareTable() {
|
|
58
|
+
this.table.classList.add("sticky-data-table");
|
|
59
|
+
if (!this.table.hasAttribute("data-sticky-table-header")) {
|
|
60
|
+
this.table.setAttribute("data-sticky-table-header", "auto");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const scroller = this.table.closest("[data-sticky-table-scroll]") || this.table.parentElement;
|
|
64
|
+
if (scroller && !scroller.hasAttribute("data-sticky-table-scroll")) {
|
|
65
|
+
scroller.setAttribute("data-sticky-table-scroll", "auto");
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
destroy() {
|
|
70
|
+
this.scroller?.removeEventListener("scroll", scheduleUpdate);
|
|
71
|
+
this.cloneShell.remove();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
syncColumnWidths() {
|
|
75
|
+
const originalCells = this.table.tHead?.querySelectorAll("th") || [];
|
|
76
|
+
const cloneCells = this.cloneTable.tHead?.querySelectorAll("th") || [];
|
|
77
|
+
originalCells.forEach((cell, index) => {
|
|
78
|
+
if (!cloneCells[index]) return;
|
|
79
|
+
cloneCells[index].style.width = `${cell.getBoundingClientRect().width}px`;
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
update() {
|
|
84
|
+
if (!document.body.contains(this.table) || !this.table.tHead || !this.scroller) {
|
|
85
|
+
this.destroy();
|
|
86
|
+
stickyTables = stickyTables.filter((stickyTable) => stickyTable !== this);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const offset = navOffset();
|
|
91
|
+
const headerRect = this.table.tHead.getBoundingClientRect();
|
|
92
|
+
const tableRect = this.table.getBoundingClientRect();
|
|
93
|
+
const scrollerRect = this.scroller.getBoundingClientRect();
|
|
94
|
+
const cloneHeight = headerRect.height || this.cloneShell.getBoundingClientRect().height;
|
|
95
|
+
const active = headerRect.top <= offset && tableRect.bottom > offset;
|
|
96
|
+
|
|
97
|
+
if (!active) {
|
|
98
|
+
this.cloneShell.style.display = "none";
|
|
99
|
+
this.table.removeAttribute("data-sticky-table-active");
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
this.syncColumnWidths();
|
|
104
|
+
|
|
105
|
+
const top = Math.min(offset, tableRect.bottom - cloneHeight);
|
|
106
|
+
const translateX = tableRect.left - scrollerRect.left;
|
|
107
|
+
|
|
108
|
+
this.cloneShell.style.display = "block";
|
|
109
|
+
this.cloneShell.style.left = `${scrollerRect.left}px`;
|
|
110
|
+
this.cloneShell.style.top = `${top}px`;
|
|
111
|
+
this.cloneShell.style.width = `${scrollerRect.width}px`;
|
|
112
|
+
this.cloneShell.style.height = `${cloneHeight}px`;
|
|
113
|
+
this.cloneTable.style.width = `${tableRect.width}px`;
|
|
114
|
+
this.cloneTable.style.transform = `translateX(${translateX}px)`;
|
|
115
|
+
this.table.setAttribute("data-sticky-table-active", "true");
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function bootStickyTables() {
|
|
120
|
+
startTableObserver();
|
|
121
|
+
|
|
122
|
+
if (navResizeObserver) navResizeObserver.disconnect();
|
|
123
|
+
if (window.ResizeObserver) {
|
|
124
|
+
const header = document.querySelector("header");
|
|
125
|
+
if (header) {
|
|
126
|
+
navResizeObserver = new ResizeObserver(scheduleUpdate);
|
|
127
|
+
navResizeObserver.observe(header);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
stickyTables.forEach((stickyTable) => stickyTable.destroy());
|
|
132
|
+
stickyTables = stickyTableCandidates().map((table) => new StickyTableHeader(table));
|
|
133
|
+
scheduleUpdate();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function startTableObserver() {
|
|
137
|
+
if (!window.MutationObserver || tableMutationObserver || !document.body) return;
|
|
138
|
+
|
|
139
|
+
tableMutationObserver = new MutationObserver((mutations) => {
|
|
140
|
+
const shouldReboot = mutations.some((mutation) => (
|
|
141
|
+
Array.from(mutation.addedNodes).some(nodeContainsCandidateTable) ||
|
|
142
|
+
Array.from(mutation.removedNodes).some(nodeContainsCandidateTable)
|
|
143
|
+
));
|
|
144
|
+
|
|
145
|
+
if (shouldReboot) scheduleBoot();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
tableMutationObserver.observe(document.body, { childList: true, subtree: true });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function nodeContainsCandidateTable(node) {
|
|
152
|
+
if (node.nodeType !== Node.ELEMENT_NODE) return false;
|
|
153
|
+
if (node.matches(".sticky-table-header-clone") || node.closest(".sticky-table-header-clone")) return false;
|
|
154
|
+
|
|
155
|
+
return node.matches("table, thead, th") || Boolean(node.querySelector("table, thead, th"));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function stickyTableCandidates() {
|
|
159
|
+
const candidates = [];
|
|
160
|
+
const seen = new Set();
|
|
161
|
+
|
|
162
|
+
document.querySelectorAll(`${MANUAL_TABLE_SELECTOR}, ${AUTO_TABLE_SELECTOR}`).forEach((table) => {
|
|
163
|
+
if (seen.has(table) || !shouldEnhanceTable(table)) return;
|
|
164
|
+
seen.add(table);
|
|
165
|
+
candidates.push(table);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
return candidates;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function shouldEnhanceTable(table) {
|
|
172
|
+
if (table.getAttribute("data-sticky-table-header") === "false") return false;
|
|
173
|
+
if (table.closest(SKIP_CONTAINER_SELECTOR)) return false;
|
|
174
|
+
if (table.closest(".sticky-table-header-clone")) return false;
|
|
175
|
+
if (table.closest("template")) return false;
|
|
176
|
+
if (table.getAttribute("role")?.toLowerCase() === "presentation") return false;
|
|
177
|
+
if (!table.tHead || !table.tHead.querySelector("th")) return false;
|
|
178
|
+
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
document.addEventListener("turbo:load", bootStickyTables);
|
|
183
|
+
document.addEventListener("DOMContentLoaded", bootStickyTables);
|
|
184
|
+
document.addEventListener("turbo:before-cache", () => {
|
|
185
|
+
if (navResizeObserver) navResizeObserver.disconnect();
|
|
186
|
+
if (tableMutationObserver) tableMutationObserver.disconnect();
|
|
187
|
+
navResizeObserver = null;
|
|
188
|
+
tableMutationObserver = null;
|
|
189
|
+
stickyTables.forEach((stickyTable) => stickyTable.destroy());
|
|
190
|
+
stickyTables = [];
|
|
191
|
+
});
|
|
192
|
+
window.addEventListener("scroll", scheduleUpdate, { passive: true });
|
|
193
|
+
window.addEventListener("resize", scheduleUpdate, { passive: true });
|
|
194
|
+
})();
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
.sticky-data-table {
|
|
2
|
+
border-collapse: separate;
|
|
3
|
+
border-spacing: 0;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
.sticky-data-table thead th {
|
|
7
|
+
background: var(--color-surface-alt);
|
|
8
|
+
box-shadow:
|
|
9
|
+
inset 0 -1px 0 var(--color-border),
|
|
10
|
+
0 10px 18px -18px rgba(15, 23, 42, 0.55);
|
|
11
|
+
transition:
|
|
12
|
+
background-color 200ms ease,
|
|
13
|
+
box-shadow 200ms ease,
|
|
14
|
+
top 200ms ease;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.dark .sticky-data-table thead th {
|
|
18
|
+
box-shadow:
|
|
19
|
+
inset 0 -1px 0 var(--color-border),
|
|
20
|
+
0 12px 22px -18px rgba(0, 0, 0, 0.85);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.sticky-table-header-clone {
|
|
24
|
+
position: fixed;
|
|
25
|
+
display: none;
|
|
26
|
+
overflow: hidden;
|
|
27
|
+
pointer-events: none;
|
|
28
|
+
z-index: 45;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.sticky-table-header-clone table {
|
|
32
|
+
border-collapse: separate;
|
|
33
|
+
border-spacing: 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.sticky-table-header-clone thead th {
|
|
37
|
+
position: static;
|
|
38
|
+
background: var(--color-surface-alt);
|
|
39
|
+
box-shadow:
|
|
40
|
+
inset 0 -1px 0 var(--color-border),
|
|
41
|
+
0 12px 22px -18px rgba(15, 23, 42, 0.65);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.dark .sticky-table-header-clone thead th {
|
|
45
|
+
box-shadow:
|
|
46
|
+
inset 0 -1px 0 var(--color-border),
|
|
47
|
+
0 14px 24px -18px rgba(0, 0, 0, 0.9);
|
|
48
|
+
}
|
|
@@ -107,4 +107,10 @@
|
|
|
107
107
|
|
|
108
108
|
<%= stylesheet_link_tag "tailwind", "data-turbo-track": "reload" %>
|
|
109
109
|
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
|
|
110
|
+
<% if Studio.sticky_table_headers %>
|
|
111
|
+
<%= stylesheet_link_tag "studio/sticky_table_header", "data-turbo-track": "reload" %>
|
|
112
|
+
<% end %>
|
|
110
113
|
<%= javascript_importmap_tags %>
|
|
114
|
+
<% if Studio.sticky_table_headers %>
|
|
115
|
+
<%= javascript_include_tag "studio/sticky_table_header", defer: true, "data-turbo-track": "reload" %>
|
|
116
|
+
<% end %>
|
data/lib/studio/engine.rb
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
module Studio
|
|
2
2
|
class Engine < ::Rails::Engine
|
|
3
|
+
initializer "studio.assets" do |app|
|
|
4
|
+
app.config.assets.precompile += %w[
|
|
5
|
+
studio/sticky_table_header.css
|
|
6
|
+
studio/sticky_table_header.js
|
|
7
|
+
]
|
|
8
|
+
end
|
|
9
|
+
|
|
3
10
|
rake_tasks do
|
|
4
11
|
load File.expand_path("../tasks/studio_email.rake", __dir__)
|
|
5
12
|
load File.expand_path("../tasks/studio_ses.rake", __dir__)
|
data/lib/studio/version.rb
CHANGED
data/lib/studio.rb
CHANGED
|
@@ -20,6 +20,7 @@ module Studio
|
|
|
20
20
|
mattr_accessor :sso_logo, default: nil
|
|
21
21
|
mattr_accessor :wallet_address_method, default: nil
|
|
22
22
|
mattr_accessor :theme_logos, default: []
|
|
23
|
+
mattr_accessor :sticky_table_headers, default: false
|
|
23
24
|
|
|
24
25
|
# ---- Authentication ------------------------------------------------------
|
|
25
26
|
# Which sign-in methods this app offers. The shared login/signup views render
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: studio-engine
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.6.
|
|
4
|
+
version: 0.6.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alex McRitchie
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-06-
|
|
11
|
+
date: 2026-06-19 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|
|
@@ -142,6 +142,8 @@ files:
|
|
|
142
142
|
- README.md
|
|
143
143
|
- app/assets/images/resend-favicon.png
|
|
144
144
|
- app/assets/images/ses-favicon.png
|
|
145
|
+
- app/assets/javascripts/studio/sticky_table_header.js
|
|
146
|
+
- app/assets/stylesheets/studio/sticky_table_header.css
|
|
145
147
|
- app/controllers/concerns/solana/session_auth.rb
|
|
146
148
|
- app/controllers/concerns/studio/admin_models.rb
|
|
147
149
|
- app/controllers/concerns/studio/error_handling.rb
|