-
+
+ );
+}
+
+/** Architectural drafting blueprint illustration for recursive dependency split cascades */
+function CascadesDraftingIllustration() {
+ return (
+
+ );
+}
+
+/** Architectural drafting blueprint illustration for RFC-8785 cryptographic audit receipts */
+function ReceiptsDraftingIllustration() {
+ return (
+
+ );
+}
+
+/** General blueprint drafting drawing for other states */
+function DefaultDraftingIllustration() {
+ return (
+
+ );
+}
+
+export type EmptyIllustrationVariant = "streams" | "cascades" | "receipts" | "default";
+
+export function Empty({
+ icon,
+ title,
+ variant,
+ children,
+}: {
+ icon?: string;
+ title: string;
+ variant?: EmptyIllustrationVariant;
+ children?: ReactNode;
+}) {
+ const resolvedVariant: EmptyIllustrationVariant =
+ variant ??
+ (icon === "water_drop"
+ ? "streams"
+ : icon === "account_tree"
+ ? "cascades"
+ : icon === "receipt_long" || icon === "receipt" || icon === "account_balance_wallet"
+ ? "receipts"
+ : "default");
+
+ return (
+
+
+ {resolvedVariant === "streams" && }
+ {resolvedVariant === "cascades" && }
+ {resolvedVariant === "receipts" && }
+ {resolvedVariant === "default" && }
+
{title}
{children &&
{children}
}
diff --git a/frontend/src/lib/__tests__/cli.test.ts b/frontend/src/lib/__tests__/cli.test.ts
new file mode 100644
index 0000000..6ffa856
--- /dev/null
+++ b/frontend/src/lib/__tests__/cli.test.ts
@@ -0,0 +1,214 @@
+import { describe, it, expect } from "vitest";
+import { toStellarCliCommand, formatCliValue } from "../cli";
+import { sv } from "../soroban";
+
+const ROUTER_ID = "CA45PGMYO5CNRPCMW3THPKI26PK2CU3JU4XTJZJ3HZ4KLEFRKUQXUTWK";
+const SPLITTER_ID = "CA4U6VUIFA33SQA5QCQYMFNEQTNN6XV43EAZ67IKXOB434V5GI5KB2SY";
+const ADDR_A = "GBZXN7PIRZGNMHGA7MUUUF4GWPY5AYPV6LY4UV2GL6VJGIQRXFDNMADI";
+const ADDR_B = "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF";
+const TOKEN_ADDR = "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC";
+
+describe("toStellarCliCommand", () => {
+ describe("StreamRouter methods", () => {
+ it("generates CLI command for create_stream", () => {
+ const cmd = toStellarCliCommand({
+ contractId: ROUTER_ID,
+ method: "create_stream",
+ args: [
+ sv.addr(ADDR_A),
+ sv.addr(ADDR_B),
+ sv.addr(TOKEN_ADDR),
+ sv.i128(100000n),
+ sv.i128(5000000n),
+ ],
+ });
+ expect(cmd).toBe(
+ `stellar contract invoke --id ${ROUTER_ID} -- create_stream --sender ${ADDR_A} --recipient ${ADDR_B} --token ${TOKEN_ADDR} --rate_per_sec 100000 --deposit 5000000`
+ );
+ });
+
+ it("generates CLI command for withdraw", () => {
+ const cmd = toStellarCliCommand({
+ contractId: ROUTER_ID,
+ method: "withdraw",
+ args: [sv.u64(12)],
+ });
+ expect(cmd).toBe(
+ `stellar contract invoke --id ${ROUTER_ID} -- withdraw --id 12`
+ );
+ });
+
+ it("generates CLI command for withdraw_all", () => {
+ const cmd = toStellarCliCommand({
+ contractId: ROUTER_ID,
+ method: "withdraw_all",
+ args: [sv.addr(ADDR_B)],
+ });
+ expect(cmd).toBe(
+ `stellar contract invoke --id ${ROUTER_ID} -- withdraw_all --recipient ${ADDR_B}`
+ );
+ });
+
+ it("generates CLI command for top_up", () => {
+ const cmd = toStellarCliCommand({
+ contractId: ROUTER_ID,
+ method: "top_up",
+ args: [sv.u64(12), sv.i128(2000000n)],
+ });
+ expect(cmd).toBe(
+ `stellar contract invoke --id ${ROUTER_ID} -- top_up --id 12 --amount 2000000`
+ );
+ });
+
+ it("generates CLI command for cancel", () => {
+ const cmd = toStellarCliCommand({
+ contractId: ROUTER_ID,
+ method: "cancel",
+ args: [sv.u64(12)],
+ });
+ expect(cmd).toBe(
+ `stellar contract invoke --id ${ROUTER_ID} -- cancel --id 12`
+ );
+ });
+
+ it("generates CLI command for withdrawable", () => {
+ const cmd = toStellarCliCommand({
+ contractId: ROUTER_ID,
+ method: "withdrawable",
+ args: [sv.u64(12)],
+ });
+ expect(cmd).toBe(
+ `stellar contract invoke --id ${ROUTER_ID} -- withdrawable --id 12`
+ );
+ });
+
+ it("generates CLI command for get_stream", () => {
+ const cmd = toStellarCliCommand({
+ contractId: ROUTER_ID,
+ method: "get_stream",
+ args: [sv.u64(12)],
+ });
+ expect(cmd).toBe(
+ `stellar contract invoke --id ${ROUTER_ID} -- get_stream --id 12`
+ );
+ });
+
+ it("generates CLI command for streams_by_sender", () => {
+ const cmd = toStellarCliCommand({
+ contractId: ROUTER_ID,
+ method: "streams_by_sender",
+ args: [sv.addr(ADDR_A)],
+ });
+ expect(cmd).toBe(
+ `stellar contract invoke --id ${ROUTER_ID} -- streams_by_sender --sender ${ADDR_A}`
+ );
+ });
+
+ it("generates CLI command for streams_by_recipient", () => {
+ const cmd = toStellarCliCommand({
+ contractId: ROUTER_ID,
+ method: "streams_by_recipient",
+ args: [sv.addr(ADDR_B)],
+ });
+ expect(cmd).toBe(
+ `stellar contract invoke --id ${ROUTER_ID} -- streams_by_recipient --recipient ${ADDR_B}`
+ );
+ });
+
+ it("generates CLI command for stream_count", () => {
+ const cmd = toStellarCliCommand({
+ contractId: ROUTER_ID,
+ method: "stream_count",
+ args: [],
+ });
+ expect(cmd).toBe(`stellar contract invoke --id ${ROUTER_ID} -- stream_count`);
+ });
+ });
+
+ describe("CascadeSplitter methods", () => {
+ it("generates CLI command for set_splits", () => {
+ const splits = [
+ { recipient: ADDR_B, bps: 5000, label: "serde" },
+ ];
+ const cmd = toStellarCliCommand({
+ contractId: SPLITTER_ID,
+ method: "set_splits",
+ args: [sv.addr(ADDR_A), sv.splits(splits)],
+ });
+ expect(cmd).toContain(`stellar contract invoke --id ${SPLITTER_ID} -- set_splits --owner ${ADDR_A} --splits `);
+ expect(cmd).toContain('"bps":5000');
+ expect(cmd).toContain('"label":"serde"');
+ expect(cmd).toContain(`"recipient":"${ADDR_B}"`);
+ });
+
+ it("generates CLI command for get_splits", () => {
+ const cmd = toStellarCliCommand({
+ contractId: SPLITTER_ID,
+ method: "get_splits",
+ args: [sv.addr(ADDR_A)],
+ });
+ expect(cmd).toBe(
+ `stellar contract invoke --id ${SPLITTER_ID} -- get_splits --owner ${ADDR_A}`
+ );
+ });
+
+ it("generates CLI command for preview", () => {
+ const cmd = toStellarCliCommand({
+ contractId: SPLITTER_ID,
+ method: "preview",
+ args: [sv.addr(ADDR_A), sv.i128(10000000n)],
+ });
+ expect(cmd).toBe(
+ `stellar contract invoke --id ${SPLITTER_ID} -- preview --owner ${ADDR_A} --amount 10000000`
+ );
+ });
+
+ it("generates CLI command for distribute", () => {
+ const cmd = toStellarCliCommand({
+ contractId: SPLITTER_ID,
+ method: "distribute",
+ args: [
+ sv.addr(ADDR_A),
+ sv.addr(ADDR_B),
+ sv.addr(TOKEN_ADDR),
+ sv.i128(10000000n),
+ ],
+ });
+ expect(cmd).toBe(
+ `stellar contract invoke --id ${SPLITTER_ID} -- distribute --from ${ADDR_A} --owner ${ADDR_B} --token ${TOKEN_ADDR} --amount 10000000`
+ );
+ });
+ });
+
+ describe("Flags and formatting options", () => {
+ it("includes --network and --source when supplied", () => {
+ const cmd = toStellarCliCommand({
+ contractId: ROUTER_ID,
+ method: "withdraw",
+ args: [sv.u64(99)],
+ source: ADDR_A,
+ network: "testnet",
+ });
+ expect(cmd).toBe(
+ `stellar contract invoke --id ${ROUTER_ID} --source ${ADDR_A} --network testnet -- withdraw --id 99`
+ );
+ });
+
+ it("accepts plain record objects as arguments", () => {
+ const cmd = toStellarCliCommand({
+ contractId: ROUTER_ID,
+ method: "top_up",
+ args: { id: 10, amount: "5000" },
+ });
+ expect(cmd).toBe(
+ `stellar contract invoke --id ${ROUTER_ID} -- top_up --id 10 --amount 5000`
+ );
+ });
+
+ it("safely escapes shell values with spaces", () => {
+ expect(formatCliValue("hello world")).toBe("'hello world'");
+ expect(formatCliValue(123)).toBe("123");
+ expect(formatCliValue(456n)).toBe("456");
+ });
+ });
+});
diff --git a/frontend/src/lib/__tests__/focus.test.ts b/frontend/src/lib/__tests__/focus.test.ts
new file mode 100644
index 0000000..becb129
--- /dev/null
+++ b/frontend/src/lib/__tests__/focus.test.ts
@@ -0,0 +1,116 @@
+import { describe, it, expect, vi } from "vitest";
+import { handleFocusTrap } from "../focus";
+
+describe("handleFocusTrap", () => {
+ function createMockContainer(elements: { tag: string; id: string; disabled?: boolean }[]) {
+ const container = {
+ contains: vi.fn((el) => elements.some((e) => e.id === el?.id)),
+ querySelectorAll: vi.fn((_selector: string) => {
+ return elements.map((item) => ({
+ id: item.id,
+ tagName: item.tag.toUpperCase(),
+ focus: vi.fn(),
+ getAttribute: vi.fn((attr: string) => (attr === "disabled" && item.disabled ? "" : null)),
+ style: {},
+ }));
+ }),
+ } as unknown as HTMLElement;
+ return container;
+ }
+
+ it("ignores non-Tab key events", () => {
+ const container = createMockContainer([{ tag: "button", id: "btn1" }]);
+ const preventDefault = vi.fn();
+ const handled = handleFocusTrap({
+ container,
+ event: { key: "Enter", shiftKey: false, preventDefault },
+ activeElement: null,
+ });
+
+ expect(handled).toBe(false);
+ expect(preventDefault).not.toHaveBeenCalled();
+ });
+
+ it("wraps from last to first on Tab", () => {
+ const elements = [
+ { id: "close-btn", tagName: "BUTTON", focus: vi.fn(), style: {} },
+ { id: "save-btn", tagName: "BUTTON", focus: vi.fn(), style: {} },
+ ];
+ const container = {
+ contains: vi.fn(() => true),
+ querySelectorAll: vi.fn(() => elements),
+ } as unknown as HTMLElement;
+
+ const preventDefault = vi.fn();
+ const handled = handleFocusTrap({
+ container,
+ event: { key: "Tab", shiftKey: false, preventDefault },
+ activeElement: elements[1] as any, // Last element
+ });
+
+ expect(handled).toBe(true);
+ expect(preventDefault).toHaveBeenCalled();
+ expect(elements[0].focus).toHaveBeenCalled();
+ });
+
+ it("wraps from first to last on Shift+Tab", () => {
+ const elements = [
+ { id: "close-btn", tagName: "BUTTON", focus: vi.fn(), style: {} },
+ { id: "save-btn", tagName: "BUTTON", focus: vi.fn(), style: {} },
+ ];
+ const container = {
+ contains: vi.fn(() => true),
+ querySelectorAll: vi.fn(() => elements),
+ } as unknown as HTMLElement;
+
+ const preventDefault = vi.fn();
+ const handled = handleFocusTrap({
+ container,
+ event: { key: "Tab", shiftKey: true, preventDefault },
+ activeElement: elements[0] as any, // First element
+ });
+
+ expect(handled).toBe(true);
+ expect(preventDefault).toHaveBeenCalled();
+ expect(elements[1].focus).toHaveBeenCalled();
+ });
+
+ it("focuses first element if focus is outside container on Tab", () => {
+ const elements = [
+ { id: "close-btn", tagName: "BUTTON", focus: vi.fn(), style: {} },
+ { id: "save-btn", tagName: "BUTTON", focus: vi.fn(), style: {} },
+ ];
+ const container = {
+ contains: vi.fn(() => false),
+ querySelectorAll: vi.fn(() => elements),
+ } as unknown as HTMLElement;
+
+ const preventDefault = vi.fn();
+ const handled = handleFocusTrap({
+ container,
+ event: { key: "Tab", shiftKey: false, preventDefault },
+ activeElement: { id: "outside" } as any,
+ });
+
+ expect(handled).toBe(true);
+ expect(preventDefault).toHaveBeenCalled();
+ expect(elements[0].focus).toHaveBeenCalled();
+ });
+
+ it("prevents default if no focusable elements are present", () => {
+ const container = {
+ contains: vi.fn(() => true),
+ querySelectorAll: vi.fn(() => []),
+ } as unknown as HTMLElement;
+
+ const preventDefault = vi.fn();
+ const handled = handleFocusTrap({
+ container,
+ event: { key: "Tab", shiftKey: false, preventDefault },
+ activeElement: null,
+ });
+
+ expect(handled).toBe(true);
+ expect(preventDefault).toHaveBeenCalled();
+ });
+});
diff --git a/frontend/src/lib/__tests__/manifest.test.ts b/frontend/src/lib/__tests__/manifest.test.ts
index bc26051..aa7827a 100644
--- a/frontend/src/lib/__tests__/manifest.test.ts
+++ b/frontend/src/lib/__tests__/manifest.test.ts
@@ -1,9 +1,17 @@
import { describe, expect, it } from "vitest";
-import { equalWeights, parseManifest } from "../manifest";
+import {
+ equalWeights,
+ MAX_SPLIT_ROWS,
+ parseCargoLock,
+ parseCargoToml,
+ parseManifest,
+ parsePackageJson,
+ parsePackageLockJson,
+} from "../manifest";
describe("parseCargoToml", () => {
it("reads all common dependency forms", () => {
- const m = parseManifest(`
+ const raw = `
[package]
name = "demo"
@@ -21,7 +29,8 @@ proptest = "1"
[target.'cfg(unix)'.dependencies]
libc = "0.2" # comment
-`);
+`;
+ const m = parseCargoToml(raw);
expect(m.name).toBe("demo");
const byName = Object.fromEntries(m.deps.map((d) => [d.name, d]));
expect(byName.serde.version).toBe("1.0");
@@ -36,17 +45,246 @@ libc = "0.2" # comment
describe("parsePackageJson", () => {
it("reads deps, peer and dev deps without duplicates", () => {
- const m = parseManifest(
- JSON.stringify({ name: "x", dependencies: { a: "^1" }, peerDependencies: { b: "2" }, devDependencies: { a: "1", c: "3" } }),
- );
+ const raw = JSON.stringify({
+ name: "x",
+ dependencies: { a: "^1" },
+ peerDependencies: { b: "2" },
+ devDependencies: { a: "1", c: "3" },
+ });
+ const m = parsePackageJson(raw);
expect(m.ecosystem).toBe("npm");
expect(m.deps.map((d) => `${d.name}:${d.kind}`)).toEqual(["a:normal", "b:peer", "c:dev"]);
});
+
it("errors on invalid json", () => {
expect(() => parseManifest("{ nope", "package.json")).toThrow(/valid JSON/);
});
});
+describe("parseCargoLock", () => {
+ const sampleCargoLock = `
+version = 3
+
+[[package]]
+name = "my-crate"
+version = "0.1.0"
+dependencies = [
+ "serde",
+ "soroban-sdk 22.0.1",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.197"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.197"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "soroban-sdk"
+version = "22.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.86"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "quote"
+version = "1.0.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "syn"
+version = "2.0.66"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+`;
+
+ it("parses direct dependencies only by default", () => {
+ const m = parseCargoLock(sampleCargoLock, { includeTransitive: false });
+ expect(m.ecosystem).toBe("cargo");
+ expect(m.name).toBe("my-crate");
+ expect(m.deps.map((d) => d.name)).toEqual(["serde", "soroban-sdk"]);
+ expect(m.deps.find((d) => d.name === "serde")?.version).toBe("1.0.197");
+ });
+
+ it("parses transitive dependencies when requested", () => {
+ const m = parseCargoLock(sampleCargoLock, { includeTransitive: true });
+ expect(m.ecosystem).toBe("cargo");
+ const names = m.deps.map((d) => d.name);
+ expect(names).toContain("serde");
+ expect(names).toContain("serde_derive");
+ expect(names).toContain("proc-macro2");
+ expect(names).toContain("quote");
+ expect(names).toContain("syn");
+ expect(names).not.toContain("my-crate");
+ });
+
+ it("caps results at MAX_SPLIT_ROWS (32) and de-duplicates", () => {
+ let bigLock = `
+[[package]]
+name = "root"
+version = "0.1.0"
+dependencies = []
+`;
+ for (let i = 0; i < 50; i++) {
+ bigLock += `
+[[package]]
+name = "dep-${i}"
+version = "1.0.${i}"
+source = "registry+https://..."
+`;
+ }
+ // Duplicate one
+ bigLock += `
+[[package]]
+name = "dep-0"
+version = "1.0.99"
+source = "registry+https://..."
+`;
+
+ const m = parseCargoLock(bigLock, { includeTransitive: true });
+ expect(m.deps.length).toBe(MAX_SPLIT_ROWS);
+ expect(m.deps.length).toBeLessThanOrEqual(32);
+ const seen = new Set(m.deps.map((d) => d.name));
+ expect(seen.size).toBe(m.deps.length);
+ });
+
+ it("routes through parseManifest with filename detection", () => {
+ const m = parseManifest(sampleCargoLock, "Cargo.lock", { includeTransitive: true });
+ expect(m.ecosystem).toBe("cargo");
+ expect(m.deps.length).toBeGreaterThan(2);
+ });
+});
+
+describe("parsePackageLockJson", () => {
+ const sampleV3Lock = JSON.stringify({
+ name: "flowpulse-frontend",
+ version: "0.1.0",
+ lockfileVersion: 3,
+ packages: {
+ "": {
+ name: "flowpulse-frontend",
+ version: "0.1.0",
+ dependencies: {
+ "@stellar/stellar-sdk": "^17.4.0",
+ react: "^19.3.1",
+ },
+ devDependencies: {
+ typescript: "~5.9.3",
+ },
+ },
+ "node_modules/@stellar/stellar-sdk": {
+ version: "17.4.0",
+ dependencies: {
+ axios: "^1.7.0",
+ eventsource: "^2.0.2",
+ },
+ },
+ "node_modules/axios": {
+ version: "1.7.9",
+ },
+ "node_modules/eventsource": {
+ version: "2.0.2",
+ },
+ "node_modules/react": {
+ version: "19.3.1",
+ },
+ "node_modules/typescript": {
+ version: "5.9.3",
+ dev: true,
+ },
+ },
+ });
+
+ it("extracts direct dependencies from v3 lockfile when includeTransitive is false", () => {
+ const m = parsePackageLockJson(sampleV3Lock, { includeTransitive: false });
+ expect(m.ecosystem).toBe("npm");
+ expect(m.name).toBe("flowpulse-frontend");
+ const names = m.deps.map((d) => d.name);
+ expect(names).toEqual(["@stellar/stellar-sdk", "react", "typescript"]);
+ const ts = m.deps.find((d) => d.name === "typescript");
+ expect(ts?.kind).toBe("dev");
+ });
+
+ it("extracts transitive dependencies from v3 lockfile when includeTransitive is true", () => {
+ const m = parsePackageLockJson(sampleV3Lock, { includeTransitive: true });
+ expect(m.ecosystem).toBe("npm");
+ const names = m.deps.map((d) => d.name);
+ expect(names).toContain("@stellar/stellar-sdk");
+ expect(names).toContain("axios");
+ expect(names).toContain("eventsource");
+ expect(names).toContain("react");
+ expect(names).toContain("typescript");
+ expect(names).not.toContain("flowpulse-frontend");
+ });
+
+ it("extracts transitive dependencies from v1 lockfile", () => {
+ const sampleV1Lock = JSON.stringify({
+ name: "v1-app",
+ version: "1.0.0",
+ lockfileVersion: 1,
+ dependencies: {
+ express: {
+ version: "4.19.2",
+ dependencies: {
+ accepts: {
+ version: "1.3.8",
+ dependencies: {
+ "mime-types": {
+ version: "2.1.35",
+ },
+ },
+ },
+ },
+ },
+ },
+ });
+
+ const direct = parsePackageLockJson(sampleV1Lock, { includeTransitive: false });
+ expect(direct.deps.map((d) => d.name)).toEqual(["express"]);
+
+ const transitive = parsePackageLockJson(sampleV1Lock, { includeTransitive: true });
+ expect(transitive.deps.map((d) => d.name)).toEqual(["express", "accepts", "mime-types"]);
+ });
+
+ it("caps at maxRows and handles nested node_modules paths correctly", () => {
+ const sampleNested = JSON.stringify({
+ name: "nested-app",
+ lockfileVersion: 2,
+ packages: {
+ "": { name: "nested-app" },
+ "node_modules/a": { version: "1.0.0" },
+ "node_modules/a/node_modules/b": { version: "2.0.0" },
+ "node_modules/@scope/pkg": { version: "3.0.0" },
+ },
+ });
+
+ const m = parsePackageLockJson(sampleNested, { includeTransitive: true, maxRows: 2 });
+ expect(m.deps.length).toBe(2);
+ expect(m.deps[0].name).toBe("a");
+ expect(m.deps[1].name).toBe("b");
+ });
+
+ it("routes through parseManifest with package-lock.json", () => {
+ const m = parseManifest(sampleV3Lock, "package-lock.json", { includeTransitive: true });
+ expect(m.ecosystem).toBe("npm");
+ expect(m.deps.map((d) => d.name)).toContain("axios");
+ });
+});
+
describe("equalWeights", () => {
it("sums exactly to the target", () => {
const w = equalWeights(7, 9000);
diff --git a/frontend/src/lib/__tests__/qr.test.ts b/frontend/src/lib/__tests__/qr.test.ts
new file mode 100644
index 0000000..fe5cc53
--- /dev/null
+++ b/frontend/src/lib/__tests__/qr.test.ts
@@ -0,0 +1,35 @@
+import { describe, it, expect } from "vitest";
+import { encodeQr, qrToSvgPath } from "../qr";
+
+describe("QR Code Generator", () => {
+ it("encodes a basic URL into a valid QR matrix", () => {
+ const url = "https://stellar.expert/explorer/testnet/tx/1234567890abcdef";
+ const qr = encodeQr(url);
+
+ expect(qr.size).toBeGreaterThanOrEqual(21);
+ expect(qr.modules.length).toBe(qr.size);
+ expect(qr.modules[0].length).toBe(qr.size);
+
+ // Verify top-left finder pattern (7x7 outer boundary has black modules)
+ expect(qr.modules[0][0]).toBe(true);
+ expect(qr.modules[0][6]).toBe(true);
+ expect(qr.modules[6][0]).toBe(true);
+ expect(qr.modules[6][6]).toBe(true);
+ expect(qr.modules[1][1]).toBe(false); // inner white space
+ expect(qr.modules[3][3]).toBe(true); // center black module
+ });
+
+ it("generates an SVG path string from the matrix", () => {
+ const qr = encodeQr("https://flowpulse.org");
+ const path = qrToSvgPath(qr);
+
+ expect(typeof path).toBe("string");
+ expect(path.startsWith("M")).toBe(true);
+ expect(path).toContain("h1v1h-1z");
+ });
+
+ it("handles empty or short strings without crashing", () => {
+ const qr = encodeQr("test");
+ expect(qr.size).toBe(21); // Version 1 size: 17 + 4*1 = 21
+ });
+});
diff --git a/frontend/src/lib/cli.ts b/frontend/src/lib/cli.ts
new file mode 100644
index 0000000..1b6d432
--- /dev/null
+++ b/frontend/src/lib/cli.ts
@@ -0,0 +1,94 @@
+import { scValToNative, xdr } from "@stellar/stellar-sdk";
+
+export const METHOD_SIGNATURES: Record
= {
+ // StreamRouter
+ create_stream: ["sender", "recipient", "token", "rate_per_sec", "deposit"],
+ withdraw: ["id"],
+ withdraw_all: ["recipient"],
+ top_up: ["id", "amount"],
+ cancel: ["id"],
+ withdrawable: ["id"],
+ get_stream: ["id"],
+ streams_by_sender: ["sender"],
+ streams_by_recipient: ["recipient"],
+ stream_count: [],
+
+ // CascadeSplitter
+ set_splits: ["owner", "splits"],
+ get_splits: ["owner"],
+ preview: ["owner", "amount"],
+ distribute: ["from", "owner", "token", "amount"],
+};
+
+export interface FormatCliOptions {
+ contractId: string;
+ method: string;
+ args?: xdr.ScVal[] | Record | Array;
+ network?: string;
+ source?: string;
+}
+
+/**
+ * Format a value into a safe shell argument representation for Stellar CLI.
+ */
+export function formatCliValue(val: unknown): string {
+ if (typeof val === "bigint" || typeof val === "number") {
+ return val.toString();
+ }
+ if (typeof val === "string") {
+ // If it contains spaces or quotes, wrap in quotes
+ if (/[\s"']/.test(val)) {
+ return `'${val.replace(/'/g, "'\\''")}'`;
+ }
+ return val;
+ }
+ if (typeof val === "boolean") {
+ return val ? "true" : "false";
+ }
+ // Array or object (e.g. splits struct or JSON payload)
+ const json = JSON.stringify(val);
+ return `'${json.replace(/'/g, "'\\''")}'`;
+}
+
+/**
+ * Generate the equivalent `stellar contract invoke` CLI command for a Soroban call.
+ */
+export function toStellarCliCommand({
+ contractId,
+ method,
+ args = [],
+ network,
+ source,
+}: FormatCliOptions): string {
+ const parts: string[] = ["stellar", "contract", "invoke", "--id", contractId];
+
+ if (source) {
+ parts.push("--source", source);
+ }
+ if (network) {
+ parts.push("--network", network);
+ }
+
+ parts.push("--", method);
+
+ let namedArgs: Record = {};
+
+ if (Array.isArray(args)) {
+ const signature = METHOD_SIGNATURES[method];
+ args.forEach((arg, index) => {
+ // Decode ScVal if applicable
+ const nativeVal =
+ arg instanceof xdr.ScVal ? scValToNative(arg as xdr.ScVal) : arg;
+ const paramName = signature && signature[index] ? signature[index] : `arg${index + 1}`;
+ namedArgs[paramName] = nativeVal;
+ });
+ } else if (args && typeof args === "object") {
+ namedArgs = args as Record;
+ }
+
+ for (const [key, val] of Object.entries(namedArgs)) {
+ parts.push(`--${key}`, formatCliValue(val));
+ }
+
+ return parts.join(" ");
+}
diff --git a/frontend/src/lib/focus.ts b/frontend/src/lib/focus.ts
new file mode 100644
index 0000000..2d6c0e7
--- /dev/null
+++ b/frontend/src/lib/focus.ts
@@ -0,0 +1,53 @@
+export const FOCUSABLE_SELECTOR =
+ 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
+
+export interface FocusTrapOptions {
+ container: HTMLElement;
+ event: { key: string; shiftKey: boolean; preventDefault: () => void };
+ activeElement: Element | null;
+}
+
+/**
+ * Handles Tab and Shift+Tab key navigation to keep focus trapped within the container.
+ * Returns true if the event was handled (trapped) and default prevented.
+ */
+export function handleFocusTrap({
+ container,
+ event,
+ activeElement,
+}: FocusTrapOptions): boolean {
+ if (event.key !== "Tab") return false;
+
+ const focusables = Array.from(
+ container.querySelectorAll(FOCUSABLE_SELECTOR)
+ ).filter((el) => {
+ // In JSDOM / headless, offsetParent might be null, so check display/visibility if needed
+ const style = (el as any).style;
+ if (style && (style.display === "none" || style.visibility === "hidden")) return false;
+ return true;
+ });
+
+ if (focusables.length === 0) {
+ event.preventDefault();
+ return true;
+ }
+
+ const first = focusables[0];
+ const last = focusables[focusables.length - 1];
+
+ if (event.shiftKey) {
+ if (activeElement === first || !container.contains(activeElement)) {
+ event.preventDefault();
+ last.focus();
+ return true;
+ }
+ } else {
+ if (activeElement === last || !container.contains(activeElement)) {
+ event.preventDefault();
+ first.focus();
+ return true;
+ }
+ }
+
+ return false;
+}
diff --git a/frontend/src/lib/manifest.ts b/frontend/src/lib/manifest.ts
index 306d01f..b4e4d68 100644
--- a/frontend/src/lib/manifest.ts
+++ b/frontend/src/lib/manifest.ts
@@ -1,8 +1,10 @@
/**
* Dependency manifest parsing for split tables.
- * Supports Cargo.toml, package.json, and Cargo.lock / package-lock.json (top-level only).
+ * Supports Cargo.toml, package.json, Cargo.lock, and package-lock.json (with transitive dependencies option).
*/
+export const MAX_SPLIT_ROWS = 32;
+
export type Ecosystem = "cargo" | "npm";
export interface Dependency {
@@ -18,9 +20,34 @@ export interface Manifest {
deps: Dependency[];
}
-export function parseManifest(text: string, filename = ""): Manifest {
+export interface ParseManifestOptions {
+ includeTransitive?: boolean;
+ maxRows?: number;
+}
+
+export function parseManifest(text: string, filename = "", options?: ParseManifestOptions): Manifest {
const t = text.trim();
- if (filename.endsWith(".json") || t.startsWith("{")) return parsePackageJson(t);
+ const lowerFile = filename.toLowerCase();
+
+ if (lowerFile.endsWith("cargo.lock") || (lowerFile.endsWith(".lock") && t.includes("[[package]]"))) {
+ return parseCargoLock(t, options);
+ }
+
+ if (
+ lowerFile.endsWith("package-lock.json") ||
+ (lowerFile.endsWith(".json") && t.includes('"lockfileVersion"'))
+ ) {
+ return parsePackageLockJson(t, options);
+ }
+
+ if (lowerFile.endsWith(".json") || t.startsWith("{")) {
+ return parsePackageJson(t);
+ }
+
+ if (t.includes("[[package]]") && !t.includes("[package]") && !t.includes("[workspace]")) {
+ return parseCargoLock(t, options);
+ }
+
return parseCargoToml(t);
}
@@ -50,6 +77,245 @@ export function parsePackageJson(text: string): Manifest {
return { ecosystem: "npm", name: typeof j.name === "string" ? j.name : undefined, deps };
}
+/**
+ * Parses Cargo.lock and optionally includes transitive dependencies.
+ * De-duplicates and caps at maxRows (default MAX_SPLIT_ROWS = 32).
+ */
+export function parseCargoLock(text: string, options?: ParseManifestOptions): Manifest {
+ const includeTransitive = options?.includeTransitive ?? false;
+ const maxRows = options?.maxRows ?? MAX_SPLIT_ROWS;
+
+ interface RawPackage {
+ name: string;
+ version: string;
+ source?: string;
+ dependencies: string[];
+ }
+
+ const packages: RawPackage[] = [];
+ let current: Partial | null = null;
+ let inDependencies = false;
+
+ for (const raw of text.split(/\r?\n/)) {
+ const line = raw.replace(/\s+#.*$/, "").trim();
+ if (!line || line.startsWith("#")) continue;
+
+ if (line === "[[package]]") {
+ if (current && current.name && current.version) {
+ packages.push({
+ name: current.name,
+ version: current.version,
+ source: current.source,
+ dependencies: current.dependencies ?? [],
+ });
+ }
+ current = { dependencies: [] };
+ inDependencies = false;
+ continue;
+ }
+
+ if (!current) continue;
+
+ if (inDependencies) {
+ if (line === "]") {
+ inDependencies = false;
+ continue;
+ }
+ const depMatch = line.match(/^"([^"]+)"/);
+ if (depMatch) {
+ const depRaw = depMatch[1].trim();
+ const depName = depRaw.split(" ")[0];
+ current.dependencies!.push(depName);
+ }
+ continue;
+ }
+
+ const kv = line.match(/^("?)([A-Za-z0-9_\-]+)\1\s*=\s*(.+)$/);
+ if (!kv) continue;
+ const key = kv[2];
+ const val = kv[3].trim();
+
+ if (key === "name") {
+ current.name = unquote(val);
+ } else if (key === "version") {
+ current.version = unquote(val);
+ } else if (key === "source") {
+ current.source = unquote(val);
+ } else if (key === "dependencies") {
+ if (val === "[") {
+ inDependencies = true;
+ } else if (val.startsWith("[") && val.endsWith("]")) {
+ const inner = val.slice(1, -1).trim();
+ if (inner) {
+ const list = inner.split(",").map((s) => s.trim().replace(/^["']|["']$/g, "")).filter(Boolean);
+ current.dependencies = list.map((d) => d.split(" ")[0]);
+ }
+ }
+ }
+ }
+
+ if (current && current.name && current.version) {
+ packages.push({
+ name: current.name,
+ version: current.version,
+ source: current.source,
+ dependencies: current.dependencies ?? [],
+ });
+ }
+
+ if (packages.length === 0) {
+ return { ecosystem: "cargo", deps: [] };
+ }
+
+ // Root packages are local packages without external source specification
+ const rootPackages = packages.filter((p) => !p.source);
+ const primaryRoot = rootPackages[0] ?? packages[0];
+ const rootNames = new Set(rootPackages.map((p) => p.name));
+
+ const deps: Dependency[] = [];
+ const seen = new Set();
+
+ const pushDep = (name: string, version: string) => {
+ if (seen.has(name) || rootNames.has(name)) return;
+ seen.add(name);
+ deps.push({
+ name,
+ version,
+ ecosystem: "cargo",
+ kind: "normal",
+ });
+ };
+
+ if (!includeTransitive) {
+ // Only direct dependencies of the root packages
+ const directDeps = new Set();
+ for (const r of rootPackages) {
+ for (const d of r.dependencies) {
+ directDeps.add(d);
+ }
+ }
+ for (const pkg of packages) {
+ if (directDeps.has(pkg.name)) {
+ pushDep(pkg.name, pkg.version);
+ if (deps.length >= maxRows) break;
+ }
+ }
+ } else {
+ // All unique transitive dependencies across the entire lock graph
+ for (const pkg of packages) {
+ pushDep(pkg.name, pkg.version);
+ if (deps.length >= maxRows) break;
+ }
+ }
+
+ return {
+ ecosystem: "cargo",
+ name: primaryRoot?.name,
+ deps: deps.slice(0, maxRows),
+ };
+}
+
+/**
+ * Parses package-lock.json (v1, v2, v3 formats) and optionally includes transitive dependencies.
+ * De-duplicates and caps at maxRows (default MAX_SPLIT_ROWS = 32).
+ */
+export function parsePackageLockJson(text: string, options?: ParseManifestOptions): Manifest {
+ const includeTransitive = options?.includeTransitive ?? false;
+ const maxRows = options?.maxRows ?? MAX_SPLIT_ROWS;
+
+ let j: Record;
+ try {
+ j = JSON.parse(text);
+ } catch {
+ throw new Error("package-lock.json is not valid JSON");
+ }
+
+ const rootName = typeof j.name === "string" ? j.name : undefined;
+ const deps: Dependency[] = [];
+ const seen = new Set();
+
+ const pushDep = (name: string, version: string, kind: Dependency["kind"] = "normal") => {
+ if (!name || seen.has(name) || name === rootName) return;
+ seen.add(name);
+ deps.push({
+ name,
+ version: String(version || "*"),
+ ecosystem: "npm",
+ kind,
+ });
+ };
+
+ // Modern npm lockfile format: v2 / v3 using "packages"
+ if (j.packages && typeof j.packages === "object") {
+ const pkgs = j.packages as Record>;
+ const rootPkg = pkgs[""] || {};
+ const directDeps = (rootPkg.dependencies || {}) as Record;
+ const directDevDeps = (rootPkg.devDependencies || {}) as Record;
+ const directPeerDeps = (rootPkg.peerDependencies || {}) as Record;
+
+ if (!includeTransitive) {
+ for (const [name, reqVer] of Object.entries(directDeps)) {
+ const resolved = pkgs[`node_modules/${name}`]?.version;
+ pushDep(name, String(resolved || reqVer), "normal");
+ if (deps.length >= maxRows) break;
+ }
+ for (const [name, reqVer] of Object.entries(directPeerDeps)) {
+ if (deps.length >= maxRows) break;
+ const resolved = pkgs[`node_modules/${name}`]?.version;
+ pushDep(name, String(resolved || reqVer), "peer");
+ }
+ for (const [name, reqVer] of Object.entries(directDevDeps)) {
+ if (deps.length >= maxRows) break;
+ const resolved = pkgs[`node_modules/${name}`]?.version;
+ pushDep(name, String(resolved || reqVer), "dev");
+ }
+ } else {
+ // Collect all packages from node_modules/*
+ for (const [pathKey, val] of Object.entries(pkgs)) {
+ if (!pathKey || pathKey === "" || !val || typeof val !== "object") continue;
+ const lastIndex = pathKey.lastIndexOf("node_modules/");
+ if (lastIndex === -1) continue;
+ const rawPkgName = pathKey.slice(lastIndex + "node_modules/".length);
+ if (!rawPkgName) continue;
+
+ const isDev = Boolean(val.dev);
+ const isPeer = Boolean(val.peer);
+ const kind: Dependency["kind"] = isPeer ? "peer" : isDev ? "dev" : "normal";
+ const ver = typeof val.version === "string" ? val.version : "*";
+
+ pushDep(rawPkgName, ver, kind);
+ if (deps.length >= maxRows) break;
+ }
+ }
+ } else if (j.dependencies && typeof j.dependencies === "object") {
+ // Legacy npm lockfile format: v1
+ const depsTree = j.dependencies as Record>;
+
+ const walkV1 = (tree: Record>) => {
+ for (const [name, node] of Object.entries(tree)) {
+ if (!node || typeof node !== "object") continue;
+ const ver = typeof node.version === "string" ? node.version : "*";
+ const isDev = Boolean(node.dev);
+ pushDep(name, ver, isDev ? "dev" : "normal");
+ if (deps.length >= maxRows) return;
+
+ if (includeTransitive && node.dependencies && typeof node.dependencies === "object") {
+ walkV1(node.dependencies as Record>);
+ if (deps.length >= maxRows) return;
+ }
+ }
+ };
+
+ walkV1(depsTree);
+ }
+
+ return {
+ ecosystem: "npm",
+ name: rootName,
+ deps: deps.slice(0, maxRows),
+ };
+}
+
/**
* Minimal TOML reader covering the dependency tables used in practice:
* [dependencies] foo = "1" | foo = { version = "1", ... } | foo.workspace = true
diff --git a/frontend/src/lib/qr.ts b/frontend/src/lib/qr.ts
new file mode 100644
index 0000000..533c8c8
--- /dev/null
+++ b/frontend/src/lib/qr.ts
@@ -0,0 +1,284 @@
+/**
+ * Lightweight, zero-dependency QR Code (Model 2) SVG generator.
+ * Implements Byte mode with standard Reed-Solomon error correction (Level M/L).
+ */
+
+// Galois Field GF(256) tables with primitive polynomial 0x11d (285)
+const EXP_TABLE = new Uint8Array(512);
+const LOG_TABLE = new Uint8Array(256);
+
+(function initGaloisField() {
+ let x = 1;
+ for (let i = 0; i < 255; i++) {
+ EXP_TABLE[i] = x;
+ EXP_TABLE[i + 255] = x;
+ LOG_TABLE[x] = i;
+ x <<= 1;
+ if (x & 0x100) x ^= 0x11d;
+ }
+})();
+
+function gfMul(x: number, y: number): number {
+ if (x === 0 || y === 0) return 0;
+ return EXP_TABLE[LOG_TABLE[x] + LOG_TABLE[y]];
+}
+
+// Generator polynomials for Reed-Solomon codes
+function rsGeneratorPoly(degree: number): Uint8Array {
+ let poly = new Uint8Array([1]);
+ for (let i = 0; i < degree; i++) {
+ const next = new Uint8Array(poly.length + 1);
+ for (let j = 0; j < poly.length; j++) {
+ next[j] ^= gfMul(poly[j], EXP_TABLE[i]);
+ next[j + 1] ^= poly[j];
+ }
+ poly = next;
+ }
+ return poly;
+}
+
+function rsComputeRemainder(data: Uint8Array, ecLength: number): Uint8Array {
+ const gen = rsGeneratorPoly(ecLength);
+ const res = new Uint8Array(ecLength);
+ for (const byte of data) {
+ const factor = byte ^ res[0];
+ res.copyWithin(0, 1);
+ res[ecLength - 1] = 0;
+ for (let i = 0; i < ecLength; i++) {
+ res[i] ^= gfMul(gen[i], factor);
+ }
+ }
+ return res;
+}
+
+// Version table capacities for Byte Mode (Level L)
+// Version 1..10 capacities and EC parameters
+interface QrVersionSpec {
+ version: number;
+ totalCodewords: number;
+ ecCodewords: number;
+ dataCodewords: number;
+ blocks: number;
+ alignment: number[];
+}
+
+const VERSION_SPECS: QrVersionSpec[] = [
+ { version: 1, totalCodewords: 26, ecCodewords: 7, dataCodewords: 19, blocks: 1, alignment: [] },
+ { version: 2, totalCodewords: 44, ecCodewords: 10, dataCodewords: 34, blocks: 1, alignment: [6, 18] },
+ { version: 3, totalCodewords: 70, ecCodewords: 15, dataCodewords: 55, blocks: 1, alignment: [6, 22] },
+ { version: 4, totalCodewords: 100, ecCodewords: 20, dataCodewords: 80, blocks: 1, alignment: [6, 26] },
+ { version: 5, totalCodewords: 134, ecCodewords: 26, dataCodewords: 108, blocks: 1, alignment: [6, 30] },
+ { version: 6, totalCodewords: 172, ecCodewords: 36, dataCodewords: 136, blocks: 2, alignment: [6, 34] },
+ { version: 7, totalCodewords: 196, ecCodewords: 40, dataCodewords: 156, blocks: 2, alignment: [6, 22, 38] },
+ { version: 8, totalCodewords: 242, ecCodewords: 48, dataCodewords: 194, blocks: 2, alignment: [6, 24, 42] },
+ { version: 9, totalCodewords: 292, ecCodewords: 60, dataCodewords: 232, blocks: 2, alignment: [6, 26, 46] },
+ { version: 10, totalCodewords: 346, ecCodewords: 72, dataCodewords: 274, blocks: 4, alignment: [6, 28, 50] },
+];
+
+export interface QrMatrix {
+ size: number;
+ modules: boolean[][];
+}
+
+/**
+ * Encodes text into a QR code boolean matrix (Version 1-10, EC Level L).
+ */
+export function encodeQr(text: string): QrMatrix {
+ const utf8 = new TextEncoder().encode(text);
+ const len = utf8.length;
+
+ // Find lowest suitable version
+ const spec = VERSION_SPECS.find((s) => s.dataCodewords >= len + 3);
+ if (!spec) {
+ throw new Error(`Text too long for supported QR versions (max ~270 bytes)`);
+ }
+
+ // 1. Bitstream assembly: Mode indicator (0100 for Byte mode) + character count + data
+ const bits: number[] = [];
+ function pushBits(val: number, count: number) {
+ for (let i = count - 1; i >= 0; i--) {
+ bits.push((val >>> i) & 1);
+ }
+ }
+
+ pushBits(0b0100, 4); // Byte mode indicator
+ pushBits(len, spec.version < 10 ? 8 : 16); // Character count indicator
+ for (const byte of utf8) {
+ pushBits(byte, 8);
+ }
+
+ // Terminator (up to 4 zeroes)
+ const maxBits = spec.dataCodewords * 8;
+ const termLen = Math.min(4, maxBits - bits.length);
+ pushBits(0, termLen);
+
+ // Pad to multiple of 8
+ while (bits.length % 8 !== 0) {
+ bits.push(0);
+ }
+
+ // Pad bytes (0xEC, 0x11 alternating)
+ const dataBytes = new Uint8Array(spec.dataCodewords);
+ for (let i = 0; i < bits.length / 8; i++) {
+ let b = 0;
+ for (let j = 0; j < 8; j++) {
+ b = (b << 1) | bits[i * 8 + j];
+ }
+ dataBytes[i] = b;
+ }
+
+ const pad = [0xec, 0x11];
+ let padIdx = 0;
+ for (let i = bits.length / 8; i < spec.dataCodewords; i++) {
+ dataBytes[i] = pad[padIdx % 2];
+ padIdx++;
+ }
+
+ // 2. Reed-Solomon Error Correction
+ const ecLengthPerBlock = Math.floor(spec.ecCodewords / spec.blocks);
+ const dataLengthPerBlock = Math.floor(spec.dataCodewords / spec.blocks);
+ const allEcBlocks: Uint8Array[] = [];
+ const allDataBlocks: Uint8Array[] = [];
+
+ for (let b = 0; b < spec.blocks; b++) {
+ const start = b * dataLengthPerBlock;
+ const blockData = dataBytes.slice(start, start + dataLengthPerBlock);
+ allDataBlocks.push(blockData);
+ allEcBlocks.push(rsComputeRemainder(blockData, ecLengthPerBlock));
+ }
+
+ // Interleave data and EC codewords
+ const finalCodewords: number[] = [];
+ for (let i = 0; i < dataLengthPerBlock; i++) {
+ for (let b = 0; b < spec.blocks; b++) {
+ finalCodewords.push(allDataBlocks[b][i]);
+ }
+ }
+ for (let i = 0; i < ecLengthPerBlock; i++) {
+ for (let b = 0; b < spec.blocks; b++) {
+ finalCodewords.push(allEcBlocks[b][i]);
+ }
+ }
+
+ // 3. Matrix construction
+ const size = 17 + 4 * spec.version;
+ const matrix: boolean[][] = Array.from({ length: size }, () => Array(size).fill(false));
+ const isFunction: boolean[][] = Array.from({ length: size }, () => Array(size).fill(false));
+
+ function setFunctionModule(r: number, c: number, val: boolean) {
+ matrix[r][c] = val;
+ isFunction[r][c] = true;
+ }
+
+ // Finder patterns at (0,0), (size-7, 0), (0, size-7)
+ function drawFinderPattern(row: number, col: number) {
+ for (let r = -1; r <= 7; r++) {
+ for (let c = -1; c <= 7; c++) {
+ const nr = row + r;
+ const nc = col + c;
+ if (nr < 0 || nr >= size || nc < 0 || nc >= size) continue;
+ const isBlack =
+ (r >= 0 && r <= 6 && (c === 0 || c === 6)) ||
+ (c >= 0 && c <= 6 && (r === 0 || r === 6)) ||
+ (r >= 2 && r <= 4 && c >= 2 && c <= 4);
+ setFunctionModule(nr, nc, isBlack);
+ }
+ }
+ }
+
+ drawFinderPattern(0, 0);
+ drawFinderPattern(0, size - 7);
+ drawFinderPattern(size - 7, 0);
+
+ // Alignment patterns for version >= 2
+ const coords = spec.alignment;
+ for (let i = 0; i < coords.length; i++) {
+ for (let j = 0; j < coords.length; j++) {
+ const r = coords[i];
+ const c = coords[j];
+ if (isFunction[r][c]) continue;
+ for (let dr = -2; dr <= 2; dr++) {
+ for (let dc = -2; dc <= 2; dc++) {
+ const isBlack = Math.max(Math.abs(dr), Math.abs(dc)) !== 1;
+ setFunctionModule(r + dr, c + dc, isBlack);
+ }
+ }
+ }
+ }
+
+ // Timing patterns
+ for (let i = 8; i < size - 8; i++) {
+ if (!isFunction[6][i]) setFunctionModule(6, i, i % 2 === 0);
+ if (!isFunction[i][6]) setFunctionModule(i, 6, i % 2 === 0);
+ }
+
+ // Dark module
+ setFunctionModule(4 * spec.version + 9, 8, true);
+
+ // Format info reserve
+ for (let i = 0; i <= 8; i++) {
+ if (!isFunction[8][i]) isFunction[8][i] = true;
+ if (!isFunction[i][8]) isFunction[i][8] = true;
+ }
+ for (let i = size - 8; i < size; i++) {
+ if (!isFunction[8][i]) isFunction[8][i] = true;
+ if (!isFunction[i][8]) isFunction[i][8] = true;
+ }
+
+ // 4. Place data bits with Mask 0 ((row + col) % 2 === 0)
+ const bitArray: number[] = [];
+ for (const byte of finalCodewords) {
+ for (let i = 7; i >= 0; i--) {
+ bitArray.push((byte >>> i) & 1);
+ }
+ }
+
+ let bitIdx = 0;
+ let upwards = true;
+ for (let right = size - 1; right > 0; right -= 2) {
+ if (right === 6) right--; // Skip vertical timing column
+ const rows = upwards
+ ? Array.from({ length: size }, (_, i) => size - 1 - i)
+ : Array.from({ length: size }, (_, i) => i);
+
+ for (const r of rows) {
+ for (const c of [right, right - 1]) {
+ if (!isFunction[r][c]) {
+ const bit = bitIdx < bitArray.length ? bitArray[bitIdx++] : 0;
+ const mask = (r + c) % 2 === 0;
+ matrix[r][c] = (bit ^ (mask ? 1 : 0)) === 1;
+ }
+ }
+ }
+ upwards = !upwards;
+ }
+
+ // 5. Write Format Information (Level L, Mask 0: bits 111011111000100)
+ const formatBits = [1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0];
+ for (let i = 0; i < 6; i++) matrix[8][i] = formatBits[i] === 1;
+ matrix[8][7] = formatBits[6] === 1;
+ matrix[8][8] = formatBits[7] === 1;
+ matrix[7][8] = formatBits[8] === 1;
+ for (let i = 9; i < 15; i++) matrix[14 - i][8] = formatBits[i] === 1;
+
+ for (let i = 0; i < 8; i++) matrix[size - 1 - i][8] = formatBits[i] === 1;
+ for (let i = 8; i < 15; i++) matrix[8][size - 15 + i] = formatBits[i] === 1;
+
+ return { size, modules: matrix };
+}
+
+/**
+ * Generates an SVG path data string for rendering the QR matrix cleanly.
+ */
+export function qrToSvgPath(matrix: QrMatrix): string {
+ const parts: string[] = [];
+ const { size, modules } = matrix;
+ for (let r = 0; r < size; r++) {
+ for (let c = 0; c < size; c++) {
+ if (modules[r][c]) {
+ parts.push(`M${c},${r}h1v1h-1z`);
+ }
+ }
+ }
+ return parts.join("");
+}