new web ract
This commit is contained in:
@@ -15,13 +15,20 @@ export function Header({ saveStatus, articleStatus, onSave, onPublish }) {
|
||||
return (
|
||||
<header className="flex items-center justify-between px-4 h-14 bg-slate-900 border-b border-slate-800 flex-shrink-0 z-20">
|
||||
<div className="flex items-center gap-3">
|
||||
<a
|
||||
href="javascript:history.back()"
|
||||
<button
|
||||
onClick={() => {
|
||||
// Если в iframe — говорим родителю закрыть редактор
|
||||
if (window.parent !== window) {
|
||||
window.parent.postMessage({ type: 'editor-close' }, '*')
|
||||
} else {
|
||||
window.close()
|
||||
}
|
||||
}}
|
||||
className="flex items-center gap-1.5 text-slate-400 hover:text-white transition-colors text-sm"
|
||||
>
|
||||
<ArrowLeft size={16} />
|
||||
<span>Назад</span>
|
||||
</a>
|
||||
<span>Закрыть</span>
|
||||
</button>
|
||||
<div className="w-px h-5 bg-slate-700" />
|
||||
<span className="text-slate-200 font-semibold text-sm tracking-tight">News CMS</span>
|
||||
</div>
|
||||
|
||||
@@ -35,38 +35,60 @@ function Sep() {
|
||||
return <div className="w-px h-5 bg-slate-200 mx-0.5 flex-shrink-0" />
|
||||
}
|
||||
|
||||
/* ── Hook: позиция дропдауна через fixed (вырывается из overflow-контейнеров) ── */
|
||||
|
||||
function useFixedDropdown() {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [pos, setPos] = useState({ top: 0, left: 0 })
|
||||
const btnRef = useRef(null)
|
||||
|
||||
const toggle = e => {
|
||||
e.preventDefault()
|
||||
if (!open && btnRef.current) {
|
||||
const r = btnRef.current.getBoundingClientRect()
|
||||
setPos({ top: r.bottom + 4, left: r.left })
|
||||
}
|
||||
setOpen(o => !o)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
const close = e => {
|
||||
if (!btnRef.current?.contains(e.target)) setOpen(false)
|
||||
}
|
||||
document.addEventListener('mousedown', close)
|
||||
return () => document.removeEventListener('mousedown', close)
|
||||
}, [open])
|
||||
|
||||
return { open, setOpen, pos, btnRef, toggle }
|
||||
}
|
||||
|
||||
/* ── Font family dropdown ── */
|
||||
|
||||
function FontDropdown({ editor, fonts }) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const ref = useRef(null)
|
||||
const { open, setOpen, pos, btnRef, toggle } = useFixedDropdown()
|
||||
const current = editor.getAttributes('textStyle').fontFamily || 'Шрифт'
|
||||
|
||||
useEffect(() => {
|
||||
const h = e => { if (!ref.current?.contains(e.target)) setOpen(false) }
|
||||
document.addEventListener('mousedown', h)
|
||||
return () => document.removeEventListener('mousedown', h)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="relative flex-shrink-0" ref={ref}>
|
||||
<div className="flex-shrink-0">
|
||||
<button
|
||||
ref={btnRef}
|
||||
type="button"
|
||||
onMouseDown={e => { e.preventDefault(); setOpen(o => !o) }}
|
||||
onMouseDown={toggle}
|
||||
className="flex items-center gap-1 px-2 h-[30px] rounded-md text-xs text-slate-600 hover:bg-slate-100 border border-transparent hover:border-slate-200 transition-colors max-w-[130px] cursor-pointer"
|
||||
title="Шрифт"
|
||||
>
|
||||
<span
|
||||
className="truncate leading-none"
|
||||
style={{ fontFamily: current !== 'Шрифт' ? current : undefined }}
|
||||
>
|
||||
<span className="truncate leading-none" style={{ fontFamily: current !== 'Шрифт' ? current : undefined }}>
|
||||
{current}
|
||||
</span>
|
||||
<ChevronDown size={11} className="flex-shrink-0 opacity-60" />
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
<div className="absolute top-[34px] left-0 z-50 bg-white border border-slate-200 rounded-xl shadow-2xl w-52 max-h-72 overflow-y-auto py-1.5">
|
||||
<div
|
||||
style={{ position: 'fixed', top: pos.top, left: pos.left, zIndex: 9999, width: 208 }}
|
||||
className="bg-white border border-slate-200 rounded-xl shadow-2xl max-h-72 overflow-y-auto py-1.5"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="w-full px-3 py-1.5 text-left text-xs text-slate-400 hover:bg-slate-50 transition-colors"
|
||||
@@ -95,21 +117,15 @@ function FontDropdown({ editor, fonts }) {
|
||||
/* ── Font size dropdown ── */
|
||||
|
||||
function SizeDropdown({ editor }) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const ref = useRef(null)
|
||||
const { open, setOpen, pos, btnRef, toggle } = useFixedDropdown()
|
||||
const current = editor.getAttributes('textStyle').fontSize?.replace('px', '') || '—'
|
||||
|
||||
useEffect(() => {
|
||||
const h = e => { if (!ref.current?.contains(e.target)) setOpen(false) }
|
||||
document.addEventListener('mousedown', h)
|
||||
return () => document.removeEventListener('mousedown', h)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="relative flex-shrink-0" ref={ref}>
|
||||
<div className="flex-shrink-0">
|
||||
<button
|
||||
ref={btnRef}
|
||||
type="button"
|
||||
onMouseDown={e => { e.preventDefault(); setOpen(o => !o) }}
|
||||
onMouseDown={toggle}
|
||||
className="flex items-center gap-1 px-1.5 h-[30px] w-14 rounded-md text-xs text-slate-600 hover:bg-slate-100 border border-transparent hover:border-slate-200 transition-colors cursor-pointer"
|
||||
title="Размер шрифта"
|
||||
>
|
||||
@@ -118,7 +134,10 @@ function SizeDropdown({ editor }) {
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
<div className="absolute top-[34px] left-0 z-50 bg-white border border-slate-200 rounded-xl shadow-2xl w-20 max-h-56 overflow-y-auto py-1.5">
|
||||
<div
|
||||
style={{ position: 'fixed', top: pos.top, left: pos.left, zIndex: 9999, width: 80 }}
|
||||
className="bg-white border border-slate-200 rounded-xl shadow-2xl max-h-56 overflow-y-auto py-1.5"
|
||||
>
|
||||
{FONT_SIZES.map(s => (
|
||||
<button
|
||||
key={s}
|
||||
@@ -138,14 +157,7 @@ function SizeDropdown({ editor }) {
|
||||
/* ── Table menu ── */
|
||||
|
||||
function TableMenu({ editor }) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const ref = useRef(null)
|
||||
|
||||
useEffect(() => {
|
||||
const h = e => { if (!ref.current?.contains(e.target)) setOpen(false) }
|
||||
document.addEventListener('mousedown', h)
|
||||
return () => document.removeEventListener('mousedown', h)
|
||||
}, [])
|
||||
const { open, setOpen, pos, btnRef, toggle } = useFixedDropdown()
|
||||
|
||||
const items = [
|
||||
{ label: 'Вставить таблицу 3×3', fn: () => editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run() },
|
||||
@@ -162,17 +174,22 @@ function TableMenu({ editor }) {
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="relative flex-shrink-0" ref={ref}>
|
||||
<div className="flex-shrink-0">
|
||||
<Btn
|
||||
onClick={() => setOpen(o => !o)}
|
||||
onClick={e => toggle({ preventDefault: () => {}, ...e })}
|
||||
active={editor.isActive('table')}
|
||||
title="Таблица"
|
||||
>
|
||||
<Table2 size={14} />
|
||||
</Btn>
|
||||
{/* Используем ref на обёртке для Btn */}
|
||||
<span ref={btnRef} style={{ display: 'none' }} />
|
||||
|
||||
{open && (
|
||||
<div className="absolute top-[34px] left-0 z-50 bg-white border border-slate-200 rounded-xl shadow-2xl w-52 py-1.5">
|
||||
<div
|
||||
style={{ position: 'fixed', top: pos.top, left: pos.left, zIndex: 9999, width: 208 }}
|
||||
className="bg-white border border-slate-200 rounded-xl shadow-2xl py-1.5"
|
||||
>
|
||||
{items.map((item, i) =>
|
||||
item === null ? (
|
||||
<div key={i} className="my-1 border-t border-slate-100" />
|
||||
@@ -181,9 +198,7 @@ function TableMenu({ editor }) {
|
||||
key={item.label}
|
||||
type="button"
|
||||
className={`w-full px-3 py-1.5 text-left text-xs transition-colors
|
||||
${item.danger
|
||||
? 'text-red-500 hover:bg-red-50'
|
||||
: 'text-slate-700 hover:bg-slate-50'}`}
|
||||
${item.danger ? 'text-red-500 hover:bg-red-50' : 'text-slate-700 hover:bg-slate-50'}`}
|
||||
onMouseDown={e => { e.preventDefault(); item.fn(); setOpen(false) }}
|
||||
>
|
||||
{item.label}
|
||||
@@ -210,7 +225,7 @@ export function Toolbar({ editor, onInsertImage, onInsertVideo, fonts }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="sticky top-0 z-10 flex items-center gap-0.5 px-3 py-1.5 bg-white border-b border-slate-200 shadow-sm overflow-x-auto flex-wrap min-h-[46px]">
|
||||
<div className="flex items-center gap-0.5 px-3 py-1.5 bg-white border-b border-slate-200 shadow-sm overflow-x-auto flex-shrink-0 min-h-[46px]" style={{ scrollbarWidth: 'none' }}>
|
||||
{/* History */}
|
||||
<Btn onClick={() => editor.chain().focus().undo().run()} disabled={!editor.can().undo()} title="Отменить (Ctrl+Z)">
|
||||
<Undo2 size={14} />
|
||||
|
||||
Reference in New Issue
Block a user