@abraca/nuxt 2.0.11 → 2.4.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 (135) 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/AMedia.d.vue.ts +1 -1
  17. package/dist/runtime/components/aware/AMedia.vue.d.ts +1 -1
  18. package/dist/runtime/components/aware/APresenceBlobs.d.vue.ts +29 -1
  19. package/dist/runtime/components/aware/APresenceBlobs.vue +54 -8
  20. package/dist/runtime/components/aware/APresenceBlobs.vue.d.ts +29 -1
  21. package/dist/runtime/components/aware/APresenceCursors.d.vue.ts +11 -0
  22. package/dist/runtime/components/aware/APresenceCursors.vue +74 -9
  23. package/dist/runtime/components/aware/APresenceCursors.vue.d.ts +11 -0
  24. package/dist/runtime/components/aware/AToggleGroup.d.vue.ts +28 -13
  25. package/dist/runtime/components/aware/AToggleGroup.vue +56 -20
  26. package/dist/runtime/components/aware/AToggleGroup.vue.d.ts +28 -13
  27. package/dist/runtime/components/docs/ADocsNavigation.d.vue.ts +1 -1
  28. package/dist/runtime/components/docs/ADocsNavigation.vue.d.ts +1 -1
  29. package/dist/runtime/components/docs/ADocsSearch.d.vue.ts +1 -1
  30. package/dist/runtime/components/docs/ADocsSearch.vue.d.ts +1 -1
  31. package/dist/runtime/components/docs/ADocsSearchButton.d.vue.ts +1 -1
  32. package/dist/runtime/components/docs/ADocsSearchButton.vue.d.ts +1 -1
  33. package/dist/runtime/components/docs/ADocsToc.d.vue.ts +2 -2
  34. package/dist/runtime/components/docs/ADocsToc.vue.d.ts +2 -2
  35. package/dist/runtime/components/editor/AEditorRedoButton.d.vue.ts +1 -1
  36. package/dist/runtime/components/editor/AEditorRedoButton.vue.d.ts +1 -1
  37. package/dist/runtime/components/editor/AEditorUndoButton.d.vue.ts +1 -1
  38. package/dist/runtime/components/editor/AEditorUndoButton.vue.d.ts +1 -1
  39. package/dist/runtime/components/editor/AFileGlbViewer.vue +27 -10
  40. package/dist/runtime/components/editor/ANodeInlineLabel.d.vue.ts +1 -1
  41. package/dist/runtime/components/editor/ANodeInlineLabel.vue.d.ts +1 -1
  42. package/dist/runtime/components/registry/APluginBrowser.d.vue.ts +23 -0
  43. package/dist/runtime/components/registry/APluginBrowser.vue +155 -0
  44. package/dist/runtime/components/registry/APluginBrowser.vue.d.ts +23 -0
  45. package/dist/runtime/components/registry/APluginCapabilityDialog.d.vue.ts +17 -0
  46. package/dist/runtime/components/registry/APluginCapabilityDialog.vue +159 -0
  47. package/dist/runtime/components/registry/APluginCapabilityDialog.vue.d.ts +17 -0
  48. package/dist/runtime/components/registry/APluginCard.d.vue.ts +20 -0
  49. package/dist/runtime/components/registry/APluginCard.vue +91 -0
  50. package/dist/runtime/components/registry/APluginCard.vue.d.ts +20 -0
  51. package/dist/runtime/components/registry/APluginDetail.d.vue.ts +18 -0
  52. package/dist/runtime/components/registry/APluginDetail.vue +252 -0
  53. package/dist/runtime/components/registry/APluginDetail.vue.d.ts +18 -0
  54. package/dist/runtime/components/renderers/ACodeRenderer.d.vue.ts +15 -0
  55. package/dist/runtime/components/renderers/ACodeRenderer.vue +68 -0
  56. package/dist/runtime/components/renderers/ACodeRenderer.vue.d.ts +15 -0
  57. package/dist/runtime/components/renderers/AGraphRenderer.vue +416 -120
  58. package/dist/runtime/components/renderers/AProseRenderer.d.vue.ts +2 -2
  59. package/dist/runtime/components/renderers/AProseRenderer.vue.d.ts +2 -2
  60. package/dist/runtime/components/renderers/sheets/ASheetsToolbar.d.vue.ts +4 -4
  61. package/dist/runtime/components/renderers/sheets/ASheetsToolbar.vue.d.ts +4 -4
  62. package/dist/runtime/components/shell/ABreadcrumbForDoc.d.vue.ts +11 -0
  63. package/dist/runtime/components/shell/ABreadcrumbForDoc.vue +16 -0
  64. package/dist/runtime/components/shell/ABreadcrumbForDoc.vue.d.ts +11 -0
  65. package/dist/runtime/components/shell/ASettingsSection.d.vue.ts +35 -0
  66. package/dist/runtime/components/shell/ASettingsSection.vue +26 -0
  67. package/dist/runtime/components/shell/ASettingsSection.vue.d.ts +35 -0
  68. package/dist/runtime/components/shell/ASidebar.d.vue.ts +1 -1
  69. package/dist/runtime/components/shell/ASidebar.vue.d.ts +1 -1
  70. package/dist/runtime/components/shell/AUserMenu.d.vue.ts +3 -0
  71. package/dist/runtime/components/shell/AUserMenu.vue +4 -0
  72. package/dist/runtime/components/shell/AUserMenu.vue.d.ts +3 -0
  73. package/dist/runtime/composables/useAbracadabraSchema.d.ts +83 -0
  74. package/dist/runtime/composables/useAbracadabraSchema.js +52 -0
  75. package/dist/runtime/composables/useAggregatedPresence.d.ts +1 -6
  76. package/dist/runtime/composables/useCalendarView.d.ts +1 -1
  77. package/dist/runtime/composables/useChat.js +1 -0
  78. package/dist/runtime/composables/useDocBreadcrumb.d.ts +21 -0
  79. package/dist/runtime/composables/useDocBreadcrumb.js +33 -0
  80. package/dist/runtime/composables/useDocEntryTyped.d.ts +60 -0
  81. package/dist/runtime/composables/useDocEntryTyped.js +70 -0
  82. package/dist/runtime/composables/useEditorDragHandle.js +18 -0
  83. package/dist/runtime/composables/useEditorSuggestions.js +2 -1
  84. package/dist/runtime/composables/useInstalledPlugins.d.ts +3 -21
  85. package/dist/runtime/composables/useInstalledPlugins.js +2 -12
  86. package/dist/runtime/composables/useMetaMenuItems.d.ts +21 -0
  87. package/dist/runtime/composables/useMetaMenuItems.js +115 -0
  88. package/dist/runtime/composables/useMetaValidator.d.ts +27 -0
  89. package/dist/runtime/composables/useMetaValidator.js +10 -0
  90. package/dist/runtime/composables/usePluginCatalog.d.ts +161 -0
  91. package/dist/runtime/composables/usePluginCatalog.js +234 -0
  92. package/dist/runtime/composables/useQuery.d.ts +79 -0
  93. package/dist/runtime/composables/useQuery.js +97 -0
  94. package/dist/runtime/composables/useSpaces.js +4 -5
  95. package/dist/runtime/composables/useTableView.d.ts +3 -3
  96. package/dist/runtime/composables/useTypedDoc.d.ts +97 -0
  97. package/dist/runtime/composables/useTypedDoc.js +114 -0
  98. package/dist/runtime/composables/useWebRTC.js +44 -5
  99. package/dist/runtime/extensions/document-meta.js +5 -0
  100. package/dist/runtime/extensions/timeline.d.ts +11 -0
  101. package/dist/runtime/extensions/timeline.js +52 -0
  102. package/dist/runtime/extensions/views/DocumentMetaView.d.vue.ts +4 -0
  103. package/dist/runtime/extensions/views/DocumentMetaView.vue +63 -0
  104. package/dist/runtime/extensions/views/DocumentMetaView.vue.d.ts +4 -0
  105. package/dist/runtime/extensions/views/TimelineItemView.d.vue.ts +4 -0
  106. package/dist/runtime/extensions/views/TimelineItemView.vue +131 -0
  107. package/dist/runtime/extensions/views/TimelineItemView.vue.d.ts +4 -0
  108. package/dist/runtime/extensions/views/TimelineView.d.vue.ts +9 -0
  109. package/dist/runtime/extensions/views/TimelineView.vue +29 -0
  110. package/dist/runtime/extensions/views/TimelineView.vue.d.ts +9 -0
  111. package/dist/runtime/locale.d.ts +2 -0
  112. package/dist/runtime/locale.js +2 -0
  113. package/dist/runtime/plugin-abracadabra.client.js +107 -6
  114. package/dist/runtime/plugin-registry.d.ts +11 -30
  115. package/dist/runtime/plugin-registry.js +2 -82
  116. package/dist/runtime/plugins/core.plugin.js +10 -4
  117. package/dist/runtime/server/api/_abracadabra/spaces.get.d.ts +1 -1
  118. package/dist/runtime/server/plugins/abracadabra-service.js +28 -0
  119. package/dist/runtime/server/utils/docCache.js +24 -3
  120. package/dist/runtime/server/utils/schemaServerSupport.d.ts +52 -0
  121. package/dist/runtime/server/utils/schemaServerSupport.js +51 -0
  122. package/dist/runtime/types.d.ts +63 -46
  123. package/dist/runtime/utils/docTypes.d.ts +15 -0
  124. package/dist/runtime/utils/docTypes.js +20 -0
  125. package/dist/runtime/utils/loadCodeMirror.d.ts +32 -0
  126. package/dist/runtime/utils/loadCodeMirror.js +65 -0
  127. package/dist/runtime/utils/loadThree.d.ts +18 -0
  128. package/dist/runtime/utils/loadThree.js +46 -0
  129. package/dist/runtime/utils/markdownToYjs.d.ts +1 -23
  130. package/dist/runtime/utils/markdownToYjs.js +5 -440
  131. package/dist/runtime/utils/schemaSupport.d.ts +60 -0
  132. package/dist/runtime/utils/schemaSupport.js +40 -0
  133. package/dist/runtime/utils/yjsConvert.d.ts +1 -14
  134. package/dist/runtime/utils/yjsConvert.js +5 -331
  135. package/package.json +86 -21
