@filipc77/cowrite 0.6.7 → 0.6.8
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/bin/cowrite.js +61 -3
- package/dist/bin/cowrite.js.map +1 -1
- package/package.json +1 -1
- package/ui/index.html +22 -1
- package/ui/modules/app.js +90 -0
- package/ui/modules/block-menu.js +251 -0
- package/ui/modules/comment-anchoring.js +112 -0
- package/ui/modules/comment-highlight.js +243 -0
- package/ui/modules/comment-sidebar.js +192 -0
- package/ui/modules/editor.js +117 -0
- package/ui/modules/file-picker.js +74 -0
- package/ui/modules/markdown-sync.js +229 -0
- package/ui/modules/preferences.js +120 -0
- package/ui/modules/state.js +73 -0
- package/ui/modules/toolbar.js +408 -0
- package/ui/modules/undo-manager.js +78 -0
- package/ui/modules/utils.js +34 -0
- package/ui/modules/ws-client.js +63 -0
- package/ui/styles.css +138 -0
- package/ui/client.js +0 -1743
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shorthand DOM selector.
|
|
5
|
+
* @param {string} sel
|
|
6
|
+
* @returns {HTMLElement}
|
|
7
|
+
*/
|
|
8
|
+
export const $ = (sel) => document.querySelector(sel);
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Escape HTML entities to prevent XSS.
|
|
12
|
+
* @param {string} text
|
|
13
|
+
* @returns {string}
|
|
14
|
+
*/
|
|
15
|
+
export function escapeHtml(text) {
|
|
16
|
+
const div = document.createElement("div");
|
|
17
|
+
div.textContent = text;
|
|
18
|
+
return div.innerHTML;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Human-readable relative time.
|
|
23
|
+
* @param {string} iso
|
|
24
|
+
* @returns {string}
|
|
25
|
+
*/
|
|
26
|
+
export function timeAgo(iso) {
|
|
27
|
+
const diff = Date.now() - new Date(iso).getTime();
|
|
28
|
+
const mins = Math.floor(diff / 60000);
|
|
29
|
+
if (mins < 1) return "just now";
|
|
30
|
+
if (mins < 60) return `${mins}m ago`;
|
|
31
|
+
const hours = Math.floor(mins / 60);
|
|
32
|
+
if (hours < 24) return `${hours}h ago`;
|
|
33
|
+
return `${Math.floor(hours / 24)}d ago`;
|
|
34
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/** @typedef {import('../../src/types.js').WSServerMessage} WSServerMessage */
|
|
4
|
+
|
|
5
|
+
import { $ } from './utils.js';
|
|
6
|
+
import { state } from './state.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Send a message over the WebSocket.
|
|
10
|
+
* @param {object} msg
|
|
11
|
+
*/
|
|
12
|
+
export function send(msg) {
|
|
13
|
+
if (state.ws && state.ws.readyState === WebSocket.OPEN) {
|
|
14
|
+
state.ws.send(JSON.stringify(msg));
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Initialize WebSocket connection with auto-reconnect.
|
|
20
|
+
* @param {object} handlers
|
|
21
|
+
* @param {(msg: any) => void} handlers.onFileUpdate
|
|
22
|
+
* @param {(msg: any) => void} handlers.onCommentsUpdate
|
|
23
|
+
* @param {(msg: any) => void} handlers.onError
|
|
24
|
+
* @param {() => void} handlers.onOpen
|
|
25
|
+
*/
|
|
26
|
+
export function initWebSocket(handlers) {
|
|
27
|
+
const statusEl = $("#status");
|
|
28
|
+
|
|
29
|
+
function connect() {
|
|
30
|
+
const protocol = location.protocol === "https:" ? "wss:" : "ws:";
|
|
31
|
+
state.ws = new WebSocket(`${protocol}//${location.host}`);
|
|
32
|
+
|
|
33
|
+
state.ws.onopen = () => {
|
|
34
|
+
statusEl.innerHTML = '<span class="status-dot"></span>Connected';
|
|
35
|
+
statusEl.className = "status connected";
|
|
36
|
+
handlers.onOpen();
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
state.ws.onclose = () => {
|
|
40
|
+
statusEl.innerHTML = '<span class="status-dot"></span>Disconnected';
|
|
41
|
+
statusEl.className = "status";
|
|
42
|
+
setTimeout(connect, 2000);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
state.ws.onmessage = (event) => {
|
|
46
|
+
/** @type {WSServerMessage} */
|
|
47
|
+
const msg = JSON.parse(event.data);
|
|
48
|
+
switch (msg.type) {
|
|
49
|
+
case "file_update":
|
|
50
|
+
handlers.onFileUpdate(msg);
|
|
51
|
+
break;
|
|
52
|
+
case "comments_update":
|
|
53
|
+
handlers.onCommentsUpdate(msg);
|
|
54
|
+
break;
|
|
55
|
+
case "error":
|
|
56
|
+
handlers.onError(msg);
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
connect();
|
|
63
|
+
}
|
package/ui/styles.css
CHANGED
|
@@ -793,6 +793,24 @@ body.sidebar-resizing {
|
|
|
793
793
|
background: var(--green-bg);
|
|
794
794
|
}
|
|
795
795
|
|
|
796
|
+
/* ---- Orphaned comments ---- */
|
|
797
|
+
.comment-card.orphaned {
|
|
798
|
+
opacity: 0.7;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
.orphaned-badge {
|
|
802
|
+
display: inline-block;
|
|
803
|
+
font-size: 9px;
|
|
804
|
+
font-weight: 600;
|
|
805
|
+
padding: 2px 8px;
|
|
806
|
+
border-radius: 100px;
|
|
807
|
+
background: rgba(212, 97, 110, 0.1);
|
|
808
|
+
color: var(--red);
|
|
809
|
+
text-transform: uppercase;
|
|
810
|
+
letter-spacing: 0.4px;
|
|
811
|
+
margin-left: 6px;
|
|
812
|
+
}
|
|
813
|
+
|
|
796
814
|
/* ---- Replies ---- */
|
|
797
815
|
.comment-replies {
|
|
798
816
|
margin-top: 12px;
|
|
@@ -1510,4 +1528,124 @@ body.sidebar-resizing {
|
|
|
1510
1528
|
background: var(--surface-hover);
|
|
1511
1529
|
}
|
|
1512
1530
|
|
|
1531
|
+
/* ---- TipTap / ProseMirror overrides ---- */
|
|
1532
|
+
.ProseMirror {
|
|
1533
|
+
outline: none;
|
|
1534
|
+
min-height: 200px;
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
.ProseMirror p.is-editor-empty:first-child::before {
|
|
1538
|
+
content: attr(data-placeholder);
|
|
1539
|
+
float: left;
|
|
1540
|
+
color: var(--text-faint);
|
|
1541
|
+
pointer-events: none;
|
|
1542
|
+
height: 0;
|
|
1543
|
+
font-style: italic;
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
.ProseMirror table {
|
|
1547
|
+
border-collapse: collapse;
|
|
1548
|
+
width: 100%;
|
|
1549
|
+
margin: 16px 0;
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
.ProseMirror th,
|
|
1553
|
+
.ProseMirror td {
|
|
1554
|
+
border: 1px solid var(--border);
|
|
1555
|
+
padding: 8px 12px;
|
|
1556
|
+
text-align: left;
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
.ProseMirror th {
|
|
1560
|
+
background: var(--surface-hover);
|
|
1561
|
+
font-weight: 600;
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
.ProseMirror ul[data-type="taskList"] {
|
|
1565
|
+
list-style: none;
|
|
1566
|
+
padding-left: 0;
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
.ProseMirror ul[data-type="taskList"] li {
|
|
1570
|
+
display: flex;
|
|
1571
|
+
align-items: flex-start;
|
|
1572
|
+
gap: 8px;
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
.ProseMirror ul[data-type="taskList"] li input[type="checkbox"] {
|
|
1576
|
+
margin-top: 4px;
|
|
1577
|
+
accent-color: var(--accent);
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
.ProseMirror .hljs {
|
|
1581
|
+
background: var(--surface) !important;
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
/* Ensure the TipTap editor inherits markdown-body styles */
|
|
1585
|
+
#fileContent .ProseMirror {
|
|
1586
|
+
font-family: var(--font-sans);
|
|
1587
|
+
color: var(--text);
|
|
1588
|
+
line-height: 1.6;
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
#fileContent .ProseMirror h1 { font-size: 2em; font-weight: 600; margin: 24px 0 16px; border-bottom: 1px solid var(--border); padding-bottom: 8px; }
|
|
1592
|
+
#fileContent .ProseMirror h2 { font-size: 1.5em; font-weight: 600; margin: 20px 0 12px; border-bottom: 1px solid var(--border); padding-bottom: 6px; }
|
|
1593
|
+
#fileContent .ProseMirror h3 { font-size: 1.25em; font-weight: 600; margin: 16px 0 8px; }
|
|
1594
|
+
#fileContent .ProseMirror h4 { font-size: 1em; font-weight: 600; margin: 12px 0 6px; }
|
|
1595
|
+
|
|
1596
|
+
#fileContent .ProseMirror p { margin: 0 0 16px; }
|
|
1597
|
+
#fileContent .ProseMirror ul,
|
|
1598
|
+
#fileContent .ProseMirror ol { padding-left: 2em; margin: 0 0 16px; }
|
|
1599
|
+
#fileContent .ProseMirror li { margin: 4px 0; }
|
|
1600
|
+
#fileContent .ProseMirror li p { margin: 0; }
|
|
1601
|
+
|
|
1602
|
+
#fileContent .ProseMirror blockquote {
|
|
1603
|
+
border-left: 3px solid var(--border);
|
|
1604
|
+
padding-left: 16px;
|
|
1605
|
+
color: var(--text-dim);
|
|
1606
|
+
margin: 0 0 16px;
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
#fileContent .ProseMirror pre {
|
|
1610
|
+
background: var(--surface);
|
|
1611
|
+
border: 1px solid var(--border);
|
|
1612
|
+
border-radius: 6px;
|
|
1613
|
+
padding: 16px;
|
|
1614
|
+
overflow-x: auto;
|
|
1615
|
+
margin: 0 0 16px;
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
#fileContent .ProseMirror code {
|
|
1619
|
+
font-family: var(--font-mono);
|
|
1620
|
+
font-size: 0.9em;
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
#fileContent .ProseMirror :not(pre) > code {
|
|
1624
|
+
background: var(--surface-hover);
|
|
1625
|
+
padding: 2px 6px;
|
|
1626
|
+
border-radius: 4px;
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
#fileContent .ProseMirror hr {
|
|
1630
|
+
border: none;
|
|
1631
|
+
border-top: 1px solid var(--border);
|
|
1632
|
+
margin: 24px 0;
|
|
1633
|
+
}
|
|
1513
1634
|
|
|
1635
|
+
#fileContent .ProseMirror a {
|
|
1636
|
+
color: var(--accent);
|
|
1637
|
+
text-decoration: none;
|
|
1638
|
+
}
|
|
1639
|
+
#fileContent .ProseMirror a:hover {
|
|
1640
|
+
text-decoration: underline;
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
#fileContent .ProseMirror img {
|
|
1644
|
+
max-width: 100%;
|
|
1645
|
+
height: auto;
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
/* Large font size mode for TipTap */
|
|
1649
|
+
body.font-large #fileContent .ProseMirror {
|
|
1650
|
+
font-size: 19px !important;
|
|
1651
|
+
}
|