bosia 0.4.0 → 0.4.2
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/package.json +5 -2
- package/src/cli/create.ts +4 -1
- package/src/core/config.ts +3 -2
- package/src/core/plugins/inspector/bun-plugin.ts +146 -0
- package/src/core/plugins/inspector/index.ts +122 -0
- package/src/core/plugins/inspector/overlay.ts +116 -0
- package/src/core/types/plugin.ts +1 -1
- package/templates/default/.prettierignore +1 -0
- package/templates/default/_gitignore +12 -0
- package/templates/demo/.prettierignore +1 -0
- package/templates/demo/_gitignore +12 -0
- package/templates/todo/.prettierignore +1 -0
- package/templates/todo/_gitignore +12 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bosia",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A fast, batteries-included fullstack framework — SSR · Svelte 5 Runes · Bun · ElysiaJS. File-based routing inspired by SvelteKit. No Node.js, no Vite, no adapters.",
|
|
6
6
|
"keywords": [
|
|
@@ -33,13 +33,15 @@
|
|
|
33
33
|
"exports": {
|
|
34
34
|
".": "./src/lib/index.ts",
|
|
35
35
|
"./client": "./src/lib/client.ts",
|
|
36
|
-
"./plugins/server-timing": "./src/core/plugins/server-timing.ts"
|
|
36
|
+
"./plugins/server-timing": "./src/core/plugins/server-timing.ts",
|
|
37
|
+
"./plugins/inspector": "./src/core/plugins/inspector/index.ts"
|
|
37
38
|
},
|
|
38
39
|
"bin": {
|
|
39
40
|
"bosia": "src/cli/index.ts"
|
|
40
41
|
},
|
|
41
42
|
"scripts": {
|
|
42
43
|
"check": "tsc --noEmit && prettier --check .",
|
|
44
|
+
"check:templates": "bun scripts/check-templates.ts",
|
|
43
45
|
"test": "bun test",
|
|
44
46
|
"test:watch": "bun test --watch"
|
|
45
47
|
},
|
|
@@ -52,6 +54,7 @@
|
|
|
52
54
|
"@tailwindcss/cli": "^4.2.1",
|
|
53
55
|
"bun-plugin-svelte": "^0.0.6",
|
|
54
56
|
"elysia": "^1.4.26",
|
|
57
|
+
"magic-string": "^0.30.0",
|
|
55
58
|
"svelte": "^5.53.6",
|
|
56
59
|
"tailwind-merge": "^3.5.0",
|
|
57
60
|
"tailwindcss": "^4.2.1"
|
package/src/cli/create.ts
CHANGED
|
@@ -148,7 +148,10 @@ function copyDir(src: string, dest: string, projectName: string, isLocal: boolea
|
|
|
148
148
|
mkdirSync(dest, { recursive: true });
|
|
149
149
|
for (const entry of readdirSync(src, { withFileTypes: true })) {
|
|
150
150
|
const srcPath = join(src, entry.name);
|
|
151
|
-
|
|
151
|
+
// npm pack strips `.gitignore` from published packages, so templates ship
|
|
152
|
+
// it as `_gitignore` and we restore the dotfile name on copy.
|
|
153
|
+
const destName = entry.name === "_gitignore" ? ".gitignore" : entry.name;
|
|
154
|
+
const destPath = join(dest, destName);
|
|
152
155
|
|
|
153
156
|
// Do not copy instructions.txt or template.json to the final project
|
|
154
157
|
if (entry.name === "instructions.txt" || entry.name === "template.json") continue;
|
package/src/core/config.ts
CHANGED
|
@@ -72,7 +72,8 @@ export async function loadBosiaConfig(cwd: string = process.cwd()): Promise<Bosi
|
|
|
72
72
|
);
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
const
|
|
75
|
+
const rawPlugins = Array.isArray(config.plugins) ? config.plugins : [];
|
|
76
|
+
const plugins = rawPlugins.filter((p): p is BosiaPlugin => Boolean(p));
|
|
76
77
|
const normalized: BosiaConfig = { plugins };
|
|
77
78
|
|
|
78
79
|
cached = normalized;
|
|
@@ -89,5 +90,5 @@ export function resetConfigCache(): void {
|
|
|
89
90
|
/** Convenience: load and return only the plugin list. */
|
|
90
91
|
export async function loadPlugins(cwd?: string): Promise<BosiaPlugin[]> {
|
|
91
92
|
const config = await loadBosiaConfig(cwd);
|
|
92
|
-
return config.plugins ?? [];
|
|
93
|
+
return (config.plugins ?? []).filter((p): p is BosiaPlugin => Boolean(p));
|
|
93
94
|
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { parse, compile } from "svelte/compiler";
|
|
2
|
+
import MagicString from "magic-string";
|
|
3
|
+
import { basename, relative } from "node:path";
|
|
4
|
+
import type { BunPlugin } from "bun";
|
|
5
|
+
|
|
6
|
+
const VIRTUAL_NS = "bosia-inspector-css";
|
|
7
|
+
|
|
8
|
+
type AnyNode = {
|
|
9
|
+
type?: string;
|
|
10
|
+
name?: string;
|
|
11
|
+
start?: number;
|
|
12
|
+
end?: number;
|
|
13
|
+
[k: string]: unknown;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// Child-bearing keys across Svelte 5 modern AST nodes. Order doesn't matter.
|
|
17
|
+
const CHILD_KEYS = [
|
|
18
|
+
"nodes", // Fragment
|
|
19
|
+
"fragment", // RegularElement, KeyBlock, SvelteElement, SvelteComponent
|
|
20
|
+
"consequent", // IfBlock (Fragment)
|
|
21
|
+
"alternate", // IfBlock (Fragment | null)
|
|
22
|
+
"body", // EachBlock, SnippetBlock (Fragment)
|
|
23
|
+
"fallback", // EachBlock (Fragment | null)
|
|
24
|
+
"pending", // AwaitBlock (Fragment | null)
|
|
25
|
+
"then", // AwaitBlock (Fragment | null)
|
|
26
|
+
"catch", // AwaitBlock (Fragment | null)
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
function walk(node: unknown, visit: (n: AnyNode) => void) {
|
|
30
|
+
if (!node) return;
|
|
31
|
+
if (Array.isArray(node)) {
|
|
32
|
+
for (const c of node) walk(c, visit);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (typeof node !== "object") return;
|
|
36
|
+
const n = node as AnyNode;
|
|
37
|
+
if (typeof n.type === "string") visit(n);
|
|
38
|
+
for (const key of CHILD_KEYS) {
|
|
39
|
+
const child = n[key];
|
|
40
|
+
if (child) walk(child, visit);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function lineColFromOffset(source: string, offset: number): { line: number; col: number } {
|
|
45
|
+
let line = 1;
|
|
46
|
+
let col = 1;
|
|
47
|
+
for (let i = 0; i < offset && i < source.length; i++) {
|
|
48
|
+
if (source[i] === "\n") {
|
|
49
|
+
line++;
|
|
50
|
+
col = 1;
|
|
51
|
+
} else {
|
|
52
|
+
col++;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return { line, col };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function injectLocs(source: string, relPath: string): string {
|
|
59
|
+
let ast: { fragment?: AnyNode };
|
|
60
|
+
try {
|
|
61
|
+
ast = parse(source, { modern: true }) as unknown as { fragment?: AnyNode };
|
|
62
|
+
} catch {
|
|
63
|
+
return source;
|
|
64
|
+
}
|
|
65
|
+
if (!ast.fragment) return source;
|
|
66
|
+
|
|
67
|
+
const ms = new MagicString(source);
|
|
68
|
+
walk(ast.fragment, (node) => {
|
|
69
|
+
if (node.type !== "RegularElement") return;
|
|
70
|
+
const name = node.name ?? "";
|
|
71
|
+
if (!name) return;
|
|
72
|
+
if (name === "script" || name === "style") return;
|
|
73
|
+
if (/^[A-Z]/.test(name)) return;
|
|
74
|
+
if (name.includes(":")) return;
|
|
75
|
+
if (typeof node.start !== "number") return;
|
|
76
|
+
const insertAt = node.start + 1 + name.length;
|
|
77
|
+
const { line, col } = lineColFromOffset(source, node.start);
|
|
78
|
+
const safe = relPath.replace(/"/g, """);
|
|
79
|
+
ms.appendLeft(insertAt, ` data-bosia-loc="${safe}:${line}:${col}"`);
|
|
80
|
+
});
|
|
81
|
+
return ms.toString();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface InspectorBunPluginOptions {
|
|
85
|
+
cwd: string;
|
|
86
|
+
target: "browser" | "bun";
|
|
87
|
+
dev: boolean;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const fnv = (s: string): string => {
|
|
91
|
+
let h = 2166136261;
|
|
92
|
+
for (let i = 0; i < s.length; i++) {
|
|
93
|
+
h ^= s.charCodeAt(i);
|
|
94
|
+
h = Math.imul(h, 16777619);
|
|
95
|
+
}
|
|
96
|
+
return (h >>> 0).toString(36);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export function createInspectorBunPlugin(opts: InspectorBunPluginOptions): BunPlugin {
|
|
100
|
+
const { cwd, target, dev } = opts;
|
|
101
|
+
const generate: "client" | "server" = target === "browser" ? "client" : "server";
|
|
102
|
+
const virtualCss = new Map<string, string>();
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
name: "bosia-inspector",
|
|
106
|
+
setup(build) {
|
|
107
|
+
build.onLoad({ filter: /\.svelte$/ }, async (args) => {
|
|
108
|
+
const source = await Bun.file(args.path).text();
|
|
109
|
+
const rel = relative(cwd, args.path);
|
|
110
|
+
const transformed = injectLocs(source, rel);
|
|
111
|
+
|
|
112
|
+
const result = compile(transformed, {
|
|
113
|
+
filename: args.path,
|
|
114
|
+
generate,
|
|
115
|
+
dev,
|
|
116
|
+
hmr: dev,
|
|
117
|
+
css: "external",
|
|
118
|
+
preserveWhitespace: dev,
|
|
119
|
+
preserveComments: dev,
|
|
120
|
+
cssHash: ({ css }) => `svelte-${fnv(css)}`,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
let js = result.js.code;
|
|
124
|
+
if (result.css?.code && generate !== "server") {
|
|
125
|
+
const uid = `${basename(args.path)}-${fnv(args.path)}-style.css`;
|
|
126
|
+
const virtualName = `${VIRTUAL_NS}:${uid}`;
|
|
127
|
+
virtualCss.set(virtualName, result.css.code);
|
|
128
|
+
js += `\nimport ${JSON.stringify(virtualName)};`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return { contents: js, loader: "ts" };
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
build.onResolve({ filter: new RegExp(`^${VIRTUAL_NS}:`) }, (args) => ({
|
|
135
|
+
path: args.path,
|
|
136
|
+
namespace: VIRTUAL_NS,
|
|
137
|
+
}));
|
|
138
|
+
|
|
139
|
+
build.onLoad({ filter: /.*/, namespace: VIRTUAL_NS }, (args) => {
|
|
140
|
+
const css = virtualCss.get(args.path) ?? "";
|
|
141
|
+
virtualCss.delete(args.path);
|
|
142
|
+
return { contents: css, loader: "css" };
|
|
143
|
+
});
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { createInspectorBunPlugin } from "./bun-plugin.ts";
|
|
3
|
+
import { getOverlayScript } from "./overlay.ts";
|
|
4
|
+
import type { BosiaPlugin } from "../../types/plugin.ts";
|
|
5
|
+
|
|
6
|
+
export interface InspectorOptions {
|
|
7
|
+
/** Editor CLI command. Defaults to `code`. */
|
|
8
|
+
editor?: "code" | "cursor" | "zed" | (string & {});
|
|
9
|
+
/** When set, alt+click opens a comment form whose contents POST here. */
|
|
10
|
+
aiEndpoint?: string;
|
|
11
|
+
/** Endpoint path the overlay POSTs to. Defaults to `/__bosia/locate`. */
|
|
12
|
+
endpoint?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function buildEditorArgs(editor: string, file: string, line: number, col: number): string[] {
|
|
16
|
+
if (editor === "zed") return [`${file}:${line}:${col}`];
|
|
17
|
+
return ["-g", `${file}:${line}:${col}`];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Inspector plugin — alt+click an element in the running dev page to jump
|
|
22
|
+
* to its source in your editor, or open a comment form that hands off to an
|
|
23
|
+
* AI agent. Dev-only: production builds inject nothing and mount no endpoint.
|
|
24
|
+
*/
|
|
25
|
+
export function inspector(options: InspectorOptions = {}): BosiaPlugin | false {
|
|
26
|
+
if (process.env.NODE_ENV === "production") return false;
|
|
27
|
+
const editor = options.editor ?? "code";
|
|
28
|
+
const endpoint = options.endpoint ?? "/__bosia/locate";
|
|
29
|
+
const aiEndpoint = options.aiEndpoint;
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
name: "inspector",
|
|
33
|
+
|
|
34
|
+
build: {
|
|
35
|
+
bunPlugins: (target) => [
|
|
36
|
+
createInspectorBunPlugin({ cwd: process.cwd(), target, dev: true }),
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
backend: {
|
|
41
|
+
before(app) {
|
|
42
|
+
return app.post(endpoint, async ({ body }: { body: unknown }) => {
|
|
43
|
+
const data = (body ?? {}) as {
|
|
44
|
+
file?: string;
|
|
45
|
+
line?: number;
|
|
46
|
+
col?: number;
|
|
47
|
+
comment?: string;
|
|
48
|
+
};
|
|
49
|
+
const file = typeof data.file === "string" ? data.file : null;
|
|
50
|
+
const line = Number.isFinite(data.line) ? Number(data.line) : null;
|
|
51
|
+
const col = Number.isFinite(data.col) ? Number(data.col) : 1;
|
|
52
|
+
if (!file || line === null) {
|
|
53
|
+
return new Response(
|
|
54
|
+
JSON.stringify({ ok: false, error: "missing file/line" }),
|
|
55
|
+
{
|
|
56
|
+
status: 400,
|
|
57
|
+
headers: { "content-type": "application/json" },
|
|
58
|
+
},
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const comment = typeof data.comment === "string" ? data.comment.trim() : "";
|
|
63
|
+
if (comment && aiEndpoint) {
|
|
64
|
+
try {
|
|
65
|
+
let origin: string;
|
|
66
|
+
try {
|
|
67
|
+
origin = new URL(aiEndpoint).origin;
|
|
68
|
+
} catch {
|
|
69
|
+
origin = "http://localhost";
|
|
70
|
+
}
|
|
71
|
+
await fetch(aiEndpoint, {
|
|
72
|
+
method: "POST",
|
|
73
|
+
headers: {
|
|
74
|
+
"content-type": "application/json",
|
|
75
|
+
origin,
|
|
76
|
+
},
|
|
77
|
+
body: JSON.stringify({ file, line, col, comment }),
|
|
78
|
+
});
|
|
79
|
+
return { ok: true, mode: "ai" as const };
|
|
80
|
+
} catch (err) {
|
|
81
|
+
console.error("[inspector] aiEndpoint POST failed:", err);
|
|
82
|
+
return new Response(
|
|
83
|
+
JSON.stringify({ ok: false, error: "ai endpoint failed" }),
|
|
84
|
+
{
|
|
85
|
+
status: 502,
|
|
86
|
+
headers: { "content-type": "application/json" },
|
|
87
|
+
},
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const proc = spawn(editor, buildEditorArgs(editor, file, line, col), {
|
|
94
|
+
detached: true,
|
|
95
|
+
stdio: "ignore",
|
|
96
|
+
});
|
|
97
|
+
proc.unref();
|
|
98
|
+
proc.on("error", (err) => {
|
|
99
|
+
console.error(`[inspector] failed to launch "${editor}":`, err);
|
|
100
|
+
});
|
|
101
|
+
} catch (err) {
|
|
102
|
+
console.error(`[inspector] failed to launch "${editor}":`, err);
|
|
103
|
+
return new Response(
|
|
104
|
+
JSON.stringify({ ok: false, error: "editor launch failed" }),
|
|
105
|
+
{
|
|
106
|
+
status: 500,
|
|
107
|
+
headers: { "content-type": "application/json" },
|
|
108
|
+
},
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
return { ok: true, mode: "editor" as const };
|
|
112
|
+
});
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
render: {
|
|
117
|
+
bodyEnd: () => getOverlayScript({ aiEndpoint, endpoint }),
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export default inspector;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { safeJsonStringify } from "../../html.ts";
|
|
2
|
+
|
|
3
|
+
export interface OverlayConfig {
|
|
4
|
+
aiEndpoint?: string;
|
|
5
|
+
endpoint: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function getOverlayScript(config: OverlayConfig): string {
|
|
9
|
+
const cfg = safeJsonStringify(config);
|
|
10
|
+
return (
|
|
11
|
+
`<script>window.__BOSIA_INSPECTOR__=${cfg};</script>\n` + `<script>${OVERLAY_IIFE}</script>`
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const OVERLAY_IIFE = `(function(){
|
|
16
|
+
var CFG=window.__BOSIA_INSPECTOR__||{};
|
|
17
|
+
var EP=CFG.endpoint||"/__bosia/locate";
|
|
18
|
+
var AI=CFG.aiEndpoint||null;
|
|
19
|
+
var altDown=false,outline=null,tip=null,form=null;
|
|
20
|
+
|
|
21
|
+
function ensureOutline(){
|
|
22
|
+
if(outline)return;
|
|
23
|
+
outline=document.createElement("div");
|
|
24
|
+
outline.style.cssText="position:fixed;pointer-events:none;border:2px solid #f73b27;background:rgba(247,59,39,.08);z-index:2147483646;border-radius:2px;transition:all .05s linear;display:none";
|
|
25
|
+
document.body.appendChild(outline);
|
|
26
|
+
tip=document.createElement("div");
|
|
27
|
+
tip.style.cssText="position:fixed;pointer-events:none;background:#111;color:#fff;font:11px/1.4 ui-monospace,monospace;padding:3px 6px;border-radius:3px;z-index:2147483647;display:none;white-space:nowrap";
|
|
28
|
+
document.body.appendChild(tip);
|
|
29
|
+
}
|
|
30
|
+
function hideOutline(){if(outline)outline.style.display="none";if(tip)tip.style.display="none"}
|
|
31
|
+
function showOutline(el,loc){
|
|
32
|
+
ensureOutline();
|
|
33
|
+
var r=el.getBoundingClientRect();
|
|
34
|
+
outline.style.display="block";
|
|
35
|
+
outline.style.left=r.left+"px";outline.style.top=r.top+"px";
|
|
36
|
+
outline.style.width=r.width+"px";outline.style.height=r.height+"px";
|
|
37
|
+
tip.style.display="block";tip.textContent=loc;
|
|
38
|
+
var ty=r.top-22;if(ty<0)ty=r.bottom+4;
|
|
39
|
+
tip.style.left=r.left+"px";tip.style.top=ty+"px";
|
|
40
|
+
}
|
|
41
|
+
function parseLoc(s){var m=/^(.+):(\\d+):(\\d+)$/.exec(s);if(!m)return null;return{file:m[1],line:+m[2],col:+m[3]}}
|
|
42
|
+
function findTarget(e){var n=e.target;while(n&&n.nodeType===1){if(n.hasAttribute&&n.hasAttribute("data-bosia-loc"))return n;n=n.parentNode}return null}
|
|
43
|
+
|
|
44
|
+
function toast(msg,err){
|
|
45
|
+
var t=document.createElement("div");
|
|
46
|
+
t.textContent=msg;
|
|
47
|
+
t.style.cssText="position:fixed;bottom:20px;left:50%;transform:translateX(-50%);background:"+(err?"#dc2626":"#111")+";color:#fff;padding:8px 14px;border-radius:6px;font:13px ui-sans-serif,system-ui,sans-serif;z-index:2147483647;box-shadow:0 4px 12px rgba(0,0,0,.2);opacity:0;transition:opacity .15s";
|
|
48
|
+
document.body.appendChild(t);
|
|
49
|
+
requestAnimationFrame(function(){t.style.opacity="1"});
|
|
50
|
+
setTimeout(function(){t.style.opacity="0";setTimeout(function(){t.remove()},200)},2200);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function send(payload,onOk){
|
|
54
|
+
fetch(EP,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify(payload)})
|
|
55
|
+
.then(function(r){return r.json().catch(function(){return{}})})
|
|
56
|
+
.then(function(j){if(j&&j.ok){onOk&&onOk(j)}else{toast("Inspector: request failed",true)}})
|
|
57
|
+
.catch(function(){toast("Inspector: network error",true)});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function closeForm(){if(form){form.remove();form=null}}
|
|
61
|
+
function openForm(loc,el){
|
|
62
|
+
closeForm();
|
|
63
|
+
var r=el.getBoundingClientRect();
|
|
64
|
+
form=document.createElement("div");
|
|
65
|
+
form.style.cssText="position:fixed;left:"+r.left+"px;top:"+(r.bottom+6)+"px;background:#fff;color:#111;border:1px solid #d4d4d8;border-radius:8px;box-shadow:0 8px 24px rgba(0,0,0,.18);padding:10px;width:340px;z-index:2147483647;font:13px ui-sans-serif,system-ui,sans-serif";
|
|
66
|
+
form.innerHTML='<div style="font-size:11px;color:#71717a;margin-bottom:6px;font-family:ui-monospace,monospace">'+loc.file+":"+loc.line+'</div>'+
|
|
67
|
+
'<textarea placeholder="Describe a fix (Enter to send, Esc to cancel, empty = open in editor)" style="width:100%;min-height:64px;border:1px solid #e4e4e7;border-radius:4px;padding:6px;font:13px ui-sans-serif,system-ui,sans-serif;resize:vertical;box-sizing:border-box;outline:none"></textarea>'+
|
|
68
|
+
'<div style="margin-top:8px;display:flex;gap:6px;justify-content:flex-end">'+
|
|
69
|
+
'<button data-cancel style="padding:4px 10px;border:1px solid #e4e4e7;background:#fff;border-radius:4px;cursor:pointer;font-size:12px">Cancel</button>'+
|
|
70
|
+
'<button data-send style="padding:4px 10px;border:0;background:#111;color:#fff;border-radius:4px;cursor:pointer;font-size:12px">Send</button>'+
|
|
71
|
+
'</div>';
|
|
72
|
+
document.body.appendChild(form);
|
|
73
|
+
var ta=form.querySelector("textarea");
|
|
74
|
+
ta.focus();
|
|
75
|
+
function submit(){
|
|
76
|
+
var comment=ta.value.trim();
|
|
77
|
+
var payload={file:loc.file,line:loc.line,col:loc.col};
|
|
78
|
+
if(comment)payload.comment=comment;
|
|
79
|
+
send(payload,function(j){toast(j.mode==="ai"?"sent to AI":"opened "+loc.file+":"+loc.line)});
|
|
80
|
+
closeForm();
|
|
81
|
+
}
|
|
82
|
+
form.querySelector("[data-send]").addEventListener("click",submit);
|
|
83
|
+
form.querySelector("[data-cancel]").addEventListener("click",closeForm);
|
|
84
|
+
ta.addEventListener("keydown",function(e){
|
|
85
|
+
if(e.key==="Escape"){e.preventDefault();closeForm()}
|
|
86
|
+
else if(e.key==="Enter"&&!e.shiftKey){e.preventDefault();submit()}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
window.addEventListener("keydown",function(e){
|
|
91
|
+
if(e.key==="Alt"||e.altKey)altDown=true;
|
|
92
|
+
if(e.key==="Escape")closeForm();
|
|
93
|
+
},true);
|
|
94
|
+
window.addEventListener("keyup",function(e){if(e.key==="Alt"){altDown=false;hideOutline()}},true);
|
|
95
|
+
window.addEventListener("blur",function(){altDown=false;hideOutline()});
|
|
96
|
+
|
|
97
|
+
window.addEventListener("mousemove",function(e){
|
|
98
|
+
if(!altDown||form){hideOutline();return}
|
|
99
|
+
var el=findTarget(e);
|
|
100
|
+
if(!el){hideOutline();return}
|
|
101
|
+
showOutline(el,el.getAttribute("data-bosia-loc"));
|
|
102
|
+
},true);
|
|
103
|
+
|
|
104
|
+
window.addEventListener("click",function(e){
|
|
105
|
+
if(!altDown)return;
|
|
106
|
+
if(form&&form.contains(e.target))return;
|
|
107
|
+
var el=findTarget(e);
|
|
108
|
+
if(!el)return;
|
|
109
|
+
e.preventDefault();e.stopPropagation();
|
|
110
|
+
var loc=parseLoc(el.getAttribute("data-bosia-loc"));
|
|
111
|
+
if(!loc)return;
|
|
112
|
+
hideOutline();
|
|
113
|
+
if(AI)openForm(loc,el);
|
|
114
|
+
else send(loc,function(){toast("opened "+loc.file+":"+loc.line)});
|
|
115
|
+
},true);
|
|
116
|
+
})();`;
|
package/src/core/types/plugin.ts
CHANGED