@abraca/nuxt 2.9.0 → 2.11.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 (70) hide show
  1. package/dist/module.d.mts +14 -0
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +2 -0
  4. package/dist/runtime/assets/editor.css +1 -1
  5. package/dist/runtime/components/AConnectionBadge.d.vue.ts +29 -0
  6. package/dist/runtime/components/AConnectionBadge.vue +79 -0
  7. package/dist/runtime/components/AConnectionBadge.vue.d.ts +29 -0
  8. package/dist/runtime/components/AEditor.d.vue.ts +2 -2
  9. package/dist/runtime/components/AEditor.vue +11 -1
  10. package/dist/runtime/components/AEditor.vue.d.ts +2 -2
  11. package/dist/runtime/components/AEncryptionModePicker.d.vue.ts +33 -0
  12. package/dist/runtime/components/AEncryptionModePicker.vue +211 -0
  13. package/dist/runtime/components/AEncryptionModePicker.vue.d.ts +33 -0
  14. package/dist/runtime/components/AModalShell.d.vue.ts +48 -0
  15. package/dist/runtime/components/AModalShell.vue +105 -0
  16. package/dist/runtime/components/AModalShell.vue.d.ts +48 -0
  17. package/dist/runtime/components/ANodePanel.d.vue.ts +8 -6
  18. package/dist/runtime/components/ANodePanel.vue +25 -0
  19. package/dist/runtime/components/ANodePanel.vue.d.ts +8 -6
  20. package/dist/runtime/components/ANodePanelHeader.d.vue.ts +20 -10
  21. package/dist/runtime/components/ANodePanelHeader.vue +17 -3
  22. package/dist/runtime/components/ANodePanelHeader.vue.d.ts +20 -10
  23. package/dist/runtime/components/ANodeSettingsPanel.d.vue.ts +2 -0
  24. package/dist/runtime/components/ANodeSettingsPanel.vue +21 -1
  25. package/dist/runtime/components/ANodeSettingsPanel.vue.d.ts +2 -0
  26. package/dist/runtime/components/ASnapshotPreviewModal.d.vue.ts +33 -0
  27. package/dist/runtime/components/ASnapshotPreviewModal.vue +430 -0
  28. package/dist/runtime/components/ASnapshotPreviewModal.vue.d.ts +33 -0
  29. package/dist/runtime/components/docs/ADocsSearch.d.vue.ts +2 -2
  30. package/dist/runtime/components/docs/ADocsSearch.vue.d.ts +2 -2
  31. package/dist/runtime/components/editor/ALocationPickerPopover.vue +28 -7
  32. package/dist/runtime/components/registry/APluginDetail.d.vue.ts +2 -2
  33. package/dist/runtime/components/registry/APluginDetail.vue.d.ts +2 -2
  34. package/dist/runtime/components/renderers/AProseRenderer.d.vue.ts +2 -2
  35. package/dist/runtime/components/renderers/AProseRenderer.vue.d.ts +2 -2
  36. package/dist/runtime/components/shell/ABreadcrumbForDoc.d.vue.ts +6 -0
  37. package/dist/runtime/components/shell/ABreadcrumbForDoc.vue +75 -3
  38. package/dist/runtime/components/shell/ABreadcrumbForDoc.vue.d.ts +6 -0
  39. package/dist/runtime/components/shell/ADocPanelServerSettings.d.vue.ts +17 -0
  40. package/dist/runtime/components/shell/ADocPanelServerSettings.vue +253 -0
  41. package/dist/runtime/components/shell/ADocPanelServerSettings.vue.d.ts +17 -0
  42. package/dist/runtime/components/shell/ADocPanelSettings.d.vue.ts +2 -0
  43. package/dist/runtime/components/shell/ADocPanelSettings.vue +15 -4
  44. package/dist/runtime/components/shell/ADocPanelSettings.vue.d.ts +2 -0
  45. package/dist/runtime/components/shell/AUserMenu.d.vue.ts +2 -2
  46. package/dist/runtime/components/shell/AUserMenu.vue.d.ts +2 -2
  47. package/dist/runtime/composables/useDocBreadcrumb.d.ts +17 -2
  48. package/dist/runtime/composables/useDocBreadcrumb.js +17 -3
  49. package/dist/runtime/composables/useDocSnapshots.d.ts +2 -1
  50. package/dist/runtime/composables/useDocSnapshots.js +5 -0
  51. package/dist/runtime/composables/useEditor.d.ts +1 -1
  52. package/dist/runtime/composables/useEditor.js +120 -0
  53. package/dist/runtime/composables/useEditorToolbar.d.ts +12 -4
  54. package/dist/runtime/composables/useEditorToolbar.js +78 -56
  55. package/dist/runtime/composables/useNodeContextMenu.d.ts +10 -0
  56. package/dist/runtime/composables/useNodeContextMenu.js +41 -1
  57. package/dist/runtime/composables/useSwipeGesture.d.ts +48 -0
  58. package/dist/runtime/composables/useSwipeGesture.js +140 -0
  59. package/dist/runtime/extensions/document-header.js +16 -6
  60. package/dist/runtime/extensions/document-meta.js +344 -19
  61. package/dist/runtime/extensions/meta-field.js +42 -0
  62. package/dist/runtime/extensions/views/DocumentMetaView.vue +33 -7
  63. package/dist/runtime/extensions/views/FieldView.vue +51 -19
  64. package/dist/runtime/extensions/views/MetaFieldView.vue +30 -4
  65. package/dist/runtime/middleware/abracadabra-auth.d.ts +1 -1
  66. package/dist/runtime/plugin-abracadabra.client.d.ts +1 -1
  67. package/dist/runtime/plugin-abracadabra.client.js +12 -2
  68. package/dist/runtime/plugin-abracadabra.server.d.ts +1 -1
  69. package/dist/runtime/plugin-shared-globals.client.d.ts +1 -1
  70. package/package.json +1 -4
