diff --git a/design_handoff_payment_intelligence_brand/README.md b/design_handoff_payment_intelligence_brand/README.md new file mode 100644 index 0000000..968e1ce --- /dev/null +++ b/design_handoff_payment_intelligence_brand/README.md @@ -0,0 +1,276 @@ +# Handoff: Payment Intelligence Modules — Brand Restyle (raafetchoukri.com) + +## Overview + +This is a full visual + UX restyle of the **Payment Intelligence Modules** suite +(`Raafet57/payment-intelligence-modules`, branch `main`). It rebrands the entire +browser-only suite to the **raafetchoukri.com** design language, adds a working +**light/dark theme**, adds a **mobile navigation menu**, removes all **em dashes** +from copy, and unifies the **sample data** so every tool reflects one coherent +synthetic payment. + +The task for Claude Code is to **port these design changes into the existing +React/TypeScript app** (Vite + React + TS, pages under `src/pages/`, layout under +`src/components/layout/`) — using the repo's established patterns — then commit and +push. Do **not** add the HTML files to the repo as production code. + +## About the Design Files + +The files in `design/` are **design references created as standalone HTML** +("Design Components", `*.dc.html`). They are prototypes that show the intended +look, layout, copy, and behavior. They are **not** to be shipped as-is. Recreate +them in the repo's existing React/TSX environment and styling system (the repo +already has `src/index.css` and component/page structure — follow it). + +Each `.dc.html` is self-contained: an inline ` + + +
+ + +
+
+
+

+ P1 · Local operational +

+

+ Session Analytics +

+

+ Counts, pass-rates, and reason codes across this session. Runs entirely in your + browser. Nothing is uploaded. +

+
+ Back to workbench +
+ +
+
+

+ Findings by severity +

+
+ +
+ {{ s.label }} + + {{ s.value }} +
+
+
+

+ Pass rate: 78% across 28 findings + in 5 analyses. +

+
+ +
+

+ By module +

+
+ +
+ {{ m.module }} + {{ m.detail }} +
+
+
+

+ Top reason codes +

+
+ +
+ {{ r.code }} + {{ r.count }} +
+
+
+
+
+ +
+
+ +

+ Local analytics only +

+
+
    +
  • + Computed from this session or synthetic scenarios only. +
  • +
  • + No telemetry, no remote analytics, nothing is uploaded. +
  • +
  • + Not a fleet metric; there is no shared backend. +
  • +
+
+
+ + +
+ + + + diff --git a/design_handoff_payment_intelligence_brand/design/BIC.dc.html b/design_handoff_payment_intelligence_brand/design/BIC.dc.html new file mode 100644 index 0000000..4197049 --- /dev/null +++ b/design_handoff_payment_intelligence_brand/design/BIC.dc.html @@ -0,0 +1,509 @@ + + + + + + + + + + + + + + + + + +
+ + +
+
+
+

+ Module · Demo +

+

+ BIC Validator & Explainer* +

+

+ Validate a BIC against ISO 9362 structural rules and look it up in a small bundled + demo snapshot. Runs entirely in your browser. Nothing is uploaded. +

+
+ 4 of 4 syntax checks passed +
+ +
+ +

+ Demo-grade snapshot. The asterisk marks this module as demonstration + data: the bundled subset is 64 primary-office entries from a single SWIFT BIC + Directory snapshot, deliberately small and not refreshed. +

+
+ +
+
+
+

+ Step 1 · Add a BIC +

+

+ BIC input +

+

+ The result updates as you type. No button, no submit. +

+
+ +
+ + BIC-shape + valid* +
+
+

+ Privacy boundary +

+

+ The BIC you paste stays in this page. No backend, no remote BIC service, no + localStorage, no telemetry, no analytics beacon. +

+
+
+ +
+
+

+ Step 2 · Read the analysis +

+

+ Checks and breakdown +

+
+
+ +
+ {{ c }} +
+
+
+
+ +
+

+ {{ b.label }} +

+

+ {{ b.value }} +

+
+
+
+
+

+ Snapshot lookup (demo) +

+

+ Deutsche Bank AG +

+

+ Frankfurt am Main, DE · primary office +

+
+
+
+ +
+

+ Demo snapshot: 64 entries from + bic-snapshot.generated.json + dated 2026-02-14 (sha256 + 7c1a9f02bd3e…). Absence from this subset does not mean a BIC is invalid. +

+
+
+ + +
+
+ + + diff --git a/design_handoff_payment_intelligence_brand/design/CBPR.dc.html b/design_handoff_payment_intelligence_brand/design/CBPR.dc.html new file mode 100644 index 0000000..de4f00c --- /dev/null +++ b/design_handoff_payment_intelligence_brand/design/CBPR.dc.html @@ -0,0 +1,413 @@ + + + + + + + + + + + + + + + + + +
+ + +
+
+
+

+ Module +

+

+ CBPR+ Readiness Checker +

+

+ Inspect AppHdr, Document namespace, and bundled CBPR+ schema-profile coverage + locally. Shape checks, not certified validation. +

+
+ Amber · 2 advisories +
+ +
+
+
+
+

+ Step 1 · Add an MX message +

+

+ AppHdr + Document +

+
+ +
+ +
+ + Shape checks only. Not a MyStandards substitute. +
+
+ +
+
+

+ Step 2 · Readiness report +

+

+ pacs.008.001.08 · CBPR+ profile +

+
+
+ +
+

+ {{ s.value }} +

+

+ {{ s.label }} +

+
+
+
+
+

+ Findings +

+
+ +
+ {{ f.sev }} + {{ f.text }} +
+
+
+
+
+
+
+ + +
+
+ + + diff --git a/design_handoff_payment_intelligence_brand/design/Connectors.dc.html b/design_handoff_payment_intelligence_brand/design/Connectors.dc.html new file mode 100644 index 0000000..a644b2c --- /dev/null +++ b/design_handoff_payment_intelligence_brand/design/Connectors.dc.html @@ -0,0 +1,356 @@ + + + + + + + + + + + + + + + + + +
+ + +
+
+

+ P3 · Enterprise / live (gated) +

+

+ Connector Readiness +

+

+ The live-data connector framework, every track disabled behind explicit gates. Runs + entirely in your browser. Nothing is uploaded. +

+
+ +
+ +

+ 6 of 6 connectors disabled · 0 live integrations + enabled. Every connector reports + live_integration = false. There is no code path that performs a real provider call from this suite. +

+
+ +
+ +
+
+
+

+ {{ c.track }} +

+

+ {{ c.name }} +

+
+ Gated · disabled +
+

+ {{ c.description }} +

+

+ Examples: {{ c.examples }} +

+
+

+ Gate requirements (unmet) +

+
    + +
  • + {{ g }} +
  • +
    +
+
+ +

+ Blocked: live_integration is false for {{ c.id }}. +

+
+
+
+ +

+ Mirrored server-side by the offline scaffold at + apps/payment-intelligence-pilot, whose registry enforces the same invariant and whose tests prove disabled connectors + cannot be called. +

+
+ + +
+
+ + + diff --git a/design_handoff_payment_intelligence_brand/design/Docs.dc.html b/design_handoff_payment_intelligence_brand/design/Docs.dc.html new file mode 100644 index 0000000..813278e --- /dev/null +++ b/design_handoff_payment_intelligence_brand/design/Docs.dc.html @@ -0,0 +1,522 @@ + + + + + + + + + + + + + + + + + +
+ + +
+
+

+ P0 · Platform +

+

+ Docs, Methodology & Support +

+

+ How the suite works, what it checks, and what it deliberately does not. Runs entirely + in your browser. Nothing is uploaded. +

+
+ +
+
+
+

+ Methodology +

+
    + +
  • + {{ m.term }}: {{ m.body + }} +
  • +
    +
+
+ +
+

+ Module guides +

+ +
+ +
+

+ Workbench & platform surfaces +

+ +
+ +
+

+ Support +

+
    +
  • + Pick a bundled scenario from the Scenario Switcher to try a module end-to-end. +
  • +
  • + Run the suite locally with + pnpm dev; the quality gate is + pnpm verify. +
  • +
  • + The hosted private-pilot scaffold lives at + apps/payment-intelligence-pilot + and runs separately. +
  • +
+
+
+ + +
+
+ + +
+
+ + + diff --git a/design_handoff_payment_intelligence_brand/design/Health.dc.html b/design_handoff_payment_intelligence_brand/design/Health.dc.html new file mode 100644 index 0000000..8ce893d --- /dev/null +++ b/design_handoff_payment_intelligence_brand/design/Health.dc.html @@ -0,0 +1,416 @@ + + + + + + + + + + + + + + + + + +
+ + +
+
+

+ P0 · Platform +

+

+ Health & Scope +

+

+ An honest build, status, and scope surface. Runs entirely in your browser. Nothing is + uploaded. +

+
+ +
+
+

+ Public suite status +

+

+ Operational (static) +

+
+
+ Version0.1.0 +
+
+ RuntimeBrowser SPA +
+
+ Live integrations0 +
+
+
+
+

+ Privacy posture +

+
    + +
  • + {{ p }} +
  • +
    +
+
+
+ +
+

+ What is and is not live +

+

+ Every capability below is local or disabled. The suite never presents live or + certified status without a real source. +

+
+ + + + + + + + + + + + + + + + + +
CapabilityLive?Notes
+ {{ row.capability }} + + No + {{ row.note }}
+
+
+ +
+

+ Maturity tiers +

+
+ +
+

+ {{ t.label }} +

+

+ {{ t.desc }} +

+
+
+
+
+
+ + +
+
+ + + diff --git a/design_handoff_payment_intelligence_brand/design/IBAN.dc.html b/design_handoff_payment_intelligence_brand/design/IBAN.dc.html new file mode 100644 index 0000000..0480cdf --- /dev/null +++ b/design_handoff_payment_intelligence_brand/design/IBAN.dc.html @@ -0,0 +1,762 @@ + + + + + + + + + + + + + + + + + +
+ + +
+
+
+

+ Module +

+

+ IBAN Workbench +

+

+ Validate, build, catalogue, and trace IBAN provenance from the bundled registry + snapshot. Runs entirely in your browser. Nothing is uploaded. +

+
+
+ +
+ + + +
+ +
+
+ +
+
+

+ Step 1 · Enter an IBAN +

+

+ IBAN input +

+
+ + IBAN + valid +
+

+ Privacy boundary +

+

+ The IBAN you enter stays in this page. There is no backend, no remote lookup, + no storage, and no telemetry. +

+
+
+
+ +
+
+

+ Build an IBAN +

+

+ From country + BBAN parts +

+
+ + + +
+

+ Computed IBAN (check digits 89) +

+

+ DE89 3704 0044 0532 0130 00 +

+
+
+
+ +
+
+

+ Registry catalogue +

+

+ Country formats (snapshot) +

+
+ + + + + + + + + + + + + + + + + +
CountryLenSEPA
+ {{ c.country }} + + {{ c.len }} + + {{ c.sepa }} +
+
+
+ +
+
+

+ Provenance +

+

+ Bundled snapshot source +

+
+ +
+ {{ p.label }} + {{ p.value }} +
+
+
+
+
+ +
+
+

+ Breakdown +

+

+ DE89 3704 0044 0532 0130 00 +

+
+
+ +
+

+ {{ b.label }} +

+

+ {{ b.value }} +

+
+
+
+
+

+ Checks +

+
+ +
+ {{ c }} +
+
+
+
+
+

+ What this module does not do +

+
    +
  • + No live BIC lookup or bank-directory enrichment. +
  • +
  • + No Verification of Payee or account-name check. +
  • +
  • + No scheme reachability check. +
  • +
+
+
+
+
+ + +
+
+ + + diff --git a/design_handoff_payment_intelligence_brand/design/Insights.dc.html b/design_handoff_payment_intelligence_brand/design/Insights.dc.html new file mode 100644 index 0000000..6fac533 --- /dev/null +++ b/design_handoff_payment_intelligence_brand/design/Insights.dc.html @@ -0,0 +1,444 @@ + + + + + + + + + + + + + + + + + +
+ + +
+
+
+

+ Module +

+

+ Payment Insights Lite +

+

+ Group ACK, NACK, pacs, and camt files you provide into local lifecycle threads. Not + live payment tracking. Not settlement monitoring. +

+
+ 5 files · 2 threads +
+ +
+
+

+ Files in this session +

+

+ Grouped by end-to-end identifier. In memory only; cleared on refresh. +

+
+ +
+ +
+

+ {{ f.name }} +

+

+ {{ f.thread }} +

+
+ {{ f.family }} +
+
+
+
+ +
+
+
+

+ Lifecycle thread +

