studio-engine 0.6.0 → 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 +15 -0
- data/README.md +15 -1
- data/app/assets/javascripts/studio/sticky_table_header.js +194 -0
- data/app/assets/stylesheets/studio/sticky_table_header.css +48 -0
- data/app/helpers/studio_theme_helper.rb +1 -1
- data/app/views/components/_emoji_swap.html.erb +17 -0
- data/app/views/layouts/studio/_head.html.erb +6 -0
- data/lib/studio/engine.rb +7 -0
- data/lib/studio/ui_primitives.rb +91 -0
- data/lib/studio/version.rb +1 -1
- data/lib/studio.rb +2 -0
- metadata +6 -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,21 @@ 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
|
+
|
|
15
|
+
## v0.6.1 (2026-06-18)
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- **`components/emoji_swap` UI primitive** plus shared CSS for nav/sidebar
|
|
19
|
+
emoji hover and focus transitions, including reduced-motion fallback and
|
|
20
|
+
aliases for the existing `nav-emoji-*` class names.
|
|
21
|
+
|
|
7
22
|
## v0.6.0 (2026-06-16)
|
|
8
23
|
|
|
9
24
|
### Changed
|
data/README.md
CHANGED
|
@@ -11,7 +11,7 @@ Shared Rails engine for McRitchie apps. Provides authentication, error handling,
|
|
|
11
11
|
gem "studio-engine", "~> 0.6"
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
Then `bundle install`. The current release is **v0.6.
|
|
14
|
+
Then `bundle install`. The current release is **v0.6.1**; see [`CHANGELOG.md`](./CHANGELOG.md) for the history.
|
|
15
15
|
|
|
16
16
|
> Published to RubyGems as of v0.4.0 (2026-05-17). New installs should use the RubyGems form, which the consumer Rails apps (`mcritchie-studio`, `turf-monster`) already use.
|
|
17
17
|
|
|
@@ -20,6 +20,7 @@ Then `bundle install`. The current release is **v0.6.0**; see [`CHANGELOG.md`](.
|
|
|
20
20
|
- **Authentication**: Passwordless magic-link auth, optional password auth, Google OAuth via OmniAuth, Solana wallet sign-in, and optional one-way SSO patterns
|
|
21
21
|
- **Error handling**: `Studio::ErrorHandling` concern with `rescue_and_log`, `ErrorLog` model with `capture!`, error log viewer at `/error_logs`
|
|
22
22
|
- **Theme system**: Dynamic CSS custom properties generated from 7 role colors (primary, dark, light, success, accent, warning, danger). Dark/light mode toggle. Admin theme editor at `/admin/theme`.
|
|
23
|
+
- **UI primitives**: Shared component partials and CSS primitives such as `components/emoji_swap` for nav/sidebar emoji hover transitions.
|
|
23
24
|
- **Operator tooling**: Shared `studio/banners/environment` banner with Dev Mode + email connector controls, `studio/banners/impersonation`, and an opt-in `Studio::Impersonation` concern for Act As session conventions.
|
|
24
25
|
- **Sluggable concern**: `before_save :set_slug` with `to_param` for human-readable URLs
|
|
25
26
|
- **ThemeSetting model**: Per-app DB overrides with fallback to config defaults
|
|
@@ -111,6 +112,19 @@ audit log, enter/exit controller actions, and any app-specific safeguards such
|
|
|
111
112
|
as binding session-token checks to `true_user` or disabling wallet-only
|
|
112
113
|
privileges while impersonating.
|
|
113
114
|
|
|
115
|
+
## UI Primitives
|
|
116
|
+
|
|
117
|
+
Render `components/emoji_swap` inside a link or button with the `group` class to
|
|
118
|
+
slide between two emoji on hover and keyboard focus. The CSS ships through
|
|
119
|
+
`studio_theme_css_tag`, including a reduced-motion fade fallback.
|
|
120
|
+
|
|
121
|
+
```erb
|
|
122
|
+
<%= link_to root_path, class: "group inline-flex items-center gap-2" do %>
|
|
123
|
+
<%= render "components/emoji_swap", base: "📊", hover: "✨" %>
|
|
124
|
+
<span>Dashboard</span>
|
|
125
|
+
<% end %>
|
|
126
|
+
```
|
|
127
|
+
|
|
114
128
|
## Overriding Views
|
|
115
129
|
|
|
116
130
|
This is a non-isolated engine -- app views at the same path automatically override engine views. For example, placing `app/views/sessions/new.html.erb` in the consuming app replaces the engine's login page.
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<%# locals: (base:, hover:, aria_label: nil, class_name: nil) %>
|
|
2
|
+
<%
|
|
3
|
+
wrapper_classes = ["studio-emoji-swap", "flex-shrink-0", local_assigns[:class_name]].compact_blank.join(" ")
|
|
4
|
+
attrs = { class: wrapper_classes }
|
|
5
|
+
|
|
6
|
+
if local_assigns[:aria_label].present?
|
|
7
|
+
attrs[:role] = "img"
|
|
8
|
+
attrs["aria-label"] = aria_label
|
|
9
|
+
else
|
|
10
|
+
attrs["aria-hidden"] = "true"
|
|
11
|
+
end
|
|
12
|
+
%>
|
|
13
|
+
|
|
14
|
+
<%= tag.span(**attrs) do %>
|
|
15
|
+
<span class="studio-emoji-swap-base" aria-hidden="true"><%= base %></span>
|
|
16
|
+
<span class="studio-emoji-swap-hover" aria-hidden="true"><%= hover %></span>
|
|
17
|
+
<% end %>
|
|
@@ -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__)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Studio
|
|
4
|
+
module UiPrimitives
|
|
5
|
+
EMOJI_SWAP_CSS = <<~CSS
|
|
6
|
+
.studio-emoji-swap,
|
|
7
|
+
.nav-emoji-swap {
|
|
8
|
+
position: relative;
|
|
9
|
+
display: inline-flex;
|
|
10
|
+
align-items: center;
|
|
11
|
+
justify-content: center;
|
|
12
|
+
vertical-align: middle;
|
|
13
|
+
width: 1.5rem;
|
|
14
|
+
height: 1.25rem;
|
|
15
|
+
overflow: hidden;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.studio-emoji-swap > span,
|
|
19
|
+
.nav-emoji-swap > span {
|
|
20
|
+
position: absolute;
|
|
21
|
+
inset: 0;
|
|
22
|
+
display: inline-flex;
|
|
23
|
+
align-items: center;
|
|
24
|
+
justify-content: center;
|
|
25
|
+
transition: transform 240ms cubic-bezier(0.34, 1.2, 0.5, 1), opacity 180ms ease;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.studio-emoji-swap-base,
|
|
29
|
+
.nav-emoji-base {
|
|
30
|
+
transform: translateX(0);
|
|
31
|
+
opacity: 1;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.studio-emoji-swap-hover,
|
|
35
|
+
.nav-emoji-hover {
|
|
36
|
+
transform: translateX(-110%);
|
|
37
|
+
opacity: 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.group:hover .studio-emoji-swap-base,
|
|
41
|
+
.group:focus-visible .studio-emoji-swap-base,
|
|
42
|
+
.group:focus-within .studio-emoji-swap-base,
|
|
43
|
+
.group:hover .nav-emoji-base,
|
|
44
|
+
.group:focus-visible .nav-emoji-base,
|
|
45
|
+
.group:focus-within .nav-emoji-base {
|
|
46
|
+
transform: translateX(110%);
|
|
47
|
+
opacity: 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.group:hover .studio-emoji-swap-hover,
|
|
51
|
+
.group:focus-visible .studio-emoji-swap-hover,
|
|
52
|
+
.group:focus-within .studio-emoji-swap-hover,
|
|
53
|
+
.group:hover .nav-emoji-hover,
|
|
54
|
+
.group:focus-visible .nav-emoji-hover,
|
|
55
|
+
.group:focus-within .nav-emoji-hover {
|
|
56
|
+
transform: translateX(0);
|
|
57
|
+
opacity: 1;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
@media (prefers-reduced-motion: reduce) {
|
|
61
|
+
.studio-emoji-swap > span,
|
|
62
|
+
.nav-emoji-swap > span {
|
|
63
|
+
transition: opacity 120ms ease;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.studio-emoji-swap-base,
|
|
67
|
+
.studio-emoji-swap-hover,
|
|
68
|
+
.nav-emoji-base,
|
|
69
|
+
.nav-emoji-hover,
|
|
70
|
+
.group:hover .studio-emoji-swap-base,
|
|
71
|
+
.group:focus-visible .studio-emoji-swap-base,
|
|
72
|
+
.group:focus-within .studio-emoji-swap-base,
|
|
73
|
+
.group:hover .studio-emoji-swap-hover,
|
|
74
|
+
.group:focus-visible .studio-emoji-swap-hover,
|
|
75
|
+
.group:focus-within .studio-emoji-swap-hover,
|
|
76
|
+
.group:hover .nav-emoji-base,
|
|
77
|
+
.group:focus-visible .nav-emoji-base,
|
|
78
|
+
.group:focus-within .nav-emoji-base,
|
|
79
|
+
.group:hover .nav-emoji-hover,
|
|
80
|
+
.group:focus-visible .nav-emoji-hover,
|
|
81
|
+
.group:focus-within .nav-emoji-hover {
|
|
82
|
+
transform: none;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
CSS
|
|
86
|
+
|
|
87
|
+
def self.css
|
|
88
|
+
EMOJI_SWAP_CSS
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
data/lib/studio/version.rb
CHANGED
data/lib/studio.rb
CHANGED
|
@@ -2,6 +2,7 @@ require "studio/version"
|
|
|
2
2
|
require "studio/engine"
|
|
3
3
|
require "studio/color_scale"
|
|
4
4
|
require "studio/theme_resolver"
|
|
5
|
+
require "studio/ui_primitives"
|
|
5
6
|
require "studio/username_generator"
|
|
6
7
|
require "studio/s3"
|
|
7
8
|
require "studio/image_cache"
|
|
@@ -19,6 +20,7 @@ module Studio
|
|
|
19
20
|
mattr_accessor :sso_logo, default: nil
|
|
20
21
|
mattr_accessor :wallet_address_method, default: nil
|
|
21
22
|
mattr_accessor :theme_logos, default: []
|
|
23
|
+
mattr_accessor :sticky_table_headers, default: false
|
|
22
24
|
|
|
23
25
|
# ---- Authentication ------------------------------------------------------
|
|
24
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
|
|
@@ -177,6 +179,7 @@ files:
|
|
|
177
179
|
- app/views/components/_badge.html.erb
|
|
178
180
|
- app/views/components/_card.html.erb
|
|
179
181
|
- app/views/components/_copy_button.html.erb
|
|
182
|
+
- app/views/components/_emoji_swap.html.erb
|
|
180
183
|
- app/views/components/_empty_state.html.erb
|
|
181
184
|
- app/views/components/_google_logo.html.erb
|
|
182
185
|
- app/views/components/_input.html.erb
|
|
@@ -230,6 +233,7 @@ files:
|
|
|
230
233
|
- lib/studio/mail_transport.rb
|
|
231
234
|
- lib/studio/s3.rb
|
|
232
235
|
- lib/studio/theme_resolver.rb
|
|
236
|
+
- lib/studio/ui_primitives.rb
|
|
233
237
|
- lib/studio/username_generator.rb
|
|
234
238
|
- lib/studio/version.rb
|
|
235
239
|
- lib/tasks/studio_email.rake
|