@dxos/react-ui-editor 0.3.10-next.ef70620 → 0.3.11-main.1caa3af

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 (161) hide show
  1. package/dist/lib/browser/index.mjs +2174 -935
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts +66 -0
  5. package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -0
  6. package/dist/types/src/components/TextEditor/TextEditor.d.ts +9 -4
  7. package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
  8. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +12 -4
  9. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
  10. package/dist/types/src/components/TextEditor/extensions/autocomplete.d.ts +10 -0
  11. package/dist/types/src/components/TextEditor/extensions/autocomplete.d.ts.map +1 -0
  12. package/dist/types/src/components/TextEditor/extensions/{basic/bundle.d.ts → basic.d.ts} +3 -2
  13. package/dist/types/src/components/TextEditor/extensions/basic.d.ts.map +1 -0
  14. package/dist/types/src/components/TextEditor/extensions/blast.d.ts +25 -0
  15. package/dist/types/src/components/TextEditor/extensions/blast.d.ts.map +1 -0
  16. package/dist/types/src/components/TextEditor/extensions/code.d.ts +2 -0
  17. package/dist/types/src/components/TextEditor/extensions/code.d.ts.map +1 -0
  18. package/dist/types/src/components/TextEditor/extensions/comments.d.ts +48 -0
  19. package/dist/types/src/components/TextEditor/extensions/comments.d.ts.map +1 -0
  20. package/dist/types/src/components/TextEditor/extensions/hr.d.ts +2 -0
  21. package/dist/types/src/components/TextEditor/extensions/hr.d.ts.map +1 -0
  22. package/dist/types/src/components/TextEditor/extensions/image.d.ts +4 -0
  23. package/dist/types/src/components/TextEditor/extensions/image.d.ts.map +1 -0
  24. package/dist/types/src/components/TextEditor/extensions/index.d.ts +14 -2
  25. package/dist/types/src/components/TextEditor/extensions/index.d.ts.map +1 -1
  26. package/dist/types/src/components/TextEditor/extensions/link.d.ts +11 -0
  27. package/dist/types/src/components/TextEditor/extensions/link.d.ts.map +1 -0
  28. package/dist/types/src/components/TextEditor/extensions/listener.d.ts +9 -0
  29. package/dist/types/src/components/TextEditor/extensions/listener.d.ts.map +1 -0
  30. package/dist/types/src/components/TextEditor/extensions/markdown/bundle.d.ts +10 -1
  31. package/dist/types/src/components/TextEditor/extensions/markdown/bundle.d.ts.map +1 -1
  32. package/dist/types/src/components/TextEditor/extensions/markdown/highlight.d.ts +36 -0
  33. package/dist/types/src/components/TextEditor/extensions/markdown/highlight.d.ts.map +1 -0
  34. package/dist/types/src/components/TextEditor/extensions/markdown/index.d.ts +1 -1
  35. package/dist/types/src/components/TextEditor/extensions/markdown/index.d.ts.map +1 -1
  36. package/dist/types/src/components/TextEditor/extensions/mention.d.ts +6 -0
  37. package/dist/types/src/components/TextEditor/extensions/mention.d.ts.map +1 -0
  38. package/dist/types/src/components/TextEditor/extensions/mermaid.d.ts +3 -0
  39. package/dist/types/src/components/TextEditor/extensions/mermaid.d.ts.map +1 -0
  40. package/dist/types/src/components/TextEditor/extensions/table.d.ts +8 -0
  41. package/dist/types/src/components/TextEditor/extensions/table.d.ts.map +1 -0
  42. package/dist/types/src/components/TextEditor/extensions/tasklist.d.ts +3 -0
  43. package/dist/types/src/components/TextEditor/extensions/tasklist.d.ts.map +1 -0
  44. package/dist/types/src/components/TextEditor/extensions/tooltip.d.ts +12 -0
  45. package/dist/types/src/components/TextEditor/extensions/tooltip.d.ts.map +1 -0
  46. package/dist/types/src/components/TextEditor/extensions/typewriter.d.ts +10 -0
  47. package/dist/types/src/components/TextEditor/extensions/typewriter.d.ts.map +1 -0
  48. package/dist/types/src/components/TextEditor/extensions/util.d.ts +5 -0
  49. package/dist/types/src/components/TextEditor/extensions/util.d.ts.map +1 -0
  50. package/dist/types/src/components/TextEditor/index.d.ts +2 -0
  51. package/dist/types/src/components/TextEditor/index.d.ts.map +1 -1
  52. package/dist/types/src/components/TextEditor/themes/default.d.ts +17 -0
  53. package/dist/types/src/components/TextEditor/themes/default.d.ts.map +1 -0
  54. package/dist/types/src/components/TextEditor/themes/index.d.ts +2 -0
  55. package/dist/types/src/components/TextEditor/themes/index.d.ts.map +1 -0
  56. package/dist/types/src/hooks/automerge/automerge.stories.d.ts +15 -0
  57. package/dist/types/src/hooks/automerge/automerge.stories.d.ts.map +1 -0
  58. package/dist/types/src/hooks/automerge/handle.d.ts.map +1 -0
  59. package/dist/types/src/{automerge/automerge-plugin → hooks/automerge}/index.d.ts +1 -1
  60. package/dist/types/src/hooks/automerge/index.d.ts.map +1 -0
  61. package/dist/types/src/hooks/automerge/plugin.d.ts.map +1 -0
  62. package/dist/types/src/{automerge/automerge-plugin/PatchSemaphore.d.ts → hooks/automerge/semaphore.d.ts} +4 -1
  63. package/dist/types/src/hooks/automerge/semaphore.d.ts.map +1 -0
  64. package/dist/types/src/{automerge/automerge-plugin/codeMirrorToAm.d.ts → hooks/automerge/update-automerge.d.ts} +2 -3
  65. package/dist/types/src/hooks/automerge/update-automerge.d.ts.map +1 -0
  66. package/dist/types/src/hooks/automerge/update-codemirror.d.ts +5 -0
  67. package/dist/types/src/hooks/automerge/update-codemirror.d.ts.map +1 -0
  68. package/dist/types/src/hooks/index.d.ts +0 -1
  69. package/dist/types/src/hooks/index.d.ts.map +1 -1
  70. package/dist/types/src/hooks/useTextModel.d.ts +19 -5
  71. package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
  72. package/dist/types/src/{components/TextEditor → hooks/yjs}/yjs.stories.d.ts +2 -1
  73. package/dist/types/src/hooks/yjs/yjs.stories.d.ts.map +1 -0
  74. package/dist/types/src/hooks/yjs/yjs.test.d.ts +2 -0
  75. package/dist/types/src/hooks/yjs/yjs.test.d.ts.map +1 -0
  76. package/dist/types/src/index.d.ts +1 -0
  77. package/dist/types/src/index.d.ts.map +1 -1
  78. package/dist/types/src/styles/markdown.d.ts +14 -11
  79. package/dist/types/src/styles/markdown.d.ts.map +1 -1
  80. package/dist/types/src/testing/replicator.d.ts.map +1 -1
  81. package/package.json +28 -33
  82. package/src/components/TextEditor/MarkdownEditor.stories.tsx +428 -0
  83. package/src/components/TextEditor/TextEditor.stories.tsx +57 -43
  84. package/src/components/TextEditor/TextEditor.tsx +98 -52
  85. package/src/components/TextEditor/extensions/autocomplete.ts +59 -0
  86. package/src/components/TextEditor/extensions/{basic/bundle.ts → basic.ts} +8 -4
  87. package/src/components/TextEditor/extensions/blast.ts +373 -0
  88. package/src/components/TextEditor/extensions/code.ts +99 -0
  89. package/src/components/TextEditor/extensions/comments.ts +388 -0
  90. package/src/components/TextEditor/extensions/hr.ts +74 -0
  91. package/src/components/TextEditor/extensions/image.ts +69 -0
  92. package/src/components/TextEditor/extensions/index.ts +14 -2
  93. package/src/components/TextEditor/extensions/{hyperlink/hyperlinkDecoration.ts → link.ts} +75 -38
  94. package/src/components/TextEditor/extensions/{change.ts → listener.ts} +5 -4
  95. package/src/components/TextEditor/extensions/markdown/bundle.ts +56 -49
  96. package/src/components/TextEditor/extensions/markdown/highlight.ts +204 -0
  97. package/src/components/TextEditor/extensions/markdown/index.ts +1 -1
  98. package/src/components/TextEditor/extensions/mention.ts +31 -0
  99. package/src/components/TextEditor/extensions/mermaid.ts +11 -0
  100. package/src/components/TextEditor/extensions/table.ts +132 -0
  101. package/src/components/TextEditor/extensions/tasklist.ts +121 -0
  102. package/src/components/TextEditor/extensions/{hyperlink/hyperlinkTooltip.tsx → tooltip.ts} +9 -9
  103. package/src/components/TextEditor/extensions/typewriter.ts +68 -0
  104. package/src/components/TextEditor/extensions/util.ts +18 -0
  105. package/src/components/TextEditor/index.ts +3 -0
  106. package/src/components/TextEditor/themes/default.ts +302 -0
  107. package/src/components/TextEditor/themes/index.ts +5 -0
  108. package/src/{automerge → hooks/automerge}/automerge.stories.tsx +7 -5
  109. package/src/{automerge/automerge-plugin → hooks/automerge}/plugin.ts +2 -1
  110. package/src/{automerge/automerge-plugin/PatchSemaphore.ts → hooks/automerge/semaphore.ts} +13 -9
  111. package/src/{automerge/automerge-plugin/codeMirrorToAm.ts → hooks/automerge/update-automerge.ts} +5 -2
  112. package/src/{automerge/automerge-plugin/amToCodemirror.ts → hooks/automerge/update-codemirror.ts} +4 -1
  113. package/src/hooks/index.ts +0 -1
  114. package/src/hooks/useTextModel.ts +101 -42
  115. package/src/{components/TextEditor → hooks/yjs}/yjs.stories.tsx +2 -1
  116. package/src/hooks/yjs/yjs.test.ts +59 -0
  117. package/src/index.ts +1 -0
  118. package/src/styles/markdown.ts +33 -24
  119. package/src/testing/replicator.ts +6 -6
  120. package/dist/types/src/automerge/automerge-plugin/PatchSemaphore.d.ts.map +0 -1
  121. package/dist/types/src/automerge/automerge-plugin/amToCodemirror.d.ts +0 -6
  122. package/dist/types/src/automerge/automerge-plugin/amToCodemirror.d.ts.map +0 -1
  123. package/dist/types/src/automerge/automerge-plugin/codeMirrorToAm.d.ts.map +0 -1
  124. package/dist/types/src/automerge/automerge-plugin/handle.d.ts.map +0 -1
  125. package/dist/types/src/automerge/automerge-plugin/index.d.ts.map +0 -1
  126. package/dist/types/src/automerge/automerge-plugin/plugin.d.ts.map +0 -1
  127. package/dist/types/src/automerge/automerge.stories.d.ts +0 -9
  128. package/dist/types/src/automerge/automerge.stories.d.ts.map +0 -1
  129. package/dist/types/src/components/TextEditor/extensions/basic/bundle.d.ts.map +0 -1
  130. package/dist/types/src/components/TextEditor/extensions/basic/index.d.ts +0 -3
  131. package/dist/types/src/components/TextEditor/extensions/basic/index.d.ts.map +0 -1
  132. package/dist/types/src/components/TextEditor/extensions/basic/theme.d.ts +0 -10
  133. package/dist/types/src/components/TextEditor/extensions/basic/theme.d.ts.map +0 -1
  134. package/dist/types/src/components/TextEditor/extensions/change.d.ts +0 -7
  135. package/dist/types/src/components/TextEditor/extensions/change.d.ts.map +0 -1
  136. package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkDecoration.d.ts +0 -11
  137. package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkDecoration.d.ts.map +0 -1
  138. package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkTooltip.d.ts +0 -8
  139. package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkTooltip.d.ts.map +0 -1
  140. package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkWidget.d.ts +0 -10
  141. package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkWidget.d.ts.map +0 -1
  142. package/dist/types/src/components/TextEditor/extensions/hyperlink/index.d.ts +0 -4
  143. package/dist/types/src/components/TextEditor/extensions/hyperlink/index.d.ts.map +0 -1
  144. package/dist/types/src/components/TextEditor/extensions/markdown/tags.d.ts +0 -17
  145. package/dist/types/src/components/TextEditor/extensions/markdown/tags.d.ts.map +0 -1
  146. package/dist/types/src/components/TextEditor/extensions/markdown/theme.d.ts +0 -21
  147. package/dist/types/src/components/TextEditor/extensions/markdown/theme.d.ts.map +0 -1
  148. package/dist/types/src/components/TextEditor/yjs.stories.d.ts.map +0 -1
  149. package/dist/types/src/hooks/useCollaboration.d.ts +0 -9
  150. package/dist/types/src/hooks/useCollaboration.d.ts.map +0 -1
  151. package/src/components/TextEditor/extensions/basic/index.ts +0 -6
  152. package/src/components/TextEditor/extensions/basic/theme.ts +0 -49
  153. package/src/components/TextEditor/extensions/hyperlink/hyperlinkWidget.tsx +0 -119
  154. package/src/components/TextEditor/extensions/hyperlink/index.ts +0 -7
  155. package/src/components/TextEditor/extensions/markdown/tags.ts +0 -38
  156. package/src/components/TextEditor/extensions/markdown/theme.ts +0 -265
  157. package/src/hooks/useCollaboration.ts +0 -45
  158. package/dist/types/src/{automerge/automerge-plugin → hooks/automerge}/handle.d.ts +0 -0
  159. package/dist/types/src/{automerge/automerge-plugin → hooks/automerge}/plugin.d.ts +0 -0
  160. package/src/{automerge/automerge-plugin → hooks/automerge}/handle.ts +0 -0
  161. package/src/{automerge/automerge-plugin → hooks/automerge}/index.ts +1 -1