+

+ INV-7741-2026 · EUR 18,720.00 +

+
+ Completed +
+ +
+ +
+
+ + +
+
+
+ {{ step.family }} + {{ step.time }} +
+

+ {{ step.text }} +

+
+
+
+
+
+
+ +
+
+ +

+ Local grouping only +

+
+
    +
  • + Threads are built from the files you provide, never a live feed. +
  • +
  • + No settlement monitoring and no scheme connectivity. +
  • +
  • + Nothing is uploaded, stored, or logged. +
  • +
+
+
+ + +
+
+ + + diff --git a/design_handoff_payment_intelligence_brand/design/NotFound.dc.html b/design_handoff_payment_intelligence_brand/design/NotFound.dc.html new file mode 100644 index 0000000..d6acbe5 --- /dev/null +++ b/design_handoff_payment_intelligence_brand/design/NotFound.dc.html @@ -0,0 +1,176 @@ + + + + + + + + + + + + + + + + + +
+ + +
+

+ Page not found +

+

+ That page isn't here +

+

+ The link you followed points to something this suite doesn't have. +

+ +
+

+ Head back to the overview and choose Scrubber or Storyteller. +

+ Back to overview +
+
+ + +
+
+ + + diff --git a/design_handoff_payment_intelligence_brand/design/Overview.dc.html b/design_handoff_payment_intelligence_brand/design/Overview.dc.html new file mode 100644 index 0000000..c5a0d4e --- /dev/null +++ b/design_handoff_payment_intelligence_brand/design/Overview.dc.html @@ -0,0 +1,1123 @@ + + + + + + + + + + + + + + + + + +
+ + +
+
+
+
+ + Browser-only · ISO 20022 · Payment intelligence +
+

+ Payment intelligence for the reference data + underneath payments. +

+

+ A privacy-first workbench for payment ops and integration teams working with ISO + 20022 XML. Scrub, explain, validate, and group message fragments locally. Everything + stays on your device. +

+ +
+ +
+

+ {{ stat.value }} +

+

+ {{ stat.label }} +

+
+
+
+
+ + +
+ +
+

+ From the payment practice +

+

+ Choose a review workflow +

+

+ Each module is a small, auditable workflow: clear input, transparent transformation, + and outputs you copy or download and keep. +

+
+ + +
+ + + + + {{ item.capLabel }} + {{ item.maturityShort }} + +
+

+ {{ item.name }} +

+

+ {{ item.summary }} +

+ Open +
+
+
+
+ +
+

+ Local operational layer (P1) +

+

+ Work across modules in one session +

+

+ A command center, scenario switcher, review queue, analytics, and an encrypted local + vault. All in-memory, all on your device. +

+
+ + +
+ + + + + {{ item.capLabel }} + {{ item.maturityShort }} + +
+

+ {{ item.name }} +

+

+ {{ item.summary }} +

+ Open +
+
+
+
+ +
+

+ Platform & expansion (P0–P3) +

+

+ Honest gates, not fake production +

+

+ Status and docs ship now. The hosted pilot and live-data connectors are gated, + disabled prototypes, never shown as live or certified. +

+
+ + +
+ + + + + {{ item.capLabel }} + {{ item.maturityShort }} + +
+

+ {{ item.name }} +

+

+ {{ item.summary }} +

+ Open +
+
+
+
+ +
+
+
+ + + +
+

+ Workflow +

+

+ How it works +

+
+
+
    + +
  • + + {{ item }} +
  • +
    +
+
+
+
+ + + +
+

+ Boundaries +

+

+ What it does not do +

+
+
+
    + +
  • + + {{ item }} +
  • +
    +
+
+
+ +
+

+ Maturity +

+

+ From static suite to gated platform +

+
+ +
+

+ {{ t.label }} +

+

+ {{ t.desc }} +

+
+
+
+
+
+ + +
+
+ + + diff --git a/design_handoff_payment_intelligence_brand/design/Pilot.dc.html b/design_handoff_payment_intelligence_brand/design/Pilot.dc.html new file mode 100644 index 0000000..088192b --- /dev/null +++ b/design_handoff_payment_intelligence_brand/design/Pilot.dc.html @@ -0,0 +1,406 @@ + + + + + + + + + + + + + + + + + +
+ + +
+
+
+

+ P2 · Private-pilot scaffold +

+

+ Private-Pilot Scaffold +

+

+ An offline, synthetic FastAPI scaffold for the hosted-pilot tier. This page runs + entirely in your browser and makes no calls to it. +

+
+ Pilot scaffold · P2 +
+ +
+ +
+

+ A prototype, never production, and separate from this static suite. +

+

+ The scaffold lives at + apps/payment-intelligence-pilot. It is a synthetic, offline FastAPI app with a loud demo-stub auth, SQLite + persistence, immutable audit, four-eyes approval, and a disabled connector registry. + The public suite does not couple to it. +

+
+
+ +
+
+

+ Pattern class (offline) +

+
    + +
  • + {{ p }} +
  • +
    +
+
+ +
+

+ API contract (offline) +

+ + + + + + + + + + +
+ {{ e.method }} + + {{ e.path }} + {{ e.note }}
+
+
+ +
+

+ Run it locally +

+

+ The scaffold is run separately from the public suite. No live link is provided here. +

+
+cd apps/payment-intelligence-pilot
+make test   # contract + gate tests (pytest)
+make run    # uvicorn on port 8100 (local only)
+
+ +
+
+ +

+ Gates before this becomes a real pilot +

+
+
    + +
  • + {{ g }} +
  • +
    +
+
+
+ + +
+
+ + + diff --git a/design_handoff_payment_intelligence_brand/design/Review.dc.html b/design_handoff_payment_intelligence_brand/design/Review.dc.html new file mode 100644 index 0000000..889fb2e --- /dev/null +++ b/design_handoff_payment_intelligence_brand/design/Review.dc.html @@ -0,0 +1,314 @@ + + + + + + + + + + + + + + + + + +
+ + +
+
+
+

+ P1 · Local operational +

+

+ Review Queue +

+

+ Triage low-confidence and failed findings from this session. Runs entirely in your + browser. Nothing is uploaded. +

+
+ Back to workbench +
+ +
+ +
+

+ {{ c.value }} +

+

+ {{ c.label }} +

+
+
+
+ +
+ +
+
+
+ {{ it.sev }} + {{ it.moduleId }} + {{ it.field }} + {{ it.status }} +
+

+ {{ it.message }} +

+
+
+ + +
+
+
+
+
+ + +
+
+ + + diff --git a/design_handoff_payment_intelligence_brand/design/SSI.dc.html b/design_handoff_payment_intelligence_brand/design/SSI.dc.html new file mode 100644 index 0000000..d674c66 --- /dev/null +++ b/design_handoff_payment_intelligence_brand/design/SSI.dc.html @@ -0,0 +1,555 @@ + + + + + + + + + + + + + + + + + +
+ + +
+
+

+ Standalone adjacent app +

+

+ SSI Control Tower +

+

+ A boundary pointer, not a Payment Intelligence module. SSI Control Tower is a + standalone adjacent app that lives under apps/ssi-control-tower. This page runs + entirely in your browser. Nothing is uploaded. +

+
+ +
+
+
+ Standalone + adjacent app +
+

+ Not a Payment Intelligence module or workflow. +

+

+ SSI Control Tower is not part of this browser-only suite and is not one of its + modules. It is a standalone adjacent app under + apps/ssi-control-tower. This page only records the boundary and points to where the separate app runs. It + never calls an API and is not coupled to the SSI runtime. +

+

+ Repo extraction and the product-boundary decision for the standalone app are still + pending. Until that decision is made, this page stays a backlog pointer rather than + a suite feature. +

+
+

+ Synthetic public prototype disclaimer +

+

+ This project is a synthetic-data public prototype for SSI readiness and + governance. It is not affiliated with any custodian or market infrastructure. It + does not connect to production SSI utilities and does not contain real settlement + instructions, client data, account numbers, or proprietary workflows. +

+
+
+ + +
+ +
+
+
+ +
+

+ What the standalone app does +

+

+ Control workflow +

+
+
+
    + +
  • + {{ c }} +
  • +
    +
+
+ +
+

+ Run it locally +

+

+ Start the standalone SSI app separately +

+
+cd apps/ssi-control-tower && make run
+# opens http://localhost:8000
+

+ The standalone app owns its Python dependencies, SQLite development data, templates, + APIs, tests, and synthetic fixtures. Read its README under apps/ssi-control-tower + for command details and demo flow. +

+
+
+ +
+

+ Rule catalogue +

+

+ Controls surfaced by the standalone app +

+
+ +
+

+ {{ r }} +

+
+
+
+
+
+ + +
+
+ + + diff --git a/design_handoff_payment_intelligence_brand/design/Scenarios.dc.html b/design_handoff_payment_intelligence_brand/design/Scenarios.dc.html new file mode 100644 index 0000000..e2e0545 --- /dev/null +++ b/design_handoff_payment_intelligence_brand/design/Scenarios.dc.html @@ -0,0 +1,404 @@ + + + + + + + + + + + + + + + + + +
+ + +
+
+

+ P0 · Scenario switcher +

+

+ Scenario Switcher +

+

+ Load bundled, deliberately synthetic ISO 20022 scenarios into the session. Runs + entirely in your browser. Nothing is uploaded. +

+
+ +
+
+

+ Choose a scenario +

+
+ + + +
+
+ +
+
+
+

+ {{ selected.label }} +

+ {{ selected.archetype }} +
+
+{{ selected.previewXml }}
+ +
+
+
+ +

+ This is synthetic data +

+
+
    +
  • + Every scenario is fabricated for demonstration. +
  • +
  • + BICs use the TEST pattern; account and IBAN tails are masked. +
  • +
  • + No network calls; the switcher works fully offline. +
  • +
+
+
+
+
+ + +
+
+ + + diff --git a/design_handoff_payment_intelligence_brand/design/Scrubber.dc.html b/design_handoff_payment_intelligence_brand/design/Scrubber.dc.html new file mode 100644 index 0000000..ab7ac72 --- /dev/null +++ b/design_handoff_payment_intelligence_brand/design/Scrubber.dc.html @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + +
+ + +
+
+
+

+ Module +

+

+ Scrubber +

+

+ Strip personally identifying fields from ISO 20022 payment XML before sharing. Runs + entirely in your browser. Nothing is uploaded. +

+
+ pacs.008 · 7 fields masked +
+ +
+
+
+
+

+ Step 1 · Add XML +

+

+ Input XML +

+

+ Parsed in your browser. Nothing is uploaded. +

+
+
+ + +
+
+ + + + + +
+

+ Step 2 · Choose what to mask +

+
+ + + +
+
+ +
+ + Scrubbed in 12 ms · in memory +
+
+ +
+
+
+

+ Step 3 · Review and export +

+

+ Scrubbed XML +

+
+
+ + +
+
+ + + +
+

+ Stats +

+
+ +
+

+ {{ s.value }} +

+

+ {{ s.label }} +

+
+
+
+
+
+
+ +
+
+ +

+ Mapping (privacy-safe) +

+
+

+ Originals are shown masked. Raw values never leave this page and are not retained + after the scrub completes. +

+
+ + + + + + + + + + + + + + + + + +
TokenMasked originalField
+ {{ row.token }} + + {{ row.masked }} + + {{ row.field }} +
+
+
+
+ + +
+
+ + + diff --git a/design_handoff_payment_intelligence_brand/design/Storyteller.dc.html b/design_handoff_payment_intelligence_brand/design/Storyteller.dc.html new file mode 100644 index 0000000..f01393d --- /dev/null +++ b/design_handoff_payment_intelligence_brand/design/Storyteller.dc.html @@ -0,0 +1,623 @@ + + + + + + + + + + + + + + + + + +
+ + +
+
+
+

+ Module +

+

+ Storyteller +

+

+ Turn an ISO 20022 payment or statement message into a plain-language narrative. Runs + entirely in your browser. Nothing is uploaded. +

+
+ pacs.008 · full narrative +
+ +
+
+
+
+

+ Step 1 · Add XML +

+

+ Input XML +

+

+ Parsed in your browser. Nothing is uploaded. +

+
+
+ + +
+
+ + + + + +
+ + Parses locally. No button telemetry. +
+
+ +
+
+
+

+ Step 3 · Read story +

+

+ Story +

+
+
+ + +
+
+ +
+ pacs.008 + Full narrative supported +
+ +
+

+ Operator summary +

+

+ {{ operatorSummary }} +

+
+ +
+ +

+ {{ p }} +

+
+
+ +
+

+ Insights ({{ insightCount }}) +

