@abraca/nuxt 2.14.0 → 2.16.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 (44) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/runtime/assets/editor.css +3 -1
  3. package/dist/runtime/components/ACodeEditor.vue +16 -2
  4. package/dist/runtime/components/ANodePanel.vue +7 -5
  5. package/dist/runtime/components/aware/ASlider.d.vue.ts +1 -1
  6. package/dist/runtime/components/aware/ASlider.vue.d.ts +1 -1
  7. package/dist/runtime/components/docs/ADocsSearchButton.d.vue.ts +1 -1
  8. package/dist/runtime/components/docs/ADocsSearchButton.vue.d.ts +1 -1
  9. package/dist/runtime/components/editor/AColorPalettePopover.vue +97 -5
  10. package/dist/runtime/components/editor/AIconPickerPopover.vue +81 -3
  11. package/dist/runtime/components/editor/AMetaNumberStepper.d.vue.ts +40 -0
  12. package/dist/runtime/components/editor/AMetaNumberStepper.vue +214 -0
  13. package/dist/runtime/components/editor/AMetaNumberStepper.vue.d.ts +40 -0
  14. package/dist/runtime/components/registry/APluginBrowser.vue +18 -2
  15. package/dist/runtime/components/renderers/ACalendarRenderer.vue +7 -1
  16. package/dist/runtime/components/renderers/calendar/ACalendarToolbar.d.vue.ts +2 -2
  17. package/dist/runtime/components/renderers/calendar/ACalendarToolbar.vue.d.ts +2 -2
  18. package/dist/runtime/components/renderers/media/MediaTransportBar.d.vue.ts +2 -2
  19. package/dist/runtime/components/renderers/media/MediaTransportBar.vue.d.ts +2 -2
  20. package/dist/runtime/components/settings/APluginInstallDialog.d.vue.ts +39 -0
  21. package/dist/runtime/components/settings/APluginInstallDialog.vue +254 -0
  22. package/dist/runtime/components/settings/APluginInstallDialog.vue.d.ts +39 -0
  23. package/dist/runtime/components/settings/APluginsTabInstalled.d.vue.ts +7 -0
  24. package/dist/runtime/components/settings/APluginsTabInstalled.vue +413 -0
  25. package/dist/runtime/components/settings/APluginsTabInstalled.vue.d.ts +7 -0
  26. package/dist/runtime/components/settings/APluginsTabPending.d.vue.ts +24 -0
  27. package/dist/runtime/components/settings/APluginsTabPending.vue +248 -0
  28. package/dist/runtime/components/settings/APluginsTabPending.vue.d.ts +24 -0
  29. package/dist/runtime/components/settings/ASettingsPluginsPanel.d.vue.ts +14 -1
  30. package/dist/runtime/components/settings/ASettingsPluginsPanel.vue +34 -80
  31. package/dist/runtime/components/settings/ASettingsPluginsPanel.vue.d.ts +14 -1
  32. package/dist/runtime/composables/useDeclinedSpacePlugins.d.ts +7 -0
  33. package/dist/runtime/composables/useDeclinedSpacePlugins.js +24 -0
  34. package/dist/runtime/composables/useLoadTimePending.d.ts +29 -0
  35. package/dist/runtime/composables/useLoadTimePending.js +37 -0
  36. package/dist/runtime/composables/usePluginCatalog.d.ts +5 -1
  37. package/dist/runtime/composables/usePluginCatalog.js +34 -0
  38. package/dist/runtime/composables/useTouchDrag.d.ts +21 -4
  39. package/dist/runtime/composables/useTouchDrag.js +30 -0
  40. package/dist/runtime/composables/useUploadedPluginStore.d.ts +43 -0
  41. package/dist/runtime/composables/useUploadedPluginStore.js +66 -0
  42. package/dist/runtime/extensions/views/MetaFieldView.vue +17 -28
  43. package/dist/runtime/plugin-abracadabra.client.js +48 -1
  44. package/package.json +1 -1
