Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

| | |
|---|---|
| **Last updated** | 2026-10-08 (wording: "verified payee" replaces "school or landlord") |
| **Last updated** | 2026-10-08 (M3-25 favicon, site header, mobile nav, motion) |
| **Docs baseline** | v0.3, worldwide scope (`PRD.md`, `ARCHITECTURE.md`, `ARCHITECTURE_ESSENTIALS.md`, `AGENTS.md`, `CLAUDE.md`, `project_structure.md`) |
| **Current phase** | Phase 0 (Foundations) → starting Phase 1 (M0 Validate) |
| **Readiness** | See [§3](#3-progress-snapshot) |
Expand Down Expand Up @@ -131,15 +131,15 @@ Readiness % = `DONE ÷ (all rows − DEFERRED − DROPPED)`. Conditional rows (P
| 1 M0 Validate | 17 | 0 | 1 | 16 | 0 | 0 | 0 | 0% |
| 2 Contract + registry | 36 | 16 | 5 | 15 | 0 | 0 | 0 | 44% |
| 3 SDK + indexer | 19 | 1 | 1 | 17 | 0 | 0 | 0 | 5% |
| 4 App | 24 | 9 | 6 | 9 | 0 | 0 | 0 | 38% |
| 4 App | 25 | 10 | 6 | 9 | 0 | 0 | 0 | 40% |
| 5 Testnet pilot | 7 | 0 | 0 | 7 | 0 | 0 | 0 | 0% |
| 6 Pre-mainnet features | 10 | 0 | 0 | 10 | 0 | 0 | 0 | 0% |
| 7 Hardening | 26 | 0 | 0 | 26 | 0 | 0 | 0 | 0% |
| 8 Launch | 8 | 0 | 0 | 8 | 0 | 0 | 0 | 0% |
| 9 Conditional ramp | 9 | 0 | 0 | 0 | 0 | 9 | 0 | n/a |
| 10 Wave + community | 8 | 1 | 0 | 7 | 0 | 0 | 0 | 13% |
| 11 Deferred parking lot | 16 | 0 | 0 | 0 | 0 | 16 | 0 | n/a |
| **All** | **199** | **39** | **15** | **120** | **0** | **25** | **0** | **22%** |
| **All** | **200** | **40** | **15** | **120** | **0** | **25** | **0** | **23%** |

---

Expand Down Expand Up @@ -314,6 +314,7 @@ Readiness % = `DONE ÷ (all rows − DEFERRED − DROPPED)`. Conditional rows (P
| M3-22 | Testnet hosting and environment config | app | P0 | IN PROGRESS | M3-19 | Public testnet URL live |
| M3-23 | Externalize all UI strings (`messages/en.json`) and add `Intl`-based locale-aware formatting; handle zero- and three-decimal currencies, non-Latin text, and RTL-safe layout | app | P0 | DONE | M3-01 | Lint or test fails on inline strings; formatting tests across at least 3 locales and currencies |
| M3-24 | CI guard: fail on hard-coded country, currency, anchor, or locale literals outside registry data, tests, and fixtures | all | P1 | TODO | M3-01, M2-01 | Guard runs in app and sdk CI; passing and failing examples tested |
| M3-25 | Favicon/app icon, persistent site header (nav, docs and GitHub links), mobile hamburger menu, reduced-motion-safe entrance motion | app | P2 | DONE | M3-01, M3-04 | Favicon set in every browser; nav usable one-handed on a narrow viewport; `prefers-reduced-motion` honored |

---

Expand Down Expand Up @@ -523,6 +524,7 @@ Newest first. One entry per PR. Required for every contribution (see §2).

| Date | PR / ref | Repo | Rows touched | Summary |
|---|---|---|---|---|
| 2026-10-08 | `feat/site-polish` | app | DONE: M3-25 (new row) | Added `app/icon.svg`/`app/apple-icon.png` (favicon and iOS icon, from the org's new lock mark in `kinlock-icon/`). Built `components/nav/SiteHeader.tsx`: a persistent header on every page, desktop nav (Send/Request/Verify) plus two separately-styled, separately-destined buttons (outline "Documentation" to `kinlock-org.github.io`, filled-neutral "GitHub" to `github.com/Kinlock-Org`), collapsing to a hamburger disclosure menu below `md` (closes on route change and Escape). Fixed the landing footer's stale docs link, which pointed at a raw GitHub markdown file instead of the real docs site. Installed the `emilkowalski/skills` `animate` skill and used it for the menu's entrance and a staggered hero fade/slide-up on the landing page (CSS-only, no new dependency, both gated under `prefers-reduced-motion`). Verified: `pnpm lint`, `pnpm typecheck`, `pnpm test` (93/93), `pnpm build` all green; visually checked desktop, mobile closed/open, and dark mode with Playwright screenshots. Not done here: the GitHub org avatar itself (no API/CLI command sets it; recommended `kinlock-icon/icon-light-1024.png`, needs a human to upload via org settings) |
| 2026-10-08 | `docs/verified-payee-wording` | app | no row changes | Synced `AGENTS.md`/`docs/PRD.md`/`docs/ARCHITECTURE_ESSENTIALS.md` from `Kinlock-Org/.github` (`scripts/sync-docs.sh`), and updated `messages/en.json` (`app.tagline`, `pages.home.heroHeadline`/`heroSubtext`/`lockBody`/`sendersBody`, `pages.send.intro`): "a verified school or landlord" replaced with "a verified payee" everywhere it was used as a generic stand-in for the product's reach, rather than naming the two current MVP categories factually. Verified: `pnpm lint`, `pnpm typecheck`, `pnpm test` (92/92, including the inline-strings guard) all green. Canonical doc source edited in `.github` PR #23 |
| 2026-10-08 | `feat/m3-13-receipt-verify` | app | DONE: M3-13 | Built `/r/[txHash]/[eventIndex]` and `/verify`, replacing their `PageStub`s. Both call the SDK's `verifyReceipt` (chain-authoritative tiered verification) and show "Valid"/"Not valid"; the "Payment to verified payee" headline (hard rule 7) renders only when `result.valid` is actually `true`. A separate `getLock` read is used purely to label the asset (code and issuer) since `VerifyReceiptResult.receipt` doesn't carry the token; falls back to an unlabeled amount rather than guessing when that lock is unavailable. New shared pieces: `lib/receipts/parse.ts` (pure link/hash parsing, unit-tested), `lib/receipts/receipt-text.ts` (message-key mapping), `components/receipts/ReceiptResult.tsx` (shared result view for both pages). Verified: `pnpm lint`, `pnpm typecheck`, `pnpm test` (92/92 passed, including new `parse.test.ts`), `pnpm build` all green |
| 2026-10-08 | `docs/link-doc-hub` | app | no row changes (org row W-10 tracked in `.github`) | Linked `Kinlock-Org.github.io` (the org's documentation-issue hub) from README, with `area:app` |
Expand Down
13 changes: 6 additions & 7 deletions app/(marketing)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@ function Hero() {
<section className="mx-auto max-w-5xl px-4 pt-16 pb-12 md:pt-20">
<div className="grid gap-10 md:grid-cols-2 md:items-center md:gap-16">
<div>
<h1 className="text-4xl leading-tight font-semibold tracking-tight md:text-5xl">
<h1 className="reveal text-4xl leading-tight font-semibold tracking-tight md:text-5xl">
{t("pages.home.heroHeadline")}
</h1>
<p className="mt-4 max-w-[36ch] text-lg text-ink/70">{t("pages.home.heroSubtext")}</p>
<p className="reveal reveal-1 mt-4 max-w-[36ch] text-lg text-ink/70">
{t("pages.home.heroSubtext")}
</p>
<a
href="/send"
className="mt-8 inline-flex items-center rounded-full bg-accent px-6 py-3 font-medium text-accent-ink transition-transform active:scale-[0.98]"
className="reveal reveal-2 mt-8 inline-flex items-center rounded-full bg-accent px-6 py-3 font-medium text-accent-ink transition-transform active:scale-[0.98]"
>
{t("pages.home.heroCta")}
</a>
Expand Down Expand Up @@ -120,10 +122,7 @@ function Footer() {
<a href="https://github.com/Kinlock-Org" className="underline underline-offset-4">
{t("pages.home.footerSource")}
</a>
<a
href="https://github.com/Kinlock-Org/.github/blob/main/docs/ARCHITECTURE_ESSENTIALS.md"
className="underline underline-offset-4"
>
<a href="https://kinlock-org.github.io" className="underline underline-offset-4">
{t("pages.home.footerDocs")}
</a>
</nav>
Expand Down
Binary file added app/apple-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
55 changes: 55 additions & 0 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
--color-accent-ink: var(--accent-ink);
--color-line: var(--line);
--font-sans: var(--font-outfit), ui-sans-serif, system-ui, sans-serif;
/* Strong ease-out for UI entrances/exits (animate skill); replaces Tailwind's weaker default. */
--ease-out: cubic-bezier(0.23, 1, 0.32, 1);
}

:root {
Expand All @@ -27,6 +29,59 @@
}
}

/* Mobile nav disclosure panel: enters from the header, per the animate skill's menu recipe. */
@keyframes menu-in {
from {
opacity: 0;
transform: translateY(-8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}

.menu-panel {
animation: menu-in 200ms var(--ease-out) both;
}

@media (prefers-reduced-motion: reduce) {
.menu-panel {
animation-duration: 1ms;
}
}

/* Hero entrance stagger (animate skill: rare/first-tier content, purpose = explanation). */
@keyframes reveal-up {
from {
opacity: 0;
transform: translateY(12px);
}
to {
opacity: 1;
transform: translateY(0);
}
}

.reveal {
animation: reveal-up 500ms var(--ease-out) both;
}

.reveal-1 {
animation-delay: 80ms;
}

.reveal-2 {
animation-delay: 160ms;
}

@media (prefers-reduced-motion: reduce) {
.reveal {
animation-duration: 1ms;
animation-delay: 0ms;
}
}

/* Visible to screen readers only (e.g. a full Stellar address next to its shortened form). */
.sr-only {
position: absolute;
Expand Down
13 changes: 13 additions & 0 deletions app/icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Outfit } from "next/font/google";
import type { ReactNode } from "react";
import { SiteHeader } from "@/components/nav/SiteHeader";
import { t } from "@/lib/i18n/messages";
import "./globals.css";

Expand All @@ -17,6 +18,7 @@ export default function RootLayout({ children }: { children: ReactNode }) {
<div className="bg-ink px-4 py-2 text-center text-sm text-paper">
{t("common.testnetBanner")}
</div>
<SiteHeader />
{children}
</body>
</html>
Expand Down
153 changes: 153 additions & 0 deletions components/nav/SiteHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
"use client";

import { usePathname } from "next/navigation";
import { useEffect, useId, useState } from "react";
import { t } from "@/lib/i18n/messages";

const LINKS = [
{ href: "/send", key: "nav.send" as const },
{ href: "/request", key: "nav.request" as const },
{ href: "/verify", key: "nav.verify" as const },
];

const navLinkClass = (active: boolean) =>
`text-sm font-medium transition-colors motion-reduce:transition-none ${
active ? "text-accent" : "text-ink/70 hover:text-accent"
}`;

/**
* Persistent site header: brand, primary nav, and two separately-styled, separately-destined
* buttons for the docs site and the GitHub repos (roadmap: docs/site-polish). Collapses to a
* disclosure menu below `md`. A client leaf per the animate skill's RSC isolation rule; the rest
* of the tree (layout, pages) stays server-rendered.
*/
export function SiteHeader() {
const pathname = usePathname();
const [open, setOpen] = useState(false);
const menuId = useId();

// biome-ignore lint/correctness/useExhaustiveDependencies: closes the menu on route change
useEffect(() => setOpen(false), [pathname]);

useEffect(() => {
if (!open) return;
const onKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") setOpen(false);
};
document.addEventListener("keydown", onKeyDown);
return () => document.removeEventListener("keydown", onKeyDown);
}, [open]);

return (
<header className="relative border-b border-line">
<div className="mx-auto flex h-16 max-w-5xl items-center justify-between px-4">
<a href="/" className="flex items-center gap-2 font-semibold tracking-tight">
<svg viewBox="0 0 1024 1024" className="h-6 w-6 fill-accent" aria-hidden="true">
<mask id="header-lock-mask">
<rect width="1024" height="1024" fill="white" />
<rect x="452" y="570" width="120" height="120" rx="18" fill="black" />
</mask>
<g mask="url(#header-lock-mask)">
<rect x="184" y="420" width="656" height="420" rx="72" />
<path
d="M332 430V330A180 180 0 0 1 692 330V430"
fill="none"
stroke="currentColor"
strokeWidth="96"
strokeLinecap="round"
strokeLinejoin="round"
/>
</g>
</svg>
{t("app.name")}
</a>

<nav className="hidden items-center gap-6 md:flex" aria-label={t("app.name")}>
{LINKS.map((link) => (
<a
key={link.href}
href={link.href}
aria-current={pathname === link.href ? "page" : undefined}
className={navLinkClass(pathname === link.href)}
>
{t(link.key)}
</a>
))}
<span className="h-4 w-px bg-line" aria-hidden="true" />
<a
href="https://kinlock-org.github.io"
className="rounded-full border border-line px-4 py-1.5 text-sm font-medium text-ink transition-colors motion-reduce:transition-none hover:border-accent hover:text-accent"
>
{t("nav.docs")}
</a>
<a
href="https://github.com/Kinlock-Org"
className="rounded-full bg-ink/5 px-4 py-1.5 text-sm font-medium text-ink transition-colors motion-reduce:transition-none hover:bg-ink/10"
>
{t("nav.github")}
</a>
</nav>

<button
type="button"
onClick={() => setOpen((v) => !v)}
aria-expanded={open}
aria-controls={menuId}
aria-label={open ? t("nav.closeMenu") : t("nav.openMenu")}
className="flex h-10 w-10 items-center justify-center md:hidden"
>
<span className="relative block h-3.5 w-5" aria-hidden="true">
<span
className={`absolute inset-x-0 top-0 h-0.5 rounded-full bg-ink transition-transform motion-reduce:transition-none ${
open ? "translate-y-[6px] rotate-45" : ""
}`}
/>
<span
className={`absolute inset-x-0 top-1/2 h-0.5 -translate-y-1/2 rounded-full bg-ink transition-opacity motion-reduce:transition-none ${
open ? "opacity-0" : "opacity-100"
}`}
/>
<span
className={`absolute inset-x-0 bottom-0 h-0.5 rounded-full bg-ink transition-transform motion-reduce:transition-none ${
open ? "-translate-y-[6px] -rotate-45" : ""
}`}
/>
</span>
</button>
</div>

{open ? (
<nav
id={menuId}
aria-label={t("app.name")}
className="menu-panel absolute inset-x-0 top-full z-10 flex flex-col gap-1 border-b border-line bg-paper p-4 shadow-sm md:hidden"
>
{LINKS.map((link) => (
<a
key={link.href}
href={link.href}
aria-current={pathname === link.href ? "page" : undefined}
className={`rounded-lg px-3 py-2.5 ${navLinkClass(pathname === link.href)}`}
>
{t(link.key)}
</a>
))}
<div className="mt-2 flex flex-col gap-2 border-t border-line pt-3">
<a
href="https://kinlock-org.github.io"
className="rounded-full border border-line px-4 py-2 text-center text-sm font-medium text-ink"
>
{t("nav.docs")}
</a>
<a
href="https://github.com/Kinlock-Org"
className="rounded-full bg-ink/5 px-4 py-2 text-center text-sm font-medium text-ink"
>
{t("nav.github")}
</a>
</div>
</nav>
) : null}
</header>
);
}
9 changes: 9 additions & 0 deletions messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@
"usdOnlyNote": "No reliable exchange rate is available for this currency, so only the USD amount is shown.",
"rateRiskNote": "Exchange rates change. The local-currency amount is an estimate and is not guaranteed."
},
"nav": {
"send": "Send",
"request": "Request",
"verify": "Verify",
"docs": "Documentation",
"github": "GitHub",
"openMenu": "Open menu",
"closeMenu": "Close menu"
},
"receipt": {
"headline": "Payment to verified payee",
"valid": "Valid",
Expand Down
Loading