@abraca/nuxt 2.25.0 → 2.27.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/module.d.mts CHANGED
@@ -32,6 +32,7 @@ declare module '@nuxt/schema' {
32
32
  entryDocId: string;
33
33
  pinServer: boolean;
34
34
  persistAuth: boolean;
35
+ deferConnect: boolean;
35
36
  authStorageKey: string;
36
37
  disabledBuiltins: string[];
37
38
  features: {
@@ -136,6 +137,20 @@ interface ModuleOptions {
136
137
  * Default: true.
137
138
  */
138
139
  persistAuth?: boolean;
140
+ /**
141
+ * Defer the SDK import + provider connection (auth challenge, WebSocket,
142
+ * initial sync) until after the app's first paint, instead of starting it
143
+ * during plugin setup. The heavy `@abraca/dabra` chunk and the network
144
+ * round-trips then stop competing with above-the-fold content for the main
145
+ * thread and the (often slow, mobile) network — a large LCP win on
146
+ * content-first sites that don't need live data above the fold.
147
+ *
148
+ * Live features (awareness, sync, chat) come online a beat after first
149
+ * paint rather than during it. Recommended for marketing / landing / docs
150
+ * sites; leave OFF for editor-first apps (dashboards) that want the provider
151
+ * connected as early as possible. Default: false.
152
+ */
153
+ deferConnect?: boolean;
139
154
  /**
140
155
  * localStorage key used by AbracadabraClient for token persistence.
141
156
  * Default: 'abracadabra:auth'
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": ">=4.0.0"
6
6
  },
7
- "version": "2.25.0",
7
+ "version": "2.27.0",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.2",
10
10
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -21,6 +21,7 @@ const module$1 = defineNuxtModule({
21
21
  url: process.env.ABRACADABRA_URL ?? "https://abra.cou.sh",
22
22
  pinServer: false,
23
23
  persistAuth: true,
24
+ deferConnect: false,
24
25
  authStorageKey: "abracadabra:auth",
25
26
  disabledBuiltins: [],
26
27
  features: {
@@ -90,6 +91,7 @@ const module$1 = defineNuxtModule({
90
91
  entryDocId: options.entryDocId ?? "",
91
92
  pinServer: options.pinServer ?? false,
92
93
  persistAuth: options.persistAuth,
94
+ deferConnect: options.deferConnect ?? false,
93
95
  authStorageKey: options.authStorageKey,
94
96
  disabledBuiltins: options.disabledBuiltins,
95
97
  features: options.features,
@@ -1,5 +1,6 @@
1
1
  <script setup>
2
2
  import { ref, computed, nextTick, shallowRef, toRaw, watch } from "vue";
3
+ import { useVirtualizer } from "@tanstack/vue-virtual";
3
4
  import { useAbracadabra, useTrash, useChat, useVoice, useToast, useAppConfig, useSyncedMap, useDocImport, useDocExport, useWindowManager, useAwareness } from "#imports";
4
5
  import { resolveDocType, getAvailableDocTypes } from "../utils/docTypes";
5
6
  import { avatarBorderStyle } from "../utils/avatarStyle";
@@ -225,6 +226,20 @@ const flatItems = computed(() => {
225
226
  }
226
227
  return result;
227
228
  });
229
+ const scrollParent = ref(null);
230
+ const ROW_HEIGHT = 32;
231
+ const rowVirtualizer = useVirtualizer(
232
+ computed(() => ({
233
+ count: flatItems.value.length,
234
+ getScrollElement: () => scrollParent.value,
235
+ estimateSize: () => ROW_HEIGHT,
236
+ overscan: 12,
237
+ getItemKey: (index) => flatItems.value[index]?.id ?? index
238
+ }))
239
+ );
240
+ const virtualRows = computed(
241
+ () => rowVirtualizer.value.getVirtualItems().map((vRow) => ({ vRow, item: flatItems.value[vRow.index], idx: vRow.index })).filter((r) => r.item)
242
+ );
228
243
  function getItemIcon(item) {
229
244
  const meta = item.meta;
230
245
  if (meta?.icon && typeof meta.icon === "string") return `i-lucide-${meta.icon}`;
@@ -983,6 +998,9 @@ function onTreeDragOver(e) {
983
998
  onImportDragOver(e);
984
999
  }
985
1000
  const focusedIndex = ref(-1);
1001
+ watch(focusedIndex, (i) => {
1002
+ if (i >= 0 && i < flatItems.value.length) rowVirtualizer.value.scrollToIndex(i, { align: "auto" });
1003
+ });
986
1004
  function onTreeKeydown(e) {
987
1005
  const items = flatItems.value;
988
1006
  if (!items.length) return;
@@ -1117,7 +1135,7 @@ defineExpose({
1117
1135
 
1118
1136
  <div
1119
1137
  v-else-if="!collapsed"
1120
- class="flex flex-col min-h-0"
1138
+ class="flex flex-col min-h-0 h-full"
1121
1139
  :class="externalDragActive ? 'ring-2 ring-inset ring-(--ui-primary)/30 rounded-(--ui-radius) bg-(--ui-primary)/3' : ''"
1122
1140
  tabindex="0"
1123
1141
  role="tree"
@@ -1171,36 +1189,40 @@ defineExpose({
1171
1189
 
1172
1190
  <ClientOnly>
1173
1191
  <template v-if="isReady">
1174
- <UEmpty
1175
- v-if="flatItems.length === 0"
1176
- icon="i-lucide-file-text"
1177
- title="No pages"
1178
- description="Create your first page."
1179
- size="sm"
1180
- />
1181
-
1182
- <TransitionGroup
1183
- v-else
1184
- name="tree-item"
1185
- tag="div"
1186
- :class="['px-1', { 'is-dragging': dragId }]"
1192
+ <div
1193
+ ref="scrollParent"
1194
+ class="flex-1 min-h-0 overflow-y-auto px-1"
1195
+ :class="{ 'is-dragging': dragId }"
1187
1196
  @dragover.prevent
1188
1197
  @dragleave="onListDragLeave"
1189
1198
  >
1199
+ <UEmpty
1200
+ v-if="flatItems.length === 0"
1201
+ icon="i-lucide-file-text"
1202
+ title="No pages"
1203
+ description="Create your first page."
1204
+ size="sm"
1205
+ />
1206
+
1190
1207
  <div
1191
- v-for="(item, idx) in flatItems"
1192
- :key="item.id"
1193
- class="tree-item relative"
1194
- :class="[
1208
+ v-else
1209
+ :style="{ height: `${rowVirtualizer.getTotalSize()}px`, position: 'relative', width: '100%' }"
1210
+ >
1211
+ <div
1212
+ v-for="{ vRow, item, idx } in virtualRows"
1213
+ :key="item.id"
1214
+ class="tree-item absolute left-0 right-0"
1215
+ :class="[
1195
1216
  dragId === item.id && !item.isTrashRoot ? 'opacity-30' : '',
1196
1217
  focusedIndex === idx ? 'ring-1 ring-inset ring-(--ui-primary)/50 rounded-sm' : ''
1197
1218
  ]"
1198
- :data-focused="focusedIndex === idx ? '' : void 0"
1199
- role="treeitem"
1200
- @dragover="onDragOver($event, item)"
1201
- @drop.stop="onDrop"
1202
- @click="focusedIndex = idx"
1203
- >
1219
+ :data-focused="focusedIndex === idx ? '' : void 0"
1220
+ :style="{ transform: `translateY(${vRow.start}px)`, height: `${vRow.size}px` }"
1221
+ role="treeitem"
1222
+ @dragover="onDragOver($event, item)"
1223
+ @drop.stop="onDrop"
1224
+ @click="focusedIndex = idx"
1225
+ >
1204
1226
  <!-- Drop indicator: line BEFORE -->
1205
1227
  <div
1206
1228
  v-if="dropIndicator?.targetId === item.id && dropIndicator.position === 'before' && !item.isTrashRoot"
@@ -1523,8 +1545,9 @@ defineExpose({
1523
1545
  />
1524
1546
  </UDropdownMenu>
1525
1547
  </div>
1548
+ </div>
1526
1549
  </div>
1527
- </TransitionGroup>
1550
+ </div>
1528
1551
 
1529
1552
  <!-- External drag hint bar -->
1530
1553
  <Transition name="drop-hint">
@@ -23,9 +23,23 @@ export function useChildTree(rootDoc, parentDocId, options) {
23
23
  }
24
24
  return result;
25
25
  });
26
+ const childrenByParent = computed(() => {
27
+ const map = /* @__PURE__ */ new Map();
28
+ for (const e of entries.value) {
29
+ let arr = map.get(e.parentId);
30
+ if (!arr) {
31
+ arr = [];
32
+ map.set(e.parentId, arr);
33
+ }
34
+ arr.push(e);
35
+ }
36
+ for (const arr of map.values()) arr.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
37
+ return map;
38
+ });
26
39
  function childrenOf(parentId) {
27
40
  const target = parentId === null ? parentDocId : parentId;
28
- return entries.value.filter((e) => e.parentId === target).sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
41
+ const bucket = childrenByParent.value.get(target);
42
+ return bucket ? bucket.slice() : [];
29
43
  }
30
44
  function descendantsOf(id) {
31
45
  const result = [];
@@ -210,6 +210,7 @@ export default defineNuxtPlugin({
210
210
  const debug = abraConfig.debug ?? false;
211
211
  const configEntryDocId = abraConfig.entryDocId || void 0;
212
212
  const pinServer = abraConfig.pinServer ?? false;
213
+ const deferConnect = abraConfig.deferConnect ?? false;
213
214
  const registry = new PluginRegistry();
214
215
  const storedDisabledBuiltins = JSON.parse(
215
216
  localStorage.getItem(STORAGE_KEY_DISABLED_BUILTINS) ?? "[]"
@@ -1512,7 +1513,26 @@ export default defineNuxtPlugin({
1512
1513
  initialUrl = currentServerUrl.value || savedServers.value[0]?.url || defaultUrl;
1513
1514
  }
1514
1515
  if (initialUrl !== currentServerUrl.value) currentServerUrl.value = initialUrl;
1515
- _initPromise = init(initialUrl);
1516
+ if (deferConnect) {
1517
+ _initPromise = new Promise((resolve, reject) => {
1518
+ let started = false;
1519
+ const run = () => {
1520
+ if (started) return;
1521
+ started = true;
1522
+ init(initialUrl).then(resolve, reject);
1523
+ };
1524
+ nuxtApp.hooks.hookOnce("app:suspense:resolve", () => {
1525
+ if (typeof requestIdleCallback === "function") {
1526
+ requestIdleCallback(run, { timeout: 2e3 });
1527
+ } else {
1528
+ setTimeout(run, 200);
1529
+ }
1530
+ });
1531
+ setTimeout(run, 3e3);
1532
+ });
1533
+ } else {
1534
+ _initPromise = init(initialUrl);
1535
+ }
1516
1536
  if (!pinServer && deepLinkServer && !savedServers.value.some((s) => s.url === deepLinkServer)) {
1517
1537
  void addServer(deepLinkServer).catch(() => {
1518
1538
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abraca/nuxt",
3
- "version": "2.25.0",
3
+ "version": "2.27.0",
4
4
  "description": "First-class Nuxt module for the Abracadabra CRDT collaboration platform",
5
5
  "repository": "abracadabra/abracadabra-nuxt",
6
6
  "license": "MIT",