@latentic/live-markdown 0.1.1 → 0.3.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,55 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.0] - 2026-08-29
4
+
5
+ ### Added
6
+
7
+ - **A host can contribute extension modules.** `extensions?: readonly
8
+ MarkdownExtension[]` on the editor, merged in one pass with the built-ins and
9
+ host-last, so a module can deliberately override a construct and the merger
10
+ still warns when a node rule is redefined. Applies in source mode as well as
11
+ wysiwyg — a keymap is not a rendering concern. Read when an editor state is
12
+ built, so building the array inline cannot remount the editor.
13
+
14
+ Toolbar contributions now reach the host through the `toolbar` slot's
15
+ context; the merger always collected them and the component dropped them.
16
+
17
+ - `composeExtensions` is renamed **`mergeExtensions`**, with the old name kept
18
+ as a deprecated alias. "compose" read as the name of an app rather than as the
19
+ verb, which is the wrong signal for a package meant for many hosts.
20
+
21
+ ### Fixed
22
+
23
+ - **A checked task box reads as checked in dark.** The tick used
24
+ `--cds-icon-on-color` — white in every theme — against a fill of
25
+ `--cds-icon-primary`, which inverts. In dark that was #ffffff on #f4f4f4, a
26
+ contrast ratio of 1.10. It now uses `--cds-background`, the inverse of the
27
+ fill by construction.
28
+
29
+ - **The task box is sized to the text.** 1rem beside 1rem text is as tall as the
30
+ whole em box; it now derives from its own `font-size: 0.875em`, with box, tick
31
+ and baseline offset all in `em` of that, and sits on the x-height instead of
32
+ hanging off the baseline.
33
+
34
+ ## [0.2.0] - 2026-08-28
35
+
36
+ ### Added
37
+
38
+ - **A host can theme fenced-code syntax colours.** Each palette entry carries a
39
+ CSS custom property alongside its colour, and the editor paints
40
+ `var(--md-code-…, <One Light value>)`, so a dark theme can restyle code.
41
+ Previously impossible: the editor takes no `extensions` prop and the generated
42
+ highlight classes are content-hashed, leaving no seam at all.
43
+
44
+ The clipboard renderer deliberately keeps literal colours. Pasted HTML arrives
45
+ with no stylesheet, so a `var()` there resolves to nothing and pastes
46
+ colourless code into Google Docs or Word — and the document being pasted into
47
+ is white whatever theme the editor was in.
48
+
49
+ Roles are named individually (`--md-code-keyword`, `--md-code-string`, …) even
50
+ where two share a value, so one can be restyled without the other. A host that
51
+ sets no variables gets a byte-identical palette: the literal is the fallback.
52
+
3
53
  ## [0.1.1] - 2026-08-24
4
54
 
5
55
  ### Added
package/README.md CHANGED
@@ -186,7 +186,8 @@ The `toolbar` render prop receives the live `EditorView`, so host buttons can dr
186
186
  | `value` | `string` | — | The markdown content (controlled) |
187
187
  | `onChange` | `(value: string, changes: DocumentTextChange[]) => void` | — | Called after edits, debounced |
188
188
  | `mode` | `"wysiwyg" \| "source"` | `"wysiwyg"` | Rich rendering or raw markdown |
189
- | `toolbar` | `(ctx: { view: EditorView }) => ReactNode` | — | Host-rendered toolbar, given the live editor view |
189
+ | `extensions` | `readonly MarkdownExtension[]` | — | Host-contributed extension modules, merged after the built-ins |
190
+ | `toolbar` | `(ctx: { view, contributions }) => ReactNode` | — | Host-rendered toolbar, given the live editor view and any toolbar items the extensions contributed |
190
191
  | `selectionActions` | `(ctx: { selection, dismiss }) => ReactNode` | — | Host-rendered actions for the current selection (e.g. a comment bubble) |
191
192
  | `linkTargets` | `ReadonlySet<string>` | — | Known file paths for wikilink resolution |
192
193
  | `onNavigateToLink` | `(path: string) => void` | — | Called on Cmd/Ctrl-click of an internal link |
@@ -209,13 +210,32 @@ Built-in extensions:
209
210
  - `tableExtension` — GFM tables with cell navigation
210
211
  - `wikilinkExtension` — `[[wikilink]]` rendering and navigation
211
212
 
