feat: standardize class names (#873)
This commit is contained in:
parent
7e1734aab6
commit
1338ad2f57
@ -66,11 +66,10 @@
|
||||
"@emotion/css": "11.10.6",
|
||||
"@emotion/react": "11.10.6",
|
||||
"@emotion/styled": "11.10.6",
|
||||
"@headlessui/react": "1.7.7",
|
||||
"@lezer/common": "1.0.2",
|
||||
"@mdx-js/mdx": "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/system": "5.11.16",
|
||||
"@mui/x-date-pickers": "5.0.20",
|
||||
@ -84,9 +83,10 @@
|
||||
"@styled-icons/material-rounded": "10.47.0",
|
||||
"@styled-icons/remix-editor": "10.46.0",
|
||||
"@styled-icons/simple-icons": "10.46.0",
|
||||
"@udecode/plate": "21.3.2",
|
||||
"@udecode/plate-juice": "21.3.2",
|
||||
"@udecode/plate-serializer-md": "21.3.2",
|
||||
"@udecode/plate": "23.7.4",
|
||||
"@udecode/plate-cursor": "23.7.4",
|
||||
"@udecode/plate-juice": "23.7.4",
|
||||
"@udecode/plate-serializer-md": "23.7.4",
|
||||
"@uiw/codemirror-extensions-langs": "4.19.16",
|
||||
"@uiw/react-codemirror": "4.19.16",
|
||||
"ajv": "8.12.0",
|
||||
@ -159,7 +159,7 @@
|
||||
"slate": "0.94.1",
|
||||
"slate-history": "0.93.0",
|
||||
"slate-hyperscript": "0.77.0",
|
||||
"slate-react": "0.95.0",
|
||||
"slate-react": "0.98.3",
|
||||
"stream-browserify": "3.0.0",
|
||||
"styled-components": "5.3.10",
|
||||
"symbol-observable": "4.0.0",
|
||||
|
@ -12,3 +12,5 @@ export const translate = () => (Component: FC) => {
|
||||
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 { invokeEvent } from '../lib/registry';
|
||||
import { getDefaultPath } from '../lib/util/collection.util';
|
||||
import { generateClassNames } from '../lib/util/theming.util';
|
||||
import { selectTheme } from '../reducers/selectors/globalUI';
|
||||
import { useAppDispatch, useAppSelector } from '../store/hooks';
|
||||
import CollectionRoute from './collections/CollectionRoute';
|
||||
@ -37,6 +38,10 @@ import type { RootState } from '@staticcms/core/store';
|
||||
import type { ComponentType } from 'react';
|
||||
import type { ConnectedProps } from 'react-redux';
|
||||
|
||||
import './App.css';
|
||||
|
||||
export const classes = generateClassNames('App', ['root', 'content']);
|
||||
|
||||
TopBarProgress.config({
|
||||
barColors: {
|
||||
0: '#000',
|
||||
@ -267,8 +272,8 @@ const App = ({
|
||||
<ScrollSync key="scroll-sync" enabled={scrollSyncEnabled}>
|
||||
<>
|
||||
<div key="back-to-top-anchor" id="back-to-top-anchor" />
|
||||
<div key="cms-root" id="cms-root" className="h-full">
|
||||
<div key="cms-wrapper" className="cms-wrapper">
|
||||
<div key="cms-root" id="cms-root" className={classes.root}>
|
||||
<div key="cms-wrapper" className={classes.content}>
|
||||
<Snackbars key="snackbars" />
|
||||
{content}
|
||||
<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 { localForage } from '@staticcms/core/lib/util';
|
||||
import { generateClassNames } from '../lib/util/theming.util';
|
||||
|
||||
import type { Config, TranslatedProps } from '@staticcms/core/interface';
|
||||
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?';
|
||||
|
||||
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 (
|
||||
<div
|
||||
key="error-boundary-container"
|
||||
className="
|
||||
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>
|
||||
<div key="error-boundary-container" className={classes.root}>
|
||||
<div className={classes.header}>
|
||||
<h1 className={classes.title}>{t('ui.errorBoundary.title')}</h1>
|
||||
<p>
|
||||
<span>{t('ui.errorBoundary.details')}</span>
|
||||
<a
|
||||
@ -175,10 +170,7 @@ class ErrorBoundary extends Component<TranslatedProps<ErrorBoundaryProps>, Error
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
data-testid="issue-url"
|
||||
className="
|
||||
text-blue-500
|
||||
hover:underline
|
||||
"
|
||||
className={classes['report-link']}
|
||||
>
|
||||
{t('ui.errorBoundary.reportIt')}
|
||||
</a>
|
||||
@ -193,19 +185,11 @@ class ErrorBoundary extends Component<TranslatedProps<ErrorBoundaryProps>, Error
|
||||
</p>
|
||||
</div>
|
||||
<hr />
|
||||
<div
|
||||
className="
|
||||
flex
|
||||
flex-col
|
||||
py-2
|
||||
px-4
|
||||
gap-2
|
||||
"
|
||||
>
|
||||
<h2 className="text-xl bold">{t('ui.errorBoundary.detailsHeading')}</h2>
|
||||
<div className={classes.content}>
|
||||
<h2 className={classes['details-title']}>{t('ui.errorBoundary.detailsHeading')}</h2>
|
||||
<p>
|
||||
{errorMessage.split('\n').map((item, index) => [
|
||||
<span key={`error-line-${index}`} className="whitespace-pre">
|
||||
<span key={`error-line-${index}`} className={classes['error-line']}>
|
||||
{item}
|
||||
</span>,
|
||||
<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 classNames from '../lib/util/classNames.util';
|
||||
import { generateClassNames } from '../lib/util/theming.util';
|
||||
import BottomNavigation from './navbar/BottomNavigation';
|
||||
import Navbar from './navbar/Navbar';
|
||||
import Sidebar from './navbar/Sidebar';
|
||||
@ -9,6 +10,16 @@ import Sidebar from './navbar/Sidebar';
|
||||
import type { ReactNode } from 'react';
|
||||
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({
|
||||
barColors: {
|
||||
0: '#000',
|
||||
@ -46,20 +57,15 @@ const MainView = ({
|
||||
showQuickCreate={showQuickCreate}
|
||||
navbarActions={navbarActions}
|
||||
/>
|
||||
<div className="flex bg-slate-50 dark:bg-slate-900">
|
||||
<div className={classes.root}>
|
||||
{showLeftNav ? <Sidebar /> : null}
|
||||
<div
|
||||
id="main-view"
|
||||
className={classNames(
|
||||
showLeftNav ? ' w-full left-0 md:w-main' : 'w-full',
|
||||
!noMargin && 'px-5 py-4',
|
||||
noScroll ? 'overflow-hidden' : 'overflow-y-auto',
|
||||
`
|
||||
h-main-mobile
|
||||
md:h-main
|
||||
relative
|
||||
styled-scrollbars
|
||||
`,
|
||||
classes.body,
|
||||
showLeftNav && classes['show-left-nav'],
|
||||
noMargin && classes['no-margin'],
|
||||
noScroll && classes['no-scroll'],
|
||||
)}
|
||||
>
|
||||
{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 ViewStyleControl from '../common/view-style/ViewStyleControl';
|
||||
import collectionClasses from './Collection.classes';
|
||||
import FilterControl from './FilterControl';
|
||||
import GroupControl from './GroupControl';
|
||||
import MobileCollectionControls from './mobile/MobileCollectionControls';
|
||||
@ -61,20 +62,7 @@ const CollectionControls = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="
|
||||
flex
|
||||
items-center
|
||||
relative
|
||||
z-20
|
||||
w-full
|
||||
justify-end
|
||||
gap-1.5
|
||||
sm:w-auto
|
||||
sm:justify-normal
|
||||
lg:gap-2
|
||||
"
|
||||
>
|
||||
<div className={collectionClasses.controls}>
|
||||
<ViewStyleControl viewStyle={viewStyle} onChangeViewStyle={onChangeViewStyle} />
|
||||
{showGroupControl || showFilterControl || showFilterControl ? (
|
||||
<MobileCollectionControls
|
||||
|
@ -4,6 +4,7 @@ import { useParams } from 'react-router-dom';
|
||||
|
||||
import useEntries from '@staticcms/core/lib/hooks/useEntries';
|
||||
import useIcon from '@staticcms/core/lib/hooks/useIcon';
|
||||
import useNewEntryUrl from '@staticcms/core/lib/hooks/useNewEntryUrl';
|
||||
import {
|
||||
selectEntryCollectionTitle,
|
||||
selectFolderEntryExtension,
|
||||
@ -11,7 +12,7 @@ import {
|
||||
import { isNotEmpty } from '@staticcms/core/lib/util/string.util';
|
||||
import { addFileTemplateFields } from '@staticcms/core/lib/widgets/stringTemplate';
|
||||
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 { FC } from 'react';
|
||||
@ -63,54 +64,19 @@ const CollectionHeader: FC<TranslatedProps<CollectionHeaderProps>> = ({ collecti
|
||||
}, [collection, collectionLabel, entries, filterTerm]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="
|
||||
flex
|
||||
flex-grow
|
||||
gap-4
|
||||
justify-normal
|
||||
xs:justify-between
|
||||
sm:justify-normal
|
||||
w-full
|
||||
truncate
|
||||
"
|
||||
>
|
||||
<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>
|
||||
<div className={collectionClasses['header-wrapper']}>
|
||||
<h2 className={collectionClasses.header}>
|
||||
<div className={collectionClasses['header-icon']}>{icon}</div>
|
||||
<div className={collectionClasses['header-label']}>{pluralLabel}</div>
|
||||
</h2>
|
||||
{newEntryUrl ? (
|
||||
<Button to={newEntryUrl} className="hidden md:flex">
|
||||
<Button to={newEntryUrl} className={collectionClasses['new-entry-button']}>
|
||||
{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 React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
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 { 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 {
|
||||
collections: Collections;
|
||||
collection?: Collection;
|
||||
@ -136,39 +152,21 @@ const CollectionSearch = ({
|
||||
[submitSearch],
|
||||
);
|
||||
|
||||
const handleClick = useCallback((event: MouseEvent) => {
|
||||
const handleClick = useCallback((event: MouseEvent<HTMLInputElement>) => {
|
||||
event.stopPropagation();
|
||||
setAnchorEl(event.currentTarget);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="relative">
|
||||
<div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
|
||||
<SearchIcon className="w-5 h-5 text-gray-500 dark:text-gray-400" />
|
||||
<div className={classes.root}>
|
||||
<div className={classes.content}>
|
||||
<div className={classes['icon-wrapper']}>
|
||||
<SearchIcon className={classes.icon} />
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
id="first_name"
|
||||
className="
|
||||
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
|
||||
"
|
||||
className={classes.input}
|
||||
placeholder={t('collection.sidebar.searchAll')}
|
||||
onKeyDown={handleKeyDown}
|
||||
onBlur={handleBlur}
|
||||
@ -178,57 +176,20 @@ const CollectionSearch = ({
|
||||
onClick={handleClick}
|
||||
/>
|
||||
</div>
|
||||
<PopperUnstyled
|
||||
<BasePopper
|
||||
open={open}
|
||||
component="div"
|
||||
placement="top"
|
||||
anchorEl={anchorEl}
|
||||
tabIndex={0}
|
||||
className="
|
||||
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
|
||||
"
|
||||
className={classes['search-in']}
|
||||
slots={{
|
||||
root: 'div',
|
||||
}}
|
||||
>
|
||||
<div key="edit-content" contentEditable={false} className={classes['search-in-content']}>
|
||||
<div className={classes['search-in-label']}>{t('collection.sidebar.searchIn')}</div>
|
||||
<div
|
||||
key="edit-content"
|
||||
contentEditable={false}
|
||||
className="
|
||||
flex
|
||||
flex-col
|
||||
min-w-[200px]
|
||||
"
|
||||
>
|
||||
<div
|
||||
className="
|
||||
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
|
||||
"
|
||||
className={classes['search-in-option']}
|
||||
onClick={e => handleSuggestionClick(e, -1)}
|
||||
onMouseDown={e => e.preventDefault()}
|
||||
>
|
||||
@ -239,52 +200,13 @@ const CollectionSearch = ({
|
||||
key={idx}
|
||||
onClick={e => handleSuggestionClick(e, idx)}
|
||||
onMouseDown={e => e.preventDefault()}
|
||||
className="
|
||||
cursor-pointer
|
||||
hover:bg-blue-500
|
||||
hover:color-gray-100
|
||||
py-2
|
||||
px-3
|
||||
"
|
||||
className={classes['search-in-option']}
|
||||
>
|
||||
{collection.label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</PopperUnstyled>
|
||||
{/* <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> */}
|
||||
</BasePopper>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -21,6 +21,7 @@ import {
|
||||
selectViewStyle,
|
||||
} from '@staticcms/core/reducers/selectors/entries';
|
||||
import Card from '../common/card/Card';
|
||||
import collectionClasses from './Collection.classes';
|
||||
import CollectionControls from './CollectionControls';
|
||||
import CollectionHeader from './CollectionHeader';
|
||||
import EntriesCollection from './entries/EntriesCollection';
|
||||
@ -37,6 +38,8 @@ import type { RootState } from '@staticcms/core/store';
|
||||
import type { ComponentType } from 'react';
|
||||
import type { ConnectedProps } from 'react-redux';
|
||||
|
||||
import './Collection.css';
|
||||
|
||||
const CollectionView = ({
|
||||
collection,
|
||||
collections,
|
||||
@ -183,11 +186,11 @@ const CollectionView = ({
|
||||
const collectionDescription = collection?.description;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full px-5 pt-4 overflow-hidden">
|
||||
<div className="flex items-center mb-4 flex-row gap-4 sm:gap-0">
|
||||
<div className={collectionClasses.root}>
|
||||
<div className={collectionClasses.content}>
|
||||
{isSearchResults ? (
|
||||
<>
|
||||
<div className="flex-grow">
|
||||
<div className={collectionClasses['search-query']}>
|
||||
<div>{t(searchResultKey, { searchTerm, collection: collection?.label })}</div>
|
||||
</div>
|
||||
<CollectionControls viewStyle={viewStyle} onChangeViewStyle={changeViewStyle} />
|
||||
@ -212,8 +215,8 @@ const CollectionView = ({
|
||||
)}
|
||||
</div>
|
||||
{collectionDescription ? (
|
||||
<div className="flex mb-4">
|
||||
<Card className="flex-grow px-3.5 py-2.5 text-sm">{collectionDescription}</Card>
|
||||
<div className={collectionClasses.description}>
|
||||
<Card className={collectionClasses['description-card']}>{collectionDescription}</Card>
|
||||
</div>
|
||||
) : null}
|
||||
{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 { translate } from 'react-polyglot';
|
||||
|
||||
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
|
||||
import Menu from '../common/menu/Menu';
|
||||
import MenuGroup from '../common/menu/MenuGroup';
|
||||
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 { 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 {
|
||||
filter: Record<string, FilterMap> | undefined;
|
||||
viewFilters: ViewFilter[] | undefined;
|
||||
@ -35,31 +48,15 @@ const FilterControl = ({
|
||||
|
||||
if (variant === 'list') {
|
||||
return (
|
||||
<div key="filter-by-list" className="flex flex-col gap-2">
|
||||
<h3
|
||||
className="
|
||||
text-lg
|
||||
font-bold
|
||||
text-gray-800
|
||||
dark:text-white
|
||||
"
|
||||
>
|
||||
{t('collection.collectionTop.filterBy')}
|
||||
</h3>
|
||||
<div key="filter-by-list" className={classes['list-root']}>
|
||||
<h3 className={classes['list-label']}>{t('collection.collectionTop.filterBy')}</h3>
|
||||
{viewFilters.map(viewFilter => {
|
||||
const checked = Boolean(viewFilter.id && filter[viewFilter?.id]?.active) ?? false;
|
||||
const labelId = `filter-list-label-${viewFilter.label}`;
|
||||
return (
|
||||
<div
|
||||
key={viewFilter.id}
|
||||
className="
|
||||
ml-1.5
|
||||
font-medium
|
||||
flex
|
||||
items-center
|
||||
text-gray-800
|
||||
dark:text-gray-300
|
||||
"
|
||||
className={classes['list-filter']}
|
||||
onClick={handleFilterClick(viewFilter)}
|
||||
>
|
||||
<input
|
||||
@ -67,13 +64,10 @@ const FilterControl = ({
|
||||
id={labelId}
|
||||
type="checkbox"
|
||||
value=""
|
||||
className=""
|
||||
checked={checked}
|
||||
readOnly
|
||||
/>
|
||||
<label className="ml-2 text-md font-medium text-gray-800 dark:text-gray-300">
|
||||
{viewFilter.label}
|
||||
</label>
|
||||
<label className={classes['list-filter-label']}>{viewFilter.label}</label>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@ -86,26 +80,27 @@ const FilterControl = ({
|
||||
key="filter-by-menu"
|
||||
label={t('collection.collectionTop.filterBy')}
|
||||
variant={anyActive ? 'contained' : 'outlined'}
|
||||
rootClassName="hidden lg:block"
|
||||
rootClassName={classes.root}
|
||||
>
|
||||
<MenuGroup>
|
||||
{viewFilters.map(viewFilter => {
|
||||
const checked = Boolean(viewFilter.id && filter[viewFilter?.id]?.active) ?? false;
|
||||
const labelId = `filter-list-label-${viewFilter.label}`;
|
||||
return (
|
||||
<MenuItemButton key={viewFilter.id} onClick={handleFilterClick(viewFilter)}>
|
||||
<MenuItemButton
|
||||
key={viewFilter.id}
|
||||
onClick={handleFilterClick(viewFilter)}
|
||||
className={classes.filter}
|
||||
>
|
||||
<input
|
||||
key={`${labelId}-${checked}`}
|
||||
id={labelId}
|
||||
type="checkbox"
|
||||
value=""
|
||||
className=""
|
||||
checked={checked}
|
||||
readOnly
|
||||
/>
|
||||
<label className="ml-2 text-sm font-medium text-gray-800 dark:text-gray-300">
|
||||
{viewFilter.label}
|
||||
</label>
|
||||
<label className={classes['filter-label']}>{viewFilter.label}</label>
|
||||
</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 { translate } from 'react-polyglot';
|
||||
|
||||
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
|
||||
import Menu from '../common/menu/Menu';
|
||||
import MenuGroup from '../common/menu/MenuGroup';
|
||||
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 { 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 {
|
||||
group: Record<string, GroupMap> | undefined;
|
||||
viewGroups: ViewGroup[] | undefined;
|
||||
@ -36,39 +50,21 @@ const GroupControl = ({
|
||||
|
||||
if (variant === 'list') {
|
||||
return (
|
||||
<div key="filter-by-list" className="flex flex-col gap-2">
|
||||
<h3
|
||||
className="
|
||||
text-lg
|
||||
font-bold
|
||||
text-gray-800
|
||||
dark:text-white
|
||||
"
|
||||
>
|
||||
{t('collection.collectionTop.groupBy')}
|
||||
</h3>
|
||||
<div key="filter-by-list" className={classes.list}>
|
||||
<h3 className={classes['list-label']}>{t('collection.collectionTop.groupBy')}</h3>
|
||||
{viewGroups.map(viewGroup => {
|
||||
const active = Boolean(viewGroup.id && group[viewGroup?.id]?.active) ?? false;
|
||||
return (
|
||||
<div
|
||||
key={viewGroup.id}
|
||||
className="
|
||||
ml-0.5
|
||||
font-medium
|
||||
flex
|
||||
items-center
|
||||
text-gray-800
|
||||
dark:text-gray-300
|
||||
"
|
||||
className={classes['list-option']}
|
||||
onClick={handleGroupClick(viewGroup)}
|
||||
>
|
||||
<label className="ml-2 text-md font-medium text-gray-800 dark:text-gray-300">
|
||||
{viewGroup.label}
|
||||
</label>
|
||||
<label className={classes['list-option-label']}>{viewGroup.label}</label>
|
||||
{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>
|
||||
);
|
||||
@ -81,7 +77,7 @@ const GroupControl = ({
|
||||
<Menu
|
||||
label={t('collection.collectionTop.groupBy')}
|
||||
variant={activeGroup ? 'contained' : 'outlined'}
|
||||
rootClassName="hidden lg:block"
|
||||
rootClassName={classes.root}
|
||||
>
|
||||
<MenuGroup>
|
||||
{viewGroups.map(viewGroup => (
|
||||
@ -89,6 +85,7 @@ const GroupControl = ({
|
||||
key={viewGroup.id}
|
||||
onClick={() => onGroupClick?.(viewGroup)}
|
||||
endIcon={viewGroup.id === activeGroup?.id ? CheckIcon : undefined}
|
||||
className={classes.option}
|
||||
>
|
||||
{viewGroup.label}
|
||||
</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 { ChevronRight as ChevronRightIcon } from '@styled-icons/material/ChevronRight';
|
||||
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 useEntries from '@staticcms/core/lib/hooks/useEntries';
|
||||
import classNames from '@staticcms/core/lib/util/classNames.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 type { Collection, Entry } from '@staticcms/core/interface';
|
||||
import type { TreeNodeData } from '@staticcms/core/lib/util/nested.util';
|
||||
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) {
|
||||
const title = node.isRoot
|
||||
? node.title
|
||||
@ -23,28 +40,46 @@ function getNodeTitle(node: TreeNodeData) {
|
||||
interface TreeNodeProps {
|
||||
collection: Collection;
|
||||
treeData: TreeNodeData[];
|
||||
rootIsActive: boolean;
|
||||
path: string;
|
||||
depth?: number;
|
||||
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 handleClick = useCallback(
|
||||
(event: MouseEvent | undefined, node: TreeNodeData, expanded: boolean) => {
|
||||
if (!rootIsActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
event?.stopPropagation();
|
||||
event?.preventDefault();
|
||||
|
||||
if (event) {
|
||||
onToggle({ node, expanded });
|
||||
} else {
|
||||
onToggle({ node, expanded: true });
|
||||
onToggle({ node, expanded: path === node.path ? expanded : true });
|
||||
}
|
||||
},
|
||||
[onToggle],
|
||||
[onToggle, path, rootIsActive],
|
||||
);
|
||||
|
||||
const sortedData = sortBy(treeData, getNodeTitle);
|
||||
|
||||
if (depth !== 0 && !rootIsActive) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{sortedData.map(node => {
|
||||
@ -62,36 +97,42 @@ const TreeNode = ({ collection, treeData, depth = 0, onToggle }: TreeNodeProps)
|
||||
|
||||
return (
|
||||
<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
|
||||
to={to}
|
||||
onClick={() => handleClick(undefined, node, !node.expanded)}
|
||||
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>
|
||||
{hasChildren && (
|
||||
<ChevronRightIcon
|
||||
onClick={event => handleClick(event, node, !node.expanded)}
|
||||
className={classNames(
|
||||
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
|
||||
`,
|
||||
)}
|
||||
className={classes['node-children-icon']}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</NavLink>
|
||||
<div className="mt-2 space-y-1.5">
|
||||
<div className={classes['node-children']}>
|
||||
{node.expanded && (
|
||||
<TreeNode
|
||||
rootIsActive={rootIsActive}
|
||||
collection={collection}
|
||||
path={path}
|
||||
depth={depth + 1}
|
||||
treeData={node.children}
|
||||
onToggle={onToggle}
|
||||
@ -153,30 +194,52 @@ const NestedCollection = ({ collection, filterTerm }: NestedCollectionProps) =>
|
||||
const [treeData, setTreeData] = useState<TreeNodeData[]>(getTreeData(collection, entries));
|
||||
const [useFilter, setUseFilter] = useState(true);
|
||||
|
||||
const [prevRootIsActive, setPrevRootIsActive] = useState(false);
|
||||
const [prevCollection, setPrevCollection] = useState<Collection | 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 rootIsActive = useMemo(
|
||||
() => pathname.startsWith(`/collections/${collection.name}`),
|
||||
[collection.name, pathname],
|
||||
);
|
||||
|
||||
const path = useMemo(() => `/${filterTerm}`, [filterTerm]);
|
||||
|
||||
useEffect(() => {
|
||||
if (collection !== prevCollection || entries !== prevEntries || filterTerm !== prevFilterTerm) {
|
||||
if (
|
||||
rootIsActive !== prevRootIsActive ||
|
||||
collection !== prevCollection ||
|
||||
entries !== prevEntries ||
|
||||
path !== prevPath
|
||||
) {
|
||||
const expanded: Record<string, boolean> = {};
|
||||
walk(treeData, node => {
|
||||
if (!rootIsActive) {
|
||||
expanded[node.path] = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.expanded) {
|
||||
expanded[node.path] = true;
|
||||
}
|
||||
});
|
||||
const newTreeData = getTreeData(collection, entries);
|
||||
|
||||
const path = `/${filterTerm}`;
|
||||
walk(newTreeData, node => {
|
||||
if (
|
||||
expanded[node.path] ||
|
||||
(useFilter &&
|
||||
path.startsWith(node.path) &&
|
||||
pathname.startsWith(`/collections/${collection.name}`))
|
||||
) {
|
||||
if (!rootIsActive) {
|
||||
node.expanded = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.isRoot) {
|
||||
node.expanded = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (expanded[node.path] || (useFilter && path.startsWith(node.path))) {
|
||||
node.expanded = true;
|
||||
}
|
||||
});
|
||||
@ -184,17 +247,21 @@ const NestedCollection = ({ collection, filterTerm }: NestedCollectionProps) =>
|
||||
setTreeData(newTreeData);
|
||||
}
|
||||
|
||||
setPrevRootIsActive(rootIsActive);
|
||||
setPrevCollection(collection);
|
||||
setPrevEntries(entries);
|
||||
setPrevFilterTerm(filterTerm);
|
||||
setPrevPath(path);
|
||||
}, [
|
||||
collection,
|
||||
entries,
|
||||
filterTerm,
|
||||
path,
|
||||
pathname,
|
||||
prevCollection,
|
||||
prevEntries,
|
||||
prevFilterTerm,
|
||||
prevPath,
|
||||
prevRootIsActive,
|
||||
rootIsActive,
|
||||
treeData,
|
||||
useFilter,
|
||||
]);
|
||||
@ -212,7 +279,15 @@ const NestedCollection = ({ collection, filterTerm }: NestedCollectionProps) =>
|
||||
[treeData],
|
||||
);
|
||||
|
||||
return <TreeNode collection={collection} treeData={treeData} onToggle={onToggle} />;
|
||||
return (
|
||||
<TreeNode
|
||||
collection={collection}
|
||||
treeData={treeData}
|
||||
onToggle={onToggle}
|
||||
rootIsActive={rootIsActive}
|
||||
path={path}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
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_NONE,
|
||||
} from '@staticcms/core/constants';
|
||||
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
|
||||
import Menu from '../common/menu/Menu';
|
||||
import MenuGroup from '../common/menu/MenuGroup';
|
||||
import MenuItemButton from '../common/menu/MenuItemButton';
|
||||
@ -20,6 +21,19 @@ import type {
|
||||
} from '@staticcms/core/interface';
|
||||
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) {
|
||||
switch (direction) {
|
||||
case SORT_DIRECTION_ASCENDING:
|
||||
@ -69,44 +83,32 @@ const SortControl = ({
|
||||
|
||||
if (variant === 'list') {
|
||||
return (
|
||||
<div key="filter-by-list" className="flex flex-col gap-2">
|
||||
<h3
|
||||
className="
|
||||
text-lg
|
||||
font-bold
|
||||
text-gray-800
|
||||
dark:text-white
|
||||
"
|
||||
>
|
||||
{t('collection.collectionTop.sortBy')}
|
||||
</h3>
|
||||
<div key="filter-by-list" className={classes.list}>
|
||||
<h3 className={classes['list-label']}>{t('collection.collectionTop.sortBy')}</h3>
|
||||
{fields.map(field => {
|
||||
const sortDir = sort?.[field.name]?.direction ?? SORT_DIRECTION_NONE;
|
||||
const nextSortDir = nextSortDirection(sortDir);
|
||||
return (
|
||||
<div
|
||||
key={field.name}
|
||||
className="
|
||||
ml-0.5
|
||||
font-medium
|
||||
flex
|
||||
items-center
|
||||
text-gray-800
|
||||
dark:text-gray-300
|
||||
"
|
||||
className={classes['list-option']}
|
||||
onClick={handleSortClick(field.name, nextSortDir)}
|
||||
>
|
||||
<label className="ml-2 text-md font-medium text-gray-800 dark:text-gray-300">
|
||||
{field.label ?? field.name}
|
||||
</label>
|
||||
<label className={classes['list-option-label']}>{field.label ?? field.name}</label>
|
||||
{field.name === selectedSort.key ? (
|
||||
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>
|
||||
);
|
||||
@ -119,7 +121,7 @@ const SortControl = ({
|
||||
<Menu
|
||||
label={t('collection.collectionTop.sortBy')}
|
||||
variant={selectedSort.key ? 'contained' : 'outlined'}
|
||||
rootClassName="hidden lg:block"
|
||||
rootClassName={classes.root}
|
||||
>
|
||||
<MenuGroup>
|
||||
{fields.map(field => {
|
||||
@ -137,6 +139,7 @@ const SortControl = ({
|
||||
: KeyboardArrowDownIcon
|
||||
: undefined
|
||||
}
|
||||
className={classes.option}
|
||||
>
|
||||
{field.label ?? field.name}
|
||||
</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 Loader from '@staticcms/core/components/common/progress/Loader';
|
||||
import entriesClasses from './Entries.classes';
|
||||
import EntryListing from './EntryListing';
|
||||
|
||||
import type { ViewStyle } from '@staticcms/core/constants/views';
|
||||
import type { Collection, Collections, Entry, TranslatedProps } from '@staticcms/core/interface';
|
||||
import type Cursor from '@staticcms/core/lib/util/Cursor';
|
||||
|
||||
import './Entries.css';
|
||||
|
||||
export interface BaseEntriesProps {
|
||||
entries: Entry[];
|
||||
page?: number;
|
||||
@ -81,20 +84,7 @@ const Entries = ({
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="
|
||||
py-2
|
||||
px-3
|
||||
rounded-md
|
||||
bg-yellow-300/75
|
||||
dark:bg-yellow-800/75
|
||||
text-sm
|
||||
"
|
||||
>
|
||||
{t('collection.entries.noEntries')}
|
||||
</div>
|
||||
);
|
||||
return <div className={entriesClasses.root}>{t('collection.entries.noEntries')}</div>;
|
||||
};
|
||||
|
||||
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 useGroups from '@staticcms/core/lib/hooks/useGroups';
|
||||
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 { selectEntriesLoaded, selectIsFetching } from '@staticcms/core/reducers/selectors/entries';
|
||||
import { useAppDispatch } from '@staticcms/core/store/hooks';
|
||||
import Button from '../../common/button/Button';
|
||||
import Entries from './Entries';
|
||||
import entriesClasses from './Entries.classes';
|
||||
|
||||
import type { ViewStyle } from '@staticcms/core/constants/views';
|
||||
import type { Collection, Entry, GroupOfEntries, TranslatedProps } from '@staticcms/core/interface';
|
||||
@ -115,25 +117,9 @@ const EntriesCollection = ({
|
||||
if (groups && groups.length > 0) {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="
|
||||
pb-3
|
||||
"
|
||||
>
|
||||
<div
|
||||
className="
|
||||
-m-1
|
||||
"
|
||||
>
|
||||
<div
|
||||
className="
|
||||
flex
|
||||
gap-2
|
||||
p-1
|
||||
overflow-x-auto
|
||||
hide-scrollbar
|
||||
"
|
||||
>
|
||||
<div className={entriesClasses.group}>
|
||||
<div className={entriesClasses['group-content-wrapper']}>
|
||||
<div className={classNames(entriesClasses['group-content'], 'CMS_Scrollbar_hide')}>
|
||||
{groups.map((group, index) => {
|
||||
const title = getGroupTitle(group, t);
|
||||
return (
|
||||
@ -141,7 +127,7 @@ const EntriesCollection = ({
|
||||
key={index}
|
||||
variant={index === selectedGroup ? 'contained' : 'text'}
|
||||
onClick={handleGroupClick(index)}
|
||||
className="whitespace-nowrap"
|
||||
className={entriesClasses['group-button']}
|
||||
>
|
||||
{title}
|
||||
</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,
|
||||
} from '@staticcms/core/lib/util/collection.util';
|
||||
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 { selectTheme } from '@staticcms/core/reducers/selectors/globalUI';
|
||||
import { useAppSelector } from '@staticcms/core/store/hooks';
|
||||
@ -27,6 +28,18 @@ import type {
|
||||
} from '@staticcms/core/interface';
|
||||
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 {
|
||||
entry: Entry;
|
||||
imageFieldName?: string | null | undefined;
|
||||
@ -113,9 +126,9 @@ const EntryCard: FC<TranslatedProps<EntryCardProps>> = ({
|
||||
|
||||
if (PreviewCardComponent) {
|
||||
return (
|
||||
<div className="h-full w-full relative overflow-visible">
|
||||
<div className="absolute -inset-1 pr-2">
|
||||
<div className="p-1 h-full w-full">
|
||||
<div className={classes.root}>
|
||||
<div className={classes['content-wrapper']}>
|
||||
<div className={classes.content}>
|
||||
<Card>
|
||||
<CardActionArea to={path}>
|
||||
<PreviewCardComponent
|
||||
@ -136,10 +149,10 @@ const EntryCard: FC<TranslatedProps<EntryCardProps>> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full w-full relative overflow-visible">
|
||||
<div className="absolute -inset-1 pr-2">
|
||||
<div className="p-1 h-full w-full">
|
||||
<Card className="h-full" title={summary}>
|
||||
<div className={classes.root}>
|
||||
<div className={classes['content-wrapper']}>
|
||||
<div className={classes.content}>
|
||||
<Card className={classes.card} title={summary}>
|
||||
<CardActionArea to={path}>
|
||||
{image && imageField ? (
|
||||
<CardMedia
|
||||
@ -151,16 +164,11 @@ const EntryCard: FC<TranslatedProps<EntryCardProps>> = ({
|
||||
/>
|
||||
) : null}
|
||||
<CardContent>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="truncate">{summary}</div>
|
||||
<div className={classes['card-content']}>
|
||||
<div className={classes['card-summary']}>{summary}</div>
|
||||
{hasLocalBackup ? (
|
||||
<InfoIcon
|
||||
className="
|
||||
w-5
|
||||
h-5
|
||||
text-blue-600
|
||||
dark:text-blue-300
|
||||
"
|
||||
className={classes['local-backup-icon']}
|
||||
title={t('ui.localBackup.hasLocalBackup')}
|
||||
/>
|
||||
) : null}
|
||||
|
@ -4,6 +4,7 @@ import { translate } from 'react-polyglot';
|
||||
import { VIEW_STYLE_TABLE } from '@staticcms/core/constants/views';
|
||||
import { selectFields, selectInferredField } from '@staticcms/core/lib/util/collection.util';
|
||||
import { toTitleCaseFromKey } from '@staticcms/core/lib/util/string.util';
|
||||
import entriesClasses from './Entries.classes';
|
||||
import EntryListingGrid from './EntryListingGrid';
|
||||
import EntryListingTable from './EntryListingTable';
|
||||
|
||||
@ -154,7 +155,7 @@ const EntryListing: FC<TranslatedProps<EntryListingProps>> = ({
|
||||
|
||||
if (viewStyle === VIEW_STYLE_TABLE) {
|
||||
return (
|
||||
<div className="pb-3 overflow-hidden">
|
||||
<div className={entriesClasses['entry-listing']}>
|
||||
<EntryListingTable
|
||||
key="table"
|
||||
entryData={entryData}
|
||||
|
@ -12,6 +12,7 @@ import { getPreviewCard } from '@staticcms/core/lib/registry';
|
||||
import classNames from '@staticcms/core/lib/util/classNames.util';
|
||||
import { selectTemplateName } from '@staticcms/core/lib/util/collection.util';
|
||||
import { isNotNullish } from '@staticcms/core/lib/util/null.util';
|
||||
import entriesClasses from './Entries.classes';
|
||||
import EntryCard from './EntryCard';
|
||||
|
||||
import type { CollectionEntryData } from '@staticcms/core/interface';
|
||||
@ -138,15 +139,7 @@ const EntryListingCardGrid: FC<EntryListingCardGridProps> = ({
|
||||
}, [cardHeights, prevCardHeights.length]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="
|
||||
relative
|
||||
w-card-grid
|
||||
h-full
|
||||
overflow-hidden
|
||||
-ml-1
|
||||
"
|
||||
>
|
||||
<div className={entriesClasses['entry-listing-cards']}>
|
||||
<AutoSizer onResize={handleResize}>
|
||||
{({ height = 0, width = 0 }) => {
|
||||
const calculatedWidth = width - 4;
|
||||
@ -161,11 +154,7 @@ const EntryListingCardGrid: FC<EntryListingCardGridProps> = ({
|
||||
return (
|
||||
<div
|
||||
key={version}
|
||||
className={classNames(
|
||||
`
|
||||
overflow-hidden
|
||||
`,
|
||||
)}
|
||||
className={entriesClasses['entry-listing-cards-grid-wrapper']}
|
||||
style={{
|
||||
width: calculatedWidth,
|
||||
height,
|
||||
@ -212,11 +201,8 @@ const EntryListingCardGrid: FC<EntryListingCardGridProps> = ({
|
||||
outerRef={scrollContainerRef}
|
||||
onScroll={onScroll}
|
||||
className={classNames(
|
||||
`
|
||||
!overflow-x-hidden
|
||||
overflow-y-auto
|
||||
styled-scrollbars
|
||||
`,
|
||||
entriesClasses['entry-listing-cards-grid'],
|
||||
'CMS_Scrollbar_root',
|
||||
)}
|
||||
style={{ position: 'unset' }}
|
||||
overscanRowCount={5}
|
||||
|
@ -3,6 +3,7 @@ import React, { useCallback, useEffect, useRef } from 'react';
|
||||
import { isNotNullish } from '@staticcms/core/lib/util/null.util';
|
||||
import { selectIsFetching } from '@staticcms/core/reducers/selectors/globalUI';
|
||||
import { useAppSelector } from '@staticcms/core/store/hooks';
|
||||
import entriesClasses from './Entries.classes';
|
||||
import EntryListingCardGrid from './EntryListingCardGrid';
|
||||
|
||||
import type { CollectionEntryData } from '@staticcms/core/interface';
|
||||
@ -57,8 +58,8 @@ const EntryListingGrid: FC<EntryListingGridProps> = ({
|
||||
}, [handleScroll]);
|
||||
|
||||
return (
|
||||
<div className="relative h-full flex-grow">
|
||||
<div ref={gridContainerRef} className="relative h-full">
|
||||
<div className={entriesClasses['entry-listing-grid']}>
|
||||
<div ref={gridContainerRef} className={entriesClasses['entry-listing-grid-container']}>
|
||||
<EntryListingCardGrid
|
||||
key="grid"
|
||||
entryData={entryData}
|
||||
@ -68,18 +69,7 @@ const EntryListingGrid: FC<EntryListingGridProps> = ({
|
||||
/>
|
||||
</div>
|
||||
{isLoadingEntries ? (
|
||||
<div
|
||||
key="loading"
|
||||
className="
|
||||
absolute
|
||||
inset-0
|
||||
flex
|
||||
items-center
|
||||
justify-center
|
||||
bg-slate-50/50
|
||||
dark:bg-slate-900/50
|
||||
"
|
||||
>
|
||||
<div key="loading" className={entriesClasses['entry-listing-loading']}>
|
||||
{t('collection.entries.loadingEntries')}
|
||||
</div>
|
||||
) : null}
|
||||
|
@ -1,10 +1,12 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { useVirtual } from 'react-virtual';
|
||||
|
||||
import classNames from '@staticcms/core/lib/util/classNames.util';
|
||||
import { isNotNullish } from '@staticcms/core/lib/util/null.util';
|
||||
import { selectIsFetching } from '@staticcms/core/reducers/selectors/globalUI';
|
||||
import { useAppSelector } from '@staticcms/core/store/hooks';
|
||||
import Table from '../../common/table/Table';
|
||||
import entriesClasses from './Entries.classes';
|
||||
import EntryRow from './EntryRow';
|
||||
|
||||
import type { CollectionEntryData } from '@staticcms/core/interface';
|
||||
@ -69,32 +71,14 @@ const EntryListingTable: FC<EntryListingTableProps> = ({
|
||||
}, [clientHeight, fetchMoreOnBottomReached, scrollHeight, scrollTop]);
|
||||
|
||||
return (
|
||||
<div
|
||||
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 className={entriesClasses['entry-listing-table']}>
|
||||
<div
|
||||
ref={tableContainerRef}
|
||||
className="
|
||||
relative
|
||||
h-full
|
||||
overflow-auto
|
||||
styled-scrollbars
|
||||
styled-scrollbars-secondary
|
||||
"
|
||||
className={classNames(
|
||||
entriesClasses['entry-listing-table-content'],
|
||||
'CMS_Scrollbar_root',
|
||||
'CMS_Scrollbar_secondary',
|
||||
)}
|
||||
>
|
||||
<Table
|
||||
columns={
|
||||
@ -128,18 +112,7 @@ const EntryListingTable: FC<EntryListingTableProps> = ({
|
||||
</Table>
|
||||
</div>
|
||||
{isLoadingEntries ? (
|
||||
<div
|
||||
key="loading"
|
||||
className="
|
||||
absolute
|
||||
inset-0
|
||||
flex
|
||||
items-center
|
||||
justify-center
|
||||
bg-slate-50/50
|
||||
dark:bg-slate-900/50
|
||||
"
|
||||
>
|
||||
<div key="loading" className={entriesClasses['entry-listing-loading']}>
|
||||
{t('collection.entries.loadingEntries')}
|
||||
</div>
|
||||
) : null}
|
||||
|
@ -15,6 +15,7 @@ import { selectTheme } from '@staticcms/core/reducers/selectors/globalUI';
|
||||
import { useAppSelector } from '@staticcms/core/store/hooks';
|
||||
import TableCell from '../../common/table/TableCell';
|
||||
import TableRow from '../../common/table/TableRow';
|
||||
import entriesClasses from './Entries.classes';
|
||||
|
||||
import type { BackupEntry, Collection, Entry, TranslatedProps } from '@staticcms/core/interface';
|
||||
import type { FC } from 'react';
|
||||
@ -75,13 +76,7 @@ const EntryRow: FC<TranslatedProps<EntryRowProps>> = ({
|
||||
}, [collection.name, entry.slug]);
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
className="
|
||||
hover:bg-gray-200
|
||||
dark:hover:bg-slate-700/70
|
||||
"
|
||||
to={path}
|
||||
>
|
||||
<TableRow className={entriesClasses['entry-listing-table-row']} to={path}>
|
||||
{collectionLabel ? (
|
||||
<TableCell key="collectionLabel" to={path}>
|
||||
{collectionLabel}
|
||||
@ -120,12 +115,7 @@ const EntryRow: FC<TranslatedProps<EntryRowProps>> = ({
|
||||
<TableCell key="unsavedChanges" to={path} shrink>
|
||||
{hasLocalBackup ? (
|
||||
<InfoIcon
|
||||
className="
|
||||
w-5
|
||||
h-5
|
||||
text-blue-600
|
||||
dark:text-blue-300
|
||||
"
|
||||
className={entriesClasses['entry-listing-local-backup']}
|
||||
title={t('ui.localBackup.hasLocalBackup')}
|
||||
/>
|
||||
) : 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 IconButton from '../../common/button/IconButton';
|
||||
import mobileCollectionControlsClasses from './MobileCollectionControls.classes';
|
||||
import MobileCollectionControlsDrawer from './MobileCollectionControlsDrawer';
|
||||
|
||||
import type { FC } from 'react';
|
||||
@ -9,6 +10,8 @@ import type { FilterControlProps } from '../FilterControl';
|
||||
import type { GroupControlProps } from '../GroupControl';
|
||||
import type { SortControlProps } from '../SortControl';
|
||||
|
||||
import './MobileCollectionControls.css';
|
||||
|
||||
export type MobileCollectionControlsProps = Omit<FilterControlProps, 'variant'> &
|
||||
Omit<GroupControlProps, 'variant'> &
|
||||
Omit<SortControlProps, 'variant'> & {
|
||||
@ -25,8 +28,12 @@ const MobileCollectionControls: FC<MobileCollectionControlsProps> = props => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<IconButton className="flex lg:hidden" variant="text" onClick={toggleMobileMenu}>
|
||||
<FilterListIcon className="w-5 h-5" />
|
||||
<IconButton
|
||||
className={mobileCollectionControlsClasses.toggle}
|
||||
variant="text"
|
||||
onClick={toggleMobileMenu}
|
||||
>
|
||||
<FilterListIcon className={mobileCollectionControlsClasses['toggle-icon']} />
|
||||
</IconButton>
|
||||
<MobileCollectionControlsDrawer
|
||||
{...props}
|
||||
|
@ -1,16 +1,16 @@
|
||||
import Drawer from '@mui/material/Drawer';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import classNames from '@staticcms/core/lib/util/classNames.util';
|
||||
import FilterControl from '../FilterControl';
|
||||
import GroupControl from '../GroupControl';
|
||||
import SortControl from '../SortControl';
|
||||
import mobileCollectionControlsClasses from './MobileCollectionControls.classes';
|
||||
|
||||
import type { FilterControlProps } from '../FilterControl';
|
||||
import type { GroupControlProps } from '../GroupControl';
|
||||
import type { SortControlProps } from '../SortControl';
|
||||
|
||||
const DRAWER_WIDTH = 240;
|
||||
|
||||
export type MobileCollectionControlsDrawerProps = Omit<FilterControlProps, 'variant'> &
|
||||
Omit<GroupControlProps, 'variant'> &
|
||||
Omit<SortControlProps, 'variant'> & {
|
||||
@ -53,34 +53,11 @@ const MobileCollectionControlsDrawer = ({
|
||||
ModalProps={{
|
||||
keepMounted: true, // Better open performance on mobile.
|
||||
}}
|
||||
sx={{
|
||||
width: '80%',
|
||||
maxWidth: DRAWER_WIDTH,
|
||||
'& .MuiBackdrop-root': {
|
||||
width: '100%',
|
||||
},
|
||||
'& .MuiDrawer-paper': {
|
||||
boxSizing: 'border-box',
|
||||
width: '80%',
|
||||
maxWidth: DRAWER_WIDTH,
|
||||
},
|
||||
}}
|
||||
slotProps={{ root: { className: mobileCollectionControlsClasses.root } }}
|
||||
>
|
||||
<div
|
||||
onClick={onMobileOpenToggle}
|
||||
className="
|
||||
px-5
|
||||
py-4
|
||||
flex
|
||||
flex-col
|
||||
gap-6
|
||||
h-full
|
||||
w-full
|
||||
overflow-y-auto
|
||||
bg-white
|
||||
dark:bg-slate-800
|
||||
styled-scrollbars
|
||||
"
|
||||
className={classNames(mobileCollectionControlsClasses.content, 'CMS_Scrollbar_root')}
|
||||
>
|
||||
{showSortControl ? (
|
||||
<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 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 Button from '../button/Button';
|
||||
import Modal from '../modal/Modal';
|
||||
|
||||
import type { TranslateProps } from 'react-polyglot';
|
||||
|
||||
import './Alert.css';
|
||||
|
||||
export const classes = generateClassNames('Alert', [
|
||||
'root',
|
||||
'title',
|
||||
'content',
|
||||
'actions',
|
||||
'confirm-button',
|
||||
]);
|
||||
|
||||
interface AlertProps {
|
||||
title: string | { key: string; options?: Record<string, unknown> };
|
||||
body: string | { key: string; options?: Record<string, unknown> };
|
||||
@ -67,44 +78,19 @@ const AlertDialog = ({ t }: TranslateProps) => {
|
||||
<Modal
|
||||
open
|
||||
onClose={handleClose}
|
||||
className="
|
||||
w-[50%]
|
||||
min-w-[300px]
|
||||
max-w-[600px]
|
||||
"
|
||||
className={classes.root}
|
||||
aria-labelledby="alert-dialog-title"
|
||||
aria-describedby="alert-dialog-description"
|
||||
>
|
||||
<div
|
||||
className="
|
||||
px-6
|
||||
py-4
|
||||
text-xl
|
||||
bold
|
||||
"
|
||||
<div className={classes.title}>{title}</div>
|
||||
<div className={classes.content}>{body}</div>
|
||||
<div className={classes.actions}>
|
||||
<Button
|
||||
onClick={handleClose}
|
||||
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}
|
||||
</Button>
|
||||
</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 { Close as CloseIcon } from '@styled-icons/material/Close';
|
||||
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 { isNullish } from '@staticcms/core/lib/util/null.util';
|
||||
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
|
||||
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;
|
||||
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) {
|
||||
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[];
|
||||
value: T | T[] | null;
|
||||
options: T[] | Option<T>[];
|
||||
value: string | string[] | null;
|
||||
options: string[] | Option[];
|
||||
disabled?: boolean;
|
||||
required?: boolean;
|
||||
displayValue: (item: T | T[] | null) => string;
|
||||
inputRef?: Ref<HTMLInputElement>;
|
||||
displayValue: (item: string | string[] | null) => string;
|
||||
onQuery: (query: string) => void;
|
||||
onChange: (value: T | T[] | undefined) => void;
|
||||
onChange: (value: string | string[] | undefined) => void;
|
||||
}
|
||||
|
||||
const Autocomplete = function <T>(
|
||||
{
|
||||
const Autocomplete = ({
|
||||
label,
|
||||
value,
|
||||
options,
|
||||
inputRef,
|
||||
disabled,
|
||||
required,
|
||||
displayValue,
|
||||
onQuery,
|
||||
onChange,
|
||||
}: AutocompleteProps<T>,
|
||||
ref: Ref<HTMLInputElement>,
|
||||
) {
|
||||
const handleChange = useCallback(
|
||||
(selectedValue: T) => {
|
||||
if (Array.isArray(value)) {
|
||||
const newValue = [...value];
|
||||
const index = newValue.indexOf(selectedValue);
|
||||
if (index > -1) {
|
||||
newValue.splice(index, 1);
|
||||
} else {
|
||||
newValue.push(selectedValue);
|
||||
}
|
||||
onQuery,
|
||||
}: AutocompleteProps) => {
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
|
||||
onChange(newValue);
|
||||
const debouncedOnQuery = useDebouncedCallback(onQuery, 200);
|
||||
|
||||
const handleInputChange = useCallback(
|
||||
(newInputValue: string) => {
|
||||
setInputValue(newInputValue);
|
||||
debouncedOnQuery(newInputValue);
|
||||
},
|
||||
[debouncedOnQuery],
|
||||
);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(selectedValue: Option | readonly Option[] | null) => {
|
||||
if (selectedValue === null) {
|
||||
if (Array.isArray(value)) {
|
||||
onChange([]);
|
||||
return;
|
||||
}
|
||||
|
||||
onChange(selectedValue);
|
||||
onChange(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
if ('value' in selectedValue) {
|
||||
onChange(selectedValue.value);
|
||||
return;
|
||||
}
|
||||
|
||||
onChange(selectedValue.map(option => option.value));
|
||||
},
|
||||
[onChange, value],
|
||||
);
|
||||
|
||||
const clear = useCallback(() => {
|
||||
const clear = useCallback(
|
||||
(event: MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
onChange(Array.isArray(value) ? [] : undefined);
|
||||
}, [onChange, value]);
|
||||
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 (
|
||||
<div className="relative w-full">
|
||||
<Combobox value={value} onChange={handleChange} disabled={disabled}>
|
||||
<div className="relative mt-1">
|
||||
<React.Fragment>
|
||||
<div
|
||||
className="
|
||||
flex
|
||||
flex-col
|
||||
flex-start
|
||||
text-sm
|
||||
font-medium
|
||||
relative
|
||||
min-h-8
|
||||
p-0
|
||||
w-full
|
||||
text-gray-800
|
||||
dark:text-gray-100
|
||||
"
|
||||
data-testid="autocomplete-input-wrapper"
|
||||
{...getRootProps()}
|
||||
ref={rootRef}
|
||||
className={classNames(
|
||||
classes.root,
|
||||
focused && classes.focused,
|
||||
disabled && classes.disabled,
|
||||
)}
|
||||
data-testid="autocomplete"
|
||||
>
|
||||
{label}
|
||||
<Combobox.Input<'input', T | T[] | null>
|
||||
ref={ref}
|
||||
className={classNames(
|
||||
`
|
||||
w-full
|
||||
bg-transparent
|
||||
border-none
|
||||
py-2
|
||||
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
|
||||
`,
|
||||
)}
|
||||
<input
|
||||
{...getInputProps()}
|
||||
ref={finalInputRef}
|
||||
className={classes.input}
|
||||
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>
|
||||
<div className={classes['button-wrapper']}>
|
||||
<IconButton
|
||||
variant="text"
|
||||
size="small"
|
||||
disabled={disabled}
|
||||
className={classes.button}
|
||||
onClick={handleDropdownButtonClick}
|
||||
>
|
||||
<KeyboardArrowDownIcon className={classes['button-icon']} aria-hidden="true" />
|
||||
</IconButton>
|
||||
{!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
|
||||
variant="text"
|
||||
size="small"
|
||||
disabled={disabled}
|
||||
className={classes.button}
|
||||
onClick={clear}
|
||||
>
|
||||
<CloseIcon className={classes['button-icon']} aria-hidden="true" />
|
||||
</IconButton>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
leave="transition ease-in duration-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
afterLeave={() => onQuery('')}
|
||||
{anchorEl && (
|
||||
<Popper open={popupOpen} anchorEl={anchorEl} style={{ width }}>
|
||||
<ul
|
||||
{...getListboxProps()}
|
||||
className={classNames(classes.options, 'CMS_Scrollbar_root', 'CMS_Scrollbar_secondary')}
|
||||
>
|
||||
<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 ? (
|
||||
<div className="relative cursor-default select-none py-2 px-4 text-gray-800 dark:text-gray-300">
|
||||
Nothing found.
|
||||
</div>
|
||||
) : (
|
||||
options.map((option, index) => {
|
||||
const { label: optionLabel, value: optionValue } = getOptionLabelAndValue(option);
|
||||
{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 (
|
||||
<Combobox.Option
|
||||
<li
|
||||
key={index}
|
||||
{...getOptionProps({ option: option as Option, index })}
|
||||
className={classNames(classes.option, selected && classes['option-selected'])}
|
||||
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>
|
||||
<span className={classes['option-label']}>{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 className={classes.checkmark}>
|
||||
<CheckIcon className={classes['checkmark-icon']} aria-hidden="true" />
|
||||
</span>
|
||||
) : null}
|
||||
</Combobox.Option>
|
||||
</li>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div className={classes.nothing}>Nothing found.</div>
|
||||
)}
|
||||
</Combobox.Options>
|
||||
</Transition>
|
||||
</div>
|
||||
</Combobox>
|
||||
</div>
|
||||
</ul>
|
||||
</Popper>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default forwardRef(Autocomplete) as <T>(
|
||||
props: AutocompleteProps<T> & { ref: Ref<HTMLButtonElement> },
|
||||
) => JSX.Element;
|
||||
export default Autocomplete;
|
||||
|
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 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 './Button.css';
|
||||
|
||||
export interface BaseBaseProps {
|
||||
variant?: 'contained' | 'outlined' | 'text';
|
||||
color?: 'primary' | 'secondary' | 'success' | 'error' | 'warning';
|
||||
@ -58,16 +60,16 @@ const Button: FC<ButtonLinkProps> = ({
|
||||
const buttonClassName = useButtonClassNames(variant, color, size, rounded);
|
||||
|
||||
const buttonClassNames = useMemo(
|
||||
() => classNames(buttonClassName, className),
|
||||
() => classNames(className, buttonClassName),
|
||||
[buttonClassName, className],
|
||||
);
|
||||
|
||||
const content = useMemo(
|
||||
() => (
|
||||
<>
|
||||
{StartIcon ? <StartIcon className="w-5 h-5 mr-2" /> : null}
|
||||
{StartIcon ? <StartIcon className={buttonClasses['start-icon']} /> : null}
|
||||
{children}
|
||||
{EndIcon ? <EndIcon className="w-5 h-5 ml-2" /> : null}
|
||||
{EndIcon ? <EndIcon className={buttonClasses['end-icon']} /> : null}
|
||||
</>
|
||||
),
|
||||
[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 Button from './Button';
|
||||
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 { ButtonLinkProps } from './Button';
|
||||
|
||||
import './IconButton.css';
|
||||
|
||||
export const classes = generateClassNames('IconButton', ['root', 'sm', 'md']);
|
||||
|
||||
export type IconButtonProps = Omit<ButtonLinkProps, 'children'> & {
|
||||
children: FC<{ className?: string }>;
|
||||
};
|
||||
@ -13,7 +18,12 @@ export type IconButtonProps = Omit<ButtonLinkProps, 'children'> & {
|
||||
const IconButton = ({ children, size = 'medium', className, ...otherProps }: ButtonLinkProps) => {
|
||||
return (
|
||||
<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}
|
||||
{...otherProps}
|
||||
>
|
||||
|
@ -1,31 +1,58 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
|
||||
|
||||
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<
|
||||
Required<BaseBaseProps>['variant'],
|
||||
Record<Required<BaseBaseProps>['color'], string>
|
||||
> = {
|
||||
contained: {
|
||||
primary: 'btn-contained-primary',
|
||||
secondary: 'btn-contained-secondary',
|
||||
success: 'btn-contained-success',
|
||||
error: 'btn-contained-error',
|
||||
warning: 'btn-contained-warning',
|
||||
primary: 'CMS_Button_contained-primary',
|
||||
secondary: 'CMS_Button_contained-secondary',
|
||||
success: 'CMS_Button_contained-success',
|
||||
error: 'CMS_Button_contained-error',
|
||||
warning: 'CMS_Button_contained-warning',
|
||||
},
|
||||
outlined: {
|
||||
primary: 'btn-outlined-primary',
|
||||
secondary: 'btn-outlined-secondary',
|
||||
success: 'btn-outlined-success',
|
||||
error: 'btn-outlined-error',
|
||||
warning: 'btn-outlined-warning',
|
||||
primary: 'CMS_Button_outlined-primary',
|
||||
secondary: 'CMS_Button_outlined-secondary',
|
||||
success: 'CMS_Button_outlined-success',
|
||||
error: 'CMS_Button_outlined-error',
|
||||
warning: 'CMS_Button_outlined-warning',
|
||||
},
|
||||
text: {
|
||||
primary: 'btn-text-primary',
|
||||
secondary: 'btn-text-secondary',
|
||||
success: 'btn-text-success',
|
||||
error: 'btn-text-error',
|
||||
warning: 'btn-text-warning',
|
||||
primary: 'CMS_Button_text-primary',
|
||||
secondary: 'CMS_Button_text-secondary',
|
||||
success: 'CMS_Button_text-success',
|
||||
error: 'CMS_Button_text-error',
|
||||
warning: 'CMS_Button_text-warning',
|
||||
},
|
||||
};
|
||||
|
||||
@ -35,11 +62,11 @@ export default function useButtonClassNames(
|
||||
size: Required<BaseBaseProps>['size'],
|
||||
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') {
|
||||
mainClass = 'btn-rounded-no-padding';
|
||||
mainClass = 'CMS_Button_root-rounded-no-padding';
|
||||
} 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]);
|
||||
|
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 classNames from '@staticcms/core/lib/util/classNames.util';
|
||||
import cardClasses from './Card.classes';
|
||||
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
import './Card.css';
|
||||
|
||||
interface CardProps {
|
||||
children: ReactNode | ReactNode[];
|
||||
className?: string;
|
||||
@ -12,25 +15,7 @@ interface CardProps {
|
||||
|
||||
const Card = ({ children, className, title }: CardProps) => {
|
||||
return (
|
||||
<div
|
||||
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}
|
||||
>
|
||||
<div className={classNames(cardClasses.root, className)} title={title}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
@ -1,6 +1,8 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import cardClasses from './Card.classes';
|
||||
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
interface CardActionAreaProps {
|
||||
@ -10,24 +12,7 @@ interface CardActionAreaProps {
|
||||
|
||||
const CardActionArea = ({ to, children }: CardActionAreaProps) => {
|
||||
return (
|
||||
<Link
|
||||
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
|
||||
"
|
||||
>
|
||||
<Link to={to} className={cardClasses.actions}>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
|
@ -1,5 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import cardClasses from './Card.classes';
|
||||
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
interface CardContentProps {
|
||||
@ -7,7 +9,7 @@ interface 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;
|
||||
|
@ -1,5 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import cardClasses from './Card.classes';
|
||||
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
interface CardHeaderProps {
|
||||
@ -7,11 +9,7 @@ interface CardHeaderProps {
|
||||
}
|
||||
|
||||
const CardHeader = ({ children }: CardHeaderProps) => {
|
||||
return (
|
||||
<h5 className="mb-2 text-2xl font-bold tracking-tight text-gray-800 dark:text-white">
|
||||
{children}
|
||||
</h5>
|
||||
);
|
||||
return <h5 className={cardClasses.header}>{children}</h5>;
|
||||
};
|
||||
|
||||
export default CardHeader;
|
||||
|
@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import Image from '../image/Image';
|
||||
import cardClasses from './Card.classes';
|
||||
|
||||
import type {
|
||||
BaseField,
|
||||
@ -31,7 +32,7 @@ const CardMedia = <EF extends BaseField = UnknownField>({
|
||||
}: CardMediaProps<EF>) => {
|
||||
return (
|
||||
<Image
|
||||
className="rounded-t-lg bg-cover bg-no-repeat bg-center w-full object-cover"
|
||||
className={cardClasses.media}
|
||||
style={{
|
||||
width: width ? `${width}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 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 './Checkbox.css';
|
||||
|
||||
export const classes = generateClassNames('Checkbox', [
|
||||
'root',
|
||||
'disabled',
|
||||
'input',
|
||||
'custom-input',
|
||||
'checkmark',
|
||||
]);
|
||||
|
||||
export interface CheckboxProps {
|
||||
checked: boolean;
|
||||
disabled?: boolean;
|
||||
@ -35,15 +46,7 @@ const Checkbox: FC<CheckboxProps> = ({ checked, disabled = false, onChange }) =>
|
||||
|
||||
return (
|
||||
<label
|
||||
className={classNames(
|
||||
`
|
||||
relative
|
||||
inline-flex
|
||||
items-center
|
||||
cursor-pointer
|
||||
`,
|
||||
disabled && 'cursor-default',
|
||||
)}
|
||||
className={classNames(classes.root, disabled && classes.disabled)}
|
||||
onClick={handleNoop}
|
||||
onKeyDown={handleKeydown}
|
||||
>
|
||||
@ -52,59 +55,16 @@ const Checkbox: FC<CheckboxProps> = ({ checked, disabled = false, onChange }) =>
|
||||
ref={inputRef}
|
||||
type="checkbox"
|
||||
checked={checked}
|
||||
className="sr-only peer hide-tap"
|
||||
className={classes.input}
|
||||
disabled={disabled}
|
||||
onChange={onChange}
|
||||
onClick={handleNoop}
|
||||
onKeyDown={handleKeydown}
|
||||
/>
|
||||
<div
|
||||
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}
|
||||
>
|
||||
<div className={classes['custom-input']} onClick={handleClick} onKeyDown={handleKeydown}>
|
||||
{checked ? (
|
||||
<CheckIcon
|
||||
className="
|
||||
w-5
|
||||
h-5
|
||||
text-white
|
||||
"
|
||||
className={classes.checkmark}
|
||||
onClick={handleClick}
|
||||
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 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 Button from '../button/Button';
|
||||
import Modal from '../modal/Modal';
|
||||
|
||||
import type { TranslateProps } from 'react-polyglot';
|
||||
|
||||
import './Confirm.css';
|
||||
|
||||
export const classes = generateClassNames('Confirm', [
|
||||
'root',
|
||||
'title',
|
||||
'content',
|
||||
'actions',
|
||||
'confirm-button',
|
||||
'cancel-button',
|
||||
]);
|
||||
|
||||
interface ConfirmProps {
|
||||
title: string | { key: string; options?: Record<string, unknown> };
|
||||
body: string | { key: string; options?: Record<string, unknown> };
|
||||
@ -83,47 +95,27 @@ const ConfirmDialog = ({ t }: TranslateProps) => {
|
||||
<Modal
|
||||
open
|
||||
onClose={handleCancel}
|
||||
className="
|
||||
w-[50%]
|
||||
min-w-[300px]
|
||||
max-w-[600px]
|
||||
"
|
||||
className={classes.root}
|
||||
aria-labelledby="confirm-dialog-title"
|
||||
aria-describedby="confirm-dialog-description"
|
||||
>
|
||||
<div
|
||||
className="
|
||||
px-6
|
||||
py-4
|
||||
text-xl
|
||||
bold
|
||||
"
|
||||
<div className={classes.title}>{title}</div>
|
||||
<div className={classes.content}>{body}</div>
|
||||
<div className={classes.actions}>
|
||||
<Button
|
||||
onClick={handleCancel}
|
||||
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}
|
||||
</Button>
|
||||
<Button onClick={handleConfirm} variant="contained" color={color}>
|
||||
<Button
|
||||
onClick={handleConfirm}
|
||||
variant="contained"
|
||||
color={color}
|
||||
className={classes['confirm-button']}
|
||||
>
|
||||
{confirm}
|
||||
</Button>
|
||||
</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 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 { FC } from 'react';
|
||||
|
||||
import './ErrorMessage.css';
|
||||
|
||||
export const classes = generateClassNames('ErrorMessage', ['root']);
|
||||
|
||||
export interface ErrorMessageProps {
|
||||
errors: FieldError[];
|
||||
className?: string;
|
||||
@ -12,21 +17,7 @@ export interface ErrorMessageProps {
|
||||
|
||||
const ErrorMessage: FC<ErrorMessageProps> = ({ errors, className }) => {
|
||||
return errors.length ? (
|
||||
<div
|
||||
key="error"
|
||||
data-testid="error"
|
||||
className={classNames(
|
||||
`
|
||||
flex
|
||||
w-full
|
||||
text-xs
|
||||
text-red-500
|
||||
px-3
|
||||
pt-2
|
||||
`,
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div key="error" data-testid="error" className={classNames(classes.root, className)}>
|
||||
{errors[0].message}
|
||||
</div>
|
||||
) : 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 classNames from '@staticcms/core/lib/util/classNames.util';
|
||||
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
|
||||
import ErrorMessage from './ErrorMessage';
|
||||
import Hint from './Hint';
|
||||
import Label from './Label';
|
||||
@ -9,6 +10,25 @@ import Label from './Label';
|
||||
import type { FieldError } from '@staticcms/core/interface';
|
||||
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 {
|
||||
label?: string;
|
||||
inputRef?: React.MutableRefObject<HTMLElement | null>;
|
||||
@ -98,53 +118,34 @@ const Field: FC<FieldProps> = ({
|
||||
const rootClassNames = useMemo(
|
||||
() =>
|
||||
classNames(
|
||||
`
|
||||
relative
|
||||
flex
|
||||
items-center
|
||||
gap-2
|
||||
border-b
|
||||
border-slate-400
|
||||
focus-within:border-blue-800
|
||||
dark:focus-within:border-blue-100
|
||||
`,
|
||||
classes.root,
|
||||
rootClassName,
|
||||
!noHightlight &&
|
||||
!disabled &&
|
||||
`
|
||||
focus-within:bg-slate-100
|
||||
dark:focus-within:bg-slate-800
|
||||
hover:bg-slate-100
|
||||
dark:hover:bg-slate-800
|
||||
`,
|
||||
!noPadding && 'pb-3',
|
||||
finalCursor === 'pointer' && 'cursor-pointer',
|
||||
finalCursor === 'text' && 'cursor-text',
|
||||
finalCursor === 'default' && 'cursor-default',
|
||||
!hasErrors && 'group/active',
|
||||
disabled && classes.disabled,
|
||||
noHightlight && classes['no-highlight'],
|
||||
noPadding && classes['no-padding'],
|
||||
finalCursor === 'pointer' && classes['cursor-pointer'],
|
||||
finalCursor === 'text' && classes['cursor-text'],
|
||||
finalCursor === 'default' && classes['cursor-default'],
|
||||
hasErrors ? classes.error : `group/active`,
|
||||
),
|
||||
[rootClassName, noHightlight, disabled, noPadding, finalCursor, hasErrors],
|
||||
);
|
||||
|
||||
const wrapperClassNames = useMemo(
|
||||
() =>
|
||||
classNames(
|
||||
`
|
||||
flex
|
||||
flex-col
|
||||
w-full
|
||||
`,
|
||||
wrapperClassName,
|
||||
forSingleList && 'mr-14',
|
||||
),
|
||||
classNames(classes.wrapper, wrapperClassName, forSingleList && classes['for-single-list']),
|
||||
[forSingleList, wrapperClassName],
|
||||
);
|
||||
|
||||
if (variant === 'inline') {
|
||||
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 className="flex items-center justify-center p-3 pb-0">
|
||||
<div className={classes['inline-wrapper']}>
|
||||
{renderedLabel}
|
||||
{renderedHint}
|
||||
{children}
|
||||
@ -163,18 +164,7 @@ const Field: FC<FieldProps> = ({
|
||||
{renderedHint}
|
||||
{renderedErrorMessage}
|
||||
</div>
|
||||
{endAdornment ? (
|
||||
<div
|
||||
className={classNames(
|
||||
`
|
||||
pr-2
|
||||
`,
|
||||
!noPadding && '-mb-3',
|
||||
)}
|
||||
>
|
||||
{endAdornment}
|
||||
</div>
|
||||
) : null}
|
||||
{endAdornment ? <div className={classes['end-adornment']}>{endAdornment}</div> : null}
|
||||
</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 classNames from '@staticcms/core/lib/util/classNames.util';
|
||||
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
|
||||
|
||||
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 {
|
||||
children: string;
|
||||
hasErrors: boolean;
|
||||
@ -28,32 +41,13 @@ const Hint: FC<HintProps> = ({
|
||||
<div
|
||||
data-testid="hint"
|
||||
className={classNames(
|
||||
`
|
||||
w-full
|
||||
flex
|
||||
text-xs
|
||||
italic
|
||||
`,
|
||||
!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-1',
|
||||
classes.root,
|
||||
disabled && classes.disabled,
|
||||
finalCursor === 'pointer' && classes['cursor-pointer'],
|
||||
finalCursor === 'text' && classes['cursor-text'],
|
||||
finalCursor === 'default' && classes['cursor-default'],
|
||||
hasErrors && classes.error,
|
||||
variant === 'inline' && classes.inline,
|
||||
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 classNames from '@staticcms/core/lib/util/classNames.util';
|
||||
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
|
||||
|
||||
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 {
|
||||
htmlFor?: string;
|
||||
children: string;
|
||||
@ -33,33 +46,13 @@ const Label: FC<LabelProps> = ({
|
||||
htmlFor={htmlFor}
|
||||
data-testid={dataTestId ?? 'label'}
|
||||
className={classNames(
|
||||
`
|
||||
w-full
|
||||
flex
|
||||
text-xs
|
||||
font-bold
|
||||
dark:font-semibold
|
||||
`,
|
||||
!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',
|
||||
classes.root,
|
||||
disabled && classes.disabled,
|
||||
finalCursor === 'pointer' && classes['cursor-pointer'],
|
||||
finalCursor === 'text' && classes['cursor-text'],
|
||||
finalCursor === 'default' && classes['cursor-default'],
|
||||
hasErrors && classes.error,
|
||||
variant === 'inline' && classes.inline,
|
||||
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 React from 'react';
|
||||
|
||||
import useMediaAsset from '@staticcms/core/lib/hooks/useMediaAsset';
|
||||
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 { useAppSelector } from '@staticcms/core/store/hooks';
|
||||
import { isEmpty } from '@staticcms/core/lib/util/string.util';
|
||||
|
||||
import type {
|
||||
BaseField,
|
||||
@ -16,6 +17,10 @@ import type {
|
||||
} from '@staticcms/core/interface';
|
||||
import type { CSSProperties } from 'react';
|
||||
|
||||
import './Image.css';
|
||||
|
||||
export const classes = generateClassNames('Image', ['root', 'empty']);
|
||||
|
||||
export interface ImageProps<EF extends BaseField> {
|
||||
src?: string;
|
||||
alt?: string;
|
||||
@ -42,18 +47,7 @@ const Image = <EF extends BaseField = UnknownField>({
|
||||
const assetSource = useMediaAsset(src, collection, field, entry ?? editingDraft);
|
||||
|
||||
if (isEmpty(src)) {
|
||||
return (
|
||||
<ImageIcon
|
||||
className="
|
||||
p-10
|
||||
rounded-md
|
||||
border
|
||||
max-w-full
|
||||
border-gray-200/75
|
||||
dark:border-slate-600/75
|
||||
"
|
||||
/>
|
||||
);
|
||||
return <ImageIcon className={classNames(classes.root, classes.empty, className)} />;
|
||||
}
|
||||
|
||||
return (
|
||||
@ -63,7 +57,7 @@ const Image = <EF extends BaseField = UnknownField>({
|
||||
src={assetSource}
|
||||
alt={alt}
|
||||
data-testid={dataTestId ?? 'image'}
|
||||
className={classNames('object-cover max-w-full overflow-hidden', className)}
|
||||
className={classNames(classes.root, className)}
|
||||
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 MenuUnstyled from '@mui/base/MenuUnstyled';
|
||||
import { Dropdown } from '@mui/base/Dropdown';
|
||||
import { Menu as BaseMenu } from '@mui/base/Menu';
|
||||
import { MenuButton } from '@mui/base/MenuButton';
|
||||
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 { generateClassNames } from '@staticcms/core/lib/util/theming.util';
|
||||
import useButtonClassNames from '../button/useButtonClassNames';
|
||||
|
||||
import type { FC, ReactNode } from 'react';
|
||||
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 {
|
||||
label: ReactNode;
|
||||
startIcon?: FC<{ className?: string }>;
|
||||
@ -24,8 +40,8 @@ export interface MenuProps {
|
||||
hideDropdownIcon?: boolean;
|
||||
hideDropdownIconOnMobile?: boolean;
|
||||
hideLabel?: boolean;
|
||||
keepMounted?: boolean;
|
||||
disabled?: boolean;
|
||||
keepMounted?: boolean;
|
||||
'data-testid'?: string;
|
||||
}
|
||||
|
||||
@ -44,103 +60,56 @@ const Menu = ({
|
||||
hideDropdownIcon = false,
|
||||
hideDropdownIconOnMobile = false,
|
||||
hideLabel = false,
|
||||
keepMounted = false,
|
||||
disabled = false,
|
||||
keepMounted = false,
|
||||
'data-testid': dataTestId,
|
||||
}: 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 menuButtonClassNames = useMemo(
|
||||
() => classNames(calculatedButtonClassName, buttonClassName, 'whitespace-nowrap'),
|
||||
() => classNames(calculatedButtonClassName, buttonClassName, classes.dropdown),
|
||||
[calculatedButtonClassName, buttonClassName],
|
||||
);
|
||||
|
||||
return (
|
||||
<ClickAwayListener mouseEvent="onMouseDown" touchEvent="onTouchStart" onClickAway={handleClose}>
|
||||
<div className={classNames('flex', rootClassName)}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleButtonClick}
|
||||
aria-controls={isOpen ? 'simple-menu' : undefined}
|
||||
aria-expanded={isOpen || undefined}
|
||||
<Dropdown>
|
||||
<div
|
||||
className={classNames(
|
||||
classes.root,
|
||||
hideLabel && classes['hide-label'],
|
||||
hideDropdownIcon && classes['hide-dropdown-icon'],
|
||||
hideDropdownIconOnMobile && classes['hide-dropdown-icon-mobile'],
|
||||
rootClassName,
|
||||
)}
|
||||
>
|
||||
<MenuButton
|
||||
aria-haspopup="menu"
|
||||
data-testid={dataTestId}
|
||||
className={menuButtonClassNames}
|
||||
disabled={disabled}
|
||||
>
|
||||
{StartIcon ? (
|
||||
<StartIcon
|
||||
className={classNames(
|
||||
`-ml-0.5 h-5 w-5`,
|
||||
!hideLabel && !hideDropdownIcon && 'mr-1.5',
|
||||
hideDropdownIconOnMobile && '!mr-0 md:!mr-1.5',
|
||||
iconClassName,
|
||||
)}
|
||||
/>
|
||||
<StartIcon className={classNames(classes['dropdown-start-icon'], iconClassName)} />
|
||||
) : null}
|
||||
{!hideLabel ? (
|
||||
<div className={classNames(classes.label, labelClassName)}>{label}</div>
|
||||
) : null}
|
||||
{!hideLabel ? <div className={labelClassName}>{label}</div> : null}
|
||||
{!hideDropdownIcon ? (
|
||||
<KeyboardArrowDownIcon
|
||||
className={classNames(
|
||||
`-mr-0.5 h-5 w-5`,
|
||||
!hideLabel && 'ml-2',
|
||||
hideDropdownIconOnMobile && '!hidden md:!block',
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<KeyboardArrowDownIcon className={classes['dropdown-icon']} aria-hidden="true" />
|
||||
) : null}
|
||||
</button>
|
||||
<MenuUnstyled
|
||||
open={isOpen}
|
||||
anchorEl={anchorEl}
|
||||
keepMounted={keepMounted}
|
||||
</MenuButton>
|
||||
<BaseMenu
|
||||
slotProps={{
|
||||
root: {
|
||||
className: `
|
||||
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
|
||||
`,
|
||||
onClick: handleClose,
|
||||
className: classes.menu,
|
||||
keepMounted,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</MenuUnstyled>
|
||||
</BaseMenu>
|
||||
</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 { generateClassNames } from '@staticcms/core/lib/util/theming.util';
|
||||
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
import './MenuGroup.css';
|
||||
|
||||
export const classes = generateClassNames('MenuGroup', ['root']);
|
||||
|
||||
export interface MenuGroupProps {
|
||||
children: ReactNode | ReactNode[];
|
||||
}
|
||||
|
||||
const MenuGroup = ({ children }: MenuGroupProps) => {
|
||||
return (
|
||||
<div
|
||||
className="
|
||||
py-1
|
||||
border-b
|
||||
border-gray-200
|
||||
dark:border-slate-700
|
||||
"
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
return <div className={classes.root}>{children}</div>;
|
||||
};
|
||||
|
||||
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 MenuItemUnstyled from '@mui/base/MenuItemUnstyled';
|
||||
|
||||
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 './MenuItemButton.css';
|
||||
|
||||
export const classes = generateClassNames('MenuItemButton', [
|
||||
'root',
|
||||
'disabled',
|
||||
'active',
|
||||
'default',
|
||||
'warning',
|
||||
'error',
|
||||
'content',
|
||||
'start-icon',
|
||||
'end-icon',
|
||||
]);
|
||||
|
||||
export interface MenuItemButtonProps {
|
||||
active?: boolean;
|
||||
onClick: (event: MouseEvent) => void;
|
||||
@ -29,50 +44,17 @@ const MenuItemButton = ({
|
||||
'data-testid': dataTestId,
|
||||
}: MenuItemButtonProps) => {
|
||||
return (
|
||||
<MenuItemUnstyled
|
||||
<MenuItem
|
||||
slotProps={{
|
||||
root: {
|
||||
className: classNames(
|
||||
className,
|
||||
active ? 'bg-slate-200 dark:bg-slate-600' : '',
|
||||
`
|
||||
px-4
|
||||
py-2
|
||||
text-sm
|
||||
w-full
|
||||
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
|
||||
`,
|
||||
classes.root,
|
||||
disabled && classes.disabled,
|
||||
active && classes.active,
|
||||
color === 'default' && classes.default,
|
||||
color === 'warning' && classes.warning,
|
||||
color === 'error' && classes.error,
|
||||
),
|
||||
},
|
||||
}}
|
||||
@ -80,12 +62,12 @@ const MenuItemButton = ({
|
||||
disabled={disabled}
|
||||
data-testid={dataTestId}
|
||||
>
|
||||
<div className="flex items-center gap-2 flex-grow">
|
||||
{StartIcon ? <StartIcon className="h-5 w-5" /> : null}
|
||||
<div className={classes.content}>
|
||||
{StartIcon ? <StartIcon className={classes['start-icon']} /> : null}
|
||||
{children}
|
||||
</div>
|
||||
{EndIcon ? <EndIcon className="h-5 w-5" /> : null}
|
||||
</MenuItemUnstyled>
|
||||
{EndIcon ? <EndIcon className={classes['end-icon']} /> : null}
|
||||
</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 { NavLink } from 'react-router-dom';
|
||||
import MenuItemUnstyled from '@mui/base/MenuItemUnstyled';
|
||||
|
||||
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 './MenuItemLink.css';
|
||||
|
||||
export const classes = generateClassNames('MenuItemLink', [
|
||||
'root',
|
||||
'active',
|
||||
'content',
|
||||
'start-icon',
|
||||
'end-icon',
|
||||
]);
|
||||
|
||||
export interface MenuItemLinkProps {
|
||||
href: string;
|
||||
children: ReactNode;
|
||||
@ -24,38 +35,21 @@ const MenuItemLink = ({
|
||||
endIcon: EndIcon,
|
||||
}: MenuItemLinkProps) => {
|
||||
return (
|
||||
<MenuItemUnstyled
|
||||
component={NavLink}
|
||||
to={href}
|
||||
<NavLink to={href}>
|
||||
<MenuItem
|
||||
slotProps={{
|
||||
root: {
|
||||
className: classNames(
|
||||
className,
|
||||
active ? 'bg-slate-100 dark:bg-slate-900' : '',
|
||||
`
|
||||
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
|
||||
`,
|
||||
),
|
||||
className: classNames(className, classes.root, active && classes.active),
|
||||
},
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2 flex-grow">
|
||||
{StartIcon ? <StartIcon className="h-5 w-5" /> : null}
|
||||
<div className={classes.content}>
|
||||
{StartIcon ? <StartIcon className={classes['start-icon']} /> : null}
|
||||
{children}
|
||||
</div>
|
||||
{EndIcon ? <EndIcon className="h-5 w-5" /> : null}
|
||||
</MenuItemUnstyled>
|
||||
{EndIcon ? <EndIcon className={classes['end-icon']} /> : null}
|
||||
</MenuItem>
|
||||
</NavLink>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import classNames from '@staticcms/core/lib/util/classNames.util';
|
||||
import modalClasses from './Modal.classes';
|
||||
|
||||
const Backdrop = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
@ -9,18 +10,7 @@ const Backdrop = React.forwardRef<
|
||||
const { open, className, ownerState: _ownerState, ...other } = props;
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
`
|
||||
fixed
|
||||
inset-0
|
||||
bg-black
|
||||
bg-opacity-50
|
||||
dark:bg-opacity-60
|
||||
z-50
|
||||
`,
|
||||
open && 'MuiBackdrop-open',
|
||||
className,
|
||||
)}
|
||||
className={classNames(modalClasses.backdrop, open && 'MuiBackdrop-open', className)}
|
||||
ref={ref}
|
||||
{...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 classNames from '@staticcms/core/lib/util/classNames.util';
|
||||
import Backdrop from './Backdrop';
|
||||
import modalClasses from './Modal.classes';
|
||||
|
||||
import type { FC, ReactNode } from 'react';
|
||||
|
||||
import './Modal.css';
|
||||
|
||||
interface ModalProps {
|
||||
open: boolean;
|
||||
children: ReactNode;
|
||||
@ -19,7 +22,7 @@ const Modal: FC<ModalProps> = ({ open, children, className, onClose }) => {
|
||||
}, [onClose]);
|
||||
|
||||
return (
|
||||
<ModalUnstyled
|
||||
<BaseModal
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
slots={{
|
||||
@ -27,42 +30,12 @@ const Modal: FC<ModalProps> = ({ open, children, className, onClose }) => {
|
||||
}}
|
||||
slotProps={{
|
||||
root: {
|
||||
className: `
|
||||
fixed
|
||||
inset-0
|
||||
overflow-y-auto
|
||||
z-50
|
||||
flex
|
||||
min-h-full
|
||||
items-center
|
||||
justify-center
|
||||
text-center
|
||||
styled-scrollbars
|
||||
`,
|
||||
className: modalClasses.root,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
`
|
||||
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>
|
||||
<div className={classNames(modalClasses.content, className)}>{children}</div>
|
||||
</BaseModal>
|
||||
);
|
||||
};
|
||||
|
||||
|
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 classNames from '@staticcms/core/lib/util/classNames.util';
|
||||
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
|
||||
|
||||
import type { FC, ReactNode } from 'react';
|
||||
|
||||
import './Pill.css';
|
||||
|
||||
export const classes = generateClassNames('Pill', [
|
||||
'root',
|
||||
'no-wrap',
|
||||
'primary',
|
||||
'default',
|
||||
'disabled',
|
||||
]);
|
||||
|
||||
interface PillProps {
|
||||
children: ReactNode | ReactNode[];
|
||||
noWrap?: boolean;
|
||||
@ -22,48 +33,18 @@ const Pill: FC<PillProps> = ({
|
||||
const colorClassNames = useMemo(() => {
|
||||
switch (color) {
|
||||
case 'primary':
|
||||
return disabled
|
||||
? `
|
||||
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
|
||||
`;
|
||||
return classes.primary;
|
||||
default:
|
||||
return disabled
|
||||
? `
|
||||
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
|
||||
`;
|
||||
return classes.default;
|
||||
}
|
||||
}, [color, disabled]);
|
||||
}, [color]);
|
||||
|
||||
return (
|
||||
<span
|
||||
className={classNames(
|
||||
`
|
||||
text-xs
|
||||
font-medium
|
||||
px-3
|
||||
py-1
|
||||
rounded-lg
|
||||
truncate
|
||||
`,
|
||||
noWrap && 'whitespace-nowrap',
|
||||
classes.root,
|
||||
noWrap && classes['no-wrap'],
|
||||
disabled && classes.disabled,
|
||||
colorClassNames,
|
||||
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 classNames from '@staticcms/core/lib/util/classNames.util';
|
||||
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
|
||||
|
||||
import type { FC } from 'react';
|
||||
|
||||
import './CircularProgress.css';
|
||||
|
||||
export const classes = generateClassNames('CircularProgress', [
|
||||
'root',
|
||||
'svg',
|
||||
'md',
|
||||
'sm',
|
||||
'sr-label',
|
||||
]);
|
||||
|
||||
export interface CircularProgressProps {
|
||||
className?: string;
|
||||
'data-testid'?: string;
|
||||
@ -16,27 +27,13 @@ const CircularProgress: FC<CircularProgressProps> = ({
|
||||
size = 'medium',
|
||||
}) => {
|
||||
return (
|
||||
<div role="status" className={className} data-testid={dataTestId}>
|
||||
<div role="status" className={classNames(classes.root, className)} data-testid={dataTestId}>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className={classNames(
|
||||
`
|
||||
mr-2
|
||||
text-gray-200
|
||||
animate-spin
|
||||
dark:text-gray-600
|
||||
fill-blue-600
|
||||
`,
|
||||
size === 'medium' &&
|
||||
`
|
||||
w-8
|
||||
h-8
|
||||
`,
|
||||
size === 'small' &&
|
||||
`
|
||||
w-5
|
||||
h-5
|
||||
`,
|
||||
classes.svg,
|
||||
size === 'medium' && classes.md,
|
||||
size === 'small' && classes.sm,
|
||||
)}
|
||||
viewBox="0 0 100 101"
|
||||
fill="none"
|
||||
@ -51,7 +48,7 @@ const CircularProgress: FC<CircularProgressProps> = ({
|
||||
fill="currentFill"
|
||||
/>
|
||||
</svg>
|
||||
<span className="sr-only">Loading...</span>
|
||||
<span className={classes['sr-label']}>Loading...</span>
|
||||
</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 { generateClassNames } from '@staticcms/core/lib/util/theming.util';
|
||||
import CircularProgress from './CircularProgress';
|
||||
|
||||
import './Loader.css';
|
||||
|
||||
export const classes = generateClassNames('Loader', ['root']);
|
||||
|
||||
export interface LoaderProps {
|
||||
children: string | string[] | undefined;
|
||||
}
|
||||
@ -35,19 +40,7 @@ const Loader = ({ children }: LoaderProps) => {
|
||||
}, [children, currentItem]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="
|
||||
absolute
|
||||
inset-0
|
||||
flex
|
||||
flex-col
|
||||
gap-2
|
||||
items-center
|
||||
justify-center
|
||||
bg-slate-50
|
||||
dark:bg-slate-900
|
||||
"
|
||||
>
|
||||
<div className={classes.root}>
|
||||
<CircularProgress />
|
||||
<div>{text}</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 classNames from '@staticcms/core/lib/util/classNames.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 './Option.css';
|
||||
|
||||
export const classes = generateClassNames('SelectOption', ['root', 'selected', 'label']);
|
||||
|
||||
export interface OptionProps<T> {
|
||||
selectedValue: T | null | T[];
|
||||
value: T | null;
|
||||
@ -28,31 +33,17 @@ const Option = function <T>({
|
||||
);
|
||||
|
||||
return (
|
||||
<OptionUnstyled
|
||||
<BaseOption
|
||||
value={value}
|
||||
data-testid={dataTestId}
|
||||
slotProps={{
|
||||
root: {
|
||||
className: classNames(
|
||||
`
|
||||
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' : '',
|
||||
),
|
||||
className: classNames(classes.root, selected && classes.selected),
|
||||
},
|
||||
}}
|
||||
>
|
||||
<span className={classNames('block truncate', selected ? 'font-medium' : 'font-normal')}>
|
||||
{children}
|
||||
</span>
|
||||
</OptionUnstyled>
|
||||
<span className={classes.label}>{children}</span>
|
||||
</BaseOption>
|
||||
);
|
||||
};
|
||||
|
||||
|
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 React, { forwardRef, useCallback, useState } from 'react';
|
||||
|
||||
import useElementSize from '@staticcms/core/lib/hooks/useElementSize';
|
||||
import classNames from '@staticcms/core/lib/util/classNames.util';
|
||||
import { isNotEmpty } from '@staticcms/core/lib/util/string.util';
|
||||
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
|
||||
import Option from './Option';
|
||||
|
||||
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 {
|
||||
label: string;
|
||||
value: number | string;
|
||||
@ -31,6 +48,7 @@ export interface SelectProps {
|
||||
options: (number | string)[] | Option[];
|
||||
required?: boolean;
|
||||
disabled?: boolean;
|
||||
rootClassName?: string;
|
||||
onChange: SelectChangeEventHandler;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
}
|
||||
@ -44,6 +62,7 @@ const Select = forwardRef(
|
||||
options,
|
||||
required = false,
|
||||
disabled,
|
||||
rootClassName,
|
||||
onChange,
|
||||
onOpenChange,
|
||||
}: SelectProps,
|
||||
@ -82,88 +101,47 @@ const Select = forwardRef(
|
||||
[onChange, value],
|
||||
);
|
||||
|
||||
const handleClick = useCallback(
|
||||
(event: MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
handleOpenChange(!open);
|
||||
},
|
||||
[handleOpenChange, open],
|
||||
);
|
||||
|
||||
const handleClickAway = useCallback(() => {
|
||||
handleOpenChange(false);
|
||||
}, [handleOpenChange]);
|
||||
|
||||
return (
|
||||
<div className="relative w-full">
|
||||
<ClickAwayListener onClickAway={handleClickAway}>
|
||||
<div className={classNames(classes.root, rootClassName)}>
|
||||
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
|
||||
<SelectUnstyled<any>
|
||||
<BaseSelect<any>
|
||||
renderValue={() => {
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="flex flex-start w-select-widget-label">
|
||||
<span className="truncate">{label ?? placeholder}</span>
|
||||
<div className={classes.value}>
|
||||
<div className={classes.label}>
|
||||
<span className={classes['label-text']}>{label ?? placeholder}</span>
|
||||
</div>
|
||||
<span
|
||||
className="
|
||||
pointer-events-none
|
||||
absolute
|
||||
inset-y-0
|
||||
right-0
|
||||
flex
|
||||
items-center
|
||||
pr-2
|
||||
"
|
||||
>
|
||||
<span className={classes.dropdown}>
|
||||
<KeyboardArrowDownIcon
|
||||
className={classNames(
|
||||
`
|
||||
h-5
|
||||
w-5
|
||||
text-gray-400
|
||||
`,
|
||||
disabled &&
|
||||
`
|
||||
text-gray-300/75
|
||||
dark:text-gray-600/75
|
||||
`,
|
||||
)}
|
||||
className={classes['dropdown-icon']}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
ref={ref}
|
||||
onClick={handleClick}
|
||||
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
|
||||
`,
|
||||
),
|
||||
className: classes.input,
|
||||
},
|
||||
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
|
||||
`,
|
||||
className: classes.popper,
|
||||
style: { width: ref ? width : 'auto' },
|
||||
disablePortal: false,
|
||||
},
|
||||
@ -172,7 +150,6 @@ const Select = forwardRef(
|
||||
disabled={disabled}
|
||||
onChange={handleChange}
|
||||
listboxOpen={open}
|
||||
onListboxOpenChange={handleOpenChange}
|
||||
data-testid="select-input"
|
||||
>
|
||||
{!Array.isArray(value) && !required ? (
|
||||
@ -194,8 +171,9 @@ const Select = forwardRef(
|
||||
</Option>
|
||||
);
|
||||
})}
|
||||
</SelectUnstyled>
|
||||
</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 classNames from '@staticcms/core/lib/util/classNames.util';
|
||||
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
|
||||
|
||||
import type { ChangeEvent, ChangeEventHandler } from 'react';
|
||||
|
||||
import './Switch.css';
|
||||
|
||||
export const classes = generateClassNames('Switch', [
|
||||
'root',
|
||||
'disabled',
|
||||
'input',
|
||||
'toggle',
|
||||
'label',
|
||||
]);
|
||||
|
||||
export interface SwitchProps {
|
||||
label?: string;
|
||||
value: boolean;
|
||||
disabled?: boolean;
|
||||
rootClassName?: string;
|
||||
inputClassName?: string;
|
||||
onChange?: ChangeEventHandler<HTMLInputElement>;
|
||||
}
|
||||
|
||||
const Switch = forwardRef<HTMLInputElement | null, SwitchProps>(
|
||||
({ label, value, disabled, onChange }, ref) => {
|
||||
({ label, value, disabled, rootClassName, inputClassName, onChange }, ref) => {
|
||||
const handleChange = useCallback(
|
||||
(event: ChangeEvent<HTMLInputElement>) => {
|
||||
onChange?.(event);
|
||||
@ -21,78 +34,19 @@ const Switch = forwardRef<HTMLInputElement | null, SwitchProps>(
|
||||
);
|
||||
|
||||
return (
|
||||
<label
|
||||
className={classNames(
|
||||
`
|
||||
relative
|
||||
inline-flex
|
||||
items-center
|
||||
cursor-pointer
|
||||
`,
|
||||
disabled && 'cursor-default',
|
||||
)}
|
||||
>
|
||||
<label className={classNames(classes.root, disabled && classes.disabled, rootClassName)}>
|
||||
<input
|
||||
data-testid="switch-input"
|
||||
ref={ref}
|
||||
type="checkbox"
|
||||
checked={value}
|
||||
className="sr-only peer"
|
||||
className={classNames(classes.input, inputClassName)}
|
||||
disabled={disabled}
|
||||
onChange={handleChange}
|
||||
onClick={() => false}
|
||||
/>
|
||||
<div
|
||||
className={classNames(
|
||||
`
|
||||
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}
|
||||
<div className={classes.toggle} />
|
||||
{label ? <span className={classes.label}>{label}</span> : null}
|
||||
</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 tableClasses from './Table.classes';
|
||||
import TableHeaderCell from './TableHeaderCell';
|
||||
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
import './Table.css';
|
||||
|
||||
interface TableCellProps {
|
||||
columns: ReactNode[];
|
||||
children: ReactNode[];
|
||||
@ -11,20 +14,16 @@ interface TableCellProps {
|
||||
|
||||
const TableCell = ({ columns, children }: TableCellProps) => {
|
||||
return (
|
||||
<div
|
||||
className="
|
||||
z-[2]
|
||||
"
|
||||
>
|
||||
<table className="w-full text-sm text-left text-gray-500 dark:text-gray-300">
|
||||
<thead className="text-xs">
|
||||
<tr className="shadow-sm">
|
||||
<div className={tableClasses.root}>
|
||||
<table className={tableClasses.table}>
|
||||
<thead className={tableClasses.header}>
|
||||
<tr className={tableClasses['header-row']}>
|
||||
{columns.map((column, index) => (
|
||||
<TableHeaderCell key={index}>{column}</TableHeaderCell>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>{children}</tbody>
|
||||
<tbody className={tableClasses.body}>{children}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
|
@ -2,6 +2,7 @@ import React, { useMemo } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import classNames from '@staticcms/core/lib/util/classNames.util';
|
||||
import tableClasses from './Table.classes';
|
||||
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
@ -16,18 +17,7 @@ const TableCell = ({ children, emphasis = false, to, shrink = false }: TableCell
|
||||
const content = useMemo(() => {
|
||||
if (to) {
|
||||
return (
|
||||
<Link
|
||||
to={to}
|
||||
className="
|
||||
w-full
|
||||
h-full
|
||||
flex
|
||||
px-4
|
||||
py-3
|
||||
whitespace-nowrap
|
||||
"
|
||||
tabIndex={-1}
|
||||
>
|
||||
<Link to={to} className={tableClasses['body-cell-link']} tabIndex={-1}>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
@ -39,24 +29,13 @@ const TableCell = ({ children, emphasis = false, to, shrink = false }: TableCell
|
||||
return (
|
||||
<td
|
||||
className={classNames(
|
||||
!to ? 'px-4 py-3' : 'p-0',
|
||||
`
|
||||
text-gray-500
|
||||
dark:text-gray-300
|
||||
`,
|
||||
emphasis && 'font-medium text-gray-800 whitespace-nowrap dark:text-white',
|
||||
shrink && 'w-0',
|
||||
tableClasses['body-cell'],
|
||||
to && tableClasses['body-cell-has-link'],
|
||||
emphasis && tableClasses['body-cell-emphasis'],
|
||||
shrink && tableClasses['body-cell-shrink'],
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className="
|
||||
h-[44px]
|
||||
truncate
|
||||
w-full
|
||||
"
|
||||
>
|
||||
{content}
|
||||
</div>
|
||||
<div className={tableClasses['body-cell-content']}>{content}</div>
|
||||
</td>
|
||||
);
|
||||
};
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import classNames from '@staticcms/core/lib/util/classNames.util';
|
||||
import { isEmpty } from '@staticcms/core/lib/util/string.util';
|
||||
import tableClasses from './Table.classes';
|
||||
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
@ -11,34 +11,8 @@ interface TableHeaderCellProps {
|
||||
|
||||
const TableHeaderCell = ({ children }: TableHeaderCellProps) => {
|
||||
return (
|
||||
<th
|
||||
scope="col"
|
||||
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
|
||||
"
|
||||
>
|
||||
<th scope="col" className={tableClasses['header-cell']}>
|
||||
<div className={tableClasses['header-cell-content']}>
|
||||
{typeof children === 'string' && isEmpty(children) ? <> </> : children}
|
||||
</div>
|
||||
</th>
|
||||
|
@ -2,6 +2,7 @@ import React, { useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import classNames from '@staticcms/core/lib/util/classNames.util';
|
||||
import tableClasses from './Table.classes';
|
||||
|
||||
import type { KeyboardEvent, ReactNode } from 'react';
|
||||
|
||||
@ -28,22 +29,7 @@ const TableRow = ({ children, className, to }: TableRowProps) => {
|
||||
|
||||
return (
|
||||
<tr
|
||||
className={classNames(
|
||||
`
|
||||
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,
|
||||
)}
|
||||
className={classNames(tableClasses['body-row'], className)}
|
||||
tabIndex={to ? 0 : -1}
|
||||
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 React, { forwardRef, useCallback, useState } from 'react';
|
||||
import { Input } from '@mui/base/Input';
|
||||
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 './TextArea.css';
|
||||
|
||||
export const classes = generateClassNames('TextArea', ['root', 'input']);
|
||||
|
||||
export interface TextAreaProps {
|
||||
value: string;
|
||||
disabled?: boolean;
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
rootClassName?: string;
|
||||
inputClassName?: string;
|
||||
'data-testid'?: string;
|
||||
onChange: ChangeEventHandler<HTMLInputElement>;
|
||||
}
|
||||
@ -20,7 +28,18 @@ function getHeight(rawHeight: string): number {
|
||||
}
|
||||
|
||||
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 autoGrow = useCallback(() => {
|
||||
@ -43,16 +62,22 @@ const TextArea = forwardRef<HTMLInputElement | null, TextAreaProps>(
|
||||
return;
|
||||
}
|
||||
|
||||
textarea.style.height = `${newHeight}px`;
|
||||
setLastAutogrowHeight(newHeight);
|
||||
|
||||
if (newHeight > MIN_TEXT_AREA_HEIGHT - MIN_BOTTOM_PADDING) {
|
||||
textarea.style.paddingBottom = `${MIN_BOTTOM_PADDING}px`;
|
||||
newHeight += MIN_BOTTOM_PADDING;
|
||||
}
|
||||
|
||||
textarea.style.height = `${newHeight}px`;
|
||||
setLastAutogrowHeight(newHeight);
|
||||
}, [lastAutogrowHeight, ref]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
autoGrow();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<InputUnstyled
|
||||
<Input
|
||||
multiline
|
||||
minRows={4}
|
||||
onInput={autoGrow}
|
||||
@ -62,28 +87,12 @@ const TextArea = forwardRef<HTMLInputElement | null, TextAreaProps>(
|
||||
data-testid={dataTestId ?? 'textarea-input'}
|
||||
slotProps={{
|
||||
root: {
|
||||
className: `
|
||||
flex
|
||||
w-full
|
||||
${className}
|
||||
`,
|
||||
className: classNames(classes.root, rootClassName),
|
||||
},
|
||||
input: {
|
||||
ref,
|
||||
placeholder,
|
||||
className: `
|
||||
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
|
||||
`,
|
||||
className: classNames(classes.input, inputClassName),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
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