@dxos/plugin-file 0.10.0 → 0.11.1
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/PLUGIN.mdl +86 -77
- package/dist/lib/FilePlugin.mjs +37 -0
- package/dist/lib/FilePlugin.mjs.map +1 -0
- package/dist/lib/FilePlugin.node.mjs +12 -0
- package/dist/lib/FilePlugin.node.mjs.map +1 -0
- package/dist/lib/capabilities/node.mjs +8 -0
- package/dist/lib/capabilities/node.mjs.map +1 -0
- package/dist/lib/capabilities.mjs +15 -0
- package/dist/lib/capabilities.mjs.map +1 -0
- package/dist/lib/chunk-FileArticle.mjs +62 -0
- package/dist/lib/chunk-FileArticle.mjs.map +1 -0
- package/dist/lib/chunk-create-object.mjs +28 -0
- package/dist/lib/chunk-create-object.mjs.map +1 -0
- package/dist/lib/chunk-create.mjs +78 -0
- package/dist/lib/chunk-create.mjs.map +1 -0
- package/dist/lib/chunk-edge-backend.mjs +18 -0
- package/dist/lib/chunk-edge-backend.mjs.map +1 -0
- package/dist/lib/chunk-file-uploader.mjs +37 -0
- package/dist/lib/chunk-file-uploader.mjs.map +1 -0
- package/dist/lib/chunk-inline-backend.mjs +19 -0
- package/dist/lib/chunk-inline-backend.mjs.map +1 -0
- package/dist/lib/chunk-markdown-extension.mjs +182 -0
- package/dist/lib/chunk-markdown-extension.mjs.map +1 -0
- package/dist/lib/chunk-meta.mjs +44 -0
- package/dist/lib/chunk-meta.mjs.map +1 -0
- package/dist/lib/chunk-operation-handler.mjs +11 -0
- package/dist/lib/chunk-operation-handler.mjs.map +1 -0
- package/dist/lib/chunk-react-surface.mjs +55 -0
- package/dist/lib/chunk-react-surface.mjs.map +1 -0
- package/dist/lib/chunk-read.mjs +32 -0
- package/dist/lib/chunk-read.mjs.map +1 -0
- package/dist/lib/chunk-settings.mjs +23 -0
- package/dist/lib/chunk-settings.mjs.map +1 -0
- package/dist/lib/chunk-skill-definition.mjs +10 -0
- package/dist/lib/chunk-skill-definition.mjs.map +1 -0
- package/dist/lib/chunk-skills.mjs +28 -0
- package/dist/lib/chunk-skills.mjs.map +1 -0
- package/dist/lib/chunk-types.mjs +98 -0
- package/dist/lib/chunk-types.mjs.map +1 -0
- package/dist/lib/components.mjs +57 -0
- package/dist/lib/components.mjs.map +1 -0
- package/dist/lib/containers.mjs +56 -0
- package/dist/lib/containers.mjs.map +1 -0
- package/dist/lib/index.mjs +4 -0
- package/dist/lib/meta.mjs +2 -0
- package/dist/lib/operations.mjs +7 -0
- package/dist/lib/operations.mjs.map +1 -0
- package/dist/lib/plugin.mjs +9 -0
- package/dist/lib/plugin.mjs.map +1 -0
- package/dist/lib/skills.mjs +2 -0
- package/dist/lib/translations.mjs +30 -0
- package/dist/lib/translations.mjs.map +1 -0
- package/dist/lib/types.mjs +2 -0
- package/dist/types/src/FilePlugin.d.ts.map +1 -1
- package/dist/types/src/FilePlugin.node.d.ts.map +1 -1
- package/dist/types/src/capabilities/FileUploadField.d.ts +12 -0
- package/dist/types/src/capabilities/FileUploadField.d.ts.map +1 -0
- package/dist/types/src/capabilities/create-object.d.ts +1 -1
- package/dist/types/src/capabilities/edge-backend.d.ts +5 -0
- package/dist/types/src/capabilities/edge-backend.d.ts.map +1 -0
- package/dist/types/src/capabilities/file-uploader.d.ts +1 -1
- package/dist/types/src/capabilities/file-uploader.d.ts.map +1 -1
- package/dist/types/src/capabilities/index.d.ts +1 -0
- package/dist/types/src/capabilities/index.d.ts.map +1 -1
- package/dist/types/src/capabilities/inline-backend.d.ts +2 -2
- package/dist/types/src/capabilities/inline-backend.d.ts.map +1 -1
- package/dist/types/src/capabilities/markdown-extension.d.ts +1 -1
- package/dist/types/src/capabilities/node.d.ts +5 -0
- package/dist/types/src/capabilities/node.d.ts.map +1 -0
- package/dist/types/src/capabilities/operation-handler.d.ts +1 -1
- package/dist/types/src/capabilities/react-surface.d.ts +1 -1
- package/dist/types/src/capabilities/react-surface.d.ts.map +1 -1
- package/dist/types/src/capabilities/settings.d.ts +1 -1
- package/dist/types/src/components/FileInput/FileInput.d.ts.map +1 -1
- package/dist/types/src/components/FilePreview/FilePreview.d.ts +1 -1
- package/dist/types/src/components/FilePreview/FilePreview.d.ts.map +1 -1
- package/dist/types/src/containers/FileArticle/FileArticle.d.ts +4 -1
- package/dist/types/src/containers/FileArticle/FileArticle.d.ts.map +1 -1
- package/dist/types/src/containers/FileSettings/FileSettings.d.ts +5 -3
- package/dist/types/src/containers/FileSettings/FileSettings.d.ts.map +1 -1
- package/dist/types/src/extensions/image.d.ts +0 -2
- package/dist/types/src/extensions/image.d.ts.map +1 -1
- package/dist/types/src/operations/create.d.ts +14 -5
- package/dist/types/src/operations/create.d.ts.map +1 -1
- package/dist/types/src/operations/read.d.ts.map +1 -1
- package/dist/types/src/types/FileCapabilities.d.ts +10 -28
- package/dist/types/src/types/FileCapabilities.d.ts.map +1 -1
- package/dist/types/src/types/Settings.d.ts +1 -1
- package/dist/types/src/types/Settings.d.ts.map +1 -1
- package/dist/types/src/types/limits.d.ts +0 -1
- package/dist/types/src/types/limits.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/dx.config.ts +10 -6
- package/package.json +45 -41
- package/src/FilePlugin.node.ts +2 -1
- package/src/FilePlugin.tsx +8 -1
- package/src/capabilities/FileUploadField.tsx +37 -0
- package/src/capabilities/edge-backend.ts +27 -0
- package/src/capabilities/file-uploader.ts +6 -4
- package/src/capabilities/index.ts +1 -0
- package/src/capabilities/inline-backend.ts +4 -21
- package/src/capabilities/markdown-extension.ts +3 -8
- package/src/capabilities/node.ts +16 -0
- package/src/capabilities/react-surface.ts +46 -0
- package/src/components/FileInput/FileInput.tsx +3 -2
- package/src/containers/FileArticle/FileArticle.tsx +49 -42
- package/src/containers/FileSettings/FileSettings.tsx +19 -14
- package/src/extensions/image.tsx +49 -28
- package/src/operations/create.test.ts +122 -32
- package/src/operations/create.ts +44 -24
- package/src/operations/read.ts +24 -6
- package/src/types/FileCapabilities.ts +10 -31
- package/src/types/Settings.ts +1 -1
- package/src/types/limits.ts +0 -2
- package/dist/lib/neutral/FileArticle-YU6NN3KN.mjs +0 -80
- package/dist/lib/neutral/FileArticle-YU6NN3KN.mjs.map +0 -7
- package/dist/lib/neutral/FilePlugin.mjs +0 -58
- package/dist/lib/neutral/FilePlugin.mjs.map +0 -7
- package/dist/lib/neutral/FilePlugin.node.mjs +0 -23
- package/dist/lib/neutral/FilePlugin.node.mjs.map +0 -7
- package/dist/lib/neutral/capabilities/index.mjs +0 -23
- package/dist/lib/neutral/capabilities/index.mjs.map +0 -7
- package/dist/lib/neutral/chunk-DKGTSUUH.mjs +0 -8
- package/dist/lib/neutral/chunk-DKGTSUUH.mjs.map +0 -7
- package/dist/lib/neutral/chunk-GMYSEHV4.mjs +0 -40
- package/dist/lib/neutral/chunk-GMYSEHV4.mjs.map +0 -7
- package/dist/lib/neutral/chunk-J5LGTIGS.mjs +0 -10
- package/dist/lib/neutral/chunk-J5LGTIGS.mjs.map +0 -7
- package/dist/lib/neutral/chunk-LBZ4MCLI.mjs +0 -45
- package/dist/lib/neutral/chunk-LBZ4MCLI.mjs.map +0 -7
- package/dist/lib/neutral/chunk-YODPX62X.mjs +0 -136
- package/dist/lib/neutral/chunk-YODPX62X.mjs.map +0 -7
- package/dist/lib/neutral/chunk-ZUXUTRAJ.mjs +0 -80
- package/dist/lib/neutral/chunk-ZUXUTRAJ.mjs.map +0 -7
- package/dist/lib/neutral/components/index.mjs +0 -74
- package/dist/lib/neutral/components/index.mjs.map +0 -7
- package/dist/lib/neutral/containers/index.mjs +0 -59
- package/dist/lib/neutral/containers/index.mjs.map +0 -7
- package/dist/lib/neutral/create-IUUG7RI6.mjs +0 -17
- package/dist/lib/neutral/create-IUUG7RI6.mjs.map +0 -7
- package/dist/lib/neutral/create-object-V667G4ZL.mjs +0 -30
- package/dist/lib/neutral/create-object-V667G4ZL.mjs.map +0 -7
- package/dist/lib/neutral/file-uploader-BCJH6TUL.mjs +0 -41
- package/dist/lib/neutral/file-uploader-BCJH6TUL.mjs.map +0 -7
- package/dist/lib/neutral/index.mjs +0 -32
- package/dist/lib/neutral/index.mjs.map +0 -7
- package/dist/lib/neutral/inline-backend-FGGGDYRB.mjs +0 -37
- package/dist/lib/neutral/inline-backend-FGGGDYRB.mjs.map +0 -7
- package/dist/lib/neutral/markdown-extension-RA5YNMTW.mjs +0 -230
- package/dist/lib/neutral/markdown-extension-RA5YNMTW.mjs.map +0 -7
- package/dist/lib/neutral/meta.json +0 -1
- package/dist/lib/neutral/meta.mjs +0 -8
- package/dist/lib/neutral/meta.mjs.map +0 -7
- package/dist/lib/neutral/operation-handler-CPDANWGH.mjs +0 -13
- package/dist/lib/neutral/operation-handler-CPDANWGH.mjs.map +0 -7
- package/dist/lib/neutral/operations/index.mjs +0 -8
- package/dist/lib/neutral/operations/index.mjs.map +0 -7
- package/dist/lib/neutral/plugin.mjs +0 -16
- package/dist/lib/neutral/plugin.mjs.map +0 -7
- package/dist/lib/neutral/react-surface-5BXKSHD2.mjs +0 -60
- package/dist/lib/neutral/react-surface-5BXKSHD2.mjs.map +0 -7
- package/dist/lib/neutral/read-HG6CSWKR.mjs +0 -28
- package/dist/lib/neutral/read-HG6CSWKR.mjs.map +0 -7
- package/dist/lib/neutral/settings-RR3RESL2.mjs +0 -28
- package/dist/lib/neutral/settings-RR3RESL2.mjs.map +0 -7
- package/dist/lib/neutral/skill-definition-YHBGWBUS.mjs +0 -15
- package/dist/lib/neutral/skill-definition-YHBGWBUS.mjs.map +0 -7
- package/dist/lib/neutral/skills/index.mjs +0 -11
- package/dist/lib/neutral/skills/index.mjs.map +0 -7
- package/dist/lib/neutral/translations.mjs +0 -36
- package/dist/lib/neutral/translations.mjs.map +0 -7
- package/dist/lib/neutral/types/index.mjs +0 -22
- package/dist/lib/neutral/types/index.mjs.map +0 -7
- package/src/capabilities/react-surface.tsx +0 -61
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { c as SettingsAtom, d as isAcceptedMimeType, o as Backend, r as Create, t as File } from "./chunk-types.mjs";
|
|
2
|
+
import { Operation } from "@dxos/compute";
|
|
3
|
+
import { Capabilities, Capability } from "@dxos/app-framework";
|
|
4
|
+
import { Blob, Database } from "@dxos/echo";
|
|
5
|
+
import * as Effect from "effect/Effect";
|
|
6
|
+
//#region src/operations/create.ts
|
|
7
|
+
var UnsupportedFileTypeError = class extends Error {
|
|
8
|
+
type;
|
|
9
|
+
constructor(type) {
|
|
10
|
+
super(`Unsupported file type: ${type}`);
|
|
11
|
+
this.type = type;
|
|
12
|
+
this.name = "UnsupportedFileTypeError";
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
var FileTooLargeError = class extends Error {
|
|
16
|
+
size;
|
|
17
|
+
limit;
|
|
18
|
+
constructor(size, limit = Blob.MAX_INLINE_SIZE) {
|
|
19
|
+
super(`File is too large: ${size} bytes (limit: ${limit} bytes)`);
|
|
20
|
+
this.size = size;
|
|
21
|
+
this.limit = limit;
|
|
22
|
+
this.name = "FileTooLargeError";
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
var NoBackendError = class extends Error {
|
|
26
|
+
constructor() {
|
|
27
|
+
super("No file storage backend is registered.");
|
|
28
|
+
this.name = "NoBackendError";
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
var FileReadError = class extends Error {
|
|
32
|
+
constructor(cause) {
|
|
33
|
+
super("Failed to read file contents.");
|
|
34
|
+
this.name = "FileReadError";
|
|
35
|
+
this.cause = cause;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Resolves the storage name to force for an upload:
|
|
40
|
+
* - the storage name the user explicitly configured in Settings.backend, if it matches a
|
|
41
|
+
* registered {@link FileCapabilities.Backend} descriptor
|
|
42
|
+
* - otherwise `undefined`, leaving `File.fromBytes`'s `storage` option unset so it falls through
|
|
43
|
+
* to the Blob registry's own configured default (edge when configured, inline otherwise)
|
|
44
|
+
* instead of hardcoding `inline`
|
|
45
|
+
*
|
|
46
|
+
* Still requires at least one descriptor to be registered, as a sanity check that the plugin's
|
|
47
|
+
* settings UI has something to show.
|
|
48
|
+
*/
|
|
49
|
+
var resolveActiveStorage = Effect.gen(function* () {
|
|
50
|
+
const backends = yield* Capability.getAll(Backend);
|
|
51
|
+
if (backends.length === 0) return yield* Effect.fail(new NoBackendError());
|
|
52
|
+
const settingsAtomOpt = yield* Capability.get(SettingsAtom).pipe(Effect.option);
|
|
53
|
+
const registryOpt = yield* Capability.get(Capabilities.AtomRegistry).pipe(Effect.option);
|
|
54
|
+
if (settingsAtomOpt._tag === "Some" && registryOpt._tag === "Some") {
|
|
55
|
+
const settings = registryOpt.value.get(settingsAtomOpt.value);
|
|
56
|
+
if (settings.backend) {
|
|
57
|
+
const match = backends.find((b) => b.storage === settings.backend);
|
|
58
|
+
if (match) return match.storage;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
var handler = Create.pipe(Operation.withHandler(Effect.fnUntraced(function* ({ file, db }) {
|
|
63
|
+
if (!isAcceptedMimeType(file.type)) return yield* Effect.fail(new UnsupportedFileTypeError(file.type));
|
|
64
|
+
const storage = yield* resolveActiveStorage;
|
|
65
|
+
const bytes = new Uint8Array(yield* Effect.tryPromise({
|
|
66
|
+
try: () => file.arrayBuffer(),
|
|
67
|
+
catch: (error) => new FileReadError(error)
|
|
68
|
+
}));
|
|
69
|
+
return { object: yield* File.fromBytes(bytes, {
|
|
70
|
+
name: file.name,
|
|
71
|
+
type: file.type,
|
|
72
|
+
storage
|
|
73
|
+
}).pipe(Effect.provide(Database.layer(db)), Effect.catchTag("BlobTooLargeError", () => Effect.fail(new FileTooLargeError(bytes.byteLength)))) };
|
|
74
|
+
})));
|
|
75
|
+
//#endregion
|
|
76
|
+
export { FileReadError, FileTooLargeError, NoBackendError, UnsupportedFileTypeError, handler as default, resolveActiveStorage };
|
|
77
|
+
|
|
78
|
+
//# sourceMappingURL=chunk-create.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chunk-create.mjs","names":[],"sources":["../../src/operations/create.ts"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\nimport * as Effect from 'effect/Effect';\n\nimport { Capabilities, Capability } from '@dxos/app-framework';\nimport { Operation } from '@dxos/compute';\nimport { Blob, Database } from '@dxos/echo';\n\nimport { File, FileCapabilities, FileOperation, Settings, isAcceptedMimeType } from '../types';\n\nexport class UnsupportedFileTypeError extends Error {\n constructor(public readonly type: string) {\n super(`Unsupported file type: ${type}`);\n this.name = 'UnsupportedFileTypeError';\n }\n}\n\nexport class FileTooLargeError extends Error {\n constructor(\n public readonly size: number,\n public readonly limit: number = Blob.MAX_INLINE_SIZE,\n ) {\n super(`File is too large: ${size} bytes (limit: ${limit} bytes)`);\n this.name = 'FileTooLargeError';\n }\n}\n\nexport class NoBackendError extends Error {\n constructor() {\n super('No file storage backend is registered.');\n this.name = 'NoBackendError';\n }\n}\n\nexport class FileReadError extends Error {\n constructor(cause: unknown) {\n super('Failed to read file contents.');\n this.name = 'FileReadError';\n this.cause = cause;\n }\n}\n\n/**\n * Resolves the storage name to force for an upload:\n * - the storage name the user explicitly configured in Settings.backend, if it matches a\n * registered {@link FileCapabilities.Backend} descriptor\n * - otherwise `undefined`, leaving `File.fromBytes`'s `storage` option unset so it falls through\n * to the Blob registry's own configured default (edge when configured, inline otherwise)\n * instead of hardcoding `inline`\n *\n * Still requires at least one descriptor to be registered, as a sanity check that the plugin's\n * settings UI has something to show.\n */\nexport const resolveActiveStorage = Effect.gen(function* () {\n const backends = yield* Capability.getAll(FileCapabilities.Backend);\n if (backends.length === 0) {\n return yield* Effect.fail(new NoBackendError());\n }\n\n const settingsAtomOpt = yield* Capability.get(FileCapabilities.SettingsAtom).pipe(Effect.option);\n const registryOpt = yield* Capability.get(Capabilities.AtomRegistry).pipe(Effect.option);\n\n if (settingsAtomOpt._tag === 'Some' && registryOpt._tag === 'Some') {\n const settings = registryOpt.value.get(settingsAtomOpt.value) as Settings.Settings;\n if (settings.backend) {\n const match = backends.find((b) => b.storage === settings.backend);\n if (match) {\n return match.storage;\n }\n }\n }\n\n return undefined;\n});\n\nconst handler: Operation.WithHandler<typeof FileOperation.Create> = FileOperation.Create.pipe(\n Operation.withHandler(\n Effect.fnUntraced(function* ({ file, db }) {\n // Validate before hitting the backend so the contract is consistent regardless of backend.\n if (!isAcceptedMimeType(file.type)) {\n return yield* Effect.fail(new UnsupportedFileTypeError(file.type));\n }\n const storage = yield* resolveActiveStorage;\n const bytes = new Uint8Array(\n yield* Effect.tryPromise({\n try: () => file.arrayBuffer(),\n catch: (error) => new FileReadError(error),\n }),\n );\n // The size cap only applies to `inline` storage — `Blob.fromBytes` enforces it internally;\n // other backends scale beyond it.\n const object = yield* File.fromBytes(bytes, {\n name: file.name,\n type: file.type,\n storage,\n }).pipe(\n Effect.provide(Database.layer(db)),\n Effect.catchTag('BlobTooLargeError', () => Effect.fail(new FileTooLargeError(bytes.byteLength))),\n );\n return { object };\n }),\n ),\n);\n\nexport default handler;\n"],"mappings":";;;;;;AAYA,IAAa,2BAAb,cAA8C,MAAM;CACtB;CAA5B,YAAY,MAA8B;EACxC,MAAM,0BAA0B,MAAM;EADZ,KAAA,OAAA;EAE1B,KAAK,OAAO;CACd;AACF;AAEA,IAAa,oBAAb,cAAuC,MAAM;CAEzB;CACA;CAFlB,YACE,MACA,QAAgC,KAAK,iBACrC;EACA,MAAM,sBAAsB,KAAK,iBAAiB,MAAM,QAAQ;EAHhD,KAAA,OAAA;EACA,KAAA,QAAA;EAGhB,KAAK,OAAO;CACd;AACF;AAEA,IAAa,iBAAb,cAAoC,MAAM;CACxC,cAAc;EACZ,MAAM,wCAAwC;EAC9C,KAAK,OAAO;CACd;AACF;AAEA,IAAa,gBAAb,cAAmC,MAAM;CACvC,YAAY,OAAgB;EAC1B,MAAM,+BAA+B;EACrC,KAAK,OAAO;EACZ,KAAK,QAAQ;CACf;AACF;;;;;;;;;;;;AAaA,IAAa,uBAAuB,OAAO,IAAI,aAAa;CAC1D,MAAM,WAAW,OAAO,WAAW,OAAO,OAAwB;CAClE,IAAI,SAAS,WAAW,GACtB,OAAO,OAAO,OAAO,KAAK,IAAI,eAAe,CAAC;CAGhD,MAAM,kBAAkB,OAAO,WAAW,IAAI,YAA6B,CAAC,CAAC,KAAK,OAAO,MAAM;CAC/F,MAAM,cAAc,OAAO,WAAW,IAAI,aAAa,YAAY,CAAC,CAAC,KAAK,OAAO,MAAM;CAEvF,IAAI,gBAAgB,SAAS,UAAU,YAAY,SAAS,QAAQ;EAClE,MAAM,WAAW,YAAY,MAAM,IAAI,gBAAgB,KAAK;EAC5D,IAAI,SAAS,SAAS;GACpB,MAAM,QAAQ,SAAS,MAAM,MAAM,EAAE,YAAY,SAAS,OAAO;GACjE,IAAI,OACF,OAAO,MAAM;EAEjB;CACF;AAGF,CAAC;AAED,IAAM,UAAA,OAAmF,KACvF,UAAU,YACR,OAAO,WAAW,WAAW,EAAE,MAAM,MAAM;CAEzC,IAAI,CAAC,mBAAmB,KAAK,IAAI,GAC/B,OAAO,OAAO,OAAO,KAAK,IAAI,yBAAyB,KAAK,IAAI,CAAC;CAEnE,MAAM,UAAU,OAAO;CACvB,MAAM,QAAQ,IAAI,WAChB,OAAO,OAAO,WAAW;EACvB,WAAW,KAAK,YAAY;EAC5B,QAAQ,UAAU,IAAI,cAAc,KAAK;CAC3C,CAAC,CACH;CAWA,OAAO,EAAE,QAAA,OARa,KAAK,UAAU,OAAO;EAC1C,MAAM,KAAK;EACX,MAAM,KAAK;EACX;CACF,CAAC,CAAC,CAAC,KACD,OAAO,QAAQ,SAAS,MAAM,EAAE,CAAC,GACjC,OAAO,SAAS,2BAA2B,OAAO,KAAK,IAAI,kBAAkB,MAAM,UAAU,CAAC,CAAC,CACjG,EACgB;AAClB,CAAC,CACH,CACF"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Capabilities, Capability } from "@dxos/app-framework";
|
|
2
|
+
import { Blob } from "@dxos/echo";
|
|
3
|
+
import { ClientCapabilities } from "@dxos/plugin-client";
|
|
4
|
+
import { FileCapabilities } from "#types";
|
|
5
|
+
import * as Effect from "effect/Effect";
|
|
6
|
+
//#region src/capabilities/edge-backend.ts
|
|
7
|
+
var edge_backend_default = Capability.makeModule(Effect.fnUntraced(function* () {
|
|
8
|
+
if (!(yield* Capability.get(ClientCapabilities.Client)).config.values.runtime?.services?.edge?.url) return Capability.contributes(Capabilities.Null, null);
|
|
9
|
+
return Capability.contributes(FileCapabilities.Backend, {
|
|
10
|
+
name: "Edge",
|
|
11
|
+
description: "Store files on the DXOS edge network. Scales beyond the inline size cap.",
|
|
12
|
+
storage: Blob.Storage.edge
|
|
13
|
+
});
|
|
14
|
+
}));
|
|
15
|
+
//#endregion
|
|
16
|
+
export { edge_backend_default as default };
|
|
17
|
+
|
|
18
|
+
//# sourceMappingURL=chunk-edge-backend.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chunk-edge-backend.mjs","names":[],"sources":["../../src/capabilities/edge-backend.ts"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\nimport * as Effect from 'effect/Effect';\n\nimport { Capabilities, Capability } from '@dxos/app-framework';\nimport { Blob } from '@dxos/echo';\nimport { ClientCapabilities } from '@dxos/plugin-client';\n\nimport { FileCapabilities } from '#types';\n\nexport default Capability.makeModule(\n Effect.fnUntraced(function* () {\n const client = yield* Capability.get(ClientCapabilities.Client);\n const edgeUrl = client.config.values.runtime?.services?.edge?.url;\n if (!edgeUrl) {\n return Capability.contributes(Capabilities.Null, null);\n }\n\n return Capability.contributes(FileCapabilities.Backend, {\n name: 'Edge',\n description: 'Store files on the DXOS edge network. Scales beyond the inline size cap.',\n storage: Blob.Storage.edge,\n });\n }),\n);\n"],"mappings":";;;;;;AAYA,IAAA,uBAAe,WAAW,WACxB,OAAO,WAAW,aAAa;CAG7B,IAAI,EADY,OADM,WAAW,IAAI,mBAAmB,MAAM,EAAA,CACvC,OAAO,OAAO,SAAS,UAAU,MAAM,KAE5D,OAAO,WAAW,YAAY,aAAa,MAAM,IAAI;CAGvD,OAAO,WAAW,YAAY,iBAAiB,SAAS;EACtD,MAAM;EACN,aAAa;EACb,SAAS,KAAK,QAAQ;CACxB,CAAC;AACH,CAAC,CACH"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Capabilities, Capability } from "@dxos/app-framework";
|
|
2
|
+
import { Blob, Database } from "@dxos/echo";
|
|
3
|
+
import { AppCapabilities } from "@dxos/app-toolkit";
|
|
4
|
+
import { FileOperation } from "#types";
|
|
5
|
+
import { EffectEx } from "@dxos/effect";
|
|
6
|
+
import * as Effect from "effect/Effect";
|
|
7
|
+
import { SpaceOperation } from "@dxos/plugin-space";
|
|
8
|
+
import * as Option from "effect/Option";
|
|
9
|
+
//#region src/capabilities/file-uploader.ts
|
|
10
|
+
var file_uploader_default = Capability.makeModule(Effect.fnUntraced(function* () {
|
|
11
|
+
const capabilities = yield* Capability.Service;
|
|
12
|
+
return Capability.contributes(AppCapabilities.FileUploader, (db, file) => {
|
|
13
|
+
const { invoke } = capabilities.get(Capabilities.OperationInvoker);
|
|
14
|
+
const program = Effect.gen(function* () {
|
|
15
|
+
const { object } = yield* invoke(FileOperation.Create, {
|
|
16
|
+
db,
|
|
17
|
+
file
|
|
18
|
+
});
|
|
19
|
+
yield* invoke(SpaceOperation.AddObject, {
|
|
20
|
+
target: db,
|
|
21
|
+
object
|
|
22
|
+
});
|
|
23
|
+
const blob = yield* Database.load(object.data);
|
|
24
|
+
const urlOption = yield* Blob.url(blob).pipe(Effect.provide(Database.layer(db)));
|
|
25
|
+
return {
|
|
26
|
+
name: object.name ?? file.name,
|
|
27
|
+
type: blob.type ?? "application/octet-stream",
|
|
28
|
+
...Option.isSome(urlOption) ? { url: urlOption.value } : {}
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
return EffectEx.runAndForwardErrors(program);
|
|
32
|
+
});
|
|
33
|
+
}));
|
|
34
|
+
//#endregion
|
|
35
|
+
export { file_uploader_default as default };
|
|
36
|
+
|
|
37
|
+
//# sourceMappingURL=chunk-file-uploader.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chunk-file-uploader.mjs","names":[],"sources":["../../src/capabilities/file-uploader.ts"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\nimport * as Effect from 'effect/Effect';\nimport * as Option from 'effect/Option';\n\nimport { Capabilities, Capability } from '@dxos/app-framework';\nimport { AppCapabilities } from '@dxos/app-toolkit';\nimport { Blob, Database } from '@dxos/echo';\nimport { EffectEx } from '@dxos/effect';\nimport { SpaceOperation } from '@dxos/plugin-space';\n\nimport { FileOperation } from '#types';\n\nexport default Capability.makeModule(\n Effect.fnUntraced(function* () {\n const capabilities = yield* Capability.Service;\n\n return Capability.contributes(AppCapabilities.FileUploader, (db, file) => {\n const { invoke } = capabilities.get(Capabilities.OperationInvoker);\n const program = Effect.gen(function* () {\n const { object } = yield* invoke(FileOperation.Create, { db, file });\n yield* invoke(SpaceOperation.AddObject, { target: db, object });\n const blob = yield* Database.load(object.data);\n const urlOption = yield* Blob.url(blob).pipe(Effect.provide(Database.layer(db)));\n return {\n name: object.name ?? file.name,\n type: blob.type ?? 'application/octet-stream',\n ...(Option.isSome(urlOption) ? { url: urlOption.value } : {}),\n };\n });\n\n return EffectEx.runAndForwardErrors(program);\n });\n }),\n);\n"],"mappings":";;;;;;;;;AAeA,IAAA,wBAAe,WAAW,WACxB,OAAO,WAAW,aAAa;CAC7B,MAAM,eAAe,OAAO,WAAW;CAEvC,OAAO,WAAW,YAAY,gBAAgB,eAAe,IAAI,SAAS;EACxE,MAAM,EAAE,WAAW,aAAa,IAAI,aAAa,gBAAgB;EACjE,MAAM,UAAU,OAAO,IAAI,aAAa;GACtC,MAAM,EAAE,WAAW,OAAO,OAAO,cAAc,QAAQ;IAAE;IAAI;GAAK,CAAC;GACnE,OAAO,OAAO,eAAe,WAAW;IAAE,QAAQ;IAAI;GAAO,CAAC;GAC9D,MAAM,OAAO,OAAO,SAAS,KAAK,OAAO,IAAI;GAC7C,MAAM,YAAY,OAAO,KAAK,IAAI,IAAI,CAAC,CAAC,KAAK,OAAO,QAAQ,SAAS,MAAM,EAAE,CAAC,CAAC;GAC/E,OAAO;IACL,MAAM,OAAO,QAAQ,KAAK;IAC1B,MAAM,KAAK,QAAQ;IACnB,GAAI,OAAO,OAAO,SAAS,IAAI,EAAE,KAAK,UAAU,MAAM,IAAI,CAAC;GAC7D;EACF,CAAC;EAED,OAAO,SAAS,oBAAoB,OAAO;CAC7C,CAAC;AACH,CAAC,CACH"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Capability } from "@dxos/app-framework";
|
|
2
|
+
import { Blob } from "@dxos/echo";
|
|
3
|
+
import { FileCapabilities } from "#types";
|
|
4
|
+
import * as Effect from "effect/Effect";
|
|
5
|
+
//#region src/capabilities/inline-backend.ts
|
|
6
|
+
/**
|
|
7
|
+
* Inline backend descriptor: file bytes are stored on the ECHO object itself.
|
|
8
|
+
* Exported standalone for direct testing.
|
|
9
|
+
*/
|
|
10
|
+
var inlineBackend = {
|
|
11
|
+
name: "Inline (ECHO)",
|
|
12
|
+
description: "Store the file bytes directly inside the ECHO document. Capped at 4MB; images, videos, and PDFs only.",
|
|
13
|
+
storage: Blob.Storage.inline
|
|
14
|
+
};
|
|
15
|
+
var inline_backend_default = Capability.makeModule(() => Effect.succeed(Capability.contributes(FileCapabilities.Backend, inlineBackend)));
|
|
16
|
+
//#endregion
|
|
17
|
+
export { inline_backend_default as default, inlineBackend };
|
|
18
|
+
|
|
19
|
+
//# sourceMappingURL=chunk-inline-backend.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chunk-inline-backend.mjs","names":[],"sources":["../../src/capabilities/inline-backend.ts"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\nimport * as Effect from 'effect/Effect';\n\nimport { Capability } from '@dxos/app-framework';\nimport { Blob } from '@dxos/echo';\n\nimport { FileCapabilities } from '#types';\n\n/**\n * Inline backend descriptor: file bytes are stored on the ECHO object itself.\n * Exported standalone for direct testing.\n */\nexport const inlineBackend: FileCapabilities.Backend = {\n name: 'Inline (ECHO)',\n description: 'Store the file bytes directly inside the ECHO document. Capped at 4MB; images, videos, and PDFs only.',\n storage: Blob.Storage.inline,\n};\n\nexport default Capability.makeModule(() =>\n Effect.succeed(Capability.contributes(FileCapabilities.Backend, inlineBackend)),\n);\n"],"mappings":";;;;;;;;;AAeA,IAAa,gBAA0C;CACrD,MAAM;CACN,aAAa;CACb,SAAS,KAAK,QAAQ;AACxB;AAEA,IAAA,yBAAe,WAAW,iBACxB,OAAO,QAAQ,WAAW,YAAY,iBAAiB,SAAS,aAAa,CAAC,CAChF"}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { Capability } from "@dxos/app-framework";
|
|
2
|
+
import { Blob, Database, Filter, Obj } from "@dxos/echo";
|
|
3
|
+
import { File } from "#types";
|
|
4
|
+
import "react";
|
|
5
|
+
import { EffectEx } from "@dxos/effect";
|
|
6
|
+
import { Status, ThemeProvider, defaultTx } from "@dxos/react-ui";
|
|
7
|
+
import { jsx } from "react/jsx-runtime";
|
|
8
|
+
import * as Effect from "effect/Effect";
|
|
9
|
+
import * as Option from "effect/Option";
|
|
10
|
+
import { MarkdownCapabilities } from "@dxos/plugin-markdown/types";
|
|
11
|
+
import { getSpace } from "@dxos/react-client/echo";
|
|
12
|
+
import { syntaxTree } from "@codemirror/language";
|
|
13
|
+
import { StateField } from "@codemirror/state";
|
|
14
|
+
import { Decoration, EditorView, WidgetType } from "@codemirror/view";
|
|
15
|
+
import { createRoot } from "react-dom/client";
|
|
16
|
+
import { EID } from "@dxos/keys";
|
|
17
|
+
import { focusField } from "@dxos/ui-editor";
|
|
18
|
+
//#region src/extensions/image.tsx
|
|
19
|
+
var WAIT_UNTIL_LOADER = 1500;
|
|
20
|
+
/**
|
|
21
|
+
* Decorate `` references that resolve to File objects with inline image previews.
|
|
22
|
+
*/
|
|
23
|
+
var image = (options) => {
|
|
24
|
+
const blobUrlCache = {};
|
|
25
|
+
const preloaded = /* @__PURE__ */ new Set();
|
|
26
|
+
const preload = (url) => {
|
|
27
|
+
if (!preloaded.has(url)) {
|
|
28
|
+
const img = document.createElement("img");
|
|
29
|
+
img.src = url;
|
|
30
|
+
preloaded.add(url);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
return [StateField.define({
|
|
34
|
+
create: (state) => Decoration.set(buildDecorations({
|
|
35
|
+
from: 0,
|
|
36
|
+
to: state.doc.length,
|
|
37
|
+
state,
|
|
38
|
+
blobUrlCache,
|
|
39
|
+
preload,
|
|
40
|
+
options
|
|
41
|
+
})),
|
|
42
|
+
update: (value, tr) => {
|
|
43
|
+
if (!tr.docChanged && !tr.selection) return value;
|
|
44
|
+
const cursor = tr.state.selection.main.head;
|
|
45
|
+
const oldCursor = tr.changes.mapPos(tr.startState.selection.main.head);
|
|
46
|
+
let from = Math.min(cursor, oldCursor);
|
|
47
|
+
let to = Math.max(cursor, oldCursor);
|
|
48
|
+
tr.changes.iterChangedRanges((_fromA, _toA, fromB, toB) => {
|
|
49
|
+
from = Math.min(from, fromB);
|
|
50
|
+
to = Math.max(to, toB);
|
|
51
|
+
});
|
|
52
|
+
from = tr.state.doc.lineAt(from).from;
|
|
53
|
+
to = tr.state.doc.lineAt(to).to;
|
|
54
|
+
return value.map(tr.changes).update({
|
|
55
|
+
filterFrom: from,
|
|
56
|
+
filterTo: to,
|
|
57
|
+
filter: () => false,
|
|
58
|
+
add: buildDecorations({
|
|
59
|
+
from,
|
|
60
|
+
to,
|
|
61
|
+
state: tr.state,
|
|
62
|
+
blobUrlCache,
|
|
63
|
+
preload,
|
|
64
|
+
options
|
|
65
|
+
})
|
|
66
|
+
});
|
|
67
|
+
},
|
|
68
|
+
provide: (field) => EditorView.decorations.from(field)
|
|
69
|
+
})];
|
|
70
|
+
};
|
|
71
|
+
var buildDecorations = ({ state, from, to, blobUrlCache, preload, options: { space } }) => {
|
|
72
|
+
const decorations = [];
|
|
73
|
+
const cursor = state.selection.main.head;
|
|
74
|
+
syntaxTree(state).iterate({
|
|
75
|
+
enter: (node) => {
|
|
76
|
+
if (node.name !== "Image") return;
|
|
77
|
+
const urlNode = node.node.getChild("URL");
|
|
78
|
+
if (!urlNode) return;
|
|
79
|
+
const hide = state.readOnly || cursor < node.from || cursor > node.to;
|
|
80
|
+
const urlText = state.sliceDoc(urlNode.from, urlNode.to);
|
|
81
|
+
if (!EID.isEID(urlText)) return;
|
|
82
|
+
let echoUri;
|
|
83
|
+
let echoSpaceId;
|
|
84
|
+
try {
|
|
85
|
+
const parsed = EID.parse(urlText);
|
|
86
|
+
echoUri = EID.getEntityId(parsed);
|
|
87
|
+
echoSpaceId = EID.getSpaceId(parsed);
|
|
88
|
+
} catch {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (!echoUri) return;
|
|
92
|
+
if (echoSpaceId && echoSpaceId !== space.id) return;
|
|
93
|
+
const cacheKey = urlText;
|
|
94
|
+
const blobUrlPromise = blobUrlCache[cacheKey] ?? (async () => {
|
|
95
|
+
const matched = await space.db.query(Filter.id(echoUri)).first();
|
|
96
|
+
if (!matched || !Obj.instanceOf(File.File, matched)) return;
|
|
97
|
+
const url = await EffectEx.runPromise(Effect.gen(function* () {
|
|
98
|
+
const blob = yield* Database.load(matched.data);
|
|
99
|
+
const urlOption = yield* Blob.url(blob);
|
|
100
|
+
if (Option.isSome(urlOption)) return urlOption.value;
|
|
101
|
+
const bytes = yield* Blob.read(blob);
|
|
102
|
+
return URL.createObjectURL(new globalThis.Blob([bytes], { type: blob.type }));
|
|
103
|
+
}).pipe(Effect.provide(Database.layer(space.db)), Effect.catchAll(() => Effect.succeed(void 0))));
|
|
104
|
+
if (!url) return;
|
|
105
|
+
const previous = blobUrlCache[cacheKey];
|
|
106
|
+
if (previous?.startsWith("blob:")) URL.revokeObjectURL(previous);
|
|
107
|
+
blobUrlCache[cacheKey] = url;
|
|
108
|
+
preload(url);
|
|
109
|
+
return url;
|
|
110
|
+
})();
|
|
111
|
+
decorations.push(Decoration.replace({
|
|
112
|
+
block: true,
|
|
113
|
+
widget: new DxnImageWidget(urlText, blobUrlPromise)
|
|
114
|
+
}).range(hide ? node.from : node.to, node.to));
|
|
115
|
+
},
|
|
116
|
+
from,
|
|
117
|
+
to
|
|
118
|
+
});
|
|
119
|
+
return decorations;
|
|
120
|
+
};
|
|
121
|
+
var createImg = (view, url) => {
|
|
122
|
+
const img = document.createElement("img");
|
|
123
|
+
img.setAttribute("src", url);
|
|
124
|
+
img.setAttribute("class", "cm-image-with-loader");
|
|
125
|
+
if (view.state.field(focusField)) img.onload = () => img.classList.add("cm-loaded-image");
|
|
126
|
+
else img.classList.add("cm-loaded-image");
|
|
127
|
+
return img;
|
|
128
|
+
};
|
|
129
|
+
var DxnImageWidget = class extends WidgetType {
|
|
130
|
+
_dxnUrl;
|
|
131
|
+
_url;
|
|
132
|
+
constructor(_dxnUrl, _url) {
|
|
133
|
+
super();
|
|
134
|
+
this._dxnUrl = _dxnUrl;
|
|
135
|
+
this._url = _url;
|
|
136
|
+
}
|
|
137
|
+
eq(other) {
|
|
138
|
+
return this._dxnUrl === other._dxnUrl;
|
|
139
|
+
}
|
|
140
|
+
toDOM(view) {
|
|
141
|
+
if (typeof this._url === "string") return createImg(view, this._url);
|
|
142
|
+
const widget = document.createElement("div");
|
|
143
|
+
widget.className = "cm-image-wrapper";
|
|
144
|
+
const loader = document.createElement("div");
|
|
145
|
+
loader.className = "mx-auto transition-opacity";
|
|
146
|
+
let loaderAdded = false;
|
|
147
|
+
const timeout = setTimeout(() => {
|
|
148
|
+
createRoot(loader).render(/* @__PURE__ */ jsx(ThemeProvider, {
|
|
149
|
+
tx: defaultTx,
|
|
150
|
+
children: /* @__PURE__ */ jsx(Status, { indeterminate: true })
|
|
151
|
+
}));
|
|
152
|
+
widget.appendChild(loader);
|
|
153
|
+
loaderAdded = true;
|
|
154
|
+
}, WAIT_UNTIL_LOADER);
|
|
155
|
+
Promise.resolve(this._url).then((url) => {
|
|
156
|
+
clearTimeout(timeout);
|
|
157
|
+
if (loaderAdded) widget.removeChild(loader);
|
|
158
|
+
if (url) widget.appendChild(createImg(view, url));
|
|
159
|
+
}).catch(() => {
|
|
160
|
+
clearTimeout(timeout);
|
|
161
|
+
if (loaderAdded) widget.removeChild(loader);
|
|
162
|
+
});
|
|
163
|
+
return widget;
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
//#endregion
|
|
167
|
+
//#region src/capabilities/markdown-extension.ts
|
|
168
|
+
var markdown_extension_default = Capability.makeModule(() => Effect.sync(() => {
|
|
169
|
+
const provider = ({ document, viewMode }) => {
|
|
170
|
+
if (viewMode === "source") return;
|
|
171
|
+
if (document) {
|
|
172
|
+
const space = getSpace(document);
|
|
173
|
+
if (!space) return;
|
|
174
|
+
return [image({ space })];
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
return Capability.contributes(MarkdownCapabilities.ExtensionProvider, [provider]);
|
|
178
|
+
}));
|
|
179
|
+
//#endregion
|
|
180
|
+
export { markdown_extension_default as default };
|
|
181
|
+
|
|
182
|
+
//# sourceMappingURL=chunk-markdown-extension.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chunk-markdown-extension.mjs","names":[],"sources":["../../src/extensions/image.tsx","../../src/capabilities/markdown-extension.ts"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\nimport { syntaxTree } from '@codemirror/language';\nimport { type EditorState, type Extension, type Range, StateField } from '@codemirror/state';\nimport { Decoration, EditorView, WidgetType } from '@codemirror/view';\nimport * as Effect from 'effect/Effect';\nimport * as Option from 'effect/Option';\nimport React from 'react';\nimport { createRoot } from 'react-dom/client';\n\nimport { Blob, Database, Filter, Obj } from '@dxos/echo';\nimport { EffectEx } from '@dxos/effect';\nimport { EID } from '@dxos/keys';\nimport { type Space } from '@dxos/react-client/echo';\nimport { Status, ThemeProvider } from '@dxos/react-ui';\nimport { defaultTx } from '@dxos/react-ui';\nimport { focusField } from '@dxos/ui-editor';\nimport { type MaybePromise } from '@dxos/util';\n\nimport { File } from '#types';\n\nconst WAIT_UNTIL_LOADER = 1500;\n\nexport type ImageOptions = {\n space: Space;\n};\n\n/**\n * Decorate `` references that resolve to File objects with inline image previews.\n */\nexport const image = (options: ImageOptions): Extension[] => {\n const blobUrlCache: Record<string, string> = {};\n const preloaded = new Set<string>();\n\n const preload = (url: string) => {\n if (!preloaded.has(url)) {\n const img = document.createElement('img');\n img.src = url;\n preloaded.add(url);\n }\n };\n\n return [\n StateField.define({\n create: (state) =>\n Decoration.set(buildDecorations({ from: 0, to: state.doc.length, state, blobUrlCache, preload, options })),\n update: (value, tr) => {\n if (!tr.docChanged && !tr.selection) {\n return value;\n }\n\n const cursor = tr.state.selection.main.head;\n const oldCursor = tr.changes.mapPos(tr.startState.selection.main.head);\n let from = Math.min(cursor, oldCursor);\n let to = Math.max(cursor, oldCursor);\n tr.changes.iterChangedRanges((_fromA, _toA, fromB, toB) => {\n from = Math.min(from, fromB);\n to = Math.max(to, toB);\n });\n\n from = tr.state.doc.lineAt(from).from;\n to = tr.state.doc.lineAt(to).to;\n\n return value.map(tr.changes).update({\n filterFrom: from,\n filterTo: to,\n filter: () => false,\n add: buildDecorations({ from, to, state: tr.state, blobUrlCache, preload, options }),\n });\n },\n provide: (field) => EditorView.decorations.from(field),\n }),\n ];\n};\n\nconst buildDecorations = ({\n state,\n from,\n to,\n blobUrlCache,\n preload,\n options: { space },\n}: {\n state: EditorState;\n from: number;\n to: number;\n blobUrlCache: Record<string, string>;\n preload: (url: string) => void;\n options: ImageOptions;\n}) => {\n const decorations: Range<Decoration>[] = [];\n const cursor = state.selection.main.head;\n syntaxTree(state).iterate({\n enter: (node) => {\n if (node.name !== 'Image') {\n return;\n }\n\n const urlNode = node.node.getChild('URL');\n if (!urlNode) {\n return;\n }\n\n const hide = state.readOnly || cursor < node.from || cursor > node.to;\n const urlText = state.sliceDoc(urlNode.from, urlNode.to);\n if (!EID.isEID(urlText)) {\n return;\n }\n\n let echoUri: string | undefined;\n let echoSpaceId: string | undefined;\n try {\n const parsed = EID.parse(urlText);\n echoUri = EID.getEntityId(parsed);\n echoSpaceId = EID.getSpaceId(parsed);\n } catch {\n return;\n }\n if (!echoUri) {\n return;\n }\n\n // Skip references to objects in other spaces.\n if (echoSpaceId && echoSpaceId !== space.id) {\n return;\n }\n\n const cacheKey = urlText;\n const cached = blobUrlCache[cacheKey];\n // Skip the DB query and Blob resolution entirely once a URL is cached — otherwise every\n // decoration rebuild (e.g. on cursor movement) re-fetches and, on the object-URL fallback\n // path, mints and leaks a new `blob:` URL even though the cached one is still valid.\n const blobUrlPromise =\n cached ??\n (async () => {\n const matched = await space.db.query(Filter.id(echoUri!)).first();\n if (!matched || !Obj.instanceOf(File.File, matched)) {\n return undefined;\n }\n\n const url = await EffectEx.runPromise(\n Effect.gen(function* () {\n const blob = yield* Database.load(matched.data);\n const urlOption = yield* Blob.url(blob);\n if (Option.isSome(urlOption)) {\n return urlOption.value;\n }\n const bytes = yield* Blob.read(blob);\n // `Uint8Array` is generic over `ArrayBufferLike` (incl. `SharedArrayBuffer`) while\n // DOM's `BlobPart` only covers `ArrayBuffer`-backed views — a gap between the DOM lib\n // types and the TS standard lib, not fixable by typing `bytes` differently.\n return URL.createObjectURL(new globalThis.Blob([bytes as BlobPart], { type: blob.type }));\n }).pipe(\n Effect.provide(Database.layer(space.db)),\n Effect.catchAll(() => Effect.succeed(undefined)),\n ),\n );\n if (!url) {\n return undefined;\n }\n // Only object URLs we minted via `createObjectURL` above need revoking — `Blob.url()`\n // results (data:/https: URLs) aren't owned by this cache.\n const previous = blobUrlCache[cacheKey];\n if (previous?.startsWith('blob:')) {\n URL.revokeObjectURL(previous);\n }\n blobUrlCache[cacheKey] = url;\n preload(url);\n return url;\n })();\n\n decorations.push(\n Decoration.replace({\n block: true,\n widget: new DxnImageWidget(urlText, blobUrlPromise),\n }).range(hide ? node.from : node.to, node.to),\n );\n },\n from,\n to,\n });\n\n return decorations;\n};\n\nconst createImg = (view: EditorView, url: string) => {\n const img = document.createElement('img');\n img.setAttribute('src', url);\n img.setAttribute('class', 'cm-image-with-loader');\n if (view.state.field(focusField)) {\n img.onload = () => img.classList.add('cm-loaded-image');\n } else {\n img.classList.add('cm-loaded-image');\n }\n return img;\n};\n\nclass DxnImageWidget extends WidgetType {\n constructor(\n readonly _dxnUrl: string,\n readonly _url: MaybePromise<string | undefined>,\n ) {\n super();\n }\n\n override eq(other: this) {\n return this._dxnUrl === other._dxnUrl;\n }\n\n override toDOM(view: EditorView) {\n if (typeof this._url === 'string') {\n return createImg(view, this._url);\n }\n\n const widget = document.createElement('div');\n widget.className = 'cm-image-wrapper';\n const loader = document.createElement('div');\n loader.className = 'mx-auto transition-opacity';\n let loaderAdded = false;\n\n const timeout = setTimeout(() => {\n const root = createRoot(loader);\n root.render(\n <ThemeProvider tx={defaultTx}>\n <Status indeterminate />\n </ThemeProvider>,\n );\n widget.appendChild(loader);\n loaderAdded = true;\n }, WAIT_UNTIL_LOADER);\n\n void Promise.resolve(this._url)\n .then((url) => {\n clearTimeout(timeout);\n if (loaderAdded) {\n widget.removeChild(loader);\n }\n if (url) {\n widget.appendChild(createImg(view, url));\n }\n })\n .catch(() => {\n clearTimeout(timeout);\n if (loaderAdded) {\n widget.removeChild(loader);\n }\n });\n\n return widget;\n }\n}\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport * as Effect from 'effect/Effect';\n\nimport { Capability } from '@dxos/app-framework';\nimport { MarkdownCapabilities, type MarkdownExtensionProvider } from '@dxos/plugin-markdown/types';\nimport { getSpace } from '@dxos/react-client/echo';\n\nimport { image } from '../extensions';\n\nexport default Capability.makeModule(() =>\n Effect.sync(() => {\n const provider: MarkdownExtensionProvider = ({ document, viewMode }) => {\n if (viewMode === 'source') {\n return undefined;\n }\n\n if (document) {\n const space = getSpace(document);\n if (!space) {\n return undefined;\n }\n return [image({ space })];\n }\n\n return undefined;\n };\n\n return Capability.contributes(MarkdownCapabilities.ExtensionProvider, [provider]);\n }),\n);\n"],"mappings":"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"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { trim } from "@dxos/util";
|
|
2
|
+
import { Plugin } from "@dxos/app-framework";
|
|
3
|
+
import { Config2 } from "@dxos/app-framework/config";
|
|
4
|
+
//#region dx.config.ts
|
|
5
|
+
var dx_config_default = Config2.make({ plugin: {
|
|
6
|
+
key: "org.dxos.plugin.file",
|
|
7
|
+
name: "File",
|
|
8
|
+
author: "DXOS",
|
|
9
|
+
description: trim`
|
|
10
|
+
FilePlugin enables DXOS Composer to store and preview binary files — images, videos, and PDFs — as
|
|
11
|
+
first-class ECHO objects. Uploaded bytes are routed through a pluggable backend system: the built-in
|
|
12
|
+
inline backend serialises the file directly onto the ECHO document, while external backends (e.g. WNFS)
|
|
13
|
+
can be contributed by other plugins.
|
|
14
|
+
|
|
15
|
+
Accepted formats are images (jpg, jpeg, png, gif, webp, svg), videos (mp4, webm, mov), and PDFs,
|
|
16
|
+
up to a 4 MB limit per file for the inline backend (other backends scale beyond it). The preview
|
|
17
|
+
surface renders the appropriate HTML element for each MIME class and surfaces inside article,
|
|
18
|
+
section, and slide roles.
|
|
19
|
+
|
|
20
|
+
A File object's \`data\` field is a reference to a \`Blob\` ECHO object (from \`@dxos/echo\`), which
|
|
21
|
+
abstracts where bytes actually live. \`Blob.url\`/\`Blob.read\` resolve a renderable URL or the raw
|
|
22
|
+
bytes regardless of backend, keeping the File object schema backend-agnostic without a separate
|
|
23
|
+
URL-resolver capability. A companion Markdown extension allows file objects to be embedded directly
|
|
24
|
+
in Markdown documents.
|
|
25
|
+
|
|
26
|
+
Backend selection is user-configurable in the plugin settings panel, which lists all registered
|
|
27
|
+
backends with their descriptions. Selecting a backend persists the choice to the ECHO-backed settings
|
|
28
|
+
atom so the preference is shared across devices.
|
|
29
|
+
`,
|
|
30
|
+
icon: {
|
|
31
|
+
key: "ph--file--regular",
|
|
32
|
+
hue: "indigo"
|
|
33
|
+
},
|
|
34
|
+
source: "https://github.com/dxos/dxos/tree/main/packages/plugins/plugin-file",
|
|
35
|
+
spec: "PLUGIN.mdl",
|
|
36
|
+
tags: ["alpha"]
|
|
37
|
+
} });
|
|
38
|
+
//#endregion
|
|
39
|
+
//#region src/meta.ts
|
|
40
|
+
var meta = Plugin.getMetaFromConfig(dx_config_default);
|
|
41
|
+
//#endregion
|
|
42
|
+
export { meta as t };
|
|
43
|
+
|
|
44
|
+
//# sourceMappingURL=chunk-meta.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chunk-meta.mjs","names":[],"sources":["../../dx.config.ts","../../src/meta.ts"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\nimport { Config2 } from '@dxos/app-framework/config';\nimport { trim } from '@dxos/util';\n\nexport default Config2.make({\n plugin: {\n key: 'org.dxos.plugin.file',\n name: 'File',\n author: 'DXOS',\n description: trim`\n FilePlugin enables DXOS Composer to store and preview binary files — images, videos, and PDFs — as\n first-class ECHO objects. Uploaded bytes are routed through a pluggable backend system: the built-in\n inline backend serialises the file directly onto the ECHO document, while external backends (e.g. WNFS)\n can be contributed by other plugins.\n\n Accepted formats are images (jpg, jpeg, png, gif, webp, svg), videos (mp4, webm, mov), and PDFs,\n up to a 4 MB limit per file for the inline backend (other backends scale beyond it). The preview\n surface renders the appropriate HTML element for each MIME class and surfaces inside article,\n section, and slide roles.\n\n A File object's \\`data\\` field is a reference to a \\`Blob\\` ECHO object (from \\`@dxos/echo\\`), which\n abstracts where bytes actually live. \\`Blob.url\\`/\\`Blob.read\\` resolve a renderable URL or the raw\n bytes regardless of backend, keeping the File object schema backend-agnostic without a separate\n URL-resolver capability. A companion Markdown extension allows file objects to be embedded directly\n in Markdown documents.\n\n Backend selection is user-configurable in the plugin settings panel, which lists all registered\n backends with their descriptions. Selecting a backend persists the choice to the ECHO-backed settings\n atom so the preference is shared across devices.\n `,\n icon: { key: 'ph--file--regular', hue: 'indigo' },\n source: 'https://github.com/dxos/dxos/tree/main/packages/plugins/plugin-file',\n spec: 'PLUGIN.mdl',\n tags: ['alpha'],\n },\n});\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport { Plugin } from '@dxos/app-framework';\n\nimport config from '../dx.config';\n\nexport const meta = Plugin.getMetaFromConfig(config);\n"],"mappings":";;;;AAOA,IAAA,oBAAe,QAAQ,KAAK,EAC1B,QAAQ;CACN,KAAK;CACL,MAAM;CACN,QAAQ;CACR,aAAa,IAAI;;;;;;;;;;;;;;;;;;;;;CAqBjB,MAAM;EAAE,KAAK;EAAqB,KAAK;CAAS;CAChD,QAAQ;CACR,MAAM;CACN,MAAM,CAAC,OAAO;AAChB,EACF,CAAC;;;AC9BD,IAAa,OAAO,OAAO,kBAAkB,iBAAM"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Capabilities, Capability } from "@dxos/app-framework";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import { FileOperationHandlerSet } from "#operations";
|
|
4
|
+
//#region src/capabilities/operation-handler.ts
|
|
5
|
+
var operation_handler_default = Capability.makeModule(Effect.fnUntraced(function* () {
|
|
6
|
+
return Capability.contributes(Capabilities.OperationHandler, FileOperationHandlerSet);
|
|
7
|
+
}));
|
|
8
|
+
//#endregion
|
|
9
|
+
export { operation_handler_default as default };
|
|
10
|
+
|
|
11
|
+
//# sourceMappingURL=chunk-operation-handler.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chunk-operation-handler.mjs","names":[],"sources":["../../src/capabilities/operation-handler.ts"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\nimport * as Effect from 'effect/Effect';\n\nimport { Capabilities, Capability } from '@dxos/app-framework';\nimport type { OperationHandlerSet } from '@dxos/compute';\n\nimport { FileOperationHandlerSet } from '#operations';\n\nexport default Capability.makeModule<OperationHandlerSet.OperationHandlerSet>(\n Effect.fnUntraced(function* () {\n return Capability.contributes(Capabilities.OperationHandler, FileOperationHandlerSet);\n }),\n);\n"],"mappings":";;;;AAWA,IAAA,4BAAe,WAAW,WACxB,OAAO,WAAW,aAAa;CAC7B,OAAO,WAAW,YAAY,aAAa,kBAAkB,uBAAuB;AACtF,CAAC,CACH"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { meta } from "#meta";
|
|
2
|
+
import { Capabilities, Capability } from "@dxos/app-framework";
|
|
3
|
+
import { File, FileAction } from "#types";
|
|
4
|
+
import { useCallback } from "react";
|
|
5
|
+
import { SchemaEx } from "@dxos/effect";
|
|
6
|
+
import { jsx } from "react/jsx-runtime";
|
|
7
|
+
import { Surface } from "@dxos/app-framework/ui";
|
|
8
|
+
import * as Effect from "effect/Effect";
|
|
9
|
+
import { AppSurface } from "@dxos/app-toolkit/ui";
|
|
10
|
+
import { FileArticle, FileSettings } from "#containers";
|
|
11
|
+
import { FileInput } from "#components";
|
|
12
|
+
//#region src/capabilities/FileUploadField.tsx
|
|
13
|
+
/**
|
|
14
|
+
* Form field that uploads a file for an upload-annotated property. It consumes the whole surface
|
|
15
|
+
* envelope, so it is registered without a `props` mapper.
|
|
16
|
+
*/
|
|
17
|
+
var FileUploadField = ({ data, onValueChange }) => {
|
|
18
|
+
const ast = data.fieldPropertyAst;
|
|
19
|
+
const handleChange = useCallback((file) => {
|
|
20
|
+
if (ast) onValueChange?.(ast, file);
|
|
21
|
+
}, [ast, onValueChange]);
|
|
22
|
+
if (!ast) return null;
|
|
23
|
+
return /* @__PURE__ */ jsx(FileInput, {
|
|
24
|
+
schema: data.schema,
|
|
25
|
+
onChange: handleChange
|
|
26
|
+
});
|
|
27
|
+
};
|
|
28
|
+
//#endregion
|
|
29
|
+
//#region src/capabilities/react-surface.ts
|
|
30
|
+
var react_surface_default = Capability.makeModule(() => Effect.succeed(Capability.contributes(Capabilities.ReactSurface, [
|
|
31
|
+
Surface.create({
|
|
32
|
+
id: "article",
|
|
33
|
+
filter: AppSurface.oneOf(AppSurface.object(AppSurface.Article, File.File), AppSurface.object(AppSurface.Section, File.File), AppSurface.object(AppSurface.Slide, File.File)),
|
|
34
|
+
component: FileArticle,
|
|
35
|
+
props: ({ role, data: { subject } }) => ({
|
|
36
|
+
role,
|
|
37
|
+
subject
|
|
38
|
+
})
|
|
39
|
+
}),
|
|
40
|
+
Surface.create({
|
|
41
|
+
id: "createForm",
|
|
42
|
+
filter: AppSurface.formInputBySchema((ast) => !!SchemaEx.findAnnotation(ast, FileAction.UploadAnnotationId)),
|
|
43
|
+
component: FileUploadField
|
|
44
|
+
}),
|
|
45
|
+
Surface.create({
|
|
46
|
+
id: "pluginSettings",
|
|
47
|
+
filter: AppSurface.settings(AppSurface.Article, meta.profile.key),
|
|
48
|
+
component: FileSettings,
|
|
49
|
+
props: ({ data: { subject } }) => ({ subject })
|
|
50
|
+
})
|
|
51
|
+
])));
|
|
52
|
+
//#endregion
|
|
53
|
+
export { react_surface_default as default };
|
|
54
|
+
|
|
55
|
+
//# sourceMappingURL=chunk-react-surface.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chunk-react-surface.mjs","names":[],"sources":["../../src/capabilities/FileUploadField.tsx","../../src/capabilities/react-surface.ts"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\nimport React, { useCallback } from 'react';\n\nimport { type Surface } from '@dxos/app-framework/ui';\nimport { type AppSurface } from '@dxos/app-toolkit/ui';\nimport { type FormFieldRendererProps } from '@dxos/react-ui-form';\n\nimport { FileInput } from '#components';\n\n/** The form renderer's own props ride alongside `data` on the surface envelope. */\nexport type FileUploadFieldProps = Surface.ComponentProps<AppSurface.FormInputData> &\n Pick<FormFieldRendererProps<File>, 'onValueChange'>;\n\n/**\n * Form field that uploads a file for an upload-annotated property. It consumes the whole surface\n * envelope, so it is registered without a `props` mapper.\n */\nexport const FileUploadField = ({ data, onValueChange }: FileUploadFieldProps) => {\n const ast = data.fieldPropertyAst;\n const handleChange = useCallback(\n (file: File) => {\n if (ast) {\n onValueChange?.(ast, file);\n }\n },\n [ast, onValueChange],\n );\n\n if (!ast) {\n return null;\n }\n\n return <FileInput schema={data.schema} onChange={handleChange} />;\n};\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport * as Effect from 'effect/Effect';\n\nimport { Capabilities, Capability } from '@dxos/app-framework';\nimport { Surface } from '@dxos/app-framework/ui';\nimport { AppSurface } from '@dxos/app-toolkit/ui';\nimport { SchemaEx } from '@dxos/effect';\n\nimport { FileArticle, FileSettings } from '#containers';\nimport { meta } from '#meta';\nimport { File, FileAction } from '#types';\n\nimport { FileUploadField } from './FileUploadField';\n\nexport default Capability.makeModule(() =>\n Effect.succeed(\n Capability.contributes(Capabilities.ReactSurface, [\n Surface.create({\n id: 'article',\n filter: AppSurface.oneOf(\n AppSurface.object(AppSurface.Article, File.File),\n AppSurface.object(AppSurface.Section, File.File),\n AppSurface.object(AppSurface.Slide, File.File),\n ),\n component: FileArticle,\n props: ({ role, data: { subject } }) => ({ role, subject }),\n }),\n Surface.create({\n id: 'createForm',\n filter: AppSurface.formInputBySchema(\n (ast) => !!SchemaEx.findAnnotation<Record<string, string[]>>(ast, FileAction.UploadAnnotationId),\n ),\n component: FileUploadField,\n }),\n Surface.create({\n id: 'pluginSettings',\n filter: AppSurface.settings(AppSurface.Article, meta.profile.key),\n component: FileSettings,\n props: ({ data: { subject } }) => ({ subject }),\n }),\n ]),\n ),\n);\n"],"mappings":";;;;;;;;;;;;;;;;AAoBA,IAAa,mBAAmB,EAAE,MAAM,oBAA0C;CAChF,MAAM,MAAM,KAAK;CACjB,MAAM,eAAe,aAClB,SAAe;EACd,IAAI,KACF,gBAAgB,KAAK,IAAI;CAE7B,GACA,CAAC,KAAK,aAAa,CACrB;CAEA,IAAI,CAAC,KACH,OAAO;CAGT,OAAO,oBAAC,WAAD;EAAW,QAAQ,KAAK;EAAQ,UAAU;CAAe,CAAA;AAClE;;;ACnBA,IAAA,wBAAe,WAAW,iBACxB,OAAO,QACL,WAAW,YAAY,aAAa,cAAc;CAChD,QAAQ,OAAO;EACb,IAAI;EACJ,QAAQ,WAAW,MACjB,WAAW,OAAO,WAAW,SAAS,KAAK,IAAI,GAC/C,WAAW,OAAO,WAAW,SAAS,KAAK,IAAI,GAC/C,WAAW,OAAO,WAAW,OAAO,KAAK,IAAI,CAC/C;EACA,WAAW;EACX,QAAQ,EAAE,MAAM,MAAM,EAAE,iBAAiB;GAAE;GAAM;EAAQ;CAC3D,CAAC;CACD,QAAQ,OAAO;EACb,IAAI;EACJ,QAAQ,WAAW,mBAChB,QAAQ,CAAC,CAAC,SAAS,eAAyC,KAAK,WAAW,kBAAkB,CACjG;EACA,WAAW;CACb,CAAC;CACD,QAAQ,OAAO;EACb,IAAI;EACJ,QAAQ,WAAW,SAAS,WAAW,SAAS,KAAK,QAAQ,GAAG;EAChE,WAAW;EACX,QAAQ,EAAE,MAAM,EAAE,iBAAiB,EAAE,QAAQ;CAC/C,CAAC;AACH,CAAC,CACH,CACF"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { a as Read } from "./chunk-types.mjs";
|
|
2
|
+
import { Operation } from "@dxos/compute";
|
|
3
|
+
import { ContentBlock } from "@dxos/types";
|
|
4
|
+
import { Blob, Database } from "@dxos/echo";
|
|
5
|
+
import * as Effect from "effect/Effect";
|
|
6
|
+
import * as Option from "effect/Option";
|
|
7
|
+
//#region src/operations/read.ts
|
|
8
|
+
var BASE64_CHUNK_SIZE = 32768;
|
|
9
|
+
var bytesToBase64 = (bytes) => {
|
|
10
|
+
let binary = "";
|
|
11
|
+
for (let offset = 0; offset < bytes.length; offset += BASE64_CHUNK_SIZE) binary += String.fromCharCode(...bytes.subarray(offset, offset + BASE64_CHUNK_SIZE));
|
|
12
|
+
return btoa(binary);
|
|
13
|
+
};
|
|
14
|
+
var handler = Read.pipe(Operation.withHandler(Effect.fn(function* ({ file }) {
|
|
15
|
+
const obj = yield* Database.load(file);
|
|
16
|
+
const blob = yield* Database.load(obj.data);
|
|
17
|
+
const type = blob.type ?? "application/octet-stream";
|
|
18
|
+
const urlOption = yield* Blob.url(blob);
|
|
19
|
+
const url = yield* Option.match(urlOption, {
|
|
20
|
+
onSome: Effect.succeed,
|
|
21
|
+
onNone: () => Blob.read(blob).pipe(Effect.map((bytes) => `data:${type};base64,${bytesToBase64(bytes)}`))
|
|
22
|
+
});
|
|
23
|
+
return ContentBlock.ContentBlockResult.make({ content: [ContentBlock.File.make({
|
|
24
|
+
url,
|
|
25
|
+
name: obj.name,
|
|
26
|
+
mediaType: type
|
|
27
|
+
})] });
|
|
28
|
+
})));
|
|
29
|
+
//#endregion
|
|
30
|
+
export { handler as default };
|
|
31
|
+
|
|
32
|
+
//# sourceMappingURL=chunk-read.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chunk-read.mjs","names":[],"sources":["../../src/operations/read.ts"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\nimport * as Effect from 'effect/Effect';\nimport * as Option from 'effect/Option';\n\nimport { Operation } from '@dxos/compute';\nimport { Blob, Database } from '@dxos/echo';\nimport { ContentBlock } from '@dxos/types';\n\nimport { FileOperation } from '../types';\n\nconst BASE64_CHUNK_SIZE = 0x8000;\n\n// `String.fromCharCode(...bytes)` blows the call stack / argument limit for large arrays, so the\n// bytes are base64-encoded in chunks. `Buffer` isn't available in browser bundles.\nconst bytesToBase64 = (bytes: Uint8Array): string => {\n let binary = '';\n for (let offset = 0; offset < bytes.length; offset += BASE64_CHUNK_SIZE) {\n binary += String.fromCharCode(...bytes.subarray(offset, offset + BASE64_CHUNK_SIZE));\n }\n return btoa(binary);\n};\n\nconst handler: Operation.WithHandler<typeof FileOperation.Read> = FileOperation.Read.pipe(\n Operation.withHandler(\n Effect.fn(function* ({ file }) {\n const obj = yield* Database.load(file);\n const blob = yield* Database.load(obj.data);\n const type = blob.type ?? 'application/octet-stream';\n const urlOption = yield* Blob.url(blob);\n const url = yield* Option.match(urlOption, {\n onSome: Effect.succeed,\n // No renderable URL from the backend (e.g. external storage without `getUrl`) — fall back\n // to reading the bytes directly and encoding them as a `data:` URL.\n onNone: () => Blob.read(blob).pipe(Effect.map((bytes) => `data:${type};base64,${bytesToBase64(bytes)}`)),\n });\n\n return ContentBlock.ContentBlockResult.make({\n content: [\n ContentBlock.File.make({\n url,\n name: obj.name,\n mediaType: type,\n }),\n ],\n });\n }),\n ),\n);\n\nexport default handler;\n"],"mappings":";;;;;;;AAaA,IAAM,oBAAoB;AAI1B,IAAM,iBAAiB,UAA8B;CACnD,IAAI,SAAS;CACb,KAAK,IAAI,SAAS,GAAG,SAAS,MAAM,QAAQ,UAAU,mBACpD,UAAU,OAAO,aAAa,GAAG,MAAM,SAAS,QAAQ,SAAS,iBAAiB,CAAC;CAErF,OAAO,KAAK,MAAM;AACpB;AAEA,IAAM,UAAA,KAA+E,KACnF,UAAU,YACR,OAAO,GAAG,WAAW,EAAE,QAAQ;CAC7B,MAAM,MAAM,OAAO,SAAS,KAAK,IAAI;CACrC,MAAM,OAAO,OAAO,SAAS,KAAK,IAAI,IAAI;CAC1C,MAAM,OAAO,KAAK,QAAQ;CAC1B,MAAM,YAAY,OAAO,KAAK,IAAI,IAAI;CACtC,MAAM,MAAM,OAAO,OAAO,MAAM,WAAW;EACzC,QAAQ,OAAO;EAGf,cAAc,KAAK,KAAK,IAAI,CAAC,CAAC,KAAK,OAAO,KAAK,UAAU,QAAQ,KAAK,UAAU,cAAc,KAAK,GAAG,CAAC;CACzG,CAAC;CAED,OAAO,aAAa,mBAAmB,KAAK,EAC1C,SAAS,CACP,aAAa,KAAK,KAAK;EACrB;EACA,MAAM,IAAI;EACV,WAAW;CACb,CAAC,CACH,EACF,CAAC;AACH,CAAC,CACH,CACF"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { meta } from "#meta";
|
|
2
|
+
import { Capability } from "@dxos/app-framework";
|
|
3
|
+
import { AppCapabilities } from "@dxos/app-toolkit";
|
|
4
|
+
import { FileCapabilities, Settings } from "#types";
|
|
5
|
+
import { createKvsStore } from "@dxos/effect";
|
|
6
|
+
import * as Effect from "effect/Effect";
|
|
7
|
+
//#region src/capabilities/settings.ts
|
|
8
|
+
var settings_default = Capability.makeModule(() => Effect.sync(() => {
|
|
9
|
+
const settingsAtom = createKvsStore({
|
|
10
|
+
key: meta.profile.key,
|
|
11
|
+
schema: Settings.Settings,
|
|
12
|
+
defaultValue: () => ({})
|
|
13
|
+
});
|
|
14
|
+
return [Capability.contributes(FileCapabilities.SettingsAtom, settingsAtom), Capability.contributes(AppCapabilities.Settings, {
|
|
15
|
+
prefix: meta.profile.key,
|
|
16
|
+
schema: Settings.Settings,
|
|
17
|
+
atom: settingsAtom
|
|
18
|
+
})];
|
|
19
|
+
}));
|
|
20
|
+
//#endregion
|
|
21
|
+
export { settings_default as default };
|
|
22
|
+
|
|
23
|
+
//# sourceMappingURL=chunk-settings.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chunk-settings.mjs","names":[],"sources":["../../src/capabilities/settings.ts"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\nimport * as Effect from 'effect/Effect';\n\nimport { Capability } from '@dxos/app-framework';\nimport { AppCapabilities } from '@dxos/app-toolkit';\nimport { createKvsStore } from '@dxos/effect';\n\nimport { meta } from '#meta';\nimport { FileCapabilities, Settings } from '#types';\n\nexport default Capability.makeModule(() =>\n Effect.sync(() => {\n const settingsAtom = createKvsStore({\n key: meta.profile.key,\n schema: Settings.Settings,\n defaultValue: () => ({}),\n });\n\n return [\n Capability.contributes(FileCapabilities.SettingsAtom, settingsAtom),\n Capability.contributes(AppCapabilities.Settings, {\n prefix: meta.profile.key,\n schema: Settings.Settings,\n atom: settingsAtom,\n }),\n ];\n }),\n);\n"],"mappings":";;;;;;;AAaA,IAAA,mBAAe,WAAW,iBACxB,OAAO,WAAW;CAChB,MAAM,eAAe,eAAe;EAClC,KAAK,KAAK,QAAQ;EAClB,QAAQ,SAAS;EACjB,qBAAqB,CAAC;CACxB,CAAC;CAED,OAAO,CACL,WAAW,YAAY,iBAAiB,cAAc,YAAY,GAClE,WAAW,YAAY,gBAAgB,UAAU;EAC/C,QAAQ,KAAK,QAAQ;EACrB,QAAQ,SAAS;EACjB,MAAM;CACR,CAAC,CACH;AACF,CAAC,CACH"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Capability } from "@dxos/app-framework";
|
|
2
|
+
import { AppCapabilities } from "@dxos/app-toolkit";
|
|
3
|
+
import * as Effect from "effect/Effect";
|
|
4
|
+
import { FileSkill } from "#skills";
|
|
5
|
+
//#region src/capabilities/skill-definition.ts
|
|
6
|
+
var skillDefinition = () => Effect.succeed([Capability.contributes(AppCapabilities.SkillDefinition, FileSkill)]);
|
|
7
|
+
//#endregion
|
|
8
|
+
export { skillDefinition as default };
|
|
9
|
+
|
|
10
|
+
//# sourceMappingURL=chunk-skill-definition.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chunk-skill-definition.mjs","names":[],"sources":["../../src/capabilities/skill-definition.ts"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\nimport * as Effect from 'effect/Effect';\n\nimport { Capability } from '@dxos/app-framework';\nimport { AppCapabilities } from '@dxos/app-toolkit';\n\nimport { FileSkill } from '#skills';\n\nconst skillDefinition = () => Effect.succeed([Capability.contributes(AppCapabilities.SkillDefinition, FileSkill)]);\n\nexport default skillDefinition;\n"],"mappings":";;;;;AAWA,IAAM,wBAAwB,OAAO,QAAQ,CAAC,WAAW,YAAY,gBAAgB,iBAAiB,SAAS,CAAC,CAAC"}
|