213
+ The built-ins load themselves. To add your own, hand the editor your modules:
214
+
215
+ ```tsx
216
+ <CodeMirrorMarkdownEditor value={md} onChange={setMd} extensions={[myExtension]} />
217
+ ```
218
+
219
+ They are merged **after** the built-ins, so a module can deliberately override a
220
+ construct — the merger warns when a node rule is redefined, so an accidental
221
+ shadow is not silent. Modules apply in source mode as well as wysiwyg: a keymap
222
+ or a plain CM6 extension is not a markdown-rendering concern, and node rules
223
+ simply go unread while the painter is off.
224
+
225
+ The prop is read when an editor state is built, not on every render, so building
226
+ the array inline costs nothing and cannot remount the editor. Changing it
227
+ mid-session takes effect at the next rebuild.
228
+
229
+ To merge modules yourself — composing a preset, or feeding a CodeMirror view you
230
+ own — `mergeExtensions` is the same function the editor uses:
231
+
212
232
  ```tsx
213
- import { composeExtensions, mathExtension, tableExtension } from "@latentic/live-markdown";
233
+ import { mergeExtensions, mathExtension, tableExtension } from "@latentic/live-markdown";
214
234
 
215
- const composed = composeExtensions([mathExtension, tableExtension]);
216
- // composed.extensions — CM6 Extension[] (each extension's node rules ride
217
- // along via a facet, so this is all the editor needs)
218
- // composed.toolbar — merged ToolbarContribution[]
235
+ const merged = mergeExtensions([mathExtension, tableExtension]);
236
+ // merged.extensions — CM6 Extension[] (each module's node rules ride along via
237
+ // a facet, so this is all a view needs)
238
+ // merged.toolbar — merged ToolbarContribution[]
219
239
  ```
220
240
 
221
241
  ### A custom extension
@@ -223,7 +243,7 @@ const composed = composeExtensions([mathExtension, tableExtension]);
223
243
  A rule is one function per Lezer node name, returning how that node paints. The `mark` combinator covers the common "style this span" case. Rules merge last-wins, so an extension can introduce a construct from its own grammar or deliberately restyle a built-in one:
224
244
 
225
245
  ```tsx
226
- import { composeExtensions, mark, type MarkdownExtension } from "@latentic/live-markdown";
246
+ import { mark, type MarkdownExtension } from "@latentic/live-markdown";
227
247
 
228
248
  const fancyEmphasis: MarkdownExtension = {
229
249
  name: "fancy-emphasis",
@@ -234,7 +254,7 @@ const fancyEmphasis: MarkdownExtension = {
234
254
  },
235
255
  };
236
256
 
237
- const composed = composeExtensions([fancyEmphasis]);
257
+ <CodeMirrorMarkdownEditor value={md} onChange={setMd} extensions={[fancyEmphasis]} />
238
258
  ```
239
259
 
240
260
  `Paint` is a closed set — line class, span mark, hide, widget, or nothing — while node names grow, so styling a construct is always one rule in one place.
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
3
  import * as _codemirror_view from '@codemirror/view';
4
- import { Decoration, ViewPlugin, DecorationSet, ViewUpdate, EditorView, Command, KeyBinding } from '@codemirror/view';
4
+ import { Decoration, ViewPlugin, DecorationSet, ViewUpdate, KeyBinding, EditorView, Command } from '@codemirror/view';
5
5
  import * as _codemirror_state from '@codemirror/state';
6
6
  import { EditorState, Facet, Extension } from '@codemirror/state';
7
7
  import { syntaxTree } from '@codemirror/language';
@@ -384,6 +384,71 @@ type CommentOnExcerpt = (excerpt: {
384
384
  range: SourceRange;
385
385
  }, anchor: CommentAnchor) => void;
386
386
 
