@dxos/ui-editor 0.8.4-main.9be5663bfe → 0.8.4-main.bc2380dfbc
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/LICENSE +102 -5
- package/README.md +1 -1
- package/dist/lib/browser/index.mjs +995 -879
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/types/index.mjs +26 -6
- package/dist/lib/browser/types/index.mjs.map +4 -4
- package/dist/lib/node-esm/index.mjs +995 -878
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/types/index.mjs +27 -6
- package/dist/lib/node-esm/types/index.mjs.map +4 -4
- package/dist/types/src/defaults.d.ts +1 -0
- package/dist/types/src/defaults.d.ts.map +1 -1
- package/dist/types/src/extensions/annotations.d.ts.map +1 -1
- package/dist/types/src/extensions/autocomplete/autocomplete.d.ts.map +1 -1
- package/dist/types/src/extensions/autocomplete/match.d.ts.map +1 -1
- package/dist/types/src/extensions/autocomplete/placeholder.d.ts +5 -2
- package/dist/types/src/extensions/autocomplete/placeholder.d.ts.map +1 -1
- package/dist/types/src/extensions/autocomplete/typeahead.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/cursor.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/sync.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/update-automerge.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/update-codemirror.d.ts.map +1 -1
- package/dist/types/src/extensions/awareness/awareness-provider.d.ts.map +1 -1
- package/dist/types/src/extensions/awareness/awareness.d.ts.map +1 -1
- package/dist/types/src/extensions/blast.d.ts.map +1 -1
- package/dist/types/src/extensions/comments.d.ts.map +1 -1
- package/dist/types/src/extensions/debug.d.ts.map +1 -1
- package/dist/types/src/extensions/dnd.d.ts.map +1 -1
- package/dist/types/src/extensions/factories.d.ts +2 -1
- package/dist/types/src/extensions/factories.d.ts.map +1 -1
- package/dist/types/src/extensions/factories.test.d.ts +2 -0
- package/dist/types/src/extensions/factories.test.d.ts.map +1 -0
- package/dist/types/src/extensions/focus.d.ts +1 -1
- package/dist/types/src/extensions/index.d.ts +2 -4
- package/dist/types/src/extensions/index.d.ts.map +1 -1
- package/dist/types/src/extensions/json.d.ts.map +1 -1
- package/dist/types/src/extensions/listener.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/changes.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/debug.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/decorate.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/highlight.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/image.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/link.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/table.d.ts.map +1 -1
- package/dist/types/src/extensions/mention.d.ts.map +1 -1
- package/dist/types/src/extensions/outliner/menu.d.ts.map +1 -1
- package/dist/types/src/extensions/outliner/outliner.d.ts.map +1 -1
- package/dist/types/src/extensions/outliner/selection.d.ts.map +1 -1
- package/dist/types/src/extensions/outliner/tree.d.ts.map +1 -1
- package/dist/types/src/extensions/preview/preview.d.ts.map +1 -1
- package/dist/types/src/extensions/replacer.d.ts.map +1 -1
- package/dist/types/src/extensions/scrolling/auto-scroll.d.ts +18 -0
- package/dist/types/src/extensions/scrolling/auto-scroll.d.ts.map +1 -0
- package/dist/types/src/extensions/scrolling/crawler.d.ts +75 -0
- package/dist/types/src/extensions/scrolling/crawler.d.ts.map +1 -0
- package/dist/types/src/extensions/scrolling/index.d.ts +5 -0
- package/dist/types/src/extensions/scrolling/index.d.ts.map +1 -0
- package/dist/types/src/extensions/scrolling/scroll-past-end.d.ts.map +1 -0
- package/dist/types/src/extensions/scrolling/scroller.d.ts +16 -0
- package/dist/types/src/extensions/scrolling/scroller.d.ts.map +1 -0
- package/dist/types/src/extensions/selection.d.ts.map +1 -1
- package/dist/types/src/extensions/snippets.d.ts +10 -0
- package/dist/types/src/extensions/snippets.d.ts.map +1 -0
- package/dist/types/src/extensions/submit.d.ts.map +1 -1
- package/dist/types/src/extensions/tags/extended-markdown.d.ts.map +1 -1
- package/dist/types/src/extensions/tags/fader.d.ts.map +1 -1
- package/dist/types/src/extensions/tags/index.d.ts +3 -1
- package/dist/types/src/extensions/tags/index.d.ts.map +1 -1
- package/dist/types/src/extensions/tags/typewriter.d.ts +43 -0
- package/dist/types/src/extensions/tags/typewriter.d.ts.map +1 -0
- package/dist/types/src/extensions/tags/typewriter.test.d.ts +2 -0
- package/dist/types/src/extensions/tags/typewriter.test.d.ts.map +1 -0
- package/dist/types/src/extensions/tags/xml-block-decoration.d.ts +31 -0
- package/dist/types/src/extensions/tags/xml-block-decoration.d.ts.map +1 -0
- package/dist/types/src/extensions/tags/xml-formatting.d.ts +24 -0
- package/dist/types/src/extensions/tags/xml-formatting.d.ts.map +1 -0
- package/dist/types/src/extensions/tags/xml-tags.d.ts +1 -8
- package/dist/types/src/extensions/tags/xml-tags.d.ts.map +1 -1
- package/dist/types/src/extensions/tags/xml-util.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +0 -1
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/styles/theme.d.ts.map +1 -1
- package/dist/types/src/types/types.d.ts +4 -4
- package/dist/types/src/types/types.d.ts.map +1 -1
- package/dist/types/src/util/cursor.d.ts.map +1 -1
- package/dist/types/src/util/debug.d.ts.map +1 -1
- package/dist/types/src/util/decorations.d.ts.map +1 -1
- package/dist/types/src/util/dom.d.ts.map +1 -1
- package/dist/types/src/util/facet.d.ts.map +1 -1
- package/dist/types/src/util/util.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +33 -36
- package/src/defaults.ts +12 -3
- package/src/extensions/autocomplete/placeholder.ts +37 -18
- package/src/extensions/automerge/automerge.test.tsx +35 -9
- package/src/extensions/automerge/automerge.ts +2 -4
- package/src/extensions/factories.test.ts +88 -0
- package/src/extensions/factories.ts +25 -6
- package/src/extensions/index.ts +2 -4
- package/src/extensions/outliner/outliner.ts +1 -1
- package/src/extensions/{auto-scroll.ts → scrolling/auto-scroll.ts} +88 -23
- package/src/extensions/{scroller.ts → scrolling/crawler.ts} +63 -45
- package/src/extensions/scrolling/index.ts +8 -0
- package/src/extensions/{scroll-past-end.ts → scrolling/scroll-past-end.ts} +6 -6
- package/src/extensions/scrolling/scroller.ts +27 -0
- package/src/extensions/snippets.ts +67 -0
- package/src/extensions/tags/index.ts +3 -1
- package/src/extensions/tags/testing/text.md +36 -0
- package/src/extensions/tags/testing/text.txt +35 -0
- package/src/extensions/tags/{wire.test.ts → typewriter.test.ts} +2 -2
- package/src/extensions/tags/typewriter.ts +594 -0
- package/src/extensions/tags/xml-block-decoration.ts +123 -0
- package/src/extensions/tags/xml-formatting.ts +125 -0
- package/src/extensions/tags/xml-tags.ts +6 -32
- package/src/extensions/tags/xml-util.test.ts +90 -3
- package/src/extensions/tags/xml-util.ts +62 -5
- package/src/index.ts +0 -1
- package/src/styles/theme.ts +20 -10
- package/src/types/types.ts +10 -2
- package/src/typings.d.ts +8 -0
- package/dist/lib/browser/chunk-HL3YF6WC.mjs +0 -22
- package/dist/lib/browser/chunk-HL3YF6WC.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-YJZGD3LY.mjs +0 -24
- package/dist/lib/node-esm/chunk-YJZGD3LY.mjs.map +0 -7
- package/dist/types/src/extensions/auto-scroll.d.ts +0 -8
- package/dist/types/src/extensions/auto-scroll.d.ts.map +0 -1
- package/dist/types/src/extensions/scroll-past-end.d.ts.map +0 -1
- package/dist/types/src/extensions/scroller.d.ts +0 -63
- package/dist/types/src/extensions/scroller.d.ts.map +0 -1
- package/dist/types/src/extensions/tags/wire.d.ts +0 -23
- package/dist/types/src/extensions/tags/wire.d.ts.map +0 -1
- package/dist/types/src/extensions/tags/wire.test.d.ts +0 -2
- package/dist/types/src/extensions/tags/wire.test.d.ts.map +0 -1
- package/dist/types/src/extensions/typewriter.d.ts +0 -10
- package/dist/types/src/extensions/typewriter.d.ts.map +0 -1
- package/src/extensions/tags/wire.ts +0 -459
- package/src/extensions/typewriter.ts +0 -68
- /package/dist/types/src/extensions/{scroll-past-end.d.ts → scrolling/scroll-past-end.d.ts} +0 -0
|
@@ -9,17 +9,27 @@ import { addEventListener, combine, throttle } from '@dxos/async';
|
|
|
9
9
|
import { Domino } from '@dxos/ui';
|
|
10
10
|
import { getSize } from '@dxos/ui-theme';
|
|
11
11
|
|
|
12
|
-
import {
|
|
12
|
+
import { crawlerActiveEffect, crawlerLineEffect } from './crawler';
|
|
13
13
|
|
|
14
14
|
/** Enable or disable autoscroll. */
|
|
15
15
|
export const autoScrollEffect = StateEffect.define<boolean>();
|
|
16
16
|
|
|
17
|
-
export type AutoScrollProps = {
|
|
17
|
+
export type AutoScrollProps = {
|
|
18
|
+
/**
|
|
19
|
+
* If true, immediately jump to the bottom and re-pin whenever the size of the
|
|
20
|
+
* document view (the scroll container) changes — e.g. when a sidebar toggles
|
|
21
|
+
* or the window is resized. This avoids the visible "stuck near bottom" gap
|
|
22
|
+
* that otherwise appears because the previous `scrollTop` no longer reaches
|
|
23
|
+
* the new content height.
|
|
24
|
+
* @default true
|
|
25
|
+
*/
|
|
26
|
+
scrollOnResize?: boolean;
|
|
27
|
+
};
|
|
18
28
|
|
|
19
29
|
/**
|
|
20
30
|
* Extension that supports pinning the scroll position and automatically scrolls to the bottom when content is added.
|
|
21
31
|
*/
|
|
22
|
-
export const autoScroll = (
|
|
32
|
+
export const autoScroll = ({ scrollOnResize = true }: AutoScrollProps = {}) => {
|
|
23
33
|
let buttonContainer: HTMLDivElement | undefined;
|
|
24
34
|
let isPinned = true;
|
|
25
35
|
let jumpPending = false;
|
|
@@ -44,11 +54,11 @@ export const autoScroll = (_: AutoScrollProps = {}) => {
|
|
|
44
54
|
if (enabled) {
|
|
45
55
|
setPinned(true);
|
|
46
56
|
view.dispatch({
|
|
47
|
-
effects:
|
|
57
|
+
effects: crawlerActiveEffect.of(true),
|
|
48
58
|
});
|
|
49
59
|
} else {
|
|
50
60
|
view.dispatch({
|
|
51
|
-
effects:
|
|
61
|
+
effects: crawlerActiveEffect.of(false),
|
|
52
62
|
});
|
|
53
63
|
}
|
|
54
64
|
}
|
|
@@ -86,9 +96,7 @@ export const autoScroll = (_: AutoScrollProps = {}) => {
|
|
|
86
96
|
const delta = scrollHeight - scrollTop - clientHeight;
|
|
87
97
|
if (delta > 0) {
|
|
88
98
|
setPinned(true);
|
|
89
|
-
view.dispatch({
|
|
90
|
-
effects: scrollerCrawlEffect.of(true),
|
|
91
|
-
});
|
|
99
|
+
view.dispatch({ effects: crawlerActiveEffect.of(true) });
|
|
92
100
|
} else if (delta < -1) {
|
|
93
101
|
setPinned(false);
|
|
94
102
|
}
|
|
@@ -101,25 +109,82 @@ export const autoScroll = (_: AutoScrollProps = {}) => {
|
|
|
101
109
|
}
|
|
102
110
|
}),
|
|
103
111
|
|
|
112
|
+
// Re-pin and jump to bottom when the scroll container itself resizes (e.g. sidebar toggle,
|
|
113
|
+
// window resize). Doc-driven height changes are handled by the updateListener above; this
|
|
114
|
+
// observer covers the case where the viewport changes while the doc length is unchanged.
|
|
115
|
+
scrollOnResize
|
|
116
|
+
? ViewPlugin.fromClass(
|
|
117
|
+
class {
|
|
118
|
+
private readonly observer: ResizeObserver;
|
|
119
|
+
private firstObservation = true;
|
|
120
|
+
private destroyed = false;
|
|
121
|
+
constructor(view: EditorView) {
|
|
122
|
+
// Throttle so a continuous drag-resize (or a flurry of layout changes) coalesces
|
|
123
|
+
// into a single re-pin per ~50ms instead of dispatching every frame.
|
|
124
|
+
const onResize = throttle(() => {
|
|
125
|
+
if (this.destroyed || !enabled) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
setPinned(true);
|
|
130
|
+
requestAnimationFrame(() => {
|
|
131
|
+
if (this.destroyed) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
view.scrollDOM.scrollTo({ top: view.scrollDOM.scrollHeight, behavior: 'instant' });
|
|
136
|
+
view.dispatch({ effects: crawlerActiveEffect.of(false) });
|
|
137
|
+
});
|
|
138
|
+
}, 50);
|
|
139
|
+
|
|
140
|
+
this.observer = new ResizeObserver(() => {
|
|
141
|
+
// Skip the initial fire that ResizeObserver emits on `observe()`.
|
|
142
|
+
if (this.firstObservation) {
|
|
143
|
+
this.firstObservation = false;
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
onResize();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
this.observer.observe(view.scrollDOM);
|
|
150
|
+
}
|
|
151
|
+
destroy() {
|
|
152
|
+
this.destroyed = true;
|
|
153
|
+
this.observer.disconnect();
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
)
|
|
157
|
+
: [],
|
|
158
|
+
|
|
104
159
|
// Detect user scroll and unpin (or re-pin if scrolled to the bottom).
|
|
105
160
|
ViewPlugin.fromClass(
|
|
106
161
|
class {
|
|
107
162
|
private readonly cleanup: () => void;
|
|
108
163
|
constructor(view: EditorView) {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
164
|
+
// Re-pin check is throttled so the listener doesn't thrash while scrolling, but
|
|
165
|
+
// unpinning must be immediate — otherwise content arriving during the throttle
|
|
166
|
+
// window re-applies the crawl effect and yanks the viewport back to the bottom.
|
|
167
|
+
const onUserScroll = throttle(() => {
|
|
168
|
+
requestAnimationFrame(() => {
|
|
169
|
+
const { scrollTop, scrollHeight, clientHeight } = view.scrollDOM;
|
|
170
|
+
const delta = scrollHeight - scrollTop - clientHeight;
|
|
171
|
+
// Sub-pixel tolerance: fractional scroll positions can leave delta at e.g. 0.5
|
|
172
|
+
// even when the user is visually at the bottom.
|
|
173
|
+
const pinned = Math.abs(delta) <= 1;
|
|
174
|
+
setPinned(pinned);
|
|
175
|
+
if (!pinned) {
|
|
176
|
+
view.dispatch({ effects: crawlerActiveEffect.of(false) });
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
}, 500);
|
|
180
|
+
|
|
181
|
+
this.cleanup = createUserScrollDetector(view.scrollDOM, () => {
|
|
182
|
+
if (isPinned) {
|
|
183
|
+
setPinned(false);
|
|
184
|
+
view.dispatch({ effects: crawlerActiveEffect.of(false) });
|
|
185
|
+
}
|
|
186
|
+
onUserScroll();
|
|
187
|
+
});
|
|
123
188
|
}
|
|
124
189
|
destroy() {
|
|
125
190
|
this.cleanup();
|
|
@@ -141,7 +206,7 @@ export const autoScroll = (_: AutoScrollProps = {}) => {
|
|
|
141
206
|
.on('click', () => {
|
|
142
207
|
setPinned(true);
|
|
143
208
|
view.dispatch({
|
|
144
|
-
effects:
|
|
209
|
+
effects: crawlerLineEffect.of({ line: -1, position: 'end', behavior: 'smooth' }),
|
|
145
210
|
});
|
|
146
211
|
});
|
|
147
212
|
|
|
@@ -40,10 +40,10 @@ export type ScrollToProps = {
|
|
|
40
40
|
};
|
|
41
41
|
|
|
42
42
|
/** Scroll to a specific line. */
|
|
43
|
-
export const
|
|
43
|
+
export const crawlerLineEffect = StateEffect.define<ScrollToProps>();
|
|
44
44
|
|
|
45
45
|
/** Start/stop crawling the end of the document. */
|
|
46
|
-
export const
|
|
46
|
+
export const crawlerActiveEffect = StateEffect.define<boolean>();
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
49
|
* Helper function to scroll to a specific line.
|
|
@@ -51,22 +51,29 @@ export const scrollerCrawlEffect = StateEffect.define<boolean>();
|
|
|
51
51
|
*/
|
|
52
52
|
export const scrollToLine = (view: EditorView, options: ScrollToProps) => {
|
|
53
53
|
view.dispatch({
|
|
54
|
-
effects:
|
|
54
|
+
effects: crawlerLineEffect.of(options),
|
|
55
55
|
});
|
|
56
56
|
};
|
|
57
57
|
|
|
58
|
-
export type
|
|
58
|
+
export type CrawlerOptions = {
|
|
59
59
|
/** Threshold in px to trigger scroll from bottom. */
|
|
60
60
|
overScroll?: number;
|
|
61
61
|
};
|
|
62
62
|
|
|
63
63
|
/**
|
|
64
|
-
*
|
|
64
|
+
* Imperative scroll-control primitive for streaming editor views.
|
|
65
|
+
*
|
|
66
|
+
* Owns the scroll-related effects (`crawlerLineEffect`, `crawlerActiveEffect`), the spring
|
|
67
|
+
* crawler that follows the bottom of the document, and the `.cm-scroller` theme (overflow,
|
|
68
|
+
* scrollbar styling, and the `::after` overscroll spacer).
|
|
69
|
+
*
|
|
70
|
+
* Use directly for jump-to-line navigation, or pair with `autoScroll` for a pin-to-bottom
|
|
71
|
+
* streaming policy. The composite `streamScroll` bundles both for the common case.
|
|
65
72
|
*/
|
|
66
|
-
export const
|
|
73
|
+
export const crawler = ({ overScroll = 0 }: CrawlerOptions = {}) => {
|
|
67
74
|
// ViewPlugin to manage scroll animations.
|
|
68
|
-
const
|
|
69
|
-
class
|
|
75
|
+
const crawlerPlugin = ViewPlugin.fromClass(
|
|
76
|
+
class CrawlerPlugin {
|
|
70
77
|
private readonly crawler: ReturnType<typeof createCrawler>;
|
|
71
78
|
constructor(private readonly view: EditorView) {
|
|
72
79
|
this.crawler = createCrawler(this.view);
|
|
@@ -126,18 +133,18 @@ export const scroller = ({ overScroll = 0 }: ScrollerOptions = {}) => {
|
|
|
126
133
|
);
|
|
127
134
|
|
|
128
135
|
return [
|
|
129
|
-
|
|
136
|
+
crawlerPlugin,
|
|
130
137
|
|
|
131
|
-
// Listen for effect.
|
|
138
|
+
// Listen for effect.
|
|
132
139
|
EditorView.updateListener.of((update) => {
|
|
133
140
|
update.transactions.forEach((transaction) => {
|
|
134
141
|
try {
|
|
135
|
-
const plugin = update.view.plugin(
|
|
142
|
+
const plugin = update.view.plugin(crawlerPlugin);
|
|
136
143
|
if (plugin) {
|
|
137
144
|
for (const effect of transaction.effects) {
|
|
138
|
-
if (effect.is(
|
|
145
|
+
if (effect.is(crawlerActiveEffect)) {
|
|
139
146
|
plugin.crawl(effect.value);
|
|
140
|
-
} else if (effect.is(
|
|
147
|
+
} else if (effect.is(crawlerLineEffect)) {
|
|
141
148
|
plugin.scroll(effect.value);
|
|
142
149
|
}
|
|
143
150
|
}
|
|
@@ -150,13 +157,13 @@ export const scroller = ({ overScroll = 0 }: ScrollerOptions = {}) => {
|
|
|
150
157
|
|
|
151
158
|
// Styles.
|
|
152
159
|
EditorView.theme({
|
|
153
|
-
'.cm-content': {
|
|
154
|
-
paddingBottom: `${overScroll}px`,
|
|
155
|
-
},
|
|
156
160
|
'.cm-scroller': {
|
|
157
161
|
overflowY: 'scroll',
|
|
158
|
-
|
|
159
|
-
|
|
162
|
+
// Browser scroll-anchoring: when widgets above the viewport resize (e.g. tool blocks
|
|
163
|
+
// expanding their TogglePanel), the browser picks a stable element near the viewport
|
|
164
|
+
// top and adjusts `scrollTop` so the user's view doesn't jump. Auto-scroll's pinning
|
|
165
|
+
// logic still has the final word when pinned (forces scrollTop to scrollHeight).
|
|
166
|
+
overflowAnchor: 'auto',
|
|
160
167
|
},
|
|
161
168
|
'.cm-scroller.cm-hide-scrollbar::-webkit-scrollbar': {
|
|
162
169
|
display: 'none',
|
|
@@ -168,6 +175,16 @@ export const scroller = ({ overScroll = 0 }: ScrollerOptions = {}) => {
|
|
|
168
175
|
'&:hover .cm-scroller::-webkit-scrollbar-thumb': {
|
|
169
176
|
background: 'var(--color-scrollbar-thumb)',
|
|
170
177
|
},
|
|
178
|
+
// Spacer below the last text line. Implemented as a real block pseudo-element
|
|
179
|
+
// (rather than `padding-bottom` on `.cm-content`) so it materializes in the
|
|
180
|
+
// scroller's `scrollHeight` regardless of how `padding` is reset by the base
|
|
181
|
+
// theme or downstream classes — this is what gives auto-scroll its head-room
|
|
182
|
+
// so the last line stays `overScroll` px above the viewport bottom.
|
|
183
|
+
'.cm-content::after': {
|
|
184
|
+
content: '""',
|
|
185
|
+
display: 'block',
|
|
186
|
+
height: `${overScroll}px`,
|
|
187
|
+
},
|
|
171
188
|
'.cm-scroll-button': {
|
|
172
189
|
position: 'absolute',
|
|
173
190
|
bottom: '0.5rem',
|
|
@@ -180,49 +197,48 @@ export const scroller = ({ overScroll = 0 }: ScrollerOptions = {}) => {
|
|
|
180
197
|
/**
|
|
181
198
|
* Creates a smooth crawler that follows the live bottom of a CodeMirror 6 EditorView.
|
|
182
199
|
*
|
|
183
|
-
* Uses a
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
200
|
+
* Uses a critically-damped spring: each frame applies a restoring force toward the
|
|
201
|
+
* target and a damping force opposing current velocity. With damping = 2·ω, the
|
|
202
|
+
* system is critically damped — fastest approach without overshoot. The spring
|
|
203
|
+
* naturally sprints when far behind and eases as it approaches, so streaming
|
|
204
|
+
* content is followed tightly without the jerk of explicit accel/decel state.
|
|
205
|
+
*
|
|
206
|
+
* Integration uses real elapsed wall-clock time so the perceived speed stays
|
|
207
|
+
* constant when requestAnimationFrame is throttled (e.g. low-power mode dropping
|
|
208
|
+
* from 60Hz to 30Hz).
|
|
187
209
|
*
|
|
188
|
-
* @param
|
|
189
|
-
* @param
|
|
190
|
-
* @param
|
|
210
|
+
* @param omega Spring stiffness in rad/s. Higher = snappier follow. ~12–18 feels good.
|
|
211
|
+
* @param snapThreshold Snap-to-target distance threshold in px.
|
|
212
|
+
* @param snapVelocity Snap-to-target velocity threshold in px/s.
|
|
191
213
|
*/
|
|
192
|
-
export function createCrawler(view: EditorView,
|
|
214
|
+
export function createCrawler(view: EditorView, omega = 5, snapThreshold = 5, snapVelocity = 50) {
|
|
193
215
|
const el = view.scrollDOM;
|
|
194
216
|
|
|
195
217
|
let currentTop = 0;
|
|
196
218
|
let velocity = 0;
|
|
197
219
|
let rafId: number | null = null;
|
|
220
|
+
let lastTime = 0;
|
|
221
|
+
|
|
222
|
+
function frame(now: number) {
|
|
223
|
+
// Clamp dt to handle long pauses (tab backgrounded) and the first frame.
|
|
224
|
+
const dt = lastTime === 0 ? 1 / 60 : Math.min(0.1, (now - lastTime) / 1000);
|
|
225
|
+
lastTime = now;
|
|
198
226
|
|
|
199
|
-
function frame() {
|
|
200
227
|
const targetTop = el.scrollHeight - el.clientHeight;
|
|
201
228
|
const delta = targetTop - currentTop;
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
if (absDelta < snapThreshold && Math.abs(velocity) < snapThreshold) {
|
|
229
|
+
if (Math.abs(delta) < snapThreshold && Math.abs(velocity) < snapVelocity) {
|
|
205
230
|
el.scrollTop = targetTop;
|
|
206
231
|
currentTop = targetTop;
|
|
207
232
|
velocity = 0;
|
|
208
233
|
rafId = null;
|
|
234
|
+
lastTime = 0;
|
|
209
235
|
return;
|
|
210
236
|
}
|
|
211
237
|
|
|
212
|
-
//
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
if (velocity !== 0 && (absDelta <= stoppingDistance || direction !== Math.sign(velocity))) {
|
|
217
|
-
// Decelerate: close enough to target or moving the wrong way.
|
|
218
|
-
velocity -= Math.sign(velocity) * Math.min(accel, Math.abs(velocity));
|
|
219
|
-
} else {
|
|
220
|
-
// Accelerate toward target, capped at maxVelocity.
|
|
221
|
-
velocity += direction * accel;
|
|
222
|
-
velocity = Math.sign(velocity) * Math.min(Math.abs(velocity), maxVelocity);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
currentTop += velocity;
|
|
238
|
+
// Critically-damped spring: a = ω²·delta − 2ω·v.
|
|
239
|
+
const accel = omega * omega * delta - 2 * omega * velocity;
|
|
240
|
+
velocity += accel * dt;
|
|
241
|
+
currentTop += velocity * dt;
|
|
226
242
|
el.scrollTop = currentTop;
|
|
227
243
|
rafId = requestAnimationFrame(frame);
|
|
228
244
|
}
|
|
@@ -231,14 +247,16 @@ export function createCrawler(view: EditorView, accel = 0.15, maxVelocity = 1, s
|
|
|
231
247
|
scroll: () => {
|
|
232
248
|
if (rafId === null) {
|
|
233
249
|
currentTop = el.scrollTop;
|
|
250
|
+
lastTime = 0;
|
|
234
251
|
rafId = requestAnimationFrame(frame);
|
|
235
252
|
}
|
|
236
253
|
},
|
|
237
254
|
cancel: () => {
|
|
238
255
|
if (rafId !== null) {
|
|
239
256
|
cancelAnimationFrame(rafId);
|
|
240
|
-
rafId = null;
|
|
241
257
|
velocity = 0;
|
|
258
|
+
lastTime = 0;
|
|
259
|
+
rafId = null;
|
|
242
260
|
}
|
|
243
261
|
},
|
|
244
262
|
};
|
|
@@ -12,15 +12,15 @@ import { EditorView, ViewPlugin } from '@codemirror/view';
|
|
|
12
12
|
*/
|
|
13
13
|
const scrollPastEndPlugin = ViewPlugin.fromClass(
|
|
14
14
|
class {
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
_height = 1_000;
|
|
16
|
+
_attrs: { style: string } | null = { style: `padding-bottom: ${this._height}px` };
|
|
17
17
|
|
|
18
18
|
update({ view }: { view: EditorView }) {
|
|
19
19
|
const lastLineBlock = view.lineBlockAt(view.state.doc.length);
|
|
20
20
|
const height = view.dom.clientHeight - lastLineBlock.height - view.documentPadding.top - 0.5;
|
|
21
|
-
if (height >= 0 && height !== this.
|
|
22
|
-
this.
|
|
23
|
-
this.
|
|
21
|
+
if (height >= 0 && height !== this._height) {
|
|
22
|
+
this._height = height;
|
|
23
|
+
this._attrs = { style: `padding-bottom: ${height}px` };
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
},
|
|
@@ -28,5 +28,5 @@ const scrollPastEndPlugin = ViewPlugin.fromClass(
|
|
|
28
28
|
|
|
29
29
|
export const scrollPastEnd = (): Extension => [
|
|
30
30
|
scrollPastEndPlugin,
|
|
31
|
-
EditorView.contentAttributes.of((view) => view.plugin(scrollPastEndPlugin)?.
|
|
31
|
+
EditorView.contentAttributes.of((view) => view.plugin(scrollPastEndPlugin)?._attrs ?? null),
|
|
32
32
|
];
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Extension } from '@codemirror/state';
|
|
6
|
+
|
|
7
|
+
import { isTruthy } from '@dxos/util';
|
|
8
|
+
|
|
9
|
+
import { type AutoScrollProps, autoScroll as autoScrollExtension } from './auto-scroll';
|
|
10
|
+
import { type CrawlerOptions, crawler } from './crawler';
|
|
11
|
+
|
|
12
|
+
export type ScrollerOptions = CrawlerOptions &
|
|
13
|
+
AutoScrollProps & {
|
|
14
|
+
/**
|
|
15
|
+
* Include the auto-scroll policy (pin-to-bottom, user-scroll unpin, scroll-to-bottom button).
|
|
16
|
+
* Set to `false` to get only the crawler primitive (line jumps, theme, overscroll spacer).
|
|
17
|
+
* @default true
|
|
18
|
+
*/
|
|
19
|
+
autoScroll?: boolean;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Composite scroll extension for streaming editor views (chat threads, transcripts, logs).
|
|
24
|
+
*/
|
|
25
|
+
export const scroller = ({ overScroll, scrollOnResize, autoScroll = true }: ScrollerOptions = {}): Extension[] => {
|
|
26
|
+
return [crawler({ overScroll }), autoScroll && autoScrollExtension({ scrollOnResize })].filter(isTruthy);
|
|
27
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Extension } from '@codemirror/state';
|
|
6
|
+
import { keymap } from '@codemirror/view';
|
|
7
|
+
|
|
8
|
+
const defaultItems = ['hello world!', 'this is a test.', 'this is [DXOS](https://dxos.org)'];
|
|
9
|
+
|
|
10
|
+
export type SnippetsOptions = {
|
|
11
|
+
delay?: number;
|
|
12
|
+
items?: string[];
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Configurable plugin that lets users cycle through pre-configured input snippets.
|
|
17
|
+
*/
|
|
18
|
+
// TODO(burdon): Review https://github.com/sergeche/codemirror-movie?tab=readme-ov-file
|
|
19
|
+
export const snippets = ({ delay = 75, items = defaultItems }: SnippetsOptions = {}): Extension => {
|
|
20
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
21
|
+
let index = 0; // TODO(burdon): Make global.
|
|
22
|
+
|
|
23
|
+
return [
|
|
24
|
+
keymap.of([
|
|
25
|
+
{
|
|
26
|
+
// Reset.
|
|
27
|
+
key: "alt-meta-'",
|
|
28
|
+
run: () => {
|
|
29
|
+
clearTimeout(timer);
|
|
30
|
+
index = 0;
|
|
31
|
+
return true;
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
// Next snippet.
|
|
36
|
+
// TODO(burdon): Press 1-9 to select snippet?
|
|
37
|
+
key: "Shift-Meta-'",
|
|
38
|
+
run: (view) => {
|
|
39
|
+
clearTimeout(timer);
|
|
40
|
+
// TODO(burdon): Add space if needed.
|
|
41
|
+
const text = items[index++];
|
|
42
|
+
if (index === items?.length) {
|
|
43
|
+
index = 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let offset = 0;
|
|
47
|
+
const insert = (delayMs = 0) => {
|
|
48
|
+
timer = setTimeout(() => {
|
|
49
|
+
const pos = view.state.selection.main.head;
|
|
50
|
+
view.dispatch({
|
|
51
|
+
changes: { from: pos, insert: text[offset++] },
|
|
52
|
+
selection: { anchor: pos + 1 },
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (offset < text.length) {
|
|
56
|
+
insert(Math.random() * delay * (text[offset] === ' ' ? 2 : 1));
|
|
57
|
+
}
|
|
58
|
+
}, delayMs);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
insert();
|
|
62
|
+
return true;
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
]),
|
|
66
|
+
];
|
|
67
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<prompt>summarize the posts</prompt>
|
|
2
|
+
<reasoning>The user wants me to summarize the posts related to the magazine in context. Let me first load the magazine object to understand its structure, and then list the candidate posts.</reasoning>
|
|
3
|
+
<status>Loading the magazine and listing candidate posts…</status>
|
|
4
|
+
<toolCall id="toolu_01WMMcsaHxTvuGxcMh2CkfL4" />
|
|
5
|
+
<toolCall id="toolu_01CxWNvXZ4jExfLEJumfMzqm" />
|
|
6
|
+
<reasoning>The magazine has 10 posts already curated, and there are no uncurated candidate posts. The user wants me to summarize the posts that are already in the magazine. Let me load them.</reasoning>
|
|
7
|
+
<status>No uncurated candidates — loading the 10 posts already in the magazine…</status>
|
|
8
|
+
<toolCall id="toolu_0152ftz33GL5M1DDi8GGDHi2" />
|
|
9
|
+
<reasoning>The magazine has 10 posts, but several are duplicates. Let me deduplicate by title/guid and summarize the unique articles. Unique articles:
|
|
10
|
+
|
|
11
|
+
1. "FBI affidavit quotes White House press dinner shooting suspect expressing rage at 'a pedophile, rapist and traitor' – US politics live" - About Cole Allen's manifesto and charges, plus King Charles visiting Trump at the White House.
|
|
12
|
+
2. "Australia news live: UK inquiry says 'cracks already beginning to show' on Aukus; search for missing five-year-old near Alice Springs grows" - Live blog covering Aukus inquiry, David Brat nominated as ambassador to Australia, fuel reserve crisis.
|
|
13
|
+
3. "Rebel Wilson gives evidence in high-profile defamation case against star of directorial debut" - Rebel Wilson testifying in defamation case brought by Charlotte MacInnes.
|
|
14
|
+
4. "Baby on board: US woman gives birth on Delta flight" - Ashley Blair delivered her baby aboard a Delta flight traveling from Atlanta to Portland.
|
|
15
|
+
5. "'Shortcomings and failures' could sink Aukus nuclear submarines plan, UK inquiry warns" - A UK House of Commons defence committee report identifies significant funding gaps threatening the Aukus submarine program.</reasoning>
|
|
16
|
+
Here's a summary of the **5 unique articles** currently in the magazine (some posts are duplicates):
|
|
17
|
+
|
|
18
|
+
6. **FBI affidavit quotes White House press dinner shooting suspect**
|
|
19
|
+
Federal charges filed against Cole Allen include excerpts from a manifesto he emailed to family and friends before attempting to charge into the White House Correspondents' Dinner. Separately, King Charles and Queen Camilla visited Donald Trump at the White House for tea and a beehive tour.
|
|
20
|
+
|
|
21
|
+
7. **Australia news live: Aukus 'cracks' & missing child search**
|
|
22
|
+
A UK House of Commons inquiry reveals "shortcomings and failings" in the Aukus submarine program. Also covered: David Brat nominated as the next US ambassador to Australia, and a push to extend Australia's fuel reserves from 30 to 60 days amid rising security concerns.
|
|
23
|
+
|
|
24
|
+
8. **Rebel Wilson testifies in defamation case**
|
|
25
|
+
Hollywood star Rebel Wilson gave evidence in a defamation lawsuit brought by Charlotte MacInnes, the lead actor of Wilson's directorial debut _The Deb_. The case centres on claims MacInnes confided to Wilson about being uncomfortable with a bathing request from a colleague.
|
|
26
|
+
|
|
27
|
+
9. **Baby born on Delta flight**
|
|
28
|
+
Passenger Ashley Blair went into labor on a packed Delta flight from Atlanta to Portland. Two off-duty EMTs on the plane delivered baby Brielle Renee just before landing — using a shoelace as a tourniquet for an IV. Mother and baby were healthy on arrival.
|
|
29
|
+
|
|
30
|
+
10. **UK inquiry warns Aukus submarine plan at risk**
|
|
31
|
+
A detailed House of Commons defence committee report finds UK submarine funding has "cracks already beginning to show," with chronic under-investment in shipbuilding and only one submarine yard (Barrow-in-Furness) already behind schedule. Australia has committed A$4.6 billion to help, but the committee warns political drift could derail the entire program.
|
|
32
|
+
<prompt>how many articles are there?</prompt>
|
|
33
|
+
<reasoning>The user is asking about the number of articles. From my previous analysis, there are 10 posts in the magazine, but only 5 unique articles (the rest are duplicates). Let me answer based on what I already know.</reasoning>
|
|
34
|
+
There are **10 posts** in the magazine, but only **5 unique articles** — each article appears twice (as duplicates from the two feeds).
|
|
35
|
+
<prompt>say your name inside an xml tag</prompt>
|
|
36
|
+
<name>Kai</name>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<prompt>summarize the posts</prompt>
|
|
2
|
+
<reasoning>The user wants me to summarize the posts related to the magazine in context. Let me first load the magazine object to understand its structure, and then list the candidate posts.</reasoning>
|
|
3
|
+
<status>Loading the magazine and listing candidate posts…</status>
|
|
4
|
+
<toolCall id="toolu_01WMMcsaHxTvuGxcMh2CkfL4" />
|
|
5
|
+
<toolCall id="toolu_01CxWNvXZ4jExfLEJumfMzqm" />
|
|
6
|
+
<reasoning>The magazine has 10 posts already curated, and there are no uncurated candidate posts. The user wants me to summarize the posts that are already in the magazine. Let me load them.</reasoning>
|
|
7
|
+
<status>No uncurated candidates — loading the 10 posts already in the magazine…</status>
|
|
8
|
+
<toolCall id="toolu_0152ftz33GL5M1DDi8GGDHi2" />
|
|
9
|
+
<reasoning>The magazine has 10 posts, but several are duplicates. Let me deduplicate by title/guid and summarize the unique articles. Unique articles:
|
|
10
|
+
1. "FBI affidavit quotes White House press dinner shooting suspect expressing rage at 'a pedophile, rapist and traitor' – US politics live" - About Cole Allen's manifesto and charges, plus King Charles visiting Trump at the White House.
|
|
11
|
+
2. "Australia news live: UK inquiry says 'cracks already beginning to show' on Aukus; search for missing five-year-old near Alice Springs grows" - Live blog covering Aukus inquiry, David Brat nominated as ambassador to Australia, fuel reserve crisis.
|
|
12
|
+
3. "Rebel Wilson gives evidence in high-profile defamation case against star of directorial debut" - Rebel Wilson testifying in defamation case brought by Charlotte MacInnes.
|
|
13
|
+
4. "Baby on board: US woman gives birth on Delta flight" - Ashley Blair delivered her baby aboard a Delta flight traveling from Atlanta to Portland.
|
|
14
|
+
5. "'Shortcomings and failures' could sink Aukus nuclear submarines plan, UK inquiry warns" - A UK House of Commons defence committee report identifies significant funding gaps threatening the Aukus submarine program.</reasoning>
|
|
15
|
+
Here's a summary of the **5 unique articles** currently in the magazine (some posts are duplicates):
|
|
16
|
+
|
|
17
|
+
1. **FBI affidavit quotes White House press dinner shooting suspect**
|
|
18
|
+
Federal charges filed against Cole Allen include excerpts from a manifesto he emailed to family and friends before attempting to charge into the White House Correspondents' Dinner. Separately, King Charles and Queen Camilla visited Donald Trump at the White House for tea and a beehive tour.
|
|
19
|
+
|
|
20
|
+
2. **Australia news live: Aukus 'cracks' & missing child search**
|
|
21
|
+
A UK House of Commons inquiry reveals "shortcomings and failings" in the Aukus submarine program. Also covered: David Brat nominated as the next US ambassador to Australia, and a push to extend Australia's fuel reserves from 30 to 60 days amid rising security concerns.
|
|
22
|
+
|
|
23
|
+
3. **Rebel Wilson testifies in defamation case**
|
|
24
|
+
Hollywood star Rebel Wilson gave evidence in a defamation lawsuit brought by Charlotte MacInnes, the lead actor of Wilson's directorial debut *The Deb*. The case centres on claims MacInnes confided to Wilson about being uncomfortable with a bathing request from a colleague.
|
|
25
|
+
|
|
26
|
+
4. **Baby born on Delta flight**
|
|
27
|
+
Passenger Ashley Blair went into labor on a packed Delta flight from Atlanta to Portland. Two off-duty EMTs on the plane delivered baby Brielle Renee just before landing — using a shoelace as a tourniquet for an IV. Mother and baby were healthy on arrival.
|
|
28
|
+
|
|
29
|
+
5. **UK inquiry warns Aukus submarine plan at risk**
|
|
30
|
+
A detailed House of Commons defence committee report finds UK submarine funding has "cracks already beginning to show," with chronic under-investment in shipbuilding and only one submarine yard (Barrow-in-Furness) already behind schedule. Australia has committed A$4.6 billion to help, but the committee warns political drift could derail the entire program.
|
|
31
|
+
<prompt>how many articles are there?</prompt>
|
|
32
|
+
<reasoning>The user is asking about the number of articles. From my previous analysis, there are 10 posts in the magazine, but only 5 unique articles (the rest are duplicates). Let me answer based on what I already know.</reasoning>
|
|
33
|
+
There are **10 posts** in the magazine, but only **5 unique articles** — each article appears twice (as duplicates from the two feeds).
|
|
34
|
+
<prompt>say your name inside an xml tag</prompt>
|
|
35
|
+
<name>Kai</name>
|
|
@@ -6,9 +6,9 @@ import { describe, test } from 'vitest';
|
|
|
6
6
|
|
|
7
7
|
import { trim } from '@dxos/util';
|
|
8
8
|
|
|
9
|
-
import { xmlElementLength } from './
|
|
9
|
+
import { xmlElementLength } from './typewriter';
|
|
10
10
|
|
|
11
|
-
describe('
|
|
11
|
+
describe('typewriter', () => {
|
|
12
12
|
test('xmlElementLength handles hyphenated custom element names', ({ expect }) => {
|
|
13
13
|
const inner = 'Hello';
|
|
14
14
|
const el = `<dom-widget>${inner}</dom-widget>`;
|