rte-builder 1.0.0 → 2.0.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/index.mjs CHANGED
@@ -8674,8 +8674,1381 @@ init_Emoji();
8674
8674
  init_Fullscreen();
8675
8675
  init_Print();
8676
8676
  init_Indent();
8677
+
8678
+ // src/collaboration/CollaborationContext.tsx
8679
+ import { createContext, useContext, useReducer, useCallback as useCallback7, useEffect as useEffect9, useRef as useRef8 } from "react";
8680
+ import { jsx as jsx10 } from "react/jsx-runtime";
8681
+ var initialState = {
8682
+ status: "disconnected",
8683
+ users: [],
8684
+ error: void 0,
8685
+ isHost: false
8686
+ };
8687
+ function collaborationReducer(state, action) {
8688
+ switch (action.type) {
8689
+ case "SET_STATUS":
8690
+ return { ...state, status: action.status, error: action.status === "error" ? state.error : void 0 };
8691
+ case "SET_USERS":
8692
+ return { ...state, users: action.users };
8693
+ case "ADD_USER":
8694
+ if (state.users.find((u) => u.id === action.user.id)) {
8695
+ return {
8696
+ ...state,
8697
+ users: state.users.map((u) => u.id === action.user.id ? action.user : u)
8698
+ };
8699
+ }
8700
+ return { ...state, users: [...state.users, action.user] };
8701
+ case "REMOVE_USER":
8702
+ return { ...state, users: state.users.filter((u) => u.id !== action.userId) };
8703
+ case "UPDATE_CURSOR":
8704
+ return {
8705
+ ...state,
8706
+ users: state.users.map(
8707
+ (u) => u.id === action.userId ? { ...u, cursor: action.cursor, lastActive: Date.now() } : u
8708
+ )
8709
+ };
8710
+ case "SET_ERROR":
8711
+ return { ...state, status: "error", error: action.error };
8712
+ case "CLEAR_ERROR":
8713
+ return { ...state, error: void 0 };
8714
+ case "SET_HOST":
8715
+ return { ...state, isHost: action.isHost };
8716
+ default:
8717
+ return state;
8718
+ }
8719
+ }
8720
+ var CollaborationContext = createContext(null);
8721
+ function generateUserColor() {
8722
+ const colors = [
8723
+ "#f87171",
8724
+ "#fb923c",
8725
+ "#fbbf24",
8726
+ "#a3e635",
8727
+ "#4ade80",
8728
+ "#2dd4bf",
8729
+ "#22d3ee",
8730
+ "#60a5fa",
8731
+ "#a78bfa",
8732
+ "#e879f9"
8733
+ ];
8734
+ return colors[Math.floor(Math.random() * colors.length)];
8735
+ }
8736
+ function CollaborationProvider({
8737
+ children,
8738
+ config,
8739
+ onStatusChange,
8740
+ onUsersChange
8741
+ }) {
8742
+ const [state, dispatch] = useReducer(collaborationReducer, initialState);
8743
+ const wsRef = useRef8(null);
8744
+ const reconnectTimeoutRef = useRef8(null);
8745
+ const reconnectAttemptsRef = useRef8(0);
8746
+ useEffect9(() => {
8747
+ onStatusChange?.(state.status);
8748
+ }, [state.status, onStatusChange]);
8749
+ useEffect9(() => {
8750
+ onUsersChange?.(state.users);
8751
+ }, [state.users, onUsersChange]);
8752
+ const connect = useCallback7(async () => {
8753
+ if (!config) return;
8754
+ dispatch({ type: "SET_STATUS", status: "connecting" });
8755
+ try {
8756
+ if (config.provider === "websocket" && config.serverUrl) {
8757
+ const url = new URL(config.serverUrl);
8758
+ url.searchParams.set("room", config.roomId);
8759
+ if (config.token) {
8760
+ url.searchParams.set("token", config.token);
8761
+ }
8762
+ const ws = new WebSocket(url.toString());
8763
+ wsRef.current = ws;
8764
+ ws.onopen = () => {
8765
+ dispatch({ type: "SET_STATUS", status: "connected" });
8766
+ reconnectAttemptsRef.current = 0;
8767
+ ws.send(JSON.stringify({
8768
+ type: "join",
8769
+ user: {
8770
+ id: config.user.id,
8771
+ name: config.user.name,
8772
+ avatar: config.user.avatar,
8773
+ color: config.user.color || generateUserColor()
8774
+ }
8775
+ }));
8776
+ };
8777
+ ws.onmessage = (event) => {
8778
+ try {
8779
+ const message = JSON.parse(event.data);
8780
+ handleCollaborationEvent(message);
8781
+ } catch {
8782
+ console.error("Failed to parse collaboration message");
8783
+ }
8784
+ };
8785
+ ws.onclose = () => {
8786
+ dispatch({ type: "SET_STATUS", status: "disconnected" });
8787
+ handleReconnect();
8788
+ };
8789
+ ws.onerror = () => {
8790
+ dispatch({ type: "SET_ERROR", error: "Connection error" });
8791
+ };
8792
+ } else if (config.provider === "webrtc") {
8793
+ dispatch({ type: "SET_ERROR", error: "WebRTC provider not yet implemented" });
8794
+ } else if (config.provider === "custom") {
8795
+ dispatch({ type: "SET_STATUS", status: "connected" });
8796
+ }
8797
+ } catch (error) {
8798
+ dispatch({ type: "SET_ERROR", error: error instanceof Error ? error.message : "Connection failed" });
8799
+ }
8800
+ }, [config]);
8801
+ const handleCollaborationEvent = useCallback7((event) => {
8802
+ switch (event.type) {
8803
+ case "user-joined":
8804
+ dispatch({ type: "ADD_USER", user: event.user });
8805
+ break;
8806
+ case "user-left":
8807
+ dispatch({ type: "REMOVE_USER", userId: event.userId });
8808
+ break;
8809
+ case "cursor-moved":
8810
+ dispatch({ type: "UPDATE_CURSOR", userId: event.userId, cursor: event.cursor });
8811
+ break;
8812
+ case "status-changed":
8813
+ dispatch({ type: "SET_STATUS", status: event.status });
8814
+ break;
8815
+ case "error":
8816
+ dispatch({ type: "SET_ERROR", error: event.message });
8817
+ break;
8818
+ }
8819
+ }, []);
8820
+ const handleReconnect = useCallback7(() => {
8821
+ if (!config?.autoReconnect) return;
8822
+ const maxAttempts = config.maxReconnectAttempts ?? 5;
8823
+ const interval = config.reconnectInterval ?? 3e3;
8824
+ if (reconnectAttemptsRef.current >= maxAttempts) {
8825
+ dispatch({ type: "SET_ERROR", error: "Max reconnection attempts reached" });
8826
+ return;
8827
+ }
8828
+ dispatch({ type: "SET_STATUS", status: "reconnecting" });
8829
+ reconnectAttemptsRef.current++;
8830
+ reconnectTimeoutRef.current = setTimeout(() => {
8831
+ connect();
8832
+ }, interval);
8833
+ }, [config, connect]);
8834
+ const disconnect = useCallback7(() => {
8835
+ if (reconnectTimeoutRef.current) {
8836
+ clearTimeout(reconnectTimeoutRef.current);
8837
+ }
8838
+ if (wsRef.current) {
8839
+ wsRef.current.close();
8840
+ wsRef.current = null;
8841
+ }
8842
+ dispatch({ type: "SET_STATUS", status: "disconnected" });
8843
+ dispatch({ type: "SET_USERS", users: [] });
8844
+ }, []);
8845
+ const updateCursor = useCallback7((cursor) => {
8846
+ if (!config || !wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) return;
8847
+ wsRef.current.send(JSON.stringify({
8848
+ type: "cursor",
8849
+ userId: config.user.id,
8850
+ cursor
8851
+ }));
8852
+ }, [config]);
8853
+ useEffect9(() => {
8854
+ return () => {
8855
+ disconnect();
8856
+ };
8857
+ }, [disconnect]);
8858
+ const value = {
8859
+ state,
8860
+ config: config ?? null,
8861
+ connect,
8862
+ disconnect,
8863
+ updateCursor,
8864
+ isEnabled: !!config
8865
+ };
8866
+ return /* @__PURE__ */ jsx10(CollaborationContext.Provider, { value, children });
8867
+ }
8868
+ function useCollaboration() {
8869
+ const context = useContext(CollaborationContext);
8870
+ if (!context) {
8871
+ throw new Error("useCollaboration must be used within a CollaborationProvider");
8872
+ }
8873
+ return context;
8874
+ }
8875
+ function useCollaborationOptional() {
8876
+ return useContext(CollaborationContext);
8877
+ }
8878
+
8879
+ // src/collaboration/PresenceIndicator.tsx
8880
+ import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
8881
+ function getInitials(name) {
8882
+ return name.split(" ").map((n) => n[0]).join("").toUpperCase().slice(0, 2);
8883
+ }
8884
+ function UserAvatar({ user, showName }) {
8885
+ return /* @__PURE__ */ jsxs10(
8886
+ "div",
8887
+ {
8888
+ className: "rte-presence-avatar",
8889
+ style: { borderColor: user.color },
8890
+ title: user.name,
8891
+ children: [
8892
+ user.avatar ? /* @__PURE__ */ jsx11("img", { src: user.avatar, alt: user.name }) : /* @__PURE__ */ jsx11("span", { style: { backgroundColor: user.color }, children: getInitials(user.name) }),
8893
+ user.isActive && /* @__PURE__ */ jsx11("div", { className: "rte-presence-active-dot" }),
8894
+ showName && /* @__PURE__ */ jsx11("div", { className: "rte-presence-name", children: user.name })
8895
+ ]
8896
+ }
8897
+ );
8898
+ }
8899
+ function StatusBadge({ status }) {
8900
+ const statusConfig = {
8901
+ connected: { label: "Connected", color: "#22c55e" },
8902
+ connecting: { label: "Connecting...", color: "#eab308" },
8903
+ reconnecting: { label: "Reconnecting...", color: "#f97316" },
8904
+ disconnected: { label: "Disconnected", color: "#6b7280" },
8905
+ error: { label: "Error", color: "#ef4444" }
8906
+ };
8907
+ const config = statusConfig[status] || statusConfig.disconnected;
8908
+ return /* @__PURE__ */ jsxs10("div", { className: "rte-presence-status", style: { color: config.color }, children: [
8909
+ /* @__PURE__ */ jsx11("div", { className: "rte-presence-status-dot", style: { backgroundColor: config.color } }),
8910
+ config.label
8911
+ ] });
8912
+ }
8913
+ function PresenceIndicator({
8914
+ maxAvatars = 5,
8915
+ showNames = false,
8916
+ className = ""
8917
+ }) {
8918
+ const collaboration = useCollaborationOptional();
8919
+ if (!collaboration?.isEnabled) {
8920
+ return null;
8921
+ }
8922
+ const { state } = collaboration;
8923
+ const visibleUsers = state.users.slice(0, maxAvatars);
8924
+ const remainingCount = Math.max(0, state.users.length - maxAvatars);
8925
+ return /* @__PURE__ */ jsxs10("div", { className: `rte-presence-indicator ${className}`, children: [
8926
+ /* @__PURE__ */ jsx11(StatusBadge, { status: state.status }),
8927
+ state.users.length > 0 && /* @__PURE__ */ jsxs10("div", { className: "rte-presence-avatars", children: [
8928
+ visibleUsers.map((user) => /* @__PURE__ */ jsx11(UserAvatar, { user, showName: showNames }, user.id)),
8929
+ remainingCount > 0 && /* @__PURE__ */ jsxs10("div", { className: "rte-presence-avatar rte-presence-more", children: [
8930
+ "+",
8931
+ remainingCount
8932
+ ] })
8933
+ ] }),
8934
+ state.error && /* @__PURE__ */ jsx11("div", { className: "rte-presence-error", title: state.error, children: "!" })
8935
+ ] });
8936
+ }
8937
+
8938
+ // src/comments/types.ts
8939
+ var DEFAULT_REACTION_EMOJIS = ["\u{1F44D}", "\u{1F44E}", "\u2764\uFE0F", "\u{1F389}", "\u{1F604}", "\u{1F615}", "\u{1F440}", "\u{1F680}"];
8940
+
8941
+ // src/comments/CommentsContext.tsx
8942
+ import { createContext as createContext2, useContext as useContext2, useReducer as useReducer2, useCallback as useCallback8, useEffect as useEffect10 } from "react";
8943
+ import { jsx as jsx12 } from "react/jsx-runtime";
8944
+ var initialState2 = {
8945
+ threads: [],
8946
+ activeThreadId: null,
8947
+ isPanelOpen: false,
8948
+ filter: "all",
8949
+ currentUser: null
8950
+ };
8951
+ function generateId() {
8952
+ return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
8953
+ }
8954
+ function commentsReducer(state, action) {
8955
+ switch (action.type) {
8956
+ case "SET_THREADS":
8957
+ return { ...state, threads: action.threads };
8958
+ case "ADD_THREAD":
8959
+ return { ...state, threads: [...state.threads, action.thread] };
8960
+ case "DELETE_THREAD":
8961
+ return {
8962
+ ...state,
8963
+ threads: state.threads.filter((t) => t.id !== action.threadId),
8964
+ activeThreadId: state.activeThreadId === action.threadId ? null : state.activeThreadId
8965
+ };
8966
+ case "RESOLVE_THREAD":
8967
+ return {
8968
+ ...state,
8969
+ threads: state.threads.map(
8970
+ (t) => t.id === action.threadId ? { ...t, status: "resolved", resolvedAt: Date.now(), resolvedBy: action.resolvedBy } : t
8971
+ )
8972
+ };
8973
+ case "REOPEN_THREAD":
8974
+ return {
8975
+ ...state,
8976
+ threads: state.threads.map(
8977
+ (t) => t.id === action.threadId ? { ...t, status: "open", resolvedAt: void 0, resolvedBy: void 0 } : t
8978
+ )
8979
+ };
8980
+ case "ADD_COMMENT":
8981
+ return {
8982
+ ...state,
8983
+ threads: state.threads.map(
8984
+ (t) => t.id === action.threadId ? { ...t, comments: [...t.comments, action.comment] } : t
8985
+ )
8986
+ };
8987
+ case "UPDATE_COMMENT":
8988
+ return {
8989
+ ...state,
8990
+ threads: state.threads.map(
8991
+ (t) => t.id === action.threadId ? {
8992
+ ...t,
8993
+ comments: t.comments.map(
8994
+ (c) => c.id === action.comment.id ? action.comment : c
8995
+ )
8996
+ } : t
8997
+ )
8998
+ };
8999
+ case "DELETE_COMMENT":
9000
+ return {
9001
+ ...state,
9002
+ threads: state.threads.map(
9003
+ (t) => t.id === action.threadId ? { ...t, comments: t.comments.filter((c) => c.id !== action.commentId) } : t
9004
+ )
9005
+ };
9006
+ case "ADD_REACTION":
9007
+ return {
9008
+ ...state,
9009
+ threads: state.threads.map(
9010
+ (t) => t.id === action.threadId ? {
9011
+ ...t,
9012
+ comments: t.comments.map((c) => {
9013
+ if (c.id !== action.commentId) return c;
9014
+ const reactions = c.reactions || [];
9015
+ const existingReaction = reactions.find((r) => r.emoji === action.emoji);
9016
+ if (existingReaction) {
9017
+ return {
9018
+ ...c,
9019
+ reactions: reactions.map(
9020
+ (r) => r.emoji === action.emoji ? { ...r, users: [...r.users, action.user] } : r
9021
+ )
9022
+ };
9023
+ }
9024
+ return {
9025
+ ...c,
9026
+ reactions: [...reactions, { emoji: action.emoji, users: [action.user] }]
9027
+ };
9028
+ })
9029
+ } : t
9030
+ )
9031
+ };
9032
+ case "REMOVE_REACTION":
9033
+ return {
9034
+ ...state,
9035
+ threads: state.threads.map(
9036
+ (t) => t.id === action.threadId ? {
9037
+ ...t,
9038
+ comments: t.comments.map((c) => {
9039
+ if (c.id !== action.commentId) return c;
9040
+ return {
9041
+ ...c,
9042
+ reactions: (c.reactions || []).map(
9043
+ (r) => r.emoji === action.emoji ? { ...r, users: r.users.filter((u) => u.id !== action.userId) } : r
9044
+ ).filter((r) => r.users.length > 0)
9045
+ };
9046
+ })
9047
+ } : t
9048
+ )
9049
+ };
9050
+ case "SET_ACTIVE_THREAD":
9051
+ return { ...state, activeThreadId: action.threadId };
9052
+ case "TOGGLE_PANEL":
9053
+ return { ...state, isPanelOpen: action.isOpen ?? !state.isPanelOpen };
9054
+ case "SET_FILTER":
9055
+ return { ...state, filter: action.filter };
9056
+ case "SET_CURRENT_USER":
9057
+ return { ...state, currentUser: action.user };
9058
+ default:
9059
+ return state;
9060
+ }
9061
+ }
9062
+ var CommentsContext = createContext2(null);
9063
+ function CommentsProvider({
9064
+ children,
9065
+ config,
9066
+ initialThreads = [],
9067
+ onThreadsChange
9068
+ }) {
9069
+ const [state, dispatch] = useReducer2(commentsReducer, {
9070
+ ...initialState2,
9071
+ threads: initialThreads,
9072
+ currentUser: config?.currentUser ?? null
9073
+ });
9074
+ const subscribersRef = { current: /* @__PURE__ */ new Set() };
9075
+ useEffect10(() => {
9076
+ if (config?.onLoad) {
9077
+ config.onLoad().then((threads) => {
9078
+ dispatch({ type: "SET_THREADS", threads });
9079
+ });
9080
+ }
9081
+ }, [config]);
9082
+ useEffect10(() => {
9083
+ onThreadsChange?.(state.threads);
9084
+ if (config?.onSave) {
9085
+ config.onSave(state.threads);
9086
+ }
9087
+ }, [state.threads, onThreadsChange, config]);
9088
+ useEffect10(() => {
9089
+ if (config?.currentUser) {
9090
+ dispatch({ type: "SET_CURRENT_USER", user: config.currentUser });
9091
+ }
9092
+ }, [config?.currentUser]);
9093
+ const emitEvent = useCallback8((event) => {
9094
+ subscribersRef.current.forEach((callback) => callback(event));
9095
+ }, []);
9096
+ const createThread = useCallback8((range, initialComment) => {
9097
+ if (!state.currentUser) return null;
9098
+ const threadId = generateId();
9099
+ const commentId = generateId();
9100
+ const now = Date.now();
9101
+ const comment = {
9102
+ id: commentId,
9103
+ threadId,
9104
+ content: initialComment,
9105
+ author: state.currentUser,
9106
+ createdAt: now,
9107
+ isEdited: false
9108
+ };
9109
+ const thread = {
9110
+ id: threadId,
9111
+ range,
9112
+ comments: [comment],
9113
+ status: "open",
9114
+ createdAt: now
9115
+ };
9116
+ dispatch({ type: "ADD_THREAD", thread });
9117
+ emitEvent({ type: "thread-created", thread });
9118
+ return thread;
9119
+ }, [state.currentUser, emitEvent]);
9120
+ const deleteThread = useCallback8((threadId) => {
9121
+ if (config?.allowDelete === false) return;
9122
+ dispatch({ type: "DELETE_THREAD", threadId });
9123
+ emitEvent({ type: "thread-deleted", threadId });
9124
+ }, [config, emitEvent]);
9125
+ const resolveThread = useCallback8((threadId) => {
9126
+ if (!state.currentUser || config?.allowResolve === false) return;
9127
+ dispatch({ type: "RESOLVE_THREAD", threadId, resolvedBy: state.currentUser });
9128
+ emitEvent({ type: "thread-resolved", threadId, resolvedBy: state.currentUser });
9129
+ }, [state.currentUser, config, emitEvent]);
9130
+ const reopenThread = useCallback8((threadId) => {
9131
+ dispatch({ type: "REOPEN_THREAD", threadId });
9132
+ emitEvent({ type: "thread-reopened", threadId });
9133
+ }, [emitEvent]);
9134
+ const addComment = useCallback8((threadId, content) => {
9135
+ if (!state.currentUser) return null;
9136
+ const comment = {
9137
+ id: generateId(),
9138
+ threadId,
9139
+ content,
9140
+ author: state.currentUser,
9141
+ createdAt: Date.now(),
9142
+ isEdited: false
9143
+ };
9144
+ dispatch({ type: "ADD_COMMENT", threadId, comment });
9145
+ emitEvent({ type: "comment-added", threadId, comment });
9146
+ return comment;
9147
+ }, [state.currentUser, emitEvent]);
9148
+ const updateComment = useCallback8((threadId, commentId, content) => {
9149
+ if (config?.allowEdit === false) return;
9150
+ const thread = state.threads.find((t) => t.id === threadId);
9151
+ const existingComment = thread?.comments.find((c) => c.id === commentId);
9152
+ if (!existingComment) return;
9153
+ const updatedComment = {
9154
+ ...existingComment,
9155
+ content,
9156
+ updatedAt: Date.now(),
9157
+ isEdited: true
9158
+ };
9159
+ dispatch({ type: "UPDATE_COMMENT", threadId, comment: updatedComment });
9160
+ emitEvent({ type: "comment-updated", threadId, comment: updatedComment });
9161
+ }, [state.threads, config, emitEvent]);
9162
+ const deleteComment = useCallback8((threadId, commentId) => {
9163
+ if (config?.allowDelete === false) return;
9164
+ dispatch({ type: "DELETE_COMMENT", threadId, commentId });
9165
+ emitEvent({ type: "comment-deleted", threadId, commentId });
9166
+ }, [config, emitEvent]);
9167
+ const addReaction = useCallback8((threadId, commentId, emoji) => {
9168
+ if (!state.currentUser || config?.allowReactions === false) return;
9169
+ const allowedEmojis = config?.reactionEmojis ?? DEFAULT_REACTION_EMOJIS;
9170
+ if (!allowedEmojis.includes(emoji)) return;
9171
+ dispatch({ type: "ADD_REACTION", threadId, commentId, emoji, user: state.currentUser });
9172
+ emitEvent({ type: "reaction-added", threadId, commentId, emoji, user: state.currentUser });
9173
+ }, [state.currentUser, config, emitEvent]);
9174
+ const removeReaction = useCallback8((threadId, commentId, emoji) => {
9175
+ if (!state.currentUser) return;
9176
+ dispatch({ type: "REMOVE_REACTION", threadId, commentId, emoji, userId: state.currentUser.id });
9177
+ emitEvent({ type: "reaction-removed", threadId, commentId, emoji, userId: state.currentUser.id });
9178
+ }, [state.currentUser, emitEvent]);
9179
+ const setActiveThread = useCallback8((threadId) => {
9180
+ dispatch({ type: "SET_ACTIVE_THREAD", threadId });
9181
+ }, []);
9182
+ const togglePanel = useCallback8((isOpen) => {
9183
+ dispatch({ type: "TOGGLE_PANEL", isOpen });
9184
+ }, []);
9185
+ const setFilter = useCallback8((filter) => {
9186
+ dispatch({ type: "SET_FILTER", filter });
9187
+ }, []);
9188
+ const getThreadByRange = useCallback8((from, to) => {
9189
+ return state.threads.find((t) => t.range.from === from && t.range.to === to);
9190
+ }, [state.threads]);
9191
+ const getFilteredThreads = useCallback8(() => {
9192
+ if (state.filter === "all") return state.threads;
9193
+ return state.threads.filter((t) => t.status === state.filter);
9194
+ }, [state.threads, state.filter]);
9195
+ const subscribe = useCallback8((callback) => {
9196
+ subscribersRef.current.add(callback);
9197
+ return () => {
9198
+ subscribersRef.current.delete(callback);
9199
+ };
9200
+ }, []);
9201
+ const value = {
9202
+ state,
9203
+ config: config ?? null,
9204
+ createThread,
9205
+ deleteThread,
9206
+ resolveThread,
9207
+ reopenThread,
9208
+ addComment,
9209
+ updateComment,
9210
+ deleteComment,
9211
+ addReaction,
9212
+ removeReaction,
9213
+ setActiveThread,
9214
+ togglePanel,
9215
+ setFilter,
9216
+ getThreadByRange,
9217
+ getFilteredThreads,
9218
+ subscribe,
9219
+ isEnabled: !!config
9220
+ };
9221
+ return /* @__PURE__ */ jsx12(CommentsContext.Provider, { value, children });
9222
+ }
9223
+ function useComments() {
9224
+ const context = useContext2(CommentsContext);
9225
+ if (!context) {
9226
+ throw new Error("useComments must be used within a CommentsProvider");
9227
+ }
9228
+ return context;
9229
+ }
9230
+ function useCommentsOptional() {
9231
+ return useContext2(CommentsContext);
9232
+ }
9233
+
9234
+ // src/comments/CommentsPanel.tsx
9235
+ import { useState as useState8 } from "react";
9236
+ import { Fragment as Fragment3, jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
9237
+ function formatRelativeTime(timestamp) {
9238
+ const now = Date.now();
9239
+ const diff = now - timestamp;
9240
+ const minutes = Math.floor(diff / 6e4);
9241
+ const hours = Math.floor(diff / 36e5);
9242
+ const days = Math.floor(diff / 864e5);
9243
+ if (minutes < 1) return "Just now";
9244
+ if (minutes < 60) return `${minutes}m ago`;
9245
+ if (hours < 24) return `${hours}h ago`;
9246
+ if (days < 7) return `${days}d ago`;
9247
+ return new Date(timestamp).toLocaleDateString();
9248
+ }
9249
+ function CommentItem({
9250
+ comment,
9251
+ threadId,
9252
+ isFirst
9253
+ }) {
9254
+ const comments = useCommentsOptional();
9255
+ const [isEditing, setIsEditing] = useState8(false);
9256
+ const [editContent, setEditContent] = useState8(comment.content);
9257
+ const [showReactions, setShowReactions] = useState8(false);
9258
+ if (!comments) return null;
9259
+ const { config, updateComment, deleteComment, addReaction, removeReaction, state } = comments;
9260
+ const canEdit = config?.allowEdit !== false && comment.author.id === state.currentUser?.id;
9261
+ const canDelete = config?.allowDelete !== false && comment.author.id === state.currentUser?.id;
9262
+ const canReact = config?.allowReactions !== false;
9263
+ const reactionEmojis = config?.reactionEmojis ?? DEFAULT_REACTION_EMOJIS;
9264
+ const handleSaveEdit = () => {
9265
+ updateComment(threadId, comment.id, editContent);
9266
+ setIsEditing(false);
9267
+ };
9268
+ const handleToggleReaction = (emoji) => {
9269
+ const reaction = comment.reactions?.find((r) => r.emoji === emoji);
9270
+ const hasReacted = reaction?.users.some((u) => u.id === state.currentUser?.id);
9271
+ if (hasReacted) {
9272
+ removeReaction(threadId, comment.id, emoji);
9273
+ } else {
9274
+ addReaction(threadId, comment.id, emoji);
9275
+ }
9276
+ setShowReactions(false);
9277
+ };
9278
+ return /* @__PURE__ */ jsxs11("div", { className: `rte-comment ${isFirst ? "rte-comment-first" : ""}`, children: [
9279
+ /* @__PURE__ */ jsxs11("div", { className: "rte-comment-header", children: [
9280
+ /* @__PURE__ */ jsxs11("div", { className: "rte-comment-author", children: [
9281
+ comment.author.avatar ? /* @__PURE__ */ jsx13("img", { src: comment.author.avatar, alt: comment.author.name, className: "rte-comment-avatar" }) : /* @__PURE__ */ jsx13("div", { className: "rte-comment-avatar-placeholder", children: comment.author.name.charAt(0).toUpperCase() }),
9282
+ /* @__PURE__ */ jsx13("span", { className: "rte-comment-author-name", children: comment.author.name })
9283
+ ] }),
9284
+ /* @__PURE__ */ jsxs11("span", { className: "rte-comment-time", children: [
9285
+ formatRelativeTime(comment.createdAt),
9286
+ comment.isEdited && " (edited)"
9287
+ ] })
9288
+ ] }),
9289
+ isEditing ? /* @__PURE__ */ jsxs11("div", { className: "rte-comment-edit", children: [
9290
+ /* @__PURE__ */ jsx13(
9291
+ "textarea",
9292
+ {
9293
+ value: editContent,
9294
+ onChange: (e) => setEditContent(e.target.value),
9295
+ className: "rte-comment-edit-input"
9296
+ }
9297
+ ),
9298
+ /* @__PURE__ */ jsxs11("div", { className: "rte-comment-edit-actions", children: [
9299
+ /* @__PURE__ */ jsx13("button", { onClick: handleSaveEdit, className: "rte-comment-btn-save", children: "Save" }),
9300
+ /* @__PURE__ */ jsx13("button", { onClick: () => setIsEditing(false), className: "rte-comment-btn-cancel", children: "Cancel" })
9301
+ ] })
9302
+ ] }) : /* @__PURE__ */ jsx13("div", { className: "rte-comment-content", dangerouslySetInnerHTML: { __html: comment.content } }),
9303
+ comment.reactions && comment.reactions.length > 0 && /* @__PURE__ */ jsx13("div", { className: "rte-comment-reactions", children: comment.reactions.map((reaction) => /* @__PURE__ */ jsxs11(
9304
+ "button",
9305
+ {
9306
+ className: `rte-comment-reaction ${reaction.users.some((u) => u.id === state.currentUser?.id) ? "active" : ""}`,
9307
+ onClick: () => handleToggleReaction(reaction.emoji),
9308
+ title: reaction.users.map((u) => u.name).join(", "),
9309
+ children: [
9310
+ reaction.emoji,
9311
+ " ",
9312
+ reaction.users.length
9313
+ ]
9314
+ },
9315
+ reaction.emoji
9316
+ )) }),
9317
+ /* @__PURE__ */ jsxs11("div", { className: "rte-comment-actions", children: [
9318
+ canReact && /* @__PURE__ */ jsxs11("div", { className: "rte-comment-reaction-picker", children: [
9319
+ /* @__PURE__ */ jsx13(
9320
+ "button",
9321
+ {
9322
+ className: "rte-comment-action-btn",
9323
+ onClick: () => setShowReactions(!showReactions),
9324
+ children: "\u{1F60A}"
9325
+ }
9326
+ ),
9327
+ showReactions && /* @__PURE__ */ jsx13("div", { className: "rte-comment-reaction-dropdown", children: reactionEmojis.map((emoji) => /* @__PURE__ */ jsx13("button", { onClick: () => handleToggleReaction(emoji), children: emoji }, emoji)) })
9328
+ ] }),
9329
+ canEdit && /* @__PURE__ */ jsx13("button", { className: "rte-comment-action-btn", onClick: () => setIsEditing(true), children: "Edit" }),
9330
+ canDelete && !isFirst && /* @__PURE__ */ jsx13(
9331
+ "button",
9332
+ {
9333
+ className: "rte-comment-action-btn rte-comment-action-delete",
9334
+ onClick: () => deleteComment(threadId, comment.id),
9335
+ children: "Delete"
9336
+ }
9337
+ )
9338
+ ] })
9339
+ ] });
9340
+ }
9341
+ function ThreadItem({ thread }) {
9342
+ const comments = useCommentsOptional();
9343
+ const [replyContent, setReplyContent] = useState8("");
9344
+ const [showReply, setShowReply] = useState8(false);
9345
+ if (!comments) return null;
9346
+ const { state, setActiveThread, addComment, resolveThread, reopenThread, deleteThread, config } = comments;
9347
+ const isActive = state.activeThreadId === thread.id;
9348
+ const canResolve = config?.allowResolve !== false;
9349
+ const handleReply = () => {
9350
+ if (replyContent.trim()) {
9351
+ addComment(thread.id, replyContent);
9352
+ setReplyContent("");
9353
+ setShowReply(false);
9354
+ }
9355
+ };
9356
+ return /* @__PURE__ */ jsxs11(
9357
+ "div",
9358
+ {
9359
+ className: `rte-thread ${isActive ? "rte-thread-active" : ""} ${thread.status === "resolved" ? "rte-thread-resolved" : ""}`,
9360
+ onClick: () => setActiveThread(thread.id),
9361
+ children: [
9362
+ /* @__PURE__ */ jsxs11("div", { className: "rte-thread-header", children: [
9363
+ /* @__PURE__ */ jsxs11("div", { className: "rte-thread-quote", children: [
9364
+ '"',
9365
+ thread.range.text.slice(0, 50),
9366
+ '..."'
9367
+ ] }),
9368
+ /* @__PURE__ */ jsxs11("div", { className: "rte-thread-meta", children: [
9369
+ /* @__PURE__ */ jsx13("span", { className: `rte-thread-status rte-thread-status-${thread.status}`, children: thread.status }),
9370
+ /* @__PURE__ */ jsxs11("span", { className: "rte-thread-count", children: [
9371
+ thread.comments.length,
9372
+ " comment",
9373
+ thread.comments.length !== 1 ? "s" : ""
9374
+ ] })
9375
+ ] })
9376
+ ] }),
9377
+ /* @__PURE__ */ jsx13("div", { className: "rte-thread-comments", children: thread.comments.map((comment, index) => /* @__PURE__ */ jsx13(
9378
+ CommentItem,
9379
+ {
9380
+ comment,
9381
+ threadId: thread.id,
9382
+ isFirst: index === 0
9383
+ },
9384
+ comment.id
9385
+ )) }),
9386
+ thread.status === "open" && /* @__PURE__ */ jsx13("div", { className: "rte-thread-reply", children: showReply ? /* @__PURE__ */ jsxs11(Fragment3, { children: [
9387
+ /* @__PURE__ */ jsx13(
9388
+ "textarea",
9389
+ {
9390
+ value: replyContent,
9391
+ onChange: (e) => setReplyContent(e.target.value),
9392
+ placeholder: "Write a reply...",
9393
+ className: "rte-thread-reply-input"
9394
+ }
9395
+ ),
9396
+ /* @__PURE__ */ jsxs11("div", { className: "rte-thread-reply-actions", children: [
9397
+ /* @__PURE__ */ jsx13("button", { onClick: handleReply, className: "rte-btn-primary", disabled: !replyContent.trim(), children: "Reply" }),
9398
+ /* @__PURE__ */ jsx13("button", { onClick: () => setShowReply(false), className: "rte-btn-secondary", children: "Cancel" })
9399
+ ] })
9400
+ ] }) : /* @__PURE__ */ jsx13("button", { onClick: () => setShowReply(true), className: "rte-thread-reply-btn", children: "Reply" }) }),
9401
+ /* @__PURE__ */ jsxs11("div", { className: "rte-thread-actions", children: [
9402
+ canResolve && thread.status === "open" && /* @__PURE__ */ jsx13("button", { onClick: () => resolveThread(thread.id), className: "rte-btn-resolve", children: "\u2713 Resolve" }),
9403
+ thread.status === "resolved" && /* @__PURE__ */ jsx13("button", { onClick: () => reopenThread(thread.id), className: "rte-btn-reopen", children: "Reopen" }),
9404
+ /* @__PURE__ */ jsx13(
9405
+ "button",
9406
+ {
9407
+ onClick: (e) => {
9408
+ e.stopPropagation();
9409
+ deleteThread(thread.id);
9410
+ },
9411
+ className: "rte-btn-delete-thread",
9412
+ children: "Delete"
9413
+ }
9414
+ )
9415
+ ] })
9416
+ ]
9417
+ }
9418
+ );
9419
+ }
9420
+ function CommentsPanel({ position = "right", className = "" }) {
9421
+ const comments = useCommentsOptional();
9422
+ if (!comments?.isEnabled || !comments.state.isPanelOpen) {
9423
+ return null;
9424
+ }
9425
+ const { state, togglePanel, setFilter, getFilteredThreads } = comments;
9426
+ const filteredThreads = getFilteredThreads();
9427
+ return /* @__PURE__ */ jsxs11("div", { className: `rte-comments-panel rte-comments-panel-${position} ${className}`, children: [
9428
+ /* @__PURE__ */ jsxs11("div", { className: "rte-comments-panel-header", children: [
9429
+ /* @__PURE__ */ jsx13("h3", { children: "Comments" }),
9430
+ /* @__PURE__ */ jsx13("button", { onClick: () => togglePanel(false), className: "rte-comments-close-btn", children: "\xD7" })
9431
+ ] }),
9432
+ /* @__PURE__ */ jsx13("div", { className: "rte-comments-filters", children: ["all", "open", "resolved"].map((filter) => /* @__PURE__ */ jsxs11(
9433
+ "button",
9434
+ {
9435
+ className: `rte-comments-filter ${state.filter === filter ? "active" : ""}`,
9436
+ onClick: () => setFilter(filter),
9437
+ children: [
9438
+ filter.charAt(0).toUpperCase() + filter.slice(1),
9439
+ /* @__PURE__ */ jsx13("span", { className: "rte-comments-filter-count", children: filter === "all" ? state.threads.length : state.threads.filter((t) => t.status === filter).length })
9440
+ ]
9441
+ },
9442
+ filter
9443
+ )) }),
9444
+ /* @__PURE__ */ jsx13("div", { className: "rte-comments-list", children: filteredThreads.length === 0 ? /* @__PURE__ */ jsx13("div", { className: "rte-comments-empty", children: state.filter === "all" ? "No comments yet. Select text and add a comment." : `No ${state.filter} comments.` }) : filteredThreads.map((thread) => /* @__PURE__ */ jsx13(ThreadItem, { thread }, thread.id)) })
9445
+ ] });
9446
+ }
9447
+
9448
+ // src/history/VersionHistoryContext.tsx
9449
+ import { createContext as createContext3, useContext as useContext3, useReducer as useReducer3, useCallback as useCallback9, useEffect as useEffect11, useRef as useRef9 } from "react";
9450
+ import { jsx as jsx14 } from "react/jsx-runtime";
9451
+ var initialState3 = {
9452
+ versions: [],
9453
+ viewingVersionId: null,
9454
+ isPanelOpen: false,
9455
+ isComparing: false,
9456
+ compareFromId: null,
9457
+ compareToId: null,
9458
+ autoSaveEnabled: true,
9459
+ autoSaveInterval: 6e4
9460
+ };
9461
+ function generateId2() {
9462
+ return `v-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
9463
+ }
9464
+ function versionHistoryReducer(state, action) {
9465
+ switch (action.type) {
9466
+ case "SET_VERSIONS":
9467
+ return { ...state, versions: action.versions };
9468
+ case "ADD_VERSION":
9469
+ return {
9470
+ ...state,
9471
+ versions: [action.version, ...state.versions]
9472
+ };
9473
+ case "DELETE_VERSION":
9474
+ return {
9475
+ ...state,
9476
+ versions: state.versions.filter((v) => v.id !== action.versionId),
9477
+ viewingVersionId: state.viewingVersionId === action.versionId ? null : state.viewingVersionId
9478
+ };
9479
+ case "PIN_VERSION":
9480
+ return {
9481
+ ...state,
9482
+ versions: state.versions.map(
9483
+ (v) => v.id === action.versionId ? { ...v, isPinned: true } : v
9484
+ )
9485
+ };
9486
+ case "UNPIN_VERSION":
9487
+ return {
9488
+ ...state,
9489
+ versions: state.versions.map(
9490
+ (v) => v.id === action.versionId ? { ...v, isPinned: false } : v
9491
+ )
9492
+ };
9493
+ case "RENAME_VERSION":
9494
+ return {
9495
+ ...state,
9496
+ versions: state.versions.map(
9497
+ (v) => v.id === action.versionId ? { ...v, title: action.title } : v
9498
+ )
9499
+ };
9500
+ case "SET_VIEWING_VERSION":
9501
+ return { ...state, viewingVersionId: action.versionId };
9502
+ case "TOGGLE_PANEL":
9503
+ return { ...state, isPanelOpen: action.isOpen ?? !state.isPanelOpen };
9504
+ case "START_COMPARE":
9505
+ return {
9506
+ ...state,
9507
+ isComparing: true,
9508
+ compareFromId: action.fromId,
9509
+ compareToId: action.toId
9510
+ };
9511
+ case "STOP_COMPARE":
9512
+ return {
9513
+ ...state,
9514
+ isComparing: false,
9515
+ compareFromId: null,
9516
+ compareToId: null
9517
+ };
9518
+ case "SET_AUTO_SAVE":
9519
+ return { ...state, autoSaveEnabled: action.enabled };
9520
+ case "SET_AUTO_SAVE_INTERVAL":
9521
+ return { ...state, autoSaveInterval: action.interval };
9522
+ default:
9523
+ return state;
9524
+ }
9525
+ }
9526
+ function countWords(text) {
9527
+ return text.trim().split(/\s+/).filter(Boolean).length;
9528
+ }
9529
+ function computeDiff(oldText, newText) {
9530
+ const changes = [];
9531
+ const oldLines = oldText.split("\n");
9532
+ const newLines = newText.split("\n");
9533
+ let i = 0;
9534
+ let j = 0;
9535
+ while (i < oldLines.length || j < newLines.length) {
9536
+ if (i >= oldLines.length) {
9537
+ changes.push({
9538
+ type: "addition",
9539
+ content: newLines[j],
9540
+ position: j
9541
+ });
9542
+ j++;
9543
+ } else if (j >= newLines.length) {
9544
+ changes.push({
9545
+ type: "deletion",
9546
+ content: oldLines[i],
9547
+ position: i
9548
+ });
9549
+ i++;
9550
+ } else if (oldLines[i] === newLines[j]) {
9551
+ i++;
9552
+ j++;
9553
+ } else {
9554
+ const oldLineInNew = newLines.indexOf(oldLines[i], j);
9555
+ const newLineInOld = oldLines.indexOf(newLines[j], i);
9556
+ if (oldLineInNew === -1 && newLineInOld === -1) {
9557
+ changes.push({
9558
+ type: "modification",
9559
+ content: `${oldLines[i]} -> ${newLines[j]}`,
9560
+ position: i
9561
+ });
9562
+ i++;
9563
+ j++;
9564
+ } else if (oldLineInNew === -1) {
9565
+ changes.push({
9566
+ type: "deletion",
9567
+ content: oldLines[i],
9568
+ position: i
9569
+ });
9570
+ i++;
9571
+ } else {
9572
+ changes.push({
9573
+ type: "addition",
9574
+ content: newLines[j],
9575
+ position: j
9576
+ });
9577
+ j++;
9578
+ }
9579
+ }
9580
+ }
9581
+ return changes;
9582
+ }
9583
+ var VersionHistoryContext = createContext3(null);
9584
+ function VersionHistoryProvider({
9585
+ children,
9586
+ config,
9587
+ initialVersions = [],
9588
+ onVersionsChange,
9589
+ getCurrentContent
9590
+ }) {
9591
+ const [state, dispatch] = useReducer3(versionHistoryReducer, {
9592
+ ...initialState3,
9593
+ versions: initialVersions,
9594
+ autoSaveEnabled: config?.autoSave ?? true,
9595
+ autoSaveInterval: config?.autoSaveInterval ?? 6e4
9596
+ });
9597
+ const subscribersRef = useRef9(/* @__PURE__ */ new Set());
9598
+ const autoSaveTimerRef = useRef9(null);
9599
+ const lastContentRef = useRef9("");
9600
+ const versionNumberRef = useRef9(initialVersions.length);
9601
+ useEffect11(() => {
9602
+ if (config?.onLoad) {
9603
+ config.onLoad().then((versions) => {
9604
+ dispatch({ type: "SET_VERSIONS", versions });
9605
+ versionNumberRef.current = versions.length;
9606
+ });
9607
+ }
9608
+ }, [config]);
9609
+ useEffect11(() => {
9610
+ onVersionsChange?.(state.versions);
9611
+ if (config?.onSave) {
9612
+ config.onSave(state.versions);
9613
+ }
9614
+ }, [state.versions, onVersionsChange, config]);
9615
+ useEffect11(() => {
9616
+ if (!state.autoSaveEnabled || !config || !getCurrentContent) return;
9617
+ autoSaveTimerRef.current = setInterval(() => {
9618
+ const { html, json } = getCurrentContent();
9619
+ if (html !== lastContentRef.current) {
9620
+ lastContentRef.current = html;
9621
+ createVersion(html, json, { isAutoSave: true });
9622
+ }
9623
+ }, state.autoSaveInterval);
9624
+ return () => {
9625
+ if (autoSaveTimerRef.current) {
9626
+ clearInterval(autoSaveTimerRef.current);
9627
+ }
9628
+ };
9629
+ }, [state.autoSaveEnabled, state.autoSaveInterval, config, getCurrentContent]);
9630
+ const emitEvent = useCallback9((event) => {
9631
+ subscribersRef.current.forEach((callback) => callback(event));
9632
+ }, []);
9633
+ const createVersion = useCallback9((content, jsonContent, options) => {
9634
+ if (!config?.currentUser) return null;
9635
+ const maxVersions = config.maxVersions ?? 100;
9636
+ let versions = state.versions;
9637
+ if (versions.length >= maxVersions) {
9638
+ const oldestUnpinned = [...versions].reverse().find((v) => !v.isPinned);
9639
+ if (oldestUnpinned) {
9640
+ versions = versions.filter((v) => v.id !== oldestUnpinned.id);
9641
+ }
9642
+ }
9643
+ versionNumberRef.current++;
9644
+ const textContent = content.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
9645
+ const version = {
9646
+ id: generateId2(),
9647
+ number: versionNumberRef.current,
9648
+ title: options?.title,
9649
+ content,
9650
+ jsonContent,
9651
+ textContent,
9652
+ author: config.currentUser,
9653
+ createdAt: Date.now(),
9654
+ wordCount: countWords(textContent),
9655
+ characterCount: textContent.length,
9656
+ isAutoSave: options?.isAutoSave ?? false,
9657
+ isPinned: false
9658
+ };
9659
+ dispatch({ type: "ADD_VERSION", version });
9660
+ emitEvent({ type: "version-created", version });
9661
+ return version;
9662
+ }, [config, state.versions, emitEvent]);
9663
+ const deleteVersion = useCallback9((versionId) => {
9664
+ dispatch({ type: "DELETE_VERSION", versionId });
9665
+ emitEvent({ type: "version-deleted", versionId });
9666
+ }, [emitEvent]);
9667
+ const restoreVersion = useCallback9((versionId) => {
9668
+ const version = state.versions.find((v) => v.id === versionId);
9669
+ if (!version) return null;
9670
+ config?.onRestore?.(version);
9671
+ emitEvent({ type: "version-restored", versionId });
9672
+ return version.content;
9673
+ }, [state.versions, config, emitEvent]);
9674
+ const pinVersion = useCallback9((versionId) => {
9675
+ dispatch({ type: "PIN_VERSION", versionId });
9676
+ emitEvent({ type: "version-pinned", versionId });
9677
+ }, [emitEvent]);
9678
+ const unpinVersion = useCallback9((versionId) => {
9679
+ dispatch({ type: "UNPIN_VERSION", versionId });
9680
+ emitEvent({ type: "version-unpinned", versionId });
9681
+ }, [emitEvent]);
9682
+ const renameVersion = useCallback9((versionId, title) => {
9683
+ dispatch({ type: "RENAME_VERSION", versionId, title });
9684
+ emitEvent({ type: "version-renamed", versionId, title });
9685
+ }, [emitEvent]);
9686
+ const viewVersion = useCallback9((versionId) => {
9687
+ dispatch({ type: "SET_VIEWING_VERSION", versionId });
9688
+ }, []);
9689
+ const getVersionContent = useCallback9((versionId) => {
9690
+ const version = state.versions.find((v) => v.id === versionId);
9691
+ return version?.content ?? null;
9692
+ }, [state.versions]);
9693
+ const compareVersions = useCallback9((fromId, toId) => {
9694
+ const fromVersion = state.versions.find((v) => v.id === fromId);
9695
+ const toVersion = state.versions.find((v) => v.id === toId);
9696
+ if (!fromVersion || !toVersion) return null;
9697
+ const changes = computeDiff(fromVersion.textContent, toVersion.textContent);
9698
+ return {
9699
+ fromVersionId: fromId,
9700
+ toVersionId: toId,
9701
+ changes,
9702
+ stats: {
9703
+ additions: changes.filter((c) => c.type === "addition").length,
9704
+ deletions: changes.filter((c) => c.type === "deletion").length,
9705
+ modifications: changes.filter((c) => c.type === "modification").length
9706
+ }
9707
+ };
9708
+ }, [state.versions]);
9709
+ const startCompare = useCallback9((fromId, toId) => {
9710
+ dispatch({ type: "START_COMPARE", fromId, toId });
9711
+ }, []);
9712
+ const stopCompare = useCallback9(() => {
9713
+ dispatch({ type: "STOP_COMPARE" });
9714
+ }, []);
9715
+ const togglePanel = useCallback9((isOpen) => {
9716
+ dispatch({ type: "TOGGLE_PANEL", isOpen });
9717
+ }, []);
9718
+ const setAutoSave = useCallback9((enabled) => {
9719
+ dispatch({ type: "SET_AUTO_SAVE", enabled });
9720
+ emitEvent({ type: "auto-save-toggled", enabled });
9721
+ }, [emitEvent]);
9722
+ const getVersion = useCallback9((versionId) => {
9723
+ return state.versions.find((v) => v.id === versionId);
9724
+ }, [state.versions]);
9725
+ const getLatestVersion = useCallback9(() => {
9726
+ return state.versions[0];
9727
+ }, [state.versions]);
9728
+ const getPinnedVersions = useCallback9(() => {
9729
+ return state.versions.filter((v) => v.isPinned);
9730
+ }, [state.versions]);
9731
+ const subscribe = useCallback9((callback) => {
9732
+ subscribersRef.current.add(callback);
9733
+ return () => {
9734
+ subscribersRef.current.delete(callback);
9735
+ };
9736
+ }, []);
9737
+ const value = {
9738
+ state,
9739
+ config: config ?? null,
9740
+ createVersion,
9741
+ deleteVersion,
9742
+ restoreVersion,
9743
+ pinVersion,
9744
+ unpinVersion,
9745
+ renameVersion,
9746
+ viewVersion,
9747
+ getVersionContent,
9748
+ compareVersions,
9749
+ startCompare,
9750
+ stopCompare,
9751
+ togglePanel,
9752
+ setAutoSave,
9753
+ getVersion,
9754
+ getLatestVersion,
9755
+ getPinnedVersions,
9756
+ subscribe,
9757
+ isEnabled: !!config
9758
+ };
9759
+ return /* @__PURE__ */ jsx14(VersionHistoryContext.Provider, { value, children });
9760
+ }
9761
+ function useVersionHistory() {
9762
+ const context = useContext3(VersionHistoryContext);
9763
+ if (!context) {
9764
+ throw new Error("useVersionHistory must be used within a VersionHistoryProvider");
9765
+ }
9766
+ return context;
9767
+ }
9768
+ function useVersionHistoryOptional() {
9769
+ return useContext3(VersionHistoryContext);
9770
+ }
9771
+
9772
+ // src/history/VersionHistoryPanel.tsx
9773
+ import { useState as useState9 } from "react";
9774
+ import { Fragment as Fragment4, jsx as jsx15, jsxs as jsxs12 } from "react/jsx-runtime";
9775
+ function formatDate(timestamp) {
9776
+ const date = new Date(timestamp);
9777
+ const now = /* @__PURE__ */ new Date();
9778
+ const isToday = date.toDateString() === now.toDateString();
9779
+ if (isToday) {
9780
+ return `Today at ${date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}`;
9781
+ }
9782
+ const yesterday = new Date(now);
9783
+ yesterday.setDate(yesterday.getDate() - 1);
9784
+ if (date.toDateString() === yesterday.toDateString()) {
9785
+ return `Yesterday at ${date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}`;
9786
+ }
9787
+ return date.toLocaleDateString([], {
9788
+ month: "short",
9789
+ day: "numeric",
9790
+ hour: "2-digit",
9791
+ minute: "2-digit"
9792
+ });
9793
+ }
9794
+ function VersionItem({
9795
+ version,
9796
+ isViewing,
9797
+ isCompareFrom,
9798
+ isCompareTo,
9799
+ onView,
9800
+ onRestore,
9801
+ onPin,
9802
+ onRename,
9803
+ onDelete,
9804
+ onCompareSelect
9805
+ }) {
9806
+ const [isRenaming, setIsRenaming] = useState9(false);
9807
+ const [newTitle, setNewTitle] = useState9(version.title || "");
9808
+ const handleRename = () => {
9809
+ onRename(newTitle);
9810
+ setIsRenaming(false);
9811
+ };
9812
+ return /* @__PURE__ */ jsxs12(
9813
+ "div",
9814
+ {
9815
+ className: `rte-version-item ${isViewing ? "rte-version-viewing" : ""} ${isCompareFrom ? "rte-version-compare-from" : ""} ${isCompareTo ? "rte-version-compare-to" : ""}`,
9816
+ children: [
9817
+ /* @__PURE__ */ jsxs12("div", { className: "rte-version-header", onClick: onView, children: [
9818
+ /* @__PURE__ */ jsxs12("div", { className: "rte-version-info", children: [
9819
+ isRenaming ? /* @__PURE__ */ jsx15(
9820
+ "input",
9821
+ {
9822
+ type: "text",
9823
+ value: newTitle,
9824
+ onChange: (e) => setNewTitle(e.target.value),
9825
+ onBlur: handleRename,
9826
+ onKeyDown: (e) => e.key === "Enter" && handleRename(),
9827
+ className: "rte-version-rename-input",
9828
+ onClick: (e) => e.stopPropagation(),
9829
+ autoFocus: true
9830
+ }
9831
+ ) : /* @__PURE__ */ jsxs12("span", { className: "rte-version-title", children: [
9832
+ version.title || `Version ${version.number}`,
9833
+ version.isPinned && /* @__PURE__ */ jsx15("span", { className: "rte-version-pin-icon", children: "\u{1F4CC}" }),
9834
+ version.isAutoSave && /* @__PURE__ */ jsx15("span", { className: "rte-version-auto-badge", children: "Auto" })
9835
+ ] }),
9836
+ /* @__PURE__ */ jsx15("span", { className: "rte-version-time", children: formatDate(version.createdAt) })
9837
+ ] }),
9838
+ /* @__PURE__ */ jsxs12("div", { className: "rte-version-author", children: [
9839
+ version.author.avatar ? /* @__PURE__ */ jsx15("img", { src: version.author.avatar, alt: version.author.name, className: "rte-version-avatar" }) : /* @__PURE__ */ jsx15("div", { className: "rte-version-avatar-placeholder", children: version.author.name.charAt(0).toUpperCase() }),
9840
+ /* @__PURE__ */ jsx15("span", { children: version.author.name })
9841
+ ] })
9842
+ ] }),
9843
+ /* @__PURE__ */ jsxs12("div", { className: "rte-version-stats", children: [
9844
+ /* @__PURE__ */ jsxs12("span", { children: [
9845
+ version.wordCount,
9846
+ " words"
9847
+ ] }),
9848
+ /* @__PURE__ */ jsxs12("span", { children: [
9849
+ version.characterCount,
9850
+ " chars"
9851
+ ] })
9852
+ ] }),
9853
+ /* @__PURE__ */ jsxs12("div", { className: "rte-version-actions", children: [
9854
+ /* @__PURE__ */ jsx15("button", { onClick: onRestore, className: "rte-version-btn", title: "Restore this version", children: "\u21BA Restore" }),
9855
+ /* @__PURE__ */ jsx15("button", { onClick: onCompareSelect, className: "rte-version-btn", title: "Compare with another version", children: "\u21C4 Compare" }),
9856
+ /* @__PURE__ */ jsx15("button", { onClick: onPin, className: "rte-version-btn", title: version.isPinned ? "Unpin" : "Pin", children: version.isPinned ? "\u{1F4CC} Unpin" : "\u{1F4CC} Pin" }),
9857
+ /* @__PURE__ */ jsx15(
9858
+ "button",
9859
+ {
9860
+ onClick: () => setIsRenaming(true),
9861
+ className: "rte-version-btn",
9862
+ title: "Rename version",
9863
+ children: "\u270F\uFE0F"
9864
+ }
9865
+ ),
9866
+ !version.isPinned && /* @__PURE__ */ jsx15("button", { onClick: onDelete, className: "rte-version-btn rte-version-btn-delete", title: "Delete", children: "\u{1F5D1}\uFE0F" })
9867
+ ] })
9868
+ ]
9869
+ }
9870
+ );
9871
+ }
9872
+ function ComparisonView({
9873
+ fromVersion,
9874
+ toVersion,
9875
+ onClose
9876
+ }) {
9877
+ const versionHistory = useVersionHistoryOptional();
9878
+ if (!versionHistory) return null;
9879
+ const comparison = versionHistory.compareVersions(fromVersion.id, toVersion.id);
9880
+ return /* @__PURE__ */ jsxs12("div", { className: "rte-version-comparison", children: [
9881
+ /* @__PURE__ */ jsxs12("div", { className: "rte-version-comparison-header", children: [
9882
+ /* @__PURE__ */ jsx15("h4", { children: "Comparing Versions" }),
9883
+ /* @__PURE__ */ jsx15("button", { onClick: onClose, className: "rte-version-comparison-close", children: "\xD7" })
9884
+ ] }),
9885
+ /* @__PURE__ */ jsxs12("div", { className: "rte-version-comparison-info", children: [
9886
+ /* @__PURE__ */ jsxs12("div", { className: "rte-version-comparison-from", children: [
9887
+ /* @__PURE__ */ jsx15("span", { className: "rte-version-comparison-label", children: "From:" }),
9888
+ fromVersion.title || `Version ${fromVersion.number}`
9889
+ ] }),
9890
+ /* @__PURE__ */ jsx15("span", { className: "rte-version-comparison-arrow", children: "\u2192" }),
9891
+ /* @__PURE__ */ jsxs12("div", { className: "rte-version-comparison-to", children: [
9892
+ /* @__PURE__ */ jsx15("span", { className: "rte-version-comparison-label", children: "To:" }),
9893
+ toVersion.title || `Version ${toVersion.number}`
9894
+ ] })
9895
+ ] }),
9896
+ comparison && /* @__PURE__ */ jsxs12("div", { className: "rte-version-comparison-stats", children: [
9897
+ /* @__PURE__ */ jsxs12("span", { className: "rte-comparison-stat rte-comparison-additions", children: [
9898
+ "+",
9899
+ comparison.stats.additions,
9900
+ " additions"
9901
+ ] }),
9902
+ /* @__PURE__ */ jsxs12("span", { className: "rte-comparison-stat rte-comparison-deletions", children: [
9903
+ "-",
9904
+ comparison.stats.deletions,
9905
+ " deletions"
9906
+ ] }),
9907
+ /* @__PURE__ */ jsxs12("span", { className: "rte-comparison-stat rte-comparison-modifications", children: [
9908
+ "~",
9909
+ comparison.stats.modifications,
9910
+ " modifications"
9911
+ ] })
9912
+ ] }),
9913
+ /* @__PURE__ */ jsx15("div", { className: "rte-version-comparison-diff", children: comparison?.changes.map((change, index) => /* @__PURE__ */ jsxs12("div", { className: `rte-diff-line rte-diff-${change.type}`, children: [
9914
+ /* @__PURE__ */ jsx15("span", { className: "rte-diff-indicator", children: change.type === "addition" ? "+" : change.type === "deletion" ? "-" : "~" }),
9915
+ /* @__PURE__ */ jsx15("span", { className: "rte-diff-content", children: change.content })
9916
+ ] }, index)) })
9917
+ ] });
9918
+ }
9919
+ function VersionHistoryPanel({ position = "right", className = "" }) {
9920
+ const versionHistory = useVersionHistoryOptional();
9921
+ const [compareFromId, setCompareFromId] = useState9(null);
9922
+ if (!versionHistory?.isEnabled || !versionHistory.state.isPanelOpen) {
9923
+ return null;
9924
+ }
9925
+ const {
9926
+ state,
9927
+ togglePanel,
9928
+ viewVersion,
9929
+ restoreVersion,
9930
+ pinVersion,
9931
+ unpinVersion,
9932
+ renameVersion,
9933
+ deleteVersion,
9934
+ setAutoSave,
9935
+ getVersion,
9936
+ createVersion,
9937
+ stopCompare
9938
+ } = versionHistory;
9939
+ const handleRestore = (versionId) => {
9940
+ const content = restoreVersion(versionId);
9941
+ if (content) {
9942
+ viewVersion(null);
9943
+ }
9944
+ };
9945
+ const handleCompareSelect = (versionId) => {
9946
+ if (!compareFromId) {
9947
+ setCompareFromId(versionId);
9948
+ } else {
9949
+ versionHistory.startCompare(compareFromId, versionId);
9950
+ setCompareFromId(null);
9951
+ }
9952
+ };
9953
+ const handleCancelCompare = () => {
9954
+ setCompareFromId(null);
9955
+ stopCompare();
9956
+ };
9957
+ const compareFromVersion = state.compareFromId ? getVersion(state.compareFromId) : void 0;
9958
+ const compareToVersion = state.compareToId ? getVersion(state.compareToId) : void 0;
9959
+ return /* @__PURE__ */ jsxs12("div", { className: `rte-version-panel rte-version-panel-${position} ${className}`, children: [
9960
+ /* @__PURE__ */ jsxs12("div", { className: "rte-version-panel-header", children: [
9961
+ /* @__PURE__ */ jsx15("h3", { children: "Version History" }),
9962
+ /* @__PURE__ */ jsx15("button", { onClick: () => togglePanel(false), className: "rte-version-close-btn", children: "\xD7" })
9963
+ ] }),
9964
+ /* @__PURE__ */ jsxs12("div", { className: "rte-version-autosave", children: [
9965
+ /* @__PURE__ */ jsxs12("label", { className: "rte-version-autosave-label", children: [
9966
+ /* @__PURE__ */ jsx15(
9967
+ "input",
9968
+ {
9969
+ type: "checkbox",
9970
+ checked: state.autoSaveEnabled,
9971
+ onChange: (e) => setAutoSave(e.target.checked)
9972
+ }
9973
+ ),
9974
+ "Auto-save versions"
9975
+ ] }),
9976
+ /* @__PURE__ */ jsx15(
9977
+ "button",
9978
+ {
9979
+ onClick: () => createVersion("", void 0, { isAutoSave: false }),
9980
+ className: "rte-version-save-btn",
9981
+ children: "Save Now"
9982
+ }
9983
+ )
9984
+ ] }),
9985
+ compareFromId && !state.isComparing && /* @__PURE__ */ jsxs12("div", { className: "rte-version-compare-mode", children: [
9986
+ /* @__PURE__ */ jsx15("span", { children: "Select another version to compare" }),
9987
+ /* @__PURE__ */ jsx15("button", { onClick: handleCancelCompare, children: "Cancel" })
9988
+ ] }),
9989
+ state.isComparing && compareFromVersion && compareToVersion && /* @__PURE__ */ jsx15(
9990
+ ComparisonView,
9991
+ {
9992
+ fromVersion: compareFromVersion,
9993
+ toVersion: compareToVersion,
9994
+ onClose: handleCancelCompare
9995
+ }
9996
+ ),
9997
+ !state.isComparing && /* @__PURE__ */ jsx15("div", { className: "rte-version-list", children: state.versions.length === 0 ? /* @__PURE__ */ jsx15("div", { className: "rte-version-empty", children: "No versions saved yet. Changes will be auto-saved periodically." }) : /* @__PURE__ */ jsxs12(Fragment4, { children: [
9998
+ state.versions.some((v) => v.isPinned) && /* @__PURE__ */ jsxs12("div", { className: "rte-version-section", children: [
9999
+ /* @__PURE__ */ jsx15("div", { className: "rte-version-section-title", children: "\u{1F4CC} Pinned" }),
10000
+ state.versions.filter((v) => v.isPinned).map((version) => /* @__PURE__ */ jsx15(
10001
+ VersionItem,
10002
+ {
10003
+ version,
10004
+ isViewing: state.viewingVersionId === version.id,
10005
+ isCompareFrom: compareFromId === version.id,
10006
+ isCompareTo: false,
10007
+ onView: () => viewVersion(version.id),
10008
+ onRestore: () => handleRestore(version.id),
10009
+ onPin: () => unpinVersion(version.id),
10010
+ onRename: (title) => renameVersion(version.id, title),
10011
+ onDelete: () => deleteVersion(version.id),
10012
+ onCompareSelect: () => handleCompareSelect(version.id)
10013
+ },
10014
+ version.id
10015
+ ))
10016
+ ] }),
10017
+ /* @__PURE__ */ jsxs12("div", { className: "rte-version-section", children: [
10018
+ /* @__PURE__ */ jsx15("div", { className: "rte-version-section-title", children: "Recent" }),
10019
+ state.versions.filter((v) => !v.isPinned).map((version) => /* @__PURE__ */ jsx15(
10020
+ VersionItem,
10021
+ {
10022
+ version,
10023
+ isViewing: state.viewingVersionId === version.id,
10024
+ isCompareFrom: compareFromId === version.id,
10025
+ isCompareTo: false,
10026
+ onView: () => viewVersion(version.id),
10027
+ onRestore: () => handleRestore(version.id),
10028
+ onPin: () => pinVersion(version.id),
10029
+ onRename: (title) => renameVersion(version.id, title),
10030
+ onDelete: () => deleteVersion(version.id),
10031
+ onCompareSelect: () => handleCompareSelect(version.id)
10032
+ },
10033
+ version.id
10034
+ ))
10035
+ ] })
10036
+ ] }) }),
10037
+ state.viewingVersionId && /* @__PURE__ */ jsxs12("div", { className: "rte-version-viewing-indicator", children: [
10038
+ /* @__PURE__ */ jsxs12("span", { children: [
10039
+ "Viewing: ",
10040
+ getVersion(state.viewingVersionId)?.title || `Version ${getVersion(state.viewingVersionId)?.number}`
10041
+ ] }),
10042
+ /* @__PURE__ */ jsx15("button", { onClick: () => viewVersion(null), className: "rte-version-back-btn", children: "\u2190 Back to current" })
10043
+ ] })
10044
+ ] });
10045
+ }
8677
10046
  export {
10047
+ CollaborationProvider,
10048
+ CommentsPanel,
10049
+ CommentsProvider,
8678
10050
  DEFAULT_FEATURES,
10051
+ DEFAULT_REACTION_EMOJIS,
8679
10052
  EMOJI_CATEGORIES,
8680
10053
  EditorRegistry,
8681
10054
  Emoji,
@@ -8683,12 +10056,15 @@ export {
8683
10056
  Fullscreen,
8684
10057
  Indent,
8685
10058
  LineHeight,
10059
+ PresenceIndicator,
8686
10060
  Print,
8687
10061
  RichTextEditor,
8688
10062
  TipTapAdapter,
8689
10063
  TipTapEditorComponent,
8690
10064
  TipTapToolbar,
8691
10065
  UnifiedEditor,
10066
+ VersionHistoryPanel,
10067
+ VersionHistoryProvider,
8692
10068
  Video,
8693
10069
  blogToolbar,
8694
10070
  codeToolbar,
@@ -8710,6 +10086,12 @@ export {
8710
10086
  registerAdapter,
8711
10087
  simpleToolbar,
8712
10088
  toolbarPresets,
8713
- unregisterAdapter
10089
+ unregisterAdapter,
10090
+ useCollaboration,
10091
+ useCollaborationOptional,
10092
+ useComments,
10093
+ useCommentsOptional,
10094
+ useVersionHistory,
10095
+ useVersionHistoryOptional
8714
10096
  };
8715
10097
  //# sourceMappingURL=index.mjs.map