@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,79 @@
1
+ /**
2
+ * useQuery — query + live-subscribe to documents in the doc-tree.
3
+ *
4
+ * Two modes:
5
+ *
6
+ * 1. **Imperative (`subscribe: false`, default)** — thin Vue-reactive
7
+ * shell around `client.queryDocs`. Call `refresh()` to re-fetch.
8
+ * Use for "give me the matching docs right now" reads.
9
+ *
10
+ * 2. **Reactive (`subscribe: true`)** — opens a `provider.subscribeQuery`
11
+ * under the hood. The first frame fills `entries`; subsequent
12
+ * `delta` frames merge in via add/remove. Resubscribes when the
13
+ * options ref changes; cancels on unmount.
14
+ *
15
+ * Reactive mode requires an active provider — `entries` stays empty
16
+ * until the provider connects and the initial snapshot arrives.
17
+ *
18
+ * Usage:
19
+ *
20
+ * // Imperative
21
+ * const { entries, loading, refresh } = useQuery({ type: 'kanban' })
22
+ * await refresh()
23
+ *
24
+ * // Reactive (mount → snapshot → live deltas → unmount cancels)
25
+ * const { entries } = useQuery(
26
+ * computed(() => ({ type: 'kanban', where: { priority: { gte: 3 } } })),
27
+ * { immediate: true, subscribe: true },
28
+ * )
29
+ */
30
+ import { type Ref } from 'vue';
31
+ import type { DocumentMeta } from '@abraca/dabra';
32
+ export interface QueryOptions {
33
+ /** Match `documents.kind` exactly. */
34
+ type?: string;
35
+ /** Narrow to children of this doc. */
36
+ parentId?: string;
37
+ /** Case-insensitive substring match on `documents.label`. */
38
+ labelContains?: string;
39
+ /** Maximum number of results (default 50, hard cap 500 server-side). */
40
+ limit?: number;
41
+ /**
42
+ * Predicate AST. Shape mirrors `@abraca/schema`'s `WhereClause`:
43
+ * `{ priority: { gte: 3 }, tags: { contains: "urgent" } }`
44
+ * Empty / missing falls through to the v1 filter set.
45
+ */
46
+ where?: unknown;
47
+ }
48
+ export interface UseQueryReturn {
49
+ /** Latest result set. Empty until `refresh()` runs at least once. */
50
+ readonly entries: Ref<readonly DocumentMeta[]>;
51
+ /** True while a fetch is in flight. */
52
+ readonly loading: Ref<boolean>;
53
+ /** Last error message, or null. Cleared on the next refresh. */
54
+ readonly error: Ref<string | null>;
55
+ /** Trigger a fresh fetch with the current options. */
56
+ refresh: () => Promise<DocumentMeta[]>;
57
+ }
58
+ export interface UseQueryHookOptions {
59
+ /**
60
+ * Run `refresh()` immediately when the composable mounts, and whenever
61
+ * the options ref changes. Default false — the caller invokes `refresh`
62
+ * explicitly. Note that when the options arg is a plain object (not a
63
+ * ref / computed), the watcher fires once at mount and then never
64
+ * again — change-tracking requires a reactive source.
65
+ */
66
+ immediate?: boolean;
67
+ /**
68
+ * Open a live `query:v1:` subscription on the provider's WebSocket
69
+ * instead of polling REST. The first `ack` frame fills `entries`;
70
+ * subsequent `delta` frames merge in. Cancels on unmount, and on
71
+ * options change (a fresh sub is opened with the new predicate).
72
+ *
73
+ * Implies `immediate: true` — subscriptions are useless without an
74
+ * initial snapshot. Falls back to imperative mode when no provider
75
+ * is bound or the WS is closed.
76
+ */
77
+ subscribe?: boolean;
78
+ }
79
+ export declare function useQuery(options: QueryOptions | Ref<QueryOptions>, hookOpts?: UseQueryHookOptions): UseQueryReturn;
@@ -0,0 +1,97 @@
1
+ import { onScopeDispose, ref, shallowRef, watch } from "vue";
2
+ import { useAbracadabra } from "#imports";
3
+ function asOptions(input) {
4
+ if (input.value !== void 0 && typeof input.effect !== "undefined") {
5
+ return input;
6
+ }
7
+ if (input.value !== void 0) {
8
+ return input;
9
+ }
10
+ return ref(input);
11
+ }
12
+ export function useQuery(options, hookOpts = {}) {
13
+ const opts = asOptions(options);
14
+ const entries = shallowRef([]);
15
+ const loading = ref(false);
16
+ const error = ref(null);
17
+ async function refresh() {
18
+ const abra = useAbracadabra();
19
+ const client = abra.client?.value;
20
+ if (!client) {
21
+ entries.value = [];
22
+ return [];
23
+ }
24
+ loading.value = true;
25
+ error.value = null;
26
+ try {
27
+ const result = await client.queryDocs(opts.value);
28
+ entries.value = Object.freeze(result.slice());
29
+ return result;
30
+ } catch (err) {
31
+ error.value = err instanceof Error ? err.message : String(err);
32
+ entries.value = [];
33
+ return [];
34
+ } finally {
35
+ loading.value = false;
36
+ }
37
+ }
38
+ let activeHandle = null;
39
+ function closeSubscription() {
40
+ if (activeHandle) {
41
+ activeHandle.cancel();
42
+ activeHandle = null;
43
+ }
44
+ }
45
+ function specFromOpts(o) {
46
+ return {
47
+ type: o.type,
48
+ parentId: o.parentId,
49
+ labelContains: o.labelContains,
50
+ limit: o.limit,
51
+ where: o.where
52
+ };
53
+ }
54
+ async function openSubscription() {
55
+ closeSubscription();
56
+ const abra = useAbracadabra();
57
+ const provider = abra.provider?.value;
58
+ if (!provider?.subscribeQuery) {
59
+ await refresh();
60
+ return;
61
+ }
62
+ loading.value = true;
63
+ error.value = null;
64
+ activeHandle = provider.subscribeQuery(specFromOpts(opts.value), {
65
+ onSnapshot: (rows) => {
66
+ entries.value = Object.freeze(rows.slice());
67
+ loading.value = false;
68
+ },
69
+ onDelta: ({ added, removed }) => {
70
+ const removedSet = new Set(removed);
71
+ const kept = entries.value.filter(
72
+ (e) => !removedSet.has(e.id)
73
+ );
74
+ const next = [...added, ...kept];
75
+ entries.value = Object.freeze(next);
76
+ },
77
+ onError: (err) => {
78
+ error.value = err.message;
79
+ }
80
+ });
81
+ }
82
+ if (hookOpts.subscribe) {
83
+ watch(
84
+ opts,
85
+ () => {
86
+ void openSubscription();
87
+ },
88
+ { immediate: true, deep: true }
89
+ );
90
+ onScopeDispose(() => closeSubscription());
91
+ } else if (hookOpts.immediate) {
92
+ watch(opts, () => {
93
+ void refresh();
94
+ }, { immediate: true, deep: true });
95
+ }
96
+ return { entries, loading, error, refresh };
97
+ }
@@ -1,5 +1,4 @@
1
1
  import { ref, computed } from "vue";
