From e52085b3e5bed2b0ea41fa6e593778fc00576f22 Mon Sep 17 00:00:00 2001 From: Piero Nicolli Date: Tue, 13 Feb 2024 12:06:29 +0100 Subject: [PATCH 1/8] wip on new Table and Contents components --- packages/components/package.json | 5 +- .../src/components/Checkbox/Checkbox.scss | 100 +++++++ .../components/Checkbox/Checkbox.stories.tsx | 18 ++ .../src/components/Checkbox/Checkbox.tsx | 26 ++ .../src/components/Container/Container.tsx | 4 +- .../components/Contents/Contents.module.scss | 11 + .../components/Contents/Contents.stories.tsx | 27 ++ .../src/components/Contents/Contents.tsx | 83 +++++ .../src/components/Contents/tableConfig.tsx | 16 + .../components/src/components/Input/Input.tsx | 4 +- .../src/components/Table/Column.scss | 9 + .../src/components/Table/Column.tsx | 19 ++ .../components/src/components/Table/Row.tsx | 34 +++ .../src/components/Table/Table.scss | 283 ++++++++++++++++++ .../src/components/Table/Table.stories.tsx | 27 ++ .../components/src/components/Table/Table.tsx | 61 ++++ .../src/components/Table/TableHeader.tsx | 28 ++ packages/types/src/content/brains.d.ts | 52 ++++ packages/types/src/content/index.d.ts | 1 + pnpm-lock.yaml | 72 ++++- 20 files changed, 875 insertions(+), 5 deletions(-) create mode 100644 packages/components/src/components/Checkbox/Checkbox.scss create mode 100644 packages/components/src/components/Checkbox/Checkbox.stories.tsx create mode 100644 packages/components/src/components/Checkbox/Checkbox.tsx create mode 100644 packages/components/src/components/Contents/Contents.module.scss create mode 100644 packages/components/src/components/Contents/Contents.stories.tsx create mode 100644 packages/components/src/components/Contents/Contents.tsx create mode 100644 packages/components/src/components/Contents/tableConfig.tsx create mode 100644 packages/components/src/components/Table/Column.scss create mode 100644 packages/components/src/components/Table/Column.tsx create mode 100644 packages/components/src/components/Table/Row.tsx create mode 100644 packages/components/src/components/Table/Table.scss create mode 100644 packages/components/src/components/Table/Table.stories.tsx create mode 100644 packages/components/src/components/Table/Table.tsx create mode 100644 packages/components/src/components/Table/TableHeader.tsx create mode 100644 packages/types/src/content/brains.d.ts diff --git a/packages/components/package.json b/packages/components/package.json index 4d417ea773..a70bacd5da 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -71,7 +71,7 @@ "devDependencies": { "@parcel/packager-ts": "2.10.3", "@parcel/transformer-typescript-types": "2.10.3", - "@plone/types": "workspace: *", + "@plone/types": "workspace:*", "@react-types/shared": "^3.22.0", "@storybook/addon-essentials": "^7.5.1", "@storybook/addon-interactions": "^7.5.1", @@ -114,8 +114,11 @@ "vitest-axe": "^0.1.0" }, "dependencies": { + "@dnd-kit/core": "^6.1.0", + "@dnd-kit/sortable": "^8.0.0", "@react-aria/utils": "^3.22.0", "@react-spectrum/utils": "^3.11.1", + "@tanstack/react-table": "^8.11.8", "classnames": "^2.3.2", "clsx": "^2.0.0", "lodash": "^4.17.21", diff --git a/packages/components/src/components/Checkbox/Checkbox.scss b/packages/components/src/components/Checkbox/Checkbox.scss new file mode 100644 index 0000000000..a003eb5331 --- /dev/null +++ b/packages/components/src/components/Checkbox/Checkbox.scss @@ -0,0 +1,100 @@ +.react-aria-Checkbox { + // TODO improve available colors + --border-color: var(--denim); + --border-color-pressed: var(--space); + --selected-color: var(--denim); + --selected-color-pressed: var(--space); + --checkmark-color: var(--air); + + display: flex; + align-items: center; + color: var(--text-color); + font-size: 1.143rem; + forced-color-adjust: none; + gap: 0.571rem; + + .checkbox { + display: flex; + width: 1.143rem; + height: 1.143rem; + align-items: center; + justify-content: center; + border: 2px solid var(--border-color); + border-radius: 4px; + transition: all 200ms; + } + + svg { + width: 1rem; + height: 1rem; + fill: none; + stroke: var(--checkmark-color); + stroke-dasharray: 22px; + stroke-dashoffset: 66; + stroke-width: 3px; + transition: all 200ms; + } + + &[data-pressed] .checkbox { + border-color: var(--border-color-pressed); + } + + &[data-focus-visible] .checkbox { + outline: 2px solid var(--focus-ring-color); + outline-offset: 2px; + } + + &[data-selected], + &[data-indeterminate] { + .checkbox { + border-color: var(--selected-color); + background: var(--selected-color); + } + + &[data-pressed] .checkbox { + border-color: var(--selected-color-pressed); + background: var(--selected-color-pressed); + } + + svg { + stroke-dashoffset: 44; + } + } + + &[data-indeterminate] { + & svg { + fill: var(--checkmark-color); + stroke: none; + } + } + + &[data-invalid] { + .checkbox { + --checkmark-color: var(--gray-50); + border-color: var(--invalid-color); + } + + &[data-pressed] .checkbox { + border-color: var(--invalid-color-pressed); + } + + &[data-selected], + &[data-indeterminate] { + .checkbox { + background: var(--invalid-color); + } + + &[data-pressed] .checkbox { + background: var(--invalid-color-pressed); + } + } + } + + &[data-disabled] { + color: var(--text-color-disabled); + + .checkbox { + border-color: var(--border-color-disabled); + } + } +} diff --git a/packages/components/src/components/Checkbox/Checkbox.stories.tsx b/packages/components/src/components/Checkbox/Checkbox.stories.tsx new file mode 100644 index 0000000000..036d17fe54 --- /dev/null +++ b/packages/components/src/components/Checkbox/Checkbox.stories.tsx @@ -0,0 +1,18 @@ +import Checkbox from './Checkbox'; +import type { Meta, StoryObj } from '@storybook/react'; + +const meta = { + title: 'Checkbox', + component: Checkbox, + tags: ['autodocs'], +} as Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + isIndeterminate: false, + children: 'Checkbox', + }, +}; diff --git a/packages/components/src/components/Checkbox/Checkbox.tsx b/packages/components/src/components/Checkbox/Checkbox.tsx new file mode 100644 index 0000000000..7855df7880 --- /dev/null +++ b/packages/components/src/components/Checkbox/Checkbox.tsx @@ -0,0 +1,26 @@ +import { + type CheckboxProps, + Checkbox as RACCheckbox, +} from 'react-aria-components'; +import './Checkbox.scss'; + +export default function Checkbox({ children, ...props }: CheckboxProps) { + return ( + + {({ isIndeterminate }) => ( + <> +
+ +
+ {children} + + )} +
+ ); +} diff --git a/packages/components/src/components/Container/Container.tsx b/packages/components/src/components/Container/Container.tsx index 14432899a2..8a92c6d16e 100644 --- a/packages/components/src/components/Container/Container.tsx +++ b/packages/components/src/components/Container/Container.tsx @@ -10,9 +10,9 @@ type ContainerProps = { /** Additional classes. */ className: string; /** Layout size */ - layout: boolean; + layout?: boolean; /** Narrow size. */ - narrow: boolean; + narrow?: boolean; }; const Container = (props: ContainerProps) => { diff --git a/packages/components/src/components/Contents/Contents.module.scss b/packages/components/src/components/Contents/Contents.module.scss new file mode 100644 index 0000000000..bb5ce74b85 --- /dev/null +++ b/packages/components/src/components/Contents/Contents.module.scss @@ -0,0 +1,11 @@ +.topbar { + display: flex; + align-items: center; + margin-bottom: 1.5rem; +} + +.search-input { + // TODO fixme + margin-top: 0 !important; + margin-inline-start: auto; +} diff --git a/packages/components/src/components/Contents/Contents.stories.tsx b/packages/components/src/components/Contents/Contents.stories.tsx new file mode 100644 index 0000000000..f76d84f9fc --- /dev/null +++ b/packages/components/src/components/Contents/Contents.stories.tsx @@ -0,0 +1,27 @@ +import Contents from './Contents'; +import type { Meta, StoryObj } from '@storybook/react'; + +const meta = { + title: 'Views/Contents', + component: Contents, + tags: ['autodocs'], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + title: 'Section name', + pathname: '/folder', + objectActions: [ + { + id: 'folderContents', + title: 'Contents', + icon: 'contents', + url: '/folder/contents', + }, + ], + loading: false, + }, +}; diff --git a/packages/components/src/components/Contents/Contents.tsx b/packages/components/src/components/Contents/Contents.tsx new file mode 100644 index 0000000000..cbfe4d79b8 --- /dev/null +++ b/packages/components/src/components/Contents/Contents.tsx @@ -0,0 +1,83 @@ +'use client'; + +import type { ActionsResponse } from '@plone/types'; +import { useState } from 'react'; +import { + Button, + // OverlayArrow, + Tooltip, + TooltipTrigger, +} from 'react-aria-components'; +import cx from 'classnames'; +import styles from './Contents.module.scss'; +import Add from '../Icons/AddIcon'; +import Breadcrumbs from '../Breadcrumbs/Breadcrumbs'; +import Container from '../Container/Container'; +import Input from '../Input/Input'; + +interface ContentsProps { + pathname: string; + objectActions: ActionsResponse['object']; + loading: boolean; + title: string; +} + +/** + * A table showing the contents of an object. + * + * It has a toolbar for interactions with the items and a searchbar for filtering. + * Items can be sorted by drag and drop. + */ +export default function Contents({ + pathname, + objectActions, + loading, + title, +}: ContentsProps) { + const [selected, setSelected] = useState([]); + // const path = getBaseUrl(pathname); + const path = pathname; + + const folderContentsActions = objectActions.find( + (action) => action.id === 'folderContents', + ); + + if (!folderContentsActions) { + // TODO current volto returns the Unauthorized component here + // it would be best if the permissions check was done at a higher level + // and this remained null + return null; + } + + return ( + + {/* TODO better loader */} + {loading &&

Loading...

} + {/* TODO helmet setting title here... or should we do it at a higher level? */} +
+
+
+ +

{title}

+
+ + + + Add content + +
+
+
+ ); +} diff --git a/packages/components/src/components/Contents/tableConfig.tsx b/packages/components/src/components/Contents/tableConfig.tsx new file mode 100644 index 0000000000..7430d206b7 --- /dev/null +++ b/packages/components/src/components/Contents/tableConfig.tsx @@ -0,0 +1,16 @@ +import { + createColumnHelper, + type Row, + flexRender, +} from '@tanstack/react-table'; +import type { Brain } from '@plone/types/src/content/brains'; + +const columnHelper = createColumnHelper(); + +export const defaultColumns = [ + columnHelper.display({ + id: 'controls', + header: ({ table }) => C, + cell: ({ row }) => , + }), +]; diff --git a/packages/components/src/components/Input/Input.tsx b/packages/components/src/components/Input/Input.tsx index 2d7ee2fa75..471f73edf3 100644 --- a/packages/components/src/components/Input/Input.tsx +++ b/packages/components/src/components/Input/Input.tsx @@ -12,6 +12,7 @@ interface InputProps extends TextFieldProps { title?: string; description?: string; error?: string[]; + className?: string; placeholder: string; } @@ -19,12 +20,13 @@ export default function Input({ title, description, error, + className, ...props }: InputProps) { return ( diff --git a/packages/components/src/components/Table/Column.scss b/packages/components/src/components/Table/Column.scss new file mode 100644 index 0000000000..ccbff0a53e --- /dev/null +++ b/packages/components/src/components/Table/Column.scss @@ -0,0 +1,9 @@ +.react-aria-Column { + .sort-indicator { + padding: 0 2px; + } + + &:not([data-sort-direction]) .sort-indicator { + visibility: hidden; + } +} diff --git a/packages/components/src/components/Table/Column.tsx b/packages/components/src/components/Table/Column.tsx new file mode 100644 index 0000000000..86d8d3917b --- /dev/null +++ b/packages/components/src/components/Table/Column.tsx @@ -0,0 +1,19 @@ +import { type ColumnProps, Column as RACColumn } from 'react-aria-components'; +import './Column.scss'; + +export default function Column(props: ColumnProps) { + return ( + + {({ allowsSorting, sortDirection }) => ( + <> + {props.children} + {allowsSorting && ( + + )} + + )} + + ); +} diff --git a/packages/components/src/components/Table/Row.tsx b/packages/components/src/components/Table/Row.tsx new file mode 100644 index 0000000000..52ba639083 --- /dev/null +++ b/packages/components/src/components/Table/Row.tsx @@ -0,0 +1,34 @@ +import { + type RowProps, + Row as RACRow, + Cell, + Collection, + useTableOptions, + Button, +} from 'react-aria-components'; +import Checkbox from '../Checkbox/Checkbox'; + +export default function Row({ + id, + columns, + children, + ...otherProps +}: RowProps) { + let { selectionBehavior, allowsDragging } = useTableOptions(); + + return ( + + {allowsDragging && ( + + + + )} + {selectionBehavior === 'toggle' && ( + + + + )} + {children} + + ); +} diff --git a/packages/components/src/components/Table/Table.scss b/packages/components/src/components/Table/Table.scss new file mode 100644 index 0000000000..c4a0829bcc --- /dev/null +++ b/packages/components/src/components/Table/Table.scss @@ -0,0 +1,283 @@ +.react-aria-Table { + --border-color: var(--smoke); + // min-height: 100px; + // align-self: start; + max-width: 100%; + + padding: 0.286rem; + border: 1px solid var(--border-color); + border-spacing: 0; + forced-color-adjust: none; + // border-radius: 6px; + // background: var(--overlay-background); + outline: none; + word-break: break-word; + + &[data-focus-visible] { + outline: 2px solid var(--focus-ring-color); + outline-offset: -1px; + } + + .react-aria-TableHeader { + color: var(--text-color); + + &:after { + display: table-row; + height: 2px; + content: ''; + } + + & tr:last-child .react-aria-Column { + border-bottom: 1px solid var(--border-color); + cursor: default; + } + } + + .react-aria-Row { + --radius-top: 6px; + --radius-bottom: 6px; + --radius: var(--radius-top) var(--radius-top) var(--radius-bottom) + var(--radius-bottom); + position: relative; + border-radius: var(--radius); + clip-path: inset(0 round var(--radius)); /* firefox */ + color: var(--text-color); + cursor: default; + font-size: 1.072rem; + outline: none; + transform: scale(1); + + &[data-focus-visible] { + outline: 2px solid var(--focus-ring-color); + outline-offset: -2px; + } + + &[data-pressed] { + background: var(--gray-100); + } + + &[data-selected] { + background: var(--highlight-background); + color: var(--highlight-foreground); + --focus-ring-color: var(--highlight-foreground); + + &[data-focus-visible], + .react-aria-Cell[data-focus-visible] { + outline-offset: -4px; + } + } + + &[data-disabled] { + color: var(--text-color-disabled); + } + } + + .react-aria-Cell, + .react-aria-Column { + padding: 4px 8px; + outline: none; + text-align: left; + + &[data-focus-visible] { + outline: 2px solid var(--focus-ring-color); + outline-offset: -2px; + } + } + + .react-aria-Cell { + transform: translateZ(0); + + &:first-child { + border-radius: var(--radius-top) 0 0 var(--radius-bottom); + } + + &:last-child { + border-radius: 0 var(--radius-top) var(--radius-bottom) 0; + } + } + + /* join selected items if :has selector is supported */ + @supports selector(:has(.foo)) { + .react-aria-Row[data-selected]:has(+ [data-selected]), + .react-aria-Row[data-selected]:has( + + .react-aria-DropIndicator + [data-selected] + ) { + --radius-bottom: 0px; + } + + .react-aria-Row[data-selected] + [data-selected], + .react-aria-Row[data-selected] + + .react-aria-DropIndicator + + [data-selected] { + --radius-top: 0px; + } + } +} + +:where(.react-aria-Row) .react-aria-Checkbox { + --selected-color: var(--highlight-foreground); + --selected-color-pressed: var(--highlight-foreground-pressed); + --checkmark-color: var(--highlight-background); + --background-color: var(--highlight-background); +} + +// .react-aria-TableBody { +// &[data-empty] { +// text-align: center; +// font-style: italic; +// } +// } + +.react-aria-ResizableTableContainer { + position: relative; + // max-width: 400px; + overflow: auto; + border: 1px solid var(--border-color); + border-radius: 6px; + background: var(--background-color); + + .react-aria-Table { + border: none; + } + + .flex-wrapper { + display: flex; + align-items: center; + } + + .column-name, + .react-aria-Button { + --background-color: var(--overlay-background); + overflow: hidden; + flex: 1; + border-color: transparent; + color: inherit; + font: inherit; + text-align: start; + text-overflow: ellipsis; + transition: background 200ms; + &[data-hovered] { + background: var(--highlight-hover); + } + + &[data-pressed] { + background: var(--highlight-pressed); + box-shadow: none; + } + + &:focus-visible { + outline: 2px solid var(--focus-ring-color); + } + } + + .react-aria-ColumnResizer { + width: 15px; + height: 25px; + box-sizing: border-box; + flex: 0 0 auto; + border: 5px; + border-style: none solid; + border-color: transparent; + background-clip: content-box; + background-color: grey; + touch-action: none; + + &[data-resizable-direction='both'] { + cursor: ew-resize; + } + + &[data-resizable-direction='left'] { + cursor: e-resize; + } + + &[data-resizable-direction='right'] { + cursor: w-resize; + } + + &[data-focus-visible] { + background-color: var(--focus-ring-color); + } + + &[data-resizing] { + border-color: var(--focus-ring-color); + background-color: transparent; + } + } + + .react-aria-Column, + .react-aria-Cell { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } +} + +.react-aria-Row { + &[data-dragging] { + opacity: 0.6; + transform: translateZ(0); + } + + [slot='drag'] { + all: unset; + width: 15px; + text-align: center; + + &[data-focus-visible] { + border-radius: 4px; + outline: 2px solid var(--focus-ring-color); + } + } +} + +.react-aria-DropIndicator[data-drop-target] { + outline: 1px solid var(--highlight-background); + transform: translateZ(0); +} + +.drag-preview { + display: flex; + width: 150px; + align-items: center; + justify-content: space-between; + padding: 4px 8px; + border-radius: 4px; + background: var(--highlight-background); + color: white; + gap: 4px; + + .badge { + padding: 0 8px; + border-radius: 4px; + background: var(--highlight-foreground); + color: var(--highlight-background); + } +} + +.react-aria-Table[data-drop-target] { + background: var(--highlight-overlay); + outline: 2px solid var(--highlight-background); + outline-offset: -1px; +} + +.react-aria-Row[data-drop-target] { + background: var(--highlight-overlay); + outline: 2px solid var(--highlight-background); +} + +.react-aria-DropIndicator[data-drop-target] { + outline: 1px solid var(--highlight-background); + transform: translateZ(0); +} + +.my-drop-indicator.active { + outline: 1px solid #e70073; + transform: translateZ(0); +} + +.react-aria-Cell img { + display: block; + width: 30px; + height: 30px; + object-fit: cover; +} diff --git a/packages/components/src/components/Table/Table.stories.tsx b/packages/components/src/components/Table/Table.stories.tsx new file mode 100644 index 0000000000..05662364ed --- /dev/null +++ b/packages/components/src/components/Table/Table.stories.tsx @@ -0,0 +1,27 @@ +import Table from './Table'; +import type { Meta, StoryObj } from '@storybook/react'; + +const meta = { + title: 'Table', + component: Table, + tags: ['autodocs'], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + columns: [ + { name: 'Name', id: 'name', isRowHeader: true }, + { name: 'Type', id: 'type' }, + { name: 'Date Modified', id: 'date' }, + ], + rows: [ + { id: '1', name: 'Games', date: '6/7/2020', type: 'File folder' }, + { id: '2', name: 'Program Files', date: '4/7/2021', type: 'File folder' }, + { id: '3', name: 'bootmgr', date: '11/20/2010', type: 'System file' }, + { id: '4', name: 'log.txt', date: '1/18/2016', type: 'Text Document' }, + ], + }, +}; diff --git a/packages/components/src/components/Table/Table.tsx b/packages/components/src/components/Table/Table.tsx new file mode 100644 index 0000000000..2a392537d1 --- /dev/null +++ b/packages/components/src/components/Table/Table.tsx @@ -0,0 +1,61 @@ +import { + type TableProps as RACTableProps, + ResizableTableContainer, + ColumnResizer, + Table as RACTable, + TableBody, + Cell, +} from 'react-aria-components'; +import './Table.scss'; +import TableHeader from './TableHeader'; +import Column from './Column'; +import Row from './Row'; + +interface ColumnType { + id: string; + name: string; + isRowHeader?: boolean; +} + +interface RowType { + id: string; + [key: string]: string; // TODO can we make this more specific? +} + +interface TableProps extends RACTableProps { + columns: C[]; + rows: R[]; +} + +/** + * A wrapper around the `react-aria-components` Table component. + * + * See https://react-spectrum.adobe.com/react-aria/Table.html + */ +export default function Table({ + columns, + rows, + ...otherProps +}: TableProps) { + return ( + + + + {(column) => ( + + {column.name} + + + )} + + + {(item) => ( + + {(column) => {item[column.id]}} + + )} + + + + ); +} diff --git a/packages/components/src/components/Table/TableHeader.tsx b/packages/components/src/components/Table/TableHeader.tsx new file mode 100644 index 0000000000..35fe470bb0 --- /dev/null +++ b/packages/components/src/components/Table/TableHeader.tsx @@ -0,0 +1,28 @@ +import { + type TableHeaderProps, + TableHeader as RACTableHeader, + useTableOptions, + Collection, +} from 'react-aria-components'; +import Checkbox from '../Checkbox/Checkbox'; +import Column from './Column'; + +export default function TableHeader({ + columns, + children, +}: TableHeaderProps) { + let { selectionBehavior, selectionMode, allowsDragging } = useTableOptions(); + + return ( + + {/* Add extra columns for drag and drop and selection. */} + {allowsDragging && } + {selectionBehavior === 'toggle' && ( + + {selectionMode === 'multiple' && } + + )} + {children} + + ); +} diff --git a/packages/types/src/content/brains.d.ts b/packages/types/src/content/brains.d.ts new file mode 100644 index 0000000000..0a1a2e6ae2 --- /dev/null +++ b/packages/types/src/content/brains.d.ts @@ -0,0 +1,52 @@ +import type { PreviewImage } from './common'; + +export interface Brain { + '@id': string; + '@type': string; + CreationDate: string; + Creator: string; + Date: string; + Description: string; + EffectiveDate: string | 'None'; // 'None' here is just for documentation + ExpirationDate: string | 'None'; // 'None' here is just for documentation + ModificationDate: string; + Subject: string[]; + Title: string; + Type: string; + UID: string; + author_name: string | null; + cmf_uid: string | null; + commentators: string[]; + created: string; + description: string; + effective: string | '1969-12-31T00:00:00+00:00'; // '1969-12-31T00:00:00+00:00' here is just for documentation + end: string | null; + exclude_from_nav: boolean; + expires: string | '2499-12-31T00:00:00+00:00'; // '2499-12-31T00:00:00+00:00' here is just for documentation + getIcon: string | null; // TODO is this correct? + getId: string; + getObjSize: string; + getPath: string; + getRemoteUrl: string | null; + getURL: string; + hasPreviewImage: boolean | null; // TODO is this correct? + head_title: string | null; // TODO is this correct? + id: string; + image_field: string; // TODO could this be more specific? + image_scales: Record | null; // TODO could this be more specific? + in_response_to: string | null; // TODO is this correct? + is_folderish: boolean; + last_comment_date: string | null; + listCreators: string[]; + location: string | null; // TODO is this correct? + mime_type: string; // TODO could this be more specific? + modified: string; + nav_title: string | null; // TODO is this correct? + portal_type: string; // TODO could this be more specific? + review_state: string; // TODO could this be more specific? + start: string | null; + sync_uid: string | null; + title: string; + total_comments: number; + type_title: string; // TODO could this be more specific? +} diff --git a/packages/types/src/content/index.d.ts b/packages/types/src/content/index.d.ts index b8e16c1269..bf16b511eb 100644 --- a/packages/types/src/content/index.d.ts +++ b/packages/types/src/content/index.d.ts @@ -4,6 +4,7 @@ import type { PreviewImage, RelatedItem, } from './common'; +import type { Brain } from './brains'; export interface Content { '@components': Expanders; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ab93ce3f7e..5f3671d7de 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -789,12 +789,21 @@ importers: packages/components: dependencies: + '@dnd-kit/core': + specifier: ^6.1.0 + version: 6.1.0(react-dom@18.2.0)(react@18.2.0) + '@dnd-kit/sortable': + specifier: ^8.0.0 + version: 8.0.0(@dnd-kit/core@6.1.0)(react@18.2.0) '@react-aria/utils': specifier: ^3.22.0 version: 3.22.0(react@18.2.0) '@react-spectrum/utils': specifier: ^3.11.1 version: 3.11.2(react@18.2.0) + '@tanstack/react-table': + specifier: ^8.11.8 + version: 8.11.8(react-dom@18.2.0)(react@18.2.0) classnames: specifier: ^2.3.2 version: 2.3.2 @@ -821,7 +830,7 @@ importers: specifier: 2.10.3 version: 2.10.3(@parcel/core@2.10.3)(typescript@5.2.2) '@plone/types': - specifier: 'workspace: *' + specifier: workspace:* version: link:../types '@react-types/shared': specifier: ^3.22.0 @@ -3570,6 +3579,49 @@ packages: resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==} engines: {node: '>=10.0.0'} + /@dnd-kit/accessibility@3.1.0(react@18.2.0): + resolution: {integrity: sha512-ea7IkhKvlJUv9iSHJOnxinBcoOI3ppGnnL+VDJ75O45Nss6HtZd8IdN8touXPDtASfeI2T2LImb8VOZcL47wjQ==} + peerDependencies: + react: '>=16.8.0' + dependencies: + react: 18.2.0 + tslib: 2.6.2 + dev: false + + /@dnd-kit/core@6.1.0(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-J3cQBClB4TVxwGo3KEjssGEXNJqGVWx17aRTZ1ob0FliR5IjYgTxl5YJbKTzA6IzrtelotH19v6y7uoIRUZPSg==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + dependencies: + '@dnd-kit/accessibility': 3.1.0(react@18.2.0) + '@dnd-kit/utilities': 3.2.2(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + tslib: 2.6.2 + dev: false + + /@dnd-kit/sortable@8.0.0(@dnd-kit/core@6.1.0)(react@18.2.0): + resolution: {integrity: sha512-U3jk5ebVXe1Lr7c2wU7SBZjcWdQP+j7peHJfCspnA81enlu88Mgd7CC8Q+pub9ubP7eKVETzJW+IBAhsqbSu/g==} + peerDependencies: + '@dnd-kit/core': ^6.1.0 + react: '>=16.8.0' + dependencies: + '@dnd-kit/core': 6.1.0(react-dom@18.2.0)(react@18.2.0) + '@dnd-kit/utilities': 3.2.2(react@18.2.0) + react: 18.2.0 + tslib: 2.6.2 + dev: false + + /@dnd-kit/utilities@3.2.2(react@18.2.0): + resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==} + peerDependencies: + react: '>=16.8.0' + dependencies: + react: 18.2.0 + tslib: 2.6.2 + dev: false + /@emotion/babel-plugin@11.11.0: resolution: {integrity: sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==} dependencies: @@ -12998,6 +13050,23 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@tanstack/react-table@8.11.8(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-NEwvIq4iSiDQozEyvbdiSdCOiLa+g5xHmdEnvwDb98FObcK6YkBOkRrs/CNqrKdDy+/lqoIllIWHk+M80GW6+g==} + engines: {node: '>=12'} + peerDependencies: + react: '>=16' + react-dom: '>=16' + dependencies: + '@tanstack/table-core': 8.11.8 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /@tanstack/table-core@8.11.8: + resolution: {integrity: sha512-DECHvtq4YW4U/gqg6etup7ydt/RB1Bi1pJaMpHUXl65ooW1d71Nv7BzD66rUdHrBSNdyiW3PLTPUQlpXjAgDeA==} + engines: {node: '>=12'} + dev: false + /@testing-library/cypress@9.0.0(cypress@13.1.0): resolution: {integrity: sha512-c1XiCGeHGGTWn0LAU12sFUfoX3qfId5gcSE2yHode+vsyHDWraxDPALjVnHd4/Fa3j4KBcc5k++Ccy6A9qnkMA==} engines: {node: '>=12', npm: '>=6'} @@ -30973,6 +31042,7 @@ packages: loose-envify: 1.4.0 object-assign: 4.1.1 prop-types: 15.7.2 + bundledDependencies: false /react@17.0.2: resolution: {integrity: sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==} From fc3c31c6a8e32b684a8f187048fafd6997267b11 Mon Sep 17 00:00:00 2001 From: Piero Nicolli Date: Tue, 13 Feb 2024 18:21:32 +0100 Subject: [PATCH 2/8] wip on table component --- packages/components/package.json | 3 - .../src/components/Checkbox/Checkbox.scss | 17 +-- .../src/components/Table/Table.scss | 132 +++++++----------- .../src/components/Table/Table.stories.tsx | 21 +++ .../components/src/components/Table/Table.tsx | 69 ++++++--- packages/components/src/styles/_theme.scss | 124 ++++++++++++++++ packages/components/src/styles/main.scss | 1 + .../src/styles/rules/_variables.scss | 55 ++++---- pnpm-lock.yaml | 69 --------- 9 files changed, 281 insertions(+), 210 deletions(-) create mode 100644 packages/components/src/styles/_theme.scss diff --git a/packages/components/package.json b/packages/components/package.json index a70bacd5da..1eb16c2615 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -114,11 +114,8 @@ "vitest-axe": "^0.1.0" }, "dependencies": { - "@dnd-kit/core": "^6.1.0", - "@dnd-kit/sortable": "^8.0.0", "@react-aria/utils": "^3.22.0", "@react-spectrum/utils": "^3.11.1", - "@tanstack/react-table": "^8.11.8", "classnames": "^2.3.2", "clsx": "^2.0.0", "lodash": "^4.17.21", diff --git a/packages/components/src/components/Checkbox/Checkbox.scss b/packages/components/src/components/Checkbox/Checkbox.scss index a003eb5331..acc2292238 100644 --- a/packages/components/src/components/Checkbox/Checkbox.scss +++ b/packages/components/src/components/Checkbox/Checkbox.scss @@ -1,10 +1,7 @@ .react-aria-Checkbox { - // TODO improve available colors - --border-color: var(--denim); - --border-color-pressed: var(--space); - --selected-color: var(--denim); - --selected-color-pressed: var(--space); - --checkmark-color: var(--air); + --selected-color: var(--highlight-background); + --selected-color-pressed: var(--highlight-background-pressed); + --checkmark-color: var(--highlight-foreground); display: flex; align-items: center; @@ -71,21 +68,21 @@ &[data-invalid] { .checkbox { --checkmark-color: var(--gray-50); - border-color: var(--invalid-color); + border-color: var(--color-invalid); } &[data-pressed] .checkbox { - border-color: var(--invalid-color-pressed); + border-color: var(--color-pressed-invalid); } &[data-selected], &[data-indeterminate] { .checkbox { - background: var(--invalid-color); + background: var(--color-invalid); } &[data-pressed] .checkbox { - background: var(--invalid-color-pressed); + background: var(--color-pressed-invalid); } } } diff --git a/packages/components/src/components/Table/Table.scss b/packages/components/src/components/Table/Table.scss index c4a0829bcc..3d2b68a768 100644 --- a/packages/components/src/components/Table/Table.scss +++ b/packages/components/src/components/Table/Table.scss @@ -1,11 +1,15 @@ .react-aria-Table { - --border-color: var(--smoke); + --q-table-border: 0 none; + --q-table-padding: 0.286rem; + --q-table-header-color: var(--sapphire); + --q-table-header-border-bottom: 1px solid var(--border-color); + --q-table-row-pressed: var(--highlight-pressed); // min-height: 100px; // align-self: start; max-width: 100%; - padding: 0.286rem; - border: 1px solid var(--border-color); + padding: var(--q-table-padding); + border: var(--q-table-border); border-spacing: 0; forced-color-adjust: none; // border-radius: 6px; @@ -19,32 +23,32 @@ } .react-aria-TableHeader { - color: var(--text-color); + color: var(--q-table-header-color); - &:after { - display: table-row; - height: 2px; - content: ''; - } + // &:after { + // content: ''; + // display: table-row; + // height: 2px; + // } & tr:last-child .react-aria-Column { - border-bottom: 1px solid var(--border-color); + border-bottom: var(--q-table-header-border-bottom); cursor: default; } } .react-aria-Row { - --radius-top: 6px; - --radius-bottom: 6px; - --radius: var(--radius-top) var(--radius-top) var(--radius-bottom) - var(--radius-bottom); + // color: var(--text-color); + // font-size: 1.072rem; position: relative; - border-radius: var(--radius); - clip-path: inset(0 round var(--radius)); /* firefox */ - color: var(--text-color); + // --radius-top: 6px; + // --radius-bottom: 6px; + // --radius: var(--radius-top) var(--radius-top) var(--radius-bottom) + // var(--radius-bottom); + // border-radius: var(--radius); + // clip-path: inset(0 round var(--radius)); /* firefox */ + // outline: none; cursor: default; - font-size: 1.072rem; - outline: none; transform: scale(1); &[data-focus-visible] { @@ -53,7 +57,7 @@ } &[data-pressed] { - background: var(--gray-100); + background: var(--q-table-row-pressed); } &[data-selected] { @@ -87,31 +91,31 @@ .react-aria-Cell { transform: translateZ(0); - &:first-child { - border-radius: var(--radius-top) 0 0 var(--radius-bottom); - } + // &:first-child { + // border-radius: var(--radius-top) 0 0 var(--radius-bottom); + // } - &:last-child { - border-radius: 0 var(--radius-top) var(--radius-bottom) 0; - } + // &:last-child { + // border-radius: 0 var(--radius-top) var(--radius-bottom) 0; + // } } /* join selected items if :has selector is supported */ - @supports selector(:has(.foo)) { - .react-aria-Row[data-selected]:has(+ [data-selected]), - .react-aria-Row[data-selected]:has( - + .react-aria-DropIndicator + [data-selected] - ) { - --radius-bottom: 0px; - } - - .react-aria-Row[data-selected] + [data-selected], - .react-aria-Row[data-selected] - + .react-aria-DropIndicator - + [data-selected] { - --radius-top: 0px; - } - } + // @supports selector(:has(.foo)) { + // .react-aria-Row[data-selected]:has(+ [data-selected]), + // .react-aria-Row[data-selected]:has( + // + .react-aria-DropIndicator + [data-selected] + // ) { + // --radius-bottom: 0px; + // } + + // .react-aria-Row[data-selected] + [data-selected], + // .react-aria-Row[data-selected] + // + .react-aria-DropIndicator + // + [data-selected] { + // --radius-top: 0px; + // } + // } } :where(.react-aria-Row) .react-aria-Checkbox { @@ -132,9 +136,9 @@ position: relative; // max-width: 400px; overflow: auto; - border: 1px solid var(--border-color); - border-radius: 6px; - background: var(--background-color); + border: var(--q-table-border); + // border-radius: 6px; + // background: var(--background-color); .react-aria-Table { border: none; @@ -145,6 +149,8 @@ align-items: center; } + // TODO double check these styles to strip out unnecessary ones + // and keep only the bare minimum .column-name, .react-aria-Button { --background-color: var(--overlay-background); @@ -170,6 +176,8 @@ } } + // TODO double check these styles to strip out unnecessary ones + // and keep only the bare minimum .react-aria-ColumnResizer { width: 15px; height: 25px; @@ -235,25 +243,6 @@ transform: translateZ(0); } -.drag-preview { - display: flex; - width: 150px; - align-items: center; - justify-content: space-between; - padding: 4px 8px; - border-radius: 4px; - background: var(--highlight-background); - color: white; - gap: 4px; - - .badge { - padding: 0 8px; - border-radius: 4px; - background: var(--highlight-foreground); - color: var(--highlight-background); - } -} - .react-aria-Table[data-drop-target] { background: var(--highlight-overlay); outline: 2px solid var(--highlight-background); @@ -264,20 +253,3 @@ background: var(--highlight-overlay); outline: 2px solid var(--highlight-background); } - -.react-aria-DropIndicator[data-drop-target] { - outline: 1px solid var(--highlight-background); - transform: translateZ(0); -} - -.my-drop-indicator.active { - outline: 1px solid #e70073; - transform: translateZ(0); -} - -.react-aria-Cell img { - display: block; - width: 30px; - height: 30px; - object-fit: cover; -} diff --git a/packages/components/src/components/Table/Table.stories.tsx b/packages/components/src/components/Table/Table.stories.tsx index 05662364ed..dd6f658ee7 100644 --- a/packages/components/src/components/Table/Table.stories.tsx +++ b/packages/components/src/components/Table/Table.stories.tsx @@ -25,3 +25,24 @@ export const Default: Story = { ], }, }; + +export const SingleSelection: Story = { + args: { + ...Default.args, + selectionMode: 'single', + }, +}; + +export const MultipleSelection: Story = { + args: { + ...Default.args, + selectionMode: 'multiple', + }, +}; + +export const Resizable: Story = { + args: { + ...Default.args, + resizableColumns: true, + }, +}; diff --git a/packages/components/src/components/Table/Table.tsx b/packages/components/src/components/Table/Table.tsx index 2a392537d1..c8a29db20f 100644 --- a/packages/components/src/components/Table/Table.tsx +++ b/packages/components/src/components/Table/Table.tsx @@ -5,6 +5,7 @@ import { Table as RACTable, TableBody, Cell, + useDragAndDrop, } from 'react-aria-components'; import './Table.scss'; import TableHeader from './TableHeader'; @@ -15,6 +16,7 @@ interface ColumnType { id: string; name: string; isRowHeader?: boolean; + // TODO support width constraints for resizable columns } interface RowType { @@ -25,6 +27,7 @@ interface RowType { interface TableProps extends RACTableProps { columns: C[]; rows: R[]; + resizableColumns?: boolean; } /** @@ -35,27 +38,53 @@ interface TableProps extends RACTableProps { export default function Table({ columns, rows, + resizableColumns, ...otherProps }: TableProps) { - return ( - - - - {(column) => ( - - {column.name} - - - )} - - - {(item) => ( - - {(column) => {item[column.id]}} - - )} - - - + const { dragAndDropHooks } = useDragAndDrop({ + getItems: (keys) => + [...keys].map((key) => ({ + 'text/plain': rows.find((row) => row.id === key)?.name || '', + })), + onReorder(e) { + if (e.target.dropPosition === 'before') { + console.log('moveBefore: key ', e.target.key, ', keys ', e.keys); + } else if (e.target.dropPosition === 'after') { + console.log('moveAfter: key ', e.target.key, ', keys ', e.keys); + } + }, + }); + + const table = ( + + + {(column) => ( + + {resizableColumns && ( +
+ + {column.name} + + +
+ )} + {!resizableColumns && column.name} +
+ )} +
+ + {(item) => ( + + {(column) => {item[column.id]}} + + )} + +
); + + if (resizableColumns) { + return {table}; + } else { + return table; + } } diff --git a/packages/components/src/styles/_theme.scss b/packages/components/src/styles/_theme.scss new file mode 100644 index 0000000000..49b62bc312 --- /dev/null +++ b/packages/components/src/styles/_theme.scss @@ -0,0 +1,124 @@ +/* color themes for dark and light modes, generated with Leonardo. + * Light: https://leonardocolor.io/theme.html?name=Light&config=%7B%22baseScale%22%3A%22Gray%22%2C%22colorScales%22%3A%5B%7B%22name%22%3A%22Gray%22%2C%22colorKeys%22%3A%5B%22%23000000%22%5D%2C%22colorspace%22%3A%22RGB%22%2C%22ratios%22%3A%5B%22-1.12%22%2C%221.45%22%2C%222.05%22%2C%223.02%22%2C%224.54%22%2C%227%22%2C%2210.86%22%5D%2C%22smooth%22%3Afalse%7D%2C%7B%22name%22%3A%22Purple%22%2C%22colorKeys%22%3A%5B%22%235e30eb%22%5D%2C%22colorspace%22%3A%22RGB%22%2C%22ratios%22%3A%5B%22-1.12%22%2C%221.45%22%2C%222.05%22%2C%223.02%22%2C%224.54%22%2C%227%22%2C%2210.86%22%5D%2C%22smooth%22%3Afalse%7D%2C%7B%22name%22%3A%22Red%22%2C%22colorKeys%22%3A%5B%22%23e32400%22%5D%2C%22colorspace%22%3A%22RGB%22%2C%22ratios%22%3A%5B%22-1.12%22%2C%221.45%22%2C%222.05%22%2C%223.02%22%2C%224.54%22%2C%227%22%2C%2210.86%22%5D%2C%22smooth%22%3Afalse%7D%5D%2C%22lightness%22%3A98%2C%22contrast%22%3A1%2C%22saturation%22%3A100%2C%22formula%22%3A%22wcag2%22%7D */ +:root { + --background-color: var(--air); + --gray-50: #ffffff; + --gray-100: #d0d0d0; + --gray-200: #afafaf; + --gray-300: #8f8f8f; + --gray-400: #717171; + --gray-500: #555555; + --gray-600: #393939; + --purple-100: #d5c9fa; + --purple-200: #b8a3f6; + --purple-300: #997cf2; + --purple-400: #7a54ef; + --purple-500: #582ddc; + --purple-600: #3c1e95; + --red-100: #f7c4ba; + --red-200: #f29887; + --red-300: #eb664d; + --red-400: #de2300; + --red-500: #a81b00; + --red-600: #731200; + --highlight-hover: rgb(0 0 0 / 0.07); + --highlight-pressed: rgb(0 0 0 / 0.15); + --shadow-exo: 0px 6px 12px 0px rgba(2, 19, 34, 0.06), + 0px 9px 18px 0px rgba(2, 19, 34, 0.18); +} + +/* Dark: https://leonardocolor.io/theme.html?name=Dark&config=%7B%22baseScale%22%3A%22Gray%22%2C%22colorScales%22%3A%5B%7B%22name%22%3A%22Gray%22%2C%22colorKeys%22%3A%5B%22%23000000%22%5D%2C%22colorspace%22%3A%22RGB%22%2C%22ratios%22%3A%5B%22-1.12%22%2C%221.45%22%2C%222.05%22%2C%223.02%22%2C%224.54%22%2C%227%22%2C%2210.86%22%5D%2C%22smooth%22%3Afalse%7D%2C%7B%22name%22%3A%22Purple%22%2C%22colorKeys%22%3A%5B%22%235e30eb%22%5D%2C%22colorspace%22%3A%22RGB%22%2C%22ratios%22%3A%5B%22-1.12%22%2C%221.45%22%2C%222.05%22%2C%223.02%22%2C%224.54%22%2C%227%22%2C%2210.86%22%5D%2C%22smooth%22%3Afalse%7D%2C%7B%22name%22%3A%22Red%22%2C%22colorKeys%22%3A%5B%22%23e32400%22%5D%2C%22colorspace%22%3A%22RGB%22%2C%22ratios%22%3A%5B%22-1.12%22%2C%221.45%22%2C%222.05%22%2C%223.02%22%2C%224.54%22%2C%227%22%2C%2210.86%22%5D%2C%22smooth%22%3Afalse%7D%5D%2C%22lightness%22%3A11%2C%22contrast%22%3A1%2C%22saturation%22%3A100%2C%22formula%22%3A%22wcag2%22%7D */ +// @media (prefers-color-scheme: dark) { +// :root { +// --background-color: #1d1d1d; +// --gray-50: #101010; +// --gray-100: #393939; +// --gray-200: #4f4f4f; +// --gray-300: #686868; +// --gray-400: #848484; +// --gray-500: #a7a7a7; +// --gray-600: #cfcfcf; +// --purple-100: #3c1e95; +// --purple-200: #522acd; +// --purple-300: #6f46ed; +// --purple-400: #8e6ef1; +// --purple-500: #b099f5; +// --purple-600: #d5c8fa; +// --red-100: #721200; +// --red-200: #9c1900; +// --red-300: #cc2000; +// --red-400: #e95034; +// --red-500: #f08c79; +// --red-600: #f7c3ba; +// --highlight-hover: rgb(255 255 255 / 0.1); +// --highlight-pressed: rgb(255 255 255 / 0.2); +// } +// } + +/* Semantic colors */ +:root { + --focus-ring-color: var(--purple-400); + --text-color: var(--gray-600); + --text-color-base: var(--gray-500); + --text-color-hover: var(--gray-600); + --text-color-disabled: var(--gray-200); + --text-color-placeholder: var(--gray-400); + --link-color: var(--purple-500); + --link-color-secondary: var(--gray-500); + --link-color-pressed: var(--purple-600); + --border-color: var(--gray-300); + --border-color-hover: var(--gray-400); + --border-color-pressed: var(--gray-400); + --border-color-disabled: var(--gray-100); + --field-background: var(--gray-50); + --field-text-color: var(--gray-600); + --overlay-background: var(--gray-50); + // --button-background: var(--gray-50); + --button-background: transparent; + --button-background-pressed: var(--background-color); + /* these colors are the same between light and dark themes + * to ensure contrast with the foreground color */ + // --highlight-background: #6f46ed; /* purple-300 from dark theme, 3.03:1 against background-color */ + --highlight-background: var(--smoke); + // --highlight-background-pressed: #522acd; /* purple-200 from dark theme */ + --highlight-background-pressed: var(--silver); + --highlight-background-invalid: #cc2000; /* red-300 from dark theme */ + // --highlight-foreground: white; /* 5.56:1 against highlight-background */ + --highlight-foreground: var(--denim); + --highlight-foreground-pressed: #ddd; + --highlight-overlay: rgb(from #6f46ed r g b / 15%); + --invalid-color: var(--red-400); + --invalid-color-pressed: var(--red-500); +} + +/* Windows high contrast mode overrides */ +@media (forced-colors: active) { + :root { + --background-color: Canvas; + --focus-ring-color: Highlight; + --text-color: ButtonText; + --text-color-base: ButtonText; + --text-color-hover: ButtonText; + --text-color-disabled: GrayText; + --text-color-placeholder: ButtonText; + --link-color: LinkText; + --link-color-secondary: LinkText; + --link-color-pressed: LinkText; + --border-color: ButtonBorder; + --border-color-hover: ButtonBorder; + --border-color-pressed: ButtonBorder; + --border-color-disabled: GrayText; + --field-background: Field; + --field-text-color: FieldText; + --overlay-background: Canvas; + --button-background: ButtonFace; + --button-background-pressed: ButtonFace; + --highlight-background: Highlight; + --highlight-background-pressed: Highlight; + --highlight-background-invalid: LinkText; + --highlight-foreground: HighlightText; + --highlight-foreground-pressed: HighlightText; + --invalid-color: LinkText; + --invalid-color-pressed: LinkText; + } +} diff --git a/packages/components/src/styles/main.scss b/packages/components/src/styles/main.scss index 891e562dc9..0e462e4d11 100644 --- a/packages/components/src/styles/main.scss +++ b/packages/components/src/styles/main.scss @@ -9,6 +9,7 @@ } @import 'typography'; +@import 'theme'; // Fundamentals // Basic elements to build other complex components diff --git a/packages/components/src/styles/rules/_variables.scss b/packages/components/src/styles/rules/_variables.scss index 15b202d850..5b7bec4334 100644 --- a/packages/components/src/styles/rules/_variables.scss +++ b/packages/components/src/styles/rules/_variables.scss @@ -98,35 +98,34 @@ $puya: hsl($temperature-base - (calc($s-rotate / 5)), 60%, 23%); --turquoise: #{$turquoise}; --puya: #{$puya}; } -/* -@media (prefers-color-scheme: dark) { -:root { - --air: #{$space}; - --space: #{$air}; - --denim: #{$snow}; - --snow: #{$denim}; - --smoke: #{$iron}; - --silver: #{$pigeon}; - --dolphin: #{$dolphin}; - --pigeon: #{$silver}; - --iron: #{$smoke}; - - --arctic: #{$royal}; - --sky: #{$sapphire}; - --azure: #{$cobalt}; - --cobalt: #{$azure}; - --sapphire: #{$sky}; - --royal: #{$arctic}; - - --flamingo: #{$candy}; - --poppy: #{$rose}; - --rose: #{$poppy}; - --candy: #{$flamingo}; - --wine: #{$ballet}; -} -} -*/ +// @media (prefers-color-scheme: dark) { +// :root { +// --air: #{$space}; +// --space: #{$air}; +// --denim: #{$snow}; + +// --snow: #{$denim}; +// --smoke: #{$iron}; +// --silver: #{$pigeon}; +// --dolphin: #{$dolphin}; +// --pigeon: #{$silver}; +// --iron: #{$smoke}; + +// --arctic: #{$royal}; +// --sky: #{$sapphire}; +// --azure: #{$cobalt}; +// --cobalt: #{$azure}; +// --sapphire: #{$sky}; +// --royal: #{$arctic}; + +// --flamingo: #{$candy}; +// --poppy: #{$rose}; +// --rose: #{$poppy}; +// --candy: #{$flamingo}; +// --wine: #{$ballet}; +// } +// } //*// TRANSPARENCIES //*// diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3e4fc7ab1b..2a3b0f3d35 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -874,21 +874,12 @@ importers: packages/components: dependencies: - '@dnd-kit/core': - specifier: ^6.1.0 - version: 6.1.0(react-dom@18.2.0)(react@18.2.0) - '@dnd-kit/sortable': - specifier: ^8.0.0 - version: 8.0.0(@dnd-kit/core@6.1.0)(react@18.2.0) '@react-aria/utils': specifier: ^3.22.0 version: 3.22.0(react@18.2.0) '@react-spectrum/utils': specifier: ^3.11.1 version: 3.11.2(react@18.2.0) - '@tanstack/react-table': - specifier: ^8.11.8 - version: 8.11.8(react-dom@18.2.0)(react@18.2.0) classnames: specifier: ^2.3.2 version: 2.3.2 @@ -4969,49 +4960,6 @@ packages: resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==} engines: {node: '>=10.0.0'} - /@dnd-kit/accessibility@3.1.0(react@18.2.0): - resolution: {integrity: sha512-ea7IkhKvlJUv9iSHJOnxinBcoOI3ppGnnL+VDJ75O45Nss6HtZd8IdN8touXPDtASfeI2T2LImb8VOZcL47wjQ==} - peerDependencies: - react: '>=16.8.0' - dependencies: - react: 18.2.0 - tslib: 2.6.2 - dev: false - - /@dnd-kit/core@6.1.0(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-J3cQBClB4TVxwGo3KEjssGEXNJqGVWx17aRTZ1ob0FliR5IjYgTxl5YJbKTzA6IzrtelotH19v6y7uoIRUZPSg==} - peerDependencies: - react: '>=16.8.0' - react-dom: '>=16.8.0' - dependencies: - '@dnd-kit/accessibility': 3.1.0(react@18.2.0) - '@dnd-kit/utilities': 3.2.2(react@18.2.0) - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - tslib: 2.6.2 - dev: false - - /@dnd-kit/sortable@8.0.0(@dnd-kit/core@6.1.0)(react@18.2.0): - resolution: {integrity: sha512-U3jk5ebVXe1Lr7c2wU7SBZjcWdQP+j7peHJfCspnA81enlu88Mgd7CC8Q+pub9ubP7eKVETzJW+IBAhsqbSu/g==} - peerDependencies: - '@dnd-kit/core': ^6.1.0 - react: '>=16.8.0' - dependencies: - '@dnd-kit/core': 6.1.0(react-dom@18.2.0)(react@18.2.0) - '@dnd-kit/utilities': 3.2.2(react@18.2.0) - react: 18.2.0 - tslib: 2.6.2 - dev: false - - /@dnd-kit/utilities@3.2.2(react@18.2.0): - resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==} - peerDependencies: - react: '>=16.8.0' - dependencies: - react: 18.2.0 - tslib: 2.6.2 - dev: false - /@emotion/babel-plugin@11.11.0: resolution: {integrity: sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==} dependencies: @@ -15187,23 +15135,6 @@ packages: resolution: {integrity: sha512-GnolmC8Fr4mvsHE1fGQmR3Nm0eBO3KnZjDU0a+P3TeQNM/dDscFGxtA7p31NplQNW3KwBw4t1RVFmz0VeKLxcw==} dev: false - /@tanstack/react-table@8.11.8(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-NEwvIq4iSiDQozEyvbdiSdCOiLa+g5xHmdEnvwDb98FObcK6YkBOkRrs/CNqrKdDy+/lqoIllIWHk+M80GW6+g==} - engines: {node: '>=12'} - peerDependencies: - react: '>=16' - react-dom: '>=16' - dependencies: - '@tanstack/table-core': 8.11.8 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - dev: false - - /@tanstack/table-core@8.11.8: - resolution: {integrity: sha512-DECHvtq4YW4U/gqg6etup7ydt/RB1Bi1pJaMpHUXl65ooW1d71Nv7BzD66rUdHrBSNdyiW3PLTPUQlpXjAgDeA==} - engines: {node: '>=12'} - dev: false - /@testing-library/cypress@9.0.0(cypress@13.1.0): resolution: {integrity: sha512-c1XiCGeHGGTWn0LAU12sFUfoX3qfId5gcSE2yHode+vsyHDWraxDPALjVnHd4/Fa3j4KBcc5k++Ccy6A9qnkMA==} engines: {node: '>=12', npm: '>=6'} From 19b3beab1d2884a3fedfef473e239c9de089b6c0 Mon Sep 17 00:00:00 2001 From: Piero Nicolli Date: Thu, 15 Feb 2024 01:34:08 +0100 Subject: [PATCH 3/8] wip on base theme colors --- .../components/src/components/Table/Row.scss | 17 ++++++ .../components/src/components/Table/Row.tsx | 6 ++- .../src/components/Table/Table.scss | 18 ------- .../src/components/Table/Table.stories.tsx | 53 ++++++++++++++++++- .../components/src/components/Table/Table.tsx | 17 +----- packages/components/src/styles/_theme.scss | 45 ++++++++-------- 6 files changed, 98 insertions(+), 58 deletions(-) create mode 100644 packages/components/src/components/Table/Row.scss diff --git a/packages/components/src/components/Table/Row.scss b/packages/components/src/components/Table/Row.scss new file mode 100644 index 0000000000..6351b9c1f4 --- /dev/null +++ b/packages/components/src/components/Table/Row.scss @@ -0,0 +1,17 @@ +.react-aria-Row { + &[data-dragging] { + opacity: 0.6; + transform: translateZ(0); + } + + [slot='drag'] { + all: unset; + width: 1em; + text-align: center; + + &[data-focus-visible] { + border-radius: 4px; + outline: 2px solid var(--focus-ring-color); + } + } +} diff --git a/packages/components/src/components/Table/Row.tsx b/packages/components/src/components/Table/Row.tsx index 52ba639083..75695f3fad 100644 --- a/packages/components/src/components/Table/Row.tsx +++ b/packages/components/src/components/Table/Row.tsx @@ -6,7 +6,9 @@ import { useTableOptions, Button, } from 'react-aria-components'; +import './Row.scss'; import Checkbox from '../Checkbox/Checkbox'; +import Draggable from '../Icons/DraggableIcon'; export default function Row({ id, @@ -20,7 +22,9 @@ export default function Row({ {allowsDragging && ( - + )} {selectionBehavior === 'toggle' && ( diff --git a/packages/components/src/components/Table/Table.scss b/packages/components/src/components/Table/Table.scss index 3d2b68a768..4c55be0d64 100644 --- a/packages/components/src/components/Table/Table.scss +++ b/packages/components/src/components/Table/Table.scss @@ -220,24 +220,6 @@ } } -.react-aria-Row { - &[data-dragging] { - opacity: 0.6; - transform: translateZ(0); - } - - [slot='drag'] { - all: unset; - width: 15px; - text-align: center; - - &[data-focus-visible] { - border-radius: 4px; - outline: 2px solid var(--focus-ring-color); - } - } -} - .react-aria-DropIndicator[data-drop-target] { outline: 1px solid var(--highlight-background); transform: translateZ(0); diff --git a/packages/components/src/components/Table/Table.stories.tsx b/packages/components/src/components/Table/Table.stories.tsx index dd6f658ee7..2f00d4371a 100644 --- a/packages/components/src/components/Table/Table.stories.tsx +++ b/packages/components/src/components/Table/Table.stories.tsx @@ -1,3 +1,4 @@ +import { useDragAndDrop } from 'react-aria-components'; import Table from './Table'; import type { Meta, StoryObj } from '@storybook/react'; @@ -26,6 +27,10 @@ export const Default: Story = { }, }; +/** + * For more fine grained control over the selection mode, + * see https://react-spectrum.adobe.com/react-aria/Table.html#single-selection + */ export const SingleSelection: Story = { args: { ...Default.args, @@ -33,6 +38,10 @@ export const SingleSelection: Story = { }, }; +/** + * For more fine grained control over the selection mode, + * see https://react-spectrum.adobe.com/react-aria/Table.html#multiple-selection + */ export const MultipleSelection: Story = { args: { ...Default.args, @@ -40,7 +49,49 @@ export const MultipleSelection: Story = { }, }; -export const Resizable: Story = { +/** + * In order to make the rows draggable, you need to pass + * the `dragAndDropHooks` prop to the Table component. + * This prop has to be generated using the `useDragAndDrop` hook, + * passing the `getItems` and `onReorder` functions. + * + * See here for more information: + * https://react-spectrum.adobe.com/react-aria/Table.html#drag-and-drop + */ +export const DraggableRows: Story = { + decorators: [ + (Story) => { + const { dragAndDropHooks } = useDragAndDrop({ + getItems: (keys) => + [...keys].map((key) => ({ + 'text/plain': + Default.args.rows.find((row) => row.id === key)?.name || '', + })), + onReorder(e) { + if (e.target.dropPosition === 'before') { + console.log('moveBefore: key ', e.target.key, ', keys ', e.keys); + } else if (e.target.dropPosition === 'after') { + console.log('moveAfter: key ', e.target.key, ', keys ', e.keys); + } + }, + }); + + return ( + <> + + + ); + }, + ], + args: { + ...Default.args, + }, +}; + +/** + * Use the `resizableColumns` prop to make the columns resizable. + */ +export const ResizableColumns: Story = { args: { ...Default.args, resizableColumns: true, diff --git a/packages/components/src/components/Table/Table.tsx b/packages/components/src/components/Table/Table.tsx index c8a29db20f..00386231c4 100644 --- a/packages/components/src/components/Table/Table.tsx +++ b/packages/components/src/components/Table/Table.tsx @@ -5,7 +5,6 @@ import { Table as RACTable, TableBody, Cell, - useDragAndDrop, } from 'react-aria-components'; import './Table.scss'; import TableHeader from './TableHeader'; @@ -41,22 +40,8 @@ export default function Table({ resizableColumns, ...otherProps }: TableProps) { - const { dragAndDropHooks } = useDragAndDrop({ - getItems: (keys) => - [...keys].map((key) => ({ - 'text/plain': rows.find((row) => row.id === key)?.name || '', - })), - onReorder(e) { - if (e.target.dropPosition === 'before') { - console.log('moveBefore: key ', e.target.key, ', keys ', e.keys); - } else if (e.target.dropPosition === 'after') { - console.log('moveAfter: key ', e.target.key, ', keys ', e.keys); - } - }, - }); - const table = ( - + {(column) => ( diff --git a/packages/components/src/styles/_theme.scss b/packages/components/src/styles/_theme.scss index 49b62bc312..798e7c9a08 100644 --- a/packages/components/src/styles/_theme.scss +++ b/packages/components/src/styles/_theme.scss @@ -57,38 +57,39 @@ /* Semantic colors */ :root { - --focus-ring-color: var(--purple-400); - --text-color: var(--gray-600); - --text-color-base: var(--gray-500); - --text-color-hover: var(--gray-600); - --text-color-disabled: var(--gray-200); - --text-color-placeholder: var(--gray-400); - --link-color: var(--purple-500); - --link-color-secondary: var(--gray-500); - --link-color-pressed: var(--purple-600); - --border-color: var(--gray-300); - --border-color-hover: var(--gray-400); - --border-color-pressed: var(--gray-400); - --border-color-disabled: var(--gray-100); - --field-background: var(--gray-50); - --field-text-color: var(--gray-600); - --overlay-background: var(--gray-50); + --focus-ring-color: var(--cobalt); + --text-color: var(--denim); + --text-color-base: var(--pigeon); + --text-color-hover: var(--iron); + --text-color-disabled: var(--silver); + --text-color-placeholder: var(--dolphin); + --link-color: var(--sapphire); + --link-color-secondary: var(--pigeon); + --link-color-pressed: var(--royal); + --border-color: var(--silver); + --border-color-hover: var(--dolphin); + --border-color-pressed: var(--dolphin); + --border-color-disabled: var(--snow); + --field-background: var(--air); + --field-text-color: var(--iron); // denim? + --overlay-background: var(--air); // --button-background: var(--gray-50); --button-background: transparent; --button-background-pressed: var(--background-color); /* these colors are the same between light and dark themes * to ensure contrast with the foreground color */ // --highlight-background: #6f46ed; /* purple-300 from dark theme, 3.03:1 against background-color */ - --highlight-background: var(--smoke); + --highlight-background: var(--azure); // --highlight-background-pressed: #522acd; /* purple-200 from dark theme */ - --highlight-background-pressed: var(--silver); - --highlight-background-invalid: #cc2000; /* red-300 from dark theme */ + --highlight-background-pressed: var(--sky); + // --highlight-background-invalid: #cc2000; /* red-300 from dark theme */ + --highlight-background-invalid: var(--poppy); // --highlight-foreground: white; /* 5.56:1 against highlight-background */ - --highlight-foreground: var(--denim); + --highlight-foreground: var(--air); --highlight-foreground-pressed: #ddd; --highlight-overlay: rgb(from #6f46ed r g b / 15%); - --invalid-color: var(--red-400); - --invalid-color-pressed: var(--red-500); + --invalid-color: var(--rose); + --invalid-color-pressed: var(--candy); } /* Windows high contrast mode overrides */ From 650a3898a6d16558144f6b769af0b0748a84b8cc Mon Sep 17 00:00:00 2001 From: Piero Nicolli Date: Thu, 15 Feb 2024 11:23:56 +0100 Subject: [PATCH 4/8] more styling --- .../src/components/Table/Column.scss | 2 + .../components/src/components/Table/Row.scss | 41 ++++++++++++++ .../src/components/Table/Table.scss | 55 +++++-------------- 3 files changed, 56 insertions(+), 42 deletions(-) diff --git a/packages/components/src/components/Table/Column.scss b/packages/components/src/components/Table/Column.scss index ccbff0a53e..1060af21f4 100644 --- a/packages/components/src/components/Table/Column.scss +++ b/packages/components/src/components/Table/Column.scss @@ -1,4 +1,6 @@ .react-aria-Column { + font-weight: 500; + .sort-indicator { padding: 0 2px; } diff --git a/packages/components/src/components/Table/Row.scss b/packages/components/src/components/Table/Row.scss index 6351b9c1f4..7e11a382a0 100644 --- a/packages/components/src/components/Table/Row.scss +++ b/packages/components/src/components/Table/Row.scss @@ -1,4 +1,45 @@ .react-aria-Row { + --q-table-row-color: var(--text-color); + --q-table-row-font-size: 1rem; + --q-table-row-pressed: var(--highlight-pressed); + position: relative; + + color: var(--q-table-row-color); + cursor: default; + font-size: var(--q-table-row-font-size); + // --radius-top: 6px; + // --radius-bottom: 6px; + // --radius: var(--radius-top) var(--radius-top) var(--radius-bottom) + // var(--radius-bottom); + // border-radius: var(--radius); + // clip-path: inset(0 round var(--radius)); /* firefox */ + outline: none; + transform: scale(1); + + &[data-focus-visible] { + outline: 2px solid var(--focus-ring-color); + outline-offset: -2px; + } + + &[data-pressed] { + background: var(--q-table-row-pressed); + } + + &[data-selected] { + background: var(--highlight-background); + color: var(--highlight-foreground); + --focus-ring-color: var(--highlight-foreground); + + &[data-focus-visible], + .react-aria-Cell[data-focus-visible] { + outline-offset: -4px; + } + } + + &[data-disabled] { + color: var(--text-color-disabled); + } + &[data-dragging] { opacity: 0.6; transform: translateZ(0); diff --git a/packages/components/src/components/Table/Table.scss b/packages/components/src/components/Table/Table.scss index 4c55be0d64..c075281aef 100644 --- a/packages/components/src/components/Table/Table.scss +++ b/packages/components/src/components/Table/Table.scss @@ -1,12 +1,20 @@ .react-aria-Table { --q-table-border: 0 none; --q-table-padding: 0.286rem; + --q-table-width: initial; + --q-table-max-width: 100%; + --q-table-header-color: var(--sapphire); + --q-table-header-font-size: 1rem; --q-table-header-border-bottom: 1px solid var(--border-color); - --q-table-row-pressed: var(--highlight-pressed); + + --q-table-cell-padding: 18px 12px; + --q-table-cell-border-bottom: 1px solid var(--smoke); + // min-height: 100px; // align-self: start; - max-width: 100%; + width: var(--q-table-width); + max-width: var(--q-table-max-width); padding: var(--q-table-padding); border: var(--q-table-border); @@ -24,6 +32,7 @@ .react-aria-TableHeader { color: var(--q-table-header-color); + font-size: var(--q-table-header-font-size); // &:after { // content: ''; @@ -37,48 +46,9 @@ } } - .react-aria-Row { - // color: var(--text-color); - // font-size: 1.072rem; - position: relative; - // --radius-top: 6px; - // --radius-bottom: 6px; - // --radius: var(--radius-top) var(--radius-top) var(--radius-bottom) - // var(--radius-bottom); - // border-radius: var(--radius); - // clip-path: inset(0 round var(--radius)); /* firefox */ - // outline: none; - cursor: default; - transform: scale(1); - - &[data-focus-visible] { - outline: 2px solid var(--focus-ring-color); - outline-offset: -2px; - } - - &[data-pressed] { - background: var(--q-table-row-pressed); - } - - &[data-selected] { - background: var(--highlight-background); - color: var(--highlight-foreground); - --focus-ring-color: var(--highlight-foreground); - - &[data-focus-visible], - .react-aria-Cell[data-focus-visible] { - outline-offset: -4px; - } - } - - &[data-disabled] { - color: var(--text-color-disabled); - } - } - .react-aria-Cell, .react-aria-Column { - padding: 4px 8px; + padding: var(--q-table-cell-padding); outline: none; text-align: left; @@ -89,6 +59,7 @@ } .react-aria-Cell { + border-bottom: var(--q-table-cell-border-bottom); transform: translateZ(0); // &:first-child { From 6dada335d9cd24ee460d3cc843c82e0669c48856 Mon Sep 17 00:00:00 2001 From: Piero Nicolli Date: Thu, 15 Feb 2024 20:04:13 +0100 Subject: [PATCH 5/8] wip on contents view --- .../components/Contents/Contents.module.scss | 4 + .../components/Contents/Contents.stories.tsx | 102 ++++++++++++++++++ .../src/components/Contents/Contents.tsx | 49 ++++++++- .../src/components/Contents/ContentsCell.tsx | 61 +++++++++++ .../src/components/Contents/tableConfig.tsx | 16 --- .../src/components/Table/Column.scss | 6 +- .../components/src/components/Table/Row.scss | 5 +- .../src/components/Table/Table.scss | 4 +- .../src/components/Table/Table.stories.tsx | 2 +- .../components/src/components/Table/Table.tsx | 4 +- packages/components/src/helpers/indexes.ts | 37 +++++++ packages/components/src/helpers/types.d.ts | 4 + 12 files changed, 272 insertions(+), 22 deletions(-) create mode 100644 packages/components/src/components/Contents/ContentsCell.tsx delete mode 100644 packages/components/src/components/Contents/tableConfig.tsx create mode 100644 packages/components/src/helpers/indexes.ts create mode 100644 packages/components/src/helpers/types.d.ts diff --git a/packages/components/src/components/Contents/Contents.module.scss b/packages/components/src/components/Contents/Contents.module.scss index bb5ce74b85..ecf17a6196 100644 --- a/packages/components/src/components/Contents/Contents.module.scss +++ b/packages/components/src/components/Contents/Contents.module.scss @@ -9,3 +9,7 @@ margin-top: 0 !important; margin-inline-start: auto; } + +.contents-table { + --q-table-width: 100%; +} diff --git a/packages/components/src/components/Contents/Contents.stories.tsx b/packages/components/src/components/Contents/Contents.stories.tsx index f76d84f9fc..a7a7ab903b 100644 --- a/packages/components/src/components/Contents/Contents.stories.tsx +++ b/packages/components/src/components/Contents/Contents.stories.tsx @@ -23,5 +23,107 @@ export const Default: Story = { }, ], loading: false, + items: [ + { + '@id': 'https://demo.plone.org/images', + '@type': 'Document', + CreationDate: '2024-02-14T22:06:52+00:00', + Creator: 'plone-6-demo-site', + Date: '2024-02-14T22:06:52+00:00', + Description: 'Image bank.', + EffectiveDate: 'None', + ExpirationDate: 'None', + ModificationDate: '2024-02-14T22:06:52+00:00', + Subject: [], + Title: 'Images', + Type: 'Page', + UID: '7b00238f184342a7a85ef4463380ac37', + author_name: null, + cmf_uid: null, + commentators: [], + created: '2024-02-14T22:06:52+00:00', + description: 'Image bank.', + effective: '1969-12-31T00:00:00+00:00', + end: null, + exclude_from_nav: true, + expires: '2499-12-31T00:00:00+00:00', + getIcon: null, + getId: 'images', + getObjSize: '0 KB', + getPath: '/Plone/images', + getRemoteUrl: null, + getURL: 'https://demo.plone.org/images', + hasPreviewImage: null, + head_title: null, + id: 'images', + image_field: '', + image_scales: null, + in_response_to: null, + is_folderish: true, + last_comment_date: null, + listCreators: ['plone-6-demo-site'], + location: null, + mime_type: 'text/plain', + modified: '2024-02-14T22:06:52+00:00', + nav_title: null, + portal_type: 'Document', + review_state: 'published', + start: null, + sync_uid: null, + title: 'Images', + total_comments: 0, + type_title: 'Page', + }, + { + '@id': 'https://demo.plone.org/my-page', + '@type': 'Document', + CreationDate: '2024-02-15T11:22:02+00:00', + Creator: 'admin', + Date: '2024-02-15T11:22:02+00:00', + Description: 'This is my page', + EffectiveDate: 'None', + ExpirationDate: 'None', + ModificationDate: '2024-02-15T11:22:02+00:00', + Subject: ['its-mine'], + Title: 'My page', + Type: 'Page', + UID: '51cb4490e3a346349093f6c423c8f28a', + author_name: null, + cmf_uid: null, + commentators: [], + created: '2024-02-15T11:22:02+00:00', + description: 'This is my page', + effective: '1969-12-31T00:00:00+00:00', + end: null, + exclude_from_nav: false, + expires: '2499-12-31T00:00:00+00:00', + getIcon: null, + getId: 'my-page', + getObjSize: '0 KB', + getPath: '/Plone/my-page', + getRemoteUrl: null, + getURL: 'https://demo.plone.org/my-page', + hasPreviewImage: null, + head_title: null, + id: 'my-page', + image_field: '', + image_scales: null, + in_response_to: null, + is_folderish: true, + last_comment_date: null, + listCreators: ['admin'], + location: null, + mime_type: 'text/plain', + modified: '2024-02-15T11:22:02+00:00', + nav_title: null, + portal_type: 'Document', + review_state: 'private', + start: null, + sync_uid: null, + title: 'My page', + total_comments: 0, + type_title: 'Page', + }, + ], }, }; diff --git a/packages/components/src/components/Contents/Contents.tsx b/packages/components/src/components/Contents/Contents.tsx index cbfe4d79b8..40f4c83ece 100644 --- a/packages/components/src/components/Contents/Contents.tsx +++ b/packages/components/src/components/Contents/Contents.tsx @@ -1,7 +1,7 @@ 'use client'; import type { ActionsResponse } from '@plone/types'; -import { useState } from 'react'; +import { ComponentProps, ReactNode, useState } from 'react'; import { Button, // OverlayArrow, @@ -9,17 +9,23 @@ import { TooltipTrigger, } from 'react-aria-components'; import cx from 'classnames'; +import type { Brain } from '@plone/types/src/content/brains'; import styles from './Contents.module.scss'; import Add from '../Icons/AddIcon'; import Breadcrumbs from '../Breadcrumbs/Breadcrumbs'; import Container from '../Container/Container'; import Input from '../Input/Input'; +import Table from '../Table/Table'; +import ContentsCell from './ContentsCell'; +import { indexes, defaultIndexes } from '../../helpers/indexes'; +import type { ArrayElement } from '../../helpers/types'; interface ContentsProps { pathname: string; objectActions: ActionsResponse['object']; loading: boolean; title: string; + items: Brain[]; } /** @@ -33,6 +39,7 @@ export default function Contents({ objectActions, loading, title, + items, }: ContentsProps) { const [selected, setSelected] = useState([]); // const path = getBaseUrl(pathname); @@ -49,6 +56,34 @@ export default function Contents({ return null; } + const columns = [ + { + id: 'title', + name: 'Title', + isRowHeader: true, + }, + ...defaultIndexes.map((index) => ({ + id: index, + name: indexes[index].label, + })), + { + id: '_actions', + name: 'Actions', + }, + ] as const; + + const rows = items.map((item) => + columns.reduce['rows']>>( + (cells, column) => ({ + ...cells, + [column.id]: ( + + ), + }), + { id: item['@id'] }, + ), + ); + return ( Add content +
+ + ); diff --git a/packages/components/src/components/Contents/ContentsCell.tsx b/packages/components/src/components/Contents/ContentsCell.tsx new file mode 100644 index 0000000000..c413b35c72 --- /dev/null +++ b/packages/components/src/components/Contents/ContentsCell.tsx @@ -0,0 +1,61 @@ +import { Brain } from '@plone/types/src/content/brains'; +import Link from '../Link/Link'; +import Page from '../Icons/PageIcon'; +import { indexes } from '../../helpers/indexes'; + +interface Props { + item: Brain; + column: keyof typeof indexes | 'title' | '_actions'; +} + +export default function ContentsCell({ item, column }: Props) { + if (column === 'title') { + return ( + + + {item.title} + {item.ExpirationDate !== 'None' && + new Date(item.ExpirationDate).getTime() < new Date().getTime() && ( + Expired + )} + {item.EffectiveDate !== 'None' && + new Date(item.EffectiveDate).getTime() > new Date().getTime() && ( + Scheduled + )} + + ); + } else if (column === '_actions') { + // TODO + return null; + } else { + if (indexes[column].type === 'boolean') { + return item[column] ? 'Yes' : 'No'; + } else if (indexes[column].type === 'string') { + if (column !== 'review_state') { + return item[column]; + } else { + return ( +
+ + {/* */} + + {item[column] || 'No workflow state'} +
+ ); + } + } else if (indexes[column].type === 'date') { + if (item[column] && item[column] !== 'None') { + // @ts-ignore TODO fix this, maybe a more strict type for the indexes? + return new Date(item[column]).toLocaleDateString(); + } else { + return 'None'; + } + } else if (indexes[column].type === 'array') { + const value = item[column]; + return Array.isArray(value) ? value.join(', ') : value; + } + } +} diff --git a/packages/components/src/components/Contents/tableConfig.tsx b/packages/components/src/components/Contents/tableConfig.tsx deleted file mode 100644 index 7430d206b7..0000000000 --- a/packages/components/src/components/Contents/tableConfig.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { - createColumnHelper, - type Row, - flexRender, -} from '@tanstack/react-table'; -import type { Brain } from '@plone/types/src/content/brains'; - -const columnHelper = createColumnHelper(); - -export const defaultColumns = [ - columnHelper.display({ - id: 'controls', - header: ({ table }) => C, - cell: ({ row }) =>
, - }), -]; diff --git a/packages/components/src/components/Table/Column.scss b/packages/components/src/components/Table/Column.scss index 1060af21f4..3c81086476 100644 --- a/packages/components/src/components/Table/Column.scss +++ b/packages/components/src/components/Table/Column.scss @@ -1,5 +1,9 @@ +:root { + --q-table-column-font-weight: 500; +} + .react-aria-Column { - font-weight: 500; + font-weight: var(--q-table-column-font-weight); .sort-indicator { padding: 0 2px; diff --git a/packages/components/src/components/Table/Row.scss b/packages/components/src/components/Table/Row.scss index 7e11a382a0..3f0906c181 100644 --- a/packages/components/src/components/Table/Row.scss +++ b/packages/components/src/components/Table/Row.scss @@ -1,7 +1,10 @@ -.react-aria-Row { +:root { --q-table-row-color: var(--text-color); --q-table-row-font-size: 1rem; --q-table-row-pressed: var(--highlight-pressed); +} + +.react-aria-Row { position: relative; color: var(--q-table-row-color); diff --git a/packages/components/src/components/Table/Table.scss b/packages/components/src/components/Table/Table.scss index c075281aef..26321b00ed 100644 --- a/packages/components/src/components/Table/Table.scss +++ b/packages/components/src/components/Table/Table.scss @@ -1,4 +1,4 @@ -.react-aria-Table { +:root { --q-table-border: 0 none; --q-table-padding: 0.286rem; --q-table-width: initial; @@ -10,7 +10,9 @@ --q-table-cell-padding: 18px 12px; --q-table-cell-border-bottom: 1px solid var(--smoke); +} +.react-aria-Table { // min-height: 100px; // align-self: start; width: var(--q-table-width); diff --git a/packages/components/src/components/Table/Table.stories.tsx b/packages/components/src/components/Table/Table.stories.tsx index 2f00d4371a..1c78f57182 100644 --- a/packages/components/src/components/Table/Table.stories.tsx +++ b/packages/components/src/components/Table/Table.stories.tsx @@ -65,7 +65,7 @@ export const DraggableRows: Story = { getItems: (keys) => [...keys].map((key) => ({ 'text/plain': - Default.args.rows.find((row) => row.id === key)?.name || '', + Default.args.rows.find((row) => row.id === key)?.id || '', })), onReorder(e) { if (e.target.dropPosition === 'before') { diff --git a/packages/components/src/components/Table/Table.tsx b/packages/components/src/components/Table/Table.tsx index 00386231c4..38dfeddb59 100644 --- a/packages/components/src/components/Table/Table.tsx +++ b/packages/components/src/components/Table/Table.tsx @@ -1,3 +1,4 @@ +import { type ReactNode } from 'react'; import { type TableProps as RACTableProps, ResizableTableContainer, @@ -20,13 +21,14 @@ interface ColumnType { interface RowType { id: string; - [key: string]: string; // TODO can we make this more specific? + [key: string]: ReactNode; // TODO can we make this more specific? } interface TableProps extends RACTableProps { columns: C[]; rows: R[]; resizableColumns?: boolean; + // TODO maybe a custom "selectall" component? Is it doable with react-aria-components? } /** diff --git a/packages/components/src/helpers/indexes.ts b/packages/components/src/helpers/indexes.ts new file mode 100644 index 0000000000..6d89c20876 --- /dev/null +++ b/packages/components/src/helpers/indexes.ts @@ -0,0 +1,37 @@ +export const indexes = { + // sortable_title: { label: 'Title', type: 'string', sort_on: 'sortable_title' }, + review_state: { label: 'Review state', type: 'string' }, + ModificationDate: { + label: 'Last modified', + type: 'date', + sort_on: 'modified', + }, + EffectiveDate: { + label: 'Publication date', + type: 'date', + sort_on: 'effective', + }, + id: { label: 'ID', type: 'string', sort_on: 'id' }, + ExpirationDate: { label: 'Expiration date', type: 'date' }, + CreationDate: { label: 'Created on', type: 'date', sort_on: 'created' }, + Subject: { label: 'Tags', type: 'array' }, + portal_type: { label: 'Type', type: 'string', sort_on: 'portal_type' }, + is_folderish: { label: 'Folder', type: 'boolean' }, + exclude_from_nav: { label: 'Excluded from navigation', type: 'boolean' }, + getObjSize: { label: 'Object Size', type: 'string' }, + last_comment_date: { label: 'Last comment date', type: 'date' }, + total_comments: { label: 'Total comments', type: 'number' }, + end: { label: 'End Date', type: 'date' }, + Description: { label: 'Description', type: 'string' }, + Creator: { label: 'Creator', type: 'string' }, + location: { label: 'Location', type: 'string' }, + UID: { label: 'UID', type: 'string' }, + start: { label: 'Start Date', type: 'date' }, + Type: { label: 'Type', type: 'string' }, +}; + +export const defaultIndexes: (keyof typeof indexes)[] = [ + 'review_state', + 'ModificationDate', + 'EffectiveDate', +]; diff --git a/packages/components/src/helpers/types.d.ts b/packages/components/src/helpers/types.d.ts new file mode 100644 index 0000000000..220a792ceb --- /dev/null +++ b/packages/components/src/helpers/types.d.ts @@ -0,0 +1,4 @@ +/** + * Get the type of the elements in an array + */ +export type ArrayElement = A extends readonly (infer T)[] ? T : never; From cef5731f1207e3df5116e9f373a53bdb85322ddf Mon Sep 17 00:00:00 2001 From: Piero Nicolli Date: Mon, 19 Feb 2024 10:51:18 +0100 Subject: [PATCH 6/8] wip on contents view --- .../components/Contents/AddContentPopover.tsx | 21 +++++++++ .../components/Contents/Contents.module.scss | 25 +++++++++++ .../components/Contents/Contents.stories.tsx | 9 ++++ .../src/components/Contents/Contents.tsx | 44 ++++++++++++++++--- .../Contents/ContentsCell.module.css | 4 ++ .../src/components/Contents/ContentsCell.tsx | 4 +- .../src/components/Table/Table.stories.tsx | 3 +- 7 files changed, 101 insertions(+), 9 deletions(-) create mode 100644 packages/components/src/components/Contents/AddContentPopover.tsx create mode 100644 packages/components/src/components/Contents/ContentsCell.module.css diff --git a/packages/components/src/components/Contents/AddContentPopover.tsx b/packages/components/src/components/Contents/AddContentPopover.tsx new file mode 100644 index 0000000000..d5d2b08aa4 --- /dev/null +++ b/packages/components/src/components/Contents/AddContentPopover.tsx @@ -0,0 +1,21 @@ +import { Dialog, Heading, Popover, Switch } from 'react-aria-components'; + +interface Props { + addableTypes: { + '@id': string; + id: string; + title: string; + }[]; +} + +export const AddContentPopover = ({ addableTypes }: Props) => { + const page = addableTypes.find((type) => type.id === 'Document'); + + return ( + + +
Link ai CT
+
+
+ ); +}; diff --git a/packages/components/src/components/Contents/Contents.module.scss b/packages/components/src/components/Contents/Contents.module.scss index ecf17a6196..815aee06c2 100644 --- a/packages/components/src/components/Contents/Contents.module.scss +++ b/packages/components/src/components/Contents/Contents.module.scss @@ -7,9 +7,34 @@ .search-input { // TODO fixme margin-top: 0 !important; + margin-inline-end: 1.875rem; margin-inline-start: auto; } .contents-table { --q-table-width: 100%; } + +.add { + width: 2.25rem; + height: 2.25rem; + padding: 0.375rem; + border: 0 none; + border-radius: 50%; + background: var(--sapphire); + color: var(--air); + cursor: pointer; + + &:hover { + background: var(--royal); + } +} + +.tooltip { + padding: 0.1875rem 0.375rem; + border-radius: 3px; + margin-top: 0.25rem; + background-color: var(--denim); + color: var(--air); + line-height: 1.5; +} diff --git a/packages/components/src/components/Contents/Contents.stories.tsx b/packages/components/src/components/Contents/Contents.stories.tsx index a7a7ab903b..88562bc2d7 100644 --- a/packages/components/src/components/Contents/Contents.stories.tsx +++ b/packages/components/src/components/Contents/Contents.stories.tsx @@ -23,6 +23,15 @@ export const Default: Story = { }, ], loading: false, + orderContent: async (baseUrl, id, delta) => { + console.log(`now PATCH https://api${baseUrl} with payload: +{ + "ordering": { + "obj_id": "${id}", + "delta": ${delta}, + }, +}`); + }, items: [ { '@id': 'https://demo.plone.org/images', diff --git a/packages/components/src/components/Contents/Contents.tsx b/packages/components/src/components/Contents/Contents.tsx index 40f4c83ece..b244a293e4 100644 --- a/packages/components/src/components/Contents/Contents.tsx +++ b/packages/components/src/components/Contents/Contents.tsx @@ -7,6 +7,7 @@ import { // OverlayArrow, Tooltip, TooltipTrigger, + useDragAndDrop, } from 'react-aria-components'; import cx from 'classnames'; import type { Brain } from '@plone/types/src/content/brains'; @@ -17,6 +18,7 @@ import Container from '../Container/Container'; import Input from '../Input/Input'; import Table from '../Table/Table'; import ContentsCell from './ContentsCell'; +import { AddContentPopover } from './AddContentPopover'; import { indexes, defaultIndexes } from '../../helpers/indexes'; import type { ArrayElement } from '../../helpers/types'; @@ -26,6 +28,8 @@ interface ContentsProps { loading: boolean; title: string; items: Brain[]; + orderContent: (baseUrl: string, id: string, delta: number) => Promise; + addableTypes: ComponentProps['addableTypes']; } /** @@ -40,6 +44,8 @@ export default function Contents({ loading, title, items, + orderContent, + addableTypes, }: ContentsProps) { const [selected, setSelected] = useState([]); // const path = getBaseUrl(pathname); @@ -84,6 +90,32 @@ export default function Contents({ ), ); + const { dragAndDropHooks } = useDragAndDrop({ + getItems: (keys) => + [...keys].map((key) => ({ + 'text/plain': key.toString(), + })), + onReorder(e) { + if (e.keys.size !== 1) { + console.error('Only one item can be moved at a time'); + } + const item = items.find((item) => item['@id'] === [...e.keys][0]); + if (!item) return; + + const initialPosition = rows.findIndex((row) => row.id === item['@id']); + if (initialPosition === -1) return; + + let finalPosition = rows.findIndex((row) => row.id === e.target.key); + if (e.target.dropPosition === 'after') finalPosition += 1; + + orderContent( + path, + item.id.replace(/^.*\//, ''), + finalPosition - initialPosition, + ); + }, + }); + return ( - - Add content + + Add content +
diff --git a/packages/components/src/components/Contents/ContentsCell.module.css b/packages/components/src/components/Contents/ContentsCell.module.css new file mode 100644 index 0000000000..5a60642d35 --- /dev/null +++ b/packages/components/src/components/Contents/ContentsCell.module.css @@ -0,0 +1,4 @@ +.title-link { + display: flex; + align-items: center; +} diff --git a/packages/components/src/components/Contents/ContentsCell.tsx b/packages/components/src/components/Contents/ContentsCell.tsx index c413b35c72..4ec7287925 100644 --- a/packages/components/src/components/Contents/ContentsCell.tsx +++ b/packages/components/src/components/Contents/ContentsCell.tsx @@ -1,4 +1,6 @@ +import cx from 'classnames'; import { Brain } from '@plone/types/src/content/brains'; +import styles from './ContentsCell.module.css'; import Link from '../Link/Link'; import Page from '../Icons/PageIcon'; import { indexes } from '../../helpers/indexes'; @@ -12,7 +14,7 @@ export default function ContentsCell({ item, column }: Props) { if (column === 'title') { return ( diff --git a/packages/components/src/components/Table/Table.stories.tsx b/packages/components/src/components/Table/Table.stories.tsx index 1c78f57182..abbf3ba28c 100644 --- a/packages/components/src/components/Table/Table.stories.tsx +++ b/packages/components/src/components/Table/Table.stories.tsx @@ -64,8 +64,7 @@ export const DraggableRows: Story = { const { dragAndDropHooks } = useDragAndDrop({ getItems: (keys) => [...keys].map((key) => ({ - 'text/plain': - Default.args.rows.find((row) => row.id === key)?.id || '', + 'text/plain': key.toString(), })), onReorder(e) { if (e.target.dropPosition === 'before') { From 9dca4d4c61a6d59e6aa60baa318b94438421ce7b Mon Sep 17 00:00:00 2001 From: Piero Nicolli Date: Mon, 19 Feb 2024 17:22:48 +0100 Subject: [PATCH 7/8] wip on contents view --- .../src/components/Popover/Popover.tsx | 17 ++-- .../src/components/Tooltip/Tooltip.tsx | 15 ++-- .../quanta/Popover/Popover.stories.tsx | 33 ++++++++ .../src/components/quanta/Popover/Popover.tsx | 11 +++ .../components/src/styles/quanta/Contents.css | 49 +++++++++++ .../components/src/styles/quanta/Popover.css | 3 + .../components/src/styles/quanta/main.css | 1 + .../src/views/Contents/AddContentPopover.tsx | 21 +++-- .../src/views/Contents/Contents.stories.tsx | 28 ++++++- .../src/views/Contents/ContentsCell.tsx | 23 +++++- .../src/views/Contents/ItemActionsPopover.tsx | 82 +++++++++++++++++++ 11 files changed, 260 insertions(+), 23 deletions(-) create mode 100644 packages/components/src/components/quanta/Popover/Popover.stories.tsx create mode 100644 packages/components/src/components/quanta/Popover/Popover.tsx create mode 100644 packages/components/src/styles/quanta/Popover.css create mode 100644 packages/components/src/views/Contents/ItemActionsPopover.tsx diff --git a/packages/components/src/components/Popover/Popover.tsx b/packages/components/src/components/Popover/Popover.tsx index d115987565..8dd8c521e9 100644 --- a/packages/components/src/components/Popover/Popover.tsx +++ b/packages/components/src/components/Popover/Popover.tsx @@ -1,23 +1,26 @@ import React from 'react'; import { - Dialog, OverlayArrow, Popover as RACPopover, PopoverProps as RACPopoverProps, } from 'react-aria-components'; +import { Dialog } from '../Dialog/Dialog'; export interface PopoverProps extends Omit { children: React.ReactNode; + arrow?: boolean; } -export function Popover({ children, ...props }: PopoverProps) { +export function Popover({ children, arrow, ...props }: PopoverProps) { return ( - - - - - + {arrow && ( + + + + + + )} {children} ); diff --git a/packages/components/src/components/Tooltip/Tooltip.tsx b/packages/components/src/components/Tooltip/Tooltip.tsx index 17213e6ce3..5dc99c9d48 100644 --- a/packages/components/src/components/Tooltip/Tooltip.tsx +++ b/packages/components/src/components/Tooltip/Tooltip.tsx @@ -7,16 +7,19 @@ import { export interface TooltipProps extends Omit { children: React.ReactNode; + arrow?: boolean; } -export function Tooltip({ children, ...props }: TooltipProps) { +export function Tooltip({ children, arrow, ...props }: TooltipProps) { return ( - - - - - + {arrow && ( + + + + + + )} {children} ); diff --git a/packages/components/src/components/quanta/Popover/Popover.stories.tsx b/packages/components/src/components/quanta/Popover/Popover.stories.tsx new file mode 100644 index 0000000000..656bdbc367 --- /dev/null +++ b/packages/components/src/components/quanta/Popover/Popover.stories.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { DialogTrigger } from 'react-aria-components'; +import { Button } from '../../Button/Button'; +import { QuantaPopover } from './Popover'; + +import type { Meta, StoryObj } from '@storybook/react'; + +import '../../../styles/basic/Popover.css'; +import '../../../styles/quanta/Popover.css'; + +const meta: Meta = { + title: 'Quanta/Popover', + component: QuantaPopover, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + render: (args: any) => ( + + + + + ), + args: { + children: 'Popover content', + }, +}; diff --git a/packages/components/src/components/quanta/Popover/Popover.tsx b/packages/components/src/components/quanta/Popover/Popover.tsx new file mode 100644 index 0000000000..55dfc9c5a7 --- /dev/null +++ b/packages/components/src/components/quanta/Popover/Popover.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import { PopoverContext } from 'react-aria-components'; +import { Popover, PopoverProps } from '../../Popover/Popover'; + +export function QuantaPopover(props: PopoverProps) { + return ( + + + + ); +} diff --git a/packages/components/src/styles/quanta/Contents.css b/packages/components/src/styles/quanta/Contents.css index 5d42fbb1b4..5f2bbc62cf 100644 --- a/packages/components/src/styles/quanta/Contents.css +++ b/packages/components/src/styles/quanta/Contents.css @@ -43,3 +43,52 @@ align-items: center; } } + +.add-content-list { + padding: 0; + margin: 0; + list-style: none; +} + +.add-content-list-item { + padding: 12px; + + & + .add-content-list-item { + border-top: 1px solid var(--quanta-smoke); + } + + a { + display: flex; + align-items: center; + color: var(--quanta-sapphire); + text-decoration: none; + + &:hover { + color: var(--quanta-royal); + text-decoration: underline; + } + } + + .icon { + margin-inline-start: auto; + } +} + +.item-actions-list { + padding: 0; + margin: 0; + list-style: none; +} + +.item-actions-list-item { + padding: 12px 0; + + .view, + .move-to-bottom { + border-bottom: 1px solid var(--quanta-smoke); + } + + .icon { + margin-inline-end: 1rem; + } +} diff --git a/packages/components/src/styles/quanta/Popover.css b/packages/components/src/styles/quanta/Popover.css new file mode 100644 index 0000000000..caf9cee464 --- /dev/null +++ b/packages/components/src/styles/quanta/Popover.css @@ -0,0 +1,3 @@ +.q.react-aria-Popover { + --border-color: transparent; +} diff --git a/packages/components/src/styles/quanta/main.css b/packages/components/src/styles/quanta/main.css index aa1df0cb84..6daed6b7d5 100644 --- a/packages/components/src/styles/quanta/main.css +++ b/packages/components/src/styles/quanta/main.css @@ -4,6 +4,7 @@ @import './TextField.css'; @import './Select.css'; @import './Table.css'; +@import './Popover.css'; /* Views */ @import './Contents.css'; diff --git a/packages/components/src/views/Contents/AddContentPopover.tsx b/packages/components/src/views/Contents/AddContentPopover.tsx index bc8efe3613..737e6313e6 100644 --- a/packages/components/src/views/Contents/AddContentPopover.tsx +++ b/packages/components/src/views/Contents/AddContentPopover.tsx @@ -1,5 +1,7 @@ import React from 'react'; -import { Dialog, Heading, Popover, Switch } from 'react-aria-components'; +import { Link } from '../../components/Link/Link'; +import { ChevronrightIcon } from '../../components/Icons'; +import { Popover } from '../../components/Popover/Popover'; interface Props { addableTypes: { @@ -10,13 +12,20 @@ interface Props { } export const AddContentPopover = ({ addableTypes }: Props) => { - const page = addableTypes.find((type) => type.id === 'Document'); + // const page = addableTypes.find((type) => type.id === 'Document'); return ( - - -
Link ai CT
-
+ +
    + {addableTypes.map((type) => ( +
  • + + {type.title} + + +
  • + ))} +
); }; diff --git a/packages/components/src/views/Contents/Contents.stories.tsx b/packages/components/src/views/Contents/Contents.stories.tsx index 7fd6d22909..7ff14b10c9 100644 --- a/packages/components/src/views/Contents/Contents.stories.tsx +++ b/packages/components/src/views/Contents/Contents.stories.tsx @@ -35,7 +35,33 @@ export const Default: Story = { }, }`); }, - addableTypes: [], + addableTypes: [ + { + '@id': 'https://demo.plone.org/@types/Document', + id: 'Document', + title: 'Page', + }, + { + '@id': 'https://demo.plone.org/@types/Event', + id: 'Event', + title: 'Event', + }, + { + '@id': 'https://demo.plone.org/@types/Image', + id: 'Image', + title: 'Image', + }, + { + '@id': 'https://demo.plone.org/@types/Link', + id: 'Link', + title: 'Link', + }, + { + '@id': 'https://demo.plone.org/@types/News Item', + id: 'News Item', + title: 'News Item', + }, + ], items: [ { '@id': 'https://demo.plone.org/images', diff --git a/packages/components/src/views/Contents/ContentsCell.tsx b/packages/components/src/views/Contents/ContentsCell.tsx index 1d10322a33..98faf3b514 100644 --- a/packages/components/src/views/Contents/ContentsCell.tsx +++ b/packages/components/src/views/Contents/ContentsCell.tsx @@ -1,8 +1,11 @@ import React from 'react'; +import { DialogTrigger } from 'react-aria-components'; import { Brain } from '@plone/types/src/content/brains'; +import { Button } from '../../components/Button/Button'; import { Link } from '../../components/Link/Link'; -import { PageIcon } from '../../components/Icons'; +import { MoreoptionsIcon, PageIcon } from '../../components/Icons'; import { indexes } from '../../helpers/indexes'; +import { ItemActionsPopover } from './ItemActionsPopover'; interface Props { item: Brain; @@ -29,8 +32,22 @@ export function ContentsCell({ item, column }: Props) { ); } else if (column === '_actions') { - // TODO - return null; + return ( + + + {}} + onMoveToTop={async () => {}} + onCopy={async () => {}} + onCut={async () => {}} + onDelete={async () => {}} + /> + + ); } else { if (indexes[column].type === 'boolean') { return item[column] ? 'Yes' : 'No'; diff --git a/packages/components/src/views/Contents/ItemActionsPopover.tsx b/packages/components/src/views/Contents/ItemActionsPopover.tsx new file mode 100644 index 0000000000..243ad7ac57 --- /dev/null +++ b/packages/components/src/views/Contents/ItemActionsPopover.tsx @@ -0,0 +1,82 @@ +import React from 'react'; +import { Link } from '../../components/Link/Link'; +import { Button } from '../../components/Button/Button'; +import { Popover } from '../../components/Popover/Popover'; +import { + EditIcon, + EyeIcon, + RowbeforeIcon, + RowafterIcon, + CutIcon, + CopyIcon, + BinIcon, +} from '../../components/Icons'; + +interface Props { + editLink: string; + viewLink: string; + onMoveToTop: () => Promise; + onMoveToBottom: () => Promise; + onCut: () => Promise; + onCopy: () => Promise; + onDelete: () => Promise; +} + +export function ItemActionsPopover({ + editLink, + viewLink, + onMoveToTop, + onMoveToBottom, + onCut, + onCopy, + onDelete, +}: Props) { + return ( + +
    +
  • + + + Edit + +
  • +
  • + + + View + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+
+ ); +} From 567b91ed1df2e154938562216b41645bd478222a Mon Sep 17 00:00:00 2001 From: Piero Nicolli Date: Tue, 2 Apr 2024 12:08:52 +0200 Subject: [PATCH 8/8] minor improvements, still wip --- .../src/components/Button/Button.tsx | 11 +++++--- .../src/components/Popover/Popover.tsx | 19 +++++++++++-- .../src/views/Contents/Contents.tsx | 28 +++++++++++++++---- .../src/views/Contents/ContentsCell.tsx | 2 +- 4 files changed, 48 insertions(+), 12 deletions(-) diff --git a/packages/components/src/components/Button/Button.tsx b/packages/components/src/components/Button/Button.tsx index b5f2ae1fa8..9e8c76b371 100644 --- a/packages/components/src/components/Button/Button.tsx +++ b/packages/components/src/components/Button/Button.tsx @@ -1,6 +1,9 @@ -import React from 'react'; +import React, { forwardRef, ForwardedRef } from 'react'; import { Button as RACButton, ButtonProps } from 'react-aria-components'; -export function Button(props: ButtonProps) { - return ; -} +export const Button = forwardRef(function _Button( + props: ButtonProps, + ref: ForwardedRef, +) { + return ; +}); diff --git a/packages/components/src/components/Popover/Popover.tsx b/packages/components/src/components/Popover/Popover.tsx index 8dd8c521e9..7718d751c7 100644 --- a/packages/components/src/components/Popover/Popover.tsx +++ b/packages/components/src/components/Popover/Popover.tsx @@ -8,10 +8,20 @@ import { Dialog } from '../Dialog/Dialog'; export interface PopoverProps extends Omit { children: React.ReactNode; + /** Mandatory when children don't contain a or dialogAriaLabelledBy */ + dialogAriaLabel?: string; + /** Mandatory when children don't contain a or dialogAriaLabel */ + dialogAriaLabelledby?: string; arrow?: boolean; } -export function Popover({ children, arrow, ...props }: PopoverProps) { +export function Popover({ + children, + dialogAriaLabel, + dialogAriaLabelledby, + arrow, + ...props +}: PopoverProps) { return ( {arrow && ( @@ -21,7 +31,12 @@ export function Popover({ children, arrow, ...props }: PopoverProps) { )} - {children} + + {children} + ); } diff --git a/packages/components/src/views/Contents/Contents.tsx b/packages/components/src/views/Contents/Contents.tsx index 2905061cf9..28d239d963 100644 --- a/packages/components/src/views/Contents/Contents.tsx +++ b/packages/components/src/views/Contents/Contents.tsx @@ -24,6 +24,7 @@ import type { ArrayElement } from '../../helpers/types'; interface ContentsProps { pathname: string; + breadcrumbs: ComponentProps['items']; objectActions: ActionsResponse['object']; loading: boolean; title: string; @@ -40,6 +41,7 @@ interface ContentsProps { */ export default function Contents({ pathname, + breadcrumbs = [], objectActions, loading, title, @@ -97,16 +99,28 @@ export default function Contents({ })), onReorder(e) { if (e.keys.size !== 1) { + // TODO mostrare toast o rendere non ordinabile quando più di un elemento è selezionato console.error('Only one item can be moved at a time'); + return; } - const item = items.find((item) => item['@id'] === [...e.keys][0]); + const target = [...e.keys][0]; + if (target === e.target.key) return; + + const item = items.find((item) => item['@id'] === target); if (!item) return; const initialPosition = rows.findIndex((row) => row.id === item['@id']); if (initialPosition === -1) return; - let finalPosition = rows.findIndex((row) => row.id === e.target.key); - if (e.target.dropPosition === 'after') finalPosition += 1; + const finalPosition = rows.findIndex((row) => row.id === e.target.key); + + let delta = finalPosition - initialPosition; + if (delta > 0 && e.target.dropPosition === 'before') delta -= 1; + if (delta < 0 && e.target.dropPosition === 'after') delta += 1; + + // if (delta !== 0) { + // orderItem(item.id, delta); + // } orderContent( path, @@ -129,8 +143,12 @@ export default function Contents({
- -

{title}

+ +

{[...breadcrumbs].slice(-1)[0]?.title}