studio-engine 0.129.0 → 0.130.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 +41 -0
- data/README.md +5 -0
- data/app/assets/tailwind/studio_engine/engine-motion.css +148 -56
- data/app/assets/tailwind/studio_engine/engine.css +150 -19
- data/app/helpers/studio/fizz_helper.rb +5 -0
- data/app/javascript/studio/controllers/style_section_nav_controller.js +4 -4
- data/app/javascript/studio/pinned_stack.js +112 -4
- data/app/views/studio/_fizz.html.erb +26 -0
- data/app/views/studio/_fizz_layer.html.erb +4 -1
- data/app/views/studio/_hold_button.html.erb +1 -2
- data/app/views/studio/_page_header.html.erb +3 -2
- data/app/views/studio/_section_header.html.erb +23 -0
- data/app/views/style/_components.html.erb +3 -5
- data/app/views/style/_host.html.erb +3 -6
- data/app/views/style/_modal_specimen.html.erb +5 -5
- data/app/views/style/_modals.html.erb +3 -5
- data/app/views/style/_site_footer.html.erb +1 -1
- data/app/views/style/_specimen.html.erb +4 -0
- data/app/views/style/_tasks.html.erb +3 -5
- data/app/views/style/_theme.html.erb +2 -2
- data/app/views/style/_tricks.html.erb +155 -216
- data/app/views/style/index.html.erb +7 -3
- data/lib/studio/version.rb +1 -1
- metadata +3 -1
|
@@ -73,6 +73,11 @@ module Studio
|
|
|
73
73
|
"--fd:#{bit[:delay]}s;--ft:#{bit[:duration]}s;--fc:#{fizz_color(bit)}"
|
|
74
74
|
end
|
|
75
75
|
|
|
76
|
+
# A fixed palette as the inline style a stack carries: `--fizz-c-1:red;...`.
|
|
77
|
+
def fizz_stack_style(colors)
|
|
78
|
+
Array(colors).first(SLOTS).each_with_index.map { |c, i| "--fizz-c-#{i + 1}:#{c}" }.join(";")
|
|
79
|
+
end
|
|
80
|
+
|
|
76
81
|
private
|
|
77
82
|
|
|
78
83
|
# Where in its own zone a bubble sits. The outer columns each throw one
|
|
@@ -11,10 +11,10 @@ import { activeIndex } from "studio/style_section_nav"
|
|
|
11
11
|
|
|
12
12
|
const ACTIVE = "btn-primary"
|
|
13
13
|
const REST = "btn-neutral"
|
|
14
|
-
// A jump lands a section by its scroll
|
|
15
|
-
// header may still be tall; the header then collapses, so the row rises and the
|
|
16
|
-
//
|
|
17
|
-
//
|
|
14
|
+
// A jump lands a section by its scroll margin, the stack's pinned heights, read while the
|
|
15
|
+
// header may still be tall; the header then collapses, so the row rises and the section
|
|
16
|
+
// does not (76px on the hub at phone width). The slack keeps the clicked section past the
|
|
17
|
+
// line, for any later scroll or resize.
|
|
18
18
|
const LINE_SLACK = 80
|
|
19
19
|
|
|
20
20
|
export default class extends Controller {
|
|
@@ -37,6 +37,11 @@
|
|
|
37
37
|
// SIZE something as tall as the header; use --nav-bottom to START something
|
|
38
38
|
// underneath it. Same distinction for --pin-<name>-h vs --pin-<name>-bottom.
|
|
39
39
|
//
|
|
40
|
+
// THE SECTION LAYER (studio/_section_header, docs/PAGE_HEADER.md) IS THE ONE LAYER
|
|
41
|
+
// THAT IS NOT A data-pin. A page has many section headers and only one is stuck, so
|
|
42
|
+
// each carries `data-pin-section`, and together they publish as one layer, `section`.
|
|
43
|
+
// See stuckSection below.
|
|
44
|
+
//
|
|
40
45
|
// Started once per document by studio/application. The listeners it adds live
|
|
41
46
|
// for the document, and a Turbo visit republishes through them rather than
|
|
42
47
|
// starting it again.
|
|
@@ -152,6 +157,62 @@ export function documentIndex(list, el) {
|
|
|
152
157
|
return list.length;
|
|
153
158
|
}
|
|
154
159
|
|
|
160
|
+
// THE SECTION LAYER: MANY HEADERS, ONE LAYER, ONLY THE STUCK ONE COUNTS.
|
|
161
|
+
//
|
|
162
|
+
// A section header is sticky inside its own <section>, so the browser does the
|
|
163
|
+
// swap: the next section's header arrives as the last one is pushed out. It joins
|
|
164
|
+
// the stack so that a table's sticky header and any other reader of
|
|
165
|
+
// --pin-stack-bottom sit under it. It cannot be a plain [data-pin]: composed() takes
|
|
166
|
+
// the lowest edge of every layer, so a header waiting below the fold would drag
|
|
167
|
+
// --pin-stack-bottom down the page, and headers sharing a name would overwrite each
|
|
168
|
+
// other's values.
|
|
169
|
+
//
|
|
170
|
+
// So all of them are one layer named `section`, always in the registry while any is
|
|
171
|
+
// on the page. The roster never changes per swap, so a swap is not a structural write,
|
|
172
|
+
// and --pin-section-top, which every header positions off, stays published when none
|
|
173
|
+
// is stuck. That layer's height is 0 until one is stuck, and then it is the part of the
|
|
174
|
+
// bar still below the layers above, so a header being pushed out gives its room back
|
|
175
|
+
// frame by frame.
|
|
176
|
+
//
|
|
177
|
+
// rects are { top, bottom, parentTop, parentBottom }, in page order; line is
|
|
178
|
+
// --pin-section-top. A header is stuck once it has left its place in the flow and part
|
|
179
|
+
// of it still shows below the line. Its section's top is that place, since it is the
|
|
180
|
+
// section's first child and the section has no top padding.
|
|
181
|
+
//
|
|
182
|
+
// NEITHER FACT READS THE HEADER'S OWN EDGE AGAINST THE LINE. While the navbar collapses
|
|
183
|
+
// the line moves first and the header's `top` follows a publish later, so a comparison
|
|
184
|
+
// would drop the bar for the whole collapse, and a bottom read off the rect would be
|
|
185
|
+
// published one step stale, with nothing left to correct it once the page is still. So
|
|
186
|
+
// the bar's bottom is what sticky will make it: one height under the line, or the end of
|
|
187
|
+
// its section while that pushes it out.
|
|
188
|
+
export function sectionBottom(r, line) {
|
|
189
|
+
return Math.min(line + (r.bottom - r.top), r.parentBottom);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function stuckSection(rects, line) {
|
|
193
|
+
var found = -1;
|
|
194
|
+
for (var i = 0; i < rects.length; i++) {
|
|
195
|
+
var r = rects[i];
|
|
196
|
+
if (r.top - r.parentTop > 0.5 && sectionBottom(r, line) > line + 0.5) found = i;
|
|
197
|
+
}
|
|
198
|
+
return found;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function measureSections(nodes, line) {
|
|
202
|
+
// Below the short-screen floor, or before the publisher runs, the CSS leaves these in
|
|
203
|
+
// the flow (engine.css, @utility section-header). A header scrolling past the line
|
|
204
|
+
// there is not stuck, so it must not count.
|
|
205
|
+
if (window.getComputedStyle(nodes[0]).position !== 'sticky') return null;
|
|
206
|
+
var rects = [];
|
|
207
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
208
|
+
var r = nodes[i].getBoundingClientRect();
|
|
209
|
+
var parent = nodes[i].parentElement.getBoundingClientRect();
|
|
210
|
+
rects.push({ top: r.top, bottom: r.bottom, parentTop: parent.top, parentBottom: parent.bottom });
|
|
211
|
+
}
|
|
212
|
+
var at = stuckSection(rects, line);
|
|
213
|
+
return at < 0 ? null : { el: nodes[at], bottom: sectionBottom(rects[at], line) };
|
|
214
|
+
}
|
|
215
|
+
|
|
155
216
|
// THE REGISTRY IS DERIVED, NEVER CACHED — and that is a correctness rule.
|
|
156
217
|
//
|
|
157
218
|
// This used to build a `pins` array once and hold each element by reference
|
|
@@ -168,8 +229,9 @@ export function documentIndex(list, el) {
|
|
|
168
229
|
// reference IMPOSSIBLE rather than merely unlikely: there is no window between
|
|
169
230
|
// a DOM patch and a re-scan in which this can be pointed at the wrong node.
|
|
170
231
|
//
|
|
171
|
-
// Returns { pins, stack, timeline }: every layer measured and
|
|
172
|
-
// stack's bottom,
|
|
232
|
+
// Returns { pins, stack, timeline, travel, sections, stuck }: every layer measured and
|
|
233
|
+
// composed, the stack's bottom, whether the header is on the scroll timeline and how
|
|
234
|
+
// far it travels, the section headers, and which of them is stuck.
|
|
173
235
|
export function readStack() {
|
|
174
236
|
var header = document.querySelector('header');
|
|
175
237
|
var timeline = onTimeline(header);
|
|
@@ -191,11 +253,28 @@ export function readStack() {
|
|
|
191
253
|
// never publish at all. Both live consumers own their header, so it is
|
|
192
254
|
// adopted as a pin even when it does not ask to be.
|
|
193
255
|
if (header && !headerPinned) list.splice(documentIndex(list, header), 0, measurePin(header, 'nav', true, timeline));
|
|
256
|
+
// In the registry at height 0 first, so the composition says where its line is.
|
|
257
|
+
var sections = document.querySelectorAll('[data-pin-section]');
|
|
258
|
+
var section = null;
|
|
259
|
+
if (sections.length) {
|
|
260
|
+
section = { el: sections[0], name: 'section', legacy: false, h: 0, bottom: 0 };
|
|
261
|
+
list.splice(documentIndex(list, sections[0]), 0, section);
|
|
262
|
+
}
|
|
263
|
+
var stack = timeline ? pinnedAt(list) : composed(list);
|
|
264
|
+
var stuck = section ? measureSections(sections, section.top) : null;
|
|
265
|
+
if (stuck) {
|
|
266
|
+
section.el = stuck.el;
|
|
267
|
+
section.h = stuck.bottom - section.top;
|
|
268
|
+
section.bottom = stuck.bottom;
|
|
269
|
+
stack = timeline ? pinnedAt(list) : composed(list);
|
|
270
|
+
}
|
|
194
271
|
return {
|
|
195
272
|
pins: list,
|
|
196
|
-
stack:
|
|
273
|
+
stack: stack,
|
|
197
274
|
timeline: timeline,
|
|
198
|
-
travel: timeline ? timelineTravel(header) : 0
|
|
275
|
+
travel: timeline ? timelineTravel(header) : 0,
|
|
276
|
+
sections: sections,
|
|
277
|
+
stuck: stuck ? stuck.el : null
|
|
199
278
|
};
|
|
200
279
|
}
|
|
201
280
|
|
|
@@ -414,6 +493,9 @@ export function startPinnedStack() {
|
|
|
414
493
|
settleTimer = null;
|
|
415
494
|
publishAll();
|
|
416
495
|
releaseFollowers();
|
|
496
|
+
// That publish measured the layers while they still held their last copies. Given back,
|
|
497
|
+
// they move to the root's values, so their edges are read once more.
|
|
498
|
+
schedule();
|
|
417
499
|
}
|
|
418
500
|
|
|
419
501
|
// The registry and the measurement are module-level (readStack, measurePin
|
|
@@ -505,12 +587,38 @@ export function startPinnedStack() {
|
|
|
505
587
|
}
|
|
506
588
|
}
|
|
507
589
|
|
|
590
|
+
// WHO WEARS THE BAR, written after every read of the publish. The box does not change
|
|
591
|
+
// with the attribute, so this resizes nothing and moves nothing. The root's attribute
|
|
592
|
+
// squares the page row's corners against the bar under it (engine.css, glass).
|
|
593
|
+
function syncSections(reading) {
|
|
594
|
+
for (var i = 0; i < reading.sections.length; i++) {
|
|
595
|
+
var node = reading.sections[i];
|
|
596
|
+
var on = node === reading.stuck;
|
|
597
|
+
if (node.hasAttribute('data-stuck') !== on) node.toggleAttribute('data-stuck', on);
|
|
598
|
+
}
|
|
599
|
+
var root = document.documentElement;
|
|
600
|
+
if (root.hasAttribute('data-pin-section-stuck') !== !!reading.stuck) root.toggleAttribute('data-pin-section-stuck', !!reading.stuck);
|
|
601
|
+
}
|
|
602
|
+
|
|
508
603
|
var lastPublished = null;
|
|
604
|
+
var lastTops = null;
|
|
509
605
|
function publishAll() {
|
|
606
|
+
// What lets a section header stick at all (engine.css): with no publisher its bar
|
|
607
|
+
// would never come, and it would lay a clear title over the page. Before the read,
|
|
608
|
+
// so the first publish on a restored scroll already sees it sticky.
|
|
609
|
+
var html = document.documentElement;
|
|
610
|
+
if (!html.hasAttribute('data-pin-live')) html.setAttribute('data-pin-live', '');
|
|
510
611
|
var reading = readStack();
|
|
511
612
|
var pins = reading.pins;
|
|
512
613
|
write(pins, reading.stack, reading.timeline, reading.travel);
|
|
614
|
+
syncSections(reading);
|
|
513
615
|
syncObserver(pins);
|
|
616
|
+
// A LAYER UNDER A LAYER SETTLES ONE PUBLISH LATER. A new --pin-page-top moves the page
|
|
617
|
+
// row without resizing it, so nothing wakes the publisher to read the row's new edge,
|
|
618
|
+
// and the layer under the row would keep a stale line once the page is still. So a
|
|
619
|
+
// publish that moved any layer's top asks for one more frame. It ends when none moved.
|
|
620
|
+
var tops = pins.map(function (p) { return p.top; }).join('|');
|
|
621
|
+
if (tops !== lastTops) { lastTops = tops; schedule(); }
|
|
514
622
|
var key = publishedKey(reading);
|
|
515
623
|
if (key !== lastPublished) {
|
|
516
624
|
lastPublished = key;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<%#
|
|
2
|
+
Fizz around any button: bubbles that rise from behind it. Render it as a layout,
|
|
3
|
+
with the button as the block.
|
|
4
|
+
|
|
5
|
+
render layout: "studio/fizz", locals: { seed: "save" } do
|
|
6
|
+
<button class="btn btn-primary">Save</button>
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
Locals:
|
|
10
|
+
seed - the string the bubble table is seeded from, so the scatter is stable
|
|
11
|
+
for this button and differs from its neighbours
|
|
12
|
+
level - :lively (default), which boils at rest and adds a layer on hover, or
|
|
13
|
+
:calm, which simmers at rest and boils on hover
|
|
14
|
+
colors - optional palette, up to 18, three per zone (--fizz-c-1..18)
|
|
15
|
+
|
|
16
|
+
Decoration only. The hold button keeps its own stack (studio/_hold_button).
|
|
17
|
+
%>
|
|
18
|
+
<% lively = local_assigns.fetch(:level, :lively).to_s == "lively" %>
|
|
19
|
+
<% fizz_style = fizz_stack_style(local_assigns[:colors]) %>
|
|
20
|
+
<span class="fizz-stack<%= " fizz-lively" if lively %>"<% if fizz_style.present? %> style="<%= fizz_style %>"<% end %>>
|
|
21
|
+
<%= render "studio/fizz_layer", seed: seed, neutral: true %>
|
|
22
|
+
<% if lively %>
|
|
23
|
+
<%= render "studio/fizz_layer", seed: "#{seed}~extra", extra: true, neutral: true %>
|
|
24
|
+
<% end %>
|
|
25
|
+
<%= yield %>
|
|
26
|
+
</span>
|
|
@@ -7,10 +7,13 @@
|
|
|
7
7
|
in each other's gaps rather than on top of each other)
|
|
8
8
|
extra - true for the hover-only layer, which rests invisible and fades in
|
|
9
9
|
when the pointer arrives (default: false)
|
|
10
|
+
neutral - true inside a .fizz-stack (studio/_fizz), a button with no hold, so
|
|
11
|
+
the layer carries the neutral class names (default: false)
|
|
10
12
|
|
|
11
13
|
The two layers also split each zone's three colour slots: the resting one
|
|
12
14
|
takes the first, the hover one alternates the second and third.
|
|
13
15
|
Decoration only: aria-hidden here, pointer-events off in CSS.
|
|
14
16
|
%>
|
|
15
17
|
<% extra ||= false %>
|
|
16
|
-
|
|
18
|
+
<% neutral ||= false %>
|
|
19
|
+
<span class="<%= neutral ? "fizz-layer" : "hold-fizz" %><%= (neutral ? " fizz-layer-extra" : " hold-fizz-extra") if extra %>" aria-hidden="true"><% fizz_bits(seed, layer: extra ? :hover : :base).each do |bit| %><i class="fizz-bit" style="<%= fizz_bit_style(bit) %>"></i><% end %></span>
|
|
@@ -74,8 +74,7 @@
|
|
|
74
74
|
<% fizz_portal = fizz && local_assigns[:fizz_portal] %>
|
|
75
75
|
<%# Static palette (gallery, or any call site with fixed colors). A runtime
|
|
76
76
|
palette rides fizz_bind instead. %>
|
|
77
|
-
<% fizz_style =
|
|
78
|
-
.each_with_index.map { |c, i| "--fizz-c-#{i + 1}:#{c}" }.join(";") %>
|
|
77
|
+
<% fizz_style = fizz_stack_style(fizz_colors) %>
|
|
79
78
|
|
|
80
79
|
<%# The stack exists so the fizz can sit BEHIND the button. Inside the button
|
|
81
80
|
a negative z-index still paints above its own background (the button's
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<%# locals: (title:, meta: nil, meta_title: nil, label: nil, tabs: [], tabs_data: {}, actions_html: nil, data: {}, row_class: "-mx-4 px-4") %>
|
|
2
|
-
<%# THE PAGE HEADER: the second layer of the
|
|
3
|
-
navbar, then this row, then a
|
|
2
|
+
<%# THE PAGE HEADER: the second layer of the pinned stack (docs/PAGE_HEADER.md).
|
|
3
|
+
navbar, then this row, then a section's header (studio/_section_header), then a
|
|
4
|
+
table's sticky header, each below the last.
|
|
4
5
|
|
|
5
6
|
AT REST it has no background and no border: a title on the left, tabs on the
|
|
6
7
|
right. ONCE STUCK under the navbar it takes the bar. Its box never changes
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<%# locals: (title:, level: :h2, row_class: "-mx-4 px-4") %>
|
|
2
|
+
<%# THE SECTION HEADER: a section's title that sticks under the rest of the pinned stack
|
|
3
|
+
until the next section's header takes its place (docs/PAGE_HEADER.md).
|
|
4
|
+
|
|
5
|
+
RENDER IT AS THE FIRST DIRECT CHILD OF ITS <section>. A sticky element travels only
|
|
6
|
+
inside its parent, and that is what gives the swap: inside a wrapper div, it would
|
|
7
|
+
leave with the wrapper. No backdrop-filter, transform or overflow on an ancestor.
|
|
8
|
+
|
|
9
|
+
title the heading's text. The heading is the sticky element, so an intro can
|
|
10
|
+
follow as its next sibling.
|
|
11
|
+
level the tag. A page should stick one level only: one bar at a time.
|
|
12
|
+
row_class the bleed. The default fits a container with px-4, as the page row's does.
|
|
13
|
+
|
|
14
|
+
AT REST a normal title. STUCK it takes the bar. Its box never changes between the two.
|
|
15
|
+
|
|
16
|
+
IT JOINS THE STACK WITH data-pin-section, NOT data-pin. Every header on the page shares
|
|
17
|
+
one layer, `section`, and only the stuck one counts (studio/pinned_stack, stuckSection).
|
|
18
|
+
So it positions off --pin-section-top, which also stands when none is stuck. Anything
|
|
19
|
+
under the stack, such as a table's sticky header, sits under it with no edit. %>
|
|
20
|
+
<%= content_tag(level, title,
|
|
21
|
+
class: "section-header #{row_class}".strip,
|
|
22
|
+
style: "top: var(--pin-section-top, var(--nav-bottom, var(--nav-h, 0px))); z-index: calc(var(--z-sticky, 30) - 1)",
|
|
23
|
+
data: { pin_section: "" }) %>
|
|
@@ -5,11 +5,9 @@
|
|
|
5
5
|
An app that does not draw the gallery here (lookbook not in its bundle, or
|
|
6
6
|
production without Studio.lookbook_in_production) says so instead of linking
|
|
7
7
|
to a 404. -%>
|
|
8
|
-
<section id="components" class="space-y-6"
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
<p class="text-muted text-sm max-w-2xl">One preview per state, in the component gallery.</p>
|
|
12
|
-
</div>
|
|
8
|
+
<section id="components" class="section-target space-y-6">
|
|
9
|
+
<%= render "studio/section_header", title: "Components" %>
|
|
10
|
+
<p class="text-muted text-sm max-w-2xl">One preview per state, in the component gallery.</p>
|
|
13
11
|
|
|
14
12
|
<% if Studio.lookbook_mounted? %>
|
|
15
13
|
<ul class="grid gap-4 sm:grid-cols-2" data-component-previews>
|
|
@@ -90,12 +90,9 @@
|
|
|
90
90
|
A reader who assumes dsModals will build a mirror instead: a second copy of
|
|
91
91
|
every card, free to drift from the one the app actually ships, which is the
|
|
92
92
|
opposite of what a living style guide is for. %>
|
|
93
|
-
<section id="host-modals" class="space-y-8" x-data="{}"
|
|
94
|
-
|
|
95
|
-
<
|
|
96
|
-
<h2 class="type-h3 text-heading"><%= Studio.app_name %></h2>
|
|
97
|
-
<p class="text-muted text-sm max-w-2xl">This app’s own modals, from <code class="font-mono text-2xs">app/views/style/host/_modals.html.erb</code>.</p>
|
|
98
|
-
</div>
|
|
93
|
+
<section id="host-modals" class="section-target space-y-8" x-data="{}">
|
|
94
|
+
<%= render "studio/section_header", title: Studio.app_name %>
|
|
95
|
+
<p class="text-muted text-sm max-w-2xl">This app’s own modals, from <code class="font-mono text-2xs">app/views/style/host/_modals.html.erb</code>.</p>
|
|
99
96
|
|
|
100
97
|
<%# The app's content. Reached only when it resolves - style/index.html.erb
|
|
101
98
|
owns that guard, because the section nav needs the same answer. %>
|
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
page when chatting (item 1).
|
|
10
10
|
- an optional row of option toggles lets the user configure the modal
|
|
11
11
|
before opening; their clicks/keys do NOT bubble to the card open (item 2).
|
|
12
|
-
- an optional active-card GLOW (.
|
|
12
|
+
- an optional active-card GLOW (.edge-rotate-glow) that reactively lights
|
|
13
13
|
when this card represents the currently-open modal + step, and slides to
|
|
14
14
|
the next card as the step machine advances (items 5/6). The glow rides an
|
|
15
15
|
un-clipped wrapper (the card itself is overflow-hidden), mirroring TM's
|
|
16
|
-
holo-wrap / holo-card split, and fades via --
|
|
16
|
+
holo-wrap / holo-card split, and fades via --edge-rotate-glow-opacity so
|
|
17
17
|
it transitions smoothly between cards.
|
|
18
18
|
|
|
19
19
|
Locals:
|
|
@@ -53,10 +53,10 @@
|
|
|
53
53
|
<%# Wrapper — carries the active-card glow (un-clipped) + the shared x-data. %>
|
|
54
54
|
<%# h-full so the card stretches to its grid row's tallest (per-row equal
|
|
55
55
|
height via the grid's default align-items: stretch). %>
|
|
56
|
-
<div class="relative h-full<%= "
|
|
56
|
+
<div class="relative h-full<%= " edge-rotate-glow rounded-xl" if glow_when %>"
|
|
57
57
|
x-data="<%= card_x_data %>"
|
|
58
58
|
<%# Fail-closed default: a STATIC inline 0 out-specifies the primitive's CSS
|
|
59
|
-
default (--
|
|
59
|
+
default (--edge-rotate-glow-opacity of 1) so the ring is OFF at first
|
|
60
60
|
paint, BEFORE Alpine hydrates the reactive :style below. Without it every
|
|
61
61
|
glow card shows its ring until Alpine boots (the fail-open flash). The
|
|
62
62
|
:style then drives 0 to 0.95 with the 0.4s cross-fade. Keep BOTH.
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
the Turbo-nav store gap fixed in 0.26.1 (style/_modals dual-guards the
|
|
68
68
|
dsModals registration so it survives Turbo visits, not just first load).
|
|
69
69
|
So the guarantee is: static inline 0 PLUS a registered dsModals. %>
|
|
70
|
-
<% if glow_when %>style="--
|
|
70
|
+
<% if glow_when %>style="--edge-rotate-glow-opacity: 0" :style="{ '--edge-rotate-glow-opacity': (<%= glow_when %>) ? '0.95' : '0' }"<% end %>>
|
|
71
71
|
<article
|
|
72
72
|
class="card overflow-hidden flex flex-col h-full<%= " opacity-95" if disabled %><%= " cursor-pointer" if clickable %>"
|
|
73
73
|
<%= 'aria-disabled="true"'.html_safe if disabled %>
|
|
@@ -150,14 +150,12 @@
|
|
|
150
150
|
target moved. The engine primitive is untouched; its real submit still posts
|
|
151
151
|
to the app-supplied submit_url. Kept on the section (always mounted) so it
|
|
152
152
|
fires even as the age-gate card closes. %>
|
|
153
|
-
<section id="modals" class="space-y-10"
|
|
153
|
+
<section id="modals" class="section-target space-y-10"
|
|
154
154
|
data-studio-controller="style-modals"
|
|
155
155
|
x-data="{}" @ds-modal-close.window="$store.dsModals.close()"
|
|
156
156
|
@age-verified.window="$store.dsModals.open('entry-confirmed', { txSignature: null, lobbyUrl: null, seedsEarned: 0 })">
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
<p class="text-muted text-sm">Click a card to open the real modal.</p>
|
|
160
|
-
</div>
|
|
157
|
+
<%= render "studio/section_header", title: "Modals" %>
|
|
158
|
+
<p class="text-muted text-sm">Click a card to open the real modal.</p>
|
|
161
159
|
|
|
162
160
|
<%# ---- The section's own modal host (dsModals) --------------------------
|
|
163
161
|
$store.dsModals is the engine's modal stack under the guide's own name:
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
sample_page = [ [ "Google's header", 0, 200, false ], [ "Select an appointment time", 200, 600, true ],
|
|
34
34
|
[ "Google's credit lines", 600, 720, false ] ]
|
|
35
35
|
%>
|
|
36
|
-
<section id="site-footer" class="space-y-5"
|
|
36
|
+
<section id="site-footer" class="section-subtarget space-y-5">
|
|
37
37
|
<div class="space-y-1">
|
|
38
38
|
<h3 class="type-h4 text-heading">Site footer and booking</h3>
|
|
39
39
|
<p class="text-muted text-sm max-w-2xl">One layout line renders the footer, and <code class="font-mono text-2xs">config.booking_url</code> turns on booking.</p>
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
klass (String, required) - the class label, shown prominently with a
|
|
7
7
|
copy button so an agent can grab the exact class list.
|
|
8
8
|
usage (String, optional) - a copyable usage snippet.
|
|
9
|
+
note (String, optional) - one short sentence on what it does.
|
|
9
10
|
disabled (Boolean, optional) - render "disabled but present": the
|
|
10
11
|
primitive stays visible + previewable but is muted/greyed
|
|
11
12
|
and flagged, for a capability the host app has turned off
|
|
@@ -38,6 +39,9 @@
|
|
|
38
39
|
<% end %>
|
|
39
40
|
</div>
|
|
40
41
|
<div class="border-t border-subtle p-3 space-y-2">
|
|
42
|
+
<% if local_assigns[:note].present? %>
|
|
43
|
+
<p class="text-sm text-body"><%= local_assigns[:note] %></p>
|
|
44
|
+
<% end %>
|
|
41
45
|
<%# Class name — prominent, with a one-click copy so an agent can grab it. %>
|
|
42
46
|
<div class="flex items-start justify-between gap-2" x-data="{ copied: false }">
|
|
43
47
|
<code x-ref="klass" class="font-mono text-xs font-bold text-heading break-all leading-relaxed flex-1 min-w-0"><%= local_assigns[:klass] %></code>
|
|
@@ -63,11 +63,9 @@
|
|
|
63
63
|
"stage-shipped" => [{ title: "SES email backend", repo: "mcritchie", who: "ST" }]
|
|
64
64
|
}
|
|
65
65
|
%>
|
|
66
|
-
<section id="tasks" class="space-y-8"
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
<p class="text-muted text-sm max-w-2xl">The real board primitive in demo mode, so a drag saves nothing.</p>
|
|
70
|
-
</div>
|
|
66
|
+
<section id="tasks" class="section-target space-y-8">
|
|
67
|
+
<%= render "studio/section_header", title: "Tasks" %>
|
|
68
|
+
<p class="text-muted text-sm max-w-2xl">The real board primitive in demo mode, so a drag saves nothing.</p>
|
|
71
69
|
|
|
72
70
|
<%# ---- The LIVE primitive (demo mode) --------------------------------- %>
|
|
73
71
|
<%# demo: true → drag animates + toasts locally, nothing POSTs. %>
|
|
@@ -53,11 +53,11 @@
|
|
|
53
53
|
|
|
54
54
|
csrf_ok = respond_to?(:protect_against_forgery?) && protect_against_forgery?
|
|
55
55
|
%>
|
|
56
|
-
<section id="theme" class="space-y-8"
|
|
56
|
+
<section id="theme" class="section-target space-y-8"
|
|
57
57
|
x-data="dsThemeEditor(<%= seed_colors.to_json %>)">
|
|
58
|
+
<%= render "studio/section_header", title: "Theme" %>
|
|
58
59
|
<div class="flex flex-wrap items-start justify-between gap-3">
|
|
59
60
|
<div class="space-y-1">
|
|
60
|
-
<h2 class="type-h3 text-heading">Theme</h2>
|
|
61
61
|
<p class="text-muted text-sm max-w-2xl">Every specimen on this page restyles from these 7 color roles.</p>
|
|
62
62
|
</div>
|
|
63
63
|
<form action="/admin/theme/regenerate" method="post" accept-charset="UTF-8" @submit.prevent="regenerate($event)">
|