1d105b1f55
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>
59 lines
2.5 KiB
TypeScript
59 lines
2.5 KiB
TypeScript
import type { Node } from '../model';
|
|
import type { AttrsSpec } from '../schema';
|
|
import { defineBlock } from '../registry';
|
|
|
|
type ListType = 'bullet' | 'ordered' | 'todo';
|
|
|
|
function indentOf(node: Node): number {
|
|
return typeof node.attrs['indent'] === 'number' ? node.attrs['indent'] : 0;
|
|
}
|
|
|
|
/**
|
|
* DRY factory for the three list variants. Lists are **flat-with-indent**: each
|
|
* item is its own top-level text block carrying an `indent` attribute (and
|
|
* `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[]; description?: string }) {
|
|
const todo = options.listType === 'todo';
|
|
|
|
const attrs: AttrsSpec = {
|
|
indent: { default: 0 },
|
|
...(todo ? { checked: { default: false } } : {}),
|
|
};
|
|
|
|
const inputRules = options.listType === 'bullet'
|
|
? [{ match: /^[-*]\s$/ }]
|
|
: options.listType === 'ordered'
|
|
? [{ match: /^\d+\.\s$/ }]
|
|
: [{ match: /^\[\s?\]\s$/ }];
|
|
|
|
return defineBlock({
|
|
type: options.type,
|
|
spec: {
|
|
content: { kind: 'text' },
|
|
group: 'list',
|
|
attrs,
|
|
toDOM: (node: Node) => ['div', {
|
|
'data-list': options.listType,
|
|
// margin shifts the item per indent level; padding leaves a gutter for the marker.
|
|
style: `margin-left:${indentOf(node) * 1.5}em;padding-left:1.5em`,
|
|
...(todo ? { 'data-checked': node.attrs['checked'] ? 'true' : 'false' } : {}),
|
|
}, 0],
|
|
parseDOM: [{ tag: `[data-list='${options.listType}']` }],
|
|
},
|
|
inputRules,
|
|
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'], 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.' });
|