feat(writekit): a way out of atoms, and a slash menu that behaves
Publish to NPM / Check version changes and publish (push) Successful in 9m58s

Two gaps an author hits within the first minute of using atom blocks:

- there was no way to add a paragraph after a non-text block. Enter on a
  selected atom now starts a paragraph below it (exitAtom, chained before
  splitBlock), and a click on the root's padding below a trailing atom
  does the same — ends-in-text just places the caret at the end
- the slash menu ignored its own overflow: keyboard navigation walked the
  highlight out of view (now scrollIntoView nearest), and a background
  wheel scroll tore the menu off its caret anchor (now prevented outside
  the menu; scrolling the list itself stays native)
- BlockMeta gains `description`; the menu shows a detail pane beside the
  list with the highlighted item's description, replaceable wholesale via
  the new #preview slot. Headless stays headless: the pane is unstyled
  text and appears only when there is a description or a slot. Preset
  blocks are all described

Both floating menus (slash, bubble) are re-layered to the combobox
convention: PopperRoot provides the positioning context OUTSIDE a bare
Portal, which resolves its target from the ConfigProvider's
teleportTarget — the previous hardcoded to="body" was overriding the
app's configured target. A Combobox itself is the wrong base here on
purpose: its keyboard lives on ComboboxInput, while a suggestion menu
must leave focus in the contenteditable — the editor is the input.

writekit 0.0.3.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-11 05:11:29 +07:00
parent 66d9faad22
commit 1d105b1f55
19 changed files with 308 additions and 78 deletions
+11 -5
View File
@@ -14,7 +14,7 @@ function indentOf(node: Node): number {
* `checked` for to-dos). Markers/numbering and indentation are presentation
* (CSS), so the model stays a simple flat block list that maps cleanly to a CRDT.
*/
function defineListBlock(options: { type: string; listType: ListType; title: string; keywords: readonly string[] }) {
function defineListBlock(options: { type: string; listType: ListType; title: string; keywords: readonly string[]; description?: string }) {
const todo = options.listType === 'todo';
const attrs: AttrsSpec = {
@@ -43,10 +43,16 @@ function defineListBlock(options: { type: string; listType: ListType; title: str
parseDOM: [{ tag: `[data-list='${options.listType}']` }],
},
inputRules,
meta: { title: options.title, icon: 'list', keywords: options.keywords, group: 'lists' },
meta: {
title: options.title,
icon: 'list',
keywords: options.keywords,
group: 'lists',
...(options.description !== undefined && { description: options.description }),
},
});
}
export const bulletedList = defineListBlock({ type: 'bulleted-list', listType: 'bullet', title: 'Bulleted list', keywords: ['ul', 'bullet', 'unordered', 'list'] });
export const numberedList = defineListBlock({ type: 'numbered-list', listType: 'ordered', title: 'Numbered list', keywords: ['ol', 'number', 'ordered', 'list'] });
export const todoList = defineListBlock({ type: 'todo-list', listType: 'todo', title: 'To-do list', keywords: ['todo', 'task', 'checkbox', 'check'] });
export const bulletedList = defineListBlock({ type: 'bulleted-list', listType: 'bullet', title: 'Bulleted list', keywords: ['ul', 'bullet', 'unordered', 'list'], description: 'Items marked with bullets; Tab indents.' });
export const numberedList = defineListBlock({ type: 'numbered-list', listType: 'ordered', title: 'Numbered list', keywords: ['ol', 'number', 'ordered', 'list'], description: 'Items numbered in order; Tab indents.' });
export const todoList = defineListBlock({ type: 'todo-list', listType: 'todo', title: 'To-do list', keywords: ['todo', 'task', 'checkbox', 'check'], description: 'Checkable tasks; Enter adds the next one.' });