feat(writekit): rename @robonen/editor to @robonen/writekit

Rename the rich-text editor package and all Editor* exports to Writekit*;
remove the old vue/editor tree.
This commit is contained in:
2026-06-15 16:54:06 +07:00
parent 55e78786d6
commit 263c32002f
149 changed files with 1563 additions and 1748 deletions
+119
View File
@@ -0,0 +1,119 @@
import { PubSub } from '@robonen/stdlib';
import { selectionEq } from '../model';
import type { Command, Dispatch } from './command';
import type { WritekitState } from './writekit-state';
import type { HistoryOptions } from './history';
import { createHistory } from './history';
import type { Transaction } from './transaction';
import { applyTransaction, createTransaction } from './transaction';
/**
* Writekit event map. A `type` (not `interface`) so it satisfies the
* `Record<string, ...>` constraint of {@link PubSub}.
*/
export interface WritekitEvents {
/** Fired for every applied transaction (local, undo/redo, or remote). */
transaction: (tr: Transaction, next: WritekitState, prev: WritekitState) => void;
/** Fired when the document changed. */
docChange: (next: WritekitState, prev: WritekitState) => void;
/** Fired when the selection changed. */
selectionChange: (next: WritekitState, prev: WritekitState) => void;
}
/**
* The headless writekit controller: owns live state, the undo history, and a
* typed event bus. The Vue layer wraps it; the CRDT adapter subscribes to it.
*/
export interface Writekit {
readonly state: WritekitState;
dispatch: Dispatch;
/** Run a command against the current state, dispatching if it applies. */
command: (cmd: Command) => boolean;
undo: () => boolean;
redo: () => boolean;
canUndo: () => boolean;
canRedo: () => boolean;
on: <K extends keyof WritekitEvents>(event: K, listener: WritekitEvents[K]) => void;
off: <K extends keyof WritekitEvents>(event: K, listener: WritekitEvents[K]) => void;
destroy: () => void;
}
export interface CreateWritekitOptions {
readonly state: WritekitState;
readonly history?: HistoryOptions;
}
/** Create an {@link Writekit} around an initial state. */
export function createWritekit(options: CreateWritekitOptions): Writekit {
let state = options.state;
// A mapped type (not the `interface`) satisfies PubSub's `Record<string, …>` constraint.
const bus = new PubSub<{ [K in keyof WritekitEvents]: WritekitEvents[K] }>();
const history = createHistory(options.history);
const dispatch: Dispatch = (tr) => {
const prev = state;
const next = applyTransaction(prev, tr);
state = next;
if (tr.meta.get('addToHistory') !== false && tr.steps.length > 0) {
history.record({
steps: tr.steps,
inverted: tr.inverted,
selectionBefore: prev.selection,
selectionAfter: next.selection,
});
}
bus.emit('transaction', tr, next, prev);
if (next.doc !== prev.doc)
bus.emit('docChange', next, prev);
if (!selectionEq(next.selection, prev.selection))
bus.emit('selectionChange', next, prev);
};
return {
get state() {
return state;
},
dispatch,
command: cmd => cmd(state, dispatch),
undo() {
const entry = history.undo();
if (!entry)
return false;
const tr = createTransaction(state);
for (let i = entry.inverted.length - 1; i >= 0; i--)
tr.step(entry.inverted[i]!);
tr.setSelection(entry.selectionBefore).setMeta('addToHistory', false).setMeta('history', 'undo');
dispatch(tr);
return true;
},
redo() {
const entry = history.redo();
if (!entry)
return false;
const tr = createTransaction(state);
for (const step of entry.steps)
tr.step(step);
tr.setSelection(entry.selectionAfter).setMeta('addToHistory', false).setMeta('history', 'redo');
dispatch(tr);
return true;
},
canUndo: history.canUndo,
canRedo: history.canRedo,
on(event, listener) {
bus.on(event, listener);
},
off(event, listener) {
bus.off(event, listener);
},
destroy() {
bus.clear('transaction').clear('docChange').clear('selectionChange');
history.clear();
},
};
}