@k2b/editor 0.1.0 → 0.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@k2b/editor",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Configurable Markdown editor for SolidJS: editor, toolbar, bubble menu, and slash menu on one shared state.",
5
5
  "type": "module",
6
6
  "sideEffects": [
@@ -69,7 +69,7 @@
69
69
  "@codemirror/state": "6.7.1",
70
70
  "@codemirror/view": "6.43.7",
71
71
  "@k2b/stdlib": "0.30.0",
72
- "@k2b/ui": "0.37.0",
72
+ "@k2b/ui": "0.38.1",
73
73
  "@lezer/common": "1.5.3",
74
74
  "@lezer/highlight": "1.2.4",
75
75
  "@lezer/markdown": "1.7.2",
@@ -2,6 +2,7 @@ import { type EditorState, type Extension, Prec, type Range, RangeSet, StateFiel
2
2
  import { Decoration, type DecorationSet, EditorView, keymap, WidgetType } from "@codemirror/view";
3
3
  import { holdsDecorations, refreshMarkdownDecorationsEffect, selectionIntersectsRange } from "./_lib/cursor-zone-field";
4
4
  import { type DataBlock, extractDataBlocks, namedBlockBody, renderDataBlockHtml } from "./lib/named-blocks";
5
+ import { blockAnchorsDrawnElsewhere } from "./named-blocks";
5
6
 
6
7
  type DataBlockRange = {
7
8
  from: number;
@@ -52,8 +53,10 @@ class DataBlockWidget extends WidgetType {
52
53
 
53
54
  override get estimatedHeight() {
54
55
  const rows = Math.max(1, this.body.split("\n").filter((line) => line.trim().length > 0).length);
55
- // 12px / 16px cells + 16px padding + divider; the handle and outer padding round up to 44px.
56
- return 44 + rows * 33;
56
+ // 12px / 16px cells + 16px padding + divider per row; the anchor label (18px line), its margins and the outer
57
+ // padding take just under 44px. Chromium and WebKit round the rows differently (one and two rows measured
58
+ // 75.98 / 109.19px and 109.89px), so the base keeps the estimate at most 1px above both.
59
+ return 43.95 + rows * 33;
57
60
  }
58
61
 
59
62
  override ignoreEvent() {
@@ -76,6 +79,8 @@ export const dataBlocksExtension = (): Extension => {
76
79
  },
77
80
  provide(field) {
78
81
  return [
82
+ // The data block widget draws its anchor label together with the block.
83
+ blockAnchorsDrawnElsewhere.of("data"),
79
84
  EditorView.decorations.from(field, (value) => value.decorations),
80
85
  EditorView.atomicRanges.of((view) => view.state.field(field).blockWidgetDecorations),
81
86
  ];
@@ -354,7 +354,7 @@ const humanizeKey = (key: string): string =>
354
354
  export const renderDataBlockHtml = (name: string | null, src: string): string => {
355
355
  const entries = parseNamedDataBlock(src);
356
356
  const handle = name
357
- ? `<div class="md-block-handle" data-block-name="${escapeHtml(name)}">@${escapeHtml(name)}</div>`
357
+ ? `<div class="md-block-handle" data-block-name="${escapeHtml(name)}">${escapeHtml(name)}</div>`
358
358
  : `<div class="md-data-handle-row"><div class="md-block-handle">data</div></div>`;
359
359
  const rows =
360
360
  entries.length > 0
@@ -392,7 +392,7 @@ export const renderNamedBlockHandlesMarkdown = (md: string | null | undefined):
392
392
  }
393
393
  for (const block of extractNamedBlocks(md)) {
394
394
  if (block.type === "data") continue;
395
- const handle = `<div class="md-block-handle" data-block-name="${escapeHtml(block.name)}">@${escapeHtml(block.name)}</div>`;
395
+ const handle = `<div class="md-block-handle" data-block-name="${escapeHtml(block.name)}">${escapeHtml(block.name)}</div>`;
396
396
  replacements.push({ from: block.handleStart, to: block.handleEnd, html: handle });
397
397
  }
398
398
 
@@ -1,4 +1,4 @@
1
- import { type Extension, type Range, RangeSet } from "@codemirror/state";
1
+ import { type Extension, Facet, type Range, RangeSet } from "@codemirror/state";
2
2
  import { Decoration, EditorView, WidgetType } from "@codemirror/view";
3
3
  import {
4
4
  blockWidgetLineNavigationExtension,
@@ -8,6 +8,13 @@ import {
8
8
  } from "./_lib/cursor-zone-field";
9
9
  import { extractNamedBlocks } from "./lib/named-blocks";
10
10
 
11
+ /**
12
+ * Block types whose anchor line another extension draws together with the block, such as data blocks. The anchor
13
+ * label leaves those lines alone; every other named block (tables, lists, …) gets the label.
14
+ */
15
+ export const blockAnchorsDrawnElsewhere = Facet.define<string, ReadonlySet<string>>({ combine: (types) => new Set(types) });
16
+
17
+ /** The anchor line `@name` above a block, shown as a quiet label until the caret enters the line. */
11
18
  class NamedBlockHandleWidget extends WidgetType {
12
19
  constructor(
13
20
  private name: string,
@@ -19,8 +26,9 @@ class NamedBlockHandleWidget extends WidgetType {
19
26
  override toDOM(view: EditorView) {
20
27
  const el = document.createElement("div");
21
28
  el.className = "cm-named-block-handle md-block-handle";
29
+ el.dataset.blockName = this.name;
22
30
  el.setAttribute("contenteditable", "false");
23
- el.textContent = `@${this.name}`;
31
+ el.textContent = this.name;
24
32
  el.onmousedown = (event) => {
25
33
  event.preventDefault();
26
34
  event.stopPropagation();
@@ -52,9 +60,7 @@ export const namedBlocksExtension = (): Extension => {
52
60
  const nextLine = state.doc.lineAt(Math.min(block.handleEnd + 1, state.doc.length));
53
61
  const sourceVisibleRange = { from: prevLine.from, to: nextLine.to };
54
62
  ranges.push(sourceVisibleRange);
55
- // Table/data handles are rendered by their block widgets together
56
- // with the preview so the handle and body collapse as one stable block.
57
- if (block.type === "table" || block.type === "data") continue;
63
+ if (state.facet(blockAnchorsDrawnElsewhere).has(block.type)) continue;
58
64
  if (selectionIntersectsRange(cursor, sourceVisibleRange.from, sourceVisibleRange.to)) continue;
59
65
  const handleDecoration = Decoration.replace({
60
66
  widget: new NamedBlockHandleWidget(block.name, block.handleStart),
@@ -205,7 +205,7 @@ export const createPrettyTablePresentation = (data: PrettyTableData, options: Pr
205
205
  export const renderPrettyTableHtml = (data: PrettyTableData, options: PrettyTableOptions = {}): string => {
206
206
  const presentation = createPrettyTablePresentation(data, options);
207
207
  const caption = data.caption
208
- ? `<div class="md-block-handle" data-block-name="${escapeHtml(data.caption)}">@${escapeHtml(data.caption)}</div>`
208
+ ? `<div class="md-block-handle" data-block-name="${escapeHtml(data.caption)}">${escapeHtml(data.caption)}</div>`
209
209
  : "";
210
210
  const headerHtml = data.headers.map((_, i) => `<th scope="col">${presentation.headerHtml(i)}</th>`).join("");
211
211
  // An all-empty GFM header is the approved header-off representation.
@@ -7,7 +7,6 @@ import type { TableHost } from "@k2b/ui/text-editor";
7
7
  import { displayCell, findTables, type Table } from "@k2b/ui/text-editor/model";
8
8
  import { isFormula } from "../formula";
9
9
  import { refreshMarkdownDecorationsEffect } from "./_lib/cursor-zone-field";
10
- import { extractNamedBlocks } from "./lib/named-blocks";
11
10
  import { applyLigatures } from "./ligatures";
12
11
  import { createPrettyTablePresentation, formatFormulaValue, type PrettyTableData, type PrettyTableOptions } from "./pretty-table";
13
12
  import { formulaPreviewWidget } from "./tables";
@@ -33,30 +32,6 @@ const presentationData = (table: Table): PrettyTableData => {
33
32
  return { headers: values(table.lines[0]), rows: table.lines.slice(2).map(values), align: table.align };
34
33
  };
35
34
 
36
- const captionDecorations = (state: EditorState): DecorationSet => {
37
- const source = state.doc.toString();
38
- const starts = new Set(findTables(source).map((table) => table.from));
39
- return Decoration.set(
40
- extractNamedBlocks(source)
41
- .filter((block) => block.type === "table" && starts.has(block.blockStart))
42
- .map((block) =>
43
- // Keep the actual handle editable. namedBlocksExtension skips table handles;
44
- // tableGrid starts on the header, so neither extension renders a second caption.
45
- Decoration.mark({ class: "md-block-handle cm-notebook-table-caption", attributes: { "data-block-name": block.name } }).range(
46
- block.handleStart,
47
- block.handleEnd,
48
- ),
49
- ),
50
- true,
51
- );
52
- };
53
-
54
- const tableCaptions = StateField.define<DecorationSet>({
55
- create: captionDecorations,
56
- update: (value, tr) => (tr.docChanged ? captionDecorations(tr.state) : value),
57
- provide: (field) => EditorView.decorations.from(field),
58
- });
59
-
60
35
  const presentationTheme = EditorView.theme({
61
36
  ".k2b-ste-table .cm-notebook-formula-editor": {
62
37
  position: "absolute",
@@ -85,7 +60,6 @@ const presentationTheme = EditorView.theme({
85
60
 
86
61
  ".k2b-ste-table .k2b-ste-cell > .md-table-cell": { padding: "0", minHeight: "0" },
87
62
  ".k2b-ste-table tr:has([data-notebook-total]) > td": { borderTop: "1px solid var(--ui-border-strong)", fontWeight: "600" },
88
- ".cm-notebook-table-caption": { margin: "0" },
89
63
  ".cm-notebook-formula-copy": {
90
64
  display: "inline-flex",
91
65
  alignItems: "center",
@@ -216,7 +190,7 @@ export const createNotebookTablePresentation = (
216
190
  } => {
217
191
  const snapshots = new WeakMap<Table, { locale: string; presentation: ReturnType<typeof createPrettyTablePresentation> }>();
218
192
  return {
219
- extension: [tableCaptions, presentationTheme, activeFormulaPreview(options)],
193
+ extension: [presentationTheme, activeFormulaPreview(options)],
220
194
  renderCell(element, _raw, table, row, column) {
221
195
  const locale = options.locale?.() ?? (element.ownerDocument.documentElement.lang || "en");
222
196
  let snapshot = snapshots.get(table);
package/src/styles.css CHANGED
@@ -142,25 +142,19 @@
142
142
  display: none !important;
143
143
  }
144
144
 
145
+ /* The anchor of a named block (`@name` in the source) reads as a quiet label above the block, in the label type of
146
+ form fields. The caret in its line shows the source again. */
145
147
  .md-block-handle {
146
148
  display: inline-flex;
147
149
  align-items: center;
148
150
  width: fit-content;
149
151
  max-width: 100%;
150
152
  margin: 0.35rem 0 0.15rem;
151
- padding: 0.125rem 0.375rem;
152
- border-radius: 0.25rem;
153
- background: rgb(239 246 255);
154
- color: rgb(37 99 235);
155
- font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
156
- font-size: 0.6875rem;
153
+ color: var(--k2b-text-muted);
154
+ font-size: 0.8125rem;
157
155
  font-weight: 600;
158
- line-height: 1.3;
159
- }
160
-
161
- .dark .md-block-handle {
162
- background: rgb(30 58 138 / 0.25);
163
- color: rgb(147 197 253);
156
+ line-height: 1.125rem;
157
+ overflow-wrap: anywhere;
164
158
  }
165
159
 
166
160
  .md-data-handle-row {