feat(creator-studio): two-row masthead with site logo and user avatar
顶栏改为知乎式两行结构:第一行左侧站点 LOGO、右侧当前用户头像, 第二行只保留正文工具条并居中,同时移除「Jio / 写文章」字样。 - bootstrap payload 新增 site/viewer 身份字段(复用 jio_get_site_logo_url 与 jio_get_avatar_url,前端不拼 URL、不做权限判断) - 拆分 draft 类型、TinyMCE 块运行时、顶栏组件,index.tsx 1065 → 735 行 - 清理死代码:未调用的 keepCaretInsideCode、永远无法打开的代码语言弹窗 (setCodeLanguage 只会收到 null,codeTargetRef 从未赋值)及其样式, 以及顶栏移除后失效的 bar-left/brand/mode/save-draft/ghost/primary 规则 - 契约测试、本地预览与截图脚本同步到新 DOM Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
5f31c58df2
commit
1970db0b56
17 files changed
+602
-580
No files matched your search
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* 固定顶栏里的正文工具条。
|
||||
* 顶栏第二行里的正文工具条。
|
||||
*
|
||||
* 每个按钮都是「上图标 + 下文字」,与公众号/知乎的单行工具栏一致;
|
||||
* 只渲染当前账号真正有权限使用的能力,全部动作由父组件转给 TinyMCE。
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* 创作页顶栏(第一行):站点 LOGO 在左,用户头像在右。
|
||||
*
|
||||
* 与知乎写作页一致:品牌与身份常驻顶部,正文工具条单独占第二行并居中,
|
||||
* 避免品牌文字挤占工具条的可视宽度。
|
||||
*
|
||||
* @package Jio
|
||||
*/
|
||||
|
||||
import { __ } from '@wordpress/i18n';
|
||||
|
||||
type Props = {
|
||||
centerUrl: string;
|
||||
site: { home?: string; name?: string; logo?: string };
|
||||
viewer: { avatar?: string };
|
||||
};
|
||||
|
||||
export function StudioMasthead({ centerUrl, site, viewer }: Props) {
|
||||
const brandName = String(site.name || '');
|
||||
const avatarUrl = String(viewer.avatar || '');
|
||||
const avatarHref = centerUrl || '/';
|
||||
|
||||
return (
|
||||
<header className="jio-creator__masthead">
|
||||
<div className="jio-creator__masthead-left">
|
||||
<a className="jio-creator__back" href={centerUrl || '/'} aria-label={__('返回作者中心', 'jio')}>
|
||||
<i className="fa fas fa-angle-left" aria-hidden="true" />
|
||||
</a>
|
||||
<a
|
||||
className="jio-creator__brand"
|
||||
href={String(site.home || '/')}
|
||||
aria-label={brandName || __('返回首页', 'jio')}
|
||||
>
|
||||
{site.logo ? (
|
||||
<img className="jio-creator__brand-logo" src={String(site.logo)} alt={brandName} />
|
||||
) : (
|
||||
<span className="jio-creator__brand-text">{brandName}</span>
|
||||
)}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<a className="jio-creator__me" href={avatarHref} aria-label={__('返回作者中心', 'jio')}>
|
||||
{avatarUrl ? (
|
||||
<img className="jio-creator__me-avatar" src={avatarUrl} alt="" width={32} height={32} />
|
||||
) : (
|
||||
<span className="jio-creator__me-avatar is-placeholder" aria-hidden="true">
|
||||
<i className="fa fas fa-user" />
|
||||
</span>
|
||||
)}
|
||||
</a>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* 创作工作台的草稿模型。
|
||||
*
|
||||
* 只描述前端持有的可编辑状态;权限、审核与付费规则始终以服务端 bootstrap 为准。
|
||||
*
|
||||
* @package Jio
|
||||
*/
|
||||
|
||||
export type Featured = { type: string; data: Record<string, any>; mediaId?: number };
|
||||
|
||||
export type PayState = {
|
||||
enabled: boolean;
|
||||
modo: 'cash' | 'points';
|
||||
price: string;
|
||||
points: number;
|
||||
vip1Price: string;
|
||||
vip2Price: string;
|
||||
payLimit: number;
|
||||
downloads: Array<Record<string, any>>;
|
||||
};
|
||||
|
||||
export type Limits = {
|
||||
titleMin?: number;
|
||||
titleMax?: number;
|
||||
titlePlaceholder?: string;
|
||||
priceMin?: number;
|
||||
priceMax?: number;
|
||||
pointsMin?: number;
|
||||
pointsMax?: number;
|
||||
};
|
||||
|
||||
export type Schedule = { enabled: boolean; at: string };
|
||||
|
||||
export type Draft = {
|
||||
postId: number;
|
||||
versionId: number;
|
||||
rev: number;
|
||||
title: string;
|
||||
content: string;
|
||||
schedule: Schedule;
|
||||
categories: number[];
|
||||
tags: string[];
|
||||
custom: Record<string, string[]>;
|
||||
featured: Featured;
|
||||
pay: PayState;
|
||||
};
|
||||
|
||||
export function initialDraft(bootstrap: Record<string, any>): Draft {
|
||||
const pay = bootstrap.pay || {};
|
||||
const selected = (bootstrap.categories && bootstrap.categories.selected) || [];
|
||||
const tags = (bootstrap.tags && bootstrap.tags.items) || [];
|
||||
return {
|
||||
postId: Number(bootstrap.sourceId || 0),
|
||||
versionId: Number(bootstrap.versionId || 0),
|
||||
rev: Number(bootstrap.versionRev || 0),
|
||||
title: String(bootstrap.title || ''),
|
||||
content: String(bootstrap.content || ''),
|
||||
schedule: { enabled: false, at: '' },
|
||||
categories: selected.map(Number),
|
||||
tags: tags.slice(),
|
||||
custom: bootstrap.custom || {},
|
||||
featured: bootstrap.featured || { type: '', data: {} },
|
||||
pay: {
|
||||
enabled: !!pay.enabled,
|
||||
modo: pay.payModo === 'points' ? 'points' : 'cash',
|
||||
price: String(pay.price || '0.00'),
|
||||
points: Number(pay.points || 0),
|
||||
vip1Price: String(pay.vip1Price || '0.00'),
|
||||
vip2Price: String(pay.vip2Price || '0.00'),
|
||||
payLimit: Number(pay.payLimit || 0),
|
||||
downloads: Array.isArray(pay.downloads) ? pay.downloads : [],
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
/**
|
||||
* TinyMCE 编辑器增强:块级删除、代码块语言角标、自适应高度。
|
||||
*
|
||||
* 这些行为都作用在编辑器 iframe 内部,与 React 状态无关,因此独立成模块。
|
||||
*
|
||||
* @package Jio
|
||||
*/
|
||||
|
||||
import { CODE_LANGUAGES } from './utils/block-markup';
|
||||
|
||||
/** 可整体删除的块级元素选择器。 */
|
||||
const BLOCK_SELECTOR = 'table,pre.enlighter-pre,hr,.quote_q,p';
|
||||
|
||||
/** 可整体删除的块级元素(排除普通段落)。 */
|
||||
const STRUCTURAL_SELECTOR = 'table,pre,hr,.quote_q,.jio-math--block,.jio-toc-placeholder';
|
||||
|
||||
/**
|
||||
* Backspace / Delete 删除相邻的块级元素。
|
||||
*
|
||||
* 表格、代码块、引用、分割线这类内容不是普通文本,TinyMCE 默认不会在
|
||||
* 空段落里按删除键时主动移除它们,导致「删不掉」。这里在光标处于空段落、
|
||||
* 且紧邻目标块时直接移除该块。
|
||||
*/
|
||||
export function removeAdjacentBlock(editor: any, event: any): void {
|
||||
const key = Number(event.keyCode);
|
||||
if (key !== 8 && key !== 46) {
|
||||
return;
|
||||
}
|
||||
const selection = editor.selection;
|
||||
if (!selection || !selection.isCollapsed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const body = editor.getBody();
|
||||
const block = selection.getNode();
|
||||
const isBlank = (node: any): boolean =>
|
||||
!!node && node.nodeType === 1 && (node.textContent || '').trim() === '' &&
|
||||
!editor.dom.getParent(node, STRUCTURAL_SELECTOR, body);
|
||||
const isBlockElement = (node: any): boolean =>
|
||||
!!node && node.nodeType === 1 && !!(node.matches?.(STRUCTURAL_SELECTOR));
|
||||
|
||||
// 光标可能落在空段落里,也可能落在块级元素的边界(TinyMCE 常见行为);
|
||||
// 两种情况下都优先删除紧邻的块,而不是让按键悄悄失效。
|
||||
const blankContainer = isBlank(block) ? block : null;
|
||||
const direct = blankContainer
|
||||
? (key === 8 ? blankContainer.previousElementSibling : blankContainer.nextElementSibling)
|
||||
: null;
|
||||
|
||||
if (isBlockElement(direct)) {
|
||||
event.preventDefault();
|
||||
editor.dom.remove(direct);
|
||||
editor.nodeChanged();
|
||||
editor.undoManager.add();
|
||||
return;
|
||||
}
|
||||
|
||||
// 光标停在块首/块尾时,删除块本身。
|
||||
const caretBlock = editor.dom.getParent(block, STRUCTURAL_SELECTOR, body);
|
||||
if (!caretBlock) {
|
||||
return;
|
||||
}
|
||||
const caretIsAtEdge = (start: boolean): boolean => {
|
||||
const probe = editor.dom.createRng();
|
||||
probe.selectNodeContents(caretBlock);
|
||||
probe.collapse(start);
|
||||
const caret = selection.getRng();
|
||||
return caret && typeof caret.compareBoundaryPoints === 'function'
|
||||
&& caret.compareBoundaryPoints(start ? 1 : 3, probe) === 0;
|
||||
};
|
||||
|
||||
if (!((key === 8 && caretIsAtEdge(true)) || (key === 46 && caretIsAtEdge(false)))) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
const parent = caretBlock.parentElement;
|
||||
editor.dom.remove(caretBlock);
|
||||
// 删掉最后一个块后补一个空段落,否则没有可放置光标的位置。
|
||||
if (parent && parent.children.length === 0) {
|
||||
editor.setContent('<p></p>');
|
||||
editor.selection.setCursorLocation(editor.getBody().firstChild, 0);
|
||||
}
|
||||
editor.nodeChanged();
|
||||
editor.undoManager.add();
|
||||
}
|
||||
|
||||
/**
|
||||
* 让 iframe 高度跟随正文增长,避免编辑器内部出现第二根滚动条。
|
||||
*
|
||||
* TinyMCE 的 iframe 不会自动撑高;这里测量内容高度并同步容器,
|
||||
* 页面整体滚动由外层工作台负责。
|
||||
*/
|
||||
export function syncEditorHeight(editor: any): void {
|
||||
if (!editor || editor.removed) {
|
||||
return;
|
||||
}
|
||||
const doc = editor.getDoc();
|
||||
const iframe = editor.getContentAreaContainer()?.querySelector('iframe') as HTMLIFrameElement | null;
|
||||
if (!doc || !iframe) {
|
||||
return;
|
||||
}
|
||||
const height = Math.max(
|
||||
320,
|
||||
doc.documentElement.scrollHeight,
|
||||
doc.body ? doc.body.scrollHeight : 0
|
||||
);
|
||||
iframe.style.height = `${height}px`;
|
||||
iframe.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
/**
|
||||
* 在编辑器里为每个代码块渲染语言角标。
|
||||
*
|
||||
* 角标是编辑期提示(纯 UI),保存前由 serializeBlocks 移除。
|
||||
*/
|
||||
export function syncCodeLabels(editor: any): void {
|
||||
const root: HTMLElement | null = editor?.getBody?.() ?? null;
|
||||
if (!root) {
|
||||
return;
|
||||
}
|
||||
root.querySelectorAll<HTMLElement>('pre.enlighter-pre[data-jio-code]').forEach((pre) => {
|
||||
const existing = pre.querySelector(':scope > .jio-code-lang');
|
||||
const language = pre.getAttribute('data-enlighter-language') || 'generic';
|
||||
const label = CODE_LANGUAGES.find((item) => item.value === language)?.label || language;
|
||||
if (existing) {
|
||||
existing.setAttribute('data-jio-code-lang', language);
|
||||
if ((existing.textContent || '') !== label) {
|
||||
existing.textContent = label;
|
||||
}
|
||||
return;
|
||||
}
|
||||
const badge = document.createElement('span');
|
||||
badge.className = 'jio-code-lang';
|
||||
badge.contentEditable = 'false';
|
||||
badge.setAttribute('data-jio-code-lang', language);
|
||||
badge.textContent = label;
|
||||
pre.insertBefore(badge, pre.firstChild);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 给表格 / 代码块 / 引用等块渲染一个浮层删除按钮。
|
||||
*
|
||||
* 与 WPS 的块操作一致:鼠标悬停到块上时在右上角出现删除入口,
|
||||
* 避免只能靠反复按退格键。
|
||||
*/
|
||||
export function installBlockActions(editor: any): void {
|
||||
const doc = editor.getDoc();
|
||||
if (!doc || doc.getElementById('jio-block-actions')) {
|
||||
return;
|
||||
}
|
||||
const bar = doc.createElement('div');
|
||||
bar.id = 'jio-block-actions';
|
||||
bar.className = 'jio-creator__block-actions';
|
||||
bar.innerHTML =
|
||||
'<button type="button" class="jio-creator__block-action" data-jio-block-delete title="' +
|
||||
'删除此块' +
|
||||
'" aria-label="删除此块"><span aria-hidden="true">✕</span></button>';
|
||||
doc.body.appendChild(bar);
|
||||
|
||||
let current: HTMLElement | null = null;
|
||||
|
||||
const hide = () => {
|
||||
current = null;
|
||||
bar.classList.remove('is-visible');
|
||||
};
|
||||
|
||||
const show = (block: HTMLElement) => {
|
||||
if (!block || block === doc.body) {
|
||||
hide();
|
||||
return;
|
||||
}
|
||||
// 空段落没有删除价值,交给退格键处理。
|
||||
if (block.tagName === 'P' && (block.textContent || '').trim() === '') {
|
||||
hide();
|
||||
return;
|
||||
}
|
||||
current = block;
|
||||
const rect = block.getBoundingClientRect();
|
||||
bar.style.top = `${rect.top + doc.documentElement.scrollTop + 6}px`;
|
||||
bar.style.left = `${rect.right - 40}px`;
|
||||
bar.classList.add('is-visible');
|
||||
};
|
||||
|
||||
doc.addEventListener('mouseover', (event: MouseEvent) => {
|
||||
const block = (event.target as HTMLElement)?.closest?.(BLOCK_SELECTOR) as HTMLElement | null;
|
||||
if (block && block !== doc.body && !block.classList.contains('jio-code-lang')) {
|
||||
show(block);
|
||||
}
|
||||
});
|
||||
|
||||
doc.addEventListener('mouseleave', hide);
|
||||
|
||||
bar.addEventListener('mousedown', (event: MouseEvent) => event.preventDefault());
|
||||
bar.addEventListener('click', (event: MouseEvent) => {
|
||||
const button = (event.target as HTMLElement)?.closest('[data-jio-block-delete]');
|
||||
if (!button || !current) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
const target = current;
|
||||
hide();
|
||||
editor.undoManager.transact(() => {
|
||||
editor.dom.remove(target);
|
||||
});
|
||||
editor.nodeChanged();
|
||||
editor.undoManager.add();
|
||||
if (editor.getBody().children.length === 0) {
|
||||
editor.setContent('<p></p>');
|
||||
editor.selection.setCursorLocation(editor.getBody().firstChild, 0);
|
||||
}
|
||||
syncEditorHeight(editor);
|
||||
});
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import { apiPost } from './services/api';
|
||||
import { PublishDrawer } from './panels/publish-drawer';
|
||||
import { EditorToolbar } from './components/editor-toolbar';
|
||||
import { PublishSettings } from './components/publish-settings';
|
||||
import { StudioMasthead } from './components/studio-masthead';
|
||||
import { type SaveStateValue } from './components/save-state';
|
||||
import { useAutosave } from './hooks/use-autosave';
|
||||
import { useLocalRecovery } from './hooks/use-local-recovery';
|
||||
@@ -22,7 +23,6 @@ import { EDITOR_CONTENT_STYLE } from './editor-content-style';
|
||||
import { hydrateBlocks, serializeBlocks } from './utils/serialize';
|
||||
import { StatusBar } from './components/status-bar';
|
||||
import {
|
||||
CODE_LANGUAGES,
|
||||
codeBlockMarkup,
|
||||
dividerMarkup,
|
||||
footnoteMarkup,
|
||||
@@ -34,6 +34,13 @@ import {
|
||||
type QuoteColor,
|
||||
} from './utils/block-markup';
|
||||
import { BlockDialog, type BlockDialogKind, type BlockDialogResult } from './components/block-dialog';
|
||||
import { type Draft, type Limits, initialDraft } from './draft';
|
||||
import {
|
||||
installBlockActions,
|
||||
removeAdjacentBlock,
|
||||
syncCodeLabels,
|
||||
syncEditorHeight,
|
||||
} from './editor-runtime';
|
||||
import './styles/studio.scss';
|
||||
|
||||
const bootstrap: Record<string, any> = window['jioCreatorStudio'] || {};
|
||||
@@ -41,296 +48,6 @@ const nonce: string = bootstrap.nonce || '';
|
||||
const STORAGE_KEY = `jio-studio-${bootstrap.sourceId || 0}`;
|
||||
const EDITOR_ID = 'jio-creator-content';
|
||||
|
||||
type Featured = { type: string; data: Record<string, any>; mediaId?: number };
|
||||
type PayState = {
|
||||
enabled: boolean;
|
||||
modo: 'cash' | 'points';
|
||||
price: string;
|
||||
points: number;
|
||||
vip1Price: string;
|
||||
vip2Price: string;
|
||||
payLimit: number;
|
||||
downloads: Array<Record<string, any>>;
|
||||
};
|
||||
|
||||
type Limits = {
|
||||
titleMin?: number;
|
||||
titleMax?: number;
|
||||
titlePlaceholder?: string;
|
||||
priceMin?: number;
|
||||
priceMax?: number;
|
||||
pointsMin?: number;
|
||||
pointsMax?: number;
|
||||
};
|
||||
|
||||
type Schedule = { enabled: boolean; at: string };
|
||||
|
||||
type Draft = {
|
||||
postId: number;
|
||||
versionId: number;
|
||||
rev: number;
|
||||
title: string;
|
||||
content: string;
|
||||
schedule: Schedule;
|
||||
categories: number[];
|
||||
tags: string[];
|
||||
custom: Record<string, string[]>;
|
||||
featured: Featured;
|
||||
pay: PayState;
|
||||
};
|
||||
|
||||
function initialDraft(): Draft {
|
||||
const pay = bootstrap.pay || {};
|
||||
const selected = (bootstrap.categories && bootstrap.categories.selected) || [];
|
||||
const tags = (bootstrap.tags && bootstrap.tags.items) || [];
|
||||
return {
|
||||
postId: Number(bootstrap.sourceId || 0),
|
||||
versionId: Number(bootstrap.versionId || 0),
|
||||
rev: Number(bootstrap.versionRev || 0),
|
||||
title: String(bootstrap.title || ''),
|
||||
content: String(bootstrap.content || ''),
|
||||
schedule: { enabled: false, at: '' },
|
||||
categories: selected.map(Number),
|
||||
tags: tags.slice(),
|
||||
custom: bootstrap.custom || {},
|
||||
featured: bootstrap.featured || { type: '', data: {} },
|
||||
pay: {
|
||||
enabled: !!pay.enabled,
|
||||
modo: pay.payModo === 'points' ? 'points' : 'cash',
|
||||
price: String(pay.price || '0.00'),
|
||||
points: Number(pay.points || 0),
|
||||
vip1Price: String(pay.vip1Price || '0.00'),
|
||||
vip2Price: String(pay.vip2Price || '0.00'),
|
||||
payLimit: Number(pay.payLimit || 0),
|
||||
downloads: Array.isArray(pay.downloads) ? pay.downloads : [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Backspace / Delete 删除相邻的块级元素。
|
||||
*
|
||||
* 表格、代码块、引用、分割线这类内容不是普通文本,TinyMCE 默认不会在
|
||||
* 空段落里按删除键时主动移除它们,导致「删不掉」。这里在光标处于空段落、
|
||||
* 且紧邻目标块时直接移除该块。
|
||||
*/
|
||||
function removeAdjacentBlock(editor: any, event: any): void {
|
||||
const key = Number(event.keyCode);
|
||||
if (key !== 8 && key !== 46) {
|
||||
return;
|
||||
}
|
||||
const selection = editor.selection;
|
||||
if (!selection || !selection.isCollapsed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const body = editor.getBody();
|
||||
const block = selection.getNode();
|
||||
const isBlank = (node: any): boolean =>
|
||||
!!node && node.nodeType === 1 && (node.textContent || '').trim() === '' &&
|
||||
!editor.dom.getParent(node, 'table,pre,hr,.quote_q,.jio-math--block,.jio-toc-placeholder', body);
|
||||
const isBlockElement = (node: any): boolean =>
|
||||
!!node && node.nodeType === 1 && !!(node.matches?.('table,pre,hr,.quote_q,.jio-math--block,.jio-toc-placeholder'));
|
||||
|
||||
// 光标可能落在空段落里,也可能落在块级元素的边界(TinyMCE 常见行为);
|
||||
// 两种情况下都优先删除紧邻的块,而不是让按键悄悄失效。
|
||||
const blankContainer = isBlank(block) ? block : null;
|
||||
const direct = blankContainer
|
||||
? (key === 8 ? blankContainer.previousElementSibling : blankContainer.nextElementSibling)
|
||||
: null;
|
||||
|
||||
if (isBlockElement(direct)) {
|
||||
event.preventDefault();
|
||||
editor.dom.remove(direct);
|
||||
editor.nodeChanged();
|
||||
editor.undoManager.add();
|
||||
return;
|
||||
}
|
||||
|
||||
// 光标停在块首/块尾时,删除块本身。
|
||||
const caretBlock = editor.dom.getParent(block, 'table,pre,hr,.quote_q,.jio-math--block,.jio-toc-placeholder', body);
|
||||
if (!caretBlock) {
|
||||
return;
|
||||
}
|
||||
const caretIsAtEdge = (start: boolean): boolean => {
|
||||
const probe = editor.dom.createRng();
|
||||
probe.selectNodeContents(caretBlock);
|
||||
probe.collapse(start);
|
||||
const caret = selection.getRng();
|
||||
return caret && typeof caret.compareBoundaryPoints === 'function'
|
||||
&& caret.compareBoundaryPoints(start ? 1 : 3, probe) === 0;
|
||||
};
|
||||
|
||||
if (!((key === 8 && caretIsAtEdge(true)) || (key === 46 && caretIsAtEdge(false)))) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
const parent = caretBlock.parentElement;
|
||||
editor.dom.remove(caretBlock);
|
||||
// 删掉最后一个块后补一个空段落,否则没有可放置光标的位置。
|
||||
if (parent && parent.children.length === 0) {
|
||||
editor.setContent('<p></p>');
|
||||
editor.selection.setCursorLocation(editor.getBody().firstChild, 0);
|
||||
}
|
||||
editor.nodeChanged();
|
||||
editor.undoManager.add();
|
||||
}
|
||||
|
||||
/**
|
||||
* 在编辑器里为每个代码块渲染语言角标,并保证回车不会跳出代码区。
|
||||
*
|
||||
* 角标是编辑期提示(纯 UI),保存前由 serializeBlocks 移除。
|
||||
*/
|
||||
/**
|
||||
* 在代码块内回车时保持在代码里换行,而不是跳出到普通段落。
|
||||
*
|
||||
* TinyMCE 默认把 pre 当成不可持续输入的块,回车会新建段落,导致代码跑到
|
||||
* 代码块外面。这里手动插入换行并保持光标在 code 内。
|
||||
*/
|
||||
/**
|
||||
* 让 iframe 高度跟随正文增长,避免编辑器内部出现第二根滚动条。
|
||||
*
|
||||
* TinyMCE 的 iframe 不会自动撑高;这里测量内容高度并同步容器,
|
||||
* 页面整体滚动由外层工作台负责。
|
||||
*/
|
||||
function syncEditorHeight(editor: any): void {
|
||||
if (!editor || editor.removed) {
|
||||
return;
|
||||
}
|
||||
const doc = editor.getDoc();
|
||||
const iframe = editor.getContentAreaContainer()?.querySelector('iframe') as HTMLIFrameElement | null;
|
||||
if (!doc || !iframe) {
|
||||
return;
|
||||
}
|
||||
const height = Math.max(
|
||||
320,
|
||||
doc.documentElement.scrollHeight,
|
||||
doc.body ? doc.body.scrollHeight : 0
|
||||
);
|
||||
iframe.style.height = `${height}px`;
|
||||
iframe.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
function keepCaretInsideCode(editor: any, event: any): boolean {
|
||||
if (Number(event.keyCode) !== 13 || event.shiftKey) {
|
||||
return false;
|
||||
}
|
||||
const pre = editor.dom.getParent(editor.selection.getNode(), 'pre.enlighter-pre');
|
||||
if (!pre) {
|
||||
return false;
|
||||
}
|
||||
event.preventDefault();
|
||||
const code = pre.querySelector('code') || pre;
|
||||
editor.selection.setCursorLocation(code, code.childNodes.length);
|
||||
editor.insertContent('<br>');
|
||||
editor.nodeChanged();
|
||||
return true;
|
||||
}
|
||||
|
||||
/** 可整体删除的块级元素选择器。 */
|
||||
const BLOCK_SELECTOR = 'table,pre.enlighter-pre,hr,.quote_q,p';
|
||||
|
||||
/**
|
||||
* 给表格 / 代码块 / 引用等块渲染一个浮层删除按钮。
|
||||
*
|
||||
* 与 WPS 的块操作一致:鼠标悬停到块上时在右上角出现删除入口,
|
||||
* 避免只能靠反复按退格键。
|
||||
*/
|
||||
function installBlockActions(editor: any): void {
|
||||
const doc = editor.getDoc();
|
||||
if (!doc || doc.getElementById('jio-block-actions')) {
|
||||
return;
|
||||
}
|
||||
const bar = doc.createElement('div');
|
||||
bar.id = 'jio-block-actions';
|
||||
bar.className = 'jio-creator__block-actions';
|
||||
bar.innerHTML =
|
||||
'<button type="button" class="jio-creator__block-action" data-jio-block-delete title="' +
|
||||
'删除此块' +
|
||||
'" aria-label="删除此块"><span aria-hidden="true">✕</span></button>';
|
||||
doc.body.appendChild(bar);
|
||||
|
||||
let current: HTMLElement | null = null;
|
||||
|
||||
const hide = () => {
|
||||
current = null;
|
||||
bar.classList.remove('is-visible');
|
||||
};
|
||||
|
||||
const show = (block: HTMLElement) => {
|
||||
if (!block || block === doc.body) {
|
||||
hide();
|
||||
return;
|
||||
}
|
||||
// 空段落没有删除价值,交给退格键处理。
|
||||
if (block.tagName === 'P' && (block.textContent || '').trim() === '') {
|
||||
hide();
|
||||
return;
|
||||
}
|
||||
current = block;
|
||||
const rect = block.getBoundingClientRect();
|
||||
bar.style.top = `${rect.top + doc.documentElement.scrollTop + 6}px`;
|
||||
bar.style.left = `${rect.right - 40}px`;
|
||||
bar.classList.add('is-visible');
|
||||
};
|
||||
|
||||
doc.addEventListener('mouseover', (event: MouseEvent) => {
|
||||
const block = (event.target as HTMLElement)?.closest?.(BLOCK_SELECTOR) as HTMLElement | null;
|
||||
if (block && block !== doc.body && !block.classList.contains('jio-code-lang')) {
|
||||
show(block);
|
||||
}
|
||||
});
|
||||
|
||||
doc.addEventListener('mouseleave', hide);
|
||||
|
||||
bar.addEventListener('mousedown', (event: MouseEvent) => event.preventDefault());
|
||||
bar.addEventListener('click', (event: MouseEvent) => {
|
||||
const button = (event.target as HTMLElement)?.closest('[data-jio-block-delete]');
|
||||
if (!button || !current) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
const target = current;
|
||||
hide();
|
||||
editor.undoManager.transact(() => {
|
||||
editor.dom.remove(target);
|
||||
});
|
||||
editor.nodeChanged();
|
||||
editor.undoManager.add();
|
||||
if (editor.getBody().children.length === 0) {
|
||||
editor.setContent('<p></p>');
|
||||
editor.selection.setCursorLocation(editor.getBody().firstChild, 0);
|
||||
}
|
||||
syncEditorHeight(editor);
|
||||
});
|
||||
}
|
||||
|
||||
function syncCodeLabels(editor: any): void {
|
||||
const root: HTMLElement | null = editor?.getBody?.() ?? null;
|
||||
if (!root) {
|
||||
return;
|
||||
}
|
||||
root.querySelectorAll<HTMLElement>('pre.enlighter-pre[data-jio-code]').forEach((pre) => {
|
||||
const existing = pre.querySelector(':scope > .jio-code-lang');
|
||||
const language = pre.getAttribute('data-enlighter-language') || 'generic';
|
||||
if (existing) {
|
||||
existing.setAttribute('data-jio-code-lang', language);
|
||||
const label = CODE_LANGUAGES.find((item) => item.value === language)?.label || language;
|
||||
if ((existing.textContent || '') !== label) {
|
||||
existing.textContent = label;
|
||||
}
|
||||
return;
|
||||
}
|
||||
const badge = document.createElement('span');
|
||||
badge.className = 'jio-code-lang';
|
||||
badge.contentEditable = 'false';
|
||||
badge.setAttribute('data-jio-code-lang', language);
|
||||
badge.textContent = CODE_LANGUAGES.find((item) => item.value === language)?.label || language;
|
||||
pre.insertBefore(badge, pre.firstChild);
|
||||
});
|
||||
}
|
||||
|
||||
function escapeAttribute(value: string): string {
|
||||
return value.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
@@ -355,7 +72,7 @@ function plainText(content: string): string {
|
||||
}
|
||||
|
||||
function Studio(): JSX.Element {
|
||||
const [draft, setDraft] = useState<Draft>(initialDraft);
|
||||
const [draft, setDraft] = useState<Draft>(() => initialDraft(bootstrap));
|
||||
const draftRef = useRef(draft);
|
||||
draftRef.current = draft;
|
||||
const editorRef = useRef<any>(null);
|
||||
@@ -372,8 +89,6 @@ function Studio(): JSX.Element {
|
||||
}));
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
const [blockDialog, setBlockDialog] = useState<BlockDialogKind | null>(null);
|
||||
const [codeLanguage, setCodeLanguage] = useState<null | string>(null);
|
||||
const codeTargetRef = useRef<any>(null);
|
||||
const [notice, setNotice] = useState<{ tone: 'ok' | 'error'; text: string } | null>(null);
|
||||
const [recovery, setRecovery] = useState<null | { savedAt: string }>(null);
|
||||
const dirtyRef = useRef(false);
|
||||
@@ -881,33 +596,30 @@ function Studio(): JSX.Element {
|
||||
|
||||
return (
|
||||
<div className="jio-creator" data-status={bootstrap.status || ''} data-editor-ready={editorReady ? 'true' : 'false'}>
|
||||
<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>
|
||||
|
||||
<EditorToolbar
|
||||
ready={editorReady}
|
||||
formatValue={formatValue}
|
||||
onFormatChange={setFormatValue}
|
||||
canUndo={historyAvailability.canUndo}
|
||||
canRedo={historyAvailability.canRedo}
|
||||
capabilities={capabilities}
|
||||
onCommand={command}
|
||||
onRememberSelection={rememberSelection}
|
||||
onOpenLink={openLink}
|
||||
onOpenMedia={openMedia}
|
||||
onInsertBlock={insertBlock}
|
||||
onInsertQuote={insertQuote}
|
||||
<div className="jio-creator__top">
|
||||
<StudioMasthead
|
||||
centerUrl={String(bootstrap.urls?.center || '')}
|
||||
site={bootstrap.site || {}}
|
||||
viewer={bootstrap.viewer || {}}
|
||||
/>
|
||||
</header>
|
||||
|
||||
<header className="jio-creator__bar">
|
||||
<EditorToolbar
|
||||
ready={editorReady}
|
||||
formatValue={formatValue}
|
||||
onFormatChange={setFormatValue}
|
||||
canUndo={historyAvailability.canUndo}
|
||||
canRedo={historyAvailability.canRedo}
|
||||
capabilities={capabilities}
|
||||
onCommand={command}
|
||||
onRememberSelection={rememberSelection}
|
||||
onOpenLink={openLink}
|
||||
onOpenMedia={openMedia}
|
||||
onInsertBlock={insertBlock}
|
||||
onInsertQuote={insertQuote}
|
||||
/>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
{recovery && (
|
||||
<div className="jio-creator__notice" role="status">
|
||||
@@ -962,48 +674,6 @@ function Studio(): JSX.Element {
|
||||
onScheduleChange={(enabled, at) => patchDraft({ schedule: { enabled, at } })}
|
||||
/>
|
||||
|
||||
{codeLanguage !== null && (
|
||||
<div className="jio-creator-dialog" role="dialog" aria-modal="true" aria-labelledby="jio-code-lang-title">
|
||||
<button type="button" className="jio-creator-dialog__backdrop" aria-label={__('关闭', 'jio')} onClick={() => setCodeLanguage(null)} />
|
||||
<div className="jio-creator-dialog__panel">
|
||||
<header className="jio-creator-dialog__head">
|
||||
<h2 id="jio-code-lang-title">{__('代码语言', 'jio')}</h2>
|
||||
<button type="button" className="jio-creator-dialog__close" onClick={() => setCodeLanguage(null)} aria-label={__('关闭', 'jio')}>
|
||||
<i className="fa fas fa-times" aria-hidden="true" />
|
||||
</button>
|
||||
</header>
|
||||
<div className="jio-creator-dialog__body">
|
||||
<div className="jio-creator__lang-grid">
|
||||
{CODE_LANGUAGES.map((item) => (
|
||||
<button
|
||||
key={item.value}
|
||||
type="button"
|
||||
className={`jio-creator__lang-item${item.value === codeLanguage ? ' is-active' : ''}`}
|
||||
onClick={() => {
|
||||
const target = codeTargetRef.current;
|
||||
codeTargetRef.current = null;
|
||||
setCodeLanguage(null);
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
target.setAttribute('data-enlighter-language', item.value);
|
||||
const code = target.querySelector('code');
|
||||
if (code) {
|
||||
code.setAttribute('data-enlighter-language', item.value);
|
||||
}
|
||||
syncCodeLabels(editorRef.current);
|
||||
updateContent(serializeBlocks(editorRef.current.getContent()));
|
||||
}}
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{blockDialog && (
|
||||
<BlockDialog
|
||||
kind={blockDialog}
|
||||
|
||||
@@ -366,37 +366,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- 代码语言选择 ---------- */
|
||||
|
||||
.jio-creator__lang-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.jio-creator__lang-item {
|
||||
padding: 9px 6px;
|
||||
border: 1px solid #e6e9e8;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
color: #4f5957;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
border-color: #b7cdc3;
|
||||
color: var(--jc-accent-strong);
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
border-color: var(--jc-accent);
|
||||
background: #eff6f3;
|
||||
color: var(--jc-accent-strong);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- 块级删除控件(接近 WPS 的「块操作」) ---------- */
|
||||
|
||||
.jio-creator__block-actions {
|
||||
@@ -454,8 +423,4 @@
|
||||
.jio-creator__status-primary {
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.jio-creator__lang-grid {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
@@ -48,13 +48,13 @@ body.jio-page-submit.has-static-nav {
|
||||
--jc-accent: #{$accent};
|
||||
--jc-accent-strong: #{$accent-strong};
|
||||
--jc-warn: #{$warn};
|
||||
--jc-bar: 64px;
|
||||
--jc-top: 112px;
|
||||
--jc-status: 52px;
|
||||
|
||||
min-height: 100vh;
|
||||
min-height: 100dvh;
|
||||
/* 固定顶栏 / 底栏属于工作台 chrome,不随正文一起滚动。 */
|
||||
padding-top: var(--jc-bar);
|
||||
padding-top: var(--jc-top);
|
||||
padding-bottom: var(--jc-status);
|
||||
background: var(--jc-paper);
|
||||
color: var(--jc-ink);
|
||||
@@ -73,65 +73,41 @@ body.jio-page-submit.has-static-nav {
|
||||
}
|
||||
}
|
||||
|
||||
.jio-creator__bar {
|
||||
/*
|
||||
* 顶栏分两行:第一行是站点身份(左 LOGO / 右头像),第二行只有正文工具条。
|
||||
* 两行合成一个固定容器,正文只需要预留一次高度。
|
||||
*/
|
||||
.jio-creator__top {
|
||||
position: fixed;
|
||||
z-index: 300;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
height: var(--jc-bar);
|
||||
align-items: center;
|
||||
gap: clamp(10px, 1.7vw, 24px);
|
||||
padding: 0 clamp(14px, 2.4vw, 30px);
|
||||
border-bottom: 1px solid var(--jc-line);
|
||||
background: rgba(250, 251, 250, 0.98);
|
||||
box-shadow: 0 2px 8px rgba(27, 46, 39, 0.035);
|
||||
}
|
||||
|
||||
.jio-creator__bar-left,
|
||||
.jio-creator__bar-actions {
|
||||
.jio-creator__masthead {
|
||||
display: flex;
|
||||
height: 56px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 0 clamp(14px, 2.4vw, 30px);
|
||||
}
|
||||
|
||||
.jio-creator__masthead-left {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
}
|
||||
|
||||
.jio-creator__bar-left {
|
||||
padding-right: clamp(10px, 1.5vw, 20px);
|
||||
border-right: 1px solid var(--jc-line);
|
||||
}
|
||||
|
||||
.jio-creator__bar-actions {
|
||||
margin-left: auto;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.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__bar-divider {
|
||||
width: 1px;
|
||||
height: 18px;
|
||||
background: var(--jc-line);
|
||||
}
|
||||
|
||||
.jio-creator__bar-mode {
|
||||
color: var(--jc-ink-soft);
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.jio-creator__back {
|
||||
display: inline-flex;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 7px;
|
||||
@@ -146,10 +122,87 @@ body.jio-page-submit.has-static-nav {
|
||||
}
|
||||
}
|
||||
|
||||
.jio-creator__brand {
|
||||
display: inline-flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid rgba(38, 118, 101, 0.35);
|
||||
outline-offset: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.jio-creator__brand-logo {
|
||||
display: block;
|
||||
width: auto;
|
||||
max-width: 168px;
|
||||
height: 30px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.jio-creator__brand-text {
|
||||
overflow: hidden;
|
||||
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.01em;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.jio-creator__me {
|
||||
display: inline-flex;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid rgba(38, 118, 101, 0.5);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.jio-creator__me-avatar {
|
||||
display: block;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(24, 39, 34, 0.08);
|
||||
border-radius: 50%;
|
||||
background: #eef1f0;
|
||||
object-fit: cover;
|
||||
|
||||
&.is-placeholder {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #9aa2a0;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.jio-creator__bar {
|
||||
display: flex;
|
||||
height: 56px;
|
||||
align-items: center;
|
||||
/* 工具条居中:两侧等宽留白,按钮组永远落在视口中线上。 */
|
||||
justify-content: center;
|
||||
/* 窄屏放不下时退回左对齐,否则居中会把最左侧的按钮挤到不可滚动区。 */
|
||||
justify-content: safe center;
|
||||
padding: 0 clamp(14px, 2.4vw, 30px);
|
||||
border-top: 1px solid var(--jc-line);
|
||||
border-bottom: 1px solid var(--jc-line);
|
||||
}
|
||||
|
||||
.jio-creator__toolbar-tools {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex: 1 1 auto;
|
||||
max-width: 100%;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
overflow-x: auto;
|
||||
@@ -277,36 +330,6 @@ body.jio-page-submit.has-static-nav {
|
||||
}
|
||||
}
|
||||
|
||||
.jio-creator__save-draft {
|
||||
display: inline-flex;
|
||||
min-height: 34px;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 7px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid var(--jc-line);
|
||||
border-radius: 7px;
|
||||
background: #fff;
|
||||
color: var(--jc-ink-soft);
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover:not(:disabled),
|
||||
&:focus-visible:not(:disabled) {
|
||||
border-color: #b7cdc3;
|
||||
background: #f7faf8;
|
||||
color: var(--jc-accent-strong);
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.55;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
.jio-creator__save {
|
||||
display: inline-flex;
|
||||
flex: 0 0 auto;
|
||||
@@ -341,48 +364,6 @@ body.jio-page-submit.has-static-nav {
|
||||
}
|
||||
}
|
||||
|
||||
.jio-creator__ghost,
|
||||
.jio-creator__primary,
|
||||
.jio-creator__btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 34px;
|
||||
padding: 0 13px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 7px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
transition: background 0.18s ease, border-color 0.18s ease, color 0.18s ease;
|
||||
}
|
||||
|
||||
.jio-creator__ghost {
|
||||
border-color: var(--jc-line);
|
||||
background: transparent;
|
||||
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
border-color: var(--jc-accent);
|
||||
color: var(--jc-accent);
|
||||
}
|
||||
}
|
||||
|
||||
.jio-creator__primary {
|
||||
background: var(--jc-accent);
|
||||
color: #fff;
|
||||
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
background: var(--jc-accent-strong);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.jio-creator__notice {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -697,31 +678,32 @@ body.jio-page-submit.has-static-nav {
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.jio-creator {
|
||||
/* 图标 + 文字的两行按钮把顶栏撑高,移动端同样保留文字。 */
|
||||
--jc-bar: 60px;
|
||||
/* 两行顶栏在手机端压缩:账号行 50px、工具条 52px。 */
|
||||
--jc-top: 102px;
|
||||
}
|
||||
|
||||
.jio-creator__bar {
|
||||
gap: 7px;
|
||||
padding: 0 8px;
|
||||
.jio-creator__masthead {
|
||||
height: 50px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.jio-creator__bar-left {
|
||||
gap: 5px;
|
||||
padding-right: 7px;
|
||||
.jio-creator__brand-logo {
|
||||
max-width: 128px;
|
||||
height: 26px;
|
||||
}
|
||||
|
||||
.jio-creator__bar-mode,
|
||||
.jio-creator__bar-divider {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.jio-creator__bar-brand {
|
||||
.jio-creator__brand-text {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.jio-creator__bar-actions {
|
||||
gap: 5px;
|
||||
.jio-creator__me-avatar {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.jio-creator__bar {
|
||||
height: 52px;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.jio-creator__toolbar-tools {
|
||||
@@ -756,37 +738,6 @@ body.jio-page-submit.has-static-nav {
|
||||
}
|
||||
}
|
||||
|
||||
.jio-creator__bar-actions .jio-creator__save {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.jio-creator__save-draft {
|
||||
width: 34px;
|
||||
min-width: 34px;
|
||||
padding: 0;
|
||||
font-size: 0;
|
||||
|
||||
i {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.jio-creator__preview {
|
||||
min-height: 32px;
|
||||
padding: 0 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.jio-creator__primary {
|
||||
min-height: 32px;
|
||||
padding: 0 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.jio-creator__ghost {
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.jio-creator__canvas {
|
||||
width: calc(100% - 20px);
|
||||
margin: 14px auto 0;
|
||||
|
||||
Reference in new issue
Block a user