studio-engine 0.126.0 → 0.127.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/CHANGELOG.md +22 -0
- data/README.md +5 -1
- data/app/assets/tailwind/studio_engine/engine.css +47 -3
- data/app/javascript/studio/controllers/page_header_controller.js +16 -2
- data/app/javascript/studio/page_header.js +7 -0
- data/app/views/components/_link_sidebar.html.erb +2 -0
- data/app/views/components/_sidebar_panel.html.erb +9 -1
- data/app/views/studio/_page_header.html.erb +5 -3
- data/lib/studio/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 22ef2ab3094c82f6f447ee86455dac3e2c70b759ec3d58f2f1a887a347c4b1ad
|
|
4
|
+
data.tar.gz: 92c5bc2d6c5f53914e33d7c50623a076791ef50faff385b4e63218e3773eb6a6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1420fb65a41535cc4c4ae4a1466a83fe46d02eca34c17ed26b1ef1fdfc1cf6ca934776f70a843380410593c7932bbc23c53fe2fe6f0b992e3bf5a19206b5e7f3
|
|
7
|
+
data.tar.gz: c88656c2c2363917fff41839b65c00ccc0f499d3c30c1a216ae783081f6712b44c200ef93500fceae04d99f6719db344ed0b213b742e104d632d9a0d063fc4d3
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,28 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.127.0 — 2026-11-04
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **Link sidebar: a soft blurred edge.** `components/_sidebar_panel` takes `edge:`, default
|
|
12
|
+
false, and `components/_link_sidebar` passes `edge: true`. From `md` up, the panel draws a band
|
|
13
|
+
along its inner edge: 40px wide, a 6px blur, fading out into the page. It is a pseudo-element
|
|
14
|
+
of the panel, so it slides with it, leaves with it and takes no clicks. It adds no scrim and no
|
|
15
|
+
darkening. A browser with no `backdrop-filter` shows the panel plain. A host panel keeps its
|
|
16
|
+
plain edge unless it opts in. The navbar baseline is regenerated for the link sidebar's
|
|
17
|
+
`data-sidebar-edge` attribute.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- **`studio/_page_header` takes a long title, and any parent.** A long title used to squeeze
|
|
22
|
+
the tabs out and overflow the page. It now wraps and stops at three lines, and the tabs move
|
|
23
|
+
to their own line when it leaves them less than 6rem, so a page has no sideways overflow at
|
|
24
|
+
any width. A short title's row is unchanged. In a flex or grid parent with a sibling above
|
|
25
|
+
it, the row no longer shows its bar at rest: the controller measures the row's place from
|
|
26
|
+
the row there, because the sentinel sits at the parent's start. `docs/PAGE_HEADER.md` loses
|
|
27
|
+
both limits.
|
|
28
|
+
|
|
7
29
|
## 0.126.0 — 2026-11-04
|
|
8
30
|
|
|
9
31
|
### Changed
|
data/README.md
CHANGED
|
@@ -1092,9 +1092,13 @@ config.navbar_account_sidebar = { flag: "gearOpen", controls: %w[gear-sidebar ge
|
|
|
1092
1092
|
|
|
1093
1093
|
### Sidebar panel lead and close hook
|
|
1094
1094
|
|
|
1095
|
-
`components/_sidebar_panel` takes
|
|
1095
|
+
`components/_sidebar_panel` takes three opt-in locals; unset, the panel renders
|
|
1096
1096
|
as before:
|
|
1097
1097
|
|
|
1098
|
+
- `edge: true` draws a soft blurred band along the panel's inner edge from `md`
|
|
1099
|
+
up, through a `data-sidebar-edge` hook and engine.css. It takes no clicks. The
|
|
1100
|
+
link sidebar passes it. It softens the page beside the panel, so leave it off
|
|
1101
|
+
a panel kept open beside live content.
|
|
1098
1102
|
- `title_html:` HTML in the title's place, for a panel whose header leads with
|
|
1099
1103
|
a live status line. It is content, not a name: it never feeds
|
|
1100
1104
|
`aria-labelledby` or `aria-label`, so a caller that passes it instead of
|
|
@@ -229,12 +229,19 @@ body.modal-open .studio-app-banner {
|
|
|
229
229
|
the pinned stack has nothing to republish. The layout is owned here: the
|
|
230
230
|
engine's views depend on no responsive utility a host may not have emitted.
|
|
231
231
|
The fade is owned too, because .fade-edge-x is in the opt-in motion layer, and
|
|
232
|
-
it marks only an end that hides a tab (studio/controllers/page_header).
|
|
232
|
+
it marks only an end that hides a tab (studio/controllers/page_header).
|
|
233
|
+
|
|
234
|
+
A LONG TITLE: the row wraps. The tabs' flex-basis is their floor, so a title that
|
|
235
|
+
leaves them less than that takes the first line and the tabs take the next, at
|
|
236
|
+
full width. A title too long for its line wraps, then stops at three lines. A short
|
|
237
|
+
title never reaches the floor, and its row is one line as before. */
|
|
233
238
|
|
|
234
239
|
@utility page-header {
|
|
235
240
|
display: flex;
|
|
241
|
+
flex-wrap: wrap;
|
|
236
242
|
align-items: center;
|
|
237
243
|
gap: 0.75rem;
|
|
244
|
+
min-width: 0;
|
|
238
245
|
padding-block: 0.5rem;
|
|
239
246
|
border-block: 1px solid transparent;
|
|
240
247
|
background-color: transparent;
|
|
@@ -256,7 +263,7 @@ body.modal-open .studio-app-banner {
|
|
|
256
263
|
display: flex;
|
|
257
264
|
align-items: baseline;
|
|
258
265
|
gap: 0.5rem;
|
|
259
|
-
flex
|
|
266
|
+
flex: 0 1 auto;
|
|
260
267
|
min-width: 0;
|
|
261
268
|
}
|
|
262
269
|
|
|
@@ -265,6 +272,14 @@ body.modal-open .studio-app-banner {
|
|
|
265
272
|
font-size: calc(var(--type-base, 16px) * 1.5);
|
|
266
273
|
font-weight: var(--type-display-weight, 800);
|
|
267
274
|
line-height: 1.25;
|
|
275
|
+
min-width: 0;
|
|
276
|
+
overflow-wrap: anywhere;
|
|
277
|
+
display: -webkit-box;
|
|
278
|
+
-webkit-box-orient: vertical;
|
|
279
|
+
-webkit-line-clamp: 3;
|
|
280
|
+
/* Up and down only. A hidden x would shave a glyph's overhang off a short title. */
|
|
281
|
+
overflow-x: visible;
|
|
282
|
+
overflow-y: clip;
|
|
268
283
|
}
|
|
269
284
|
|
|
270
285
|
@utility page-header-meta {
|
|
@@ -278,7 +293,7 @@ body.modal-open .studio-app-banner {
|
|
|
278
293
|
@utility page-header-tabs {
|
|
279
294
|
--page-header-fade-start: 0px;
|
|
280
295
|
--page-header-fade-end: 0px;
|
|
281
|
-
flex: 1 1
|
|
296
|
+
flex: 1 1 6rem;
|
|
282
297
|
min-width: 0;
|
|
283
298
|
overflow-x: auto;
|
|
284
299
|
scrollbar-width: none;
|
|
@@ -1085,6 +1100,35 @@ html[data-page-transition="unfold-top"]::view-transition-new(root) { animation-n
|
|
|
1085
1100
|
}
|
|
1086
1101
|
}
|
|
1087
1102
|
|
|
1103
|
+
/* THE SIDEBAR'S EDGE (components/_sidebar_panel, edge: true). A soft band along the panel's
|
|
1104
|
+
inner edge, where the page behind is blurred and fades out to sharp, so the panel
|
|
1105
|
+
stands off a busy page with no scrim and no darkening.
|
|
1106
|
+
|
|
1107
|
+
It is a pseudo-element of the panel: it moves with the slide, it leaves with the
|
|
1108
|
+
panel, and it takes no clicks, so an outside click still reaches the page. A
|
|
1109
|
+
transform on the panel does not start a new backdrop root, so it blurs the page.
|
|
1110
|
+
40px wide, a 6px blur, held for the 10px nearest the panel, then fading to sharp.
|
|
1111
|
+
right: 100% measures from the padding box, so the band runs over the panel's 1px
|
|
1112
|
+
border. Stopping short of it left the border as a lit line beside the blur.
|
|
1113
|
+
|
|
1114
|
+
FROM md UP ONLY. Below that the link sidebar fills the width, so nothing sits
|
|
1115
|
+
beside it, and a phone does not pay for a backdrop filter through the slide. A
|
|
1116
|
+
browser with no backdrop-filter gets an empty box, so the panel is plain. */
|
|
1117
|
+
@media (min-width: 768px) {
|
|
1118
|
+
[data-sidebar-edge]::before {
|
|
1119
|
+
content: "";
|
|
1120
|
+
position: absolute;
|
|
1121
|
+
inset-block: 0;
|
|
1122
|
+
right: 100%;
|
|
1123
|
+
width: 2.5rem;
|
|
1124
|
+
pointer-events: none;
|
|
1125
|
+
-webkit-backdrop-filter: blur(6px);
|
|
1126
|
+
backdrop-filter: blur(6px);
|
|
1127
|
+
-webkit-mask-image: linear-gradient(to left, #000 25%, transparent);
|
|
1128
|
+
mask-image: linear-gradient(to left, #000 25%, transparent);
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1088
1132
|
/* THE SIDEBAR ROW'S DESCRIPTION (components/_link_sidebar, _geo_signpost). At rest
|
|
1089
1133
|
a row with a description is one line, the height of any other row. On hover or
|
|
1090
1134
|
keyboard focus the description fades in under the label, and no layout changes:
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// carries data-studio-controller-failed and engine.css gives the row its bar at
|
|
11
11
|
// all times, so a stuck row never lays a transparent title over the page.
|
|
12
12
|
import { Controller } from "@hotwired/stimulus"
|
|
13
|
-
import { hiddenEnds, isStuck, revealBy } from "studio/page_header"
|
|
13
|
+
import { hiddenEnds, isStuck, revealBy, sentinelMarksThePlace } from "studio/page_header"
|
|
14
14
|
|
|
15
15
|
export default class extends Controller {
|
|
16
16
|
connect() {
|
|
@@ -42,7 +42,7 @@ export default class extends Controller {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
sync() {
|
|
45
|
-
const stuck = isStuck(this.row.getBoundingClientRect().top, this.
|
|
45
|
+
const stuck = isStuck(this.row.getBoundingClientRect().top, this.restTop())
|
|
46
46
|
const ends = this.tabs ? hiddenEnds(this.tabs) : null
|
|
47
47
|
|
|
48
48
|
this.row.toggleAttribute("data-stuck", stuck)
|
|
@@ -51,6 +51,20 @@ export default class extends Controller {
|
|
|
51
51
|
this.tabs.toggleAttribute("data-fade-end", ends.end)
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
// The top of the row's place in the flow. In a flex or grid parent only the row
|
|
55
|
+
// can say, so it is unstuck for one read. No frame paints in between, and the box
|
|
56
|
+
// keeps its size, so nothing moves and the stack republishes nothing.
|
|
57
|
+
restTop() {
|
|
58
|
+
if (sentinelMarksThePlace(getComputedStyle(this.element.parentElement).display)) {
|
|
59
|
+
return this.element.getBoundingClientRect().top
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
this.row.style.position = "static"
|
|
63
|
+
const top = this.row.getBoundingClientRect().top
|
|
64
|
+
this.row.style.position = ""
|
|
65
|
+
return top
|
|
66
|
+
}
|
|
67
|
+
|
|
54
68
|
// Moves the tabs only: scrollIntoView would also move the page.
|
|
55
69
|
revealOpenTab() {
|
|
56
70
|
const open = this.tabs?.querySelector("[aria-current]")
|
|
@@ -8,6 +8,13 @@ export function isStuck(rowTop, restTop) {
|
|
|
8
8
|
return rowTop - restTop > 0.5
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
// Whether the sentinel marks the row's place. It is absolutely positioned, so in a
|
|
12
|
+
// block parent it sits where the row starts. In a flex or grid parent it sits at the
|
|
13
|
+
// parent's start instead, whatever is above the row.
|
|
14
|
+
export function sentinelMarksThePlace(parentDisplay) {
|
|
15
|
+
return !/flex|grid/.test(parentDisplay)
|
|
16
|
+
}
|
|
17
|
+
|
|
11
18
|
// Which ends of the tabs hide a tab, so a fade marks only an end with more to see.
|
|
12
19
|
export function hiddenEnds({ scrollLeft, clientWidth, scrollWidth }) {
|
|
13
20
|
return {
|
|
@@ -150,6 +150,7 @@
|
|
|
150
150
|
outside_action: close_action,
|
|
151
151
|
escape_action: close_action,
|
|
152
152
|
display_class: "hidden md:flex",
|
|
153
|
+
edge: true,
|
|
153
154
|
role: "navigation",
|
|
154
155
|
aria_label: sidebar_admin ? "Admin menu" : "Links menu" %>
|
|
155
156
|
|
|
@@ -164,5 +165,6 @@
|
|
|
164
165
|
escape_action: close_action,
|
|
165
166
|
display_class: "flex md:hidden",
|
|
166
167
|
width_class: "w-full",
|
|
168
|
+
edge: true,
|
|
167
169
|
role: "navigation",
|
|
168
170
|
aria_label: sidebar_admin ? "Admin menu" : "Links menu" %>
|
|
@@ -19,13 +19,17 @@
|
|
|
19
19
|
would cover the expanded header. engine.css (THE PINNED EDGE ON THE SCROLL
|
|
20
20
|
TIMELINE) moves it with the header on the scroll timeline instead.
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
Three opt-in locals (unset, the panel renders as it did in 0.113.0):
|
|
23
23
|
title_html the header's LEAD SLOT: HTML in the title's place, for a panel
|
|
24
24
|
whose lead is a live status line rather than a label. It is
|
|
25
25
|
content, NOT a name: it never feeds aria-labelledby or
|
|
26
26
|
aria-label, so a caller that passes it instead of `title` must
|
|
27
27
|
pass aria_label, or the panel has no accessible name. `title`
|
|
28
28
|
wins when both are passed. Escaped unless html_safe.
|
|
29
|
+
edge true stamps data-sidebar-edge, the soft blurred band along the
|
|
30
|
+
panel's inner edge, drawn by engine.css (THE SIDEBAR'S EDGE) from md
|
|
31
|
+
up. It softens the page beside the panel, so it suits a menu that
|
|
32
|
+
opens over the page, not a panel kept open beside live content.
|
|
29
33
|
close_hook true stamps the close button data-sidebar-panel-close="<id>"
|
|
30
34
|
too, so a host's own document handler can match the button
|
|
31
35
|
to its panel by id. Needs `id`; without one it adds nothing.
|
|
@@ -49,6 +53,7 @@
|
|
|
49
53
|
<% aria_label = local_assigns.fetch(:aria_label, title) %>
|
|
50
54
|
<% labelledby = id.present? && title.present? ? "#{id}-title" : nil %>
|
|
51
55
|
<% close_hook = local_assigns.fetch(:close_hook, false) && id.present? %>
|
|
56
|
+
<% edge = local_assigns.fetch(:edge, false) %>
|
|
52
57
|
|
|
53
58
|
<aside
|
|
54
59
|
<% if id.present? %>id="<%= id %>"<% end %>
|
|
@@ -76,6 +81,9 @@
|
|
|
76
81
|
class="<%= display_class %> fixed right-0 <%= width_class %> bg-surface border-l border-subtle shadow-2xl <%= z_class %> flex-col <%= panel_class %>"
|
|
77
82
|
style="top:var(--nav-bottom, var(--nav-h, 6rem));height:calc(100% - var(--nav-bottom, var(--nav-h, 6rem)))"
|
|
78
83
|
data-pin-edge
|
|
84
|
+
<% if edge %>
|
|
85
|
+
data-sidebar-edge
|
|
86
|
+
<% end %>
|
|
79
87
|
role="<%= role %>"
|
|
80
88
|
<% if labelledby %>aria-labelledby="<%= labelledby %>"<% elsif aria_label.present? %>aria-label="<%= aria_label %>"<% end %>>
|
|
81
89
|
<div class="flex flex-col h-full">
|
|
@@ -33,9 +33,11 @@
|
|
|
33
33
|
|
|
34
34
|
THE SENTINEL stays at the row's place in the flow, and the row's distance from
|
|
35
35
|
it is what "stuck" means, on every kind of header. It is absolutely positioned,
|
|
36
|
-
so a space-y parent adds nothing for it.
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
so a space-y parent adds nothing for it. In a flex or grid parent it sits at the
|
|
37
|
+
parent's start, so the controller measures the row's place from the row. It
|
|
38
|
+
carries the controller, so the row keeps its data-studio-controller for the
|
|
39
|
+
page's own. So the row carries no top margin: the space above it belongs to its
|
|
40
|
+
container. %>
|
|
39
41
|
<div class="page-header-sentinel" data-studio-controller="page-header" aria-hidden="true"></div>
|
|
40
42
|
<%= content_tag(tabs.any? ? :nav : :div,
|
|
41
43
|
class: "page-header sticky #{row_class}".strip,
|
data/lib/studio/version.rb
CHANGED
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.
|
|
4
|
+
version: 0.127.0
|
|
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-11-
|
|
11
|
+
date: 2026-11-05 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|