studio-engine 0.4.12 → 0.4.13

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: 38d351424eeef931252e2c219759db0fdd55605176d2daf7ca10ba283332144f
4
- data.tar.gz: d23cacb846cc912b926bca4093bf6d9a8d0de3c1332147244cb0005d82ace99b
3
+ metadata.gz: 8b6894996dc059e056097232586c6cc442344d2185c2cd435add21e5f78f57f4
4
+ data.tar.gz: c2a7a5346ac290461292100403af6fda95ee301d346c8b8de23bf05c91fc6ee5
5
5
  SHA512:
6
- metadata.gz: 6e3af533680b72fc4a010c6c07143d75f332d7f6b82c8f5ef8310bfdd979258f2050d7bb581c39aad61c8187d7510e30abb5d621a7d4626769122f20bafe8957
7
- data.tar.gz: 9434c0b55cd9275491cddaad6eae404ca78b30dd7d3e0fe42e29ae0b5ae8f5ee78b7a346b8e00c89ad8c4f881097e512092d9c607d023670bb017416a1bfb2b8
6
+ metadata.gz: 7280d2c5f81ce34a3a979bd7bad01b767646075949bebf93ebd3c6277758f96f54f0132a59e119abfdd3a8c4ecd01b08f08f4bcd386b8747ed3b3d8348e2dcba
7
+ data.tar.gz: 8f8e965dcb50315ec81c90f225fa916581bd02f00aba0d5fab9ddebb9d29528239dbff0e876c72854724f74820e6a1dbe1cc262d4bdfd3ab6957f3c2d6d60296
data/CHANGELOG.md CHANGED
@@ -2,6 +2,19 @@
2
2
 
3
3
  The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html) — `MAJOR.MINOR.PATCH`. Both consumer Rails apps pin to a tag in their `Gemfile`; bumping the tag is a release.
4
4
 
5
+ ## v0.4.13 (2026-06-02)
6
+
7
+ Promotes `components/_avatar_cropper` onto the shared crop-photo modal — completing the image-upload extraction started in v0.4.12. The avatar cropper is the **deferred-form-field** counterpart to `imageUploadHost`: it stages a cropped PNG on a hidden file input + shows a round preview, and the enclosing form (signup / profile edit) submits later (vs. `imageUploadHost`, which submits immediately).
8
+
9
+ ### Changed
10
+ - **`components/_avatar_cropper`** now drives its crop through the shared `crop-photo` modal (`Alpine.store('modals').open('crop-photo', { imageUrl })`) and renders `studio/cropper_assets`, replacing the old bespoke `z-[110]` overlay + direct cropper.js load + the `avatarCropper()` factory (now `avatarCropperHost()`). The parent gets the cropped Blob back via the `crop-photo-confirmed` window event.
11
+
12
+ ### Integration
13
+ Consumers rendering `components/avatar_cropper` now need the v0.4.12 image-upload integration: the `crop-photo` modal registered in the modal-host block (see v0.4.12 → Integration). The partial renders `studio/cropper_assets` itself, so cropper.js + the factories load where it's used.
14
+
15
+ ### Migration
16
+ Apps that kept a local override of `components/_avatar_cropper` to route it through the shared modal (Turf Monster) can **delete the override** and use this.
17
+
5
18
  ## v0.4.12 (2026-06-02)
6
19
 
7
20
  Promotes the image crop-and-upload UI out of Turf Monster into the engine: a shared cropper modal, the immediate-save upload host, the loading-card-around-a-Turbo-submit helper, and the generic "saving" card. Any consumer can now add a cropped image upload (avatar, banner, logo, OG image) with one `imageUploadHost(...)` x-data plus the cropper assets partial — no bespoke JS.
@@ -1,15 +1,22 @@
1
- <%# locals: (form_field_name: "user[avatar]") %>
2
- <%# Self-contained avatar cropper: Cropper.js CDN + Alpine.js component %>
3
- <%# Output: sets a cropped 256x256 PNG File on the hidden file input for standard Rails upload %>
1
+ <%# Avatar cropper a deferred form field. Stages a cropped 256x256 PNG on a
2
+ hidden file input (the enclosing form submits it later) and shows a round
3
+ preview. The crop interaction runs through the SHARED crop-photo modal
4
+ (Alpine.store('modals').open('crop-photo', { imageUrl })) — so register that
5
+ modal in your host block per the v0.4.12 integration. The parent receives the
6
+ cropped Blob back via the 'crop-photo-confirmed' window event.
4
7
 
5
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.6.2/cropper.min.css">
6
- <script src="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.6.2/cropper.min.js"></script>
8
+ Distinct from imageUploadHost (immediate-save): this STAGES the file and
9
+ lets the enclosing form own the submit (signup / profile edit).
7
10
 
8
- <div x-data="avatarCropper()" class="space-y-3">
9
- <%# Hidden file input that receives the cropped file %>
11
+ Locals: form_field_name (e.g. "user[avatar]"). %>
12
+
13
+ <%= render "studio/cropper_assets" %>
14
+
15
+ <div x-data="avatarCropperHost()"
16
+ @crop-photo-confirmed.window="applyCrop($event.detail.blob)"
17
+ class="space-y-3">
10
18
  <input type="file" name="<%= form_field_name %>" x-ref="hiddenFileInput" class="hidden">
11
19
 
12
- <%# Preview + pick button %>
13
20
  <div class="flex items-center gap-4">
14
21
  <div class="relative w-16 h-16 rounded-full overflow-hidden bg-surface-alt border-2 border-subtle flex-shrink-0">
15
22
  <template x-if="croppedUrl">
