kritzel-stencil 0.1.73 → 0.1.75

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.
Files changed (188) hide show
  1. package/dist/cjs/index-Dc7LOVhs.js +2 -2
  2. package/dist/cjs/index.cjs.js +131 -86
  3. package/dist/cjs/{kritzel-active-users_41.cjs.entry.js → kritzel-active-users_42.cjs.entry.js} +593 -177
  4. package/dist/cjs/kritzel-brush-style.cjs.entry.js +1 -1
  5. package/dist/cjs/loader.cjs.js +1 -1
  6. package/dist/cjs/stencil.cjs.js +1 -1
  7. package/dist/cjs/{workspace.migrations-DcwqsqPC.js → workspace.migrations-DkmVO6dE.js} +164 -49
  8. package/dist/collection/classes/core/core.class.js +9 -1
  9. package/dist/collection/classes/core/store.class.js +20 -6
  10. package/dist/collection/classes/core/viewport.class.js +9 -3
  11. package/dist/collection/classes/handlers/selection.handler.js +15 -2
  12. package/dist/collection/classes/managers/anchor.manager.js +101 -44
  13. package/dist/collection/classes/objects/base-object.class.js +2 -0
  14. package/dist/collection/classes/objects/custom-element.class.js +1 -0
  15. package/dist/collection/classes/objects/group.class.js +1 -0
  16. package/dist/collection/classes/objects/image.class.js +1 -0
  17. package/dist/collection/classes/objects/line.class.js +1 -0
  18. package/dist/collection/classes/objects/path.class.js +1 -0
  19. package/dist/collection/classes/objects/selection-box.class.js +1 -0
  20. package/dist/collection/classes/objects/selection-group.class.js +13 -1
  21. package/dist/collection/classes/objects/shape.class.js +1 -0
  22. package/dist/collection/classes/objects/text.class.js +1 -0
  23. package/dist/collection/classes/providers/broadcast-sync-provider.class.js +5 -0
  24. package/dist/collection/classes/providers/hocuspocus-sync-provider.class.js +120 -85
  25. package/dist/collection/classes/providers/indexeddb-sync-provider.class.js +5 -0
  26. package/dist/collection/classes/providers/websocket-sync-provider.class.js +5 -0
  27. package/dist/collection/classes/structures/app-state-map.structure.js +15 -4
  28. package/dist/collection/classes/structures/object-map.structure.js +85 -7
  29. package/dist/collection/classes/tools/brush-tool.class.js +4 -0
  30. package/dist/collection/classes/tools/line-tool.class.js +4 -0
  31. package/dist/collection/classes/tools/shape-tool.class.js +2 -0
  32. package/dist/collection/collection-manifest.json +3 -2
  33. package/dist/collection/components/core/kritzel-awareness-cursors/kritzel-awareness-cursors.css +110 -0
  34. package/dist/collection/components/core/kritzel-awareness-cursors/kritzel-awareness-cursors.js +347 -0
  35. package/dist/collection/components/core/kritzel-cursor-trail/kritzel-cursor-trail.js +1 -1
  36. package/dist/collection/components/core/kritzel-editor/kritzel-editor.js +3 -3
  37. package/dist/collection/components/core/kritzel-engine/kritzel-engine.js +150 -109
  38. package/dist/collection/components/shared/kritzel-avatar/kritzel-avatar.js +3 -3
  39. package/dist/collection/components/shared/kritzel-brush-style/kritzel-brush-style.js +1 -1
  40. package/dist/collection/components/shared/kritzel-button/kritzel-button.js +2 -2
  41. package/dist/collection/components/shared/kritzel-color/kritzel-color.js +2 -2
  42. package/dist/collection/components/shared/kritzel-color-palette/kritzel-color-palette.js +1 -1
  43. package/dist/collection/components/shared/kritzel-dropdown/kritzel-dropdown.js +1 -1
  44. package/dist/collection/components/shared/kritzel-font/kritzel-font.js +1 -1
  45. package/dist/collection/components/shared/kritzel-font-size/kritzel-font-size.js +1 -1
  46. package/dist/collection/components/shared/kritzel-input/kritzel-input.js +1 -1
  47. package/dist/collection/components/shared/kritzel-master-detail/kritzel-master-detail.js +3 -3
  48. package/dist/collection/components/shared/kritzel-menu/kritzel-menu.js +1 -1
  49. package/dist/collection/components/shared/kritzel-menu-item/kritzel-menu-item.js +2 -2
  50. package/dist/collection/components/shared/kritzel-numeric-input/kritzel-numeric-input.js +1 -1
  51. package/dist/collection/components/shared/kritzel-opacity-slider/kritzel-opacity-slider.js +1 -1
  52. package/dist/collection/components/shared/kritzel-portal/kritzel-portal.js +1 -1
  53. package/dist/collection/components/shared/kritzel-slide-toggle/kritzel-slide-toggle.js +1 -1
  54. package/dist/collection/components/shared/kritzel-split-button/kritzel-split-button.js +1 -1
  55. package/dist/collection/components/shared/kritzel-stroke-size/kritzel-stroke-size.js +1 -1
  56. package/dist/collection/components/shared/kritzel-tooltip/kritzel-tooltip.js +2 -2
  57. package/dist/collection/components/ui/kritzel-back-to-content/kritzel-back-to-content.js +1 -1
  58. package/dist/collection/components/ui/kritzel-context-menu/kritzel-context-menu.js +1 -1
  59. package/dist/collection/components/ui/kritzel-controls/kritzel-controls.js +5 -5
  60. package/dist/collection/components/ui/kritzel-current-user/kritzel-current-user.js +1 -1
  61. package/dist/collection/components/ui/kritzel-current-user-dialog/kritzel-current-user-dialog.js +1 -1
  62. package/dist/collection/components/ui/kritzel-export/kritzel-export.js +1 -1
  63. package/dist/collection/components/ui/kritzel-login-dialog/kritzel-login-dialog.js +1 -1
  64. package/dist/collection/components/ui/kritzel-more-menu/kritzel-more-menu.js +1 -1
  65. package/dist/collection/components/ui/kritzel-settings/kritzel-settings.js +1 -1
  66. package/dist/collection/components/ui/kritzel-share-dialog/kritzel-share-dialog.js +2 -2
  67. package/dist/collection/components/ui/kritzel-utility-panel/kritzel-utility-panel.js +1 -1
  68. package/dist/collection/constants/schema.constants.js +1 -1
  69. package/dist/collection/constants/version.js +1 -1
  70. package/dist/collection/interfaces/remote-cursor.interface.js +1 -0
  71. package/dist/collection/migrations/workspace.migrations.js +10 -1
  72. package/dist/components/index.d.ts +2 -0
  73. package/dist/components/index.js +1 -1
  74. package/dist/components/kritzel-active-users.js +1 -1
  75. package/dist/components/kritzel-avatar.js +1 -1
  76. package/dist/components/kritzel-awareness-cursors.d.ts +11 -0
  77. package/dist/components/kritzel-awareness-cursors.js +1 -0
  78. package/dist/components/kritzel-back-to-content.js +1 -1
  79. package/dist/components/kritzel-brush-style.js +1 -1
  80. package/dist/components/kritzel-button.js +1 -1
  81. package/dist/components/kritzel-color-palette.js +1 -1
  82. package/dist/components/kritzel-color.js +1 -1
  83. package/dist/components/kritzel-context-menu.js +1 -1
  84. package/dist/components/kritzel-controls.js +1 -1
  85. package/dist/components/kritzel-current-user-dialog.js +1 -1
  86. package/dist/components/kritzel-current-user.js +1 -1
  87. package/dist/components/kritzel-cursor-trail.js +1 -1
  88. package/dist/components/kritzel-dropdown.js +1 -1
  89. package/dist/components/kritzel-editor.js +1 -1
  90. package/dist/components/kritzel-engine.js +1 -1
  91. package/dist/components/kritzel-export.js +1 -1
  92. package/dist/components/kritzel-font-family.js +1 -1
  93. package/dist/components/kritzel-font-size.js +1 -1
  94. package/dist/components/kritzel-font.js +1 -1
  95. package/dist/components/kritzel-input.js +1 -1
  96. package/dist/components/kritzel-login-dialog.js +1 -1
  97. package/dist/components/kritzel-master-detail.js +1 -1
  98. package/dist/components/kritzel-menu-item.js +1 -1
  99. package/dist/components/kritzel-menu.js +1 -1
  100. package/dist/components/kritzel-more-menu.js +1 -1
  101. package/dist/components/kritzel-numeric-input.js +1 -1
  102. package/dist/components/kritzel-opacity-slider.js +1 -1
  103. package/dist/components/kritzel-portal.js +1 -1
  104. package/dist/components/kritzel-settings.js +1 -1
  105. package/dist/components/kritzel-share-dialog.js +1 -1
  106. package/dist/components/kritzel-slide-toggle.js +1 -1
  107. package/dist/components/kritzel-split-button.js +1 -1
  108. package/dist/components/kritzel-stroke-size.js +1 -1
  109. package/dist/components/kritzel-tool-config.js +1 -1
  110. package/dist/components/kritzel-tooltip.js +1 -1
  111. package/dist/components/kritzel-utility-panel.js +1 -1
  112. package/dist/components/kritzel-workspace-manager.js +1 -1
  113. package/dist/components/{p-Dp8idtVD.js → p-0kShCfeb.js} +1 -1
  114. package/dist/components/{p-B47JuZiD.js → p-2OYw6GJ7.js} +1 -1
  115. package/dist/components/p-7o2FWtFx.js +1 -0
  116. package/dist/components/{p-C5KuV1pK.js → p-BA0ayKqO.js} +1 -1
  117. package/dist/components/{p-NbNVTRk6.js → p-BEJQ2kP7.js} +1 -1
  118. package/dist/components/p-BSipRoFx.js +1 -0
  119. package/dist/components/{p-CDadAOMw.js → p-BeFUNGEI.js} +1 -1
  120. package/dist/components/{p-35nrk8s0.js → p-BiByyU2C.js} +1 -1
  121. package/dist/components/{p-CCAWSyDD.js → p-BiouZo1q.js} +1 -1
  122. package/dist/components/{p-CSExtYKI.js → p-ByR0VXeU.js} +1 -1
  123. package/dist/components/{p-1MGcXTLv.js → p-C1uR_ZNW.js} +1 -1
  124. package/dist/components/{p-x8PzaMuD.js → p-C69Stayh.js} +1 -1
  125. package/dist/components/{p-Ch0UlFwq.js → p-C7SBI_0T.js} +1 -1
  126. package/dist/components/{p-DEzfXrGX.js → p-CAIGuV2J.js} +1 -1
  127. package/dist/components/p-CJ2eHeoV.js +1 -0
  128. package/dist/components/p-CW-VyJgK.js +1 -0
  129. package/dist/components/{p-DW4ADV9w.js → p-CZhyKp-f.js} +1 -1
  130. package/dist/components/p-CsR4owzk.js +1 -0
  131. package/dist/components/{p-BG1IxseV.js → p-CsoDfhD5.js} +1 -1
  132. package/dist/components/{p-BgDhcDNw.js → p-D0MQFmqi.js} +1 -1
  133. package/dist/components/{p-DpFu5yAt.js → p-D1O7DxL4.js} +1 -1
  134. package/dist/components/{p-B5ouV8EQ.js → p-DRbG92F9.js} +1 -1
  135. package/dist/components/{p-C3eaM9TB.js → p-DS0xx1eT.js} +1 -1
  136. package/dist/components/{p-jx8VOz7S.js → p-DSzQ6H2j.js} +1 -1
  137. package/dist/components/{p-DsIlDGDO.js → p-DXjuuVq9.js} +1 -1
  138. package/dist/components/p-DXpYcAnT.js +1 -0
  139. package/dist/components/{p-DiFVw6IQ.js → p-Da46jw3N.js} +1 -1
  140. package/dist/components/{p-C6kZf91d.js → p-Dj_Qjga5.js} +1 -1
  141. package/dist/components/{p-Do0Q5-iC.js → p-DvIEvoZu.js} +1 -1
  142. package/dist/components/{p-CnVzLD5e.js → p-GYI7sDxr.js} +1 -1
  143. package/dist/components/{p-CcBM_ClD.js → p-HLbqRJGs.js} +1 -1
  144. package/dist/components/{p-VHyNcODZ.js → p-KQzWumjB.js} +1 -1
  145. package/dist/components/{p-VAkeZOZL.js → p-TyR-YTXm.js} +1 -1
  146. package/dist/components/p-WmxufeOo.js +9 -0
  147. package/dist/components/{p-CHtn5xr6.js → p-b4gyXoju.js} +1 -1
  148. package/dist/components/p-iRL0wQHQ.js +1 -0
  149. package/dist/components/{p-CqLaHE27.js → p-kj9wbLY8.js} +1 -1
  150. package/dist/components/{p-DaHq4iG1.js → p-xM-_OeRO.js} +1 -1
  151. package/dist/esm/index-MV-81ybv.js +2 -2
  152. package/dist/esm/index.js +132 -87
  153. package/dist/esm/{kritzel-active-users_41.entry.js → kritzel-active-users_42.entry.js} +593 -178
  154. package/dist/esm/kritzel-brush-style.entry.js +1 -1
  155. package/dist/esm/loader.js +1 -1
  156. package/dist/esm/stencil.js +1 -1
  157. package/dist/esm/{workspace.migrations-BGixvB76.js → workspace.migrations-D48_Bqvh.js} +164 -49
  158. package/dist/stencil/index.esm.js +1 -1
  159. package/dist/stencil/p-0dbd9a2f.entry.js +9 -0
  160. package/dist/stencil/p-D48_Bqvh.js +1 -0
  161. package/dist/stencil/{p-016ad76a.entry.js → p-fc21e29c.entry.js} +1 -1
  162. package/dist/stencil/stencil.esm.js +1 -1
  163. package/dist/types/classes/core/store.class.d.ts +10 -2
  164. package/dist/types/classes/managers/anchor.manager.d.ts +4 -0
  165. package/dist/types/classes/objects/base-object.class.d.ts +1 -0
  166. package/dist/types/classes/objects/selection-group.class.d.ts +5 -0
  167. package/dist/types/classes/providers/broadcast-sync-provider.class.d.ts +2 -0
  168. package/dist/types/classes/providers/hocuspocus-sync-provider.class.d.ts +37 -1
  169. package/dist/types/classes/providers/indexeddb-sync-provider.class.d.ts +2 -0
  170. package/dist/types/classes/providers/websocket-sync-provider.class.d.ts +2 -0
  171. package/dist/types/classes/structures/object-map.structure.d.ts +38 -0
  172. package/dist/types/components/core/kritzel-awareness-cursors/kritzel-awareness-cursors.d.ts +26 -0
  173. package/dist/types/components.d.ts +39 -4
  174. package/dist/types/constants/schema.constants.d.ts +1 -1
  175. package/dist/types/constants/version.d.ts +1 -1
  176. package/dist/types/interfaces/object.interface.d.ts +1 -0
  177. package/dist/types/interfaces/remote-cursor.interface.d.ts +17 -0
  178. package/dist/types/interfaces/sync-provider.interface.d.ts +16 -0
  179. package/dist/types/interfaces/theme.interface.d.ts +7 -0
  180. package/package.json +1 -1
  181. package/dist/components/p-BvToKcu1.js +0 -1
  182. package/dist/components/p-By3NOY-k.js +0 -9
  183. package/dist/components/p-CNro30tB.js +0 -1
  184. package/dist/components/p-Duv3EM3w.js +0 -1
  185. package/dist/components/p-KFsLHwYm.js +0 -1
  186. package/dist/components/p-hCORwbZh.js +0 -1
  187. package/dist/stencil/p-67c79d75.entry.js +0 -9
  188. package/dist/stencil/p-BGixvB76.js +0 -1
