mirror of
https://github.com/ClipFusion-org/clipfusion.git
synced 2025-08-03 16:55:08 +00:00
improved ux of swipe to delete (you can't click on the project for now tho)
This commit is contained in:
parent
c52cd12b3d
commit
2631fb79bb
@ -36,7 +36,7 @@ import Link from "next/link";
|
|||||||
import StickyTopContainer from "@/components/sticky-top-container";
|
import StickyTopContainer from "@/components/sticky-top-container";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import truncate from "@/lib/truncate";
|
import truncate from "@/lib/truncate";
|
||||||
import SwipeToDelete, { useSwipeToDeleteContext } from "@/components/swipe-to-delete";
|
import SwipeToDelete from "@/components/swipe-to-delete";
|
||||||
|
|
||||||
type SortingType = "byCreationDate"
|
type SortingType = "byCreationDate"
|
||||||
| "byEditDate"
|
| "byEditDate"
|
||||||
@ -450,10 +450,10 @@ const ProjectContainer = ({
|
|||||||
</AspectRatio>
|
</AspectRatio>
|
||||||
);
|
);
|
||||||
|
|
||||||
return isMobile ? (
|
return isMobile && !selecting ? (
|
||||||
<div className=" w-[100% + 5 * var(--spacing)] -mx-5 origin-center overflow-hidden">
|
<div className=" w-[100% + 5 * var(--spacing)] -mx-5 overflow-hidden">
|
||||||
<SwipeToDelete onDelete={() => deleteProject(project.uuid)} transitionDuration={200} deleteThreshold={50}>
|
<SwipeToDelete onDelete={() => deleteProject(project.uuid)}>
|
||||||
<div className="w-screen bg-background px-5 py-2 -mt-[1px] h-[calc(100% + 2px)]">
|
<div className="w-screen bg-background px-5 py-2">
|
||||||
{projectComponent}
|
{projectComponent}
|
||||||
</div>
|
</div>
|
||||||
</SwipeToDelete>
|
</SwipeToDelete>
|
||||||
@ -463,7 +463,6 @@ const ProjectContainer = ({
|
|||||||
|
|
||||||
export default function Home(): ReactNode {
|
export default function Home(): ReactNode {
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const { deleting } = useSwipeToDeleteContext();
|
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
const [debouncedSearch] = useDebounce(search, 300);
|
const [debouncedSearch] = useDebounce(search, 300);
|
||||||
const [selecting, setSelecting] = useState(false);
|
const [selecting, setSelecting] = useState(false);
|
||||||
|
@ -6,7 +6,6 @@ import ThemeProvider from "./theme-provider";
|
|||||||
import Analytics from "./analytics";
|
import Analytics from "./analytics";
|
||||||
import { Toaster } from "@/components/ui/sonner";
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
import PWAHead from "./pwa-head";
|
import PWAHead from "./pwa-head";
|
||||||
import { SwipeToDeleteContextProvider } from "@/components/swipe-to-delete";
|
|
||||||
|
|
||||||
const geist = Geist({
|
const geist = Geist({
|
||||||
variable: "--font-geist",
|
variable: "--font-geist",
|
||||||
@ -44,9 +43,7 @@ export default async function RootLayout({
|
|||||||
</head>
|
</head>
|
||||||
<body className={`${geist.variable} ${geistMono.variable} antialiased`}>
|
<body className={`${geist.variable} ${geistMono.variable} antialiased`}>
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<SwipeToDeleteContextProvider>
|
{children}
|
||||||
{children}
|
|
||||||
</SwipeToDeleteContextProvider>
|
|
||||||
<Toaster/>
|
<Toaster/>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</body>
|
</body>
|
||||||
|
@ -1,226 +1,192 @@
|
|||||||
'use client';
|
'use client'
|
||||||
import React, { createContext, Dispatch, ReactNode, SetStateAction, useCallback, useContext, useEffect, useRef, useState } from "react";
|
import { useRef, useState, useEffect, FC, ReactNode } from 'react'
|
||||||
import "./styles.css";
|
|
||||||
|
|
||||||
export interface Props {
|
type SwipeToDeleteProps = {
|
||||||
|
children: ReactNode;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
onDeleteConfirm?: (onSuccess: () => void, onCancel: () => void) => void;
|
height?: number ;
|
||||||
deleteComponent?: React.ReactNode;
|
backgroundClass?: string;
|
||||||
disabled?: boolean;
|
|
||||||
height?: number;
|
|
||||||
transitionDuration?: number;
|
|
||||||
deleteWidth?: number;
|
|
||||||
deleteThreshold?: number;
|
|
||||||
showDeleteAction?: boolean;
|
|
||||||
deleteColor?: string;
|
|
||||||
deleteText?: string;
|
deleteText?: string;
|
||||||
className?: string;
|
fadeOnDeletion?: boolean;
|
||||||
id?: string;
|
|
||||||
rtl?: boolean;
|
|
||||||
children?: React.ReactNode;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
const SwipeToDelete: FC<SwipeToDeleteProps> = ({
|
||||||
const cursorPosition = (event: any) => {
|
|
||||||
if (event?.touches?.[0]?.clientX) return event.touches[0].clientX;
|
|
||||||
if (event?.clientX) return event?.clientX;
|
|
||||||
if (event?.nativeEvent?.touches?.[0]?.clientX) return event.nativeEvent.touches[0].clientX;
|
|
||||||
return event?.nativeEvent?.clientX;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface SwipeToDeleteContextProps {
|
|
||||||
deleting: boolean;
|
|
||||||
setDeleting: Dispatch<SetStateAction<boolean>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SwipeToDeleteContext = createContext<SwipeToDeleteContextProps | undefined>(undefined);
|
|
||||||
|
|
||||||
export const useSwipeToDeleteContext = (): SwipeToDeleteContextProps => {
|
|
||||||
const data = useContext(SwipeToDeleteContext);
|
|
||||||
if (!data) throw new Error("SwipeToDeleteContext is not defined");
|
|
||||||
return data;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SwipeToDeleteContextProvider = ({
|
|
||||||
children
|
|
||||||
}: {
|
|
||||||
children: ReactNode
|
|
||||||
}) => {
|
|
||||||
const [deleting, setDeleting] = useState(false);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SwipeToDeleteContext.Provider value={{ deleting: deleting, setDeleting: setDeleting }}>
|
|
||||||
{children}
|
|
||||||
</SwipeToDeleteContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SwipeToDelete = ({
|
|
||||||
onDelete,
|
|
||||||
onDeleteConfirm,
|
|
||||||
deleteComponent,
|
|
||||||
disabled = false,
|
|
||||||
height = 50,
|
|
||||||
transitionDuration = 250,
|
|
||||||
deleteWidth = 75,
|
|
||||||
deleteThreshold = 75,
|
|
||||||
showDeleteAction = true,
|
|
||||||
deleteColor = "rgba(252, 58, 48, 1.00)",
|
|
||||||
deleteText = "Delete",
|
|
||||||
className = "",
|
|
||||||
id = "",
|
|
||||||
rtl = false,
|
|
||||||
children,
|
children,
|
||||||
}: Props) => {
|
onDelete,
|
||||||
const { deleting, setDeleting } = useSwipeToDeleteContext();
|
height = 60,
|
||||||
const [touching, setTouching] = useState(false);
|
backgroundClass = 'oklch(63.7% 0.237 25.331)',
|
||||||
const [translate, setTranslate] = useState(0);
|
deleteText = 'Delete',
|
||||||
const [internalDeleting, setInternalDeleting] = useState(false);
|
fadeOnDeletion = true
|
||||||
|
}) => {
|
||||||
const startTouchPosition = useRef(0);
|
|
||||||
const initTranslate = useRef(0);
|
|
||||||
const container = useRef<HTMLDivElement>(null);
|
const container = useRef<HTMLDivElement>(null);
|
||||||
const containerWidth: number = container.current?.getBoundingClientRect().width || 0;
|
const content = useRef<HTMLDivElement>(null);
|
||||||
const deleteWithoutConfirmThreshold: number = containerWidth * (deleteThreshold / 100);
|
const text = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
const onStart = useCallback(
|
// drag state
|
||||||
(event: React.TouchEvent | React.MouseEvent) => {
|
const [dragX, setDragX] = useState(0);
|
||||||
if (disabled) return;
|
const [dragging, setDragging] = useState(false);
|
||||||
if (touching) return;
|
const [startX, setStartX] = useState(0);
|
||||||
startTouchPosition.current = cursorPosition(event);
|
const [velocity, setVelocity] = useState(0);
|
||||||
initTranslate.current = translate;
|
const lastTimeRef = useRef<number>(0);
|
||||||
setTouching(true);
|
const lastXRef = useRef<number>(0);
|
||||||
setDeleting(true);
|
|
||||||
},
|
|
||||||
[disabled, touching, translate, deleting, setDeleting]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
// collapse state
|
||||||
// setDeleting(touching);
|
const [isCollapsing, setIsCollapsing] = useState(false);
|
||||||
}, [touching]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
// measure width and thresholds
|
||||||
const root = container.current;
|
const width = container.current?.offsetWidth ?? window.innerWidth;
|
||||||
root?.style.setProperty("--rstdiHeight", height + "px");
|
const threshold = width / 2;
|
||||||
root?.style.setProperty("--rstdiTransitionDuration", transitionDuration + "ms");
|
const rubberMax = width * 0.7;
|
||||||
root?.style.setProperty("--rstdiIsRtl", rtl ? "1" : "-1");
|
|
||||||
root?.style.setProperty("--rstdiDeleteColor", deleteColor);
|
|
||||||
root?.style.setProperty("--rstdiDeleteWidth", deleteWidth + "px");
|
|
||||||
}, [deleteColor, deleteWidth, height, rtl, transitionDuration, deleting]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
// rubber-band effect
|
||||||
const root = container.current;
|
const rubber = (delta: number, customRubberMax?: number) => {
|
||||||
root?.style.setProperty("--rstdiTranslate", translate * (rtl ? -1 : 1) + "px");
|
const max = customRubberMax ?? rubberMax;
|
||||||
const shiftDelete = -translate >= deleteWithoutConfirmThreshold;
|
const sign = delta < 0 ? -1 : 1;
|
||||||
root?.style.setProperty(
|
const abs = Math.abs(delta);
|
||||||
`--rstdiButtonMargin${rtl ? "Right" : "Left"}`,
|
if (abs <= max) return delta;
|
||||||
(shiftDelete ? containerWidth + translate : containerWidth - deleteWidth) + "px"
|
return sign * (max + Math.sqrt(abs - max));
|
||||||
);
|
}
|
||||||
}, [translate, deleteWidth, containerWidth, rtl, deleteWithoutConfirmThreshold]);
|
|
||||||
|
|
||||||
const onMove = useCallback(
|
// do we show the sticky delete inside content?
|
||||||
function (event: TouchEvent | MouseEvent) {
|
const isSticky = dragX < -rubberMax;
|
||||||
if (!touching) return;
|
|
||||||
if (!rtl && cursorPosition(event) > startTouchPosition.current - initTranslate.current)
|
|
||||||
return setTranslate(0);
|
|
||||||
if (rtl && cursorPosition(event) < startTouchPosition.current - initTranslate.current)
|
|
||||||
return setTranslate(0);
|
|
||||||
setTranslate(cursorPosition(event) - startTouchPosition.current + initTranslate.current);
|
|
||||||
},
|
|
||||||
[rtl, touching]
|
|
||||||
);
|
|
||||||
|
|
||||||
|
// pointer start
|
||||||
|
const handleStart = (pageX: number) => {
|
||||||
|
if (isCollapsing) return;
|
||||||
|
setDragging(true);
|
||||||
|
setStartX(pageX - dragX);
|
||||||
|
lastTimeRef.current = performance.now();
|
||||||
|
lastXRef.current = pageX;
|
||||||
|
content.current?.classList.remove('ios-ease');
|
||||||
|
};
|
||||||
|
|
||||||
const onMouseMove = useCallback(
|
// pointer move
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
const handleMove = (pageX: number) => {
|
||||||
function (event: MouseEvent): any {
|
if (!dragging) return;
|
||||||
onMove(event);
|
const now = performance.now();
|
||||||
},
|
const dt = now - lastTimeRef.current;
|
||||||
[onMove]
|
const dx = pageX - lastXRef.current;
|
||||||
);
|
setVelocity(dx / dt * 1000);
|
||||||
|
lastTimeRef.current = now;
|
||||||
|
lastXRef.current = pageX;
|
||||||
|
|
||||||
|
const raw = pageX - startX;
|
||||||
|
const x = dragX < 0 ? rubber(raw) : rubber(raw, width * 0.1);
|
||||||
|
setDragX(x);
|
||||||
|
};
|
||||||
|
|
||||||
const onTouchMove = useCallback(
|
const handleDelete = () => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// slide away
|
||||||
function (event: TouchEvent): any {
|
content.current?.classList.add('ios-ease');
|
||||||
onMove(event);
|
setDragX(-width);
|
||||||
},
|
|
||||||
[onMove]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onDeleteConfirmed = useCallback(() => {
|
// collapse after a slight delay (via CSS)
|
||||||
setInternalDeleting(() => true);
|
setIsCollapsing(true);
|
||||||
window.setTimeout(onDelete, transitionDuration);
|
setTimeout(onDelete, 300); // matches the CSS timings below
|
||||||
}, [onDelete, transitionDuration]);
|
};
|
||||||
|
|
||||||
const onDeleteCancel = useCallback(() => {
|
// pointer end
|
||||||
setTouching(() => false);
|
const handleEnd = () => {
|
||||||
setTranslate(() => 0);
|
if (!dragging) return;
|
||||||
setInternalDeleting(() => false);
|
setDragging(false);
|
||||||
startTouchPosition.current = 0;
|
|
||||||
initTranslate.current = 0;
|
|
||||||
}, [onDelete, transitionDuration]);
|
|
||||||
|
|
||||||
const onDeleteClick = useCallback(() => {
|
const shouldDelete =
|
||||||
if (onDeleteConfirm) {
|
Math.abs(dragX) > threshold ||
|
||||||
onDeleteConfirm(onDeleteConfirmed, onDeleteCancel);
|
velocity < -1000;
|
||||||
} else {
|
if (!shouldDelete) {
|
||||||
onDeleteConfirmed();
|
content.current?.classList.add('ios-ease');
|
||||||
|
text.current?.classList.add('ios-ease');
|
||||||
|
const textWidth = text.current ? text.current.getBoundingClientRect().width : 0;
|
||||||
|
if (dragX < -50 && text.current) setDragX(-textWidth * 1.5);
|
||||||
|
else setDragX(0);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}, [onDeleteConfirm, onDeleteConfirmed]);
|
|
||||||
|
|
||||||
const onMouseUp = useCallback(
|
handleDelete();
|
||||||
function () {
|
}
|
||||||
startTouchPosition.current = 0;
|
|
||||||
const acceptableMove = -deleteWidth * 0.7;
|
|
||||||
const showDelete = showDeleteAction ? (rtl ? -1 : 1) * translate < acceptableMove : false;
|
|
||||||
const notShowDelete = showDeleteAction ? (rtl ? -1 : 1) * translate >= acceptableMove : true;
|
|
||||||
const deleteWithoutConfirm = (rtl ? 1 : -1) * translate >= deleteWithoutConfirmThreshold;
|
|
||||||
if (deleteWithoutConfirm) {
|
|
||||||
setTranslate(() => -containerWidth);
|
|
||||||
} else if (notShowDelete) {
|
|
||||||
setTranslate(() => 0);
|
|
||||||
} else if (showDelete && !deleteWithoutConfirm) {
|
|
||||||
setTranslate(() => (rtl ? 1 : -1) * deleteWidth);
|
|
||||||
}
|
|
||||||
setTouching(() => false);
|
|
||||||
setDeleting(false);
|
|
||||||
if (deleteWithoutConfirm) onDeleteClick();
|
|
||||||
},
|
|
||||||
[containerWidth, deleteWidth, deleteWithoutConfirmThreshold, onDeleteClick, rtl, translate, deleting, setDeleting]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (touching) {
|
const node = container.current
|
||||||
window.addEventListener("mousemove", onMouseMove);
|
if (!node) return
|
||||||
window.addEventListener("touchmove", onTouchMove);
|
const onDown = (e: PointerEvent) => {
|
||||||
window.addEventListener("mouseup", onMouseUp);
|
node.setPointerCapture(e.pointerId)
|
||||||
window.addEventListener("touchend", onMouseUp);
|
handleStart(e.pageX)
|
||||||
} else {
|
|
||||||
window.removeEventListener("mousemove", onMouseMove);
|
|
||||||
window.removeEventListener("touchmove", onTouchMove);
|
|
||||||
window.removeEventListener("mouseup", onMouseUp);
|
|
||||||
window.removeEventListener("touchend", onMouseUp);
|
|
||||||
}
|
}
|
||||||
|
const onMove = (e: PointerEvent) => handleMove(e.pageX)
|
||||||
|
const onUp = (e: PointerEvent) => {
|
||||||
|
handleEnd()
|
||||||
|
node.releasePointerCapture(e.pointerId)
|
||||||
|
}
|
||||||
|
node.addEventListener('pointerdown', onDown)
|
||||||
|
node.addEventListener('pointermove', onMove)
|
||||||
|
node.addEventListener('pointerup', onUp)
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener("mousemove", onMouseMove);
|
node.removeEventListener('pointerdown', onDown)
|
||||||
window.removeEventListener("touchmove", onTouchMove);
|
node.removeEventListener('pointermove', onMove)
|
||||||
window.removeEventListener("mouseup", onMouseUp);
|
node.removeEventListener('pointerup', onUp)
|
||||||
window.removeEventListener("touchend", onMouseUp);
|
}
|
||||||
};
|
}, [dragging, dragX, velocity])
|
||||||
}, [onMouseMove, onMouseUp, onTouchMove, touching]);
|
|
||||||
|
const deleteTransform = isCollapsing
|
||||||
|
? `translateX(calc(${dragX}px + 5rem))`
|
||||||
|
: (isSticky ? `translateX(calc(${dragX}px + 5rem))` : `translateX(max(0rem, calc(${dragX}px + 5rem)))`);
|
||||||
|
|
||||||
|
const opacityTransparent = fadeOnDeletion ? 0 : 1;
|
||||||
|
|
||||||
|
const background = dragX < 0
|
||||||
|
? isCollapsing ? (opacityTransparent == 0 ? 'transparent' : backgroundClass) : backgroundClass
|
||||||
|
: 'transparent';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id={id} className={`rstdi${internalDeleting ? " deleting" : ""} ${className}`} ref={container}>
|
<div
|
||||||
<div className={`delete${internalDeleting ? " deleting" : ""}`}>
|
ref={container}
|
||||||
<button onClick={onDeleteClick}>{deleteComponent ? deleteComponent : deleteText}</button>
|
className="relative overflow-hidden select-none"
|
||||||
</div>
|
style={{
|
||||||
|
height: isCollapsing ? 0 : height,
|
||||||
|
transition: isCollapsing
|
||||||
|
? 'height 300ms cubic-bezier(0.24, 1.04, 0.56, 1)'
|
||||||
|
: undefined,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Fixed red background + delete text */}
|
||||||
<div
|
<div
|
||||||
className={`content${internalDeleting ? " deleting" : ""}${!touching ? " transition" : ""}`}
|
className={`absolute inset-0 flex items-center justify-end pr-4`}
|
||||||
onMouseDown={onStart}
|
style={{
|
||||||
onTouchStart={onStart}>
|
background: background,
|
||||||
|
transition: dragX > 1 ? '' : 'background 300ms'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<button className="text-white font-semibold h-full" style={{
|
||||||
|
transform: deleteTransform,
|
||||||
|
transition: 'transform 300ms cubic-bezier(0.24, 1.04, 0.56, 1), opacity 300ms',
|
||||||
|
opacity: isCollapsing ? opacityTransparent : 1
|
||||||
|
}} onClick={handleDelete} ref={text}>
|
||||||
|
{deleteText}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Swipeable content */}
|
||||||
|
<div
|
||||||
|
ref={content}
|
||||||
|
className="relative ios-ease"
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
inset: 0,
|
||||||
|
transform: `translateX(${dragX}px)`,
|
||||||
|
transition: dragging
|
||||||
|
? ''
|
||||||
|
: 'transform 300ms cubic-bezier(0.24, 1.04, 0.56, 1)',
|
||||||
|
touchAction: 'none',
|
||||||
|
pointerEvents: 'none',
|
||||||
|
willChange: 'transform',
|
||||||
|
backfaceVisibility: 'hidden',
|
||||||
|
transformStyle: 'preserve-3d'
|
||||||
|
}}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default SwipeToDelete;
|
@ -1,7 +1,3 @@
|
|||||||
import { SwipeToDelete, SwipeToDeleteContextProvider, useSwipeToDeleteContext } from "./SwipeToDelete";
|
import SwipeToDelete from "./SwipeToDelete";
|
||||||
|
|
||||||
export default SwipeToDelete;
|
export default SwipeToDelete;
|
||||||
export {
|
|
||||||
SwipeToDeleteContextProvider,
|
|
||||||
useSwipeToDeleteContext
|
|
||||||
};
|
|
@ -1,78 +1,8 @@
|
|||||||
/* rstdi = react-swipe-to-delete-ios */
|
/* iOS-style timing */
|
||||||
|
.ios-ease {
|
||||||
.rstdi {
|
transition-timing-function: cubic-bezier(0.24, 1.04, 0.56, 1);
|
||||||
--rstdiHeight: 30px;
|
|
||||||
--rstdiTransitionDuration: 250ms;
|
|
||||||
--rstdiTranslate: 0px;
|
|
||||||
--rstdiIsRtl: 0;
|
|
||||||
--rstdiDeleteColor: rgba(252, 58, 48, 1);
|
|
||||||
--rstdiDeleteWidth: 75px;
|
|
||||||
--rstdiButtonMarginRight: 0px;
|
|
||||||
--rstdiButtonMarginLeft: 0px;
|
|
||||||
|
|
||||||
width: auto;
|
|
||||||
position: relative;
|
|
||||||
box-sizing: border-box;
|
|
||||||
height: 100%;
|
|
||||||
max-height: 100%;
|
|
||||||
overscroll-behavior: none;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
/* Rubber-band “resistance”: smaller drag as you pull harder */
|
||||||
.rstdi *,
|
[data-rubber] {
|
||||||
.rstdi *:before,
|
touch-action: none;
|
||||||
.rstdi *:after {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rstdi.deleting {
|
|
||||||
transition: all var(--rstdiTransitionDuration) cubic-bezier(0.33, 1, 0.68, 1);
|
|
||||||
opacity: 0;
|
|
||||||
max-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rstdi .content {
|
|
||||||
height: 100%;
|
|
||||||
width: auto;
|
|
||||||
position: relative;
|
|
||||||
transform: translateX(var(--rstdiTranslate));
|
|
||||||
}
|
|
||||||
|
|
||||||
.rstdi .content.transition {
|
|
||||||
transition: all var(--rstdiTransitionDuration) cubic-bezier(0.33, 1, 0.68, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.rstdi .content.deleting {
|
|
||||||
height: 0%;
|
|
||||||
width: auto;
|
|
||||||
position: relative;
|
|
||||||
transform: translateX(-100%);
|
|
||||||
transition: all var(--rstdiTransitionDuration) cubic-bezier(0.33, 1, 0.68, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.rstdi .delete {
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
margin-top: 0px;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
top: 0;
|
|
||||||
background: var(--rstdiDeleteColor);
|
|
||||||
font-weight: 400;
|
|
||||||
display: inline-flex;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rstdi .delete button {
|
|
||||||
width: var(--rstdiDeleteWidth);
|
|
||||||
transition: transform var(--rstdiTransitionDuration) cubic-bezier(0.33, 1, 0.68, 1);
|
|
||||||
transform: translateX(var(--rstdiButtonMarginLeft));
|
|
||||||
text-align: center;
|
|
||||||
height: 100%;
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
color: white;
|
|
||||||
font-size: 1rem;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user