This commit is contained in:
Juergen Kunz
2025-06-30 12:02:02 +00:00
parent 1c25554c38
commit 1db74177b3
3 changed files with 304 additions and 101 deletions

View File

@ -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 type="default" size="sm" @click=${this.handleCrop}>
Save
</dees-button>
` : html`
${!this.isProcessing ? html`
<dees-button type="primary" @click=${this.close}>
Done
</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();