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,125 @@
<script setup lang="ts">
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import type { RemoteCursor } from '../../crdt';
import { useWritekitContext } from '../context';
export interface WritekitRemoteCursorsProps {
cursors: readonly RemoteCursor[];
}
const props = defineProps<WritekitRemoteCursorsProps>();
const ctx = useWritekitContext();
interface Box {
top: number;
left: number;
width: number;
height: number;
}
interface RenderedCursor {
clientId: string;
name: string;
color: string;
caret: Box | null;
highlights: Box[];
}
const container = ref<HTMLElement | null>(null);
const rendered = ref<RenderedCursor[]>([]);
function domPoint(blockId: string, offset: number): { node: Node; offset: number } | null {
const host = ctx.blockElements.get(blockId);
return host ? ctx.selection.offsetToDomPoint(host, offset) : null;
}
function relativize(rect: DOMRect, base: DOMRect): Box {
return { top: rect.top - base.top, left: rect.left - base.left, width: rect.width, height: rect.height || 18 };
}
function recompute(): void {
const root = container.value;
if (!root || typeof document === 'undefined') {
rendered.value = [];
return;
}
// Measure against the overlay itself — it is the positioned ancestor the
// caret/highlight children are laid out in, so coordinates line up exactly.
const base = root.getBoundingClientRect();
const next: RenderedCursor[] = [];
for (const cursor of props.cursors) {
const selection = cursor.selection;
if (!selection || selection.kind !== 'text')
continue;
const focusPoint = domPoint(selection.focus.blockId, selection.focus.offset);
if (!focusPoint)
continue;
const caretRange = document.createRange();
caretRange.setStart(focusPoint.node, focusPoint.offset);
caretRange.collapse(true);
const caret = relativize(caretRange.getBoundingClientRect(), base);
const highlights: Box[] = [];
const anchorPoint = domPoint(selection.anchor.blockId, selection.anchor.offset);
const collapsed = selection.anchor.blockId === selection.focus.blockId && selection.anchor.offset === selection.focus.offset;
if (anchorPoint && !collapsed) {
const range = document.createRange();
range.setStart(anchorPoint.node, anchorPoint.offset);
range.setEnd(focusPoint.node, focusPoint.offset);
if (range.collapsed) {
// Selection runs backwards (focus before anchor) — swap the ends.
range.setStart(focusPoint.node, focusPoint.offset);
range.setEnd(anchorPoint.node, anchorPoint.offset);
}
for (const rect of Array.from(range.getClientRects())) {
if (rect.width > 0)
highlights.push(relativize(rect, base));
}
}
next.push({
clientId: cursor.clientId,
name: cursor.user?.name ?? 'Anon',
color: cursor.user?.color ?? '#ef4444',
caret,
highlights,
});
}
rendered.value = next;
}
function schedule(): void {
void nextTick(recompute);
}
watch(() => props.cursors, schedule, { deep: true });
ctx.writekit.on('transaction', schedule);
onMounted(recompute);
onBeforeUnmount(() => ctx.writekit.off('transaction', schedule));
</script>
<template>
<div ref="container" class="writekit-remote-cursors" aria-hidden="true">
<template v-for="cursor in rendered" :key="cursor.clientId">
<div
v-for="(hl, i) in cursor.highlights"
:key="`${cursor.clientId}-hl-${i}`"
class="writekit-remote-selection"
:style="{ top: `${hl.top}px`, left: `${hl.left}px`, width: `${hl.width}px`, height: `${hl.height}px`, '--cursor-color': cursor.color }"
/>
<div
v-if="cursor.caret"
class="writekit-remote-caret"
:style="{ top: `${cursor.caret.top}px`, left: `${cursor.caret.left}px`, height: `${cursor.caret.height}px`, '--cursor-color': cursor.color }"
>
<span class="writekit-remote-caret-label">{{ cursor.name }}</span>
</div>
</template>
</div>
</template>