studio-engine 0.54.0 → 0.54.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3419d6ec43e811060b90f6e628feabc249bb6790f9b911c258512ca2fa13f87e
4
- data.tar.gz: e91ce80dcd5465ce9660091cb7570327322e1a9aff79d115f4ba0116a7da4a38
3
+ metadata.gz: b79896d29716fe1408c39d86c9e2d19d4e6dc12dd28749ff85054c29e5ba9a77
4
+ data.tar.gz: 033a9a7e0e4799ef5ed0522e3fb6b6d1652ab4239d7de625add1e24ac81a5386
5
5
  SHA512:
6
- metadata.gz: 308d06c9339512b02da78a7c5877134a91b38f20cb89f6e4692b1f734839250a03d15fd1cdea463255ffe49b2e7a675d8f95624cbaf1e4f275ae4ddc83e7b480
7
- data.tar.gz: f920799c11d5fdb4ad8757e4afedea52592dcce1d103e7a53f5e666cf62c386994fc9d2d0bb284ea3530840291c5e375fb2f5d279e537b4253c24a60838319e8
6
+ metadata.gz: dcd13075e85ee744c86ec901f62e749320f916a72c7c04b1530a5715dcdbcec3adcaa39dc1b8236327bc5a2de4238ef1585cf42853444ff92eeae0ff7cfcc702
7
+ data.tar.gz: 6d242c947ec1d582202bb53652091f6ab4ed111cadfa721aab578df1f89f7b8b9a9bdb4bcd9f57b5b8a15ba9d8c9200b2358668ec2058d59f0c210072e93a79a
data/CHANGELOG.md CHANGED
@@ -4,6 +4,41 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ### Fixed
8
+
9
+ - **`.conic-surface` keeps its wash inside its own box.** The wash was an
10
+ OVERSIZED pseudo (`inset: -50%`) spun by a `transform` keyframe, cut back to
11
+ the element by the host's `overflow: hidden`. That clip is not dependable: the
12
+ rotating child is composited, and a `backdrop-filter` in its compositing path
13
+ drops it. The `.surface-glass` specimen — a backdrop-filter child of the wash
14
+ itself — is exactly that path, and on admin/style in Chromium the wash escaped
15
+ as a rotated slab across BOTH conic specimens.
16
+
17
+ The element no longer moves. The pseudo sits at `inset: 0` with
18
+ `border-radius: inherit`, and the sweep animates a registered
19
+ `--conic-surface-angle` inside the gradient — the technique §9b already used.
20
+ Nothing extends past the box, the rounded corners come from the pseudo, and no
21
+ clip has to hold. Same picture, one fewer thing that can fail.
22
+ `overflow: hidden` stays for the host's CONTENT.
23
+
24
+ The trade, stated: a registered-property animation repaints on the main thread
25
+ where a transform rode the compositor. At the default 16s sweep that is cheap,
26
+ and §9b already pays it at 4s.
27
+
28
+ - **`.studio-team-glow` is demoed and documented as the wrapper it is.** Its two
29
+ wedge layers are pseudos at z-index -1/-2, and CSS paints an element's own
30
+ background BEFORE its negative-z descendants — so a background on the glow HOST
31
+ is covered BY the wedges, not the other way round, and the ring becomes a wash
32
+ across the whole card face. What covers the middle is an opaque CHILD, which
33
+ paints later; that is TM's `holo-wrap` / `holo-card` split, and what both real
34
+ consumers already ship.
35
+
36
+ The §9b comment claimed the opposite ("host needs an opaque background"), and
37
+ the Effects specimen followed it, so the style guide showed two washed slabs
38
+ where the selection ring belongs. The specimen, its copyable snippet, and the
39
+ contract comment now all state the same thing. The primitive's geometry is
40
+ unchanged — the consumers tuned against it are untouched.
41
+
7
42
  ### Added
8
43
 
9
44
  - **`studio/fields/_date_of_birth`** — the engine's one date-of-birth field,
