@abraca/nuxt 2.0.10 → 2.3.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 (126) hide show
  1. package/dist/module.d.mts +68 -0
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +99 -4
  4. package/dist/runtime/components/ACodeEditor.d.vue.ts +26 -0
  5. package/dist/runtime/components/ACodeEditor.vue +268 -0
  6. package/dist/runtime/components/ACodeEditor.vue.d.ts +26 -0
  7. package/dist/runtime/components/ADocumentTree.vue +52 -20
  8. package/dist/runtime/components/AEditor.d.vue.ts +20 -13
  9. package/dist/runtime/components/AEditor.vue +55 -2
  10. package/dist/runtime/components/AEditor.vue.d.ts +20 -13
  11. package/dist/runtime/components/ANodePanel.vue +64 -60
  12. package/dist/runtime/components/ANotificationBell.d.vue.ts +1 -1
  13. package/dist/runtime/components/ANotificationBell.vue.d.ts +1 -1
  14. package/dist/runtime/components/ASpaceFormModal.d.vue.ts +2 -2
  15. package/dist/runtime/components/ASpaceFormModal.vue.d.ts +2 -2
  16. package/dist/runtime/components/aware/APresenceBlobs.d.vue.ts +29 -1
  17. package/dist/runtime/components/aware/APresenceBlobs.vue +54 -8
  18. package/dist/runtime/components/aware/APresenceBlobs.vue.d.ts +29 -1
  19. package/dist/runtime/components/aware/APresenceCursors.d.vue.ts +11 -0
  20. package/dist/runtime/components/aware/APresenceCursors.vue +74 -9
  21. package/dist/runtime/components/aware/APresenceCursors.vue.d.ts +11 -0
  22. package/dist/runtime/components/aware/AToggleGroup.d.vue.ts +28 -13
  23. package/dist/runtime/components/aware/AToggleGroup.vue +56 -20
  24. package/dist/runtime/components/aware/AToggleGroup.vue.d.ts +28 -13
  25. package/dist/runtime/components/docs/ADocsNavigation.d.vue.ts +1 -1
  26. package/dist/runtime/components/docs/ADocsNavigation.vue.d.ts +1 -1
  27. package/dist/runtime/components/docs/ADocsSearchButton.d.vue.ts +1 -1
  28. package/dist/runtime/components/docs/ADocsSearchButton.vue.d.ts +1 -1
  29. package/dist/runtime/components/docs/ADocsToc.d.vue.ts +2 -2
  30. package/dist/runtime/components/docs/ADocsToc.vue.d.ts +2 -2
  31. package/dist/runtime/components/editor/AEditorRedoButton.d.vue.ts +1 -1
  32. package/dist/runtime/components/editor/AEditorRedoButton.vue.d.ts +1 -1
  33. package/dist/runtime/components/editor/AEditorUndoButton.d.vue.ts +1 -1
  34. package/dist/runtime/components/editor/AEditorUndoButton.vue.d.ts +1 -1
  35. package/dist/runtime/components/editor/ANodeInlineLabel.d.vue.ts +1 -1
  36. package/dist/runtime/components/editor/ANodeInlineLabel.vue.d.ts +1 -1
  37. package/dist/runtime/components/registry/APluginBrowser.d.vue.ts +23 -0
  38. package/dist/runtime/components/registry/APluginBrowser.vue +155 -0
  39. package/dist/runtime/components/registry/APluginBrowser.vue.d.ts +23 -0
  40. package/dist/runtime/components/registry/APluginCapabilityDialog.d.vue.ts +17 -0
  41. package/dist/runtime/components/registry/APluginCapabilityDialog.vue +159 -0
  42. package/dist/runtime/components/registry/APluginCapabilityDialog.vue.d.ts +17 -0
  43. package/dist/runtime/components/registry/APluginCard.d.vue.ts +20 -0
  44. package/dist/runtime/components/registry/APluginCard.vue +91 -0
  45. package/dist/runtime/components/registry/APluginCard.vue.d.ts +20 -0
  46. package/dist/runtime/components/registry/APluginDetail.d.vue.ts +18 -0
  47. package/dist/runtime/components/registry/APluginDetail.vue +252 -0
  48. package/dist/runtime/components/registry/APluginDetail.vue.d.ts +18 -0
  49. package/dist/runtime/components/renderers/ACodeRenderer.d.vue.ts +15 -0
  50. package/dist/runtime/components/renderers/ACodeRenderer.vue +68 -0
  51. package/dist/runtime/components/renderers/ACodeRenderer.vue.d.ts +15 -0
  52. package/dist/runtime/components/renderers/AGraphRenderer.vue +416 -120
  53. package/dist/runtime/components/renderers/AProseRenderer.d.vue.ts +2 -2
  54. package/dist/runtime/components/renderers/AProseRenderer.vue.d.ts +2 -2
  55. package/dist/runtime/components/shell/ABreadcrumbForDoc.d.vue.ts +11 -0
  56. package/dist/runtime/components/shell/ABreadcrumbForDoc.vue +16 -0
  57. package/dist/runtime/components/shell/ABreadcrumbForDoc.vue.d.ts +11 -0
  58. package/dist/runtime/components/shell/ASettingsSection.d.vue.ts +35 -0
  59. package/dist/runtime/components/shell/ASettingsSection.vue +26 -0
  60. package/dist/runtime/components/shell/ASettingsSection.vue.d.ts +35 -0
  61. package/dist/runtime/components/shell/ASidebar.d.vue.ts +1 -1
  62. package/dist/runtime/components/shell/ASidebar.vue.d.ts +1 -1
  63. package/dist/runtime/components/shell/AUserMenu.d.vue.ts +3 -0
  64. package/dist/runtime/components/shell/AUserMenu.vue +4 -0
  65. package/dist/runtime/components/shell/AUserMenu.vue.d.ts +3 -0
  66. package/dist/runtime/composables/useAbracadabraSchema.d.ts +83 -0
  67. package/dist/runtime/composables/useAbracadabraSchema.js +52 -0
  68. package/dist/runtime/composables/useAggregatedPresence.d.ts +1 -6
  69. package/dist/runtime/composables/useCalendarView.d.ts +1 -1
  70. package/dist/runtime/composables/useChat.js +1 -0
  71. package/dist/runtime/composables/useDocBreadcrumb.d.ts +21 -0
  72. package/dist/runtime/composables/useDocBreadcrumb.js +33 -0
  73. package/dist/runtime/composables/useDocEntryTyped.d.ts +60 -0
  74. package/dist/runtime/composables/useDocEntryTyped.js +70 -0
  75. package/dist/runtime/composables/useEditorDragHandle.js +18 -0
  76. package/dist/runtime/composables/useEditorSuggestions.js +2 -1
  77. package/dist/runtime/composables/useInstalledPlugins.d.ts +3 -21
  78. package/dist/runtime/composables/useInstalledPlugins.js +2 -12
  79. package/dist/runtime/composables/useMetaMenuItems.d.ts +21 -0
  80. package/dist/runtime/composables/useMetaMenuItems.js +115 -0
  81. package/dist/runtime/composables/useMetaValidator.d.ts +27 -0
  82. package/dist/runtime/composables/useMetaValidator.js +10 -0
  83. package/dist/runtime/composables/usePluginCatalog.d.ts +161 -0
  84. package/dist/runtime/composables/usePluginCatalog.js +234 -0
  85. package/dist/runtime/composables/useQuery.d.ts +79 -0
  86. package/dist/runtime/composables/useQuery.js +97 -0
  87. package/dist/runtime/composables/useSpaces.js +4 -5
  88. package/dist/runtime/composables/useTableView.d.ts +3 -3
  89. package/dist/runtime/composables/useTypedDoc.d.ts +97 -0
  90. package/dist/runtime/composables/useTypedDoc.js +114 -0
  91. package/dist/runtime/composables/useWebRTC.js +44 -5
  92. package/dist/runtime/extensions/document-meta.js +5 -0
  93. package/dist/runtime/extensions/timeline.d.ts +11 -0
  94. package/dist/runtime/extensions/timeline.js +52 -0
  95. package/dist/runtime/extensions/views/DocumentMetaView.d.vue.ts +4 -0
  96. package/dist/runtime/extensions/views/DocumentMetaView.vue +63 -0
  97. package/dist/runtime/extensions/views/DocumentMetaView.vue.d.ts +4 -0
  98. package/dist/runtime/extensions/views/TimelineItemView.d.vue.ts +4 -0
  99. package/dist/runtime/extensions/views/TimelineItemView.vue +131 -0
  100. package/dist/runtime/extensions/views/TimelineItemView.vue.d.ts +4 -0
  101. package/dist/runtime/extensions/views/TimelineView.d.vue.ts +9 -0
  102. package/dist/runtime/extensions/views/TimelineView.vue +29 -0
  103. package/dist/runtime/extensions/views/TimelineView.vue.d.ts +9 -0
  104. package/dist/runtime/locale.d.ts +2 -0
  105. package/dist/runtime/locale.js +2 -0
  106. package/dist/runtime/plugin-abracadabra.client.js +107 -6
  107. package/dist/runtime/plugin-registry.d.ts +11 -30
  108. package/dist/runtime/plugin-registry.js +2 -82
  109. package/dist/runtime/plugins/core.plugin.js +10 -4
  110. package/dist/runtime/server/api/_abracadabra/spaces.get.d.ts +1 -1
  111. package/dist/runtime/server/plugins/abracadabra-service.js +28 -0
  112. package/dist/runtime/server/utils/docCache.js +24 -3
  113. package/dist/runtime/server/utils/schemaServerSupport.d.ts +52 -0
  114. package/dist/runtime/server/utils/schemaServerSupport.js +51 -0
  115. package/dist/runtime/types.d.ts +63 -46
  116. package/dist/runtime/utils/docTypes.d.ts +15 -0
  117. package/dist/runtime/utils/docTypes.js +20 -0
  118. package/dist/runtime/utils/loadCodeMirror.d.ts +32 -0
  119. package/dist/runtime/utils/loadCodeMirror.js +65 -0
  120. package/dist/runtime/utils/markdownToYjs.d.ts +1 -23
  121. package/dist/runtime/utils/markdownToYjs.js +5 -440
  122. package/dist/runtime/utils/schemaSupport.d.ts +60 -0
  123. package/dist/runtime/utils/schemaSupport.js +40 -0
  124. package/dist/runtime/utils/yjsConvert.d.ts +1 -14
  125. package/dist/runtime/utils/yjsConvert.js +5 -331
  126. package/package.json +84 -23
