feat(creator-studio): bottom status bar, editor-rendered math/footnote, code language picker, block delete

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
kate522andClaude Opus 5 committed 2026-11-07 00:38:39 +08:00
1 parent 6928127bfa
commit 4a119358ca
11 files changed
+754 -63

No files matched your search

@@ -0,0 +1,125 @@
/**
* 底部固定状态栏。
*
* 与公众号/知乎一致:左侧是字数与输入法状态,中间是草稿时间,右侧是预览与发布。
* 固定在视口底部,不随正文滚动。
*
* @package Jio
*/
import { __, sprintf } from '@wordpress/i18n';
import { SaveState, type SaveStateValue } from './save-state';
type Props = {
charCount: number;
saveState: SaveStateValue;
isPublishedEdit: boolean;
saving: boolean;
onSaveDraft: () => void;
onPreview: () => void;
onPublish: () => void;
};
const MINUTE = 60 * 1000;
const HOUR = 60 * MINUTE;
const DAY = 24 * HOUR;
const MONTH = 30 * DAY;
const YEAR = 365 * DAY;
/** 相对时间:刚刚 / X分钟前 / X小时前 … 与参考编辑器一致。 */
export function relativeTime(iso: string, now = Date.now()): string {
const at = Date.parse(iso);
if (!Number.isFinite(at)) {
return '';
}
const diff = Math.max(0, now - at);
if (diff < MINUTE) {
return __('刚刚', 'jio');
}
if (diff < HOUR) {
return sprintf(
/* translators: %d: minutes */
__('%d分钟前', 'jio'),
Math.floor(diff / MINUTE)
);
}
if (diff < DAY) {
return sprintf(
/* translators: %d: hours */
__('%d小时前', 'jio'),
Math.floor(diff / HOUR)
);
}
if (diff < MONTH) {
return sprintf(
/* translators: %d: days */
__('%d天前', 'jio'),
Math.floor(diff / DAY)
);
}
if (diff < YEAR) {
return sprintf(
/* translators: %d: months */
__('%d个月前', 'jio'),
Math.floor(diff / MONTH)
);
}
return sprintf(
/* translators: %d: years */
__('%d年前', 'jio'),
Math.floor(diff / YEAR)
);
}
const STATUS_LABELS: Record<SaveStateValue['state'], string> = {
ready: __('草稿', 'jio'),
saving: __('保存中', 'jio'),
saved: __('草稿', 'jio'),
error: __('保存失败', 'jio'),
conflict: __('冲突', 'jio'),
submitting: __('提交中', 'jio'),
submitted: __('已提交', 'jio'),
};
export function StatusBar({
charCount,
saveState,
isPublishedEdit,
saving,
onSaveDraft,
onPreview,
onPublish,
}: Props) {
const savedAt = saveState.at ? relativeTime(saveState.at) : '';
const status = STATUS_LABELS[saveState.state] || STATUS_LABELS.ready;
return (
<footer className="jio-creator__status" role="contentinfo">
<div className="jio-creator__status-left">
<span className="jio-creator__status-count">
{__('字数', 'jio')}:{charCount}
</span>
<span className="jio-creator__status-mode">{__('Markdown 语法输入中', 'jio')}</span>
</div>
<div className="jio-creator__status-meta">
<SaveState value={saveState} />
<span className="jio-creator__status-dot" aria-hidden="true">·</span>
<span className="jio-creator__status-time">{savedAt || status}</span>
</div>
<div className="jio-creator__status-actions">
<button type="button" className="jio-creator__status-ghost" onClick={onSaveDraft} disabled={saving}>
{__('保存草稿', 'jio')}
</button>
<button type="button" className="jio-creator__status-ghost" onClick={onPreview} disabled={saving}>
{__('预览', 'jio')}
</button>
<button type="button" className="jio-creator__status-primary" onClick={onPublish} disabled={saving}>
{isPublishedEdit ? __('提交改稿', 'jio') : __('发布', 'jio')}
</button>
</div>
</footer>
);
}