fix(creator-studio): 固定公众号式创作工具栏
- 顶部改为 fixed 创作工具栏:左侧返回/品牌/模式,中间文章标题与保存状态, 右侧撤销/重做、保存草稿、预览、发布 - 新增工作台级撤销/重做快照栈,连续编辑合并记录;支持 Ctrl/Cmd+Z / Shift+Z, 不覆盖 RichText 输入框自身的编辑历史 - 移动端收敛工具栏布局,保留撤销/重做、预览、发布 - 先保存草稿再请求审核/直接发布;请求使用服务端更新后的文章与版本 ID - 固定工具栏以独立 chrome 层布局,正文避开顶部工具栏高度 - 补充固定工具栏、历史操作与键盘快捷键契约 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
f5e9420266
commit
9e8913ea0a
9 files changed
+518
-50
No files matched your search
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* 工作台级撤销/重做历史。
|
||||
*
|
||||
* BlockEditorProvider 独立运行时不带 core/editor 的 post history store,
|
||||
* 因此为标题、摘要、区块树保存快照。输入以 450ms 合并成一条历史记录,
|
||||
* 避免每个字符都占用一步。
|
||||
*
|
||||
* @package Jio
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from '@wordpress/element';
|
||||
|
||||
type Snapshot<T> = { value: T; signature: string };
|
||||
type HistoryState<T> = { past: Snapshot<T>[]; future: Snapshot<T>[]; present: Snapshot<T>; pending: T | null };
|
||||
|
||||
type Options<T> = {
|
||||
initial: T;
|
||||
onRestore: (value: T) => void;
|
||||
limit?: number;
|
||||
mergeDelay?: number;
|
||||
};
|
||||
|
||||
function copy<T>(value: T): T {
|
||||
return JSON.parse(JSON.stringify(value)) as T;
|
||||
}
|
||||
|
||||
function snapshot<T>(value: T): Snapshot<T> {
|
||||
const cloned = copy(value);
|
||||
return { value: cloned, signature: JSON.stringify(cloned) };
|
||||
}
|
||||
|
||||
export function useEditorHistory<T>({ initial, onRestore, limit = 80, mergeDelay = 450 }: Options<T>) {
|
||||
const state = useRef<HistoryState<T>>({
|
||||
past: [],
|
||||
future: [],
|
||||
present: snapshot(initial),
|
||||
pending: null,
|
||||
});
|
||||
const timer = useRef<number | null>(null);
|
||||
const restoreRef = useRef(onRestore);
|
||||
restoreRef.current = onRestore;
|
||||
const [availability, setAvailability] = useState({ canUndo: false, canRedo: false });
|
||||
|
||||
const updateAvailability = useCallback(() => {
|
||||
setAvailability({
|
||||
canUndo: state.current.past.length > 0 || state.current.pending !== null,
|
||||
canRedo: state.current.future.length > 0,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const flush = useCallback(() => {
|
||||
if (timer.current !== null) {
|
||||
window.clearTimeout(timer.current);
|
||||
timer.current = null;
|
||||
}
|
||||
const current = state.current;
|
||||
if (current.pending === null) {
|
||||
return;
|
||||
}
|
||||
const next = snapshot(current.pending);
|
||||
current.pending = null;
|
||||
if (next.signature === current.present.signature) {
|
||||
updateAvailability();
|
||||
return;
|
||||
}
|
||||
current.past = [...current.past, current.present].slice(-limit);
|
||||
current.present = next;
|
||||
current.future = [];
|
||||
updateAvailability();
|
||||
}, [limit, updateAvailability]);
|
||||
|
||||
const record = useCallback(
|
||||
(value: T) => {
|
||||
state.current.pending = copy(value);
|
||||
if (timer.current !== null) {
|
||||
window.clearTimeout(timer.current);
|
||||
}
|
||||
timer.current = window.setTimeout(flush, mergeDelay);
|
||||
updateAvailability();
|
||||
},
|
||||
[flush, mergeDelay, updateAvailability]
|
||||
);
|
||||
|
||||
const undo = useCallback(() => {
|
||||
flush();
|
||||
const current = state.current;
|
||||
if (!current.past.length) {
|
||||
return false;
|
||||
}
|
||||
const previous = current.past[current.past.length - 1];
|
||||
current.past = current.past.slice(0, -1);
|
||||
current.future = [current.present, ...current.future].slice(0, limit);
|
||||
current.present = previous;
|
||||
restoreRef.current(copy(previous.value));
|
||||
updateAvailability();
|
||||
return true;
|
||||
}, [flush, limit, updateAvailability]);
|
||||
|
||||
const redo = useCallback(() => {
|
||||
flush();
|
||||
const current = state.current;
|
||||
if (!current.future.length) {
|
||||
return false;
|
||||
}
|
||||
const next = current.future[0];
|
||||
current.future = current.future.slice(1);
|
||||
current.past = [...current.past, current.present].slice(-limit);
|
||||
current.present = next;
|
||||
restoreRef.current(copy(next.value));
|
||||
updateAvailability();
|
||||
return true;
|
||||
}, [flush, limit, updateAvailability]);
|
||||
|
||||
useEffect(() => () => {
|
||||
if (timer.current !== null) {
|
||||
window.clearTimeout(timer.current);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return { ...availability, record, undo, redo, flush };
|
||||
}
|
||||
@@ -27,6 +27,7 @@ import { InsertPanel, type InsertItem } from './panels/insert-panel';
|
||||
import { buildInsertGroups } from './utils/insert-items';
|
||||
import { SaveState, type SaveStateValue } from './components/save-state';
|
||||
import { useAutosave } from './hooks/use-autosave';
|
||||
import { useEditorHistory } from './hooks/use-editor-history';
|
||||
import { useLocalRecovery } from './hooks/use-local-recovery';
|
||||
import { normalizePayForRequest } from './utils/pay';
|
||||
import './styles/studio.scss';
|
||||
@@ -109,12 +110,55 @@ function initialDraft(): Draft {
|
||||
function Studio(): JSX.Element {
|
||||
const [draft, setDraft] = useState<Draft>(initialDraft);
|
||||
const [blocks, setBlocks] = useState(() => parse(draft.content || ''));
|
||||
const [saveState, setSaveState] = useState<SaveStateValue>({ state: 'ready', at: '' });
|
||||
const blocksRef = useRef(blocks);
|
||||
blocksRef.current = blocks;
|
||||
const draftRef = useRef(draft);
|
||||
draftRef.current = draft;
|
||||
const [saveState, setSaveState] = useState<SaveStateValue>(() => ({
|
||||
state: bootstrap.sourceId || bootstrap.versionId || bootstrap.content ? 'saved' : 'ready',
|
||||
at: '',
|
||||
}));
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
const [insertOpen, setInsertOpen] = useState(false);
|
||||
const [notice, setNotice] = useState<{ tone: 'ok' | 'error'; text: string } | null>(null);
|
||||
const [recovery, setRecovery] = useState<null | { savedAt: string }>(null);
|
||||
const dirtyRef = useRef(false);
|
||||
const restoreBlockSignatureRef = useRef('');
|
||||
|
||||
const restoreHistory = useCallback((value: { blocks: any[]; title: string; excerpt: string }) => {
|
||||
dirtyRef.current = true;
|
||||
blocksRef.current = value.blocks;
|
||||
restoreBlockSignatureRef.current = serialize(value.blocks);
|
||||
setBlocks(value.blocks);
|
||||
const next = { ...draftRef.current, title: value.title, excerpt: value.excerpt };
|
||||
draftRef.current = next;
|
||||
setDraft(next);
|
||||
}, []);
|
||||
const history = useEditorHistory({
|
||||
initial: { blocks, title: draft.title, excerpt: draft.excerpt },
|
||||
onRestore: restoreHistory,
|
||||
});
|
||||
const historyRef = useRef(history);
|
||||
historyRef.current = history;
|
||||
|
||||
const undo = useCallback(() => historyRef.current.undo(), []);
|
||||
const redo = useCallback(() => historyRef.current.redo(), []);
|
||||
|
||||
useEffect(() => {
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
if (!(event.metaKey || event.ctrlKey) || event.key.toLowerCase() !== 'z') {
|
||||
return;
|
||||
}
|
||||
const target = event.target as HTMLElement | null;
|
||||
if (target?.closest('input, textarea, [contenteditable="true"]')) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.shiftKey ? redo() : undo();
|
||||
};
|
||||
window.addEventListener('keydown', onKeyDown);
|
||||
return () => window.removeEventListener('keydown', onKeyDown);
|
||||
}, [redo, undo]);
|
||||
|
||||
const capabilities = bootstrap.capabilities || {};
|
||||
const limits: Limits = {
|
||||
@@ -130,11 +174,32 @@ function Studio(): JSX.Element {
|
||||
|
||||
const patchDraft = useCallback((patch: Partial<Draft>) => {
|
||||
dirtyRef.current = true;
|
||||
setDraft((current) => ({ ...current, ...patch }));
|
||||
const next = { ...draftRef.current, ...patch };
|
||||
draftRef.current = next;
|
||||
if ('title' in patch || 'excerpt' in patch) {
|
||||
historyRef.current.record({
|
||||
blocks: blocksRef.current,
|
||||
title: next.title,
|
||||
excerpt: next.excerpt,
|
||||
});
|
||||
}
|
||||
setDraft(next);
|
||||
}, []);
|
||||
|
||||
const onChangeBlocks = useCallback((next: any[]) => {
|
||||
dirtyRef.current = true;
|
||||
blocksRef.current = next;
|
||||
const signature = serialize(next);
|
||||
if (restoreBlockSignatureRef.current !== '' && signature === restoreBlockSignatureRef.current) {
|
||||
restoreBlockSignatureRef.current = '';
|
||||
setBlocks(next);
|
||||
return;
|
||||
}
|
||||
historyRef.current.record({
|
||||
blocks: next,
|
||||
title: draftRef.current.title,
|
||||
excerpt: draftRef.current.excerpt,
|
||||
});
|
||||
setBlocks(next);
|
||||
}, []);
|
||||
|
||||
@@ -183,7 +248,9 @@ function Studio(): JSX.Element {
|
||||
const versionId = Number(response.version_id || draft.versionId || 0);
|
||||
const rev = Number(response.rev || response.versionRev || draft.rev || 0);
|
||||
dirtyRef.current = false;
|
||||
setDraft((current) => ({ ...current, postId: savedId, versionId, rev }));
|
||||
const updated = { ...draftRef.current, postId: savedId, versionId, rev };
|
||||
draftRef.current = updated;
|
||||
setDraft(updated);
|
||||
setSaveState({ state: 'saved', at: new Date().toLocaleTimeString() });
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
@@ -220,7 +287,13 @@ function Studio(): JSX.Element {
|
||||
}
|
||||
const response = await apiPost(
|
||||
bootstrap.creator.submitUrl,
|
||||
buildPayload({ publish_now: publishNow, captcha_proof: captchaProof }),
|
||||
buildPayload({
|
||||
post_id: draftRef.current.postId,
|
||||
version_id: draftRef.current.versionId,
|
||||
rev: draftRef.current.rev,
|
||||
publish_now: publishNow,
|
||||
captcha_proof: captchaProof,
|
||||
}),
|
||||
nonce
|
||||
);
|
||||
dirtyRef.current = false;
|
||||
@@ -249,7 +322,7 @@ function Studio(): JSX.Element {
|
||||
try {
|
||||
const response = await apiPost(
|
||||
bootstrap.creator.previewUrl,
|
||||
{ post_id: draft.postId, version_id: draft.versionId },
|
||||
{ post_id: draftRef.current.postId, version_id: draftRef.current.versionId },
|
||||
nonce
|
||||
);
|
||||
if (response.url) {
|
||||
@@ -315,12 +388,10 @@ function Studio(): JSX.Element {
|
||||
if (raw) {
|
||||
const parsed = JSON.parse(raw);
|
||||
if (typeof parsed.title === 'string') {
|
||||
dirtyRef.current = true;
|
||||
setDraft((current) => ({ ...current, title: parsed.title }));
|
||||
patchDraft({ title: parsed.title });
|
||||
}
|
||||
if (typeof parsed.content === 'string') {
|
||||
dirtyRef.current = true;
|
||||
setBlocks(parse(parsed.content));
|
||||
onChangeBlocks(parse(parsed.content));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -340,28 +411,59 @@ function Studio(): JSX.Element {
|
||||
(item: InsertItem) => {
|
||||
setInsertOpen(false);
|
||||
dirtyRef.current = true;
|
||||
setBlocks((current: any[]) => {
|
||||
// 空文档时直接替换初始的空白段落,避免留下一个空行。
|
||||
const next = [...current];
|
||||
if (next.length === 1 && next[0].name === 'core/paragraph' && !next[0].attributes?.content) {
|
||||
return [createBlock(item.name, item.attributes || {})];
|
||||
}
|
||||
return [...next, createBlock(item.name, item.attributes || {})];
|
||||
const current = [...blocksRef.current];
|
||||
let next: any[];
|
||||
// 空文档时直接替换初始的空白段落,避免留下一个空行。
|
||||
if (current.length === 1 && current[0].name === 'core/paragraph' && !current[0].attributes?.content) {
|
||||
next = [createBlock(item.name, item.attributes || {})];
|
||||
} else {
|
||||
next = [...current, createBlock(item.name, item.attributes || {})];
|
||||
}
|
||||
blocksRef.current = next;
|
||||
historyRef.current.record({
|
||||
blocks: next,
|
||||
title: draftRef.current.title,
|
||||
excerpt: draftRef.current.excerpt,
|
||||
});
|
||||
setBlocks(next);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="jio-creator" data-status={bootstrap.status || ''} data-blocks={selectedBlockCount}> <header className="jio-creator__bar">
|
||||
<a className="jio-creator__back" href={bootstrap.urls?.center || '/'}>
|
||||
<i className="fa fas fa-angle-left" aria-hidden="true" />
|
||||
<span>{__('返回', 'jio')}</span>
|
||||
</a>
|
||||
<div className="jio-creator__brand">Jio Creator Studio</div>
|
||||
<div className="jio-creator__actions">
|
||||
<div className="jio-creator" data-status={bootstrap.status || ''} data-blocks={selectedBlockCount}>
|
||||
<header className="jio-creator__bar">
|
||||
<div className="jio-creator__bar-left">
|
||||
<a className="jio-creator__back" href={bootstrap.urls?.center || '/'} aria-label={__('返回作者中心', 'jio')}>
|
||||
<i className="fa fas fa-angle-left" aria-hidden="true" />
|
||||
</a>
|
||||
<span className="jio-creator__bar-brand">Jio</span>
|
||||
<span className="jio-creator__bar-divider" aria-hidden="true" />
|
||||
<span className="jio-creator__bar-mode">
|
||||
{isPublishedEdit ? __('修改文章', 'jio') : __('写文章', 'jio')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="jio-creator__bar-center">
|
||||
<span className="jio-creator__doc-title">
|
||||
{draft.title.trim() || __('无标题文章', 'jio')}
|
||||
</span>
|
||||
<SaveState value={saveState} />
|
||||
<button type="button" className="jio-creator__ghost" onClick={() => void preview()}>
|
||||
</div>
|
||||
|
||||
<div className="jio-creator__bar-actions">
|
||||
<div className="jio-creator__history" role="group" aria-label={__('编辑历史', 'jio')}>
|
||||
<button type="button" className="jio-creator__icon-btn" onClick={() => history.undo()} disabled={!history.canUndo} aria-label={__('撤销', 'jio')} title={__('撤销', 'jio')}>
|
||||
<i className="fa fas fa-undo" aria-hidden="true" />
|
||||
</button>
|
||||
<button type="button" className="jio-creator__icon-btn" onClick={() => history.redo()} disabled={!history.canRedo} aria-label={__('重做', 'jio')} title={__('重做', 'jio')}>
|
||||
<i className="fa fas fa-redo" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" className="jio-creator__ghost jio-creator__save-draft" onClick={() => void save(false)}>
|
||||
{__('保存草稿', 'jio')}
|
||||
</button>
|
||||
<button type="button" className="jio-creator__ghost jio-creator__preview" onClick={() => void preview()}>
|
||||
{__('预览', 'jio')}
|
||||
</button>
|
||||
<button type="button" className="jio-creator__primary" onClick={() => setDrawerOpen(true)}>
|
||||
|
||||
@@ -21,6 +21,12 @@ $warn: #b4573f;
|
||||
background: $paper;
|
||||
}
|
||||
|
||||
/* 主题固定导航会给 body.nav-fixed 预留 88px;工作台隐藏了导航,不应保留空位。 */
|
||||
body.jio-page-submit.nav-fixed,
|
||||
body.jio-page-submit.has-static-nav {
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
|
||||
.jio-page-submit .header,
|
||||
.jio-page-submit .footer,
|
||||
.jio-page-submit .float-btn,
|
||||
@@ -50,6 +56,8 @@ $warn: #b4573f;
|
||||
|
||||
min-height: 100vh;
|
||||
min-height: 100dvh;
|
||||
/* 固定顶栏属于工作台 chrome,不随正文一起滚动。 */
|
||||
padding-top: var(--jc-bar);
|
||||
background: var(--jc-paper);
|
||||
color: var(--jc-ink);
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
@@ -68,43 +76,192 @@ $warn: #b4573f;
|
||||
}
|
||||
|
||||
.jio-creator__bar {
|
||||
position: sticky;
|
||||
z-index: 30;
|
||||
position: fixed;
|
||||
z-index: 300;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
height: var(--jc-bar);
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 0 clamp(16px, 4vw, 40px);
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
padding: 0 clamp(16px, 3vw, 34px);
|
||||
border-bottom: 1px solid var(--jc-line);
|
||||
background: rgba(250, 251, 250, 0.92);
|
||||
backdrop-filter: blur(10px);
|
||||
background: rgba(250, 251, 250, 0.97);
|
||||
backdrop-filter: blur(14px);
|
||||
box-shadow: 0 2px 8px rgba(27, 46, 39, 0.035);
|
||||
}
|
||||
|
||||
.jio-creator__bar-left,
|
||||
.jio-creator__bar-center,
|
||||
.jio-creator__bar-actions {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.jio-creator__bar-left {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.jio-creator__bar-center {
|
||||
flex: 1 1 auto;
|
||||
justify-content: center;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.jio-creator__doc-title {
|
||||
max-width: min(32vw, 360px);
|
||||
overflow: hidden;
|
||||
color: #65736d;
|
||||
font-size: 13px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.jio-creator__bar-actions {
|
||||
flex: 0 0 auto;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.jio-creator__back {
|
||||
display: inline-flex;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
justify-content: center;
|
||||
border-radius: 7px;
|
||||
color: var(--jc-ink-soft);
|
||||
text-decoration: none;
|
||||
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
background: #edf3f0;
|
||||
color: var(--jc-accent);
|
||||
outline: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.jio-creator__brand {
|
||||
margin-right: auto;
|
||||
color: var(--jc-ink-soft);
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.06em;
|
||||
.jio-creator__bar-brand {
|
||||
color: var(--jc-accent-strong);
|
||||
font-family: "Songti SC", "Noto Serif SC", "Source Han Serif SC", serif;
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.04em;
|
||||
}
|
||||
|
||||
.jio-creator__actions {
|
||||
display: flex;
|
||||
.jio-creator__bar-divider {
|
||||
width: 1px;
|
||||
height: 18px;
|
||||
background: var(--jc-line);
|
||||
}
|
||||
|
||||
.jio-creator__bar-mode {
|
||||
color: var(--jc-ink-soft);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.jio-creator__doc-title {
|
||||
max-width: min(32vw, 360px);
|
||||
overflow: hidden;
|
||||
color: #65736d;
|
||||
font-size: 13px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.jio-creator__history {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
gap: 2px;
|
||||
padding-right: 8px;
|
||||
border-right: 1px solid var(--jc-line);
|
||||
}
|
||||
|
||||
.jio-creator__icon-btn {
|
||||
display: inline-flex;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 0;
|
||||
border-radius: 7px;
|
||||
background: transparent;
|
||||
color: var(--jc-ink-soft);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover:not(:disabled),
|
||||
&:focus-visible:not(:disabled) {
|
||||
background: #edf3f0;
|
||||
color: var(--jc-accent-strong);
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
color: #c7d0cc;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
.jio-creator__save-draft {
|
||||
border-color: var(--jc-line);
|
||||
background: #fff;
|
||||
color: var(--jc-ink-soft);
|
||||
}
|
||||
|
||||
.jio-creator__save-draft:hover,
|
||||
.jio-creator__save-draft:focus-visible {
|
||||
border-color: #b7cdc3;
|
||||
background: #f7faf8;
|
||||
color: var(--jc-accent-strong);
|
||||
}
|
||||
|
||||
.jio-creator__icon-btn {
|
||||
display: inline-flex;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 0;
|
||||
border-radius: 7px;
|
||||
background: transparent;
|
||||
color: var(--jc-ink-soft);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover:not(:disabled),
|
||||
&:focus-visible:not(:disabled) {
|
||||
background: #edf3f0;
|
||||
color: var(--jc-accent-strong);
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
color: #c7d0cc;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
.jio-creator__history {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
padding-right: 8px;
|
||||
border-right: 1px solid var(--jc-line);
|
||||
}
|
||||
|
||||
.jio-creator__save-draft {
|
||||
border-color: var(--jc-line);
|
||||
background: #fff;
|
||||
color: var(--jc-ink-soft);
|
||||
}
|
||||
|
||||
.jio-creator__save-draft:hover,
|
||||
.jio-creator__save-draft:focus-visible {
|
||||
border-color: #b7cdc3;
|
||||
background: #f7faf8;
|
||||
color: var(--jc-accent-strong);
|
||||
}
|
||||
|
||||
.jio-creator__save {
|
||||
@@ -1136,16 +1293,61 @@ $warn: #b4573f;
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.jio-creator {
|
||||
--jc-bar: 54px;
|
||||
}
|
||||
|
||||
.jio-creator__brand {
|
||||
display: none;
|
||||
--jc-bar: 56px;
|
||||
}
|
||||
|
||||
.jio-creator__bar {
|
||||
gap: 10px;
|
||||
gap: 8px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.jio-creator__bar-left,
|
||||
.jio-creator__bar-center,
|
||||
.jio-creator__bar-actions {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.jio-creator__bar-brand {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.jio-creator__bar-mode,
|
||||
.jio-creator__bar-divider,
|
||||
.jio-creator__doc-title,
|
||||
.jio-creator__save-draft {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.jio-creator__bar-center {
|
||||
flex: 0 0 auto;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.jio-creator__bar-actions {
|
||||
flex: 1 1 auto;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.jio-creator__icon-btn {
|
||||
width: 28px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.jio-creator__history {
|
||||
gap: 0;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.jio-creator__preview {
|
||||
min-height: 32px;
|
||||
padding: 0 9px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.jio-creator__primary {
|
||||
min-height: 32px;
|
||||
padding: 0 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.jio-creator__save time {
|
||||
|
||||
Reference in new issue
Block a user