@defensestation/y-comments-blocknote 1.0.0-dev.84f89ec
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/components/CreateCommentButton.d.ts +9 -0
- package/dist/components/CreateCommentButton.js +46 -0
- package/dist/components/FloatingComposer.d.ts +8 -0
- package/dist/components/FloatingComposer.js +147 -0
- package/dist/components/FloatingThread.d.ts +12 -0
- package/dist/components/FloatingThread.js +95 -0
- package/dist/components/ThreadBody.d.ts +8 -0
- package/dist/components/ThreadBody.js +190 -0
- package/dist/components/ThreadsSidebar.d.ts +13 -0
- package/dist/components/ThreadsSidebar.js +69 -0
- package/dist/components/floating-utils.d.ts +12 -0
- package/dist/components/floating-utils.js +12 -0
- package/dist/components/icons.d.ts +8 -0
- package/dist/components/icons.js +20 -0
- package/dist/context.d.ts +47 -0
- package/dist/context.js +173 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +9 -0
- package/dist/style.css +371 -0
- package/dist/time.d.ts +2 -0
- package/dist/time.js +18 -0
- package/package.json +63 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
const base = (props) => ({
|
|
3
|
+
width: 16,
|
|
4
|
+
height: 16,
|
|
5
|
+
viewBox: "0 0 24 24",
|
|
6
|
+
fill: "none",
|
|
7
|
+
stroke: "currentColor",
|
|
8
|
+
strokeWidth: 2,
|
|
9
|
+
strokeLinecap: "round",
|
|
10
|
+
strokeLinejoin: "round",
|
|
11
|
+
"aria-hidden": true,
|
|
12
|
+
...props,
|
|
13
|
+
});
|
|
14
|
+
export const CommentIcon = (props) => (_jsx("svg", { ...base(props), children: _jsx("path", { d: "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" }) }));
|
|
15
|
+
export const CheckIcon = (props) => (_jsxs("svg", { ...base(props), children: [_jsx("circle", { cx: "12", cy: "12", r: "9" }), _jsx("path", { d: "m8.5 12.5 2.5 2.5 4.5-5" })] }));
|
|
16
|
+
export const ReopenIcon = (props) => (_jsxs("svg", { ...base(props), children: [_jsx("path", { d: "M3 12a9 9 0 1 0 3-6.7" }), _jsx("path", { d: "M3 4v5h5" })] }));
|
|
17
|
+
export const TrashIcon = (props) => (_jsxs("svg", { ...base(props), children: [_jsx("path", { d: "M3 6h18" }), _jsx("path", { d: "M8 6V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2" }), _jsx("path", { d: "M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" })] }));
|
|
18
|
+
export const EditIcon = (props) => (_jsx("svg", { ...base(props), children: _jsx("path", { d: "M17 3a2.8 2.8 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5z" }) }));
|
|
19
|
+
export const EmojiIcon = (props) => (_jsxs("svg", { ...base(props), children: [_jsx("circle", { cx: "12", cy: "12", r: "9" }), _jsx("path", { d: "M8 14s1.5 2 4 2 4-2 4-2" }), _jsx("line", { x1: "9", y1: "9", x2: "9.01", y2: "9" }), _jsx("line", { x1: "15", y1: "9", x2: "15.01", y2: "9" })] }));
|
|
20
|
+
export const CloseIcon = (props) => (_jsxs("svg", { ...base(props), children: [_jsx("path", { d: "M18 6 6 18" }), _jsx("path", { d: "m6 6 12 12" })] }));
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { Resolution, ResolveUsersFn, Thread, ThreadStore, User } from "@defensestation/y-comments-core";
|
|
2
|
+
import type { BlockNoteEditor } from "@blocknote/core";
|
|
3
|
+
import { type ReactNode } from "react";
|
|
4
|
+
export type AnyBlockNoteEditor = BlockNoteEditor<any, any, any>;
|
|
5
|
+
export interface CommentsContextValue {
|
|
6
|
+
editor: AnyBlockNoteEditor;
|
|
7
|
+
store: ThreadStore;
|
|
8
|
+
docId: string;
|
|
9
|
+
threads: Thread[];
|
|
10
|
+
resolutions: Map<string, Resolution>;
|
|
11
|
+
selectedThreadId: string | null;
|
|
12
|
+
selectThread: (threadId: string | null) => void;
|
|
13
|
+
/** Selection captured when the composer was opened; null = closed. */
|
|
14
|
+
composer: {
|
|
15
|
+
from: number;
|
|
16
|
+
to: number;
|
|
17
|
+
} | null;
|
|
18
|
+
openComposer: () => void;
|
|
19
|
+
closeComposer: () => void;
|
|
20
|
+
users: Map<string, User>;
|
|
21
|
+
/** Ask the provider to resolve (and cache) these user ids. */
|
|
22
|
+
requestUsers: (userIds: string[]) => void;
|
|
23
|
+
}
|
|
24
|
+
export declare function useComments(): CommentsContextValue;
|
|
25
|
+
export interface CommentsProviderProps {
|
|
26
|
+
editor: AnyBlockNoteEditor;
|
|
27
|
+
store: ThreadStore;
|
|
28
|
+
/** Document id used against the thread store. */
|
|
29
|
+
docId: string;
|
|
30
|
+
/** Lookup for avatars/names, same contract as BlockNote's resolveUsers. */
|
|
31
|
+
resolveUsers: ResolveUsersFn;
|
|
32
|
+
/**
|
|
33
|
+
* Persist fresh anchors after successful re-anchoring. Off by default —
|
|
34
|
+
* it writes to the store on read. See the README before enabling.
|
|
35
|
+
*/
|
|
36
|
+
selfHeal?: boolean;
|
|
37
|
+
children: ReactNode;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Owns the comment machinery for one editor + document:
|
|
41
|
+
* installs the decorations plugin into BlockNote's ProseMirror view,
|
|
42
|
+
* subscribes to the thread store, and exposes everything to the UI
|
|
43
|
+
* components via context.
|
|
44
|
+
*
|
|
45
|
+
* Nothing under this provider ever writes to the Y.Doc.
|
|
46
|
+
*/
|
|
47
|
+
export declare function CommentsProvider(props: CommentsProviderProps): import("react").JSX.Element;
|
package/dist/context.js
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { selectThread as pmSelectThread, setPendingRange as pmSetPendingRange, setThreads as pmSetThreads, yCommentsPlugin, yCommentsPluginKey, } from "@defensestation/y-comments-prosemirror";
|
|
3
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, } from "react";
|
|
4
|
+
const CommentsContext = createContext(null);
|
|
5
|
+
export function useComments() {
|
|
6
|
+
const ctx = useContext(CommentsContext);
|
|
7
|
+
if (!ctx) {
|
|
8
|
+
throw new Error("y-comments: this component must be rendered inside <CommentsProvider>.");
|
|
9
|
+
}
|
|
10
|
+
return ctx;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Owns the comment machinery for one editor + document:
|
|
14
|
+
* installs the decorations plugin into BlockNote's ProseMirror view,
|
|
15
|
+
* subscribes to the thread store, and exposes everything to the UI
|
|
16
|
+
* components via context.
|
|
17
|
+
*
|
|
18
|
+
* Nothing under this provider ever writes to the Y.Doc.
|
|
19
|
+
*/
|
|
20
|
+
export function CommentsProvider(props) {
|
|
21
|
+
const { editor, store, docId, resolveUsers, selfHeal } = props;
|
|
22
|
+
const [threads, setThreadsState] = useState([]);
|
|
23
|
+
const [resolutions, setResolutions] = useState(new Map());
|
|
24
|
+
const [selectedThreadId, setSelectedThreadId] = useState(null);
|
|
25
|
+
const [composer, setComposer] = useState(null);
|
|
26
|
+
const [users, setUsers] = useState(new Map());
|
|
27
|
+
const threadsRef = useRef([]);
|
|
28
|
+
const pendingUserIds = useRef(new Set());
|
|
29
|
+
const withView = useCallback((fn) => {
|
|
30
|
+
try {
|
|
31
|
+
fn(editor.prosemirrorView);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// editor not mounted yet — callers re-run once it is
|
|
35
|
+
}
|
|
36
|
+
}, [editor]);
|
|
37
|
+
// Install the ProseMirror plugin (unless the host already registered it
|
|
38
|
+
// as a BlockNote extension) and subscribe to the thread store.
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
let disposed = false;
|
|
41
|
+
let installedByUs = false;
|
|
42
|
+
let unsubscribe;
|
|
43
|
+
let retryTimer;
|
|
44
|
+
const start = () => {
|
|
45
|
+
if (disposed) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
let view;
|
|
49
|
+
try {
|
|
50
|
+
view = editor.prosemirrorView;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
retryTimer = setTimeout(start, 50); // BlockNoteView not mounted yet
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (yCommentsPluginKey.getState(view.state) === undefined) {
|
|
57
|
+
editor._tiptapEditor.registerPlugin(yCommentsPlugin({
|
|
58
|
+
selfHeal,
|
|
59
|
+
store,
|
|
60
|
+
onThreadSelect: (threadId) => setSelectedThreadId(threadId),
|
|
61
|
+
onResolutionsChange: (r) => setResolutions(new Map(r)),
|
|
62
|
+
}));
|
|
63
|
+
installedByUs = true;
|
|
64
|
+
}
|
|
65
|
+
pmSetThreads(view, threadsRef.current);
|
|
66
|
+
// Polling stores re-emit unchanged lists; dropping those early keeps
|
|
67
|
+
// both React and the plugin from re-rendering every poll tick.
|
|
68
|
+
const serializeThreads = (threads) => JSON.stringify(threads, (_key, value) => value instanceof Uint8Array ? Array.from(value) : value);
|
|
69
|
+
let lastSerialized = serializeThreads(threadsRef.current);
|
|
70
|
+
unsubscribe = store.subscribe(docId, (nextThreads) => {
|
|
71
|
+
if (disposed) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const serialized = serializeThreads(nextThreads);
|
|
75
|
+
if (serialized === lastSerialized) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
lastSerialized = serialized;
|
|
79
|
+
threadsRef.current = nextThreads;
|
|
80
|
+
setThreadsState(nextThreads);
|
|
81
|
+
withView((v) => pmSetThreads(v, nextThreads));
|
|
82
|
+
});
|
|
83
|
+
};
|
|
84
|
+
start();
|
|
85
|
+
return () => {
|
|
86
|
+
disposed = true;
|
|
87
|
+
if (retryTimer !== undefined) {
|
|
88
|
+
clearTimeout(retryTimer);
|
|
89
|
+
}
|
|
90
|
+
unsubscribe?.();
|
|
91
|
+
if (installedByUs) {
|
|
92
|
+
try {
|
|
93
|
+
editor._tiptapEditor.unregisterPlugin(yCommentsPluginKey);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// view already destroyed
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
}, [editor, store, docId, selfHeal, withView]);
|
|
101
|
+
const selectThread = useCallback((threadId) => {
|
|
102
|
+
setSelectedThreadId(threadId);
|
|
103
|
+
withView((view) => pmSelectThread(view, threadId));
|
|
104
|
+
}, [withView]);
|
|
105
|
+
const openComposer = useCallback(() => {
|
|
106
|
+
try {
|
|
107
|
+
const { from, to } = editor.prosemirrorState.selection;
|
|
108
|
+
if (from === to) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
setComposer({ from, to });
|
|
112
|
+
// keep the selection visible while focus moves into the composer
|
|
113
|
+
withView((view) => pmSetPendingRange(view, { from, to }));
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// not mounted
|
|
117
|
+
}
|
|
118
|
+
}, [editor, withView]);
|
|
119
|
+
const closeComposer = useCallback(() => {
|
|
120
|
+
setComposer(null);
|
|
121
|
+
withView((view) => pmSetPendingRange(view, null));
|
|
122
|
+
}, [withView]);
|
|
123
|
+
const requestUsers = useCallback((userIds) => {
|
|
124
|
+
const missing = userIds.filter((id) => !pendingUserIds.current.has(id));
|
|
125
|
+
if (missing.length === 0) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
for (const id of missing) {
|
|
129
|
+
pendingUserIds.current.add(id);
|
|
130
|
+
}
|
|
131
|
+
resolveUsers(missing).then((resolved) => {
|
|
132
|
+
setUsers((prev) => {
|
|
133
|
+
const next = new Map(prev);
|
|
134
|
+
for (const user of resolved) {
|
|
135
|
+
next.set(user.id, user);
|
|
136
|
+
}
|
|
137
|
+
return next;
|
|
138
|
+
});
|
|
139
|
+
}, () => {
|
|
140
|
+
for (const id of missing) {
|
|
141
|
+
pendingUserIds.current.delete(id); // allow retry
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
}, [resolveUsers]);
|
|
145
|
+
const value = useMemo(() => ({
|
|
146
|
+
editor,
|
|
147
|
+
store,
|
|
148
|
+
docId,
|
|
149
|
+
threads,
|
|
150
|
+
resolutions,
|
|
151
|
+
selectedThreadId,
|
|
152
|
+
selectThread,
|
|
153
|
+
composer,
|
|
154
|
+
openComposer,
|
|
155
|
+
closeComposer,
|
|
156
|
+
users,
|
|
157
|
+
requestUsers,
|
|
158
|
+
}), [
|
|
159
|
+
editor,
|
|
160
|
+
store,
|
|
161
|
+
docId,
|
|
162
|
+
threads,
|
|
163
|
+
resolutions,
|
|
164
|
+
selectedThreadId,
|
|
165
|
+
selectThread,
|
|
166
|
+
composer,
|
|
167
|
+
openComposer,
|
|
168
|
+
closeComposer,
|
|
169
|
+
users,
|
|
170
|
+
requestUsers,
|
|
171
|
+
]);
|
|
172
|
+
return _jsx(CommentsContext.Provider, { value: value, children: props.children });
|
|
173
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { CommentsProvider, useComments, type CommentsProviderProps, type CommentsContextValue, type AnyBlockNoteEditor, } from "./context.js";
|
|
2
|
+
export { CreateCommentButton } from "./components/CreateCommentButton.js";
|
|
3
|
+
export { FloatingComposer } from "./components/FloatingComposer.js";
|
|
4
|
+
export { FloatingThread } from "./components/FloatingThread.js";
|
|
5
|
+
export { ThreadsSidebar, type ThreadsSidebarProps, type ThreadsFilter } from "./components/ThreadsSidebar.js";
|
|
6
|
+
export { ThreadBody, type ThreadBodyProps } from "./components/ThreadBody.js";
|
|
7
|
+
export { DefaultThreadStoreAuth, AllowAllThreadStoreAuth, InMemoryThreadStore, RestThreadStore, PushThreadStore, OptimisticThreadStore, type PushTransport, type Anchor, type Comment, type CommentBody, type Resolution, type Thread, type ThreadStore, type ThreadStoreAuth, type User, type ResolveUsersFn, } from "@defensestation/y-comments-core";
|
|
8
|
+
export { anchorFromSelection, anchorFromRange, yCommentsPlugin, yCommentsPluginKey, } from "@defensestation/y-comments-prosemirror";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { CommentsProvider, useComments, } from "./context.js";
|
|
2
|
+
export { CreateCommentButton } from "./components/CreateCommentButton.js";
|
|
3
|
+
export { FloatingComposer } from "./components/FloatingComposer.js";
|
|
4
|
+
export { FloatingThread } from "./components/FloatingThread.js";
|
|
5
|
+
export { ThreadsSidebar } from "./components/ThreadsSidebar.js";
|
|
6
|
+
export { ThreadBody } from "./components/ThreadBody.js";
|
|
7
|
+
// Re-exports so hosts only need this package for common flows.
|
|
8
|
+
export { DefaultThreadStoreAuth, AllowAllThreadStoreAuth, InMemoryThreadStore, RestThreadStore, PushThreadStore, OptimisticThreadStore, } from "@defensestation/y-comments-core";
|
|
9
|
+
export { anchorFromSelection, anchorFromRange, yCommentsPlugin, yCommentsPluginKey, } from "@defensestation/y-comments-prosemirror";
|
package/dist/style.css
ADDED
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
/* @defensestation/y-comments — themed via BlockNote CSS variables
|
|
2
|
+
(--bn-colors-*) with standalone fallbacks, so components work both inside
|
|
3
|
+
and next to a BlockNoteView. Override the --y-comments-* variables to
|
|
4
|
+
re-skin. */
|
|
5
|
+
|
|
6
|
+
:root {
|
|
7
|
+
--y-comments-highlight-bg: rgba(252, 188, 3, 0.35);
|
|
8
|
+
--y-comments-highlight-active-bg: rgba(252, 188, 3, 0.6);
|
|
9
|
+
--y-comments-bg: var(--bn-colors-menu-background, #ffffff);
|
|
10
|
+
--y-comments-text: var(--bn-colors-menu-text, #3f4045);
|
|
11
|
+
--y-comments-muted: var(--bn-colors-side-menu, #cfcfcf);
|
|
12
|
+
--y-comments-border: var(--bn-colors-border, #efefef);
|
|
13
|
+
--y-comments-accent: var(--bn-colors-highlights-blue-background, #307bf6);
|
|
14
|
+
--y-comments-radius: var(--bn-border-radius, 6px);
|
|
15
|
+
--y-comments-font: var(
|
|
16
|
+
--bn-font-family,
|
|
17
|
+
"Inter", "SF Pro Display", -apple-system, BlinkMacSystemFont, sans-serif
|
|
18
|
+
);
|
|
19
|
+
--y-comments-shadow: 0 4px 12px rgb(0 0 0 / 0.15);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/* Dark theme is opt-in so the comment UI always matches the editor rather
|
|
23
|
+
than the OS: set data-y-comments-theme="dark" on any ancestor (e.g.
|
|
24
|
+
<html> or your app root), typically alongside BlockNoteView's
|
|
25
|
+
theme="dark". */
|
|
26
|
+
[data-y-comments-theme="dark"] {
|
|
27
|
+
--y-comments-bg: var(--bn-colors-menu-background, #1f1f1f);
|
|
28
|
+
--y-comments-text: var(--bn-colors-menu-text, #cfcfcf);
|
|
29
|
+
--y-comments-border: var(--bn-colors-border, #353535);
|
|
30
|
+
--y-comments-shadow: 0 4px 12px rgb(0 0 0 / 0.5);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/* ---------- editor highlights (decorations) ---------- */
|
|
34
|
+
|
|
35
|
+
.y-comments-highlight {
|
|
36
|
+
background: var(--y-comments-highlight-bg);
|
|
37
|
+
border-bottom: 2px solid var(--y-comments-highlight-active-bg);
|
|
38
|
+
cursor: pointer;
|
|
39
|
+
transition: background 0.15s ease;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.y-comments-highlight.y-comments-active,
|
|
43
|
+
.y-comments-highlight:hover {
|
|
44
|
+
background: var(--y-comments-highlight-active-bg);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@keyframes y-comments-flash-frames {
|
|
48
|
+
0%, 60% { background: var(--y-comments-highlight-active-bg); }
|
|
49
|
+
100% { background: var(--y-comments-highlight-bg); }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.y-comments-flash {
|
|
53
|
+
animation: y-comments-flash-frames 1.2s ease;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* Selection being commented on while the composer has focus. */
|
|
57
|
+
.y-comments-pending {
|
|
58
|
+
background: var(--y-comments-highlight-bg);
|
|
59
|
+
border-bottom: 2px dashed var(--y-comments-highlight-active-bg);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* ---------- shared chrome ---------- */
|
|
63
|
+
|
|
64
|
+
.y-comments-floating,
|
|
65
|
+
.y-comments-sidebar,
|
|
66
|
+
.y-comments-card {
|
|
67
|
+
font-family: var(--y-comments-font);
|
|
68
|
+
color: var(--y-comments-text);
|
|
69
|
+
font-size: 14px;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.y-comments-floating {
|
|
73
|
+
background: var(--y-comments-bg);
|
|
74
|
+
border: 1px solid var(--y-comments-border);
|
|
75
|
+
border-radius: var(--y-comments-radius);
|
|
76
|
+
box-shadow: var(--y-comments-shadow);
|
|
77
|
+
padding: 10px;
|
|
78
|
+
width: 320px;
|
|
79
|
+
max-height: 420px;
|
|
80
|
+
overflow-y: auto;
|
|
81
|
+
z-index: 3000;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.y-comments-composer {
|
|
85
|
+
display: flex;
|
|
86
|
+
flex-direction: column;
|
|
87
|
+
gap: 6px;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.y-comments-input {
|
|
91
|
+
font-family: inherit;
|
|
92
|
+
font-size: 14px;
|
|
93
|
+
color: inherit;
|
|
94
|
+
background: transparent;
|
|
95
|
+
border: 1px solid var(--y-comments-border);
|
|
96
|
+
border-radius: var(--y-comments-radius);
|
|
97
|
+
padding: 6px 8px;
|
|
98
|
+
resize: vertical;
|
|
99
|
+
min-height: 32px;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.y-comments-input:focus {
|
|
103
|
+
outline: none;
|
|
104
|
+
border-color: var(--y-comments-accent);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.y-comments-composer-actions {
|
|
108
|
+
display: flex;
|
|
109
|
+
justify-content: flex-end;
|
|
110
|
+
gap: 6px;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.y-comments-button {
|
|
114
|
+
font-family: inherit;
|
|
115
|
+
font-size: 13px;
|
|
116
|
+
border: 1px solid var(--y-comments-border);
|
|
117
|
+
background: transparent;
|
|
118
|
+
color: inherit;
|
|
119
|
+
border-radius: var(--y-comments-radius);
|
|
120
|
+
padding: 4px 10px;
|
|
121
|
+
cursor: pointer;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.y-comments-button-primary {
|
|
125
|
+
background: var(--y-comments-accent);
|
|
126
|
+
border-color: var(--y-comments-accent);
|
|
127
|
+
color: #ffffff;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.y-comments-button:disabled {
|
|
131
|
+
opacity: 0.5;
|
|
132
|
+
cursor: default;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.y-comments-icon-button {
|
|
136
|
+
display: inline-flex;
|
|
137
|
+
align-items: center;
|
|
138
|
+
border: none;
|
|
139
|
+
background: transparent;
|
|
140
|
+
color: inherit;
|
|
141
|
+
opacity: 0.6;
|
|
142
|
+
cursor: pointer;
|
|
143
|
+
padding: 2px;
|
|
144
|
+
border-radius: var(--y-comments-radius);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.y-comments-icon-button:hover {
|
|
148
|
+
opacity: 1;
|
|
149
|
+
background: var(--y-comments-border);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.y-comments-error {
|
|
153
|
+
color: #d33131;
|
|
154
|
+
font-size: 12px;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/* ---------- thread & comments ---------- */
|
|
158
|
+
|
|
159
|
+
.y-comments-thread {
|
|
160
|
+
display: flex;
|
|
161
|
+
flex-direction: column;
|
|
162
|
+
gap: 8px;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/* Long threads scroll in the middle; header, quote and reply box stay put. */
|
|
166
|
+
.y-comments-comments {
|
|
167
|
+
display: flex;
|
|
168
|
+
flex-direction: column;
|
|
169
|
+
gap: 8px;
|
|
170
|
+
overflow-y: auto;
|
|
171
|
+
max-height: var(--y-comments-thread-max-height, 280px);
|
|
172
|
+
overscroll-behavior: contain;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.y-comments-thread-header {
|
|
176
|
+
display: flex;
|
|
177
|
+
align-items: center;
|
|
178
|
+
gap: 6px;
|
|
179
|
+
min-height: 20px;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.y-comments-thread-header-spacer {
|
|
183
|
+
flex: 1;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.y-comments-badge {
|
|
187
|
+
font-size: 11px;
|
|
188
|
+
font-weight: 600;
|
|
189
|
+
text-transform: uppercase;
|
|
190
|
+
letter-spacing: 0.03em;
|
|
191
|
+
padding: 1px 6px;
|
|
192
|
+
border-radius: 999px;
|
|
193
|
+
background: var(--y-comments-highlight-bg);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.y-comments-badge-resolved {
|
|
197
|
+
background: rgba(48, 209, 88, 0.25);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.y-comments-quote {
|
|
201
|
+
margin: 0;
|
|
202
|
+
padding: 2px 8px;
|
|
203
|
+
border-left: 3px solid var(--y-comments-highlight-active-bg);
|
|
204
|
+
color: inherit;
|
|
205
|
+
opacity: 0.75;
|
|
206
|
+
font-style: italic;
|
|
207
|
+
white-space: pre-wrap;
|
|
208
|
+
overflow-wrap: anywhere;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.y-comments-comment-header {
|
|
212
|
+
display: flex;
|
|
213
|
+
align-items: center;
|
|
214
|
+
gap: 6px;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.y-comments-avatar {
|
|
218
|
+
width: 20px;
|
|
219
|
+
height: 20px;
|
|
220
|
+
border-radius: 50%;
|
|
221
|
+
object-fit: cover;
|
|
222
|
+
flex: none;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.y-comments-avatar-fallback {
|
|
226
|
+
display: inline-flex;
|
|
227
|
+
align-items: center;
|
|
228
|
+
justify-content: center;
|
|
229
|
+
background: var(--y-comments-accent);
|
|
230
|
+
color: #ffffff;
|
|
231
|
+
font-size: 11px;
|
|
232
|
+
font-weight: 600;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.y-comments-username {
|
|
236
|
+
font-weight: 600;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
.y-comments-timestamp {
|
|
240
|
+
font-size: 12px;
|
|
241
|
+
opacity: 0.6;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.y-comments-comment-actions {
|
|
245
|
+
margin-left: auto;
|
|
246
|
+
display: inline-flex;
|
|
247
|
+
gap: 2px;
|
|
248
|
+
opacity: 0;
|
|
249
|
+
transition: opacity 0.1s ease;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.y-comments-comment:hover .y-comments-comment-actions {
|
|
253
|
+
opacity: 1;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.y-comments-comment-body {
|
|
257
|
+
white-space: pre-wrap;
|
|
258
|
+
overflow-wrap: anywhere;
|
|
259
|
+
padding-left: 26px;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.y-comments-reactions {
|
|
263
|
+
display: flex;
|
|
264
|
+
flex-wrap: wrap;
|
|
265
|
+
gap: 4px;
|
|
266
|
+
padding-left: 26px;
|
|
267
|
+
align-items: center;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
.y-comments-reaction {
|
|
271
|
+
font-family: inherit;
|
|
272
|
+
font-size: 12px;
|
|
273
|
+
border: 1px solid var(--y-comments-border);
|
|
274
|
+
background: transparent;
|
|
275
|
+
color: inherit;
|
|
276
|
+
border-radius: 999px;
|
|
277
|
+
padding: 1px 8px;
|
|
278
|
+
cursor: pointer;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
.y-comments-reaction-mine {
|
|
282
|
+
border-color: var(--y-comments-accent);
|
|
283
|
+
background: color-mix(in srgb, var(--y-comments-accent) 12%, transparent);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
.y-comments-reaction-picker-wrap {
|
|
287
|
+
position: relative;
|
|
288
|
+
display: inline-flex;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.y-comments-reaction-picker {
|
|
292
|
+
position: absolute;
|
|
293
|
+
bottom: calc(100% + 4px);
|
|
294
|
+
left: 0;
|
|
295
|
+
display: flex;
|
|
296
|
+
gap: 2px;
|
|
297
|
+
padding: 4px;
|
|
298
|
+
background: var(--y-comments-bg);
|
|
299
|
+
border: 1px solid var(--y-comments-border);
|
|
300
|
+
border-radius: var(--y-comments-radius);
|
|
301
|
+
box-shadow: var(--y-comments-shadow);
|
|
302
|
+
z-index: 1;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/* ---------- sidebar ---------- */
|
|
306
|
+
|
|
307
|
+
.y-comments-sidebar {
|
|
308
|
+
display: flex;
|
|
309
|
+
flex-direction: column;
|
|
310
|
+
gap: 10px;
|
|
311
|
+
min-width: 280px;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
.y-comments-sidebar-header {
|
|
315
|
+
display: flex;
|
|
316
|
+
align-items: center;
|
|
317
|
+
justify-content: space-between;
|
|
318
|
+
gap: 8px;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
.y-comments-sidebar-title {
|
|
322
|
+
font-weight: 700;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
.y-comments-filter {
|
|
326
|
+
display: inline-flex;
|
|
327
|
+
border: 1px solid var(--y-comments-border);
|
|
328
|
+
border-radius: var(--y-comments-radius);
|
|
329
|
+
overflow: hidden;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
.y-comments-filter-button {
|
|
333
|
+
font-family: inherit;
|
|
334
|
+
font-size: 12px;
|
|
335
|
+
padding: 3px 10px;
|
|
336
|
+
border: none;
|
|
337
|
+
background: transparent;
|
|
338
|
+
color: inherit;
|
|
339
|
+
cursor: pointer;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
.y-comments-filter-button-active {
|
|
343
|
+
background: var(--y-comments-accent);
|
|
344
|
+
color: #ffffff;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
.y-comments-card {
|
|
348
|
+
background: var(--y-comments-bg);
|
|
349
|
+
border: 1px solid var(--y-comments-border);
|
|
350
|
+
border-radius: var(--y-comments-radius);
|
|
351
|
+
padding: 10px;
|
|
352
|
+
cursor: pointer;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
.y-comments-card-selected {
|
|
356
|
+
border-color: var(--y-comments-accent);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
.y-comments-group-label {
|
|
360
|
+
font-size: 12px;
|
|
361
|
+
font-weight: 700;
|
|
362
|
+
text-transform: uppercase;
|
|
363
|
+
letter-spacing: 0.05em;
|
|
364
|
+
opacity: 0.6;
|
|
365
|
+
margin-top: 6px;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
.y-comments-empty {
|
|
369
|
+
opacity: 0.6;
|
|
370
|
+
font-style: italic;
|
|
371
|
+
}
|
package/dist/time.d.ts
ADDED
package/dist/time.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** "just now", "5m", "3h", "2d", then a locale date — GitHub-style. */
|
|
2
|
+
export function relativeTime(iso, now = new Date()) {
|
|
3
|
+
const then = new Date(iso);
|
|
4
|
+
const seconds = Math.max(0, (now.getTime() - then.getTime()) / 1000);
|
|
5
|
+
if (seconds < 60) {
|
|
6
|
+
return "just now";
|
|
7
|
+
}
|
|
8
|
+
if (seconds < 3600) {
|
|
9
|
+
return `${Math.floor(seconds / 60)}m`;
|
|
10
|
+
}
|
|
11
|
+
if (seconds < 86400) {
|
|
12
|
+
return `${Math.floor(seconds / 3600)}h`;
|
|
13
|
+
}
|
|
14
|
+
if (seconds < 86400 * 7) {
|
|
15
|
+
return `${Math.floor(seconds / 86400)}d`;
|
|
16
|
+
}
|
|
17
|
+
return then.toLocaleDateString();
|
|
18
|
+
}
|