Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m54s
176 lines
5.0 KiB
React
176 lines
5.0 KiB
React
import { useState } from 'react'
|
|
import { IconChevronRight, IconDotsVertical } from '@tabler/icons-react'
|
|
import ContextMenu from './ContextMenu.jsx'
|
|
|
|
/**
|
|
* MobileCardView - компонент для отображения табличных данных в виде карточек на мобильных
|
|
* Поддерживает swipe gestures для действий
|
|
*/
|
|
function MobileCardView({
|
|
items,
|
|
onItemClick,
|
|
renderContent, // функция для рендера содержимого карточки
|
|
actions // массив действий { label, icon, onClick, variant }
|
|
}) {
|
|
const [swipedItem, setSwipedItem] = useState(null)
|
|
const [touchStart, setTouchStart] = useState(null)
|
|
const [touchEnd, setTouchEnd] = useState(null)
|
|
|
|
// минимальная дистанция свайпа в px
|
|
const minSwipeDistance = 50
|
|
|
|
const onTouchStart = (e, item) => {
|
|
setTouchEnd(null)
|
|
setTouchStart(e.targetTouches[0].clientX)
|
|
}
|
|
|
|
const onTouchMove = (e, item) => {
|
|
setTouchEnd(e.targetTouches[0].clientX)
|
|
}
|
|
|
|
const onTouchEnd = (item) => {
|
|
if (!touchStart || !touchEnd) return
|
|
|
|
const distance = touchStart - touchEnd
|
|
const isLeftSwipe = distance > minSwipeDistance
|
|
const isRightSwipe = distance < -minSwipeDistance
|
|
|
|
if (isLeftSwipe) {
|
|
setSwipedItem(item)
|
|
} else if (isRightSwipe) {
|
|
setSwipedItem(null)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="mobile-card-view d-md-none">
|
|
{items.map((item, index) => {
|
|
const isSwiped = swipedItem === item
|
|
|
|
return (
|
|
<div
|
|
key={item.id || index}
|
|
className={`mobile-card ${isSwiped ? 'swiped' : ''}`}
|
|
onTouchStart={(e) => onTouchStart(e, item)}
|
|
onTouchMove={(e) => onTouchMove(e, item)}
|
|
onTouchEnd={() => onTouchEnd(item)}
|
|
onClick={() => !isSwiped && onItemClick && onItemClick(item)}
|
|
>
|
|
<div className="mobile-card-content">
|
|
{renderContent(item)}
|
|
|
|
<div className="mobile-card-arrow">
|
|
<IconChevronRight size={20} />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Меню действий */}
|
|
<div className="mobile-card-actions">
|
|
<ContextMenu items={actions.map(action => ({
|
|
...action,
|
|
onClick: () => action.onClick(item)
|
|
}))}>
|
|
<button className="btn btn-icon btn-ghost-secondary">
|
|
<IconDotsVertical size={20} />
|
|
</button>
|
|
</ContextMenu>
|
|
</div>
|
|
|
|
{/* Swipe actions */}
|
|
{isSwiped && (
|
|
<div className="mobile-card-swipe-actions">
|
|
{actions.slice(0, 2).map((action, idx) => {
|
|
const Icon = action.icon
|
|
return (
|
|
<button
|
|
key={idx}
|
|
className={`btn btn-${action.variant || 'primary'} btn-icon`}
|
|
onClick={() => action.onClick(item)}
|
|
title={action.label}
|
|
>
|
|
{Icon && <Icon size={20} />}
|
|
</button>
|
|
)
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
})}
|
|
|
|
<style jsx>{`
|
|
.mobile-card-view {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.5rem;
|
|
padding: 0.5rem;
|
|
}
|
|
|
|
.mobile-card {
|
|
position: relative;
|
|
background: var(--tblr-bg-surface, white);
|
|
border: 1px solid var(--tblr-border-color, #e9ecef);
|
|
border-radius: 0.5rem;
|
|
overflow: hidden;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.mobile-card.swiped .mobile-card-content {
|
|
transform: translateX(-80px);
|
|
}
|
|
|
|
.mobile-card-content {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 1rem;
|
|
transition: transform 0.3s ease;
|
|
background: var(--tblr-bg-surface, white);
|
|
position: relative;
|
|
z-index: 2;
|
|
}
|
|
|
|
.mobile-card-content > :first-child {
|
|
flex: 1;
|
|
}
|
|
|
|
.mobile-card-arrow {
|
|
color: var(--tblr-muted, #6c757d);
|
|
margin-left: 0.5rem;
|
|
}
|
|
|
|
.mobile-card-actions {
|
|
position: absolute;
|
|
top: 0.5rem;
|
|
right: 0.5rem;
|
|
z-index: 3;
|
|
}
|
|
|
|
.mobile-card-swipe-actions {
|
|
position: absolute;
|
|
right: 0;
|
|
top: 0;
|
|
bottom: 0;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
padding: 0 0.5rem;
|
|
z-index: 1;
|
|
}
|
|
|
|
.mobile-card:active {
|
|
transform: scale(0.98);
|
|
}
|
|
|
|
@media (min-width: 768px) {
|
|
.mobile-card-view {
|
|
display: none;
|
|
}
|
|
}
|
|
`}</style>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default MobileCardView
|
|
|