@@ -0,0 +1,24 @@
1
+ import type { PluginManifest } from '@abraca/plugin';
2
+ export interface PendingSpacePluginEntry {
3
+ /** Plugin id from the manifest. */
4
+ id: string;
5
+ /** Resolved bundle URL. */
6
+ url: string;
7
+ /** Version, when the space-plugins map records one. */
8
+ version?: string;
9
+ /** Resolved manifest, when the URL exposed a sibling JSON. */
10
+ manifest?: PluginManifest;
11
+ }
12
+ type __VLS_Props = {
13
+ /**
14
+ * Space-declared plugins that did NOT auto-load (not in registry).
15
+ * Pre-filtered by the host's space-plugins watcher. When omitted, the
16
+ * tab shows the empty-state explainer.
17
+ */
18
+ declared?: readonly PendingSpacePluginEntry[];
19
+ /** Active space id. Required for decline persistence to be per-space. */
20
+ spaceId?: string;
21
+ };
22
+ 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>;
23
+ declare const _default: typeof __VLS_export;
24
+ export default _default;
@@ -1,3 +1,16 @@
1
- declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
1
+ export interface BuiltinPluginEntry {
2
+ name: string;
3
+ label?: string;
4
+ version?: string;
5
+ description?: string;
6
+ icon?: string;
7
+ }
8
+ type __VLS_Props = {
9
+ /** Host-registered first-party plugins; toggle state lives in localStorage. */
10
+ builtins?: readonly BuiltinPluginEntry[];
11
+ /** Initial tab. */
12
+ initialTab?: 'browse' | 'installed' | 'pending';
13
+ };
14
+ 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>;
2
15
  declare const _default: typeof __VLS_export;
3
16
  export default _default;
@@ -1,96 +1,50 @@
1
1
  <script setup>