387
+ interface ToolbarContribution {
388
+ readonly id: string;
389
+ readonly group: "heading" | "format" | "block" | "insert" | string;
390
+ readonly label: string;
391
+ readonly icon: ReactNode;
392
+ readonly shortcut?: string;
393
+ readonly isActive?: (caretContext: CaretContextSnapshot) => boolean;
394
+ readonly run: (view: _codemirror_view.EditorView) => void;
395
+ }
396
+ interface CaretContextSnapshot {
397
+ readonly bold: boolean;
398
+ readonly italic: boolean;
399
+ readonly code: boolean;
400
+ readonly link: boolean;
401
+ readonly heading: 1 | 2 | 3 | 4 | 5 | 6 | 0;
402
+ readonly bulletList: boolean;
403
+ readonly orderedList: boolean;
404
+ readonly blockquote: boolean;
405
+ }
406
+ interface MarkdownExtension {
407
+ readonly name: string;
408
+ readonly version: string;
409
+ readonly description?: string;
410
+ /** Node rules for constructs this extension's grammar introduces (or
411
+ * deliberately overrides) — merged into the decoration painter via
412
+ * `nodeRulesFacet`, so an extension never edits the base table. */
413
+ readonly rules?: NodeRules;
414
+ readonly extensions?: Extension[];
415
+ readonly keymap?: KeyBinding[];
416
+ readonly toolbar?: ToolbarContribution[];
417
+ }
418
+
419
+ interface ComposedExtension {
420
+ extensions: Extension[];
421
+ toolbar: ToolbarContribution[];
422
+ }
423
+ /**
424
+ * Merge extension modules into one set of CodeMirror extensions plus the
425
+ * toolbar items they contribute. Later modules win: a host's rules are applied
426
+ * after the built-ins, so it can deliberately override a construct.
427
+ */
428
+ declare function mergeExtensions(modules: readonly MarkdownExtension[]): ComposedExtension;
429
+ /**
430
+ * @deprecated Renamed to {@link mergeExtensions}. "compose" reads as the app
431
+ * this package was extracted from rather than as the verb; the editor is meant
432
+ * for more hosts than that one. Kept as an alias so the rename is not breaking.
433
+ */
434
+ declare const composeExtensions: typeof mergeExtensions;
435
+
436
+ declare const highlightExtension: MarkdownExtension;
437
+
438
+ declare const footnoteExtension: MarkdownExtension;
439
+
440
+ declare const mathExtension: MarkdownExtension;
441
+
442
+ declare const mermaidExtension: MarkdownExtension;
443
+
444
+ /**
445
+ * A FACTORY, not a const: each composition gets its own editing surface (the
446
+ * one-active-edit state), so two mounted editors can never share a cell edit.
447
+ */
448
+ declare function tableExtension(): MarkdownExtension;
449
+
450
+ declare const wikilinkExtension: MarkdownExtension;
451
+
387
452
  type CodeMirrorEditorMode = "wysiwyg" | "source";
388
453
  /** A non-empty editor selection, in document byte offsets. */
