@quillmark/svelte 0.20.1 → 0.22.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.
Files changed (85) hide show
  1. package/NOTICE +6 -7
  2. package/README.md +1 -1
  3. package/THEMING.md +1 -1
  4. package/dist/core/address.d.ts +6 -1
  5. package/dist/core/address.js +12 -3
  6. package/dist/core/bloom.d.ts +9 -4
  7. package/dist/core/bloom.js +18 -49
  8. package/dist/core/codec/atoms.d.ts +1 -1
  9. package/dist/core/codec/atoms.js +26 -11
  10. package/dist/core/codec/blocks.d.ts +5 -5
  11. package/dist/core/codec/blocks.js +27 -8
  12. package/dist/core/codec/code.d.ts +4 -4
  13. package/dist/core/codec/code.js +52 -22
  14. package/dist/core/codec/decode.d.ts +5 -5
  15. package/dist/core/codec/decode.js +18 -12
  16. package/dist/core/codec/encode.js +6 -1
  17. package/dist/core/codec/field.d.ts +22 -15
  18. package/dist/core/codec/field.js +66 -26
  19. package/dist/core/codec/index.d.ts +1 -1
  20. package/dist/core/codec/index.js +2 -2
  21. package/dist/core/codec/inputrules.d.ts +4 -0
  22. package/dist/core/codec/inputrules.js +44 -6
  23. package/dist/core/codec/link-paste.d.ts +11 -0
  24. package/dist/core/codec/link-paste.js +32 -0
  25. package/dist/core/codec/lists.d.ts +5 -4
  26. package/dist/core/codec/lists.js +87 -31
  27. package/dist/core/codec/nesting.d.ts +20 -0
  28. package/dist/core/codec/nesting.js +62 -0
  29. package/dist/core/codec/prose.css +145 -63
  30. package/dist/core/codec/schema.d.ts +2 -2
  31. package/dist/core/codec/schema.js +71 -18
  32. package/dist/core/codec/slash.js +0 -0
  33. package/dist/core/codec/table-copy.d.ts +17 -0
  34. package/dist/core/codec/table-copy.js +84 -0
  35. package/dist/core/codec/table-view.d.ts +6 -7
  36. package/dist/core/codec/table-view.js +281 -201
  37. package/dist/core/codec/table.d.ts +2 -9
  38. package/dist/core/codec/table.js +10 -19
  39. package/dist/core/codec/tasks.d.ts +19 -0
  40. package/dist/core/codec/tasks.js +140 -0
  41. package/dist/core/errors.d.ts +2 -2
  42. package/dist/core/rungs.d.ts +6 -0
  43. package/dist/core/rungs.js +11 -0
  44. package/dist/core/theme.css +24 -12
  45. package/dist/visual/ArrayField.svelte +236 -78
  46. package/dist/visual/ArrayField.svelte.d.ts +0 -2
  47. package/dist/visual/BooleanField.svelte +12 -1
  48. package/dist/visual/Card.svelte +41 -23
  49. package/dist/visual/Card.svelte.d.ts +3 -2
  50. package/dist/visual/CardControls.svelte +15 -15
  51. package/dist/visual/DateField.svelte +93 -64
  52. package/dist/visual/EnumField.svelte +9 -4
  53. package/dist/visual/Field.svelte +20 -16
  54. package/dist/visual/Field.svelte.d.ts +4 -2
  55. package/dist/visual/FieldHint.svelte +22 -5
  56. package/dist/visual/FieldLabel.svelte +5 -33
  57. package/dist/visual/FieldLabel.svelte.d.ts +0 -2
  58. package/dist/visual/FormatPopover.svelte +78 -81
  59. package/dist/visual/MatrixField.svelte +28 -19
  60. package/dist/visual/NumberField.svelte +32 -3
  61. package/dist/visual/ObjectField.svelte +40 -24
  62. package/dist/visual/ObjectField.svelte.d.ts +6 -0
  63. package/dist/visual/ProseField.svelte +10 -13
  64. package/dist/visual/ProseField.svelte.d.ts +5 -2
  65. package/dist/visual/ProseValue.svelte +25 -19
  66. package/dist/visual/ProseValue.svelte.d.ts +4 -4
  67. package/dist/visual/TextField.svelte +3 -2
  68. package/dist/visual/TipsCard.svelte +7 -7
  69. package/dist/visual/VariantField.svelte +1 -1
  70. package/dist/visual/VariantField.svelte.d.ts +1 -1
  71. package/dist/visual/VisualEditorInner.svelte +121 -39
  72. package/dist/visual/controls.css +21 -2
  73. package/dist/visual/icons/Icon.svelte +3 -2
  74. package/dist/visual/icons/Icon.svelte.d.ts +3 -2
  75. package/dist/visual/icons/nodes.d.ts +25 -21
  76. package/dist/visual/icons/nodes.js +9 -7
  77. package/dist/visual/leaves.d.ts +12 -9
  78. package/dist/visual/leaves.js +19 -0
  79. package/dist/visual/motion.d.ts +25 -3
  80. package/dist/visual/motion.js +58 -9
  81. package/dist/visual/strings.d.ts +15 -9
  82. package/dist/visual/strings.js +19 -12
  83. package/dist/visual/structure.d.ts +21 -31
  84. package/dist/visual/structure.js +54 -51
  85. package/package.json +3 -3
