@abraca/nuxt 1.6.0 → 1.8.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.
Files changed (43) hide show
  1. package/dist/module.d.mts +6 -0
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +16 -2
  4. package/dist/runtime/assets/sources.css +1 -0
  5. package/dist/runtime/components/ADocumentTree.d.vue.ts +11 -1
  6. package/dist/runtime/components/ADocumentTree.vue +13 -6
  7. package/dist/runtime/components/ADocumentTree.vue.d.ts +11 -1
  8. package/dist/runtime/components/renderers/AChecklistRenderer.vue +22 -4
  9. package/dist/runtime/components/renderers/ADashboardRenderer.vue +4 -2
  10. package/dist/runtime/components/renderers/AGalleryRenderer.vue +97 -70
  11. package/dist/runtime/components/renderers/AGraphRenderer.vue +209 -58
  12. package/dist/runtime/components/renderers/AKanbanRenderer.vue +145 -34
  13. package/dist/runtime/components/renderers/AMediaRenderer.vue +27 -17
  14. package/dist/runtime/components/renderers/AOutlineRenderer.vue +38 -23
  15. package/dist/runtime/components/renderers/ASlidesRenderer.d.vue.ts +21 -0
  16. package/dist/runtime/components/renderers/ASlidesRenderer.vue +591 -0
  17. package/dist/runtime/components/renderers/ASlidesRenderer.vue.d.ts +21 -0
  18. package/dist/runtime/components/renderers/ASpatialRenderer.vue +23 -0
  19. package/dist/runtime/components/renderers/ATableRenderer.vue +20 -391
  20. package/dist/runtime/components/renderers/gallery/AGalleryItemCard.d.vue.ts +40 -0
  21. package/dist/runtime/components/renderers/gallery/AGalleryItemCard.vue +227 -0
  22. package/dist/runtime/components/renderers/gallery/AGalleryItemCard.vue.d.ts +40 -0
  23. package/dist/runtime/components/renderers/spatial/SpatialTransformInputs.d.vue.ts +16 -0
  24. package/dist/runtime/components/renderers/spatial/SpatialTransformInputs.vue +66 -0
  25. package/dist/runtime/components/renderers/spatial/SpatialTransformInputs.vue.d.ts +16 -0
  26. package/dist/runtime/components/renderers/table/ATableFlatMode.d.vue.ts +2 -0
  27. package/dist/runtime/components/renderers/table/ATableFlatMode.vue +184 -21
  28. package/dist/runtime/components/renderers/table/ATableFlatMode.vue.d.ts +2 -0
  29. package/dist/runtime/components/renderers/table/ATableHierarchyMode.d.vue.ts +26 -0
  30. package/dist/runtime/components/renderers/table/ATableHierarchyMode.vue +662 -0
  31. package/dist/runtime/components/renderers/table/ATableHierarchyMode.vue.d.ts +26 -0
  32. package/dist/runtime/composables/useAwareness.js +14 -3
  33. package/dist/runtime/composables/useBackgroundSync.js +19 -1
  34. package/dist/runtime/composables/useFileIndex.js +38 -17
  35. package/dist/runtime/composables/useSearchIndex.js +41 -16
  36. package/dist/runtime/composables/useSlidesNavigation.d.ts +45 -0
  37. package/dist/runtime/composables/useSlidesNavigation.js +185 -0
  38. package/dist/runtime/composables/useYDoc.d.ts +1 -1
  39. package/dist/runtime/composables/useYDoc.js +47 -9
  40. package/dist/runtime/locale.d.ts +38 -0
  41. package/dist/runtime/locale.js +41 -3
  42. package/dist/runtime/utils/docTypes.js +17 -0
  43. package/package.json +3 -3