@@ -521,10 +521,22 @@
521
521
  transition, so a consumer can fade the glow in/out (the living style guide
522
522
  slides it between step cards as a modal's step machine advances).
523
523
 
524
- Host needs an opaque background (the ring shows only around the edge) and must
525
- NOT be clipped by an overflow-hidden ancestor near its border.
526
-
527
- Usage: <div class="studio-team-glow rounded-xl bg-surface p-5">Selected</div>
524
+ THE HOST IS A BARE WRAPPER, NOT THE CARD. Both wedge layers are pseudo-elements
525
+ at z-index -1 / -2, and CSS paints an element's own background FIRST, then its
526
+ negative-z descendants on top of it (CSS 2.1 Appendix E, steps 1 and 2). So a
527
+ background on the HOST cannot cover the wedges — they wash its whole face, and
528
+ the ring is gone. What covers the middle is an opaque CHILD in front: in-flow
529
+ descendants paint later (step 3), leaving only the thickness-wide band around
530
+ the edge. That is TM's holo-wrap / holo-card split, and it is what both
531
+ consumers ship — the engine's style/_modal_specimen (bare glow wrapper + the
532
+ opaque .card article) and the hub's release-phase-meter (bare host + the bar's
533
+ own fill). Demoing it the other way is what shipped a washed card to
534
+ admin/style; the host also must NOT be clipped by an overflow-hidden ancestor
535
+ near its border.
536
+
537
+ Usage: <div class="studio-team-glow rounded-xl">
538
+ <div class="rounded-xl bg-surface p-5">Selected</div>
539
+ </div>
528
540
  red glow: style="--studio-team-glow-color: #E11D48"
529
541
  Tune: --studio-team-glow-color / -opacity / -speed / -thickness / -bloom
530
542
  ========================================================================== */
@@ -604,12 +616,36 @@
604
616
  11. conic-surface — slow rotating conic-gradient wash (authored fresh)
605
617
  A subtle animated ambient background for hero panels / empty states: a conic
606
618
  gradient sweeps between two themed stops (CTA -> surface by default) on a slow
607
- rotation. The gradient lives on a ::before that rotates, so content kept above
608
- it stays crisp. The element's radius clips the wash; give it one.
619
+ rotation. The gradient lives on a ::before behind the content, so content kept
620
+ above it stays crisp.
621
+
622
+ THE WASH IS CONTAINED BY CONSTRUCTION, NOT BY A CLIP. The first cut rotated an
623
+ OVERSIZED child (`inset: -50%` + `transform: rotate`) and leaned on the host's
624
+ `overflow: hidden` to cut it back to the box. That clip is not dependable: the
625
+ rotating child is composited, and a `backdrop-filter` anywhere in its
626
+ compositing path (its OWN .surface-glass child on the style guide, or an
627
+ unrelated overlay elsewhere on the page) drops the clip — the wash then paints
628
+ outside the box as a rotated slab. Observed on admin/style in Chromium, on both
629
+ conic specimens, with the .surface-glass demo the reliable trigger.
630
+
631
+ So the element never moves. The ::before sits at `inset: 0` with
632
+ `border-radius: inherit`, and the sweep animates a registered @property angle
633
+ inside the gradient — the same technique §9b uses. Nothing extends past the
634
+ box, the rounded corners come from the pseudo itself, and no clip has to hold.
635
+ `overflow: hidden` stays for the host's CONTENT, not for the wash.
636
+
637
+ The cost of the trade, stated: a registered-property animation repaints the
638
+ gradient on the main thread each frame, where a transform rode the compositor.
639
+ At the default 16s sweep that is cheap, and §9b already pays it at 4s.
609
640
 
610
641
  Usage: <div class="conic-surface rounded-2xl p-8"> ...content... </div>
611
642
  Tune: --conic-surface-from / --conic-surface-to / --conic-surface-duration
612
643
  ========================================================================== */