+
+ +
+ {{ ins.sev }} + {{ ins.text }} +
+
+
+
+
+
+ +
+

+ Key fields +

+

+ A schema-aware projection of the parsed message. The raw XML payload is never stored. +

+
+ +
+ {{ f.label }} + {{ f.value }} +
+
+
+
+
+ + +
+
+ + + diff --git a/design_handoff_payment_intelligence_brand/design/SuiteFooter.dc.html b/design_handoff_payment_intelligence_brand/design/SuiteFooter.dc.html new file mode 100644 index 0000000..e617542 --- /dev/null +++ b/design_handoff_payment_intelligence_brand/design/SuiteFooter.dc.html @@ -0,0 +1,175 @@ + + + + + + + + + + + + + + diff --git a/design_handoff_payment_intelligence_brand/design/SuiteHeader.dc.html b/design_handoff_payment_intelligence_brand/design/SuiteHeader.dc.html new file mode 100644 index 0000000..0310d4b --- /dev/null +++ b/design_handoff_payment_intelligence_brand/design/SuiteHeader.dc.html @@ -0,0 +1,291 @@ + + + + + + + + + +
+
+ + RC + Payment Intelligence/ Modules + + +
+ + + + + +
+
+
+ +
+
+
+ + + diff --git a/design_handoff_payment_intelligence_brand/design/Vault.dc.html b/design_handoff_payment_intelligence_brand/design/Vault.dc.html new file mode 100644 index 0000000..eb81834 --- /dev/null +++ b/design_handoff_payment_intelligence_brand/design/Vault.dc.html @@ -0,0 +1,411 @@ + + + + + + + + + + + + + + + + + +
+ + +
+
+

+ P1 · Local operational +

+

+ Local Vault +

+

+ Passphrase-encrypted, user-triggered export and import of session data. Runs entirely + in your browser. Nothing is uploaded. +

+
+ +
+
+ +

+ Used to derive an AES-GCM key with PBKDF2 in your browser. It is never stored or + sent anywhere. If you lose it, the bundle cannot be decrypted. +

+ +
+ +
+
+

+ Export encrypted bundle +

+

+ Encrypts 3 documents and 4 analysis results from this session and downloads a + single file. +

+ +
+
+

+ Import a bundle +

+

+ Reads a vault file you choose, decrypts it with the passphrase above, and loads it + into this session only. +

+ +
+
+ +

+ Encrypted bundle + downloaded. Nothing was written to browser storage. +

+ +
+
+ +

+ Vault security model +

+
+
    +
  • + No cloud vault, no account, no server. Encryption happens locally with WebCrypto. +
  • +
  • + No browser storage. The only persistence is a file you explicitly download. +
  • +
  • + Lose the passphrase and the bundle is unrecoverable by design. +
  • +
  • + Prototype: review the key-handling threat model before relying on it. +
  • +
+
+
+
+ + +
+
+ + + diff --git a/design_handoff_payment_intelligence_brand/design/Workbench.dc.html b/design_handoff_payment_intelligence_brand/design/Workbench.dc.html new file mode 100644 index 0000000..1f2d55f --- /dev/null +++ b/design_handoff_payment_intelligence_brand/design/Workbench.dc.html @@ -0,0 +1,518 @@ + + + + + + + + + + + + + + + + + +
+ + +
+
+
+

+ P1 · Local operational +

+

+ Workbench Command Center +

+

+ An in-memory command center over this browser session. Runs entirely in your + browser. Nothing is uploaded. +

+
+
+ Load a + scenario + +
+
+ +
+ +
+

+ {{ m.value }} +

+

+ {{ m.label }} +

+
+
+
+ +
+ Staged for storyteller: Okonkwo Logistics cross-border + transfer +
+ Go to module + +
+
+ +
+
+

+ Session documents +

+

+ Held in memory only. Cleared on refresh; nothing is stored or uploaded. +

+
+ +
+
+
+

+ {{ d.label }} +

+

+ {{ d.meta }} +

+
+ +
+
+ Hand off to: + + {{ t }} + +
+
+
+
+
+ +
+
+

+ Findings +

+ +
+

+ Suggested next action: triage warnings and criticals in the local review queue. +

+
+ +
+ {{ f.sev }} +
+

+ {{ f.text }} +

+

+ {{ f.meta }} +