@@ -4,6 +4,7 @@ import { HocuspocusProvider, HocuspocusProviderWebsocket } from "@hocuspocus/pro
4
4
  * Supports multiplexing - multiple documents can share the same WebSocket connection
5
5
  */
6
6
  export class HocuspocusSyncProvider {
7
+ type = 'network';
7
8
  provider;
8
9
  isConnected = false;
9
10
  isSynced = false;
@@ -11,16 +12,87 @@ export class HocuspocusSyncProvider {
11
12
  isDestroyed = false;
12
13
  connectTimeout = null;
13
14
  pendingConnectReject = null;
15
+ connectionTimeoutMs;
16
+ _connectionStatus = 'disconnected';
17
+ visibilityHandler = null;
18
+ onlineHandler = null;
14
19
  get awareness() {
15
20
  return this.provider.awareness;
16
21
  }
22
+ get connectionStatus() {
23
+ return this._connectionStatus;
24
+ }
17
25
  // Static shared WebSocket instance for multiplexing
18
26
  static sharedWebSocketProvider = null;
19
27
  constructor(docName, doc, options) {
20
28
  const name = options?.name || docName;
21
29
  const url = options?.url || 'ws://localhost:1234';
30
+ this.connectionTimeoutMs = options?.connectionTimeout ?? 10000;
22
31
  // Use provided websocketProvider or the static shared one
23
32
  const websocketProvider = options?.websocketProvider || HocuspocusSyncProvider.sharedWebSocketProvider;
33
+ // Build reconnect config from options
34
+ const reconnectConfig = {};
35
+ if (options?.delay !== undefined)
36
+ reconnectConfig.delay = options.delay;
37
+ if (options?.factor !== undefined)
38
+ reconnectConfig.factor = options.factor;
39
+ if (options?.maxAttempts !== undefined)
40
+ reconnectConfig.maxAttempts = options.maxAttempts;
41
+ if (options?.minDelay !== undefined)
42
+ reconnectConfig.minDelay = options.minDelay;
43
+ if (options?.maxDelay !== undefined)
44
+ reconnectConfig.maxDelay = options.maxDelay;
45
+ const onConnect = () => {
46
+ if (this.isDestroyed) {
47
+ return;
48
+ }
49
+ this.isConnected = true;
50
+ this._connectionStatus = 'connected';
51
+ if (!options?.quiet) {
52
+ console.info(`Hocuspocus connected: ${name}`);
53
+ }
54
+ if (options?.onConnect) {
55
+ options.onConnect();
56
+ }
57
+ };
58
+ const onDisconnect = () => {
59
+ if (this.isDestroyed) {
60
+ return;
61
+ }
62
+ this.isConnected = false;
63
+ this.isSynced = false;
64
+ this._connectionStatus = 'disconnected';
65
+ if (!options?.quiet) {
66
+ console.info(`Hocuspocus disconnected: ${name}`);
67
+ }
68
+ if (options?.onDisconnect) {
69
+ options.onDisconnect();
70
+ }
71
+ };
72
+ const onSynced = () => {
73
+ if (this.isDestroyed) {
74
+ return;
75
+ }
76
+ this.isSynced = true;
77
+ this._connectionStatus = 'synced';
78
+ if (!options?.quiet) {
79
+ console.info(`Hocuspocus synced: ${name}`);
80
+ }
81
+ if (options?.onSynced) {
82
+ options.onSynced();
83
+ }
84
+ };
85
+ const onStatus = (data) => {
86
+ if (this.isDestroyed) {
87
+ return;
88
+ }
89
+ if (data.status === 'connecting') {
90
+ this._connectionStatus = 'connecting';
91
+ }
92
+ if (options?.onStatus) {
93
+ options.onStatus(data);
94
+ }
95
+ };
24
96
  if (websocketProvider) {
25
97
  // Multiplexing mode - use shared WebSocket connection
26
98
  this.usesSharedSocket = true;
@@ -29,48 +101,11 @@ export class HocuspocusSyncProvider {
29
101
  name,
30
102
  document: doc,
31
103
  token: options?.token || null,
32
- onStatus: (data) => {
33
- if (options?.onStatus) {
34
- options.onStatus(data);
35
- }
36
- },
37
- onConnect: () => {
38
- if (this.isConnected || this.isDestroyed) {
39
- return;
40
- }
41
- this.isConnected = true;
42
- if (!options?.quiet) {
43
- console.info(`Hocuspocus connected: ${name}`);
44
- }
45
- if (options?.onConnect) {
46
- options.onConnect();
47
- }
48
- },
49
- onDisconnect: () => {
50
- if (this.isDestroyed || (!this.isConnected && !this.isSynced)) {
51
- return;
52
- }
53
- this.isConnected = false;
54
- this.isSynced = false;
55
- if (!options?.quiet) {
56
- console.info(`Hocuspocus disconnected: ${name}`);
57
- }
58
- if (options?.onDisconnect) {
59
- options.onDisconnect();
60
- }
61
- },
62
- onSynced: () => {
63
- if (this.isSynced || this.isDestroyed) {
64
- return;
65
- }
66
- this.isSynced = true;
67
- if (!options?.quiet) {
68
- console.info(`Hocuspocus synced: ${name}`);
69
- }
70
- if (options?.onSynced) {
71
- options.onSynced();
72
- }
73
- },
104
+ onStatus,
105
+ onConnect,
106
+ onDisconnect,
107
+ onSynced,
108
+ ...reconnectConfig,
74
109
  };
75
110
  // Add optional settings
76
111
  if (options?.forceSyncInterval !== undefined) {
@@ -95,48 +130,11 @@ export class HocuspocusSyncProvider {
95
130
  document: doc,
96
131
  token: options?.token || null,
97
132
  autoConnect: false,
98
- onStatus: (data) => {
99
- if (options?.onStatus) {
100
- options.onStatus(data);
101
- }
102
- },
103
- onConnect: () => {
104
- if (this.isConnected || this.isDestroyed) {
105
- return;
106
- }
107
- this.isConnected = true;
108
- if (!options?.quiet) {
109
- console.info(`Hocuspocus connected: ${name}`);
110
- }
111
- if (options?.onConnect) {
112
- options.onConnect();
113
- }
114
- },
115
- onDisconnect: () => {
116
- if (this.isDestroyed || (!this.isConnected && !this.isSynced)) {
117
- return;
118
- }
119
- this.isConnected = false;
120
- this.isSynced = false;
121
- if (!options?.quiet) {
122
- console.info(`Hocuspocus disconnected: ${name}`);
123
- }
124
- if (options?.onDisconnect) {
125
- options.onDisconnect();
126
- }
127
- },
128
- onSynced: () => {
129
- if (this.isSynced || this.isDestroyed) {
130
- return;
131
- }
132
- this.isSynced = true;
133
- if (!options?.quiet) {
134
- console.info(`Hocuspocus synced: ${name}`);
135
- }
136
- if (options?.onSynced) {
137
- options.onSynced();
138
- }
139
- },
133
+ onStatus,
134
+ onConnect,
135
+ onDisconnect,
136
+ onSynced,
137
+ ...reconnectConfig,
140
138
  };
141
139
  // Add optional settings
142
140
  if (options?.forceSyncInterval !== undefined) {
@@ -153,6 +151,35 @@ export class HocuspocusSyncProvider {
153
151
  console.info(`Hocuspocus Provider initialized: ${url}/${name}`);
154
152
  }
155
153
  }
154
+ this.setupBrowserEventListeners();
155
+ }
156
+ setupBrowserEventListeners() {
157
+ if (typeof document !== 'undefined') {
158
+ this.visibilityHandler = () => {
159
+ if (document.visibilityState === 'visible' && !this.isConnected && !this.isDestroyed) {
160
+ this.provider.connect();
161
+ }
162
+ };
163
+ document.addEventListener('visibilitychange', this.visibilityHandler);
164
+ }
165
+ if (typeof window !== 'undefined') {
166
+ this.onlineHandler = () => {
167
+ if (!this.isConnected && !this.isDestroyed) {
168
+ this.provider.connect();
169
+ }
170
+ };
171
+ window.addEventListener('online', this.onlineHandler);
172
+ }
173
+ }
174
+ removeBrowserEventListeners() {
175
+ if (this.visibilityHandler && typeof document !== 'undefined') {
176
+ document.removeEventListener('visibilitychange', this.visibilityHandler);
177
+ this.visibilityHandler = null;
178
+ }
179
+ if (this.onlineHandler && typeof window !== 'undefined') {
180
+ window.removeEventListener('online', this.onlineHandler);
181
+ this.onlineHandler = null;
182
+ }
156
183
  }
157
184
  /**
158
185
  * Create a shared WebSocket connection for multiplexing
@@ -215,6 +242,7 @@ export class HocuspocusSyncProvider {
215
242
  if (this.isSynced || this.isDestroyed) {
216
243
  return;
217
244
  }
245
+ this._connectionStatus = 'connecting';
218
246
  return new Promise((resolve, reject) => {
219
247
  // Store reject function so we can cancel the connection if destroyed
220
248
  this.pendingConnectReject = reject;
@@ -222,7 +250,7 @@ export class HocuspocusSyncProvider {
222
250
  this.pendingConnectReject = null;
223
251
  this.connectTimeout = null;
224
252
  reject(new Error('Hocuspocus connection timeout'));
225
- }, 10000); // 10 second timeout
253
+ }, this.connectionTimeoutMs);
226
254
  const syncHandler = () => {
227
255
  if (this.connectTimeout) {
228
256
  clearTimeout(this.connectTimeout);
@@ -252,6 +280,10 @@ export class HocuspocusSyncProvider {
252
280
  }
253
281
  });
254
282
  }
283
+ async reconnect() {
284
+ this.disconnect();
285
+ return this.connect();
286
+ }
255
287
  disconnect() {
256
288
  // Cancel any pending connection attempt
257
289
  if (this.connectTimeout) {
@@ -272,6 +304,7 @@ export class HocuspocusSyncProvider {
272
304
  }
273
305
  this.isConnected = false;
274
306
  this.isSynced = false;
307
+ this._connectionStatus = 'disconnected';
275
308
  }
276
309
  destroy() {
277
310
  // Mark as destroyed first to prevent any callbacks from doing work
@@ -284,10 +317,12 @@ export class HocuspocusSyncProvider {
284
317
  if (this.pendingConnectReject) {
285
318
  this.pendingConnectReject = null; // Don't reject, just abandon the promise
286
319
  }
320
+ this.removeBrowserEventListeners();
287
321
  if (this.provider) {
288
322
  this.provider.destroy();
289
323
  }
290
324
  this.isConnected = false;
291
325
  this.isSynced = false;
326
+ this._connectionStatus = 'disconnected';
292
327
  }
293
328
  }
@@ -3,6 +3,7 @@ import { IndexeddbPersistence } from "y-indexeddb";
3
3
  * IndexedDB sync provider for local persistence
4
4
  */
5
5
  export class IndexedDBSyncProvider {
6
+ type = 'local';
6
7
  provider;
7
8
  isConnected = false;
8
9
  constructor(docName, doc, options) {
@@ -24,6 +25,10 @@ export class IndexedDBSyncProvider {
24
25
  // IndexedDB doesn't need explicit disconnect
25
26
  this.isConnected = false;
26
27
  }
28
+ async reconnect() {
29
+ this.disconnect();
30
+ return this.connect();
31
+ }
27
32
  destroy() {
28
33
  if (this.provider) {
29
34
  this.provider.destroy();
@@ -3,6 +3,7 @@ import { WebsocketProvider } from "y-websocket";
3
3
  * WebSocket sync provider for real-time collaboration
4
4
  */
5
5
  export class WebSocketSyncProvider {
6
+ type = 'network';
6
7
  provider;
7
8
  isConnected = false;
8
9
  _quiet = false;
@@ -91,6 +92,10 @@ export class WebSocketSyncProvider {
91
92
  }
92
93
  this.isConnected = false;
93
94
  }
95
+ async reconnect() {
96
+ this.disconnect();
97
+ return this.connect();
98
+ }
94
99
  destroy() {
95
100
  if (this.provider) {
96
101
  this.provider.destroy();
@@ -79,13 +79,24 @@ export class KritzelAppStateMap {
79
79
  this.handleWorkspacesChange(event);
80
80
  };
81
81
  this._workspacesMap.observe(this._workspacesObserver);
82
- // Connect all providers in parallel (settle individually so one failure doesn't block the rest)
83
- const results = await Promise.allSettled(this._providers.map(p => p.connect()));
84
- results.forEach((result, i) => {
82
+ // Separate local providers (IndexedDB, BroadcastChannel) from network providers (Hocuspocus, WebSocket)
83
+ // Local providers are awaited so data is available immediately; network providers sync in the background
84
+ // via Yjs observers that are already registered above.
85
+ const localProviders = this._providers.filter(p => p.type === 'local');
86
+ const networkProviders = this._providers.filter(p => p.type === 'network');
87
+ // Await local providers for immediate data availability
88
+ const localResults = await Promise.allSettled(localProviders.map(p => p.connect()));
89
+ localResults.forEach((result, i) => {
85
90
  if (result.status === 'rejected') {
86
- console.error(`[Kritzel] Sync provider "${this._providers[i]?.constructor.name}" failed to connect:`, result.reason);
91
+ console.error(`[Kritzel] Sync provider "${localProviders[i]?.constructor.name}" failed to connect:`, result.reason);
87
92
  }
88
93
  });
94
+ // Connect network providers in the background (remote data arrives via Yjs observers)
95
+ for (const provider of networkProviders) {
96
+ provider.connect().catch(err => {
97
+ console.error(`[Kritzel] Network sync provider "${provider.constructor.name}" failed to connect:`, err);
98
+ });
99
+ }
89
100
  this._isReady = true;
90
101
  // Run any pending schema migrations before loading data
91
102
  const quietMigrations = !core.store.state.debugInfo.showMigrationInfo;
@@ -34,6 +34,7 @@ export class KritzelObjectMap {
34
34
  _stackItemPoppedHandler = null;
35
35
  _awarenessChangeHandler = null;
36
36
  _awarenessChangeCallbacks = [];
37
+ _objectsChangeCallbacks = [];
37
38
  _lastAwarenessEmitTime = 0;
38
39
  _awarenessEmitTimeout = null;
39
40
  AWARENESS_THROTTLE_INTERVAL = 100; // milliseconds
@@ -50,6 +51,19 @@ export class KritzelObjectMap {
50
51
  get awareness() {
51
52
  return this._awareness;
52
53
  }
54
+ /**
55
+ * Whether a network awareness instance is available.
56
+ */
57
+ get hasAwareness() {
58
+ return !!this._awareness;
59
+ }
60
+ /**
61
+ * Returns the local client ID from the awareness instance.
62
+ * Used to filter out the local user when rendering remote cursors.
63
+ */
64
+ get localClientId() {
65
+ return this._awareness?.clientID ?? null;
66
+ }
53
67
  /**
54
68
  * Sets the local user identity in the awareness state.
55
69
  * This broadcasts the user's identity to all connected peers.
@@ -58,12 +72,21 @@ export class KritzelObjectMap {
58
72
  if (!this._awareness || !user) {
59
73
  return;
60
74
  }
75
+ const displayName = user.displayName || user.firstName || 'Anonymous';
61
76
  this._awareness.setLocalStateField('user', {
62
77
  id: user.id,
63
- name: user.displayName || user.firstName || 'Anonymous',
64
- color: user.color,
78
+ displayName,
79
+ color: user.color || this.generateColorFromName(displayName),
65
80
  });
66
81
  }
82
+ generateColorFromName(name) {
83
+ let hash = 0;
84
+ for (let i = 0; i < name.length; i++) {
85
+ hash = name.charCodeAt(i) + ((hash << 5) - hash);
86
+ }
87
+ const hue = Math.abs(hash % 360);
88
+ return `hsl(${hue}, 45%, 55%)`;
89
+ }
67
90
  /**
68
91
  * Updates the local cursor position in the awareness state.
69
92
  * This broadcasts the cursor position to all connected peers.
@@ -83,6 +106,37 @@ export class KritzelObjectMap {
83
106
  }
84
107
  this._awareness.setLocalStateField('cursor', null);
85
108
  }
109
+ /**
110
+ * Sets the ID of the object currently being drawn by the local user.
111
+ * Remote clients use this to derive cursor position from the object's latest
112
+ * coordinates instead of the throttled awareness cursor, reducing desync.
113
+ * Pass `null` when drawing ends.
114
+ */
115
+ setActiveDrawingObject(objectId) {
116
+ if (!this._awareness) {
117
+ return;
118
+ }
119
+ this._awareness.setLocalStateField('activeObjectId', objectId);
120
+ }
121
+ /**
122
+ * Broadcasts the local user's selection box bounds via awareness.
123
+ * Remote clients use this to render the selection rectangle in the user's color.
124
+ */
125
+ setLocalSelectionBox(box) {
126
+ if (!this._awareness) {
127
+ return;
128
+ }
129
+ this._awareness.setLocalStateField('selectionBox', box);
130
+ }
131
+ /**
132
+ * Clears the local selection box from awareness (e.g., when selection completes or is cancelled).
133
+ */
134
+ clearLocalSelectionBox() {
135
+ if (!this._awareness) {
136
+ return;
137
+ }
138
+ this._awareness.setLocalStateField('selectionBox', null);
139
+ }
86
140
  /**
87
141
  * Registers a callback to be invoked when the awareness state changes.
88
142
  * The callback receives the full awareness states map.
@@ -90,6 +144,14 @@ export class KritzelObjectMap {
90
144
  onAwarenessChange(callback) {
91
145
  this._awarenessChangeCallbacks.push(callback);
92
146
  }
147
+ /**
148
+ * Registers a callback to be invoked when remote object changes are received.
149
+ * Used by the awareness cursors component to re-derive cursor positions
150
+ * from the latest object data when a remote user is actively drawing.
151
+ */
152
+ onObjectsChange(callback) {
153
+ this._objectsChangeCallbacks.push(callback);
154
+ }
93
155
  /**
94
156
  * Returns the Yjs UndoManager instance for managing undo/redo operations.
95
157
  * @returns The UndoManager instance, or `null` if not initialized
@@ -208,16 +270,28 @@ export class KritzelObjectMap {
208
270
  this.handleObjectsChange(event);
209
271
  };
210
272
  this._objectsMap.observe(this._objectsObserver);
211
- // Connect all providers in parallel (settle individually so one failure doesn't block the rest)
212
- const results = await Promise.allSettled(this._providers.map(p => p.connect()));
213
- results.forEach((result, i) => {
273
+ // Separate local providers (IndexedDB, BroadcastChannel) from network providers (Hocuspocus, WebSocket)
274
+ // Local providers are awaited so data is available immediately; network providers sync in the background
275
+ // via Yjs observers that are already registered above.
276
+ const localProviders = this._providers.filter(p => p.type === 'local');
277
+ const networkProviders = this._providers.filter(p => p.type === 'network');
278
+ // Await local providers for immediate data availability
279
+ const localResults = await Promise.allSettled(localProviders.map(p => p.connect()));
280
+ localResults.forEach((result, i) => {
214
281
  if (result.status === 'rejected') {
215
- console.error(`[Kritzel] Sync provider "${this._providers[i]?.constructor.name}" failed to connect:`, result.reason);
282
+ console.error(`[Kritzel] Sync provider "${localProviders[i]?.constructor.name}" failed to connect:`, result.reason);
216
283
  }
217
284
  });
285
+ // Connect network providers in the background (remote data arrives via Yjs observers)
286
+ for (const provider of networkProviders) {
287
+ provider.connect().catch(err => {
288
+ console.error(`[Kritzel] Network sync provider "${provider.constructor.name}" failed to connect:`, err);
289
+ });
290
+ }
218
291
  this._isReady = true;
219
292
  // Find the first provider that exposes awareness (network providers)
220
- for (const provider of this._providers) {
293
+ // Awareness is available immediately after provider construction, before connect() resolves
294
+ for (const provider of networkProviders) {
221
295
  if (provider.awareness) {
222
296
  this._awareness = provider.awareness;
223
297
  break;
@@ -388,6 +462,10 @@ export class KritzelObjectMap {
388
462
  if (updatedObjects.length > 0) {
389
463
  this._core?.engine.emitObjectsUpdated(updatedObjects.map(obj => ({ object: obj, changedProperties: [] })));
390
464
  }
465
+ // Notify subscribers of remote object changes (e.g., awareness cursors)
466
+ for (const callback of this._objectsChangeCallbacks) {
467
+ callback();
468
+ }
391
469
  }
392
470
  /**
393
471
  * Initializes document metadata if not already set.
@@ -55,6 +55,7 @@ export class KritzelBrushTool extends KritzelBaseTool {
55
55
  path.isCompleted = false;
56
56
  this._currentPathId = path.id;
57
57
  this._core.store.state.objects.insert(path);
58
+ this._core.store.state.objects?.setActiveDrawingObject(path.id);
58
59
  }
59
60
  }
60
61
  if (event.pointerType === 'touch' || event.pointerType === 'pen') {
@@ -78,6 +79,7 @@ export class KritzelBrushTool extends KritzelBaseTool {
78
79
  path.isCompleted = false;
79
80
  this._currentPathId = path.id;
80
81
  this._core.store.state.objects.insert(path);
82
+ this._core.store.state.objects?.setActiveDrawingObject(path.id);
81
83
  }
82
84
  }
83
85
  }
@@ -168,6 +170,7 @@ export class KritzelBrushTool extends KritzelBaseTool {
168
170
  this._core.engine.emitObjectsChange();
169
171
  this._core.engine.emitObjectsAdded([currentPath]);
170
172
  }
173
+ this._core.store.state.objects?.setActiveDrawingObject(null);
171
174
  this._currentPathId = null;
172
175
  }
173
176
  }
@@ -183,6 +186,7 @@ export class KritzelBrushTool extends KritzelBaseTool {
183
186
  this._core.engine.emitObjectsChange();
184
187
  this._core.engine.emitObjectsAdded([currentPath]);
185
188
  }
189
+ this._core.store.state.objects?.setActiveDrawingObject(null);
186
190
  this._currentPathId = null;
187
191
  }
188
192
  }
@@ -70,6 +70,7 @@ export class KritzelLineTool extends KritzelBaseTool {
70
70
  line.isCompleted = false;
71
71
  this._currentLineId = line.id;
72
72
  this._core.store.state.objects.insert(line);
73
+ this._core.store.state.objects?.setActiveDrawingObject(line.id);
73
74
  }
74
75
  }
75
76
  if (event.pointerType === 'touch' || event.pointerType === 'pen') {
@@ -99,6 +100,7 @@ export class KritzelLineTool extends KritzelBaseTool {
99
100
  line.isCompleted = false;
100
101
  this._currentLineId = line.id;
101
102
  this._core.store.state.objects.insert(line);
103
+ this._core.store.state.objects?.setActiveDrawingObject(line.id);
102
104
  }
103
105
  }
104
106
  }
@@ -197,6 +199,7 @@ export class KritzelLineTool extends KritzelBaseTool {
197
199
  // Switch to selection tool and select the drawn line
198
200
  this.selectLineAndSwitchTool(currentLine);
199
201
  }
202
+ this._core.store.state.objects?.setActiveDrawingObject(null);
200
203
  this._currentLineId = null;
201
204
  }
202
205
  }
@@ -214,6 +217,7 @@ export class KritzelLineTool extends KritzelBaseTool {
214
217
  // Switch to selection tool and select the drawn line
215
218
  this.selectLineAndSwitchTool(currentLine);
216
219
  }
220
+ this._core.store.state.objects?.setActiveDrawingObject(null);
217
221
  this._currentLineId = null;
218
222
  }
219
223
  }
@@ -179,6 +179,7 @@ export class KritzelShapeTool extends KritzelBaseTool {
179
179
  scale: lockScale ? 1 : viewportScale,
180
180
  });
181
181
  this._core.store.state.objects.insert(this.currentShape);
182
+ this._core.store.state.objects?.setActiveDrawingObject(this.currentShape.id);
182
183
  this._core.rerender();
183
184
  }
184
185
  /**
@@ -239,6 +240,7 @@ export class KritzelShapeTool extends KritzelBaseTool {
239
240
  this._core.store.setState('activeTool', KritzelToolRegistry.getTool('selection'));
240
241
  }
241
242
  this.isDrawing = false;
243
+ this._core.store.state.objects?.setActiveDrawingObject(null);
242
244
  this.currentShape = null;
243
245
  this._core.rerender();
244
246
  }
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "entries": [
3
- "components/shared/kritzel-dropdown/kritzel-dropdown.js",
4
- "components/shared/kritzel-brush-style/kritzel-brush-style.js",
5
3
  "components/core/kritzel-engine/kritzel-engine.js",
6
4
  "components/ui/kritzel-context-menu/kritzel-context-menu.js",
5
+ "components/shared/kritzel-dropdown/kritzel-dropdown.js",
6
+ "components/shared/kritzel-brush-style/kritzel-brush-style.js",
7
7
  "components/shared/kritzel-font-family/kritzel-font-family.js",
8
8
  "components/shared/kritzel-line-endings/kritzel-line-endings.js",
9
9
  "components/shared/kritzel-pill-tabs/kritzel-pill-tabs.js",
10
10
  "components/shared/kritzel-shape-fill/kritzel-shape-fill.js",
11
+ "components/core/kritzel-awareness-cursors/kritzel-awareness-cursors.js",
11
12
  "components/core/kritzel-cursor-trail/kritzel-cursor-trail.js",
12
13
  "components/core/kritzel-editor/kritzel-editor.js",
13
14
  "components/shared/kritzel-avatar/kritzel-avatar.js",