+
{mode === "login" && (
- <>
-
Sign in
+
+
Sign in
Enter your shop's account to get to your counter.
{info && (
-
+
+
{info}
)}
New shop? Create an account instead
- >
+
)}
{mode === "signup" && (
- <>
-
Set up your shop
+
+
Set up your shop
You'll be the owner — invite your staff once you're in.
Already have an account? Sign in
- >
+
)}
{mode === "confirm" && (
- <>
-
Check your email
+
+
+
+
+
Check your email
Enter the confirmation code we sent to {email}.
-
void resendConfirmationCode(email)}
- className={LINK_CLASS}
- >
+ void resendConfirmationCode(email)} className={LINK_CLASS}>
Resend code
- >
+
)}
{mode === "new_password" && (
- <>
-
Choose a password
+
+
Choose a password
First sign-in on an invited account — set your own password to continue.
- >
+
)}
);
diff --git a/apps/web/src/components/ThemeToggle.tsx b/apps/web/src/components/ThemeToggle.tsx
index dd86824..5c4ca1e 100644
--- a/apps/web/src/components/ThemeToggle.tsx
+++ b/apps/web/src/components/ThemeToggle.tsx
@@ -12,9 +12,11 @@ export function ThemeToggle() {
onClick={toggleTheme}
aria-label={isDark ? "Switch to light mode" : "Switch to dark mode"}
title={isDark ? "Switch to light mode" : "Switch to dark mode"}
- className="flex h-8 w-8 items-center justify-center rounded-full text-slate-500 transition-colors hover:bg-slate-100 hover:text-slate-700 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-200"
+ className="flex h-8 w-8 items-center justify-center rounded-full text-slate-500 transition-all hover:bg-slate-100 hover:text-slate-700 active:scale-90 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-200"
>
- {isDark ?
:
}
+
+ {isDark ? : }
+
);
}
diff --git a/apps/web/src/index.css b/apps/web/src/index.css
index 7cd42da..0c09360 100644
--- a/apps/web/src/index.css
+++ b/apps/web/src/index.css
@@ -4,6 +4,26 @@
body {
@apply bg-slate-50 text-slate-900 transition-colors dark:bg-slate-950 dark:text-slate-100;
+ font-family: "Inter", ui-sans-serif, system-ui, sans-serif;
+ font-feature-settings: "cv11", "ss01";
+}
+
+.font-display {
+ font-family: "Lexend", "Inter", ui-sans-serif, system-ui, sans-serif;
+}
+
+/* Staggered entrance animations — apply alongside animate-fade-in-up */
+.delay-1 { animation-delay: 0.08s; }
+.delay-2 { animation-delay: 0.16s; }
+.delay-3 { animation-delay: 0.24s; }
+.delay-4 { animation-delay: 0.32s; }
+.delay-5 { animation-delay: 0.4s; }
+
+.bg-grid-slate {
+ background-image:
+ linear-gradient(to right, rgb(100 116 139 / 0.07) 1px, transparent 1px),
+ linear-gradient(to bottom, rgb(100 116 139 / 0.07) 1px, transparent 1px);
+ background-size: 32px 32px;
}
@keyframes fadeIn {
diff --git a/apps/web/src/pages/HeroPage.tsx b/apps/web/src/pages/HeroPage.tsx
index f92dd8a..45e01b2 100644
--- a/apps/web/src/pages/HeroPage.tsx
+++ b/apps/web/src/pages/HeroPage.tsx
@@ -1,20 +1,32 @@
-import { BoxIcon, WifiOffIcon, MergeIcon, SparklesIcon, BoltIcon, ArrowRightIcon } from "../components/icons";
+import {
+ Boxes,
+ WifiOff,
+ GitMerge,
+ Sparkles,
+ Zap,
+ ArrowRight,
+ ShieldCheck,
+ ExternalLink,
+ Radio,
+ ScanLine,
+ BadgeCheck,
+} from "lucide-react";
import { AuthPanel } from "../components/AuthPanel";
import { ThemeToggle } from "../components/ThemeToggle";
const STEPS = [
{
- icon: WifiOffIcon,
+ icon: WifiOff,
title: "Two counters go offline",
body: 'Each keeps taking sales locally — no blocked UI, no "connection lost" dead end.',
},
{
- icon: MergeIcon,
+ icon: GitMerge,
title: "They reconnect, in any order",
body: "Vector clocks and a PN-Counter CRDT merge both counters' changes into one mathematically correct state — never a silent overwrite.",
},
{
- icon: SparklesIcon,
+ icon: Sparkles,
title: "Genuine conflicts get a human call",
body: "Only a real same-field disagreement is ever flagged — with an AI-generated, plain-language explanation to help the human decide fast.",
},
@@ -22,25 +34,41 @@ const STEPS = [
const FEATURES = [
{
- icon: WifiOffIcon,
+ icon: WifiOff,
title: "Offline-first",
body: "Full offline queueing via IndexedDB — sell, restock, and edit with zero connection.",
},
{
- icon: MergeIcon,
+ icon: GitMerge,
title: "Conflict-free merge",
body: "Vector clocks + a PN-Counter CRDT guarantee a correct, non-lossy result regardless of arrival order.",
},
{
- icon: BoltIcon,
+ icon: Zap,
title: "Real-time sync",
body: "WebSocket push keeps every open counter live the moment a change lands.",
},
{
- icon: SparklesIcon,
+ icon: Sparkles,
title: "AI-assisted review",
body: "Amazon Bedrock explains genuine conflicts in plain language — advisory only, never the decision-maker.",
},
+ {
+ icon: ShieldCheck,
+ title: "Real multi-tenant auth",
+ body: "Cognito-backed owner/manager/counter-staff roles — every route verifies a signed token, never a client-supplied ID.",
+ },
+ {
+ icon: ScanLine,
+ title: "Barcode quick-entry",
+ body: "Camera-based scan resolves an item instantly, then you explicitly choose sell or restock.",
+ },
+];
+
+const STATS = [
+ { value: "0", label: "sales silently lost" },
+ { value: "100%", label: "orderings proven correct" },
+ { value: "24/7", label: "offline-tolerant" },
];
/**
@@ -50,88 +78,136 @@ const FEATURES = [
*/
export function HeroPage() {
return (
-
-
-
-
-
+
+ {/* Ambient background: soft grid + floating gradient blobs */}
+
+
+
+
+
+
-
-
-
-
+
+
+
Offline-first inventory sync, proven with math, not hope
-
- Inventory that never loses a sale — even offline.
+
+ Inventory that never{" "}
+
+ loses a sale
+
+ {" "}— even offline.
-
+
Two billing counters, one shaky connection. StockSync guarantees that when both go offline and sell the
same item, reconnecting always converges to one correct stock count — never a silent overwrite.
-
+
+
+
+ {STATS.map((stat) => (
+
+
{stat.value}
+
{stat.label}
+
+ ))}
+
-
- How it works
-
+
+
+
+
+ How it works
+
+
+ Three steps, zero lost sales
+
+
+
+
{STEPS.map((step, i) => (
-
-
-
+
+
+
-
Step {i + 1}
-
{step.title}
-
{step.body}
+
Step {i + 1}
+
{step.title}
+
{step.body}
))}
-
-
+
+
+
+
+ Built for the hard 20%
+
+
+ Everything a real deployment needs
+
+
+
{FEATURES.map((feature) => (
-
-
-
{feature.title}
-
{feature.body}
+
+
+
+
+
{feature.title}
+
{feature.body}
))}
-
+
-
+
Built for a hackathon demo — see the README for what's in scope and what's deliberately not.
diff --git a/apps/web/tailwind.config.js b/apps/web/tailwind.config.js
index 55f0484..0797d7e 100644
--- a/apps/web/tailwind.config.js
+++ b/apps/web/tailwind.config.js
@@ -3,7 +3,41 @@ export default {
darkMode: "class",
content: ["./index.html", "./src/**/*.{ts,tsx}"],
theme: {
- extend: {},
+ extend: {
+ fontFamily: {
+ sans: ["Inter", "ui-sans-serif", "system-ui", "sans-serif"],
+ display: ["Lexend", "Inter", "ui-sans-serif", "system-ui", "sans-serif"],
+ },
+ keyframes: {
+ "fade-in-up": {
+ from: { opacity: "0", transform: "translateY(14px)" },
+ to: { opacity: "1", transform: "translateY(0)" },
+ },
+ "fade-in": {
+ from: { opacity: "0" },
+ to: { opacity: "1" },
+ },
+ float: {
+ "0%, 100%": { transform: "translate(0, 0) scale(1)" },
+ "50%": { transform: "translate(2%, -4%) scale(1.05)" },
+ },
+ "float-slow": {
+ "0%, 100%": { transform: "translate(0, 0) scale(1)" },
+ "50%": { transform: "translate(-3%, 3%) scale(1.08)" },
+ },
+ shimmer: {
+ "0%": { backgroundPosition: "-200% 0" },
+ "100%": { backgroundPosition: "200% 0" },
+ },
+ },
+ animation: {
+ "fade-in-up": "fade-in-up 0.7s cubic-bezier(0.16, 1, 0.3, 1) both",
+ "fade-in": "fade-in 0.5s ease-out both",
+ float: "float 9s ease-in-out infinite",
+ "float-slow": "float-slow 12s ease-in-out infinite",
+ shimmer: "shimmer 2.5s linear infinite",
+ },
+ },
},
plugins: [],
};
diff --git a/package-lock.json b/package-lock.json
index 9a378d2..b416bf8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -68,6 +68,7 @@
"@stocksync/core": "*",
"@zxing/browser": "^0.2.1",
"idb": "^8.0.3",
+ "lucide-react": "^1.47.0",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
@@ -6333,6 +6334,15 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/lucide-react": {
+ "version": "1.47.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.47.0.tgz",
+ "integrity": "sha512-o8C23aXpNQypRY73W7fW02EyvWJinEMXgKeGjFoKym0zj3Q73hD97A1IQps1g8C14HTGsOpZL0Am+xjfgFZAbg==",
+ "license": "ISC",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/lz-string": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",