studio-engine 0.97.0 → 0.98.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.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +89 -0
  3. data/app/javascript/studio/alpine_scopes.js +66 -0
  4. data/app/javascript/studio/alpine_shims.js +4 -0
  5. data/app/javascript/studio/application.js +4 -1
  6. data/app/javascript/studio/birthday.js +351 -0
  7. data/app/javascript/studio/confirm_interstitial.js +44 -0
  8. data/app/javascript/studio/controllers/identity_mini_controller.js +32 -0
  9. data/app/javascript/studio/controllers/style_modals_controller.js +41 -0
  10. data/app/javascript/studio/cropper.js +281 -0
  11. data/app/javascript/studio/identity_mini.js +74 -0
  12. data/app/javascript/studio/image_upload.js +189 -0
  13. data/app/javascript/studio/profile_form.js +88 -0
  14. data/app/javascript/studio/stimulus.js +4 -0
  15. data/app/javascript/studio/style_guide.js +13 -0
  16. data/app/javascript/studio/style_modals.js +214 -0
  17. data/app/views/components/_avatar_cropper.html.erb +3 -38
  18. data/app/views/layouts/studio/_head.html.erb +12 -2
  19. data/app/views/studio/_alpine_scopes_guard.html.erb +178 -0
  20. data/app/views/studio/_birthday_assets.html.erb +6 -240
  21. data/app/views/studio/_confirm_interstitial.html.erb +31 -18
  22. data/app/views/studio/_cropper_assets.html.erb +9 -8
  23. data/app/views/studio/modals/_crop_photo.html.erb +1 -1
  24. data/app/views/studio/modals/_image_upload.html.erb +8 -321
  25. data/app/views/studio/modals/blocks/_birthday.html.erb +2 -3
  26. data/app/views/studio/profiles/_birthday_fields.html.erb +0 -1
  27. data/app/views/studio/profiles/_identity_mini.html.erb +5 -62
  28. data/app/views/studio/profiles/_plain_save.html.erb +11 -0
  29. data/app/views/studio/profiles/edit.html.erb +1 -6
  30. data/app/views/style/_modals.html.erb +36 -420
  31. data/lib/studio/version.rb +1 -1
  32. metadata +14 -3
  33. data/app/views/studio/profiles/_birthday_picker_script.html.erb +0 -146
  34. data/app/views/studio/profiles/_form_script.html.erb +0 -65