@@ -23,8 +30,7 @@
23
30
  </div>
24
31
  </template>
25
32
  </div>
26
- <button type="button" @click="$refs.filePicker.click()"
27
- class="btn btn-outline btn-sm">
33
+ <button type="button" @click="$refs.filePicker.click()" class="btn btn-outline btn-sm">
28
34
  <span x-text="croppedUrl ? 'Change Photo' : 'Upload Photo'"></span>
29
35
  </button>
30
36
  <template x-if="croppedUrl">
@@ -32,101 +38,41 @@
32
38
  </template>
33
39
  </div>
34
40
 
35
- <%# Invisible file picker %>
36
41
  <input type="file" x-ref="filePicker" @change="onFileSelected($event)" accept="image/*" class="hidden">
37
-
38
- <%# Crop overlay %>
39
- <template x-if="showCropModal">
40
- <div class="fixed inset-0 z-[110] flex items-center justify-center" @keydown.escape.window="cancelCrop()">
41
- <div class="absolute inset-0 bg-black/70" @click="cancelCrop()"></div>
42
- <div class="relative bg-surface rounded-2xl border border-subtle shadow-2xl p-6 max-w-md w-full mx-4">
43
- <h3 class="text-heading font-bold text-lg mb-4">Crop Photo</h3>
44
- <div class="w-full" style="max-height: 360px;">
45
- <img x-ref="cropImage" :src="rawImageUrl" class="max-w-full" style="display: block;">
46
- </div>
47
- <div class="flex justify-end gap-3 mt-4">
48
- <button type="button" @click="cancelCrop()" class="btn btn-outline btn-sm">Cancel</button>
49
- <button type="button" @click="confirmCrop()" class="btn btn-primary btn-sm">Crop & Save</button>
50
- </div>
51
- </div>
52
- </div>
53
- </template>
54
42
  </div>
55
43
 
56
44
  <script>
57
- function avatarCropper() {
45
+ function avatarCropperHost() {
58
46
  return {
59
- cropper: null,
60
- showCropModal: false,
61
- rawImageUrl: null,
62
47
  croppedUrl: null,
63
48
 
64
49
  onFileSelected(event) {
65
50
  var file = event.target.files[0];
66
51
  if (!file) return;
67
-
68
52
  var self = this;
69
53
  var reader = new FileReader();
70
54
  reader.onload = function(e) {
71
- self.rawImageUrl = e.target.result;
72
- self.showCropModal = true;
73
- self.$nextTick(function() {
74
- self.initCropper();
75
- });
55
+ // Hand the raw image to the shared modal host; the crop-photo modal
56
+ // owns the cropper UI and posts back via 'crop-photo-confirmed'.
57
+ Alpine.store('modals').open('crop-photo', { imageUrl: e.target.result });
76
58
  };
77
59
  reader.readAsDataURL(file);
78
- // Reset so same file can be re-selected
79
- event.target.value = '';
60
+ event.target.value = ''; // allow re-selecting the same file
80
61
  },
81
62
 
82
- initCropper() {
83
- if (this.cropper) { this.cropper.destroy(); this.cropper = null; }
84
- var image = this.$refs.cropImage;
85
- this.cropper = new Cropper(image, {
86
- aspectRatio: 1,
87
- viewMode: 1,
88
- dragMode: 'move',
89
- autoCropArea: 0.9,
90
- cropBoxResizable: true,
91
- cropBoxMovable: true,
92
- background: false,
93
- guides: true
94
- });
95
- },
96
-
97
- confirmCrop() {
98
- var self = this;
99
- var canvas = this.cropper.getCroppedCanvas({ width: 256, height: 256 });
100
- canvas.toBlob(function(blob) {
101
- // Revoke previous blob URL to prevent memory leak
102
- if (self.croppedUrl) URL.revokeObjectURL(self.croppedUrl);
103
- // Set cropped preview
104
- self.croppedUrl = URL.createObjectURL(blob);
105
-
106
- // Assign file to hidden input via DataTransfer
107
- var file = new File([blob], 'avatar.png', { type: 'image/png' });
108
- var dt = new DataTransfer();
109
- dt.items.add(file);
110
- self.$refs.hiddenFileInput.files = dt.files;
111
-
112
- self.closeCropModal();
113
- }, 'image/png');
114
- },
115
-
116
- cancelCrop() {
117
- this.closeCropModal();
118
- },
119
-
120
- closeCropModal() {
121
- if (this.cropper) { this.cropper.destroy(); this.cropper = null; }
122
- this.showCropModal = false;
123
- this.rawImageUrl = null;
63
+ applyCrop(blob) {
64
+ if (!blob) return;
65
+ if (this.croppedUrl) URL.revokeObjectURL(this.croppedUrl);
66
+ this.croppedUrl = URL.createObjectURL(blob);
67
+ var file = new File([blob], 'avatar.png', { type: 'image/png' });
68
+ var dt = new DataTransfer();
69
+ dt.items.add(file);
70
+ this.$refs.hiddenFileInput.files = dt.files;
124
71
  },
125
72
 
126
73
  removePhoto() {
127
74
  if (this.croppedUrl) URL.revokeObjectURL(this.croppedUrl);
128
75
  this.croppedUrl = null;
129
- // Clear the hidden file input
130
76
  var dt = new DataTransfer();
131
77
  this.$refs.hiddenFileInput.files = dt.files;
132
78
  }
@@ -1,3 +1,3 @@
1
1
  module Studio
2
- VERSION = "0.4.12"
2
+ VERSION = "0.4.13"
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.4.12
4
+ version: 0.4.13
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex McRitchie