7 Commits

Author SHA1 Message Date
robonen 551b3bd921 fix(writekit): a node selection is a real DOM range, not an absence of one
Publish to NPM / Check version changes and publish (push) Successful in 10m0s
Selecting an atom used to clear every native range and leave the editable
root focused with NO selection. The browser then invents a caret at the
START of the content, `selectionchange` reads it, and the model's node
selection is overwritten by a text caret in the first block — so the
Enter meant to exit the freshly inserted atom split the opening
paragraph, and typing replaced its text. Reproduced live within a minute
of using the slash menu.

- the bridge now writes a node selection as `range.selectNode(blockEl)`;
  the read path maps that range to null, so selectionchange keeps its
  hands off the model
- beforeinput guards the node-selection state: browser edits through the
  element-wrapping range are prevented; delete falls through to
  deleteSelection, insertParagraph to exitAtom
- browser regression test walks the exact race: select atom → DOM range
  exists → selectionchange rewrites nothing → Enter lands a paragraph
  below the atom

writekit 0.0.4.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 05:52:54 +07:00
robonen 1d105b1f55 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>
2026-08-11 05:28:06 +07:00
robonen 66d9faad22 build: bump privitives to 0.0.6 and writekit to 0.0.2
Publish to NPM / Check version changes and publish (push) Successful in 9m53s
2026-08-11 03:45:34 +07:00
robonen cc93715c03 fix(writekit): attr coercion stops erasing data, validate runs, undo coalesces
Three fixes driven by building a real consumer (cyrille studio) on 0.0.1, each
pinned by tests:

- `coerceAttrs` treated the spec as a whitelist: any attribute a block
  definition did not declare was silently deleted by the first
  `normalizeDocument` pass — and since normalization runs on load and consumers
  autosave, the erasure wrote itself back to storage. Coercion now fills
  defaults and keeps unknown keys verbatim. Parse rules build attrs explicitly,
  so pasted markup cannot smuggle keys through this path; the CRDT never calls
  coercion, so replica semantics are unchanged.

- `AttrSpec.validate` was consulted only by `validateDocument`, which nothing
  in the library calls — it looked like enforcement and was inert. A provided
  value failing `validate` now falls back to the declared default,
  deterministically (CRDT-safe given one spec) and loudly in dev.

- Undo recorded one entry per transaction — one keystroke per Ctrl+Z, and 200
  keystrokes evicted the entire earlier history. Plain typing in one block now
  coalesces within a 500ms window by concatenation, which preserves the replay
  invariant (`inverted` stays in application order, replayed reversed), counts
  as ONE entry against maxSize, and never merges across blocks, structural
  changes, or a foreign transaction (remote setDoc, undo/redo, selection-only
  moves interrupt the chain). `coalesceMs: 0` opts out.

Also: `component` in a block definition may now be a lazy loader
(`() => import('./Card.vue')`) — a registry imported for its schema (codecs,
tests, server-side normalizers) then carries no view graph; the view wraps the
loader in a cached async component on first render.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 03:04:12 +07:00
robonen c6b6b93e81 chore: update dependencies and package manager versions across projects
Publish to NPM / Check version changes and publish (push) Failing after 10m54s
2026-07-31 04:21:32 +07:00
robonen e73e8d2cdd chore: update dependencies and configurations across multiple packages
Publish to NPM / Check version changes and publish (push) Failing after 10m44s
2026-06-18 02:02:58 +07:00
robonen 263c32002f 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.
2026-06-15 16:54:06 +07:00