389
454
  interface EditorSelectionSnapshot {
@@ -399,13 +464,27 @@ interface CodeMirrorMarkdownEditorProps {
399
464
  linkTargets?: ReadonlySet<string>;
400
465
  onNavigateToLink?: (path: string) => void;
401
466
  /**
402
- * Host-rendered toolbar. The editor owns the live `EditorView` and hands it to
403
- * the slot; the host builds whatever toolbar UI it wants (formatting buttons,
404
- * file actions, ) around it. Omit for a chromeless editor. Return a STABLE
405
- * element shape so the host's own memoisation can hold across keystrokes.
467
+ * Extension modules contributed by the host, merged AFTER the built-ins so a
468
+ * host can deliberately override a construct (the merger warns when a node
469
+ * rule is redefined).
470
+ *
471
+ * Read when an editor state is built — on mount, and on the swaps that
472
+ * rebuild one — not on every render, so passing a fresh array each time costs
473
+ * nothing and cannot remount the editor. Changing it mid-session therefore
474
+ * takes effect at the next rebuild.
475
+ *
476
+ * Applies in BOTH modes: a module's node rules simply go unread while the
477
+ * decoration painter is off, but its keymaps and plain CodeMirror extensions
478
+ * are not markdown-rendering concerns and should not be silently dropped in
479
+ * source mode.
406
480
  */
481
+ extensions?: readonly MarkdownExtension[];
482
+ /** Host-rendered toolbar. `contributions` carries the toolbar items the
483
+ * extension modules asked for; the host decides whether and how to render
484
+ * them alongside its own. */
407
485
  toolbar?: (ctx: {
408
486
  view: EditorView;
487
+ contributions: readonly ToolbarContribution[];
409
488
  }) => ReactNode;
410
489
  /**
411
490
  * Host-rendered actions for the current text selection (e.g. a comment / ask
@@ -456,7 +535,7 @@ interface CodeMirrorMarkdownEditorProps {
456
535
  */
457
536
  onFlushReady?: (flush: (() => void) | null) => void;
458
537
  }
459
- declare function CodeMirrorMarkdownEditorInner({ mode, onChange, value, workspaceRoot, filePath, linkTargets, onNavigateToLink, toolbar, selectionActions, resolveImageSrc, saveImageBytes, onOpenExternalUrl, onCommentOnExcerpt, renderClipboardHtml, onAfterContentSwap, onFlushReady, }: CodeMirrorMarkdownEditorProps): react.JSX.Element;
538
+ declare function CodeMirrorMarkdownEditorInner({ mode, onChange, value, workspaceRoot, filePath, linkTargets, onNavigateToLink, extensions: hostExtensions, toolbar, selectionActions, resolveImageSrc, saveImageBytes, onOpenExternalUrl, onCommentOnExcerpt, renderClipboardHtml, onAfterContentSwap, onFlushReady, }: CodeMirrorMarkdownEditorProps): react.JSX.Element;
460
539
  /**
461
540
  * Memoised export — same reason as the Tiptap editor. AppShell
462
541
  * re-renders on every chat-thread token; without memoisation each
@@ -504,60 +583,6 @@ declare const blockCommands: {
504
583
  */
505
584
  declare function onEditorUpdate(view: EditorView, fn: (update: ViewUpdate) => void): () => void;
506
585
 
507
- interface ToolbarContribution {
508
- readonly id: string;
509
- readonly group: "heading" | "format" | "block" | "insert" | string;
510
- readonly label: string;
511
- readonly icon: ReactNode;
512
- readonly shortcut?: string;
513
- readonly isActive?: (caretContext: CaretContextSnapshot) => boolean;
514
- readonly run: (view: _codemirror_view.EditorView) => void;
515
- }
516
- interface CaretContextSnapshot {
517
- readonly bold: boolean;
518
- readonly italic: boolean;
519
- readonly code: boolean;
520
- readonly link: boolean;
521
- readonly heading: 1 | 2 | 3 | 4 | 5 | 6 | 0;
522
- readonly bulletList: boolean;
523
- readonly orderedList: boolean;
524
- readonly blockquote: boolean;
525
- }
526
- interface MarkdownExtension {
527
- readonly name: string;
528
- readonly version: string;
529
- readonly description?: string;
530
- /** Node rules for constructs this extension's grammar introduces (or
531
- * deliberately overrides) — merged into the decoration painter via
532
- * `nodeRulesFacet`, so an extension never edits the base table. */
533
- readonly rules?: NodeRules;
534
- readonly extensions?: Extension[];
535
- readonly keymap?: KeyBinding[];
536
- readonly toolbar?: ToolbarContribution[];
537
- }
538
-
539
- interface ComposedExtension {
540
- extensions: Extension[];
541
- toolbar: ToolbarContribution[];
542
- }
543
- declare function composeExtensions(modules: readonly MarkdownExtension[]): ComposedExtension;
544
-
545
- declare const highlightExtension: MarkdownExtension;
546
-
547
- declare const footnoteExtension: MarkdownExtension;
548
-
549
- declare const mathExtension: MarkdownExtension;
550
-
551
- declare const mermaidExtension: MarkdownExtension;
552
-
553
- /**
554
- * A FACTORY, not a const: each composition gets its own editing surface (the
555
- * one-active-edit state), so two mounted editors can never share a cell edit.
556
- */
557
- declare function tableExtension(): MarkdownExtension;
558
-
559
- declare const wikilinkExtension: MarkdownExtension;
560
-
561
586
  /**
562
587
  * Mermaid rendering, decoupled from CodeMirror — shared by the editor widget,
563
588
  * the document export (which ships the SVG to the backend), and the clipboard
@@ -852,4 +877,4 @@ interface ResolveWorkspaceLinkOptions {
852
877
  }
853
878
  declare function resolveWorkspaceLink(href: string, options: ResolveWorkspaceLinkOptions): ResolvedWorkspaceLink | null;
854
879
 
855
- export { type CaretContextSnapshot, type CodeMirrorEditorMode, CodeMirrorMarkdownEditor, type CodeMirrorMarkdownEditorProps, type ComposedExtension, type DocumentTextChange, type EditorSelectionSnapshot, type Frontmatter, type FrontmatterValue, type HighlightedSpan, IMAGE_EDIT_ALT_EVENT, type ImageEditAltEventDetail, type ImageInsertOptions, type ImageInsertResult, type ImageResolveContext, type InlineScanMatch, type InlineScanRule, type MarkdownDocument, type MarkdownExtension, type MermaidRenderResult, type NodeContext, type NodeRule, type NodeRules, type OpenExternalUrl, type Paint, type ResolveImageSrc, type ResolveWorkspaceLinkOptions, type ResolvedWorkspaceLink, type SaveImageBytes, type SourceRange, type ToolbarContribution, blockCommands, buildImageMarkdown, composeExtensions, computeFileDir, defaultResolveImageSrc, dirnamePath, editorBaseTheme, escapeAttr, escapeText, extractImageBlobs, extractImageFiles, footnoteExtension, formatCommands, getCachedMermaidPng, hasUriScheme, headingLine, hideAlways, highlightExtension, highlightFenceSpans, imageInsertHandlers, inlineScanRulesFacet, insertImageBlob, isAbsolutePath, isMermaidFenceInfo, joinPath, line, mark, markdownDecorationsPlugin, mathExtension, mermaidExtension, nodeRulesFacet, onEditorUpdate, parseFrontmatter, parseWikilinkBody, pickImageFileForCaret, raw, renderMermaidToSvg, resolveWikilinkTarget, resolveWorkspaceLink, scanInline, serializeMarkdown, setFrontmatterField, showImageActionMenu, structural, tableExtension, treeAt, warmMermaidPng, wikilinkExtension };
880
+ export { type CaretContextSnapshot, type CodeMirrorEditorMode, CodeMirrorMarkdownEditor, type CodeMirrorMarkdownEditorProps, type ComposedExtension, type DocumentTextChange, type EditorSelectionSnapshot, type Frontmatter, type FrontmatterValue, type HighlightedSpan, IMAGE_EDIT_ALT_EVENT, type ImageEditAltEventDetail, type ImageInsertOptions, type ImageInsertResult, type ImageResolveContext, type InlineScanMatch, type InlineScanRule, type MarkdownDocument, type MarkdownExtension, type MermaidRenderResult, type NodeContext, type NodeRule, type NodeRules, type OpenExternalUrl, type Paint, type ResolveImageSrc, type ResolveWorkspaceLinkOptions, type ResolvedWorkspaceLink, type SaveImageBytes, type SourceRange, type ToolbarContribution, blockCommands, buildImageMarkdown, composeExtensions, computeFileDir, defaultResolveImageSrc, dirnamePath, editorBaseTheme, escapeAttr, escapeText, extractImageBlobs, extractImageFiles, footnoteExtension, formatCommands, getCachedMermaidPng, hasUriScheme, headingLine, hideAlways, highlightExtension, highlightFenceSpans, imageInsertHandlers, inlineScanRulesFacet, insertImageBlob, isAbsolutePath, isMermaidFenceInfo, joinPath, line, mark, markdownDecorationsPlugin, mathExtension, mergeExtensions, mermaidExtension, nodeRulesFacet, onEditorUpdate, parseFrontmatter, parseWikilinkBody, pickImageFileForCaret, raw, renderMermaidToSvg, resolveWikilinkTarget, resolveWorkspaceLink, scanInline, serializeMarkdown, setFrontmatterField, showImageActionMenu, structural, tableExtension, treeAt, warmMermaidPng, wikilinkExtension };
package/dist/index.js CHANGED
@@ -443,23 +443,59 @@ function onEditorUpdate(view, fn) {
443
443
  };
444
444
  }
445
445
  var CODE_PALETTE = [
446
- { tag: [tags.keyword, tags.modifier, tags.operatorKeyword], color: "#a626a4" },
447
- { tag: [tags.string, tags.special(tags.string)], color: "#50a14f" },
448
- { tag: tags.comment, color: "#a0a1a7", fontStyle: "italic" },
449
- { tag: [tags.number, tags.bool, tags.null, tags.atom], color: "#986801" },
450
- { tag: [tags.function(tags.variableName), tags.function(tags.propertyName)], color: "#4078f2" },
451
- { tag: [tags.typeName, tags.className, tags.namespace], color: "#c18401" },
452
- { tag: tags.definition(tags.variableName), color: "#e45649" },
453
- { tag: tags.propertyName, color: "#4078f2" },
454
- { tag: [tags.tagName, tags.self], color: "#e45649" },
455
- { tag: tags.attributeName, color: "#986801" },
456
- { tag: [tags.regexp, tags.escape], color: "#0184bc" },
457
- { tag: tags.invalid, color: "#ca1243" }
446
+ {
447
+ tag: [tags.keyword, tags.modifier, tags.operatorKeyword],
448
+ color: "#a626a4",
449
+ cssVar: "--md-code-keyword"
450
+ },
451
+ {
452
+ tag: [tags.string, tags.special(tags.string)],
453
+ color: "#50a14f",
454
+ cssVar: "--md-code-string"
455
+ },
456
+ {
457
+ tag: tags.comment,
458
+ color: "#a0a1a7",
459
+ cssVar: "--md-code-comment",
460
+ fontStyle: "italic"
461
+ },
462
+ {
463
+ tag: [tags.number, tags.bool, tags.null, tags.atom],
464
+ color: "#986801",
465
+ cssVar: "--md-code-literal"
466
+ },
467
+ {
468
+ tag: [tags.function(tags.variableName), tags.function(tags.propertyName)],
469
+ color: "#4078f2",
470
+ cssVar: "--md-code-function"
471
+ },
472
+ {
473
+ tag: [tags.typeName, tags.className, tags.namespace],
474
+ color: "#c18401",
475
+ cssVar: "--md-code-type"
476
+ },
477
+ {
478
+ tag: tags.definition(tags.variableName),
479
+ color: "#e45649",
480
+ cssVar: "--md-code-variable"
481
+ },
482
+ { tag: tags.propertyName, color: "#4078f2", cssVar: "--md-code-property" },
483
+ { tag: [tags.tagName, tags.self], color: "#e45649", cssVar: "--md-code-tag" },
484
+ { tag: tags.attributeName, color: "#986801", cssVar: "--md-code-attribute" },
485
+ { tag: [tags.regexp, tags.escape], color: "#0184bc", cssVar: "--md-code-regexp" },
486
+ { tag: tags.invalid, color: "#ca1243", cssVar: "--md-code-invalid" }
458
487
  ];
488
+ function themedColor(spec) {
489
+ return `var(${spec.cssVar}, ${spec.color})`;
490
+ }
459
491
 
460
492
  // src/codemirror/code/codeHighlight.ts
461
493
  var style = HighlightStyle.define(
462
- CODE_PALETTE.map((spec) => ({ tag: spec.tag, color: spec.color, fontStyle: spec.fontStyle }))
494
+ CODE_PALETTE.map((spec) => ({
495
+ tag: spec.tag,
496
+ color: themedColor(spec),
497
+ fontStyle: spec.fontStyle
498
+ }))
463
499
  );
464
500
  var codeHighlight = syntaxHighlighting(style);
465
501
 
@@ -2579,23 +2615,30 @@ var editorBaseTheme = EditorView.theme({
2579
2615
  marginRight: "0.3em",
2580
2616
  fontWeight: "normal"
2581
2617
  },
2582
- // Task list checkbox — drawn as a Carbon checkbox, not the native control: a
2583
- // 1rem square that fills with the icon token and shows a white tick when
2584
- // checked. `appearance: none` is what replaces WebKit's small rounded default;
2585
- // the box then matches the design system rather than approximating it with an
2586
- // accent colour over the native shape.
2618
+ // Task list checkbox — drawn as a Carbon checkbox, not the native control.
2619
+ // `appearance: none` is what replaces WebKit's small rounded default; the box
2620
+ // then matches the design system rather than approximating it with an accent
2621
+ // colour over the native shape.
2622
+ //
2623
+ // Sized in `em`, not `rem`: a 1rem box beside 1rem text is as tall as the
2624
+ // whole em box, so it towered over lowercase letters, whose cap height is
2625
+ // nearer 0.7em. One knob — the checkbox's own `font-size` — drives the box,
2626
+ // the tick and the baseline offset, so the parts cannot drift apart, and the
2627
+ // control tracks the text if a list ever renders at another size.
2587
2628
  ".cm-task-checkbox": {
2588
2629
  appearance: "none",
2589
2630
  WebkitAppearance: "none",
2590
2631
  boxSizing: "border-box",
2591
2632
  position: "relative",
2592
- width: "1rem",
2593
- height: "1rem",
2594
- margin: "0 0.4em 0 0",
2633
+ fontSize: "0.875em",
2634
+ width: "1em",
2635
+ height: "1em",
2636
+ margin: "0 0.45em 0 0",
2595
2637
  cursor: "pointer",
2596
- verticalAlign: "-0.15em",
2597
- border: "0.0625rem solid var(--cds-icon-primary, #161616)",
2598
- borderRadius: "0.0625rem",
2638
+ // Centres the box on the x-height rather than hanging it off the baseline.
2639
+ verticalAlign: "-0.2em",
2640
+ border: "0.0625em solid var(--cds-icon-primary, #161616)",
2641
+ borderRadius: "0.0625em",
2599
2642
  background: "transparent"
2600
2643
  },
2601
2644
  ".cm-task-checkbox:checked": {
@@ -2603,15 +2646,20 @@ var editorBaseTheme = EditorView.theme({
2603
2646
  borderColor: "var(--cds-icon-primary, #161616)"
2604
2647
  },
2605
2648
  // The tick: an L (right + bottom border) rotated 45° into a check.
2649
+ //
2650
+ // Drawn in `--cds-background`, NOT `--cds-icon-on-color`. "On color" means an
2651
+ // icon on a branded fill and is white in every theme, while the box here is
2652
+ // filled with `--cds-icon-primary` — which inverts. In dark that was a white
2653
+ // tick on a near-white box: a checked item read as unchecked.
2606
2654
  ".cm-task-checkbox:checked::after": {
2607
2655
  content: "''",
2608
2656
  position: "absolute",
2609
- left: "0.3125rem",
2610
- top: "0.0625rem",
2611
- width: "0.25rem",
2612
- height: "0.5rem",
2613
- border: "solid var(--cds-icon-on-color, #ffffff)",
2614
- borderWidth: "0 0.125rem 0.125rem 0",
2657
+ left: "0.3125em",
2658
+ top: "0.0625em",
2659
+ width: "0.25em",
2660
+ height: "0.5em",
2661
+ border: "solid var(--cds-background, #ffffff)",
2662
+ borderWidth: "0 0.125em 0.125em 0",
2615
2663
  transform: "rotate(45deg)"
2616
2664
  },
2617
2665
  ".cm-task-checkbox:focus-visible": {
@@ -4314,7 +4362,7 @@ function pickImageFileForCaret(view) {
4314
4362
  };
4315
4363
  input.click();
4316
4364
  }
4317
- function composeExtensions(modules) {
4365
+ function mergeExtensions(modules) {
4318
4366
  const extensions = [];
4319
4367
  const toolbar = [];
4320
4368
  const allKeyBindings = [];
@@ -4338,6 +4386,7 @@ function composeExtensions(modules) {
4338
4386
  if (allKeyBindings.length) extensions.push(keymap.of(allKeyBindings));
4339
4387
  return { extensions, toolbar };
4340
4388
  }
4389
+ var composeExtensions = mergeExtensions;
4341
4390
  var HIGHLIGHT_RE = /==([^=\n]+?)==/g;
4342
4391
  var HIDE2 = Decoration.replace({});
4343
4392
  var highlightMark = Decoration.mark({ class: "cm-highlight" });
@@ -5540,6 +5589,7 @@ function CodeMirrorMarkdownEditorInner({
5540
5589
  filePath,
5541
5590
  linkTargets,
5542
5591
  onNavigateToLink,
5592
+ extensions: hostExtensions,
5543
5593
  toolbar,
5544
5594
  selectionActions,
5545
5595
  resolveImageSrc,
@@ -5588,6 +5638,9 @@ function CodeMirrorMarkdownEditorInner({
5588
5638
  const syncedHashRef = useRef(/* @__PURE__ */ new Map());
