fix(structure): group components into groups inside the repo
This commit is contained in:
@@ -0,0 +1,395 @@
|
||||
import {
|
||||
DeesElement,
|
||||
customElement,
|
||||
html,
|
||||
property,
|
||||
css,
|
||||
cssManager,
|
||||
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/dees-icon.js';
|
||||
import '../../00group-button/dees-button/dees-button.js';
|
||||
import '../../dees-windowlayer/dees-windowlayer.js';
|
||||
import { DeesWindowLayer } from '../../dees-windowlayer/dees-windowlayer.js';
|
||||
import { ImageCropper } from './profilepicture.cropper.js';
|
||||
import type { ProfileShape } from './dees-input-profilepicture.js';
|
||||
|
||||
@customElement('dees-profilepicture-modal')
|
||||
export class ProfilePictureModal extends DeesElement {
|
||||
@property({ type: String })
|
||||
accessor initialImage: string = '';
|
||||
|
||||
@property({ type: String })
|
||||
accessor shape: ProfileShape = 'round';
|
||||
|
||||
@property({ type: Number })
|
||||
accessor outputSize: number = 800;
|
||||
|
||||
@property({ type: Number })
|
||||
accessor outputQuality: number = 0.95;
|
||||
|
||||
@state()
|
||||
accessor currentStep: 'crop' | 'preview' = 'crop';
|
||||
|
||||
@state()
|
||||
accessor croppedImage: string = '';
|
||||
|
||||
@state()
|
||||
accessor isProcessing: boolean = false;
|
||||
|
||||
private cropper: ImageCropper | null = null;
|
||||
private windowLayer: any;
|
||||
private zIndex: number = 0;
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
font-family: ${cssGeistFontFamily};
|
||||
color: ${cssManager.bdTheme('#333', '#fff')};
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: var(--z-index);
|
||||
}
|
||||
|
||||
.modal-container {
|
||||
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;
|
||||
transform: translateY(10px) scale(0.98);
|
||||
opacity: 0;
|
||||
animation: modalShow 0.25s cubic-bezier(0.4, 0, 0.2, 1) forwards;
|
||||
}
|
||||
|
||||
@keyframes modalShow {
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0px) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
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: center;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
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;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: ${cssManager.bdTheme('#71717a', '#71717a')};
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.close-button:hover {
|
||||
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 {
|
||||
flex: 1;
|
||||
padding: 24px;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.cropper-container {
|
||||
width: 100%;
|
||||
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: 20px;
|
||||
}
|
||||
|
||||
.preview-image {
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
object-fit: cover;
|
||||
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 {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.preview-image.square {
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.success-message {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
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: 20px 24px;
|
||||
border-top: 1px solid ${cssManager.bdTheme('rgba(0, 0, 0, 0.06)', 'rgba(255, 255, 255, 0.06)')};
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.instructions {
|
||||
text-align: center;
|
||||
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
max-width: 320px;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.modal-container {
|
||||
width: calc(100vw - 32px);
|
||||
margin: 16px;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 24px;
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
async connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
// 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());
|
||||
|
||||
// Register with z-index registry
|
||||
zIndexRegistry.register(this, this.zIndex);
|
||||
}
|
||||
|
||||
async disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
|
||||
// Cleanup
|
||||
if (this.cropper) {
|
||||
this.cropper.destroy();
|
||||
}
|
||||
|
||||
if (this.windowLayer) {
|
||||
await this.windowLayer.destroy();
|
||||
}
|
||||
|
||||
// Unregister from z-index registry
|
||||
zIndexRegistry.unregister(this);
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
return html`
|
||||
<div class="modal-container" @click=${(e: Event) => e.stopPropagation()}>
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title">
|
||||
${this.currentStep === 'crop' ? 'Adjust Image' : 'Success'}
|
||||
</h3>
|
||||
<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">
|
||||
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">Saving...</div>
|
||||
` : html`
|
||||
<img
|
||||
class="preview-image ${this.shape}"
|
||||
src="${this.croppedImage}"
|
||||
alt="Cropped preview"
|
||||
/>
|
||||
<div class="success-message">
|
||||
<dees-icon icon="lucide:check" iconSize="16"></dees-icon>
|
||||
<span>Looking good!</span>
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
${this.currentStep === 'crop' ? html`
|
||||
<dees-button type="destructive" size="sm" @click=${this.close}>
|
||||
Cancel
|
||||
</dees-button>
|
||||
<dees-button type="default" size="sm" @click=${this.handleCrop}>
|
||||
Save
|
||||
</dees-button>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
async firstUpdated() {
|
||||
if (this.currentStep === 'crop') {
|
||||
await this.initializeCropper();
|
||||
}
|
||||
}
|
||||
|
||||
private async initializeCropper(): Promise<void> {
|
||||
await this.updateComplete;
|
||||
|
||||
const container = this.shadowRoot!.getElementById('cropperContainer');
|
||||
if (!container) return;
|
||||
|
||||
this.cropper = new ImageCropper({
|
||||
container,
|
||||
image: this.initialImage,
|
||||
shape: this.shape,
|
||||
aspectRatio: 1,
|
||||
outputSize: this.outputSize,
|
||||
outputQuality: this.outputQuality,
|
||||
});
|
||||
|
||||
await this.cropper.initialize();
|
||||
}
|
||||
|
||||
private async handleCrop(): Promise<void> {
|
||||
if (!this.cropper) return;
|
||||
|
||||
try {
|
||||
this.isProcessing = true;
|
||||
this.currentStep = 'preview';
|
||||
await this.updateComplete;
|
||||
|
||||
// Get cropped image
|
||||
const croppedData = await this.cropper.getCroppedImage();
|
||||
this.croppedImage = croppedData;
|
||||
|
||||
// Simulate processing time for better UX
|
||||
await new Promise(resolve => setTimeout(resolve, 800));
|
||||
|
||||
this.isProcessing = false;
|
||||
|
||||
// Emit save event
|
||||
this.dispatchEvent(new CustomEvent('save', {
|
||||
detail: { croppedImage: this.croppedImage },
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
|
||||
// Auto close after showing success
|
||||
setTimeout(() => {
|
||||
this.close();
|
||||
}, 1500);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error cropping image:', error);
|
||||
this.isProcessing = false;
|
||||
}
|
||||
}
|
||||
|
||||
private close(): void {
|
||||
this.remove();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user