noteloom 0.1.5 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -149,7 +149,94 @@ import { DocumentExportButton } from 'noteloom';
149
149
  <DocumentExportButton label="View source" />
150
150
  ```
151
151
 
152
- It opens a modal with JSON/HTML/Text tabs (reading live from the store every time it opens) and a Copy button — useful for debugging, or as a starting point for a real "export" feature.
152
+ It opens a modal with JSON/Simple JSON/HTML/Text tabs (reading live from the store every time it opens) and a Copy button — useful for debugging, or as a starting point for a real "export" feature.
153
+
154
+ ## A simpler JSON shape for storage/API/CRUD use
155
+
156
+ `exportDocumentJSON()` above returns the *internal engine format* — the same normalized, id-referenced graph `EditorStore` operates on (blocks reference other blocks by id; text lives in a separate `runs` collection, not embedded inline). That shape is what makes per-run reactivity, O(1) structural edits, and real nesting (toggle lists, tables, inline atomic chips) work — it's not going to look like a simple flat document, on purpose.
157
+
158
+ If you just want something simpler to store, send over an API, or hand-edit — self-contained blocks in an array, `children` for nesting, no id-references to resolve — use the second, optional export/import pair instead:
159
+
160
+ ```js
161
+ import { exportDocumentSimpleJSON, importDocumentSimpleJSON } from 'noteloom';
162
+
163
+ const json = exportDocumentSimpleJSON(store, registry, inlineRegistry);
164
+ // {
165
+ // "version": 1,
166
+ // "blocks": [
167
+ // { "id": "p1", "type": "paragraph", "data": { "text": "Hello <strong>world</strong>" } },
168
+ // { "id": "h1", "type": "heading", "data": { "text": "Key features", "level": 3 } },
169
+ // {
170
+ // "id": "li1", "type": "listItem",
171
+ // "data": { "text": "Nested item", "ordered": false, "checked": null },
172
+ // "children": [ /* nested listItem blocks, same shape */ ]
173
+ // },
174
+ // {
175
+ // "id": "t1", "type": "table",
176
+ // "data": { "columns": [{ "id": "c1", "label": "Name" }], "rows": [["Cell text"]] }
177
+ // }
178
+ // ]
179
+ // }
180
+
181
+ // ...later, or on a different machine/process:
182
+ const doc = importDocumentSimpleJSON(json, registry, inlineRegistry); // -> { rootId, blocks, runs }
183
+ const store2 = new EditorStore(doc);
184
+ ```
185
+
186
+ Rich text (`data.text`) is an HTML string — the exact same per-run serialization every block type's own clipboard-copy `toHTML` already produces, so marks (bold/italic/underline/strike/code/sub/superscript/color/highlight/link) and atomic inline chips (checkbox/date/select/mention) round-trip through it the same way copy/paste already does. `table` is flattened specially (`data.columns` + `data.rows`, a 2D array) rather than exposing the internal table/row/cell block chain — the single biggest simplification versus the internal shape. Block/run ids are preserved on both export and import (useful for referencing/updating a specific block from an external system).
187
+
188
+ One existing, by-design limitation carried over from clipboard paste: an atomic inline type's *core* value round-trips (a checkbox's checked state + label, a date's ISO value, a select's chosen value + label) but its full `options` list does not — only the currently-selected option survives, the same as pasting one of these chips into another instance of the editor today.
189
+
190
+ This is purely an additive, alternate *interchange* format — the internal engine format above is unaffected either way, and this is not a replacement for it.
191
+
192
+ ## Right-to-left / multi-language text
193
+
194
+ Every block defaults to `dir="auto"` — the browser's own Unicode bidi algorithm detects direction per block from its first strong character, so a document mixing LTR and RTL blocks (an English heading over an Arabic paragraph, say) just works with zero configuration. For the cases `auto` can't infer on its own (most commonly an empty block, which has no text yet to detect a direction from), set an explicit override:
195
+
196
+ ```js
197
+ import { updateBlockProps } from 'noteloom';
198
+
199
+ // Document-wide default:
200
+ store.applyOperation(updateBlockProps(store.getRootId(), { dir: 'rtl' }));
201
+ // Or just one block:
202
+ store.applyOperation(updateBlockProps(blockId, { dir: 'rtl' }));
203
+ ```
204
+
205
+ A block's own `dir` wins over the document's; the block gutter menu also has a "Switch to right-to-left"/"left-to-right" item that sets this per-block. Code blocks are always `dir="ltr"` regardless of the surrounding document's default — code syntax (brackets, operators) is structurally LTR no matter what language a comment or string literal happens to be written in.
206
+
207
+ This pass covers the reading/typing/gutter-position direction itself; a full logical-properties (`margin-inline-start` etc.) audit of every pixel value in `style.css` is deliberately out of scope for now — the highest-impact pieces (list/checkbox marker position, blockquote border side, block gutter position) already flip correctly.
208
+
209
+ ## Printing & PDF
210
+
211
+ `style.css` includes a built-in `@media print` stylesheet: every piece of editing chrome (the block gutter, all portaled menus, the floating toolbar, resize handles, the mobile action bar, etc.) is hidden automatically, and a block hidden via "Hide in preview" stays hidden in the printout too, regardless of whether the app happens to be toggled into preview mode at the moment you print — printing always behaves like preview mode.
212
+
213
+ There's no bundled PDF-generation library (that would need a real dependency like jsPDF/pdfmake, conflicting with staying zero-runtime-dependency) — the browser's own print-to-PDF is the intended path:
214
+
215
+ ```js
216
+ window.print(); // Ctrl+P / Cmd+P works too — "Save as PDF" in the print dialog is your PDF export
217
+ ```
218
+
219
+ This only cleans up the *editor's* own chrome. A host app's own outer UI (nav bar, sidebar, its own toolbar) needs its own `@media print` rules the same way — see `examples/basic/src/style.css` for a worked example, since that chrome lives entirely outside this package.
220
+
221
+ ## Voice typing
222
+
223
+ `useVoiceTyping()` wraps the browser's native Web Speech API (`SpeechRecognition`) for continuous dictation mixed with spoken structural commands — say "heading one", "new paragraph", "bulleted list", "quote", "undo", etc. while dictating, and the current block converts (or a new one is inserted) instead of those words being typed as text:
224
+
225
+ ```jsx
226
+ import { useVoiceTyping } from 'noteloom';
227
+
228
+ function MicButton() {
229
+ const voice = useVoiceTyping();
230
+ if (!voice.isSupported) return null; // e.g. Firefox — no bundled fallback, degrades to nothing
231
+ return (
232
+ <button onClick={() => (voice.isListening ? voice.stop() : voice.start())}>
233
+ {voice.isListening ? 'Stop dictation' : 'Start dictation'}
234
+ </button>
235
+ );
236
+ }
237
+ ```
238
+
239
+ No speech-to-text SDK is bundled (same zero-runtime-dependency reasoning as PDF export above) — this is built entirely on the browser's own `SpeechRecognition`/`webkitSpeechRecognition`, so `isSupported` is `false` wherever that API doesn't exist. A command is only recognized when an entire *finalized* spoken utterance (a natural pause before/after, as reported by the Speech API itself) matches a known phrase exactly — see `src/voice/voiceCommands.js` for the full table — so a command word merely mentioned mid-sentence while dictating prose is never misread as a command.
153
240
 
154
241
  ## Mobile / touch support
155
242
 
@@ -299,6 +386,14 @@ registry.register('myBlock', {
299
386
  });
300
387
  ```
