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 +4 -4
- data/CHANGELOG.md +13 -0
- data/app/views/components/_avatar_cropper.html.erb +29 -83
- data/lib/studio/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8b6894996dc059e056097232586c6cc442344d2185c2cd435add21e5f78f57f4
|
|
4
|
+
data.tar.gz: c2a7a5346ac290461292100403af6fda95ee301d346c8b8de23bf05c91fc6ee5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
<%#
|
|
2
|
-
|
|
3
|
-
|
|
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
|
-
|
|
6
|
-
|
|
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
|
-
|
|
9
|
-
|
|
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
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
//
|
|
79
|
-
event.target.value = '';
|
|
60
|
+
event.target.value = ''; // allow re-selecting the same file
|
|
80
61
|
},
|
|
81
62
|
|
|
82
|
-
|
|
83
|
-
if (
|
|
84
|
-
|
|
85
|
-
this.
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
}
|
data/lib/studio/version.rb
CHANGED