2
- import { useRoute } from "#imports";
3
2
  import { useAbracadabra } from "./useAbracadabra.js";
4
3
  const loading = ref(false);
5
4
  const error = ref(null);
@@ -7,10 +6,10 @@ export function useSpaces() {
7
6
  const abra = useAbracadabra();
8
7
  const spaces = computed(() => abra.currentServerSpaces.value ?? []);
9
8
  const currentSpace = computed(() => {
10
- const route = useRoute();
11
- const docId = route.params.docId;
12
- if (!docId) return spaces.value[0] ?? null;
13
- return spaces.value.find((s) => s.id === docId) ?? null;
9
+ const entry = abra.savedServers?.value?.find?.((s) => s.url === abra.currentServerUrl?.value);
10
+ const entryDocId = entry?.entryDocId;
11
+ if (!entryDocId) return spaces.value[0] ?? null;
12
+ return spaces.value.find((s) => s.doc_id === entryDocId) ?? spaces.value[0] ?? null;
14
13
  });
15
14
  async function refresh() {
16
15
  loading.value = true;
@@ -5,9 +5,9 @@ export declare function useTableView(tree: {
5
5
  treeMap: any;
6
6
  }, tableDocId: string): {
7
7
  tableMeta: import("vue").ComputedRef<DocPageMeta>;
8
- explicitMode: import("vue").ComputedRef<"flat" | "hierarchy" | undefined>;
9
- detectedMode: import("vue").ComputedRef<"flat" | "hierarchy">;
10
- effectiveMode: import("vue").ComputedRef<"flat" | "hierarchy">;
8
+ explicitMode: import("vue").ComputedRef<"hierarchy" | "flat" | undefined>;
9
+ detectedMode: import("vue").ComputedRef<"hierarchy" | "flat">;
10
+ effectiveMode: import("vue").ComputedRef<"hierarchy" | "flat">;
11
11
  setMode: (mode: "hierarchy" | "flat" | undefined) => void;
12
12
  columnDefs: import("vue").ComputedRef<TableColumnDef[]>;
13
13
  addColumn: (def: TableColumnDef) => void;
@@ -0,0 +1,97 @@
1
+ /**
2
+ * useTypedDoc — typed reads + validated writes against the Abracadabra
3
+ * doc-tree, parameterised by a `@abraca/schema` registry.
4
+ *
5
+ * Day 2 of the Phase 3 schema rollout.
6
+ *
7
+ * The Nuxt module reads / writes the doc-tree through `useSyncedMap` on
8
+ * the root Y.Doc — it does NOT use `DocumentManager.docs(...)` from
9
+ * `@abraca/dabra`, because the module instantiates `AbracadabraProvider`
10
+ * directly (no DM wrapper). This composable is the Nuxt-side analogue:
11
+ * a typed projection over `useDocTree`'s reactive entries, with optional
12
+ * pre-write validation against the supplied registry.
13
+ *
14
+ * // app code
15
+ * import { kanbanSchema } from '@abraca/schema'
16
+ * const { get, all, update } = useTypedDoc(kanbanSchema)
17
+ * const board = computed(() => get('kanban', boardId.value))
18
+ * // board.value?.meta.kanbanColumnWidth is typed
19
+ * update('kanban', boardId.value, { kanbanColumnWidth: 'wide' })
20
+ *
21
+ * Behaviour:
22
+ * - `get(type, id)` returns null when the entry is missing OR its
23
+ * stored `type` doesn't match (cross-schema reads return null —
24
+ * matches the server-side smoke contract).
25
+ * - `update(type, id, patch)` validates the *merged* meta before
26
+ * writing. Throws `MetaValidationError`-equivalent on mismatch.
27
+ * - `set(type, id, meta)` validates the replacement meta.
28
+ * - `clear(type, id, keys)` is a thin wrapper over `updateMeta` —
29
+ * no validation (clearing keys can never fail validation).
30
+ * - Reads do NOT auto-call `runMigrations`. Pass `migrate: true` in
31
+ * the per-call options or use `useDocEntryTyped` (Day 3).
32
+ *
33
+ * Type inference: when a typed `SchemaRegistry<TMap>` is passed, every
34
+ * read/write narrows `meta` to `TMap[N]`. Untyped registries fall back
35
+ * to `Record<string, unknown>`.
36
+ */
37
+ import { type ComputedRef } from 'vue';
38
+ import type { SchemaRegistryLike } from '../utils/schemaSupport.js';
39
+ /** Extract the doc-type names from a typed registry. */
40
+ type SchemaDocTypeName<S> = S extends {
41
+ __metaMap?: infer M;
42
+ } ? keyof M & string : string;
43
+ /** Extract the meta type for a given doc-type name from a registry. */
44
+ type SchemaMetaOf<S, N extends string> = S extends {
45
+ __metaMap?: infer M;
46
+ } ? N extends keyof M ? M[N] : Record<string, unknown> : Record<string, unknown>;
47
+ export interface TypedTreeEntry<M> {
48
+ readonly id: string;
49
+ readonly type: string;
50
+ readonly label: string;
51
+ readonly parentId: string | null;
52
+ readonly order: number;
53
+ readonly meta: M | undefined;
54
+ readonly createdAt: number | undefined;
55
+ readonly updatedAt: number | undefined;
56
+ }
57
+ /**
58
+ * Thrown by typed write methods when validation fails.
59
+ *
60
+ * Mirrors `MetaValidationError` from `@abraca/dabra` — kept module-local
61
+ * because the Nuxt write path doesn't go through `MetaManager`.
62
+ * Consumers can `import { MetaValidationError } from '@abraca/nuxt'` to
63
+ * type-narrow without dragging in `@abraca/dabra`.
64
+ */
65
+ export declare class MetaValidationError extends Error {
66
+ readonly docId: string;
67
+ readonly docType: string;
68
+ readonly errors: ReadonlyArray<{
69
+ path: ReadonlyArray<PropertyKey>;
70
+ message: string;
71
+ code?: string;
72
+ }>;
73
+ constructor(docId: string, docType: string, errors: ReadonlyArray<{
74
+ path: ReadonlyArray<PropertyKey>;
75
+ message: string;
76
+ code?: string;
77
+ }>);
78
+ }
79
+ export declare class TypedDocTypeMismatchError extends Error {
80
+ readonly docId: string;
81
+ readonly expectedType: string;
82
+ readonly actualType: string | undefined;
83
+ constructor(docId: string, expectedType: string, actualType: string | undefined);
84
+ }
85
+ export declare function useTypedDoc<S extends SchemaRegistryLike>(schema: S): {
86
+ /** Synchronous typed read. Returns null on missing entry or type mismatch. */
87
+ get: <N extends SchemaDocTypeName<S>>(type: N, id: string) => TypedTreeEntry<SchemaMetaOf<S, N>> | null;
88
+ /** Reactive list of every entry whose stored `type` matches `type`. */
89
+ all: <N extends SchemaDocTypeName<S>>(type: N) => ComputedRef<TypedTreeEntry<SchemaMetaOf<S, N>>[]>;
90
+ /** Validated meta merge. Throws on mismatch / validation failure. */
91
+ update: <N extends SchemaDocTypeName<S>>(type: N, id: string, patch: Partial<SchemaMetaOf<S, N>>) => void;
92
+ /** Validated meta replacement. Throws on mismatch / validation failure. */
93
+ set: <N extends SchemaDocTypeName<S>>(type: N, id: string, meta: SchemaMetaOf<S, N>) => void;
94
+ /** Delete specific keys. No validation (clearing can't fail validation). */
95
+ clear: <N extends SchemaDocTypeName<S>>(type: N, id: string, keys: ReadonlyArray<keyof SchemaMetaOf<S, N> & string>) => void;
96
+ };
97
+ export {};
@@ -0,0 +1,114 @@
1
+ import { computed } from "vue";
2
+ import { useAbracadabra } from "./useAbracadabra.js";
3
+ import { useDocTree } from "./useDocTree.js";
4
+ export class MetaValidationError extends Error {
5
+ docId;
6
+ docType;
7
+ errors;
8
+ constructor(docId, docType, errors) {
9
+ super(
10
+ `meta validation failed for doc ${docId} (type "${docType}"): ` + errors.map((e) => `${e.path.join(".") || "<root>"}: ${e.message}`).join("; ")
11
+ );
12
+ this.name = "MetaValidationError";
13
+ this.docId = docId;
14
+ this.docType = docType;
15
+ this.errors = errors;
16
+ }
17
+ }
18
+ export class TypedDocTypeMismatchError extends Error {
19
+ docId;
20
+ expectedType;
21
+ actualType;
22
+ constructor(docId, expectedType, actualType) {
23
+ super(
24
+ `typed write on document ${docId} expected type "${expectedType}" but stored type is ` + (actualType === void 0 ? "(none)" : `"${actualType}"`)
25
+ );
26
+ this.name = "TypedDocTypeMismatchError";
27
+ this.docId = docId;
28
+ this.expectedType = expectedType;
29
+ this.actualType = actualType;
30
+ }
31
+ }
32
+ function projectEntry(entry, expectedType) {
33
+ if (!entry) return null;
34
+ if (entry.type !== expectedType) return null;
35
+ return {
36
+ id: entry.id,
37
+ type: expectedType,
38
+ label: entry.label,
39
+ parentId: entry.parentId,
40
+ order: entry.order,
41
+ meta: entry.meta,
42
+ createdAt: entry.createdAt,
43
+ updatedAt: entry.updatedAt
44
+ };
45
+ }
46
+ export function useTypedDoc(schema) {
47
+ const abra = useAbracadabra();
48
+ const tree = useDocTree();
49
+ function readEntry(id) {
50
+ return tree.getEntry(id);
51
+ }
52
+ function ensureType(id, expectedType) {
53
+ const entry = readEntry(id);
54
+ if (!entry) {
55
+ throw new TypedDocTypeMismatchError(id, expectedType, void 0);
56
+ }
57
+ if (entry.type !== expectedType) {
58
+ throw new TypedDocTypeMismatchError(id, expectedType, entry.type);
59
+ }
60
+ return entry;
61
+ }
62
+ function validateOrThrow(id, type, meta) {
63
+ const result = schema.validateMeta(type, meta);
64
+ if (result.ok) return result.value ?? meta;
65
+ throw new MetaValidationError(id, type, result.errors);
66
+ }
67
+ function writeMeta(id, nextMeta) {
68
+ const doc = abra.doc.value;
69
+ if (!doc) return;
70
+ const treeMap = doc.getMap("doc-tree");
71
+ const entry = treeMap.get(id);
72
+ if (!entry) return;
73
+ treeMap.set(id, {
74
+ ...entry,
75
+ meta: nextMeta,
76
+ updatedAt: Date.now()
77
+ });
78
+ }
79
+ return {
80
+ get(type, id) {
81
+ return projectEntry(readEntry(id), type);
82
+ },
83
+ all(type) {
84
+ return computed(
85
+ () => tree.entries.value.filter((e) => e.type === type && !e.trashed).map((e) => projectEntry(e, type)).filter((e) => e !== null)
86
+ );
87
+ },
88
+ update(type, id, patch) {
89
+ const entry = ensureType(id, type);
90
+ const merged = {
91
+ ...entry.meta ?? {},
92
+ ...patch
93
+ };
94
+ validateOrThrow(id, type, merged);
95
+ writeMeta(id, merged);
96
+ },
97
+ set(type, id, meta) {
98
+ ensureType(id, type);
99
+ validateOrThrow(id, type, meta);
100
+ writeMeta(id, meta);
101
+ },
102
+ clear(type, id, keys) {
103
+ const entry = ensureType(id, type);
104
+ if (!entry.meta) return;
105
+ const src = entry.meta;
106
+ const dropped = new Set(keys.map((k) => k));
107
+ const next = {};
108
+ for (const [k, v] of Object.entries(src)) {
109
+ if (!dropped.has(k)) next[k] = v;
110
+ }
111
+ writeMeta(id, next);
112
+ }
113
+ };
114
+ }
@@ -18,18 +18,41 @@ function _wireEvents(rtc) {
18
18
  rtc.on(event, handler);
19
19
  _unbinds.push(() => rtc.off(event, handler));
20
20
  };
21
+ function normalizePeer(p) {
22
+ const peerId = p?.peerId ?? p?.peer_id;
23
+ return {
24
+ ...p,
25
+ peerId,
26
+ peer_id: peerId,
27
+ displayName: p?.displayName ?? p?.name,
28
+ color: p?.color
29
+ };
30
+ }
21
31
  on("connected", () => {
22
32
  isConnected.value = true;
23
33
  webrtcStatus.value = "connected";
24
- localPeerId.value = rtc.localPeerId;
34
+ const newLocalId = rtc.localPeerId;
35
+ localPeerId.value = newLocalId;
36
+ const next = /* @__PURE__ */ new Map();
37
+ const sdkPeers = rtc.peers instanceof Map ? rtc.peers : null;
38
+ if (sdkPeers) {
39
+ for (const [pid, p] of sdkPeers.entries()) {
40
+ if (!pid || pid === newLocalId) continue;
41
+ next.set(pid, normalizePeer(p));
42
+ }
43
+ }
44
+ peers.value = next;
25
45
  });
26
46
  on("disconnected", () => {
27
47
  isConnected.value = false;
28
48
  webrtcStatus.value = "disconnected";
29
49
  });
30
50
  on("peerJoined", (peer) => {
51
+ const norm = normalizePeer(peer);
52
+ if (!norm.peerId) return;
53
+ if (norm.peerId === localPeerId.value) return;
31
54
  const next = new Map(peers.value);
32
- next.set(peer.peerId, peer);
55
+ next.set(norm.peerId, norm);
33
56
  peers.value = next;
34
57
  });
35
58
  on("peerLeft", ({ peerId }) => {
@@ -40,6 +63,13 @@ function _wireEvents(rtc) {
40
63
  nextE2ee.delete(peerId);
41
64
  e2eeEstablishedPeers.value = nextE2ee;
42
65
  });
66
+ on("peerProfile", ({ peerId, name, color }) => {
67
+ const existing = peers.value.get(peerId);
68
+ if (!existing) return;
69
+ const next = new Map(peers.value);
70
+ next.set(peerId, { ...existing, displayName: name, name, color });
71
+ peers.value = next;
72
+ });
43
73
  on("peerConnectionState", ({ peerId, state }) => {
44
74
  const existing = peers.value.get(peerId);
45
75
  if (existing) {
@@ -100,7 +130,7 @@ function fromBase64Url(str) {
100
130
  }
101
131
  async function connect(options) {
102
132
  if (_rtc && isConnected.value) return;
103
- const { provider, keystore, isClaimed } = useAbracadabra();
133
+ const { provider, keystore, isClaimed, userName, userColor } = useAbracadabra();
104
134
  if (!provider.value) return;
105
135
  const config = useRuntimeConfig();
106
136
  const webrtcConfig = config.public.abracadabra?.webrtc ?? {};
@@ -138,8 +168,17 @@ async function connect(options) {
138
168
  }
139
169
  const rtc = AbracadabraWebRTC.fromProvider(provider.value, {
140
170
  iceServers: resolvedIceServers,
141
- enableFileTransfer: webrtcConfig.fileTransfer ?? false,
142
- e2ee: e2eeIdentity
171
+ // Default file-transfer ON. The SDK's default (false) means
172
+ // `useFileTransfer().sendFile()` queues bytes onto a data channel that
173
+ // never gets created → silent 0 % stuck transfer. Apps that explicitly
174
+ // want to disable can pass `webrtc.fileTransfer: false` in nuxt.config.
175
+ enableFileTransfer: webrtcConfig.fileTransfer ?? true,
176
+ e2ee: e2eeIdentity,
177
+ // Pass identity through so peers can label each other in the mesh.
178
+ // Without these the SDK's `peerProfile` event never fires and the UI
179
+ // shows opaque peer IDs ("peer-jl0z7m" etc.) instead of usernames.
180
+ displayName: userName.value || void 0,
181
+ color: userColor.value || void 0
143
182
  });
144
183
  _initWebRTC(rtc);
145
184
  await rtc.connect();
@@ -1,5 +1,7 @@
1
1
  import { Node } from "@tiptap/core";
2
2
  import { Plugin, PluginKey, TextSelection } from "@tiptap/pm/state";
3
+ import { VueNodeViewRenderer } from "@tiptap/vue-3";
4
+ import DocumentMetaView from "./views/DocumentMetaView.vue";
3
5
  export const DocumentMeta = Node.create({
4
6
  name: "documentMeta",
5
7
  content: "inline*",
@@ -10,6 +12,9 @@ export const DocumentMeta = Node.create({
10
12
  renderHTML() {
11
13
  return ["div", { "data-type": "document-meta" }, 0];
12
14
  },
15
+ addNodeView() {
16
+ return VueNodeViewRenderer(DocumentMetaView);
17
+ },
13
18
  addKeyboardShortcuts() {
14
19
  return {
15
20
  Backspace: ({ editor }) => {
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Timeline + TimelineItem TipTap nodes.
3
+ *
4
+ * Vertical event timeline with per-item icon, label, and freeform date.
5
+ * Optional builtin — disabled via `abracadabra.disabledBuiltins: ['timeline']`.
6
+ *
7
+ * Ported 1:1 from cou-sh/app/extensions/timeline.ts.
8
+ */
9
+ import { Node } from '@tiptap/core';
10
+ export declare const TimelineItem: Node<any, any>;
11
+ export declare const Timeline: Node<any, any>;
@@ -0,0 +1,52 @@
1
+ import { Node, mergeAttributes } from "@tiptap/core";
2
+ import { VueNodeViewRenderer } from "@tiptap/vue-3";
3
+ import TimelineView from "./views/TimelineView.vue";
4
+ import TimelineItemView from "./views/TimelineItemView.vue";
5
+ export const TimelineItem = Node.create({
6
+ name: "timelineItem",
7
+ content: "block+",
8
+ addAttributes() {
9
+ return {
10
+ date: { default: "" },
11
+ label: { default: "Event" },
12
+ icon: { default: "" }
13
+ };
14
+ },
15
+ parseHTML() {
16
+ return [
17
+ {
18
+ tag: "div[data-timeline-item]",
19
+ getAttrs: (el) => ({
20
+ date: el.getAttribute("data-date") || "",
21
+ label: el.getAttribute("data-label") || "Event",
22
+ icon: el.getAttribute("data-icon") || ""
23
+ })
24
+ }
25
+ ];
26
+ },
27
+ renderHTML({ HTMLAttributes }) {
28
+ return ["div", mergeAttributes({
29
+ "data-timeline-item": "",
30
+ "data-date": HTMLAttributes.date,
31
+ "data-label": HTMLAttributes.label,
32
+ "data-icon": HTMLAttributes.icon
33
+ }), 0];
34
+ },
35
+ addNodeView() {
36
+ return VueNodeViewRenderer(TimelineItemView);
37
+ }
38
+ });
39
+ export const Timeline = Node.create({
40
+ name: "timeline",
41
+ group: "block",
42
+ content: "timelineItem+",
43
+ parseHTML() {
44
+ return [{ tag: "div[data-timeline]" }];
45
+ },
46
+ renderHTML() {
47
+ return ["div", { "data-timeline": "" }, 0];
48
+ },
49
+ addNodeView() {
50
+ return VueNodeViewRenderer(TimelineView);
51
+ }
52
+ });
@@ -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,63 @@
1
+ <script setup>
2
+ import { computed } from "vue";
3
+ import { NodeViewWrapper, NodeViewContent } from "@tiptap/vue-3";
4
+ import { buildMetaMenuItems } from "../../composables/useMetaMenuItems";
5
+ const props = defineProps({
6
+ decorations: { type: Array, required: true },
7
+ selected: { type: Boolean, required: true },
8
+ updateAttributes: { type: Function, required: true },
9
+ deleteNode: { type: Function, required: true },
10
+ node: { type: null, required: true },
11
+ view: { type: null, required: true },
12
+ getPos: { type: null, required: true },
13
+ innerDecorations: { type: null, required: true },
14
+ editor: { type: Object, required: true },
15
+ extension: { type: Object, required: true },
16
+ HTMLAttributes: { type: Object, required: true }
17
+ });
18
+ const items = computed(() => {
19
+ void props.node.content.size;
20
+ return buildMetaMenuItems(props.editor);
21
+ });
22
+ </script>
23
+
24
+ <template>
25
+ <NodeViewWrapper
26
+ as="div"
27
+ data-type="document-meta"
28
+ class="group/meta"
29
+ :class="{ 'meta-empty': node.content.size === 0 && !props.editor.isEditable }"
30
+ draggable="false"
31
+ @dragstart.prevent.stop
32
+ >
33
+ <NodeViewContent
34
+ as="span"
35
+ style="display: contents"
36
+ />
37
+ <span
38
+ v-show="props.editor.isEditable"
39
+ contenteditable="false"
40
+ class="inline-flex items-center"
41
+ >
42
+ <UDropdownMenu
43
+ :items="items"
44
+ :content="{ side: 'bottom', align: 'start' }"
45
+ :ui="{ content: 'w-48' }"
46
+ >
47
+ <UButton
48
+ icon="i-lucide-plus"
49
+ size="2xs"
50
+ color="neutral"
51
+ variant="ghost"
52
+ class="opacity-0 group-hover/meta:opacity-40 hover:!opacity-100 transition-opacity"
53
+ />
54
+ </UDropdownMenu>
55
+ </span>
56
+ <span
57
+ v-if="node.content.size === 0 && props.editor.isEditable"
58
+ class="text-sm text-(--ui-text-muted) pointer-events-none opacity-60"
59
+ >
60
+ Type '/' to add a property…
61
+ </span>
62
+ </NodeViewWrapper>
63
+ </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,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;