vzcode 0.23.0 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/index-8628ebab.js +111 -0
- package/dist/assets/{index-c63b4be7.css → index-cf19de3b.css} +1 -1
- package/dist/assets/{worker-d5f0575a.js → worker-b775ddff.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +19 -9
- package/src/client/App.tsx +111 -30
- package/src/client/CodeEditor/getOrCreateEditor.ts +10 -1
- package/src/client/CodeEditor/index.tsx +9 -5
- package/src/client/CodeEditor/json1PresenceBroadcast.js +2 -0
- package/src/client/CodeEditor/json1PresenceDisplay.js +45 -1
- package/src/client/CodeEditor/widgets.ts +183 -10
- package/src/client/PresenceNotifications/index.tsx +145 -0
- package/src/client/PresenceNotifications/style.scss +17 -0
- package/src/client/PrettierErrorOverlay/index.tsx +9 -0
- package/src/client/PrettierErrorOverlay/style.scss +9 -0
- package/src/client/Settings.tsx +11 -1
- package/src/client/Sidebar/FileListing.tsx +0 -1
- package/src/client/Sidebar/Item.tsx +20 -11
- package/src/client/reducer.ts +119 -0
- package/src/client/style.scss +3 -0
- package/src/client/themes/index.ts +7 -0
- package/src/client/{themes.ts → themes/themes.ts} +5 -5
- package/src/client/{useDynamicTheme.ts → themes/useDynamicTheme.ts} +1 -1
- package/src/client/themes/vizhubTheme/colors.ts +80 -0
- package/src/client/themes/vizhubTheme/index.ts +166 -0
- package/src/client/usePrettier/index.ts +32 -14
- package/src/client/usePrettier/worker.ts +1 -1
- package/dist/assets/index-49f71876.js +0 -110
- package/src/client/useTabsState.ts +0 -58
- package/src/client/vizhubTheme.js +0 -1
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
ViewPlugin,
|
|
5
5
|
Decoration,
|
|
6
6
|
WidgetType,
|
|
7
|
+
keymap,
|
|
7
8
|
} from '@codemirror/view';
|
|
8
9
|
import {
|
|
9
10
|
Annotation,
|
|
@@ -165,22 +166,27 @@ export const widgets = ({
|
|
|
165
166
|
regexp: /rotate\(-?\d*\.?\d*\)/g,
|
|
166
167
|
cursor: 'move',
|
|
167
168
|
onDragStart(text, setText, e) {
|
|
168
|
-
rotationOrigin = { x: e.
|
|
169
|
+
rotationOrigin = { x: e.clientX, y: e.clientY };
|
|
169
170
|
},
|
|
170
171
|
|
|
171
172
|
onDrag(text, setText, e) {
|
|
172
173
|
if (rotationOrigin == null) return;
|
|
174
|
+
const rotationDegree = Math.round(
|
|
175
|
+
(Math.atan2(
|
|
176
|
+
rotationOrigin.y - e.clientY,
|
|
177
|
+
e.clientX - rotationOrigin.x,
|
|
178
|
+
) *
|
|
179
|
+
180) /
|
|
180
|
+
Math.PI,
|
|
181
|
+
);
|
|
173
182
|
//Calculate the angle between the x axis and a line from where the user first clicks to the current location of the mouse.
|
|
174
|
-
setText(
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
e.screenX - rotationOrigin.x,
|
|
179
|
-
) *
|
|
180
|
-
180) /
|
|
181
|
-
Math.PI,
|
|
182
|
-
)})`,
|
|
183
|
+
setText(`rotate(${rotationDegree})`);
|
|
184
|
+
const updateDragEvent = new CustomEvent(
|
|
185
|
+
'updateRotateDrag',
|
|
186
|
+
{ detail: rotationDegree },
|
|
183
187
|
);
|
|
188
|
+
|
|
189
|
+
document.dispatchEvent(updateDragEvent);
|
|
184
190
|
},
|
|
185
191
|
onDragEnd(text, setText) {
|
|
186
192
|
rotationOrigin = null;
|
|
@@ -312,3 +318,170 @@ class ColorWidget extends WidgetType {
|
|
|
312
318
|
return false;
|
|
313
319
|
}
|
|
314
320
|
}
|
|
321
|
+
|
|
322
|
+
export const highlightWidgets = ViewPlugin.fromClass(
|
|
323
|
+
class {
|
|
324
|
+
showHighlight: boolean;
|
|
325
|
+
view: EditorView;
|
|
326
|
+
constructor(view: EditorView) {
|
|
327
|
+
this.showHighlight = false;
|
|
328
|
+
this.view = view;
|
|
329
|
+
}
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
decorations: (v) => {
|
|
333
|
+
if (!v.showHighlight) {
|
|
334
|
+
return Decoration.none;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const interactOpportunities = [];
|
|
338
|
+
const lines = v.view.state.doc.iter();
|
|
339
|
+
let line = lines.next();
|
|
340
|
+
|
|
341
|
+
//Offset is the number of characters before the regex so the highlighting can be placed properly.
|
|
342
|
+
let offset = 0;
|
|
343
|
+
while (!line.done) {
|
|
344
|
+
if (line.value === '\n') {
|
|
345
|
+
offset++;
|
|
346
|
+
line = lines.next();
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
const interactiveOccurances = line.value.matchAll(
|
|
350
|
+
//The below line contains all of the Regexes for all of the interactive widgets.
|
|
351
|
+
/\"\#([0-9]|[A-F]|[a-f]){6}\"|rotate\(-?\d*\.?\d*\)|https?:\/\/[^ "]+|vec2\(-?\b\d+\.?\d*\b\s*(,\s*-?\b\d+\.?\d*\b)?\)|true|false|(?<!\#)-?\b\d+\.?\d*\b|rgb\(.*\)/dg,
|
|
352
|
+
);
|
|
353
|
+
let interactOccurance =
|
|
354
|
+
interactiveOccurances.next();
|
|
355
|
+
while (!interactOccurance.done) {
|
|
356
|
+
const offsetInteract = interactOccurance.value;
|
|
357
|
+
offsetInteract.indices[0][0] += offset;
|
|
358
|
+
offsetInteract.indices[0][1] += offset;
|
|
359
|
+
interactOpportunities.push(offsetInteract);
|
|
360
|
+
interactOccurance = interactiveOccurances.next();
|
|
361
|
+
}
|
|
362
|
+
offset += line.value.length;
|
|
363
|
+
line = lines.next();
|
|
364
|
+
}
|
|
365
|
+
return Decoration.set(
|
|
366
|
+
interactOpportunities.map((opportunity) => {
|
|
367
|
+
return {
|
|
368
|
+
from: opportunity.indices[0][0],
|
|
369
|
+
to: opportunity.indices[0][1],
|
|
370
|
+
value: Decoration.mark({
|
|
371
|
+
class: 'cm-interact ',
|
|
372
|
+
}),
|
|
373
|
+
};
|
|
374
|
+
}),
|
|
375
|
+
);
|
|
376
|
+
},
|
|
377
|
+
eventHandlers: {
|
|
378
|
+
keydown(event, view) {
|
|
379
|
+
if (event.key == 'Alt') {
|
|
380
|
+
this.showHighlight = true;
|
|
381
|
+
}
|
|
382
|
+
},
|
|
383
|
+
keyup(event, view) {
|
|
384
|
+
if (event.key == 'Alt') {
|
|
385
|
+
this.showHighlight = false;
|
|
386
|
+
}
|
|
387
|
+
},
|
|
388
|
+
},
|
|
389
|
+
},
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
export const rotationIndicator = ViewPlugin.fromClass(
|
|
393
|
+
class {
|
|
394
|
+
view: EditorView;
|
|
395
|
+
textPosition?: number;
|
|
396
|
+
rotation: number;
|
|
397
|
+
constructor(view) {
|
|
398
|
+
this.view = view;
|
|
399
|
+
this.textPosition = null;
|
|
400
|
+
this.rotation = 0;
|
|
401
|
+
|
|
402
|
+
document.addEventListener(
|
|
403
|
+
'updateRotateDrag',
|
|
404
|
+
(e: CustomEvent) => {
|
|
405
|
+
this.textPosition = this.view.posAtCoords(
|
|
406
|
+
rotationOrigin,
|
|
407
|
+
false,
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
this.rotation = e.detail;
|
|
411
|
+
},
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
},
|
|
415
|
+
{
|
|
416
|
+
decorations: (v) => {
|
|
417
|
+
if (rotationOrigin === null) {
|
|
418
|
+
return Decoration.none;
|
|
419
|
+
}
|
|
420
|
+
return Decoration.set([
|
|
421
|
+
{
|
|
422
|
+
from: v.textPosition,
|
|
423
|
+
to: v.textPosition,
|
|
424
|
+
value: Decoration.widget({
|
|
425
|
+
side: -1,
|
|
426
|
+
widget: new RotationCircle(v.rotation),
|
|
427
|
+
}),
|
|
428
|
+
},
|
|
429
|
+
]);
|
|
430
|
+
},
|
|
431
|
+
},
|
|
432
|
+
);
|
|
433
|
+
|
|
434
|
+
class RotationCircle extends WidgetType {
|
|
435
|
+
angle: number;
|
|
436
|
+
constructor(angle: number) {
|
|
437
|
+
super();
|
|
438
|
+
this.angle = angle;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
toDOM(view: EditorView): HTMLElement {
|
|
442
|
+
const parent = document.createElement('div');
|
|
443
|
+
|
|
444
|
+
parent.setAttribute('style', 'width:20px;height:20px');
|
|
445
|
+
parent.className = 'color-circle-parent';
|
|
446
|
+
const svg = document.createElementNS(
|
|
447
|
+
'http://www.w3.org/2000/svg',
|
|
448
|
+
'svg',
|
|
449
|
+
);
|
|
450
|
+
const colorCircle = document.createElementNS(
|
|
451
|
+
'http://www.w3.org/2000/svg',
|
|
452
|
+
'circle',
|
|
453
|
+
);
|
|
454
|
+
colorCircle.setAttributeNS(null, 'fill', '#808080');
|
|
455
|
+
colorCircle.setAttributeNS(null, 'r', '5');
|
|
456
|
+
colorCircle.setAttributeNS(null, 'cx', '5');
|
|
457
|
+
colorCircle.setAttributeNS(null, 'cy', '5');
|
|
458
|
+
|
|
459
|
+
const indicatorLine = document.createElementNS(
|
|
460
|
+
'http://www.w3.org/2000/svg',
|
|
461
|
+
'line',
|
|
462
|
+
);
|
|
463
|
+
indicatorLine.setAttributeNS(null, 'x1', '10');
|
|
464
|
+
indicatorLine.setAttributeNS(null, 'x2', '5');
|
|
465
|
+
indicatorLine.setAttributeNS(null, 'y1', '5');
|
|
466
|
+
indicatorLine.setAttributeNS(null, 'y2', '5');
|
|
467
|
+
indicatorLine.setAttributeNS(null, 'stroke', 'black');
|
|
468
|
+
indicatorLine.setAttributeNS(
|
|
469
|
+
null,
|
|
470
|
+
'transform',
|
|
471
|
+
`rotate(${360 - this.angle},5,5)`,
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
svg.setAttributeNS(null, 'viewBox', '0 0 10 10');
|
|
475
|
+
svg.setAttributeNS(null, 'width', '20');
|
|
476
|
+
svg.setAttributeNS(null, 'height', '20');
|
|
477
|
+
|
|
478
|
+
svg.appendChild(colorCircle);
|
|
479
|
+
svg.appendChild(indicatorLine);
|
|
480
|
+
|
|
481
|
+
parent.appendChild(svg);
|
|
482
|
+
return parent;
|
|
483
|
+
}
|
|
484
|
+
ignoreEvent() {
|
|
485
|
+
return false;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { useState, useEffect, useRef } from 'react';
|
|
2
|
+
import './style.scss';
|
|
3
|
+
|
|
4
|
+
type PresenceId = string;
|
|
5
|
+
|
|
6
|
+
// A presence notification is a message that is displayed when a user joins or leaves a session.
|
|
7
|
+
// It has the following properties:
|
|
8
|
+
// - `user` is the username of the user who joined or left the session.
|
|
9
|
+
// - `join` is a boolean value that indicates whether the user joined or left the session.
|
|
10
|
+
// - `true` means user has joined the session.
|
|
11
|
+
// - `false` means user has left the session.
|
|
12
|
+
type PresenceNotification = {
|
|
13
|
+
user: string;
|
|
14
|
+
join: boolean;
|
|
15
|
+
presenceId: PresenceId;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const PresenceNotifications = ({
|
|
19
|
+
docPresence,
|
|
20
|
+
}: {
|
|
21
|
+
docPresence?: any;
|
|
22
|
+
}) => {
|
|
23
|
+
const [presenceNotifications, setPresenceNotifications] =
|
|
24
|
+
useState<Array<PresenceNotification>>([]);
|
|
25
|
+
|
|
26
|
+
// const [presenceIds, setPresenceIds] = useState<
|
|
27
|
+
// Array<any>
|
|
28
|
+
// >([]);
|
|
29
|
+
const alreadyJoinedPresenceIds = useRef<Set<PresenceId>>(
|
|
30
|
+
new Set(),
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
// console.log('useEffect triggered.');
|
|
35
|
+
if (docPresence) {
|
|
36
|
+
// See https://share.github.io/sharedb/presence
|
|
37
|
+
docPresence.on('receive', (presenceId, update) => {
|
|
38
|
+
// Reason for notifications appearing on every cursor movement:
|
|
39
|
+
// code doesn't check if a notification about a user joining was previously sent
|
|
40
|
+
|
|
41
|
+
// Attempted solutions:
|
|
42
|
+
// - create an array called presenceIds that keeps track of the presences that join and leave
|
|
43
|
+
// - if presenceId is in presenceIds: local user was already alerted that a remote user joined
|
|
44
|
+
// - if presenceId is not in presenceIds: local user needs to be notified about a new user
|
|
45
|
+
// - problems:
|
|
46
|
+
// - the array is always empty inside useEffect (array works outside of useEffect)
|
|
47
|
+
// - causes check to fail
|
|
48
|
+
// - check docPresence.remotePresences, which keeps track of all the remote presences on the doc
|
|
49
|
+
// - see https://share.github.io/sharedb/api/presence
|
|
50
|
+
// - if presenceId is in docPresence.remotePresences: local user was already alerted that a remote user joined
|
|
51
|
+
// - if presenceId is not in docPresence.remotePresences: local user needs to be notified about a new user
|
|
52
|
+
// - problems:
|
|
53
|
+
// - useEffects is called when docPresence changes (in some cases, it's when the remote presence changes)
|
|
54
|
+
// - remotePresences already has the remote presence in the array, so check fails
|
|
55
|
+
// - could add another property to presence (true: already notified, false: need to be notified)
|
|
56
|
+
// - doesn't seem like an ideal solution
|
|
57
|
+
|
|
58
|
+
// console.log('docPresence:', docPresence);
|
|
59
|
+
// console.log('docPresence remotePresences:', docPresence.remotePresences);
|
|
60
|
+
// console.log('check for presence:', docPresence.remotePresences[presenceId]);
|
|
61
|
+
|
|
62
|
+
// TODO figure out how to get username from `presenceId`.
|
|
63
|
+
const user = 'someone';
|
|
64
|
+
|
|
65
|
+
// `true` means user has joined the session.
|
|
66
|
+
// `false` means user has left the session.
|
|
67
|
+
const join = update !== null;
|
|
68
|
+
|
|
69
|
+
console.log('join', join);
|
|
70
|
+
|
|
71
|
+
// Figure out if we have ever seen this user before.
|
|
72
|
+
|
|
73
|
+
// if (join && docPresence.remotePresences[presenceId] === undefined){
|
|
74
|
+
if (join) {
|
|
75
|
+
if (
|
|
76
|
+
!alreadyJoinedPresenceIds.current.has(
|
|
77
|
+
presenceId,
|
|
78
|
+
)
|
|
79
|
+
) {
|
|
80
|
+
alreadyJoinedPresenceIds.current.add(
|
|
81
|
+
presenceId,
|
|
82
|
+
);
|
|
83
|
+
setPresenceNotifications((prev) => [
|
|
84
|
+
...prev,
|
|
85
|
+
{ user, join, presenceId },
|
|
86
|
+
]);
|
|
87
|
+
|
|
88
|
+
// Remove it after 5 seconds.
|
|
89
|
+
setTimeout(() => {
|
|
90
|
+
setPresenceNotifications((prev) =>
|
|
91
|
+
prev.filter(
|
|
92
|
+
(notification) =>
|
|
93
|
+
notification.presenceId !==
|
|
94
|
+
presenceId &&
|
|
95
|
+
notification.join !== join,
|
|
96
|
+
),
|
|
97
|
+
);
|
|
98
|
+
}, 5000);
|
|
99
|
+
}
|
|
100
|
+
} else if (!join) {
|
|
101
|
+
setPresenceNotifications((prev) => [
|
|
102
|
+
...prev,
|
|
103
|
+
{ user, join, presenceId },
|
|
104
|
+
]);
|
|
105
|
+
|
|
106
|
+
// Remove it after 5 seconds.
|
|
107
|
+
// TODO clean up this duplication.
|
|
108
|
+
setTimeout(() => {
|
|
109
|
+
setPresenceNotifications((prev) =>
|
|
110
|
+
prev.filter(
|
|
111
|
+
(notification) =>
|
|
112
|
+
notification.presenceId !== presenceId &&
|
|
113
|
+
notification.join !== join,
|
|
114
|
+
),
|
|
115
|
+
);
|
|
116
|
+
}, 5000);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}, [docPresence]);
|
|
121
|
+
|
|
122
|
+
// console.log(
|
|
123
|
+
// 'presenceNotifications',
|
|
124
|
+
// presenceNotifications,
|
|
125
|
+
// );
|
|
126
|
+
// console.log(
|
|
127
|
+
// 'presenceIds',
|
|
128
|
+
// presenceIds,
|
|
129
|
+
// );
|
|
130
|
+
|
|
131
|
+
return presenceNotifications.length > 0 ? (
|
|
132
|
+
<div className="vz-notification">
|
|
133
|
+
{presenceNotifications.map((notification, i) => {
|
|
134
|
+
const { user, join } = notification;
|
|
135
|
+
return (
|
|
136
|
+
<div key={i} className="vz-notification-item">
|
|
137
|
+
{join
|
|
138
|
+
? `${user} has joined the session.`
|
|
139
|
+
: `${user} has left the session.`}
|
|
140
|
+
</div>
|
|
141
|
+
);
|
|
142
|
+
})}
|
|
143
|
+
</div>
|
|
144
|
+
) : null;
|
|
145
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
.vz-notification {
|
|
2
|
+
position: fixed;
|
|
3
|
+
display: flex;
|
|
4
|
+
flex-direction: column;
|
|
5
|
+
gap: 10px;
|
|
6
|
+
bottom: 20px;
|
|
7
|
+
right: 20px;
|
|
8
|
+
|
|
9
|
+
color: rgb(154, 160, 172);
|
|
10
|
+
padding: 15px;
|
|
11
|
+
|
|
12
|
+
.vz-notification-item {
|
|
13
|
+
background-color: rgb(54, 65, 89);
|
|
14
|
+
padding: 5px;
|
|
15
|
+
border-radius: 5px;
|
|
16
|
+
}
|
|
17
|
+
}
|
package/src/client/Settings.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback } from 'react';
|
|
1
|
+
import { useCallback, useRef } from 'react';
|
|
2
2
|
import { Button, Modal, Form } from './bootstrap';
|
|
3
3
|
import { ThemeLabel, themes } from './themes';
|
|
4
4
|
|
|
@@ -9,6 +9,8 @@ import { ThemeLabel, themes } from './themes';
|
|
|
9
9
|
// { value: 30, label: '30 seconds' },
|
|
10
10
|
// ];
|
|
11
11
|
|
|
12
|
+
export let username = 'Anonymous';
|
|
13
|
+
|
|
12
14
|
export const Settings = ({
|
|
13
15
|
show,
|
|
14
16
|
onClose,
|
|
@@ -41,6 +43,12 @@ export const Settings = ({
|
|
|
41
43
|
// [],
|
|
42
44
|
// );
|
|
43
45
|
|
|
46
|
+
const usernameRef = useRef<HTMLInputElement>(null);
|
|
47
|
+
const handleUsernameChange = () => {
|
|
48
|
+
const usernameValue = usernameRef.current?.value;
|
|
49
|
+
username = usernameValue;
|
|
50
|
+
};
|
|
51
|
+
|
|
44
52
|
return show ? (
|
|
45
53
|
<Modal
|
|
46
54
|
className="vz-settings"
|
|
@@ -56,6 +64,8 @@ export const Settings = ({
|
|
|
56
64
|
<Form.Label>Username</Form.Label>
|
|
57
65
|
<Form.Control
|
|
58
66
|
type="text"
|
|
67
|
+
ref={usernameRef}
|
|
68
|
+
onChange={handleUsernameChange}
|
|
59
69
|
placeholder="Enter username"
|
|
60
70
|
/>
|
|
61
71
|
<Form.Text className="text-muted">
|
|
@@ -19,9 +19,9 @@ export const Item = ({
|
|
|
19
19
|
const [renameValue, setRenameValue] = useState('');
|
|
20
20
|
|
|
21
21
|
const onKeyDown = useCallback((event) => {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
if (event.key === 'Enter'
|
|
22
|
+
if (event.key === 'Escape') {
|
|
23
|
+
setIsRenaming(false);
|
|
24
|
+
} else if (event.key === 'Enter') {
|
|
25
25
|
event.target.blur();
|
|
26
26
|
}
|
|
27
27
|
}, []);
|
|
@@ -42,9 +42,13 @@ export const Item = ({
|
|
|
42
42
|
const [showModal, setShowModal] = useState(false);
|
|
43
43
|
|
|
44
44
|
// Function to open the delete file confirmation modal
|
|
45
|
-
const handleModalOpen = useCallback(
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
const handleModalOpen = useCallback(
|
|
46
|
+
(event: React.MouseEvent) => {
|
|
47
|
+
event.stopPropagation();
|
|
48
|
+
setShowModal(true);
|
|
49
|
+
},
|
|
50
|
+
[],
|
|
51
|
+
);
|
|
48
52
|
|
|
49
53
|
// Function to close the modal
|
|
50
54
|
const handleModalClose = useCallback(() => {
|
|
@@ -52,10 +56,14 @@ export const Item = ({
|
|
|
52
56
|
}, []);
|
|
53
57
|
|
|
54
58
|
// Function to handle confirmation on modal
|
|
55
|
-
const handleConfirmModal = useCallback(
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
+
const handleConfirmModal = useCallback(
|
|
60
|
+
(event: React.MouseEvent) => {
|
|
61
|
+
event.stopPropagation();
|
|
62
|
+
setShowModal(false);
|
|
63
|
+
handleDeleteClick();
|
|
64
|
+
},
|
|
65
|
+
[handleDeleteClick],
|
|
66
|
+
);
|
|
59
67
|
|
|
60
68
|
const handleMouseEnter = useCallback(() => {
|
|
61
69
|
setIsHovered(true);
|
|
@@ -98,7 +106,8 @@ export const Item = ({
|
|
|
98
106
|
<i
|
|
99
107
|
className="bx bxs-edit utilities"
|
|
100
108
|
style={{ color: '#abdafb' }}
|
|
101
|
-
onClick={() => {
|
|
109
|
+
onClick={(event: React.MouseEvent) => {
|
|
110
|
+
event.stopPropagation();
|
|
102
111
|
setIsRenaming(!isRenaming);
|
|
103
112
|
setRenameValue(isDir ? name : children);
|
|
104
113
|
}}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { FileId } from '../types';
|
|
2
|
+
import { act } from 'react-dom/test-utils';
|
|
3
|
+
|
|
4
|
+
export const reducer = (state, action) => {
|
|
5
|
+
switch (action.type) {
|
|
6
|
+
// TODO phase this out
|
|
7
|
+
|
|
8
|
+
// The ordered list of tabs.
|
|
9
|
+
// TODO make this a URL param
|
|
10
|
+
case 'set_tab_list': {
|
|
11
|
+
return { ...state, tabList: action.tabList };
|
|
12
|
+
}
|
|
13
|
+
// TODO make this a URL param
|
|
14
|
+
case 'set_active_fileId': {
|
|
15
|
+
return {
|
|
16
|
+
...state,
|
|
17
|
+
activeFileId: action.activeFileId,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
case 'open_tab': {
|
|
21
|
+
if (!state.tabList.includes(action.fileId)) {
|
|
22
|
+
return {
|
|
23
|
+
...state,
|
|
24
|
+
activeFileId: action.fileId,
|
|
25
|
+
tabList: [...state.tabList, action.fileId],
|
|
26
|
+
};
|
|
27
|
+
} else {
|
|
28
|
+
return {
|
|
29
|
+
...state,
|
|
30
|
+
activeFileId: action.fileId,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
case 'close_tab': {
|
|
35
|
+
const i = state.tabList.findIndex(
|
|
36
|
+
(fileId) => fileId === action.fileIdToRemove,
|
|
37
|
+
);
|
|
38
|
+
if (i != -1) {
|
|
39
|
+
const newTabList = [
|
|
40
|
+
...state.tabList.slice(0, i),
|
|
41
|
+
...state.tabList.slice(i + 1),
|
|
42
|
+
];
|
|
43
|
+
if (state.activeFileId === action.fileIdToRemove) {
|
|
44
|
+
// set the new active file to the next tab over,
|
|
45
|
+
if (newTabList.length > 0) {
|
|
46
|
+
return {
|
|
47
|
+
...state,
|
|
48
|
+
activeFileId:
|
|
49
|
+
i === 0 ? newTabList[i] : newTabList[i - 1],
|
|
50
|
+
tabList: newTabList,
|
|
51
|
+
};
|
|
52
|
+
} else {
|
|
53
|
+
// or clear out the active file
|
|
54
|
+
// if we've closed the last tab.
|
|
55
|
+
return {
|
|
56
|
+
...state,
|
|
57
|
+
activeFileId: null,
|
|
58
|
+
tabList: newTabList,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
...state,
|
|
64
|
+
tabList: newTabList,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
...state,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
case 'multi_close_tab': {
|
|
72
|
+
let newTabList = [...state.tabList]; // Create a copy of the tabList array.
|
|
73
|
+
let newActiveFileId: FileId = state.activeFileId;
|
|
74
|
+
action.idsToDelete.forEach((id) => {
|
|
75
|
+
const i = newTabList.findIndex(
|
|
76
|
+
(fileId) => fileId === id,
|
|
77
|
+
);
|
|
78
|
+
if (i !== -1) {
|
|
79
|
+
// Remove the tab from the tab list.
|
|
80
|
+
newTabList = [
|
|
81
|
+
...newTabList.slice(0, i),
|
|
82
|
+
...newTabList.slice(i + 1),
|
|
83
|
+
];
|
|
84
|
+
// If we are closing the active file,
|
|
85
|
+
if (newActiveFileId === id) {
|
|
86
|
+
// set the new active file to the next tab over,
|
|
87
|
+
if (newTabList.length > 0) {
|
|
88
|
+
newActiveFileId =
|
|
89
|
+
i === 0 ? newTabList[i] : newTabList[i - 1];
|
|
90
|
+
} else {
|
|
91
|
+
// or clear out the active file if we've closed the last tab.
|
|
92
|
+
newActiveFileId = null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
return {
|
|
98
|
+
...state,
|
|
99
|
+
tabList: newTabList,
|
|
100
|
+
activeFileId: newActiveFileId,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
// The current theme.
|
|
104
|
+
// TODO persist this in local storage
|
|
105
|
+
case 'set_Theme': {
|
|
106
|
+
return {
|
|
107
|
+
...state,
|
|
108
|
+
theme: action.themeLabel,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
case 'set_Is_Settings_Open': {
|
|
112
|
+
return {
|
|
113
|
+
...state,
|
|
114
|
+
isSettingsOpen: action.value,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
throw Error('Unknown action: ' + action.type);
|
|
119
|
+
};
|
package/src/client/style.scss
CHANGED
|
@@ -7,7 +7,7 @@ import { githubDark } from '@uiw/codemirror-theme-github';
|
|
|
7
7
|
import { material } from '@uiw/codemirror-theme-material';
|
|
8
8
|
import { nord } from '@uiw/codemirror-theme-nord';
|
|
9
9
|
import { xcodeLight } from '@uiw/codemirror-theme-xcode';
|
|
10
|
-
|
|
10
|
+
import { vizhubTheme } from './vizhubTheme';
|
|
11
11
|
|
|
12
12
|
// CodeMirror themes
|
|
13
13
|
export type ThemeLabel =
|
|
@@ -19,8 +19,8 @@ export type ThemeLabel =
|
|
|
19
19
|
| 'oneDark'
|
|
20
20
|
| 'okaidia'
|
|
21
21
|
| 'github'
|
|
22
|
-
| 'xcode'
|
|
23
|
-
|
|
22
|
+
| 'xcode'
|
|
23
|
+
| 'vizhub';
|
|
24
24
|
|
|
25
25
|
export type ThemeOption = {
|
|
26
26
|
value: any;
|
|
@@ -37,7 +37,7 @@ export const themes: Array<ThemeOption> = [
|
|
|
37
37
|
{ value: okaidia, label: 'okaidia' },
|
|
38
38
|
{ value: githubDark, label: 'github' },
|
|
39
39
|
{ value: xcodeLight, label: 'xcode' },
|
|
40
|
-
|
|
40
|
+
{ value: vizhubTheme, label: 'vizhub' },
|
|
41
41
|
];
|
|
42
42
|
|
|
43
43
|
// Map theme labels to theme values
|
|
@@ -53,4 +53,4 @@ export const themeOptionsByLabel: Record<
|
|
|
53
53
|
);
|
|
54
54
|
|
|
55
55
|
// The default theme
|
|
56
|
-
export const defaultTheme: ThemeLabel = '
|
|
56
|
+
export const defaultTheme: ThemeLabel = 'vizhub';
|