@@ -0,0 +1,26 @@
1
+ import type { useTableView } from '../../../composables/useTableView.js';
2
+ import type { AbracadabraLocale } from '../../../locale.js';
3
+ type TableLabels = AbracadabraLocale['renderers']['table'];
4
+ type __VLS_Props = {
5
+ tree: ReturnType<typeof import('../../../composables/useChildTree').useChildTree>;
6
+ tableView: ReturnType<typeof useTableView>;
7
+ states: Array<Record<string, any>>;
8
+ myClientId: number;
9
+ setLocalState: (state: Record<string, any>) => void;
10
+ editable?: boolean;
11
+ labels?: Partial<TableLabels>;
12
+ };
13
+ declare function addColumn(): void;
14
+ declare function addRow(): void;
15
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {
16
+ addColumn: typeof addColumn;
17
+ addRow: typeof addRow;
18
+ }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
19
+ openNode: (id: string, label: string) => any;
20
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
21
+ onOpenNode?: ((id: string, label: string) => any) | undefined;
22
+ }>, {
23
+ editable: boolean;
24
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
25
+ declare const _default: typeof __VLS_export;
26
+ export default _default;
@@ -13,20 +13,31 @@ export function useAwareness() {
13
13
  states.value = new Map(awareness.getStates());
14
14
  }
15
15
  let _unsub = null;
16
+ let _rafId = null;
17
+ let _rafPending = false;
18
+ const _throttledSync = () => {
19
+ if (_rafPending) return;
20
+ _rafPending = true;
21
+ _rafId = requestAnimationFrame(() => {
22
+ _rafPending = false;
23
+ _rafId = null;
24
+ _syncStates();
25
+ });
26
+ };
16
27
  function _subscribe() {
17
28
  _unsub?.();
18
29
  _unsub = null;
19
30
  const awareness = provider.value?.awareness;
20
31
  if (!awareness) return;
21
32
  _syncStates();
22
- const handler = () => _syncStates();
23
- awareness.on("change", handler);
24
- _unsub = () => awareness.off("change", handler);
33
+ awareness.on("change", _throttledSync);
34
+ _unsub = () => awareness.off("change", _throttledSync);
25
35
  }
26
36
  watch(provider, () => _subscribe(), { immediate: true });
27
37
  tryOnScopeDispose(() => {
28
38
  _unsub?.();
29
39
  _unsub = null;
40
+ if (_rafId !== null) cancelAnimationFrame(_rafId);
30
41
  });
31
42
  const localState = computed(() => {
32
43
  const awareness = provider.value?.awareness;
@@ -6,6 +6,8 @@ const lastCompletedAt = ref(null);
6
6
  let _manager = null;
7
7
  let _stopPeriodicSync = null;
8
8
  let _periodicIntervalMs = 5 * 60 * 1e3;
9
+ let _pendingStateChanges = null;
10
+ let _flushScheduled = false;
9
11
  const syncedCount = computed(() => {
10
12
  let n = 0;
11
13
  for (const s of syncStates.value.values()) {
@@ -32,8 +34,22 @@ const progress = computed(() => {
32
34
  export function _initBackgroundSync(manager, intervalMs = 5 * 60 * 1e3) {
33
35
  _manager = manager;
34
36
  _periodicIntervalMs = intervalMs;
37
+ _pendingStateChanges = null;
38
+ _flushScheduled = false;
35
39
  _manager.on("stateChanged", ({ docId, state }) => {
36
- syncStates.value = new Map(syncStates.value).set(docId, state);
40
+ if (!_pendingStateChanges) _pendingStateChanges = /* @__PURE__ */ new Map();
41
+ _pendingStateChanges.set(docId, state);
42
+ if (!_flushScheduled) {
43
+ _flushScheduled = true;
44
+ queueMicrotask(() => {
45
+ _flushScheduled = false;
46
+ if (!_pendingStateChanges) return;
47
+ const next = new Map(syncStates.value);
48
+ for (const [id, s] of _pendingStateChanges) next.set(id, s);
49
+ _pendingStateChanges = null;
50
+ syncStates.value = next;
51
+ });
52
+ }
37
53
  });
38
54
  _manager.init().then(() => {
39
55
  const existing = _manager.getSyncStatus();
@@ -47,6 +63,8 @@ export function _destroyBackgroundSync() {
47
63
  _stopPeriodicSync = null;
48
64
  _manager?.destroy();
49
65
  _manager = null;
66
+ _pendingStateChanges = null;
67
+ _flushScheduled = false;
50
68
  syncStates.value = /* @__PURE__ */ new Map();
51
69
  isSyncing.value = false;
52
70
  isPaused.value = false;
@@ -1,4 +1,5 @@
1
1
  import { watch } from "vue";
2
+ import { useDebounceFn } from "@vueuse/core";
2
3
  import * as Y from "yjs";
3
4
  import { useAbracadabra } from "./useAbracadabra.js";
4
5
  import { useBackgroundSync } from "./useBackgroundSync.js";
@@ -29,26 +30,46 @@ export function _destroyFileIndex() {
29
30
  function _startWatcher() {
30
31
  const { syncStates } = useBackgroundSync();
31
32
  const { provider } = useAbracadabra();
32
- const stop = watch(syncStates, async (states) => {
33
- const p = provider.value;
34
- if (!p) return;
35
- for (const [docId, state] of states.entries()) {
36
- if (state.status !== "synced") continue;
37
- try {
38
- const childProvider = await p.loadChild(docId);
39
- await childProvider.ready;
40
- const treeMap = p.document.getMap("doc-tree");
41
- const docLabel = treeMap.get(docId)?.label ?? "";
42
- const files = [];
43
- walkXmlForFiles(childProvider.document.getXmlFragment("default"), docId, docLabel, files);
44
- for (const [key, val] of _fileEntries.entries()) {
45
- if (val.docId === docId) _fileEntries.delete(key);
33
+ const pendingDocIds = /* @__PURE__ */ new Set();
34
+ let isProcessing = false;
35
+ async function processPending() {
36
+ if (isProcessing) return;
37
+ isProcessing = true;
38
+ try {
39
+ const batch = [...pendingDocIds];
40
+ pendingDocIds.clear();
41
+ const p = provider.value;
42
+ if (!p) return;
43
+ for (let i = 0; i < batch.length; i++) {
44
+ const docId = batch[i];
45
+ try {
46
+ const childProvider = await p.loadChild(docId);
47
+ await childProvider.ready;
48
+ const treeMap = p.document.getMap("doc-tree");
49
+ const docLabel = treeMap.get(docId)?.label ?? "";
50
+ const files = [];
51
+ walkXmlForFiles(childProvider.document.getXmlFragment("default"), docId, docLabel, files);
52
+ for (const [key, val] of _fileEntries.entries()) {
53
+ if (val.docId === docId) _fileEntries.delete(key);
54
+ }
55
+ for (const f of files) _fileEntries.set(f.uploadId, f);
56
+ } catch (e) {
57
+ if (import.meta.dev) console.warn(`[abracadabra] file-index: failed to index doc ${docId}:`, e);
46
58
  }
47
- for (const f of files) _fileEntries.set(f.uploadId, f);
48
- } catch (e) {
49
- if (import.meta.dev) console.warn(`[abracadabra] file-index: failed to index doc ${docId}:`, e);
59
+ if (i % 5 === 4) await new Promise((r) => setTimeout(r, 0));
50
60
  }
61
+ } finally {
62
+ isProcessing = false;
63
+ if (pendingDocIds.size > 0) debouncedProcess();
64
+ }
65
+ }
66
+ const debouncedProcess = useDebounceFn(processPending, 500);
67
+ const stop = watch(syncStates, (states) => {
68
+ for (const [docId, state] of states.entries()) {
69
+ if (state.status !== "synced") continue;
70
+ pendingDocIds.add(docId);
51
71
  }
72
+ if (pendingDocIds.size > 0) debouncedProcess();
52
73
  }, { deep: false });
53
74
  _stopWatcher = stop;
54
75
  }
@@ -1,9 +1,11 @@
1
1
  import { ref, computed, watch } from "vue";
2
+ import { useDebounceFn } from "@vueuse/core";
2
3
  import * as Y from "yjs";
3
4
  import { useAbracadabra } from "./useAbracadabra.js";
4
5
  import { useBackgroundSync } from "./useBackgroundSync.js";
5
6
  let _searchIndex = null;
6
7
  const _indexedVersions = /* @__PURE__ */ new Map();
8
+ const _lastSyncedCache = /* @__PURE__ */ new Map();
7
9
  const _isReady = ref(false);
8
10
  const _indexedCount = ref(0);
9
11
  const _error = ref(null);
@@ -51,6 +53,7 @@ export function _initSearchIndex(serverOrigin) {
51
53
  _searchIndex = new SearchIndex(serverOrigin);
52
54
  _isReady.value = true;
53
55
  _indexedVersions.clear();
56
+ _lastSyncedCache.clear();
54
57
  _indexedCount.value = 0;
55
58
  _startWatcher();
56
59
  }).catch((e) => {
@@ -65,34 +68,56 @@ export function _destroySearchIndex() {
65
68
  _searchIndex = null;
66
69
  _isReady.value = false;
67
70
  _indexedVersions.clear();
71
+ _lastSyncedCache.clear();
68
72
  _indexedCount.value = 0;
69
73
  _error.value = null;
70
74
  }
71
75
  function _startWatcher() {
72
76
  const { syncStates } = useBackgroundSync();
73
77
  const { provider } = useAbracadabra();
74
- const stop = watch(syncStates, async (states) => {
78
+ const pendingDocIds = /* @__PURE__ */ new Set();
79
+ let isProcessing = false;
80
+ async function processPending() {
81
+ if (isProcessing) return;
82
+ isProcessing = true;
83
+ try {
84
+ const batch = [...pendingDocIds];
85
+ pendingDocIds.clear();
86
+ const p = provider.value;
87
+ if (!p || !_searchIndex) return;
88
+ for (let i = 0; i < batch.length; i++) {
89
+ const docId = batch[i];
90
+ try {
91
+ const childProvider = await p.loadChild(docId);
92
+ await childProvider.ready;
93
+ const rootDoc = p.document;
94
+ const treeMap = rootDoc.getMap("doc-tree");
95
+ const entry = treeMap.get(docId);
96
+ const texts = extractDocTexts(childProvider.document, entry?.label ?? "", entry?.meta);
97
+ await _searchIndex.index(docId, texts);
98
+ _indexedVersions.set(docId, _lastSyncedCache.get(docId) ?? 0);
99
+ _indexedCount.value = _indexedVersions.size;
100
+ } catch (e) {
101
+ if (import.meta.dev) console.warn(`[abracadabra] search: failed to index doc ${docId}:`, e);
102
+ }
103
+ if (i % 5 === 4) await new Promise((r) => setTimeout(r, 0));
104
+ }
105
+ } finally {
106
+ isProcessing = false;
107
+ if (pendingDocIds.size > 0) debouncedProcess();
108
+ }
109
+ }
110
+ const debouncedProcess = useDebounceFn(processPending, 500);
111
+ const stop = watch(syncStates, (states) => {
75
112
  if (!_searchIndex) return;
76
- const p = provider.value;
77
- if (!p) return;
78
113
  for (const [docId, state] of states.entries()) {
79
114
  if (state.status !== "synced") continue;
80
115
  const lastSynced = state.lastSynced ?? 0;
81
116
  if (_indexedVersions.get(docId) === lastSynced) continue;
82
- try {
83
- const childProvider = await p.loadChild(docId);
84
- await childProvider.ready;
85
- const rootDoc = p.document;
86
- const treeMap = rootDoc.getMap("doc-tree");
87
- const entry = treeMap.get(docId);
88
- const texts = extractDocTexts(childProvider.document, entry?.label ?? "", entry?.meta);
89
- await _searchIndex.index(docId, texts);
90
- _indexedVersions.set(docId, lastSynced);
91
- _indexedCount.value = _indexedVersions.size;
92
- } catch (e) {
93
- if (import.meta.dev) console.warn(`[abracadabra] search: failed to index doc ${docId}:`, e);
94
- }
117
+ _lastSyncedCache.set(docId, lastSynced);
118
+ pendingDocIds.add(docId);
95
119
  }
120
+ if (pendingDocIds.size > 0) debouncedProcess();
96
121
  }, { deep: false });
97
122
  _stopWatcher = stop;
98
123
  }
@@ -0,0 +1,45 @@
1
+ import type { Ref } from 'vue';
2
+ import type { TreeEntry } from './useChildTree.js';
3
+ export interface SlidePosition {
4
+ groupIndex: number;
5
+ subIndex: number;
6
+ }
7
+ export interface SlidesNavigationOptions {
8
+ tree: {
9
+ childrenOf: (parentId: string | null) => TreeEntry[];
10
+ treeMap: any;
11
+ };
12
+ docId: string;
13
+ setLocalState: (state: any) => void;
14
+ followingUser: () => string | null | undefined;
15
+ states: Ref<any[]>;
16
+ }
17
+ export declare function useSlidesNavigation(options: SlidesNavigationOptions): {
18
+ position: Ref<{
19
+ groupIndex: number;
20
+ subIndex: number;
21
+ }, SlidePosition | {
22
+ groupIndex: number;
23
+ subIndex: number;
24
+ }>;
25
+ currentSlideId: import("vue").ComputedRef<string | null>;
26
+ currentSlide: import("vue").ComputedRef<TreeEntry | null>;
27
+ canGoLeft: import("vue").ComputedRef<boolean>;
28
+ canGoRight: import("vue").ComputedRef<boolean>;
29
+ canGoUp: import("vue").ComputedRef<boolean>;
30
+ canGoDown: import("vue").ComputedRef<boolean>;
31
+ goLeft: () => void;
32
+ goRight: () => void;
33
+ goUp: () => void;
34
+ goDown: () => void;
35
+ goTo: (pos: SlidePosition) => void;
36
+ goNext: () => void;
37
+ goPrev: () => void;
38
+ groups: import("vue").ComputedRef<TreeEntry[]>;
39
+ subSlidesOf: (groupId: string) => TreeEntry[];
40
+ currentNumber: import("vue").ComputedRef<number>;
41
+ totalSlides: import("vue").ComputedRef<number>;
42
+ currentTransition: import("vue").ComputedRef<string>;
43
+ lastDirection: Ref<"left" | "right" | "up" | "down", "left" | "right" | "up" | "down">;
44
+ adjacentSlideIds: import("vue").ComputedRef<string[]>;
45
+ };
@@ -0,0 +1,185 @@
1
+ import { ref, computed, watch, nextTick, onBeforeUnmount } from "vue";
2
+ export function useSlidesNavigation(options) {
3
+ const { tree, setLocalState, followingUser, states } = options;
4
+ const position = ref({ groupIndex: 0, subIndex: 0 });
5
+ let syncingFromFollow = false;
6
+ let lastAppliedT = 0;
7
+ const lastDirection = ref("right");
8
+ const groups = computed(() => tree.childrenOf(null));
9
+ function subSlidesOf(groupId) {
10
+ return tree.childrenOf(groupId);
11
+ }
12
+ const currentSlideId = computed(() => {
13
+ const g = groups.value[position.value.groupIndex];
14
+ if (!g) return null;
15
+ if (position.value.subIndex === 0) return g.id;
16
+ const subs = subSlidesOf(g.id);
17
+ const sub = subs[position.value.subIndex - 1];
18
+ return sub?.id ?? g.id;
19
+ });
20
+ const currentSlide = computed(() => {
21
+ const g = groups.value[position.value.groupIndex];
22
+ if (!g) return null;
23
+ if (position.value.subIndex === 0) return g;
24
+ const subs = subSlidesOf(g.id);
25
+ return subs[position.value.subIndex - 1] ?? g;
26
+ });
27
+ const canGoLeft = computed(() => position.value.groupIndex > 0);
28
+ const canGoRight = computed(() => position.value.groupIndex < groups.value.length - 1);
29
+ const canGoUp = computed(() => position.value.subIndex > 0);
30
+ const canGoDown = computed(() => {
31
+ const g = groups.value[position.value.groupIndex];
32
+ if (!g) return false;
33
+ const subs = subSlidesOf(g.id);
34
+ return position.value.subIndex < subs.length;
35
+ });
36
+ function goLeft() {
37
+ if (!canGoLeft.value) return;
38
+ lastDirection.value = "left";
39
+ position.value = { groupIndex: position.value.groupIndex - 1, subIndex: 0 };
40
+ broadcastPosition();
41
+ }
42
+ function goRight() {
43
+ if (!canGoRight.value) return;
44
+ lastDirection.value = "right";
45
+ position.value = { groupIndex: position.value.groupIndex + 1, subIndex: 0 };
46
+ broadcastPosition();
47
+ }
48
+ function goUp() {
49
+ if (!canGoUp.value) return;
50
+ lastDirection.value = "up";
51
+ position.value = { ...position.value, subIndex: position.value.subIndex - 1 };
52
+ broadcastPosition();
53
+ }
54
+ function goDown() {
55
+ if (!canGoDown.value) return;
56
+ lastDirection.value = "down";
57
+ position.value = { ...position.value, subIndex: position.value.subIndex + 1 };
58
+ broadcastPosition();
59
+ }
60
+ function goTo(pos) {
61
+ const gi = Math.max(0, Math.min(pos.groupIndex, groups.value.length - 1));
62
+ const g = groups.value[gi];
63
+ const maxSub = g ? subSlidesOf(g.id).length : 0;
64
+ const si = Math.max(0, Math.min(pos.subIndex, maxSub));
65
+ position.value = { groupIndex: gi, subIndex: si };
66
+ broadcastPosition();
67
+ }
68
+ function goNext() {
69
+ if (canGoDown.value) goDown();
70
+ else if (canGoRight.value) goRight();
71
+ }
72
+ function goPrev() {
73
+ if (canGoUp.value) goUp();
74
+ else if (canGoLeft.value) {
75
+ lastDirection.value = "left";
76
+ const newGi = position.value.groupIndex - 1;
77
+ const g = groups.value[newGi];
78
+ const maxSub = g ? subSlidesOf(g.id).length : 0;
79
+ position.value = { groupIndex: newGi, subIndex: maxSub };
80
+ broadcastPosition();
81
+ }
82
+ }
83
+ const totalSlides = computed(() => {
84
+ let count = 0;
85
+ for (const g of groups.value) {
86
+ count += 1 + subSlidesOf(g.id).length;
87
+ }
88
+ return count;
89
+ });
90
+ const currentNumber = computed(() => {
91
+ let n = 0;
92
+ for (let i = 0; i < groups.value.length; i++) {
93
+ const g = groups.value[i];
94
+ if (i < position.value.groupIndex) {
95
+ n += 1 + subSlidesOf(g.id).length;
96
+ } else if (i === position.value.groupIndex) {
97
+ n += position.value.subIndex + 1;
98
+ break;
99
+ }
100
+ }
101
+ return n;
102
+ });
103
+ const currentTransition = computed(() => {
104
+ return currentSlide.value?.meta?.slidesTransition ?? "fade";
105
+ });
106
+ const adjacentSlideIds = computed(() => {
107
+ const ids = [];
108
+ const gi = position.value.groupIndex;
109
+ const si = position.value.subIndex;
110
+ if (gi > 0) ids.push(groups.value[gi - 1].id);
111
+ if (gi < groups.value.length - 1) ids.push(groups.value[gi + 1].id);
112
+ const g = groups.value[gi];
113
+ if (g) {
114
+ const subs = subSlidesOf(g.id);
115
+ if (si === 1) ids.push(g.id);
116
+ else if (si > 1 && subs[si - 2]) ids.push(subs[si - 2].id);
117
+ if (si === 0 && subs.length > 0) ids.push(subs[0].id);
118
+ else if (si > 0 && subs[si]) ids.push(subs[si].id);
119
+ }
120
+ return ids;
121
+ });
122
+ function broadcastPosition() {
123
+ if (syncingFromFollow || followingUser()) return;
124
+ setLocalState({
125
+ "slides:position": {
126
+ groupIndex: position.value.groupIndex,
127
+ subIndex: position.value.subIndex,
128
+ t: Date.now()
129
+ }
130
+ });
131
+ }
132
+ watch([followingUser, states], () => {
133
+ const key = followingUser();
134
+ if (!key) return;
135
+ const state = states.value.find((s) => s.user?.publicKey === key);
136
+ const pos = state?.["slides:position"];
137
+ if (!pos || pos.t <= lastAppliedT) return;
138
+ lastAppliedT = pos.t;
139
+ syncingFromFollow = true;
140
+ goTo({ groupIndex: pos.groupIndex, subIndex: pos.subIndex });
141
+ nextTick(() => {
142
+ syncingFromFollow = false;
143
+ });
144
+ });
145
+ watch(groups, (gs) => {
146
+ if (gs.length === 0) {
147
+ position.value = { groupIndex: 0, subIndex: 0 };
148
+ return;
149
+ }
150
+ const gi = Math.min(position.value.groupIndex, gs.length - 1);
151
+ const g = gs[gi];
152
+ const maxSub = g ? subSlidesOf(g.id).length : 0;
153
+ const si = Math.min(position.value.subIndex, maxSub);
154
+ if (gi !== position.value.groupIndex || si !== position.value.subIndex) {
155
+ position.value = { groupIndex: gi, subIndex: si };
156
+ }
157
+ });
158
+ broadcastPosition();
159
+ onBeforeUnmount(() => {
160
+ setLocalState({ "slides:position": null });
161
+ });
162
+ return {
163
+ position,
164
+ currentSlideId,
165
+ currentSlide,
166
+ canGoLeft,
167
+ canGoRight,
168
+ canGoUp,
169
+ canGoDown,
170
+ goLeft,
171
+ goRight,
172
+ goUp,
173
+ goDown,
174
+ goTo,
175
+ goNext,
176
+ goPrev,
177
+ groups,
178
+ subSlidesOf,
179
+ currentNumber,
180
+ totalSlides,
181
+ currentTransition,
182
+ lastDirection,
183
+ adjacentSlideIds
184
+ };
185
+ }
@@ -4,7 +4,7 @@
4
4
  * IMPORTANT: All `doc` and `provider` parameters must be ShallowRef.
5
5
  * Never pass plain refs or reactive() wrappers — Vue's deep proxy breaks Yjs internals.
6
6
  *
7
- * Ported from cou-sh/app/composables/useYDoc.ts with minor cleanup.
7
+ * Ported from cou-sh/app/composables/useYDoc.ts.
8
8
  * Three.js multiplayer utilities (useMultiplayerObject, useSmoothedCursors) are
9
9
  * included for parity but require Three.js in the consuming app.
10
10
  */
@@ -4,6 +4,7 @@ import {
4
4
  shallowRef,
5
5
  computed,
6
6
  watch,
7
+ nextTick,
7
8
  onMounted,
8
9
  onBeforeUnmount
9
10
  } from "vue";
@@ -11,11 +12,17 @@ export function useSyncedMap(doc, mapName) {
11
12
  const state = reactive({});
12
13
  const ymap = shallowRef(null);
13
14
  const lastUpdateLocal = shallowRef(true);
15
+ function readVal(map, key) {
16
+ const raw = map.get(key);
17
+ if (raw == null) return void 0;
18
+ return raw && typeof raw === "object" && "toJSON" in raw && typeof raw.toJSON === "function" && raw._map !== void 0 ? raw.toJSON() : raw;
19
+ }
14
20
  const observer = (event) => {
15
21
  lastUpdateLocal.value = event.transaction.local;
16
22
  event.keysChanged.forEach((key) => {
17
- if (ymap.value.has(key)) state[key] = ymap.value.get(key);
18
- else delete state[key];
23
+ if (ymap.value.has(key)) {
24
+ state[key] = readVal(ymap.value, key);
25
+ } else delete state[key];
19
26
  });
20
27
  };
21
28
  const init = (newDoc) => {
@@ -23,9 +30,20 @@ export function useSyncedMap(doc, mapName) {
23
30
  ymap.value = newDoc.getMap(mapName);
24
31
  for (const key in state) delete state[key];
25
32
  ymap.value.forEach((value, key) => {
26
- state[key] = value;
33
+ state[key] = value && typeof value === "object" && "toJSON" in value && typeof value.toJSON === "function" && value._map !== void 0 ? value.toJSON() : value;
27
34
  });
28
35
  ymap.value.observe(observer);
36
+ nextTick(() => {
37
+ if (!ymap.value) return;
38
+ const map = ymap.value;
39
+ map.forEach((value, key) => {
40
+ const v = value && typeof value === "object" && "toJSON" in value && typeof value.toJSON === "function" && value._map !== void 0 ? value.toJSON() : value;
41
+ if (state[key] !== v) state[key] = v;
42
+ });
43
+ for (const key in state) {
44
+ if (!map.has(key)) delete state[key];
45
+ }
46
+ });
29
47
  };
30
48
  watch(doc, (d) => d && init(d), { immediate: true });
31
49
  onBeforeUnmount(() => {
@@ -37,7 +55,7 @@ export function useSyncedMap(doc, mapName) {
37
55
  set: (key, value) => ymap.value?.set(key, value),
38
56
  remove: (key) => ymap.value?.delete(key),
39
57
  clear: () => ymap.value?.clear(),
40
- get: (key) => ymap.value?.get(key),
58
+ get: (key) => ymap.value ? readVal(ymap.value, key) : void 0,
41
59
  yMap: computed(() => ymap.value)
42
60
  };
43
61
  }
@@ -63,6 +81,10 @@ export function useSyncedArray(doc, arrayName) {
63
81
  yarray.value = newDoc.getArray(arrayName);
64
82
  list.value = yarray.value.toArray();
65
83
  yarray.value.observe(observer);
84
+ nextTick(() => {
85
+ if (!yarray.value) return;
86
+ list.value = yarray.value.toArray();
87
+ });
66
88
  };
67
89
  watch(doc, (d) => d && init(d), { immediate: true });
68
90
  onBeforeUnmount(() => {
@@ -95,6 +117,10 @@ export function useSyncedText(doc, textName) {
95
117
  ytext.value = newDoc.getText(textName);
96
118
  text.value = ytext.value.toString();
97
119
  ytext.value.observe(observer);
120
+ nextTick(() => {
121
+ if (!ytext.value) return;
122
+ text.value = ytext.value.toString();
123
+ });
98
124
  };
99
125
  watch(doc, (d) => d && init(d), { immediate: true });
100
126
  onBeforeUnmount(() => {
@@ -118,20 +144,32 @@ export function useSyncedXml(doc, xmlName) {
118
144
  export function useAwarenessOf(provider) {
119
145
  const states = ref([]);
120
146
  const currentUser = ref(null);
121
- const update = () => {
122
- if (!provider.value) return;
147
+ let _rafId = null;
148
+ const flush = () => {
149
+ _rafId = null;
150
+ if (!provider.value?.awareness) return;
123
151
  const awareness = provider.value.awareness;
124
152
  states.value = Array.from(awareness.getStates().entries()).map(([id, state]) => ({ clientId: id, ...state }));
125
153
  const local = awareness.getLocalState();
126
154
  if (local) currentUser.value = { clientId: awareness.clientID, ...local };
127
155
  };
156
+ const update = () => {
157
+ if (_rafId === null) _rafId = requestAnimationFrame(flush);
158
+ };
128
159
  watch(provider, (p, oldP) => {
129
- if (oldP) oldP.awareness.off("change", update);
130
- if (p) {
160
+ if (oldP?.awareness) oldP.awareness.off("change", update);
161
+ if (_rafId !== null) {
162
+ cancelAnimationFrame(_rafId);
163
+ _rafId = null;
164
+ }
165
+ if (p?.awareness) {
131
166
  p.awareness.on("change", update);
132
- update();
167
+ flush();
133
168
  }
134
169
  }, { immediate: true });
170
+ onBeforeUnmount(() => {
171
+ if (_rafId !== null) cancelAnimationFrame(_rafId);
172
+ });
135
173
  const setLocalState = (state) => {
136
174
  if (!provider.value) return;
137
175
  const awareness = provider.value.awareness;