@@ -0,0 +1,373 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ // Based on a demo by Joel Besada and Kushagra Gour.
4
+ // https://twitter.com/JoelBesada/status/670343885655293952
5
+ // https://github.com/chinchang/code-blast-codemirror/blob/master/code-blast.js
6
+ //
7
+
8
+ import { type Extension } from '@codemirror/state';
9
+ import { EditorView, keymap } from '@codemirror/view';
10
+ import defaultsDeep from 'lodash.defaultsdeep';
11
+
12
+ import { invariant } from '@dxos/invariant';
13
+
14
+ export type BlastOptions = {
15
+ effect?: number;
16
+ maxParticles: number;
17
+ particleGravity: number;
18
+ particleAlphaFadeout: number;
19
+ particleSize: { min: number; max: number };
20
+ particleNumRange: { min: number; max: number };
21
+ particleVelocityRange: { x: [number, number]; y: [number, number] };
22
+ particleShrinkRate: number;
23
+ shakeIntensity: number;
24
+ color?: () => number[];
25
+ };
26
+
27
+ export const defaultOptions: BlastOptions = {
28
+ effect: 2,
29
+ maxParticles: 200,
30
+ particleGravity: 0.08,
31
+ particleAlphaFadeout: 0.996,
32
+ particleSize: { min: 2, max: 8 },
33
+ particleNumRange: { min: 5, max: 10 },
34
+ particleVelocityRange: { x: [-1, 1], y: [-3.5, -1.5] },
35
+ particleShrinkRate: 0.95,
36
+ shakeIntensity: 5,
37
+ };
38
+
39
+ export const blast = (options: Partial<BlastOptions> = defaultOptions): Extension => {
40
+ let blaster: Blaster | undefined;
41
+ let last = 0;
42
+
43
+ const getPoint = (view: EditorView, pos: number) => {
44
+ const { left: x = 0, top: y = 0 } = view.coordsForChar(pos) ?? {};
45
+ const element = document.elementFromPoint(x, y);
46
+
47
+ const { offsetLeft: left = 0, offsetTop: top = 0 } = view.scrollDOM.parentElement ?? {};
48
+ const point = { x: x - left, y: y - top };
49
+
50
+ return { element, point };
51
+ };
52
+
53
+ return [
54
+ // Cursor moved.
55
+ EditorView.updateListener.of((update) => {
56
+ // NOTE: The MarkdownEditor may recreated the EditorView.
57
+ if (blaster?.node !== update.view.scrollDOM) {
58
+ if (blaster) {
59
+ blaster.destroy();
60
+ }
61
+
62
+ blaster = new Blaster(
63
+ update.view.scrollDOM,
64
+ defaultsDeep(
65
+ {
66
+ particleGravity: 0.2,
67
+ particleShrinkRate: 0.995,
68
+ color: () => [100 + Math.random() * 100, 0, 0],
69
+ },
70
+ options,
71
+ defaultOptions,
72
+ ),
73
+ );
74
+ blaster.initialize();
75
+ blaster.start(); // TODO(burdon): Stop/clean-up.
76
+ } else {
77
+ blaster.resize();
78
+ }
79
+
80
+ const current = update.state.selection.main.head;
81
+ if (current !== last) {
82
+ last = current;
83
+ // TODO(burdon): Null if end of line.
84
+ const { element, point } = getPoint(update.view, current - 1);
85
+ if (element && point) {
86
+ blaster.spawn({ element, point });
87
+ }
88
+ }
89
+ }),
90
+
91
+ keymap.of([
92
+ {
93
+ any: (_, event) => {
94
+ if (blaster) {
95
+ if (event.key === 'Enter' && event.shiftKey) {
96
+ blaster.shake({ time: 0.8 });
97
+ return true;
98
+ }
99
+ }
100
+
101
+ return false;
102
+ },
103
+ },
104
+ ]),
105
+ ];
106
+ };
107
+
108
+ //
109
+ // Blaster (no CM deps).
110
+ //
111
+
112
+ class Blaster {
113
+ private readonly _effect: Effect;
114
+
115
+ _canvas: HTMLCanvasElement | undefined;
116
+ _ctx: CanvasRenderingContext2D | undefined | null;
117
+
118
+ _running = false;
119
+ _lastTime: number | undefined;
120
+ _shakeTime = 0;
121
+ _shakeTimeMax = 0;
122
+
123
+ _particles: Particle[] = [];
124
+ _particlePointer = 0;
125
+
126
+ _lastPoint = { x: 0, y: 0 };
127
+
128
+ constructor(private readonly _node: HTMLElement, private readonly _options: BlastOptions) {
129
+ this._effect = this._options.effect === 1 ? new Effect1(_options) : new Effect2(_options);
130
+ }
131
+
132
+ get node() {
133
+ return this._node;
134
+ }
135
+
136
+ initialize() {
137
+ // console.log('initialize');
138
+ invariant(!this._canvas && !this._ctx);
139
+
140
+ this._canvas = document.createElement('canvas');
141
+ this._canvas.id = 'code-blast-canvas';
142
+ this._canvas.style.position = 'absolute';
143
+ this._canvas.style.zIndex = '0';
144
+ this._canvas.style.pointerEvents = 'none';
145
+ // this._canvas.style.border = '2px dashed red';
146
+
147
+ this._ctx = this._canvas.getContext('2d');
148
+
149
+ const parent = this._node.parentElement?.parentElement;
150
+ parent?.appendChild(this._canvas);
151
+
152
+ this.resize();
153
+ }
154
+
155
+ destroy() {
156
+ this.stop();
157
+ // console.log('destroy');
158
+ if (this._canvas) {
159
+ this._canvas.remove();
160
+ this._canvas = undefined;
161
+ this._ctx = undefined;
162
+ }
163
+ }
164
+
165
+ resize() {
166
+ if (this._node.parentElement && this._canvas) {
167
+ const { offsetLeft: x, offsetTop: y, offsetWidth: width, offsetHeight: height } = this._node.parentElement;
168
+ this._canvas.style.top = `${y}px`;
169
+ this._canvas.style.left = `${x}px`;
170
+ this._canvas.width = width;
171
+ this._canvas.height = height;
172
+ }
173
+ }
174
+
175
+ start() {
176
+ // console.log('start');
177
+ invariant(this._canvas && this._ctx);
178
+ this._running = true;
179
+ this.loop();
180
+ }
181
+
182
+ stop() {
183
+ // console.log('stop');
184
+ this._running = false;
185
+ this._node.style.transform = 'translate(0px, 0px)';
186
+ }
187
+
188
+ loop() {
189
+ if (!this._running || !this._canvas || !this._ctx) {
190
+ return;
191
+ }
192
+
193
+ this._ctx.clearRect(0, 0, this._canvas.width ?? 0, this._canvas.height ?? 0);
194
+
195
+ // Calculate the delta from the previous frame.
196
+ const now = new Date().getTime();
197
+ this._lastTime ??= now;
198
+ const dt = (now - this._lastTime) / 1000;
199
+ this._lastTime = now;
200
+
201
+ if (this._shakeTime > 0) {
202
+ this._shakeTime -= dt;
203
+ const magnitude = (this._shakeTime / this._shakeTimeMax) * this._options.shakeIntensity;
204
+ const shakeX = random(-magnitude, magnitude);
205
+ const shakeY = random(-magnitude, magnitude);
206
+ this._node!.style.transform = `translate(${shakeX}px,${shakeY}px)`;
207
+ }
208
+
209
+ this.drawParticles();
210
+
211
+ requestAnimationFrame(this.loop.bind(this));
212
+ }
213
+
214
+ shake = throttle<{ time: number }>(({ time }) => {
215
+ this._shakeTime = this._shakeTimeMax || time;
216
+ this._shakeTimeMax = time;
217
+ }, 100);
218
+
219
+ spawn = throttle<{ element: Element; point: { x: number; y: number } }>(({ element, point }) => {
220
+ const color = getRGBComponents(element, this._options.color);
221
+ const numParticles = random(this._options.particleNumRange.min, this._options.particleNumRange.max);
222
+ const dir = this._lastPoint.x === point.x ? 0 : this._lastPoint.x < point.x ? 1 : -1;
223
+ this._lastPoint = point;
224
+ for (let i = numParticles; i--; i > 0) {
225
+ this._particles[this._particlePointer] = this._effect.create(point.x - dir * 16, point.y, color);
226
+ this._particlePointer = (this._particlePointer + 1) % this._options.maxParticles;
227
+ }
228
+ }, 100);
229
+
230
+ drawParticles() {
231
+ for (let i = this._particles.length; i--; i > 0) {
232
+ const particle = this._particles[i];
233
+ if (!particle) {
234
+ continue;
235
+ }
236
+
237
+ if (particle.alpha < 0.01 || particle.size <= 0.5) {
238
+ continue;
239
+ }
240
+
241
+ this._effect.update(this._ctx!, particle);
242
+ }
243
+ }
244
+ }
245
+
246
+ //
247
+ // Effects
248
+ //
249
+
250
+ type Particle = {
251
+ x: number;
252
+ y: number;
253
+ vx: number;
254
+ vy: number;
255
+ size: number;
256
+ color: (string | number)[];
257
+ alpha: number;
258
+ theta?: number;
259
+ drag?: number;
260
+ wander?: number;
261
+ };
262
+
263
+ abstract class Effect {
264
+ constructor(protected readonly _options: BlastOptions) {}
265
+ abstract create(x: number, y: number, color: Particle['color']): Particle;
266
+ abstract update(ctx: CanvasRenderingContext2D, particle: Particle): void;
267
+ }
268
+
269
+ class Effect1 extends Effect {
270
+ create(x: number, y: number, color: Particle['color']) {
271
+ return {
272
+ x,
273
+ y: y + 10,
274
+ vx: random(this._options.particleVelocityRange.x[0], this._options.particleVelocityRange.x[1]),
275
+ vy: random(this._options.particleVelocityRange.y[0], this._options.particleVelocityRange.y[1]),
276
+ size: random(this._options.particleSize.min, this._options.particleSize.max),
277
+ color,
278
+ alpha: 1,
279
+ };
280
+ }
281
+
282
+ update(ctx: CanvasRenderingContext2D, particle: Particle) {
283
+ particle.vy += this._options.particleGravity;
284
+ particle.x += particle.vx;
285
+ particle.y += particle.vy;
286
+ particle.alpha *= this._options.particleAlphaFadeout;
287
+
288
+ ctx.fillStyle = `rgba(${particle.color[0]},${particle.color[1]},${particle.color[2]},${particle.alpha})`;
289
+ ctx.fillRect(Math.round(particle.x - 1), Math.round(particle.y - 1), particle.size, particle.size);
290
+ }
291
+ }
292
+
293
+ /**
294
+ * Based on Soulwire's demo.
295
+ * http://codepen.io/soulwire/pen/foktm
296
+ */
297
+ class Effect2 extends Effect {
298
+ create(x: number, y: number, color: Particle['color']) {
299
+ return {
300
+ x,
301
+ y: y + 10,
302
+ vx: random(this._options.particleVelocityRange.x[0], this._options.particleVelocityRange.x[1]),
303
+ vy: random(this._options.particleVelocityRange.y[0], this._options.particleVelocityRange.y[1]),
304
+ size: random(this._options.particleSize.min, this._options.particleSize.max),
305
+ color,
306
+ alpha: 1,
307
+ theta: (random(0, 360) * Math.PI) / 180,
308
+ drag: 0.92,
309
+ wander: 0.15,
310
+ };
311
+ }
312
+
313
+ update(ctx: CanvasRenderingContext2D, particle: Particle) {
314
+ particle.vy += this._options.particleGravity;
315
+ particle.x += particle.vx;
316
+ particle.y += particle.vy;
317
+ particle.vx *= particle.drag!;
318
+ particle.vy *= particle.drag!;
319
+ particle.theta! += random(-0.5, 0.5);
320
+ particle.vx += Math.sin(particle.theta!) * 0.1;
321
+ particle.vy += Math.cos(particle.theta!) * 0.1;
322
+ particle.size *= this._options.particleShrinkRate;
323
+ particle.alpha *= this._options.particleAlphaFadeout;
324
+
325
+ ctx.fillStyle = `rgba(${particle.color[0]},${particle.color[1]},${particle.color[2]},${particle.alpha})`;
326
+ ctx.beginPath();
327
+ ctx.arc(Math.round(particle.x - 1), Math.round(particle.y - 1), particle.size, 0, 2 * Math.PI);
328
+ ctx.fill();
329
+ }
330
+ }
331
+
332
+ //
333
+ // Utils
334
+ //
335
+
336
+ function throttle<T>(callback: (arg: T) => void, limit: number): (arg: T) => void {
337
+ let wait = false;
338
+ return function (...args: any[]) {
339
+ if (!wait) {
340
+ // @ts-ignore
341
+ callback.apply(this, args);
342
+ wait = true;
343
+ setTimeout(() => {
344
+ wait = false;
345
+ }, limit);
346
+ }
347
+ };
348
+ }
349
+
350
+ const getRGBComponents = (node: Element, color: BlastOptions['color']): Particle['color'] => {
351
+ if (typeof color === 'function') {
352
+ return color();
353
+ }
354
+
355
+ const bgColor = getComputedStyle(node).color;
356
+ if (bgColor) {
357
+ const x = bgColor.match(/(\d+), (\d+), (\d+)/)?.slice(1);
358
+ if (x) {
359
+ return x;
360
+ }
361
+ }
362
+
363
+ return [50, 50, 50];
364
+ };
365
+
366
+ const random = (min: number, max: number) => {
367
+ if (!max) {
368
+ max = min;
369
+ min = 0;
370
+ }
371
+
372
+ return min + ~~(Math.random() * (max - min + 1));
373
+ };
@@ -0,0 +1,99 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { type Range } from '@codemirror/state';
6
+ import {
7
+ ViewPlugin,
8
+ type DecorationSet,
9
+ EditorView,
10
+ type ViewUpdate,
11
+ Decoration,
12
+ type BlockInfo,
13
+ } from '@codemirror/view';
14
+ import get from 'lodash.get';
15
+
16
+ import { mx } from '@dxos/react-ui-theme';
17
+
18
+ import { tokens } from '../../../styles';
19
+
20
+ const CODE_REGEX = /```[\s\S]*?```/gs;
21
+
22
+ // TODO(burdon): Reconcile with theme.
23
+ const styles = EditorView.baseTheme({
24
+ '& .cm-codeblock': {
25
+ fontFamily: get(tokens, 'fontFamily.mono', []).join(','),
26
+ },
27
+ '&light [aria-readonly="true"] .cm-codeblock': {
28
+ background: get(tokens, 'extend.colors.neutral.50'),
29
+ },
30
+ '&dark [aria-readonly="true"] .cm-codeblock': {
31
+ background: get(tokens, 'extend.colors.neutral.850'),
32
+ },
33
+ '& [aria-readonly="true"] .cm-codeblock': {
34
+ display: 'block',
35
+ paddingInline: '4px !important',
36
+ },
37
+ '& [aria-readonly="true"] .cm-codeblock-first': {
38
+ paddingTop: '4px !important',
39
+ borderTopLeftRadius: '4px',
40
+ borderTopRightRadius: '4px',
41
+ },
42
+ '& [aria-readonly="true"] .cm-codeblock-last': {
43
+ paddingBottom: '4px !important',
44
+ borderBottomLeftRadius: '4px',
45
+ borderBottomRightRadius: '4px',
46
+ },
47
+ });
48
+
49
+ // TODO(burdon): Start from EditorView.lineBlockAt(pos).
50
+ const getLineRange = (lines: BlockInfo[], from: number, to: number) => {
51
+ const start = lines.findIndex((line) => line.from >= from);
52
+ const end = lines.findIndex((line) => line.to >= to);
53
+ return [start, end];
54
+ };
55
+
56
+ export const code = () => {
57
+ const getDecorations = (view: EditorView) => {
58
+ const decorations: Range<Decoration>[] = [];
59
+ if (view.state.readOnly) {
60
+ const text = view.state.doc.sliceString(0);
61
+ const matches = text.matchAll(CODE_REGEX);
62
+ const blocks = view.viewportLineBlocks;
63
+ for (const match of matches) {
64
+ const range = getLineRange(blocks, match.index!, match.index! + match[0].length);
65
+ for (let i = range[0]; i <= range[1]; i++) {
66
+ const block = blocks[i];
67
+ decorations.push(
68
+ Decoration.line({
69
+ class: mx('cm-codeblock', i === range[0] && 'cm-codeblock-first', i === range[1] && 'cm-codeblock-last'),
70
+ }).range(block.from),
71
+ );
72
+ }
73
+ }
74
+ }
75
+
76
+ return Decoration.set(decorations);
77
+ };
78
+
79
+ return [
80
+ ViewPlugin.fromClass(
81
+ class {
82
+ decorations: DecorationSet;
83
+ constructor(view: EditorView) {
84
+ this.decorations = getDecorations(view);
85
+ }
86
+
87
+ update(update: ViewUpdate) {
88
+ if (update.docChanged || update.viewportChanged) {
89
+ this.decorations = getDecorations(update.view);
90
+ }
91
+ }
92
+ },
93
+ {
94
+ decorations: (value) => value.decorations,
95
+ },
96
+ ),
97
+ styles,
98
+ ];
99
+ };