@@ -1,6 +1,8 @@
1
- import { liftListItem, sinkListItem, splitListItem } from 'prosemirror-schema-list';
1
+ import { liftListItem, sinkListItem } from 'prosemirror-schema-list';
2
2
  import { EditorState, Selection, TextSelection } from 'prosemirror-state';
3
- import { chainCommands, joinTextblockBackward } from 'prosemirror-commands';
3
+ import { chainCommands, joinTextblockBackward, joinTextblockForward } from 'prosemirror-commands';
4
+ import { withinNesting } from './nesting.js';
5
+ import { splitTaskItem } from './tasks.js';
4
6
  /** The caret's enclosing `list_item` when it sits at the very start of that item's
5
7
  * first block: the position both structural keys branch on. `null` otherwise
6
8
  * (mid-text, a non-first block of a multi-paragraph item, a non-empty selection),
@@ -72,41 +74,68 @@ function paragraphAboveList(itemType, paragraph) {
72
74
  };
73
75
  }
74
76
  /**
75
- * Backspace at the start of a list's first item → lift it: one level out for a
76
- * nested item, out of the list entirely (to a paragraph) at the top.
77
+ * Backspace at the start of an item → lift it: one level out for a nested item, out of
78
+ * the list to a paragraph at the top, whichever item it is. The first press takes off
79
+ * the item's own shape and joins nothing; the press after is the join
80
+ * ({@link joinOntoList}).
81
+ *
82
+ * Lifting a later item out splits its list, and an ordered list's items past the cut
83
+ * keep their numbers: the list after what the item held starts where they were, where a
84
+ * fresh `ordered_list` would count from 1 again.
77
85
  */
