@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.
- package/dist/module.json +1 -1
- package/dist/runtime/assets/editor.css +3 -1
- package/dist/runtime/components/ACodeEditor.vue +16 -2
- package/dist/runtime/components/ANodePanel.vue +7 -5
- package/dist/runtime/components/aware/ASlider.d.vue.ts +1 -1
- package/dist/runtime/components/aware/ASlider.vue.d.ts +1 -1
- package/dist/runtime/components/docs/ADocsSearchButton.d.vue.ts +1 -1
- package/dist/runtime/components/docs/ADocsSearchButton.vue.d.ts +1 -1
- package/dist/runtime/components/editor/AColorPalettePopover.vue +97 -5
- package/dist/runtime/components/editor/AIconPickerPopover.vue +81 -3
- package/dist/runtime/components/editor/AMetaNumberStepper.d.vue.ts +40 -0
- package/dist/runtime/components/editor/AMetaNumberStepper.vue +214 -0
- package/dist/runtime/components/editor/AMetaNumberStepper.vue.d.ts +40 -0
- package/dist/runtime/components/registry/APluginBrowser.vue +18 -2
- package/dist/runtime/components/renderers/ACalendarRenderer.vue +7 -1
- package/dist/runtime/components/renderers/calendar/ACalendarToolbar.d.vue.ts +2 -2
- package/dist/runtime/components/renderers/calendar/ACalendarToolbar.vue.d.ts +2 -2
- package/dist/runtime/components/renderers/media/MediaTransportBar.d.vue.ts +2 -2
- package/dist/runtime/components/renderers/media/MediaTransportBar.vue.d.ts +2 -2
- package/dist/runtime/components/settings/APluginInstallDialog.d.vue.ts +39 -0
- package/dist/runtime/components/settings/APluginInstallDialog.vue +254 -0
- package/dist/runtime/components/settings/APluginInstallDialog.vue.d.ts +39 -0
- package/dist/runtime/components/settings/APluginsTabInstalled.d.vue.ts +7 -0
- package/dist/runtime/components/settings/APluginsTabInstalled.vue +413 -0
- package/dist/runtime/components/settings/APluginsTabInstalled.vue.d.ts +7 -0
- package/dist/runtime/components/settings/APluginsTabPending.d.vue.ts +24 -0
- package/dist/runtime/components/settings/APluginsTabPending.vue +248 -0
- package/dist/runtime/components/settings/APluginsTabPending.vue.d.ts +24 -0
- package/dist/runtime/components/settings/ASettingsPluginsPanel.d.vue.ts +14 -1
- package/dist/runtime/components/settings/ASettingsPluginsPanel.vue +34 -80
- package/dist/runtime/components/settings/ASettingsPluginsPanel.vue.d.ts +14 -1
- package/dist/runtime/composables/useDeclinedSpacePlugins.d.ts +7 -0
- package/dist/runtime/composables/useDeclinedSpacePlugins.js +24 -0
- package/dist/runtime/composables/useLoadTimePending.d.ts +29 -0
- package/dist/runtime/composables/useLoadTimePending.js +37 -0
- package/dist/runtime/composables/usePluginCatalog.d.ts +5 -1
- package/dist/runtime/composables/usePluginCatalog.js +34 -0
- package/dist/runtime/composables/useTouchDrag.d.ts +21 -4
- package/dist/runtime/composables/useTouchDrag.js +30 -0
- package/dist/runtime/composables/useUploadedPluginStore.d.ts +43 -0
- package/dist/runtime/composables/useUploadedPluginStore.js +66 -0
- package/dist/runtime/extensions/views/MetaFieldView.vue +17 -28
- package/dist/runtime/plugin-abracadabra.client.js +48 -1
- 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
|
-
|
|
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 {
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
<
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
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:
|
|
31
|
-
dragOverId:
|
|
32
|
-
dragOverContainer:
|
|
33
|
-
isDragging:
|
|
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
|
-
<
|
|
788
|
-
class="
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
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
|
|
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
|
-
|
|
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 ─────────────────────────────────────────────────────────── -->
|