+
+
+
+
+
+
+
+ + +
+
+ + + diff --git a/design_handoff_payment_intelligence_brand/design/support.js b/design_handoff_payment_intelligence_brand/design/support.js new file mode 100644 index 0000000..a682a72 --- /dev/null +++ b/design_handoff_payment_intelligence_brand/design/support.js @@ -0,0 +1,1621 @@ +// GENERATED from dc-runtime/src/*.ts — do not edit. Rebuild with `cd dc-runtime && bun run build`. +"use strict"; +(() => { + var __defProp = Object.defineProperty; + var __defNormalProp = (obj, key, value) => + key in obj + ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) + : (obj[key] = value); + var __publicField = (obj, key, value) => + __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); + + // src/react.ts + function getReact() { + const R = window.React; + if (!R) throw new Error("dc-runtime: window.React is not available yet"); + return R; + } + function getReactDOM() { + const RD = window.ReactDOM; + if (!RD) throw new Error("dc-runtime: window.ReactDOM is not available yet"); + return RD; + } + var h = (...args) => getReact().createElement(...args); + + // src/parse.ts + function parseDcDocument(doc) { + const dc = doc.querySelector("x-dc"); + if (!dc) return null; + const scriptEl = doc.querySelector("script[data-dc-script]"); + const { props, preview } = parseDataProps(scriptEl?.getAttribute("data-props") ?? null); + return { + template: dc.innerHTML, + js: scriptEl ? scriptEl.textContent || "" : "", + props, + preview, + }; + } + function parseDcText(src) { + const openMatch = /]*)?>/.exec(src); + if (!openMatch) return null; + const close = src.lastIndexOf(""); + if (close === -1 || close < openMatch.index) return null; + const template = src.slice(openMatch.index + openMatch[0].length, close); + const doc = new DOMParser().parseFromString(src, "text/html"); + const scriptEl = doc.querySelector("script[data-dc-script]"); + const { props, preview } = parseDataProps(scriptEl?.getAttribute("data-props") ?? null); + return { + template, + js: scriptEl ? scriptEl.textContent || "" : "", + props, + preview, + }; + } + function parseDataProps(raw) { + if (!raw) return { props: null, preview: null }; + let parsed; + try { + parsed = JSON.parse(raw); + } catch { + return { props: null, preview: null }; + } + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { + return { props: null, preview: null }; + } + const obj = parsed; + const preview = obj.$preview && typeof obj.$preview === "object" ? obj.$preview : null; + const rest = {}; + for (const k of Object.keys(obj)) { + if (k[0] !== "$") rest[k] = obj[k]; + } + return { props: Object.keys(rest).length ? rest : null, preview }; + } + function dcNameFromPath(pathname) { + let p = pathname || ""; + try { + p = decodeURIComponent(p); + } catch {} + const base = p.split("/").pop() || "Root"; + return base.replace(/\.dc\.html$/, "").replace(/\.html?$/, "") || "Root"; + } + + // src/boot.ts + var BASE_CSS = ` + .sc-placeholder{background:rgba(255,255,255,.3);border:1px solid rgba(0,0,0,.5); + border-radius:2px;box-sizing:border-box;overflow:hidden} + @keyframes sc-shine{0%{background-position:100% 50%}100%{background-position:0% 50%}} + html.sc-dc-streaming .sc-placeholder, + html.sc-dc-streaming .sc-interp.sc-missing{position:relative; + background:color-mix(in srgb,currentColor 5%,transparent); + border-color:transparent} + html.sc-dc-streaming .sc-placeholder::before, + html.sc-dc-streaming .sc-interp.sc-missing::before{content:''; + position:absolute;inset:0;pointer-events:none; + background:linear-gradient(90deg,rgba(217,119,87,0) 25%,rgba(247,225,211,.95) 37%,rgba(217,119,87,0) 63%); + background-size:400% 100%;animation:sc-shine 1.4s ease infinite} + html.sc-dc-streaming .sc-placeholder:nth-child(n+9 of .sc-placeholder)::before, + html.sc-dc-streaming .sc-interp.sc-missing:nth-child(n+9 of .sc-interp.sc-missing)::before{animation:none; + background:color-mix(in srgb,currentColor 8%,transparent)} + .sc-placeholder-error{padding:4px 8px;font:11px/1.4 ui-monospace,monospace; + color:rgba(0,0,0,.7);word-break:break-word} + .sc-interp.sc-missing{display:inline-block;width:2em;height:1em;overflow:hidden; + vertical-align:text-bottom;background:rgba(255,255,255,.3);border:1px solid rgba(0,0,0,.5); + border-radius:2px;box-sizing:border-box;color:transparent; + user-select:none} + .sc-interp.sc-unresolved{font-family:ui-monospace,monospace;font-size:.85em; + color:rgba(0,0,0,.5);background:rgba(0,0,0,.05);border-radius:3px; + padding:0 3px} + .sc-host.sc-has-error{position:relative} + .sc-logic-error{position:absolute;top:8px;left:8px;z-index:2147483647;max-width:60ch; + padding:6px 10px;background:#b00020;color:#fff;font:12px/1.4 ui-monospace,monospace; + border-radius:4px;white-space:pre-wrap;pointer-events:none} + /* Mirrors PRINT_BASELINE_CSS in apps/web deck-stage-export.ts \u2014 keep both + in sync until dc-runtime regains a build step. */ + @media print { + @page { margin: 0.5cm; } + figure, table { break-inside: avoid; } + #dc-root, #dc-root > .sc-host { height: auto; } + *, *::before, *::after { + print-color-adjust: exact; -webkit-print-color-adjust: exact; + backdrop-filter: none !important; -webkit-backdrop-filter: none !important; + animation-delay: -99s !important; animation-duration: .001s !important; + animation-iteration-count: 1 !important; animation-fill-mode: both !important; + animation-play-state: running !important; transition-duration: 0s !important; + } + } + `; + var FULL_PAGE_CSS = "html,body{height:100%;margin:0}#dc-root,#dc-root>.sc-host{height:100%}"; + function rootNameForDocument(doc, loc) { + let bootPath = loc.pathname || ""; + if (!/\.dc\.html?$/i.test(safeDecode(bootPath))) { + try { + bootPath = new URL(doc.baseURI || "/").pathname; + } catch {} + } + return dcNameFromPath(bootPath); + } + function safeDecode(s) { + try { + return decodeURIComponent(s); + } catch { + return s; + } + } + function boot(runtime, doc = document) { + const parsed = parseDcDocument(doc); + if (!parsed) return null; + const React = getReact(); + const rootName = rootNameForDocument(doc, location); + runtime.markFetched(rootName); + runtime.setRootName(rootName); + runtime.adoptParsed(rootName, parsed); + fetch(location.href) + .then((res) => (res.ok ? res.text() : "")) + .then((t) => { + const raw = t ? parseDcText(t) : null; + if (raw?.template) runtime.updateHtml(rootName, raw.template); + }) + .catch(() => {}); + const dc = doc.querySelector("x-dc"); + const hostEl = doc.createElement("div"); + hostEl.id = "dc-root"; + dc.replaceWith(hostEl); + if (!parsed.preview) { + const s = doc.createElement("style"); + s.textContent = FULL_PAGE_CSS; + doc.head.appendChild(s); + } + const Root = runtime.getDC(rootName); + const entry = runtime.registry.get(rootName); + function StandaloneRoot() { + const [, setTick] = React.useState(0); + React.useEffect(() => { + const sub = () => setTick((n) => n + 1); + entry.subs.add(sub); + return () => { + entry.subs.delete(sub); + }; + }, []); + const defaults = React.useMemo(() => { + const d = {}; + for (const k in entry.propsMeta || {}) { + const v = entry.propsMeta?.[k]?.default; + if (v !== void 0) d[k] = v; + } + return d; + }, [entry.propsMeta]); + return h(Root, { ...defaults, ...(entry.propOverrides || {}) }); + } + const ReactDOM = getReactDOM(); + if (ReactDOM.createRoot) ReactDOM.createRoot(hostEl).render(h(StandaloneRoot)); + else ReactDOM.render(h(StandaloneRoot), hostEl); + return rootName; + } + + // src/expr.ts + var IDENT_RE = /^[A-Za-z_$][A-Za-z0-9_$]*/; + var NUMBER_RE = /^-?\d+(\.\d+)?$/; + function resolve(vals, src) { + const expr = String(src).trim(); + if (!expr) return void 0; + if (expr[0] === "(" && expr[expr.length - 1] === ")" && parensWrapWhole(expr)) { + return resolve(vals, expr.slice(1, -1)); + } + const eq = findTopLevelEquality(expr); + if (eq) { + const lv = resolve(vals, expr.slice(0, eq.index)); + const rv = resolve(vals, expr.slice(eq.index + eq.op.length)); + switch (eq.op) { + case "===": + return lv === rv; + case "!==": + return lv !== rv; + case "==": + return lv == rv; + default: + return lv != rv; + } + } + if (expr[0] === "!") return !resolve(vals, expr.slice(1)); + if (expr === "true") return true; + if (expr === "false") return false; + if (expr === "null") return null; + if (expr === "undefined") return void 0; + if (NUMBER_RE.test(expr)) return Number(expr); + if ( + expr.length >= 2 && + (expr[0] === '"' || expr[0] === "'") && + expr[expr.length - 1] === expr[0] + ) { + return expr.slice(1, -1); + } + return resolvePath(vals, expr); + } + function parensWrapWhole(expr) { + let depth = 0; + for (let i = 0; i < expr.length - 1; i++) { + if (expr[i] === "(") depth++; + else if (expr[i] === ")") { + depth--; + if (depth === 0) return false; + } + } + return true; + } + function findTopLevelEquality(expr) { + let depth = 0; + for (let i = 0; i < expr.length; i++) { + const c = expr[i]; + if (c === "[" || c === "(") depth++; + else if (c === "]" || c === ")") depth--; + else if (depth === 0 && (c === "=" || c === "!") && expr[i + 1] === "=") { + if (i > 0 && (expr[i - 1] === "=" || expr[i - 1] === "!")) continue; + if (!expr.slice(0, i).trim()) continue; + const op = expr[i + 2] === "=" ? c + "==" : c + "="; + return { index: i, op }; + } + } + return null; + } + function resolvePath(vals, expr) { + const head = expr.match(IDENT_RE); + if (!head) return void 0; + let cur = vals == null ? void 0 : vals[head[0]]; + let i = head[0].length; + while (i < expr.length) { + if (expr[i] === ".") { + const m = expr.slice(i + 1).match(IDENT_RE) || expr.slice(i + 1).match(/^\d+/); + if (!m) return void 0; + cur = cur == null ? void 0 : cur[m[0]]; + i += 1 + m[0].length; + } else if (expr[i] === "[") { + let depth = 1; + let j = i + 1; + while (j < expr.length && depth > 0) { + if (expr[j] === "[") depth++; + else if (expr[j] === "]") { + depth--; + if (depth === 0) break; + } + j++; + } + if (depth !== 0) return void 0; + const key = resolve(vals, expr.slice(i + 1, j)); + cur = cur == null ? void 0 : cur[key]; + i = j + 1; + } else { + return void 0; + } + } + return cur; + } + + // src/encode.ts + var CAMEL_ATTR = "sc-camel-"; + var RAW_WRAP = { + select: "sc-raw-select", + table: "sc-raw-table", + tbody: "sc-raw-tbody", + thead: "sc-raw-thead", + tfoot: "sc-raw-tfoot", + tr: "sc-raw-tr", + td: "sc-raw-td", + th: "sc-raw-th", + caption: "sc-raw-caption", + }; + var RAW_UNWRAP = Object.fromEntries(Object.entries(RAW_WRAP).map(([k, v]) => [v, k])); + var EVENT_MAP = { + onclick: "onClick", + onchange: "onChange", + oninput: "onInput", + onsubmit: "onSubmit", + onkeydown: "onKeyDown", + onkeyup: "onKeyUp", + onkeypress: "onKeyPress", + onmousedown: "onMouseDown", + onmouseup: "onMouseUp", + onmouseenter: "onMouseEnter", + onmouseleave: "onMouseLeave", + onfocus: "onFocus", + onblur: "onBlur", + ondoubleclick: "onDoubleClick", + oncontextmenu: "onContextMenu", + }; + var ATTRS = `(?:[^>"']|"[^"]*"|'[^']*')*`; + var IMPORT_SELF_CLOSE_RE = new RegExp("<(x-import|dc-import)(" + ATTRS + ")/>", "gi"); + var CAMEL_ATTR_RE = /(\s)([a-z]+[A-Z][A-Za-z0-9]*)(\s*=)/g; + function encodeCase(html) { + html = html.replace(IMPORT_SELF_CLOSE_RE, (_, t, a) => "<" + t + a + ">"); + html = html.replace(/)/gi, "/gi, ""); + html = html.replace( + CAMEL_ATTR_RE, + (_, sp, name, eq) => + sp + CAMEL_ATTR + name.replace(/[A-Z]/g, (c) => "-" + c.toLowerCase()) + eq, + ); + for (const [real, alias] of Object.entries(RAW_WRAP)) { + html = html.replace(new RegExp("(])", "gi"), "$1" + alias); + } + return html; + } + function kebabToCamel(s) { + return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase()); + } + function cssToObj(css) { + const o = {}; + for (const decl of css.split(";")) { + const i = decl.indexOf(":"); + if (i < 0) continue; + const prop = decl.slice(0, i).trim(); + o[prop.startsWith("--") ? prop : kebabToCamel(prop)] = decl.slice(i + 1).trim(); + } + return o; + } + function compileAttr(raw) { + const whole = raw.match(/^\s*\{\{([\s\S]+?)\}\}\s*$/); + if (whole) { + const path = whole[1]; + return (vals) => resolve(vals, path); + } + if (raw.includes("{{")) { + const parts = raw.split(/\{\{([\s\S]+?)\}\}/g); + return (vals) => parts.map((s, i) => (i & 1 ? (resolve(vals, s) ?? "") : s)).join(""); + } + return () => raw; + } + + // src/compile.ts + function collectProps(node, kind, host) { + const propGetters = []; + const pseudoClasses = []; + let hintSize = null; + for (const { name, value } of [...node.attributes]) { + if (name === "sc-name" || name === "data-dc-tpl") continue; + let key = name; + if (key.startsWith(CAMEL_ATTR)) key = kebabToCamel(key.slice(CAMEL_ATTR.length)); + if (key === "hint-size") { + hintSize = value; + continue; + } + if (key.startsWith("style-")) { + pseudoClasses.push(host.pseudoClass(key.slice(6), value)); + continue; + } + if (kind !== "dom") { + if ( + key.includes("-") && + !(kind === "x-import" && (key.startsWith("aria-") || key.startsWith("data-"))) + ) + key = kebabToCamel(key); + } else { + if (key === "class") key = "className"; + else if (key === "for") key = "htmlFor"; + else if (key.startsWith("on")) + key = EVENT_MAP[key] || "on" + key[2].toUpperCase() + key.slice(3); + } + propGetters.push([key, compileAttr(value)]); + } + return { propGetters, pseudoClasses, hintSize }; + } + var HOST_STYLE_PROPS = /* @__PURE__ */ new Set([ + "position", + "left", + "right", + "top", + "bottom", + "inset", + "width", + "height", + "z-index", + "transform", + ]); + function hostPositionStyle(style) { + const all = + typeof style === "string" + ? cssToObj(style) + : style != null && typeof style === "object" + ? style + : null; + if (!all) return void 0; + const out = {}; + for (const [k, v] of Object.entries(all)) { + const kebab = k.replace(/[A-Z]/g, (c) => "-" + c.toLowerCase()); + if (HOST_STYLE_PROPS.has(kebab)) out[k] = v; + } + return Object.keys(out).length ? out : void 0; + } + function compileTemplate(html, host) { + const tpl = document.createElement("template"); + //! nosemgrep: direct-inner-html-assignment + tpl.innerHTML = encodeCase(html); + let tplN = 0; + (function stamp(node) { + if (node.nodeType === Node.ELEMENT_NODE) { + node.setAttribute("data-dc-tpl", String(tplN++)); + } + for (const c of node.childNodes) stamp(c); + })(tpl.content); + const builders = walkChildren(tpl.content, host); + const render = (vals, ctx) => builders.map((b, i) => b(vals || {}, ctx, i)); + render.__annotated = tpl.innerHTML; + return render; + } + function walkChildren(node, host) { + return [...node.childNodes].map((c) => walk(c, host)).filter((b) => b != null); + } + function walk(node, host) { + if (node.nodeType === Node.TEXT_NODE) return walkText(node); + if (node.nodeType !== Node.ELEMENT_NODE) return null; + const el = node; + const tag = el.tagName.toLowerCase(); + if (tag === "sc-for") return walkFor(el, host); + if (tag === "sc-if") return walkIf(el, host); + if (tag === "x-import") return walkXImport(el, host); + if (tag === "sc-helmet") return host.helmet(el); + if (tag === "dc-import") return walkComponent(el, host); + return walkElement(el, host); + } + var warnedHoles = /* @__PURE__ */ new Set(); + function warnUnresolved(ctx, what) { + const key = (ctx?.__name || "?") + "\0" + what; + if (warnedHoles.has(key)) return; + warnedHoles.add(key); + console.warn("[dc-runtime] " + (ctx?.__name || "template") + ": " + what); + } + function walkText(node) { + const txt = node.nodeValue ?? ""; + if (!txt.includes("{{")) { + if (!txt.trim() && !txt.includes(" ")) return null; + return () => txt; + } + const parts = txt.split(/\{\{([\s\S]+?)\}\}/g); + return (vals, ctx, key) => + h( + getReact().Fragment, + { key }, + ...parts.map((p, i) => { + if (!(i & 1)) return p; + const v = resolve(vals, p); + if (v === void 0) { + if (!ctx?.__streamingNow) { + if (document.body?.hasAttribute("data-dc-editor-on")) { + return h( + "span", + { key: i, className: "sc-interp sc-unresolved" }, + "{{ " + p.trim() + " }}", + ); + } + warnUnresolved(ctx, "{{ " + p.trim() + " }} never resolved \u2014 rendered as empty"); + return null; + } + return h("span", { key: i, className: "sc-interp sc-missing" }, p.trim()); + } + if (getReact().isValidElement(v) || Array.isArray(v)) { + return h(getReact().Fragment, { key: i }, v); + } + if (v === null || typeof v === "boolean") return null; + return h("span", { key: i, className: "sc-interp" }, String(v)); + }), + ); + } + function walkFor(el, host) { + const listGet = compileAttr(el.getAttribute("list") || ""); + const asName = el.getAttribute("as") || "item"; + const hintN = parseInt(el.getAttribute("hint-placeholder-count") || "0", 10); + const kids = walkChildren(el, host); + const listSrc = el.getAttribute("list") || ""; + return (vals, ctx, key) => { + let list = listGet(vals); + if (!Array.isArray(list)) { + if (!ctx?.__streamingNow) { + if (list !== void 0 && list !== null) { + warnUnresolved( + ctx, + 'sc-for list="' + listSrc + '" is not an array (' + typeof list + ")", + ); + } + list = []; + } else { + list = hintN > 0 ? Array(hintN).fill(void 0) : []; + } + } + return h( + getReact().Fragment, + { key }, + list.map((item, i) => { + const sub = { ...vals, [asName]: item, $index: i }; + return h( + getReact().Fragment, + { key: i }, + kids.map((b, j) => b(sub, ctx, j)), + ); + }), + ); + }; + } + function walkIf(el, host) { + const valGet = compileAttr(el.getAttribute("value") || ""); + const hintRaw = el.getAttribute("hint-placeholder-val"); + const hintGet = hintRaw != null ? compileAttr(hintRaw) : null; + const kids = walkChildren(el, host); + return (vals, ctx, key) => { + let v = valGet(vals); + if (v === void 0 && hintGet && ctx?.__streamingNow) v = hintGet(vals); + return v + ? h( + getReact().Fragment, + { key }, + kids.map((b, j) => b(vals, ctx, j)), + ) + : null; + }; + } + function walkComponent(el, host) { + const name = el.getAttribute("name") || el.getAttribute("component") || ""; + el.removeAttribute("name"); + el.removeAttribute("component"); + const tplId = el.getAttribute("data-dc-tpl"); + const styleRaw = el.getAttribute("style"); + el.removeAttribute("style"); + const styleGet = styleRaw != null ? compileAttr(styleRaw) : null; + const { propGetters, hintSize } = collectProps(el, "dc-import", host); + const kids = walkChildren(el, host); + return (vals, ctx, key) => { + const props = { + key, + __hintSize: hintSize, + __tplId: tplId, + __hostStyle: styleGet ? hostPositionStyle(styleGet(vals)) : void 0, + }; + for (const [k, g] of propGetters) { + const v = g(vals); + if (k === "dcProps") { + if (v && typeof v === "object") Object.assign(props, v); + continue; + } + props[k] = v; + } + if (kids.length) props.children = kids.map((b, j) => b(vals, ctx, j)); + return h(host.component(name), props); + }; + } + function walkXImport(el, host) { + const globalNameGet = compileAttr(el.getAttribute("component-from-global-scope") || ""); + const exportNameGet = compileAttr( + el.getAttribute("component") || el.getAttribute("name") || "", + ); + const fromRaw = + el.getAttribute("from") || el.getAttribute("src") || el.getAttribute("import") || ""; + const urls = fromRaw.trim() ? fromRaw.trim().split(/\s+/) : []; + const url = urls.length ? urls[urls.length - 1] : ""; + const kindOf = (u) => (/\.(jsx|tsx)(\?|#|$)/i.test(u) ? "jsx" : "js"); + const tplId = el.getAttribute("data-dc-tpl"); + const styleRaw = el.getAttribute("style"); + el.removeAttribute("style"); + const styleGet = styleRaw != null ? compileAttr(styleRaw) : null; + const wrap = tplId != null || styleGet != null; + const { propGetters, hintSize } = collectProps(el, "x-import", host); + const hasContent = el.children.length > 0 || !!(el.textContent || "").trim(); + const kids = hasContent ? walkChildren(el, host) : []; + const urlBindable = fromRaw.includes("{{"); + if (urls.length && !urlBindable) { + let prev; + for (const u of urls) prev = host.loadExternal(kindOf(u), u, prev); + } + const evalName = (g, vals) => { + const v = g(vals); + const s = v == null ? "" : String(v); + return s.includes("{{") ? "" : s; + }; + return (vals, ctx, key) => { + const globalName = evalName(globalNameGet, vals); + const name = globalName || evalName(exportNameGet, vals); + const C = + !name || urlBindable + ? null + : globalName + ? host.resolveExternalGlobal(url, globalName) + : host.resolveExternal(url, name); + const hostStyle = styleGet ? hostPositionStyle(styleGet(vals)) : void 0; + const wrapper = wrap + ? { + key, + className: "sc-host-x", + "data-dc-tpl": tplId, + style: hostStyle || { display: "contents" }, + } + : null; + if (!C) { + const error = urlBindable + ? "x-import `from` cannot contain {{ \u2026 }} \u2014 module URLs are resolved at parse time; use a literal URL" + : host.resolveExternalError(url, name); + const ph = host.placeholder({ + key: wrapper ? void 0 : key, + name, + hintSize, + error, + }); + return wrapper ? h("div", wrapper, ph) : ph; + } + const props = wrapper ? {} : { key }; + let unresolvedHole = false; + for (const [k, g] of propGetters) { + if (k === "component" || k === "componentFromGlobalScope" || k === "from") { + continue; + } + const v = g(vals); + if (v === void 0) unresolvedHole = true; + if (k === "dcProps") { + if (v && typeof v === "object") Object.assign(props, v); + continue; + } + props[k] = v; + } + if (unresolvedHole && ctx?.__htmlStreamingNow) { + const ph = host.placeholder({ + key: wrapper ? void 0 : key, + name, + hintSize, + error: null, + }); + return wrapper ? h("div", wrapper, ph) : ph; + } + if (kids.length) props.children = kids.map((b, j) => b(vals, ctx, j)); + return wrapper ? h("div", wrapper, h(C, props)) : h(C, props); + }; + } + function walkElement(el, host) { + const realTag = RAW_UNWRAP[el.localName] || el.localName; + const tplId = el.getAttribute("data-dc-tpl"); + const { propGetters, pseudoClasses } = collectProps(el, "dom", host); + const kids = walkChildren(el, host); + return (vals, ctx, key) => { + const props = { key, "data-dc-tpl": tplId }; + for (const [k, g] of propGetters) { + let v = g(vals); + if (k === "style" && typeof v === "string") v = cssToObj(v); + if ((k === "value" || k === "checked") && v === void 0) { + v = k === "checked" ? false : ""; + } + props[k] = v; + } + if (pseudoClasses.length) { + props.className = [props.className, ...pseudoClasses].filter(Boolean).join(" "); + } + return h(realTag, props, ...kids.map((b, j) => b(vals, ctx, j))); + }; + } + + // src/logic.ts + var StreamableLogic = class { + constructor(props) { + __publicField(this, "props"); + __publicField(this, "state", {}); + /** Back-pointer to the wrapper component, installed after construction. */ + __publicField(this, "__host"); + this.props = props || {}; + } + setState(update, cb) { + this.__host && this.__host.__setLogicState(update, cb); + } + forceUpdate() { + this.__host && this.__host.forceUpdate(); + } + componentDidMount() {} + componentDidUpdate(_prevProps) {} + componentWillUnmount() {} + /** The flat object the template renders against (merged over props). */ + renderVals() { + return {}; + } + }; + function evalDcLogic(src) { + //! nosemgrep: eval-and-function-constructor + const fn = new Function( + "DCLogic", + "StreamableLogic", + "React", + src + '\n;return (typeof Component!=="undefined"&&Component)||undefined;', + ); + return fn(StreamableLogic, StreamableLogic, getReact()); + } + + // src/component.ts + function shallowEqual(a, b) { + if (!b) return false; + const ak = Object.keys(a).filter((k) => k !== "children"); + const bk = Object.keys(b).filter((k) => k !== "children"); + if (ak.length !== bk.length) return false; + for (const k of ak) if (a[k] !== b[k]) return false; + return true; + } + function Placeholder({ name, hintSize, streaming, error }) { + const [w, hgt] = (hintSize || "100%,60px").split(","); + return h( + "div", + { + className: "sc-placeholder" + (streaming ? " sc-streaming" : ""), + style: { width: w.trim(), height: hgt && hgt.trim() }, + title: name, + }, + error + ? h("div", { className: "sc-placeholder-error" }, (name ? name + ": " : "") + error) + : null, + ); + } + function hintToMin(hint) { + if (!hint) return void 0; + const [w, hgt] = hint.split(","); + return { minWidth: w.trim(), minHeight: hgt && hgt.trim() }; + } + function createComponentFactory(registry, ensureFetched) { + const React = getReact(); + const AncestorContext = React.createContext([]); + class StreamableComponent extends React.Component { + constructor(props) { + super(props); + __publicField(this, "__name"); + __publicField(this, "__sub"); + __publicField(this, "__needsDidMount", false); + /** Snapshot of the registry's streaming flags taken at render time — + * builders read it off the RenderCtx (this) to pick placeholder vs + * render-nothing for unresolved values. */ + __publicField(this, "__streamingNow", false); + __publicField(this, "__htmlStreamingNow", false); + /** When a construct throws, remember the (class, registry.ver, props) + * triple so render-time reconcile doesn't re-attempt it on every parent + * re-render. A registry bump (new class, template, external module + * resolving via bumpAll) changes `ver` and breaks the memo so an + * env-dependent constructor can self-heal. */ + __publicField(this, "__failedLogic", null); + __publicField(this, "__failedUserProps", null); + __publicField(this, "__failedVer", -1); + /** Per-instance constructor error — kept here (not on the registry entry) + * so one instance's successful construct can't hide a sibling's failure, + * and a construct can never wipe an eval error `updateJs` recorded on + * `r.logicError`. */ + __publicField(this, "__ctorError", null); + __publicField(this, "logic"); + this.__name = props.__name; + this.state = { __v: 0, __err: null }; + this.__sub = () => { + if (this.state.__err) this.setState({ __err: null }); + this.forceUpdate(); + }; + this.__makeLogic(registry.get(this.__name).Logic, null); + ensureFetched(this.__name); + } + /** Error-boundary hook: a render crash anywhere in this DC's subtree + * (its own template, an x-import'd component, a child DC without its + * own deeper boundary) lands here instead of unmounting the page. */ + static getDerivedStateFromError(e) { + return { __err: e instanceof Error && e.message ? e.message : String(e) }; + } + componentDidCatch(e, info) { + console.error( + "[dc-runtime] render error in <" + this.__name + ">:", + e, + info?.componentStack || "", + ); + } + /** Instantiate the logic class (or the no-op base) and adopt `prevState` + * over its initial state — used both at mount and on hot-swap. */ + __makeLogic(Logic, prevState) { + const L = Logic || StreamableLogic; + try { + this.logic = new L(this.__userProps()); + this.__failedLogic = null; + this.__failedUserProps = null; + this.__ctorError = null; + } catch (e) { + console.error(e); + this.__failedLogic = Logic; + this.__failedUserProps = this.__userProps(); + this.__failedVer = registry.get(this.__name).ver; + this.__ctorError = + this.__name + ": " + (e instanceof Error && e.message ? e.message : String(e)); + this.logic = new StreamableLogic(this.__userProps()); + } + this.logic.__host = this; + if (prevState) this.logic.state = { ...(this.logic.state || {}), ...prevState }; + } + /** The props the author's logic + template see — internal __-prefixed + * wiring stripped. */ + __userProps() { + const { __name, __hintSize, __tplId, __hostStyle, ...rest } = this.props; + return rest; + } + __setLogicState(update, cb) { + const prev = this.logic.state; + const patch = typeof update === "function" ? update(prev) : update; + this.logic.state = { ...prev, ...patch }; + this.setState((s) => ({ __v: s.__v + 1 }), cb); + } + /** Swap the logic instance when the registry's Logic class changed + * (streaming completion, hot reload). State carries over; didMount + * re-fires after the swap commits so refs exist. */ + __reconcileLogic() { + const r = registry.get(this.__name); + const Next = r.Logic; + const Cur = this.logic.constructor; + if ( + Next === Cur || + (!Next && Cur === StreamableLogic) || + (Next === this.__failedLogic && + r.ver === this.__failedVer && + shallowEqual(this.__userProps(), this.__failedUserProps)) + ) { + return; + } + if (!this.__needsDidMount) { + try { + this.logic.componentWillUnmount(); + } catch (e) { + console.error(e); + } + } + this.__makeLogic(Next, this.logic.state); + this.__needsDidMount = true; + } + componentDidMount() { + registry.get(this.__name).subs.add(this.__sub); + try { + this.logic.componentDidMount(); + } catch (e) { + console.error(e); + } + } + componentDidUpdate(prevProps) { + this.logic.props = this.__userProps(); + if (this.__needsDidMount) { + if (this.state.__err || !registry.get(this.__name).tpl) return; + this.__needsDidMount = false; + try { + this.logic.componentDidMount(); + } catch (e) { + console.error(e); + } + } else { + try { + this.logic.componentDidUpdate(prevProps); + } catch (e) { + console.error(e); + } + } + } + componentWillUnmount() { + registry.get(this.__name).subs.delete(this.__sub); + if (!this.__needsDidMount) { + try { + this.logic.componentWillUnmount(); + } catch (e) { + console.error(e); + } + } + } + render() { + const r = registry.get(this.__name); + const cls = + "sc-host" + + (r.htmlStreaming ? " sc-streaming-html" : "") + + (r.jsStreaming ? " sc-streaming-js" : ""); + const hintStyle = r.htmlStreaming ? hintToMin(this.props.__hintSize) : void 0; + const hostStyle = + this.props.__hostStyle || hintStyle + ? { ...(hintStyle || {}), ...(this.props.__hostStyle || {}) } + : void 0; + const hostBase = { + className: cls, + style: hostStyle, + "data-sc-name": this.__name, + "data-dc-tpl": this.props.__tplId, + }; + const chain = Array.isArray(this.context) ? this.context : []; + if (chain.includes(this.__name)) { + const cycle = [...chain.slice(chain.indexOf(this.__name)), this.__name].join(" \u2192 "); + return h( + "div", + { ...hostBase, className: cls + " sc-has-error" }, + h(Placeholder, { + name: this.__name, + hintSize: this.props.__hintSize, + error: "circular import: " + cycle, + }), + ); + } + if (this.state.__err) { + return h( + "div", + { ...hostBase, className: cls + " sc-has-error" }, + h( + "div", + { className: "sc-logic-error", "data-omelette-chrome": "" }, + this.__name + ": " + this.state.__err, + ), + h(Placeholder, { + name: this.__name, + hintSize: this.props.__hintSize, + error: this.state.__err, + }), + ); + } + this.__reconcileLogic(); + if (!r.tpl) { + return h( + "div", + hostBase, + h(Placeholder, { name: this.__name, hintSize: this.props.__hintSize }), + ); + } + const userProps = this.__userProps(); + this.logic.props = userProps; + let vals = userProps; + let renderErr = r.logicError || this.__ctorError; + try { + vals = { ...userProps, ...(this.logic.renderVals() || {}) }; + } catch (e) { + console.error(e); + renderErr = + this.__name + + ".renderVals(): " + + (e instanceof Error && e.message ? e.message : String(e)); + } + this.__streamingNow = !!(r.htmlStreaming || r.jsStreaming); + this.__htmlStreamingNow = !!r.htmlStreaming; + return h( + "div", + { ...hostBase, className: cls + (renderErr ? " sc-has-error" : "") }, + renderErr && + h("div", { className: "sc-logic-error", "data-omelette-chrome": "" }, renderErr), + h(AncestorContext.Provider, { value: [...chain, this.__name] }, r.tpl(vals, this)), + ); + } + } + __publicField(StreamableComponent, "contextType", AncestorContext); + const named = /* @__PURE__ */ new Map(); + function getDC(name) { + const hit = named.get(name); + if (hit) return hit; + function Dispatcher(p) { + const [, setTick] = React.useState(0); + React.useEffect(() => { + const sub = () => setTick((n) => n + 1); + registry.get(name).subs.add(sub); + return () => { + registry.get(name).subs.delete(sub); + }; + }, []); + ensureFetched(name); + return h(StreamableComponent, { ...p, __name: name }); + } + Dispatcher.displayName = name; + named.set(name, Dispatcher); + return Dispatcher; + } + return { + getDC, + StreamableComponent, + }; + } + + // src/external.ts + var isCustomElementName = (n) => !n.includes(".") && n.includes("-"); + function isRenderableType(g) { + if (typeof g === "function") return !isElementClass(g); + return typeof g === "object" && g !== null && typeof g.$$typeof === "symbol"; + } + function resolveDottedPath(root, name) { + let cur = root; + for (const seg of name.split(".")) { + if (cur == null) return void 0; + cur = cur[seg]; + } + return cur; + } + var BABEL_URL = "https://unpkg.com/@babel/standalone@7.26.4/babel.min.js"; + var GLOBAL_POLL_INTERVAL_MS = 50; + var GLOBAL_POLL_TIMEOUT_MS = 3e4; + function createExternalModules(onResolved) { + const cache = /* @__PURE__ */ new Map(); + let babelLoading = null; + const reportedMissing = /* @__PURE__ */ new Map(); + const polling = /* @__PURE__ */ new Set(); + function ensureBabel() { + if (window.Babel) return Promise.resolve(); + if (babelLoading) return babelLoading; + babelLoading = new Promise((res, rej) => { + const s = document.createElement("script"); + s.src = BABEL_URL; + s.crossOrigin = "anonymous"; + s.onload = () => res(); + s.onerror = rej; + document.head.appendChild(s); + }); + return babelLoading; + } + const pending = /* @__PURE__ */ new Map(); + function load(kind, url, after) { + const existing = pending.get(url); + if (existing) return existing; + cache.set(url, null); + console.info("[dc-runtime] x-import: loading", url, "(" + kind + ")"); + const ready = Promise.all([ + kind === "jsx" ? ensureBabel() : Promise.resolve(), + after ?? Promise.resolve(), + ]); + const p = ready + .then(() => fetch(url)) + .then((r) => { + if (!r.ok) throw new Error("HTTP " + r.status); + return r.text(); + }) + .then((src) => { + const code = + kind === "jsx" + ? window.Babel.transform(src, { + filename: url, + presets: ["react", "typescript"], + }).code + : src; + const module = { exports: {} }; + const before = new Set(Object.keys(window)); + //! nosemgrep: eval-and-function-constructor + new Function("React", "module", "exports", "require", code)( + getReact(), + module, + module.exports, + () => ({}), + ); + const globals = {}; + for (const k of Object.keys(window)) { + if (!before.has(k) && typeof window[k] === "function") { + globals[k] = window[k]; + } + } + cache.set(url, { mod: module.exports, globals }); + console.info( + "[dc-runtime] x-import: loaded", + url, + "\u2014 exports:", + Object.keys(module.exports), + "window globals:", + Object.keys(globals), + ); + onResolved(); + }) + .catch((e) => { + cache.set(url, { + mod: {}, + globals: {}, + error: "failed to load: " + (e instanceof Error && e.message ? e.message : String(e)), + }); + console.error("[dc-runtime] x-import: FAILED to load", url, "(" + kind + ")", e); + onResolved(); + }); + pending.set(url, p); + return p; + } + function resolve2(url, name) { + const entry = cache.get(url); + if (!entry) return null; + const { mod, globals } = entry; + const C = + (mod && mod[name]) || + (globals && globals[name]) || + (typeof window !== "undefined" && window[name]) || + (mod && mod.default); + if (typeof C === "function") return C; + const key = url + "\0" + name; + if (!reportedMissing.has(key)) { + reportedMissing.set( + key, + entry.error || + 'no export named "' + name + '" (has: ' + Object.keys(mod).join(", ") + ")", + ); + console.error( + "[dc-runtime] x-import: module", + url, + "loaded but has no component named", + JSON.stringify(name), + "\u2014 available exports:", + Object.keys(mod), + "window globals:", + Object.keys(globals), + ". The module must `module.exports = {" + name + "}` or set `window." + name + "`.", + ); + } + return null; + } + function waitForGlobal(name) { + if (polling.has(name)) return; + polling.add(name); + const started = Date.now(); + const isCE = isCustomElementName(name); + const tick = () => { + const found = isCE + ? customElements.get(name) + : isRenderableType(resolveDottedPath(window, name)); + if (found) { + polling.delete(name); + onResolved(); + return; + } + if (Date.now() - started >= GLOBAL_POLL_TIMEOUT_MS) { + console.warn( + "[dc-runtime] x-import: global", + JSON.stringify(name), + "never appeared on window after " + GLOBAL_POLL_TIMEOUT_MS + "ms", + ); + return; + } + setTimeout(tick, GLOBAL_POLL_INTERVAL_MS); + }; + setTimeout(tick, GLOBAL_POLL_INTERVAL_MS); + } + function resolveGlobal(url, name) { + const isCE = isCustomElementName(name); + if (!url) { + if (isCE) { + if (customElements.get(name)) return name; + waitForGlobal(name); + return null; + } + const g2 = resolveDottedPath(window, name); + if (isRenderableType(g2)) return g2; + waitForGlobal(name); + return null; + } + const entry = cache.get(url); + if (!entry) return null; + if (isCE && customElements.get(name)) return name; + const g = entry.globals[name] ?? resolveDottedPath(window, name); + if (isRenderableType(g)) return g; + if (name.includes(".")) return null; + const key = url + "\0global\0" + name; + if (!reportedMissing.has(key)) { + reportedMissing.set(key, null); + if (isCE && !customElements.get(name)) { + console.warn( + "[dc-runtime] x-import:", + url, + "loaded but no custom element", + JSON.stringify(name), + "is registered and window." + + name + + " is not a function \u2014 rendering <" + + name + + "> as an unknown element.", + ); + } + } + return name; + } + function getError(url, name) { + const entry = cache.get(url); + if (entry?.error) return entry.error; + return reportedMissing.get(url + "\0" + name) || null; + } + return { load, resolve: resolve2, resolveGlobal, getError }; + } + function isElementClass(g) { + try { + return ( + typeof g === "function" && + typeof HTMLElement !== "undefined" && + g.prototype instanceof HTMLElement + ); + } catch { + return false; + } + } + + // src/atomics.ts + var ATOMIC_CSS = + // layout + ".fx{display:flex}.col{display:flex;flex-direction:column}.grid{display:grid}.ac{align-items:center}.jc{justify-content:center}.jb{justify-content:space-between}.f1{flex:1}.noshrink{flex-shrink:0}.wrap{flex-wrap:wrap}.fw5{font-weight:500}.fw6{font-weight:600}.fw7{font-weight:700}.fw8{font-weight:800}.fs11{font-size:11px}.fs12{font-size:12px}.fs13{font-size:13px}.fs14{font-size:14px}.fs15{font-size:15px}.fs16{font-size:16px}.fs20{font-size:20px}.fs22{font-size:22px}.upper{text-transform:uppercase}.tc{text-align:center}.nowrap{white-space:nowrap}.gap8{gap:8px}.gap10{gap:10px}.gap12{gap:12px}.gap16{gap:16px}.gap24{gap:24px}.m0{margin:0}.mt8{margin-top:8px}.mt12{margin-top:12px}.mt16{margin-top:16px}.mb8{margin-bottom:8px}.mb12{margin-bottom:12px}.mb16{margin-bottom:16px}.posrel{position:relative}.posabs{position:absolute}.round{border-radius:50%}.ohide{overflow:hidden}.bbox{box-sizing:border-box}.pointer{cursor:pointer}.w100{width:100%}.b0{border:none}"; + + // src/helmet.ts + var DESIGN_DOC_MODE_RE = + /]*\bname\s*=\s*["']design_doc_mode["'][^>]*\b(?:content|value)\s*=\s*["'](\w+)["']/i; + var CANVAS_BG = "#f0eee9"; + function createHelmetManager(doc, isStreaming) { + const mounted = /* @__PURE__ */ new Set(); + const live = /* @__PURE__ */ new Map(); + let designDocMode = null; + let canvasStyleEl = null; + function postDesignMode(mode) { + if (window.parent === window) return; + try { + window.parent.postMessage({ type: "__dc_design_mode", mode }, "*"); + } catch {} + } + function setDesignDocMode(mode) { + if (mode === designDocMode) return; + designDocMode = mode; + postDesignMode(mode); + if (mode === "canvas") { + doc.documentElement.setAttribute("data-dc-canvas", ""); + canvasStyleEl = doc.createElement("style"); + canvasStyleEl.setAttribute("data-dc-canvas", ""); + canvasStyleEl.textContent = `html,body{background:${CANVAS_BG}}#dc-root>.sc-host{position:relative}`; + doc.head.appendChild(canvasStyleEl); + } else { + doc.documentElement.removeAttribute("data-dc-canvas"); + canvasStyleEl?.remove(); + canvasStyleEl = null; + } + } + window.addEventListener("message", (e) => { + if (!designDocMode || (e.data && e.data.type) !== "__dc_probe") return; + postDesignMode(designDocMode); + }); + function compile(node) { + const raw = [...node.children]; + const helmetClosed = node.nextSibling != null || node.parentNode?.nextSibling != null; + if (node.hasAttribute("data-dc-atomics") && !mounted.has("__dc-atomics")) { + mounted.add("__dc-atomics"); + const el = doc.createElement("style"); + el.id = "__dc-atomics"; + el.textContent = ATOMIC_CSS; + doc.head.appendChild(el); + } + return (_vals, ctx) => { + const name = (ctx && ctx.__name) || ""; + const streaming = !!(name && isStreaming(name)); + for (let i = 0; i < raw.length; i++) { + const child = raw[i]; + const tag = child.tagName; + const mayBePartial = streaming && !helmetClosed && i === raw.length - 1; + if (tag === "SCRIPT") { + if (mayBePartial) continue; + const key = "SCRIPT|" + (child.getAttribute("src") || child.textContent || ""); + if (mounted.has(key)) continue; + mounted.add(key); + const el = doc.createElement("script"); + for (const { name: an, value } of [...child.attributes]) el.setAttribute(an, value); + if (child.textContent) el.textContent = child.textContent; + doc.head.appendChild(el); + } else if (tag === "LINK" || tag === "META") { + if (mayBePartial) continue; + const key = + tag + + "|" + + (child.getAttribute("href") || child.getAttribute("src") || child.outerHTML); + if (mounted.has(key)) continue; + mounted.add(key); + doc.head.appendChild(child.cloneNode(true)); + } else { + const key = name + "|" + i; + let el = live.get(key); + if (!el || el.tagName !== tag) { + if (el) el.remove(); + el = doc.createElement(tag.toLowerCase()); + live.set(key, el); + doc.head.appendChild(el); + } + for (const { name: an, value } of [...child.attributes]) { + if (el.getAttribute(an) !== value) el.setAttribute(an, value); + } + if (el.textContent !== child.textContent) el.textContent = child.textContent; + } + } + return null; + }; + } + return { compile, setDesignDocMode }; + } + + // src/pseudo.ts + function createPseudoSheet(doc) { + let el = null; + const cache = /* @__PURE__ */ new Map(); + let n = 0; + return (pseudo, css) => { + const k = pseudo + "|" + css; + const hit = cache.get(k); + if (hit) return hit; + if (!el) { + el = doc.createElement("style"); + doc.head.appendChild(el); + } + const cls = "scp" + (n++).toString(36); + const sel = + pseudo === "before" || pseudo === "after" + ? "." + cls + "::" + pseudo + : "." + cls + ":" + pseudo; + el.sheet.insertRule(sel + "{" + css + "}", el.sheet.cssRules.length); + cache.set(k, cls); + return cls; + }; + } + + // src/registry.ts + function createRegistry() { + const entries = /* @__PURE__ */ Object.create(null); + function get(name) { + return ( + entries[name] || + (entries[name] = { + html: "", + tpl: null, + Logic: null, + jsStreaming: false, + htmlStreaming: false, + ver: 0, + subs: /* @__PURE__ */ new Set(), + fetched: false, + }) + ); + } + function bump(name) { + const r = get(name); + r.ver++; + for (const fn of r.subs) fn(); + } + return { + entries, + get, + bump, + bumpAll() { + for (const n in entries) bump(n); + }, + }; + } + + // src/runtime.ts + var COMPONENT_DIR = "."; + function createRuntime(doc = document) { + const registry = createRegistry(); + const pseudoClass = createPseudoSheet(doc); + const helmet = createHelmetManager(doc, (name) => registry.get(name).htmlStreaming); + const external = createExternalModules(() => registry.bumpAll()); + const factory = createComponentFactory(registry, ensureFetched); + const host = { + component: (name) => factory.getDC(name), + placeholder: (props) => h(Placeholder, props), + helmet: (node) => helmet.compile(node), + loadExternal: (kind, url, after) => external.load(kind, url, after), + resolveExternal: (url, name) => external.resolve(url, name), + resolveExternalGlobal: (url, name) => external.resolveGlobal(url, name), + resolveExternalError: (url, name) => external.getError(url, name), + pseudoClass, + }; + function ensureFetched(name) { + const r = registry.get(name); + if (r.fetched) return; + r.fetched = true; + const url = COMPONENT_DIR + "/" + encodeURIComponent(name) + ".dc.html"; + fetch(url) + .then((res) => { + if (!res.ok) { + console.error( + "[dc-runtime] sibling fetch for <" + name + "/> failed:", + url, + "returned", + res.status, + "\u2014 the reference renders as an empty placeholder.", + ); + return ""; + } + return res.text(); + }) + .then((t) => { + if (!t) return; + const parsed = parseDcText(t); + if (!parsed) { + console.error( + "[dc-runtime] sibling fetch for <" + name + "/>:", + url, + "has no block \u2014 not a Design Component.", + ); + return; + } + if (parsed.props) r.propsMeta = parsed.props; + if (parsed.preview) r.preview = parsed.preview; + if (parsed.template && !r.html) updateHtml(name, parsed.template); + if (parsed.js && !r.Logic) updateJs(name, parsed.js); + }) + .catch((e) => + console.error("[dc-runtime] sibling fetch for <" + name + "/> threw:", url, e), + ); + } + let rootName = null; + function updateHtml(name, html) { + const r = registry.get(name); + r.html = html; + if (name === rootName) { + const mode = DESIGN_DOC_MODE_RE.exec(html)?.[1] ?? null; + if (mode || !r.htmlStreaming) helmet.setDesignDocMode(mode); + } + try { + r.tpl = compileTemplate(html, host); + } catch (e) { + console.error("[dc-runtime] template compile FAILED for", name, e); + } + registry.bump(name); + } + function updateJs(name, src) { + const r = registry.get(name); + const seq = (r.jsSeq = (r.jsSeq || 0) + 1); + try { + const Cls = evalDcLogic(src); + if (r.jsSeq !== seq) return; + if (typeof Cls !== "function") { + r.logicError = + name + + ".dc.html: + + Payment Intelligence Modules. ISO 20022 review tools - Invoice 2025-04 — consulting + Invoice 2025-04 · consulting diff --git a/public/samples/scrubber/index.json b/public/samples/scrubber/index.json index 3d15d3f..df8a56d 100644 --- a/public/samples/scrubber/index.json +++ b/public/samples/scrubber/index.json @@ -2,25 +2,25 @@ "samples": [ { "id": "pacs008-customer", - "label": "pacs.008 — Customer credit transfer", + "label": "pacs.008 · Customer credit transfer", "kind": "pacs.008", "path": "/samples/scrubber/pacs008-customer-credit-transfer.xml" }, { "id": "pacs009-fi", - "label": "pacs.009 — FI credit transfer (cover)", + "label": "pacs.009 · FI credit transfer (cover)", "kind": "pacs.009", "path": "/samples/scrubber/pacs009-fi-credit-transfer.xml" }, { "id": "pacs002-status", - "label": "pacs.002 — Payment status report", + "label": "pacs.002 · Payment status report", "kind": "pacs.002", "path": "/samples/scrubber/pacs002-status-report.xml" }, { "id": "camt053-statement", - "label": "camt.053 — Bank statement", + "label": "camt.053 · Bank statement", "kind": "camt.053", "path": "/samples/scrubber/camt053-bank-statement.xml" } diff --git a/public/samples/scrubber/pacs008-customer-credit-transfer.xml b/public/samples/scrubber/pacs008-customer-credit-transfer.xml index 04f7dd8..c1ff700 100644 --- a/public/samples/scrubber/pacs008-customer-credit-transfer.xml +++ b/public/samples/scrubber/pacs008-customer-credit-transfer.xml @@ -2,8 +2,8 @@ - SAMPLE-PACS008-001 - 2025-04-15T09:25:00+00:00 + TX-20260612-7741 + 2026-06-12T09:14:22+00:00 1 INDA @@ -11,57 +11,55 @@ - SAMPLE-PACS008-INSTR-001 - SAMPLE-E2E-001 + TX-20260612-7741-INSTR + INV-7741-2026 7a562c67-ca16-48ba-b074-65581be6f001 - 2450 - 2025-04-15 - DEBT + 18720.00 + 2026-06-12 + SHAR - CBRLGB2LXXX + DEUTDEFF - RBOSGBCHXXX + NWBKGB2L - Sandra Baldrick + Okonkwo Logistics GmbH - 10 Bean Street - London - GB + Mainzer Landstrasse 191 + Frankfurt am Main + DE - sandra.b@example.com + treasury@okonkwo-logistics.example - - 25698745 - + DE89370400440532013000 - CBRLGB2LXXX + DEUTDEFF - DEUTDEFFXXX + NWBKGB2L - Albert Tate + Meridian Trading Ltd - 9 Dimitrie - Bucharest - RO + 14 Eastcheap + London + GB @@ -70,7 +68,7 @@ - Invoice 2025-INV-77 + Invoice 7741 freight services May 2026 diff --git a/public/samples/storyteller/index.json b/public/samples/storyteller/index.json index 221fa8a..dcb0bda 100644 --- a/public/samples/storyteller/index.json +++ b/public/samples/storyteller/index.json @@ -1,50 +1,57 @@ { "samples": [ + { + "id": "pacs008-serial", + "label": "pacs.008 · Serial customer credit transfer", + "kind": "pacs.008", + "support": "full", + "path": "/samples/storyteller/pacs008-serial-credit-transfer.xml" + }, { "id": "pacs008-cover", - "label": "pacs.008 — Cover payment (USD)", + "label": "pacs.008 · Cover payment (USD)", "kind": "pacs.008", "support": "full", "path": "/samples/storyteller/pacs008-cover-payment.xml" }, { "id": "pacs009-fi", - "label": "pacs.009 — FI credit transfer (INR)", + "label": "pacs.009 · FI credit transfer (INR)", "kind": "pacs.009", "support": "full", "path": "/samples/storyteller/pacs009-fi-credit-transfer.xml" }, { "id": "camt053-multi", - "label": "camt.053 — Daily statement (multi-entry)", + "label": "camt.053 · Daily statement (multi-entry)", "kind": "camt.053", "support": "full", "path": "/samples/storyteller/camt053-multi-entry-statement.xml" }, { "id": "pacs002-reject", - "label": "pacs.002 — Reject (MD07)", + "label": "pacs.002 · Reject (MD07)", "kind": "pacs.002", "support": "structured", "path": "/samples/storyteller/pacs002-reject-md07.xml" }, { "id": "pacs004-return", - "label": "pacs.004 — Return (AC06)", + "label": "pacs.004 · Return (AC06)", "kind": "pacs.004", "support": "structured", "path": "/samples/storyteller/pacs004-return-ac06.xml" }, { "id": "camt052-intraday", - "label": "camt.052 — Intraday account report", + "label": "camt.052 · Intraday account report", "kind": "camt.052", "support": "structured", "path": "/samples/storyteller/camt052-intraday-report.xml" }, { "id": "camt054-credit", - "label": "camt.054 — Credit notification", + "label": "camt.054 · Credit notification", "kind": "camt.054", "support": "structured", "path": "/samples/storyteller/camt054-credit-notification.xml" diff --git a/public/samples/storyteller/pacs008-serial-credit-transfer.xml b/public/samples/storyteller/pacs008-serial-credit-transfer.xml new file mode 100644 index 0000000..5016ae0 --- /dev/null +++ b/public/samples/storyteller/pacs008-serial-credit-transfer.xml @@ -0,0 +1,72 @@ + + + + + TX-20260612-7741 + 2026-06-12T09:14:22+00:00 + 1 + + INDA + + + + + TX-20260612-7741-INSTR + INV-7741-2026 + 7a562c67-ca16-48ba-b074-65581be6f001 + + 18720.00 + 2026-06-12 + SHAR + + + DEUTDEFF + + + + + NWBKGB2L + + + + Okonkwo Logistics GmbH + + Mainzer Landstrasse 191 + Frankfurt am Main + DE + + + + + DE89370400440532013000 + + + + + DEUTDEFF + + + + + NWBKGB2L + + + + Meridian Trading Ltd + + 14 Eastcheap + London + GB + + + + + GB29NWBK60161331926819 + + + + Invoice 7741 freight services May 2026 + + + + diff --git a/scripts/build-bic-snapshot.ts b/scripts/build-bic-snapshot.ts index 5d23629..55e26c8 100644 --- a/scripts/build-bic-snapshot.ts +++ b/scripts/build-bic-snapshot.ts @@ -2,7 +2,7 @@ * Build-time generator for `src/lib/bic/data/bic-snapshot.generated.json` * (and its sidecar `*.meta.json`) from the read-only SWIFT BIC Directory * extract. The bundled subset is intentionally small (30 entries) and is - * for demonstration only — it is not authoritative bank-directory data. + * for demonstration only · it is not authoritative bank-directory data. * * Source dir is read-only; the script never writes back into it. * @@ -11,7 +11,7 @@ * - --check: regenerate and assert byte equality against the committed * copies; exit non-zero on drift * - * The output is fully deterministic — no `generatedAt`, no absolute paths, + * The output is fully deterministic · no `generatedAt`, no absolute paths, * no machine-local fields. `sourceFile` is the basename of the directory * file, never an absolute path. */ @@ -30,7 +30,7 @@ const SHA_BASENAME = "sha256sum.txt"; const SNAPSHOT_DATE = "2026-01-03"; const DEMO_NOTICE = - "Demonstration only — the bundled BIC data is not accurate and not for production use."; + "Demonstration only · the bundled BIC data is not accurate and not for production use."; const DEFAULT_SOURCE_DIR = "/Users/Shared/AgentWork/knowledge/swift-payments/extracted-reference/_by-source/BIC-DIRECTORY-F-2026-01-03-TXT__64fa657003"; @@ -287,7 +287,7 @@ async function checkAgainstCommitted(outputs: BuildOutputs): Promise { } if (failures.length > 0) { - for (const f of failures) console.error(`data:bic:check — ${f}`); + for (const f of failures) console.error(`data:bic:check · ${f}`); process.exitCode = 1; throw new Error("data:bic:check failed"); } @@ -299,11 +299,11 @@ async function main(): Promise { const outputs = build(); if (checkMode) { await checkAgainstCommitted(outputs); - console.log("data:bic:check — committed JSON + meta match regenerated output (deterministic)."); + console.log("data:bic:check · committed JSON + meta match regenerated output (deterministic)."); } else { await writeOutputs(outputs); console.log( - `data:bic — wrote ${OUT_DATA_PATH} and ${OUT_META_PATH} ` + + `data:bic · wrote ${OUT_DATA_PATH} and ${OUT_META_PATH} ` + `(deterministic, no time/local fields).`, ); } diff --git a/scripts/build-cbprplus-schema-manifest.ts b/scripts/build-cbprplus-schema-manifest.ts index afb06fc..aab3e34 100644 --- a/scripts/build-cbprplus-schema-manifest.ts +++ b/scripts/build-cbprplus-schema-manifest.ts @@ -276,7 +276,7 @@ async function checkAgainstCommitted(outputs: BuildOutputs): Promise { } if (failures.length > 0) { - for (const f of failures) console.error(`data:cbpr:check — ${f}`); + for (const f of failures) console.error(`data:cbpr:check · ${f}`); process.exitCode = 1; throw new Error("data:cbpr:check failed"); } @@ -289,12 +289,12 @@ async function main(): Promise { if (checkMode) { await checkAgainstCommitted(outputs); console.log( - "data:cbpr:check — committed JSON + meta match regenerated output (deterministic).", + "data:cbpr:check · committed JSON + meta match regenerated output (deterministic).", ); } else { await writeOutputs(outputs); console.log( - `data:cbpr — wrote ${OUT_DATA_PATH} and ${OUT_META_PATH} ` + + `data:cbpr · wrote ${OUT_DATA_PATH} and ${OUT_META_PATH} ` + `(deterministic, no time/local fields).`, ); } diff --git a/scripts/build-iban-registry.ts b/scripts/build-iban-registry.ts index 0f23125..6873b88 100644 --- a/scripts/build-iban-registry.ts +++ b/scripts/build-iban-registry.ts @@ -4,7 +4,7 @@ * * Imports only pure helpers from `src/lib/iban/internal/*` via the * `scripts/lib/parse-registry.ts` parser. Never imports the runtime - * `validate.ts`, `registry.ts`, or any generated artifact — that decoupling + * `validate.ts`, `registry.ts`, or any generated artifact · that decoupling * is intentional so the cross-check is a pure function of the freshly parsed * source. * @@ -13,7 +13,7 @@ * - --check: regenerate to a temp dir and assert byte equality against * the committed copies; exit non-zero on drift * - * The output is fully deterministic — no `generatedAt`, no absolute paths, + * The output is fully deterministic · no `generatedAt`, no absolute paths, * no machine-local fields. `sourceFile` is the basename of the registry * file, never an absolute path. */ @@ -180,7 +180,7 @@ async function checkAgainstCommitted(outputs: BuildOutputs): Promise { } if (failures.length > 0) { - for (const f of failures) console.error(`data:iban:check — ${f}`); + for (const f of failures) console.error(`data:iban:check · ${f}`); process.exitCode = 1; throw new Error("data:iban:check failed"); } @@ -193,12 +193,12 @@ async function main(): Promise { if (checkMode) { await checkAgainstCommitted(outputs); console.log( - "data:iban:check — committed JSON + meta match regenerated output (deterministic).", + "data:iban:check · committed JSON + meta match regenerated output (deterministic).", ); } else { await writeOutputs(outputs); console.log( - `data:iban — wrote ${OUT_DATA_PATH} and ${OUT_META_PATH} ` + + `data:iban · wrote ${OUT_DATA_PATH} and ${OUT_META_PATH} ` + `(deterministic, no time/local fields).`, ); } diff --git a/scripts/lib/parse-bic-directory.ts b/scripts/lib/parse-bic-directory.ts index 04e8a10..eb88266 100644 --- a/scripts/lib/parse-bic-directory.ts +++ b/scripts/lib/parse-bic-directory.ts @@ -1,7 +1,7 @@ /** * Pure parser for the SWIFT BIC Directory `IDENTIFIERS-BIC-V1-*.txt` file. * Imports only Node-stdlib APIs. Never imports the runtime BIC module or - * any generated artifact — the cross-check is a pure function of the + * any generated artifact · the cross-check is a pure function of the * freshly parsed source. * * The file is a single-line-per-record TSV: row 0 is a column header, row 1 @@ -16,7 +16,7 @@ export interface BicDirectoryRow { recordContentType: string; /** Fixed RECORD STATUS column (`C` for current records in this snapshot). */ recordStatus: string; - /** ID column — typically the BIC11. */ + /** ID column · typically the BIC11. */ id: string; /** ID TYPE column (e.g. `BIC11`, `BIC8`). */ idType: string; diff --git a/scripts/lib/parse-registry.ts b/scripts/lib/parse-registry.ts index 71ed15e..7d3ca6f 100644 --- a/scripts/lib/parse-registry.ts +++ b/scripts/lib/parse-registry.ts @@ -1,6 +1,6 @@ /** * Pure parser for the SWIFT IBAN Registry .txt file. Imports only Node-stdlib - * APIs and the language-level pure helpers under `src/lib/iban/internal/*` — + * APIs and the language-level pure helpers under `src/lib/iban/internal/*` · * never the runtime `validate.ts`, `registry.ts`, or any generated artifact. * * The registry .txt is a transposed TSV: row 0 is a "Data element" header, diff --git a/scripts/lib/zip-read.ts b/scripts/lib/zip-read.ts index b8427a2..58475fc 100644 --- a/scripts/lib/zip-read.ts +++ b/scripts/lib/zip-read.ts @@ -10,7 +10,7 @@ * file headers' optional Zip64 fields. The MyStandards export is a small * single-volume archive well below the 4 GiB Zip32 limits. * - * Imports `node:fs`, `node:zlib`, and `Buffer` only — no third-party deps. + * Imports `node:fs`, `node:zlib`, and `Buffer` only · no third-party deps. */ import { readFileSync } from "node:fs"; diff --git a/scripts/privacy-audit.sh b/scripts/privacy-audit.sh index 98c9879..2fa2946 100755 --- a/scripts/privacy-audit.sh +++ b/scripts/privacy-audit.sh @@ -1,11 +1,11 @@ #!/usr/bin/env bash -# Privacy audit — fails if browser-side persistence or external network calls +# Privacy audit · fails if browser-side persistence or external network calls # appear inside src/. # # What we ban inside src/: # - localStorage / sessionStorage / indexedDB used as objects (e.g. `localStorage.setItem`) # - navigator.sendBeacon(...) -# - fetch("http…") / fetch('http…') — any absolute URL begins a network call +# - fetch("http…") / fetch('http…') · any absolute URL begins a network call # that leaves this site. # # What we additionally ban inside the IBAN module (`src/lib/iban`, @@ -23,8 +23,8 @@ # after the identifier so we only flag actual API usage. # # Exit codes: -# 0 — clean -# 1 — at least one banned pattern found +# 0 · clean +# 1 · at least one banned pattern found set -euo pipefail diff --git a/src/App.workbench.test.tsx b/src/App.workbench.test.tsx index 9bd34e5..0f46559 100644 --- a/src/App.workbench.test.tsx +++ b/src/App.workbench.test.tsx @@ -23,7 +23,7 @@ const ROUTES: [string, RegExp][] = [ ["/pilot", /private-pilot scaffold/i], ]; -describe("App — workbench & platform routes", () => { +describe("App · workbench & platform routes", () => { it.each(ROUTES)("renders %s", (path, heading) => { renderAt(path); expect(screen.getByRole("heading", { level: 1, name: heading })).toBeInTheDocument(); diff --git a/src/components/bic/BicBreakdown.tsx b/src/components/bic/BicBreakdown.tsx index ce6a492..ea87828 100644 --- a/src/components/bic/BicBreakdown.tsx +++ b/src/components/bic/BicBreakdown.tsx @@ -37,15 +37,15 @@ function buildRows(components: BicComponents): ComponentRow[] { value: components.locationCode, hint: advisoryHints.length > 0 - ? `Two alphanumerics — ${advisoryHints.join(", ")}.` - : "Two alphanumerics — typically the city or routing region.", + ? `Two alphanumerics · ${advisoryHints.join(", ")}.` + : "Two alphanumerics · typically the city or routing region.", }, { id: "branchCode", label: "Branch code", value: components.branchCode, hint: components.isPrimaryOffice - ? "XXX — primary office." + ? "XXX · primary office." : "Three alphanumerics identifying a specific branch.", }, ]; @@ -61,10 +61,10 @@ export function BicBreakdown({ result }: BicBreakdownProps) { aria-label="Pretty-printed BIC" className="mt-2 break-all rounded-md border border-border bg-card px-3 py-2 font-mono text-base text-foreground" > - {result.pretty || result.normalized || "—"} + {result.pretty || result.normalized || "·"}

