@charcuterie/ui 3.4.1 → 3.6.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/dist/Stepper/Stepper.d.ts +89 -0
- package/dist/Stepper/Stepper.d.ts.map +1 -0
- package/dist/Stepper/Stepper.js +87 -0
- package/dist/Stepper/Stepper.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/markdownEditorCodeMirror/livePreview.d.ts.map +1 -1
- package/dist/markdownEditorCodeMirror/livePreview.js +346 -3
- package/dist/markdownEditorCodeMirror/livePreview.js.map +1 -1
- package/dist/markdownEditorCodeMirror/livePreviewRanges.d.ts +99 -1
- package/dist/markdownEditorCodeMirror/livePreviewRanges.d.ts.map +1 -1
- package/dist/markdownEditorCodeMirror/livePreviewRanges.js +370 -19
- package/dist/markdownEditorCodeMirror/livePreviewRanges.js.map +1 -1
- package/package.json +1 -1
- package/src/Stepper/Stepper.mdx +86 -0
- package/src/Stepper/Stepper.stories.tsx +224 -0
- package/src/Stepper/Stepper.test.tsx +112 -0
- package/src/Stepper/Stepper.tsx +246 -0
- package/src/index.ts +7 -0
- package/src/markdownEditorCodeMirror/MarkdownEditorCodeMirror.mdx +28 -0
- package/src/markdownEditorCodeMirror/MarkdownEditorCodeMirror.stories.tsx +42 -0
- package/src/markdownEditorCodeMirror/livePreview.ts +465 -3
- package/src/markdownEditorCodeMirror/livePreviewRanges.test.ts +349 -8
- package/src/markdownEditorCodeMirror/livePreviewRanges.ts +669 -27
- package/src/sourceRules.test.ts +8 -1
|
@@ -26,8 +26,16 @@
|
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
28
|
import { syntaxTree } from "@codemirror/language"
|
|
29
|
-
import type {
|
|
30
|
-
|
|
29
|
+
import type {
|
|
30
|
+
EditorState,
|
|
31
|
+
Extension,
|
|
32
|
+
Range,
|
|
33
|
+
} from "@codemirror/state"
|
|
34
|
+
import {
|
|
35
|
+
Prec,
|
|
36
|
+
StateEffect,
|
|
37
|
+
StateField,
|
|
38
|
+
} from "@codemirror/state"
|
|
31
39
|
import type {
|
|
32
40
|
DecorationSet,
|
|
33
41
|
ViewUpdate,
|
|
@@ -35,6 +43,7 @@ import type {
|
|
|
35
43
|
import {
|
|
36
44
|
Decoration,
|
|
37
45
|
EditorView,
|
|
46
|
+
keymap,
|
|
38
47
|
ViewPlugin,
|
|
39
48
|
WidgetType,
|
|
40
49
|
} from "@codemirror/view"
|
|
@@ -42,12 +51,31 @@ import {
|
|
|
42
51
|
import type {
|
|
43
52
|
LivePreviewLineKind,
|
|
44
53
|
LivePreviewMarkKind,
|
|
54
|
+
LivePreviewTableRow,
|
|
55
|
+
LivePreviewTableSegment,
|
|
56
|
+
} from "./livePreviewRanges.ts"
|
|
57
|
+
import {
|
|
58
|
+
toLivePreviewRanges,
|
|
59
|
+
toLivePreviewTableRanges,
|
|
45
60
|
} from "./livePreviewRanges.ts"
|
|
46
|
-
import { toLivePreviewRanges } from "./livePreviewRanges.ts"
|
|
47
61
|
|
|
48
62
|
/** Marks a rendered link so the click handler can find its target. */
|
|
49
63
|
const URL_ATTRIBUTE = "data-charcuterie-url"
|
|
50
64
|
|
|
65
|
+
/**
|
|
66
|
+
* A rendered cell's offset into its own table, so a click can put
|
|
67
|
+
* the caret back in the markdown it was drawn from.
|
|
68
|
+
*
|
|
69
|
+
* Relative, not absolute, and for the same reason `TaskWidget` reads
|
|
70
|
+
* its position back from the DOM: a widget outlives the offsets it
|
|
71
|
+
* was built at. Editing a paragraph *above* a table moves every
|
|
72
|
+
* offset in it without changing a character of the table, and the
|
|
73
|
+
* widget is reused — `eq` compares the markdown, which did not
|
|
74
|
+
* change. Relative plus `posAtDOM` at click time is correct in both
|
|
75
|
+
* cases; a captured absolute offset silently is not.
|
|
76
|
+
*/
|
|
77
|
+
const CELL_OFFSET_ATTRIBUTE = "data-charcuterie-cell-offset"
|
|
78
|
+
|
|
51
79
|
/** Toggle the "edit Markdown" mode. */
|
|
52
80
|
export const setLivePreviewRawMode =
|
|
53
81
|
StateEffect.define<boolean>()
|
|
@@ -75,6 +103,41 @@ export const livePreviewRawModeField =
|
|
|
75
103
|
},
|
|
76
104
|
})
|
|
77
105
|
|
|
106
|
+
const setIsEditorFocused = StateEffect.define<boolean>()
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Focus, as editor state.
|
|
110
|
+
*
|
|
111
|
+
* The plugin can read `view.hasFocus` directly; the block pass is a
|
|
112
|
+
* `StateField` and has no view to ask. It still needs the answer,
|
|
113
|
+
* because an unfocused editor reveals nothing — without it the
|
|
114
|
+
* caret's default resting place, offset 0, counts as "inside" a
|
|
115
|
+
* table that opens the document, and a description that starts with
|
|
116
|
+
* a table loads showing pipes.
|
|
117
|
+
*/
|
|
118
|
+
const isEditorFocusedField = StateField.define<boolean>({
|
|
119
|
+
create: () => false,
|
|
120
|
+
update: (isFocused, transaction) => {
|
|
121
|
+
let isNextFocused = isFocused
|
|
122
|
+
|
|
123
|
+
for (const effect of transaction.effects) {
|
|
124
|
+
if (effect.is(setIsEditorFocused)) {
|
|
125
|
+
isNextFocused = effect.value
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return isNextFocused
|
|
130
|
+
},
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The facet CodeMirror provides for exactly this: turn a focus
|
|
135
|
+
* change into an effect, so state can depend on it.
|
|
136
|
+
*/
|
|
137
|
+
const focusTracker = EditorView.focusChangeEffect.of(
|
|
138
|
+
(_state, isFocusing) => setIsEditorFocused.of(isFocusing),
|
|
139
|
+
)
|
|
140
|
+
|
|
78
141
|
const LINE_CLASSES: Record<LivePreviewLineKind, string> = {
|
|
79
142
|
blockquote: "cm-md-blockquote",
|
|
80
143
|
code: "cm-md-codeblock",
|
|
@@ -212,6 +275,183 @@ class TaskWidget extends WidgetType {
|
|
|
212
275
|
}
|
|
213
276
|
}
|
|
214
277
|
|
|
278
|
+
/**
|
|
279
|
+
* A cell's content run, as DOM.
|
|
280
|
+
*
|
|
281
|
+
* A plain text node when there is nothing to say about it — the
|
|
282
|
+
* common case, and one fewer element per cell in a table that might
|
|
283
|
+
* have a hundred.
|
|
284
|
+
*/
|
|
285
|
+
const toSegmentNode = (
|
|
286
|
+
segment: LivePreviewTableSegment,
|
|
287
|
+
view: EditorView,
|
|
288
|
+
) => {
|
|
289
|
+
if (segment.type === "image") {
|
|
290
|
+
const image = document.createElement("img")
|
|
291
|
+
|
|
292
|
+
image.setAttribute("src", segment.url)
|
|
293
|
+
|
|
294
|
+
image.setAttribute("alt", segment.alt)
|
|
295
|
+
|
|
296
|
+
image.className = "cm-md-image"
|
|
297
|
+
|
|
298
|
+
image.addEventListener("load", () => {
|
|
299
|
+
view.requestMeasure()
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
return image
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (segment.markKinds.length === 0 && !segment.url) {
|
|
306
|
+
return document.createTextNode(segment.text)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const span = document.createElement("span")
|
|
310
|
+
|
|
311
|
+
span.className = segment.markKinds
|
|
312
|
+
.map((markKind) => MARK_CLASSES[markKind])
|
|
313
|
+
.join(" ")
|
|
314
|
+
|
|
315
|
+
if (segment.url) {
|
|
316
|
+
span.setAttribute(URL_ATTRIBUTE, segment.url)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
span.textContent = segment.text
|
|
320
|
+
|
|
321
|
+
return span
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* A table, standing in for its own pipes.
|
|
326
|
+
*
|
|
327
|
+
* This is the one widget here that replaces *lines* rather than a
|
|
328
|
+
* span, and the only construct in the language whose meaning is
|
|
329
|
+
* geometry: a column is not a decoration you can hang on the text,
|
|
330
|
+
* because the text of column two is on four different lines. So the
|
|
331
|
+
* markdown stops being drawn and this draws instead — and the way
|
|
332
|
+
* back is the caret, exactly as it is for a link or an image.
|
|
333
|
+
* Clicking a cell dispatches a selection into that cell, the
|
|
334
|
+
* decoration drops on the next update, and the pipes are back with
|
|
335
|
+
* the caret already in the right one.
|
|
336
|
+
*
|
|
337
|
+
* `eq` compares the table's markdown source. Two identical tables
|
|
338
|
+
* never share a widget instance — `eq` is only ever consulted for
|
|
339
|
+
* the same position — so the slice is a complete identity, and it
|
|
340
|
+
* is stable under edits elsewhere in the document, which is what
|
|
341
|
+
* stops every keystroke in a long description from rebuilding every
|
|
342
|
+
* table below it.
|
|
343
|
+
*/
|
|
344
|
+
class TableWidget extends WidgetType {
|
|
345
|
+
constructor(
|
|
346
|
+
private readonly source: string,
|
|
347
|
+
private readonly rows: readonly LivePreviewTableRow[],
|
|
348
|
+
private readonly tableFrom: number,
|
|
349
|
+
) {
|
|
350
|
+
super()
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
eq(other: TableWidget) {
|
|
354
|
+
return other.source === this.source
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
toDOM(view: EditorView) {
|
|
358
|
+
const scroll = document.createElement("div")
|
|
359
|
+
|
|
360
|
+
scroll.className = "cm-md-table-scroll"
|
|
361
|
+
|
|
362
|
+
const table = document.createElement("table")
|
|
363
|
+
|
|
364
|
+
table.className = "cm-md-table-rendered"
|
|
365
|
+
|
|
366
|
+
const head = document.createElement("thead")
|
|
367
|
+
|
|
368
|
+
const body = document.createElement("tbody")
|
|
369
|
+
|
|
370
|
+
for (const row of this.rows) {
|
|
371
|
+
const rowElement = document.createElement("tr")
|
|
372
|
+
|
|
373
|
+
for (const cell of row.cells) {
|
|
374
|
+
const cellElement = document.createElement(
|
|
375
|
+
row.isHeader ? "th" : "td",
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
cellElement.style.textAlign = cell.alignment
|
|
379
|
+
|
|
380
|
+
cellElement.setAttribute(
|
|
381
|
+
CELL_OFFSET_ATTRIBUTE,
|
|
382
|
+
String(cell.from - this.tableFrom),
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
for (const segment of cell.segments) {
|
|
386
|
+
cellElement.append(toSegmentNode(segment, view))
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
rowElement.append(cellElement)
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
;(row.isHeader ? head : body).append(rowElement)
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (head.childElementCount > 0) {
|
|
396
|
+
table.append(head)
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
table.append(body)
|
|
400
|
+
|
|
401
|
+
scroll.append(table)
|
|
402
|
+
|
|
403
|
+
scroll.addEventListener("mousedown", (event) => {
|
|
404
|
+
const target = event.target
|
|
405
|
+
|
|
406
|
+
if (!(target instanceof HTMLElement)) {
|
|
407
|
+
return
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// A link in a cell is still a link; `openLinkOnClick` gets it
|
|
411
|
+
// on the way up.
|
|
412
|
+
if (target.closest(`[${URL_ATTRIBUTE}]`)) {
|
|
413
|
+
return
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const cellElement = target.closest(
|
|
417
|
+
`[${CELL_OFFSET_ATTRIBUTE}]`,
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
if (!cellElement) {
|
|
421
|
+
return
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
event.preventDefault()
|
|
425
|
+
|
|
426
|
+
const offset = Number(
|
|
427
|
+
cellElement.getAttribute(CELL_OFFSET_ATTRIBUTE),
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
view.focus()
|
|
431
|
+
|
|
432
|
+
view.dispatch({
|
|
433
|
+
selection: {
|
|
434
|
+
anchor: view.posAtDOM(scroll) + offset,
|
|
435
|
+
},
|
|
436
|
+
})
|
|
437
|
+
})
|
|
438
|
+
|
|
439
|
+
return scroll
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* The widget handles its own events.
|
|
444
|
+
*
|
|
445
|
+
* Without this CodeMirror also treats the click as a caret
|
|
446
|
+
* placement and lands wherever the coordinates map to — which for
|
|
447
|
+
* a block widget is its first or last position, never the cell
|
|
448
|
+
* that was clicked.
|
|
449
|
+
*/
|
|
450
|
+
ignoreEvent() {
|
|
451
|
+
return true
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
215
455
|
const toDecorations = (view: EditorView): DecorationSet => {
|
|
216
456
|
const isRawMode = view.state.field(
|
|
217
457
|
livePreviewRawModeField,
|
|
@@ -319,6 +559,86 @@ const toDecorations = (view: EditorView): DecorationSet => {
|
|
|
319
559
|
return Decoration.set(decorations, true)
|
|
320
560
|
}
|
|
321
561
|
|
|
562
|
+
/**
|
|
563
|
+
* The block pass: tables, from a `StateField`.
|
|
564
|
+
*
|
|
565
|
+
* `EditorView.decorations.from` a field rather than a plugin, and
|
|
566
|
+
* that is not a style choice — CodeMirror throws
|
|
567
|
+
* `Block decorations may not be specified via plugins` if you try
|
|
568
|
+
* it the other way, because replacing four lines with one element
|
|
569
|
+
* changes the block structure the viewport is measured against.
|
|
570
|
+
* The whole document is scanned every time, which is what the
|
|
571
|
+
* plugin's `visibleRanges` optimisation buys back for everything
|
|
572
|
+
* else.
|
|
573
|
+
*/
|
|
574
|
+
const toTableDecorations = (state: EditorState) => {
|
|
575
|
+
const isRawMode = state.field(
|
|
576
|
+
livePreviewRawModeField,
|
|
577
|
+
false,
|
|
578
|
+
)
|
|
579
|
+
|
|
580
|
+
const selections = state.field(
|
|
581
|
+
isEditorFocusedField,
|
|
582
|
+
false,
|
|
583
|
+
)
|
|
584
|
+
? state.selection.ranges.map((range) => ({
|
|
585
|
+
from: range.from,
|
|
586
|
+
to: range.to,
|
|
587
|
+
}))
|
|
588
|
+
: []
|
|
589
|
+
|
|
590
|
+
const text = state.doc.toString()
|
|
591
|
+
|
|
592
|
+
return Decoration.set(
|
|
593
|
+
toLivePreviewTableRanges({
|
|
594
|
+
isRawMode: isRawMode ?? false,
|
|
595
|
+
selections,
|
|
596
|
+
text,
|
|
597
|
+
tree: syntaxTree(state),
|
|
598
|
+
}).map((range) =>
|
|
599
|
+
Decoration.replace({
|
|
600
|
+
// Whole lines, which is what `block` requires — and also
|
|
601
|
+
// what makes the table a paragraph in the flow rather than
|
|
602
|
+
// a very tall character.
|
|
603
|
+
block: true,
|
|
604
|
+
widget: new TableWidget(
|
|
605
|
+
text.slice(range.from, range.to),
|
|
606
|
+
range.rows,
|
|
607
|
+
range.from,
|
|
608
|
+
),
|
|
609
|
+
}).range(range.from, range.to),
|
|
610
|
+
),
|
|
611
|
+
true,
|
|
612
|
+
)
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const livePreviewTableField =
|
|
616
|
+
StateField.define<DecorationSet>({
|
|
617
|
+
create: toTableDecorations,
|
|
618
|
+
update: (decorations, transaction) => {
|
|
619
|
+
if (
|
|
620
|
+
transaction.docChanged ||
|
|
621
|
+
transaction.selection !== undefined ||
|
|
622
|
+
transaction.effects.some(
|
|
623
|
+
(effect) =>
|
|
624
|
+
effect.is(setLivePreviewRawMode) ||
|
|
625
|
+
effect.is(setIsEditorFocused),
|
|
626
|
+
) ||
|
|
627
|
+
// Parsing is incremental and time-sliced, so a long
|
|
628
|
+
// document's tables arrive over several transactions that
|
|
629
|
+
// change nothing else. Comparing the tree by identity is
|
|
630
|
+
// the cheap way to notice.
|
|
631
|
+
syntaxTree(transaction.state) !==
|
|
632
|
+
syntaxTree(transaction.startState)
|
|
633
|
+
) {
|
|
634
|
+
return toTableDecorations(transaction.state)
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
return decorations
|
|
638
|
+
},
|
|
639
|
+
provide: (field) => EditorView.decorations.from(field),
|
|
640
|
+
})
|
|
641
|
+
|
|
322
642
|
const livePreviewPlugin = ViewPlugin.fromClass(
|
|
323
643
|
class {
|
|
324
644
|
decorations: DecorationSet
|
|
@@ -352,6 +672,80 @@ const livePreviewPlugin = ViewPlugin.fromClass(
|
|
|
352
672
|
},
|
|
353
673
|
)
|
|
354
674
|
|
|
675
|
+
/**
|
|
676
|
+
* Step *into* a table with the arrow keys.
|
|
677
|
+
*
|
|
678
|
+
* CodeMirror moves the caret over a block widget, not through it:
|
|
679
|
+
* pressing Down on the line above a rendered table lands on the
|
|
680
|
+
* line below it, and the table's markdown is unreachable without a
|
|
681
|
+
* mouse. That is fine for a widget with nothing to edit inside;
|
|
682
|
+
* this one is four lines of the user's own text.
|
|
683
|
+
*
|
|
684
|
+
* So the vertical move is inspected before it happens, and when it
|
|
685
|
+
* would jump a table, the caret is put at that table's edge
|
|
686
|
+
* instead — the near edge, so Down enters at the top and Up enters
|
|
687
|
+
* at the bottom. The table stands down on the next update and the
|
|
688
|
+
* pipes are there with the caret already in them.
|
|
689
|
+
*
|
|
690
|
+
* Only for a collapsed selection: Shift+Down is *selecting* across
|
|
691
|
+
* the table, where jumping it is right — the text is still in the
|
|
692
|
+
* document and still in the selection.
|
|
693
|
+
*/
|
|
694
|
+
const toTableStep =
|
|
695
|
+
(isForward: boolean) => (view: EditorView) => {
|
|
696
|
+
const { main } = view.state.selection
|
|
697
|
+
|
|
698
|
+
if (!main.empty) {
|
|
699
|
+
return false
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
const destination = view.moveVertically(
|
|
703
|
+
main,
|
|
704
|
+
isForward,
|
|
705
|
+
).head
|
|
706
|
+
|
|
707
|
+
const from = Math.min(main.head, destination)
|
|
708
|
+
|
|
709
|
+
const to = Math.max(main.head, destination)
|
|
710
|
+
|
|
711
|
+
if (from === to) {
|
|
712
|
+
return false
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
let target: number | null = null
|
|
716
|
+
|
|
717
|
+
view.state
|
|
718
|
+
.field(livePreviewTableField)
|
|
719
|
+
.between(from, to, (rangeFrom, rangeTo) => {
|
|
720
|
+
target = isForward ? rangeFrom : rangeTo
|
|
721
|
+
|
|
722
|
+
return false
|
|
723
|
+
})
|
|
724
|
+
|
|
725
|
+
if (target === null) {
|
|
726
|
+
return false
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
view.dispatch({
|
|
730
|
+
scrollIntoView: true,
|
|
731
|
+
selection: { anchor: target },
|
|
732
|
+
})
|
|
733
|
+
|
|
734
|
+
return true
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* `Prec.high` because the default keymap also binds these, and the
|
|
739
|
+
* first handler that returns `true` wins. Registration order in the
|
|
740
|
+
* extension array is not a promise about precedence.
|
|
741
|
+
*/
|
|
742
|
+
const tableStepKeymap = Prec.high(
|
|
743
|
+
keymap.of([
|
|
744
|
+
{ key: "ArrowDown", run: toTableStep(true) },
|
|
745
|
+
{ key: "ArrowUp", run: toTableStep(false) },
|
|
746
|
+
]),
|
|
747
|
+
)
|
|
748
|
+
|
|
355
749
|
/**
|
|
356
750
|
* Open the link under the pointer.
|
|
357
751
|
*
|
|
@@ -496,9 +890,70 @@ const livePreviewTheme = EditorView.theme({
|
|
|
496
890
|
".cm-md-strong": {
|
|
497
891
|
fontWeight: "700",
|
|
498
892
|
},
|
|
893
|
+
/**
|
|
894
|
+
* The *editing* view of a table: the pipes, monospaced so the
|
|
895
|
+
* columns line up as typed. This is what the caret reveals, and
|
|
896
|
+
* what raw mode shows.
|
|
897
|
+
*/
|
|
499
898
|
".cm-md-table": {
|
|
500
899
|
fontFamily: "var(--font-mono)",
|
|
501
900
|
},
|
|
901
|
+
".cm-md-table-rendered": {
|
|
902
|
+
borderCollapse: "collapse",
|
|
903
|
+
},
|
|
904
|
+
".cm-md-table-rendered td, .cm-md-table-rendered th": {
|
|
905
|
+
border: "1px solid var(--color-border-subtle)",
|
|
906
|
+
cursor: "text",
|
|
907
|
+
/**
|
|
908
|
+
* Undo the editor's own wrapping rules inside a cell.
|
|
909
|
+
*
|
|
910
|
+
* `.cm-lineWrapping` sets `overflow-wrap: anywhere` so a long
|
|
911
|
+
* unbroken line cannot escape the gutter, and a table cell
|
|
912
|
+
* inherits it — which in the Narrow View broke `Port` across
|
|
913
|
+
* two lines as `Por` / `t`. A cell wraps at spaces like prose,
|
|
914
|
+
* and breaks a word only when the word alone will not fit.
|
|
915
|
+
*/
|
|
916
|
+
overflowWrap: "break-word",
|
|
917
|
+
padding: "var(--space-1) var(--space-2)",
|
|
918
|
+
verticalAlign: "top",
|
|
919
|
+
whiteSpace: "normal",
|
|
920
|
+
wordBreak: "normal",
|
|
921
|
+
},
|
|
922
|
+
/**
|
|
923
|
+
* A blank cell still owns a row's worth of height.
|
|
924
|
+
*
|
|
925
|
+
* Without it a row whose cells are all empty collapses to its
|
|
926
|
+
* padding and the table looks like it lost a row — which, in a
|
|
927
|
+
* surface where a blank cell is how you say "nothing here yet",
|
|
928
|
+
* is the wrong answer to a legitimate table.
|
|
929
|
+
*/
|
|
930
|
+
".cm-md-table-rendered td:empty::after, .cm-md-table-rendered th:empty::after":
|
|
931
|
+
{
|
|
932
|
+
content: '"\\200b"',
|
|
933
|
+
},
|
|
934
|
+
/**
|
|
935
|
+
* No `text-align` here: the delimiter row aligns the header cell
|
|
936
|
+
* along with its column, and that is set inline per cell. A rule
|
|
937
|
+
* would be dead code that looks like it is doing the work.
|
|
938
|
+
*/
|
|
939
|
+
".cm-md-table-rendered th": {
|
|
940
|
+
backgroundColor: "var(--color-surface-sunken)",
|
|
941
|
+
fontWeight: "var(--font-weight-semibold)",
|
|
942
|
+
},
|
|
943
|
+
/**
|
|
944
|
+
* The scroll box, not the table.
|
|
945
|
+
*
|
|
946
|
+
* A table wider than the editor has to go somewhere. Letting it
|
|
947
|
+
* overflow puts columns under the gutter or off the edge of a
|
|
948
|
+
* wrapped line; scrolling the block is the same answer every
|
|
949
|
+
* document renderer lands on, and it keeps the *editor* from
|
|
950
|
+
* scrolling sideways as a whole.
|
|
951
|
+
*/
|
|
952
|
+
".cm-md-table-scroll": {
|
|
953
|
+
margin: "var(--space-2) 0",
|
|
954
|
+
maxWidth: "100%",
|
|
955
|
+
overflowX: "auto",
|
|
956
|
+
},
|
|
502
957
|
".cm-md-task": {
|
|
503
958
|
accentColor: "var(--color-intent-accent-solid)",
|
|
504
959
|
marginInlineEnd: "0.35em",
|
|
@@ -528,8 +983,15 @@ const rawModeClass = EditorView.editorAttributes.of(
|
|
|
528
983
|
|
|
529
984
|
export const livePreview = (): Extension => [
|
|
530
985
|
livePreviewRawModeField,
|
|
986
|
+
isEditorFocusedField,
|
|
987
|
+
focusTracker,
|
|
988
|
+
// The block pass, before the plugin: a field's decorations are
|
|
989
|
+
// resolved first anyway, but declaring it first keeps the reading
|
|
990
|
+
// order the same as the drawing order.
|
|
991
|
+
livePreviewTableField,
|
|
531
992
|
livePreviewPlugin,
|
|
532
993
|
livePreviewTheme,
|
|
533
994
|
openLinkOnClick,
|
|
995
|
+
tableStepKeymap,
|
|
534
996
|
rawModeClass,
|
|
535
997
|
]
|