644
+ @property --conic-surface-angle {
645
+ syntax: "<angle>";
646
+ inherits: false;
647
+ initial-value: 0deg;
648
+ }
613
649
  .conic-surface {
614
650
  position: relative;
615
651
  isolation: isolate;
@@ -619,10 +655,11 @@
619
655
  .conic-surface::before {
620
656
  content: "";
621
657
  position: absolute;
622
- inset: -50%;
658
+ inset: 0;
659
+ border-radius: inherit;
623
660
  z-index: -1;
624
661
  background: conic-gradient(
625
- from 0deg,
662
+ from var(--conic-surface-angle),
626
663
  var(--conic-surface-from, var(--color-cta)),
627
664
  var(--conic-surface-to, var(--color-surface)),
628
665
  var(--conic-surface-from, var(--color-cta))
@@ -631,7 +668,7 @@
631
668
  }
632
669
 
633
670
  @keyframes conic-surface-spin {
634
- to { transform: rotate(360deg); }
671
+ to { --conic-surface-angle: 360deg; }
635
672
  }
636
673
 
637
674
  /* =============================================================================
@@ -27,8 +27,11 @@
27
27
  JS — the three selects. `alpine` comes from the enclosing
28
28
  studioProfileForm and is true only once that component has booted.
29
29
  %>
30
- <label class="block text-sm text-secondary mb-2 font-medium" for="profile_birthday">Date of birth</label>
31
-
30
+ <%# NO FIELD LABEL HERE, deliberately (operator's call, 2026-08-15). The row is
31
+ already titled "Birthday" by the section wrapper, and the three selects carry
32
+ their own MONTH / DAY / YEAR labels — a "Date of birth" line between the two
33
+ said the same thing a third time. The no-JS <input type="date"> keeps its id
34
+ so a label elsewhere could still point at it. %>
32
35
  <%
33
36
  y = user.birth_year
34
37
  m = user.birth_month
@@ -48,8 +51,27 @@
48
51
  <template x-if="alpine">
49
52
  <div>
50
53
  <%# The value the form actually submits. The selects write here; nothing
51
- types into it. %>
52
- <input type="hidden" name="profile[birthday]" :value="isoValue">
54
+ types into it.
55
+
56
+ THE NAME IS BOUND, AND THAT IS THE WHOLE POINT. A partly-filled date
57
+ has no ISO value, and submitting a BLANK birthday is not "no change" to
58
+ this controller — it is DELETE, and it clears all three columns on
59
+ purpose (see #birthday_attributes: someone deleting their birthday
60
+ means it). Those two facts met in a data-loss bug: a stored Jan 31,
61
+ switch the month to February, onMonthChange blanks the 31st because
62
+ February has no 31st, and the form is now one Save away from wiping a
63
+ birthday the person never asked to delete.
64
+
65
+ On THIS path a blank is always an edit in progress and never an
66
+ intentional clear, because the selects offer no empty option — once a
67
+ part is set the user cannot unset it. The intentional clear lives on
68
+ the no-JS <input type="date"> above, where it is a real gesture.
69
+
70
+ So while the date is incomplete the field submits NOTHING: Alpine
71
+ removes an attribute bound to false, an input with no name is not
72
+ submitted, and the controller's `return {} if raw.nil?` makes an absent
73
+ param a no-op. The birthday is left exactly as it was. %>
74
+ <input type="hidden" :name="complete ? 'profile[birthday]' : false" :value="isoValue">
53
75
 
54
76
  <%= render "studio/fields/date_of_birth" %>
55
77
  </div>
@@ -82,12 +82,64 @@
82
82
  this.publish();
83
83
  },
84
84
 
85
- // `fields` belongs to the enclosing studioProfileForm. Alpine's scope chain
86
- // makes it reachable and it is the SAME object, so assigning through it is
87
- // what raises the save bar. Without this the field would set a value the
88
- // dirty check never notices.
85
+ // WHAT THE DIRTY CHECK IS SHOWN, and why it is not isoValue.
86
+ //
87
+ // isoValue is "" for EVERY incomplete date, so publishing it made a whole
88
+ // class of edit invisible: on an empty birthday, choosing a month wrote ""
89
+ // over "" and the save bar never appeared. The operator reported exactly
90
+ // that. Choosing a month IS a change — the form just had no way to see it.
91
+ //
92
+ // So the signal is the three parts, which differ whenever any one of them
93
+ // does. It stays equal to isoValue for a COMPLETE date (same string, same
94
+ // shape) so a finished edit compares correctly against the value the page
95
+ // loaded with, and Discard restores it to that same string.
96
+ //
97
+ // `fields` belongs to the enclosing studioProfileForm — Alpine's scope
98
+ // chain makes it reachable and it is the SAME object, so assigning through
99
+ // it is what raises the save bar.
100
+ // THE EMPTY CASE IS NOT A PARTIAL ONE, and conflating them makes the page
101
+ // arrive already dirty. studioProfileForm's initial.birthday is "" for a
102
+ // person with no birthday on file, so an untouched empty field must also
103
+ // read "" — joining three blanks gives "--", which compares as a change and
104
+ // would raise the save bar on load, before anyone touched anything.
105
+ get signature() {
106
+ if (this.complete) return this.isoValue;
107
+ if (!this.year && !this.month && !this.day) return "";
108
+ return [this.year, this.month, this.day].join("-");
109
+ },
110
+
89
111
  publish() {
90
- if (this.fields) this.fields.birthday = this.isoValue;
112
+ if (this.fields) this.fields.birthday = this.signature;
113
+ },
114
+
115
+ // DISCARD MUST REACH THE SELECTS. studioProfileForm#discard reassigns its
116
+ // `fields` object, which restores every input bound with x-model — and the
117
+ // three selects are not, because they are bound to THIS component's own
118
+ // month/day/year. So Discard used to leave them showing the abandoned edit
119
+ // while the bar disappeared, and the hidden input went on carrying it: the
120
+ // next save would write the change the user had just discarded.
121
+ //
122
+ // Watching `fields.birthday` is what joins them. Discard sets it back to
123
+ // the initial string, this fires, and the selects follow — and because the
124
+ // guard below returns early when the value already matches, the publish →
125
+ // watch → publish path cannot loop.
126
+ init() {
127
+ var self = this;
128
+ this.publish();
129
+ this.$watch("fields.birthday", function (value) {
130
+ if (value === self.signature) return;
131
+ self.adopt(value);
132
+ });
133
+ },
134
+
135
+ // Takes an ISO date (or a blank) and puts the selects on it. Anything that
136
+ // is not a full date empties all three, which is what an initial blank and
137
+ // a cleared birthday both mean.
138
+ adopt(value) {
139
+ var m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(value || ""));
140
+ this.year = m ? String(+m[1]) : "";
141
+ this.month = m ? String(+m[2]) : "";
142
+ this.day = m ? String(+m[3]) : "";
91
143
  }
92
144
  };
93
145
  };
@@ -297,12 +297,22 @@
297
297
  </div>
298
298
  <% end %>
299
299
 
300
+ <%# The glow host is a BARE wrapper and the opaque face is a CHILD. The ring
301
+ pseudos sit at z-index -1/-2, and a negative-z descendant paints ABOVE its
302
+ host's own background — so a background on the HOST washes the whole card
303
+ face and there is no ring left. The child in front is what covers the
304
+ middle, exactly as TM's holo-wrap / holo-card split does. See
305
+ engine-motion.css §9b. %>
300
306
  <%= render layout: "style/specimen",
301
307
  locals: { klass: ".studio-team-glow (knob: --studio-team-glow-color)",
302
- usage: %(<div class="studio-team-glow rounded-xl bg-surface p-5">Selected</div>\n<div class="studio-team-glow rounded-xl bg-surface p-5" style="--studio-team-glow-color: #E11D48">Ravens</div>) } do %>
308
+ usage: %(<div class="studio-team-glow rounded-xl">\n <div class="rounded-xl bg-surface p-5">Selected</div>\n</div>) } do %>
303
309
  <div class="flex flex-wrap items-center justify-center gap-6">
304
- <div class="studio-team-glow rounded-xl bg-surface border border-subtle px-5 py-4 text-sm font-semibold text-heading text-center">Selected</div>
305
- <div class="studio-team-glow rounded-xl bg-surface border border-subtle px-5 py-4 text-sm font-semibold text-heading text-center" style="--studio-team-glow-color: #E11D48">Ravens</div>
310
+ <div class="studio-team-glow rounded-xl">
311
+ <div class="rounded-xl bg-surface border border-subtle px-5 py-4 text-sm font-semibold text-heading text-center">Selected</div>
312
+ </div>
313
+ <div class="studio-team-glow rounded-xl" style="--studio-team-glow-color: #E11D48">
314
+ <div class="rounded-xl bg-surface border border-subtle px-5 py-4 text-sm font-semibold text-heading text-center">Ravens</div>
315
+ </div>
306
316
  </div>
307
317
  <% end %>
308
318
 
@@ -1,3 +1,3 @@
1
1
  module Studio
2
- VERSION = "0.54.0"
2
+ VERSION = "0.54.1"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: studio-engine
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.54.0
4
+ version: 0.54.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex McRitchie