mirror of
https://github.com/Such-Software/such.software.git
synced 2026-10-17 05:32:05 +02:00
Merge feat/burst-and-hashbags: handover burst, Hash Bags launch
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 3.0 KiB |
@@ -19,6 +19,18 @@ export const metadata: Metadata = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const apps = [
|
const apps = [
|
||||||
|
{
|
||||||
|
name: "Hash Bags",
|
||||||
|
tagline: "Non-custodial multi-chain wallet. Wownero-first.",
|
||||||
|
icon: "/images/products/hash-bags.png",
|
||||||
|
color: "text-emerald-600 dark:text-emerald-400",
|
||||||
|
borderColor: "border-emerald-500/20",
|
||||||
|
href: "/products/hash-bags",
|
||||||
|
links: [
|
||||||
|
{ platform: "google" as const, href: "https://play.google.com/store/apps/details?id=com.suchsoftware.hashwallet" },
|
||||||
|
{ platform: "apple" as const, href: null },
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "Bloomword",
|
name: "Bloomword",
|
||||||
tagline: "Grow your vocab: a daily word game with a living 3D tree",
|
tagline: "Grow your vocab: a daily word game with a living 3D tree",
|
||||||
|
|||||||
@@ -327,3 +327,40 @@
|
|||||||
html[data-splash="skip"] #splash-overlay {
|
html[data-splash="skip"] #splash-overlay {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Splash handover burst: the four pillar cells disperse as the mark hands over to the page.
|
||||||
|
The blobs sit where the monogram's four cells were (a 2x2 grid about the centre), so this
|
||||||
|
reads as the logo coming apart rather than as a generic flash.
|
||||||
|
|
||||||
|
ONLY transform and opacity animate, both compositor properties. This deliberately avoids
|
||||||
|
animating filter/background: the matrix animation pegged a core doing exactly that, because
|
||||||
|
a filter change re-renders the whole filter graph every frame. Soft edges come from a static
|
||||||
|
radial-gradient that is painted once and then only transformed. */
|
||||||
|
@keyframes splash-burst {
|
||||||
|
from { transform: translate(-50%, -50%) scale(0.35); opacity: var(--burst-peak); }
|
||||||
|
to { transform: translate(-50%, -50%) scale(3.1); opacity: 0; }
|
||||||
|
}
|
||||||
|
.splash-burst-blob {
|
||||||
|
position: absolute;
|
||||||
|
width: 34vmin;
|
||||||
|
height: 34vmin;
|
||||||
|
border-radius: 9999px;
|
||||||
|
opacity: 0;
|
||||||
|
will-change: transform, opacity;
|
||||||
|
animation: splash-burst 420ms cubic-bezier(0.16, 0.84, 0.44, 1) forwards;
|
||||||
|
/* Half-transparent by construction: the gradient's own alpha never reaches 1, and
|
||||||
|
--burst-peak holds the whole layer under half opacity. */
|
||||||
|
--burst-peak: 0.42;
|
||||||
|
}
|
||||||
|
.dark .splash-burst-blob { --burst-peak: 0.5; }
|
||||||
|
.splash-burst-s { background: radial-gradient(circle, rgb(26 92 56 / 0.55), transparent 70%); }
|
||||||
|
.splash-burst-u { background: radial-gradient(circle, rgb(184 54 112 / 0.55), transparent 70%); }
|
||||||
|
.splash-burst-c { background: radial-gradient(circle, rgb(23 106 146 / 0.55), transparent 70%); }
|
||||||
|
.splash-burst-h { background: radial-gradient(circle, rgb(18 133 126 / 0.55), transparent 70%); }
|
||||||
|
.dark .splash-burst-s { background: radial-gradient(circle, rgb(53 146 92 / 0.55), transparent 70%); }
|
||||||
|
.dark .splash-burst-u { background: radial-gradient(circle, rgb(232 104 158 / 0.55), transparent 70%); }
|
||||||
|
.dark .splash-burst-c { background: radial-gradient(circle, rgb(74 173 224 / 0.55), transparent 70%); }
|
||||||
|
.dark .splash-burst-h { background: radial-gradient(circle, rgb(58 212 198 / 0.55), transparent 70%); }
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.splash-burst-blob { display: none; }
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
import { Header } from "@/components/layout/header";
|
||||||
|
import { MobileNav } from "@/components/layout/mobile-nav";
|
||||||
|
import { Card } from "@repo/ui/components/card";
|
||||||
|
import { StoreButtons } from "@/components/store-buttons";
|
||||||
|
import { JsonLd, breadcrumbLd, softwareAppLd } from "@/components/seo/json-ld";
|
||||||
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
import type { Metadata } from "next";
|
||||||
|
|
||||||
|
const DESCRIPTION =
|
||||||
|
"Open-source, non-custodial wallet for Monero, Wownero, Bitcoin, and nine more chains. Wownero-first, with Ledger, Trezor, and BitBox support. Free on Android; iOS and desktop coming soon.";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Hash Bags: Non-Custodial Multi-Chain Crypto Wallet | Such Software",
|
||||||
|
description: DESCRIPTION,
|
||||||
|
alternates: { canonical: "/products/hash-bags" },
|
||||||
|
openGraph: {
|
||||||
|
title: "Hash Bags: Non-Custodial Multi-Chain Crypto Wallet | Such Software",
|
||||||
|
description: DESCRIPTION,
|
||||||
|
type: "website",
|
||||||
|
images: ["/images/branding/OG_banner_v2_light.png"],
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: "summary_large_image",
|
||||||
|
images: ["/images/branding/OG_banner_v2_light.png"],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const chains = [
|
||||||
|
{ name: "Monero", ticker: "XMR" },
|
||||||
|
{ name: "Wownero", ticker: "WOW" },
|
||||||
|
{ name: "Bitcoin", ticker: "BTC" },
|
||||||
|
{ name: "Litecoin", ticker: "LTC, incl. MWEB" },
|
||||||
|
{ name: "Bitcoin Cash", ticker: "BCH" },
|
||||||
|
{ name: "Dogecoin", ticker: "DOGE" },
|
||||||
|
{ name: "Nano", ticker: "XNO" },
|
||||||
|
{ name: "Ethereum", ticker: "ETH + ERC-20s" },
|
||||||
|
{ name: "Polygon", ticker: "POL" },
|
||||||
|
{ name: "Base", ticker: "BASE" },
|
||||||
|
{ name: "Arbitrum", ticker: "ARB" },
|
||||||
|
{ name: "BNB Smart Chain", ticker: "BSC" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function HashBagsPage() {
|
||||||
|
return (
|
||||||
|
<main className="relative min-h-screen flex flex-col items-center bg-background text-foreground">
|
||||||
|
<JsonLd data={breadcrumbLd([
|
||||||
|
{ name: "Home", path: "/" },
|
||||||
|
{ name: "Products", path: "/products" },
|
||||||
|
{ name: "Hash Bags", path: "/products/hash-bags" },
|
||||||
|
])} />
|
||||||
|
<JsonLd data={softwareAppLd({
|
||||||
|
name: "Hash Bags",
|
||||||
|
path: "/products/hash-bags",
|
||||||
|
description: DESCRIPTION,
|
||||||
|
category: "FinanceApplication",
|
||||||
|
operatingSystem: "Android",
|
||||||
|
price: "0",
|
||||||
|
})} />
|
||||||
|
<Header />
|
||||||
|
<div id="main-content" className="z-10 w-full max-w-4xl mx-auto py-20 px-4 pb-24 md:pb-20">
|
||||||
|
<Link href="/products" className="text-sm text-muted-foreground hover:text-foreground mb-8 inline-block">
|
||||||
|
← Back to Products
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-5 mb-6">
|
||||||
|
<Image
|
||||||
|
src="/images/products/hash-bags.png"
|
||||||
|
alt="Hash Bags icon"
|
||||||
|
width={80}
|
||||||
|
height={80}
|
||||||
|
className="rounded-[22%]"
|
||||||
|
/>
|
||||||
|
<h1 className="text-4xl md:text-5xl font-bold text-emerald-600 dark:text-emerald-400">
|
||||||
|
Hash Bags
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="prose dark:prose-invert max-w-none mb-12">
|
||||||
|
<p className="text-xl text-muted-foreground leading-relaxed">
|
||||||
|
A wallet for your coins. Hash Bags is an open-source, non-custodial, multi-chain
|
||||||
|
crypto wallet: your keys and your funds stay on your device, and we never hold
|
||||||
|
either. Free on Android today, with iOS and desktop on the way.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-12">
|
||||||
|
<StoreButtons
|
||||||
|
links={[
|
||||||
|
{ platform: "google", href: "https://play.google.com/store/apps/details?id=com.suchsoftware.hashwallet" },
|
||||||
|
{ platform: "apple", href: null },
|
||||||
|
]}
|
||||||
|
extraLinks={[{ href: "https://hash.boats", label: "hash.boats" }]}
|
||||||
|
/>
|
||||||
|
<p className="mt-4 text-sm text-muted-foreground">
|
||||||
|
Live on Google Play. The iOS build is in review, and macOS, Linux, and Windows
|
||||||
|
follow after that.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section className="mb-12">
|
||||||
|
<h2 className="text-2xl font-bold mb-4">Why it exists</h2>
|
||||||
|
<div className="prose dark:prose-invert max-w-none">
|
||||||
|
<p className="text-muted-foreground leading-relaxed">
|
||||||
|
Cake Wallet ended Wownero support in early 2026. Hash Bags is a fork that keeps
|
||||||
|
Wownero a first-class mobile experience, maintained by the project's founder,
|
||||||
|
while shipping a slimmer and more focused multi-coin wallet without the chains and
|
||||||
|
integrations we would rather not maintain or recommend.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="mb-12">
|
||||||
|
<h2 className="text-2xl font-bold mb-4">Twelve chains</h2>
|
||||||
|
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
|
||||||
|
{chains.map((c) => (
|
||||||
|
<Card key={c.name} className="glass-card p-4">
|
||||||
|
<div className="font-semibold">{c.name}</div>
|
||||||
|
<div className="text-sm text-muted-foreground">{c.ticker}</div>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="mb-12">
|
||||||
|
<h2 className="text-2xl font-bold mb-4">Hardware wallets</h2>
|
||||||
|
<p className="text-muted-foreground leading-relaxed">
|
||||||
|
Ledger, Trezor, and BitBox are supported, so the keys can stay off the phone
|
||||||
|
entirely if that is how you prefer to hold them.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div className="text-center">
|
||||||
|
<Link href="/products" className="text-sm text-muted-foreground hover:text-foreground transition-colors">
|
||||||
|
← All products
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<MobileNav />
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -31,6 +31,14 @@ const products: Product[] = [
|
|||||||
icon: "/images/products/custom-websites.svg",
|
icon: "/images/products/custom-websites.svg",
|
||||||
releaseDate: "2025-01-01",
|
releaseDate: "2025-01-01",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
slug: "hash-bags",
|
||||||
|
title: "Hash Bags",
|
||||||
|
description: "Open-source, non-custodial wallet for Monero, Wownero, Bitcoin, and nine more chains. Wownero-first, with Ledger, Trezor, and BitBox support. Android now; iOS and desktop coming.",
|
||||||
|
color: "emerald",
|
||||||
|
icon: "/images/products/hash-bags.png",
|
||||||
|
releaseDate: "2026-08-17",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
slug: "smirk-wallet",
|
slug: "smirk-wallet",
|
||||||
title: "Smirk Wallet",
|
title: "Smirk Wallet",
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export default function sitemap(): MetadataRoute.Sitemap {
|
|||||||
{ url: `${base}/products/neroswap`, lastModified: updated, changeFrequency: "monthly", priority: 0.8 },
|
{ url: `${base}/products/neroswap`, lastModified: updated, changeFrequency: "monthly", priority: 0.8 },
|
||||||
{ url: `${base}/products/webshops`, lastModified: updated, changeFrequency: "monthly", priority: 0.8 },
|
{ url: `${base}/products/webshops`, lastModified: updated, changeFrequency: "monthly", priority: 0.8 },
|
||||||
{ url: `${base}/products/custom-websites`, lastModified: updated, changeFrequency: "monthly", priority: 0.8 },
|
{ url: `${base}/products/custom-websites`, lastModified: updated, changeFrequency: "monthly", priority: 0.8 },
|
||||||
|
{ url: `${base}/products/hash-bags`, lastModified: updated, changeFrequency: "weekly", priority: 0.8 },
|
||||||
{ url: `${base}/products/smirk-wallet`, lastModified: updated, changeFrequency: "monthly", priority: 0.8 },
|
{ url: `${base}/products/smirk-wallet`, lastModified: updated, changeFrequency: "monthly", priority: 0.8 },
|
||||||
{ url: `${base}/products/bauhaus-echo`, lastModified: updated, changeFrequency: "monthly", priority: 0.8 },
|
{ url: `${base}/products/bauhaus-echo`, lastModified: updated, changeFrequency: "monthly", priority: 0.8 },
|
||||||
{ url: `${base}/products/vegan-iq`, lastModified: updated, changeFrequency: "monthly", priority: 0.8 },
|
{ url: `${base}/products/vegan-iq`, lastModified: updated, changeFrequency: "monthly", priority: 0.8 },
|
||||||
|
|||||||
@@ -12,6 +12,16 @@ const SPLASH_MS = 1337;
|
|||||||
// gate stacked on the first.
|
// gate stacked on the first.
|
||||||
const FADE_MS = 260;
|
const FADE_MS = 260;
|
||||||
|
|
||||||
|
// The four monogram cells, in the 2x2 grid the mark ends on: S and C in the left column,
|
||||||
|
// U and H in the right. The burst throws one blob out of each, in that letter's own colour,
|
||||||
|
// so the handover reads as the logo dispersing rather than as an unrelated flash.
|
||||||
|
const BURST_CELLS = [
|
||||||
|
{ key: "s", left: "44%", top: "44%" },
|
||||||
|
{ key: "u", left: "56%", top: "44%" },
|
||||||
|
{ key: "c", left: "44%", top: "56%" },
|
||||||
|
{ key: "h", left: "56%", top: "56%" },
|
||||||
|
] as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* First-load splash: the pillar signature.
|
* First-load splash: the pillar signature.
|
||||||
*
|
*
|
||||||
@@ -77,30 +87,55 @@ export function HeroSplash({ onEnter, leaving }: { onEnter: () => void; leaving?
|
|||||||
};
|
};
|
||||||
}, [leaving]);
|
}, [leaving]);
|
||||||
|
|
||||||
|
const fade = { transitionDuration: `${FADE_MS}ms`, opacity: fadeOut ? 0 : 1 };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
id="splash-overlay"
|
id="splash-overlay"
|
||||||
role="presentation"
|
role="presentation"
|
||||||
className={`fixed inset-0 z-50 flex items-center justify-center overflow-hidden bg-background ${
|
className={`fixed inset-0 z-50 flex items-center justify-center overflow-hidden ${
|
||||||
leaving ? `pointer-events-none transition-opacity ${fadeOut ? "opacity-0" : "opacity-100"}` : ""
|
leaving ? "pointer-events-none" : "bg-background"
|
||||||
}`}
|
}`}
|
||||||
style={leaving ? { transitionDuration: `${FADE_MS}ms` } : undefined}
|
|
||||||
>
|
>
|
||||||
{/* The mark is the only content, so it is the LCP candidate: an <img> is a valid
|
{/* The ground and the mark fade on their own, NOT as one container: the burst has to
|
||||||
LCP element where an inline <svg> is not. */}
|
outlive them, and fading the whole overlay would take the burst down with it in
|
||||||
<img
|
the same 260ms. */}
|
||||||
src="/images/animations/morph_splash_light.svg"
|
{leaving && (
|
||||||
alt="Such Software"
|
<div className="absolute inset-0 bg-background transition-opacity" style={fade} />
|
||||||
fetchPriority="high"
|
)}
|
||||||
className="h-auto w-[min(62vw,38vh)] dark:hidden"
|
|
||||||
/>
|
{/* The handover burst: one blob per monogram cell, in that cell's own colour, thrown
|
||||||
<img
|
outward from where the letter was. aria-hidden and pointer-events-none: it is pure
|
||||||
src="/images/animations/morph_splash.svg"
|
decoration over content that is already interactive underneath. */}
|
||||||
alt=""
|
{leaving && (
|
||||||
aria-hidden="true"
|
<div aria-hidden="true" className="pointer-events-none absolute inset-0">
|
||||||
fetchPriority="high"
|
{BURST_CELLS.map((cell, i) => (
|
||||||
className="hidden h-auto w-[min(62vw,38vh)] dark:block"
|
<span
|
||||||
/>
|
key={cell.key}
|
||||||
|
className={`splash-burst-blob splash-burst-${cell.key}`}
|
||||||
|
style={{ left: cell.left, top: cell.top, animationDelay: `${i * 25}ms` }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* The mark is the largest thing on the first screen, so it is the LCP candidate: an
|
||||||
|
<img> is a valid LCP element where an inline <svg> is not. */}
|
||||||
|
<div className={leaving ? "relative transition-opacity" : "relative"} style={leaving ? fade : undefined}>
|
||||||
|
<img
|
||||||
|
src="/images/animations/morph_splash_light.svg"
|
||||||
|
alt="Such Software"
|
||||||
|
fetchPriority="high"
|
||||||
|
className="h-auto w-[min(62vw,38vh)] dark:hidden"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
src="/images/animations/morph_splash.svg"
|
||||||
|
alt=""
|
||||||
|
aria-hidden="true"
|
||||||
|
fetchPriority="high"
|
||||||
|
className="hidden h-auto w-[min(62vw,38vh)] dark:block"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -140,7 +140,8 @@ export function HomeShell({ children }: { children: ReactNode }) {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setSplashUp(false);
|
setSplashUp(false);
|
||||||
tryRelease();
|
tryRelease();
|
||||||
}, 300); // the overlay's 260ms cross-fade, plus a frame
|
}, 540); // the 420ms burst plus its 75ms stagger, not just the 260ms cross-fade:
|
||||||
|
// unmounting on the fade alone cut the blobs off mid-flight
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
Reference in New Issue
Block a user