muspe-cli
Version:
MusPE Advanced Framework v2.1.3 - Mobile User-friendly Simple Progressive Engine with Enhanced CLI Tools, Specialized E-Commerce Templates, Material Design 3, Progressive Enhancement, Mobile Optimizations, Performance Analysis, and Enterprise-Grade Develo
475 lines (390 loc) • 11.4 kB
JavaScript
// MusPE UI - Action Sheet Component (iOS-style bottom sheet)
class ActionSheet {
constructor(options = {}) {
this.options = {
title: '',
message: '',
actions: [],
cancelButton: true,
destructiveButton: null,
closeOnBackdrop: true,
closeOnAction: true,
...options
};
this.element = null;
this.backdrop = null;
this.isOpen = false;
this.callbacks = {
onOpen: null,
onClose: null,
onAction: null,
...options.callbacks
};
}
render() {
// Create backdrop
this.backdrop = document.createElement('div');
this.backdrop.className = 'muspe-actionsheet-backdrop';
// Create action sheet
this.element = document.createElement('div');
this.element.className = 'muspe-actionsheet';
this.element.innerHTML = `
<div class="muspe-actionsheet__content">
${this.renderHeader()}
${this.renderActions()}
${this.renderCancel()}
</div>
`;
this.attachEvents();
return this.element;
}
renderHeader() {
if (!this.options.title && !this.options.message) return '';
return `
<div class="muspe-actionsheet__header">
${this.options.title ? `<h3 class="muspe-actionsheet__title">${this.options.title}</h3>` : ''}
${this.options.message ? `<p class="muspe-actionsheet__message">${this.options.message}</p>` : ''}
</div>
`;
}
renderActions() {
if (this.options.actions.length === 0) return '';
const actions = this.options.actions.map((action, index) => {
const classes = ['muspe-actionsheet__action'];
if (action.destructive) {
classes.push('muspe-actionsheet__action--destructive');
}
if (action.disabled) {
classes.push('muspe-actionsheet__action--disabled');
}
return `
<button class="${classes.join(' ')}" data-action="${index}" ${action.disabled ? 'disabled' : ''}>
${action.icon ? `<span class="muspe-actionsheet__icon">${action.icon}</span>` : ''}
<span class="muspe-actionsheet__text">${action.text}</span>
</button>
`;
}).join('');
return `<div class="muspe-actionsheet__actions">${actions}</div>`;
}
renderCancel() {
if (!this.options.cancelButton) return '';
const cancelText = typeof this.options.cancelButton === 'string' ?
this.options.cancelButton : 'Cancel';
return `
<div class="muspe-actionsheet__cancel">
<button class="muspe-actionsheet__cancel-btn" data-action="cancel">
${cancelText}
</button>
</div>
`;
}
attachEvents() {
if (!this.element) return;
// Action buttons
const actionButtons = this.element.querySelectorAll('[data-action]');
actionButtons.forEach(button => {
button.addEventListener('click', (e) => {
const actionIndex = e.target.closest('[data-action]').dataset.action;
if (actionIndex === 'cancel') {
this.close();
return;
}
const action = this.options.actions[parseInt(actionIndex)];
if (action && !action.disabled) {
if (action.callback) {
action.callback();
}
if (this.callbacks.onAction) {
this.callbacks.onAction(action, parseInt(actionIndex));
}
if (this.options.closeOnAction !== false) {
this.close();
}
}
});
});
// Backdrop click
if (this.backdrop && this.options.closeOnBackdrop) {
this.backdrop.addEventListener('click', (e) => {
if (e.target === this.backdrop) {
this.close();
}
});
}
// Swipe down to close
this.attachSwipeGesture();
}
attachSwipeGesture() {
let startY = 0;
let currentY = 0;
let isDragging = false;
const content = this.element.querySelector('.muspe-actionsheet__content');
if (!content) return;
const handleTouchStart = (e) => {
startY = e.touches[0].clientY;
isDragging = true;
content.style.transition = 'none';
};
const handleTouchMove = (e) => {
if (!isDragging) return;
currentY = e.touches[0].clientY;
const deltaY = currentY - startY;
if (deltaY > 0) {
content.style.transform = `translateY(${deltaY}px)`;
}
};
const handleTouchEnd = () => {
if (!isDragging) return;
isDragging = false;
content.style.transition = '';
const deltaY = currentY - startY;
if (deltaY > 100) {
this.close();
} else {
content.style.transform = 'translateY(0)';
}
};
content.addEventListener('touchstart', handleTouchStart, { passive: true });
content.addEventListener('touchmove', handleTouchMove, { passive: true });
content.addEventListener('touchend', handleTouchEnd, { passive: true });
}
open() {
if (this.isOpen) return;
// Add to DOM
document.body.appendChild(this.backdrop);
document.body.appendChild(this.element);
// Prevent body scroll
document.body.style.overflow = 'hidden';
// Force reflow
this.element.offsetHeight;
// Add open classes
this.backdrop.classList.add('muspe-actionsheet-backdrop--open');
this.element.classList.add('muspe-actionsheet--open');
this.isOpen = true;
if (this.callbacks.onOpen) {
this.callbacks.onOpen();
}
}
close() {
if (!this.isOpen) return;
// Remove open classes
this.backdrop.classList.remove('muspe-actionsheet-backdrop--open');
this.element.classList.remove('muspe-actionsheet--open');
// Wait for animation
setTimeout(() => {
// Remove from DOM
if (this.backdrop && this.backdrop.parentNode) {
this.backdrop.parentNode.removeChild(this.backdrop);
}
if (this.element && this.element.parentNode) {
this.element.parentNode.removeChild(this.element);
}
// Restore body scroll
document.body.style.overflow = '';
this.isOpen = false;
if (this.callbacks.onClose) {
this.callbacks.onClose();
}
}, 300);
}
destroy() {
if (this.isOpen) {
this.close();
}
this.element = null;
this.backdrop = null;
}
}
// CSS Styles for Action Sheet
const actionSheetStyles = `
.muspe-actionsheet-backdrop {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.4);
z-index: var(--muspe-z-modal);
opacity: 0;
transition: opacity var(--muspe-transition-normal);
}
.muspe-actionsheet-backdrop--open {
opacity: 1;
}
.muspe-actionsheet {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: calc(var(--muspe-z-modal) + 1);
transform: translateY(100%);
transition: transform var(--muspe-transition-normal);
}
.muspe-actionsheet--open {
transform: translateY(0);
}
.muspe-actionsheet__content {
background: var(--muspe-white);
border-radius: var(--muspe-radius-xl) var(--muspe-radius-xl) 0 0;
padding: var(--muspe-space-4) var(--muspe-space-4) env(safe-area-inset-bottom);
position: relative;
}
.muspe-actionsheet__content::before {
content: '';
position: absolute;
top: var(--muspe-space-2);
left: 50%;
transform: translateX(-50%);
width: 36px;
height: 4px;
background: var(--muspe-gray-300);
border-radius: 2px;
}
.muspe-actionsheet__header {
text-align: center;
padding: var(--muspe-space-4) 0;
border-bottom: 1px solid var(--muspe-gray-200);
margin-bottom: var(--muspe-space-4);
}
.muspe-actionsheet__title {
font-size: var(--muspe-font-size-lg);
font-weight: 600;
color: var(--muspe-gray-900);
margin: 0 0 var(--muspe-space-2) 0;
}
.muspe-actionsheet__message {
font-size: var(--muspe-font-size-sm);
color: var(--muspe-gray-600);
margin: 0;
line-height: 1.5;
}
.muspe-actionsheet__actions {
display: flex;
flex-direction: column;
gap: 1px;
background: var(--muspe-gray-200);
border-radius: var(--muspe-radius-lg);
overflow: hidden;
margin-bottom: var(--muspe-space-3);
}
.muspe-actionsheet__action {
display: flex;
align-items: center;
justify-content: center;
gap: var(--muspe-space-3);
background: var(--muspe-white);
color: var(--muspe-primary);
font-size: var(--muspe-font-size-lg);
font-weight: 500;
padding: var(--muspe-space-4);
border: none;
cursor: pointer;
transition: background-color var(--muspe-transition-fast);
min-height: 56px;
width: 100%;
}
.muspe-actionsheet__action:active {
background: var(--muspe-gray-100);
}
.muspe-actionsheet__action--destructive {
color: var(--muspe-error);
}
.muspe-actionsheet__action--disabled {
color: var(--muspe-gray-400);
cursor: not-allowed;
}
.muspe-actionsheet__action--disabled:active {
background: var(--muspe-white);
}
.muspe-actionsheet__icon {
font-size: 1.2em;
display: flex;
align-items: center;
}
.muspe-actionsheet__text {
flex: 1;
text-align: left;
}
.muspe-actionsheet__cancel {
background: var(--muspe-gray-200);
border-radius: var(--muspe-radius-lg);
overflow: hidden;
}
.muspe-actionsheet__cancel-btn {
display: flex;
align-items: center;
justify-content: center;
background: var(--muspe-white);
color: var(--muspe-gray-700);
font-size: var(--muspe-font-size-lg);
font-weight: 600;
padding: var(--muspe-space-4);
border: none;
cursor: pointer;
transition: background-color var(--muspe-transition-fast);
min-height: 56px;
width: 100%;
}
.muspe-actionsheet__cancel-btn:active {
background: var(--muspe-gray-100);
}
/* Dark mode */
(prefers-color-scheme: dark) {
.muspe-actionsheet__content {
background: var(--muspe-gray-800);
}
.muspe-actionsheet__content::before {
background: var(--muspe-gray-600);
}
.muspe-actionsheet__header {
border-bottom-color: var(--muspe-gray-700);
}
.muspe-actionsheet__title {
color: var(--muspe-gray-100);
}
.muspe-actionsheet__message {
color: var(--muspe-gray-400);
}
.muspe-actionsheet__actions {
background: var(--muspe-gray-700);
}
.muspe-actionsheet__action {
background: var(--muspe-gray-800);
}
.muspe-actionsheet__action:active {
background: var(--muspe-gray-700);
}
.muspe-actionsheet__cancel {
background: var(--muspe-gray-700);
}
.muspe-actionsheet__cancel-btn {
background: var(--muspe-gray-800);
color: var(--muspe-gray-300);
}
.muspe-actionsheet__cancel-btn:active {
background: var(--muspe-gray-700);
}
}
/* Safe area support */
(padding: max(0px)) {
.muspe-actionsheet__content {
padding-bottom: max(var(--muspe-space-4), env(safe-area-inset-bottom));
padding-left: max(var(--muspe-space-4), env(safe-area-inset-left));
padding-right: max(var(--muspe-space-4), env(safe-area-inset-right));
}
}
/* Reduced motion */
(prefers-reduced-motion: reduce) {
.muspe-actionsheet-backdrop,
.muspe-actionsheet {
transition: none;
}
}
`;
// Auto-inject styles
if (typeof document !== 'undefined') {
const styleSheet = document.createElement('style');
styleSheet.textContent = actionSheetStyles;
document.head.appendChild(styleSheet);
}
export default ActionSheet;