301
388
 
389
+ ## Accessibility
390
+
391
+ - Every portaled popover that's a genuine standalone action menu (the block gutter's Duplicate/Move/Hide/Delete menu, the block-range action menu, a table column's options menu) is keyboard-operable: opening one moves real focus onto its first item, ArrowUp/ArrowDown move between items (wrapping), Home/End jump to the first/last, and Escape closes it and returns focus to whatever opened it — not just a name-only `role="menu"` that only responds to mouse clicks.
392
+ - `Modal` moves focus into the dialog (its first focusable element) on open and restores it to whatever had focus before on close — not a full focus trap (this package stays zero-dependency, and its dialogs are short, single-purpose forms, not deep navigable UI), just "focus doesn't go missing."
393
+ - Structural actions that don't otherwise move focus anywhere describable (duplicate/move/hide/delete a block, or a whole selected range) announce what happened via a shared, visually-hidden `aria-live="polite"` region — screen-reader users get "Block deleted"/"3 blocks moved up" instead of silence.
394
+ - Embed images have a real, separately-authored `alt` text field (a toolbar button opens a small dialog to set it) — `alt` is never silently filled in from the uploaded file's raw filename or a pasted URL string, since neither is meaningful alt text.
395
+ - Table header cells have `scope="col"`, and the column-resize/embed-resize sliders both expose `aria-valuemin/valuemax/valuenow`.
396
+
302
397
  ## Development
303
398
 
304
399
  ```bash
@@ -311,6 +406,9 @@ npm run build # library build (dist/, ESM + CJS)
311
406
  ## Known limitations
312
407
 
313
408
  - No accessibility affordance exists for grouping sibling list items under a shared `role="list"` container (each list item is an independent block, not wrapped in one) — adding `role="listitem"` without that ancestor would be worse than no role at all, so it's deliberately left out pending a bigger structural change.
409
+ - The library doesn't render your editor's own root/surface element (that's host-rendered — see `examples/basic/src/App.jsx`'s `EditorSurface`), so it can't add `role="document"`/`aria-label` there itself; the example app demonstrates doing this on your own surface element, which is worth copying into your own app.
314
410
  - Cross-block mark toggling (bold/italic/underline over a selection spanning multiple blocks) applies as one store operation per block, not a single atomic undo step.
315
411
  - `select`'s option-adding UI and any `createSelectFieldType`-based type's options (e.g. an "Assignee" @-mention) are meant as a starting point — a real app will want to wire its own people/options source.
316
- - Automated tests run under jsdom; there is no automated real-browser test suite. If you hit an edge case jsdom can't reproduce (anything involving actual native `contentEditable` browser quirks), please file an issue with the exact browser/OS and steps.
412
+ - RTL support covers direction resolution (`dir="auto"` + per-block/document override) and the highest-impact visual pieces (list markers, blockquote border, block gutter position) a full logical-properties rewrite of every hardcoded pixel value in `style.css` is a bigger follow-up, not yet done.
413
+ - Voice typing (`useVoiceTyping`) only acts on *finalized* speech results, not interim/in-progress ones, and command detection requires a spoken command to be its own complete utterance — there's no explicit "command mode" trigger (push-to-command, wake phrase) yet, just pause-based auto-detection.
414
+ - Automated tests run under jsdom; there is no automated real-browser test suite. If you hit an edge case jsdom can't reproduce (anything involving actual native `contentEditable` browser quirks, or the real Web Speech API), please file an issue with the exact browser/OS and steps.