🐉 Drag and Drop scaffold

This commit is contained in:
2021-09-30 11:53:04 -07:00
parent c134d2fc49
commit ebd5a8b95d
72 changed files with 2112 additions and 90 deletions

View File

@@ -3,6 +3,7 @@ module.exports = {
"plugin:react/recommended", // Uses the recommended rules from @eslint-plugin-react
"plugin:@typescript-eslint/recommended", // Uses the recommended rules from the @typescript-eslint/eslint-plugin
"plugin:prettier/recommended", // Enables eslint-plugin-prettier and eslint-config-prettier. This will display prettier errors as ESLint errors.
"plugin:css-modules/recommended",
],
parser: "@typescript-eslint/parser",
parserOptions: {
@@ -12,7 +13,7 @@ module.exports = {
jsx: true, // Allows for the parsing of JSX
},
},
plugins: ["@typescript-eslint"],
plugins: ["@typescript-eslint", "css-modules"],
settings: {
"import/resolver": {
node: {

24
declarations.d.ts vendored Normal file
View File

@@ -0,0 +1,24 @@
declare module "*.module.css" {
const classes: { [key: string]: string };
export default classes;
}
declare module "*.module.scss" {
const classes: { [key: string]: string };
export default classes;
}
declare module "*.module.sass" {
const classes: { [key: string]: string };
export default classes;
}
declare module "*.module.less" {
const classes: { [key: string]: string };
export default classes;
}
declare module "*.module.styl" {
const classes: { [key: string]: string };
export default classes;
}

View File

@@ -18,6 +18,9 @@
"dependencies": {
"@babel/runtime": "^7.13.17",
"@bluelovers/fast-glob": "https://github.com/rishighan/fast-glob-v2-api.git",
"@dnd-kit/core": "^4.0.0",
"@dnd-kit/modifiers": "^4.0.0",
"@dnd-kit/sortable": "^5.0.0",
"@types/event-stream": "^3.3.34",
"@types/mime-types": "^2.1.0",
"@types/react": "^17.0.3",
@@ -112,10 +115,12 @@
"concurrently": "^4.0.0",
"connected-react-router": "^6.9.1",
"css-loader": "^5.1.2",
"css-modules-typescript-loader": "^4.0.1",
"eslint": "^7.22.0",
"eslint-config-airbnb": "^18.2.1",
"eslint-config-airbnb-base": "^14.2.1",
"eslint-config-prettier": "^8.1.0",
"eslint-plugin-css-modules": "^2.11.0",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-jsx-a11y": "^6.0.3",
"eslint-plugin-prettier": "^3.3.1",

View File

@@ -294,24 +294,36 @@ $border-color: red;
}
}
}
.carousel-status {
position: absolute;
background-color: #999;
text-shadow: none;
border-radius: 5px;
top: 10px;
right: 30px;
padding: 5px;
font-size: 10px;
color: #fff;
}
// next and prev controls
.control-next {
border-top-right-radius: 10px;
border-bottom-right-radius: 10px;
&:hover {
border-top-right-radius: 10px;
border-bottom-right-radius: 10px;
&:hover {
border-top-right-radius: 10px;
border-bottom-right-radius: 10px;
}
}
}
.control-prev {
border-top-left-radius: 10px;
border-bottom-left-radius: 10px;
&:hover {
border-top-left-radius: 10px;
border-bottom-left-radius: 10px;
&:hover {
border-top-left-radius: 10px;
border-bottom-left-radius: 10px;
}
}
}
}
// airdcpp downloads tab
.tabs {

View File

@@ -7,21 +7,16 @@ import ComicVineSearchForm from "./ComicVineSearchForm";
import AcquisitionPanel from "./AcquisitionPanel";
import DownloadsPanel from "./DownloadsPanel";
import SlidingPane from "react-sliding-pane";
import "react-responsive-carousel/lib/styles/carousel.min.css";
import { Carousel } from "react-responsive-carousel";
import Select, { components } from "react-select";
import { RemovableItems } from "./SortableGrid/SortableGrid";
import "react-sliding-pane/dist/react-sliding-pane.css";
import "react-loader-spinner/dist/loader/css/react-spinner-loader.css";
import Loader from "react-loader-spinner";
import { isEmpty, isUndefined, isNil, map } from "lodash";
import { isEmpty, isUndefined, isNil } from "lodash";
import { RootState } from "threetwo-ui-typings";
import { fetchComicVineMatches } from "../actions/fileops.actions";
import {
extractComicArchive,
getComicBookDetailById,
} from "../actions/comicinfo.actions";
import { getComicBookDetailById } from "../actions/comicinfo.actions";
import { detectTradePaperbacks } from "../shared/utils/tradepaperback.utils";
import dayjs from "dayjs";
const prettyBytes = require("pretty-bytes");
@@ -136,43 +131,7 @@ export const ComicDetail = ({}: ComicDetailProps): ReactElement => {
},
},
editComicArchive: {
content: () => (
<>
<div className="content">
<span className="tags has-addons">
<span className="tag">Pages</span>
<span className="tag is-warning">
{extractedComicBookArchive.length}
</span>
</span>
</div>
<Carousel
width={300}
dynamicHeight
showStatus={false}
showIndicators={false}
showThumbs={false}
useKeyboardArrows
>
{map(extractedComicBookArchive, (page, idx) => {
return (
<div key={idx}>
<img
src={
"http://localhost:3000/" +
page.containedIn +
"/" +
page.name +
page.extension
}
alt=""
/>
</div>
);
})}
</Carousel>
</>
),
content: () => <></>,
},
};
@@ -182,25 +141,6 @@ export const ComicDetail = ({}: ComicDetailProps): ReactElement => {
setVisible(true);
}, [dispatch, comicBookDetailData]);
const openDrawerForEditingComicArchive = useCallback(() => {
dispatch(
extractComicArchive(
comicBookDetailData.rawFileDetails.containedIn +
"/" +
comicBookDetailData.rawFileDetails.name +
comicBookDetailData.rawFileDetails.extension,
{
extractTarget: "book",
targetExtractionFolder:
"./userdata/expanded/" + comicBookDetailData.rawFileDetails.name,
extractionMode: "all",
},
),
);
setSlidingPanelContentId("editComicArchive");
setVisible(true);
}, [dispatch, comicBookDetailData, extractedComicBookArchive]);
const [active, setActive] = useState(1);
const createDescriptionMarkup = (html) => {
return { __html: html };
@@ -274,9 +214,13 @@ export const ComicDetail = ({}: ComicDetailProps): ReactElement => {
},
{
id: 2,
icon: <i className="fas fa-puzzle-piece"></i>,
name: "Other Metadata",
content: <div key={2}>bastard</div>,
icon: <i className="fas fa-file-archive"></i>,
name: "Archive Operations",
content: (
<div key={2}>
<RemovableItems />
</div>
),
},
{
id: 3,
@@ -449,7 +393,7 @@ export const ComicDetail = ({}: ComicDetailProps): ReactElement => {
};
// Determine which cover image to use:
// 1. from the locally imported, non-CV-scraped version, or
// 1. from the locally imported or
// 2. from the CV-scraped version
let imagePath = "";
let comicBookTitle = "";
@@ -506,9 +450,6 @@ export const ComicDetail = ({}: ComicDetailProps): ReactElement => {
case "match-on-comic-vine":
openDrawerWithCVMatches();
break;
case "edit-comic-archive":
openDrawerForEditingComicArchive();
break;
default:
console.log("No valid action selected.");
break;

View File

@@ -0,0 +1,329 @@
import React, { useEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
import {
Announcements,
closestCenter,
CollisionDetection,
DragOverlay,
DndContext,
DropAnimation,
defaultDropAnimation,
KeyboardSensor,
Modifiers,
MouseSensor,
MeasuringConfiguration,
PointerActivationConstraint,
ScreenReaderInstructions,
TouchSensor,
UniqueIdentifier,
useSensor,
useSensors,
} from "@dnd-kit/core";
import {
arrayMove,
useSortable,
SortableContext,
sortableKeyboardCoordinates,
SortingStrategy,
rectSortingStrategy,
AnimateLayoutChanges,
} from "@dnd-kit/sortable";
import { createRange } from "./utilities";
import { Item, List, Wrapper } from "./components";
export interface Props {
activationConstraint?: PointerActivationConstraint;
animateLayoutChanges?: AnimateLayoutChanges;
adjustScale?: boolean;
collisionDetection?: CollisionDetection;
Container?: any; // To-do: Fix me
dropAnimation?: DropAnimation | null;
itemCount?: number;
items?: string[];
handle?: boolean;
measuring?: MeasuringConfiguration;
modifiers?: Modifiers;
renderItem?: any;
removable?: boolean;
strategy?: SortingStrategy;
useDragOverlay?: boolean;
getItemStyles?(args: {
id: UniqueIdentifier;
index: number;
isSorting: boolean;
isDragOverlay: boolean;
overIndex: number;
isDragging: boolean;
}): React.CSSProperties;
wrapperStyle?(args: {
index: number;
isDragging: boolean;
id: string;
}): React.CSSProperties;
isDisabled?(id: UniqueIdentifier): boolean;
}
const defaultDropAnimationConfig: DropAnimation = {
...defaultDropAnimation,
dragSourceOpacity: 0.5,
};
const screenReaderInstructions: ScreenReaderInstructions = {
draggable: `
To pick up a sortable item, press the space bar.
While sorting, use the arrow keys to move the item.
Press space again to drop the item in its new position, or press escape to cancel.
`,
};
export function Sortable({
activationConstraint,
animateLayoutChanges,
adjustScale = false,
Container = List,
collisionDetection = closestCenter,
dropAnimation = defaultDropAnimationConfig,
getItemStyles = () => ({}),
handle = false,
itemCount = 16,
items: initialItems,
isDisabled = () => false,
measuring,
modifiers,
removable,
renderItem,
strategy = rectSortingStrategy,
useDragOverlay = true,
wrapperStyle = () => ({}),
}: Props) {
const [items, setItems] = useState<string[]>(
() =>
initialItems ??
createRange<string>(itemCount, (index) => (index + 1).toString()),
);
const [activeId, setActiveId] = useState<string | null>(null);
const sensors = useSensors(
useSensor(MouseSensor, {
activationConstraint,
}),
useSensor(TouchSensor, {
activationConstraint,
}),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
}),
);
const isFirstAnnouncement = useRef(true);
const getIndex = items.indexOf.bind(items);
const getPosition = (id: string) => getIndex(id) + 1;
const activeIndex = activeId ? getIndex(activeId) : -1;
const handleRemove = removable
? (id: string) => setItems((items) => items.filter((item) => item !== id))
: undefined;
const announcements: Announcements = {
onDragStart(id) {
return `Picked up sortable item ${id}. Sortable item ${id} is in position ${getPosition(
id,
)} of ${items.length}`;
},
onDragOver(id, overId) {
// In this specific use-case, the picked up item's `id` is always the same as the first `over` id.
// The first `onDragOver` event therefore doesn't need to be announced, because it is called
// immediately after the `onDragStart` announcement and is redundant.
if (isFirstAnnouncement.current === true) {
isFirstAnnouncement.current = false;
return;
}
if (overId) {
return `Sortable item ${id} was moved into position ${getPosition(
overId,
)} of ${items.length}`;
}
return;
},
onDragEnd(id, overId) {
if (overId) {
return `Sortable item ${id} was dropped at position ${getPosition(
overId,
)} of ${items.length}`;
}
return;
},
onDragCancel(id) {
return `Sorting was cancelled. Sortable item ${id} was dropped and returned to position ${getPosition(
id,
)} of ${items.length}.`;
},
};
useEffect(() => {
if (!activeId) {
isFirstAnnouncement.current = true;
}
}, [activeId]);
return (
<DndContext
announcements={announcements}
screenReaderInstructions={screenReaderInstructions}
sensors={sensors}
collisionDetection={collisionDetection}
onDragStart={({ active }) => {
if (!active) {
return;
}
setActiveId(active.id);
}}
onDragEnd={({ over }) => {
setActiveId(null);
if (over) {
const overIndex = getIndex(over.id);
if (activeIndex !== overIndex) {
setItems((items) => arrayMove(items, activeIndex, overIndex));
}
}
}}
onDragCancel={() => setActiveId(null)}
measuring={measuring}
modifiers={modifiers}
>
<Wrapper center>
<SortableContext items={items} strategy={strategy}>
<Container>
{items.map((value, index) => (
<SortableItem
key={value}
id={value}
handle={handle}
index={index}
style={getItemStyles}
wrapperStyle={wrapperStyle}
disabled={isDisabled(value)}
renderItem={renderItem}
onRemove={handleRemove}
animateLayoutChanges={animateLayoutChanges}
useDragOverlay={useDragOverlay}
/>
))}
</Container>
</SortableContext>
</Wrapper>
{useDragOverlay
? createPortal(
<DragOverlay
adjustScale={adjustScale}
dropAnimation={dropAnimation}
>
{activeId ? (
<Item
value={items[activeIndex]}
handle={handle}
renderItem={renderItem}
wrapperStyle={wrapperStyle({
index: activeIndex,
isDragging: true,
id: items[activeIndex],
})}
style={getItemStyles({
id: items[activeIndex],
index: activeIndex,
isSorting: activeId !== null,
isDragging: true,
overIndex: -1,
isDragOverlay: true,
})}
dragOverlay
/>
) : null}
</DragOverlay>,
document.body,
)
: null}
</DndContext>
);
}
interface SortableItemProps {
animateLayoutChanges?: AnimateLayoutChanges;
disabled?: boolean;
id: string;
index: number;
handle: boolean;
useDragOverlay?: boolean;
onRemove?(id: string): void;
style(values: any): React.CSSProperties;
renderItem?(args: any): React.ReactElement;
wrapperStyle({
index,
isDragging,
id,
}: {
index: number;
isDragging: boolean;
id: string;
}): React.CSSProperties;
}
export function SortableItem({
disabled,
animateLayoutChanges,
id,
index,
handle,
onRemove,
style,
renderItem,
useDragOverlay,
wrapperStyle,
}: SortableItemProps) {
const {
attributes,
isDragging,
isSorting,
listeners,
overIndex,
setNodeRef,
transform,
transition,
} = useSortable({
animateLayoutChanges,
id,
disabled,
});
return (
<Item
ref={setNodeRef}
value={id}
disabled={disabled}
dragging={isDragging}
sorting={isSorting}
handle={handle}
renderItem={renderItem}
index={index}
style={style({
index,
id,
isDragging,
isSorting,
overIndex,
})}
onRemove={onRemove ? () => onRemove(id) : undefined}
transform={transform}
transition={!useDragOverlay && isDragging ? "none" : transition}
wrapperStyle={wrapperStyle({ index, isDragging, id })}
listeners={listeners}
data-index={index}
data-id={id}
dragOverlay={!useDragOverlay && isDragging}
{...attributes}
/>
);
}

View File

@@ -0,0 +1,141 @@
import React from "react";
import { MeasuringStrategy } from "@dnd-kit/core";
import {
AnimateLayoutChanges,
defaultAnimateLayoutChanges,
rectSortingStrategy,
} from "@dnd-kit/sortable";
import { Sortable, Props as SortableProps } from "./Sortable";
import { GridContainer } from "./components";
export default {
title: "Presets/Sortable/Grid",
};
const props: Partial<SortableProps> = {
adjustScale: true,
Container: (props: any) => <GridContainer {...props} columns={5} />,
strategy: rectSortingStrategy,
wrapperStyle: () => ({
width: 140,
height: 140,
}),
};
export const BasicSetup = () => <Sortable {...props} />;
export const WithoutDragOverlay = () => (
<Sortable {...props} useDragOverlay={false} />
);
export const LargeFirstTile = () => (
<Sortable
{...props}
getItemStyles={({ index }) => {
if (index === 0) {
return {
fontSize: "2rem",
padding: "36px 40px",
};
}
return {};
}}
wrapperStyle={({ index }) => {
if (index === 0) {
return {
height: 288,
gridRowStart: "span 2",
gridColumnStart: "span 2",
};
}
return {
width: 140,
height: 140,
};
}}
/>
);
export const VariableSizes = () => (
<Sortable
{...props}
itemCount={14}
getItemStyles={({ index }) => {
if (index === 0 || index === 9) {
return {
fontSize: "2rem",
padding: "36px 40px",
};
}
return {};
}}
wrapperStyle={({ index }) => {
if (index === 0 || index === 9) {
return {
height: 288,
gridRowStart: "span 2",
gridColumnStart: "span 2",
};
}
return {
width: 140,
height: 140,
};
}}
/>
);
export const DragHandle = () => <Sortable {...props} handle />;
export const ScrollContainer = () => (
<div
style={{
height: "50vh",
margin: "0 auto",
overflow: "auto",
}}
>
<Sortable {...props} />
</div>
);
export const PressDelay = () => (
<Sortable
{...props}
activationConstraint={{
delay: 250,
tolerance: 5,
}}
/>
);
export const MinimumDistance = () => (
<Sortable
{...props}
activationConstraint={{
distance: 15,
}}
/>
);
export const RemovableItems = () => {
const animateLayoutChanges: AnimateLayoutChanges = (args) =>
args.isSorting || args.wasDragging
? defaultAnimateLayoutChanges(args)
: true;
return (
<Sortable
{...props}
animateLayoutChanges={animateLayoutChanges}
measuring={{ droppable: { strategy: MeasuringStrategy.Always } }}
removable
handle={false}
/>
);
};

View File

@@ -0,0 +1,27 @@
.Button {
padding: 14px 20px;
border: none;
background-color: #242836;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
color: #f6f8ff;
cursor: pointer;
outline: none;
transform: scale(1);
transition: transform 0.2s, background 0.4s;
&:hover,
&:focus {
background-color: #2f3545;
transform: scale(1.02);
}
&:focus {
box-shadow: 0 0 0 4px #4c9ffe;
}
&:active {
transform: scale(0.95);
}
}

View File

@@ -0,0 +1,7 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'Button': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -0,0 +1,16 @@
import React, {HTMLAttributes} from 'react';
import classNames from 'classnames';
import styles from './Button.module.css';
export interface Props extends HTMLAttributes<HTMLButtonElement> {
children: React.ReactNode;
}
export function Button({children, ...props}: Props) {
return (
<button className={classNames(styles.Button)} {...props}>
{children}
</button>
);
}

View File

@@ -0,0 +1 @@
export {Button} from './Button';

View File

@@ -0,0 +1,42 @@
.ConfirmModal {
--width: 250px;
--height: 120px;
display: flex;
flex-direction: column;
width: var(--width);
height: var(--height);
position: fixed;
top: calc(100vh / 2 - var(--height) / 2);
left: calc(100vw / 2 - var(--width) / 2);
border-radius: 10px;
box-shadow: -1px 0 15px 0 rgba(34, 33, 81, 0.01),
0px 15px 15px 0 rgba(34, 33, 81, 0.25);
padding: 15px;
box-sizing: border-box;
background-color: white;
text-align: center;
z-index: 1;
h1 {
flex: 1;
font-size: 16px;
font-weight: normal;
line-height: 20px;
}
button {
width: 50px;
height: 23px;
background: #242836;
color: #f6f8ff;
border: none;
border-radius: 3px;
margin: 0 5px;
cursor: pointer;
&:hover {
background-color: #2f3545;
}
}
}

View File

@@ -0,0 +1,7 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'ConfirmModal': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -0,0 +1,21 @@
import React, {PropsWithChildren} from 'react';
import styles from './ConfirmModal.module.css';
interface Props {
onConfirm(): void;
onDeny(): void;
}
export const ConfirmModal = ({
onConfirm,
onDeny,
children,
}: PropsWithChildren<Props>) => (
<div className={styles.ConfirmModal}>
<h1>{children}</h1>
<div>
<button onClick={onConfirm}>Yes</button>
<button onClick={onDeny}>No</button>
</div>
</div>
);

View File

@@ -0,0 +1 @@
export {ConfirmModal} from './ConfirmModal';

View File

@@ -0,0 +1,103 @@
.Container {
display: flex;
flex-direction: column;
grid-auto-rows: max-content;
overflow: hidden;
box-sizing: border-box;
appearance: none;
outline: none;
min-width: 350px;
margin: 10px;
border-radius: 5px;
min-height: 200px;
transition: background-color 350ms ease;
background-color: rgba(246, 246, 246, 1);
border: 1px solid rgba(0, 0, 0, 0.05);
font-size: 1em;
ul {
display: grid;
grid-gap: 10px;
grid-template-columns: repeat(var(--columns, 1), 1fr);
list-style: none;
padding: 20px;
margin: 0;
}
&.scrollable {
ul {
overflow-y: auto;
}
}
&.placeholder {
justify-content: center;
align-items: center;
cursor: pointer;
color: rgba(0, 0, 0, 0.5);
background-color: transparent;
border-style: dashed;
border-color: rgba(0, 0, 0, 0.08);
&:hover {
border-color: rgba(0, 0, 0, 0.15);
}
}
&.hover {
background-color: rgb(235, 235, 235, 1);
}
&.unstyled {
overflow: visible;
background-color: transparent !important;
border: none !important;
}
&.horizontal {
width: 100%;
ul {
grid-auto-flow: column;
}
}
&.shadow {
box-shadow: 0 1px 10px 0 rgba(34, 33, 81, 0.1);
}
&:focus-visible {
border-color: transparent;
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0), 0 0px 0px 2px #4c9ffe;
}
}
.Header {
display: flex;
padding: 5px 20px;
padding-right: 8px;
align-items: center;
justify-content: space-between;
background-color: #fff;
border-top-left-radius: 5px;
border-top-right-radius: 5px;
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
&:hover {
.Actions > * {
opacity: 1 !important;
}
}
}
.Actions {
display: flex;
> *:first-child:not(:last-child) {
opacity: 0;
&:focus-visible {
opacity: 1;
}
}
}

View File

@@ -0,0 +1,15 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'Actions': string;
'Container': string;
'Header': string;
'horizontal': string;
'hover': string;
'placeholder': string;
'scrollable': string;
'shadow': string;
'unstyled': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -0,0 +1,81 @@
import React, {forwardRef} from 'react';
import classNames from 'classnames';
import {Handle, Remove} from '../Item';
import styles from './Container.module.css';
export interface Props {
children: React.ReactNode;
columns?: number;
label?: string;
style?: React.CSSProperties;
horizontal?: boolean;
hover?: boolean;
handleProps?: React.HTMLAttributes<any>;
scrollable?: boolean;
shadow?: boolean;
placeholder?: boolean;
unstyled?: boolean;
onClick?(): void;
onRemove?(): void;
}
export const Container = forwardRef<HTMLDivElement, Props>(
(
{
children,
columns = 1,
handleProps,
horizontal,
hover,
onClick,
onRemove,
label,
placeholder,
style,
scrollable,
shadow,
unstyled,
...props
}: Props,
ref
) => {
const Component = onClick ? 'button' : 'div';
return (
<Component
{...props}
ref={ref}
style={
{
...style,
'--columns': columns,
} as React.CSSProperties
}
className={classNames(
styles.Container,
unstyled && styles.unstyled,
horizontal && styles.horizontal,
hover && styles.hover,
placeholder && styles.placeholder,
scrollable && styles.scrollable,
shadow && styles.shadow
)}
onClick={onClick}
tabIndex={onClick ? 0 : undefined}
>
{label ? (
<div className={styles.Header}>
{label}
<div className={styles.Actions}>
{onRemove ? <Remove onClick={onRemove} /> : undefined}
<Handle {...handleProps} />
</div>
</div>
) : null}
{placeholder ? children : <ul>{children}</ul>}
</Component>
);
}
);

View File

@@ -0,0 +1,2 @@
export {Container} from './Container';
export type {Props as ContainerProps} from './Container';

View File

@@ -0,0 +1,21 @@
import React from "react";
import { useDraggable } from "@dnd-kit/core";
export const Draggable = (props) => {
const { attributes, listeners, setNodeRef, transform } = useDraggable({
id: "draggable",
});
const style = transform
? {
transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,
}
: undefined;
return (
<button ref={setNodeRef} style={style} {...listeners} {...attributes}>
{props.children}
</button>
);
};
export default Draggable;

View File

@@ -0,0 +1,128 @@
.Draggable {
position: relative;
display: flex;
flex-direction: column;
justify-content: center;
transition: transform 250ms ease;
transform: translate3d(var(--translate-x, 0), var(--translate-y, 0), 0);
button {
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto;
min-height: 54px;
flex-shrink: 1;
appearance: none;
outline: none;
border: 0;
padding: 8px 18px;
background-color: #181a22;
border-radius: 5px;
box-shadow: var(--box-shadow);
transform: scale(var(--scale, 1));
transition: transform 250ms cubic-bezier(0.18, 0.67, 0.6, 1.22),
box-shadow 300ms ease;
}
&:not(.handle) {
button {
touch-action: none;
cursor: grab;
&:focus-visible:not(.active &) {
box-shadow: 0 0 0 3px #4c9ffe;
}
}
}
&.handle {
button {
--action-background: rgba(255, 255, 255, 0.1);
> svg {
margin-right: 5px;
}
> button {
margin-right: -10px;
}
}
}
img {
width: 140px;
user-select: none;
pointer-events: none;
}
label {
display: block;
flex-shrink: 1;
padding: 10px;
transition: opacity 250ms ease;
text-align: center;
font-size: 1rem;
font-weight: 300;
color: #8d8d8d;
user-select: none;
cursor: url('/cursor.svg'), auto;
animation-name: pulse;
animation-duration: 1.5s;
animation-delay: 2s;
animation-iteration-count: infinite;
animation-timing-function: ease;
animation-direction: alternate;
}
&.dragging {
z-index: 1;
transition: none;
* {
cursor: grabbing;
}
button {
--scale: 1.06;
--box-shadow: -1px 0 15px 0 rgba(34, 33, 81, 0.01),
0px 15px 15px 0 rgba(34, 33, 81, 0.25);
&:focus-visible {
--box-shadow: 0 0px 10px 2px #4c9ffe;
}
}
label {
animation: none;
opacity: 0;
}
}
&.dragOverlay {
button {
animation: pop 250ms cubic-bezier(0.18, 0.67, 0.6, 1.22);
}
}
}
@keyframes pulse {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes pop {
0% {
transform: scale(1);
}
100% {
transform: scale(var(--scale));
box-shadow: var(--box-shadow);
}
}

View File

@@ -0,0 +1,79 @@
import React, {forwardRef} from 'react';
import classNames from 'classnames';
import type {DraggableSyntheticListeners, Translate} from '@dnd-kit/core';
import {Handle} from '../Item/components/Handle';
import {
draggable,
draggableHorizontal,
draggableVertical,
} from './draggable-svg';
import styles from './Draggable.module.css';
export enum Axis {
All,
Vertical,
Horizontal,
}
interface Props {
axis?: Axis;
dragOverlay?: boolean;
dragging?: boolean;
handle?: boolean;
label?: string;
listeners?: DraggableSyntheticListeners;
style?: React.CSSProperties;
translate?: Translate;
}
export const Draggable = forwardRef<HTMLButtonElement, Props>(
function Draggable(
{
axis,
dragOverlay,
dragging,
handle,
label,
listeners,
translate,
...props
},
ref
) {
return (
<div
className={classNames(
styles.Draggable,
dragOverlay && styles.dragOverlay,
dragging && styles.dragging,
handle && styles.handle
)}
style={
{
'--translate-x': `${translate?.x ?? 0}px`,
'--translate-y': `${translate?.y ?? 0}px`,
} as React.CSSProperties
}
>
<button
ref={ref}
{...props}
aria-label="Draggable"
data-cypress="draggable-item"
{...(handle ? {} : listeners)}
tabIndex={handle ? -1 : undefined}
>
{axis === Axis.Vertical
? draggableVertical
: axis === Axis.Horizontal
? draggableHorizontal
: draggable}
{handle ? <Handle {...(handle ? listeners : {})} /> : null}
</button>
{label ? <label>{label}</label> : null}
</div>
);
}
);

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
export { Axis, Draggable } from "./Draggable";

View File

@@ -0,0 +1,19 @@
import React from "react";
import { useDroppable } from "@dnd-kit/core";
export const Droppable = (props) => {
const { isOver, setNodeRef } = useDroppable({
id: "droppable",
});
const style = {
color: isOver ? "green" : undefined,
};
return (
<div ref={setNodeRef} style={style}>
{props.children}
</div>
);
};
export default Droppable;

View File

@@ -0,0 +1,51 @@
.Droppable {
position: relative;
padding-top: 80px;
text-align: center;
border-radius: 10px;
width: 340px;
height: 340px;
box-sizing: border-box;
background-color: #fff;
box-shadow: inset rgba(201, 211, 219, 0.5) 0 0 0 2px,
rgba(255, 255, 255, 0) 0 0 0 1px, rgba(201, 211, 219, 0.25) 20px 14px 24px;
transition: box-shadow 250ms ease;
> svg {
position: absolute;
left: 50%;
top: 50%;
width: 200px;
transform: translate3d(-50%, -50%, 0);
opacity: 0.8;
transition: opacity 300ms ease, transform 200ms ease;
user-select: none;
pointer-events: none;
}
&.dragging {
> svg {
opacity: 0.8;
}
}
&.over {
box-shadow: inset #1eb99d 0 0 0 3px, rgba(201, 211, 219, 0.5) 20px 14px 24px;
> svg {
opacity: 1;
}
&.dropped {
box-shadow: inset rgba(201, 211, 219, 0.7) 0 0 0 3px,
rgba(201, 211, 219, 0.5) 20px 14px 24px;
}
}
&.dropped {
> svg {
opacity: 0.2;
transform: translate3d(-50%, 100%, 0) scale(0.8);
}
}
}

View File

@@ -0,0 +1,34 @@
import React from 'react';
import {useDroppable, UniqueIdentifier} from '@dnd-kit/core';
import classNames from 'classnames';
import {droppable} from './droppable-svg';
import styles from './Droppable.module.css';
interface Props {
children: React.ReactNode;
dragging: boolean;
id: UniqueIdentifier;
}
export function Droppable({children, id, dragging}: Props) {
const {isOver, setNodeRef} = useDroppable({
id,
});
return (
<div
ref={setNodeRef}
className={classNames(
styles.Droppable,
isOver && styles.over,
dragging && styles.dragging,
children && styles.dropped
)}
aria-label="Droppable region"
>
{children}
{droppable}
</div>
);
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
export {Droppable} from './Droppable';

View File

@@ -0,0 +1,35 @@
import React, { useState } from "react";
import { DndContext } from "@dnd-kit/core";
import { Draggable } from "./Draggable";
import { Droppable } from "./Droppable";
export const DroppableGrid = () => {
const containers = ["A", "B", "C"];
const [parent, setParent] = useState(null);
const draggableMarkup = <Draggable id="draggable">Drag me</Draggable>;
return (
<DndContext onDragEnd={handleDragEnd}>
{parent === null ? draggableMarkup : null}
{containers.map((id) => (
// We updated the Droppable component so it would accept an `id`
// prop and pass it to `useDroppable`
<Droppable key={id} id={id}>
{parent === id ? draggableMarkup : "Drop here"}
</Droppable>
))}
</DndContext>
);
function handleDragEnd(event) {
const { over } = event;
// If the item is dropped over a container, set it as the parent
// otherwise reset the parent to `null`
setParent(over ? over.id : null);
}
};
export default DroppableGrid;

View File

@@ -0,0 +1,5 @@
.FloatingControls {
position: fixed;
top: 25px;
right: 25px;
}

View File

@@ -0,0 +1,7 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'FloatingControls': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -0,0 +1,12 @@
import React from 'react';
import classNames from 'classnames';
import styles from './FloatingControls.module.css';
export interface Props {
children: React.ReactNode;
}
export function FloatingControls({children}: Props) {
return <div className={classNames(styles.FloatingControls)}>{children}</div>;
}

View File

@@ -0,0 +1 @@
export {FloatingControls} from './FloatingControls';

View File

@@ -0,0 +1,30 @@
.Grid {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: repeating-linear-gradient(
0deg,
transparent,
transparent calc(var(--grid-size) - 1px),
#ddd calc(var(--grid-size) - 1px),
#ddd var(--grid-size)
),
repeating-linear-gradient(
-90deg,
transparent,
transparent calc(var(--grid-size) - 1px),
#ddd calc(var(--grid-size) - 1px),
#ddd var(--grid-size)
);
background-size: var(--grid-size) var(--grid-size);
z-index: -1;
pointer-events: none;
}
.RangeSlider {
position: fixed;
right: 20px;
bottom: 20px;
}

View File

@@ -0,0 +1,8 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'Grid': string;
'RangeSlider': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -0,0 +1,22 @@
import React from 'react';
import styles from './Grid.module.css';
export interface Props {
size: number;
step?: number;
onSizeChange(size: number): void;
}
export function Grid({size}: Props) {
return (
<div
className={styles.Grid}
style={
{
'--grid-size': `${size}px`,
} as React.CSSProperties
}
/>
);
}

View File

@@ -0,0 +1 @@
export {Grid} from './Grid';

View File

@@ -0,0 +1,15 @@
.GridContainer {
max-width: 800px;
display: grid;
grid-template-columns: repeat(var(--col-count), 1fr);
grid-gap: 10px;
padding: 20px;
@media (max-width: 850px) {
grid-template-columns: repeat(calc(var(--col-count) - 1), 1fr);
}
@media (max-width: 650px) {
grid-template-columns: repeat(calc(var(--col-count) - 2), 1fr);
}
}

View File

@@ -0,0 +1,7 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'GridContainer': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -0,0 +1,23 @@
import React from 'react';
import styles from './GridContainer.module.css';
export interface Props {
children: React.ReactNode;
columns: number;
}
export function GridContainer({children, columns}: Props) {
return (
<ul
className={styles.GridContainer}
style={
{
'--col-count': columns,
} as React.CSSProperties
}
>
{children}
</ul>
);
}

View File

@@ -0,0 +1 @@
export {GridContainer} from './GridContainer';

View File

@@ -0,0 +1,145 @@
$font-weight: 400;
$background-color: #000;
$border-color: #efefef;
$text-color: #333;
$handle-color: rgba(0, 0, 0, 0.25);
$box-shadow-border: 0 0 0 calc(1px / var(--scale-x, 1)) rgba(63, 63, 68, 0.05);
$box-shadow-common: 0 1px calc(3px / var(--scale-x, 1)) 0 rgba(34, 33, 81, 0.15);
$box-shadow: $box-shadow-border, $box-shadow-common;
$focused-outline-color: #4c9ffe;
@keyframes pop {
0% {
transform: scale(1);
box-shadow: var(--box-shadow);
}
100% {
transform: scale(var(--scale));
box-shadow: var(--box-shadow-picked-up);
}
}
@keyframes fadeIn {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.Wrapper {
display: flex;
box-sizing: border-box;
transform: translate3d(var(--translate-x, 0), var(--translate-y, 0), 0)
scaleX(var(--scale-x, 1)) scaleY(var(--scale-y, 1));
transform-origin: 0 0;
touch-action: manipulation;
&.fadeIn {
animation: fadeIn 500ms ease;
}
&.dragOverlay {
--scale: 1.05;
--box-shadow: $box-shadow;
--box-shadow-picked-up: $box-shadow-border,
-1px 0 15px 0 rgba(34, 33, 81, 0.01),
0px 15px 15px 0 rgba(34, 33, 81, 0.25);
z-index: 999;
}
}
.Item {
position: relative;
display: flex;
flex-grow: 1;
align-items: center;
padding: 18px 20px;
background-color: #ddd;
box-shadow: $box-shadow;
outline: none;
border-radius: calc(4px / var(--scale-x, 1));
box-sizing: border-box;
list-style: none;
transform-origin: 50% 50%;
-webkit-tap-highlight-color: transparent;
color: $text-color;
font-weight: $font-weight;
font-size: 1rem;
white-space: nowrap;
transform: scale(var(--scale, 1));
transition: box-shadow 200ms cubic-bezier(0.18, 0.67, 0.6, 1.22);
&:focus-visible {
box-shadow: 0 0px 4px 1px $focused-outline-color, $box-shadow;
}
&:not(.withHandle) {
touch-action: manipulation;
cursor: grab;
}
&.dragging:not(.dragOverlay) {
opacity: var(--dragging-opacity, 0.5);
z-index: 0;
&:focus {
box-shadow: $box-shadow;
}
}
&.disabled {
color: #999;
background-color: #f1f1f1;
&:focus {
box-shadow: 0 0px 4px 1px rgba(0, 0, 0, 0.1), $box-shadow;
}
cursor: not-allowed;
}
&.dragOverlay {
cursor: inherit;
/* box-shadow: 0 0px 6px 2px $focused-outline-color; */
animation: pop 200ms cubic-bezier(0.18, 0.67, 0.6, 1.22);
transform: scale(var(--scale));
box-shadow: var(--box-shadow-picked-up);
opacity: 1;
}
&.color:before {
content: '';
position: absolute;
top: 50%;
transform: translateY(-50%);
left: 0;
height: 100%;
width: 3px;
display: block;
border-top-left-radius: 3px;
border-bottom-left-radius: 3px;
background-color: var(--color);
}
&:hover {
.Remove {
visibility: visible;
}
}
}
.Remove {
visibility: hidden;
}
.Actions {
display: flex;
align-self: flex-start;
margin-top: -12px;
margin-left: auto;
margin-bottom: -15px;
margin-right: -10px;
}

View File

@@ -0,0 +1,150 @@
import React, { useEffect } from "react";
import classNames from "classnames";
import type { DraggableSyntheticListeners } from "@dnd-kit/core";
import type { Transform } from "@dnd-kit/utilities";
import { Handle, Remove } from "./components";
import styles from "./Item.module.css";
export interface Props {
dragOverlay?: boolean;
color?: string;
disabled?: boolean;
dragging?: boolean;
handle?: boolean;
height?: number;
index?: number;
fadeIn?: boolean;
transform?: Transform | null;
listeners?: DraggableSyntheticListeners;
sorting?: boolean;
style?: React.CSSProperties;
transition?: string | null;
wrapperStyle?: React.CSSProperties;
value: React.ReactNode;
onRemove?(): void;
renderItem?(args: {
dragOverlay: boolean;
dragging: boolean;
sorting: boolean;
index: number | undefined;
fadeIn: boolean;
listeners: DraggableSyntheticListeners;
ref: React.Ref<HTMLElement>;
style: React.CSSProperties | undefined;
transform: Props["transform"];
transition: Props["transition"];
value: Props["value"];
}): React.ReactElement;
}
export const Item = React.memo(
React.forwardRef<HTMLLIElement, Props>(
(
{
color,
dragOverlay,
dragging,
disabled,
fadeIn,
handle,
height,
index,
listeners,
onRemove,
renderItem,
sorting,
style,
transition,
transform,
value,
wrapperStyle,
...props
},
ref,
) => {
useEffect(() => {
if (!dragOverlay) {
return;
}
document.body.style.cursor = "grabbing";
return () => {
document.body.style.cursor = "";
};
}, [dragOverlay]);
return renderItem ? (
renderItem({
dragOverlay: Boolean(dragOverlay),
dragging: Boolean(dragging),
sorting: Boolean(sorting),
index,
fadeIn: Boolean(fadeIn),
listeners,
ref,
style,
transform,
transition,
value,
})
) : (
<li
className={classNames(
styles.Wrapper,
fadeIn && styles.fadeIn,
sorting && styles.sorting,
dragOverlay && styles.dragOverlay,
)}
style={
{
...wrapperStyle,
transition,
"--translate-x": transform
? `${Math.round(transform.x)}px`
: undefined,
"--translate-y": transform
? `${Math.round(transform.y)}px`
: undefined,
"--scale-x": transform?.scaleX
? `${transform.scaleX}`
: undefined,
"--scale-y": transform?.scaleY
? `${transform.scaleY}`
: undefined,
"--index": index,
"--color": color,
} as React.CSSProperties
}
ref={ref}
>
<div
className={classNames(
styles.Item,
dragging && styles.dragging,
handle && styles.withHandle,
dragOverlay && styles.dragOverlay,
disabled && styles.disabled,
color && styles.color,
)}
style={style}
data-cypress="draggable-item"
{...(!handle ? listeners : undefined)}
{...props}
tabIndex={!handle ? 0 : undefined}
>
{value}
<span className={styles.Actions}>
{onRemove ? (
<Remove className={styles.Remove} onClick={onRemove} />
) : null}
{handle ? <Handle {...listeners} /> : null}
</span>
</div>
</li>
);
},
),
);

View File

@@ -0,0 +1,50 @@
$focused-outline-color: #4c9ffe;
.Action {
display: flex;
width: 12px;
padding: 15px;
align-items: center;
justify-content: center;
flex: 0 0 auto;
touch-action: none;
cursor: var(--cursor, pointer);
border-radius: 5px;
border: none;
outline: none;
appearance: none;
background-color: transparent;
-webkit-tap-highlight-color: transparent;
@media (hover: hover) {
&:hover {
background-color: var(--action-background, rgba(0, 0, 0, 0.05));
svg {
fill: #6f7b88;
}
}
}
svg {
flex: 0 0 auto;
margin: auto;
height: 100%;
overflow: visible;
fill: #919eab;
}
&:active {
background-color: var(--background, rgba(0, 0, 0, 0.05));
svg {
fill: var(--fill, #788491);
}
}
&:focus-visible {
outline: none;
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0),
0 0px 0px 2px $focused-outline-color;
}
}

View File

@@ -0,0 +1,7 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'Action': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -0,0 +1,30 @@
import React, {CSSProperties} from 'react';
import classNames from 'classnames';
import styles from './Action.module.css';
export interface Props extends React.HTMLAttributes<HTMLButtonElement> {
active?: {
fill: string;
background: string;
};
cursor?: CSSProperties['cursor'];
}
export function Action({active, className, cursor, style, ...props}: Props) {
return (
<button
{...props}
className={classNames(styles.Action, className)}
tabIndex={0}
style={
{
...style,
cursor,
'--fill': active?.fill,
'--background': active?.background,
} as CSSProperties
}
/>
);
}

View File

@@ -0,0 +1,2 @@
export {Action} from './Action';
export type {Props as ActionProps} from './Action';

View File

@@ -0,0 +1,13 @@
import React from 'react';
import {Action, ActionProps} from '../Action';
export function Handle(props: ActionProps) {
return (
<Action cursor="grab" data-cypress="draggable-handle" {...props}>
<svg viewBox="0 0 20 20" width="12">
<path d="M7 2a2 2 0 1 0 .001 4.001A2 2 0 0 0 7 2zm0 6a2 2 0 1 0 .001 4.001A2 2 0 0 0 7 8zm0 6a2 2 0 1 0 .001 4.001A2 2 0 0 0 7 14zm6-8a2 2 0 1 0-.001-4.001A2 2 0 0 0 13 6zm0 2a2 2 0 1 0 .001 4.001A2 2 0 0 0 13 8zm0 6a2 2 0 1 0 .001 4.001A2 2 0 0 0 13 14z"></path>
</svg>
</Action>
);
}

View File

@@ -0,0 +1 @@
export {Handle} from './Handle';

View File

@@ -0,0 +1,19 @@
import React from 'react';
import {Action, ActionProps} from '../Action';
export function Remove(props: ActionProps) {
return (
<Action
{...props}
active={{
fill: 'rgba(255, 70, 70, 0.95)',
background: 'rgba(255, 70, 70, 0.1)',
}}
>
<svg width="8" viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg">
<path d="M2.99998 -0.000206962C2.7441 -0.000206962 2.48794 0.0972617 2.29294 0.292762L0.292945 2.29276C-0.0980552 2.68376 -0.0980552 3.31682 0.292945 3.70682L7.58591 10.9998L0.292945 18.2928C-0.0980552 18.6838 -0.0980552 19.3168 0.292945 19.7068L2.29294 21.7068C2.68394 22.0978 3.31701 22.0978 3.70701 21.7068L11 14.4139L18.2929 21.7068C18.6829 22.0978 19.317 22.0978 19.707 21.7068L21.707 19.7068C22.098 19.3158 22.098 18.6828 21.707 18.2928L14.414 10.9998L21.707 3.70682C22.098 3.31682 22.098 2.68276 21.707 2.29276L19.707 0.292762C19.316 -0.0982383 18.6829 -0.0982383 18.2929 0.292762L11 7.58573L3.70701 0.292762C3.51151 0.0972617 3.25585 -0.000206962 2.99998 -0.000206962Z" />
</svg>
</Action>
);
}

View File

@@ -0,0 +1 @@
export {Remove} from './Remove';

View File

@@ -0,0 +1,3 @@
export {Action} from './Action';
export {Handle} from './Handle';
export {Remove} from './Remove';

View File

@@ -0,0 +1,2 @@
export {Item} from './Item';
export {Action, Handle, Remove} from './components';

View File

@@ -0,0 +1,25 @@
.List {
display: grid;
grid-auto-rows: max-content;
box-sizing: border-box;
min-width: 350px;
grid-gap: 10px;
padding: 20px;
padding-bottom: 0;
margin: 10px;
border-radius: 5px;
min-height: 200px;
transition: background-color 350ms ease;
grid-template-columns: repeat(var(--columns, 1), 1fr);
&:after {
content: '';
height: 10px;
grid-column-start: span var(--columns, 1);
}
&.horizontal {
width: 100%;
grid-auto-flow: column;
}
}

View File

@@ -0,0 +1,8 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'List': string;
'horizontal': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -0,0 +1,30 @@
import React, {forwardRef} from 'react';
import classNames from 'classnames';
import styles from './List.module.css';
export interface Props {
children: React.ReactNode;
columns?: number;
style?: React.CSSProperties;
horizontal?: boolean;
}
export const List = forwardRef<HTMLUListElement, Props>(
({children, columns = 1, horizontal, style}: Props, ref) => {
return (
<ul
ref={ref}
style={
{
...style,
'--columns': columns,
} as React.CSSProperties
}
className={classNames(styles.List, horizontal && styles.horizontal)}
>
{children}
</ul>
);
}
);

View File

@@ -0,0 +1 @@
export {List} from './List';

View File

@@ -0,0 +1,8 @@
.OverflowWrapper {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
overflow: hidden;
}

View File

@@ -0,0 +1,7 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'OverflowWrapper': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -0,0 +1,11 @@
import React from 'react';
import styles from './OverflowWrapper.module.css';
interface Props {
children: React.ReactNode;
}
export function OverflowWrapper({children}: Props) {
return <div className={styles.OverflowWrapper}>{children}</div>;
}

View File

@@ -0,0 +1 @@
export {OverflowWrapper} from './OverflowWrapper';

View File

@@ -0,0 +1,11 @@
.Wrapper {
display: flex;
width: 100%;
box-sizing: border-box;
padding: 20px;
justify-content: flex-start;
&.center {
justify-content: center;
}
}

View File

@@ -0,0 +1,8 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'Wrapper': string;
'center': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -0,0 +1,21 @@
import React from 'react';
import classNames from 'classnames';
import styles from './Wrapper.module.css';
interface Props {
children: React.ReactNode;
center?: boolean;
style?: React.CSSProperties;
}
export function Wrapper({children, center, style}: Props) {
return (
<div
className={classNames(styles.Wrapper, center && styles.center)}
style={style}
>
{children}
</div>
);
}

View File

@@ -0,0 +1 @@
export {Wrapper} from './Wrapper';

View File

@@ -0,0 +1,13 @@
export { Button } from "./Button";
export { ConfirmModal } from "./ConfirmModal";
export { Container } from "./Container";
export type { ContainerProps } from "./Container";
export { Draggable } from "./Draggable";
export { Droppable } from "./Droppable";
export { Item, Action, Handle, Remove } from "./Item";
export { FloatingControls } from "./FloatingControls";
export { Grid } from "./Grid";
export { GridContainer } from "./GridContainer";
export { List } from "./List";
export { OverflowWrapper } from "./OverflowWrapper";
export { Wrapper } from "./Wrapper";

View File

@@ -0,0 +1,8 @@
const defaultInitializer = (index: number) => index;
export function createRange<T = number>(
length: number,
initializer: (index: number) => any = defaultInitializer
): T[] {
return [...new Array(length)].map((_, index) => initializer(index));
}

View File

@@ -0,0 +1 @@
export {createRange} from './createRange';

View File

@@ -10,13 +10,31 @@
"sourceMap": true,
"outDir": "./dist/",
"skipLibCheck": true,
"lib": ["dom", "dom.iterable", "esnext", "webworker"]
"lib": [
"dom",
"dom.iterable",
"esnext",
"webworker"
],
"plugins": [
{
"name": "typescript-plugin-css-modules"
}
]
},
"settings": {
"eslint.workingDirectories": [
{ "directory": "./node_modules", "changeProcessCWD": true }
{
"directory": "./node_modules",
"changeProcessCWD": true
}
]
},
"exclude": ["./src/server "],
"include": ["./src/client/*", "./src/client/types/**/*.d.ts"]
}
"exclude": [
"./src/server "
],
"include": [
"./src/client/*",
"./src/client/types/**/*.d.ts"
]
}

View File

@@ -40,7 +40,17 @@ module.exports = {
},
{
test: /\.css$/,
use: ["style-loader", "css-loader"],
use: [
{ loader: "style-loader" },
{
loader: "css-loader",
options: {
modules: true,
importLoaders: 1,
sourceMap: true,
},
},
],
},
{
test: /\.(scss|sass)$/,

View File

@@ -1035,6 +1035,45 @@
resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.5.tgz#9283c9ce5b289a3c4f61c12757469e59377f81f3"
integrity sha512-6nFkfkmSeV/rqSaS4oWHgmpnYw194f6hmWF5is6b0J1naJZoiD0NTc9AiUwPHvWsowkjuHErCZT1wa0jg+BLIA==
"@dnd-kit/accessibility@^3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@dnd-kit/accessibility/-/accessibility-3.0.0.tgz#b56e3750414fd907b7d6972b3116aa8f96d07fde"
integrity sha512-QwaQ1IJHQHMMuAGOOYHQSx7h7vMZPfO97aDts8t5N/MY7n2QTDSnW+kF7uRQ1tVBkr6vJ+BqHWG5dlgGvwVjow==
dependencies:
tslib "^2.0.0"
"@dnd-kit/core@^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@dnd-kit/core/-/core-4.0.0.tgz#14e530f859d95ca49aea57878c95bc5f15cf5aac"
integrity sha512-ZbQxOoZs/eH8FJlQAipw+FVF6AvjoTR6aAF6JrweJ4Ic45WhuxHHRN/7u50MYhaMuvG6HF24qks0hZKQUe1EDg==
dependencies:
"@dnd-kit/accessibility" "^3.0.0"
"@dnd-kit/utilities" "^3.0.0"
tslib "^2.0.0"
"@dnd-kit/modifiers@^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@dnd-kit/modifiers/-/modifiers-4.0.0.tgz#d1577b806b2319f14a1a0a155f270e672cfca636"
integrity sha512-4OkNTamneH9u3YMJqG6yJ6cwFoEd/4yY9BF39TgmDh9vyMK2MoPZFVAV0vOEm193ZYsPczq3Af5tJFtJhR9jJQ==
dependencies:
"@dnd-kit/utilities" "^3.0.0"
tslib "^2.0.0"
"@dnd-kit/sortable@^5.0.0":
version "5.0.0"
resolved "https://registry.yarnpkg.com/@dnd-kit/sortable/-/sortable-5.0.0.tgz#33841fcbc2f2490aaae42906b7497a2033242faa"
integrity sha512-CvTuaTIqzvZe8r1Lt+2tPK7VZKfE0SQkFTcAMayf8jS6MFgX6TV5GAIb1tKvbqrGXkqqTZeA2EaeMzjFyyfqkQ==
dependencies:
"@dnd-kit/utilities" "^3.0.0"
tslib "^2.0.0"
"@dnd-kit/utilities@^3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@dnd-kit/utilities/-/utilities-3.0.0.tgz#43b86e9d911b625ee54786e9daae5a521949b8a6"
integrity sha512-E2UG8guqa532xB8kEe2rFEI/U2W7zuxv0zYorL3knnGJxMGBoI7132i4ULmQs/aunQvNy8kj6Ts39n+APeU7BQ==
dependencies:
tslib "^2.0.0"
"@emotion/cache@^11.4.0":
version "11.4.0"
resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.4.0.tgz#293fc9d9a7a38b9aad8e9337e5014366c3b09ac0"
@@ -4299,6 +4338,14 @@ css-loader@^5.1.2:
schema-utils "^3.0.0"
semver "^7.3.5"
css-modules-typescript-loader@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/css-modules-typescript-loader/-/css-modules-typescript-loader-4.0.1.tgz#0b818cf647fefd8f9fb3d4469374e69ab1e72742"
integrity sha512-vXrUAwPGcRaopnGdg7I5oqv/NSSKQRN5L80m3f49uSGinenU5DTNsMFHS+2roh5tXqpY5+yAAKAl7A2HDvumzg==
dependencies:
line-diff "^2.0.1"
loader-utils "^1.2.3"
css-select-base-adapter@^0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz#3b2ff4972cc362ab88561507a95408a1432135d7"
@@ -5242,6 +5289,14 @@ eslint-module-utils@^2.6.2:
debug "^3.2.7"
pkg-dir "^2.0.0"
eslint-plugin-css-modules@^2.11.0:
version "2.11.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-css-modules/-/eslint-plugin-css-modules-2.11.0.tgz#8de4d01d523a2d51c03043fa8004aab6b6cf3b1a"
integrity sha512-CLvQvJOMlCywZzaI4HVu7QH/ltgNXvCg7giJGiE+sA9wh5zQ+AqTgftAzrERV22wHe1p688wrU/Zwxt1Ry922w==
dependencies:
gonzales-pe "^4.0.3"
lodash "^4.17.2"
eslint-plugin-import@^2.22.1:
version "2.24.2"
resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.24.2.tgz#2c8cd2e341f3885918ee27d18479910ade7bb4da"
@@ -6421,6 +6476,13 @@ globule@^1.0.0:
lodash "~4.17.10"
minimatch "~3.0.2"
gonzales-pe@^4.0.3:
version "4.3.0"
resolved "https://registry.yarnpkg.com/gonzales-pe/-/gonzales-pe-4.3.0.tgz#fe9dec5f3c557eead09ff868c65826be54d067b3"
integrity sha512-otgSPpUmdWJ43VXyiNgEYE4luzHCL2pz4wQ0OnDluC6Eg4Ko3Vexy/SrSynglw/eR+OhkzmqFCZa/OFa/RgAOQ==
dependencies:
minimist "^1.2.5"
got@^6.7.1:
version "6.7.1"
resolved "https://registry.yarnpkg.com/got/-/got-6.7.1.tgz#240cd05785a9a18e561dc1b44b41c763ef1e8db0"
@@ -8363,6 +8425,11 @@ lcid@^2.0.0:
dependencies:
invert-kv "^2.0.0"
levdist@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/levdist/-/levdist-1.0.0.tgz#91d7a3044964f2ccc421a0477cac827fe75c5718"
integrity sha1-kdejBElk8szEIaBHfKyCf+dcVxg=
leven@2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/leven/-/leven-2.1.0.tgz#c2e7a9f772094dee9d34202ae8acce4687875580"
@@ -8499,6 +8566,13 @@ libnpmversion@^1.2.1:
semver "^7.3.5"
stringify-package "^1.0.1"
line-diff@^2.0.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/line-diff/-/line-diff-2.1.1.tgz#a389799b931375a3b1e764964ad0b0b3ce60d6f6"
integrity sha512-vswdynAI5AMPJacOo2o+JJ4caDJbnY2NEqms4MhMW0NJbjh3skP/brpVTAgBxrg55NRZ2Vtw88ef18hnagIpYQ==
dependencies:
levdist "^1.0.0"
lines-and-columns@^1.1.6:
version "1.1.6"
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00"
@@ -8542,7 +8616,7 @@ loader-runner@^4.2.0:
resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.2.0.tgz#d7022380d66d14c5fb1d496b89864ebcfd478384"
integrity sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw==
loader-utils@^1.0.0, loader-utils@^1.1.0, loader-utils@^1.4.0:
loader-utils@^1.0.0, loader-utils@^1.1.0, loader-utils@^1.2.3, loader-utils@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.0.tgz#c579b5e34cb34b1a74edc6c1fb36bfa371d5a613"
integrity sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==
@@ -8625,7 +8699,7 @@ lodash.truncate@^4.4.2:
resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193"
integrity sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=
lodash@^4.0.0, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.7.0, lodash@~4.17.10:
lodash@^4.0.0, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.2, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.7.0, lodash@~4.17.10:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
@@ -13370,7 +13444,7 @@ tslib@^1.13.0, tslib@^1.8.1, tslib@^1.9.0:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
tslib@^2.0.1, tslib@^2.0.3, tslib@^2.3.0, tslib@^2.3.1:
tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.3.0, tslib@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01"
integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==