5589
5639
  const currentFileRef = useRef(filePath);
5590
5640
  const modeInitializedRef = useRef(decorationsEnabled);
5641
+ const hostExtensionsRef = useRef(hostExtensions);
5642
+ hostExtensionsRef.current = hostExtensions;
5643
+ const toolbarContributionsRef = useRef([]);
5591
5644
  function buildExtensions() {
5592
5645
  const base = [
5593
5646
  history(),
@@ -5747,16 +5800,20 @@ function CodeMirrorMarkdownEditorInner({
5747
5800
  ];
5748
5801
  if (decorationsEnabled) {
5749
5802
  base.push(markdownDecorationsPlugin);
5750
- const composed = composeExtensions([
5803
+ }
5804
+ const merged = mergeExtensions([
5805
+ ...decorationsEnabled ? [
5751
5806
  wikilinkExtension,
5752
5807
  highlightExtension,
5753
5808
  footnoteExtension,
5754
5809
  mathExtension,
5755
5810
  mermaidExtension,
5756
5811
  tableExtension()
5757
- ]);
5758
- base.push(...composed.extensions);
5759
- }
5812
+ ] : [],
5813
+ ...hostExtensionsRef.current ?? []
5814
+ ]);
5815
+ base.push(...merged.extensions);
5816
+ toolbarContributionsRef.current = merged.toolbar;
5760
5817
  return base;
5761
5818
  }
5762
5819
  const buildExtensionsRef = useRef(buildExtensions);
@@ -5914,7 +5971,7 @@ function CodeMirrorMarkdownEditorInner({
5914
5971
  view.dispatch({ selection: EditorSelection.cursor(head) });
5915
5972
  }, []);
5916
5973
  const toolbarNode = useMemo(
5917
- () => viewForToolbar && toolbar ? toolbar({ view: viewForToolbar }) : null,
5974
+ () => viewForToolbar && toolbar ? toolbar({ view: viewForToolbar, contributions: toolbarContributionsRef.current }) : null,
5918
5975
  [viewForToolbar, toolbar]
5919
5976
  );
5920
5977
  const selectionNode = useMemo(
@@ -5929,6 +5986,6 @@ function CodeMirrorMarkdownEditorInner({
5929
5986
  }
5930
5987
  var CodeMirrorMarkdownEditor = memo(CodeMirrorMarkdownEditorInner);
5931
5988
 
5932
- export { CodeMirrorMarkdownEditor, IMAGE_EDIT_ALT_EVENT, blockCommands, buildImageMarkdown, composeExtensions, computeFileDir, defaultResolveImageSrc, dirnamePath, editorBaseTheme, escapeAttr, escapeText, extractImageBlobs, extractImageFiles, footnoteExtension, formatCommands, getCachedMermaidPng, hasUriScheme, headingLine, hideAlways, highlightExtension, highlightFenceSpans, imageInsertHandlers, inlineScanRulesFacet, insertImageBlob, isAbsolutePath, isMermaidFenceInfo, joinPath, line, mark, markdownDecorationsPlugin, mathExtension, mermaidExtension, nodeRulesFacet, onEditorUpdate, parseFrontmatter, parseWikilinkBody, pickImageFileForCaret, raw, renderMermaidToSvg, resolveWikilinkTarget, resolveWorkspaceLink, scanInline, serializeMarkdown, setFrontmatterField, showImageActionMenu, structural, tableExtension, treeAt, warmMermaidPng, wikilinkExtension };
5989
+ export { CodeMirrorMarkdownEditor, IMAGE_EDIT_ALT_EVENT, blockCommands, buildImageMarkdown, composeExtensions, computeFileDir, defaultResolveImageSrc, dirnamePath, editorBaseTheme, escapeAttr, escapeText, extractImageBlobs, extractImageFiles, footnoteExtension, formatCommands, getCachedMermaidPng, hasUriScheme, headingLine, hideAlways, highlightExtension, highlightFenceSpans, imageInsertHandlers, inlineScanRulesFacet, insertImageBlob, isAbsolutePath, isMermaidFenceInfo, joinPath, line, mark, markdownDecorationsPlugin, mathExtension, mergeExtensions, mermaidExtension, nodeRulesFacet, onEditorUpdate, parseFrontmatter, parseWikilinkBody, pickImageFileForCaret, raw, renderMermaidToSvg, resolveWikilinkTarget, resolveWorkspaceLink, scanInline, serializeMarkdown, setFrontmatterField, showImageActionMenu, structural, tableExtension, treeAt, warmMermaidPng, wikilinkExtension };
5933
5990
  //# sourceMappingURL=index.js.map
5934
5991
  //# sourceMappingURL=index.js.map