@tangle-network/sandbox-ui 0.3.10 → 0.3.12

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.
@@ -9756,6 +9756,7 @@ function EditorProvider({
9756
9756
  websocketUrl,
9757
9757
  documentName,
9758
9758
  token,
9759
+ tokenExpiresAt,
9759
9760
  user,
9760
9761
  autoConnect = true,
9761
9762
  autoReconnect = true,
@@ -9763,6 +9764,7 @@ function EditorProvider({
9763
9764
  onConnectionChange,
9764
9765
  onSync,
9765
9766
  onAuthError,
9767
+ onRefreshToken,
9766
9768
  children
9767
9769
  }) {
9768
9770
  const [connectionState, setConnectionState] = useState("disconnected");
@@ -9771,6 +9773,12 @@ function EditorProvider({
9771
9773
  const docRef = useRef(null);
9772
9774
  const providerRef = useRef(null);
9773
9775
  const reconnectAttemptsRef = useRef(0);
9776
+ const tokenRef = useRef(token);
9777
+ const tokenExpiryRef = useRef(tokenExpiresAt);
9778
+ const refreshPromiseRef = useRef(null);
9779
+ const refreshTimerRef = useRef(null);
9780
+ tokenRef.current = token;
9781
+ tokenExpiryRef.current = tokenExpiresAt;
9774
9782
  if (!docRef.current) {
9775
9783
  docRef.current = new Y3.Doc();
9776
9784
  }
@@ -9804,6 +9812,52 @@ function EditorProvider({
9804
9812
  },
9805
9813
  []
9806
9814
  );
9815
+ const clearRefreshTimer = useCallback(() => {
9816
+ if (refreshTimerRef.current != null) {
9817
+ window.clearTimeout(refreshTimerRef.current);
9818
+ refreshTimerRef.current = null;
9819
+ }
9820
+ }, []);
9821
+ const refreshToken = useCallback(async () => {
9822
+ if (!onRefreshToken) {
9823
+ return null;
9824
+ }
9825
+ if (refreshPromiseRef.current) {
9826
+ return refreshPromiseRef.current;
9827
+ }
9828
+ const refreshPromise = (async () => {
9829
+ const next = await onRefreshToken();
9830
+ const resolvedToken = typeof next === "string" ? next : next.token;
9831
+ const resolvedExpiry = typeof next === "string" ? void 0 : next.expiresAt;
9832
+ tokenRef.current = resolvedToken;
9833
+ tokenExpiryRef.current = resolvedExpiry;
9834
+ return resolvedToken;
9835
+ })().catch((error) => {
9836
+ onAuthError?.(
9837
+ error instanceof Error ? error : new Error(String(error))
9838
+ );
9839
+ return null;
9840
+ }).finally(() => {
9841
+ refreshPromiseRef.current = null;
9842
+ });
9843
+ refreshPromiseRef.current = refreshPromise;
9844
+ return refreshPromise;
9845
+ }, [onAuthError, onRefreshToken]);
9846
+ const scheduleTokenRefresh = useCallback(() => {
9847
+ clearRefreshTimer();
9848
+ if (!tokenExpiryRef.current || !onRefreshToken || typeof window === "undefined") {
9849
+ return;
9850
+ }
9851
+ const refreshAtMs = tokenExpiryRef.current * 1e3 - 6e4;
9852
+ const delay = refreshAtMs - Date.now();
9853
+ if (delay <= 0) {
9854
+ void refreshToken();
9855
+ return;
9856
+ }
9857
+ refreshTimerRef.current = window.setTimeout(() => {
9858
+ void refreshToken();
9859
+ }, delay);
9860
+ }, [clearRefreshTimer, onRefreshToken, refreshToken]);
9807
9861
  const connect = useCallback(() => {
9808
9862
  if (providerRef.current) {
9809
9863
  providerRef.current.connect();
@@ -9814,12 +9868,13 @@ function EditorProvider({
9814
9868
  url: websocketUrl,
9815
9869
  name: documentName,
9816
9870
  document: doc3,
9817
- token,
9871
+ token: async () => tokenRef.current,
9818
9872
  // @ts-expect-error -- connect is valid at runtime but missing from type defs
9819
9873
  connect: true,
9820
9874
  onConnect: () => {
9821
9875
  reconnectAttemptsRef.current = 0;
9822
9876
  updateConnectionState("connected");
9877
+ scheduleTokenRefresh();
9823
9878
  },
9824
9879
  onSynced: () => {
9825
9880
  setIsSynced(true);
@@ -9829,6 +9884,7 @@ function EditorProvider({
9829
9884
  onDisconnect: () => {
9830
9885
  updateConnectionState("disconnected");
9831
9886
  setIsSynced(false);
9887
+ clearRefreshTimer();
9832
9888
  if (autoReconnect && reconnectAttemptsRef.current < maxReconnectAttempts) {
9833
9889
  reconnectAttemptsRef.current += 1;
9834
9890
  const delay = Math.min(
@@ -9844,8 +9900,19 @@ function EditorProvider({
9844
9900
  },
9845
9901
  onAuthenticationFailed: ({ reason }) => {
9846
9902
  const error = new Error(reason ?? "Authentication failed");
9847
- onAuthError?.(error);
9848
9903
  updateConnectionState("disconnected");
9904
+ clearRefreshTimer();
9905
+ if (onRefreshToken) {
9906
+ void refreshToken().then((nextToken) => {
9907
+ if (nextToken && providerRef.current) {
9908
+ providerRef.current.connect();
9909
+ return;
9910
+ }
9911
+ onAuthError?.(error);
9912
+ });
9913
+ return;
9914
+ }
9915
+ onAuthError?.(error);
9849
9916
  },
9850
9917
  onAwarenessUpdate: () => {
9851
9918
  updateCollaborators(provider.awareness);
@@ -9861,16 +9928,19 @@ function EditorProvider({
9861
9928
  websocketUrl,
9862
9929
  documentName,
9863
9930
  doc3,
9864
- token,
9865
9931
  user.name,
9866
9932
  user.userId,
9867
9933
  userColor,
9868
9934
  autoReconnect,
9869
9935
  maxReconnectAttempts,
9936
+ clearRefreshTimer,
9870
9937
  updateConnectionState,
9871
9938
  updateCollaborators,
9872
9939
  onSync,
9873
- onAuthError
9940
+ onAuthError,
9941
+ onRefreshToken,
9942
+ refreshToken,
9943
+ scheduleTokenRefresh
9874
9944
  ]);
9875
9945
  const disconnect = useCallback(() => {
9876
9946
  if (providerRef.current) {
@@ -9883,12 +9953,13 @@ function EditorProvider({
9883
9953
  connect();
9884
9954
  }
9885
9955
  return () => {
9956
+ clearRefreshTimer();
9886
9957
  if (providerRef.current) {
9887
9958
  providerRef.current.destroy();
9888
9959
  providerRef.current = null;
9889
9960
  }
9890
9961
  };
9891
- }, [autoConnect, connect]);
9962
+ }, [autoConnect, clearRefreshTimer, connect]);
9892
9963
  const contextValue = useMemo(
9893
9964
  () => ({
9894
9965
  doc: doc3,
@@ -10682,6 +10753,21 @@ function connectionLabel(state) {
10682
10753
  return "Offline";
10683
10754
  }
10684
10755
  }
10756
+ function connectionDescription(state, collaborators, readOnly) {
10757
+ if (readOnly) {
10758
+ return state === "disconnected" ? "Live access is paused. You can keep reading while the editor reconnects." : "You are viewing the live document in read-only mode.";
10759
+ }
10760
+ switch (state) {
10761
+ case "synced":
10762
+ return collaborators > 0 ? `You and ${collaborators} collaborator${collaborators === 1 ? "" : "s"} are editing the same document.` : "You are editing the live document. Changes sync automatically.";
10763
+ case "connected":
10764
+ case "connecting":
10765
+ return "Connecting the live document. Local edits stay in place while sync catches up.";
10766
+ case "disconnected":
10767
+ default:
10768
+ return "Live updates are paused. You can keep editing and reconnect when the transport is healthy again.";
10769
+ }
10770
+ }
10685
10771
  function CollaborativeDocumentSurface({
10686
10772
  markdown,
10687
10773
  placeholder,
@@ -10693,26 +10779,30 @@ function CollaborativeDocumentSurface({
10693
10779
  const { state } = useEditorConnection();
10694
10780
  const { collaborators } = useCollaborators();
10695
10781
  const initialContent = useMemo4(() => markdownToHtml(markdown), [markdown]);
10782
+ const collaboratorCount = collaborators.length + 1;
10696
10783
  return /* @__PURE__ */ jsxs3("div", { className: cn("flex h-full min-h-0 flex-col gap-3", className), children: [
10697
10784
  /* @__PURE__ */ jsxs3("div", { className: "flex flex-wrap items-center justify-between gap-3 rounded-[var(--radius-lg)] border border-[var(--border-subtle)] bg-[var(--bg-card)]/80 px-3 py-2", children: [
10698
- /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-2 text-xs text-[var(--text-muted)]", children: [
10699
- /* @__PURE__ */ jsxs3(
10700
- "span",
10701
- {
10702
- className: cn(
10703
- "inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 font-medium",
10704
- connectionTone(state)
10705
- ),
10706
- children: [
10707
- state === "disconnected" ? /* @__PURE__ */ jsx5(WifiOff, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ jsx5(Wifi, { className: "h-3.5 w-3.5" }),
10708
- connectionLabel(state)
10709
- ]
10710
- }
10711
- ),
10712
- /* @__PURE__ */ jsxs3("span", { className: "inline-flex items-center gap-1.5 rounded-full border border-[var(--border-subtle)] bg-[var(--bg-input)] px-2.5 py-1", children: [
10713
- /* @__PURE__ */ jsx5(Users, { className: "h-3.5 w-3.5" }),
10714
- collaborators.length === 0 ? "Solo editing" : `${collaborators.length + 1} active`
10715
- ] })
10785
+ /* @__PURE__ */ jsxs3("div", { className: "min-w-0 space-y-2", children: [
10786
+ /* @__PURE__ */ jsxs3("div", { className: "flex flex-wrap items-center gap-2 text-xs text-[var(--text-muted)]", children: [
10787
+ /* @__PURE__ */ jsxs3(
10788
+ "span",
10789
+ {
10790
+ className: cn(
10791
+ "inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 font-medium",
10792
+ connectionTone(state)
10793
+ ),
10794
+ children: [
10795
+ state === "disconnected" ? /* @__PURE__ */ jsx5(WifiOff, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ jsx5(Wifi, { className: "h-3.5 w-3.5" }),
10796
+ connectionLabel(state)
10797
+ ]
10798
+ }
10799
+ ),
10800
+ /* @__PURE__ */ jsxs3("span", { className: "inline-flex items-center gap-1.5 rounded-full border border-[var(--border-subtle)] bg-[var(--bg-input)] px-2.5 py-1", children: [
10801
+ /* @__PURE__ */ jsx5(Users, { className: "h-3.5 w-3.5" }),
10802
+ collaborators.length === 0 ? "Solo editing" : `${collaboratorCount} active`
10803
+ ] })
10804
+ ] }),
10805
+ /* @__PURE__ */ jsx5("p", { className: "text-xs text-[var(--text-muted)]", children: connectionDescription(state, collaborators.length, readOnly) })
10716
10806
  ] }),
10717
10807
  /* @__PURE__ */ jsx5(CollaboratorsList, { collaborators })
10718
10808
  ] }),
@@ -10763,6 +10853,7 @@ function DocumentEditorPane({
10763
10853
  const activeMode = mode ?? uncontrolledMode;
10764
10854
  const isCollaborative = backend === "collaborative" && Boolean(collaboration);
10765
10855
  const isDirty = normalizeMarkdown(draft) !== normalizeMarkdown(markdown);
10856
+ const saveStateLabel = readOnly ? "Read only" : isCollaborative ? isDirty ? "Snapshot pending" : "Live document current" : isDirty ? "Unsaved changes" : "Saved";
10766
10857
  useEffect5(() => {
10767
10858
  setDraft(markdown);
10768
10859
  }, [markdown]);
@@ -10814,8 +10905,8 @@ function DocumentEditorPane({
10814
10905
  ),
10815
10906
  /* @__PURE__ */ jsxs3("div", { className: "flex flex-wrap items-center gap-2 text-xs text-[var(--text-muted)]", children: [
10816
10907
  toolbar,
10817
- /* @__PURE__ */ jsx5("span", { className: "rounded-full border border-[var(--border-subtle)] bg-[var(--bg-card)] px-2.5 py-1 font-medium", children: isCollaborative ? "Collaborative" : "Local draft" }),
10818
- isDirty && /* @__PURE__ */ jsx5("span", { children: "Unsaved changes" }),
10908
+ /* @__PURE__ */ jsx5("span", { className: "rounded-full border border-[var(--border-subtle)] bg-[var(--bg-card)] px-2.5 py-1 font-medium", children: isCollaborative ? "Live document" : "Local draft" }),
10909
+ /* @__PURE__ */ jsx5("span", { className: "rounded-full border border-[var(--border-subtle)] bg-[var(--bg-input)] px-2.5 py-1", children: saveStateLabel }),
10819
10910
  onSave && !readOnly && /* @__PURE__ */ jsxs3(
10820
10911
  "button",
10821
10912
  {
@@ -10864,7 +10955,7 @@ function DocumentEditorPane({
10864
10955
  className: editorClassName,
10865
10956
  onChange: handleChange
10866
10957
  }
10867
- ) }) : localEditor;
10958
+ ) }, collaboration.documentName) : localEditor;
10868
10959
  return /* @__PURE__ */ jsx5(
10869
10960
  Tabs,
10870
10961
  {
@@ -484,7 +484,7 @@ import { lazy, Suspense } from "react";
484
484
  import { Download as Download2, X as X3 } from "lucide-react";
485
485
  import { Fragment, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
486
486
  var LazyDocumentEditorPane = lazy(async () => {
487
- const module = await import("./document-editor-pane-GRIQOJHB.js");
487
+ const module = await import("./document-editor-pane-AVKKXSLG.js");
488
488
  return { default: module.DocumentEditorPane };
489
489
  });
490
490
  function FileArtifactPane({
@@ -18,7 +18,7 @@ import {
18
18
  FileArtifactPane,
19
19
  FileTree,
20
20
  filterFileTree
21
- } from "./chunk-ZYGWTIWO.js";
21
+ } from "./chunk-IAIJUFM6.js";
22
22
  import {
23
23
  ArtifactPane
24
24
  } from "./chunk-W4LM3QYZ.js";
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import {
3
3
  DocumentEditorPane
4
- } from "./chunk-MGCVTFKB.js";
4
+ } from "./chunk-4HT5J6CE.js";
5
5
  import "./chunk-Q56BYXQF.js";
6
6
  import "./chunk-W4LM3QYZ.js";
7
7
  import "./chunk-LTFK464G.js";
@@ -46,6 +46,10 @@ interface EditorUser {
46
46
  color?: string;
47
47
  userId?: string;
48
48
  }
49
+ interface EditorTokenRefreshResult {
50
+ token: string;
51
+ expiresAt?: number;
52
+ }
49
53
  /**
50
54
  * Props for EditorProvider.
51
55
  */
@@ -56,6 +60,8 @@ interface EditorProviderProps {
56
60
  documentName: string;
57
61
  /** JWT token for authentication */
58
62
  token: string;
63
+ /** Unix timestamp (seconds) when the current token expires */
64
+ tokenExpiresAt?: number;
59
65
  /** Current user information for awareness */
60
66
  user: EditorUser;
61
67
  /** Auto-connect on mount (default: true) */
@@ -70,6 +76,8 @@ interface EditorProviderProps {
70
76
  onSync?: () => void;
71
77
  /** Callback on authentication error */
72
78
  onAuthError?: (error: Error) => void;
79
+ /** Optional token refresh callback used before reconnect/auth retry */
80
+ onRefreshToken?: () => Promise<EditorTokenRefreshResult | string>;
73
81
  /** Children components */
74
82
  children: ReactNode;
75
83
  }
@@ -77,7 +85,7 @@ interface EditorProviderProps {
77
85
  * EditorProvider wraps children with Hocuspocus collaboration context.
78
86
  * Manages WebSocket connection, Y.Doc, and awareness state.
79
87
  */
80
- declare function EditorProvider({ websocketUrl, documentName, token, user, autoConnect, autoReconnect, maxReconnectAttempts, onConnectionChange, onSync, onAuthError, children, }: EditorProviderProps): react_jsx_runtime.JSX.Element;
88
+ declare function EditorProvider({ websocketUrl, documentName, token, tokenExpiresAt, user, autoConnect, autoReconnect, maxReconnectAttempts, onConnectionChange, onSync, onAuthError, onRefreshToken, children, }: EditorProviderProps): react_jsx_runtime.JSX.Element;
81
89
  /**
82
90
  * Hook to access the editor context.
83
91
  * Must be used within an EditorProvider.
@@ -113,4 +121,4 @@ interface DocumentEditorPaneProps extends Omit<ArtifactPaneProps, "children" | "
113
121
  */
114
122
  declare function DocumentEditorPane({ eyebrow, title, subtitle, meta, headerActions, footer, className, contentClassName, tabs, toolbar, markdown, mode, defaultMode, onModeChange, backend, placeholder, autoFocus, readOnly, onChange, onSave, saving, saveLabel, previewClassName, editorClassName, collaboration, }: DocumentEditorPaneProps): react_jsx_runtime.JSX.Element;
115
123
 
116
- export { type Collaborator as C, type DocumentEditorBackend as D, type EditorContextValue as E, type ConnectionState as a, type DocumentEditorMode as b, DocumentEditorPane as c, type DocumentEditorPaneCollaborationConfig as d, type DocumentEditorPaneProps as e, EditorProvider as f, type EditorProviderProps as g, type EditorUser as h, useEditorContext as u };
124
+ export { type Collaborator as C, type DocumentEditorBackend as D, type EditorContextValue as E, type ConnectionState as a, type DocumentEditorMode as b, DocumentEditorPane as c, type DocumentEditorPaneCollaborationConfig as d, type DocumentEditorPaneProps as e, EditorProvider as f, type EditorProviderProps as g, type EditorTokenRefreshResult as h, type EditorUser as i, useEditorContext as u };
package/dist/editor.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { C as Collaborator, a as ConnectionState } from './document-editor-pane-Bk-9MQmw.js';
2
- export { D as DocumentEditorBackend, b as DocumentEditorMode, c as DocumentEditorPane, d as DocumentEditorPaneCollaborationConfig, e as DocumentEditorPaneProps, E as EditorContextValue, f as EditorProvider, g as EditorProviderProps, h as EditorUser, u as useEditorContext } from './document-editor-pane-Bk-9MQmw.js';
1
+ import { C as Collaborator, a as ConnectionState } from './document-editor-pane-Xnl8SmA7.js';
2
+ export { D as DocumentEditorBackend, b as DocumentEditorMode, c as DocumentEditorPane, d as DocumentEditorPaneCollaborationConfig, e as DocumentEditorPaneProps, E as EditorContextValue, f as EditorProvider, g as EditorProviderProps, h as EditorTokenRefreshResult, i as EditorUser, u as useEditorContext } from './document-editor-pane-Xnl8SmA7.js';
3
3
  import * as react_jsx_runtime from 'react/jsx-runtime';
4
4
  import { Editor } from '@tiptap/react';
5
5
  import { HocuspocusProvider } from '@hocuspocus/provider';
package/dist/editor.js CHANGED
@@ -11,7 +11,7 @@ import {
11
11
  useEditorConnection,
12
12
  useEditorContext,
13
13
  useYjsState
14
- } from "./chunk-MGCVTFKB.js";
14
+ } from "./chunk-4HT5J6CE.js";
15
15
  import "./chunk-Q56BYXQF.js";
16
16
  import "./chunk-W4LM3QYZ.js";
17
17
  import "./chunk-LTFK464G.js";
package/dist/files.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { a as FileTabData } from './file-tabs-BLfxfmAH.js';
2
2
  export { F as FileNode, b as FileTabs, c as FileTabsProps, d as FileTree, e as FileTreeProps, f as FileTreeVisibilityOptions, g as filterFileTree } from './file-tabs-BLfxfmAH.js';
3
3
  import * as react_jsx_runtime from 'react/jsx-runtime';
4
- import { b as DocumentEditorMode, D as DocumentEditorBackend, d as DocumentEditorPaneCollaborationConfig } from './document-editor-pane-Bk-9MQmw.js';
4
+ import { b as DocumentEditorMode, D as DocumentEditorBackend, d as DocumentEditorPaneCollaborationConfig } from './document-editor-pane-Xnl8SmA7.js';
5
5
  import { A as ArtifactPaneProps } from './artifact-pane-Bh45Ssco.js';
6
6
  import 'react';
7
7
  import '@hocuspocus/provider';
package/dist/files.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  FileTabs,
5
5
  FileTree,
6
6
  filterFileTree
7
- } from "./chunk-ZYGWTIWO.js";
7
+ } from "./chunk-IAIJUFM6.js";
8
8
  import "./chunk-W4LM3QYZ.js";
9
9
  import "./chunk-LTFK464G.js";
10
10
  import "./chunk-RQHJBTEU.js";
package/dist/index.d.ts CHANGED
@@ -35,7 +35,7 @@ import '@radix-ui/react-tabs';
35
35
  import '@radix-ui/react-progress';
36
36
  import '@radix-ui/react-switch';
37
37
  import '@radix-ui/react-label';
38
- import './document-editor-pane-Bk-9MQmw.js';
38
+ import './document-editor-pane-Xnl8SmA7.js';
39
39
  import '@hocuspocus/provider';
40
40
  import 'yjs';
41
41
  import 'nanostores';
package/dist/index.js CHANGED
@@ -89,7 +89,7 @@ import {
89
89
  StatusBar,
90
90
  TerminalPanel,
91
91
  WorkspaceLayout
92
- } from "./chunk-RQOX5JRR.js";
92
+ } from "./chunk-QMKWQF6F.js";
93
93
  import "./chunk-OEX7NZE3.js";
94
94
  import {
95
95
  EmptyState,
@@ -158,7 +158,7 @@ import {
158
158
  FileTabs,
159
159
  FileTree,
160
160
  filterFileTree
161
- } from "./chunk-ZYGWTIWO.js";
161
+ } from "./chunk-IAIJUFM6.js";
162
162
  import {
163
163
  ArtifactPane
164
164
  } from "./chunk-W4LM3QYZ.js";
package/dist/workspace.js CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  StatusBar,
11
11
  TerminalPanel,
12
12
  WorkspaceLayout
13
- } from "./chunk-RQOX5JRR.js";
13
+ } from "./chunk-QMKWQF6F.js";
14
14
  import "./chunk-OEX7NZE3.js";
15
15
  import "./chunk-MUOL44AE.js";
16
16
  import "./chunk-6H3EFUUC.js";
@@ -21,7 +21,7 @@ import "./chunk-CJ2RYVZH.js";
21
21
  import "./chunk-BX6AQMUS.js";
22
22
  import "./chunk-YDBXQQLC.js";
23
23
  import "./chunk-TQN3VR4F.js";
24
- import "./chunk-ZYGWTIWO.js";
24
+ import "./chunk-IAIJUFM6.js";
25
25
  import {
26
26
  ArtifactPane
27
27
  } from "./chunk-W4LM3QYZ.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/sandbox-ui",
3
- "version": "0.3.10",
3
+ "version": "0.3.12",
4
4
  "description": "Unified UI component library for Tangle Sandbox — primitives, chat, dashboard, terminal, editor, and workspace components",
5
5
  "repository": {
6
6
  "type": "git",