@trycua/cua 0.2.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/README.md +121 -0
- package/bin/cua.js +37 -0
- package/browser/cua_sdk-ffi.d.ts +6 -0
- package/browser/cua_sdk-ffi.js +3 -0
- package/browser/cua_sdk-ffi.ts +14 -0
- package/browser/cua_sdk.d.ts +2237 -0
- package/browser/cua_sdk.js +2607 -0
- package/browser/cua_sdk.ts +3644 -0
- package/browser/cyclops_sdk_schema-ffi.d.ts +6 -0
- package/browser/cyclops_sdk_schema-ffi.js +3 -0
- package/browser/cyclops_sdk_schema-ffi.ts +14 -0
- package/browser/cyclops_sdk_schema.d.ts +996 -0
- package/browser/cyclops_sdk_schema.js +2194 -0
- package/browser/cyclops_sdk_schema.ts +2934 -0
- package/browser/fleet_sdk-ffi.d.ts +28 -0
- package/browser/fleet_sdk-ffi.js +3 -0
- package/browser/fleet_sdk-ffi.ts +35 -0
- package/browser/fleet_sdk.d.ts +3056 -0
- package/browser/fleet_sdk.js +5088 -0
- package/browser/fleet_sdk.ts +6830 -0
- package/browser/index.d.ts +2 -0
- package/browser/index.js +18 -0
- package/browser/index.web.ts +34 -0
- package/browser/tsconfig.json +20 -0
- package/browser/wasm-bindgen/index.d.ts +2346 -0
- package/browser/wasm-bindgen/index.js +6642 -0
- package/browser/wasm-bindgen/index_bg.wasm +0 -0
- package/browser/wasm-bindgen/index_bg.wasm.d.ts +1154 -0
- package/dist/index.d.ts +118 -0
- package/dist/index.js +207 -0
- package/dist/mcp.d.ts +33 -0
- package/dist/mcp.js +41 -0
- package/dist/native/cua_sdk-ffi.d.ts +1038 -0
- package/dist/native/cua_sdk-ffi.js +5095 -0
- package/dist/native/cua_sdk.d.ts +15906 -0
- package/dist/native/cua_sdk.js +27061 -0
- package/dist/native/index.d.ts +7 -0
- package/dist/native/index.js +12 -0
- package/dist/native/node-runtime.d.ts +72 -0
- package/dist/native/node-runtime.js +35 -0
- package/dist/spaces/cursorArt.d.ts +27 -0
- package/dist/spaces/cursorArt.js +59 -0
- package/dist/spaces/errors.d.ts +81 -0
- package/dist/spaces/errors.js +102 -0
- package/dist/spaces/events.d.ts +154 -0
- package/dist/spaces/events.js +182 -0
- package/dist/spaces/groups.d.ts +130 -0
- package/dist/spaces/groups.js +275 -0
- package/dist/spaces/host.d.ts +71 -0
- package/dist/spaces/host.js +154 -0
- package/dist/spaces/index.d.ts +48 -0
- package/dist/spaces/index.js +44 -0
- package/dist/spaces/pip.d.ts +128 -0
- package/dist/spaces/pip.js +250 -0
- package/dist/spaces/presence.d.ts +187 -0
- package/dist/spaces/presence.js +449 -0
- package/dist/spaces/presenceTypes.check.d.ts +12 -0
- package/dist/spaces/presenceTypes.check.js +1 -0
- package/dist/spaces/presenceTypes.d.ts +47 -0
- package/dist/spaces/presenceTypes.js +7 -0
- package/dist/spaces/routines.d.ts +158 -0
- package/dist/spaces/routines.js +339 -0
- package/dist/spaces/thread.d.ts +147 -0
- package/dist/spaces/thread.js +285 -0
- package/dist/spaces/transport/http.d.ts +51 -0
- package/dist/spaces/transport/http.js +113 -0
- package/dist/spaces/transport/index.d.ts +19 -0
- package/dist/spaces/transport/index.js +19 -0
- package/dist/spaces/transport/session.d.ts +120 -0
- package/dist/spaces/transport/session.js +188 -0
- package/dist/spaces/transport/tauri.d.ts +50 -0
- package/dist/spaces/transport/tauri.js +90 -0
- package/dist/spaces/transport/types.d.ts +123 -0
- package/dist/spaces/transport/types.js +59 -0
- package/dist/teleport/controller.d.ts +34 -0
- package/dist/teleport/controller.js +107 -0
- package/dist/teleport/drop.d.ts +62 -0
- package/dist/teleport/drop.js +138 -0
- package/dist/teleport/dropZone.d.ts +59 -0
- package/dist/teleport/dropZone.js +166 -0
- package/dist/teleport/element.d.ts +28 -0
- package/dist/teleport/element.js +340 -0
- package/dist/teleport/index.d.ts +32 -0
- package/dist/teleport/index.js +32 -0
- package/dist/teleport/install.d.ts +31 -0
- package/dist/teleport/install.js +65 -0
- package/dist/teleport/model.d.ts +245 -0
- package/dist/teleport/model.js +334 -0
- package/dist/teleport/windowDrag.d.ts +86 -0
- package/dist/teleport/windowDrag.js +71 -0
- package/package.json +93 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* {@link TeleportPickerController}: the headless state machine bound to a
|
|
3
|
+
* {@link TeleportHost}. Frameworks subscribe to it (React:
|
|
4
|
+
* `useSyncExternalStore(c.subscribe, () => c.state)`; the web component
|
|
5
|
+
* re-renders on each change).
|
|
6
|
+
*/
|
|
7
|
+
import { canConfirm, canPlan, initialState, planSensitive, reduce, } from "./model.js";
|
|
8
|
+
function message(e) {
|
|
9
|
+
return e instanceof Error ? e.message : String(e);
|
|
10
|
+
}
|
|
11
|
+
export class TeleportPickerController {
|
|
12
|
+
#state;
|
|
13
|
+
#listeners = new Set();
|
|
14
|
+
#generation = 0;
|
|
15
|
+
host;
|
|
16
|
+
constructor(host, options) {
|
|
17
|
+
this.host = host;
|
|
18
|
+
this.#state = initialState(options.spaceName);
|
|
19
|
+
if (options.preselect) {
|
|
20
|
+
this.#state = reduce(this.#state, {
|
|
21
|
+
type: "preselect",
|
|
22
|
+
entry: options.preselect.entry,
|
|
23
|
+
files: options.preselect.files ?? [],
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
get state() {
|
|
28
|
+
return this.#state;
|
|
29
|
+
}
|
|
30
|
+
/** Subscribe to changes; returns the unsubscribe. */
|
|
31
|
+
subscribe = (listener) => {
|
|
32
|
+
this.#listeners.add(listener);
|
|
33
|
+
return () => this.#listeners.delete(listener);
|
|
34
|
+
};
|
|
35
|
+
dispatch = (event) => {
|
|
36
|
+
const next = reduce(this.#state, event);
|
|
37
|
+
if (next === this.#state)
|
|
38
|
+
return;
|
|
39
|
+
this.#state = next;
|
|
40
|
+
for (const l of [...this.#listeners])
|
|
41
|
+
l();
|
|
42
|
+
};
|
|
43
|
+
/** Loads (or reloads) the catalog. */
|
|
44
|
+
async load() {
|
|
45
|
+
const gen = ++this.#generation;
|
|
46
|
+
try {
|
|
47
|
+
const entries = await this.host.catalog();
|
|
48
|
+
if (gen === this.#generation)
|
|
49
|
+
this.dispatch({ type: "loaded", entries });
|
|
50
|
+
}
|
|
51
|
+
catch (e) {
|
|
52
|
+
if (gen === this.#generation)
|
|
53
|
+
this.dispatch({ type: "failed", message: message(e), cause: e });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** Opens the options for the selected (or given) app. */
|
|
57
|
+
choose(id) {
|
|
58
|
+
this.dispatch(id === undefined ? { type: "choose" } : { type: "choose", id });
|
|
59
|
+
}
|
|
60
|
+
/** Opens the native chooser and adds what the user picked. */
|
|
61
|
+
async chooseFiles() {
|
|
62
|
+
if (!this.host.chooseFiles)
|
|
63
|
+
return;
|
|
64
|
+
const files = await this.host.chooseFiles();
|
|
65
|
+
if (files.length)
|
|
66
|
+
this.dispatch({ type: "files", files });
|
|
67
|
+
}
|
|
68
|
+
/** Builds the plan (sizes, installs, consent items). */
|
|
69
|
+
async plan() {
|
|
70
|
+
const s = this.#state;
|
|
71
|
+
if (s.step !== "options" || !canPlan(s) || !s.entry || !s.move)
|
|
72
|
+
return;
|
|
73
|
+
this.dispatch({ type: "plan" });
|
|
74
|
+
try {
|
|
75
|
+
const groups = planSensitive(s);
|
|
76
|
+
const plan = await this.host.plan(s.entry, {
|
|
77
|
+
moves: s.move,
|
|
78
|
+
files: s.files,
|
|
79
|
+
...(groups.length ? { sensitiveGroups: groups } : {}),
|
|
80
|
+
});
|
|
81
|
+
this.dispatch({ type: "planned", plan });
|
|
82
|
+
}
|
|
83
|
+
catch (e) {
|
|
84
|
+
this.dispatch({ type: "failed", message: message(e), cause: e });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/** Runs the approved plan with the user's consent. */
|
|
88
|
+
async confirm() {
|
|
89
|
+
const s = this.#state;
|
|
90
|
+
if (s.step !== "consent" || !canConfirm(s) || !s.plan)
|
|
91
|
+
return;
|
|
92
|
+
const plan = s.plan;
|
|
93
|
+
this.dispatch({ type: "confirm" });
|
|
94
|
+
try {
|
|
95
|
+
const report = await this.host.run(plan, {
|
|
96
|
+
approved: true,
|
|
97
|
+
acknowledgeSensitive: s.acknowledged,
|
|
98
|
+
saveToKeyvault: s.saveToKeyvault,
|
|
99
|
+
acknowledgeRelayPlaintext: s.acknowledgedRelayPlaintext,
|
|
100
|
+
}, (event) => this.dispatch({ type: "progress", event }));
|
|
101
|
+
this.dispatch({ type: "finished", report });
|
|
102
|
+
}
|
|
103
|
+
catch (e) {
|
|
104
|
+
this.dispatch({ type: "failed", message: message(e), cause: e });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drag payloads, read the same way as the Rust core
|
|
3
|
+
* (`cua_teleport::ux::drop`; both run `testdata/drops.json`).
|
|
4
|
+
*
|
|
5
|
+
* Paths and `file://` URIs sort into apps (`.app`, `.desktop`, `.lnk`),
|
|
6
|
+
* files and folders, and URLs. In a plain browser a Finder drop carries no
|
|
7
|
+
* paths, only file names; {@link classifyBrowserDrop} then matches an
|
|
8
|
+
* `X.app` name against the catalog.
|
|
9
|
+
*/
|
|
10
|
+
import type { CatalogEntry } from "./model.js";
|
|
11
|
+
export declare const MAX_DROP_ITEMS = 256;
|
|
12
|
+
export type DropKind = "empty" | "app" | "files" | "url";
|
|
13
|
+
export interface DropPayload {
|
|
14
|
+
kind: DropKind;
|
|
15
|
+
apps: string[];
|
|
16
|
+
files: string[];
|
|
17
|
+
urls: string[];
|
|
18
|
+
ignored: string[];
|
|
19
|
+
}
|
|
20
|
+
export declare function isAppPath(path: string): boolean;
|
|
21
|
+
/** `file:///a%20b/` -> `/a b/`; `null` for another host. */
|
|
22
|
+
export declare function fileUriToPath(uri: string): string | null;
|
|
23
|
+
/** Parses paths, `file://` URIs, URLs or `text/uri-list` blobs. */
|
|
24
|
+
export declare function parseDrop(items: readonly string[]): DropPayload;
|
|
25
|
+
/** What a browser drop carries (a `DataTransfer`, flattened). */
|
|
26
|
+
export interface BrowserDrop {
|
|
27
|
+
/** `text/uri-list`, when present. */
|
|
28
|
+
uriList?: string;
|
|
29
|
+
/** `text/plain`, when present. */
|
|
30
|
+
text?: string;
|
|
31
|
+
/** Dropped file names (no paths in a browser). */
|
|
32
|
+
fileNames?: string[];
|
|
33
|
+
}
|
|
34
|
+
/** Reads a DOM `DataTransfer` into a {@link BrowserDrop}. */
|
|
35
|
+
export declare function readDataTransfer(dt: {
|
|
36
|
+
getData(type: string): string;
|
|
37
|
+
files?: ArrayLike<{
|
|
38
|
+
name: string;
|
|
39
|
+
}>;
|
|
40
|
+
}): BrowserDrop;
|
|
41
|
+
export type BrowserDropResult = {
|
|
42
|
+
kind: "app";
|
|
43
|
+
entry: CatalogEntry | null;
|
|
44
|
+
name: string;
|
|
45
|
+
path?: string;
|
|
46
|
+
} | {
|
|
47
|
+
kind: "files";
|
|
48
|
+
names: string[];
|
|
49
|
+
paths: string[];
|
|
50
|
+
} | {
|
|
51
|
+
kind: "url";
|
|
52
|
+
urls: string[];
|
|
53
|
+
} | {
|
|
54
|
+
kind: "empty";
|
|
55
|
+
};
|
|
56
|
+
/** The catalog row an app file name or bundle path names (`Blender.app`). */
|
|
57
|
+
export declare function entryForAppName(catalog: readonly CatalogEntry[], nameOrPath: string): CatalogEntry | null;
|
|
58
|
+
/**
|
|
59
|
+
* Classifies a browser drop: URIs first (a Dock or Finder drag in Chromium
|
|
60
|
+
* carries `file://` URIs), then file names (`X.app` is an app).
|
|
61
|
+
*/
|
|
62
|
+
export declare function classifyBrowserDrop(drop: BrowserDrop, catalog: readonly CatalogEntry[]): BrowserDropResult;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drag payloads, read the same way as the Rust core
|
|
3
|
+
* (`cua_teleport::ux::drop`; both run `testdata/drops.json`).
|
|
4
|
+
*
|
|
5
|
+
* Paths and `file://` URIs sort into apps (`.app`, `.desktop`, `.lnk`),
|
|
6
|
+
* files and folders, and URLs. In a plain browser a Finder drop carries no
|
|
7
|
+
* paths, only file names; {@link classifyBrowserDrop} then matches an
|
|
8
|
+
* `X.app` name against the catalog.
|
|
9
|
+
*/
|
|
10
|
+
export const MAX_DROP_ITEMS = 256;
|
|
11
|
+
export function isAppPath(path) {
|
|
12
|
+
const l = path.toLowerCase();
|
|
13
|
+
return l.endsWith(".app") || l.endsWith(".desktop") || l.endsWith(".lnk");
|
|
14
|
+
}
|
|
15
|
+
function isAbsolute(p) {
|
|
16
|
+
return p.startsWith("/") || /^[A-Za-z]:[\\/]/.test(p) || p.startsWith("\\\\");
|
|
17
|
+
}
|
|
18
|
+
function trimSeparators(p) {
|
|
19
|
+
const t = p.replace(/[/\\]+$/, "");
|
|
20
|
+
return t === "" ? p : t;
|
|
21
|
+
}
|
|
22
|
+
function percentDecode(s) {
|
|
23
|
+
const bytes = [];
|
|
24
|
+
for (let i = 0; i < s.length;) {
|
|
25
|
+
if (s[i] === "%") {
|
|
26
|
+
const hex = s.slice(i + 1, i + 3);
|
|
27
|
+
if (!/^[0-9a-fA-F]{2}$/.test(hex))
|
|
28
|
+
return null;
|
|
29
|
+
bytes.push(parseInt(hex, 16));
|
|
30
|
+
i += 3;
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
const code = s.codePointAt(i);
|
|
34
|
+
const ch = String.fromCodePoint(code);
|
|
35
|
+
for (const b of new TextEncoder().encode(ch))
|
|
36
|
+
bytes.push(b);
|
|
37
|
+
i += ch.length;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(new Uint8Array(bytes));
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** `file:///a%20b/` -> `/a b/`; `null` for another host. */
|
|
48
|
+
export function fileUriToPath(uri) {
|
|
49
|
+
let rest = uri.slice("file://".length);
|
|
50
|
+
if (rest.startsWith("localhost"))
|
|
51
|
+
rest = rest.slice("localhost".length);
|
|
52
|
+
if (!rest.startsWith("/"))
|
|
53
|
+
return null;
|
|
54
|
+
const decoded = percentDecode(rest);
|
|
55
|
+
if (decoded == null)
|
|
56
|
+
return null;
|
|
57
|
+
if (/^\/[A-Za-z]:/.test(decoded) && decoded.length > 3)
|
|
58
|
+
return decoded.slice(1);
|
|
59
|
+
return decoded;
|
|
60
|
+
}
|
|
61
|
+
/** Parses paths, `file://` URIs, URLs or `text/uri-list` blobs. */
|
|
62
|
+
export function parseDrop(items) {
|
|
63
|
+
const out = { kind: "empty", apps: [], files: [], urls: [], ignored: [] };
|
|
64
|
+
const seen = new Set();
|
|
65
|
+
const lines = items
|
|
66
|
+
.flatMap((i) => i.split(/\r?\n/))
|
|
67
|
+
.map((l) => l.trim())
|
|
68
|
+
.filter((l) => l !== "" && !l.startsWith("#"))
|
|
69
|
+
.slice(0, MAX_DROP_ITEMS);
|
|
70
|
+
for (const line of lines) {
|
|
71
|
+
if (seen.has(line))
|
|
72
|
+
continue;
|
|
73
|
+
seen.add(line);
|
|
74
|
+
const lower = line.toLowerCase();
|
|
75
|
+
if (lower.startsWith("http://") || lower.startsWith("https://")) {
|
|
76
|
+
out.urls.push(line);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
let path = line;
|
|
80
|
+
if (lower.startsWith("file://")) {
|
|
81
|
+
const p = fileUriToPath(line);
|
|
82
|
+
if (p == null) {
|
|
83
|
+
out.ignored.push(line);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
path = p;
|
|
87
|
+
}
|
|
88
|
+
if (!isAbsolute(path)) {
|
|
89
|
+
out.ignored.push(line);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const t = trimSeparators(path);
|
|
93
|
+
if (isAppPath(t))
|
|
94
|
+
out.apps.push(t);
|
|
95
|
+
else
|
|
96
|
+
out.files.push(t);
|
|
97
|
+
}
|
|
98
|
+
out.kind = out.apps.length ? "app" : out.files.length ? "files" : out.urls.length ? "url" : "empty";
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
/** Reads a DOM `DataTransfer` into a {@link BrowserDrop}. */
|
|
102
|
+
export function readDataTransfer(dt) {
|
|
103
|
+
const names = [];
|
|
104
|
+
if (dt.files)
|
|
105
|
+
for (let i = 0; i < dt.files.length; i++)
|
|
106
|
+
names.push(dt.files[i].name);
|
|
107
|
+
return { uriList: dt.getData("text/uri-list") || undefined, text: dt.getData("text/plain") || undefined, fileNames: names };
|
|
108
|
+
}
|
|
109
|
+
/** The catalog row an app file name or bundle path names (`Blender.app`). */
|
|
110
|
+
export function entryForAppName(catalog, nameOrPath) {
|
|
111
|
+
const base = trimSeparators(nameOrPath).split(/[/\\]/).pop() ?? nameOrPath;
|
|
112
|
+
const name = base.replace(/\.(app|desktop|lnk)$/i, "");
|
|
113
|
+
const lower = name.toLowerCase();
|
|
114
|
+
return (catalog.find((e) => e.hostPath != null && trimSeparators(e.hostPath) === trimSeparators(nameOrPath)) ??
|
|
115
|
+
catalog.find((e) => e.name === name) ??
|
|
116
|
+
catalog.find((e) => e.hostAppId?.toLowerCase() === lower || e.id === lower) ??
|
|
117
|
+
null);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Classifies a browser drop: URIs first (a Dock or Finder drag in Chromium
|
|
121
|
+
* carries `file://` URIs), then file names (`X.app` is an app).
|
|
122
|
+
*/
|
|
123
|
+
export function classifyBrowserDrop(drop, catalog) {
|
|
124
|
+
const parsed = parseDrop([drop.uriList ?? "", drop.text ?? ""].filter(Boolean));
|
|
125
|
+
if (parsed.kind === "app") {
|
|
126
|
+
const path = parsed.apps[0];
|
|
127
|
+
return { kind: "app", entry: entryForAppName(catalog, path), name: path, path };
|
|
128
|
+
}
|
|
129
|
+
const appName = (drop.fileNames ?? []).find(isAppPath);
|
|
130
|
+
if (appName)
|
|
131
|
+
return { kind: "app", entry: entryForAppName(catalog, appName), name: appName };
|
|
132
|
+
if (parsed.kind === "files" || (drop.fileNames ?? []).length > 0) {
|
|
133
|
+
return { kind: "files", names: drop.fileNames ?? [], paths: parsed.files };
|
|
134
|
+
}
|
|
135
|
+
if (parsed.kind === "url")
|
|
136
|
+
return { kind: "url", urls: parsed.urls };
|
|
137
|
+
return { kind: "empty" };
|
|
138
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one drop zone for a Space: a dashed well captioned "Drop a file or
|
|
3
|
+
* window", with "Send file…" and "Teleport an app…" below it. Files, app
|
|
4
|
+
* bundles and dragged windows all land on the same zone; the host decides what
|
|
5
|
+
* each becomes (a verified file send, or the "Teleport an app…" picker). The
|
|
6
|
+
* same zone as the Cua Spaces app's.
|
|
7
|
+
*
|
|
8
|
+
* - {@link dropZoneStatusLine}: what the zone says it did, concretely (a
|
|
9
|
+
* file is only "sent" once the Space verified it).
|
|
10
|
+
* - {@link renderTeleportDropZone}: the markup, for any framework.
|
|
11
|
+
* - `<cua-drop-zone>` ({@link defineDropZoneElement}): the web component.
|
|
12
|
+
* Set `.status` and, for drags the DOM does not see (a webview's native
|
|
13
|
+
* file drop, a window drag), `.over`. Fires `cua-drop` (`detail`: the
|
|
14
|
+
* `DataTransfer` of a DOM drop), `cua-send-file` and `cua-teleport-app`.
|
|
15
|
+
* Put `data-teleport-target` on it to make it a window-drag target.
|
|
16
|
+
*/
|
|
17
|
+
export declare const DROP_ZONE_CAPTION = "Drop a file or window";
|
|
18
|
+
export declare const DROP_ZONE_SEND_FILE = "Send file\u2026";
|
|
19
|
+
export declare const DROP_ZONE_TELEPORT_APP = "Teleport an app\u2026";
|
|
20
|
+
/** One file the Space verified. */
|
|
21
|
+
export interface DropZoneFile {
|
|
22
|
+
name: string;
|
|
23
|
+
bytes: number;
|
|
24
|
+
/** Where it landed in the Space. */
|
|
25
|
+
dest: string;
|
|
26
|
+
}
|
|
27
|
+
export type DropZoneStatus = {
|
|
28
|
+
kind: "idle";
|
|
29
|
+
} | {
|
|
30
|
+
kind: "sending";
|
|
31
|
+
label: string;
|
|
32
|
+
} | {
|
|
33
|
+
kind: "sent";
|
|
34
|
+
files: DropZoneFile[];
|
|
35
|
+
} | {
|
|
36
|
+
kind: "failed";
|
|
37
|
+
message: string;
|
|
38
|
+
};
|
|
39
|
+
export declare const idleDropZone: DropZoneStatus;
|
|
40
|
+
/** The status line under the buttons, or null when there is nothing to say. */
|
|
41
|
+
export declare function dropZoneStatusLine(status: DropZoneStatus): string | null;
|
|
42
|
+
/** "Sending report.pdf…" / "Sending 3 files…". */
|
|
43
|
+
export declare function sendingLabel(names: readonly string[]): string;
|
|
44
|
+
/** Whether a client-space point is inside `element`'s box. */
|
|
45
|
+
export declare function isInsideZone(element: {
|
|
46
|
+
getBoundingClientRect(): DOMRect;
|
|
47
|
+
} | null, point: {
|
|
48
|
+
x: number;
|
|
49
|
+
y: number;
|
|
50
|
+
}): boolean;
|
|
51
|
+
export declare const DROP_ZONE_STYLES = "\n:host { --fg: var(--cua-teleport-fg, #16181d); --muted: var(--cua-teleport-muted, #6b7280);\n --accent: var(--cua-teleport-accent, #2563eb); --border: var(--cua-teleport-border, #d1d5db);\n --danger: var(--cua-teleport-danger, #b91c1c); --radius: var(--cua-teleport-radius, 10px);\n display: block; color: var(--fg); font: 13px/1.35 system-ui, -apple-system, sans-serif; }\n@media (prefers-color-scheme: dark) { :host { --fg: var(--cua-teleport-fg, #eceef2); --muted: var(--cua-teleport-muted, #9aa0aa);\n --accent: var(--cua-teleport-accent, #5b8def); --border: var(--cua-teleport-border, #3a3d44); --danger: var(--cua-teleport-danger, #f87171); } }\n.zone { display: flex; flex-direction: column; align-items: center; gap: 6px; padding: 14px 12px 12px; text-align: center;\n border: 1.5px dashed var(--border); border-radius: var(--radius); background: color-mix(in srgb, var(--fg) 4%, transparent); }\n.zone[data-drop-target=true] { border-style: solid; border-color: var(--accent); background: color-mix(in srgb, var(--accent) 10%, transparent); }\n.zone[data-busy=true] { opacity: .72; }\n.caption { margin: 0; color: var(--muted); font-size: 12.5px; }\n.actions { display: flex; flex-wrap: wrap; justify-content: center; gap: 6px; margin-top: 2px; }\nbutton { padding: 4px 12px; font: inherit; font-size: 12.5px; color: var(--fg); cursor: pointer;\n background: color-mix(in srgb, var(--fg) 8%, transparent); border: 1px solid var(--border); border-radius: 999px; }\nbutton:hover:not(:disabled) { background: color-mix(in srgb, var(--fg) 14%, transparent); }\nbutton:disabled { opacity: .5; cursor: default; }\n.status { margin: 2px 0 0; max-width: 40ch; color: var(--muted); font-size: 11.5px; overflow-wrap: anywhere; }\n.status[data-kind=failed] { color: var(--danger); }\n";
|
|
52
|
+
/** The zone's markup. Buttons carry `data-act="send-file"` / `data-act="teleport-app"`. */
|
|
53
|
+
export declare function renderTeleportDropZone(state: {
|
|
54
|
+
over?: boolean;
|
|
55
|
+
status?: DropZoneStatus;
|
|
56
|
+
canSendFile?: boolean;
|
|
57
|
+
}): string;
|
|
58
|
+
/** Registers `<cua-drop-zone>` (idempotent). Nothing is registered on import. */
|
|
59
|
+
export declare function defineDropZoneElement(registry?: CustomElementRegistry): void;
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one drop zone for a Space: a dashed well captioned "Drop a file or
|
|
3
|
+
* window", with "Send file…" and "Teleport an app…" below it. Files, app
|
|
4
|
+
* bundles and dragged windows all land on the same zone; the host decides what
|
|
5
|
+
* each becomes (a verified file send, or the "Teleport an app…" picker). The
|
|
6
|
+
* same zone as the Cua Spaces app's.
|
|
7
|
+
*
|
|
8
|
+
* - {@link dropZoneStatusLine}: what the zone says it did, concretely (a
|
|
9
|
+
* file is only "sent" once the Space verified it).
|
|
10
|
+
* - {@link renderTeleportDropZone}: the markup, for any framework.
|
|
11
|
+
* - `<cua-drop-zone>` ({@link defineDropZoneElement}): the web component.
|
|
12
|
+
* Set `.status` and, for drags the DOM does not see (a webview's native
|
|
13
|
+
* file drop, a window drag), `.over`. Fires `cua-drop` (`detail`: the
|
|
14
|
+
* `DataTransfer` of a DOM drop), `cua-send-file` and `cua-teleport-app`.
|
|
15
|
+
* Put `data-teleport-target` on it to make it a window-drag target.
|
|
16
|
+
*/
|
|
17
|
+
import { formatBytes } from "./model.js";
|
|
18
|
+
export const DROP_ZONE_CAPTION = "Drop a file or window";
|
|
19
|
+
export const DROP_ZONE_SEND_FILE = "Send file…";
|
|
20
|
+
export const DROP_ZONE_TELEPORT_APP = "Teleport an app…";
|
|
21
|
+
export const idleDropZone = Object.freeze({ kind: "idle" });
|
|
22
|
+
/** The status line under the buttons, or null when there is nothing to say. */
|
|
23
|
+
export function dropZoneStatusLine(status) {
|
|
24
|
+
switch (status.kind) {
|
|
25
|
+
case "idle":
|
|
26
|
+
return null;
|
|
27
|
+
case "sending":
|
|
28
|
+
return `Sending ${status.label}…`;
|
|
29
|
+
case "sent": {
|
|
30
|
+
const [first] = status.files;
|
|
31
|
+
if (!first)
|
|
32
|
+
return null;
|
|
33
|
+
if (status.files.length === 1)
|
|
34
|
+
return `${first.name} (${formatBytes(first.bytes)}) verified in ${first.dest}`;
|
|
35
|
+
const total = status.files.reduce((sum, f) => sum + f.bytes, 0);
|
|
36
|
+
return `${status.files.length} files (${formatBytes(total)}) verified`;
|
|
37
|
+
}
|
|
38
|
+
case "failed":
|
|
39
|
+
return status.message;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** "Sending report.pdf…" / "Sending 3 files…". */
|
|
43
|
+
export function sendingLabel(names) {
|
|
44
|
+
if (names.length === 1)
|
|
45
|
+
return names[0].split(/[\\/]/).pop() || names[0];
|
|
46
|
+
return `${names.length} files`;
|
|
47
|
+
}
|
|
48
|
+
/** Whether a client-space point is inside `element`'s box. */
|
|
49
|
+
export function isInsideZone(element, point) {
|
|
50
|
+
if (!element)
|
|
51
|
+
return false;
|
|
52
|
+
const b = element.getBoundingClientRect();
|
|
53
|
+
if (b.width === 0 && b.height === 0)
|
|
54
|
+
return false;
|
|
55
|
+
return point.x >= b.left && point.x <= b.right && point.y >= b.top && point.y <= b.bottom;
|
|
56
|
+
}
|
|
57
|
+
export const DROP_ZONE_STYLES = `
|
|
58
|
+
:host { --fg: var(--cua-teleport-fg, #16181d); --muted: var(--cua-teleport-muted, #6b7280);
|
|
59
|
+
--accent: var(--cua-teleport-accent, #2563eb); --border: var(--cua-teleport-border, #d1d5db);
|
|
60
|
+
--danger: var(--cua-teleport-danger, #b91c1c); --radius: var(--cua-teleport-radius, 10px);
|
|
61
|
+
display: block; color: var(--fg); font: 13px/1.35 system-ui, -apple-system, sans-serif; }
|
|
62
|
+
@media (prefers-color-scheme: dark) { :host { --fg: var(--cua-teleport-fg, #eceef2); --muted: var(--cua-teleport-muted, #9aa0aa);
|
|
63
|
+
--accent: var(--cua-teleport-accent, #5b8def); --border: var(--cua-teleport-border, #3a3d44); --danger: var(--cua-teleport-danger, #f87171); } }
|
|
64
|
+
.zone { display: flex; flex-direction: column; align-items: center; gap: 6px; padding: 14px 12px 12px; text-align: center;
|
|
65
|
+
border: 1.5px dashed var(--border); border-radius: var(--radius); background: color-mix(in srgb, var(--fg) 4%, transparent); }
|
|
66
|
+
.zone[data-drop-target=true] { border-style: solid; border-color: var(--accent); background: color-mix(in srgb, var(--accent) 10%, transparent); }
|
|
67
|
+
.zone[data-busy=true] { opacity: .72; }
|
|
68
|
+
.caption { margin: 0; color: var(--muted); font-size: 12.5px; }
|
|
69
|
+
.actions { display: flex; flex-wrap: wrap; justify-content: center; gap: 6px; margin-top: 2px; }
|
|
70
|
+
button { padding: 4px 12px; font: inherit; font-size: 12.5px; color: var(--fg); cursor: pointer;
|
|
71
|
+
background: color-mix(in srgb, var(--fg) 8%, transparent); border: 1px solid var(--border); border-radius: 999px; }
|
|
72
|
+
button:hover:not(:disabled) { background: color-mix(in srgb, var(--fg) 14%, transparent); }
|
|
73
|
+
button:disabled { opacity: .5; cursor: default; }
|
|
74
|
+
.status { margin: 2px 0 0; max-width: 40ch; color: var(--muted); font-size: 11.5px; overflow-wrap: anywhere; }
|
|
75
|
+
.status[data-kind=failed] { color: var(--danger); }
|
|
76
|
+
`;
|
|
77
|
+
const esc = (s) => String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
78
|
+
/** The zone's markup. Buttons carry `data-act="send-file"` / `data-act="teleport-app"`. */
|
|
79
|
+
export function renderTeleportDropZone(state) {
|
|
80
|
+
const status = state.status ?? idleDropZone;
|
|
81
|
+
const busy = status.kind === "sending";
|
|
82
|
+
const line = dropZoneStatusLine(status);
|
|
83
|
+
const off = busy ? " disabled" : "";
|
|
84
|
+
return `<div class="zone" part="zone"${state.over ? ' data-drop-target="true"' : ""}${busy ? ' data-busy="true"' : ""}>
|
|
85
|
+
<p class="caption">${esc(DROP_ZONE_CAPTION)}</p>
|
|
86
|
+
<div class="actions">${state.canSendFile === false ? "" : `<button type="button" data-act="send-file"${off}>${esc(DROP_ZONE_SEND_FILE)}</button>`}<button type="button" data-act="teleport-app"${off}>${esc(DROP_ZONE_TELEPORT_APP)}</button></div>
|
|
87
|
+
${line ? `<p class="status" role="status" data-kind="${status.kind}">${esc(line)}</p>` : ""}
|
|
88
|
+
</div>`;
|
|
89
|
+
}
|
|
90
|
+
/** Registers `<cua-drop-zone>` (idempotent). Nothing is registered on import. */
|
|
91
|
+
export function defineDropZoneElement(registry = customElements) {
|
|
92
|
+
if (registry.get("cua-drop-zone"))
|
|
93
|
+
return;
|
|
94
|
+
class DropZone extends HTMLElement {
|
|
95
|
+
#over = false;
|
|
96
|
+
#domOver = false;
|
|
97
|
+
#status = idleDropZone;
|
|
98
|
+
set over(v) {
|
|
99
|
+
this.#over = !!v;
|
|
100
|
+
this.#render();
|
|
101
|
+
}
|
|
102
|
+
get over() {
|
|
103
|
+
return this.#over || this.#domOver;
|
|
104
|
+
}
|
|
105
|
+
set status(s) {
|
|
106
|
+
this.#status = s ?? idleDropZone;
|
|
107
|
+
this.#render();
|
|
108
|
+
}
|
|
109
|
+
get status() {
|
|
110
|
+
return this.#status;
|
|
111
|
+
}
|
|
112
|
+
connectedCallback() {
|
|
113
|
+
if (!this.shadowRoot) {
|
|
114
|
+
const root = this.attachShadow({ mode: "open" });
|
|
115
|
+
root.addEventListener("click", this.#onClick);
|
|
116
|
+
this.addEventListener("dragover", this.#onDragOver);
|
|
117
|
+
this.addEventListener("dragleave", this.#onDragLeave);
|
|
118
|
+
this.addEventListener("drop", this.#onDrop);
|
|
119
|
+
}
|
|
120
|
+
this.#render();
|
|
121
|
+
}
|
|
122
|
+
#fire(type, detail) {
|
|
123
|
+
this.dispatchEvent(new CustomEvent(type, { detail, bubbles: true, composed: true }));
|
|
124
|
+
}
|
|
125
|
+
#onClick = (ev) => {
|
|
126
|
+
const act = ev.target.closest("[data-act]")?.dataset.act;
|
|
127
|
+
if (act === "send-file")
|
|
128
|
+
this.#fire("cua-send-file");
|
|
129
|
+
if (act === "teleport-app")
|
|
130
|
+
this.#fire("cua-teleport-app");
|
|
131
|
+
};
|
|
132
|
+
#onDragOver = (ev) => {
|
|
133
|
+
ev.preventDefault();
|
|
134
|
+
if (!this.#domOver) {
|
|
135
|
+
this.#domOver = true;
|
|
136
|
+
this.#render();
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
#onDragLeave = (ev) => {
|
|
140
|
+
const into = ev.relatedTarget;
|
|
141
|
+
if (into && (into === this || this.contains(into) || this.shadowRoot?.contains(into)))
|
|
142
|
+
return;
|
|
143
|
+
this.#domOver = false;
|
|
144
|
+
this.#render();
|
|
145
|
+
};
|
|
146
|
+
#onDrop = (ev) => {
|
|
147
|
+
ev.preventDefault();
|
|
148
|
+
this.#domOver = false;
|
|
149
|
+
this.#render();
|
|
150
|
+
const dt = ev.dataTransfer;
|
|
151
|
+
if (dt)
|
|
152
|
+
this.#fire("cua-drop", dt);
|
|
153
|
+
};
|
|
154
|
+
#render() {
|
|
155
|
+
if (!this.shadowRoot)
|
|
156
|
+
return;
|
|
157
|
+
this.toggleAttribute("data-over", this.over);
|
|
158
|
+
this.shadowRoot.innerHTML = `<style>${DROP_ZONE_STYLES}</style>${renderTeleportDropZone({
|
|
159
|
+
over: this.over,
|
|
160
|
+
status: this.#status,
|
|
161
|
+
canSendFile: !this.hasAttribute("no-send-file"),
|
|
162
|
+
})}`;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
registry.define("cua-drop-zone", DropZone);
|
|
166
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Framework-agnostic web components over the headless picker:
|
|
3
|
+
*
|
|
4
|
+
* - `<cua-teleport-picker>`: "Teleport an app…" (search, icons, recents,
|
|
5
|
+
* capability levels, the move choice, the consent screen listing every
|
|
6
|
+
* path and secret, progress). Set `.host` (a {@link TeleportHost}) and the
|
|
7
|
+
* `space-name` attribute; optional `.preselect` jumps to one app. Fires
|
|
8
|
+
* `cua-teleport-close` and `cua-teleport-done` (`detail`: the report).
|
|
9
|
+
* - `<cua-drop-zone>`: the one dashed zone for files, apps and windows
|
|
10
|
+
* (see `dropZone.ts`).
|
|
11
|
+
* - `<cua-teleport-drop>`: the drop zone shown while a window or app is
|
|
12
|
+
* dragged: the dragged window's preview, the app and its capability, the
|
|
13
|
+
* target Space. Set `.state` (a {@link WindowDropState}) and `space-name`.
|
|
14
|
+
*
|
|
15
|
+
* Call {@link defineTeleportElements} once in a browser or webview; nothing
|
|
16
|
+
* is registered on import, so Node can import this module.
|
|
17
|
+
*
|
|
18
|
+
* Theming: CSS custom properties `--cua-teleport-bg`, `-fg`, `-muted`,
|
|
19
|
+
* `-accent`, `-border`, `-danger`, `-radius`; light and dark defaults follow
|
|
20
|
+
* `prefers-color-scheme`.
|
|
21
|
+
*/
|
|
22
|
+
import { TeleportPickerController } from "./controller.js";
|
|
23
|
+
import type { WindowDropState } from "./windowDrag.js";
|
|
24
|
+
export declare const TELEPORT_STYLES = "\n:host { --bg: var(--cua-teleport-bg, #ffffff); --fg: var(--cua-teleport-fg, #16181d);\n --muted: var(--cua-teleport-muted, #6b7280); --accent: var(--cua-teleport-accent, #2563eb);\n --border: var(--cua-teleport-border, #e5e7eb); --danger: var(--cua-teleport-danger, #b91c1c);\n --radius: var(--cua-teleport-radius, 10px); --row: color-mix(in srgb, var(--fg) 5%, var(--bg));\n display: block; color: var(--fg); background: var(--bg); font: 13px/1.4 system-ui, -apple-system, sans-serif; }\n@media (prefers-color-scheme: dark) { :host { --bg: var(--cua-teleport-bg, #1c1d21); --fg: var(--cua-teleport-fg, #eceef2);\n --muted: var(--cua-teleport-muted, #9aa0aa); --accent: var(--cua-teleport-accent, #5b8def);\n --border: var(--cua-teleport-border, #2e3036); --danger: var(--cua-teleport-danger, #f87171); } }\n.panel { display: flex; flex-direction: column; gap: 12px; padding: 16px; min-height: 100%; box-sizing: border-box; }\nh1 { font-size: 15px; margin: 0; font-weight: 600; }\n.hint { color: var(--muted); margin: 0; }\ninput[type=search] { width: 100%; box-sizing: border-box; padding: 8px 10px; border-radius: 8px; border: 1px solid var(--border);\n background: var(--bg); color: var(--fg); font: inherit; }\n.list { display: flex; flex-direction: column; gap: 2px; overflow: auto; flex: 1; min-height: 120px; }\n.section { color: var(--muted); font-size: 11px; text-transform: uppercase; letter-spacing: .04em; margin: 8px 4px 2px; }\n.row { display: grid; grid-template-columns: 28px 1fr auto; gap: 10px; align-items: center; padding: 6px 8px;\n border-radius: 8px; border: 1px solid transparent; background: none; color: inherit; font: inherit; text-align: left; cursor: pointer; }\n.row[aria-selected=true] { background: var(--row); border-color: var(--accent); }\n.row:disabled { cursor: default; opacity: .55; }\n.icon { width: 28px; height: 28px; border-radius: 7px; object-fit: contain; }\n.mono { width: 28px; height: 28px; border-radius: 7px; display: grid; place-items: center; background: var(--row); font-weight: 600; }\n.name { font-weight: 500; } .sub { color: var(--muted); font-size: 12px; }\n.badge { font-size: 11px; padding: 2px 8px; border-radius: 999px; border: 1px solid var(--border); color: var(--muted); white-space: nowrap; }\n.badge.full { color: var(--accent); border-color: var(--accent); }\n.choices { display: flex; flex-direction: column; gap: 6px; }\n.choice { display: flex; gap: 8px; align-items: flex-start; padding: 8px; border: 1px solid var(--border); border-radius: 8px; }\n.files { display: flex; flex-direction: column; gap: 4px; }\n.file { display: flex; justify-content: space-between; gap: 8px; padding: 4px 8px; background: var(--row); border-radius: 6px; font-family: ui-monospace, monospace; font-size: 12px; }\n.items { display: flex; flex-direction: column; gap: 4px; }\n.item { display: grid; grid-template-columns: auto 1fr auto; gap: 8px; align-items: baseline; padding: 6px 8px; border-radius: 6px; background: var(--row); }\n.item .k { font-size: 11px; color: var(--muted); text-transform: uppercase; }\n.item.secret .k { color: var(--danger); }\n.detail { color: var(--muted); font-size: 12px; word-break: break-all; }\n.warn { color: var(--muted); font-size: 12px; margin: 0; }\n.error { color: var(--danger); }\n.bar { height: 6px; border-radius: 3px; background: var(--row); overflow: hidden; }\n.bar > span { display: block; height: 100%; background: var(--accent); }\nfooter { display: flex; justify-content: flex-end; gap: 8px; margin-top: auto; }\nbutton.btn { padding: 6px 14px; border-radius: 8px; border: 1px solid var(--border); background: var(--bg); color: var(--fg); font: inherit; cursor: pointer; }\na.btn { padding: 6px 14px; border-radius: 8px; border: 1px solid var(--border); font: inherit; text-decoration: none; }\na.primary, button.primary { background: var(--accent); border-color: var(--accent); color: #fff; }\nbutton.btn:disabled { opacity: .5; cursor: default; }\n.drop { display: grid; grid-template-columns: 96px 1fr; gap: 12px; align-items: center; padding: 12px; border-radius: var(--radius);\n border: 2px dashed var(--border); }\n.drop.over { border-color: var(--accent); }\n.drop .thumb { width: 96px; height: 64px; border-radius: 6px; object-fit: cover; background: var(--row); }\n";
|
|
25
|
+
export declare function renderPicker(c: TeleportPickerController, icons: Map<string, string | null>): string;
|
|
26
|
+
export declare function renderDropZone(state: WindowDropState, spaceName: string): string;
|
|
27
|
+
/** Registers `<cua-teleport-picker>`, `<cua-teleport-drop>` and `<cua-drop-zone>` (idempotent). */
|
|
28
|
+
export declare function defineTeleportElements(registry?: CustomElementRegistry): void;
|