@@ -0,0 +1,131 @@
1
+ <script setup>
2
+ import { computed, ref, watch } from "vue";
3
+ import { NodeViewWrapper, NodeViewContent } from "@tiptap/vue-3";
4
+ import ANodeInlineLabel from "../../components/editor/ANodeInlineLabel.vue";
5
+ import AIconPickerPopover from "../../components/editor/AIconPickerPopover.vue";
6
+ const props = defineProps({
7
+ decorations: { type: Array, required: true },
8
+ selected: { type: Boolean, required: true },
9
+ updateAttributes: { type: Function, required: true },
10
+ deleteNode: { type: Function, required: true },
11
+ node: { type: null, required: true },
12
+ view: { type: null, required: true },
13
+ getPos: { type: null, required: true },
14
+ innerDecorations: { type: null, required: true },
15
+ editor: { type: Object, required: true },
16
+ extension: { type: Object, required: true },
17
+ HTMLAttributes: { type: Object, required: true }
18
+ });
19
+ const date = computed(() => props.node.attrs.date || "");
20
+ const label = computed(() => props.node.attrs.label || "");
21
+ const icon = computed(() => props.node.attrs.icon || "");
22
+ const iconShort = computed(() => icon.value.replace(/^i-lucide-/, ""));
23
+ const iconPopoverOpen = ref(false);
24
+ const datePopoverOpen = ref(false);
25
+ const dateDraft = ref("");
26
+ watch(datePopoverOpen, (open) => {
27
+ if (open) dateDraft.value = date.value;
28
+ });
29
+ function setLabel(v) {
30
+ props.updateAttributes({ label: v });
31
+ }
32
+ function setIcon(name) {
33
+ props.updateAttributes({ icon: name ? `i-lucide-${name}` : "" });
34
+ iconPopoverOpen.value = false;
35
+ }
36
+ function commitDate() {
37
+ props.updateAttributes({ date: dateDraft.value.trim() });
38
+ datePopoverOpen.value = false;
39
+ }
40
+ function onDateKeydown(e) {
41
+ if (e.key === "Enter") {
42
+ e.preventDefault();
43
+ commitDate();
44
+ }
45
+ }
46
+ </script>
47
+
48
+ <template>
49
+ <NodeViewWrapper
50
+ as="div"
51
+ class="group/item relative ps-8 pb-5 last:pb-0"
52
+ >
53
+ <div
54
+ class="absolute left-0 top-1.5 size-4 rounded-full bg-(--ui-bg) border-2 border-(--ui-border-accented) flex items-center justify-center"
55
+ contenteditable="false"
56
+ >
57
+ <UIcon
58
+ v-if="icon"
59
+ :name="icon"
60
+ class="size-2.5 text-(--ui-primary)"
61
+ />
62
+ <div
63
+ v-else
64
+ class="size-1.5 rounded-full bg-(--ui-primary)"
65
+ />
66
+ </div>
67
+ <div
68
+ class="flex items-center gap-2 text-sm"
69
+ contenteditable="false"
70
+ >
71
+ <AIconPickerPopover
72
+ :model-value="iconShort"
73
+ :open="iconPopoverOpen"
74
+ @update:model-value="setIcon"
75
+ @update:open="iconPopoverOpen = $event"
76
+ >
77
+ <button
78
+ class="shrink-0 size-5 flex items-center justify-center rounded-sm hover:bg-(--ui-bg-elevated)/60 text-(--ui-text-muted)"
79
+ :title="icon ? 'Change icon' : 'Add icon'"
80
+ @click.stop="iconPopoverOpen = true"
81
+ >
82
+ <UIcon
83
+ :name="icon || 'i-lucide-plus'"
84
+ class="size-4"
85
+ :class="!icon ? 'opacity-0 group-hover/item:opacity-60 transition-opacity' : ''"
86
+ />
87
+ </button>
88
+ </AIconPickerPopover>
89
+ <ANodeInlineLabel
90
+ :model-value="label"
91
+ placeholder="Event"
92
+ variant="title"
93
+ @update:model-value="setLabel"
94
+ />
95
+ <UPopover
96
+ v-model:open="datePopoverOpen"
97
+ :content="{ align: 'end' }"
98
+ >
99
+ <button
100
+ class="text-xs text-(--ui-text-muted) hover:text-(--ui-text) rounded-sm hover:bg-(--ui-bg-elevated)/60 px-1.5 py-0.5 inline-flex items-center gap-1"
101
+ type="button"
102
+ >
103
+ <UIcon
104
+ name="i-lucide-calendar"
105
+ class="size-3"
106
+ />
107
+ <span v-if="date">{{ date }}</span>
108
+ <span
109
+ v-else
110
+ class="italic"
111
+ >Add date</span>
112
+ </button>
113
+ <template #content>
114
+ <div class="p-2 w-56 flex flex-col gap-1">
115
+ <UInput
116
+ v-model="dateDraft"
117
+ autofocus
118
+ size="sm"
119
+ placeholder="e.g. 2026-04-20 or April 2026"
120
+ @keydown="onDateKeydown"
121
+ @blur="commitDate"
122
+ />
123
+ </div>
124
+ </template>
125
+ </UPopover>
126
+ </div>
127
+ <div class="mt-1 text-sm text-(--ui-text-muted)">
128
+ <NodeViewContent class="[&>*:first-child]:mt-0 [&>*:last-child]:mb-0 [&>*]:my-1" />
129
+ </div>
130
+ </NodeViewWrapper>
131
+ </template>
@@ -0,0 +1,4 @@
1
+ import type { NodeViewProps } from '@tiptap/vue-3';
2
+ declare const __VLS_export: import("vue").DefineComponent<NodeViewProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<NodeViewProps> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
3
+ declare const _default: typeof __VLS_export;
4
+ export default _default;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * NodeView for the `timeline` block — renders the vertical guide line.
3
+ *
4
+ * Ported 1:1 from cou-sh/app/components/editor/TimelineView.vue.
5
+ */
6
+ import type { NodeViewProps } from '@tiptap/vue-3';
7
+ declare const __VLS_export: import("vue").DefineComponent<NodeViewProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<NodeViewProps> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
8
+ declare const _default: typeof __VLS_export;
9
+ export default _default;
@@ -0,0 +1,29 @@
1
+ <script setup>
2
+ import { NodeViewWrapper, NodeViewContent } from "@tiptap/vue-3";
3
+ defineProps({
4
+ decorations: { type: Array, required: true },
5
+ selected: { type: Boolean, required: true },
6
+ updateAttributes: { type: Function, required: true },
7
+ deleteNode: { type: Function, required: true },
8
+ node: { type: null, required: true },
9
+ view: { type: null, required: true },
10
+ getPos: { type: null, required: true },
11
+ innerDecorations: { type: null, required: true },
12
+ editor: { type: Object, required: true },
13
+ extension: { type: Object, required: true },
14
+ HTMLAttributes: { type: Object, required: true }
15
+ });
16
+ </script>
17
+
18
+ <template>
19
+ <NodeViewWrapper
20
+ as="div"
21
+ class="my-5 ms-2 relative"
22
+ >
23
+ <div
24
+ class="absolute left-2 top-2 bottom-2 w-px bg-(--ui-border)"
25
+ contenteditable="false"
26
+ />
27
+ <NodeViewContent />
28
+ </NodeViewWrapper>
29
+ </template>
@@ -0,0 +1,9 @@
1
+ /**
2
+ * NodeView for the `timeline` block — renders the vertical guide line.
3
+ *
4
+ * Ported 1:1 from cou-sh/app/components/editor/TimelineView.vue.
5
+ */
6
+ import type { NodeViewProps } from '@tiptap/vue-3';
7
+ declare const __VLS_export: import("vue").DefineComponent<NodeViewProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<NodeViewProps> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
8
+ declare const _default: typeof __VLS_export;
9
+ export default _default;
@@ -232,6 +232,8 @@ export interface AbracadabraLocale {
232
232
  graph: {
233
233
  addNode: string;
234
234
  empty: string;
235
+ loading: string;
236
+ untitled: string;
235
237
  pin: string;
236
238
  unpin: string;
237
239
  openAsDoc: string;
@@ -218,6 +218,8 @@ export const DEFAULT_LOCALE = {
218
218
  graph: {
219
219
  addNode: "Add node",
220
220
  empty: "No nodes yet",
221
+ loading: "Loading graph\u2026",
222
+ untitled: "New node",
221
223
  pin: "Pin position",
222
224
  unpin: "Unpin",
223
225
  openAsDoc: "Open as document",
@@ -130,6 +130,10 @@ function toNuxtUIPrimary(name) {
130
130
  function toNuxtUINeutral(name) {
131
131
  return NUXT_UI_NEUTRAL_COLORS.has(name) ? name : CUSTOM_TO_NUXT_UI_NEUTRAL[name] ?? "zinc";
132
132
  }
133
+ function hostHasCustomPalette(name) {
134
+ if (typeof document === "undefined") return false;
135
+ return getComputedStyle(document.documentElement).getPropertyValue(`--color-${name}-500`).trim() !== "";
136
+ }
133
137
  const STORAGE_KEY_EXTERNAL_PLUGINS = "abracadabra_external_plugins";
134
138
  const STORAGE_KEY_DISABLED_BUILTINS = "abracadabra_disabled_builtins";
135
139
  const CLAIMED_FLAG_KEY = "abracadabra_was_claimed";
@@ -238,10 +242,29 @@ export default defineNuxtPlugin({
238
242
  localStorage.setItem(STORAGE_KEY_EXTERNAL_PLUGINS, JSON.stringify(updated));
239
243
  }
240
244
  }
245
+ const { _collectSchemaRegistries } = await import("./composables/useAbracadabraSchema.js");
246
+ const { schemas: schemaRegistries, plugins: inlinePlugins } = await _collectSchemaRegistries(nuxtApp);
247
+ for (const plugin of inlinePlugins) {
248
+ registry.register(plugin);
249
+ }
241
250
  registry.freeze();
242
251
  if (debug) {
243
252
  console.log("[abracadabra] plugins:", registry.getPlugins().map((p) => p.name));
244
253
  }
254
+ const schemaCfg = abraConfig.schema ?? {};
255
+ if (debug && schemaRegistries.length > 0) {
256
+ console.log(
257
+ "[abracadabra] schema registries attached:",
258
+ schemaRegistries.map((r) => Array.from(r.types.keys())),
259
+ "validate=",
260
+ schemaCfg.validate,
261
+ "migrateOnRead=",
262
+ schemaCfg.migrateOnRead
263
+ );
264
+ }
265
+ if (schemaCfg.bundleDir && debug) {
266
+ console.log("[abracadabra] server bundle_dir advertised:", schemaCfg.bundleDir);
267
+ }
245
268
  const doc = shallowRef(new Y.Doc());
246
269
  const provider = shallowRef(null);
247
270
  const client = shallowRef(null);
@@ -300,6 +323,7 @@ export default defineNuxtPlugin({
300
323
  let _serversLoaded = false;
301
324
  let _initPromise = null;
302
325
  let _wsp = null;
326
+ let _tokenManager = null;
303
327
  let authFailureCount = 0;
304
328
  const AUTH_FAILURE_LIMIT = 3;
305
329
  let lastClientIds = /* @__PURE__ */ new Set();
@@ -421,14 +445,18 @@ export default defineNuxtPlugin({
421
445
  userColorName.value = colorName;
422
446
  localStorage.setItem("abracadabra_usercolor", colorName);
423
447
  const appConfig = useAppConfig();
424
- if (appConfig.ui?.colors) appConfig.ui.colors.primary = toNuxtUIPrimary(colorName);
448
+ if (appConfig.ui?.colors) {
449
+ appConfig.ui.colors.primary = hostHasCustomPalette(colorName) ? colorName : toNuxtUIPrimary(colorName);
450
+ }
425
451
  publishAwarenessUser();
426
452
  }
427
453
  function setNeutralColor(colorName) {
428
454
  userNeutralColorName.value = colorName;
429
455
  localStorage.setItem("abracadabra_neutralcolor", colorName);
430
456
  const appConfig = useAppConfig();
431
- if (appConfig.ui?.colors) appConfig.ui.colors.neutral = toNuxtUINeutral(colorName);
457
+ if (appConfig.ui?.colors) {
458
+ appConfig.ui.colors.neutral = hostHasCustomPalette(colorName) ? colorName : toNuxtUINeutral(colorName);
459
+ }
432
460
  }
433
461
  function setUserStatusIcon(icon) {
434
462
  const normalized = icon.replace(/^i-lucide-/, "");
@@ -474,6 +502,13 @@ export default defineNuxtPlugin({
474
502
  }
475
503
  provider.value = null;
476
504
  }
505
+ if (_tokenManager) {
506
+ try {
507
+ _tokenManager.dispose();
508
+ } catch {
509
+ }
510
+ _tokenManager = null;
511
+ }
477
512
  client.value = null;
478
513
  keystore.value = null;
479
514
  isReady.value = false;
@@ -537,6 +572,7 @@ export default defineNuxtPlugin({
537
572
  addLog("No active session \u2014 registering passkey from scratch", "auth");
538
573
  await client.value.registerWithKey({
539
574
  publicKey: newPubKey,
575
+ x25519Key: x25519PublicKey,
540
576
  deviceName,
541
577
  displayName: userName.value,
542
578
  inviteCode: pendingInviteCode.value ?? void 0
@@ -623,6 +659,15 @@ export default defineNuxtPlugin({
623
659
  }
624
660
  async function logout() {
625
661
  addLog("Clearing identity...", "auth");
662
+ const sessionId = _tokenManager?.currentSession?.sessionId;
663
+ if (client.value && sessionId) {
664
+ try {
665
+ await client.value.revokeDeviceSession(sessionId);
666
+ } catch (e) {
667
+ if (import.meta.dev) console.warn("[abracadabra] revoke device session failed", e);
668
+ }
669
+ }
670
+ _tokenManager?.clearSession();
626
671
  if (client.value) client.value.logout();
627
672
  if (keystore.value) await keystore.value.clear();
628
673
  localStorage.removeItem("abracadabra_privkey");
@@ -783,7 +828,7 @@ export default defineNuxtPlugin({
783
828
  import("@noble/hashes/sha2.js")
784
829
  ]);
785
830
  setSdkModule(sdkModule);
786
- const { AbracadabraClient, AbracadabraProvider, AbracadabraWS, CryptoIdentityKeystore } = sdkModule;
831
+ const { AbracadabraClient, AbracadabraProvider, AbracadabraWS, CryptoIdentityKeystore, TokenManager, LocalStorageDeviceSessionStorage } = sdkModule;
787
832
  const ed = edModule.default ?? edModule;
788
833
  if (!ed.hashes) {
789
834
  throw new Error("@noble/ed25519 v3: `hashes` export missing from imported namespace \u2014 check Vite optimizeDeps");
@@ -827,24 +872,38 @@ export default defineNuxtPlugin({
827
872
  const storedColor = localStorage.getItem("abracadabra_usercolor");
828
873
  const storedNeutral = localStorage.getItem("abracadabra_neutralcolor");
829
874
  const derived = deriveColorFromPubKey(pubKey);
875
+ const appConfig = useAppConfig();
876
+ const fromAppConfigPrimary = appConfig.ui?.colors?.primary;
877
+ const fromAppConfigNeutral = appConfig.ui?.colors?.neutral;
878
+ let primaryFromHost = false;
879
+ let neutralFromHost = false;
830
880
  if (storedColor && UI_COLORS.includes(storedColor)) {
831
881
  const h = COLOR_HUES[storedColor] ?? 0;
832
882
  userColor.value = `hsl(${h}, 70%, 75%)`;
833
883
  userColorName.value = storedColor;
884
+ primaryFromHost = true;
885
+ } else if (fromAppConfigPrimary && UI_COLORS.includes(fromAppConfigPrimary)) {
886
+ const h = COLOR_HUES[fromAppConfigPrimary] ?? 0;
887
+ userColor.value = `hsl(${h}, 70%, 75%)`;
888
+ userColorName.value = fromAppConfigPrimary;
889
+ primaryFromHost = true;
834
890
  } else {
835
891
  userColor.value = derived.hsl;
836
892
  userColorName.value = derived.name;
837
893
  }
838
894
  if (storedNeutral && UI_NEUTRALS.includes(storedNeutral)) {
839
895
  userNeutralColorName.value = storedNeutral;
896
+ neutralFromHost = true;
897
+ } else if (fromAppConfigNeutral && UI_NEUTRALS.includes(fromAppConfigNeutral)) {
898
+ userNeutralColorName.value = fromAppConfigNeutral;
899
+ neutralFromHost = true;
840
900
  } else {
841
901
  userNeutralColorName.value = derived.neutralName;
842
902
  }
843
903
  try {
844
- const appConfig = useAppConfig();
845
904
  if (appConfig.ui?.colors) {
846
- appConfig.ui.colors.primary = toNuxtUIPrimary(userColorName.value);
847
- appConfig.ui.colors.neutral = toNuxtUINeutral(userNeutralColorName.value);
905
+ appConfig.ui.colors.primary = primaryFromHost || hostHasCustomPalette(userColorName.value) ? userColorName.value : toNuxtUIPrimary(userColorName.value);
906
+ appConfig.ui.colors.neutral = neutralFromHost || hostHasCustomPalette(userNeutralColorName.value) ? userNeutralColorName.value : toNuxtUINeutral(userNeutralColorName.value);
848
907
  }
849
908
  } catch {
850
909
  }
@@ -860,6 +919,24 @@ export default defineNuxtPlugin({
860
919
  const _client = new AbracadabraClient({ url: serverUrl, persistAuth, storageKey: authStorageKey });
861
920
  client.value = _client;
862
921
  addLog(`Server: ${serverUrl}`, "connection");
922
+ const tm = new TokenManager({
923
+ client: _client,
924
+ storage: new LocalStorageDeviceSessionStorage(`${authStorageKey}:device-session`)
925
+ });
926
+ _tokenManager = tm;
927
+ tm.on("refresh", () => addLog("JWT auto-refreshed via device session", "auth"));
928
+ tm.on("session-expired", () => {
929
+ addLog("Device session rejected \u2014 please re-authenticate", "auth");
930
+ if (identityState.value === "claimed") identityState.value = "needsReauth";
931
+ });
932
+ if (tm.hasSession && !_client.isTokenValid()) {
933
+ try {
934
+ await tm.bootstrap();
935
+ addLog("Refreshed JWT via stored device session", "auth");
936
+ } catch (e) {
937
+ addLog(`Device session refresh failed: ${e instanceof Error ? e.message : String(e)}`, "auth");
938
+ }
939
+ }
863
940
  let useExistingToken = _client.isTokenValid();
864
941
  if (useExistingToken) {
865
942
  try {
@@ -876,6 +953,7 @@ export default defineNuxtPlugin({
876
953
  }
877
954
  ;
878
955
  _client.token = null;
956
+ tm.clearSession();
879
957
  useExistingToken = false;
880
958
  }
881
959
  }
@@ -913,6 +991,23 @@ export default defineNuxtPlugin({
913
991
  }
914
992
  _client.updateMe({ displayName: userName.value }).catch(() => {
915
993
  });
994
+ if (_client.isTokenValid() && !tm.hasSession) {
995
+ try {
996
+ const sess = await _client.requestDeviceSession({
997
+ publicKey: pubKey,
998
+ deviceName: "Abracadabra Web (" + (navigator.platform || "Browser") + ")"
999
+ });
1000
+ await tm.attachSession({
1001
+ sessionId: sess.sessionId,
1002
+ sessionToken: sess.sessionToken,
1003
+ expiresAt: sess.expiresAt
1004
+ });
1005
+ addLog("Device session registered \u2014 JWT auto-refresh enabled", "auth");
1006
+ } catch (e) {
1007
+ const msg = e instanceof Error ? e.message : String(e);
1008
+ addLog(`Device session unavailable: ${msg.slice(0, 80)}`, "auth");
1009
+ }
1010
+ }
916
1011
  const [info, spaceDocs] = await Promise.all([
917
1012
  fetchServerInfo(serverUrl),
918
1013
  _client.listSpaces().catch((e) => {
@@ -967,6 +1062,12 @@ export default defineNuxtPlugin({
967
1062
  document: _doc,
968
1063
  websocketProvider: wsp,
969
1064
  client: _client,
1065
+ // Route every WS auth handshake (initial + reconnects) through
1066
+ // the TokenManager so an expired JWT is silently refreshed via
1067
+ // the device session before being sent. Without this, a tab
1068
+ // left open past the 4 h JWT TTL ends up reconnecting forever
1069
+ // with a dead token.
1070
+ token: () => tm.getValidToken(),
970
1071
  onStatus({ status: s }) {
971
1072
  if (s === status.value) return;
972
1073
  const oldStatus = status.value;
@@ -1,34 +1,15 @@
1
- import type { Extension } from '@tiptap/core';
2
- import type { AbracadabraPlugin, AbracadabraPageType, AbracadabraToolbarItem, AbracadabraSuggestionItem, AbracadabraDragHandleItem, AbracadabraMentionProvider, AbracadabraAwarenessContribution, AbracadabraCommandItem, AbracadabraNodePanelSlot, AbracadabraSettingsPanel, AbracadabraKeyboardShortcut, EditorPluginCtx, DragHandlePluginCtx, CommandPaletteCtx } from './types.js';
3
1
  /**
4
- * PluginRegistrymanages the lifecycle of AbracadabraPlugin instances.
2
+ * The module's plugin registry a thin wrapper around `@abraca/plugin`'s
3
+ * generic `PluginRegistry`, typed with `AbracadabraPlugin` so consumers get
4
+ * fully-typed aggregator returns (toolbar item type, page type, command
5
+ * palette item type, etc.) without casts.
5
6
  *
6
- * Instantiated once per Nuxt app inside the main client plugin.
7
- * After freezePluginRegistry() is called, no new plugins can be registered.
8
- * All aggregator methods are safe to call after freezing.
7
+ * The class extension is intentionally empty all behaviour lives in the
8
+ * shared base. We re-export the class under the historical name so existing
9
+ * imports (`import { PluginRegistry } from './plugin-registry'`) keep
10
+ * resolving.
9
11
  */
10
- export declare class PluginRegistry {
11
- private _plugins;
12
- private _frozen;
13
- register(plugin: AbracadabraPlugin): void;
14
- /** Lock the registry. Called after all plugins (built-in + external) are loaded. */
15
- freeze(): void;
16
- isFrozen(): boolean;
17
- getPlugins(): readonly AbracadabraPlugin[];
18
- getAllExtensions(): Extension[];
19
- /** Waits until all plugins with async extension bundles have finished loading. */
20
- waitForExtensions(): Promise<void>;
21
- getServerExtensions(): Extension[];
22
- getAllPageTypes(): Record<string, AbracadabraPageType>;
23
- getAllCustomHandlers(): Record<string, (...args: any[]) => any>;
24
- getAllToolbarItems(ctx: EditorPluginCtx): AbracadabraToolbarItem[][];
25
- getAllBubbleMenuItems(ctx: EditorPluginCtx): AbracadabraToolbarItem[][];
26
- getAllSuggestionItems(ctx: EditorPluginCtx): AbracadabraSuggestionItem[][];
27
- getAllDragHandleItems(ctx: DragHandlePluginCtx): AbracadabraDragHandleItem[][];
28
- getAllMentionProviders(): AbracadabraMentionProvider[];
29
- getAllAwarenessContributions(): AbracadabraAwarenessContribution[];
30
- getAllCommandPaletteItems(ctx: CommandPaletteCtx): Promise<AbracadabraCommandItem[]>;
31
- getAllNodePanelSlots(): AbracadabraNodePanelSlot[];
32
- getSettingsPanels(): AbracadabraSettingsPanel[];
33
- getAllKeyboardShortcuts(): AbracadabraKeyboardShortcut[];
12
+ import { PluginRegistry as BasePluginRegistry } from '@abraca/plugin';
13
+ import type { AbracadabraPlugin } from './types.js';
14
+ export declare class PluginRegistry extends BasePluginRegistry<AbracadabraPlugin> {
34
15
  }
@@ -1,83 +1,3 @@
1
- export class PluginRegistry {
2
- _plugins = [];
3
- _frozen = false;
4
- register(plugin) {
5
- if (this._frozen) {
6
- console.warn(`[abracadabra] Plugin registry frozen \u2014 cannot register "${plugin.name}"`);
7
- return;
8
- }
9
- if (this._plugins.some((p) => p.name === plugin.name)) {
10
- console.warn(`[abracadabra] Plugin "${plugin.name}" already registered`);
11
- return;
12
- }
13
- this._plugins.push(plugin);
14
- }
15
- /** Lock the registry. Called after all plugins (built-in + external) are loaded. */
16
- freeze() {
17
- this._frozen = true;
18
- }
19
- isFrozen() {
20
- return this._frozen;
21
- }
22
- getPlugins() {
23
- return this._plugins;
24
- }
25
- // ── Aggregated getters ───────────────────────────────────────────────────────
26
- getAllExtensions() {
27
- return this._plugins.flatMap((p) => p.extensions?.() ?? []);
28
- }
29
- /** Waits until all plugins with async extension bundles have finished loading. */
30
- async waitForExtensions() {
31
- await Promise.all(this._plugins.map((p) => p.extensionsReady ?? Promise.resolve()));
32
- }
33
- getServerExtensions() {
34
- return this._plugins.flatMap((p) => p.serverExtensions?.() ?? []);
35
- }
36
- getAllPageTypes() {
37
- const merged = {};
38
- for (const p of this._plugins) {
39
- if (p.pageTypes) Object.assign(merged, p.pageTypes);
40
- }
41
- return merged;
42
- }
43
- getAllCustomHandlers() {
44
- const merged = {};
45
- for (const p of this._plugins) {
46
- if (p.customHandlers) Object.assign(merged, p.customHandlers());
47
- }
48
- return merged;
49
- }
50
- getAllToolbarItems(ctx) {
51
- return this._plugins.flatMap((p) => p.toolbarItems?.(ctx) ?? []);
52
- }
53
- getAllBubbleMenuItems(ctx) {
54
- return this._plugins.flatMap((p) => p.bubbleMenuItems?.(ctx) ?? []);
55
- }
56
- getAllSuggestionItems(ctx) {
57
- return this._plugins.flatMap((p) => p.suggestionItems?.(ctx) ?? []);
58
- }
59
- getAllDragHandleItems(ctx) {
60
- return this._plugins.flatMap((p) => p.dragHandleItems?.(ctx) ?? []);
61
- }
62
- getAllMentionProviders() {
63
- return this._plugins.flatMap((p) => p.mentionProviders ?? []);
64
- }
65
- getAllAwarenessContributions() {
66
- return this._plugins.flatMap((p) => p.awarenessContributions ?? []);
67
- }
68
- async getAllCommandPaletteItems(ctx) {
69
- const results = await Promise.all(
70
- this._plugins.filter((p) => p.commandPaletteItems).map((p) => Promise.resolve(p.commandPaletteItems(ctx)))
71
- );
72
- return results.flat().filter((item) => !item.when || item.when(ctx));
73
- }
74
- getAllNodePanelSlots() {
75
- return this._plugins.flatMap((p) => p.nodePanelSlots ?? []);
76
- }
77
- getSettingsPanels() {
78
- return this._plugins.flatMap((p) => p.settingsPanel ? [p.settingsPanel] : []);
79
- }
80
- getAllKeyboardShortcuts() {
81
- return this._plugins.flatMap((p) => p.keyboardShortcuts ?? []);
82
- }
1
+ import { PluginRegistry as BasePluginRegistry } from "@abraca/plugin";
2
+ export class PluginRegistry extends BasePluginRegistry {
83
3
  }
@@ -16,7 +16,9 @@ const OPTIONAL_BUILTIN_EXTENSIONS = /* @__PURE__ */ new Set([
16
16
  "mathBlock",
17
17
  "mathInline",
18
18
  "diff",
19
- "svgEmbed"
19
+ "svgEmbed",
20
+ "timeline",
21
+ "timelineItem"
20
22
  ]);
21
23
  async function loadClientExtensions() {
22
24
  const [
@@ -73,7 +75,8 @@ async function loadClientExtensions() {
73
75
  { ColorSwatch },
74
76
  { MathBlock, MathInline },
75
77
  { Diff },
76
- { SvgEmbed }
78
+ { SvgEmbed },
79
+ { Timeline, TimelineItem }
77
80
  ] = await Promise.all([
78
81
  import("@tiptap/extension-task-list"),
79
82
  import("@tiptap/extension-task-item"),
@@ -128,7 +131,8 @@ async function loadClientExtensions() {
128
131
  import("../extensions/color-swatch.js"),
129
132
  import("../extensions/math.js"),
130
133
  import("../extensions/diff.js"),
131
- import("../extensions/svg-embed.js")
134
+ import("../extensions/svg-embed.js"),
135
+ import("../extensions/timeline.js")
132
136
  ]);
133
137
  const lowlight = createLowlight(common);
134
138
  const extensions = [
@@ -202,7 +206,9 @@ async function loadClientExtensions() {
202
206
  MathBlock,
203
207
  MathInline,
204
208
  Diff,
205
- SvgEmbed
209
+ SvgEmbed,
210
+ Timeline,
211
+ TimelineItem
206
212
  ];
207
213
  try {
208
214
  const emojiPkg = "@tiptap/extension-emoji";
@@ -1,2 +1,2 @@
1
- declare const _default: import("h3").EventHandler<Omit<import("h3").EventHandlerRequest, "body">, Promise<never[] | unknown[]>>;
1
+ declare const _default: import("h3").EventHandler<Omit<import("h3").EventHandlerRequest, "body">, Promise<unknown[] | never[]>>;
2
2
  export default _default;
@@ -4,6 +4,7 @@ import { useStorage } from "nitropack/runtime/storage";
4
4
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
5
5
  import { dirname, join } from "node:path";
6
6
  import { registerServerPlugin, bootRunners, shutdownAllRunners } from "../utils/serverRunner.js";
7
+ import { buildServerSchemaValidator } from "../utils/schemaServerSupport.js";
7
8
  import { createDocCacheAPI } from "../utils/docCache.js";
8
9
  import { docTreeCacheRunner } from "../runners/doc-tree-cache.js";
9
10
  import { initSlugMap, loadPersistedSlugMap } from "../utils/slugMap.js";
@@ -212,7 +213,34 @@ export default defineNitroPlugin(async (nitroApp) => {
212
213
  };
213
214
  registerServerPlugin(coreServerPlugin);
214
215
  }
216
+ const attachedSchemas = [];
217
+ const schemaCfg = publicConfig?.schema ?? {};
218
+ if (schemaCfg.bundleDir) {
219
+ console.log(
220
+ `[abracadabra-service] schema bundle_dir advertised: ${schemaCfg.bundleDir} (server-side bundle_dir = same path; informational only \u2014 server is authoritative)`
221
+ );
222
+ }
223
+ Object.assign(ctx, {
224
+ attachSchema: (registry) => {
225
+ if (!registry || typeof registry !== "object") return;
226
+ if (typeof registry.validateMeta !== "function") {
227
+ console.warn("[abracadabra-service] attachSchema received a value missing validateMeta() \u2014 skipping");
228
+ return;
229
+ }
230
+ attachedSchemas.push(registry);
231
+ }
232
+ });
215
233
  await nitroApp.hooks.callHook("abracadabra:before-runners", ctx);
234
+ delete ctx.attachSchema;
235
+ if (attachedSchemas.length > 0) {
236
+ const validator = buildServerSchemaValidator(attachedSchemas);
237
+ if (validator) ctx.validateMeta = validator;
238
+ const advertisedTypes = /* @__PURE__ */ new Set();
239
+ for (const r of attachedSchemas) for (const t of r.types.keys()) advertisedTypes.add(t);
240
+ console.log(
241
+ `[abracadabra-service] ${attachedSchemas.length} schema registr${attachedSchemas.length === 1 ? "y" : "ies"} attached covering types: [${[...advertisedTypes].sort().join(", ")}]`
242
+ );
243
+ }
216
244
  await bootRunners(ctx);
217
245
  console.log("[abracadabra-service] All runners started");
218
246
  nitroApp.hooks.hook("close", async () => {