@@ -1,146 +0,0 @@
1
- <%# The birthday field's Alpine component.
2
-
3
- Supplies exactly what studio/fields/_date_of_birth binds to — month, day,
4
- year, months, dayOptions, years, onMonthChange() — plus the one thing the
5
- profile page needs on top: it writes the assembled ISO date into the
6
- enclosing studioProfileForm's `fields.birthday`, which is what raises the
7
- save bar.
8
-
9
- THIS REPLACED A CALENDAR POPOVER, and the replacement is the point. That
10
- component shipped four defects in three days:
11
-
12
- · its day grid used `grid-cols-7`, which exists in NO consumer bundle, so
13
- the seven weekday letters stacked into one column and the popover grew to
14
- the height of the page;
15
- · a flipped popover positioned itself from a hardcoded 340px height when
16
- the real popover is ~245px, landing ~95px too high;
17
- · place() ran on every scroll and RE-DECIDED the side, so it snapped
18
- between above and below mid-scroll — a 254px jump between two 60px steps;
19
- · and it came back OPEN after a browser Back.
20
-
21
- Three selects have none of those failure modes. There is no position, no
22
- flip, no popover, and no scroll listener — so there is nothing to place
23
- wrongly, nothing to re-decide, and nothing to restore open.
24
- %>
25
- <script>
26
- window.studioBirthdayFields = function (initial) {
27
- var parts = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(initial || ""));
28
-
29
- return {
30
- year: parts ? String(+parts[1]) : "",
31
- month: parts ? String(+parts[2]) : "",
32
- day: parts ? String(+parts[3]) : "",
33
-
34
- months: [
35
- { n: 1, label: "January" }, { n: 2, label: "February" },
36
- { n: 3, label: "March" }, { n: 4, label: "April" },
37
- { n: 5, label: "May" }, { n: 6, label: "June" },
38
- { n: 7, label: "July" }, { n: 8, label: "August" },
39
- { n: 9, label: "September" }, { n: 10, label: "October" },
40
- { n: 11, label: "November" }, { n: 12, label: "December" }
41
- ],
42
-
43
- // Oldest year offered. 120 covers every living person with room to spare;
44
- // the newest is THIS year, because a birthday cannot be in the future and
45
- // the cheapest way to say so is not to offer one.
46
- get years() {
47
- var now = new Date().getFullYear();
48
- var out = [];
49
- for (var y = now; y >= now - 120; y--) out.push(y);
50
- return out;
51
- },
52
-
53
- // The days that exist in the chosen month — 30 in April, and 29 in a
54
- // February only when the chosen year is a leap year. Day 0 of the NEXT
55
- // month is the last day of this one, which is the standard trick and
56
- // handles the leap rule without one being written down.
57
- get dayOptions() {
58
- var m = parseInt(this.month, 10);
59
- var y = parseInt(this.year, 10) || 2000; // a leap year, so Feb offers 29 before a year is picked
60
- if (!m) return Array.from({ length: 31 }, function (_, i) { return i + 1; });
61
-
62
- var count = new Date(y, m, 0).getDate();
63
- return Array.from({ length: count }, function (_, i) { return i + 1; });
64
- },
65
-
66
- get complete() {
67
- return !!(this.year && this.month && this.day);
68
- },
69
-
70
- get isoValue() {
71
- if (!this.complete) return "";
72
- return this.year + "-" +
73
- String(this.month).padStart(2, "0") + "-" +
74
- String(this.day).padStart(2, "0");
75
- },
76
-
77
- // Named to match what the shared field binds to. Clears a day that the new
78
- // month or year does not have — pick 31 January then switch to February and
79
- // the 31st stops existing — then publishes.
80
- onMonthChange() {
81
- if (this.day && parseInt(this.day, 10) > this.dayOptions.length) this.day = "";
82
- this.publish();
83
- },
84
-
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
-
111
- publish() {
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]) : "";
143
- }
144
- };
145
- };
146
- </script>
@@ -1,65 +0,0 @@
1
- <%# The edit form's Alpine component.
2
-
3
- Its own partial so the page above stays readable and this can be reasoned
4
- about as behaviour rather than markup.
5
- %>
6
- <script>
7
- window.studioProfileForm = function (initial) {
8
- return {
9
- alpine: true,
10
- fields: Object.assign({}, initial),
11
- initial: Object.assign({}, initial),
12
-
13
- // Trimmed on both sides: whitespace is not a change, and the controller
14
- // trims on the way in too — so the bar agrees with what saving would do.
15
- changed(key) {
16
- return String(this.fields[key] || "").trim() !== String(this.initial[key] || "").trim();
17
- },
18
-
19
- get changeCount() {
20
- var self = this;
21
- return Object.keys(this.fields).filter(function (k) { return self.changed(k); }).length;
22
- },
23
-
24
- get dirty() { return this.changeCount > 0; },
25
-
26
- discard() {
27
- this.fields = Object.assign({}, this.initial);
28
- },
29
-
30
- init() {
31
- var self = this;
32
-
33
- // Guard the exit BOTH ways. beforeunload covers a hard navigation or a
34
- // closed tab; turbo:before-visit covers a Turbo Drive visit, which never
35
- // fires beforeunload — an app with Turbo on would otherwise drop edits
36
- // silently on any in-app link.
37
- this._beforeUnload = function (e) {
38
- if (!self.dirty) return;
39
- e.preventDefault();
40
- e.returnValue = "";
41
- };
42
- window.addEventListener("beforeunload", this._beforeUnload);
43
-
44
- this._beforeVisit = function (e) {
45
- if (!self.dirty) return;
46
- if (!window.confirm("You have unsaved changes. Leave without saving?")) {
47
- e.preventDefault();
48
- }
49
- };
50
- document.addEventListener("turbo:before-visit", this._beforeVisit);
51
-
52
- // Submitting is not leaving. Drop both guards before the form goes, or
53
- // saving prompts you about the very changes you just chose to save.
54
- this.$refs.form.addEventListener("submit", function () { self.teardown(); });
55
- },
56
-
57
- teardown() {
58
- window.removeEventListener("beforeunload", this._beforeUnload);
59
- document.removeEventListener("turbo:before-visit", this._beforeVisit);
60
- },
61
-
62
- destroy() { this.teardown(); }
63
- };
64
- };
65
- </script>