- Normalized: {result.normalized || "—"} + Normalized: {result.normalized || "·"} {result.expanded && result.expanded !== result.normalized ? ( <> {" · "}Expanded to BIC11:{" "} @@ -116,7 +116,7 @@ export function BicBreakdown({ result }: BicBreakdownProps) {

- Demonstration only — the bundled BIC data is not accurate, and absence does not mean the + Demonstration only · the bundled BIC data is not accurate, and absence does not mean the BIC is invalid.

diff --git a/src/components/bic/BicSamples.tsx b/src/components/bic/BicSamples.tsx index 9ceae11..8fe7f01 100644 --- a/src/components/bic/BicSamples.tsx +++ b/src/components/bic/BicSamples.tsx @@ -11,7 +11,7 @@ interface BicSamplesProps { /** * Picker that loads a bundled demo BIC into the input. The list is bundled - * — no network fetch. + * · no network fetch. */ export function BicSamples({ samples, value, onPick, disabled }: BicSamplesProps) { const options = useMemo(() => { diff --git a/src/components/iban/IbanBreakdown.tsx b/src/components/iban/IbanBreakdown.tsx index 5bd81ab..8682b75 100644 --- a/src/components/iban/IbanBreakdown.tsx +++ b/src/components/iban/IbanBreakdown.tsx @@ -53,7 +53,7 @@ export function IbanBreakdown({ result }: IbanBreakdownProps) { aria-label="Pretty-printed IBAN" className="mt-2 break-all rounded-md border border-border bg-card px-3 py-2 font-mono text-base text-foreground" > - {result.pretty || result.normalized || "—"} + {result.pretty || result.normalized || "·"}

@@ -62,7 +62,7 @@ export function IbanBreakdown({ result }: IbanBreakdownProps) { aria-label="Normalized IBAN" className="mt-2 break-all rounded-md border border-border bg-card px-3 py-2 font-mono text-sm text-muted-foreground" > - {result.normalized || "—"} + {result.normalized || "·"}

diff --git a/src/components/iban/IbanBuildPanel.tsx b/src/components/iban/IbanBuildPanel.tsx index cbd8a1f..88d1501 100644 --- a/src/components/iban/IbanBuildPanel.tsx +++ b/src/components/iban/IbanBuildPanel.tsx @@ -118,7 +118,7 @@ export function IbanBuildPanel({ setCopyState("copied"); window.setTimeout(() => setCopyState("idle"), 1500); } catch { - // Clipboard denied — fall through quietly. + // Clipboard denied · fall through quietly. } }; diff --git a/src/components/iban/IbanProvenancePanel.tsx b/src/components/iban/IbanProvenancePanel.tsx index 9868a1e..35ef9b2 100644 --- a/src/components/iban/IbanProvenancePanel.tsx +++ b/src/components/iban/IbanProvenancePanel.tsx @@ -12,7 +12,7 @@ const nonGoals = [ const privacyBoundary = [ "All parsing, validation, and assembly happen in this page using bundled data.", - "No backend or remote IBAN service is contacted — there is no fetch or XHR in this module.", + "No backend or remote IBAN service is contacted · there is no fetch or XHR in this module.", "No localStorage, sessionStorage, IndexedDB, or cookies are written.", "No analytics, telemetry, or remote logging.", ]; diff --git a/src/components/iban/IbanSamples.tsx b/src/components/iban/IbanSamples.tsx index 2fb2645..01416d2 100644 --- a/src/components/iban/IbanSamples.tsx +++ b/src/components/iban/IbanSamples.tsx @@ -11,7 +11,7 @@ interface IbanSamplesProps { /** * Country picker that loads a known-good registry example IBAN into the - * input. The list is bundled — no network fetch. + * input. The list is bundled · no network fetch. */ export function IbanSamples({ countries, value, onPick, disabled }: IbanSamplesProps) { const options = useMemo(() => { diff --git a/src/components/iban/IbanValidatePanel.tsx b/src/components/iban/IbanValidatePanel.tsx index 75c9f50..738242d 100644 --- a/src/components/iban/IbanValidatePanel.tsx +++ b/src/components/iban/IbanValidatePanel.tsx @@ -61,7 +61,7 @@ export function IbanValidatePanel({ initialIban, onOpenInBuild }: IbanValidatePa setCopyState("copied"); window.setTimeout(() => setCopyState("idle"), 1500); } catch { - // Clipboard access denied — fall back silently; users can still select. + // Clipboard access denied · fall back silently; users can still select. } }; diff --git a/src/components/layout/SuiteFooter.tsx b/src/components/layout/SuiteFooter.tsx index 5367754..2430050 100644 --- a/src/components/layout/SuiteFooter.tsx +++ b/src/components/layout/SuiteFooter.tsx @@ -7,7 +7,7 @@ const platformLinks = [...surfacesByGroup("workbench"), ...surfacesByGroup("plat export function SuiteFooter() { return ( -