api_binding_ui 3.0.0 → 3.1.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 +15 -1
- package/SKILL.md +1 -1
- package/dist/app/{index-0n23yy5r.js → index-4mgcb4wd.js} +138 -138
- package/dist/app/index.html +1 -1
- package/dist/cli.js +136 -99
- package/dist/overlay.js +24 -0
- package/dist/server/handler.d.ts +18 -0
- package/dist/studio/BindingStudio.d.ts +3 -1
- package/dist/studio/index.js +7 -5
- package/dist/studio/index.js.map +3 -3
- package/dist/vite/plugin.d.ts +36 -0
- package/dist/vite.js +201 -0
- package/package.json +8 -4
- package/spec/FRAMEWORK.md +2 -1
package/dist/app/index.html
CHANGED
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
<html lang="en">
|
|
3
3
|
<head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>Bind API</title>
|
|
4
4
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@500;600&display=swap" />
|
|
5
|
-
<style>html, body, #root { height: 100%; margin: 0; overflow: hidden; background: #f8fafc; }</style><link rel="stylesheet" crossorigin href="./index-aaxy714b.css"><script type="module" crossorigin src="./index-
|
|
5
|
+
<style>html, body, #root { height: 100%; margin: 0; overflow: hidden; background: #f8fafc; }</style><link rel="stylesheet" crossorigin href="./index-aaxy714b.css"><script type="module" crossorigin src="./index-4mgcb4wd.js"></script></head>
|
|
6
6
|
<body><div id="root"></div></body>
|
|
7
7
|
</html>
|
package/dist/cli.js
CHANGED
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli/cli.ts
|
|
4
4
|
import { createServer } from "node:http";
|
|
5
|
-
import { promises as
|
|
6
|
-
import
|
|
7
|
-
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { promises as fs3 } from "node:fs";
|
|
6
|
+
import path3 from "node:path";
|
|
8
7
|
|
|
9
8
|
// src/framework/names.ts
|
|
10
9
|
function humanize(id) {
|
|
@@ -763,16 +762,123 @@ ${apiType.join(`
|
|
|
763
762
|
return { files, issues };
|
|
764
763
|
}
|
|
765
764
|
|
|
766
|
-
// src/
|
|
765
|
+
// src/server/handler.ts
|
|
767
766
|
import { promises as fs } from "node:fs";
|
|
768
767
|
import path from "node:path";
|
|
768
|
+
import { fileURLToPath } from "node:url";
|
|
769
|
+
var TYPES = { ".html": "text/html; charset=utf-8", ".js": "text/javascript", ".css": "text/css", ".svg": "image/svg+xml", ".map": "application/json", ".woff2": "font/woff2" };
|
|
770
|
+
var DIST = fileURLToPath(new URL(".", import.meta.url));
|
|
771
|
+
var APP_DIR = path.join(DIST, "app");
|
|
772
|
+
var OVERLAY_FILE = path.join(DIST, "overlay.js");
|
|
773
|
+
var isUrl = (s) => /^https?:\/\//i.test(s);
|
|
774
|
+
async function readDocs(src, base) {
|
|
775
|
+
if (isUrl(src)) {
|
|
776
|
+
const res = await fetch(src);
|
|
777
|
+
if (!res.ok)
|
|
778
|
+
throw new Error(`${src} returned HTTP ${res.status}.`);
|
|
779
|
+
return res.text();
|
|
780
|
+
}
|
|
781
|
+
if (!/\.(ya?ml|json)$/i.test(src))
|
|
782
|
+
throw new Error(`"${src}" is not a .yaml, .yml or .json file.`);
|
|
783
|
+
return fs.readFile(path.resolve(base, src), "utf8");
|
|
784
|
+
}
|
|
785
|
+
async function writeFileAtomic(file, text) {
|
|
786
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
787
|
+
await fs.writeFile(tmp, text, "utf8");
|
|
788
|
+
await fs.rename(tmp, file);
|
|
789
|
+
}
|
|
790
|
+
async function readBody(req, limit = 20 * 1024 * 1024) {
|
|
791
|
+
const chunks = [];
|
|
792
|
+
let size = 0;
|
|
793
|
+
for await (const c of req) {
|
|
794
|
+
size += c.length;
|
|
795
|
+
if (size > limit)
|
|
796
|
+
throw new Error("Body too large.");
|
|
797
|
+
chunks.push(c);
|
|
798
|
+
}
|
|
799
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
800
|
+
}
|
|
801
|
+
function send(res, status, body, type = "text/plain; charset=utf-8") {
|
|
802
|
+
res.writeHead(status, { "Content-Type": type, "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff" }).end(body);
|
|
803
|
+
}
|
|
804
|
+
function createStudioHandler({ file, prefix, allowed }) {
|
|
805
|
+
const base = path.dirname(file);
|
|
806
|
+
return async (req, res) => {
|
|
807
|
+
const url = new URL(req.url ?? "/", "http://local");
|
|
808
|
+
if (prefix && url.pathname !== prefix && !url.pathname.startsWith(prefix + "/"))
|
|
809
|
+
return false;
|
|
810
|
+
const rel = url.pathname.slice(prefix.length) || "/";
|
|
811
|
+
try {
|
|
812
|
+
if (prefix && url.pathname === prefix) {
|
|
813
|
+
res.writeHead(302, { Location: prefix + "/" + url.search }).end();
|
|
814
|
+
return true;
|
|
815
|
+
}
|
|
816
|
+
if (rel.startsWith("/api/") && !allowed(req)) {
|
|
817
|
+
send(res, 403, "Forbidden.");
|
|
818
|
+
return true;
|
|
819
|
+
}
|
|
820
|
+
if (rel === "/api/bindings") {
|
|
821
|
+
if (req.method === "GET")
|
|
822
|
+
send(res, 200, await fs.readFile(file, "utf8"), "application/json");
|
|
823
|
+
else if (req.method === "PUT") {
|
|
824
|
+
const text = await readBody(req);
|
|
825
|
+
try {
|
|
826
|
+
parseBindingsFile(text);
|
|
827
|
+
} catch (e) {
|
|
828
|
+
send(res, 400, e.message);
|
|
829
|
+
return true;
|
|
830
|
+
}
|
|
831
|
+
await writeFileAtomic(file, text);
|
|
832
|
+
send(res, 204, "");
|
|
833
|
+
} else
|
|
834
|
+
send(res, 405, "Method not allowed.");
|
|
835
|
+
return true;
|
|
836
|
+
}
|
|
837
|
+
if (rel === "/api/openapi" && req.method === "GET") {
|
|
838
|
+
const src = url.searchParams.get("src") ?? "";
|
|
839
|
+
try {
|
|
840
|
+
send(res, 200, await readDocs(src, base), src.endsWith(".json") ? "application/json" : "text/yaml; charset=utf-8");
|
|
841
|
+
} catch (e) {
|
|
842
|
+
send(res, 502, e.message);
|
|
843
|
+
}
|
|
844
|
+
return true;
|
|
845
|
+
}
|
|
846
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
847
|
+
send(res, 405, "Method not allowed.");
|
|
848
|
+
return true;
|
|
849
|
+
}
|
|
850
|
+
if (rel === "/overlay.js") {
|
|
851
|
+
send(res, 200, await fs.readFile(OVERLAY_FILE), TYPES[".js"]);
|
|
852
|
+
return true;
|
|
853
|
+
}
|
|
854
|
+
const target = path.resolve(APP_DIR, rel === "/" ? "index.html" : decodeURIComponent(rel.slice(1)));
|
|
855
|
+
if (!target.startsWith(APP_DIR + path.sep)) {
|
|
856
|
+
send(res, 404, "Not found.");
|
|
857
|
+
return true;
|
|
858
|
+
}
|
|
859
|
+
const data = await fs.readFile(target).catch(() => null);
|
|
860
|
+
if (!data) {
|
|
861
|
+
send(res, 404, "Not found.");
|
|
862
|
+
return true;
|
|
863
|
+
}
|
|
864
|
+
send(res, 200, data, TYPES[path.extname(target)] ?? "application/octet-stream");
|
|
865
|
+
} catch (e) {
|
|
866
|
+
send(res, 500, e.message);
|
|
867
|
+
}
|
|
868
|
+
return true;
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// src/cli/scan.ts
|
|
873
|
+
import { promises as fs2 } from "node:fs";
|
|
874
|
+
import path2 from "node:path";
|
|
769
875
|
var LITERAL = /componentId\s*(?:=\s*\{?\s*|:\s*)(["'`])([a-z0-9.-]+)\1/g;
|
|
770
876
|
var SKIP = new Set(["node_modules", "dist", "build", ".git", ".next", ".output"]);
|
|
771
877
|
async function* sources(dir) {
|
|
772
|
-
for (const e of await
|
|
878
|
+
for (const e of await fs2.readdir(dir, { withFileTypes: true })) {
|
|
773
879
|
if (SKIP.has(e.name))
|
|
774
880
|
continue;
|
|
775
|
-
const p =
|
|
881
|
+
const p = path2.join(dir, e.name);
|
|
776
882
|
if (e.isDirectory())
|
|
777
883
|
yield* sources(p);
|
|
778
884
|
else if (/\.(tsx?|jsx?)$/.test(e.name))
|
|
@@ -782,9 +888,9 @@ async function* sources(dir) {
|
|
|
782
888
|
async function scanIds(dir, file) {
|
|
783
889
|
const found = new Map;
|
|
784
890
|
for await (const p of sources(dir))
|
|
785
|
-
for (const m of (await
|
|
891
|
+
for (const m of (await fs2.readFile(p, "utf8")).matchAll(LITERAL))
|
|
786
892
|
if (!found.has(m[2]))
|
|
787
|
-
found.set(m[2],
|
|
893
|
+
found.set(m[2], path2.relative(dir, p));
|
|
788
894
|
const registered = new Set(file.components.map((c) => c.id));
|
|
789
895
|
const issues = [];
|
|
790
896
|
for (const [id, where] of found)
|
|
@@ -802,6 +908,7 @@ var HELP = `Usage:
|
|
|
802
908
|
api-binding generate [bindings.json] [--check] [--src <dir>]
|
|
803
909
|
|
|
804
910
|
studio Opens the binding studio on http://localhost:<port>. Edits are saved to the file.
|
|
911
|
+
Inside a Vite app, the dev plugin (api_binding_ui/vite) serves the same studio instead.
|
|
805
912
|
generate Writes one React Query hook per screen into output.dir, calling the Orval client.
|
|
806
913
|
--check writes nothing; exits 1 when the files are out of date (for CI).
|
|
807
914
|
--src also checks that every registered id appears in the app's source, and the reverse.`;
|
|
@@ -819,34 +926,17 @@ function args(argv) {
|
|
|
819
926
|
}
|
|
820
927
|
return { rest, flags };
|
|
821
928
|
}
|
|
822
|
-
var isUrl = (s) => /^https?:\/\//i.test(s);
|
|
823
|
-
async function readDocs(src, base) {
|
|
824
|
-
if (isUrl(src)) {
|
|
825
|
-
const res = await fetch(src);
|
|
826
|
-
if (!res.ok)
|
|
827
|
-
throw new Error(`${src} returned HTTP ${res.status}.`);
|
|
828
|
-
return res.text();
|
|
829
|
-
}
|
|
830
|
-
if (!/\.(ya?ml|json)$/i.test(src))
|
|
831
|
-
throw new Error(`"${src}" is not a .yaml, .yml or .json file.`);
|
|
832
|
-
return fs2.readFile(path2.resolve(base, src), "utf8");
|
|
833
|
-
}
|
|
834
929
|
function report(issues) {
|
|
835
930
|
for (const i of issues)
|
|
836
931
|
console.log(`${i.severity === "error" ? "error " : "warning"} ${[i.component ?? i.screen, i.slot].filter(Boolean).join(" · ")}: ${i.message}`);
|
|
837
932
|
}
|
|
838
|
-
async function writeFile(file, text) {
|
|
839
|
-
const tmp = `${file}.${process.pid}.tmp`;
|
|
840
|
-
await fs2.writeFile(tmp, text, "utf8");
|
|
841
|
-
await fs2.rename(tmp, file);
|
|
842
|
-
}
|
|
843
933
|
async function runGenerate(filePath, flags) {
|
|
844
|
-
const base =
|
|
845
|
-
const file = parseBindingsFile(await
|
|
934
|
+
const base = path3.dirname(filePath);
|
|
935
|
+
const file = parseBindingsFile(await fs3.readFile(filePath, "utf8"));
|
|
846
936
|
const catalog = catalogFromText(await readDocs(file.openapi, base), file.openapi);
|
|
847
937
|
const { files, issues } = generate(file, catalog);
|
|
848
938
|
if (typeof flags.src === "string")
|
|
849
|
-
issues.push(...await scanIds(
|
|
939
|
+
issues.push(...await scanIds(path3.resolve(flags.src), file));
|
|
850
940
|
report(issues);
|
|
851
941
|
const errors = issues.filter((i) => i.severity === "error").length;
|
|
852
942
|
if (errors) {
|
|
@@ -854,98 +944,45 @@ async function runGenerate(filePath, flags) {
|
|
|
854
944
|
${errors} error(s). Nothing was written.`);
|
|
855
945
|
return 1;
|
|
856
946
|
}
|
|
857
|
-
const out =
|
|
858
|
-
await
|
|
859
|
-
const wanted = new Map(files.map((f) => [
|
|
860
|
-
const existing = (await
|
|
947
|
+
const out = path3.resolve(base, file.output.dir);
|
|
948
|
+
await fs3.mkdir(out, { recursive: true });
|
|
949
|
+
const wanted = new Map(files.map((f) => [path3.join(out, f.path), f.content]));
|
|
950
|
+
const existing = (await fs3.readdir(out)).filter((n) => n.endsWith(".ts")).map((n) => path3.join(out, n));
|
|
861
951
|
const changed = [];
|
|
862
952
|
const stale = [];
|
|
863
953
|
for (const [p, content] of wanted)
|
|
864
|
-
if (await
|
|
954
|
+
if (await fs3.readFile(p, "utf8").catch(() => null) !== content)
|
|
865
955
|
changed.push(p);
|
|
866
956
|
for (const p of existing)
|
|
867
|
-
if (!wanted.has(p) && (await
|
|
957
|
+
if (!wanted.has(p) && (await fs3.readFile(p, "utf8")).startsWith(`// ${GENERATED_MARK}`))
|
|
868
958
|
stale.push(p);
|
|
869
959
|
if (flags.check) {
|
|
870
960
|
if (changed.length || stale.length) {
|
|
871
|
-
console.log(`Out of date: ${[...changed, ...stale].map((p) =>
|
|
961
|
+
console.log(`Out of date: ${[...changed, ...stale].map((p) => path3.relative(process.cwd(), p)).join(", ")}`);
|
|
872
962
|
return 1;
|
|
873
963
|
}
|
|
874
964
|
console.log("Generated files are up to date.");
|
|
875
965
|
return 0;
|
|
876
966
|
}
|
|
877
967
|
for (const p of changed)
|
|
878
|
-
await
|
|
968
|
+
await writeFileAtomic(p, wanted.get(p));
|
|
879
969
|
for (const p of stale)
|
|
880
|
-
await
|
|
881
|
-
console.log(`${files.length - 2} screen hook(s) in ${
|
|
970
|
+
await fs3.unlink(p);
|
|
971
|
+
console.log(`${files.length - 2} screen hook(s) in ${path3.relative(process.cwd(), out) || "."}: ${changed.length} written, ${stale.length} removed.`);
|
|
882
972
|
return 0;
|
|
883
973
|
}
|
|
884
|
-
var TYPES = { ".html": "text/html; charset=utf-8", ".js": "text/javascript", ".css": "text/css", ".svg": "image/svg+xml", ".map": "application/json", ".woff2": "font/woff2" };
|
|
885
|
-
async function body(req, limit = 20 * 1024 * 1024) {
|
|
886
|
-
const chunks = [];
|
|
887
|
-
let size = 0;
|
|
888
|
-
for await (const c of req) {
|
|
889
|
-
size += c.length;
|
|
890
|
-
if (size > limit)
|
|
891
|
-
throw new Error("Body too large.");
|
|
892
|
-
chunks.push(c);
|
|
893
|
-
}
|
|
894
|
-
return Buffer.concat(chunks).toString("utf8");
|
|
895
|
-
}
|
|
896
974
|
async function runStudio(filePath, flags) {
|
|
897
975
|
const port = Number(flags.port ?? 5180);
|
|
898
|
-
|
|
899
|
-
parseBindingsFile(await fs2.readFile(filePath, "utf8"));
|
|
900
|
-
const app = fileURLToPath(new URL("./app/", import.meta.url));
|
|
976
|
+
parseBindingsFile(await fs3.readFile(filePath, "utf8"));
|
|
901
977
|
const origins = new Set([`http://localhost:${port}`, `http://127.0.0.1:${port}`]);
|
|
902
|
-
const
|
|
903
|
-
|
|
904
|
-
};
|
|
978
|
+
const allowed = (req) => origins.has(`http://${req.headers.host}`) && (!req.headers.origin || origins.has(String(req.headers.origin)));
|
|
979
|
+
const handle = createStudioHandler({ file: filePath, prefix: "", allowed });
|
|
905
980
|
const server = createServer(async (req, res) => {
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
return send(res, 403, "Forbidden host.");
|
|
910
|
-
if (req.headers.origin && !origins.has(req.headers.origin))
|
|
911
|
-
return send(res, 403, "Forbidden origin.");
|
|
912
|
-
const url = new URL(req.url ?? "/", `http://${host}`);
|
|
913
|
-
if (url.pathname === "/api/bindings") {
|
|
914
|
-
if (req.method === "GET")
|
|
915
|
-
return send(res, 200, await fs2.readFile(filePath, "utf8"), "application/json");
|
|
916
|
-
if (req.method === "PUT") {
|
|
917
|
-
const text = await body(req);
|
|
918
|
-
try {
|
|
919
|
-
parseBindingsFile(text);
|
|
920
|
-
} catch (e) {
|
|
921
|
-
return send(res, 400, e.message);
|
|
922
|
-
}
|
|
923
|
-
await writeFile(filePath, text);
|
|
924
|
-
return send(res, 204, "");
|
|
925
|
-
}
|
|
926
|
-
return send(res, 405, "Method not allowed.");
|
|
927
|
-
}
|
|
928
|
-
if (url.pathname === "/api/openapi" && req.method === "GET") {
|
|
929
|
-
const src = url.searchParams.get("src") ?? "";
|
|
930
|
-
try {
|
|
931
|
-
return send(res, 200, await readDocs(src, base), src.endsWith(".json") ? "application/json" : "text/yaml; charset=utf-8");
|
|
932
|
-
} catch (e) {
|
|
933
|
-
return send(res, 502, e.message);
|
|
934
|
-
}
|
|
935
|
-
}
|
|
936
|
-
if (req.method !== "GET")
|
|
937
|
-
return send(res, 405, "Method not allowed.");
|
|
938
|
-
const rel = url.pathname === "/" ? "index.html" : decodeURIComponent(url.pathname.slice(1));
|
|
939
|
-
const target = path2.resolve(app, rel);
|
|
940
|
-
if (!target.startsWith(app))
|
|
941
|
-
return send(res, 404, "Not found.");
|
|
942
|
-
const data = await fs2.readFile(target).catch(() => null);
|
|
943
|
-
if (!data)
|
|
944
|
-
return send(res, 404, "Not found.");
|
|
945
|
-
res.writeHead(200, { "Content-Type": TYPES[path2.extname(target)] ?? "application/octet-stream", "X-Content-Type-Options": "nosniff" }).end(data);
|
|
946
|
-
} catch (e) {
|
|
947
|
-
send(res, 500, e.message);
|
|
981
|
+
if (!origins.has(`http://${req.headers.host}`)) {
|
|
982
|
+
res.writeHead(403).end("Forbidden host.");
|
|
983
|
+
return;
|
|
948
984
|
}
|
|
985
|
+
await handle(req, res);
|
|
949
986
|
});
|
|
950
987
|
server.listen(port, "127.0.0.1", () => console.log(`Binding studio: http://localhost:${port}
|
|
951
988
|
Editing ${filePath}. Ctrl+C to stop.`));
|
|
@@ -953,7 +990,7 @@ Editing ${filePath}. Ctrl+C to stop.`));
|
|
|
953
990
|
async function main() {
|
|
954
991
|
const [cmd, ...argv] = process.argv.slice(2);
|
|
955
992
|
const { rest, flags } = args(argv);
|
|
956
|
-
const filePath =
|
|
993
|
+
const filePath = path3.resolve(rest[0] ?? "bindings.json");
|
|
957
994
|
if (cmd === "studio")
|
|
958
995
|
return runStudio(filePath, flags);
|
|
959
996
|
if (cmd === "generate")
|
package/dist/overlay.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
function m(o,i){let n;for(let e of i)if(o.startsWith(e+".")&&(!n||e.length>n.length))n=e;return n}function b(o,i){let n=new Map;for(let s of o){let r=m(s,i);if(r)n.set(r,(n.get(r)??0)+1)}let e;for(let[s,r]of n){let l=e?n.get(e):-1;if(r>l||r===l&&s.length>e.length)e=s}return e}var y=new URL(".",import.meta.url).pathname,c="data-component-id",p=null;async function x(){if(p)return p;try{p=((await(await fetch(y+"api/bindings",{cache:"no-store"})).json()).screens??[]).map((i)=>i.id)}catch{p=[]}return p}var f=(o)=>o.getClientRects().length>0&&getComputedStyle(o).visibility!=="hidden";function w(){let o=[...document.querySelectorAll('dialog[open], [role="dialog"], [role="alertdialog"], [aria-modal="true"]')].filter(f);for(let i of o.reverse()){let n=[...i.querySelectorAll(`[${c}]`)].filter(f).map((e)=>e.getAttribute(c));if(n.length)return n}return[...document.querySelectorAll(`[${c}]`)].filter(f).map((i)=>i.getAttribute(c))}var v=`
|
|
2
|
+
:host { all: initial; }
|
|
3
|
+
.fab { position: fixed; z-index: 2147483000; right: max(20px, env(safe-area-inset-right, 0px)); bottom: max(20px, env(safe-area-inset-bottom, 0px));
|
|
4
|
+
display: grid; grid-template-columns: auto 0fr; align-items: center; height: 44px; padding: 0 13px; border-radius: 10px; cursor: pointer;
|
|
5
|
+
border: 1px solid #e2e8f0; background: #fff; color: #0f172a; font: 500 13px/1 Inter, system-ui, -apple-system, sans-serif;
|
|
6
|
+
box-shadow: 0 1px 2px rgba(15,23,42,.06), 0 6px 16px -6px rgba(15,23,42,.18);
|
|
7
|
+
transition: grid-template-columns 240ms cubic-bezier(.2,.7,.2,1), box-shadow 200ms ease, transform 200ms cubic-bezier(.2,.7,.2,1); }
|
|
8
|
+
.fab svg { color: #3b82f6; display: block; }
|
|
9
|
+
.fab span { overflow: hidden; }
|
|
10
|
+
.fab span > span { display: block; padding-left: 8px; white-space: nowrap; opacity: 0; transition: opacity 180ms ease; }
|
|
11
|
+
.fab:hover, .fab:focus-visible { grid-template-columns: auto 1fr; transform: translateY(-1px); box-shadow: 0 1px 2px rgba(15,23,42,.06), 0 10px 24px -8px rgba(15,23,42,.24); }
|
|
12
|
+
.fab:hover span > span, .fab:focus-visible span > span { opacity: 1; transition-delay: 60ms; }
|
|
13
|
+
.fab:focus-visible { outline: 2px solid #3b82f6; outline-offset: 2px; }
|
|
14
|
+
.fab[hidden] { display: none; }
|
|
15
|
+
dialog { width: min(1440px, calc(100vw - 32px)); height: min(920px, calc(100dvh - 32px)); max-width: none; max-height: none; margin: auto; padding: 0;
|
|
16
|
+
border: 0; border-radius: 12px; overflow: hidden; background: #f8fafc; box-shadow: 0 24px 64px -16px rgba(15,23,42,.35); animation: in 240ms cubic-bezier(.2,.7,.2,1); }
|
|
17
|
+
dialog::backdrop { background: rgba(15,23,42,.32); backdrop-filter: blur(2px); }
|
|
18
|
+
iframe { width: 100%; height: 100%; border: 0; display: block; }
|
|
19
|
+
.hl { position: fixed; z-index: 2147482999; pointer-events: none; border: 2px solid #3b82f6; border-radius: 6px; background: rgba(59,130,246,.08); display: none; }
|
|
20
|
+
@keyframes in { from { opacity: 0; transform: translateY(8px) scale(.985); } }
|
|
21
|
+
@media (prefers-reduced-motion: reduce) { * { animation-duration: 1ms !important; transition-duration: 1ms !important; } }
|
|
22
|
+
`,k='<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 7 4 12l5 5M15 7l5 5-5 5"/></svg>';function h(){if(document.getElementById("api-binding-overlay"))return;let o=document.createElement("div");o.id="api-binding-overlay";let i=o.attachShadow({mode:"open"});i.innerHTML=`<style>${v}</style>
|
|
23
|
+
<button class="fab" type="button" aria-label="Bind API" title="Bind API (Alt+click a component to open it)">${k}<span><span>Bind API</span></span></button>
|
|
24
|
+
<dialog aria-label="Bind API"></dialog><div class="hl"></div>`,document.body.appendChild(o);let n=i.querySelector(".fab"),e=i.querySelector("dialog"),s=i.querySelector(".hl"),r=async(t)=>{let a=await x(),d=t?void 0:b(w(),a),u=new URLSearchParams({embed:"1"});if(d)u.set("screen",d);if(t)u.set("component",t);e.innerHTML=`<iframe title="Bind API studio" src="${y}?${u}"></iframe>`,n.hidden=!0,e.showModal()},l=()=>{if(!e.open)return;e.close(),e.innerHTML="",n.hidden=!1,n.focus()};n.addEventListener("click",()=>void r()),e.addEventListener("close",l),window.addEventListener("message",(t)=>{if(t.origin===location.origin&&t.data==="api-binding:close")l()});let g=(t)=>t.altKey&&!e.open?t.target?.closest?.(`[${c}]`)??null:null;document.addEventListener("click",(t)=>{let a=g(t);if(!a)return;t.preventDefault(),t.stopPropagation(),r(a.getAttribute(c))},!0),document.addEventListener("mousemove",(t)=>{let a=g(t);if(!a){s.style.display="none";return}let d=a.getBoundingClientRect();Object.assign(s.style,{display:"block",left:`${d.left-2}px`,top:`${d.top-2}px`,width:`${d.width+4}px`,height:`${d.height+4}px`})},{passive:!0}),document.addEventListener("keyup",(t)=>{if(t.key==="Alt")s.style.display="none"})}if(document.body)h();else document.addEventListener("DOMContentLoaded",h);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Studio HTTP handling shared by the CLI server and the Vite plugin: the studio page, the bindings file, the docs. */
|
|
2
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
3
|
+
export interface StudioHandlerOptions {
|
|
4
|
+
/** Absolute path of bindings.json. */
|
|
5
|
+
file: string;
|
|
6
|
+
/** Path prefix the studio is served under, e.g. "" or "/__binding". */
|
|
7
|
+
prefix: string;
|
|
8
|
+
/** True when the request may read and write the file (host and origin checks live with the caller). */
|
|
9
|
+
allowed: (req: IncomingMessage) => boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare const APP_DIR: string;
|
|
12
|
+
export declare const OVERLAY_FILE: string;
|
|
13
|
+
/** The OpenAPI text: fetched for a URL, read for a .yaml/.yml/.json path relative to `base`. */
|
|
14
|
+
export declare function readDocs(src: string, base: string): Promise<string>;
|
|
15
|
+
/** Writes atomically, so an interrupted save never leaves half a file. */
|
|
16
|
+
export declare function writeFileAtomic(file: string, text: string): Promise<void>;
|
|
17
|
+
/** Handles a request under `prefix`. Returns false when the path is not the studio's, so the caller can pass it on. */
|
|
18
|
+
export declare function createStudioHandler({ file, prefix, allowed }: StudioHandlerOptions): (req: IncomingMessage, res: ServerResponse) => Promise<boolean>;
|
|
@@ -8,9 +8,11 @@ export interface BindingStudioProps {
|
|
|
8
8
|
docsUrl?: (openapi: string) => string;
|
|
9
9
|
/** Screen to open first. */
|
|
10
10
|
initialScreen?: ScreenId;
|
|
11
|
+
/** Component to open first; its screen is found from its id. */
|
|
12
|
+
initialComponent?: string;
|
|
11
13
|
/** Shown in the top bar. */
|
|
12
14
|
title?: string;
|
|
13
15
|
/** Shows a close button in the top bar. */
|
|
14
16
|
onClose?: () => void;
|
|
15
17
|
}
|
|
16
|
-
export declare function BindingStudio({ store, docsUrl, initialScreen, title, onClose }: BindingStudioProps): React.JSX.Element;
|
|
18
|
+
export declare function BindingStudio({ store, docsUrl, initialScreen, initialComponent, title, onClose }: BindingStudioProps): React.JSX.Element;
|
package/dist/studio/index.js
CHANGED
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
catalogFromText2,
|
|
3
3
|
loadCatalog2,
|
|
4
4
|
humanize,
|
|
5
|
+
screenOf2,
|
|
5
6
|
fromFile2,
|
|
6
7
|
toFile2,
|
|
7
8
|
createEngine2,
|
|
@@ -2471,7 +2472,7 @@ var EMPTY_HINT = /* @__PURE__ */ jsxs16("aside", {
|
|
|
2471
2472
|
})
|
|
2472
2473
|
]
|
|
2473
2474
|
});
|
|
2474
|
-
function BindingStudio({ store, docsUrl = same, initialScreen, title = "Bind API", onClose }) {
|
|
2475
|
+
function BindingStudio({ store, docsUrl = same, initialScreen, initialComponent, title = "Bind API", onClose }) {
|
|
2475
2476
|
const [file, setFile] = React14.useState(null);
|
|
2476
2477
|
const [fileError, setFileError] = React14.useState();
|
|
2477
2478
|
const latest = React14.useRef({ file: null, index: {} });
|
|
@@ -2626,13 +2627,14 @@ function BindingStudio({ store, docsUrl = same, initialScreen, title = "Bind API
|
|
|
2626
2627
|
initial: model.index,
|
|
2627
2628
|
save,
|
|
2628
2629
|
title,
|
|
2629
|
-
initialScreen,
|
|
2630
|
+
initialScreen: initialComponent ? screenOf2(initialComponent, model.fw.registry.screens.map((s) => s.id)) ?? initialScreen : initialScreen,
|
|
2631
|
+
initialComponent,
|
|
2630
2632
|
settingsButton
|
|
2631
2633
|
});
|
|
2632
2634
|
}
|
|
2633
|
-
function Workbench({ fw, initial, save, title, initialScreen, settingsButton }) {
|
|
2635
|
+
function Workbench({ fw, initial, save, title, initialScreen, initialComponent, settingsButton }) {
|
|
2634
2636
|
const { index, loaded, status, removed, put, drop, undo, dismiss, retry } = useBindings(initial, save);
|
|
2635
|
-
const [place, go] = React14.useState({ screen: initialScreen });
|
|
2637
|
+
const [place, go] = React14.useState({ screen: initialScreen, component: initialComponent });
|
|
2636
2638
|
const engine = React14.useMemo(() => createEngine2(fw, index), [fw, index]);
|
|
2637
2639
|
const screen = place.screen && engine.SCR.get(place.screen) || fw.registry.screens[0];
|
|
2638
2640
|
const comp = screen && place.component ? engine.compOf(screen.id, place.component) : undefined;
|
|
@@ -2734,5 +2736,5 @@ export {
|
|
|
2734
2736
|
BindingStudio
|
|
2735
2737
|
};
|
|
2736
2738
|
|
|
2737
|
-
//# debugId=
|
|
2739
|
+
//# debugId=033A885DE9DD742964756E2164756E21
|
|
2738
2740
|
//# sourceMappingURL=index.js.map
|