quoin-editor 0.3.0 → 0.4.1

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
@@ -65,7 +65,7 @@ export function TransitionEditor() {
65
65
  }
66
66
  ```
67
67
 
68
- What breaks is a store that rebuilds the document on the way through, so what comes back is a structural copy rather than the array the canvas emitted. Arriving behind the canvas's own state, that copy reads as a foreign document and replaces live text with an older snapshot. The damage is not a clean revert: the characters the snapshot predates are dropped and the ones typed after them are kept, so `ZZTYPED hello world` can land as `ZZYED hello wold`. A Redux or Zustand round trip that clones the document, and a server-synced store, are the usual sources.
68
+ Since 0.4.1, stores that clone the document on the way through, such as a Redux or Zustand round trip or a server-synced store, are supported too. The canvas recognises an array it emitted however late it comes back, and a structural copy of one as long as it is within the last 64 edits.
69
69
 
70
70
  Debounce the side effect, not the state. `useAutosave` does exactly that: it tracks the values you give it, waits for the edits to settle, then calls your `save`, and it retries a failed save with exponential backoff.
71
71
 
@@ -96,7 +96,7 @@ export function AutosavingEditor({ initial }: { initial: QuoinBlock[] }) {
96
96
  }
97
97
  ```
98
98
 
99
- `save` may return anything, so a mutation, an axios call or a `fetch` that resolves to a body goes straight in with no wrapper to discard the value. A save fails by rejecting, or by resolving to an explicit `{ ok: false }`; everything else, `undefined` included, is a success. `status` moves `idle` to `saving` to `saved`, or to `retrying` while it backs off, and `retry()` tries again at once.
99
+ `save` may return anything, so a mutation, an axios call or a `fetch` that resolves to a body goes straight in with no wrapper to discard the value. A save fails by rejecting, or by resolving to an explicit `{ ok: false }`; everything else, `undefined` included, is a success. `status` moves `idle` to `saving` to `saved`, or to `retrying` while it backs off, and `retry()` tries again at once. Since 0.4.1 one save runs at a time, with the newest payload queued behind it, and a pending edit is saved at once when the component unmounts or the page is hidden (`flush()` does the same on demand). Retries stop after `maxRetries` (default 8) or when `isRetryable(error)` returns false; `status` is then `failed` and `error` holds the reason.
100
100
 
101
101
  ### Loading, undoing and discarding
102
102
 
@@ -104,7 +104,7 @@ You can hand the canvas a new `blocks` array at any time to load, undo or merge
104
104
 
105
105
  When you echo `onChange` back into `blocks`, as every sample above does, the last array you supplied is the edited document, so `setBlocks(saved)` differs from it and resets the canvas with nothing more to do.
106
106
 
107
- The case that needs care is a host that does not echo edits back. There, the last array you supplied is still the original, and an array structurally equal to it is ignored: a "discard changes" button that re-supplies the original document does nothing. To force a reset, remount the canvas with a React `key`. The sample below uses a `key` so it works either way.
107
+ The case that needs care is a host that does not echo edits back. There, the last array you supplied is still the original, and an array structurally equal to it is ignored: a "discard changes" button that re-supplies the original document does nothing. The same goes for restoring a copy of a state from the last 64 edits, which reads as a late echo of the canvas's own output. To force a reset, remount the canvas with a React `key`. The sample below uses a `key` so it works either way.
108
108
 
109
109
  ```tsx
110
110
  import { useState } from 'react';
@@ -311,7 +311,7 @@ Each core block keeps its content under its own keys. A host that persists block
311
311
 
312
312
  Two of these are worth saying out loud, because both have been guessed wrong:
313
313
 
314
- - **The callout's prose is `body`, not `text`.** The label over the field reads "Callout text", which is what the author sees; `body` is what the block stores.
314
+ - **The callout's prose is `body`, not `text`.** The label over the field reads "Callout text", which is what the author sees; `body` is what the block stores. `tone` is one of `info`, `tip`, `warning`, `success` or `danger`, each drawn in its own colour; anything else falls back to the neutral border and fill.
315
315
  - **Markdown blocks store markdown source, not rendered HTML.** Quoin edits and stores the source and renders it as plain text; supply a `preview` if you want it rendered.
316
316
 
317
317
  The optional fields are written after a block's first save: an image's `caption` when the author types one, its `width` and `height` once the uploaded bytes have been measured, a file's `size` when the upload reports it. `src/blocks/data.test.ts` reads the keys back out of each editor's source and fails if one drifts from the table above.
@@ -477,19 +477,22 @@ Because it draws previews and never editors, a block type registered without a `
477
477
  --quoin-background: oklch(1 0 0);
478
478
  --quoin-foreground: oklch(0.145 0 0);
479
479
  --quoin-muted: oklch(0.97 0 0);
480
- --quoin-muted-foreground: oklch(0.556 0 0);
480
+ --quoin-muted-foreground: oklch(0.54 0 0);
481
481
  --quoin-secondary: oklch(0.97 0 0);
482
482
  --quoin-secondary-foreground: oklch(0.205 0 0);
483
483
  --quoin-popover: oklch(1 0 0);
484
484
  --quoin-popover-foreground: oklch(0.145 0 0);
485
485
  --quoin-border: oklch(0.922 0 0);
486
486
  --quoin-input: oklch(0.922 0 0);
487
- --quoin-ring: oklch(0.708 0 0);
487
+ --quoin-ring: oklch(0.45 0 0);
488
488
  --quoin-primary: oklch(0.205 0 0);
489
489
  --quoin-primary-foreground: oklch(0.985 0 0);
490
490
  --quoin-accent: oklch(0.97 0 0);
491
491
  --quoin-accent-foreground: oklch(0.205 0 0);
492
- --quoin-destructive: oklch(0.577 0.245 27.325);
492
+ --quoin-destructive: oklch(0.55 0.245 27.325);
493
+ --quoin-info: oklch(0.55 0.17 250);
494
+ --quoin-success: oklch(0.55 0.14 155);
495
+ --quoin-warning: oklch(0.65 0.16 65);
493
496
  --quoin-radius: 0.625rem;
494
497
  }
495
498
 
@@ -504,12 +507,15 @@ Because it draws previews and never editors, a block type registered without a `
504
507
  --quoin-popover-foreground: oklch(0.985 0 0);
505
508
  --quoin-border: oklch(1 0 0 / 10%);
506
509
  --quoin-input: oklch(1 0 0 / 15%);
507
- --quoin-ring: oklch(0.556 0 0);
510
+ --quoin-ring: oklch(0.708 0 0);
508
511
  --quoin-primary: oklch(0.922 0 0);
509
512
  --quoin-primary-foreground: oklch(0.205 0 0);
510
513
  --quoin-accent: oklch(0.269 0 0);
511
514
  --quoin-accent-foreground: oklch(0.985 0 0);
512
515
  --quoin-destructive: oklch(0.704 0.191 22.216);
516
+ --quoin-info: oklch(0.72 0.14 250);
517
+ --quoin-success: oklch(0.74 0.15 155);
518
+ --quoin-warning: oklch(0.79 0.15 65);
513
519
  --quoin-radius: 0.625rem;
514
520
  }
515
521
  ```
@@ -548,10 +554,17 @@ Point the prefixed names at your own in one block on `.quoin`. The canvas root c
548
554
  --quoin-accent: var(--accent);
549
555
  --quoin-accent-foreground: var(--accent-foreground);
550
556
  --quoin-destructive: var(--destructive);
557
+ --quoin-info: var(--info);
558
+ --quoin-success: var(--success);
559
+ --quoin-warning: var(--warning);
551
560
  --quoin-radius: var(--radius);
552
561
  }
