update
This commit is contained in:
@ -49,12 +49,21 @@ export class DeesInputProfilePicture extends DeesInputBase<DeesInputProfilePictu
|
||||
@property({ type: Array })
|
||||
public acceptedFormats: string[] = ['image/jpeg', 'image/png', 'image/webp'];
|
||||
|
||||
@property({ type: Number })
|
||||
public outputSize: number = 800; // Output resolution in pixels
|
||||
|
||||
@property({ type: Number })
|
||||
public outputQuality: number = 0.95; // 0-1 quality for JPEG
|
||||
|
||||
@state()
|
||||
private isHovered: boolean = false;
|
||||
|
||||
@state()
|
||||
private isDragging: boolean = false;
|
||||
|
||||
@state()
|
||||
private isLoading: boolean = false;
|
||||
|
||||
private modalInstance: ProfilePictureModal | null = null;
|
||||
|
||||
public static styles = [
|
||||
@ -86,8 +95,8 @@ export class DeesInputProfilePicture extends DeesInputBase<DeesInputProfilePictu
|
||||
.profile-picture {
|
||||
width: var(--size, 120px);
|
||||
height: var(--size, 120px);
|
||||
background: ${cssManager.bdTheme('hsl(210 20% 98%)', 'hsl(215 27.9% 16.9%)')};
|
||||
border: 3px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')};
|
||||
background: ${cssManager.bdTheme('#f5f5f5', '#18181b')};
|
||||
border: 3px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@ -105,12 +114,12 @@ export class DeesInputProfilePicture extends DeesInputBase<DeesInputProfilePictu
|
||||
}
|
||||
|
||||
.profile-picture.dragging {
|
||||
border-color: ${cssManager.bdTheme('hsl(222.2 47.4% 11.2%)', 'hsl(210 20% 98%)')};
|
||||
box-shadow: 0 0 0 4px ${cssManager.bdTheme('hsl(222.2 47.4% 11.2% / 0.1)', 'hsl(210 20% 98% / 0.1)')};
|
||||
border-color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')};
|
||||
box-shadow: 0 0 0 4px ${cssManager.bdTheme('rgba(59, 130, 246, 0.15)', 'rgba(96, 165, 250, 0.15)')};
|
||||
}
|
||||
|
||||
.profile-picture:hover {
|
||||
border-color: ${cssManager.bdTheme('hsl(222.2 47.4% 11.2%)', 'hsl(210 20% 98%)')};
|
||||
border-color: ${cssManager.bdTheme('#d4d4d8', '#3f3f46')};
|
||||
}
|
||||
|
||||
.profile-picture:disabled {
|
||||
@ -125,7 +134,7 @@ export class DeesInputProfilePicture extends DeesInputBase<DeesInputProfilePictu
|
||||
}
|
||||
|
||||
.placeholder-icon {
|
||||
color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')};
|
||||
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
|
||||
}
|
||||
|
||||
.overlay {
|
||||
@ -156,28 +165,32 @@ export class DeesInputProfilePicture extends DeesInputBase<DeesInputProfilePictu
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border: none;
|
||||
background: ${cssManager.bdTheme('rgba(255, 255, 255, 0.95)', 'rgba(39, 39, 42, 0.95)')};
|
||||
border: 1px solid ${cssManager.bdTheme('rgba(0, 0, 0, 0.1)', 'rgba(255, 255, 255, 0.1)')};
|
||||
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
pointer-events: auto;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.overlay-button:hover {
|
||||
background: white;
|
||||
background: ${cssManager.bdTheme('#ffffff', '#3f3f46')};
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.overlay-button.delete {
|
||||
background: rgba(239, 68, 68, 0.9);
|
||||
background: ${cssManager.bdTheme('rgba(239, 68, 68, 0.9)', 'rgba(220, 38, 38, 0.9)')};
|
||||
color: white;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.overlay-button.delete:hover {
|
||||
background: rgb(239, 68, 68);
|
||||
background: ${cssManager.bdTheme('#ef4444', '#dc2626')};
|
||||
}
|
||||
|
||||
.drop-zone-text {
|
||||
@ -194,6 +207,62 @@ export class DeesInputProfilePicture extends DeesInputBase<DeesInputProfilePictu
|
||||
.hidden-input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Loading animation */
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: ${cssManager.bdTheme('rgba(255, 255, 255, 0.8)', 'rgba(0, 0, 0, 0.8)')};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: inherit;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.loading-overlay.show {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 3px solid ${cssManager.bdTheme('rgba(0, 0, 0, 0.1)', 'rgba(255, 255, 255, 0.1)')};
|
||||
border-top-color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')};
|
||||
border-radius: 50%;
|
||||
animation: spin 0.6s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.05);
|
||||
opacity: 0.8;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.profile-picture.clicking {
|
||||
animation: pulse 0.3s ease-out;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@ -210,7 +279,7 @@ export class DeesInputProfilePicture extends DeesInputBase<DeesInputProfilePictu
|
||||
@drop=${this.handleDrop}
|
||||
style="--size: ${this.size}px"
|
||||
>
|
||||
<div class="profile-picture ${this.shape} ${this.isDragging ? 'dragging' : ''}">
|
||||
<div class="profile-picture ${this.shape} ${this.isDragging ? 'dragging' : ''} ${this.isLoading && !this.value ? 'clicking' : ''}">
|
||||
${this.value ? html`
|
||||
<img class="profile-image" src="${this.value}" alt="Profile picture" />
|
||||
` : html`
|
||||
@ -241,6 +310,12 @@ export class DeesInputProfilePicture extends DeesInputBase<DeesInputProfilePictu
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${this.isLoading && !this.value ? html`
|
||||
<div class="loading-overlay show">
|
||||
<div class="loading-spinner"></div>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -259,7 +334,21 @@ export class DeesInputProfilePicture extends DeesInputBase<DeesInputProfilePictu
|
||||
|
||||
if (!this.value) {
|
||||
// If no image, open file picker
|
||||
this.isLoading = true;
|
||||
const input = this.shadowRoot!.querySelector('.hidden-input') as HTMLInputElement;
|
||||
|
||||
// Set up a focus handler to detect when the dialog is closed without selection
|
||||
const handleFocus = () => {
|
||||
setTimeout(() => {
|
||||
// Check if no file was selected
|
||||
if (!input.files || input.files.length === 0) {
|
||||
this.isLoading = false;
|
||||
}
|
||||
window.removeEventListener('focus', handleFocus);
|
||||
}, 300);
|
||||
};
|
||||
|
||||
window.addEventListener('focus', handleFocus);
|
||||
input.click();
|
||||
}
|
||||
}
|
||||
@ -268,6 +357,9 @@ export class DeesInputProfilePicture extends DeesInputBase<DeesInputProfilePictu
|
||||
const input = event.target as HTMLInputElement;
|
||||
const file = input.files?.[0];
|
||||
|
||||
// Always reset loading state when file dialog interaction completes
|
||||
this.isLoading = false;
|
||||
|
||||
if (file) {
|
||||
this.processFile(file);
|
||||
}
|
||||
@ -337,6 +429,8 @@ export class DeesInputProfilePicture extends DeesInputBase<DeesInputProfilePictu
|
||||
this.modalInstance = new ProfilePictureModal();
|
||||
this.modalInstance.shape = this.shape;
|
||||
this.modalInstance.initialImage = imageToEdit;
|
||||
this.modalInstance.outputSize = this.outputSize;
|
||||
this.modalInstance.outputQuality = this.outputQuality;
|
||||
|
||||
this.modalInstance.addEventListener('save', (event: CustomEvent) => {
|
||||
this.value = event.detail.croppedImage;
|
||||
|
@ -6,6 +6,8 @@ export interface CropperOptions {
|
||||
shape: ProfileShape;
|
||||
aspectRatio: number;
|
||||
minSize?: number;
|
||||
outputSize?: number;
|
||||
outputQuality?: number;
|
||||
}
|
||||
|
||||
export class ImageCropper {
|
||||
@ -37,6 +39,8 @@ export class ImageCropper {
|
||||
constructor(options: CropperOptions) {
|
||||
this.options = {
|
||||
minSize: 50,
|
||||
outputSize: 800, // Higher default resolution
|
||||
outputQuality: 0.95, // Higher quality
|
||||
...options
|
||||
};
|
||||
|
||||
@ -96,8 +100,8 @@ export class ImageCropper {
|
||||
container.appendChild(this.canvas);
|
||||
container.appendChild(this.overlayCanvas);
|
||||
|
||||
// Calculate image scale and position
|
||||
const scale = Math.max(
|
||||
// Calculate image scale to fit within container (not fill)
|
||||
const scale = Math.min(
|
||||
containerSize / this.img.width,
|
||||
containerSize / this.img.height
|
||||
);
|
||||
@ -107,7 +111,12 @@ export class ImageCropper {
|
||||
this.imageOffsetY = (containerSize - this.img.height * scale) / 2;
|
||||
|
||||
// Initialize crop area
|
||||
this.cropSize = containerSize * 0.8;
|
||||
// Make the crop area fit within the actual image bounds
|
||||
const scaledImageWidth = this.img.width * scale;
|
||||
const scaledImageHeight = this.img.height * scale;
|
||||
const maxCropSize = Math.min(scaledImageWidth, scaledImageHeight, containerSize * 0.8);
|
||||
|
||||
this.cropSize = maxCropSize * 0.8; // Start at 80% of max possible size
|
||||
this.cropX = (containerSize - this.cropSize) / 2;
|
||||
this.cropY = (containerSize - this.cropSize) / 2;
|
||||
}
|
||||
@ -162,8 +171,14 @@ export class ImageCropper {
|
||||
const dx = x - this.dragStartX;
|
||||
const dy = y - this.dragStartY;
|
||||
|
||||
this.cropX = Math.max(0, Math.min(this.canvas.width - this.cropSize, this.cropX + dx));
|
||||
this.cropY = Math.max(0, Math.min(this.canvas.height - this.cropSize, this.cropY + dy));
|
||||
// Constrain crop area to image bounds
|
||||
const minX = this.imageOffsetX;
|
||||
const maxX = this.imageOffsetX + this.img.width * this.imageScale - this.cropSize;
|
||||
const minY = this.imageOffsetY;
|
||||
const maxY = this.imageOffsetY + this.img.height * this.imageScale - this.cropSize;
|
||||
|
||||
this.cropX = Math.max(minX, Math.min(maxX, this.cropX + dx));
|
||||
this.cropY = Math.max(minY, Math.min(maxY, this.cropY + dy));
|
||||
|
||||
this.dragStartX = x;
|
||||
this.dragStartY = y;
|
||||
@ -247,36 +262,52 @@ export class ImageCropper {
|
||||
const dx = x - this.dragStartX;
|
||||
const dy = y - this.dragStartY;
|
||||
|
||||
// Get image bounds
|
||||
const imgLeft = this.imageOffsetX;
|
||||
const imgTop = this.imageOffsetY;
|
||||
const imgRight = this.imageOffsetX + this.img.width * this.imageScale;
|
||||
const imgBottom = this.imageOffsetY + this.img.height * this.imageScale;
|
||||
|
||||
switch (this.resizeHandle) {
|
||||
case 'se':
|
||||
this.cropSize = Math.max(this.minCropSize, Math.min(
|
||||
this.cropSize + Math.max(dx, dy),
|
||||
Math.min(
|
||||
this.canvas.width - this.cropX,
|
||||
this.canvas.height - this.cropY
|
||||
imgRight - this.cropX,
|
||||
imgBottom - this.cropY
|
||||
)
|
||||
));
|
||||
break;
|
||||
case 'nw':
|
||||
const newSize = Math.max(this.minCropSize, this.cropSize - Math.max(dx, dy));
|
||||
const sizeDiff = this.cropSize - newSize;
|
||||
this.cropX += sizeDiff;
|
||||
this.cropY += sizeDiff;
|
||||
const newX = this.cropX + sizeDiff;
|
||||
const newY = this.cropY + sizeDiff;
|
||||
if (newX >= imgLeft && newY >= imgTop) {
|
||||
this.cropX = newX;
|
||||
this.cropY = newY;
|
||||
this.cropSize = newSize;
|
||||
}
|
||||
break;
|
||||
case 'ne':
|
||||
const neSizeDx = Math.max(dx, -dy);
|
||||
const neNewSize = Math.max(this.minCropSize, this.cropSize + neSizeDx);
|
||||
const neSizeDiff = neNewSize - this.cropSize;
|
||||
this.cropY -= neSizeDiff;
|
||||
const neNewY = this.cropY - neSizeDiff;
|
||||
if (neNewY >= imgTop && this.cropX + neNewSize <= imgRight) {
|
||||
this.cropY = neNewY;
|
||||
this.cropSize = neNewSize;
|
||||
}
|
||||
break;
|
||||
case 'sw':
|
||||
const swSizeDx = Math.max(-dx, dy);
|
||||
const swNewSize = Math.max(this.minCropSize, this.cropSize + swSizeDx);
|
||||
const swSizeDiff = swNewSize - this.cropSize;
|
||||
this.cropX -= swSizeDiff;
|
||||
const swNewX = this.cropX - swSizeDiff;
|
||||
if (swNewX >= imgLeft && this.cropY + swNewSize <= imgBottom) {
|
||||
this.cropX = swNewX;
|
||||
this.cropSize = swNewSize;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -286,6 +317,10 @@ export class ImageCropper {
|
||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
this.overlayCtx.clearRect(0, 0, this.overlayCanvas.width, this.overlayCanvas.height);
|
||||
|
||||
// Fill background
|
||||
this.ctx.fillStyle = '#000000';
|
||||
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
|
||||
// Draw image
|
||||
this.ctx.drawImage(
|
||||
this.img,
|
||||
@ -295,9 +330,14 @@ export class ImageCropper {
|
||||
this.img.height * this.imageScale
|
||||
);
|
||||
|
||||
// Draw overlay
|
||||
// Draw overlay only over the image area
|
||||
this.overlayCtx.fillStyle = 'rgba(0, 0, 0, 0.5)';
|
||||
this.overlayCtx.fillRect(0, 0, this.overlayCanvas.width, this.overlayCanvas.height);
|
||||
this.overlayCtx.fillRect(
|
||||
this.imageOffsetX,
|
||||
this.imageOffsetY,
|
||||
this.img.width * this.imageScale,
|
||||
this.img.height * this.imageScale
|
||||
);
|
||||
|
||||
// Clear crop area
|
||||
this.overlayCtx.save();
|
||||
@ -365,13 +405,21 @@ export class ImageCropper {
|
||||
const cropCanvas = document.createElement('canvas');
|
||||
const cropCtx = cropCanvas.getContext('2d')!;
|
||||
|
||||
// Set output size
|
||||
const outputSize = 400;
|
||||
// Calculate the actual crop size in original image pixels
|
||||
const scale = 1 / this.imageScale;
|
||||
const originalCropSize = this.cropSize * scale;
|
||||
|
||||
// Use requested output size, but warn if upscaling
|
||||
const outputSize = this.options.outputSize!;
|
||||
|
||||
if (outputSize > originalCropSize) {
|
||||
console.info(`Profile picture: Upscaling from ${Math.round(originalCropSize)}px to ${outputSize}px`);
|
||||
}
|
||||
|
||||
cropCanvas.width = outputSize;
|
||||
cropCanvas.height = outputSize;
|
||||
|
||||
// Calculate source coordinates
|
||||
const scale = 1 / this.imageScale;
|
||||
const sx = (this.cropX - this.imageOffsetX) * scale;
|
||||
const sy = (this.cropY - this.imageOffsetY) * scale;
|
||||
const sSize = this.cropSize * scale;
|
||||
@ -383,6 +431,10 @@ export class ImageCropper {
|
||||
cropCtx.clip();
|
||||
}
|
||||
|
||||
// Enable image smoothing for quality
|
||||
cropCtx.imageSmoothingEnabled = true;
|
||||
cropCtx.imageSmoothingQuality = 'high';
|
||||
|
||||
// Draw cropped image
|
||||
cropCtx.drawImage(
|
||||
this.img,
|
||||
@ -390,7 +442,11 @@ export class ImageCropper {
|
||||
0, 0, outputSize, outputSize
|
||||
);
|
||||
|
||||
return cropCanvas.toDataURL('image/jpeg', 0.9);
|
||||
// Detect format from original image
|
||||
const isPng = this.options.image.includes('image/png');
|
||||
const format = isPng ? 'image/png' : 'image/jpeg';
|
||||
|
||||
return cropCanvas.toDataURL(format, this.options.outputQuality);
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
|
@ -8,11 +8,14 @@ import {
|
||||
state,
|
||||
type TemplateResult,
|
||||
} from '@design.estate/dees-element';
|
||||
import * as colors from '../00colors.js';
|
||||
import { cssGeistFontFamily } from '../00fonts.js';
|
||||
import { zIndexRegistry } from '../00zindex.js';
|
||||
import '../dees-icon.js';
|
||||
import '../dees-button.js';
|
||||
import '../dees-windowlayer.js';
|
||||
import { DeesWindowLayer } from '../dees-windowlayer.js';
|
||||
import { ImageCropper } from './profilepicture.cropper.js';
|
||||
import { zIndexRegistry } from '../00zindex.js';
|
||||
import type { ProfileShape } from './dees-input-profilepicture.js';
|
||||
|
||||
@customElement('dees-profilepicture-modal')
|
||||
@ -23,6 +26,12 @@ export class ProfilePictureModal extends DeesElement {
|
||||
@property({ type: String })
|
||||
public shape: ProfileShape = 'round';
|
||||
|
||||
@property({ type: Number })
|
||||
public outputSize: number = 800;
|
||||
|
||||
@property({ type: Number })
|
||||
public outputQuality: number = 0.95;
|
||||
|
||||
@state()
|
||||
private currentStep: 'crop' | 'preview' = 'crop';
|
||||
|
||||
@ -40,6 +49,8 @@ export class ProfilePictureModal extends DeesElement {
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
font-family: ${cssGeistFontFamily};
|
||||
color: ${cssManager.bdTheme('#333', '#fff')};
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
@ -52,44 +63,53 @@ export class ProfilePictureModal extends DeesElement {
|
||||
}
|
||||
|
||||
.modal-container {
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(224 71.4% 4.1%)')};
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
width: 90%;
|
||||
max-width: 600px;
|
||||
max-height: 90vh;
|
||||
background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
|
||||
border-radius: 12px;
|
||||
border: 1px solid ${cssManager.bdTheme('rgba(0, 0, 0, 0.08)', 'rgba(255, 255, 255, 0.08)')};
|
||||
box-shadow: ${cssManager.bdTheme(
|
||||
'0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)',
|
||||
'0 20px 25px -5px rgba(0, 0, 0, 0.3), 0 10px 10px -5px rgba(0, 0, 0, 0.2)'
|
||||
)};
|
||||
width: 480px;
|
||||
max-width: calc(100vw - 32px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
animation: modalSlideIn 0.3s ease-out;
|
||||
transform: translateY(10px) scale(0.98);
|
||||
opacity: 0;
|
||||
animation: modalShow 0.25s cubic-bezier(0.4, 0, 0.2, 1) forwards;
|
||||
}
|
||||
|
||||
@keyframes modalSlideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
@keyframes modalShow {
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
transform: translateY(0px) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 24px;
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')};
|
||||
height: 52px;
|
||||
padding: 0 20px;
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('rgba(0, 0, 0, 0.06)', 'rgba(255, 255, 255, 0.06)')};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 20px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: ${cssManager.bdTheme('hsl(224 71.4% 4.1%)', 'hsl(210 20% 98%)')};
|
||||
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: none;
|
||||
@ -99,13 +119,17 @@ export class ProfilePictureModal extends DeesElement {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')};
|
||||
transition: all 0.2s ease;
|
||||
color: ${cssManager.bdTheme('#71717a', '#71717a')};
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.close-button:hover {
|
||||
background: ${cssManager.bdTheme('hsl(210 20% 98%)', 'hsl(215 27.9% 16.9%)')};
|
||||
color: ${cssManager.bdTheme('hsl(224 71.4% 4.1%)', 'hsl(210 20% 98%)')};
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.05)', 'rgba(255, 255, 255, 0.05)')};
|
||||
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
|
||||
}
|
||||
|
||||
.close-button:active {
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.08)', 'rgba(255, 255, 255, 0.08)')};
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
@ -115,28 +139,39 @@ export class ProfilePictureModal extends DeesElement {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.cropper-container {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
max-width: 360px;
|
||||
aspect-ratio: 1;
|
||||
position: relative;
|
||||
background: ${cssManager.bdTheme('#000000', '#000000')};
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: ${cssManager.bdTheme(
|
||||
'inset 0 2px 4px rgba(0, 0, 0, 0.06)',
|
||||
'inset 0 2px 4px rgba(0, 0, 0, 0.2)'
|
||||
)};
|
||||
}
|
||||
|
||||
.preview-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.preview-image {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
object-fit: cover;
|
||||
border: 3px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')};
|
||||
border: 4px solid ${cssManager.bdTheme('#ffffff', '#18181b')};
|
||||
box-shadow: ${cssManager.bdTheme(
|
||||
'0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)',
|
||||
'0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.2)'
|
||||
)};
|
||||
}
|
||||
|
||||
.preview-image.round {
|
||||
@ -144,41 +179,51 @@ export class ProfilePictureModal extends DeesElement {
|
||||
}
|
||||
|
||||
.preview-image.square {
|
||||
border-radius: 12px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.success-message {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 24px;
|
||||
background: ${cssManager.bdTheme('hsl(142 69% 45% / 0.1)', 'hsl(142 69% 55% / 0.1)')};
|
||||
color: ${cssManager.bdTheme('hsl(142 69% 45%)', 'hsl(142 69% 55%)')};
|
||||
border-radius: 8px;
|
||||
gap: 10px;
|
||||
padding: 10px 20px;
|
||||
background: ${cssManager.bdTheme('#10b981', '#10b981')};
|
||||
color: white;
|
||||
border-radius: 100px;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
animation: successPulse 0.4s ease-out;
|
||||
}
|
||||
|
||||
@keyframes successPulse {
|
||||
0% { transform: scale(0.9); opacity: 0; }
|
||||
50% { transform: scale(1.02); }
|
||||
100% { transform: scale(1); opacity: 1; }
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
padding: 24px;
|
||||
border-top: 1px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')};
|
||||
padding: 20px 24px;
|
||||
border-top: 1px solid ${cssManager.bdTheme('rgba(0, 0, 0, 0.06)', 'rgba(255, 255, 255, 0.06)')};
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
gap: 10px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.instructions {
|
||||
text-align: center;
|
||||
color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')};
|
||||
font-size: 14px;
|
||||
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
max-width: 320px;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 3px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')};
|
||||
border-top-color: ${cssManager.bdTheme('hsl(222.2 47.4% 11.2%)', 'hsl(210 20% 98%)')};
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 3px solid ${cssManager.bdTheme('rgba(0, 0, 0, 0.1)', 'rgba(255, 255, 255, 0.1)')};
|
||||
border-top-color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')};
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
animation: spin 0.6s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
@ -186,21 +231,33 @@ export class ProfilePictureModal extends DeesElement {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.modal-container {
|
||||
width: calc(100vw - 32px);
|
||||
margin: 16px;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 24px;
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
async connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
// Get z-index from registry
|
||||
// Create window layer first (it will get its own z-index)
|
||||
this.windowLayer = await DeesWindowLayer.createAndShow({
|
||||
blur: true,
|
||||
});
|
||||
this.windowLayer.addEventListener('click', () => this.close());
|
||||
|
||||
// Now get z-index for modal (will be above window layer)
|
||||
this.zIndex = zIndexRegistry.getNextZIndex();
|
||||
this.style.setProperty('--z-index', this.zIndex.toString());
|
||||
|
||||
// Add window layer
|
||||
this.windowLayer = document.createElement('dees-windowlayer');
|
||||
this.windowLayer.addEventListener('click', () => this.close());
|
||||
document.body.appendChild(this.windowLayer);
|
||||
|
||||
// Register with z-index registry
|
||||
zIndexRegistry.register(this, this.zIndex);
|
||||
}
|
||||
@ -214,7 +271,7 @@ export class ProfilePictureModal extends DeesElement {
|
||||
}
|
||||
|
||||
if (this.windowLayer) {
|
||||
this.windowLayer.remove();
|
||||
await this.windowLayer.destroy();
|
||||
}
|
||||
|
||||
// Unregister from z-index registry
|
||||
@ -226,24 +283,24 @@ export class ProfilePictureModal extends DeesElement {
|
||||
<div class="modal-container" @click=${(e: Event) => e.stopPropagation()}>
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title">
|
||||
${this.currentStep === 'crop' ? 'Crop Profile Picture' : 'Preview'}
|
||||
${this.currentStep === 'crop' ? 'Adjust Image' : 'Success'}
|
||||
</h3>
|
||||
<button class="close-button" @click=${this.close}>
|
||||
<dees-icon icon="lucide:x" iconSize="20"></dees-icon>
|
||||
<button class="close-button" @click=${this.close} title="Close">
|
||||
<dees-icon icon="lucide:x" iconSize="16"></dees-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
${this.currentStep === 'crop' ? html`
|
||||
<div class="instructions">
|
||||
Drag and resize the selection area to crop your profile picture
|
||||
Position and resize the square to select your profile area
|
||||
</div>
|
||||
<div class="cropper-container" id="cropperContainer"></div>
|
||||
` : html`
|
||||
<div class="preview-container">
|
||||
${this.isProcessing ? html`
|
||||
<div class="loading-spinner"></div>
|
||||
<div class="instructions">Processing image...</div>
|
||||
<div class="instructions">Saving...</div>
|
||||
` : html`
|
||||
<img
|
||||
class="preview-image ${this.shape}"
|
||||
@ -251,8 +308,8 @@ export class ProfilePictureModal extends DeesElement {
|
||||
alt="Cropped preview"
|
||||
/>
|
||||
<div class="success-message">
|
||||
<dees-icon icon="lucide:checkCircle" iconSize="20"></dees-icon>
|
||||
<span>Profile picture updated successfully!</span>
|
||||
<dees-icon icon="lucide:check" iconSize="16"></dees-icon>
|
||||
<span>Looking good!</span>
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
@ -261,19 +318,13 @@ export class ProfilePictureModal extends DeesElement {
|
||||
|
||||
<div class="modal-footer">
|
||||
${this.currentStep === 'crop' ? html`
|
||||
<dees-button type="secondary" @click=${this.close}>
|
||||
<dees-button type="destructive" size="sm" @click=${this.close}>
|
||||
Cancel
|
||||
</dees-button>
|
||||
<dees-button type="primary" @click=${this.handleCrop}>
|
||||
Crop & Save
|
||||
</dees-button>
|
||||
` : html`
|
||||
${!this.isProcessing ? html`
|
||||
<dees-button type="primary" @click=${this.close}>
|
||||
Done
|
||||
<dees-button type="default" size="sm" @click=${this.handleCrop}>
|
||||
Save
|
||||
</dees-button>
|
||||
` : ''}
|
||||
`}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@ -296,6 +347,8 @@ export class ProfilePictureModal extends DeesElement {
|
||||
image: this.initialImage,
|
||||
shape: this.shape,
|
||||
aspectRatio: 1,
|
||||
outputSize: this.outputSize,
|
||||
outputQuality: this.outputQuality,
|
||||
});
|
||||
|
||||
await this.cropper.initialize();
|
||||
|
Reference in New Issue
Block a user