2
- import { computed } from "vue";
3
- import { usePluginRegistry } from "../../composables/usePluginRegistry";
4
- const registry = usePluginRegistry();
5
- const pluginList = computed(
6
- () => registry.getPlugins().map((p) => ({
7
- name: p.name,
8
- description: p.description ?? "",
9
- icon: p.icon ?? "i-lucide-plug",
10
- hasExtensions: !!p.extensions?.length,
11
- hasPageTypes: !!p.pageTypes?.length,
12
- hasToolbar: !!p.toolbarItems?.length,
13
- hasSuggestions: !!p.suggestionItems?.length
14
- }))
2
+ import { computed, ref } from "vue";
3
+ import { useAbracadabra } from "../../composables/useAbracadabra";
4
+ const props = defineProps({
5
+ builtins: { type: Array, required: false },
6
+ initialTab: { type: String, required: false }
7
+ });
8
+ const tab = ref(props.initialTab ?? "installed");
9
+ const builtins = computed(() => props.builtins ?? []);
10
+ const abra = useAbracadabra();
11
+ const serverUrl = computed(
12
+ () => abra.currentServerUrl?.value || void 0
15
13
  );
14
+ const tabItems = computed(() => [
15
+ { label: "Browse", value: "browse", icon: "i-lucide-store" },
16
+ { label: "Installed", value: "installed", icon: "i-lucide-package" },
17
+ { label: "Pending", value: "pending", icon: "i-lucide-hourglass" }
18
+ ]);
16
19
  </script>
17
20
 
18
21
  <template>
19
- <div>
20
- <div class="mb-4">
22
+ <div class="flex flex-col gap-4 max-w-3xl">
23
+ <div>
21
24
  <h3 class="text-base font-semibold">
22
25
  Plugins
23
26
  </h3>
24
27
  <p class="text-sm text-(--ui-text-muted) mt-1">
25
- Installed plugins extending the editor and platform.
28
+ Browse the registry, manage installed plugins, and review what the active space is asking to load.
26
29
  </p>
27
30
  </div>
28
31
 
29
- <div class="flex flex-col gap-2">
30
- <div
31
- v-for="plugin in pluginList"
32
- :key="plugin.name"
33
- class="flex items-center gap-3 p-3 rounded-lg border border-(--ui-border)"
34
- >
35
- <UIcon
36
- :name="plugin.icon"
37
- class="size-5 text-(--ui-text-muted) shrink-0"
38
- />
39
- <div class="flex-1 min-w-0">
40
- <p class="text-sm font-medium">
41
- {{ plugin.name }}
42
- </p>
43
- <p
44
- v-if="plugin.description"
45
- class="text-xs text-(--ui-text-muted) mt-0.5"
46
- >
47
- {{ plugin.description }}
48
- </p>
49
- <div class="flex flex-wrap gap-1 mt-1">
50
- <UBadge
51
- v-if="plugin.hasExtensions"
52
- color="info"
53
- variant="subtle"
54
- label="Extensions"
55
- size="xs"
56
- />
57
- <UBadge
58
- v-if="plugin.hasPageTypes"
59
- color="success"
60
- variant="subtle"
61
- label="Page Types"
62
- size="xs"
63
- />
64
- <UBadge
65
- v-if="plugin.hasToolbar"
66
- color="warning"
67
- variant="subtle"
68
- label="Toolbar"
69
- size="xs"
70
- />
71
- <UBadge
72
- v-if="plugin.hasSuggestions"
73
- color="neutral"
74
- variant="subtle"
75
- label="Suggestions"
76
- size="xs"
77
- />
78
- </div>
79
- </div>
80
- </div>
32
+ <UTabs
33
+ v-model="tab"
34
+ :items="tabItems"
35
+ :ui="{ list: 'w-fit' }"
36
+ />
81
37
 
82
- <div
83
- v-if="pluginList.length === 0"
84
- class="flex flex-col items-center justify-center py-12"
85
- >
86
- <UIcon
87
- name="i-lucide-plug"
88
- class="size-10 text-(--ui-text-dimmed) mb-3"
89
- />
90
- <p class="text-sm text-(--ui-text-muted)">
91
- No plugins installed.
92
- </p>
93
- </div>
38
+ <div v-if="tab === 'browse'" class="min-h-[24rem]">
39
+ <APluginBrowser :server-url="serverUrl" />
40
+ </div>
41
+
42
+ <div v-else-if="tab === 'installed'">
43
+ <APluginsTabInstalled :builtins="builtins" />
44
+ </div>
45
+
46
+ <div v-else-if="tab === 'pending'">
47
+ <APluginsTabPending />
94
48
  </div>
95
49
  </div>
96
50
  </template>
@@ -1,3 +1,16 @@
1
- declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
1
+ export interface BuiltinPluginEntry {
2
+ name: string;
3
+ label?: string;
4
+ version?: string;
5
+ description?: string;
6
+ icon?: string;
7
+ }
8
+ type __VLS_Props = {
9
+ /** Host-registered first-party plugins; toggle state lives in localStorage. */
10
+ builtins?: readonly BuiltinPluginEntry[];
11
+ /** Initial tab. */
12
+ initialTab?: 'browse' | 'installed' | 'pending';
13
+ };
14
+ 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>;
2
15
  declare const _default: typeof __VLS_export;
3
16
  export default _default;
@@ -0,0 +1,7 @@
1
+ export declare function useDeclinedSpacePlugins(): {
2
+ declined: import("@vueuse/shared").RemovableRef<Record<string, string[]>>;
3
+ isDeclined: (spaceId: string, pluginId: string) => boolean;
4
+ decline: (spaceId: string, pluginId: string) => void;
5
+ reset: (spaceId: string, pluginId: string) => void;
6
+ clearSpace: (spaceId: string) => void;
7
+ };
@@ -0,0 +1,24 @@
1
+ import { useLocalStorage } from "@vueuse/core";
2
+ const STORAGE_KEY = "abracadabra_declined_space_plugins";
3
+ export function useDeclinedSpacePlugins() {
4
+ const declined = useLocalStorage(STORAGE_KEY, {});
5
+ function isDeclined(spaceId, pluginId) {
6
+ return (declined.value[spaceId] ?? []).includes(pluginId);
7
+ }
8
+ function decline(spaceId, pluginId) {
9
+ const cur = declined.value[spaceId] ?? [];
10
+ if (cur.includes(pluginId)) return;
11
+ declined.value = { ...declined.value, [spaceId]: [...cur, pluginId] };
12
+ }
13
+ function reset(spaceId, pluginId) {
14
+ const cur = declined.value[spaceId] ?? [];
15
+ if (!cur.includes(pluginId)) return;
16
+ declined.value = { ...declined.value, [spaceId]: cur.filter((id) => id !== pluginId) };
17
+ }
18
+ function clearSpace(spaceId) {
19
+ if (!declined.value[spaceId]) return;
20
+ const { [spaceId]: _, ...rest } = declined.value;
21
+ declined.value = rest;
22
+ }
23
+ return { declined, isDeclined, decline, reset, clearSpace };
24
+ }
@@ -0,0 +1,29 @@
1
+ import type { PluginCapability } from '@abraca/plugin';
2
+ export interface LoadTimePendingEntry {
3
+ /** Installed-entry URL (also the join key against `useInstalledPlugins`). */
4
+ url: string;
5
+ /** Plugin id from the freshly-fetched manifest. */
6
+ id: string;
7
+ /** Version we just observed upstream — DIFFERS from `acknowledgedVersion`. */
8
+ observedVersion: string;
9
+ /** Required capabilities currently declared upstream. */
10
+ observedRequired: PluginCapability[];
11
+ /**
12
+ * Why we queued — `"version-changed"`, `"capabilities-grew"`, or
13
+ * `"never-acknowledged"`. Drives the dialog header copy on review.
14
+ */
15
+ reason: 'version-changed' | 'capabilities-grew' | 'never-acknowledged';
16
+ /** Wall-clock when the loader queued this entry. */
17
+ queuedAt: number;
18
+ }
19
+ /** Loader-side: enqueue or replace an entry. Idempotent on `url`. */
20
+ export declare function _enqueueLoadTimePending(entry: LoadTimePendingEntry): void;
21
+ /** Loader / tab-side: drop an entry once the user re-acks or removes it. */
22
+ export declare function _clearLoadTimePending(url: string): void;
23
+ /** Loader-side: read once at boot to know what was previously queued. */
24
+ export declare function _readLoadTimePending(): LoadTimePendingEntry[];
25
+ export declare function useLoadTimePending(): {
26
+ entries: import("@vueuse/shared").RemovableRef<LoadTimePendingEntry[]>;
27
+ clear: (url: string) => void;
28
+ clearAll: () => void;
29
+ };
@@ -0,0 +1,37 @@
1
+ import { useLocalStorage } from "@vueuse/core";
2
+ const STORAGE_KEY = "abracadabra_load_time_pending";
3
+ function read() {
4
+ try {
5
+ return JSON.parse(localStorage.getItem(STORAGE_KEY) ?? "[]");
6
+ } catch {
7
+ return [];
8
+ }
9
+ }
10
+ function write(list) {
11
+ try {
12
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(list));
13
+ } catch {
14
+ }
15
+ }
16
+ export function _enqueueLoadTimePending(entry) {
17
+ const list = read().filter((e) => e.url !== entry.url);
18
+ list.push(entry);
19
+ write(list);
20
+ }
21
+ export function _clearLoadTimePending(url) {
22
+ const list = read().filter((e) => e.url !== url);
23
+ write(list);
24
+ }
25
+ export function _readLoadTimePending() {
26
+ return read();
27
+ }
28
+ export function useLoadTimePending() {
29
+ const entries = useLocalStorage(STORAGE_KEY, []);
30
+ function clear(url) {
31
+ entries.value = entries.value.filter((e) => e.url !== url);
32
+ }
33
+ function clearAll() {
34
+ entries.value = [];
35
+ }
36
+ return { entries, clear, clearAll };
37
+ }
@@ -1,4 +1,4 @@
1
- import type { PluginManifest, PluginCapability } from '@abraca/plugin';
1
+ import type { PluginManifest, PluginCapability, ExternalPluginEntry } from '@abraca/plugin';
2
2
  export interface CatalogPlugin {
3
3
  id: string;
4
4
  name: string | null;
@@ -138,6 +138,10 @@ export declare function usePluginCatalog(): {
138
138
  required: PluginCapability[];
139
139
  optional: PluginCapability[];
140
140
  };
141
+ capabilitiesGrew: (prevAcked: readonly PluginCapability[] | undefined, currentRequired: readonly PluginCapability[]) => PluginCapability[];
142
+ needsTrustReprompt: (entry: Pick<ExternalPluginEntry, "trustAcknowledgedAt" | "acknowledgedVersion" | "acknowledgedCapabilities">, manifest: Pick<PluginManifest, "version" | "capabilities">) => boolean;
143
+ resolveExternalManifest: (bundleUrl: string) => Promise<PluginManifest | null>;
144
+ synthesizeUnknownManifest: (idOrUrl: string, version?: string) => PluginManifest;
141
145
  /** Current server policy. `null` until `loadPolicy()` resolves. */
142
146
  policy: import("vue").Ref<{
143
147
  registry_url: string;
@@ -163,6 +163,36 @@ export function usePluginCatalog() {
163
163
  optional: [...m.capabilities.optional ?? []]
164
164
  };
165
165
  }
166
+ function capabilitiesGrew(prevAcked, currentRequired) {
167
+ const acked = new Set(prevAcked ?? []);
168
+ return currentRequired.filter((c) => !acked.has(c));
169
+ }
170
+ async function resolveExternalManifest(bundleUrl) {
171
+ try {
172
+ const manifestUrl = bundleUrl.replace(/\/[^/]+$/, (m2) => `${m2.replace(/\.[^.]+$/, "")}.manifest.json`);
173
+ const m = await $fetch(manifestUrl);
174
+ if (m && typeof m === "object" && typeof m.id === "string") return m;
175
+ return null;
176
+ } catch {
177
+ return null;
178
+ }
179
+ }
180
+ function synthesizeUnknownManifest(idOrUrl, version = "0.0.0") {
181
+ const m = {
182
+ manifestVersion: 1,
183
+ id: idOrUrl,
184
+ version,
185
+ capabilities: { required: [], optional: [] },
186
+ contributes: {}
187
+ };
188
+ return m;
189
+ }
190
+ function needsTrustReprompt(entry, manifest) {
191
+ if (!entry.trustAcknowledgedAt) return true;
192
+ if (entry.acknowledgedVersion !== manifest.version) return true;
193
+ const required = manifest.capabilities.required ?? [];
194
+ return capabilitiesGrew(entry.acknowledgedCapabilities, required).length > 0;
195
+ }
166
196
  async function loadPolicy(serverUrl) {
167
197
  const url = `${serverUrl.replace(/\/$/, "")}/plugins/policy`;
168
198
  const fetched = await $fetch(url);
@@ -200,6 +230,10 @@ export function usePluginCatalog() {
200
230
  installFromUrl,
201
231
  isInstalled,
202
232
  capabilitiesFor,
233
+ capabilitiesGrew,
234
+ needsTrustReprompt,
235
+ resolveExternalManifest,
236
+ synthesizeUnknownManifest,
203
237
  /** Current server policy. `null` until `loadPolicy()` resolves. */
204
238
  policy,
205
239
  loadPolicy,
@@ -1,3 +1,4 @@
1
+ import type { Ref } from 'vue';
1
2
  /**
2
3
  * Mobile-compatible drag & drop using pointer events.
3
4
  * Works on both touch and mouse devices, replacing HTML5 DragEvent handlers.
@@ -26,11 +27,27 @@ export declare function useTouchDrag(opts: {
26
27
  idAttr?: string;
27
28
  /** Data attribute name for containers (default: 'data-drop-container') */
28
29
  containerAttr?: string;
30
+ /**
31
+ * Navigate by range (prev/next) when the drag ghost dwells near the
32
+ * horizontal edges of `el` — e.g. a calendar dragging an event to the left
33
+ * edge steps to the previous period. Fires on a repeating interval while held
34
+ * at the edge, so a quick pass-through (or a drop on an edge cell) doesn't
35
+ * trigger it; resets the moment the pointer leaves the edge zone.
36
+ */
37
+ edgeNav?: {
38
+ el: Ref<HTMLElement | null>;
39
+ onPrev: () => void;
40
+ onNext: () => void;
41
+ /** px from the edge that counts as "at the edge" (default 48) */
42
+ threshold?: number;
43
+ /** ms between repeated nav steps while held at an edge (default 700) */
44
+ intervalMs?: number;
45
+ };
29
46
  }): {
30
- dragId: import("vue").Ref<string | null, string | null>;
31
- dragOverId: import("vue").Ref<string | null, string | null>;
32
- dragOverContainer: import("vue").Ref<string | null, string | null>;
33
- isDragging: import("vue").Ref<boolean, boolean>;
47
+ dragId: Ref<string | null, string | null>;
48
+ dragOverId: Ref<string | null, string | null>;
49
+ dragOverContainer: Ref<string | null, string | null>;
50
+ isDragging: Ref<boolean, boolean>;
34
51
  handlePointerDown: (e: PointerEvent, id: string) => void;
35
52
  cancel: () => void;
36
53
  };
@@ -7,6 +7,33 @@ export function useTouchDrag(opts) {
7
7
  const dragOverId = ref(null);
8
8
  const dragOverContainer = ref(null);
9
9
  const isDragging = ref(false);
10
+ let edgeNavTimer = null;
11
+ let edgeNavDir = null;
12
+ function clearEdgeNav() {
13
+ if (edgeNavTimer) {
14
+ clearInterval(edgeNavTimer);
15
+ edgeNavTimer = null;
16
+ }
17
+ edgeNavDir = null;
18
+ }
19
+ function handleEdgeNav(clientX) {
20
+ const cfg = opts.edgeNav;
21
+ const el = cfg?.el.value;
22
+ if (!cfg || !el) return;
23
+ const rect = el.getBoundingClientRect();
24
+ const threshold = cfg.threshold ?? 48;
25
+ let dir = null;
26
+ if (clientX <= rect.left + threshold) dir = "prev";
27
+ else if (clientX >= rect.right - threshold) dir = "next";
28
+ if (dir === edgeNavDir) return;
29
+ clearEdgeNav();
30
+ if (!dir) return;
31
+ edgeNavDir = dir;
32
+ edgeNavTimer = setInterval(() => {
33
+ if (edgeNavDir === "prev") cfg.onPrev();
34
+ else if (edgeNavDir === "next") cfg.onNext();
35
+ }, cfg.intervalMs ?? 700);
36
+ }
10
37
  let startX = 0;
11
38
  let startY = 0;
12
39
  let delayTimer = null;
@@ -131,6 +158,7 @@ export function useTouchDrag(opts) {
131
158
  opts.onMoveOverContainer(dragId.value, container, e);
132
159
  }
133
160
  autoScroll(e.clientY);
161
+ handleEdgeNav(e.clientX);
134
162
  }
135
163
  function autoScroll(clientY) {
136
164
  const threshold = 60;
@@ -143,6 +171,7 @@ export function useTouchDrag(opts) {
143
171
  }
144
172
  function onDocPointerUp() {
145
173
  cancelPending();
174
+ clearEdgeNav();
146
175
  document.removeEventListener("pointermove", onDocPointerMove);
147
176
  document.removeEventListener("pointerup", onDocPointerUp);
148
177
  document.removeEventListener("pointercancel", onDocPointerUp);
@@ -175,6 +204,7 @@ export function useTouchDrag(opts) {
175
204
  dragOverContainer.value = null;
176
205
  isDragging.value = false;
177
206
  pendingId = null;
207
+ clearEdgeNav();
178
208
  if (delayTimer) {
179
209
  clearTimeout(delayTimer);
180
210
  delayTimer = null;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * `useUploadedPluginStore()` — IndexedDB-backed storage for plugin bundles
3
+ * the user uploaded directly (Upload button on the Installed tab). Keys
4
+ * are the SHA-256 of the bundle bytes; values are the bytes + MIME.
5
+ *
6
+ * The plugin loader (`plugin-abracadabra.client.ts`) recognises URLs of
7
+ * the form `idb-plugin:<sha256>` and resolves them by reading bytes from
8
+ * here, wrapping in a fresh `Blob`, and creating a `blob:` URL for
9
+ * `import()`. Blob URLs are session-scoped, so the bytes have to live in
10
+ * IDB to survive reload.
11
+ *
12
+ * Internal helpers (`_*`) are used by the loader. Public API is the
13
+ * composable for components.
14
+ */
15
+ interface StoredBundle {
16
+ sha256: string;
17
+ bytes: Uint8Array;
18
+ mime: string;
19
+ uploadedAt: number;
20
+ }
21
+ /** Store an uploaded bundle by its SHA-256 hex. Idempotent. */
22
+ export declare function _putUploadedPluginBlob(sha256: string, bytes: Uint8Array, mime?: string): Promise<void>;
23
+ /** Read an uploaded bundle. Returns `null` when the key is unknown. */
24
+ export declare function _getUploadedPluginBlob(sha256: string): Promise<StoredBundle | null>;
25
+ /** Delete an uploaded bundle. Used by `uninstall()` for upload-origin entries. */
26
+ export declare function _deleteUploadedPluginBlob(sha256: string): Promise<void>;
27
+ /**
28
+ * Resolve an `idb-plugin:<sha256>` URL to a session-scoped `blob:` URL
29
+ * suitable for dynamic `import()`. Returns `null` when the sha256 isn't
30
+ * found — the loader treats that as a missing-bundle error.
31
+ */
32
+ export declare function _resolveIdbPluginUrl(idbUrl: string): Promise<string | null>;
33
+ /**
34
+ * Composable surface — read-only for components. Components write via
35
+ * `<APluginsTabInstalled>`'s upload handler which calls
36
+ * `_putUploadedPluginBlob` directly.
37
+ */
38
+ export declare function useUploadedPluginStore(): {
39
+ get: typeof _getUploadedPluginBlob;
40
+ delete: typeof _deleteUploadedPluginBlob;
41
+ resolveUrl: typeof _resolveIdbPluginUrl;
42
+ };
43
+ export {};
@@ -0,0 +1,66 @@
1
+ const DB_NAME = "abracadabra_uploaded_plugins";
2
+ const STORE_NAME = "bundles";
3
+ const DB_VERSION = 1;
4
+ let _dbPromise = null;
5
+ function getDb() {
6
+ if (_dbPromise) return _dbPromise;
7
+ _dbPromise = new Promise((resolve, reject) => {
8
+ const req = indexedDB.open(DB_NAME, DB_VERSION);
9
+ req.onupgradeneeded = () => {
10
+ const db = req.result;
11
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
12
+ db.createObjectStore(STORE_NAME, { keyPath: "sha256" });
13
+ }
14
+ };
15
+ req.onsuccess = () => resolve(req.result);
16
+ req.onerror = () => reject(req.error);
17
+ });
18
+ return _dbPromise;
19
+ }
20
+ export async function _putUploadedPluginBlob(sha256, bytes, mime = "text/javascript") {
21
+ const db = await getDb();
22
+ await new Promise((resolve, reject) => {
23
+ const tx = db.transaction(STORE_NAME, "readwrite");
24
+ const store = tx.objectStore(STORE_NAME);
25
+ const record = { sha256, bytes, mime, uploadedAt: Date.now() };
26
+ const req = store.put(record);
27
+ req.onsuccess = () => resolve();
28
+ req.onerror = () => reject(req.error);
29
+ });
30
+ }
31
+ export async function _getUploadedPluginBlob(sha256) {
32
+ const db = await getDb();
33
+ return new Promise((resolve, reject) => {
34
+ const tx = db.transaction(STORE_NAME, "readonly");
35
+ const store = tx.objectStore(STORE_NAME);
36
+ const req = store.get(sha256);
37
+ req.onsuccess = () => resolve(req.result ?? null);
38
+ req.onerror = () => reject(req.error);
39
+ });
40
+ }
41
+ export async function _deleteUploadedPluginBlob(sha256) {
42
+ const db = await getDb();
43
+ await new Promise((resolve, reject) => {
44
+ const tx = db.transaction(STORE_NAME, "readwrite");
45
+ const store = tx.objectStore(STORE_NAME);
46
+ const req = store.delete(sha256);
47
+ req.onsuccess = () => resolve();
48
+ req.onerror = () => reject(req.error);
49
+ });
50
+ }
51
+ export async function _resolveIdbPluginUrl(idbUrl) {
52
+ const prefix = "idb-plugin:";
53
+ if (!idbUrl.startsWith(prefix)) return null;
54
+ const sha = idbUrl.slice(prefix.length);
55
+ const record = await _getUploadedPluginBlob(sha);
56
+ if (!record) return null;
57
+ const blob = new Blob([new Uint8Array(record.bytes)], { type: record.mime });
58
+ return URL.createObjectURL(blob);
59
+ }
60
+ export function useUploadedPluginStore() {
61
+ return {
62
+ get: _getUploadedPluginBlob,
63
+ delete: _deleteUploadedPluginBlob,
64
+ resolveUrl: _resolveIdbPluginUrl
65
+ };
66
+ }
@@ -4,6 +4,7 @@ import { NodeViewWrapper } from "@tiptap/vue-3";
4
4
  import AColorPalettePopover from "../../components/editor/AColorPalettePopover.vue";
5
5
  import AIconPickerPopover from "../../components/editor/AIconPickerPopover.vue";
6
6
  import ALocationPickerPopover from "../../components/editor/ALocationPickerPopover.vue";
7
+ import AMetaNumberStepper from "../../components/editor/AMetaNumberStepper.vue";
7
8
  import { useAbraLocale } from "../../composables/useAbraLocale";
8
9
  import {
9
10
  DateFormatter,
@@ -62,6 +63,8 @@ const sliderMin = computed(() => props.node.attrs.sliderMin ?? 0);
62
63
  const sliderMax = computed(() => props.node.attrs.sliderMax ?? 100);
63
64
  const sliderStep = computed(() => props.node.attrs.sliderStep ?? 1);
64
65
  const unit = computed(() => props.node.attrs.unit ?? "");
66
+ const numMin = computed(() => props.node.attrs.sliderMin);
67
+ const numMax = computed(() => props.node.attrs.sliderMax);
65
68
  function getStr(key) {
66
69
  return storage()?.pageMeta?.[key] ?? "";
67
70
  }
@@ -239,16 +242,6 @@ function onRatingWheel(e) {
239
242
  const next = Math.max(0, Math.min(max, current + delta));
240
243
  if (next !== current) patch({ [metaKey.value]: next });
241
244
  }
242
- function onNumberWheel(e) {
243
- const delta = -Math.sign(e.deltaY);
244
- if (!delta) return;
245
- const step = (sliderStep.value || 1) * (e.shiftKey ? 10 : 1);
246
- const current = getNum(metaKey.value);
247
- const min = sliderMin.value;
248
- const max = sliderMax.value;
249
- const next = Math.max(min, Math.min(max, current + delta * step));
250
- if (next !== current) patch({ [metaKey.value]: next });
251
- }
252
245
  function onSliderWheel(e) {
253
246
  const delta = -Math.sign(e.deltaY);
254
247
  if (!delta) return;
@@ -784,13 +777,20 @@ function removeOption(opt) {
784
777
 
785
778
  <!-- ── number ─────────────────────────────────────────────────────────── -->
786
779
  <template v-else-if="fieldType === 'number'">
787
- <div
788
- class="flex items-center gap-1.5 h-7 px-2.5 border border-(--ui-border) rounded-md text-sm"
789
- @mousedown.stop
790
- @touchstart.stop
791
- @wheel.prevent="onNumberWheel"
780
+ <AMetaNumberStepper
781
+ class="min-w-40"
782
+ :model-value="getOptNum(metaKey)"
783
+ :min="numMin"
784
+ :max="numMax"
785
+ :step="sliderStep"
786
+ :unit="unit"
787
+ :disabled="!isEditable"
788
+ @update:model-value="onNumber"
792
789
  >
793
- <template v-if="fieldLabel">
790
+ <template
791
+ v-if="fieldLabel"
792
+ #label
793
+ >
794
794
  <input
795
795
  v-if="editingLabel"
796
796
  ref="labelInputEl"
@@ -807,18 +807,7 @@ function removeOption(opt) {
807
807
  @click.stop="startEditLabel"
808
808
  >{{ fieldLabel }}</span>
809
809
  </template>
810
- <UInputNumber
811
- size="xs"
812
- :model-value="getOptNum(metaKey)"
813
- :step="sliderStep"
814
- class="max-w-32"
815
- @update:model-value="onNumber"
816
- />
817
- <span
818
- v-if="unit"
819
- class="text-(--ui-text-muted) shrink-0 text-xs"
820
- >{{ unit }}</span>
821
- </div>
810
+ </AMetaNumberStepper>
822
811
  </template>
823
812
 
824
813
  <!-- ── toggle ─────────────────────────────────────────────────────────── -->