@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.
@@ -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;
@@ -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
+ }
@@ -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
@@ -0,0 +1,2 @@
1
+ /** "just now", "5m", "3h", "2d", then a locale date — GitHub-style. */
2
+ export declare function relativeTime(iso: string, now?: Date): string;
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
+ }