diff --git a/.eslintrc.js b/.eslintrc.js index cbb76f8..37687f4 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -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: { diff --git a/declarations.d.ts b/declarations.d.ts new file mode 100644 index 0000000..8f8fc24 --- /dev/null +++ b/declarations.d.ts @@ -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; +} diff --git a/package.json b/package.json index d4c6eb6..309d5bf 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/client/assets/scss/App.scss b/src/client/assets/scss/App.scss index 51b7c7a..b6acc06 100644 --- a/src/client/assets/scss/App.scss +++ b/src/client/assets/scss/App.scss @@ -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 { diff --git a/src/client/components/ComicDetail.tsx b/src/client/components/ComicDetail.tsx index d842b57..7e96cad 100644 --- a/src/client/components/ComicDetail.tsx +++ b/src/client/components/ComicDetail.tsx @@ -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: () => ( - <> -
- - Pages - - {extractedComicBookArchive.length} - - -
- - {map(extractedComicBookArchive, (page, idx) => { - return ( -
- -
- ); - })} -
- - ), + 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: , - name: "Other Metadata", - content:
bastard
, + icon: , + name: "Archive Operations", + content: ( +
+ +
+ ), }, { 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; diff --git a/src/client/components/SortableGrid/Sortable.tsx b/src/client/components/SortableGrid/Sortable.tsx new file mode 100644 index 0000000..1a942c4 --- /dev/null +++ b/src/client/components/SortableGrid/Sortable.tsx @@ -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( + () => + initialItems ?? + createRange(itemCount, (index) => (index + 1).toString()), + ); + const [activeId, setActiveId] = useState(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 ( + { + 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} + > + + + + {items.map((value, index) => ( + + ))} + + + + {useDragOverlay + ? createPortal( + + {activeId ? ( + + ) : null} + , + document.body, + ) + : null} + + ); +} + +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 ( + 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} + /> + ); +} diff --git a/src/client/components/SortableGrid/SortableGrid.tsx b/src/client/components/SortableGrid/SortableGrid.tsx new file mode 100644 index 0000000..16eaf2e --- /dev/null +++ b/src/client/components/SortableGrid/SortableGrid.tsx @@ -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 = { + adjustScale: true, + Container: (props: any) => , + strategy: rectSortingStrategy, + wrapperStyle: () => ({ + width: 140, + height: 140, + }), +}; + +export const BasicSetup = () => ; + +export const WithoutDragOverlay = () => ( + +); + +export const LargeFirstTile = () => ( + { + 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 = () => ( + { + 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 = () => ; + +export const ScrollContainer = () => ( +
+ +
+); + +export const PressDelay = () => ( + +); + +export const MinimumDistance = () => ( + +); + +export const RemovableItems = () => { + const animateLayoutChanges: AnimateLayoutChanges = (args) => + args.isSorting || args.wasDragging + ? defaultAnimateLayoutChanges(args) + : true; + + return ( + + ); +}; diff --git a/src/client/components/SortableGrid/components/Button/Button.module.css b/src/client/components/SortableGrid/components/Button/Button.module.css new file mode 100644 index 0000000..f4b754b --- /dev/null +++ b/src/client/components/SortableGrid/components/Button/Button.module.css @@ -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); + } +} diff --git a/src/client/components/SortableGrid/components/Button/Button.module.css.d.ts b/src/client/components/SortableGrid/components/Button/Button.module.css.d.ts new file mode 100644 index 0000000..1b17034 --- /dev/null +++ b/src/client/components/SortableGrid/components/Button/Button.module.css.d.ts @@ -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; diff --git a/src/client/components/SortableGrid/components/Button/Button.tsx b/src/client/components/SortableGrid/components/Button/Button.tsx new file mode 100644 index 0000000..1279ecf --- /dev/null +++ b/src/client/components/SortableGrid/components/Button/Button.tsx @@ -0,0 +1,16 @@ +import React, {HTMLAttributes} from 'react'; +import classNames from 'classnames'; + +import styles from './Button.module.css'; + +export interface Props extends HTMLAttributes { + children: React.ReactNode; +} + +export function Button({children, ...props}: Props) { + return ( + + ); +} diff --git a/src/client/components/SortableGrid/components/Button/index.ts b/src/client/components/SortableGrid/components/Button/index.ts new file mode 100644 index 0000000..2cdeff8 --- /dev/null +++ b/src/client/components/SortableGrid/components/Button/index.ts @@ -0,0 +1 @@ +export {Button} from './Button'; diff --git a/src/client/components/SortableGrid/components/ConfirmModal/ConfirmModal.module.css b/src/client/components/SortableGrid/components/ConfirmModal/ConfirmModal.module.css new file mode 100644 index 0000000..a6c7898 --- /dev/null +++ b/src/client/components/SortableGrid/components/ConfirmModal/ConfirmModal.module.css @@ -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; + } + } +} diff --git a/src/client/components/SortableGrid/components/ConfirmModal/ConfirmModal.module.css.d.ts b/src/client/components/SortableGrid/components/ConfirmModal/ConfirmModal.module.css.d.ts new file mode 100644 index 0000000..92ad301 --- /dev/null +++ b/src/client/components/SortableGrid/components/ConfirmModal/ConfirmModal.module.css.d.ts @@ -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; diff --git a/src/client/components/SortableGrid/components/ConfirmModal/ConfirmModal.tsx b/src/client/components/SortableGrid/components/ConfirmModal/ConfirmModal.tsx new file mode 100644 index 0000000..99e297b --- /dev/null +++ b/src/client/components/SortableGrid/components/ConfirmModal/ConfirmModal.tsx @@ -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) => ( +
+

{children}

+
+ + +
+
+); diff --git a/src/client/components/SortableGrid/components/ConfirmModal/index.ts b/src/client/components/SortableGrid/components/ConfirmModal/index.ts new file mode 100644 index 0000000..1027bee --- /dev/null +++ b/src/client/components/SortableGrid/components/ConfirmModal/index.ts @@ -0,0 +1 @@ +export {ConfirmModal} from './ConfirmModal'; diff --git a/src/client/components/SortableGrid/components/Container/Container.module.css b/src/client/components/SortableGrid/components/Container/Container.module.css new file mode 100644 index 0000000..1fd675f --- /dev/null +++ b/src/client/components/SortableGrid/components/Container/Container.module.css @@ -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; + } + } +} diff --git a/src/client/components/SortableGrid/components/Container/Container.module.css.d.ts b/src/client/components/SortableGrid/components/Container/Container.module.css.d.ts new file mode 100644 index 0000000..067ff72 --- /dev/null +++ b/src/client/components/SortableGrid/components/Container/Container.module.css.d.ts @@ -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; diff --git a/src/client/components/SortableGrid/components/Container/Container.tsx b/src/client/components/SortableGrid/components/Container/Container.tsx new file mode 100644 index 0000000..d48b904 --- /dev/null +++ b/src/client/components/SortableGrid/components/Container/Container.tsx @@ -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; + scrollable?: boolean; + shadow?: boolean; + placeholder?: boolean; + unstyled?: boolean; + onClick?(): void; + onRemove?(): void; +} + +export const Container = forwardRef( + ( + { + children, + columns = 1, + handleProps, + horizontal, + hover, + onClick, + onRemove, + label, + placeholder, + style, + scrollable, + shadow, + unstyled, + ...props + }: Props, + ref + ) => { + const Component = onClick ? 'button' : 'div'; + + return ( + + {label ? ( +
+ {label} +
+ {onRemove ? : undefined} + +
+
+ ) : null} + {placeholder ? children :
    {children}
} +
+ ); + } +); diff --git a/src/client/components/SortableGrid/components/Container/index.ts b/src/client/components/SortableGrid/components/Container/index.ts new file mode 100644 index 0000000..e8236dc --- /dev/null +++ b/src/client/components/SortableGrid/components/Container/index.ts @@ -0,0 +1,2 @@ +export {Container} from './Container'; +export type {Props as ContainerProps} from './Container'; diff --git a/src/client/components/SortableGrid/components/Draggable.tsx b/src/client/components/SortableGrid/components/Draggable.tsx new file mode 100644 index 0000000..599915e --- /dev/null +++ b/src/client/components/SortableGrid/components/Draggable.tsx @@ -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 ( + + ); +}; + +export default Draggable; diff --git a/src/client/components/SortableGrid/components/Draggable/Draggable.module.css b/src/client/components/SortableGrid/components/Draggable/Draggable.module.css new file mode 100644 index 0000000..ee5bb0c --- /dev/null +++ b/src/client/components/SortableGrid/components/Draggable/Draggable.module.css @@ -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); + } +} diff --git a/src/client/components/SortableGrid/components/Draggable/Draggable.tsx b/src/client/components/SortableGrid/components/Draggable/Draggable.tsx new file mode 100644 index 0000000..c2645c8 --- /dev/null +++ b/src/client/components/SortableGrid/components/Draggable/Draggable.tsx @@ -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( + function Draggable( + { + axis, + dragOverlay, + dragging, + handle, + label, + listeners, + translate, + ...props + }, + ref + ) { + return ( +
+ + {label ? : null} +
+ ); + } +); diff --git a/src/client/components/SortableGrid/components/Draggable/draggable-svg.tsx b/src/client/components/SortableGrid/components/Draggable/draggable-svg.tsx new file mode 100644 index 0000000..3e5f294 --- /dev/null +++ b/src/client/components/SortableGrid/components/Draggable/draggable-svg.tsx @@ -0,0 +1,48 @@ +import React from 'react'; + +export const draggable = ( + + + + + +); + +export const draggableVertical = ( + + + +); + +export const draggableHorizontal = ( + + + +); diff --git a/src/client/components/SortableGrid/components/Draggable/index.ts b/src/client/components/SortableGrid/components/Draggable/index.ts new file mode 100644 index 0000000..0f94ad3 --- /dev/null +++ b/src/client/components/SortableGrid/components/Draggable/index.ts @@ -0,0 +1 @@ +export { Axis, Draggable } from "./Draggable"; diff --git a/src/client/components/SortableGrid/components/Droppable.tsx b/src/client/components/SortableGrid/components/Droppable.tsx new file mode 100644 index 0000000..d427bb1 --- /dev/null +++ b/src/client/components/SortableGrid/components/Droppable.tsx @@ -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 ( +
+ {props.children} +
+ ); +}; + +export default Droppable; diff --git a/src/client/components/SortableGrid/components/Droppable/Droppable.module.css b/src/client/components/SortableGrid/components/Droppable/Droppable.module.css new file mode 100644 index 0000000..70e77c9 --- /dev/null +++ b/src/client/components/SortableGrid/components/Droppable/Droppable.module.css @@ -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); + } + } +} diff --git a/src/client/components/SortableGrid/components/Droppable/Droppable.tsx b/src/client/components/SortableGrid/components/Droppable/Droppable.tsx new file mode 100644 index 0000000..e3f480a --- /dev/null +++ b/src/client/components/SortableGrid/components/Droppable/Droppable.tsx @@ -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 ( +
+ {children} + {droppable} +
+ ); +} diff --git a/src/client/components/SortableGrid/components/Droppable/droppable-svg.tsx b/src/client/components/SortableGrid/components/Droppable/droppable-svg.tsx new file mode 100644 index 0000000..e1b3e27 --- /dev/null +++ b/src/client/components/SortableGrid/components/Droppable/droppable-svg.tsx @@ -0,0 +1,18 @@ +import React from 'react'; + +export const droppable = ( + + + + + +); diff --git a/src/client/components/SortableGrid/components/Droppable/index.ts b/src/client/components/SortableGrid/components/Droppable/index.ts new file mode 100644 index 0000000..a9eddaa --- /dev/null +++ b/src/client/components/SortableGrid/components/Droppable/index.ts @@ -0,0 +1 @@ +export {Droppable} from './Droppable'; diff --git a/src/client/components/SortableGrid/components/DroppableGrid.tsx b/src/client/components/SortableGrid/components/DroppableGrid.tsx new file mode 100644 index 0000000..5ad37dc --- /dev/null +++ b/src/client/components/SortableGrid/components/DroppableGrid.tsx @@ -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 = Drag me; + + return ( + + {parent === null ? draggableMarkup : null} + + {containers.map((id) => ( + // We updated the Droppable component so it would accept an `id` + // prop and pass it to `useDroppable` + + {parent === id ? draggableMarkup : "Drop here"} + + ))} + + ); + + 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; diff --git a/src/client/components/SortableGrid/components/FloatingControls/FloatingControls.module.css b/src/client/components/SortableGrid/components/FloatingControls/FloatingControls.module.css new file mode 100644 index 0000000..cf8dea8 --- /dev/null +++ b/src/client/components/SortableGrid/components/FloatingControls/FloatingControls.module.css @@ -0,0 +1,5 @@ +.FloatingControls { + position: fixed; + top: 25px; + right: 25px; +} diff --git a/src/client/components/SortableGrid/components/FloatingControls/FloatingControls.module.css.d.ts b/src/client/components/SortableGrid/components/FloatingControls/FloatingControls.module.css.d.ts new file mode 100644 index 0000000..55802f2 --- /dev/null +++ b/src/client/components/SortableGrid/components/FloatingControls/FloatingControls.module.css.d.ts @@ -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; diff --git a/src/client/components/SortableGrid/components/FloatingControls/FloatingControls.tsx b/src/client/components/SortableGrid/components/FloatingControls/FloatingControls.tsx new file mode 100644 index 0000000..d504c4d --- /dev/null +++ b/src/client/components/SortableGrid/components/FloatingControls/FloatingControls.tsx @@ -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
{children}
; +} diff --git a/src/client/components/SortableGrid/components/FloatingControls/index.ts b/src/client/components/SortableGrid/components/FloatingControls/index.ts new file mode 100644 index 0000000..efa7417 --- /dev/null +++ b/src/client/components/SortableGrid/components/FloatingControls/index.ts @@ -0,0 +1 @@ +export {FloatingControls} from './FloatingControls'; diff --git a/src/client/components/SortableGrid/components/Grid/Grid.module.css b/src/client/components/SortableGrid/components/Grid/Grid.module.css new file mode 100644 index 0000000..31c1c09 --- /dev/null +++ b/src/client/components/SortableGrid/components/Grid/Grid.module.css @@ -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; +} diff --git a/src/client/components/SortableGrid/components/Grid/Grid.module.css.d.ts b/src/client/components/SortableGrid/components/Grid/Grid.module.css.d.ts new file mode 100644 index 0000000..2e04c36 --- /dev/null +++ b/src/client/components/SortableGrid/components/Grid/Grid.module.css.d.ts @@ -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; diff --git a/src/client/components/SortableGrid/components/Grid/Grid.tsx b/src/client/components/SortableGrid/components/Grid/Grid.tsx new file mode 100644 index 0000000..20d8216 --- /dev/null +++ b/src/client/components/SortableGrid/components/Grid/Grid.tsx @@ -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 ( +
+ ); +} diff --git a/src/client/components/SortableGrid/components/Grid/index.ts b/src/client/components/SortableGrid/components/Grid/index.ts new file mode 100644 index 0000000..efd9c71 --- /dev/null +++ b/src/client/components/SortableGrid/components/Grid/index.ts @@ -0,0 +1 @@ +export {Grid} from './Grid'; diff --git a/src/client/components/SortableGrid/components/GridContainer/GridContainer.module.css b/src/client/components/SortableGrid/components/GridContainer/GridContainer.module.css new file mode 100644 index 0000000..3e1be88 --- /dev/null +++ b/src/client/components/SortableGrid/components/GridContainer/GridContainer.module.css @@ -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); + } +} diff --git a/src/client/components/SortableGrid/components/GridContainer/GridContainer.module.css.d.ts b/src/client/components/SortableGrid/components/GridContainer/GridContainer.module.css.d.ts new file mode 100644 index 0000000..2c401e4 --- /dev/null +++ b/src/client/components/SortableGrid/components/GridContainer/GridContainer.module.css.d.ts @@ -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; diff --git a/src/client/components/SortableGrid/components/GridContainer/GridContainer.tsx b/src/client/components/SortableGrid/components/GridContainer/GridContainer.tsx new file mode 100644 index 0000000..8cfe184 --- /dev/null +++ b/src/client/components/SortableGrid/components/GridContainer/GridContainer.tsx @@ -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 ( +
    + {children} +
+ ); +} diff --git a/src/client/components/SortableGrid/components/GridContainer/index.ts b/src/client/components/SortableGrid/components/GridContainer/index.ts new file mode 100644 index 0000000..063c01e --- /dev/null +++ b/src/client/components/SortableGrid/components/GridContainer/index.ts @@ -0,0 +1 @@ +export {GridContainer} from './GridContainer'; diff --git a/src/client/components/SortableGrid/components/Item/Item.module.css b/src/client/components/SortableGrid/components/Item/Item.module.css new file mode 100644 index 0000000..25581dd --- /dev/null +++ b/src/client/components/SortableGrid/components/Item/Item.module.css @@ -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; +} diff --git a/src/client/components/SortableGrid/components/Item/Item.tsx b/src/client/components/SortableGrid/components/Item/Item.tsx new file mode 100644 index 0000000..dcb55f4 --- /dev/null +++ b/src/client/components/SortableGrid/components/Item/Item.tsx @@ -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; + style: React.CSSProperties | undefined; + transform: Props["transform"]; + transition: Props["transition"]; + value: Props["value"]; + }): React.ReactElement; +} + +export const Item = React.memo( + React.forwardRef( + ( + { + 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, + }) + ) : ( +
  • +
    + {value} + + {onRemove ? ( + + ) : null} + {handle ? : null} + +
    +
  • + ); + }, + ), +); diff --git a/src/client/components/SortableGrid/components/Item/components/Action/Action.module.css b/src/client/components/SortableGrid/components/Item/components/Action/Action.module.css new file mode 100644 index 0000000..8fa6bfc --- /dev/null +++ b/src/client/components/SortableGrid/components/Item/components/Action/Action.module.css @@ -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; + } +} diff --git a/src/client/components/SortableGrid/components/Item/components/Action/Action.module.css.d.ts b/src/client/components/SortableGrid/components/Item/components/Action/Action.module.css.d.ts new file mode 100644 index 0000000..73364ab --- /dev/null +++ b/src/client/components/SortableGrid/components/Item/components/Action/Action.module.css.d.ts @@ -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; diff --git a/src/client/components/SortableGrid/components/Item/components/Action/Action.tsx b/src/client/components/SortableGrid/components/Item/components/Action/Action.tsx new file mode 100644 index 0000000..5faee68 --- /dev/null +++ b/src/client/components/SortableGrid/components/Item/components/Action/Action.tsx @@ -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 { + active?: { + fill: string; + background: string; + }; + cursor?: CSSProperties['cursor']; +} + +export function Action({active, className, cursor, style, ...props}: Props) { + return ( +