@tangle-network/agent-app 0.45.28 → 0.45.30
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/dist/{DesignCanvas-3RRPVPCP.js → DesignCanvas-RSWQCXM3.js} +2 -2
- package/dist/{DesignCanvasEditor-KAMMEX7E.js → DesignCanvasEditor-XZMGIST7.js} +4 -4
- package/dist/{use-file-mentions-CZ-Ua_sb.d.ts → agent-session-controls-BwImzYKC.d.ts} +32 -1
- package/dist/alerting/index.d.ts +178 -0
- package/dist/alerting/index.js +9 -0
- package/dist/alerting/index.js.map +1 -0
- package/dist/assistant/index.d.ts +65 -42
- package/dist/assistant/index.js +31 -12
- package/dist/assistant/index.js.map +1 -1
- package/dist/chat-react/index.d.ts +20 -140
- package/dist/chat-react/index.js +15 -133
- package/dist/chat-react/index.js.map +1 -1
- package/dist/chat-routes/index.js +7 -7
- package/dist/chunk-COP2K4LF.js +190 -0
- package/dist/chunk-COP2K4LF.js.map +1 -0
- package/dist/{chunk-NC2V63EL.js → chunk-FNM7JYME.js} +88 -21
- package/dist/chunk-FNM7JYME.js.map +1 -0
- package/dist/{chunk-WC43OQMR.js → chunk-IFYJDX3J.js} +867 -1260
- package/dist/chunk-IFYJDX3J.js.map +1 -0
- package/dist/chunk-L7FHRORY.js +763 -0
- package/dist/chunk-L7FHRORY.js.map +1 -0
- package/dist/{chunk-4QVG4WJQ.js → chunk-OYTMILRK.js} +10 -3
- package/dist/chunk-OYTMILRK.js.map +1 -0
- package/dist/{chunk-OYGM5WLD.js → chunk-PURHUAOR.js} +52 -15
- package/dist/chunk-PURHUAOR.js.map +1 -0
- package/dist/{chunk-AWPK7ZDS.js → chunk-QGOU3VXH.js} +3 -3
- package/dist/{chunk-OTLEYHOG.js → chunk-RRH23CGS.js} +24 -4
- package/dist/chunk-RRH23CGS.js.map +1 -0
- package/dist/{chunk-5FAKBTCK.js → chunk-XDB7IBYE.js} +22 -1
- package/dist/chunk-XDB7IBYE.js.map +1 -0
- package/dist/design-canvas-react/engine.d.ts +15 -1
- package/dist/design-canvas-react/engine.js +6 -2
- package/dist/design-canvas-react/index.d.ts +13 -1
- package/dist/design-canvas-react/index.js +13 -8
- package/dist/design-canvas-react/index.js.map +1 -1
- package/dist/design-canvas-react/lazy.js +1 -1
- package/dist/preflight/cli.js +2 -1
- package/dist/preflight/cli.js.map +1 -1
- package/dist/preflight/index.d.ts +35 -2
- package/dist/preflight/index.js +6 -3
- package/dist/sandbox/index.d.ts +11 -1
- package/dist/sandbox/index.js +1 -1
- package/dist/teams/index.js +5 -5
- package/dist/teams/invitations-api.js +4 -4
- package/dist/web-react/index.d.ts +26 -35
- package/dist/web-react/index.js +10 -10
- package/package.json +19 -3
- package/dist/chunk-4QVG4WJQ.js.map +0 -1
- package/dist/chunk-5FAKBTCK.js.map +0 -1
- package/dist/chunk-E4DHYENH.js +0 -292
- package/dist/chunk-E4DHYENH.js.map +0 -1
- package/dist/chunk-NC2V63EL.js.map +0 -1
- package/dist/chunk-OTLEYHOG.js.map +0 -1
- package/dist/chunk-OYGM5WLD.js.map +0 -1
- package/dist/chunk-WC43OQMR.js.map +0 -1
- /package/dist/{DesignCanvas-3RRPVPCP.js.map → DesignCanvas-RSWQCXM3.js.map} +0 -0
- /package/dist/{DesignCanvasEditor-KAMMEX7E.js.map → DesignCanvasEditor-XZMGIST7.js.map} +0 -0
- /package/dist/{chunk-AWPK7ZDS.js.map → chunk-QGOU3VXH.js.map} +0 -0
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
checkSlackCredential
|
|
3
|
+
} from "./chunk-COP2K4LF.js";
|
|
4
|
+
|
|
1
5
|
// src/preflight/index.ts
|
|
2
6
|
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
3
7
|
function nowMs() {
|
|
@@ -175,6 +179,22 @@ function httpHeadProbe(config) {
|
|
|
175
179
|
}
|
|
176
180
|
};
|
|
177
181
|
}
|
|
182
|
+
function slackAlertProbe(config) {
|
|
183
|
+
const keyLabel = config.keySecret ?? "SLACK_BOT_TOKEN";
|
|
184
|
+
return {
|
|
185
|
+
name: config.name ?? "slack-alerting",
|
|
186
|
+
critical: config.critical ?? false,
|
|
187
|
+
run: async () => {
|
|
188
|
+
const verdict = await checkSlackCredential({
|
|
189
|
+
token: config.token,
|
|
190
|
+
timeoutMs: config.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
191
|
+
fetchImpl: config.fetchImpl
|
|
192
|
+
});
|
|
193
|
+
if (verdict.live) return { ok: true, detail: `live as ${verdict.botId} in ${verdict.team}` };
|
|
194
|
+
return { ok: false, detail: `${verdict.detail} (secret: ${keyLabel})` };
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
}
|
|
178
198
|
async function runOne(probe) {
|
|
179
199
|
const critical = probe.critical ?? true;
|
|
180
200
|
const start = nowMs();
|
|
@@ -245,7 +265,8 @@ export {
|
|
|
245
265
|
routerChatProbe,
|
|
246
266
|
sandboxAuthProbe,
|
|
247
267
|
httpHeadProbe,
|
|
268
|
+
slackAlertProbe,
|
|
248
269
|
runPreflight,
|
|
249
270
|
formatPreflightReport
|
|
250
271
|
};
|
|
251
|
-
//# sourceMappingURL=chunk-
|
|
272
|
+
//# sourceMappingURL=chunk-XDB7IBYE.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/preflight/index.ts"],"sourcesContent":["/**\n * `/preflight` — deploy-time secret-liveness probes.\n *\n * WHY THIS EXISTS: on 2026-07-15 four secrets were simultaneously dead in one\n * production day — a dead `SANDBOX_API_KEY`, a stale `SANDBOX_API_URL`, and a\n * dead LiteLLM router key + URL. Each one was present in `wrangler secret list`\n * (so nothing looked wrong) yet invalid against its live endpoint, and nothing\n * anywhere checked liveness. CI cannot hold production secrets, so this binds\n * at DEPLOY time instead: a product declares a handful of probes built from its\n * real env, the deploy workflow runs `agent-app-preflight` as a step, and a\n * dead secret fails the deploy with a message that names exactly which secret\n * to rotate.\n *\n * A probe is `{ name, run, critical? }`; `run()` returns `{ ok, detail? }`.\n * The standard builders (`requiredValueProbe`, `routerChatProbe`,\n * `sandboxAuthProbe`, `httpHeadProbe`, `slackAlertProbe`) each take explicit config — they read\n * nothing global — so the same probe runs identically in a deploy step, a test,\n * or a local check. `runPreflight` fans\n * the probes out, times each, and folds them into a pass/fail report: any\n * failed CRITICAL probe fails the whole run (probes are critical by default).\n *\n * Server-only: probes carry live API keys and hit live endpoints. This subpath\n * must never reach a browser bundle.\n */\n\n/** One probe's outcome. `detail` should name the secret to rotate on failure. */\nexport interface PreflightProbeResult {\n ok: boolean\n detail?: string\n}\n\n/**\n * A liveness probe. `run` performs one cheap live call and maps the result to\n * `{ ok, detail }`. `critical` defaults to `true` — a failed critical probe\n * fails the whole preflight (and the deploy).\n */\nexport interface PreflightProbe {\n name: string\n run: () => Promise<PreflightProbeResult>\n critical?: boolean\n}\n\n/** Per-probe verdict enriched with the resolved criticality and measured latency. */\nexport interface PreflightProbeVerdict {\n name: string\n ok: boolean\n critical: boolean\n latencyMs: number\n detail?: string\n}\n\n/** Aggregate of every probe verdict plus the overall pass/fail decision. */\nexport interface PreflightReport {\n /** `false` if any critical probe failed. */\n ok: boolean\n probes: PreflightProbeVerdict[]\n passed: number\n failed: number\n criticalFailures: number\n durationMs: number\n}\n\nimport { checkSlackCredential } from '../alerting/slack'\n\n/** Deploy-time deadline for a single probe. Cold upstreams are slow; a dead\n * endpoint should still fail fast, so 10s is the ceiling, not the target. */\nconst DEFAULT_TIMEOUT_MS = 10_000\n\nfunction nowMs(): number {\n return typeof performance !== 'undefined' ? performance.now() : Date.now()\n}\n\nfunction isAbortLike(err: unknown): boolean {\n return err instanceof Error && (err.name === 'TimeoutError' || err.name === 'AbortError')\n}\n\n/** Strip bearer tokens / key material before an upstream string is surfaced in\n * a report (deploy logs are not always private). */\nfunction sanitizeUpstreamMessage(input: unknown): string {\n const message = input instanceof Error ? input.message : String(input)\n return message\n .replace(/Bearer\\s+[^\\s]+/gi, 'Bearer [redacted]')\n .replace(/\\b(?:sk|pk|tc)[_-][A-Za-z0-9_-]{8,}\\b/g, '[redacted-key]')\n}\n\nfunction snippet(body: string): string {\n const trimmed = body.trim()\n if (!trimmed) return ''\n const clipped = trimmed.length > 180 ? `${trimmed.slice(0, 180)}…` : trimmed\n return `: ${sanitizeUpstreamMessage(clipped)}`\n}\n\ntype ProbeOutcome =\n | { kind: 'status'; status: number; bodyText: string }\n | { kind: 'timeout'; timeoutMs: number }\n | { kind: 'network'; message: string }\n\ninterface HttpProbeCall {\n fetchImpl: typeof fetch\n url: string\n method: string\n headers?: Record<string, string>\n body?: string\n timeoutMs: number\n}\n\n/** One live HTTP call, folded to a probe outcome. Never throws: a timeout, a\n * DNS/connection failure, and any thrown error all become an outcome so the\n * probe can classify them into an actionable detail. */\nasync function runHttp(call: HttpProbeCall): Promise<ProbeOutcome> {\n let response: Response\n try {\n response = await call.fetchImpl(call.url, {\n method: call.method,\n headers: call.headers,\n body: call.body,\n signal: AbortSignal.timeout(call.timeoutMs),\n })\n } catch (err) {\n if (isAbortLike(err)) return { kind: 'timeout', timeoutMs: call.timeoutMs }\n return { kind: 'network', message: sanitizeUpstreamMessage(err) }\n }\n let bodyText = ''\n try {\n bodyText = await response.text()\n } catch {\n bodyText = ''\n }\n return { kind: 'status', status: response.status, bodyText }\n}\n\ninterface AuthedClassifyContext {\n /** Full endpoint reached, for the message. */\n endpoint: string\n /** How to name the API-key secret when the endpoint reports auth failure. */\n keyLabel: string\n /** How to name the URL secret when the endpoint is unreachable. */\n urlLabel: string\n}\n\n/**\n * Shared classification for an authed liveness endpoint (router, sandbox):\n * 2xx → live; 401/403 → the KEY is dead, name it; 503 → the UPSTREAM is down,\n * the key still looks valid, don't rotate; timeout / unreachable → the URL is\n * likely stale, name it; anything else → an unexpected status with a snippet.\n */\nfunction classifyAuthed(outcome: ProbeOutcome, ctx: AuthedClassifyContext): PreflightProbeResult {\n switch (outcome.kind) {\n case 'status': {\n const { status, bodyText } = outcome\n if (status >= 200 && status < 300) return { ok: true, detail: `${status} OK` }\n if (status === 401 || status === 403) {\n return {\n ok: false,\n detail: `DEAD KEY — ${ctx.endpoint} returned ${status}; rotate ${ctx.keyLabel}`,\n }\n }\n if (status === 503) {\n return {\n ok: false,\n detail: `UPSTREAM DOWN — ${ctx.endpoint} returned 503; ${ctx.keyLabel} still looks valid, retry or check the provider (do NOT rotate)`,\n }\n }\n return { ok: false, detail: `UNEXPECTED ${status} from ${ctx.endpoint}${snippet(bodyText)}` }\n }\n case 'timeout':\n return {\n ok: false,\n detail: `TIMEOUT after ${outcome.timeoutMs}ms reaching ${ctx.endpoint} — check ${ctx.urlLabel}`,\n }\n case 'network':\n return {\n ok: false,\n detail: `UNREACHABLE ${ctx.endpoint} (${outcome.message}) — check ${ctx.urlLabel}`,\n }\n }\n}\n\nfunction trimTrailingSlash(url: string): string {\n return url.replace(/\\/+$/, '')\n}\n\n// --- Standard probe builders --------------------------------------------------\n\n/** Configuration for a required non-empty production value. */\nexport interface RequiredValueProbeConfig {\n /** Human-readable value name, normally the environment variable name. */\n name: string\n /** Value supplied by the caller. It is checked but never included in output. */\n value: string | null | undefined\n /** Default `true`. */\n critical?: boolean\n /** Failure detail. Defaults to `NAME is unset`. */\n missingDetail?: string\n}\n\n/**\n * Require a non-empty string without ever printing its value.\n *\n * This covers local signing keys and other values that have no external\n * endpoint to probe. Credentials with a live API should use a liveness probe\n * instead, because presence alone cannot detect an expired key.\n */\nexport function requiredValueProbe(config: RequiredValueProbeConfig): PreflightProbe {\n return {\n name: `required:${config.name}`,\n critical: config.critical,\n run: async () => {\n const ok = typeof config.value === 'string' && config.value.trim().length > 0\n return { ok, detail: ok ? undefined : (config.missingDetail ?? `${config.name} is unset`) }\n },\n }\n}\n\n/** Define configuration options for probing an LLM router with authentication and model details */\nexport interface RouterChatProbeConfig {\n /** LLM router base URL (LiteLLM / OpenAI-compatible), e.g. `https://router…`. */\n baseUrl: string\n apiKey: string\n /** A cheap model id available on the router. */\n model: string\n /** Probe name in the report. Default `'router-chat'`. */\n name?: string\n /** Default `true`. */\n critical?: boolean\n /** Env-var name of the API key, named verbatim in a dead-key failure. */\n keySecret?: string\n /** Env-var name of the base URL, named verbatim in an unreachable failure. */\n urlSecret?: string\n /** Per-probe deadline. Default 10s. */\n timeoutMs?: number\n /** Injection seam for tests; defaults to global `fetch`. */\n fetchImpl?: typeof fetch\n}\n\n/**\n * Probe an OpenAI-compatible LLM router with one cheap `POST /chat/completions`\n * (`max_tokens: 1`). 200 → live; 401/403 → dead router key; 503 → upstream\n * provider down (key still valid); timeout / unreachable → check the router URL.\n */\nexport function routerChatProbe(config: RouterChatProbeConfig): PreflightProbe {\n const keyLabel = config.keySecret ?? 'the router API key'\n const urlLabel = config.urlSecret ?? 'the router base URL'\n return {\n name: config.name ?? 'router-chat',\n critical: config.critical,\n run: async () => {\n const base = trimTrailingSlash(config.baseUrl)\n const endpoint = `${base}/chat/completions`\n const outcome = await runHttp({\n fetchImpl: config.fetchImpl ?? fetch,\n url: endpoint,\n method: 'POST',\n headers: {\n Authorization: `Bearer ${config.apiKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n model: config.model,\n messages: [{ role: 'user', content: 'ping' }],\n max_tokens: 1,\n }),\n timeoutMs: config.timeoutMs ?? DEFAULT_TIMEOUT_MS,\n })\n return classifyAuthed(outcome, { endpoint, keyLabel, urlLabel })\n },\n }\n}\n\n/** Define configuration options for probing sandbox authentication endpoints */\nexport interface SandboxAuthProbeConfig {\n /** Sandbox API base URL. */\n baseUrl: string\n apiKey: string\n /** Probe name in the report. Default `'sandbox-auth'`. */\n name?: string\n /** Default `true`. */\n critical?: boolean\n /** Env-var name of the API key, named verbatim in a dead-key failure. */\n keySecret?: string\n /** Env-var name of the base URL, named verbatim in an unreachable failure. */\n urlSecret?: string\n /** Per-probe deadline. Default 10s. */\n timeoutMs?: number\n /** Injection seam for tests; defaults to global `fetch`. */\n fetchImpl?: typeof fetch\n}\n\n/**\n * Probe the sandbox API with a cheap authed `GET /v1/sandboxes?limit=1`.\n * 200 → live; 401/403 → dead sandbox key; 503 → sandbox platform down (key\n * still valid); timeout / unreachable → check the sandbox URL.\n */\nexport function sandboxAuthProbe(config: SandboxAuthProbeConfig): PreflightProbe {\n const keyLabel = config.keySecret ?? 'the sandbox API key'\n const urlLabel = config.urlSecret ?? 'the sandbox base URL'\n return {\n name: config.name ?? 'sandbox-auth',\n critical: config.critical,\n run: async () => {\n const base = trimTrailingSlash(config.baseUrl)\n const endpoint = `${base}/v1/sandboxes?limit=1`\n const outcome = await runHttp({\n fetchImpl: config.fetchImpl ?? fetch,\n url: endpoint,\n method: 'GET',\n headers: { Authorization: `Bearer ${config.apiKey}` },\n timeoutMs: config.timeoutMs ?? DEFAULT_TIMEOUT_MS,\n })\n return classifyAuthed(outcome, { endpoint, keyLabel, urlLabel })\n },\n }\n}\n\n/** Define configuration options for performing an HTTP HEAD probe to check URL availability */\nexport interface HttpHeadProbeConfig {\n /** Probe name in the report. */\n name: string\n /** URL to `HEAD`. */\n url: string\n /**\n * Accepted status(es). A single number requires an exact match; an array\n * requires membership. Omitted → any 2xx/3xx (the host is up and the path\n * resolves) counts as live.\n */\n expectStatus?: number | number[]\n /** Default `true`. */\n critical?: boolean\n /** Env-var name of the URL, named verbatim in a failure. */\n urlSecret?: string\n /** Per-probe deadline. Default 10s. */\n timeoutMs?: number\n /** Injection seam for tests; defaults to global `fetch`. */\n fetchImpl?: typeof fetch\n}\n\nfunction statusMatches(status: number, expect?: number | number[]): boolean {\n if (expect === undefined) return status >= 200 && status < 400\n if (Array.isArray(expect)) return expect.includes(status)\n return status === expect\n}\n\nfunction describeExpected(expect?: number | number[]): string {\n if (expect === undefined) return '2xx/3xx'\n if (Array.isArray(expect)) return expect.join(' or ')\n return String(expect)\n}\n\n/**\n * Probe a plain reachability endpoint (e.g. a platform base URL) with a `HEAD`.\n * Confirms the URL is live and resolving — the class of failure behind a stale\n * platform URL that still sits in the secret store.\n */\nexport function httpHeadProbe(config: HttpHeadProbeConfig): PreflightProbe {\n const urlLabel = config.urlSecret ?? `the URL for ${config.name}`\n return {\n name: config.name,\n critical: config.critical,\n run: async () => {\n const outcome = await runHttp({\n fetchImpl: config.fetchImpl ?? fetch,\n url: config.url,\n method: 'HEAD',\n timeoutMs: config.timeoutMs ?? DEFAULT_TIMEOUT_MS,\n })\n switch (outcome.kind) {\n case 'status': {\n if (statusMatches(outcome.status, config.expectStatus)) {\n return { ok: true, detail: `${outcome.status} OK` }\n }\n return {\n ok: false,\n detail: `UNEXPECTED ${outcome.status} from ${config.url} (expected ${describeExpected(config.expectStatus)}) — check ${urlLabel}`,\n }\n }\n case 'timeout':\n return {\n ok: false,\n detail: `TIMEOUT after ${outcome.timeoutMs}ms reaching ${config.url} — check ${urlLabel}`,\n }\n case 'network':\n return {\n ok: false,\n detail: `UNREACHABLE ${config.url} (${outcome.message}) — check ${urlLabel}`,\n }\n }\n },\n }\n}\n\n/** Define configuration options for probing that the Slack alerting channel is live */\nexport interface SlackAlertProbeConfig {\n /** Slack bot token (`xoxb-…`). */\n token: string | undefined\n /** Probe name in the report. Default `'slack-alerting'`. */\n name?: string\n /**\n * Default `false`. A dead alerting channel is a serious finding but not a\n * reason to refuse a deploy — the deploy is often the fix for whatever the\n * alerts were about, and blocking it would make a broken alert channel an\n * outage of its own.\n */\n critical?: boolean\n /** Env-var name of the token, named verbatim in a dead-credential failure. */\n keySecret?: string\n /** Per-probe deadline. Default 10s. */\n timeoutMs?: number\n /** Injection seam for tests; defaults to global `fetch`. */\n fetchImpl?: typeof fetch\n}\n\n/**\n * Probe that the Slack alerting credential is alive, via `auth.test` — no\n * message is posted.\n *\n * This closes the gap that hid a total alerting outage: every Slack credential\n * the org held was revoked at once and nothing noticed, because an incoming\n * webhook is write-only (the only way to test one is to post to it) and the\n * secret sat in the store looking configured. A bot token can be ASKED, so a\n * deploy can answer the question nobody was asking.\n *\n * Non-critical by default — see `critical`.\n */\nexport function slackAlertProbe(config: SlackAlertProbeConfig): PreflightProbe {\n const keyLabel = config.keySecret ?? 'SLACK_BOT_TOKEN'\n return {\n name: config.name ?? 'slack-alerting',\n critical: config.critical ?? false,\n run: async () => {\n const verdict = await checkSlackCredential({\n token: config.token,\n timeoutMs: config.timeoutMs ?? DEFAULT_TIMEOUT_MS,\n fetchImpl: config.fetchImpl,\n })\n if (verdict.live) return { ok: true, detail: `live as ${verdict.botId} in ${verdict.team}` }\n return { ok: false, detail: `${verdict.detail} (secret: ${keyLabel})` }\n },\n }\n}\n\n// --- Runner + report ----------------------------------------------------------\n\nasync function runOne(probe: PreflightProbe): Promise<PreflightProbeVerdict> {\n const critical = probe.critical ?? true\n const start = nowMs()\n try {\n const result = await probe.run()\n return {\n name: probe.name,\n ok: result.ok,\n critical,\n latencyMs: Math.round(nowMs() - start),\n detail: result.detail,\n }\n } catch (err) {\n return {\n name: probe.name,\n ok: false,\n critical,\n latencyMs: Math.round(nowMs() - start),\n detail: `probe threw: ${sanitizeUpstreamMessage(err)}`,\n }\n }\n}\n\n/**\n * Run every probe (concurrently), time each, and fold into a report. The run\n * fails (`ok: false`) iff a critical probe fails; a failed non-critical probe\n * is a warning that does not block the deploy.\n */\nexport async function runPreflight(probes: PreflightProbe[]): Promise<PreflightReport> {\n const start = nowMs()\n const verdicts = await Promise.all(probes.map(runOne))\n const failed = verdicts.filter((v) => !v.ok)\n const criticalFailures = failed.filter((v) => v.critical).length\n return {\n ok: criticalFailures === 0,\n probes: verdicts,\n passed: verdicts.length - failed.length,\n failed: failed.length,\n criticalFailures,\n durationMs: Math.round(nowMs() - start),\n }\n}\n\ninterface FormatRow {\n status: string\n name: string\n latency: string\n detail: string\n}\n\n/** Render a report as an aligned, operator-readable table + verdict line. Pure\n * (no I/O) so it is trivially testable and reusable by the bin. */\nexport function formatPreflightReport(report: PreflightReport): string {\n const header: FormatRow = { status: 'STATUS', name: 'PROBE', latency: 'LATENCY', detail: 'DETAIL' }\n const rows: FormatRow[] = report.probes.map((p) => ({\n status: p.ok ? 'PASS' : p.critical ? 'FAIL' : 'WARN',\n name: p.name,\n latency: `${p.latencyMs}ms`,\n detail: p.detail ?? '',\n }))\n const statusW = Math.max(header.status.length, ...rows.map((r) => r.status.length))\n const nameW = Math.max(header.name.length, ...rows.map((r) => r.name.length))\n const latencyW = Math.max(header.latency.length, ...rows.map((r) => r.latency.length))\n const line = (r: FormatRow): string =>\n `${r.status.padEnd(statusW)} ${r.name.padEnd(nameW)} ${r.latency.padStart(latencyW)} ${r.detail}`.trimEnd()\n\n const out: string[] = [\n line(header),\n `${'-'.repeat(statusW)} ${'-'.repeat(nameW)} ${'-'.repeat(latencyW)} ------`,\n ...rows.map(line),\n '',\n ]\n if (report.ok) {\n const warn = report.failed > 0 ? ` (${report.failed} non-critical warning(s))` : ''\n out.push(`Preflight PASSED — ${report.passed}/${report.probes.length} probe(s) live${warn}`)\n } else {\n const dead = report.probes\n .filter((p) => !p.ok && p.critical)\n .map((p) => p.name)\n .join(', ')\n out.push(`Preflight FAILED — ${report.criticalFailures} critical probe(s) dead: ${dead}`)\n out.push('Rotate the secret named in each FAIL row above, then redeploy.')\n }\n return out.join('\\n')\n}\n"],"mappings":"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","names":[]}
|
|
@@ -267,6 +267,20 @@ declare function resolveExportParams(page: ScenePage, opts: {
|
|
|
267
267
|
includeBleed?: boolean;
|
|
268
268
|
preset?: ExportPreset;
|
|
269
269
|
}): ResolvedExportParams;
|
|
270
|
+
/** Hard ceiling for node cache rasterization. Without it a deep zoom (max 32)
|
|
271
|
+
* on a retina display would rasterize page-size elements into canvases beyond
|
|
272
|
+
* browser backing-store limits; 4x supersampling covers every realistic
|
|
273
|
+
* crispness-critical range (fit-page → 200% at dpr 2). */
|
|
274
|
+
declare const NODE_CACHE_PIXEL_RATIO_MAX = 4;
|
|
275
|
+
/**
|
|
276
|
+
* Pixel ratio a Konva node bitmap cache should rasterize at so the cached
|
|
277
|
+
* bitmap matches the screen exactly: the node's absolute scale (content-group
|
|
278
|
+
* zoom) × device pixel ratio. Konva's cache default ignores zoom, so the
|
|
279
|
+
* bitmap is re-scaled at draw time — bilinear-soft at fit-page zooms like
|
|
280
|
+
* 0.47, badly upscaled on zoom-in. ElementNode caches at this ratio and
|
|
281
|
+
* export.ts restores cleared caches at it, keeping one policy in one place.
|
|
282
|
+
*/
|
|
283
|
+
declare function resolveNodeCachePixelRatio(absoluteScale: number, devicePixelRatio: number): number;
|
|
270
284
|
/**
|
|
271
285
|
* Walk image nodes to find the src of any that may have caused a SecurityError
|
|
272
286
|
* when the stage called toDataURL. Returns the first offending src found, or
|
|
@@ -383,4 +397,4 @@ interface InsertTemplate {
|
|
|
383
397
|
* default so every canvas gets a usable set out of the box. */
|
|
384
398
|
declare const DEFAULT_INSERT_TEMPLATES: readonly InsertTemplate[];
|
|
385
399
|
|
|
386
|
-
export { type AddElementInput, type AddPageInput, type BindSlotInput, DEFAULT_INSERT_TEMPLATES, DUPLICATE_OFFSET, type DeleteElementInput, type DeletePageInput, type DuplicatePageInput, ExportCropRect, ExportPreset, type ExportViewSnapshot, type GroupElementsInput, type InsertPageGeometry, type InsertTemplate, MAX_INSERT_DIMENSION, type MarqueeSelectOptions, type MultiSetAttrsEntry, type NudgeDelta, type ReorderElementInput, type ReorderPageInput, type ResolvedExportParams, SceneCommand, type SetAttrsInput, type SetDocumentTitleInput, type SetPageGuidesInput, type SetPagePropsInput, SnapEngine, SnapTarget, type UngroupElementInput, type ZoomPanConfig, ZoomPanMath, addElementCommand, addPageCommand, bindSlotCommand, buildInsertImageOp, centeredPosition, collectGridTargets, createSnapEngine, createZoomPanMath, deleteElementCommand, deletePageCommand, documentCropToStageCoords, duplicatePageCommand, fittedSize, groupElementsCommand, hitTestPoint, identifyTaintedSrc, isCrossOriginSrc, isExportHiddenNodeName, marqueeSelect, mintElementId, multiSetAttrsCommand, nudgeDelta, reorderElementCommand, reorderPageCommand, resolveExportParams, setAttrsCommand, setDocumentTitleCommand, setPageGuidesCommand, setPagePropsCommand, ungroupElementCommand };
|
|
400
|
+
export { type AddElementInput, type AddPageInput, type BindSlotInput, DEFAULT_INSERT_TEMPLATES, DUPLICATE_OFFSET, type DeleteElementInput, type DeletePageInput, type DuplicatePageInput, ExportCropRect, ExportPreset, type ExportViewSnapshot, type GroupElementsInput, type InsertPageGeometry, type InsertTemplate, MAX_INSERT_DIMENSION, type MarqueeSelectOptions, type MultiSetAttrsEntry, NODE_CACHE_PIXEL_RATIO_MAX, type NudgeDelta, type ReorderElementInput, type ReorderPageInput, type ResolvedExportParams, SceneCommand, type SetAttrsInput, type SetDocumentTitleInput, type SetPageGuidesInput, type SetPagePropsInput, SnapEngine, SnapTarget, type UngroupElementInput, type ZoomPanConfig, ZoomPanMath, addElementCommand, addPageCommand, bindSlotCommand, buildInsertImageOp, centeredPosition, collectGridTargets, createSnapEngine, createZoomPanMath, deleteElementCommand, deletePageCommand, documentCropToStageCoords, duplicatePageCommand, fittedSize, groupElementsCommand, hitTestPoint, identifyTaintedSrc, isCrossOriginSrc, isExportHiddenNodeName, marqueeSelect, mintElementId, multiSetAttrsCommand, nudgeDelta, reorderElementCommand, reorderPageCommand, resolveExportParams, resolveNodeCachePixelRatio, setAttrsCommand, setDocumentTitleCommand, setPageGuidesCommand, setPagePropsCommand, ungroupElementCommand };
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
DEFAULT_INSERT_TEMPLATES,
|
|
4
4
|
DUPLICATE_OFFSET,
|
|
5
5
|
MAX_INSERT_DIMENSION,
|
|
6
|
+
NODE_CACHE_PIXEL_RATIO_MAX,
|
|
6
7
|
buildInsertImageOp,
|
|
7
8
|
centeredPosition,
|
|
8
9
|
collectGridTargets,
|
|
@@ -17,8 +18,9 @@ import {
|
|
|
17
18
|
marqueeSelect,
|
|
18
19
|
mintElementId,
|
|
19
20
|
nudgeDelta,
|
|
20
|
-
resolveExportParams
|
|
21
|
-
|
|
21
|
+
resolveExportParams,
|
|
22
|
+
resolveNodeCachePixelRatio
|
|
23
|
+
} from "../chunk-OYTMILRK.js";
|
|
22
24
|
import {
|
|
23
25
|
SCENE_COMMAND_HISTORY_LIMIT,
|
|
24
26
|
addElementCommand,
|
|
@@ -47,6 +49,7 @@ export {
|
|
|
47
49
|
DEFAULT_INSERT_TEMPLATES,
|
|
48
50
|
DUPLICATE_OFFSET,
|
|
49
51
|
MAX_INSERT_DIMENSION,
|
|
52
|
+
NODE_CACHE_PIXEL_RATIO_MAX,
|
|
50
53
|
SCENE_COMMAND_HISTORY_LIMIT,
|
|
51
54
|
addElementCommand,
|
|
52
55
|
addPageCommand,
|
|
@@ -75,6 +78,7 @@ export {
|
|
|
75
78
|
reorderElementCommand,
|
|
76
79
|
reorderPageCommand,
|
|
77
80
|
resolveExportParams,
|
|
81
|
+
resolveNodeCachePixelRatio,
|
|
78
82
|
scaleForPreset,
|
|
79
83
|
setAttrsCommand,
|
|
80
84
|
setDocumentTitleCommand,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { D as DesignCanvasProps, c as createSceneCommandStack, E as ExportTriggerOptions, a as DesignCanvasMode } from '../command-stack-TNPWJCzW.js';
|
|
2
2
|
export { A as ApplySceneResult, b as EditorSceneState, S as SCENE_COMMAND_HISTORY_LIMIT, d as SceneCommand, e as SceneCommandStack, f as SceneCommandStackWithReapply, g as SnapEngine, h as SnapResult, i as SnapTarget, j as SnapTargetKind, k as SnapTargets, Z as ZoomPanMath } from '../command-stack-TNPWJCzW.js';
|
|
3
3
|
import { InsertPageGeometry, InsertTemplate, MultiSetAttrsEntry } from './engine.js';
|
|
4
|
-
export { AddElementInput, AddPageInput, BindSlotInput, DEFAULT_INSERT_TEMPLATES, DUPLICATE_OFFSET, DeleteElementInput, DeletePageInput, DuplicatePageInput, ExportViewSnapshot, GroupElementsInput, MAX_INSERT_DIMENSION, MarqueeSelectOptions, NudgeDelta, ReorderElementInput, ReorderPageInput, ResolvedExportParams, SetAttrsInput, SetDocumentTitleInput, SetPageGuidesInput, SetPagePropsInput, UngroupElementInput, ZoomPanConfig, addElementCommand, addPageCommand, bindSlotCommand, buildInsertImageOp, centeredPosition, collectGridTargets, createSnapEngine, createZoomPanMath, deleteElementCommand, deletePageCommand, documentCropToStageCoords, duplicatePageCommand, fittedSize, groupElementsCommand, hitTestPoint, identifyTaintedSrc, isCrossOriginSrc, isExportHiddenNodeName, marqueeSelect, mintElementId, multiSetAttrsCommand, nudgeDelta, reorderElementCommand, reorderPageCommand, resolveExportParams, setAttrsCommand, setDocumentTitleCommand, setPageGuidesCommand, setPagePropsCommand, ungroupElementCommand } from './engine.js';
|
|
4
|
+
export { AddElementInput, AddPageInput, BindSlotInput, DEFAULT_INSERT_TEMPLATES, DUPLICATE_OFFSET, DeleteElementInput, DeletePageInput, DuplicatePageInput, ExportViewSnapshot, GroupElementsInput, MAX_INSERT_DIMENSION, MarqueeSelectOptions, NODE_CACHE_PIXEL_RATIO_MAX, NudgeDelta, ReorderElementInput, ReorderPageInput, ResolvedExportParams, SetAttrsInput, SetDocumentTitleInput, SetPageGuidesInput, SetPagePropsInput, UngroupElementInput, ZoomPanConfig, addElementCommand, addPageCommand, bindSlotCommand, buildInsertImageOp, centeredPosition, collectGridTargets, createSnapEngine, createZoomPanMath, deleteElementCommand, deletePageCommand, documentCropToStageCoords, duplicatePageCommand, fittedSize, groupElementsCommand, hitTestPoint, identifyTaintedSrc, isCrossOriginSrc, isExportHiddenNodeName, marqueeSelect, mintElementId, multiSetAttrsCommand, nudgeDelta, reorderElementCommand, reorderPageCommand, resolveExportParams, resolveNodeCachePixelRatio, setAttrsCommand, setDocumentTitleCommand, setPageGuidesCommand, setPagePropsCommand, ungroupElementCommand } from './engine.js';
|
|
5
5
|
import { c as SceneDocument, S as ScenePage, a as SceneElement, P as PageBleed, b as PageGuides } from '../model-DnfO_PdJ.js';
|
|
6
6
|
import { f as ExportPreset } from '../export-presets-mgVulRaV.js';
|
|
7
7
|
export { d as ExportCropRect, h as bleedAwareExportBounds, s as scaleForPreset } from '../export-presets-mgVulRaV.js';
|
|
@@ -31,6 +31,18 @@ interface KonvaNodeLike {
|
|
|
31
31
|
visible(): boolean;
|
|
32
32
|
visible(v: boolean): void;
|
|
33
33
|
getAttr(key: string): unknown;
|
|
34
|
+
isCached(): boolean;
|
|
35
|
+
clearCache(): void;
|
|
36
|
+
cache(config?: {
|
|
37
|
+
pixelRatio?: number;
|
|
38
|
+
}): void;
|
|
39
|
+
getAbsoluteScale(): {
|
|
40
|
+
x: number;
|
|
41
|
+
y: number;
|
|
42
|
+
};
|
|
43
|
+
/** Konva.Container only — leaf shapes lack it; the cache walk treats its
|
|
44
|
+
* absence as "no children". */
|
|
45
|
+
getChildren?(): KonvaNodeLike[];
|
|
34
46
|
}
|
|
35
47
|
interface KonvaLayerLike {
|
|
36
48
|
getChildren(): KonvaNodeLike[];
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
downloadDataUrl,
|
|
13
13
|
exportDocumentJson,
|
|
14
14
|
exportPageDataUrl
|
|
15
|
-
} from "../chunk-
|
|
15
|
+
} from "../chunk-PURHUAOR.js";
|
|
16
16
|
import {
|
|
17
17
|
BTN,
|
|
18
18
|
BTN_ACTIVE,
|
|
@@ -41,13 +41,14 @@ import {
|
|
|
41
41
|
buildRulerTicks,
|
|
42
42
|
formatRulerLabel,
|
|
43
43
|
selectTickStep
|
|
44
|
-
} from "../chunk-
|
|
44
|
+
} from "../chunk-FNM7JYME.js";
|
|
45
45
|
import "../chunk-LSQR6HM6.js";
|
|
46
46
|
import "../chunk-ZN5J47UX.js";
|
|
47
47
|
import {
|
|
48
48
|
DEFAULT_INSERT_TEMPLATES,
|
|
49
49
|
DUPLICATE_OFFSET,
|
|
50
50
|
MAX_INSERT_DIMENSION,
|
|
51
|
+
NODE_CACHE_PIXEL_RATIO_MAX,
|
|
51
52
|
buildInsertImageOp,
|
|
52
53
|
centeredPosition,
|
|
53
54
|
collectGridTargets,
|
|
@@ -62,8 +63,9 @@ import {
|
|
|
62
63
|
marqueeSelect,
|
|
63
64
|
mintElementId,
|
|
64
65
|
nudgeDelta,
|
|
65
|
-
resolveExportParams
|
|
66
|
-
|
|
66
|
+
resolveExportParams,
|
|
67
|
+
resolveNodeCachePixelRatio
|
|
68
|
+
} from "../chunk-OYTMILRK.js";
|
|
67
69
|
import {
|
|
68
70
|
SCENE_COMMAND_HISTORY_LIMIT,
|
|
69
71
|
addElementCommand,
|
|
@@ -91,7 +93,7 @@ import {
|
|
|
91
93
|
import {
|
|
92
94
|
DesignCanvasChromeLazy,
|
|
93
95
|
DesignCanvasLazy
|
|
94
|
-
} from "../chunk-
|
|
96
|
+
} from "../chunk-QGOU3VXH.js";
|
|
95
97
|
import "../chunk-F7VH4G5C.js";
|
|
96
98
|
|
|
97
99
|
// src/design-canvas-react/components/CanvasInsertPanel.tsx
|
|
@@ -283,7 +285,7 @@ function CanvasInsertPanel({
|
|
|
283
285
|
setDragOver(false);
|
|
284
286
|
if (e.dataTransfer?.files?.length) void handleFiles(e.dataTransfer.files);
|
|
285
287
|
},
|
|
286
|
-
className: `flex flex-col items-center justify-center gap-2 rounded-lg border-2 border-dashed px-4 py-8 text-center transition-colors disabled:opacity-50 ${dragOver ? "border-[var(--brand-primary)] bg-[var(--brand-primary)]
|
|
288
|
+
className: `flex flex-col items-center justify-center gap-2 rounded-lg border-2 border-dashed px-4 py-8 text-center transition-colors disabled:opacity-50 ${dragOver ? "border-[var(--brand-primary)] bg-[color-mix(in_srgb,var(--brand-primary)_5%,transparent)]" : "border-[var(--border-default)] hover:border-[color-mix(in_srgb,var(--brand-primary)_40%,transparent)]"}`,
|
|
287
289
|
children: [
|
|
288
290
|
busy ? /* @__PURE__ */ jsx(SpinnerGlyph, { className: "h-6 w-6 text-[var(--brand-primary)]" }) : /* @__PURE__ */ jsx(UploadGlyph, { className: "h-6 w-6 text-[var(--text-muted)]" }),
|
|
289
291
|
/* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-[var(--text-primary)]", children: busy ? "Uploading\u2026" : "Drop an image or click to upload" }),
|
|
@@ -312,7 +314,7 @@ function CanvasInsertPanel({
|
|
|
312
314
|
type: "button",
|
|
313
315
|
disabled: busy,
|
|
314
316
|
onClick: () => void runInsert(() => tpl.build(page)),
|
|
315
|
-
className: "flex h-20 flex-col items-center justify-center gap-1.5 rounded-md border border-[var(--border-default)] bg-[var(--bg-input)] text-xs font-medium text-[var(--text-primary)] transition-colors hover:border-[var(--brand-primary)]
|
|
317
|
+
className: "flex h-20 flex-col items-center justify-center gap-1.5 rounded-md border border-[var(--border-default)] bg-[var(--bg-input)] text-xs font-medium text-[var(--text-primary)] transition-colors hover:border-[color-mix(in_srgb,var(--brand-primary)_40%,transparent)] disabled:opacity-50",
|
|
316
318
|
children: [
|
|
317
319
|
/* @__PURE__ */ jsx("span", { className: "flex h-7 items-center justify-center", children: /* @__PURE__ */ jsx(TemplatePreview, { shape: templateShape(tpl) }) }),
|
|
318
320
|
/* @__PURE__ */ jsx("span", { children: tpl.label })
|
|
@@ -409,6 +411,7 @@ function LayersPanel({ page, selectedElementIds, canWrite, onSetAttrs, onReorder
|
|
|
409
411
|
return /* @__PURE__ */ jsxs2("div", { className: "flex h-full flex-col overflow-hidden text-[var(--text-primary)]", children: [
|
|
410
412
|
/* @__PURE__ */ jsx2("div", { className: "shrink-0 border-b border-[var(--border-default)] px-3 py-1.5 text-[11px] font-medium uppercase tracking-wide text-[var(--text-muted)]", children: "Layers" }),
|
|
411
413
|
/* @__PURE__ */ jsxs2("div", { className: "min-h-0 flex-1 overflow-y-auto", children: [
|
|
414
|
+
visible.length === 0 ? /* @__PURE__ */ jsx2("div", { className: "px-3 py-2 text-[11px] text-[var(--text-muted)]", children: "No layers yet" }) : null,
|
|
412
415
|
visible.map((row) => {
|
|
413
416
|
const { element } = row;
|
|
414
417
|
const isSelected = selectedElementIds.includes(element.id);
|
|
@@ -446,7 +449,7 @@ function LayersPanel({ page, selectedElementIds, canWrite, onSetAttrs, onReorder
|
|
|
446
449
|
},
|
|
447
450
|
className: [
|
|
448
451
|
"group flex items-center gap-1.5 py-1 pr-2 text-[13px] transition-colors",
|
|
449
|
-
isSelected ? "bg-[var(--brand-primary)]
|
|
452
|
+
isSelected ? "bg-[color-mix(in_srgb,var(--brand-primary)_15%,transparent)] text-[var(--text-primary)]" : "hover:bg-[color-mix(in_srgb,var(--border-default)_40%,transparent)] text-[var(--text-secondary)]",
|
|
450
453
|
dragOverIndex === row.ownerIndex ? "border-t border-[var(--brand-primary)]" : ""
|
|
451
454
|
].join(" "),
|
|
452
455
|
style: { paddingLeft: 8 + row.depth * INDENT_PX },
|
|
@@ -556,6 +559,7 @@ export {
|
|
|
556
559
|
LAYERS_PANEL_ROW_LIMIT,
|
|
557
560
|
LayersPanel,
|
|
558
561
|
MAX_INSERT_DIMENSION,
|
|
562
|
+
NODE_CACHE_PIXEL_RATIO_MAX,
|
|
559
563
|
PagesStrip,
|
|
560
564
|
Rulers,
|
|
561
565
|
SCENE_COMMAND_HISTORY_LIMIT,
|
|
@@ -600,6 +604,7 @@ export {
|
|
|
600
604
|
reorderElementCommand,
|
|
601
605
|
reorderPageCommand,
|
|
602
606
|
resolveExportParams,
|
|
607
|
+
resolveNodeCachePixelRatio,
|
|
603
608
|
scaleForPreset,
|
|
604
609
|
selectTickStep,
|
|
605
610
|
setAttrsCommand,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/design-canvas-react/components/CanvasInsertPanel.tsx","../../src/design-canvas-react/components/LayersPanel.tsx","../../src/design-canvas-react/components/layer-tree.ts"],"sourcesContent":["/**\n * The human \"add element to the canvas\" side panel — a reusable left rail any\n * canvas consumer can mount so a person (not just the agent) can put images,\n * shapes, and text on the canvas. Designed for the `renderSidePanel` slot of\n * {@link DesignCanvasProps}; pass `renderSidePanel={(ctx) => <CanvasInsertPanel … />}`.\n *\n * Endpoints are per-app, so every I/O path is a callback — no product route is\n * hardcoded:\n * - `onUploadImage(file) => Promise<url>` — optional. When provided, the host\n * stores the file and returns the src to insert. The url MUST be http(s) or\n * a rooted `/api/` path (enforced by `assertSceneMediaSrc` before insertion);\n * a `data:` url is rejected, matching the scene model's media boundary.\n * - `loadGenerations?()` — optional provider for \"already generated in this\n * workspace\" images; omit to hide the tab.\n * - `templates?` — optional template set; defaults to {@link DEFAULT_INSERT_TEMPLATES}.\n *\n * Insertion goes through `onInsert`, which the host wires to its\n * `onApplyOperations` pipeline (server-validated, undoable) — the same path\n * every other edit takes.\n *\n * Tokens/icons follow the canvas convention: CSS-var design tokens and inline\n * SVG glyphs, no icon-library or Tailwind-semantic-token dependency.\n */\n\nimport { useEffect, useRef, useState, type ReactElement } from 'react'\nimport type { SceneOperation } from '../../design-canvas/operations'\nimport { assertSceneMediaSrc } from '../../design-canvas/model'\nimport {\n DEFAULT_INSERT_TEMPLATES,\n buildInsertImageOp,\n type InsertPageGeometry,\n type InsertTemplate,\n} from '../insert-builders'\nimport { ImageGlyph, ShapesGlyph } from './glyphs'\n\n/** An already-generated image the panel can offer for one-click insertion. */\nexport interface InsertGeneration {\n id: string\n /** The image url to insert. Must satisfy the scene media boundary\n * (http(s) or rooted `/api/`); rejected otherwise at insert time. */\n url: string\n /** Optional prompt/label shown as the tile's title. */\n label?: string\n}\n\nexport interface CanvasInsertPanelProps {\n canWrite: boolean\n /** The active page new elements are added to. */\n page: InsertPageGeometry\n /** Submit operations through the host's apply pipeline. */\n onInsert(operations: SceneOperation[]): Promise<unknown>\n /** Store an uploaded file and return its src (http(s) or rooted `/api/`). */\n onUploadImage?(file: File): Promise<string>\n /** Optional provider for the Generations tab; omit to hide it. */\n loadGenerations?(): Promise<InsertGeneration[]>\n /** Drop-in templates; defaults to the built-in starter set. */\n templates?: readonly InsertTemplate[]\n /** Accepted upload mime types. Default: PNG/JPEG/GIF/WebP. */\n accept?: string\n className?: string\n}\n\ntype Tab = 'uploads' | 'templates' | 'generations'\n\nconst DEFAULT_ACCEPT = 'image/png,image/jpeg,image/gif,image/webp'\n\n/** Read an image url's natural dimensions in the browser; resolves `{0,0}` when\n * Image is unavailable (SSR/tests), the load fails, or the probe times out —\n * builders fall back to the size cap, so a failed probe still inserts a sensibly\n * sized element rather than blocking insertion. */\nconst PROBE_TIMEOUT_MS = 4000\n\nfunction probeImageSize(url: string): Promise<{ width: number; height: number }> {\n return new Promise((resolve) => {\n if (typeof Image === 'undefined') return resolve({ width: 0, height: 0 })\n let settled = false\n const finish = (size: { width: number; height: number }) => {\n if (settled) return\n settled = true\n resolve(size)\n }\n const img = new Image()\n img.onload = () => finish({ width: img.naturalWidth, height: img.naturalHeight })\n img.onerror = () => finish({ width: 0, height: 0 })\n // A URL that never loads (or an environment that never fires load/error)\n // must not hang the insert forever.\n setTimeout(() => finish({ width: 0, height: 0 }), PROBE_TIMEOUT_MS)\n img.src = url\n })\n}\n\nfunction UploadGlyph({ className }: { className?: string }) {\n return (\n <svg className={className} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden>\n <path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 9l5-5 5 5M12 4v12\" />\n </svg>\n )\n}\n\nfunction SparkleGlyph({ className }: { className?: string }) {\n return (\n <svg className={className} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden>\n <path d=\"M12 3v3m0 12v3M3 12h3m12 0h3M5.6 5.6l2.1 2.1m8.6 8.6 2.1 2.1m0-12.8-2.1 2.1M7.7 16.3l-2.1 2.1\" />\n </svg>\n )\n}\n\nfunction SpinnerGlyph({ className }: { className?: string }) {\n return (\n <svg className={`${className ?? ''} animate-spin`} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" aria-hidden>\n <path d=\"M21 12a9 9 0 1 1-6.2-8.6\" strokeLinecap=\"round\" />\n </svg>\n )\n}\n\n/** Probe-page geometry for deriving a template's primary element kind. The\n * result drives only the tile preview, so the exact dimensions don't matter. */\nconst PREVIEW_PROBE_PAGE: InsertPageGeometry = { pageId: 'preview', width: 1000, height: 1000 }\n\n/** The visual a template tile shows. Derived from the first element the\n * template's (pure) `build` produces, so custom templates render correctly\n * too — not just the built-in set. */\ntype TemplateShape = 'heading' | 'body' | 'rect' | 'ellipse' | 'other'\n\n/** A text element is treated as a heading (shown as \"T\") when it reads as a\n * title — bold or set in a large face; otherwise body copy (shown as \"¶\"). */\nconst HEADING_FONT_SIZE = 32\n\nfunction templateShape(tpl: InsertTemplate): TemplateShape {\n try {\n const ops = tpl.build(PREVIEW_PROBE_PAGE)\n const added = ops.find((op) => op.type === 'add_element')\n if (!added || added.type !== 'add_element') return 'other'\n const el = added.element\n if (el.kind === 'rect') return 'rect'\n if (el.kind === 'ellipse') return 'ellipse'\n if (el.kind === 'text') {\n const isHeading = el.fontStyle.includes('bold') || el.fontSize >= HEADING_FONT_SIZE\n return isHeading ? 'heading' : 'body'\n }\n return 'other'\n } catch {\n // A throwing build can't preview; fall through to the neutral shape glyph.\n return 'other'\n }\n}\n\n/** A small visual standing in for what the template inserts, so tiles are\n * distinguishable at a glance instead of four identical text chips. */\nfunction TemplatePreview({ shape }: { shape: TemplateShape }) {\n if (shape === 'rect') {\n return <span className=\"block h-6 w-9 rounded-sm bg-[var(--brand-primary)]\" aria-hidden />\n }\n if (shape === 'ellipse') {\n return <span className=\"block h-7 w-7 rounded-full bg-[var(--brand-primary)]\" aria-hidden />\n }\n // Text tiles show a glyph: a bold \"T\" for a heading, a paragraph mark \"¶\"\n // for body copy.\n if (shape === 'heading' || shape === 'body') {\n return (\n <span\n className={`block leading-none text-[var(--text-primary)] ${\n shape === 'heading' ? 'text-2xl font-bold' : 'text-lg'\n }`}\n aria-hidden\n >\n {shape === 'heading' ? 'T' : '¶'}\n </span>\n )\n }\n return <ShapesGlyph className=\"h-6 w-6 text-[var(--text-muted)]\" />\n}\n\nexport function CanvasInsertPanel({\n canWrite,\n page,\n onInsert,\n onUploadImage,\n loadGenerations,\n templates = DEFAULT_INSERT_TEMPLATES,\n accept = DEFAULT_ACCEPT,\n className,\n}: CanvasInsertPanelProps) {\n const [tab, setTab] = useState<Tab>(() => (templates.length > 0 ? 'templates' : 'uploads'))\n const [busy, setBusy] = useState(false)\n const [error, setError] = useState('')\n const [dragOver, setDragOver] = useState(false)\n const [generations, setGenerations] = useState<InsertGeneration[]>([])\n const [generationsLoaded, setGenerationsLoaded] = useState(false)\n const fileInputRef = useRef<HTMLInputElement | null>(null)\n\n useEffect(() => {\n if (tab === 'uploads' && !onUploadImage) {\n setTab(templates.length > 0 ? 'templates' : loadGenerations ? 'generations' : 'uploads')\n }\n if (tab === 'templates' && templates.length === 0) {\n setTab(onUploadImage ? 'uploads' : loadGenerations ? 'generations' : 'uploads')\n }\n if (tab === 'generations' && !loadGenerations) {\n setTab(templates.length > 0 ? 'templates' : onUploadImage ? 'uploads' : 'templates')\n }\n }, [tab, templates.length, onUploadImage, loadGenerations])\n\n useEffect(() => {\n if (tab !== 'generations' || generationsLoaded || !loadGenerations) return\n let cancelled = false\n void (async () => {\n try {\n const rows = await loadGenerations()\n if (!cancelled) setGenerations(rows.filter((g) => g.url))\n } catch (err) {\n if (!cancelled) setError(err instanceof Error ? err.message : 'Could not load generations')\n } finally {\n if (!cancelled) setGenerationsLoaded(true)\n }\n })()\n return () => {\n cancelled = true\n }\n }, [tab, generationsLoaded, loadGenerations])\n\n async function insertImageFromUrl(url: string) {\n // Validate the media boundary BEFORE probing — a data: or sandbox-local url\n // throws here (surfaced as an inline error) without a wasted image load.\n assertSceneMediaSrc(url, 'image src')\n const natural = await probeImageSize(url)\n await onInsert([buildInsertImageOp(url, natural, page)])\n }\n\n async function handleFiles(files: FileList | File[]) {\n if (!canWrite || busy) return\n if (!onUploadImage) return\n const list = Array.from(files).filter((f) => f.type.startsWith('image/'))\n if (list.length === 0) {\n setError('Only image files can be added to the canvas')\n return\n }\n setBusy(true)\n setError('')\n try {\n for (const file of list) {\n const url = await onUploadImage(file)\n await insertImageFromUrl(url)\n }\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Upload failed')\n } finally {\n setBusy(false)\n }\n }\n\n async function runInsert(build: () => SceneOperation[] | Promise<SceneOperation[]>) {\n if (!canWrite || busy) return\n setBusy(true)\n setError('')\n try {\n await onInsert(await build())\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Insert failed')\n } finally {\n setBusy(false)\n }\n }\n\n const tabs: Array<{ id: Tab; label: string; icon: (p: { className?: string }) => ReactElement; show: boolean }> = [\n { id: 'uploads', label: 'Uploads', icon: ImageGlyph, show: !!onUploadImage },\n { id: 'templates', label: 'Templates', icon: ShapesGlyph, show: templates.length > 0 },\n { id: 'generations', label: 'Generations', icon: SparkleGlyph, show: !!loadGenerations },\n ]\n const visibleTabs = tabs.filter((t) => t.show)\n\n return (\n <div className={`flex h-full min-h-0 flex-col bg-[var(--bg-input)] text-[var(--text-primary)] ${className ?? ''}`}>\n <div className=\"flex shrink-0 border-b border-[var(--border-default)]\">\n {visibleTabs.map(({ id, label, icon: Icon }) => (\n <button\n key={id}\n type=\"button\"\n onClick={() => setTab(id)}\n // These tabs sit flush against the top and side edges of a panel\n // that clips its overflow, so the floor's outward ring is cut off on\n // three sides and reads as a stray underline. The negative offset\n // draws the same ring inside the tab; width and colour still come\n // from the tokens.\n className={`flex flex-1 items-center justify-center gap-1.5 px-2 py-2.5 text-xs font-medium transition-colors focus-visible:[outline-offset:-2px] ${\n tab === id\n ? 'border-b-2 border-[var(--brand-primary)] text-[var(--text-primary)]'\n : 'text-[var(--text-muted)] hover:text-[var(--text-primary)]'\n }`}\n >\n <Icon className=\"h-3.5 w-3.5\" />\n {label}\n </button>\n ))}\n </div>\n\n {!canWrite ? (\n <div className=\"flex flex-1 items-center justify-center p-4 text-center text-sm text-[var(--text-muted)]\">\n You have view-only access to this design.\n </div>\n ) : (\n <div className=\"min-h-0 flex-1 overflow-y-auto p-3\">\n {tab === 'uploads' && (\n <div className=\"flex flex-col gap-3\">\n <button\n type=\"button\"\n disabled={busy}\n onClick={() => fileInputRef.current?.click()}\n onDragOver={(e) => {\n e.preventDefault()\n setDragOver(true)\n }}\n onDragLeave={() => setDragOver(false)}\n onDrop={(e) => {\n e.preventDefault()\n setDragOver(false)\n if (e.dataTransfer?.files?.length) void handleFiles(e.dataTransfer.files)\n }}\n className={`flex flex-col items-center justify-center gap-2 rounded-lg border-2 border-dashed px-4 py-8 text-center transition-colors disabled:opacity-50 ${\n dragOver\n ? 'border-[var(--brand-primary)] bg-[var(--brand-primary)]/5'\n : 'border-[var(--border-default)] hover:border-[var(--brand-primary)]/40'\n }`}\n >\n {busy ? (\n <SpinnerGlyph className=\"h-6 w-6 text-[var(--brand-primary)]\" />\n ) : (\n <UploadGlyph className=\"h-6 w-6 text-[var(--text-muted)]\" />\n )}\n <span className=\"text-sm font-medium text-[var(--text-primary)]\">\n {busy ? 'Uploading…' : 'Drop an image or click to upload'}\n </span>\n <span className=\"text-xs text-[var(--text-muted)]\">PNG, JPEG, GIF, or WebP</span>\n </button>\n <input\n ref={fileInputRef}\n type=\"file\"\n accept={accept}\n multiple\n className=\"hidden\"\n onChange={(e) => {\n if (e.target.files?.length) void handleFiles(e.target.files)\n e.target.value = ''\n }}\n />\n </div>\n )}\n\n {tab === 'templates' && (\n <div className=\"grid grid-cols-2 gap-2\">\n {templates.map((tpl) => (\n <button\n key={tpl.id}\n type=\"button\"\n disabled={busy}\n onClick={() => void runInsert(() => tpl.build(page))}\n className=\"flex h-20 flex-col items-center justify-center gap-1.5 rounded-md border border-[var(--border-default)] bg-[var(--bg-input)] text-xs font-medium text-[var(--text-primary)] transition-colors hover:border-[var(--brand-primary)]/40 disabled:opacity-50\"\n >\n <span className=\"flex h-7 items-center justify-center\">\n <TemplatePreview shape={templateShape(tpl)} />\n </span>\n <span>{tpl.label}</span>\n </button>\n ))}\n </div>\n )}\n\n {tab === 'generations' && (\n <div className=\"flex flex-col gap-2\">\n {!generationsLoaded ? (\n <div className=\"flex items-center justify-center py-8 text-[var(--text-muted)]\">\n <SpinnerGlyph className=\"h-5 w-5\" />\n </div>\n ) : generations.length === 0 ? (\n <p className=\"px-1 py-6 text-center text-sm text-[var(--text-muted)]\">\n No generated images yet. Ask the agent to generate one.\n </p>\n ) : (\n <div className=\"grid grid-cols-2 gap-2\">\n {generations.map((gen) => (\n <button\n key={gen.id}\n type=\"button\"\n disabled={busy}\n onClick={() => {\n if (busy || !canWrite) return\n setBusy(true)\n setError('')\n insertImageFromUrl(gen.url)\n .catch((err) => setError(err instanceof Error ? err.message : 'Insert failed'))\n .finally(() => setBusy(false))\n }}\n title={gen.label}\n className=\"group relative aspect-square overflow-hidden rounded-md border border-[var(--border-default)] bg-[var(--bg-input)] transition-colors hover:border-[var(--brand-primary)] disabled:opacity-50\"\n >\n {/* eslint-disable-next-line jsx-a11y/img-redundant-alt */}\n <img src={gen.url} alt={gen.label || 'Generated image'} className=\"h-full w-full object-cover\" />\n </button>\n ))}\n </div>\n )}\n </div>\n )}\n\n {error ? <p className=\"mt-3 text-xs leading-5 text-[var(--text-danger,#dc2626)]\">{error}</p> : null}\n </div>\n )}\n </div>\n )\n}\n","/**\n * Layers panel — reverse-z list of the active page's elements. Highest z-index\n * is at the top. Groups show their children indented beneath them. Clicking\n * selects an element; meta-click adds to selection. Double-clicking the name\n * opens an inline rename input. Eye and lock icons toggle visibility/locked.\n * Rows can be drag-reordered within the list (siblings only; cross-group reorder\n * is a pending integrator concern). Capped at LAYERS_PANEL_ROW_LIMIT rows.\n */\n\nimport { useCallback, useMemo, useRef, useState } from 'react'\nimport type { SceneElement, ScenePage } from '../../design-canvas/model'\nimport { flattenLayerTree, LAYERS_PANEL_ROW_LIMIT } from './layer-tree'\nimport {\n EllipseGlyph,\n EyeGlyph,\n EyeOffGlyph,\n GroupGlyph,\n ImageGlyph,\n LineGlyph,\n LockGlyph,\n RectGlyph,\n SlotGlyph,\n TextGlyph,\n UnlockGlyph,\n VideoGlyph,\n} from './glyphs'\nimport type { SceneElementKind } from '../../design-canvas/model'\n\nfunction KindIcon({ kind, className }: { kind: SceneElementKind; className?: string }) {\n switch (kind) {\n case 'rect': return <RectGlyph className={className} />\n case 'ellipse': return <EllipseGlyph className={className} />\n case 'line': return <LineGlyph className={className} />\n case 'text': return <TextGlyph className={className} />\n case 'image': return <ImageGlyph className={className} />\n case 'video': return <VideoGlyph className={className} />\n case 'group': return <GroupGlyph className={className} />\n }\n}\n\nexport interface LayersPanelProps {\n page: ScenePage\n selectedElementIds: string[]\n canWrite: boolean\n /** Emit a set_attrs command for the given element. */\n onSetAttrs(elementId: string, attrs: Partial<Pick<SceneElement, 'name' | 'visible' | 'locked'>>): void\n /** Emit a reorder_element command. */\n onReorder(elementId: string, toIndex: number): void\n onSelect(elementId: string, additive: boolean): void\n}\n\nconst INDENT_PX = 16\n\nexport function LayersPanel({ page, selectedElementIds, canWrite, onSetAttrs, onReorder, onSelect }: LayersPanelProps) {\n const rows = useMemo(() => flattenLayerTree(page), [page])\n const [renamingId, setRenamingId] = useState<string | null>(null)\n const [renameValue, setRenameValue] = useState('')\n\n // Drag-reorder state: we track the dragged row's ownerIndex and the target\n // index within the SAME owner. Cross-owner reorder is not supported here.\n const dragRowRef = useRef<{ elementId: string; ownerIndex: number; ownerLength: number } | null>(null)\n const [dragOverIndex, setDragOverIndex] = useState<number | null>(null)\n\n function startRename(element: SceneElement) {\n setRenamingId(element.id)\n setRenameValue(element.name)\n }\n\n function commitRename(elementId: string) {\n const name = renameValue.trim()\n if (name.length > 0) onSetAttrs(elementId, { name })\n setRenamingId(null)\n }\n\n const visible = rows.length > LAYERS_PANEL_ROW_LIMIT ? rows.slice(0, LAYERS_PANEL_ROW_LIMIT) : rows\n\n return (\n <div className=\"flex h-full flex-col overflow-hidden text-[var(--text-primary)]\">\n <div className=\"shrink-0 border-b border-[var(--border-default)] px-3 py-1.5 text-[11px] font-medium uppercase tracking-wide text-[var(--text-muted)]\">\n Layers\n </div>\n\n <div className=\"min-h-0 flex-1 overflow-y-auto\">\n {visible.map((row) => {\n const { element } = row\n const isSelected = selectedElementIds.includes(element.id)\n const isRenaming = renamingId === element.id\n\n return (\n <div\n key={element.id}\n data-layer-row={element.id}\n draggable={canWrite}\n onDragStart={() => {\n dragRowRef.current = {\n elementId: element.id,\n ownerIndex: row.ownerIndex,\n ownerLength: row.ownerLength,\n }\n }}\n onDragOver={(event) => {\n if (!dragRowRef.current) return\n // Only allow reorder within the same owner (same parentGroupId + depth)\n event.preventDefault()\n setDragOverIndex(row.ownerIndex)\n }}\n onDragLeave={() => setDragOverIndex(null)}\n onDrop={() => {\n const drag = dragRowRef.current\n if (!drag) return\n if (drag.elementId !== element.id) {\n onReorder(drag.elementId, row.ownerIndex)\n }\n dragRowRef.current = null\n setDragOverIndex(null)\n }}\n onDragEnd={() => {\n dragRowRef.current = null\n setDragOverIndex(null)\n }}\n className={[\n 'group flex items-center gap-1.5 py-1 pr-2 text-[13px] transition-colors',\n isSelected ? 'bg-[var(--brand-primary)]/15 text-[var(--text-primary)]' : 'hover:bg-[var(--border-default)]/40 text-[var(--text-secondary)]',\n dragOverIndex === row.ownerIndex ? 'border-t border-[var(--brand-primary)]' : '',\n ].join(' ')}\n style={{ paddingLeft: 8 + row.depth * INDENT_PX }}\n >\n {isRenaming ? (\n <input\n autoFocus\n value={renameValue}\n onChange={(event) => setRenameValue(event.target.value)}\n onBlur={() => commitRename(element.id)}\n onKeyDown={(event) => {\n if (event.key === 'Enter') commitRename(element.id)\n if (event.key === 'Escape') setRenamingId(null)\n event.stopPropagation()\n }}\n className=\"min-w-0 flex-1 rounded border border-[var(--border-default)] bg-[var(--bg-input)] px-1 py-0 text-[13px] text-[var(--text-primary)] focus:border-[var(--brand-primary)]\"\n />\n ) : (\n // Selection is a real button (sibling of the eye/lock buttons) so\n // the row is not an interactive control nesting interactive\n // controls (axe: nested-interactive).\n <button\n type=\"button\"\n onClick={(event) => onSelect(element.id, event.metaKey || event.ctrlKey)}\n onDoubleClick={() => {\n if (canWrite) startRename(element)\n }}\n title={element.name}\n className=\"flex min-w-0 flex-1 cursor-pointer items-center gap-1.5 bg-transparent text-left\"\n >\n <KindIcon kind={element.kind} className=\"h-3.5 w-3.5 shrink-0 opacity-60\" />\n {element.slot ? <SlotGlyph className=\"h-3 w-3 shrink-0 text-[var(--brand-primary)]\" /> : null}\n <span className=\"min-w-0 flex-1 truncate\">{element.name}</span>\n </button>\n )}\n\n {/* Visibility toggle */}\n <button\n type=\"button\"\n aria-label={element.visible ? 'Hide element' : 'Show element'}\n aria-pressed={!element.visible}\n onClick={(event) => {\n event.stopPropagation()\n if (canWrite) onSetAttrs(element.id, { visible: !element.visible })\n }}\n disabled={!canWrite}\n className={[\n 'shrink-0 rounded p-0.5 transition-opacity group-hover:opacity-100 hover:opacity-100 focus-visible:opacity-100',\n element.visible ? 'opacity-0' : 'opacity-100',\n ].join(' ')}\n >\n {element.visible\n ? <EyeGlyph className=\"h-3.5 w-3.5\" />\n : <EyeOffGlyph className=\"h-3.5 w-3.5 text-[var(--text-muted)]\" />}\n </button>\n\n {/* Lock toggle */}\n <button\n type=\"button\"\n aria-label={element.locked ? 'Unlock element' : 'Lock element'}\n aria-pressed={!!element.locked}\n onClick={(event) => {\n event.stopPropagation()\n if (canWrite) onSetAttrs(element.id, { locked: !element.locked })\n }}\n disabled={!canWrite}\n className={[\n 'shrink-0 rounded p-0.5 transition-opacity group-hover:opacity-100 hover:opacity-100 focus-visible:opacity-100',\n element.locked ? 'opacity-100' : 'opacity-0',\n ].join(' ')}\n >\n {element.locked\n ? <LockGlyph className=\"h-3.5 w-3.5 text-[var(--text-warning)]\" />\n : <UnlockGlyph className=\"h-3.5 w-3.5\" />}\n </button>\n </div>\n )\n })}\n\n {rows.length > LAYERS_PANEL_ROW_LIMIT ? (\n <div className=\"px-3 py-2 text-[11px] text-[var(--text-muted)]\">\n +{rows.length - LAYERS_PANEL_ROW_LIMIT} more elements — select a group to scope the list\n </div>\n ) : null}\n </div>\n </div>\n )\n}\n","/**\n * Pure layer-tree helpers: flatten a page's element array into an ordered list\n * for the LayersPanel, preserving group nesting. Extracted so render order,\n * depth, and selection math can be unit-tested independently of React.\n */\n\nimport type { SceneElement, ScenePage } from '../../design-canvas/model'\n\n/** Describe a layer row with its element, depth, grouping, ownership, and parent group details */\nexport interface LayerRow {\n element: SceneElement\n depth: number\n /** True when this row is a group whose children follow it in the list. */\n isGroup: boolean\n /** Index within its owner (page.elements or group.children). */\n ownerIndex: number\n /** Length of the owner array — needed for z-order bound math. */\n ownerLength: number\n /** Id of the containing group, or null for page-root elements. */\n parentGroupId: string | null\n}\n\n/**\n * Flatten `page.elements` into display order for the layers panel.\n *\n * Z-order is bottom→top in the element array; the layers panel shows\n * top→bottom (highest z-index first). A group row precedes its children\n * (the group is \"above\" its children in the panel, reflecting that the group\n * node itself paints last in Konva when empty; children paint inside it).\n *\n * Depth increases by 1 for each group level, starting at 0 for page-root\n * elements. Groups are expanded; collapsed state is a view concern the caller\n * tracks separately.\n */\nexport function flattenLayerTree(page: ScenePage): LayerRow[] {\n const rows: LayerRow[] = []\n\n function visit(elements: SceneElement[], depth: number, parentGroupId: string | null) {\n // Reverse so highest z-index appears at the top of the panel.\n for (let i = elements.length - 1; i >= 0; i -= 1) {\n const element = elements[i]!\n rows.push({\n element,\n depth,\n isGroup: element.kind === 'group',\n ownerIndex: i,\n ownerLength: elements.length,\n parentGroupId,\n })\n if (element.kind === 'group') {\n visit(element.children, depth + 1, element.id)\n }\n }\n }\n\n visit(page.elements, 0, null)\n return rows\n}\n\n/** The maximum number of rows the layers panel renders before truncating. */\nexport const LAYERS_PANEL_ROW_LIMIT = 500\n"],"mappings":"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","names":["useRef","useState","jsx","jsxs","useState","useRef"]}
|
|
1
|
+
{"version":3,"sources":["../../src/design-canvas-react/components/CanvasInsertPanel.tsx","../../src/design-canvas-react/components/LayersPanel.tsx","../../src/design-canvas-react/components/layer-tree.ts"],"sourcesContent":["/**\n * The human \"add element to the canvas\" side panel — a reusable left rail any\n * canvas consumer can mount so a person (not just the agent) can put images,\n * shapes, and text on the canvas. Designed for the `renderSidePanel` slot of\n * {@link DesignCanvasProps}; pass `renderSidePanel={(ctx) => <CanvasInsertPanel … />}`.\n *\n * Endpoints are per-app, so every I/O path is a callback — no product route is\n * hardcoded:\n * - `onUploadImage(file) => Promise<url>` — optional. When provided, the host\n * stores the file and returns the src to insert. The url MUST be http(s) or\n * a rooted `/api/` path (enforced by `assertSceneMediaSrc` before insertion);\n * a `data:` url is rejected, matching the scene model's media boundary.\n * - `loadGenerations?()` — optional provider for \"already generated in this\n * workspace\" images; omit to hide the tab.\n * - `templates?` — optional template set; defaults to {@link DEFAULT_INSERT_TEMPLATES}.\n *\n * Insertion goes through `onInsert`, which the host wires to its\n * `onApplyOperations` pipeline (server-validated, undoable) — the same path\n * every other edit takes.\n *\n * Tokens/icons follow the canvas convention: CSS-var design tokens and inline\n * SVG glyphs, no icon-library or Tailwind-semantic-token dependency.\n */\n\nimport { useEffect, useRef, useState, type ReactElement } from 'react'\nimport type { SceneOperation } from '../../design-canvas/operations'\nimport { assertSceneMediaSrc } from '../../design-canvas/model'\nimport {\n DEFAULT_INSERT_TEMPLATES,\n buildInsertImageOp,\n type InsertPageGeometry,\n type InsertTemplate,\n} from '../insert-builders'\nimport { ImageGlyph, ShapesGlyph } from './glyphs'\n\n/** An already-generated image the panel can offer for one-click insertion. */\nexport interface InsertGeneration {\n id: string\n /** The image url to insert. Must satisfy the scene media boundary\n * (http(s) or rooted `/api/`); rejected otherwise at insert time. */\n url: string\n /** Optional prompt/label shown as the tile's title. */\n label?: string\n}\n\nexport interface CanvasInsertPanelProps {\n canWrite: boolean\n /** The active page new elements are added to. */\n page: InsertPageGeometry\n /** Submit operations through the host's apply pipeline. */\n onInsert(operations: SceneOperation[]): Promise<unknown>\n /** Store an uploaded file and return its src (http(s) or rooted `/api/`). */\n onUploadImage?(file: File): Promise<string>\n /** Optional provider for the Generations tab; omit to hide it. */\n loadGenerations?(): Promise<InsertGeneration[]>\n /** Drop-in templates; defaults to the built-in starter set. */\n templates?: readonly InsertTemplate[]\n /** Accepted upload mime types. Default: PNG/JPEG/GIF/WebP. */\n accept?: string\n className?: string\n}\n\ntype Tab = 'uploads' | 'templates' | 'generations'\n\nconst DEFAULT_ACCEPT = 'image/png,image/jpeg,image/gif,image/webp'\n\n/** Read an image url's natural dimensions in the browser; resolves `{0,0}` when\n * Image is unavailable (SSR/tests), the load fails, or the probe times out —\n * builders fall back to the size cap, so a failed probe still inserts a sensibly\n * sized element rather than blocking insertion. */\nconst PROBE_TIMEOUT_MS = 4000\n\nfunction probeImageSize(url: string): Promise<{ width: number; height: number }> {\n return new Promise((resolve) => {\n if (typeof Image === 'undefined') return resolve({ width: 0, height: 0 })\n let settled = false\n const finish = (size: { width: number; height: number }) => {\n if (settled) return\n settled = true\n resolve(size)\n }\n const img = new Image()\n img.onload = () => finish({ width: img.naturalWidth, height: img.naturalHeight })\n img.onerror = () => finish({ width: 0, height: 0 })\n // A URL that never loads (or an environment that never fires load/error)\n // must not hang the insert forever.\n setTimeout(() => finish({ width: 0, height: 0 }), PROBE_TIMEOUT_MS)\n img.src = url\n })\n}\n\nfunction UploadGlyph({ className }: { className?: string }) {\n return (\n <svg className={className} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden>\n <path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 9l5-5 5 5M12 4v12\" />\n </svg>\n )\n}\n\nfunction SparkleGlyph({ className }: { className?: string }) {\n return (\n <svg className={className} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden>\n <path d=\"M12 3v3m0 12v3M3 12h3m12 0h3M5.6 5.6l2.1 2.1m8.6 8.6 2.1 2.1m0-12.8-2.1 2.1M7.7 16.3l-2.1 2.1\" />\n </svg>\n )\n}\n\nfunction SpinnerGlyph({ className }: { className?: string }) {\n return (\n <svg className={`${className ?? ''} animate-spin`} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" aria-hidden>\n <path d=\"M21 12a9 9 0 1 1-6.2-8.6\" strokeLinecap=\"round\" />\n </svg>\n )\n}\n\n/** Probe-page geometry for deriving a template's primary element kind. The\n * result drives only the tile preview, so the exact dimensions don't matter. */\nconst PREVIEW_PROBE_PAGE: InsertPageGeometry = { pageId: 'preview', width: 1000, height: 1000 }\n\n/** The visual a template tile shows. Derived from the first element the\n * template's (pure) `build` produces, so custom templates render correctly\n * too — not just the built-in set. */\ntype TemplateShape = 'heading' | 'body' | 'rect' | 'ellipse' | 'other'\n\n/** A text element is treated as a heading (shown as \"T\") when it reads as a\n * title — bold or set in a large face; otherwise body copy (shown as \"¶\"). */\nconst HEADING_FONT_SIZE = 32\n\nfunction templateShape(tpl: InsertTemplate): TemplateShape {\n try {\n const ops = tpl.build(PREVIEW_PROBE_PAGE)\n const added = ops.find((op) => op.type === 'add_element')\n if (!added || added.type !== 'add_element') return 'other'\n const el = added.element\n if (el.kind === 'rect') return 'rect'\n if (el.kind === 'ellipse') return 'ellipse'\n if (el.kind === 'text') {\n const isHeading = el.fontStyle.includes('bold') || el.fontSize >= HEADING_FONT_SIZE\n return isHeading ? 'heading' : 'body'\n }\n return 'other'\n } catch {\n // A throwing build can't preview; fall through to the neutral shape glyph.\n return 'other'\n }\n}\n\n/** A small visual standing in for what the template inserts, so tiles are\n * distinguishable at a glance instead of four identical text chips. */\nfunction TemplatePreview({ shape }: { shape: TemplateShape }) {\n if (shape === 'rect') {\n return <span className=\"block h-6 w-9 rounded-sm bg-[var(--brand-primary)]\" aria-hidden />\n }\n if (shape === 'ellipse') {\n return <span className=\"block h-7 w-7 rounded-full bg-[var(--brand-primary)]\" aria-hidden />\n }\n // Text tiles show a glyph: a bold \"T\" for a heading, a paragraph mark \"¶\"\n // for body copy.\n if (shape === 'heading' || shape === 'body') {\n return (\n <span\n className={`block leading-none text-[var(--text-primary)] ${\n shape === 'heading' ? 'text-2xl font-bold' : 'text-lg'\n }`}\n aria-hidden\n >\n {shape === 'heading' ? 'T' : '¶'}\n </span>\n )\n }\n return <ShapesGlyph className=\"h-6 w-6 text-[var(--text-muted)]\" />\n}\n\nexport function CanvasInsertPanel({\n canWrite,\n page,\n onInsert,\n onUploadImage,\n loadGenerations,\n templates = DEFAULT_INSERT_TEMPLATES,\n accept = DEFAULT_ACCEPT,\n className,\n}: CanvasInsertPanelProps) {\n const [tab, setTab] = useState<Tab>(() => (templates.length > 0 ? 'templates' : 'uploads'))\n const [busy, setBusy] = useState(false)\n const [error, setError] = useState('')\n const [dragOver, setDragOver] = useState(false)\n const [generations, setGenerations] = useState<InsertGeneration[]>([])\n const [generationsLoaded, setGenerationsLoaded] = useState(false)\n const fileInputRef = useRef<HTMLInputElement | null>(null)\n\n useEffect(() => {\n if (tab === 'uploads' && !onUploadImage) {\n setTab(templates.length > 0 ? 'templates' : loadGenerations ? 'generations' : 'uploads')\n }\n if (tab === 'templates' && templates.length === 0) {\n setTab(onUploadImage ? 'uploads' : loadGenerations ? 'generations' : 'uploads')\n }\n if (tab === 'generations' && !loadGenerations) {\n setTab(templates.length > 0 ? 'templates' : onUploadImage ? 'uploads' : 'templates')\n }\n }, [tab, templates.length, onUploadImage, loadGenerations])\n\n useEffect(() => {\n if (tab !== 'generations' || generationsLoaded || !loadGenerations) return\n let cancelled = false\n void (async () => {\n try {\n const rows = await loadGenerations()\n if (!cancelled) setGenerations(rows.filter((g) => g.url))\n } catch (err) {\n if (!cancelled) setError(err instanceof Error ? err.message : 'Could not load generations')\n } finally {\n if (!cancelled) setGenerationsLoaded(true)\n }\n })()\n return () => {\n cancelled = true\n }\n }, [tab, generationsLoaded, loadGenerations])\n\n async function insertImageFromUrl(url: string) {\n // Validate the media boundary BEFORE probing — a data: or sandbox-local url\n // throws here (surfaced as an inline error) without a wasted image load.\n assertSceneMediaSrc(url, 'image src')\n const natural = await probeImageSize(url)\n await onInsert([buildInsertImageOp(url, natural, page)])\n }\n\n async function handleFiles(files: FileList | File[]) {\n if (!canWrite || busy) return\n if (!onUploadImage) return\n const list = Array.from(files).filter((f) => f.type.startsWith('image/'))\n if (list.length === 0) {\n setError('Only image files can be added to the canvas')\n return\n }\n setBusy(true)\n setError('')\n try {\n for (const file of list) {\n const url = await onUploadImage(file)\n await insertImageFromUrl(url)\n }\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Upload failed')\n } finally {\n setBusy(false)\n }\n }\n\n async function runInsert(build: () => SceneOperation[] | Promise<SceneOperation[]>) {\n if (!canWrite || busy) return\n setBusy(true)\n setError('')\n try {\n await onInsert(await build())\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Insert failed')\n } finally {\n setBusy(false)\n }\n }\n\n const tabs: Array<{ id: Tab; label: string; icon: (p: { className?: string }) => ReactElement; show: boolean }> = [\n { id: 'uploads', label: 'Uploads', icon: ImageGlyph, show: !!onUploadImage },\n { id: 'templates', label: 'Templates', icon: ShapesGlyph, show: templates.length > 0 },\n { id: 'generations', label: 'Generations', icon: SparkleGlyph, show: !!loadGenerations },\n ]\n const visibleTabs = tabs.filter((t) => t.show)\n\n return (\n <div className={`flex h-full min-h-0 flex-col bg-[var(--bg-input)] text-[var(--text-primary)] ${className ?? ''}`}>\n <div className=\"flex shrink-0 border-b border-[var(--border-default)]\">\n {visibleTabs.map(({ id, label, icon: Icon }) => (\n <button\n key={id}\n type=\"button\"\n onClick={() => setTab(id)}\n // These tabs sit flush against the top and side edges of a panel\n // that clips its overflow, so the floor's outward ring is cut off on\n // three sides and reads as a stray underline. The negative offset\n // draws the same ring inside the tab; width and colour still come\n // from the tokens.\n className={`flex flex-1 items-center justify-center gap-1.5 px-2 py-2.5 text-xs font-medium transition-colors focus-visible:[outline-offset:-2px] ${\n tab === id\n ? 'border-b-2 border-[var(--brand-primary)] text-[var(--text-primary)]'\n : 'text-[var(--text-muted)] hover:text-[var(--text-primary)]'\n }`}\n >\n <Icon className=\"h-3.5 w-3.5\" />\n {label}\n </button>\n ))}\n </div>\n\n {!canWrite ? (\n <div className=\"flex flex-1 items-center justify-center p-4 text-center text-sm text-[var(--text-muted)]\">\n You have view-only access to this design.\n </div>\n ) : (\n <div className=\"min-h-0 flex-1 overflow-y-auto p-3\">\n {tab === 'uploads' && (\n <div className=\"flex flex-col gap-3\">\n <button\n type=\"button\"\n disabled={busy}\n onClick={() => fileInputRef.current?.click()}\n onDragOver={(e) => {\n e.preventDefault()\n setDragOver(true)\n }}\n onDragLeave={() => setDragOver(false)}\n onDrop={(e) => {\n e.preventDefault()\n setDragOver(false)\n if (e.dataTransfer?.files?.length) void handleFiles(e.dataTransfer.files)\n }}\n className={`flex flex-col items-center justify-center gap-2 rounded-lg border-2 border-dashed px-4 py-8 text-center transition-colors disabled:opacity-50 ${\n dragOver\n ? 'border-[var(--brand-primary)] bg-[color-mix(in_srgb,var(--brand-primary)_5%,transparent)]'\n : 'border-[var(--border-default)] hover:border-[color-mix(in_srgb,var(--brand-primary)_40%,transparent)]'\n }`}\n >\n {busy ? (\n <SpinnerGlyph className=\"h-6 w-6 text-[var(--brand-primary)]\" />\n ) : (\n <UploadGlyph className=\"h-6 w-6 text-[var(--text-muted)]\" />\n )}\n <span className=\"text-sm font-medium text-[var(--text-primary)]\">\n {busy ? 'Uploading…' : 'Drop an image or click to upload'}\n </span>\n <span className=\"text-xs text-[var(--text-muted)]\">PNG, JPEG, GIF, or WebP</span>\n </button>\n <input\n ref={fileInputRef}\n type=\"file\"\n accept={accept}\n multiple\n className=\"hidden\"\n onChange={(e) => {\n if (e.target.files?.length) void handleFiles(e.target.files)\n e.target.value = ''\n }}\n />\n </div>\n )}\n\n {tab === 'templates' && (\n <div className=\"grid grid-cols-2 gap-2\">\n {templates.map((tpl) => (\n <button\n key={tpl.id}\n type=\"button\"\n disabled={busy}\n onClick={() => void runInsert(() => tpl.build(page))}\n className=\"flex h-20 flex-col items-center justify-center gap-1.5 rounded-md border border-[var(--border-default)] bg-[var(--bg-input)] text-xs font-medium text-[var(--text-primary)] transition-colors hover:border-[color-mix(in_srgb,var(--brand-primary)_40%,transparent)] disabled:opacity-50\"\n >\n <span className=\"flex h-7 items-center justify-center\">\n <TemplatePreview shape={templateShape(tpl)} />\n </span>\n <span>{tpl.label}</span>\n </button>\n ))}\n </div>\n )}\n\n {tab === 'generations' && (\n <div className=\"flex flex-col gap-2\">\n {!generationsLoaded ? (\n <div className=\"flex items-center justify-center py-8 text-[var(--text-muted)]\">\n <SpinnerGlyph className=\"h-5 w-5\" />\n </div>\n ) : generations.length === 0 ? (\n <p className=\"px-1 py-6 text-center text-sm text-[var(--text-muted)]\">\n No generated images yet. Ask the agent to generate one.\n </p>\n ) : (\n <div className=\"grid grid-cols-2 gap-2\">\n {generations.map((gen) => (\n <button\n key={gen.id}\n type=\"button\"\n disabled={busy}\n onClick={() => {\n if (busy || !canWrite) return\n setBusy(true)\n setError('')\n insertImageFromUrl(gen.url)\n .catch((err) => setError(err instanceof Error ? err.message : 'Insert failed'))\n .finally(() => setBusy(false))\n }}\n title={gen.label}\n className=\"group relative aspect-square overflow-hidden rounded-md border border-[var(--border-default)] bg-[var(--bg-input)] transition-colors hover:border-[var(--brand-primary)] disabled:opacity-50\"\n >\n {/* eslint-disable-next-line jsx-a11y/img-redundant-alt */}\n <img src={gen.url} alt={gen.label || 'Generated image'} className=\"h-full w-full object-cover\" />\n </button>\n ))}\n </div>\n )}\n </div>\n )}\n\n {error ? <p className=\"mt-3 text-xs leading-5 text-[var(--text-danger,#dc2626)]\">{error}</p> : null}\n </div>\n )}\n </div>\n )\n}\n","/**\n * Layers panel — reverse-z list of the active page's elements. Highest z-index\n * is at the top. Groups show their children indented beneath them. Clicking\n * selects an element; meta-click adds to selection. Double-clicking the name\n * opens an inline rename input. Eye and lock icons toggle visibility/locked.\n * Rows can be drag-reordered within the list (siblings only; cross-group reorder\n * is a pending integrator concern). Capped at LAYERS_PANEL_ROW_LIMIT rows.\n */\n\nimport { useCallback, useMemo, useRef, useState } from 'react'\nimport type { SceneElement, ScenePage } from '../../design-canvas/model'\nimport { flattenLayerTree, LAYERS_PANEL_ROW_LIMIT } from './layer-tree'\nimport {\n EllipseGlyph,\n EyeGlyph,\n EyeOffGlyph,\n GroupGlyph,\n ImageGlyph,\n LineGlyph,\n LockGlyph,\n RectGlyph,\n SlotGlyph,\n TextGlyph,\n UnlockGlyph,\n VideoGlyph,\n} from './glyphs'\nimport type { SceneElementKind } from '../../design-canvas/model'\n\nfunction KindIcon({ kind, className }: { kind: SceneElementKind; className?: string }) {\n switch (kind) {\n case 'rect': return <RectGlyph className={className} />\n case 'ellipse': return <EllipseGlyph className={className} />\n case 'line': return <LineGlyph className={className} />\n case 'text': return <TextGlyph className={className} />\n case 'image': return <ImageGlyph className={className} />\n case 'video': return <VideoGlyph className={className} />\n case 'group': return <GroupGlyph className={className} />\n }\n}\n\nexport interface LayersPanelProps {\n page: ScenePage\n selectedElementIds: string[]\n canWrite: boolean\n /** Emit a set_attrs command for the given element. */\n onSetAttrs(elementId: string, attrs: Partial<Pick<SceneElement, 'name' | 'visible' | 'locked'>>): void\n /** Emit a reorder_element command. */\n onReorder(elementId: string, toIndex: number): void\n onSelect(elementId: string, additive: boolean): void\n}\n\nconst INDENT_PX = 16\n\nexport function LayersPanel({ page, selectedElementIds, canWrite, onSetAttrs, onReorder, onSelect }: LayersPanelProps) {\n const rows = useMemo(() => flattenLayerTree(page), [page])\n const [renamingId, setRenamingId] = useState<string | null>(null)\n const [renameValue, setRenameValue] = useState('')\n\n // Drag-reorder state: we track the dragged row's ownerIndex and the target\n // index within the SAME owner. Cross-owner reorder is not supported here.\n const dragRowRef = useRef<{ elementId: string; ownerIndex: number; ownerLength: number } | null>(null)\n const [dragOverIndex, setDragOverIndex] = useState<number | null>(null)\n\n function startRename(element: SceneElement) {\n setRenamingId(element.id)\n setRenameValue(element.name)\n }\n\n function commitRename(elementId: string) {\n const name = renameValue.trim()\n if (name.length > 0) onSetAttrs(elementId, { name })\n setRenamingId(null)\n }\n\n const visible = rows.length > LAYERS_PANEL_ROW_LIMIT ? rows.slice(0, LAYERS_PANEL_ROW_LIMIT) : rows\n\n return (\n <div className=\"flex h-full flex-col overflow-hidden text-[var(--text-primary)]\">\n <div className=\"shrink-0 border-b border-[var(--border-default)] px-3 py-1.5 text-[11px] font-medium uppercase tracking-wide text-[var(--text-muted)]\">\n Layers\n </div>\n\n <div className=\"min-h-0 flex-1 overflow-y-auto\">\n {visible.length === 0 ? (\n <div className=\"px-3 py-2 text-[11px] text-[var(--text-muted)]\">No layers yet</div>\n ) : null}\n {visible.map((row) => {\n const { element } = row\n const isSelected = selectedElementIds.includes(element.id)\n const isRenaming = renamingId === element.id\n\n return (\n <div\n key={element.id}\n data-layer-row={element.id}\n draggable={canWrite}\n onDragStart={() => {\n dragRowRef.current = {\n elementId: element.id,\n ownerIndex: row.ownerIndex,\n ownerLength: row.ownerLength,\n }\n }}\n onDragOver={(event) => {\n if (!dragRowRef.current) return\n // Only allow reorder within the same owner (same parentGroupId + depth)\n event.preventDefault()\n setDragOverIndex(row.ownerIndex)\n }}\n onDragLeave={() => setDragOverIndex(null)}\n onDrop={() => {\n const drag = dragRowRef.current\n if (!drag) return\n if (drag.elementId !== element.id) {\n onReorder(drag.elementId, row.ownerIndex)\n }\n dragRowRef.current = null\n setDragOverIndex(null)\n }}\n onDragEnd={() => {\n dragRowRef.current = null\n setDragOverIndex(null)\n }}\n className={[\n 'group flex items-center gap-1.5 py-1 pr-2 text-[13px] transition-colors',\n isSelected ? 'bg-[color-mix(in_srgb,var(--brand-primary)_15%,transparent)] text-[var(--text-primary)]' : 'hover:bg-[color-mix(in_srgb,var(--border-default)_40%,transparent)] text-[var(--text-secondary)]',\n dragOverIndex === row.ownerIndex ? 'border-t border-[var(--brand-primary)]' : '',\n ].join(' ')}\n style={{ paddingLeft: 8 + row.depth * INDENT_PX }}\n >\n {isRenaming ? (\n <input\n autoFocus\n value={renameValue}\n onChange={(event) => setRenameValue(event.target.value)}\n onBlur={() => commitRename(element.id)}\n onKeyDown={(event) => {\n if (event.key === 'Enter') commitRename(element.id)\n if (event.key === 'Escape') setRenamingId(null)\n event.stopPropagation()\n }}\n className=\"min-w-0 flex-1 rounded border border-[var(--border-default)] bg-[var(--bg-input)] px-1 py-0 text-[13px] text-[var(--text-primary)] focus:border-[var(--brand-primary)]\"\n />\n ) : (\n // Selection is a real button (sibling of the eye/lock buttons) so\n // the row is not an interactive control nesting interactive\n // controls (axe: nested-interactive).\n <button\n type=\"button\"\n onClick={(event) => onSelect(element.id, event.metaKey || event.ctrlKey)}\n onDoubleClick={() => {\n if (canWrite) startRename(element)\n }}\n title={element.name}\n className=\"flex min-w-0 flex-1 cursor-pointer items-center gap-1.5 bg-transparent text-left\"\n >\n <KindIcon kind={element.kind} className=\"h-3.5 w-3.5 shrink-0 opacity-60\" />\n {element.slot ? <SlotGlyph className=\"h-3 w-3 shrink-0 text-[var(--brand-primary)]\" /> : null}\n <span className=\"min-w-0 flex-1 truncate\">{element.name}</span>\n </button>\n )}\n\n {/* Visibility toggle */}\n <button\n type=\"button\"\n aria-label={element.visible ? 'Hide element' : 'Show element'}\n aria-pressed={!element.visible}\n onClick={(event) => {\n event.stopPropagation()\n if (canWrite) onSetAttrs(element.id, { visible: !element.visible })\n }}\n disabled={!canWrite}\n className={[\n 'shrink-0 rounded p-0.5 transition-opacity group-hover:opacity-100 hover:opacity-100 focus-visible:opacity-100',\n element.visible ? 'opacity-0' : 'opacity-100',\n ].join(' ')}\n >\n {element.visible\n ? <EyeGlyph className=\"h-3.5 w-3.5\" />\n : <EyeOffGlyph className=\"h-3.5 w-3.5 text-[var(--text-muted)]\" />}\n </button>\n\n {/* Lock toggle */}\n <button\n type=\"button\"\n aria-label={element.locked ? 'Unlock element' : 'Lock element'}\n aria-pressed={!!element.locked}\n onClick={(event) => {\n event.stopPropagation()\n if (canWrite) onSetAttrs(element.id, { locked: !element.locked })\n }}\n disabled={!canWrite}\n className={[\n 'shrink-0 rounded p-0.5 transition-opacity group-hover:opacity-100 hover:opacity-100 focus-visible:opacity-100',\n element.locked ? 'opacity-100' : 'opacity-0',\n ].join(' ')}\n >\n {element.locked\n ? <LockGlyph className=\"h-3.5 w-3.5 text-[var(--text-warning)]\" />\n : <UnlockGlyph className=\"h-3.5 w-3.5\" />}\n </button>\n </div>\n )\n })}\n\n {rows.length > LAYERS_PANEL_ROW_LIMIT ? (\n <div className=\"px-3 py-2 text-[11px] text-[var(--text-muted)]\">\n +{rows.length - LAYERS_PANEL_ROW_LIMIT} more elements — select a group to scope the list\n </div>\n ) : null}\n </div>\n </div>\n )\n}\n","/**\n * Pure layer-tree helpers: flatten a page's element array into an ordered list\n * for the LayersPanel, preserving group nesting. Extracted so render order,\n * depth, and selection math can be unit-tested independently of React.\n */\n\nimport type { SceneElement, ScenePage } from '../../design-canvas/model'\n\n/** Describe a layer row with its element, depth, grouping, ownership, and parent group details */\nexport interface LayerRow {\n element: SceneElement\n depth: number\n /** True when this row is a group whose children follow it in the list. */\n isGroup: boolean\n /** Index within its owner (page.elements or group.children). */\n ownerIndex: number\n /** Length of the owner array — needed for z-order bound math. */\n ownerLength: number\n /** Id of the containing group, or null for page-root elements. */\n parentGroupId: string | null\n}\n\n/**\n * Flatten `page.elements` into display order for the layers panel.\n *\n * Z-order is bottom→top in the element array; the layers panel shows\n * top→bottom (highest z-index first). A group row precedes its children\n * (the group is \"above\" its children in the panel, reflecting that the group\n * node itself paints last in Konva when empty; children paint inside it).\n *\n * Depth increases by 1 for each group level, starting at 0 for page-root\n * elements. Groups are expanded; collapsed state is a view concern the caller\n * tracks separately.\n */\nexport function flattenLayerTree(page: ScenePage): LayerRow[] {\n const rows: LayerRow[] = []\n\n function visit(elements: SceneElement[], depth: number, parentGroupId: string | null) {\n // Reverse so highest z-index appears at the top of the panel.\n for (let i = elements.length - 1; i >= 0; i -= 1) {\n const element = elements[i]!\n rows.push({\n element,\n depth,\n isGroup: element.kind === 'group',\n ownerIndex: i,\n ownerLength: elements.length,\n parentGroupId,\n })\n if (element.kind === 'group') {\n visit(element.children, depth + 1, element.id)\n }\n }\n }\n\n visit(page.elements, 0, null)\n return rows\n}\n\n/** The maximum number of rows the layers panel renders before truncating. */\nexport const LAYERS_PANEL_ROW_LIMIT = 500\n"],"mappings":"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","names":["useRef","useState","jsx","jsxs","useState","useRef"]}
|
package/dist/preflight/cli.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/preflight/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * `agent-app-preflight` — the deploy-time probe runner.\n *\n * Loads a product's `preflight.config.mjs` (default export: `PreflightProbe[]`),\n * runs every probe, prints the operator table, and exits non-zero when a\n * critical probe fails so a deploy step can gate on it.\n *\n * Run it where the REAL secrets are — after `wrangler secret put …`, not in CI.\n * CI has no secrets, so a probe there proves nothing.\n *\n * agent-app-preflight # ./preflight.config.mjs\n * agent-app-preflight ops/probes.mjs # explicit path\n *\n * This bin existed only as a promise in the module docs until 0.44.0; at least\n * one product had hand-rolled a byte-identical `scripts/preflight.mjs` around\n * `runPreflight` + `formatPreflightReport` to fill the gap.\n */\nimport { pathToFileURL } from 'node:url'\nimport { resolve } from 'node:path'\nimport { existsSync } from 'node:fs'\nimport { formatPreflightReport, runPreflight, type PreflightProbe } from './index'\nimport { invokedAsScript } from '../signoff/invoked-as-script'\n\nconst DEFAULT_CONFIG = 'preflight.config.mjs'\n\n/**\n * Load the probe list from a config module. Fail loud on every miss — a\n * preflight that silently probes nothing and exits 0 is worse than no\n * preflight, because it reports the deploy as verified.\n */\nexport async function loadProbes(configPath: string): Promise<PreflightProbe[]> {\n const abs = resolve(process.cwd(), configPath)\n if (!existsSync(abs)) {\n throw new Error(\n `agent-app-preflight: no config at \"${abs}\". Create one that default-exports a PreflightProbe[], or pass a path.`,\n )\n }\n const mod: unknown = await import(pathToFileURL(abs).href)\n const probes = (mod as { default?: unknown }).default\n if (!Array.isArray(probes)) {\n throw new Error(\n `agent-app-preflight: \"${abs}\" must default-export an array of probes; got ${typeof probes}.`,\n )\n }\n if (probes.length === 0) {\n throw new Error(`agent-app-preflight: \"${abs}\" exported an empty probe list — nothing would be verified.`)\n }\n return probes as PreflightProbe[]\n}\n\n/** Run the probes named by `argv` and resolve with the process exit code. */\nexport async function runPreflightCli(argv: readonly string[]): Promise<number> {\n const probes = await loadProbes(argv[0] ?? DEFAULT_CONFIG)\n const report = await runPreflight(probes)\n console.log(formatPreflightReport(report))\n return report.ok ? 0 : 1\n}\n\n/* c8 ignore start — process wiring, exercised by the bin itself */\nif (invokedAsScript(import.meta.url, process.argv[1])) {\n runPreflightCli(process.argv.slice(2))\n .then((code) => {\n process.exit(code)\n })\n .catch((err: unknown) => {\n console.error(err instanceof Error ? err.message : String(err))\n process.exit(1)\n })\n}\n/* c8 ignore stop */\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../../src/preflight/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * `agent-app-preflight` — the deploy-time probe runner.\n *\n * Loads a product's `preflight.config.mjs` (default export: `PreflightProbe[]`),\n * runs every probe, prints the operator table, and exits non-zero when a\n * critical probe fails so a deploy step can gate on it.\n *\n * Run it where the REAL secrets are — after `wrangler secret put …`, not in CI.\n * CI has no secrets, so a probe there proves nothing.\n *\n * agent-app-preflight # ./preflight.config.mjs\n * agent-app-preflight ops/probes.mjs # explicit path\n *\n * This bin existed only as a promise in the module docs until 0.44.0; at least\n * one product had hand-rolled a byte-identical `scripts/preflight.mjs` around\n * `runPreflight` + `formatPreflightReport` to fill the gap.\n */\nimport { pathToFileURL } from 'node:url'\nimport { resolve } from 'node:path'\nimport { existsSync } from 'node:fs'\nimport { formatPreflightReport, runPreflight, type PreflightProbe } from './index'\nimport { invokedAsScript } from '../signoff/invoked-as-script'\n\nconst DEFAULT_CONFIG = 'preflight.config.mjs'\n\n/**\n * Load the probe list from a config module. Fail loud on every miss — a\n * preflight that silently probes nothing and exits 0 is worse than no\n * preflight, because it reports the deploy as verified.\n */\nexport async function loadProbes(configPath: string): Promise<PreflightProbe[]> {\n const abs = resolve(process.cwd(), configPath)\n if (!existsSync(abs)) {\n throw new Error(\n `agent-app-preflight: no config at \"${abs}\". Create one that default-exports a PreflightProbe[], or pass a path.`,\n )\n }\n const mod: unknown = await import(pathToFileURL(abs).href)\n const probes = (mod as { default?: unknown }).default\n if (!Array.isArray(probes)) {\n throw new Error(\n `agent-app-preflight: \"${abs}\" must default-export an array of probes; got ${typeof probes}.`,\n )\n }\n if (probes.length === 0) {\n throw new Error(`agent-app-preflight: \"${abs}\" exported an empty probe list — nothing would be verified.`)\n }\n return probes as PreflightProbe[]\n}\n\n/** Run the probes named by `argv` and resolve with the process exit code. */\nexport async function runPreflightCli(argv: readonly string[]): Promise<number> {\n const probes = await loadProbes(argv[0] ?? DEFAULT_CONFIG)\n const report = await runPreflight(probes)\n console.log(formatPreflightReport(report))\n return report.ok ? 0 : 1\n}\n\n/* c8 ignore start — process wiring, exercised by the bin itself */\nif (invokedAsScript(import.meta.url, process.argv[1])) {\n runPreflightCli(process.argv.slice(2))\n .then((code) => {\n process.exit(code)\n })\n .catch((err: unknown) => {\n console.error(err instanceof Error ? err.message : String(err))\n process.exit(1)\n })\n}\n/* c8 ignore stop */\n"],"mappings":";;;;;;;;;;;AAkBA,SAAS,qBAAqB;AAC9B,SAAS,eAAe;AACxB,SAAS,kBAAkB;AAI3B,IAAM,iBAAiB;AAOvB,eAAsB,WAAW,YAA+C;AAC9E,QAAM,MAAM,QAAQ,QAAQ,IAAI,GAAG,UAAU;AAC7C,MAAI,CAAC,WAAW,GAAG,GAAG;AACpB,UAAM,IAAI;AAAA,MACR,sCAAsC,GAAG;AAAA,IAC3C;AAAA,EACF;AACA,QAAM,MAAe,MAAM,OAAO,cAAc,GAAG,EAAE;AACrD,QAAM,SAAU,IAA8B;AAC9C,MAAI,CAAC,MAAM,QAAQ,MAAM,GAAG;AAC1B,UAAM,IAAI;AAAA,MACR,yBAAyB,GAAG,iDAAiD,OAAO,MAAM;AAAA,IAC5F;AAAA,EACF;AACA,MAAI,OAAO,WAAW,GAAG;AACvB,UAAM,IAAI,MAAM,yBAAyB,GAAG,kEAA6D;AAAA,EAC3G;AACA,SAAO;AACT;AAGA,eAAsB,gBAAgB,MAA0C;AAC9E,QAAM,SAAS,MAAM,WAAW,KAAK,CAAC,KAAK,cAAc;AACzD,QAAM,SAAS,MAAM,aAAa,MAAM;AACxC,UAAQ,IAAI,sBAAsB,MAAM,CAAC;AACzC,SAAO,OAAO,KAAK,IAAI;AACzB;AAGA,IAAI,gBAAgB,YAAY,KAAK,QAAQ,KAAK,CAAC,CAAC,GAAG;AACrD,kBAAgB,QAAQ,KAAK,MAAM,CAAC,CAAC,EAClC,KAAK,CAAC,SAAS;AACd,YAAQ,KAAK,IAAI;AAAA,EACnB,CAAC,EACA,MAAM,CAAC,QAAiB;AACvB,YAAQ,MAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAC9D,YAAQ,KAAK,CAAC;AAAA,EAChB,CAAC;AACL;","names":[]}
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*
|
|
14
14
|
* A probe is `{ name, run, critical? }`; `run()` returns `{ ok, detail? }`.
|
|
15
15
|
* The standard builders (`requiredValueProbe`, `routerChatProbe`,
|
|
16
|
-
* `sandboxAuthProbe`, `httpHeadProbe`) each take explicit config — they read
|
|
16
|
+
* `sandboxAuthProbe`, `httpHeadProbe`, `slackAlertProbe`) each take explicit config — they read
|
|
17
17
|
* nothing global — so the same probe runs identically in a deploy step, a test,
|
|
18
18
|
* or a local check. `runPreflight` fans
|
|
19
19
|
* the probes out, times each, and folds them into a pass/fail report: any
|
|
@@ -151,6 +151,39 @@ interface HttpHeadProbeConfig {
|
|
|
151
151
|
* platform URL that still sits in the secret store.
|
|
152
152
|
*/
|
|
153
153
|
declare function httpHeadProbe(config: HttpHeadProbeConfig): PreflightProbe;
|
|
154
|
+
/** Define configuration options for probing that the Slack alerting channel is live */
|
|
155
|
+
interface SlackAlertProbeConfig {
|
|
156
|
+
/** Slack bot token (`xoxb-…`). */
|
|
157
|
+
token: string | undefined;
|
|
158
|
+
/** Probe name in the report. Default `'slack-alerting'`. */
|
|
159
|
+
name?: string;
|
|
160
|
+
/**
|
|
161
|
+
* Default `false`. A dead alerting channel is a serious finding but not a
|
|
162
|
+
* reason to refuse a deploy — the deploy is often the fix for whatever the
|
|
163
|
+
* alerts were about, and blocking it would make a broken alert channel an
|
|
164
|
+
* outage of its own.
|
|
165
|
+
*/
|
|
166
|
+
critical?: boolean;
|
|
167
|
+
/** Env-var name of the token, named verbatim in a dead-credential failure. */
|
|
168
|
+
keySecret?: string;
|
|
169
|
+
/** Per-probe deadline. Default 10s. */
|
|
170
|
+
timeoutMs?: number;
|
|
171
|
+
/** Injection seam for tests; defaults to global `fetch`. */
|
|
172
|
+
fetchImpl?: typeof fetch;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Probe that the Slack alerting credential is alive, via `auth.test` — no
|
|
176
|
+
* message is posted.
|
|
177
|
+
*
|
|
178
|
+
* This closes the gap that hid a total alerting outage: every Slack credential
|
|
179
|
+
* the org held was revoked at once and nothing noticed, because an incoming
|
|
180
|
+
* webhook is write-only (the only way to test one is to post to it) and the
|
|
181
|
+
* secret sat in the store looking configured. A bot token can be ASKED, so a
|
|
182
|
+
* deploy can answer the question nobody was asking.
|
|
183
|
+
*
|
|
184
|
+
* Non-critical by default — see `critical`.
|
|
185
|
+
*/
|
|
186
|
+
declare function slackAlertProbe(config: SlackAlertProbeConfig): PreflightProbe;
|
|
154
187
|
/**
|
|
155
188
|
* Run every probe (concurrently), time each, and fold into a report. The run
|
|
156
189
|
* fails (`ok: false`) iff a critical probe fails; a failed non-critical probe
|
|
@@ -161,4 +194,4 @@ declare function runPreflight(probes: PreflightProbe[]): Promise<PreflightReport
|
|
|
161
194
|
* (no I/O) so it is trivially testable and reusable by the bin. */
|
|
162
195
|
declare function formatPreflightReport(report: PreflightReport): string;
|
|
163
196
|
|
|
164
|
-
export { type HttpHeadProbeConfig, type PreflightProbe, type PreflightProbeResult, type PreflightProbeVerdict, type PreflightReport, type RequiredValueProbeConfig, type RouterChatProbeConfig, type SandboxAuthProbeConfig, formatPreflightReport, httpHeadProbe, requiredValueProbe, routerChatProbe, runPreflight, sandboxAuthProbe };
|
|
197
|
+
export { type HttpHeadProbeConfig, type PreflightProbe, type PreflightProbeResult, type PreflightProbeVerdict, type PreflightReport, type RequiredValueProbeConfig, type RouterChatProbeConfig, type SandboxAuthProbeConfig, type SlackAlertProbeConfig, formatPreflightReport, httpHeadProbe, requiredValueProbe, routerChatProbe, runPreflight, sandboxAuthProbe, slackAlertProbe };
|