553
562
  ```
554
563
 
564
+ `--quoin-info`, `--quoin-success` and `--quoin-warning` arrived in 0.4.0 and are read only by the callout block, for its rule and the wash behind it, never for type. If your own theme has no equivalent, leave those three out of the block and the defaults above stand; the rest of the editor does not touch them.
565
+
566
+ `--quoin-ring` colours the keyboard focus outline, which is solid and drawn at full strength, so it should reach 3:1 against both `--quoin-background` and `--quoin-popover`. A pale value, such as the grey many shadcn themes keep for `--ring`, fails that, so give `--quoin-ring` a value of its own that does.
567
+
555
568
  That one block covers both modes: the tokens on the right already flip with your own dark class, and each mapping beats Quoin's `:root` and `.dark` defaults alike. Write it unlayered, or in a layer that sorts after `quoin`, so it outranks those defaults.
556
569
 
557
570
  To change one token rather than adopt a whole theme, declare it on `:root`, and pair it with a `.dark` rule if you theme dark at all. Pinning a token on `:root` alone also beats Quoin's `.dark` value for it, which would freeze that one token in its light form.
@@ -712,6 +725,27 @@ From `quoin-editor/headless`: the transforms and the two hooks listed above, wit
712
725
 
713
726
  From `quoin-editor/styles.css`: the stylesheet.
714
727
 
728
+ ## Accessibility
729
+
730
+ What the test suite checks:
731
+
732
+ - axe runs over the canvas, the block cards, the inserter, the slash palette and the shortcuts reference.
733
+ - Controls carry accessible names, and the icons in the canvas, the block options menu, the inserter and the slash palette are hidden from assistive technology.
734
+ - Blocks reorder by keyboard, with announcements that name each block by label and position.
735
+ - Escape leaves an open block and returns focus to its Edit button.
736
+ - The default light and dark tokens keep the keyboard focus outline at 3:1 or more and highlighted-row text at 4.5:1 or more, and focusable controls draw the focus as an outline, which Windows forced colours mode keeps.
737
+
738
+ The checks are automated. A manual pass with NVDA and VoiceOver is not recorded yet.
739
+
740
+ Known gaps in 0.4:
741
+
742
+ - A markdown block is read as source. At rest it is plain text, so a screen reader reads the markdown characters as written.
743
+ - Keyboard drag with Space and the arrow keys can drop a block in place, and its announcement can name the wrong position. Use Move up and Move down in the block's options menu to reorder by keyboard.
744
+
745
+ ## Stability
746
+
747
+ Quoin is 0.x. A minor version may contain breaking changes, and each one is listed in the [changelog](CHANGELOG.md). If you need stability, pin a minor with `~0.4.0`, which takes patch releases only.
748
+
715
749
  ## Licence
716
750
 
717
751
  MIT. Copyright (c) 2026 Amine Sboui. See [LICENSE](LICENSE).
package/dist/headless.cjs CHANGED
@@ -328,15 +328,18 @@ function shouldOpenPalette(value, selectionStart, selectionEnd) {
328
328
  return previous === "\n" || previous === " " || previous === " ";
329
329
  }
330
330
 
331
+ // src/markdown/platform.ts
332
+ function isMac() {
333
+ if (typeof navigator === "undefined") return false;
334
+ const { userAgentData, platform } = navigator;
335
+ return /mac|iphone|ipad/i.test(userAgentData?.platform || platform || "");
336
+ }
337
+
331
338
  // src/markdown/shortcut-label.ts
332
339
  var MAC_LABEL = { mod: "\u2318", alt: "\u2325", shift: "\u21E7" };
333
340
  var OTHER_LABEL = { mod: "Ctrl", alt: "Alt", shift: "Shift" };
334
- function onMac() {
335
- if (typeof navigator === "undefined") return false;
336
- return /Mac|iPhone|iPad/.test(navigator.userAgent);
337
- }
338
341
  function formatShortcut(hint) {
339
- const mac = onMac();
342
+ const mac = isMac();
340
343
  const labels = mac ? MAC_LABEL : OTHER_LABEL;
341
344
  const parts = hint.split("+").map((part) => labels[part] ?? part.toUpperCase());
342
345
  return parts.join(mac ? "" : "+");
@@ -400,9 +403,10 @@ function useMarkdownKeymap({
400
403
  }) {
401
404
  const onKeyDown = React.useCallback(
402
405
  (event) => {
406
+ if (event.nativeEvent.isComposing || event.keyCode === 229) return;
403
407
  const el = event.currentTarget;
404
408
  const { value, selectionStart, selectionEnd } = el;
405
- const mod = event.metaKey || event.ctrlKey;
409
+ const mod = isMac() ? event.metaKey : event.ctrlKey;
406
410
  const run = (edit) => {
407
411
  if (!edit) return;
408
412
  event.preventDefault();
@@ -462,11 +466,17 @@ function useMarkdownKeymap({
462
466
  }
463
467
 
464
468
  // src/hooks/useAutoGrow.ts
469
+ var React3 = __toESM(require("react"), 1);
470
+
471
+ // src/hooks/use-isomorphic-layout-effect.ts
465
472
  var React2 = __toESM(require("react"), 1);
473
+ var useIsomorphicLayoutEffect = typeof document === "undefined" ? React2.useEffect : React2.useLayoutEffect;
474
+
475
+ // src/hooks/useAutoGrow.ts
466
476
  function useAutoGrow(value) {
467
- const ref = React2.useRef(null);
468
- const lastWidth = React2.useRef(null);
469
- const fit = React2.useCallback(() => {
477
+ const ref = React3.useRef(null);
478
+ const lastWidth = React3.useRef(null);
479
+ const fit = React3.useCallback(() => {
470
480
  const el = ref.current;
471
481
  if (!el) return;
472
482
  const scroller = document.scrollingElement ?? document.documentElement;
@@ -475,8 +485,8 @@ function useAutoGrow(value) {
475
485
  if (el.scrollHeight > 0) el.style.height = `${el.scrollHeight}px`;
476
486
  if (scroller.scrollTop !== scrollTop) scroller.scrollTop = scrollTop;
477
487
  }, []);
478
- React2.useLayoutEffect(fit, [fit, value]);
479
- React2.useEffect(() => {
488
+ useIsomorphicLayoutEffect(fit, [fit, value]);
489
+ React3.useEffect(() => {
480
490
  const el = ref.current;
481
491
  if (!el || typeof ResizeObserver === "undefined") return;
482
492
  const observer = new ResizeObserver(([entry]) => {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/headless.ts","../src/markdown/edit.ts","../src/markdown/line-at.ts","../src/markdown/line-toggles.ts","../src/markdown/list-continuation.ts","../src/markdown/indent-list.ts","../src/markdown/wrap-selection.ts","../src/markdown/insert-command.ts","../src/markdown/slash-trigger.ts","../src/markdown/shortcut-label.ts","../src/markdown/caret-coordinates.ts","../src/markdown/useMarkdownKeymap.ts","../src/hooks/useAutoGrow.ts"],"sourcesContent":["export { applyEdit, type Edit } from './markdown/edit';\nexport { lineAt } from './markdown/line-at';\nexport { toggleLinePrefix, type LinePrefix } from './markdown/line-toggles';\nexport { continueList } from './markdown/list-continuation';\nexport { indentList } from './markdown/indent-list';\nexport { wrapSelection, linkPaste, type WrapKind } from './markdown/wrap-selection';\nexport { insertCommand, type InsertableCommand } from './markdown/insert-command';\nexport { shouldOpenPalette } from './markdown/slash-trigger';\nexport { formatShortcut } from './markdown/shortcut-label';\nexport { caretRect, type CaretRect } from './markdown/caret-coordinates';\nexport { useMarkdownKeymap } from './markdown/useMarkdownKeymap';\nexport { useAutoGrow } from './hooks/useAutoGrow';\n","/** A text transform result: the next value and where the selection should land. */\nexport type Edit = { value: string; selectionStart: number; selectionEnd: number };\n\nfunction changedSpan(before: string, after: string) {\n let start = 0;\n const shortest = Math.min(before.length, after.length);\n while (start < shortest && before[start] === after[start]) start += 1;\n\n let endBefore = before.length;\n let endAfter = after.length;\n while (endBefore > start && endAfter > start && before[endBefore - 1] === after[endAfter - 1]) {\n endBefore -= 1;\n endAfter -= 1;\n }\n\n return { start, endBefore, replacement: after.slice(start, endAfter) };\n}\n\n// Writing through execCommand keeps the transform as one entry on the browser's\n// own undo stack. Either path commits el.value rather than the edit's intended\n// value: a browser is trusted to write what it was asked, and if it ever\n// wrote something else, committing our own intent instead would silently\n// diverge React's state from the DOM.\nfunction writeNatively(el: HTMLTextAreaElement, replacement: string): boolean {\n if (typeof document.execCommand !== 'function') return false;\n try {\n return replacement === ''\n ? document.execCommand('delete')\n : document.execCommand('insertText', false, replacement);\n } catch {\n return false;\n }\n}\n\n/** Applies an edit to a textarea through the native undo stack when the browser allows it, so Ctrl+Z still works. */\nexport function applyEdit(\n el: HTMLTextAreaElement,\n edit: Edit,\n commit: (value: string) => void,\n): void {\n if (el.value !== edit.value) {\n const { start, endBefore, replacement } = changedSpan(el.value, edit.value);\n el.setSelectionRange(start, endBefore);\n if (!writeNatively(el, replacement)) {\n el.value = edit.value;\n }\n commit(el.value);\n }\n el.setSelectionRange(edit.selectionStart, edit.selectionEnd);\n}\n","const FENCE = /^ {0,3}(```|~~~)/;\n\nexport type Line = { start: number; end: number; text: string };\n\n/** The line containing the offset, with its start and end offsets and its text (no trailing newline). */\nexport function lineAt(value: string, offset: number): Line {\n const start = value.lastIndexOf('\\n', offset - 1) + 1;\n const next = value.indexOf('\\n', offset);\n const end = next === -1 ? value.length : next;\n return { start, end, text: value.slice(start, end) };\n}\n\nexport function isFenceDelimiter(text: string): boolean {\n return FENCE.test(text);\n}\n\nexport function insideFence(value: string, offset: number): boolean {\n const lines = value.slice(0, offset).split('\\n');\n if (isFenceDelimiter(lines[lines.length - 1])) return true;\n\n let open = false;\n for (let i = 0; i < lines.length - 1; i += 1) {\n if (isFenceDelimiter(lines[i])) open = !open;\n }\n return open;\n}\n","import type { Edit } from './edit';\nimport { insideFence, isFenceDelimiter, lineAt } from './line-at';\n\n/** A line-level markdown prefix: a heading level, a bullet or a numbered item. */\nexport type LinePrefix = 'h1' | 'h2' | 'h3' | 'bullet' | 'ordered';\n\nconst EXISTING = /^(\\s*)(?:#{1,6} |[-*+] \\[[ xX]\\] |[-*+] |\\d+[.)] )?/;\n\nconst PREFIX: Record<LinePrefix, (index: number) => string> = {\n h1: () => '# ',\n h2: () => '## ',\n h3: () => '### ',\n bullet: () => '- ',\n ordered: (index) => `${index + 1}. `,\n};\n\nconst CARRIES: Record<LinePrefix, RegExp> = {\n h1: /^\\s*# /,\n h2: /^\\s*## /,\n h3: /^\\s*### /,\n bullet: /^\\s*[-*+] /,\n ordered: /^\\s*\\d+[.)] /,\n};\n\n/** Adds the prefix to every line in the selection, or removes it when every line already carries it; fenced code is left alone. */\nexport function toggleLinePrefix(\n kind: LinePrefix,\n value: string,\n selectionStart: number,\n selectionEnd: number,\n): Edit {\n const first = lineAt(value, selectionStart);\n const last = lineAt(value, selectionEnd);\n const block = value.slice(first.start, last.end);\n const lines = block.split('\\n');\n\n let offset = first.start;\n const eligible = lines.map((text) => {\n const at = offset;\n offset += text.length + 1;\n return !insideFence(value, at) && !isFenceDelimiter(text);\n });\n\n const carried = lines.every((text, i) => !eligible[i] || CARRIES[kind].test(text));\n\n const rewritten = lines.map((text, i) => {\n if (!eligible[i]) return text;\n const match = EXISTING.exec(text);\n const indent = match?.[1] ?? '';\n const body = text.slice(match?.[0].length ?? 0);\n return carried ? indent + body : indent + PREFIX[kind](i) + body;\n });\n\n const joined = rewritten.join('\\n');\n const next = value.slice(0, first.start) + joined + value.slice(last.end);\n\n if (selectionStart !== selectionEnd) {\n return { value: next, selectionStart: first.start, selectionEnd: first.start + joined.length };\n }\n\n const caret = Math.max(first.start, selectionStart + (rewritten[0].length - lines[0].length));\n return { value: next, selectionStart: caret, selectionEnd: caret };\n}\n","import type { Edit } from './edit';\nimport { insideFence, lineAt, type Line } from './line-at';\n\nexport const LIST_MARKER = /^(\\s*)((?:[-*+] \\[[ xX]\\] )|(?:[-*+] )|(?:\\d+[.)] )|(?:> ))(.*)$/;\n\nconst INDENT = ' ';\n\nfunction nextMarker(marker: string): string {\n const ordered = /^(\\d+)([.)] )$/.exec(marker);\n if (ordered) return `${Number(ordered[1]) + 1}${ordered[2]}`;\n const task = /^([-*+]) \\[[ xX]\\] $/.exec(marker);\n if (task) return `${task[1]} [ ] `;\n return marker;\n}\n\n// Both the dead item on Enter and the caret at the content start on Backspace\n// mean the same thing: climb out one level, or drop the marker at the top.\nfunction climbOut(value: string, line: Line, indent: string, marker: string, content: string): Edit {\n const outdent = indent.length >= INDENT.length;\n\n if (outdent) {\n const text = indent.slice(INDENT.length) + marker + content;\n const at = line.start + indent.length - INDENT.length + marker.length;\n return {\n value: value.slice(0, line.start) + text + value.slice(line.end),\n selectionStart: at,\n selectionEnd: at,\n };\n }\n\n // A stripped marker with only a single newline above it reads to CommonMark\n // as a lazy continuation of the previous list item's paragraph, not a new\n // block, so a blank line is needed whenever that previous line is a list item.\n const leavesList = line.start > 0 && LIST_MARKER.test(lineAt(value, line.start - 1).text);\n const text = leavesList ? `\\n${content}` : content;\n const at = leavesList ? line.start + 1 : line.start;\n\n return {\n value: value.slice(0, line.start) + text + value.slice(line.end),\n selectionStart: at,\n selectionEnd: at,\n };\n}\n\n/** Continues a list item on Enter, or climbs out of the list on an empty item or Backspace at the marker; returns null when the caret is not in a list item or is inside a code fence, so the default key behaviour runs. */\nexport function continueList(\n value: string,\n caret: number,\n key: 'Enter' | 'Backspace',\n): Edit | null {\n if (insideFence(value, caret)) return null;\n\n const line = lineAt(value, caret);\n const match = LIST_MARKER.exec(line.text);\n if (!match) return null;\n\n const [, indent, marker, content] = match;\n\n if (key === 'Backspace') {\n if (caret !== line.start + indent.length + marker.length) return null;\n return climbOut(value, line, indent, marker, content);\n }\n\n if (content.trim() === '') return climbOut(value, line, indent, marker, content);\n\n const inserted = `\\n${indent}${nextMarker(marker)}`;\n const at = caret + inserted.length;\n return {\n value: value.slice(0, caret) + inserted + value.slice(caret),\n selectionStart: at,\n selectionEnd: at,\n };\n}\n","import type { Edit } from './edit';\nimport { insideFence, lineAt } from './line-at';\nimport { LIST_MARKER } from './list-continuation';\n\nconst INDENT = ' ';\n\nfunction indentOf(text: string): number {\n return /^\\s*/.exec(text)?.[0].length ?? 0;\n}\n\n// Indenting more than one level past the line above turns a list item into an\n// indented code block, so the first line of the range is capped against it.\nfunction shift(text: string, direction: 1 | -1, cap: number | undefined): string {\n const current = indentOf(text);\n if (direction === -1) return text.slice(Math.min(current, INDENT.length));\n if (cap !== undefined && current >= cap + INDENT.length) return text;\n return INDENT + text;\n}\n\n/** Indents or outdents the selected list items; returns null when the selection is not entirely list items, is inside a code fence, or cannot move further, so Tab keeps its default behaviour. */\nexport function indentList(\n value: string,\n selectionStart: number,\n selectionEnd: number,\n direction: 1 | -1,\n): Edit | null {\n if (insideFence(value, selectionStart)) return null;\n\n const first = lineAt(value, selectionStart);\n const last = lineAt(value, selectionEnd);\n const block = value.slice(first.start, last.end);\n const lines = block.split('\\n');\n if (!lines.every((text) => LIST_MARKER.test(text))) return null;\n\n const cap = first.start === 0 ? 0 : indentOf(lineAt(value, first.start - 1).text);\n const shifted = lines.map((text, i) => shift(text, direction, i === 0 ? cap : undefined));\n if (shifted.every((text, i) => text === lines[i])) return null;\n\n const joined = shifted.join('\\n');\n return {\n value: value.slice(0, first.start) + joined + value.slice(last.end),\n selectionStart: selectionStart + (shifted[0].length - lines[0].length),\n selectionEnd: selectionEnd + (joined.length - block.length),\n };\n}\n","import type { Edit } from './edit';\n\n/** An inline markdown wrapper: bold, italic, code or a link. */\nexport type WrapKind = 'bold' | 'italic' | 'code' | 'link';\n\nconst DELIMITER: Record<Exclude<WrapKind, 'link'>, string> = {\n bold: '**',\n italic: '_',\n code: '`',\n};\n\nconst WHOLE_LINK = /^\\[([^\\]]*)\\]\\(([^)]*)\\)$/;\nconst BARE_URL = /^https?:\\/\\/[^\\s]+$/;\n\nfunction unwrap(value: string, start: number, end: number, delimiter: string): Edit | null {\n const selected = value.slice(start, end);\n const width = delimiter.length;\n\n if (selected.length >= 2 * width && selected.startsWith(delimiter) && selected.endsWith(delimiter)) {\n const inner = selected.slice(width, selected.length - width);\n return {\n value: value.slice(0, start) + inner + value.slice(end),\n selectionStart: start,\n selectionEnd: start + inner.length,\n };\n }\n\n const outside =\n start >= width &&\n value.slice(start - width, start) === delimiter &&\n value.slice(end, end + width) === delimiter;\n\n if (!outside) return null;\n\n return {\n value: value.slice(0, start - width) + selected + value.slice(end + width),\n selectionStart: start - width,\n selectionEnd: start - width + selected.length,\n };\n}\n\nfunction link(value: string, start: number, end: number): Edit {\n const selected = value.slice(start, end);\n const whole = WHOLE_LINK.exec(selected);\n\n if (whole) {\n const text = whole[1];\n return {\n value: value.slice(0, start) + text + value.slice(end),\n selectionStart: start,\n selectionEnd: start + text.length,\n };\n }\n\n const opened = `[${selected}](`;\n const at = selected === '' ? start + 1 : start + opened.length;\n return {\n value: value.slice(0, start) + opened + ')' + value.slice(end),\n selectionStart: at,\n selectionEnd: at,\n };\n}\n\n/** Wraps the selection in the markers for the kind, or removes them when it is already wrapped. */\nexport function wrapSelection(\n kind: WrapKind,\n value: string,\n selectionStart: number,\n selectionEnd: number,\n): Edit {\n if (kind === 'link') return link(value, selectionStart, selectionEnd);\n\n const delimiter = DELIMITER[kind];\n const stripped = unwrap(value, selectionStart, selectionEnd, delimiter);\n if (stripped) return stripped;\n\n const selected = value.slice(selectionStart, selectionEnd);\n\n if (kind === 'code' && selected.includes('\\n')) {\n const at = selectionStart + 4;\n return {\n value: value.slice(0, selectionStart) + '```\\n' + selected + '\\n```' + value.slice(selectionEnd),\n selectionStart: at,\n selectionEnd: at + selected.length,\n };\n }\n\n const at = selectionStart + delimiter.length;\n return {\n value: value.slice(0, selectionStart) + delimiter + selected + delimiter + value.slice(selectionEnd),\n selectionStart: at,\n selectionEnd: at + selected.length,\n };\n}\n\n/** Turns a pasted URL over a selection into a markdown link; returns null when nothing is selected or the paste is not a bare URL, so the paste goes through unchanged. */\nexport function linkPaste(\n value: string,\n selectionStart: number,\n selectionEnd: number,\n pasted: string,\n): Edit | null {\n if (selectionStart === selectionEnd) return null;\n\n const url = pasted.trim();\n if (!BARE_URL.test(url)) return null;\n\n const inserted = `[${value.slice(selectionStart, selectionEnd)}](${url})`;\n const at = selectionStart + inserted.length;\n return {\n value: value.slice(0, selectionStart) + inserted + value.slice(selectionEnd),\n selectionStart: at,\n selectionEnd: at,\n };\n}\n","import type { Edit } from './edit';\nimport { lineAt } from './line-at';\n\n/** The fields `insertCommand` needs. `TextCommand` satisfies this structurally. */\nexport type InsertableCommand = {\n insert: string;\n caretOffset: number;\n selectLength?: number;\n separate: boolean;\n};\n\n/** Inserts a command's markdown at the caret, starting a new line (or a blank line if it asks) when the caret is mid-line. */\nexport function insertCommand(command: InsertableCommand, value: string, caret: number): Edit {\n const line = lineAt(value, caret);\n const before = value.slice(line.start, caret).replace(/[ \\t]+$/, '');\n const cut = line.start + before.length;\n const prefix = before.trim() === '' ? '' : command.separate ? '\\n\\n' : '\\n';\n const at = cut + prefix.length + command.caretOffset;\n\n return {\n value: value.slice(0, cut) + prefix + command.insert + value.slice(caret),\n selectionStart: at,\n selectionEnd: at + (command.selectLength ?? 0),\n };\n}\n","import { insideFence } from './line-at';\n\n/** Whether typing \"/\" here should open the command palette: only at a word start, with no selection and outside a code fence. */\nexport function shouldOpenPalette(\n value: string,\n selectionStart: number,\n selectionEnd: number,\n): boolean {\n if (selectionStart !== selectionEnd) return false;\n if (insideFence(value, selectionStart)) return false;\n if (selectionStart === 0) return true;\n\n const previous = value[selectionStart - 1];\n return previous === '\\n' || previous === ' ' || previous === '\\t';\n}\n","const MAC_LABEL: Record<string, string> = { mod: '⌘', alt: '⌥', shift: '⇧' };\nconst OTHER_LABEL: Record<string, string> = { mod: 'Ctrl', alt: 'Alt', shift: 'Shift' };\n\nfunction onMac(): boolean {\n if (typeof navigator === 'undefined') return false;\n return /Mac|iPhone|iPad/.test(navigator.userAgent);\n}\n\n/** Renders a \"mod+shift+b\" style hint as the platform's own label, such as ⌘⇧B on a Mac and Ctrl+Shift+B elsewhere. */\nexport function formatShortcut(hint: string): string {\n const mac = onMac();\n const labels = mac ? MAC_LABEL : OTHER_LABEL;\n const parts = hint.split('+').map((part) => labels[part] ?? part.toUpperCase());\n return parts.join(mac ? '' : '+');\n}\n","type CopiedProperty =\n | 'boxSizing'\n | 'fontFamily'\n | 'fontSize'\n | 'fontStyle'\n | 'fontWeight'\n | 'letterSpacing'\n | 'lineHeight'\n | 'paddingBottom'\n | 'paddingLeft'\n | 'paddingRight'\n | 'paddingTop'\n | 'textIndent'\n | 'textTransform'\n | 'wordBreak'\n | 'wordSpacing'\n | 'borderBottomWidth'\n | 'borderLeftWidth'\n | 'borderRightWidth'\n | 'borderTopWidth';\n\nconst COPIED: CopiedProperty[] = [\n 'boxSizing',\n 'fontFamily',\n 'fontSize',\n 'fontStyle',\n 'fontWeight',\n 'letterSpacing',\n 'lineHeight',\n 'paddingBottom',\n 'paddingLeft',\n 'paddingRight',\n 'paddingTop',\n 'textIndent',\n 'textTransform',\n 'wordBreak',\n 'wordSpacing',\n 'borderBottomWidth',\n 'borderLeftWidth',\n 'borderRightWidth',\n 'borderTopWidth',\n];\n\n/** The caret's position and line height, in pixels relative to the textarea's top left corner. */\nexport type CaretRect = { top: number; left: number; height: number };\n\n/** A textarea exposes no caret geometry, so the text before the caret is laid\n * out again in a hidden div that copies the box's own metrics, and a marker at\n * the end of it is measured. Returns null when the textarea is not laid out\n * (hidden or detached), because there is nothing to measure. */\nexport function caretRect(el: HTMLTextAreaElement): CaretRect | null {\n if (el.offsetWidth === 0) return null;\n\n const style = window.getComputedStyle(el);\n const mirror = document.createElement('div');\n for (const property of COPIED) mirror.style[property] = style[property];\n mirror.style.position = 'absolute';\n mirror.style.top = '0';\n mirror.style.left = '-9999px';\n mirror.style.visibility = 'hidden';\n mirror.style.whiteSpace = 'pre-wrap';\n mirror.style.overflowWrap = 'break-word';\n mirror.style.width = `${el.clientWidth}px`;\n mirror.textContent = el.value.slice(0, el.selectionStart);\n\n const marker = document.createElement('span');\n marker.textContent = '\\u200b';\n mirror.appendChild(marker);\n document.body.appendChild(mirror);\n\n const top = marker.offsetTop;\n const left = marker.offsetLeft;\n const height = marker.offsetHeight || Number.parseFloat(style.lineHeight) || 0;\n mirror.remove();\n\n if (height === 0) return null;\n return { top: top - el.scrollTop, left, height };\n}\n","import * as React from 'react';\nimport { applyEdit, type Edit } from './edit';\nimport { indentList } from './indent-list';\nimport { toggleLinePrefix, type LinePrefix } from './line-toggles';\nimport { continueList } from './list-continuation';\nimport { shouldOpenPalette } from './slash-trigger';\nimport { linkPaste, wrapSelection, type WrapKind } from './wrap-selection';\n\nconst WRAP: Record<string, WrapKind> = { b: 'bold', i: 'italic', e: 'code', k: 'link' };\n\n// Shift turns the 8 key into \"*\", so the physical code is the only stable match.\nconst HEADING: Record<string, LinePrefix> = { Digit1: 'h1', Digit2: 'h2', Digit3: 'h3' };\nconst LIST: Record<string, LinePrefix> = { Digit7: 'ordered', Digit8: 'bullet' };\n\n/** Keyboard and paste handlers that give a plain textarea markdown shortcuts, list continuation, Tab indent and the slash palette trigger. */\nexport function useMarkdownKeymap({\n commit,\n onOpenPalette,\n}: {\n commit: (markdown: string) => void;\n onOpenPalette: () => void;\n}) {\n const onKeyDown = React.useCallback(\n (event: React.KeyboardEvent<HTMLTextAreaElement>) => {\n const el = event.currentTarget;\n const { value, selectionStart, selectionEnd } = el;\n const mod = event.metaKey || event.ctrlKey;\n\n const run = (edit: Edit | null) => {\n if (!edit) return;\n event.preventDefault();\n applyEdit(el, edit, commit);\n };\n\n if (mod && event.altKey) {\n const kind = HEADING[event.code];\n if (kind) run(toggleLinePrefix(kind, value, selectionStart, selectionEnd));\n return;\n }\n\n if (mod && event.shiftKey) {\n const kind = LIST[event.code];\n if (kind) run(toggleLinePrefix(kind, value, selectionStart, selectionEnd));\n return;\n }\n\n if (mod) {\n const kind = WRAP[event.key.toLowerCase()];\n if (kind) run(wrapSelection(kind, value, selectionStart, selectionEnd));\n return;\n }\n\n if (event.key === '/') {\n if (!shouldOpenPalette(value, selectionStart, selectionEnd)) return;\n event.preventDefault();\n onOpenPalette();\n return;\n }\n\n if (event.key === 'Enter' && !event.shiftKey) {\n run(continueList(value, selectionStart, 'Enter'));\n return;\n }\n\n if (event.key === 'Backspace' && selectionStart === selectionEnd) {\n run(continueList(value, selectionStart, 'Backspace'));\n return;\n }\n\n if (event.key === 'Tab') {\n run(indentList(value, selectionStart, selectionEnd, event.shiftKey ? -1 : 1));\n }\n },\n [commit, onOpenPalette],\n );\n\n const onPaste = React.useCallback(\n (event: React.ClipboardEvent<HTMLTextAreaElement>) => {\n const el = event.currentTarget;\n const edit = linkPaste(\n el.value,\n el.selectionStart,\n el.selectionEnd,\n event.clipboardData.getData('text/plain'),\n );\n if (!edit) return;\n event.preventDefault();\n applyEdit(el, edit, commit);\n },\n [commit],\n );\n\n return { onKeyDown, onPaste };\n}\n","import * as React from 'react';\n\n/** Height follows the content, and is recomputed when the box gets narrower or\n * wider, because a re-wrap changes the line count without changing the value.\n * Only a width change refits: reacting to the height this hook just set would\n * feed the observer its own output. */\nexport function useAutoGrow<T extends HTMLTextAreaElement>(value: string) {\n const ref = React.useRef<T>(null);\n const lastWidth = React.useRef<number | null>(null);\n\n // Measuring means collapsing to \"auto\" first, which shrinks the page enough that\n // the browser clamps the scroll position, and growing back does not undo that.\n // Both steps run before paint, so restoring the scroll here is not visible.\n const fit = React.useCallback(() => {\n const el = ref.current;\n if (!el) return;\n const scroller = document.scrollingElement ?? document.documentElement;\n const scrollTop = scroller.scrollTop;\n el.style.height = 'auto';\n if (el.scrollHeight > 0) el.style.height = `${el.scrollHeight}px`;\n if (scroller.scrollTop !== scrollTop) scroller.scrollTop = scrollTop;\n }, []);\n\n React.useLayoutEffect(fit, [fit, value]);\n\n React.useEffect(() => {\n const el = ref.current;\n if (!el || typeof ResizeObserver === 'undefined') return;\n const observer = new ResizeObserver(([entry]) => {\n const width = entry.contentRect.width;\n if (width === lastWidth.current) return;\n lastWidth.current = width;\n fit();\n });\n observer.observe(el);\n return () => observer.disconnect();\n }, [fit]);\n\n return ref;\n}\n"],"mappings":"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","names":["text","at","INDENT","at","React"]}
1
+ {"version":3,"sources":["../src/headless.ts","../src/markdown/edit.ts","../src/markdown/line-at.ts","../src/markdown/line-toggles.ts","../src/markdown/list-continuation.ts","../src/markdown/indent-list.ts","../src/markdown/wrap-selection.ts","../src/markdown/insert-command.ts","../src/markdown/slash-trigger.ts","../src/markdown/platform.ts","../src/markdown/shortcut-label.ts","../src/markdown/caret-coordinates.ts","../src/markdown/useMarkdownKeymap.ts","../src/hooks/useAutoGrow.ts","../src/hooks/use-isomorphic-layout-effect.ts"],"sourcesContent":["export { applyEdit, type Edit } from './markdown/edit';\nexport { lineAt } from './markdown/line-at';\nexport { toggleLinePrefix, type LinePrefix } from './markdown/line-toggles';\nexport { continueList } from './markdown/list-continuation';\nexport { indentList } from './markdown/indent-list';\nexport { wrapSelection, linkPaste, type WrapKind } from './markdown/wrap-selection';\nexport { insertCommand, type InsertableCommand } from './markdown/insert-command';\nexport { shouldOpenPalette } from './markdown/slash-trigger';\nexport { formatShortcut } from './markdown/shortcut-label';\nexport { caretRect, type CaretRect } from './markdown/caret-coordinates';\nexport { useMarkdownKeymap } from './markdown/useMarkdownKeymap';\nexport { useAutoGrow } from './hooks/useAutoGrow';\n","/** A text transform result: the next value and where the selection should land. */\nexport type Edit = { value: string; selectionStart: number; selectionEnd: number };\n\nfunction changedSpan(before: string, after: string) {\n let start = 0;\n const shortest = Math.min(before.length, after.length);\n while (start < shortest && before[start] === after[start]) start += 1;\n\n let endBefore = before.length;\n let endAfter = after.length;\n while (endBefore > start && endAfter > start && before[endBefore - 1] === after[endAfter - 1]) {\n endBefore -= 1;\n endAfter -= 1;\n }\n\n return { start, endBefore, replacement: after.slice(start, endAfter) };\n}\n\n// Writing through execCommand keeps the transform as one entry on the browser's\n// own undo stack. Either path commits el.value rather than the edit's intended\n// value: a browser is trusted to write what it was asked, and if it ever\n// wrote something else, committing our own intent instead would silently\n// diverge React's state from the DOM.\nfunction writeNatively(el: HTMLTextAreaElement, replacement: string): boolean {\n if (typeof document.execCommand !== 'function') return false;\n try {\n return replacement === ''\n ? document.execCommand('delete')\n : document.execCommand('insertText', false, replacement);\n } catch {\n return false;\n }\n}\n\n/** Applies an edit to a textarea through the native undo stack when the browser allows it, so Ctrl+Z still works. */\nexport function applyEdit(\n el: HTMLTextAreaElement,\n edit: Edit,\n commit: (value: string) => void,\n): void {\n if (el.value !== edit.value) {\n const { start, endBefore, replacement } = changedSpan(el.value, edit.value);\n el.setSelectionRange(start, endBefore);\n if (!writeNatively(el, replacement)) {\n el.value = edit.value;\n }\n commit(el.value);\n }\n el.setSelectionRange(edit.selectionStart, edit.selectionEnd);\n}\n","const FENCE = /^ {0,3}(```|~~~)/;\n\nexport type Line = { start: number; end: number; text: string };\n\n/** The line containing the offset, with its start and end offsets and its text (no trailing newline). */\nexport function lineAt(value: string, offset: number): Line {\n const start = value.lastIndexOf('\\n', offset - 1) + 1;\n const next = value.indexOf('\\n', offset);\n const end = next === -1 ? value.length : next;\n return { start, end, text: value.slice(start, end) };\n}\n\nexport function isFenceDelimiter(text: string): boolean {\n return FENCE.test(text);\n}\n\nexport function insideFence(value: string, offset: number): boolean {\n const lines = value.slice(0, offset).split('\\n');\n if (isFenceDelimiter(lines[lines.length - 1])) return true;\n\n let open = false;\n for (let i = 0; i < lines.length - 1; i += 1) {\n if (isFenceDelimiter(lines[i])) open = !open;\n }\n return open;\n}\n","import type { Edit } from './edit';\nimport { insideFence, isFenceDelimiter, lineAt } from './line-at';\n\n/** A line-level markdown prefix: a heading level, a bullet or a numbered item. */\nexport type LinePrefix = 'h1' | 'h2' | 'h3' | 'bullet' | 'ordered';\n\nconst EXISTING = /^(\\s*)(?:#{1,6} |[-*+] \\[[ xX]\\] |[-*+] |\\d+[.)] )?/;\n\nconst PREFIX: Record<LinePrefix, (index: number) => string> = {\n h1: () => '# ',\n h2: () => '## ',\n h3: () => '### ',\n bullet: () => '- ',\n ordered: (index) => `${index + 1}. `,\n};\n\nconst CARRIES: Record<LinePrefix, RegExp> = {\n h1: /^\\s*# /,\n h2: /^\\s*## /,\n h3: /^\\s*### /,\n bullet: /^\\s*[-*+] /,\n ordered: /^\\s*\\d+[.)] /,\n};\n\n/** Adds the prefix to every line in the selection, or removes it when every line already carries it; fenced code is left alone. */\nexport function toggleLinePrefix(\n kind: LinePrefix,\n value: string,\n selectionStart: number,\n selectionEnd: number,\n): Edit {\n const first = lineAt(value, selectionStart);\n const last = lineAt(value, selectionEnd);\n const block = value.slice(first.start, last.end);\n const lines = block.split('\\n');\n\n let offset = first.start;\n const eligible = lines.map((text) => {\n const at = offset;\n offset += text.length + 1;\n return !insideFence(value, at) && !isFenceDelimiter(text);\n });\n\n const carried = lines.every((text, i) => !eligible[i] || CARRIES[kind].test(text));\n\n const rewritten = lines.map((text, i) => {\n if (!eligible[i]) return text;\n const match = EXISTING.exec(text);\n const indent = match?.[1] ?? '';\n const body = text.slice(match?.[0].length ?? 0);\n return carried ? indent + body : indent + PREFIX[kind](i) + body;\n });\n\n const joined = rewritten.join('\\n');\n const next = value.slice(0, first.start) + joined + value.slice(last.end);\n\n if (selectionStart !== selectionEnd) {\n return { value: next, selectionStart: first.start, selectionEnd: first.start + joined.length };\n }\n\n const caret = Math.max(first.start, selectionStart + (rewritten[0].length - lines[0].length));\n return { value: next, selectionStart: caret, selectionEnd: caret };\n}\n","import type { Edit } from './edit';\nimport { insideFence, lineAt, type Line } from './line-at';\n\nexport const LIST_MARKER = /^(\\s*)((?:[-*+] \\[[ xX]\\] )|(?:[-*+] )|(?:\\d+[.)] )|(?:> ))(.*)$/;\n\nconst INDENT = ' ';\n\nfunction nextMarker(marker: string): string {\n const ordered = /^(\\d+)([.)] )$/.exec(marker);\n if (ordered) return `${Number(ordered[1]) + 1}${ordered[2]}`;\n const task = /^([-*+]) \\[[ xX]\\] $/.exec(marker);\n if (task) return `${task[1]} [ ] `;\n return marker;\n}\n\n// Both the dead item on Enter and the caret at the content start on Backspace\n// mean the same thing: climb out one level, or drop the marker at the top.\nfunction climbOut(value: string, line: Line, indent: string, marker: string, content: string): Edit {\n const outdent = indent.length >= INDENT.length;\n\n if (outdent) {\n const text = indent.slice(INDENT.length) + marker + content;\n const at = line.start + indent.length - INDENT.length + marker.length;\n return {\n value: value.slice(0, line.start) + text + value.slice(line.end),\n selectionStart: at,\n selectionEnd: at,\n };\n }\n\n // A stripped marker with only a single newline above it reads to CommonMark\n // as a lazy continuation of the previous list item's paragraph, not a new\n // block, so a blank line is needed whenever that previous line is a list item.\n const leavesList = line.start > 0 && LIST_MARKER.test(lineAt(value, line.start - 1).text);\n const text = leavesList ? `\\n${content}` : content;\n const at = leavesList ? line.start + 1 : line.start;\n\n return {\n value: value.slice(0, line.start) + text + value.slice(line.end),\n selectionStart: at,\n selectionEnd: at,\n };\n}\n\n/** Continues a list item on Enter, or climbs out of the list on an empty item or Backspace at the marker; returns null when the caret is not in a list item or is inside a code fence, so the default key behaviour runs. */\nexport function continueList(\n value: string,\n caret: number,\n key: 'Enter' | 'Backspace',\n): Edit | null {\n if (insideFence(value, caret)) return null;\n\n const line = lineAt(value, caret);\n const match = LIST_MARKER.exec(line.text);\n if (!match) return null;\n\n const [, indent, marker, content] = match;\n\n if (key === 'Backspace') {\n if (caret !== line.start + indent.length + marker.length) return null;\n return climbOut(value, line, indent, marker, content);\n }\n\n if (content.trim() === '') return climbOut(value, line, indent, marker, content);\n\n const inserted = `\\n${indent}${nextMarker(marker)}`;\n const at = caret + inserted.length;\n return {\n value: value.slice(0, caret) + inserted + value.slice(caret),\n selectionStart: at,\n selectionEnd: at,\n };\n}\n","import type { Edit } from './edit';\nimport { insideFence, lineAt } from './line-at';\nimport { LIST_MARKER } from './list-continuation';\n\nconst INDENT = ' ';\n\nfunction indentOf(text: string): number {\n return /^\\s*/.exec(text)?.[0].length ?? 0;\n}\n\n// Indenting more than one level past the line above turns a list item into an\n// indented code block, so the first line of the range is capped against it.\nfunction shift(text: string, direction: 1 | -1, cap: number | undefined): string {\n const current = indentOf(text);\n if (direction === -1) return text.slice(Math.min(current, INDENT.length));\n if (cap !== undefined && current >= cap + INDENT.length) return text;\n return INDENT + text;\n}\n\n/** Indents or outdents the selected list items; returns null when the selection is not entirely list items, is inside a code fence, or cannot move further, so Tab keeps its default behaviour. */\nexport function indentList(\n value: string,\n selectionStart: number,\n selectionEnd: number,\n direction: 1 | -1,\n): Edit | null {\n if (insideFence(value, selectionStart)) return null;\n\n const first = lineAt(value, selectionStart);\n const last = lineAt(value, selectionEnd);\n const block = value.slice(first.start, last.end);\n const lines = block.split('\\n');\n if (!lines.every((text) => LIST_MARKER.test(text))) return null;\n\n const cap = first.start === 0 ? 0 : indentOf(lineAt(value, first.start - 1).text);\n const shifted = lines.map((text, i) => shift(text, direction, i === 0 ? cap : undefined));\n if (shifted.every((text, i) => text === lines[i])) return null;\n\n const joined = shifted.join('\\n');\n return {\n value: value.slice(0, first.start) + joined + value.slice(last.end),\n selectionStart: selectionStart + (shifted[0].length - lines[0].length),\n selectionEnd: selectionEnd + (joined.length - block.length),\n };\n}\n","import type { Edit } from './edit';\n\n/** An inline markdown wrapper: bold, italic, code or a link. */\nexport type WrapKind = 'bold' | 'italic' | 'code' | 'link';\n\nconst DELIMITER: Record<Exclude<WrapKind, 'link'>, string> = {\n bold: '**',\n italic: '_',\n code: '`',\n};\n\nconst WHOLE_LINK = /^\\[([^\\]]*)\\]\\(([^)]*)\\)$/;\nconst BARE_URL = /^https?:\\/\\/[^\\s]+$/;\n\nfunction unwrap(value: string, start: number, end: number, delimiter: string): Edit | null {\n const selected = value.slice(start, end);\n const width = delimiter.length;\n\n if (selected.length >= 2 * width && selected.startsWith(delimiter) && selected.endsWith(delimiter)) {\n const inner = selected.slice(width, selected.length - width);\n return {\n value: value.slice(0, start) + inner + value.slice(end),\n selectionStart: start,\n selectionEnd: start + inner.length,\n };\n }\n\n const outside =\n start >= width &&\n value.slice(start - width, start) === delimiter &&\n value.slice(end, end + width) === delimiter;\n\n if (!outside) return null;\n\n return {\n value: value.slice(0, start - width) + selected + value.slice(end + width),\n selectionStart: start - width,\n selectionEnd: start - width + selected.length,\n };\n}\n\nfunction link(value: string, start: number, end: number): Edit {\n const selected = value.slice(start, end);\n const whole = WHOLE_LINK.exec(selected);\n\n if (whole) {\n const text = whole[1];\n return {\n value: value.slice(0, start) + text + value.slice(end),\n selectionStart: start,\n selectionEnd: start + text.length,\n };\n }\n\n const opened = `[${selected}](`;\n const at = selected === '' ? start + 1 : start + opened.length;\n return {\n value: value.slice(0, start) + opened + ')' + value.slice(end),\n selectionStart: at,\n selectionEnd: at,\n };\n}\n\n/** Wraps the selection in the markers for the kind, or removes them when it is already wrapped. */\nexport function wrapSelection(\n kind: WrapKind,\n value: string,\n selectionStart: number,\n selectionEnd: number,\n): Edit {\n if (kind === 'link') return link(value, selectionStart, selectionEnd);\n\n const delimiter = DELIMITER[kind];\n const stripped = unwrap(value, selectionStart, selectionEnd, delimiter);\n if (stripped) return stripped;\n\n const selected = value.slice(selectionStart, selectionEnd);\n\n if (kind === 'code' && selected.includes('\\n')) {\n const at = selectionStart + 4;\n return {\n value: value.slice(0, selectionStart) + '```\\n' + selected + '\\n```' + value.slice(selectionEnd),\n selectionStart: at,\n selectionEnd: at + selected.length,\n };\n }\n\n const at = selectionStart + delimiter.length;\n return {\n value: value.slice(0, selectionStart) + delimiter + selected + delimiter + value.slice(selectionEnd),\n selectionStart: at,\n selectionEnd: at + selected.length,\n };\n}\n\n/** Turns a pasted URL over a selection into a markdown link; returns null when nothing is selected or the paste is not a bare URL, so the paste goes through unchanged. */\nexport function linkPaste(\n value: string,\n selectionStart: number,\n selectionEnd: number,\n pasted: string,\n): Edit | null {\n if (selectionStart === selectionEnd) return null;\n\n const url = pasted.trim();\n if (!BARE_URL.test(url)) return null;\n\n const inserted = `[${value.slice(selectionStart, selectionEnd)}](${url})`;\n const at = selectionStart + inserted.length;\n return {\n value: value.slice(0, selectionStart) + inserted + value.slice(selectionEnd),\n selectionStart: at,\n selectionEnd: at,\n };\n}\n","import type { Edit } from './edit';\nimport { lineAt } from './line-at';\n\n/** The fields `insertCommand` needs. `TextCommand` satisfies this structurally. */\nexport type InsertableCommand = {\n insert: string;\n caretOffset: number;\n selectLength?: number;\n separate: boolean;\n};\n\n/** Inserts a command's markdown at the caret, starting a new line (or a blank line if it asks) when the caret is mid-line. */\nexport function insertCommand(command: InsertableCommand, value: string, caret: number): Edit {\n const line = lineAt(value, caret);\n const before = value.slice(line.start, caret).replace(/[ \\t]+$/, '');\n const cut = line.start + before.length;\n const prefix = before.trim() === '' ? '' : command.separate ? '\\n\\n' : '\\n';\n const at = cut + prefix.length + command.caretOffset;\n\n return {\n value: value.slice(0, cut) + prefix + command.insert + value.slice(caret),\n selectionStart: at,\n selectionEnd: at + (command.selectLength ?? 0),\n };\n}\n","import { insideFence } from './line-at';\n\n/** Whether typing \"/\" here should open the command palette: only at a word start, with no selection and outside a code fence. */\nexport function shouldOpenPalette(\n value: string,\n selectionStart: number,\n selectionEnd: number,\n): boolean {\n if (selectionStart !== selectionEnd) return false;\n if (insideFence(value, selectionStart)) return false;\n if (selectionStart === 0) return true;\n\n const previous = value[selectionStart - 1];\n return previous === '\\n' || previous === ' ' || previous === '\\t';\n}\n","type NavigatorWithData = Navigator & { userAgentData?: { platform?: string } };\n\n/** True on macOS and iOS, where the shortcut modifier is Command rather than Ctrl. Read on each call so a stubbed navigator is honoured. */\nexport function isMac(): boolean {\n if (typeof navigator === 'undefined') return false;\n const { userAgentData, platform } = navigator as NavigatorWithData;\n return /mac|iphone|ipad/i.test(userAgentData?.platform || platform || '');\n}\n","import { isMac } from './platform';\n\nconst MAC_LABEL: Record<string, string> = { mod: '⌘', alt: '⌥', shift: '⇧' };\nconst OTHER_LABEL: Record<string, string> = { mod: 'Ctrl', alt: 'Alt', shift: 'Shift' };\n\n/** Renders a \"mod+shift+b\" style hint as the platform's own label, such as ⌘⇧B on a Mac and Ctrl+Shift+B elsewhere. */\nexport function formatShortcut(hint: string): string {\n const mac = isMac();\n const labels = mac ? MAC_LABEL : OTHER_LABEL;\n const parts = hint.split('+').map((part) => labels[part] ?? part.toUpperCase());\n return parts.join(mac ? '' : '+');\n}\n","type CopiedProperty =\n | 'boxSizing'\n | 'fontFamily'\n | 'fontSize'\n | 'fontStyle'\n | 'fontWeight'\n | 'letterSpacing'\n | 'lineHeight'\n | 'paddingBottom'\n | 'paddingLeft'\n | 'paddingRight'\n | 'paddingTop'\n | 'textIndent'\n | 'textTransform'\n | 'wordBreak'\n | 'wordSpacing'\n | 'borderBottomWidth'\n | 'borderLeftWidth'\n | 'borderRightWidth'\n | 'borderTopWidth';\n\nconst COPIED: CopiedProperty[] = [\n 'boxSizing',\n 'fontFamily',\n 'fontSize',\n 'fontStyle',\n 'fontWeight',\n 'letterSpacing',\n 'lineHeight',\n 'paddingBottom',\n 'paddingLeft',\n 'paddingRight',\n 'paddingTop',\n 'textIndent',\n 'textTransform',\n 'wordBreak',\n 'wordSpacing',\n 'borderBottomWidth',\n 'borderLeftWidth',\n 'borderRightWidth',\n 'borderTopWidth',\n];\n\n/** The caret's position and line height, in pixels relative to the textarea's top left corner. */\nexport type CaretRect = { top: number; left: number; height: number };\n\n/** A textarea exposes no caret geometry, so the text before the caret is laid\n * out again in a hidden div that copies the box's own metrics, and a marker at\n * the end of it is measured. Returns null when the textarea is not laid out\n * (hidden or detached), because there is nothing to measure. */\nexport function caretRect(el: HTMLTextAreaElement): CaretRect | null {\n if (el.offsetWidth === 0) return null;\n\n const style = window.getComputedStyle(el);\n const mirror = document.createElement('div');\n for (const property of COPIED) mirror.style[property] = style[property];\n mirror.style.position = 'absolute';\n mirror.style.top = '0';\n mirror.style.left = '-9999px';\n mirror.style.visibility = 'hidden';\n mirror.style.whiteSpace = 'pre-wrap';\n mirror.style.overflowWrap = 'break-word';\n mirror.style.width = `${el.clientWidth}px`;\n mirror.textContent = el.value.slice(0, el.selectionStart);\n\n const marker = document.createElement('span');\n marker.textContent = '\\u200b';\n mirror.appendChild(marker);\n document.body.appendChild(mirror);\n\n const top = marker.offsetTop;\n const left = marker.offsetLeft;\n const height = marker.offsetHeight || Number.parseFloat(style.lineHeight) || 0;\n mirror.remove();\n\n if (height === 0) return null;\n return { top: top - el.scrollTop, left, height };\n}\n","import * as React from 'react';\nimport { applyEdit, type Edit } from './edit';\nimport { indentList } from './indent-list';\nimport { toggleLinePrefix, type LinePrefix } from './line-toggles';\nimport { continueList } from './list-continuation';\nimport { isMac } from './platform';\nimport { shouldOpenPalette } from './slash-trigger';\nimport { linkPaste, wrapSelection, type WrapKind } from './wrap-selection';\n\nconst WRAP: Record<string, WrapKind> = { b: 'bold', i: 'italic', e: 'code', k: 'link' };\n\n// Shift turns the 8 key into \"*\", so the physical code is the only stable match.\nconst HEADING: Record<string, LinePrefix> = { Digit1: 'h1', Digit2: 'h2', Digit3: 'h3' };\nconst LIST: Record<string, LinePrefix> = { Digit7: 'ordered', Digit8: 'bullet' };\n\n/** Keyboard and paste handlers that give a plain textarea markdown shortcuts, list continuation, Tab indent and the slash palette trigger. */\nexport function useMarkdownKeymap({\n commit,\n onOpenPalette,\n}: {\n commit: (markdown: string) => void;\n onOpenPalette: () => void;\n}) {\n const onKeyDown = React.useCallback(\n (event: React.KeyboardEvent<HTMLTextAreaElement>) => {\n // Safari reports keyCode 229 for the keydown that follows compositionend.\n if (event.nativeEvent.isComposing || event.keyCode === 229) return;\n\n const el = event.currentTarget;\n const { value, selectionStart, selectionEnd } = el;\n const mod = isMac() ? event.metaKey : event.ctrlKey;\n\n const run = (edit: Edit | null) => {\n if (!edit) return;\n event.preventDefault();\n applyEdit(el, edit, commit);\n };\n\n if (mod && event.altKey) {\n const kind = HEADING[event.code];\n if (kind) run(toggleLinePrefix(kind, value, selectionStart, selectionEnd));\n return;\n }\n\n if (mod && event.shiftKey) {\n const kind = LIST[event.code];\n if (kind) run(toggleLinePrefix(kind, value, selectionStart, selectionEnd));\n return;\n }\n\n if (mod) {\n const kind = WRAP[event.key.toLowerCase()];\n if (kind) run(wrapSelection(kind, value, selectionStart, selectionEnd));\n return;\n }\n\n if (event.key === '/') {\n if (!shouldOpenPalette(value, selectionStart, selectionEnd)) return;\n event.preventDefault();\n onOpenPalette();\n return;\n }\n\n if (event.key === 'Enter' && !event.shiftKey) {\n run(continueList(value, selectionStart, 'Enter'));\n return;\n }\n\n if (event.key === 'Backspace' && selectionStart === selectionEnd) {\n run(continueList(value, selectionStart, 'Backspace'));\n return;\n }\n\n if (event.key === 'Tab') {\n run(indentList(value, selectionStart, selectionEnd, event.shiftKey ? -1 : 1));\n }\n },\n [commit, onOpenPalette],\n );\n\n const onPaste = React.useCallback(\n (event: React.ClipboardEvent<HTMLTextAreaElement>) => {\n const el = event.currentTarget;\n const edit = linkPaste(\n el.value,\n el.selectionStart,\n el.selectionEnd,\n event.clipboardData.getData('text/plain'),\n );\n if (!edit) return;\n event.preventDefault();\n applyEdit(el, edit, commit);\n },\n [commit],\n );\n\n return { onKeyDown, onPaste };\n}\n","import * as React from 'react';\nimport { useIsomorphicLayoutEffect } from './use-isomorphic-layout-effect';\n\n/** Height follows the content, and is recomputed when the box gets narrower or\n * wider, because a re-wrap changes the line count without changing the value.\n * Only a width change refits: reacting to the height this hook just set would\n * feed the observer its own output. */\nexport function useAutoGrow<T extends HTMLTextAreaElement>(value: string): React.RefObject<T> {\n const ref = React.useRef<T>(null);\n const lastWidth = React.useRef<number | null>(null);\n\n // Measuring means collapsing to \"auto\" first, which shrinks the page enough that\n // the browser clamps the scroll position, and growing back does not undo that.\n // Both steps run before paint, so restoring the scroll here is not visible.\n const fit = React.useCallback(() => {\n const el = ref.current;\n if (!el) return;\n const scroller = document.scrollingElement ?? document.documentElement;\n const scrollTop = scroller.scrollTop;\n el.style.height = 'auto';\n if (el.scrollHeight > 0) el.style.height = `${el.scrollHeight}px`;\n if (scroller.scrollTop !== scrollTop) scroller.scrollTop = scrollTop;\n }, []);\n\n useIsomorphicLayoutEffect(fit, [fit, value]);\n\n React.useEffect(() => {\n const el = ref.current;\n if (!el || typeof ResizeObserver === 'undefined') return;\n const observer = new ResizeObserver(([entry]) => {\n const width = entry.contentRect.width;\n if (width === lastWidth.current) return;\n lastWidth.current = width;\n fit();\n });\n observer.observe(el);\n return () => observer.disconnect();\n }, [fit]);\n\n // React 18's types refuse `RefObject<T | null>` on a `ref` prop, and `RefObject<T>` fits both majors.\n return ref as React.RefObject<T>;\n}\n","import * as React from 'react';\n\nexport const useIsomorphicLayoutEffect = typeof document === 'undefined' ? React.useEffect : React.useLayoutEffect;\n"],"mappings":"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","names":["text","at","INDENT","at","React","React"]}
@@ -76,6 +76,6 @@ declare function useMarkdownKeymap({ commit, onOpenPalette, }: {
76
76
  * wider, because a re-wrap changes the line count without changing the value.
77
77
  * Only a width change refits: reacting to the height this hook just set would
78
78
  * feed the observer its own output. */
79
- declare function useAutoGrow<T extends HTMLTextAreaElement>(value: string): React.RefObject<T | null>;
79
+ declare function useAutoGrow<T extends HTMLTextAreaElement>(value: string): React.RefObject<T>;
80
80
 
81
81
  export { type CaretRect, type Edit, type InsertableCommand, type LinePrefix, type WrapKind, applyEdit, caretRect, continueList, formatShortcut, indentList, insertCommand, lineAt, linkPaste, shouldOpenPalette, toggleLinePrefix, useAutoGrow, useMarkdownKeymap, wrapSelection };
@@ -76,6 +76,6 @@ declare function useMarkdownKeymap({ commit, onOpenPalette, }: {
76
76
  * wider, because a re-wrap changes the line count without changing the value.
77
77
  * Only a width change refits: reacting to the height this hook just set would
78
78
  * feed the observer its own output. */
79
- declare function useAutoGrow<T extends HTMLTextAreaElement>(value: string): React.RefObject<T | null>;
79
+ declare function useAutoGrow<T extends HTMLTextAreaElement>(value: string): React.RefObject<T>;
80
80
 
81
81
  export { type CaretRect, type Edit, type InsertableCommand, type LinePrefix, type WrapKind, applyEdit, caretRect, continueList, formatShortcut, indentList, insertCommand, lineAt, linkPaste, shouldOpenPalette, toggleLinePrefix, useAutoGrow, useMarkdownKeymap, wrapSelection };
package/dist/headless.js CHANGED
@@ -280,15 +280,18 @@ function shouldOpenPalette(value, selectionStart, selectionEnd) {
280
280
  return previous === "\n" || previous === " " || previous === " ";
281
281
  }
282
282
 
283
+ // src/markdown/platform.ts
284
+ function isMac() {
285
+ if (typeof navigator === "undefined") return false;
286
+ const { userAgentData, platform } = navigator;
287
+ return /mac|iphone|ipad/i.test(userAgentData?.platform || platform || "");
288
+ }
289
+
283
290
  // src/markdown/shortcut-label.ts
284
291
  var MAC_LABEL = { mod: "\u2318", alt: "\u2325", shift: "\u21E7" };
285
292
  var OTHER_LABEL = { mod: "Ctrl", alt: "Alt", shift: "Shift" };
286
- function onMac() {
287
- if (typeof navigator === "undefined") return false;
288
- return /Mac|iPhone|iPad/.test(navigator.userAgent);
289
- }
290
293
  function formatShortcut(hint) {
291
- const mac = onMac();
294
+ const mac = isMac();
292
295
  const labels = mac ? MAC_LABEL : OTHER_LABEL;
293
296
  const parts = hint.split("+").map((part) => labels[part] ?? part.toUpperCase());
294
297
  return parts.join(mac ? "" : "+");
@@ -352,9 +355,10 @@ function useMarkdownKeymap({
352
355
  }) {
353
356
  const onKeyDown = React.useCallback(
354
357
  (event) => {
358
+ if (event.nativeEvent.isComposing || event.keyCode === 229) return;
355
359
  const el = event.currentTarget;
356
360
  const { value, selectionStart, selectionEnd } = el;
357
- const mod = event.metaKey || event.ctrlKey;
361
+ const mod = isMac() ? event.metaKey : event.ctrlKey;
358
362
  const run = (edit) => {
359
363
  if (!edit) return;
360
364
  event.preventDefault();
@@ -414,11 +418,17 @@ function useMarkdownKeymap({
414
418
  }
415
419
 
416
420
  // src/hooks/useAutoGrow.ts
421
+ import * as React3 from "react";
422
+
423
+ // src/hooks/use-isomorphic-layout-effect.ts
417
424
  import * as React2 from "react";
425
+ var useIsomorphicLayoutEffect = typeof document === "undefined" ? React2.useEffect : React2.useLayoutEffect;
426
+
427
+ // src/hooks/useAutoGrow.ts
418
428
  function useAutoGrow(value) {
419
- const ref = React2.useRef(null);
420
- const lastWidth = React2.useRef(null);
421
- const fit = React2.useCallback(() => {
429
+ const ref = React3.useRef(null);
430
+ const lastWidth = React3.useRef(null);
431
+ const fit = React3.useCallback(() => {
422
432
  const el = ref.current;
423
433
  if (!el) return;
424
434
  const scroller = document.scrollingElement ?? document.documentElement;
@@ -427,8 +437,8 @@ function useAutoGrow(value) {
427
437
  if (el.scrollHeight > 0) el.style.height = `${el.scrollHeight}px`;
428
438
  if (scroller.scrollTop !== scrollTop) scroller.scrollTop = scrollTop;
429
439
  }, []);
430
- React2.useLayoutEffect(fit, [fit, value]);
431
- React2.useEffect(() => {
440
+ useIsomorphicLayoutEffect(fit, [fit, value]);
441
+ React3.useEffect(() => {
432
442
  const el = ref.current;
433
443
  if (!el || typeof ResizeObserver === "undefined") return;
434
444
  const observer = new ResizeObserver(([entry]) => {