78
- function liftAtListStart(itemType) {
86
+ function liftAtItemStart(itemType) {
79
87
  return (state, dispatch) => {
80
88
  const at = atItemStart(state, itemType);
81
- if (!at || at.itemIndex !== 0)
89
+ if (!at)
82
90
  return false;
83
- return liftListItem(itemType)(state, dispatch);
91
+ const list = state.doc.nodeAt(at.listPos);
92
+ const cut = list?.type.name === 'ordered_list' && !at.nested && at.itemIndex < list.childCount - 1;
93
+ const listEnd = at.listPos + (list?.nodeSize ?? 0);
94
+ return liftListItem(itemType)(state, dispatch &&
95
+ ((tr) => {
96
+ if (cut)
97
+ renumberEndingAt(tr, tr.mapping.map(listEnd, -1), list.attrs.start + at.itemIndex + 1);
98
+ dispatch(tr);
99
+ }));
84
100
  };
85
101
  }
102
+ /** Start the ordered list ending at `end` at `start`: the items a lift cut off, which
103
+ * end where the whole list did, past whatever the lifted item held. */
104
+ function renumberEndingAt(tr, end, start) {
105
+ const list = tr.doc.resolve(end).nodeBefore;
106
+ if (list?.type.name === 'ordered_list' && list.attrs.start !== start)
107
+ tr.setNodeMarkup(end - list.nodeSize, undefined, { ...list.attrs, start });
108
+ }
86
109
  /**
87
- * Backspace at the start of any later item → join its text into the previous
88
- * item's last block, so the two items become one line.
110
+ * Backspace at the head of a block that stands right after a list → join its text into
111
+ * the list's last line, as two paragraphs join. The base keymap's answer there wraps the
112
+ * block into a fresh item instead, which undoes the lift that just took it out of the
113
+ * list ({@link liftAtItemStart}) and leaves the key toggling.
89
114
  *
90
- * Not the base keymap's `joinBackward`, which merges the block and leaves
91
- * `list_item(paragraph, paragraph)`: the item's marker gone while its text stays
92
- * on its own line, which the reference quill typesets as an unnumbered
93
- * continuation paragraph.
94
- *
95
- * Only between two blocks of one kind. A fence is not a line to put another line on,
96
- * and the join across that edge would retype one side's whole text as the other's
97
- * (`code.ts`, which refuses the same join between siblings); the base keymap's answer
98
- * stands here instead, merging the items with each block kept whole.
115
+ * Between a fence and prose the text does not join: a fence is not a line to put another
116
+ * line on, and the join would retype one side's whole text as the other's (`code.ts`,
117
+ * which refuses the same join between siblings). The block moves into the list's last
118
+ * item whole instead, a block of that item under its first.
99
119
  */
100
- function mergeIntoPreviousItem(itemType) {
120
+ function joinOntoList(itemType) {
101
121
  return (state, dispatch, view) => {
102
- const at = atItemStart(state, itemType);
103
- if (!at || at.itemIndex === 0)
122
+ const { $from, empty } = state.selection;
123
+ if (!empty || !$from.parent.isTextblock || $from.parentOffset !== 0)
124
+ return false;
125
+ const before = state.doc.resolve($from.before()).nodeBefore;
126
+ if (before?.firstChild?.type !== itemType)
104
127
  return false;
105
- const into = lastBlockIn(state.doc.nodeAt(at.listPos)?.child(at.itemIndex - 1));
106
- if (!into || !!into.type.spec.code !== !!state.selection.$from.parent.type.spec.code) {
128
+ const into = lastBlockIn(before);
129
+ if (!into)
107
130
  return false;
131
+ if (!!into.type.spec.code === !!$from.parent.type.spec.code)
132
+ return joinTextblockBackward(state, dispatch, view);
133
+ if (dispatch) {
134
+ const at = $from.before() - 2;
135
+ const tr = state.tr.delete($from.before(), $from.after()).insert(at, $from.parent);
136
+ dispatch(tr.setSelection(TextSelection.create(tr.doc, at + 1)).scrollIntoView());
108
137
  }
109
- return joinTextblockBackward(state, dispatch, view);
138
+ return true;
110
139
  };
111
140
  }
112
141
  /** Whether the caret sits inside a `list_item` at any depth: a quote's paragraph
@@ -128,6 +157,10 @@ function inItem($pos, itemType) {
128
157
  * The reflection carries the edit and not the caret, which stays where the key was
129
158
  * pressed: a lift moves the item below, and Delete does not follow it.
130
159
  *
160
+ * An empty item is the exception: its line is the whole of what Delete edits, so the
161
+ * item below moves up into it rather than being lifted, and the empty item is spent. A
162
+ * fence below takes no prose line (`joinOntoList`), so there the empty line goes alone.
163
+ *
131
164
  * Declining wherever the chain does is what keeps this to the list's own seams: a
132
165
  * boundary inside one item, and the list's outer edge, stay the base keymap's, where
133
166
  * the two keys already answer alike.
@@ -142,13 +175,35 @@ function backspaceBelow(itemType, press) {
142
175
  const below = Selection.near(state.doc.resolve($from.after()), 1);
143
176
  if (below.from <= $from.pos)
144
177
  return false;
178
+ if ($from.parent.content.size === 0) {
179
+ if (!!below.$from.parent.type.spec.code === !!$from.parent.type.spec.code)
180
+ return joinTextblockForward(state, dispatch);
181
+ if (dispatch) {
182
+ const tr = state.tr.deleteRange($from.before(), $from.after());
183
+ dispatch(tr.setSelection(Selection.near(tr.doc.resolve(tr.mapping.map(below.from)), 1)));
184
+ }
185
+ return true;
186
+ }
145
187
  const pressed = $from.pos;
146
188
  const keepCaret = (tr) => dispatch?.(tr.setSelection(TextSelection.create(tr.doc, tr.mapping.map(pressed))));
147
189
  return press(EditorState.create({ doc: state.doc, selection: below }), dispatch && keepCaret);
148
190
  };
149
191
  }
192
+ /** Tab → `sinkListItem`, declined where the sink would nest past `MAX_NESTING_DEPTH`
193
+ * (`nesting.ts`), so Tab there leaves the body as it does on an item with nothing to
194
+ * sink under. */
195
+ function sinkWithin(itemType) {
196
+ const sink = sinkListItem(itemType);
197
+ return (state, dispatch) => {
198
+ let sunk;
199
+ if (!sink(state, (tr) => (sunk = tr)) || !sunk || !withinNesting(sunk.doc))
200
+ return false;
201
+ dispatch?.(sunk);
202
+ return true;
203
+ };
204
+ }
150
205
  /**
151
- * The list bindings for a block-schema leaf: `{}` for the inline/plaintext
206
+ * The list bindings for a block-schema leaf: `{}` for the one-line and plain
152
207
  * schemas, which declare no list nodes.
153
208
  *
154
209
  * `Tab` / `Shift-Tab` hold the list link of the body's Tab chain. Surfaces that own
@@ -160,11 +215,12 @@ function backspaceBelow(itemType, press) {
160
215
  * shell structural keymap (VISUAL_EDITOR §Settled and open).
161
216
  *
162
217
  * `Enter` is a chain in precedence order: the escape-above gesture, then
163
- * `splitListItem`, then `liftListItem`. The middle link carries two behaviors of
218
+ * `splitListItem` (`splitTaskItem`, which leaves a task item's new half an open task),
219
+ * then `liftListItem`. The middle link carries two behaviors of
164
220
  * its own: it splits a non-empty item, and on an empty item it either splits the
165
221
  * wrapping item (a nested empty item, so Enter lifts exactly one level) or bails
166
- * so the third link lifts a top-level empty item out to a paragraph. `Backspace`
167
- * forks on the item's index: the first item lifts, any later one merges. `Delete` is
222
+ * so the third link lifts a top-level empty item out to a paragraph. `Backspace` lifts
223
+ * an item at its start, and joins a block onto the list it stands after. `Delete` is
168
224
  * that same chain at the seam below rather than a second table of cases beside it, so
169
225
  * the two keys cannot come to differ over a seam this link reaches. An empty line is
170
226
  * not one: the block link takes it ahead of this chain (`blocks.ts`), the caret's own
@@ -175,11 +231,11 @@ export function listKeymap(schema) {
175
231
  const paragraph = schema.nodes.paragraph;
176
232
  if (!itemType || !paragraph)
177
233
  return {};
178
- const backspace = chainCommands(liftAtListStart(itemType), mergeIntoPreviousItem(itemType));
234
+ const backspace = chainCommands(liftAtItemStart(itemType), joinOntoList(itemType));
179
235
  return {
180
- Tab: sinkListItem(itemType),
236
+ Tab: sinkWithin(itemType),
181
237
  'Shift-Tab': liftListItem(itemType),
182
- Enter: chainCommands(paragraphAboveList(itemType, paragraph), splitListItem(itemType), liftListItem(itemType)),
238
+ Enter: chainCommands(paragraphAboveList(itemType, paragraph), splitTaskItem(itemType), liftListItem(itemType)),
183
239
  Backspace: backspace,
184
240
  Delete: backspaceBelow(itemType, backspace)
185
241
  };
@@ -0,0 +1,20 @@
1
+ import type { Node as PMNode } from 'prosemirror-model';
2
+ import { Plugin } from 'prosemirror-state';
3
+ /** The store's bound on the containers around one line: a commit nesting past it is
4
+ * refused, and `@quillmark/wasm` exports no constant to read it from. */
5
+ export declare const MAX_NESTING_DEPTH = 12;
6
+ /** The most containers any block in `doc` stands inside, counted as the store counts
7
+ * them: a `list_item` or a `blockquote` is one, the list around an item none, and no
8
+ * node holds an element (`decode.ts`). */
9
+ export declare function nestingDepth(doc: PMNode): number;
10
+ /** Whether `doc` nests within {@link MAX_NESTING_DEPTH}: what a command that nests asks
11
+ * of the doc it would dispatch. */
12
+ export declare function withinNesting(doc: PMNode): boolean;
13
+ /**
14
+ * Refuses a transaction leaving `doc` past {@link MAX_NESTING_DEPTH}, which the commit
15
+ * would lose: a paste or a drop into a deep item, whose slice the import bounded alone.
16
+ * The nesting commands ask first and keep their key; this is what reaches the rest, and
17
+ * a refused paste lands nothing. Past the bound, a transaction passes where it leaves the
18
+ * deepest level no deeper, so a body that arrived that deep keeps its Enter.
19
+ */
20
+ export declare function nestingPlugin(): Plugin;
@@ -0,0 +1,62 @@
1
+ import { Plugin } from 'prosemirror-state';
2
+ import { ReplaceAroundStep, ReplaceStep } from 'prosemirror-transform';
3
+ /** The store's bound on the containers around one line: a commit nesting past it is
4
+ * refused, and `@quillmark/wasm` exports no constant to read it from. */
5
+ export const MAX_NESTING_DEPTH = 12;
6
+ /** The most containers any block in `doc` stands inside, counted as the store counts
7
+ * them: a `list_item` or a `blockquote` is one, the list around an item none, and no
8
+ * node holds an element (`decode.ts`). */
9
+ export function nestingDepth(doc) {
10
+ const walk = (node, depth) => {
11
+ let deepest = depth;
12
+ node.forEach((child) => {
13
+ if (child.isTextblock || child.isAtom)
14
+ return;
15
+ const name = child.type.name;
16
+ const d = depth + (name === 'list_item' || name === 'blockquote' ? 1 : 0);
17
+ deepest = Math.max(deepest, walk(child, d));
18
+ });
19
+ return deepest;
20
+ };
21
+ return walk(doc, 0);
22
+ }
23
+ /** Whether `doc` nests within {@link MAX_NESTING_DEPTH}: what a command that nests asks
24
+ * of the doc it would dispatch. */
25
+ export function withinNesting(doc) {
26
+ return nestingDepth(doc) <= MAX_NESTING_DEPTH;
27
+ }
28
+ /** Whether a step of `tr` can move a block under another container: any replace but
29
+ * inline content inside one textblock, which is every keystroke's. Mark and attribute
30
+ * steps move no node. */
31
+ function mayNest(tr) {
32
+ return tr.steps.some((step, i) => {
33
+ if (step instanceof ReplaceAroundStep)
34
+ return true;
35
+ if (!(step instanceof ReplaceStep))
36
+ return false;
37
+ const doc = tr.docs[i];
38
+ const $from = doc.resolve(step.from);
39
+ if (!$from.parent.isTextblock || !$from.sameParent(doc.resolve(step.to)))
40
+ return true;
41
+ let block = false;
42
+ step.slice.content.forEach((node) => (block ||= node.isBlock));
43
+ return block;
44
+ });
45
+ }
46
+ /**
47
+ * Refuses a transaction leaving `doc` past {@link MAX_NESTING_DEPTH}, which the commit
48
+ * would lose: a paste or a drop into a deep item, whose slice the import bounded alone.
49
+ * The nesting commands ask first and keep their key; this is what reaches the rest, and
50
+ * a refused paste lands nothing. Past the bound, a transaction passes where it leaves the
51
+ * deepest level no deeper, so a body that arrived that deep keeps its Enter.
52
+ */
53
+ export function nestingPlugin() {
54
+ return new Plugin({
55
+ filterTransaction: (tr) => {
56
+ if (!tr.docChanged || !mayNest(tr))
57
+ return true;
58
+ const after = nestingDepth(tr.doc);
59
+ return after <= MAX_NESTING_DEPTH || after <= nestingDepth(tr.before);
60
+ }
61
+ });
62
+ }
@@ -29,6 +29,14 @@ li.ProseMirror-selectednode::after {
29
29
  border: var(--_qm-ring);
30
30
  left: calc(var(--_qm-space-4) * -1);
31
31
  }
32
+ /* A node selection is drawn while its leaf holds the focus, as a text selection is: a
33
+ ring left behind on a field the focus has moved off names a subject no key reaches. */
34
+ .ProseMirror:not(:focus-within) .ProseMirror-selectednode {
35
+ outline: none;
36
+ }
37
+ .ProseMirror:not(:focus-within) li.ProseMirror-selectednode::after {
38
+ border: none;
39
+ }
32
40
 
33
41
  /* The gap cursor (`prosemirror-gapcursor`, imported beside this file): a caret
34
42
  between two blocks, where the document holds no text position for an ordinary one.
@@ -140,6 +148,20 @@ li.ProseMirror-selectednode::after {
140
148
  font-size: inherit;
141
149
  }
142
150
 
151
+ /* A fence's language at the label rung in its top corner, read off the `data-lang` the
152
+ node writes (`schema.ts`). Generated content, so it is never text the caret reaches
153
+ or a copy takes; the code's lines wrap around it. Read-only: the language is changed
154
+ in source mode. */
155
+ .ProseMirror pre[data-lang]::before {
156
+ content: attr(data-lang);
157
+ float: inline-end;
158
+ margin-inline-start: var(--_qm-space-3);
159
+ font-size: var(--_qm-text-label);
160
+ color: var(--_qm-ink-label);
161
+ user-select: none;
162
+ pointer-events: none;
163
+ }
164
+
143
165
  /* A link takes the surface's ink (the UA's blue is the `#8cf` defect again) and a
144
166
  dotted underline. Dotted rather than solid because `underline` is a mark in the
145
167
  same schema, and a solid rule renders an authored `<u>` and a link identically in a
@@ -183,6 +205,59 @@ li.ProseMirror-selectednode::after {
183
205
  list-style: revert;
184
206
  }
185
207
 
208
+ /* A task item's checkbox (`tasks.ts`) stands after the item's marker and ahead of its
209
+ first line, the text wrapping back under it, as the paper sets the box. Its face is a
210
+ matrix tick's: the input drawn `appearance: none` as an edged well, held at the
211
+ accent, the check glyph over it in the surface ink. A disabled box is a held leaf's
212
+ or a copy's, and takes no press. */
213
+ .ProseMirror .qm-task {
214
+ position: relative;
215
+ float: left;
216
+ display: flex;
217
+ align-items: center;
218
+ height: 1lh;
219
+ margin-right: var(--_qm-space-2);
220
+ }
221
+ .ProseMirror .qm-task-box {
222
+ appearance: none;
223
+ margin: 0;
224
+ width: var(--_qm-glyph-control);
225
+ height: var(--_qm-glyph-control);
226
+ border: var(--_qm-border-width) solid var(--_qm-border);
227
+ border-radius: calc(var(--_qm-radius-inner) / 2);
228
+ background: var(--_qm-surface-well);
229
+ cursor: pointer;
230
+ transition:
231
+ background-color var(--_qm-duration-fast) var(--_qm-ease-reverse),
232
+ border-color var(--_qm-duration-fast) var(--_qm-ease-reverse);
233
+ }
234
+ .ProseMirror .qm-task-box:checked {
235
+ background: var(--_qm-accent);
236
+ border-color: var(--_qm-accent);
237
+ }
238
+ .ProseMirror .qm-task-box:disabled {
239
+ cursor: default;
240
+ }
241
+ .ProseMirror .qm-task-box:focus-visible {
242
+ outline: var(--_qm-ring);
243
+ outline-offset: var(--_qm-ring-offset);
244
+ }
245
+ .ProseMirror .qm-task-mark {
246
+ position: absolute;
247
+ top: 50%;
248
+ left: 0;
249
+ width: var(--_qm-glyph-control);
250
+ height: var(--_qm-glyph-control);
251
+ translate: 0 -50%;
252
+ pointer-events: none;
253
+ color: var(--_qm-surface);
254
+ opacity: 0;
255
+ transition: opacity var(--_qm-duration-fast) var(--_qm-ease-reverse);
256
+ }
257
+ .ProseMirror .qm-task:has(:checked) .qm-task-mark {
258
+ opacity: 1;
259
+ }
260
+
186
261
  /* Six levels, one weight, no size step. The quill reads no level at all, so a size
187
262
  ramp here would be the editor predicting a hierarchy the paper does not set; the
188
263
  level stays in the model, which is where the codec round-trips it. It is also all
@@ -227,9 +302,12 @@ li.ProseMirror-selectednode::after {
227
302
  empty line the caret holds. Only while the leaf has the focus: the decoration follows
228
303
  the selection, which an unfocused leaf keeps. Upright, being chrome about the key
229
304
  rather than an invitation in the document's voice, and never over a ghost, which is
230
- the leaf's own word for the same empty line. */
305
+ the leaf's own word for the same empty line. It comes up once the caret has rested
306
+ there, a new empty line starting the wait over. */
231
307
  .ProseMirror-focused .qm-slash-hint:not(.qm-prose-placeholder)[data-slash-hint]::after {
232
308
  content: attr(data-slash-hint);
309
+ animation: qm-slash-hint-in var(--_qm-reveal) both;
310
+ animation-delay: var(--_qm-hint-wait);
233
311
  display: block;
234
312
  margin-top: -1lh;
235
313
  white-space: nowrap;
@@ -239,6 +317,12 @@ li.ProseMirror-selectednode::after {
239
317
  pointer-events: none;
240
318
  }
241
319
 
320
+ @keyframes qm-slash-hint-in {
321
+ from {
322
+ opacity: 0;
323
+ }
324
+ }
325
+
242
326
  /* ── The table island ────────────────────────────────────────────────────────
243
327
 
244
328
  The one construct in this file with chrome in it. The rest of the stylesheet
@@ -313,19 +397,16 @@ li.ProseMirror-selectednode::after {
313
397
  .qm-table {
314
398
  border-collapse: separate;
315
399
  border-spacing: 0;
316
- border: var(--_qm-border-width) solid var(--_qm-border);
317
- border-radius: var(--_qm-radius-inner);
400
+ border: var(--_qm-border-width) solid var(--_qm-rule);
401
+ border-radius: var(--_qm-radius);
318
402
  }
319
- /* Two tiers of rule, because a table draws two different things. The frame and the
320
- header's rule are structure and keep the full hairline; the lines between cells are
321
- rhythm and drop to the faint rung, so the grid reads as one object with divisions
322
- rather than as a stack of boxes. `separate` with zero spacing rather than
323
- `collapse`: a collapsed border cannot be rounded, and the two tiers would fight
324
- over which one wins an edge. */
403
+ /* One rule at one strength, the frame's and every line between cells, so the grid
404
+ reads at the weight of the type it holds. `separate` with zero spacing rather than
405
+ `collapse`: a collapsed border cannot be rounded. */
325
406
  .qm-table-cell {
326
407
  position: relative;
327
- border-right: var(--_qm-border-width) solid var(--_qm-border-faint);
328
- border-bottom: var(--_qm-border-width) solid var(--_qm-border-faint);
408
+ border-right: var(--_qm-border-width) solid var(--_qm-rule);
409
+ border-bottom: var(--_qm-border-width) solid var(--_qm-rule);
329
410
  padding: var(--_qm-space) var(--_qm-space-2);
330
411
  vertical-align: top;
331
412
  font-weight: inherit;
@@ -339,13 +420,29 @@ li.ProseMirror-selectednode::after {
339
420
  }
340
421
  /* The header row is the one weight distinction the island draws: `header` is a
341
422
  separate field in the model rather than a flag on a row, so the surface says so.
342
- It takes a tone with it, which is the same move a field makes: the well rung, a
343
- band cut into the paper rather than a layer stacked on it, and a rung either way
344
- rather than a shadow. The rest of a table's type is paper's. */
423
+ It takes a tone with it, the band rung, a step toward the ink at both poles. The
424
+ rest of a table's type is paper's. */
345
425
  .qm-table-header-row .qm-table-cell {
346
426
  font-weight: var(--_qm-weight-strong);
347
- background: var(--_qm-surface-well);
348
- border-bottom: var(--_qm-border-width) solid var(--_qm-border);
427
+ background: var(--_qm-surface-band);
428
+ }
429
+ /* The corner cells take the frame's corner, inside its stroke, so a header's band or a
430
+ selection's wash keeps to the rounded edge rather than painting a square one over
431
+ it. The first row is the colgroup's next sibling's, whichever section holds it. */
432
+ .qm-table > colgroup + * > tr:first-child > .qm-table-cell:first-child {
433
+ border-start-start-radius: calc(var(--_qm-radius) - var(--_qm-border-width));
434
+ }
435
+ .qm-table > colgroup + * > tr:first-child > .qm-table-cell:last-child {
436
+ border-start-end-radius: calc(var(--_qm-radius) - var(--_qm-border-width));
437
+ }
438
+ .qm-table
439
+ > :is(:last-child, thead:has(+ tbody:empty))
440
+ > tr:last-child
441
+ > .qm-table-cell:first-child {
442
+ border-end-start-radius: calc(var(--_qm-radius) - var(--_qm-border-width));
443
+ }
444
+ .qm-table > :is(:last-child, thead:has(+ tbody:empty)) > tr:last-child > .qm-table-cell:last-child {
445
+ border-end-end-radius: calc(var(--_qm-radius) - var(--_qm-border-width));
349
446
  }
350
447
  /* A cell holds a whole nested view. Its measure floor keeps an empty table from
351
448
  collapsing to a row of hairlines with nothing to aim at, and the caret is the
@@ -400,20 +497,16 @@ li.ProseMirror-selectednode::after {
400
497
  color: var(--_qm-ink);
401
498
  cursor: pointer;
402
499
  }
403
- /* The bar every one of them draws: a pill the length of what it acts on, a rhythm rung
404
- thick where the target is the tap floor. Stated once, so a grip and an add bar cannot
405
- drift into two shapes.
406
-
407
- One tone, not a hover ladder: the control is drawn only while it is the one the
408
- pointer is on or the one a gesture is about, so a resting tone under that would be a
409
- rung nothing ever reaches. The selected grip's tint below is the one exception, and it
410
- says which line the next command acts on rather than where the pointer is. */
500
+ /* The pill every one of them draws, a rhythm rung thick where the target is the tap
501
+ floor. Stated once, so a grip and an add bar cannot drift into two shapes. The selected
502
+ grip's tint below says which line the next command acts on rather than where the
503
+ pointer is. */
411
504
  :is(.qm-table-grip-bar, .qm-table-add-bar) {
412
505
  display: flex;
413
506
  align-items: center;
414
507
  justify-content: center;
415
508
  border-radius: var(--_qm-radius-pill);
416
- background: var(--_qm-border);
509
+ background: var(--_qm-surface-band);
417
510
  transition:
418
511
  background-color var(--_qm-duration-fast) var(--_qm-ease-reverse),
419
512
  color var(--_qm-duration-fast) var(--_qm-ease-reverse);
@@ -424,32 +517,34 @@ li.ProseMirror-selectednode::after {
424
517
  width: var(--_qm-space-3);
425
518
  height: var(--_qm-space-3);
426
519
  }
427
- /* A grip runs along the line it names; an add bar runs across the axis it appends to.
428
- So the two families map one `data-axis` onto opposite orientations, and orientation
429
- is the one thing the rule above cannot share with them: a column's grip is as wide as
430
- its column, and the bar that adds a column is as tall as the table. */
431
- .qm-table-grip[data-axis='column'] .qm-table-grip-bar,
520
+ /* An add bar runs across the axis it appends to, the length of the table; a grip is a
521
+ short pill centred on the line it names, the tap floor long. */
432
522
  .qm-table-add[data-axis='row'] .qm-table-add-bar {
433
523
  width: 100%;
434
524
  height: var(--_qm-space-3);
435
525
  }
436
- .qm-table-grip[data-axis='row'] .qm-table-grip-bar,
437
526
  .qm-table-add[data-axis='column'] .qm-table-add-bar {
438
527
  width: var(--_qm-space-3);
439
528
  height: 100%;
440
529
  }
530
+ .qm-table-grip[data-axis='column'] .qm-table-grip-bar {
531
+ width: var(--_qm-tap-min);
532
+ height: var(--_qm-space-3);
533
+ }
534
+ .qm-table-grip[data-axis='row'] .qm-table-grip-bar {
535
+ width: var(--_qm-space-3);
536
+ height: var(--_qm-tap-min);
537
+ }
441
538
 
442
- /* One grip per line, and it is a bar rather than a glyph: what a grip acts on is the
443
- row or column it sits against, and spanning that line says so better than an icon
444
- could. The bar carries the grip's own marks — two rows of three, the shape a thing
445
- you can pick up has everywhere else — so the control reads as draggable before it is
446
- dragged.
539
+ /* One grip per line. The pill carries the grip's own marks at the label ink — two rows
540
+ of three, the shape a thing you can pick up has everywhere else — so the control reads
541
+ as draggable before it is dragged.
447
542
 
448
- The target is the band and the bar is drawn inside it: a grip spans its line on one
449
- axis and the tap floor on the other, and the bar is drawn thinner inside that. A grip
450
- sized to its own bar would be a target the thickness of the mark on it, on a control
451
- the pointer has to find. */
543
+ The target is the band and the pill is drawn inside it: a grip spans its line on one
544
+ axis and the tap floor on the other. A grip sized to its own pill would be a target
545
+ the size of the mark on it, on a control the pointer has to find. */
452
546
  .qm-table-grip {
547
+ color: var(--_qm-ink-label);
453
548
  /* The pointer owns this gesture, not the page's scroll: a grip drags. */
454
549
  touch-action: none;
455
550
  }
@@ -488,37 +583,24 @@ li.ProseMirror-selectednode::after {
488
583
  height: var(--_qm-tap-min);
489
584
  }
490
585
  /* The reveal is the control's own (CODEC §"The table island"): one comes up while the
491
- pointer is on that control, while it holds the focus, or while it is what a gesture is
492
- about — the selected line's grip, and the lifted one, which is drawn through a drag the
493
- pointer has travelled off. The controls' boxes meet at no point, so what is drawn is
494
- the one line the pointer has picked out.
586
+ pointer is on that control, or while it is what a gesture is about — the selected line's grip, and the lifted one, which is drawn through a drag the
587
+ pointer has travelled off. A cell under the pointer draws its row's and its column's
588
+ grips (`table-view.ts` §`onHover`), one pill on each axis and none anywhere else.
495
589
 
496
590
  Opacity rather than absence, so every control keeps its box and is hit-tested wherever
497
- it sits. A grip's reach is the pointer's alone and it holds no focus, so it carries no
498
- focus rung: what a keyboard draws a line with is the cell the selection runs through
499
- (`table-view.ts` §`cellKeys`), and the wash and the selected grip's tint are what say
500
- so. The two bars are in the tab order, the Tab that declines off the last cell landing
501
- on them, so the rung there draws one a keyboard has reached. */
591
+ it sits. A grip's reach and a bar's are the pointer's alone and neither holds focus, so
592
+ neither carries a focus rung: what a keyboard draws a line with is the cell the
593
+ selection runs through (`table-view.ts` §`cellKeys`), and the wash and the selected
594
+ grip's tint are what say so. */
502
595
  :is(.qm-table-grip, .qm-table-add) {
503
596
  opacity: 0;
504
597
  transition: opacity var(--_qm-duration-fast) var(--_qm-ease-reverse);
505
598
  }
506
- .qm-table-add:is(:hover, :focus-visible),
507
- .qm-table-grip:is(:hover, [aria-pressed='true'], .qm-table-lifted) {
599
+ .qm-table-add:hover,
600
+ .qm-table-grip:is([data-near], :hover, [aria-pressed='true'], .qm-table-lifted) {
508
601
  opacity: 1;
509
602
  }
510
603
 
511
- /* The ring, on the two add bars and nowhere else in this file. A prose leaf's focus
512
- indicator is its caret and a cell's is too, so the outline is dropped there; a control
513
- that comes up out of nothing has the reveal, and a ring under a keyboard is what says
514
- the focus is there rather than on the paper behind it. Declared here rather than
515
- reached for as `.qm-focus-ring`: that recipe is the visual tier's stylesheet, and the
516
- island is inside the leaf, which is the codec's. */
517
- .qm-table-add:focus-visible {
518
- outline: var(--_qm-ring);
519
- outline-offset: var(--_qm-ring-offset);
520
- }
521
-
522
604
  /* A weighted table spans the leaf, laid out fixed, so the stored weights are the
523
605
  proportions its columns draw, as the page's fractional columns span the text block
524
606
  (`table-view.ts` §`weigh`). An auto-fit table keeps its content's width. */
@@ -582,7 +664,7 @@ li.ProseMirror-selectednode::after {
582
664
  display: none;
583
665
  }
584
666
  /* The header toggle hangs under the first row's last cell, flush with the grid's
585
- trailing edge, clear of whatever a held row raises over its first. */
667
+ trailing edge. */
586
668
  .qm-table-align[data-header] {
587
669
  margin-inline: auto 0;
588
670
  }
@@ -23,7 +23,7 @@ export declare const plaintextSchema: Schema<string, never>;
23
23
  */
24
24
  export declare const cellSchema: Schema<"hard_break", string>;
25
25
  /**
26
- * A `plaintext` field without `inline`: paragraphs and hard breaks, no marks, no
26
+ * A `text` field declaring `multiline: true`: paragraphs and hard breaks, no marks, no
27
27
  * islands, no containers, which is the shape `Content::is_plain` admits. Mark-free
28
28
  * for the reason {@link plaintextSchema} is.
29
29
  *
@@ -38,7 +38,7 @@ export declare function leafSchema(opts: {
38
38
  plaintext?: boolean;
39
39
  inline?: boolean;
40
40
  }): Schema;
41
- /** Whether `schema` can carry formatting at all: false for the two plaintext schemas.
41
+ /** Whether `schema` can carry formatting at all: false for the two mark-free schemas.
42
42
  * What a mark command asks before it offers itself. */
43
43
  export declare function hasMarks(schema: Schema): boolean;
44
44
  /** True for the one-line schemas, which declare no `hard_break`; decode branches on it. */