package/dist/module.d.mts CHANGED
@@ -30,6 +30,7 @@ declare module '@nuxt/schema' {
30
30
  abracadabra: {
31
31
  url: string;
32
32
  entryDocId: string;
33
+ pinServer: boolean;
33
34
  persistAuth: boolean;
34
35
  authStorageKey: string;
35
36
  disabledBuiltins: string[];
@@ -115,6 +116,19 @@ interface ModuleOptions {
115
116
  * entryDocId > saved server entryDocId > first Space (kind="space" top-level doc)
116
117
  */
117
118
  entryDocId?: string;
119
+ /**
120
+ * Pin the app to the configured `url` and ignore any persisted active
121
+ * server (`abracadabra_current_server`) or `?server=` deep-link override.
122
+ * Default: false — the switcher's last-used / deep-linked server wins, which
123
+ * is what multi-server apps (arcana, aperio) want.
124
+ *
125
+ * Set `true` for single-server consumers (marketing, landing) so a visitor
126
+ * who once connected the origin to another server can't get silently pinned
127
+ * to it on later loads while `entryDocId` still points at the configured
128
+ * server's doc — the failure mode is a phantom/empty entry doc with no sync
129
+ * or awareness. When `true`, boot always uses `url`.
130
+ */
131
+ pinServer?: boolean;
118
132
  /**
119
133
  * Whether to persist the JWT token across page reloads via localStorage.
120
134
  * Default: true.
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": ">=4.0.0"
6
6
  },
7
- "version": "2.9.0",
7
+ "version": "2.11.0",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.2",
10
10
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -19,6 +19,7 @@ const module$1 = defineNuxtModule({
19
19
  },
20
20
  defaults: {
21
21
  url: process.env.ABRACADABRA_URL ?? "https://abra.cou.sh",
22
+ pinServer: false,
22
23
  persistAuth: true,
23
24
  authStorageKey: "abracadabra:auth",
24
25
  disabledBuiltins: [],
@@ -85,6 +86,7 @@ const module$1 = defineNuxtModule({
85
86
  },
86
87
  url: options.url,
87
88
  entryDocId: options.entryDocId ?? "",
89
+ pinServer: options.pinServer ?? false,
88
90
  persistAuth: options.persistAuth,
89
91
  authStorageKey: options.authStorageKey,
90
92
  disabledBuiltins: options.disabledBuiltins,
@@ -1 +1 @@
1
- html.dark .tiptap .shiki,html.dark .tiptap .shiki span{background-color:var(--ui-bg-muted)!important;color:var(--shiki-dark)!important}.collaboration-carets__caret{border-left:1px solid #0d0d0d;border-right:1px solid #0d0d0d;margin-left:-1px;margin-right:-1px;opacity:0;pointer-events:none;position:relative;transition:opacity .3s ease;word-break:normal}.collaboration-carets__caret.is-hidden{display:none}.collaboration-carets__caret.is-active{opacity:1}.collaboration-carets__label{border-radius:3px 3px 3px 0;color:#0d0d0d;font-size:12px;font-style:normal;font-weight:600;left:-1px;line-height:normal;padding:.1rem .3rem;position:absolute;top:-1.4em;-webkit-user-select:none;-moz-user-select:none;user-select:none;white-space:nowrap}.ProseMirror-yjs-selection,.collaboration-carets__selection{background-color:var(--collaboration-selection-color)!important;pointer-events:none}.search-highlight{background-color:color-mix(in srgb,var(--color-primary-400) 35%,transparent);border-radius:2px;padding:0 1px}.doc-passage-highlight{background-color:color-mix(in srgb,var(--color-success-400) 35%,transparent);border-radius:2px;padding:0 1px}
1
+ html.dark .tiptap .shiki,html.dark .tiptap .shiki span{background-color:var(--ui-bg-muted)!important;color:var(--shiki-dark)!important}.collaboration-carets__caret{border-left:1px solid #0d0d0d;border-right:1px solid #0d0d0d;margin-left:-1px;margin-right:-1px;opacity:0;pointer-events:none;position:relative;transition:opacity .3s ease;word-break:normal}.collaboration-carets__caret.is-hidden{display:none}.collaboration-carets__caret.is-active{opacity:1}.collaboration-carets__label{border-radius:3px 3px 3px 0;color:#0d0d0d;font-size:12px;font-style:normal;font-weight:600;left:-1px;line-height:normal;padding:.1rem .3rem;position:absolute;top:-1.4em;-webkit-user-select:none;-moz-user-select:none;user-select:none;white-space:nowrap}.ProseMirror-yjs-selection,.collaboration-carets__selection{background-color:var(--collaboration-selection-color)!important;pointer-events:none}.search-highlight{background-color:color-mix(in srgb,var(--color-primary-400) 35%,transparent);border-radius:2px;padding:0 1px}.doc-passage-highlight{background-color:color-mix(in srgb,var(--color-success-400) 35%,transparent);border-radius:2px;padding:0 1px}.tiptap{min-height:100%;padding-bottom:8rem}.tiptap.file-drop-active{border-radius:4px;outline:2px dashed var(--ui-primary);outline-offset:4px}.tiptap .document-header{font-size:2.5rem;font-weight:800;letter-spacing:-.025em;line-height:1.2;margin-bottom:1rem}.tiptap [data-type=document-meta]{align-items:center;display:flex;flex-wrap:wrap;font-size:.875rem;gap:.375rem;line-height:1.75rem;margin-bottom:1.5rem;min-height:1.75rem}.tiptap [data-type=document-meta][data-cursor-in-meta=true][data-empty=true]:after{color:var(--ui-text-dimmed);content:"Type '/' to add a property…";pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.prose-variant .tiptap [data-type=document-meta]{display:none}
@@ -0,0 +1,29 @@
1
+ type __VLS_Props = {
2
+ /** The document's child provider (AbracadabraProvider or a local wrapper). */
3
+ provider?: any;
4
+ /**
5
+ * Override the four status strings. `label` is the short status word
6
+ * (also the aria-label); `tooltip` is the longer hover explanation.
7
+ */
8
+ labels?: Partial<{
9
+ authFailedCached: {
10
+ label: string;
11
+ tooltip: string;
12
+ };
13
+ authFailedNoCache: {
14
+ label: string;
15
+ tooltip: string;
16
+ };
17
+ offlineCached: {
18
+ label: string;
19
+ tooltip: string;
20
+ };
21
+ offlineNoCache: {
22
+ label: string;
23
+ tooltip: string;
24
+ };
25
+ }>;
26
+ };
27
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
28
+ declare const _default: typeof __VLS_export;
29
+ export default _default;
@@ -0,0 +1,79 @@
1
+ <script setup>
2
+ import { computed, ref, watch } from "vue";
3
+ const props = defineProps({
4
+ provider: { type: null, required: false },
5
+ labels: { type: Object, required: false }
6
+ });
7
+ const DEFAULTS = {
8
+ authFailedCached: {
9
+ label: "Cached copy",
10
+ tooltip: "Server rejected the connection \u2014 showing the last cached copy."
11
+ },
12
+ authFailedNoCache: {
13
+ label: "No access",
14
+ tooltip: "Server rejected the connection and no cached copy is available."
15
+ },
16
+ offlineCached: {
17
+ label: "Offline",
18
+ tooltip: "Server is unreachable \u2014 showing the last cached copy. Edits will sync when you reconnect."
19
+ },
20
+ offlineNoCache: {
21
+ label: "Not yet synced",
22
+ tooltip: "Server is unreachable and this document hasn't been synced to this device yet. Reconnect to load it."
23
+ }
24
+ };
25
+ const isRemoteProvider = computed(
26
+ () => !!props.provider && typeof props.provider.connectionStatus === "string"
27
+ );
28
+ const connStatus = ref("connecting");
29
+ const authFailed = ref(false);
30
+ watch(
31
+ () => props.provider,
32
+ (p, _old, onCleanup) => {
33
+ if (!p || typeof p.connectionStatus !== "string") return;
34
+ connStatus.value = p.connectionStatus ?? "disconnected";
35
+ authFailed.value = false;
36
+ const onStatus = (e) => {
37
+ connStatus.value = e?.status ?? "disconnected";
38
+ if (e?.status === "connected") authFailed.value = false;
39
+ };
40
+ const onAuthFail = () => {
41
+ authFailed.value = true;
42
+ };
43
+ p.on?.("status", onStatus);
44
+ p.on?.("authenticationFailed", onAuthFail);
45
+ onCleanup(() => {
46
+ p.off?.("status", onStatus);
47
+ p.off?.("authenticationFailed", onAuthFail);
48
+ });
49
+ },
50
+ { immediate: true }
51
+ );
52
+ const isOffline = computed(
53
+ () => isRemoteProvider.value && (authFailed.value || connStatus.value !== "connected")
54
+ );
55
+ const badge = computed(() => {
56
+ if (!isRemoteProvider.value || !isOffline.value) return null;
57
+ const hasCache = !!props.provider?.hasCachedContent;
58
+ const strings = { ...DEFAULTS, ...props.labels };
59
+ if (authFailed.value) {
60
+ return hasCache ? strings.authFailedCached : strings.authFailedNoCache;
61
+ }
62
+ return hasCache ? strings.offlineCached : strings.offlineNoCache;
63
+ });
64
+ </script>
65
+
66
+ <template>
67
+ <UTooltip
68
+ v-if="badge"
69
+ :text="badge.tooltip"
70
+ :content="{ side: 'bottom' }"
71
+ >
72
+ <span
73
+ class="size-2 rounded-full shrink-0 ring-1 ring-inset ring-black/10"
74
+ :class="authFailed ? 'bg-(--ui-color-error-500)' : 'bg-(--ui-color-warning-500)'"
75
+ role="status"
76
+ :aria-label="badge.label"
77
+ />
78
+ </UTooltip>
79
+ </template>
@@ -0,0 +1,29 @@
1
+ type __VLS_Props = {
2
+ /** The document's child provider (AbracadabraProvider or a local wrapper). */
3
+ provider?: any;
4
+ /**
5
+ * Override the four status strings. `label` is the short status word
6
+ * (also the aria-label); `tooltip` is the longer hover explanation.
7
+ */
8
+ labels?: Partial<{
9
+ authFailedCached: {
10
+ label: string;
11
+ tooltip: string;
12
+ };
13
+ authFailedNoCache: {
14
+ label: string;
15
+ tooltip: string;
16
+ };
17
+ offlineCached: {
18
+ label: string;
19
+ tooltip: string;
20
+ };
21
+ offlineNoCache: {
22
+ label: string;
23
+ tooltip: string;
24
+ };
25
+ }>;
26
+ };
27
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
28
+ declare const _default: typeof __VLS_export;
29
+ export default _default;
@@ -73,16 +73,16 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
73
73
  editor: import("vue").ComputedRef<any>;
74
74
  }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
75
75
  rename: (label: string) => any;
76
+ ready: () => any;
76
77
  update: (content: any) => any;
77
78
  "update:modelValue": (value: any) => any;
78
79
  updateMeta: (patch: Partial<DocPageMeta>) => any;
79
- ready: () => any;
80
80
  }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