@@ -0,0 +1,234 @@
1
+ import { ref, computed, shallowRef } from "vue";
2
+ import { useRuntimeConfig } from "#imports";
3
+ import { useInstalledPlugins } from "./useInstalledPlugins.js";
4
+ export function usePluginCatalog() {
5
+ const cfg = useRuntimeConfig();
6
+ const base = cfg.public.abracadabra?.pluginRegistry?.url || "http://127.0.0.1:8787";
7
+ const plugins = shallowRef([]);
8
+ const isLoading = ref(false);
9
+ const error = ref(null);
10
+ const pagination = ref(null);
11
+ const details = shallowRef({});
12
+ const policy = ref(null);
13
+ async function list(params = {}) {
14
+ isLoading.value = true;
15
+ error.value = null;
16
+ try {
17
+ const url = new URL(`${base.replace(/\/$/, "")}/v1/plugins`);
18
+ if (params.category) url.searchParams.set("category", params.category);
19
+ if (params.search) url.searchParams.set("search", params.search);
20
+ if (typeof params.limit === "number") url.searchParams.set("limit", String(params.limit));
21
+ if (typeof params.offset === "number") url.searchParams.set("offset", String(params.offset));
22
+ const result = await $fetch(url.toString());
23
+ plugins.value = result.plugins;
24
+ pagination.value = result.pagination;
25
+ return result;
26
+ } catch (e) {
27
+ error.value = e instanceof Error ? e : new Error(String(e));
28
+ throw error.value;
29
+ } finally {
30
+ isLoading.value = false;
31
+ }
32
+ }
33
+ async function get(id) {
34
+ if (details.value[id]) return details.value[id];
35
+ const detail = await $fetch(`${base.replace(/\/$/, "")}/v1/plugins/${encodeURIComponent(id)}`);
36
+ details.value = { ...details.value, [id]: detail };
37
+ return detail;
38
+ }
39
+ async function getLatest(id) {
40
+ return await $fetch(
41
+ `${base.replace(/\/$/, "")}/v1/plugins/${encodeURIComponent(id)}/latest`
42
+ );
43
+ }
44
+ async function getVersion(id, version) {
45
+ return await $fetch(
46
+ `${base.replace(/\/$/, "")}/v1/plugins/${encodeURIComponent(id)}/versions/${encodeURIComponent(version)}`
47
+ );
48
+ }
49
+ const installed = useInstalledPlugins();
50
+ function isInstalled(id) {
51
+ return installed.entries.value.some((e) => e.name === id || e.url.includes(`/${id}/`));
52
+ }
53
+ async function install(id) {
54
+ const decision = policyDecisionFor(id);
55
+ if (decision.state === "blocked") {
56
+ throw new Error(`install refused by server policy: ${decision.reason}`);
57
+ }
58
+ const latest = await getLatest(id);
59
+ if (!latest.artifact_url) {
60
+ throw new Error(
61
+ `plugin "${id}" version ${latest.version} has no artifact URL \u2014 registry artifact storage not configured yet`
62
+ );
63
+ }
64
+ installed.install(latest.artifact_url);
65
+ installed.updateMeta(latest.artifact_url, {
66
+ name: id,
67
+ version: latest.version,
68
+ label: latest.manifest.name ?? id,
69
+ description: latest.manifest.description
70
+ });
71
+ return latest;
72
+ }
73
+ async function installFromUrl(url) {
74
+ const p = policy.value;
75
+ if (p && !p.allow_unsafe_install) {
76
+ throw new Error(
77
+ "install refused by server policy: out-of-registry URL/file installs are disabled"
78
+ );
79
+ }
80
+ installed.install(url);
81
+ }
82
+ async function checkUpdates() {
83
+ const p = policy.value;
84
+ if (p && p.auto_update === "off") return [];
85
+ const outcomes = [];
86
+ for (const entry of installed.entries.value) {
87
+ if (!entry.name || !entry.version) continue;
88
+ let latest;
89
+ try {
90
+ latest = await getLatest(entry.name);
91
+ } catch {
92
+ continue;
93
+ }
94
+ const decision = policyDecisionFor(entry.name);
95
+ if (decision.state === "blocked") {
96
+ outcomes.push({
97
+ pluginId: entry.name,
98
+ fromVersion: entry.version,
99
+ toVersion: latest.version,
100
+ state: "skipped-blocked",
101
+ newCapabilities: [],
102
+ reason: decision.reason
103
+ });
104
+ continue;
105
+ }
106
+ const bump = semverBump(entry.version, latest.version);
107
+ if (!bump) continue;
108
+ if (!bumpAllowed(bump, p?.auto_update ?? "patch")) {
109
+ outcomes.push({
110
+ pluginId: entry.name,
111
+ fromVersion: entry.version,
112
+ toVersion: latest.version,
113
+ state: "skipped-policy",
114
+ newCapabilities: [],
115
+ reason: `policy is '${p?.auto_update ?? "patch"}', would not apply '${bump}' bump`
116
+ });
117
+ continue;
118
+ }
119
+ let installedCaps = [];
120
+ try {
121
+ const installedDetail = await getVersion(entry.name, entry.version);
122
+ installedCaps = installedDetail.manifest.capabilities.required ?? [];
123
+ } catch {
124
+ }
125
+ const latestCaps = latest.manifest.capabilities.required ?? [];
126
+ const newCaps = latestCaps.filter((c) => !installedCaps.includes(c));
127
+ const requireReview = p?.require_review_on_cap_growth ?? true;
128
+ if (newCaps.length > 0 && requireReview) {
129
+ outcomes.push({
130
+ pluginId: entry.name,
131
+ fromVersion: entry.version,
132
+ toVersion: latest.version,
133
+ state: "pending-review",
134
+ newCapabilities: newCaps,
135
+ reason: "new capabilities requested"
136
+ });
137
+ continue;
138
+ }
139
+ if (latest.artifact_url) {
140
+ installed.uninstall(entry.url);
141
+ installed.install(latest.artifact_url);
142
+ installed.updateMeta(latest.artifact_url, {
143
+ name: entry.name,
144
+ version: latest.version,
145
+ label: latest.manifest.name ?? entry.name,
146
+ description: latest.manifest.description
147
+ });
148
+ }
149
+ outcomes.push({
150
+ pluginId: entry.name,
151
+ fromVersion: entry.version,
152
+ toVersion: latest.version,
153
+ state: "applied",
154
+ newCapabilities: newCaps
155
+ });
156
+ }
157
+ return outcomes;
158
+ }
159
+ function capabilitiesFor(detail) {
160
+ const m = "manifest" in detail ? detail.manifest : detail;
161
+ return {
162
+ required: [...m.capabilities.required ?? []],
163
+ optional: [...m.capabilities.optional ?? []]
164
+ };
165
+ }
166
+ async function loadPolicy(serverUrl) {
167
+ const url = `${serverUrl.replace(/\/$/, "")}/plugins/policy`;
168
+ const fetched = await $fetch(url);
169
+ policy.value = fetched;
170
+ return fetched;
171
+ }
172
+ function policyDecisionFor(pluginId) {
173
+ const p = policy.value;
174
+ if (!p) return { state: "unknown" };
175
+ if (p.allowlist.includes(pluginId)) return { state: "allowed" };
176
+ if (p.allow_user_install) return { state: "gated" };
177
+ return {
178
+ state: "blocked",
179
+ reason: "Your server only permits plugins from its allowlist."
180
+ };
181
+ }
182
+ const categories = computed(() => {
183
+ const set = /* @__PURE__ */ new Set();
184
+ for (const p of plugins.value) for (const c of p.categories) set.add(c);
185
+ return [...set].sort();
186
+ });
187
+ return {
188
+ /** Public catalog (latest page fetched). Refresh via `list()`. */
189
+ plugins,
190
+ /** Distinct category tags across `plugins`, alphabetical. */
191
+ categories,
192
+ pagination,
193
+ isLoading,
194
+ error,
195
+ list,
196
+ get,
197
+ getLatest,
198
+ getVersion,
199
+ install,
200
+ installFromUrl,
201
+ isInstalled,
202
+ capabilitiesFor,
203
+ /** Current server policy. `null` until `loadPolicy()` resolves. */
204
+ policy,
205
+ loadPolicy,
206
+ /** Per-plugin install gate. Drives every Install button across the UI. */
207
+ policyDecisionFor,
208
+ checkUpdates
209
+ };
210
+ }
211
+ function semverBump(from, to) {
212
+ const a = parseSemver(from);
213
+ const b = parseSemver(to);
214
+ if (!a || !b) return null;
215
+ if (b.major > a.major) return "major";
216
+ if (b.major < a.major) return null;
217
+ if (b.minor > a.minor) return "minor";
218
+ if (b.minor < a.minor) return null;
219
+ if (b.patch > a.patch) return "patch";
220
+ return null;
221
+ }
222
+ function parseSemver(v) {
223
+ const cleaned = v.replace(/^v/, "").split(/[-+]/)[0];
224
+ if (!cleaned) return null;
225
+ const parts = cleaned.split(".").map((p) => Number.parseInt(p, 10));
226
+ if (parts.length !== 3 || parts.some((p) => Number.isNaN(p))) return null;
227
+ return { major: parts[0], minor: parts[1], patch: parts[2] };
228
+ }
229
+ function bumpAllowed(bump, policy) {
230
+ if (policy === "off") return false;
231
+ if (policy === "patch") return bump === "patch";
232
+ if (policy === "minor") return bump === "patch" || bump === "minor";
233
+ return true;
234
+ }
@@ -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
+ }