feat: mobile support (#831)

This commit is contained in:
Daniel Lautzenheiser 2023-05-31 11:55:53 -04:00 committed by GitHub
parent 7744df1103
commit 83c9d91b88
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
67 changed files with 2210 additions and 1056 deletions

View File

@ -870,7 +870,7 @@ collections:
- value: 2
label: Another fancy label
- value: c
label: And one more fancy label
label: And one more fancy label test test test test test test test
- label: Value and Label With Default
name: value_and_label_with_default
widget: select

View File

@ -84,9 +84,9 @@
"@styled-icons/material-rounded": "10.47.0",
"@styled-icons/remix-editor": "10.46.0",
"@styled-icons/simple-icons": "10.46.0",
"@udecode/plate": "21.1.4",
"@udecode/plate-juice": "21.0.0",
"@udecode/plate-serializer-md": "21.0.0",
"@udecode/plate": "21.3.2",
"@udecode/plate-juice": "21.3.2",
"@udecode/plate-serializer-md": "21.3.2",
"@uiw/codemirror-extensions-langs": "4.19.16",
"@uiw/react-codemirror": "4.19.16",
"ajv": "8.12.0",
@ -156,10 +156,10 @@
"sanitize-filename": "1.6.3",
"scheduler": "0.23.0",
"semaphore": "1.1.0",
"slate": "0.91.4",
"slate-history": "0.86.0",
"slate": "0.94.1",
"slate-history": "0.93.0",
"slate-hyperscript": "0.77.0",
"slate-react": "0.91.10",
"slate-react": "0.95.0",
"stream-browserify": "3.0.0",
"styled-components": "5.3.10",
"symbol-observable": "4.0.0",
@ -271,6 +271,7 @@
"tailwindcss": "3.3.1",
"to-string-loader": "1.2.0",
"ts-jest": "29.1.0",
"ts-node": "10.9.1",
"tsconfig-paths-webpack-plugin": "4.0.1",
"typescript": "5.0.4",
"webpack": "5.80.0",

View File

@ -16,6 +16,7 @@ import addExtensions from './extensions';
import { getPhrases } from './lib/phrases';
import { selectLocale } from './reducers/selectors/config';
import { store } from './store';
import useMeta from './lib/hooks/useMeta';
import type { AnyAction } from '@reduxjs/toolkit';
import type { ConnectedProps } from 'react-redux';
@ -45,6 +46,8 @@ import ReactDOM from 'react-dom';
ReactDOM.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.usingClientEntryPoint = true;
const TranslatedApp = ({ locale, config }: AppRootProps) => {
useMeta({ name: 'viewport', content: 'width=device-width, initial-scale=1.0' });
if (!config) {
return null;
}

View File

@ -2,11 +2,12 @@ import React from 'react';
import TopBarProgress from 'react-topbar-progress-indicator';
import classNames from '../lib/util/classNames.util';
import BottomNavigation from './navbar/BottomNavigation';
import Navbar from './navbar/Navbar';
import Sidebar from './navbar/Sidebar';
import type { ReactNode } from 'react';
import type { Breadcrumb } from '../interface';
import type { Breadcrumb, Collection } from '../interface';
TopBarProgress.config({
barColors: {
@ -25,6 +26,7 @@ interface MainViewProps {
noMargin?: boolean;
noScroll?: boolean;
children: ReactNode;
collection?: Collection;
}
const MainView = ({
@ -35,6 +37,7 @@ const MainView = ({
noMargin = false,
noScroll = false,
navbarActions,
collection,
}: MainViewProps) => {
return (
<>
@ -48,11 +51,12 @@ const MainView = ({
<div
id="main-view"
className={classNames(
showLeftNav ? 'w-main left-64' : 'w-full',
showLeftNav ? ' w-full left-0 md:w-main' : 'w-full',
!noMargin && 'px-5 py-4',
noScroll ? 'overflow-hidden' : 'overflow-y-auto',
`
h-main
h-main-mobile
md:h-main
relative
styled-scrollbars
`,
@ -61,6 +65,7 @@ const MainView = ({
{children}
</div>
</div>
<BottomNavigation collection={collection} />
</>
);
};

View File

@ -1,9 +1,10 @@
import React from 'react';
import React, { useMemo } from 'react';
import ViewStyleControl from '../common/view-style/ViewStyleControl';
import FilterControl from './FilterControl';
import GroupControl from './GroupControl';
import MobileCollectionControls from './mobile/MobileCollectionControls';
import SortControl from './SortControl';
import ViewStyleControl from '../common/view-style/ViewStyleControl';
import type { ViewStyle } from '@staticcms/core/constants/views';
import type {
@ -12,7 +13,6 @@ import type {
SortableField,
SortDirection,
SortMap,
TranslatedProps,
ViewFilter,
ViewGroup,
} from '@staticcms/core/interface';
@ -41,34 +41,69 @@ const CollectionControls = ({
viewGroups,
onFilterClick,
onGroupClick,
t,
filter,
group,
}: TranslatedProps<CollectionControlsProps>) => {
}: CollectionControlsProps) => {
const showGroupControl = useMemo(
() => Boolean(viewGroups && onGroupClick && group && viewGroups.length > 0),
[group, onGroupClick, viewGroups],
);
const showFilterControl = useMemo(
() => Boolean(viewFilters && onFilterClick && filter && viewFilters.length > 0),
[filter, onFilterClick, viewFilters],
);
const showSortControl = useMemo(
() => Boolean(sortableFields && onSortClick && sort && sortableFields.length > 0),
[onSortClick, sort, sortableFields],
);
return (
<div className="flex gap-2 items-center relative z-20">
<ViewStyleControl viewStyle={viewStyle} onChangeViewStyle={onChangeViewStyle} />
{viewGroups && onGroupClick && group
? viewGroups.length > 0 && (
<GroupControl viewGroups={viewGroups} onGroupClick={onGroupClick} t={t} group={group} />
)
: null}
{viewFilters && onFilterClick && filter
? viewFilters.length > 0 && (
<FilterControl
viewFilters={viewFilters}
onFilterClick={onFilterClick}
t={t}
filter={filter}
/>
)
: null}
{sortableFields && onSortClick && sort
? sortableFields.length > 0 && (
<SortControl fields={sortableFields} sort={sort} onSortClick={onSortClick} />
)
: null}
</div>
<>
<div
className="
flex
items-center
relative
z-20
w-full
justify-end
gap-1.5
sm:w-auto
sm:justify-normal
lg:gap-2
flex-[1_0_0%]
"
>
<ViewStyleControl viewStyle={viewStyle} onChangeViewStyle={onChangeViewStyle} />
{showGroupControl || showFilterControl || showFilterControl ? (
<MobileCollectionControls
showFilterControl={showFilterControl}
viewFilters={viewFilters}
onFilterClick={onFilterClick}
filter={filter}
showGroupControl={showGroupControl}
viewGroups={viewGroups}
onGroupClick={onGroupClick}
group={group}
showSortControl={showSortControl}
fields={sortableFields}
sort={sort}
onSortClick={onSortClick}
/>
) : null}
{showGroupControl ? (
<GroupControl viewGroups={viewGroups} onGroupClick={onGroupClick} group={group} />
) : null}
{showFilterControl ? (
<FilterControl viewFilters={viewFilters} onFilterClick={onFilterClick} filter={filter} />
) : null}
{showSortControl ? (
<SortControl fields={sortableFields} sort={sort} onSortClick={onSortClick} />
) : null}
</div>
</>
);
};

View File

@ -11,26 +11,25 @@ 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 type { Collection, Entry, TranslatedProps } from '@staticcms/core/interface';
import type { FC } from 'react';
interface CollectionHeaderProps {
collection: Collection;
newEntryUrl?: string;
}
const CollectionHeader = ({
collection,
newEntryUrl,
t,
}: TranslatedProps<CollectionHeaderProps>) => {
const CollectionHeader: FC<TranslatedProps<CollectionHeaderProps>> = ({ collection, t }) => {
const collectionLabel = collection.label;
const collectionLabelSingular = collection.label_singular;
const icon = useIcon(collection.icon);
const params = useParams();
const filterTerm = useMemo(() => params['*'], [params]);
const newEntryUrl = useNewEntryUrl(collection, filterTerm);
const icon = useIcon(collection.icon);
const entries = useEntries(collection);
const pluralLabel = useMemo(() => {
@ -65,7 +64,18 @@ const CollectionHeader = ({
return (
<>
<div className="flex flex-grow gap-4">
<div
className="
flex
flex-grow
gap-4
justify-normal
xs:justify-between
sm:justify-normal
w-full
truncate
"
>
<h2
className="
text-xl
@ -75,13 +85,25 @@ const CollectionHeader = ({
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>
{pluralLabel}
<div
className="
w-collection-header
flex-grow
truncate
"
>
{pluralLabel}
</div>
</h2>
{newEntryUrl ? (
<Button to={newEntryUrl}>
<Button to={newEntryUrl} className="hidden md:flex">
{t('collection.collectionTop.newButton', {
collectionLabel: collectionLabelSingular || pluralLabel,
})}
@ -92,4 +114,4 @@ const CollectionHeader = ({
);
};
export default translate()(CollectionHeader);
export default translate()(CollectionHeader) as FC<CollectionHeaderProps>;

View File

@ -42,7 +42,14 @@ const SingleCollectionPage: FC<SingleCollectionPageProps> = ({
const breadcrumbs = useBreadcrumbs(collection, filterTerm);
return (
<MainView breadcrumbs={breadcrumbs} showQuickCreate showLeftNav noScroll noMargin>
<MainView
breadcrumbs={breadcrumbs}
collection={collection}
showQuickCreate
showLeftNav
noScroll
noMargin
>
<CollectionView
name={name}
searchTerm={searchTerm}

View File

@ -136,6 +136,10 @@ const CollectionSearch = ({
[submitSearch],
);
const handleClick = useCallback((event: MouseEvent) => {
event.stopPropagation();
}, []);
return (
<div>
<div className="relative">
@ -171,6 +175,7 @@ const CollectionSearch = ({
onFocus={handleFocus}
value={query}
onChange={handleQueryChange}
onClick={handleClick}
/>
</div>
<PopperUnstyled
@ -191,7 +196,7 @@ const CollectionSearch = ({
ring-opacity-5
focus:outline-none
sm:text-sm
z-40
z-[1300]
dark:bg-slate-700
dark:shadow-lg
"

View File

@ -9,13 +9,11 @@ import {
sortByField as sortByFieldAction,
} from '@staticcms/core/actions/entries';
import { SORT_DIRECTION_ASCENDING } from '@staticcms/core/constants';
import { getNewEntryUrl } from '@staticcms/core/lib/urlHelper';
import {
selectSortableFields,
selectViewFilters,
selectViewGroups,
} from '@staticcms/core/lib/util/collection.util';
import { isNotEmpty } from '@staticcms/core/lib/util/string.util';
import {
selectEntriesFilter,
selectEntriesGroup,
@ -42,7 +40,6 @@ import type { ConnectedProps } from 'react-redux';
const CollectionView = ({
collection,
collections,
collectionName,
isSearchResults,
isSingleSearchResult,
searchTerm,
@ -67,16 +64,6 @@ const CollectionView = ({
setPrevCollection(collection);
}, [collection]);
const newEntryUrl = useMemo(() => {
if (!collectionName || !collection) {
return undefined;
}
return 'fields' in collection && collection.create
? `${getNewEntryUrl(collectionName)}${isNotEmpty(filterTerm) ? `/${filterTerm}` : ''}`
: '';
}, [collection, collectionName, filterTerm]);
const searchResultKey = useMemo(
() => `collection.collectionTop.searchResults${isSingleSearchResult ? 'InCollection' : ''}`,
[isSingleSearchResult],
@ -196,18 +183,18 @@ const CollectionView = ({
const collectionDescription = collection?.description;
return (
<div className="flex flex-col h-full px-5 pt-4">
<div className="flex items-center mb-4">
<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">
{isSearchResults ? (
<>
<div className="flex-grow">
<div>{t(searchResultKey, { searchTerm, collection: collection?.label })}</div>
</div>
<CollectionControls viewStyle={viewStyle} onChangeViewStyle={changeViewStyle} t={t} />
<CollectionControls viewStyle={viewStyle} onChangeViewStyle={changeViewStyle} />
</>
) : (
<>
<CollectionHeader collection={collection} newEntryUrl={newEntryUrl} />
{collection ? <CollectionHeader collection={collection} /> : null}
<CollectionControls
viewStyle={viewStyle}
onChangeViewStyle={changeViewStyle}
@ -216,7 +203,6 @@ const CollectionView = ({
sort={sort}
viewFilters={viewFilters ?? []}
viewGroups={viewGroups ?? []}
t={t}
onFilterClick={onFilterClick}
onGroupClick={onGroupClick}
filter={filter}
@ -270,7 +256,6 @@ function mapStateToProps(state: RootState, ownProps: TranslatedProps<CollectionV
filterTerm,
collection,
collections,
collectionName: name,
sort,
sortableFields,
viewFilters,

View File

@ -6,19 +6,21 @@ import MenuGroup from '../common/menu/MenuGroup';
import MenuItemButton from '../common/menu/MenuItemButton';
import type { FilterMap, TranslatedProps, ViewFilter } from '@staticcms/core/interface';
import type { MouseEvent } from 'react';
import type { FC, MouseEvent } from 'react';
interface FilterControlProps {
filter: Record<string, FilterMap>;
viewFilters: ViewFilter[];
onFilterClick: (viewFilter: ViewFilter) => void;
export interface FilterControlProps {
filter: Record<string, FilterMap> | undefined;
viewFilters: ViewFilter[] | undefined;
variant?: 'menu' | 'list';
onFilterClick: ((viewFilter: ViewFilter) => void) | undefined;
}
const FilterControl = ({
viewFilters,
t,
filter = {},
viewFilters = [],
variant = 'menu',
onFilterClick,
filter,
t,
}: TranslatedProps<FilterControlProps>) => {
const anyActive = useMemo(() => Object.keys(filter).some(key => filter[key]?.active), [filter]);
@ -26,15 +28,65 @@ const FilterControl = ({
(viewFilter: ViewFilter) => (event: MouseEvent) => {
event.stopPropagation();
event.preventDefault();
onFilterClick(viewFilter);
onFilterClick?.(viewFilter);
},
[onFilterClick],
);
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>
{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
"
onClick={handleFilterClick(viewFilter)}
>
<input
key={`${labelId}-${checked}`}
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>
</div>
);
})}
</div>
);
}
return (
<Menu
key="filter-by-menu"
label={t('collection.collectionTop.filterBy')}
variant={anyActive ? 'contained' : 'outlined'}
rootClassName="hidden lg:block"
>
<MenuGroup>
{viewFilters.map(viewFilter => {
@ -62,4 +114,4 @@ const FilterControl = ({
);
};
export default translate()(FilterControl);
export default translate()(FilterControl) as FC<FilterControlProps>;

View File

@ -1,5 +1,5 @@
import { Check as CheckIcon } from '@styled-icons/material/Check';
import React, { useMemo } from 'react';
import React, { useCallback, useMemo } from 'react';
import { translate } from 'react-polyglot';
import Menu from '../common/menu/Menu';
@ -7,31 +7,87 @@ import MenuGroup from '../common/menu/MenuGroup';
import MenuItemButton from '../common/menu/MenuItemButton';
import type { GroupMap, TranslatedProps, ViewGroup } from '@staticcms/core/interface';
import type { FC, MouseEvent } from 'react';
interface GroupControlProps {
group: Record<string, GroupMap>;
viewGroups: ViewGroup[];
onGroupClick: (viewGroup: ViewGroup) => void;
export interface GroupControlProps {
group: Record<string, GroupMap> | undefined;
viewGroups: ViewGroup[] | undefined;
variant?: 'menu' | 'list';
onGroupClick: ((viewGroup: ViewGroup) => void) | undefined;
}
const GroupControl = ({
viewGroups,
group,
t,
viewGroups = [],
group = {},
variant = 'menu',
onGroupClick,
t,
}: TranslatedProps<GroupControlProps>) => {
const activeGroup = useMemo(() => Object.values(group).find(f => f.active === true), [group]);
const handleGroupClick = useCallback(
(viewGroup: ViewGroup) => (event: MouseEvent) => {
event.stopPropagation();
event.preventDefault();
onGroupClick?.(viewGroup);
},
[onGroupClick],
);
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>
{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
"
onClick={handleGroupClick(viewGroup)}
>
<label className="ml-2 text-md font-medium text-gray-800 dark:text-gray-300">
{viewGroup.label}
</label>
{active ? (
<CheckIcon key="checkmark" className="ml-2 w-6 h-6 text-blue-500" />
) : (
<div key="not-checked" className="ml-2 w-6 h-6" />
)}
</div>
);
})}
</div>
);
}
return (
<Menu
label={t('collection.collectionTop.groupBy')}
variant={activeGroup ? 'contained' : 'outlined'}
rootClassName="hidden lg:block"
>
<MenuGroup>
{viewGroups.map(viewGroup => (
<MenuItemButton
key={viewGroup.id}
onClick={() => onGroupClick(viewGroup)}
onClick={() => onGroupClick?.(viewGroup)}
endIcon={viewGroup.id === activeGroup?.id ? CheckIcon : undefined}
>
{viewGroup.label}
@ -42,4 +98,4 @@ const GroupControl = ({
);
};
export default translate()(GroupControl);
export default translate()(GroupControl) as FC<GroupControlProps>;

View File

@ -11,6 +11,7 @@ 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';
function getNodeTitle(node: TreeNodeData) {
const title = node.isRoot
@ -29,6 +30,20 @@ interface TreeNodeProps {
const TreeNode = ({ collection, treeData, depth = 0, onToggle }: TreeNodeProps) => {
const collectionName = collection.name;
const handleClick = useCallback(
(event: MouseEvent | undefined, node: TreeNodeData, expanded: boolean) => {
event?.stopPropagation();
event?.preventDefault();
if (event) {
onToggle({ node, expanded });
} else {
onToggle({ node, expanded: true });
}
},
[onToggle],
);
const sortedData = sortBy(treeData, getNodeTitle);
return (
<>
@ -50,7 +65,7 @@ const TreeNode = ({ collection, treeData, depth = 0, onToggle }: TreeNodeProps)
<div className={classNames(depth !== 0 && 'ml-8')}>
<NavLink
to={to}
onClick={() => onToggle({ node, expanded: !node.expanded })}
onClick={() => handleClick(undefined, node, !node.expanded)}
data-testid={node.path}
icon={<ArticleIcon className={classNames(depth === 0 ? 'h-6 w-6' : 'h-5 w-5')} />}
>
@ -58,6 +73,7 @@ const TreeNode = ({ collection, treeData, depth = 0, onToggle }: TreeNodeProps)
<div>{title}</div>
{hasChildren && (
<ChevronRightIcon
onClick={event => handleClick(event, node, !node.expanded)}
className={classNames(
node.expanded && 'rotate-90 transform',
`
@ -135,7 +151,6 @@ const NestedCollection = ({ collection, filterTerm }: NestedCollectionProps) =>
const entries = useEntries(collection);
const [treeData, setTreeData] = useState<TreeNodeData[]>(getTreeData(collection, entries));
const [selected, setSelected] = useState<TreeNodeData | null>(null);
const [useFilter, setUseFilter] = useState(true);
const [prevCollection, setPrevCollection] = useState<Collection | null>(null);
@ -186,22 +201,15 @@ const NestedCollection = ({ collection, filterTerm }: NestedCollectionProps) =>
const onToggle = useCallback(
({ node, expanded }: { node: TreeNodeData; expanded: boolean }) => {
if (!selected || selected.path === node.path || expanded) {
setTreeData(
updateNode(treeData, node, node => ({
...node,
expanded,
})),
);
setSelected(node);
setUseFilter(false);
} else {
// don't collapse non selected nodes when clicked
setSelected(node);
setUseFilter(false);
}
setTreeData(
updateNode(treeData, node, node => ({
...node,
expanded,
})),
);
setUseFilter(false);
},
[selected, treeData],
[treeData],
);
return <TreeNode collection={collection} treeData={treeData} onToggle={onToggle} />;

View File

@ -1,6 +1,6 @@
import { KeyboardArrowDown as KeyboardArrowDownIcon } from '@styled-icons/material/KeyboardArrowDown';
import { KeyboardArrowUp as KeyboardArrowUpIcon } from '@styled-icons/material/KeyboardArrowUp';
import React, { useMemo } from 'react';
import React, { useCallback, useMemo } from 'react';
import { translate } from 'react-polyglot';
import {
@ -18,6 +18,7 @@ import type {
SortMap,
TranslatedProps,
} from '@staticcms/core/interface';
import type { FC, MouseEvent } from 'react';
function nextSortDirection(direction: SortDirection) {
switch (direction) {
@ -30,13 +31,20 @@ function nextSortDirection(direction: SortDirection) {
}
}
interface SortControlProps {
fields: SortableField[];
onSortClick: (key: string, direction?: SortDirection) => Promise<void>;
export interface SortControlProps {
fields: SortableField[] | undefined;
sort: SortMap | undefined;
variant?: 'menu' | 'list';
onSortClick: ((key: string, direction?: SortDirection) => Promise<void>) | undefined;
}
const SortControl = ({ t, fields, onSortClick, sort }: TranslatedProps<SortControlProps>) => {
const SortControl = ({
fields = [],
sort = {},
variant = 'menu',
onSortClick,
t,
}: TranslatedProps<SortControlProps>) => {
const selectedSort = useMemo(() => {
if (!sort) {
return { key: undefined, direction: undefined };
@ -50,10 +58,68 @@ const SortControl = ({ t, fields, onSortClick, sort }: TranslatedProps<SortContr
return sortValues[0];
}, [sort]);
const handleSortClick = useCallback(
(key: string, direction?: SortDirection) => (event: MouseEvent) => {
event.stopPropagation();
event.preventDefault();
onSortClick?.(key, direction);
},
[onSortClick],
);
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>
{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
"
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>
{field.name === selectedSort.key ? (
selectedSort.direction === SORT_DIRECTION_ASCENDING ? (
<KeyboardArrowUpIcon key="checkmark" className="ml-2 w-6 h-6 text-blue-500" />
) : (
<KeyboardArrowDownIcon key="checkmark" className="ml-2 w-6 h-6 text-blue-500" />
)
) : (
<div key="not-checked" className="ml-2 w-6 h-6" />
)}
</div>
);
})}
</div>
);
}
return (
<Menu
label={t('collection.collectionTop.sortBy')}
variant={selectedSort.key ? 'contained' : 'outlined'}
rootClassName="hidden lg:block"
>
<MenuGroup>
{fields.map(field => {
@ -62,7 +128,7 @@ const SortControl = ({ t, fields, onSortClick, sort }: TranslatedProps<SortContr
return (
<MenuItemButton
key={field.name}
onClick={() => onSortClick(field.name, nextSortDir)}
onClick={handleSortClick(field.name, nextSortDir)}
active={field.name === selectedSort.key}
endIcon={
field.name === selectedSort.key
@ -81,4 +147,4 @@ const SortControl = ({ t, fields, onSortClick, sort }: TranslatedProps<SortContr
);
};
export default translate()(SortControl);
export default translate()(SortControl) as FC<SortControlProps>;

View File

@ -81,7 +81,20 @@ const Entries = ({
);
}
return <div>{t('collection.entries.noEntries')}</div>;
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>
);
};
export default translate()(Entries);

View File

@ -6,10 +6,10 @@ 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 type { ViewStyle } from '@staticcms/core/constants/views';
@ -104,39 +104,64 @@ const EntriesCollection = ({
[collection, dispatch],
);
const [selectedGroup, setSelectedGroup] = useState(0);
const handleGroupClick = useCallback(
(index: number) => () => {
setSelectedGroup(index);
},
[],
);
if (groups && groups.length > 0) {
return (
<>
{groups.map((group, index) => {
const title = getGroupTitle(group, t);
return (
<div key={group.id} id={group.id}>
<h2
className={classNames(
`
px-2
pt-4
pb-2
`,
index === 0 && 'pt-0',
)}
>
{title}
</h2>
<Entries
collection={collection}
entries={getGroupEntries(filteredEntries, group.paths)}
isFetching={isFetching}
collectionName={collection.label}
viewStyle={viewStyle}
cursor={cursor}
handleCursorActions={handleCursorActions}
page={page}
filterTerm={filterTerm}
/>
<div
className="
pb-3
"
>
<div
className="
-m-1
"
>
<div
className="
flex
gap-2
p-1
overflow-x-auto
hide-scrollbar
"
>
{groups.map((group, index) => {
const title = getGroupTitle(group, t);
return (
<Button
key={index}
variant={index === selectedGroup ? 'contained' : 'text'}
onClick={handleGroupClick(index)}
className="whitespace-nowrap"
>
{title}
</Button>
);
})}
</div>
);
})}
</div>
</div>
<Entries
key={`entries-with-group-${groups[selectedGroup].id}`}
collection={collection}
entries={getGroupEntries(filteredEntries, groups[selectedGroup].paths)}
isFetching={isFetching}
collectionName={collection.label}
viewStyle={viewStyle}
cursor={cursor}
handleCursorActions={handleCursorActions}
page={page}
filterTerm={filterTerm}
/>
</>
);
}

View File

@ -113,52 +113,64 @@ const EntryCard: FC<TranslatedProps<EntryCardProps>> = ({
if (PreviewCardComponent) {
return (
<Card>
<CardActionArea to={path}>
<PreviewCardComponent
collection={collection}
fields={fields}
entry={entry}
widgetFor={widgetFor}
widgetsFor={widgetsFor}
theme={theme}
hasLocalBackup={hasLocalBackup}
/>
</CardActionArea>
</Card>
<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>
<CardActionArea to={path}>
<PreviewCardComponent
collection={collection}
fields={fields}
entry={entry}
widgetFor={widgetFor}
widgetsFor={widgetsFor}
theme={theme}
hasLocalBackup={hasLocalBackup}
/>
</CardActionArea>
</Card>
</div>
</div>
</div>
);
}
return (
<Card className="h-full" title={summary}>
<CardActionArea to={path}>
{image && imageField ? (
<CardMedia
height="140"
image={image}
collection={collection}
field={imageField}
entry={entry}
/>
) : null}
<CardContent>
<div className="flex w-full items-center justify-between">
<div className="whitespace-nowrap overflow-hidden text-ellipsis">{summary}</div>
{hasLocalBackup ? (
<InfoIcon
className="
w-5
h-5
text-blue-600
dark:text-blue-300
"
title={t('ui.localBackup.hasLocalBackup')}
/>
) : null}
</div>
</CardContent>
</CardActionArea>
</Card>
<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}>
<CardActionArea to={path}>
{image && imageField ? (
<CardMedia
height="140"
image={image}
collection={collection}
field={imageField}
entry={entry}
/>
) : null}
<CardContent>
<div className="flex w-full items-center justify-between">
<div className="truncate">{summary}</div>
{hasLocalBackup ? (
<InfoIcon
className="
w-5
h-5
text-blue-600
dark:text-blue-300
"
title={t('ui.localBackup.hasLocalBackup')}
/>
) : null}
</div>
</CardContent>
</CardActionArea>
</Card>
</div>
</div>
</div>
);
};

View File

@ -47,7 +47,7 @@ const CardWrapper = ({
? style.left ?? COLLECTION_CARD_MARGIN * columnIndex
: style.left
}`,
),
) + 4,
[columnIndex, style.left],
);
@ -138,12 +138,22 @@ const EntryListingCardGrid: FC<EntryListingCardGridProps> = ({
}, [cardHeights, prevCardHeights.length]);
return (
<div className="relative w-full h-full">
<div
className="
relative
w-card-grid
h-full
overflow-hidden
-ml-1
"
>
<AutoSizer onResize={handleResize}>
{({ height = 0, width = 0 }) => {
const calculatedWidth = width - 4;
const columnWidthWithGutter = COLLECTION_CARD_WIDTH + COLLECTION_CARD_MARGIN;
const columnCount = Math.floor(width / columnWidthWithGutter);
const nonGutterSpace = (width - COLLECTION_CARD_MARGIN * columnCount) / width;
const columnCount = Math.max(Math.floor(calculatedWidth / columnWidthWithGutter), 1);
const nonGutterSpace =
(calculatedWidth - COLLECTION_CARD_MARGIN * columnCount) / calculatedWidth;
const columnWidth = (1 / columnCount) * nonGutterSpace;
const rowCount = Math.ceil(entryData.length / columnCount);
@ -157,7 +167,7 @@ const EntryListingCardGrid: FC<EntryListingCardGridProps> = ({
`,
)}
style={{
width,
width: calculatedWidth,
height,
}}
>
@ -165,8 +175,8 @@ const EntryListingCardGrid: FC<EntryListingCardGridProps> = ({
columnCount={columnCount}
columnWidth={index =>
index + 1 === columnCount
? width * columnWidth
: width * columnWidth + COLLECTION_CARD_MARGIN
? calculatedWidth * columnWidth
: calculatedWidth * columnWidth + COLLECTION_CARD_MARGIN
}
rowCount={rowCount}
rowHeight={index => {
@ -189,7 +199,7 @@ const EntryListingCardGrid: FC<EntryListingCardGridProps> = ({
return rowHeight;
}}
width={width}
width={calculatedWidth}
height={height}
itemData={
{
@ -203,7 +213,7 @@ const EntryListingCardGrid: FC<EntryListingCardGridProps> = ({
onScroll={onScroll}
className={classNames(
`
overflow-hidden
!overflow-x-hidden
overflow-y-auto
styled-scrollbars
`,

View File

@ -57,8 +57,8 @@ const EntryListingGrid: FC<EntryListingGridProps> = ({
}, [handleScroll]);
return (
<div className="relative h-full overflow-hidden">
<div ref={gridContainerRef} className="relative h-full overflow-hidden">
<div className="relative h-full flex-grow">
<div ref={gridContainerRef} className="relative h-full">
<EntryListingCardGrid
key="grid"
entryData={entryData}
@ -71,14 +71,14 @@ const EntryListingGrid: FC<EntryListingGridProps> = ({
<div
key="loading"
className="
absolute
inset-0
flex
items-center
justify-center
bg-slate-50/50
dark:bg-slate-900/50
"
absolute
inset-0
flex
items-center
justify-center
bg-slate-50/50
dark:bg-slate-900/50
"
>
{t('collection.entries.loadingEntries')}
</div>

View File

@ -80,6 +80,7 @@ const EntryRow: FC<TranslatedProps<EntryRowProps>> = ({
hover:bg-gray-200
dark:hover:bg-slate-700/70
"
to={path}
>
{collectionLabel ? (
<TableCell key="collectionLabel" to={path}>

View File

@ -0,0 +1,40 @@
import { FilterList as FilterListIcon } from '@styled-icons/material/FilterList';
import React, { useCallback, useState } from 'react';
import IconButton from '../../common/button/IconButton';
import MobileCollectionControlsDrawer from './MobileCollectionControlsDrawer';
import type { FC } from 'react';
import type { FilterControlProps } from '../FilterControl';
import type { GroupControlProps } from '../GroupControl';
import type { SortControlProps } from '../SortControl';
export type MobileCollectionControlsProps = Omit<FilterControlProps, 'variant'> &
Omit<GroupControlProps, 'variant'> &
Omit<SortControlProps, 'variant'> & {
showGroupControl: boolean;
showFilterControl: boolean;
showSortControl: boolean;
};
const MobileCollectionControls: FC<MobileCollectionControlsProps> = props => {
const [mobileOpen, setMobileOpen] = useState(false);
const toggleMobileMenu = useCallback(() => {
setMobileOpen(old => !old);
}, []);
return (
<>
<IconButton className="flex lg:hidden" variant="text" onClick={toggleMobileMenu}>
<FilterListIcon className="w-5 h-5" />
</IconButton>
<MobileCollectionControlsDrawer
{...props}
mobileOpen={mobileOpen}
onMobileOpenToggle={toggleMobileMenu}
/>
</>
);
};
export default MobileCollectionControls;

View File

@ -0,0 +1,117 @@
import SwipeableDrawer from '@mui/material/SwipeableDrawer';
import React, { useMemo } from 'react';
import FilterControl from '../FilterControl';
import GroupControl from '../GroupControl';
import SortControl from '../SortControl';
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'> & {
mobileOpen: boolean;
onMobileOpenToggle: () => void;
} & {
showGroupControl: boolean;
showFilterControl: boolean;
showSortControl: boolean;
};
const MobileCollectionControlsDrawer = ({
mobileOpen,
onMobileOpenToggle,
showFilterControl,
filter,
viewFilters,
onFilterClick,
showGroupControl,
group,
viewGroups,
onGroupClick,
showSortControl,
sort,
fields,
onSortClick,
}: MobileCollectionControlsDrawerProps) => {
const iOS = useMemo(
() => typeof navigator !== 'undefined' && /iPad|iPhone|iPod/.test(navigator.userAgent),
[],
);
const container = useMemo(
() => (typeof window !== 'undefined' ? window.document.body : undefined),
[],
);
return (
<SwipeableDrawer
disableBackdropTransition={!iOS}
disableDiscovery={iOS}
container={container}
variant="temporary"
open={mobileOpen}
onOpen={onMobileOpenToggle}
onClose={onMobileOpenToggle}
anchor="right"
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,
},
}}
>
<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
"
>
{showSortControl ? (
<SortControl fields={fields} sort={sort} onSortClick={onSortClick} variant="list" />
) : null}
{showFilterControl ? (
<FilterControl
viewFilters={viewFilters}
onFilterClick={onFilterClick}
filter={filter}
variant="list"
/>
) : null}
{showGroupControl ? (
<GroupControl
viewGroups={viewGroups}
onGroupClick={onGroupClick}
group={group}
variant="list"
/>
) : null}
</div>
</SwipeableDrawer>
);
};
export default MobileCollectionControlsDrawer;

View File

@ -63,7 +63,8 @@ const Autocomplete = function <T>(
<div
className="
flex
items-center
flex-col
flex-start
text-sm
font-medium
relative
@ -90,8 +91,8 @@ const Autocomplete = function <T>(
leading-5
focus:ring-0
outline-none
basis-60
flex-grow
truncate
`,
disabled
? `
@ -194,7 +195,9 @@ const Autocomplete = function <T>(
>
<span
className={classNames(
'block truncate',
`
block
`,
selected ? 'font-medium' : 'font-normal',
)}
>

View File

@ -4,13 +4,13 @@ import Button from './Button';
import classNames from '@staticcms/core/lib/util/classNames.util';
import type { FC } from 'react';
import type { ButtonProps } from './Button';
import type { ButtonLinkProps } from './Button';
export type IconButtonProps = Omit<ButtonProps, 'children'> & {
export type IconButtonProps = Omit<ButtonLinkProps, 'children'> & {
children: FC<{ className?: string }>;
};
const IconButton = ({ children, size = 'medium', className, ...otherProps }: ButtonProps) => {
const IconButton = ({ children, size = 'medium', className, ...otherProps }: ButtonLinkProps) => {
return (
<Button
className={classNames(size === 'small' && 'px-0.5', size === 'medium' && 'px-1.5', className)}

View File

@ -22,6 +22,10 @@ const CardActionArea = ({ to, children }: CardActionAreaProps) => {
justify-start
hover:bg-gray-200
dark:hover:bg-slate-700/70
focus:outline-none
focus:ring-4
focus:ring-gray-200
dark:focus:ring-slate-700
"
>
{children}

View File

@ -52,7 +52,7 @@ const Checkbox: FC<CheckboxProps> = ({ checked, disabled = false, onChange }) =>
ref={inputRef}
type="checkbox"
checked={checked}
className="sr-only peer"
className="sr-only peer hide-tap"
disabled={disabled}
onChange={onChange}
onClick={handleNoop}

View File

@ -23,6 +23,8 @@ export interface FieldProps {
disabled: boolean;
disableClick?: boolean;
endAdornment?: ReactNode;
rootClassName?: string;
wrapperClassName?: string;
}
const Field: FC<FieldProps> = ({
@ -39,6 +41,8 @@ const Field: FC<FieldProps> = ({
disabled,
disableClick = false,
endAdornment,
rootClassName,
wrapperClassName,
}) => {
const finalCursor = useCursor(cursor, disabled);
@ -104,6 +108,7 @@ const Field: FC<FieldProps> = ({
focus-within:border-blue-800
dark:focus-within:border-blue-100
`,
rootClassName,
!noHightlight &&
!disabled &&
`
@ -118,7 +123,7 @@ const Field: FC<FieldProps> = ({
finalCursor === 'default' && 'cursor-default',
!hasErrors && 'group/active',
),
[finalCursor, disabled, hasErrors, noHightlight, noPadding],
[rootClassName, noHightlight, disabled, noPadding, finalCursor, hasErrors],
);
const wrapperClassNames = useMemo(
@ -129,9 +134,10 @@ const Field: FC<FieldProps> = ({
flex-col
w-full
`,
wrapperClassName,
forSingleList && 'mr-14',
),
[forSingleList],
[forSingleList, wrapperClassName],
);
if (variant === 'inline') {

View File

@ -16,9 +16,14 @@ export interface MenuProps {
color?: BaseBaseProps['color'];
size?: BaseBaseProps['size'];
rounded?: boolean | 'no-padding';
className?: string;
rootClassName?: string;
iconClassName?: string;
buttonClassName?: string;
labelClassName?: string;
children: ReactNode | ReactNode[];
hideDropdownIcon?: boolean;
hideDropdownIconOnMobile?: boolean;
hideLabel?: boolean;
keepMounted?: boolean;
disabled?: boolean;
'data-testid'?: string;
@ -31,9 +36,14 @@ const Menu = ({
color = 'primary',
size = 'medium',
rounded = false,
className,
rootClassName,
iconClassName,
buttonClassName,
labelClassName,
children,
hideDropdownIcon = false,
hideDropdownIconOnMobile = false,
hideLabel = false,
keepMounted = false,
disabled = false,
'data-testid': dataTestId,
@ -56,16 +66,16 @@ const Menu = ({
setAnchorEl(null);
}, []);
const buttonClassName = useButtonClassNames(variant, color, size, rounded);
const calculatedButtonClassName = useButtonClassNames(variant, color, size, rounded);
const menuButtonClassNames = useMemo(
() => classNames(className, buttonClassName),
[buttonClassName, className],
() => classNames(calculatedButtonClassName, buttonClassName, 'whitespace-nowrap'),
[calculatedButtonClassName, buttonClassName],
);
return (
<ClickAwayListener mouseEvent="onMouseDown" touchEvent="onTouchStart" onClickAway={handleClose}>
<div className="flex">
<div className={classNames('flex', rootClassName)}>
<button
type="button"
onClick={handleButtonClick}
@ -76,10 +86,26 @@ const Menu = ({
className={menuButtonClassNames}
disabled={disabled}
>
{StartIcon ? <StartIcon className="-ml-0.5 mr-1.5 h-5 w-5" /> : null}
{label}
{StartIcon ? (
<StartIcon
className={classNames(
`-ml-0.5 h-5 w-5`,
!hideLabel && !hideDropdownIcon && 'mr-1.5',
hideDropdownIconOnMobile && '!mr-0 md:!mr-1.5',
iconClassName,
)}
/>
) : null}
{!hideLabel ? <div className={labelClassName}>{label}</div> : null}
{!hideDropdownIcon ? (
<KeyboardArrowDownIcon className="-mr-0.5 ml-2 h-5 w-5" aria-hidden="true" />
<KeyboardArrowDownIcon
className={classNames(
`-mr-0.5 h-5 w-5`,
!hideLabel && 'ml-2',
hideDropdownIconOnMobile && '!hidden md:!block',
)}
aria-hidden="true"
/>
) : null}
</button>
<MenuUnstyled

View File

@ -61,6 +61,7 @@ const Pill: FC<PillProps> = ({
px-3
py-1
rounded-lg
truncate
`,
noWrap && 'whitespace-nowrap',
colorClassNames,

View File

@ -59,7 +59,6 @@ const Select = forwardRef(
},
[onOpenChange],
);
const handleButtonClick = useCallback(() => handleOpenChange(!open), [handleOpenChange, open]);
const handleChange = useCallback(
(_event: MouseEvent | KeyboardEvent | FocusEvent | null, selectedValue: number | string) => {
@ -89,8 +88,10 @@ const Select = forwardRef(
<SelectUnstyled<any>
renderValue={() => {
return (
<>
{label ?? placeholder}
<div className="w-full">
<div className="flex flex-start w-select-widget-label">
<span className="truncate">{label ?? placeholder}</span>
</div>
<span
className="
pointer-events-none
@ -118,13 +119,12 @@ const Select = forwardRef(
aria-hidden="true"
/>
</span>
</>
</div>
);
}}
slotProps={{
root: {
ref,
onClick: handleButtonClick,
className: classNames(
`
flex
@ -160,11 +160,11 @@ const Select = forwardRef(
ring-opacity-5
focus:outline-none
sm:text-sm
z-50
z-[100]
dark:bg-slate-700
dark:shadow-lg
`,
style: { width },
style: { width: ref ? width : 'auto' },
disablePortal: false,
},
}}

View File

@ -26,6 +26,7 @@ const TableCell = ({ children, emphasis = false, to, shrink = false }: TableCell
py-3
whitespace-nowrap
"
tabIndex={-1}
>
{children}
</Link>
@ -50,6 +51,8 @@ const TableCell = ({ children, emphasis = false, to, shrink = false }: TableCell
<div
className="
h-[44px]
truncate
w-full
"
>
{content}

View File

@ -35,6 +35,8 @@ const TableHeaderCell = ({ children }: TableHeaderCellProps) => {
dark:border-gray-700
dark:bg-slate-800
text-[14px]
truncate
w-full
"
>
{typeof children === 'string' && isEmpty(children) ? <>&nbsp;</> : children}

View File

@ -1,15 +1,31 @@
import React from 'react';
import React, { useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import classNames from '@staticcms/core/lib/util/classNames.util';
import type { ReactNode } from 'react';
import type { KeyboardEvent, ReactNode } from 'react';
interface TableRowProps {
children: ReactNode;
className?: string;
to?: string;
}
const TableRow = ({ children, className }: TableRowProps) => {
const TableRow = ({ children, className, to }: TableRowProps) => {
const navigate = useNavigate();
const handleKeyDown = useCallback(
(event: KeyboardEvent) => {
if (!to) {
return;
}
if (event.key === 'Enter' || event.key === 'Space') {
navigate(to);
}
},
[navigate, to],
);
return (
<tr
className={classNames(
@ -22,9 +38,14 @@ const TableRow = ({ children, className }: TableRowProps) => {
hover:bg-slate-50
dark:bg-slate-800
dark:hover:bg-slate-700
focus:outline-none
focus:bg-gray-100
focus:dark:bg-slate-700
`,
className,
)}
tabIndex={to ? 0 : -1}
onKeyDown={handleKeyDown}
>
{children}
</tr>

View File

@ -19,6 +19,8 @@ export interface BaseTextFieldProps {
placeholder?: string;
endAdornment?: ReactNode;
startAdornment?: ReactNode;
rootClassName?: string;
inputClassName?: string;
}
export interface NumberTextFieldProps extends BaseTextFieldProps {
@ -49,6 +51,8 @@ const TextField: FC<TextFieldProps> = ({
onClick,
startAdornment,
endAdornment,
rootClassName,
inputClassName,
...otherProps
}) => {
const finalCursor = useCursor(cursor, disabled);
@ -67,10 +71,13 @@ const TextField: FC<TextFieldProps> = ({
endAdornment={endAdornment}
slotProps={{
root: {
className: `
flex
w-full
`,
className: classNames(
`
flex
w-full
`,
rootClassName,
),
},
input: {
ref: inputRef,
@ -79,6 +86,7 @@ const TextField: FC<TextFieldProps> = ({
w-full
text-sm
`,
inputClassName,
variant === 'borderless' &&
`
h-6

View File

@ -15,7 +15,7 @@ interface ViewStyleControlPros {
const ViewStyleControl = ({ viewStyle, onChangeViewStyle }: ViewStyleControlPros) => {
return (
<div className="flex items-center gap-1.5 mr-1">
<div className="flex items-center gap-1.5 lg:mr-1">
<IconButton
variant="text"
className={classNames(viewStyle === VIEW_STYLE_TABLE && 'text-blue-500 dark:text-blue-500')}

View File

@ -22,6 +22,7 @@ export default function useWidgetsFor(
collection: Collection,
fields: Field[],
entry: Entry,
data: EntryData = entry.data,
): {
widgetFor: WidgetFor;
widgetsFor: WidgetsFor;
@ -35,9 +36,19 @@ export default function useWidgetsFor(
if (!config) {
return null;
}
return getWidgetFor(config, collection, name, fields, entry, theme, inferredFields);
return getWidgetFor(
config,
collection,
name,
fields,
entry,
theme,
inferredFields,
fields,
data,
);
},
[collection, config, entry, fields, inferredFields, theme],
[collection, config, data, entry, fields, inferredFields, theme],
);
/**

View File

@ -3,7 +3,7 @@ import { ScrollSyncPane } from 'react-scroll-sync';
import { EDITOR_SIZE_COMPACT } from '@staticcms/core/constants/views';
import useBreadcrumbs from '@staticcms/core/lib/hooks/useBreadcrumbs';
import { getI18nInfo, getPreviewEntry, hasI18n } from '@staticcms/core/lib/i18n';
import { getI18nInfo, hasI18n } from '@staticcms/core/lib/i18n';
import classNames from '@staticcms/core/lib/util/classNames.util';
import {
getFileFromSlug,
@ -169,6 +169,11 @@ const EditorInterface = ({
const collectHasI18n = hasI18n(collection);
const [showMobilePreview, setShowMobilePreview] = useState(false);
const toggleMobilePreview = useCallback(() => {
setShowMobilePreview(old => !old);
}, []);
const editor = (
<div
key={defaultLocale}
@ -181,7 +186,13 @@ const EditorInterface = ({
`
overflow-y-auto
styled-scrollbars
h-main
h-main-mobile
md:h-main
`,
showMobilePreview &&
`
hidden
lg:block
`,
)}
>
@ -204,10 +215,12 @@ const EditorInterface = ({
<div
key={selectedLocale}
className="
flex
w-full
overflow-y-auto
styled-scrollbars
h-main
h-main-mobile
md:h-main
"
>
<EditorControlPane
@ -227,9 +240,36 @@ const EditorInterface = ({
[collection, entry, fields, fieldsErrors, handleLocaleChange, selectedLocale, submitted, t],
);
const previewEntry = collectHasI18n
? getPreviewEntry(entry, selectedLocale[0], defaultLocale)
: entry;
const mobileLocaleEditor = useMemo(
() => (
<div
key={selectedLocale}
className="
w-full
overflow-y-auto
styled-scrollbars
h-main-mobile
flex
md:hidden
"
>
<EditorControlPane
collection={collection}
entry={entry}
fields={fields}
fieldsErrors={fieldsErrors}
locale={selectedLocale}
onLocaleChange={handleLocaleChange}
allowDefaultLocale
submitted={submitted}
canChangeLocale
hideBorder
t={t}
/>
</div>
),
[collection, entry, fields, fieldsErrors, handleLocaleChange, selectedLocale, submitted, t],
);
const editorWithPreview = (
<div
@ -238,27 +278,31 @@ const EditorInterface = ({
grid
h-full
`,
editorSize === EDITOR_SIZE_COMPACT ? 'grid-cols-editor' : 'grid-cols-2',
editorSize === EDITOR_SIZE_COMPACT ? 'lg:grid-cols-editor' : 'lg:grid-cols-2',
)}
>
<ScrollSyncPane>{editor}</ScrollSyncPane>
<EditorPreviewPane
collection={collection}
previewInFrame={previewInFrame}
entry={previewEntry}
entry={entry}
fields={fields}
editorSize={editorSize}
showMobilePreview={showMobilePreview}
/>
</div>
);
const editorSideBySideLocale = (
<div className="grid grid-cols-2 h-full">
<ScrollSyncPane>{editor}</ScrollSyncPane>
<ScrollSyncPane>
<>{editorLocale}</>
</ScrollSyncPane>
</div>
<>
<div className="grid-cols-2 h-full hidden lg:grid">
<ScrollSyncPane>{editor}</ScrollSyncPane>
<ScrollSyncPane>
<>{editorLocale}</>
</ScrollSyncPane>
</div>
{mobileLocaleEditor}
</>
);
const summary = useMemo(() => selectEntryCollectionTitle(collection, entry), [collection, entry]);
@ -296,6 +340,9 @@ const EditorInterface = ({
toggleScrollSync={handleToggleScrollSync}
toggleI18n={handleToggleI18n}
slug={slug}
showMobilePreview={showMobilePreview}
onMobilePreviewToggle={toggleMobilePreview}
className="flex"
/>
}
>

View File

@ -11,6 +11,7 @@ import React, { useCallback, useMemo } from 'react';
import { translate } from 'react-polyglot';
import { deleteLocalBackup, loadEntry } from '@staticcms/core/actions/entries';
import classNames from '@staticcms/core/lib/util/classNames.util';
import { selectAllowDeletion } from '@staticcms/core/lib/util/collection.util';
import { selectIsFetching } from '@staticcms/core/reducers/selectors/globalUI';
import { useAppDispatch, useAppSelector } from '@staticcms/core/store/hooks';
@ -18,6 +19,7 @@ import confirm from '../common/confirm/Confirm';
import Menu from '../common/menu/Menu';
import MenuGroup from '../common/menu/MenuGroup';
import MenuItemButton from '../common/menu/MenuItemButton';
import IconButton from '../common/button/IconButton';
import type { Collection, EditorPersistOptions, TranslatedProps } from '@staticcms/core/interface';
import type { FC, MouseEventHandler } from 'react';
@ -44,6 +46,9 @@ export interface EditorToolbarProps {
toggleScrollSync: MouseEventHandler;
toggleI18n: MouseEventHandler;
slug?: string | undefined;
className?: string;
showMobilePreview: boolean;
onMobilePreviewToggle: () => void;
}
const EditorToolbar = ({
@ -67,6 +72,9 @@ const EditorToolbar = ({
toggleScrollSync,
toggleI18n,
slug,
className,
showMobilePreview,
onMobilePreviewToggle,
}: TranslatedProps<EditorToolbarProps>) => {
const canCreate = useMemo(
() => ('folder' in collection && collection.create) ?? false,
@ -164,13 +172,22 @@ const EditorToolbar = ({
return useMemo(
() => (
<div className="flex gap-2">
<div
className={classNames(
`
flex
gap-2
`,
className,
)}
>
{showI18nToggle || showPreviewToggle || canDelete ? (
<Menu
key="extra-menu"
label={<MoreVertIcon className="w-5 h-5" />}
variant="text"
className="px-1.5"
rootClassName="hidden lg:flex"
buttonClassName="px-1.5"
hideDropdownIcon
>
<MenuGroup>
@ -215,12 +232,39 @@ const EditorToolbar = ({
) : null}
</Menu>
) : null}
{showPreviewToggle ? (
<IconButton
key="show-preview-button"
title={t('editor.editorInterface.preview')}
variant={showMobilePreview ? 'contained' : 'text'}
onClick={onMobilePreviewToggle}
className="flex lg:hidden"
>
<EyeIcon className="w-5 h-5" />
</IconButton>
) : null}
{canDelete ? (
<IconButton
key="delete-button"
title={t('editor.editorToolbar.deleteEntry')}
color="error"
variant="text"
onClick={onDelete}
className="flex lg:hidden"
>
<TrashIcon className="w-5 h-5" />
</IconButton>
) : null}
<Menu
label={
isPublished ? t('editor.editorToolbar.published') : t('editor.editorToolbar.publish')
}
color={isPublished ? 'success' : 'primary'}
disabled={isLoading || (menuItems.length == 1 && menuItems[0].length === 0)}
startIcon={PublishIcon}
iconClassName="flex !md:hidden"
labelClassName="hidden md:block"
hideDropdownIconOnMobile
>
{menuItems.map((group, index) => (
<MenuGroup key={`menu-group-${index}`}>{group}</MenuGroup>
@ -229,6 +273,7 @@ const EditorToolbar = ({
</div>
),
[
className,
showI18nToggle,
showPreviewToggle,
canDelete,
@ -241,6 +286,8 @@ const EditorToolbar = ({
toggleScrollSync,
scrollSyncActive,
onDelete,
showMobilePreview,
onMobilePreviewToggle,
isPublished,
menuItems,
],

View File

@ -30,6 +30,7 @@ export interface EditorControlPaneProps {
hideBorder: boolean;
slug?: string;
onLocaleChange?: (locale: string) => void;
allowDefaultLocale?: boolean;
}
const EditorControlPane = ({
@ -43,6 +44,7 @@ const EditorControlPane = ({
hideBorder,
slug,
onLocaleChange,
allowDefaultLocale = false,
t,
}: TranslatedProps<EditorControlPaneProps>) => {
const pathField = useMemo(
@ -95,12 +97,13 @@ const EditorControlPane = ({
flex
flex-col
min-h-full
w-full
`,
!hideBorder &&
`
border-r
border-slate-400
`,
lg:border-r
border-slate-400
`,
)}
>
{i18n?.locales && locale ? (
@ -117,6 +120,7 @@ const EditorControlPane = ({
})}
canChangeLocale={canChangeLocale}
onLocaleChange={onLocaleChange}
allowDefaultLocale={allowDefaultLocale}
/>
</div>
) : null}

View File

@ -9,6 +9,7 @@ interface LocaleDropdownProps {
defaultLocale: string;
dropdownText: string;
canChangeLocale: boolean;
allowDefaultLocale: boolean;
onLocaleChange?: (locale: string) => void;
}
@ -17,6 +18,7 @@ const LocaleDropdown = ({
defaultLocale,
dropdownText,
canChangeLocale,
allowDefaultLocale,
onLocaleChange,
}: LocaleDropdownProps) => {
if (!canChangeLocale) {
@ -39,7 +41,7 @@ const LocaleDropdown = ({
<Menu label={dropdownText}>
<MenuGroup>
{locales
.filter(locale => locale !== defaultLocale)
.filter(locale => allowDefaultLocale || locale !== defaultLocale)
.map(locale => (
<MenuItemButton key={locale} onClick={() => onLocaleChange?.(locale)}>
{locale}

View File

@ -90,6 +90,10 @@ const FrameGlobalStyles = `
height: 10px; /* Mostly for horizontal scrollbars */
}
.styled-scrollbars::-webkit-scrollbar-corner {
background: rgba(0,0,0,0);
}
.styled-scrollbars::-webkit-scrollbar-thumb {
/* Foreground */
background: var(--scrollbar-foreground);
@ -107,10 +111,11 @@ export interface EditorPreviewPaneProps {
entry: Entry;
previewInFrame: boolean;
editorSize: EditorSize;
showMobilePreview: boolean;
}
const PreviewPane = (props: TranslatedProps<EditorPreviewPaneProps>) => {
const { editorSize, entry, collection, fields, previewInFrame, t } = props;
const EditorPreviewPane = (props: TranslatedProps<EditorPreviewPaneProps>) => {
const { editorSize, entry, collection, fields, previewInFrame, showMobilePreview, t } = props;
const config = useAppSelector(selectConfig);
@ -175,62 +180,67 @@ const PreviewPane = (props: TranslatedProps<EditorPreviewPaneProps>) => {
<div
className={classNames(
`
h-main
h-main-mobile
md:h-main
absolute
top-16
right-0
w-full
`,
editorSize === EDITOR_SIZE_COMPACT ? 'w-preview' : 'w-6/12',
editorSize === EDITOR_SIZE_COMPACT ? 'lg:w-preview' : 'lg:w-6/12',
!showMobilePreview &&
`
hidden
lg:block
`,
)}
>
{!entry || !entry.data ? null : (
<ErrorBoundary config={config}>
{previewInFrame ? (
<Frame
key="preview-frame"
<ErrorBoundary config={config}>
{previewInFrame ? (
<Frame
key="preview-frame"
id="preview-pane"
head={previewStyles}
initialContent={initialFrameContent}
className="w-full h-full"
>
{!collection ? (
t('collection.notFound')
) : (
<PreviewFrameContent
key="preview-frame-content"
previewComponent={previewComponent}
previewProps={{ ...previewProps }}
/>
)}
</Frame>
) : (
<ScrollSyncPane key="preview-wrapper-scroll-sync">
<div
key="preview-wrapper"
id="preview-pane"
head={previewStyles}
initialContent={initialFrameContent}
className="w-full h-full"
>
{!collection ? (
t('collection.notFound')
) : (
<PreviewFrameContent
key="preview-frame-content"
previewComponent={previewComponent}
previewProps={{ ...previewProps }}
/>
)}
</Frame>
) : (
<ScrollSyncPane key="preview-wrapper-scroll-sync">
<div
key="preview-wrapper"
id="preview-pane"
className="
className="
overflow-y-auto
styled-scrollbars
h-full
"
>
{!collection ? (
t('collection.notFound')
) : (
<>
{previewStyles}
<EditorPreviewContent
key="preview-wrapper-content"
previewComponent={previewComponent}
previewProps={{ ...previewProps, document, window }}
/>
</>
)}
</div>
</ScrollSyncPane>
)}
</ErrorBoundary>
)}
>
{!collection ? (
t('collection.notFound')
) : (
<>
{previewStyles}
<EditorPreviewContent
key="preview-wrapper-content"
previewComponent={previewComponent}
previewProps={{ ...previewProps, document, window }}
/>
</>
)}
</div>
</ScrollSyncPane>
)}
</ErrorBoundary>
</div>,
element,
'preview-content',
@ -240,14 +250,14 @@ const PreviewPane = (props: TranslatedProps<EditorPreviewPaneProps>) => {
config,
editorSize,
element,
entry,
initialFrameContent,
previewComponent,
previewInFrame,
previewProps,
previewStyles,
showMobilePreview,
t,
]);
};
export default translate()(PreviewPane) as FC<EditorPreviewPaneProps>;
export default translate()(EditorPreviewPane) as FC<EditorPreviewPaneProps>;

View File

@ -0,0 +1,80 @@
import { Add as AddIcon } from '@styled-icons/material/Add';
import { Menu as MenuIcon } from '@styled-icons/material/Menu';
import { OpenInNew as OpenInNewIcon } from '@styled-icons/material/OpenInNew';
import React, { useCallback, useMemo, useState } from 'react';
import { useParams } from 'react-router-dom';
import useNewEntryUrl from '@staticcms/core/lib/hooks/useNewEntryUrl';
import { isNotEmpty } from '@staticcms/core/lib/util/string.util';
import { selectDisplayUrl } from '@staticcms/core/reducers/selectors/config';
import { useAppSelector } from '@staticcms/core/store/hooks';
import IconButton from '../common/button/IconButton';
import NavigationDrawer from './NavigationDrawer';
import QuickCreate from './QuickCreate';
import type { Collection } from '@staticcms/core/interface';
import type { FC } from 'react';
export interface BottomNavigationProps {
collection: Collection | undefined;
}
const BottomNavigation: FC<BottomNavigationProps> = ({ collection }) => {
const params = useParams();
const filterTerm = useMemo(() => params['*'], [params]);
const newEntryUrl = useNewEntryUrl(collection, filterTerm);
const displayUrl = useAppSelector(selectDisplayUrl);
const [mobileOpen, setMobileOpen] = useState(false);
const toggleMobileMenu = useCallback(() => {
setMobileOpen(old => !old);
}, []);
return (
<>
<div
className="
fixed
bottom-0
left-0
right-0
h-16
shadow-bottom-navigation
bg-gray-50
dark:bg-gray-800
flex
px-6
py-1
md:hidden
"
>
<IconButton variant="text" className="flex-grow" onClick={toggleMobileMenu}>
<MenuIcon className="w-6 h-6" />
</IconButton>
{isNotEmpty(newEntryUrl) ? (
<IconButton to={newEntryUrl} variant="text" className="flex-grow">
<AddIcon className="w-6 h-6" />
</IconButton>
) : (
<QuickCreate
key="quick-create"
variant="text"
rootClassName="flex-grow"
buttonClassName="w-full"
hideDropdownIcon
hideLabel
/>
)}
{displayUrl ? (
<IconButton variant="text" className="flex-grow" href={displayUrl}>
<OpenInNewIcon className="h-6 w-6" />
</IconButton>
) : null}
</div>
<NavigationDrawer mobileOpen={mobileOpen} onMobileOpenToggle={toggleMobileMenu} />
</>
);
};
export default BottomNavigation;

View File

@ -0,0 +1,142 @@
import { ArrowBack as ArrowBackIcon } from '@styled-icons/material/ArrowBack';
import React, { Fragment, useMemo } from 'react';
import { Link } from 'react-router-dom';
import classNames from '@staticcms/core/lib/util/classNames.util';
import type { Breadcrumb } from '@staticcms/core/interface';
import type { FC } from 'react';
export interface BreadcrumbsProps {
breadcrumbs: Breadcrumb[];
inEditor?: boolean;
}
const Breadcrumbs: FC<BreadcrumbsProps> = ({ breadcrumbs, inEditor = false }) => {
const finalNonEditorBreadcrumb = useMemo(() => {
const nonEditorBreadcrumbs = breadcrumbs.filter(b => !b.editor);
if (nonEditorBreadcrumbs.length === 0) {
return undefined;
}
return nonEditorBreadcrumbs[nonEditorBreadcrumbs.length - 1];
}, [breadcrumbs]);
return (
<div
className="
flex
h-full
md:w-auto
items-center
text-xl
font-semibold
gap-1
text-gray-800
dark:text-white
flex-grow
truncate
"
>
<div
className="
hidden
md:flex
overflow-hidden
relative
flex-grow
h-full
"
>
<div
className="
w-full
absolute
inset-0
flex
items-center
gap-1
"
>
{breadcrumbs.map((breadcrumb, index) =>
breadcrumb.name ? (
<Fragment key={`breadcrumb-${index}`}>
{index > 0 ? <span key={`separator-${index}`}>&#62;</span> : null}
{breadcrumb.to ? (
<Link
key={`link-${index}`}
className={classNames(
`
hover:text-gray-400
dark:hover:text-gray-400
overflow-hidden
whitespace-nowrap
focus:outline-none
focus:ring-4
focus:ring-gray-200
dark:focus:ring-slate-700
`,
index + 1 === breadcrumbs.length ? 'text-ellipsis' : 'flex-shrink-0',
)}
to={breadcrumb.to}
>
{breadcrumb.name}
</Link>
) : (
<span
key={`text-${index}`}
className={classNames(
`
truncate
`,
index + 1 === breadcrumbs.length ? 'text-ellipsis' : 'flex-shrink-0',
)}
>
{breadcrumb.name}
</span>
)}
</Fragment>
) : null,
)}
</div>
</div>
{finalNonEditorBreadcrumb ? (
finalNonEditorBreadcrumb.to ? (
<Link
key="final-non-editor-breadcrumb-link"
className="
flex
md:hidden
gap-2
items-center
truncate
w-full
focus:outline-none
focus:ring-4
focus:ring-gray-200
dark:focus:ring-slate-700
"
to={finalNonEditorBreadcrumb.to}
>
{inEditor ? <ArrowBackIcon className="w-6 h-6" /> : null}
{finalNonEditorBreadcrumb.name}
</Link>
) : (
<div
key="final-non-editor-breadcrumb-text"
className="
block
md:hidden
truncate
w-full
"
>
{finalNonEditorBreadcrumb?.name ?? ''}
</div>
)
) : null}
</div>
);
};
export default Breadcrumbs;

View File

@ -1,18 +1,19 @@
import { OpenInNew as OpenInNewIcon } from '@styled-icons/material/OpenInNew';
import React, { Fragment, useEffect } from 'react';
import React, { useEffect, useMemo } from 'react';
import { translate } from 'react-polyglot';
import { Link } from 'react-router-dom';
import { checkBackendStatus } from '@staticcms/core/actions/status';
import classNames from '@staticcms/core/lib/util/classNames.util';
import { selectConfig, selectDisplayUrl } from '@staticcms/core/reducers/selectors/config';
import { useAppDispatch, useAppSelector } from '@staticcms/core/store/hooks';
import Button from '../common/button/Button';
import { StaticCmsIcon } from '../images/_index';
import Breadcrumbs from './Breadcrumbs';
import QuickCreate from './QuickCreate';
import SettingsDropdown from './SettingsDropdown';
import type { Breadcrumb, TranslatedProps } from '@staticcms/core/interface';
import type { ComponentType, ReactNode } from 'react';
import type { FC, ReactNode } from 'react';
export interface NavbarProps {
breadcrumbs?: Breadcrumb[];
@ -40,6 +41,11 @@ const Navbar = ({
};
}, [dispatch]);
const inEditor = useMemo(
() => Boolean(breadcrumbs.length > 0 && breadcrumbs[breadcrumbs.length - 1].editor),
[breadcrumbs],
);
return (
<nav
className="
@ -52,49 +58,56 @@ const Navbar = ({
"
>
<div key="nav" className="mx-auto pr-2 sm:pr-4 lg:pr-5">
<div className="relative flex h-16 items-center justify-between">
<div className="flex flex-1 items-center justify-center h-full sm:items-stretch sm:justify-start gap-4">
<div className="flex flex-shrink-0 items-center justify-center bg-slate-500 dark:bg-slate-700 w-16">
<div
className={classNames(
`
relative
flex
h-16
items-center
justify-between
gap-2
`,
inEditor && 'pl-3 md:pl-0',
)}
>
<div className="flex flex-1 h-full items-stretch justify-start gap-2 md:gap-4 truncate">
<div
className={classNames(
`
flex-shrink-0
items-center
justify-center
w-16
bg-slate-500
dark:bg-slate-700
`,
inEditor ? 'hidden md:flex' : 'flex',
)}
>
{config?.logo_url ? (
<div
className="inline-flex h-10 w-10 bg-cover bg-no-repeat bg-center object-cover"
className="h-10 w-10 bg-cover bg-no-repeat bg-center object-cover"
style={{ backgroundImage: `url('${config.logo_url}')` }}
/>
) : (
<StaticCmsIcon className="inline-flex w-10 h-10" />
)}
</div>
<div className="flex h-full items-center text-xl font-semibold gap-1 text-gray-800 dark:text-white">
{breadcrumbs.map((breadcrumb, index) =>
breadcrumb.name ? (
<Fragment key={`breadcrumb-${index}`}>
{index > 0 ? <span key={`separator-${index}`}>&#62;</span> : null}
{breadcrumb.to ? (
<Link
key={`link-${index}`}
className="hover:text-gray-400 dark:hover:text-gray-400"
to={breadcrumb.to}
>
{breadcrumb.name}
</Link>
) : (
<span key={`text-${index}`}>{breadcrumb.name}</span>
)}
</Fragment>
) : null,
<StaticCmsIcon className="w-10 h-10" />
)}
</div>
<Breadcrumbs breadcrumbs={breadcrumbs} inEditor={inEditor} />
</div>
<div className="flex gap-3 items-center">
{displayUrl ? (
<Button variant="text" className="flex gap-2" href={displayUrl}>
{displayUrl}
<Button variant="text" className="gap-2 hidden lg:flex" href={displayUrl}>
<div className="hidden lg:flex">{displayUrl}</div>
<OpenInNewIcon className="h-5 w-5 text-gray-400 dark:text-gray-500" />
</Button>
) : null}
{showQuickCreate ? <QuickCreate key="quick-create" /> : null}
{showQuickCreate ? (
<QuickCreate key="quick-create" rootClassName="hidden md:block" />
) : null}
{navbarActions}
<SettingsDropdown />
<SettingsDropdown inEditor={inEditor} />
</div>
</div>
</div>
@ -102,4 +115,4 @@ const Navbar = ({
);
};
export default translate()(Navbar) as ComponentType<NavbarProps>;
export default translate()(Navbar) as FC<NavbarProps>;

View File

@ -0,0 +1,56 @@
import SwipeableDrawer from '@mui/material/SwipeableDrawer';
import React, { useMemo } from 'react';
import SidebarContent from './SidebarContent';
const DRAWER_WIDTH = 320;
interface NavigationDrawerProps {
mobileOpen: boolean;
onMobileOpenToggle: () => void;
}
const NavigationDrawer = ({ mobileOpen, onMobileOpenToggle }: NavigationDrawerProps) => {
const iOS = useMemo(
() => typeof navigator !== 'undefined' && /iPad|iPhone|iPod/.test(navigator.userAgent),
[],
);
const container = useMemo(
() => (typeof window !== 'undefined' ? window.document.body : undefined),
[],
);
return (
<SwipeableDrawer
disableBackdropTransition={!iOS}
disableDiscovery={iOS}
container={container}
variant="temporary"
open={mobileOpen}
onOpen={onMobileOpenToggle}
onClose={onMobileOpenToggle}
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,
},
}}
>
<div onClick={onMobileOpenToggle} className="w-full h-full">
<SidebarContent />
</div>
</SwipeableDrawer>
);
};
export default NavigationDrawer;

View File

@ -6,12 +6,19 @@ import { getNewEntryUrl } from '@staticcms/core/lib/urlHelper';
import { selectCollections } from '@staticcms/core/reducers/selectors/collections';
import { useAppSelector } from '@staticcms/core/store/hooks';
import Menu from '../common/menu/Menu';
import MenuItemLink from '../common/menu/MenuItemLink';
import MenuGroup from '../common/menu/MenuGroup';
import MenuItemLink from '../common/menu/MenuItemLink';
import type { TranslateProps } from 'react-polyglot';
import type { TranslatedProps } from '@staticcms/core/interface';
import type { FC } from 'react';
import type { MenuProps } from '../common/menu/Menu';
const QuickCreate = ({ t }: TranslateProps) => {
export type QuickCreateProps = Pick<
MenuProps,
'rootClassName' | 'buttonClassName' | 'hideDropdownIcon' | 'hideLabel' | 'variant'
>;
const QuickCreate: FC<TranslatedProps<QuickCreateProps>> = ({ t, ...menuProps }) => {
const collections = useAppSelector(selectCollections);
const createableCollections = useMemo(
@ -23,7 +30,7 @@ const QuickCreate = ({ t }: TranslateProps) => {
);
return (
<Menu label={t('app.header.quickAdd')} startIcon={AddIcon}>
<Menu label={t('app.header.quickAdd')} startIcon={AddIcon} {...menuProps}>
<MenuGroup>
{createableCollections.map(collection => (
<MenuItemLink key={collection.name} href={getNewEntryUrl(collection.name)}>
@ -35,4 +42,4 @@ const QuickCreate = ({ t }: TranslateProps) => {
);
};
export default translate()(QuickCreate);
export default translate()(QuickCreate) as FC<QuickCreateProps>;

View File

@ -13,8 +13,8 @@ import MenuGroup from '../common/menu/MenuGroup';
import MenuItemButton from '../common/menu/MenuItemButton';
import Switch from '../common/switch/Switch';
import type { ChangeEvent, MouseEvent } from 'react';
import type { TranslateProps } from 'react-polyglot';
import type { TranslatedProps } from '@staticcms/core/interface';
import type { ChangeEvent, FC, MouseEvent } from 'react';
interface AvatarImageProps {
imageUrl: string | undefined;
@ -28,7 +28,11 @@ const AvatarImage = ({ imageUrl }: AvatarImageProps) => {
);
};
const SettingsDropdown = ({ t }: TranslateProps) => {
export interface SettingsDropdownProps {
inEditor: boolean;
}
const SettingsDropdown: FC<TranslatedProps<SettingsDropdownProps>> = ({ inEditor, t }) => {
const dispatch = useAppDispatch();
const user = useAppSelector(selectUser);
const [isDarkMode, setIsDarkMode] = useState(document.documentElement.classList.contains('dark'));
@ -84,6 +88,7 @@ const SettingsDropdown = ({ t }: TranslateProps) => {
variant="outlined"
rounded={!user?.avatar_url || 'no-padding'}
hideDropdownIcon
rootClassName={inEditor ? 'hidden md:flex' : ''}
>
<MenuGroup>
<MenuItemButton key="dark-mode" onClick={handleToggleDarkMode} startIcon={MoonIcon}>
@ -102,4 +107,4 @@ const SettingsDropdown = ({ t }: TranslateProps) => {
);
};
export default translate()(SettingsDropdown);
export default translate()(SettingsDropdown) as FC<SettingsDropdownProps>;

View File

@ -1,139 +1,19 @@
import { Photo as PhotoIcon } from '@styled-icons/material/Photo';
import React, { useCallback, useMemo } from 'react';
import { translate } from 'react-polyglot';
import { useNavigate, useParams } from 'react-router-dom';
import React from 'react';
import { getIcon } from '@staticcms/core/lib/hooks/useIcon';
import { getAdditionalLinks } from '@staticcms/core/lib/registry';
import classNames from '@staticcms/core/lib/util/classNames.util';
import { selectCollections } from '@staticcms/core/reducers/selectors/collections';
import { selectIsSearchEnabled } from '@staticcms/core/reducers/selectors/config';
import { useAppSelector } from '@staticcms/core/store/hooks';
import CollectionSearch from '../collections/CollectionSearch';
import NestedCollection from '../collections/NestedCollection';
import NavLink from './NavLink';
import SidebarContent from './SidebarContent';
import type { Collection } from '@staticcms/core/interface';
import type { FC } from 'react';
import type { TranslateProps } from 'react-polyglot';
const Sidebar: FC<TranslateProps> = ({ t }) => {
const { name, searchTerm, ...params } = useParams();
const filterTerm = useMemo(() => params['*'] ?? '', [params]);
const navigate = useNavigate();
const isSearchEnabled = useAppSelector(selectIsSearchEnabled);
const collections = useAppSelector(selectCollections);
const collection = useMemo(
() => (name ? collections[name] : collections[0]) as Collection | undefined,
[collections, name],
);
const collectionLinks = useMemo(
() =>
Object.values(collections)
.filter(collection => collection.hide !== true)
.map(collection => {
const collectionName = collection.name;
const icon = getIcon(collection.icon);
if ('nested' in collection) {
return (
<NestedCollection
key={`nested-${collectionName}`}
collection={collection}
filterTerm={filterTerm}
data-testid={collectionName}
/>
);
}
return (
<NavLink key={collectionName} to={`/collections/${collectionName}`} icon={icon}>
{collection.label}
</NavLink>
);
}),
[collections, filterTerm],
);
const additionalLinks = useMemo(() => getAdditionalLinks(), []);
const links = useMemo(
() =>
Object.values(additionalLinks).map(
({ id, title, data, options: { icon: iconName } = {} }) => {
const icon = getIcon(iconName);
return typeof data === 'string' ? (
<NavLink key={title} href={data} icon={icon}>
{title}
</NavLink>
) : (
<NavLink key={title} to={`/page/${id}`} icon={icon}>
{title}
</NavLink>
);
},
),
[additionalLinks],
);
const searchCollections = useCallback(
(query?: string, collection?: string) => {
if (!query) {
return;
}
if (collection) {
navigate(`/collections/${collection}/search/${query}`);
} else {
navigate(`/search/${query}`);
}
},
[navigate],
);
const Sidebar: FC = () => {
return (
<aside
className={classNames(
'w-sidebar-expanded',
'h-main sm:fixed sm:z-20 sm:shadow-sidebar lg:block lg:z-auto lg:shadow-none',
)}
className={classNames('w-sidebar-expanded', 'h-main-mobile md:h-main hidden md:block')}
aria-label="Sidebar"
>
<div
className="
px-3
py-4
h-full
w-full
overflow-y-auto
bg-white
dark:bg-slate-800
styled-scrollbars
"
>
<ul className="space-y-2">
{isSearchEnabled && (
<CollectionSearch
searchTerm={searchTerm}
collections={collections}
collection={collection}
onSubmit={(query: string, collection?: string) =>
searchCollections(query, collection)
}
/>
)}
{collectionLinks}
{links}
<NavLink key="Media" to="/media" icon={<PhotoIcon className="h-6 w-6" />}>
{t('app.header.media')}
</NavLink>
</ul>
</div>
<SidebarContent />
</aside>
);
};
export default translate()(Sidebar) as FC;
export default Sidebar;

View File

@ -0,0 +1,128 @@
import { Photo as PhotoIcon } from '@styled-icons/material/Photo';
import React, { useCallback, useMemo } from 'react';
import { translate } from 'react-polyglot';
import { useNavigate, useParams } from 'react-router-dom';
import { getIcon } from '@staticcms/core/lib/hooks/useIcon';
import { getAdditionalLinks } from '@staticcms/core/lib/registry';
import { selectCollections } from '@staticcms/core/reducers/selectors/collections';
import { selectIsSearchEnabled } from '@staticcms/core/reducers/selectors/config';
import { useAppSelector } from '@staticcms/core/store/hooks';
import CollectionSearch from '../collections/CollectionSearch';
import NestedCollection from '../collections/NestedCollection';
import NavLink from './NavLink';
import type { Collection } from '@staticcms/core/interface';
import type { FC } from 'react';
import type { TranslateProps } from 'react-polyglot';
const SidebarContent: FC<TranslateProps> = ({ t }) => {
const { name, searchTerm, ...params } = useParams();
const filterTerm = useMemo(() => params['*'] ?? '', [params]);
const navigate = useNavigate();
const isSearchEnabled = useAppSelector(selectIsSearchEnabled);
const collections = useAppSelector(selectCollections);
const collection = useMemo(
() => (name ? collections[name] : collections[0]) as Collection | undefined,
[collections, name],
);
const collectionLinks = useMemo(
() =>
Object.values(collections)
.filter(collection => collection.hide !== true)
.map(collection => {
const collectionName = collection.name;
const icon = getIcon(collection.icon);
if ('nested' in collection) {
return (
<NestedCollection
key={`nested-${collectionName}`}
collection={collection}
filterTerm={filterTerm}
data-testid={collectionName}
/>
);
}
return (
<NavLink key={collectionName} to={`/collections/${collectionName}`} icon={icon}>
{collection.label}
</NavLink>
);
}),
[collections, filterTerm],
);
const additionalLinks = useMemo(() => getAdditionalLinks(), []);
const links = useMemo(
() =>
Object.values(additionalLinks).map(
({ id, title, data, options: { icon: iconName } = {} }) => {
const icon = getIcon(iconName);
return typeof data === 'string' ? (
<NavLink key={title} href={data} icon={icon}>
{title}
</NavLink>
) : (
<NavLink key={title} to={`/page/${id}`} icon={icon}>
{title}
</NavLink>
);
},
),
[additionalLinks],
);
const searchCollections = useCallback(
(query?: string, collection?: string) => {
if (!query) {
return;
}
if (collection) {
navigate(`/collections/${collection}/search/${query}`);
} else {
navigate(`/search/${query}`);
}
},
[navigate],
);
return (
<div
className="
px-3
py-4
h-full
w-full
overflow-y-auto
bg-white
dark:bg-slate-800
styled-scrollbars
"
>
<ul className="space-y-2">
{isSearchEnabled && (
<CollectionSearch
searchTerm={searchTerm}
collections={collections}
collection={collection}
onSubmit={(query: string, collection?: string) => searchCollections(query, collection)}
/>
)}
{collectionLinks}
{links}
<NavLink key="Media" to="/media" icon={<PhotoIcon className="h-6 w-6" />}>
{t('app.header.media')}
</NavLink>
</ul>
</div>
);
};
export default translate()(SidebarContent) as FC;

View File

@ -72,6 +72,9 @@ export const INFERABLE_FIELDS: Record<string, InferredField> = {
'cover',
'hero',
'logo',
'cover_image',
'cover-image',
'coverimage',
],
defaultPreview: value => value,
fallbackToFirstField: false,

View File

@ -1103,6 +1103,7 @@ export interface SvgProps {
export interface Breadcrumb {
name?: string;
to?: string;
editor?: boolean;
}
export interface MediaLibraryDisplayURL {

View File

@ -83,6 +83,7 @@ export default function useBreadcrumbs(
collectionLabel: collection.label_singular || collection.label,
})
: summary,
editor: true,
});
}

View File

@ -0,0 +1,25 @@
import { useEffect } from 'react';
export interface MetaProps {
name: string;
content: string;
}
export default function useMeta({ name, content }: MetaProps) {
useEffect(() => {
const head = document.querySelector('head');
if (!head) {
return;
}
const meta = document.createElement('meta');
meta.setAttribute('name', name);
meta.setAttribute('content', content);
head.appendChild(meta);
return () => {
head.removeChild(meta);
};
}, [content, name]);
}

View File

@ -0,0 +1,21 @@
import { useMemo } from 'react';
import { getNewEntryUrl } from '../urlHelper';
import { isNotEmpty } from '../util/string.util';
import type { Collection } from '@staticcms/core/interface';
export default function useNewEntryUrl(
collection: Collection | undefined,
filterTerm: string | undefined,
) {
return useMemo(() => {
if (!collection) {
return undefined;
}
return 'fields' in collection && collection.create
? `${getNewEntryUrl(collection.name)}${isNotEmpty(filterTerm) ? `/${filterTerm}` : ''}`
: '';
}, [collection, filterTerm]);
}

View File

@ -69,8 +69,7 @@ export function getLocaleDataPath(locale: string) {
}
export function getDataPath(locale: string, defaultLocale: string) {
const dataPath = locale !== defaultLocale ? getLocaleDataPath(locale) : ['data'];
return dataPath;
return locale !== defaultLocale ? getLocaleDataPath(locale) : ['data'];
}
export function getFilePath(

View File

@ -8,13 +8,15 @@
@layer components {
html {
@apply overflow-hidden;
@apply overflow-hidden
w-screen;
}
body {
@apply text-gray-800
dark:text-gray-100
overflow-hidden;
overflow-hidden
w-screen;
}
/**
@ -29,7 +31,8 @@
focus:outline-none
focus:ring-4
focus:ring-gray-200
dark:focus:ring-slate-700;
dark:focus:ring-slate-700
border;
}
.btn {
@ -64,8 +67,7 @@
}
.btn-contained-primary {
@apply border
border-transparent
@apply border-transparent
bg-blue-700
hover:bg-blue-800
text-white
@ -80,7 +82,6 @@
.btn-outlined-primary {
@apply text-gray-800
bg-transparent
border
border-gray-200
hover:bg-slate-100
hover:border-blue-400
@ -101,6 +102,7 @@
.btn-text-primary {
@apply bg-transparent
border-transparent
text-gray-800
hover:text-blue-700
hover:bg-blue-100
@ -110,13 +112,11 @@
dark:hover:text-blue-400
dark:hover:bg-slate-700/75
dark:disabled:text-slate-600/75
dark:disabled:border-slate-600/75
dark:disabled:hover:bg-transparent;
}
.btn-contained-secondary {
@apply border
font-medium
@apply font-medium
text-gray-600
bg-white
border-gray-200/75
@ -140,7 +140,6 @@
.btn-outlined-secondary {
@apply text-gray-800
bg-transparent
border
border-gray-200
hover:bg-gray-100
hover:text-gray-800
@ -161,6 +160,7 @@
.btn-text-secondary {
@apply bg-transparent
border-transparent
text-gray-800
hover:text-gray-800
hover:bg-gray-100
@ -170,13 +170,11 @@
dark:hover:text-white
dark:hover:bg-gray-700
dark:disabled:text-gray-400/20
dark:disabled:border-gray-600/20
dark:disabled:hover:bg-transparent;
}
.btn-contained-success {
@apply border
border-transparent
@apply border-transparent
bg-green-600
hover:bg-green-700
text-white
@ -190,7 +188,6 @@
.btn-outlined-success {
@apply bg-transparent
border
text-green-500
border-green-200
hover:bg-green-100
@ -210,6 +207,7 @@
.btn-text-success {
@apply bg-transparent
border-transparent
text-green-500
hover:text-green-700
hover:bg-green-100
@ -219,13 +217,11 @@
dark:hover:text-green-500
dark:hover:bg-green-700/10
dark:disabled:text-slate-600/75
dark:disabled:border-slate-600/75
dark:disabled:hover:bg-transparent;
}
.btn-contained-error {
@apply border
border-transparent
@apply border-transparent
bg-red-600
hover:bg-red-700
text-white
@ -239,7 +235,6 @@
.btn-outlined-error {
@apply bg-transparent
border
text-red-500
border-red-200
hover:bg-red-100
@ -259,6 +254,7 @@
.btn-text-error {
@apply bg-transparent
border-transparent
text-red-500
hover:text-red-700
hover:bg-red-100
@ -268,13 +264,11 @@
dark:hover:text-red-500
dark:hover:bg-red-700/10
dark:disabled:text-slate-600/75
dark:disabled:border-slate-600/75
dark:disabled:hover:bg-transparent;
}
.btn-contained-warning {
@apply border
border-transparent
@apply border-transparent
bg-yellow-500
hover:bg-yellow-600
text-white
@ -308,6 +302,7 @@
.btn-text-warning {
@apply bg-transparent
border-transparent
text-yellow-500
hover:text-yellow-600
hover:bg-yellow-50
@ -317,7 +312,6 @@
dark:hover:text-yellow-500
dark:hover:bg-yellow-700/10
dark:disabled:text-slate-600/75
dark:disabled:border-slate-600/75
dark:disabled:hover:bg-transparent;
}
@ -379,6 +373,10 @@
height: 10px; /* Mostly for horizontal scrollbars */
}
.styled-scrollbars::-webkit-scrollbar-corner {
background: rgba(0, 0, 0, 0);
}
.styled-scrollbars::-webkit-scrollbar-thumb {
/* Foreground */
background: var(--scrollbar-foreground);
@ -389,6 +387,20 @@
background: var(--scrollbar-background);
}
.hide-scrollbar {
-ms-overflow-style: none; /* Internet Explorer 10+ */
scrollbar-width: none; /* Firefox */
}
.hide-scrollbar::-webkit-scrollbar {
display: none; /* Safari and Chrome */
}
.hide-tap {
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
-webkit-tap-highlight-color: transparent;
}
table {
tbody {
tr {

View File

@ -249,6 +249,7 @@ const DateTimeControl: FC<WidgetControlProps<string | Date, DateTimeField>> = ({
{...convertMuiTextFieldProps(props)}
inputRef={ref}
cursor="pointer"
inputClassName="truncate"
/>
<NowButton
key="mobile-date-now"
@ -282,6 +283,7 @@ const DateTimeControl: FC<WidgetControlProps<string | Date, DateTimeField>> = ({
forSingleList={forSingleList}
cursor="pointer"
disabled={disabled}
wrapperClassName="!w-date-widget"
>
<LocalizationProvider key="localization-provider" dateAdapter={AdapterDateFns}>
{dateTimePicker}

View File

@ -350,7 +350,7 @@ const withFileControl = ({ forImage = false }: WithFileControlProps = {}) => {
if (Array.isArray(internalValue) ? internalValue.length === 0 : isEmpty(internalValue)) {
return (
<div key="selection" className="flex flex-col gap-2 px-3 pt-2 pb-4">
<div key="controls" className="flex gap-2">
<div key="controls" className="flex gap-2 flex-col xs:flex-row">
<Button
buttonRef={uploadButtonRef}
color="primary"
@ -388,7 +388,7 @@ const withFileControl = ({ forImage = false }: WithFileControlProps = {}) => {
)}
>
{renderedImagesLinks}
<div key="controls" className="flex gap-2">
<div key="controls" className="flex gap-2 flex-col xs:flex-row">
<Button
buttonRef={uploadButtonRef}
color="primary"

View File

@ -364,7 +364,7 @@ const ListControl: FC<WidgetControlProps<ValueOrNestedValue[], ListField>> = pro
<Menu
label={t('editor.editorWidgets.list.addType', { item: label })}
variant="outlined"
className="w-full z-20"
buttonClassName="w-full z-20"
data-testid="list-type-add"
disabled={disabled}
>

View File

@ -1,5 +1,5 @@
import { MDXProvider } from '@mdx-js/react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import { VFileMessage } from 'vfile-message';
import { withMdxImage } from '@staticcms/core/components/common/image/Image';
@ -11,20 +11,33 @@ import { processShortcodeConfigToMdx } from './plate/serialization/slate/process
import type { MarkdownField, WidgetPreviewProps } from '@staticcms/core/interface';
import type { FC } from 'react';
import type { UseMdxState } from './plate/hooks/useMdx';
interface FallbackComponentProps {
error: string;
interface MdxComponentProps {
state: UseMdxState;
}
function FallbackComponent({ error }: FallbackComponentProps) {
const message = new VFileMessage(error);
message.fatal = true;
return (
<pre>
<code>{String(message)}</code>
</pre>
);
}
// Create a preview component that can handle errors with try-catch block; for catching invalid JS expressions errors that ErrorBoundary cannot catch.
const MdxComponent: FC<MdxComponentProps> = ({ state }) => {
const Result = useMemo(() => state.file?.result as FC | undefined, [state]);
if (!Result) {
return null;
}
try {
return <Result key="result" />;
} catch (error) {
const message = new VFileMessage(String(error));
message.fatal = true;
return (
<pre key="error">
<code>{String(message)}</code>
</pre>
);
}
};
const MarkdownPreview: FC<WidgetPreviewProps<string, MarkdownField>> = previewProps => {
const { value, collection, field } = previewProps;
@ -40,7 +53,7 @@ const MarkdownPreview: FC<WidgetPreviewProps<string, MarkdownField>> = previewPr
);
const [state, setValue] = useMdx(`editor-${id}.mdx`, value ?? '');
const [prevValue, setPrevValue] = useState('');
const [prevValue, setPrevValue] = useState<string | null>(null);
useEffect(() => {
if (prevValue !== value) {
const parsedValue = processShortcodeConfigToMdx(getShortcodes(), value ?? '');
@ -49,35 +62,13 @@ const MarkdownPreview: FC<WidgetPreviewProps<string, MarkdownField>> = previewPr
}
}, [prevValue, setValue, value]);
// Create a preview component that can handle errors with try-catch block; for catching invalid JS expressions errors that ErrorBoundary cannot catch.
const MdxComponent = useCallback(() => {
if (!state.file) {
return null;
}
try {
return (state.file.result as FC)({});
} catch (error) {
return <FallbackComponent error={String(error)} />;
}
}, [state.file]);
return useMemo(() => {
if (!value) {
return null;
}
return (
<div>
{state.file && state.file.result ? (
<MDXProvider components={components}>
<MdxComponent />
</MDXProvider>
) : null}
</div>
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [MdxComponent]);
return (
<div key="markdown-preview">
<MDXProvider components={components}>
<MdxComponent state={state} />{' '}
</MDXProvider>
</div>
);
};
export default MarkdownPreview;

View File

@ -280,6 +280,7 @@ const PlateEditor: FC<PlateEditorProps> = ({
...editableProps,
onFocus,
onBlur,
className: '!outline-none',
}}
>
<div key="editor-inner-wrapper" ref={innerEditorContainerRef}>

View File

@ -40,7 +40,7 @@ const ShortcodeToolbarButton: FC<ShortcodeToolbarButtonProps> = ({ disabled }) =
keepMounted
hideDropdownIcon
variant="text"
className="
buttonClassName="
py-0.5
px-0.5
h-6

View File

@ -8,6 +8,7 @@ import { VFileMessage } from 'vfile-message';
import useDebouncedCallback from '@staticcms/core/lib/hooks/useDebouncedCallback';
import flattenListItemParagraphs from '../serialization/slate/flattenListItemParagraphs';
import useDebounce from '@staticcms/core/lib/hooks/useDebounce';
export interface UseMdxState {
file: VFile | null;
@ -49,10 +50,11 @@ export default function useMdx(
);
const setValue = useDebouncedCallback(setValueCallback, 100);
const debouncedState = useDebounce(state, 150);
useEffect(() => {
setValue(input);
}, [input, setValue]);
return [state, setValue];
return [debouncedState, setValue];
}

View File

@ -64,7 +64,7 @@ export function getToolbarButtons(
keepMounted
hideDropdownIcon
variant="text"
className="
buttonClassName="
py-0.5
px-0.5
h-6

View File

@ -322,7 +322,7 @@ const RelationControl: FC<WidgetControlProps<string | string[], RelationField>>
label={
<>
{Array.isArray(selectedValue) && selectedValue.length > 0 ? (
<div className="flex flex-wrap gap-2 w-full p-2 pr-0 max-w-fit">
<div className="flex flex-wrap gap-2 p-2 pr-0 w-relation-widget-label">
{selectedValue.map(selectValue => {
const option = uniqueOptionsByValue[selectValue];
return (

View File

@ -87,7 +87,14 @@ const UUIDControl: FC<WidgetControlProps<string, UUIDField>> = ({
) : null
}
>
<TextField type="text" inputRef={ref} value={internalValue} disabled={disabled} readonly />
<TextField
type="text"
inputRef={ref}
value={internalValue}
disabled={disabled}
readonly
inputClassName="truncate"
/>
</Field>
);
};

View File

@ -4,13 +4,14 @@ module.exports = {
extend: {
height: {
main: "calc(100vh - 64px)",
"main-mobile": "calc(100vh - 128px)",
"media-library-dialog": "80vh",
"media-card": "240px",
"media-preview-image": "104px",
"media-card-image": "196px",
"image-card": "120px",
input: "24px",
"table-full": "calc(100% - 40px)"
"table-full": "calc(100% - 40px)",
},
minHeight: {
8: "2rem",
@ -26,12 +27,20 @@ module.exports = {
"media-card": "240px",
"media-preview-image": "126px",
"image-card": "120px",
"card-grid": "calc(100% + 8px)",
"breadcrumb-title-small": "calc(100vw - 126px)",
"breadcrumb-title": "calc(100vw * .4)",
"collection-header": "calc(100% - 32px)",
"date-widget": "calc(100% - 58px)",
"relation-widget-label": "calc(100% - 32px)",
"select-widget-label": "calc(100% - 12px)",
},
maxWidth: {
"media-search": "400px",
},
boxShadow: {
sidebar: "0 10px 15px 18px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)",
"bottom-navigation": "0 -10px 10px -5px rgb(0 0 0 / 0.15)",
},
gridTemplateColumns: {
editor: "450px auto",
@ -42,5 +51,24 @@ module.exports = {
sans: ["Inter var", "Inter", "ui-sans-serif", "system-ui", "sans-serif"],
},
},
screens: {
xs: "480px",
// => @media (min-width: 480px) { ... }
sm: "640px",
// => @media (min-width: 640px) { ... }
md: "768px",
// => @media (min-width: 768px) { ... }
lg: "1024px",
// => @media (min-width: 1024px) { ... }
xl: "1280px",
// => @media (min-width: 1280px) { ... }
"2xl": "1536px",
// => @media (min-width: 1536px) { ... }
},
},
};

1096
yarn.lock

File diff suppressed because it is too large Load Diff