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
@@ -0,0 +1,19 @@
import { describe, expect, it } from 'vitest';
import { isInteractiveTarget } from '../view/interactive';
describe('isInteractiveTarget', () => {
it('matches atom controls and contenteditable=false islands, not writekit text', () => {
const root = document.createElement('div');
root.setAttribute('contenteditable', 'true');
root.innerHTML = '<p class="text">hi</p><figure contenteditable="false"><input class="cap"></figure>';
document.body.append(root);
expect(isInteractiveTarget(root.querySelector('input.cap'))).toBe(true);
expect(isInteractiveTarget(root.querySelector('figure'))).toBe(true);
expect(isInteractiveTarget(root.querySelector('p.text'))).toBe(false);
expect(isInteractiveTarget(root)).toBe(false);
expect(isInteractiveTarget(null)).toBe(false);
root.remove();
});
});
@@ -0,0 +1,61 @@
import { describe, expect, it } from 'vitest';
import { createBlockElementRegistry, createSelectionBridge } from '../view/selection';
/**
* Builds the single-contenteditable DOM shape the view produces: one
* `[data-writekit-content]` root containing plain `[data-block-content]` block
* elements. Runs in jsdom (logic project) to prove the cross-block selection
* mapping without a real browser.
*/
function buildDoc() {
const root = document.createElement('div');
root.setAttribute('data-writekit-content', '');
const a = document.createElement('p');
a.setAttribute('data-block-content', '');
a.setAttribute('data-block-id', 'a');
a.textContent = 'hello';
const b = document.createElement('p');
b.setAttribute('data-block-content', '');
b.setAttribute('data-block-id', 'b');
b.textContent = 'world';
root.append(a, b);
document.body.replaceChildren(root);
const registry = createBlockElementRegistry();
registry.set('a', a);
registry.set('b', b);
return { root, a, b, registry };
}
describe('selection bridge (jsdom)', () => {
it('round-trips offset ↔ DOM point within a block', () => {
const { root, a, registry } = buildDoc();
const bridge = createSelectionBridge(() => root, registry);
const point = bridge.offsetToDomPoint(a, 3);
expect(bridge.domPointToOffset(a, point.node, point.offset)).toBe(3);
});
it('reads a cross-block native selection as a cross-block model range', () => {
const { root, a, b, registry } = buildDoc();
const bridge = createSelectionBridge(() => root, registry);
const sel = globalThis.getSelection!()!;
sel.removeAllRanges();
const range = document.createRange();
range.setStart(a.firstChild!, 1);
range.setEnd(b.firstChild!, 3);
sel.addRange(range);
const model = bridge.read();
expect(model?.kind).toBe('text');
if (model?.kind === 'text') {
expect(model.anchor).toEqual({ blockId: 'a', offset: 1 });
expect(model.focus).toEqual({ blockId: 'b', offset: 3 });
}
});
});
@@ -0,0 +1,20 @@
import { describe, expect, it } from 'vitest';
import { createDefaultRegistry } from '../preset';
import { getSlashItems } from '../view/ui';
describe('getSlashItems', () => {
it('returns every block with meta when the query is empty', () => {
const items = getSlashItems(createDefaultRegistry());
const types = items.map(item => item.type);
expect(types).toContain('heading');
expect(types).toContain('image');
expect(items.length).toBeGreaterThan(5);
});
it('filters by title and keywords', () => {
const registry = createDefaultRegistry();
expect(getSlashItems(registry, 'quote').some(item => item.type === 'blockquote')).toBe(true);
expect(getSlashItems(registry, 'h1').some(item => item.type === 'heading')).toBe(true);
expect(getSlashItems(registry, 'zzzz')).toEqual([]);
});
});