feat: standardize class names (#873)
This commit is contained in:
parent
7e1734aab6
commit
1338ad2f57
@ -66,11 +66,10 @@
|
|||||||
"@emotion/css": "11.10.6",
|
"@emotion/css": "11.10.6",
|
||||||
"@emotion/react": "11.10.6",
|
"@emotion/react": "11.10.6",
|
||||||
"@emotion/styled": "11.10.6",
|
"@emotion/styled": "11.10.6",
|
||||||
"@headlessui/react": "1.7.7",
|
|
||||||
"@lezer/common": "1.0.2",
|
"@lezer/common": "1.0.2",
|
||||||
"@mdx-js/mdx": "2.3.0",
|
"@mdx-js/mdx": "2.3.0",
|
||||||
"@mdx-js/react": "2.3.0",
|
"@mdx-js/react": "2.3.0",
|
||||||
"@mui/base": "5.0.0-alpha.124",
|
"@mui/base": "5.0.0-beta.14",
|
||||||
"@mui/material": "5.11.16",
|
"@mui/material": "5.11.16",
|
||||||
"@mui/system": "5.11.16",
|
"@mui/system": "5.11.16",
|
||||||
"@mui/x-date-pickers": "5.0.20",
|
"@mui/x-date-pickers": "5.0.20",
|
||||||
@ -84,9 +83,10 @@
|
|||||||
"@styled-icons/material-rounded": "10.47.0",
|
"@styled-icons/material-rounded": "10.47.0",
|
||||||
"@styled-icons/remix-editor": "10.46.0",
|
"@styled-icons/remix-editor": "10.46.0",
|
||||||
"@styled-icons/simple-icons": "10.46.0",
|
"@styled-icons/simple-icons": "10.46.0",
|
||||||
"@udecode/plate": "21.3.2",
|
"@udecode/plate": "23.7.4",
|
||||||
"@udecode/plate-juice": "21.3.2",
|
"@udecode/plate-cursor": "23.7.4",
|
||||||
"@udecode/plate-serializer-md": "21.3.2",
|
"@udecode/plate-juice": "23.7.4",
|
||||||
|
"@udecode/plate-serializer-md": "23.7.4",
|
||||||
"@uiw/codemirror-extensions-langs": "4.19.16",
|
"@uiw/codemirror-extensions-langs": "4.19.16",
|
||||||
"@uiw/react-codemirror": "4.19.16",
|
"@uiw/react-codemirror": "4.19.16",
|
||||||
"ajv": "8.12.0",
|
"ajv": "8.12.0",
|
||||||
@ -159,7 +159,7 @@
|
|||||||
"slate": "0.94.1",
|
"slate": "0.94.1",
|
||||||
"slate-history": "0.93.0",
|
"slate-history": "0.93.0",
|
||||||
"slate-hyperscript": "0.77.0",
|
"slate-hyperscript": "0.77.0",
|
||||||
"slate-react": "0.95.0",
|
"slate-react": "0.98.3",
|
||||||
"stream-browserify": "3.0.0",
|
"stream-browserify": "3.0.0",
|
||||||
"styled-components": "5.3.10",
|
"styled-components": "5.3.10",
|
||||||
"symbol-observable": "4.0.0",
|
"symbol-observable": "4.0.0",
|
||||||
|
@ -12,3 +12,5 @@ export const translate = () => (Component: FC) => {
|
|||||||
return React.createElement(Component, { t, ...props });
|
return React.createElement(Component, { t, ...props });
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useTranslate = () => (key: string, _options: unknown) => key;
|
||||||
|
3
packages/core/src/components/App.css
Normal file
3
packages/core/src/components/App.css
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
.CMS_App_root {
|
||||||
|
@apply h-full;
|
||||||
|
}
|
@ -20,6 +20,7 @@ import { currentBackend } from '@staticcms/core/backend';
|
|||||||
import { changeTheme } from '../actions/globalUI';
|
import { changeTheme } from '../actions/globalUI';
|
||||||
import { invokeEvent } from '../lib/registry';
|
import { invokeEvent } from '../lib/registry';
|
||||||
import { getDefaultPath } from '../lib/util/collection.util';
|
import { getDefaultPath } from '../lib/util/collection.util';
|
||||||
|
import { generateClassNames } from '../lib/util/theming.util';
|
||||||
import { selectTheme } from '../reducers/selectors/globalUI';
|
import { selectTheme } from '../reducers/selectors/globalUI';
|
||||||
import { useAppDispatch, useAppSelector } from '../store/hooks';
|
import { useAppDispatch, useAppSelector } from '../store/hooks';
|
||||||
import CollectionRoute from './collections/CollectionRoute';
|
import CollectionRoute from './collections/CollectionRoute';
|
||||||
@ -37,6 +38,10 @@ import type { RootState } from '@staticcms/core/store';
|
|||||||
import type { ComponentType } from 'react';
|
import type { ComponentType } from 'react';
|
||||||
import type { ConnectedProps } from 'react-redux';
|
import type { ConnectedProps } from 'react-redux';
|
||||||
|
|
||||||
|
import './App.css';
|
||||||
|
|
||||||
|
export const classes = generateClassNames('App', ['root', 'content']);
|
||||||
|
|
||||||
TopBarProgress.config({
|
TopBarProgress.config({
|
||||||
barColors: {
|
barColors: {
|
||||||
0: '#000',
|
0: '#000',
|
||||||
@ -267,8 +272,8 @@ const App = ({
|
|||||||
<ScrollSync key="scroll-sync" enabled={scrollSyncEnabled}>
|
<ScrollSync key="scroll-sync" enabled={scrollSyncEnabled}>
|
||||||
<>
|
<>
|
||||||
<div key="back-to-top-anchor" id="back-to-top-anchor" />
|
<div key="back-to-top-anchor" id="back-to-top-anchor" />
|
||||||
<div key="cms-root" id="cms-root" className="h-full">
|
<div key="cms-root" id="cms-root" className={classes.root}>
|
||||||
<div key="cms-wrapper" className="cms-wrapper">
|
<div key="cms-wrapper" className={classes.content}>
|
||||||
<Snackbars key="snackbars" />
|
<Snackbars key="snackbars" />
|
||||||
{content}
|
{content}
|
||||||
<Alert key="alert" />
|
<Alert key="alert" />
|
||||||
|
43
packages/core/src/components/ErrorBoundary.css
Normal file
43
packages/core/src/components/ErrorBoundary.css
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
.CMS_ErrorBoundary_root {
|
||||||
|
@apply flex
|
||||||
|
flex-col
|
||||||
|
bg-slate-50
|
||||||
|
dark:bg-slate-900
|
||||||
|
min-h-screen
|
||||||
|
gap-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_ErrorBoundary_header {
|
||||||
|
@apply flex
|
||||||
|
flex-col
|
||||||
|
py-2
|
||||||
|
px-4
|
||||||
|
gap-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_ErrorBoundary_title {
|
||||||
|
@apply text-2xl
|
||||||
|
font-bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_ErrorBoundary_report-link {
|
||||||
|
@apply text-blue-500
|
||||||
|
hover:underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_ErrorBoundary_content {
|
||||||
|
@apply flex
|
||||||
|
flex-col
|
||||||
|
py-2
|
||||||
|
px-4
|
||||||
|
gap-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_ErrorBoundary_details-title {
|
||||||
|
@apply text-xl
|
||||||
|
font-bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_ErrorBoundary_error-line {
|
||||||
|
@apply whitespace-pre;
|
||||||
|
}
|
@ -6,10 +6,23 @@ import { translate } from 'react-polyglot';
|
|||||||
import yaml from 'yaml';
|
import yaml from 'yaml';
|
||||||
|
|
||||||
import { localForage } from '@staticcms/core/lib/util';
|
import { localForage } from '@staticcms/core/lib/util';
|
||||||
|
import { generateClassNames } from '../lib/util/theming.util';
|
||||||
|
|
||||||
import type { Config, TranslatedProps } from '@staticcms/core/interface';
|
import type { Config, TranslatedProps } from '@staticcms/core/interface';
|
||||||
import type { ComponentClass, ReactNode } from 'react';
|
import type { ComponentClass, ReactNode } from 'react';
|
||||||
|
|
||||||
|
import './ErrorBoundary.css';
|
||||||
|
|
||||||
|
export const classes = generateClassNames('ErrorBoundary', [
|
||||||
|
'root',
|
||||||
|
'header',
|
||||||
|
'title',
|
||||||
|
'report-link',
|
||||||
|
'content',
|
||||||
|
'details-title',
|
||||||
|
'error-line',
|
||||||
|
]);
|
||||||
|
|
||||||
const ISSUE_URL = 'https://github.com/StaticJsCMS/static-cms/issues/new?';
|
const ISSUE_URL = 'https://github.com/StaticJsCMS/static-cms/issues/new?';
|
||||||
|
|
||||||
function getIssueTemplate(version: string, provider: string, browser: string, config: string) {
|
function getIssueTemplate(version: string, provider: string, browser: string, config: string) {
|
||||||
@ -147,27 +160,9 @@ class ErrorBoundary extends Component<TranslatedProps<ErrorBoundaryProps>, Error
|
|||||||
return this.props.children;
|
return this.props.children;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div
|
<div key="error-boundary-container" className={classes.root}>
|
||||||
key="error-boundary-container"
|
<div className={classes.header}>
|
||||||
className="
|
<h1 className={classes.title}>{t('ui.errorBoundary.title')}</h1>
|
||||||
flex
|
|
||||||
flex-col
|
|
||||||
bg-slate-50
|
|
||||||
dark:bg-slate-900
|
|
||||||
min-h-screen
|
|
||||||
gap-2
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="
|
|
||||||
flex
|
|
||||||
flex-col
|
|
||||||
py-2
|
|
||||||
px-4
|
|
||||||
gap-2
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<h1 className="text-2xl bold">{t('ui.errorBoundary.title')}</h1>
|
|
||||||
<p>
|
<p>
|
||||||
<span>{t('ui.errorBoundary.details')}</span>
|
<span>{t('ui.errorBoundary.details')}</span>
|
||||||
<a
|
<a
|
||||||
@ -175,10 +170,7 @@ class ErrorBoundary extends Component<TranslatedProps<ErrorBoundaryProps>, Error
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
data-testid="issue-url"
|
data-testid="issue-url"
|
||||||
className="
|
className={classes['report-link']}
|
||||||
text-blue-500
|
|
||||||
hover:underline
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
{t('ui.errorBoundary.reportIt')}
|
{t('ui.errorBoundary.reportIt')}
|
||||||
</a>
|
</a>
|
||||||
@ -193,19 +185,11 @@ class ErrorBoundary extends Component<TranslatedProps<ErrorBoundaryProps>, Error
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
<div
|
<div className={classes.content}>
|
||||||
className="
|
<h2 className={classes['details-title']}>{t('ui.errorBoundary.detailsHeading')}</h2>
|
||||||
flex
|
|
||||||
flex-col
|
|
||||||
py-2
|
|
||||||
px-4
|
|
||||||
gap-2
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<h2 className="text-xl bold">{t('ui.errorBoundary.detailsHeading')}</h2>
|
|
||||||
<p>
|
<p>
|
||||||
{errorMessage.split('\n').map((item, index) => [
|
{errorMessage.split('\n').map((item, index) => [
|
||||||
<span key={`error-line-${index}`} className="whitespace-pre">
|
<span key={`error-line-${index}`} className={classes['error-line']}>
|
||||||
{item}
|
{item}
|
||||||
</span>,
|
</span>,
|
||||||
<br key={`error-break-${index}`} />,
|
<br key={`error-break-${index}`} />,
|
||||||
|
30
packages/core/src/components/MainView.css
Normal file
30
packages/core/src/components/MainView.css
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
.CMS_MainView_root {
|
||||||
|
@apply flex
|
||||||
|
bg-slate-50
|
||||||
|
dark:bg-slate-900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_MainView_body {
|
||||||
|
@apply h-main-mobile
|
||||||
|
md:h-main
|
||||||
|
relative
|
||||||
|
w-full;
|
||||||
|
|
||||||
|
&.CMS_MainView_show-left-nav {
|
||||||
|
@apply left-0
|
||||||
|
md:w-main;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.CMS_MainView_no-margin) {
|
||||||
|
@apply px-5
|
||||||
|
py-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.CMS_MainView_no-scroll {
|
||||||
|
@apply overflow-hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.CMS_MainView_no-scroll) {
|
||||||
|
@apply overflow-y-auto;
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,7 @@ import React from 'react';
|
|||||||
import TopBarProgress from 'react-topbar-progress-indicator';
|
import TopBarProgress from 'react-topbar-progress-indicator';
|
||||||
|
|
||||||
import classNames from '../lib/util/classNames.util';
|
import classNames from '../lib/util/classNames.util';
|
||||||
|
import { generateClassNames } from '../lib/util/theming.util';
|
||||||
import BottomNavigation from './navbar/BottomNavigation';
|
import BottomNavigation from './navbar/BottomNavigation';
|
||||||
import Navbar from './navbar/Navbar';
|
import Navbar from './navbar/Navbar';
|
||||||
import Sidebar from './navbar/Sidebar';
|
import Sidebar from './navbar/Sidebar';
|
||||||
@ -9,6 +10,16 @@ import Sidebar from './navbar/Sidebar';
|
|||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import type { Breadcrumb, Collection } from '../interface';
|
import type { Breadcrumb, Collection } from '../interface';
|
||||||
|
|
||||||
|
import './MainView.css';
|
||||||
|
|
||||||
|
export const classes = generateClassNames('MainView', [
|
||||||
|
'root',
|
||||||
|
'body',
|
||||||
|
'show-left-nav',
|
||||||
|
'no-margin',
|
||||||
|
'no-scroll',
|
||||||
|
]);
|
||||||
|
|
||||||
TopBarProgress.config({
|
TopBarProgress.config({
|
||||||
barColors: {
|
barColors: {
|
||||||
0: '#000',
|
0: '#000',
|
||||||
@ -46,20 +57,15 @@ const MainView = ({
|
|||||||
showQuickCreate={showQuickCreate}
|
showQuickCreate={showQuickCreate}
|
||||||
navbarActions={navbarActions}
|
navbarActions={navbarActions}
|
||||||
/>
|
/>
|
||||||
<div className="flex bg-slate-50 dark:bg-slate-900">
|
<div className={classes.root}>
|
||||||
{showLeftNav ? <Sidebar /> : null}
|
{showLeftNav ? <Sidebar /> : null}
|
||||||
<div
|
<div
|
||||||
id="main-view"
|
id="main-view"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
showLeftNav ? ' w-full left-0 md:w-main' : 'w-full',
|
classes.body,
|
||||||
!noMargin && 'px-5 py-4',
|
showLeftNav && classes['show-left-nav'],
|
||||||
noScroll ? 'overflow-hidden' : 'overflow-y-auto',
|
noMargin && classes['no-margin'],
|
||||||
`
|
noScroll && classes['no-scroll'],
|
||||||
h-main-mobile
|
|
||||||
md:h-main
|
|
||||||
relative
|
|
||||||
styled-scrollbars
|
|
||||||
`,
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
@ -0,0 +1,17 @@
|
|||||||
|
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
|
||||||
|
|
||||||
|
const collectionClasses = generateClassNames('Collection', [
|
||||||
|
'root',
|
||||||
|
'content',
|
||||||
|
'search-query',
|
||||||
|
'description',
|
||||||
|
'description-card',
|
||||||
|
'controls',
|
||||||
|
'header-wrapper',
|
||||||
|
'header',
|
||||||
|
'header-icon',
|
||||||
|
'header-label',
|
||||||
|
'new-entry-button',
|
||||||
|
]);
|
||||||
|
|
||||||
|
export default collectionClasses;
|
87
packages/core/src/components/collections/Collection.css
Normal file
87
packages/core/src/components/collections/Collection.css
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
.CMS_Collection_root {
|
||||||
|
@apply flex
|
||||||
|
flex-col
|
||||||
|
h-full
|
||||||
|
px-5
|
||||||
|
pt-4
|
||||||
|
overflow-hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Collection_content {
|
||||||
|
@apply flex
|
||||||
|
items-center
|
||||||
|
mb-4
|
||||||
|
flex-row
|
||||||
|
gap-4
|
||||||
|
sm:gap-0
|
||||||
|
flex-wrap
|
||||||
|
md:flex-nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Collection_search-query {
|
||||||
|
@apply flex-grow;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Collection_description {
|
||||||
|
@apply flex
|
||||||
|
mb-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Collection_description-card {
|
||||||
|
@apply flex-grow
|
||||||
|
px-3.5
|
||||||
|
py-2.5
|
||||||
|
text-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Collection_controls {
|
||||||
|
@apply flex
|
||||||
|
items-center
|
||||||
|
relative
|
||||||
|
z-20
|
||||||
|
md:flex-grow-0
|
||||||
|
flex-grow
|
||||||
|
justify-end
|
||||||
|
gap-1.5
|
||||||
|
sm:w-auto
|
||||||
|
lg:gap-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Collection_header-wrapper {
|
||||||
|
@apply flex
|
||||||
|
gap-2
|
||||||
|
sm:gap-2
|
||||||
|
md:gap-4
|
||||||
|
md:w-full
|
||||||
|
justify-normal
|
||||||
|
xs:justify-between
|
||||||
|
sm:justify-normal
|
||||||
|
truncate;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Collection_header {
|
||||||
|
@apply text-xl
|
||||||
|
font-semibold
|
||||||
|
flex
|
||||||
|
items-center
|
||||||
|
text-gray-800
|
||||||
|
dark:text-gray-300
|
||||||
|
gap-2
|
||||||
|
md:w-auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Collection_header-icon {
|
||||||
|
@apply flex
|
||||||
|
items-center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Collection_header-label {
|
||||||
|
@apply max-w-collection-header
|
||||||
|
md:flex-grow
|
||||||
|
truncate;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Collection_new-entry-button {
|
||||||
|
@apply hidden
|
||||||
|
md:flex;
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
|
|
||||||
import ViewStyleControl from '../common/view-style/ViewStyleControl';
|
import ViewStyleControl from '../common/view-style/ViewStyleControl';
|
||||||
|
import collectionClasses from './Collection.classes';
|
||||||
import FilterControl from './FilterControl';
|
import FilterControl from './FilterControl';
|
||||||
import GroupControl from './GroupControl';
|
import GroupControl from './GroupControl';
|
||||||
import MobileCollectionControls from './mobile/MobileCollectionControls';
|
import MobileCollectionControls from './mobile/MobileCollectionControls';
|
||||||
@ -61,20 +62,7 @@ const CollectionControls = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div className={collectionClasses.controls}>
|
||||||
className="
|
|
||||||
flex
|
|
||||||
items-center
|
|
||||||
relative
|
|
||||||
z-20
|
|
||||||
w-full
|
|
||||||
justify-end
|
|
||||||
gap-1.5
|
|
||||||
sm:w-auto
|
|
||||||
sm:justify-normal
|
|
||||||
lg:gap-2
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<ViewStyleControl viewStyle={viewStyle} onChangeViewStyle={onChangeViewStyle} />
|
<ViewStyleControl viewStyle={viewStyle} onChangeViewStyle={onChangeViewStyle} />
|
||||||
{showGroupControl || showFilterControl || showFilterControl ? (
|
{showGroupControl || showFilterControl || showFilterControl ? (
|
||||||
<MobileCollectionControls
|
<MobileCollectionControls
|
||||||
|
@ -4,6 +4,7 @@ import { useParams } from 'react-router-dom';
|
|||||||
|
|
||||||
import useEntries from '@staticcms/core/lib/hooks/useEntries';
|
import useEntries from '@staticcms/core/lib/hooks/useEntries';
|
||||||
import useIcon from '@staticcms/core/lib/hooks/useIcon';
|
import useIcon from '@staticcms/core/lib/hooks/useIcon';
|
||||||
|
import useNewEntryUrl from '@staticcms/core/lib/hooks/useNewEntryUrl';
|
||||||
import {
|
import {
|
||||||
selectEntryCollectionTitle,
|
selectEntryCollectionTitle,
|
||||||
selectFolderEntryExtension,
|
selectFolderEntryExtension,
|
||||||
@ -11,7 +12,7 @@ import {
|
|||||||
import { isNotEmpty } from '@staticcms/core/lib/util/string.util';
|
import { isNotEmpty } from '@staticcms/core/lib/util/string.util';
|
||||||
import { addFileTemplateFields } from '@staticcms/core/lib/widgets/stringTemplate';
|
import { addFileTemplateFields } from '@staticcms/core/lib/widgets/stringTemplate';
|
||||||
import Button from '../common/button/Button';
|
import Button from '../common/button/Button';
|
||||||
import useNewEntryUrl from '@staticcms/core/lib/hooks/useNewEntryUrl';
|
import collectionClasses from './Collection.classes';
|
||||||
|
|
||||||
import type { Collection, Entry, TranslatedProps } from '@staticcms/core/interface';
|
import type { Collection, Entry, TranslatedProps } from '@staticcms/core/interface';
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
@ -63,54 +64,19 @@ const CollectionHeader: FC<TranslatedProps<CollectionHeaderProps>> = ({ collecti
|
|||||||
}, [collection, collectionLabel, entries, filterTerm]);
|
}, [collection, collectionLabel, entries, filterTerm]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className={collectionClasses['header-wrapper']}>
|
||||||
<div
|
<h2 className={collectionClasses.header}>
|
||||||
className="
|
<div className={collectionClasses['header-icon']}>{icon}</div>
|
||||||
flex
|
<div className={collectionClasses['header-label']}>{pluralLabel}</div>
|
||||||
flex-grow
|
</h2>
|
||||||
gap-4
|
{newEntryUrl ? (
|
||||||
justify-normal
|
<Button to={newEntryUrl} className={collectionClasses['new-entry-button']}>
|
||||||
xs:justify-between
|
{t('collection.collectionTop.newButton', {
|
||||||
sm:justify-normal
|
collectionLabel: collectionLabelSingular || pluralLabel,
|
||||||
w-full
|
})}
|
||||||
truncate
|
</Button>
|
||||||
"
|
) : null}
|
||||||
>
|
</div>
|
||||||
<h2
|
|
||||||
className="
|
|
||||||
text-xl
|
|
||||||
font-semibold
|
|
||||||
flex
|
|
||||||
items-center
|
|
||||||
text-gray-800
|
|
||||||
dark:text-gray-300
|
|
||||||
gap-2
|
|
||||||
flex-grow
|
|
||||||
w-full
|
|
||||||
md:grow-0
|
|
||||||
md:w-auto
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<div className="flex items-center">{icon}</div>
|
|
||||||
<div
|
|
||||||
className="
|
|
||||||
w-collection-header
|
|
||||||
flex-grow
|
|
||||||
truncate
|
|
||||||
"
|
|
||||||
>
|
|
||||||
{pluralLabel}
|
|
||||||
</div>
|
|
||||||
</h2>
|
|
||||||
{newEntryUrl ? (
|
|
||||||
<Button to={newEntryUrl} className="hidden md:flex">
|
|
||||||
{t('collection.collectionTop.newButton', {
|
|
||||||
collectionLabel: collectionLabelSingular || pluralLabel,
|
|
||||||
})}
|
|
||||||
</Button>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -0,0 +1,80 @@
|
|||||||
|
.CMS_CollectionSearch_content {
|
||||||
|
@apply relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_CollectionSearch_icon-wrapper {
|
||||||
|
@apply absolute
|
||||||
|
inset-y-0
|
||||||
|
left-0
|
||||||
|
flex
|
||||||
|
items-center
|
||||||
|
pl-3
|
||||||
|
pointer-events-none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_CollectionSearch_icon {
|
||||||
|
@apply w-5
|
||||||
|
h-5
|
||||||
|
text-gray-500
|
||||||
|
dark:text-gray-400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_CollectionSearch_input {
|
||||||
|
@apply block
|
||||||
|
w-full
|
||||||
|
p-1.5
|
||||||
|
pl-10
|
||||||
|
text-sm
|
||||||
|
text-gray-800
|
||||||
|
border
|
||||||
|
border-gray-300
|
||||||
|
rounded-lg
|
||||||
|
bg-gray-50
|
||||||
|
focus-visible:outline-none
|
||||||
|
focus:ring-4
|
||||||
|
focus:ring-gray-200
|
||||||
|
dark:bg-gray-700
|
||||||
|
dark:border-gray-600
|
||||||
|
dark:placeholder-gray-400
|
||||||
|
dark:text-white
|
||||||
|
dark:focus:ring-slate-700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_CollectionSearch_search-in {
|
||||||
|
@apply absolute
|
||||||
|
overflow-auto
|
||||||
|
rounded-md
|
||||||
|
bg-white
|
||||||
|
text-base
|
||||||
|
shadow-md
|
||||||
|
ring-1
|
||||||
|
ring-black
|
||||||
|
ring-opacity-5
|
||||||
|
focus:outline-none
|
||||||
|
sm:text-sm
|
||||||
|
z-[1300]
|
||||||
|
dark:bg-slate-700
|
||||||
|
dark:shadow-lg;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_CollectionSearch_search-in-content {
|
||||||
|
@apply flex
|
||||||
|
flex-col
|
||||||
|
min-w-[200px];
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_CollectionSearch_search-in-label {
|
||||||
|
@apply text-base
|
||||||
|
text-slate-500
|
||||||
|
dark:text-slate-400
|
||||||
|
py-2
|
||||||
|
px-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_CollectionSearch_search-in-option {
|
||||||
|
@apply cursor-pointer
|
||||||
|
hover:bg-blue-500
|
||||||
|
hover:text-gray-100
|
||||||
|
py-2
|
||||||
|
px-3;
|
||||||
|
}
|
@ -1,11 +1,27 @@
|
|||||||
import PopperUnstyled from '@mui/base/PopperUnstyled';
|
import { Popper as BasePopper } from '@mui/base/Popper';
|
||||||
import { Search as SearchIcon } from '@styled-icons/material/Search';
|
import { Search as SearchIcon } from '@styled-icons/material/Search';
|
||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { translate } from 'react-polyglot';
|
import { translate } from 'react-polyglot';
|
||||||
|
|
||||||
|
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
|
||||||
|
|
||||||
import type { Collection, Collections, TranslatedProps } from '@staticcms/core/interface';
|
import type { Collection, Collections, TranslatedProps } from '@staticcms/core/interface';
|
||||||
import type { ChangeEvent, FocusEvent, KeyboardEvent, MouseEvent } from 'react';
|
import type { ChangeEvent, FocusEvent, KeyboardEvent, MouseEvent } from 'react';
|
||||||
|
|
||||||
|
import './CollectionSearch.css';
|
||||||
|
|
||||||
|
export const classes = generateClassNames('CollectionSearch', [
|
||||||
|
'root',
|
||||||
|
'content',
|
||||||
|
'icon-wrapper',
|
||||||
|
'icon',
|
||||||
|
'input',
|
||||||
|
'search-in',
|
||||||
|
'search-in-content',
|
||||||
|
'search-in-label',
|
||||||
|
'search-in-option',
|
||||||
|
]);
|
||||||
|
|
||||||
interface CollectionSearchProps {
|
interface CollectionSearchProps {
|
||||||
collections: Collections;
|
collections: Collections;
|
||||||
collection?: Collection;
|
collection?: Collection;
|
||||||
@ -136,39 +152,21 @@ const CollectionSearch = ({
|
|||||||
[submitSearch],
|
[submitSearch],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleClick = useCallback((event: MouseEvent) => {
|
const handleClick = useCallback((event: MouseEvent<HTMLInputElement>) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
setAnchorEl(event.currentTarget);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className={classes.root}>
|
||||||
<div className="relative">
|
<div className={classes.content}>
|
||||||
<div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
|
<div className={classes['icon-wrapper']}>
|
||||||
<SearchIcon className="w-5 h-5 text-gray-500 dark:text-gray-400" />
|
<SearchIcon className={classes.icon} />
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="first_name"
|
id="first_name"
|
||||||
className="
|
className={classes.input}
|
||||||
block
|
|
||||||
w-full
|
|
||||||
p-1.5
|
|
||||||
pl-10
|
|
||||||
text-sm
|
|
||||||
text-gray-800
|
|
||||||
border
|
|
||||||
border-gray-300
|
|
||||||
rounded-lg
|
|
||||||
bg-gray-50
|
|
||||||
focus-visible:outline-none
|
|
||||||
focus:ring-4
|
|
||||||
focus:ring-gray-200
|
|
||||||
dark:bg-gray-700
|
|
||||||
dark:border-gray-600
|
|
||||||
dark:placeholder-gray-400
|
|
||||||
dark:text-white
|
|
||||||
dark:focus:ring-slate-700
|
|
||||||
"
|
|
||||||
placeholder={t('collection.sidebar.searchAll')}
|
placeholder={t('collection.sidebar.searchAll')}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
@ -178,57 +176,20 @@ const CollectionSearch = ({
|
|||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<PopperUnstyled
|
<BasePopper
|
||||||
open={open}
|
open={open}
|
||||||
component="div"
|
|
||||||
placement="top"
|
placement="top"
|
||||||
anchorEl={anchorEl}
|
anchorEl={anchorEl}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
className="
|
className={classes['search-in']}
|
||||||
absolute
|
slots={{
|
||||||
overflow-auto
|
root: 'div',
|
||||||
rounded-md
|
}}
|
||||||
bg-white
|
|
||||||
text-base
|
|
||||||
shadow-md
|
|
||||||
ring-1
|
|
||||||
ring-black
|
|
||||||
ring-opacity-5
|
|
||||||
focus:outline-none
|
|
||||||
sm:text-sm
|
|
||||||
z-[1300]
|
|
||||||
dark:bg-slate-700
|
|
||||||
dark:shadow-lg
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
<div
|
<div key="edit-content" contentEditable={false} className={classes['search-in-content']}>
|
||||||
key="edit-content"
|
<div className={classes['search-in-label']}>{t('collection.sidebar.searchIn')}</div>
|
||||||
contentEditable={false}
|
|
||||||
className="
|
|
||||||
flex
|
|
||||||
flex-col
|
|
||||||
min-w-[200px]
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
className="
|
className={classes['search-in-option']}
|
||||||
text-md
|
|
||||||
text-slate-500
|
|
||||||
dark:text-slate-400
|
|
||||||
py-2
|
|
||||||
px-3
|
|
||||||
"
|
|
||||||
>
|
|
||||||
{t('collection.sidebar.searchIn')}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="
|
|
||||||
cursor-pointer
|
|
||||||
hover:bg-blue-500
|
|
||||||
hover:color-gray-100
|
|
||||||
py-2
|
|
||||||
px-3
|
|
||||||
"
|
|
||||||
onClick={e => handleSuggestionClick(e, -1)}
|
onClick={e => handleSuggestionClick(e, -1)}
|
||||||
onMouseDown={e => e.preventDefault()}
|
onMouseDown={e => e.preventDefault()}
|
||||||
>
|
>
|
||||||
@ -239,52 +200,13 @@ const CollectionSearch = ({
|
|||||||
key={idx}
|
key={idx}
|
||||||
onClick={e => handleSuggestionClick(e, idx)}
|
onClick={e => handleSuggestionClick(e, idx)}
|
||||||
onMouseDown={e => e.preventDefault()}
|
onMouseDown={e => e.preventDefault()}
|
||||||
className="
|
className={classes['search-in-option']}
|
||||||
cursor-pointer
|
|
||||||
hover:bg-blue-500
|
|
||||||
hover:color-gray-100
|
|
||||||
py-2
|
|
||||||
px-3
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
{collection.label}
|
{collection.label}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</PopperUnstyled>
|
</BasePopper>
|
||||||
{/* <Popover
|
|
||||||
id="search-popover"
|
|
||||||
open={open}
|
|
||||||
anchorEl={anchorEl}
|
|
||||||
onClose={handleClose}
|
|
||||||
disableAutoFocus
|
|
||||||
disableEnforceFocus
|
|
||||||
disableScrollLock
|
|
||||||
hideBackdrop
|
|
||||||
anchorOrigin={{
|
|
||||||
vertical: 'bottom',
|
|
||||||
horizontal: 'left',
|
|
||||||
}}
|
|
||||||
sx={{
|
|
||||||
width: 300
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<div>{t('collection.sidebar.searchIn')}</div>
|
|
||||||
<div onClick={e => handleSuggestionClick(e, -1)} onMouseDown={e => e.preventDefault()}>
|
|
||||||
{t('collection.sidebar.allCollections')}
|
|
||||||
</div>
|
|
||||||
{collections.map((collection, idx) => (
|
|
||||||
<div
|
|
||||||
key={idx}
|
|
||||||
onClick={e => handleSuggestionClick(e, idx)}
|
|
||||||
onMouseDown={e => e.preventDefault()}
|
|
||||||
>
|
|
||||||
{collection.label}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Popover> */}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -21,6 +21,7 @@ import {
|
|||||||
selectViewStyle,
|
selectViewStyle,
|
||||||
} from '@staticcms/core/reducers/selectors/entries';
|
} from '@staticcms/core/reducers/selectors/entries';
|
||||||
import Card from '../common/card/Card';
|
import Card from '../common/card/Card';
|
||||||
|
import collectionClasses from './Collection.classes';
|
||||||
import CollectionControls from './CollectionControls';
|
import CollectionControls from './CollectionControls';
|
||||||
import CollectionHeader from './CollectionHeader';
|
import CollectionHeader from './CollectionHeader';
|
||||||
import EntriesCollection from './entries/EntriesCollection';
|
import EntriesCollection from './entries/EntriesCollection';
|
||||||
@ -37,6 +38,8 @@ import type { RootState } from '@staticcms/core/store';
|
|||||||
import type { ComponentType } from 'react';
|
import type { ComponentType } from 'react';
|
||||||
import type { ConnectedProps } from 'react-redux';
|
import type { ConnectedProps } from 'react-redux';
|
||||||
|
|
||||||
|
import './Collection.css';
|
||||||
|
|
||||||
const CollectionView = ({
|
const CollectionView = ({
|
||||||
collection,
|
collection,
|
||||||
collections,
|
collections,
|
||||||
@ -183,11 +186,11 @@ const CollectionView = ({
|
|||||||
const collectionDescription = collection?.description;
|
const collectionDescription = collection?.description;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full px-5 pt-4 overflow-hidden">
|
<div className={collectionClasses.root}>
|
||||||
<div className="flex items-center mb-4 flex-row gap-4 sm:gap-0">
|
<div className={collectionClasses.content}>
|
||||||
{isSearchResults ? (
|
{isSearchResults ? (
|
||||||
<>
|
<>
|
||||||
<div className="flex-grow">
|
<div className={collectionClasses['search-query']}>
|
||||||
<div>{t(searchResultKey, { searchTerm, collection: collection?.label })}</div>
|
<div>{t(searchResultKey, { searchTerm, collection: collection?.label })}</div>
|
||||||
</div>
|
</div>
|
||||||
<CollectionControls viewStyle={viewStyle} onChangeViewStyle={changeViewStyle} />
|
<CollectionControls viewStyle={viewStyle} onChangeViewStyle={changeViewStyle} />
|
||||||
@ -212,8 +215,8 @@ const CollectionView = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{collectionDescription ? (
|
{collectionDescription ? (
|
||||||
<div className="flex mb-4">
|
<div className={collectionClasses.description}>
|
||||||
<Card className="flex-grow px-3.5 py-2.5 text-sm">{collectionDescription}</Card>
|
<Card className={collectionClasses['description-card']}>{collectionDescription}</Card>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{entries}
|
{entries}
|
||||||
|
42
packages/core/src/components/collections/FilterControl.css
Normal file
42
packages/core/src/components/collections/FilterControl.css
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
.CMS_FilterControl_root {
|
||||||
|
@apply hidden
|
||||||
|
lg:block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_FilterControl_filter-label {
|
||||||
|
@apply ml-2
|
||||||
|
text-sm
|
||||||
|
font-medium
|
||||||
|
text-gray-800
|
||||||
|
dark:text-gray-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_FilterControl_list-root {
|
||||||
|
@apply flex
|
||||||
|
flex-col
|
||||||
|
gap-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_FilterControl_list-label {
|
||||||
|
@apply text-lg
|
||||||
|
font-bold
|
||||||
|
text-gray-800
|
||||||
|
dark:text-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_FilterControl_list-filter {
|
||||||
|
@apply ml-1.5
|
||||||
|
font-medium
|
||||||
|
flex
|
||||||
|
items-center
|
||||||
|
text-gray-800
|
||||||
|
dark:text-gray-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_FilterControl_list-filter-label {
|
||||||
|
@apply ml-2
|
||||||
|
text-base
|
||||||
|
font-medium
|
||||||
|
text-gray-800
|
||||||
|
dark:text-gray-300;
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
import React, { useCallback, useMemo } from 'react';
|
import React, { useCallback, useMemo } from 'react';
|
||||||
import { translate } from 'react-polyglot';
|
import { translate } from 'react-polyglot';
|
||||||
|
|
||||||
|
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
|
||||||
import Menu from '../common/menu/Menu';
|
import Menu from '../common/menu/Menu';
|
||||||
import MenuGroup from '../common/menu/MenuGroup';
|
import MenuGroup from '../common/menu/MenuGroup';
|
||||||
import MenuItemButton from '../common/menu/MenuItemButton';
|
import MenuItemButton from '../common/menu/MenuItemButton';
|
||||||
@ -8,6 +9,18 @@ import MenuItemButton from '../common/menu/MenuItemButton';
|
|||||||
import type { FilterMap, TranslatedProps, ViewFilter } from '@staticcms/core/interface';
|
import type { FilterMap, TranslatedProps, ViewFilter } from '@staticcms/core/interface';
|
||||||
import type { FC, MouseEvent } from 'react';
|
import type { FC, MouseEvent } from 'react';
|
||||||
|
|
||||||
|
import './FilterControl.css';
|
||||||
|
|
||||||
|
export const classes = generateClassNames('FilterControl', [
|
||||||
|
'root',
|
||||||
|
'filter',
|
||||||
|
'filter-label',
|
||||||
|
'list-root',
|
||||||
|
'list-label',
|
||||||
|
'list-filter',
|
||||||
|
'list-filter-label',
|
||||||
|
]);
|
||||||
|
|
||||||
export interface FilterControlProps {
|
export interface FilterControlProps {
|
||||||
filter: Record<string, FilterMap> | undefined;
|
filter: Record<string, FilterMap> | undefined;
|
||||||
viewFilters: ViewFilter[] | undefined;
|
viewFilters: ViewFilter[] | undefined;
|
||||||
@ -35,31 +48,15 @@ const FilterControl = ({
|
|||||||
|
|
||||||
if (variant === 'list') {
|
if (variant === 'list') {
|
||||||
return (
|
return (
|
||||||
<div key="filter-by-list" className="flex flex-col gap-2">
|
<div key="filter-by-list" className={classes['list-root']}>
|
||||||
<h3
|
<h3 className={classes['list-label']}>{t('collection.collectionTop.filterBy')}</h3>
|
||||||
className="
|
|
||||||
text-lg
|
|
||||||
font-bold
|
|
||||||
text-gray-800
|
|
||||||
dark:text-white
|
|
||||||
"
|
|
||||||
>
|
|
||||||
{t('collection.collectionTop.filterBy')}
|
|
||||||
</h3>
|
|
||||||
{viewFilters.map(viewFilter => {
|
{viewFilters.map(viewFilter => {
|
||||||
const checked = Boolean(viewFilter.id && filter[viewFilter?.id]?.active) ?? false;
|
const checked = Boolean(viewFilter.id && filter[viewFilter?.id]?.active) ?? false;
|
||||||
const labelId = `filter-list-label-${viewFilter.label}`;
|
const labelId = `filter-list-label-${viewFilter.label}`;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={viewFilter.id}
|
key={viewFilter.id}
|
||||||
className="
|
className={classes['list-filter']}
|
||||||
ml-1.5
|
|
||||||
font-medium
|
|
||||||
flex
|
|
||||||
items-center
|
|
||||||
text-gray-800
|
|
||||||
dark:text-gray-300
|
|
||||||
"
|
|
||||||
onClick={handleFilterClick(viewFilter)}
|
onClick={handleFilterClick(viewFilter)}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
@ -67,13 +64,10 @@ const FilterControl = ({
|
|||||||
id={labelId}
|
id={labelId}
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
value=""
|
value=""
|
||||||
className=""
|
|
||||||
checked={checked}
|
checked={checked}
|
||||||
readOnly
|
readOnly
|
||||||
/>
|
/>
|
||||||
<label className="ml-2 text-md font-medium text-gray-800 dark:text-gray-300">
|
<label className={classes['list-filter-label']}>{viewFilter.label}</label>
|
||||||
{viewFilter.label}
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@ -86,26 +80,27 @@ const FilterControl = ({
|
|||||||
key="filter-by-menu"
|
key="filter-by-menu"
|
||||||
label={t('collection.collectionTop.filterBy')}
|
label={t('collection.collectionTop.filterBy')}
|
||||||
variant={anyActive ? 'contained' : 'outlined'}
|
variant={anyActive ? 'contained' : 'outlined'}
|
||||||
rootClassName="hidden lg:block"
|
rootClassName={classes.root}
|
||||||
>
|
>
|
||||||
<MenuGroup>
|
<MenuGroup>
|
||||||
{viewFilters.map(viewFilter => {
|
{viewFilters.map(viewFilter => {
|
||||||
const checked = Boolean(viewFilter.id && filter[viewFilter?.id]?.active) ?? false;
|
const checked = Boolean(viewFilter.id && filter[viewFilter?.id]?.active) ?? false;
|
||||||
const labelId = `filter-list-label-${viewFilter.label}`;
|
const labelId = `filter-list-label-${viewFilter.label}`;
|
||||||
return (
|
return (
|
||||||
<MenuItemButton key={viewFilter.id} onClick={handleFilterClick(viewFilter)}>
|
<MenuItemButton
|
||||||
|
key={viewFilter.id}
|
||||||
|
onClick={handleFilterClick(viewFilter)}
|
||||||
|
className={classes.filter}
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
key={`${labelId}-${checked}`}
|
key={`${labelId}-${checked}`}
|
||||||
id={labelId}
|
id={labelId}
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
value=""
|
value=""
|
||||||
className=""
|
|
||||||
checked={checked}
|
checked={checked}
|
||||||
readOnly
|
readOnly
|
||||||
/>
|
/>
|
||||||
<label className="ml-2 text-sm font-medium text-gray-800 dark:text-gray-300">
|
<label className={classes['filter-label']}>{viewFilter.label}</label>
|
||||||
{viewFilter.label}
|
|
||||||
</label>
|
|
||||||
</MenuItemButton>
|
</MenuItemButton>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
47
packages/core/src/components/collections/GroupControl.css
Normal file
47
packages/core/src/components/collections/GroupControl.css
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
.CMS_GroupControl_root {
|
||||||
|
@apply hidden
|
||||||
|
lg:block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_GroupControl_list {
|
||||||
|
@apply flex
|
||||||
|
flex-col
|
||||||
|
gap-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_GroupControl_list-label {
|
||||||
|
@apply text-lg
|
||||||
|
font-bold
|
||||||
|
text-gray-800
|
||||||
|
dark:text-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_GroupControl_list-option {
|
||||||
|
@apply ml-0.5
|
||||||
|
font-medium
|
||||||
|
flex
|
||||||
|
items-center
|
||||||
|
text-gray-800
|
||||||
|
dark:text-gray-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_GroupControl_list-option-label {
|
||||||
|
@apply ml-2
|
||||||
|
text-base
|
||||||
|
font-medium
|
||||||
|
text-gray-800
|
||||||
|
dark:text-gray-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_GroupControl_list-option-checked-icon {
|
||||||
|
@apply ml-2
|
||||||
|
w-6
|
||||||
|
h-6
|
||||||
|
text-blue-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_GroupControl_list-option-not-checked {
|
||||||
|
@apply ml-2
|
||||||
|
w-6
|
||||||
|
h-6;
|
||||||
|
}
|
@ -2,6 +2,7 @@ import { Check as CheckIcon } from '@styled-icons/material/Check';
|
|||||||
import React, { useCallback, useMemo } from 'react';
|
import React, { useCallback, useMemo } from 'react';
|
||||||
import { translate } from 'react-polyglot';
|
import { translate } from 'react-polyglot';
|
||||||
|
|
||||||
|
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
|
||||||
import Menu from '../common/menu/Menu';
|
import Menu from '../common/menu/Menu';
|
||||||
import MenuGroup from '../common/menu/MenuGroup';
|
import MenuGroup from '../common/menu/MenuGroup';
|
||||||
import MenuItemButton from '../common/menu/MenuItemButton';
|
import MenuItemButton from '../common/menu/MenuItemButton';
|
||||||
@ -9,6 +10,19 @@ import MenuItemButton from '../common/menu/MenuItemButton';
|
|||||||
import type { GroupMap, TranslatedProps, ViewGroup } from '@staticcms/core/interface';
|
import type { GroupMap, TranslatedProps, ViewGroup } from '@staticcms/core/interface';
|
||||||
import type { FC, MouseEvent } from 'react';
|
import type { FC, MouseEvent } from 'react';
|
||||||
|
|
||||||
|
import './GroupControl.css';
|
||||||
|
|
||||||
|
export const classes = generateClassNames('GroupControl', [
|
||||||
|
'root',
|
||||||
|
'option',
|
||||||
|
'list',
|
||||||
|
'list-label',
|
||||||
|
'list-option',
|
||||||
|
'list-option-label',
|
||||||
|
'list-option-checked-icon',
|
||||||
|
'list-option-not-checked',
|
||||||
|
]);
|
||||||
|
|
||||||
export interface GroupControlProps {
|
export interface GroupControlProps {
|
||||||
group: Record<string, GroupMap> | undefined;
|
group: Record<string, GroupMap> | undefined;
|
||||||
viewGroups: ViewGroup[] | undefined;
|
viewGroups: ViewGroup[] | undefined;
|
||||||
@ -36,39 +50,21 @@ const GroupControl = ({
|
|||||||
|
|
||||||
if (variant === 'list') {
|
if (variant === 'list') {
|
||||||
return (
|
return (
|
||||||
<div key="filter-by-list" className="flex flex-col gap-2">
|
<div key="filter-by-list" className={classes.list}>
|
||||||
<h3
|
<h3 className={classes['list-label']}>{t('collection.collectionTop.groupBy')}</h3>
|
||||||
className="
|
|
||||||
text-lg
|
|
||||||
font-bold
|
|
||||||
text-gray-800
|
|
||||||
dark:text-white
|
|
||||||
"
|
|
||||||
>
|
|
||||||
{t('collection.collectionTop.groupBy')}
|
|
||||||
</h3>
|
|
||||||
{viewGroups.map(viewGroup => {
|
{viewGroups.map(viewGroup => {
|
||||||
const active = Boolean(viewGroup.id && group[viewGroup?.id]?.active) ?? false;
|
const active = Boolean(viewGroup.id && group[viewGroup?.id]?.active) ?? false;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={viewGroup.id}
|
key={viewGroup.id}
|
||||||
className="
|
className={classes['list-option']}
|
||||||
ml-0.5
|
|
||||||
font-medium
|
|
||||||
flex
|
|
||||||
items-center
|
|
||||||
text-gray-800
|
|
||||||
dark:text-gray-300
|
|
||||||
"
|
|
||||||
onClick={handleGroupClick(viewGroup)}
|
onClick={handleGroupClick(viewGroup)}
|
||||||
>
|
>
|
||||||
<label className="ml-2 text-md font-medium text-gray-800 dark:text-gray-300">
|
<label className={classes['list-option-label']}>{viewGroup.label}</label>
|
||||||
{viewGroup.label}
|
|
||||||
</label>
|
|
||||||
{active ? (
|
{active ? (
|
||||||
<CheckIcon key="checkmark" className="ml-2 w-6 h-6 text-blue-500" />
|
<CheckIcon key="checkmark" className={classes['list-option-checked-icon']} />
|
||||||
) : (
|
) : (
|
||||||
<div key="not-checked" className="ml-2 w-6 h-6" />
|
<div key="not-checked" className={classes['list-option-not-checked']} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -81,7 +77,7 @@ const GroupControl = ({
|
|||||||
<Menu
|
<Menu
|
||||||
label={t('collection.collectionTop.groupBy')}
|
label={t('collection.collectionTop.groupBy')}
|
||||||
variant={activeGroup ? 'contained' : 'outlined'}
|
variant={activeGroup ? 'contained' : 'outlined'}
|
||||||
rootClassName="hidden lg:block"
|
rootClassName={classes.root}
|
||||||
>
|
>
|
||||||
<MenuGroup>
|
<MenuGroup>
|
||||||
{viewGroups.map(viewGroup => (
|
{viewGroups.map(viewGroup => (
|
||||||
@ -89,6 +85,7 @@ const GroupControl = ({
|
|||||||
key={viewGroup.id}
|
key={viewGroup.id}
|
||||||
onClick={() => onGroupClick?.(viewGroup)}
|
onClick={() => onGroupClick?.(viewGroup)}
|
||||||
endIcon={viewGroup.id === activeGroup?.id ? CheckIcon : undefined}
|
endIcon={viewGroup.id === activeGroup?.id ? CheckIcon : undefined}
|
||||||
|
className={classes.option}
|
||||||
>
|
>
|
||||||
{viewGroup.label}
|
{viewGroup.label}
|
||||||
</MenuItemButton>
|
</MenuItemButton>
|
||||||
|
@ -0,0 +1,62 @@
|
|||||||
|
.CMS_NestedCollection_root-node {
|
||||||
|
&.CMS_NestedCollection_active {
|
||||||
|
&.CMS_NestedCollection_expanded {
|
||||||
|
& > .CMS_NestedCollection_link {
|
||||||
|
& .CMS_NestedCollection_node-children-icon {
|
||||||
|
@apply rotate-90
|
||||||
|
transform;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_NestedCollection_active {
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_NestedCollection_expanded {
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_NestedCollection_root-node-icon {
|
||||||
|
@apply h-6
|
||||||
|
w-6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_NestedCollection_node {
|
||||||
|
@apply ml-8;
|
||||||
|
|
||||||
|
&.CMS_NestedCollection_expanded {
|
||||||
|
& > .CMS_NestedCollection_link {
|
||||||
|
& .CMS_NestedCollection_node-children-icon {
|
||||||
|
@apply rotate-90
|
||||||
|
transform;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_NestedCollection_node-icon {
|
||||||
|
@apply h-5
|
||||||
|
w-5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_NestedCollection_node-content {
|
||||||
|
@apply flex
|
||||||
|
w-full
|
||||||
|
gap-2
|
||||||
|
items-center
|
||||||
|
justify-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_NestedCollection_node-children-icon {
|
||||||
|
@apply transition-transform
|
||||||
|
h-5
|
||||||
|
w-5
|
||||||
|
group-focus-within/active-list:text-blue-500
|
||||||
|
group-hover/active-list:text-blue-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_NestedCollection_node-children {
|
||||||
|
@apply mt-2
|
||||||
|
space-y-1.5;
|
||||||
|
}
|
@ -1,18 +1,35 @@
|
|||||||
import { Article as ArticleIcon } from '@styled-icons/material/Article';
|
import { Article as ArticleIcon } from '@styled-icons/material/Article';
|
||||||
import { ChevronRight as ChevronRightIcon } from '@styled-icons/material/ChevronRight';
|
import { ChevronRight as ChevronRightIcon } from '@styled-icons/material/ChevronRight';
|
||||||
import sortBy from 'lodash/sortBy';
|
import sortBy from 'lodash/sortBy';
|
||||||
import React, { Fragment, useCallback, useEffect, useState } from 'react';
|
import React, { Fragment, useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
import useEntries from '@staticcms/core/lib/hooks/useEntries';
|
import useEntries from '@staticcms/core/lib/hooks/useEntries';
|
||||||
import classNames from '@staticcms/core/lib/util/classNames.util';
|
import classNames from '@staticcms/core/lib/util/classNames.util';
|
||||||
import { getTreeData } from '@staticcms/core/lib/util/nested.util';
|
import { getTreeData } from '@staticcms/core/lib/util/nested.util';
|
||||||
|
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
|
||||||
import NavLink from '../navbar/NavLink';
|
import NavLink from '../navbar/NavLink';
|
||||||
|
|
||||||
import type { Collection, Entry } from '@staticcms/core/interface';
|
import type { Collection, Entry } from '@staticcms/core/interface';
|
||||||
import type { TreeNodeData } from '@staticcms/core/lib/util/nested.util';
|
import type { TreeNodeData } from '@staticcms/core/lib/util/nested.util';
|
||||||
import type { MouseEvent } from 'react';
|
import type { MouseEvent } from 'react';
|
||||||
|
|
||||||
|
import './NestedCollection.css';
|
||||||
|
|
||||||
|
export const classes = generateClassNames('NestedCollection', [
|
||||||
|
'root',
|
||||||
|
'active',
|
||||||
|
'expanded',
|
||||||
|
'root-node',
|
||||||
|
'root-node-icon',
|
||||||
|
'link',
|
||||||
|
'node',
|
||||||
|
'node-icon',
|
||||||
|
'node-content',
|
||||||
|
'node-children-icon',
|
||||||
|
'node-children',
|
||||||
|
]);
|
||||||
|
|
||||||
function getNodeTitle(node: TreeNodeData) {
|
function getNodeTitle(node: TreeNodeData) {
|
||||||
const title = node.isRoot
|
const title = node.isRoot
|
||||||
? node.title
|
? node.title
|
||||||
@ -23,28 +40,46 @@ function getNodeTitle(node: TreeNodeData) {
|
|||||||
interface TreeNodeProps {
|
interface TreeNodeProps {
|
||||||
collection: Collection;
|
collection: Collection;
|
||||||
treeData: TreeNodeData[];
|
treeData: TreeNodeData[];
|
||||||
|
rootIsActive: boolean;
|
||||||
|
path: string;
|
||||||
depth?: number;
|
depth?: number;
|
||||||
onToggle: ({ node, expanded }: { node: TreeNodeData; expanded: boolean }) => void;
|
onToggle: ({ node, expanded }: { node: TreeNodeData; expanded: boolean }) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TreeNode = ({ collection, treeData, depth = 0, onToggle }: TreeNodeProps) => {
|
const TreeNode = ({
|
||||||
|
collection,
|
||||||
|
treeData,
|
||||||
|
rootIsActive,
|
||||||
|
path,
|
||||||
|
depth = 0,
|
||||||
|
onToggle,
|
||||||
|
}: TreeNodeProps) => {
|
||||||
const collectionName = collection.name;
|
const collectionName = collection.name;
|
||||||
|
|
||||||
const handleClick = useCallback(
|
const handleClick = useCallback(
|
||||||
(event: MouseEvent | undefined, node: TreeNodeData, expanded: boolean) => {
|
(event: MouseEvent | undefined, node: TreeNodeData, expanded: boolean) => {
|
||||||
|
if (!rootIsActive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
event?.stopPropagation();
|
event?.stopPropagation();
|
||||||
event?.preventDefault();
|
event?.preventDefault();
|
||||||
|
|
||||||
if (event) {
|
if (event) {
|
||||||
onToggle({ node, expanded });
|
onToggle({ node, expanded });
|
||||||
} else {
|
} else {
|
||||||
onToggle({ node, expanded: true });
|
onToggle({ node, expanded: path === node.path ? expanded : true });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[onToggle],
|
[onToggle, path, rootIsActive],
|
||||||
);
|
);
|
||||||
|
|
||||||
const sortedData = sortBy(treeData, getNodeTitle);
|
const sortedData = sortBy(treeData, getNodeTitle);
|
||||||
|
|
||||||
|
if (depth !== 0 && !rootIsActive) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{sortedData.map(node => {
|
{sortedData.map(node => {
|
||||||
@ -62,36 +97,42 @@ const TreeNode = ({ collection, treeData, depth = 0, onToggle }: TreeNodeProps)
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment key={node.path}>
|
<Fragment key={node.path}>
|
||||||
<div className={classNames(depth !== 0 && 'ml-8')}>
|
<div
|
||||||
|
className={classNames(
|
||||||
|
depth === 0 ? classes['root-node'] : classes.node,
|
||||||
|
depth === 0 && rootIsActive && classes.active,
|
||||||
|
node.expanded && classes.expanded,
|
||||||
|
)}
|
||||||
|
>
|
||||||
<NavLink
|
<NavLink
|
||||||
to={to}
|
to={to}
|
||||||
onClick={() => handleClick(undefined, node, !node.expanded)}
|
onClick={() => handleClick(undefined, node, !node.expanded)}
|
||||||
data-testid={node.path}
|
data-testid={node.path}
|
||||||
icon={<ArticleIcon className={classNames(depth === 0 ? 'h-6 w-6' : 'h-5 w-5')} />}
|
className={classes.link}
|
||||||
|
icon={
|
||||||
|
<ArticleIcon
|
||||||
|
className={classNames(
|
||||||
|
depth === 0 ? classes['root-node-icon'] : classes['node-icon'],
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div className="flex w-full gap-2 items-center justify-between">
|
<div className={classes['node-content']}>
|
||||||
<div>{title}</div>
|
<div>{title}</div>
|
||||||
{hasChildren && (
|
{hasChildren && (
|
||||||
<ChevronRightIcon
|
<ChevronRightIcon
|
||||||
onClick={event => handleClick(event, node, !node.expanded)}
|
onClick={event => handleClick(event, node, !node.expanded)}
|
||||||
className={classNames(
|
className={classes['node-children-icon']}
|
||||||
node.expanded && 'rotate-90 transform',
|
|
||||||
`
|
|
||||||
transition-transform
|
|
||||||
h-5
|
|
||||||
w-5
|
|
||||||
group-focus-within/active-list:text-blue-500
|
|
||||||
group-hover/active-list:text-blue-500
|
|
||||||
`,
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
<div className="mt-2 space-y-1.5">
|
<div className={classes['node-children']}>
|
||||||
{node.expanded && (
|
{node.expanded && (
|
||||||
<TreeNode
|
<TreeNode
|
||||||
|
rootIsActive={rootIsActive}
|
||||||
collection={collection}
|
collection={collection}
|
||||||
|
path={path}
|
||||||
depth={depth + 1}
|
depth={depth + 1}
|
||||||
treeData={node.children}
|
treeData={node.children}
|
||||||
onToggle={onToggle}
|
onToggle={onToggle}
|
||||||
@ -153,30 +194,52 @@ const NestedCollection = ({ collection, filterTerm }: NestedCollectionProps) =>
|
|||||||
const [treeData, setTreeData] = useState<TreeNodeData[]>(getTreeData(collection, entries));
|
const [treeData, setTreeData] = useState<TreeNodeData[]>(getTreeData(collection, entries));
|
||||||
const [useFilter, setUseFilter] = useState(true);
|
const [useFilter, setUseFilter] = useState(true);
|
||||||
|
|
||||||
|
const [prevRootIsActive, setPrevRootIsActive] = useState(false);
|
||||||
const [prevCollection, setPrevCollection] = useState<Collection | null>(null);
|
const [prevCollection, setPrevCollection] = useState<Collection | null>(null);
|
||||||
const [prevEntries, setPrevEntries] = useState<Entry[] | null>(null);
|
const [prevEntries, setPrevEntries] = useState<Entry[] | null>(null);
|
||||||
const [prevFilterTerm, setPrevFilterTerm] = useState<string | null>(null);
|
const [prevPath, setPrevPath] = useState<string | null>(null);
|
||||||
|
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
|
|
||||||
|
const rootIsActive = useMemo(
|
||||||
|
() => pathname.startsWith(`/collections/${collection.name}`),
|
||||||
|
[collection.name, pathname],
|
||||||
|
);
|
||||||
|
|
||||||
|
const path = useMemo(() => `/${filterTerm}`, [filterTerm]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (collection !== prevCollection || entries !== prevEntries || filterTerm !== prevFilterTerm) {
|
if (
|
||||||
|
rootIsActive !== prevRootIsActive ||
|
||||||
|
collection !== prevCollection ||
|
||||||
|
entries !== prevEntries ||
|
||||||
|
path !== prevPath
|
||||||
|
) {
|
||||||
const expanded: Record<string, boolean> = {};
|
const expanded: Record<string, boolean> = {};
|
||||||
walk(treeData, node => {
|
walk(treeData, node => {
|
||||||
|
if (!rootIsActive) {
|
||||||
|
expanded[node.path] = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (node.expanded) {
|
if (node.expanded) {
|
||||||
expanded[node.path] = true;
|
expanded[node.path] = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const newTreeData = getTreeData(collection, entries);
|
const newTreeData = getTreeData(collection, entries);
|
||||||
|
|
||||||
const path = `/${filterTerm}`;
|
|
||||||
walk(newTreeData, node => {
|
walk(newTreeData, node => {
|
||||||
if (
|
if (!rootIsActive) {
|
||||||
expanded[node.path] ||
|
node.expanded = false;
|
||||||
(useFilter &&
|
return;
|
||||||
path.startsWith(node.path) &&
|
}
|
||||||
pathname.startsWith(`/collections/${collection.name}`))
|
|
||||||
) {
|
if (node.isRoot) {
|
||||||
|
node.expanded = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expanded[node.path] || (useFilter && path.startsWith(node.path))) {
|
||||||
node.expanded = true;
|
node.expanded = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -184,17 +247,21 @@ const NestedCollection = ({ collection, filterTerm }: NestedCollectionProps) =>
|
|||||||
setTreeData(newTreeData);
|
setTreeData(newTreeData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setPrevRootIsActive(rootIsActive);
|
||||||
setPrevCollection(collection);
|
setPrevCollection(collection);
|
||||||
setPrevEntries(entries);
|
setPrevEntries(entries);
|
||||||
setPrevFilterTerm(filterTerm);
|
setPrevPath(path);
|
||||||
}, [
|
}, [
|
||||||
collection,
|
collection,
|
||||||
entries,
|
entries,
|
||||||
filterTerm,
|
filterTerm,
|
||||||
|
path,
|
||||||
pathname,
|
pathname,
|
||||||
prevCollection,
|
prevCollection,
|
||||||
prevEntries,
|
prevEntries,
|
||||||
prevFilterTerm,
|
prevPath,
|
||||||
|
prevRootIsActive,
|
||||||
|
rootIsActive,
|
||||||
treeData,
|
treeData,
|
||||||
useFilter,
|
useFilter,
|
||||||
]);
|
]);
|
||||||
@ -212,7 +279,15 @@ const NestedCollection = ({ collection, filterTerm }: NestedCollectionProps) =>
|
|||||||
[treeData],
|
[treeData],
|
||||||
);
|
);
|
||||||
|
|
||||||
return <TreeNode collection={collection} treeData={treeData} onToggle={onToggle} />;
|
return (
|
||||||
|
<TreeNode
|
||||||
|
collection={collection}
|
||||||
|
treeData={treeData}
|
||||||
|
onToggle={onToggle}
|
||||||
|
rootIsActive={rootIsActive}
|
||||||
|
path={path}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default NestedCollection;
|
export default NestedCollection;
|
||||||
|
50
packages/core/src/components/collections/SortControl.css
Normal file
50
packages/core/src/components/collections/SortControl.css
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
.CMS_SortControl_root {
|
||||||
|
@apply hidden
|
||||||
|
lg:block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_SortControl_option {
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_SortControl_list {
|
||||||
|
@apply flex
|
||||||
|
flex-col
|
||||||
|
gap-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_SortControl_list-label {
|
||||||
|
@apply text-lg
|
||||||
|
font-bold
|
||||||
|
text-gray-800
|
||||||
|
dark:text-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_SortControl_list-option {
|
||||||
|
@apply ml-0.5
|
||||||
|
font-medium
|
||||||
|
flex
|
||||||
|
items-center
|
||||||
|
text-gray-800
|
||||||
|
dark:text-gray-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_SortControl_list-option-label {
|
||||||
|
@apply ml-2
|
||||||
|
text-base
|
||||||
|
font-medium
|
||||||
|
text-gray-800
|
||||||
|
dark:text-gray-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_SortControl_list-option-sorted-icon {
|
||||||
|
@apply ml-2
|
||||||
|
w-6
|
||||||
|
h-6
|
||||||
|
text-blue-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_SortControl_list-option-not-sorted {
|
||||||
|
@apply ml-2
|
||||||
|
w-6
|
||||||
|
h-6;
|
||||||
|
}
|
@ -8,6 +8,7 @@ import {
|
|||||||
SORT_DIRECTION_DESCENDING,
|
SORT_DIRECTION_DESCENDING,
|
||||||
SORT_DIRECTION_NONE,
|
SORT_DIRECTION_NONE,
|
||||||
} from '@staticcms/core/constants';
|
} from '@staticcms/core/constants';
|
||||||
|
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
|
||||||
import Menu from '../common/menu/Menu';
|
import Menu from '../common/menu/Menu';
|
||||||
import MenuGroup from '../common/menu/MenuGroup';
|
import MenuGroup from '../common/menu/MenuGroup';
|
||||||
import MenuItemButton from '../common/menu/MenuItemButton';
|
import MenuItemButton from '../common/menu/MenuItemButton';
|
||||||
@ -20,6 +21,19 @@ import type {
|
|||||||
} from '@staticcms/core/interface';
|
} from '@staticcms/core/interface';
|
||||||
import type { FC, MouseEvent } from 'react';
|
import type { FC, MouseEvent } from 'react';
|
||||||
|
|
||||||
|
import './SortControl.css';
|
||||||
|
|
||||||
|
export const classes = generateClassNames('SortControl', [
|
||||||
|
'root',
|
||||||
|
'option',
|
||||||
|
'list',
|
||||||
|
'list-label',
|
||||||
|
'list-option',
|
||||||
|
'list-option-label',
|
||||||
|
'list-option-sorted-icon',
|
||||||
|
'list-option-not-sorted',
|
||||||
|
]);
|
||||||
|
|
||||||
function nextSortDirection(direction: SortDirection) {
|
function nextSortDirection(direction: SortDirection) {
|
||||||
switch (direction) {
|
switch (direction) {
|
||||||
case SORT_DIRECTION_ASCENDING:
|
case SORT_DIRECTION_ASCENDING:
|
||||||
@ -69,44 +83,32 @@ const SortControl = ({
|
|||||||
|
|
||||||
if (variant === 'list') {
|
if (variant === 'list') {
|
||||||
return (
|
return (
|
||||||
<div key="filter-by-list" className="flex flex-col gap-2">
|
<div key="filter-by-list" className={classes.list}>
|
||||||
<h3
|
<h3 className={classes['list-label']}>{t('collection.collectionTop.sortBy')}</h3>
|
||||||
className="
|
|
||||||
text-lg
|
|
||||||
font-bold
|
|
||||||
text-gray-800
|
|
||||||
dark:text-white
|
|
||||||
"
|
|
||||||
>
|
|
||||||
{t('collection.collectionTop.sortBy')}
|
|
||||||
</h3>
|
|
||||||
{fields.map(field => {
|
{fields.map(field => {
|
||||||
const sortDir = sort?.[field.name]?.direction ?? SORT_DIRECTION_NONE;
|
const sortDir = sort?.[field.name]?.direction ?? SORT_DIRECTION_NONE;
|
||||||
const nextSortDir = nextSortDirection(sortDir);
|
const nextSortDir = nextSortDirection(sortDir);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={field.name}
|
key={field.name}
|
||||||
className="
|
className={classes['list-option']}
|
||||||
ml-0.5
|
|
||||||
font-medium
|
|
||||||
flex
|
|
||||||
items-center
|
|
||||||
text-gray-800
|
|
||||||
dark:text-gray-300
|
|
||||||
"
|
|
||||||
onClick={handleSortClick(field.name, nextSortDir)}
|
onClick={handleSortClick(field.name, nextSortDir)}
|
||||||
>
|
>
|
||||||
<label className="ml-2 text-md font-medium text-gray-800 dark:text-gray-300">
|
<label className={classes['list-option-label']}>{field.label ?? field.name}</label>
|
||||||
{field.label ?? field.name}
|
|
||||||
</label>
|
|
||||||
{field.name === selectedSort.key ? (
|
{field.name === selectedSort.key ? (
|
||||||
selectedSort.direction === SORT_DIRECTION_ASCENDING ? (
|
selectedSort.direction === SORT_DIRECTION_ASCENDING ? (
|
||||||
<KeyboardArrowUpIcon key="checkmark" className="ml-2 w-6 h-6 text-blue-500" />
|
<KeyboardArrowUpIcon
|
||||||
|
key="checkmark"
|
||||||
|
className={classes['list-option-sorted-icon']}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<KeyboardArrowDownIcon key="checkmark" className="ml-2 w-6 h-6 text-blue-500" />
|
<KeyboardArrowDownIcon
|
||||||
|
key="checkmark"
|
||||||
|
className={classes['list-option-sorted-icon']}
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<div key="not-checked" className="ml-2 w-6 h-6" />
|
<div key="not-checked" className={classes['list-option-not-sorted']} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -119,7 +121,7 @@ const SortControl = ({
|
|||||||
<Menu
|
<Menu
|
||||||
label={t('collection.collectionTop.sortBy')}
|
label={t('collection.collectionTop.sortBy')}
|
||||||
variant={selectedSort.key ? 'contained' : 'outlined'}
|
variant={selectedSort.key ? 'contained' : 'outlined'}
|
||||||
rootClassName="hidden lg:block"
|
rootClassName={classes.root}
|
||||||
>
|
>
|
||||||
<MenuGroup>
|
<MenuGroup>
|
||||||
{fields.map(field => {
|
{fields.map(field => {
|
||||||
@ -137,6 +139,7 @@ const SortControl = ({
|
|||||||
: KeyboardArrowDownIcon
|
: KeyboardArrowDownIcon
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
|
className={classes.option}
|
||||||
>
|
>
|
||||||
{field.label ?? field.name}
|
{field.label ?? field.name}
|
||||||
</MenuItemButton>
|
</MenuItemButton>
|
||||||
|
@ -0,0 +1,22 @@
|
|||||||
|
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
|
||||||
|
|
||||||
|
const entriesClasses = generateClassNames('Entries', [
|
||||||
|
'root',
|
||||||
|
'group',
|
||||||
|
'group-content-wrapper',
|
||||||
|
'group-content',
|
||||||
|
'group-button',
|
||||||
|
'entry-listing',
|
||||||
|
'entry-listing-loading',
|
||||||
|
'entry-listing-grid',
|
||||||
|
'entry-listing-grid-container',
|
||||||
|
'entry-listing-cards',
|
||||||
|
'entry-listing-cards-grid-wrapper',
|
||||||
|
'entry-listing-cards-grid',
|
||||||
|
'entry-listing-table',
|
||||||
|
'entry-listing-table-content',
|
||||||
|
'entry-listing-table-row',
|
||||||
|
'entry-listing-local-backup',
|
||||||
|
]);
|
||||||
|
|
||||||
|
export default entriesClasses;
|
104
packages/core/src/components/collections/entries/Entries.css
Normal file
104
packages/core/src/components/collections/entries/Entries.css
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
.CMS_Entries_root {
|
||||||
|
@apply py-2
|
||||||
|
px-3
|
||||||
|
rounded-md
|
||||||
|
bg-yellow-300/75
|
||||||
|
dark:bg-yellow-800/75
|
||||||
|
text-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Entries_group {
|
||||||
|
@apply pb-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Entries_group-content-wrapper {
|
||||||
|
@apply -m-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Entries_group-content {
|
||||||
|
@apply flex
|
||||||
|
gap-2
|
||||||
|
p-1
|
||||||
|
overflow-x-auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Entries_group-button {
|
||||||
|
@apply whitespace-nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Entries_entry-listing {
|
||||||
|
@apply pb-3
|
||||||
|
overflow-hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Entries_entry-listing-loading {
|
||||||
|
@apply absolute
|
||||||
|
inset-0
|
||||||
|
flex
|
||||||
|
items-center
|
||||||
|
justify-center
|
||||||
|
bg-slate-50/50
|
||||||
|
dark:bg-slate-900/50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Entries_entry-listing-grid {
|
||||||
|
@apply relative
|
||||||
|
h-full
|
||||||
|
flex-grow;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Entries_entry-listing-grid-container {
|
||||||
|
@apply relative
|
||||||
|
h-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Entries_entry-listing-cards {
|
||||||
|
@apply relative
|
||||||
|
w-card-grid
|
||||||
|
h-full
|
||||||
|
overflow-hidden
|
||||||
|
-ml-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Entries_entry-listing-cards-grid-wrapper {
|
||||||
|
@apply overflow-hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Entries_entry-listing-cards-grid {
|
||||||
|
@apply !overflow-x-hidden
|
||||||
|
overflow-y-auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Entries_entry-listing-table {
|
||||||
|
@apply relative
|
||||||
|
max-h-full
|
||||||
|
h-full
|
||||||
|
overflow-hidden
|
||||||
|
p-1.5
|
||||||
|
bg-white
|
||||||
|
shadow-sm
|
||||||
|
border
|
||||||
|
border-gray-100
|
||||||
|
dark:bg-slate-800
|
||||||
|
dark:border-gray-700/40
|
||||||
|
dark:shadow-md
|
||||||
|
rounded-xl;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Entries_entry-listing-table-content {
|
||||||
|
@apply relative
|
||||||
|
h-full
|
||||||
|
overflow-auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Entries_entry-listing-table-row {
|
||||||
|
@apply hover:bg-gray-200
|
||||||
|
dark:hover:bg-slate-700/70;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Entries_entry-listing-local-backup {
|
||||||
|
@apply w-5
|
||||||
|
h-5
|
||||||
|
text-blue-600
|
||||||
|
dark:text-blue-300;
|
||||||
|
}
|
@ -2,12 +2,15 @@ import React, { useMemo } from 'react';
|
|||||||
import { translate } from 'react-polyglot';
|
import { translate } from 'react-polyglot';
|
||||||
|
|
||||||
import Loader from '@staticcms/core/components/common/progress/Loader';
|
import Loader from '@staticcms/core/components/common/progress/Loader';
|
||||||
|
import entriesClasses from './Entries.classes';
|
||||||
import EntryListing from './EntryListing';
|
import EntryListing from './EntryListing';
|
||||||
|
|
||||||
import type { ViewStyle } from '@staticcms/core/constants/views';
|
import type { ViewStyle } from '@staticcms/core/constants/views';
|
||||||
import type { Collection, Collections, Entry, TranslatedProps } from '@staticcms/core/interface';
|
import type { Collection, Collections, Entry, TranslatedProps } from '@staticcms/core/interface';
|
||||||
import type Cursor from '@staticcms/core/lib/util/Cursor';
|
import type Cursor from '@staticcms/core/lib/util/Cursor';
|
||||||
|
|
||||||
|
import './Entries.css';
|
||||||
|
|
||||||
export interface BaseEntriesProps {
|
export interface BaseEntriesProps {
|
||||||
entries: Entry[];
|
entries: Entry[];
|
||||||
page?: number;
|
page?: number;
|
||||||
@ -81,20 +84,7 @@ const Entries = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return <div className={entriesClasses.root}>{t('collection.entries.noEntries')}</div>;
|
||||||
<div
|
|
||||||
className="
|
|
||||||
py-2
|
|
||||||
px-3
|
|
||||||
rounded-md
|
|
||||||
bg-yellow-300/75
|
|
||||||
dark:bg-yellow-800/75
|
|
||||||
text-sm
|
|
||||||
"
|
|
||||||
>
|
|
||||||
{t('collection.entries.noEntries')}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default translate()(Entries);
|
export default translate()(Entries);
|
||||||
|
@ -6,11 +6,13 @@ import { loadEntries, traverseCollectionCursor } from '@staticcms/core/actions/e
|
|||||||
import useEntries from '@staticcms/core/lib/hooks/useEntries';
|
import useEntries from '@staticcms/core/lib/hooks/useEntries';
|
||||||
import useGroups from '@staticcms/core/lib/hooks/useGroups';
|
import useGroups from '@staticcms/core/lib/hooks/useGroups';
|
||||||
import { Cursor } from '@staticcms/core/lib/util';
|
import { Cursor } from '@staticcms/core/lib/util';
|
||||||
|
import classNames from '@staticcms/core/lib/util/classNames.util';
|
||||||
import { selectCollectionEntriesCursor } from '@staticcms/core/reducers/selectors/cursors';
|
import { selectCollectionEntriesCursor } from '@staticcms/core/reducers/selectors/cursors';
|
||||||
import { selectEntriesLoaded, selectIsFetching } from '@staticcms/core/reducers/selectors/entries';
|
import { selectEntriesLoaded, selectIsFetching } from '@staticcms/core/reducers/selectors/entries';
|
||||||
import { useAppDispatch } from '@staticcms/core/store/hooks';
|
import { useAppDispatch } from '@staticcms/core/store/hooks';
|
||||||
import Button from '../../common/button/Button';
|
import Button from '../../common/button/Button';
|
||||||
import Entries from './Entries';
|
import Entries from './Entries';
|
||||||
|
import entriesClasses from './Entries.classes';
|
||||||
|
|
||||||
import type { ViewStyle } from '@staticcms/core/constants/views';
|
import type { ViewStyle } from '@staticcms/core/constants/views';
|
||||||
import type { Collection, Entry, GroupOfEntries, TranslatedProps } from '@staticcms/core/interface';
|
import type { Collection, Entry, GroupOfEntries, TranslatedProps } from '@staticcms/core/interface';
|
||||||
@ -115,25 +117,9 @@ const EntriesCollection = ({
|
|||||||
if (groups && groups.length > 0) {
|
if (groups && groups.length > 0) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div className={entriesClasses.group}>
|
||||||
className="
|
<div className={entriesClasses['group-content-wrapper']}>
|
||||||
pb-3
|
<div className={classNames(entriesClasses['group-content'], 'CMS_Scrollbar_hide')}>
|
||||||
"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="
|
|
||||||
-m-1
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="
|
|
||||||
flex
|
|
||||||
gap-2
|
|
||||||
p-1
|
|
||||||
overflow-x-auto
|
|
||||||
hide-scrollbar
|
|
||||||
"
|
|
||||||
>
|
|
||||||
{groups.map((group, index) => {
|
{groups.map((group, index) => {
|
||||||
const title = getGroupTitle(group, t);
|
const title = getGroupTitle(group, t);
|
||||||
return (
|
return (
|
||||||
@ -141,7 +127,7 @@ const EntriesCollection = ({
|
|||||||
key={index}
|
key={index}
|
||||||
variant={index === selectedGroup ? 'contained' : 'text'}
|
variant={index === selectedGroup ? 'contained' : 'text'}
|
||||||
onClick={handleGroupClick(index)}
|
onClick={handleGroupClick(index)}
|
||||||
className="whitespace-nowrap"
|
className={entriesClasses['group-button']}
|
||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -0,0 +1,40 @@
|
|||||||
|
.CMS_EntryCard_root {
|
||||||
|
@apply h-full
|
||||||
|
w-full
|
||||||
|
relative
|
||||||
|
overflow-visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_EntryCard_content-wrapper {
|
||||||
|
@apply absolute
|
||||||
|
-inset-1
|
||||||
|
pr-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_EntryCard_content {
|
||||||
|
@apply p-1
|
||||||
|
h-full
|
||||||
|
w-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_EntryCard_card {
|
||||||
|
@apply h-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_EntryCard_card-content {
|
||||||
|
@apply flex
|
||||||
|
w-full
|
||||||
|
items-center
|
||||||
|
justify-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_EntryCard_card-summary {
|
||||||
|
@apply truncate;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_EntryCard_local-backup-icon {
|
||||||
|
@apply w-5
|
||||||
|
h-5
|
||||||
|
text-blue-600
|
||||||
|
dark:text-blue-300;
|
||||||
|
}
|
@ -9,6 +9,7 @@ import {
|
|||||||
selectTemplateName,
|
selectTemplateName,
|
||||||
} from '@staticcms/core/lib/util/collection.util';
|
} from '@staticcms/core/lib/util/collection.util';
|
||||||
import localForage from '@staticcms/core/lib/util/localForage';
|
import localForage from '@staticcms/core/lib/util/localForage';
|
||||||
|
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
|
||||||
import { selectConfig } from '@staticcms/core/reducers/selectors/config';
|
import { selectConfig } from '@staticcms/core/reducers/selectors/config';
|
||||||
import { selectTheme } from '@staticcms/core/reducers/selectors/globalUI';
|
import { selectTheme } from '@staticcms/core/reducers/selectors/globalUI';
|
||||||
import { useAppSelector } from '@staticcms/core/store/hooks';
|
import { useAppSelector } from '@staticcms/core/store/hooks';
|
||||||
@ -27,6 +28,18 @@ import type {
|
|||||||
} from '@staticcms/core/interface';
|
} from '@staticcms/core/interface';
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
|
|
||||||
|
import './EntryCard.css';
|
||||||
|
|
||||||
|
export const classes = generateClassNames('EntryCard', [
|
||||||
|
'root',
|
||||||
|
'content-wrapper',
|
||||||
|
'content',
|
||||||
|
'card',
|
||||||
|
'card-content',
|
||||||
|
'card-summary',
|
||||||
|
'local-backup-icon',
|
||||||
|
]);
|
||||||
|
|
||||||
export interface EntryCardProps {
|
export interface EntryCardProps {
|
||||||
entry: Entry;
|
entry: Entry;
|
||||||
imageFieldName?: string | null | undefined;
|
imageFieldName?: string | null | undefined;
|
||||||
@ -113,9 +126,9 @@ const EntryCard: FC<TranslatedProps<EntryCardProps>> = ({
|
|||||||
|
|
||||||
if (PreviewCardComponent) {
|
if (PreviewCardComponent) {
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full relative overflow-visible">
|
<div className={classes.root}>
|
||||||
<div className="absolute -inset-1 pr-2">
|
<div className={classes['content-wrapper']}>
|
||||||
<div className="p-1 h-full w-full">
|
<div className={classes.content}>
|
||||||
<Card>
|
<Card>
|
||||||
<CardActionArea to={path}>
|
<CardActionArea to={path}>
|
||||||
<PreviewCardComponent
|
<PreviewCardComponent
|
||||||
@ -136,10 +149,10 @@ const EntryCard: FC<TranslatedProps<EntryCardProps>> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full relative overflow-visible">
|
<div className={classes.root}>
|
||||||
<div className="absolute -inset-1 pr-2">
|
<div className={classes['content-wrapper']}>
|
||||||
<div className="p-1 h-full w-full">
|
<div className={classes.content}>
|
||||||
<Card className="h-full" title={summary}>
|
<Card className={classes.card} title={summary}>
|
||||||
<CardActionArea to={path}>
|
<CardActionArea to={path}>
|
||||||
{image && imageField ? (
|
{image && imageField ? (
|
||||||
<CardMedia
|
<CardMedia
|
||||||
@ -151,16 +164,11 @@ const EntryCard: FC<TranslatedProps<EntryCardProps>> = ({
|
|||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="flex w-full items-center justify-between">
|
<div className={classes['card-content']}>
|
||||||
<div className="truncate">{summary}</div>
|
<div className={classes['card-summary']}>{summary}</div>
|
||||||
{hasLocalBackup ? (
|
{hasLocalBackup ? (
|
||||||
<InfoIcon
|
<InfoIcon
|
||||||
className="
|
className={classes['local-backup-icon']}
|
||||||
w-5
|
|
||||||
h-5
|
|
||||||
text-blue-600
|
|
||||||
dark:text-blue-300
|
|
||||||
"
|
|
||||||
title={t('ui.localBackup.hasLocalBackup')}
|
title={t('ui.localBackup.hasLocalBackup')}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
@ -4,6 +4,7 @@ import { translate } from 'react-polyglot';
|
|||||||
import { VIEW_STYLE_TABLE } from '@staticcms/core/constants/views';
|
import { VIEW_STYLE_TABLE } from '@staticcms/core/constants/views';
|
||||||
import { selectFields, selectInferredField } from '@staticcms/core/lib/util/collection.util';
|
import { selectFields, selectInferredField } from '@staticcms/core/lib/util/collection.util';
|
||||||
import { toTitleCaseFromKey } from '@staticcms/core/lib/util/string.util';
|
import { toTitleCaseFromKey } from '@staticcms/core/lib/util/string.util';
|
||||||
|
import entriesClasses from './Entries.classes';
|
||||||
import EntryListingGrid from './EntryListingGrid';
|
import EntryListingGrid from './EntryListingGrid';
|
||||||
import EntryListingTable from './EntryListingTable';
|
import EntryListingTable from './EntryListingTable';
|
||||||
|
|
||||||
@ -154,7 +155,7 @@ const EntryListing: FC<TranslatedProps<EntryListingProps>> = ({
|
|||||||
|
|
||||||
if (viewStyle === VIEW_STYLE_TABLE) {
|
if (viewStyle === VIEW_STYLE_TABLE) {
|
||||||
return (
|
return (
|
||||||
<div className="pb-3 overflow-hidden">
|
<div className={entriesClasses['entry-listing']}>
|
||||||
<EntryListingTable
|
<EntryListingTable
|
||||||
key="table"
|
key="table"
|
||||||
entryData={entryData}
|
entryData={entryData}
|
||||||
|
@ -12,6 +12,7 @@ import { getPreviewCard } from '@staticcms/core/lib/registry';
|
|||||||
import classNames from '@staticcms/core/lib/util/classNames.util';
|
import classNames from '@staticcms/core/lib/util/classNames.util';
|
||||||
import { selectTemplateName } from '@staticcms/core/lib/util/collection.util';
|
import { selectTemplateName } from '@staticcms/core/lib/util/collection.util';
|
||||||
import { isNotNullish } from '@staticcms/core/lib/util/null.util';
|
import { isNotNullish } from '@staticcms/core/lib/util/null.util';
|
||||||
|
import entriesClasses from './Entries.classes';
|
||||||
import EntryCard from './EntryCard';
|
import EntryCard from './EntryCard';
|
||||||
|
|
||||||
import type { CollectionEntryData } from '@staticcms/core/interface';
|
import type { CollectionEntryData } from '@staticcms/core/interface';
|
||||||
@ -138,15 +139,7 @@ const EntryListingCardGrid: FC<EntryListingCardGridProps> = ({
|
|||||||
}, [cardHeights, prevCardHeights.length]);
|
}, [cardHeights, prevCardHeights.length]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={entriesClasses['entry-listing-cards']}>
|
||||||
className="
|
|
||||||
relative
|
|
||||||
w-card-grid
|
|
||||||
h-full
|
|
||||||
overflow-hidden
|
|
||||||
-ml-1
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<AutoSizer onResize={handleResize}>
|
<AutoSizer onResize={handleResize}>
|
||||||
{({ height = 0, width = 0 }) => {
|
{({ height = 0, width = 0 }) => {
|
||||||
const calculatedWidth = width - 4;
|
const calculatedWidth = width - 4;
|
||||||
@ -161,11 +154,7 @@ const EntryListingCardGrid: FC<EntryListingCardGridProps> = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={version}
|
key={version}
|
||||||
className={classNames(
|
className={entriesClasses['entry-listing-cards-grid-wrapper']}
|
||||||
`
|
|
||||||
overflow-hidden
|
|
||||||
`,
|
|
||||||
)}
|
|
||||||
style={{
|
style={{
|
||||||
width: calculatedWidth,
|
width: calculatedWidth,
|
||||||
height,
|
height,
|
||||||
@ -212,11 +201,8 @@ const EntryListingCardGrid: FC<EntryListingCardGridProps> = ({
|
|||||||
outerRef={scrollContainerRef}
|
outerRef={scrollContainerRef}
|
||||||
onScroll={onScroll}
|
onScroll={onScroll}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
`
|
entriesClasses['entry-listing-cards-grid'],
|
||||||
!overflow-x-hidden
|
'CMS_Scrollbar_root',
|
||||||
overflow-y-auto
|
|
||||||
styled-scrollbars
|
|
||||||
`,
|
|
||||||
)}
|
)}
|
||||||
style={{ position: 'unset' }}
|
style={{ position: 'unset' }}
|
||||||
overscanRowCount={5}
|
overscanRowCount={5}
|
||||||
|
@ -3,6 +3,7 @@ import React, { useCallback, useEffect, useRef } from 'react';
|
|||||||
import { isNotNullish } from '@staticcms/core/lib/util/null.util';
|
import { isNotNullish } from '@staticcms/core/lib/util/null.util';
|
||||||
import { selectIsFetching } from '@staticcms/core/reducers/selectors/globalUI';
|
import { selectIsFetching } from '@staticcms/core/reducers/selectors/globalUI';
|
||||||
import { useAppSelector } from '@staticcms/core/store/hooks';
|
import { useAppSelector } from '@staticcms/core/store/hooks';
|
||||||
|
import entriesClasses from './Entries.classes';
|
||||||
import EntryListingCardGrid from './EntryListingCardGrid';
|
import EntryListingCardGrid from './EntryListingCardGrid';
|
||||||
|
|
||||||
import type { CollectionEntryData } from '@staticcms/core/interface';
|
import type { CollectionEntryData } from '@staticcms/core/interface';
|
||||||
@ -57,8 +58,8 @@ const EntryListingGrid: FC<EntryListingGridProps> = ({
|
|||||||
}, [handleScroll]);
|
}, [handleScroll]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative h-full flex-grow">
|
<div className={entriesClasses['entry-listing-grid']}>
|
||||||
<div ref={gridContainerRef} className="relative h-full">
|
<div ref={gridContainerRef} className={entriesClasses['entry-listing-grid-container']}>
|
||||||
<EntryListingCardGrid
|
<EntryListingCardGrid
|
||||||
key="grid"
|
key="grid"
|
||||||
entryData={entryData}
|
entryData={entryData}
|
||||||
@ -68,18 +69,7 @@ const EntryListingGrid: FC<EntryListingGridProps> = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{isLoadingEntries ? (
|
{isLoadingEntries ? (
|
||||||
<div
|
<div key="loading" className={entriesClasses['entry-listing-loading']}>
|
||||||
key="loading"
|
|
||||||
className="
|
|
||||||
absolute
|
|
||||||
inset-0
|
|
||||||
flex
|
|
||||||
items-center
|
|
||||||
justify-center
|
|
||||||
bg-slate-50/50
|
|
||||||
dark:bg-slate-900/50
|
|
||||||
"
|
|
||||||
>
|
|
||||||
{t('collection.entries.loadingEntries')}
|
{t('collection.entries.loadingEntries')}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||||
import { useVirtual } from 'react-virtual';
|
import { useVirtual } from 'react-virtual';
|
||||||
|
|
||||||
|
import classNames from '@staticcms/core/lib/util/classNames.util';
|
||||||
import { isNotNullish } from '@staticcms/core/lib/util/null.util';
|
import { isNotNullish } from '@staticcms/core/lib/util/null.util';
|
||||||
import { selectIsFetching } from '@staticcms/core/reducers/selectors/globalUI';
|
import { selectIsFetching } from '@staticcms/core/reducers/selectors/globalUI';
|
||||||
import { useAppSelector } from '@staticcms/core/store/hooks';
|
import { useAppSelector } from '@staticcms/core/store/hooks';
|
||||||
import Table from '../../common/table/Table';
|
import Table from '../../common/table/Table';
|
||||||
|
import entriesClasses from './Entries.classes';
|
||||||
import EntryRow from './EntryRow';
|
import EntryRow from './EntryRow';
|
||||||
|
|
||||||
import type { CollectionEntryData } from '@staticcms/core/interface';
|
import type { CollectionEntryData } from '@staticcms/core/interface';
|
||||||
@ -69,32 +71,14 @@ const EntryListingTable: FC<EntryListingTableProps> = ({
|
|||||||
}, [clientHeight, fetchMoreOnBottomReached, scrollHeight, scrollTop]);
|
}, [clientHeight, fetchMoreOnBottomReached, scrollHeight, scrollTop]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={entriesClasses['entry-listing-table']}>
|
||||||
className="
|
|
||||||
relative
|
|
||||||
max-h-full
|
|
||||||
h-full
|
|
||||||
overflow-hidden
|
|
||||||
p-1.5
|
|
||||||
bg-white
|
|
||||||
shadow-sm
|
|
||||||
border
|
|
||||||
border-gray-100
|
|
||||||
dark:bg-slate-800
|
|
||||||
dark:border-gray-700/40
|
|
||||||
dark:shadow-md
|
|
||||||
rounded-xl
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
ref={tableContainerRef}
|
ref={tableContainerRef}
|
||||||
className="
|
className={classNames(
|
||||||
relative
|
entriesClasses['entry-listing-table-content'],
|
||||||
h-full
|
'CMS_Scrollbar_root',
|
||||||
overflow-auto
|
'CMS_Scrollbar_secondary',
|
||||||
styled-scrollbars
|
)}
|
||||||
styled-scrollbars-secondary
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
<Table
|
<Table
|
||||||
columns={
|
columns={
|
||||||
@ -128,18 +112,7 @@ const EntryListingTable: FC<EntryListingTableProps> = ({
|
|||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
{isLoadingEntries ? (
|
{isLoadingEntries ? (
|
||||||
<div
|
<div key="loading" className={entriesClasses['entry-listing-loading']}>
|
||||||
key="loading"
|
|
||||||
className="
|
|
||||||
absolute
|
|
||||||
inset-0
|
|
||||||
flex
|
|
||||||
items-center
|
|
||||||
justify-center
|
|
||||||
bg-slate-50/50
|
|
||||||
dark:bg-slate-900/50
|
|
||||||
"
|
|
||||||
>
|
|
||||||
{t('collection.entries.loadingEntries')}
|
{t('collection.entries.loadingEntries')}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
@ -15,6 +15,7 @@ import { selectTheme } from '@staticcms/core/reducers/selectors/globalUI';
|
|||||||
import { useAppSelector } from '@staticcms/core/store/hooks';
|
import { useAppSelector } from '@staticcms/core/store/hooks';
|
||||||
import TableCell from '../../common/table/TableCell';
|
import TableCell from '../../common/table/TableCell';
|
||||||
import TableRow from '../../common/table/TableRow';
|
import TableRow from '../../common/table/TableRow';
|
||||||
|
import entriesClasses from './Entries.classes';
|
||||||
|
|
||||||
import type { BackupEntry, Collection, Entry, TranslatedProps } from '@staticcms/core/interface';
|
import type { BackupEntry, Collection, Entry, TranslatedProps } from '@staticcms/core/interface';
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
@ -75,13 +76,7 @@ const EntryRow: FC<TranslatedProps<EntryRowProps>> = ({
|
|||||||
}, [collection.name, entry.slug]);
|
}, [collection.name, entry.slug]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRow
|
<TableRow className={entriesClasses['entry-listing-table-row']} to={path}>
|
||||||
className="
|
|
||||||
hover:bg-gray-200
|
|
||||||
dark:hover:bg-slate-700/70
|
|
||||||
"
|
|
||||||
to={path}
|
|
||||||
>
|
|
||||||
{collectionLabel ? (
|
{collectionLabel ? (
|
||||||
<TableCell key="collectionLabel" to={path}>
|
<TableCell key="collectionLabel" to={path}>
|
||||||
{collectionLabel}
|
{collectionLabel}
|
||||||
@ -120,12 +115,7 @@ const EntryRow: FC<TranslatedProps<EntryRowProps>> = ({
|
|||||||
<TableCell key="unsavedChanges" to={path} shrink>
|
<TableCell key="unsavedChanges" to={path} shrink>
|
||||||
{hasLocalBackup ? (
|
{hasLocalBackup ? (
|
||||||
<InfoIcon
|
<InfoIcon
|
||||||
className="
|
className={entriesClasses['entry-listing-local-backup']}
|
||||||
w-5
|
|
||||||
h-5
|
|
||||||
text-blue-600
|
|
||||||
dark:text-blue-300
|
|
||||||
"
|
|
||||||
title={t('ui.localBackup.hasLocalBackup')}
|
title={t('ui.localBackup.hasLocalBackup')}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
@ -0,0 +1,10 @@
|
|||||||
|
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
|
||||||
|
|
||||||
|
const mobileCollectionControlsClasses = generateClassNames('MobileCollectionControls', [
|
||||||
|
'root',
|
||||||
|
'content',
|
||||||
|
'toggle',
|
||||||
|
'toggle-icon',
|
||||||
|
]);
|
||||||
|
|
||||||
|
export default mobileCollectionControlsClasses;
|
@ -0,0 +1,37 @@
|
|||||||
|
.CMS_MobileCollectionControls_root {
|
||||||
|
@apply w-[80%]
|
||||||
|
max-w-[240px];
|
||||||
|
|
||||||
|
& .MuiBackdrop-root {
|
||||||
|
@apply w-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .MuiDrawer-paper {
|
||||||
|
@apply box-border
|
||||||
|
w-[80%]
|
||||||
|
max-w-[240px];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_MobileCollectionControls_content {
|
||||||
|
@apply px-5
|
||||||
|
py-4
|
||||||
|
flex
|
||||||
|
flex-col
|
||||||
|
gap-6
|
||||||
|
h-full
|
||||||
|
w-full
|
||||||
|
overflow-y-auto
|
||||||
|
bg-white
|
||||||
|
dark:bg-slate-800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_MobileCollectionControls_toggle {
|
||||||
|
@apply flex
|
||||||
|
lg:!hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_MobileCollectionControls_toggle-icon {
|
||||||
|
@apply w-5
|
||||||
|
h-5;
|
||||||
|
}
|
@ -2,6 +2,7 @@ import { FilterList as FilterListIcon } from '@styled-icons/material/FilterList'
|
|||||||
import React, { useCallback, useState } from 'react';
|
import React, { useCallback, useState } from 'react';
|
||||||
|
|
||||||
import IconButton from '../../common/button/IconButton';
|
import IconButton from '../../common/button/IconButton';
|
||||||
|
import mobileCollectionControlsClasses from './MobileCollectionControls.classes';
|
||||||
import MobileCollectionControlsDrawer from './MobileCollectionControlsDrawer';
|
import MobileCollectionControlsDrawer from './MobileCollectionControlsDrawer';
|
||||||
|
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
@ -9,6 +10,8 @@ import type { FilterControlProps } from '../FilterControl';
|
|||||||
import type { GroupControlProps } from '../GroupControl';
|
import type { GroupControlProps } from '../GroupControl';
|
||||||
import type { SortControlProps } from '../SortControl';
|
import type { SortControlProps } from '../SortControl';
|
||||||
|
|
||||||
|
import './MobileCollectionControls.css';
|
||||||
|
|
||||||
export type MobileCollectionControlsProps = Omit<FilterControlProps, 'variant'> &
|
export type MobileCollectionControlsProps = Omit<FilterControlProps, 'variant'> &
|
||||||
Omit<GroupControlProps, 'variant'> &
|
Omit<GroupControlProps, 'variant'> &
|
||||||
Omit<SortControlProps, 'variant'> & {
|
Omit<SortControlProps, 'variant'> & {
|
||||||
@ -25,8 +28,12 @@ const MobileCollectionControls: FC<MobileCollectionControlsProps> = props => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<IconButton className="flex lg:hidden" variant="text" onClick={toggleMobileMenu}>
|
<IconButton
|
||||||
<FilterListIcon className="w-5 h-5" />
|
className={mobileCollectionControlsClasses.toggle}
|
||||||
|
variant="text"
|
||||||
|
onClick={toggleMobileMenu}
|
||||||
|
>
|
||||||
|
<FilterListIcon className={mobileCollectionControlsClasses['toggle-icon']} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<MobileCollectionControlsDrawer
|
<MobileCollectionControlsDrawer
|
||||||
{...props}
|
{...props}
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
import Drawer from '@mui/material/Drawer';
|
import Drawer from '@mui/material/Drawer';
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
|
|
||||||
|
import classNames from '@staticcms/core/lib/util/classNames.util';
|
||||||
import FilterControl from '../FilterControl';
|
import FilterControl from '../FilterControl';
|
||||||
import GroupControl from '../GroupControl';
|
import GroupControl from '../GroupControl';
|
||||||
import SortControl from '../SortControl';
|
import SortControl from '../SortControl';
|
||||||
|
import mobileCollectionControlsClasses from './MobileCollectionControls.classes';
|
||||||
|
|
||||||
import type { FilterControlProps } from '../FilterControl';
|
import type { FilterControlProps } from '../FilterControl';
|
||||||
import type { GroupControlProps } from '../GroupControl';
|
import type { GroupControlProps } from '../GroupControl';
|
||||||
import type { SortControlProps } from '../SortControl';
|
import type { SortControlProps } from '../SortControl';
|
||||||
|
|
||||||
const DRAWER_WIDTH = 240;
|
|
||||||
|
|
||||||
export type MobileCollectionControlsDrawerProps = Omit<FilterControlProps, 'variant'> &
|
export type MobileCollectionControlsDrawerProps = Omit<FilterControlProps, 'variant'> &
|
||||||
Omit<GroupControlProps, 'variant'> &
|
Omit<GroupControlProps, 'variant'> &
|
||||||
Omit<SortControlProps, 'variant'> & {
|
Omit<SortControlProps, 'variant'> & {
|
||||||
@ -53,34 +53,11 @@ const MobileCollectionControlsDrawer = ({
|
|||||||
ModalProps={{
|
ModalProps={{
|
||||||
keepMounted: true, // Better open performance on mobile.
|
keepMounted: true, // Better open performance on mobile.
|
||||||
}}
|
}}
|
||||||
sx={{
|
slotProps={{ root: { className: mobileCollectionControlsClasses.root } }}
|
||||||
width: '80%',
|
|
||||||
maxWidth: DRAWER_WIDTH,
|
|
||||||
'& .MuiBackdrop-root': {
|
|
||||||
width: '100%',
|
|
||||||
},
|
|
||||||
'& .MuiDrawer-paper': {
|
|
||||||
boxSizing: 'border-box',
|
|
||||||
width: '80%',
|
|
||||||
maxWidth: DRAWER_WIDTH,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
onClick={onMobileOpenToggle}
|
onClick={onMobileOpenToggle}
|
||||||
className="
|
className={classNames(mobileCollectionControlsClasses.content, 'CMS_Scrollbar_root')}
|
||||||
px-5
|
|
||||||
py-4
|
|
||||||
flex
|
|
||||||
flex-col
|
|
||||||
gap-6
|
|
||||||
h-full
|
|
||||||
w-full
|
|
||||||
overflow-y-auto
|
|
||||||
bg-white
|
|
||||||
dark:bg-slate-800
|
|
||||||
styled-scrollbars
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
{showSortControl ? (
|
{showSortControl ? (
|
||||||
<SortControl fields={fields} sort={sort} onSortClick={onSortClick} variant="list" />
|
<SortControl fields={fields} sort={sort} onSortClick={onSortClick} variant="list" />
|
||||||
|
26
packages/core/src/components/common/alert/Alert.css
Normal file
26
packages/core/src/components/common/alert/Alert.css
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
.CMS_Alert_root {
|
||||||
|
@apply w-[50%]
|
||||||
|
min-w-[300px]
|
||||||
|
max-w-[600px];
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Alert_title {
|
||||||
|
@apply px-6
|
||||||
|
py-4
|
||||||
|
text-xl;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Alert_content {
|
||||||
|
@apply px-6
|
||||||
|
pb-4
|
||||||
|
text-sm
|
||||||
|
text-slate-500
|
||||||
|
dark:text-slate-400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Alert_actions {
|
||||||
|
@apply p-2
|
||||||
|
flex
|
||||||
|
justify-end
|
||||||
|
gap-2;
|
||||||
|
}
|
@ -2,12 +2,23 @@ import React, { useCallback, useMemo, useState } from 'react';
|
|||||||
import { translate } from 'react-polyglot';
|
import { translate } from 'react-polyglot';
|
||||||
|
|
||||||
import AlertEvent from '@staticcms/core/lib/util/events/AlertEvent';
|
import AlertEvent from '@staticcms/core/lib/util/events/AlertEvent';
|
||||||
|
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
|
||||||
import { useWindowEvent } from '@staticcms/core/lib/util/window.util';
|
import { useWindowEvent } from '@staticcms/core/lib/util/window.util';
|
||||||
import Button from '../button/Button';
|
import Button from '../button/Button';
|
||||||
import Modal from '../modal/Modal';
|
import Modal from '../modal/Modal';
|
||||||
|
|
||||||
import type { TranslateProps } from 'react-polyglot';
|
import type { TranslateProps } from 'react-polyglot';
|
||||||
|
|
||||||
|
import './Alert.css';
|
||||||
|
|
||||||
|
export const classes = generateClassNames('Alert', [
|
||||||
|
'root',
|
||||||
|
'title',
|
||||||
|
'content',
|
||||||
|
'actions',
|
||||||
|
'confirm-button',
|
||||||
|
]);
|
||||||
|
|
||||||
interface AlertProps {
|
interface AlertProps {
|
||||||
title: string | { key: string; options?: Record<string, unknown> };
|
title: string | { key: string; options?: Record<string, unknown> };
|
||||||
body: string | { key: string; options?: Record<string, unknown> };
|
body: string | { key: string; options?: Record<string, unknown> };
|
||||||
@ -67,44 +78,19 @@ const AlertDialog = ({ t }: TranslateProps) => {
|
|||||||
<Modal
|
<Modal
|
||||||
open
|
open
|
||||||
onClose={handleClose}
|
onClose={handleClose}
|
||||||
className="
|
className={classes.root}
|
||||||
w-[50%]
|
|
||||||
min-w-[300px]
|
|
||||||
max-w-[600px]
|
|
||||||
"
|
|
||||||
aria-labelledby="alert-dialog-title"
|
aria-labelledby="alert-dialog-title"
|
||||||
aria-describedby="alert-dialog-description"
|
aria-describedby="alert-dialog-description"
|
||||||
>
|
>
|
||||||
<div
|
<div className={classes.title}>{title}</div>
|
||||||
className="
|
<div className={classes.content}>{body}</div>
|
||||||
px-6
|
<div className={classes.actions}>
|
||||||
py-4
|
<Button
|
||||||
text-xl
|
onClick={handleClose}
|
||||||
bold
|
variant="contained"
|
||||||
"
|
color={color}
|
||||||
>
|
className={classes['confirm-button']}
|
||||||
{title}
|
>
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="
|
|
||||||
px-6
|
|
||||||
pb-4
|
|
||||||
text-sm
|
|
||||||
text-slate-500
|
|
||||||
dark:text-slate-400
|
|
||||||
"
|
|
||||||
>
|
|
||||||
{body}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="
|
|
||||||
p-2
|
|
||||||
flex
|
|
||||||
justify-end
|
|
||||||
gap-2
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<Button onClick={handleClose} variant="contained" color={color}>
|
|
||||||
{okay}
|
{okay}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -0,0 +1,123 @@
|
|||||||
|
.CMS_Autocomplete_root {
|
||||||
|
@apply relative
|
||||||
|
w-full;
|
||||||
|
|
||||||
|
&.CMS_Autocomplete_disabled {
|
||||||
|
& .CMS_Autocomplete_input {
|
||||||
|
@apply text-gray-300
|
||||||
|
dark:text-gray-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .CMS_Autocomplete_button-icon {
|
||||||
|
@apply text-gray-300/75
|
||||||
|
dark:text-gray-600/75;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Autocomplete_input {
|
||||||
|
@apply w-full
|
||||||
|
bg-transparent
|
||||||
|
border-none
|
||||||
|
py-2
|
||||||
|
pl-3
|
||||||
|
pr-10
|
||||||
|
text-sm
|
||||||
|
leading-5
|
||||||
|
focus:ring-0
|
||||||
|
outline-none
|
||||||
|
flex-grow
|
||||||
|
truncate
|
||||||
|
text-gray-800
|
||||||
|
dark:text-gray-100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Autocomplete_button-wrapper {
|
||||||
|
@apply absolute
|
||||||
|
inset-y-0
|
||||||
|
right-0
|
||||||
|
flex
|
||||||
|
items-center
|
||||||
|
pr-2
|
||||||
|
gap-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Autocomplete_button {
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Autocomplete_button-icon {
|
||||||
|
@apply h-5
|
||||||
|
w-5
|
||||||
|
text-gray-400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Autocomplete_options {
|
||||||
|
@apply max-h-60
|
||||||
|
w-full
|
||||||
|
overflow-auto
|
||||||
|
rounded-md
|
||||||
|
bg-white
|
||||||
|
py-1
|
||||||
|
text-base
|
||||||
|
shadow-md
|
||||||
|
ring-1
|
||||||
|
ring-black
|
||||||
|
ring-opacity-5
|
||||||
|
focus:outline-none
|
||||||
|
sm:text-sm
|
||||||
|
z-30
|
||||||
|
dark:bg-slate-700
|
||||||
|
dark:shadow-lg;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Autocomplete_nothing {
|
||||||
|
@apply relative
|
||||||
|
cursor-default
|
||||||
|
select-none
|
||||||
|
py-2
|
||||||
|
px-4
|
||||||
|
text-gray-800
|
||||||
|
dark:text-gray-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Autocomplete_option {
|
||||||
|
@apply relative
|
||||||
|
select-none
|
||||||
|
py-2
|
||||||
|
pl-10
|
||||||
|
pr-4
|
||||||
|
cursor-pointer
|
||||||
|
text-gray-800
|
||||||
|
dark:text-gray-100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Autocomplete_option-selected {
|
||||||
|
@apply bg-gray-100
|
||||||
|
dark:bg-slate-600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Autocomplete_option-selected {
|
||||||
|
& .CMS_Autocomplete_option-label {
|
||||||
|
@apply font-medium;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Autocomplete_option-label {
|
||||||
|
@apply block
|
||||||
|
font-normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Autocomplete_checkmark {
|
||||||
|
@apply absolute
|
||||||
|
inset-y-0
|
||||||
|
left-0
|
||||||
|
flex
|
||||||
|
items-center
|
||||||
|
pl-3
|
||||||
|
text-blue-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Autocomplete_checkmark-icon {
|
||||||
|
@apply h-5
|
||||||
|
w-5;
|
||||||
|
}
|
@ -1,262 +1,262 @@
|
|||||||
import { Combobox, Transition } from '@headlessui/react';
|
import { Popper } from '@mui/base/Popper';
|
||||||
|
import { useAutocomplete } from '@mui/base/useAutocomplete';
|
||||||
|
import { unstable_useForkRef as useForkRef } from '@mui/utils';
|
||||||
import { Check as CheckIcon } from '@styled-icons/material/Check';
|
import { Check as CheckIcon } from '@styled-icons/material/Check';
|
||||||
import { Close as CloseIcon } from '@styled-icons/material/Close';
|
import { Close as CloseIcon } from '@styled-icons/material/Close';
|
||||||
import { KeyboardArrowDown as KeyboardArrowDownIcon } from '@styled-icons/material/KeyboardArrowDown';
|
import { KeyboardArrowDown as KeyboardArrowDownIcon } from '@styled-icons/material/KeyboardArrowDown';
|
||||||
import React, { Fragment, forwardRef, useCallback } from 'react';
|
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
import useDebouncedCallback from '@staticcms/core/lib/hooks/useDebouncedCallback';
|
||||||
import classNames from '@staticcms/core/lib/util/classNames.util';
|
import classNames from '@staticcms/core/lib/util/classNames.util';
|
||||||
|
import { isNullish } from '@staticcms/core/lib/util/null.util';
|
||||||
|
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
|
||||||
import IconButton from '../button/IconButton';
|
import IconButton from '../button/IconButton';
|
||||||
|
|
||||||
import type { ReactNode, Ref } from 'react';
|
import type { MouseEvent, ReactNode, Ref } from 'react';
|
||||||
|
|
||||||
export interface Option<T> {
|
import './Autocomplete.css';
|
||||||
|
|
||||||
|
export const classes = generateClassNames('Autocomplete', [
|
||||||
|
'root',
|
||||||
|
'focused',
|
||||||
|
'disabled',
|
||||||
|
'input',
|
||||||
|
'button-wrapper',
|
||||||
|
'button',
|
||||||
|
'button-icon',
|
||||||
|
'options',
|
||||||
|
'nothing',
|
||||||
|
'option',
|
||||||
|
'option-selected',
|
||||||
|
'option-label',
|
||||||
|
'checkmark',
|
||||||
|
'checkmark-icon',
|
||||||
|
]);
|
||||||
|
|
||||||
|
export interface Option {
|
||||||
label: string;
|
label: string;
|
||||||
value: T;
|
value: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getOptionLabelAndValue<T>(option: T | Option<T>): Option<T> {
|
function getOptionLabelAndValue(option: string | Option): Option {
|
||||||
if (option && typeof option === 'object' && 'label' in option && 'value' in option) {
|
if (option && typeof option === 'object' && 'label' in option && 'value' in option) {
|
||||||
return option;
|
return option;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { label: String(option), value: option };
|
return { label: option, value: option };
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AutocompleteChangeEventHandler<T> = (value: T | T[]) => void;
|
export type AutocompleteChangeEventHandler = (value: string | string[]) => void;
|
||||||
|
|
||||||
export interface AutocompleteProps<T> {
|
export interface AutocompleteProps {
|
||||||
label: ReactNode | ReactNode[];
|
label: ReactNode | ReactNode[];
|
||||||
value: T | T[] | null;
|
value: string | string[] | null;
|
||||||
options: T[] | Option<T>[];
|
options: string[] | Option[];
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
displayValue: (item: T | T[] | null) => string;
|
inputRef?: Ref<HTMLInputElement>;
|
||||||
|
displayValue: (item: string | string[] | null) => string;
|
||||||
onQuery: (query: string) => void;
|
onQuery: (query: string) => void;
|
||||||
onChange: (value: T | T[] | undefined) => void;
|
onChange: (value: string | string[] | undefined) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Autocomplete = function <T>(
|
const Autocomplete = ({
|
||||||
{
|
label,
|
||||||
label,
|
value,
|
||||||
value,
|
options,
|
||||||
options,
|
inputRef,
|
||||||
disabled,
|
disabled,
|
||||||
required,
|
required,
|
||||||
displayValue,
|
onChange,
|
||||||
onQuery,
|
onQuery,
|
||||||
onChange,
|
}: AutocompleteProps) => {
|
||||||
}: AutocompleteProps<T>,
|
const [inputValue, setInputValue] = useState('');
|
||||||
ref: Ref<HTMLInputElement>,
|
|
||||||
) {
|
const debouncedOnQuery = useDebouncedCallback(onQuery, 200);
|
||||||
|
|
||||||
|
const handleInputChange = useCallback(
|
||||||
|
(newInputValue: string) => {
|
||||||
|
setInputValue(newInputValue);
|
||||||
|
debouncedOnQuery(newInputValue);
|
||||||
|
},
|
||||||
|
[debouncedOnQuery],
|
||||||
|
);
|
||||||
|
|
||||||
const handleChange = useCallback(
|
const handleChange = useCallback(
|
||||||
(selectedValue: T) => {
|
(selectedValue: Option | readonly Option[] | null) => {
|
||||||
if (Array.isArray(value)) {
|
if (selectedValue === null) {
|
||||||
const newValue = [...value];
|
if (Array.isArray(value)) {
|
||||||
const index = newValue.indexOf(selectedValue);
|
onChange([]);
|
||||||
if (index > -1) {
|
return;
|
||||||
newValue.splice(index, 1);
|
|
||||||
} else {
|
|
||||||
newValue.push(selectedValue);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onChange(newValue);
|
onChange(undefined);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
onChange(selectedValue);
|
if ('value' in selectedValue) {
|
||||||
|
onChange(selectedValue.value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange(selectedValue.map(option => option.value));
|
||||||
},
|
},
|
||||||
[onChange, value],
|
[onChange, value],
|
||||||
);
|
);
|
||||||
|
|
||||||
const clear = useCallback(() => {
|
const clear = useCallback(
|
||||||
onChange(Array.isArray(value) ? [] : undefined);
|
(event: MouseEvent) => {
|
||||||
}, [onChange, value]);
|
event.stopPropagation();
|
||||||
|
onChange(Array.isArray(value) ? [] : undefined);
|
||||||
|
setInputValue('');
|
||||||
|
debouncedOnQuery('');
|
||||||
|
},
|
||||||
|
[debouncedOnQuery, onChange, value],
|
||||||
|
);
|
||||||
|
|
||||||
|
const finalOptions = useMemo(() => options.map(getOptionLabelAndValue), [options]);
|
||||||
|
const optionsByValue = useMemo(
|
||||||
|
() =>
|
||||||
|
finalOptions.reduce((acc, option) => {
|
||||||
|
acc[option.value] = option;
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, Option>),
|
||||||
|
[finalOptions],
|
||||||
|
);
|
||||||
|
|
||||||
|
const finalValue = useMemo(() => {
|
||||||
|
if (isNullish(value)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return optionsByValue[value];
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.map(v => optionsByValue[v]).filter(v => Boolean(v));
|
||||||
|
}, [optionsByValue, value]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
getRootProps,
|
||||||
|
getInputProps,
|
||||||
|
getListboxProps,
|
||||||
|
getOptionProps,
|
||||||
|
groupedOptions,
|
||||||
|
focused,
|
||||||
|
popupOpen,
|
||||||
|
anchorEl,
|
||||||
|
setAnchorEl,
|
||||||
|
} = useAutocomplete({
|
||||||
|
options: finalOptions,
|
||||||
|
value: finalValue,
|
||||||
|
inputValue,
|
||||||
|
multiple: Array.isArray(value),
|
||||||
|
disabled,
|
||||||
|
openOnFocus: true,
|
||||||
|
onChange: (_event, selectedValue) => handleChange(selectedValue),
|
||||||
|
onInputChange: (_event, newQueryInput) => handleInputChange(newQueryInput),
|
||||||
|
filterOptions: options => options,
|
||||||
|
clearOnBlur: false,
|
||||||
|
clearOnEscape: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const ref = useRef<HTMLDivElement>();
|
||||||
|
const rootRef = useForkRef(ref, setAnchorEl);
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const localInputRef = (getInputProps() as any)
|
||||||
|
.ref as React.MutableRefObject<HTMLInputElement | null>;
|
||||||
|
const finalInputRef = useForkRef(localInputRef, inputRef);
|
||||||
|
const handleDropdownButtonClick = useCallback(() => {
|
||||||
|
localInputRef.current?.blur();
|
||||||
|
localInputRef.current?.click();
|
||||||
|
}, [localInputRef]);
|
||||||
|
|
||||||
|
const width = anchorEl?.clientWidth;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative w-full">
|
<React.Fragment>
|
||||||
<Combobox value={value} onChange={handleChange} disabled={disabled}>
|
<div
|
||||||
<div className="relative mt-1">
|
{...getRootProps()}
|
||||||
<div
|
ref={rootRef}
|
||||||
className="
|
className={classNames(
|
||||||
flex
|
classes.root,
|
||||||
flex-col
|
focused && classes.focused,
|
||||||
flex-start
|
disabled && classes.disabled,
|
||||||
text-sm
|
)}
|
||||||
font-medium
|
data-testid="autocomplete"
|
||||||
relative
|
>
|
||||||
min-h-8
|
{label}
|
||||||
p-0
|
<input
|
||||||
w-full
|
{...getInputProps()}
|
||||||
text-gray-800
|
ref={finalInputRef}
|
||||||
dark:text-gray-100
|
className={classes.input}
|
||||||
"
|
data-testid="autocomplete-input"
|
||||||
data-testid="autocomplete-input-wrapper"
|
/>
|
||||||
|
<div className={classes['button-wrapper']}>
|
||||||
|
<IconButton
|
||||||
|
variant="text"
|
||||||
|
size="small"
|
||||||
|
disabled={disabled}
|
||||||
|
className={classes.button}
|
||||||
|
onClick={handleDropdownButtonClick}
|
||||||
>
|
>
|
||||||
{label}
|
<KeyboardArrowDownIcon className={classes['button-icon']} aria-hidden="true" />
|
||||||
<Combobox.Input<'input', T | T[] | null>
|
</IconButton>
|
||||||
ref={ref}
|
{!required ? (
|
||||||
className={classNames(
|
<IconButton
|
||||||
`
|
variant="text"
|
||||||
w-full
|
size="small"
|
||||||
bg-transparent
|
disabled={disabled}
|
||||||
border-none
|
className={classes.button}
|
||||||
py-2
|
onClick={clear}
|
||||||
pl-3
|
|
||||||
pr-10
|
|
||||||
text-sm
|
|
||||||
leading-5
|
|
||||||
focus:ring-0
|
|
||||||
outline-none
|
|
||||||
flex-grow
|
|
||||||
truncate
|
|
||||||
`,
|
|
||||||
disabled
|
|
||||||
? `
|
|
||||||
text-gray-300
|
|
||||||
dark:text-gray-500
|
|
||||||
`
|
|
||||||
: `
|
|
||||||
text-gray-800
|
|
||||||
dark:text-gray-100
|
|
||||||
`,
|
|
||||||
)}
|
|
||||||
data-testid="autocomplete-input"
|
|
||||||
displayValue={displayValue}
|
|
||||||
onChange={event => onQuery(event.target.value)}
|
|
||||||
/>
|
|
||||||
<div className="absolute inset-y-0 right-0 flex items-center pr-2 gap-1">
|
|
||||||
<Combobox.Button>
|
|
||||||
<KeyboardArrowDownIcon
|
|
||||||
className={classNames(
|
|
||||||
`
|
|
||||||
h-5
|
|
||||||
w-5
|
|
||||||
text-gray-400
|
|
||||||
`,
|
|
||||||
disabled &&
|
|
||||||
`
|
|
||||||
text-gray-300/75
|
|
||||||
dark:text-gray-600/75
|
|
||||||
`,
|
|
||||||
)}
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</Combobox.Button>
|
|
||||||
{!required ? (
|
|
||||||
<IconButton variant="text" disabled={disabled} onClick={clear}>
|
|
||||||
<CloseIcon
|
|
||||||
className={classNames(
|
|
||||||
`
|
|
||||||
h-5
|
|
||||||
w-5
|
|
||||||
text-gray-400
|
|
||||||
`,
|
|
||||||
disabled &&
|
|
||||||
`
|
|
||||||
text-gray-300/75
|
|
||||||
dark:text-gray-600/75
|
|
||||||
`,
|
|
||||||
)}
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</IconButton>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Transition
|
|
||||||
as={Fragment}
|
|
||||||
leave="transition ease-in duration-100"
|
|
||||||
leaveFrom="opacity-100"
|
|
||||||
leaveTo="opacity-0"
|
|
||||||
afterLeave={() => onQuery('')}
|
|
||||||
>
|
|
||||||
<Combobox.Options
|
|
||||||
data-testid="autocomplete-options"
|
|
||||||
className={`
|
|
||||||
absolute
|
|
||||||
mt-1
|
|
||||||
max-h-60
|
|
||||||
w-full
|
|
||||||
overflow-auto
|
|
||||||
rounded-md
|
|
||||||
bg-white
|
|
||||||
py-1
|
|
||||||
text-base
|
|
||||||
shadow-md
|
|
||||||
ring-1
|
|
||||||
ring-black
|
|
||||||
ring-opacity-5
|
|
||||||
focus:outline-none
|
|
||||||
sm:text-sm
|
|
||||||
z-30
|
|
||||||
dark:bg-slate-700
|
|
||||||
dark:shadow-lg
|
|
||||||
`}
|
|
||||||
>
|
>
|
||||||
{options.length === 0 ? (
|
<CloseIcon className={classes['button-icon']} aria-hidden="true" />
|
||||||
<div className="relative cursor-default select-none py-2 px-4 text-gray-800 dark:text-gray-300">
|
</IconButton>
|
||||||
Nothing found.
|
) : null}
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
options.map((option, index) => {
|
|
||||||
const { label: optionLabel, value: optionValue } = getOptionLabelAndValue(option);
|
|
||||||
|
|
||||||
const selected = Array.isArray(value)
|
|
||||||
? value.includes(optionValue)
|
|
||||||
: value === optionValue;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Combobox.Option
|
|
||||||
key={index}
|
|
||||||
data-testid={`autocomplete-option-${optionValue}`}
|
|
||||||
className={({ active }) =>
|
|
||||||
classNames(
|
|
||||||
`
|
|
||||||
relative
|
|
||||||
select-none
|
|
||||||
py-2
|
|
||||||
pl-10
|
|
||||||
pr-4
|
|
||||||
cursor-pointer
|
|
||||||
text-gray-800
|
|
||||||
dark:text-gray-100
|
|
||||||
`,
|
|
||||||
(selected || active) &&
|
|
||||||
`
|
|
||||||
bg-gray-100
|
|
||||||
dark:bg-slate-600
|
|
||||||
`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
value={optionValue}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className={classNames(
|
|
||||||
`
|
|
||||||
block
|
|
||||||
`,
|
|
||||||
selected ? 'font-medium' : 'font-normal',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{optionLabel}
|
|
||||||
</span>
|
|
||||||
{selected ? (
|
|
||||||
<span className="absolute inset-y-0 left-0 flex items-center pl-3 text-blue-500">
|
|
||||||
<CheckIcon className="h-5 w-5" aria-hidden="true" />
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
</Combobox.Option>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
</Combobox.Options>
|
|
||||||
</Transition>
|
|
||||||
</div>
|
</div>
|
||||||
</Combobox>
|
</div>
|
||||||
</div>
|
{anchorEl && (
|
||||||
|
<Popper open={popupOpen} anchorEl={anchorEl} style={{ width }}>
|
||||||
|
<ul
|
||||||
|
{...getListboxProps()}
|
||||||
|
className={classNames(classes.options, 'CMS_Scrollbar_root', 'CMS_Scrollbar_secondary')}
|
||||||
|
>
|
||||||
|
{groupedOptions.length > 0 ? (
|
||||||
|
groupedOptions.map((option, index) => {
|
||||||
|
const { label: optionLabel, value: optionValue } = getOptionLabelAndValue(
|
||||||
|
option as Option,
|
||||||
|
);
|
||||||
|
|
||||||
|
const selected = Array.isArray(value)
|
||||||
|
? value.includes(optionValue)
|
||||||
|
: value === optionValue;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
key={index}
|
||||||
|
{...getOptionProps({ option: option as Option, index })}
|
||||||
|
className={classNames(classes.option, selected && classes['option-selected'])}
|
||||||
|
data-testid={`autocomplete-option-${optionValue}`}
|
||||||
|
>
|
||||||
|
<span className={classes['option-label']}>{optionLabel}</span>
|
||||||
|
{selected ? (
|
||||||
|
<span className={classes.checkmark}>
|
||||||
|
<CheckIcon className={classes['checkmark-icon']} aria-hidden="true" />
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<div className={classes.nothing}>Nothing found.</div>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</Popper>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default forwardRef(Autocomplete) as <T>(
|
export default Autocomplete;
|
||||||
props: AutocompleteProps<T> & { ref: Ref<HTMLButtonElement> },
|
|
||||||
) => JSX.Element;
|
|
||||||
|
11
packages/core/src/components/common/button/Button.css
Normal file
11
packages/core/src/components/common/button/Button.css
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
.CMS_Button_start-icon {
|
||||||
|
@apply w-5
|
||||||
|
h-5
|
||||||
|
mr-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Button_end-icon {
|
||||||
|
@apply w-5
|
||||||
|
h-5
|
||||||
|
ml-2;
|
||||||
|
}
|
@ -2,10 +2,12 @@ import React, { useMemo } from 'react';
|
|||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import classNames from '@staticcms/core/lib/util/classNames.util';
|
import classNames from '@staticcms/core/lib/util/classNames.util';
|
||||||
import useButtonClassNames from './useButtonClassNames';
|
import useButtonClassNames, { buttonClasses } from './useButtonClassNames';
|
||||||
|
|
||||||
import type { CSSProperties, FC, MouseEventHandler, ReactNode, Ref } from 'react';
|
import type { CSSProperties, FC, MouseEventHandler, ReactNode, Ref } from 'react';
|
||||||
|
|
||||||
|
import './Button.css';
|
||||||
|
|
||||||
export interface BaseBaseProps {
|
export interface BaseBaseProps {
|
||||||
variant?: 'contained' | 'outlined' | 'text';
|
variant?: 'contained' | 'outlined' | 'text';
|
||||||
color?: 'primary' | 'secondary' | 'success' | 'error' | 'warning';
|
color?: 'primary' | 'secondary' | 'success' | 'error' | 'warning';
|
||||||
@ -58,16 +60,16 @@ const Button: FC<ButtonLinkProps> = ({
|
|||||||
const buttonClassName = useButtonClassNames(variant, color, size, rounded);
|
const buttonClassName = useButtonClassNames(variant, color, size, rounded);
|
||||||
|
|
||||||
const buttonClassNames = useMemo(
|
const buttonClassNames = useMemo(
|
||||||
() => classNames(buttonClassName, className),
|
() => classNames(className, buttonClassName),
|
||||||
[buttonClassName, className],
|
[buttonClassName, className],
|
||||||
);
|
);
|
||||||
|
|
||||||
const content = useMemo(
|
const content = useMemo(
|
||||||
() => (
|
() => (
|
||||||
<>
|
<>
|
||||||
{StartIcon ? <StartIcon className="w-5 h-5 mr-2" /> : null}
|
{StartIcon ? <StartIcon className={buttonClasses['start-icon']} /> : null}
|
||||||
{children}
|
{children}
|
||||||
{EndIcon ? <EndIcon className="w-5 h-5 ml-2" /> : null}
|
{EndIcon ? <EndIcon className={buttonClasses['end-icon']} /> : null}
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
[EndIcon, StartIcon, children],
|
[EndIcon, StartIcon, children],
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
.CMS_IconButton_root {
|
||||||
|
&.CMS_IconButton_sm {
|
||||||
|
@apply px-0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.CMS_IconButton_md {
|
||||||
|
@apply px-1.5;
|
||||||
|
}
|
||||||
|
}
|
@ -1,11 +1,16 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import Button from './Button';
|
|
||||||
import classNames from '@staticcms/core/lib/util/classNames.util';
|
import classNames from '@staticcms/core/lib/util/classNames.util';
|
||||||
|
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
|
||||||
|
import Button from './Button';
|
||||||
|
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
import type { ButtonLinkProps } from './Button';
|
import type { ButtonLinkProps } from './Button';
|
||||||
|
|
||||||
|
import './IconButton.css';
|
||||||
|
|
||||||
|
export const classes = generateClassNames('IconButton', ['root', 'sm', 'md']);
|
||||||
|
|
||||||
export type IconButtonProps = Omit<ButtonLinkProps, 'children'> & {
|
export type IconButtonProps = Omit<ButtonLinkProps, 'children'> & {
|
||||||
children: FC<{ className?: string }>;
|
children: FC<{ className?: string }>;
|
||||||
};
|
};
|
||||||
@ -13,7 +18,12 @@ export type IconButtonProps = Omit<ButtonLinkProps, 'children'> & {
|
|||||||
const IconButton = ({ children, size = 'medium', className, ...otherProps }: ButtonLinkProps) => {
|
const IconButton = ({ children, size = 'medium', className, ...otherProps }: ButtonLinkProps) => {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
className={classNames(size === 'small' && 'px-0.5', size === 'medium' && 'px-1.5', className)}
|
className={classNames(
|
||||||
|
className,
|
||||||
|
classes.root,
|
||||||
|
size === 'small' && classes.sm,
|
||||||
|
size === 'medium' && classes.md,
|
||||||
|
)}
|
||||||
size={size}
|
size={size}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
>
|
>
|
||||||
|
@ -1,31 +1,58 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
|
||||||
|
|
||||||
import type { BaseBaseProps } from './Button';
|
import type { BaseBaseProps } from './Button';
|
||||||
|
|
||||||
|
export const buttonClasses = generateClassNames('Button', [
|
||||||
|
'root-sm',
|
||||||
|
'root',
|
||||||
|
'root-rounded-no-padding',
|
||||||
|
'root-rounded-sm',
|
||||||
|
'root-rounded',
|
||||||
|
'contained-primary',
|
||||||
|
'contained-secondary',
|
||||||
|
'contained-success',
|
||||||
|
'contained-error',
|
||||||
|
'contained-warning',
|
||||||
|
'outlined-primary',
|
||||||
|
'outlined-secondary',
|
||||||
|
'outlined-success',
|
||||||
|
'outlined-error',
|
||||||
|
'outlined-warning',
|
||||||
|
'text-primary',
|
||||||
|
'text-secondary',
|
||||||
|
'text-success',
|
||||||
|
'text-error',
|
||||||
|
'text-warning',
|
||||||
|
'start-icon',
|
||||||
|
'end-icon',
|
||||||
|
]);
|
||||||
|
|
||||||
const classes: Record<
|
const classes: Record<
|
||||||
Required<BaseBaseProps>['variant'],
|
Required<BaseBaseProps>['variant'],
|
||||||
Record<Required<BaseBaseProps>['color'], string>
|
Record<Required<BaseBaseProps>['color'], string>
|
||||||
> = {
|
> = {
|
||||||
contained: {
|
contained: {
|
||||||
primary: 'btn-contained-primary',
|
primary: 'CMS_Button_contained-primary',
|
||||||
secondary: 'btn-contained-secondary',
|
secondary: 'CMS_Button_contained-secondary',
|
||||||
success: 'btn-contained-success',
|
success: 'CMS_Button_contained-success',
|
||||||
error: 'btn-contained-error',
|
error: 'CMS_Button_contained-error',
|
||||||
warning: 'btn-contained-warning',
|
warning: 'CMS_Button_contained-warning',
|
||||||
},
|
},
|
||||||
outlined: {
|
outlined: {
|
||||||
primary: 'btn-outlined-primary',
|
primary: 'CMS_Button_outlined-primary',
|
||||||
secondary: 'btn-outlined-secondary',
|
secondary: 'CMS_Button_outlined-secondary',
|
||||||
success: 'btn-outlined-success',
|
success: 'CMS_Button_outlined-success',
|
||||||
error: 'btn-outlined-error',
|
error: 'CMS_Button_outlined-error',
|
||||||
warning: 'btn-outlined-warning',
|
warning: 'CMS_Button_outlined-warning',
|
||||||
},
|
},
|
||||||
text: {
|
text: {
|
||||||
primary: 'btn-text-primary',
|
primary: 'CMS_Button_text-primary',
|
||||||
secondary: 'btn-text-secondary',
|
secondary: 'CMS_Button_text-secondary',
|
||||||
success: 'btn-text-success',
|
success: 'CMS_Button_text-success',
|
||||||
error: 'btn-text-error',
|
error: 'CMS_Button_text-error',
|
||||||
warning: 'btn-text-warning',
|
warning: 'CMS_Button_text-warning',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -35,11 +62,11 @@ export default function useButtonClassNames(
|
|||||||
size: Required<BaseBaseProps>['size'],
|
size: Required<BaseBaseProps>['size'],
|
||||||
rounded: boolean | 'no-padding',
|
rounded: boolean | 'no-padding',
|
||||||
) {
|
) {
|
||||||
let mainClass = size === 'small' ? 'btn-sm' : 'btn';
|
let mainClass = size === 'small' ? 'CMS_Button_root-sm' : 'CMS_Button_root';
|
||||||
if (rounded === 'no-padding') {
|
if (rounded === 'no-padding') {
|
||||||
mainClass = 'btn-rounded-no-padding';
|
mainClass = 'CMS_Button_root-rounded-no-padding';
|
||||||
} else if (rounded) {
|
} else if (rounded) {
|
||||||
mainClass = size === 'small' ? 'btn-rounded-sm' : 'btn-rounded';
|
mainClass = size === 'small' ? 'CMS_Button_root-rounded-sm' : 'CMS_Button_root-rounded';
|
||||||
}
|
}
|
||||||
|
|
||||||
return useMemo(() => `${mainClass} ${classes[variant][color]}`, [color, mainClass, variant]);
|
return useMemo(() => `${mainClass} ${classes[variant][color]}`, [color, mainClass, variant]);
|
||||||
|
5
packages/core/src/components/common/card/Card.classes.ts
Normal file
5
packages/core/src/components/common/card/Card.classes.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
|
||||||
|
|
||||||
|
const cardClasses = generateClassNames('Card', ['root', 'header', 'content', 'media', 'actions']);
|
||||||
|
|
||||||
|
export default cardClasses;
|
55
packages/core/src/components/common/card/Card.css
Normal file
55
packages/core/src/components/common/card/Card.css
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
.CMS_Card_root {
|
||||||
|
@apply bg-white
|
||||||
|
border
|
||||||
|
border-gray-100
|
||||||
|
rounded-lg
|
||||||
|
shadow-sm
|
||||||
|
dark:bg-slate-800
|
||||||
|
dark:border-gray-700/40
|
||||||
|
dark:shadow-md
|
||||||
|
flex
|
||||||
|
flex-col
|
||||||
|
h-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Card_header {
|
||||||
|
@apply mb-2
|
||||||
|
text-2xl
|
||||||
|
font-bold
|
||||||
|
tracking-tight
|
||||||
|
text-gray-800
|
||||||
|
dark:text-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Card_content {
|
||||||
|
@apply w-full
|
||||||
|
p-5
|
||||||
|
font-normal
|
||||||
|
text-gray-800
|
||||||
|
dark:text-gray-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Card_media {
|
||||||
|
@apply rounded-t-lg
|
||||||
|
bg-cover
|
||||||
|
bg-no-repeat
|
||||||
|
bg-center
|
||||||
|
w-full
|
||||||
|
object-cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Card_actions {
|
||||||
|
@apply h-full
|
||||||
|
w-full
|
||||||
|
relative
|
||||||
|
flex
|
||||||
|
flex-col
|
||||||
|
rounded-lg
|
||||||
|
justify-start
|
||||||
|
hover:bg-gray-200
|
||||||
|
dark:hover:bg-slate-700/70
|
||||||
|
focus:outline-none
|
||||||
|
focus:ring-4
|
||||||
|
focus:ring-gray-200
|
||||||
|
dark:focus:ring-slate-700;
|
||||||
|
}
|
@ -1,9 +1,12 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import classNames from '@staticcms/core/lib/util/classNames.util';
|
import classNames from '@staticcms/core/lib/util/classNames.util';
|
||||||
|
import cardClasses from './Card.classes';
|
||||||
|
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
|
import './Card.css';
|
||||||
|
|
||||||
interface CardProps {
|
interface CardProps {
|
||||||
children: ReactNode | ReactNode[];
|
children: ReactNode | ReactNode[];
|
||||||
className?: string;
|
className?: string;
|
||||||
@ -12,25 +15,7 @@ interface CardProps {
|
|||||||
|
|
||||||
const Card = ({ children, className, title }: CardProps) => {
|
const Card = ({ children, className, title }: CardProps) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={classNames(cardClasses.root, className)} title={title}>
|
||||||
className={classNames(
|
|
||||||
`
|
|
||||||
bg-white
|
|
||||||
border
|
|
||||||
border-gray-100
|
|
||||||
rounded-lg
|
|
||||||
shadow-sm
|
|
||||||
dark:bg-slate-800
|
|
||||||
dark:border-gray-700/40
|
|
||||||
dark:shadow-md
|
|
||||||
flex
|
|
||||||
flex-col
|
|
||||||
h-full
|
|
||||||
`,
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
title={title}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import cardClasses from './Card.classes';
|
||||||
|
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
interface CardActionAreaProps {
|
interface CardActionAreaProps {
|
||||||
@ -10,24 +12,7 @@ interface CardActionAreaProps {
|
|||||||
|
|
||||||
const CardActionArea = ({ to, children }: CardActionAreaProps) => {
|
const CardActionArea = ({ to, children }: CardActionAreaProps) => {
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link to={to} className={cardClasses.actions}>
|
||||||
to={to}
|
|
||||||
className="
|
|
||||||
h-full
|
|
||||||
w-full
|
|
||||||
relative
|
|
||||||
flex
|
|
||||||
flex-col
|
|
||||||
rounded-lg
|
|
||||||
justify-start
|
|
||||||
hover:bg-gray-200
|
|
||||||
dark:hover:bg-slate-700/70
|
|
||||||
focus:outline-none
|
|
||||||
focus:ring-4
|
|
||||||
focus:ring-gray-200
|
|
||||||
dark:focus:ring-slate-700
|
|
||||||
"
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import cardClasses from './Card.classes';
|
||||||
|
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
interface CardContentProps {
|
interface CardContentProps {
|
||||||
@ -7,7 +9,7 @@ interface CardContentProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const CardContent = ({ children }: CardContentProps) => {
|
const CardContent = ({ children }: CardContentProps) => {
|
||||||
return <p className="w-full p-5 font-normal text-gray-800 dark:text-gray-300">{children}</p>;
|
return <p className={cardClasses.content}>{children}</p>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CardContent;
|
export default CardContent;
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import cardClasses from './Card.classes';
|
||||||
|
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
interface CardHeaderProps {
|
interface CardHeaderProps {
|
||||||
@ -7,11 +9,7 @@ interface CardHeaderProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const CardHeader = ({ children }: CardHeaderProps) => {
|
const CardHeader = ({ children }: CardHeaderProps) => {
|
||||||
return (
|
return <h5 className={cardClasses.header}>{children}</h5>;
|
||||||
<h5 className="mb-2 text-2xl font-bold tracking-tight text-gray-800 dark:text-white">
|
|
||||||
{children}
|
|
||||||
</h5>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CardHeader;
|
export default CardHeader;
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import Image from '../image/Image';
|
import Image from '../image/Image';
|
||||||
|
import cardClasses from './Card.classes';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
BaseField,
|
BaseField,
|
||||||
@ -31,7 +32,7 @@ const CardMedia = <EF extends BaseField = UnknownField>({
|
|||||||
}: CardMediaProps<EF>) => {
|
}: CardMediaProps<EF>) => {
|
||||||
return (
|
return (
|
||||||
<Image
|
<Image
|
||||||
className="rounded-t-lg bg-cover bg-no-repeat bg-center w-full object-cover"
|
className={cardClasses.media}
|
||||||
style={{
|
style={{
|
||||||
width: width ? `${width}px` : undefined,
|
width: width ? `${width}px` : undefined,
|
||||||
height: height ? `${height}px` : undefined,
|
height: height ? `${height}px` : undefined,
|
||||||
|
66
packages/core/src/components/common/checkbox/Checkbox.css
Normal file
66
packages/core/src/components/common/checkbox/Checkbox.css
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
.CMS_Checkbox_root {
|
||||||
|
@apply relative
|
||||||
|
inline-flex
|
||||||
|
items-center
|
||||||
|
cursor-pointer;
|
||||||
|
|
||||||
|
&.CMS_Checkbox_disabled {
|
||||||
|
@apply cursor-default;
|
||||||
|
|
||||||
|
& .CMS_Checkbox_input {
|
||||||
|
& + .CMS_Checkbox_custom-input {
|
||||||
|
@apply bg-blue-600/25
|
||||||
|
after:border-gray-500/75;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& .CMS_Checkbox_custom-input {
|
||||||
|
@apply bg-gray-100/75
|
||||||
|
dark:bg-gray-700/75;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Checkbox_input {
|
||||||
|
@apply sr-only;
|
||||||
|
|
||||||
|
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
|
||||||
|
&:focus + .CMS_Checkbox_custom-input {
|
||||||
|
@apply ring-4
|
||||||
|
ring-blue-300
|
||||||
|
dark:ring-blue-800;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:checked + .CMS_Checkbox_custom-input {
|
||||||
|
@apply after:translate-x-full
|
||||||
|
bg-blue-600
|
||||||
|
after:border-white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Checkbox_custom-input {
|
||||||
|
@apply w-6
|
||||||
|
h-6
|
||||||
|
text-blue-600
|
||||||
|
border-gray-300
|
||||||
|
rounded
|
||||||
|
focus:ring-blue-500
|
||||||
|
dark:focus:ring-blue-600
|
||||||
|
dark:ring-offset-gray-800
|
||||||
|
focus:ring-2
|
||||||
|
dark:border-gray-600
|
||||||
|
select-none
|
||||||
|
flex
|
||||||
|
items-center
|
||||||
|
justify-center
|
||||||
|
bg-gray-100
|
||||||
|
dark:bg-gray-700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Checkbox_checkmark {
|
||||||
|
@apply w-5
|
||||||
|
h-5
|
||||||
|
text-white;
|
||||||
|
}
|
@ -2,9 +2,20 @@ import { Check as CheckIcon } from '@styled-icons/material/Check';
|
|||||||
import React, { useCallback, useRef } from 'react';
|
import React, { useCallback, useRef } from 'react';
|
||||||
|
|
||||||
import classNames from '@staticcms/core/lib/util/classNames.util';
|
import classNames from '@staticcms/core/lib/util/classNames.util';
|
||||||
|
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
|
||||||
|
|
||||||
import type { ChangeEventHandler, FC, KeyboardEvent, MouseEvent } from 'react';
|
import type { ChangeEventHandler, FC, KeyboardEvent, MouseEvent } from 'react';
|
||||||
|
|
||||||
|
import './Checkbox.css';
|
||||||
|
|
||||||
|
export const classes = generateClassNames('Checkbox', [
|
||||||
|
'root',
|
||||||
|
'disabled',
|
||||||
|
'input',
|
||||||
|
'custom-input',
|
||||||
|
'checkmark',
|
||||||
|
]);
|
||||||
|
|
||||||
export interface CheckboxProps {
|
export interface CheckboxProps {
|
||||||
checked: boolean;
|
checked: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
@ -35,15 +46,7 @@ const Checkbox: FC<CheckboxProps> = ({ checked, disabled = false, onChange }) =>
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<label
|
<label
|
||||||
className={classNames(
|
className={classNames(classes.root, disabled && classes.disabled)}
|
||||||
`
|
|
||||||
relative
|
|
||||||
inline-flex
|
|
||||||
items-center
|
|
||||||
cursor-pointer
|
|
||||||
`,
|
|
||||||
disabled && 'cursor-default',
|
|
||||||
)}
|
|
||||||
onClick={handleNoop}
|
onClick={handleNoop}
|
||||||
onKeyDown={handleKeydown}
|
onKeyDown={handleKeydown}
|
||||||
>
|
>
|
||||||
@ -52,59 +55,16 @@ const Checkbox: FC<CheckboxProps> = ({ checked, disabled = false, onChange }) =>
|
|||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={checked}
|
checked={checked}
|
||||||
className="sr-only peer hide-tap"
|
className={classes.input}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
onClick={handleNoop}
|
onClick={handleNoop}
|
||||||
onKeyDown={handleKeydown}
|
onKeyDown={handleKeydown}
|
||||||
/>
|
/>
|
||||||
<div
|
<div className={classes['custom-input']} onClick={handleClick} onKeyDown={handleKeydown}>
|
||||||
className={classNames(
|
|
||||||
`
|
|
||||||
w-6
|
|
||||||
h-6
|
|
||||||
peer
|
|
||||||
peer-focus:ring-4
|
|
||||||
peer-focus:ring-blue-300
|
|
||||||
dark:peer-focus:ring-blue-800
|
|
||||||
peer-checked:after:translate-x-full
|
|
||||||
text-blue-600
|
|
||||||
border-gray-300
|
|
||||||
rounded
|
|
||||||
focus:ring-blue-500
|
|
||||||
dark:focus:ring-blue-600
|
|
||||||
dark:ring-offset-gray-800
|
|
||||||
focus:ring-2
|
|
||||||
dark:border-gray-600
|
|
||||||
select-none
|
|
||||||
flex
|
|
||||||
items-center
|
|
||||||
justify-center
|
|
||||||
`,
|
|
||||||
disabled
|
|
||||||
? `
|
|
||||||
peer-checked:bg-blue-600/25
|
|
||||||
peer-checked:after:border-gray-500/75
|
|
||||||
bg-gray-100/75
|
|
||||||
dark:bg-gray-700/75
|
|
||||||
`
|
|
||||||
: `
|
|
||||||
peer-checked:bg-blue-600
|
|
||||||
peer-checked:after:border-white
|
|
||||||
bg-gray-100
|
|
||||||
dark:bg-gray-700
|
|
||||||
`,
|
|
||||||
)}
|
|
||||||
onClick={handleClick}
|
|
||||||
onKeyDown={handleKeydown}
|
|
||||||
>
|
|
||||||
{checked ? (
|
{checked ? (
|
||||||
<CheckIcon
|
<CheckIcon
|
||||||
className="
|
className={classes.checkmark}
|
||||||
w-5
|
|
||||||
h-5
|
|
||||||
text-white
|
|
||||||
"
|
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
onKeyDown={handleKeydown}
|
onKeyDown={handleKeydown}
|
||||||
/>
|
/>
|
||||||
|
26
packages/core/src/components/common/confirm/Confirm.css
Normal file
26
packages/core/src/components/common/confirm/Confirm.css
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
.CMS_Confirm_root {
|
||||||
|
@apply w-[50%]
|
||||||
|
min-w-[300px]
|
||||||
|
max-w-[600px];
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Confirm_title {
|
||||||
|
@apply px-6
|
||||||
|
py-4
|
||||||
|
text-xl;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Confirm_content {
|
||||||
|
@apply px-6
|
||||||
|
pb-4
|
||||||
|
text-sm
|
||||||
|
text-slate-500
|
||||||
|
dark:text-slate-400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Confirm_actions {
|
||||||
|
@apply p-2
|
||||||
|
flex
|
||||||
|
justify-end
|
||||||
|
gap-2;
|
||||||
|
}
|
@ -2,12 +2,24 @@ import React, { useCallback, useMemo, useState } from 'react';
|
|||||||
import { translate } from 'react-polyglot';
|
import { translate } from 'react-polyglot';
|
||||||
|
|
||||||
import ConfirmEvent from '@staticcms/core/lib/util/events/ConfirmEvent';
|
import ConfirmEvent from '@staticcms/core/lib/util/events/ConfirmEvent';
|
||||||
|
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
|
||||||
import { useWindowEvent } from '@staticcms/core/lib/util/window.util';
|
import { useWindowEvent } from '@staticcms/core/lib/util/window.util';
|
||||||
import Button from '../button/Button';
|
import Button from '../button/Button';
|
||||||
import Modal from '../modal/Modal';
|
import Modal from '../modal/Modal';
|
||||||
|
|
||||||
import type { TranslateProps } from 'react-polyglot';
|
import type { TranslateProps } from 'react-polyglot';
|
||||||
|
|
||||||
|
import './Confirm.css';
|
||||||
|
|
||||||
|
export const classes = generateClassNames('Confirm', [
|
||||||
|
'root',
|
||||||
|
'title',
|
||||||
|
'content',
|
||||||
|
'actions',
|
||||||
|
'confirm-button',
|
||||||
|
'cancel-button',
|
||||||
|
]);
|
||||||
|
|
||||||
interface ConfirmProps {
|
interface ConfirmProps {
|
||||||
title: string | { key: string; options?: Record<string, unknown> };
|
title: string | { key: string; options?: Record<string, unknown> };
|
||||||
body: string | { key: string; options?: Record<string, unknown> };
|
body: string | { key: string; options?: Record<string, unknown> };
|
||||||
@ -83,47 +95,27 @@ const ConfirmDialog = ({ t }: TranslateProps) => {
|
|||||||
<Modal
|
<Modal
|
||||||
open
|
open
|
||||||
onClose={handleCancel}
|
onClose={handleCancel}
|
||||||
className="
|
className={classes.root}
|
||||||
w-[50%]
|
|
||||||
min-w-[300px]
|
|
||||||
max-w-[600px]
|
|
||||||
"
|
|
||||||
aria-labelledby="confirm-dialog-title"
|
aria-labelledby="confirm-dialog-title"
|
||||||
aria-describedby="confirm-dialog-description"
|
aria-describedby="confirm-dialog-description"
|
||||||
>
|
>
|
||||||
<div
|
<div className={classes.title}>{title}</div>
|
||||||
className="
|
<div className={classes.content}>{body}</div>
|
||||||
px-6
|
<div className={classes.actions}>
|
||||||
py-4
|
<Button
|
||||||
text-xl
|
onClick={handleCancel}
|
||||||
bold
|
variant="text"
|
||||||
"
|
color="secondary"
|
||||||
>
|
className={classes['cancel-button']}
|
||||||
{title}
|
>
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="
|
|
||||||
px-6
|
|
||||||
pb-4
|
|
||||||
text-sm
|
|
||||||
text-slate-500
|
|
||||||
dark:text-slate-400
|
|
||||||
"
|
|
||||||
>
|
|
||||||
{body}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="
|
|
||||||
p-2
|
|
||||||
flex
|
|
||||||
justify-end
|
|
||||||
gap-2
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<Button onClick={handleCancel} variant="text" color="secondary">
|
|
||||||
{cancel}
|
{cancel}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleConfirm} variant="contained" color={color}>
|
<Button
|
||||||
|
onClick={handleConfirm}
|
||||||
|
variant="contained"
|
||||||
|
color={color}
|
||||||
|
className={classes['confirm-button']}
|
||||||
|
>
|
||||||
{confirm}
|
{confirm}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
.CMS_ErrorMessage_root {
|
||||||
|
@apply flex
|
||||||
|
w-full
|
||||||
|
text-xs
|
||||||
|
text-red-500
|
||||||
|
px-3
|
||||||
|
pt-2;
|
||||||
|
}
|
@ -1,10 +1,15 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import classNames from '@staticcms/core/lib/util/classNames.util';
|
import classNames from '@staticcms/core/lib/util/classNames.util';
|
||||||
|
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
|
||||||
|
|
||||||
import type { FieldError } from '@staticcms/core/interface';
|
import type { FieldError } from '@staticcms/core/interface';
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
|
|
||||||
|
import './ErrorMessage.css';
|
||||||
|
|
||||||
|
export const classes = generateClassNames('ErrorMessage', ['root']);
|
||||||
|
|
||||||
export interface ErrorMessageProps {
|
export interface ErrorMessageProps {
|
||||||
errors: FieldError[];
|
errors: FieldError[];
|
||||||
className?: string;
|
className?: string;
|
||||||
@ -12,21 +17,7 @@ export interface ErrorMessageProps {
|
|||||||
|
|
||||||
const ErrorMessage: FC<ErrorMessageProps> = ({ errors, className }) => {
|
const ErrorMessage: FC<ErrorMessageProps> = ({ errors, className }) => {
|
||||||
return errors.length ? (
|
return errors.length ? (
|
||||||
<div
|
<div key="error" data-testid="error" className={classNames(classes.root, className)}>
|
||||||
key="error"
|
|
||||||
data-testid="error"
|
|
||||||
className={classNames(
|
|
||||||
`
|
|
||||||
flex
|
|
||||||
w-full
|
|
||||||
text-xs
|
|
||||||
text-red-500
|
|
||||||
px-3
|
|
||||||
pt-2
|
|
||||||
`,
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{errors[0].message}
|
{errors[0].message}
|
||||||
</div>
|
</div>
|
||||||
) : null;
|
) : null;
|
||||||
|
59
packages/core/src/components/common/field/Field.css
Normal file
59
packages/core/src/components/common/field/Field.css
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
.CMS_Field_root {
|
||||||
|
@apply relative
|
||||||
|
flex
|
||||||
|
items-center
|
||||||
|
gap-2
|
||||||
|
border-b
|
||||||
|
border-slate-400
|
||||||
|
focus-within:border-blue-800
|
||||||
|
dark:focus-within:border-blue-100;
|
||||||
|
|
||||||
|
&:not(.CMS_Field_disabled):not(.CMS_Field_no-highlight) {
|
||||||
|
@apply focus-within:bg-slate-100
|
||||||
|
dark:focus-within:bg-slate-800
|
||||||
|
hover:bg-slate-100
|
||||||
|
dark:hover:bg-slate-800;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.CMS_Field_no-padding) {
|
||||||
|
@apply pb-3;
|
||||||
|
|
||||||
|
& > .CMS_Field_end-adornment {
|
||||||
|
@apply -mb-3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Field_cursor-pointer {
|
||||||
|
@apply cursor-pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Field_cursor-text {
|
||||||
|
@apply cursor-text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Field_cursor-default {
|
||||||
|
@apply cursor-default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Field_wrapper {
|
||||||
|
@apply flex
|
||||||
|
flex-col
|
||||||
|
w-full;
|
||||||
|
|
||||||
|
&.CMS_Field_for-single-list {
|
||||||
|
@apply mr-14;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Field_inline-wrapper {
|
||||||
|
@apply flex
|
||||||
|
items-center
|
||||||
|
justify-center
|
||||||
|
p-3
|
||||||
|
pb-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Field_end-adornment {
|
||||||
|
@apply pr-2;
|
||||||
|
}
|
@ -2,6 +2,7 @@ import React, { useMemo } from 'react';
|
|||||||
|
|
||||||
import useCursor from '@staticcms/core/lib/hooks/useCursor';
|
import useCursor from '@staticcms/core/lib/hooks/useCursor';
|
||||||
import classNames from '@staticcms/core/lib/util/classNames.util';
|
import classNames from '@staticcms/core/lib/util/classNames.util';
|
||||||
|
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
|
||||||
import ErrorMessage from './ErrorMessage';
|
import ErrorMessage from './ErrorMessage';
|
||||||
import Hint from './Hint';
|
import Hint from './Hint';
|
||||||
import Label from './Label';
|
import Label from './Label';
|
||||||
@ -9,6 +10,25 @@ import Label from './Label';
|
|||||||
import type { FieldError } from '@staticcms/core/interface';
|
import type { FieldError } from '@staticcms/core/interface';
|
||||||
import type { FC, MouseEvent, ReactNode } from 'react';
|
import type { FC, MouseEvent, ReactNode } from 'react';
|
||||||
|
|
||||||
|
import './Field.css';
|
||||||
|
|
||||||
|
export const classes = generateClassNames('Field', [
|
||||||
|
'root',
|
||||||
|
'inline',
|
||||||
|
'wrapper',
|
||||||
|
'inline-wrapper',
|
||||||
|
'disabled',
|
||||||
|
'no-highlight',
|
||||||
|
'no-padding',
|
||||||
|
'cursor-pointer',
|
||||||
|
'cursor-text',
|
||||||
|
'cursor-default',
|
||||||
|
'error',
|
||||||
|
'valid',
|
||||||
|
'for-single-list',
|
||||||
|
'end-adornment',
|
||||||
|
]);
|
||||||
|
|
||||||
export interface FieldProps {
|
export interface FieldProps {
|
||||||
label?: string;
|
label?: string;
|
||||||
inputRef?: React.MutableRefObject<HTMLElement | null>;
|
inputRef?: React.MutableRefObject<HTMLElement | null>;
|
||||||
@ -98,53 +118,34 @@ const Field: FC<FieldProps> = ({
|
|||||||
const rootClassNames = useMemo(
|
const rootClassNames = useMemo(
|
||||||
() =>
|
() =>
|
||||||
classNames(
|
classNames(
|
||||||
`
|
classes.root,
|
||||||
relative
|
|
||||||
flex
|
|
||||||
items-center
|
|
||||||
gap-2
|
|
||||||
border-b
|
|
||||||
border-slate-400
|
|
||||||
focus-within:border-blue-800
|
|
||||||
dark:focus-within:border-blue-100
|
|
||||||
`,
|
|
||||||
rootClassName,
|
rootClassName,
|
||||||
!noHightlight &&
|
disabled && classes.disabled,
|
||||||
!disabled &&
|
noHightlight && classes['no-highlight'],
|
||||||
`
|
noPadding && classes['no-padding'],
|
||||||
focus-within:bg-slate-100
|
finalCursor === 'pointer' && classes['cursor-pointer'],
|
||||||
dark:focus-within:bg-slate-800
|
finalCursor === 'text' && classes['cursor-text'],
|
||||||
hover:bg-slate-100
|
finalCursor === 'default' && classes['cursor-default'],
|
||||||
dark:hover:bg-slate-800
|
hasErrors ? classes.error : `group/active`,
|
||||||
`,
|
|
||||||
!noPadding && 'pb-3',
|
|
||||||
finalCursor === 'pointer' && 'cursor-pointer',
|
|
||||||
finalCursor === 'text' && 'cursor-text',
|
|
||||||
finalCursor === 'default' && 'cursor-default',
|
|
||||||
!hasErrors && 'group/active',
|
|
||||||
),
|
),
|
||||||
[rootClassName, noHightlight, disabled, noPadding, finalCursor, hasErrors],
|
[rootClassName, noHightlight, disabled, noPadding, finalCursor, hasErrors],
|
||||||
);
|
);
|
||||||
|
|
||||||
const wrapperClassNames = useMemo(
|
const wrapperClassNames = useMemo(
|
||||||
() =>
|
() =>
|
||||||
classNames(
|
classNames(classes.wrapper, wrapperClassName, forSingleList && classes['for-single-list']),
|
||||||
`
|
|
||||||
flex
|
|
||||||
flex-col
|
|
||||||
w-full
|
|
||||||
`,
|
|
||||||
wrapperClassName,
|
|
||||||
forSingleList && 'mr-14',
|
|
||||||
),
|
|
||||||
[forSingleList, wrapperClassName],
|
[forSingleList, wrapperClassName],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (variant === 'inline') {
|
if (variant === 'inline') {
|
||||||
return (
|
return (
|
||||||
<div data-testid="inline-field" className={rootClassNames} onClick={handleOnClick}>
|
<div
|
||||||
|
data-testid="inline-field"
|
||||||
|
className={`${rootClassNames} ${classes.inline}`}
|
||||||
|
onClick={handleOnClick}
|
||||||
|
>
|
||||||
<div data-testid="inline-field-wrapper" className={wrapperClassNames}>
|
<div data-testid="inline-field-wrapper" className={wrapperClassNames}>
|
||||||
<div className="flex items-center justify-center p-3 pb-0">
|
<div className={classes['inline-wrapper']}>
|
||||||
{renderedLabel}
|
{renderedLabel}
|
||||||
{renderedHint}
|
{renderedHint}
|
||||||
{children}
|
{children}
|
||||||
@ -163,18 +164,7 @@ const Field: FC<FieldProps> = ({
|
|||||||
{renderedHint}
|
{renderedHint}
|
||||||
{renderedErrorMessage}
|
{renderedErrorMessage}
|
||||||
</div>
|
</div>
|
||||||
{endAdornment ? (
|
{endAdornment ? <div className={classes['end-adornment']}>{endAdornment}</div> : null}
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
`
|
|
||||||
pr-2
|
|
||||||
`,
|
|
||||||
!noPadding && '-mb-3',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{endAdornment}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
44
packages/core/src/components/common/field/Hint.css
Normal file
44
packages/core/src/components/common/field/Hint.css
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
.CMS_Hint_root {
|
||||||
|
@apply w-full
|
||||||
|
flex
|
||||||
|
text-xs
|
||||||
|
italic;
|
||||||
|
|
||||||
|
&:not(.CMS_Hint_error) {
|
||||||
|
&.CMS_Hint_disabled {
|
||||||
|
@apply text-slate-300
|
||||||
|
dark:text-slate-600;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.CMS_Hint_disabled) {
|
||||||
|
@apply text-slate-500
|
||||||
|
dark:text-slate-400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.CMS_Hint_disabled) {
|
||||||
|
@apply group-focus-within/active:text-blue-500
|
||||||
|
group-hover/active:text-blue-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.CMS_Hint_inline) {
|
||||||
|
@apply px-3
|
||||||
|
pt-1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Hint_cursor-pointer {
|
||||||
|
@apply cursor-pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Hint_cursor-text {
|
||||||
|
@apply cursor-text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Hint_cursor-default {
|
||||||
|
@apply cursor-default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Hint_error {
|
||||||
|
@apply text-red-500;
|
||||||
|
}
|
@ -2,9 +2,22 @@ import React from 'react';
|
|||||||
|
|
||||||
import useCursor from '@staticcms/core/lib/hooks/useCursor';
|
import useCursor from '@staticcms/core/lib/hooks/useCursor';
|
||||||
import classNames from '@staticcms/core/lib/util/classNames.util';
|
import classNames from '@staticcms/core/lib/util/classNames.util';
|
||||||
|
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
|
||||||
|
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
|
|
||||||
|
import './Hint.css';
|
||||||
|
|
||||||
|
export const classes = generateClassNames('Hint', [
|
||||||
|
'root',
|
||||||
|
'inline',
|
||||||
|
'disabled',
|
||||||
|
'cursor-pointer',
|
||||||
|
'cursor-text',
|
||||||
|
'cursor-default',
|
||||||
|
'error',
|
||||||
|
]);
|
||||||
|
|
||||||
export interface HintProps {
|
export interface HintProps {
|
||||||
children: string;
|
children: string;
|
||||||
hasErrors: boolean;
|
hasErrors: boolean;
|
||||||
@ -28,32 +41,13 @@ const Hint: FC<HintProps> = ({
|
|||||||
<div
|
<div
|
||||||
data-testid="hint"
|
data-testid="hint"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
`
|
classes.root,
|
||||||
w-full
|
disabled && classes.disabled,
|
||||||
flex
|
finalCursor === 'pointer' && classes['cursor-pointer'],
|
||||||
text-xs
|
finalCursor === 'text' && classes['cursor-text'],
|
||||||
italic
|
finalCursor === 'default' && classes['cursor-default'],
|
||||||
`,
|
hasErrors && classes.error,
|
||||||
!disabled &&
|
variant === 'inline' && classes.inline,
|
||||||
`
|
|
||||||
group-focus-within/active:text-blue-500
|
|
||||||
group-hover/active:text-blue-500
|
|
||||||
`,
|
|
||||||
finalCursor === 'pointer' && 'cursor-pointer',
|
|
||||||
finalCursor === 'text' && 'cursor-text',
|
|
||||||
finalCursor === 'default' && 'cursor-default',
|
|
||||||
hasErrors
|
|
||||||
? 'text-red-500'
|
|
||||||
: disabled
|
|
||||||
? `
|
|
||||||
text-slate-300
|
|
||||||
dark:text-slate-600
|
|
||||||
`
|
|
||||||
: `
|
|
||||||
text-slate-500
|
|
||||||
dark:text-slate-400
|
|
||||||
`,
|
|
||||||
variant === 'default' && 'px-3 pt-1',
|
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
45
packages/core/src/components/common/field/Label.css
Normal file
45
packages/core/src/components/common/field/Label.css
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
.CMS_Label_root {
|
||||||
|
@apply w-full
|
||||||
|
flex
|
||||||
|
text-xs
|
||||||
|
font-bold
|
||||||
|
dark:font-semibold;
|
||||||
|
|
||||||
|
&:not(.CMS_Label_error) {
|
||||||
|
&.CMS_Label_disabled {
|
||||||
|
@apply text-slate-300
|
||||||
|
dark:text-slate-600;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.CMS_Label_disabled) {
|
||||||
|
@apply text-slate-500
|
||||||
|
dark:text-slate-400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.CMS_Label_disabled) {
|
||||||
|
@apply group-focus-within/active:text-blue-500
|
||||||
|
group-hover/active:text-blue-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.CMS_Label_inline) {
|
||||||
|
@apply px-3
|
||||||
|
pt-3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Label_cursor-pointer {
|
||||||
|
@apply cursor-pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Label_cursor-text {
|
||||||
|
@apply cursor-text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Label_cursor-default {
|
||||||
|
@apply cursor-default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Label_error {
|
||||||
|
@apply text-red-500;
|
||||||
|
}
|
@ -2,9 +2,22 @@ import React from 'react';
|
|||||||
|
|
||||||
import useCursor from '@staticcms/core/lib/hooks/useCursor';
|
import useCursor from '@staticcms/core/lib/hooks/useCursor';
|
||||||
import classNames from '@staticcms/core/lib/util/classNames.util';
|
import classNames from '@staticcms/core/lib/util/classNames.util';
|
||||||
|
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
|
||||||
|
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
|
|
||||||
|
import './Label.css';
|
||||||
|
|
||||||
|
export const classes = generateClassNames('Label', [
|
||||||
|
'root',
|
||||||
|
'disabled',
|
||||||
|
'cursor-pointer',
|
||||||
|
'cursor-text',
|
||||||
|
'cursor-default',
|
||||||
|
'error',
|
||||||
|
'inline',
|
||||||
|
]);
|
||||||
|
|
||||||
export interface LabelProps {
|
export interface LabelProps {
|
||||||
htmlFor?: string;
|
htmlFor?: string;
|
||||||
children: string;
|
children: string;
|
||||||
@ -33,33 +46,13 @@ const Label: FC<LabelProps> = ({
|
|||||||
htmlFor={htmlFor}
|
htmlFor={htmlFor}
|
||||||
data-testid={dataTestId ?? 'label'}
|
data-testid={dataTestId ?? 'label'}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
`
|
classes.root,
|
||||||
w-full
|
disabled && classes.disabled,
|
||||||
flex
|
finalCursor === 'pointer' && classes['cursor-pointer'],
|
||||||
text-xs
|
finalCursor === 'text' && classes['cursor-text'],
|
||||||
font-bold
|
finalCursor === 'default' && classes['cursor-default'],
|
||||||
dark:font-semibold
|
hasErrors && classes.error,
|
||||||
`,
|
variant === 'inline' && classes.inline,
|
||||||
!disabled &&
|
|
||||||
`
|
|
||||||
group-focus-within/active:text-blue-500
|
|
||||||
group-hover/active:text-blue-500
|
|
||||||
`,
|
|
||||||
finalCursor === 'pointer' && 'cursor-pointer',
|
|
||||||
finalCursor === 'text' && 'cursor-text',
|
|
||||||
finalCursor === 'default' && 'cursor-default',
|
|
||||||
hasErrors
|
|
||||||
? 'text-red-500'
|
|
||||||
: disabled
|
|
||||||
? `
|
|
||||||
text-slate-300
|
|
||||||
dark:text-slate-600
|
|
||||||
`
|
|
||||||
: `
|
|
||||||
text-slate-500
|
|
||||||
dark:text-slate-400
|
|
||||||
`,
|
|
||||||
variant === 'default' && 'px-3 pt-3',
|
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
16
packages/core/src/components/common/image/Image.css
Normal file
16
packages/core/src/components/common/image/Image.css
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
.CMS_Image_root {
|
||||||
|
&:not(.CMS_Image_empty) {
|
||||||
|
@apply object-cover
|
||||||
|
max-w-full
|
||||||
|
overflow-hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.CMS_Image_empty {
|
||||||
|
@apply p-10
|
||||||
|
rounded-md
|
||||||
|
border
|
||||||
|
max-w-full
|
||||||
|
border-gray-200/75
|
||||||
|
dark:border-slate-600/75;
|
||||||
|
}
|
||||||
|
}
|
@ -1,11 +1,12 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Image as ImageIcon } from '@styled-icons/material-outlined/Image';
|
import { Image as ImageIcon } from '@styled-icons/material-outlined/Image';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
import useMediaAsset from '@staticcms/core/lib/hooks/useMediaAsset';
|
import useMediaAsset from '@staticcms/core/lib/hooks/useMediaAsset';
|
||||||
import classNames from '@staticcms/core/lib/util/classNames.util';
|
import classNames from '@staticcms/core/lib/util/classNames.util';
|
||||||
|
import { isEmpty } from '@staticcms/core/lib/util/string.util';
|
||||||
|
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
|
||||||
import { selectEditingDraft } from '@staticcms/core/reducers/selectors/entryDraft';
|
import { selectEditingDraft } from '@staticcms/core/reducers/selectors/entryDraft';
|
||||||
import { useAppSelector } from '@staticcms/core/store/hooks';
|
import { useAppSelector } from '@staticcms/core/store/hooks';
|
||||||
import { isEmpty } from '@staticcms/core/lib/util/string.util';
|
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
BaseField,
|
BaseField,
|
||||||
@ -16,6 +17,10 @@ import type {
|
|||||||
} from '@staticcms/core/interface';
|
} from '@staticcms/core/interface';
|
||||||
import type { CSSProperties } from 'react';
|
import type { CSSProperties } from 'react';
|
||||||
|
|
||||||
|
import './Image.css';
|
||||||
|
|
||||||
|
export const classes = generateClassNames('Image', ['root', 'empty']);
|
||||||
|
|
||||||
export interface ImageProps<EF extends BaseField> {
|
export interface ImageProps<EF extends BaseField> {
|
||||||
src?: string;
|
src?: string;
|
||||||
alt?: string;
|
alt?: string;
|
||||||
@ -42,18 +47,7 @@ const Image = <EF extends BaseField = UnknownField>({
|
|||||||
const assetSource = useMediaAsset(src, collection, field, entry ?? editingDraft);
|
const assetSource = useMediaAsset(src, collection, field, entry ?? editingDraft);
|
||||||
|
|
||||||
if (isEmpty(src)) {
|
if (isEmpty(src)) {
|
||||||
return (
|
return <ImageIcon className={classNames(classes.root, classes.empty, className)} />;
|
||||||
<ImageIcon
|
|
||||||
className="
|
|
||||||
p-10
|
|
||||||
rounded-md
|
|
||||||
border
|
|
||||||
max-w-full
|
|
||||||
border-gray-200/75
|
|
||||||
dark:border-slate-600/75
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -63,7 +57,7 @@ const Image = <EF extends BaseField = UnknownField>({
|
|||||||
src={assetSource}
|
src={assetSource}
|
||||||
alt={alt}
|
alt={alt}
|
||||||
data-testid={dataTestId ?? 'image'}
|
data-testid={dataTestId ?? 'image'}
|
||||||
className={classNames('object-cover max-w-full overflow-hidden', className)}
|
className={classNames(classes.root, className)}
|
||||||
style={style}
|
style={style}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
69
packages/core/src/components/common/menu/Menu.css
Normal file
69
packages/core/src/components/common/menu/Menu.css
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
.CMS_Menu_root {
|
||||||
|
@apply flex;
|
||||||
|
|
||||||
|
&:not(.CMS_Menu_hide-label) {
|
||||||
|
&:not(.CMS_Menu_hide-dropdown-icon) {
|
||||||
|
& .CMS_Menu_dropdown {
|
||||||
|
& .CMS_Menu_dropdown-start-icon {
|
||||||
|
@apply mr-1.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& .CMS_Menu_dropdown {
|
||||||
|
& .CMS_Menu_dropdown-icon {
|
||||||
|
@apply ml-2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.CMS_Menu_hide-dropdown-icon-mobile {
|
||||||
|
& .CMS_Menu_dropdown {
|
||||||
|
& .CMS_Menu_dropdown-start-icon {
|
||||||
|
@apply !mr-0
|
||||||
|
md:!mr-1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .CMS_Menu_dropdown-icon {
|
||||||
|
@apply !hidden
|
||||||
|
md:!block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Menu_dropdown {
|
||||||
|
@apply whitespace-nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Menu_dropdown-start-icon {
|
||||||
|
@apply -ml-0.5
|
||||||
|
h-5
|
||||||
|
w-5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Menu_dropdown-icon {
|
||||||
|
@apply -mr-0.5
|
||||||
|
h-5
|
||||||
|
w-5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Menu_menu {
|
||||||
|
@apply absolute
|
||||||
|
right-0
|
||||||
|
z-40
|
||||||
|
w-56
|
||||||
|
origin-top-right
|
||||||
|
rounded-md
|
||||||
|
bg-white
|
||||||
|
dark:bg-slate-800
|
||||||
|
shadow-md
|
||||||
|
border
|
||||||
|
border-gray-200
|
||||||
|
focus:outline-none
|
||||||
|
divide-y
|
||||||
|
divide-gray-100
|
||||||
|
dark:border-gray-700
|
||||||
|
dark:divide-gray-600
|
||||||
|
dark:shadow-lg;
|
||||||
|
}
|
@ -1,14 +1,30 @@
|
|||||||
import ClickAwayListener from '@mui/base/ClickAwayListener';
|
import { Dropdown } from '@mui/base/Dropdown';
|
||||||
import MenuUnstyled from '@mui/base/MenuUnstyled';
|
import { Menu as BaseMenu } from '@mui/base/Menu';
|
||||||
|
import { MenuButton } from '@mui/base/MenuButton';
|
||||||
import { KeyboardArrowDown as KeyboardArrowDownIcon } from '@styled-icons/material/KeyboardArrowDown';
|
import { KeyboardArrowDown as KeyboardArrowDownIcon } from '@styled-icons/material/KeyboardArrowDown';
|
||||||
import React, { useCallback, useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
|
|
||||||
import classNames from '@staticcms/core/lib/util/classNames.util';
|
import classNames from '@staticcms/core/lib/util/classNames.util';
|
||||||
|
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
|
||||||
import useButtonClassNames from '../button/useButtonClassNames';
|
import useButtonClassNames from '../button/useButtonClassNames';
|
||||||
|
|
||||||
import type { FC, ReactNode } from 'react';
|
import type { FC, ReactNode } from 'react';
|
||||||
import type { BaseBaseProps } from '../button/Button';
|
import type { BaseBaseProps } from '../button/Button';
|
||||||
|
|
||||||
|
import './Menu.css';
|
||||||
|
|
||||||
|
export const classes = generateClassNames('Menu', [
|
||||||
|
'root',
|
||||||
|
'hide-dropdown-icon',
|
||||||
|
'hide-label',
|
||||||
|
'hide-dropdown-icon-mobile',
|
||||||
|
'dropdown',
|
||||||
|
'dropdown-start-icon',
|
||||||
|
'dropdown-icon',
|
||||||
|
'label',
|
||||||
|
'menu',
|
||||||
|
]);
|
||||||
|
|
||||||
export interface MenuProps {
|
export interface MenuProps {
|
||||||
label: ReactNode;
|
label: ReactNode;
|
||||||
startIcon?: FC<{ className?: string }>;
|
startIcon?: FC<{ className?: string }>;
|
||||||
@ -24,8 +40,8 @@ export interface MenuProps {
|
|||||||
hideDropdownIcon?: boolean;
|
hideDropdownIcon?: boolean;
|
||||||
hideDropdownIconOnMobile?: boolean;
|
hideDropdownIconOnMobile?: boolean;
|
||||||
hideLabel?: boolean;
|
hideLabel?: boolean;
|
||||||
keepMounted?: boolean;
|
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
keepMounted?: boolean;
|
||||||
'data-testid'?: string;
|
'data-testid'?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -44,103 +60,56 @@ const Menu = ({
|
|||||||
hideDropdownIcon = false,
|
hideDropdownIcon = false,
|
||||||
hideDropdownIconOnMobile = false,
|
hideDropdownIconOnMobile = false,
|
||||||
hideLabel = false,
|
hideLabel = false,
|
||||||
keepMounted = false,
|
|
||||||
disabled = false,
|
disabled = false,
|
||||||
|
keepMounted = false,
|
||||||
'data-testid': dataTestId,
|
'data-testid': dataTestId,
|
||||||
}: MenuProps) => {
|
}: MenuProps) => {
|
||||||
const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(null);
|
|
||||||
const isOpen = Boolean(anchorEl);
|
|
||||||
|
|
||||||
const handleButtonClick = useCallback(
|
|
||||||
(event: React.MouseEvent<HTMLButtonElement>) => {
|
|
||||||
if (isOpen) {
|
|
||||||
setAnchorEl(null);
|
|
||||||
} else {
|
|
||||||
setAnchorEl(event.currentTarget);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[isOpen],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleClose = useCallback(() => {
|
|
||||||
setAnchorEl(null);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const calculatedButtonClassName = useButtonClassNames(variant, color, size, rounded);
|
const calculatedButtonClassName = useButtonClassNames(variant, color, size, rounded);
|
||||||
|
|
||||||
const menuButtonClassNames = useMemo(
|
const menuButtonClassNames = useMemo(
|
||||||
() => classNames(calculatedButtonClassName, buttonClassName, 'whitespace-nowrap'),
|
() => classNames(calculatedButtonClassName, buttonClassName, classes.dropdown),
|
||||||
[calculatedButtonClassName, buttonClassName],
|
[calculatedButtonClassName, buttonClassName],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ClickAwayListener mouseEvent="onMouseDown" touchEvent="onTouchStart" onClickAway={handleClose}>
|
<Dropdown>
|
||||||
<div className={classNames('flex', rootClassName)}>
|
<div
|
||||||
<button
|
className={classNames(
|
||||||
type="button"
|
classes.root,
|
||||||
onClick={handleButtonClick}
|
hideLabel && classes['hide-label'],
|
||||||
aria-controls={isOpen ? 'simple-menu' : undefined}
|
hideDropdownIcon && classes['hide-dropdown-icon'],
|
||||||
aria-expanded={isOpen || undefined}
|
hideDropdownIconOnMobile && classes['hide-dropdown-icon-mobile'],
|
||||||
|
rootClassName,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<MenuButton
|
||||||
aria-haspopup="menu"
|
aria-haspopup="menu"
|
||||||
data-testid={dataTestId}
|
data-testid={dataTestId}
|
||||||
className={menuButtonClassNames}
|
className={menuButtonClassNames}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
{StartIcon ? (
|
{StartIcon ? (
|
||||||
<StartIcon
|
<StartIcon className={classNames(classes['dropdown-start-icon'], iconClassName)} />
|
||||||
className={classNames(
|
) : null}
|
||||||
`-ml-0.5 h-5 w-5`,
|
{!hideLabel ? (
|
||||||
!hideLabel && !hideDropdownIcon && 'mr-1.5',
|
<div className={classNames(classes.label, labelClassName)}>{label}</div>
|
||||||
hideDropdownIconOnMobile && '!mr-0 md:!mr-1.5',
|
|
||||||
iconClassName,
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
) : null}
|
) : null}
|
||||||
{!hideLabel ? <div className={labelClassName}>{label}</div> : null}
|
|
||||||
{!hideDropdownIcon ? (
|
{!hideDropdownIcon ? (
|
||||||
<KeyboardArrowDownIcon
|
<KeyboardArrowDownIcon className={classes['dropdown-icon']} aria-hidden="true" />
|
||||||
className={classNames(
|
|
||||||
`-mr-0.5 h-5 w-5`,
|
|
||||||
!hideLabel && 'ml-2',
|
|
||||||
hideDropdownIconOnMobile && '!hidden md:!block',
|
|
||||||
)}
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
) : null}
|
) : null}
|
||||||
</button>
|
</MenuButton>
|
||||||
<MenuUnstyled
|
<BaseMenu
|
||||||
open={isOpen}
|
|
||||||
anchorEl={anchorEl}
|
|
||||||
keepMounted={keepMounted}
|
|
||||||
slotProps={{
|
slotProps={{
|
||||||
root: {
|
root: {
|
||||||
className: `
|
className: classes.menu,
|
||||||
absolute
|
keepMounted,
|
||||||
right-0
|
|
||||||
z-40
|
|
||||||
w-56
|
|
||||||
origin-top-right
|
|
||||||
rounded-md
|
|
||||||
bg-white
|
|
||||||
dark:bg-slate-800
|
|
||||||
shadow-md
|
|
||||||
border
|
|
||||||
border-gray-200
|
|
||||||
focus:outline-none
|
|
||||||
divide-y
|
|
||||||
divide-gray-100
|
|
||||||
dark:border-gray-700
|
|
||||||
dark:divide-gray-600
|
|
||||||
dark:shadow-lg
|
|
||||||
`,
|
|
||||||
onClick: handleClose,
|
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</MenuUnstyled>
|
</BaseMenu>
|
||||||
</div>
|
</div>
|
||||||
</ClickAwayListener>
|
</Dropdown>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
6
packages/core/src/components/common/menu/MenuGroup.css
Normal file
6
packages/core/src/components/common/menu/MenuGroup.css
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
.CMS_MenuGroup_root {
|
||||||
|
@apply py-1
|
||||||
|
border-b
|
||||||
|
border-gray-200
|
||||||
|
dark:border-slate-700;
|
||||||
|
}
|
@ -1,24 +1,19 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
|
||||||
|
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
|
import './MenuGroup.css';
|
||||||
|
|
||||||
|
export const classes = generateClassNames('MenuGroup', ['root']);
|
||||||
|
|
||||||
export interface MenuGroupProps {
|
export interface MenuGroupProps {
|
||||||
children: ReactNode | ReactNode[];
|
children: ReactNode | ReactNode[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const MenuGroup = ({ children }: MenuGroupProps) => {
|
const MenuGroup = ({ children }: MenuGroupProps) => {
|
||||||
return (
|
return <div className={classes.root}>{children}</div>;
|
||||||
<div
|
|
||||||
className="
|
|
||||||
py-1
|
|
||||||
border-b
|
|
||||||
border-gray-200
|
|
||||||
dark:border-slate-700
|
|
||||||
"
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default MenuGroup;
|
export default MenuGroup;
|
||||||
|
84
packages/core/src/components/common/menu/MenuItemButton.css
Normal file
84
packages/core/src/components/common/menu/MenuItemButton.css
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
.CMS_MenuItemButton_root {
|
||||||
|
@apply px-4
|
||||||
|
py-2
|
||||||
|
text-sm
|
||||||
|
w-full
|
||||||
|
text-left
|
||||||
|
flex
|
||||||
|
items-center
|
||||||
|
justify-between
|
||||||
|
cursor-pointer;
|
||||||
|
|
||||||
|
&:not(.CMS_MenuItemButton_disabled) {
|
||||||
|
&.CMS_MenuItemButton_active {
|
||||||
|
@apply bg-slate-200
|
||||||
|
dark:bg-slate-600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.CMS_MenuItemButton_default {
|
||||||
|
@apply text-gray-800
|
||||||
|
dark:text-gray-300;
|
||||||
|
|
||||||
|
&.CMS_MenuItemButton_disabled {
|
||||||
|
@apply text-gray-500
|
||||||
|
dark:text-gray-700;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.CMS_MenuItemButton_disabled) {
|
||||||
|
@apply hover:bg-gray-200
|
||||||
|
dark:hover:bg-slate-600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.CMS_MenuItemButton_warning {
|
||||||
|
@apply text-yellow-600
|
||||||
|
dark:text-yellow-500;
|
||||||
|
|
||||||
|
&.CMS_MenuItemButton_disabled {
|
||||||
|
@apply text-yellow-300
|
||||||
|
dark:hover:bg-yellow-800;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.CMS_MenuItemButton_disabled) {
|
||||||
|
@apply hover:text-white
|
||||||
|
hover:bg-yellow-500
|
||||||
|
dark:hover:text-yellow-100
|
||||||
|
dark:hover:bg-yellow-600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.CMS_MenuItemButton_error {
|
||||||
|
@apply text-red-500
|
||||||
|
dark:text-red-500;
|
||||||
|
|
||||||
|
&.CMS_MenuItemButton_disabled {
|
||||||
|
@apply text-red-200
|
||||||
|
dark:hover:bg-red-800;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.CMS_MenuItemButton_disabled) {
|
||||||
|
@apply hover:text-white
|
||||||
|
hover:bg-red-500
|
||||||
|
dark:hover:text-red-100
|
||||||
|
dark:hover:bg-red-600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_MenuItemButton_content {
|
||||||
|
@apply flex
|
||||||
|
items-center
|
||||||
|
gap-2
|
||||||
|
flex-grow;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_MenuItemButton_start-icon {
|
||||||
|
@apply h-5
|
||||||
|
w-5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_MenuItemButton_end-icon {
|
||||||
|
@apply h-5
|
||||||
|
w-5;
|
||||||
|
}
|
@ -1,10 +1,25 @@
|
|||||||
|
import { MenuItem } from '@mui/base/MenuItem';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import MenuItemUnstyled from '@mui/base/MenuItemUnstyled';
|
|
||||||
|
|
||||||
import classNames from '@staticcms/core/lib/util/classNames.util';
|
import classNames from '@staticcms/core/lib/util/classNames.util';
|
||||||
|
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
|
||||||
|
|
||||||
import type { FC, MouseEvent, ReactNode } from 'react';
|
import type { FC, MouseEvent, ReactNode } from 'react';
|
||||||
|
|
||||||
|
import './MenuItemButton.css';
|
||||||
|
|
||||||
|
export const classes = generateClassNames('MenuItemButton', [
|
||||||
|
'root',
|
||||||
|
'disabled',
|
||||||
|
'active',
|
||||||
|
'default',
|
||||||
|
'warning',
|
||||||
|
'error',
|
||||||
|
'content',
|
||||||
|
'start-icon',
|
||||||
|
'end-icon',
|
||||||
|
]);
|
||||||
|
|
||||||
export interface MenuItemButtonProps {
|
export interface MenuItemButtonProps {
|
||||||
active?: boolean;
|
active?: boolean;
|
||||||
onClick: (event: MouseEvent) => void;
|
onClick: (event: MouseEvent) => void;
|
||||||
@ -29,50 +44,17 @@ const MenuItemButton = ({
|
|||||||
'data-testid': dataTestId,
|
'data-testid': dataTestId,
|
||||||
}: MenuItemButtonProps) => {
|
}: MenuItemButtonProps) => {
|
||||||
return (
|
return (
|
||||||
<MenuItemUnstyled
|
<MenuItem
|
||||||
slotProps={{
|
slotProps={{
|
||||||
root: {
|
root: {
|
||||||
className: classNames(
|
className: classNames(
|
||||||
className,
|
className,
|
||||||
active ? 'bg-slate-200 dark:bg-slate-600' : '',
|
classes.root,
|
||||||
`
|
disabled && classes.disabled,
|
||||||
px-4
|
active && classes.active,
|
||||||
py-2
|
color === 'default' && classes.default,
|
||||||
text-sm
|
color === 'warning' && classes.warning,
|
||||||
w-full
|
color === 'error' && classes.error,
|
||||||
text-left
|
|
||||||
disabled:text-gray-300
|
|
||||||
flex
|
|
||||||
items-center
|
|
||||||
justify-between
|
|
||||||
cursor-pointer
|
|
||||||
dark:disabled:text-gray-800
|
|
||||||
`,
|
|
||||||
color === 'default' &&
|
|
||||||
`
|
|
||||||
text-gray-800
|
|
||||||
dark:text-gray-300
|
|
||||||
hover:bg-gray-200
|
|
||||||
dark:hover:bg-slate-600
|
|
||||||
`,
|
|
||||||
color === 'warning' &&
|
|
||||||
`
|
|
||||||
text-yellow-600
|
|
||||||
dark:text-yellow-500
|
|
||||||
hover:text-white
|
|
||||||
hover:bg-yellow-500
|
|
||||||
dark:hover:text-yellow-100
|
|
||||||
dark:hover:bg-yellow-600
|
|
||||||
`,
|
|
||||||
color === 'error' &&
|
|
||||||
`
|
|
||||||
text-red-500
|
|
||||||
dark:text-red-500
|
|
||||||
hover:text-white
|
|
||||||
hover:bg-red-500
|
|
||||||
dark:hover:text-red-100
|
|
||||||
dark:hover:bg-red-600
|
|
||||||
`,
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
@ -80,12 +62,12 @@ const MenuItemButton = ({
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
data-testid={dataTestId}
|
data-testid={dataTestId}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 flex-grow">
|
<div className={classes.content}>
|
||||||
{StartIcon ? <StartIcon className="h-5 w-5" /> : null}
|
{StartIcon ? <StartIcon className={classes['start-icon']} /> : null}
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
{EndIcon ? <EndIcon className="h-5 w-5" /> : null}
|
{EndIcon ? <EndIcon className={classes['end-icon']} /> : null}
|
||||||
</MenuItemUnstyled>
|
</MenuItem>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
36
packages/core/src/components/common/menu/MenuItemLink.css
Normal file
36
packages/core/src/components/common/menu/MenuItemLink.css
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
.CMS_MenuItemLink_root {
|
||||||
|
@apply px-4
|
||||||
|
py-2
|
||||||
|
text-sm
|
||||||
|
text-gray-800
|
||||||
|
dark:text-gray-300
|
||||||
|
w-full
|
||||||
|
text-left
|
||||||
|
flex
|
||||||
|
items-center
|
||||||
|
justify-between
|
||||||
|
hover:bg-slate-100
|
||||||
|
dark:hover:bg-slate-900;
|
||||||
|
|
||||||
|
&.CMS_MenuItemLink_active {
|
||||||
|
@apply bg-slate-100
|
||||||
|
dark:bg-slate-900;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_MenuItemLink_content {
|
||||||
|
@apply flex
|
||||||
|
items-center
|
||||||
|
gap-2
|
||||||
|
flex-grow;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_MenuItemLink_start-icon {
|
||||||
|
@apply h-5
|
||||||
|
w-5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_MenuItemLink_end-icon {
|
||||||
|
@apply h-5
|
||||||
|
w-5;
|
||||||
|
}
|
@ -1,11 +1,22 @@
|
|||||||
|
import { MenuItem } from '@mui/base/MenuItem';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { NavLink } from 'react-router-dom';
|
import { NavLink } from 'react-router-dom';
|
||||||
import MenuItemUnstyled from '@mui/base/MenuItemUnstyled';
|
|
||||||
|
|
||||||
import classNames from '@staticcms/core/lib/util/classNames.util';
|
import classNames from '@staticcms/core/lib/util/classNames.util';
|
||||||
|
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
|
||||||
|
|
||||||
import type { FC, ReactNode } from 'react';
|
import type { FC, ReactNode } from 'react';
|
||||||
|
|
||||||
|
import './MenuItemLink.css';
|
||||||
|
|
||||||
|
export const classes = generateClassNames('MenuItemLink', [
|
||||||
|
'root',
|
||||||
|
'active',
|
||||||
|
'content',
|
||||||
|
'start-icon',
|
||||||
|
'end-icon',
|
||||||
|
]);
|
||||||
|
|
||||||
export interface MenuItemLinkProps {
|
export interface MenuItemLinkProps {
|
||||||
href: string;
|
href: string;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
@ -24,38 +35,21 @@ const MenuItemLink = ({
|
|||||||
endIcon: EndIcon,
|
endIcon: EndIcon,
|
||||||
}: MenuItemLinkProps) => {
|
}: MenuItemLinkProps) => {
|
||||||
return (
|
return (
|
||||||
<MenuItemUnstyled
|
<NavLink to={href}>
|
||||||
component={NavLink}
|
<MenuItem
|
||||||
to={href}
|
slotProps={{
|
||||||
slotProps={{
|
root: {
|
||||||
root: {
|
className: classNames(className, classes.root, active && classes.active),
|
||||||
className: classNames(
|
},
|
||||||
className,
|
}}
|
||||||
active ? 'bg-slate-100 dark:bg-slate-900' : '',
|
>
|
||||||
`
|
<div className={classes.content}>
|
||||||
px-4
|
{StartIcon ? <StartIcon className={classes['start-icon']} /> : null}
|
||||||
py-2
|
{children}
|
||||||
text-sm
|
</div>
|
||||||
text-gray-800
|
{EndIcon ? <EndIcon className={classes['end-icon']} /> : null}
|
||||||
dark:text-gray-300
|
</MenuItem>
|
||||||
w-full
|
</NavLink>
|
||||||
text-left
|
|
||||||
flex
|
|
||||||
items-center
|
|
||||||
justify-between
|
|
||||||
hover:bg-slate-100
|
|
||||||
dark:hover:bg-slate-900
|
|
||||||
`,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2 flex-grow">
|
|
||||||
{StartIcon ? <StartIcon className="h-5 w-5" /> : null}
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
{EndIcon ? <EndIcon className="h-5 w-5" /> : null}
|
|
||||||
</MenuItemUnstyled>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import classNames from '@staticcms/core/lib/util/classNames.util';
|
import classNames from '@staticcms/core/lib/util/classNames.util';
|
||||||
|
import modalClasses from './Modal.classes';
|
||||||
|
|
||||||
const Backdrop = React.forwardRef<
|
const Backdrop = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
@ -9,18 +10,7 @@ const Backdrop = React.forwardRef<
|
|||||||
const { open, className, ownerState: _ownerState, ...other } = props;
|
const { open, className, ownerState: _ownerState, ...other } = props;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(modalClasses.backdrop, open && 'MuiBackdrop-open', className)}
|
||||||
`
|
|
||||||
fixed
|
|
||||||
inset-0
|
|
||||||
bg-black
|
|
||||||
bg-opacity-50
|
|
||||||
dark:bg-opacity-60
|
|
||||||
z-50
|
|
||||||
`,
|
|
||||||
open && 'MuiBackdrop-open',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...other}
|
{...other}
|
||||||
/>
|
/>
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
|
||||||
|
|
||||||
|
const modalClasses = generateClassNames('Modal', ['root', 'content', 'backdrop']);
|
||||||
|
|
||||||
|
export default modalClasses;
|
34
packages/core/src/components/common/modal/Modal.css
Normal file
34
packages/core/src/components/common/modal/Modal.css
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
.CMS_Modal_root {
|
||||||
|
@apply fixed
|
||||||
|
inset-0
|
||||||
|
overflow-y-auto
|
||||||
|
z-50
|
||||||
|
flex
|
||||||
|
min-h-full
|
||||||
|
items-center
|
||||||
|
justify-center
|
||||||
|
text-center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Modal_backdrop {
|
||||||
|
@apply fixed
|
||||||
|
inset-0
|
||||||
|
bg-black
|
||||||
|
bg-opacity-50
|
||||||
|
dark:bg-opacity-60
|
||||||
|
z-50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Modal_content {
|
||||||
|
@apply transform
|
||||||
|
overflow-visible
|
||||||
|
rounded-lg
|
||||||
|
text-left
|
||||||
|
align-middle
|
||||||
|
shadow-xl
|
||||||
|
transition-all
|
||||||
|
bg-white
|
||||||
|
dark:bg-slate-800
|
||||||
|
z-[51]
|
||||||
|
outline-none;
|
||||||
|
}
|
@ -1,11 +1,14 @@
|
|||||||
import ModalUnstyled from '@mui/base/ModalUnstyled';
|
import { Modal as BaseModal } from '@mui/base/Modal';
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
|
|
||||||
import classNames from '@staticcms/core/lib/util/classNames.util';
|
import classNames from '@staticcms/core/lib/util/classNames.util';
|
||||||
import Backdrop from './Backdrop';
|
import Backdrop from './Backdrop';
|
||||||
|
import modalClasses from './Modal.classes';
|
||||||
|
|
||||||
import type { FC, ReactNode } from 'react';
|
import type { FC, ReactNode } from 'react';
|
||||||
|
|
||||||
|
import './Modal.css';
|
||||||
|
|
||||||
interface ModalProps {
|
interface ModalProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
@ -19,7 +22,7 @@ const Modal: FC<ModalProps> = ({ open, children, className, onClose }) => {
|
|||||||
}, [onClose]);
|
}, [onClose]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalUnstyled
|
<BaseModal
|
||||||
open={open}
|
open={open}
|
||||||
onClose={handleClose}
|
onClose={handleClose}
|
||||||
slots={{
|
slots={{
|
||||||
@ -27,42 +30,12 @@ const Modal: FC<ModalProps> = ({ open, children, className, onClose }) => {
|
|||||||
}}
|
}}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
root: {
|
root: {
|
||||||
className: `
|
className: modalClasses.root,
|
||||||
fixed
|
|
||||||
inset-0
|
|
||||||
overflow-y-auto
|
|
||||||
z-50
|
|
||||||
flex
|
|
||||||
min-h-full
|
|
||||||
items-center
|
|
||||||
justify-center
|
|
||||||
text-center
|
|
||||||
styled-scrollbars
|
|
||||||
`,
|
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div className={classNames(modalClasses.content, className)}>{children}</div>
|
||||||
className={classNames(
|
</BaseModal>
|
||||||
`
|
|
||||||
transform
|
|
||||||
overflow-visible
|
|
||||||
rounded-lg
|
|
||||||
text-left
|
|
||||||
align-middle
|
|
||||||
shadow-xl
|
|
||||||
transition-all
|
|
||||||
bg-white
|
|
||||||
dark:bg-slate-800
|
|
||||||
z-[51]
|
|
||||||
outline-none
|
|
||||||
`,
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</ModalUnstyled>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
42
packages/core/src/components/common/pill/Pill.css
Normal file
42
packages/core/src/components/common/pill/Pill.css
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
.CMS_Pill_root {
|
||||||
|
@apply text-xs
|
||||||
|
font-medium
|
||||||
|
px-3
|
||||||
|
py-1
|
||||||
|
rounded-lg
|
||||||
|
truncate;
|
||||||
|
|
||||||
|
&.CMS_Pill_no-wrap {
|
||||||
|
@apply whitespace-nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.CMS_Pill_primary {
|
||||||
|
@apply bg-blue-700
|
||||||
|
text-gray-100
|
||||||
|
dark:bg-blue-700
|
||||||
|
dark:text-gray-100;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.CMS_Pill_default {
|
||||||
|
@apply bg-gray-200
|
||||||
|
text-gray-800
|
||||||
|
dark:bg-gray-700
|
||||||
|
dark:text-gray-100;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.CMS_Pill_disabled {
|
||||||
|
&.CMS_Pill_primary {
|
||||||
|
@apply bg-blue-300/75
|
||||||
|
text-gray-100/75
|
||||||
|
dark:bg-blue-700/25
|
||||||
|
dark:text-gray-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.CMS_Pill_default {
|
||||||
|
@apply bg-gray-100
|
||||||
|
text-gray-400/75
|
||||||
|
dark:bg-gray-800/75
|
||||||
|
dark:text-gray-500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,20 @@
|
|||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
|
|
||||||
import classNames from '@staticcms/core/lib/util/classNames.util';
|
import classNames from '@staticcms/core/lib/util/classNames.util';
|
||||||
|
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
|
||||||
|
|
||||||
import type { FC, ReactNode } from 'react';
|
import type { FC, ReactNode } from 'react';
|
||||||
|
|
||||||
|
import './Pill.css';
|
||||||
|
|
||||||
|
export const classes = generateClassNames('Pill', [
|
||||||
|
'root',
|
||||||
|
'no-wrap',
|
||||||
|
'primary',
|
||||||
|
'default',
|
||||||
|
'disabled',
|
||||||
|
]);
|
||||||
|
|
||||||
interface PillProps {
|
interface PillProps {
|
||||||
children: ReactNode | ReactNode[];
|
children: ReactNode | ReactNode[];
|
||||||
noWrap?: boolean;
|
noWrap?: boolean;
|
||||||
@ -22,48 +33,18 @@ const Pill: FC<PillProps> = ({
|
|||||||
const colorClassNames = useMemo(() => {
|
const colorClassNames = useMemo(() => {
|
||||||
switch (color) {
|
switch (color) {
|
||||||
case 'primary':
|
case 'primary':
|
||||||
return disabled
|
return classes.primary;
|
||||||
? `
|
|
||||||
bg-blue-300/75
|
|
||||||
text-gray-100/75
|
|
||||||
dark:bg-blue-700/25
|
|
||||||
dark:text-gray-500
|
|
||||||
`
|
|
||||||
: `
|
|
||||||
bg-blue-700
|
|
||||||
text-gray-100
|
|
||||||
dark:bg-blue-700
|
|
||||||
dark:text-gray-100
|
|
||||||
`;
|
|
||||||
default:
|
default:
|
||||||
return disabled
|
return classes.default;
|
||||||
? `
|
|
||||||
bg-gray-100
|
|
||||||
text-gray-400/75
|
|
||||||
dark:bg-gray-800/75
|
|
||||||
dark:text-gray-500
|
|
||||||
`
|
|
||||||
: `
|
|
||||||
bg-gray-200
|
|
||||||
text-gray-800
|
|
||||||
dark:bg-gray-700
|
|
||||||
dark:text-gray-100
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
}, [color, disabled]);
|
}, [color]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={classNames(
|
className={classNames(
|
||||||
`
|
classes.root,
|
||||||
text-xs
|
noWrap && classes['no-wrap'],
|
||||||
font-medium
|
disabled && classes.disabled,
|
||||||
px-3
|
|
||||||
py-1
|
|
||||||
rounded-lg
|
|
||||||
truncate
|
|
||||||
`,
|
|
||||||
noWrap && 'whitespace-nowrap',
|
|
||||||
colorClassNames,
|
colorClassNames,
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
|
@ -0,0 +1,21 @@
|
|||||||
|
.CMS_CircularProgress_svg {
|
||||||
|
@apply mr-2
|
||||||
|
text-gray-200
|
||||||
|
animate-spin
|
||||||
|
dark:text-gray-600
|
||||||
|
fill-blue-600;
|
||||||
|
|
||||||
|
&.CMS_CircularProgress_md {
|
||||||
|
@apply w-8
|
||||||
|
h-8;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.CMS_CircularProgress_sm {
|
||||||
|
@apply w-5
|
||||||
|
h-5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_CircularProgress_sr-label {
|
||||||
|
@apply sr-only;
|
||||||
|
}
|
@ -1,9 +1,20 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import classNames from '@staticcms/core/lib/util/classNames.util';
|
import classNames from '@staticcms/core/lib/util/classNames.util';
|
||||||
|
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
|
||||||
|
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
|
|
||||||
|
import './CircularProgress.css';
|
||||||
|
|
||||||
|
export const classes = generateClassNames('CircularProgress', [
|
||||||
|
'root',
|
||||||
|
'svg',
|
||||||
|
'md',
|
||||||
|
'sm',
|
||||||
|
'sr-label',
|
||||||
|
]);
|
||||||
|
|
||||||
export interface CircularProgressProps {
|
export interface CircularProgressProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
'data-testid'?: string;
|
'data-testid'?: string;
|
||||||
@ -16,27 +27,13 @@ const CircularProgress: FC<CircularProgressProps> = ({
|
|||||||
size = 'medium',
|
size = 'medium',
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div role="status" className={className} data-testid={dataTestId}>
|
<div role="status" className={classNames(classes.root, className)} data-testid={dataTestId}>
|
||||||
<svg
|
<svg
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
`
|
classes.svg,
|
||||||
mr-2
|
size === 'medium' && classes.md,
|
||||||
text-gray-200
|
size === 'small' && classes.sm,
|
||||||
animate-spin
|
|
||||||
dark:text-gray-600
|
|
||||||
fill-blue-600
|
|
||||||
`,
|
|
||||||
size === 'medium' &&
|
|
||||||
`
|
|
||||||
w-8
|
|
||||||
h-8
|
|
||||||
`,
|
|
||||||
size === 'small' &&
|
|
||||||
`
|
|
||||||
w-5
|
|
||||||
h-5
|
|
||||||
`,
|
|
||||||
)}
|
)}
|
||||||
viewBox="0 0 100 101"
|
viewBox="0 0 100 101"
|
||||||
fill="none"
|
fill="none"
|
||||||
@ -51,7 +48,7 @@ const CircularProgress: FC<CircularProgressProps> = ({
|
|||||||
fill="currentFill"
|
fill="currentFill"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<span className="sr-only">Loading...</span>
|
<span className={classes['sr-label']}>Loading...</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
11
packages/core/src/components/common/progress/Loader.css
Normal file
11
packages/core/src/components/common/progress/Loader.css
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
.CMS_Loader_root {
|
||||||
|
@apply absolute
|
||||||
|
inset-0
|
||||||
|
flex
|
||||||
|
flex-col
|
||||||
|
gap-2
|
||||||
|
items-center
|
||||||
|
justify-center
|
||||||
|
bg-slate-50
|
||||||
|
dark:bg-slate-900;
|
||||||
|
}
|
@ -1,7 +1,12 @@
|
|||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
|
||||||
import CircularProgress from './CircularProgress';
|
import CircularProgress from './CircularProgress';
|
||||||
|
|
||||||
|
import './Loader.css';
|
||||||
|
|
||||||
|
export const classes = generateClassNames('Loader', ['root']);
|
||||||
|
|
||||||
export interface LoaderProps {
|
export interface LoaderProps {
|
||||||
children: string | string[] | undefined;
|
children: string | string[] | undefined;
|
||||||
}
|
}
|
||||||
@ -35,19 +40,7 @@ const Loader = ({ children }: LoaderProps) => {
|
|||||||
}, [children, currentItem]);
|
}, [children, currentItem]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={classes.root}>
|
||||||
className="
|
|
||||||
absolute
|
|
||||||
inset-0
|
|
||||||
flex
|
|
||||||
flex-col
|
|
||||||
gap-2
|
|
||||||
items-center
|
|
||||||
justify-center
|
|
||||||
bg-slate-50
|
|
||||||
dark:bg-slate-900
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<CircularProgress />
|
<CircularProgress />
|
||||||
<div>{text}</div>
|
<div>{text}</div>
|
||||||
</div>
|
</div>
|
||||||
|
29
packages/core/src/components/common/select/Option.css
Normal file
29
packages/core/src/components/common/select/Option.css
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
.CMS_SelectOption_root {
|
||||||
|
@apply relative
|
||||||
|
select-none
|
||||||
|
py-2
|
||||||
|
px-4
|
||||||
|
cursor-pointer
|
||||||
|
text-gray-800
|
||||||
|
hover:bg-blue-500
|
||||||
|
dark:text-gray-100;
|
||||||
|
|
||||||
|
&.CMS_SelectOption_selected {
|
||||||
|
@apply bg-blue-400/75;
|
||||||
|
|
||||||
|
& .CMS_SelectOption_label {
|
||||||
|
@apply font-medium;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.CMS_SelectOption_selected) {
|
||||||
|
& .CMS_SelectOption_label {
|
||||||
|
@apply font-normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_SelectOption_label {
|
||||||
|
@apply block
|
||||||
|
truncate;
|
||||||
|
}
|
@ -1,11 +1,16 @@
|
|||||||
import OptionUnstyled from '@mui/base/OptionUnstyled';
|
import { Option as BaseOption } from '@mui/base/Option';
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
|
|
||||||
import classNames from '@staticcms/core/lib/util/classNames.util';
|
import classNames from '@staticcms/core/lib/util/classNames.util';
|
||||||
import { isNotNullish } from '@staticcms/core/lib/util/null.util';
|
import { isNotNullish } from '@staticcms/core/lib/util/null.util';
|
||||||
|
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
|
||||||
|
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
|
import './Option.css';
|
||||||
|
|
||||||
|
export const classes = generateClassNames('SelectOption', ['root', 'selected', 'label']);
|
||||||
|
|
||||||
export interface OptionProps<T> {
|
export interface OptionProps<T> {
|
||||||
selectedValue: T | null | T[];
|
selectedValue: T | null | T[];
|
||||||
value: T | null;
|
value: T | null;
|
||||||
@ -28,31 +33,17 @@ const Option = function <T>({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OptionUnstyled
|
<BaseOption
|
||||||
value={value}
|
value={value}
|
||||||
data-testid={dataTestId}
|
data-testid={dataTestId}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
root: {
|
root: {
|
||||||
className: classNames(
|
className: classNames(classes.root, selected && classes.selected),
|
||||||
`
|
|
||||||
relative
|
|
||||||
select-none
|
|
||||||
py-2
|
|
||||||
px-4
|
|
||||||
cursor-pointer
|
|
||||||
text-gray-800
|
|
||||||
hover:bg-blue-500
|
|
||||||
dark:text-gray-100
|
|
||||||
`,
|
|
||||||
selected ? 'bg-blue-400/75' : '',
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className={classNames('block truncate', selected ? 'font-medium' : 'font-normal')}>
|
<span className={classes.label}>{children}</span>
|
||||||
{children}
|
</BaseOption>
|
||||||
</span>
|
|
||||||
</OptionUnstyled>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
81
packages/core/src/components/common/select/Select.css
Normal file
81
packages/core/src/components/common/select/Select.css
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
.CMS_Select_root {
|
||||||
|
@apply relative
|
||||||
|
w-full;
|
||||||
|
|
||||||
|
&.CMS_Select_disabled {
|
||||||
|
& .CMS_Select_input {
|
||||||
|
@apply text-gray-300/75
|
||||||
|
dark:text-gray-600/75;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .CMS_Select_value {
|
||||||
|
& .CMS_Select_dropdown {
|
||||||
|
& .CMS_Select_dropdown-icon {
|
||||||
|
@apply text-gray-300/75
|
||||||
|
dark:text-gray-600/75;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Select_value {
|
||||||
|
@apply w-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Select_label {
|
||||||
|
@apply flex
|
||||||
|
w-select-widget-label;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Select_label-text {
|
||||||
|
@apply truncate;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Select_dropdown {
|
||||||
|
@apply pointer-events-none
|
||||||
|
absolute
|
||||||
|
inset-y-0
|
||||||
|
right-0
|
||||||
|
flex
|
||||||
|
items-center
|
||||||
|
pr-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Select_dropdown-icon {
|
||||||
|
@apply h-5
|
||||||
|
w-5
|
||||||
|
text-gray-400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Select_input {
|
||||||
|
@apply flex
|
||||||
|
items-center
|
||||||
|
text-sm
|
||||||
|
font-medium
|
||||||
|
relative
|
||||||
|
min-h-8
|
||||||
|
px-4
|
||||||
|
py-1.5
|
||||||
|
w-full
|
||||||
|
text-gray-800
|
||||||
|
dark:text-gray-100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Select_popper {
|
||||||
|
@apply max-h-60
|
||||||
|
overflow-auto
|
||||||
|
rounded-md
|
||||||
|
bg-white
|
||||||
|
py-1
|
||||||
|
text-base
|
||||||
|
shadow-md
|
||||||
|
ring-1
|
||||||
|
ring-black
|
||||||
|
ring-opacity-5
|
||||||
|
focus:outline-none
|
||||||
|
sm:text-sm
|
||||||
|
z-[100]
|
||||||
|
dark:bg-slate-700
|
||||||
|
dark:shadow-lg;
|
||||||
|
}
|
@ -1,14 +1,31 @@
|
|||||||
import SelectUnstyled from '@mui/base/SelectUnstyled';
|
import { Select as BaseSelect } from '@mui/base/Select';
|
||||||
|
import ClickAwayListener from '@mui/material/ClickAwayListener';
|
||||||
import { KeyboardArrowDown as KeyboardArrowDownIcon } from '@styled-icons/material/KeyboardArrowDown';
|
import { KeyboardArrowDown as KeyboardArrowDownIcon } from '@styled-icons/material/KeyboardArrowDown';
|
||||||
import React, { forwardRef, useCallback, useState } from 'react';
|
import React, { forwardRef, useCallback, useState } from 'react';
|
||||||
|
|
||||||
import useElementSize from '@staticcms/core/lib/hooks/useElementSize';
|
import useElementSize from '@staticcms/core/lib/hooks/useElementSize';
|
||||||
import classNames from '@staticcms/core/lib/util/classNames.util';
|
import classNames from '@staticcms/core/lib/util/classNames.util';
|
||||||
import { isNotEmpty } from '@staticcms/core/lib/util/string.util';
|
import { isNotEmpty } from '@staticcms/core/lib/util/string.util';
|
||||||
|
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
|
||||||
import Option from './Option';
|
import Option from './Option';
|
||||||
|
|
||||||
import type { FocusEvent, KeyboardEvent, MouseEvent, ReactNode, Ref } from 'react';
|
import type { FocusEvent, KeyboardEvent, MouseEvent, ReactNode, Ref } from 'react';
|
||||||
|
|
||||||
|
import './Select.css';
|
||||||
|
|
||||||
|
export const classes = generateClassNames('Select', [
|
||||||
|
'root',
|
||||||
|
'disabled',
|
||||||
|
'input',
|
||||||
|
'value',
|
||||||
|
'label',
|
||||||
|
'label-text',
|
||||||
|
'dropdown',
|
||||||
|
'dropdown-icon',
|
||||||
|
'input',
|
||||||
|
'popper',
|
||||||
|
]);
|
||||||
|
|
||||||
export interface Option {
|
export interface Option {
|
||||||
label: string;
|
label: string;
|
||||||
value: number | string;
|
value: number | string;
|
||||||
@ -31,6 +48,7 @@ export interface SelectProps {
|
|||||||
options: (number | string)[] | Option[];
|
options: (number | string)[] | Option[];
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
rootClassName?: string;
|
||||||
onChange: SelectChangeEventHandler;
|
onChange: SelectChangeEventHandler;
|
||||||
onOpenChange?: (open: boolean) => void;
|
onOpenChange?: (open: boolean) => void;
|
||||||
}
|
}
|
||||||
@ -44,6 +62,7 @@ const Select = forwardRef(
|
|||||||
options,
|
options,
|
||||||
required = false,
|
required = false,
|
||||||
disabled,
|
disabled,
|
||||||
|
rootClassName,
|
||||||
onChange,
|
onChange,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
}: SelectProps,
|
}: SelectProps,
|
||||||
@ -82,120 +101,79 @@ const Select = forwardRef(
|
|||||||
[onChange, value],
|
[onChange, value],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
const handleClick = useCallback(
|
||||||
<div className="relative w-full">
|
(event: MouseEvent) => {
|
||||||
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
|
event.stopPropagation();
|
||||||
<SelectUnstyled<any>
|
event.preventDefault();
|
||||||
renderValue={() => {
|
handleOpenChange(!open);
|
||||||
return (
|
},
|
||||||
<div className="w-full">
|
[handleOpenChange, open],
|
||||||
<div className="flex flex-start w-select-widget-label">
|
);
|
||||||
<span className="truncate">{label ?? placeholder}</span>
|
|
||||||
</div>
|
|
||||||
<span
|
|
||||||
className="
|
|
||||||
pointer-events-none
|
|
||||||
absolute
|
|
||||||
inset-y-0
|
|
||||||
right-0
|
|
||||||
flex
|
|
||||||
items-center
|
|
||||||
pr-2
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<KeyboardArrowDownIcon
|
|
||||||
className={classNames(
|
|
||||||
`
|
|
||||||
h-5
|
|
||||||
w-5
|
|
||||||
text-gray-400
|
|
||||||
`,
|
|
||||||
disabled &&
|
|
||||||
`
|
|
||||||
text-gray-300/75
|
|
||||||
dark:text-gray-600/75
|
|
||||||
`,
|
|
||||||
)}
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
slotProps={{
|
|
||||||
root: {
|
|
||||||
ref,
|
|
||||||
className: classNames(
|
|
||||||
`
|
|
||||||
flex
|
|
||||||
items-center
|
|
||||||
text-sm
|
|
||||||
font-medium
|
|
||||||
relative
|
|
||||||
min-h-8
|
|
||||||
px-4
|
|
||||||
py-1.5
|
|
||||||
w-full
|
|
||||||
text-gray-800
|
|
||||||
dark:text-gray-100
|
|
||||||
`,
|
|
||||||
disabled &&
|
|
||||||
`
|
|
||||||
text-gray-300/75
|
|
||||||
dark:text-gray-600/75
|
|
||||||
`,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
popper: {
|
|
||||||
className: `
|
|
||||||
max-h-60
|
|
||||||
overflow-auto
|
|
||||||
rounded-md
|
|
||||||
bg-white
|
|
||||||
py-1
|
|
||||||
text-base
|
|
||||||
shadow-md
|
|
||||||
ring-1
|
|
||||||
ring-black
|
|
||||||
ring-opacity-5
|
|
||||||
focus:outline-none
|
|
||||||
sm:text-sm
|
|
||||||
z-[100]
|
|
||||||
dark:bg-slate-700
|
|
||||||
dark:shadow-lg
|
|
||||||
`,
|
|
||||||
style: { width: ref ? width : 'auto' },
|
|
||||||
disablePortal: false,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
value={value}
|
|
||||||
disabled={disabled}
|
|
||||||
onChange={handleChange}
|
|
||||||
listboxOpen={open}
|
|
||||||
onListboxOpenChange={handleOpenChange}
|
|
||||||
data-testid="select-input"
|
|
||||||
>
|
|
||||||
{!Array.isArray(value) && !required ? (
|
|
||||||
<Option value="" selectedValue={value}>
|
|
||||||
<i>None</i>
|
|
||||||
</Option>
|
|
||||||
) : null}
|
|
||||||
{options.map((option, index) => {
|
|
||||||
const { label: optionLabel, value: optionValue } = getOptionLabelAndValue(option);
|
|
||||||
|
|
||||||
return (
|
const handleClickAway = useCallback(() => {
|
||||||
<Option
|
handleOpenChange(false);
|
||||||
key={index}
|
}, [handleOpenChange]);
|
||||||
value={optionValue}
|
|
||||||
selectedValue={value}
|
return (
|
||||||
data-testid={`select-option-${optionValue}`}
|
<ClickAwayListener onClickAway={handleClickAway}>
|
||||||
>
|
<div className={classNames(classes.root, rootClassName)}>
|
||||||
{optionLabel}
|
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
|
||||||
|
<BaseSelect<any>
|
||||||
|
renderValue={() => {
|
||||||
|
return (
|
||||||
|
<div className={classes.value}>
|
||||||
|
<div className={classes.label}>
|
||||||
|
<span className={classes['label-text']}>{label ?? placeholder}</span>
|
||||||
|
</div>
|
||||||
|
<span className={classes.dropdown}>
|
||||||
|
<KeyboardArrowDownIcon
|
||||||
|
className={classes['dropdown-icon']}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
ref={ref}
|
||||||
|
onClick={handleClick}
|
||||||
|
slotProps={{
|
||||||
|
root: {
|
||||||
|
className: classes.input,
|
||||||
|
},
|
||||||
|
popper: {
|
||||||
|
className: classes.popper,
|
||||||
|
style: { width: ref ? width : 'auto' },
|
||||||
|
disablePortal: false,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
value={value}
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={handleChange}
|
||||||
|
listboxOpen={open}
|
||||||
|
data-testid="select-input"
|
||||||
|
>
|
||||||
|
{!Array.isArray(value) && !required ? (
|
||||||
|
<Option value="" selectedValue={value}>
|
||||||
|
<i>None</i>
|
||||||
</Option>
|
</Option>
|
||||||
);
|
) : null}
|
||||||
})}
|
{options.map((option, index) => {
|
||||||
</SelectUnstyled>
|
const { label: optionLabel, value: optionValue } = getOptionLabelAndValue(option);
|
||||||
</div>
|
|
||||||
|
return (
|
||||||
|
<Option
|
||||||
|
key={index}
|
||||||
|
value={optionValue}
|
||||||
|
selectedValue={value}
|
||||||
|
data-testid={`select-option-${optionValue}`}
|
||||||
|
>
|
||||||
|
{optionLabel}
|
||||||
|
</Option>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</BaseSelect>
|
||||||
|
</div>
|
||||||
|
</ClickAwayListener>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
65
packages/core/src/components/common/switch/Switch.css
Normal file
65
packages/core/src/components/common/switch/Switch.css
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
.CMS_Switch_root {
|
||||||
|
@apply relative
|
||||||
|
inline-flex
|
||||||
|
items-center
|
||||||
|
cursor-pointer;
|
||||||
|
|
||||||
|
&.CMS_Switch_disabled {
|
||||||
|
@apply cursor-default;
|
||||||
|
|
||||||
|
& .CMS_Switch_toggle {
|
||||||
|
@apply peer-checked:bg-blue-600/25
|
||||||
|
after:bg-gray-500/75
|
||||||
|
after:border-gray-500/75
|
||||||
|
peer-checked:after:border-gray-500/75;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.CMS_Switch_disabled) {
|
||||||
|
& .CMS_Switch_toggle {
|
||||||
|
@apply after:bg-white
|
||||||
|
after:border-gray-300;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Switch_input {
|
||||||
|
@apply sr-only;
|
||||||
|
|
||||||
|
&:focus + .CMS_Switch_toggle {
|
||||||
|
@apply ring-4
|
||||||
|
ring-blue-300
|
||||||
|
dark:ring-blue-800;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:checked + .CMS_Switch_toggle {
|
||||||
|
@apply bg-blue-600
|
||||||
|
after:border-white
|
||||||
|
after:translate-x-full;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Switch_toggle {
|
||||||
|
@apply w-11
|
||||||
|
h-6
|
||||||
|
bg-slate-200
|
||||||
|
rounded-full
|
||||||
|
dark:bg-slate-700
|
||||||
|
after:content-['']
|
||||||
|
after:absolute after:top-0.5
|
||||||
|
after:left-[2px]
|
||||||
|
after:border
|
||||||
|
after:rounded-full
|
||||||
|
after:h-5
|
||||||
|
after:w-5
|
||||||
|
after:transition-all
|
||||||
|
dark:border-gray-600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Switch_label {
|
||||||
|
@apply ml-3
|
||||||
|
text-sm
|
||||||
|
font-medium
|
||||||
|
text-gray-800
|
||||||
|
dark:text-gray-300;
|
||||||
|
}
|
@ -1,18 +1,31 @@
|
|||||||
import React, { forwardRef, useCallback } from 'react';
|
import React, { forwardRef, useCallback } from 'react';
|
||||||
|
|
||||||
import classNames from '@staticcms/core/lib/util/classNames.util';
|
import classNames from '@staticcms/core/lib/util/classNames.util';
|
||||||
|
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
|
||||||
|
|
||||||
import type { ChangeEvent, ChangeEventHandler } from 'react';
|
import type { ChangeEvent, ChangeEventHandler } from 'react';
|
||||||
|
|
||||||
|
import './Switch.css';
|
||||||
|
|
||||||
|
export const classes = generateClassNames('Switch', [
|
||||||
|
'root',
|
||||||
|
'disabled',
|
||||||
|
'input',
|
||||||
|
'toggle',
|
||||||
|
'label',
|
||||||
|
]);
|
||||||
|
|
||||||
export interface SwitchProps {
|
export interface SwitchProps {
|
||||||
label?: string;
|
label?: string;
|
||||||
value: boolean;
|
value: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
rootClassName?: string;
|
||||||
|
inputClassName?: string;
|
||||||
onChange?: ChangeEventHandler<HTMLInputElement>;
|
onChange?: ChangeEventHandler<HTMLInputElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Switch = forwardRef<HTMLInputElement | null, SwitchProps>(
|
const Switch = forwardRef<HTMLInputElement | null, SwitchProps>(
|
||||||
({ label, value, disabled, onChange }, ref) => {
|
({ label, value, disabled, rootClassName, inputClassName, onChange }, ref) => {
|
||||||
const handleChange = useCallback(
|
const handleChange = useCallback(
|
||||||
(event: ChangeEvent<HTMLInputElement>) => {
|
(event: ChangeEvent<HTMLInputElement>) => {
|
||||||
onChange?.(event);
|
onChange?.(event);
|
||||||
@ -21,78 +34,19 @@ const Switch = forwardRef<HTMLInputElement | null, SwitchProps>(
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<label
|
<label className={classNames(classes.root, disabled && classes.disabled, rootClassName)}>
|
||||||
className={classNames(
|
|
||||||
`
|
|
||||||
relative
|
|
||||||
inline-flex
|
|
||||||
items-center
|
|
||||||
cursor-pointer
|
|
||||||
`,
|
|
||||||
disabled && 'cursor-default',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<input
|
<input
|
||||||
data-testid="switch-input"
|
data-testid="switch-input"
|
||||||
ref={ref}
|
ref={ref}
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={value}
|
checked={value}
|
||||||
className="sr-only peer"
|
className={classNames(classes.input, inputClassName)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
onClick={() => false}
|
onClick={() => false}
|
||||||
/>
|
/>
|
||||||
<div
|
<div className={classes.toggle} />
|
||||||
className={classNames(
|
{label ? <span className={classes.label}>{label}</span> : null}
|
||||||
`
|
|
||||||
w-11
|
|
||||||
h-6
|
|
||||||
bg-slate-200
|
|
||||||
rounded-full
|
|
||||||
peer
|
|
||||||
peer-focus:ring-4
|
|
||||||
peer-focus:ring-blue-300
|
|
||||||
dark:peer-focus:ring-blue-800
|
|
||||||
dark:bg-slate-700
|
|
||||||
peer-checked:after:translate-x-full
|
|
||||||
after:content-['']
|
|
||||||
after:absolute after:top-0.5
|
|
||||||
after:left-[2px]
|
|
||||||
after:border
|
|
||||||
after:rounded-full
|
|
||||||
after:h-5
|
|
||||||
after:w-5
|
|
||||||
after:transition-all
|
|
||||||
dark:border-gray-600
|
|
||||||
`,
|
|
||||||
disabled
|
|
||||||
? `
|
|
||||||
peer-checked:bg-blue-600/25
|
|
||||||
after:bg-gray-500/75
|
|
||||||
after:border-gray-500/75
|
|
||||||
peer-checked:after:border-gray-500/75
|
|
||||||
`
|
|
||||||
: `
|
|
||||||
peer-checked:bg-blue-600
|
|
||||||
after:bg-white
|
|
||||||
after:border-gray-300
|
|
||||||
peer-checked:after:border-white
|
|
||||||
`,
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{label ? (
|
|
||||||
<span
|
|
||||||
className="
|
|
||||||
ml-3
|
|
||||||
text-sm
|
|
||||||
font-medium
|
|
||||||
text-gray-800
|
|
||||||
dark:text-gray-300
|
|
||||||
"
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
</label>
|
</label>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
20
packages/core/src/components/common/table/Table.classes.ts
Normal file
20
packages/core/src/components/common/table/Table.classes.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
|
||||||
|
|
||||||
|
const tableClasses = generateClassNames('Table', [
|
||||||
|
'root',
|
||||||
|
'table',
|
||||||
|
'header',
|
||||||
|
'header-row',
|
||||||
|
'header-cell',
|
||||||
|
'header-cell-content',
|
||||||
|
'body',
|
||||||
|
'body-row',
|
||||||
|
'body-cell',
|
||||||
|
'body-cell-has-link',
|
||||||
|
'body-cell-emphasis',
|
||||||
|
'body-cell-shrink',
|
||||||
|
'body-cell-content',
|
||||||
|
'body-cell-link',
|
||||||
|
]);
|
||||||
|
|
||||||
|
export default tableClasses;
|
96
packages/core/src/components/common/table/Table.css
Normal file
96
packages/core/src/components/common/table/Table.css
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
.CMS_Table_root {
|
||||||
|
@apply z-[2];
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Table_table {
|
||||||
|
@apply w-full
|
||||||
|
text-sm
|
||||||
|
text-left
|
||||||
|
text-gray-500
|
||||||
|
dark:text-gray-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Table_header {
|
||||||
|
@apply text-xs;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Table_header-row {
|
||||||
|
@apply shadow-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Table_header-cell {
|
||||||
|
@apply font-bold
|
||||||
|
sticky
|
||||||
|
top-0
|
||||||
|
border-0
|
||||||
|
p-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Table_header-cell-content {
|
||||||
|
@apply px-4
|
||||||
|
py-3
|
||||||
|
text-gray-800
|
||||||
|
border-gray-100
|
||||||
|
border-b
|
||||||
|
bg-white
|
||||||
|
dark:text-white
|
||||||
|
dark:border-gray-700
|
||||||
|
dark:bg-slate-800
|
||||||
|
text-[14px]
|
||||||
|
truncate
|
||||||
|
w-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Table_body-row {
|
||||||
|
@apply border-t
|
||||||
|
first:border-t-0
|
||||||
|
border-gray-100
|
||||||
|
dark:border-gray-700
|
||||||
|
bg-white
|
||||||
|
hover:bg-slate-50
|
||||||
|
dark:bg-slate-800
|
||||||
|
dark:hover:bg-slate-700
|
||||||
|
focus:outline-none
|
||||||
|
focus:bg-gray-100
|
||||||
|
focus:dark:bg-slate-700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Table_body-cell {
|
||||||
|
@apply text-gray-500
|
||||||
|
dark:text-gray-300;
|
||||||
|
|
||||||
|
&.CMS_Table_body-cell-has-link {
|
||||||
|
@apply p-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.CMS_Table_body-cell-has-link) {
|
||||||
|
@apply px-4
|
||||||
|
py-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.CMS_Table_body-cell-emphasis {
|
||||||
|
@apply font-medium
|
||||||
|
text-gray-800
|
||||||
|
whitespace-nowrap
|
||||||
|
dark:text-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.CMS_Table_body-cell-shrink {
|
||||||
|
@apply w-0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Table_body-cell-content {
|
||||||
|
@apply h-[44px]
|
||||||
|
truncate
|
||||||
|
w-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_Table_body-cell-link {
|
||||||
|
@apply w-full
|
||||||
|
h-full
|
||||||
|
flex
|
||||||
|
px-4
|
||||||
|
py-3
|
||||||
|
whitespace-nowrap;
|
||||||
|
}
|
@ -1,9 +1,12 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import tableClasses from './Table.classes';
|
||||||
import TableHeaderCell from './TableHeaderCell';
|
import TableHeaderCell from './TableHeaderCell';
|
||||||
|
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
|
import './Table.css';
|
||||||
|
|
||||||
interface TableCellProps {
|
interface TableCellProps {
|
||||||
columns: ReactNode[];
|
columns: ReactNode[];
|
||||||
children: ReactNode[];
|
children: ReactNode[];
|
||||||
@ -11,20 +14,16 @@ interface TableCellProps {
|
|||||||
|
|
||||||
const TableCell = ({ columns, children }: TableCellProps) => {
|
const TableCell = ({ columns, children }: TableCellProps) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={tableClasses.root}>
|
||||||
className="
|
<table className={tableClasses.table}>
|
||||||
z-[2]
|
<thead className={tableClasses.header}>
|
||||||
"
|
<tr className={tableClasses['header-row']}>
|
||||||
>
|
|
||||||
<table className="w-full text-sm text-left text-gray-500 dark:text-gray-300">
|
|
||||||
<thead className="text-xs">
|
|
||||||
<tr className="shadow-sm">
|
|
||||||
{columns.map((column, index) => (
|
{columns.map((column, index) => (
|
||||||
<TableHeaderCell key={index}>{column}</TableHeaderCell>
|
<TableHeaderCell key={index}>{column}</TableHeaderCell>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>{children}</tbody>
|
<tbody className={tableClasses.body}>{children}</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -2,6 +2,7 @@ import React, { useMemo } from 'react';
|
|||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import classNames from '@staticcms/core/lib/util/classNames.util';
|
import classNames from '@staticcms/core/lib/util/classNames.util';
|
||||||
|
import tableClasses from './Table.classes';
|
||||||
|
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
@ -16,18 +17,7 @@ const TableCell = ({ children, emphasis = false, to, shrink = false }: TableCell
|
|||||||
const content = useMemo(() => {
|
const content = useMemo(() => {
|
||||||
if (to) {
|
if (to) {
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link to={to} className={tableClasses['body-cell-link']} tabIndex={-1}>
|
||||||
to={to}
|
|
||||||
className="
|
|
||||||
w-full
|
|
||||||
h-full
|
|
||||||
flex
|
|
||||||
px-4
|
|
||||||
py-3
|
|
||||||
whitespace-nowrap
|
|
||||||
"
|
|
||||||
tabIndex={-1}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
@ -39,24 +29,13 @@ const TableCell = ({ children, emphasis = false, to, shrink = false }: TableCell
|
|||||||
return (
|
return (
|
||||||
<td
|
<td
|
||||||
className={classNames(
|
className={classNames(
|
||||||
!to ? 'px-4 py-3' : 'p-0',
|
tableClasses['body-cell'],
|
||||||
`
|
to && tableClasses['body-cell-has-link'],
|
||||||
text-gray-500
|
emphasis && tableClasses['body-cell-emphasis'],
|
||||||
dark:text-gray-300
|
shrink && tableClasses['body-cell-shrink'],
|
||||||
`,
|
|
||||||
emphasis && 'font-medium text-gray-800 whitespace-nowrap dark:text-white',
|
|
||||||
shrink && 'w-0',
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div
|
<div className={tableClasses['body-cell-content']}>{content}</div>
|
||||||
className="
|
|
||||||
h-[44px]
|
|
||||||
truncate
|
|
||||||
w-full
|
|
||||||
"
|
|
||||||
>
|
|
||||||
{content}
|
|
||||||
</div>
|
|
||||||
</td>
|
</td>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import classNames from '@staticcms/core/lib/util/classNames.util';
|
|
||||||
import { isEmpty } from '@staticcms/core/lib/util/string.util';
|
import { isEmpty } from '@staticcms/core/lib/util/string.util';
|
||||||
|
import tableClasses from './Table.classes';
|
||||||
|
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
@ -11,34 +11,8 @@ interface TableHeaderCellProps {
|
|||||||
|
|
||||||
const TableHeaderCell = ({ children }: TableHeaderCellProps) => {
|
const TableHeaderCell = ({ children }: TableHeaderCellProps) => {
|
||||||
return (
|
return (
|
||||||
<th
|
<th scope="col" className={tableClasses['header-cell']}>
|
||||||
scope="col"
|
<div className={tableClasses['header-cell-content']}>
|
||||||
className={classNames(
|
|
||||||
`
|
|
||||||
font-bold
|
|
||||||
sticky
|
|
||||||
top-0
|
|
||||||
border-0
|
|
||||||
p-0
|
|
||||||
`,
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="
|
|
||||||
px-4
|
|
||||||
py-3
|
|
||||||
text-gray-800
|
|
||||||
border-gray-100
|
|
||||||
border-b
|
|
||||||
bg-white
|
|
||||||
dark:text-white
|
|
||||||
dark:border-gray-700
|
|
||||||
dark:bg-slate-800
|
|
||||||
text-[14px]
|
|
||||||
truncate
|
|
||||||
w-full
|
|
||||||
"
|
|
||||||
>
|
|
||||||
{typeof children === 'string' && isEmpty(children) ? <> </> : children}
|
{typeof children === 'string' && isEmpty(children) ? <> </> : children}
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
|
@ -2,6 +2,7 @@ import React, { useCallback } from 'react';
|
|||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import classNames from '@staticcms/core/lib/util/classNames.util';
|
import classNames from '@staticcms/core/lib/util/classNames.util';
|
||||||
|
import tableClasses from './Table.classes';
|
||||||
|
|
||||||
import type { KeyboardEvent, ReactNode } from 'react';
|
import type { KeyboardEvent, ReactNode } from 'react';
|
||||||
|
|
||||||
@ -28,22 +29,7 @@ const TableRow = ({ children, className, to }: TableRowProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<tr
|
<tr
|
||||||
className={classNames(
|
className={classNames(tableClasses['body-row'], className)}
|
||||||
`
|
|
||||||
border-t
|
|
||||||
first:border-t-0
|
|
||||||
border-gray-100
|
|
||||||
dark:border-gray-700
|
|
||||||
bg-white
|
|
||||||
hover:bg-slate-50
|
|
||||||
dark:bg-slate-800
|
|
||||||
dark:hover:bg-slate-700
|
|
||||||
focus:outline-none
|
|
||||||
focus:bg-gray-100
|
|
||||||
focus:dark:bg-slate-700
|
|
||||||
`,
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
tabIndex={to ? 0 : -1}
|
tabIndex={to ? 0 : -1}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
>
|
>
|
||||||
|
18
packages/core/src/components/common/text-field/TextArea.css
Normal file
18
packages/core/src/components/common/text-field/TextArea.css
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
.CMS_TextArea_root {
|
||||||
|
@apply flex
|
||||||
|
w-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CMS_TextArea_input {
|
||||||
|
@apply w-full
|
||||||
|
min-h-[80px]
|
||||||
|
px-3
|
||||||
|
bg-transparent
|
||||||
|
outline-none
|
||||||
|
text-sm
|
||||||
|
font-medium
|
||||||
|
text-gray-800
|
||||||
|
dark:text-gray-100
|
||||||
|
disabled:text-gray-300
|
||||||
|
dark:disabled:text-gray-500;
|
||||||
|
}
|
@ -1,13 +1,21 @@
|
|||||||
import InputUnstyled from '@mui/base/InputUnstyled';
|
import { Input } from '@mui/base/Input';
|
||||||
import React, { forwardRef, useCallback, useState } from 'react';
|
import React, { forwardRef, useCallback, useLayoutEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import classNames from '@staticcms/core/lib/util/classNames.util';
|
||||||
|
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
|
||||||
|
|
||||||
import type { ChangeEventHandler, RefObject } from 'react';
|
import type { ChangeEventHandler, RefObject } from 'react';
|
||||||
|
|
||||||
|
import './TextArea.css';
|
||||||
|
|
||||||
|
export const classes = generateClassNames('TextArea', ['root', 'input']);
|
||||||
|
|
||||||
export interface TextAreaProps {
|
export interface TextAreaProps {
|
||||||
value: string;
|
value: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
className?: string;
|
rootClassName?: string;
|
||||||
|
inputClassName?: string;
|
||||||
'data-testid'?: string;
|
'data-testid'?: string;
|
||||||
onChange: ChangeEventHandler<HTMLInputElement>;
|
onChange: ChangeEventHandler<HTMLInputElement>;
|
||||||
}
|
}
|
||||||
@ -20,7 +28,18 @@ function getHeight(rawHeight: string): number {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const TextArea = forwardRef<HTMLInputElement | null, TextAreaProps>(
|
const TextArea = forwardRef<HTMLInputElement | null, TextAreaProps>(
|
||||||
({ value, disabled, placeholder, className, 'data-testid': dataTestId, onChange }, ref) => {
|
(
|
||||||
|
{
|
||||||
|
value,
|
||||||
|
disabled,
|
||||||
|
placeholder,
|
||||||
|
rootClassName,
|
||||||
|
inputClassName,
|
||||||
|
'data-testid': dataTestId,
|
||||||
|
onChange,
|
||||||
|
},
|
||||||
|
ref,
|
||||||
|
) => {
|
||||||
const [lastAutogrowHeight, setLastAutogrowHeight] = useState(MIN_TEXT_AREA_HEIGHT);
|
const [lastAutogrowHeight, setLastAutogrowHeight] = useState(MIN_TEXT_AREA_HEIGHT);
|
||||||
|
|
||||||
const autoGrow = useCallback(() => {
|
const autoGrow = useCallback(() => {
|
||||||
@ -43,16 +62,22 @@ const TextArea = forwardRef<HTMLInputElement | null, TextAreaProps>(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
textarea.style.height = `${newHeight}px`;
|
|
||||||
setLastAutogrowHeight(newHeight);
|
|
||||||
|
|
||||||
if (newHeight > MIN_TEXT_AREA_HEIGHT - MIN_BOTTOM_PADDING) {
|
if (newHeight > MIN_TEXT_AREA_HEIGHT - MIN_BOTTOM_PADDING) {
|
||||||
textarea.style.paddingBottom = `${MIN_BOTTOM_PADDING}px`;
|
textarea.style.paddingBottom = `${MIN_BOTTOM_PADDING}px`;
|
||||||
|
newHeight += MIN_BOTTOM_PADDING;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
textarea.style.height = `${newHeight}px`;
|
||||||
|
setLastAutogrowHeight(newHeight);
|
||||||
}, [lastAutogrowHeight, ref]);
|
}, [lastAutogrowHeight, ref]);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
autoGrow();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InputUnstyled
|
<Input
|
||||||
multiline
|
multiline
|
||||||
minRows={4}
|
minRows={4}
|
||||||
onInput={autoGrow}
|
onInput={autoGrow}
|
||||||
@ -62,28 +87,12 @@ const TextArea = forwardRef<HTMLInputElement | null, TextAreaProps>(
|
|||||||
data-testid={dataTestId ?? 'textarea-input'}
|
data-testid={dataTestId ?? 'textarea-input'}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
root: {
|
root: {
|
||||||
className: `
|
className: classNames(classes.root, rootClassName),
|
||||||
flex
|
|
||||||
w-full
|
|
||||||
${className}
|
|
||||||
`,
|
|
||||||
},
|
},
|
||||||
input: {
|
input: {
|
||||||
ref,
|
ref,
|
||||||
placeholder,
|
placeholder,
|
||||||
className: `
|
className: classNames(classes.input, inputClassName),
|
||||||
w-full
|
|
||||||
min-h-[80px]
|
|
||||||
px-3
|
|
||||||
bg-transparent
|
|
||||||
outline-none
|
|
||||||
text-sm
|
|
||||||
font-medium
|
|
||||||
text-gray-800
|
|
||||||
dark:text-gray-100
|
|
||||||
disabled:text-gray-300
|
|
||||||
dark:disabled:text-gray-500
|
|
||||||
`,
|
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user