81
81
  onRename?: ((label: string) => any) | undefined;
82
+ onReady?: (() => any) | undefined;
82
83
  onUpdate?: ((content: any) => any) | undefined;
83
84
  "onUpdate:modelValue"?: ((value: any) => any) | undefined;
84
85
  onUpdateMeta?: ((patch: Partial<DocPageMeta>) => any) | undefined;
85
- onReady?: (() => any) | undefined;
86
86
  }>, {
87
87
  contentType: "json" | "html" | "markdown";
88
88
  editable: boolean;
@@ -6,6 +6,7 @@ import { useSyncedMap } from "../composables/useYDoc";
6
6
  import { resolveDocType, GEO_TYPE_META_SCHEMAS } from "../utils/docTypes";
7
7
  import { schemaFieldToAttrs, userFieldToAttrs } from "../extensions/meta-field";
8
8
  import { useEditor } from "../composables/useEditor";
9
+ import { useDocTree } from "../composables/useDocTree";
9
10
  import { useEditorToolbar } from "../composables/useEditorToolbar";
10
11
  import { useEditorSuggestions } from "../composables/useEditorSuggestions";
11
12
  import { useEditorDragHandle } from "../composables/useEditorDragHandle";
@@ -57,12 +58,21 @@ watch(ready, (val) => {
57
58
  const editorRef = ref(null);
58
59
  const tiptapEditor = computed(() => editorRef.value?.editor ?? null);
59
60
  defineExpose({ editor: tiptapEditor });
60
- const { items: toolbarItems } = useEditorToolbar({ docId: props.docId });
61
+ const { bubbleItems: toolbarItems } = useEditorToolbar({ docId: props.docId });
62
+ const docTree = useDocTree();
61
63
  const resolvedMetaSchema = computed(() => {
62
64
  if (props.metaSchema) return props.metaSchema;
63
65
  const geoType = props.docMeta?.geoType;
64
66
  if (geoType && geoType in GEO_TYPE_META_SCHEMAS)
65
67
  return GEO_TYPE_META_SCHEMAS[geoType];
68
+ let pid = docTree.getEntry(props.docId)?.parentId ?? void 0;
69
+ while (pid) {
70
+ const entry = docTree.getEntry(pid);
71
+ if (!entry) break;
72
+ const schema = resolveDocType(entry.type, registry).metaSchema;
73
+ if (schema && schema.length > 0) return schema;
74
+ pid = entry.parentId ?? void 0;
75
+ }
66
76
  return resolveDocType(props.parentType, registry).metaSchema ?? [];
67
77
  });
68
78
  const { items: allSuggestionItems, propertiesOnlyItems } = useEditorSuggestions({ docId: props.docId });
@@ -73,16 +73,16 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
73
73
  editor: import("vue").ComputedRef<any>;
74
74
  }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
75
75
  rename: (label: string) => any;
76
+ ready: () => any;
76
77
  update: (content: any) => any;
77
78
  "update:modelValue": (value: any) => any;
78
79
  updateMeta: (patch: Partial<DocPageMeta>) => any;
79
- ready: () => any;
80
80
  }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
81
81
  onRename?: ((label: string) => any) | undefined;
82
+ onReady?: (() => any) | undefined;
82
83
  onUpdate?: ((content: any) => any) | undefined;
83
84
  "onUpdate:modelValue"?: ((value: any) => any) | undefined;
84
85
  onUpdateMeta?: ((patch: Partial<DocPageMeta>) => any) | undefined;
85
- onReady?: (() => any) | undefined;
86
86
  }>, {
87
87
  contentType: "json" | "html" | "markdown";
88
88
  editable: boolean;
@@ -0,0 +1,33 @@
1
+ import type { DocEncryptionInfo } from '@abraca/dabra';
2
+ type __VLS_Props = {
3
+ docId: string;
4
+ /** Override any displayed string. */
5
+ labels?: Partial<typeof DEFAULTS>;
6
+ };
7
+ declare const DEFAULTS: {
8
+ noneTitle: string;
9
+ noneDesc: string;
10
+ cseTitle: string;
11
+ cseDesc: string;
12
+ e2eTitle: string;
13
+ e2eDesc: string;
14
+ cannotDowngrade: string;
15
+ loadFailed: string;
16
+ retry: string;
17
+ enableE2ETitle: string;
18
+ cannotBeUndone: string;
19
+ warningServerCantRead: string;
20
+ warningNoSearch: string;
21
+ warningNoOffline: string;
22
+ warningKeysRequired: string;
23
+ warningCannotUndo: string;
24
+ cancel: string;
25
+ enableE2E: string;
26
+ };
27
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
28
+ updated: (args_0: DocEncryptionInfo) => any;
29
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
30
+ onUpdated?: ((args_0: DocEncryptionInfo) => any) | undefined;
31
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
32
+ declare const _default: typeof __VLS_export;
33
+ export default _default;
@@ -0,0 +1,211 @@
1
+ <script setup>
2
+ import { computed, onMounted, ref } from "vue";
3
+ import { useAbracadabra } from "../composables/useAbracadabra";
4
+ import AModalShell from "./AModalShell.vue";
5
+ const props = defineProps({
6
+ docId: { type: String, required: true },
7
+ labels: { type: Object, required: false }
8
+ });
9
+ const emit = defineEmits(["updated"]);
10
+ const DEFAULTS = {
11
+ noneTitle: "No encryption",
12
+ noneDesc: "The server stores and can read this document. Full-text search, offline cache, and previews all work.",
13
+ cseTitle: "Client-side encryption",
14
+ cseDesc: "Encrypted before it leaves your device. The server stores ciphertext but can still coordinate sync.",
15
+ e2eTitle: "End-to-end encryption",
16
+ e2eDesc: "Only people with the document key can read it. The server never sees plaintext.",
17
+ cannotDowngrade: "Encryption can only be strengthened, never reduced.",
18
+ loadFailed: "Couldn't load encryption settings",
19
+ retry: "Retry",
20
+ enableE2ETitle: "Enable end-to-end encryption?",
21
+ cannotBeUndone: "This cannot be undone.",
22
+ warningServerCantRead: "The server will no longer be able to read this document.",
23
+ warningNoSearch: "Server-side full-text search will stop working.",
24
+ warningNoOffline: "Some offline and preview features may be limited.",
25
+ warningKeysRequired: "Anyone who needs access must hold the document key.",
26
+ warningCannotUndo: "You cannot switch back to a weaker mode later.",
27
+ cancel: "Cancel",
28
+ enableE2E: "Enable end-to-end encryption"
29
+ };
30
+ const t = computed(() => ({ ...DEFAULTS, ...props.labels }));
31
+ const { client } = useAbracadabra();
32
+ const encInfo = ref(null);
33
+ const loading = ref(true);
34
+ const error = ref(null);
35
+ const saving = ref(false);
36
+ const showE2EConfirm = ref(false);
37
+ const pendingMode = ref("none");
38
+ const STRICTNESS = { none: 0, cse: 1, e2e: 2 };
39
+ const modes = computed(() => {
40
+ const effective = encInfo.value?.effective_mode ?? "none";
41
+ const current = STRICTNESS[effective];
42
+ return [
43
+ {
44
+ value: "none",
45
+ label: t.value.noneTitle,
46
+ description: t.value.noneDesc,
47
+ icon: "i-lucide-lock-open",
48
+ disabled: current > 0
49
+ },
50
+ {
51
+ value: "cse",
52
+ label: t.value.cseTitle,
53
+ description: t.value.cseDesc,
54
+ icon: "i-lucide-shield",
55
+ disabled: current > 1
56
+ },
57
+ {
58
+ value: "e2e",
59
+ label: t.value.e2eTitle,
60
+ description: t.value.e2eDesc,
61
+ icon: "i-lucide-lock-keyhole",
62
+ disabled: false
63
+ }
64
+ ];
65
+ });
66
+ const selected = computed(() => encInfo.value?.effective_mode ?? "none");
67
+ async function load() {
68
+ loading.value = true;
69
+ error.value = null;
70
+ try {
71
+ encInfo.value = await client.value.getDocEncryption(props.docId);
72
+ } catch (e) {
73
+ error.value = String(e);
74
+ } finally {
75
+ loading.value = false;
76
+ }
77
+ }
78
+ function handleSelect(mode) {
79
+ if (mode === selected.value) return;
80
+ pendingMode.value = mode;
81
+ if (mode === "e2e") {
82
+ showE2EConfirm.value = true;
83
+ } else {
84
+ void applyMode(mode);
85
+ }
86
+ }
87
+ async function applyMode(mode) {
88
+ showE2EConfirm.value = false;
89
+ saving.value = true;
90
+ try {
91
+ await client.value.setDocEncryption(props.docId, mode);
92
+ await load();
93
+ if (encInfo.value) emit("updated", encInfo.value);
94
+ } catch (e) {
95
+ error.value = String(e);
96
+ } finally {
97
+ saving.value = false;
98
+ }
99
+ }
100
+ onMounted(load);
101
+ </script>
102
+
103
+ <template>
104
+ <div class="space-y-3">
105
+ <div
106
+ v-if="loading"
107
+ class="space-y-2"
108
+ >
109
+ <USkeleton class="h-14 w-full rounded-lg" />
110
+ <USkeleton class="h-14 w-full rounded-lg" />
111
+ <USkeleton class="h-14 w-full rounded-lg" />
112
+ </div>
113
+
114
+ <UAlert
115
+ v-else-if="error"
116
+ color="error"
117
+ variant="soft"
118
+ icon="i-lucide-alert-circle"
119
+ :title="t.loadFailed"
120
+ :description="error"
121
+ >
122
+ <template #actions>
123
+ <UButton
124
+ size="sm"
125
+ color="error"
126
+ variant="soft"
127
+ :label="t.retry"
128
+ @click="load"
129
+ />
130
+ </template>
131
+ </UAlert>
132
+
133
+ <template v-else>
134
+ <URadioGroup
135
+ :model-value="selected"
136
+ :items="modes"
137
+ value-key="value"
138
+ variant="card"
139
+ color="primary"
140
+ size="md"
141
+ :ui="{ fieldset: 'gap-2', item: 'w-full items-start', wrapper: 'flex-1', label: 'flex items-center gap-2 font-medium text-default' }"
142
+ :disabled="saving"
143
+ @update:model-value="(v) => handleSelect(v)"
144
+ >
145
+ <template #label="{ item }">
146
+ <UIcon
147
+ :name="item.icon"
148
+ class="size-4 shrink-0 text-(--ui-text-muted)"
149
+ />
150
+ <span>{{ item.label }}</span>
151
+ </template>
152
+ <template #description="{ item }">
153
+ <span class="text-xs leading-snug text-(--ui-text-muted)">{{ item.description }}</span>
154
+ </template>
155
+ </URadioGroup>
156
+
157
+ <UAlert
158
+ v-if="selected !== 'none'"
159
+ color="warning"
160
+ variant="subtle"
161
+ size="sm"
162
+ icon="i-lucide-alert-triangle"
163
+ :title="t.cannotDowngrade"
164
+ />
165
+ </template>
166
+
167
+ <AModalShell
168
+ :open="showE2EConfirm"
169
+ max-width="sm:max-w-md"
170
+ @update:open="showE2EConfirm = $event"
171
+ >
172
+ <div class="flex flex-col gap-4">
173
+ <h2 class="text-base font-semibold text-(--ui-text-highlighted)">
174
+ {{ t.enableE2ETitle }}
175
+ </h2>
176
+ <UAlert
177
+ color="warning"
178
+ icon="i-lucide-alert-triangle"
179
+ variant="soft"
180
+ :title="t.cannotBeUndone"
181
+ >
182
+ <template #description>
183
+ <ul class="list-disc pl-4 mt-1 space-y-1 text-sm">
184
+ <li>{{ t.warningServerCantRead }}</li>
185
+ <li>{{ t.warningNoSearch }}</li>
186
+ <li>{{ t.warningNoOffline }}</li>
187
+ <li>{{ t.warningKeysRequired }}</li>
188
+ <li><strong>{{ t.warningCannotUndo }}</strong></li>
189
+ </ul>
190
+ </template>
191
+ </UAlert>
192
+ </div>
193
+ <template #footer>
194
+ <div class="flex gap-2 justify-end w-full">
195
+ <UButton
196
+ color="neutral"
197
+ variant="ghost"
198
+ :label="t.cancel"
199
+ @click="showE2EConfirm = false"
200
+ />
201
+ <UButton
202
+ color="error"
203
+ :loading="saving"
204
+ :label="t.enableE2E"
205
+ @click="applyMode('e2e')"
206
+ />
207
+ </div>
208
+ </template>
209
+ </AModalShell>
210
+ </div>
211
+ </template>
@@ -0,0 +1,33 @@
1
+ import type { DocEncryptionInfo } from '@abraca/dabra';
2
+ type __VLS_Props = {
3
+ docId: string;
4
+ /** Override any displayed string. */
5
+ labels?: Partial<typeof DEFAULTS>;
6
+ };
7
+ declare const DEFAULTS: {
8
+ noneTitle: string;
9
+ noneDesc: string;
10
+ cseTitle: string;
11
+ cseDesc: string;
12
+ e2eTitle: string;
13
+ e2eDesc: string;
14
+ cannotDowngrade: string;
15
+ loadFailed: string;
16
+ retry: string;
17
+ enableE2ETitle: string;
18
+ cannotBeUndone: string;
19
+ warningServerCantRead: string;
20
+ warningNoSearch: string;
21
+ warningNoOffline: string;
22
+ warningKeysRequired: string;
23
+ warningCannotUndo: string;
24
+ cancel: string;
25
+ enableE2E: string;
26
+ };
27
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
28
+ updated: (args_0: DocEncryptionInfo) => any;
29
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
30
+ onUpdated?: ((args_0: DocEncryptionInfo) => any) | undefined;
31
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
32
+ declare const _default: typeof __VLS_export;
33
+ export default _default;
@@ -0,0 +1,48 @@
1
+ type __VLS_Props = {
2
+ open: boolean;
3
+ /** Tailwind max-width class for the modal content (e.g. `sm:max-w-3xl`). */
4
+ maxWidth?: string;
5
+ closeable?: boolean;
6
+ /** Standard modal heading — guarantees the canonical look every modal shares. */
7
+ title?: string;
8
+ /** Optional muted line under the title (e.g. a short description). */
9
+ subtitle?: string;
10
+ /** When false, Esc / overlay-click cannot dismiss the modal (forced choice). */
11
+ dismissible?: boolean;
12
+ /** Enable horizontal swipe gestures for multi-step navigation. */
13
+ swipeable?: boolean;
14
+ /** Whether forward (swipe left) navigation is available. */
15
+ canSwipeForward?: boolean;
16
+ /** Whether backward (swipe right) navigation is available. */
17
+ canSwipeBack?: boolean;
18
+ };
19
+ declare var __VLS_19: {}, __VLS_21: {};
20
+ type __VLS_Slots = {} & {
21
+ default?: (props: typeof __VLS_19) => any;
22
+ } & {
23
+ footer?: (props: typeof __VLS_21) => any;
24
+ };
25
+ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
26
+ "update:open": (v: boolean) => any;
27
+ "swipe-forward": () => any;
28
+ "swipe-back": () => any;
29
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
30
+ "onUpdate:open"?: ((v: boolean) => any) | undefined;
31
+ "onSwipe-forward"?: (() => any) | undefined;
32
+ "onSwipe-back"?: (() => any) | undefined;
33
+ }>, {
34
+ maxWidth: string;
35
+ closeable: boolean;
36
+ dismissible: boolean;
37
+ swipeable: boolean;
38
+ canSwipeForward: boolean;
39
+ canSwipeBack: boolean;
40
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
41
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
42
+ declare const _default: typeof __VLS_export;
43
+ export default _default;
44
+ type __VLS_WithSlots<T, S> = T & {
45
+ new (): {
46
+ $slots: S;
47
+ };
48
+ };