mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-25 11:55:03 +03:00
Refined navigation design in AdminX
refs. https://github.com/TryGhost/Team/issues/3432
This commit is contained in:
parent
5559b2234e
commit
5bfbd5a7a0
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><defs></defs><title>add</title><line x1="0.75" y1="12" x2="23.25" y2="12" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5px"></line><line x1="12" y1="0.75" x2="12" y2="23.25" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5px"></line></svg>
|
After Width: | Height: | Size: 401 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><defs></defs><title>navigation-menu</title><line x1="2.25" y1="18.003" x2="21.75" y2="18.003" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5px"></line><line x1="2.25" y1="12.003" x2="21.75" y2="12.003" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5px"></line><line x1="2.25" y1="6.003" x2="21.75" y2="6.003" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5px"></line></svg>
|
After Width: | Height: | Size: 587 B |
@ -17,6 +17,8 @@ interface TextFieldProps {
|
||||
onBlur?: (event: React.FocusEvent<HTMLInputElement>) => void;
|
||||
className?: string;
|
||||
maxLength?: number;
|
||||
containerClassName?: string;
|
||||
unstyled?: boolean;
|
||||
}
|
||||
|
||||
const TextField: React.FC<TextFieldProps> = ({
|
||||
@ -33,22 +35,23 @@ const TextField: React.FC<TextFieldProps> = ({
|
||||
onBlur,
|
||||
className = '',
|
||||
maxLength,
|
||||
containerClassName = '',
|
||||
unstyled = false,
|
||||
...props
|
||||
}) => {
|
||||
const id = useId();
|
||||
|
||||
return (
|
||||
<div className='flex flex-col'>
|
||||
{title && <Heading className={hideTitle ? 'sr-only' : ''} grey={value ? true : false} htmlFor={id} useLabelTag={true}>{title}</Heading>}
|
||||
<input
|
||||
ref={inputRef}
|
||||
className={clsx(
|
||||
const textFieldClasses = !unstyled && clsx(
|
||||
'h-10 border-b py-2',
|
||||
clearBg ? 'bg-transparent' : 'bg-grey-75 px-[10px]',
|
||||
error ? `border-red` : `border-grey-500 hover:border-grey-700 focus:border-black`,
|
||||
(title && !hideTitle && !clearBg) && `mt-2`,
|
||||
className
|
||||
)}
|
||||
);
|
||||
|
||||
const field = <input
|
||||
ref={inputRef}
|
||||
className={textFieldClasses || className}
|
||||
id={id}
|
||||
maxLength={maxLength}
|
||||
placeholder={placeholder}
|
||||
@ -56,10 +59,19 @@ const TextField: React.FC<TextFieldProps> = ({
|
||||
value={value}
|
||||
onBlur={onBlur}
|
||||
onChange={onChange}
|
||||
{...props} />
|
||||
{...props} />;
|
||||
|
||||
if (title || hint) {
|
||||
return (
|
||||
<div className={`flex flex-col ${containerClassName}`}>
|
||||
{title && <Heading className={hideTitle ? 'sr-only' : ''} grey={value ? true : false} htmlFor={id} useLabelTag={true}>{title}</Heading>}
|
||||
{field}
|
||||
{hint && <Hint color={error ? 'red' : ''}>{hint}</Hint>}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return field;
|
||||
}
|
||||
};
|
||||
|
||||
export default TextField;
|
||||
|
@ -6,11 +6,11 @@ import TabView, {Tab} from '../../../admin-x-ds/global/TabView';
|
||||
import ThemePreview from './designAndBranding/ThemePreview';
|
||||
import ThemeSettings from './designAndBranding/ThemeSettings';
|
||||
import useForm from '../../../hooks/useForm';
|
||||
import {CustomThemeSetting, Post, Setting, SettingValue, SiteData} from '../../../types/api';
|
||||
import {CustomThemeSetting, Post, Setting, SettingValue} from '../../../types/api';
|
||||
import {PreviewModalContent} from '../../../admin-x-ds/global/modal/PreviewModal';
|
||||
import {ServicesContext} from '../../providers/ServiceProvider';
|
||||
import {SettingsContext} from '../../providers/SettingsProvider';
|
||||
import {getSettingValues} from '../../../utils/helpers';
|
||||
import {getHomepageUrl, getSettingValues} from '../../../utils/helpers';
|
||||
|
||||
const Sidebar: React.FC<{
|
||||
brandSettings: BrandSettingValues
|
||||
@ -47,13 +47,6 @@ const Sidebar: React.FC<{
|
||||
);
|
||||
};
|
||||
|
||||
function getHomepageUrl(siteData: SiteData): string {
|
||||
const url = new URL(siteData.url);
|
||||
const subdir = url.pathname.endsWith('/') ? url.pathname : `${url.pathname}/`;
|
||||
|
||||
return `${url.origin}${subdir}`;
|
||||
}
|
||||
|
||||
const DesignModal: React.FC = () => {
|
||||
const modal = useModal();
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import Heading from '../../../admin-x-ds/global/Heading';
|
||||
import Modal from '../../../admin-x-ds/global/modal/Modal';
|
||||
import NavigationEditor from './navigation/NavigationEditor';
|
||||
import NiceModal, {useModal} from '@ebay/nice-modal-react';
|
||||
import TabView from '../../../admin-x-ds/global/TabView';
|
||||
import useSettingGroup from '../../../hooks/useSettingGroup';
|
||||
import {NavigationItem} from './navigation/NavigationItemEditor';
|
||||
import {getSettingValues} from '../../../utils/helpers';
|
||||
@ -26,7 +26,8 @@ const NavigationModal = NiceModal.create(() => {
|
||||
<Modal
|
||||
buttonsDisabled={saveState === 'saving'}
|
||||
scrolling={true}
|
||||
size='full'
|
||||
size='lg'
|
||||
stickyFooter={true}
|
||||
title='Navigation'
|
||||
onCancel={() => modal.remove()}
|
||||
onOk={async () => {
|
||||
@ -34,10 +35,22 @@ const NavigationModal = NiceModal.create(() => {
|
||||
modal.remove();
|
||||
}}
|
||||
>
|
||||
<Heading className="mt-6" level={6}>Primary navigation</Heading>
|
||||
<NavigationEditor baseUrl={siteData!.url} items={navigation} setItems={items => updateSetting('navigation', JSON.stringify(items))} />
|
||||
<Heading level={6}>Secondary navigation</Heading>
|
||||
<NavigationEditor baseUrl={siteData!.url} items={secondaryNavigation} setItems={items => updateSetting('secondary_navigation', JSON.stringify(items))} />
|
||||
<div className='-mb-8 mt-6'>
|
||||
<TabView
|
||||
tabs={[
|
||||
{
|
||||
id: 'primary-nav',
|
||||
title: 'Primary navigation',
|
||||
contents: <NavigationEditor baseUrl={siteData!.url} items={navigation} setItems={items => updateSetting('navigation', JSON.stringify(items))} />
|
||||
},
|
||||
{
|
||||
id: 'secondary-nav',
|
||||
title: 'Secondary navigation',
|
||||
contents: <NavigationEditor baseUrl={siteData!.url} items={secondaryNavigation} setItems={items => updateSetting('secondary_navigation', JSON.stringify(items))} />
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
});
|
||||
|
@ -31,7 +31,6 @@ const SortableItem: React.FC<{
|
||||
<NavigationItemEditor
|
||||
ref={setNodeRef}
|
||||
baseUrl={baseUrl}
|
||||
className="flex gap-4 p-2"
|
||||
dragHandleProps={{...attributes, ...listeners}}
|
||||
item={item}
|
||||
style={style}
|
||||
@ -49,7 +48,7 @@ const NavigationEditor: React.FC<{
|
||||
// Copy items to a local state we can reorder without changing IDs, so that drag and drop animations work nicely
|
||||
const [draggableItems, setLocalDraggableItems] = useState<DraggableItem[]>(items.map((item, index) => ({...item, id: index.toString()})));
|
||||
|
||||
const [newItem, setNewItem] = useState<NavigationItem>({label: '', url: ''});
|
||||
const [newItem, setNewItem] = useState<NavigationItem>({label: '', url: baseUrl});
|
||||
const [draggingId, setDraggingId] = useState<string | null>(null);
|
||||
|
||||
const setDraggableItems = (newItems: DraggableItem[]) => {
|
||||
@ -62,8 +61,10 @@ const NavigationEditor: React.FC<{
|
||||
};
|
||||
|
||||
const addItem = () => {
|
||||
if (newItem.label && newItem.url) {
|
||||
setDraggableItems(draggableItems.concat({...newItem, id: draggableItems.length.toString()}));
|
||||
setNewItem({label: '', url: ''});
|
||||
setNewItem({label: '', url: baseUrl});
|
||||
}
|
||||
};
|
||||
|
||||
const removeItem = (id: string) => {
|
||||
@ -79,7 +80,7 @@ const NavigationEditor: React.FC<{
|
||||
setDraggingId(null);
|
||||
};
|
||||
|
||||
return <div className="mb-6 mt-4 rounded border border-grey-100 px-4 pb-4 pt-2">
|
||||
return <div className="w-full">
|
||||
<DndContext
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={event => moveItem(event.active.id as string, event.over?.id as string)}
|
||||
@ -105,10 +106,11 @@ const NavigationEditor: React.FC<{
|
||||
</DragOverlay>
|
||||
</DndContext>
|
||||
|
||||
<div className="flex gap-4 p-2 pl-14">
|
||||
<TextField value={newItem.label} onChange={e => setNewItem({...newItem, label: e.target.value})} />
|
||||
<TextField value={newItem.url} onChange={e => setNewItem({...newItem, url: e.target.value})} />
|
||||
<Button icon="user-add" onClick={addItem} />
|
||||
<div className="flex items-center gap-3 p-2">
|
||||
<span className='inline-block w-8'></span>
|
||||
<TextField className='grow' placeholder='New item label' value={newItem.label} onChange={e => setNewItem({...newItem, label: e.target.value})} />
|
||||
<TextField className='ml-2 grow' value={newItem.url} onChange={e => setNewItem({...newItem, url: e.target.value})} />
|
||||
<Button icon="add" iconColorClass='text-green stroke-2' size='sm' onClick={addItem} />
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
|
@ -1,4 +1,5 @@
|
||||
import Button from '../../../../admin-x-ds/global/Button';
|
||||
import Icon from '../../../../admin-x-ds/global/Icon';
|
||||
import React, {forwardRef, useEffect, useState} from 'react';
|
||||
import TextField from '../../../../admin-x-ds/global/form/TextField';
|
||||
import clsx from 'clsx';
|
||||
@ -101,14 +102,28 @@ const NavigationItemEditor = forwardRef<HTMLDivElement, NavigationItemEditorProp
|
||||
updateItem?.({url: save});
|
||||
};
|
||||
|
||||
const containerClasses = clsx(
|
||||
'flex w-full items-center gap-3 rounded border-b border-grey-200 bg-white py-4 hover:bg-grey-100',
|
||||
isDragging && 'opacity-75'
|
||||
);
|
||||
|
||||
const dragHandleClasses = clsx(
|
||||
'ml-2 cursor-grab pl-2',
|
||||
isDragging ? 'cursor-grabbing' : 'cursor-grap'
|
||||
);
|
||||
|
||||
const textFieldClasses = clsx(
|
||||
'grow border-b border-transparent bg-white px-2 py-0.5 hover:border-grey-300 focus:border-grey-600'
|
||||
);
|
||||
|
||||
return (
|
||||
<div ref={ref} className={clsx('flex gap-4 bg-white p-2', isDragging && 'opacity-75')} {...props}>
|
||||
<button className="pl-3 pr-2" type="button" {...dragHandleProps}>
|
||||
<span className="inline-block h-3 w-3 rounded-full bg-black" />
|
||||
<div ref={ref} className={containerClasses} {...props}>
|
||||
<button className={dragHandleClasses} type='button' {...dragHandleProps}>
|
||||
<Icon colorClass='text-grey-500' name='hamburger' size='sm' />
|
||||
</button>
|
||||
<TextField value={item.label} onChange={e => updateItem?.({label: e.target.value})} />
|
||||
<TextField value={urlValue} onBlur={updateUrl} onChange={e => setUrlValue(e.target.value)} />
|
||||
<Button icon="trash" onClick={onDelete} />
|
||||
<TextField className={textFieldClasses} value={item.label} unstyled onChange={e => updateItem?.({label: e.target.value})} />
|
||||
<TextField className={textFieldClasses} value={urlValue} unstyled onBlur={updateUrl} onChange={e => setUrlValue(e.target.value)} />
|
||||
<Button className='mr-2' icon="trash" size='sm' onClick={onDelete} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {Setting, SettingValue, User} from '../types/api';
|
||||
import {Setting, SettingValue, SiteData, User} from '../types/api';
|
||||
|
||||
export interface IGhostPaths {
|
||||
adminRoot: string;
|
||||
@ -92,3 +92,10 @@ export function downloadFile(url: string) {
|
||||
|
||||
iframe.setAttribute('src', url);
|
||||
}
|
||||
|
||||
export function getHomepageUrl(siteData: SiteData): string {
|
||||
const url = new URL(siteData.url);
|
||||
const subdir = url.pathname.endsWith('/') ? url.pathname : `${url.pathname}/`;
|
||||
|
||||
return `${url.origin}${subdir}`;
|
||||
}
|
Loading…
Reference in New Issue
Block a user